summaryrefslogtreecommitdiff
path: root/mobile
diff options
context:
space:
mode:
Diffstat (limited to 'mobile')
-rw-r--r--mobile/android/.eslintrc110
-rw-r--r--mobile/android/LICENSE373
-rw-r--r--mobile/android/Makefile.in11
-rw-r--r--mobile/android/app.mozbuild17
-rw-r--r--mobile/android/app/assets/example_asset.txt1
-rw-r--r--mobile/android/app/assets/parental_controls_theme.pngbin0 -> 2915 bytes
-rw-r--r--mobile/android/app/assets/publicsuffixlist8406
-rw-r--r--mobile/android/app/build.gradle406
-rw-r--r--mobile/android/app/checkstyle.xml63
-rw-r--r--mobile/android/app/lint.xml223
-rw-r--r--mobile/android/app/mobile.icobin0 -> 4286 bytes
-rw-r--r--mobile/android/app/mobile.js920
-rw-r--r--mobile/android/app/moz.build30
-rw-r--r--mobile/android/app/omnijar/build.gradle33
-rw-r--r--mobile/android/app/src/androidTest/AndroidManifest.xml61
-rw-r--r--mobile/android/app/src/test/java/org/mozilla/gecko/TestGeckoApplication.java27
-rw-r--r--mobile/android/app/ua-update.json.in15
-rw-r--r--mobile/android/base/AdjustConstants.java.in32
-rw-r--r--mobile/android/base/AndroidManifest.xml.in447
-rw-r--r--mobile/android/base/AppConstants.java.in349
-rw-r--r--mobile/android/base/FennecManifest_permissions.xml.in65
-rw-r--r--mobile/android/base/GcmAndroidManifest_permissions.xml.in4
-rw-r--r--mobile/android/base/GcmAndroidManifest_services.xml.in29
-rw-r--r--mobile/android/base/Makefile.in594
-rw-r--r--mobile/android/base/adjust-sdk-sandbox.token1
-rw-r--r--mobile/android/base/adjust_sdk_app_token.in3
-rw-r--r--mobile/android/base/aidl/org/mozilla/gecko/media/FormatParam.aidl7
-rw-r--r--mobile/android/base/aidl/org/mozilla/gecko/media/ICodec.aidl26
-rw-r--r--mobile/android/base/aidl/org/mozilla/gecko/media/ICodecCallbacks.aidl16
-rw-r--r--mobile/android/base/aidl/org/mozilla/gecko/media/IMediaDrmBridge.aidl25
-rw-r--r--mobile/android/base/aidl/org/mozilla/gecko/media/IMediaDrmBridgeCallbacks.aidl31
-rw-r--r--mobile/android/base/aidl/org/mozilla/gecko/media/IMediaManager.aidl18
-rw-r--r--mobile/android/base/aidl/org/mozilla/gecko/media/Sample.aidl7
-rw-r--r--mobile/android/base/aidl/org/mozilla/gecko/media/SessionKeyInfo.aidl7
-rw-r--r--mobile/android/base/android-services.mozbuild1084
-rw-r--r--mobile/android/base/crashreporter/res/drawable-mdpi/crash_reporter.pngbin0 -> 2833 bytes
-rw-r--r--mobile/android/base/crashreporter/res/drawable/textbox_bg.xml22
-rw-r--r--mobile/android/base/crashreporter/res/layout/crash_reporter.xml126
-rw-r--r--mobile/android/base/crashreporter/res/values/colors.xml13
-rw-r--r--mobile/android/base/crashreporter/res/values/styles.xml15
-rw-r--r--mobile/android/base/geckoview.ddf75
-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.java593
-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.java1520
-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.java460
-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
-rw-r--r--mobile/android/base/locales/Makefile.in114
-rw-r--r--mobile/android/base/locales/en-US/android_strings.dtd848
-rw-r--r--mobile/android/base/locales/en-US/search_strings.dtd28
-rw-r--r--mobile/android/base/locales/en-US/sync_strings.dtd126
-rw-r--r--mobile/android/base/locales/moz.build8
-rw-r--r--mobile/android/base/moz.build1147
-rw-r--r--mobile/android/base/package-name.txt.in2
-rw-r--r--mobile/android/base/resources/anim/grow_fade_in.xml21
-rw-r--r--mobile/android/base/resources/anim/overlay_check_entry.xml10
-rw-r--r--mobile/android/base/resources/anim/overlay_check_exit.xml11
-rw-r--r--mobile/android/base/resources/anim/overlay_pop.xml25
-rw-r--r--mobile/android/base/resources/anim/overlay_slide_down.xml10
-rw-r--r--mobile/android/base/resources/anim/overlay_slide_up.xml9
-rw-r--r--mobile/android/base/resources/anim/popup_hide.xml16
-rw-r--r--mobile/android/base/resources/anim/popup_show.xml16
-rw-r--r--mobile/android/base/resources/color/action_bar_menu_item_colors.xml26
-rw-r--r--mobile/android/base/resources/color/action_bar_secondary_menu_item_colors.xml15
-rw-r--r--mobile/android/base/resources/color/facet_button_text_color.xml9
-rw-r--r--mobile/android/base/resources/color/pressed_about_page_header_grey.xml12
-rw-r--r--mobile/android/base/resources/color/primary_text.xml7
-rw-r--r--mobile/android/base/resources/color/primary_text_selector.xml7
-rw-r--r--mobile/android/base/resources/color/recyclerview_selector.xml12
-rw-r--r--mobile/android/base/resources/color/secondary_text.xml7
-rw-r--r--mobile/android/base/resources/color/select_item_multichoice.xml7
-rw-r--r--mobile/android/base/resources/color/state_pressed_toolbar_grey_pressed.xml12
-rw-r--r--mobile/android/base/resources/color/tab_item_title.xml12
-rw-r--r--mobile/android/base/resources/color/tab_new_tab_strip_colors.xml14
-rw-r--r--mobile/android/base/resources/color/tab_strip_item_bg.xml48
-rw-r--r--mobile/android/base/resources/color/tab_strip_item_title.xml18
-rw-r--r--mobile/android/base/resources/color/tab_text_color.xml7
-rw-r--r--mobile/android/base/resources/color/tabs_counter_text_color.xml14
-rw-r--r--mobile/android/base/resources/color/tertiary_text.xml7
-rw-r--r--mobile/android/base/resources/color/toolbar_display_layout_bg.xml11
-rw-r--r--mobile/android/base/resources/color/top_sites_grid_item_title.xml14
-rw-r--r--mobile/android/base/resources/color/url_bar_title.xml21
-rw-r--r--mobile/android/base/resources/color/url_bar_title_hint.xml15
-rw-r--r--mobile/android/base/resources/drawable-hdpi-v11/alert_camera.pngbin0 -> 181 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi-v11/alert_download.pngbin0 -> 273 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi-v11/alert_guest.pngbin0 -> 761 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi-v11/alert_mic.pngbin0 -> 327 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi-v11/alert_mic_camera.pngbin0 -> 242 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi-v11/ic_menu_back.pngbin0 -> 292 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi-v11/ic_menu_bookmark_add.pngbin0 -> 1405 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi-v11/ic_menu_forward.pngbin0 -> 291 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi-v11/ic_menu_reload.pngbin0 -> 663 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi-v11/ic_status_logo.pngbin0 -> 528 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi-v11/star_blue.pngbin0 -> 1050 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/ab_add_search_engine.pngbin0 -> 593 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/ab_copy.pngbin0 -> 148 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/ab_cut.pngbin0 -> 508 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/ab_done.pngbin0 -> 379 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/ab_mic.pngbin0 -> 363 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/ab_paste.pngbin0 -> 253 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/ab_qrcode.pngbin0 -> 169 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/ab_search.pngbin0 -> 799 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/ab_select_all.pngbin0 -> 143 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/alert_camera.pngbin0 -> 206 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/alert_download.pngbin0 -> 218 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/alert_download_animation_1.pngbin0 -> 266 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/alert_download_animation_2.pngbin0 -> 267 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/alert_download_animation_3.pngbin0 -> 268 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/alert_download_animation_4.pngbin0 -> 267 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/alert_download_animation_5.pngbin0 -> 270 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/alert_download_animation_6.pngbin0 -> 270 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/alert_guest.pngbin0 -> 878 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/alert_mic.pngbin0 -> 278 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/alert_mic_camera.pngbin0 -> 247 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/arrow_up.pngbin0 -> 276 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/blank.pngbin0 -> 82 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/casting.pngbin0 -> 244 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/casting_active.pngbin0 -> 302 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/close.pngbin0 -> 296 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/close_edit_mode_dark.pngbin0 -> 234 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/close_edit_mode_light.pngbin0 -> 235 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/color_picker_row_bg.9.pngbin0 -> 121 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/device_desktop.pngbin0 -> 217 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/device_mobile.pngbin0 -> 165 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/dropshadow.9.pngbin0 -> 367 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/favicon_globe.pngbin0 -> 1227 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/find_close.pngbin0 -> 234 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/find_next.pngbin0 -> 176 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/find_prev.pngbin0 -> 167 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/firefox_settings_alert.pngbin0 -> 412 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/flat_icon.pngbin0 -> 620 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/folder_closed.pngbin0 -> 339 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/globe_light.pngbin0 -> 2014 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/grid_icon_bg_activated.9.pngbin0 -> 105 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/grid_icon_bg_focused.9.pngbin0 -> 139 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/handle_end.pngbin0 -> 570 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/handle_middle.pngbin0 -> 692 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/handle_start.pngbin0 -> 581 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/helper_readerview_bookmark.webpbin0 -> 1710 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/home_bg.pngbin0 -> 1645 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/home_group_collapsed.pngbin0 -> 228 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/home_star.pngbin0 -> 3167 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/home_tab_menu_strip.9.pngbin0 -> 88 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/homepage_banner_firstrun.pngbin0 -> 530 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/ic_action_settings.pngbin0 -> 446 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/ic_media_pause.pngbin0 -> 107 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/ic_media_play.pngbin0 -> 230 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/ic_menu_share.pngbin0 -> 494 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/ic_status_logo.pngbin0 -> 537 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/ic_url_bar_tab.pngbin0 -> 195 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/ic_widget_new_tab.pngbin0 -> 173 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/ic_widget_search.pngbin0 -> 396 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/icon_bookmarks_empty.pngbin0 -> 926 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/icon_home_empty_firefox.pngbin0 -> 2579 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/icon_key.pngbin0 -> 922 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/icon_most_recent_empty.pngbin0 -> 1508 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/icon_openinapp.pngbin0 -> 307 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/icon_pageaction.pngbin0 -> 152 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/icon_remote_tabs_empty.pngbin0 -> 742 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/icon_search_empty_firefox.pngbin0 -> 1741 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/icon_shareplane.pngbin0 -> 1449 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/img_check.pngbin0 -> 669 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/location.pngbin0 -> 851 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/lock_disabled.pngbin0 -> 790 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/lock_inactive.pngbin0 -> 535 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/lock_secure.pngbin0 -> 535 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/media_bar_pause.pngbin0 -> 166 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/media_bar_play.pngbin0 -> 336 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/media_bar_stop.pngbin0 -> 401 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/menu.pngbin0 -> 138 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/menu_item_check.pngbin0 -> 502 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/menu_item_more.pngbin0 -> 96 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/menu_item_uncheck.pngbin0 -> 254 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/network_error.pngbin0 -> 1160 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/notification_media.webpbin0 -> 484 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/open_in_browser.pngbin0 -> 194 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/overlay_bookmark_icon.pngbin0 -> 1181 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/overlay_bookmarked_already_icon.pngbin0 -> 578 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/overlay_check.pngbin0 -> 1194 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/pause.pngbin0 -> 231 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/phone.pngbin0 -> 528 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/pin.pngbin0 -> 201 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/play.pngbin0 -> 300 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/private_masq.pngbin0 -> 1660 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/progress.9.pngbin0 -> 343 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/reader.pngbin0 -> 315 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/reader_active.pngbin0 -> 316 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/reading_list_folder.pngbin0 -> 424 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/search_clear.pngbin0 -> 238 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/search_history.pngbin0 -> 419 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/search_icon_active.pngbin0 -> 450 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/search_icon_inactive.pngbin0 -> 456 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/search_launcher.pngbin0 -> 3473 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/search_plus.pngbin0 -> 213 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/settings_notifications.pngbin0 -> 385 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/shareplane.pngbin0 -> 947 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/shield_disabled.pngbin0 -> 1683 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/shield_enabled.pngbin0 -> 1508 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/status_icon_readercache.pngbin0 -> 445 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/suggestedsites_amazon.pngbin0 -> 4253 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/suggestedsites_facebook.pngbin0 -> 603 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/suggestedsites_twitter.pngbin0 -> 1304 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/suggestedsites_wikipedia.pngbin0 -> 2037 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/suggestedsites_youtube.pngbin0 -> 2773 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/switch_button_icon.pngbin0 -> 186 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/tab_audio_playing.pngbin0 -> 290 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/tab_close.pngbin0 -> 178 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/tab_close_active.pngbin0 -> 186 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/tab_indicator_background.9.pngbin0 -> 107 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/tab_indicator_divider.9.pngbin0 -> 75 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/tab_indicator_selected.9.pngbin0 -> 88 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/tab_indicator_selected_focused.9.pngbin0 -> 93 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/tab_new.pngbin0 -> 219 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/tab_preview_masq.pngbin0 -> 1131 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/tabs_count.pngbin0 -> 145 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/tabs_count_foreground.pngbin0 -> 146 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/tabs_normal.pngbin0 -> 216 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/tabs_panel_nav_back.pngbin0 -> 453 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/tabs_private.pngbin0 -> 429 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/tip_addsearch.pngbin0 -> 2606 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/top_site_add.pngbin0 -> 113 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/tracking_protection_toolbar_illustration.pngbin0 -> 2474 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/undo_button_icon.pngbin0 -> 436 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/url_bar_entry_default.9.pngbin0 -> 217 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/url_bar_entry_default_pb.9.pngbin0 -> 252 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/url_bar_entry_pressed.9.pngbin0 -> 252 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/url_bar_entry_pressed_pb.9.pngbin0 -> 259 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/urlbar_stop.pngbin0 -> 276 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/validation_arrow.pngbin0 -> 221 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/validation_arrow_inverted.pngbin0 -> 240 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/validation_bg.9.pngbin0 -> 409 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/warning_major.pngbin0 -> 283 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/warning_minor.pngbin0 -> 283 bytes
-rw-r--r--mobile/android/base/resources/drawable-hdpi/widget_bg.9.pngbin0 -> 354 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_back.pngbin0 -> 471 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_forward.pngbin0 -> 360 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_reload.pngbin0 -> 779 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-hdpi-v11/tabs_count.pngbin0 -> 171 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-hdpi-v11/tabs_count_foreground.pngbin0 -> 144 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-hdpi-v11/toolbar_favicon_default.pngbin0 -> 134 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-hdpi-v11/url_bar_entry_default.9.pngbin0 -> 261 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-hdpi-v11/url_bar_entry_default_pb.9.pngbin0 -> 281 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-hdpi-v11/url_bar_entry_pressed.9.pngbin0 -> 281 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-hdpi-v11/url_bar_entry_pressed_pb.9.pngbin0 -> 281 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-v11/browser_toolbar_action_bar_button.xml77
-rw-r--r--mobile/android/base/resources/drawable-large-v11/url_bar_nav_button.xml36
-rw-r--r--mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_back.pngbin0 -> 598 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_forward.pngbin0 -> 393 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_reload.pngbin0 -> 998 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-xhdpi-v11/tabs_count.pngbin0 -> 196 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-xhdpi-v11/tabs_count_foreground.pngbin0 -> 163 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-xhdpi-v11/toolbar_favicon_default.pngbin0 -> 153 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-xhdpi-v11/url_bar_entry_default.9.pngbin0 -> 351 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-xhdpi-v11/url_bar_entry_default_pb.9.pngbin0 -> 367 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-xhdpi-v11/url_bar_entry_pressed.9.pngbin0 -> 373 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-xhdpi-v11/url_bar_entry_pressed_pb.9.pngbin0 -> 367 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-xxhdpi-v11/ic_menu_back.pngbin0 -> 845 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-xxhdpi-v11/ic_menu_forward.pngbin0 -> 544 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-xxhdpi-v11/ic_menu_reload.pngbin0 -> 1324 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-xxhdpi-v11/tabs_count.pngbin0 -> 232 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-xxhdpi-v11/tabs_count_foreground.pngbin0 -> 196 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-xxhdpi-v11/toolbar_favicon_default.pngbin0 -> 193 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-xxhdpi-v11/url_bar_entry_default.9.pngbin0 -> 497 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-xxhdpi-v11/url_bar_entry_default_pb.9.pngbin0 -> 499 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-xxhdpi-v11/url_bar_entry_pressed.9.pngbin0 -> 497 bytes
-rw-r--r--mobile/android/base/resources/drawable-large-xxhdpi-v11/url_bar_entry_pressed_pb.9.pngbin0 -> 500 bytes
-rw-r--r--mobile/android/base/resources/drawable-nodpi/cloud.pngbin0 -> 2188 bytes
-rw-r--r--mobile/android/base/resources/drawable-nodpi/firstrun_account.pngbin0 -> 8733 bytes
-rw-r--r--mobile/android/base/resources/drawable-nodpi/firstrun_bookmarks.pngbin0 -> 19817 bytes
-rw-r--r--mobile/android/base/resources/drawable-nodpi/firstrun_data_off.pngbin0 -> 18905 bytes
-rw-r--r--mobile/android/base/resources/drawable-nodpi/firstrun_data_on.pngbin0 -> 16918 bytes
-rw-r--r--mobile/android/base/resources/drawable-nodpi/firstrun_readerview.pngbin0 -> 9895 bytes
-rw-r--r--mobile/android/base/resources/drawable-nodpi/firstrun_signin.pngbin0 -> 39900 bytes
-rw-r--r--mobile/android/base/resources/drawable-nodpi/firstrun_sync.pngbin0 -> 16191 bytes
-rw-r--r--mobile/android/base/resources/drawable-nodpi/firstrun_tabqueue_off.pngbin0 -> 8779 bytes
-rw-r--r--mobile/android/base/resources/drawable-nodpi/firstrun_tabqueue_on.pngbin0 -> 9444 bytes
-rw-r--r--mobile/android/base/resources/drawable-nodpi/firstrun_urlbar.pngbin0 -> 12926 bytes
-rwxr-xr-xmobile/android/base/resources/drawable-nodpi/icon_recent.pngbin0 -> 774 bytes
-rw-r--r--mobile/android/base/resources/drawable-v12/toast_button_background.xml27
-rw-r--r--mobile/android/base/resources/drawable-v21/logo.xml15
-rw-r--r--mobile/android/base/resources/drawable-xhdpi-v11/alert_camera.pngbin0 -> 204 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi-v11/alert_download.pngbin0 -> 273 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi-v11/alert_guest.pngbin0 -> 1016 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi-v11/alert_mic.pngbin0 -> 345 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi-v11/alert_mic_camera.pngbin0 -> 268 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_back.pngbin0 -> 361 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_bookmark_add.pngbin0 -> 1910 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_forward.pngbin0 -> 368 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_reload.pngbin0 -> 777 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi-v11/ic_status_logo.pngbin0 -> 728 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi-v11/star_blue.pngbin0 -> 1418 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/ab_add_search_engine.pngbin0 -> 672 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/ab_copy.pngbin0 -> 174 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/ab_cut.pngbin0 -> 602 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/ab_done.pngbin0 -> 501 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/ab_mic.pngbin0 -> 412 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/ab_paste.pngbin0 -> 300 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/ab_qrcode.pngbin0 -> 177 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/ab_search.pngbin0 -> 989 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/ab_select_all.pngbin0 -> 138 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/alert_camera.pngbin0 -> 269 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/alert_download.pngbin0 -> 294 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/alert_download_animation_1.pngbin0 -> 266 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/alert_download_animation_2.pngbin0 -> 267 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/alert_download_animation_3.pngbin0 -> 268 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/alert_download_animation_4.pngbin0 -> 267 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/alert_download_animation_5.pngbin0 -> 270 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/alert_download_animation_6.pngbin0 -> 270 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/alert_guest.pngbin0 -> 289 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/alert_mic.pngbin0 -> 404 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/alert_mic_camera.pngbin0 -> 337 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/arrow_up.pngbin0 -> 247 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/blank.pngbin0 -> 81 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/casting.pngbin0 -> 370 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/casting_active.pngbin0 -> 442 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/close.pngbin0 -> 373 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/close_edit_mode_dark.pngbin0 -> 283 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/close_edit_mode_light.pngbin0 -> 288 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/color_picker_row_bg.9.pngbin0 -> 123 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/device_desktop.pngbin0 -> 246 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/device_mobile.pngbin0 -> 215 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/dropshadow.9.pngbin0 -> 463 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/favicon_globe.pngbin0 -> 1617 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/find_close.pngbin0 -> 293 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/find_next.pngbin0 -> 216 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/find_prev.pngbin0 -> 212 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/firefox_settings_alert.pngbin0 -> 622 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/flat_icon.pngbin0 -> 810 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/folder_closed.pngbin0 -> 445 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/globe_light.pngbin0 -> 2597 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/grid_icon_bg_activated.9.pngbin0 -> 116 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/grid_icon_bg_focused.9.pngbin0 -> 165 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/handle_end.pngbin0 -> 707 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/handle_middle.pngbin0 -> 890 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/handle_start.pngbin0 -> 725 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/helper_readerview_bookmark.webpbin0 -> 2042 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/home_group_collapsed.pngbin0 -> 204 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/home_tab_menu_strip.9.pngbin0 -> 96 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/homepage_banner_firstrun.pngbin0 -> 707 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/ic_action_settings.pngbin0 -> 629 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/ic_media_pause.pngbin0 -> 113 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/ic_media_play.pngbin0 -> 282 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/ic_menu_share.pngbin0 -> 713 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/ic_status_logo.pngbin0 -> 691 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/ic_url_bar_tab.pngbin0 -> 243 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/ic_widget_new_tab.pngbin0 -> 184 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/ic_widget_search.pngbin0 -> 516 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/icon_bookmarks_empty.pngbin0 -> 1131 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/icon_home_empty_firefox.pngbin0 -> 3271 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/icon_key.pngbin0 -> 1212 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/icon_most_recent_empty.pngbin0 -> 1927 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/icon_openinapp.pngbin0 -> 420 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/icon_pageaction.pngbin0 -> 244 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/icon_remote_tabs_empty.pngbin0 -> 841 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/icon_search_empty_firefox.pngbin0 -> 2323 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/icon_shareplane.pngbin0 -> 1823 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/img_check.pngbin0 -> 907 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/location.pngbin0 -> 1019 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/lock_disabled.pngbin0 -> 821 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/lock_inactive.pngbin0 -> 465 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/lock_secure.pngbin0 -> 465 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/media_bar_pause.pngbin0 -> 161 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/media_bar_play.pngbin0 -> 390 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/media_bar_stop.pngbin0 -> 509 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/menu.pngbin0 -> 162 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/menu_item_check.pngbin0 -> 555 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/menu_item_more.pngbin0 -> 111 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/menu_item_uncheck.pngbin0 -> 315 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/network_error.pngbin0 -> 1677 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/notification_media.webpbin0 -> 616 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/open_in_browser.pngbin0 -> 212 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/overlay_bookmark_icon.pngbin0 -> 1513 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/overlay_bookmarked_already_icon.pngbin0 -> 755 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/overlay_check.pngbin0 -> 1586 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/pause.pngbin0 -> 220 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/phone.pngbin0 -> 689 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/pin.pngbin0 -> 227 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/play.pngbin0 -> 244 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/private_masq.pngbin0 -> 2007 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/progress.9.pngbin0 -> 425 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/push_notification.pngbin0 -> 446 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/reader.pngbin0 -> 355 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/reader_active.pngbin0 -> 360 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/reading_list_folder.pngbin0 -> 542 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/search_clear.pngbin0 -> 292 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/search_history.pngbin0 -> 561 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/search_icon_active.pngbin0 -> 575 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/search_icon_inactive.pngbin0 -> 593 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/search_launcher.pngbin0 -> 4931 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/search_plus.pngbin0 -> 231 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/settings_notifications.pngbin0 -> 493 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/shareplane.pngbin0 -> 1215 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/shield_disabled.pngbin0 -> 2222 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/shield_enabled.pngbin0 -> 2097 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/status_icon_readercache.pngbin0 -> 531 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/suggestedsites_amazon.pngbin0 -> 5924 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/suggestedsites_facebook.pngbin0 -> 775 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/suggestedsites_restricted_fxsupport.pngbin0 -> 2269 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/suggestedsites_restricted_mozilla.pngbin0 -> 2324 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/suggestedsites_twitter.pngbin0 -> 1769 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/suggestedsites_webmaker.pngbin0 -> 4712 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/suggestedsites_wikipedia.pngbin0 -> 2874 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/suggestedsites_youtube.pngbin0 -> 3686 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/switch_button_icon.pngbin0 -> 222 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/tab_audio_playing.pngbin0 -> 341 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/tab_close.pngbin0 -> 190 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/tab_close_active.pngbin0 -> 204 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/tab_indicator_background.9.pngbin0 -> 110 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/tab_indicator_divider.9.pngbin0 -> 75 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/tab_indicator_selected.9.pngbin0 -> 97 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/tab_indicator_selected_focused.9.pngbin0 -> 103 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/tab_new.pngbin0 -> 293 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/tab_preview_masq.pngbin0 -> 1460 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/tabs_count.pngbin0 -> 165 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/tabs_count_foreground.pngbin0 -> 172 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/tabs_normal.pngbin0 -> 248 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/tabs_panel_nav_back.pngbin0 -> 589 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/tabs_private.pngbin0 -> 554 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/tip_addsearch.pngbin0 -> 3120 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/top_site_add.pngbin0 -> 118 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/tracking_protection_toolbar_illustration.pngbin0 -> 3450 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/undo_button_icon.pngbin0 -> 552 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/url_bar_entry_default.9.pngbin0 -> 285 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/url_bar_entry_default_pb.9.pngbin0 -> 318 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/url_bar_entry_pressed.9.pngbin0 -> 309 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/url_bar_entry_pressed_pb.9.pngbin0 -> 310 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/urlbar_stop.pngbin0 -> 341 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/validation_arrow.pngbin0 -> 262 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/validation_arrow_inverted.pngbin0 -> 272 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/validation_bg.9.pngbin0 -> 487 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/warning_major.pngbin0 -> 507 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/warning_minor.pngbin0 -> 516 bytes
-rw-r--r--mobile/android/base/resources/drawable-xhdpi/widget_bg.9.pngbin0 -> 1224 bytes
-rw-r--r--mobile/android/base/resources/drawable-xlarge-hdpi-v11/ic_menu_bookmark_add.pngbin0 -> 765 bytes
-rw-r--r--mobile/android/base/resources/drawable-xlarge-hdpi-v11/star_blue.pngbin0 -> 685 bytes
-rw-r--r--mobile/android/base/resources/drawable-xlarge-xhdpi-v11/ic_menu_bookmark_add.pngbin0 -> 958 bytes
-rw-r--r--mobile/android/base/resources/drawable-xlarge-xhdpi-v11/star_blue.pngbin0 -> 794 bytes
-rw-r--r--mobile/android/base/resources/drawable-xlarge-xxhdpi-v11/ic_menu_bookmark_add.pngbin0 -> 1419 bytes
-rw-r--r--mobile/android/base/resources/drawable-xlarge-xxhdpi-v11/star_blue.pngbin0 -> 1193 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi-v11/ic_status_logo.pngbin0 -> 1343 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/ab_mic.pngbin0 -> 587 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/ab_qrcode.pngbin0 -> 230 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/arrow_up.pngbin0 -> 372 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/close_edit_mode_dark.pngbin0 -> 377 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/close_edit_mode_light.pngbin0 -> 377 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/device_desktop.pngbin0 -> 298 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/device_mobile.pngbin0 -> 238 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/dropshadow.9.pngbin0 -> 672 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/exit_fullscreen.pngbin0 -> 842 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/flat_icon.pngbin0 -> 1240 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/folder_closed.pngbin0 -> 635 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/fullscreen.pngbin0 -> 842 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/globe_light.pngbin0 -> 3642 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/helper_readerview_bookmark.webpbin0 -> 3048 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/home_group_collapsed.pngbin0 -> 293 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/homepage_banner_firstrun.pngbin0 -> 985 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/ic_action_settings.pngbin0 -> 818 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/ic_media_pause.pngbin0 -> 135 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/ic_media_play.pngbin0 -> 374 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/ic_menu_share.pngbin0 -> 832 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/ic_widget_new_tab.pngbin0 -> 238 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/ic_widget_search.pngbin0 -> 717 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/icon_key.pngbin0 -> 1789 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/icon_search_empty_firefox.pngbin0 -> 3465 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/icon_shareplane.pngbin0 -> 2610 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/img_check.pngbin0 -> 1276 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/location.pngbin0 -> 1533 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/lock_disabled.pngbin0 -> 1215 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/lock_inactive.pngbin0 -> 777 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/lock_secure.pngbin0 -> 777 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/menu.pngbin0 -> 217 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/menu_item_check.pngbin0 -> 688 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/menu_item_uncheck.pngbin0 -> 391 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/network_error.pngbin0 -> 1944 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/notification_media.webpbin0 -> 974 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/overlay_bookmark_icon.pngbin0 -> 2143 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/overlay_bookmarked_already_icon.pngbin0 -> 1113 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/overlay_check.pngbin0 -> 2144 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/private_masq.pngbin0 -> 2872 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/push_notification.pngbin0 -> 611 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/reading_list_folder.pngbin0 -> 726 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/search_clear.pngbin0 -> 370 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/search_history.pngbin0 -> 781 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/search_icon_active.pngbin0 -> 856 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/search_icon_inactive.pngbin0 -> 858 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/search_launcher.pngbin0 -> 8383 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/search_plus.pngbin0 -> 273 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/shareplane.pngbin0 -> 1774 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/shield_disabled.pngbin0 -> 2926 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/shield_enabled.pngbin0 -> 2789 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/status_icon_readercache.pngbin0 -> 712 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/suggestedsites_amazon.pngbin0 -> 9225 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/suggestedsites_facebook.pngbin0 -> 1152 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/suggestedsites_twitter.pngbin0 -> 2932 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/suggestedsites_wikipedia.pngbin0 -> 4456 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/suggestedsites_youtube.pngbin0 -> 5537 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/tab_close.pngbin0 -> 278 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/tab_close_active.pngbin0 -> 292 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/tab_new.pngbin0 -> 348 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/tab_preview_masq.pngbin0 -> 2075 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/tabs_panel_nav_back.pngbin0 -> 816 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/tracking_protection_toolbar_illustration.pngbin0 -> 5660 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/url_bar_entry_default.9.pngbin0 -> 406 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/url_bar_entry_default_pb.9.pngbin0 -> 418 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/url_bar_entry_pressed.9.pngbin0 -> 425 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/url_bar_entry_pressed_pb.9.pngbin0 -> 423 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/urlbar_stop.pngbin0 -> 318 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/warning_major.pngbin0 -> 408 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxhdpi/warning_minor.pngbin0 -> 407 bytes
-rw-r--r--mobile/android/base/resources/drawable-xxxhdpi/search_launcher.pngbin0 -> 12725 bytes
-rw-r--r--mobile/android/base/resources/drawable/action_bar_button.xml24
-rw-r--r--mobile/android/base/resources/drawable/action_bar_button_inverse.xml23
-rw-r--r--mobile/android/base/resources/drawable/action_bar_button_negative.xml20
-rw-r--r--mobile/android/base/resources/drawable/action_bar_button_positive.xml20
-rw-r--r--mobile/android/base/resources/drawable/alert_download_animation.xml16
-rw-r--r--mobile/android/base/resources/drawable/arrow_down.xml10
-rw-r--r--mobile/android/base/resources/drawable/as_bin.xml9
-rw-r--r--mobile/android/base/resources/drawable/as_bookmark.xml9
-rw-r--r--mobile/android/base/resources/drawable/as_contextmenu_divider.xml9
-rw-r--r--mobile/android/base/resources/drawable/as_copy.xml9
-rw-r--r--mobile/android/base/resources/drawable/as_dimiss.xml9
-rw-r--r--mobile/android/base/resources/drawable/as_home.xml9
-rw-r--r--mobile/android/base/resources/drawable/as_private.xml9
-rw-r--r--mobile/android/base/resources/drawable/as_share.xml9
-rw-r--r--mobile/android/base/resources/drawable/as_tab.xml9
-rw-r--r--mobile/android/base/resources/drawable/autocomplete_list_bg.xml14
-rw-r--r--mobile/android/base/resources/drawable/bookmark_folder_arrow_up.xml14
-rw-r--r--mobile/android/base/resources/drawable/button_background_action_blue_round.xml11
-rw-r--r--mobile/android/base/resources/drawable/button_background_action_orange_round.xml11
-rw-r--r--mobile/android/base/resources/drawable/button_enabled_action_blue_round.xml11
-rw-r--r--mobile/android/base/resources/drawable/button_enabled_action_orange_round.xml11
-rw-r--r--mobile/android/base/resources/drawable/button_pressed_action_blue_round.xml11
-rw-r--r--mobile/android/base/resources/drawable/button_pressed_action_orange_round.xml11
-rw-r--r--mobile/android/base/resources/drawable/close_edit_mode_selector.xml17
-rw-r--r--mobile/android/base/resources/drawable/color_picker_checkmark.xml12
-rw-r--r--mobile/android/base/resources/drawable/divider_vertical.xml12
-rw-r--r--mobile/android/base/resources/drawable/edit_text_default.xml24
-rw-r--r--mobile/android/base/resources/drawable/edit_text_focused.xml25
-rw-r--r--mobile/android/base/resources/drawable/facet_button_background.xml15
-rw-r--r--mobile/android/base/resources/drawable/facet_button_background_default.xml8
-rw-r--r--mobile/android/base/resources/drawable/facet_button_background_pressed.xml8
-rw-r--r--mobile/android/base/resources/drawable/home_banner.xml38
-rw-r--r--mobile/android/base/resources/drawable/home_history_clear_button_bg.xml23
-rw-r--r--mobile/android/base/resources/drawable/home_pager_empty_state.xml16
-rw-r--r--mobile/android/base/resources/drawable/ic_as_bookmarked.xml9
-rw-r--r--mobile/android/base/resources/drawable/ic_as_visited.xml9
-rw-r--r--mobile/android/base/resources/drawable/icon_grid_item_bg.xml28
-rw-r--r--mobile/android/base/resources/drawable/logo.xml9
-rw-r--r--mobile/android/base/resources/drawable/menu_item_action_bar_bg.xml24
-rw-r--r--mobile/android/base/resources/drawable/menu_item_state.xml24
-rw-r--r--mobile/android/base/resources/drawable/overlay_share_bookmark_button.xml12
-rw-r--r--mobile/android/base/resources/drawable/overlay_share_button_background.xml15
-rw-r--r--mobile/android/base/resources/drawable/overlay_share_button_background_first.xml29
-rw-r--r--mobile/android/base/resources/drawable/panel_auth_button.xml38
-rw-r--r--mobile/android/base/resources/drawable/progressbar.xml9
-rw-r--r--mobile/android/base/resources/drawable/push_notification.pngbin0 -> 391 bytes
-rw-r--r--mobile/android/base/resources/drawable/remote_tabs_setup_button_background.xml20
-rw-r--r--mobile/android/base/resources/drawable/search_row_background.xml10
-rw-r--r--mobile/android/base/resources/drawable/search_suggestion_button.xml19
-rw-r--r--mobile/android/base/resources/drawable/search_suggestion_prompt_no.xml20
-rw-r--r--mobile/android/base/resources/drawable/search_suggestion_prompt_yes.xml20
-rw-r--r--mobile/android/base/resources/drawable/shaped_button.xml19
-rw-r--r--mobile/android/base/resources/drawable/site_security_level.xml18
-rw-r--r--mobile/android/base/resources/drawable/site_security_unknown.xml12
-rw-r--r--mobile/android/base/resources/drawable/tab_history_bg.xml27
-rw-r--r--mobile/android/base/resources/drawable/tab_history_icon_state.xml24
-rw-r--r--mobile/android/base/resources/drawable/tab_item_close_button.xml18
-rw-r--r--mobile/android/base/resources/drawable/tab_panel_tab_background.xml38
-rw-r--r--mobile/android/base/resources/drawable/tab_queue_dismiss_button_foreground.xml14
-rw-r--r--mobile/android/base/resources/drawable/tab_row.xml16
-rw-r--r--mobile/android/base/resources/drawable/tab_strip_button.xml45
-rw-r--r--mobile/android/base/resources/drawable/tab_strip_divider.xml18
-rw-r--r--mobile/android/base/resources/drawable/tab_thumbnail.xml87
-rw-r--r--mobile/android/base/resources/drawable/tabs_panel_indicator.xml55
-rw-r--r--mobile/android/base/resources/drawable/tabs_panel_indicator_selected.xml9
-rw-r--r--mobile/android/base/resources/drawable/tabs_panel_indicator_selected_private.xml9
-rw-r--r--mobile/android/base/resources/drawable/tabs_strip_indicator.xml48
-rw-r--r--mobile/android/base/resources/drawable/toast_background.xml10
-rw-r--r--mobile/android/base/resources/drawable/toast_button_background.xml30
-rw-r--r--mobile/android/base/resources/drawable/toolbar_favicon_default.xml7
-rw-r--r--mobile/android/base/resources/drawable/toolbar_grey_round.xml10
-rw-r--r--mobile/android/base/resources/drawable/top_sites_thumbnail_bg.xml12
-rw-r--r--mobile/android/base/resources/drawable/url_bar_bg.xml15
-rw-r--r--mobile/android/base/resources/drawable/url_bar_entry.xml37
-rw-r--r--mobile/android/base/resources/drawable/url_bar_nav_button.xml9
-rw-r--r--mobile/android/base/resources/drawable/url_bar_translating_edge.xml9
-rw-r--r--mobile/android/base/resources/drawable/widget_button_left.xml14
-rw-r--r--mobile/android/base/resources/drawable/widget_button_left_default.xml19
-rw-r--r--mobile/android/base/resources/drawable/widget_button_left_pressed.xml19
-rw-r--r--mobile/android/base/resources/drawable/widget_button_middle.xml13
-rw-r--r--mobile/android/base/resources/drawable/widget_button_middle_pressed.xml15
-rw-r--r--mobile/android/base/resources/drawable/widget_button_right.xml13
-rw-r--r--mobile/android/base/resources/drawable/widget_button_right_pressed.xml19
-rw-r--r--mobile/android/base/resources/layout-large-v11/browser_toolbar.xml153
-rw-r--r--mobile/android/base/resources/layout-large-v11/tabs_counter.xml16
-rw-r--r--mobile/android/base/resources/layout-xlarge-v11/font_size_preference.xml52
-rw-r--r--mobile/android/base/resources/layout/actionbar.xml31
-rw-r--r--mobile/android/base/resources/layout/activity_stream.xml6
-rw-r--r--mobile/android/base/resources/layout/activity_stream_card_history_item.xml123
-rw-r--r--mobile/android/base/resources/layout/activity_stream_contextmenu_bottomsheet.xml78
-rw-r--r--mobile/android/base/resources/layout/activity_stream_contextmenu_popupmenu.xml21
-rw-r--r--mobile/android/base/resources/layout/activity_stream_main_highlightstitle.xml28
-rw-r--r--mobile/android/base/resources/layout/activity_stream_main_toppanel.xml26
-rw-r--r--mobile/android/base/resources/layout/activity_stream_topsites_card.xml61
-rw-r--r--mobile/android/base/resources/layout/activity_stream_topsites_page.xml6
-rw-r--r--mobile/android/base/resources/layout/anchored_popup.xml25
-rw-r--r--mobile/android/base/resources/layout/as_content.xml8
-rw-r--r--mobile/android/base/resources/layout/autocomplete_list.xml11
-rw-r--r--mobile/android/base/resources/layout/autocomplete_list_item.xml14
-rw-r--r--mobile/android/base/resources/layout/basic_color_picker_dialog.xml21
-rw-r--r--mobile/android/base/resources/layout/bookmark_edit.xml51
-rw-r--r--mobile/android/base/resources/layout/bookmark_folder_row.xml13
-rw-r--r--mobile/android/base/resources/layout/bookmark_item_row.xml10
-rw-r--r--mobile/android/base/resources/layout/bookmark_screenshot_row.xml35
-rw-r--r--mobile/android/base/resources/layout/browser_search.xml35
-rw-r--r--mobile/android/base/resources/layout/browser_toolbar.xml112
-rw-r--r--mobile/android/base/resources/layout/button_toast.xml19
-rw-r--r--mobile/android/base/resources/layout/color_picker_row.xml13
-rw-r--r--mobile/android/base/resources/layout/customtabs_activity.xml50
-rw-r--r--mobile/android/base/resources/layout/datetime_picker.xml138
-rw-r--r--mobile/android/base/resources/layout/default_doorhanger.xml34
-rw-r--r--mobile/android/base/resources/layout/doorhanger.xml76
-rw-r--r--mobile/android/base/resources/layout/doorhanger_security.xml31
-rw-r--r--mobile/android/base/resources/layout/find_in_page_content.xml49
-rw-r--r--mobile/android/base/resources/layout/firstrun_animation_container.xml30
-rw-r--r--mobile/android/base/resources/layout/firstrun_basepanel_checkable_fragment.xml58
-rw-r--r--mobile/android/base/resources/layout/firstrun_sync_fragment.xml54
-rw-r--r--mobile/android/base/resources/layout/font_size_preference.xml54
-rw-r--r--mobile/android/base/resources/layout/gecko_app.xml177
-rw-r--r--mobile/android/base/resources/layout/history_sync_setup.xml35
-rw-r--r--mobile/android/base/resources/layout/home_banner.xml15
-rw-r--r--mobile/android/base/resources/layout/home_banner_content.xml36
-rw-r--r--mobile/android/base/resources/layout/home_bookmarks_panel.xml20
-rw-r--r--mobile/android/base/resources/layout/home_combined_back_item.xml13
-rw-r--r--mobile/android/base/resources/layout/home_combined_history_panel.xml52
-rw-r--r--mobile/android/base/resources/layout/home_empty_panel.xml45
-rw-r--r--mobile/android/base/resources/layout/home_header_row.xml7
-rw-r--r--mobile/android/base/resources/layout/home_item_row.xml10
-rw-r--r--mobile/android/base/resources/layout/home_pager.xml25
-rw-r--r--mobile/android/base/resources/layout/home_remote_tabs_group.xml60
-rw-r--r--mobile/android/base/resources/layout/home_remote_tabs_hidden_devices.xml17
-rw-r--r--mobile/android/base/resources/layout/home_search_item_row.xml12
-rw-r--r--mobile/android/base/resources/layout/home_smartfolder.xml50
-rw-r--r--mobile/android/base/resources/layout/home_suggestion_prompt.xml56
-rw-r--r--mobile/android/base/resources/layout/home_top_sites_panel.xml10
-rw-r--r--mobile/android/base/resources/layout/icon_grid.xml10
-rw-r--r--mobile/android/base/resources/layout/icon_grid_item.xml55
-rw-r--r--mobile/android/base/resources/layout/list_item_header.xml14
-rw-r--r--mobile/android/base/resources/layout/login_doorhanger.xml17
-rw-r--r--mobile/android/base/resources/layout/login_edit_dialog.xml31
-rw-r--r--mobile/android/base/resources/layout/media_casting.xml41
-rw-r--r--mobile/android/base/resources/layout/menu_action_bar.xml15
-rw-r--r--mobile/android/base/resources/layout/menu_item_switcher_layout.xml32
-rw-r--r--mobile/android/base/resources/layout/menu_popup.xml18
-rw-r--r--mobile/android/base/resources/layout/menu_secondary_action_bar.xml14
-rw-r--r--mobile/android/base/resources/layout/overlay_share_button.xml24
-rw-r--r--mobile/android/base/resources/layout/overlay_share_dialog.xml83
-rw-r--r--mobile/android/base/resources/layout/overlay_share_send_tab_item.xml10
-rw-r--r--mobile/android/base/resources/layout/panel_article_item.xml35
-rw-r--r--mobile/android/base/resources/layout/panel_auth_layout.xml39
-rw-r--r--mobile/android/base/resources/layout/panel_back_item.xml26
-rw-r--r--mobile/android/base/resources/layout/panel_icon_item.xml36
-rw-r--r--mobile/android/base/resources/layout/panel_image_item.xml44
-rw-r--r--mobile/android/base/resources/layout/panel_item_container.xml8
-rw-r--r--mobile/android/base/resources/layout/pin_site_dialog.xml43
-rw-r--r--mobile/android/base/resources/layout/preference_checkbox.xml24
-rw-r--r--mobile/android/base/resources/layout/preference_panels.xml30
-rw-r--r--mobile/android/base/resources/layout/preference_rightalign_icon.xml43
-rw-r--r--mobile/android/base/resources/layout/preference_search_engine.xml48
-rw-r--r--mobile/android/base/resources/layout/preference_search_tip.xml34
-rw-r--r--mobile/android/base/resources/layout/preference_set_homepage.xml38
-rw-r--r--mobile/android/base/resources/layout/private_tabs_panel.xml25
-rw-r--r--mobile/android/base/resources/layout/restricted_firstrun_welcome_fragment.xml73
-rw-r--r--mobile/android/base/resources/layout/search_activity_main.xml65
-rw-r--r--mobile/android/base/resources/layout/search_bar.xml43
-rw-r--r--mobile/android/base/resources/layout/search_empty.xml51
-rw-r--r--mobile/android/base/resources/layout/search_engine_bar_item.xml32
-rw-r--r--mobile/android/base/resources/layout/search_engine_bar_label.xml23
-rw-r--r--mobile/android/base/resources/layout/search_engine_row.xml30
-rw-r--r--mobile/android/base/resources/layout/search_fragment_post_search.xml30
-rw-r--r--mobile/android/base/resources/layout/search_fragment_pre_search.xml27
-rw-r--r--mobile/android/base/resources/layout/search_history_row.xml14
-rw-r--r--mobile/android/base/resources/layout/search_sugestions.xml12
-rw-r--r--mobile/android/base/resources/layout/search_suggestions_row.xml31
-rw-r--r--mobile/android/base/resources/layout/search_widget.xml67
-rw-r--r--mobile/android/base/resources/layout/select_dialog_list.xml13
-rw-r--r--mobile/android/base/resources/layout/select_dialog_multichoice.xml22
-rw-r--r--mobile/android/base/resources/layout/select_dialog_singlechoice.xml24
-rw-r--r--mobile/android/base/resources/layout/site_identity.xml101
-rw-r--r--mobile/android/base/resources/layout/site_setting_item.xml53
-rw-r--r--mobile/android/base/resources/layout/suggestion_item.xml31
-rw-r--r--mobile/android/base/resources/layout/tab_history_item_row.xml58
-rw-r--r--mobile/android/base/resources/layout/tab_history_layout.xml17
-rw-r--r--mobile/android/base/resources/layout/tab_menu_strip.xml15
-rw-r--r--mobile/android/base/resources/layout/tab_prompt_input.xml32
-rw-r--r--mobile/android/base/resources/layout/tab_queue_prompt.xml142
-rw-r--r--mobile/android/base/resources/layout/tab_queue_toast.xml38
-rw-r--r--mobile/android/base/resources/layout/tab_strip.xml10
-rw-r--r--mobile/android/base/resources/layout/tab_strip_inner.xml28
-rw-r--r--mobile/android/base/resources/layout/tab_strip_item.xml13
-rw-r--r--mobile/android/base/resources/layout/tab_strip_item_view.xml42
-rw-r--r--mobile/android/base/resources/layout/tabs_counter.xml17
-rw-r--r--mobile/android/base/resources/layout/tabs_layout_item_view.xml71
-rw-r--r--mobile/android/base/resources/layout/tabs_list_item_view.xml68
-rw-r--r--mobile/android/base/resources/layout/tabs_panel_default.xml100
-rw-r--r--mobile/android/base/resources/layout/tabs_panel_indicator.xml13
-rw-r--r--mobile/android/base/resources/layout/tabs_panel_view.xml14
-rw-r--r--mobile/android/base/resources/layout/toolbar_display_layout.xml52
-rw-r--r--mobile/android/base/resources/layout/toolbar_edit_layout.xml49
-rw-r--r--mobile/android/base/resources/layout/top_sites_grid_item_view.xml25
-rw-r--r--mobile/android/base/resources/layout/tracking_protection_prompt.xml106
-rw-r--r--mobile/android/base/resources/layout/two_line_folder_row.xml43
-rw-r--r--mobile/android/base/resources/layout/two_line_page_row.xml51
-rw-r--r--mobile/android/base/resources/layout/validation_message.xml42
-rw-r--r--mobile/android/base/resources/layout/zoomed_view.xml51
-rw-r--r--mobile/android/base/resources/menu-large/browser_app_menu.xml116
-rw-r--r--mobile/android/base/resources/menu-v11/preferences_search_menu.xml11
-rw-r--r--mobile/android/base/resources/menu-v11/tabs_menu.xml20
-rw-r--r--mobile/android/base/resources/menu-v11/titlebar_contextmenu.xml20
-rw-r--r--mobile/android/base/resources/menu-xlarge/browser_app_menu.xml117
-rw-r--r--mobile/android/base/resources/menu/activitystream_contextmenu.xml47
-rw-r--r--mobile/android/base/resources/menu/browser_app_menu.xml116
-rw-r--r--mobile/android/base/resources/menu/browsersearch_contextmenu.xml11
-rw-r--r--mobile/android/base/resources/menu/gecko_app_menu.xml10
-rw-r--r--mobile/android/base/resources/menu/home_contextmenu.xml38
-rw-r--r--mobile/android/base/resources/menu/home_remote_tabs_client_contextmenu.xml11
-rw-r--r--mobile/android/base/resources/menu/preferences_search_menu.xml10
-rw-r--r--mobile/android/base/resources/menu/tabs_menu.xml20
-rw-r--r--mobile/android/base/resources/menu/titlebar_contextmenu.xml26
-rw-r--r--mobile/android/base/resources/raw/bookmarkdefaults_favicon_addons.pngbin0 -> 1815 bytes
-rw-r--r--mobile/android/base/resources/raw/bookmarkdefaults_favicon_restricted_support.pngbin0 -> 20144 bytes
-rw-r--r--mobile/android/base/resources/raw/bookmarkdefaults_favicon_restricted_webmaker.pngbin0 -> 5455 bytes
-rw-r--r--mobile/android/base/resources/raw/bookmarkdefaults_favicon_support.pngbin0 -> 19290 bytes
-rw-r--r--mobile/android/base/resources/raw/fake_home_items.json15
-rw-r--r--mobile/android/base/resources/raw/topdomains.txt455
-rw-r--r--mobile/android/base/resources/values-land/dimens.xml11
-rw-r--r--mobile/android/base/resources/values-land/integers.xml11
-rw-r--r--mobile/android/base/resources/values-land/styles.xml32
-rw-r--r--mobile/android/base/resources/values-large-land-v11/dimens.xml12
-rw-r--r--mobile/android/base/resources/values-large-land-v11/styles.xml34
-rw-r--r--mobile/android/base/resources/values-large-v16/dimens.xml8
-rw-r--r--mobile/android/base/resources/values-large-v16/styles.xml13
-rw-r--r--mobile/android/base/resources/values-large/bool.xml11
-rw-r--r--mobile/android/base/resources/values-large/dimens.xml37
-rw-r--r--mobile/android/base/resources/values-large/integers.xml12
-rw-r--r--mobile/android/base/resources/values-large/styles.xml89
-rw-r--r--mobile/android/base/resources/values-sw240dp/dimens.xml10
-rw-r--r--mobile/android/base/resources/values-sw360dp/dimens.xml13
-rw-r--r--mobile/android/base/resources/values-sw400dp/dimens.xml10
-rw-r--r--mobile/android/base/resources/values-v11/dimens.xml13
-rw-r--r--mobile/android/base/resources/values-v11/styles.xml112
-rw-r--r--mobile/android/base/resources/values-v11/themes.xml45
-rw-r--r--mobile/android/base/resources/values-v13/search_styles.xml20
-rw-r--r--mobile/android/base/resources/values-v13/styles.xml15
-rw-r--r--mobile/android/base/resources/values-v14/themes.xml27
-rw-r--r--mobile/android/base/resources/values-v16/search_styles.xml19
-rw-r--r--mobile/android/base/resources/values-v16/styles.xml29
-rw-r--r--mobile/android/base/resources/values-v19/dimens.xml8
-rw-r--r--mobile/android/base/resources/values-v19/styles.xml41
-rw-r--r--mobile/android/base/resources/values-v21/dimens.xml8
-rw-r--r--mobile/android/base/resources/values-v21/integers.xml10
-rw-r--r--mobile/android/base/resources/values-v21/styles.xml12
-rw-r--r--mobile/android/base/resources/values-v21/themes.xml40
-rw-r--r--mobile/android/base/resources/values-w400dp/styles.xml12
-rw-r--r--mobile/android/base/resources/values-xlarge-land-v11/dimens.xml10
-rw-r--r--mobile/android/base/resources/values-xlarge-land-v11/styles.xml23
-rw-r--r--mobile/android/base/resources/values-xlarge-v11/dimens.xml11
-rw-r--r--mobile/android/base/resources/values-xlarge-v11/integers.xml13
-rw-r--r--mobile/android/base/resources/values-xlarge-v11/styles.xml27
-rw-r--r--mobile/android/base/resources/values/arrays.xml176
-rw-r--r--mobile/android/base/resources/values/attrs.xml188
-rw-r--r--mobile/android/base/resources/values/bool.xml18
-rw-r--r--mobile/android/base/resources/values/colors.xml148
-rw-r--r--mobile/android/base/resources/values/dimens.xml227
-rw-r--r--mobile/android/base/resources/values/ids.xml23
-rw-r--r--mobile/android/base/resources/values/integers.xml17
-rw-r--r--mobile/android/base/resources/values/search_attrs.xml12
-rw-r--r--mobile/android/base/resources/values/search_colors.xml24
-rw-r--r--mobile/android/base/resources/values/search_dimens.xml30
-rw-r--r--mobile/android/base/resources/values/search_styles.xml40
-rw-r--r--mobile/android/base/resources/values/styles.xml832
-rw-r--r--mobile/android/base/resources/values/themes.xml123
-rw-r--r--mobile/android/base/resources/values/vpi__attrs.xml59
-rw-r--r--mobile/android/base/resources/values/vpi__defaults.xml26
-rw-r--r--mobile/android/base/resources/xml-v11/preference_headers.xml74
-rw-r--r--mobile/android/base/resources/xml-v11/preferences_default_browser_tablet.xml11
-rw-r--r--mobile/android/base/resources/xml-v11/preferences_privacy_clear_tablet.xml16
-rw-r--r--mobile/android/base/resources/xml-v11/preferences_search.xml33
-rw-r--r--mobile/android/base/resources/xml/preference_headers.xml25
-rw-r--r--mobile/android/base/resources/xml/preferences.xml85
-rw-r--r--mobile/android/base/resources/xml/preferences_accessibility.xml35
-rw-r--r--mobile/android/base/resources/xml/preferences_advanced.xml100
-rw-r--r--mobile/android/base/resources/xml/preferences_general.xml37
-rw-r--r--mobile/android/base/resources/xml/preferences_general_tablet.xml43
-rw-r--r--mobile/android/base/resources/xml/preferences_home.xml34
-rw-r--r--mobile/android/base/resources/xml/preferences_locale.xml19
-rw-r--r--mobile/android/base/resources/xml/preferences_notifications.xml16
-rw-r--r--mobile/android/base/resources/xml/preferences_privacy.xml113
-rw-r--r--mobile/android/base/resources/xml/preferences_search.xml40
-rw-r--r--mobile/android/base/resources/xml/preferences_vendor.xml24
-rw-r--r--mobile/android/base/resources/xml/search_preferences.xml9
-rw-r--r--mobile/android/base/resources/xml/search_widget_info.xml12
-rw-r--r--mobile/android/base/resources/xml/searchable.xml7
-rw-r--r--mobile/android/base/strings.xml.in639
-rw-r--r--mobile/android/bouncer/AndroidManifest.xml.in96
-rw-r--r--mobile/android/bouncer/Makefile.in20
-rw-r--r--mobile/android/bouncer/assets/example_asset.txt1
-rw-r--r--mobile/android/bouncer/build.gradle76
-rw-r--r--mobile/android/bouncer/java/org/mozilla/bouncer/BouncerService.java129
-rw-r--r--mobile/android/bouncer/java/org/mozilla/gecko/BrowserApp.java46
-rw-r--r--mobile/android/bouncer/moz.build45
-rw-r--r--mobile/android/bouncer/res/drawable-v21/logo.xml15
-rw-r--r--mobile/android/bouncer/res/drawable/logo.xml9
-rw-r--r--mobile/android/branding/aurora/configure.sh10
-rw-r--r--mobile/android/branding/aurora/content/about.pngbin0 -> 18490 bytes
-rw-r--r--mobile/android/branding/aurora/content/favicon32.pngbin0 -> 2554 bytes
-rw-r--r--mobile/android/branding/aurora/content/favicon64.pngbin0 -> 7298 bytes
-rw-r--r--mobile/android/branding/aurora/content/jar.mn9
-rw-r--r--mobile/android/branding/aurora/content/moz.build7
-rw-r--r--mobile/android/branding/aurora/locales/en-US/brand.dtd7
-rw-r--r--mobile/android/branding/aurora/locales/en-US/brand.properties6
-rw-r--r--mobile/android/branding/aurora/locales/jar.mn11
-rw-r--r--mobile/android/branding/aurora/locales/moz.build7
-rw-r--r--mobile/android/branding/aurora/moz.build7
-rw-r--r--mobile/android/branding/aurora/res/drawable-hdpi/icon.pngbin0 -> 8661 bytes
-rw-r--r--mobile/android/branding/aurora/res/drawable-hdpi/large_icon.pngbin0 -> 23595 bytes
-rw-r--r--mobile/android/branding/aurora/res/drawable-hdpi/launcher_widget.pngbin0 -> 13139 bytes
-rw-r--r--mobile/android/branding/aurora/res/drawable-hdpi/widget_icon.pngbin0 -> 6172 bytes
-rw-r--r--mobile/android/branding/aurora/res/drawable-xhdpi/icon.pngbin0 -> 14754 bytes
-rw-r--r--mobile/android/branding/aurora/res/drawable-xhdpi/large_icon.pngbin0 -> 42771 bytes
-rw-r--r--mobile/android/branding/aurora/res/drawable-xhdpi/launcher_widget.pngbin0 -> 20101 bytes
-rw-r--r--mobile/android/branding/aurora/res/drawable-xhdpi/widget_icon.pngbin0 -> 10097 bytes
-rw-r--r--mobile/android/branding/aurora/res/drawable-xxhdpi/icon.pngbin0 -> 23595 bytes
-rw-r--r--mobile/android/branding/aurora/res/drawable-xxhdpi/large_icon.pngbin0 -> 26247 bytes
-rw-r--r--mobile/android/branding/aurora/res/drawable-xxhdpi/launcher_widget.pngbin0 -> 30058 bytes
-rw-r--r--mobile/android/branding/aurora/res/drawable-xxhdpi/widget_icon.pngbin0 -> 20543 bytes
-rw-r--r--mobile/android/branding/aurora/res/drawable-xxxhdpi/icon.pngbin0 -> 42775 bytes
-rw-r--r--mobile/android/branding/beta/configure.sh10
-rw-r--r--mobile/android/branding/beta/content/about.pngbin0 -> 20047 bytes
-rw-r--r--mobile/android/branding/beta/content/favicon32.pngbin0 -> 2361 bytes
-rw-r--r--mobile/android/branding/beta/content/favicon64.pngbin0 -> 6632 bytes
-rw-r--r--mobile/android/branding/beta/content/jar.mn9
-rw-r--r--mobile/android/branding/beta/content/moz.build7
-rw-r--r--mobile/android/branding/beta/locales/en-US/brand.dtd7
-rw-r--r--mobile/android/branding/beta/locales/en-US/brand.properties6
-rw-r--r--mobile/android/branding/beta/locales/jar.mn11
-rw-r--r--mobile/android/branding/beta/locales/moz.build7
-rw-r--r--mobile/android/branding/beta/moz.build7
-rw-r--r--mobile/android/branding/beta/res/drawable-hdpi/icon.pngbin0 -> 7327 bytes
-rw-r--r--mobile/android/branding/beta/res/drawable-hdpi/large_icon.pngbin0 -> 18707 bytes
-rw-r--r--mobile/android/branding/beta/res/drawable-hdpi/launcher_widget.pngbin0 -> 13836 bytes
-rw-r--r--mobile/android/branding/beta/res/drawable-hdpi/widget_icon.pngbin0 -> 6514 bytes
-rw-r--r--mobile/android/branding/beta/res/drawable-xhdpi/icon.pngbin0 -> 14223 bytes
-rw-r--r--mobile/android/branding/beta/res/drawable-xhdpi/large_icon.pngbin0 -> 36645 bytes
-rw-r--r--mobile/android/branding/beta/res/drawable-xhdpi/launcher_widget.pngbin0 -> 20826 bytes
-rw-r--r--mobile/android/branding/beta/res/drawable-xhdpi/widget_icon.pngbin0 -> 9929 bytes
-rw-r--r--mobile/android/branding/beta/res/drawable-xxhdpi/icon.pngbin0 -> 18707 bytes
-rw-r--r--mobile/android/branding/beta/res/drawable-xxhdpi/large_icon.pngbin0 -> 18461 bytes
-rw-r--r--mobile/android/branding/beta/res/drawable-xxhdpi/launcher_widget.pngbin0 -> 37201 bytes
-rw-r--r--mobile/android/branding/beta/res/drawable-xxhdpi/widget_icon.pngbin0 -> 17359 bytes
-rw-r--r--mobile/android/branding/beta/res/drawable-xxxhdpi/icon.pngbin0 -> 36668 bytes
-rw-r--r--mobile/android/branding/nightly/configure.sh9
-rw-r--r--mobile/android/branding/nightly/content/about.pngbin0 -> 18175 bytes
-rw-r--r--mobile/android/branding/nightly/content/favicon32.pngbin0 -> 2470 bytes
-rw-r--r--mobile/android/branding/nightly/content/favicon64.pngbin0 -> 6981 bytes
-rw-r--r--mobile/android/branding/nightly/content/jar.mn9
-rw-r--r--mobile/android/branding/nightly/content/moz.build7
-rw-r--r--mobile/android/branding/nightly/locales/en-US/brand.dtd7
-rw-r--r--mobile/android/branding/nightly/locales/en-US/brand.properties6
-rw-r--r--mobile/android/branding/nightly/locales/jar.mn11
-rw-r--r--mobile/android/branding/nightly/locales/moz.build7
-rw-r--r--mobile/android/branding/nightly/moz.build7
-rw-r--r--mobile/android/branding/nightly/res/drawable-hdpi/icon.pngbin0 -> 8156 bytes
-rw-r--r--mobile/android/branding/nightly/res/drawable-hdpi/large_icon.pngbin0 -> 21673 bytes
-rw-r--r--mobile/android/branding/nightly/res/drawable-hdpi/launcher_widget.pngbin0 -> 13139 bytes
-rw-r--r--mobile/android/branding/nightly/res/drawable-hdpi/widget_icon.pngbin0 -> 3043 bytes
-rw-r--r--mobile/android/branding/nightly/res/drawable-xhdpi/icon.pngbin0 -> 14195 bytes
-rw-r--r--mobile/android/branding/nightly/res/drawable-xhdpi/large_icon.pngbin0 -> 40712 bytes
-rw-r--r--mobile/android/branding/nightly/res/drawable-xhdpi/launcher_widget.pngbin0 -> 20101 bytes
-rw-r--r--mobile/android/branding/nightly/res/drawable-xhdpi/widget_icon.pngbin0 -> 4259 bytes
-rw-r--r--mobile/android/branding/nightly/res/drawable-xxhdpi/icon.pngbin0 -> 21649 bytes
-rw-r--r--mobile/android/branding/nightly/res/drawable-xxhdpi/large_icon.pngbin0 -> 26957 bytes
-rw-r--r--mobile/android/branding/nightly/res/drawable-xxhdpi/launcher_widget.pngbin0 -> 30058 bytes
-rw-r--r--mobile/android/branding/nightly/res/drawable-xxhdpi/widget_icon.pngbin0 -> 6968 bytes
-rw-r--r--mobile/android/branding/nightly/res/drawable-xxxhdpi/icon.pngbin0 -> 40615 bytes
-rw-r--r--mobile/android/branding/official/configure.sh10
-rw-r--r--mobile/android/branding/official/content/about.pngbin0 -> 20047 bytes
-rw-r--r--mobile/android/branding/official/content/favicon32.pngbin0 -> 2361 bytes
-rw-r--r--mobile/android/branding/official/content/favicon64.pngbin0 -> 6628 bytes
-rw-r--r--mobile/android/branding/official/content/jar.mn9
-rw-r--r--mobile/android/branding/official/content/moz.build7
-rw-r--r--mobile/android/branding/official/locales/en-US/brand.dtd7
-rw-r--r--mobile/android/branding/official/locales/en-US/brand.properties6
-rw-r--r--mobile/android/branding/official/locales/jar.mn11
-rw-r--r--mobile/android/branding/official/locales/moz.build7
-rw-r--r--mobile/android/branding/official/moz.build7
-rw-r--r--mobile/android/branding/official/res/drawable-hdpi/icon.pngbin0 -> 7902 bytes
-rw-r--r--mobile/android/branding/official/res/drawable-hdpi/large_icon.pngbin0 -> 21306 bytes
-rw-r--r--mobile/android/branding/official/res/drawable-hdpi/launcher_widget.pngbin0 -> 13305 bytes
-rw-r--r--mobile/android/branding/official/res/drawable-hdpi/widget_icon.pngbin0 -> 5977 bytes
-rw-r--r--mobile/android/branding/official/res/drawable-xhdpi/icon.pngbin0 -> 13891 bytes
-rw-r--r--mobile/android/branding/official/res/drawable-xhdpi/large_icon.pngbin0 -> 37644 bytes
-rw-r--r--mobile/android/branding/official/res/drawable-xhdpi/launcher_widget.pngbin0 -> 20302 bytes
-rw-r--r--mobile/android/branding/official/res/drawable-xhdpi/widget_icon.pngbin0 -> 9193 bytes
-rw-r--r--mobile/android/branding/official/res/drawable-xxhdpi/icon.pngbin0 -> 21306 bytes
-rw-r--r--mobile/android/branding/official/res/drawable-xxhdpi/large_icon.pngbin0 -> 21556 bytes
-rw-r--r--mobile/android/branding/official/res/drawable-xxhdpi/launcher_widget.pngbin0 -> 29503 bytes
-rw-r--r--mobile/android/branding/official/res/drawable-xxhdpi/widget_icon.pngbin0 -> 16737 bytes
-rw-r--r--mobile/android/branding/official/res/drawable-xxxhdpi/icon.pngbin0 -> 37610 bytes
-rw-r--r--mobile/android/branding/unofficial/configure.sh9
-rw-r--r--mobile/android/branding/unofficial/content/about.pngbin0 -> 13042 bytes
-rw-r--r--mobile/android/branding/unofficial/content/favicon32.pngbin0 -> 1558 bytes
-rw-r--r--mobile/android/branding/unofficial/content/favicon64.pngbin0 -> 5677 bytes
-rw-r--r--mobile/android/branding/unofficial/content/jar.mn9
-rw-r--r--mobile/android/branding/unofficial/content/moz.build7
-rw-r--r--mobile/android/branding/unofficial/locales/en-US/brand.dtd7
-rw-r--r--mobile/android/branding/unofficial/locales/en-US/brand.properties6
-rw-r--r--mobile/android/branding/unofficial/locales/jar.mn11
-rw-r--r--mobile/android/branding/unofficial/locales/moz.build7
-rw-r--r--mobile/android/branding/unofficial/moz.build7
-rw-r--r--mobile/android/branding/unofficial/res/drawable-hdpi/icon.pngbin0 -> 6498 bytes
-rw-r--r--mobile/android/branding/unofficial/res/drawable-hdpi/large_icon.pngbin0 -> 17583 bytes
-rw-r--r--mobile/android/branding/unofficial/res/drawable-hdpi/launcher_widget.pngbin0 -> 13139 bytes
-rw-r--r--mobile/android/branding/unofficial/res/drawable-xhdpi/icon.pngbin0 -> 10371 bytes
-rw-r--r--mobile/android/branding/unofficial/res/drawable-xhdpi/large_icon.pngbin0 -> 17583 bytes
-rw-r--r--mobile/android/branding/unofficial/res/drawable-xhdpi/launcher_widget.pngbin0 -> 20101 bytes
-rw-r--r--mobile/android/branding/unofficial/res/drawable-xhdpi/widget_icon.pngbin0 -> 4252 bytes
-rw-r--r--mobile/android/branding/unofficial/res/drawable-xxhdpi/icon.pngbin0 -> 17583 bytes
-rw-r--r--mobile/android/branding/unofficial/res/drawable-xxhdpi/large_icon.pngbin0 -> 17583 bytes
-rw-r--r--mobile/android/branding/unofficial/res/drawable-xxhdpi/launcher_widget.pngbin0 -> 30058 bytes
-rw-r--r--mobile/android/build.mk66
-rw-r--r--mobile/android/build/classycle/LICENSE.txt22
-rw-r--r--mobile/android/build/classycle/classycle-1.4.1.jarbin0 -> 124106 bytes
-rw-r--r--mobile/android/chrome/content/.eslintrc23
-rw-r--r--mobile/android/chrome/content/ActionBarHandler.js731
-rw-r--r--mobile/android/chrome/content/CastingApps.js850
-rw-r--r--mobile/android/chrome/content/ConsoleAPI.js96
-rw-r--r--mobile/android/chrome/content/EmbedRT.js82
-rw-r--r--mobile/android/chrome/content/FeedHandler.js120
-rw-r--r--mobile/android/chrome/content/Feedback.js64
-rw-r--r--mobile/android/chrome/content/FindHelper.js197
-rw-r--r--mobile/android/chrome/content/InputWidgetHelper.js98
-rw-r--r--mobile/android/chrome/content/Linkify.js108
-rw-r--r--mobile/android/chrome/content/MasterPassword.js67
-rw-r--r--mobile/android/chrome/content/MemoryObserver.js88
-rw-r--r--mobile/android/chrome/content/OfflineApps.js77
-rw-r--r--mobile/android/chrome/content/PermissionsHelper.js188
-rw-r--r--mobile/android/chrome/content/PluginHelper.js221
-rw-r--r--mobile/android/chrome/content/PresentationView.js63
-rw-r--r--mobile/android/chrome/content/PresentationView.xul15
-rw-r--r--mobile/android/chrome/content/PrintHelper.js73
-rw-r--r--mobile/android/chrome/content/Reader.js290
-rw-r--r--mobile/android/chrome/content/RemoteDebugger.js355
-rw-r--r--mobile/android/chrome/content/SelectHelper.js161
-rw-r--r--mobile/android/chrome/content/WebcompatReporter.js144
-rw-r--r--mobile/android/chrome/content/WebrtcUI.js302
-rw-r--r--mobile/android/chrome/content/about.js151
-rw-r--r--mobile/android/chrome/content/about.xhtml77
-rw-r--r--mobile/android/chrome/content/aboutAccounts.js351
-rw-r--r--mobile/android/chrome/content/aboutAccounts.xhtml83
-rw-r--r--mobile/android/chrome/content/aboutAddons.js609
-rw-r--r--mobile/android/chrome/content/aboutAddons.xhtml63
-rw-r--r--mobile/android/chrome/content/aboutCertError.xhtml264
-rw-r--r--mobile/android/chrome/content/aboutDownloads.js373
-rw-r--r--mobile/android/chrome/content/aboutDownloads.xhtml62
-rw-r--r--mobile/android/chrome/content/aboutHealthReport.js192
-rw-r--r--mobile/android/chrome/content/aboutHealthReport.xhtml32
-rw-r--r--mobile/android/chrome/content/aboutHome.xhtml22
-rw-r--r--mobile/android/chrome/content/aboutLogins.js518
-rw-r--r--mobile/android/chrome/content/aboutLogins.xhtml90
-rw-r--r--mobile/android/chrome/content/aboutPrivateBrowsing.js32
-rw-r--r--mobile/android/chrome/content/aboutPrivateBrowsing.xhtml43
-rw-r--r--mobile/android/chrome/content/aboutRights.xhtml95
-rw-r--r--mobile/android/chrome/content/bindings/checkbox.xml75
-rw-r--r--mobile/android/chrome/content/bindings/settings.xml66
-rw-r--r--mobile/android/chrome/content/blockedSite.xhtml195
-rw-r--r--mobile/android/chrome/content/browser.css7
-rw-r--r--mobile/android/chrome/content/browser.js6999
-rw-r--r--mobile/android/chrome/content/browser.xul17
-rw-r--r--mobile/android/chrome/content/config.js673
-rw-r--r--mobile/android/chrome/content/config.xhtml86
-rw-r--r--mobile/android/chrome/content/content.js159
-rw-r--r--mobile/android/chrome/content/geckoview.js32
-rw-r--r--mobile/android/chrome/content/geckoview.xul16
-rw-r--r--mobile/android/chrome/content/healthreport-prefs.js6
-rw-r--r--mobile/android/chrome/content/languages.properties114
-rw-r--r--mobile/android/chrome/content/netError.xhtml406
-rw-r--r--mobile/android/chrome/jar.mn71
-rw-r--r--mobile/android/chrome/moz.build13
-rw-r--r--mobile/android/components/AboutRedirector.js132
-rw-r--r--mobile/android/components/AddonUpdateService.js75
-rw-r--r--mobile/android/components/BlocklistPrompt.js61
-rw-r--r--mobile/android/components/BrowserCLH.js47
-rw-r--r--mobile/android/components/ColorPicker.js55
-rw-r--r--mobile/android/components/ContentDispatchChooser.js83
-rw-r--r--mobile/android/components/ContentPermissionPrompt.js146
-rw-r--r--mobile/android/components/DirectoryProvider.js214
-rw-r--r--mobile/android/components/FilePicker.js302
-rw-r--r--mobile/android/components/FxAccountsPush.js164
-rw-r--r--mobile/android/components/HelperAppDialog.js373
-rw-r--r--mobile/android/components/ImageBlockingPolicy.js125
-rw-r--r--mobile/android/components/LoginManagerPrompter.js413
-rw-r--r--mobile/android/components/MobileComponents.manifest126
-rw-r--r--mobile/android/components/NSSDialogService.js276
-rw-r--r--mobile/android/components/PersistentNotificationHandler.js78
-rw-r--r--mobile/android/components/PresentationDevicePrompt.js134
-rw-r--r--mobile/android/components/PresentationRequestUIGlue.js86
-rw-r--r--mobile/android/components/PromptService.js878
-rw-r--r--mobile/android/components/SessionStore.idl86
-rw-r--r--mobile/android/components/SessionStore.js1794
-rw-r--r--mobile/android/components/SiteSpecificUserAgent.js33
-rw-r--r--mobile/android/components/Snippets.js446
-rw-r--r--mobile/android/components/TabSource.js91
-rw-r--r--mobile/android/components/XPIDialogService.js49
-rw-r--r--mobile/android/components/build/moz.build31
-rw-r--r--mobile/android/components/build/nsAndroidHistory.cpp395
-rw-r--r--mobile/android/components/build/nsAndroidHistory.h97
-rw-r--r--mobile/android/components/build/nsBrowserComponents.h7
-rw-r--r--mobile/android/components/build/nsBrowserModule.cpp47
-rw-r--r--mobile/android/components/build/nsIShellService.idl26
-rw-r--r--mobile/android/components/build/nsShellService.cpp30
-rw-r--r--mobile/android/components/build/nsShellService.h29
-rw-r--r--mobile/android/components/extensions/.eslintrc.js5
-rw-r--r--mobile/android/components/extensions/ext-pageAction.js169
-rw-r--r--mobile/android/components/extensions/extensions-mobile.manifest5
-rw-r--r--mobile/android/components/extensions/jar.mn6
-rw-r--r--mobile/android/components/extensions/moz.build16
-rw-r--r--mobile/android/components/extensions/schemas/jar.mn6
-rw-r--r--mobile/android/components/extensions/schemas/moz.build7
-rw-r--r--mobile/android/components/extensions/schemas/page_action.json239
-rw-r--r--mobile/android/components/extensions/test/mochitest/.eslintrc.js10
-rw-r--r--mobile/android/components/extensions/test/mochitest/chrome.ini7
-rw-r--r--mobile/android/components/extensions/test/mochitest/head.js15
-rw-r--r--mobile/android/components/extensions/test/mochitest/mochitest.ini6
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_all_apis.html23
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_pageAction.html99
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_pageAction_popup.html169
-rw-r--r--mobile/android/components/moz.build48
-rwxr-xr-xmobile/android/config/js_wrapper.sh20
-rw-r--r--mobile/android/config/mozconfigs/android-api-15-frontend/nightly43
-rw-r--r--mobile/android/config/mozconfigs/android-api-15-gradle-dependencies/nightly45
-rw-r--r--mobile/android/config/mozconfigs/android-api-15-gradle/nightly23
-rw-r--r--mobile/android/config/mozconfigs/android-api-15/debug16
-rw-r--r--mobile/android/config/mozconfigs/android-api-15/l10n-nightly27
-rw-r--r--mobile/android/config/mozconfigs/android-api-15/l10n-release28
-rw-r--r--mobile/android/config/mozconfigs/android-api-15/nightly18
-rw-r--r--mobile/android/config/mozconfigs/android-api-15/release16
-rw-r--r--mobile/android/config/mozconfigs/android-x86/debug15
-rw-r--r--mobile/android/config/mozconfigs/android-x86/l10n-nightly26
-rw-r--r--mobile/android/config/mozconfigs/android-x86/l10n-release27
-rw-r--r--mobile/android/config/mozconfigs/android-x86/nightly17
-rw-r--r--mobile/android/config/mozconfigs/android-x86/release16
-rw-r--r--mobile/android/config/mozconfigs/common83
-rw-r--r--mobile/android/config/mozconfigs/common.override11
-rw-r--r--mobile/android/config/mozconfigs/public-partner/distribution_sample/mozconfig121
-rw-r--r--mobile/android/config/proguard/adjust-keeps.cfg20
-rw-r--r--mobile/android/config/proguard/appcompat-v7-keeps.cfg11
-rw-r--r--mobile/android/config/proguard/leakcanary-keeps.cfg7
-rw-r--r--mobile/android/config/proguard/play-services-keeps.cfg19
-rw-r--r--mobile/android/config/proguard/proguard-android.cfg78
-rw-r--r--mobile/android/config/proguard/proguard.cfg185
-rw-r--r--mobile/android/config/proguard/strip-libs.cfg41
-rw-r--r--mobile/android/config/tooltool-manifests/android-frontend/releng.manifest57
-rw-r--r--mobile/android/config/tooltool-manifests/android-gradle-dependencies/releng.manifest41
-rw-r--r--mobile/android/config/tooltool-manifests/android-x86/releng.manifest72
-rw-r--r--mobile/android/config/tooltool-manifests/android/releng.manifest90
-rw-r--r--mobile/android/confvars.sh60
-rwxr-xr-xmobile/android/debug_sign_tool.py187
-rw-r--r--mobile/android/docs/Makefile177
-rw-r--r--mobile/android/docs/adjust.rst179
-rw-r--r--mobile/android/docs/bouncer.rst38
-rw-r--r--mobile/android/docs/conf.py258
-rw-r--r--mobile/android/docs/defaultdomains.rst90
-rw-r--r--mobile/android/docs/index.rst26
-rw-r--r--mobile/android/docs/localeswitching.rst97
-rw-r--r--mobile/android/docs/make.bat242
-rw-r--r--mobile/android/docs/shutdown.rst77
-rw-r--r--mobile/android/docs/uitelemetry.rst271
-rw-r--r--mobile/android/extensions/flyweb/bootstrap.js154
-rw-r--r--mobile/android/extensions/flyweb/content/aboutFlyWeb.css29
-rw-r--r--mobile/android/extensions/flyweb/content/aboutFlyWeb.js73
-rw-r--r--mobile/android/extensions/flyweb/content/aboutFlyWeb.xhtml47
-rw-r--r--mobile/android/extensions/flyweb/content/icon-64.pngbin0 -> 1311 bytes
-rw-r--r--mobile/android/extensions/flyweb/install.rdf.in31
-rw-r--r--mobile/android/extensions/flyweb/jar.mn10
-rw-r--r--mobile/android/extensions/flyweb/locale/en-US/aboutFlyWeb.dtd7
-rw-r--r--mobile/android/extensions/flyweb/locale/en-US/flyweb.properties5
-rw-r--r--mobile/android/extensions/flyweb/moz.build15
-rw-r--r--mobile/android/extensions/moz.build11
-rw-r--r--mobile/android/fonts/CharisSILCompact-B.ttfbin0 -> 1676072 bytes
-rw-r--r--mobile/android/fonts/CharisSILCompact-BI.ttfbin0 -> 1667812 bytes
-rw-r--r--mobile/android/fonts/CharisSILCompact-I.ttfbin0 -> 1693988 bytes
-rw-r--r--mobile/android/fonts/CharisSILCompact-R.ttfbin0 -> 1727656 bytes
-rw-r--r--mobile/android/fonts/ClearSans-Bold.ttfbin0 -> 140136 bytes
-rw-r--r--mobile/android/fonts/ClearSans-BoldItalic.ttfbin0 -> 156124 bytes
-rw-r--r--mobile/android/fonts/ClearSans-Italic.ttfbin0 -> 155672 bytes
-rw-r--r--mobile/android/fonts/ClearSans-Light.ttfbin0 -> 145976 bytes
-rw-r--r--mobile/android/fonts/ClearSans-Medium.ttfbin0 -> 148892 bytes
-rw-r--r--mobile/android/fonts/ClearSans-MediumItalic.ttfbin0 -> 155228 bytes
-rw-r--r--mobile/android/fonts/ClearSans-Regular.ttfbin0 -> 142572 bytes
-rw-r--r--mobile/android/fonts/ClearSans-Thin.ttfbin0 -> 147004 bytes
-rw-r--r--mobile/android/fonts/moz.build21
-rw-r--r--mobile/android/geckoview/build.gradle176
-rw-r--r--mobile/android/geckoview/proguard-rules.txt175
-rw-r--r--mobile/android/geckoview/src/main/AndroidManifest.xml39
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/AlarmReceiver.java42
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/AndroidGamepadManager.java425
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/BaseGeckoInterface.java169
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/ContextGetter.java15
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/CrashHandler.java478
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java503
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAccessibility.java410
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java2239
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoBatteryManager.java202
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditable.java1589
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableClient.java33
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableListener.java43
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoHalDefines.java27
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java1060
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java491
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfile.java1002
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfileDirectories.java230
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoScreenOrientation.java423
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSharedPrefs.java318
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java677
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoView.java736
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewChrome.java81
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewContent.java56
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewFragment.java52
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputConnectionListener.java25
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputMethods.java76
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/NSSBridge.java55
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/NotificationListener.java17
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/PrefsHelper.java308
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/SysInfo.java237
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/TouchEventInterceptor.java14
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/JNITarget.java14
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/ReflectionTarget.java18
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/RobocopTarget.java15
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WebRTCJNITarget.java14
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WrapForJNI.java51
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BitmapUtils.java290
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BufferedImage.java94
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BufferedImageGLInfo.java35
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/DynamicToolbarAnimator.java605
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/FloatSize.java54
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/FullScreenState.java12
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java694
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java282
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/IntSize.java89
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerRenderer.java275
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java711
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/NativePanZoomController.java300
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/Overscroll.java21
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/OverscrollEdgeEffect.java162
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomController.java38
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomTarget.java15
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanningPerfAPI.java73
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PointUtils.java51
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ProgressiveUpdateData.java29
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RectUtils.java126
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RenderTask.java80
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/StackScroller.java695
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceTextureListener.java38
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ViewTransform.java28
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/ByteBufferInputStream.java64
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/DirectBufferAllocator.java52
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/GeckoLoader.java549
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/JNIObject.java11
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeReference.java13
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeZip.java84
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/SafeIntent.java134
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionBlock.java133
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/Permissions.java210
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionsHelper.java32
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/ByteBufferInputStream.java38
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/MatrixBlobCursor.java366
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridge.java387
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridgeException.java18
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityResultHandler.java11
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityResultHandlerMap.java24
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityUtils.java72
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/BundleEventListener.java25
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/Clipboard.java117
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ContextUtils.java51
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/DateUtil.java55
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/EventCallback.java29
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/FileUtils.java259
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/FloatUtils.java43
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GamepadUtils.java140
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBackgroundThread.java76
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoEventListener.java14
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoJarReader.java261
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoRequest.java94
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java169
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareUtils.java117
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/INIParser.java176
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/INISection.java123
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IOUtils.java129
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/InputOptionsUtils.java45
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java109
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/JSONUtils.java69
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/MenuUtils.java33
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeEventListener.java23
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeJSContainer.java37
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeJSObject.java533
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NetworkUtils.java177
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NonEvictingLruCache.java44
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/PrefUtils.java70
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ProxySelector.java155
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/RawResource.java52
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/StringUtils.java293
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ThreadUtils.java247
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/UIAsyncTask.java121
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/UUIDUtil.java19
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WeakReferenceHandler.java27
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WindowUtils.java59
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/publicsuffix/PublicSuffix.java121
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/publicsuffix/PublicSuffixPatterns.java117
-rw-r--r--mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/ISelfBrailleService.java147
-rw-r--r--mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/SelfBrailleClient.java267
-rw-r--r--mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/WriteData.java192
-rw-r--r--mobile/android/geckoview_example/build.gradle63
-rw-r--r--mobile/android/geckoview_example/proguard-rules.pro17
-rw-r--r--mobile/android/geckoview_example/src/androidTest/java/org/mozilla/geckoview_example/ApplicationTest.java13
-rw-r--r--mobile/android/geckoview_example/src/androidTest/java/org/mozilla/geckoview_example/GeckoViewActivityTest.java32
-rw-r--r--mobile/android/geckoview_example/src/main/AndroidManifest.xml21
-rw-r--r--mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java148
-rw-r--r--mobile/android/geckoview_example/src/main/res/layout/geckoview_activity.xml13
-rw-r--r--mobile/android/geckoview_example/src/main/res/values/colors.xml6
-rw-r--r--mobile/android/geckoview_example/src/main/res/values/strings.xml3
-rw-r--r--mobile/android/geckoview_example/src/test/java/org/mozilla/geckoview_example/ExampleUnitTest.java15
-rw-r--r--mobile/android/gradle.configure59
-rw-r--r--mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/1.5.0-SNAPSHOT/gradle-plugin-1.5.0-SNAPSHOT-javadoc.jarbin0 -> 62835 bytes
-rw-r--r--mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/1.5.0-SNAPSHOT/gradle-plugin-1.5.0-SNAPSHOT-sources.jarbin0 -> 9106 bytes
-rw-r--r--mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/1.5.0-SNAPSHOT/gradle-plugin-1.5.0-SNAPSHOT.jarbin0 -> 59392 bytes
-rw-r--r--mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/1.5.0-SNAPSHOT/gradle-plugin-1.5.0-SNAPSHOT.pom34
-rw-r--r--mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/1.5.0-SNAPSHOT/maven-metadata-local.xml12
-rw-r--r--mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/maven-metadata-local.xml11
-rw-r--r--mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/1.5.0-SNAPSHOT/maven-metadata-local.xml12
-rw-r--r--mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/1.5.0-SNAPSHOT/sdk-manager-plugin-1.5.0-SNAPSHOT-javadoc.jarbin0 -> 62835 bytes
-rw-r--r--mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/1.5.0-SNAPSHOT/sdk-manager-plugin-1.5.0-SNAPSHOT-sources.jarbin0 -> 9106 bytes
-rw-r--r--mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/1.5.0-SNAPSHOT/sdk-manager-plugin-1.5.0-SNAPSHOT.jarbin0 -> 59392 bytes
-rw-r--r--mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/1.5.0-SNAPSHOT/sdk-manager-plugin-1.5.0-SNAPSHOT.pom57
-rw-r--r--mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/maven-metadata-local.xml11
-rw-r--r--mobile/android/gradle/with_gecko_binaries.gradle105
-rw-r--r--mobile/android/installer/Makefile.in98
-rw-r--r--mobile/android/installer/allowed-dupes.mn157
-rw-r--r--mobile/android/installer/mobile-l10n.js6
-rw-r--r--mobile/android/installer/moz.build6
-rw-r--r--mobile/android/installer/package-manifest.in561
-rw-r--r--mobile/android/installer/removed-files.in4
-rw-r--r--mobile/android/javaaddons/Makefile.in9
-rw-r--r--mobile/android/javaaddons/java/org/mozilla/javaaddons/JavaAddonInterfaceV1.java51
-rw-r--r--mobile/android/javaaddons/moz.build11
-rw-r--r--mobile/android/locales/Makefile.in87
-rw-r--r--mobile/android/locales/all-locales89
-rw-r--r--mobile/android/locales/en-US/chrome/about.dtd24
-rw-r--r--mobile/android/locales/en-US/chrome/aboutAccounts.dtd13
-rw-r--r--mobile/android/locales/en-US/chrome/aboutAccounts.properties16
-rw-r--r--mobile/android/locales/en-US/chrome/aboutAddons.dtd15
-rw-r--r--mobile/android/locales/en-US/chrome/aboutAddons.properties11
-rw-r--r--mobile/android/locales/en-US/chrome/aboutCertError.dtd38
-rw-r--r--mobile/android/locales/en-US/chrome/aboutDevices.dtd14
-rw-r--r--mobile/android/locales/en-US/chrome/aboutDownloads.dtd15
-rw-r--r--mobile/android/locales/en-US/chrome/aboutDownloads.properties17
-rw-r--r--mobile/android/locales/en-US/chrome/aboutHealthReport.dtd6
-rw-r--r--mobile/android/locales/en-US/chrome/aboutHome.dtd7
-rw-r--r--mobile/android/locales/en-US/chrome/aboutHome.properties5
-rw-r--r--mobile/android/locales/en-US/chrome/aboutLogins.dtd10
-rw-r--r--mobile/android/locales/en-US/chrome/aboutLogins.properties27
-rw-r--r--mobile/android/locales/en-US/chrome/aboutPrivateBrowsing.dtd25
-rw-r--r--mobile/android/locales/en-US/chrome/browser.properties462
-rw-r--r--mobile/android/locales/en-US/chrome/checkbox.dtd6
-rw-r--r--mobile/android/locales/en-US/chrome/config.dtd21
-rw-r--r--mobile/android/locales/en-US/chrome/config.properties9
-rw-r--r--mobile/android/locales/en-US/chrome/devicePrompt.properties5
-rw-r--r--mobile/android/locales/en-US/chrome/handling.properties5
-rw-r--r--mobile/android/locales/en-US/chrome/phishing.dtd23
-rw-r--r--mobile/android/locales/en-US/chrome/pippki.properties85
-rw-r--r--mobile/android/locales/en-US/chrome/sync.properties40
-rw-r--r--mobile/android/locales/en-US/chrome/webcompatReporter.properties12
-rw-r--r--mobile/android/locales/en-US/defines.inc12
-rw-r--r--mobile/android/locales/en-US/mobile-l10n.js7
-rw-r--r--mobile/android/locales/filter.py45
-rw-r--r--mobile/android/locales/jar.mn99
-rw-r--r--mobile/android/locales/l10n.ini18
-rw-r--r--mobile/android/locales/maemo-locales77
-rw-r--r--mobile/android/locales/moz.build7
-rw-r--r--mobile/android/mach_commands.py216
-rw-r--r--mobile/android/modules/Accounts.jsm178
-rw-r--r--mobile/android/modules/AndroidLog.jsm92
-rw-r--r--mobile/android/modules/DelayedInit.jsm177
-rw-r--r--mobile/android/modules/DownloadNotifications.jsm291
-rw-r--r--mobile/android/modules/FxAccountsWebChannel.jsm394
-rw-r--r--mobile/android/modules/HelperApps.jsm229
-rw-r--r--mobile/android/modules/Home.jsm487
-rw-r--r--mobile/android/modules/HomeProvider.jsm407
-rw-r--r--mobile/android/modules/JNI.jsm1167
-rw-r--r--mobile/android/modules/JavaAddonManager.jsm115
-rw-r--r--mobile/android/modules/LightweightThemeConsumer.jsm44
-rw-r--r--mobile/android/modules/MediaPlayerApp.jsm166
-rw-r--r--mobile/android/modules/Messaging.jsm183
-rw-r--r--mobile/android/modules/NetErrorHelper.jsm175
-rw-r--r--mobile/android/modules/Notifications.jsm259
-rw-r--r--mobile/android/modules/PageActions.jsm113
-rw-r--r--mobile/android/modules/Prompt.jsm234
-rw-r--r--mobile/android/modules/RuntimePermissions.jsm41
-rw-r--r--mobile/android/modules/SSLExceptions.jsm118
-rw-r--r--mobile/android/modules/Sanitizer.jsm303
-rw-r--r--mobile/android/modules/SharedPreferences.jsm254
-rw-r--r--mobile/android/modules/Snackbars.jsm72
-rw-r--r--mobile/android/modules/TabMirror.jsm153
-rw-r--r--mobile/android/modules/WebsiteMetadata.jsm475
-rw-r--r--mobile/android/modules/dbg-browser-actors.js77
-rw-r--r--mobile/android/modules/moz.build33
-rw-r--r--mobile/android/moz.build36
-rw-r--r--mobile/android/moz.configure86
-rw-r--r--mobile/android/search/java/org/mozilla/search/AcceptsSearchQuery.java48
-rw-r--r--mobile/android/search/java/org/mozilla/search/Constants.java20
-rw-r--r--mobile/android/search/java/org/mozilla/search/PostSearchFragment.java243
-rw-r--r--mobile/android/search/java/org/mozilla/search/PreSearchFragment.java218
-rw-r--r--mobile/android/search/java/org/mozilla/search/SearchActivity.java436
-rw-r--r--mobile/android/search/java/org/mozilla/search/SearchPreferenceActivity.java118
-rw-r--r--mobile/android/search/java/org/mozilla/search/SearchWidget.java135
-rw-r--r--mobile/android/search/java/org/mozilla/search/autocomplete/AutoCompleteAdapter.java82
-rw-r--r--mobile/android/search/java/org/mozilla/search/autocomplete/SearchBar.java201
-rw-r--r--mobile/android/search/java/org/mozilla/search/autocomplete/SuggestionsFragment.java263
-rw-r--r--mobile/android/search/java/org/mozilla/search/ui/BackCaptureEditText.java36
-rw-r--r--mobile/android/search/java/org/mozilla/search/ui/FacetBar.java124
-rw-r--r--mobile/android/search/manifests/SearchAndroidManifest_activities.xml.in61
-rw-r--r--mobile/android/search/manifests/SearchAndroidManifest_permissions.xml.in3
-rw-r--r--mobile/android/search/manifests/SearchAndroidManifest_services.xml.in0
-rw-r--r--mobile/android/search/search_activity_sources.mozbuild20
-rw-r--r--mobile/android/search/strings/search_strings.xml.in20
-rw-r--r--mobile/android/services/README.txt1
-rw-r--r--mobile/android/services/manifests/FxAccountAndroidManifest_activities.xml.in63
-rw-r--r--mobile/android/services/manifests/FxAccountAndroidManifest_permissions.xml.in18
-rw-r--r--mobile/android/services/manifests/FxAccountAndroidManifest_services.xml.in34
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/ReadingListConstants.java23
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/common/EditorBranch.java82
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/common/GlobalConstants.java90
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/common/PrefsBranch.java83
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/Logger.java232
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/AndroidLevelCachingLogWriter.java132
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/AndroidLogWriter.java46
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/LevelFilteringLogWriter.java67
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/LogWriter.java29
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/PrintLogWriter.java77
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/SimpleTagLogWriter.java21
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/StringLogWriter.java57
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/TagLogWriter.java55
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/ThreadLocalTagLogWriter.java25
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/common/telemetry/TelemetryWrapper.java56
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/db/CursorDumper.java99
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/db/Tab.java86
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccount20CreateDelegate.java52
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccount20LoginDelegate.java36
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient.java24
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient20.java914
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClientException.java133
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountRemoteError.java33
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountUtils.java217
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/PasswordStretcher.java12
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/QuickPasswordStretcher.java35
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/SkewHandler.java111
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountAbstractClient.java224
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountAbstractClientException.java68
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountOAuthClient10.java129
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountOAuthRemoteError.java19
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/profile/FxAccountProfileClient10.java59
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/nativecode/NativeCrypto.java60
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/preferences/PreferenceFragment.java326
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/background/preferences/PreferenceManagerCompat.java226
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/browserid/ASNUtils.java82
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/browserid/BrowserIDKeyPair.java35
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/browserid/DSACryptoImplementation.java255
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/browserid/JSONWebTokenUtils.java245
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/browserid/MockMyIDTokenFactory.java128
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/browserid/RSACryptoImplementation.java182
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/browserid/SigningPrivateKey.java41
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/browserid/VerifyingPublicKey.java34
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/AbstractBrowserIDRemoteVerifierClient.java95
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDRemoteVerifierClient10.java62
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDRemoteVerifierClient20.java58
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDVerifierClient.java9
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDVerifierDelegate.java13
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDVerifierException.java41
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/AccountLoader.java227
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FirefoxAccounts.java222
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountConstants.java75
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDevice.java81
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDeviceRegistrator.java282
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountPushHandler.java95
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/SyncStatusListener.java31
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/CustomColorPreference.java52
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountAbstractActivity.java80
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountConfirmAccountActivityWeb.java11
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountFinishMigratingActivityWeb.java11
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountGetStartedActivityWeb.java11
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusActivity.java228
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusFragment.java949
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountUpdateCredentialsActivityWeb.java11
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountWebFlowActivity.java91
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/PicassoPreferenceIconTarget.java63
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AccountPickler.java362
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java929
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxADefaultLoginStateMachineDelegate.java84
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountAuthenticator.java385
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountAuthenticatorService.java55
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountLoginDelegate.java26
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountLoginException.java33
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/BaseRequestDelegate.java49
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Cohabiting.java50
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Doghouse.java25
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Engaged.java91
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/FxAccountLoginStateMachine.java84
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/FxAccountLoginTransition.java68
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Married.java117
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/MigratedFromSync11.java28
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Separated.java25
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/State.java72
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/StateFactory.java206
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/TokensAndKeysState.java45
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountDeletedService.java154
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountUpgradeReceiver.java133
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountNotificationManager.java114
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountProfileService.java107
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSchedulePolicy.java178
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java568
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncDelegate.java110
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncService.java28
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncStatusHelper.java113
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/SchedulePolicy.java43
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/push/RegisterUserAgentResponse.java19
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/push/SubscribeChannelResponse.java19
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClient.java410
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClientException.java81
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/AlreadySyncingException.java22
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/BackoffHandler.java34
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/BadRequiredFieldJSONException.java5
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/CollectionKeys.java199
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/CommandProcessor.java261
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/CommandRunner.java22
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/CredentialException.java56
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/CryptoRecord.java255
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/DelayedWorkTracker.java69
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/EngineSettings.java31
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/ExtendedJSONObject.java426
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/GlobalSession.java1167
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/HTTPFailureException.java47
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/InfoCollections.java103
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/InfoConfiguration.java93
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/InfoCounts.java67
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/JSONRecordFetcher.java145
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/KeyBundleProvider.java11
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobal.java372
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobalException.java45
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobalMissingEnginesException.java9
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobalNotSetException.java9
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/NoCollectionKeysSetException.java16
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/NodeAuthenticationException.java16
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonArrayJSONException.java17
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonObjectJSONException.java17
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/NullClusterURLException.java16
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/PersistedMetaGlobal.java86
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/PrefsBackoffHandler.java59
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/README.txt1
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/Server11PreviousPostFailedException.java12
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/Server11RecordPostFailedException.java12
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/SharedPreferencesClientsDataDelegate.java121
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/Sync11Configuration.java84
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConfiguration.java480
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConfigurationException.java16
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConstants.java20
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncException.java34
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/SynchronizerConfiguration.java68
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/ThreadPool.java15
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/UnexpectedJSONException.java25
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/UnknownSynchronizerConfigurationVersionException.java16
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/Utils.java575
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/CryptoException.java19
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/CryptoInfo.java232
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/HKDF.java128
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/HMACVerificationException.java12
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/KeyBundle.java135
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/MissingCryptoInputException.java9
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/NoKeyBundleException.java9
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/PBKDF2.java78
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/PersistedCrypto5Keys.java103
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/ClientsDataDelegate.java28
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/FreshStartDelegate.java10
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/GlobalSessionCallback.java49
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/JSONRecordFetchDelegate.java19
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/KeyUploadDelegate.java21
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/MetaGlobalDelegate.java15
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/WipeServerDelegate.java10
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/Crypto5MiddlewareRepository.java76
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/Crypto5MiddlewareRepositorySession.java172
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/MiddlewareRepository.java22
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/MiddlewareRepositorySession.java185
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/AbstractBearerTokenAuthHeaderProvider.java34
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/AuthHeaderProvider.java30
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BaseResource.java565
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BaseResourceDelegate.java44
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BasicAuthHeaderProvider.java51
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BearerAuthHeaderProvider.java22
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BrowserIDAuthHeaderProvider.java23
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/ConnectionMonitorThread.java44
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/GzipNonChunkedCompressingEntity.java92
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HMACAuthHeaderProvider.java257
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HandleProgressException.java15
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HawkAuthHeaderProvider.java403
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HttpResponseObserver.java20
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/MozResponse.java225
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/Resource.java20
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/ResourceDelegate.java55
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SRPConstants.java174
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncResponse.java157
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageCollectionRequest.java145
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageCollectionRequestDelegate.java9
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRecordRequest.java95
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRequest.java204
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRequestDelegate.java38
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRequestIncrementalDelegate.java9
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageResponse.java85
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/TLSSocketFactory.java62
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/WBOCollectionRequestDelegate.java35
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/WBORequestDelegate.java14
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/BookmarkNeedsReparentingException.java17
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/BookmarksRepository.java16
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ConstrainedServer11Repository.java51
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/FetchFailedException.java11
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/HashSetStoreTracker.java61
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/HistoryRepository.java16
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/IdentityRecordFactory.java15
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InactiveSessionException.java17
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidBookmarkTypeException.java17
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidRequestException.java16
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidSessionTransitionException.java17
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/MultipleRecordsForGuidException.java16
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoContentProviderException.java25
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoGuidForIdException.java16
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoStoreDelegateException.java11
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NullCursorException.java17
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ParentNotFoundException.java17
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ProfileDatabaseException.java17
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RecordFactory.java13
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RecordFilter.java11
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Repository.java18
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySession.java384
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySessionBundle.java55
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11Repository.java144
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11RepositorySession.java104
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreFailedException.java11
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreTracker.java82
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreTrackingRepositorySession.java102
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksDataAccessor.java326
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksRepository.java25
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksRepositorySession.java1107
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryDataAccessor.java188
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepository.java25
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepositorySession.java208
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepository.java74
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepositoryDataAccessor.java232
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepositorySession.java792
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksDeletionManager.java239
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksInsertionManager.java298
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BrowserContractHelpers.java154
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/CachedSQLiteOpenHelper.java62
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/ClientsDatabase.java252
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/ClientsDatabaseAccessor.java178
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FennecTabsRepository.java383
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FormHistoryRepositorySession.java723
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/PasswordsRepositorySession.java725
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/RepoUtils.java290
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/VisitsHelper.java130
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferrableRepositorySessionCreationDelegate.java41
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionBeginDelegate.java46
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionFetchRecordsDelegate.java56
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionFinishDelegate.java51
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionStoreDelegate.java57
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionBeginDelegate.java23
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionCleanDelegate.java12
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionCreationDelegate.java15
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionFetchRecordsDelegate.java27
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionFinishDelegate.java16
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionGuidsSinceDelegate.java10
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionStoreDelegate.java23
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionWipeDelegate.java13
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/BookmarkRecord.java488
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/BookmarkRecordFactory.java25
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/ClientRecord.java231
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/ClientRecordFactory.java17
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/FormHistoryRecord.java139
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/HistoryRecord.java217
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/HistoryRecordFactory.java25
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/PasswordRecord.java205
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/PasswordRecordFactory.java19
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/Record.java308
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/RecordParseException.java14
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/TabsRecord.java153
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/TabsRecordFactory.java17
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/VersionConstants.java14
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloader.java310
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderDelegate.java91
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchMeta.java165
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploader.java344
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BufferSizeTracker.java103
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/MayUploadProvider.java9
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/Payload.java66
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegate.java185
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/RecordUploadRunnable.java176
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/Constants.java29
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/InvalidSyncKeyException.java9
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/activities/ActivityUtils.java34
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/activities/WebURLFinder.java161
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AbstractNonRepositorySyncStage.java26
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AbstractSessionManagingSyncStage.java43
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AndroidBrowserBookmarksServerSyncStage.java80
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AndroidBrowserHistoryServerSyncStage.java74
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/CheckPreconditionsStage.java13
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/CompletedStage.java16
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/EnsureCrypto5KeysStage.java192
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FennecTabsServerSyncStage.java40
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchInfoCollectionsStage.java44
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchInfoConfigurationStage.java59
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchMetaGlobalStage.java79
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FormHistoryServerSyncStage.java76
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/GlobalSyncStage.java93
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/NoSuchStageException.java13
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/PasswordsServerSyncStage.java38
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SafeConstrainedServer11Repository.java110
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/ServerSyncStage.java627
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SyncClientsEngineStage.java691
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/UploadMetaGlobalStage.java18
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ConcurrentRecordConsumer.java122
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordConsumer.java26
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsChannel.java292
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsChannelDelegate.java13
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsConsumerDelegate.java23
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SerialRecordConsumer.java131
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ServerLocalSynchronizer.java18
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ServerLocalSynchronizerSession.java78
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SessionNotBegunException.java19
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/Synchronizer.java105
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerDelegate.java10
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerSession.java425
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerSessionDelegate.java13
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/UnbundleError.java19
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/UnexpectedSessionException.java26
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/telemetry/TelemetryContract.java56
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerClient.java330
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerClientDelegate.java19
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerException.java89
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerToken.java19
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/util/PRNGFixes.java339
-rw-r--r--mobile/android/services/src/main/res/drawable-hdpi/fxaccount_sync_error.pngbin0 -> 543 bytes
-rw-r--r--mobile/android/services/src/main/res/drawable-hdpi/sync_avatar_default.pngbin0 -> 5146 bytes
-rw-r--r--mobile/android/services/src/main/res/drawable-hdpi/sync_desktop.pngbin0 -> 196 bytes
-rw-r--r--mobile/android/services/src/main/res/drawable-hdpi/sync_desktop_inactive.pngbin0 -> 211 bytes
-rw-r--r--mobile/android/services/src/main/res/drawable-hdpi/sync_mobile.pngbin0 -> 163 bytes
-rw-r--r--mobile/android/services/src/main/res/drawable-hdpi/sync_mobile_inactive.pngbin0 -> 165 bytes
-rw-r--r--mobile/android/services/src/main/res/drawable-hdpi/sync_promo.pngbin0 -> 994 bytes
-rw-r--r--mobile/android/services/src/main/res/drawable-xhdpi/fxaccount_sync_error.pngbin0 -> 716 bytes
-rw-r--r--mobile/android/services/src/main/res/drawable-xhdpi/sync_desktop.pngbin0 -> 229 bytes
-rw-r--r--mobile/android/services/src/main/res/drawable-xhdpi/sync_desktop_inactive.pngbin0 -> 244 bytes
-rw-r--r--mobile/android/services/src/main/res/drawable-xhdpi/sync_mobile.pngbin0 -> 210 bytes
-rw-r--r--mobile/android/services/src/main/res/drawable-xhdpi/sync_mobile_inactive.pngbin0 -> 215 bytes
-rw-r--r--mobile/android/services/src/main/res/drawable-xhdpi/sync_promo.pngbin0 -> 1236 bytes
-rw-r--r--mobile/android/services/src/main/res/drawable-xxhdpi/fxaccount_sync_error.pngbin0 -> 1070 bytes
-rw-r--r--mobile/android/services/src/main/res/drawable-xxhdpi/sync_avatar_default.pngbin0 -> 11124 bytes
-rw-r--r--mobile/android/services/src/main/res/drawable-xxhdpi/sync_desktop.pngbin0 -> 339 bytes
-rw-r--r--mobile/android/services/src/main/res/drawable-xxhdpi/sync_desktop_inactive.pngbin0 -> 363 bytes
-rw-r--r--mobile/android/services/src/main/res/drawable-xxhdpi/sync_mobile.pngbin0 -> 246 bytes
-rw-r--r--mobile/android/services/src/main/res/drawable-xxhdpi/sync_mobile_inactive.pngbin0 -> 249 bytes
-rw-r--r--mobile/android/services/src/main/res/layout/fxaccount_preference_list_fragment.xml40
-rw-r--r--mobile/android/services/src/main/res/layout/fxaccount_status_error_preference.xml66
-rw-r--r--mobile/android/services/src/main/res/layout/homescreen_prompt.xml92
-rw-r--r--mobile/android/services/src/main/res/layout/simple_helper_ui.xml61
-rw-r--r--mobile/android/services/src/main/res/menu/fxaccount_status_menu.xml8
-rw-r--r--mobile/android/services/src/main/res/values-v11/fxaccount_styles.xml21
-rw-r--r--mobile/android/services/src/main/res/values/fxaccount_colors.xml9
-rw-r--r--mobile/android/services/src/main/res/values/fxaccount_dimens.xml18
-rw-r--r--mobile/android/services/src/main/res/values/fxaccount_styles.xml27
-rw-r--r--mobile/android/services/src/main/res/xml/fxaccount_authenticator.xml11
-rw-r--r--mobile/android/services/src/main/res/xml/fxaccount_options.xml18
-rw-r--r--mobile/android/services/src/main/res/xml/fxaccount_status_prefscreen.xml142
-rw-r--r--mobile/android/services/src/main/res/xml/fxaccount_syncadapter.xml12
-rw-r--r--mobile/android/services/strings.xml.in86
-rw-r--r--mobile/android/stumbler/Makefile.in9
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/AppGlobals.java82
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/Prefs.java205
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/LocalPreferenceReceiver.java70
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/SafeReceiver.java43
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/SystemReceiver.java41
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/Reporter.java219
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/StumblerService.java254
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/BSSIDBlockList.java65
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/SSIDBlockList.java41
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/WifiBlockListInterface.java11
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/DataStorageContract.java38
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/DataStorageManager.java473
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/PersistedStats.java99
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/StumblerBundle.java187
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/GPSScanner.java293
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/LocationBlockList.java105
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/ScanManager.java191
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/WifiScanner.java228
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellInfo.java391
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScanner.java178
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScannerImplementation.java299
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/uploadthread/AsyncUploader.java214
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/uploadthread/UploadAlarmReceiver.java138
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/AbstractCommunicator.java158
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/NetworkUtils.java32
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/PersistentIntentService.java85
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/TelemetryWrapper.java35
-rw-r--r--mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/Zipper.java48
-rw-r--r--mobile/android/stumbler/manifests/StumblerManifest_services.xml.in32
-rw-r--r--mobile/android/stumbler/moz.build12
-rw-r--r--mobile/android/stumbler/stumbler_sources.mozbuild36
-rw-r--r--mobile/android/tests/.eslintrc18
-rw-r--r--mobile/android/tests/background/junit3/AndroidManifest.xml.in23
-rw-r--r--mobile/android/tests/background/junit3/Makefile.in13
-rw-r--r--mobile/android/tests/background/junit3/background_junit3_sources.mozbuild78
-rw-r--r--mobile/android/tests/background/junit3/instrumentation.ini22
-rw-r--r--mobile/android/tests/background/junit3/moz.build42
-rw-r--r--mobile/android/tests/background/junit3/res/drawable-hdpi/icon.pngbin0 -> 7639 bytes
-rw-r--r--mobile/android/tests/background/junit3/res/drawable-ldpi/icon.pngbin0 -> 2979 bytes
-rw-r--r--mobile/android/tests/background/junit3/res/drawable-mdpi/icon.pngbin0 -> 4625 bytes
-rw-r--r--mobile/android/tests/background/junit3/res/layout/main.xml12
-rw-r--r--mobile/android/tests/background/junit3/res/values/strings.xml6
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestAndroidLogWriters.java68
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestUtils.java159
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestWaitHelper.java356
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/AndroidBrowserRepositoryTestCase.java818
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserBookmarksRepository.java636
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryRepository.java450
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestBookmarks.java1063
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestClientsDatabase.java200
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestClientsDatabaseAccessor.java128
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestFennecTabsRepositorySession.java297
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestFormHistoryRepositorySession.java441
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestPasswordsRepository.java482
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestTopSites.java92
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/TestAccountLoader.java163
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/TestBrowserIDKeyPairGeneration.java49
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java134
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/AndroidSyncTestCase.java52
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/DBHelpers.java84
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/DBProviderTestCase.java73
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/nativecode/test/TestNativeCrypto.java175
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/AndroidSyncTestCaseWithAccounts.java128
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestClientsStage.java95
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestResetting.java198
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestStoreTracking.java377
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestSyncConfiguration.java146
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestWebURLFinder.java49
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/BookmarkHelpers.java216
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultBeginDelegate.java33
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultCleanDelegate.java21
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultDelegate.java52
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultFetchDelegate.java106
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultFinishDelegate.java60
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultGuidsSinceDelegate.java19
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultSessionCreationDelegate.java53
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultStoreDelegate.java71
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectBeginDelegate.java22
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectBeginFailDelegate.java16
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFetchDelegate.java32
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFetchSinceDelegate.java47
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFinishDelegate.java17
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFinishFailDelegate.java15
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectGuidsSinceDelegate.java41
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidRequestFetchDelegate.java24
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidTypeStoreDelegate.java18
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectManyStoredDelegate.java48
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectNoGUIDsSinceDelegate.java33
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectNoStoreDelegate.java11
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectStoreCompletedDelegate.java17
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectStoredDelegate.java39
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/HistoryHelpers.java90
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/PasswordHelpers.java94
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SessionTestHelper.java82
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessBeginDelegate.java20
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessCreationDelegate.java18
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFetchDelegate.java22
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFinishDelegate.java20
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessStoreDelegate.java20
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java69
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/CommandHelpers.java40
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java52
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockAbstractNonRepositorySyncStage.java13
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockClientsDataDelegate.java66
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockClientsDatabaseAccessor.java76
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java57
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java63
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockRecord.java34
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java12
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockSharedPreferences.java137
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/WBORepository.java231
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java171
-rw-r--r--mobile/android/tests/background/junit4/resources/dlc_sync_deleted_item.json8
-rw-r--r--mobile/android/tests/background/junit4/resources/dlc_sync_old_format.json23
-rw-r--r--mobile/android/tests/background/junit4/resources/dlc_sync_single_font.json23
-rw-r--r--mobile/android/tests/background/junit4/resources/experiments.json99
-rw-r--r--mobile/android/tests/background/junit4/resources/feed_atom_blogger.xml13
-rw-r--r--mobile/android/tests/background/junit4/resources/feed_atom_feedburner.xml2
-rw-r--r--mobile/android/tests/background/junit4/resources/feed_atom_planetmozilla.xml4996
-rw-r--r--mobile/android/tests/background/junit4/resources/feed_atom_wikipedia.xml34
-rw-r--r--mobile/android/tests/background/junit4/resources/feed_rss10_planetmozilla.xml3860
-rw-r--r--mobile/android/tests/background/junit4/resources/feed_rss20_planetmozilla.xml3853
-rw-r--r--mobile/android/tests/background/junit4/resources/feed_rss_heise.xml1965
-rw-r--r--mobile/android/tests/background/junit4/resources/feed_rss_medium.xml100
-rw-r--r--mobile/android/tests/background/junit4/resources/feed_rss_spon.xml314
-rw-r--r--mobile/android/tests/background/junit4/resources/feed_rss_tumblr.xml95
-rw-r--r--mobile/android/tests/background/junit4/resources/feed_rss_wikipedia.xml21
-rw-r--r--mobile/android/tests/background/junit4/resources/feed_rss_wordpress.xml84
-rw-r--r--mobile/android/tests/background/junit4/resources/robolectric.properties3
-rw-r--r--mobile/android/tests/background/junit4/src/com/keepsafe/switchboard/TestSwitchboard.java142
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestBackoff.java114
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestBrowserIDAuthHeaderProvider.java23
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestClientsEngineStage.java806
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestCredentialsEndToEnd.java72
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestGlobalSession.java436
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestHeaderParsing.java29
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestLineByLineHandling.java115
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestMetaGlobal.java347
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestResource.java102
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestRetryAfter.java87
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestServer11Repository.java48
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestSyncStorageRequest.java269
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/SynchronizerHelpers.java282
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCollectionKeys.java197
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCommandProcessor.java117
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCryptoRecord.java302
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecord.java330
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecordsChannel.java229
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestResetCommands.java153
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServer11RepositorySession.java231
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServerLocalSynchronizer.java237
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSyncConfiguration.java39
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSynchronizer.java398
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSynchronizerSession.java306
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestUtils.java154
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/BaseTestStorageRequestDelegate.java61
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessDelegate.java35
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionBeginDelegate.java38
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionCreationDelegate.java36
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionFetchRecordsDelegate.java44
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionFinishDelegate.java37
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionStoreDelegate.java39
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositoryWipeDelegate.java36
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/HTTPServerTestHelper.java226
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockGlobalSessionCallback.java91
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockResourceDelegate.java85
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockServer.java73
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockSyncClientsEngineStage.java71
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockWBOServer.java28
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/test/TestHTTPServerTestHelper.java102
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/GeckoNetworkManagerTest.java51
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/GlobalPageMetadataTest.java174
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/TestGeckoProfile.java254
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/activitystream/TestActivityStream.java85
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/common/log/writers/test/TestLogWriters.java179
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/DelegatingTestContentProvider.java86
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/TestTabsProvider.java338
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/TestTabsProviderRemoteTabs.java244
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/fxa/test/TestFxAccountClient20.java41
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/fxa/test/TestFxAccountUtils.java131
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/test/EntityTestHelper.java34
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java72
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/CommandHelpers.java40
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java51
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockAbstractNonRepositorySyncStage.java13
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockClientsDataDelegate.java66
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockClientsDatabaseAccessor.java76
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java57
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java60
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockRecord.java51
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java11
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockSharedPreferences.java137
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/TestRunner.java125
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WBORepository.java230
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java172
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestASNUtils.java45
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestDSACryptoImplementation.java60
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestJSONWebTokenUtils.java151
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestRSACryptoImplementation.java56
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/cleanup/TestFileCleanupController.java92
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/cleanup/TestFileCleanupService.java106
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserContractTest.java67
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHighlightsTest.java438
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryTest.java341
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryVisitsTest.java338
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryVisitsTestBase.java77
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderVisitsTest.java301
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/distribution/TestReferrerDescriptor.java33
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestDownloadAction.java607
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestStudyAction.java119
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestSyncAction.java276
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestVerifyAction.java123
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/catalog/TestDownloadContentBuilder.java69
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/catalog/TestDownloadContentCatalog.java262
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/knownsites/TestKnownSiteBlogger.java74
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/knownsites/TestKnownSiteMedium.java66
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/knownsites/TestKnownSiteTumblr.java62
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/parser/TestSimpleFeedParser.java323
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/TestSkewHandler.java70
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/MockFxAccountClient.java226
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/TestFxAccountLoginStateMachine.java205
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/TestStateFactory.java91
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/helpers/AssertUtil.java29
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/home/TestHomeConfigPrefsBackendMigration.java264
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconDescriptor.java56
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconDescriptorComparator.java152
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconRequest.java81
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconRequestBuilder.java159
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconResponse.java148
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconTask.java575
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconsHelper.java139
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestContentProviderLoader.java31
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestDataUriLoader.java46
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestDiskLoader.java94
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestIconDownloader.java112
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestIconGenerator.java128
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestJarLoader.java31
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestLegacyLoader.java152
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestMemoryLoader.java78
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestAboutPagesPreparer.java73
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestAddDefaultIconUrl.java79
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestFilterKnownFailureUrls.java60
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestFilterMimeTypes.java67
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestFilterPrivilegedUrls.java86
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestLookupIconUrl.java101
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestColorProcessor.java59
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestDiskProcessor.java100
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestMemoryProcessor.java134
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestResizingProcessor.java111
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/permissions/TestPermissions.java253
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/TestPushManager.java238
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/TestPushState.java70
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestAutopushClient.java30
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestLiveAutopushClient.java171
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestBase32.java53
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestCryptoInfo.java144
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestHKDF.java143
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestKeyBundle.java65
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestPBKDF2.java124
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestPersistedCrypto5Keys.java83
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestSRPConstants.java45
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/middleware/test/TestCrypto5MiddlewareRepositorySession.java291
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestHMACAuthHeaderProvider.java165
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestHawkAuthHeaderProvider.java145
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestLiveHawkAuth.java181
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestUserAgentHeaders.java131
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/BrowserContractHelpersTest.java33
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/VisitsHelperTest.java144
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/test/TestBookmarksInsertionManager.java221
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/domain/TestClientRecord.java103
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/domain/test/TestFormHistoryRecord.java92
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderDelegateTest.java186
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderTest.java543
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/test/TestRepositorySessionBundle.java47
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/test/TestSafeConstrainedServer11Repository.java144
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/BatchMetaTest.java282
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploaderTest.java441
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/PayloadTest.java137
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegateTest.java404
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/RecordUploadRunnableTest.java38
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestEnsureCrypto5KeysStage.java237
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestFetchMetaGlobalStage.java391
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestStageLookup.java41
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestExtendedJSONObject.java203
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestInfoCollections.java101
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestPersistedMetaGlobal.java105
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/measurements/TestSearchCountMeasurements.java161
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/measurements/TestSessionMeasurements.java124
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/pingbuilders/TestTelemetryPingBuilder.java84
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/schedulers/TestTelemetryUploadAllPingsImmediatelyScheduler.java59
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/stores/TestTelemetryJSONFilePingStore.java250
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/tokenserver/test/TestTokenServerClient.java335
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/NetworkUtilsTest.java185
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestContextUtils.java38
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestDateUtil.java89
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestFileUtils.java339
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestIntentUtils.java73
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestStringUtils.java122
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestUUIDUtil.java51
-rw-r--r--mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/publicsuffix/TestPublicSuffix.java62
-rw-r--r--mobile/android/tests/background/moz.build9
-rw-r--r--mobile/android/tests/browser/chrome/basic_article.html16
-rw-r--r--mobile/android/tests/browser/chrome/basic_article_mobile.html19
-rw-r--r--mobile/android/tests/browser/chrome/chrome.ini49
-rw-r--r--mobile/android/tests/browser/chrome/desktopmode_user_agent.sjs11
-rw-r--r--mobile/android/tests/browser/chrome/devicesearch.xml17
-rw-r--r--mobile/android/tests/browser/chrome/head.js72
-rw-r--r--mobile/android/tests/browser/chrome/head_search.js46
-rw-r--r--mobile/android/tests/browser/chrome/memory_page_1.html16
-rw-r--r--mobile/android/tests/browser/chrome/memory_page_2.html16
-rw-r--r--mobile/android/tests/browser/chrome/memory_page_3.html16
-rw-r--r--mobile/android/tests/browser/chrome/memory_page_4.html16
-rw-r--r--mobile/android/tests/browser/chrome/session_formdata_sample.html20
-rw-r--r--mobile/android/tests/browser/chrome/simpleservice.xml15
-rw-r--r--mobile/android/tests/browser/chrome/test_about_logins.html106
-rw-r--r--mobile/android/tests/browser/chrome/test_accounts.html48
-rw-r--r--mobile/android/tests/browser/chrome/test_android_log.html95
-rw-r--r--mobile/android/tests/browser/chrome/test_app_constants.html35
-rw-r--r--mobile/android/tests/browser/chrome/test_awsy_lite.html258
-rw-r--r--mobile/android/tests/browser/chrome/test_debugger_server.html53
-rw-r--r--mobile/android/tests/browser/chrome/test_desktop_useragent.html75
-rw-r--r--mobile/android/tests/browser/chrome/test_device_search_engine.html75
-rw-r--r--mobile/android/tests/browser/chrome/test_get_last_visited.html106
-rw-r--r--mobile/android/tests/browser/chrome/test_hidden_select_option.html103
-rw-r--r--mobile/android/tests/browser/chrome/test_home_provider.html165
-rw-r--r--mobile/android/tests/browser/chrome/test_identity_mode.html58
-rw-r--r--mobile/android/tests/browser/chrome/test_java_addons.html118
-rw-r--r--mobile/android/tests/browser/chrome/test_jni.html82
-rw-r--r--mobile/android/tests/browser/chrome/test_migrate_ui.html57
-rw-r--r--mobile/android/tests/browser/chrome/test_network_manager.html41
-rw-r--r--mobile/android/tests/browser/chrome/test_offline_page.html111
-rw-r--r--mobile/android/tests/browser/chrome/test_reader_view.html56
-rw-r--r--mobile/android/tests/browser/chrome/test_resource_substitutions.html72
-rw-r--r--mobile/android/tests/browser/chrome/test_restricted_profiles.html57
-rw-r--r--mobile/android/tests/browser/chrome/test_select_disabled.html86
-rw-r--r--mobile/android/tests/browser/chrome/test_selectoraddtab.html92
-rw-r--r--mobile/android/tests/browser/chrome/test_session_form_data.html274
-rw-r--r--mobile/android/tests/browser/chrome/test_session_scroll_position.html310
-rw-r--r--mobile/android/tests/browser/chrome/test_session_zombification.html185
-rw-r--r--mobile/android/tests/browser/chrome/test_shared_preferences.html255
-rw-r--r--mobile/android/tests/browser/chrome/test_simple_discovery.html86
-rw-r--r--mobile/android/tests/browser/chrome/test_video_discovery.html154
-rw-r--r--mobile/android/tests/browser/chrome/test_web_channel.html121
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/163.wrating.com/a.gif@a=&c=860010-0503010000bin0 -> 43 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/163.wrating.com/a1.js1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/adgeo.163.com/ad_cookies0
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/analytics.163.com/ntes.js1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/jr@site=netease&affiliate=homepage&cat=homepage&type=adend&location=11
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/jr@site=netease&affiliate=homepage&cat=homepage&type=popup&location=11
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=banner360x65&location=1.html1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=banner360x65&location=2.html1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column360x100&location=1.html1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column360x100&location=2.html43
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column360x100&location=3.html43
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column360x100&location=5.html43
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column360x100&location=6.html43
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=1.html1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=2.html1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=3.html43
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=4.html43
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=5.html43
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=6.html43
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column600x80&location=1.html1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x100&location=1.html1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x100&location=2.html43
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x180&location=1.html43
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x180&location=2.html1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x180&location=3.html43
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x180&location=4.html1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x300&location=1.html43
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x300&location=2.html1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=textlinkhouse&location=1.html15
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=textlinkhouse&location=2.html15
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.126.net/channel1/55x20_bai.gifbin0 -> 1667 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.126.net/channel1/55x20_lan.gifbin0 -> 1789 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.126.net/channel5/008976/bolon_110302.pngbin0 -> 501 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.126.net/channel5/360/360100_110318.jpgbin0 -> 10245 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/6/20110406182512d4541.jpgbin0 -> 4979 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/20110408075741e084c.jpgbin0 -> 3946 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/2011040808080199ae7.jpgbin0 -> 4259 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/201104080835397174e.jpgbin0 -> 6215 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/201104080847137e997.jpgbin0 -> 5682 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/20110408085323b9296.jpgbin0 -> 5246 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/20110408092834ed61d.jpgbin0 -> 7343 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/201104080930016f866.jpgbin0 -> 3899 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/201104080934433598e.jpgbin0 -> 4137 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/2011040809550649773.jpgbin0 -> 6902 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/20110408104255a47ce.jpgbin0 -> 4358 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/201104081119113f37f.jpgbin0 -> 5600 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/2011040811445023471.jpgbin0 -> 5544 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/2011040814544385564.jpgbin0 -> 4738 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/2011040815090608fd9.jpgbin0 -> 3513 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/9/20110409022720f974c.jpgbin0 -> 5284 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/netease/wzdzbs.gifbin0 -> 2664 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/digi/linzj/1102/03/191.jpgbin0 -> 12682 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/img09/icon/icon.pngbin0 -> 579 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/attr.pngbin0 -> 930 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/icon_product_listv0.0.3.pngbin0 -> 2616 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/iconv0.0.7.pngbin0 -> 4028 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/neteasy_mallv0.0.1.pngbin0 -> 911 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/theme_blue.pngbin0 -> 1782 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/yodao_bg_blue.jpgbin0 -> 9514 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/game/20110216/ql/x/390x100.jpgbin0 -> 19460 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/game/20110216/ql/x/600x80.gifbin0 -> 24228 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/hr/20110216/hz/360x100.jpgbin0 -> 17913 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/sales/2011/03/ly/390x100.jpgbin0 -> 19684 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/sales/2011/04/hy/190x100.jpgbin0 -> 15226 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/sales/2011/04/wb/360x65.jpgbin0 -> 17561 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/sales/2011/04/yd/190x180.jpgbin0 -> 17036 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/7/20110407093718ef414.jpgbin0 -> 2133 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/7/20110407202028db993.jpgbin0 -> 5987 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/201104080728304dcb2.jpgbin0 -> 3565 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408082635b6897.jpgbin0 -> 4289 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/201104080828458908d.jpgbin0 -> 5934 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/2011040808393075049.jpgbin0 -> 6499 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/2011040809433960d68.jpgbin0 -> 6093 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408100357df2b1.jpgbin0 -> 4460 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408115631ad273.jpgbin0 -> 5471 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408120203d0f08.jpgbin0 -> 2217 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/201104081242198a4ba.jpgbin0 -> 4224 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/2011040812525484a8f.jpgbin0 -> 3088 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408125931e0a79.jpgbin0 -> 3858 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408140704d246b.jpgbin0 -> 4978 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408144428d419d.jpgbin0 -> 5797 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/2011040814452013ef7.jpgbin0 -> 2241 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/2011040814525199c07.jpgbin0 -> 3245 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/201104082245192ae96.jpgbin0 -> 4952 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/www/v2011/css/theme_blue1227.css1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/www/v2011/img/tg_news.jpgbin0 -> 17987 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/163homepage/biaoshi.gifbin0 -> 1290 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/163homepage/bj110.gifbin0 -> 2397 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/rpic/fld3/fld_homepage.js987
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/rpic/fld3/flsclasses.js30
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/techpro/shangpin/20110331/36-65.jpgbin0 -> 10682 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/techpro/tuangou/20110218/170-80.jpgbin0 -> 15209 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/yodaoimages/pack.r091221/scripts/autocomplete.163.165290.js1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/auto/2011/3/30/20110330215354a8c7a.jpgbin0 -> 4872 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/book/2011/4/7/201104071025387042e.jpgbin0 -> 3085 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/book/2011/4/7/20110407103153df111.jpgbin0 -> 4546 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/book/2011/4/8/20110408105903d5d53.jpgbin0 -> 7608 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/book/2011/4/8/20110408110145beb70.jpgbin0 -> 6091 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/cnews/js/ntes_jslib_1.x.js14
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/edu/2011/4/6/20110406220601277f0.jpgbin0 -> 3783 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/edu/2011/4/9/20110409001451f646c.jpgbin0 -> 4987 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/ent/2011/4/8/20110408183341f6142.jpgbin0 -> 5721 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/game/2011/4/8/20110408091923ca1d8.jpgbin0 -> 6027 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/game/2011/4/8/20110408100456977e5.jpgbin0 -> 4119 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/game/2011/4/8/2011040810253254779.jpgbin0 -> 5955 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/house/2011/4/7/201104070846149dec5.jpgbin0 -> 3578 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/house/2011/4/8/20110408094024dfb90.gifbin0 -> 11109 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/lady/2011/4/7/20110407235235eb565.jpgbin0 -> 4436 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/lady/2011/4/8/20110408082553b8653.jpgbin0 -> 4341 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/life/2011/2/24/20110224214610e49c1.jpgbin0 -> 2161 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/life/2011/4/1/20110401105148c65f3.jpgbin0 -> 4423 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/life/2011/4/6/20110406140048c8dea.jpgbin0 -> 1627 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/life/2011/4/6/201104061402503e782.jpgbin0 -> 3107 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/life/2011/4/8/20110408175702d86a7.jpgbin0 -> 3168 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/mobile/2011/4/8/201104080904537def0.jpgbin0 -> 5446 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/sports/2011/4/8/20110408164530e0dfd.jpgbin0 -> 2390 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/sports/2011/4/8/20110408224146ca253.jpgbin0 -> 3646 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/sports/2011/4/8/20110408234759dabf8.jpgbin0 -> 4856 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/travel/2011/4/7/2011040719553034b7b.jpgbin0 -> 6179 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/video/2011/4/8/20110408143144afad3.jpgbin0 -> 6793 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/www/logo/logo_png.pngbin0 -> 992 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/auto/2011/4/8/20110408091859b1da7.jpgbin0 -> 4015 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/auto/2011/4/8/201104080930543aaa8.jpgbin0 -> 3573 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/book/2011/4/8/20110408102221db369.jpgbin0 -> 4521 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/digi/2011/4/8/20110408144717d8da9.jpgbin0 -> 3677 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/ent/2011/4/8/20110408074407aed87.jpgbin0 -> 3444 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/ent/2011/4/8/201104080804383b8a7.jpgbin0 -> 3395 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/ent/2011/4/8/2011040809044637924.jpgbin0 -> 5649 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/game/2011/4/5/2011040502293054a8f.jpgbin0 -> 4196 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/game/2011/4/8/201104081007164a116.jpgbin0 -> 4072 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/game/2011/4/8/201104081009084803f.jpgbin0 -> 6167 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/game/2011/4/8/2011040811265683661.jpgbin0 -> 4767 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/home/2011/4/7/20110407131936bb4ec.pngbin0 -> 17647 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/house/2011/4/8/201104080927161a54f.jpgbin0 -> 6032 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/lady/2011/4/7/2011040711484089cba.jpgbin0 -> 4902 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/lady/2011/4/8/20110408014720d3fc0.jpgbin0 -> 3338 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/lady/2011/4/8/20110408224817711dd.jpgbin0 -> 4223 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/life/2011/3/7/20110307134125752e1.jpgbin0 -> 1661 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/mobile/2011/4/8/2011040809135520264.jpgbin0 -> 4486 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/photo/0008/2010-01-30/120x90_5U980MMS294H0008.JPGbin0 -> 3639 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/sports/2011/4/8/20110408211535eae49.jpgbin0 -> 5071 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/stock/2011/3/1/201103010846298829b.jpgbin0 -> 3457 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/stock/2011/4/8/201104080929109dd6d.pngbin0 -> 18442 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/stock/2011/4/8/20110408121505602ea.jpgbin0 -> 5538 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/stock/2011/4/8/20110408183832fdfa0.pngbin0 -> 11191 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/video/2011/4/7/20110407105038a01d2.jpgbin0 -> 4582 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/video/2011/4/7/2011040715531564880.jpgbin0 -> 4680 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/video/2011/4/8/2011040809594909a0a.jpgbin0 -> 5232 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/imgrc.ph.126.net/40YCPhfL6uaLg3xA4ISWew==/4227754150194064440.jpgbin0 -> 25468 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/imgrc.ph.126.net/F4Oc-9fe_HYFRsSk0SRMmA==/4223532025543403580.jpgbin0 -> 16689 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/imgrc.ph.126.net/VfPeCwJ6ufovjjY9ueyUxA==/4224939400426958880.jpgbin0 -> 21972 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/imgrc.ph.126.net/chRhUK9Mxz9gdCzkEUzn5w==/4226346775310512150.jpgbin0 -> 13490 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/imgrc.ph.126.net/nSvNUs-5vbkySqbYp-lnLw==/4226628250287222807.jpgbin0 -> 26297 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/oimagea4.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2FPjU3gbin0 -> 1940 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/oimagea8.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2F2WEnFWbin0 -> 1739 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/oimagea8.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2F2x2iAObin0 -> 2126 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/oimagea8.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2F40hcYlbin0 -> 1943 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/oimageb2.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2F46NVMebin0 -> 1902 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/oimageb3.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2FTyjFqbin0 -> 1757 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/oimagec1.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2F3SWBUhbin0 -> 1512 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/oimagec7.ydstatic.com/image@w=128&h=128&url=http%3A%2F%2F126.fm%2F3cAjJDbin0 -> 3307 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/p.mail.163.com/mailinfo/img/mail1.gifbin0 -> 576 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/p.mail.163.com/mailinfo/img/mail2.gifbin0 -> 574 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/p.mail.163.com/mailinfo/ntes_mail_info_www_1222.js156
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/p.mail.163.com/mailinfo/shownewmsg_www_1222.htm.html25
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/pro.163.com/js.ng/site=netease&affiliate=homepage&cat=homepage&type=flash&location=11
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/qn.163.com/images/qnyh20110411.jpgbin0 -> 3411 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/show.mediav.com/s@type=1&db=mediav&pub=118_2620_36413&cus=0_0_0_0_0&wh=360x100&btype=1&js=1.html8
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/www.163.com/index.html4024
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/www.163.com/mediav.gif8
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/163.com/zjs.ipinyou.com/2011032517331513260_2342_190180.js1
-rw-r--r--mobile/android/tests/browser/chrome/tp5/README1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/baidu.com/c.baidu.com/c.gif@t=0&q=mozilla&p=0&pn=1.html0
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/baidu.com/open.baidu.com/stat/image/Icon_Aladdin.gifbin0 -> 534 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/aladdin/img/table/bg.gifbin0 -> 3241 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/img/arr.gifbin0 -> 254 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/img/baidu_jgylogo1.gifbin0 -> 708 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/img/i2.pngbin0 -> 575 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/js/bdsug.js@v=1.0.3.01
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/s@wd=mozilla.html123
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/user/js/u.js1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/b.scorecardresearch.com/b2@c1=2&c2=6035051&c3=&c4=www.bbc.co.uk%2Fnews%2F&c5=&c6=&c15=&cv=1.3&cj=1.html0
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/bbc.112.2o7.net/b/ss/bbcwglobalprod/1/H.21--NS/0@AQB=1&pccr=true&AQE=1bin0 -> 43 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/edge.quantserve.com/quant.js28
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/js.revsci.net/gateway/gw.js@csid=J087814
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbc.co.uk/js/app/av/emp/1_1_3_0_0_426652_426614_1/config.sjson@edition=us&site=news&section=%2FFrontpage195
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/css/screen/shared/19_58/3pt_ads.css1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/img/1_0_1/cream/hi/news/news-blocks.gifbin0 -> 1657 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/app/bbccom/19_52/s_code.js1091
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/app/bbccom/19_61/bbccom.js1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/common/3_2/bbc_fmtj_common.js1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/config/apps/4_5/bbc_fmtj_config.js1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/core/3_2/bbc_fmtj.js1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/locationservices/locator/v4_0/locator.js1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/50112000/jpg/_50112416_010706746-1.jpgbin0 -> 11114 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/50906000/jpg/_50906324_006353309-2.jpgbin0 -> 3541 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/51990000/jpg/_51990536_011672235-1.jpgbin0 -> 5558 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52015000/jpg/_52015349_flag_reuters_144.jpgbin0 -> 6663 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52054000/jpg/_52054442_mj.144.jpgbin0 -> 4326 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52057000/jpg/_52057539_arniecomp.jpgbin0 -> 3448 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52058000/jpg/_52058296_holdring_thinks.jpgbin0 -> 2444 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52058000/jpg/_52058744_jex_1012144_de27-1.jpgbin0 -> 3705 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52063000/jpg/_52063276_52063272.jpgbin0 -> 4166 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52064000/jpg/_52064940_94471941.jpgbin0 -> 4893 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52065000/jpg/_52065323_aionscreenshot,ncsoft.jpgbin0 -> 5113 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52068000/jpg/_52068942_jex_1012675_de09-1.jpgbin0 -> 3628 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52069000/jpg/_52069270_011711396-1.jpgbin0 -> 3051 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52072000/jpg/_52072075_52072074.jpgbin0 -> 13528 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52072000/jpg/_52072121_-3.jpgbin0 -> 2890 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52072000/jpg/_52072276_jex_1012855_de27-1.jpgbin0 -> 3829 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52073000/jpg/_52073406_008253948-1.jpgbin0 -> 6021 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52073000/jpg/_52073764_011717136-1.jpgbin0 -> 2708 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52074000/jpg/_52074033_jex_1013006_de27.jpgbin0 -> 4850 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52075000/jpg/_52075786_stewart_getty304.jpgbin0 -> 4819 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52076000/jpg/_52076863_jex_1013152_de27-1.jpgbin0 -> 5668 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52077000/jpg/_52077604_jex_1013246_de27-1.jpgbin0 -> 4745 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52077000/jpg/_52077792_52077791.jpgbin0 -> 4397 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52077000/jpg/_52077993_ivory_coast.jpgbin0 -> 5486 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52078000/jpg/_52078134_astuteshoot.jpgbin0 -> 5378 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52078000/jpg/_52078945_jex_1013338_de27-1.jpgbin0 -> 5509 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52079000/jpg/_52079170_jex_1013354_de30-1.jpgbin0 -> 6634 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/sol/shared/img/v4/commonwealth_games_2010/cg_bbccom_banner_sprite2.pngbin0 -> 42881 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/components/components.css1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/accordian_overlay.pngbin0 -> 2910 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/arrow_foldout.gifbin0 -> 1056 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/arrow_foldout.pngbin0 -> 351 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/england-map.pngbin0 -> 3899 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/geo-digest-vertical-panel.gifbin0 -> 126 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/languages-sprite.gifbin0 -> 11937 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/livestats-sprite-ko.pngbin0 -> 4729 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/livestats-sprite.gifbin0 -> 4687 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/nav-divider.pngbin0 -> 126 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/news_masthead.gifbin0 -> 970 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/personalisation-help-icon.gifbin0 -> 139 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/programmes-iplayer-brand.pngbin0 -> 2240 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/red-masthead.pngbin0 -> 37257 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/roadicon.gifbin0 -> 185 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/scotland-map-hover.pngbin0 -> 8445 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/scotland-map.png@v.2bin0 -> 3600 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/sprite_most_watched.gifbin0 -> 5093 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/sprite_most_watched_ko.pngbin0 -> 3460 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/story_sprite.gifbin0 -> 2181 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/subnav-divider.pngbin0 -> 126 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/wales-map-hover.pngbin0 -> 2248 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/wales-map.pngbin0 -> 2599 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/world-map.pngbin0 -> 10166 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/skin.css1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/global.css1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/GVL3-icons-test.pngbin0 -> 15094 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/carousel-prev-next-3.pngbin0 -> 1594 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/cbl.pngbin0 -> 265 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/cbr.pngbin0 -> 250 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/foldout-arrow.gifbin0 -> 2284 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/gvl3-grid-2.pngbin0 -> 296 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/gvl3-icons-0-2.gifbin0 -> 10638 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/gvl3-icons-0-2.pngbin0 -> 16784 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/gvl3-live-icon-inverted.gifbin0 -> 186 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/icons/listen-charcoal.pngbin0 -> 300 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/icons/play-charcoal.pngbin0 -> 222 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/index-quote.pngbin0 -> 345 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/live-icon-32.gifbin0 -> 371 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/market-data-down.gifbin0 -> 110 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/market-data-down.pngbin0 -> 180 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/market-data-up.gifbin0 -> 108 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/market-data-up.pngbin0 -> 189 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/most_watched.pngbin0 -> 1823 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/search.pngbin0 -> 390 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/select-arrow.pngbin0 -> 223 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/services-alert.gifbin0 -> 1005 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/services-mail.gifbin0 -> 256 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/services-mobile.gifbin0 -> 123 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/services-podcast.gifbin0 -> 210 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/services-rss.gifbin0 -> 343 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/story_sprite.gifbin0 -> 3595 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/story_sprite.pngbin0 -> 1859 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/traffic_icon.gifbin0 -> 295 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/transparencies/rgba-0-0-0-07.pngbin0 -> 130 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/layout/index.css1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/mobile.css1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/print.css1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/type.css300
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/node1.bbcimg.co.uk/glow/gloader.0.1.4.js18
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/pixel.quantserve.com/pixel/p-ccrmZLtMqYB8w.gifbin0 -> 35 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/pixel.quantserve.com/pixel/r.html0
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/sa.bbc.co.uk/bbc/bbc/s.gifbin0 -> 43 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/sa.bbc.co.uk/bbc/bbc/s.htmlbin0 -> 43 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.3.2/newnav/img/search_icon.pngbin0 -> 287 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/autosuggest_loader.gifbin0 -> 673 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/blocks/dark.pngbin0 -> 1023 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/blocks/light.pngbin0 -> 965 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/main_sprite.pngbin0 -> 2063 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/mast_bg.pngbin0 -> 158 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/mast_colours.pngbin0 -> 666 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/more_arrow.pngbin0 -> 3630 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/mothball/bg.jpgbin0 -> 336 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/mothball/i.gifbin0 -> 1278 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/nav_divider.pngbin0 -> 130 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/panel.pngbin0 -> 2257 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/search_icon.pngbin0 -> 287 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/suggest_sprite.pngbin0 -> 1200 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/suggest_sprite_rtl.pngbin0 -> 1317 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/tooltip.pngbin0 -> 1274 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/script/barlesque.js1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/style/main.css1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/requirejs/0.6.4/sharedmodules/require.js1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/wwtravel/img/ic/304-170/1300928948164652012_1.jpgbin0 -> 14679 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/wwtravel/img/ic/304-170/130203147123329681316_1.jpgbin0 -> 15764 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/www.bbc.co.uk/bbc.com/images/interstitial/arrow.gifbin0 -> 190 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/www.bbc.co.uk/bbc.com/images/interstitial/header.gifbin0 -> 3642 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/www.bbc.co.uk/bbc.com/images/interstitial/header_travel.gifbin0 -> 3642 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/bbc.co.uk/www.bbc.co.uk/news/index.html2982
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/blst.msn.com/as/wea3/i/en-us/law/30.gifbin0 -> 939 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/23/6B8E88315584A40B04E32D89551E.jpgbin0 -> 7805 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/2F/9EFAECEC174B21FB83D10C82522D2.jpgbin0 -> 3070 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/38/FAF3346E94CF4579ECAB641703868.jpgbin0 -> 3178 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/5B/CC662FC6233C7449D9C7F9796801D.jpgbin0 -> 13639 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/76/CAF5FAB7F245F96327F2B4C806D.jpgbin0 -> 8252 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/80/82E2A652E4A790B140675E74293AD6.jpgbin0 -> 4090 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/B7/EB75D45B8948F72EE451223E95A96.gifbin0 -> 2477 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/CE/19F603C3122D48B6554BBD495195.jpgbin0 -> 9665 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/CF/59B3CB34EF11B221719175143187.jpgbin0 -> 3115 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/D8/41FF8CA0A47CC8208E684FA1BE6D6.jpgbin0 -> 10214 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/E2/37BA92E210D341BFDBF4126422A3D2.gifbin0 -> 657 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/EA/9BECE90994978BFAE6F38561515E8.jpgbin0 -> 3964 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/FF/6B3EB94D554DA0488C66DC31482D48.jpgbin0 -> 4099 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/gbl/lg/csl/favicon.icobin0 -> 4286 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/css/1d/b0ebeba5ed4ca3c158e6d6059f5074.css1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/07/617475cf39bf6f5c0bd6ecb985335c.gifbin0 -> 48 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/09/4ebdf19a1ce03cce12e11926256422.gifbin0 -> 79 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/0c/c57bc2a7d38843d7c4aa8028fc9f82.gifbin0 -> 1142 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/11/999518480e3c07301320f84f4bd855.pngbin0 -> 384 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/16/9798fea395258497f598bba500bf83.pngbin0 -> 2257 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/1a/57011fe37f98be0ee74ce87a62ba9b.pngbin0 -> 13041 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/50/f63ed0301e8b02a8a42d8590a46291.gifbin0 -> 1383 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/61/379589e51e05637f600f129f305b52.pngbin0 -> 1616 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/61/def0ebad64d00fda0702cb7b8179ea.pngbin0 -> 4670 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/62/b5797d19976f0955d6d5d5c87ec996.jpgbin0 -> 12284 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/77/b23a82d78a0605243aad8f44e8c079.gifbin0 -> 56 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/94/8b0fe9bcd1399077fdc9374e5f314d_1.pngbin0 -> 12823 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/b9/ab98403e7de9ce52839e5de99d27e5.gifbin0 -> 203 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/c6/7980776cb684844c20339b839ac35e.gifbin0 -> 7210 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/d7/fb6441a4c45cb3a3b2f592d914a3cd.gifbin0 -> 72 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/f8/614595fba50d96389708a4135776e4.gifbin0 -> 43 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/fb/f017d9e8cc630c5e02659b6eaf35fa.gifbin0 -> 2544 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/ff/290e7f0b12fa8a201581c74c1ae75a.gifbin0 -> 74 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/icons/BING_websearch_2.jpgbin0 -> 4082 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/icons/adchoices_gif.gifbin0 -> 554 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/col.stj.s-msn.com/br/sc/js/jquery/jquery-1.4.2.min.js154
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/static.foxsports.com/content/fscom/img/2011/04/07/040711-Golf-Tiger-Woods-1120pm-PI_20110407142414593_116_175.JPGbin0 -> 8415 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/udc.msn.com/c.gifbin0 -> 42 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/www.bing.com/partner/primedns.gifbin0 -> 43 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/msn.com/www.msn.com/index.html13
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/a/1302214109/images/loader.gifbin0 -> 759 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/a/1302214109/images/twitter_logo_header.pngbin0 -> 3079 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/a/1302214109/javascripts/lib/jquery.tipsy.min.js@13021146483
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/a/1302214109/javascripts/lib/mustache.js@1302114648403
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/1129087853/151aec2f-1534-4f61-9f3e-1e787cb51a8b_mini.pngbin0 -> 2006 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/1139176116/5c42a320-1e91-4d89-a034-0f140d2f23ba_mini.pngbin0 -> 1946 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/1277610502/Untitled-9_mini.jpgbin0 -> 2185 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/316019228/326994260_1117936370_0_mini.jpegbin0 -> 544 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/81990615/nightexterior-1_mini.jpgbin0 -> 1506 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/959692632/13659_1215732676789_1332990286_30703899_6344768_n_mini.jpgbin0 -> 503 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/sticky/default_profile_images/default_profile_4_mini.pngbin0 -> 543 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/favicon.icobin0 -> 1150 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/icon_lock.gifbin0 -> 226 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/reject_small.gifbin0 -> 385 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/spinner.gifbin0 -> 457 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/sprite-icons.pngbin0 -> 20815 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/toggle_down_dark.pngbin0 -> 258 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/javascripts/dismissable.js@13021146481
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/stylesheets/following.css@1302114648.css1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/1239180764/GlassblowerX_mini.jpgbin0 -> 718 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/1248229613/redsugarskullnecklace4-pola_mini.jpgbin0 -> 750 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/333032766/5600_106787006838_550741838_2009237_6385345_n_mini.jpgbin0 -> 536 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/754757071/rawr_mini.jpgbin0 -> 903 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/874705507/01_3_mini.jpgbin0 -> 1068 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/959721336/16869_103046893051833_100000395672538_70559_3952672_n_1__mini.jpgbin0 -> 582 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/ajax.gifbin0 -> 1737 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/arr-inline-form.gifbin0 -> 68 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/arr2.gifbin0 -> 68 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/arrow_right_dark.pngbin0 -> 398 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/bg-btn-blue.pngbin0 -> 380 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/bg-btn-signup_gold.pngbin0 -> 346 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/btn-bg.gifbin0 -> 593 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/btn_green_arrow.gifbin0 -> 1849 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/btn_green_arrow_small.gifbin0 -> 1563 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/btn_red_small.gifbin0 -> 1370 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-blue.gifbin0 -> 635 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-chart.gifbin0 -> 589 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-dark.gifbin0 -> 612 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-green.gifbin0 -> 600 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-mint.gifbin0 -> 605 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-pink.gifbin0 -> 609 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-red.gifbin0 -> 592 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-yellow.gifbin0 -> 947 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn.gifbin0 -> 594 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/checkmark.gifbin0 -> 64 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/close_small.pngbin0 -> 246 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/commercial/garuda-overlay.gifbin0 -> 162 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/dialog_arrows_sprite.gifbin0 -> 232 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/divider.pngbin0 -> 189 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/divot.gifbin0 -> 49 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/divvy-up.pngbin0 -> 262 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/divvy.gifbin0 -> 99 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/divvy.pngbin0 -> 276 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/follow_check.gifbin0 -> 156 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/geo_chrome_help_banner_back.pngbin0 -> 12138 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/geo_creation_hint_arrow.gifbin0 -> 114 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/geo_firefox_help_banner_back.pngbin0 -> 28756 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/geo_ie_gtb_help_banner_back.pngbin0 -> 18814 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon-mobile.gifbin0 -> 66 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_add.pngbin0 -> 3221 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_direct_reply.gifbin0 -> 371 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_lock.gifbin0 -> 226 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_remove.pngbin0 -> 3255 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_reply.gifbin0 -> 336 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_throbber.gifbin0 -> 864 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_trash.gifbin0 -> 148 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/inline-media.pngbin0 -> 30404 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/larry-shadowed-big.pngbin0 -> 3960 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/lock_icon_small.pngbin0 -> 282 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/more.gifbin0 -> 129 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/nav_search_submit.pngbin0 -> 634 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/check.pngbin0 -> 242 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/connect_129px.pngbin0 -> 6761 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/connect_146px.pngbin0 -> 7595 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/connect_170px.pngbin0 -> 8809 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/connect_236px.pngbin0 -> 13755 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/gradient-background.pngbin0 -> 346 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/rays-box.jpgbin0 -> 4641 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/t_170px.pngbin0 -> 392 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/petal_spinner.gifbin0 -> 3971 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/retweet/retweet-x.pngbin0 -> 238 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/round-btn-hover.gifbin0 -> 2470 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/round-btn.gifbin0 -> 2470 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/rss.gifbin0 -> 408 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/spinner.gifbin0 -> 457 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/sprite-icons.pngbin0 -> 20815 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/sprite-icons.png@v3bin0 -> 20815 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/tables/tablesorter-indicators.pngbin0 -> 451 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/thumb-bird-bw.gifbin0 -> 972 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/tipsy/tipsy-east.gifbin0 -> 3224 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/tipsy/tipsy-north.gifbin0 -> 3223 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/tipsy/tipsy-south.gifbin0 -> 3222 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/tipsy/tipsy-west.gifbin0 -> 3224 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_closed.gifbin0 -> 70 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_down_dark.gifbin0 -> 150 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_down_dark.pngbin0 -> 258 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_down_light.gifbin0 -> 154 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_down_light.pngbin0 -> 277 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_opened.gifbin0 -> 68 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_up_dark.gifbin0 -> 150 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_up_dark.pngbin0 -> 288 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toptweet-overlay.gifbin0 -> 295 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/translator/translator.pngbin0 -> 995 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/trendtip-pointer.gifbin0 -> 63 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/verified/verified.pngbin0 -> 737 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/verified/verified_small.pngbin0 -> 401 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/warning-sign.pngbin0 -> 4324 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/javascripts/geov1.js@13021146481
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/javascripts/twitter.js@13022155222435
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/phoenix/img/sprite-icons.pngbin0 -> 20815 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/stylesheets/geo.css@1302114648.css1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/stylesheets/twitter.css@1302114648.css1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_background_images/30261844/ICHCTwitterBG.jpgbin0 -> 172378 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/1063331761/LOLmart_150_mini.jpgbin0 -> 967 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/1124077786/batvatar_mini.pngbin0 -> 4250 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/1155395599/Memebase_small_mini.pngbin0 -> 7866 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/1289641028/CH_mini.jpgbin0 -> 5381 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/1296459376/profile_image_1301694822477_mini.jpgbin0 -> 550 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/700174615/twitter_mini.pngbin0 -> 845 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/724048626/Picture_3895-1_mini.jpgbin0 -> 1338 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/959827428/25000_1397284054938_1317351118_31101620_485629_n_mini.jpgbin0 -> 1437 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/959952929/ci_300x300_mini.jpgbin0 -> 359 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/sticky/default_profile_images/default_profile_1_mini.pngbin0 -> 619 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/sticky/default_profile_images/default_profile_2_mini.pngbin0 -> 712 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/sticky/default_profile_images/default_profile_6_mini.pngbin0 -> 706 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/arrow_right_dark.pngbin0 -> 398 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/buttons/bg-btn.gifbin0 -> 594 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/check.pngbin0 -> 242 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/connect_129px.pngbin0 -> 6761 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/connect_146px.pngbin0 -> 7595 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/connect_170px.pngbin0 -> 8809 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/connect_236px.pngbin0 -> 13755 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/gradient-background.pngbin0 -> 346 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/rays-box.jpgbin0 -> 4641 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/t_170px.pngbin0 -> 392 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/sprite-icons.pngbin0 -> 20815 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/javascripts/api.js@13021146481
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/javascripts/lib/gears_init.js@130211464887
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/stylesheets/buttons_new.css@1302114648.css1
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1092057020/eli_avatar_mini.pngbin0 -> 1441 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1096286685/newpink_copy_mini.jpgbin0 -> 942 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1110864280/41628_1144937489_2484_q_mini.jpgbin0 -> 890 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1213876440/27539_32561485399_2579_n_bigger.jpegbin0 -> 5378 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1260578495/191281_1758367531945_1621722394_1723810_2598069_o_mini.jpgbin0 -> 3734 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1299269362/10839_196974151498_693676498_3960874_1853030_n_mini.jpgbin0 -> 4384 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1302143328/Profile_copy_mini.jpgbin0 -> 24379 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/sticky/default_profile_images/default_profile_3_mini.pngbin0 -> 626 bytes
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/ajax.googleapis.com/ajax/libs/jquery/1.3.0/jquery.min.js19
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/twitter.com/ICHCheezburger.html1203
-rwxr-xr-xmobile/android/tests/browser/chrome/tp5/twitter.com/www.google.com/jsapi39
-rw-r--r--mobile/android/tests/browser/chrome/video_controls.html10
-rw-r--r--mobile/android/tests/browser/chrome/video_discovery.html77
-rw-r--r--mobile/android/tests/browser/chrome/video_discovery.sjs27
-rw-r--r--mobile/android/tests/browser/chrome/web_channel.html89
-rw-r--r--mobile/android/tests/browser/junit3/AndroidManifest.xml.in23
-rw-r--r--mobile/android/tests/browser/junit3/Makefile.in13
-rw-r--r--mobile/android/tests/browser/junit3/instrumentation.ini9
-rw-r--r--mobile/android/tests/browser/junit3/moz.build55
-rw-r--r--mobile/android/tests/browser/junit3/res/drawable-hdpi/icon.pngbin0 -> 7639 bytes
-rw-r--r--mobile/android/tests/browser/junit3/res/drawable-ldpi/icon.pngbin0 -> 2979 bytes
-rw-r--r--mobile/android/tests/browser/junit3/res/drawable-mdpi/icon.pngbin0 -> 4625 bytes
-rw-r--r--mobile/android/tests/browser/junit3/res/layout/main.xml12
-rw-r--r--mobile/android/tests/browser/junit3/res/values/strings.xml6
-rw-r--r--mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestDistribution.java37
-rw-r--r--mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestGeckoBackgroundThread.java56
-rw-r--r--mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestGeckoMenu.java72
-rw-r--r--mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestGeckoProfilesProvider.java50
-rw-r--r--mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestGeckoSharedPrefs.java153
-rw-r--r--mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestImageDownloader.java205
-rw-r--r--mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestJarReader.java124
-rw-r--r--mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestRawResource.java66
-rw-r--r--mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestSuggestedSites.java473
-rw-r--r--mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/harness/BrowserInstrumentationTestRunner.java33
-rw-r--r--mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/harness/BrowserTestListener.java42
-rw-r--r--mobile/android/tests/browser/moz.build17
-rw-r--r--mobile/android/tests/browser/robocop/AndroidManifest.xml.in67
-rw-r--r--mobile/android/tests/browser/robocop/Firefox.jpgbin0 -> 9775 bytes
-rw-r--r--mobile/android/tests/browser/robocop/Makefile.in67
-rw-r--r--mobile/android/tests/browser/robocop/README12
-rw-r--r--mobile/android/tests/browser/robocop/README.rst61
-rw-r--r--mobile/android/tests/browser/robocop/assets/README4
-rw-r--r--mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v27.dbbin0 -> 114688 bytes
-rw-r--r--mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v28.dbbin0 -> 116736 bytes
-rw-r--r--mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v29.dbbin0 -> 117760 bytes
-rw-r--r--mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v30.dbbin0 -> 368640 bytes
-rw-r--r--mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v31.dbbin0 -> 352256 bytes
-rw-r--r--mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v32.dbbin0 -> 360448 bytes
-rw-r--r--mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v33.dbbin0 -> 360448 bytes
-rw-r--r--mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v34.dbbin0 -> 610304 bytes
-rw-r--r--mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v35.dbbin0 -> 622593 bytes
-rw-r--r--mobile/android/tests/browser/robocop/assets/ico_decoder_favicons/golem_favicon.icobin0 -> 40648 bytes
-rw-r--r--mobile/android/tests/browser/robocop/assets/ico_decoder_favicons/microsoft_favicon.icobin0 -> 17174 bytes
-rw-r--r--mobile/android/tests/browser/robocop/assets/ico_decoder_favicons/nvidia_favicon.icobin0 -> 25214 bytes
-rw-r--r--mobile/android/tests/browser/robocop/assets/mock-package.zipbin0 -> 5650 bytes
-rw-r--r--mobile/android/tests/browser/robocop/assets/reading_list_bookmarks_migration/browser.dbbin0 -> 466944 bytes
-rw-r--r--mobile/android/tests/browser/robocop/assets/reading_list_bookmarks_migration/readercache/DWUP3U4ERC6TKJVSYXKJLHHEFY.json1
-rw-r--r--mobile/android/tests/browser/robocop/assets/reading_list_bookmarks_migration/readercache/KWNV7PXD3JFOJBQJVFXI3CQKNE.json1
-rw-r--r--mobile/android/tests/browser/robocop/assets/testcheck2-motionevents444
-rw-r--r--mobile/android/tests/browser/robocop/green.swfbin0 -> 112 bytes
-rw-r--r--mobile/android/tests/browser/robocop/javascript_redirect.sjs8
-rw-r--r--mobile/android/tests/browser/robocop/libs/robotium-solo-5.5.4.jarbin0 -> 129423 bytes
-rw-r--r--mobile/android/tests/browser/robocop/link_discovery.html8
-rw-r--r--mobile/android/tests/browser/robocop/moz.build34
-rw-r--r--mobile/android/tests/browser/robocop/reader_mode_pages/basic_article.html16
-rw-r--r--mobile/android/tests/browser/robocop/reader_mode_pages/developer.mozilla.org/en/XULRunner/Build_Instructions.html373
-rw-r--r--mobile/android/tests/browser/robocop/reader_mode_pages/not_an_article.html132
-rw-r--r--mobile/android/tests/browser/robocop/res/values/strings.xml9
-rw-r--r--mobile/android/tests/browser/robocop/robocop.ini118
-rw-r--r--mobile/android/tests/browser/robocop/robocop_404.sjs28
-rw-r--r--mobile/android/tests/browser/robocop/robocop_adobe_flash.html17
-rw-r--r--mobile/android/tests/browser/robocop/robocop_autophone.ini1
-rw-r--r--mobile/android/tests/browser/robocop/robocop_big_link.html13
-rw-r--r--mobile/android/tests/browser/robocop/robocop_big_mailto.html13
-rw-r--r--mobile/android/tests/browser/robocop/robocop_blank_01.html7
-rw-r--r--mobile/android/tests/browser/robocop/robocop_blank_02.html8
-rw-r--r--mobile/android/tests/browser/robocop/robocop_blank_03.html7
-rw-r--r--mobile/android/tests/browser/robocop/robocop_blank_04.html7
-rw-r--r--mobile/android/tests/browser/robocop/robocop_blank_05.html7
-rw-r--r--mobile/android/tests/browser/robocop/robocop_boxes.html42
-rw-r--r--mobile/android/tests/browser/robocop/robocop_dynamic.sjs18
-rw-r--r--mobile/android/tests/browser/robocop/robocop_geolocation.html20
-rw-r--r--mobile/android/tests/browser/robocop/robocop_getusermedia.html86
-rw-r--r--mobile/android/tests/browser/robocop/robocop_getusermedia2.html83
-rw-r--r--mobile/android/tests/browser/robocop/robocop_head.js848
-rw-r--r--mobile/android/tests/browser/robocop/robocop_input.html165
-rw-r--r--mobile/android/tests/browser/robocop/robocop_javascript.html20
-rw-r--r--mobile/android/tests/browser/robocop/robocop_link_to_slow_loading.html12
-rw-r--r--mobile/android/tests/browser/robocop/robocop_login_01.html21
-rw-r--r--mobile/android/tests/browser/robocop/robocop_login_02.html21
-rw-r--r--mobile/android/tests/browser/robocop/robocop_offline_storage.html8
-rw-r--r--mobile/android/tests/browser/robocop/robocop_picture_link.html13
-rw-r--r--mobile/android/tests/browser/robocop/robocop_popup.html12
-rw-r--r--mobile/android/tests/browser/robocop/robocop_search.html11
-rw-r--r--mobile/android/tests/browser/robocop/robocop_slow_loading.html23
-rw-r--r--mobile/android/tests/browser/robocop/robocop_suggestions.sjs32
-rw-r--r--mobile/android/tests/browser/robocop/robocop_testharness.js74
-rw-r--r--mobile/android/tests/browser/robocop/robocop_text_page.html27
-rw-r--r--mobile/android/tests/browser/robocop/roboextender/Makefile.in9
-rw-r--r--mobile/android/tests/browser/robocop/roboextender/base/robocop_home_banner.html37
-rw-r--r--mobile/android/tests/browser/robocop/roboextender/base/robocop_prompt_gridinput.html51
-rw-r--r--mobile/android/tests/browser/robocop/roboextender/bootstrap.js65
-rw-r--r--mobile/android/tests/browser/robocop/roboextender/chrome.manifest1
-rw-r--r--mobile/android/tests/browser/robocop/roboextender/install.rdf19
-rw-r--r--mobile/android/tests/browser/robocop/roboextender/moz.build12
-rw-r--r--mobile/android/tests/browser/robocop/simple_redirect.sjs5
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Actions.java126
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Assert.java25
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Driver.java44
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Element.java27
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecInstrumentationTestRunner.java79
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecMochitestAssert.java254
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeActions.java482
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeDriver.java392
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeElement.java116
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecTalosAssert.java74
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/LaunchFennecWithConfigurationActivity.java40
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/PaintedSurface.java105
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RoboCopException.java24
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopShare1.java17
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopShare2.java17
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopUtils.java58
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/StructuredLogger.java188
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/AboutHomeTest.java252
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseRobocopTest.java288
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseTest.java976
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentContextMenuTest.java135
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentProviderTest.java255
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/DatabaseHelper.java170
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/JavascriptBridgeTest.java107
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/JavascriptTest.java87
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventHelper.java210
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventReplayer.java224
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/PixelTest.java117
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/SelectionHandlerTest.java56
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/SessionTest.java407
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/StringHelper.java401
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITest.java203
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITestContext.java51
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AboutHomeComponent.java193
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AppMenuComponent.java295
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/BaseComponent.java36
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/GeckoViewComponent.java343
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/TabStripComponent.java56
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/ToolbarComponent.java326
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/AssertionHelper.java112
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/DeviceHelper.java108
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/FrameworkHelper.java94
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoClickHelper.java50
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoHelper.java49
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/HelperInitializer.java30
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/JavascriptBridge.java394
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/JavascriptMessageParser.java100
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/NavigationHelper.java104
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/RobotiumHelper.java43
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/WaitHelper.java215
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testANRReporter.java240
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomePageNavigation.java107
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomeVisibility.java57
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutPage.java47
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAccessibleCarets.java76
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testActivityStreamContextMenu.java94
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddSearchEngine.java172
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddonManager.java79
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAdobeFlash.java39
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAppMenuPathways.java77
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAxisLocking.java58
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBackButtonInEditMode.java47
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmark.java72
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarkFolders.java169
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarkKeyword.java28
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarklets.java46
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarksPanel.java174
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserDatabaseHelperUpgrades.java150
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserDiscovery.java13
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserProvider.java1921
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserSearchVisibility.java69
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBug1217581.java31
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testCheck2.java61
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testCheck3.java61
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDBUtils.java70
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDistribution.java556
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDoorHanger.java205
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testEventDispatcher.java450
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilePicker.java52
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilterOpenTab.java133
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFindInPage.java107
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFlingCorrectness.java52
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFormHistory.java104
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGeckoProfile.java295
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGeckoRequest.java121
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGetUserMedia.java159
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHistory.java74
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHistoryService.java12
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHomeBanner.java94
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHomeListsProvider.java118
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testICODecoder.java238
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputConnection.java349
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputUrlBar.java136
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testJarReader.java70
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testJavascriptBridge.java69
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLinkContextMenu.java37
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLoad.java23
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLoginsProvider.java387
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testMailToContextMenu.java26
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testNativeCrypto.java288
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testNewTab.java65
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testOSLocale.java137
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPanCorrectness.java49
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPasswordEncrypt.java125
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPasswordProvider.java104
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPermissions.java72
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPictureLinkContextMenu.java52
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrefsObserver.java81
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrivateBrowsing.java89
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPromptGridInput.java47
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReaderCacheMigration.java62
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReaderModeTitle.java19
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReadingListCache.java12
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReadingListToBookmarksMigration.java217
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRestrictions.java39
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRuntimePermissionsAPI.java48
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSearchHistoryProvider.java379
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSearchSuggestions.java115
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionHistory.java37
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionOOMRestore.java54
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionOOMSave.java87
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testShareLink.java265
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSnackbarAPI.java52
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testStateWhileLoading.java40
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testStumblerSetting.java90
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testThumbnails.java116
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testTrackingProtection.java65
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testUITelemetry.java56
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testUnifiedTelemetryClientId.java265
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testVideoControls.java9
-rw-r--r--mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testVkbOverlap.java105
-rw-r--r--mobile/android/tests/browser/robocop/testAccessibleCarets.html45
-rw-r--r--mobile/android/tests/browser/robocop/testAccessibleCarets.js323
-rw-r--r--mobile/android/tests/browser/robocop/testAccessibleCarets2.html23
-rw-r--r--mobile/android/tests/browser/robocop/testBrowserDiscovery.js150
-rw-r--r--mobile/android/tests/browser/robocop/testEventDispatcher.js44
-rw-r--r--mobile/android/tests/browser/robocop/testFilePicker.js73
-rw-r--r--mobile/android/tests/browser/robocop/testFindInPage.js89
-rw-r--r--mobile/android/tests/browser/robocop/testGeckoRequest.js40
-rw-r--r--mobile/android/tests/browser/robocop/testHistoryService.js128
-rw-r--r--mobile/android/tests/browser/robocop/testJavascriptBridge.js52
-rw-r--r--mobile/android/tests/browser/robocop/testReaderCacheMigration.js23
-rw-r--r--mobile/android/tests/browser/robocop/testReadingListCache.js126
-rw-r--r--mobile/android/tests/browser/robocop/testRuntimePermissionsAPI.js20
-rw-r--r--mobile/android/tests/browser/robocop/testSnackbarAPI.js21
-rw-r--r--mobile/android/tests/browser/robocop/testTrackingProtection.js166
-rw-r--r--mobile/android/tests/browser/robocop/testUITelemetry.js154
-rw-r--r--mobile/android/tests/browser/robocop/testUnifiedTelemetryClientId.js50
-rw-r--r--mobile/android/tests/browser/robocop/testVideoControls.js157
-rw-r--r--mobile/android/tests/browser/robocop/test_viewport.sjs33
-rw-r--r--mobile/android/tests/browser/robocop/tracking_bad.html12
-rw-r--r--mobile/android/tests/browser/robocop/tracking_good.html12
-rw-r--r--mobile/android/tests/browser/robocop/video-pattern.oggbin0 -> 299507 bytes
-rw-r--r--mobile/android/tests/browser/robocop/video-pattern.webmbin0 -> 220609 bytes
-rw-r--r--mobile/android/tests/browser/robocop/video_controls.html10
-rw-r--r--mobile/android/tests/javaaddons/AndroidManifest.xml.in14
-rw-r--r--mobile/android/tests/javaaddons/Makefile.in11
-rw-r--r--mobile/android/tests/javaaddons/moz.build23
-rw-r--r--mobile/android/tests/javaaddons/res/values/strings.xml3
-rw-r--r--mobile/android/tests/javaaddons/src/org/mozilla/javaaddons/test/ClassWithNoRecognizedConstructors.java11
-rw-r--r--mobile/android/tests/javaaddons/src/org/mozilla/javaaddons/test/JavaAddonV0.java24
-rw-r--r--mobile/android/tests/javaaddons/src/org/mozilla/javaaddons/test/JavaAddonV1.java59
-rw-r--r--mobile/android/tests/moz.build18
-rw-r--r--mobile/android/themes/core/about.css50
-rw-r--r--mobile/android/themes/core/aboutAccounts.css91
-rw-r--r--mobile/android/themes/core/aboutAddons.css332
-rw-r--r--mobile/android/themes/core/aboutBase.css114
-rw-r--r--mobile/android/themes/core/aboutDownloads.css50
-rw-r--r--mobile/android/themes/core/aboutHealthReport.css15
-rw-r--r--mobile/android/themes/core/aboutLogins.css238
-rw-r--r--mobile/android/themes/core/aboutMemory.css149
-rw-r--r--mobile/android/themes/core/aboutPage.css117
-rw-r--r--mobile/android/themes/core/aboutPrivateBrowsing.css84
-rw-r--r--mobile/android/themes/core/aboutReader.css120
-rw-r--r--mobile/android/themes/core/aboutReaderContent.css114
-rw-r--r--mobile/android/themes/core/aboutReaderControls.css290
-rw-r--r--mobile/android/themes/core/aboutSupport.css97
-rw-r--r--mobile/android/themes/core/config.css333
-rw-r--r--mobile/android/themes/core/content.css414
-rw-r--r--mobile/android/themes/core/defines.css18
-rw-r--r--mobile/android/themes/core/images/about-btn-darkgrey.pngbin0 -> 123 bytes
-rw-r--r--mobile/android/themes/core/images/accessiblecaret-normal-hdpi.pngbin0 -> 693 bytes
-rw-r--r--mobile/android/themes/core/images/accessiblecaret-normal-xhdpi.pngbin0 -> 924 bytes
-rw-r--r--mobile/android/themes/core/images/accessiblecaret-normal-xxhdpi.pngbin0 -> 1394 bytes
-rw-r--r--mobile/android/themes/core/images/accessiblecaret-tilt-left-hdpi.pngbin0 -> 532 bytes
-rw-r--r--mobile/android/themes/core/images/accessiblecaret-tilt-left-xhdpi.pngbin0 -> 706 bytes
-rw-r--r--mobile/android/themes/core/images/accessiblecaret-tilt-left-xxhdpi.pngbin0 -> 1140 bytes
-rw-r--r--mobile/android/themes/core/images/accessiblecaret-tilt-right-hdpi.pngbin0 -> 538 bytes
-rw-r--r--mobile/android/themes/core/images/accessiblecaret-tilt-right-xhdpi.pngbin0 -> 668 bytes
-rw-r--r--mobile/android/themes/core/images/accessiblecaret-tilt-right-xxhdpi.pngbin0 -> 1101 bytes
-rw-r--r--mobile/android/themes/core/images/amo-logo.pngbin0 -> 635 bytes
-rw-r--r--mobile/android/themes/core/images/arrowdown-16.pngbin0 -> 158 bytes
-rw-r--r--mobile/android/themes/core/images/arrowup-16.pngbin0 -> 151 bytes
-rw-r--r--mobile/android/themes/core/images/blocked-warning.pngbin0 -> 926 bytes
-rw-r--r--mobile/android/themes/core/images/cast-active.svg14
-rw-r--r--mobile/android/themes/core/images/cast-ready.svg14
-rw-r--r--mobile/android/themes/core/images/certerror-warning.pngbin0 -> 926 bytes
-rw-r--r--mobile/android/themes/core/images/checkbox_checked.pngbin0 -> 322 bytes
-rw-r--r--mobile/android/themes/core/images/checkbox_checked_disabled.pngbin0 -> 260 bytes
-rw-r--r--mobile/android/themes/core/images/checkbox_checked_pressed.pngbin0 -> 444 bytes
-rw-r--r--mobile/android/themes/core/images/checkbox_unchecked.pngbin0 -> 130 bytes
-rw-r--r--mobile/android/themes/core/images/checkbox_unchecked_disabled.pngbin0 -> 135 bytes
-rw-r--r--mobile/android/themes/core/images/checkbox_unchecked_pressed.pngbin0 -> 242 bytes
-rw-r--r--mobile/android/themes/core/images/chevron.pngbin0 -> 279 bytes
-rw-r--r--mobile/android/themes/core/images/config-plus.pngbin0 -> 255 bytes
-rw-r--r--mobile/android/themes/core/images/dropmarker-right.svg7
-rw-r--r--mobile/android/themes/core/images/dropmarker.svg7
-rw-r--r--mobile/android/themes/core/images/errorpage-warning.pngbin0 -> 926 bytes
-rw-r--r--mobile/android/themes/core/images/exitfullscreen.svg10
-rw-r--r--mobile/android/themes/core/images/fullscreen.svg10
-rw-r--r--mobile/android/themes/core/images/grey-caution.svg4
-rw-r--r--mobile/android/themes/core/images/icon_key_emptypage.svg11
-rw-r--r--mobile/android/themes/core/images/lock.pngbin0 -> 350 bytes
-rw-r--r--mobile/android/themes/core/images/logo-hdpi.pngbin0 -> 64040 bytes
-rw-r--r--mobile/android/themes/core/images/mute.svg10
-rw-r--r--mobile/android/themes/core/images/pause.svg10
-rw-r--r--mobile/android/themes/core/images/placeholder_image.svg62
-rw-r--r--mobile/android/themes/core/images/play.svg10
-rw-r--r--mobile/android/themes/core/images/privatebrowsing-mask-and-shield.svg22
-rw-r--r--mobile/android/themes/core/images/privatebrowsing-mask.pngbin0 -> 2872 bytes
-rw-r--r--mobile/android/themes/core/images/reader-minus-hdpi.pngbin0 -> 135 bytes
-rw-r--r--mobile/android/themes/core/images/reader-minus-xhdpi.pngbin0 -> 135 bytes
-rw-r--r--mobile/android/themes/core/images/reader-minus-xxhdpi.pngbin0 -> 192 bytes
-rw-r--r--mobile/android/themes/core/images/reader-plus-hdpi.pngbin0 -> 287 bytes
-rw-r--r--mobile/android/themes/core/images/reader-plus-xhdpi.pngbin0 -> 322 bytes
-rw-r--r--mobile/android/themes/core/images/reader-plus-xxhdpi.pngbin0 -> 429 bytes
-rw-r--r--mobile/android/themes/core/images/reader-style-icon-hdpi.pngbin0 -> 891 bytes
-rw-r--r--mobile/android/themes/core/images/reader-style-icon-xhdpi.pngbin0 -> 1179 bytes
-rw-r--r--mobile/android/themes/core/images/reader-style-icon-xxhdpi.pngbin0 -> 1791 bytes
-rw-r--r--mobile/android/themes/core/images/scrubber.svg10
-rw-r--r--mobile/android/themes/core/images/search-clear-30.pngbin0 -> 270 bytes
-rw-r--r--mobile/android/themes/core/images/search.pngbin0 -> 487 bytes
-rw-r--r--mobile/android/themes/core/images/textfield.pngbin0 -> 95 bytes
-rw-r--r--mobile/android/themes/core/images/throbber.pngbin0 -> 481 bytes
-rw-r--r--mobile/android/themes/core/images/unmute.svg10
-rw-r--r--mobile/android/themes/core/images/wordmark-hdpi.pngbin0 -> 798 bytes
-rw-r--r--mobile/android/themes/core/jar.mn98
-rw-r--r--mobile/android/themes/core/moz.build7
-rw-r--r--mobile/android/themes/core/netError.css226
-rw-r--r--mobile/android/themes/core/scrollbar-apz.css10
-rw-r--r--mobile/android/themes/core/spinner.css124
-rw-r--r--mobile/android/themes/core/touchcontrols.css255
-rw-r--r--mobile/android/thirdparty/AndroidManifest.xml4
-rw-r--r--mobile/android/thirdparty/README3
-rw-r--r--mobile/android/thirdparty/build.gradle54
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/ConnectionClosedException.java50
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/ConnectionReuseStrategy.java70
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/Consts.java51
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/ContentTooLongException.java50
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/FormattedHeader.java60
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/Header.java74
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/HeaderElement.java108
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/HeaderElementIterator.java57
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/HeaderIterator.java56
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpClientConnection.java102
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpConnection.java104
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpConnectionFactory.java42
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpConnectionMetrics.java77
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpEntity.java199
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpEntityEnclosingRequest.java60
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpException.java67
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpHeaders.java206
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpHost.java290
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpInetConnection.java47
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpMessage.java210
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpRequest.java53
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpRequestFactory.java43
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpRequestInterceptor.java68
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpResponse.java155
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpResponseFactory.java67
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpResponseInterceptor.java68
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpServerConnection.java88
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpStatus.java175
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpVersion.java110
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/MalformedChunkCodingException.java57
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/MessageConstraintException.java50
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/MethodNotSupportedException.java59
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/NameValuePair.java47
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/NoHttpResponseException.java50
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/ParseException.java61
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/ProtocolException.java67
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/ProtocolVersion.java264
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/README.txt1
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/ReasonPhraseCatalog.java51
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/RequestLine.java49
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/StatusLine.java52
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/TokenIterator.java59
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/TruncatedChunkException.java48
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/UnsupportedHttpVersionException.java57
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/androidextra/Base64.java741
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/androidextra/HttpClientAndroidLog.java113
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/annotation/GuardedBy.java76
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/annotation/Immutable.java59
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/annotation/NotThreadSafe.java50
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/annotation/ThreadSafe.java51
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/annotation/package-info.java34
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AUTH.java64
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthOption.java62
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthProtocolState.java33
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthScheme.java130
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthSchemeFactory.java51
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthSchemeProvider.java46
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthSchemeRegistry.java155
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthScope.java302
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthState.java235
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthenticationException.java70
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/BasicUserPrincipal.java89
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/ChallengeState.java38
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/ContextAwareAuthScheme.java62
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/Credentials.java44
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/InvalidCredentialsException.java69
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/MalformedChallengeException.java70
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/NTCredentials.java177
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/NTUserPrincipal.java113
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/UsernamePasswordCredentials.java120
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/package-info.java31
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/params/AuthPNames.java74
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/params/AuthParamBean.java55
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/params/AuthParams.java82
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/params/package-info.java32
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/AuthCache.java49
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/AuthenticationHandler.java101
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/AuthenticationStrategy.java130
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/BackoffManager.java54
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/CircularRedirectException.java68
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/ClientProtocolException.java61
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/ConnectionBackoffStrategy.java64
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/CookieStore.java71
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/CredentialsProvider.java71
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/HttpClient.java258
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/HttpRequestRetryHandler.java60
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/HttpResponseException.java52
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/NonRepeatableRequestException.java72
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/RedirectException.java69
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/RedirectHandler.java77
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/RedirectStrategy.java81
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/RequestDirector.java77
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/ResponseHandler.java54
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/ServiceUnavailableRetryStrategy.java60
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/UserTokenHandler.java58
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/CacheResponseStatus.java55
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HeaderConstants.java81
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheContext.java71
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheEntry.java263
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheEntrySerializationException.java48
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheEntrySerializer.java54
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheInvalidator.java58
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheStorage.java81
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheUpdateCallback.java52
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheUpdateException.java48
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/InputLimit.java76
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/Resource.java60
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/ResourceFactory.java67
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/package.html78
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/config/AuthSchemes.java71
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/config/CookieSpecs.java69
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/config/RequestConfig.java442
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/config/package-info.java31
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/DecompressingEntity.java105
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/DeflateDecompressingEntity.java96
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/DeflateInputStream.java228
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/EntityBuilder.java342
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/GzipCompressingEntity.java113
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/GzipDecompressingEntity.java80
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/LazyDecompressingInputStream.java105
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/UrlEncodedFormEntity.java107
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/package-info.java31
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/AbortableHttpRequest.java82
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/AbstractExecutionAwareRequest.java131
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/CloseableHttpResponse.java40
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/Configurable.java44
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpDelete.java77
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpEntityEnclosingRequestBase.java76
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpExecutionAware.java47
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpGet.java77
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpHead.java80
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpOptions.java100
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpPatch.java75
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpPost.java84
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpPut.java76
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpRequestBase.java124
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpRequestWrapper.java171
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpTrace.java79
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpUriRequest.java84
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/RequestBuilder.java351
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/package-info.java31
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/package-info.java31
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/AllClientPNames.java63
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/AuthPolicy.java79
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/ClientPNames.java133
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/ClientParamBean.java106
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/CookiePolicy.java80
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/HttpClientParamConfig.java88
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/HttpClientParams.java116
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/package-info.java32
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/ClientContext.java132
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/ClientContextConfigurer.java72
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/HttpClientContext.java249
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestAcceptEncoding.java62
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestAddCookies.java204
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestAuthCache.java147
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestAuthenticationBase.java126
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestClientConnControl.java92
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestDefaultHeaders.java89
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestExpectContinue.java82
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestProxyAuthentication.java92
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestTargetAuthentication.java83
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/ResponseAuthCache.java151
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/ResponseContentEncoding.java110
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/ResponseProcessCookies.java156
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/package-info.java31
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/CloneUtils.java86
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/DateUtils.java250
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/HttpClientUtils.java149
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/Idn.java42
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/JdkIdn.java71
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/Punycode.java54
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/Rfc3492Idn.java141
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/URIBuilder.java490
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/URIUtils.java428
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/URLEncodedUtils.java628
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/package-info.java31
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/concurrent/BasicFuture.java154
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/concurrent/Cancellable.java39
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/concurrent/FutureCallback.java44
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/concurrent/package-info.java31
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/ConnectionConfig.java192
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/Lookup.java40
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/MessageConstraints.java113
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/Registry.java63
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/RegistryBuilder.java72
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/SocketConfig.java197
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/package-info.java31
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/BasicEofSensorWatcher.java105
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/BasicManagedEntity.java208
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ClientConnectionManager.java117
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ClientConnectionManagerFactory.java47
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ClientConnectionOperator.java107
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ClientConnectionRequest.java74
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ConnectTimeoutException.java94
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ConnectionKeepAliveStrategy.java66
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ConnectionPoolTimeoutException.java60
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ConnectionReleaseTrigger.java70
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ConnectionRequest.java69
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/DnsResolver.java54
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/EofSensorInputStream.java289
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/EofSensorWatcher.java95
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/HttpClientConnectionManager.java176
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/HttpConnectionFactory.java41
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/HttpHostConnectException.java82
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/HttpInetSocketAddress.java65
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/HttpRoutedConnection.java81
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ManagedClientConnection.java228
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ManagedHttpClientConnection.java80
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/MultihomePlainSocketFactory.java173
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/OperatedClientConnection.java155
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/SchemePortResolver.java43
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/UnsupportedSchemeException.java51
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/package-info.java31
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnConnectionPNames.java64
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnConnectionParamBean.java59
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnManagerPNames.java67
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnManagerParamBean.java63
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnManagerParams.java147
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnPerRoute.java46
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnPerRouteBean.java112
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnRoutePNames.java79
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnRouteParamBean.java70
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnRouteParams.java178
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/package-info.java32
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/BasicRouteDirector.java181
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/HttpRoute.java328
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/HttpRouteDirector.java74
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/HttpRoutePlanner.java67
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/RouteInfo.java161
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/RouteTracker.java366
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/package-info.java31
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/HostNameResolver.java52
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/LayeredSchemeSocketFactory.java68
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/LayeredSocketFactory.java69
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/LayeredSocketFactoryAdaptor.java53
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/PlainSocketFactory.java160
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/Scheme.java260
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeLayeredSocketFactory.java69
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeLayeredSocketFactoryAdaptor.java57
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeLayeredSocketFactoryAdaptor2.java74
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeRegistry.java168
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeSocketFactory.java130
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeSocketFactoryAdaptor.java100
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SocketFactory.java127
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SocketFactoryAdaptor.java97
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/package-info.java32
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/socket/ConnectionSocketFactory.java80
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/socket/LayeredConnectionSocketFactory.java63
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/socket/PlainConnectionSocketFactory.java83
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/socket/package-info.java31
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/AbstractVerifier.java386
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/AllowAllHostnameVerifier.java54
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/BrowserCompatHostnameVerifier.java67
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/DistinguishedNameParser.java131
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/PrivateKeyDetails.java63
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/PrivateKeyStrategy.java44
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/SSLConnectionSocketFactory.java295
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/SSLContextBuilder.java259
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/SSLContexts.java90
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/SSLInitializationException.java37
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/SSLSocketFactory.java570
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/StrictHostnameVerifier.java69
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/TokenParser.java266
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/TrustSelfSignedStrategy.java45
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/TrustStrategy.java57
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/X509HostnameVerifier.java85
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/package-info.java31
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/util/InetAddressUtils.java123
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/util/package-info.java31
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/ClientCookie.java62
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/Cookie.java137
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieAttributeHandler.java73
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieIdentityComparator.java80
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieOrigin.java94
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookiePathComparator.java81
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieRestrictionViolationException.java61
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieSpec.java109
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieSpecFactory.java51
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieSpecProvider.java46
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieSpecRegistry.java167
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/MalformedCookieException.java71
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/SM.java43
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/SetCookie.java109
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/SetCookie2.java60
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/package-info.java31
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/params/CookieSpecPNames.java65
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/params/CookieSpecParamBean.java62
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/params/package-info.java32
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/AbstractHttpEntity.java191
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/BasicHttpEntity.java125
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/BufferedHttpEntity.java125
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/ByteArrayEntity.java131
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/ContentLengthStrategy.java57
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/ContentProducer.java44
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/ContentType.java305
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/EntityTemplate.java76
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/FileEntity.java121
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/HttpEntityWrapper.java105
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/InputStreamEntity.java155
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/SerializableEntity.java126
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/StringEntity.java188
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/AbstractMultipartForm.java211
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/FormBodyPart.java116
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/Header.java144
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/HttpBrowserCompatibleMultipart.java79
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/HttpMultipartMode.java43
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/HttpRFC6532Multipart.java72
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/HttpStrictMultipart.java72
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/MIME.java53
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/MinimalField.java63
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/MultipartEntityBuilder.java207
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/MultipartFormEntity.java100
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/AbstractContentBody.java96
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/ByteArrayBody.java111
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/ContentBody.java43
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/ContentDescriptor.java89
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/FileBody.java144
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/InputStreamBody.java112
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/StringBody.java197
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/package-info.java31
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/package-info.java31
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/package-info.java31
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/AbstractHttpClientConnection.java323
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/AbstractHttpServerConnection.java310
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/BHttpConnectionBase.java393
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/ConnSupport.java75
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultBHttpClientConnection.java182
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultBHttpClientConnectionFactory.java103
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultBHttpServerConnection.java174
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultBHttpServerConnectionFactory.java103
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultConnectionReuseStrategy.java189
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultHttpClientConnection.java70
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultHttpRequestFactory.java109
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultHttpResponseFactory.java109
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultHttpServerConnection.java71
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/EnglishReasonPhraseCatalog.java224
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/HttpConnectionMetricsImpl.java148
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/NoConnectionReuseStrategy.java53
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/SocketHttpClientConnection.java283
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/SocketHttpServerConnection.java271
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/AuthSchemeBase.java169
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/BasicScheme.java219
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/BasicSchemeFactory.java71
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/DigestScheme.java489
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/DigestSchemeFactory.java71
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/HttpAuthenticator.java245
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/HttpEntityDigester.java75
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/NTLMEngine.java70
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/NTLMEngineException.java67
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/NTLMEngineImpl.java1672
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/NTLMScheme.java164
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/NTLMSchemeFactory.java56
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/RFC2617Scheme.java151
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/SpnegoTokenGenerator.java47
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/UnsupportedDigestAlgorithmException.java69
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/package-info.java32
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AIMDBackoffManager.java164
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AbstractAuthenticationHandler.java189
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AbstractHttpClient.java990
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AuthenticationStrategyAdaptor.java172
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AuthenticationStrategyImpl.java245
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AutoRetryHttpClient.java190
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/BasicAuthCache.java105
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/BasicCookieStore.java144
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/BasicCredentialsProvider.java109
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/BasicResponseHandler.java73
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/ClientParamsStack.java269
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/Clock.java43
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/CloseableHttpClient.java244
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/CloseableHttpResponseProxy.java87
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/ContentEncodingHttpClient.java93
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DecompressingHttpClient.java214
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultBackoffStrategy.java54
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultConnectionKeepAliveStrategy.java71
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultHttpClient.java225
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultHttpRequestRetryHandler.java203
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultProxyAuthenticationHandler.java90
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultRedirectHandler.java180
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultRedirectStrategy.java233
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultRedirectStrategyAdaptor.java81
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultRequestDirector.java1150
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultServiceUnavailableRetryStrategy.java80
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultTargetAuthenticationHandler.java91
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultUserTokenHandler.java101
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/EntityEnclosingRequestWrapper.java113
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/FutureRequestExecutionMetrics.java156
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/FutureRequestExecutionService.java142
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/HttpAuthenticator.java61
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/HttpClientBuilder.java954
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/HttpClients.java85
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/HttpRequestFutureTask.java118
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/HttpRequestTaskCallable.java119
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/InternalHttpClient.java242
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/LaxRedirectStrategy.java65
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/MinimalHttpClient.java156
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/NoopUserTokenHandler.java47
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/NullBackoffStrategy.java47
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/ProxyAuthenticationStrategy.java57
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/ProxyClient.java254
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/RedirectLocations.java227
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/RequestWrapper.java164
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/RoutedRequest.java67
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/StandardHttpRequestRetryHandler.java80
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/SystemClock.java40
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/SystemDefaultCredentialsProvider.java145
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/SystemDefaultHttpClient.java149
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/TargetAuthenticationStrategy.java57
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/TunnelRefusedException.java58
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/AsynchronousValidationRequest.java178
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/AsynchronousValidator.java150
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/BasicHttpCache.java376
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/BasicHttpCacheStorage.java96
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/BasicIdGenerator.java86
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheConfig.java764
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheEntity.java99
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheEntryUpdater.java173
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheInvalidator.java288
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheKeyGenerator.java178
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheMap.java50
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheValidityPolicy.java320
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheableRequestPolicy.java96
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CachedHttpResponseGenerator.java166
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CachedResponseSuitabilityChecker.java346
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CachingExec.java870
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CachingHttpClientBuilder.java149
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CachingHttpClients.java74
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CombinedEntity.java104
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ConditionalRequestBuilder.java140
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/DefaultFailureCache.java143
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/DefaultHttpCacheEntrySerializer.java71
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ExponentialBackOffSchedulingStrategy.java174
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/FailureCache.java57
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/FailureCacheValue.java67
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/FileResource.java77
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/FileResourceFactory.java111
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/HeapResource.java67
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/HeapResourceFactory.java83
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/HttpCache.java166
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/IOUtils.java109
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ImmediateSchedulingStrategy.java88
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ManagedHttpCacheStorage.java163
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/OptionsHttp11Response.java182
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/Proxies.java56
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/RequestProtocolCompliance.java376
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/RequestProtocolError.java40
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ResourceReference.java62
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ResponseCachingPolicy.java311
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ResponseProtocolCompliance.java251
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ResponseProxyHandler.java88
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/SchedulingStrategy.java45
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/SizeLimitedResponseReader.java150
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/Variant.java55
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/WarningValue.java370
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/package.html42
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/package-info.java51
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/AbstractClientConnAdapter.java369
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/AbstractPoolEntry.java262
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/AbstractPooledConnAdapter.java191
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/BasicClientConnectionManager.java276
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/BasicHttpClientConnectionManager.java370
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/CPool.java67
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/CPoolEntry.java101
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/CPoolProxy.java245
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/ConnectionShutdownException.java50
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultClientConnection.java292
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultClientConnectionOperator.java263
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultHttpResponseParser.java168
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultHttpResponseParserFactory.java77
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultHttpRoutePlanner.java123
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultManagedHttpClientConnection.java135
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultProxyRoutePlanner.java66
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultResponseParser.java125
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultRoutePlanner.java107
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultSchemePortResolver.java61
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/HttpClientConnectionOperator.java173
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/HttpConnPool.java84
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/HttpPoolEntry.java98
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/IdleConnectionHandler.java181
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/InMemoryDnsResolver.java94
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/LoggingInputStream.java145
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/LoggingManagedHttpClientConnection.java132
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/LoggingOutputStream.java104
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/LoggingSessionInputBuffer.java138
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/LoggingSessionOutputBuffer.java118
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/ManagedClientConnectionImpl.java461
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/ManagedHttpClientConnectionFactory.java121
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/PoolingClientConnectionManager.java328
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/PoolingHttpClientConnectionManager.java516
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/ProxySelectorRoutePlanner.java279
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/SchemeRegistryFactory.java90
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/SingleClientConnManager.java427
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/SystemDefaultDnsResolver.java47
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/SystemDefaultRoutePlanner.java132
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/Wire.java152
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/package-info.java32
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/AbstractConnPool.java234
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/BasicPoolEntry.java163
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/BasicPoolEntryRef.java76
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/BasicPooledConnAdapter.java75
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/ConnPoolByRoute.java829
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/PoolEntryRequest.java69
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/RouteSpecificPool.java313
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/ThreadSafeClientConnManager.java377
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/WaitingThread.java198
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/WaitingThreadAborter.java69
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/package-info.java33
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/AbstractCookieAttributeHandler.java52
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/AbstractCookieSpec.java104
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicClientCookie.java361
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicClientCookie2.java101
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicCommentHandler.java51
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicDomainHandler.java116
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicExpiresHandler.java66
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicMaxAgeHandler.java67
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicPathHandler.java87
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicSecureHandler.java60
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BestMatchSpec.java207
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BestMatchSpecFactory.java86
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BrowserCompatSpec.java219
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BrowserCompatSpecFactory.java92
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BrowserCompatVersionAttributeHandler.java66
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/CookieSpecBase.java121
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/DateParseException.java62
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/DateUtils.java156
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/IgnoreSpec.java63
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/IgnoreSpecFactory.java58
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/NetscapeDomainHandler.java106
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/NetscapeDraftHeaderParser.java138
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/NetscapeDraftSpec.java171
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/NetscapeDraftSpecFactory.java81
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/PublicSuffixFilter.java132
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/PublicSuffixListParser.java127
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2109DomainHandler.java120
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2109Spec.java240
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2109SpecFactory.java86
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2109VersionHandler.java74
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965CommentUrlAttributeHandler.java66
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965DiscardAttributeHandler.java66
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965DomainAttributeHandler.java185
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965PortAttributeHandler.java160
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965Spec.java239
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965SpecFactory.java86
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965VersionAttributeHandler.java94
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/package-info.java32
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/DisallowIdentityContentLengthStrategy.java63
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/EntityDeserializer.java143
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/EntitySerializer.java121
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/LaxContentLengthStrategy.java126
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/StrictContentLengthStrategy.java116
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/package-info.java31
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/BackoffStrategyExec.java104
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/ClientExecChain.java74
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/ConnectionHolder.java153
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/HttpResponseProxy.java185
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/MainClientExec.java568
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/MinimalClientExec.java251
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/ProtocolExec.java214
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/RedirectExec.java185
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/RequestAbortedException.java55
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/RequestEntityProxy.java137
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/ResponseEntityProxy.java152
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/RetryExec.java126
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/ServiceUnavailableRetryExec.java108
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/TunnelRefusedException.java55
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/package-info.java31
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/AbstractMessageParser.java284
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/AbstractMessageWriter.java119
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/AbstractSessionInputBuffer.java401
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/AbstractSessionOutputBuffer.java307
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/ChunkedInputStream.java301
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/ChunkedOutputStream.java208
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/ContentLengthInputStream.java232
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/ContentLengthOutputStream.java136
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestParser.java140
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestParserFactory.java71
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestWriter.java69
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestWriterFactory.java63
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseParser.java141
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseParserFactory.java71
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseWriter.java69
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseWriterFactory.java63
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/HttpRequestParser.java102
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/HttpRequestWriter.java62
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/HttpResponseParser.java103
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/HttpResponseWriter.java62
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/HttpTransportMetricsImpl.java63
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/IdentityInputStream.java99
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/IdentityOutputStream.java104
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/SessionInputBufferImpl.java399
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/SessionOutputBufferImpl.java283
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/SocketInputBuffer.java108
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/SocketOutputBuffer.java75
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/package-info.java32
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/package-info.java32
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/pool/BasicConnFactory.java177
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/pool/BasicConnPool.java89
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/pool/BasicPoolEntry.java64
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/pool/package-info.java32
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/BufferInfo.java58
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/EofSensor.java42
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/HttpMessageParser.java56
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/HttpMessageParserFactory.java42
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/HttpMessageWriter.java54
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/HttpMessageWriterFactory.java41
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/HttpTransportMetrics.java48
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/SessionInputBuffer.java152
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/SessionOutputBuffer.java120
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/package-info.java32
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/AbstractHttpMessage.java164
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeader.java91
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeaderElement.java162
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeaderElementIterator.java151
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeaderIterator.java175
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeaderValueFormatter.java422
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeaderValueParser.java359
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHttpEntityEnclosingRequest.java75
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHttpRequest.java114
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHttpResponse.java218
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicLineFormatter.java319
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicLineParser.java456
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicListHeaderIterator.java190
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicNameValuePair.java111
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicRequestLine.java83
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicStatusLine.java100
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicTokenIterator.java414
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BufferedHeader.java130
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/HeaderGroup.java311
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/HeaderValueFormatter.java122
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/HeaderValueParser.java134
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/LineFormatter.java131
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/LineParser.java137
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/ParserCursor.java100
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/package-info.java32
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/package-info.java42
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/AbstractHttpParams.java124
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/BasicHttpParams.java190
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/CoreConnectionPNames.java170
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/CoreProtocolPNames.java152
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/DefaultedHttpParams.java163
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpAbstractParamBean.java48
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpConnectionParamBean.java71
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpConnectionParams.java243
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpParamConfig.java78
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpParams.java195
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpParamsNames.java57
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpProtocolParamBean.java69
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpProtocolParams.java240
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/SyncBasicHttpParams.java89
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/package-info.java32
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/AbstractConnPool.java533
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/ConnFactory.java44
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/ConnPool.java68
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/ConnPoolControl.java56
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/PoolEntry.java183
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/PoolEntryCallback.java41
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/PoolEntryFuture.java155
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/PoolStats.java114
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/RouteSpecificPool.java184
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/package-info.java32
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/BasicHttpContext.java95
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/BasicHttpProcessor.java246
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ChainBuilder.java126
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/DefaultedHttpContext.java84
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ExecutionContext.java80
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HTTP.java135
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpContext.java75
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpCoreContext.java152
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpDateGenerator.java77
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpExpectationVerifier.java81
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpProcessor.java55
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpProcessorBuilder.java151
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestExecutor.java311
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestHandler.java62
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestHandlerMapper.java50
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestHandlerRegistry.java107
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestHandlerResolver.java51
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestInterceptorList.java103
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpResponseInterceptorList.java103
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpService.java447
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ImmutableHttpProcessor.java143
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestConnControl.java69
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestContent.java127
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestDate.java65
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestExpectContinue.java93
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestTargetHost.java95
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestUserAgent.java79
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ResponseConnControl.java105
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ResponseContent.java133
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ResponseDate.java66
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ResponseServer.java71
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/SyncBasicHttpContext.java74
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/UriHttpRequestHandlerMapper.java115
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/UriPatternMatcher.java165
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/package-info.java32
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/Args.java111
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/Asserts.java62
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/ByteArrayBuffer.java347
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/CharArrayBuffer.java464
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/CharsetUtils.java58
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/EncodingUtils.java152
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/EntityUtils.java291
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/ExceptionUtils.java82
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/LangUtils.java101
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/NetUtils.java54
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/TextUtils.java54
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/VersionInfo.java324
-rw-r--r--mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/package-info.java31
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/ActivityHandler.java781
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/ActivityKind.java35
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/ActivityPackage.java100
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/ActivityState.java151
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/Adjust.java79
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/AdjustAttribution.java62
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/AdjustConfig.java128
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/AdjustEvent.java112
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/AdjustFactory.java141
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/AdjustInstance.java86
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/AdjustReferrerReceiver.java35
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/AttributionHandler.java155
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/Constants.java53
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/DeviceInfo.java290
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/IActivityHandler.java36
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/IAttributionHandler.java20
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/ILogger.java20
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/IPackageHandler.java27
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/IRequestHandler.java9
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/LICENSE21
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/LogLevel.java19
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/Logger.java107
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/OnAttributionChangedListener.java5
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/PackageBuilder.java291
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/PackageHandler.java274
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/Reflection.java210
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/RequestHandler.java210
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/UnitTestActivity.java38
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/Util.java202
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/plugin/AndroidIdUtil.java10
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/plugin/MacAddressUtil.java82
-rw-r--r--mobile/android/thirdparty/com/adjust/sdk/plugin/Plugin.java12
-rw-r--r--mobile/android/thirdparty/com/jakewharton/disklrucache/DiskLruCache.java943
-rw-r--r--mobile/android/thirdparty/com/jakewharton/disklrucache/StrictLineReader.java196
-rw-r--r--mobile/android/thirdparty/com/jakewharton/disklrucache/Util.java77
-rw-r--r--mobile/android/thirdparty/com/keepsafe/switchboard/AsyncConfigLoader.java54
-rw-r--r--mobile/android/thirdparty/com/keepsafe/switchboard/DeviceUuidFactory.java70
-rw-r--r--mobile/android/thirdparty/com/keepsafe/switchboard/Preferences.java105
-rw-r--r--mobile/android/thirdparty/com/keepsafe/switchboard/Switch.java72
-rw-r--r--mobile/android/thirdparty/com/keepsafe/switchboard/SwitchBoard.java390
-rw-r--r--mobile/android/thirdparty/com/squareup/leakcanary/LeakCanary.java21
-rw-r--r--mobile/android/thirdparty/com/squareup/leakcanary/RefWatcher.java20
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Action.java83
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/AssetBitmapHunter.java51
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/BitmapHunter.java357
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Cache.java64
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Callback.java31
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/ContactsPhotoBitmapHunter.java130
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/ContentStreamBitmapHunter.java67
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/DeferredRequestCreator.java70
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Dispatcher.java315
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Downloader.java99
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/FetchAction.java30
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/FileBitmapHunter.java57
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/GetAction.java30
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/ImageViewAction.java75
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/LruCache.java146
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/MarkableInputStream.java157
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/MediaStoreBitmapHunter.java116
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/NetworkBitmapHunter.java113
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Picasso.java522
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/PicassoDrawable.java186
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/PicassoExecutorService.java81
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Request.java307
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/RequestCreator.java374
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/ResourceBitmapHunter.java55
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Stats.java143
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/StatsSnapshot.java120
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Target.java45
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/TargetAction.java46
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Transformation.java34
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/UrlConnectionDownloader.java100
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Utils.java304
-rw-r--r--mobile/android/thirdparty/org/json/simple/ItemList.java147
-rw-r--r--mobile/android/thirdparty/org/json/simple/JSONArray.java107
-rw-r--r--mobile/android/thirdparty/org/json/simple/JSONAware.java12
-rw-r--r--mobile/android/thirdparty/org/json/simple/JSONObject.java129
-rw-r--r--mobile/android/thirdparty/org/json/simple/JSONStreamAware.java15
-rw-r--r--mobile/android/thirdparty/org/json/simple/JSONValue.java272
-rw-r--r--mobile/android/thirdparty/org/json/simple/LICENSE.txt202
-rw-r--r--mobile/android/thirdparty/org/json/simple/parser/ContainerFactory.java23
-rw-r--r--mobile/android/thirdparty/org/json/simple/parser/ContentHandler.java110
-rw-r--r--mobile/android/thirdparty/org/json/simple/parser/JSONParser.java533
-rw-r--r--mobile/android/thirdparty/org/json/simple/parser/ParseException.java96
-rw-r--r--mobile/android/thirdparty/org/json/simple/parser/Yylex.java688
-rw-r--r--mobile/android/thirdparty/org/json/simple/parser/Yytoken.java58
-rw-r--r--mobile/android/thirdparty/org/lucasr/dspec/DesignSpec.java645
-rw-r--r--mobile/android/thirdparty/org/lucasr/dspec/RawResource.java60
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/BinaryDecoder.java43
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/BinaryEncoder.java43
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/CharEncoding.java127
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/Decoder.java56
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/DecoderException.java90
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/Encoder.java47
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/EncoderException.java91
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/StringDecoder.java41
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/StringEncoder.java41
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/StringEncoderComparator.java87
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base32.java471
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base32InputStream.java85
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base32OutputStream.java85
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base64.java756
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base64InputStream.java89
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base64OutputStream.java89
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/BaseNCodec.java445
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/BaseNCodecInputStream.java132
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/BaseNCodecOutputStream.java142
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/BinaryCodec.java297
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Hex.java302
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/StringUtils.java287
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/package.html21
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/digest/DigestUtils.java583
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/digest/package.html21
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/AbstractCaverphone.java78
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/Caverphone.java104
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/Caverphone1.java126
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/Caverphone2.java129
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/ColognePhonetic.java417
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/DoubleMetaphone.java1106
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/Metaphone.java408
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/RefinedSoundex.java203
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/Soundex.java279
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/SoundexUtils.java124
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/package.html21
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/BCodec.java209
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/QCodec.java312
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/QuotedPrintableCodec.java388
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/RFC1522Codec.java179
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/URLCodec.java362
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/Utils.java50
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/package.html23
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/overview.html29
-rw-r--r--mobile/android/thirdparty/org/mozilla/apache/commons/codec/package.html100
-rw-r--r--mobile/locales/Makefile.in151
-rw-r--r--mobile/locales/en-US/chrome/region.properties96
-rw-r--r--mobile/locales/en-US/overrides/appstrings.properties41
-rw-r--r--mobile/locales/en-US/overrides/netError.dtd222
-rw-r--r--mobile/locales/en-US/overrides/passwordmgr.properties22
-rw-r--r--mobile/locales/en-US/searchplugins/amazondotcom.xml16
-rw-r--r--mobile/locales/en-US/searchplugins/bing.xml24
-rw-r--r--mobile/locales/en-US/searchplugins/duckduckgo.xml23
-rw-r--r--mobile/locales/en-US/searchplugins/google-nocodes.xml16
-rw-r--r--mobile/locales/en-US/searchplugins/google.xml17
-rw-r--r--mobile/locales/en-US/searchplugins/list.txt8
-rw-r--r--mobile/locales/en-US/searchplugins/qwant.xml18
-rw-r--r--mobile/locales/en-US/searchplugins/twitter.xml13
-rw-r--r--mobile/locales/en-US/searchplugins/wikipedia.xml23
-rw-r--r--mobile/locales/en-US/searchplugins/yahoo.xml45
-rw-r--r--mobile/locales/filter.py45
-rw-r--r--mobile/locales/jar.mn18
-rw-r--r--mobile/locales/l10n.ini19
-rw-r--r--mobile/locales/moz.build7
4246 files changed, 468237 insertions, 0 deletions
diff --git a/mobile/android/.eslintrc b/mobile/android/.eslintrc
new file mode 100644
index 0000000000..ce106c2d11
--- /dev/null
+++ b/mobile/android/.eslintrc
@@ -0,0 +1,110 @@
+env:
+ browser: true
+
+globals:
+ Components: false
+
+ # TODO: Create custom rule for `Cu.import`
+ AddonManager: false
+ AppConstants: false
+ Downloads: false
+ File: false
+ FileUtils: false
+ HelperApps: true # TODO: Can be more specific here.
+ JNI: true # TODO: Can be more specific here.
+ LightweightThemeManager: false
+ Messaging: false
+ Notifications: false
+ OS: false
+ ParentalControls: false
+ PrivateBrowsingUtils: false
+ Prompt: false
+ Services: false
+ SharedPreferences: false
+ strings: false
+ Strings: false
+ Task: false
+ TelemetryStopwatch: false
+ UITelemetry: false
+ UserAgentOverrides: 0
+ XPCOMUtils: false
+ ctypes: false
+ dump: false
+ exports: false
+ importScripts: false
+ module: false
+ require: false
+ uuidgen: false
+
+ Iterator: false # TODO: Remove - deprecated!
+
+rules:
+ global-strict: 0 # Overridden by "strict"
+ no-underscore-dangle: 0 # We allow trailing underscores in names.
+
+ # We disable everything to get all files to pass w/o updating them.
+ # We'll re-enable one by one.
+ camelcase: 0
+ comma-dangle: 0
+ comma-spacing: 0
+ consistent-return: 0
+ curly: 0
+ dot-notation: 0
+ eqeqeq: 0
+ key-spacing: 0
+ new-cap: 0
+ no-caller: 0
+ no-constant-condition: 0
+ no-empty: 0
+ no-extra-bind: 0
+ no-extra-semi: 0
+ no-loop-func: 0
+ no-multi-spaces: 0
+ no-new-object: 0
+ no-octal: 0
+ no-return-assign: 0
+ no-shadow: 0
+ no-trailing-spaces: 0
+ no-unused-vars: 0
+ no-use-before-define: 0
+ quotes: 0 # [2, "double"]
+ semi: 0
+ space-infix-ops: 0
+ space-unary-ops: 0 # 2: https://github.com/eslint/eslint/issues/2764
+ strict: 0
+
+ #"ecmaFeatures": {
+ # "forOf": true,
+ # "jsx": true,
+ #},
+ #"rules": {
+ # // turn off all kinds of stuff that we actually do want, because
+ # // right now, we're bootstrapping the linting infrastructure. We'll
+ # // want to audit these rules, and start turning them on and fixing the
+ # // problems they find, one at a time.
+
+ # // Eslint built-in rules are documented at <http://eslint.org/docs/rules/>
+ # "camelcase": 0, // TODO: Remove (use default)
+ # "consistent-return": 0, // TODO: Remove (use default)
+ # dot-location: 0, // [2, property],
+ # "eqeqeq": 0, // TBD. Might need to be separate for content & chrome
+ # "global-strict": 0, // Leave as zero (this will be unsupported in eslint 1.0.0)
+ # "linebreak-style": [2, "unix"],
+ # "new-cap": 0, // TODO: Remove (use default)
+ # "no-catch-shadow": 0, // TODO: Remove (use default)
+ # "no-console": 0, // Leave as 0. We use console logging in content code.
+ # "no-empty": 0, // TODO: Remove (use default)
+ # "no-extra-bind": 0, // Leave as 0
+ # "no-extra-boolean-cast": 0, // TODO: Remove (use default)
+ # "no-multi-spaces": 0, // TBD.
+ # "no-new": 0, // TODO: Remove (use default)
+ # "no-redeclare": 0, // TODO: Remove (use default)
+ # "no-return-assign": 0, // TODO: Remove (use default)
+ # "no-underscore-dangle": 0, // Leave as 0. Commonly used for private variables.
+ # "no-unneeded-ternary": 2,
+ # "no-unused-expressions": 0, // TODO: Remove (use default)
+ # "no-unused-vars": 0, // TODO: Remove (use default)
+ # "no-use-before-define": 0, // TODO: Remove (use default)
+ # "quotes": [2, "double", "avoid-escape"],
+ # "strict": 0, // [2, "function"],
+ #}
diff --git a/mobile/android/LICENSE b/mobile/android/LICENSE
new file mode 100644
index 0000000000..14e2f777f6
--- /dev/null
+++ b/mobile/android/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
diff --git a/mobile/android/Makefile.in b/mobile/android/Makefile.in
new file mode 100644
index 0000000000..e86aaf15ab
--- /dev/null
+++ b/mobile/android/Makefile.in
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+include $(topsrcdir)/config/rules.mk
+include $(topsrcdir)/testing/testsuite-targets.mk
+
+package-mobile-tests:
+ $(MAKE) stage-mochitest DIST_BIN=$(DEPTH)/$(DIST)/bin/xulrunner
+ $(NSINSTALL) -D $(DIST)/$(PKG_PATH)
+ @(cd $(PKG_STAGE) && tar $(TAR_CREATE_FLAGS) - *) | bzip2 -f > $(DIST)/$(PKG_PATH)$(TEST_PACKAGE)
diff --git a/mobile/android/app.mozbuild b/mobile/android/app.mozbuild
new file mode 100644
index 0000000000..b29d15c077
--- /dev/null
+++ b/mobile/android/app.mozbuild
@@ -0,0 +1,17 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+include('/toolkit/toolkit.mozbuild')
+
+if CONFIG['ENABLE_TESTS']:
+ DIRS += ['/testing/instrumentation']
+
+if CONFIG['MOZ_EXTENSIONS']:
+ DIRS += ['/extensions']
+
+DIRS += [
+ '/%s' % CONFIG['MOZ_BRANDING_DIRECTORY'],
+ '/mobile/android',
+]
diff --git a/mobile/android/app/assets/example_asset.txt b/mobile/android/app/assets/example_asset.txt
new file mode 100644
index 0000000000..34338f983e
--- /dev/null
+++ b/mobile/android/app/assets/example_asset.txt
@@ -0,0 +1 @@
+This is an example asset.
diff --git a/mobile/android/app/assets/parental_controls_theme.png b/mobile/android/app/assets/parental_controls_theme.png
new file mode 100644
index 0000000000..64d76469eb
--- /dev/null
+++ b/mobile/android/app/assets/parental_controls_theme.png
Binary files differ
diff --git a/mobile/android/app/assets/publicsuffixlist b/mobile/android/app/assets/publicsuffixlist
new file mode 100644
index 0000000000..7f834b1a89
--- /dev/null
+++ b/mobile/android/app/assets/publicsuffixlist
@@ -0,0 +1,8406 @@
+xn--9krt00a
+xn--fjq720a
+xn--ngbe9e0a
+xn--mgba7c0bbn0a
+xn--80ao21a
+xn--80aqecdr1a
+xn--xkc2al3hye2a
+xn--9dbq2a
+xn--1qqw23a
+xn--8y0a063a
+xn--kcrx77d1x4a
+xn--mgba3a4f16a
+xn--nyqy26a
+xn--3oq18vl8pn36a
+xn--t60b56a
+xn--wgbl6a
+xn--nnx388a
+xn--42c2d9a
+aaa
+tiaa
+ba
+org.ba
+mil.ba
+com.ba
+net.ba
+blogspot.ba
+edu.ba
+gov.ba
+alibaba
+cba
+toshiba
+mba
+nba
+ca
+ab.ca
+mb.ca
+nb.ca
+bc.ca
+gc.ca
+qc.ca
+pe.ca
+nf.ca
+sk.ca
+yk.ca
+nl.ca
+on.ca
+co.ca
+no-ip.ca
+ns.ca
+nt.ca
+blogspot.ca
+nu.ca
+amica
+telefonica
+pramerica
+africa
+corsica
+avianca
+sca
+stada
+tienda
+honda
+moda
+asda
+ltda
+gea
+cfa
+bofa
+ga
+omega
+ipiranga
+yoga
+doha
+shiksha
+lancia
+media
+shia
+kia
+nokia
+xperia
+asia
+ninja
+osaka
+vodka
+edeka
+otsuka
+la
+c.la
+org.la
+com.la
+info.la
+per.la
+bnr.la
+net.la
+int.la
+edu.la
+gov.la
+shangrila
+cuisinella
+redumbrella
+ma
+ac.ma
+org.ma
+co.ma
+press.ma
+net.ma
+gov.ma
+yokohama
+xn--nqv7fs00ema
+mma
+na
+ca.na
+cc.na
+name.na
+org.na
+mobi.na
+school.na
+com.na
+in.na
+co.na
+info.na
+pro.na
+dr.na
+or.na
+us.na
+ws.na
+tv.na
+mx.na
+vana
+sina
+barcelona
+aetna
+pa
+gob.pa
+ac.pa
+med.pa
+sld.pa
+ing.pa
+org.pa
+com.pa
+nom.pa
+abo.pa
+net.pa
+edu.pa
+xn--mgbayh7gpa
+arpa
+e164.arpa
+ip6.arpa
+uri.arpa
+urn.arpa
+in-addr.arpa
+iris.arpa
+qa
+name.qa
+org.qa
+sch.qa
+mil.qa
+com.qa
+net.qa
+blogspot.qa
+edu.qa
+gov.qa
+zara
+camera
+xn--mgba3a4fra
+nra
+xn--w4r85el8fhu5dnra
+sakura
+natura
+sa
+pub.sa
+med.sa
+org.sa
+sch.sa
+com.sa
+net.sa
+edu.sa
+gov.sa
+casa
+visa
+data
+athleta
+delta
+toyota
+vista
+ua
+vinnica.ua
+crimea.ua
+zaporizhzhia.ua
+vinnytsia.ua
+odesa.ua
+odessa.ua
+yalta.ua
+poltava.ua
+sb.ua
+dominic.ua
+kirovograd.ua
+od.ua
+uzhgorod.ua
+zaporizhzhe.ua
+rivne.ua
+te.ua
+if.ua
+lg.ua
+org.ua
+kh.ua
+chernivtsi.ua
+khmelnytskyi.ua
+ck.ua
+mk.ua
+lugansk.ua
+donetsk.ua
+lutsk.ua
+ivano-frankivsk.ua
+dnepropetrovsk.ua
+dnipropetrovsk.ua
+ternopil.ua
+sebastopol.ua
+sevastopol.ua
+pl.ua
+km.ua
+com.ua
+sm.ua
+krym.ua
+cn.ua
+dn.ua
+in.ua
+kherson.ua
+vn.ua
+volyn.ua
+co.ua
+rovno.ua
+dp.ua
+pp.ua
+zp.ua
+cr.ua
+zhitomir.ua
+kr.ua
+zhytomyr.ua
+ks.ua
+net.ua
+lt.ua
+zt.ua
+edu.ua
+cv.ua
+nikolaev.ua
+kiev.ua
+mykolaiv.ua
+chernihiv.ua
+kharkiv.ua
+lviv.ua
+kyiv.ua
+kv.ua
+lv.ua
+gov.ua
+chernigov.ua
+kharkov.ua
+rv.ua
+khmelnitskiy.ua
+sumy.ua
+cherkasy.ua
+cherkassy.ua
+chernovtsy.ua
+biz.ua
+uz.ua
+va
+java
+bbva
+teva
+viva
+okinawa
+axa
+lacaixa
+nagoya
+web.za
+ac.za
+agric.za
+org.za
+mil.za
+school.za
+nom.za
+tm.za
+co.za
+blogspot.co.za
+ngo.za
+grondar.za
+nis.za
+net.za
+alt.za
+edu.za
+gov.za
+law.za
+pizza
+xn--mgbai9a5eva00b
+xn--xhq521b
+xn--vuq861b
+xn--1ck2e1b
+xn--mgbtx2b
+xn--cck2b3b
+xn--d1acj3b
+xn--zfr164b
+xn--fiq64b
+xn--czr694b
+xn--jlq61u9w7b
+xn--rovu88b
+xn--mgbx4cd0ab
+cab
+rehab
+nab
+arab
+tab
+bb
+store.bb
+org.bb
+com.bb
+co.bb
+info.bb
+net.bb
+edu.bb
+gov.bb
+tv.bb
+biz.bb
+abb
+jcb
+scb
+xn--80asehdb
+imdb
+ceb
+gb
+lb
+org.lb
+com.lb
+net.lb
+edu.lb
+gov.lb
+mlb
+xn--mgbb9fbpob
+sb
+org.sb
+com.sb
+net.sb
+edu.sb
+gov.sb
+xn--vermgensberater-ctb
+starhub
+club
+samsclub
+pub
+xn--vermgensberatung-pwb
+xn--45q11c
+xn--wgbh1c
+xn--fzc2c9e2c
+xn--mk1bu44c
+xn--e1a4c
+xn--q9jyb4c
+xn--bck1b9a5dre4c
+xn--pbt977c
+xn--g2xx48c
+xn--h2brj9c
+xn--45brj9c
+xn--s9brj9c
+xn--gecrj9c
+ac
+org.ac
+mil.ac
+com.ac
+net.ac
+edu.ac
+gov.ac
+xn--90a3ac
+xn--o1ac.xn--90a3ac
+xn--c1avg.xn--90a3ac
+xn--o1ach.xn--90a3ac
+xn--90azh.xn--90a3ac
+xn--d1at.xn--90a3ac
+xn--80au.xn--90a3ac
+adac
+abc
+bbc
+icbc
+xn--mgbqly7c0a67fbc
+hsbc
+cc
+fantasyleague.cc
+scrapping.cc
+game-server.cc
+myphotos.cc
+ftpaccess.cc
+xn--54b7fta0cc
+xn--l1acc
+ec
+k12.ec
+gob.ec
+med.ec
+org.ec
+mil.ec
+com.ec
+fin.ec
+info.ec
+pro.ec
+net.ec
+edu.ec
+gov.ec
+quebec
+nec
+comsec
+symantec
+hdfc
+bananarepublic
+catholic
+organic
+clinic
+panasonic
+citic
+lc
+org.lc
+com.lc
+co.lc
+net.lc
+edu.lc
+gov.lc
+oy.lc
+jlc
+mc
+tm.mc
+asso.mc
+xn--qcka1pmc
+nc
+asso.nc
+montblanc
+pnc
+mtpc
+leclerc
+sc
+org.sc
+com.sc
+net.sc
+edu.sc
+gov.sc
+csc
+tc
+htc
+stc
+wtc
+vc
+org.vc
+mil.vc
+com.vc
+net.vc
+edu.vc
+gov.vc
+qvc
+iwc
+pwc
+nyc
+xn--czru2d
+xn--kprw13d
+xn--11b4c3d
+xn--fpcrj9c3d
+xn--55qx5d
+xn--kpry57d
+xn--eckvdtc9d
+ad
+nom.ad
+dad
+read
+download
+xn--mgbab2bd
+cd
+gov.cd
+xn--clchc0ea0b2g2a9gcd
+mcd
+med
+clubmed
+red
+kred
+exposed
+limited
+wed
+cfd
+gd
+xn--mgbt3dhd
+phd
+thd
+id
+desa.id
+web.id
+ac.id
+sch.id
+mil.id
+co.id
+blogspot.co.id
+go.id
+or.id
+net.id
+my.id
+biz.id
+raid
+bid
+android
+pid
+madrid
+build
+gold
+world
+md
+blogspot.md
+band
+land
+static.land
+sites.static.land
+dev.static.land
+newholland
+saarland
+bond
+fund
+food
+prod
+creditcard
+barclaycard
+vanguard
+xn--b4w605ferd
+krd
+co.krd
+edu.krd
+ford
+sd
+med.sd
+org.sd
+com.sd
+info.sd
+net.sd
+edu.sd
+gov.sd
+tv.sd
+msd
+merckmsd
+td
+blogspot.td
+ltd
+cloud
+xn--ngbc5azd
+xn--flw351e
+xn--mgbbh1a71e
+xn--gk3at1e
+xn--i1b6b1a6a2e
+xn--hxt814e
+xn--3e0b707e
+ae
+ac.ae
+org.ae
+sch.ae
+mil.ae
+co.ae
+net.ae
+blogspot.ae
+gov.ae
+xn--90ae
+be
+ac.be
+blogspot.be
+latrobe
+tube
+youtube
+place
+space
+stackspace.space
+extraspace
+alsace
+ice
+office
+dance
+reliance
+finance
+esurance
+insurance
+lifeinsurance
+travelersinsurance
+science
+airforce
+de
+traeumtgerade.de
+dnshome.de
+isteingeek.de
+com.de
+leitungsen.de
+istmein.de
+goip.de
+logoip.de
+blogspot.de
+lebtimnetz.de
+fuettertdasnetz.de
+glade
+trade
+woodside
+onyourside
+guide
+nationwide
+linde
+xn--node
+ee
+lib.ee
+med.ee
+fie.ee
+org.ee
+pri.ee
+riik.ee
+com.ee
+blogspot.com.ee
+aip.ee
+edu.ee
+gov.ee
+ieee
+coffee
+ggee
+free
+degree
+cafe
+safe
+life
+metlife
+ge
+org.ge
+mil.ge
+com.ge
+net.ge
+pvt.ge
+edu.ge
+gov.ge
+fage
+mortgage
+page
+storage
+voyage
+dodge
+college
+exchange
+orange
+george
+guge
+ie
+blogspot.ie
+gov.ie
+politie
+abbvie
+movie
+je
+org.je
+co.je
+net.je
+blogspot.co.ke
+bike
+like
+nike
+firmdale
+sale
+forsale
+able
+bible
+audible
+oracle
+circle
+kindle
+gle
+google
+mobile
+smile
+lasalle
+aquarelle
+mutuelle
+apple
+schule
+style
+lifestyle
+me
+i234.me
+brasilia.me
+ac.me
+dscloud.me
+daplie.me
+org.me
+diskstation.me
+co.me
+loginto.me
+hopto.me
+noip.me
+webhop.me
+dnsfor.me
+myds.me
+ddns.me
+its.me
+net.me
+edu.me
+priv.me
+gov.me
+synology.me
+game
+name
+forgot.her.name
+forgot.his.name
+meme
+prime
+showtime
+lancome
+rightathome
+chrome
+wme
+ne
+cologne
+online
+wine
+one
+phone
+capitalone
+redstone
+bridgestone
+firestone
+zone
+melbourne
+chloe
+moe
+pe
+gob.pe
+org.pe
+mil.pe
+com.pe
+nom.pe
+net.pe
+blogspot.pe
+edu.pe
+gripe
+skype
+re
+com.re
+nom.re
+asso.re
+blogspot.re
+care
+healthcare
+compare
+software
+cbre
+here
+fire
+store
+theatre
+secure
+insure
+furniture
+accenture
+azure
+se
+a.se
+b.se
+lanbib.se
+komforb.se
+c.se
+ac.se
+d.se
+bd.se
+brand.se
+kommunalforbund.se
+e.se
+f.se
+g.se
+org.se
+h.se
+fh.se
+i.se
+parti.se
+k.se
+fhsk.se
+l.se
+m.se
+com.se
+tm.se
+n.se
+naturbruksgymn.se
+o.se
+p.se
+pp.se
+r.se
+s.se
+press.se
+t.se
+blogspot.se
+u.se
+fhv.se
+w.se
+x.se
+komvux.se
+y.se
+z.se
+case
+lease
+chase
+reise
+cruise
+homesense
+horse
+house
+date
+estate
+realestate
+allstate
+site
+cyon.site
+website
+vote
+arte
+deloitte
+lotte
+institute
+blue
+clinique
+boutique
+ve
+e12.ve
+web.ve
+gob.ve
+rec.ve
+tec.ve
+store.ve
+org.ve
+mil.ve
+com.ve
+firm.ve
+co.ve
+info.ve
+arts.ve
+net.ve
+int.ve
+edu.ve
+gov.ve
+save
+live
+drive
+progressive
+active
+love
+xn--tckwe
+rwe
+luxe
+xn--gckr3f0f
+xn--mix891f
+xn--mix082f
+xn--kpu716f
+xn--nqv7f
+af
+org.af
+com.af
+net.af
+edu.af
+gov.af
+bf
+gov.bf
+xn--mgb9awbf
+cf
+blogspot.cf
+xn--p1acf
+sncf
+xn--j1aef
+pamperedchef
+off
+gf
+maif
+xn--d1alf
+golf
+nf
+web.nf
+rec.nf
+store.nf
+com.nf
+firm.nf
+info.nf
+other.nf
+per.nf
+arts.nf
+net.nf
+prof
+pf
+org.pf
+com.pf
+edu.pf
+surf
+tf
+wtf
+xn--mgbaakc7dvf
+wf
+xn--55qw42g
+xn--6frz82g
+xn--3ds443g
+xn--j6w193g
+xn--ses554g
+xn--estv75g
+xn--5tzm5g
+xn--rhqv96g
+xn--mgberp4a5d4a87g
+xn--c2br7g
+ag
+org.ag
+com.ag
+nom.ag
+co.ag
+net.ag
+dvag
+bg
+0.bg
+1.bg
+2.bg
+3.bg
+4.bg
+5.bg
+6.bg
+7.bg
+8.bg
+9.bg
+a.bg
+b.bg
+c.bg
+d.bg
+e.bg
+f.bg
+g.bg
+h.bg
+i.bg
+j.bg
+k.bg
+l.bg
+m.bg
+n.bg
+o.bg
+p.bg
+q.bg
+r.bg
+s.bg
+t.bg
+blogspot.bg
+u.bg
+v.bg
+w.bg
+x.bg
+y.bg
+z.bg
+cg
+bcg
+xn--mgbc0a9azcg
+eg
+name.eg
+org.eg
+sci.eg
+mil.eg
+com.eg
+blogspot.com.eg
+eun.eg
+net.eg
+edu.eg
+gov.eg
+aeg
+gg
+org.gg
+co.gg
+net.gg
+aig
+vig
+kg
+org.kg
+mil.kg
+com.kg
+net.kg
+edu.kg
+gov.kg
+mg
+prd.mg
+org.mg
+mil.mg
+com.mg
+nom.mg
+tm.mg
+co.mg
+edu.mg
+gov.mg
+kpmg
+ng
+name.ng
+org.ng
+sch.ng
+i.ng
+mobi.ng
+mil.ng
+com.ng
+blogspot.com.ng
+net.ng
+edu.ng
+gov.ng
+wang
+ing
+bing
+plumbing
+racing
+trading
+wedding
+fishing
+clothing
+viking
+booking
+cooking
+sling
+cleaning
+training
+ping
+shopping
+engineering
+catering
+dating
+marketing
+lighting
+consulting
+genting
+voting
+hosting
+spreadbetting
+giving
+living
+ong
+song
+versicherung
+samsung
+vermögensberatung
+dog
+blog
+goog
+bloomberg
+org
+c.cdn77.org
+rsc.cdn77.org
+mysecuritycamera.org
+pimienta.org
+za.org
+is-very-bad.org
+misconfused.org
+is-saved.org
+is-found.org
+is-very-good.org
+ae.org
+is-very-nice.org
+hepforge.org
+stuff-4-sale.org
+from-me.org
+servegame.org
+sellsyourhome.org
+podzone.org
+boldlygoingnowhere.org
+ssl.origin.cdn77-secure.org
+is-a-candidate.org
+hobby-site.org
+blogsite.org
+is-a-chef.org
+readmyblog.org
+is-a-geek.org
+isa-geek.org
+hk.org
+tunk.org
+is-very-evil.org
+cable-modem.org
+mlbfan.org
+ufcfan.org
+collegefan.org
+nflfan.org
+is-a-celticsfan.org
+is-a-bruinsfan.org
+is-a-patsfan.org
+is-a-soxfan.org
+poivron.org
+dnsdojo.org
+zapto.org
+hopto.org
+no-ip.org
+selfip.org
+webhop.org
+homeftp.org
+serveftp.org
+myftp.org
+potager.org
+sweetpepper.org
+is-a-linux-user.org
+certmgr.org
+dynalias.org
+dnsalias.org
+dsmynas.org
+wmflabs.org
+servebbs.org
+familyds.org
+couchpotatofries.org
+js.org
+read-books.org
+homedns.org
+blogdns.org
+duckdns.org
+doomdns.org
+dyndns.org
+home.dyndns.org
+go.dyndns.org
+dvrdns.org
+gotdns.org
+kicks-ass.org
+bmoattachments.org
+us.org
+is-very-sweet.org
+endoftheinternet.org
+endofinternet.org
+is-a-knight.org
+dontexist.org
+doesntexist.org
+game-host.org
+is-lost.org
+eu.org
+q-a.eu.org
+ca.eu.org
+mc.eu.org
+cd.eu.org
+be.eu.org
+de.eu.org
+ee.eu.org
+ie.eu.org
+me.eu.org
+se.eu.org
+bg.eu.org
+ng.eu.org
+ch.eu.org
+fi.eu.org
+si.eu.org
+dk.eu.org
+mk.eu.org
+sk.eu.org
+uk.eu.org
+al.eu.org
+il.eu.org
+nl.eu.org
+pl.eu.org
+cn.eu.org
+in.eu.org
+no.eu.org
+ro.eu.org
+asso.eu.org
+jp.eu.org
+fr.eu.org
+gr.eu.org
+hr.eu.org
+kr.eu.org
+tr.eu.org
+es.eu.org
+is.eu.org
+paris.eu.org
+us.eu.org
+at.eu.org
+net.eu.org
+it.eu.org
+lt.eu.org
+mt.eu.org
+int.eu.org
+pt.eu.org
+au.eu.org
+edu.eu.org
+hu.eu.org
+lu.eu.org
+ru.eu.org
+lv.eu.org
+cy.eu.org
+my.eu.org
+cz.eu.org
+nz.eu.org
+homeunix.org
+homelinux.org
+tuxfamily.org
+hamburg
+joburg
+sg
+org.sg
+com.sg
+per.sg
+net.sg
+blogspot.sg
+edu.sg
+gov.sg
+xn--5su34j936bgsg
+tg
+ug
+ac.ug
+sc.ug
+ne.ug
+org.ug
+com.ug
+co.ug
+go.ug
+or.ug
+blogspot.ug
+vg
+xn--c1avg
+dwg
+xn--80aswg
+xn--xkc2dl3a5ee0h
+xn--o3cw4h
+xn--efvy88h
+xn--mgbaam7a8h
+bh
+org.bh
+com.bh
+net.bh
+edu.bh
+gov.bh
+gmbh
+ch
+gotdns.ch
+blogspot.ch
+coach
+tech
+rich
+zuerich
+search
+cancerresearch
+church
+bosch
+watch
+swatch
+xn--pgbs0dh
+xn--mgbpl2fh
+kfh
+gh
+org.gh
+mil.gh
+com.gh
+edu.gh
+gov.gh
+caseih
+mh
+xn--j1amh
+ricoh
+ph
+org.ph
+i.ph
+mil.ph
+com.ph
+ngo.ph
+net.ph
+edu.ph
+gov.ph
+sh
+hashbang.sh
+org.sh
+mil.sh
+com.sh
+net.sh
+gov.sh
+cash
+monash
+dish
+fish
+irish
+th
+ac.th
+mi.th
+in.th
+co.th
+go.th
+or.th
+net.th
+faith
+health
+rexroth
+abarth
+earth
+ovh
+nerdpol.ovh
+bzh
+xn--kput3i
+xn--io0a7i
+ai
+off.ai
+org.ai
+com.ai
+net.ai
+xn--p1ai
+dubai
+hyundai
+panerai
+chintai
+bi
+org.bi
+com.bi
+co.bi
+or.bi
+edu.bi
+abudhabi
+obi
+mobi
+dscloud.mobi
+sbi
+ci
+xn--aroport-bya.ci
+ac.ci
+ed.ci
+md.ci
+presse.ci
+org.ci
+com.ci
+co.ci
+go.ci
+asso.ci
+or.ci
+net.ci
+int.ci
+aéroport.ci
+edu.ci
+gouv.ci
+gucci
+tci
+kddi
+audi
+xn--fhbei
+taipei
+fi
+aland.fi
+iki.fi
+blogspot.fi
+dy.fi
+sanofi
+gi
+mod.gi
+ltd.gi
+org.gi
+com.gi
+edu.gi
+gov.gi
+hitachi
+archi
+yodobashi
+mitsubishi
+shouji
+ki
+org.ki
+com.ki
+info.ki
+net.ki
+edu.ki
+gov.ki
+biz.ki
+xn--cg4bki
+wiki
+helsinki
+ski
+suzuki
+li
+blogspot.li
+richardli
+ismaili
+miami
+ni
+web.ni
+gob.ni
+ac.ni
+org.ni
+mil.ni
+com.ni
+nom.ni
+in.ni
+co.ni
+info.ni
+pp.ni
+net.ni
+int.ni
+edu.ni
+biz.ni
+cipriani
+lamborghini
+mini
+erni
+moi
+ferrari
+si
+blogspot.si
+desi
+maserati
+citi
+infiniti
+bharti
+bugatti
+tui
+vi
+k12.vi
+org.vi
+com.vi
+co.vi
+net.vi
+kiwi
+praxi
+taxi
+fyi
+xn--mgbai9azgqp6j
+xn--lgbbat1ad8j
+xn--4gq48lf9j
+bj
+asso.bj
+blogspot.bj
+barreau.bj
+gouv.bj
+dj
+jnj
+sj
+tj
+web.tj
+ac.tj
+nic.tj
+name.tj
+org.tj
+mil.tj
+com.tj
+co.tj
+go.tj
+net.tj
+int.tj
+test.tj
+edu.tj
+gov.tj
+biz.tj
+xn--tiq49xqyj
+xn--3pxu8k
+xn--fct429k
+lefrak
+feedback
+black
+lundbeck
+click
+emerck
+duck
+dk
+store.dk
+reg.dk
+firm.dk
+co.dk
+blogspot.dk
+biz.dk
+tdk
+seek
+temasek
+hk
+xn--uc0ay4a.hk
+xn--gmqw5a.hk
+xn--od0aq3b.hk
+inc.hk
+xn--wcvs22d.hk
+xn--lcvr32d.hk
+xn--55qx5d.hk
+ltd.hk
+xn--tn0ag.hk
+xn--od0alg.hk
+org.hk
+xn--gmq050i.hk
+xn--io0a7i.hk
+xn--mk0axi.hk
+xn--mxtq1m.hk
+com.hk
+xn--ciqpn.hk
+net.hk
+blogspot.hk
+edu.hk
+idv.hk
+gov.hk
+xn--uc0atv.hk
+xn--zf0avx.hk
+个人.hk
+個人.hk
+箇人.hk
+å…¬å¸.hk
+政府.hk
+網絡.hk
+网絡.hk
+組織.hk
+组織.hk
+組织.hk
+组织.hk
+網络.hk
+网络.hk
+敎育.hk
+教育.hk
+nhk
+bostik
+sandvik
+lk
+web.lk
+ac.lk
+soc.lk
+ltd.lk
+org.lk
+sch.lk
+hotel.lk
+com.lk
+assn.lk
+ngo.lk
+grp.lk
+net.lk
+int.lk
+edu.lk
+gov.lk
+talk
+dclk
+silk
+mk
+name.mk
+inf.mk
+org.mk
+com.mk
+net.mk
+blogspot.mk
+edu.mk
+gov.mk
+bank
+hdfcbank
+statebank
+commbank
+everbank
+netbank
+softbank
+ubank
+ink
+link
+cyon.link
+mypep.link
+pink
+book
+pk
+web.pk
+gob.pk
+org.pk
+gok.pk
+fam.pk
+com.pk
+gon.pk
+info.pk
+gop.pk
+gos.pk
+net.pk
+edu.pk
+gov.pk
+biz.pk
+work
+network
+foodnetwork
+sk
+blogspot.sk
+tk
+uk
+ac.uk
+plc.uk
+ltd.uk
+police.uk
+me.uk
+org.uk
+co.uk
+no-ip.co.uk
+blogspot.co.uk
+nhs.uk
+net.uk
+gov.uk
+service.gov.uk
+xn--w4rs40l
+al
+org.al
+mil.al
+com.al
+net.al
+blogspot.al
+edu.al
+gov.al
+global
+cal
+deal
+gal
+legal
+financial
+lplfinancial
+social
+memorial
+prudential
+final
+international
+digital
+capital
+dental
+total
+mutual
+northwesternmutual
+cl
+gob.cl
+mil.cl
+co.cl
+blogspot.cl
+gov.cl
+lidl
+citadel
+spiegel
+chanel
+channel
+cookingchannel
+travelchannel
+weatherchannel
+tel
+intel
+airtel
+mattel
+travel
+xn--ogbpf8fl
+xn--mgbtf8fl
+afl
+nfl
+gl
+org.gl
+com.gl
+co.gl
+net.gl
+edu.gl
+dhl
+pohl
+il
+k12.il
+ac.il
+idf.il
+org.il
+muni.il
+co.il
+blogspot.co.il
+net.il
+gov.il
+fail
+email
+gmail
+hotmail
+mil
+statoil
+ril
+lixil
+baseball
+basketball
+football
+call
+tmall
+dell
+shell
+honeywell
+williamhill
+jll
+ml
+presse.ml
+org.ml
+com.ml
+net.ml
+edu.ml
+gov.ml
+gouv.ml
+nl
+virtueeldomein.nl
+co.nl
+blogspot.nl
+bv.nl
+bnl
+onl
+aol
+futbol
+lol
+cool
+school
+tirol
+uol
+pl
+swidnica.pl
+legnica.pl
+gda.pl
+ostroda.pl
+nowaruda.pl
+media.pl
+warmia.pl
+gdynia.pl
+dlugoleka.pl
+ostroleka.pl
+malopolska.pl
+ustka.pl
+turystyka.pl
+pila.pl
+szkola.pl
+stalowa-wola.pl
+konskowola.pl
+gmina.pl
+babia-gora.pl
+jelenia-gora.pl
+jgora.pl
+zgora.pl
+nysa.pl
+miasta.pl
+bielawa.pl
+ilawa.pl
+olawa.pl
+warszawa.pl
+limanowa.pl
+bialowieza.pl
+lomza.pl
+boleslawiec.pl
+sosnowiec.pl
+ostrowiec.pl
+mielec.pl
+zgorzelec.pl
+wroc.pl
+pc.pl
+med.pl
+aid.pl
+stargard.pl
+gorlice.pl
+gliwice.pl
+starachowice.pl
+prochowice.pl
+polkowice.pl
+katowice.pl
+kobierzyce.pl
+swinoujscie.pl
+pomorskie.pl
+podlasie.pl
+podhale.pl
+opole.pl
+zakopane.pl
+realestate.pl
+pomorze.pl
+mazowsze.pl
+elblag.pl
+kolobrzeg.pl
+tarnobrzeg.pl
+org.pl
+walbrzych.pl
+nieruchomosci.pl
+targi.pl
+suwalki.pl
+turek.pl
+wloclawek.pl
+rybnik.pl
+elk.pl
+sanok.pl
+bialystok.pl
+lebork.pl
+malbork.pl
+slask.pl
+lezajsk.pl
+gdansk.pl
+slupsk.pl
+przeworsk.pl
+wlocl.pl
+rel.pl
+travel.pl
+mail.pl
+mil.pl
+com.pl
+radom.pl
+nom.pl
+bytom.pl
+gsm.pl
+tourism.pl
+tm.pl
+atm.pl
+zagan.pl
+poznan.pl
+lubin.pl
+szczecin.pl
+wolomin.pl
+konin.pl
+bedzin.pl
+swiebodzin.pl
+wielun.pl
+olsztyn.pl
+ketrzyn.pl
+cieszyn.pl
+co.pl
+info.pl
+olecko.pl
+klodzko.pl
+naklo.pl
+mielno.pl
+kepno.pl
+kutno.pl
+szczytno.pl
+opoczno.pl
+gniezno.pl
+jaworzno.pl
+agro.pl
+auto.pl
+grajewo.pl
+mragowo.pl
+beep.pl
+sklep.pl
+ostrowwlkp.pl
+shop.pl
+zachpomor.pl
+sos.pl
+powiat.pl
+net.pl
+sopot.pl
+art.pl
+czest.pl
+edu.pl
+priv.pl
+gov.pl
+pa.gov.pl
+sa.gov.pl
+wsa.gov.pl
+pinb.gov.pl
+winb.gov.pl
+ic.gov.pl
+witd.gov.pl
+psse.gov.pl
+wif.gov.pl
+umig.gov.pl
+ug.gov.pl
+wiih.gov.pl
+ugim.gov.pl
+oirm.gov.pl
+um.gov.pl
+oum.gov.pl
+sdn.gov.pl
+sko.gov.pl
+po.gov.pl
+uppo.gov.pl
+so.gov.pl
+starostwo.gov.pl
+ap.gov.pl
+psp.gov.pl
+kmpsp.gov.pl
+kppsp.gov.pl
+kwpsp.gov.pl
+mup.gov.pl
+pup.gov.pl
+kwp.gov.pl
+zp.gov.pl
+wskr.gov.pl
+sr.gov.pl
+is.gov.pl
+wios.gov.pl
+us.gov.pl
+uzs.gov.pl
+konsulat.gov.pl
+rzgw.gov.pl
+piw.gov.pl
+griw.gov.pl
+wiw.gov.pl
+mw.gov.pl
+upow.gov.pl
+uw.gov.pl
+wzmiuw.gov.pl
+wuoz.gov.pl
+wroclaw.pl
+wodzislaw.pl
+waw.pl
+glogow.pl
+krakow.pl
+lukow.pl
+pruszkow.pl
+zarow.pl
+wegrow.pl
+augustow.pl
+skoczow.pl
+rzeszow.pl
+sex.pl
+kaszuby.pl
+bieszczady.pl
+beskidy.pl
+tychy.pl
+sejny.pl
+kazimierz-dolny.pl
+lapy.pl
+tgory.pl
+mazury.pl
+pulawy.pl
+kartuzy.pl
+rawa-maz.pl
+karpacz.pl
+lowicz.pl
+bydgoszcz.pl
+czeladz.pl
+biz.pl
+kalisz.pl
+pisz.pl
+olkusz.pl
+lpl
+sarl
+frl
+srl
+sl
+org.sl
+com.sl
+net.sl
+edu.sl
+gov.sl
+tl
+gov.tl
+istanbul
+xn--6qq986b3xl
+xn--3bst00m
+xn--mxtq1m
+xn--jvr189m
+am
+blogspot.am
+cam
+webcam
+amsterdam
+stream
+team
+amfam
+shriram
+xn--qxam
+bm
+org.bm
+com.bm
+net.bm
+edu.bm
+gov.bm
+ibm
+cm
+com.cm
+co.cm
+net.cm
+gov.cm
+dm
+org.dm
+com.dm
+net.dm
+edu.dm
+gov.dm
+fm
+ifm
+gm
+xn--fzys8d69uvgm
+hm
+im
+ac.im
+org.im
+com.im
+co.im
+plc.co.im
+ltd.co.im
+ro.im
+net.im
+tt.im
+tv.im
+kim
+xn--4gbrim
+km
+prd.km
+veterinaire.km
+presse.km
+org.km
+mil.km
+com.km
+nom.km
+tm.km
+medecin.km
+asso.km
+coop.km
+notaires.km
+pharmaciens.km
+ass.km
+edu.km
+gov.km
+gouv.km
+film
+stockholm
+om
+med.om
+org.om
+com.om
+museum.om
+co.om
+pro.om
+net.om
+edu.om
+gov.om
+bom
+com
+qa2.com
+servemp3.com
+from-ca.com
+africa.com
+from-ga.com
+teaches-yoga.com
+from-ia.com
+from-ma.com
+is-a-llama.com
+iamallama.com
+from-pa.com
+is-a-cpa.com
+mysecuritycamera.com
+sa.com
+from-va.com
+from-wa.com
+za.com
+dyndns-web.com
+gb.com
+flynnhub.com
+homesecuritymac.com
+from-dc.com
+from-nc.com
+myvnc.com
+homesecuritypc.com
+qc.com
+serveirc.com
+from-sc.com
+stufftoread.com
+is-certified.com
+is-not-certified.com
+cloudcontrolled.com
+from-id.com
+from-md.com
+from-nd.com
+is-with-theband.com
+from-sd.com
+githubcloud.com
+gist.githubcloud.com
+rhcloud.com
+myqnapcloud.com
+alpha-myqnapcloud.com
+dev-myqnapcloud.com
+outsystemscloud.com
+withyoutube.com
+dyndns-office.com
+de.com
+from-de.com
+googlecode.com
+us-1.evennode.com
+eu-1.evennode.com
+us-2.evennode.com
+eu-2.evennode.com
+dyndns-free.com
+servehalflife.com
+serveexchange.com
+is-a-techie.com
+likes-pie.com
+servequake.com
+servecounterstrike.com
+on-aptible.com
+withgoogle.com
+servegame.com
+is-into-anime.com
+dyndns-home.com
+dyndns-at-home.com
+from-ne.com
+is-gone.com
+cechire.com
+xenapponazure.com
+se.com
+is-a-nurse.com
+operaunite.com
+hobby-site.com
+yolasite.com
+dyndns-remote.com
+blogsyte.com
+is-a-cubicle-slave.com
+is-a-conservative.com
+rackmaze.com
+is-a-chef.com
+townnews-staging.com
+ddnsking.com
+workisboring.com
+pgfog.com
+dyndns-blog.com
+writesthisblog.com
+from-nh.com
+from-oh.com
+onthewifi.com
+from-hi.com
+dyndns-wiki.com
+from-mi.com
+from-ri.com
+from-wi.com
+from-nj.com
+from-ak.com
+ciscofreak.com
+is-slick.com
+is-a-geek.com
+isa-geek.com
+hk.com
+elasticbeanstalk.com
+from-ok.com
+dyndns-work.com
+dyndns-at-work.com
+uk.com
+from-al.com
+is-a-liberal.com
+from-fl.com
+from-il.com
+dyndns-mail.com
+bounty-full.com
+alpha.bounty-full.com
+beta.bounty-full.com
+simple-url.com
+neat-url.com
+herokussl.com
+from-nm.com
+health-carereform.com
+servesarcasm.com
+is-a-republican.com
+is-a-bulls-fan.com
+is-a-nascarfan.com
+is-a-musician.com
+is-a-libertarian.com
+cn.com
+is-a-green.com
+from-in.com
+from-mn.com
+gotpantheon.com
+est-a-la-masion.com
+est-le-patron.com
+est-a-la-maison.com
+unusualperson.com
+jpn.com
+from-tn.com
+mydrobo.com
+co.com
+dnsdojo.com
+from-mo.com
+no.com
+ro.com
+servep2p.com
+dyndns-ip.com
+selfip.com
+logoip.com
+ditchyourip.com
+getmyip.com
+sinaapp.com
+vipsinaapp.com
+firebaseapp.com
+1kapp.com
+cloudcontrolapp.com
+meteorapp.com
+eu.meteorapp.com
+pagefrontapp.com
+herokuapp.com
+serveftp.com
+servehttp.com
+ar.com
+from-ar.com
+is-a-rockstar.com
+br.com
+ownprovider.com
+servebeer.com
+is-an-engineer.com
+is-a-blogger.com
+is-a-teacher.com
+is-a-photographer.com
+is-a-hard-worker.com
+is-a-designer.com
+is-a-personaltrainer.com
+is-an-entertainer.com
+is-a-landscaper.com
+is-a-bookkeeper.com
+is-a-caterer.com
+is-a-painter.com
+is-a-hunter.com
+dyndns-server.com
+damnserver.com
+is-a-player.com
+is-a-lawyer.com
+pagespeedmobilizer.com
+gr.com
+kr.com
+xen.prgmr.com
+from-or.com
+is-a-financialadvisor.com
+is-an-actor.com
+is-a-doctor.com
+myasustor.com
+from-pr.com
+dyn-o-saur.com
+est-mon-blogueur.com
+servehumour.com
+dynalias.com
+dnsalias.com
+dsmynas.com
+servebbs.com
+dyndns-pics.com
+servepics.com
+securitytactics.com
+familyds.com
+3utilities.com
+saves-the-whales.com
+is-into-games.com
+quicksytes.com
+point2this.com
+googleapis.com
+from-ks.com
+net-freaks.com
+myshopblocks.com
+from-ms.com
+bloxcms.com
+blogdns.com
+doomdns.com
+gotdns.com
+dynns.com
+is-into-cartoons.com
+freebox-os.com
+freeboxos.com
+is-into-cars.com
+dreamhosters.com
+sells-for-less.com
+is-an-actress.com
+us.com
+compute-1.amazonaws.com
+z-1.compute-1.amazonaws.com
+z-2.compute-1.amazonaws.com
+s3-external-1.amazonaws.com
+s3-eu-central-1.amazonaws.com
+s3.eu-central-1.amazonaws.com
+s3-sa-east-1.amazonaws.com
+us-east-1.amazonaws.com
+s3-ap-northeast-1.amazonaws.com
+s3-ap-southeast-1.amazonaws.com
+s3-us-west-1.amazonaws.com
+s3-eu-west-1.amazonaws.com
+s3-us-gov-west-1.amazonaws.com
+s3-fips-us-gov-west-1.amazonaws.com
+s3-external-2.amazonaws.com
+s3-ap-northeast-2.amazonaws.com
+s3.ap-northeast-2.amazonaws.com
+s3-ap-southeast-2.amazonaws.com
+s3-us-west-2.amazonaws.com
+s3.amazonaws.com
+elb.amazonaws.com
+compute.amazonaws.com
+eu-central-1.compute.amazonaws.com
+sa-east-1.compute.amazonaws.com
+ap-northeast-1.compute.amazonaws.com
+ap-southeast-1.compute.amazonaws.com
+us-west-1.compute.amazonaws.com
+eu-west-1.compute.amazonaws.com
+us-gov-west-1.compute.amazonaws.com
+ap-northeast-2.compute.amazonaws.com
+ap-southeast-2.compute.amazonaws.com
+us-west-2.compute.amazonaws.com
+is-a-democrat.com
+from-ct.com
+is-leet.com
+is-uberleet.com
+from-mt.com
+is-an-accountant.com
+is-a-student.com
+space-to-rent.com
+githubusercontent.com
+codespot.com
+blogspot.com
+appspot.com
+is-a-anarchist.com
+is-an-anarchist.com
+is-a-socialist.com
+is-a-therapist.com
+is-an-artist.com
+dontexist.com
+doesntexist.com
+nfshost.com
+from-ut.com
+isa-hockeynut.com
+from-vt.com
+sells-for-u.com
+4u.com
+eu.com
+hu.com
+issmarterthanyou.com
+ru.com
+is-a-guru.com
+from-nv.com
+from-wv.com
+apps.fbsbx.com
+mex.com
+homeunix.com
+betainabox.com
+from-tx.com
+homelinux.com
+likescandy.com
+from-ky.com
+dnsiskinky.com
+myactivedirectory.com
+uy.com
+from-wy.com
+geekgalaxy.com
+unicom
+mom
+room
+alstom
+pm
+farm
+statefarm
+sm
+tm
+org.tm
+mil.tm
+com.tm
+nom.tm
+co.tm
+net.tm
+edu.tm
+gov.tm
+museum
+xn--correios-e-telecomunicaes-ghc29a.museum
+vantaa.museum
+judaica.museum
+eastafrica.museum
+mallorca.museum
+canada.museum
+casadelamoneda.museum
+florida.museum
+philadelphiaarea.museum
+undersea.museum
+savannahga.museum
+chattanooga.museum
+omaha.museum
+ushuaia.museum
+columbia.museum
+britishcolumbia.museum
+media.museum
+georgia.museum
+philadelphia.museum
+australia.museum
+filatelia.museum
+virginia.museum
+california.museum
+russia.museum
+donostia.museum
+alaska.museum
+nebraska.museum
+lajolla.museum
+xn--lns-qla.museum
+missoula.museum
+fortmissoula.museum
+alabama.museum
+panama.museum
+cinema.museum
+moma.museum
+roma.museum
+americana.museum
+indiana.museum
+pasadena.museum
+southcarolina.museum
+barcelona.museum
+santabarbara.museum
+svizzera.museum
+usa.museum
+atlanta.museum
+minnesota.museum
+plaza.museum
+washingtondc.museum
+quebec.museum
+encyclopedic.museum
+pacific.museum
+oceanographic.museum
+public.museum
+music.museum
+chiropractic.museum
+celtic.museum
+midatlantic.museum
+uvic.museum
+nyc.museum
+baghdad.museum
+farmstead.museum
+mad.museum
+railroad.museum
+madrid.museum
+beeldengeluid.museum
+field.museum
+and.museum
+england.museum
+finland.museum
+judygarland.museum
+scotland.museum
+portland.museum
+maryland.museum
+hembygdsforbund.museum
+imageandsound.museum
+airguard.museum
+oxford.museum
+palace.museum
+birthplace.museum
+space.museum
+ambulance.museum
+resistance.museum
+coastaldefence.museum
+intelligence.museum
+science.museum
+historyofscience.museum
+niepce.museum
+force.museum
+volkenkunde.museum
+mesaverde.museum
+tree.museum
+trustee.museum
+santafe.museum
+wildlife.museum
+village.museum
+heritage.museum
+nationalheritage.museum
+cambridge.museum
+gorge.museum
+muncie.museum
+wallonie.museum
+sherbrooke.museum
+bale.museum
+bible.museum
+motorcycle.museum
+missile.museum
+textile.museum
+skole.museum
+castle.museum
+halloffame.museum
+time.museum
+maritime.museum
+costume.museum
+sologne.museum
+online.museum
+lucerne.museum
+square.museum
+delaware.museum
+stateofdelaware.museum
+lancashire.museum
+yorkshire.museum
+newhampshire.museum
+histoire.museum
+baltimore.museum
+nature.museum
+architecture.museum
+furniture.museum
+culture.museum
+agriculture.museum
+usculture.museum
+louvre.museum
+database.museum
+francaise.museum
+suisse.museum
+house.museum
+mulhouse.museum
+chocolate.museum
+karate.museum
+state.museum
+estate.museum
+countryestate.museum
+uscountryestate.museum
+yosemite.museum
+corvette.museum
+oceanographique.museum
+bellevue.museum
+interactive.museum
+automotive.museum
+juif.museum
+burghof.museum
+building.museum
+museumvereniging.museum
+viking.museum
+whaling.museum
+mining.museum
+timekeeping.museum
+living.museum
+kunstsammlung.museum
+frog.museum
+nuremberg.museum
+starnberg.museum
+nuernberg.museum
+freiburg.museum
+elburg.museum
+hamburg.museum
+naumburg.museum
+marburg.museum
+williamsburg.museum
+colonialwilliamsburg.museum
+christiansburg.museum
+stpetersburg.museum
+salzburg.museum
+fribourg.museum
+luxembourg.museum
+utah.museum
+research.museum
+historisch.museum
+touch.museum
+xn--h1aegh.museum
+pittsburgh.museum
+british.museum
+jewish.museum
+health.museum
+north.museum
+fortworth.museum
+monmouth.museum
+youth.museum
+xn--9dbhblg6di.museum
+hawaii.museum
+helsinki.museum
+ski.museum
+dali.museum
+salvadordali.museum
+assisi.museum
+cincinnati.museum
+geelvinck.museum
+clock.museum
+watch-and-clock.museum
+watchandclock.museum
+jfk.museum
+sibenik.museum
+silk.museum
+norfolk.museum
+annefrank.museum
+tank.museum
+cranbrook.museum
+denmark.museum
+steiermark.museum
+york.museum
+newyork.museum
+medical.museum
+archaeological.museum
+gemological.museum
+zoological.museum
+botanical.museum
+historical.museum
+montreal.museum
+memorial.museum
+educational.museum
+national.museum
+coal.museum
+cultural.museum
+portal.museum
+virtual.museum
+naval.museum
+brunel.museum
+basel.museum
+brussel.museum
+virtuel.museum
+oregontrail.museum
+brasil.museum
+baseball.museum
+surgeonshall.museum
+shell.museum
+elvendrell.museum
+bill.museum
+mill.museum
+windmill.museum
+pubol.museum
+school.museum
+bristol.museum
+amsterdam.museum
+steam.museum
+cheltenham.museum
+durham.museum
+tcm.museum
+salem.museum
+jerusalem.museum
+film.museum
+stockholm.museum
+ulm.museum
+farm.museum
+journalism.museum
+naturalhistorymuseum.museum
+planetarium.museum
+aquarium.museum
+arboretum.museum
+can.museum
+american.museum
+nativeamerican.museum
+michigan.museum
+indian.museum
+egyptian.museum
+isleofman.museum
+saskatchewan.museum
+schokoladen.museum
+sweden.museum
+garden.museum
+botanicgarden.museum
+botanicalgarden.museum
+childrensgarden.museum
+usgarden.museum
+copenhagen.museum
+muenchen.museum
+westfalen.museum
+natuurwetenschappen.museum
+children.museum
+vlaanderen.museum
+uhren.museum
+heimatunduhren.museum
+giessen.museum
+design.museum
+artanddesign.museum
+kunstunddesign.museum
+bahn.museum
+eisenbahn.museum
+stjohn.museum
+berlin.museum
+austin.museum
+koeln.museum
+lincoln.museum
+bonn.museum
+schoenbrunn.museum
+london.museum
+oregon.museum
+soundandvision.museum
+television.museum
+mansion.museum
+communication.museum
+education.museum
+arteducation.museum
+foundation.museum
+creation.museum
+association.museum
+aviation.museum
+telekommunikation.museum
+assassination.museum
+harvestcelebration.museum
+corporation.museum
+illustration.museum
+civilisation.museum
+plantation.museum
+station.museum
+preservation.museum
+environmentalconservation.museum
+civilization.museum
+collection.museum
+science-fiction.museum
+exhibition.museum
+iron.museum
+handson.museum
+jamison.museum
+jefferson.museum
+larsson.museum
+capebreton.museum
+clinton.museum
+boston.museum
+bern.museum
+modern.museum
+western.museum
+bauern.museum
+luzern.museum
+paderborn.museum
+koebenhavn.museum
+town.museum
+xn--comunicaes-v6a2o.museum
+bilbao.museum
+artdeco.museum
+newmexico.museum
+sanfrancisco.museum
+paleo.museum
+chicago.museum
+otago.museum
+sandiego.museum
+fundacio.museum
+presidio.museum
+ontario.museum
+monticello.museum
+maritimo.museum
+torino.museum
+anthro.museum
+riodejaneiro.museum
+workshop.museum
+iraq.museum
+war.museum
+coldwar.museum
+civilwar.museum
+ddr.museum
+amber.museum
+cyber.museum
+carrier.museum
+lewismiller.museum
+franziskaner.museum
+newspaper.museum
+theater.museum
+exeter.museum
+center.museum
+sciencecenter.museum
+culturalcenter.museum
+museumcenter.museum
+artcenter.museum
+manchester.museum
+rochester.museum
+muenster.museum
+computer.museum
+air.museum
+openair.museum
+labor.museum
+dinosaur.museum
+labour.museum
+karikatur.museum
+glas.museum
+dallas.museum
+hellas.museum
+texas.museum
+kids.museum
+grandrapids.museum
+sciences.museum
+naturalsciences.museum
+landes.museum
+juedisches.museum
+historisches.museum
+medizinhistorisches.museum
+naturhistorisches.museum
+schlesisches.museum
+humanities.museum
+wales.museum
+losangeles.museum
+sciencesnaturelles.museum
+bruxelles.museum
+versailles.museum
+meeres.museum
+figueres.museum
+castres.museum
+historichouses.museum
+neues.museum
+cadaques.museum
+antiques.museum
+americanantiques.museum
+usantiques.museum
+comunicações.museum
+correios-e-telecomunicações.museum
+palmsprings.museum
+baths.museum
+indianapolis.museum
+paris.museum
+saintlouis.museum
+axis.museum
+brussels.museum
+dolls.museum
+nationalfirearms.museum
+stalbans.museum
+lans.museum
+childrens.museum
+mansions.museum
+communications.museum
+posts-and-telecommunications.museum
+läns.museum
+flanders.museum
+settlers.museum
+farmers.museum
+miners.museum
+sciencecenters.museum
+glass.museum
+press.museum
+crafts.museum
+artsandcrafts.museum
+plants.museum
+pilots.museum
+arts.museum
+finearts.museum
+decorativearts.museum
+usdecorativearts.museum
+usarts.museum
+beauxarts.museum
+bus.museum
+columbus.museum
+circus.museum
+portlligat.museum
+project.museum
+stadt.museum
+museet.museum
+indianmarket.museum
+detroit.museum
+settlement.museum
+environment.museum
+farmequipment.museum
+convent.museum
+depot.museum
+art.museum
+birdart.museum
+fineart.museum
+stuttgart.museum
+jewishart.museum
+rockart.museum
+americanart.museum
+cartoonart.museum
+asmatart.museum
+contemporaryart.museum
+seaport.museum
+transport.museum
+newport.museum
+frankfurt.museum
+broadcast.museum
+eastcoast.museum
+southwest.museum
+loyalist.museum
+kunst.museum
+delmenhorst.museum
+marylhurst.museum
+trust.museum
+bergbau.museum
+coloradoplateau.museum
+cymru.museum
+moscow.museum
+nrw.museum
+essex.museum
+phoenix.museum
+manx.museum
+satx.museum
+chesapeakebay.museum
+gateway.museum
+railway.museum
+pharmacy.museum
+cody.museum
+bushey.museum
+berkeley.museum
+valley.museum
+brandywinevalley.museum
+trolley.museum
+sydney.museum
+money.museum
+surrey.museum
+guernsey.museum
+newjersey.museum
+archaeology.museum
+geology.museum
+entomology.museum
+technology.museum
+ethnology.museum
+zoology.museum
+anthropology.museum
+topology.museum
+horology.museum
+photography.museum
+philately.museum
+family.museum
+academy.museum
+astronomy.museum
+botany.museum
+nyny.museum
+spy.museum
+contemporary.museum
+military.museum
+embroidery.museum
+gallery.museum
+artgallery.museum
+discovery.museum
+jewelry.museum
+freemasonry.museum
+history.museum
+scienceandhistory.museum
+sciencehistory.museum
+livinghistory.museum
+uslivinghistory.museum
+localhistory.museum
+naturalhistory.museum
+computerhistory.museum
+ushistory.museum
+scienceandindustry.museum
+epilepsy.museum
+society.museum
+historicalsociety.museum
+community.museum
+university.museum
+county.museum
+graz.museum
+schweiz.museum
+linz.museum
+badajoz.museum
+santacruz.museum
+иком.museum
+ירושלי×.museum
+forum
+zm
+ac.zm
+org.zm
+sch.zm
+mil.zm
+com.zm
+co.zm
+info.zm
+net.zm
+edu.zm
+gov.zm
+biz.zm
+xn--imr513n
+durban
+republican
+fan
+jpmorgan
+agakhan
+guardian
+theguardian
+man
+warman
+loan
+nissan
+xihuan
+anquan
+caravan
+olayan
+cbn
+cn
+ha.cn
+hb.cn
+ac.cn
+sc.cn
+xn--55qx5d.cn
+gd.cn
+sd.cn
+he.cn
+xn--od0alg.cn
+org.cn
+ah.cn
+qh.cn
+sh.cn
+xn--io0a7i.cn
+hi.cn
+bj.cn
+fj.cn
+tj.cn
+xj.cn
+zj.cn
+hk.cn
+hl.cn
+mil.cn
+jl.cn
+nm.cn
+com.cn
+s3.cn-north-1.amazonaws.com.cn
+compute.amazonaws.com.cn
+cn-north-1.compute.amazonaws.com.cn
+hn.cn
+ln.cn
+sn.cn
+yn.cn
+mo.cn
+cq.cn
+gs.cn
+js.cn
+net.cn
+edu.cn
+gov.cn
+tw.cn
+gx.cn
+jx.cn
+nx.cn
+sx.cn
+gz.cn
+xz.cn
+å…¬å¸.cn
+網絡.cn
+网络.cn
+bcn
+gdn
+akdn
+garden
+green
+kaufen
+volkswagen
+kitchen
+immobilien
+wien
+men
+open
+ren
+vlaanderen
+reisen
+seven
+gn
+ac.gn
+org.gn
+com.gn
+net.gn
+edu.gn
+gov.gn
+design
+verisign
+hn
+gob.hn
+org.hn
+mil.hn
+com.hn
+net.hn
+edu.hn
+in
+ac.in
+nic.in
+ind.in
+org.in
+mil.in
+firm.in
+gen.in
+co.in
+res.in
+net.in
+blogspot.in
+edu.in
+gov.in
+calvinklein
+virgin
+skin
+berlin
+pin
+lupin
+vin
+win
+xin
+kn
+org.kn
+net.kn
+edu.kn
+gov.kn
+koeln
+lincoln
+mn
+nyc.mn
+org.mn
+edu.mn
+gov.mn
+london
+fashion
+creditunion
+vision
+eurovision
+education
+foundation
+playstation
+protection
+auction
+construction
+nikon
+salon
+mormon
+canon
+qpon
+coupon
+liaison
+maison
+scjohnson
+epson
+emerson
+ericsson
+norton
+boston
+pn
+org.pn
+co.pn
+net.pn
+edu.pn
+gov.pn
+kpn
+cern
+bayern
+porn
+sn
+org.sn
+com.sn
+perso.sn
+blogspot.sn
+art.sn
+edu.sn
+univ.sn
+gouv.sn
+tn
+ind.tn
+defense.tn
+org.tn
+intl.tn
+com.tn
+mincom.tn
+tourism.tn
+turen.tn
+fin.tn
+info.tn
+perso.tn
+ens.tn
+rns.tn
+nat.tn
+net.tn
+agrinet.tn
+edunet.tn
+rnrt.tn
+rnu.tn
+gov.tn
+mtn
+fun
+run
+datsun
+yamaxun
+yun
+vn
+ac.vn
+name.vn
+org.vn
+health.vn
+com.vn
+info.vn
+pro.vn
+net.vn
+int.vn
+blogspot.vn
+edu.vn
+gov.vn
+biz.vn
+crown
+town
+capetown
+xn--yfro4i67o
+ao
+pb.ao
+ed.ao
+og.ao
+co.ao
+it.ao
+gv.ao
+taobao
+bo
+gob.bo
+org.bo
+mil.bo
+com.bo
+net.bo
+int.bo
+edu.bo
+gov.bo
+tv.bo
+hbo
+weibo
+globo
+co
+web.co
+rec.co
+org.co
+mil.co
+com.co
+blogspot.com.co
+nom.co
+firm.co
+info.co
+arts.co
+net.co
+int.co
+edu.co
+gov.co
+aco
+eco
+iveco
+nico
+aramco
+blanco
+bradesco
+cisco
+do
+web.do
+gob.do
+sld.do
+org.do
+mil.do
+com.do
+net.do
+art.do
+edu.do
+gov.do
+abogado
+fido
+xn--mgbca7dzdo
+ceo
+video
+rodeo
+meo
+alfaromeo
+fo
+info
+barrel-of-knowledge.info
+barrell-of-knowledge.info
+ilovecollege.info
+groks-the.info
+here-for-more.info
+nsupdate.info
+knowsitall.info
+dvrcam.info
+no-ip.info
+selfip.info
+webhop.info
+for-our.info
+groks-this.info
+dyndns.info
+lego
+aigo
+ngo
+mango
+bingo
+whoswho
+io
+gitlab.io
+github.io
+hzc.io
+nid.io
+drud.io
+boxfuse.io
+pantheonsite.io
+ngrok.io
+browsersafetymark.io
+com.io
+dedyn.io
+hasura-app.io
+backplaneapp.io
+sandcats.io
+protonet.io
+spacekit.io
+bio
+radio
+audio
+studio
+jio
+rio
+physio
+jo
+name.jo
+org.jo
+sch.jo
+mil.jo
+com.jo
+net.jo
+edu.jo
+gov.jo
+gallo
+ollo
+mo
+org.mo
+com.mo
+net.mo
+edu.mo
+gov.mo
+gmo
+limo
+immo
+promo
+no
+xn--kranghke-b0a.no
+xn--vegrshei-c0a.no
+xn--gildeskl-g0a.no
+xn--kvnangen-k0a.no
+xn--ygarden-p1a.no
+xn--srreisa-q1a.no
+xn--tnsberg-q1a.no
+xn--sr-odal-q1a.no
+xn--ldingen-q1a.no
+xn--sr-fron-q1a.no
+xn--hyanger-q1a.no
+xn--hnefoss-q1a.no
+xn--trgstad-r1a.no
+xn--stjrdal-s1a.no
+xn--rennesy-v1a.no
+xn--skjervy-v1a.no
+xn--hgebostad-g3a.no
+xn--jrpeland-54a.no
+xn--lrenskog-54a.no
+xn--hylandet-54a.no
+xn--mjndalen-64a.no
+xn--bhcavuotna-s4a.no
+xn--mlatvuopmi-s4a.no
+xn--davvenjrga-y4a.no
+xn--bearalvhki-y4a.no
+xn--bhccavuotna-k7a.no
+xn--vre-eiker-k8a.no
+xn--sr-aurdal-l8a.no
+xn--krdsherad-m8a.no
+aa.no
+gs.aa.no
+xn--nmesjevuemie-tcba.no
+odda.no
+davvesiida.no
+volda.no
+stranda.no
+sauda.no
+xn--l-1fa.no
+xn--s-1fa.no
+xn--h-2fa.no
+xn--eveni-0qa01ga.no
+vaga.no
+vega.no
+tolga.no
+davvenjarga.no
+unjarga.no
+davvenjárga.no
+unjárga.no
+ha.no
+xn--fl-zia.no
+badaddja.no
+lesja.no
+leka.no
+karasjohka.no
+kárášjohka.no
+leangaviika.no
+gangaviika.no
+leaŋgaviika.no
+gáŋgaviika.no
+narviika.no
+fla.no
+overhalla.no
+smola.no
+sola.no
+vennesla.no
+xn--mli-tla.no
+xn--mot-tla.no
+xn--rde-ula.no
+sula.no
+smøla.no
+rauma.no
+xn--rst-0na.no
+xn--bod-2na.no
+xn--risa-5na.no
+xn--slat-5na.no
+rana.no
+mo-i-rana.no
+frana.no
+trana.no
+tana.no
+divtasvuodna.no
+vikna.no
+somna.no
+sømna.no
+donna.no
+dønna.no
+arna.no
+nesna.no
+giehtavuoatna.no
+bahccavuotna.no
+báhccavuotna.no
+bahcavuotna.no
+báhcavuotna.no
+navuotna.no
+gaivuotna.no
+gáivuotna.no
+omasvuotna.no
+divttasvuotna.no
+návuotna.no
+xn--yer-zna.no
+fræna.no
+træna.no
+xn--rdal-poa.no
+xn--snes-poa.no
+xn--vgan-qoa.no
+xn--snsa-roa.no
+xn--skjk-soa.no
+xn--brum-voa.no
+xn--frna-woa.no
+xn--trna-woa.no
+xn--klbu-woa.no
+loppa.no
+xn--loabt-0qa.no
+xn--muost-0qa.no
+xn--bievt-0qa.no
+xn--lhppi-xqa.no
+xn--hbmer-xqa.no
+xn--hpmir-xqa.no
+xn--sknit-yqa.no
+xn--rsta-fra.no
+xn--smna-gra.no
+xn--dnna-gra.no
+xn--frde-gra.no
+xn--sgne-gra.no
+xn--srum-gra.no
+xn--lten-gra.no
+xn--bmlo-gra.no
+xn--rros-gra.no
+xn--smla-hra.no
+xn--frya-hra.no
+xn--tjme-hra.no
+xn--hobl-ira.no
+xn--risr-ira.no
+xn--rady-ira.no
+xn--andy-ira.no
+xn--asky-ira.no
+xn--mely-ira.no
+xn--lury-ira.no
+xn--dyry-ira.no
+utsira.no
+xn--vard-jra.no
+xn--flor-jra.no
+xn--vads-jra.no
+aukra.no
+xn--fjord-lra.no
+xn--seral-lra.no
+xn--rholt-mra.no
+xn--snase-nra.no
+flora.no
+xn--linds-pra.no
+xn--lrdal-sra.no
+hitra.no
+xn--tysvr-vra.no
+snasa.no
+snoasa.no
+raisa.no
+nordreisa.no
+sorreisa.no
+sørreisa.no
+ráisa.no
+galsa.no
+halsa.no
+romsa.no
+tromsa.no
+rissa.no
+fusa.no
+snåsa.no
+aknoluokta.no
+ákŋoluokta.no
+alta.no
+xn--bjddar-pta.no
+xn--unjrga-rta.no
+hammarfeasta.no
+hámmárfeasta.no
+frosta.no
+orsta.no
+ørsta.no
+xn--skierv-uta.no
+xn--lesund-hua.no
+xn--kfjord-iua.no
+xn--mlselv-iua.no
+xn--moreke-jua.no
+xn--merker-kua.no
+xn--rland-uua.no
+xn--rskog-uua.no
+xn--ksnes-uua.no
+xn--ryken-vua.no
+xn--drbak-wua.no
+xn--gjvik-wua.no
+xn--osyro-wua.no
+xn--sandy-yua.no
+xn--karmy-yua.no
+xn--trany-yua.no
+xn--finny-yua.no
+xn--avery-yua.no
+xn--troms-zua.no
+va.no
+gs.va.no
+krokstadelva.no
+skierva.no
+xn--nvuotna-hwa.no
+xn--vler-qoa.xn--stfold-9xa.no
+xn--krehamn-dxa.no
+xn--sknland-fxa.no
+xn--holtlen-hxa.no
+xn--oppegrd-ixa.no
+xn--langevg-jxa.no
+xn--berlevg-jxa.no
+xn--rlingen-mxa.no
+xn--kvfjord-nxa.no
+xn--laheadju-7ya.no
+xn--givuotna-8ya.no
+xn--srfold-bya.no
+xn--rmskog-bya.no
+xn--ryrvik-bya.no
+xn--jlster-bya.no
+xn--mosjen-eya.no
+xn--bjarky-fya.no
+xn--indery-fya.no
+xn--ostery-fya.no
+xn--kvitsy-fya.no
+xn--krager-gya.no
+froya.no
+frøya.no
+xn--btsfjord-9za.no
+xn--leagaviika-52b.no
+xn--hcesuolo-7ya35b.no
+xn--vg-yiab.no
+xn--blt-elab.no
+xn--slt-elab.no
+xn--rdy-0nab.no
+xn--sndre-land-0cb.no
+xn--stre-toten-zcb.no
+xn--sr-varanger-ggb.no
+xn--sandnessjen-ogb.no
+xn--ystre-slidre-ujb.no
+xn--aurskog-hland-jnb.no
+xn--hery-ira.xn--mre-og-romsdal-qqb.no
+sande.xn--mre-og-romsdal-qqb.no
+xn--stjrdalshalsen-sqb.no
+xn--hmmrfeasta-s4ac.no
+xn--brnnysund-m8ac.no
+xn--lt-liac.no
+xn--gls-elac.no
+xn--bidr-5nac.no
+xn--lgrd-poac.no
+xn--brnny-wuac.no
+xn--vrggt-xqad.no
+herad.no
+kvinnherad.no
+krodsherad.no
+krødsherad.no
+sauherad.no
+ibestad.no
+rakkestad.no
+hyllestad.no
+nannestad.no
+trogstad.no
+trøgstad.no
+flakstad.no
+fredrikstad.no
+vevelstad.no
+grimstad.no
+hagebostad.no
+hægebostad.no
+harstad.no
+gjerstad.no
+xn--bdddj-mrabd.no
+eid.no
+hareid.no
+kviteseid.no
+namdalseid.no
+sorfold.no
+sørfold.no
+sande.vestfold.no
+valer.ostfold.no
+våler.østfold.no
+sondre-land.no
+søndre-land.no
+nordre-land.no
+os.hordaland.no
+xn--b-5ga.nordland.no
+xn--hery-ira.nordland.no
+bo.nordland.no
+heroy.nordland.no
+herøy.nordland.no
+bø.nordland.no
+meland.no
+hjelmeland.no
+jorpeland.no
+jørpeland.no
+iveland.no
+bygland.no
+skanland.no
+skånland.no
+aurskog-holand.no
+froland.no
+orland.no
+aurland.no
+ørland.no
+sortland.no
+aurskog-høland.no
+strand.no
+tvedestrand.no
+balestrand.no
+holmestrand.no
+lillesand.no
+kristiansand.no
+forsand.no
+lund.no
+solund.no
+sund.no
+midsund.no
+tjeldsund.no
+haugesund.no
+alesund.no
+Ã¥lesund.no
+hokksund.no
+kvalsund.no
+kristiansund.no
+farsund.no
+egersund.no
+eigersund.no
+fetsund.no
+bronnoysund.no
+brønnøysund.no
+svalbard.no
+gs.svalbard.no
+oppegard.no
+algard.no
+afjord.no
+vindafjord.no
+kafjord.no
+kvafjord.no
+eidfjord.no
+sandefjord.no
+flekkefjord.no
+snillfjord.no
+leirfjord.no
+storfjord.no
+balsfjord.no
+batsfjord.no
+båtsfjord.no
+tysfjord.no
+Ã¥fjord.no
+kåfjord.no
+kvæfjord.no
+seljord.no
+stord.no
+oppegård.no
+ålgård.no
+nes.buskerud.no
+xn--nttery-byae.no
+rade.no
+molde.no
+forde.no
+førde.no
+råde.no
+rygge.no
+stange.no
+naamesjevuemie.no
+nååmesjevuemie.no
+laakesvuemie.no
+aejrie.no
+fedje.no
+skodje.no
+selje.no
+moareke.no
+moåreke.no
+kraanghke.no
+kråanghke.no
+ringerike.no
+tokke.no
+stokke.no
+giske.no
+fauske.no
+bamble.no
+bykle.no
+valle.no
+stathelle.no
+hole.no
+time.no
+tjome.no
+tjøme.no
+grane.no
+sogne.no
+søgne.no
+hemne.no
+lierne.no
+etne.no
+kommune.no
+bryne.no
+vestre-slidre.no
+oystre-slidre.no
+øystre-slidre.no
+aure.no
+dovre.no
+snaase.no
+snåase.no
+aarborte.no
+vaapste.no
+grue.no
+xn--porsgu-sta26f.no
+xn--rhkkervju-01af.no
+xn--mtta-vrjjat-k7af.no
+of.no
+gs.of.no
+hof.no
+sf.no
+gs.sf.no
+vf.no
+gs.vf.no
+xn--nry-yla5g.no
+xn--vry-yla5g.no
+rollag.no
+langevag.no
+berlevag.no
+vang.no
+ullensvang.no
+grong.no
+eidskog.no
+romskog.no
+rømskog.no
+lorenskog.no
+lørenskog.no
+orskog.no
+ørskog.no
+berg.no
+randaberg.no
+spydeberg.no
+eidsberg.no
+flesberg.no
+kongsberg.no
+tonsberg.no
+tønsberg.no
+sarpsborg.no
+alstahaug.no
+langevåg.no
+berlevåg.no
+xn--msy-ula0h.no
+xn--ggaviika-8ya47h.no
+xn--koluokta-7ya57h.no
+ah.no
+gs.ah.no
+vegarshei.no
+vegårshei.no
+frei.no
+bearalvahki.no
+bearalváhki.no
+ski.no
+amli.no
+Ã¥mli.no
+malatvuopmi.no
+málatvuopmi.no
+lahppi.no
+láhppi.no
+dielddanuorri.no
+evenassi.no
+evenášši.no
+xn--vgsy-qoa0j.no
+xn--krjohka-hwab49j.no
+drobak.no
+drøbak.no
+skjak.no
+siellak.no
+vik.no
+spjelkavik.no
+malvik.no
+svelvik.no
+ulvik.no
+gamvik.no
+lenvik.no
+gjovik.no
+larvik.no
+narvik.no
+kopervik.no
+leirvik.no
+royrvik.no
+røyrvik.no
+hasvik.no
+leksvik.no
+mosvik.no
+gjøvik.no
+enebakk.no
+karasjok.no
+xn--vler-qoa.hedmark.no
+valer.hedmark.no
+våler.hedmark.no
+os.hedmark.no
+xn--b-5ga.telemark.no
+bo.telemark.no
+bø.telemark.no
+aremark.no
+skjåk.no
+al.no
+surnadal.no
+brumunddal.no
+norddal.no
+drangedal.no
+etnedal.no
+hemsedal.no
+nissedal.no
+nittedal.no
+sigdal.no
+lyngdal.no
+orkdal.no
+meldal.no
+hattfjelldal.no
+folldal.no
+midtre-gauldal.no
+suldal.no
+mandal.no
+arendal.no
+sogndal.no
+bindal.no
+hornindal.no
+rindal.no
+sokndal.no
+sunndal.no
+jondal.no
+nord-odal.no
+sor-odal.no
+sør-odal.no
+oppdal.no
+ardal.no
+lardal.no
+marnardal.no
+engerdal.no
+lerdal.no
+verdal.no
+sirdal.no
+stjordal.no
+stordal.no
+nord-aurdal.no
+sor-aurdal.no
+sør-aurdal.no
+hurdal.no
+Ã¥rdal.no
+lærdal.no
+stjørdal.no
+gjesdal.no
+kvinesdal.no
+fyresdal.no
+vaksdal.no
+sande.more-og-romsdal.no
+heroy.more-og-romsdal.no
+sande.møre-og-romsdal.no
+herøy.møre-og-romsdal.no
+gausdal.no
+saltdal.no
+hjartdal.no
+naustdal.no
+alvdal.no
+stor-elvdal.no
+nore-og-uvdal.no
+tydal.no
+gildeskal.no
+aseral.no
+Ã¥seral.no
+folkebibl.no
+fylkesbibl.no
+sel.no
+hadsel.no
+hl.no
+gs.hl.no
+mil.no
+trysil.no
+fjell.no
+austevoll.no
+tingvoll.no
+askvoll.no
+eidsvoll.no
+nl.no
+gs.nl.no
+ol.no
+gs.ol.no
+hobol.no
+gol.no
+hol.no
+rl.no
+gs.rl.no
+Ã¥l.no
+gildeskål.no
+hobøl.no
+haram.no
+kvam.no
+fm.no
+gs.fm.no
+hm.no
+gs.hm.no
+trondheim.no
+austrheim.no
+jessheim.no
+bjerkreim.no
+askim.no
+lom.no
+tm.no
+gs.tm.no
+modum.no
+museum.no
+barum.no
+gjerdrum.no
+elverum.no
+sorum.no
+hurum.no
+bærum.no
+sørum.no
+slattum.no
+vagan.no
+namsskogan.no
+vågan.no
+siljan.no
+roan.no
+gran.no
+verran.no
+nesodden.no
+notodden.no
+halden.no
+oygarden.no
+øygarden.no
+masfjorden.no
+steigen.no
+salangen.no
+ballangen.no
+kvanangen.no
+kvænangen.no
+gratangen.no
+nesoddtangen.no
+lavangen.no
+vossevangen.no
+lodingen.no
+lødingen.no
+ralingen.no
+rælingen.no
+lyngen.no
+bergen.no
+skien.no
+torsken.no
+royken.no
+røyken.no
+songdalen.no
+rendalen.no
+mjondalen.no
+mjøndalen.no
+modalen.no
+holtalen.no
+gulen.no
+holtålen.no
+drammen.no
+mosjoen.no
+sandnessjoen.no
+gloppen.no
+stjordalshalsen.no
+stjørdalshalsen.no
+osen.no
+loten.no
+vestre-toten.no
+ostre-toten.no
+østre-toten.no
+horten.no
+løten.no
+sykkylven.no
+vanylven.no
+jan-mayen.no
+gs.jan-mayen.no
+mosjøen.no
+sandnessjøen.no
+frogn.no
+bjugn.no
+troandin.no
+granvin.no
+bokn.no
+audnedaln.no
+akrehamn.no
+Ã¥krehamn.no
+tinn.no
+porsgrunn.no
+nord-fron.no
+sor-fron.no
+sør-fron.no
+beiarn.no
+stavern.no
+vefsn.no
+skaun.no
+stryn.no
+xn--vestvgy-ixa6o.no
+co.no
+bodo.no
+vardo.no
+sveio.no
+fuossko.no
+bomlo.no
+bømlo.no
+andasuolo.no
+cahcesuolo.no
+Äáhcesuolo.no
+oslo.no
+gs.oslo.no
+skedsmo.no
+kautokeino.no
+kragero.no
+floro.no
+osoyro.no
+osøyro.no
+vadso.no
+tromso.no
+dep.no
+nordkapp.no
+klepp.no
+bajddar.no
+bájddar.no
+baidar.no
+fitjar.no
+gaular.no
+hamar.no
+tysvar.no
+leikanger.no
+orkanger.no
+bremanger.no
+tananger.no
+samnanger.no
+sor-varanger.no
+sør-varanger.no
+porsanger.no
+flatanger.no
+stavanger.no
+levanger.no
+hoyanger.no
+høyanger.no
+kongsvinger.no
+lier.no
+steinkjer.no
+jevnaker.no
+meraker.no
+ringsaker.no
+ullensaker.no
+nedre-eiker.no
+ovre-eiker.no
+øvre-eiker.no
+marker.no
+asker.no
+meråker.no
+fjaler.no
+hvaler.no
+habmer.no
+hábmer.no
+lillehammer.no
+lunner.no
+jolster.no
+jølster.no
+luster.no
+oyer.no
+øyer.no
+hapmir.no
+hápmir.no
+mr.no
+gs.mr.no
+risor.no
+tr.no
+gs.tr.no
+báidár.no
+tysvær.no
+risør.no
+lindas.no
+sandnes.no
+agdenes.no
+birkenes.no
+kirkenes.no
+moskenes.no
+evenes.no
+hemnes.no
+gjemnes.no
+evje-og-hornnes.no
+asnes.no
+lindesnes.no
+oksnes.no
+øksnes.no
+fosnes.no
+tysnes.no
+Ã¥snes.no
+vestnes.no
+vgs.no
+fhs.no
+lavagis.no
+roros.no
+røros.no
+namsos.no
+honefoss.no
+hønefoss.no
+moss.no
+voss.no
+melhus.no
+nes.akershus.no
+Ã¥s.no
+lindås.no
+loabat.no
+voagat.no
+varggat.no
+matta-varjjat.no
+mátta-várjjat.no
+balat.no
+salat.no
+sálat.no
+muosat.no
+stat.no
+bievat.no
+ruovat.no
+hoylandet.no
+høylandet.no
+fet.no
+tynset.no
+skedsmokorset.no
+nesset.no
+skiptvet.no
+skanit.no
+skánit.no
+raholt.no
+råholt.no
+nt.no
+gs.nt.no
+amot.no
+Ã¥mot.no
+blogspot.no
+st.no
+gs.st.no
+hammerfest.no
+rost.no
+røst.no
+idrett.no
+loabát.no
+várggát.no
+bálát.no
+sálát.no
+muosát.no
+bievát.no
+bu.no
+gs.bu.no
+klabu.no
+andebu.no
+ringebu.no
+rennebu.no
+selbu.no
+klæbu.no
+bardu.no
+beardu.no
+porsangu.no
+ivgu.no
+porsáŋgu.no
+alaheadju.no
+álaheadju.no
+budejju.no
+rahkkeravju.no
+ráhkkerávju.no
+fuoisku.no
+guovdageaidnu.no
+deatnu.no
+priv.no
+malselv.no
+målselv.no
+nesseby.no
+tranby.no
+lebesby.no
+vestby.no
+radoy.no
+andoy.no
+sandoy.no
+rodoy.no
+vestvagoy.no
+bjarkoy.no
+askoy.no
+meloy.no
+karmoy.no
+tranoy.no
+finnoy.no
+bronnoy.no
+hamaroy.no
+naroy.no
+varoy.no
+inderoy.no
+osteroy.no
+notteroy.no
+averoy.no
+luroy.no
+dyroy.no
+masoy.no
+rennesoy.no
+vagsoy.no
+karlsoy.no
+kvitsoy.no
+skjervoy.no
+radøy.no
+andøy.no
+sandøy.no
+rødøy.no
+vestvågøy.no
+bjarkøy.no
+askøy.no
+meløy.no
+karmøy.no
+tranøy.no
+finnøy.no
+brønnøy.no
+inderøy.no
+osterøy.no
+nøtterøy.no
+averøy.no
+lurøy.no
+dyrøy.no
+nærøy.no
+værøy.no
+rennesøy.no
+vågsøy.no
+kvitsøy.no
+måsøy.no
+skjervøy.no
+gálsá.no
+áltá.no
+skiervá.no
+vågå.no
+hå.no
+bådåddjå.no
+flå.no
+bodø.no
+vardø.no
+kragerø.no
+florø.no
+vadsø.no
+tromsø.no
+ikano
+casino
+latino
+uno
+boo
+foo
+goo
+yahoo
+ooo
+tattoo
+sapo
+zippo
+ro
+rec.ro
+store.ro
+org.ro
+com.ro
+nom.ro
+firm.ro
+tm.ro
+info.ro
+shop.ro
+arts.ro
+nt.ro
+blogspot.ro
+www.ro
+aero
+caa.aero
+dgca.aero
+media.aero
+club.aero
+aeroclub.aero
+airtraffic.aero
+aerobatic.aero
+air-surveillance.aero
+ambulance.aero
+maintenance.aero
+insurance.aero
+conference.aero
+exchange.aero
+aerodrome.aero
+engine.aero
+airline.aero
+magazine.aero
+software.aero
+trading.aero
+gliding.aero
+paragliding.aero
+hanggliding.aero
+groundhandling.aero
+modelling.aero
+ballooning.aero
+catering.aero
+leasing.aero
+consulting.aero
+parachuting.aero
+skydiving.aero
+research.aero
+journal.aero
+fuel.aero
+council.aero
+control.aero
+air-traffic-control.aero
+design.aero
+union.aero
+certification.aero
+recreation.aero
+accident-investigation.aero
+navigation.aero
+association.aero
+passenger-association.aero
+civilaviation.aero
+federation.aero
+production.aero
+accident-prevention.aero
+cargo.aero
+championship.aero
+group.aero
+workinggroup.aero
+trader.aero
+engineer.aero
+broker.aero
+trainer.aero
+charter.aero
+author.aero
+educator.aero
+logistics.aero
+services.aero
+res.aero
+works.aero
+press.aero
+express.aero
+agents.aero
+aircraft.aero
+rotorcraft.aero
+freight.aero
+flight.aero
+microlight.aero
+homebuilt.aero
+consultant.aero
+student.aero
+amusement.aero
+entertainment.aero
+government.aero
+equipment.aero
+pilot.aero
+airport.aero
+journalist.aero
+scientist.aero
+crew.aero
+show.aero
+emergency.aero
+repbody.aero
+safety.aero
+ferrero
+zero
+pro
+aaa.pro
+aca.pro
+cpa.pro
+med.pro
+eng.pro
+bar.pro
+jur.pro
+avocat.pro
+acct.pro
+recht.pro
+law.pro
+so
+org.so
+com.so
+net.so
+to
+org.to
+mil.to
+com.to
+net.to
+edu.to
+gov.to
+photo
+moto
+voto
+kyoto
+lotto
+auto
+vivo
+volvo
+saxo
+tokyo
+cheap
+gap
+map
+sap
+jcp
+jeep
+gp
+org.gp
+mobi.gp
+com.gp
+asso.gp
+net.gp
+edu.gp
+rip
+vip
+zip
+jp
+xn--f6qx53a.jp
+xn--ntso0iqx3a.jp
+xn--6btw5a.jp
+xn--uuwu58a.jp
+xn--kltx9a.jp
+chiba.jp
+yokaichiba.chiba.jp
+noda.chiba.jp
+mihama.chiba.jp
+nagareyama.chiba.jp
+tateyama.chiba.jp
+mobara.chiba.jp
+nagara.chiba.jp
+ichihara.chiba.jp
+sodegaura.chiba.jp
+sakura.chiba.jp
+katsuura.chiba.jp
+sosa.chiba.jp
+shimofusa.chiba.jp
+yachimata.chiba.jp
+narita.chiba.jp
+hanamigawa.chiba.jp
+omigawa.chiba.jp
+kamogawa.chiba.jp
+ichikawa.chiba.jp
+mutsuzawa.chiba.jp
+kashiwa.chiba.jp
+kamagaya.chiba.jp
+ichinomiya.chiba.jp
+sakae.chiba.jp
+togane.chiba.jp
+inzai.chiba.jp
+chosei.chiba.jp
+asahi.chiba.jp
+funabashi.chiba.jp
+choshi.chiba.jp
+otaki.chiba.jp
+kozaki.chiba.jp
+isumi.chiba.jp
+shiroi.chiba.jp
+yokoshibahikari.chiba.jp
+midori.chiba.jp
+katori.chiba.jp
+kujukuri.chiba.jp
+shisui.chiba.jp
+chonan.chiba.jp
+kyonan.chiba.jp
+yotsukaido.chiba.jp
+matsudo.chiba.jp
+tohnosho.chiba.jp
+shirako.chiba.jp
+tako.chiba.jp
+abiko.chiba.jp
+narashino.chiba.jp
+minamiboso.chiba.jp
+oamishirasato.chiba.jp
+tomisato.chiba.jp
+chuo.chiba.jp
+yachiyo.chiba.jp
+onjuku.chiba.jp
+urayasu.chiba.jp
+kimitsu.chiba.jp
+futtsu.chiba.jp
+kisarazu.chiba.jp
+saga.jp
+saga.saga.jp
+hamatama.saga.jp
+kiyama.saga.jp
+kashima.saga.jp
+tara.saga.jp
+kitagata.saga.jp
+kitahata.saga.jp
+arita.saga.jp
+nishiarita.saga.jp
+ariake.saga.jp
+kamimine.saga.jp
+genkai.saga.jp
+kyuragi.saga.jp
+ogi.saga.jp
+omachi.saga.jp
+ouchi.saga.jp
+shiroishi.saga.jp
+kanzaki.saga.jp
+fukudomi.saga.jp
+yoshinogari.saga.jp
+imari.saga.jp
+hizen.saga.jp
+taku.saga.jp
+kouhoku.saga.jp
+tosu.saga.jp
+karatsu.saga.jp
+shiga.jp
+koka.shiga.jp
+nagahama.shiga.jp
+moriyama.shiga.jp
+takashima.shiga.jp
+maibara.shiga.jp
+notogawa.shiga.jp
+torahime.shiga.jp
+hikone.shiga.jp
+ryuoh.shiga.jp
+nishiazai.shiga.jp
+kosei.shiga.jp
+takatsuki.shiga.jp
+higashiomi.shiga.jp
+omihachiman.shiga.jp
+konan.shiga.jp
+aisho.shiga.jp
+gamo.shiga.jp
+toyosato.shiga.jp
+koto.shiga.jp
+ritto.shiga.jp
+yasu.shiga.jp
+kusatsu.shiga.jp
+otsu.shiga.jp
+osaka.jp
+kishiwada.osaka.jp
+ikeda.osaka.jp
+toyonaka.osaka.jp
+chihayaakasaka.osaka.jp
+higashiosaka.osaka.jp
+tadaoka.osaka.jp
+kaizuka.osaka.jp
+sayama.osaka.jp
+osakasayama.osaka.jp
+kadoma.osaka.jp
+matsubara.osaka.jp
+kashiwara.osaka.jp
+fujiidera.osaka.jp
+hirakata.osaka.jp
+kita.osaka.jp
+suita.osaka.jp
+neyagawa.osaka.jp
+higashiyodogawa.osaka.jp
+nose.osaka.jp
+shijonawate.osaka.jp
+minoh.osaka.jp
+sakai.osaka.jp
+moriguchi.osaka.jp
+tondabayashi.osaka.jp
+takaishi.osaka.jp
+taishi.osaka.jp
+nishi.osaka.jp
+higashisumiyoshi.osaka.jp
+ibaraki.osaka.jp
+misaki.osaka.jp
+takatsuki.osaka.jp
+izumi.osaka.jp
+tajiri.osaka.jp
+kumatori.osaka.jp
+kanan.osaka.jp
+hannan.osaka.jp
+sennan.osaka.jp
+yao.osaka.jp
+kawachinagano.osaka.jp
+izumisano.osaka.jp
+katano.osaka.jp
+abeno.osaka.jp
+habikino.osaka.jp
+toyono.osaka.jp
+minato.osaka.jp
+daito.osaka.jp
+shimamoto.osaka.jp
+chuo.osaka.jp
+izumiotsu.osaka.jp
+settsu.osaka.jp
+fukuoka.jp
+yamada.fukuoka.jp
+soeda.fukuoka.jp
+onga.fukuoka.jp
+koga.fukuoka.jp
+kasuga.fukuoka.jp
+ukiha.fukuoka.jp
+miyawaka.fukuoka.jp
+iizuka.fukuoka.jp
+nakama.fukuoka.jp
+hisayama.fukuoka.jp
+miyama.fukuoka.jp
+kawara.fukuoka.jp
+nogata.fukuoka.jp
+hakata.fukuoka.jp
+munakata.fukuoka.jp
+takata.fukuoka.jp
+omuta.fukuoka.jp
+nakagawa.fukuoka.jp
+yanagawa.fukuoka.jp
+tagawa.fukuoka.jp
+saigawa.fukuoka.jp
+okawa.fukuoka.jp
+hirokawa.fukuoka.jp
+ashiya.fukuoka.jp
+kasuya.fukuoka.jp
+yame.fukuoka.jp
+kurume.fukuoka.jp
+kurate.fukuoka.jp
+sue.fukuoka.jp
+shonai.fukuoka.jp
+tachiarai.fukuoka.jp
+kurogi.fukuoka.jp
+fukuchi.fukuoka.jp
+higashi.fukuoka.jp
+yukuhashi.fukuoka.jp
+nishi.fukuoka.jp
+okagaki.fukuoka.jp
+mizumaki.fukuoka.jp
+tsuiki.fukuoka.jp
+oki.fukuoka.jp
+inatsuki.fukuoka.jp
+minami.fukuoka.jp
+shinyoshitomi.fukuoka.jp
+umi.fukuoka.jp
+ogori.fukuoka.jp
+sasaguri.fukuoka.jp
+usui.fukuoka.jp
+keisen.fukuoka.jp
+buzen.fukuoka.jp
+chikuzen.fukuoka.jp
+chikugo.fukuoka.jp
+kaho.fukuoka.jp
+toho.fukuoka.jp
+chikuho.fukuoka.jp
+onojo.fukuoka.jp
+chikujo.fukuoka.jp
+miyako.fukuoka.jp
+chikushino.fukuoka.jp
+oto.fukuoka.jp
+chuo.fukuoka.jp
+dazaifu.fukuoka.jp
+shingu.fukuoka.jp
+toyotsu.fukuoka.jp
+shizuoka.jp
+gotemba.shizuoka.jp
+shimada.shizuoka.jp
+fujieda.shizuoka.jp
+yoshida.shizuoka.jp
+shimoda.shizuoka.jp
+shizuoka.shizuoka.jp
+mishima.shizuoka.jp
+haibara.shizuoka.jp
+makinohara.shizuoka.jp
+iwata.shizuoka.jp
+kakegawa.shizuoka.jp
+kikugawa.shizuoka.jp
+fujikawa.shizuoka.jp
+fujinomiya.shizuoka.jp
+arai.shizuoka.jp
+kosai.shizuoka.jp
+morimachi.shizuoka.jp
+fuji.shizuoka.jp
+omaezaki.shizuoka.jp
+matsuzaki.shizuoka.jp
+kannami.shizuoka.jp
+atami.shizuoka.jp
+izunokuni.shizuoka.jp
+fukuroi.shizuoka.jp
+kawanehon.shizuoka.jp
+susono.shizuoka.jp
+ito.shizuoka.jp
+hamamatsu.shizuoka.jp
+numazu.shizuoka.jp
+kawazu.shizuoka.jp
+izu.shizuoka.jp
+yaizu.shizuoka.jp
+higashiizu.shizuoka.jp
+nishiizu.shizuoka.jp
+minamiizu.shizuoka.jp
+shimizu.shizuoka.jp
+saitama.jp
+yoshida.saitama.jp
+toda.saitama.jp
+hasuda.saitama.jp
+hidaka.saitama.jp
+asaka.saitama.jp
+shiraoka.saitama.jp
+soka.saitama.jp
+saitama.saitama.jp
+sayama.saitama.jp
+moroyama.saitama.jp
+hatoyama.saitama.jp
+higashimatsuyama.saitama.jp
+tsurugashima.saitama.jp
+kawajima.saitama.jp
+iruma.saitama.jp
+ina.saitama.jp
+okegawa.saitama.jp
+namegawa.saitama.jp
+tokigawa.saitama.jp
+ogawa.saitama.jp
+arakawa.saitama.jp
+yoshikawa.saitama.jp
+kamikawa.saitama.jp
+urawa.saitama.jp
+tokorozawa.saitama.jp
+kumagaya.saitama.jp
+koshigaya.saitama.jp
+hatogaya.saitama.jp
+fukaya.saitama.jp
+omiya.saitama.jp
+niiza.saitama.jp
+kasukabe.saitama.jp
+kawagoe.saitama.jp
+ogose.saitama.jp
+satte.saitama.jp
+yokoze.saitama.jp
+warabi.saitama.jp
+kawaguchi.saitama.jp
+miyoshi.saitama.jp
+matsubushi.saitama.jp
+yorii.saitama.jp
+otaki.saitama.jp
+shiki.saitama.jp
+kuki.saitama.jp
+iwatsuki.saitama.jp
+ryokami.saitama.jp
+yoshimi.saitama.jp
+fujimi.saitama.jp
+kamiizumi.saitama.jp
+ranzan.saitama.jp
+sakado.saitama.jp
+yashio.saitama.jp
+honjo.saitama.jp
+ogano.saitama.jp
+minano.saitama.jp
+fujimino.saitama.jp
+hanno.saitama.jp
+yono.saitama.jp
+miyashiro.saitama.jp
+nagatoro.saitama.jp
+misato.saitama.jp
+kamisato.saitama.jp
+sugito.saitama.jp
+kitamoto.saitama.jp
+kazo.saitama.jp
+chichibu.saitama.jp
+higashichichibu.saitama.jp
+kounosu.saitama.jp
+hanyu.saitama.jp
+wakayama.jp
+arida.wakayama.jp
+kamitonda.wakayama.jp
+hidaka.wakayama.jp
+shirahama.wakayama.jp
+mihama.wakayama.jp
+wakayama.wakayama.jp
+kitayama.wakayama.jp
+kudoyama.wakayama.jp
+nachikatsuura.wakayama.jp
+yura.wakayama.jp
+yuasa.wakayama.jp
+aridagawa.wakayama.jp
+kozagawa.wakayama.jp
+hirogawa.wakayama.jp
+kinokawa.wakayama.jp
+koya.wakayama.jp
+koza.wakayama.jp
+tanabe.wakayama.jp
+iwade.wakayama.jp
+katsuragi.wakayama.jp
+taiji.wakayama.jp
+inami.wakayama.jp
+kainan.wakayama.jp
+gobo.wakayama.jp
+kimino.wakayama.jp
+misato.wakayama.jp
+hashimoto.wakayama.jp
+kushimoto.wakayama.jp
+shingu.wakayama.jp
+okayama.jp
+soja.okayama.jp
+kasaoka.okayama.jp
+okayama.okayama.jp
+tsuyama.okayama.jp
+hayashima.okayama.jp
+ibara.okayama.jp
+nishiawakura.okayama.jp
+akaiwa.okayama.jp
+maniwa.okayama.jp
+yakage.okayama.jp
+wake.okayama.jp
+nagi.okayama.jp
+asakuchi.okayama.jp
+setouchi.okayama.jp
+takahashi.okayama.jp
+misaki.okayama.jp
+kurashiki.okayama.jp
+niimi.okayama.jp
+kumenan.okayama.jp
+bizen.okayama.jp
+satosho.okayama.jp
+shinjo.okayama.jp
+tamano.okayama.jp
+kagamino.okayama.jp
+shoo.okayama.jp
+kibichuo.okayama.jp
+toyama.jp
+yamada.toyama.jp
+toga.toyama.jp
+takaoka.toyama.jp
+tateyama.toyama.jp
+toyama.toyama.jp
+johana.toyama.jp
+taira.toyama.jp
+nakaniikawa.toyama.jp
+namerikawa.toyama.jp
+oyabe.toyama.jp
+kurobe.toyama.jp
+asahi.toyama.jp
+kamiichi.toyama.jp
+funahashi.toyama.jp
+unazuki.toyama.jp
+inami.toyama.jp
+tonami.toyama.jp
+himi.toyama.jp
+nyuzen.toyama.jp
+nanto.toyama.jp
+fuchu.toyama.jp
+fukumitsu.toyama.jp
+imizu.toyama.jp
+uozu.toyama.jp
+kagoshima.jp
+kouyama.kagoshima.jp
+kagoshima.kagoshima.jp
+isa.kagoshima.jp
+kanoya.kagoshima.jp
+kawanabe.kagoshima.jp
+nakatane.kagoshima.jp
+minamitane.kagoshima.jp
+akune.kagoshima.jp
+nishinoomote.kagoshima.jp
+satsumasendai.kagoshima.jp
+makurazaki.kagoshima.jp
+hioki.kagoshima.jp
+amami.kagoshima.jp
+izumi.kagoshima.jp
+yusui.kagoshima.jp
+isen.kagoshima.jp
+kinko.kagoshima.jp
+soo.kagoshima.jp
+matsumoto.kagoshima.jp
+tarumizu.kagoshima.jp
+hiroshima.jp
+naka.hiroshima.jp
+saka.hiroshima.jp
+fukuyama.hiroshima.jp
+higashihiroshima.hiroshima.jp
+etajima.hiroshima.jp
+osakikamijima.hiroshima.jp
+shobara.hiroshima.jp
+takehara.hiroshima.jp
+mihara.hiroshima.jp
+sera.hiroshima.jp
+kaita.hiroshima.jp
+daiwa.hiroshima.jp
+otake.hiroshima.jp
+kure.hiroshima.jp
+hatsukaichi.hiroshima.jp
+onomichi.hiroshima.jp
+shinichi.hiroshima.jp
+seranishi.hiroshima.jp
+miyoshi.hiroshima.jp
+asaminami.hiroshima.jp
+kui.hiroshima.jp
+jinsekikogen.hiroshima.jp
+hongo.hiroshima.jp
+kumano.hiroshima.jp
+fuchu.hiroshima.jp
+tokushima.jp
+ichiba.tokushima.jp
+tokushima.tokushima.jp
+komatsushima.tokushima.jp
+mima.tokushima.jp
+nakagawa.tokushima.jp
+matsushige.tokushima.jp
+mugi.tokushima.jp
+sanagochi.tokushima.jp
+miyoshi.tokushima.jp
+wajiki.tokushima.jp
+minami.tokushima.jp
+aizumi.tokushima.jp
+shishikui.tokushima.jp
+anan.tokushima.jp
+kainan.tokushima.jp
+itano.tokushima.jp
+naruto.tokushima.jp
+fukushima.jp
+futaba.fukushima.jp
+otama.fukushima.jp
+kaneyama.fukushima.jp
+koriyama.fukushima.jp
+mishima.fukushima.jp
+fukushima.fukushima.jp
+soma.fukushima.jp
+okuma.fukushima.jp
+kitashiobara.fukushima.jp
+tanagura.fukushima.jp
+kitakata.fukushima.jp
+kawamata.fukushima.jp
+hirata.fukushima.jp
+sukagawa.fukushima.jp
+samegawa.fukushima.jp
+yugawa.fukushima.jp
+tamakawa.fukushima.jp
+shirakawa.fukushima.jp
+asakawa.fukushima.jp
+ishikawa.fukushima.jp
+hanawa.fukushima.jp
+showa.fukushima.jp
+aizubange.fukushima.jp
+namie.fukushima.jp
+date.fukushima.jp
+iitate.fukushima.jp
+bandai.fukushima.jp
+tenei.fukushima.jp
+higashi.fukushima.jp
+kagamiishi.fukushima.jp
+iwaki.fukushima.jp
+izumizaki.fukushima.jp
+yabuki.fukushima.jp
+kunimi.fukushima.jp
+koori.fukushima.jp
+yamatsuri.fukushima.jp
+taishin.fukushima.jp
+omotego.fukushima.jp
+nishigo.fukushima.jp
+nango.fukushima.jp
+shimogo.fukushima.jp
+ono.fukushima.jp
+furudono.fukushima.jp
+hirono.fukushima.jp
+inawashiro.fukushima.jp
+yamato.fukushima.jp
+aizumisato.fukushima.jp
+miharu.fukushima.jp
+aizuwakamatsu.fukushima.jp
+nishiaizu.fukushima.jp
+yanaizu.fukushima.jp
+gunma.jp
+kawaba.gunma.jp
+chiyoda.gunma.jp
+annaka.gunma.jp
+yoshioka.gunma.jp
+fujioka.gunma.jp
+tomioka.gunma.jp
+takayama.gunma.jp
+higashiagatsuma.gunma.jp
+katashina.gunma.jp
+kanna.gunma.jp
+naganohara.gunma.jp
+kanra.gunma.jp
+ora.gunma.jp
+itakura.gunma.jp
+tamamura.gunma.jp
+numata.gunma.jp
+shimonita.gunma.jp
+ota.gunma.jp
+shibukawa.gunma.jp
+meiwa.gunma.jp
+showa.gunma.jp
+maebashi.gunma.jp
+tatebayashi.gunma.jp
+takasaki.gunma.jp
+isesaki.gunma.jp
+minakami.gunma.jp
+oizumi.gunma.jp
+tsumagoi.gunma.jp
+midori.gunma.jp
+nakanojo.gunma.jp
+ueno.gunma.jp
+tsukiyono.gunma.jp
+shinto.gunma.jp
+nanmoku.gunma.jp
+kusatsu.gunma.jp
+kiryu.gunma.jp
+nara.jp
+kashiba.nara.jp
+yamatotakada.nara.jp
+uda.nara.jp
+ouda.nara.jp
+ikaruga.nara.jp
+kamikitayama.nara.jp
+shimokitayama.nara.jp
+yamatokoriyama.nara.jp
+ikoma.nara.jp
+kashihara.nara.jp
+nara.nara.jp
+nosegawa.nara.jp
+tenkawa.nara.jp
+miyake.nara.jp
+yamazoe.nara.jp
+gose.nara.jp
+mitsue.nara.jp
+sakurai.nara.jp
+kawai.nara.jp
+katsuragi.nara.jp
+shimoichi.nara.jp
+kawanishi.nara.jp
+oji.nara.jp
+kanmaki.nara.jp
+kurotaki.nara.jp
+kawakami.nara.jp
+soni.nara.jp
+tenri.nara.jp
+takatori.nara.jp
+heguri.nara.jp
+ando.nara.jp
+oyodo.nara.jp
+sango.nara.jp
+shinjo.nara.jp
+yoshino.nara.jp
+higashiyoshino.nara.jp
+tawaramoto.nara.jp
+koryo.nara.jp
+yamagata.jp
+oishida.yamagata.jp
+shirataka.yamagata.jp
+tsuruoka.yamagata.jp
+nakayama.yamagata.jp
+murayama.yamagata.jp
+kaneyama.yamagata.jp
+kaminoyama.yamagata.jp
+ohkura.yamagata.jp
+yamagata.yamagata.jp
+funagata.yamagata.jp
+takahata.yamagata.jp
+sakata.yamagata.jp
+sakegawa.yamagata.jp
+mamurogawa.yamagata.jp
+nishikawa.yamagata.jp
+mikawa.yamagata.jp
+obanazawa.yamagata.jp
+yonezawa.yamagata.jp
+tozawa.yamagata.jp
+yuza.yamagata.jp
+sagae.yamagata.jp
+yamanobe.yamagata.jp
+iide.yamagata.jp
+higashine.yamagata.jp
+oe.yamagata.jp
+nagai.yamagata.jp
+shonai.yamagata.jp
+asahi.yamagata.jp
+kawanishi.yamagata.jp
+oguni.yamagata.jp
+tendo.yamagata.jp
+shinjo.yamagata.jp
+nanyo.yamagata.jp
+kahoku.yamagata.jp
+niigata.jp
+aga.niigata.jp
+muika.niigata.jp
+nagaoka.niigata.jp
+uonuma.niigata.jp
+minamiuonuma.niigata.jp
+shibata.niigata.jp
+niigata.niigata.jp
+yoita.niigata.jp
+itoigawa.niigata.jp
+sekikawa.niigata.jp
+yuzawa.niigata.jp
+kariwa.niigata.jp
+ojiya.niigata.jp
+mitsuke.niigata.jp
+tsubame.niigata.jp
+tainai.niigata.jp
+tokamachi.niigata.jp
+kashiwazaki.niigata.jp
+izumozaki.niigata.jp
+tagami.niigata.jp
+murakami.niigata.jp
+omi.niigata.jp
+tsunan.niigata.jp
+gosen.niigata.jp
+sado.niigata.jp
+tochio.niigata.jp
+sanjo.niigata.jp
+yahiko.niigata.jp
+myoko.niigata.jp
+kamo.niigata.jp
+agano.niigata.jp
+seiro.niigata.jp
+seirou.niigata.jp
+joetsu.niigata.jp
+akita.jp
+oga.akita.jp
+kosaka.akita.jp
+kamioka.akita.jp
+ogata.akita.jp
+hachirogata.akita.jp
+akita.akita.jp
+kitaakita.akita.jp
+ikawa.akita.jp
+kyowa.akita.jp
+gojome.akita.jp
+mitane.akita.jp
+higashinaruse.akita.jp
+odate.akita.jp
+yokote.akita.jp
+moriyoshi.akita.jp
+katagami.akita.jp
+kamikoani.akita.jp
+daisen.akita.jp
+nikaho.akita.jp
+honjo.akita.jp
+yurihonjo.akita.jp
+kazuno.akita.jp
+noshiro.akita.jp
+fujisato.akita.jp
+misato.akita.jp
+honjyo.akita.jp
+semboku.akita.jp
+happou.akita.jp
+oita.jp
+bungotakada.oita.jp
+hasama.oita.jp
+himeshima.oita.jp
+usa.oita.jp
+taketa.oita.jp
+hita.oita.jp
+oita.oita.jp
+kokonoe.oita.jp
+kamitsue.oita.jp
+hiji.oita.jp
+kunisaki.oita.jp
+saiki.oita.jp
+usuki.oita.jp
+tsukumi.oita.jp
+bungoono.oita.jp
+yufu.oita.jp
+kuju.oita.jp
+beppu.oita.jp
+kusu.oita.jp
+kagawa.jp
+naoshima.kagawa.jp
+kotohira.kagawa.jp
+higashikagawa.kagawa.jp
+ayagawa.kagawa.jp
+marugame.kagawa.jp
+kanonji.kagawa.jp
+zentsuji.kagawa.jp
+sanuki.kagawa.jp
+uchinomi.kagawa.jp
+tonosho.kagawa.jp
+manno.kagawa.jp
+mitoyo.kagawa.jp
+takamatsu.kagawa.jp
+tadotsu.kagawa.jp
+utazu.kagawa.jp
+kanagawa.jp
+matsuda.kanagawa.jp
+yokosuka.kanagawa.jp
+hiratsuka.kanagawa.jp
+zama.kanagawa.jp
+ebina.kanagawa.jp
+minamiashigara.kanagawa.jp
+isehara.kanagawa.jp
+sagamihara.kanagawa.jp
+odawara.kanagawa.jp
+yugawara.kanagawa.jp
+miura.kanagawa.jp
+kamakura.kanagawa.jp
+yamakita.kanagawa.jp
+aikawa.kanagawa.jp
+kiyokawa.kanagawa.jp
+samukawa.kanagawa.jp
+fujisawa.kanagawa.jp
+ninomiya.kanagawa.jp
+hakone.kanagawa.jp
+ayase.kanagawa.jp
+nakai.kanagawa.jp
+kaisei.kanagawa.jp
+atsugi.kanagawa.jp
+zushi.kanagawa.jp
+chigasaki.kanagawa.jp
+oi.kanagawa.jp
+tsukui.kanagawa.jp
+hadano.kanagawa.jp
+oiso.kanagawa.jp
+yamato.kanagawa.jp
+ishikawa.jp
+uchinada.ishikawa.jp
+kaga.ishikawa.jp
+shika.ishikawa.jp
+wajima.ishikawa.jp
+tsubata.ishikawa.jp
+kawakita.ishikawa.jp
+kanazawa.ishikawa.jp
+tsurugi.ishikawa.jp
+nonoichi.ishikawa.jp
+nomi.ishikawa.jp
+hakui.ishikawa.jp
+hakusan.ishikawa.jp
+nanao.ishikawa.jp
+noto.ishikawa.jp
+nakanoto.ishikawa.jp
+kahoku.ishikawa.jp
+komatsu.ishikawa.jp
+anamizu.ishikawa.jp
+suzu.ishikawa.jp
+okinawa.jp
+naha.okinawa.jp
+tarama.okinawa.jp
+kumejima.okinawa.jp
+uruma.okinawa.jp
+kadena.okinawa.jp
+izena.okinawa.jp
+onna.okinawa.jp
+nishihara.okinawa.jp
+hirara.okinawa.jp
+ishikawa.okinawa.jp
+okinawa.okinawa.jp
+iheya.okinawa.jp
+ginoza.okinawa.jp
+urasoe.okinawa.jp
+yaese.okinawa.jp
+higashi.okinawa.jp
+shimoji.okinawa.jp
+ishigaki.okinawa.jp
+tonaki.okinawa.jp
+tokashiki.okinawa.jp
+kunigami.okinawa.jp
+gushikami.okinawa.jp
+zamami.okinawa.jp
+ogimi.okinawa.jp
+taketomi.okinawa.jp
+aguni.okinawa.jp
+yonaguni.okinawa.jp
+itoman.okinawa.jp
+yomitan.okinawa.jp
+ginowan.okinawa.jp
+nakijin.okinawa.jp
+kin.okinawa.jp
+nago.okinawa.jp
+nanjo.okinawa.jp
+kitadaito.okinawa.jp
+minamidaito.okinawa.jp
+motobu.okinawa.jp
+nakagusuku.okinawa.jp
+kitanakagusuku.okinawa.jp
+tomigusuku.okinawa.jp
+yonabaru.okinawa.jp
+haebaru.okinawa.jp
+xn--vgu402c.jp
+xn--7t0a264c.jp
+xn--d5qv7z876c.jp
+xn--5rtp49c.jp
+ac.jp
+xn--1lqs71d.jp
+xn--rht3d.jp
+xn--zbx025d.jp
+xn--5js045d.jp
+xn--klt787d.jp
+xn--kltp7d.jp
+xn--4it168d.jp
+ad.jp
+ed.jp
+xn--rht61e.jp
+xn--2m4a15e.jp
+xn--k7yn95e.jp
+mie.jp
+toba.mie.jp
+matsusaka.mie.jp
+suzuka.mie.jp
+mihama.mie.jp
+kameyama.mie.jp
+miyama.mie.jp
+shima.mie.jp
+kuwana.mie.jp
+meiwa.mie.jp
+kiwa.mie.jp
+inabe.mie.jp
+kawagoe.mie.jp
+ise.mie.jp
+minamiise.mie.jp
+watarai.mie.jp
+misugi.mie.jp
+asahi.mie.jp
+yokkaichi.mie.jp
+tamaki.mie.jp
+kisosaki.mie.jp
+taki.mie.jp
+taiki.mie.jp
+nabari.mie.jp
+tado.mie.jp
+kiho.mie.jp
+kumano.mie.jp
+ureshino.mie.jp
+udono.mie.jp
+komono.mie.jp
+tsu.mie.jp
+ehime.jp
+yawatahama.ehime.jp
+niihama.ehime.jp
+matsuyama.ehime.jp
+uwajima.ehime.jp
+kamijima.ehime.jp
+ikata.ehime.jp
+namikata.ehime.jp
+tobe.ehime.jp
+honai.ehime.jp
+masaki.ehime.jp
+imabari.ehime.jp
+ainan.ehime.jp
+kumakogen.ehime.jp
+toon.ehime.jp
+saijo.ehime.jp
+uchiko.ehime.jp
+matsuno.ehime.jp
+shikokuchuo.ehime.jp
+iyo.ehime.jp
+seiyo.ehime.jp
+kihoku.ehime.jp
+ozu.ehime.jp
+ne.jp
+shimane.jp
+hamada.shimane.jp
+ohda.shimane.jp
+masuda.shimane.jp
+yatsuka.shimane.jp
+ama.shimane.jp
+nishinoshima.shimane.jp
+okinoshima.shimane.jp
+hikawa.shimane.jp
+shimane.shimane.jp
+matsue.shimane.jp
+akagi.shimane.jp
+yasugi.shimane.jp
+kakinoki.shimane.jp
+hikimi.shimane.jp
+unnan.shimane.jp
+yakumo.shimane.jp
+izumo.shimane.jp
+higashiizumo.shimane.jp
+okuizumo.shimane.jp
+tsuwano.shimane.jp
+misato.shimane.jp
+gotsu.shimane.jp
+tamayu.shimane.jp
+iwate.jp
+yahaba.iwate.jp
+yamada.iwate.jp
+noda.iwate.jp
+morioka.iwate.jp
+tanohata.iwate.jp
+rikuzentakata.iwate.jp
+sumita.iwate.jp
+fujisawa.iwate.jp
+mizusawa.iwate.jp
+shiwa.iwate.jp
+ichinohe.iwate.jp
+ninohe.iwate.jp
+kunohe.iwate.jp
+iwate.iwate.jp
+fudai.iwate.jp
+karumai.iwate.jp
+kawai.iwate.jp
+otsuchi.iwate.jp
+kamaishi.iwate.jp
+shizukuishi.iwate.jp
+joboji.iwate.jp
+kuji.iwate.jp
+hanamaki.iwate.jp
+kuzumaki.iwate.jp
+kanegasaki.iwate.jp
+ichinoseki.iwate.jp
+kitakami.iwate.jp
+hiraizumi.iwate.jp
+iwaizumi.iwate.jp
+miyako.iwate.jp
+hirono.iwate.jp
+tono.iwate.jp
+ofunato.iwate.jp
+oshu.iwate.jp
+xn--uisz3g.jp
+xn--ntsq17g.jp
+lg.jp
+xn--32vp30h.jp
+xn--rny31h.jp
+xn--uist22h.jp
+xn--elqq16h.jp
+xn--mkru45i.jp
+miyagi.jp
+kakuda.miyagi.jp
+shiogama.miyagi.jp
+shikama.miyagi.jp
+matsushima.miyagi.jp
+higashimatsushima.miyagi.jp
+iwanuma.miyagi.jp
+ogawara.miyagi.jp
+ohira.miyagi.jp
+shibata.miyagi.jp
+murata.miyagi.jp
+onagawa.miyagi.jp
+furukawa.miyagi.jp
+taiwa.miyagi.jp
+tomiya.miyagi.jp
+wakuya.miyagi.jp
+tome.miyagi.jp
+semine.miyagi.jp
+shiroishi.miyagi.jp
+ishinomaki.miyagi.jp
+kawasaki.miyagi.jp
+osaki.miyagi.jp
+kami.miyagi.jp
+watari.miyagi.jp
+marumori.miyagi.jp
+natori.miyagi.jp
+zao.miyagi.jp
+tagajo.miyagi.jp
+misato.miyagi.jp
+yamamoto.miyagi.jp
+rifu.miyagi.jp
+minamisanriku.miyagi.jp
+shichikashuku.miyagi.jp
+tochigi.jp
+haga.tochigi.jp
+ashikaga.tochigi.jp
+tsuga.tochigi.jp
+moka.tochigi.jp
+oyama.tochigi.jp
+karasuyama.tochigi.jp
+kanuma.tochigi.jp
+nasushiobara.tochigi.jp
+ohtawara.tochigi.jp
+ohira.tochigi.jp
+sakura.tochigi.jp
+nishikata.tochigi.jp
+yaita.tochigi.jp
+kaminokawa.tochigi.jp
+takanezawa.tochigi.jp
+utsunomiya.tochigi.jp
+shioya.tochigi.jp
+ujiie.tochigi.jp
+shimotsuke.tochigi.jp
+iwafune.tochigi.jp
+ichikai.tochigi.jp
+motegi.tochigi.jp
+tochigi.tochigi.jp
+nogi.tochigi.jp
+mashiko.tochigi.jp
+nikko.tochigi.jp
+sano.tochigi.jp
+kuroiso.tochigi.jp
+bato.tochigi.jp
+mibu.tochigi.jp
+nasu.tochigi.jp
+aichi.jp
+handa.aichi.jp
+ama.aichi.jp
+takahama.aichi.jp
+mihama.aichi.jp
+inuyama.aichi.jp
+tobishima.aichi.jp
+tsushima.aichi.jp
+tahara.aichi.jp
+shitara.aichi.jp
+kira.aichi.jp
+higashiura.aichi.jp
+iwakura.aichi.jp
+chita.aichi.jp
+kota.aichi.jp
+toyota.aichi.jp
+toyokawa.aichi.jp
+inazawa.aichi.jp
+ichinomiya.aichi.jp
+kariya.aichi.jp
+kanie.aichi.jp
+toyoake.aichi.jp
+asuke.aichi.jp
+tokoname.aichi.jp
+toyone.aichi.jp
+kasugai.aichi.jp
+tokai.aichi.jp
+aisai.aichi.jp
+toei.aichi.jp
+owariasahi.aichi.jp
+oguchi.aichi.jp
+toyohashi.aichi.jp
+miyoshi.aichi.jp
+komaki.aichi.jp
+okazaki.aichi.jp
+isshiki.aichi.jp
+yatomi.aichi.jp
+gamagori.aichi.jp
+hekinan.aichi.jp
+konan.aichi.jp
+nisshin.aichi.jp
+togo.aichi.jp
+nishio.aichi.jp
+anjo.aichi.jp
+shinshiro.aichi.jp
+fuso.aichi.jp
+seto.aichi.jp
+obu.aichi.jp
+oharu.aichi.jp
+kiyosu.aichi.jp
+shikatsu.aichi.jp
+chiryu.aichi.jp
+hazu.aichi.jp
+kochi.jp
+yasuda.kochi.jp
+hidaka.kochi.jp
+motoyama.kochi.jp
+mihara.kochi.jp
+yusuhara.kochi.jp
+nakamura.kochi.jp
+tosa.kochi.jp
+nishitosa.kochi.jp
+kitagawa.kochi.jp
+niyodogawa.kochi.jp
+sakawa.kochi.jp
+okawa.kochi.jp
+geisei.kochi.jp
+ochi.kochi.jp
+kochi.kochi.jp
+umaji.kochi.jp
+aki.kochi.jp
+susaki.kochi.jp
+otsuki.kochi.jp
+kagami.kochi.jp
+kami.kochi.jp
+nahari.kochi.jp
+sukumo.kochi.jp
+ino.kochi.jp
+tsuno.kochi.jp
+higashitsuno.kochi.jp
+muroto.kochi.jp
+toyo.kochi.jp
+otoyo.kochi.jp
+nankoku.kochi.jp
+tosashimizu.kochi.jp
+yamaguchi.jp
+tokuyama.yamaguchi.jp
+oshima.yamaguchi.jp
+toyota.yamaguchi.jp
+ube.yamaguchi.jp
+tabuse.yamaguchi.jp
+hagi.yamaguchi.jp
+shimonoseki.yamaguchi.jp
+iwakuni.yamaguchi.jp
+hikari.yamaguchi.jp
+shunan.yamaguchi.jp
+nagato.yamaguchi.jp
+abu.yamaguchi.jp
+hofu.yamaguchi.jp
+mitou.yamaguchi.jp
+kudamatsu.yamaguchi.jp
+yuu.yamaguchi.jp
+yamanashi.jp
+fujiyoshida.yamanashi.jp
+tabayama.yamanashi.jp
+uenohara.yamanashi.jp
+nishikatsura.yamanashi.jp
+hayakawa.yamanashi.jp
+fujikawa.yamanashi.jp
+narusawa.yamanashi.jp
+showa.yamanashi.jp
+kosuge.yamanashi.jp
+kai.yamanashi.jp
+nakamichi.yamanashi.jp
+yamanashi.yamanashi.jp
+doshi.yamanashi.jp
+nirasaki.yamanashi.jp
+fuefuki.yamanashi.jp
+otsuki.yamanashi.jp
+yamanakako.yamanashi.jp
+fujikawaguchiko.yamanashi.jp
+oshino.yamanashi.jp
+ichikawamisato.yamanashi.jp
+hokuto.yamanashi.jp
+chuo.yamanashi.jp
+minami-alps.yamanashi.jp
+nanbu.yamanashi.jp
+minobu.yamanashi.jp
+kofu.yamanashi.jp
+koshu.yamanashi.jp
+tsuru.yamanashi.jp
+ibaraki.jp
+tsukuba.ibaraki.jp
+koga.ibaraki.jp
+naka.ibaraki.jp
+hitachinaka.ibaraki.jp
+kasama.ibaraki.jp
+omitama.ibaraki.jp
+iwama.ibaraki.jp
+kashima.ibaraki.jp
+shimotsuma.ibaraki.jp
+ina.ibaraki.jp
+uchihara.ibaraki.jp
+yawara.ibaraki.jp
+kasumigaura.ibaraki.jp
+tsuchiura.ibaraki.jp
+yamagata.ibaraki.jp
+namegata.ibaraki.jp
+hitachiota.ibaraki.jp
+sakuragawa.ibaraki.jp
+ogawa.ibaraki.jp
+sowa.ibaraki.jp
+hitachiomiya.ibaraki.jp
+moriya.ibaraki.jp
+tomobe.ibaraki.jp
+toride.ibaraki.jp
+tone.ibaraki.jp
+shimodate.ibaraki.jp
+sakai.ibaraki.jp
+tokai.ibaraki.jp
+oarai.ibaraki.jp
+chikusei.ibaraki.jp
+takahagi.ibaraki.jp
+asahi.ibaraki.jp
+hitachi.ibaraki.jp
+ibaraki.ibaraki.jp
+ryugasaki.ibaraki.jp
+inashiki.ibaraki.jp
+yuki.ibaraki.jp
+ami.ibaraki.jp
+tamatsukuri.ibaraki.jp
+bando.ibaraki.jp
+daigo.ibaraki.jp
+miho.ibaraki.jp
+itako.ibaraki.jp
+fujishiro.ibaraki.jp
+joso.ibaraki.jp
+shirosato.ibaraki.jp
+mito.ibaraki.jp
+yachiyo.ibaraki.jp
+suifu.ibaraki.jp
+ushiku.ibaraki.jp
+kamisu.ibaraki.jp
+nagasaki.jp
+obama.nagasaki.jp
+tsushima.nagasaki.jp
+kawatana.nagasaki.jp
+shimabara.nagasaki.jp
+omura.nagasaki.jp
+matsuura.nagasaki.jp
+chijiwa.nagasaki.jp
+isahaya.nagasaki.jp
+saikai.nagasaki.jp
+seihi.nagasaki.jp
+nagasaki.nagasaki.jp
+iki.nagasaki.jp
+hasami.nagasaki.jp
+unzen.nagasaki.jp
+sasebo.nagasaki.jp
+hirado.nagasaki.jp
+oseto.nagasaki.jp
+goto.nagasaki.jp
+shinkamigoto.nagasaki.jp
+togitsu.nagasaki.jp
+kuchinotsu.nagasaki.jp
+futsu.nagasaki.jp
+miyazaki.jp
+shiiba.miyazaki.jp
+hyuga.miyazaki.jp
+nobeoka.miyazaki.jp
+morotsuka.miyazaki.jp
+kushima.miyazaki.jp
+nishimera.miyazaki.jp
+kitaura.miyazaki.jp
+kitakata.miyazaki.jp
+mimata.miyazaki.jp
+kitagawa.miyazaki.jp
+kadogawa.miyazaki.jp
+aya.miyazaki.jp
+takanabe.miyazaki.jp
+gokase.miyazaki.jp
+kobayashi.miyazaki.jp
+takazaki.miyazaki.jp
+miyazaki.miyazaki.jp
+kawaminami.miyazaki.jp
+kunitomi.miyazaki.jp
+shintomi.miyazaki.jp
+nichinan.miyazaki.jp
+kijo.miyazaki.jp
+miyakonojo.miyazaki.jp
+ebino.miyazaki.jp
+tsuno.miyazaki.jp
+saito.miyazaki.jp
+takaharu.miyazaki.jp
+aomori.jp
+towada.aomori.jp
+tsuruta.aomori.jp
+misawa.aomori.jp
+hachinohe.aomori.jp
+shichinohe.aomori.jp
+sannohe.aomori.jp
+gonohe.aomori.jp
+rokunohe.aomori.jp
+oirase.aomori.jp
+hiranai.aomori.jp
+itayanagi.aomori.jp
+kuroishi.aomori.jp
+noheji.aomori.jp
+hirosaki.aomori.jp
+hashikami.aomori.jp
+owani.aomori.jp
+nakadomari.aomori.jp
+aomori.aomori.jp
+shingo.aomori.jp
+takko.aomori.jp
+tsugaru.aomori.jp
+mutsu.aomori.jp
+tottori.jp
+kawahara.tottori.jp
+kotoura.tottori.jp
+wakasa.tottori.jp
+misasa.tottori.jp
+koge.tottori.jp
+tottori.tottori.jp
+nichinan.tottori.jp
+yonago.tottori.jp
+hino.tottori.jp
+sakaiminato.tottori.jp
+nanbu.tottori.jp
+yazu.tottori.jp
+chizu.tottori.jp
+fukui.jp
+ikeda.fukui.jp
+tsuruga.fukui.jp
+obama.fukui.jp
+takahama.fukui.jp
+mihama.fukui.jp
+katsuyama.fukui.jp
+wakasa.fukui.jp
+sabae.fukui.jp
+sakai.fukui.jp
+ohi.fukui.jp
+eiheiji.fukui.jp
+fukui.fukui.jp
+echizen.fukui.jp
+minamiechizen.fukui.jp
+ono.fukui.jp
+xn--8ltr62k.jp
+xn--5rtq34k.jp
+xn--djty4k.jp
+xn--nit225k.jp
+xn--4it797k.jp
+xn--pssu33l.jp
+xn--qqqt11m.jp
+xn--c3s14m.jp
+xn--1lqs03n.jp
+xn--ehqz56n.jp
+xn--0trq7p7nn.jp
+xn--tor131o.jp
+xn--kbrq7o.jp
+co.jp
+hokkaido.jp
+ikeda.hokkaido.jp
+teshikaga.hokkaido.jp
+shibecha.hokkaido.jp
+hidaka.hokkaido.jp
+bifuka.hokkaido.jp
+shiranuka.hokkaido.jp
+kuriyama.hokkaido.jp
+tohma.hokkaido.jp
+kitahiroshima.hokkaido.jp
+fukushima.hokkaido.jp
+saroma.hokkaido.jp
+atsuma.hokkaido.jp
+abira.hokkaido.jp
+akabira.hokkaido.jp
+obira.hokkaido.jp
+furubira.hokkaido.jp
+ozora.hokkaido.jp
+higashikagura.hokkaido.jp
+toyoura.hokkaido.jp
+mikasa.hokkaido.jp
+tsukigata.hokkaido.jp
+numata.hokkaido.jp
+nakagawa.hokkaido.jp
+fukagawa.hokkaido.jp
+sunagawa.hokkaido.jp
+kamisunagawa.hokkaido.jp
+urakawa.hokkaido.jp
+asahikawa.hokkaido.jp
+higashikawa.hokkaido.jp
+takikawa.hokkaido.jp
+kamikawa.hokkaido.jp
+shimokawa.hokkaido.jp
+mukawa.hokkaido.jp
+iwamizawa.hokkaido.jp
+eniwa.hokkaido.jp
+kyowa.hokkaido.jp
+toya.hokkaido.jp
+matsumae.hokkaido.jp
+nanae.hokkaido.jp
+shikabe.hokkaido.jp
+kayabe.hokkaido.jp
+horonobe.hokkaido.jp
+otobe.hokkaido.jp
+naie.hokkaido.jp
+mashike.hokkaido.jp
+otofuke.hokkaido.jp
+imakane.hokkaido.jp
+okoppe.hokkaido.jp
+nishiokoppe.hokkaido.jp
+chitose.hokkaido.jp
+date.hokkaido.jp
+hakodate.hokkaido.jp
+takinoue.hokkaido.jp
+bibai.hokkaido.jp
+tomakomai.hokkaido.jp
+wakkanai.hokkaido.jp
+horokanai.hokkaido.jp
+iwanai.hokkaido.jp
+kamoenai.hokkaido.jp
+utashinai.hokkaido.jp
+kikonai.hokkaido.jp
+kuromatsunai.hokkaido.jp
+nakasatsunai.hokkaido.jp
+biei.hokkaido.jp
+yoichi.hokkaido.jp
+kembuchi.hokkaido.jp
+shiriuchi.hokkaido.jp
+esashi.hokkaido.jp
+akkeshi.hokkaido.jp
+rankoshi.hokkaido.jp
+moseushi.hokkaido.jp
+rishirifuji.hokkaido.jp
+shimamaki.hokkaido.jp
+taiki.hokkaido.jp
+niki.hokkaido.jp
+kitami.hokkaido.jp
+toyotomi.hokkaido.jp
+shikaoi.hokkaido.jp
+shiraoi.hokkaido.jp
+shari.hokkaido.jp
+ishikari.hokkaido.jp
+tomari.hokkaido.jp
+abashiri.hokkaido.jp
+rishiri.hokkaido.jp
+biratori.hokkaido.jp
+kutchan.hokkaido.jp
+muroran.hokkaido.jp
+esan.hokkaido.jp
+shakotan.hokkaido.jp
+rebun.hokkaido.jp
+toyako.hokkaido.jp
+erimo.hokkaido.jp
+yakumo.hokkaido.jp
+furano.hokkaido.jp
+kamifurano.hokkaido.jp
+minamifurano.hokkaido.jp
+hiroo.hokkaido.jp
+obihiro.hokkaido.jp
+kushiro.hokkaido.jp
+haboro.hokkaido.jp
+bihoro.hokkaido.jp
+kamishihoro.hokkaido.jp
+ashoro.hokkaido.jp
+nanporo.hokkaido.jp
+nayoro.hokkaido.jp
+nemuro.hokkaido.jp
+kiyosato.hokkaido.jp
+oketo.hokkaido.jp
+hokuto.hokkaido.jp
+assabu.hokkaido.jp
+shintoku.hokkaido.jp
+wassamu.hokkaido.jp
+oumu.hokkaido.jp
+niikappu.hokkaido.jp
+otoineppu.hokkaido.jp
+kunneppu.hokkaido.jp
+pippu.hokkaido.jp
+otaru.hokkaido.jp
+takasu.hokkaido.jp
+ebetsu.hokkaido.jp
+aibetsu.hokkaido.jp
+shibetsu.hokkaido.jp
+ashibetsu.hokkaido.jp
+noboribetsu.hokkaido.jp
+embetsu.hokkaido.jp
+mombetsu.hokkaido.jp
+nakatombetsu.hokkaido.jp
+honbetsu.hokkaido.jp
+hamatonbetsu.hokkaido.jp
+kimobetsu.hokkaido.jp
+sobetsu.hokkaido.jp
+tobetsu.hokkaido.jp
+rikubetsu.hokkaido.jp
+chippubetsu.hokkaido.jp
+tsubetsu.hokkaido.jp
+shinshinotsu.hokkaido.jp
+sarufutsu.hokkaido.jp
+urausu.hokkaido.jp
+uryu.hokkaido.jp
+hokuryu.hokkaido.jp
+shimizu.hokkaido.jp
+koshimizu.hokkaido.jp
+go.jp
+hyogo.jp
+tamba.hyogo.jp
+sanda.hyogo.jp
+kasuga.hyogo.jp
+taka.hyogo.jp
+toyooka.hyogo.jp
+yoka.hyogo.jp
+takarazuka.hyogo.jp
+sasayama.hyogo.jp
+harima.hyogo.jp
+inagawa.hyogo.jp
+kakogawa.hyogo.jp
+ichikawa.hyogo.jp
+kamikawa.hyogo.jp
+yokawa.hyogo.jp
+ashiya.hyogo.jp
+nishinomiya.hyogo.jp
+kasai.hyogo.jp
+akashi.hyogo.jp
+taishi.hyogo.jp
+kawanishi.hyogo.jp
+awaji.hyogo.jp
+minamiawaji.hyogo.jp
+himeji.hyogo.jp
+aogaki.hyogo.jp
+amagasaki.hyogo.jp
+fukusaki.hyogo.jp
+nishiwaki.hyogo.jp
+goshiki.hyogo.jp
+miki.hyogo.jp
+itami.hyogo.jp
+aioi.hyogo.jp
+kamigori.hyogo.jp
+sannan.hyogo.jp
+shinonsen.hyogo.jp
+asago.hyogo.jp
+takasago.hyogo.jp
+ako.hyogo.jp
+takino.hyogo.jp
+ono.hyogo.jp
+tatsuno.hyogo.jp
+yashiro.hyogo.jp
+shiso.hyogo.jp
+sumoto.hyogo.jp
+sayo.hyogo.jp
+yabu.hyogo.jp
+shingu.hyogo.jp
+nagano.jp
+hakuba.nagano.jp
+wada.nagano.jp
+miyada.nagano.jp
+ikeda.nagano.jp
+ueda.nagano.jp
+iida.nagano.jp
+yasaka.nagano.jp
+ikusaka.nagano.jp
+suzaka.nagano.jp
+ooshika.nagano.jp
+yasuoka.nagano.jp
+takayama.nagano.jp
+iiyama.nagano.jp
+kisofukushima.nagano.jp
+iijima.nagano.jp
+chikuma.nagano.jp
+ina.nagano.jp
+tateshina.nagano.jp
+iizuna.nagano.jp
+hara.nagano.jp
+togura.nagano.jp
+miasa.nagano.jp
+yamagata.nagano.jp
+miyota.nagano.jp
+nakagawa.nagano.jp
+nagawa.nagano.jp
+ogawa.nagano.jp
+matsukawa.nagano.jp
+karuizawa.nagano.jp
+minowa.nagano.jp
+minamiminowa.nagano.jp
+ookuwa.nagano.jp
+suwa.nagano.jp
+shimosuwa.nagano.jp
+okaya.nagano.jp
+hiraya.nagano.jp
+sakae.nagano.jp
+komagane.nagano.jp
+obuse.nagano.jp
+takagi.nagano.jp
+asahi.nagano.jp
+achi.nagano.jp
+omachi.nagano.jp
+shinanomachi.nagano.jp
+yamanouchi.nagano.jp
+togakushi.nagano.jp
+sakaki.nagano.jp
+minamimaki.nagano.jp
+otaki.nagano.jp
+kitaaiki.nagano.jp
+minamiaiki.nagano.jp
+aoki.nagano.jp
+mochizuki.nagano.jp
+kawakami.nagano.jp
+fujimi.nagano.jp
+omi.nagano.jp
+tomi.nagano.jp
+otari.nagano.jp
+shiojiri.nagano.jp
+takamori.nagano.jp
+anan.nagano.jp
+nozawaonsen.nagano.jp
+sakuho.nagano.jp
+nagano.nagano.jp
+nakano.nagano.jp
+chino.nagano.jp
+azumino.nagano.jp
+tatsuno.nagano.jp
+komoro.nagano.jp
+nagiso.nagano.jp
+kiso.nagano.jp
+matsumoto.nagano.jp
+saku.nagano.jp
+chikuhoku.nagano.jp
+agematsu.nagano.jp
+kumamoto.jp
+yamaga.kumamoto.jp
+kashima.kumamoto.jp
+nishihara.kumamoto.jp
+amakusa.kumamoto.jp
+kamiamakusa.kumamoto.jp
+minamata.kumamoto.jp
+mifune.kumamoto.jp
+kikuchi.kumamoto.jp
+hitoyoshi.kumamoto.jp
+mashiki.kumamoto.jp
+uki.kumamoto.jp
+oguni.kumamoto.jp
+minamioguni.kumamoto.jp
+takamori.kumamoto.jp
+arao.kumamoto.jp
+yatsushiro.kumamoto.jp
+aso.kumamoto.jp
+yamato.kumamoto.jp
+kumamoto.kumamoto.jp
+sumoto.kumamoto.jp
+uto.kumamoto.jp
+gyokuto.kumamoto.jp
+choyo.kumamoto.jp
+nagasu.kumamoto.jp
+ozu.kumamoto.jp
+kyoto.jp
+kyotamba.kyoto.jp
+seika.kyoto.jp
+kameoka.kyoto.jp
+wazuka.kyoto.jp
+fukuchiyama.kyoto.jp
+higashiyama.kyoto.jp
+kumiyama.kyoto.jp
+yamashina.kyoto.jp
+ujitawara.kyoto.jp
+yawata.kyoto.jp
+kita.kyoto.jp
+tanabe.kyoto.jp
+kyotanabe.kyoto.jp
+ayabe.kyoto.jp
+ide.kyoto.jp
+ine.kyoto.jp
+uji.kyoto.jp
+oyamazaki.kyoto.jp
+minami.kyoto.jp
+nantan.kyoto.jp
+kyotango.kyoto.jp
+muko.kyoto.jp
+kamo.kyoto.jp
+minamiyamashiro.kyoto.jp
+nakagyo.kyoto.jp
+nagaokakyo.kyoto.jp
+sakyo.kyoto.jp
+joyo.kyoto.jp
+maizuru.kyoto.jp
+miyazu.kyoto.jp
+kizu.kyoto.jp
+xn--1ctwo.jp
+tokyo.jp
+machida.tokyo.jp
+sumida.tokyo.jp
+chiyoda.tokyo.jp
+mitaka.tokyo.jp
+katsushika.tokyo.jp
+tama.tokyo.jp
+okutama.tokyo.jp
+higashimurayama.tokyo.jp
+musashimurayama.tokyo.jp
+aogashima.tokyo.jp
+akishima.tokyo.jp
+oshima.tokyo.jp
+toshima.tokyo.jp
+kouzushima.tokyo.jp
+nerima.tokyo.jp
+hinohara.tokyo.jp
+ogasawara.tokyo.jp
+kodaira.tokyo.jp
+hamura.tokyo.jp
+fussa.tokyo.jp
+kita.tokyo.jp
+ota.tokyo.jp
+shinagawa.tokyo.jp
+edogawa.tokyo.jp
+arakawa.tokyo.jp
+tachikawa.tokyo.jp
+setagaya.tokyo.jp
+shibuya.tokyo.jp
+komae.tokyo.jp
+hinode.tokyo.jp
+ome.tokyo.jp
+higashikurume.tokyo.jp
+kiyose.tokyo.jp
+koganei.tokyo.jp
+inagi.tokyo.jp
+adachi.tokyo.jp
+kunitachi.tokyo.jp
+itabashi.tokyo.jp
+kokubunji.tokyo.jp
+hachioji.tokyo.jp
+suginami.tokyo.jp
+mizuho.tokyo.jp
+hachijo.tokyo.jp
+nakano.tokyo.jp
+hino.tokyo.jp
+musashino.tokyo.jp
+akiruno.tokyo.jp
+meguro.tokyo.jp
+higashiyamato.tokyo.jp
+minato.tokyo.jp
+taito.tokyo.jp
+koto.tokyo.jp
+chuo.tokyo.jp
+bunkyo.tokyo.jp
+chofu.tokyo.jp
+fuchu.tokyo.jp
+shinjuku.tokyo.jp
+xn--6orx2r.jp
+gr.jp
+or.jp
+xn--efvn9s.jp
+xn--4pvxs.jp
+blogspot.jp
+xn--8pvr4u.jp
+gifu.jp
+ikeda.gifu.jp
+hida.gifu.jp
+tomika.gifu.jp
+takayama.gifu.jp
+hashima.gifu.jp
+ena.gifu.jp
+sekigahara.gifu.jp
+kakamigahara.gifu.jp
+kasahara.gifu.jp
+yamagata.gifu.jp
+kitagata.gifu.jp
+ibigawa.gifu.jp
+nakatsugawa.gifu.jp
+shirakawa.gifu.jp
+higashishirakawa.gifu.jp
+mitake.gifu.jp
+kawaue.gifu.jp
+sakahogi.gifu.jp
+anpachi.gifu.jp
+wanouchi.gifu.jp
+ogaki.gifu.jp
+seki.gifu.jp
+toki.gifu.jp
+mizunami.gifu.jp
+tajimi.gifu.jp
+kani.gifu.jp
+tarui.gifu.jp
+ginan.gifu.jp
+godo.gifu.jp
+gujo.gifu.jp
+minokamo.gifu.jp
+mino.gifu.jp
+yoro.gifu.jp
+hichiso.gifu.jp
+gifu.gifu.jp
+motosu.gifu.jp
+kasamatsu.gifu.jp
+yaotsu.gifu.jp
+xn--klty5x.jp
+xn--djrs72d6uy.jp
+xn--rht27z.jp
+ç¦äº•.jp
+æ±äº¬.jp
+大分.jp
+é³¥å–.jp
+å±±å£.jp
+宮城.jp
+茨城.jp
+愛媛.jp
+富山.jp
+岡山.jp
+和歌山.jp
+ç¦å²¡.jp
+é™å²¡.jp
+鹿å…島.jp
+広島.jp
+徳島.jp
+ç¦å³¶.jp
+宮崎.jp
+é•·å´Ž.jp
+神奈å·.jp
+石å·.jp
+香å·.jp
+兵庫.jp
+山形.jp
+岩手.jp
+栃木.jp
+熊本.jp
+島根.jp
+山梨.jp
+é’森.jp
+新潟.jp
+埼玉.jp
+秋田.jp
+愛知.jp
+高知.jp
+沖縄.jp
+奈良.jp
+åƒè‘‰.jp
+ä½è³€.jp
+滋賀.jp
+北海é“.jp
+京都.jp
+三é‡.jp
+長野.jp
+å²é˜œ.jp
+大阪.jp
+群馬.jp
+kp
+tra.kp
+org.kp
+com.kp
+rep.kp
+edu.kp
+gov.kp
+help
+mp
+camp
+jmp
+dnp
+gop
+hiphop
+shop
+dunlop
+coop
+top
+app
+aarp
+sharp
+makeup
+gallup
+group
+stcgroup
+kuokgroup
+olayangroup
+rsvp
+xn--mgbi4ecexp
+aq
+xn--y9a3aq
+gq
+iq
+org.iq
+mil.iq
+com.iq
+net.iq
+edu.iq
+gov.iq
+mq
+esq
+ar
+gob.ar
+org.ar
+mil.ar
+com.ar
+blogspot.com.ar
+tur.ar
+net.ar
+int.ar
+edu.ar
+gov.ar
+xn--mgberp4a5d4ar
+bar
+car
+goodyear
+solar
+mopar
+tatar
+star
+movistar
+neustar
+jaguar
+br
+g12.br
+b.br
+imb.br
+rec.br
+psc.br
+etc.br
+med.br
+bmd.br
+fnd.br
+ind.br
+trd.br
+ggf.br
+inf.br
+leg.br
+slg.br
+zlg.br
+cng.br
+eng.br
+blog.br
+flog.br
+vlog.br
+ppg.br
+org.br
+wiki.br
+psi.br
+eti.br
+taxi.br
+lel.br
+mil.br
+qsl.br
+am.br
+adm.br
+fm.br
+cim.br
+com.br
+blogspot.com.br
+ecn.br
+eco.br
+odo.br
+teo.br
+bio.br
+radio.br
+pro.br
+ato.br
+mp.br
+emp.br
+tmp.br
+coop.br
+esp.br
+arq.br
+far.br
+agr.br
+jor.br
+ntr.br
+tur.br
+jus.br
+mus.br
+mat.br
+net.br
+vet.br
+cnt.br
+fot.br
+not.br
+art.br
+fst.br
+edu.br
+adv.br
+gov.br
+srv.br
+tv.br
+cr
+sa.cr
+ac.cr
+ed.cr
+fi.cr
+co.cr
+go.cr
+or.cr
+weber
+soccer
+kinder
+beer
+engineer
+pioneer
+career
+grainger
+boehringer
+rocher
+kosher
+weather
+brother
+frontier
+cartier
+locker
+poker
+broker
+dealer
+schaeffler
+chrysler
+lamer
+sener
+juniper
+theater
+vermögensberater
+walter
+center
+lancaster
+monster
+blockbuster
+computer
+discover
+swiftcover
+landrover
+observer
+wolterskluwer
+lawyer
+pfizer
+fr
+greta.fr
+assedic.fr
+prd.fr
+huissier-justice.fr
+chirurgiens-dentistes-en-france.fr
+veterinaire.fr
+presse.fr
+cci.fr
+chambagri.fr
+com.fr
+nom.fr
+tm.fr
+pharmacien.fr
+medecin.fr
+asso.fr
+experts-comptables.fr
+notaires.fr
+chirurgiens-dentistes.fr
+avoues.fr
+fbx-os.fr
+freebox-os.fr
+fbxos.fr
+freeboxos.fr
+avocat.fr
+blogspot.fr
+geometre-expert.fr
+port.fr
+aeroport.fr
+gouv.fr
+xn--mgbqly7cvafr
+sfr
+gr
+org.gr
+com.gr
+net.gr
+blogspot.gr
+edu.gr
+gov.gr
+hr
+name.hr
+com.hr
+from.hr
+blogspot.hr
+iz.hr
+ruhr
+ir
+xn--mgba3a4f16a.ir
+xn--mgba3a4fra.ir
+ac.ir
+id.ir
+org.ir
+sch.ir
+co.ir
+net.ir
+gov.ir
+ايران.ir
+ایران.ir
+hair
+repair
+weir
+flir
+kr
+ac.kr
+sc.kr
+ne.kr
+pe.kr
+re.kr
+kg.kr
+gyeonggi.kr
+gyeongbuk.kr
+chungbuk.kr
+jeonbuk.kr
+mil.kr
+seoul.kr
+gyeongnam.kr
+chungnam.kr
+jeonnam.kr
+ulsan.kr
+busan.kr
+incheon.kr
+daejeon.kr
+gangwon.kr
+co.kr
+go.kr
+or.kr
+es.kr
+hs.kr
+ms.kr
+blogspot.kr
+daegu.kr
+jeju.kr
+gwangju.kr
+flickr
+lr
+org.lr
+com.lr
+net.lr
+edu.lr
+gov.lr
+mr
+blogspot.mr
+gov.mr
+nr
+org.nr
+com.nr
+info.nr
+net.nr
+edu.nr
+gov.nr
+biz.nr
+scor
+author
+frontdoor
+actor
+doctor
+realtor
+pr
+isla.pr
+ac.pr
+name.pr
+prof.pr
+org.pr
+com.pr
+info.pr
+pro.pr
+net.pr
+est.pr
+edu.pr
+gov.pr
+biz.pr
+sr
+tr
+k12.tr
+web.tr
+nc.tr
+gov.nc.tr
+name.tr
+org.tr
+bel.tr
+tel.tr
+mil.tr
+pol.tr
+com.tr
+blogspot.com.tr
+gen.tr
+info.tr
+kep.tr
+dr.tr
+bbs.tr
+net.tr
+edu.tr
+av.tr
+gov.tr
+tv.tr
+biz.tr
+ftr
+mtr
+dabur
+dvr
+xn--fiqs8s
+xn--fiqz9s
+as
+gov.as
+bnpparibas
+vegas
+villas
+christmas
+sas
+bs
+org.bs
+com.bs
+net.bs
+edu.bs
+gov.bs
+cbs
+jobs
+sbs
+ubs
+graphics
+pics
+kerrylogistics
+analytics
+docs
+ads
+dds
+lds
+mcdonalds
+goodhands
+fairwinds
+diamonds
+homegoods
+cards
+es
+gob.es
+org.es
+com.es
+blogspot.com.es
+nom.es
+edu.es
+services
+xn--mgb2ddes
+codes
+watches
+hughes
+supplies
+industries
+properties
+kerryproperties
+viajes
+ladbrokes
+wales
+motorcycles
+hoteles
+singles
+staples
+games
+homes
+hermes
+tunes
+shoes
+recipes
+tires
+pictures
+ventures
+ses
+enterprises
+cruises
+courses
+associates
+gives
+gs
+holdings
+xn--fiq228c5hs
+is
+cupcake.is
+org.is
+com.is
+net.is
+int.is
+blogspot.is
+edu.is
+gov.is
+xn--90ais
+tennis
+paris
+gratis
+rocks
+sucks
+xn--80adxhks
+works
+ls
+org.ls
+co.ls
+deals
+rentals
+brussels
+hotels
+kerryhotels
+marshalls
+mls
+tools
+ms
+org.ms
+com.ms
+net.ms
+edu.ms
+gov.ms
+bms
+systems
+claims
+fans
+frogans
+loans
+passagens
+bargains
+domains
+origins
+vacations
+productions
+solutions
+coupons
+duns
+condos
+juegos
+vuelos
+zappos
+photos
+autos
+ps
+sec.ps
+org.ps
+com.ps
+plo.ps
+net.ps
+edu.ps
+gov.ps
+scholarships
+philips
+tips
+ups
+rs
+ac.rs
+org.rs
+in.rs
+co.rs
+blogspot.rs
+edu.rs
+gov.rs
+cars
+pars
+guitars
+crs
+builders
+careers
+rogers
+travelers
+farmers
+winners
+partners
+flowers
+contractors
+tatamotors
+jprs
+tours
+glass
+business
+fitness
+press
+express
+americanexpress
+orientexpress
+lanxess
+swiss
+beats
+cityeats
+boats
+tickets
+markets
+gifts
+yachts
+flights
+accountants
+apartments
+investments
+events
+boots
+parts
+us
+ca.us
+k12.ca.us
+lib.ca.us
+cc.ca.us
+ga.us
+k12.ga.us
+lib.ga.us
+cc.ga.us
+ia.us
+k12.ia.us
+lib.ia.us
+cc.ia.us
+la.us
+k12.la.us
+lib.la.us
+cc.la.us
+ma.us
+k12.ma.us
+paroch.k12.ma.us
+chtr.k12.ma.us
+pvt.k12.ma.us
+lib.ma.us
+cc.ma.us
+pa.us
+k12.pa.us
+lib.pa.us
+cc.pa.us
+isa.us
+va.us
+k12.va.us
+lib.va.us
+cc.va.us
+wa.us
+k12.wa.us
+lib.wa.us
+cc.wa.us
+dc.us
+k12.dc.us
+lib.dc.us
+cc.dc.us
+nc.us
+k12.nc.us
+lib.nc.us
+cc.nc.us
+sc.us
+k12.sc.us
+lib.sc.us
+cc.sc.us
+fed.us
+id.us
+k12.id.us
+lib.id.us
+cc.id.us
+md.us
+k12.md.us
+lib.md.us
+cc.md.us
+nd.us
+lib.nd.us
+cc.nd.us
+sd.us
+lib.sd.us
+cc.sd.us
+drud.us
+de.us
+k12.de.us
+lib.de.us
+cc.de.us
+land-4-sale.us
+stuff-4-sale.us
+me.us
+k12.me.us
+lib.me.us
+cc.me.us
+ne.us
+k12.ne.us
+lib.ne.us
+cc.ne.us
+nh.us
+k12.nh.us
+lib.nh.us
+cc.nh.us
+oh.us
+k12.oh.us
+lib.oh.us
+cc.oh.us
+hi.us
+lib.hi.us
+cc.hi.us
+mi.us
+k12.mi.us
+lib.mi.us
+cc.mi.us
+dni.us
+ri.us
+k12.ri.us
+lib.ri.us
+cc.ri.us
+vi.us
+k12.vi.us
+lib.vi.us
+cc.vi.us
+wi.us
+k12.wi.us
+lib.wi.us
+cc.wi.us
+nj.us
+k12.nj.us
+lib.nj.us
+cc.nj.us
+ak.us
+k12.ak.us
+lib.ak.us
+cc.ak.us
+ok.us
+k12.ok.us
+lib.ok.us
+cc.ok.us
+al.us
+k12.al.us
+lib.al.us
+cc.al.us
+fl.us
+k12.fl.us
+lib.fl.us
+cc.fl.us
+il.us
+k12.il.us
+lib.il.us
+cc.il.us
+nm.us
+k12.nm.us
+lib.nm.us
+cc.nm.us
+golffan.us
+in.us
+k12.in.us
+lib.in.us
+cc.in.us
+mn.us
+k12.mn.us
+lib.mn.us
+cc.mn.us
+nsn.us
+tn.us
+k12.tn.us
+lib.tn.us
+cc.tn.us
+co.us
+k12.co.us
+lib.co.us
+cc.co.us
+mo.us
+k12.mo.us
+lib.mo.us
+cc.mo.us
+pointto.us
+noip.us
+ar.us
+k12.ar.us
+lib.ar.us
+cc.ar.us
+or.us
+k12.or.us
+lib.or.us
+cc.or.us
+pr.us
+k12.pr.us
+lib.pr.us
+cc.pr.us
+as.us
+k12.as.us
+lib.as.us
+cc.as.us
+kids.us
+ks.us
+k12.ks.us
+lib.ks.us
+cc.ks.us
+ms.us
+k12.ms.us
+lib.ms.us
+cc.ms.us
+ct.us
+k12.ct.us
+lib.ct.us
+cc.ct.us
+mt.us
+k12.mt.us
+lib.mt.us
+cc.mt.us
+ut.us
+k12.ut.us
+lib.ut.us
+cc.ut.us
+vt.us
+k12.vt.us
+lib.vt.us
+cc.vt.us
+gu.us
+k12.gu.us
+lib.gu.us
+cc.gu.us
+nv.us
+k12.nv.us
+lib.nv.us
+cc.nv.us
+wv.us
+cc.wv.us
+tx.us
+k12.tx.us
+lib.tx.us
+cc.tx.us
+is-by.us
+ky.us
+k12.ky.us
+lib.ky.us
+cc.ky.us
+ny.us
+k12.ny.us
+lib.ny.us
+cc.ny.us
+wy.us
+k12.wy.us
+lib.wy.us
+cc.wy.us
+az.us
+k12.az.us
+lib.az.us
+cc.az.us
+haus
+bauhaus
+airbus
+locus
+eus
+fresenius
+plus
+lexus
+nexus
+tvs
+ws
+org.ws
+com.ws
+dyndns.ws
+mypets.ws
+net.ws
+edu.ws
+gov.ws
+aws
+reviews
+news
+windows
+barclays
+macys
+toys
+xn--czrs0t
+at
+ac.at
+co.at
+blogspot.co.at
+info.at
+or.at
+gv.at
+priv.at
+biz.at
+cat
+eat
+seat
+chat
+fiat
+lat
+etisalat
+imamat
+democrat
+bt
+org.bt
+com.bt
+net.bt
+edu.bt
+gov.bt
+bbt
+lgbt
+contact
+select
+iselect
+uconnect
+direct
+nextdirect
+schmidt
+et
+name.et
+org.et
+com.et
+info.et
+net.et
+edu.et
+gov.et
+biz.et
+bet
+meet
+piaget
+target
+diet
+cricket
+market
+net
+dynv6.net
+r.cdn77.net
+from-la.net
+mysecuritycamera.net
+za.net
+gb.net
+eating-organic.net
+mymediapc.net
+in-the-band.net
+privatizehealthinsurance.net
+office-on-the.net
+azure-mobile.net
+bounceme.net
+dynathome.net
+redirectme.net
+podzone.net
+thruhere.net
+se.net
+scrapper-site.net
+rackmaze.net
+is-a-chef.net
+serveblog.net
+is-a-geek.net
+isa-geek.net
+uk.net
+cdn77-ssl.net
+pgafan.net
+nhlfan.net
+in.net
+from-co.net
+dnsdojo.net
+no-ip.net
+homeip.net
+selfip.net
+jp.net
+at-band-camp.net
+ham-radio-op.net
+webhop.net
+cloudapp.net
+homeftp.net
+serveftp.net
+dynalias.net
+dnsalias.net
+dsmynas.net
+servebbs.net
+familyds.net
+buyshouses.net
+azurewebsites.net
+sytes.net
+ddns.net
+blogdns.net
+cloudfunctions.net
+kicks-ass.net
+myeffect.net
+endofinternet.net
+serveminecraft.net
+broke-it.net
+does-it.net
+sells-it.net
+gets-it.net
+mydissent.net
+cloudfront.net
+dontexist.net
+hu.net
+homeunix.net
+mypsx.net
+homelinux.net
+a.prod.fastly.net
+global.prod.fastly.net
+a.ssl.fastly.net
+b.ssl.fastly.net
+global.ssl.fastly.net
+from-ny.net
+from-az.net
+myfritz.net
+iinet
+pet
+pictet
+vet
+gift
+loft
+microsoft
+gt
+gob.gt
+ind.gt
+org.gt
+mil.gt
+com.gt
+net.gt
+edu.gt
+ht
+med.ht
+org.ht
+rel.ht
+pol.ht
+com.ht
+firm.ht
+info.ht
+pro.ht
+perso.ht
+asso.ht
+shop.ht
+coop.ht
+net.ht
+adult.ht
+art.ht
+edu.ht
+gouv.ht
+it
+taa.it
+ba.it
+ca.it
+lucca.it
+vda.it
+tempio-olbia.it
+tempioolbia.it
+brescia.it
+lombardia.it
+foggia.it
+perugia.it
+puglia.it
+sicilia.it
+reggio-emilia.it
+reggioemilia.it
+friuli-venezia-giulia.it
+friulivenezia-giulia.it
+friuli-ve-giulia.it
+friulive-giulia.it
+friuli-v-giulia.it
+friuliv-giulia.it
+friuli-veneziagiulia.it
+friuliveneziagiulia.it
+friuli-vegiulia.it
+friulivegiulia.it
+friuli-vgiulia.it
+friulivgiulia.it
+verbania.it
+lucania.it
+campania.it
+catania.it
+sardinia.it
+iglesias-carbonia.it
+iglesiascarbonia.it
+isernia.it
+pistoia.it
+calabria.it
+reggio-calabria.it
+reggiocalabria.it
+umbria.it
+trani-barletta-andria.it
+barletta-trani-andria.it
+tranibarlettaandria.it
+barlettatraniandria.it
+alessandria.it
+imperia.it
+liguria.it
+vibo-valentia.it
+vibovalentia.it
+pavia.it
+venezia.it
+la-spezia.it
+laspezia.it
+gorizia.it
+aquila.it
+laquila.it
+biella.it
+roma.it
+parma.it
+na.it
+toscana.it
+modena.it
+siena.it
+forli-cesena.it
+forlicesena.it
+emilia-romagna.it
+emiliaromagna.it
+sardegna.it
+bologna.it
+messina.it
+latina.it
+enna.it
+ravenna.it
+ancona.it
+cremona.it
+verona.it
+savona.it
+genoa.it
+pa.it
+ra.it
+pescara.it
+massa-carrara.it
+massacarrara.it
+ferrara.it
+novara.it
+matera.it
+ogliastra.it
+dell-ogliastra.it
+dellogliastra.it
+sa.it
+pisa.it
+carrara-massa.it
+carraramassa.it
+siracusa.it
+ragusa.it
+ta.it
+basilicata.it
+macerata.it
+caserta.it
+aosta.it
+valle-d-aosta.it
+val-d-aosta.it
+valled-aosta.it
+vald-aosta.it
+valle-aosta.it
+valle-daosta.it
+val-daosta.it
+valledaosta.it
+valdaosta.it
+valleaosta.it
+trani-andria-barletta.it
+andria-trani-barletta.it
+traniandriabarletta.it
+andriatranibarletta.it
+caltanissetta.it
+padua.it
+va.it
+padova.it
+genova.it
+mantova.it
+monza-e-della-brianza.it
+monza-brianza.it
+monzaedellabrianza.it
+monzabrianza.it
+monzaebrianza.it
+piacenza.it
+vicenza.it
+cosenza.it
+potenza.it
+monza.it
+cb.it
+mb.it
+umb.it
+vb.it
+fc.it
+sic.it
+lc.it
+mc.it
+pc.it
+rc.it
+vc.it
+pd.it
+ud.it
+ce.it
+lecce.it
+venice.it
+florence.it
+fe.it
+ge.it
+trentino-a-adige.it
+trentinoa-adige.it
+alto-adige.it
+trentino-alto-adige.it
+trentinoalto-adige.it
+trentino-aadige.it
+trentinoaadige.it
+altoadige.it
+trentino-altoadige.it
+trentinoaltoadige.it
+marche.it
+le.it
+me.it
+rome.it
+udine.it
+pordenone.it
+frosinone.it
+crotone.it
+pe.it
+re.it
+varese.it
+molise.it
+te.it
+piemonte.it
+trieste.it
+aoste.it
+vallee-aoste.it
+valleeaoste.it
+ve.it
+firenze.it
+ag.it
+bg.it
+fg.it
+lig.it
+og.it
+pg.it
+rg.it
+pug.it
+fvg.it
+ch.it
+bi.it
+ci.it
+lodi.it
+fi.it
+li.it
+vercelli.it
+napoli.it
+cesena-forli.it
+cesenaforli.it
+mi.it
+trapani.it
+andria-barletta-trani.it
+andriabarlettatrani.it
+rimini.it
+terni.it
+pi.it
+ri.it
+bari.it
+cagliari.it
+sassari.it
+si.it
+brindisi.it
+chieti.it
+rieti.it
+asti.it
+vi.it
+al.it
+cal.it
+bl.it
+cl.it
+mol.it
+trentino-sued-tirol.it
+trentinosued-tirol.it
+trentino-sud-tirol.it
+trentinosud-tirol.it
+trentino-s-tirol.it
+trentinos-tirol.it
+suedtirol.it
+trentino-suedtirol.it
+trentinosuedtirol.it
+trentino-sudtirol.it
+trentinosudtirol.it
+trentino-stirol.it
+trentinostirol.it
+cam.it
+fm.it
+im.it
+lom.it
+rm.it
+an.it
+milan.it
+balsan.it
+bn.it
+cn.it
+en.it
+ven.it
+bozen.it
+turin.it
+mn.it
+pmn.it
+pn.it
+rn.it
+tn.it
+ao.it
+vao.it
+bo.it
+viterbo.it
+co.it
+lecco.it
+cuneo.it
+go.it
+rovigo.it
+campidano-medio.it
+campidanomedio.it
+olbia-tempio.it
+olbiatempio.it
+sondrio.it
+lazio.it
+lo.it
+mo.it
+bergamo.it
+teramo.it
+como.it
+fermo.it
+palermo.it
+no.it
+medio-campidano.it
+mediocampidano.it
+milano.it
+oristano.it
+bolzano.it
+ascoli-piceno.it
+ascolipiceno.it
+pesaro-urbino.it
+pesarourbino.it
+avellino.it
+torino.it
+trentino.it
+salerno.it
+livorno.it
+belluno.it
+po.it
+ro.it
+urbino-pesaro.it
+urbinopesaro.it
+catanzaro.it
+nuoro.it
+so.it
+treviso.it
+campobasso.it
+to.it
+prato.it
+veneto.it
+grosseto.it
+taranto.it
+agrigento.it
+trento.it
+benevento.it
+arezzo.it
+abruzzo.it
+ap.it
+sp.it
+tp.it
+aq.it
+ar.it
+mar.it
+sar.it
+br.it
+abr.it
+cr.it
+fr.it
+gr.it
+kr.it
+emr.it
+or.it
+pr.it
+sr.it
+tr.it
+vr.it
+bas.it
+carbonia-iglesias.it
+carboniaiglesias.it
+bs.it
+cs.it
+naples.it
+is.it
+ms.it
+tos.it
+ss.it
+ts.it
+vs.it
+at.it
+bt.it
+ct.it
+lt.it
+mt.it
+piedmont.it
+ot.it
+blogspot.it
+pt.it
+vt.it
+edu.it
+lu.it
+nu.it
+pu.it
+av.it
+gov.it
+pv.it
+sv.it
+tv.it
+vv.it
+lombardy.it
+aosta-valley.it
+aostavalley.it
+sicily.it
+tuscany.it
+laz.it
+bz.it
+cz.it
+pz.it
+credit
+reit
+fit
+mit
+rmit
+intuit
+xn--mgba3a3ejt
+hkt
+lt
+blogspot.lt
+gov.lt
+adult
+mt
+org.mt
+com.mt
+blogspot.com.mt
+net.mt
+edu.mt
+sandvikcoromant
+restaurant
+accountant
+gent
+management
+router.management
+equipment
+rent
+int
+eu.int
+mint
+goldpoint
+vistaprint
+dupont
+discount
+bot
+scot
+dot
+got
+hot
+jot
+barefoot
+homedepot
+spot
+pt
+nome.pt
+org.pt
+publ.pt
+com.pt
+net.pt
+int.pt
+blogspot.pt
+edu.pt
+gov.pt
+art
+walmart
+smart
+expert
+report
+support
+srt
+st
+embaixada.st
+saotome.st
+principe.st
+store.st
+org.st
+mil.st
+com.st
+co.st
+consulado.st
+net.st
+edu.st
+gov.st
+comcast
+fast
+best
+budapest
+rest
+quest
+ist
+florist
+dentist
+host
+post
+epost
+auspost
+trust
+tt
+name.tt
+org.tt
+mobi.tt
+travel.tt
+com.tt
+museum.tt
+co.tt
+info.tt
+aero.tt
+pro.tt
+coop.tt
+jobs.tt
+net.tt
+int.tt
+edu.tt
+gov.tt
+biz.tt
+hyatt
+ntt
+ott
+abbott
+marriott
+hangout
+next
+yt
+jetzt
+xn--9et52u
+xn--pssy2u
+au
+sa.au
+wa.au
+vic.au
+id.au
+qld.au
+conf.au
+org.au
+com.au
+blogspot.com.au
+asn.au
+info.au
+tas.au
+act.au
+net.au
+nt.au
+edu.au
+sa.edu.au
+wa.edu.au
+vic.edu.au
+qld.edu.au
+tas.edu.au
+act.edu.au
+nt.edu.au
+nsw.edu.au
+gov.au
+sa.gov.au
+wa.gov.au
+vic.gov.au
+qld.gov.au
+tas.gov.au
+nsw.au
+oz.au
+itau
+cu
+inf.cu
+org.cu
+com.cu
+net.cu
+edu.cu
+gov.cu
+icu
+edu
+baidu
+eu
+mycd.eu
+hu
+2000.hu
+erotica.hu
+media.hu
+erotika.hu
+tozsde.hu
+org.hu
+suli.hu
+hotel.hu
+reklam.hu
+film.hu
+tm.hu
+forum.hu
+ingatlan.hu
+co.hu
+video.hu
+info.hu
+konyvelo.hu
+casino.hu
+shop.hu
+agrar.hu
+lakas.hu
+utazas.hu
+games.hu
+news.hu
+bolt.hu
+blogspot.hu
+sport.hu
+priv.hu
+sex.hu
+szex.hu
+city.hu
+jogasz.hu
+sohu
+tushu
+lu
+blogspot.lu
+mu
+ac.mu
+org.mu
+com.mu
+co.mu
+or.mu
+net.mu
+gov.mu
+nu
+merseine.nu
+mine.nu
+shacknet.nu
+menu
+wanggou
+you
+cyou
+ru
+vologda.ru
+kaluga.ru
+chuvashia.ru
+kalmykia.ru
+karelia.ru
+bashkiria.ru
+khakassia.ru
+buryatia.ru
+udmurtia.ru
+yakutia.ru
+mordovia.ru
+nakhodka.ru
+dudinka.ru
+kamchatka.ru
+vyatka.ru
+chukotka.ru
+joshkar-ola.ru
+tula.ru
+kostroma.ru
+palana.ru
+samara.ru
+chita.ru
+tuva.ru
+adygeya.ru
+penza.ru
+spb.ru
+ac.ru
+volgograd.ru
+zgrad.ru
+rnd.ru
+belgorod.ru
+ulan-ude.ru
+marine.ru
+cbg.ru
+koenig.ru
+org.ru
+e-burg.ru
+orenburg.ru
+yekaterinburg.ru
+voronezh.ru
+kustanai.ru
+altai.ru
+komi.ru
+mari.ru
+nalchik.ru
+vladivostok.ru
+k-uralsk.ru
+arkhangelsk.ru
+norilsk.ru
+msk.ru
+omsk.ru
+tomsk.ru
+nsk.ru
+murmansk.ru
+bryansk.ru
+smolensk.ru
+chelyabinsk.ru
+yuzhno-sakhalinsk.ru
+vdonsk.ru
+krasnoyarsk.ru
+novosibirsk.ru
+simbirsk.ru
+pyatigorsk.ru
+kursk.ru
+amursk.ru
+tsk.ru
+lipetsk.ru
+irkutsk.ru
+izhevsk.ru
+khabarovsk.ru
+rubtsovsk.ru
+baikal.ru
+jamal.ru
+yamal.ru
+mari-el.ru
+chel.ru
+mil.ru
+oskol.ru
+stavropol.ru
+oryol.ru
+yaroslavl.ru
+udm.ru
+com.ru
+tom.ru
+perm.ru
+kuban.ru
+magadan.ru
+kurgan.ru
+astrakhan.ru
+syzran.ru
+dagestan.ru
+tatarstan.ru
+kazan.ru
+ryazan.ru
+tyumen.ru
+sakhalin.ru
+vrn.ru
+tsaritsyn.ru
+ivanovo.ru
+kemerovo.ru
+pp.ru
+jar.ru
+tver.ru
+kchr.ru
+bir.ru
+vladimir.ru
+amur.ru
+mytis.ru
+kms.ru
+kuzbass.ru
+net.ru
+int.ru
+blogspot.ru
+fareast.ru
+test.ru
+surgut.ru
+edu.ru
+khv.ru
+tambov.ru
+gov.ru
+nov.ru
+nnov.ru
+kirov.ru
+saratov.ru
+stv.ru
+cmw.ru
+grozny.ru
+vladikavkaz.ru
+nkz.ru
+snz.ru
+ptz.ru
+cymru
+pru
+guru
+su
+vologda.su
+kaluga.su
+kalmykia.su
+karelia.su
+bashkiria.su
+khakassia.su
+mordovia.su
+tula.su
+tuva.su
+adygeya.su
+penza.su
+spb.su
+lenug.su
+sochi.su
+togliatti.su
+nalchik.su
+arkhangelsk.su
+msk.su
+murmansk.su
+bryansk.su
+obninsk.su
+troitsk.su
+pokrovsk.su
+kurgan.su
+dagestan.su
+ivanovo.su
+krasnodar.su
+vladimir.su
+balashov.su
+nov.su
+grozny.su
+vladikavkaz.su
+komatsu
+fujitsu
+hisamitsu
+vu
+org.vu
+com.vu
+net.vu
+edu.vu
+ryukyu
+bv
+cv
+blogspot.cv
+dev
+hiv
+lv
+id.lv
+conf.lv
+org.lv
+mil.lv
+com.lv
+asn.lv
+net.lv
+edu.lv
+gov.lv
+mv
+name.mv
+org.mv
+mil.mv
+com.mv
+museum.mv
+info.mv
+aero.mv
+pro.mv
+coop.mv
+net.mv
+int.mv
+edu.mv
+gov.mv
+biz.mv
+gov
+mov
+trv
+sv
+gob.sv
+red.sv
+org.sv
+com.sv
+edu.sv
+tv
+on-the-web.tv
+worse-than.tv
+better-than.tv
+dyndns.tv
+dtv
+hgtv
+itv
+nowtv
+xn--vhquv
+aw
+com.aw
+shaw
+law
+bw
+org.bw
+co.bw
+cw
+org.cw
+com.cw
+net.cw
+edu.cw
+pccw
+review
+new
+sew
+gw
+mw
+ac.mw
+org.mw
+com.mw
+museum.mw
+co.mw
+coop.mw
+net.mw
+int.mw
+edu.mw
+gov.mw
+biz.mw
+bmw
+moscow
+how
+show
+now
+wow
+pw
+ed.pw
+ne.pw
+co.pw
+go.pw
+or.pw
+belau.pw
+rw
+ac.rw
+mil.rw
+com.rw
+co.rw
+net.rw
+int.rw
+edu.rw
+gov.rw
+gouv.rw
+nrw
+tw
+xn--zf0ao64a.tw
+xn--czrw28b.tw
+club.tw
+game.tw
+org.tw
+mil.tw
+com.tw
+net.tw
+blogspot.tw
+edu.tw
+idv.tw
+gov.tw
+xn--uc0atv.tw
+ebiz.tw
+商業.tw
+組織.tw
+網路.tw
+ax
+tax
+cx
+ath.cx
+gov.cx
+nadex
+fedex
+yandex
+amex
+banamex
+forex
+sex
+netflix
+tjx
+mx
+gob.mx
+org.mx
+com.mx
+net.mx
+blogspot.mx
+edu.mx
+gmx
+xn--ygbi2ammx
+box
+xbox
+fox
+xerox
+fujixerox
+xn--ngbrx
+sx
+gov.sx
+tjmaxx
+tkmaxx
+xxx
+xn--unup4y
+xn--30rr7y
+day
+holiday
+blackfriday
+today
+play
+pay
+alipay
+toray
+nissay
+broadway
+by
+of.by
+mil.by
+com.by
+blogspot.com.by
+gov.by
+baby
+ac.cy
+ltd.cy
+name.cy
+org.cy
+com.cy
+blogspot.com.cy
+tm.cy
+pro.cy
+ekloges.cy
+press.cy
+net.cy
+parliament.cy
+gov.cy
+biz.cy
+pharmacy
+agency
+godaddy
+study
+hockey
+bentley
+sydney
+money
+attorney
+mckinsey
+gy
+org.gy
+com.gy
+co.gy
+net.gy
+edu.gy
+gov.gy
+technology
+energy
+photography
+diy
+ky
+org.ky
+com.ky
+net.ky
+edu.ky
+gov.ky
+sky
+ly
+plc.ly
+med.ly
+id.ly
+org.ly
+sch.ly
+com.ly
+net.ly
+edu.ly
+gov.ly
+fly
+mobily
+family
+americanfamily
+ally
+lilly
+supply
+my
+name.my
+org.my
+mil.my
+com.my
+net.my
+blogspot.my
+edu.my
+gov.my
+academy
+army
+tiffany
+company
+afamilycompany
+sony
+joy
+soy
+py
+org.py
+mil.py
+com.py
+coop.py
+net.py
+edu.py
+gov.py
+grocery
+surgery
+gallery
+delivery
+jewelry
+directory
+country
+luxury
+sy
+org.sy
+mil.sy
+com.sy
+net.sy
+edu.sy
+gov.sy
+lipsy
+safety
+city
+telecity
+fidelity
+xfinity
+community
+security
+university
+realty
+party
+property
+beauty
+uy
+gub.uy
+org.uy
+mil.uy
+com.uy
+blogspot.com.uy
+net.uy
+edu.uy
+buy
+bestbuy
+navy
+oldnavy
+sexy
+az
+name.az
+org.az
+mil.az
+com.az
+info.az
+pro.az
+pp.az
+net.az
+int.az
+edu.az
+gov.az
+biz.az
+bz
+za.bz
+org.bz
+com.bz
+net.bz
+edu.bz
+gov.bz
+cz
+e4.cz
+realm.cz
+co.cz
+blogspot.cz
+dz
+org.dz
+pol.dz
+com.dz
+asso.dz
+net.dz
+art.dz
+edu.dz
+gov.dz
+biz
+dscloud.biz
+for-the.biz
+for-some.biz
+for-more.biz
+mmafan.biz
+no-ip.biz
+selfip.biz
+webhop.biz
+myftp.biz
+for-better.biz
+dyndns.biz
+gbiz
+kz
+org.kz
+mil.kz
+com.kz
+net.kz
+edu.kz
+gov.kz
+nz
+xn--mori-qsa.nz
+ac.nz
+org.nz
+health.nz
+cri.nz
+maori.nz
+mÄori.nz
+iwi.nz
+kiwi.nz
+geek.nz
+mil.nz
+school.nz
+gen.nz
+co.nz
+blogspot.co.nz
+net.nz
+parliament.nz
+govt.nz
+anz
+allfinanz
+schwarz
+sz
+ac.sz
+org.sz
+co.sz
+tz
+ac.tz
+sc.tz
+me.tz
+ne.tz
+mobi.tz
+hotel.tz
+mil.tz
+co.tz
+info.tz
+go.tz
+or.tz
+tv.tz
+uz
+org.uz
+com.uz
+co.uz
+net.uz
+nowruz
+xyz
+fhapp.xyz
+buzz
+ελ
+моÑква
+Ñрб
+орг.Ñрб
+од.Ñрб
+ак.Ñрб
+обр.Ñрб
+пр.Ñрб
+упр.Ñрб
+бг
+орг
+мкд
+қаз
+дети
+католик
+бел
+ком
+онлайн
+мон
+укр
+руÑ
+Ñайт
+рф
+ею
+Õ°Õ¡Õµ
+קו×
+سوريا
+مليسيا
+عرب
+المغرب
+شبكة
+السعودية
+سورية
+السعودیة
+امارات
+اتصالات
+بھارت
+الجزائر
+بازار
+مصر
+قطر
+تونس
+موقع
+عراق
+بيتك
+كاثوليك
+كوم
+پاكستان
+پاکستان
+سودان
+ايران
+ایران
+عمان
+العليان
+الاردن
+اليمن
+Ùلسطين
+همراه
+السعوديه
+ارامكو
+ابوظبي
+موبايلي
+السعودیۃ
+नेट
+भारत
+संगठन
+कॉम
+ভারত
+বাংলা
+ਭਾਰਤ
+ભારત
+இநà¯à®¤à®¿à®¯à®¾
+இலஙà¯à®•à¯ˆ
+சிஙà¯à®•à®ªà¯à®ªà¯‚à®°à¯
+భారతà±
+ලංකà·
+คอม
+ไทย
+გე
+ã¿ã‚“ãª
+ストア
+ãƒã‚¤ãƒ³ãƒˆ
+クラウド
+コム
+グーグル
+セール
+ファッション
+ä¼ä¸š
+广东
+娱ä¹
+诺基亚
+我爱你
+中信
+政务
+移动
+å¾®åš
+å…«å¦
+é¤åŽ…
+å…¬å¸
+食å“
+慈善
+集团
+中国
+中國
+网å€
+新加å¡
+商城
+ç å®
+时尚
+佛山
+一å·åº—
+商店
+网店
+嘉里大酒店
+政府
+å¥åº·
+ä¿¡æ¯
+游æˆ
+香格里拉
+大拿
+天主教
+手机
+机构
+组织机构
+商标
+谷歌
+飞利浦
+香港
+å°æ¹¾
+å°ç£
+臺ç£
+购物
+世界
+公益
+点看
+電訊盈科
+网站
+書ç±
+在线
+网络
+中文网
+工行
+手表
+通販
+大众汽车
+è”通
+嘉里
+淡马锡
+澳門
+澳门
+æ–°é—»
+家電
+한국
+ë‹·ë„·
+삼성
+ë‹·ì»´ \ No newline at end of file
diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle
new file mode 100644
index 0000000000..18586cadb5
--- /dev/null
+++ b/mobile/android/app/build.gradle
@@ -0,0 +1,406 @@
+buildDir "${topobjdir}/gradle/build/mobile/android/app"
+
+apply plugin: 'android-sdk-manager' // Must come before 'com.android.*'.
+apply plugin: 'com.android.application'
+apply plugin: 'checkstyle'
+
+android {
+ compileSdkVersion 23
+ buildToolsVersion mozconfig.substs.ANDROID_BUILD_TOOLS_VERSION
+
+ defaultConfig {
+ targetSdkVersion 23
+ minSdkVersion 15
+ applicationId mozconfig.substs.ANDROID_PACKAGE_NAME
+ testApplicationId 'org.mozilla.roboexample.test'
+ testInstrumentationRunner 'org.mozilla.gecko.FennecInstrumentationTestRunner'
+ manifestPlaceholders = [
+ ANDROID_PACKAGE_NAME: mozconfig.substs.ANDROID_PACKAGE_NAME,
+ MOZ_ANDROID_MIN_SDK_VERSION: mozconfig.substs.MOZ_ANDROID_MIN_SDK_VERSION,
+ MOZ_ANDROID_SHARED_ID: "${mozconfig.substs.ANDROID_PACKAGE_NAME}.sharedID",
+ ]
+ // Used by Robolectric based tests; see TestRunner.
+ buildConfigField 'String', 'BUILD_DIR', "\"${project.buildDir}\""
+
+ vectorDrawables.useSupportLibrary = true
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_7
+ targetCompatibility JavaVersion.VERSION_1_7
+ }
+
+ dexOptions {
+ javaMaxHeapSize "2g"
+ }
+
+ lintOptions {
+ abortOnError true
+ }
+
+ buildTypes {
+ release {
+ shrinkResources true
+ minifyEnabled true
+ proguardFile "${topsrcdir}/mobile/android/config/proguard/proguard.cfg"
+ }
+ }
+
+ productFlavors {
+ // For API 21+ - with multi dex, this will be faster for local development.
+ local {
+ // For multi dex, setting `minSdkVersion 21` allows the Android gradle plugin to
+ // pre-DEX each module and produce an APK that can be tested on
+ // Android Lollipop without time consuming DEX merging processes.
+ minSdkVersion 21
+ dexOptions {
+ preDexLibraries true
+ // We only call `MultiDex.install()` for the automation build flavor
+ // so this may not work. However, I don't think the multidex support
+ // library is necessary for 21+, so I expect that it will work.
+ multiDexEnabled true
+ }
+ }
+ // For API < 21 - does not support multi dex because local development
+ // is slow in that case. Most builds will not require multi dex so this
+ // should not be an issue.
+ localOld {
+ }
+ // Automation builds.
+ automation {
+ dexOptions {
+ // As of FF48 on beta, the "test", "lint", etc. treeherder jobs fail because they
+ // exceed the method limit. Beta includes Adjust and its GPS dependencies, which
+ // increase the method count & explain the failures. Furthermore, this error only
+ // occurs on debug builds because we don't proguard.
+ //
+ // We enable multidex as an easy, quick-fix with minimal side effects but before we
+ // move to gradle for our production builds, we should re-evaluate this decision
+ // (bug 1286677).
+ multiDexEnabled true
+ }
+ }
+ }
+
+ sourceSets {
+ main {
+ manifest.srcFile "${project.buildDir}/generated/source/preprocessed_manifest/AndroidManifest.xml"
+
+ aidl {
+ srcDir "${topsrcdir}/mobile/android/base/aidl"
+ }
+
+ java {
+ srcDir "${topsrcdir}/mobile/android/base/java"
+ srcDir "${topsrcdir}/mobile/android/search/java"
+ srcDir "${topsrcdir}/mobile/android/javaaddons/java"
+ srcDir "${topsrcdir}/mobile/android/services/src/main/java"
+
+ if (mozconfig.substs.MOZ_ANDROID_MLS_STUMBLER) {
+ srcDir "${topsrcdir}/mobile/android/stumbler/java"
+ }
+
+ if (!mozconfig.substs.MOZ_CRASHREPORTER) {
+ exclude 'org/mozilla/gecko/CrashReporter.java'
+ }
+
+ if (!mozconfig.substs.MOZ_NATIVE_DEVICES) {
+ exclude 'org/mozilla/gecko/ChromeCastDisplay.java'
+ exclude 'org/mozilla/gecko/ChromeCastPlayer.java'
+ exclude 'org/mozilla/gecko/GeckoMediaPlayer.java'
+ exclude 'org/mozilla/gecko/GeckoPresentationDisplay.java'
+ exclude 'org/mozilla/gecko/MediaPlayerManager.java'
+ }
+
+ if (mozconfig.substs.MOZ_WEBRTC) {
+ srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/audio_device/android/java/src"
+ srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/video_capture/android/java/src"
+ srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/video_render/android/java/src"
+ }
+
+ if (mozconfig.substs.MOZ_INSTALL_TRACKING) {
+ exclude 'org/mozilla/gecko/adjust/StubAdjustHelper.java'
+ } else {
+ exclude 'org/mozilla/gecko/adjust/AdjustHelper.java'
+ }
+
+ if (!mozconfig.substs.MOZ_ANDROID_GCM) {
+ exclude 'org/mozilla/gecko/gcm/**/*.java'
+ exclude 'org/mozilla/gecko/push/**/*.java'
+ }
+
+ srcDir "${project.buildDir}/generated/source/preprocessed_code" // See syncPreprocessedCode.
+ }
+
+ res {
+ srcDir "${topsrcdir}/${mozconfig.substs.MOZ_BRANDING_DIRECTORY}/res"
+ srcDir "${project.buildDir}/generated/source/preprocessed_resources" // See syncPreprocessedResources.
+ srcDir "${topsrcdir}/mobile/android/base/resources"
+ srcDir "${topsrcdir}/mobile/android/services/src/main/res"
+ if (mozconfig.substs.MOZ_CRASHREPORTER) {
+ srcDir "${topsrcdir}/mobile/android/base/crashreporter/res"
+ }
+ }
+
+ assets {
+ if (mozconfig.substs.MOZ_ANDROID_DISTRIBUTION_DIRECTORY && !mozconfig.substs.MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER) {
+ // If we are packaging the bouncer, it will have the distribution, so don't put
+ // it in the main APK as well.
+ srcDir "${mozconfig.substs.MOZ_ANDROID_DISTRIBUTION_DIRECTORY}/assets"
+ }
+ srcDir "${topsrcdir}/mobile/android/app/assets"
+ }
+ }
+
+ test {
+ java {
+ srcDir "${topsrcdir}/mobile/android/tests/background/junit4/src"
+
+ if (!mozconfig.substs.MOZ_ANDROID_GCM) {
+ exclude 'org/mozilla/gecko/gcm/**/*.java'
+ exclude 'org/mozilla/gecko/push/**/*.java'
+ }
+ }
+ resources {
+ srcDir "${topsrcdir}/mobile/android/tests/background/junit4/resources"
+ }
+ }
+
+ androidTest {
+ java {
+ srcDir "${topsrcdir}/mobile/android/tests/browser/robocop/src"
+ srcDir "${topsrcdir}/mobile/android/tests/background/junit3/src"
+ srcDir "${topsrcdir}/mobile/android/tests/browser/junit3/src"
+ srcDir "${topsrcdir}/mobile/android/tests/javaddons/src"
+ }
+ res {
+ srcDir "${topsrcdir}/mobile/android/tests/browser/robocop/res"
+ }
+ assets {
+ srcDir "${topsrcdir}/mobile/android/tests/browser/robocop/assets"
+ }
+ }
+ }
+
+ testOptions {
+ unitTests.all {
+ // We'd like to use (Runtime.runtime.availableProcessors()/2), but
+ // we have tests that start test servers and the bound ports
+ // collide. We'll fix this soon to have much faster test cycles.
+ maxParallelForks 1
+ }
+ }
+}
+
+dependencies {
+ compile 'com.android.support:multidex:1.0.0'
+
+ compile "com.android.support:support-v4:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+ compile "com.android.support:appcompat-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+ compile "com.android.support:cardview-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+ compile "com.android.support:recyclerview-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+ compile "com.android.support:design:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+ compile "com.android.support:customtabs:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+ compile "com.android.support:palette-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+
+ if (mozconfig.substs.MOZ_NATIVE_DEVICES) {
+ compile "com.android.support:mediarouter-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+ compile "com.google.android.gms:play-services-basement:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
+ compile "com.google.android.gms:play-services-base:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
+ compile "com.google.android.gms:play-services-cast:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
+ }
+
+ if (mozconfig.substs.MOZ_INSTALL_TRACKING) {
+ compile "com.google.android.gms:play-services-ads:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
+ compile "com.google.android.gms:play-services-basement:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
+ }
+
+ if (mozconfig.substs.MOZ_ANDROID_GCM) {
+ compile "com.google.android.gms:play-services-basement:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
+ compile "com.google.android.gms:play-services-base:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
+ compile "com.google.android.gms:play-services-gcm:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
+ compile "com.google.android.gms:play-services-measurement:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
+ }
+
+ // Include LeakCanary in most gradle based builds. LeakCanary adds about 5k methods, so we disable
+ // it for the (non-proguarded, non-multidex) localOld builds to allow space for other libraries.
+ // Gradle based tests include the no-op version. Mach based builds only include the no-op version
+ // of this library.
+ // It doesn't seem like there is a non-trivial way to be conditional on 'localOld', so instead we explicitly
+ // define a version of leakcanary for every flavor:
+ localCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta1'
+ localOldCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
+ automationCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
+ testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
+
+ compile project(':geckoview')
+ compile project(':thirdparty')
+
+ testCompile 'junit:junit:4.12'
+ testCompile 'org.robolectric:robolectric:3.1.2'
+ testCompile 'org.simpleframework:simple-http:6.0.1'
+ testCompile 'org.mockito:mockito-core:1.10.19'
+
+ // Including the Robotium JAR directly can cause issues with dexing.
+ androidTestCompile 'com.jayway.android.robotium:robotium-solo:5.5.4'
+}
+
+// TODO: (bug 1261486): This impl is not robust -
+// we just wanted to land something.
+task checkstyle(type: Checkstyle) {
+ configFile file("checkstyle.xml")
+ // TODO: should use sourceSets from project instead of hard-coded str.
+ source '../base/java/'
+ // TODO: This ignores our pre-processed resources.
+ include '**/*.java'
+ // TODO: classpath should probably be something.
+ classpath = files()
+}
+
+task syncPreprocessedCode(type: Sync, dependsOn: rootProject.generateCodeAndResources) {
+ into("${project.buildDir}/generated/source/preprocessed_code")
+ from("${topobjdir}/mobile/android/base/generated/preprocessed") {
+ // All other preprocessed code is included in the geckoview project.
+ include '**/AdjustConstants.java'
+ }
+}
+
+// The localization system uses the moz.build preprocessor to interpolate a .dtd
+// file of XML entity definitions into an XML file of elements referencing those
+// entities. (Each locale produces its own .dtd file, backstopped by the en-US
+// .dtd file in tree.) Android Studio (and IntelliJ) don't handle these inline
+// entities smoothly. This filter merely expands the entities in place, making
+// them appear properly throughout the IDE. Be aware that this assumes that the
+// JVM's file.encoding is utf-8. See comments in
+// mobile/android/mach_commands.py.
+class ExpandXMLEntitiesFilter extends FilterReader {
+ ExpandXMLEntitiesFilter(Reader input) {
+ // Extremely inefficient, but whatever.
+ super(new StringReader(groovy.xml.XmlUtil.serialize(new XmlParser(false, false, true).parse(input))))
+ }
+}
+
+task syncPreprocessedResources(type: Sync, dependsOn: rootProject.generateCodeAndResources) {
+ into("${project.buildDir}/generated/source/preprocessed_resources")
+ from("${topobjdir}/mobile/android/base/res")
+ filesMatching('**/strings.xml') {
+ filter(ExpandXMLEntitiesFilter)
+ }
+}
+
+// It's not easy -- see the backout in Bug 1242213 -- to change the <manifest>
+// package for Fennec. Gradle has grown a mechanism to achieve what we want for
+// Fennec, however, with applicationId. To use the same manifest as moz.build,
+// we replace the package with org.mozilla.gecko (the eventual package) here.
+task rewriteManifestPackage(type: Copy, dependsOn: rootProject.generateCodeAndResources) {
+ into("${project.buildDir}/generated/source/preprocessed_manifest")
+ from("${topobjdir}/mobile/android/base/AndroidManifest.xml")
+ filter { it.replaceFirst(/package=".*?"/, 'package="org.mozilla.gecko"') }
+}
+
+apply from: "${topsrcdir}/mobile/android/gradle/with_gecko_binaries.gradle"
+
+android.applicationVariants.all { variant ->
+ variant.preBuild.dependsOn rewriteManifestPackage
+ variant.preBuild.dependsOn syncPreprocessedCode
+ variant.preBuild.dependsOn syncPreprocessedResources
+
+ // Automation builds don't include Gecko binaries, since those binaries are
+ // not produced until after build time (at package time). Therefore,
+ // automation builds include the Gecko binaries into the APK at package
+ // time. The "withGeckoBinaries" variant of the :geckoview project also
+ // does this. (It does what it says on the tin!) For notes on this
+ // approach, see mobile/android/gradle/with_gecko_binaries.gradle.
+
+ // Like 'local' or 'localOld'.
+ def productFlavor = variant.productFlavors[0].name
+
+ // :app uses :geckoview:release and handles it's own Gecko binary inclusion,
+ // even though this would be most naturally done in the :geckoview project.
+ if (!productFlavor.equals('automation')) {
+ configureVariantWithGeckoBinaries(variant)
+ }
+}
+
+apply plugin: 'spoon'
+
+spoon {
+ // For now, let's be verbose.
+ debug = true
+ // It's not helpful to pass when we don't have a device connected.
+ failIfNoDeviceConnected = true
+
+ def spoonPackageName
+ if (gradle.startParameter.taskNames.contains('runBrowserTests')) {
+ spoonPackageName = 'org.mozilla.tests.browser.junit3'
+ }
+ if (gradle.startParameter.taskNames.contains('runBackgroundTests')) {
+ spoonPackageName = 'org.mozilla.gecko.background'
+ }
+ if (project.hasProperty('spoonPackageName')) {
+ // Command line overrides everything.
+ spoonPackageName = project.spoonPackageName
+ }
+ if (spoonPackageName) {
+ instrumentationArgs = ['-e', "package=${spoonPackageName}".toString()]
+ }
+}
+
+// See discussion at https://github.com/stanfy/spoon-gradle-plugin/issues/9.
+afterEvaluate {
+ tasks["spoonLocal${android.testBuildType.capitalize()}AndroidTest"].outputs.upToDateWhen { false }
+
+ // This is an awkward way to define different sets of instrumentation tests.
+ // The task name itself is fished at runtime and the package name configured
+ // in the spoon configuration.
+ task runBrowserTests {
+ dependsOn tasks["spoonLocalOldDebugAndroidTest"]
+ }
+ task runBackgroundTests {
+ dependsOn tasks["spoonLocalOldDebugAndroidTest"]
+ }
+}
+
+// Bug 1299015: Complain to treeherder if checkstyle, lint, or unittest fails. It's not obvious
+// how to listen to individual errors in most cases, so we just link to the reports for now.
+def makeTaskExecutionListener(artifactRootUrl) {
+ return new TaskExecutionListener() {
+ void beforeExecute(Task task) {
+ // Do nothing.
+ }
+
+ void afterExecute(Task task, TaskState state) {
+ if (!state.failure) {
+ return
+ }
+
+ // Link to the failing report. The task path and the report path
+ // depend on the android-lint task in
+ // taskcluster/ci/android-stuff/kind.yml. It's not possible to link
+ // directly, so for now consumers will need to copy-paste the URL.
+ switch (task.path) {
+ case ':app:checkstyle':
+ def url = "${artifactRootUrl}/public/android/checkstyle/checkstyle.xml"
+ println "TEST-UNEXPECTED-FAIL | android-checkstyle | Checkstyle rule violations were found. See the report at: $url"
+ break
+
+ case ':app:lintAutomationDebug':
+ def url = "${artifactRootUrl}/public/android/lint/lint-results-automationDebug.html"
+ println "TEST-UNEXPECTED-FAIL | android-lint | Lint found errors in the project; aborting build. See the report at: $url"
+ break
+
+ case ':app:testAutomationDebugUnitTest':
+ def url = "${artifactRootUrl}/public/android/unittest/automationDebug/index.html"
+ println "TEST-UNEXPECTED-FAIL | android-test | There were failing tests. See the report at: $url"
+ break
+ }
+ }
+ }
+}
+
+// TASK_ID and RUN_ID are provided by docker-worker; see
+// https://docs.taskcluster.net/manual/execution/workers/docker-worker.
+if (System.env.TASK_ID && System.env.RUN_ID) {
+ def artifactRootUrl = "https://queue.taskcluster.net/v1/task/${System.env.TASK_ID}/runs/${System.env.RUN_ID}/artifacts"
+ gradle.addListener(makeTaskExecutionListener(artifactRootUrl))
+}
diff --git a/mobile/android/app/checkstyle.xml b/mobile/android/app/checkstyle.xml
new file mode 100644
index 0000000000..09a200d28a
--- /dev/null
+++ b/mobile/android/app/checkstyle.xml
@@ -0,0 +1,63 @@
+<?xml version="1.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/. -->
+<!DOCTYPE module PUBLIC
+ "-//Puppy Crawl//DTD Check Configuration 1.2//EN"
+ "http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
+
+<!-- TODO: Clean up code & add checks:
+ - WhitespaceAround
+ - EmptyLineSeparator
+ - NeedBraces
+ - LeftCurly // placement of "{" in new scope or literal
+ - RightCurly // placement of "}" in close scope or literal
+ - Indentation
+ - OneStatementPerLine
+ - OperatorWrap
+ - SeparatorWrap
+ - MultipleVariableDeclarations
+ - FallThrough
+ (I spent too much time already)
+
+ Maybe add:
+ - OneTopLevelClass
+ - OverloadMethodsDeclarationOrder
+ - Empty*Block // better to catch errors!
+ (I spent too much time already)
+
+ See http://checkstyle.sourceforge.net/google_style.html
+ for a good set of defaults.
+-->
+
+<module name="Checker">
+ <property name="charset" value="UTF-8"/>
+
+ <!-- TODO: <property name="fileExtensions" value="java, properties, xml"/> -->
+
+ <module name="FileTabCharacter"> <!-- No tabs! -->
+ <property name="eachLine" value="true"/>
+ </module>
+ <module name="RegexpSingleline"> <!-- excess whitespace -->
+ <property name="format" value="\s+$"/>
+ <property name="message" value="Trailing whitespace"/>
+ </module>
+
+ <module name="TreeWalker">
+ <module name="GenericWhitespace"/> <!-- whitespace for generics -->
+ <module name="NoLineWrap">
+ <property name="tokens" value="IMPORT,PACKAGE_DEF"/>
+ </module>
+ <module name="OuterTypeFilename"/> <!-- `class Lol` only in Lol.java -->
+ <module name="WhitespaceAfter">
+ <!-- TODO: (bug 1263059) Remove specific tokens to enable CAST check. -->
+ <property name="tokens" value="COMMA, SEMI"/>
+ </module>
+ <module name="WhitespaceAround">
+ <property name="allowEmptyConstructors" value="true"/>
+ <property name="allowEmptyMethods" value="true"/>
+ <property name="allowEmptyTypes" value="true"/>
+ </module>
+ </module>
+
+</module>
diff --git a/mobile/android/app/lint.xml b/mobile/android/app/lint.xml
new file mode 100644
index 0000000000..6d818a6c1f
--- /dev/null
+++ b/mobile/android/app/lint.xml
@@ -0,0 +1,223 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<lint>
+ <!-- Enable relevant checks disabled by default -->
+ <issue id="NegativeMargin" severity="warning" />
+
+ <!-- We have a custom menu and don't conform to the recommended styles. -->
+ <issue id="IconColors" severity="ignore" />
+
+ <!-- We use our own preprocessing to either add or remove
+ `android:debuggable` when building with mach so it's
+ not actually hard-coded. We can probably remove this
+ warning when we switch to gradle. -->
+ <issue id="HardcodedDebugMode" severity="ignore" />
+
+ <!-- We have our own l10n system & don't use the platform's plurals. -->
+ <issue id="PluralsCandidate" severity="ignore" />
+
+ <!-- We don't want to have to follow the SDK release schedule: we can keep
+ the warning in order to not forget that there's a new SDK, but there's
+ no need to break on update. -->
+ <issue id="OldTargetApi" severity="warning" />
+
+ <!-- We want all lint warnings to be fatal errors.
+ Right now, we set these to lint warnings so:
+
+ DO NOT ADD TO THIS LIST.
+
+ We did this so we can land lint in automation
+ and not fail everything. -->
+ <issue id="AppCompatResource" severity="warning" />
+ <issue id="GoogleAppIndexingDeepLinkError" severity="warning" />
+ <issue id="GoogleAppIndexingUrlError" severity="warning" />
+ <issue id="Instantiatable" severity="warning" />
+ <issue id="LongLogTag" severity="warning" />
+ <issue id="MissingPermission" severity="warning" />
+ <issue id="NewApi" severity="warning" />
+ <issue id="OnClick" severity="warning" />
+ <issue id="ReferenceType" severity="warning" />
+ <issue id="ResourceAsColor" severity="warning" />
+ <issue id="ResourceType" severity="warning" />
+ <issue id="ValidFragment" severity="warning" />
+ <issue id="WrongConstant" severity="warning" />
+
+ <!-- We fixed all "Registered" lint errors. However the current gradle plugin has a bug where
+ it ignores @SuppressLint annotations for this check. See CrashReporter class and
+ https://code.google.com/p/android/issues/detail?id=204846 -->
+ <issue id="Registered" severity="warning" />
+
+ <!-- WHEN YOU FIX A LINT WARNING, ADD IT TO THIS LIST.
+
+ We want all lint warnings to be fatal errors.
+ This is the list of checks that we've explicitly
+ set as errors. Ideally, once we have no more warnings,
+ we switch to the `warningsAsErrors` lint option
+ (bug 1253737) rather than listing everything explicitly. -->
+ <issue id="AaptCrash" severity="error" />
+ <issue id="Accessibility" severity="error" />
+ <issue id="AccidentalOctal" severity="error" />
+ <issue id="AdapterViewChildren" severity="error" />
+ <issue id="AddJavascriptInterface" severity="error" />
+ <issue id="AllowBackup" severity="error" />
+ <issue id="AlwaysShowAction" severity="error" />
+ <issue id="AndroidGradlePluginVersion" severity="error" />
+ <issue id="AppCompatMethod" severity="error" />
+ <issue id="AppIndexingError" severity="error" />
+ <issue id="AppIndexingWarning" severity="error" />
+ <issue id="Assert" severity="error" />
+ <issue id="ButtonCase" severity="error" />
+ <issue id="ButtonOrder" severity="error" />
+ <issue id="ByteOrderMark" severity="error" />
+ <issue id="CheckResult" severity="error" />
+ <issue id="Correctness" severity="error" />
+ <issue id="CutPasteId" severity="error" />
+ <issue id="DalvikOverride" severity="error" />
+ <issue id="DeviceAdmin" severity="error" />
+ <issue id="DisableBaselineAlignment" severity="error" />
+ <issue id="DrawAllocation" severity="error" />
+ <issue id="DuplicateActivity" severity="error" />
+ <issue id="DuplicateDefinition" severity="error" />
+ <issue id="DuplicateIds" severity="error" />
+ <issue id="DuplicateIncludedIds" severity="error" />
+ <issue id="DuplicateUsesFeature" severity="error" />
+ <issue id="ExportedContentProvider" severity="error" />
+ <issue id="ExportedPreferenceActivity" severity="error" />
+ <issue id="ExtraText" severity="error" />
+ <issue id="ExtraTranslation" severity="error" />
+ <issue id="FloatMath" severity="error" />
+ <issue id="FullBackupContent" severity="error" />
+ <issue id="GetInstance" severity="error" />
+ <issue id="GifUsage" severity="error" />
+ <issue id="GradleCompatible" severity="error" />
+ <issue id="GradleDependency" severity="error" />
+ <issue id="GradleDeprecated" severity="error" />
+ <issue id="GradleDynamicVersion" severity="error" />
+ <issue id="GradleGetter" severity="error" />
+ <issue id="GradleIdeError" severity="error" />
+ <issue id="GradlePath" severity="error" />
+ <issue id="GrantAllUris" severity="error" />
+ <issue id="GridLayout" severity="error" />
+ <issue id="HandlerLeak" severity="error" />
+ <issue id="HardcodedText" severity="error" />
+ <issue id="IconExtension" severity="error" />
+ <issue id="IconLauncherShape" severity="error" />
+ <issue id="IconMixedNinePatch" severity="error" />
+ <issue id="IconNoDpi" severity="error" />
+ <issue id="IllegalResourceRef" severity="error" />
+ <issue id="ImpliedQuantity" severity="error" />
+ <issue id="InOrMmUsage" severity="error" />
+ <issue id="IncludeLayoutParam" severity="error" />
+ <issue id="InconsistentArrays" severity="error" />
+ <issue id="InefficientWeight" severity="error" />
+ <issue id="InnerclassSeparator" severity="error" />
+ <issue id="Internationalization" severity="error" />
+ <issue id="InvalidId" severity="error" />
+ <issue id="InvalidPackage" severity="error" />
+ <issue id="InvalidResourceFolder" severity="error" />
+ <issue id="JavascriptInterface" severity="error" />
+ <issue id="LabelFor" severity="error" />
+ <issue id="LibraryCustomView" severity="error" />
+ <issue id="LocalSuppress" severity="error" />
+ <issue id="LocaleFolder" severity="error" />
+ <issue id="LogTagMismatch" severity="error" />
+ <issue id="MangledCRLF" severity="error" />
+ <issue id="ManifestOrder" severity="error" />
+ <issue id="ManifestTypo" severity="error" />
+ <issue id="MenuTitle" severity="error" />
+ <issue id="MergeRootFrame" severity="error" />
+ <issue id="MipmapIcons" severity="error" />
+ <issue id="MissingApplicationIcon" severity="error" />
+ <issue id="MissingId" severity="error" />
+ <issue id="MissingPrefix" severity="error" />
+ <issue id="MissingQuantity" severity="error" />
+ <issue id="MissingRegistered" severity="error" />
+ <issue id="MissingSuperCall" severity="error" />
+ <issue id="MissingTranslation" severity="error" />
+ <issue id="MissingVersion" severity="error" />
+ <issue id="MockLocation" severity="error" />
+ <issue id="MultipleUsesSdk" severity="error" />
+ <issue id="NamespaceTypo" severity="error" />
+ <issue id="NestedScrolling" severity="error" />
+ <issue id="NfcTechWhitespace" severity="error" />
+ <issue id="NotSibling" severity="error" />
+ <issue id="ObsoleteLayoutParam" severity="error" />
+ <issue id="OnClick" severity="error" />
+ <issue id="Orientation" severity="error" />
+ <issue id="Override" severity="error" />
+ <issue id="OverrideAbstract" severity="error" />
+ <issue id="PackagedPrivateKey" severity="error" />
+ <issue id="ParcelCreator" severity="error" />
+ <issue id="Performance" severity="error" />
+ <issue id="Proguard" severity="error" />
+ <issue id="ProguardSplit" severity="error" />
+ <issue id="PropertyEscape" severity="error" />
+ <issue id="ProtectedPermissions" severity="error" />
+ <issue id="PxUsage" severity="error" />
+ <issue id="Range" severity="error" />
+ <issue id="RelativeOverlap" severity="error" />
+ <issue id="RequiredSize" severity="error" />
+ <issue id="ResAuto" severity="error" />
+ <issue id="ResourceCycle" severity="error" />
+ <issue id="ResourceName" severity="error" />
+ <issue id="ResourceType" severity="error" />
+ <issue id="RtlCompat" severity="error" />
+ <issue id="RtlEnabled" severity="error" />
+ <issue id="ScrollViewCount" severity="error" />
+ <issue id="ScrollViewSize" severity="error" />
+ <issue id="SecureRandom" severity="error" />
+ <issue id="Security" severity="error" />
+ <issue id="ServiceCast" severity="error" />
+ <issue id="SetJavaScriptEnabled" severity="error" />
+ <issue id="ShiftFlags" severity="error" />
+ <issue id="ShortAlarm" severity="error" />
+ <issue id="ShowToast" severity="error" />
+ <issue id="SignatureOrSystemPermissions" severity="error" />
+ <issue id="StringFormatCount" severity="error" />
+ <issue id="StringFormatInvalid" severity="error" />
+ <issue id="StringFormatMatches" severity="error" />
+ <issue id="StringShouldBeInt" severity="error" />
+ <issue id="SuspiciousImport" severity="error" />
+ <issue id="TextFields" severity="error" />
+ <issue id="TextViewEdits" severity="error" />
+ <issue id="TooDeepLayout" severity="error" />
+ <issue id="TooManyViews" severity="error" />
+ <issue id="TrulyRandom" severity="error" />
+ <issue id="TypographyDashes" severity="error" />
+ <issue id="TypographyFractions" severity="error" />
+ <issue id="TypographyOther" severity="error" />
+ <issue id="Typos" severity="error" />
+ <issue id="UniqueConstants" severity="error" />
+ <issue id="UniquePermission" severity="error" />
+ <issue id="UnknownId" severity="error" />
+ <issue id="UnknownIdInLayout" severity="error" />
+ <issue id="UnlocalizedSms" severity="error" />
+ <issue id="UnusedNamespace" severity="error" />
+ <issue id="UnusedQuantity" severity="error" />
+ <issue id="UnusedResources" severity="error">
+ <!-- The moz.build based build system leaves a .mkdir.done file lying around in the
+ preprocessed_resources res/raw folder. Lint reports it as unused. We should get
+ rid of the file eventually. See bug 1268948. -->
+ <ignore path="**/raw/.mkdir.done" />
+ </issue>
+ <issue id="Usability" severity="error" />
+ <issue id="UseCheckPermission" severity="error" />
+ <issue id="UseCompoundDrawables" severity="error" />
+ <issue id="UselessLeaf" severity="error" />
+ <issue id="UsesMinSdkAttributes" severity="error" />
+ <issue id="UsingHttp" severity="error" />
+ <issue id="ViewHolder" severity="error" />
+ <issue id="ViewTag" severity="error" />
+ <issue id="Wakelock" severity="error" />
+ <issue id="WebViewLayout" severity="error" />
+ <issue id="WorldReadableFiles" severity="error" />
+ <issue id="WorldWriteableFiles" severity="error" />
+ <issue id="WrongCall" severity="error" />
+ <issue id="WrongCase" severity="error" />
+ <issue id="WrongConstant" severity="error" />
+ <issue id="WrongFolder" severity="error" />
+ <issue id="WrongManifestParent" severity="error" />
+ <issue id="WrongRegion" severity="error" />
+ <issue id="WrongThread" severity="error" />
+ <issue id="WrongViewCast" severity="error" />
+
+</lint>
diff --git a/mobile/android/app/mobile.ico b/mobile/android/app/mobile.ico
new file mode 100644
index 0000000000..38312abac4
--- /dev/null
+++ b/mobile/android/app/mobile.ico
Binary files differ
diff --git a/mobile/android/app/mobile.js b/mobile/android/app/mobile.js
new file mode 100644
index 0000000000..2a64297c4b
--- /dev/null
+++ b/mobile/android/app/mobile.js
@@ -0,0 +1,920 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#filter substitution
+
+// For browser.xml binding
+//
+// cacheRatio* is a ratio that determines the amount of pixels to cache. The
+// ratio is multiplied by the viewport width or height to get the displayport's
+// width or height, respectively.
+//
+// (divide integer value by 1000 to get the ratio)
+//
+// For instance: cachePercentageWidth is 1500
+// viewport height is 500
+// => display port height will be 500 * 1.5 = 750
+//
+pref("toolkit.browser.cacheRatioWidth", 2000);
+pref("toolkit.browser.cacheRatioHeight", 3000);
+
+// How long before a content view (a handle to a remote scrollable object)
+// expires.
+pref("toolkit.browser.contentViewExpire", 3000);
+
+pref("toolkit.defaultChromeURI", "chrome://browser/content/browser.xul");
+pref("browser.chromeURL", "chrome://browser/content/");
+
+// If a tab has not been active for this long (seconds), then it may be
+// turned into a zombie tab to preemptively free up memory. -1 disables time-based
+// expiration (but low-memory conditions may still require the tab to be zombified).
+pref("browser.tabs.expireTime", 900);
+
+// Disables zombification of background tabs under memory pressure.
+// Intended for use in testing, where we don't want the tab running the
+// test harness code to be zombified.
+pref("browser.tabs.disableBackgroundZombification", false);
+
+// Control whether tab content should try to load from disk cache when network
+// is offline.
+// Controlled by Switchboard experiment "offline-cache".
+pref("browser.tabs.useCache", false);
+
+// From libpref/src/init/all.js, extended to allow a slightly wider zoom range.
+pref("zoom.minPercent", 20);
+pref("zoom.maxPercent", 400);
+pref("toolkit.zoomManager.zoomValues", ".2,.3,.5,.67,.8,.9,1,1.1,1.2,1.33,1.5,1.7,2,2.4,3,4");
+
+// Mobile will use faster, less durable mode.
+pref("toolkit.storage.synchronous", 0);
+
+pref("browser.viewport.desktopWidth", 980);
+// The default fallback zoom level to render pages at. Set to -1 to fit page; otherwise
+// the value is divided by 1000 and clamped to hard-coded min/max scale values.
+pref("browser.viewport.defaultZoom", -1);
+
+// Show/Hide scrollbars when active/inactive
+pref("ui.showHideScrollbars", 1);
+pref("ui.useOverlayScrollbars", 1);
+pref("ui.scrollbarFadeBeginDelay", 450);
+pref("ui.scrollbarFadeDuration", 0);
+
+/* turn off the caret blink after 10 cycles */
+pref("ui.caretBlinkCount", 10);
+
+/* cache prefs */
+pref("browser.cache.disk.enable", true);
+pref("browser.cache.disk.capacity", 20480); // kilobytes
+pref("browser.cache.disk.max_entry_size", 4096); // kilobytes
+pref("browser.cache.disk.smart_size.enabled", true);
+pref("browser.cache.disk.smart_size.first_run", true);
+
+#ifdef MOZ_PKG_SPECIAL
+// low memory devices
+pref("browser.cache.memory.enable", false);
+#else
+pref("browser.cache.memory.enable", true);
+#endif
+pref("browser.cache.memory.capacity", 1024); // kilobytes
+
+pref("browser.cache.memory_limit", 5120); // 5 MB
+
+/* image cache prefs */
+pref("image.cache.size", 1048576); // bytes
+
+/* offline cache prefs */
+pref("browser.offline-apps.notify", true);
+pref("browser.cache.offline.enable", true);
+pref("browser.cache.offline.capacity", 5120); // kilobytes
+pref("offline-apps.quota.warn", 1024); // kilobytes
+
+// cache compression turned off for now - see bug #715198
+pref("browser.cache.compression_level", 0);
+
+/* disable some protocol warnings */
+pref("network.protocol-handler.warn-external.tel", false);
+pref("network.protocol-handler.warn-external.sms", false);
+pref("network.protocol-handler.warn-external.mailto", false);
+pref("network.protocol-handler.warn-external.vnd.youtube", false);
+
+/* http prefs */
+pref("network.http.pipelining", true);
+pref("network.http.pipelining.ssl", true);
+pref("network.http.proxy.pipelining", true);
+pref("network.http.pipelining.maxrequests" , 6);
+pref("network.http.keep-alive.timeout", 109);
+pref("network.http.max-connections", 20);
+pref("network.http.max-persistent-connections-per-server", 6);
+pref("network.http.max-persistent-connections-per-proxy", 20);
+
+// spdy
+pref("network.http.spdy.push-allowance", 32768);
+pref("network.http.spdy.default-hpack-buffer", 4096); // 4k
+
+// See bug 545869 for details on why these are set the way they are
+pref("network.buffer.cache.count", 24);
+pref("network.buffer.cache.size", 16384);
+
+// predictive actions
+pref("network.predictor.enabled", true);
+pref("network.predictor.max-db-size", 2097152); // bytes
+pref("network.predictor.preserve", 50); // percentage of predictor data to keep when cleaning up
+
+// Use JS mDNS as a fallback
+pref("network.mdns.use_js_fallback", true);
+
+/* history max results display */
+pref("browser.display.history.maxresults", 100);
+
+/* How many times should have passed before the remote tabs list is refreshed */
+pref("browser.display.remotetabs.timeout", 10);
+
+/* session history */
+pref("browser.sessionhistory.max_total_viewers", 1);
+pref("browser.sessionhistory.max_entries", 50);
+pref("browser.sessionhistory.contentViewerTimeout", 360);
+pref("browser.sessionhistory.bfcacheIgnoreMemoryPressure", false);
+
+/* session store */
+pref("browser.sessionstore.resume_session_once", false);
+pref("browser.sessionstore.resume_from_crash", true);
+pref("browser.sessionstore.interval", 10000); // milliseconds
+pref("browser.sessionstore.backupInterval", 120000); // milliseconds -> 2 minutes
+pref("browser.sessionstore.max_tabs_undo", 10);
+pref("browser.sessionstore.max_resumed_crashes", 1);
+pref("browser.sessionstore.privacy_level", 0); // saving data: 0 = all, 1 = unencrypted sites, 2 = never
+pref("browser.sessionstore.debug_logging", false);
+
+/* these should help performance */
+pref("mozilla.widget.force-24bpp", true);
+pref("mozilla.widget.use-buffer-pixmap", true);
+pref("mozilla.widget.disable-native-theme", true);
+pref("layout.reflow.synthMouseMove", false);
+pref("layout.css.report_errors", false);
+
+/* download manager (don't show the window or alert) */
+pref("browser.download.useDownloadDir", true);
+pref("browser.download.folderList", 1); // Default to ~/Downloads
+pref("browser.download.manager.showAlertOnComplete", false);
+pref("browser.download.manager.showAlertInterval", 2000);
+pref("browser.download.manager.retention", 2);
+pref("browser.download.manager.showWhenStarting", false);
+pref("browser.download.manager.closeWhenDone", true);
+pref("browser.download.manager.openDelay", 0);
+pref("browser.download.manager.focusWhenStarting", false);
+pref("browser.download.manager.flashCount", 2);
+pref("browser.download.manager.displayedHistoryDays", 7);
+pref("browser.download.manager.addToRecentDocs", true);
+
+/* download helper */
+pref("browser.helperApps.deleteTempFileOnExit", false);
+
+/* password manager */
+pref("signon.rememberSignons", true);
+pref("signon.autofillForms.http", true);
+pref("signon.expireMasterPassword", false);
+pref("signon.debug", false);
+
+/* form helper (scroll to and optionally zoom into editable fields) */
+pref("formhelper.mode", 2); // 0 = disabled, 1 = enabled, 2 = dynamic depending on screen size
+pref("formhelper.autozoom", true);
+
+/* find helper */
+pref("findhelper.autozoom", true);
+
+/* autocomplete */
+pref("browser.formfill.enable", true);
+
+/* spellcheck */
+pref("layout.spellcheckDefault", 0);
+
+/* new html5 forms */
+pref("dom.experimental_forms", true);
+pref("dom.forms.number", true);
+
+/* extension manager and xpinstall */
+pref("xpinstall.whitelist.directRequest", false);
+pref("xpinstall.whitelist.fileRequest", false);
+pref("xpinstall.whitelist.add", "https://addons.mozilla.org,https://testpilot.firefox.com");
+
+pref("xpinstall.signatures.required", true);
+
+pref("extensions.enabledScopes", 1);
+pref("extensions.autoupdate.enabled", true);
+pref("extensions.autoupdate.interval", 86400);
+pref("extensions.update.enabled", true);
+pref("extensions.update.interval", 86400);
+pref("extensions.dss.enabled", false);
+pref("extensions.dss.switchPending", false);
+pref("extensions.ignoreMTimeChanges", false);
+pref("extensions.logging.enabled", false);
+pref("extensions.hideInstallButton", true);
+pref("extensions.showMismatchUI", false);
+pref("extensions.hideUpdateButton", false);
+pref("extensions.strictCompatibility", false);
+pref("extensions.minCompatibleAppVersion", "11.0");
+
+pref("extensions.update.url", "https://versioncheck.addons.mozilla.org/update/VersionCheck.php?reqVersion=%REQ_VERSION%&id=%ITEM_ID%&version=%ITEM_VERSION%&maxAppVersion=%ITEM_MAXAPPVERSION%&status=%ITEM_STATUS%&appID=%APP_ID%&appVersion=%APP_VERSION%&appOS=%APP_OS%&appABI=%APP_ABI%&locale=%APP_LOCALE%&currentAppVersion=%CURRENT_APP_VERSION%&updateType=%UPDATE_TYPE%&compatMode=%COMPATIBILITY_MODE%");
+pref("extensions.update.background.url", "https://versioncheck-bg.addons.mozilla.org/update/VersionCheck.php?reqVersion=%REQ_VERSION%&id=%ITEM_ID%&version=%ITEM_VERSION%&maxAppVersion=%ITEM_MAXAPPVERSION%&status=%ITEM_STATUS%&appID=%APP_ID%&appVersion=%APP_VERSION%&appOS=%APP_OS%&appABI=%APP_ABI%&locale=%APP_LOCALE%&currentAppVersion=%CURRENT_APP_VERSION%&updateType=%UPDATE_TYPE%&compatMode=%COMPATIBILITY_MODE%");
+
+pref("extensions.hotfix.id", "firefox-android-hotfix@mozilla.org");
+pref("extensions.hotfix.cert.checkAttributes", true);
+pref("extensions.hotfix.certs.1.sha1Fingerprint", "91:53:98:0C:C1:86:DF:47:8F:35:22:9E:11:C9:A7:31:04:49:A1:AA");
+
+/* preferences for the Get Add-ons pane */
+pref("extensions.getAddons.cache.enabled", true);
+pref("extensions.getAddons.maxResults", 15);
+pref("extensions.getAddons.recommended.browseURL", "https://addons.mozilla.org/%LOCALE%/android/recommended/");
+pref("extensions.getAddons.recommended.url", "https://services.addons.mozilla.org/%LOCALE%/android/api/%API_VERSION%/list/featured/all/%MAX_RESULTS%/%OS%/%VERSION%");
+pref("extensions.getAddons.search.browseURL", "https://addons.mozilla.org/%LOCALE%/android/search?q=%TERMS%&platform=%OS%&appver=%VERSION%");
+pref("extensions.getAddons.search.url", "https://services.addons.mozilla.org/%LOCALE%/android/api/%API_VERSION%/search/%TERMS%/all/%MAX_RESULTS%/%OS%/%VERSION%/%COMPATIBILITY_MODE%");
+pref("extensions.getAddons.browseAddons", "https://addons.mozilla.org/%LOCALE%/android/");
+pref("extensions.getAddons.get.url", "https://services.addons.mozilla.org/%LOCALE%/android/api/%API_VERSION%/search/guid:%IDS%?src=mobile&appOS=%OS%&appVersion=%VERSION%");
+pref("extensions.getAddons.getWithPerformance.url", "https://services.addons.mozilla.org/%LOCALE%/android/api/%API_VERSION%/search/guid:%IDS%?src=mobile&appOS=%OS%&appVersion=%VERSION%&tMain=%TIME_MAIN%&tFirstPaint=%TIME_FIRST_PAINT%&tSessionRestored=%TIME_SESSION_RESTORED%");
+
+/* preference for the locale picker */
+pref("extensions.getLocales.get.url", "");
+pref("extensions.compatability.locales.buildid", "0");
+
+/* Don't let XPIProvider install distribution add-ons; we do our own thing on mobile. */
+pref("extensions.installDistroAddons", false);
+
+// Add-on content security policies.
+pref("extensions.webextensions.base-content-security-policy", "script-src 'self' https://* moz-extension: blob: filesystem: 'unsafe-eval' 'unsafe-inline'; object-src 'self' https://* moz-extension: blob: filesystem:;");
+pref("extensions.webextensions.default-content-security-policy", "script-src 'self'; object-src 'self';");
+
+/* block popups by default, and notify the user about blocked popups */
+pref("dom.disable_open_during_load", true);
+pref("privacy.popups.showBrowserMessage", true);
+
+/* disable opening windows with the dialog feature */
+pref("dom.disable_window_open_dialog_feature", true);
+pref("dom.disable_window_showModalDialog", true);
+pref("dom.disable_window_print", true);
+pref("dom.disable_window_find", true);
+
+pref("keyword.enabled", true);
+pref("browser.fixup.domainwhitelist.localhost", true);
+
+pref("accessibility.typeaheadfind", false);
+pref("accessibility.typeaheadfind.timeout", 5000);
+pref("accessibility.typeaheadfind.flashBar", 1);
+pref("accessibility.typeaheadfind.linksonly", false);
+pref("accessibility.typeaheadfind.casesensitive", 0);
+pref("accessibility.browsewithcaret_shortcut.enabled", false);
+
+// Whether the character encoding menu is under the main Firefox button. This
+// preference is a string so that localizers can alter it.
+pref("browser.menu.showCharacterEncoding", "chrome://browser/locale/browser.properties");
+
+// pointer to the default engine name
+pref("browser.search.defaultenginename", "chrome://browser/locale/region.properties");
+// SSL error page behaviour
+pref("browser.ssl_override_behavior", 2);
+pref("browser.xul.error_pages.expert_bad_cert", false);
+
+// ordering of search engines in the engine list.
+pref("browser.search.order.1", "chrome://browser/locale/region.properties");
+pref("browser.search.order.2", "chrome://browser/locale/region.properties");
+pref("browser.search.order.3", "chrome://browser/locale/region.properties");
+
+// Market-specific search defaults
+pref("browser.search.geoSpecificDefaults", true);
+pref("browser.search.geoSpecificDefaults.url", "https://search.services.mozilla.com/1/%APP%/%VERSION%/%CHANNEL%/%LOCALE%/%REGION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%");
+
+// US specific default (used as a fallback if the geoSpecificDefaults request fails).
+pref("browser.search.defaultenginename.US", "chrome://browser/locale/region.properties");
+pref("browser.search.order.US.1", "chrome://browser/locale/region.properties");
+pref("browser.search.order.US.2", "chrome://browser/locale/region.properties");
+pref("browser.search.order.US.3", "chrome://browser/locale/region.properties");
+
+// disable updating
+pref("browser.search.update", false);
+
+// disable search suggestions by default
+pref("browser.search.suggest.enabled", false);
+pref("browser.search.suggest.prompted", false);
+
+// tell the search service that we don't really expose the "current engine"
+pref("browser.search.noCurrentEngine", true);
+
+// Control media casting & mirroring features
+pref("browser.casting.enabled", true);
+#ifdef RELEASE_OR_BETA
+// Chromecast mirroring is broken (bug 1131084)
+pref("browser.mirroring.enabled", false);
+#else
+pref("browser.mirroring.enabled", true);
+#endif
+
+// Enable sparse localization by setting a few package locale overrides
+pref("chrome.override_package.global", "browser");
+pref("chrome.override_package.mozapps", "browser");
+pref("chrome.override_package.passwordmgr", "browser");
+
+// enable xul error pages
+pref("browser.xul.error_pages.enabled", true);
+
+// disable color management
+pref("gfx.color_management.mode", 0);
+
+// 0=fixed margin, 1=velocity bias, 2=dynamic resolution, 3=no margins, 4=prediction bias
+pref("gfx.displayport.strategy", 1);
+
+// all of the following displayport strategy prefs will be divided by 1000
+// to obtain some multiplier which is then used in the strategy.
+// fixed margin strategy options
+pref("gfx.displayport.strategy_fm.multiplier", -1); // displayport dimension multiplier
+pref("gfx.displayport.strategy_fm.danger_x", -1); // danger zone on x-axis when multiplied by viewport width
+pref("gfx.displayport.strategy_fm.danger_y", -1); // danger zone on y-axis when multiplied by viewport height
+
+// velocity bias strategy options
+pref("gfx.displayport.strategy_vb.multiplier", -1); // displayport dimension multiplier
+pref("gfx.displayport.strategy_vb.threshold", -1); // velocity threshold in inches/frame
+pref("gfx.displayport.strategy_vb.reverse_buffer", -1); // fraction of buffer to keep in reverse direction from scroll
+pref("gfx.displayport.strategy_vb.danger_x_base", -1); // danger zone on x-axis when multiplied by viewport width
+pref("gfx.displayport.strategy_vb.danger_y_base", -1); // danger zone on y-axis when multiplied by viewport height
+pref("gfx.displayport.strategy_vb.danger_x_incr", -1); // additional danger zone on x-axis when multiplied by viewport width and velocity
+pref("gfx.displayport.strategy_vb.danger_y_incr", -1); // additional danger zone on y-axis when multiplied by viewport height and velocity
+
+// prediction bias strategy options
+pref("gfx.displayport.strategy_pb.threshold", -1); // velocity threshold in inches/frame
+
+// Allow 24-bit colour when the hardware supports it
+pref("gfx.android.rgb16.force", false);
+
+// Allow GLContexts to be attached/detached from SurfaceTextures
+pref("gfx.SurfaceTexture.detach.enabled", true);
+
+// don't allow JS to move and resize existing windows
+pref("dom.disable_window_move_resize", true);
+
+// prevent click image resizing for nsImageDocument
+pref("browser.enable_click_image_resizing", false);
+
+// open in tab preferences
+// 0=default window, 1=current window/tab, 2=new window, 3=new tab in most window
+pref("browser.link.open_external", 3);
+pref("browser.link.open_newwindow", 3);
+// 0=force all new windows to tabs, 1=don't force, 2=only force those with no features set
+pref("browser.link.open_newwindow.restriction", 0);
+
+// show images option
+// 0=never, 1=always, 2=cellular-only
+pref("browser.image_blocking", 1);
+
+// controls which bits of private data to clear. by default we clear them all.
+pref("privacy.item.cache", true);
+pref("privacy.item.cookies", true);
+pref("privacy.item.offlineApps", true);
+pref("privacy.item.history", true);
+pref("privacy.item.searchHistory", true);
+pref("privacy.item.formdata", true);
+pref("privacy.item.downloads", true);
+pref("privacy.item.passwords", true);
+pref("privacy.item.sessions", true);
+pref("privacy.item.geolocation", true);
+pref("privacy.item.siteSettings", true);
+pref("privacy.item.syncAccount", true);
+
+// enable geo
+pref("geo.enabled", true);
+
+// content sink control -- controls responsiveness during page load
+// see https://bugzilla.mozilla.org/show_bug.cgi?id=481566#c9
+//pref("content.sink.enable_perf_mode", 2); // 0 - switch, 1 - interactive, 2 - perf
+//pref("content.sink.pending_event_mode", 0);
+//pref("content.sink.perf_deflect_count", 1000000);
+//pref("content.sink.perf_parse_time", 50000000);
+
+// Disable the JS engine's gc on memory pressure, since we do one in the mobile
+// browser (bug 669346).
+pref("javascript.options.gc_on_memory_pressure", false);
+
+#ifdef MOZ_PKG_SPECIAL
+// low memory devices
+pref("javascript.options.mem.gc_high_frequency_heap_growth_max", 120);
+pref("javascript.options.mem.gc_high_frequency_heap_growth_min", 120);
+pref("javascript.options.mem.gc_high_frequency_high_limit_mb", 40);
+pref("javascript.options.mem.gc_high_frequency_low_limit_mb", 10);
+pref("javascript.options.mem.gc_low_frequency_heap_growth", 120);
+pref("javascript.options.mem.high_water_mark", 16);
+pref("javascript.options.mem.gc_allocation_threshold_mb", 3);
+pref("javascript.options.mem.gc_min_empty_chunk_count", 1);
+pref("javascript.options.mem.gc_max_empty_chunk_count", 2);
+#else
+pref("javascript.options.mem.high_water_mark", 32);
+#endif
+
+pref("dom.max_chrome_script_run_time", 0); // disable slow script dialog for chrome
+pref("dom.max_script_run_time", 20);
+
+// Absolute path to the devtools unix domain socket file used
+// to communicate with a usb cable via adb forward.
+pref("devtools.debugger.unix-domain-socket", "/data/data/@ANDROID_PACKAGE_NAME@/firefox-debugger-socket");
+
+pref("devtools.remote.usb.enabled", false);
+pref("devtools.remote.wifi.enabled", false);
+
+pref("font.size.inflation.minTwips", 0);
+
+// When true, zooming will be enabled on all sites, even ones that declare user-scalable=no.
+pref("browser.ui.zoom.force-user-scalable", false);
+
+// When removing this Nightly flag, also remember to remove the flags surrounding this feature
+// in GeckoPreferences and BrowserApp (see bug 1245930).
+#ifdef NIGHTLY_BUILD
+pref("ui.zoomedview.enabled", true);
+#else
+pref("ui.zoomedview.enabled", false);
+#endif
+pref("ui.zoomedview.keepLimitSize", 16); // value in layer pixels, used to not keep the large elements in the cluster list (Bug 1191041)
+pref("ui.zoomedview.limitReadableSize", 8); // value in layer pixels
+pref("ui.zoomedview.defaultZoomFactor", 2);
+pref("ui.zoomedview.simplified", true); // Do not display all the zoomed view controls, do not use size heurisistic
+
+pref("ui.touch.radius.enabled", false);
+pref("ui.touch.radius.leftmm", 3);
+pref("ui.touch.radius.topmm", 5);
+pref("ui.touch.radius.rightmm", 3);
+pref("ui.touch.radius.bottommm", 2);
+pref("ui.touch.radius.visitedWeight", 120);
+
+pref("ui.mouse.radius.enabled", false);
+pref("ui.mouse.radius.leftmm", 3);
+pref("ui.mouse.radius.topmm", 5);
+pref("ui.mouse.radius.rightmm", 3);
+pref("ui.mouse.radius.bottommm", 2);
+pref("ui.mouse.radius.visitedWeight", 120);
+pref("ui.mouse.radius.reposition", true);
+
+// The percentage of the screen that needs to be scrolled before toolbar
+// manipulation is allowed.
+pref("browser.ui.scroll-toolbar-threshold", 10);
+
+// Maximum distance from the point where the user pressed where we still
+// look for text to select
+pref("browser.ui.selection.distance", 250);
+
+// plugins
+pref("plugin.disable", false);
+pref("dom.ipc.plugins.enabled", false);
+
+// This pref isn't actually used anymore, but we're leaving this here to avoid changing
+// the default so that we can migrate a user-set pref. See bug 885357.
+pref("plugins.click_to_play", true);
+// The default value for nsIPluginTag.enabledState (STATE_CLICKTOPLAY = 1)
+pref("plugin.default.state", 1);
+
+// product URLs
+// The breakpad report server to link to in about:crashes
+pref("breakpad.reportURL", "https://crash-stats.mozilla.com/report/index/");
+
+pref("app.support.baseURL", "https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/");
+pref("app.supportURL", "https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/mobile-help");
+pref("app.faqURL", "https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/faq");
+
+// URL for feedback page
+// This should be kept in sync with the "feedback_link" string defined in strings.xml.in
+pref("app.feedbackURL", "https://input.mozilla.org/feedback/android/%VERSION%/%CHANNEL%/?utm_source=feedback-prompt");
+
+pref("app.privacyURL", "https://www.mozilla.org/privacy/firefox/");
+pref("app.creditsURL", "https://www.mozilla.org/credits/");
+pref("app.channelURL", "https://www.mozilla.org/%LOCALE%/firefox/channel/");
+#if MOZ_UPDATE_CHANNEL == aurora
+pref("app.releaseNotesURL", "https://www.mozilla.com/%LOCALE%/mobile/%VERSION%/auroranotes/");
+#elif MOZ_UPDATE_CHANNEL == beta
+pref("app.releaseNotesURL", "https://www.mozilla.com/%LOCALE%/mobile/%VERSION%beta/releasenotes/");
+#else
+pref("app.releaseNotesURL", "https://www.mozilla.com/%LOCALE%/mobile/%VERSION%/releasenotes/");
+#endif
+
+// Name of alternate about: page for certificate errors (when undefined, defaults to about:neterror)
+pref("security.alternate_certificate_error_page", "certerror");
+
+pref("security.warn_viewing_mixed", false); // Warning is disabled. See Bug 616712.
+
+// Block insecure active content on https pages
+pref("security.mixed_content.block_active_content", true);
+
+// Enable pinning
+pref("security.cert_pinning.enforcement_level", 1);
+
+// Only fetch OCSP for EV certificates
+pref("security.OCSP.enabled", 2);
+
+// Override some named colors to avoid inverse OS themes
+pref("ui.-moz-dialog", "#efebe7");
+pref("ui.-moz-dialogtext", "#101010");
+pref("ui.-moz-field", "#fff");
+pref("ui.-moz-fieldtext", "#1a1a1a");
+pref("ui.-moz-buttonhoverface", "#f3f0ed");
+pref("ui.-moz-buttonhovertext", "#101010");
+pref("ui.-moz-combobox", "#fff");
+pref("ui.-moz-comboboxtext", "#101010");
+pref("ui.buttonface", "#ece7e2");
+pref("ui.buttonhighlight", "#fff");
+pref("ui.buttonshadow", "#aea194");
+pref("ui.buttontext", "#101010");
+pref("ui.captiontext", "#101010");
+pref("ui.graytext", "#b1a598");
+pref("ui.highlight", "#fad184");
+pref("ui.highlighttext", "#1a1a1a");
+pref("ui.infobackground", "#f5f5b5");
+pref("ui.infotext", "#000");
+pref("ui.menu", "#f7f5f3");
+pref("ui.menutext", "#101010");
+pref("ui.threeddarkshadow", "#000");
+pref("ui.threedface", "#ece7e2");
+pref("ui.threedhighlight", "#fff");
+pref("ui.threedlightshadow", "#ece7e2");
+pref("ui.threedshadow", "#aea194");
+pref("ui.window", "#efebe7");
+pref("ui.windowtext", "#101010");
+pref("ui.windowframe", "#efebe7");
+
+/* prefs used by the update timer system (including blocklist pings) */
+pref("app.update.timerFirstInterval", 30000); // milliseconds
+pref("app.update.timerMinimumDelay", 30); // seconds
+
+// used by update service to decide whether or not to
+// automatically download an update
+pref("app.update.autodownload", "wifi");
+pref("app.update.url.android", "https://aus5.mozilla.org/update/4/%PRODUCT%/%VERSION%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/%MOZ_VERSION%/update.xml");
+
+#ifdef MOZ_UPDATER
+/* prefs used specifically for updating the app */
+pref("app.update.enabled", false);
+pref("app.update.channel", "@MOZ_UPDATE_CHANNEL@");
+
+#endif
+
+// replace newlines with spaces on paste into single-line text boxes
+pref("editor.singleLine.pasteNewlines", 2);
+
+// threshold where a tap becomes a drag, in 1/240" reference pixels
+// The names of the preferences are to be in sync with EventStateManager.cpp
+pref("ui.dragThresholdX", 25);
+pref("ui.dragThresholdY", 25);
+
+pref("layers.acceleration.disabled", false);
+pref("layers.async-video.enabled", true);
+
+pref("apz.content_response_timeout", 600);
+pref("apz.allow_immediate_handoff", false);
+pref("apz.touch_start_tolerance", "0.06");
+pref("apz.axis_lock.breakout_angle", "0.7853982"); // PI / 4 (45 degrees)
+// APZ physics settings reviewed by UX
+pref("apz.axis_lock.mode", 1); // Use "strict" axis locking
+pref("apz.fling_curve_function_x1", "0.59");
+pref("apz.fling_curve_function_y1", "0.46");
+pref("apz.fling_curve_function_x2", "0.05");
+pref("apz.fling_curve_function_y2", "1.00");
+pref("apz.fling_curve_threshold_inches_per_ms", "0.01");
+// apz.fling_friction and apz.fling_stopped_threshold are currently ignored by Fennec.
+pref("apz.fling_friction", "0.004");
+pref("apz.fling_stopped_threshold", "0.0");
+pref("apz.max_velocity_inches_per_ms", "0.07");
+pref("apz.fling_accel_interval_ms", 750);
+pref("apz.overscroll.enabled", true);
+
+pref("layers.progressive-paint", true);
+pref("layers.low-precision-buffer", true);
+pref("layers.low-precision-resolution", "0.25");
+pref("layers.low-precision-opacity", "1.0");
+// We want to limit layers for two reasons:
+// 1) We can't scroll smoothly if we have to many draw calls
+// 2) Pages that have too many layers consume too much memory and crash.
+// By limiting the number of layers on mobile we're making the main thread
+// work harder keep scrolling smooth and memory low.
+pref("layers.max-active", 20);
+
+pref("notification.feature.enabled", true);
+pref("dom.webnotifications.enabled", true);
+
+// prevent tooltips from showing up
+pref("browser.chrome.toolbar_tips", false);
+
+// don't allow meta-refresh when backgrounded
+pref("browser.meta_refresh_when_inactive.disabled", true);
+
+// prevent video elements from preloading too much data
+pref("media.preload.default", 1); // default to preload none
+pref("media.preload.auto", 2); // preload metadata if preload=auto
+pref("media.cache_size", 32768); // 32MB media cache
+// Try to save battery by not resuming reading from a connection until we fall
+// below 10s of buffered data.
+pref("media.cache_resume_threshold", 10);
+pref("media.cache_readahead_limit", 30);
+
+// Number of video frames we buffer while decoding video.
+// On Android this is decided by a similar value which varies for
+// each OMX decoder |OMX_PARAM_PORTDEFINITIONTYPE::nBufferCountMin|. This
+// number must be less than the OMX equivalent or gecko will think it is
+// chronically starved of video frames. All decoders seen so far have a value
+// of at least 4.
+pref("media.video-queue.default-size", 3);
+
+// Enable the MediaCodec PlatformDecoderModule by default.
+pref("media.android-media-codec.enabled", true);
+pref("media.android-media-codec.preferred", true);
+// Run decoder in seperate process.
+pref("media.android-remote-codec.enabled", false);
+
+// Enable MSE
+pref("media.mediasource.enabled", true);
+
+pref("media.mediadrm-widevinecdm.visible", true);
+
+// optimize images memory usage
+pref("image.downscale-during-decode.enabled", true);
+
+pref("browser.safebrowsing.downloads.enabled", false);
+
+pref("browser.safebrowsing.id", @MOZ_APP_UA_NAME@);
+
+// True if this is the first time we are showing about:firstrun
+pref("browser.firstrun.show.uidiscovery", true);
+pref("browser.firstrun.show.localepicker", false);
+
+// True if you always want dump() to work
+//
+// On Android, you also need to do the following for the output
+// to show up in logcat:
+//
+// $ adb shell stop
+// $ adb shell setprop log.redirect-stdio true
+// $ adb shell start
+pref("browser.dom.window.dump.enabled", true);
+
+// controls if we want camera support
+pref("device.camera.enabled", true);
+pref("media.realtime_decoder.enabled", true);
+
+pref("javascript.options.showInConsole", true);
+
+pref("full-screen-api.enabled", true);
+
+pref("direct-texture.force.enabled", false);
+pref("direct-texture.force.disabled", false);
+
+// This fraction in 1000ths of velocity remains after every animation frame when the velocity is low.
+pref("ui.scrolling.friction_slow", -1);
+// This fraction in 1000ths of velocity remains after every animation frame when the velocity is high.
+pref("ui.scrolling.friction_fast", -1);
+// The maximum velocity change factor between events, per ms, in 1000ths.
+// Direction changes are excluded.
+pref("ui.scrolling.max_event_acceleration", -1);
+// The rate of deceleration when the surface has overscrolled, in 1000ths.
+pref("ui.scrolling.overscroll_decel_rate", -1);
+// The fraction of the surface which can be overscrolled before it must snap back, in 1000ths.
+pref("ui.scrolling.overscroll_snap_limit", -1);
+// The minimum amount of space that must be present for an axis to be considered scrollable,
+// in 1/1000ths of pixels.
+pref("ui.scrolling.min_scrollable_distance", -1);
+// The axis lock mode for panning behaviour - set between standard, free and sticky
+pref("ui.scrolling.axis_lock_mode", "standard");
+// Negate scroll, true will make the mouse scroll wheel move the screen the same direction as with most desktops or laptops.
+pref("ui.scrolling.negate_wheel_scroll", true);
+// Determine the dead zone for gamepad joysticks. Higher values result in larger dead zones; use a negative value to
+// auto-detect based on reported hardware values
+pref("ui.scrolling.gamepad_dead_zone", 115);
+
+// Prefs for fling acceleration
+pref("ui.scrolling.fling_accel_interval", -1);
+pref("ui.scrolling.fling_accel_base_multiplier", -1);
+pref("ui.scrolling.fling_accel_supplemental_multiplier", -1);
+
+// Prefs for fling curving
+pref("ui.scrolling.fling_curve_function_x1", -1);
+pref("ui.scrolling.fling_curve_function_y1", -1);
+pref("ui.scrolling.fling_curve_function_x2", -1);
+pref("ui.scrolling.fling_curve_function_y2", -1);
+pref("ui.scrolling.fling_curve_threshold_velocity", -1);
+pref("ui.scrolling.fling_curve_max_velocity", -1);
+pref("ui.scrolling.fling_curve_newton_iterations", -1);
+
+// Enable accessibility mode if platform accessibility is enabled.
+pref("accessibility.accessfu.activate", 2);
+pref("accessibility.accessfu.quicknav_modes", "Link,Heading,FormElement,Landmark,ListItem");
+// Active quicknav mode, index value of list from quicknav_modes
+pref("accessibility.accessfu.quicknav_index", 0);
+// Setting for an utterance order (0 - description first, 1 - description last).
+pref("accessibility.accessfu.utterance", 1);
+// Whether to skip images with empty alt text
+pref("accessibility.accessfu.skip_empty_images", true);
+
+// Transmit UDP busy-work to the LAN when anticipating low latency
+// network reads and on wifi to mitigate 802.11 Power Save Polling delays
+pref("network.tickle-wifi.enabled", true);
+
+// Mobile manages state by autodetection
+pref("network.manage-offline-status", true);
+
+// increase the timeout clamp for background tabs to 15 minutes
+pref("dom.min_background_timeout_value", 900000);
+
+// Media plugins for libstagefright playback on android
+pref("media.plugins.enabled", true);
+
+// Stagefright's OMXCodec::CreationFlags. The interesting flag values are:
+// 0 = Let Stagefright choose hardware or software decoding (default)
+// 8 = Force software decoding
+// 16 = Force hardware decoding
+pref("media.stagefright.omxcodec.flags", 0);
+
+// Coalesce touch events to prevent them from flooding the event queue
+pref("dom.event.touch.coalescing.enabled", false);
+
+// default orientation for the app, default to undefined
+// the java GeckoScreenOrientationListener needs this to be defined
+pref("app.orientation.default", "");
+
+// On memory pressure, release dirty but unused pages held by jemalloc
+// back to the system.
+pref("memory.free_dirty_pages", true);
+
+pref("layout.framevisibility.numscrollportwidths", 1);
+pref("layout.framevisibility.numscrollportheights", 1);
+
+pref("layers.enable-tiles", true);
+
+// Enable the dynamic toolbar
+pref("browser.chrome.dynamictoolbar", true);
+
+// Hide common parts of URLs like "www." or "http://"
+pref("browser.urlbar.trimURLs", true);
+
+#ifdef MOZ_PKG_SPECIAL
+// Disable webgl on ARMv6 because running the reftests takes
+// too long for some reason (bug 843738)
+pref("webgl.disabled", true);
+#endif
+
+// initial web feed readers list
+pref("browser.contentHandlers.types.0.title", "chrome://browser/locale/region.properties");
+pref("browser.contentHandlers.types.0.uri", "chrome://browser/locale/region.properties");
+pref("browser.contentHandlers.types.0.type", "application/vnd.mozilla.maybe.feed");
+pref("browser.contentHandlers.types.1.title", "chrome://browser/locale/region.properties");
+pref("browser.contentHandlers.types.1.uri", "chrome://browser/locale/region.properties");
+pref("browser.contentHandlers.types.1.type", "application/vnd.mozilla.maybe.feed");
+pref("browser.contentHandlers.types.2.title", "chrome://browser/locale/region.properties");
+pref("browser.contentHandlers.types.2.uri", "chrome://browser/locale/region.properties");
+pref("browser.contentHandlers.types.2.type", "application/vnd.mozilla.maybe.feed");
+pref("browser.contentHandlers.types.3.title", "chrome://browser/locale/region.properties");
+pref("browser.contentHandlers.types.3.uri", "chrome://browser/locale/region.properties");
+pref("browser.contentHandlers.types.3.type", "application/vnd.mozilla.maybe.feed");
+
+// Shortnumber matching needed for e.g. Brazil:
+// 01187654321 can be found with 87654321
+pref("dom.phonenumber.substringmatching.BR", 8);
+pref("dom.phonenumber.substringmatching.CO", 10);
+pref("dom.phonenumber.substringmatching.VE", 7);
+
+// Enable hardware-accelerated Skia canvas
+pref("gfx.canvas.azure.backends", "skia");
+pref("gfx.canvas.azure.accelerated", true);
+
+// See ua-update.json.in for the packaged UA override list
+pref("general.useragent.updates.enabled", true);
+pref("general.useragent.updates.url", "https://dynamicua.cdn.mozilla.net/0/%APP_ID%");
+pref("general.useragent.updates.interval", 604800); // 1 week
+pref("general.useragent.updates.retry", 86400); // 1 day
+
+// When true, phone number linkification is enabled.
+pref("browser.ui.linkify.phone", false);
+
+// Enables/disables Spatial Navigation
+pref("snav.enabled", true);
+
+// This url, if changed, MUST continue to point to an https url. Pulling arbitrary content to inject into
+// this page over http opens us up to a man-in-the-middle attack that we'd rather not face. If you are a downstream
+// repackager of this code using an alternate snippet url, please keep your users safe
+pref("browser.snippets.updateUrl", "https://snippets.cdn.mozilla.net/json/%SNIPPETS_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/");
+
+// How frequently we check for new snippets, in seconds (1 day)
+pref("browser.snippets.updateInterval", 86400);
+
+// URL used to check for user's country code. Please do not directly use this code or Snippets key.
+// Contact MLS team for your own credentials. https://location.services.mozilla.com/contact
+pref("browser.snippets.geoUrl", "https://location.services.mozilla.com/v1/country?key=fff72d56-b040-4205-9a11-82feda9d83a3");
+
+// URL used to ping metrics with stats about which snippets have been shown
+pref("browser.snippets.statsUrl", "https://snippets-stats.mozilla.org/mobile");
+
+// These prefs require a restart to take effect.
+pref("browser.snippets.enabled", true);
+pref("browser.snippets.syncPromo.enabled", true);
+pref("browser.snippets.firstrunHomepage.enabled", true);
+
+// The mode of home provider syncing.
+// 0: Sync always
+// 1: Sync only when on wifi
+pref("home.sync.updateMode", 0);
+
+// How frequently to check if we should sync home provider data.
+pref("home.sync.checkIntervalSecs", 3600);
+
+// Enable device storage API
+pref("device.storage.enabled", true);
+
+// Enable meta-viewport support for font inflation code
+pref("dom.meta-viewport.enabled", true);
+
+// Enable GMP support in the addon manager.
+pref("media.gmp-provider.enabled", true);
+
+// The default color scheme in reader mode (light, dark, auto)
+// auto = color automatically adjusts according to ambient light level
+// (auto only works on platforms where the 'devicelight' event is enabled)
+pref("reader.color_scheme", "auto");
+
+// Color scheme values available in reader mode UI.
+pref("reader.color_scheme.values", "[\"dark\",\"auto\",\"light\"]");
+
+// Whether to use a vertical or horizontal toolbar.
+pref("reader.toolbar.vertical", false);
+
+// Telemetry settings.
+// Whether to use the unified telemetry behavior, requires a restart.
+pref("toolkit.telemetry.unified", false);
+
+// Unified AccessibleCarets (touch-caret and selection-carets).
+pref("layout.accessiblecaret.enabled", true);
+
+// AccessibleCaret CSS for the Android L style assets.
+pref("layout.accessiblecaret.width", "22.0");
+pref("layout.accessiblecaret.height", "22.0");
+pref("layout.accessiblecaret.margin-left", "-11.5");
+
+// Android needs to show the caret when long tapping on an empty content.
+pref("layout.accessiblecaret.caret_shown_when_long_tapping_on_empty_content", true);
+
+// Androids carets are always tilt to match the text selection guideline.
+pref("layout.accessiblecaret.always_tilt", true);
+
+// Selection change notifications generated by Javascript changes
+// update active AccessibleCarets / UI interactions.
+pref("layout.accessiblecaret.allow_script_change_updates", true);
+
+// Optionally provide haptic feedback on longPress selection events.
+pref("layout.accessiblecaret.hapticfeedback", true);
+
+// Initial text selection on long-press is enhanced to provide
+// a smarter phone-number selection for direct-dial ActionBar action.
+pref("layout.accessiblecaret.extend_selection_for_phone_number", true);
+
+// Disable sending console to logcat on release builds.
+#ifdef RELEASE_OR_BETA
+pref("consoleservice.logcat", false);
+#else
+pref("consoleservice.logcat", true);
+#endif
+
+#ifndef RELEASE_OR_BETA
+// Enable VR on mobile, making it enable by default.
+pref("dom.vr.enabled", true);
+#endif
+
+pref("browser.tabs.showAudioPlayingIcon", true);
+
+pref("dom.serviceWorkers.enabled", false);
+pref("dom.serviceWorkers.interception.enabled", false);
+pref("dom.serviceWorkers.openWindow.enabled", false);
+
+pref("dom.push.debug", false);
+// The upstream autopush endpoint must have the Google API key corresponding to
+// the App's sender ID; we bake this assumption directly into the URL.
+pref("dom.push.serverURL", "https://updates.push.services.mozilla.com/v1/gcm/@MOZ_ANDROID_GCM_SENDERID@");
+pref("dom.push.maxRecentMessageIDsPerSubscription", 0);
+
+#ifdef MOZ_ANDROID_GCM
+pref("dom.push.enabled", false);
+#endif
+
+// The remote content URL where FxAccountsWebChannel messages originate. Must use HTTPS.
+pref("identity.fxaccounts.remote.webchannel.uri", "https://accounts.firefox.com");
+
+// The remote URL of the Firefox Account profile server.
+pref("identity.fxaccounts.remote.profile.uri", "https://profile.accounts.firefox.com/v1");
+
+// The remote URL of the Firefox Account oauth server.
+pref("identity.fxaccounts.remote.oauth.uri", "https://oauth.accounts.firefox.com/v1");
+
+// Token server used by Firefox Account-authenticated Sync.
+pref("identity.sync.tokenserver.uri", "https://token.services.mozilla.com/1.0/sync/1.5");
+
+// Enable Presentation API
+pref("dom.presentation.enabled", false);
+pref("dom.presentation.discovery.enabled", true);
+pref("dom.presentation.discovery.legacy.enabled", true); // for TV 2.5 backward capability
+
+pref("dom.audiochannel.audioCompeting", true);
+pref("dom.audiochannel.mediaControl", true);
+
+// Space separated list of URLS that are allowed to send objects (instead of
+// only strings) through webchannels. This list is duplicated in browser/app/profile/firefox.js
+pref("webchannel.allowObject.urlWhitelist", "https://accounts.firefox.com https://content.cdn.mozilla.net https://input.mozilla.org https://support.mozilla.org https://install.mozilla.org");
+
+pref("media.openUnsupportedTypeWithExternalApp", true);
diff --git a/mobile/android/app/moz.build b/mobile/android/app/moz.build
new file mode 100644
index 0000000000..cfa46fe096
--- /dev/null
+++ b/mobile/android/app/moz.build
@@ -0,0 +1,30 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+for var in ('APP_NAME', 'APP_VERSION'):
+ DEFINES[var] = CONFIG['MOZ_%s' % var]
+
+for var in ('MOZ_UPDATER', 'MOZ_APP_UA_NAME', 'ANDROID_PACKAGE_NAME'):
+ DEFINES[var] = CONFIG[var]
+
+for var in ('MOZ_ANDROID_GCM', ):
+ if CONFIG[var]:
+ DEFINES[var] = 1
+
+for var in ('MOZ_ANDROID_GCM_SENDERID', ):
+ if CONFIG[var]:
+ DEFINES[var] = CONFIG[var]
+
+if CONFIG['MOZ_PKG_SPECIAL']:
+ DEFINES['MOZ_PKG_SPECIAL'] = CONFIG['MOZ_PKG_SPECIAL']
+
+JS_PREFERENCE_PP_FILES += [
+ 'mobile.js',
+]
+
+FINAL_TARGET_PP_FILES += [
+ 'ua-update.json.in',
+]
diff --git a/mobile/android/app/omnijar/build.gradle b/mobile/android/app/omnijar/build.gradle
new file mode 100644
index 0000000000..c5274a618a
--- /dev/null
+++ b/mobile/android/app/omnijar/build.gradle
@@ -0,0 +1,33 @@
+buildDir "${topobjdir}/gradle/build/mobile/android/omnijar"
+
+apply plugin: 'java'
+
+// This project is a dummy project; the JAR produced is not used. The :app
+// project uses the set of inputs here to check if the omnijar needs to be
+// rebuilt. By listing them here as resource directories, IntelliJ labels each
+// checked directly nicely. Why list the directories here? There's a mismatch
+// between SourceDirectorySet and TaskInputs: the former is directory oriented,
+// while the latter is more general. That means its easy to convert this list
+// into inputs for :app, but not vice-versa. Sadly this implies that :app
+// evaluation depends on :omnijar, but the evaluation overhead is low enough
+// that we accept it.
+sourceSets {
+ main {
+ // Depend on the Gecko resources in mobile/android.
+ resources {
+ srcDir "${topsrcdir}/mobile/android/chrome"
+ srcDir "${topsrcdir}/mobile/android/components"
+ srcDir "${topsrcdir}/mobile/android/locales"
+ srcDir "${topsrcdir}/mobile/android/modules"
+ srcDir "${topsrcdir}/mobile/android/themes"
+ srcDir "${topsrcdir}/toolkit"
+ }
+ }
+}
+
+apply plugin: 'idea'
+
+idea {
+ module {
+ }
+}
diff --git a/mobile/android/app/src/androidTest/AndroidManifest.xml b/mobile/android/app/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000000..9478f5b079
--- /dev/null
+++ b/mobile/android/app/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.mozilla.roboexample.test"
+ android:sharedUserId="${MOZ_ANDROID_SHARED_ID}"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-sdk android:minSdkVersion="${MOZ_ANDROID_MIN_SDK_VERSION}"
+ android:targetSdkVersion="23"/>
+ <!-- TODO: re-instate maxSdkVersion. -->
+
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+
+ <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
+
+ <instrumentation
+ android:name="org.mozilla.gecko.FennecInstrumentationTestRunner"
+ android:targetPackage="${ANDROID_PACKAGE_NAME}" />
+
+ <application
+ android:label="@string/app_name">
+
+ <uses-library android:name="android.test.runner" />
+
+ <!-- Fake handlers to ensure that we have some share intents to show in our share handler list -->
+ <activity android:name="org.mozilla.gecko.RobocopShare1"
+ android:label="Robocop fake activity">
+
+ <intent-filter android:label="Fake robocop share handler 1">
+ <action android:name="android.intent.action.SEND" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="text/*" />
+ <data android:mimeType="image/*" />
+ </intent-filter>
+
+ </activity>
+
+ <activity android:name="org.mozilla.gecko.RobocopShare2"
+ android:label="Robocop fake activity 2">
+
+ <intent-filter android:label="Fake robocop share handler 2">
+ <action android:name="android.intent.action.SEND" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="text/*" />
+ <data android:mimeType="image/*" />
+ </intent-filter>
+
+ </activity>
+
+ <activity android:name="org.mozilla.gecko.LaunchFennecWithConfigurationActivity"
+ android:label="Robocop Fennec">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ </application>
+
+</manifest>
diff --git a/mobile/android/app/src/test/java/org/mozilla/gecko/TestGeckoApplication.java b/mobile/android/app/src/test/java/org/mozilla/gecko/TestGeckoApplication.java
new file mode 100644
index 0000000000..fee9a426d1
--- /dev/null
+++ b/mobile/android/app/src/test/java/org/mozilla/gecko/TestGeckoApplication.java
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko;
+
+import android.app.Application;
+
+import org.robolectric.TestLifecycleApplication;
+
+import java.lang.reflect.Method;
+
+/**
+ * GeckoApplication isn't test-lifecycle friendly: onCreate is called multiple times, which
+ * re-registers Gecko event listeners, which fails. This class is magically named so that
+ * Robolectric uses it instead of the application defined in the Android manifest. See
+ * http://robolectric.blogspot.ca/2013/04/the-test-lifecycle-in-20.html.
+ */
+public class TestGeckoApplication extends Application implements TestLifecycleApplication {
+ @Override public void beforeTest(Method method) {
+ }
+
+ @Override public void prepareTest(Object test) {
+ }
+
+ @Override public void afterTest(Method method) {
+ }
+}
diff --git a/mobile/android/app/ua-update.json.in b/mobile/android/app/ua-update.json.in
new file mode 100644
index 0000000000..97f1b99a4d
--- /dev/null
+++ b/mobile/android/app/ua-update.json.in
@@ -0,0 +1,15 @@
+#filter slashslash
+// Everything after the first // on a line will be removed by the preproccesor.
+// Send these sites a custom user-agent. Bugs should be included with an entry.
+// NOTE: trailing commas are not valid JSON and will prevent the CDN from syncing.
+{
+ "weather.yahoo.co.jp": "Mozilla/5.0 (Linux; Android 5.0.2; Galaxy Nexus Build/IMM76B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Mobile Safari/537.36",
+ // bug 1177298, lohaco.jp
+ "lohaco.jp": "Mozilla/5.0 (Linux; Android 5.0.2; Galaxy Nexus Build/IMM76B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Mobile Safari/537.36",
+ // bug 1177298, www.nhk.or.jp
+ "nhk.or.jp": "\\)\\s# AppleWebKit ",
+ // bug 1177298, uniqlo.com
+ "uniqlo.com": "\\)\\s#) Mobile Safari ",
+ // bug 1338260, directv.com
+ "directv.com": "Mozilla/5.0 (Linux; Android 6.0.1; SM-G920F Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36"
+}
diff --git a/mobile/android/base/AdjustConstants.java.in b/mobile/android/base/AdjustConstants.java.in
new file mode 100644
index 0000000000..6388cbbf91
--- /dev/null
+++ b/mobile/android/base/AdjustConstants.java.in
@@ -0,0 +1,32 @@
+//#filter substitution
+//#include @OBJDIR@/adjust_sdk_app_token
+/* -*- 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.adjust.AdjustHelperInterface;
+//#ifdef MOZ_INSTALL_TRACKING
+import org.mozilla.gecko.adjust.AdjustHelper;
+//#else
+import org.mozilla.gecko.adjust.StubAdjustHelper;
+//#endif
+
+public class AdjustConstants {
+ public static final String MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN =
+//#ifdef MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN
+ "@MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN@";
+//#else
+ null;
+//#endif
+
+ public static AdjustHelperInterface getAdjustHelper() {
+//#ifdef MOZ_INSTALL_TRACKING
+ return new AdjustHelper();
+//#else
+ return new StubAdjustHelper();
+//#endif
+ }
+}
diff --git a/mobile/android/base/AndroidManifest.xml.in b/mobile/android/base/AndroidManifest.xml.in
new file mode 100644
index 0000000000..2ec98c35ab
--- /dev/null
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -0,0 +1,447 @@
+#filter substitution
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="@ANDROID_PACKAGE_NAME@"
+ android:installLocation="auto"
+ android:versionCode="@ANDROID_VERSION_CODE@"
+ android:versionName="@MOZ_APP_VERSION@"
+#ifdef MOZ_ANDROID_SHARED_ID
+ android:sharedUserId="@MOZ_ANDROID_SHARED_ID@"
+#endif
+ >
+ <uses-sdk android:minSdkVersion="@MOZ_ANDROID_MIN_SDK_VERSION@"
+#ifdef MOZ_ANDROID_MAX_SDK_VERSION
+ android:maxSdkVersion="@MOZ_ANDROID_MAX_SDK_VERSION@"
+#endif
+ android:targetSdkVersion="23"/>
+
+<!-- The bouncer APK and the main APK should define the same set of
+ <permission>, <uses-permission>, and <uses-feature> elements. This reduces
+ the likelihood of permission-related surprises when installing the main APK
+ on top of a pre-installed bouncer APK. Add such shared elements in the
+ fileincluded here, so that they can be referenced by both APKs. -->
+#include FennecManifest_permissions.xml.in
+
+ <application android:label="@string/moz_app_displayname"
+ android:icon="@drawable/icon"
+ android:logo="@drawable/logo"
+ android:name="@MOZ_ANDROID_APPLICATION_CLASS@"
+ android:hardwareAccelerated="true"
+ android:allowBackup="false"
+# The preprocessor does not yet support arbitrary parentheses, so this cannot
+# be parenthesized thus to clarify that the logical AND operator has precedence:
+# !defined(MOZILLA_OFFICIAL) || (defined(NIGHTLY_BUILD) && defined(MOZ_DEBUG))
+#if !defined(MOZILLA_OFFICIAL) || defined(NIGHTLY_BUILD) && defined(MOZ_DEBUG)
+ android:debuggable="true">
+#else
+ android:debuggable="false">
+#endif
+
+ <meta-data android:name="com.sec.android.support.multiwindow" android:value="true"/>
+
+#ifdef MOZ_NATIVE_DEVICES
+ <!-- This resources comes from Google Play Services. Required for casting support. -->
+ <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />
+ <service android:name="org.mozilla.gecko.RemotePresentationService" android:exported="false"/>
+
+#endif
+
+ <!-- This activity handles all incoming Intents and dispatches them to other activities. -->
+ <activity android:name="org.mozilla.gecko.LauncherActivity"
+ android:theme="@android:style/Theme.Translucent.NoTitleBar"
+ android:relinquishTaskIdentity="true"
+ android:taskAffinity=""
+ android:excludeFromRecents="true" />
+
+ <!-- Fennec is shipped as the Android package named
+ org.mozilla.{fennec,firefox,firefox_beta}. The internal Java
+ package hierarchy inside the Android package used to have an
+ org.mozilla.{fennec,firefox,firefox_beta} subtree *and* an
+ org.mozilla.gecko subtree; it now only has org.mozilla.gecko. -->
+ <activity android:name="@MOZ_ANDROID_BROWSER_INTENT_CLASS@"
+ android:label="@string/moz_app_displayname"
+ android:taskAffinity="@ANDROID_PACKAGE_NAME@.BROWSER"
+ android:alwaysRetainTaskState="true"
+ android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|locale|layoutDirection|smallestScreenSize|screenLayout"
+ android:windowSoftInputMode="stateUnspecified|adjustResize"
+ android:launchMode="singleTask"
+ android:exported="true"
+ android:theme="@style/Gecko.App" />
+
+ <!-- Bug 1256615 / Bug 1268455: We published an .App alias and we need to maintain it
+ forever. If we don't, home screen shortcuts will disappear because the intent
+ filter details change. -->
+ <activity-alias android:name=".App"
+ android:label="@MOZ_APP_DISPLAYNAME@"
+ android:targetActivity="org.mozilla.gecko.LauncherActivity">
+
+ <!-- android:priority ranges between -1000 and 1000. We never want
+ another activity to usurp the MAIN action, so we ratchet our
+ priority up. -->
+ <intent-filter android:priority="999">
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER"/>
+ <category android:name="android.intent.category.APP_BROWSER" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="http" />
+ <data android:scheme="https" />
+ <data android:scheme="about" />
+ <data android:scheme="javascript" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:scheme="file" />
+ <data android:scheme="http" />
+ <data android:scheme="https" />
+ <data android:mimeType="text/html"/>
+ <data android:mimeType="text/plain"/>
+ <data android:mimeType="application/xhtml+xml"/>
+ </intent-filter>
+
+ <meta-data android:name="com.sec.minimode.icon.portrait.normal"
+ android:resource="@drawable/icon"/>
+
+ <meta-data android:name="com.sec.minimode.icon.landscape.normal"
+ android:resource="@drawable/icon" />
+
+ <intent-filter>
+ <action android:name="org.mozilla.gecko.ACTION_ALERT_CALLBACK" />
+ </intent-filter>
+
+ <intent-filter>
+ <action android:name="org.mozilla.gecko.GUEST_SESSION_INPROGRESS" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+
+ <intent-filter>
+ <action android:name="org.mozilla.gecko.UPDATE"/>
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+
+ <intent-filter>
+ <action android:name="android.intent.action.WEB_SEARCH" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="" />
+ <data android:scheme="http" />
+ <data android:scheme="https" />
+ </intent-filter>
+
+ <!-- For XPI installs from websites and the download manager. -->
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:scheme="file" />
+ <data android:scheme="http" />
+ <data android:scheme="https" />
+ <data android:mimeType="application/x-xpinstall" />
+ </intent-filter>
+
+ <!-- For XPI installs from file: URLs. -->
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:host="" />
+ <data android:scheme="file" />
+ <data android:pathPattern=".*\\.xpi" />
+ </intent-filter>
+
+#ifdef MOZ_ANDROID_BEAM
+ <intent-filter>
+ <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:scheme="http" />
+ <data android:scheme="https" />
+ </intent-filter>
+#endif
+
+ <!-- For debugging -->
+ <intent-filter>
+ <action android:name="org.mozilla.gecko.DEBUG" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity-alias>
+
+ <service android:name="org.mozilla.gecko.GeckoService" />
+
+ <activity android:name="org.mozilla.gecko.trackingprotection.TrackingProtectionPrompt"
+ android:launchMode="singleTop"
+ android:theme="@style/OverlayActivity" />
+
+ <activity android:name="org.mozilla.gecko.promotion.SimpleHelperUI"
+ android:launchMode="singleTop"
+ android:theme="@style/OverlayActivity" />
+
+ <activity android:name="org.mozilla.gecko.promotion.HomeScreenPrompt"
+ android:launchMode="singleTop"
+ android:theme="@style/OverlayActivity" />
+
+ <!-- The main reason for the Tab Queue build flag is to not mess with the VIEW intent filter
+ before the rest of the plumbing is in place -->
+
+ <service android:name="org.mozilla.gecko.tabqueue.TabQueueService" />
+
+ <activity android:name="org.mozilla.gecko.tabqueue.TabQueuePrompt"
+ android:launchMode="singleTop"
+ android:theme="@style/OverlayActivity" />
+
+ <receiver android:name="org.mozilla.gecko.restrictions.RestrictionProvider">
+ <intent-filter>
+ <action android:name="android.intent.action.GET_RESTRICTION_ENTRIES" />
+ </intent-filter>
+ </receiver>
+
+ <!-- Masquerade as the Resolver so that we can be opened from the Marketplace. -->
+ <activity-alias
+ android:name="com.android.internal.app.ResolverActivity"
+ android:targetActivity="@MOZ_ANDROID_BROWSER_INTENT_CLASS@"
+ android:exported="true" />
+
+ <receiver android:name="org.mozilla.gecko.GeckoUpdateReceiver">
+ <intent-filter>
+ <action android:name="@ANDROID_PACKAGE_NAME@.CHECK_UPDATE_RESULT" />
+ </intent-filter>
+ </receiver>
+
+ <receiver android:name="org.mozilla.gecko.GeckoMessageReceiver"
+ android:exported="false">
+ </receiver>
+
+ <!-- Catch install referrer so we can do post-install work. -->
+ <receiver android:name="org.mozilla.gecko.distribution.ReferrerReceiver"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="com.android.vending.INSTALL_REFERRER" />
+ </intent-filter>
+ </receiver>
+
+ <service android:name="org.mozilla.gecko.Restarter"
+ android:exported="false"
+ android:process="@MANGLED_ANDROID_PACKAGE_NAME@.Restarter">
+ </service>
+
+ <service android:name="org.mozilla.gecko.media.MediaControlService"
+ android:exported="false">
+ </service>
+
+ <receiver android:name="org.mozilla.gecko.AlarmReceiver" >
+ </receiver>
+
+ <receiver
+ android:name="org.mozilla.gecko.notifications.WhatsNewReceiver"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="android.intent.action.PACKAGE_REPLACED" />
+ <data android:scheme="package" android:path="org.mozilla.gecko" />
+ </intent-filter>
+ </receiver>
+
+ <receiver
+ android:name="org.mozilla.gecko.notifications.NotificationReceiver"
+ android:exported="false">
+ <!-- Notification API V2 -->
+ <intent-filter>
+ <action android:name="@ANDROID_PACKAGE_NAME@.helperBroadcastAction" />
+ <action android:name="@ANDROID_PACKAGE_NAME@.NOTIFICATION_CLICK" />
+ <action android:name="@ANDROID_PACKAGE_NAME@.NOTIFICATION_CLOSE" />
+ <data android:scheme="moz-notification" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </receiver>
+
+#include ../services/manifests/FxAccountAndroidManifest_activities.xml.in
+#ifdef MOZ_ANDROID_SEARCH_ACTIVITY
+#include ../search/manifests/SearchAndroidManifest_activities.xml.in
+#endif
+
+#if MOZ_CRASHREPORTER
+ <activity android:name="org.mozilla.gecko.CrashReporter"
+ android:process="@ANDROID_PACKAGE_NAME@.CrashReporter"
+ android:label="@string/crash_reporter_title"
+ android:icon="@drawable/crash_reporter"
+ android:theme="@style/Gecko"
+ android:exported="false"
+ android:excludeFromRecents="true">
+ <intent-filter>
+ <action android:name="org.mozilla.gecko.reportCrash" />
+ </intent-filter>
+ </activity>
+#endif
+
+ <activity android:name="org.mozilla.gecko.preferences.GeckoPreferences"
+ android:theme="@style/Gecko.Preferences"
+ android:configChanges="orientation|screenSize|locale|layoutDirection"
+ android:excludeFromRecents="true"/>
+
+ <provider android:name="org.mozilla.gecko.db.BrowserProvider"
+ android:authorities="@ANDROID_PACKAGE_NAME@.db.browser"
+ android:exported="false"/>
+
+ <provider android:name="org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy"
+ android:authorities="@ANDROID_PACKAGE_NAME@.partnerbookmarks"
+ android:exported="false"/>
+
+ <!-- Share overlay activity
+
+ Setting launchMode="singleTop" ensures onNewIntent is called when the Activity is
+ reused. Ideally we create a new instance but Android L breaks this (bug 1137928). -->
+ <activity android:name="org.mozilla.gecko.overlays.ui.ShareDialog"
+ android:label="@string/overlay_share_label"
+ android:theme="@style/OverlayActivity"
+ android:configChanges="keyboard|keyboardHidden|mcc|mnc|locale|layoutDirection"
+ android:launchMode="singleTop"
+ android:windowSoftInputMode="stateAlwaysHidden|adjustResize">
+
+ <intent-filter>
+ <action android:name="android.intent.action.SEND" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="text/plain" />
+ </intent-filter>
+
+ </activity>
+
+#ifdef MOZ_ANDROID_CUSTOM_TABS
+ <activity android:name="org.mozilla.gecko.customtabs.CustomTabsActivity"
+ android:theme="@style/Theme.AppCompat.NoActionBar" />
+#endif
+
+ <!-- Service to handle requests from overlays. -->
+ <service android:name="org.mozilla.gecko.overlays.service.OverlayActionService" />
+
+ <!--
+ Ensure that passwords provider runs in its own process. (Bug 718760.)
+ Process name is per-application to avoid loading CPs from multiple
+ Fennec versions into the same process. (Bug 749727.)
+ Process name is a mangled version to avoid a Talos bug. (Bug 750548.)
+ -->
+ <provider android:name="org.mozilla.gecko.db.PasswordsProvider"
+ android:label="@string/sync_configure_engines_title_passwords"
+ android:authorities="@ANDROID_PACKAGE_NAME@.db.passwords"
+ android:exported="false"
+ android:process="@MANGLED_ANDROID_PACKAGE_NAME@.PasswordsProvider"/>
+
+ <provider android:name="org.mozilla.gecko.db.LoginsProvider"
+ android:label="@string/sync_configure_engines_title_passwords"
+ android:authorities="@ANDROID_PACKAGE_NAME@.db.logins"
+ android:exported="false"/>
+
+ <provider android:name="org.mozilla.gecko.db.FormHistoryProvider"
+ android:label="@string/sync_configure_engines_title_history"
+ android:authorities="@ANDROID_PACKAGE_NAME@.db.formhistory"
+ android:exported="false"/>
+
+ <provider android:name="org.mozilla.gecko.GeckoProfilesProvider"
+ android:authorities="@ANDROID_PACKAGE_NAME@.profiles"
+ android:exported="false"/>
+
+ <provider android:name="org.mozilla.gecko.db.TabsProvider"
+ android:label="@string/sync_configure_engines_title_tabs"
+ android:authorities="@ANDROID_PACKAGE_NAME@.db.tabs"
+ android:exported="false"/>
+
+ <provider android:name="org.mozilla.gecko.db.HomeProvider"
+ android:authorities="@ANDROID_PACKAGE_NAME@.db.home"
+ android:exported="false"/>
+
+ <provider android:name="org.mozilla.gecko.db.SearchHistoryProvider"
+ android:authorities="@ANDROID_PACKAGE_NAME@.db.searchhistory"
+ android:exported="false"/>
+
+ <service
+ android:exported="false"
+ android:name="org.mozilla.gecko.updater.UpdateService"
+ android:process="@MANGLED_ANDROID_PACKAGE_NAME@.UpdateService">
+ </service>
+
+ <service
+ android:exported="false"
+ android:name="org.mozilla.gecko.notifications.NotificationService">
+ </service>
+
+ <service
+ android:exported="false"
+ android:name="org.mozilla.gecko.dlc.DownloadContentService">
+ </service>
+
+ <service
+ android:exported="false"
+ android:name="org.mozilla.gecko.feeds.FeedService">
+ </service>
+
+ <!-- DON'T EXPORT THIS, please! An attacker could delete arbitrary files. -->
+ <service
+ android:exported="false"
+ android:name="org.mozilla.gecko.cleanup.FileCleanupService">
+ </service>
+
+ <receiver
+ android:name="org.mozilla.gecko.feeds.FeedAlarmReceiver"
+ android:exported="false" />
+
+ <receiver
+ android:name="org.mozilla.gecko.BootReceiver"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="android.intent.action.BOOT_COMPLETED"></action>
+ </intent-filter>
+ </receiver>
+
+ <receiver
+ android:name="org.mozilla.gecko.PackageReplacedReceiver"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="android.intent.action.MY_PACKAGE_REPLACED"></action>
+ </intent-filter>
+ </receiver>
+
+ <service
+ android:name="org.mozilla.gecko.telemetry.TelemetryUploadService"
+ android:exported="false"/>
+
+#ifdef MOZ_ANDROID_CUSTOM_TABS
+ <service
+ android:name="org.mozilla.gecko.customtabs.GeckoCustomTabsService"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.support.customtabs.action.CustomTabsService" />
+ </intent-filter>
+ </service>
+#endif
+
+#include ../services/manifests/FxAccountAndroidManifest_services.xml.in
+
+ <service
+ android:name="org.mozilla.gecko.tabqueue.TabReceivedService"
+ android:exported="false" />
+
+
+#ifdef MOZ_ANDROID_SEARCH_ACTIVITY
+#include ../search/manifests/SearchAndroidManifest_services.xml.in
+#endif
+#ifdef MOZ_ANDROID_MLS_STUMBLER
+#include ../stumbler/manifests/StumblerManifest_services.xml.in
+#endif
+
+#ifdef MOZ_ANDROID_GCM
+#include GcmAndroidManifest_services.xml.in
+#endif
+
+ <service
+ android:name="org.mozilla.gecko.media.MediaManager"
+ android:enabled="true"
+ android:exported="false"
+ android:process=":media"
+ android:isolatedProcess="false">
+ </service>
+
+ </application>
+</manifest>
diff --git a/mobile/android/base/AppConstants.java.in b/mobile/android/base/AppConstants.java.in
new file mode 100644
index 0000000000..25a6a456ef
--- /dev/null
+++ b/mobile/android/base/AppConstants.java.in
@@ -0,0 +1,349 @@
+//#filter substitution
+/* -*- 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 android.os.Build;
+//#ifdef MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE
+import android.support.multidex.MultiDex;
+//#endif
+
+/**
+ * A collection of constants that pertain to the build and runtime state of the
+ * application. Typically these are sourced from build-time definitions (see
+ * Makefile.in). This is a Java-side substitute for nsIXULAppInfo, amongst
+ * other things.
+ *
+ * See also SysInfo.java, which includes some of the values available from
+ * nsSystemInfo inside Gecko.
+ */
+// Normally, we'd annotate with @RobocopTarget. Since AppConstants is compiled
+// before RobocopTarget, we instead add o.m.g.AppConstants directly to the
+// Proguard configuration.
+public class AppConstants {
+ public static final String ANDROID_PACKAGE_NAME = "@ANDROID_PACKAGE_NAME@";
+ public static final String MANGLED_ANDROID_PACKAGE_NAME = "@MANGLED_ANDROID_PACKAGE_NAME@";
+
+ public static final String MOZ_ANDROID_SHARED_FXACCOUNT_TYPE = "@ANDROID_PACKAGE_NAME@_fxaccount";
+
+ /**
+ * Encapsulates access to compile-time version definitions, allowing
+ * for dead code removal for particular APKs.
+ */
+ public static final class Versions {
+ public static final int MIN_SDK_VERSION = @MOZ_ANDROID_MIN_SDK_VERSION@;
+ public static final int MAX_SDK_VERSION =
+//#ifdef MOZ_ANDROID_MAX_SDK_VERSION
+ @MOZ_ANDROID_MAX_SDK_VERSION@;
+//#else
+ 999;
+//#endif
+
+ /*
+ * The SDK_INT >= N check can only pass if our MAX_SDK_VERSION is
+ * _greater than or equal_ to that number, because otherwise we
+ * won't be installed on the device.
+ *
+ * If MIN_SDK_VERSION is greater than or equal to the number, there
+ * is no need to do the runtime check.
+ */
+ public static final boolean feature16Plus = MIN_SDK_VERSION >= 16 || (MAX_SDK_VERSION >= 16 && Build.VERSION.SDK_INT >= 16);
+ public static final boolean feature17Plus = MIN_SDK_VERSION >= 17 || (MAX_SDK_VERSION >= 17 && Build.VERSION.SDK_INT >= 17);
+ public static final boolean feature19Plus = MIN_SDK_VERSION >= 19 || (MAX_SDK_VERSION >= 19 && Build.VERSION.SDK_INT >= 19);
+ public static final boolean feature20Plus = MIN_SDK_VERSION >= 20 || (MAX_SDK_VERSION >= 20 && Build.VERSION.SDK_INT >= 20);
+ public static final boolean feature21Plus = MIN_SDK_VERSION >= 21 || (MAX_SDK_VERSION >= 21 && Build.VERSION.SDK_INT >= 21);
+
+ /*
+ * If our MIN_SDK_VERSION is 14 or higher, we must be an ICS device.
+ * If our MAX_SDK_VERSION is lower than ICS, we must not be an ICS device.
+ * Otherwise, we need a range check.
+ */
+ public static final boolean preMarshmallow = MAX_SDK_VERSION < 23 || (MIN_SDK_VERSION < 23 && Build.VERSION.SDK_INT < 23);
+ public static final boolean preLollipop = MAX_SDK_VERSION < 21 || (MIN_SDK_VERSION < 21 && Build.VERSION.SDK_INT < 21);
+ public static final boolean preJBMR2 = MAX_SDK_VERSION < 18 || (MIN_SDK_VERSION < 18 && Build.VERSION.SDK_INT < 18);
+ public static final boolean preJBMR1 = MAX_SDK_VERSION < 17 || (MIN_SDK_VERSION < 17 && Build.VERSION.SDK_INT < 17);
+ public static final boolean preJB = MAX_SDK_VERSION < 16 || (MIN_SDK_VERSION < 16 && Build.VERSION.SDK_INT < 16);
+ public static final boolean preN = MAX_SDK_VERSION < 24 || (MIN_SDK_VERSION < 24 && Build.VERSION.SDK_INT < 24);
+ }
+
+ /**
+ * The name of the Java class that represents the android application.
+ */
+ public static final String MOZ_ANDROID_APPLICATION_CLASS = "@MOZ_ANDROID_APPLICATION_CLASS@";
+ /**
+ * The name of the Java class that launches the browser activity.
+ */
+ public static final String MOZ_ANDROID_BROWSER_INTENT_CLASS = "@MOZ_ANDROID_BROWSER_INTENT_CLASS@";
+ /**
+ * The name of the Java class that launches the search activity.
+ */
+ public static final String MOZ_ANDROID_SEARCH_INTENT_CLASS = "@MOZ_ANDROID_SEARCH_INTENT_CLASS@";
+
+ public static final String GRE_MILESTONE = "@GRE_MILESTONE@";
+
+ public static final String MOZ_APP_ABI = "@MOZ_APP_ABI@";
+ public static final String MOZ_APP_BASENAME = "@MOZ_APP_BASENAME@";
+
+ // For the benefit of future archaeologists:
+ // GRE_BUILDID is exactly the same as MOZ_APP_BUILDID unless you're running
+ // on XULRunner, which is never the case on Android.
+ public static final String MOZ_APP_BUILDID = "@MOZ_BUILDID@";
+ public static final String MOZ_APP_ID = "@MOZ_APP_ID@";
+ public static final String MOZ_APP_NAME = "@MOZ_APP_NAME@";
+ public static final String MOZ_APP_VENDOR = "@MOZ_APP_VENDOR@";
+ public static final String MOZ_APP_VERSION = "@MOZ_APP_VERSION@";
+ public static final String MOZ_APP_DISPLAYNAME = "@MOZ_APP_DISPLAYNAME@";
+ // MOZ_APP_UA_NAME is already quoted when it gets substituted, like MOZILLA_VERSION.
+ public static final String MOZ_APP_UA_NAME = @MOZ_APP_UA_NAME@;
+
+ // MOZILLA_VERSION is already quoted when it gets substituted in. If we
+ // add additional quotes we end up with ""x.y"", which is a syntax error.
+ public static final String MOZILLA_VERSION = @MOZILLA_VERSION@;
+
+ public static final String MOZ_MOZILLA_API_KEY = "@MOZ_MOZILLA_API_KEY@";
+ public static final boolean MOZ_STUMBLER_BUILD_TIME_ENABLED =
+//#ifdef MOZ_ANDROID_MLS_STUMBLER
+ true;
+//#else
+ false;
+//#endif
+
+ public static final boolean MOZ_ANDROID_GCM =
+//#ifdef MOZ_ANDROID_GCM
+ true;
+//#else
+ false;
+//#endif
+
+ public static final String MOZ_ANDROID_GCM_SENDERID =
+//#ifdef MOZ_ANDROID_GCM_SENDERID
+ "@MOZ_ANDROID_GCM_SENDERID@";
+//#else
+ null;
+//#endif
+
+ public static final String MOZ_CHILD_PROCESS_NAME = "@MOZ_CHILD_PROCESS_NAME@";
+ public static final String MOZ_UPDATE_CHANNEL = "@MOZ_UPDATE_CHANNEL@";
+ public static final String OMNIJAR_NAME = "@OMNIJAR_NAME@";
+ public static final String OS_TARGET = "@OS_TARGET@";
+ public static final String TARGET_XPCOM_ABI = @TARGET_XPCOM_ABI@;
+
+ public static final String USER_AGENT_BOT_LIKE = "Redirector/" + AppConstants.MOZ_APP_VERSION +
+ " (Android; rv:" + AppConstants.MOZ_APP_VERSION + ")";
+
+ public static final String USER_AGENT_FENNEC_MOBILE = "Mozilla/5.0 (Android " +
+ Build.VERSION.RELEASE + "; Mobile; rv:" +
+ AppConstants.MOZ_APP_VERSION + ") Gecko/" +
+ AppConstants.MOZ_APP_VERSION + " Firefox/" +
+ AppConstants.MOZ_APP_VERSION;
+
+ public static final String USER_AGENT_FENNEC_TABLET = "Mozilla/5.0 (Android " +
+ Build.VERSION.RELEASE + "; Tablet; rv:" +
+ AppConstants.MOZ_APP_VERSION + ") Gecko/" +
+ AppConstants.MOZ_APP_VERSION + " Firefox/" +
+ AppConstants.MOZ_APP_VERSION;
+
+ public static final int MOZ_MIN_CPU_VERSION = @MOZ_MIN_CPU_VERSION@;
+
+ public static final boolean MOZ_ANDROID_ANR_REPORTER =
+//#ifdef MOZ_ANDROID_ANR_REPORTER
+ true;
+//#else
+ false;
+//#endif
+
+ public static final String MOZ_PKG_SPECIAL =
+//#ifdef MOZ_PKG_SPECIAL
+ "@MOZ_PKG_SPECIAL@";
+//#else
+ null;
+//#endif
+
+ public static final boolean MOZ_EXCLUDE_HYPHENATION_DICTIONARIES =
+//#ifdef MOZ_EXCLUDE_HYPHENATION_DICTIONARIES
+ true;
+//#else
+ false;
+//#endif
+
+ public static final boolean MOZ_SERVICES_HEALTHREPORT =
+//#ifdef MOZ_SERVICES_HEALTHREPORT
+ true;
+//#else
+ false;
+//#endif
+
+ public static final boolean MOZ_TELEMETRY_ON_BY_DEFAULT =
+//#ifdef MOZ_TELEMETRY_ON_BY_DEFAULT
+ true;
+//#else
+ false;
+//#endif
+
+ public static final String TELEMETRY_PREF_NAME =
+ "toolkit.telemetry.enabled";
+
+ public static final boolean MOZ_TELEMETRY_REPORTING =
+//#ifdef MOZ_TELEMETRY_REPORTING
+ true;
+//#else
+ false;
+//#endif
+
+ public static final boolean MOZ_CRASHREPORTER =
+//#if MOZ_CRASHREPORTER
+ true;
+//#else
+ false;
+//#endif
+
+ public static final boolean MOZ_DATA_REPORTING =
+//#ifdef MOZ_DATA_REPORTING
+ true;
+//#else
+ false;
+//#endif
+
+ public static final boolean MOZ_LOCALE_SWITCHER =
+//#ifdef MOZ_LOCALE_SWITCHER
+ true;
+//#else
+ false;
+//#endif
+
+ public static final boolean MOZ_UPDATER =
+//#ifdef MOZ_UPDATER
+ true;
+//#else
+ false;
+//#endif
+
+ // Android Beam is only supported on API14+, so we don't even bother building
+ // it if this APK doesn't include API14 support.
+ public static final boolean MOZ_ANDROID_BEAM =
+//#ifdef MOZ_ANDROID_BEAM
+ true;
+//#else
+ false;
+//#endif
+
+ // See this wiki page for more details about channel specific build defines:
+ // https://wiki.mozilla.org/Platform/Channel-specific_build_defines
+ public static final boolean RELEASE_OR_BETA =
+//#ifdef RELEASE_OR_BETA
+ true;
+//#else
+ false;
+//#endif
+
+ public static final boolean NIGHTLY_BUILD =
+//#ifdef NIGHTLY_BUILD
+ true;
+//#else
+ false;
+//#endif
+
+ public static final boolean DEBUG_BUILD =
+//#ifdef MOZ_DEBUG
+ true;
+//#else
+ false;
+//#endif
+
+ public static final boolean MOZ_MEDIA_PLAYER =
+//#ifdef MOZ_NATIVE_DEVICES
+ true;
+//#else
+ false;
+//#endif
+
+ // Official corresponds, roughly, to whether this build is performed on
+ // Mozilla's continuous integration infrastructure. You should disable
+ // developer-only functionality when this flag is set.
+ public static final boolean MOZILLA_OFFICIAL =
+//#ifdef MOZILLA_OFFICIAL
+ true;
+//#else
+ false;
+//#endif
+
+ public static final boolean ANDROID_DOWNLOADS_INTEGRATION =
+//#ifdef MOZ_ANDROID_DOWNLOADS_INTEGRATION
+ true;
+//#else
+ false;
+//#endif
+
+ public static final boolean MOZ_DRAGGABLE_URLBAR = false;
+
+ public static final boolean MOZ_INSTALL_TRACKING =
+//#ifdef MOZ_INSTALL_TRACKING
+ true;
+//#else
+ false;
+//#endif
+
+ public static final boolean MOZ_SWITCHBOARD =
+//#ifdef MOZ_SWITCHBOARD
+ true;
+//#else
+ false;
+//#endif
+
+ /**
+ * Target CPU architecture: "armeabi-v7a", "x86, "mips", ..
+ */
+ public static final String ANDROID_CPU_ARCH = "@ANDROID_CPU_ARCH@";
+
+ public static final boolean MOZ_ANDROID_EXCLUDE_FONTS =
+//#ifdef MOZ_ANDROID_EXCLUDE_FONTS
+ true;
+//#else
+ false;
+//#endif
+
+ public static final boolean MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE =
+//#ifdef MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE
+ true;
+//#else
+ false;
+//#endif
+
+ public static final boolean MOZ_ANDROID_CUSTOM_TABS =
+//#ifdef MOZ_ANDROID_CUSTOM_TABS
+ true;
+//#else
+ false;
+//#endif
+
+ // (bug 1266820) Temporarily disabled since no one is working on it.
+ public static final boolean SCREENSHOTS_IN_BOOKMARKS_ENABLED = false;
+
+ /**
+ * Enables multidex depending on build flags. For more information,
+ * see `multiDexEnabled true` in mobile/android/app/build.gradle.
+ *
+ * As a method, this shouldn't be in AppConstants, but it's
+ * the only semi-relevant Java file that we pre-process.
+ */
+ public static void maybeInstallMultiDex(final Context context) {
+//#ifdef MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE
+ if (BuildConfig.FLAVOR.equals("automation")) {
+ MultiDex.install(context);
+ }
+//#else
+ // Do nothing.
+//#endif
+ }
+
+ public static final boolean MOZ_ANDROID_ACTIVITY_STREAM =
+//#ifdef MOZ_ANDROID_ACTIVITY_STREAM
+ true;
+//#else
+ false;
+//#endif
+}
diff --git a/mobile/android/base/FennecManifest_permissions.xml.in b/mobile/android/base/FennecManifest_permissions.xml.in
new file mode 100644
index 0000000000..cf3365582c
--- /dev/null
+++ b/mobile/android/base/FennecManifest_permissions.xml.in
@@ -0,0 +1,65 @@
+<!-- The bouncer APK and the Fennec APK should define the same set of
+ <permission>, <uses-permission>, and <uses-feature> elements. This reduces
+ the likelihood of permission-related surprises when installing the main APK
+ on top of a pre-installed bouncer APK. Add such elements here, so that
+ they can be easily shared between the two APKs. -->
+
+#include ../services/manifests/FxAccountAndroidManifest_permissions.xml.in
+
+#ifdef MOZ_ANDROID_SEARCH_ACTIVITY
+#include ../search/manifests/SearchAndroidManifest_permissions.xml.in
+#endif
+
+<!-- Bug 1261302: we have two new permissions to request,
+ RECEIVE_BOOT_COMPLETED and the permission for push. We want to ask for
+ them during the same release, which should be Fennec 48. Therefore we
+ decouple the push permission from MOZ_ANDROID_GCM to let it ride ahead
+ (potentially) of the push feature. -->
+#include GcmAndroidManifest_permissions.xml.in
+
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+ <!-- READ_EXTERNAL_STORAGE was added in API 16, and is only enforced in API
+ 19+. We declare it so that the bouncer APK and the main APK have the
+ same set of permissions. -->
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
+ <uses-permission android:name="com.android.launcher.permission.UNINSTALL_SHORTCUT"/>
+ <uses-permission android:name="com.android.browser.permission.READ_HISTORY_BOOKMARKS"/>
+
+ <uses-permission android:name="android.permission.WAKE_LOCK"/>
+ <uses-permission android:name="android.permission.VIBRATE"/>
+#ifdef MOZ_ANDROID_DOWNLOADS_INTEGRATION
+ <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
+#endif
+
+ <uses-feature android:name="android.hardware.location" android:required="false"/>
+ <uses-feature android:name="android.hardware.location.gps" android:required="false"/>
+ <uses-feature android:name="android.hardware.touchscreen"/>
+
+ <!-- Tab Queue -->
+ <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
+
+#ifdef MOZ_ANDROID_BEAM
+ <!-- Android Beam support -->
+ <uses-permission android:name="android.permission.NFC"/>
+ <uses-feature android:name="android.hardware.nfc" android:required="false"/>
+#endif
+
+#ifdef MOZ_WEBRTC
+ <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+ <uses-feature android:name="android.hardware.audio.low_latency" android:required="false"/>
+ <uses-feature android:name="android.hardware.camera.any" android:required="false"/>
+ <uses-feature android:name="android.hardware.microphone" android:required="false"/>
+#endif
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-feature android:name="android.hardware.camera" android:required="false"/>
+ <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
+
+ <!-- App requires OpenGL ES 2.0 -->
+ <uses-feature android:glEsVersion="0x00020000" android:required="true" />
diff --git a/mobile/android/base/GcmAndroidManifest_permissions.xml.in b/mobile/android/base/GcmAndroidManifest_permissions.xml.in
new file mode 100644
index 0000000000..b6fe5fffa0
--- /dev/null
+++ b/mobile/android/base/GcmAndroidManifest_permissions.xml.in
@@ -0,0 +1,4 @@
+ <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
+ <!-- Avoid a linter warning by not double-including WAKE_LOCK.
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
+ -->
diff --git a/mobile/android/base/GcmAndroidManifest_services.xml.in b/mobile/android/base/GcmAndroidManifest_services.xml.in
new file mode 100644
index 0000000000..de1c77b3be
--- /dev/null
+++ b/mobile/android/base/GcmAndroidManifest_services.xml.in
@@ -0,0 +1,29 @@
+ <!-- Handle GCM registration updates from on-device Google Play Services. -->
+ <service
+ android:name="org.mozilla.gecko.gcm.GcmInstanceIDListenerService"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="com.google.android.gms.iid.InstanceID"/>
+ </intent-filter>
+ </service>
+
+ <!-- Provided by on-device Google Play Services. Directs inbound messages to internal listener service. -->
+ <receiver
+ android:name="com.google.android.gms.gcm.GcmReceiver"
+ android:exported="true"
+ android:permission="com.google.android.c2dm.permission.SEND">
+ <intent-filter>
+ <action android:name="com.google.android.c2dm.intent.RECEIVE" />
+ <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
+ <category android:name="@ANDROID_PACKAGE_NAME@" />
+ </intent-filter>
+ </receiver>
+
+ <!-- Handle messages directed by the GCM receiver. -->
+ <service
+ android:name="org.mozilla.gecko.gcm.GcmMessageListenerService"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="com.google.android.c2dm.intent.RECEIVE" />
+ </intent-filter>
+ </service>
diff --git a/mobile/android/base/Makefile.in b/mobile/android/base/Makefile.in
new file mode 100644
index 0000000000..fe2f9d3ec2
--- /dev/null
+++ b/mobile/android/base/Makefile.in
@@ -0,0 +1,594 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# We call mach -> Make -> gradle -> mach, which races to find and
+# create .mozconfig files and to generate targets.
+ifdef MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE
+.NOTPARALLEL:
+endif
+
+MOZ_BUILDID := $(shell awk '{print $$3}' $(DEPTH)/buildid.h)
+
+# Set the appropriate version code, based on the existance of the
+# MOZ_APP_ANDROID_VERSION_CODE variable.
+ifdef MOZ_APP_ANDROID_VERSION_CODE
+ ANDROID_VERSION_CODE:=$(MOZ_APP_ANDROID_VERSION_CODE)
+else
+ ANDROID_VERSION_CODE:=$(shell $(PYTHON) \
+ $(topsrcdir)/python/mozbuild/mozbuild/android_version_code.py \
+ --verbose \
+ --with-android-cpu-arch=$(ANDROID_CPU_ARCH) \
+ $(if $(MOZ_ANDROID_MIN_SDK_VERSION),--with-android-min-sdk=$(MOZ_ANDROID_MIN_SDK_VERSION)) \
+ $(if $(MOZ_ANDROID_MAX_SDK_VERSION),--with-android-max-sdk=$(MOZ_ANDROID_MAX_SDK_VERSION)) \
+ $(MOZ_BUILDID))
+endif
+
+DEFINES += \
+ -DANDROID_VERSION_CODE=$(ANDROID_VERSION_CODE) \
+ -DMOZ_ANDROID_SHARED_ID="$(MOZ_ANDROID_SHARED_ID)" \
+ -DMOZ_BUILDID=$(MOZ_BUILDID) \
+ $(NULL)
+
+GARBAGE += \
+ classes.dex \
+ gecko.ap_ \
+ res/values/strings.xml \
+ res/raw/browsersearch.json \
+ res/raw/suggestedsites.json \
+ .aapt.deps \
+ GeneratedJNINatives.h \
+ GeneratedJNIWrappers.cpp \
+ GeneratedJNIWrappers.h \
+ FennecJNINatives.h \
+ FennecJNIWrappers.cpp \
+ FennecJNIWrappers.h \
+ $(NULL)
+
+GARBAGE_DIRS += classes db jars res sync services generated
+
+# The bootclasspath is functionally identical to the classpath, but allows the
+# classes given to redefine classes in core packages, such as java.lang.
+# android.jar is here as it provides Android's definition of the Java Standard
+# Library. The compatability lib here tweaks a few of the core classes to paint
+# over changes in behaviour between versions.
+JAVA_BOOTCLASSPATH := \
+ $(ANDROID_SDK)/android.jar \
+ $(NULL)
+
+JAVA_BOOTCLASSPATH := $(subst $(NULL) ,:,$(strip $(JAVA_BOOTCLASSPATH)))
+
+JAVA_CLASSPATH += \
+ $(ANDROID_SUPPORT_ANNOTATIONS_JAR_LIB) \
+ $(ANDROID_SUPPORT_V4_AAR_LIB) \
+ $(ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB) \
+ $(ANDROID_APPCOMPAT_V7_AAR_LIB) \
+ $(ANDROID_SUPPORT_VECTOR_DRAWABLE_AAR_LIB) \
+ $(ANDROID_ANIMATED_VECTOR_DRAWABLE_AAR_LIB) \
+ $(ANDROID_CARDVIEW_V7_AAR_LIB) \
+ $(ANDROID_DESIGN_AAR_LIB) \
+ $(ANDROID_RECYCLERVIEW_V7_AAR_LIB) \
+ $(ANDROID_CUSTOMTABS_AAR_LIB) \
+ $(ANDROID_PALETTE_V7_AAR_LIB) \
+ $(NULL)
+
+# If native devices are enabled, add Google Play Services and some of the v7
+# compat libraries.
+ifdef MOZ_NATIVE_DEVICES
+ JAVA_CLASSPATH += \
+ $(ANDROID_PLAY_SERVICES_BASE_AAR_LIB) \
+ $(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
+ $(ANDROID_PLAY_SERVICES_CAST_AAR_LIB) \
+ $(ANDROID_MEDIAROUTER_V7_AAR_LIB) \
+ $(ANDROID_MEDIAROUTER_V7_AAR_INTERNAL_LIB) \
+ $(NULL)
+endif
+
+ifdef MOZ_ANDROID_GCM
+ JAVA_CLASSPATH += \
+ $(ANDROID_PLAY_SERVICES_BASE_AAR_LIB) \
+ $(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
+ $(ANDROID_PLAY_SERVICES_GCM_AAR_LIB) \
+ $(ANDROID_PLAY_SERVICES_MEASUREMENT_AAR_LIB) \
+ $(NULL)
+endif
+
+ifdef MOZ_INSTALL_TRACKING
+ JAVA_CLASSPATH += \
+ $(ANDROID_PLAY_SERVICES_ADS_AAR_LIB) \
+ $(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
+ $(NULL)
+endif
+
+JAVA_CLASSPATH := $(subst $(NULL) ,:,$(strip $(JAVA_CLASSPATH)))
+
+# Library jars that we're bundling: these are subject to Proguard before inclusion
+# into classes.dex.
+java_bundled_libs := \
+ $(ANDROID_SUPPORT_V4_AAR_LIB) \
+ $(ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB) \
+ $(ANDROID_APPCOMPAT_V7_AAR_LIB) \
+ $(ANDROID_SUPPORT_VECTOR_DRAWABLE_AAR_LIB) \
+ $(ANDROID_ANIMATED_VECTOR_DRAWABLE_AAR_LIB) \
+ $(ANDROID_CARDVIEW_V7_AAR_LIB) \
+ $(ANDROID_DESIGN_AAR_LIB) \
+ $(ANDROID_RECYCLERVIEW_V7_AAR_LIB) \
+ $(ANDROID_CUSTOMTABS_AAR_LIB) \
+ $(ANDROID_PALETTE_V7_AAR_LIB) \
+ $(NULL)
+
+ifdef MOZ_NATIVE_DEVICES
+ java_bundled_libs += \
+ $(ANDROID_PLAY_SERVICES_BASE_AAR_LIB) \
+ $(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
+ $(ANDROID_PLAY_SERVICES_CAST_AAR_LIB) \
+ $(ANDROID_MEDIAROUTER_V7_AAR_LIB) \
+ $(ANDROID_MEDIAROUTER_V7_AAR_INTERNAL_LIB) \
+ $(NULL)
+endif
+
+ifdef MOZ_ANDROID_GCM
+ java_bundled_libs += \
+ $(ANDROID_PLAY_SERVICES_BASE_AAR_LIB) \
+ $(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
+ $(ANDROID_PLAY_SERVICES_GCM_AAR_LIB) \
+ $(ANDROID_PLAY_SERVICES_MEASUREMENT_AAR_LIB) \
+ $(NULL)
+endif
+
+ifdef MOZ_INSTALL_TRACKING
+ java_bundled_libs += \
+ $(ANDROID_PLAY_SERVICES_ADS_AAR_LIB) \
+ $(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
+ $(NULL)
+endif
+
+# uniq purloined from http://stackoverflow.com/a/16151140.
+uniq = $(if $1,$(firstword $1) $(call uniq,$(filter-out $(firstword $1),$1)))
+
+java_bundled_libs := $(call uniq,$(java_bundled_libs))
+java_bundled_libs := $(subst $(NULL) ,:,$(strip $(java_bundled_libs)))
+
+GECKOVIEW_JARS = \
+ constants.jar \
+ gecko-R.jar \
+ gecko-mozglue.jar \
+ gecko-util.jar \
+ gecko-view.jar \
+ sync-thirdparty.jar \
+ $(NULL)
+
+ifdef MOZ_INSTALL_TRACKING
+GECKOVIEW_JARS += gecko-thirdparty-adjust_sdk.jar
+endif
+
+geckoview_jars_classpath := $(subst $(NULL) ,:,$(strip $(GECKOVIEW_JARS)))
+
+FENNEC_JARS = \
+ gecko-browser.jar \
+ gecko-thirdparty.jar \
+ services.jar \
+ ../javaaddons/javaaddons-1.0.jar \
+ $(NULL)
+
+ifdef MOZ_WEBRTC
+FENNEC_JARS += webrtc.jar
+endif
+
+ifdef MOZ_ANDROID_SEARCH_ACTIVITY
+FENNEC_JARS += search-activity.jar
+endif
+
+ifdef MOZ_ANDROID_MLS_STUMBLER
+FENNEC_JARS += ../stumbler/stumbler.jar
+endif
+
+# All the jars we're compiling from source. (not to be confused with
+# java_bundled_libs, which holds the jars which we're including as binaries).
+ALL_JARS = \
+ $(GECKOVIEW_JARS) \
+ $(FENNEC_JARS) \
+ $(NULL)
+
+# The list of jars in Java classpath notation (colon-separated).
+all_jars_classpath := $(subst $(NULL) ,:,$(strip $(ALL_JARS)))
+
+include $(topsrcdir)/config/config.mk
+
+library_jars := \
+ $(ANDROID_SDK)/android.jar \
+ $(NULL)
+
+# Android 23 (Marshmallow) deprecated a part of the Android platform, namely the
+# org.apache.http package. Fennec removed all code that referenced said package
+# in order to easily ship to Android 23 devices (and, by extension, all devices
+# before Android 23).
+#
+# Google did not remove all code that referenced said package in their own
+# com.google.android.gms namespace! It turns out that the org.apache.http
+# package is not removed, only deprecated and hidden by default. Google added a
+# a `useLibrary` Gradle directive that allows legacy code to reference the
+# package, which is still (hidden) in the Android 23 platform.
+#
+# Fennec code doesn't need to compile against the deprecated package, since our
+# code doesn't reference the package anymore. However, we do need to Proguard
+# against the deprecated package. If we don't, Proguard -- which is a global
+# optimization -- sees Google libraries referencing "non-existent" libraries and
+# complains. The solution is to mimic the `useLibraries` directive by declaring
+# the legacy package as a provided library jar.
+#
+# See https://bugzilla.mozilla.org/show_bug.cgi?id=1233238#c19 for symptoms and
+# more discussion.
+ifdef MOZ_INSTALL_TRACKING
+library_jars += $(ANDROID_SDK)/optional/org.apache.http.legacy.jar
+endif # MOZ_INSTALL_TRACKING
+
+library_jars := $(subst $(NULL) ,:,$(strip $(library_jars)))
+
+gradle_dir := $(topobjdir)/gradle/build/mobile/android
+
+ifdef MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE
+.gradle.deps: .aapt.deps FORCE
+ @$(TOUCH) $@
+ $(topsrcdir)/mach gradle \
+ app:assembleAutomationDebug app:assembleAutomationDebugAndroidTest -x lint
+
+classes.dex: .gradle.deps
+ $(REPORT_BUILD)
+ cp $(gradle_dir)/app/intermediates/transforms/dex/automation/debug/folders/1000/1f/main/classes.dex $@
+else
+classes.dex: .proguard.deps
+ $(REPORT_BUILD)
+ $(DX) --dex --output=classes.dex jars-proguarded
+endif
+
+ifdef MOZ_DISABLE_PROGUARD
+ PROGUARD_PASSES=0
+else
+ ifdef MOZ_DEBUG
+ PROGUARD_PASSES=1
+ else
+ ifndef MOZILLA_OFFICIAL
+ PROGUARD_PASSES=1
+ else
+ PROGUARD_PASSES=6
+ endif
+ endif
+endif
+
+proguard_config_dir=$(topsrcdir)/mobile/android/config/proguard
+
+# This stanza ensures that the set of GeckoView classes does not depend on too
+# much of Fennec, where "too much" is defined as the set of potentially
+# non-GeckoView classes that GeckoView already depended on at a certain point in
+# time. The idea is to set a high-water mark that is not to be crossed.
+classycle_jar := $(topsrcdir)/mobile/android/build/classycle/classycle-1.4.1.jar
+.geckoview.deps: geckoview.ddf $(classycle_jar) $(ALL_JARS)
+ $(JAVA) -cp $(classycle_jar) \
+ classycle.dependency.DependencyChecker \
+ -mergeInnerClasses \
+ -dependencies=@$< \
+ $(ALL_JARS)
+ @$(TOUCH) $@
+
+# First, we delete debugging information from libraries. Having line-number
+# information for libraries for which we lack the source isn't useful, so this
+# saves us a bit of space. Importantly, Proguard has a bug causing it to
+# sometimes corrupt this information if present (which it does for some of the
+# included libraries). This corruption prevents dex from completing, so we need
+# to get rid of it. This prevents us from seeing line numbers in stack traces
+# for stack frames inside libraries.
+#
+# This step can occur much earlier than the main Proguard pass: it needs only
+# gecko-R.jar to have been compiled (as that's where the library R.java files
+# end up), but it does block the main Proguard pass.
+.bundled.proguard.deps: gecko-R.jar $(proguard_config_dir)/strip-libs.cfg
+ $(REPORT_BUILD)
+ @$(TOUCH) $@
+ $(JAVA) \
+ -Xmx512m -Xms128m \
+ -jar $(ANDROID_SDK_ROOT)/tools/proguard/lib/proguard.jar \
+ @$(proguard_config_dir)/strip-libs.cfg \
+ -injars $(subst ::,:,$(java_bundled_libs))\
+ -outjars bundled-jars-nodebug \
+ -libraryjars $(library_jars):gecko-R.jar
+
+# We touch the target file before invoking Proguard so that Proguard's
+# outputs are fresher than the target, preventing a subsequent
+# invocation from thinking Proguard's outputs are stale. This is safe
+# because Make removes the target file if any recipe command fails.
+.proguard.deps: .geckoview.deps .bundled.proguard.deps $(ALL_JARS) $(proguard_config_dir)/proguard.cfg
+ $(REPORT_BUILD)
+ @$(TOUCH) $@
+ $(JAVA) \
+ -Xmx512m -Xms128m \
+ -jar $(ANDROID_SDK_ROOT)/tools/proguard/lib/proguard.jar \
+ @$(proguard_config_dir)/proguard.cfg \
+ -optimizationpasses $(PROGUARD_PASSES) \
+ -injars $(subst ::,:,$(all_jars_classpath)):bundled-jars-nodebug \
+ -outjars jars-proguarded \
+ -libraryjars $(library_jars)
+
+ANNOTATION_PROCESSOR_JAR_FILES := $(DEPTH)/build/annotationProcessors/annotationProcessors.jar
+
+# This annotation processing step also generates
+# GeneratedJNIWrappers.h and GeneratedJNINatives.h
+GeneratedJNIWrappers.cpp: $(ANNOTATION_PROCESSOR_JAR_FILES) $(GECKOVIEW_JARS)
+ $(JAVA) -classpath $(geckoview_jars_classpath):$(JAVA_BOOTCLASSPATH):$(JAVA_CLASSPATH):$(ANNOTATION_PROCESSOR_JAR_FILES) \
+ org.mozilla.gecko.annotationProcessors.AnnotationProcessor \
+ Generated $(GECKOVIEW_JARS)
+
+# This annotation processing step also generates
+# FennecJNIWrappers.h and FennecJNINatives.h
+FennecJNIWrappers.cpp: $(ANNOTATION_PROCESSOR_JAR_FILES) $(FENNEC_JARS)
+ $(JAVA) -classpath $(all_jars_classpath):$(JAVA_BOOTCLASSPATH):$(JAVA_CLASSPATH):$(ANNOTATION_PROCESSOR_JAR_FILES) \
+ org.mozilla.gecko.annotationProcessors.AnnotationProcessor \
+ Fennec $(FENNEC_JARS)
+
+# Certain source files need to be preprocessed. This special rule
+# generates these files into generated/org/mozilla/gecko for
+# consumption by the build system and IDEs.
+
+# The list in moz.build looks like
+# 'preprocessed/org/mozilla/gecko/AppConstants.java'. The list in
+# constants_PP_JAVAFILES looks like
+# 'generated/preprocessed/org/mozilla/gecko/AppConstants.java'. We
+# need to write AppConstants.java.in to
+# generated/preprocessed/org/mozilla/gecko.
+preprocessed := $(addsuffix .in,$(subst generated/preprocessed/org/mozilla/gecko/,,$(filter generated/preprocessed/org/mozilla/gecko/%,$(constants_PP_JAVAFILES))))
+
+preprocessed_PATH := generated/preprocessed/org/mozilla/gecko
+preprocessed_KEEP_PATH := 1
+preprocessed_FLAGS := --marker='//\\\#'
+
+PP_TARGETS += preprocessed
+
+include $(topsrcdir)/config/rules.mk
+
+not_android_res_files := \
+ *.mkdir.done* \
+ *.DS_Store* \
+ *\#* \
+ *.rej \
+ *.orig \
+ $(NULL)
+
+# This uses the fact that Android resource directories list all
+# resource files one subdirectory below the parent resource directory.
+android_res_files := $(filter-out $(not_android_res_files),$(wildcard $(addsuffix /*,$(wildcard $(addsuffix /*,$(ANDROID_RES_DIRS))))))
+
+$(ANDROID_GENERATED_RESFILES): $(call mkdir_deps,$(sort $(dir $(ANDROID_GENERATED_RESFILES))))
+
+# [Comment 1/3] We don't have correct dependencies for strings.xml at
+# this point, so we always recursively invoke the submake to check the
+# dependencies. Sigh. And, with multilocale builds, there will be
+# multiple strings.xml files, and we need to rebuild gecko.ap_ if any
+# of them change. But! mobile/android/base/locales does not have
+# enough information to actually build res/values/strings.xml during a
+# language repack. So rather than adding rules into the main
+# makefile, and trying to work around the lack of information, we
+# force a rebuild of gecko.ap_ during packaging. See below.
+
+# Since the sub-Make is forced, it doesn't matter that we touch the
+# target file before the command. If in the future we stop forcing
+# the sub-Make, touching the target file first is better, because the
+# sub-Make outputs will be fresher than the target, and not require
+# rebuilding. This is all safe because Make removes the target file
+# if any recipe command fails. It is crucial that the sub-Make touch
+# the target files (those depending on .locales.deps) only when there
+# contents have changed; otherwise, this will force rebuild them as
+# part of every build.
+.locales.deps: FORCE
+ $(TOUCH) $@
+ $(MAKE) -C locales
+
+
+# This .deps pattern saves an invocation of the sub-Make: the single
+# invocation generates strings.xml, browsersearch.json, and
+# suggestedsites.json. The trailing semi-colon defines an empty
+# recipe: defining no recipe at all causes Make to treat the target
+# differently, in a way that defeats our dependencies.
+res/values/strings.xml: .locales.deps ;
+res/raw/browsersearch.json: .locales.deps ;
+res/raw/suggestedsites.json: .locales.deps ;
+
+all_resources = \
+ $(DEPTH)/mobile/android/base/AndroidManifest.xml \
+ $(android_res_files) \
+ $(ANDROID_GENERATED_RESFILES) \
+ $(NULL)
+
+# All of generated/org/mozilla/gecko/R.java, gecko.ap_, and R.txt are
+# produced by aapt; this saves aapt invocations. The trailing
+# semi-colon defines an empty recipe; defining no recipe at all causes
+# Make to treat the target differently, in a way that defeats our
+# dependencies.
+
+generated/org/mozilla/gecko/R.java: .aapt.deps ;
+
+# Only add libraries that contain resources here. We (unecessarily) generate an identical R.java which
+# is copied into each of these locations, and each of these files contains thousands of fields.
+# Each unnecessary copy therefore wastes unnecessary fields in the output dex file.
+# Note: usually proguard will help clean this up after the fact, but having too many fields will cause
+# dexing to fail, regardless of any later optimisations proguard could later make to bring us back
+# under the limit.
+# Ideally we would fix our aapt invocations to correctly generate minimal copies of R.java for each
+# package, but that seems redundant since gradle builds are able to correctly generate these files.
+
+# If native devices are enabled, add Google Play Services, build their resources
+# (no resources) generated/android/support/v4/R.java: .aapt.deps ;
+generated/android/support/v7/appcompat/R.java: .aapt.deps ;
+# (no resources) generated/android/support/graphics/drawable/animated/R.java: .aapt.deps ;
+# (no resources) generated/android/support/graphics/drawable/R.java: .aapt.deps ;
+generated/android/support/v7/cardview/R.java: .aapt.deps ;
+generated/android/support/design/R.java: .aapt.deps ;
+generated/android/support/v7/mediarouter/R.java: .aapt.deps ;
+generated/android/support/v7/recyclerview/R.java: .aapt.deps ;
+# (no resources) generated/android/support/customtabs/R.java: .aapt.deps ;
+# (no resources) generated/android/support/v7/palette/R.java: .aapt.deps ;
+generated/com/google/android/gms/R.java: .aapt.deps ;
+generated/com/google/android/gms/ads/R.java: .aapt.deps ;
+generated/com/google/android/gms/base/R.java: .aapt.deps ;
+generated/com/google/android/gms/cast/R.java: .aapt.deps ;
+# (no resources) generated/com/google/android/gms/gcm/R.java: .aapt.deps ;
+# (no resources) generated/com/google/android/gms/measurement/R.java: .aapt.deps ;
+
+gecko.ap_: .aapt.deps ;
+R.txt: .aapt.deps ;
+
+# [Comment 2/3] This tom-foolery provides a target that forces a
+# rebuild of gecko.ap_. This is used during packaging to ensure that
+# resources are fresh. The alternative would be complicated; see
+# [Comment 1/3].
+
+gecko-nodeps/R.java: .aapt.nodeps ;
+gecko-nodeps.ap_: .aapt.nodeps ;
+gecko-nodeps/R.txt: .aapt.nodeps ;
+
+# This ignores the default set of resources ignored by aapt, plus
+# files starting with '#'. (Emacs produces temp files named #temp#.)
+# This doesn't actually set the environment variable; it's used as a
+# parameter in the aapt invocation below. Consider updating
+# not_android_res_files as well.
+
+ANDROID_AAPT_IGNORE := !.svn:!.git:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*.scc:*~:\#*:*.rej:*.orig
+
+# 1: target file.
+# 2: dependencies.
+# 3: name of ap_ file to write.
+# 4: directory to write R.java into.
+# 5: directory to write R.txt into.
+# We touch the target file before invoking aapt so that aapt's outputs
+# are fresher than the target, preventing a subsequent invocation from
+# thinking aapt's outputs are stale. This is safe because Make
+# removes the target file if any recipe command fails.
+
+define aapt_command
+$(1): $$(call mkdir_deps,$(filter-out ./,$(dir $(3) $(4) $(5)))) $(2)
+ @$$(TOUCH) $$@
+ $$(AAPT) package -f -m \
+ -M AndroidManifest.xml \
+ -I $(ANDROID_SDK)/android.jar \
+ $(if $(MOZ_ANDROID_MAX_SDK_VERSION),--max-res-version $(MOZ_ANDROID_MAX_SDK_VERSION),) \
+ --auto-add-overlay \
+ $$(addprefix -S ,$$(ANDROID_RES_DIRS)) \
+ $$(addprefix -A ,$$(ANDROID_ASSETS_DIRS)) \
+ $(if $(ANDROID_EXTRA_PACKAGES),--extra-packages $$(subst $$(NULL) ,:,$$(strip $$(ANDROID_EXTRA_PACKAGES)))) \
+ $(if $(ANDROID_EXTRA_RES_DIRS),$$(addprefix -S ,$$(ANDROID_EXTRA_RES_DIRS))) \
+ --custom-package org.mozilla.gecko \
+ --no-version-vectors \
+ -F $(3) \
+ -J $(4) \
+ --output-text-symbols $(5) \
+ --ignore-assets "$$(ANDROID_AAPT_IGNORE)"
+endef
+
+# [Comment 3/3] The first of these rules is used during regular
+# builds. The second writes an ap_ file that is only used during
+# packaging. It doesn't write the normal ap_, or R.java, since we
+# don't want the packaging step to write anything that would make a
+# further no-op build do work. See also
+# toolkit/mozapps/installer/packager.mk.
+
+# .aapt.deps: $(all_resources)
+$(eval $(call aapt_command,.aapt.deps,$(all_resources),gecko.ap_,generated/,./))
+
+ifdef MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE
+.aapt.nodeps: FORCE
+ cp $(gradle_dir)/app/intermediates/res/resources-automation-debug.ap_ gecko-nodeps.ap_
+else
+# .aapt.nodeps: $(DEPTH)/mobile/android/base/AndroidManifest.xml FORCE
+$(eval $(call aapt_command,.aapt.nodeps,$(DEPTH)/mobile/android/base/AndroidManifest.xml FORCE,gecko-nodeps.ap_,gecko-nodeps/,gecko-nodeps/))
+endif
+
+# Override the Java settings with some specific android settings
+include $(topsrcdir)/config/android-common.mk
+
+update-generated-wrappers:
+ @cp $(CURDIR)/GeneratedJNIWrappers.cpp \
+ $(CURDIR)/GeneratedJNIWrappers.h \
+ $(CURDIR)/GeneratedJNINatives.h $(topsrcdir)/widget/android
+ @echo Updated generated JNI code
+
+update-fennec-wrappers:
+ @cp $(CURDIR)/FennecJNIWrappers.cpp \
+ $(CURDIR)/FennecJNIWrappers.h \
+ $(CURDIR)/FennecJNINatives.h $(topsrcdir)/widget/android/fennec
+ @echo Updated Fennec JNI code
+
+.PHONY: update-generated-wrappers
+
+# This target is only used by IDE integrations. It rebuilds resources
+# that end up in omni.ja using the equivalent of |mach build faster|,
+# does most of the packaging step, and then updates omni.ja in
+# place. If you're not using an IDE, you should be using |mach build
+# mobile/android && mach package|.
+$(ABS_DIST)/fennec/$(OMNIJAR_NAME): FORCE
+ $(REPORT_BUILD)
+ $(MAKE) -C ../../../faster
+ $(MAKE) -C ../installer stage-package
+ $(MKDIR) -p $(@D)
+ rsync --update $(DIST)/fennec/$(notdir $(OMNIJAR_NAME)) $@
+ $(RM) $(DIST)/fennec/$(notdir $(OMNIJAR_NAME))
+
+# Targets built very early during a Gradle build.
+gradle-targets: $(foreach f,$(constants_PP_JAVAFILES),$(f))
+gradle-targets: $(DEPTH)/mobile/android/base/AndroidManifest.xml
+gradle-targets: $(ANDROID_GENERATED_RESFILES)
+
+ifndef MOZILLA_OFFICIAL
+# Local developers update omni.ja during their builds. There's a
+# chicken-and-egg problem here.
+gradle-omnijar: $(abspath $(DIST)/fennec/$(OMNIJAR_NAME))
+else
+# In automation, omni.ja is built only during packaging.
+gradle-omnijar:
+endif
+
+.PHONY: gradle-targets gradle-omnijar
+
+# GeneratedJNIWrappers.cpp target also generates
+# GeneratedJNIWrappers.h and GeneratedJNINatives.h
+# FennecJNIWrappers.cpp target also generates
+# FennecJNIWrappers.h and FennecJNINatives.h
+ifndef MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE
+libs:: GeneratedJNIWrappers.cpp
+ @(diff GeneratedJNIWrappers.cpp $(topsrcdir)/widget/android/GeneratedJNIWrappers.cpp >/dev/null && \
+ diff GeneratedJNIWrappers.h $(topsrcdir)/widget/android/GeneratedJNIWrappers.h >/dev/null && \
+ diff GeneratedJNINatives.h $(topsrcdir)/widget/android/GeneratedJNINatives.h >/dev/null) || \
+ (echo '*****************************************************' && \
+ echo '*** Error: The generated JNI code has changed ***' && \
+ echo '* To update generated code in the tree, please run *' && \
+ echo && \
+ echo ' make -C $(CURDIR) update-generated-wrappers' && \
+ echo && \
+ echo '* Repeat the build, and check in any changes. *' && \
+ echo '*****************************************************' && \
+ exit 1)
+
+libs:: FennecJNIWrappers.cpp
+ @(diff FennecJNIWrappers.cpp $(topsrcdir)/widget/android/fennec/FennecJNIWrappers.cpp >/dev/null && \
+ diff FennecJNIWrappers.h $(topsrcdir)/widget/android/fennec/FennecJNIWrappers.h >/dev/null && \
+ diff FennecJNINatives.h $(topsrcdir)/widget/android/fennec/FennecJNINatives.h >/dev/null) || \
+ (echo '*****************************************************' && \
+ echo '*** Error: The Fennec JNI code has changed ***' && \
+ echo '* To update generated code in the tree, please run *' && \
+ echo && \
+ echo ' make -C $(CURDIR) update-fennec-wrappers' && \
+ echo && \
+ echo '* Repeat the build, and check in any changes. *' && \
+ echo '*****************************************************' && \
+ exit 1)
+endif
+
+libs:: classes.dex
+ $(INSTALL) classes.dex $(FINAL_TARGET)
+
+# Generate Java binder interfaces from AIDL files.
+aidl_src_path := $(srcdir)/aidl
+aidl_target_path := generated
+media_pkg := org/mozilla/gecko/media
+
+$(aidl_target_path)/$(media_pkg)/%.java:$(aidl_src_path)/$(media_pkg)/%.aidl
+ @echo "Processing AIDL: $< => $@"
+ $(AIDL) -p$(ANDROID_SDK)/framework.aidl -I$(aidl_src_path) -o$(aidl_target_path) $<
diff --git a/mobile/android/base/adjust-sdk-sandbox.token b/mobile/android/base/adjust-sdk-sandbox.token
new file mode 100644
index 0000000000..bb5bd50943
--- /dev/null
+++ b/mobile/android/base/adjust-sdk-sandbox.token
@@ -0,0 +1 @@
+ABCDEFGHIJKL
diff --git a/mobile/android/base/adjust_sdk_app_token.in b/mobile/android/base/adjust_sdk_app_token.in
new file mode 100644
index 0000000000..07ac44d3f4
--- /dev/null
+++ b/mobile/android/base/adjust_sdk_app_token.in
@@ -0,0 +1,3 @@
+//#ifdef MOZ_INSTALL_TRACKING
+//#define MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN @MOZ_ADJUST_SDK_KEY@
+//#endif
diff --git a/mobile/android/base/aidl/org/mozilla/gecko/media/FormatParam.aidl b/mobile/android/base/aidl/org/mozilla/gecko/media/FormatParam.aidl
new file mode 100644
index 0000000000..91ce56d463
--- /dev/null
+++ b/mobile/android/base/aidl/org/mozilla/gecko/media/FormatParam.aidl
@@ -0,0 +1,7 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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;
+
+parcelable FormatParam; \ No newline at end of file
diff --git a/mobile/android/base/aidl/org/mozilla/gecko/media/ICodec.aidl b/mobile/android/base/aidl/org/mozilla/gecko/media/ICodec.aidl
new file mode 100644
index 0000000000..7b434a5b63
--- /dev/null
+++ b/mobile/android/base/aidl/org/mozilla/gecko/media/ICodec.aidl
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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;
+
+// Non-default types used in interface.
+import android.os.Bundle;
+import android.view.Surface;
+import org.mozilla.gecko.media.FormatParam;
+import org.mozilla.gecko.media.ICodecCallbacks;
+import org.mozilla.gecko.media.Sample;
+
+interface ICodec {
+ void setCallbacks(in ICodecCallbacks callbacks);
+ boolean configure(in FormatParam format, inout Surface surface, int flags);
+ oneway void start();
+ oneway void stop();
+ oneway void flush();
+ oneway void release();
+
+ Sample dequeueInput(int size);
+ oneway void queueInput(in Sample sample);
+
+ oneway void releaseOutput(in Sample sample);
+}
diff --git a/mobile/android/base/aidl/org/mozilla/gecko/media/ICodecCallbacks.aidl b/mobile/android/base/aidl/org/mozilla/gecko/media/ICodecCallbacks.aidl
new file mode 100644
index 0000000000..59e637f55e
--- /dev/null
+++ b/mobile/android/base/aidl/org/mozilla/gecko/media/ICodecCallbacks.aidl
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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;
+
+// Non-default types used in interface.
+import org.mozilla.gecko.media.FormatParam;
+import org.mozilla.gecko.media.Sample;
+
+interface ICodecCallbacks {
+ oneway void onInputExhausted();
+ oneway void onOutputFormatChanged(in FormatParam format);
+ oneway void onOutput(in Sample sample);
+ oneway void onError(boolean fatal);
+} \ No newline at end of file
diff --git a/mobile/android/base/aidl/org/mozilla/gecko/media/IMediaDrmBridge.aidl b/mobile/android/base/aidl/org/mozilla/gecko/media/IMediaDrmBridge.aidl
new file mode 100644
index 0000000000..515e4b7d08
--- /dev/null
+++ b/mobile/android/base/aidl/org/mozilla/gecko/media/IMediaDrmBridge.aidl
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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;
+
+// Non-default types used in interface.
+import org.mozilla.gecko.media.IMediaDrmBridgeCallbacks;
+
+interface IMediaDrmBridge {
+ void setCallbacks(in IMediaDrmBridgeCallbacks callbacks);
+
+ oneway void createSession(int createSessionToken,
+ int promiseId,
+ String initDataType,
+ in byte[] initData);
+
+ oneway void updateSession(int promiseId,
+ String sessionId,
+ in byte[] response);
+
+ oneway void closeSession(int promiseId, String sessionId);
+
+ oneway void release();
+}
diff --git a/mobile/android/base/aidl/org/mozilla/gecko/media/IMediaDrmBridgeCallbacks.aidl b/mobile/android/base/aidl/org/mozilla/gecko/media/IMediaDrmBridgeCallbacks.aidl
new file mode 100644
index 0000000000..b3918417e6
--- /dev/null
+++ b/mobile/android/base/aidl/org/mozilla/gecko/media/IMediaDrmBridgeCallbacks.aidl
@@ -0,0 +1,31 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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;
+
+// Non-default types used in interface.
+import org.mozilla.gecko.media.SessionKeyInfo;
+
+interface IMediaDrmBridgeCallbacks {
+
+ oneway void onSessionCreated(int createSessionToken,
+ int promiseId,
+ in byte[] sessionId,
+ in byte[] request);
+
+ oneway void onSessionUpdated(int promiseId, in byte[] sessionId);
+
+ oneway void onSessionClosed(int promiseId, in byte[] sessionId);
+
+ oneway void onSessionMessage(in byte[] sessionId,
+ int sessionMessageType,
+ in byte[] request);
+
+ oneway void onSessionError(in byte[] sessionId, String message);
+
+ oneway void onSessionBatchedKeyChanged(in byte[] sessionId,
+ in SessionKeyInfo[] keyInfos);
+
+ oneway void onRejectPromise(int promiseId, String message);
+}
diff --git a/mobile/android/base/aidl/org/mozilla/gecko/media/IMediaManager.aidl b/mobile/android/base/aidl/org/mozilla/gecko/media/IMediaManager.aidl
new file mode 100644
index 0000000000..023733d9db
--- /dev/null
+++ b/mobile/android/base/aidl/org/mozilla/gecko/media/IMediaManager.aidl
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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;
+
+// Non-default types used in interface.
+import org.mozilla.gecko.media.ICodec;
+import org.mozilla.gecko.media.IMediaDrmBridge;
+
+interface IMediaManager {
+ /** Creates a remote ICodec object. */
+ ICodec createCodec();
+
+ /** Creates a remote IMediaDrmBridge object. */
+ IMediaDrmBridge createRemoteMediaDrmBridge(in String keySystem,
+ in String stubId);
+}
diff --git a/mobile/android/base/aidl/org/mozilla/gecko/media/Sample.aidl b/mobile/android/base/aidl/org/mozilla/gecko/media/Sample.aidl
new file mode 100644
index 0000000000..0d55c76fc6
--- /dev/null
+++ b/mobile/android/base/aidl/org/mozilla/gecko/media/Sample.aidl
@@ -0,0 +1,7 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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;
+
+parcelable Sample; \ No newline at end of file
diff --git a/mobile/android/base/aidl/org/mozilla/gecko/media/SessionKeyInfo.aidl b/mobile/android/base/aidl/org/mozilla/gecko/media/SessionKeyInfo.aidl
new file mode 100644
index 0000000000..1ec8f63c73
--- /dev/null
+++ b/mobile/android/base/aidl/org/mozilla/gecko/media/SessionKeyInfo.aidl
@@ -0,0 +1,7 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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;
+
+parcelable SessionKeyInfo; \ No newline at end of file
diff --git a/mobile/android/base/android-services.mozbuild b/mobile/android/base/android-services.mozbuild
new file mode 100644
index 0000000000..118a0c44c9
--- /dev/null
+++ b/mobile/android/base/android-services.mozbuild
@@ -0,0 +1,1084 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+sync_thirdparty_java_files = [
+ 'ch/boye/httpclientandroidlib/androidextra/Base64.java',
+ 'ch/boye/httpclientandroidlib/androidextra/HttpClientAndroidLog.java',
+ 'ch/boye/httpclientandroidlib/annotation/GuardedBy.java',
+ 'ch/boye/httpclientandroidlib/annotation/Immutable.java',
+ 'ch/boye/httpclientandroidlib/annotation/NotThreadSafe.java',
+ 'ch/boye/httpclientandroidlib/annotation/package-info.java',
+ 'ch/boye/httpclientandroidlib/annotation/ThreadSafe.java',
+ 'ch/boye/httpclientandroidlib/auth/AUTH.java',
+ 'ch/boye/httpclientandroidlib/auth/AuthenticationException.java',
+ 'ch/boye/httpclientandroidlib/auth/AuthOption.java',
+ 'ch/boye/httpclientandroidlib/auth/AuthProtocolState.java',
+ 'ch/boye/httpclientandroidlib/auth/AuthScheme.java',
+ 'ch/boye/httpclientandroidlib/auth/AuthSchemeFactory.java',
+ 'ch/boye/httpclientandroidlib/auth/AuthSchemeProvider.java',
+ 'ch/boye/httpclientandroidlib/auth/AuthSchemeRegistry.java',
+ 'ch/boye/httpclientandroidlib/auth/AuthScope.java',
+ 'ch/boye/httpclientandroidlib/auth/AuthState.java',
+ 'ch/boye/httpclientandroidlib/auth/BasicUserPrincipal.java',
+ 'ch/boye/httpclientandroidlib/auth/ChallengeState.java',
+ 'ch/boye/httpclientandroidlib/auth/ContextAwareAuthScheme.java',
+ 'ch/boye/httpclientandroidlib/auth/Credentials.java',
+ 'ch/boye/httpclientandroidlib/auth/InvalidCredentialsException.java',
+ 'ch/boye/httpclientandroidlib/auth/MalformedChallengeException.java',
+ 'ch/boye/httpclientandroidlib/auth/NTCredentials.java',
+ 'ch/boye/httpclientandroidlib/auth/NTUserPrincipal.java',
+ 'ch/boye/httpclientandroidlib/auth/package-info.java',
+ 'ch/boye/httpclientandroidlib/auth/params/AuthParamBean.java',
+ 'ch/boye/httpclientandroidlib/auth/params/AuthParams.java',
+ 'ch/boye/httpclientandroidlib/auth/params/AuthPNames.java',
+ 'ch/boye/httpclientandroidlib/auth/params/package-info.java',
+ 'ch/boye/httpclientandroidlib/auth/UsernamePasswordCredentials.java',
+ 'ch/boye/httpclientandroidlib/client/AuthCache.java',
+ 'ch/boye/httpclientandroidlib/client/AuthenticationHandler.java',
+ 'ch/boye/httpclientandroidlib/client/AuthenticationStrategy.java',
+ 'ch/boye/httpclientandroidlib/client/BackoffManager.java',
+ 'ch/boye/httpclientandroidlib/client/cache/CacheResponseStatus.java',
+ 'ch/boye/httpclientandroidlib/client/cache/HeaderConstants.java',
+ 'ch/boye/httpclientandroidlib/client/cache/HttpCacheContext.java',
+ 'ch/boye/httpclientandroidlib/client/cache/HttpCacheEntry.java',
+ 'ch/boye/httpclientandroidlib/client/cache/HttpCacheEntrySerializationException.java',
+ 'ch/boye/httpclientandroidlib/client/cache/HttpCacheEntrySerializer.java',
+ 'ch/boye/httpclientandroidlib/client/cache/HttpCacheInvalidator.java',
+ 'ch/boye/httpclientandroidlib/client/cache/HttpCacheStorage.java',
+ 'ch/boye/httpclientandroidlib/client/cache/HttpCacheUpdateCallback.java',
+ 'ch/boye/httpclientandroidlib/client/cache/HttpCacheUpdateException.java',
+ 'ch/boye/httpclientandroidlib/client/cache/InputLimit.java',
+ 'ch/boye/httpclientandroidlib/client/cache/Resource.java',
+ 'ch/boye/httpclientandroidlib/client/cache/ResourceFactory.java',
+ 'ch/boye/httpclientandroidlib/client/CircularRedirectException.java',
+ 'ch/boye/httpclientandroidlib/client/ClientProtocolException.java',
+ 'ch/boye/httpclientandroidlib/client/config/AuthSchemes.java',
+ 'ch/boye/httpclientandroidlib/client/config/CookieSpecs.java',
+ 'ch/boye/httpclientandroidlib/client/config/package-info.java',
+ 'ch/boye/httpclientandroidlib/client/config/RequestConfig.java',
+ 'ch/boye/httpclientandroidlib/client/ConnectionBackoffStrategy.java',
+ 'ch/boye/httpclientandroidlib/client/CookieStore.java',
+ 'ch/boye/httpclientandroidlib/client/CredentialsProvider.java',
+ 'ch/boye/httpclientandroidlib/client/entity/DecompressingEntity.java',
+ 'ch/boye/httpclientandroidlib/client/entity/DeflateDecompressingEntity.java',
+ 'ch/boye/httpclientandroidlib/client/entity/DeflateInputStream.java',
+ 'ch/boye/httpclientandroidlib/client/entity/EntityBuilder.java',
+ 'ch/boye/httpclientandroidlib/client/entity/GzipCompressingEntity.java',
+ 'ch/boye/httpclientandroidlib/client/entity/GzipDecompressingEntity.java',
+ 'ch/boye/httpclientandroidlib/client/entity/LazyDecompressingInputStream.java',
+ 'ch/boye/httpclientandroidlib/client/entity/package-info.java',
+ 'ch/boye/httpclientandroidlib/client/entity/UrlEncodedFormEntity.java',
+ 'ch/boye/httpclientandroidlib/client/HttpClient.java',
+ 'ch/boye/httpclientandroidlib/client/HttpRequestRetryHandler.java',
+ 'ch/boye/httpclientandroidlib/client/HttpResponseException.java',
+ 'ch/boye/httpclientandroidlib/client/methods/AbortableHttpRequest.java',
+ 'ch/boye/httpclientandroidlib/client/methods/AbstractExecutionAwareRequest.java',
+ 'ch/boye/httpclientandroidlib/client/methods/CloseableHttpResponse.java',
+ 'ch/boye/httpclientandroidlib/client/methods/Configurable.java',
+ 'ch/boye/httpclientandroidlib/client/methods/HttpDelete.java',
+ 'ch/boye/httpclientandroidlib/client/methods/HttpEntityEnclosingRequestBase.java',
+ 'ch/boye/httpclientandroidlib/client/methods/HttpExecutionAware.java',
+ 'ch/boye/httpclientandroidlib/client/methods/HttpGet.java',
+ 'ch/boye/httpclientandroidlib/client/methods/HttpHead.java',
+ 'ch/boye/httpclientandroidlib/client/methods/HttpOptions.java',
+ 'ch/boye/httpclientandroidlib/client/methods/HttpPatch.java',
+ 'ch/boye/httpclientandroidlib/client/methods/HttpPost.java',
+ 'ch/boye/httpclientandroidlib/client/methods/HttpPut.java',
+ 'ch/boye/httpclientandroidlib/client/methods/HttpRequestBase.java',
+ 'ch/boye/httpclientandroidlib/client/methods/HttpRequestWrapper.java',
+ 'ch/boye/httpclientandroidlib/client/methods/HttpTrace.java',
+ 'ch/boye/httpclientandroidlib/client/methods/HttpUriRequest.java',
+ 'ch/boye/httpclientandroidlib/client/methods/package-info.java',
+ 'ch/boye/httpclientandroidlib/client/methods/RequestBuilder.java',
+ 'ch/boye/httpclientandroidlib/client/NonRepeatableRequestException.java',
+ 'ch/boye/httpclientandroidlib/client/package-info.java',
+ 'ch/boye/httpclientandroidlib/client/params/AllClientPNames.java',
+ 'ch/boye/httpclientandroidlib/client/params/AuthPolicy.java',
+ 'ch/boye/httpclientandroidlib/client/params/ClientParamBean.java',
+ 'ch/boye/httpclientandroidlib/client/params/ClientPNames.java',
+ 'ch/boye/httpclientandroidlib/client/params/CookiePolicy.java',
+ 'ch/boye/httpclientandroidlib/client/params/HttpClientParamConfig.java',
+ 'ch/boye/httpclientandroidlib/client/params/HttpClientParams.java',
+ 'ch/boye/httpclientandroidlib/client/params/package-info.java',
+ 'ch/boye/httpclientandroidlib/client/protocol/ClientContext.java',
+ 'ch/boye/httpclientandroidlib/client/protocol/ClientContextConfigurer.java',
+ 'ch/boye/httpclientandroidlib/client/protocol/HttpClientContext.java',
+ 'ch/boye/httpclientandroidlib/client/protocol/package-info.java',
+ 'ch/boye/httpclientandroidlib/client/protocol/RequestAcceptEncoding.java',
+ 'ch/boye/httpclientandroidlib/client/protocol/RequestAddCookies.java',
+ 'ch/boye/httpclientandroidlib/client/protocol/RequestAuthCache.java',
+ 'ch/boye/httpclientandroidlib/client/protocol/RequestAuthenticationBase.java',
+ 'ch/boye/httpclientandroidlib/client/protocol/RequestClientConnControl.java',
+ 'ch/boye/httpclientandroidlib/client/protocol/RequestDefaultHeaders.java',
+ 'ch/boye/httpclientandroidlib/client/protocol/RequestExpectContinue.java',
+ 'ch/boye/httpclientandroidlib/client/protocol/RequestProxyAuthentication.java',
+ 'ch/boye/httpclientandroidlib/client/protocol/RequestTargetAuthentication.java',
+ 'ch/boye/httpclientandroidlib/client/protocol/ResponseAuthCache.java',
+ 'ch/boye/httpclientandroidlib/client/protocol/ResponseContentEncoding.java',
+ 'ch/boye/httpclientandroidlib/client/protocol/ResponseProcessCookies.java',
+ 'ch/boye/httpclientandroidlib/client/RedirectException.java',
+ 'ch/boye/httpclientandroidlib/client/RedirectHandler.java',
+ 'ch/boye/httpclientandroidlib/client/RedirectStrategy.java',
+ 'ch/boye/httpclientandroidlib/client/RequestDirector.java',
+ 'ch/boye/httpclientandroidlib/client/ResponseHandler.java',
+ 'ch/boye/httpclientandroidlib/client/ServiceUnavailableRetryStrategy.java',
+ 'ch/boye/httpclientandroidlib/client/UserTokenHandler.java',
+ 'ch/boye/httpclientandroidlib/client/utils/CloneUtils.java',
+ 'ch/boye/httpclientandroidlib/client/utils/DateUtils.java',
+ 'ch/boye/httpclientandroidlib/client/utils/HttpClientUtils.java',
+ 'ch/boye/httpclientandroidlib/client/utils/Idn.java',
+ 'ch/boye/httpclientandroidlib/client/utils/JdkIdn.java',
+ 'ch/boye/httpclientandroidlib/client/utils/package-info.java',
+ 'ch/boye/httpclientandroidlib/client/utils/Punycode.java',
+ 'ch/boye/httpclientandroidlib/client/utils/Rfc3492Idn.java',
+ 'ch/boye/httpclientandroidlib/client/utils/URIBuilder.java',
+ 'ch/boye/httpclientandroidlib/client/utils/URIUtils.java',
+ 'ch/boye/httpclientandroidlib/client/utils/URLEncodedUtils.java',
+ 'ch/boye/httpclientandroidlib/concurrent/BasicFuture.java',
+ 'ch/boye/httpclientandroidlib/concurrent/Cancellable.java',
+ 'ch/boye/httpclientandroidlib/concurrent/FutureCallback.java',
+ 'ch/boye/httpclientandroidlib/concurrent/package-info.java',
+ 'ch/boye/httpclientandroidlib/config/ConnectionConfig.java',
+ 'ch/boye/httpclientandroidlib/config/Lookup.java',
+ 'ch/boye/httpclientandroidlib/config/MessageConstraints.java',
+ 'ch/boye/httpclientandroidlib/config/package-info.java',
+ 'ch/boye/httpclientandroidlib/config/Registry.java',
+ 'ch/boye/httpclientandroidlib/config/RegistryBuilder.java',
+ 'ch/boye/httpclientandroidlib/config/SocketConfig.java',
+ 'ch/boye/httpclientandroidlib/conn/BasicEofSensorWatcher.java',
+ 'ch/boye/httpclientandroidlib/conn/BasicManagedEntity.java',
+ 'ch/boye/httpclientandroidlib/conn/ClientConnectionManager.java',
+ 'ch/boye/httpclientandroidlib/conn/ClientConnectionManagerFactory.java',
+ 'ch/boye/httpclientandroidlib/conn/ClientConnectionOperator.java',
+ 'ch/boye/httpclientandroidlib/conn/ClientConnectionRequest.java',
+ 'ch/boye/httpclientandroidlib/conn/ConnectionKeepAliveStrategy.java',
+ 'ch/boye/httpclientandroidlib/conn/ConnectionPoolTimeoutException.java',
+ 'ch/boye/httpclientandroidlib/conn/ConnectionReleaseTrigger.java',
+ 'ch/boye/httpclientandroidlib/conn/ConnectionRequest.java',
+ 'ch/boye/httpclientandroidlib/conn/ConnectTimeoutException.java',
+ 'ch/boye/httpclientandroidlib/conn/DnsResolver.java',
+ 'ch/boye/httpclientandroidlib/conn/EofSensorInputStream.java',
+ 'ch/boye/httpclientandroidlib/conn/EofSensorWatcher.java',
+ 'ch/boye/httpclientandroidlib/conn/HttpClientConnectionManager.java',
+ 'ch/boye/httpclientandroidlib/conn/HttpConnectionFactory.java',
+ 'ch/boye/httpclientandroidlib/conn/HttpHostConnectException.java',
+ 'ch/boye/httpclientandroidlib/conn/HttpInetSocketAddress.java',
+ 'ch/boye/httpclientandroidlib/conn/HttpRoutedConnection.java',
+ 'ch/boye/httpclientandroidlib/conn/ManagedClientConnection.java',
+ 'ch/boye/httpclientandroidlib/conn/ManagedHttpClientConnection.java',
+ 'ch/boye/httpclientandroidlib/conn/MultihomePlainSocketFactory.java',
+ 'ch/boye/httpclientandroidlib/conn/OperatedClientConnection.java',
+ 'ch/boye/httpclientandroidlib/conn/package-info.java',
+ 'ch/boye/httpclientandroidlib/conn/params/ConnConnectionParamBean.java',
+ 'ch/boye/httpclientandroidlib/conn/params/ConnConnectionPNames.java',
+ 'ch/boye/httpclientandroidlib/conn/params/ConnManagerParamBean.java',
+ 'ch/boye/httpclientandroidlib/conn/params/ConnManagerParams.java',
+ 'ch/boye/httpclientandroidlib/conn/params/ConnManagerPNames.java',
+ 'ch/boye/httpclientandroidlib/conn/params/ConnPerRoute.java',
+ 'ch/boye/httpclientandroidlib/conn/params/ConnPerRouteBean.java',
+ 'ch/boye/httpclientandroidlib/conn/params/ConnRouteParamBean.java',
+ 'ch/boye/httpclientandroidlib/conn/params/ConnRouteParams.java',
+ 'ch/boye/httpclientandroidlib/conn/params/ConnRoutePNames.java',
+ 'ch/boye/httpclientandroidlib/conn/params/package-info.java',
+ 'ch/boye/httpclientandroidlib/conn/routing/BasicRouteDirector.java',
+ 'ch/boye/httpclientandroidlib/conn/routing/HttpRoute.java',
+ 'ch/boye/httpclientandroidlib/conn/routing/HttpRouteDirector.java',
+ 'ch/boye/httpclientandroidlib/conn/routing/HttpRoutePlanner.java',
+ 'ch/boye/httpclientandroidlib/conn/routing/package-info.java',
+ 'ch/boye/httpclientandroidlib/conn/routing/RouteInfo.java',
+ 'ch/boye/httpclientandroidlib/conn/routing/RouteTracker.java',
+ 'ch/boye/httpclientandroidlib/conn/scheme/HostNameResolver.java',
+ 'ch/boye/httpclientandroidlib/conn/scheme/LayeredSchemeSocketFactory.java',
+ 'ch/boye/httpclientandroidlib/conn/scheme/LayeredSocketFactory.java',
+ 'ch/boye/httpclientandroidlib/conn/scheme/LayeredSocketFactoryAdaptor.java',
+ 'ch/boye/httpclientandroidlib/conn/scheme/package-info.java',
+ 'ch/boye/httpclientandroidlib/conn/scheme/PlainSocketFactory.java',
+ 'ch/boye/httpclientandroidlib/conn/scheme/Scheme.java',
+ 'ch/boye/httpclientandroidlib/conn/scheme/SchemeLayeredSocketFactory.java',
+ 'ch/boye/httpclientandroidlib/conn/scheme/SchemeLayeredSocketFactoryAdaptor.java',
+ 'ch/boye/httpclientandroidlib/conn/scheme/SchemeLayeredSocketFactoryAdaptor2.java',
+ 'ch/boye/httpclientandroidlib/conn/scheme/SchemeRegistry.java',
+ 'ch/boye/httpclientandroidlib/conn/scheme/SchemeSocketFactory.java',
+ 'ch/boye/httpclientandroidlib/conn/scheme/SchemeSocketFactoryAdaptor.java',
+ 'ch/boye/httpclientandroidlib/conn/scheme/SocketFactory.java',
+ 'ch/boye/httpclientandroidlib/conn/scheme/SocketFactoryAdaptor.java',
+ 'ch/boye/httpclientandroidlib/conn/SchemePortResolver.java',
+ 'ch/boye/httpclientandroidlib/conn/socket/ConnectionSocketFactory.java',
+ 'ch/boye/httpclientandroidlib/conn/socket/LayeredConnectionSocketFactory.java',
+ 'ch/boye/httpclientandroidlib/conn/socket/package-info.java',
+ 'ch/boye/httpclientandroidlib/conn/socket/PlainConnectionSocketFactory.java',
+ 'ch/boye/httpclientandroidlib/conn/ssl/AbstractVerifier.java',
+ 'ch/boye/httpclientandroidlib/conn/ssl/AllowAllHostnameVerifier.java',
+ 'ch/boye/httpclientandroidlib/conn/ssl/BrowserCompatHostnameVerifier.java',
+ 'ch/boye/httpclientandroidlib/conn/ssl/DistinguishedNameParser.java',
+ 'ch/boye/httpclientandroidlib/conn/ssl/package-info.java',
+ 'ch/boye/httpclientandroidlib/conn/ssl/PrivateKeyDetails.java',
+ 'ch/boye/httpclientandroidlib/conn/ssl/PrivateKeyStrategy.java',
+ 'ch/boye/httpclientandroidlib/conn/ssl/SSLConnectionSocketFactory.java',
+ 'ch/boye/httpclientandroidlib/conn/ssl/SSLContextBuilder.java',
+ 'ch/boye/httpclientandroidlib/conn/ssl/SSLContexts.java',
+ 'ch/boye/httpclientandroidlib/conn/ssl/SSLInitializationException.java',
+ 'ch/boye/httpclientandroidlib/conn/ssl/SSLSocketFactory.java',
+ 'ch/boye/httpclientandroidlib/conn/ssl/StrictHostnameVerifier.java',
+ 'ch/boye/httpclientandroidlib/conn/ssl/TokenParser.java',
+ 'ch/boye/httpclientandroidlib/conn/ssl/TrustSelfSignedStrategy.java',
+ 'ch/boye/httpclientandroidlib/conn/ssl/TrustStrategy.java',
+ 'ch/boye/httpclientandroidlib/conn/ssl/X509HostnameVerifier.java',
+ 'ch/boye/httpclientandroidlib/conn/UnsupportedSchemeException.java',
+ 'ch/boye/httpclientandroidlib/conn/util/InetAddressUtils.java',
+ 'ch/boye/httpclientandroidlib/conn/util/package-info.java',
+ 'ch/boye/httpclientandroidlib/ConnectionClosedException.java',
+ 'ch/boye/httpclientandroidlib/ConnectionReuseStrategy.java',
+ 'ch/boye/httpclientandroidlib/Consts.java',
+ 'ch/boye/httpclientandroidlib/ContentTooLongException.java',
+ 'ch/boye/httpclientandroidlib/cookie/ClientCookie.java',
+ 'ch/boye/httpclientandroidlib/cookie/Cookie.java',
+ 'ch/boye/httpclientandroidlib/cookie/CookieAttributeHandler.java',
+ 'ch/boye/httpclientandroidlib/cookie/CookieIdentityComparator.java',
+ 'ch/boye/httpclientandroidlib/cookie/CookieOrigin.java',
+ 'ch/boye/httpclientandroidlib/cookie/CookiePathComparator.java',
+ 'ch/boye/httpclientandroidlib/cookie/CookieRestrictionViolationException.java',
+ 'ch/boye/httpclientandroidlib/cookie/CookieSpec.java',
+ 'ch/boye/httpclientandroidlib/cookie/CookieSpecFactory.java',
+ 'ch/boye/httpclientandroidlib/cookie/CookieSpecProvider.java',
+ 'ch/boye/httpclientandroidlib/cookie/CookieSpecRegistry.java',
+ 'ch/boye/httpclientandroidlib/cookie/MalformedCookieException.java',
+ 'ch/boye/httpclientandroidlib/cookie/package-info.java',
+ 'ch/boye/httpclientandroidlib/cookie/params/CookieSpecParamBean.java',
+ 'ch/boye/httpclientandroidlib/cookie/params/CookieSpecPNames.java',
+ 'ch/boye/httpclientandroidlib/cookie/params/package-info.java',
+ 'ch/boye/httpclientandroidlib/cookie/SetCookie.java',
+ 'ch/boye/httpclientandroidlib/cookie/SetCookie2.java',
+ 'ch/boye/httpclientandroidlib/cookie/SM.java',
+ 'ch/boye/httpclientandroidlib/entity/AbstractHttpEntity.java',
+ 'ch/boye/httpclientandroidlib/entity/BasicHttpEntity.java',
+ 'ch/boye/httpclientandroidlib/entity/BufferedHttpEntity.java',
+ 'ch/boye/httpclientandroidlib/entity/ByteArrayEntity.java',
+ 'ch/boye/httpclientandroidlib/entity/ContentLengthStrategy.java',
+ 'ch/boye/httpclientandroidlib/entity/ContentProducer.java',
+ 'ch/boye/httpclientandroidlib/entity/ContentType.java',
+ 'ch/boye/httpclientandroidlib/entity/EntityTemplate.java',
+ 'ch/boye/httpclientandroidlib/entity/FileEntity.java',
+ 'ch/boye/httpclientandroidlib/entity/HttpEntityWrapper.java',
+ 'ch/boye/httpclientandroidlib/entity/InputStreamEntity.java',
+ 'ch/boye/httpclientandroidlib/entity/mime/AbstractMultipartForm.java',
+ 'ch/boye/httpclientandroidlib/entity/mime/content/AbstractContentBody.java',
+ 'ch/boye/httpclientandroidlib/entity/mime/content/ByteArrayBody.java',
+ 'ch/boye/httpclientandroidlib/entity/mime/content/ContentBody.java',
+ 'ch/boye/httpclientandroidlib/entity/mime/content/ContentDescriptor.java',
+ 'ch/boye/httpclientandroidlib/entity/mime/content/FileBody.java',
+ 'ch/boye/httpclientandroidlib/entity/mime/content/InputStreamBody.java',
+ 'ch/boye/httpclientandroidlib/entity/mime/content/package-info.java',
+ 'ch/boye/httpclientandroidlib/entity/mime/content/StringBody.java',
+ 'ch/boye/httpclientandroidlib/entity/mime/FormBodyPart.java',
+ 'ch/boye/httpclientandroidlib/entity/mime/Header.java',
+ 'ch/boye/httpclientandroidlib/entity/mime/HttpBrowserCompatibleMultipart.java',
+ 'ch/boye/httpclientandroidlib/entity/mime/HttpMultipartMode.java',
+ 'ch/boye/httpclientandroidlib/entity/mime/HttpRFC6532Multipart.java',
+ 'ch/boye/httpclientandroidlib/entity/mime/HttpStrictMultipart.java',
+ 'ch/boye/httpclientandroidlib/entity/mime/MIME.java',
+ 'ch/boye/httpclientandroidlib/entity/mime/MinimalField.java',
+ 'ch/boye/httpclientandroidlib/entity/mime/MultipartEntityBuilder.java',
+ 'ch/boye/httpclientandroidlib/entity/mime/MultipartFormEntity.java',
+ 'ch/boye/httpclientandroidlib/entity/mime/package-info.java',
+ 'ch/boye/httpclientandroidlib/entity/package-info.java',
+ 'ch/boye/httpclientandroidlib/entity/SerializableEntity.java',
+ 'ch/boye/httpclientandroidlib/entity/StringEntity.java',
+ 'ch/boye/httpclientandroidlib/FormattedHeader.java',
+ 'ch/boye/httpclientandroidlib/Header.java',
+ 'ch/boye/httpclientandroidlib/HeaderElement.java',
+ 'ch/boye/httpclientandroidlib/HeaderElementIterator.java',
+ 'ch/boye/httpclientandroidlib/HeaderIterator.java',
+ 'ch/boye/httpclientandroidlib/HttpClientConnection.java',
+ 'ch/boye/httpclientandroidlib/HttpConnection.java',
+ 'ch/boye/httpclientandroidlib/HttpConnectionFactory.java',
+ 'ch/boye/httpclientandroidlib/HttpConnectionMetrics.java',
+ 'ch/boye/httpclientandroidlib/HttpEntity.java',
+ 'ch/boye/httpclientandroidlib/HttpEntityEnclosingRequest.java',
+ 'ch/boye/httpclientandroidlib/HttpException.java',
+ 'ch/boye/httpclientandroidlib/HttpHeaders.java',
+ 'ch/boye/httpclientandroidlib/HttpHost.java',
+ 'ch/boye/httpclientandroidlib/HttpInetConnection.java',
+ 'ch/boye/httpclientandroidlib/HttpMessage.java',
+ 'ch/boye/httpclientandroidlib/HttpRequest.java',
+ 'ch/boye/httpclientandroidlib/HttpRequestFactory.java',
+ 'ch/boye/httpclientandroidlib/HttpRequestInterceptor.java',
+ 'ch/boye/httpclientandroidlib/HttpResponse.java',
+ 'ch/boye/httpclientandroidlib/HttpResponseFactory.java',
+ 'ch/boye/httpclientandroidlib/HttpResponseInterceptor.java',
+ 'ch/boye/httpclientandroidlib/HttpServerConnection.java',
+ 'ch/boye/httpclientandroidlib/HttpStatus.java',
+ 'ch/boye/httpclientandroidlib/HttpVersion.java',
+ 'ch/boye/httpclientandroidlib/impl/AbstractHttpClientConnection.java',
+ 'ch/boye/httpclientandroidlib/impl/AbstractHttpServerConnection.java',
+ 'ch/boye/httpclientandroidlib/impl/auth/AuthSchemeBase.java',
+ 'ch/boye/httpclientandroidlib/impl/auth/BasicScheme.java',
+ 'ch/boye/httpclientandroidlib/impl/auth/BasicSchemeFactory.java',
+ 'ch/boye/httpclientandroidlib/impl/auth/DigestScheme.java',
+ 'ch/boye/httpclientandroidlib/impl/auth/DigestSchemeFactory.java',
+ 'ch/boye/httpclientandroidlib/impl/auth/HttpAuthenticator.java',
+ 'ch/boye/httpclientandroidlib/impl/auth/HttpEntityDigester.java',
+ 'ch/boye/httpclientandroidlib/impl/auth/NTLMEngine.java',
+ 'ch/boye/httpclientandroidlib/impl/auth/NTLMEngineException.java',
+ 'ch/boye/httpclientandroidlib/impl/auth/NTLMEngineImpl.java',
+ 'ch/boye/httpclientandroidlib/impl/auth/NTLMScheme.java',
+ 'ch/boye/httpclientandroidlib/impl/auth/NTLMSchemeFactory.java',
+ 'ch/boye/httpclientandroidlib/impl/auth/package-info.java',
+ 'ch/boye/httpclientandroidlib/impl/auth/RFC2617Scheme.java',
+ 'ch/boye/httpclientandroidlib/impl/auth/SpnegoTokenGenerator.java',
+ 'ch/boye/httpclientandroidlib/impl/auth/UnsupportedDigestAlgorithmException.java',
+ 'ch/boye/httpclientandroidlib/impl/BHttpConnectionBase.java',
+ 'ch/boye/httpclientandroidlib/impl/client/AbstractAuthenticationHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/client/AbstractHttpClient.java',
+ 'ch/boye/httpclientandroidlib/impl/client/AIMDBackoffManager.java',
+ 'ch/boye/httpclientandroidlib/impl/client/AuthenticationStrategyAdaptor.java',
+ 'ch/boye/httpclientandroidlib/impl/client/AuthenticationStrategyImpl.java',
+ 'ch/boye/httpclientandroidlib/impl/client/AutoRetryHttpClient.java',
+ 'ch/boye/httpclientandroidlib/impl/client/BasicAuthCache.java',
+ 'ch/boye/httpclientandroidlib/impl/client/BasicCookieStore.java',
+ 'ch/boye/httpclientandroidlib/impl/client/BasicCredentialsProvider.java',
+ 'ch/boye/httpclientandroidlib/impl/client/BasicResponseHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/AsynchronousValidationRequest.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/AsynchronousValidator.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/BasicHttpCache.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/BasicHttpCacheStorage.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/BasicIdGenerator.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/CacheableRequestPolicy.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/CacheConfig.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/CachedHttpResponseGenerator.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/CachedResponseSuitabilityChecker.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/CacheEntity.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/CacheEntryUpdater.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/CacheInvalidator.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/CacheKeyGenerator.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/CacheMap.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/CacheValidityPolicy.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/CachingExec.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/CachingHttpClientBuilder.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/CachingHttpClients.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/CombinedEntity.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/ConditionalRequestBuilder.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/DefaultFailureCache.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/DefaultHttpCacheEntrySerializer.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/ExponentialBackOffSchedulingStrategy.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/FailureCache.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/FailureCacheValue.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/FileResource.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/FileResourceFactory.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/HeapResource.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/HeapResourceFactory.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/HttpCache.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/ImmediateSchedulingStrategy.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/IOUtils.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/ManagedHttpCacheStorage.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/OptionsHttp11Response.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/Proxies.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/RequestProtocolCompliance.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/RequestProtocolError.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/ResourceReference.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/ResponseCachingPolicy.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/ResponseProtocolCompliance.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/ResponseProxyHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/SchedulingStrategy.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/SizeLimitedResponseReader.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/Variant.java',
+ 'ch/boye/httpclientandroidlib/impl/client/cache/WarningValue.java',
+ 'ch/boye/httpclientandroidlib/impl/client/ClientParamsStack.java',
+ 'ch/boye/httpclientandroidlib/impl/client/Clock.java',
+ 'ch/boye/httpclientandroidlib/impl/client/CloseableHttpClient.java',
+ 'ch/boye/httpclientandroidlib/impl/client/CloseableHttpResponseProxy.java',
+ 'ch/boye/httpclientandroidlib/impl/client/ContentEncodingHttpClient.java',
+ 'ch/boye/httpclientandroidlib/impl/client/DecompressingHttpClient.java',
+ 'ch/boye/httpclientandroidlib/impl/client/DefaultBackoffStrategy.java',
+ 'ch/boye/httpclientandroidlib/impl/client/DefaultConnectionKeepAliveStrategy.java',
+ 'ch/boye/httpclientandroidlib/impl/client/DefaultHttpClient.java',
+ 'ch/boye/httpclientandroidlib/impl/client/DefaultHttpRequestRetryHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/client/DefaultProxyAuthenticationHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/client/DefaultRedirectHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/client/DefaultRedirectStrategy.java',
+ 'ch/boye/httpclientandroidlib/impl/client/DefaultRedirectStrategyAdaptor.java',
+ 'ch/boye/httpclientandroidlib/impl/client/DefaultRequestDirector.java',
+ 'ch/boye/httpclientandroidlib/impl/client/DefaultServiceUnavailableRetryStrategy.java',
+ 'ch/boye/httpclientandroidlib/impl/client/DefaultTargetAuthenticationHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/client/DefaultUserTokenHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/client/EntityEnclosingRequestWrapper.java',
+ 'ch/boye/httpclientandroidlib/impl/client/FutureRequestExecutionMetrics.java',
+ 'ch/boye/httpclientandroidlib/impl/client/FutureRequestExecutionService.java',
+ 'ch/boye/httpclientandroidlib/impl/client/HttpAuthenticator.java',
+ 'ch/boye/httpclientandroidlib/impl/client/HttpClientBuilder.java',
+ 'ch/boye/httpclientandroidlib/impl/client/HttpClients.java',
+ 'ch/boye/httpclientandroidlib/impl/client/HttpRequestFutureTask.java',
+ 'ch/boye/httpclientandroidlib/impl/client/HttpRequestTaskCallable.java',
+ 'ch/boye/httpclientandroidlib/impl/client/InternalHttpClient.java',
+ 'ch/boye/httpclientandroidlib/impl/client/LaxRedirectStrategy.java',
+ 'ch/boye/httpclientandroidlib/impl/client/MinimalHttpClient.java',
+ 'ch/boye/httpclientandroidlib/impl/client/NoopUserTokenHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/client/NullBackoffStrategy.java',
+ 'ch/boye/httpclientandroidlib/impl/client/package-info.java',
+ 'ch/boye/httpclientandroidlib/impl/client/ProxyAuthenticationStrategy.java',
+ 'ch/boye/httpclientandroidlib/impl/client/ProxyClient.java',
+ 'ch/boye/httpclientandroidlib/impl/client/RedirectLocations.java',
+ 'ch/boye/httpclientandroidlib/impl/client/RequestWrapper.java',
+ 'ch/boye/httpclientandroidlib/impl/client/RoutedRequest.java',
+ 'ch/boye/httpclientandroidlib/impl/client/StandardHttpRequestRetryHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/client/SystemClock.java',
+ 'ch/boye/httpclientandroidlib/impl/client/SystemDefaultCredentialsProvider.java',
+ 'ch/boye/httpclientandroidlib/impl/client/SystemDefaultHttpClient.java',
+ 'ch/boye/httpclientandroidlib/impl/client/TargetAuthenticationStrategy.java',
+ 'ch/boye/httpclientandroidlib/impl/client/TunnelRefusedException.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/AbstractClientConnAdapter.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/AbstractPooledConnAdapter.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/AbstractPoolEntry.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/BasicClientConnectionManager.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/BasicHttpClientConnectionManager.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/ConnectionShutdownException.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/CPool.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/CPoolEntry.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/CPoolProxy.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/DefaultClientConnection.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/DefaultClientConnectionOperator.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/DefaultHttpResponseParser.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/DefaultHttpResponseParserFactory.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/DefaultHttpRoutePlanner.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/DefaultManagedHttpClientConnection.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/DefaultProxyRoutePlanner.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/DefaultResponseParser.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/DefaultRoutePlanner.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/DefaultSchemePortResolver.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/HttpClientConnectionOperator.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/HttpConnPool.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/HttpPoolEntry.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/IdleConnectionHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/InMemoryDnsResolver.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/LoggingInputStream.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/LoggingManagedHttpClientConnection.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/LoggingOutputStream.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/LoggingSessionInputBuffer.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/LoggingSessionOutputBuffer.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/ManagedClientConnectionImpl.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/ManagedHttpClientConnectionFactory.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/package-info.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/PoolingClientConnectionManager.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/PoolingHttpClientConnectionManager.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/ProxySelectorRoutePlanner.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/SchemeRegistryFactory.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/SingleClientConnManager.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/SystemDefaultDnsResolver.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/SystemDefaultRoutePlanner.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/tsccm/AbstractConnPool.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/tsccm/BasicPooledConnAdapter.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/tsccm/BasicPoolEntry.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/tsccm/BasicPoolEntryRef.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/tsccm/ConnPoolByRoute.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/tsccm/package-info.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/tsccm/PoolEntryRequest.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/tsccm/RouteSpecificPool.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/tsccm/ThreadSafeClientConnManager.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/tsccm/WaitingThread.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/tsccm/WaitingThreadAborter.java',
+ 'ch/boye/httpclientandroidlib/impl/conn/Wire.java',
+ 'ch/boye/httpclientandroidlib/impl/ConnSupport.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/AbstractCookieAttributeHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/AbstractCookieSpec.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/BasicClientCookie.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/BasicClientCookie2.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/BasicCommentHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/BasicDomainHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/BasicExpiresHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/BasicMaxAgeHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/BasicPathHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/BasicSecureHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/BestMatchSpec.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/BestMatchSpecFactory.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/BrowserCompatSpec.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/BrowserCompatSpecFactory.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/BrowserCompatVersionAttributeHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/CookieSpecBase.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/DateParseException.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/DateUtils.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/IgnoreSpec.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/IgnoreSpecFactory.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/NetscapeDomainHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/NetscapeDraftHeaderParser.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/NetscapeDraftSpec.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/NetscapeDraftSpecFactory.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/package-info.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/PublicSuffixFilter.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/PublicSuffixListParser.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/RFC2109DomainHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/RFC2109Spec.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/RFC2109SpecFactory.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/RFC2109VersionHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/RFC2965CommentUrlAttributeHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/RFC2965DiscardAttributeHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/RFC2965DomainAttributeHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/RFC2965PortAttributeHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/RFC2965Spec.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/RFC2965SpecFactory.java',
+ 'ch/boye/httpclientandroidlib/impl/cookie/RFC2965VersionAttributeHandler.java',
+ 'ch/boye/httpclientandroidlib/impl/DefaultBHttpClientConnection.java',
+ 'ch/boye/httpclientandroidlib/impl/DefaultBHttpClientConnectionFactory.java',
+ 'ch/boye/httpclientandroidlib/impl/DefaultBHttpServerConnection.java',
+ 'ch/boye/httpclientandroidlib/impl/DefaultBHttpServerConnectionFactory.java',
+ 'ch/boye/httpclientandroidlib/impl/DefaultConnectionReuseStrategy.java',
+ 'ch/boye/httpclientandroidlib/impl/DefaultHttpClientConnection.java',
+ 'ch/boye/httpclientandroidlib/impl/DefaultHttpRequestFactory.java',
+ 'ch/boye/httpclientandroidlib/impl/DefaultHttpResponseFactory.java',
+ 'ch/boye/httpclientandroidlib/impl/DefaultHttpServerConnection.java',
+ 'ch/boye/httpclientandroidlib/impl/EnglishReasonPhraseCatalog.java',
+ 'ch/boye/httpclientandroidlib/impl/entity/DisallowIdentityContentLengthStrategy.java',
+ 'ch/boye/httpclientandroidlib/impl/entity/EntityDeserializer.java',
+ 'ch/boye/httpclientandroidlib/impl/entity/EntitySerializer.java',
+ 'ch/boye/httpclientandroidlib/impl/entity/LaxContentLengthStrategy.java',
+ 'ch/boye/httpclientandroidlib/impl/entity/package-info.java',
+ 'ch/boye/httpclientandroidlib/impl/entity/StrictContentLengthStrategy.java',
+ 'ch/boye/httpclientandroidlib/impl/execchain/BackoffStrategyExec.java',
+ 'ch/boye/httpclientandroidlib/impl/execchain/ClientExecChain.java',
+ 'ch/boye/httpclientandroidlib/impl/execchain/ConnectionHolder.java',
+ 'ch/boye/httpclientandroidlib/impl/execchain/HttpResponseProxy.java',
+ 'ch/boye/httpclientandroidlib/impl/execchain/MainClientExec.java',
+ 'ch/boye/httpclientandroidlib/impl/execchain/MinimalClientExec.java',
+ 'ch/boye/httpclientandroidlib/impl/execchain/package-info.java',
+ 'ch/boye/httpclientandroidlib/impl/execchain/ProtocolExec.java',
+ 'ch/boye/httpclientandroidlib/impl/execchain/RedirectExec.java',
+ 'ch/boye/httpclientandroidlib/impl/execchain/RequestAbortedException.java',
+ 'ch/boye/httpclientandroidlib/impl/execchain/RequestEntityProxy.java',
+ 'ch/boye/httpclientandroidlib/impl/execchain/ResponseEntityProxy.java',
+ 'ch/boye/httpclientandroidlib/impl/execchain/RetryExec.java',
+ 'ch/boye/httpclientandroidlib/impl/execchain/ServiceUnavailableRetryExec.java',
+ 'ch/boye/httpclientandroidlib/impl/execchain/TunnelRefusedException.java',
+ 'ch/boye/httpclientandroidlib/impl/HttpConnectionMetricsImpl.java',
+ 'ch/boye/httpclientandroidlib/impl/io/AbstractMessageParser.java',
+ 'ch/boye/httpclientandroidlib/impl/io/AbstractMessageWriter.java',
+ 'ch/boye/httpclientandroidlib/impl/io/AbstractSessionInputBuffer.java',
+ 'ch/boye/httpclientandroidlib/impl/io/AbstractSessionOutputBuffer.java',
+ 'ch/boye/httpclientandroidlib/impl/io/ChunkedInputStream.java',
+ 'ch/boye/httpclientandroidlib/impl/io/ChunkedOutputStream.java',
+ 'ch/boye/httpclientandroidlib/impl/io/ContentLengthInputStream.java',
+ 'ch/boye/httpclientandroidlib/impl/io/ContentLengthOutputStream.java',
+ 'ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestParser.java',
+ 'ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestParserFactory.java',
+ 'ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestWriter.java',
+ 'ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestWriterFactory.java',
+ 'ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseParser.java',
+ 'ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseParserFactory.java',
+ 'ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseWriter.java',
+ 'ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseWriterFactory.java',
+ 'ch/boye/httpclientandroidlib/impl/io/HttpRequestParser.java',
+ 'ch/boye/httpclientandroidlib/impl/io/HttpRequestWriter.java',
+ 'ch/boye/httpclientandroidlib/impl/io/HttpResponseParser.java',
+ 'ch/boye/httpclientandroidlib/impl/io/HttpResponseWriter.java',
+ 'ch/boye/httpclientandroidlib/impl/io/HttpTransportMetricsImpl.java',
+ 'ch/boye/httpclientandroidlib/impl/io/IdentityInputStream.java',
+ 'ch/boye/httpclientandroidlib/impl/io/IdentityOutputStream.java',
+ 'ch/boye/httpclientandroidlib/impl/io/package-info.java',
+ 'ch/boye/httpclientandroidlib/impl/io/SessionInputBufferImpl.java',
+ 'ch/boye/httpclientandroidlib/impl/io/SessionOutputBufferImpl.java',
+ 'ch/boye/httpclientandroidlib/impl/io/SocketInputBuffer.java',
+ 'ch/boye/httpclientandroidlib/impl/io/SocketOutputBuffer.java',
+ 'ch/boye/httpclientandroidlib/impl/NoConnectionReuseStrategy.java',
+ 'ch/boye/httpclientandroidlib/impl/package-info.java',
+ 'ch/boye/httpclientandroidlib/impl/pool/BasicConnFactory.java',
+ 'ch/boye/httpclientandroidlib/impl/pool/BasicConnPool.java',
+ 'ch/boye/httpclientandroidlib/impl/pool/BasicPoolEntry.java',
+ 'ch/boye/httpclientandroidlib/impl/pool/package-info.java',
+ 'ch/boye/httpclientandroidlib/impl/SocketHttpClientConnection.java',
+ 'ch/boye/httpclientandroidlib/impl/SocketHttpServerConnection.java',
+ 'ch/boye/httpclientandroidlib/io/BufferInfo.java',
+ 'ch/boye/httpclientandroidlib/io/EofSensor.java',
+ 'ch/boye/httpclientandroidlib/io/HttpMessageParser.java',
+ 'ch/boye/httpclientandroidlib/io/HttpMessageParserFactory.java',
+ 'ch/boye/httpclientandroidlib/io/HttpMessageWriter.java',
+ 'ch/boye/httpclientandroidlib/io/HttpMessageWriterFactory.java',
+ 'ch/boye/httpclientandroidlib/io/HttpTransportMetrics.java',
+ 'ch/boye/httpclientandroidlib/io/package-info.java',
+ 'ch/boye/httpclientandroidlib/io/SessionInputBuffer.java',
+ 'ch/boye/httpclientandroidlib/io/SessionOutputBuffer.java',
+ 'ch/boye/httpclientandroidlib/MalformedChunkCodingException.java',
+ 'ch/boye/httpclientandroidlib/message/AbstractHttpMessage.java',
+ 'ch/boye/httpclientandroidlib/message/BasicHeader.java',
+ 'ch/boye/httpclientandroidlib/message/BasicHeaderElement.java',
+ 'ch/boye/httpclientandroidlib/message/BasicHeaderElementIterator.java',
+ 'ch/boye/httpclientandroidlib/message/BasicHeaderIterator.java',
+ 'ch/boye/httpclientandroidlib/message/BasicHeaderValueFormatter.java',
+ 'ch/boye/httpclientandroidlib/message/BasicHeaderValueParser.java',
+ 'ch/boye/httpclientandroidlib/message/BasicHttpEntityEnclosingRequest.java',
+ 'ch/boye/httpclientandroidlib/message/BasicHttpRequest.java',
+ 'ch/boye/httpclientandroidlib/message/BasicHttpResponse.java',
+ 'ch/boye/httpclientandroidlib/message/BasicLineFormatter.java',
+ 'ch/boye/httpclientandroidlib/message/BasicLineParser.java',
+ 'ch/boye/httpclientandroidlib/message/BasicListHeaderIterator.java',
+ 'ch/boye/httpclientandroidlib/message/BasicNameValuePair.java',
+ 'ch/boye/httpclientandroidlib/message/BasicRequestLine.java',
+ 'ch/boye/httpclientandroidlib/message/BasicStatusLine.java',
+ 'ch/boye/httpclientandroidlib/message/BasicTokenIterator.java',
+ 'ch/boye/httpclientandroidlib/message/BufferedHeader.java',
+ 'ch/boye/httpclientandroidlib/message/HeaderGroup.java',
+ 'ch/boye/httpclientandroidlib/message/HeaderValueFormatter.java',
+ 'ch/boye/httpclientandroidlib/message/HeaderValueParser.java',
+ 'ch/boye/httpclientandroidlib/message/LineFormatter.java',
+ 'ch/boye/httpclientandroidlib/message/LineParser.java',
+ 'ch/boye/httpclientandroidlib/message/package-info.java',
+ 'ch/boye/httpclientandroidlib/message/ParserCursor.java',
+ 'ch/boye/httpclientandroidlib/MessageConstraintException.java',
+ 'ch/boye/httpclientandroidlib/MethodNotSupportedException.java',
+ 'ch/boye/httpclientandroidlib/NameValuePair.java',
+ 'ch/boye/httpclientandroidlib/NoHttpResponseException.java',
+ 'ch/boye/httpclientandroidlib/package-info.java',
+ 'ch/boye/httpclientandroidlib/params/AbstractHttpParams.java',
+ 'ch/boye/httpclientandroidlib/params/BasicHttpParams.java',
+ 'ch/boye/httpclientandroidlib/params/CoreConnectionPNames.java',
+ 'ch/boye/httpclientandroidlib/params/CoreProtocolPNames.java',
+ 'ch/boye/httpclientandroidlib/params/DefaultedHttpParams.java',
+ 'ch/boye/httpclientandroidlib/params/HttpAbstractParamBean.java',
+ 'ch/boye/httpclientandroidlib/params/HttpConnectionParamBean.java',
+ 'ch/boye/httpclientandroidlib/params/HttpConnectionParams.java',
+ 'ch/boye/httpclientandroidlib/params/HttpParamConfig.java',
+ 'ch/boye/httpclientandroidlib/params/HttpParams.java',
+ 'ch/boye/httpclientandroidlib/params/HttpParamsNames.java',
+ 'ch/boye/httpclientandroidlib/params/HttpProtocolParamBean.java',
+ 'ch/boye/httpclientandroidlib/params/HttpProtocolParams.java',
+ 'ch/boye/httpclientandroidlib/params/package-info.java',
+ 'ch/boye/httpclientandroidlib/params/SyncBasicHttpParams.java',
+ 'ch/boye/httpclientandroidlib/ParseException.java',
+ 'ch/boye/httpclientandroidlib/pool/AbstractConnPool.java',
+ 'ch/boye/httpclientandroidlib/pool/ConnFactory.java',
+ 'ch/boye/httpclientandroidlib/pool/ConnPool.java',
+ 'ch/boye/httpclientandroidlib/pool/ConnPoolControl.java',
+ 'ch/boye/httpclientandroidlib/pool/package-info.java',
+ 'ch/boye/httpclientandroidlib/pool/PoolEntry.java',
+ 'ch/boye/httpclientandroidlib/pool/PoolEntryCallback.java',
+ 'ch/boye/httpclientandroidlib/pool/PoolEntryFuture.java',
+ 'ch/boye/httpclientandroidlib/pool/PoolStats.java',
+ 'ch/boye/httpclientandroidlib/pool/RouteSpecificPool.java',
+ 'ch/boye/httpclientandroidlib/protocol/BasicHttpContext.java',
+ 'ch/boye/httpclientandroidlib/protocol/BasicHttpProcessor.java',
+ 'ch/boye/httpclientandroidlib/protocol/ChainBuilder.java',
+ 'ch/boye/httpclientandroidlib/protocol/DefaultedHttpContext.java',
+ 'ch/boye/httpclientandroidlib/protocol/ExecutionContext.java',
+ 'ch/boye/httpclientandroidlib/protocol/HTTP.java',
+ 'ch/boye/httpclientandroidlib/protocol/HttpContext.java',
+ 'ch/boye/httpclientandroidlib/protocol/HttpCoreContext.java',
+ 'ch/boye/httpclientandroidlib/protocol/HttpDateGenerator.java',
+ 'ch/boye/httpclientandroidlib/protocol/HttpExpectationVerifier.java',
+ 'ch/boye/httpclientandroidlib/protocol/HttpProcessor.java',
+ 'ch/boye/httpclientandroidlib/protocol/HttpProcessorBuilder.java',
+ 'ch/boye/httpclientandroidlib/protocol/HttpRequestExecutor.java',
+ 'ch/boye/httpclientandroidlib/protocol/HttpRequestHandler.java',
+ 'ch/boye/httpclientandroidlib/protocol/HttpRequestHandlerMapper.java',
+ 'ch/boye/httpclientandroidlib/protocol/HttpRequestHandlerRegistry.java',
+ 'ch/boye/httpclientandroidlib/protocol/HttpRequestHandlerResolver.java',
+ 'ch/boye/httpclientandroidlib/protocol/HttpRequestInterceptorList.java',
+ 'ch/boye/httpclientandroidlib/protocol/HttpResponseInterceptorList.java',
+ 'ch/boye/httpclientandroidlib/protocol/HttpService.java',
+ 'ch/boye/httpclientandroidlib/protocol/ImmutableHttpProcessor.java',
+ 'ch/boye/httpclientandroidlib/protocol/package-info.java',
+ 'ch/boye/httpclientandroidlib/protocol/RequestConnControl.java',
+ 'ch/boye/httpclientandroidlib/protocol/RequestContent.java',
+ 'ch/boye/httpclientandroidlib/protocol/RequestDate.java',
+ 'ch/boye/httpclientandroidlib/protocol/RequestExpectContinue.java',
+ 'ch/boye/httpclientandroidlib/protocol/RequestTargetHost.java',
+ 'ch/boye/httpclientandroidlib/protocol/RequestUserAgent.java',
+ 'ch/boye/httpclientandroidlib/protocol/ResponseConnControl.java',
+ 'ch/boye/httpclientandroidlib/protocol/ResponseContent.java',
+ 'ch/boye/httpclientandroidlib/protocol/ResponseDate.java',
+ 'ch/boye/httpclientandroidlib/protocol/ResponseServer.java',
+ 'ch/boye/httpclientandroidlib/protocol/SyncBasicHttpContext.java',
+ 'ch/boye/httpclientandroidlib/protocol/UriHttpRequestHandlerMapper.java',
+ 'ch/boye/httpclientandroidlib/protocol/UriPatternMatcher.java',
+ 'ch/boye/httpclientandroidlib/ProtocolException.java',
+ 'ch/boye/httpclientandroidlib/ProtocolVersion.java',
+ 'ch/boye/httpclientandroidlib/ReasonPhraseCatalog.java',
+ 'ch/boye/httpclientandroidlib/RequestLine.java',
+ 'ch/boye/httpclientandroidlib/StatusLine.java',
+ 'ch/boye/httpclientandroidlib/TokenIterator.java',
+ 'ch/boye/httpclientandroidlib/TruncatedChunkException.java',
+ 'ch/boye/httpclientandroidlib/UnsupportedHttpVersionException.java',
+ 'ch/boye/httpclientandroidlib/util/Args.java',
+ 'ch/boye/httpclientandroidlib/util/Asserts.java',
+ 'ch/boye/httpclientandroidlib/util/ByteArrayBuffer.java',
+ 'ch/boye/httpclientandroidlib/util/CharArrayBuffer.java',
+ 'ch/boye/httpclientandroidlib/util/CharsetUtils.java',
+ 'ch/boye/httpclientandroidlib/util/EncodingUtils.java',
+ 'ch/boye/httpclientandroidlib/util/EntityUtils.java',
+ 'ch/boye/httpclientandroidlib/util/ExceptionUtils.java',
+ 'ch/boye/httpclientandroidlib/util/LangUtils.java',
+ 'ch/boye/httpclientandroidlib/util/NetUtils.java',
+ 'ch/boye/httpclientandroidlib/util/package-info.java',
+ 'ch/boye/httpclientandroidlib/util/TextUtils.java',
+ 'ch/boye/httpclientandroidlib/util/VersionInfo.java',
+ 'org/json/simple/ItemList.java',
+ 'org/json/simple/JSONArray.java',
+ 'org/json/simple/JSONAware.java',
+ 'org/json/simple/JSONObject.java',
+ 'org/json/simple/JSONStreamAware.java',
+ 'org/json/simple/JSONValue.java',
+ 'org/json/simple/parser/ContainerFactory.java',
+ 'org/json/simple/parser/ContentHandler.java',
+ 'org/json/simple/parser/JSONParser.java',
+ 'org/json/simple/parser/ParseException.java',
+ 'org/json/simple/parser/Yylex.java',
+ 'org/json/simple/parser/Yytoken.java',
+ 'org/mozilla/apache/commons/codec/binary/Base32.java',
+ 'org/mozilla/apache/commons/codec/binary/Base32InputStream.java',
+ 'org/mozilla/apache/commons/codec/binary/Base32OutputStream.java',
+ 'org/mozilla/apache/commons/codec/binary/Base64.java',
+ 'org/mozilla/apache/commons/codec/binary/Base64InputStream.java',
+ 'org/mozilla/apache/commons/codec/binary/Base64OutputStream.java',
+ 'org/mozilla/apache/commons/codec/binary/BaseNCodec.java',
+ 'org/mozilla/apache/commons/codec/binary/BaseNCodecInputStream.java',
+ 'org/mozilla/apache/commons/codec/binary/BaseNCodecOutputStream.java',
+ 'org/mozilla/apache/commons/codec/binary/BinaryCodec.java',
+ 'org/mozilla/apache/commons/codec/binary/Hex.java',
+ 'org/mozilla/apache/commons/codec/binary/StringUtils.java',
+ 'org/mozilla/apache/commons/codec/BinaryDecoder.java',
+ 'org/mozilla/apache/commons/codec/BinaryEncoder.java',
+ 'org/mozilla/apache/commons/codec/CharEncoding.java',
+ 'org/mozilla/apache/commons/codec/Decoder.java',
+ 'org/mozilla/apache/commons/codec/DecoderException.java',
+ 'org/mozilla/apache/commons/codec/digest/DigestUtils.java',
+ 'org/mozilla/apache/commons/codec/Encoder.java',
+ 'org/mozilla/apache/commons/codec/EncoderException.java',
+ 'org/mozilla/apache/commons/codec/language/AbstractCaverphone.java',
+ 'org/mozilla/apache/commons/codec/language/Caverphone.java',
+ 'org/mozilla/apache/commons/codec/language/Caverphone1.java',
+ 'org/mozilla/apache/commons/codec/language/Caverphone2.java',
+ 'org/mozilla/apache/commons/codec/language/ColognePhonetic.java',
+ 'org/mozilla/apache/commons/codec/language/DoubleMetaphone.java',
+ 'org/mozilla/apache/commons/codec/language/Metaphone.java',
+ 'org/mozilla/apache/commons/codec/language/RefinedSoundex.java',
+ 'org/mozilla/apache/commons/codec/language/Soundex.java',
+ 'org/mozilla/apache/commons/codec/language/SoundexUtils.java',
+ 'org/mozilla/apache/commons/codec/net/BCodec.java',
+ 'org/mozilla/apache/commons/codec/net/QCodec.java',
+ 'org/mozilla/apache/commons/codec/net/QuotedPrintableCodec.java',
+ 'org/mozilla/apache/commons/codec/net/RFC1522Codec.java',
+ 'org/mozilla/apache/commons/codec/net/URLCodec.java',
+ 'org/mozilla/apache/commons/codec/net/Utils.java',
+ 'org/mozilla/apache/commons/codec/StringDecoder.java',
+ 'org/mozilla/apache/commons/codec/StringEncoder.java',
+ 'org/mozilla/apache/commons/codec/StringEncoderComparator.java',
+]
+
+sync_java_files = [TOPSRCDIR + '/mobile/android/services/src/main/java/org/mozilla/gecko/' + x for x in [
+ 'background/common/EditorBranch.java',
+ 'background/common/GlobalConstants.java',
+ 'background/common/log/Logger.java',
+ 'background/common/log/writers/AndroidLevelCachingLogWriter.java',
+ 'background/common/log/writers/AndroidLogWriter.java',
+ 'background/common/log/writers/LevelFilteringLogWriter.java',
+ 'background/common/log/writers/LogWriter.java',
+ 'background/common/log/writers/PrintLogWriter.java',
+ 'background/common/log/writers/SimpleTagLogWriter.java',
+ 'background/common/log/writers/StringLogWriter.java',
+ 'background/common/log/writers/TagLogWriter.java',
+ 'background/common/log/writers/ThreadLocalTagLogWriter.java',
+ 'background/common/PrefsBranch.java',
+ 'background/common/telemetry/TelemetryWrapper.java',
+ 'background/db/CursorDumper.java',
+ 'background/db/Tab.java',
+ 'background/fxa/FxAccount20CreateDelegate.java',
+ 'background/fxa/FxAccount20LoginDelegate.java',
+ 'background/fxa/FxAccountClient.java',
+ 'background/fxa/FxAccountClient20.java',
+ 'background/fxa/FxAccountClientException.java',
+ 'background/fxa/FxAccountRemoteError.java',
+ 'background/fxa/FxAccountUtils.java',
+ 'background/fxa/oauth/FxAccountAbstractClient.java',
+ 'background/fxa/oauth/FxAccountAbstractClientException.java',
+ 'background/fxa/oauth/FxAccountOAuthClient10.java',
+ 'background/fxa/oauth/FxAccountOAuthRemoteError.java',
+ 'background/fxa/PasswordStretcher.java',
+ 'background/fxa/profile/FxAccountProfileClient10.java',
+ 'background/fxa/QuickPasswordStretcher.java',
+ 'background/fxa/SkewHandler.java',
+ 'background/nativecode/NativeCrypto.java',
+ 'background/preferences/PreferenceFragment.java',
+ 'background/preferences/PreferenceManagerCompat.java',
+ 'background/ReadingListConstants.java',
+ 'browserid/ASNUtils.java',
+ 'browserid/BrowserIDKeyPair.java',
+ 'browserid/DSACryptoImplementation.java',
+ 'browserid/JSONWebTokenUtils.java',
+ 'browserid/MockMyIDTokenFactory.java',
+ 'browserid/RSACryptoImplementation.java',
+ 'browserid/SigningPrivateKey.java',
+ 'browserid/verifier/AbstractBrowserIDRemoteVerifierClient.java',
+ 'browserid/verifier/BrowserIDRemoteVerifierClient10.java',
+ 'browserid/verifier/BrowserIDRemoteVerifierClient20.java',
+ 'browserid/verifier/BrowserIDVerifierClient.java',
+ 'browserid/verifier/BrowserIDVerifierDelegate.java',
+ 'browserid/verifier/BrowserIDVerifierException.java',
+ 'browserid/VerifyingPublicKey.java',
+ 'fxa/AccountLoader.java',
+ 'fxa/activities/CustomColorPreference.java',
+ 'fxa/activities/FxAccountAbstractActivity.java',
+ 'fxa/activities/FxAccountConfirmAccountActivityWeb.java',
+ 'fxa/activities/FxAccountFinishMigratingActivityWeb.java',
+ 'fxa/activities/FxAccountGetStartedActivityWeb.java',
+ 'fxa/activities/FxAccountStatusActivity.java',
+ 'fxa/activities/FxAccountStatusFragment.java',
+ 'fxa/activities/FxAccountUpdateCredentialsActivityWeb.java',
+ 'fxa/activities/FxAccountWebFlowActivity.java',
+ 'fxa/activities/PicassoPreferenceIconTarget.java',
+ 'fxa/authenticator/AccountPickler.java',
+ 'fxa/authenticator/AndroidFxAccount.java',
+ 'fxa/authenticator/FxAccountAuthenticator.java',
+ 'fxa/authenticator/FxAccountAuthenticatorService.java',
+ 'fxa/authenticator/FxAccountLoginDelegate.java',
+ 'fxa/authenticator/FxAccountLoginException.java',
+ 'fxa/authenticator/FxADefaultLoginStateMachineDelegate.java',
+ 'fxa/FirefoxAccounts.java',
+ 'fxa/FxAccountConstants.java',
+ 'fxa/FxAccountDevice.java',
+ 'fxa/FxAccountDeviceRegistrator.java',
+ 'fxa/FxAccountPushHandler.java',
+ 'fxa/login/BaseRequestDelegate.java',
+ 'fxa/login/Cohabiting.java',
+ 'fxa/login/Doghouse.java',
+ 'fxa/login/Engaged.java',
+ 'fxa/login/FxAccountLoginStateMachine.java',
+ 'fxa/login/FxAccountLoginTransition.java',
+ 'fxa/login/Married.java',
+ 'fxa/login/MigratedFromSync11.java',
+ 'fxa/login/Separated.java',
+ 'fxa/login/State.java',
+ 'fxa/login/StateFactory.java',
+ 'fxa/login/TokensAndKeysState.java',
+ 'fxa/receivers/FxAccountDeletedService.java',
+ 'fxa/receivers/FxAccountUpgradeReceiver.java',
+ 'fxa/sync/FxAccountNotificationManager.java',
+ 'fxa/sync/FxAccountProfileService.java',
+ 'fxa/sync/FxAccountSchedulePolicy.java',
+ 'fxa/sync/FxAccountSyncAdapter.java',
+ 'fxa/sync/FxAccountSyncDelegate.java',
+ 'fxa/sync/FxAccountSyncService.java',
+ 'fxa/sync/FxAccountSyncStatusHelper.java',
+ 'fxa/sync/SchedulePolicy.java',
+ 'fxa/SyncStatusListener.java',
+ 'push/autopush/AutopushClient.java',
+ 'push/autopush/AutopushClientException.java',
+ 'push/RegisterUserAgentResponse.java',
+ 'push/SubscribeChannelResponse.java',
+ 'sync/AlreadySyncingException.java',
+ 'sync/BackoffHandler.java',
+ 'sync/BadRequiredFieldJSONException.java',
+ 'sync/CollectionKeys.java',
+ 'sync/CommandProcessor.java',
+ 'sync/CommandRunner.java',
+ 'sync/CredentialException.java',
+ 'sync/crypto/CryptoException.java',
+ 'sync/crypto/CryptoInfo.java',
+ 'sync/crypto/HKDF.java',
+ 'sync/crypto/HMACVerificationException.java',
+ 'sync/crypto/KeyBundle.java',
+ 'sync/crypto/MissingCryptoInputException.java',
+ 'sync/crypto/NoKeyBundleException.java',
+ 'sync/crypto/PBKDF2.java',
+ 'sync/crypto/PersistedCrypto5Keys.java',
+ 'sync/CryptoRecord.java',
+ 'sync/DelayedWorkTracker.java',
+ 'sync/delegates/ClientsDataDelegate.java',
+ 'sync/delegates/FreshStartDelegate.java',
+ 'sync/delegates/GlobalSessionCallback.java',
+ 'sync/delegates/JSONRecordFetchDelegate.java',
+ 'sync/delegates/KeyUploadDelegate.java',
+ 'sync/delegates/MetaGlobalDelegate.java',
+ 'sync/delegates/WipeServerDelegate.java',
+ 'sync/EngineSettings.java',
+ 'sync/ExtendedJSONObject.java',
+ 'sync/GlobalSession.java',
+ 'sync/HTTPFailureException.java',
+ 'sync/InfoCollections.java',
+ 'sync/InfoConfiguration.java',
+ 'sync/InfoCounts.java',
+ 'sync/JSONRecordFetcher.java',
+ 'sync/KeyBundleProvider.java',
+ 'sync/MetaGlobal.java',
+ 'sync/MetaGlobalException.java',
+ 'sync/MetaGlobalMissingEnginesException.java',
+ 'sync/MetaGlobalNotSetException.java',
+ 'sync/middleware/Crypto5MiddlewareRepository.java',
+ 'sync/middleware/Crypto5MiddlewareRepositorySession.java',
+ 'sync/middleware/MiddlewareRepository.java',
+ 'sync/middleware/MiddlewareRepositorySession.java',
+ 'sync/net/AbstractBearerTokenAuthHeaderProvider.java',
+ 'sync/net/AuthHeaderProvider.java',
+ 'sync/net/BaseResource.java',
+ 'sync/net/BaseResourceDelegate.java',
+ 'sync/net/BasicAuthHeaderProvider.java',
+ 'sync/net/BearerAuthHeaderProvider.java',
+ 'sync/net/BrowserIDAuthHeaderProvider.java',
+ 'sync/net/ConnectionMonitorThread.java',
+ 'sync/net/GzipNonChunkedCompressingEntity.java',
+ 'sync/net/HandleProgressException.java',
+ 'sync/net/HawkAuthHeaderProvider.java',
+ 'sync/net/HMACAuthHeaderProvider.java',
+ 'sync/net/HttpResponseObserver.java',
+ 'sync/net/MozResponse.java',
+ 'sync/net/Resource.java',
+ 'sync/net/ResourceDelegate.java',
+ 'sync/net/SRPConstants.java',
+ 'sync/net/SyncResponse.java',
+ 'sync/net/SyncStorageCollectionRequest.java',
+ 'sync/net/SyncStorageCollectionRequestDelegate.java',
+ 'sync/net/SyncStorageRecordRequest.java',
+ 'sync/net/SyncStorageRequest.java',
+ 'sync/net/SyncStorageRequestDelegate.java',
+ 'sync/net/SyncStorageRequestIncrementalDelegate.java',
+ 'sync/net/SyncStorageResponse.java',
+ 'sync/net/TLSSocketFactory.java',
+ 'sync/net/WBOCollectionRequestDelegate.java',
+ 'sync/net/WBORequestDelegate.java',
+ 'sync/NoCollectionKeysSetException.java',
+ 'sync/NodeAuthenticationException.java',
+ 'sync/NonArrayJSONException.java',
+ 'sync/NonObjectJSONException.java',
+ 'sync/NullClusterURLException.java',
+ 'sync/PersistedMetaGlobal.java',
+ 'sync/PrefsBackoffHandler.java',
+ 'sync/repositories/android/AndroidBrowserBookmarksDataAccessor.java',
+ 'sync/repositories/android/AndroidBrowserBookmarksRepository.java',
+ 'sync/repositories/android/AndroidBrowserBookmarksRepositorySession.java',
+ 'sync/repositories/android/AndroidBrowserHistoryDataAccessor.java',
+ 'sync/repositories/android/AndroidBrowserHistoryRepository.java',
+ 'sync/repositories/android/AndroidBrowserHistoryRepositorySession.java',
+ 'sync/repositories/android/AndroidBrowserRepository.java',
+ 'sync/repositories/android/AndroidBrowserRepositoryDataAccessor.java',
+ 'sync/repositories/android/AndroidBrowserRepositorySession.java',
+ 'sync/repositories/android/BookmarksDeletionManager.java',
+ 'sync/repositories/android/BookmarksInsertionManager.java',
+ 'sync/repositories/android/BrowserContractHelpers.java',
+ 'sync/repositories/android/CachedSQLiteOpenHelper.java',
+ 'sync/repositories/android/ClientsDatabase.java',
+ 'sync/repositories/android/ClientsDatabaseAccessor.java',
+ 'sync/repositories/android/FennecTabsRepository.java',
+ 'sync/repositories/android/FormHistoryRepositorySession.java',
+ 'sync/repositories/android/PasswordsRepositorySession.java',
+ 'sync/repositories/android/RepoUtils.java',
+ 'sync/repositories/android/VisitsHelper.java',
+ 'sync/repositories/BookmarkNeedsReparentingException.java',
+ 'sync/repositories/BookmarksRepository.java',
+ 'sync/repositories/ConstrainedServer11Repository.java',
+ 'sync/repositories/delegates/DeferrableRepositorySessionCreationDelegate.java',
+ 'sync/repositories/delegates/DeferredRepositorySessionBeginDelegate.java',
+ 'sync/repositories/delegates/DeferredRepositorySessionFetchRecordsDelegate.java',
+ 'sync/repositories/delegates/DeferredRepositorySessionFinishDelegate.java',
+ 'sync/repositories/delegates/DeferredRepositorySessionStoreDelegate.java',
+ 'sync/repositories/delegates/RepositorySessionBeginDelegate.java',
+ 'sync/repositories/delegates/RepositorySessionCleanDelegate.java',
+ 'sync/repositories/delegates/RepositorySessionCreationDelegate.java',
+ 'sync/repositories/delegates/RepositorySessionFetchRecordsDelegate.java',
+ 'sync/repositories/delegates/RepositorySessionFinishDelegate.java',
+ 'sync/repositories/delegates/RepositorySessionGuidsSinceDelegate.java',
+ 'sync/repositories/delegates/RepositorySessionStoreDelegate.java',
+ 'sync/repositories/delegates/RepositorySessionWipeDelegate.java',
+ 'sync/repositories/domain/BookmarkRecord.java',
+ 'sync/repositories/domain/BookmarkRecordFactory.java',
+ 'sync/repositories/domain/ClientRecord.java',
+ 'sync/repositories/domain/ClientRecordFactory.java',
+ 'sync/repositories/domain/FormHistoryRecord.java',
+ 'sync/repositories/domain/HistoryRecord.java',
+ 'sync/repositories/domain/HistoryRecordFactory.java',
+ 'sync/repositories/domain/PasswordRecord.java',
+ 'sync/repositories/domain/PasswordRecordFactory.java',
+ 'sync/repositories/domain/Record.java',
+ 'sync/repositories/domain/RecordParseException.java',
+ 'sync/repositories/domain/TabsRecord.java',
+ 'sync/repositories/domain/TabsRecordFactory.java',
+ 'sync/repositories/domain/VersionConstants.java',
+ 'sync/repositories/downloaders/BatchingDownloader.java',
+ 'sync/repositories/downloaders/BatchingDownloaderDelegate.java',
+ 'sync/repositories/FetchFailedException.java',
+ 'sync/repositories/HashSetStoreTracker.java',
+ 'sync/repositories/HistoryRepository.java',
+ 'sync/repositories/IdentityRecordFactory.java',
+ 'sync/repositories/InactiveSessionException.java',
+ 'sync/repositories/InvalidBookmarkTypeException.java',
+ 'sync/repositories/InvalidRequestException.java',
+ 'sync/repositories/InvalidSessionTransitionException.java',
+ 'sync/repositories/MultipleRecordsForGuidException.java',
+ 'sync/repositories/NoContentProviderException.java',
+ 'sync/repositories/NoGuidForIdException.java',
+ 'sync/repositories/NoStoreDelegateException.java',
+ 'sync/repositories/NullCursorException.java',
+ 'sync/repositories/ParentNotFoundException.java',
+ 'sync/repositories/ProfileDatabaseException.java',
+ 'sync/repositories/RecordFactory.java',
+ 'sync/repositories/RecordFilter.java',
+ 'sync/repositories/Repository.java',
+ 'sync/repositories/RepositorySession.java',
+ 'sync/repositories/RepositorySessionBundle.java',
+ 'sync/repositories/Server11Repository.java',
+ 'sync/repositories/Server11RepositorySession.java',
+ 'sync/repositories/StoreFailedException.java',
+ 'sync/repositories/StoreTracker.java',
+ 'sync/repositories/StoreTrackingRepositorySession.java',
+ 'sync/repositories/uploaders/BatchingUploader.java',
+ 'sync/repositories/uploaders/BatchMeta.java',
+ 'sync/repositories/uploaders/BufferSizeTracker.java',
+ 'sync/repositories/uploaders/MayUploadProvider.java',
+ 'sync/repositories/uploaders/Payload.java',
+ 'sync/repositories/uploaders/PayloadUploadDelegate.java',
+ 'sync/repositories/uploaders/RecordUploadRunnable.java',
+ 'sync/Server11PreviousPostFailedException.java',
+ 'sync/Server11RecordPostFailedException.java',
+ 'sync/setup/activities/ActivityUtils.java',
+ 'sync/setup/activities/WebURLFinder.java',
+ 'sync/setup/Constants.java',
+ 'sync/setup/InvalidSyncKeyException.java',
+ 'sync/SharedPreferencesClientsDataDelegate.java',
+ 'sync/stage/AbstractNonRepositorySyncStage.java',
+ 'sync/stage/AbstractSessionManagingSyncStage.java',
+ 'sync/stage/AndroidBrowserBookmarksServerSyncStage.java',
+ 'sync/stage/AndroidBrowserHistoryServerSyncStage.java',
+ 'sync/stage/CheckPreconditionsStage.java',
+ 'sync/stage/CompletedStage.java',
+ 'sync/stage/EnsureCrypto5KeysStage.java',
+ 'sync/stage/FennecTabsServerSyncStage.java',
+ 'sync/stage/FetchInfoCollectionsStage.java',
+ 'sync/stage/FetchInfoConfigurationStage.java',
+ 'sync/stage/FetchMetaGlobalStage.java',
+ 'sync/stage/FormHistoryServerSyncStage.java',
+ 'sync/stage/GlobalSyncStage.java',
+ 'sync/stage/NoSuchStageException.java',
+ 'sync/stage/PasswordsServerSyncStage.java',
+ 'sync/stage/SafeConstrainedServer11Repository.java',
+ 'sync/stage/ServerSyncStage.java',
+ 'sync/stage/SyncClientsEngineStage.java',
+ 'sync/stage/UploadMetaGlobalStage.java',
+ 'sync/Sync11Configuration.java',
+ 'sync/SyncConfiguration.java',
+ 'sync/SyncConfigurationException.java',
+ 'sync/SyncConstants.java',
+ 'sync/SyncException.java',
+ 'sync/synchronizer/ConcurrentRecordConsumer.java',
+ 'sync/synchronizer/RecordConsumer.java',
+ 'sync/synchronizer/RecordsChannel.java',
+ 'sync/synchronizer/RecordsChannelDelegate.java',
+ 'sync/synchronizer/RecordsConsumerDelegate.java',
+ 'sync/synchronizer/SerialRecordConsumer.java',
+ 'sync/synchronizer/ServerLocalSynchronizer.java',
+ 'sync/synchronizer/ServerLocalSynchronizerSession.java',
+ 'sync/synchronizer/SessionNotBegunException.java',
+ 'sync/synchronizer/Synchronizer.java',
+ 'sync/synchronizer/SynchronizerDelegate.java',
+ 'sync/synchronizer/SynchronizerSession.java',
+ 'sync/synchronizer/SynchronizerSessionDelegate.java',
+ 'sync/synchronizer/UnbundleError.java',
+ 'sync/synchronizer/UnexpectedSessionException.java',
+ 'sync/SynchronizerConfiguration.java',
+ 'sync/telemetry/TelemetryContract.java',
+ 'sync/ThreadPool.java',
+ 'sync/UnexpectedJSONException.java',
+ 'sync/UnknownSynchronizerConfigurationVersionException.java',
+ 'sync/Utils.java',
+ 'tokenserver/TokenServerClient.java',
+ 'tokenserver/TokenServerClientDelegate.java',
+ 'tokenserver/TokenServerException.java',
+ 'tokenserver/TokenServerToken.java',
+ 'util/PRNGFixes.java',
+]]
diff --git a/mobile/android/base/crashreporter/res/drawable-mdpi/crash_reporter.png b/mobile/android/base/crashreporter/res/drawable-mdpi/crash_reporter.png
new file mode 100644
index 0000000000..c9f495d30b
--- /dev/null
+++ b/mobile/android/base/crashreporter/res/drawable-mdpi/crash_reporter.png
Binary files differ
diff --git a/mobile/android/base/crashreporter/res/drawable/textbox_bg.xml b/mobile/android/base/crashreporter/res/drawable/textbox_bg.xml
new file mode 100644
index 0000000000..bcc884a08c
--- /dev/null
+++ b/mobile/android/base/crashreporter/res/drawable/textbox_bg.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_enabled="true">
+ <shape>
+ <solid android:color="@color/textbox_background" />
+ <stroke android:width="1dp" android:color="@color/textbox_stroke" />
+ </shape>
+ </item>
+
+ <item android:state_enabled="false">
+ <shape>
+ <solid android:color="@color/textbox_background_disabled" />
+ <stroke android:width="1dp" android:color="@color/textbox_stroke_disabled" />
+ </shape>
+ </item>
+
+</selector>
diff --git a/mobile/android/base/crashreporter/res/layout/crash_reporter.xml b/mobile/android/base/crashreporter/res/layout/crash_reporter.xml
new file mode 100644
index 0000000000..b7285dc202
--- /dev/null
+++ b/mobile/android/base/crashreporter/res/layout/crash_reporter.xml
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fillViewport="true">
+
+ <LinearLayout android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:background="@color/toolbar_grey">
+
+ <LinearLayout android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:orientation="vertical"
+ android:padding="10dp"
+ android:layout_weight="1">
+
+ <TextView android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="10dip"
+ android:textSize="30sp"
+ android:textColor="#000"
+ android:layout_gravity="center_horizontal"
+ android:fontFamily="sans-serif-light"
+ android:text="@string/crash_sorry"/>
+
+ <TextView android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="10dip"
+ android:textAppearance="@style/TextAppearance"
+ android:text="@string/crash_message2"/>
+
+ <CheckBox android:id="@+id/send_report"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checked="true"
+ android:textColor="@color/primary_text"
+ android:layout_marginBottom="10dp"
+ android:text="@string/crash_send_report_message3"/>
+
+ <EditText android:id="@+id/comment"
+ style="@style/CrashReporter.EditText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ems="10"
+ android:inputType="textMultiLine"
+ android:lines="5"
+ android:gravity="top"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:layout_marginBottom="10dp"
+ android:hint="@string/crash_comment" />
+
+ <CheckBox android:id="@+id/include_url"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/primary_text"
+ android:textAppearance="@style/TextAppearance"
+ android:layout_marginBottom="10dp"
+ android:text="@string/crash_include_url2"/>
+
+ <CheckBox android:id="@+id/allow_contact"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/primary_text"
+ android:textAppearance="@style/TextAppearance"
+ android:layout_marginBottom="10dp"
+ android:text="@string/crash_allow_contact2"/>
+
+ <org.mozilla.gecko.widget.ClickableWhenDisabledEditText
+ android:id="@+id/email"
+ style="@style/CrashReporter.EditText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ems="10"
+ android:inputType="textEmailAddress"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp"
+ android:enabled="false"
+ android:clickable="true"
+ android:hint="@string/crash_email" />
+
+ </LinearLayout>
+
+ <View android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="#999" />
+
+ <LinearLayout android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_gravity="bottom">
+
+ <Button android:id="@+id/close"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1.0"
+ android:padding="15dp"
+ android:onClick="onCloseClick"
+ android:text="@string/crash_close_label"
+ android:textAppearance="?android:attr/textAppearance"
+ android:background="@drawable/action_bar_button"/>
+
+ <View android:layout_width="1dp"
+ android:layout_height="match_parent"
+ android:background="#999" />
+
+ <Button android:id="@+id/restart"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1.0"
+ android:padding="15dp"
+ android:onClick="onRestartClick"
+ android:text="@string/crash_restart_label"
+ android:textAppearance="?android:attr/textAppearance"
+ android:background="@drawable/action_bar_button"/>
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+</ScrollView>
diff --git a/mobile/android/base/crashreporter/res/values/colors.xml b/mobile/android/base/crashreporter/res/values/colors.xml
new file mode 100644
index 0000000000..7eeab5b694
--- /dev/null
+++ b/mobile/android/base/crashreporter/res/values/colors.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+ <!-- Crash Reporter Colors -->
+ <color name="textbox_background">#FFF</color>
+ <color name="textbox_background_disabled">#DDD</color>
+ <color name="textbox_stroke">#000</color>
+ <color name="textbox_stroke_disabled">#666</color>
+</resources>
+
diff --git a/mobile/android/base/crashreporter/res/values/styles.xml b/mobile/android/base/crashreporter/res/values/styles.xml
new file mode 100644
index 0000000000..17f5409e49
--- /dev/null
+++ b/mobile/android/base/crashreporter/res/values/styles.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
+ <!-- Crash Reporter Styles -->
+ <style name="CrashReporter" />
+
+ <style name="CrashReporter.EditText">
+ <item name="android:background">@drawable/textbox_bg</item>
+ <item name="android:padding">10dp</item>
+ <item name="android:textAppearance">@style/TextAppearance</item>
+ </style>
+</resources>
diff --git a/mobile/android/base/geckoview.ddf b/mobile/android/base/geckoview.ddf
new file mode 100644
index 0000000000..015a0d3e72
--- /dev/null
+++ b/mobile/android/base/geckoview.ddf
@@ -0,0 +1,75 @@
+# This is a Classycle dependency definition file that asserts that the contents
+# of the GeckoView library (Classycle set [lib]) is a dependency (but does not
+# depend) on Fennec (Classycle set [main]). The additional Classycle set
+# [middle] consists of classes referenced by GeckoView that probably should not
+# be referenced. We want this middle set to shrink over time.
+
+show allResults
+
+[lib] = \
+ org.mozilla.gecko.gfx.* \
+ org.mozilla.gecko.mozglue.* \
+ org.mozilla.gecko.sqlite.* \
+ org.mozilla.gecko.util.* \
+ org.mozilla.gecko.AndroidGamepadManager \
+ org.mozilla.gecko.AppConstants \
+ org.mozilla.gecko.BaseGeckoInterface \
+ org.mozilla.gecko.ContextGetter \
+ org.mozilla.gecko.CrashHandler \
+ org.mozilla.gecko.EventDispatcher \
+ org.mozilla.gecko.GeckoAccessibility \
+ org.mozilla.gecko.GeckoAppShell \
+ org.mozilla.gecko.GeckoBatteryManager \
+ org.mozilla.gecko.GeckoEditable \
+ org.mozilla.gecko.GeckoEditableClient \
+ org.mozilla.gecko.GeckoEditableListener \
+ org.mozilla.gecko.GeckoEvent \
+ org.mozilla.gecko.GeckoInputConnection \
+ org.mozilla.gecko.GeckoJavaSampler \
+ org.mozilla.gecko.GeckoNetworkManager \
+ org.mozilla.gecko.GeckoProfile \
+ org.mozilla.gecko.GeckoScreenOrientation \
+ org.mozilla.gecko.GeckoSharedPrefs \
+ org.mozilla.gecko.GeckoThread \
+ org.mozilla.gecko.GeckoView \
+ org.mozilla.gecko.GlobalHistory \
+ org.mozilla.gecko.InputMethods \
+ org.mozilla.gecko.NSSBridge \
+ org.mozilla.gecko.NotificationClient \
+ org.mozilla.gecko.NotificationHandler \
+ org.mozilla.gecko.PrefsHelper \
+ org.mozilla.gecko.SysInfo \
+ org.mozilla.gecko.TouchEventInterceptor \
+ org.mozilla.gecko.ZoomConstraints
+
+[middle] = \
+ org.mozilla.gecko.prompts.* \
+ org.mozilla.gecko.FormAssistPopup \
+ org.mozilla.gecko.GeckoActivity \
+ org.mozilla.gecko.GeckoApp \
+ org.mozilla.gecko.GeckoProfileDirectories \
+ org.mozilla.gecko.GuestSession \
+ org.mozilla.gecko.R \
+ org.mozilla.gecko.Tab \
+ org.mozilla.gecko.Tabs \
+ org.mozilla.gecko.Telemetry \
+ org.mozilla.gecko.TelemetryContract \
+ org.mozilla.gecko.ThumbnailHelper \
+ org.mozilla.gecko.db.BrowserDB \
+ org.mozilla.gecko.db.LocalBrowserDB \
+ org.mozilla.gecko.distribution.Distribution \
+ org.mozilla.gecko.icons.*
+
+[main] = org.mozilla.gecko.* excluding [lib] [middle]
+
+check sets [lib] [middle] [main]
+
+# Bug 1107134: it appears that Classycle can be fooled if the Java
+# compiler inlines a constant from [main] into [lib]. That is, [main]
+# really does depend on [lib] but Classycle only sees the dependency
+# with some javac versions. For now, disable the check. Yes, this
+# processing is useless without this check.
+# check [lib] directlyIndependentOf [main]
+
+# This fails; if this passed, GeckoView would be ready to extract from Fennec.
+# check [lib] independentOf [middle]
diff --git a/mobile/android/base/java/org/mozilla/gecko/ANRReporter.java b/mobile/android/base/java/org/mozilla/gecko/ANRReporter.java
new file mode 100644
index 0000000000..3c29edef35
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/ANRReporter.java
@@ -0,0 +1,596 @@
+/* -*- 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
new file mode 100644
index 0000000000..705d700af1
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/AboutPages.java
@@ -0,0 +1,117 @@
+/* -*- 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
new file mode 100644
index 0000000000..5892c16b61
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/AccountsHelper.java
@@ -0,0 +1,318 @@
+/* -*- 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
new file mode 100644
index 0000000000..7f2eb219e6
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/ActionBarTextSelection.java
@@ -0,0 +1,256 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..709c0056fd
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/ActionModeCompat.java
@@ -0,0 +1,135 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..c9021b7105
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/ActionModeCompatView.java
@@ -0,0 +1,202 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..7174c65807
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/ActivityHandlerHelper.java
@@ -0,0 +1,61 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..39ca25b679
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/BootReceiver.java
@@ -0,0 +1,27 @@
+/* -*- 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
new file mode 100644
index 0000000000..5eddca3cf7
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -0,0 +1,4261 @@
+/* -*- 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
new file mode 100644
index 0000000000..c5c041c7ac
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserLocaleManager.java
@@ -0,0 +1,439 @@
+/* -*- 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
new file mode 100644
index 0000000000..cff6ea6439
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/ChromeCastDisplay.java
@@ -0,0 +1,112 @@
+/* -*- 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
new file mode 100644
index 0000000000..c531b8c377
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/ChromeCastPlayer.java
@@ -0,0 +1,509 @@
+/* -*- 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
new file mode 100644
index 0000000000..ce2384a4d4
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/CrashReporter.java
@@ -0,0 +1,480 @@
+/* -*- 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
new file mode 100644
index 0000000000..98274b752b
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/CustomEditText.java
@@ -0,0 +1,89 @@
+/* -*- 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
new file mode 100644
index 0000000000..725c25d6e8
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/DataReportingNotification.java
@@ -0,0 +1,133 @@
+/* -*- 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
new file mode 100644
index 0000000000..44aaa14a0a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/DevToolsAuthHelper.java
@@ -0,0 +1,52 @@
+/* -*- 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
new file mode 100644
index 0000000000..9aa3f96a44
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/DoorHangerPopup.java
@@ -0,0 +1,361 @@
+/* -*- 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
new file mode 100644
index 0000000000..ff3ac6110a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/DownloadsIntegration.java
@@ -0,0 +1,235 @@
+/* -*- 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
new file mode 100644
index 0000000000..28f542d5c8
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/DynamicToolbar.java
@@ -0,0 +1,218 @@
+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
new file mode 100644
index 0000000000..38c38a9eba
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/EditBookmarkDialog.java
@@ -0,0 +1,252 @@
+/* -*- 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
new file mode 100644
index 0000000000..e71bb4c52f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/Experiments.java
@@ -0,0 +1,119 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..8ac5428a44
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/FilePicker.java
@@ -0,0 +1,227 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..7629ea546e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/FilePickerResultHandler.java
@@ -0,0 +1,282 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..efa04a04e1
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/FindInPageBar.java
@@ -0,0 +1,256 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..5c7f932c00
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java
@@ -0,0 +1,459 @@
+/* -*- 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
new file mode 100644
index 0000000000..774ca60249
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoActivity.java
@@ -0,0 +1,100 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..ce6b8abd03
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoActivityStatus.java
@@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..05fa2bbf81
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -0,0 +1,2878 @@
+/* -*- 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
new file mode 100644
index 0000000000..18a6e6535c
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java
@@ -0,0 +1,314 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..319eccec11
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoJavaSampler.java
@@ -0,0 +1,211 @@
+/* -*- 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
new file mode 100644
index 0000000000..c199aad554
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoMediaPlayer.java
@@ -0,0 +1,27 @@
+/* -*- 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
new file mode 100644
index 0000000000..b7f4870c21
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoMessageReceiver.java
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..df9844d7b0
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoPresentationDisplay.java
@@ -0,0 +1,22 @@
+/* -*- 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
new file mode 100644
index 0000000000..8a9c461c54
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoProfilesProvider.java
@@ -0,0 +1,149 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..3a99fd2a14
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoService.java
@@ -0,0 +1,236 @@
+/* -*- 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
new file mode 100644
index 0000000000..f73c42e405
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoUpdateReceiver.java
@@ -0,0 +1,25 @@
+/* -*- 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
new file mode 100644
index 0000000000..c1d9c4939a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/GlobalHistory.java
@@ -0,0 +1,178 @@
+/* -*- 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
new file mode 100644
index 0000000000..d9d12962ca
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/GlobalPageMetadata.java
@@ -0,0 +1,182 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..69502f44a2
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/GuestSession.java
@@ -0,0 +1,51 @@
+/* -*- 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
new file mode 100644
index 0000000000..efe9576d7d
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/IntentHelper.java
@@ -0,0 +1,593 @@
+/* -*- 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;
+ }
+
+ // 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
new file mode 100644
index 0000000000..4de8fa423c
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/LauncherActivity.java
@@ -0,0 +1,110 @@
+/* -*- 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
new file mode 100644
index 0000000000..795caa925a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/LocaleManager.java
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..e030b95e9e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/Locales.java
@@ -0,0 +1,136 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..bd109058cd
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/MediaCastingBar.java
@@ -0,0 +1,131 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..fc0ce82cfd
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java
@@ -0,0 +1,323 @@
+/* -*- 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
new file mode 100644
index 0000000000..94ca761b96
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/MemoryMonitor.java
@@ -0,0 +1,279 @@
+/* -*- 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
new file mode 100644
index 0000000000..814c09995c
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/MotionEventInterceptor.java
@@ -0,0 +1,13 @@
+/* -*- 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
new file mode 100644
index 0000000000..37dd8c304d
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/PackageReplacedReceiver.java
@@ -0,0 +1,38 @@
+/* -*- 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
new file mode 100644
index 0000000000..e44096489b
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/PresentationMediaPlayerManager.java
@@ -0,0 +1,149 @@
+/* -*- 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
new file mode 100644
index 0000000000..3e5b5ffb30
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/PresentationView.java
@@ -0,0 +1,27 @@
+/* -*- 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
new file mode 100644
index 0000000000..077b2d29b4
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/PrintHelper.java
@@ -0,0 +1,124 @@
+/* -*- 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
new file mode 100644
index 0000000000..39b6899d3f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/PrivateTab.java
@@ -0,0 +1,28 @@
+/* -*- 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
new file mode 100644
index 0000000000..b4aee9370a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/RemoteClientsDialogFragment.java
@@ -0,0 +1,133 @@
+/* -*- 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
new file mode 100644
index 0000000000..b5a5527c9e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/RemotePresentationService.java
@@ -0,0 +1,150 @@
+/* -*- 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
new file mode 100644
index 0000000000..b049f76278
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/Restarter.java
@@ -0,0 +1,50 @@
+/* -*- 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
new file mode 100644
index 0000000000..5cb404ce8a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/ScreenManagerHelper.java
@@ -0,0 +1,43 @@
+/* -*- 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
new file mode 100644
index 0000000000..64f101e515
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/ScreenshotObserver.java
@@ -0,0 +1,146 @@
+/* -*- 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
new file mode 100644
index 0000000000..d29aaadc73
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/SessionParser.java
@@ -0,0 +1,140 @@
+/* -*- 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
new file mode 100644
index 0000000000..1066da0799
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/SharedPreferencesHelper.java
@@ -0,0 +1,311 @@
+/* -*- 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
new file mode 100644
index 0000000000..e39d25dd87
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/SiteIdentity.java
@@ -0,0 +1,249 @@
+/* -*- 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
new file mode 100644
index 0000000000..3283e7c375
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/SnackbarBuilder.java
@@ -0,0 +1,257 @@
+/* -*- 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
new file mode 100644
index 0000000000..e43bbef1f4
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/SuggestClient.java
@@ -0,0 +1,142 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..6010a3dd94
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/Tab.java
@@ -0,0 +1,843 @@
+/* -*- 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
new file mode 100644
index 0000000000..c7e024fe03
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/Tabs.java
@@ -0,0 +1,1021 @@
+/* -*- 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
new file mode 100644
index 0000000000..342445bf21
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/Telemetry.java
@@ -0,0 +1,246 @@
+/* -*- 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
new file mode 100644
index 0000000000..0c2051a9da
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/TelemetryContract.java
@@ -0,0 +1,307 @@
+/* -*- 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
new file mode 100644
index 0000000000..3a70124316
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/ThumbnailHelper.java
@@ -0,0 +1,246 @@
+/* -*- 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
new file mode 100644
index 0000000000..c0c9307dcf
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/ZoomedView.java
@@ -0,0 +1,838 @@
+/* -*- 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
new file mode 100644
index 0000000000..d1c3f59169
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/activitystream/ActivityStream.java
@@ -0,0 +1,149 @@
+/* -*- 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
new file mode 100644
index 0000000000..aee0bba63a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/adjust/AdjustBrowserAppDelegate.java
@@ -0,0 +1,52 @@
+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
new file mode 100644
index 0000000000..19399e735b
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/adjust/AdjustHelper.java
@@ -0,0 +1,75 @@
+/* -*- 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
new file mode 100644
index 0000000000..aeb7b4334e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/adjust/AdjustHelperInterface.java
@@ -0,0 +1,22 @@
+/* -*- 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
new file mode 100644
index 0000000000..6dadd2261d
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/adjust/AttributionHelperListener.java
@@ -0,0 +1,17 @@
+/* -*- 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
new file mode 100644
index 0000000000..ddfed84bd7
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/adjust/StubAdjustHelper.java
@@ -0,0 +1,31 @@
+/* -*- 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
new file mode 100644
index 0000000000..63e8e168ec
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/animation/AnimationUtils.java
@@ -0,0 +1,21 @@
+/* -*- 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
new file mode 100644
index 0000000000..bf8007bbfa
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/animation/HeightChangeAnimation.java
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..dc2403bbd1
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/animation/PropertyAnimator.java
@@ -0,0 +1,342 @@
+/* -*- 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
new file mode 100644
index 0000000000..7e8377f55f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/animation/Rotate3DAnimation.java
@@ -0,0 +1,97 @@
+/*
+ * 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
new file mode 100644
index 0000000000..3ea2e84373
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/animation/ViewHelper.java
@@ -0,0 +1,109 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..447b837e86
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/cleanup/FileCleanupController.java
@@ -0,0 +1,81 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..76aff733a6
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/cleanup/FileCleanupService.java
@@ -0,0 +1,80 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..b1bf567b0c
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
@@ -0,0 +1,177 @@
+/* -*- 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
new file mode 100644
index 0000000000..7960f78324
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/GeckoCustomTabsService.java
@@ -0,0 +1,65 @@
+/* -*- 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
new file mode 100644
index 0000000000..2e056cc1ea
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/AbstractPerProfileDatabaseProvider.java
@@ -0,0 +1,79 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..7e289b76fd
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/AbstractTransactionalProvider.java
@@ -0,0 +1,328 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..418d547ed4
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BaseTable.java
@@ -0,0 +1,64 @@
+/* -*- 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
new file mode 100644
index 0000000000..51c8d964fb
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java
@@ -0,0 +1,785 @@
+/* -*- 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
new file mode 100644
index 0000000000..4219e45b17
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
@@ -0,0 +1,205 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..f823d90609
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java
@@ -0,0 +1,2237 @@
+/* -*- 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
new file mode 100644
index 0000000000..eb75d0be96
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
@@ -0,0 +1,2340 @@
+/* -*- 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
new file mode 100644
index 0000000000..cfa2f870fb
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/DBUtils.java
@@ -0,0 +1,450 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..ff2f5238e6
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/FormHistoryProvider.java
@@ -0,0 +1,166 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..1a241f9dac
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/HomeProvider.java
@@ -0,0 +1,194 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..8c219282fc
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
@@ -0,0 +1,1938 @@
+/* -*- 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
new file mode 100644
index 0000000000..a9a55e51d0
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalSearches.java
@@ -0,0 +1,28 @@
+/* -*- 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
new file mode 100644
index 0000000000..c7bd9475c0
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalTabsAccessor.java
@@ -0,0 +1,320 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..7f2c4a7367
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalURLMetadata.java
@@ -0,0 +1,240 @@
+/* -*- 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
new file mode 100644
index 0000000000..9df41a1691
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalUrlAnnotations.java
@@ -0,0 +1,253 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..d2d5048510
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LoginsProvider.java
@@ -0,0 +1,520 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..2f5e11ed46
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/PasswordsProvider.java
@@ -0,0 +1,348 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..7075c6e8ac
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/PerProfileDatabaseProvider.java
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..288d9cae7d
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/PerProfileDatabases.java
@@ -0,0 +1,94 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..07f057c117
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/RemoteClient.java
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..f7660c1f7d
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/RemoteTab.java
@@ -0,0 +1,90 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..d48604f036
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/SQLiteBridgeContentProvider.java
@@ -0,0 +1,471 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..05d31fefd6
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/SearchHistoryProvider.java
@@ -0,0 +1,127 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..e050a4f932
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/Searches.java
@@ -0,0 +1,12 @@
+/* -*- 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
new file mode 100644
index 0000000000..8be18c089e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/SharedBrowserDatabaseProvider.java
@@ -0,0 +1,128 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..89b12904bb
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/SuggestedSites.java
@@ -0,0 +1,629 @@
+/* -*- 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
new file mode 100644
index 0000000000..37a605ee1b
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/Table.java
@@ -0,0 +1,47 @@
+/* -*- 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
new file mode 100644
index 0000000000..1be004ca7f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/TabsAccessor.java
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..09e4d9cf5e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/TabsProvider.java
@@ -0,0 +1,361 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..7973839e26
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/URLMetadata.java
@@ -0,0 +1,25 @@
+/* -*- 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
new file mode 100644
index 0000000000..49bbb74e7e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/URLMetadataTable.java
@@ -0,0 +1,92 @@
+/* -*- 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
new file mode 100644
index 0000000000..dcae6ee79a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/UrlAnnotations.java
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..a1c54d3c33
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/delegates/BookmarkStateChangeDelegate.java
@@ -0,0 +1,237 @@
+/* -*- 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
new file mode 100644
index 0000000000..70b1349927
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/delegates/BrowserAppDelegate.java
@@ -0,0 +1,78 @@
+/* -*- 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
new file mode 100644
index 0000000000..c67b8a18a4
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/delegates/BrowserAppDelegateWithReference.java
@@ -0,0 +1,29 @@
+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
new file mode 100644
index 0000000000..5f3aa9c597
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/delegates/OfflineTabStatusDelegate.java
@@ -0,0 +1,119 @@
+/* -*- 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
new file mode 100644
index 0000000000..f048372f73
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/delegates/ScreenshotDelegate.java
@@ -0,0 +1,80 @@
+/* -*- 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
new file mode 100644
index 0000000000..ebd3991ea7
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/delegates/TabsTrayVisibilityAwareDelegate.java
@@ -0,0 +1,38 @@
+/* -*- 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
new file mode 100644
index 0000000000..a7b0fe32d3
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/distribution/Distribution.java
@@ -0,0 +1,1046 @@
+/* -*- 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
new file mode 100644
index 0000000000..11ed4811f2
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/distribution/DistributionStoreCallback.java
@@ -0,0 +1,61 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..78a77221d0
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/distribution/PartnerBookmarksProviderProxy.java
@@ -0,0 +1,322 @@
+/* -*- 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
new file mode 100644
index 0000000000..2dad21a48f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/distribution/PartnerBrowserCustomizationsClient.java
@@ -0,0 +1,43 @@
+/* -*- 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
new file mode 100644
index 0000000000..4a1be656b2
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/distribution/ReferrerDescriptor.java
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..3651d6068f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/distribution/ReferrerReceiver.java
@@ -0,0 +1,107 @@
+/* -*- 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
new file mode 100644
index 0000000000..28d6b238df
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/dlc/BaseAction.java
@@ -0,0 +1,166 @@
+/* -*- 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
new file mode 100644
index 0000000000..e44704c6c1
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/dlc/CleanupAction.java
@@ -0,0 +1,49 @@
+/* -*- 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
new file mode 100644
index 0000000000..8618d4699c
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadAction.java
@@ -0,0 +1,325 @@
+/* -*- 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
new file mode 100644
index 0000000000..3729cf2e06
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadContentService.java
@@ -0,0 +1,144 @@
+/* -*- 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
new file mode 100644
index 0000000000..e15a17bbee
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/dlc/StudyAction.java
@@ -0,0 +1,81 @@
+/* -*- 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
new file mode 100644
index 0000000000..104bdad183
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/dlc/SyncAction.java
@@ -0,0 +1,263 @@
+/* -*- 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
new file mode 100644
index 0000000000..e96a62eaea
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/dlc/VerifyAction.java
@@ -0,0 +1,63 @@
+/* -*- 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
new file mode 100644
index 0000000000..61f7992ca8
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContent.java
@@ -0,0 +1,189 @@
+/* -*- 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
new file mode 100644
index 0000000000..40c8045730
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentBootstrap.java
@@ -0,0 +1,161 @@
+/* -*- 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
new file mode 100644
index 0000000000..243e2d4eb2
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentBuilder.java
@@ -0,0 +1,238 @@
+/* -*- 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
new file mode 100644
index 0000000000..43ba4e82e0
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/dlc/catalog/DownloadContentCatalog.java
@@ -0,0 +1,303 @@
+/* -*- 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
new file mode 100644
index 0000000000..d317a21eea
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/ContentNotificationsDelegate.java
@@ -0,0 +1,89 @@
+/* -*- 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
new file mode 100644
index 0000000000..d943b4f81c
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/FeedAlarmReceiver.java
@@ -0,0 +1,31 @@
+/* -*- 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
new file mode 100644
index 0000000000..76c1b7e309
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/FeedFetcher.java
@@ -0,0 +1,110 @@
+/* -*- 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
new file mode 100644
index 0000000000..3744862153
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/FeedService.java
@@ -0,0 +1,168 @@
+/* -*- 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
new file mode 100644
index 0000000000..09a2b12b6c
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/action/CheckForUpdatesAction.java
@@ -0,0 +1,281 @@
+/* -*- 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
new file mode 100644
index 0000000000..b778938fdd
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/action/EnrollSubscriptionsAction.java
@@ -0,0 +1,101 @@
+/* -*- 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
new file mode 100644
index 0000000000..acfaa8b4d6
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/action/FeedAction.java
@@ -0,0 +1,58 @@
+/* -*- 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
new file mode 100644
index 0000000000..f5bf39997d
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/action/SetupAlarmsAction.java
@@ -0,0 +1,146 @@
+/* -*- 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
new file mode 100644
index 0000000000..fbfce1af2f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/action/SubscribeToFeedAction.java
@@ -0,0 +1,79 @@
+/* -*- 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
new file mode 100644
index 0000000000..6f955c1857
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/action/WithdrawSubscriptionsAction.java
@@ -0,0 +1,109 @@
+/* -*- 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
new file mode 100644
index 0000000000..febfbb0c7d
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSite.java
@@ -0,0 +1,38 @@
+/* -*- 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
new file mode 100644
index 0000000000..6bb3629bf1
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteBlogger.java
@@ -0,0 +1,29 @@
+/* -*- 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
new file mode 100644
index 0000000000..a96e83fcdb
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteMedium.java
@@ -0,0 +1,29 @@
+/* -*- 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
new file mode 100644
index 0000000000..c9f480013c
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteTumblr.java
@@ -0,0 +1,33 @@
+/* -*- 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
new file mode 100644
index 0000000000..a74b41a749
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteWordpress.java
@@ -0,0 +1,26 @@
+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
new file mode 100644
index 0000000000..aefc72aa71
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/parser/Feed.java
@@ -0,0 +1,70 @@
+/* -*- 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
new file mode 100644
index 0000000000..8d8f6d44ea
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/parser/Item.java
@@ -0,0 +1,49 @@
+/* -*- 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
new file mode 100644
index 0000000000..afb1b7cb20
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/parser/SimpleFeedParser.java
@@ -0,0 +1,367 @@
+/* -*- 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
new file mode 100644
index 0000000000..7ce7f193fd
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/subscriptions/FeedSubscription.java
@@ -0,0 +1,130 @@
+/* -*- 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
new file mode 100644
index 0000000000..d5940d7581
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/DataPanel.java
@@ -0,0 +1,47 @@
+/* -*- 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
new file mode 100644
index 0000000000..93dd0c254e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunAnimationContainer.java
@@ -0,0 +1,94 @@
+/* -*- 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
new file mode 100644
index 0000000000..c2838ee3e0
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPager.java
@@ -0,0 +1,174 @@
+/* -*- 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
new file mode 100644
index 0000000000..3f901d07b0
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPagerConfig.java
@@ -0,0 +1,107 @@
+/* -*- 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
new file mode 100644
index 0000000000..4b27dbc734
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPanel.java
@@ -0,0 +1,80 @@
+/* -*- 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
new file mode 100644
index 0000000000..efc91d20f4
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/RestrictedWelcomePanel.java
@@ -0,0 +1,61 @@
+/* -*- 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
new file mode 100644
index 0000000000..2f489c84ed
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/SyncPanel.java
@@ -0,0 +1,61 @@
+/* -*- 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
new file mode 100644
index 0000000000..3c2ed83124
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/TabQueuePanel.java
@@ -0,0 +1,92 @@
+/* -*- 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
new file mode 100644
index 0000000000..0616cd229b
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/gcm/GcmInstanceIDListenerService.java
@@ -0,0 +1,35 @@
+/* -*- 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
new file mode 100644
index 0000000000..7962d7dc31
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/gcm/GcmMessageListenerService.java
@@ -0,0 +1,38 @@
+/* -*- 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
new file mode 100644
index 0000000000..024905eb09
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/gcm/GcmTokenClient.java
@@ -0,0 +1,131 @@
+/* -*- 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
new file mode 100644
index 0000000000..a9f5b72f3b
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/health/HealthRecorder.java
@@ -0,0 +1,40 @@
+/* -*- 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
new file mode 100644
index 0000000000..ad65918e1e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/health/SessionInformation.java
@@ -0,0 +1,138 @@
+/* -*- 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
new file mode 100644
index 0000000000..65a9729859
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/health/StubbedHealthRecorder.java
@@ -0,0 +1,53 @@
+/* -*- 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
new file mode 100644
index 0000000000..566422faff
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/BookmarkFolderView.java
@@ -0,0 +1,147 @@
+/* -*- 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
new file mode 100644
index 0000000000..a1efff0491
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/BookmarkScreenshotRow.java
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..b311166933
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListAdapter.java
@@ -0,0 +1,352 @@
+/* -*- 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
new file mode 100644
index 0000000000..94157be105
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListView.java
@@ -0,0 +1,218 @@
+ /* -*- 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
new file mode 100644
index 0000000000..4b47819966
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/BookmarksPanel.java
@@ -0,0 +1,316 @@
+/* -*- 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
new file mode 100644
index 0000000000..7732932fe3
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/BrowserSearch.java
@@ -0,0 +1,1316 @@
+/* -*- 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
new file mode 100644
index 0000000000..f288a27456
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/ClientsAdapter.java
@@ -0,0 +1,373 @@
+/* -*- 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
new file mode 100644
index 0000000000..402ed26e7b
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
@@ -0,0 +1,433 @@
+/* -*- 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
new file mode 100644
index 0000000000..a2c1b72c24
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryItem.java
@@ -0,0 +1,127 @@
+/* -*- 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
new file mode 100644
index 0000000000..c9afecd636
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
@@ -0,0 +1,697 @@
+/* -*- 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
new file mode 100644
index 0000000000..e813e4c44a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryRecyclerView.java
@@ -0,0 +1,145 @@
+/* -*- 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
new file mode 100644
index 0000000000..d2c136219f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/DynamicPanel.java
@@ -0,0 +1,393 @@
+/* -*- 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
new file mode 100644
index 0000000000..7168c1576a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/FramePanelLayout.java
@@ -0,0 +1,52 @@
+/* -*- 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
new file mode 100644
index 0000000000..7a49559f67
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HistorySectionsHelper.java
@@ -0,0 +1,80 @@
+/* -*- 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
new file mode 100644
index 0000000000..98d1ae6d8b
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeAdapter.java
@@ -0,0 +1,224 @@
+/* -*- 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
new file mode 100644
index 0000000000..10f5db39e5
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeBanner.java
@@ -0,0 +1,315 @@
+/* -*- 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
new file mode 100644
index 0000000000..08e79be3a3
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfig.java
@@ -0,0 +1,1694 @@
+/* -*- 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
new file mode 100644
index 0000000000..914d0fdd19
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigLoader.java
@@ -0,0 +1,83 @@
+/* -*- 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
new file mode 100644
index 0000000000..a2d80788c2
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java
@@ -0,0 +1,663 @@
+/* -*- 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
new file mode 100644
index 0000000000..cefa0329da
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeContextMenuInfo.java
@@ -0,0 +1,82 @@
+/* -*- 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
new file mode 100644
index 0000000000..7badd69290
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeExpandableListView.java
@@ -0,0 +1,68 @@
+/* -*- 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
new file mode 100644
index 0000000000..da6e9b7038
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java
@@ -0,0 +1,498 @@
+/* -*- 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
new file mode 100644
index 0000000000..d179a27ce0
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeListView.java
@@ -0,0 +1,138 @@
+/* -*- 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
new file mode 100644
index 0000000000..4915f0c919
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomePager.java
@@ -0,0 +1,564 @@
+/* -*- 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
new file mode 100644
index 0000000000..bfd6c5624a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomePanelsManager.java
@@ -0,0 +1,368 @@
+/* -*- 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
new file mode 100644
index 0000000000..1525969a00
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeScreen.java
@@ -0,0 +1,57 @@
+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
new file mode 100644
index 0000000000..2bbd82a8df
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/ImageLoader.java
@@ -0,0 +1,164 @@
+/* -*- 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
new file mode 100644
index 0000000000..26edf13ff9
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/MultiTypeCursorAdapter.java
@@ -0,0 +1,100 @@
+/* -*- 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
new file mode 100644
index 0000000000..d66919344f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/PanelAuthCache.java
@@ -0,0 +1,82 @@
+/* -*- 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
new file mode 100644
index 0000000000..1ad91b7caf
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/PanelAuthLayout.java
@@ -0,0 +1,63 @@
+/* -*- 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
new file mode 100644
index 0000000000..4772e08abb
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/PanelBackItemView.java
@@ -0,0 +1,48 @@
+/* -*- 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
new file mode 100644
index 0000000000..50c4dbc075
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/PanelHeaderView.java
@@ -0,0 +1,28 @@
+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
new file mode 100644
index 0000000000..089e178376
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/PanelInfoManager.java
@@ -0,0 +1,162 @@
+/* -*- 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
new file mode 100644
index 0000000000..2a97d42bc8
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/PanelItemView.java
@@ -0,0 +1,136 @@
+/* -*- 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
new file mode 100644
index 0000000000..2c2d89ae04
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/PanelLayout.java
@@ -0,0 +1,747 @@
+/* -*- 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
new file mode 100644
index 0000000000..505fb9b0df
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/PanelListView.java
@@ -0,0 +1,83 @@
+/* -*- 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
new file mode 100644
index 0000000000..9145ab1e1c
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/PanelRecyclerView.java
@@ -0,0 +1,178 @@
+/* -*- 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
new file mode 100644
index 0000000000..fa632bccd0
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/PanelRecyclerViewAdapter.java
@@ -0,0 +1,137 @@
+/* -*- 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
new file mode 100644
index 0000000000..d43a97f31a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/PanelRefreshLayout.java
@@ -0,0 +1,90 @@
+/* -*- 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
new file mode 100644
index 0000000000..cf03c50c0b
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/PanelViewAdapter.java
@@ -0,0 +1,113 @@
+/* -*- 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
new file mode 100644
index 0000000000..a69db0b418
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/PanelViewItemHandler.java
@@ -0,0 +1,59 @@
+/* -*- 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
new file mode 100644
index 0000000000..230b1d3290
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/PinSiteDialog.java
@@ -0,0 +1,256 @@
+/* -*- 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
new file mode 100755
index 0000000000..3091f77da9
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java
@@ -0,0 +1,454 @@
+/* -*- 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
new file mode 100644
index 0000000000..43497ae6ca
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/RemoteTabsExpandableListState.java
@@ -0,0 +1,163 @@
+/* -*- 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
new file mode 100644
index 0000000000..9b2d2746a7
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/SearchEngine.java
@@ -0,0 +1,102 @@
+/* -*- 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
new file mode 100644
index 0000000000..be5b3b461f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineAdapter.java
@@ -0,0 +1,122 @@
+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
new file mode 100644
index 0000000000..6a6509bcbe
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineBar.java
@@ -0,0 +1,148 @@
+/* -*- 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
new file mode 100644
index 0000000000..5b97a8f5f7
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java
@@ -0,0 +1,494 @@
+/* -*- 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
new file mode 100644
index 0000000000..f7b5b65866
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/SearchLoader.java
@@ -0,0 +1,114 @@
+/* -*- 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
new file mode 100644
index 0000000000..b8889c0331
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/SimpleCursorLoader.java
@@ -0,0 +1,147 @@
+/*
+ * 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
new file mode 100644
index 0000000000..039b65e829
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/SpacingDecoration.java
@@ -0,0 +1,20 @@
+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
new file mode 100644
index 0000000000..b302d35225
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/TabMenuStrip.java
@@ -0,0 +1,127 @@
+/* -*- 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
new file mode 100644
index 0000000000..a09add80b3
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/TabMenuStripLayout.java
@@ -0,0 +1,246 @@
+/* -*- 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
new file mode 100644
index 0000000000..c17aff209a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridItemView.java
@@ -0,0 +1,312 @@
+/* -*- 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
new file mode 100644
index 0000000000..58a05b1986
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridView.java
@@ -0,0 +1,169 @@
+/* -*- 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
new file mode 100644
index 0000000000..f39e51ac5e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/TopSitesPanel.java
@@ -0,0 +1,968 @@
+/* -*- 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
new file mode 100644
index 0000000000..dd45014b0a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/TopSitesThumbnailView.java
@@ -0,0 +1,102 @@
+/* -*- 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
new file mode 100644
index 0000000000..68eb8daa5c
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/TwoLinePageRow.java
@@ -0,0 +1,324 @@
+/* -*- 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
new file mode 100644
index 0000000000..ef0c105d30
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStream.java
@@ -0,0 +1,145 @@
+/* -*- 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
new file mode 100644
index 0000000000..09f6705d78
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeFragment.java
@@ -0,0 +1,39 @@
+/* -*- 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
new file mode 100644
index 0000000000..4decc82180
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeScreen.java
@@ -0,0 +1,73 @@
+/* -*- 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
new file mode 100644
index 0000000000..24348dfe09
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamItem.java
@@ -0,0 +1,196 @@
+/* -*- 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
new file mode 100644
index 0000000000..f7cda2e7fd
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamRecyclerAdapter.java
@@ -0,0 +1,135 @@
+/* -*- 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
new file mode 100644
index 0000000000..525d3b426b
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/ActivityStreamContextMenu.java
@@ -0,0 +1,239 @@
+/* -*- 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
new file mode 100644
index 0000000000..e95867c369
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/BottomSheetContextMenu.java
@@ -0,0 +1,102 @@
+/* -*- 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
new file mode 100644
index 0000000000..56615937bd
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/PopupContextMenu.java
@@ -0,0 +1,76 @@
+/* -*- 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
new file mode 100644
index 0000000000..096f0c597e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/CirclePageIndicator.java
@@ -0,0 +1,568 @@
+/*
+ * 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
new file mode 100644
index 0000000000..b436a466f7
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesCard.java
@@ -0,0 +1,105 @@
+/* -*- 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
new file mode 100644
index 0000000000..45fdc0d1a2
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPage.java
@@ -0,0 +1,38 @@
+/* -*- 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
new file mode 100644
index 0000000000..29e6aca3dd
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPageAdapter.java
@@ -0,0 +1,117 @@
+/* -*- 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
new file mode 100644
index 0000000000..dc824d9029
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPagerAdapter.java
@@ -0,0 +1,124 @@
+/* -*- 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
new file mode 100644
index 0000000000..0232a4ea6f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/IconCallback.java
@@ -0,0 +1,13 @@
+/* -*- 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
new file mode 100644
index 0000000000..359c47e53a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/IconDescriptor.java
@@ -0,0 +1,96 @@
+/* -*- 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
new file mode 100644
index 0000000000..3c6064825a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/IconDescriptorComparator.java
@@ -0,0 +1,67 @@
+/* -*- 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
new file mode 100644
index 0000000000..be000642ed
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/IconRequest.java
@@ -0,0 +1,181 @@
+/* -*- 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
new file mode 100644
index 0000000000..d9fd9ec5af
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/IconRequestBuilder.java
@@ -0,0 +1,143 @@
+/* -*- 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
new file mode 100644
index 0000000000..aad7849808
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/IconRequestExecutor.java
@@ -0,0 +1,152 @@
+/* -*- 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
new file mode 100644
index 0000000000..726619eb9a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/IconResponse.java
@@ -0,0 +1,167 @@
+/* -*- 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
new file mode 100644
index 0000000000..411a31980f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/IconTask.java
@@ -0,0 +1,222 @@
+/* -*- 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
new file mode 100644
index 0000000000..a5505a6941
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/Icons.java
@@ -0,0 +1,35 @@
+/* -*- 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
new file mode 100644
index 0000000000..d351eb4b75
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/IconsHelper.java
@@ -0,0 +1,140 @@
+/* -*- 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
new file mode 100644
index 0000000000..43f5d0ac60
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/decoders/FaviconDecoder.java
@@ -0,0 +1,197 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..44e3f1252b
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/decoders/ICODecoder.java
@@ -0,0 +1,396 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..82ff91a55b
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/decoders/IconDirectoryEntry.java
@@ -0,0 +1,212 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..cc196b91e3
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/decoders/LoadFaviconResult.java
@@ -0,0 +1,133 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..be8e6d7def
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/loader/ContentProviderLoader.java
@@ -0,0 +1,96 @@
+/* -*- 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
new file mode 100644
index 0000000000..9ddc138ec2
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/loader/DataUriLoader.java
@@ -0,0 +1,36 @@
+/* -*- 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
new file mode 100644
index 0000000000..18a38e32b9
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/loader/DiskLoader.java
@@ -0,0 +1,27 @@
+/* -*- 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
new file mode 100644
index 0000000000..3ae9d15d0e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/loader/IconDownloader.java
@@ -0,0 +1,219 @@
+/* -*- 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
new file mode 100644
index 0000000000..e0139345d8
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/loader/IconGenerator.java
@@ -0,0 +1,168 @@
+/* -*- 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
new file mode 100644
index 0000000000..8158babc38
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/loader/IconLoader.java
@@ -0,0 +1,23 @@
+/* -*- 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
new file mode 100644
index 0000000000..882d32da5f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/loader/JarLoader.java
@@ -0,0 +1,45 @@
+/* -*- 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
new file mode 100644
index 0000000000..d1efc3ad97
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/loader/LegacyLoader.java
@@ -0,0 +1,74 @@
+/* -*- 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
new file mode 100644
index 0000000000..98b651fc76
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/loader/MemoryLoader.java
@@ -0,0 +1,31 @@
+/* -*- 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
new file mode 100644
index 0000000000..d335cbf514
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/preparation/AboutPagesPreparer.java
@@ -0,0 +1,39 @@
+/* -*- 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
new file mode 100644
index 0000000000..5bc7d1c1f2
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/preparation/AddDefaultIconUrl.java
@@ -0,0 +1,39 @@
+/* -*- 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
new file mode 100644
index 0000000000..effd31a03d
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterKnownFailureUrls.java
@@ -0,0 +1,29 @@
+/* -*- 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
new file mode 100644
index 0000000000..a12dad2ad4
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterMimeTypes.java
@@ -0,0 +1,39 @@
+/* -*- 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
new file mode 100644
index 0000000000..abf34c0385
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/preparation/FilterPrivilegedUrls.java
@@ -0,0 +1,30 @@
+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
new file mode 100644
index 0000000000..0c76411120
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/preparation/LookupIconUrl.java
@@ -0,0 +1,56 @@
+/* -*- 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
new file mode 100644
index 0000000000..466307ead6
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/preparation/Preparer.java
@@ -0,0 +1,19 @@
+/* -*- 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
new file mode 100644
index 0000000000..3f71100346
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/processing/ColorProcessor.java
@@ -0,0 +1,61 @@
+/* -*- 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
new file mode 100644
index 0000000000..150aa503b8
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/processing/DiskProcessor.java
@@ -0,0 +1,36 @@
+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
new file mode 100644
index 0000000000..245faded54
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/processing/MemoryProcessor.java
@@ -0,0 +1,38 @@
+/* -*- 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
new file mode 100644
index 0000000000..df7d63c6cc
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/processing/Processor.java
@@ -0,0 +1,21 @@
+/* -*- 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
new file mode 100644
index 0000000000..63b4790219
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/processing/ResizingProcessor.java
@@ -0,0 +1,68 @@
+/* -*- 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
new file mode 100644
index 0000000000..3c0e2a5542
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/storage/DiskStorage.java
@@ -0,0 +1,293 @@
+/* -*- 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
new file mode 100644
index 0000000000..b45cb0fce1
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/storage/FailureCache.java
@@ -0,0 +1,70 @@
+/* -*- 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
new file mode 100644
index 0000000000..e0a96f7c7e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/icons/storage/MemoryStorage.java
@@ -0,0 +1,112 @@
+/* -*- 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
new file mode 100644
index 0000000000..33a97955f4
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManager.java
@@ -0,0 +1,195 @@
+/* -*- 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
new file mode 100644
index 0000000000..f361773ca5
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManagerV1.java
@@ -0,0 +1,260 @@
+/* -*- 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
new file mode 100644
index 0000000000..0f27c1febc
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/lwt/LightweightTheme.java
@@ -0,0 +1,455 @@
+/* -*- 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
new file mode 100644
index 0000000000..c0ae6eaedf
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/lwt/LightweightThemeDrawable.java
@@ -0,0 +1,133 @@
+/* -*- 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
new file mode 100644
index 0000000000..6f23790b9e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/mdns/MulticastDNSManager.java
@@ -0,0 +1,535 @@
+/* -*- 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
new file mode 100644
index 0000000000..c9c620606e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/AsyncCodec.java
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..fd670e21b5
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/AsyncCodecFactory.java
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..93a63bcb5d
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/AudioFocusAgent.java
@@ -0,0 +1,135 @@
+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
new file mode 100644
index 0000000000..b0a26dfb31
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/Codec.java
@@ -0,0 +1,366 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..3025c14d06
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/CodecProxy.java
@@ -0,0 +1,191 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..c6762672d1
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/FormatParam.java
@@ -0,0 +1,133 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..7b3bda3fd7
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrm.java
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..6ccaf80dfc
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV21.java
@@ -0,0 +1,627 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..74144f28ed
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV23.java
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..3df01f1fe2
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/JellyBeanAsyncCodec.java
@@ -0,0 +1,405 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..2aad674b6a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/LocalMediaDrmBridge.java
@@ -0,0 +1,162 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..2aa7830508
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
@@ -0,0 +1,431 @@
+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
new file mode 100644
index 0000000000..faca2389ee
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/MediaDrmProxy.java
@@ -0,0 +1,307 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..fcb0fc6594
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/MediaManager.java
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..260ca73c14
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/RemoteManager.java
@@ -0,0 +1,224 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..d65bb7872f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/RemoteMediaDrmBridge.java
@@ -0,0 +1,152 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..8aed0f8515
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/RemoteMediaDrmBridgeStub.java
@@ -0,0 +1,247 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..b7a98da8ab
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/Sample.java
@@ -0,0 +1,264 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..9041e37561
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/SamplePool.java
@@ -0,0 +1,115 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..b41ef36253
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/SessionKeyInfo.java
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..508b9d015f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/VideoPlayer.java
@@ -0,0 +1,204 @@
+/* -*- 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
new file mode 100644
index 0000000000..512f320025
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenu.java
@@ -0,0 +1,928 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..dfcb31c5fb
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenuInflater.java
@@ -0,0 +1,163 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..21df4208d6
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/menu/GeckoMenuItem.java
@@ -0,0 +1,472 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..d774bdd370
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/menu/GeckoSubMenu.java
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..882187dd60
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/menu/MenuItemActionBar.java
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..5b50693348
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/menu/MenuItemDefault.java
@@ -0,0 +1,152 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..d01f526876
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/menu/MenuItemSwitcherLayout.java
@@ -0,0 +1,188 @@
+/* -*- 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
new file mode 100644
index 0000000000..ce4da8b7fc
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/menu/MenuPanel.java
@@ -0,0 +1,36 @@
+/* -*- 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
new file mode 100644
index 0000000000..227cc76309
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/menu/MenuPopup.java
@@ -0,0 +1,76 @@
+/* -*- 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
new file mode 100644
index 0000000000..cf22685c2c
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/mozglue/SharedMemBuffer.java
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..bc43a27554
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/mozglue/SharedMemory.java
@@ -0,0 +1,171 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..c46c010507
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationClient.java
@@ -0,0 +1,324 @@
+/* -*- 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
new file mode 100644
index 0000000000..1e33031b5e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationHelper.java
@@ -0,0 +1,366 @@
+/* -*- 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
new file mode 100644
index 0000000000..c3dd43297b
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationReceiver.java
@@ -0,0 +1,106 @@
+/* -*- 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
new file mode 100644
index 0000000000..04b94cd1a6
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationService.java
@@ -0,0 +1,37 @@
+/* -*- 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
new file mode 100644
index 0000000000..6e799bf742
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/notifications/WhatsNewReceiver.java
@@ -0,0 +1,99 @@
+/* -*- 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
new file mode 100644
index 0000000000..16f5560d3d
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/overlays/OverlayConstants.java
@@ -0,0 +1,68 @@
+/* -*- 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
new file mode 100644
index 0000000000..7182fcce73
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/overlays/service/OverlayActionService.java
@@ -0,0 +1,126 @@
+/* -*- 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
new file mode 100644
index 0000000000..df233d74a0
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/overlays/service/ShareData.java
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..71931e6838
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/AddBookmark.java
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..5abcbd99f5
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/SendTab.java
@@ -0,0 +1,296 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..768176d635
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/ShareMethod.java
@@ -0,0 +1,82 @@
+/*This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..8b7bc872b4
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/OverlayDialogButton.java
@@ -0,0 +1,128 @@
+/* -*- 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
new file mode 100644
index 0000000000..08e9c59f55
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabDeviceListArrayAdapter.java
@@ -0,0 +1,185 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..4fc6caaa99
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabList.java
@@ -0,0 +1,150 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..79da526da5
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/SendTabTargetSelectedListener.java
@@ -0,0 +1,25 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..156fdda2a0
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/overlays/ui/ShareDialog.java
@@ -0,0 +1,493 @@
+/* -*- 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
new file mode 100644
index 0000000000..b68a018f2d
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/AlignRightLinkPreference.java
@@ -0,0 +1,24 @@
+/* -*- 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
new file mode 100644
index 0000000000..bb71ce78b7
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/AndroidImport.java
@@ -0,0 +1,230 @@
+/* -*- 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
new file mode 100644
index 0000000000..0f1d3ec3f1
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/AndroidImportPreference.java
@@ -0,0 +1,112 @@
+/* -*- 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
new file mode 100644
index 0000000000..fb4a8f751d
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/AppCompatPreferenceActivity.java
@@ -0,0 +1,115 @@
+/*
+ * 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
new file mode 100644
index 0000000000..5218cd06db
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/ClearOnShutdownPref.java
@@ -0,0 +1,37 @@
+/* -*- 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
new file mode 100644
index 0000000000..2934ca88e4
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/CustomCheckBoxPreference.java
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..ee5a46bef2
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/CustomListCategory.java
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..8b7e0e7b39
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/CustomListPreference.java
@@ -0,0 +1,182 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..1e235640e3
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/DistroSharedPrefsImport.java
@@ -0,0 +1,61 @@
+/* -*- 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
new file mode 100644
index 0000000000..c77c2cc23a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/FontSizePreference.java
@@ -0,0 +1,192 @@
+/* -*- 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
new file mode 100644
index 0000000000..6be9e6ea50
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferenceFragment.java
@@ -0,0 +1,296 @@
+/* -*- 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
new file mode 100644
index 0000000000..5ab1bc3fd7
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
@@ -0,0 +1,1520 @@
+/* -*- 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_CRASHREPORTER_ENABLED.equals(key)) {
+ if (!AppConstants.MOZ_CRASHREPORTER || !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
new file mode 100644
index 0000000000..774f78c536
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/LinkPreference.java
@@ -0,0 +1,35 @@
+/* -*- 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
new file mode 100644
index 0000000000..f56ea58b9d
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/ListCheckboxPreference.java
@@ -0,0 +1,58 @@
+/* -*- 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
new file mode 100644
index 0000000000..c962a3d197
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/LocaleListPreference.java
@@ -0,0 +1,316 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..c545472e24
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/ModifiableHintPreference.java
@@ -0,0 +1,67 @@
+/* -*- 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
new file mode 100644
index 0000000000..5749bf29df
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/MultiChoicePreference.java
@@ -0,0 +1,271 @@
+/* -*- 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
new file mode 100644
index 0000000000..580d613cab
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/MultiPrefMultiChoicePreference.java
@@ -0,0 +1,116 @@
+/* -*- 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
new file mode 100644
index 0000000000..337d9dd2fa
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/PanelsPreference.java
@@ -0,0 +1,255 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..d44b6eaa9e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/PanelsPreferenceCategory.java
@@ -0,0 +1,261 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..61eff98e78
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/PrivateDataPreference.java
@@ -0,0 +1,67 @@
+/* -*- 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
new file mode 100644
index 0000000000..3ba80b562d
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/SearchEnginePreference.java
@@ -0,0 +1,183 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..47db8b9b08
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/SearchPreferenceCategory.java
@@ -0,0 +1,145 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..55be702c4c
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/SetHomepagePreference.java
@@ -0,0 +1,124 @@
+/* -*- 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
new file mode 100644
index 0000000000..350ac8fc0a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/SyncPreference.java
@@ -0,0 +1,103 @@
+/* -*- 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
new file mode 100644
index 0000000000..c1eeb6bd50
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/promotion/AddToHomeScreenPromotion.java
@@ -0,0 +1,237 @@
+/* -*- 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
new file mode 100644
index 0000000000..0f2df8a2c3
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/promotion/HomeScreenPrompt.java
@@ -0,0 +1,237 @@
+/* -*- 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
new file mode 100644
index 0000000000..db5a531c66
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/promotion/ReaderViewBookmarkPromotion.java
@@ -0,0 +1,103 @@
+/* -*- 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
new file mode 100644
index 0000000000..b6b857fb9f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/promotion/SimpleHelperUI.java
@@ -0,0 +1,194 @@
+/* -*- 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
new file mode 100644
index 0000000000..3d66eeea89
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/ColorPickerInput.java
@@ -0,0 +1,59 @@
+/* -*- 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
new file mode 100644
index 0000000000..bc7d7ac206
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/IconGridInput.java
@@ -0,0 +1,171 @@
+/* -*- 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
new file mode 100644
index 0000000000..502f1156d9
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/IntentChooserPrompt.java
@@ -0,0 +1,158 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..1509ab6264
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/IntentHandler.java
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..11121b2cce
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/Prompt.java
@@ -0,0 +1,586 @@
+/* -*- 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
new file mode 100644
index 0000000000..752f5c24cf
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/PromptInput.java
@@ -0,0 +1,398 @@
+/* -*- 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
new file mode 100644
index 0000000000..720086c92f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListAdapter.java
@@ -0,0 +1,281 @@
+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
new file mode 100644
index 0000000000..48ace735c1
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListItem.java
@@ -0,0 +1,128 @@
+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
new file mode 100644
index 0000000000..8155cc1c64
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/PromptService.java
@@ -0,0 +1,72 @@
+/* -*- 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
new file mode 100644
index 0000000000..ab490e79cf
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/TabInput.java
@@ -0,0 +1,107 @@
+/* -*- 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
new file mode 100644
index 0000000000..42a7c6a90b
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/push/Fetched.java
@@ -0,0 +1,71 @@
+/* -*- 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
new file mode 100644
index 0000000000..9c1fab5f99
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/push/PushClient.java
@@ -0,0 +1,110 @@
+/* -*- 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
new file mode 100644
index 0000000000..42ef60b619
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/push/PushManager.java
@@ -0,0 +1,354 @@
+/* -*- 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
new file mode 100644
index 0000000000..a991774ff1
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/push/PushRegistration.java
@@ -0,0 +1,126 @@
+/* -*- 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
new file mode 100644
index 0000000000..8d3a92e480
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/push/PushService.java
@@ -0,0 +1,460 @@
+/* -*- 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.fxa.FxAccountPushHandler;
+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",
+ "FxAccountsPush:Initialized",
+ "FxAccountsPush:ReceivedPushMessageToDecode:Response",
+ "History:GetPrePathLastVisitedTimeMilliseconds",
+ };
+
+ private enum GeckoComponent {
+ FxAccountsPush,
+ 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 boolean isReadyFxAccountsPush = 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!");
+ GeckoAppShell.notifyObservers("FxAccountsPush:ReceivedPushMessageToDecode",
+ message.toString(),
+ GeckoThread.State.PROFILE_READY);
+ }
+
+ 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 ("FxAccountsPush:Initialized".equals(event)) {
+ processComponentState(GeckoComponent.FxAccountsPush, true);
+ 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 ("FxAccountsPush:ReceivedPushMessageToDecode:Response".equals(event)) {
+ FxAccountPushHandler.handleFxAPushMessage(context, message);
+ 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.FxAccountsPush) {
+ isReadyFxAccountsPush = isReady;
+
+ } else if (component == GeckoComponent.PushServiceAndroidGCM) {
+ isReadyPushServiceAndroidGCM = isReady;
+ }
+
+ // Send all pending messages to Gecko.
+ if (canSendPushMessagesToGecko()) {
+ sendPushMessagesToGecko(pendingPushMessages);
+ pendingPushMessages.clear();
+ }
+ }
+
+ private boolean canSendPushMessagesToGecko() {
+ return isReadyFxAccountsPush && 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
new file mode 100644
index 0000000000..686bf5a0d1
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/push/PushState.java
@@ -0,0 +1,137 @@
+/* -*- 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
new file mode 100644
index 0000000000..ecf752591f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/push/PushSubscription.java
@@ -0,0 +1,81 @@
+/* -*- 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
new file mode 100644
index 0000000000..e70aac5b5b
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/reader/ReaderModeUtils.java
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..e01ff79aca
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/reader/ReadingListHelper.java
@@ -0,0 +1,154 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..e60abac71b
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/reader/SavedReaderViewHelper.java
@@ -0,0 +1,247 @@
+/* -*- 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
new file mode 100644
index 0000000000..480078a98d
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/restrictions/DefaultConfiguration.java
@@ -0,0 +1,34 @@
+/* -*- 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
new file mode 100644
index 0000000000..f9663ccf7e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/restrictions/GuestProfileConfiguration.java
@@ -0,0 +1,83 @@
+/* -*- 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
new file mode 100644
index 0000000000..f794c57825
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/restrictions/Restrictable.java
@@ -0,0 +1,112 @@
+/* -*- 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
new file mode 100644
index 0000000000..15a0b97f44
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictedProfileConfiguration.java
@@ -0,0 +1,129 @@
+/* -*- 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
new file mode 100644
index 0000000000..523cc113b4
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionCache.java
@@ -0,0 +1,99 @@
+/* -*- 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
new file mode 100644
index 0000000000..7c40da734b
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionConfiguration.java
@@ -0,0 +1,31 @@
+/* -*- 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
new file mode 100644
index 0000000000..26b9a446f2
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionProvider.java
@@ -0,0 +1,84 @@
+/* -*- 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
new file mode 100644
index 0000000000..0cf6808103
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/restrictions/Restrictions.java
@@ -0,0 +1,127 @@
+/* -*- 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
new file mode 100644
index 0000000000..d4d9938e26
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/search/SearchEngine.java
@@ -0,0 +1,304 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..4b33db40a7
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/search/SearchEngineManager.java
@@ -0,0 +1,764 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..667eb8f6c4
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueueHelper.java
@@ -0,0 +1,357 @@
+/* -*- 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
new file mode 100644
index 0000000000..ead16ccba1
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueuePrompt.java
@@ -0,0 +1,215 @@
+/* -*- 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
new file mode 100644
index 0000000000..ebb1bd761e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueueService.java
@@ -0,0 +1,342 @@
+/* -*- 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
new file mode 100644
index 0000000000..4f5baacdbd
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabReceivedService.java
@@ -0,0 +1,130 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..b7bd833763
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/PrivateTabsPanel.java
@@ -0,0 +1,63 @@
+/* -*- 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
new file mode 100644
index 0000000000..0b6a30d7a0
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabCurve.java
@@ -0,0 +1,70 @@
+/* -*- 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
new file mode 100644
index 0000000000..7b06c994cd
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryController.java
@@ -0,0 +1,87 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..e6deabdcf9
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryFragment.java
@@ -0,0 +1,172 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..112dbc07dc
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryItemRow.java
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..6c608b2ac7
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryPage.java
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..7ea02407e9
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabPanelBackButton.java
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..5d37193433
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStrip.java
@@ -0,0 +1,170 @@
+/* -*- 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
new file mode 100644
index 0000000000..8778aac312
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripAdapter.java
@@ -0,0 +1,98 @@
+/* -*- 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
new file mode 100644
index 0000000000..27eaed125d
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripItemView.java
@@ -0,0 +1,254 @@
+/* -*- 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
new file mode 100644
index 0000000000..f3ec19cef6
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripView.java
@@ -0,0 +1,449 @@
+/* -*- 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
new file mode 100644
index 0000000000..ead7db9fe6
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java
@@ -0,0 +1,712 @@
+/* -*- 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
new file mode 100644
index 0000000000..d5362f1f10
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayout.java
@@ -0,0 +1,216 @@
+/* -*- 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
new file mode 100644
index 0000000000..367da640ff
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutAdapter.java
@@ -0,0 +1,100 @@
+/* -*- 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
new file mode 100644
index 0000000000..975e779d6e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutItemView.java
@@ -0,0 +1,172 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..090d74f9db
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutRecyclerAdapter.java
@@ -0,0 +1,124 @@
+/* -*- 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
new file mode 100644
index 0000000000..8cf2f8eded
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayout.java
@@ -0,0 +1,118 @@
+/* -*- 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
new file mode 100644
index 0000000000..471abf8839
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayoutAnimator.java
@@ -0,0 +1,65 @@
+/* -*- 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
new file mode 100644
index 0000000000..2be1270100
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanel.java
@@ -0,0 +1,456 @@
+/* -*- 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
new file mode 100644
index 0000000000..09254bf767
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanelThumbnailView.java
@@ -0,0 +1,52 @@
+/* -*- 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
new file mode 100644
index 0000000000..36e9e47391
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsTouchHelperCallback.java
@@ -0,0 +1,69 @@
+/* -*- 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
new file mode 100644
index 0000000000..6ed4bb0d44
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryConstants.java
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..fae674b2dc
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryCorePingDelegate.java
@@ -0,0 +1,188 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..c702bb92c0
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryDispatcher.java
@@ -0,0 +1,118 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..b6ee9c2d88
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPing.java
@@ -0,0 +1,34 @@
+/* -*- 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
new file mode 100644
index 0000000000..329f5b8036
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPreferences.java
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..543281174a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java
@@ -0,0 +1,347 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..61229b21b8
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/CampaignIdMeasurements.java
@@ -0,0 +1,37 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..c08ad6c021
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SearchCountMeasurements.java
@@ -0,0 +1,100 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..6f7d2127a1
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SessionMeasurements.java
@@ -0,0 +1,99 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..3f5480f379
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryCorePingBuilder.java
@@ -0,0 +1,247 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..57fa0fd8bf
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryPingBuilder.java
@@ -0,0 +1,87 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..047a646c30
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadAllPingsImmediatelyScheduler.java
@@ -0,0 +1,32 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..63305aad5a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadScheduler.java
@@ -0,0 +1,26 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..d523821465
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryJSONFilePingStore.java
@@ -0,0 +1,301 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..7d781cf26b
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryPingStore.java
@@ -0,0 +1,66 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..07f17590d1
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/text/FloatingActionModeCallback.java
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..7a09624d41
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/text/FloatingToolbarTextSelection.java
@@ -0,0 +1,206 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..9fcbce4a4a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/text/TextAction.java
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..29e8e43f58
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/text/TextSelection.java
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..4a1559823a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/AutocompleteHandler.java
@@ -0,0 +1,10 @@
+/* -*- 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
new file mode 100644
index 0000000000..267c95e096
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/BackButton.java
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..b24e3b3ea5
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbar.java
@@ -0,0 +1,960 @@
+/* -*- 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
new file mode 100644
index 0000000000..a5fc57f1ad
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarPhone.java
@@ -0,0 +1,128 @@
+/* -*- 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
new file mode 100644
index 0000000000..5588ddcd3c
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarPhoneBase.java
@@ -0,0 +1,219 @@
+/* -*- 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
new file mode 100644
index 0000000000..2159341617
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarTablet.java
@@ -0,0 +1,211 @@
+/* -*- 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
new file mode 100644
index 0000000000..e818bb95ca
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbarTabletBase.java
@@ -0,0 +1,182 @@
+/* -*- 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
new file mode 100644
index 0000000000..55567fba30
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/CanvasDelegate.java
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..f95bb5e8a5
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/ForwardButton.java
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..68194e2227
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/NavButton.java
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..9361d59076
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/PageActionLayout.java
@@ -0,0 +1,371 @@
+/* -*- 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
new file mode 100644
index 0000000000..4164854945
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/PhoneTabsButton.java
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..003dada2da
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/ShapedButton.java
@@ -0,0 +1,109 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..c14829aec2
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/ShapedButtonFrameLayout.java
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..14230a2ecb
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/SiteIdentityPopup.java
@@ -0,0 +1,571 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..1e0ca516b4
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/TabCounter.java
@@ -0,0 +1,154 @@
+/* -*- 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
new file mode 100644
index 0000000000..163ed4a510
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarDisplayLayout.java
@@ -0,0 +1,530 @@
+/* -*- 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
new file mode 100644
index 0000000000..c9731a4014
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarEditLayout.java
@@ -0,0 +1,348 @@
+/* -*- 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
new file mode 100644
index 0000000000..b385f815a3
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarEditText.java
@@ -0,0 +1,630 @@
+/* -*- 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
new file mode 100644
index 0000000000..f881de154a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarPrefs.java
@@ -0,0 +1,78 @@
+/* -*- 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
new file mode 100644
index 0000000000..43181cbef5
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/ToolbarProgressView.java
@@ -0,0 +1,195 @@
+/*
+ * 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
new file mode 100644
index 0000000000..dcc62b6d44
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/trackingprotection/TrackingProtectionPrompt.java
@@ -0,0 +1,131 @@
+/* -*- 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
new file mode 100644
index 0000000000..f0ad78e776
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/PostUpdateHandler.java
@@ -0,0 +1,120 @@
+/* -*- 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
new file mode 100644
index 0000000000..7ccc43e282
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/UpdateService.java
@@ -0,0 +1,795 @@
+/* -*- 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
new file mode 100644
index 0000000000..c4d198ae73
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/UpdateServiceHelper.java
@@ -0,0 +1,213 @@
+/* -*- 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
new file mode 100644
index 0000000000..ec227d1ce6
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/util/ColorUtil.java
@@ -0,0 +1,44 @@
+/* -*- 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
new file mode 100644
index 0000000000..f3c9eef837
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/util/DrawableUtil.java
@@ -0,0 +1,66 @@
+/* -*- 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
new file mode 100644
index 0000000000..1e5c2a723c
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/util/ResourceDrawableUtils.java
@@ -0,0 +1,136 @@
+/* -*- 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
new file mode 100644
index 0000000000..6414dec9fb
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/util/TouchTargetUtil.java
@@ -0,0 +1,48 @@
+/* -*- 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
new file mode 100644
index 0000000000..0033e72a0c
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/util/UnusedResourcesUtil.java
@@ -0,0 +1,128 @@
+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
new file mode 100644
index 0000000000..180e821e7c
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/util/ViewUtil.java
@@ -0,0 +1,33 @@
+/* -*- 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
new file mode 100644
index 0000000000..8cde1ee05f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/ActivityChooserModel.java
@@ -0,0 +1,1359 @@
+/*
+ * 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
new file mode 100644
index 0000000000..6bd1e36e46
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/AllCapsTextView.java
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..a504c58320
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/AnchoredPopup.java
@@ -0,0 +1,130 @@
+/* -*- 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
new file mode 100644
index 0000000000..f1343b0fbf
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/AnimatedHeightLayout.java
@@ -0,0 +1,77 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..4f14682034
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/BasicColorPicker.java
@@ -0,0 +1,140 @@
+/* -*- 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
new file mode 100644
index 0000000000..b740592fe2
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/CheckableLinearLayout.java
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..206341212c
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/ClickableWhenDisabledEditText.java
@@ -0,0 +1,25 @@
+/* -*- 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
new file mode 100644
index 0000000000..96b20a6c31
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/ContentSecurityDoorHanger.java
@@ -0,0 +1,127 @@
+/* -*- 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
new file mode 100644
index 0000000000..63cb84c5aa
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/CropImageView.java
@@ -0,0 +1,143 @@
+/* -*- 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
new file mode 100644
index 0000000000..67f1bcd1d8
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/DateTimePicker.java
@@ -0,0 +1,665 @@
+/*
+ * 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
new file mode 100644
index 0000000000..cb8716af74
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/DefaultDoorHanger.java
@@ -0,0 +1,190 @@
+/* -*- 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
new file mode 100644
index 0000000000..5beec3a5c7
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/DefaultItemAnimatorBase.java
@@ -0,0 +1,685 @@
+/* -*- 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
new file mode 100644
index 0000000000..7d32278e43
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/DoorHanger.java
@@ -0,0 +1,220 @@
+/* -*- 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
new file mode 100644
index 0000000000..98f1e57e14
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/DoorhangerConfig.java
@@ -0,0 +1,127 @@
+/* -*- 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
new file mode 100644
index 0000000000..44f88e668f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/EllipsisTextView.java
@@ -0,0 +1,65 @@
+/* -*- 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
new file mode 100644
index 0000000000..b4d1e13d96
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/ExternalIntentDuringPrivateBrowsingPromptFragment.java
@@ -0,0 +1,106 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..08bb55ef63
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/FadedMultiColorTextView.java
@@ -0,0 +1,108 @@
+/* -*- 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
new file mode 100644
index 0000000000..866b7ecbd0
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/FadedSingleColorTextView.java
@@ -0,0 +1,74 @@
+/* -*- 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
new file mode 100644
index 0000000000..e104330832
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/FadedTextView.java
@@ -0,0 +1,48 @@
+/* -*- 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
new file mode 100644
index 0000000000..4652345b4d
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/FaviconView.java
@@ -0,0 +1,268 @@
+/* -*- 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
new file mode 100644
index 0000000000..f1662896e9
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/FilledCardView.java
@@ -0,0 +1,39 @@
+/* -*- 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
new file mode 100644
index 0000000000..042e748510
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/FlowLayout.java
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..d864792a6e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/GeckoActionProvider.java
@@ -0,0 +1,360 @@
+/* -*- 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
new file mode 100644
index 0000000000..7e7f506621
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/GeckoPopupMenu.java
@@ -0,0 +1,189 @@
+/* -*- 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
new file mode 100644
index 0000000000..9c98e8a0d9
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/HistoryDividerItemDecoration.java
@@ -0,0 +1,66 @@
+/* -*- 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
new file mode 100644
index 0000000000..71987bf8c9
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/IconTabWidget.java
@@ -0,0 +1,111 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..232674813b
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/LoginDoorHanger.java
@@ -0,0 +1,228 @@
+/* -*- 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
new file mode 100644
index 0000000000..a0c6049c52
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/RecyclerViewClickSupport.java
@@ -0,0 +1,105 @@
+/* -*- 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
new file mode 100644
index 0000000000..ff0709cb76
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/ResizablePathDrawable.java
@@ -0,0 +1,117 @@
+/* -*- 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
new file mode 100644
index 0000000000..a102981eed
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/RoundedCornerLayout.java
@@ -0,0 +1,79 @@
+/* -*- 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
new file mode 100644
index 0000000000..4d4a922758
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/SiteLogins.java
@@ -0,0 +1,16 @@
+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
new file mode 100644
index 0000000000..0b77e9d1ca
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/SquaredImageView.java
@@ -0,0 +1,21 @@
+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
new file mode 100644
index 0000000000..c0dca0bec3
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/SquaredRelativeLayout.java
@@ -0,0 +1,33 @@
+/* -*- 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
new file mode 100644
index 0000000000..8267fe8a3e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/SwipeDismissListViewTouchListener.java
@@ -0,0 +1,356 @@
+/*
+ * 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
new file mode 100644
index 0000000000..848e2f6ed1
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/TabThumbnailWrapper.java
@@ -0,0 +1,38 @@
+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
new file mode 100644
index 0000000000..5ab00ea7f2
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/ThumbnailView.java
@@ -0,0 +1,86 @@
+/* -*- 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
new file mode 100644
index 0000000000..52e0b1fd04
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/TouchDelegateWithReset.java
@@ -0,0 +1,134 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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
new file mode 100644
index 0000000000..b5ad36ab71
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/TwoWayView.java
@@ -0,0 +1,7191 @@
+/*
+ * 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
new file mode 100644
index 0000000000..c84686e905
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedEditText.java
@@ -0,0 +1,172 @@
+// 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
new file mode 100644
index 0000000000..a95fe2d9f2
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedFrameLayout.java
@@ -0,0 +1,172 @@
+// 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
new file mode 100644
index 0000000000..88e94c6c74
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedImageButton.java
@@ -0,0 +1,200 @@
+// 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
new file mode 100644
index 0000000000..befbe6fb5e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedImageView.java
@@ -0,0 +1,199 @@
+// 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
new file mode 100644
index 0000000000..87ec58ce05
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedLinearLayout.java
@@ -0,0 +1,167 @@
+// 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
new file mode 100644
index 0000000000..14ef25c622
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedRelativeLayout.java
@@ -0,0 +1,172 @@
+// 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
new file mode 100644
index 0000000000..294abd9baf
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedTextSwitcher.java
@@ -0,0 +1,167 @@
+// 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
new file mode 100644
index 0000000000..51a23a406c
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedTextView.java
@@ -0,0 +1,172 @@
+// 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
new file mode 100644
index 0000000000..77ecfd271c
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedView.java
@@ -0,0 +1,172 @@
+// 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
new file mode 100644
index 0000000000..e731a0ebe4
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/themed/ThemedView.java.frag
@@ -0,0 +1,211 @@
+//#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
new file mode 100644
index 0000000000..3b5a00b403
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/themed/generate_themed_views.py
@@ -0,0 +1,72 @@
+#!/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)
diff --git a/mobile/android/base/locales/Makefile.in b/mobile/android/base/locales/Makefile.in
new file mode 100644
index 0000000000..ce0636c126
--- /dev/null
+++ b/mobile/android/base/locales/Makefile.in
@@ -0,0 +1,114 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+include $(topsrcdir)/config/config.mk
+
+# special case some locale codes, he and id
+# http://code.google.com/p/android/issues/detail?id=3639
+AB_rCD = $(if $(filter he, $(AB_CD)),iw,$(if $(filter id, $(AB_CD)),in,$(subst -,-r,$(AB_CD))))
+
+# The search strings path is always passed to strings.xml.in; the
+# decision to include is made based on the feature flag at the
+# inclusion site.
+SEARCHSTRINGSPATH = $(abspath $(call MERGE_FILE,search_strings.dtd))
+
+SYNCSTRINGSPATH = $(abspath $(call MERGE_FILE,sync_strings.dtd))
+STRINGSPATH = $(abspath $(call MERGE_FILE,android_strings.dtd))
+ifeq (,$(XPI_NAME))
+BRANDPATH = $(topobjdir)/dist/bin/chrome/$(AB_CD)/locale/branding/brand.dtd
+else
+BRANDPATH = $(ABS_DIST)/xpi-stage/$(XPI_NAME)/chrome/$(AB_CD)/locale/branding/brand.dtd
+endif
+$(warnIfEmpty,AB_CD) # todo: $(errorIfEmpty )
+
+dir-res-values := ../res/values
+strings-xml := $(dir-res-values)/strings.xml
+strings-xml-in := $(srcdir)/../strings.xml.in
+
+GARBAGE += $(strings-xml)
+
+dir-res-raw := ../res/raw
+suggestedsites := $(dir-res-raw)/suggestedsites.json
+browsersearch := $(dir-res-raw)/browsersearch.json
+
+libs realchrome:: \
+ $(strings-xml) \
+ $(NULL)
+
+chrome-%:: AB_CD=$*
+chrome-%::
+ @$(MAKE) \
+ $(dir-res-values)-$(AB_rCD)/strings.xml \
+ $(dir-res-raw)-$(AB_rCD)/suggestedsites.json \
+ $(dir-res-raw)-$(AB_rCD)/browsersearch.json \
+ AB_CD=$*
+
+# Determine the ../res/values[-*]/ path
+strings-xml-bypath = $(filter %/strings.xml,$(MAKECMDGOALS))
+ifeq (,$(strip $(strings-xml-bypath)))
+ strings-xml-bypath = $(strings-xml)
+endif
+dir-strings-xml = $(patsubst %/,%,$(dir $(strings-xml-bypath)))
+
+strings-xml-preqs =\
+ $(strings-xml-in) \
+ $(BRANDPATH) \
+ $(STRINGSPATH) \
+ $(SEARCHSTRINGSPATH) \
+ $(SYNCSTRINGSPATH) \
+ $(if $(IS_LANGUAGE_REPACK),FORCE) \
+ $(NULL)
+
+$(dir-strings-xml)/strings.xml: $(strings-xml-preqs)
+ $(call py_action,preprocessor, \
+ $(DEFINES) \
+ $(ACDEFINES) \
+ -DANDROID_PACKAGE_NAME=$(ANDROID_PACKAGE_NAME) \
+ -DBRANDPATH='$(BRANDPATH)' \
+ -DMOZ_APP_DISPLAYNAME='@MOZ_APP_DISPLAYNAME@' \
+ -DSTRINGSPATH='$(STRINGSPATH)' \
+ -DSYNCSTRINGSPATH='$(SYNCSTRINGSPATH)' \
+ -DSEARCHSTRINGSPATH='$(SEARCHSTRINGSPATH)' \
+ $< \
+ -o $@)
+
+# Arg 1: Valid Make identifier, like suggestedsites.
+# Arg 2: File name, like suggestedsites.json.
+define generated_file_template
+
+# Determine the ../res/raw[-*] path. This can be ../res/raw when no
+# locale is explicitly specified.
+$(1)-bypath = $(filter %/$(2),$(MAKECMDGOALS))
+ifeq (,$$(strip $$($(1)-bypath)))
+ $(1)-bypath = $($(1))
+endif
+$(1)-dstdir-raw = $$(patsubst %/,%,$$(dir $$($(1)-bypath)))
+
+GARBAGE += $($(1))
+
+libs realchrome:: $($(1))
+endef
+
+# L10NBASEDIR is not defined for en-US.
+l10n-srcdir := $(if $(filter en-US,$(AB_CD)),,$(or $(realpath $(L10NBASEDIR)),$(abspath $(L10NBASEDIR)))/$(AB_CD)/mobile/chrome)
+
+$(eval $(call generated_file_template,suggestedsites,suggestedsites.json))
+
+$(suggestedsites-dstdir-raw)/suggestedsites.json: FORCE
+ $(call py_action,generate_suggestedsites, \
+ --verbose \
+ --android-package-name=$(ANDROID_PACKAGE_NAME) \
+ --resources=$(srcdir)/../resources \
+ $(if $(filter en-US,$(AB_CD)),,--srcdir=$(l10n-srcdir)) \
+ --srcdir=$(topsrcdir)/mobile/locales/en-US/chrome \
+ $@)
+
+$(eval $(call generated_file_template,browsersearch,browsersearch.json))
+
+$(browsersearch-dstdir-raw)/browsersearch.json: FORCE
+ $(call py_action,generate_browsersearch, \
+ --verbose \
+ $(if $(filter en-US,$(AB_CD)),,--srcdir=$(l10n-srcdir)) \
+ --srcdir=$(topsrcdir)/mobile/locales/en-US/chrome \
+ $@)
diff --git a/mobile/android/base/locales/en-US/android_strings.dtd b/mobile/android/base/locales/en-US/android_strings.dtd
new file mode 100644
index 0000000000..140e2a2168
--- /dev/null
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -0,0 +1,848 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY firstrun_panel_title_welcome "Welcome">
+
+<!ENTITY firstrun_urlbar_message "Welcome to &brandShortName;">
+<!ENTITY firstrun_urlbar_subtext "Find things faster with helpful search suggestion shortcuts.">
+<!ENTITY firstrun_bookmarks_title "History">
+<!ENTITY firstrun_bookmarks_message "Your faves, front and center">
+<!ENTITY firstrun_bookmarks_subtext "Get results from your bookmarks and history when you search.">
+<!ENTITY firstrun_data_title "Data">
+<!ENTITY firstrun_data_message "Less data, more savings">
+<!ENTITY firstrun_data_subtext2 "Turn off images to spend less data on every site you visit.">
+<!ENTITY firstrun_sync_title "Sync">
+<!ENTITY firstrun_sync_message "&brandShortName;, always by your side">
+<!ENTITY firstrun_sync_subtext "Sync your tabs, passwords, and more everywhere you use it.">
+<!ENTITY firstrun_signin_message "Get connected, get started">
+<!ENTITY firstrun_signin_button "Sign in to Sync">
+<!ENTITY onboard_start_button_browser "Start Browsing">
+<!ENTITY firstrun_button_notnow "Not right now">
+<!ENTITY firstrun_button_next "Next">
+
+<!ENTITY firstrun_tabqueue_title "Links">
+<!-- Localization note (firstrun_tabqueue_message): 'Tab queue' is a feature that allows users to queue up or save links from outside of Firefox (without switching apps) - these links will be loaded in Firefox the next time Firefox is opened. -->
+<!ENTITY firstrun_tabqueue_message_off "Turn on Tab queue">
+<!ENTITY firstrun_tabqueue_subtext_off "Save links for later in &brandShortName; when tapping them in other apps.">
+
+<!ENTITY firstrun_tabqueue_message_on "Success!">
+<!ENTITY firstrun_tabqueue_subtext_on "You can always turn this off in &settings; under &pref_category_general;.">
+
+<!ENTITY firstrun_readerview_title "Articles">
+<!-- Localization note (firstrun_readerview_message): This is a casual way of describing getting rid of unnecessary things, and is referring to simplifying websites so only the article text and images are visible, removing unnecessary headers or ads. -->
+<!ENTITY firstrun_readerview_message "Lose the clutter">
+<!ENTITY firstrun_readerview_subtext "Use Reader View to make articles nicer to read \u2014 even offline.">
+
+<!-- Localization note (firstrun_devices_title): This is a casual way of addressing the user, somewhat referring to their online identity (which would include other devices, Firefox usage, accounts, etc). -->
+<!ENTITY firstrun_account_title "You">
+<!ENTITY firstrun_account_message "Have &brandShortName; on another device?">
+
+<!ENTITY onboard_start_restricted1 "Stay safe and in control with this simplified version of &brandShortName;.">
+
+<!-- Localization note: These are used as the titles of different pages on the home screen.
+ They are automatically converted to all caps by the Android platform. -->
+<!ENTITY bookmarks_title "Bookmarks">
+<!ENTITY history_title "History">
+
+<!ENTITY switch_to_tab "Switch to tab">
+
+<!-- Localization note: Shown in a snackbar when tab is loaded from cache while device was offline. -->
+<!ENTITY tab_offline_version "Showing offline version">
+
+<!ENTITY crash_reporter_title "&brandShortName; Crash Reporter">
+<!ENTITY crash_message2 "&brandShortName; had a problem and crashed. Your tabs should be listed on the &brandShortName; Start page when you restart.">
+<!ENTITY crash_send_report_message3 "Tell &vendorShortName; about this crash so they can fix it">
+<!ENTITY crash_include_url2 "Include the address of the page I was on">
+<!ENTITY crash_sorry "We\'re sorry">
+<!ENTITY crash_comment "Add a comment (comments are publicly visible)">
+<!ENTITY crash_allow_contact2 "Allow &vendorShortName; to contact me about this report">
+<!ENTITY crash_email "Your email">
+<!ENTITY crash_closing_alert "Exit without sending a crash report?">
+<!ENTITY sending_crash_report "Sending crash report\u2026">
+<!ENTITY crash_close_label "Close">
+<!ENTITY crash_restart_label "Restart &brandShortName;">
+
+<!ENTITY url_bar_default_text2 "Search or enter address">
+
+<!ENTITY bookmark "Bookmark">
+<!ENTITY bookmark_remove "Remove bookmark">
+<!ENTITY bookmark_added "Bookmark added">
+<!-- Localization note (bookmark_already_added) : This string is
+ used as a label in a toast. It is the verb "to bookmark", not
+ the noun "a bookmark". -->
+<!ENTITY bookmark_already_added "Already bookmarked">
+<!ENTITY bookmark_removed "Bookmark removed">
+<!ENTITY bookmark_updated "Bookmark updated">
+<!ENTITY bookmark_options "Options">
+<!ENTITY screenshot_added_to_bookmarks "Screenshot added to bookmarks">
+<!-- Localization note (screenshot_folder_label_in_bookmarks): We save links to screenshots
+ the user takes. The folder we store these links in is located in the bookmarks list
+ and is labeled by this String. -->
+<!ENTITY screenshot_folder_label_in_bookmarks "Screenshots">
+<!ENTITY readinglist_smartfolder_label_in_bookmarks "Reading List">
+
+<!-- Localization note (bookmark_folder_items): The variable is replaced by the number of items
+ in the folder. -->
+<!ENTITY bookmark_folder_items "&formatD; items">
+<!ENTITY bookmark_folder_one_item "1 item">
+
+<!ENTITY reader_saved_offline "Saved offline">
+<!-- Localization note (reader_switch_to_bookmarks) : This
+ string is used as an action in a snackbar - it lets you
+ "switch" to the bookmarks (saved items) panel. -->
+<!ENTITY reader_switch_to_bookmarks "Switch">
+
+<!ENTITY history_today_section "Today">
+<!ENTITY history_yesterday_section "Yesterday">
+<!ENTITY history_week_section3 "Last 7 days">
+<!ENTITY history_older_section3 "Older than 6 months">
+
+<!ENTITY search "Search">
+<!ENTITY reload "Reload">
+<!ENTITY forward "Forward">
+<!ENTITY menu "Menu">
+<!ENTITY back "Back">
+<!ENTITY stop "Stop">
+<!ENTITY site_security "Site Security">
+<!ENTITY edit_mode_cancel "Cancel">
+
+<!ENTITY close_tab "Close Tab">
+<!ENTITY one_tab "1 tab">
+<!-- Localization note (num_tabs2) : Number of tabs is always more than one.
+ We can't use android plural forms, sadly. See bug #753859. -->
+<!ENTITY num_tabs2 "&formatD; tabs">
+<!ENTITY new_tab_opened "New tab opened">
+<!ENTITY new_private_tab_opened "New private tab opened">
+<!-- Localization note (switch_button_message): This string should be as short
+ as possible because it's shown as a label in a toast. Ideally, this string
+ is upper-case, to match Google and Android's convention. -->
+<!ENTITY switch_button_message "SWITCH">
+<!-- Localization note (tab_title_prefix_is_playing_audio): This string is not
+ visible in the UI, but rather used as a text-to-speech content description
+ for sight-impaired a11y users. The content description is set on a tab
+ title in a list of open tabs when content in that tab is playing audio.
+ &formatS; will be replaced with the title of the tab, as received from the
+ web page. When audio is not playing in a tab, &formatS; will be used as
+ the content description. -->
+<!ENTITY tab_title_prefix_is_playing_audio "Playing audio – &formatS;">
+
+<!ENTITY settings "Settings">
+<!ENTITY settings_title "Settings">
+<!ENTITY pref_category_general "General">
+<!ENTITY pref_category_general_summary3 "Home, language, tab queue">
+
+<!-- Localization note (pref_category_language) : This is the preferences
+ section in which the user picks the locale in which to display Firefox
+ UI. The locale includes both language and region concepts. -->
+<!ENTITY pref_category_language "Language">
+<!ENTITY pref_category_language_summary "Change the language of your browser">
+<!ENTITY pref_browser_locale "Browser language">
+
+<!-- Localization note (locale_system_default) : This string indicates that
+ Firefox will use the locale currently selected in Android's settings
+ to display browser chrome. -->
+<!ENTITY locale_system_default "System default">
+
+<!-- Localization note (overlay_share_label) : This is the label that appears
+ in Android's intent chooser when sending a link to Firefox to bookmark,
+ send to another device, or add to Reading List. -->
+<!ENTITY overlay_share_label "Add to &brandShortName;">
+
+<!-- Localization note (overlay_share_bookmark_btn_label) : This string is
+ used in the share overlay menu to select an action. It is the verb
+ "to bookmark", not the noun "a bookmark". -->
+<!ENTITY overlay_share_bookmark_btn_label "Bookmark">
+<!ENTITY overlay_share_bookmark_btn_label_already "Already bookmarked">
+<!ENTITY overlay_share_send_other "Send to other devices">
+
+<!-- Localization note (overlay_share_send_tab_btn_label) : Used on the
+ share overlay menu to represent the "Send Tab" action when the user
+ either has not set up Sync, or has no other devices to send a tab
+ to. -->
+<!ENTITY overlay_share_send_tab_btn_label "Send to another device">
+<!ENTITY overlay_share_no_url "No link found in this share">
+<!ENTITY overlay_share_select_device "Select device">
+<!-- Localization note (overlay_no_synced_devices) : Used when the menu option
+ to send a tab to a synced device is pressed and no other synced devices
+ are found. -->
+<!ENTITY overlay_no_synced_devices "No Firefox Account connected devices found">
+
+<!ENTITY pref_category_search3 "Search">
+<!ENTITY pref_category_search_summary2 "Add, set default, show suggestions">
+<!ENTITY pref_category_accessibility "Accessibility">
+<!ENTITY pref_category_accessibility_summary2 "Text size, zoom, voice input">
+<!ENTITY pref_category_privacy_short "Privacy">
+<!ENTITY pref_category_privacy_summary4 "Tracking, logins, data choices">
+<!ENTITY pref_category_vendor2 "&vendorShortName; &brandShortName;">
+<!ENTITY pref_category_vendor_summary2 "About &brandShortName;, FAQs, feedback">
+<!ENTITY pref_category_datareporting "Data choices">
+<!ENTITY pref_category_logins "Logins">
+<!ENTITY pref_learn_more "Learn more">
+<!ENTITY pref_category_installed_search_engines "Installed search engines">
+<!ENTITY pref_category_add_search_providers "Add more search providers">
+<!ENTITY pref_category_search_restore_defaults "Restore search engines">
+<!ENTITY pref_search_restore_defaults "Restore defaults">
+<!ENTITY pref_search_restore_defaults_summary "Restore defaults">
+<!-- Localization note (pref_search_hint) : "TIP" as in "hint", "clue" etc. Displayed as an
+ advisory message on the customise search providers settings page explaining how to add new
+ search providers.
+ The &formatI; in the string will be replaced by a small image of the icon described, and can be moved to wherever
+ it is applicable. -->
+<!ENTITY pref_search_hint2 "TIP: Add any website to your list of search providers by long-pressing on its search field and then touching the &formatI; icon.">
+<!ENTITY pref_category_advanced "Advanced">
+<!-- Localization note (pref_category_advanced_summary3): “data saver†in this
+ context means consuming less data, e.g. by not loading images, not
+ “storing dataâ€. -->
+<!ENTITY pref_category_advanced_summary3 "Restore tabs, data saver, developer tools">
+<!ENTITY pref_category_notifications "Notifications">
+<!ENTITY pref_category_notifications_summary "New features, website updates">
+<!ENTITY pref_content_notifications "Website updates">
+<!ENTITY pref_content_notifications_summary2 "Discover new content from supported sites">
+<!ENTITY pref_developer_remotedebugging_usb "Remote debugging via USB">
+<!ENTITY pref_developer_remotedebugging_wifi "Remote debugging via Wi-Fi">
+<!ENTITY pref_developer_remotedebugging_wifi_disabled_summary "Wi-Fi debugging requires your device to have a QR code reader app installed.">
+<!ENTITY pref_remember_signons2 "Remember logins">
+<!ENTITY pref_manage_logins "Manage logins">
+
+<!ENTITY pref_category_home "Home">
+<!ENTITY pref_category_home_summary "Customize your homepage">
+<!ENTITY pref_category_home_panels "Panels">
+<!ENTITY pref_category_home_add_ons "Add-ons">
+<!ENTITY pref_home_updates2 "Content updates">
+<!ENTITY pref_home_updates_enabled "Enabled">
+<!ENTITY pref_home_updates_wifi "Only over Wi-Fi">
+<!ENTITY pref_category_home_homepage "Homepage">
+<!ENTITY home_homepage_title "Set a Homepage">
+<!-- Localization note (home_homepage_radio_user_address): The user will see a series of radio
+ buttons to choose the homepage they'd like to start on. When they click the radio
+ button for this string, they will use the built-in default Firefox homepage (about:home). -->
+<!ENTITY home_homepage_radio_default "&brandShortName; Home">
+<!-- Localization note (home_homepage_radio_user_address): The user will see a series of radio
+ buttons to choose the homepage they'd like to start on. When they click the radio
+ button for this string, a text field will appear below the radio button and allow the
+ user to insert an address of their choice. -->
+<!ENTITY home_homepage_radio_user_address "Custom">
+<!-- Localization note (home_homepage_hint_user_address): The user will see a series of
+ radio buttons to choose the homepage they'd like to start on. When they click a
+ particular radio button, a text field will appear below the radio button and allow the
+ user to insert an address of their choice. This string is the hint text to that
+ text field. -->
+<!ENTITY home_homepage_hint_user_address "Enter address or search term">
+
+<!-- Localization note: These are shown in the left sidebar on tablets -->
+<!ENTITY pref_header_general "General">
+<!ENTITY pref_header_search "Search">
+<!ENTITY pref_header_privacy_short "Privacy">
+<!ENTITY pref_header_accessibility "Accessibility">
+<!ENTITY pref_header_notifications "Notifications">
+<!ENTITY pref_header_advanced "Advanced">
+<!ENTITY pref_header_help "Help">
+<!ENTITY pref_header_vendor "&vendorShortName;">
+
+<!ENTITY pref_cookies_menu "Cookies">
+<!ENTITY pref_cookies_accept_all "Enabled">
+<!ENTITY pref_cookies_not_accept_foreign "Enabled, excluding 3rd party">
+<!ENTITY pref_cookies_disabled "Disabled">
+
+<!-- Localization note (pref_category_data_saver): “data saver†in this
+ context means consuming less data, e.g. by not loading images, not
+ “storing dataâ€. -->
+<!ENTITY pref_category_data_saver "Data saver">
+<!ENTITY pref_category_media "Media">
+<!ENTITY pref_category_developer_tools "Developer tools">
+
+<!ENTITY pref_tap_to_load_images_title2 "Show images">
+<!ENTITY pref_tap_to_load_images_enabled "Always">
+<!ENTITY pref_tap_to_load_images_data "Only over Wi-Fi">
+<!ENTITY pref_tap_to_load_images_disabled2 "Blocked">
+
+<!ENTITY pref_show_web_fonts "Show web fonts">
+<!ENTITY pref_show_web_fonts_summary2 "Download remote fonts when loading a page">
+
+<!ENTITY pref_tracking_protection_title2 "Tracking Protection">
+<!ENTITY pref_tracking_protection_summary3 "Enabled in Private Browsing">
+<!ENTITY pref_donottrack_title "Do not track">
+<!ENTITY pref_donottrack_summary "&brandShortName; will tell sites that you do not want to be tracked">
+
+<!ENTITY pref_tracking_protection_enabled "Enabled">
+<!ENTITY pref_tracking_protection_enabled_pb "Enabled in Private Browsing">
+<!ENTITY pref_tracking_protection_disabled "Disabled">
+
+<!ENTITY pref_whats_new_notification "What\'s new in &brandShortName;">
+<!ENTITY pref_whats_new_notification_summary "Learn about new features after an update">
+
+<!-- Localization note (pref_category_experimental): Title of a sub category in the 'advanced' category
+ for experimental features. -->
+<!ENTITY pref_category_experimental "Experimental features">
+
+<!-- Custom Tabs is an Android API for allowing third-party apps to open URLs in a customized UI.
+ Instead of switching to the browser it appears as if the user stays in the third-party app.
+ For more see: https://developer.chrome.com/multidevice/android/customtabs -->
+<!ENTITY pref_custom_tabs "Custom Tabs">
+<!ENTITY pref_custom_tabs_summary3 "Allow apps to open websites using a customized version of &brandShortName;">
+
+<!-- Localization note (pref_activity_stream): Experimental feature, see https://testpilot.firefox.com/experiments/activity-stream -->
+<!ENTITY pref_activity_stream "Activity Stream">
+<!ENTITY pref_activity_stream_summary "A rich visual history feed and a reimagined home page make it easier than ever to find exactly what you\'re looking for in &brandShortName;.">
+
+<!ENTITY tracking_protection_prompt_title "Now with Tracking Protection">
+<!ENTITY tracking_protection_prompt_text "Actively block tracking elements so you don\'t have to worry.">
+<!ENTITY tracking_protection_prompt_tip_text "Visit Privacy settings to learn more">
+<!ENTITY tracking_protection_prompt_action_button "Got it!">
+
+<!ENTITY tab_queue_toast_message3 "Tab saved in &brandShortName;">
+<!ENTITY tab_queue_toast_action "Open now">
+<!ENTITY tab_queue_prompt_title "Opening multiple links?">
+<!ENTITY tab_queue_prompt_text4 "Save them until the next time you open &brandShortName;.">
+<!ENTITY tab_queue_prompt_tip_text2 "You can change this later in Settings">
+<!-- Localization note (tab_queue_prompt_permit_drawing_over_apps): This additional text is shown if the
+ user needs to enable an Android setting in order to enable tab queues. -->
+<!ENTITY tab_queue_prompt_permit_drawing_over_apps "Turn on Permit drawing over other apps">
+<!ENTITY tab_queue_prompt_positive_action_button "Enable">
+<!ENTITY tab_queue_prompt_negative_action_button "Not now">
+<!-- Localization note (tab_queue_prompt_settings_button): This button is shown if the user needs to
+ enable a permission in Android's setting in order to enable tab queues. -->
+<!ENTITY tab_queue_prompt_settings_button "Go to Settings">
+<!ENTITY tab_queue_notification_title "&brandShortName;">
+<!-- Localization note (tab_queue_notification_text_plural2) : The
+ formatD is replaced with the number of tabs queued. The
+ number of tabs queued is always more than one. We can't use
+ Android plural forms, sadly. See Bug #753859. -->
+<!ENTITY tab_queue_notification_text_plural2 "&formatD; tabs waiting">
+<!-- Localization note (tab_queue_notification_text_singular2) : This is the
+ text of a notification; we expect only one tab queued. -->
+<!ENTITY tab_queue_notification_text_singular2 "1 tab waiting">
+
+<!-- Localization note (tab_queue_notification_settings): This notification text is shown if a tab
+ has been queued but we are missing the system permission to show an overlay. -->
+<!ENTITY tab_queue_notification_settings "To \&quot;Open multiple links\&quot;, please enable the \'Draw over other apps\' permission for &brandShortName;">
+
+<!ENTITY content_notification_summary "&brandShortName;">
+<!-- Localization note (content_notification_title_plural): &formatD; will be replaced with the number of websites that
+ have been updated (new content is available). The number of websites is always more than one (>= 2). For a single
+ update the website title is used instead of this string.
+ We can't use Android plural forms, sadly. See Bug #753859. -->
+<!ENTITY content_notification_title_plural "&formatD; websites updated">
+<!-- Localization note (content_notification_action_settings2): This label will be shown as an action in a content notification.
+ Clicking the action will jump to the notification settings of the app. -->
+<!ENTITY content_notification_action_settings2 "Settings">
+<!-- Localization note(content_notification_action_read_now): This label will be shown as an action in a content notification.
+ Clicking the action will open all new content in the browser. -->
+<!ENTITY content_notification_action_read_now "Read now">
+<!-- Localization note (content_notification_updated_on): &formatS; will be replaced with a medium sized version of the
+ date, depending on locale. For en_US this is for example: Feb 24, 2016. For more details see the Android developer
+ documentation for DateFormat.getMediumDateFormat(). -->
+<!ENTITY content_notification_updated_on "Updated on &formatS;">
+
+<!ENTITY pref_char_encoding "Character encoding">
+<!ENTITY pref_char_encoding_on "Show menu">
+<!ENTITY pref_char_encoding_off "Don\'t show menu">
+<!ENTITY pref_clear_private_data2 "Clear private data">
+<!-- Localization note (pref_clear_private_data_now_tablet): This action to clear private data is only shown on tablets.
+ The action is shown below a header saying "Clear private data"; See pref_clear_private_data -->
+<!ENTITY pref_clear_private_data_now_tablet "Clear now">
+<!ENTITY pref_clear_on_exit_title3 "Clear private data on exit">
+<!ENTITY pref_clear_on_exit_summary2 "&brandShortName; will automatically clear your data whenever you select \u0022Quit\u0022 from the main menu">
+<!ENTITY pref_clear_on_exit_dialog_title "Select which data to clear">
+<!ENTITY pref_plugins "Plugins">
+<!ENTITY pref_plugins_enabled "Enabled">
+<!ENTITY pref_plugins_tap_to_play2 "Touch to play">
+<!ENTITY pref_plugins_disabled "Disabled">
+<!ENTITY pref_text_size "Text size">
+<!ENTITY pref_restore_tabs "Restore tabs">
+<!ENTITY pref_restore_always "Always restore">
+<!ENTITY pref_restore_quit "Don\'t restore after quitting &brandShortName;">
+<!ENTITY pref_font_size_tiny "Tiny">
+<!ENTITY pref_font_size_small "Small">
+<!ENTITY pref_font_size_medium "Medium">
+<!ENTITY pref_font_size_large "Large">
+<!ENTITY pref_font_size_xlarge "Extra Large">
+<!ENTITY pref_font_size_set "Set">
+<!-- Localization note (pref_font_size_adjust_char): A button with a small version of this character
+(or combination of characters) is used to decrease the preview font size; a larger version of the
+same character/combination is used to increase the preview font size. It should be a concise
+representation of the language it is used in that will help show the text in the preview will change
+size. -->
+<!ENTITY pref_font_size_adjust_char "A">
+
+<!-- Localization note (pref_font_size_preview_text): This paragraph is used as an example to
+ demonstrate the font size setting. It is meant to be whimsical and fun. -->
+<!ENTITY pref_font_size_preview_text "The quick orange fox jumps over your expectations with more speed, more flexibility and more security. As a non-profit, we\'re free to innovate on your behalf without any pressure to compromise. That means a better experience for you and a brighter future for the Web.">
+
+<!ENTITY pref_media_autoplay_enabled "Allow autoplay">
+<!ENTITY pref_media_autoplay_enabled_summary "Control if websites can autoplay videos and other media content">
+<!ENTITY pref_zoom_force_enabled "Always enable zoom">
+<!ENTITY pref_zoom_force_enabled_summary "Force override so you can zoom any page">
+<!ENTITY pref_voice_input "Voice input">
+<!ENTITY pref_voice_input_summary2 "Allow voice dictation in the URL bar">
+<!ENTITY pref_qrcode_enabled "QR code reader">
+<!ENTITY pref_qrcode_enabled_summary2 "Allow QR scanner in the URL bar">
+
+<!ENTITY pref_use_master_password "Use master password">
+<!ENTITY pref_sync2 "Sign in">
+<!ENTITY pref_sync_summary2 "Sync your tabs, bookmarks, logins, history">
+<!ENTITY pref_search_suggestions "Show search suggestions">
+<!ENTITY pref_history_search_suggestions "Show search history">
+<!ENTITY pref_import_options "Import options">
+<!ENTITY pref_import_android_summary "Import bookmarks and history from the native browser">
+<!ENTITY pref_private_data_history2 "Browsing history">
+<!ENTITY pref_private_data_searchHistory "Search history">
+<!ENTITY pref_private_data_formdata2 "Form history">
+<!ENTITY pref_private_data_cookies2 "Cookies &amp; active logins">
+<!ENTITY pref_private_data_passwords2 "Saved logins">
+<!ENTITY pref_private_data_cache "Cache">
+<!ENTITY pref_private_data_offlineApps "Offline website data">
+<!ENTITY pref_private_data_siteSettings2 "Site settings">
+<!ENTITY pref_private_data_downloadFiles2 "Downloads">
+<!ENTITY pref_private_data_syncedTabs "Synced tabs">
+
+<!ENTITY pref_default_browser "Make default browser">
+<!ENTITY pref_default_browser_mozilla_support_tablet "Visit Mozilla Support">
+<!ENTITY pref_about_firefox "About &brandShortName;">
+<!ENTITY pref_vendor_faqs "FAQs">
+<!ENTITY pref_vendor_feedback "Give feedback">
+
+<!ENTITY pref_dialog_set_default "Set as default">
+<!ENTITY pref_dialog_default "Default">
+<!ENTITY pref_dialog_remove "Remove">
+
+<!ENTITY pref_search_last_toast "You can\'t remove or disable your last search engine.">
+
+<!ENTITY pref_panels_show "Show">
+<!ENTITY pref_panels_hide "Hide">
+<!ENTITY pref_panels_reorder "Change order">
+<!ENTITY pref_panels_move_up "Move up">
+<!ENTITY pref_panels_move_down "Move down">
+
+<!ENTITY datareporting_notification_title "&brandShortName; stats &amp; data">
+<!ENTITY datareporting_notification_action "Choose what to share">
+<!-- Used in datareporting_notification_ticket_text, but unused in strings.xml. -->
+<!ENTITY datareporting_notification_action_long "Choose what information to share">
+<!ENTITY datareporting_notification_summary "To improve your experience, &brandShortName; automatically sends some information to &vendorShortName;.">
+<!-- When this item is removed, also remove datareporting_notification_action_long:
+ it is unused in strings.xml. -->
+<!ENTITY datareporting_notification_ticker_text "&datareporting_notification_title;: &datareporting_notification_action_long;">
+
+<!-- Localization note (datareporting_fhr_title, datareporting_fhr_summary2,
+ reporting_telemetry_title, datareporting_telemetry_summary,
+ datareporting_crashreporter_summary) : These match the strings in
+ en-US/chrome/browser/preferences/advanced.dtd (healthReportSection.label,
+ healthReportDesc.label, telemetrySection.label, telemetryDesc.label,
+ crashReporterDesc.label). -->
+<!ENTITY datareporting_fhr_title "&brandShortName; Health Report">
+<!ENTITY datareporting_fhr_summary2 "Shares data with &vendorShortName; about your browser health and helps you understand your browser performance">
+<!ENTITY datareporting_abouthr_title "View my Health Report">
+<!ENTITY datareporting_telemetry_title "Telemetry">
+<!ENTITY datareporting_telemetry_summary "Shares performance, usage, hardware and customization data about your browser with &vendorShortName; to help us make &brandShortName; better">
+<!ENTITY datareporting_crashreporter_summary "&brandShortName; submits crash reports to help &vendorShortName; make your browser more stable and secure">
+<!-- Localization note (datareporting_crashreporter_title_short) : This string matches
+ (crashReporterSection.label) in en-US/chrome/browser/preferences/advanced.dtd.-->
+<!ENTITY datareporting_crashreporter_title_short "Crash Reporter">
+<!ENTITY datareporting_wifi_title2 "&vendorShortName; Location Service">
+<!ENTITY datareporting_wifi_geolocation_summary4 "Help &vendorShortName; map the world! Share the approximate Wi-Fi and cellular location of your device to improve our geolocation service.">
+<!-- Localization note (pref_update_autodownload2) : This should mention downloading
+ specifically, since the pref only prevents automatic downloads and not the
+ actual notification that an update is available. -->
+<!ENTITY pref_update_autodownload3 "Automatic updates">
+<!ENTITY pref_update_autodownload_wifi "Only over Wi-Fi">
+<!ENTITY pref_update_autodownload_never "Never">
+<!ENTITY pref_update_autodownload_always "Always">
+
+<!-- Localization note (help_menu) : This string is used in the main menu-->
+<!ENTITY help_menu "Help">
+
+<!ENTITY quit "Quit">
+
+<!ENTITY addons "Add-ons">
+<!ENTITY logins "Logins">
+<!ENTITY downloads "Downloads">
+<!ENTITY char_encoding "Character Encoding">
+
+<!ENTITY share "Share">
+<!ENTITY share_title "Share via">
+<!ENTITY share_image_failed "Unable to share this image">
+<!ENTITY save_as_pdf "Save as PDF">
+<!ENTITY print "Print">
+<!ENTITY find_in_page "Find in page">
+<!ENTITY desktop_mode "Request desktop site">
+<!ENTITY page "Page">
+<!ENTITY tools "Tools">
+<!ENTITY new_tab "New tab">
+<!ENTITY new_private_tab "New private tab">
+<!ENTITY close_all_tabs "Close All Tabs">
+<!ENTITY close_private_tabs "Close Private Tabs">
+<!ENTITY tabs_normal "Tabs">
+<!ENTITY tabs_private "Private">
+<!ENTITY set_image_fail "Unable to set image">
+<!ENTITY set_image_path_fail "Unable to save image">
+<!ENTITY set_image_chooser_title "Set Image As">
+
+<!-- Localization note (find_text, find_prev, find_next, find_close) : These strings are used
+ as alternate text for accessibility. They are not visible in the UI. -->
+<!ENTITY find_text "Find in Page">
+<!ENTITY find_prev "Previous">
+<!ENTITY find_next "Next">
+<!ENTITY find_close "Close">
+
+<!-- Localization note (media_sending_to, media_play, media_pause, media_stop) : These strings are used
+ as alternate text for accessibility. They are not visible in the UI. -->
+<!ENTITY media_sending_to "Sending to Device">
+<!ENTITY media_play "Play">
+<!ENTITY media_pause "Pause">
+<!ENTITY media_stop "Stop">
+
+<!ENTITY contextmenu_open_new_tab "Open in New Tab">
+<!ENTITY contextmenu_open_private_tab "Open in Private Tab">
+<!ENTITY contextmenu_remove "Remove">
+<!ENTITY contextmenu_add_to_launcher "Add to Home Screen">
+<!ENTITY contextmenu_share "Share">
+<!ENTITY contextmenu_pasteandgo "Paste &amp; Go">
+<!ENTITY contextmenu_paste "Paste">
+<!ENTITY contextmenu_copyurl "Copy Address">
+<!ENTITY contextmenu_edit_bookmark "Edit">
+<!ENTITY contextmenu_subscribe "Subscribe to Page">
+<!ENTITY contextmenu_site_settings "Edit Site Settings">
+<!ENTITY contextmenu_top_sites_edit "Edit">
+<!ENTITY contextmenu_top_sites_pin "Pin Site">
+<!ENTITY contextmenu_top_sites_unpin "Unpin Site">
+<!ENTITY contextmenu_add_search_engine "Add a Search Engine">
+
+<!-- Localization note (doorhanger_login_no_username): This string is used in the save-login doorhanger
+ where normally a username would be displayed. In this case, no username was found, and this placeholder
+ contains brackets to indicate this is not actually a username, but rather a placeholder -->
+<!ENTITY doorhanger_login_no_username "[No username]">
+<!ENTITY doorhanger_login_edit_title "Edit login">
+<!ENTITY doorhanger_login_edit_username_hint "Username">
+<!ENTITY doorhanger_login_edit_password_hint "Password">
+<!ENTITY doorhanger_login_edit_toggle "Show password">
+<!ENTITY doorhanger_login_edit_toast_error "Failed to save login">
+<!ENTITY doorhanger_login_select_message "Copy password from &formatS;?">
+<!ENTITY doorhanger_login_select_toast_copy "Password copied to clipboard">
+<!ENTITY doorhanger_login_select_toast_copy_error "Couldn\'t copy password">
+<!ENTITY doorhanger_login_select_action_text "Select another login">
+<!ENTITY doorhanger_login_select_title "Copy password from">
+
+<!-- Localization note (pref_prevent_magnifying_glass): Label for setting that controls
+ whether or not the magnifying glass is disabled. -->
+<!ENTITY pref_magnifying_glass_enabled "Magnify small areas">
+<!ENTITY pref_magnifying_glass_enabled_summary2 "Enlarge links and form fields when touching near them">
+
+<!-- Localization note (pref_scroll_title_bar2): Label for setting that controls
+ whether or not the dynamic toolbar is enabled. -->
+<!ENTITY pref_scroll_title_bar2 "Full-screen browsing">
+<!ENTITY pref_scroll_title_bar_summary2 "Hide the &brandShortName; toolbar when scrolling down a page">
+
+<!ENTITY pref_tab_queue_title3 "Tab queue">
+<!ENTITY pref_tab_queue_summary4 "Save links until the next time you open &brandShortName;">
+
+<!-- Localization note (page_removed): This string appears in a toast message when
+ any page is removed frome about:home. This includes pages that are in history,
+ bookmarks, or reading list. -->
+<!ENTITY page_removed "Page removed">
+
+<!ENTITY bookmark_edit_title "Edit Bookmark">
+<!ENTITY bookmark_edit_name "Name">
+<!ENTITY bookmark_edit_location "Location">
+<!ENTITY bookmark_edit_keyword "Keyword">
+
+<!-- Localization note (site_settings_*) : These strings are used in the "Site Settings"
+ dialog that appears after selecting the "Edit Site Settings" context menu item. -->
+<!ENTITY site_settings_title3 "Site Settings">
+<!ENTITY site_settings_cancel "Cancel">
+<!ENTITY site_settings_clear "Clear">
+
+<!-- Localization note : These strings are used as alternate text for accessibility.
+ They are not visible in the UI. -->
+<!ENTITY page_action_dropmarker_description "Additional Actions">
+
+<!ENTITY masterpassword_create_title "Create Master Password">
+<!ENTITY masterpassword_remove_title "Remove Master Password">
+<!ENTITY masterpassword_password "Password">
+<!ENTITY masterpassword_confirm "Confirm password">
+
+<!ENTITY button_ok "OK">
+<!ENTITY button_cancel "Cancel">
+<!ENTITY button_yes "Yes">
+<!ENTITY button_no "No">
+<!ENTITY button_clear_data "Clear data">
+<!ENTITY button_set "Set">
+<!ENTITY button_clear "Clear">
+<!ENTITY button_copy "Copy">
+
+<!ENTITY home_top_sites_title "Top Sites">
+<!-- Localization note (home_top_sites_add): This string is used as placeholder
+ text underneath empty thumbnails in the Top Sites page on about:home. -->
+<!ENTITY home_top_sites_add "Add a site">
+
+<!-- Localization note (home_title): This string should be kept in sync
+ with the page title defined in aboutHome.dtd -->
+<!ENTITY home_title "&brandShortName; Home">
+<!ENTITY home_history_title "History">
+<!ENTITY home_synced_devices_smartfolder "Synced devices">
+<!ENTITY home_synced_devices_number "&formatD; devices">
+<!-- Localization note (home_synced_devices_one_device): This is the singular version of home_synced_devices_number, referring to the number of devices a user has synced. -->
+<!ENTITY home_synced_devices_one "1 device">
+<!ENTITY home_history_back_to2 "Back to full History">
+<!ENTITY home_clear_history_button "Clear browsing history">
+<!ENTITY home_clear_history_confirm "Are you sure you want to clear your history?">
+<!ENTITY home_bookmarks_empty "Bookmarks you save show up here.">
+<!ENTITY home_closed_tabs_title2 "Recently closed">
+<!ENTITY home_last_tabs_empty "Your recent tabs show up here.">
+<!ENTITY home_restore_all "Restore all">
+<!ENTITY home_closed_tabs_number "&formatD; tabs">
+<!-- Localization note (home_closed_tabs_one): This is the singular version of home_closed_tabs_number, referring to the number of recently closed tabs available. -->
+<!ENTITY home_closed_tabs_one "1 tab">
+<!ENTITY home_most_recent_empty "Websites you visited most recently show up here.">
+<!-- Localization note (home_most_recent_emptyhint2): "Psst" is a sound that might be used to attract someone's attention unobtrusively, and intended to hint at Private Browsing to the user.
+ The placeholders &formatS1; and &formatS2; are used to mark the location of text underlining. -->
+<!ENTITY home_most_recent_emptyhint2 "Psst: using a &formatS1;New Private Tab&formatS2; won\'t save your history.">
+
+<!-- Localization note (home_default_empty): This string is used as the default text when there
+ is no data to show in an about:home panel that was created by an add-on. -->
+<!ENTITY home_default_empty "No content could be found for this panel.">
+
+<!-- Localization note (home_back_up_to_filter): The variable is replaced by the name of the
+ previous location in the navigation, such as the previous folder -->
+<!ENTITY home_move_back_to_filter "Back to &formatS;">
+
+<!-- Localization note (home_remote_tabs_many_hidden_devices) : The
+ formatD is replaced with the number of hidden devices. The
+ number of hidden devices is always more than one. We can't use
+ Android plural forms, sadly. See Bug #753859. -->
+<!ENTITY home_remote_tabs_many_hidden_devices "&formatD; devices hidden">
+<!-- Localization note (home_remote_tabs_hidden_devices_title) : This is the
+ title of a dialog; we expect more than one device. -->
+<!ENTITY home_remote_tabs_hidden_devices_title "Hidden devices">
+<!-- Localization note (home_remote_tabs_unhide_selected_devices) : This is
+ the text of a button; we expect more than one device. -->
+<!ENTITY home_remote_tabs_unhide_selected_devices "Unhide selected devices">
+
+<!ENTITY remote_tabs_panel_moved_title "Where did my tabs go?">
+<!ENTITY remote_tabs_panel_moved_desc "We\'ve moved your tabs from other devices into a panel on your home page that can be easily accessed every time you open a new tab.">
+<!ENTITY remote_tabs_panel_moved_link "Take me to my new panel.">
+
+<!ENTITY pin_site_dialog_hint "Enter a search keyword">
+
+<!ENTITY filepicker_title "Choose File">
+<!ENTITY filepicker_audio_title "Choose or record a sound">
+<!ENTITY filepicker_image_title "Choose or take a picture">
+<!ENTITY filepicker_video_title "Choose or record a video">
+
+<!-- Site identity popup -->
+<!ENTITY identity_connected_to "You are connected to">
+<!-- Localization note (identity_run_by) : This string appears between a
+domain name (above) and an organization name (below). E.g.
+
+example.com
+which is run by
+Example Enterprises, Inc.
+
+The layout of the identity dialog prevents combining this into a single string with
+substitution variables. If it is difficult to translate the sense of the string
+with that structure, consider a translation which ignores the preceding domain and
+just addresses the organization to follow, e.g. "This site is run by " -->
+<!ENTITY identity_connection_secure "Secure Connection">
+<!ENTITY identity_connection_insecure "Insecure connection">
+<!ENTITY identity_connection_chromeui "This is a secure &brandShortName; page">
+
+<!-- Mixed content notifications in site identity popup -->
+<!ENTITY mixed_content_blocked_all1 "&brandShortName; has blocked insecure content on this page.">
+<!ENTITY mixed_content_blocked_some1 "&brandShortName; has blocked some of the insecure content on this page.">
+<!ENTITY mixed_content_display_loaded1 "Parts of this page are not secure (such as images).">
+<!ENTITY mixed_content_protection_disabled1 "You have disabled protection from insecure content.">
+
+<!-- Tracking content notifications in site identity popup -->
+<!ENTITY doorhanger_tracking_title2 "Tracking Protection">
+<!ENTITY doorhanger_tracking_state_enabled "Enabled">
+<!ENTITY doorhanger_tracking_state_disabled "Disabled">
+<!ENTITY doorhanger_tracking_message_enabled1 "Attempts to track your online behavior have been blocked.">
+<!ENTITY doorhanger_tracking_message_disabled2 "This page includes elements that may track your browsing.">
+
+<!-- Common mixed and tracking content strings in site identity popup -->
+<!ENTITY learn_more "Learn More">
+<!ENTITY enable_protection "Enable protection">
+<!ENTITY disable_protection "Disable protection">
+
+<!ENTITY private_data_success "Private data cleared">
+<!ENTITY private_data_fail "Some private data could not be cleared">
+
+<!ENTITY bookmarkhistory_button_import "Import">
+<!ENTITY bookmarkhistory_import_both "Importing bookmarks and history
+ from Android">
+<!ENTITY bookmarkhistory_import_bookmarks "Importing bookmarks
+ from Android">
+<!ENTITY bookmarkhistory_import_history "Importing history
+ from Android">
+<!ENTITY bookmarkhistory_import_wait "Please wait...">
+
+<!ENTITY suggestions_prompt3 "Would you like to turn on search suggestions?">
+<!-- Localization note (search_bar_item_desc): When the user clicks the url bar
+ and starts typing, a list of icons of search engines appears at the bottom
+ of the screen. When a user clicks an icon, the entered text will be searched
+ via the search engine that uses the icon they clicked. This text is used
+ for screen reader users when they hover each icon - &formatS; will be
+ replaced with the name of the currently highlighted icon. -->
+<!ENTITY search_bar_item_desc "Search with &formatS;">
+
+<!-- Localization note (suggestion_for_engine): The placeholder &formatS1; will be
+ replaced with the name of the search engine. The placeholder &formatS2; will be
+ replaced with the search query. -->
+<!ENTITY suggestion_for_engine "Search &formatS1; for &formatS2;">
+
+<!ENTITY searchable_description "Bookmarks and history">
+
+ <!-- Updater notifications -->
+<!ENTITY updater_start_title2 "Update available for &brandShortName;">
+<!ENTITY updater_start_select2 "Touch to download">
+
+<!ENTITY updater_downloading_title2 "Downloading &brandShortName;">
+<!ENTITY updater_downloading_title_failed2 "Download failed">
+<!ENTITY updater_downloading_select2 "Touch to apply update once downloaded">
+<!ENTITY updater_downloading_retry2 "Touch to retry">
+
+<!ENTITY updater_apply_title2 "Update available for &brandShortName;">
+<!ENTITY updater_apply_select2 "Touch to update">
+
+<!-- Localization note (updater_permission_text): This text is shown in a notification and as a snackbar
+ if the app requires a runtime permission to download updates. Currently, the updater only sees
+ remotely advertised updates in the Nightly and Aurora channels. -->
+<!ENTITY updater_permission_text "To download files and updates, allow &brandShortName; permission to access storage.">
+<!-- LOCALIZATION NOTE (updater_permission_allow): This action is shown in a snackbar along with updater_permission_text. -->
+<!ENTITY updater_permission_allow "Allow">
+
+ <!-- Guest mode -->
+<!ENTITY new_guest_session "New Guest Session">
+<!ENTITY exit_guest_session "Exit Guest Session">
+<!ENTITY guest_session_dialog_continue "Continue">
+<!ENTITY guest_session_dialog_cancel "Cancel">
+<!ENTITY new_guest_session_title "&brandShortName; will now restart">
+<!ENTITY new_guest_session_text2 "The person using it will not be able to see any of your personal browsing data (like saved logins, history or bookmarks).\n\nWhen your guest is done, their browsing data will be deleted and your session will be restored.">
+<!ENTITY guest_browsing_notification_title "Guest browsing is enabled">
+<!ENTITY guest_browsing_notification_text "Tap to exit">
+
+<!ENTITY exit_guest_session_title "&brandShortName; will now restart">
+<!ENTITY exit_guest_session_text "The browsing data from this session will be deleted.">
+
+<!-- Miscellaneous -->
+<!-- LOCALIZATION NOTE (ellipsis): This text is appended to a piece of text that does not fit in the
+ designated space. Use the unicode ellipsis char, \u2026, or use "..." if \u2026 doesn't suit
+ traditions in your locale. -->
+<!ENTITY ellipsis "…">
+
+<!ENTITY colon ":">
+
+<!-- LOCALIZATION NOTE (percent): The percent sign is appended after a number to
+ display a percentage value. formatS is the number, #37 is the code to display a percent sign.
+ This format string is typically used by getString method, in such method the percent sign
+ is a reserved caracter. In order to display one percent sign in the result of getString,
+ double percent signs must be inserted in the format string.
+ This entity is used in the zoomed view to display the zoom factor-->
+<!ENTITY percent "&formatS;&#37;&#37;">
+
+<!-- These are only used for accessibility for the done and overflow-menu buttons in the actionbar.
+ They are never shown to users -->
+<!ENTITY actionbar_menu "Menu">
+<!ENTITY actionbar_done "Done">
+
+<!-- Voice search in the awesome bar -->
+<!ENTITY voicesearch_prompt "Speak now">
+
+<!-- Localization note (remote_tabs_last_synced): the variable is replaced by a
+ "relative time span string" produced by Android. This string describes the
+ time the tabs were last synced relative to the current time; examples
+ include "42 minutes ago", "4 days ago", "last week", etc. The subject of
+ "Last synced" is one of the user's other Sync clients, typically Firefox on
+ their desktop or laptop.-->
+<!ENTITY remote_tabs_last_synced "Last synced: &formatS;">
+<!-- Localization note: Used when the sync has not happend yet, showed in place of a date -->
+<!ENTITY remote_tabs_never_synced "Last synced: never">
+
+<!-- LOCALIZATION NOTE (intent_uri_private_browsing_prompt): This string will
+ appear in an alert when a user, who is currently in private browsing,
+ clicks a link that will open an external Android application. "&formatS;"
+ will be replaced with the name of the application that will be opened. -->
+<!ENTITY intent_uri_private_browsing_prompt "This link will open in &formatS;. Are you sure you want to exit Private Browsing?">
+<!-- LOCALIZATION NOTE (intent_uri_private_browsing_multiple_match_title): This
+ string will appear as the title of an alert when a user, who is currently
+ in private browsing, clicks a link that will open an external Android
+ application and more than one application is available to open that link.
+ We don't have control over the style of this dialog and it looks
+ unpolished when this string is longer than one line so ideally keep it
+ short! -->
+<!ENTITY intent_uri_private_browsing_multiple_match_title "Exit Private Browsing?">
+
+<!-- DevTools Authentication -->
+<!-- LOCALIZATION NOTE (devtools_auth_scan_header): This header text appears
+ above a QR reader that is actively scanning for QR codes. The expected QR
+ code has already been displayed by the client trying to connect (such as
+ desktop Firefox via WebIDE), so you just need to aim this device at the QR
+ code. -->
+<!ENTITY devtools_auth_scan_header "Scanning for the QR code displayed on your other device">
+
+<!-- Restrictable features -->
+<!-- Localization note: These are features the device owner (e.g. parent) can enable or disable for
+ a restricted profile (e.g. child). Used inside the Android settings UI. -->
+<!ENTITY restrictable_feature_addons_installation "Add-ons">
+<!ENTITY restrictable_feature_addons_installation_description "Add features or functionality to Firefox. Note: Add-ons can disable certain restrictions.">
+<!ENTITY restrictable_feature_private_browsing "Private Browsing">
+<!ENTITY restrictable_feature_private_browsing_description "Allows family members to browse without saving information about the sites and pages they\'ve visited.">
+<!ENTITY restrictable_feature_clear_history "Clear History">
+<!ENTITY restrictable_feature_clear_history_description "Allows family members to delete information about the sites and pages they\'ve visited.">
+<!ENTITY restrictable_feature_advanced_settings "Advanced Settings">
+<!ENTITY restrictable_feature_advanced_settings_description "This includes importing bookmarks, restoring tabs and automated updates. Turn off for simplified settings suitable for any family member.">
+<!ENTITY restrictable_feature_camera_microphone "Camera &amp; Microphone">
+<!ENTITY restrictable_feature_camera_microphone_description "Allows family members to engage in real time communication on websites.">
+<!ENTITY restrictable_feature_block_list "Block List">
+<!ENTITY restrictable_feature_block_list_description "Block websites that include sensitive content.">
+
+<!-- Default Bookmarks titles-->
+<!-- LOCALIZATION NOTE (bookmarks_about_browser): link title for about:fennec -->
+<!ENTITY bookmarks_about_browser "Firefox: About your browser">
+<!-- LOCALIZATION NOTE (bookmarks_addons): link title for https://addons.mozilla.org/en-US/mobile -->
+<!ENTITY bookmarks_addons "Firefox: Customize with add-ons">
+<!-- LOCALIZATION NOTE (bookmarks_support): link title for https://support.mozilla.org/ -->
+<!ENTITY bookmarks_support "Firefox: Support">
+<!-- LOCALIZATION NOTE (bookmarks_restricted_support): link title for https://support.mozilla.org/kb/controlledaccess -->
+<!ENTITY bookmarks_restricted_support2 "Firefox Help and Support for restricted profiles on Android tablets">
+<!-- LOCALIZATION NOTE (bookmarks_restricted_webmaker):link title for https://webmaker.org -->
+<!ENTITY bookmarks_restricted_webmaker "Learn the Web: Mozilla Webmaker">
+
+<!-- LOCALIZATION NOTE (unsupported_sdk_version): The user installed a build of this app that does not support
+ the Android version of this device. the formatS1 is replaced by the CPU ABI (e.g., ARMv7); the formatS2 is
+ replaced by the Android OS version (e.g., 14)-->
+<!ENTITY unsupported_sdk_version "Sorry! This &brandShortName; won\'t work on this device (&formatS1;, &formatS2;). Please download the correct version.">
+
+<!ENTITY eol_notification_title2 "&brandShortName; will no longer update">
+<!ENTITY eol_notification_summary "Tap to learn more">
+
+<!-- LOCALIZATION NOTE (whatsnew_notification_title, whatsnew_notification_summary): These strings
+ are used for a system notification that's shown to users after the app updates. -->
+<!ENTITY whatsnew_notification_title "&brandShortName; is up to date">
+<!ENTITY whatsnew_notification_summary "Find out what\'s new in this version">
+
+<!ENTITY promotion_add_to_homescreen "Add to home screen">
+
+<!ENTITY helper_first_offline_bookmark_title "Read offline">
+<!ENTITY helper_first_offline_bookmark_message "Find your Reader View items in Bookmarks, even offline.">
+<!ENTITY helper_first_offline_bookmark_button "Go to Bookmarks">
+
+<!ENTITY helper_triple_readerview_open_title "Available offline">
+<!ENTITY helper_triple_readerview_open_message "Bookmark Reader View items to read them offline.">
+<!ENTITY helper_triple_readerview_open_button "Add to Bookmarks">
+
+<!ENTITY activity_stream_topsites "Top Sites">
+<!ENTITY activity_stream_highlights "Highlights">
+
+<!-- LOCALIZATION NOTE (activity_stream_highlight_label_bookmarked): This label is shown in the Activity
+Stream list for highlights sourced from th user's bookmarks. -->
+<!ENTITY activity_stream_highlight_label_bookmarked "Bookmarked">
+<!-- LOCALIZATION NOTE (activity_stream_highlight_label_visited): This label is shown in the Activity
+Stream list for highlights sourced from th user's bookmarks. -->
+<!ENTITY activity_stream_highlight_label_visited "Visited">
+
+<!-- LOCALIZATION NOTE (activity_stream_dismiss): This label is shown in the Activity Stream context menu,
+and allows hiding a URL/page from highlights or topsites. The page remains in history/bookmarks, but
+is simply hidden from the Activity Stream panel. -->
+<!ENTITY activity_stream_dismiss "Dismiss">
+<!ENTITY activity_stream_delete_history "Delete from History">
diff --git a/mobile/android/base/locales/en-US/search_strings.dtd b/mobile/android/base/locales/en-US/search_strings.dtd
new file mode 100644
index 0000000000..fe8180cff7
--- /dev/null
+++ b/mobile/android/base/locales/en-US/search_strings.dtd
@@ -0,0 +1,28 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY search_app_name '&brandShortName; Search'>
+
+<!-- Localization note (search_bar_hint): The &formatS; will be replaced with the name of
+ the currently selected search engine. -->
+<!ENTITY search_bar_hint 'Search with &formatS;'>
+
+<!ENTITY search_empty_title2 'Start searching'>
+<!ENTITY search_empty_message 'Quickly search for anything you want'>
+
+<!-- Localization note (search_plus_content_description): This is the content description
+ for the "+" icon that appears at the end of search suggestions. -->
+<!ENTITY search_plus_content_description 'Add to search bar'>
+
+<!ENTITY search_pref_title 'Settings'>
+<!ENTITY search_pref_button_content_description 'Settings'>
+
+<!ENTITY pref_clearHistory_confirmation 'History cleared'>
+<!ENTITY pref_clearHistory_dialogMessage 'Delete all search history from this device?'>
+<!ENTITY pref_clearHistory_title 'Clear search history'>
+
+<!ENTITY search_widget_button_label 'Search'>
+
+<!ENTITY network_error_title 'No internet connection'>
+<!ENTITY network_error_message 'Tap here to check your network settings'>
diff --git a/mobile/android/base/locales/en-US/sync_strings.dtd b/mobile/android/base/locales/en-US/sync_strings.dtd
new file mode 100644
index 0000000000..117600cac8
--- /dev/null
+++ b/mobile/android/base/locales/en-US/sync_strings.dtd
@@ -0,0 +1,126 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- Don't localize these. They're here until they have
+ a better place to live. -->
+<!ENTITY syncBrand.fullName.label "Firefox Sync">
+<!ENTITY syncBrand.shortName.label "Sync">
+
+<!-- Main titles. -->
+<!ENTITY sync.title.connect.label 'Connect to &syncBrand.shortName.label;'>
+
+<!-- J-PAKE Key Screen -->
+<!ENTITY sync.subtitle.connect.label 'To activate your new device, select “Set up &syncBrand.shortName.label;†on the device.'>
+<!ENTITY sync.subtitle.pair.label 'To activate, select “Pair a device†on your other device.'>
+<!ENTITY sync.pin.default.label '...\n...\n...\n'>
+<!ENTITY sync.link.nodevice.label 'I don\&apos;t have the device with me…'>
+
+<!-- Configure Engines -->
+<!ENTITY sync.configure.engines.title.passwords2 'Logins'>
+<!ENTITY sync.configure.engines.title.history 'History'>
+<!ENTITY sync.configure.engines.title.tabs 'Tabs'>
+
+<!-- Localization note (sync.default.client.name): Default string of the "Device
+ name" menu item upon setting up Firefox Sync. The placeholder &formatS1
+ will be replaced by the name of the Firefox release channel and &formatS2
+ by the model name of the Android device. Examples look like "Aurora on
+ GT-I1950" and "Fennec on MI 2S". -->
+<!ENTITY sync.default.client.name '&formatS1; on &formatS2;'>
+
+<!-- Bookmark folder strings -->
+<!ENTITY bookmarks.folder.menu.label 'Bookmarks Menu'>
+<!ENTITY bookmarks.folder.places.label ''>
+<!ENTITY bookmarks.folder.tags.label 'Tags'>
+<!ENTITY bookmarks.folder.toolbar.label 'Bookmarks Toolbar'>
+<!ENTITY bookmarks.folder.other.label 'Other Bookmarks'>
+<!ENTITY bookmarks.folder.desktop.label 'Desktop Bookmarks'>
+<!ENTITY bookmarks.folder.mobile.label 'Mobile Bookmarks'>
+<!-- Pinned sites on about:home. This folder should never be shown to the user, but we have to give it a string name -->
+<!ENTITY bookmarks.folder.pinned.label 'Pinned'>
+
+<!-- Firefox Account strings. -->
+
+<!-- Localization note: these are shown in screens after the user has
+ created or signed in to an account, and take the user back to
+ Firefox. -->
+<!ENTITY fxaccount_back_to_browsing 'Back to browsing'>
+
+<!ENTITY fxaccount_getting_started_welcome_to_sync 'Welcome to &syncBrand.shortName.label;'>
+<!ENTITY fxaccount_getting_started_description2 'Sign in to sync your tabs, bookmarks, logins &amp; more.'>
+<!ENTITY fxaccount_getting_started_get_started 'Get started'>
+<!ENTITY fxaccount_getting_started_old_firefox 'Using an older version of &syncBrand.shortName.label;?'>
+
+<!ENTITY fxaccount_status_signed_in_as 'Signed in as'>
+<!ENTITY fxaccount_status_manage_account 'Manage account'>
+<!ENTITY fxaccount_status_auth_server 'Account server'>
+<!ENTITY fxaccount_status_sync_now 'Sync now'>
+<!ENTITY fxaccount_status_syncing2 'Syncing…'>
+<!ENTITY fxaccount_status_device_name 'Device name'>
+<!ENTITY fxaccount_status_sync_server 'Sync server'>
+<!ENTITY fxaccount_status_sync '&syncBrand.shortName.label;'>
+<!ENTITY fxaccount_status_sync_enabled '&syncBrand.shortName.label;: enabled'>
+<!ENTITY fxaccount_status_needs_verification2 'Your account needs to be verified. Tap to resend verification email.'>
+<!ENTITY fxaccount_status_needs_credentials 'Cannot connect. Tap to sign in.'>
+<!ENTITY fxaccount_status_needs_upgrade 'You need to upgrade &brandShortName; to sign in.'>
+<!ENTITY fxaccount_status_needs_master_sync_automatically_enabled '&syncBrand.shortName.label; is set up, but not syncing automatically. Toggle “Auto-sync data†in Android Settings &gt; Data Usage.'>
+<!ENTITY fxaccount_status_needs_master_sync_automatically_enabled_v21 '&syncBrand.shortName.label; is set up, but not syncing automatically. Toggle “Auto-sync data†in the menu of Android Settings &gt; Accounts.'>
+<!ENTITY fxaccount_status_needs_finish_migrating 'Tap to sign in to your new Firefox Account.'>
+<!ENTITY fxaccount_status_bookmarks 'Bookmarks'>
+<!ENTITY fxaccount_status_history 'History'>
+<!ENTITY fxaccount_status_passwords2 'Logins'>
+<!ENTITY fxaccount_status_tabs 'Open tabs'>
+<!ENTITY fxaccount_status_legal 'Legal' >
+<!-- Localization note: when tapped, the following two strings link to
+ external web pages. Compare fxaccount_policy_{linktos,linkprivacy}:
+ these strings are separated to accommodate languages that decline
+ the two uses differently. -->
+<!ENTITY fxaccount_status_linktos2 'Terms of service'>
+<!ENTITY fxaccount_status_linkprivacy2 'Privacy notice'>
+<!ENTITY fxaccount_status_more 'More&ellipsis;'>
+<!ENTITY fxaccount_remove_account 'Disconnect&ellipsis;'>
+
+<!ENTITY fxaccount_remove_account_dialog_title 'Remove Firefox Account?'>
+<!ENTITY fxaccount_remove_account_dialog_message '&brandShortName; will stop syncing with your account, but won’t delete any of your browsing data on this device.'>
+<!-- Localization note: format string below will be replaced
+ with the Firefox Account's email address. -->
+<!ENTITY fxaccount_remove_account_toast 'Firefox Account &formatS; removed.'>
+
+<!ENTITY fxaccount_enable_debug_mode 'Enable Debug Mode'>
+
+<!-- Localization note: this is the name shown by the Android system
+ itself for a Firefox Account. Don't localize this. -->
+<!ENTITY fxaccount_account_type_label 'Firefox'>
+
+<!-- Localization note: these are shown by the Android system itself,
+ when the user navigates to the Android > Accounts > {Firefox
+ Account} Screen. The link takes the user to the Firefox Account
+ status activity, which lets them manage their Firefox
+ Account. -->
+<!ENTITY fxaccount_options_title '&syncBrand.shortName.label; Options'>
+<!ENTITY fxaccount_options_configure_title 'Configure &syncBrand.shortName.label;'>
+
+<!-- Localization note: these error messages are shown after a request
+ has been made to the remote server, and an error of some type has
+ been returned. -->
+<!ENTITY fxaccount_remote_error_UPGRADE_REQUIRED 'You need to upgrade Firefox'>
+
+<!-- Localization note: the format string will be fxaccount_sign_in_button_label, linkified. -->
+<!ENTITY fxaccount_remote_error_ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS_2 'Account already exists. &formatS1;'>
+<!ENTITY fxaccount_remote_error_ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST 'Invalid email or password'>
+<!ENTITY fxaccount_remote_error_INCORRECT_PASSWORD 'Invalid email or password'>
+<!ENTITY fxaccount_remote_error_ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT 'Account is not verified'>
+<!ENTITY fxaccount_remote_error_CLIENT_HAS_SENT_TOO_MANY_REQUESTS 'Server busy, try again soon'>
+<!ENTITY fxaccount_remote_error_SERVICE_TEMPORARILY_UNAVAILABLE_TO_DUE_HIGH_LOAD 'Server busy, try again soon'>
+<!ENTITY fxaccount_remote_error_UNKNOWN_ERROR 'There was a problem'>
+<!ENTITY fxaccount_remote_error_ACCOUNT_LOCKED 'Account is locked. &formatS1;'>
+
+<!ENTITY fxaccount_sync_sign_in_error_notification_title2 '&syncBrand.shortName.label; is not connected'>
+<!-- Localization note: the format string below will be replaced
+ with the Firefox Account's email address. -->
+<!ENTITY fxaccount_sync_sign_in_error_notification_text2 'Tap to sign in as &formatS;'>
+
+<!ENTITY fxaccount_sync_finish_migrating_notification_title 'Finish upgrading &syncBrand.shortName.label;?'>
+<!-- Localization note: the format string below will be replaced
+ with the Firefox Account's email address. -->
+<!ENTITY fxaccount_sync_finish_migrating_notification_text 'Tap to sign in as &formatS;'>
diff --git a/mobile/android/base/locales/moz.build b/mobile/android/base/locales/moz.build
new file mode 100644
index 0000000000..079d4d6409
--- /dev/null
+++ b/mobile/android/base/locales/moz.build
@@ -0,0 +1,8 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+if CONFIG['MOZ_ANDROID_SEARCH_ACTIVITY']:
+ DEFINES['MOZ_ANDROID_SEARCH_ACTIVITY'] = 1
diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build
new file mode 100644
index 0000000000..6c88464ab5
--- /dev/null
+++ b/mobile/android/base/moz.build
@@ -0,0 +1,1147 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += ['locales']
+
+CONFIGURE_SUBST_FILES += ['adjust_sdk_app_token']
+
+include('android-services.mozbuild')
+
+geckoview_source_dir = TOPSRCDIR + '/mobile/android/geckoview/src/main/'
+geckoview_thirdparty_source_dir = TOPSRCDIR + '/mobile/android/geckoview/src/thirdparty/'
+thirdparty_source_dir = TOPSRCDIR + '/mobile/android/thirdparty/'
+
+constants_jar = add_java_jar('constants')
+constants_jar.sources += [geckoview_source_dir + 'java/org/mozilla/gecko/' + x for x in [
+ 'annotation/JNITarget.java',
+ 'annotation/ReflectionTarget.java',
+ 'annotation/RobocopTarget.java',
+ 'annotation/WebRTCJNITarget.java',
+ 'annotation/WrapForJNI.java',
+ 'SysInfo.java',
+]]
+constants_jar.sources += ['java/org/mozilla/gecko/' + x for x in [
+ 'adjust/AdjustHelperInterface.java',
+ 'adjust/AttributionHelperListener.java',
+ 'db/BrowserContract.java',
+ 'LocaleManager.java',
+ 'Locales.java',
+]]
+constants_jar.generated_sources = [
+ 'preprocessed/org/mozilla/gecko/AdjustConstants.java',
+ 'preprocessed/org/mozilla/gecko/AppConstants.java',
+]
+constants_jar.extra_jars = [
+ CONFIG['ANDROID_SUPPORT_ANNOTATIONS_JAR_LIB'],
+ CONFIG['ANDROID_SUPPORT_V4_AAR_LIB'],
+ CONFIG['ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB'],
+ CONFIG['ANDROID_APPCOMPAT_V7_AAR_LIB'],
+]
+
+if CONFIG['MOZ_INSTALL_TRACKING']:
+ constants_jar.sources += ['java/org/mozilla/gecko/' + x for x in [
+ 'adjust/AdjustHelper.java',
+ ]]
+ constants_jar.extra_jars += [
+ 'gecko-thirdparty-adjust_sdk.jar',
+ ]
+else:
+ constants_jar.sources += ['java/org/mozilla/gecko/' + x for x in [
+ 'adjust/StubAdjustHelper.java',
+ ]]
+
+resjar = add_java_jar('gecko-R')
+resjar.sources = []
+resjar.generated_sources += [
+ 'org/mozilla/gecko/R.java',
+]
+
+if CONFIG['ANDROID_SUPPORT_V4_AAR']:
+ ANDROID_EXTRA_PACKAGES += ['android.support.v4']
+ ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_SUPPORT_V4_AAR_RES']]
+# (no resources) resjar.generated_sources += ['android/support/v4/R.java']
+if CONFIG['ANDROID_APPCOMPAT_V7_AAR']:
+ ANDROID_EXTRA_PACKAGES += ['android.support.v7.appcompat']
+ ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_APPCOMPAT_V7_AAR_RES']]
+ resjar.generated_sources += ['android/support/v7/appcompat/R.java']
+if CONFIG['ANDROID_SUPPORT_VECTOR_DRAWABLE_AAR']:
+ ANDROID_EXTRA_PACKAGES += ['android.support.graphics.drawable']
+ ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_SUPPORT_VECTOR_DRAWABLE_AAR_RES']]
+# (no reosurces) resjar.generated_sources += ['android/support/graphics/drawable/R.java']
+if CONFIG['ANDROID_ANIMATED_VECTOR_DRAWABLE_AAR']:
+ ANDROID_EXTRA_PACKAGES += ['android.support.graphics.drawable.animated']
+ ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_ANIMATED_VECTOR_DRAWABLE_AAR_RES']]
+# (no resources) resjar.generated_sources += ['android/support/graphics/drawable/animated/R.java']
+if CONFIG['ANDROID_CARDVIEW_V7_AAR']:
+ ANDROID_EXTRA_PACKAGES += ['android.support.v7.cardview']
+ ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_CARDVIEW_V7_AAR_RES']]
+ resjar.generated_sources += ['android/support/v7/cardview/R.java']
+if CONFIG['ANDROID_DESIGN_AAR']:
+ ANDROID_EXTRA_PACKAGES += ['android.support.design']
+ ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_DESIGN_AAR_RES']]
+ resjar.generated_sources += ['android/support/design/R.java']
+if CONFIG['ANDROID_RECYCLERVIEW_V7_AAR']:
+ ANDROID_EXTRA_PACKAGES += ['android.support.v7.recyclerview']
+ ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_RECYCLERVIEW_V7_AAR_RES']]
+ resjar.generated_sources += ['android/support/v7/recyclerview/R.java']
+if CONFIG['ANDROID_CUSTOMTABS_AAR']:
+ ANDROID_EXTRA_PACKAGES += ['android.support.customtabs']
+ ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_CUSTOMTABS_AAR_RES']]
+# (no resources) resjar.generated_sources += ['android/support/customtabs/R.java']
+if CONFIG['ANDROID_PALETTE_V7_AAR']:
+ ANDROID_EXTRA_PACKAGES += ['android.support.v7.palette']
+ ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PALETTE_V7_AAR_RES']]
+# (no resources) resjar.generated_sources += ['android/support/v7/palette/R.java']
+
+resjar.javac_flags += ['-Xlint:all']
+
+mgjar = add_java_jar('gecko-mozglue')
+mgjar.sources += [geckoview_source_dir + 'java/org/mozilla/gecko/' + x for x in [
+ 'mozglue/ByteBufferInputStream.java',
+ 'mozglue/DirectBufferAllocator.java',
+ 'mozglue/GeckoLoader.java',
+ 'mozglue/JNIObject.java',
+ 'mozglue/NativeReference.java',
+ 'mozglue/NativeZip.java',
+ 'mozglue/SafeIntent.java',
+]]
+mgjar.generated_sources = [] # Keep it this way.
+mgjar.extra_jars += [
+ CONFIG['ANDROID_SUPPORT_ANNOTATIONS_JAR_LIB'],
+ 'constants.jar',
+]
+mgjar.javac_flags += ['-Xlint:all']
+
+gujar = add_java_jar('gecko-util')
+gujar.sources += [geckoview_source_dir + 'java/org/mozilla/gecko/' + x for x in [
+ 'util/ActivityResultHandler.java',
+ 'util/ActivityResultHandlerMap.java',
+ 'util/ActivityUtils.java',
+ 'util/BundleEventListener.java',
+ 'util/Clipboard.java',
+ 'util/ContextUtils.java',
+ 'util/DateUtil.java',
+ 'util/EventCallback.java',
+ 'util/FileUtils.java',
+ 'util/FloatUtils.java',
+ 'util/GamepadUtils.java',
+ 'util/GeckoBackgroundThread.java',
+ 'util/GeckoEventListener.java',
+ 'util/GeckoJarReader.java',
+ 'util/GeckoRequest.java',
+ 'util/HardwareCodecCapabilityUtils.java',
+ 'util/HardwareUtils.java',
+ 'util/INIParser.java',
+ 'util/INISection.java',
+ 'util/InputOptionsUtils.java',
+ 'util/IntentUtils.java',
+ 'util/IOUtils.java',
+ 'util/JSONUtils.java',
+ 'util/MenuUtils.java',
+ 'util/NativeEventListener.java',
+ 'util/NativeJSContainer.java',
+ 'util/NativeJSObject.java',
+ 'util/NetworkUtils.java',
+ 'util/NonEvictingLruCache.java',
+ 'util/PrefUtils.java',
+ 'util/ProxySelector.java',
+ 'util/publicsuffix/PublicSuffix.java',
+ 'util/publicsuffix/PublicSuffixPatterns.java',
+ 'util/RawResource.java',
+ 'util/StringUtils.java',
+ 'util/ThreadUtils.java',
+ 'util/UIAsyncTask.java',
+ 'util/UUIDUtil.java',
+ 'util/WeakReferenceHandler.java',
+ 'util/WindowUtils.java',
+]]
+gujar.extra_jars = [
+ CONFIG['ANDROID_SUPPORT_ANNOTATIONS_JAR_LIB'],
+ CONFIG['ANDROID_SUPPORT_V4_AAR_LIB'],
+ CONFIG['ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB'],
+ 'constants.jar',
+ 'gecko-mozglue.jar',
+]
+gujar.javac_flags += ['-Xlint:all,-deprecation']
+
+stjar = add_java_jar('sync-thirdparty')
+stjar.sources += [ thirdparty_source_dir + f for f in sync_thirdparty_java_files ]
+stjar.javac_flags = ['-Xlint:none']
+
+services_jar = add_java_jar('services')
+services_jar.sources += sync_java_files
+services_jar.extra_jars = [
+ CONFIG['ANDROID_SUPPORT_ANNOTATIONS_JAR_LIB'],
+ CONFIG['ANDROID_SUPPORT_V4_AAR_LIB'],
+ CONFIG['ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB'],
+ CONFIG['ANDROID_APPCOMPAT_V7_AAR_LIB'],
+ 'constants.jar',
+ 'gecko-R.jar',
+ 'gecko-mozglue.jar',
+ 'gecko-thirdparty.jar',
+ 'gecko-util.jar',
+ 'sync-thirdparty.jar',
+]
+services_jar.javac_flags += ['-Xlint:all,-deprecation']
+
+if CONFIG['MOZ_WEBRTC']:
+ video_root = TOPSRCDIR + '/media/webrtc/trunk/webrtc/modules/video_capture/android/java/src/org/webrtc/videoengine/'
+ video_render_root = TOPSRCDIR + '/media/webrtc/trunk/webrtc/modules/video_render/android/java/src/org/webrtc/videoengine/'
+ audio_root = TOPSRCDIR + '/media/webrtc/trunk/webrtc/modules/audio_device/android/java/src/org/webrtc/voiceengine/'
+ wrjar = add_java_jar('webrtc')
+ wrjar.sources += [
+ video_root + 'CaptureCapabilityAndroid.java',
+ video_root + 'VideoCaptureAndroid.java',
+ video_root + 'VideoCaptureDeviceInfoAndroid.java',
+ video_render_root + 'ViEAndroidGLES20.java',
+ video_render_root + 'ViERenderer.java',
+ ]
+ wrjar.sources += [
+ audio_root + 'AudioManagerAndroid.java',
+ audio_root + 'WebRtcAudioManager.java',
+ audio_root + 'WebRtcAudioRecord.java',
+ audio_root + 'WebRtcAudioTrack.java',
+ audio_root + 'WebRtcAudioUtils.java',
+ ]
+ wrjar.extra_jars = [
+ 'constants.jar',
+ 'gecko-R.jar',
+ 'gecko-browser.jar',
+ 'gecko-mozglue.jar',
+ 'gecko-util.jar',
+ 'gecko-view.jar',
+ ]
+ wrjar.javac_flags += ['-Xlint:all,-deprecation,-cast']
+
+gvjar = add_java_jar('gecko-view')
+
+gvjar.sources += [geckoview_source_dir + 'java/org/mozilla/gecko/' + x
+ for x in [
+ 'AlarmReceiver.java',
+ 'AndroidGamepadManager.java',
+ 'BaseGeckoInterface.java',
+ 'ContextGetter.java',
+ 'CrashHandler.java',
+ 'EventDispatcher.java',
+ 'GeckoAccessibility.java',
+ 'GeckoAppShell.java',
+ 'GeckoBatteryManager.java',
+ 'GeckoEditable.java',
+ 'GeckoEditableClient.java',
+ 'GeckoEditableListener.java',
+ 'GeckoHalDefines.java',
+ 'GeckoInputConnection.java',
+ 'GeckoNetworkManager.java',
+ 'GeckoProfile.java',
+ 'GeckoProfileDirectories.java',
+ 'GeckoScreenOrientation.java',
+ 'GeckoSharedPrefs.java',
+ 'GeckoThread.java',
+ 'GeckoView.java',
+ 'GeckoViewChrome.java',
+ 'GeckoViewContent.java',
+ 'GeckoViewFragment.java',
+ 'gfx/BitmapUtils.java',
+ 'gfx/BufferedImage.java',
+ 'gfx/BufferedImageGLInfo.java',
+ 'gfx/DynamicToolbarAnimator.java',
+ 'gfx/FloatSize.java',
+ 'gfx/FullScreenState.java',
+ 'gfx/GeckoLayerClient.java',
+ 'gfx/ImmutableViewportMetrics.java',
+ 'gfx/IntSize.java',
+ 'gfx/LayerRenderer.java',
+ 'gfx/LayerView.java',
+ 'gfx/NativePanZoomController.java',
+ 'gfx/Overscroll.java',
+ 'gfx/OverscrollEdgeEffect.java',
+ 'gfx/PanningPerfAPI.java',
+ 'gfx/PanZoomController.java',
+ 'gfx/PanZoomTarget.java',
+ 'gfx/PointUtils.java',
+ 'gfx/ProgressiveUpdateData.java',
+ 'gfx/RectUtils.java',
+ 'gfx/RenderTask.java',
+ 'gfx/StackScroller.java',
+ 'gfx/SurfaceTextureListener.java',
+ 'gfx/ViewTransform.java',
+ 'InputConnectionListener.java',
+ 'InputMethods.java',
+ 'NotificationListener.java',
+ 'NSSBridge.java',
+ 'permissions/PermissionBlock.java',
+ 'permissions/Permissions.java',
+ 'permissions/PermissionsHelper.java',
+ 'PrefsHelper.java',
+ 'sqlite/ByteBufferInputStream.java',
+ 'sqlite/MatrixBlobCursor.java',
+ 'sqlite/SQLiteBridge.java',
+ 'sqlite/SQLiteBridgeException.java',
+ 'TouchEventInterceptor.java',
+]]
+
+gvjar.sources += [geckoview_thirdparty_source_dir + f for f in [
+ 'java/com/googlecode/eyesfree/braille/selfbraille/ISelfBrailleService.java',
+ 'java/com/googlecode/eyesfree/braille/selfbraille/SelfBrailleClient.java',
+ 'java/com/googlecode/eyesfree/braille/selfbraille/WriteData.java',
+]]
+
+gvjar.extra_jars += [
+ CONFIG['ANDROID_SUPPORT_ANNOTATIONS_JAR_LIB'],
+ CONFIG['ANDROID_SUPPORT_V4_AAR_LIB'],
+ CONFIG['ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB'],
+ 'constants.jar',
+ 'gecko-mozglue.jar',
+ 'gecko-util.jar',
+]
+
+gvjar.javac_flags += [
+ '-Xlint:all,-deprecation,-fallthrough',
+ '-J-Xmx512m',
+ '-J-Xms128m'
+]
+
+
+gbjar = add_java_jar('gecko-browser')
+
+gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
+ 'AboutPages.java',
+ 'AccountsHelper.java',
+ 'ActionBarTextSelection.java',
+ 'ActionModeCompat.java',
+ 'ActionModeCompatView.java',
+ 'ActivityHandlerHelper.java',
+ 'activitystream/ActivityStream.java',
+ 'adjust/AdjustBrowserAppDelegate.java',
+ 'animation/AnimationUtils.java',
+ 'animation/HeightChangeAnimation.java',
+ 'animation/PropertyAnimator.java',
+ 'animation/Rotate3DAnimation.java',
+ 'animation/ViewHelper.java',
+ 'ANRReporter.java',
+ 'BootReceiver.java',
+ 'BrowserApp.java',
+ 'BrowserLocaleManager.java',
+ 'cleanup/FileCleanupController.java',
+ 'cleanup/FileCleanupService.java',
+ 'CustomEditText.java',
+ 'customtabs/CustomTabsActivity.java',
+ 'customtabs/GeckoCustomTabsService.java',
+ 'DataReportingNotification.java',
+ 'db/AbstractPerProfileDatabaseProvider.java',
+ 'db/AbstractTransactionalProvider.java',
+ 'db/BaseTable.java',
+ 'db/BrowserDatabaseHelper.java',
+ 'db/BrowserDB.java',
+ 'db/BrowserProvider.java',
+ 'db/DBUtils.java',
+ 'db/FormHistoryProvider.java',
+ 'db/HomeProvider.java',
+ 'db/LocalBrowserDB.java',
+ 'db/LocalSearches.java',
+ 'db/LocalTabsAccessor.java',
+ 'db/LocalUrlAnnotations.java',
+ 'db/LocalURLMetadata.java',
+ 'db/LoginsProvider.java',
+ 'db/PasswordsProvider.java',
+ 'db/PerProfileDatabaseProvider.java',
+ 'db/PerProfileDatabases.java',
+ 'db/RemoteClient.java',
+ 'db/RemoteTab.java',
+ 'db/Searches.java',
+ 'db/SearchHistoryProvider.java',
+ 'db/SharedBrowserDatabaseProvider.java',
+ 'db/SQLiteBridgeContentProvider.java',
+ 'db/SuggestedSites.java',
+ 'db/Table.java',
+ 'db/TabsAccessor.java',
+ 'db/TabsProvider.java',
+ 'db/UrlAnnotations.java',
+ 'db/URLMetadata.java',
+ 'db/URLMetadataTable.java',
+ 'delegates/BookmarkStateChangeDelegate.java',
+ 'delegates/BrowserAppDelegate.java',
+ 'delegates/BrowserAppDelegateWithReference.java',
+ 'delegates/OfflineTabStatusDelegate.java',
+ 'delegates/ScreenshotDelegate.java',
+ 'delegates/TabsTrayVisibilityAwareDelegate.java',
+ 'DevToolsAuthHelper.java',
+ 'distribution/Distribution.java',
+ 'distribution/DistributionStoreCallback.java',
+ 'distribution/PartnerBookmarksProviderProxy.java',
+ 'distribution/PartnerBrowserCustomizationsClient.java',
+ 'distribution/ReferrerDescriptor.java',
+ 'distribution/ReferrerReceiver.java',
+ 'dlc/BaseAction.java',
+ 'dlc/catalog/DownloadContent.java',
+ 'dlc/catalog/DownloadContentBootstrap.java',
+ 'dlc/catalog/DownloadContentBuilder.java',
+ 'dlc/catalog/DownloadContentCatalog.java',
+ 'dlc/DownloadAction.java',
+ 'dlc/DownloadContentService.java',
+ 'dlc/StudyAction.java',
+ 'dlc/SyncAction.java',
+ 'dlc/VerifyAction.java',
+ 'DoorHangerPopup.java',
+ 'DownloadsIntegration.java',
+ 'DynamicToolbar.java',
+ 'EditBookmarkDialog.java',
+ 'Experiments.java',
+ 'feeds/action/CheckForUpdatesAction.java',
+ 'feeds/action/EnrollSubscriptionsAction.java',
+ 'feeds/action/FeedAction.java',
+ 'feeds/action/SetupAlarmsAction.java',
+ 'feeds/action/SubscribeToFeedAction.java',
+ 'feeds/action/WithdrawSubscriptionsAction.java',
+ 'feeds/ContentNotificationsDelegate.java',
+ 'feeds/FeedAlarmReceiver.java',
+ 'feeds/FeedFetcher.java',
+ 'feeds/FeedService.java',
+ 'feeds/knownsites/KnownSite.java',
+ 'feeds/knownsites/KnownSiteBlogger.java',
+ 'feeds/knownsites/KnownSiteMedium.java',
+ 'feeds/knownsites/KnownSiteTumblr.java',
+ 'feeds/knownsites/KnownSiteWordpress.java',
+ 'feeds/parser/Feed.java',
+ 'feeds/parser/Item.java',
+ 'feeds/parser/SimpleFeedParser.java',
+ 'feeds/subscriptions/FeedSubscription.java',
+ 'FilePicker.java',
+ 'FilePickerResultHandler.java',
+ 'FindInPageBar.java',
+ 'firstrun/DataPanel.java',
+ 'firstrun/FirstrunAnimationContainer.java',
+ 'firstrun/FirstrunPager.java',
+ 'firstrun/FirstrunPagerConfig.java',
+ 'firstrun/FirstrunPanel.java',
+ 'firstrun/RestrictedWelcomePanel.java',
+ 'firstrun/SyncPanel.java',
+ 'firstrun/TabQueuePanel.java',
+ 'FormAssistPopup.java',
+ 'GeckoActivity.java',
+ 'GeckoActivityStatus.java',
+ 'GeckoApp.java',
+ 'GeckoApplication.java',
+ 'GeckoJavaSampler.java',
+ 'GeckoMessageReceiver.java',
+ 'GeckoProfilesProvider.java',
+ 'GeckoService.java',
+ 'GeckoUpdateReceiver.java',
+ 'GlobalHistory.java',
+ 'GlobalPageMetadata.java',
+ 'GuestSession.java',
+ 'health/HealthRecorder.java',
+ 'health/SessionInformation.java',
+ 'health/StubbedHealthRecorder.java',
+ 'home/activitystream/ActivityStream.java',
+ 'home/activitystream/ActivityStreamHomeFragment.java',
+ 'home/activitystream/ActivityStreamHomeScreen.java',
+ 'home/activitystream/menu/ActivityStreamContextMenu.java',
+ 'home/activitystream/menu/BottomSheetContextMenu.java',
+ 'home/activitystream/menu/PopupContextMenu.java',
+ 'home/activitystream/StreamItem.java',
+ 'home/activitystream/StreamRecyclerAdapter.java',
+ 'home/activitystream/topsites/CirclePageIndicator.java',
+ 'home/activitystream/topsites/TopSitesCard.java',
+ 'home/activitystream/topsites/TopSitesPage.java',
+ 'home/activitystream/topsites/TopSitesPageAdapter.java',
+ 'home/activitystream/topsites/TopSitesPagerAdapter.java',
+ 'home/BookmarkFolderView.java',
+ 'home/BookmarkScreenshotRow.java',
+ 'home/BookmarksListAdapter.java',
+ 'home/BookmarksListView.java',
+ 'home/BookmarksPanel.java',
+ 'home/BrowserSearch.java',
+ 'home/ClientsAdapter.java',
+ 'home/CombinedHistoryAdapter.java',
+ 'home/CombinedHistoryItem.java',
+ 'home/CombinedHistoryPanel.java',
+ 'home/CombinedHistoryRecyclerView.java',
+ 'home/DynamicPanel.java',
+ 'home/FramePanelLayout.java',
+ 'home/HistorySectionsHelper.java',
+ 'home/HomeAdapter.java',
+ 'home/HomeBanner.java',
+ 'home/HomeConfig.java',
+ 'home/HomeConfigLoader.java',
+ 'home/HomeConfigPrefsBackend.java',
+ 'home/HomeContextMenuInfo.java',
+ 'home/HomeExpandableListView.java',
+ 'home/HomeFragment.java',
+ 'home/HomeListView.java',
+ 'home/HomePager.java',
+ 'home/HomePanelsManager.java',
+ 'home/HomeScreen.java',
+ 'home/ImageLoader.java',
+ 'home/MultiTypeCursorAdapter.java',
+ 'home/PanelAuthCache.java',
+ 'home/PanelAuthLayout.java',
+ 'home/PanelBackItemView.java',
+ 'home/PanelHeaderView.java',
+ 'home/PanelInfoManager.java',
+ 'home/PanelItemView.java',
+ 'home/PanelLayout.java',
+ 'home/PanelListView.java',
+ 'home/PanelRecyclerView.java',
+ 'home/PanelRecyclerViewAdapter.java',
+ 'home/PanelRefreshLayout.java',
+ 'home/PanelViewAdapter.java',
+ 'home/PanelViewItemHandler.java',
+ 'home/PinSiteDialog.java',
+ 'home/RecentTabsAdapter.java',
+ 'home/RemoteTabsExpandableListState.java',
+ 'home/SearchEngine.java',
+ 'home/SearchEngineAdapter.java',
+ 'home/SearchEngineBar.java',
+ 'home/SearchEngineRow.java',
+ 'home/SearchLoader.java',
+ 'home/SimpleCursorLoader.java',
+ 'home/SpacingDecoration.java',
+ 'home/TabMenuStrip.java',
+ 'home/TabMenuStripLayout.java',
+ 'home/TopSitesGridItemView.java',
+ 'home/TopSitesGridView.java',
+ 'home/TopSitesPanel.java',
+ 'home/TopSitesThumbnailView.java',
+ 'home/TwoLinePageRow.java',
+ 'icons/decoders/FaviconDecoder.java',
+ 'icons/decoders/ICODecoder.java',
+ 'icons/decoders/IconDirectoryEntry.java',
+ 'icons/decoders/LoadFaviconResult.java',
+ 'icons/IconCallback.java',
+ 'icons/IconDescriptor.java',
+ 'icons/IconDescriptorComparator.java',
+ 'icons/IconRequest.java',
+ 'icons/IconRequestBuilder.java',
+ 'icons/IconRequestExecutor.java',
+ 'icons/IconResponse.java',
+ 'icons/Icons.java',
+ 'icons/IconsHelper.java',
+ 'icons/IconTask.java',
+ 'icons/loader/ContentProviderLoader.java',
+ 'icons/loader/DataUriLoader.java',
+ 'icons/loader/DiskLoader.java',
+ 'icons/loader/IconDownloader.java',
+ 'icons/loader/IconGenerator.java',
+ 'icons/loader/IconLoader.java',
+ 'icons/loader/JarLoader.java',
+ 'icons/loader/LegacyLoader.java',
+ 'icons/loader/MemoryLoader.java',
+ 'icons/preparation/AboutPagesPreparer.java',
+ 'icons/preparation/AddDefaultIconUrl.java',
+ 'icons/preparation/FilterKnownFailureUrls.java',
+ 'icons/preparation/FilterMimeTypes.java',
+ 'icons/preparation/FilterPrivilegedUrls.java',
+ 'icons/preparation/LookupIconUrl.java',
+ 'icons/preparation/Preparer.java',
+ 'icons/processing/ColorProcessor.java',
+ 'icons/processing/DiskProcessor.java',
+ 'icons/processing/MemoryProcessor.java',
+ 'icons/processing/Processor.java',
+ 'icons/processing/ResizingProcessor.java',
+ 'icons/storage/DiskStorage.java',
+ 'icons/storage/FailureCache.java',
+ 'icons/storage/MemoryStorage.java',
+ 'IntentHelper.java',
+ 'javaaddons/JavaAddonManager.java',
+ 'javaaddons/JavaAddonManagerV1.java',
+ 'LauncherActivity.java',
+ 'lwt/LightweightTheme.java',
+ 'lwt/LightweightThemeDrawable.java',
+ 'mdns/MulticastDNSManager.java',
+ 'media/AsyncCodec.java',
+ 'media/AsyncCodecFactory.java',
+ 'media/AudioFocusAgent.java',
+ 'media/Codec.java',
+ 'media/CodecProxy.java',
+ 'media/FormatParam.java',
+ 'media/GeckoMediaDrm.java',
+ 'media/GeckoMediaDrmBridgeV21.java',
+ 'media/GeckoMediaDrmBridgeV23.java',
+ 'media/JellyBeanAsyncCodec.java',
+ 'media/LocalMediaDrmBridge.java',
+ 'media/MediaControlService.java',
+ 'media/MediaDrmProxy.java',
+ 'media/MediaManager.java',
+ 'media/RemoteManager.java',
+ 'media/RemoteMediaDrmBridge.java',
+ 'media/RemoteMediaDrmBridgeStub.java',
+ 'media/Sample.java',
+ 'media/SamplePool.java',
+ 'media/SessionKeyInfo.java',
+ 'media/VideoPlayer.java',
+ 'MediaCastingBar.java',
+ 'MemoryMonitor.java',
+ 'menu/GeckoMenu.java',
+ 'menu/GeckoMenuInflater.java',
+ 'menu/GeckoMenuItem.java',
+ 'menu/GeckoSubMenu.java',
+ 'menu/MenuItemActionBar.java',
+ 'menu/MenuItemDefault.java',
+ 'menu/MenuItemSwitcherLayout.java',
+ 'menu/MenuPanel.java',
+ 'menu/MenuPopup.java',
+ 'MotionEventInterceptor.java',
+ 'mozglue/SharedMemBuffer.java',
+ 'mozglue/SharedMemory.java',
+ 'notifications/NotificationClient.java',
+ 'notifications/NotificationHelper.java',
+ 'notifications/NotificationReceiver.java',
+ 'notifications/NotificationService.java',
+ 'notifications/WhatsNewReceiver.java',
+ 'overlays/OverlayConstants.java',
+ 'overlays/service/OverlayActionService.java',
+ 'overlays/service/ShareData.java',
+ 'overlays/service/sharemethods/AddBookmark.java',
+ 'overlays/service/sharemethods/SendTab.java',
+ 'overlays/service/sharemethods/ShareMethod.java',
+ 'overlays/ui/OverlayDialogButton.java',
+ 'overlays/ui/SendTabDeviceListArrayAdapter.java',
+ 'overlays/ui/SendTabList.java',
+ 'overlays/ui/SendTabTargetSelectedListener.java',
+ 'overlays/ui/ShareDialog.java',
+ 'PackageReplacedReceiver.java',
+ 'preferences/AlignRightLinkPreference.java',
+ 'preferences/AndroidImport.java',
+ 'preferences/AndroidImportPreference.java',
+ 'preferences/AppCompatPreferenceActivity.java',
+ 'preferences/ClearOnShutdownPref.java',
+ 'preferences/CustomCheckBoxPreference.java',
+ 'preferences/CustomListCategory.java',
+ 'preferences/CustomListPreference.java',
+ 'preferences/DistroSharedPrefsImport.java',
+ 'preferences/FontSizePreference.java',
+ 'preferences/GeckoPreferenceFragment.java',
+ 'preferences/GeckoPreferences.java',
+ 'preferences/LinkPreference.java',
+ 'preferences/ListCheckboxPreference.java',
+ 'preferences/LocaleListPreference.java',
+ 'preferences/ModifiableHintPreference.java',
+ 'preferences/MultiChoicePreference.java',
+ 'preferences/MultiPrefMultiChoicePreference.java',
+ 'preferences/PanelsPreference.java',
+ 'preferences/PanelsPreferenceCategory.java',
+ 'preferences/PrivateDataPreference.java',
+ 'preferences/SearchEnginePreference.java',
+ 'preferences/SearchPreferenceCategory.java',
+ 'preferences/SetHomepagePreference.java',
+ 'preferences/SyncPreference.java',
+ 'PresentationView.java',
+ 'PrintHelper.java',
+ 'PrivateTab.java',
+ 'promotion/AddToHomeScreenPromotion.java',
+ 'promotion/HomeScreenPrompt.java',
+ 'promotion/ReaderViewBookmarkPromotion.java',
+ 'promotion/SimpleHelperUI.java',
+ 'prompts/ColorPickerInput.java',
+ 'prompts/IconGridInput.java',
+ 'prompts/IntentChooserPrompt.java',
+ 'prompts/IntentHandler.java',
+ 'prompts/Prompt.java',
+ 'prompts/PromptInput.java',
+ 'prompts/PromptListAdapter.java',
+ 'prompts/PromptListItem.java',
+ 'prompts/PromptService.java',
+ 'prompts/TabInput.java',
+ 'reader/ReaderModeUtils.java',
+ 'reader/ReadingListHelper.java',
+ 'reader/SavedReaderViewHelper.java',
+ 'RemoteClientsDialogFragment.java',
+ 'Restarter.java',
+ 'restrictions/DefaultConfiguration.java',
+ 'restrictions/GuestProfileConfiguration.java',
+ 'restrictions/Restrictable.java',
+ 'restrictions/RestrictedProfileConfiguration.java',
+ 'restrictions/RestrictionCache.java',
+ 'restrictions/RestrictionConfiguration.java',
+ 'restrictions/RestrictionProvider.java',
+ 'restrictions/Restrictions.java',
+ 'ScreenManagerHelper.java',
+ 'ScreenshotObserver.java',
+ 'search/SearchEngine.java',
+ 'search/SearchEngineManager.java',
+ 'SessionParser.java',
+ 'SharedPreferencesHelper.java',
+ 'SiteIdentity.java',
+ 'SnackbarBuilder.java',
+ 'SuggestClient.java',
+ 'Tab.java',
+ 'tabqueue/TabQueueHelper.java',
+ 'tabqueue/TabQueuePrompt.java',
+ 'tabqueue/TabQueueService.java',
+ 'tabqueue/TabReceivedService.java',
+ 'Tabs.java',
+ 'tabs/PrivateTabsPanel.java',
+ 'tabs/TabCurve.java',
+ 'tabs/TabHistoryController.java',
+ 'tabs/TabHistoryFragment.java',
+ 'tabs/TabHistoryItemRow.java',
+ 'tabs/TabHistoryPage.java',
+ 'tabs/TabPanelBackButton.java',
+ 'tabs/TabsGridLayout.java',
+ 'tabs/TabsLayout.java',
+ 'tabs/TabsLayoutAdapter.java',
+ 'tabs/TabsLayoutItemView.java',
+ 'tabs/TabsLayoutRecyclerAdapter.java',
+ 'tabs/TabsListLayout.java',
+ 'tabs/TabsListLayoutAnimator.java',
+ 'tabs/TabsPanel.java',
+ 'tabs/TabsPanelThumbnailView.java',
+ 'tabs/TabsTouchHelperCallback.java',
+ 'Telemetry.java',
+ 'telemetry/measurements/CampaignIdMeasurements.java',
+ 'telemetry/measurements/SearchCountMeasurements.java',
+ 'telemetry/measurements/SessionMeasurements.java',
+ 'telemetry/pingbuilders/TelemetryCorePingBuilder.java',
+ 'telemetry/pingbuilders/TelemetryPingBuilder.java',
+ 'telemetry/schedulers/TelemetryUploadAllPingsImmediatelyScheduler.java',
+ 'telemetry/schedulers/TelemetryUploadScheduler.java',
+ 'telemetry/stores/TelemetryJSONFilePingStore.java',
+ 'telemetry/stores/TelemetryPingStore.java',
+ 'telemetry/TelemetryConstants.java',
+ 'telemetry/TelemetryCorePingDelegate.java',
+ 'telemetry/TelemetryDispatcher.java',
+ 'telemetry/TelemetryPing.java',
+ 'telemetry/TelemetryPreferences.java',
+ 'telemetry/TelemetryUploadService.java',
+ 'TelemetryContract.java',
+ 'text/FloatingActionModeCallback.java',
+ 'text/FloatingToolbarTextSelection.java',
+ 'text/TextAction.java',
+ 'text/TextSelection.java',
+ 'ThumbnailHelper.java',
+ 'toolbar/AutocompleteHandler.java',
+ 'toolbar/BackButton.java',
+ 'toolbar/BrowserToolbar.java',
+ 'toolbar/BrowserToolbarPhone.java',
+ 'toolbar/BrowserToolbarPhoneBase.java',
+ 'toolbar/BrowserToolbarTablet.java',
+ 'toolbar/BrowserToolbarTabletBase.java',
+ 'toolbar/CanvasDelegate.java',
+ 'toolbar/ForwardButton.java',
+ 'toolbar/NavButton.java',
+ 'toolbar/PageActionLayout.java',
+ 'toolbar/PhoneTabsButton.java',
+ 'toolbar/ShapedButton.java',
+ 'toolbar/ShapedButtonFrameLayout.java',
+ 'toolbar/SiteIdentityPopup.java',
+ 'toolbar/TabCounter.java',
+ 'toolbar/ToolbarDisplayLayout.java',
+ 'toolbar/ToolbarEditLayout.java',
+ 'toolbar/ToolbarEditText.java',
+ 'toolbar/ToolbarPrefs.java',
+ 'toolbar/ToolbarProgressView.java',
+ 'trackingprotection/TrackingProtectionPrompt.java',
+ 'updater/PostUpdateHandler.java',
+ 'updater/UpdateService.java',
+ 'updater/UpdateServiceHelper.java',
+ 'util/ColorUtil.java',
+ 'util/DrawableUtil.java',
+ 'util/ResourceDrawableUtils.java',
+ 'util/TouchTargetUtil.java',
+ 'util/ViewUtil.java',
+ 'widget/ActivityChooserModel.java',
+ 'widget/AllCapsTextView.java',
+ 'widget/AnchoredPopup.java',
+ 'widget/AnimatedHeightLayout.java',
+ 'widget/BasicColorPicker.java',
+ 'widget/CheckableLinearLayout.java',
+ 'widget/ClickableWhenDisabledEditText.java',
+ 'widget/ContentSecurityDoorHanger.java',
+ 'widget/CropImageView.java',
+ 'widget/DateTimePicker.java',
+ 'widget/DefaultDoorHanger.java',
+ 'widget/DefaultItemAnimatorBase.java',
+ 'widget/DoorHanger.java',
+ 'widget/DoorhangerConfig.java',
+ 'widget/EllipsisTextView.java',
+ 'widget/ExternalIntentDuringPrivateBrowsingPromptFragment.java',
+ 'widget/FadedMultiColorTextView.java',
+ 'widget/FadedSingleColorTextView.java',
+ 'widget/FadedTextView.java',
+ 'widget/FaviconView.java',
+ 'widget/FilledCardView.java',
+ 'widget/FlowLayout.java',
+ 'widget/GeckoActionProvider.java',
+ 'widget/GeckoPopupMenu.java',
+ 'widget/HistoryDividerItemDecoration.java',
+ 'widget/IconTabWidget.java',
+ 'widget/LoginDoorHanger.java',
+ 'widget/RecyclerViewClickSupport.java',
+ 'widget/ResizablePathDrawable.java',
+ 'widget/RoundedCornerLayout.java',
+ 'widget/SiteLogins.java',
+ 'widget/SquaredImageView.java',
+ 'widget/SquaredRelativeLayout.java',
+ 'widget/SwipeDismissListViewTouchListener.java',
+ 'widget/TabThumbnailWrapper.java',
+ 'widget/ThumbnailView.java',
+ 'widget/TouchDelegateWithReset.java',
+ 'widget/TwoWayView.java',
+ 'ZoomedView.java',
+]]
+# The following sources are checked in to version control but
+# generated by a script (java/org/mozilla/gecko/widget/themed/generate_themed_views.py).
+# If you're editing this list, make sure to edit that script.
+gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
+ 'widget/themed/ThemedEditText.java',
+ 'widget/themed/ThemedFrameLayout.java',
+ 'widget/themed/ThemedImageButton.java',
+ 'widget/themed/ThemedImageView.java',
+ 'widget/themed/ThemedLinearLayout.java',
+ 'widget/themed/ThemedRelativeLayout.java',
+ 'widget/themed/ThemedTextSwitcher.java',
+ 'widget/themed/ThemedTextView.java',
+ 'widget/themed/ThemedView.java',
+]]
+android_package_dir = CONFIG['ANDROID_PACKAGE_NAME'].replace('.', '/')
+gbjar.generated_sources = [] # Keep it this way.
+gbjar.extra_jars += [
+ CONFIG['ANDROID_SUPPORT_ANNOTATIONS_JAR_LIB'],
+ CONFIG['ANDROID_SUPPORT_V4_AAR_LIB'],
+ CONFIG['ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB'],
+ 'constants.jar'
+]
+if CONFIG['MOZ_CRASHREPORTER']:
+ gbjar.sources += [ 'java/org/mozilla/gecko/CrashReporter.java' ]
+ ANDROID_RES_DIRS += [ 'crashreporter/res' ]
+
+if CONFIG['MOZ_ANDROID_GCM']:
+ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
+ 'gcm/GcmInstanceIDListenerService.java',
+ 'gcm/GcmMessageListenerService.java',
+ 'gcm/GcmTokenClient.java',
+ 'push/Fetched.java',
+ 'push/PushClient.java',
+ 'push/PushManager.java',
+ 'push/PushRegistration.java',
+ 'push/PushService.java',
+ 'push/PushState.java',
+ 'push/PushSubscription.java',
+ ]]
+
+if (CONFIG['MOZ_ANDROID_MAX_SDK_VERSION']):
+ max_sdk_version = int(CONFIG['MOZ_ANDROID_MAX_SDK_VERSION'])
+else:
+ max_sdk_version = 999
+
+# Only bother to include new tablet code if we're building for tablet-capable
+# OS releases.
+if max_sdk_version >= 11:
+ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
+ 'tabs/TabStrip.java',
+ 'tabs/TabStripAdapter.java',
+ 'tabs/TabStripItemView.java',
+ 'tabs/TabStripView.java'
+ ]]
+
+gbjar.extra_jars += [
+ OBJDIR + '/../javaaddons/javaaddons-1.0.jar',
+ 'gecko-R.jar',
+ 'gecko-mozglue.jar',
+ 'gecko-thirdparty.jar',
+ 'gecko-util.jar',
+ 'gecko-view.jar',
+ 'sync-thirdparty.jar',
+ 'services.jar',
+]
+
+moz_native_devices_jars = [
+ CONFIG['ANDROID_MEDIAROUTER_V7_AAR_LIB'],
+ CONFIG['ANDROID_MEDIAROUTER_V7_AAR_INTERNAL_LIB'],
+ CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR_LIB'],
+ CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB'],
+ CONFIG['ANDROID_PLAY_SERVICES_CAST_AAR_LIB'],
+]
+moz_native_devices_sources = ['java/org/mozilla/gecko/' + x for x in [
+ 'ChromeCastDisplay.java',
+ 'ChromeCastPlayer.java',
+ 'GeckoMediaPlayer.java',
+ 'GeckoPresentationDisplay.java',
+ 'MediaPlayerManager.java',
+ 'PresentationMediaPlayerManager.java',
+ 'RemotePresentationService.java',
+]]
+if CONFIG['MOZ_NATIVE_DEVICES']:
+ gbjar.extra_jars += moz_native_devices_jars
+ gbjar.sources += moz_native_devices_sources
+
+ if CONFIG['ANDROID_MEDIAROUTER_V7_AAR']:
+ ANDROID_EXTRA_PACKAGES += ['android.support.v7.mediarouter']
+ ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_MEDIAROUTER_V7_AAR_RES']]
+ resjar.generated_sources += ['android/support/v7/mediarouter/R.java']
+
+ if CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR']:
+ ANDROID_EXTRA_PACKAGES += ['com.google.android.gms.base']
+ ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR_RES']]
+ resjar.generated_sources += ['com/google/android/gms/base/R.java']
+
+ if CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR']:
+ ANDROID_EXTRA_PACKAGES += ['com.google.android.gms']
+ ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR_RES']]
+ resjar.generated_sources += ['com/google/android/gms/R.java']
+
+ if CONFIG['ANDROID_PLAY_SERVICES_CAST_AAR']:
+ ANDROID_EXTRA_PACKAGES += ['com.google.android.gms.cast']
+ ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_CAST_AAR_RES']]
+ resjar.generated_sources += ['com/google/android/gms/cast/R.java']
+
+if CONFIG['MOZ_ANDROID_GCM']:
+ gbjar.extra_jars += [
+ CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR_LIB'],
+ CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB'],
+ CONFIG['ANDROID_PLAY_SERVICES_GCM_AAR_LIB'],
+ CONFIG['ANDROID_PLAY_SERVICES_MEASUREMENT_AAR_LIB'],
+ ]
+
+ if CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR']:
+ ANDROID_EXTRA_PACKAGES += ['com.google.android.gms']
+ ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR_RES']]
+ resjar.generated_sources += ['com/google/android/gms/R.java']
+
+ if CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR']:
+ ANDROID_EXTRA_PACKAGES += ['com.google.android.gms']
+ ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR_RES']]
+ resjar.generated_sources += ['com/google/android/gms/R.java']
+
+ if CONFIG['ANDROID_PLAY_SERVICES_GCM_AAR']:
+ ANDROID_EXTRA_PACKAGES += ['com.google.android.gms.gcm']
+ ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_GCM_AAR_RES']]
+# (no resources) resjar.generated_sources += ['com/google/android/gms/gcm/R.java']
+
+ if CONFIG['ANDROID_PLAY_SERVICES_MEASUREMENT_AAR']:
+ ANDROID_EXTRA_PACKAGES += ['com.google.android.gms.measurement']
+ ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_MEASUREMENT_AAR_RES']]
+# (no resources) resjar.generated_sources += ['android/support/v7/palette/R.java']
+
+if CONFIG['MOZ_INSTALL_TRACKING']:
+ gbjar.extra_jars += [
+ CONFIG['ANDROID_PLAY_SERVICES_ADS_AAR_LIB'],
+ CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB'],
+ ]
+
+ if CONFIG['ANDROID_PLAY_SERVICES_ADS_AAR']:
+ ANDROID_EXTRA_PACKAGES += ['com.google.android.gms.ads']
+ ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_ADS_AAR_RES']]
+ resjar.generated_sources += ['com/google/android/gms/ads/R.java']
+
+ if CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR']:
+ ANDROID_EXTRA_PACKAGES += ['com.google.android.gms']
+ ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR_RES']]
+ resjar.generated_sources += ['com/google/android/gms/R.java']
+
+gbjar.extra_jars += [CONFIG['ANDROID_APPCOMPAT_V7_AAR_LIB']]
+gbjar.extra_jars += [CONFIG['ANDROID_SUPPORT_VECTOR_DRAWABLE_AAR_LIB']]
+gbjar.extra_jars += [CONFIG['ANDROID_ANIMATED_VECTOR_DRAWABLE_AAR_LIB']]
+gbjar.extra_jars += [CONFIG['ANDROID_CARDVIEW_V7_AAR_LIB']]
+gbjar.extra_jars += [CONFIG['ANDROID_DESIGN_AAR_LIB']]
+gbjar.extra_jars += [CONFIG['ANDROID_RECYCLERVIEW_V7_AAR_LIB']]
+gbjar.extra_jars += [CONFIG['ANDROID_CUSTOMTABS_AAR_LIB']]
+gbjar.extra_jars += [CONFIG['ANDROID_PALETTE_V7_AAR_LIB']]
+
+gbjar.javac_flags += ['-Xlint:all,-deprecation,-fallthrough', '-J-Xmx512m', '-J-Xms128m']
+
+# gecko-thirdparty is a good place to put small independent libraries
+gtjar = add_java_jar('gecko-thirdparty')
+gtjar.sources += [ thirdparty_source_dir + f for f in [
+ 'com/jakewharton/disklrucache/DiskLruCache.java',
+ 'com/jakewharton/disklrucache/StrictLineReader.java',
+ 'com/jakewharton/disklrucache/Util.java',
+ 'com/keepsafe/switchboard/AsyncConfigLoader.java',
+ 'com/keepsafe/switchboard/DeviceUuidFactory.java',
+ 'com/keepsafe/switchboard/Preferences.java',
+ 'com/keepsafe/switchboard/Switch.java',
+ 'com/keepsafe/switchboard/SwitchBoard.java',
+ 'com/squareup/leakcanary/LeakCanary.java',
+ 'com/squareup/leakcanary/RefWatcher.java',
+ 'com/squareup/picasso/Action.java',
+ 'com/squareup/picasso/AssetBitmapHunter.java',
+ 'com/squareup/picasso/BitmapHunter.java',
+ 'com/squareup/picasso/Cache.java',
+ 'com/squareup/picasso/Callback.java',
+ 'com/squareup/picasso/ContactsPhotoBitmapHunter.java',
+ 'com/squareup/picasso/ContentStreamBitmapHunter.java',
+ 'com/squareup/picasso/DeferredRequestCreator.java',
+ 'com/squareup/picasso/Dispatcher.java',
+ 'com/squareup/picasso/Downloader.java',
+ 'com/squareup/picasso/FetchAction.java',
+ 'com/squareup/picasso/FileBitmapHunter.java',
+ 'com/squareup/picasso/GetAction.java',
+ 'com/squareup/picasso/ImageViewAction.java',
+ 'com/squareup/picasso/LruCache.java',
+ 'com/squareup/picasso/MarkableInputStream.java',
+ 'com/squareup/picasso/MediaStoreBitmapHunter.java',
+ 'com/squareup/picasso/NetworkBitmapHunter.java',
+ 'com/squareup/picasso/Picasso.java',
+ 'com/squareup/picasso/PicassoDrawable.java',
+ 'com/squareup/picasso/PicassoExecutorService.java',
+ 'com/squareup/picasso/Request.java',
+ 'com/squareup/picasso/RequestCreator.java',
+ 'com/squareup/picasso/ResourceBitmapHunter.java',
+ 'com/squareup/picasso/Stats.java',
+ 'com/squareup/picasso/StatsSnapshot.java',
+ 'com/squareup/picasso/Target.java',
+ 'com/squareup/picasso/TargetAction.java',
+ 'com/squareup/picasso/Transformation.java',
+ 'com/squareup/picasso/UrlConnectionDownloader.java',
+ 'com/squareup/picasso/Utils.java'
+] ]
+gtjar.extra_jars = [
+ CONFIG['ANDROID_SUPPORT_ANNOTATIONS_JAR_LIB'],
+ CONFIG['ANDROID_SUPPORT_V4_AAR_LIB'],
+ CONFIG['ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB'],
+]
+
+if not CONFIG['MOZILLA_OFFICIAL']:
+ gtjar.sources += [ thirdparty_source_dir + f for f in [
+ 'org/lucasr/dspec/DesignSpec.java',
+ 'org/lucasr/dspec/RawResource.java'
+ ] ]
+
+if CONFIG['MOZ_INSTALL_TRACKING']:
+ adjustjar = add_java_jar('gecko-thirdparty-adjust_sdk')
+ adjustjar.sources += [ thirdparty_source_dir + f for f in [
+ 'com/adjust/sdk/ActivityHandler.java',
+ 'com/adjust/sdk/ActivityKind.java',
+ 'com/adjust/sdk/ActivityPackage.java',
+ 'com/adjust/sdk/ActivityState.java',
+ 'com/adjust/sdk/Adjust.java',
+ 'com/adjust/sdk/AdjustAttribution.java',
+ 'com/adjust/sdk/AdjustConfig.java',
+ 'com/adjust/sdk/AdjustEvent.java',
+ 'com/adjust/sdk/AdjustFactory.java',
+ 'com/adjust/sdk/AdjustInstance.java',
+ 'com/adjust/sdk/AdjustReferrerReceiver.java',
+ 'com/adjust/sdk/AttributionHandler.java',
+ 'com/adjust/sdk/Constants.java',
+ 'com/adjust/sdk/DeviceInfo.java',
+ 'com/adjust/sdk/IActivityHandler.java',
+ 'com/adjust/sdk/IAttributionHandler.java',
+ 'com/adjust/sdk/ILogger.java',
+ 'com/adjust/sdk/IPackageHandler.java',
+ 'com/adjust/sdk/IRequestHandler.java',
+ 'com/adjust/sdk/Logger.java',
+ 'com/adjust/sdk/LogLevel.java',
+ 'com/adjust/sdk/OnAttributionChangedListener.java',
+ 'com/adjust/sdk/PackageBuilder.java',
+ 'com/adjust/sdk/PackageHandler.java',
+ 'com/adjust/sdk/plugin/AndroidIdUtil.java',
+ 'com/adjust/sdk/plugin/MacAddressUtil.java',
+ 'com/adjust/sdk/plugin/Plugin.java',
+ 'com/adjust/sdk/Reflection.java',
+ 'com/adjust/sdk/RequestHandler.java',
+ 'com/adjust/sdk/UnitTestActivity.java',
+ 'com/adjust/sdk/Util.java'
+ ] ]
+ adjustjar.extra_jars += [
+ 'sync-thirdparty.jar',
+ ]
+
+# Putting branding earlier allows branders to override default resources.
+ANDROID_RES_DIRS += [
+ '/' + CONFIG['MOZ_BRANDING_DIRECTORY'] + '/res',
+ 'resources',
+ '/mobile/android/services/src/main/res',
+ '!res',
+]
+
+ANDROID_GENERATED_RESFILES += [
+ 'res/raw/browsersearch.json',
+ 'res/raw/suggestedsites.json',
+ 'res/values/strings.xml',
+]
+
+ANDROID_ASSETS_DIRS += [
+ '/mobile/android/app/assets',
+]
+
+if CONFIG['MOZ_ANDROID_DISTRIBUTION_DIRECTORY']:
+ # If you change this, also change its equivalent in mobile/android/bouncer.
+ if not CONFIG['MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER']:
+ # If we are packaging the bouncer, it will have the distribution, so don't put
+ # it in the main APK as well.
+ ANDROID_ASSETS_DIRS += [
+ '%' + CONFIG['MOZ_ANDROID_DISTRIBUTION_DIRECTORY'] + '/assets',
+ ]
+
+# We do not expose MOZ_ADJUST_SDK_KEY here because that # would leak the value
+# to build logs. Instead we expose the token quietly where appropriate in
+# Makefile.in.
+for var in ('MOZ_ANDROID_ANR_REPORTER', 'MOZ_DEBUG',
+ 'MOZ_ANDROID_SEARCH_ACTIVITY', 'MOZ_NATIVE_DEVICES', 'MOZ_ANDROID_MLS_STUMBLER',
+ 'MOZ_ANDROID_DOWNLOADS_INTEGRATION', 'MOZ_INSTALL_TRACKING',
+ 'MOZ_ANDROID_GCM', 'MOZ_ANDROID_EXCLUDE_FONTS', 'MOZ_LOCALE_SWITCHER',
+ 'MOZ_ANDROID_BEAM', 'MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE',
+ 'MOZ_SWITCHBOARD', 'MOZ_ANDROID_CUSTOM_TABS',
+ 'MOZ_ANDROID_ACTIVITY_STREAM'):
+ if CONFIG[var]:
+ DEFINES[var] = 1
+
+for var in ('MOZ_UPDATER', 'MOZ_PKG_SPECIAL', 'MOZ_ANDROID_GCM_SENDERID'):
+ if CONFIG[var]:
+ DEFINES[var] = CONFIG[var]
+
+for var in ('ANDROID_PACKAGE_NAME', 'ANDROID_CPU_ARCH',
+ 'GRE_MILESTONE', 'MOZ_APP_BASENAME', 'MOZ_MOZILLA_API_KEY',
+ 'MOZ_APP_DISPLAYNAME', 'MOZ_APP_UA_NAME', 'MOZ_APP_ID', 'MOZ_APP_NAME',
+ 'MOZ_APP_VENDOR', 'MOZ_APP_VERSION', 'MOZ_CHILD_PROCESS_NAME',
+ 'MOZ_ANDROID_APPLICATION_CLASS', 'MOZ_ANDROID_BROWSER_INTENT_CLASS', 'MOZ_ANDROID_SEARCH_INTENT_CLASS',
+ 'MOZ_CRASHREPORTER', 'MOZ_UPDATE_CHANNEL', 'OMNIJAR_NAME',
+ 'OS_TARGET', 'TARGET_XPCOM_ABI'):
+ DEFINES[var] = CONFIG[var]
+
+# Mangle our package name to avoid Bug 750548.
+DEFINES['MANGLED_ANDROID_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME'].replace('fennec', 'f3nn3c')
+DEFINES['MOZ_APP_ABI'] = CONFIG['TARGET_XPCOM_ABI']
+if not CONFIG['COMPILE_ENVIRONMENT']:
+ # These should really come from the included binaries, but that's not easy.
+ DEFINES['MOZ_APP_ABI'] = 'arm-eabi-gcc3' # Observe quote differences here ...
+ DEFINES['TARGET_XPCOM_ABI'] = '"arm-eabi-gcc3"' # ... and here.
+
+if '-march=armv7' in CONFIG['OS_CFLAGS']:
+ DEFINES['MOZ_MIN_CPU_VERSION'] = 7
+else:
+ DEFINES['MOZ_MIN_CPU_VERSION'] = 5
+
+if CONFIG['MOZ_ANDROID_SEARCH_ACTIVITY']:
+ # The Search Activity is mostly independent of Fennec proper, but
+ # it does depend on Geckoview. Therefore, we build it as a jar
+ # that depends on the Geckoview jars.
+ search_source_dir = SRCDIR + '/../search'
+ include('../search/search_activity_sources.mozbuild')
+
+ search_activity = add_java_jar('search-activity')
+ search_activity.sources += [search_source_dir + '/' + f for f in search_activity_sources]
+ search_activity.javac_flags += ['-Xlint:all']
+ search_activity.extra_jars = [
+ CONFIG['ANDROID_SUPPORT_ANNOTATIONS_JAR_LIB'],
+ CONFIG['ANDROID_SUPPORT_V4_AAR_LIB'],
+ CONFIG['ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB'],
+ 'constants.jar',
+ 'gecko-R.jar',
+ 'gecko-browser.jar',
+ 'gecko-mozglue.jar',
+ 'gecko-thirdparty.jar',
+ 'gecko-util.jar',
+ 'gecko-view.jar',
+ ]
+
+FINAL_TARGET_PP_FILES += ['package-name.txt.in']
+
+DEFINES['OBJDIR'] = OBJDIR
+DEFINES['TOPOBJDIR'] = TOPOBJDIR
+
+OBJDIR_PP_FILES.mobile.android.base += [
+ 'AndroidManifest.xml.in',
+]
+
+gbjar.sources += ['generated/org/mozilla/gecko/' + x for x in [
+ 'media/ICodec.java',
+ 'media/ICodecCallbacks.java',
+ 'media/IMediaDrmBridge.java',
+ 'media/IMediaDrmBridgeCallbacks.java',
+ 'media/IMediaManager.java',
+]]
diff --git a/mobile/android/base/package-name.txt.in b/mobile/android/base/package-name.txt.in
new file mode 100644
index 0000000000..c7b9731cb3
--- /dev/null
+++ b/mobile/android/base/package-name.txt.in
@@ -0,0 +1,2 @@
+#filter substitution
+@ANDROID_PACKAGE_NAME@
diff --git a/mobile/android/base/resources/anim/grow_fade_in.xml b/mobile/android/base/resources/anim/grow_fade_in.xml
new file mode 100644
index 0000000000..bf7a3f23d9
--- /dev/null
+++ b/mobile/android/base/resources/anim/grow_fade_in.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <scale android:fromXScale="1.0"
+ android:toXScale="1.0"
+ android:fromYScale="0.3"
+ android:toYScale="1.0"
+ android:pivotX="0%"
+ android:pivotY="0%"
+ android:duration="150"/>
+
+ <alpha android:interpolator="@android:anim/decelerate_interpolator"
+ android:fromAlpha="0.0"
+ android:toAlpha="1.0"
+ android:duration="150"/>
+
+</set>
diff --git a/mobile/android/base/resources/anim/overlay_check_entry.xml b/mobile/android/base/resources/anim/overlay_check_entry.xml
new file mode 100644
index 0000000000..5a80612a7f
--- /dev/null
+++ b/mobile/android/base/resources/anim/overlay_check_entry.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<alpha
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="250"
+ android:fromAlpha="0.0"
+ android:toAlpha="1.0" />
diff --git a/mobile/android/base/resources/anim/overlay_check_exit.xml b/mobile/android/base/resources/anim/overlay_check_exit.xml
new file mode 100644
index 0000000000..9c42eaa0b0
--- /dev/null
+++ b/mobile/android/base/resources/anim/overlay_check_exit.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<alpha
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="250"
+ android:fromAlpha="1.0"
+ android:toAlpha="0.0"
+ android:fillAfter="true" />
diff --git a/mobile/android/base/resources/anim/overlay_pop.xml b/mobile/android/base/resources/anim/overlay_pop.xml
new file mode 100644
index 0000000000..6b5c412dce
--- /dev/null
+++ b/mobile/android/base/resources/anim/overlay_pop.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fillAfter="true" >
+
+ <scale
+ android:duration="300"
+ android:fromXScale="1"
+ android:fromYScale="1"
+ android:pivotX="50%"
+ android:pivotY="50%"
+ android:toXScale="2"
+ android:toYScale="2" >
+ </scale>
+
+ <scale
+ android:duration="300"
+ android:fromXScale="1"
+ android:fromYScale="1"
+ android:pivotX="50%"
+ android:pivotY="50%"
+ android:toXScale="0.5"
+ android:toYScale="0.5" >
+ </scale>
+
+</set>
diff --git a/mobile/android/base/resources/anim/overlay_slide_down.xml b/mobile/android/base/resources/anim/overlay_slide_down.xml
new file mode 100644
index 0000000000..c7007458cc
--- /dev/null
+++ b/mobile/android/base/resources/anim/overlay_slide_down.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@android:integer/config_longAnimTime"
+ android:fromYDelta="0"
+ android:toYDelta="100%p"
+ android:fillAfter="true" />
diff --git a/mobile/android/base/resources/anim/overlay_slide_up.xml b/mobile/android/base/resources/anim/overlay_slide_up.xml
new file mode 100644
index 0000000000..2541493ed3
--- /dev/null
+++ b/mobile/android/base/resources/anim/overlay_slide_up.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@android:integer/config_longAnimTime"
+ android:fromYDelta="100%p"
+ android:toYDelta="0" />
diff --git a/mobile/android/base/resources/anim/popup_hide.xml b/mobile/android/base/resources/anim/popup_hide.xml
new file mode 100644
index 0000000000..5ea5bc2cbe
--- /dev/null
+++ b/mobile/android/base/resources/anim/popup_hide.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:interpolator="@android:anim/decelerate_interpolator"
+ android:duration="150">
+
+ <translate android:fromYDelta="-0"
+ android:toYDelta="-20"/>
+
+ <alpha android:fromAlpha="1.0"
+ android:toAlpha="0.0"/>
+
+</set>
diff --git a/mobile/android/base/resources/anim/popup_show.xml b/mobile/android/base/resources/anim/popup_show.xml
new file mode 100644
index 0000000000..6057b9063f
--- /dev/null
+++ b/mobile/android/base/resources/anim/popup_show.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:interpolator="@android:anim/decelerate_interpolator"
+ android:duration="150">
+
+ <translate android:fromYDelta="-20"
+ android:toYDelta="0"/>
+
+ <alpha android:fromAlpha="0.0"
+ android:toAlpha="1.0"/>
+
+</set>
diff --git a/mobile/android/base/resources/color/action_bar_menu_item_colors.xml b/mobile/android/base/resources/color/action_bar_menu_item_colors.xml
new file mode 100644
index 0000000000..756463d68c
--- /dev/null
+++ b/mobile/android/base/resources/color/action_bar_menu_item_colors.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <item gecko:state_private="true"
+ android:state_enabled="false"
+ android:color="@color/toolbar_icon_grey"
+ />
+
+ <item gecko:state_private="true"
+ android:state_enabled="true"
+ android:color="@color/tabs_tray_icon_grey"
+ />
+
+ <item gecko:state_private="false"
+ android:state_enabled="false"
+ android:color="@color/disabled_grey"
+ />
+
+ <item android:color="@color/toolbar_icon_grey" />
+
+</selector>
diff --git a/mobile/android/base/resources/color/action_bar_secondary_menu_item_colors.xml b/mobile/android/base/resources/color/action_bar_secondary_menu_item_colors.xml
new file mode 100644
index 0000000000..c72ce0923f
--- /dev/null
+++ b/mobile/android/base/resources/color/action_bar_secondary_menu_item_colors.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <item android:state_enabled="false"
+ android:color="@color/disabled_grey"
+ />
+
+ <item android:color="@color/toolbar_icon_grey" />
+
+</selector>
diff --git a/mobile/android/base/resources/color/facet_button_text_color.xml b/mobile/android/base/resources/color/facet_button_text_color.xml
new file mode 100644
index 0000000000..90da6e3247
--- /dev/null
+++ b/mobile/android/base/resources/color/facet_button_text_color.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- facet is selected -->
+ <item android:state_checked="true" android:color="@color/facet_button_text_color_selected" />
+
+ <!-- default -->
+ <item android:color="@color/facet_button_text_color_default" />
+</selector>
diff --git a/mobile/android/base/resources/color/pressed_about_page_header_grey.xml b/mobile/android/base/resources/color/pressed_about_page_header_grey.xml
new file mode 100644
index 0000000000..18533ff670
--- /dev/null
+++ b/mobile/android/base/resources/color/pressed_about_page_header_grey.xml
@@ -0,0 +1,12 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_pressed="true"
+ android:drawable="@color/about_page_header_grey" />
+
+ <item android:drawable="@android:color/transparent"/>
+
+</selector>
diff --git a/mobile/android/base/resources/color/primary_text.xml b/mobile/android/base/resources/color/primary_text.xml
new file mode 100644
index 0000000000..59d5699db3
--- /dev/null
+++ b/mobile/android/base/resources/color/primary_text.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_enabled="false" android:color="@color/text_color_primary_disable_only" />
+ <item android:color="@color/placeholder_active_grey"/>
+
+</selector>
diff --git a/mobile/android/base/resources/color/primary_text_selector.xml b/mobile/android/base/resources/color/primary_text_selector.xml
new file mode 100644
index 0000000000..d21a60880c
--- /dev/null
+++ b/mobile/android/base/resources/color/primary_text_selector.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_enabled="false" android:color="@color/disabled_grey" />
+ <item android:color="@color/text_and_tabs_tray_grey" />
+
+</selector>
diff --git a/mobile/android/base/resources/color/recyclerview_selector.xml b/mobile/android/base/resources/color/recyclerview_selector.xml
new file mode 100644
index 0000000000..6ecc3c5e21
--- /dev/null
+++ b/mobile/android/base/resources/color/recyclerview_selector.xml
@@ -0,0 +1,12 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_pressed="true"
+ android:drawable="@color/highlight" />
+
+ <item android:drawable="@android:color/transparent"/>
+
+</selector>
diff --git a/mobile/android/base/resources/color/secondary_text.xml b/mobile/android/base/resources/color/secondary_text.xml
new file mode 100644
index 0000000000..73bebcd151
--- /dev/null
+++ b/mobile/android/base/resources/color/secondary_text.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_enabled="false" android:color="@color/text_color_primary_disable_only" />
+ <item android:color="@color/placeholder_grey"/>
+
+</selector>
diff --git a/mobile/android/base/resources/color/select_item_multichoice.xml b/mobile/android/base/resources/color/select_item_multichoice.xml
new file mode 100644
index 0000000000..9eb9e2178a
--- /dev/null
+++ b/mobile/android/base/resources/color/select_item_multichoice.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_enabled="false" android:color="#999" />
+ <item android:color="#000"/>
+
+</selector>
diff --git a/mobile/android/base/resources/color/state_pressed_toolbar_grey_pressed.xml b/mobile/android/base/resources/color/state_pressed_toolbar_grey_pressed.xml
new file mode 100644
index 0000000000..41eb992dee
--- /dev/null
+++ b/mobile/android/base/resources/color/state_pressed_toolbar_grey_pressed.xml
@@ -0,0 +1,12 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_pressed="true"
+ android:drawable="@color/toolbar_grey_pressed" />
+
+ <item android:drawable="@android:color/transparent"/>
+
+</selector>
diff --git a/mobile/android/base/resources/color/tab_item_title.xml b/mobile/android/base/resources/color/tab_item_title.xml
new file mode 100644
index 0000000000..443d4c11d4
--- /dev/null
+++ b/mobile/android/base/resources/color/tab_item_title.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_checked="true"
+ android:color="@color/about_page_header_grey"/>
+
+ <item android:state_pressed="true"
+ android:color="@color/about_page_header_grey"/>
+
+ <item android:color="@color/tabs_tray_icon_grey"/>
+
+</selector>
diff --git a/mobile/android/base/resources/color/tab_new_tab_strip_colors.xml b/mobile/android/base/resources/color/tab_new_tab_strip_colors.xml
new file mode 100644
index 0000000000..fd7a97b21c
--- /dev/null
+++ b/mobile/android/base/resources/color/tab_new_tab_strip_colors.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <item gecko:state_light="true"
+ android:color="@color/toolbar_icon_grey"/>
+
+ <item android:color="@color/tabs_tray_icon_grey"/>
+
+</selector> \ No newline at end of file
diff --git a/mobile/android/base/resources/color/tab_strip_item_bg.xml b/mobile/android/base/resources/color/tab_strip_item_bg.xml
new file mode 100644
index 0000000000..b8e7e298bf
--- /dev/null
+++ b/mobile/android/base/resources/color/tab_strip_item_bg.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <item android:state_pressed="true"
+ android:state_checked="true"
+ gecko:state_private="true"
+ android:color="@color/highlight_nav_pb" />
+
+ <item android:state_checked="true"
+ gecko:state_private="true"
+ android:color="@color/tabs_tray_grey_pressed" />
+
+ <item android:state_pressed="true"
+ gecko:state_private="true"
+ android:color="@color/highlight_dark_focused" />
+
+ <item android:state_pressed="true"
+ android:state_checked="true"
+ android:color="@color/toolbar_grey_pressed" />
+
+ <!-- Lightweight Themes -->
+ <item android:state_checked="true"
+ gecko:state_light="true"
+ android:color="@color/background_normal_lwt" />
+ <item android:state_checked="true"
+ gecko:state_dark="true"
+ android:color="@color/background_normal_lwt" />
+ <item android:state_pressed="true"
+ gecko:state_light="true"
+ android:color="@color/tablet_highlight_lwt" />
+ <item android:state_pressed="true"
+ gecko:state_dark="true"
+ android:color="@color/tablet_highlight_dark_lwt" />
+
+ <item android:state_checked="true"
+ android:color="@color/toolbar_grey" />
+
+ <item android:state_pressed="true"
+ android:color="@color/tabs_tray_grey_pressed" />
+
+ <item android:color="@android:color/transparent"/>
+
+</selector>
diff --git a/mobile/android/base/resources/color/tab_strip_item_title.xml b/mobile/android/base/resources/color/tab_strip_item_title.xml
new file mode 100644
index 0000000000..f4fca97505
--- /dev/null
+++ b/mobile/android/base/resources/color/tab_strip_item_title.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <item android:state_checked="true"
+ gecko:state_private="true"
+ android:color="@color/about_page_header_grey"/>
+
+ <item android:state_checked="true"
+ android:color="@color/placeholder_grey"/>
+
+ <item android:state_checked="false"
+ gecko:state_dark="true"
+ android:color="@color/about_page_header_grey"/>
+
+ <item android:color="@color/placeholder_grey"/>
+
+</selector>
diff --git a/mobile/android/base/resources/color/tab_text_color.xml b/mobile/android/base/resources/color/tab_text_color.xml
new file mode 100644
index 0000000000..51aba7e2a6
--- /dev/null
+++ b/mobile/android/base/resources/color/tab_text_color.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true"
+ android:color="@color/placeholder_grey" />
+
+ <item android:color="@color/panel_tab_text_normal"/>
+</selector> \ No newline at end of file
diff --git a/mobile/android/base/resources/color/tabs_counter_text_color.xml b/mobile/android/base/resources/color/tabs_counter_text_color.xml
new file mode 100644
index 0000000000..945634b7a6
--- /dev/null
+++ b/mobile/android/base/resources/color/tabs_counter_text_color.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <item gecko:state_private="true"
+ android:color="@color/tabs_tray_grey_pressed"/>
+
+ <item android:color="@color/toolbar_grey"/>
+
+</selector> \ No newline at end of file
diff --git a/mobile/android/base/resources/color/tertiary_text.xml b/mobile/android/base/resources/color/tertiary_text.xml
new file mode 100644
index 0000000000..54884519c6
--- /dev/null
+++ b/mobile/android/base/resources/color/tertiary_text.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_enabled="false" android:color="@color/text_color_primary_disable_only" />
+ <item android:color="@color/text_color_tertiary"/>
+
+</selector>
diff --git a/mobile/android/base/resources/color/toolbar_display_layout_bg.xml b/mobile/android/base/resources/color/toolbar_display_layout_bg.xml
new file mode 100644
index 0000000000..9a99e0858b
--- /dev/null
+++ b/mobile/android/base/resources/color/toolbar_display_layout_bg.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- These colors are defined from the drawables url_bar_entry_default_* -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <item gecko:state_private="true" android:color="@color/placeholder_active_grey"/>
+
+ <item android:color="#FFFFFF"/>
+
+</selector>
diff --git a/mobile/android/base/resources/color/top_sites_grid_item_title.xml b/mobile/android/base/resources/color/top_sites_grid_item_title.xml
new file mode 100644
index 0000000000..89ca50294d
--- /dev/null
+++ b/mobile/android/base/resources/color/top_sites_grid_item_title.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!-- empty with no url -->
+ <item android:state_empty="true" android:color="#80777777" />
+
+ <!-- default -->
+ <item android:color="@color/placeholder_grey"/>
+
+</selector>
diff --git a/mobile/android/base/resources/color/url_bar_title.xml b/mobile/android/base/resources/color/url_bar_title.xml
new file mode 100644
index 0000000000..473bee41d2
--- /dev/null
+++ b/mobile/android/base/resources/color/url_bar_title.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <!-- private browsing mode -->
+ <item gecko:state_private="true" android:color="@color/private_active_text" />
+
+ <!-- dark theme -->
+ <item gecko:state_dark="true" android:color="@color/private_active_text"/>
+
+ <!-- light theme -->
+ <item gecko:state_light="true" android:color="@color/placeholder_active_grey"/>
+
+ <!-- normal mode -->
+ <item android:color="@color/placeholder_active_grey"/>
+
+</selector>
diff --git a/mobile/android/base/resources/color/url_bar_title_hint.xml b/mobile/android/base/resources/color/url_bar_title_hint.xml
new file mode 100644
index 0000000000..0c748ac0bf
--- /dev/null
+++ b/mobile/android/base/resources/color/url_bar_title_hint.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <!-- private browsing mode -->
+ <item gecko:state_private="true" android:color="#FF7F828A" />
+
+ <!-- normal mode -->
+ <item android:color="#FF666666"/>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable-hdpi-v11/alert_camera.png b/mobile/android/base/resources/drawable-hdpi-v11/alert_camera.png
new file mode 100644
index 0000000000..11800d0f10
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi-v11/alert_camera.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi-v11/alert_download.png b/mobile/android/base/resources/drawable-hdpi-v11/alert_download.png
new file mode 100644
index 0000000000..1b8f59e567
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi-v11/alert_download.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi-v11/alert_guest.png b/mobile/android/base/resources/drawable-hdpi-v11/alert_guest.png
new file mode 100644
index 0000000000..650b8246d1
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi-v11/alert_guest.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi-v11/alert_mic.png b/mobile/android/base/resources/drawable-hdpi-v11/alert_mic.png
new file mode 100644
index 0000000000..4b0248b8bb
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi-v11/alert_mic.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi-v11/alert_mic_camera.png b/mobile/android/base/resources/drawable-hdpi-v11/alert_mic_camera.png
new file mode 100644
index 0000000000..091ec077dd
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi-v11/alert_mic_camera.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi-v11/ic_menu_back.png b/mobile/android/base/resources/drawable-hdpi-v11/ic_menu_back.png
new file mode 100644
index 0000000000..d0ec44a967
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi-v11/ic_menu_back.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi-v11/ic_menu_bookmark_add.png b/mobile/android/base/resources/drawable-hdpi-v11/ic_menu_bookmark_add.png
new file mode 100644
index 0000000000..a58abd2f14
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi-v11/ic_menu_bookmark_add.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi-v11/ic_menu_forward.png b/mobile/android/base/resources/drawable-hdpi-v11/ic_menu_forward.png
new file mode 100644
index 0000000000..3a8db422f4
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi-v11/ic_menu_forward.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi-v11/ic_menu_reload.png b/mobile/android/base/resources/drawable-hdpi-v11/ic_menu_reload.png
new file mode 100644
index 0000000000..d0ae488c76
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi-v11/ic_menu_reload.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi-v11/ic_status_logo.png b/mobile/android/base/resources/drawable-hdpi-v11/ic_status_logo.png
new file mode 100644
index 0000000000..5524dd072f
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi-v11/ic_status_logo.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi-v11/star_blue.png b/mobile/android/base/resources/drawable-hdpi-v11/star_blue.png
new file mode 100644
index 0000000000..fc292debb8
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi-v11/star_blue.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/ab_add_search_engine.png b/mobile/android/base/resources/drawable-hdpi/ab_add_search_engine.png
new file mode 100644
index 0000000000..77b99cdc77
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/ab_add_search_engine.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/ab_copy.png b/mobile/android/base/resources/drawable-hdpi/ab_copy.png
new file mode 100644
index 0000000000..6bf796c4f5
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/ab_copy.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/ab_cut.png b/mobile/android/base/resources/drawable-hdpi/ab_cut.png
new file mode 100644
index 0000000000..0b41f5431c
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/ab_cut.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/ab_done.png b/mobile/android/base/resources/drawable-hdpi/ab_done.png
new file mode 100644
index 0000000000..6b81da3c07
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/ab_done.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/ab_mic.png b/mobile/android/base/resources/drawable-hdpi/ab_mic.png
new file mode 100644
index 0000000000..13f8eb356e
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/ab_mic.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/ab_paste.png b/mobile/android/base/resources/drawable-hdpi/ab_paste.png
new file mode 100644
index 0000000000..58fb8a31e9
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/ab_paste.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/ab_qrcode.png b/mobile/android/base/resources/drawable-hdpi/ab_qrcode.png
new file mode 100644
index 0000000000..727201a2c7
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/ab_qrcode.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/ab_search.png b/mobile/android/base/resources/drawable-hdpi/ab_search.png
new file mode 100644
index 0000000000..0d217f11a1
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/ab_search.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/ab_select_all.png b/mobile/android/base/resources/drawable-hdpi/ab_select_all.png
new file mode 100644
index 0000000000..7488ed5713
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/ab_select_all.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/alert_camera.png b/mobile/android/base/resources/drawable-hdpi/alert_camera.png
new file mode 100644
index 0000000000..6ae4be0787
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/alert_camera.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/alert_download.png b/mobile/android/base/resources/drawable-hdpi/alert_download.png
new file mode 100644
index 0000000000..b55e779fc6
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/alert_download.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/alert_download_animation_1.png b/mobile/android/base/resources/drawable-hdpi/alert_download_animation_1.png
new file mode 100644
index 0000000000..22d6288ea4
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/alert_download_animation_1.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/alert_download_animation_2.png b/mobile/android/base/resources/drawable-hdpi/alert_download_animation_2.png
new file mode 100644
index 0000000000..dc402e9b8b
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/alert_download_animation_2.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/alert_download_animation_3.png b/mobile/android/base/resources/drawable-hdpi/alert_download_animation_3.png
new file mode 100644
index 0000000000..719bcbdabf
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/alert_download_animation_3.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/alert_download_animation_4.png b/mobile/android/base/resources/drawable-hdpi/alert_download_animation_4.png
new file mode 100644
index 0000000000..4319d0039c
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/alert_download_animation_4.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/alert_download_animation_5.png b/mobile/android/base/resources/drawable-hdpi/alert_download_animation_5.png
new file mode 100644
index 0000000000..f8d1a15d6c
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/alert_download_animation_5.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/alert_download_animation_6.png b/mobile/android/base/resources/drawable-hdpi/alert_download_animation_6.png
new file mode 100644
index 0000000000..dffe363386
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/alert_download_animation_6.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/alert_guest.png b/mobile/android/base/resources/drawable-hdpi/alert_guest.png
new file mode 100644
index 0000000000..17fc059c3e
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/alert_guest.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/alert_mic.png b/mobile/android/base/resources/drawable-hdpi/alert_mic.png
new file mode 100644
index 0000000000..d218bc88c4
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/alert_mic.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/alert_mic_camera.png b/mobile/android/base/resources/drawable-hdpi/alert_mic_camera.png
new file mode 100644
index 0000000000..ef33012b57
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/alert_mic_camera.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/arrow_up.png b/mobile/android/base/resources/drawable-hdpi/arrow_up.png
new file mode 100644
index 0000000000..dfa79aaff4
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/arrow_up.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/blank.png b/mobile/android/base/resources/drawable-hdpi/blank.png
new file mode 100644
index 0000000000..56535e7efd
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/blank.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/casting.png b/mobile/android/base/resources/drawable-hdpi/casting.png
new file mode 100644
index 0000000000..3da63dc55b
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/casting.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/casting_active.png b/mobile/android/base/resources/drawable-hdpi/casting_active.png
new file mode 100644
index 0000000000..baf55c4cce
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/casting_active.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/close.png b/mobile/android/base/resources/drawable-hdpi/close.png
new file mode 100644
index 0000000000..b14612fa0c
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/close.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/close_edit_mode_dark.png b/mobile/android/base/resources/drawable-hdpi/close_edit_mode_dark.png
new file mode 100644
index 0000000000..1e28f00c5b
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/close_edit_mode_dark.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/close_edit_mode_light.png b/mobile/android/base/resources/drawable-hdpi/close_edit_mode_light.png
new file mode 100644
index 0000000000..4c82e5696e
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/close_edit_mode_light.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/color_picker_row_bg.9.png b/mobile/android/base/resources/drawable-hdpi/color_picker_row_bg.9.png
new file mode 100644
index 0000000000..50460696ab
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/color_picker_row_bg.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/device_desktop.png b/mobile/android/base/resources/drawable-hdpi/device_desktop.png
new file mode 100644
index 0000000000..ef5300abd6
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/device_desktop.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/device_mobile.png b/mobile/android/base/resources/drawable-hdpi/device_mobile.png
new file mode 100644
index 0000000000..6e58c94495
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/device_mobile.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/dropshadow.9.png b/mobile/android/base/resources/drawable-hdpi/dropshadow.9.png
new file mode 100644
index 0000000000..1273996c54
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/dropshadow.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/favicon_globe.png b/mobile/android/base/resources/drawable-hdpi/favicon_globe.png
new file mode 100644
index 0000000000..235af27208
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/favicon_globe.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/find_close.png b/mobile/android/base/resources/drawable-hdpi/find_close.png
new file mode 100644
index 0000000000..e98f59e686
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/find_close.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/find_next.png b/mobile/android/base/resources/drawable-hdpi/find_next.png
new file mode 100644
index 0000000000..788f45c7ea
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/find_next.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/find_prev.png b/mobile/android/base/resources/drawable-hdpi/find_prev.png
new file mode 100644
index 0000000000..4c40a1e8ab
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/find_prev.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/firefox_settings_alert.png b/mobile/android/base/resources/drawable-hdpi/firefox_settings_alert.png
new file mode 100644
index 0000000000..7094c9aad5
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/firefox_settings_alert.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/flat_icon.png b/mobile/android/base/resources/drawable-hdpi/flat_icon.png
new file mode 100644
index 0000000000..d4b946f086
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/flat_icon.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/folder_closed.png b/mobile/android/base/resources/drawable-hdpi/folder_closed.png
new file mode 100644
index 0000000000..87adf55bfc
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/folder_closed.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/globe_light.png b/mobile/android/base/resources/drawable-hdpi/globe_light.png
new file mode 100644
index 0000000000..39098f7769
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/globe_light.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/grid_icon_bg_activated.9.png b/mobile/android/base/resources/drawable-hdpi/grid_icon_bg_activated.9.png
new file mode 100644
index 0000000000..d2ca3fdd43
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/grid_icon_bg_activated.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/grid_icon_bg_focused.9.png b/mobile/android/base/resources/drawable-hdpi/grid_icon_bg_focused.9.png
new file mode 100644
index 0000000000..510d297ea6
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/grid_icon_bg_focused.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/handle_end.png b/mobile/android/base/resources/drawable-hdpi/handle_end.png
new file mode 100644
index 0000000000..9cb9d6fbb4
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/handle_end.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/handle_middle.png b/mobile/android/base/resources/drawable-hdpi/handle_middle.png
new file mode 100644
index 0000000000..075a30029b
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/handle_middle.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/handle_start.png b/mobile/android/base/resources/drawable-hdpi/handle_start.png
new file mode 100644
index 0000000000..ccbc1767da
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/handle_start.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/helper_readerview_bookmark.webp b/mobile/android/base/resources/drawable-hdpi/helper_readerview_bookmark.webp
new file mode 100644
index 0000000000..f1f22df5c2
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/helper_readerview_bookmark.webp
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/home_bg.png b/mobile/android/base/resources/drawable-hdpi/home_bg.png
new file mode 100644
index 0000000000..6cde348254
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/home_bg.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/home_group_collapsed.png b/mobile/android/base/resources/drawable-hdpi/home_group_collapsed.png
new file mode 100644
index 0000000000..ca2f764f18
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/home_group_collapsed.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/home_star.png b/mobile/android/base/resources/drawable-hdpi/home_star.png
new file mode 100644
index 0000000000..29ff5b0f1d
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/home_star.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/home_tab_menu_strip.9.png b/mobile/android/base/resources/drawable-hdpi/home_tab_menu_strip.9.png
new file mode 100644
index 0000000000..319cc773c2
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/home_tab_menu_strip.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/homepage_banner_firstrun.png b/mobile/android/base/resources/drawable-hdpi/homepage_banner_firstrun.png
new file mode 100644
index 0000000000..915eac7de2
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/homepage_banner_firstrun.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/ic_action_settings.png b/mobile/android/base/resources/drawable-hdpi/ic_action_settings.png
new file mode 100644
index 0000000000..de96174a31
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/ic_action_settings.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/ic_media_pause.png b/mobile/android/base/resources/drawable-hdpi/ic_media_pause.png
new file mode 100644
index 0000000000..3f4f2b3161
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/ic_media_pause.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/ic_media_play.png b/mobile/android/base/resources/drawable-hdpi/ic_media_play.png
new file mode 100644
index 0000000000..172dfd9d65
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/ic_media_play.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/ic_menu_share.png b/mobile/android/base/resources/drawable-hdpi/ic_menu_share.png
new file mode 100644
index 0000000000..66a0ad1983
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/ic_menu_share.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/ic_status_logo.png b/mobile/android/base/resources/drawable-hdpi/ic_status_logo.png
new file mode 100644
index 0000000000..4cd8e3c071
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/ic_status_logo.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/ic_url_bar_tab.png b/mobile/android/base/resources/drawable-hdpi/ic_url_bar_tab.png
new file mode 100644
index 0000000000..97272c7f80
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/ic_url_bar_tab.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/ic_widget_new_tab.png b/mobile/android/base/resources/drawable-hdpi/ic_widget_new_tab.png
new file mode 100644
index 0000000000..48a9664140
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/ic_widget_new_tab.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/ic_widget_search.png b/mobile/android/base/resources/drawable-hdpi/ic_widget_search.png
new file mode 100644
index 0000000000..c885787b6f
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/ic_widget_search.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/icon_bookmarks_empty.png b/mobile/android/base/resources/drawable-hdpi/icon_bookmarks_empty.png
new file mode 100644
index 0000000000..a3645a054b
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/icon_bookmarks_empty.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/icon_home_empty_firefox.png b/mobile/android/base/resources/drawable-hdpi/icon_home_empty_firefox.png
new file mode 100644
index 0000000000..2a490d7ac8
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/icon_home_empty_firefox.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/icon_key.png b/mobile/android/base/resources/drawable-hdpi/icon_key.png
new file mode 100644
index 0000000000..bc742e161e
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/icon_key.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/icon_most_recent_empty.png b/mobile/android/base/resources/drawable-hdpi/icon_most_recent_empty.png
new file mode 100644
index 0000000000..92b41a224a
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/icon_most_recent_empty.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/icon_openinapp.png b/mobile/android/base/resources/drawable-hdpi/icon_openinapp.png
new file mode 100644
index 0000000000..0527956daa
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/icon_openinapp.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/icon_pageaction.png b/mobile/android/base/resources/drawable-hdpi/icon_pageaction.png
new file mode 100644
index 0000000000..2b4b09003c
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/icon_pageaction.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/icon_remote_tabs_empty.png b/mobile/android/base/resources/drawable-hdpi/icon_remote_tabs_empty.png
new file mode 100644
index 0000000000..e520d2db85
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/icon_remote_tabs_empty.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/icon_search_empty_firefox.png b/mobile/android/base/resources/drawable-hdpi/icon_search_empty_firefox.png
new file mode 100644
index 0000000000..fd271d4233
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/icon_search_empty_firefox.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/icon_shareplane.png b/mobile/android/base/resources/drawable-hdpi/icon_shareplane.png
new file mode 100644
index 0000000000..cdc6e01ae7
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/icon_shareplane.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/img_check.png b/mobile/android/base/resources/drawable-hdpi/img_check.png
new file mode 100644
index 0000000000..bb01e07713
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/img_check.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/location.png b/mobile/android/base/resources/drawable-hdpi/location.png
new file mode 100644
index 0000000000..18c54718a3
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/location.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/lock_disabled.png b/mobile/android/base/resources/drawable-hdpi/lock_disabled.png
new file mode 100644
index 0000000000..e7f3eb56cf
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/lock_disabled.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/lock_inactive.png b/mobile/android/base/resources/drawable-hdpi/lock_inactive.png
new file mode 100644
index 0000000000..f1bfe63cc5
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/lock_inactive.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/lock_secure.png b/mobile/android/base/resources/drawable-hdpi/lock_secure.png
new file mode 100644
index 0000000000..c1e95f3d86
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/lock_secure.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/media_bar_pause.png b/mobile/android/base/resources/drawable-hdpi/media_bar_pause.png
new file mode 100644
index 0000000000..46e838347b
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/media_bar_pause.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/media_bar_play.png b/mobile/android/base/resources/drawable-hdpi/media_bar_play.png
new file mode 100644
index 0000000000..36f0797f76
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/media_bar_play.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/media_bar_stop.png b/mobile/android/base/resources/drawable-hdpi/media_bar_stop.png
new file mode 100644
index 0000000000..db971c20f2
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/media_bar_stop.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/menu.png b/mobile/android/base/resources/drawable-hdpi/menu.png
new file mode 100644
index 0000000000..0e028bb792
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/menu.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/menu_item_check.png b/mobile/android/base/resources/drawable-hdpi/menu_item_check.png
new file mode 100644
index 0000000000..e13d6288af
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/menu_item_check.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/menu_item_more.png b/mobile/android/base/resources/drawable-hdpi/menu_item_more.png
new file mode 100644
index 0000000000..509e8de55b
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/menu_item_more.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/menu_item_uncheck.png b/mobile/android/base/resources/drawable-hdpi/menu_item_uncheck.png
new file mode 100644
index 0000000000..2fa5d52513
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/menu_item_uncheck.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/network_error.png b/mobile/android/base/resources/drawable-hdpi/network_error.png
new file mode 100644
index 0000000000..bdaa961d34
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/network_error.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/notification_media.webp b/mobile/android/base/resources/drawable-hdpi/notification_media.webp
new file mode 100644
index 0000000000..b4fae68468
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/notification_media.webp
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/open_in_browser.png b/mobile/android/base/resources/drawable-hdpi/open_in_browser.png
new file mode 100644
index 0000000000..e78d96665d
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/open_in_browser.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/overlay_bookmark_icon.png b/mobile/android/base/resources/drawable-hdpi/overlay_bookmark_icon.png
new file mode 100644
index 0000000000..6cb5559934
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/overlay_bookmark_icon.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/overlay_bookmarked_already_icon.png b/mobile/android/base/resources/drawable-hdpi/overlay_bookmarked_already_icon.png
new file mode 100644
index 0000000000..c5f91c58d9
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/overlay_bookmarked_already_icon.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/overlay_check.png b/mobile/android/base/resources/drawable-hdpi/overlay_check.png
new file mode 100644
index 0000000000..b69ed0dea6
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/overlay_check.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/pause.png b/mobile/android/base/resources/drawable-hdpi/pause.png
new file mode 100644
index 0000000000..17266e6089
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/pause.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/phone.png b/mobile/android/base/resources/drawable-hdpi/phone.png
new file mode 100644
index 0000000000..7fad6f05b0
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/phone.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/pin.png b/mobile/android/base/resources/drawable-hdpi/pin.png
new file mode 100644
index 0000000000..8111a04dc7
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/pin.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/play.png b/mobile/android/base/resources/drawable-hdpi/play.png
new file mode 100644
index 0000000000..8e599c16db
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/play.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/private_masq.png b/mobile/android/base/resources/drawable-hdpi/private_masq.png
new file mode 100644
index 0000000000..8791f24bc4
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/private_masq.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/progress.9.png b/mobile/android/base/resources/drawable-hdpi/progress.9.png
new file mode 100644
index 0000000000..5293a77d43
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/progress.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/reader.png b/mobile/android/base/resources/drawable-hdpi/reader.png
new file mode 100644
index 0000000000..2ac1b8d46a
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/reader.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/reader_active.png b/mobile/android/base/resources/drawable-hdpi/reader_active.png
new file mode 100644
index 0000000000..99851ae7c5
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/reader_active.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/reading_list_folder.png b/mobile/android/base/resources/drawable-hdpi/reading_list_folder.png
new file mode 100644
index 0000000000..052994110d
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/reading_list_folder.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/search_clear.png b/mobile/android/base/resources/drawable-hdpi/search_clear.png
new file mode 100644
index 0000000000..4dbefcb687
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/search_clear.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/search_history.png b/mobile/android/base/resources/drawable-hdpi/search_history.png
new file mode 100644
index 0000000000..a552fef20d
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/search_history.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/search_icon_active.png b/mobile/android/base/resources/drawable-hdpi/search_icon_active.png
new file mode 100644
index 0000000000..65e921896c
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/search_icon_active.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/search_icon_inactive.png b/mobile/android/base/resources/drawable-hdpi/search_icon_inactive.png
new file mode 100644
index 0000000000..b777bc3be9
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/search_icon_inactive.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/search_launcher.png b/mobile/android/base/resources/drawable-hdpi/search_launcher.png
new file mode 100644
index 0000000000..70c0d7630a
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/search_launcher.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/search_plus.png b/mobile/android/base/resources/drawable-hdpi/search_plus.png
new file mode 100644
index 0000000000..e20d9c6d29
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/search_plus.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/settings_notifications.png b/mobile/android/base/resources/drawable-hdpi/settings_notifications.png
new file mode 100644
index 0000000000..d2b23cf74a
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/settings_notifications.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/shareplane.png b/mobile/android/base/resources/drawable-hdpi/shareplane.png
new file mode 100644
index 0000000000..748893b493
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/shareplane.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/shield_disabled.png b/mobile/android/base/resources/drawable-hdpi/shield_disabled.png
new file mode 100644
index 0000000000..90f669af28
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/shield_disabled.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/shield_enabled.png b/mobile/android/base/resources/drawable-hdpi/shield_enabled.png
new file mode 100644
index 0000000000..ff6fb6d807
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/shield_enabled.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/status_icon_readercache.png b/mobile/android/base/resources/drawable-hdpi/status_icon_readercache.png
new file mode 100644
index 0000000000..142ac4bdd8
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/status_icon_readercache.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/suggestedsites_amazon.png b/mobile/android/base/resources/drawable-hdpi/suggestedsites_amazon.png
new file mode 100644
index 0000000000..776cc4934c
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/suggestedsites_amazon.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/suggestedsites_facebook.png b/mobile/android/base/resources/drawable-hdpi/suggestedsites_facebook.png
new file mode 100644
index 0000000000..340b9a5e4d
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/suggestedsites_facebook.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/suggestedsites_twitter.png b/mobile/android/base/resources/drawable-hdpi/suggestedsites_twitter.png
new file mode 100644
index 0000000000..f3d9fd2380
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/suggestedsites_twitter.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/suggestedsites_wikipedia.png b/mobile/android/base/resources/drawable-hdpi/suggestedsites_wikipedia.png
new file mode 100644
index 0000000000..9c6efc14ad
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/suggestedsites_wikipedia.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/suggestedsites_youtube.png b/mobile/android/base/resources/drawable-hdpi/suggestedsites_youtube.png
new file mode 100644
index 0000000000..7e73e60750
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/suggestedsites_youtube.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/switch_button_icon.png b/mobile/android/base/resources/drawable-hdpi/switch_button_icon.png
new file mode 100644
index 0000000000..56c4cd0506
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/switch_button_icon.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/tab_audio_playing.png b/mobile/android/base/resources/drawable-hdpi/tab_audio_playing.png
new file mode 100644
index 0000000000..4635cf5aef
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/tab_audio_playing.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/tab_close.png b/mobile/android/base/resources/drawable-hdpi/tab_close.png
new file mode 100644
index 0000000000..7aadf4c45b
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/tab_close.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/tab_close_active.png b/mobile/android/base/resources/drawable-hdpi/tab_close_active.png
new file mode 100644
index 0000000000..39752bb916
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/tab_close_active.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/tab_indicator_background.9.png b/mobile/android/base/resources/drawable-hdpi/tab_indicator_background.9.png
new file mode 100644
index 0000000000..e225e8ce94
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/tab_indicator_background.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/tab_indicator_divider.9.png b/mobile/android/base/resources/drawable-hdpi/tab_indicator_divider.9.png
new file mode 100644
index 0000000000..f1338d8031
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/tab_indicator_divider.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/tab_indicator_selected.9.png b/mobile/android/base/resources/drawable-hdpi/tab_indicator_selected.9.png
new file mode 100644
index 0000000000..7ebd02f6f7
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/tab_indicator_selected.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/tab_indicator_selected_focused.9.png b/mobile/android/base/resources/drawable-hdpi/tab_indicator_selected_focused.9.png
new file mode 100644
index 0000000000..272bbaeade
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/tab_indicator_selected_focused.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/tab_new.png b/mobile/android/base/resources/drawable-hdpi/tab_new.png
new file mode 100644
index 0000000000..8e8557c830
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/tab_new.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/tab_preview_masq.png b/mobile/android/base/resources/drawable-hdpi/tab_preview_masq.png
new file mode 100644
index 0000000000..69dd8a093c
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/tab_preview_masq.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/tabs_count.png b/mobile/android/base/resources/drawable-hdpi/tabs_count.png
new file mode 100644
index 0000000000..c8db01a1aa
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/tabs_count.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/tabs_count_foreground.png b/mobile/android/base/resources/drawable-hdpi/tabs_count_foreground.png
new file mode 100644
index 0000000000..c9b9c055b0
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/tabs_count_foreground.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/tabs_normal.png b/mobile/android/base/resources/drawable-hdpi/tabs_normal.png
new file mode 100644
index 0000000000..49b4ec8bda
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/tabs_normal.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/tabs_panel_nav_back.png b/mobile/android/base/resources/drawable-hdpi/tabs_panel_nav_back.png
new file mode 100644
index 0000000000..cc74ae4d6d
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/tabs_panel_nav_back.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/tabs_private.png b/mobile/android/base/resources/drawable-hdpi/tabs_private.png
new file mode 100644
index 0000000000..50ff113093
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/tabs_private.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/tip_addsearch.png b/mobile/android/base/resources/drawable-hdpi/tip_addsearch.png
new file mode 100644
index 0000000000..8f02949c5b
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/tip_addsearch.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/top_site_add.png b/mobile/android/base/resources/drawable-hdpi/top_site_add.png
new file mode 100644
index 0000000000..cbfd8e4d41
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/top_site_add.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/tracking_protection_toolbar_illustration.png b/mobile/android/base/resources/drawable-hdpi/tracking_protection_toolbar_illustration.png
new file mode 100644
index 0000000000..9984d7f3ec
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/tracking_protection_toolbar_illustration.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/undo_button_icon.png b/mobile/android/base/resources/drawable-hdpi/undo_button_icon.png
new file mode 100644
index 0000000000..9964ccd0f8
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/undo_button_icon.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/url_bar_entry_default.9.png b/mobile/android/base/resources/drawable-hdpi/url_bar_entry_default.9.png
new file mode 100644
index 0000000000..1d8cc90559
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/url_bar_entry_default.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/url_bar_entry_default_pb.9.png b/mobile/android/base/resources/drawable-hdpi/url_bar_entry_default_pb.9.png
new file mode 100644
index 0000000000..0bd6115fcf
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/url_bar_entry_default_pb.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/url_bar_entry_pressed.9.png b/mobile/android/base/resources/drawable-hdpi/url_bar_entry_pressed.9.png
new file mode 100644
index 0000000000..91be77476c
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/url_bar_entry_pressed.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/url_bar_entry_pressed_pb.9.png b/mobile/android/base/resources/drawable-hdpi/url_bar_entry_pressed_pb.9.png
new file mode 100644
index 0000000000..6b076e75ea
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/url_bar_entry_pressed_pb.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/urlbar_stop.png b/mobile/android/base/resources/drawable-hdpi/urlbar_stop.png
new file mode 100644
index 0000000000..3e8ac68fc6
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/urlbar_stop.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/validation_arrow.png b/mobile/android/base/resources/drawable-hdpi/validation_arrow.png
new file mode 100644
index 0000000000..c0563ce942
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/validation_arrow.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/validation_arrow_inverted.png b/mobile/android/base/resources/drawable-hdpi/validation_arrow_inverted.png
new file mode 100644
index 0000000000..165d3c2e19
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/validation_arrow_inverted.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/validation_bg.9.png b/mobile/android/base/resources/drawable-hdpi/validation_bg.9.png
new file mode 100644
index 0000000000..bd83160e74
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/validation_bg.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/warning_major.png b/mobile/android/base/resources/drawable-hdpi/warning_major.png
new file mode 100644
index 0000000000..61cd21f978
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/warning_major.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/warning_minor.png b/mobile/android/base/resources/drawable-hdpi/warning_minor.png
new file mode 100644
index 0000000000..c78b77a134
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/warning_minor.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-hdpi/widget_bg.9.png b/mobile/android/base/resources/drawable-hdpi/widget_bg.9.png
new file mode 100644
index 0000000000..a5df36d997
--- /dev/null
+++ b/mobile/android/base/resources/drawable-hdpi/widget_bg.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_back.png b/mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_back.png
new file mode 100644
index 0000000000..d18fc9cd5f
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_back.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_forward.png b/mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_forward.png
new file mode 100644
index 0000000000..22a822d678
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_forward.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_reload.png b/mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_reload.png
new file mode 100644
index 0000000000..7cc5d918bb
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_reload.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-hdpi-v11/tabs_count.png b/mobile/android/base/resources/drawable-large-hdpi-v11/tabs_count.png
new file mode 100644
index 0000000000..7db22b0ca1
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-hdpi-v11/tabs_count.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-hdpi-v11/tabs_count_foreground.png b/mobile/android/base/resources/drawable-large-hdpi-v11/tabs_count_foreground.png
new file mode 100644
index 0000000000..3875c80cc7
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-hdpi-v11/tabs_count_foreground.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-hdpi-v11/toolbar_favicon_default.png b/mobile/android/base/resources/drawable-large-hdpi-v11/toolbar_favicon_default.png
new file mode 100644
index 0000000000..9daea10b72
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-hdpi-v11/toolbar_favicon_default.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-hdpi-v11/url_bar_entry_default.9.png b/mobile/android/base/resources/drawable-large-hdpi-v11/url_bar_entry_default.9.png
new file mode 100644
index 0000000000..a2e973ea0c
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-hdpi-v11/url_bar_entry_default.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-hdpi-v11/url_bar_entry_default_pb.9.png b/mobile/android/base/resources/drawable-large-hdpi-v11/url_bar_entry_default_pb.9.png
new file mode 100644
index 0000000000..0e91b33abc
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-hdpi-v11/url_bar_entry_default_pb.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-hdpi-v11/url_bar_entry_pressed.9.png b/mobile/android/base/resources/drawable-large-hdpi-v11/url_bar_entry_pressed.9.png
new file mode 100644
index 0000000000..a55ad0b9c3
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-hdpi-v11/url_bar_entry_pressed.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-hdpi-v11/url_bar_entry_pressed_pb.9.png b/mobile/android/base/resources/drawable-large-hdpi-v11/url_bar_entry_pressed_pb.9.png
new file mode 100644
index 0000000000..ac6476de70
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-hdpi-v11/url_bar_entry_pressed_pb.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-v11/browser_toolbar_action_bar_button.xml b/mobile/android/base/resources/drawable-large-v11/browser_toolbar_action_bar_button.xml
new file mode 100644
index 0000000000..e1654c1cf6
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-v11/browser_toolbar_action_bar_button.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <item gecko:state_private="true"
+ android:state_pressed="true"
+ android:state_enabled="true">
+
+ <inset android:insetTop="@dimen/tablet_browser_toolbar_menu_item_inset_vertical"
+ android:insetBottom="@dimen/tablet_browser_toolbar_menu_item_inset_vertical"
+ android:insetLeft="@dimen/tablet_browser_toolbar_menu_item_inset_horizontal"
+ android:insetRight="@dimen/tablet_browser_toolbar_menu_item_inset_horizontal">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/text_and_tabs_tray_grey"/>
+ <corners android:radius="@dimen/standard_corner_radius"/>
+ </shape>
+ </inset>
+
+ </item>
+
+ <item gecko:state_private="true"
+ android:state_focused="true"
+ android:state_pressed="false">
+
+ <inset android:insetTop="@dimen/tablet_browser_toolbar_menu_item_inset_vertical"
+ android:insetBottom="@dimen/tablet_browser_toolbar_menu_item_inset_vertical"
+ android:insetLeft="@dimen/tablet_browser_toolbar_menu_item_inset_horizontal"
+ android:insetRight="@dimen/tablet_browser_toolbar_menu_item_inset_horizontal">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/placeholder_active_grey"/>
+ <corners android:radius="@dimen/standard_corner_radius"/>
+ </shape>
+ </inset>
+
+ </item>
+
+ <item android:state_pressed="true"
+ android:state_enabled="true">
+
+ <inset android:insetTop="@dimen/tablet_browser_toolbar_menu_item_inset_vertical"
+ android:insetBottom="@dimen/tablet_browser_toolbar_menu_item_inset_vertical"
+ android:insetLeft="@dimen/tablet_browser_toolbar_menu_item_inset_horizontal"
+ android:insetRight="@dimen/tablet_browser_toolbar_menu_item_inset_horizontal">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/toolbar_grey_pressed"/>
+ <corners android:radius="@dimen/standard_corner_radius"/>
+ </shape>
+ </inset>
+
+ </item>
+
+ <item android:state_focused="true"
+ android:state_pressed="false">
+
+ <inset android:insetTop="@dimen/tablet_browser_toolbar_menu_item_inset_vertical"
+ android:insetBottom="@dimen/tablet_browser_toolbar_menu_item_inset_vertical"
+ android:insetLeft="@dimen/tablet_browser_toolbar_menu_item_inset_horizontal"
+ android:insetRight="@dimen/tablet_browser_toolbar_menu_item_inset_horizontal">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/tablet_highlight_focused"/>
+ <corners android:radius="@dimen/standard_corner_radius"/>
+ </shape>
+ </inset>
+
+ </item>
+
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@android:color/transparent"/>
+ </shape>
+ </item>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable-large-v11/url_bar_nav_button.xml b/mobile/android/base/resources/drawable-large-v11/url_bar_nav_button.xml
new file mode 100644
index 0000000000..0a34179271
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-v11/url_bar_nav_button.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <!-- private pressed state -->
+ <item gecko:state_private="true"
+ android:state_pressed="true"
+ android:drawable="@color/text_and_tabs_tray_grey"/>
+
+ <!-- focused state -->
+ <item gecko:state_private="true"
+ android:state_focused="true"
+ android:state_pressed="false"
+ android:drawable="@color/placeholder_active_grey"/>
+
+ <!-- pressed state -->
+ <item android:state_pressed="true"
+ android:drawable="@color/toolbar_grey_pressed"/>
+
+ <!-- focused state -->
+ <item android:state_focused="true"
+ android:state_pressed="false"
+ android:drawable="@color/tablet_highlight_focused"/>
+
+ <!-- private browsing mode -->
+ <item gecko:state_private="true"
+ android:drawable="@color/tabs_tray_grey_pressed"/>
+
+ <!-- normal mode -->
+ <item android:drawable="@color/toolbar_grey"/>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_back.png b/mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_back.png
new file mode 100644
index 0000000000..78a33ffab4
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_back.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_forward.png b/mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_forward.png
new file mode 100644
index 0000000000..7a284903f3
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_forward.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_reload.png b/mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_reload.png
new file mode 100644
index 0000000000..a9c7b3f62d
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_reload.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-xhdpi-v11/tabs_count.png b/mobile/android/base/resources/drawable-large-xhdpi-v11/tabs_count.png
new file mode 100644
index 0000000000..16e41c0cef
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-xhdpi-v11/tabs_count.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-xhdpi-v11/tabs_count_foreground.png b/mobile/android/base/resources/drawable-large-xhdpi-v11/tabs_count_foreground.png
new file mode 100644
index 0000000000..85dc05e4a6
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-xhdpi-v11/tabs_count_foreground.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-xhdpi-v11/toolbar_favicon_default.png b/mobile/android/base/resources/drawable-large-xhdpi-v11/toolbar_favicon_default.png
new file mode 100644
index 0000000000..1fb9f7386e
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-xhdpi-v11/toolbar_favicon_default.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-xhdpi-v11/url_bar_entry_default.9.png b/mobile/android/base/resources/drawable-large-xhdpi-v11/url_bar_entry_default.9.png
new file mode 100644
index 0000000000..2c8c0d80f8
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-xhdpi-v11/url_bar_entry_default.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-xhdpi-v11/url_bar_entry_default_pb.9.png b/mobile/android/base/resources/drawable-large-xhdpi-v11/url_bar_entry_default_pb.9.png
new file mode 100644
index 0000000000..670104ed36
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-xhdpi-v11/url_bar_entry_default_pb.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-xhdpi-v11/url_bar_entry_pressed.9.png b/mobile/android/base/resources/drawable-large-xhdpi-v11/url_bar_entry_pressed.9.png
new file mode 100644
index 0000000000..26dc0221b5
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-xhdpi-v11/url_bar_entry_pressed.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-xhdpi-v11/url_bar_entry_pressed_pb.9.png b/mobile/android/base/resources/drawable-large-xhdpi-v11/url_bar_entry_pressed_pb.9.png
new file mode 100644
index 0000000000..224738af61
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-xhdpi-v11/url_bar_entry_pressed_pb.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-xxhdpi-v11/ic_menu_back.png b/mobile/android/base/resources/drawable-large-xxhdpi-v11/ic_menu_back.png
new file mode 100644
index 0000000000..33b45b31d0
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-xxhdpi-v11/ic_menu_back.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-xxhdpi-v11/ic_menu_forward.png b/mobile/android/base/resources/drawable-large-xxhdpi-v11/ic_menu_forward.png
new file mode 100644
index 0000000000..ac5166cd82
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-xxhdpi-v11/ic_menu_forward.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-xxhdpi-v11/ic_menu_reload.png b/mobile/android/base/resources/drawable-large-xxhdpi-v11/ic_menu_reload.png
new file mode 100644
index 0000000000..d582128a17
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-xxhdpi-v11/ic_menu_reload.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-xxhdpi-v11/tabs_count.png b/mobile/android/base/resources/drawable-large-xxhdpi-v11/tabs_count.png
new file mode 100644
index 0000000000..af9ee4d3ad
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-xxhdpi-v11/tabs_count.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-xxhdpi-v11/tabs_count_foreground.png b/mobile/android/base/resources/drawable-large-xxhdpi-v11/tabs_count_foreground.png
new file mode 100644
index 0000000000..fa05120352
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-xxhdpi-v11/tabs_count_foreground.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-xxhdpi-v11/toolbar_favicon_default.png b/mobile/android/base/resources/drawable-large-xxhdpi-v11/toolbar_favicon_default.png
new file mode 100644
index 0000000000..bbccd51ef4
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-xxhdpi-v11/toolbar_favicon_default.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-xxhdpi-v11/url_bar_entry_default.9.png b/mobile/android/base/resources/drawable-large-xxhdpi-v11/url_bar_entry_default.9.png
new file mode 100644
index 0000000000..1099a2d807
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-xxhdpi-v11/url_bar_entry_default.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-xxhdpi-v11/url_bar_entry_default_pb.9.png b/mobile/android/base/resources/drawable-large-xxhdpi-v11/url_bar_entry_default_pb.9.png
new file mode 100644
index 0000000000..3aba71baea
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-xxhdpi-v11/url_bar_entry_default_pb.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-xxhdpi-v11/url_bar_entry_pressed.9.png b/mobile/android/base/resources/drawable-large-xxhdpi-v11/url_bar_entry_pressed.9.png
new file mode 100644
index 0000000000..779f2721f5
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-xxhdpi-v11/url_bar_entry_pressed.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-large-xxhdpi-v11/url_bar_entry_pressed_pb.9.png b/mobile/android/base/resources/drawable-large-xxhdpi-v11/url_bar_entry_pressed_pb.9.png
new file mode 100644
index 0000000000..e15f58ab26
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-xxhdpi-v11/url_bar_entry_pressed_pb.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-nodpi/cloud.png b/mobile/android/base/resources/drawable-nodpi/cloud.png
new file mode 100644
index 0000000000..22261d8127
--- /dev/null
+++ b/mobile/android/base/resources/drawable-nodpi/cloud.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-nodpi/firstrun_account.png b/mobile/android/base/resources/drawable-nodpi/firstrun_account.png
new file mode 100644
index 0000000000..38c77afaaf
--- /dev/null
+++ b/mobile/android/base/resources/drawable-nodpi/firstrun_account.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-nodpi/firstrun_bookmarks.png b/mobile/android/base/resources/drawable-nodpi/firstrun_bookmarks.png
new file mode 100644
index 0000000000..28dd68f520
--- /dev/null
+++ b/mobile/android/base/resources/drawable-nodpi/firstrun_bookmarks.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-nodpi/firstrun_data_off.png b/mobile/android/base/resources/drawable-nodpi/firstrun_data_off.png
new file mode 100644
index 0000000000..5985b61c52
--- /dev/null
+++ b/mobile/android/base/resources/drawable-nodpi/firstrun_data_off.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-nodpi/firstrun_data_on.png b/mobile/android/base/resources/drawable-nodpi/firstrun_data_on.png
new file mode 100644
index 0000000000..3dc037a48a
--- /dev/null
+++ b/mobile/android/base/resources/drawable-nodpi/firstrun_data_on.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-nodpi/firstrun_readerview.png b/mobile/android/base/resources/drawable-nodpi/firstrun_readerview.png
new file mode 100644
index 0000000000..c611f08c9b
--- /dev/null
+++ b/mobile/android/base/resources/drawable-nodpi/firstrun_readerview.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-nodpi/firstrun_signin.png b/mobile/android/base/resources/drawable-nodpi/firstrun_signin.png
new file mode 100644
index 0000000000..38d1c8c862
--- /dev/null
+++ b/mobile/android/base/resources/drawable-nodpi/firstrun_signin.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-nodpi/firstrun_sync.png b/mobile/android/base/resources/drawable-nodpi/firstrun_sync.png
new file mode 100644
index 0000000000..9f2f3cbc83
--- /dev/null
+++ b/mobile/android/base/resources/drawable-nodpi/firstrun_sync.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-nodpi/firstrun_tabqueue_off.png b/mobile/android/base/resources/drawable-nodpi/firstrun_tabqueue_off.png
new file mode 100644
index 0000000000..9e986685ad
--- /dev/null
+++ b/mobile/android/base/resources/drawable-nodpi/firstrun_tabqueue_off.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-nodpi/firstrun_tabqueue_on.png b/mobile/android/base/resources/drawable-nodpi/firstrun_tabqueue_on.png
new file mode 100644
index 0000000000..5439f7a5c8
--- /dev/null
+++ b/mobile/android/base/resources/drawable-nodpi/firstrun_tabqueue_on.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-nodpi/firstrun_urlbar.png b/mobile/android/base/resources/drawable-nodpi/firstrun_urlbar.png
new file mode 100644
index 0000000000..779cdd9729
--- /dev/null
+++ b/mobile/android/base/resources/drawable-nodpi/firstrun_urlbar.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-nodpi/icon_recent.png b/mobile/android/base/resources/drawable-nodpi/icon_recent.png
new file mode 100755
index 0000000000..d93658f089
--- /dev/null
+++ b/mobile/android/base/resources/drawable-nodpi/icon_recent.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-v12/toast_button_background.xml b/mobile/android/base/resources/drawable-v12/toast_button_background.xml
new file mode 100644
index 0000000000..856d070c2b
--- /dev/null
+++ b/mobile/android/base/resources/drawable-v12/toast_button_background.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/toast_button_pressed" />
+ <corners
+ android:topRightRadius="@dimen/toast_button_corner_radius"
+ android:bottomRightRadius="@dimen/toast_button_corner_radius"
+ android:topLeftRadius="0dp"
+ android:bottomLeftRadius="0dp" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/toast_button_background" />
+ <corners
+ android:topRightRadius="@dimen/toast_button_corner_radius"
+ android:bottomRightRadius="@dimen/toast_button_corner_radius"
+ android:topLeftRadius="0dp"
+ android:bottomLeftRadius="0dp" />
+ </shape>
+ </item>
+</selector>
diff --git a/mobile/android/base/resources/drawable-v21/logo.xml b/mobile/android/base/resources/drawable-v21/logo.xml
new file mode 100644
index 0000000000..568cbec00e
--- /dev/null
+++ b/mobile/android/base/resources/drawable-v21/logo.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- The action bar scales the application icon to be too large (bug 1132751)
+ so add some padding to prevent it from scaling so much. -->
+<inset
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:drawable="@drawable/icon"
+ android:insetTop="6dp"
+ android:insetBottom="6dp"
+ android:insetLeft="6dp"
+ android:insetRight="6dp"
+ />
diff --git a/mobile/android/base/resources/drawable-xhdpi-v11/alert_camera.png b/mobile/android/base/resources/drawable-xhdpi-v11/alert_camera.png
new file mode 100644
index 0000000000..ead824430f
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi-v11/alert_camera.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi-v11/alert_download.png b/mobile/android/base/resources/drawable-xhdpi-v11/alert_download.png
new file mode 100644
index 0000000000..1b8f59e567
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi-v11/alert_download.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi-v11/alert_guest.png b/mobile/android/base/resources/drawable-xhdpi-v11/alert_guest.png
new file mode 100644
index 0000000000..1a17f03beb
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi-v11/alert_guest.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi-v11/alert_mic.png b/mobile/android/base/resources/drawable-xhdpi-v11/alert_mic.png
new file mode 100644
index 0000000000..79489919e7
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi-v11/alert_mic.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi-v11/alert_mic_camera.png b/mobile/android/base/resources/drawable-xhdpi-v11/alert_mic_camera.png
new file mode 100644
index 0000000000..26ba6520b9
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi-v11/alert_mic_camera.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_back.png b/mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_back.png
new file mode 100644
index 0000000000..c26cdd7534
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_back.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_bookmark_add.png b/mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_bookmark_add.png
new file mode 100644
index 0000000000..8084baf984
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_bookmark_add.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_forward.png b/mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_forward.png
new file mode 100644
index 0000000000..2d6ba52ac7
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_forward.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_reload.png b/mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_reload.png
new file mode 100644
index 0000000000..d456630ee9
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_reload.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi-v11/ic_status_logo.png b/mobile/android/base/resources/drawable-xhdpi-v11/ic_status_logo.png
new file mode 100644
index 0000000000..c9045fd0ea
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi-v11/ic_status_logo.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi-v11/star_blue.png b/mobile/android/base/resources/drawable-xhdpi-v11/star_blue.png
new file mode 100644
index 0000000000..56c9367c07
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi-v11/star_blue.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/ab_add_search_engine.png b/mobile/android/base/resources/drawable-xhdpi/ab_add_search_engine.png
new file mode 100644
index 0000000000..2ba5dc0b74
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/ab_add_search_engine.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/ab_copy.png b/mobile/android/base/resources/drawable-xhdpi/ab_copy.png
new file mode 100644
index 0000000000..131b4bb1b7
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/ab_copy.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/ab_cut.png b/mobile/android/base/resources/drawable-xhdpi/ab_cut.png
new file mode 100644
index 0000000000..0805bbbefe
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/ab_cut.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/ab_done.png b/mobile/android/base/resources/drawable-xhdpi/ab_done.png
new file mode 100644
index 0000000000..0639b034eb
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/ab_done.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/ab_mic.png b/mobile/android/base/resources/drawable-xhdpi/ab_mic.png
new file mode 100644
index 0000000000..79c459bdbc
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/ab_mic.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/ab_paste.png b/mobile/android/base/resources/drawable-xhdpi/ab_paste.png
new file mode 100644
index 0000000000..744320f16a
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/ab_paste.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/ab_qrcode.png b/mobile/android/base/resources/drawable-xhdpi/ab_qrcode.png
new file mode 100644
index 0000000000..d0d4c3a0ca
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/ab_qrcode.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/ab_search.png b/mobile/android/base/resources/drawable-xhdpi/ab_search.png
new file mode 100644
index 0000000000..67063dd6c7
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/ab_search.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/ab_select_all.png b/mobile/android/base/resources/drawable-xhdpi/ab_select_all.png
new file mode 100644
index 0000000000..028299a83f
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/ab_select_all.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/alert_camera.png b/mobile/android/base/resources/drawable-xhdpi/alert_camera.png
new file mode 100644
index 0000000000..bfc736c552
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/alert_camera.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/alert_download.png b/mobile/android/base/resources/drawable-xhdpi/alert_download.png
new file mode 100644
index 0000000000..283ba08673
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/alert_download.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/alert_download_animation_1.png b/mobile/android/base/resources/drawable-xhdpi/alert_download_animation_1.png
new file mode 100644
index 0000000000..22d6288ea4
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/alert_download_animation_1.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/alert_download_animation_2.png b/mobile/android/base/resources/drawable-xhdpi/alert_download_animation_2.png
new file mode 100644
index 0000000000..dc402e9b8b
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/alert_download_animation_2.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/alert_download_animation_3.png b/mobile/android/base/resources/drawable-xhdpi/alert_download_animation_3.png
new file mode 100644
index 0000000000..719bcbdabf
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/alert_download_animation_3.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/alert_download_animation_4.png b/mobile/android/base/resources/drawable-xhdpi/alert_download_animation_4.png
new file mode 100644
index 0000000000..4319d0039c
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/alert_download_animation_4.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/alert_download_animation_5.png b/mobile/android/base/resources/drawable-xhdpi/alert_download_animation_5.png
new file mode 100644
index 0000000000..f8d1a15d6c
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/alert_download_animation_5.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/alert_download_animation_6.png b/mobile/android/base/resources/drawable-xhdpi/alert_download_animation_6.png
new file mode 100644
index 0000000000..dffe363386
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/alert_download_animation_6.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/alert_guest.png b/mobile/android/base/resources/drawable-xhdpi/alert_guest.png
new file mode 100644
index 0000000000..f48ae407cb
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/alert_guest.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/alert_mic.png b/mobile/android/base/resources/drawable-xhdpi/alert_mic.png
new file mode 100644
index 0000000000..527d42c425
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/alert_mic.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/alert_mic_camera.png b/mobile/android/base/resources/drawable-xhdpi/alert_mic_camera.png
new file mode 100644
index 0000000000..1e2c29861e
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/alert_mic_camera.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/arrow_up.png b/mobile/android/base/resources/drawable-xhdpi/arrow_up.png
new file mode 100644
index 0000000000..ea344e7a47
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/arrow_up.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/blank.png b/mobile/android/base/resources/drawable-xhdpi/blank.png
new file mode 100644
index 0000000000..c6efe7e8ef
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/blank.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/casting.png b/mobile/android/base/resources/drawable-xhdpi/casting.png
new file mode 100644
index 0000000000..a208402349
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/casting.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/casting_active.png b/mobile/android/base/resources/drawable-xhdpi/casting_active.png
new file mode 100644
index 0000000000..0f5d93c730
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/casting_active.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/close.png b/mobile/android/base/resources/drawable-xhdpi/close.png
new file mode 100644
index 0000000000..e23ea4666b
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/close.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/close_edit_mode_dark.png b/mobile/android/base/resources/drawable-xhdpi/close_edit_mode_dark.png
new file mode 100644
index 0000000000..93bee42daf
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/close_edit_mode_dark.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/close_edit_mode_light.png b/mobile/android/base/resources/drawable-xhdpi/close_edit_mode_light.png
new file mode 100644
index 0000000000..11e9d19ce3
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/close_edit_mode_light.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/color_picker_row_bg.9.png b/mobile/android/base/resources/drawable-xhdpi/color_picker_row_bg.9.png
new file mode 100644
index 0000000000..106c4c0b74
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/color_picker_row_bg.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/device_desktop.png b/mobile/android/base/resources/drawable-xhdpi/device_desktop.png
new file mode 100644
index 0000000000..5b0abd02d4
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/device_desktop.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/device_mobile.png b/mobile/android/base/resources/drawable-xhdpi/device_mobile.png
new file mode 100644
index 0000000000..ee665d2b6c
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/device_mobile.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/dropshadow.9.png b/mobile/android/base/resources/drawable-xhdpi/dropshadow.9.png
new file mode 100644
index 0000000000..5f346ab705
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/dropshadow.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/favicon_globe.png b/mobile/android/base/resources/drawable-xhdpi/favicon_globe.png
new file mode 100644
index 0000000000..e4d5949117
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/favicon_globe.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/find_close.png b/mobile/android/base/resources/drawable-xhdpi/find_close.png
new file mode 100644
index 0000000000..a76c69e097
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/find_close.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/find_next.png b/mobile/android/base/resources/drawable-xhdpi/find_next.png
new file mode 100644
index 0000000000..3a7dda2d95
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/find_next.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/find_prev.png b/mobile/android/base/resources/drawable-xhdpi/find_prev.png
new file mode 100644
index 0000000000..9e3c8f2da2
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/find_prev.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/firefox_settings_alert.png b/mobile/android/base/resources/drawable-xhdpi/firefox_settings_alert.png
new file mode 100644
index 0000000000..b62d67c887
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/firefox_settings_alert.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/flat_icon.png b/mobile/android/base/resources/drawable-xhdpi/flat_icon.png
new file mode 100644
index 0000000000..bc9569f471
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/flat_icon.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/folder_closed.png b/mobile/android/base/resources/drawable-xhdpi/folder_closed.png
new file mode 100644
index 0000000000..63bb529a3d
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/folder_closed.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/globe_light.png b/mobile/android/base/resources/drawable-xhdpi/globe_light.png
new file mode 100644
index 0000000000..1bb177089f
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/globe_light.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/grid_icon_bg_activated.9.png b/mobile/android/base/resources/drawable-xhdpi/grid_icon_bg_activated.9.png
new file mode 100644
index 0000000000..ef25e20765
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/grid_icon_bg_activated.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/grid_icon_bg_focused.9.png b/mobile/android/base/resources/drawable-xhdpi/grid_icon_bg_focused.9.png
new file mode 100644
index 0000000000..27660f0d3d
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/grid_icon_bg_focused.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/handle_end.png b/mobile/android/base/resources/drawable-xhdpi/handle_end.png
new file mode 100644
index 0000000000..a637c37469
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/handle_end.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/handle_middle.png b/mobile/android/base/resources/drawable-xhdpi/handle_middle.png
new file mode 100644
index 0000000000..95b85bc232
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/handle_middle.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/handle_start.png b/mobile/android/base/resources/drawable-xhdpi/handle_start.png
new file mode 100644
index 0000000000..05532b2393
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/handle_start.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/helper_readerview_bookmark.webp b/mobile/android/base/resources/drawable-xhdpi/helper_readerview_bookmark.webp
new file mode 100644
index 0000000000..c7ef9cc7df
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/helper_readerview_bookmark.webp
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/home_group_collapsed.png b/mobile/android/base/resources/drawable-xhdpi/home_group_collapsed.png
new file mode 100644
index 0000000000..80aea0ddcf
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/home_group_collapsed.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/home_tab_menu_strip.9.png b/mobile/android/base/resources/drawable-xhdpi/home_tab_menu_strip.9.png
new file mode 100644
index 0000000000..b9847a1a3f
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/home_tab_menu_strip.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/homepage_banner_firstrun.png b/mobile/android/base/resources/drawable-xhdpi/homepage_banner_firstrun.png
new file mode 100644
index 0000000000..b53bcb9441
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/homepage_banner_firstrun.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/ic_action_settings.png b/mobile/android/base/resources/drawable-xhdpi/ic_action_settings.png
new file mode 100644
index 0000000000..c76a98b640
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/ic_action_settings.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/ic_media_pause.png b/mobile/android/base/resources/drawable-xhdpi/ic_media_pause.png
new file mode 100644
index 0000000000..6174480ea1
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/ic_media_pause.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/ic_media_play.png b/mobile/android/base/resources/drawable-xhdpi/ic_media_play.png
new file mode 100644
index 0000000000..601f4293b0
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/ic_media_play.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/ic_menu_share.png b/mobile/android/base/resources/drawable-xhdpi/ic_menu_share.png
new file mode 100644
index 0000000000..31cca8e157
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/ic_menu_share.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/ic_status_logo.png b/mobile/android/base/resources/drawable-xhdpi/ic_status_logo.png
new file mode 100644
index 0000000000..915d7510a4
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/ic_status_logo.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/ic_url_bar_tab.png b/mobile/android/base/resources/drawable-xhdpi/ic_url_bar_tab.png
new file mode 100644
index 0000000000..be2b74138c
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/ic_url_bar_tab.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/ic_widget_new_tab.png b/mobile/android/base/resources/drawable-xhdpi/ic_widget_new_tab.png
new file mode 100644
index 0000000000..8a5cbae011
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/ic_widget_new_tab.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/ic_widget_search.png b/mobile/android/base/resources/drawable-xhdpi/ic_widget_search.png
new file mode 100644
index 0000000000..e846f008d5
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/ic_widget_search.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/icon_bookmarks_empty.png b/mobile/android/base/resources/drawable-xhdpi/icon_bookmarks_empty.png
new file mode 100644
index 0000000000..168e76e921
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/icon_bookmarks_empty.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/icon_home_empty_firefox.png b/mobile/android/base/resources/drawable-xhdpi/icon_home_empty_firefox.png
new file mode 100644
index 0000000000..92b9660714
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/icon_home_empty_firefox.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/icon_key.png b/mobile/android/base/resources/drawable-xhdpi/icon_key.png
new file mode 100644
index 0000000000..747733125e
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/icon_key.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/icon_most_recent_empty.png b/mobile/android/base/resources/drawable-xhdpi/icon_most_recent_empty.png
new file mode 100644
index 0000000000..710fded09e
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/icon_most_recent_empty.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/icon_openinapp.png b/mobile/android/base/resources/drawable-xhdpi/icon_openinapp.png
new file mode 100644
index 0000000000..4caa44ea88
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/icon_openinapp.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/icon_pageaction.png b/mobile/android/base/resources/drawable-xhdpi/icon_pageaction.png
new file mode 100644
index 0000000000..2dd6b052b5
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/icon_pageaction.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/icon_remote_tabs_empty.png b/mobile/android/base/resources/drawable-xhdpi/icon_remote_tabs_empty.png
new file mode 100644
index 0000000000..04c0f0d738
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/icon_remote_tabs_empty.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/icon_search_empty_firefox.png b/mobile/android/base/resources/drawable-xhdpi/icon_search_empty_firefox.png
new file mode 100644
index 0000000000..8332271d8e
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/icon_search_empty_firefox.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/icon_shareplane.png b/mobile/android/base/resources/drawable-xhdpi/icon_shareplane.png
new file mode 100644
index 0000000000..f2dfe2d5ab
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/icon_shareplane.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/img_check.png b/mobile/android/base/resources/drawable-xhdpi/img_check.png
new file mode 100644
index 0000000000..986143b243
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/img_check.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/location.png b/mobile/android/base/resources/drawable-xhdpi/location.png
new file mode 100644
index 0000000000..8a6b546bd0
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/location.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/lock_disabled.png b/mobile/android/base/resources/drawable-xhdpi/lock_disabled.png
new file mode 100644
index 0000000000..9a879161b8
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/lock_disabled.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/lock_inactive.png b/mobile/android/base/resources/drawable-xhdpi/lock_inactive.png
new file mode 100644
index 0000000000..84611eb42b
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/lock_inactive.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/lock_secure.png b/mobile/android/base/resources/drawable-xhdpi/lock_secure.png
new file mode 100644
index 0000000000..75a43cd30f
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/lock_secure.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/media_bar_pause.png b/mobile/android/base/resources/drawable-xhdpi/media_bar_pause.png
new file mode 100644
index 0000000000..e19fbd864d
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/media_bar_pause.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/media_bar_play.png b/mobile/android/base/resources/drawable-xhdpi/media_bar_play.png
new file mode 100644
index 0000000000..6e1b7d2141
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/media_bar_play.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/media_bar_stop.png b/mobile/android/base/resources/drawable-xhdpi/media_bar_stop.png
new file mode 100644
index 0000000000..6b63c3ea30
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/media_bar_stop.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/menu.png b/mobile/android/base/resources/drawable-xhdpi/menu.png
new file mode 100644
index 0000000000..01af3bb042
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/menu.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/menu_item_check.png b/mobile/android/base/resources/drawable-xhdpi/menu_item_check.png
new file mode 100644
index 0000000000..9943ead84c
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/menu_item_check.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/menu_item_more.png b/mobile/android/base/resources/drawable-xhdpi/menu_item_more.png
new file mode 100644
index 0000000000..e6f71ec5bc
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/menu_item_more.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/menu_item_uncheck.png b/mobile/android/base/resources/drawable-xhdpi/menu_item_uncheck.png
new file mode 100644
index 0000000000..01c610fbfb
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/menu_item_uncheck.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/network_error.png b/mobile/android/base/resources/drawable-xhdpi/network_error.png
new file mode 100644
index 0000000000..c5613865cd
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/network_error.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/notification_media.webp b/mobile/android/base/resources/drawable-xhdpi/notification_media.webp
new file mode 100644
index 0000000000..460fe7a12c
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/notification_media.webp
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/open_in_browser.png b/mobile/android/base/resources/drawable-xhdpi/open_in_browser.png
new file mode 100644
index 0000000000..c03c0207b1
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/open_in_browser.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/overlay_bookmark_icon.png b/mobile/android/base/resources/drawable-xhdpi/overlay_bookmark_icon.png
new file mode 100644
index 0000000000..db438939c1
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/overlay_bookmark_icon.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/overlay_bookmarked_already_icon.png b/mobile/android/base/resources/drawable-xhdpi/overlay_bookmarked_already_icon.png
new file mode 100644
index 0000000000..5995d2eb1f
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/overlay_bookmarked_already_icon.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/overlay_check.png b/mobile/android/base/resources/drawable-xhdpi/overlay_check.png
new file mode 100644
index 0000000000..db17eda8dd
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/overlay_check.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/pause.png b/mobile/android/base/resources/drawable-xhdpi/pause.png
new file mode 100644
index 0000000000..6aae6c5eb7
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/pause.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/phone.png b/mobile/android/base/resources/drawable-xhdpi/phone.png
new file mode 100644
index 0000000000..19308f870a
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/phone.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/pin.png b/mobile/android/base/resources/drawable-xhdpi/pin.png
new file mode 100644
index 0000000000..644e0043cc
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/pin.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/play.png b/mobile/android/base/resources/drawable-xhdpi/play.png
new file mode 100644
index 0000000000..cc5c7b9e45
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/play.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/private_masq.png b/mobile/android/base/resources/drawable-xhdpi/private_masq.png
new file mode 100644
index 0000000000..5ea5f5beb9
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/private_masq.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/progress.9.png b/mobile/android/base/resources/drawable-xhdpi/progress.9.png
new file mode 100644
index 0000000000..8682ea26ef
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/progress.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/push_notification.png b/mobile/android/base/resources/drawable-xhdpi/push_notification.png
new file mode 100644
index 0000000000..77e0be6891
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/push_notification.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/reader.png b/mobile/android/base/resources/drawable-xhdpi/reader.png
new file mode 100644
index 0000000000..b202314d40
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/reader.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/reader_active.png b/mobile/android/base/resources/drawable-xhdpi/reader_active.png
new file mode 100644
index 0000000000..8a9872be1e
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/reader_active.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/reading_list_folder.png b/mobile/android/base/resources/drawable-xhdpi/reading_list_folder.png
new file mode 100644
index 0000000000..c7ad1cade1
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/reading_list_folder.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/search_clear.png b/mobile/android/base/resources/drawable-xhdpi/search_clear.png
new file mode 100644
index 0000000000..b0881ed32a
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/search_clear.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/search_history.png b/mobile/android/base/resources/drawable-xhdpi/search_history.png
new file mode 100644
index 0000000000..ed661a729e
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/search_history.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/search_icon_active.png b/mobile/android/base/resources/drawable-xhdpi/search_icon_active.png
new file mode 100644
index 0000000000..baef8bea87
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/search_icon_active.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/search_icon_inactive.png b/mobile/android/base/resources/drawable-xhdpi/search_icon_inactive.png
new file mode 100644
index 0000000000..d3f73d7e2c
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/search_icon_inactive.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/search_launcher.png b/mobile/android/base/resources/drawable-xhdpi/search_launcher.png
new file mode 100644
index 0000000000..be0fd65fb4
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/search_launcher.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/search_plus.png b/mobile/android/base/resources/drawable-xhdpi/search_plus.png
new file mode 100644
index 0000000000..de3b631cf7
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/search_plus.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/settings_notifications.png b/mobile/android/base/resources/drawable-xhdpi/settings_notifications.png
new file mode 100644
index 0000000000..3e96f6deb5
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/settings_notifications.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/shareplane.png b/mobile/android/base/resources/drawable-xhdpi/shareplane.png
new file mode 100644
index 0000000000..44e6878aaf
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/shareplane.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/shield_disabled.png b/mobile/android/base/resources/drawable-xhdpi/shield_disabled.png
new file mode 100644
index 0000000000..20e58daf78
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/shield_disabled.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/shield_enabled.png b/mobile/android/base/resources/drawable-xhdpi/shield_enabled.png
new file mode 100644
index 0000000000..c95a4876e1
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/shield_enabled.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/status_icon_readercache.png b/mobile/android/base/resources/drawable-xhdpi/status_icon_readercache.png
new file mode 100644
index 0000000000..e5211c71be
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/status_icon_readercache.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/suggestedsites_amazon.png b/mobile/android/base/resources/drawable-xhdpi/suggestedsites_amazon.png
new file mode 100644
index 0000000000..012060dd9a
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/suggestedsites_amazon.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/suggestedsites_facebook.png b/mobile/android/base/resources/drawable-xhdpi/suggestedsites_facebook.png
new file mode 100644
index 0000000000..b97f9cba90
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/suggestedsites_facebook.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/suggestedsites_restricted_fxsupport.png b/mobile/android/base/resources/drawable-xhdpi/suggestedsites_restricted_fxsupport.png
new file mode 100644
index 0000000000..25338e447d
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/suggestedsites_restricted_fxsupport.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/suggestedsites_restricted_mozilla.png b/mobile/android/base/resources/drawable-xhdpi/suggestedsites_restricted_mozilla.png
new file mode 100644
index 0000000000..03a2e9491c
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/suggestedsites_restricted_mozilla.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/suggestedsites_twitter.png b/mobile/android/base/resources/drawable-xhdpi/suggestedsites_twitter.png
new file mode 100644
index 0000000000..75bc311c07
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/suggestedsites_twitter.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/suggestedsites_webmaker.png b/mobile/android/base/resources/drawable-xhdpi/suggestedsites_webmaker.png
new file mode 100644
index 0000000000..92f7cfecf7
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/suggestedsites_webmaker.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/suggestedsites_wikipedia.png b/mobile/android/base/resources/drawable-xhdpi/suggestedsites_wikipedia.png
new file mode 100644
index 0000000000..76140d48b2
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/suggestedsites_wikipedia.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/suggestedsites_youtube.png b/mobile/android/base/resources/drawable-xhdpi/suggestedsites_youtube.png
new file mode 100644
index 0000000000..2896a9c17a
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/suggestedsites_youtube.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/switch_button_icon.png b/mobile/android/base/resources/drawable-xhdpi/switch_button_icon.png
new file mode 100644
index 0000000000..6dfa79e518
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/switch_button_icon.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/tab_audio_playing.png b/mobile/android/base/resources/drawable-xhdpi/tab_audio_playing.png
new file mode 100644
index 0000000000..cb4466e0e2
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/tab_audio_playing.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/tab_close.png b/mobile/android/base/resources/drawable-xhdpi/tab_close.png
new file mode 100644
index 0000000000..8e4908e0c5
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/tab_close.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/tab_close_active.png b/mobile/android/base/resources/drawable-xhdpi/tab_close_active.png
new file mode 100644
index 0000000000..d2071e7c37
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/tab_close_active.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/tab_indicator_background.9.png b/mobile/android/base/resources/drawable-xhdpi/tab_indicator_background.9.png
new file mode 100644
index 0000000000..8b561749af
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/tab_indicator_background.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/tab_indicator_divider.9.png b/mobile/android/base/resources/drawable-xhdpi/tab_indicator_divider.9.png
new file mode 100644
index 0000000000..f1338d8031
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/tab_indicator_divider.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/tab_indicator_selected.9.png b/mobile/android/base/resources/drawable-xhdpi/tab_indicator_selected.9.png
new file mode 100644
index 0000000000..e78a2ddba6
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/tab_indicator_selected.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/tab_indicator_selected_focused.9.png b/mobile/android/base/resources/drawable-xhdpi/tab_indicator_selected_focused.9.png
new file mode 100644
index 0000000000..3e1fe5560f
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/tab_indicator_selected_focused.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/tab_new.png b/mobile/android/base/resources/drawable-xhdpi/tab_new.png
new file mode 100644
index 0000000000..76a5a11827
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/tab_new.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/tab_preview_masq.png b/mobile/android/base/resources/drawable-xhdpi/tab_preview_masq.png
new file mode 100644
index 0000000000..119542216f
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/tab_preview_masq.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/tabs_count.png b/mobile/android/base/resources/drawable-xhdpi/tabs_count.png
new file mode 100644
index 0000000000..e4c71cb140
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/tabs_count.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/tabs_count_foreground.png b/mobile/android/base/resources/drawable-xhdpi/tabs_count_foreground.png
new file mode 100644
index 0000000000..a5a75227ca
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/tabs_count_foreground.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/tabs_normal.png b/mobile/android/base/resources/drawable-xhdpi/tabs_normal.png
new file mode 100644
index 0000000000..f76b0221c3
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/tabs_normal.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/tabs_panel_nav_back.png b/mobile/android/base/resources/drawable-xhdpi/tabs_panel_nav_back.png
new file mode 100644
index 0000000000..79d8ae2855
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/tabs_panel_nav_back.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/tabs_private.png b/mobile/android/base/resources/drawable-xhdpi/tabs_private.png
new file mode 100644
index 0000000000..14a3f4b79e
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/tabs_private.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/tip_addsearch.png b/mobile/android/base/resources/drawable-xhdpi/tip_addsearch.png
new file mode 100644
index 0000000000..d7c18bdbfb
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/tip_addsearch.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/top_site_add.png b/mobile/android/base/resources/drawable-xhdpi/top_site_add.png
new file mode 100644
index 0000000000..7ddc503bf7
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/top_site_add.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/tracking_protection_toolbar_illustration.png b/mobile/android/base/resources/drawable-xhdpi/tracking_protection_toolbar_illustration.png
new file mode 100644
index 0000000000..744d3573ca
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/tracking_protection_toolbar_illustration.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/undo_button_icon.png b/mobile/android/base/resources/drawable-xhdpi/undo_button_icon.png
new file mode 100644
index 0000000000..5992b4f5c4
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/undo_button_icon.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/url_bar_entry_default.9.png b/mobile/android/base/resources/drawable-xhdpi/url_bar_entry_default.9.png
new file mode 100644
index 0000000000..e0cf4cf909
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/url_bar_entry_default.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/url_bar_entry_default_pb.9.png b/mobile/android/base/resources/drawable-xhdpi/url_bar_entry_default_pb.9.png
new file mode 100644
index 0000000000..ca9e114b69
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/url_bar_entry_default_pb.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/url_bar_entry_pressed.9.png b/mobile/android/base/resources/drawable-xhdpi/url_bar_entry_pressed.9.png
new file mode 100644
index 0000000000..8cbbff1902
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/url_bar_entry_pressed.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/url_bar_entry_pressed_pb.9.png b/mobile/android/base/resources/drawable-xhdpi/url_bar_entry_pressed_pb.9.png
new file mode 100644
index 0000000000..f34244e4cb
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/url_bar_entry_pressed_pb.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/urlbar_stop.png b/mobile/android/base/resources/drawable-xhdpi/urlbar_stop.png
new file mode 100644
index 0000000000..0151951599
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/urlbar_stop.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/validation_arrow.png b/mobile/android/base/resources/drawable-xhdpi/validation_arrow.png
new file mode 100644
index 0000000000..0dfea5c2fa
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/validation_arrow.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/validation_arrow_inverted.png b/mobile/android/base/resources/drawable-xhdpi/validation_arrow_inverted.png
new file mode 100644
index 0000000000..9122e2ce62
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/validation_arrow_inverted.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/validation_bg.9.png b/mobile/android/base/resources/drawable-xhdpi/validation_bg.9.png
new file mode 100644
index 0000000000..773aa03a8e
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/validation_bg.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/warning_major.png b/mobile/android/base/resources/drawable-xhdpi/warning_major.png
new file mode 100644
index 0000000000..896062e988
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/warning_major.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/warning_minor.png b/mobile/android/base/resources/drawable-xhdpi/warning_minor.png
new file mode 100644
index 0000000000..84fcff9bb8
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/warning_minor.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/widget_bg.9.png b/mobile/android/base/resources/drawable-xhdpi/widget_bg.9.png
new file mode 100644
index 0000000000..cbf377ac35
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xhdpi/widget_bg.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xlarge-hdpi-v11/ic_menu_bookmark_add.png b/mobile/android/base/resources/drawable-xlarge-hdpi-v11/ic_menu_bookmark_add.png
new file mode 100644
index 0000000000..54d88fd13a
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xlarge-hdpi-v11/ic_menu_bookmark_add.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xlarge-hdpi-v11/star_blue.png b/mobile/android/base/resources/drawable-xlarge-hdpi-v11/star_blue.png
new file mode 100644
index 0000000000..b80c5ac44b
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xlarge-hdpi-v11/star_blue.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xlarge-xhdpi-v11/ic_menu_bookmark_add.png b/mobile/android/base/resources/drawable-xlarge-xhdpi-v11/ic_menu_bookmark_add.png
new file mode 100644
index 0000000000..e54d42905b
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xlarge-xhdpi-v11/ic_menu_bookmark_add.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xlarge-xhdpi-v11/star_blue.png b/mobile/android/base/resources/drawable-xlarge-xhdpi-v11/star_blue.png
new file mode 100644
index 0000000000..c0278d5748
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xlarge-xhdpi-v11/star_blue.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xlarge-xxhdpi-v11/ic_menu_bookmark_add.png b/mobile/android/base/resources/drawable-xlarge-xxhdpi-v11/ic_menu_bookmark_add.png
new file mode 100644
index 0000000000..0fa0711374
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xlarge-xxhdpi-v11/ic_menu_bookmark_add.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xlarge-xxhdpi-v11/star_blue.png b/mobile/android/base/resources/drawable-xlarge-xxhdpi-v11/star_blue.png
new file mode 100644
index 0000000000..c9cf496220
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xlarge-xxhdpi-v11/star_blue.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi-v11/ic_status_logo.png b/mobile/android/base/resources/drawable-xxhdpi-v11/ic_status_logo.png
new file mode 100644
index 0000000000..0bb9777d74
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi-v11/ic_status_logo.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/ab_mic.png b/mobile/android/base/resources/drawable-xxhdpi/ab_mic.png
new file mode 100644
index 0000000000..14e4cff784
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/ab_mic.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/ab_qrcode.png b/mobile/android/base/resources/drawable-xxhdpi/ab_qrcode.png
new file mode 100644
index 0000000000..8ad7859749
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/ab_qrcode.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/arrow_up.png b/mobile/android/base/resources/drawable-xxhdpi/arrow_up.png
new file mode 100644
index 0000000000..193bbc3b4f
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/arrow_up.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/close_edit_mode_dark.png b/mobile/android/base/resources/drawable-xxhdpi/close_edit_mode_dark.png
new file mode 100644
index 0000000000..0f0e95debc
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/close_edit_mode_dark.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/close_edit_mode_light.png b/mobile/android/base/resources/drawable-xxhdpi/close_edit_mode_light.png
new file mode 100644
index 0000000000..5f9a2f7e5e
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/close_edit_mode_light.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/device_desktop.png b/mobile/android/base/resources/drawable-xxhdpi/device_desktop.png
new file mode 100644
index 0000000000..6a386c4e71
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/device_desktop.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/device_mobile.png b/mobile/android/base/resources/drawable-xxhdpi/device_mobile.png
new file mode 100644
index 0000000000..d32a9b3533
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/device_mobile.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/dropshadow.9.png b/mobile/android/base/resources/drawable-xxhdpi/dropshadow.9.png
new file mode 100644
index 0000000000..a408d91af5
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/dropshadow.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/exit_fullscreen.png b/mobile/android/base/resources/drawable-xxhdpi/exit_fullscreen.png
new file mode 100644
index 0000000000..2be3dbac70
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/exit_fullscreen.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/flat_icon.png b/mobile/android/base/resources/drawable-xxhdpi/flat_icon.png
new file mode 100644
index 0000000000..57d83a59e7
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/flat_icon.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/folder_closed.png b/mobile/android/base/resources/drawable-xxhdpi/folder_closed.png
new file mode 100644
index 0000000000..15d03e7e4e
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/folder_closed.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/fullscreen.png b/mobile/android/base/resources/drawable-xxhdpi/fullscreen.png
new file mode 100644
index 0000000000..2e39898be4
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/fullscreen.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/globe_light.png b/mobile/android/base/resources/drawable-xxhdpi/globe_light.png
new file mode 100644
index 0000000000..120bfd2e6a
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/globe_light.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/helper_readerview_bookmark.webp b/mobile/android/base/resources/drawable-xxhdpi/helper_readerview_bookmark.webp
new file mode 100644
index 0000000000..04fd2c54fb
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/helper_readerview_bookmark.webp
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/home_group_collapsed.png b/mobile/android/base/resources/drawable-xxhdpi/home_group_collapsed.png
new file mode 100644
index 0000000000..319ab3d50d
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/home_group_collapsed.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/homepage_banner_firstrun.png b/mobile/android/base/resources/drawable-xxhdpi/homepage_banner_firstrun.png
new file mode 100644
index 0000000000..3ef8f157d0
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/homepage_banner_firstrun.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/ic_action_settings.png b/mobile/android/base/resources/drawable-xxhdpi/ic_action_settings.png
new file mode 100644
index 0000000000..9ce42fffe3
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/ic_action_settings.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/ic_media_pause.png b/mobile/android/base/resources/drawable-xxhdpi/ic_media_pause.png
new file mode 100644
index 0000000000..29d216e707
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/ic_media_pause.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/ic_media_play.png b/mobile/android/base/resources/drawable-xxhdpi/ic_media_play.png
new file mode 100644
index 0000000000..648e6f67ad
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/ic_media_play.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/ic_menu_share.png b/mobile/android/base/resources/drawable-xxhdpi/ic_menu_share.png
new file mode 100644
index 0000000000..de6f092ee3
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/ic_menu_share.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/ic_widget_new_tab.png b/mobile/android/base/resources/drawable-xxhdpi/ic_widget_new_tab.png
new file mode 100644
index 0000000000..2ab09b8fb8
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/ic_widget_new_tab.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/ic_widget_search.png b/mobile/android/base/resources/drawable-xxhdpi/ic_widget_search.png
new file mode 100644
index 0000000000..d6ba7c8465
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/ic_widget_search.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/icon_key.png b/mobile/android/base/resources/drawable-xxhdpi/icon_key.png
new file mode 100644
index 0000000000..0f6925b04e
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/icon_key.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/icon_search_empty_firefox.png b/mobile/android/base/resources/drawable-xxhdpi/icon_search_empty_firefox.png
new file mode 100644
index 0000000000..b16c1ab79f
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/icon_search_empty_firefox.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/icon_shareplane.png b/mobile/android/base/resources/drawable-xxhdpi/icon_shareplane.png
new file mode 100644
index 0000000000..63e9f25193
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/icon_shareplane.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/img_check.png b/mobile/android/base/resources/drawable-xxhdpi/img_check.png
new file mode 100644
index 0000000000..13e9204641
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/img_check.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/location.png b/mobile/android/base/resources/drawable-xxhdpi/location.png
new file mode 100644
index 0000000000..7abc57ef86
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/location.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/lock_disabled.png b/mobile/android/base/resources/drawable-xxhdpi/lock_disabled.png
new file mode 100644
index 0000000000..0396ff06e4
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/lock_disabled.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/lock_inactive.png b/mobile/android/base/resources/drawable-xxhdpi/lock_inactive.png
new file mode 100644
index 0000000000..3276f338b2
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/lock_inactive.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/lock_secure.png b/mobile/android/base/resources/drawable-xxhdpi/lock_secure.png
new file mode 100644
index 0000000000..19e3a8fad5
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/lock_secure.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/menu.png b/mobile/android/base/resources/drawable-xxhdpi/menu.png
new file mode 100644
index 0000000000..a6b457fb14
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/menu.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/menu_item_check.png b/mobile/android/base/resources/drawable-xxhdpi/menu_item_check.png
new file mode 100644
index 0000000000..381f918568
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/menu_item_check.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/menu_item_uncheck.png b/mobile/android/base/resources/drawable-xxhdpi/menu_item_uncheck.png
new file mode 100644
index 0000000000..11be568d90
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/menu_item_uncheck.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/network_error.png b/mobile/android/base/resources/drawable-xxhdpi/network_error.png
new file mode 100644
index 0000000000..4074ac2a85
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/network_error.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/notification_media.webp b/mobile/android/base/resources/drawable-xxhdpi/notification_media.webp
new file mode 100644
index 0000000000..2485a4bf18
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/notification_media.webp
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/overlay_bookmark_icon.png b/mobile/android/base/resources/drawable-xxhdpi/overlay_bookmark_icon.png
new file mode 100644
index 0000000000..05dc926f2c
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/overlay_bookmark_icon.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/overlay_bookmarked_already_icon.png b/mobile/android/base/resources/drawable-xxhdpi/overlay_bookmarked_already_icon.png
new file mode 100644
index 0000000000..9afb290c4d
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/overlay_bookmarked_already_icon.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/overlay_check.png b/mobile/android/base/resources/drawable-xxhdpi/overlay_check.png
new file mode 100644
index 0000000000..1bc3abe8ef
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/overlay_check.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/private_masq.png b/mobile/android/base/resources/drawable-xxhdpi/private_masq.png
new file mode 100644
index 0000000000..e62cdbe137
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/private_masq.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/push_notification.png b/mobile/android/base/resources/drawable-xxhdpi/push_notification.png
new file mode 100644
index 0000000000..3c4ba474e3
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/push_notification.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/reading_list_folder.png b/mobile/android/base/resources/drawable-xxhdpi/reading_list_folder.png
new file mode 100644
index 0000000000..c29d1b9a57
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/reading_list_folder.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/search_clear.png b/mobile/android/base/resources/drawable-xxhdpi/search_clear.png
new file mode 100644
index 0000000000..f21257bea6
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/search_clear.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/search_history.png b/mobile/android/base/resources/drawable-xxhdpi/search_history.png
new file mode 100644
index 0000000000..fbcdbdbbae
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/search_history.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/search_icon_active.png b/mobile/android/base/resources/drawable-xxhdpi/search_icon_active.png
new file mode 100644
index 0000000000..093b066c99
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/search_icon_active.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/search_icon_inactive.png b/mobile/android/base/resources/drawable-xxhdpi/search_icon_inactive.png
new file mode 100644
index 0000000000..4117c6332c
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/search_icon_inactive.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/search_launcher.png b/mobile/android/base/resources/drawable-xxhdpi/search_launcher.png
new file mode 100644
index 0000000000..6c8fc76783
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/search_launcher.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/search_plus.png b/mobile/android/base/resources/drawable-xxhdpi/search_plus.png
new file mode 100644
index 0000000000..8ac7df9e98
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/search_plus.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/shareplane.png b/mobile/android/base/resources/drawable-xxhdpi/shareplane.png
new file mode 100644
index 0000000000..c9436aab80
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/shareplane.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/shield_disabled.png b/mobile/android/base/resources/drawable-xxhdpi/shield_disabled.png
new file mode 100644
index 0000000000..57d669d5a6
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/shield_disabled.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/shield_enabled.png b/mobile/android/base/resources/drawable-xxhdpi/shield_enabled.png
new file mode 100644
index 0000000000..edf20af117
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/shield_enabled.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/status_icon_readercache.png b/mobile/android/base/resources/drawable-xxhdpi/status_icon_readercache.png
new file mode 100644
index 0000000000..f6e070b029
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/status_icon_readercache.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/suggestedsites_amazon.png b/mobile/android/base/resources/drawable-xxhdpi/suggestedsites_amazon.png
new file mode 100644
index 0000000000..5b0d2fdc5f
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/suggestedsites_amazon.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/suggestedsites_facebook.png b/mobile/android/base/resources/drawable-xxhdpi/suggestedsites_facebook.png
new file mode 100644
index 0000000000..46e2db5882
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/suggestedsites_facebook.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/suggestedsites_twitter.png b/mobile/android/base/resources/drawable-xxhdpi/suggestedsites_twitter.png
new file mode 100644
index 0000000000..781b7b5a11
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/suggestedsites_twitter.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/suggestedsites_wikipedia.png b/mobile/android/base/resources/drawable-xxhdpi/suggestedsites_wikipedia.png
new file mode 100644
index 0000000000..a324694e91
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/suggestedsites_wikipedia.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/suggestedsites_youtube.png b/mobile/android/base/resources/drawable-xxhdpi/suggestedsites_youtube.png
new file mode 100644
index 0000000000..d201b62210
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/suggestedsites_youtube.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/tab_close.png b/mobile/android/base/resources/drawable-xxhdpi/tab_close.png
new file mode 100644
index 0000000000..400319394d
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/tab_close.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/tab_close_active.png b/mobile/android/base/resources/drawable-xxhdpi/tab_close_active.png
new file mode 100644
index 0000000000..279135f939
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/tab_close_active.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/tab_new.png b/mobile/android/base/resources/drawable-xxhdpi/tab_new.png
new file mode 100644
index 0000000000..e857037c62
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/tab_new.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/tab_preview_masq.png b/mobile/android/base/resources/drawable-xxhdpi/tab_preview_masq.png
new file mode 100644
index 0000000000..9b24f329fd
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/tab_preview_masq.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/tabs_panel_nav_back.png b/mobile/android/base/resources/drawable-xxhdpi/tabs_panel_nav_back.png
new file mode 100644
index 0000000000..3b21f3aa2a
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/tabs_panel_nav_back.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/tracking_protection_toolbar_illustration.png b/mobile/android/base/resources/drawable-xxhdpi/tracking_protection_toolbar_illustration.png
new file mode 100644
index 0000000000..2c86b5baae
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/tracking_protection_toolbar_illustration.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/url_bar_entry_default.9.png b/mobile/android/base/resources/drawable-xxhdpi/url_bar_entry_default.9.png
new file mode 100644
index 0000000000..e7b58136c7
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/url_bar_entry_default.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/url_bar_entry_default_pb.9.png b/mobile/android/base/resources/drawable-xxhdpi/url_bar_entry_default_pb.9.png
new file mode 100644
index 0000000000..b5b5a8d328
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/url_bar_entry_default_pb.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/url_bar_entry_pressed.9.png b/mobile/android/base/resources/drawable-xxhdpi/url_bar_entry_pressed.9.png
new file mode 100644
index 0000000000..7c2ac33cb5
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/url_bar_entry_pressed.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/url_bar_entry_pressed_pb.9.png b/mobile/android/base/resources/drawable-xxhdpi/url_bar_entry_pressed_pb.9.png
new file mode 100644
index 0000000000..5eb9cecb41
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/url_bar_entry_pressed_pb.9.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/urlbar_stop.png b/mobile/android/base/resources/drawable-xxhdpi/urlbar_stop.png
new file mode 100644
index 0000000000..510cd7b3c3
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/urlbar_stop.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/warning_major.png b/mobile/android/base/resources/drawable-xxhdpi/warning_major.png
new file mode 100644
index 0000000000..172160fb53
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/warning_major.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxhdpi/warning_minor.png b/mobile/android/base/resources/drawable-xxhdpi/warning_minor.png
new file mode 100644
index 0000000000..e93ead0e8b
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxhdpi/warning_minor.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable-xxxhdpi/search_launcher.png b/mobile/android/base/resources/drawable-xxxhdpi/search_launcher.png
new file mode 100644
index 0000000000..1f70d13dbe
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xxxhdpi/search_launcher.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable/action_bar_button.xml b/mobile/android/base/resources/drawable/action_bar_button.xml
new file mode 100644
index 0000000000..fe36bc43d1
--- /dev/null
+++ b/mobile/android/base/resources/drawable/action_bar_button.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_pressed="true"
+ android:state_enabled="true">
+ <shape>
+ <solid android:color="@color/highlight" />
+ </shape>
+ </item>
+
+ <item android:state_focused="true"
+ android:state_pressed="false">
+ <shape>
+ <solid android:color="@color/highlight_focused" />
+ </shape>
+ </item>
+
+ <item android:drawable="@android:color/transparent"/>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/action_bar_button_inverse.xml b/mobile/android/base/resources/drawable/action_bar_button_inverse.xml
new file mode 100644
index 0000000000..b853873312
--- /dev/null
+++ b/mobile/android/base/resources/drawable/action_bar_button_inverse.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_pressed="true">
+ <shape>
+ <solid android:color="@color/highlight_dark" />
+ </shape>
+ </item>
+
+ <item android:state_focused="true"
+ android:state_pressed="false">
+ <shape>
+ <solid android:color="@color/highlight_dark_focused" />
+ </shape>
+ </item>
+
+ <item android:drawable="@android:color/transparent"/>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/action_bar_button_negative.xml b/mobile/android/base/resources/drawable/action_bar_button_negative.xml
new file mode 100644
index 0000000000..7611d70ba5
--- /dev/null
+++ b/mobile/android/base/resources/drawable/action_bar_button_negative.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_pressed="false">
+ <shape>
+ <solid android:color="@color/toolbar_menu_dark_grey" />
+ </shape>
+ </item>
+
+ <item android:state_pressed="true">
+ <shape>
+ <solid android:color="@color/toolbar_grey_pressed" />
+ </shape>
+ </item>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/action_bar_button_positive.xml b/mobile/android/base/resources/drawable/action_bar_button_positive.xml
new file mode 100644
index 0000000000..ac7020b97c
--- /dev/null
+++ b/mobile/android/base/resources/drawable/action_bar_button_positive.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_pressed="false">
+ <shape>
+ <solid android:color="@color/link_blue"/>
+ </shape>
+ </item>
+
+ <item android:state_pressed="true">
+ <shape>
+ <solid android:color="@color/link_blue_pressed" />
+ </shape>
+ </item>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/alert_download_animation.xml b/mobile/android/base/resources/drawable/alert_download_animation.xml
new file mode 100644
index 0000000000..e50472f0b6
--- /dev/null
+++ b/mobile/android/base/resources/drawable/alert_download_animation.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
+ android:oneshot="false">
+
+ <item android:drawable="@drawable/alert_download_animation_1" android:duration="150" />
+ <item android:drawable="@drawable/alert_download_animation_2" android:duration="150" />
+ <item android:drawable="@drawable/alert_download_animation_3" android:duration="150" />
+ <item android:drawable="@drawable/alert_download_animation_4" android:duration="150" />
+ <item android:drawable="@drawable/alert_download_animation_5" android:duration="150" />
+ <item android:drawable="@drawable/alert_download_animation_6" android:duration="150" />
+
+</animation-list>
diff --git a/mobile/android/base/resources/drawable/arrow_down.xml b/mobile/android/base/resources/drawable/arrow_down.xml
new file mode 100644
index 0000000000..cfb14ed4c6
--- /dev/null
+++ b/mobile/android/base/resources/drawable/arrow_down.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<rotate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:drawable="@drawable/arrow_up"
+ android:fromDegrees="180"
+ android:toDegrees="180"/>
+
diff --git a/mobile/android/base/resources/drawable/as_bin.xml b/mobile/android/base/resources/drawable/as_bin.xml
new file mode 100644
index 0000000000..46de6104ea
--- /dev/null
+++ b/mobile/android/base/resources/drawable/as_bin.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M20.75,5L16,5v-2.75a1.252,1.252 0,0 0,-1.25 -1.25h-5.5a1.252,1.252 0,0 0,-1.25 1.25L8,5h-4.75a1.25,1.25 0,0 0,0 2.5L5,7.5v14.25a1.252,1.252 0,0 0,1.25 1.25h11.5a1.252,1.252 0,0 0,1.25 -1.25L19,7.5h1.75A1.25,1.25 0,0 0,20.75 5ZM10.5,3.5h3v1.5h-3v-1.5ZM16.5,20.5h-9v-13h9v13ZM10.5,18h0a0.5,0.5 0,0 1,-0.5 -0.5v-7a0.5,0.5 0,0 1,0.5 -0.5h0a0.5,0.5 0,0 1,0.5 0.5v7A0.5,0.5 0,0 1,10.5 18ZM13.5,18h0a0.5,0.5 0,0 1,-0.5 -0.5v-7a0.5,0.5 0,0 1,0.5 -0.5h0a0.5,0.5 0,0 1,0.5 0.5v7A0.5,0.5 0,0 1,13.5 18Z"/>
+</vector>
diff --git a/mobile/android/base/resources/drawable/as_bookmark.xml b/mobile/android/base/resources/drawable/as_bookmark.xml
new file mode 100644
index 0000000000..890838be3b
--- /dev/null
+++ b/mobile/android/base/resources/drawable/as_bookmark.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12.037,5.333l1.706,3.361 0.569,1.121 1.239,0.211 3.617,0.619 -2.617,2.851 -0.81,0.884 0.181,1.185 0.583,3.8 -3.312,-1.758 -1.179,-0.626 -1.176,0.632 -3.232,1.736 0.582,-3.788 0.184,-1.194 -0.822,-0.886 -2.633,-2.84 3.676,-0.619 1.272,-0.214 0.563,-1.161 1.609,-3.319M12.01,1a1.335,1.335 0,0 0,-1.085 0.895l-2.747,5.667 -5.877,0.99c-1.345,0.219 -1.679,1.186 -0.744,2.148l4.16,4.486 -0.969,6.311c-0.147,0.948 0.242,1.5 0.925,1.5a2,2 0,0 0,0.891 -0.249l5.457,-2.931 5.521,2.931a2,2 0,0 0,0.892 0.249c0.683,0 1.07,-0.555 0.926,-1.5l-0.966,-6.311 4.111,-4.481c0.936,-0.966 0.6,-1.934 -0.744,-2.153l-5.789,-0.99L13.1,1.9A1.333,1.333 0,0 0,12.01 1h0Z"/>
+</vector>
diff --git a/mobile/android/base/resources/drawable/as_contextmenu_divider.xml b/mobile/android/base/resources/drawable/as_contextmenu_divider.xml
new file mode 100644
index 0000000000..f24f9a2382
--- /dev/null
+++ b/mobile/android/base/resources/drawable/as_contextmenu_divider.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+ android:insetLeft="72dp">
+ <shape>
+ <size
+ android:height="1dp"/>
+ <solid android:color="@color/disabled_grey"/>
+ </shape>
+</inset> \ No newline at end of file
diff --git a/mobile/android/base/resources/drawable/as_copy.xml b/mobile/android/base/resources/drawable/as_copy.xml
new file mode 100644
index 0000000000..516459edbe
--- /dev/null
+++ b/mobile/android/base/resources/drawable/as_copy.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M21.75,23h-12.5a1.25,1.25 0,0 1,-1.25 -1.25v-14.5a1.25,1.25 0,0 1,1.25 -1.25h9a1.252,1.252 0,0 1,0.884 0.366l3.5,3.5a1.252,1.252 0,0 1,0.366 0.884v11A1.25,1.25 0,0 1,21.75 23ZM10.5,20.5h10v-9.232l-2.768,-2.768L10.5,8.5v12ZM7,15.5h-3.5v-12h7.232l1.5,1.5h3.511a1.16,1.16 0,0 0,-0.109 -0.134l-3.5,-3.5A1.252,1.252 0,0 0,11.25 1h-9a1.25,1.25 0,0 0,-1.25 1.25v14.5a1.25,1.25 0,0 0,1.25 1.25L7,18v-2.5ZM18.5,12L17,12v-1.5a0.5,0.5 0,0 0,-1 0v2a0.5,0.5 0,0 0,0.5 0.5h2A0.5,0.5 0,0 0,18.5 12Z"/>
+</vector>
diff --git a/mobile/android/base/resources/drawable/as_dimiss.xml b/mobile/android/base/resources/drawable/as_dimiss.xml
new file mode 100644
index 0000000000..ccc028e483
--- /dev/null
+++ b/mobile/android/base/resources/drawable/as_dimiss.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M13.779,12l7.867,-7.866a1.25,1.25 0,0 0,-1.768 -1.768l-7.866,7.866 -7.866,-7.866a1.25,1.25 0,1 0,-1.768 1.768L10.244,12l-7.866,7.866a1.25,1.25 0,0 0,1.768 1.768l7.866,-7.866 7.866,7.866a1.25,1.25 0,0 0,1.768 -1.768Z"/>
+</vector>
diff --git a/mobile/android/base/resources/drawable/as_home.xml b/mobile/android/base/resources/drawable/as_home.xml
new file mode 100644
index 0000000000..aece2b1959
--- /dev/null
+++ b/mobile/android/base/resources/drawable/as_home.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M18.012,22.969h-4v-6h-4v6h-4a1.075,1.075 0,0 1,-1 -1v-8.5a1.091,1.091 0,0 1,0.5 -1l5.5,-5.5a1.413,1.413 0,0 1,2 0l5.5,5.5a1.538,1.538 0,0 1,0.5 1v8.5A1.075,1.075 0,0 1,18.012 22.969ZM22.012,13.281a1.246,1.246 0,0 1,-0.884 -0.366l-9.116,-9.116 -9.116,9.116a1.25,1.25 0,0 1,-1.768 -1.768l10,-10a1.251,1.251 0,0 1,1.768 0l10,10A1.25,1.25 0,0 1,22.012 13.281Z"/>
+</vector>
diff --git a/mobile/android/base/resources/drawable/as_private.xml b/mobile/android/base/resources/drawable/as_private.xml
new file mode 100644
index 0000000000..96c8fbdd23
--- /dev/null
+++ b/mobile/android/base/resources/drawable/as_private.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M18,17.823c-2.29,0 -3.873,-2.692 -6.069,-2.692s-3.916,2.692 -6.069,2.692c-2.826,0 -4.912,-2.616 -4.946,-7.1 -0.021,-2.783 0.829,-3.671 4.5,-3.671s4.742,1.468 6.519,1.468 2.852,-1.468 6.519,-1.468 4.517,0.888 4.5,3.671C22.909,15.207 20.823,17.823 18,17.823ZM7.21,10.481c-2.229,0.1 -3.147,1.393 -3.147,1.713s1.478,1.224 2.923,1.224 3.147,-0.518 3.147,-0.979A2.611,2.611 0,0 0,7.207 10.481ZM16.652,10.481a2.611,2.611 0,0 0,-2.923 1.958c0,0.461 1.7,0.979 3.147,0.979s2.923,-0.9 2.923,-1.224S18.878,10.576 16.649,10.481Z"/>
+</vector>
diff --git a/mobile/android/base/resources/drawable/as_share.xml b/mobile/android/base/resources/drawable/as_share.xml
new file mode 100644
index 0000000000..ecb0f200b3
--- /dev/null
+++ b/mobile/android/base/resources/drawable/as_share.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M19,15a3.987,3.987 0,0 0,-2.839 1.18l-7.208,-3.6a3.6,3.6 0,0 0,0 -1.16l7.208,-3.6A4,4 0,1 0,15 5a3.936,3.936 0,0 0,0.047 0.58l-7.208,3.6a4,4 0,1 0,0 5.64l7.208,3.6a3.936,3.936 0,0 0,-0.047 0.58A4,4 0,1 0,19 15Z"/>
+</vector>
diff --git a/mobile/android/base/resources/drawable/as_tab.xml b/mobile/android/base/resources/drawable/as_tab.xml
new file mode 100644
index 0000000000..b8c4b19e95
--- /dev/null
+++ b/mobile/android/base/resources/drawable/as_tab.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M22,20.015L2,20.015a1.266,1.266 0,0 1,-1.266 -1.266v-2.531A1.266,1.266 0,0 1,2 14.952c0.618,0 1.248,-3.239 1.484,-4.459 0.622,-3.2 1.266,-6.508 3.971,-6.508h9.09c2.705,0 3.349,3.309 3.971,6.508 0.236,1.22 0.866,4.459 1.484,4.459a1.266,1.266 0,0 1,1.266 1.266v2.531A1.266,1.266 0,0 1,22 20.015ZM3.266,17.483h17.468v-0.3c-1.668,-0.883 -2.193,-3.583 -2.7,-6.21 -0.237,-1.219 -0.867,-4.459 -1.485,-4.459h-9.09c-0.618,0 -1.248,3.24 -1.485,4.459 -0.511,2.627 -1.036,5.327 -2.7,6.21v0.3Z"/>
+</vector>
diff --git a/mobile/android/base/resources/drawable/autocomplete_list_bg.xml b/mobile/android/base/resources/drawable/autocomplete_list_bg.xml
new file mode 100644
index 0000000000..9747b48a22
--- /dev/null
+++ b/mobile/android/base/resources/drawable/autocomplete_list_bg.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+
+ <solid android:color="@android:color/white"/>
+
+ <stroke android:width="1dp"
+ android:color="@color/placeholder_grey" />
+
+</shape> \ No newline at end of file
diff --git a/mobile/android/base/resources/drawable/bookmark_folder_arrow_up.xml b/mobile/android/base/resources/drawable/bookmark_folder_arrow_up.xml
new file mode 100644
index 0000000000..717cc59526
--- /dev/null
+++ b/mobile/android/base/resources/drawable/bookmark_folder_arrow_up.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- State drawables will stretch drawables to be the same size and neither
+ android:constantSize nor variablePadding fix this, so we hard-code padding
+ in to ensure that they display at their true size. -->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+ android:drawable="@drawable/arrow_up"
+ android:insetTop="4dp"
+ android:insetRight="5dp"
+ android:insetBottom="4dp"
+ android:insetLeft="5dp" />
diff --git a/mobile/android/base/resources/drawable/button_background_action_blue_round.xml b/mobile/android/base/resources/drawable/button_background_action_blue_round.xml
new file mode 100644
index 0000000000..9b5ba80682
--- /dev/null
+++ b/mobile/android/base/resources/drawable/button_background_action_blue_round.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true"
+ android:drawable="@drawable/button_pressed_action_blue_round" />
+ <item android:state_enabled="true"
+ android:drawable="@drawable/button_enabled_action_blue_round" />
+</selector>
diff --git a/mobile/android/base/resources/drawable/button_background_action_orange_round.xml b/mobile/android/base/resources/drawable/button_background_action_orange_round.xml
new file mode 100644
index 0000000000..02a6c6673e
--- /dev/null
+++ b/mobile/android/base/resources/drawable/button_background_action_orange_round.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true"
+ android:drawable="@drawable/button_pressed_action_orange_round" />
+ <item android:state_enabled="true"
+ android:drawable="@drawable/button_enabled_action_orange_round" />
+</selector>
diff --git a/mobile/android/base/resources/drawable/button_enabled_action_blue_round.xml b/mobile/android/base/resources/drawable/button_enabled_action_blue_round.xml
new file mode 100644
index 0000000000..94a6b99352
--- /dev/null
+++ b/mobile/android/base/resources/drawable/button_enabled_action_blue_round.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle" >
+ <solid android:color="@color/link_blue_pressed" />
+ <corners
+ android:radius="@dimen/standard_corner_radius" />
+</shape>
diff --git a/mobile/android/base/resources/drawable/button_enabled_action_orange_round.xml b/mobile/android/base/resources/drawable/button_enabled_action_orange_round.xml
new file mode 100644
index 0000000000..7a9d55b109
--- /dev/null
+++ b/mobile/android/base/resources/drawable/button_enabled_action_orange_round.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle" >
+ <solid android:color="@color/action_orange" />
+ <corners
+ android:radius="@dimen/standard_corner_radius" />
+</shape>
diff --git a/mobile/android/base/resources/drawable/button_pressed_action_blue_round.xml b/mobile/android/base/resources/drawable/button_pressed_action_blue_round.xml
new file mode 100644
index 0000000000..bca19fab6a
--- /dev/null
+++ b/mobile/android/base/resources/drawable/button_pressed_action_blue_round.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle" >
+ <solid android:color="@color/link_blue" />
+ <corners
+ android:radius="@dimen/standard_corner_radius" />
+</shape>
diff --git a/mobile/android/base/resources/drawable/button_pressed_action_orange_round.xml b/mobile/android/base/resources/drawable/button_pressed_action_orange_round.xml
new file mode 100644
index 0000000000..2720b7fba2
--- /dev/null
+++ b/mobile/android/base/resources/drawable/button_pressed_action_orange_round.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle" >
+ <solid android:color="@color/action_orange_pressed" />
+ <corners
+ android:radius="@dimen/standard_corner_radius" />
+</shape>
diff --git a/mobile/android/base/resources/drawable/close_edit_mode_selector.xml b/mobile/android/base/resources/drawable/close_edit_mode_selector.xml
new file mode 100644
index 0000000000..aab2a469c1
--- /dev/null
+++ b/mobile/android/base/resources/drawable/close_edit_mode_selector.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <item gecko:state_dark="true"
+ android:drawable="@drawable/close_edit_mode_dark"/>
+
+ <item gecko:state_private="true"
+ android:drawable="@drawable/close_edit_mode_light"/>
+
+ <item android:drawable="@drawable/close_edit_mode_light"/>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/color_picker_checkmark.xml b/mobile/android/base/resources/drawable/color_picker_checkmark.xml
new file mode 100644
index 0000000000..645ec81155
--- /dev/null
+++ b/mobile/android/base/resources/drawable/color_picker_checkmark.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="ring"
+ android:innerRadius="15dip"
+ android:thickness="4dip"
+ android:useLevel="false">
+ <solid android:color="@android:color/white"/>
+</shape>
diff --git a/mobile/android/base/resources/drawable/divider_vertical.xml b/mobile/android/base/resources/drawable/divider_vertical.xml
new file mode 100644
index 0000000000..d326d94f95
--- /dev/null
+++ b/mobile/android/base/resources/drawable/divider_vertical.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+
+ <solid android:color="@color/toolbar_divider_grey"/>
+ <size android:width="1dp" />
+
+</shape>
diff --git a/mobile/android/base/resources/drawable/edit_text_default.xml b/mobile/android/base/resources/drawable/edit_text_default.xml
new file mode 100644
index 0000000000..edb6632db3
--- /dev/null
+++ b/mobile/android/base/resources/drawable/edit_text_default.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!-- Make sure the border only appears at the bottom of the background -->
+ <item
+ android:top="-2dp"
+ android:right="-2dp"
+ android:left="-2dp">
+ <shape>
+ <!-- Padding creates vertical space between the text and the underline,
+ as well as right padding for search icon/clear button -->
+ <padding
+ android:top="@dimen/search_bar_padding_y"
+ android:bottom="@dimen/search_bar_padding_y"
+ android:right="@dimen/search_bar_padding_right"/>
+ <solid android:color="@android:color/transparent"/>
+ <stroke android:width="1dp" android:color="@color/tabs_tray_icon_grey"/>
+ </shape>
+ </item>
+
+</layer-list>
diff --git a/mobile/android/base/resources/drawable/edit_text_focused.xml b/mobile/android/base/resources/drawable/edit_text_focused.xml
new file mode 100644
index 0000000000..38782652e9
--- /dev/null
+++ b/mobile/android/base/resources/drawable/edit_text_focused.xml
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!-- Make sure the border only appears at the bottom of the background -->
+ <item
+ android:top="-3dp"
+ android:right="-3dp"
+ android:left="-3dp">
+ <shape>
+ <!-- Padding creates vertical space between the text and the underline,
+ as well as right padding for search icon/clear button -->
+ <padding
+ android:top="@dimen/search_bar_padding_y"
+ android:bottom="@dimen/search_bar_padding_y"
+ android:right="@dimen/search_bar_padding_right"/>
+ <solid android:color="@android:color/transparent"/>
+ <!-- We apply a color filter to set the color for the selected search engine -->
+ <stroke android:width="2dp" android:color="@android:color/white"/>
+ </shape>
+ </item>
+
+</layer-list>
diff --git a/mobile/android/base/resources/drawable/facet_button_background.xml b/mobile/android/base/resources/drawable/facet_button_background.xml
new file mode 100644
index 0000000000..de1b6e996f
--- /dev/null
+++ b/mobile/android/base/resources/drawable/facet_button_background.xml
@@ -0,0 +1,15 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <!--facet button is pressed (omitting currently-selected facet)-->
+ <item
+ android:state_pressed="true"
+ android:state_checked="false"
+ android:drawable="@drawable/facet_button_background_pressed"/>
+
+ <!--default-->
+ <item
+ android:drawable="@drawable/facet_button_background_default"/>
+</selector>
diff --git a/mobile/android/base/resources/drawable/facet_button_background_default.xml b/mobile/android/base/resources/drawable/facet_button_background_default.xml
new file mode 100644
index 0000000000..b3358d2ede
--- /dev/null
+++ b/mobile/android/base/resources/drawable/facet_button_background_default.xml
@@ -0,0 +1,8 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/facet_button_background_color_default" />
+</shape>
diff --git a/mobile/android/base/resources/drawable/facet_button_background_pressed.xml b/mobile/android/base/resources/drawable/facet_button_background_pressed.xml
new file mode 100644
index 0000000000..0a46f60573
--- /dev/null
+++ b/mobile/android/base/resources/drawable/facet_button_background_pressed.xml
@@ -0,0 +1,8 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/facet_button_background_color_pressed" />
+</shape>
diff --git a/mobile/android/base/resources/drawable/home_banner.xml b/mobile/android/base/resources/drawable/home_banner.xml
new file mode 100644
index 0000000000..ea536ced0f
--- /dev/null
+++ b/mobile/android/base/resources/drawable/home_banner.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_pressed="true">
+ <layer-list>
+ <item android:left="-2dp"
+ android:right="-2dp"
+ android:bottom="-2dp">
+
+ <shape android:shape="rectangle" >
+ <stroke android:width="2dp"
+ android:color="#FFE0E4E7" />
+ <solid android:color="#FFC5D0DA" />
+ </shape>
+ </item>
+ </layer-list>
+ </item>
+
+ <item>
+ <layer-list>
+ <item android:left="-2dp"
+ android:right="-2dp"
+ android:bottom="-2dp">
+
+ <shape android:shape="rectangle" >
+ <stroke android:width="2dp"
+ android:color="#FFE0E4E7" />
+ <solid android:color="@color/about_page_header_grey" />
+ </shape>
+ </item>
+ </layer-list>
+ </item>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/home_history_clear_button_bg.xml b/mobile/android/base/resources/drawable/home_history_clear_button_bg.xml
new file mode 100644
index 0000000000..f4fee99f4c
--- /dev/null
+++ b/mobile/android/base/resources/drawable/home_history_clear_button_bg.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ - This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item>
+ <shape android:shape="rectangle" >
+ <stroke android:width="1dp"
+ android:color="@color/toolbar_divider_grey" />
+ <padding android:top="1dp" />
+ </shape>
+ </item>
+ <item>
+ <selector>
+ <item android:state_pressed="true"
+ android:drawable="@color/toolbar_grey_pressed" />
+ <item android:drawable="@color/toolbar_grey"/>
+ </selector>
+ </item>
+</layer-list> \ No newline at end of file
diff --git a/mobile/android/base/resources/drawable/home_pager_empty_state.xml b/mobile/android/base/resources/drawable/home_pager_empty_state.xml
new file mode 100644
index 0000000000..71389ebc63
--- /dev/null
+++ b/mobile/android/base/resources/drawable/home_pager_empty_state.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <item android:maxLevel="0" android:drawable="@android:color/white"/>
+
+ <item>
+ <bitmap android:src="@drawable/icon_home_empty_firefox"
+ android:gravity="center"/>
+ </item>
+
+</layer-list> \ No newline at end of file
diff --git a/mobile/android/base/resources/drawable/ic_as_bookmarked.xml b/mobile/android/base/resources/drawable/ic_as_bookmarked.xml
new file mode 100644
index 0000000000..0337186417
--- /dev/null
+++ b/mobile/android/base/resources/drawable/ic_as_bookmarked.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="@color/activity_stream_icon"
+ android:pathData="M12.01,1a1.34,1.34 0,0 0,-1.085 0.89l-2.747,5.67 -5.877,0.99c-1.345,0.22 -1.679,1.19 -0.744,2.15l4.16,4.49 -0.969,6.31c-0.147,0.94 0.242,1.5 0.925,1.5a1.986,1.986 0,0 0,0.891 -0.25l5.457,-2.93 5.521,2.93a1.993,1.993 0,0 0,0.892 0.25c0.683,0 1.07,-0.56 0.926,-1.5l-0.966,-6.31 4.111,-4.49c0.936,-0.96 0.6,-1.93 -0.744,-2.15l-5.789,-0.99 -2.877,-5.67a1.339,1.339 0,0 0,-1.085 -0.89h0Z"/>
+</vector>
diff --git a/mobile/android/base/resources/drawable/ic_as_visited.xml b/mobile/android/base/resources/drawable/ic_as_visited.xml
new file mode 100644
index 0000000000..05006ef955
--- /dev/null
+++ b/mobile/android/base/resources/drawable/ic_as_visited.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="@color/activity_stream_icon"
+ android:pathData="M12,18a6,6 0,1 1,6 -6A6,6 0,0 1,12 18ZM12,9a3,3 0,1 0,3 3A3,3 0,0 0,12 9Z"/>
+</vector>
diff --git a/mobile/android/base/resources/drawable/icon_grid_item_bg.xml b/mobile/android/base/resources/drawable/icon_grid_item_bg.xml
new file mode 100644
index 0000000000..45c9cb1f35
--- /dev/null
+++ b/mobile/android/base/resources/drawable/icon_grid_item_bg.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_focused="true"
+ android:state_pressed="true"
+ android:drawable="@drawable/grid_icon_bg_focused" />
+
+ <item android:state_activated="true"
+ android:drawable="@drawable/grid_icon_bg_activated" />
+
+ <item android:drawable="@android:color/transparent" />
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/logo.xml b/mobile/android/base/resources/drawable/logo.xml
new file mode 100644
index 0000000000..e188f80dce
--- /dev/null
+++ b/mobile/android/base/resources/drawable/logo.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- Overidden. -->
+<bitmap
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/icon"/>
diff --git a/mobile/android/base/resources/drawable/menu_item_action_bar_bg.xml b/mobile/android/base/resources/drawable/menu_item_action_bar_bg.xml
new file mode 100644
index 0000000000..ad00f49f96
--- /dev/null
+++ b/mobile/android/base/resources/drawable/menu_item_action_bar_bg.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_pressed="true"
+ android:state_enabled="true">
+ <shape>
+ <solid android:color="@color/toolbar_grey_pressed" />
+ </shape>
+ </item>
+
+ <item android:state_focused="true"
+ android:state_pressed="false">
+ <shape>
+ <solid android:color="@color/highlight_focused" />
+ </shape>
+ </item>
+
+ <item android:drawable="@android:color/transparent"/>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/menu_item_state.xml b/mobile/android/base/resources/drawable/menu_item_state.xml
new file mode 100644
index 0000000000..c7063f4e3c
--- /dev/null
+++ b/mobile/android/base/resources/drawable/menu_item_state.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <item gecko:state_more="true"
+ android:drawable="@drawable/menu_item_more"/>
+
+ <item gecko:state_more="false"
+ android:state_checkable="true"
+ android:state_checked="true"
+ android:drawable="@drawable/menu_item_check"/>
+
+ <item gecko:state_more="false"
+ android:state_checkable="true"
+ android:state_checked="false"
+ android:drawable="@drawable/menu_item_uncheck"/>
+
+ <item android:drawable="@android:color/transparent"/>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/overlay_share_bookmark_button.xml b/mobile/android/base/resources/drawable/overlay_share_bookmark_button.xml
new file mode 100644
index 0000000000..bc1d51c5de
--- /dev/null
+++ b/mobile/android/base/resources/drawable/overlay_share_bookmark_button.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:state_enabled="false"
+ android:drawable="@drawable/overlay_bookmarked_already_icon"/>
+ <item
+ android:drawable="@drawable/overlay_bookmark_icon"/>
+</selector>
diff --git a/mobile/android/base/resources/drawable/overlay_share_button_background.xml b/mobile/android/base/resources/drawable/overlay_share_button_background.xml
new file mode 100644
index 0000000000..6077ad38af
--- /dev/null
+++ b/mobile/android/base/resources/drawable/overlay_share_button_background.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ - This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ -->
+
+<!-- Should be kept in sync with overlay_share_button_background_first.xml -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_pressed="true" android:drawable="@color/toolbar_grey_pressed" />
+ <item android:drawable="@color/toolbar_grey"/>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/overlay_share_button_background_first.xml b/mobile/android/base/resources/drawable/overlay_share_button_background_first.xml
new file mode 100644
index 0000000000..65ee5de9de
--- /dev/null
+++ b/mobile/android/base/resources/drawable/overlay_share_button_background_first.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ - This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ -->
+
+<!-- Should be kept in sync with overlay_share_button_background.xml
+
+ This first item in the list has rounded corners. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_pressed="true">
+ <shape>
+ <solid android:color="@color/toolbar_grey_pressed"/>
+ <corners android:topLeftRadius="@dimen/standard_corner_radius"
+ android:topRightRadius="@dimen/standard_corner_radius"/>
+ </shape>
+ </item>
+
+ <item>
+ <shape>
+ <solid android:color="@color/toolbar_grey"/>
+ <corners android:topLeftRadius="@dimen/standard_corner_radius"
+ android:topRightRadius="@dimen/standard_corner_radius"/>
+ </shape>
+ </item>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/panel_auth_button.xml b/mobile/android/base/resources/drawable/panel_auth_button.xml
new file mode 100644
index 0000000000..4a9adf428c
--- /dev/null
+++ b/mobile/android/base/resources/drawable/panel_auth_button.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_pressed="true">
+ <layer-list>
+ <item android:left="-2dp"
+ android:right="-2dp"
+ android:top="-2dp">
+
+ <shape android:shape="rectangle" >
+ <stroke android:width="2dp"
+ android:color="#FFE0E4E7" />
+ <solid android:color="#FFC5D0DA" />
+ </shape>
+ </item>
+ </layer-list>
+ </item>
+
+ <item>
+ <layer-list>
+ <item android:left="-2dp"
+ android:right="-2dp"
+ android:top="-2dp">
+
+ <shape android:shape="rectangle" >
+ <stroke android:width="2dp"
+ android:color="#FFE0E4E7" />
+ <solid android:color="@color/about_page_header_grey" />
+ </shape>
+ </item>
+ </layer-list>
+ </item>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/progressbar.xml b/mobile/android/base/resources/drawable/progressbar.xml
new file mode 100644
index 0000000000..627484a1f3
--- /dev/null
+++ b/mobile/android/base/resources/drawable/progressbar.xml
@@ -0,0 +1,9 @@
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@android:id/progress">
+ <clip>
+ <shape>
+ <solid android:color="@color/fennec_ui_orange"/>
+ </shape>
+ </clip>
+ </item>
+</layer-list>
diff --git a/mobile/android/base/resources/drawable/push_notification.png b/mobile/android/base/resources/drawable/push_notification.png
new file mode 100644
index 0000000000..2a52dbd505
--- /dev/null
+++ b/mobile/android/base/resources/drawable/push_notification.png
Binary files differ
diff --git a/mobile/android/base/resources/drawable/remote_tabs_setup_button_background.xml b/mobile/android/base/resources/drawable/remote_tabs_setup_button_background.xml
new file mode 100644
index 0000000000..dad4e51249
--- /dev/null
+++ b/mobile/android/base/resources/drawable/remote_tabs_setup_button_background.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/remote_tabs_setup_button_background_hit"/>
+ <corners android:radius="@dimen/standard_corner_radius"/>
+ </shape>
+ </item>
+
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/action_orange"/>
+ <corners android:radius="@dimen/standard_corner_radius"/>
+ </shape>
+ </item>
+</selector>
diff --git a/mobile/android/base/resources/drawable/search_row_background.xml b/mobile/android/base/resources/drawable/search_row_background.xml
new file mode 100644
index 0000000000..ded70ec6d2
--- /dev/null
+++ b/mobile/android/base/resources/drawable/search_row_background.xml
@@ -0,0 +1,10 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_pressed="true" android:drawable="@color/row_background_pressed" />
+ <item android:drawable="@color/row_background"/>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/search_suggestion_button.xml b/mobile/android/base/resources/drawable/search_suggestion_button.xml
new file mode 100644
index 0000000000..b91fd4bf0d
--- /dev/null
+++ b/mobile/android/base/resources/drawable/search_suggestion_button.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true">
+ <shape>
+ <solid android:color="@color/toolbar_grey_pressed"/>
+ <corners android:radius="@dimen/standard_corner_radius"/>
+ </shape>
+ </item>
+
+ <item>
+ <shape>
+ <solid android:color="@color/toolbar_grey"/>
+ <corners android:radius="@dimen/standard_corner_radius"/>
+ </shape>
+ </item>
+</selector>
diff --git a/mobile/android/base/resources/drawable/search_suggestion_prompt_no.xml b/mobile/android/base/resources/drawable/search_suggestion_prompt_no.xml
new file mode 100644
index 0000000000..b976cfa5ad
--- /dev/null
+++ b/mobile/android/base/resources/drawable/search_suggestion_prompt_no.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true">
+ <shape>
+ <solid android:color="@color/toolbar_grey_pressed"/>
+ <corners android:radius="@dimen/doorhanger_rounded_corner_radius"/>
+ </shape>
+ </item>
+
+ <item>
+ <shape>
+ <solid android:color="@color/toolbar_menu_dark_grey"/>
+ <corners android:radius="@dimen/doorhanger_rounded_corner_radius"/>
+ </shape>
+ </item>
+</selector>
diff --git a/mobile/android/base/resources/drawable/search_suggestion_prompt_yes.xml b/mobile/android/base/resources/drawable/search_suggestion_prompt_yes.xml
new file mode 100644
index 0000000000..12551497be
--- /dev/null
+++ b/mobile/android/base/resources/drawable/search_suggestion_prompt_yes.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+<item android:state_pressed="true">
+ <shape>
+ <solid android:color="@color/link_blue_pressed"/>
+ <corners android:radius="@dimen/doorhanger_rounded_corner_radius"/>
+ </shape>
+</item>
+
+<item>
+ <shape>
+ <solid android:color="@color/link_blue"/>
+ <corners android:radius="@dimen/doorhanger_rounded_corner_radius"/>
+ </shape>
+</item>
+</selector>
diff --git a/mobile/android/base/resources/drawable/shaped_button.xml b/mobile/android/base/resources/drawable/shaped_button.xml
new file mode 100644
index 0000000000..74fc45f850
--- /dev/null
+++ b/mobile/android/base/resources/drawable/shaped_button.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- If you change this view, update ShapedButton*,
+ which dynamically resets to this view. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_pressed="true"
+ android:drawable="@color/highlight_shaped"/>
+
+ <item android:state_focused="true"
+ android:state_pressed="false"
+ android:drawable="@color/highlight_shaped_focused"/>
+
+ <item android:drawable="@color/text_and_tabs_tray_grey"/>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/site_security_level.xml b/mobile/android/base/resources/drawable/site_security_level.xml
new file mode 100644
index 0000000000..5fee9fffac
--- /dev/null
+++ b/mobile/android/base/resources/drawable/site_security_level.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<level-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:maxLevel="0" android:drawable="@drawable/site_security_unknown"/>
+ <item android:maxLevel="1" android:drawable="@drawable/lock_secure"/>
+ <item android:maxLevel="2" android:drawable="@drawable/lock_secure"/>
+ <item android:maxLevel="3" android:drawable="@drawable/warning_minor"/>
+ <item android:maxLevel="4" android:drawable="@drawable/lock_disabled"/>
+ <item android:maxLevel="5" android:drawable="@drawable/shield_enabled"/>
+ <item android:maxLevel="6" android:drawable="@drawable/shield_disabled"/>
+
+ <!-- Special icon used for about:home -->
+ <item android:maxLevel="999" android:drawable="@drawable/search_icon_inactive" />
+</level-list>
diff --git a/mobile/android/base/resources/drawable/site_security_unknown.xml b/mobile/android/base/resources/drawable/site_security_unknown.xml
new file mode 100644
index 0000000000..86ff863e1d
--- /dev/null
+++ b/mobile/android/base/resources/drawable/site_security_unknown.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- The favicon drawable is not the same dimensions as the site security
+ lock icons so we offset it using this drawable to compensate. -->
+<inset
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:drawable="@drawable/favicon_globe"
+ android:insetTop="@dimen/site_security_unknown_inset_top"
+ android:insetBottom="@dimen/site_security_unknown_inset_bottom"/>
diff --git a/mobile/android/base/resources/drawable/tab_history_bg.xml b/mobile/android/base/resources/drawable/tab_history_bg.xml
new file mode 100644
index 0000000000..1819ddd581
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tab_history_bg.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item>
+ <shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle" >
+ <stroke
+ android:width="@dimen/tab_history_bg_width"
+ android:color="@color/tab_history_border_color" />
+
+ <padding android:top="@dimen/tab_history_border_padding" />
+ </shape>
+ </item>
+ <item>
+ <shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle" >
+ <solid
+ android:width="@dimen/tab_history_bg_width"
+ android:color="@color/toolbar_grey" />
+ </shape>
+ </item>
+</layer-list>
diff --git a/mobile/android/base/resources/drawable/tab_history_icon_state.xml b/mobile/android/base/resources/drawable/tab_history_icon_state.xml
new file mode 100644
index 0000000000..504dd58041
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tab_history_icon_state.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_enabled="false">
+ <shape>
+ <solid android:color="@color/tab_history_favicon_background" />
+ <stroke android:width="@dimen/tab_history_favicon_border_disabled"
+ android:color="@color/tab_history_favicon_border" />
+ </shape>
+ </item>
+
+ <item android:state_enabled="true">
+ <shape>
+ <solid android:color="@color/tab_history_favicon_background" />
+ <stroke android:width="@dimen/tab_history_favicon_border_enabled"
+ android:color="@color/tab_history_favicon_border" />
+ </shape>
+ </item>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/tab_item_close_button.xml b/mobile/android/base/resources/drawable/tab_item_close_button.xml
new file mode 100644
index 0000000000..4012346331
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tab_item_close_button.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!-- pressed state -->
+ <item android:state_pressed="true"
+ android:drawable="@drawable/tab_close_active"/>
+
+ <item android:state_checked="true"
+ android:drawable="@drawable/tab_close_active"/>
+
+ <!-- normal mode -->
+ <item android:drawable="@drawable/tab_close"/>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/tab_panel_tab_background.xml b/mobile/android/base/resources/drawable/tab_panel_tab_background.xml
new file mode 100644
index 0000000000..c69bfbd81e
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tab_panel_tab_background.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <item gecko:state_private="true">
+ <layer-list>
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/private_toolbar_grey"/>
+ </shape>
+ </item>
+
+ <item>
+ <bitmap android:src="@drawable/tab_preview_masq"
+ android:gravity="center"/>
+ </item>
+ </layer-list>
+ </item>
+
+ <item>
+ <layer-list>
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/about_page_header_grey"/>
+ </shape>
+ </item>
+
+ <item>
+ <bitmap android:src="@drawable/globe_light"
+ android:gravity="center"/>
+ </item>
+ </layer-list>
+ </item>
+</selector>
diff --git a/mobile/android/base/resources/drawable/tab_queue_dismiss_button_foreground.xml b/mobile/android/base/resources/drawable/tab_queue_dismiss_button_foreground.xml
new file mode 100644
index 0000000000..843ce58706
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tab_queue_dismiss_button_foreground.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_pressed="true"
+ android:color="@color/tab_queue_dismiss_button_foreground_pressed" />
+
+ <item android:color="@color/tab_queue_dismiss_button_foreground"/>
+
+</selector> \ No newline at end of file
diff --git a/mobile/android/base/resources/drawable/tab_row.xml b/mobile/android/base/resources/drawable/tab_row.xml
new file mode 100644
index 0000000000..cefd990f34
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tab_row.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_focused="true"
+ android:drawable="@color/tab_row_pressed"/>
+
+ <item android:state_pressed="true"
+ android:drawable="@color/tab_row_pressed"/>
+
+ <item android:drawable="@android:color/transparent"/>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/tab_strip_button.xml b/mobile/android/base/resources/drawable/tab_strip_button.xml
new file mode 100644
index 0000000000..7daa9d5c41
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tab_strip_button.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <item android:state_pressed="true"
+ android:state_enabled="true">
+
+ <inset android:insetTop="@dimen/tablet_tab_strip_button_inset"
+ android:insetBottom="@dimen/tablet_tab_strip_button_inset"
+ android:insetLeft="@dimen/tablet_tab_strip_button_inset"
+ android:insetRight="@dimen/tablet_tab_strip_button_inset">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/highlight_dark"/>
+ <corners android:radius="@dimen/standard_corner_radius"/>
+ </shape>
+ </inset>
+
+ </item>
+
+ <item android:state_focused="true"
+ android:state_pressed="false">
+
+ <inset android:insetTop="@dimen/tablet_tab_strip_button_inset"
+ android:insetBottom="@dimen/tablet_tab_strip_button_inset"
+ android:insetLeft="@dimen/tablet_tab_strip_button_inset"
+ android:insetRight="@dimen/tablet_tab_strip_button_inset">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/tablet_highlight_focused"/>
+ <corners android:radius="@dimen/standard_corner_radius"/>
+ </shape>
+ </inset>
+
+ </item>
+
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@android:color/transparent"/>
+ </shape>
+ </item>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/tab_strip_divider.xml b/mobile/android/base/resources/drawable/tab_strip_divider.xml
new file mode 100644
index 0000000000..4ff4ff31f1
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tab_strip_divider.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+
+ <solid android:color="#555555"/>
+
+ <size android:width="1dp"
+ android:height="30dp"/>
+
+ <!-- We draw this ourselves in TabStripView.draw() and to avoid implementing more
+ than we have to, only bottom padding is taken into account. -->
+ <padding android:bottom="6dp"/>
+
+</shape>
diff --git a/mobile/android/base/resources/drawable/tab_thumbnail.xml b/mobile/android/base/resources/drawable/tab_thumbnail.xml
new file mode 100644
index 0000000000..e51de927ef
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tab_thumbnail.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <item android:state_focused="true">
+
+ <shape android:shape="rectangle">
+ <!-- @color/fennec_ui_orange with alpha -->
+ <solid android:color="#B3FF9500"/>
+ <corners android:radius="@dimen/standard_corner_radius"/>
+ </shape>
+
+ </item>
+
+ <item android:state_focused="true"
+ gecko:state_private="true">
+
+ <shape android:shape="rectangle">
+ <!-- @color/private_browsing_purple with alpha -->
+ <solid android:color="#B3CF68FF"/>
+ <corners android:radius="@dimen/standard_corner_radius"/>
+ </shape>
+
+ </item>
+
+ <item android:state_pressed="true"
+ gecko:state_private="true">
+
+ <shape android:shape="rectangle">
+ <!-- @color/private_browsing_purple with alpha -->
+ <solid android:color="#B3CF68FF"/>
+ <corners android:radius="@dimen/standard_corner_radius"/>
+ </shape>
+
+ </item>
+
+ <item android:state_pressed="true">
+
+ <shape android:shape="rectangle">
+ <!-- @color/fennec_ui_orange with alpha -->
+ <solid android:color="#B3FF9500"/>
+ <corners android:radius="@dimen/standard_corner_radius"/>
+ </shape>
+
+ </item>
+
+ <item gecko:state_recording="true">
+
+ <shape android:shape="rectangle">
+ <solid android:color="#FFFF0000"/>
+ <corners android:radius="@dimen/standard_corner_radius"/>
+ </shape>
+
+ </item>
+
+ <item android:state_focused="false"
+ android:state_pressed="false"
+ android:state_checked="true"
+ gecko:state_recording="false"
+ gecko:state_private="true">
+
+ <shape android:shape="rectangle">
+ <solid android:color="@color/private_browsing_purple"/>
+ <corners android:radius="@dimen/standard_corner_radius"/>
+ </shape>
+
+ </item>
+
+ <item android:state_focused="false"
+ android:state_pressed="false"
+ android:state_checked="true"
+ gecko:state_recording="false">
+
+ <shape android:shape="rectangle">
+ <solid android:color="@color/fennec_ui_orange"/>
+ <corners android:radius="@dimen/standard_corner_radius"/>
+ </shape>
+
+ </item>
+
+ <item android:drawable="@android:color/transparent"/>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/tabs_panel_indicator.xml b/mobile/android/base/resources/drawable/tabs_panel_indicator.xml
new file mode 100644
index 0000000000..4c1ab76554
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tabs_panel_indicator.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <item android:state_focused="false"
+ android:state_selected="false"
+ android:state_pressed="false"
+ android:drawable="@android:color/transparent"/>
+
+ <item gecko:state_private="true"
+ android:state_focused="false"
+ android:state_selected="true"
+ android:state_pressed="false"
+ android:drawable="@drawable/tabs_panel_indicator_selected_private"/>
+
+ <item android:state_focused="false"
+ android:state_selected="true"
+ android:state_pressed="false"
+ android:drawable="@drawable/tabs_panel_indicator_selected"/>
+
+ <item android:state_focused="true"
+ android:state_selected="false"
+ android:state_pressed="false"
+ android:drawable="@color/highlight_dark_focused"/>
+
+ <item android:state_focused="true"
+ android:state_selected="true"
+ android:state_pressed="false"
+ android:drawable="@drawable/tab_indicator_selected_focused"/>
+
+ <item android:state_focused="false"
+ android:state_selected="false"
+ android:state_pressed="true"
+ android:drawable="@color/highlight_dark"/>
+
+ <item android:state_focused="false"
+ android:state_selected="true"
+ android:state_pressed="true"
+ android:drawable="@color/highlight_dark"/>
+
+ <item android:state_focused="true"
+ android:state_selected="false"
+ android:state_pressed="true"
+ android:drawable="@color/highlight_dark"/>
+
+ <item android:state_focused="true"
+ android:state_selected="true"
+ android:state_pressed="true"
+ android:drawable="@color/highlight_dark"/>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/tabs_panel_indicator_selected.xml b/mobile/android/base/resources/drawable/tabs_panel_indicator_selected.xml
new file mode 100644
index 0000000000..c74b343e56
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tabs_panel_indicator_selected.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+ android:insetTop="@dimen/tabs_panel_indicator_selected_padding_top"
+ android:drawable="@color/fennec_ui_orange"
+ />
diff --git a/mobile/android/base/resources/drawable/tabs_panel_indicator_selected_private.xml b/mobile/android/base/resources/drawable/tabs_panel_indicator_selected_private.xml
new file mode 100644
index 0000000000..93dc049865
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tabs_panel_indicator_selected_private.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+ android:insetTop="@dimen/tabs_panel_indicator_selected_padding_top"
+ android:drawable="@color/private_browsing_purple"
+ />
diff --git a/mobile/android/base/resources/drawable/tabs_strip_indicator.xml b/mobile/android/base/resources/drawable/tabs_strip_indicator.xml
new file mode 100644
index 0000000000..32ca3115a2
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tabs_strip_indicator.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_focused="false"
+ android:state_selected="false"
+ android:state_pressed="false"
+ android:drawable="@android:color/transparent"/>
+
+ <item android:state_focused="false"
+ android:state_selected="true"
+ android:state_pressed="false"
+ android:drawable="@drawable/tab_indicator_selected"/>
+
+ <item android:state_focused="true"
+ android:state_selected="false"
+ android:state_pressed="false"
+ android:drawable="@color/highlight_dark_focused"/>
+
+ <item android:state_focused="true"
+ android:state_selected="true"
+ android:state_pressed="false"
+ android:drawable="@drawable/tab_indicator_selected_focused"/>
+
+ <item android:state_focused="false"
+ android:state_selected="false"
+ android:state_pressed="true"
+ android:drawable="@color/highlight_dark"/>
+
+ <item android:state_focused="false"
+ android:state_selected="true"
+ android:state_pressed="true"
+ android:drawable="@color/highlight_dark"/>
+
+ <item android:state_focused="true"
+ android:state_selected="false"
+ android:state_pressed="true"
+ android:drawable="@color/highlight_dark"/>
+
+ <item android:state_focused="true"
+ android:state_selected="true"
+ android:state_pressed="true"
+ android:drawable="@color/highlight_dark"/>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/toast_background.xml b/mobile/android/base/resources/drawable/toast_background.xml
new file mode 100644
index 0000000000..55cd9d9b21
--- /dev/null
+++ b/mobile/android/base/resources/drawable/toast_background.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/toast_background" />
+ <corners android:radius="@dimen/toast_button_corner_radius" />
+</shape>
diff --git a/mobile/android/base/resources/drawable/toast_button_background.xml b/mobile/android/base/resources/drawable/toast_button_background.xml
new file mode 100644
index 0000000000..6570d9b45f
--- /dev/null
+++ b/mobile/android/base/resources/drawable/toast_button_background.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- On Android pre v12/3.0/Gingerbread, bottom left and bottom
+ right are swapped. These values correct this bug; the resources
+ that don't need correction are in res/drawable-v12. -->
+ <item android:state_pressed="true">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/toast_button_pressed" />
+ <corners
+ android:topRightRadius="@dimen/toast_button_corner_radius"
+ android:bottomLeftRadius="@dimen/toast_button_corner_radius"
+ android:topLeftRadius="0dp"
+ android:bottomRightRadius="0dp" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/toast_button_background" />
+ <corners
+ android:topRightRadius="@dimen/toast_button_corner_radius"
+ android:bottomLeftRadius="@dimen/toast_button_corner_radius"
+ android:topLeftRadius="0dp"
+ android:bottomRightRadius="0dp" />
+ </shape>
+ </item>
+</selector>
diff --git a/mobile/android/base/resources/drawable/toolbar_favicon_default.xml b/mobile/android/base/resources/drawable/toolbar_favicon_default.xml
new file mode 100644
index 0000000000..92e294fb3b
--- /dev/null
+++ b/mobile/android/base/resources/drawable/toolbar_favicon_default.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/favicon_globe" />
diff --git a/mobile/android/base/resources/drawable/toolbar_grey_round.xml b/mobile/android/base/resources/drawable/toolbar_grey_round.xml
new file mode 100644
index 0000000000..ada0146ddf
--- /dev/null
+++ b/mobile/android/base/resources/drawable/toolbar_grey_round.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/toolbar_grey"/>
+ <corners android:radius="@dimen/standard_corner_radius"/>
+</shape>
+
diff --git a/mobile/android/base/resources/drawable/top_sites_thumbnail_bg.xml b/mobile/android/base/resources/drawable/top_sites_thumbnail_bg.xml
new file mode 100644
index 0000000000..9d106441e1
--- /dev/null
+++ b/mobile/android/base/resources/drawable/top_sites_thumbnail_bg.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <size android:height="2dp"
+ android:width="2dp"/>
+ <solid android:color="#FFFFFFFF"/>
+ <stroke android:width="1dp" android:color="#FFDDDDDD"/>
+</shape>
diff --git a/mobile/android/base/resources/drawable/url_bar_bg.xml b/mobile/android/base/resources/drawable/url_bar_bg.xml
new file mode 100644
index 0000000000..52954c1cec
--- /dev/null
+++ b/mobile/android/base/resources/drawable/url_bar_bg.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <!-- private browsing mode -->
+ <item gecko:state_private="true" android:drawable="@color/tabs_tray_grey_pressed"/>
+
+ <!-- normal mode -->
+ <item android:drawable="@color/toolbar_grey"/>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/url_bar_entry.xml b/mobile/android/base/resources/drawable/url_bar_entry.xml
new file mode 100644
index 0000000000..4090ceb8e7
--- /dev/null
+++ b/mobile/android/base/resources/drawable/url_bar_entry.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <!-- private browsing mode -->
+ <item gecko:state_private="true"
+ android:state_focused="true"
+ android:drawable="@drawable/url_bar_entry_pressed_pb"/>
+
+ <item gecko:state_private="true"
+ android:state_pressed="true"
+ android:drawable="@drawable/url_bar_entry_pressed_pb"/>
+
+ <item gecko:state_private="true"
+ android:state_selected="true"
+ android:drawable="@drawable/url_bar_entry_pressed_pb"/>
+
+ <item gecko:state_private="true"
+ android:drawable="@drawable/url_bar_entry_default_pb"/>
+
+ <!-- normal modes -->
+ <item android:state_focused="true"
+ android:drawable="@drawable/url_bar_entry_pressed"/>
+
+ <item android:state_pressed="true"
+ android:drawable="@drawable/url_bar_entry_pressed"/>
+
+ <item android:state_selected="true"
+ android:drawable="@drawable/url_bar_entry_pressed"/>
+
+ <item android:drawable="@drawable/url_bar_entry_default"/>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/url_bar_nav_button.xml b/mobile/android/base/resources/drawable/url_bar_nav_button.xml
new file mode 100644
index 0000000000..2afadaf5e2
--- /dev/null
+++ b/mobile/android/base/resources/drawable/url_bar_nav_button.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- This asset is properly available in large-* dirs so this null
+ reference exists for build time on API 9 builds. -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@null"/>
diff --git a/mobile/android/base/resources/drawable/url_bar_translating_edge.xml b/mobile/android/base/resources/drawable/url_bar_translating_edge.xml
new file mode 100644
index 0000000000..379499284a
--- /dev/null
+++ b/mobile/android/base/resources/drawable/url_bar_translating_edge.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<clip xmlns:android="http://schemas.android.com/apk/res/android"
+ android:drawable="@drawable/url_bar_entry"
+ android:clipOrientation="horizontal"
+ android:gravity="right"/> \ No newline at end of file
diff --git a/mobile/android/base/resources/drawable/widget_button_left.xml b/mobile/android/base/resources/drawable/widget_button_left.xml
new file mode 100644
index 0000000000..9891f8ada9
--- /dev/null
+++ b/mobile/android/base/resources/drawable/widget_button_left.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_pressed="true"
+ android:drawable="@drawable/widget_button_left_pressed"/>
+
+ <!-- The left button is gray in its off state -->
+ <item android:drawable="@drawable/widget_button_left_default"/>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/widget_button_left_default.xml b/mobile/android/base/resources/drawable/widget_button_left_default.xml
new file mode 100644
index 0000000000..7aff6bc1a8
--- /dev/null
+++ b/mobile/android/base/resources/drawable/widget_button_left_default.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- These drawables have to be wrapped in a layer-list in order to produce padding at
+ the bottom of the drawable. That padding ensures the drawable doesn't block the
+ orange strip in widget_bg.9.png -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:bottom="@dimen/widget_bg_border_offset">
+ <shape android:shape="rectangle">
+ <corners android:topLeftRadius="@dimen/widget_drawable_corner_radius"
+ android:topRightRadius="0dp"
+ android:bottomLeftRadius="0dp"
+ android:bottomRightRadius="0dp"/>
+ <solid android:color="@color/toolbar_grey"/>
+ </shape>
+ </item>
+</layer-list>
diff --git a/mobile/android/base/resources/drawable/widget_button_left_pressed.xml b/mobile/android/base/resources/drawable/widget_button_left_pressed.xml
new file mode 100644
index 0000000000..d4ae5a715e
--- /dev/null
+++ b/mobile/android/base/resources/drawable/widget_button_left_pressed.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- These drawables have to be wrapped in a layer-list in order to produce padding at
+ the bottom of the drawable. That padding ensures the drawable doesn't block the
+ orange strip in widget_bg.9.png -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:bottom="@dimen/widget_bg_border_offset">
+ <shape android:shape="rectangle">
+ <corners android:topLeftRadius="@dimen/widget_drawable_corner_radius"
+ android:topRightRadius="0dp"
+ android:bottomLeftRadius="0dp"
+ android:bottomRightRadius="0dp"/>
+ <solid android:color="@color/widget_button_pressed"/>
+ </shape>
+ </item>
+</layer-list>
diff --git a/mobile/android/base/resources/drawable/widget_button_middle.xml b/mobile/android/base/resources/drawable/widget_button_middle.xml
new file mode 100644
index 0000000000..e7d74b0cc1
--- /dev/null
+++ b/mobile/android/base/resources/drawable/widget_button_middle.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_pressed="true"
+ android:drawable="@drawable/widget_button_middle_pressed"/>
+
+ <item android:drawable="@android:color/transparent"/>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/widget_button_middle_pressed.xml b/mobile/android/base/resources/drawable/widget_button_middle_pressed.xml
new file mode 100644
index 0000000000..19236d641b
--- /dev/null
+++ b/mobile/android/base/resources/drawable/widget_button_middle_pressed.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- These drawables have to be wrapped in a layer-list in order to produce padding at
+ the bottom of the drawable. That padding ensures the drawable doesn't block the
+ orange strip in widget_bg.9.png -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:bottom="@dimen/widget_bg_border_offset">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/widget_button_pressed"/>
+ </shape>
+ </item>
+</layer-list>
diff --git a/mobile/android/base/resources/drawable/widget_button_right.xml b/mobile/android/base/resources/drawable/widget_button_right.xml
new file mode 100644
index 0000000000..54fae20185
--- /dev/null
+++ b/mobile/android/base/resources/drawable/widget_button_right.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_pressed="true"
+ android:drawable="@drawable/widget_button_right_pressed"/>
+
+ <item android:drawable="@android:color/transparent"/>
+
+</selector>
diff --git a/mobile/android/base/resources/drawable/widget_button_right_pressed.xml b/mobile/android/base/resources/drawable/widget_button_right_pressed.xml
new file mode 100644
index 0000000000..2ebf614898
--- /dev/null
+++ b/mobile/android/base/resources/drawable/widget_button_right_pressed.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- These drawables have to be wrapped in a layer-list in order to produce padding at
+ the bottom of the drawable. That padding ensures the drawable doesn't block the
+ orange strip in widget_bg.9.png -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:bottom="@dimen/widget_bg_border_offset">
+ <shape android:shape="rectangle">
+ <corners android:topLeftRadius="0dp"
+ android:topRightRadius="@dimen/widget_drawable_corner_radius"
+ android:bottomLeftRadius="0dp"
+ android:bottomRightRadius="0dp"/>
+ <solid android:color="@color/widget_button_pressed"/>
+ </shape>
+ </item>
+</layer-list>
diff --git a/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml b/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml
new file mode 100644
index 0000000000..77a968369f
--- /dev/null
+++ b/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml
@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <ImageView android:id="@+id/url_bar_entry"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignLeft="@+id/back"
+ android:layout_toLeftOf="@id/menu_items"
+ android:layout_marginLeft="@dimen/tablet_nav_button_width_half"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:duplicateParentState="true"
+ android:clickable="false"
+ android:focusable="false"
+ android:background="@drawable/url_bar_entry"/>
+
+ <!-- The attributes statically defined here are for the expanded
+ forward button. We translate/hide the forward button in code -
+ see BrowserToolbarTablet.animateForwardButton.
+
+ (for alpha) We want the button hidden to start so alpha=0.
+
+ (for layout_width) The visible area of the forward button is a
+ nav_button_width and the non-visible area slides halfway
+ under the back button. This non-visible area is used to
+ ensure the forward button background fully covers the space
+ to the right of the back button.
+
+ (for layout_marginLeft) We left align with back,
+ but only need to hide halfway underneath.
+
+ (for paddingLeft) We use left padding to center the
+ arrow in the visible area as opposed to the true width. -->
+ <org.mozilla.gecko.toolbar.ForwardButton
+ style="@style/UrlBar.ImageButton.BrowserToolbarColors"
+ android:id="@+id/forward"
+ android:layout_alignLeft="@id/back"
+ android:contentDescription="@string/forward"
+ android:layout_height="match_parent"
+ android:paddingTop="0dp"
+ android:paddingBottom="0dp"
+ android:layout_marginTop="11.5dp"
+ android:layout_marginBottom="11.5dp"
+ android:layout_gravity="center_vertical"
+ android:layout_centerVertical="true"
+ android:src="@drawable/ic_menu_forward"
+ android:background="@drawable/url_bar_nav_button"
+ android:alpha="0"
+ android:layout_width="@dimen/tablet_nav_button_width_plus_half"
+ android:layout_marginLeft="@dimen/tablet_nav_button_width_half"
+ android:paddingLeft="18dp"/>
+
+ <org.mozilla.gecko.toolbar.BackButton android:id="@id/back"
+ style="@style/UrlBar.ImageButton.BrowserToolbarColors"
+ android:layout_width="@dimen/tablet_nav_button_width"
+ android:layout_height="@dimen/tablet_nav_button_width"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="12dp"
+ android:layout_alignParentLeft="true"
+ android:src="@drawable/ic_menu_back"
+ android:contentDescription="@string/back"
+ android:background="@drawable/url_bar_nav_button"/>
+
+ <org.mozilla.gecko.toolbar.ToolbarEditLayout android:id="@+id/edit_layout"
+ style="@style/UrlBar.Button"
+ android:paddingRight="12dp"
+ android:visibility="gone"
+ android:orientation="horizontal"
+ android:layout_toRightOf="@id/back"
+ android:layout_toLeftOf="@id/menu_items"/>
+
+ <!-- Note: we set the padding on the site security icon to increase its tappable area. -->
+ <org.mozilla.gecko.toolbar.ToolbarDisplayLayout android:id="@+id/display_layout"
+ style="@style/UrlBar.Button.Container"
+ android:layout_toRightOf="@id/back"
+ android:layout_toLeftOf="@id/menu_items"
+ android:paddingRight="4dip"/>
+
+ <LinearLayout android:id="@+id/menu_items"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:layout_marginLeft="6dp"
+ android:orientation="horizontal"
+ android:layout_toLeftOf="@id/tabs"/>
+
+ <org.mozilla.gecko.widget.themed.ThemedImageButton
+ android:id="@+id/tabs"
+ style="@style/UrlBar.ImageButton"
+ android:layout_toLeftOf="@id/menu"
+ android:layout_alignWithParentIfMissing="true"
+ android:background="@drawable/browser_toolbar_action_bar_button"/>
+
+ <!-- In a 56x60dp space, centering 24dp image will leave 16x18dp. -->
+ <org.mozilla.gecko.toolbar.TabCounter android:id="@+id/tabs_counter"
+ style="@style/UrlBar.ImageButton"
+ android:layout_alignLeft="@id/tabs"
+ android:layout_alignRight="@id/tabs"
+ android:layout_alignTop="@id/tabs"
+ android:layout_alignBottom="@id/tabs"
+ android:layout_marginTop="18dp"
+ android:layout_marginBottom="18dp"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ android:background="@drawable/tabs_count"/>
+
+ <!-- Bug 1144707. Use clickable View instead of menu button margin to prevent
+ edit mode actiivation when user clicks on the edge of the screen. -->
+ <View android:id="@id/menu_margin"
+ android:layout_width="6dp"
+ android:layout_height="match_parent"
+ android:layout_alignParentRight="true"
+ android:clickable="true"
+ android:visibility="gone"/>
+
+ <org.mozilla.gecko.widget.themed.ThemedFrameLayout
+ android:id="@+id/menu"
+ style="@style/UrlBar.ImageButton"
+ android:layout_toLeftOf="@id/menu_margin"
+ android:layout_alignWithParentIfMissing="true"
+ android:contentDescription="@string/menu"
+ android:background="@drawable/browser_toolbar_action_bar_button">
+
+ <org.mozilla.gecko.widget.themed.ThemedImageView
+ android:id="@+id/menu_icon"
+ style="@style/UrlBar.ImageButton.BrowserToolbarColors"
+ android:layout_height="@dimen/browser_toolbar_menu_icon_height"
+ android:layout_width="wrap_content"
+ android:scaleType="centerInside"
+ android:src="@drawable/menu"
+ android:layout_gravity="center"/>
+
+ </org.mozilla.gecko.widget.themed.ThemedFrameLayout>
+
+ <!-- We draw after the menu items so when they are hidden, the cancel button,
+ which is thus drawn on top, may be pressed. -->
+ <org.mozilla.gecko.widget.themed.ThemedImageView
+ android:id="@+id/edit_cancel"
+ style="@style/UrlBar.ImageButton"
+ android:layout_width="@dimen/browser_toolbar_icon_width"
+ android:layout_height="@dimen/browser_toolbar_height"
+ android:layout_weight="0.0"
+ android:layout_alignParentRight="true"
+ android:src="@drawable/close_edit_mode_selector"
+ android:contentDescription="@string/edit_mode_cancel"
+ android:visibility="gone"/>
+
+</merge>
diff --git a/mobile/android/base/resources/layout-large-v11/tabs_counter.xml b/mobile/android/base/resources/layout-large-v11/tabs_counter.xml
new file mode 100644
index 0000000000..df771a2317
--- /dev/null
+++ b/mobile/android/base/resources/layout-large-v11/tabs_counter.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<org.mozilla.gecko.widget.themed.ThemedTextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="24dip"
+ android:layout_height="24dip"
+ android:paddingTop="3dip"
+ android:paddingLeft="4dip"
+ android:background="@drawable/tabs_count_foreground"
+ android:textAppearance="@style/TextAppearance.Micro"
+ android:textColor="@color/tabs_counter_text_color"
+ android:textStyle="bold"
+ android:duplicateParentState="true"
+ android:gravity="center"/>
diff --git a/mobile/android/base/resources/layout-xlarge-v11/font_size_preference.xml b/mobile/android/base/resources/layout-xlarge-v11/font_size_preference.xml
new file mode 100644
index 0000000000..a406b55a2a
--- /dev/null
+++ b/mobile/android/base/resources/layout-xlarge-v11/font_size_preference.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <ScrollView android:id="@+id/scrolling_container"
+ android:layout_width="match_parent"
+ android:layout_height="350dp"
+ android:layout_margin="8dp"
+ android:padding="8dp"
+ android:scrollbars="vertical"
+ android:scrollbarStyle="outsideOverlay"
+ android:fadeScrollbars="true"
+ android:requiresFadingEdge="vertical">
+
+ <TextView android:id="@+id/preview"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/pref_font_size_preview_text"
+ android:textColor="#ff000000"/>
+
+ </ScrollView>
+
+ <LinearLayout android:id="@+id/button_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:layout_marginRight="4dp"
+ android:orientation="horizontal">
+
+ <Button android:id="@+id/decrease_preview_font_button"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:text="@string/pref_font_size_adjust_char"
+ android:textSize="8sp"/>
+
+ <Button android:id="@+id/increase_preview_font_button"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:text="@string/pref_font_size_adjust_char"
+ android:textSize="16sp"/>
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/actionbar.xml b/mobile/android/base/resources/layout/actionbar.xml
new file mode 100644
index 0000000000..ecb5124dee
--- /dev/null
+++ b/mobile/android/base/resources/layout/actionbar.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <Button android:id="@+id/actionmode_title"
+ android:layout_height="match_parent"
+ android:layout_width="wrap_content"
+ style="@style/GeckoActionBar.Title"/>
+
+ <!-- Draw a separator to the left of the title -->
+ <View android:layout_height="match_parent"
+ android:layout_width="1dp"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="10dp"
+ android:background="@color/text_color_secondary_inverse"/>
+
+ <LinearLayout android:id="@+id/actionbar_buttons"
+ android:layout_height="match_parent"
+ android:layout_width="0dip"
+ android:layout_weight="1"
+ style="@style/GeckoActionBar.Buttons"/>
+
+ <ImageButton android:id="@+id/actionbar_menu"
+ android:layout_height="match_parent"
+ android:layout_width="@dimen/browser_toolbar_icon_width"
+ style="@style/GeckoActionBar.Button.MenuButton"/>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/activity_stream.xml b/mobile/android/base/resources/layout/activity_stream.xml
new file mode 100644
index 0000000000..b40c01cdee
--- /dev/null
+++ b/mobile/android/base/resources/layout/activity_stream.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.mozilla.gecko.home.activitystream.ActivityStreamHomeScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="#FAFAFA"/>
diff --git a/mobile/android/base/resources/layout/activity_stream_card_history_item.xml b/mobile/android/base/resources/layout/activity_stream_card_history_item.xml
new file mode 100644
index 0000000000..7f411278d0
--- /dev/null
+++ b/mobile/android/base/resources/layout/activity_stream_card_history_item.xml
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="utf-8"?><!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="10dp"
+ android:layout_marginEnd="@dimen/activity_stream_base_margin"
+ android:layout_marginLeft="@dimen/activity_stream_base_margin"
+ android:layout_marginRight="@dimen/activity_stream_base_margin"
+ android:layout_marginStart="@dimen/activity_stream_base_margin"
+ android:layout_marginTop="0dp"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:background="?android:attr/selectableItemBackground">
+
+ <org.mozilla.gecko.widget.FaviconView
+ android:id="@+id/icon"
+ android:layout_width="@dimen/favicon_bg"
+ android:layout_height="@dimen/favicon_bg"
+ android:layout_marginLeft="@dimen/activity_stream_base_margin"
+ android:layout_marginStart="@dimen/activity_stream_base_margin"
+ android:layout_marginTop="@dimen/activity_stream_base_margin"
+ android:layout_marginBottom="@dimen/activity_stream_base_margin"
+ android:layout_gravity="center"
+ gecko:enableRoundCorners="false"
+ tools:background="@drawable/favicon_globe" />
+
+ <ImageView
+ android:id="@+id/menu"
+ android:layout_width="wrap_content"
+ android:layout_height="36dp"
+ android:layout_margin="2dp"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:layout_gravity="right|top"
+ android:contentDescription="@string/menu"
+ android:src="@drawable/menu"
+ android:padding="@dimen/activity_stream_base_margin" />
+
+ <TextView
+ android:id="@+id/page"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ tools:text="twitter"
+ android:textSize="12sp"
+ android:textColor="@color/activity_stream_subtitle"
+ android:layout_toRightOf="@id/icon"
+ android:layout_toEndOf="@id/icon"
+ android:layout_toLeftOf="@id/menu"
+ android:layout_toStartOf="@id/menu"
+ android:paddingTop="@dimen/activity_stream_base_margin"
+ android:paddingLeft="@dimen/activity_stream_base_margin"
+ android:paddingStart="@dimen/activity_stream_base_margin"/>
+
+ <TextView
+ android:id="@+id/card_history_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_toEndOf="@id/icon"
+ android:layout_toRightOf="@id/icon"
+ android:maxLines="3"
+ android:ellipsize="end"
+ android:paddingLeft="@dimen/activity_stream_base_margin"
+ android:paddingStart="@dimen/activity_stream_base_margin"
+ android:textSize="14sp"
+ android:textStyle="bold"
+ android:textColor="#ff000000"
+ android:layout_below="@id/page"
+ android:layout_toLeftOf="@id/menu"
+ android:layout_toStartOf="@id/menu"
+ tools:text="Descriptive title of a page that is veeeeeeery long - maybe even too long?" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_toRightOf="@id/icon"
+ android:layout_toEndOf="@id/icon"
+ android:layout_alignParentBottom="true"
+ android:paddingLeft="@dimen/activity_stream_base_margin"
+ android:paddingStart="@dimen/activity_stream_base_margin"
+ android:paddingRight="@dimen/activity_stream_base_margin"
+ android:paddingEnd="@dimen/activity_stream_base_margin"
+ android:paddingTop="4dp"
+ android:paddingBottom="@dimen/activity_stream_base_margin"
+ android:gravity="center_vertical"
+ android:layout_below="@id/card_history_label">
+
+ <ImageView
+ android:id="@+id/source_icon"
+ android:layout_width="12dp"
+ android:layout_height="12dp" />
+
+ <TextView
+ android:id="@+id/card_history_source"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="2dp"
+ android:textSize="12sp"
+ android:layout_weight="1"
+ android:textColor="@color/activity_stream_subtitle"
+ tools:text="Bookmarked" />
+
+ <TextView
+ android:id="@+id/card_history_time_since"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="12sp"
+ android:textColor="@color/activity_stream_timestamp"
+ tools:text="20m" />
+
+ </LinearLayout>
+ </RelativeLayout>
+</android.support.v7.widget.CardView>
diff --git a/mobile/android/base/resources/layout/activity_stream_contextmenu_bottomsheet.xml b/mobile/android/base/resources/layout/activity_stream_contextmenu_bottomsheet.xml
new file mode 100644
index 0000000000..327587e6e2
--- /dev/null
+++ b/mobile/android/base/resources/layout/activity_stream_contextmenu_bottomsheet.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:id="@+id/info_wrapper"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="10dp">
+
+ <org.mozilla.gecko.widget.FaviconView
+ android:id="@+id/icon"
+ android:layout_width="@dimen/favicon_bg"
+ android:layout_height="@dimen/favicon_bg"
+ android:layout_gravity="center"
+ gecko:enableRoundCorners="false"
+ tools:background="@drawable/favicon_globe"/>
+
+ <TextView
+ android:id="@+id/url"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_toEndOf="@id/icon"
+ android:layout_toRightOf="@id/icon"
+ android:paddingLeft="@dimen/activity_stream_base_margin"
+ android:paddingStart="@dimen/activity_stream_base_margin"
+ android:textColor="@color/activity_stream_subtitle"
+ android:textSize="12sp"
+ tools:text="twitter"/>
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/url"
+ android:layout_toEndOf="@id/icon"
+ android:layout_toRightOf="@id/icon"
+ android:ellipsize="end"
+ android:maxLines="3"
+ android:paddingLeft="@dimen/activity_stream_base_margin"
+ android:paddingStart="@dimen/activity_stream_base_margin"
+ android:textColor="#ff000000"
+ android:textSize="14sp"
+ android:textStyle="bold"
+ tools:text="Descriptive title of a page that is veeeeeeery long - maybe even too long?"/>
+ </RelativeLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="0.5dp"
+ android:layout_marginTop="4dp"
+ android:background="@color/disabled_grey"
+ android:padding="4dp"/>
+
+ <android.support.v4.widget.NestedScrollView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <android.support.design.widget.NavigationView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/menu"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:itemTextAppearance="@style/ActivityStreamContextMenuText"
+ android:theme="@style/ActivityStreamContextMenuStyle"
+ app:menu="@menu/activitystream_contextmenu"/>
+
+ </android.support.v4.widget.NestedScrollView>
+
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/activity_stream_contextmenu_popupmenu.xml b/mobile/android/base/resources/layout/activity_stream_contextmenu_popupmenu.xml
new file mode 100644
index 0000000000..a21d675285
--- /dev/null
+++ b/mobile/android/base/resources/layout/activity_stream_contextmenu_popupmenu.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="32dp"
+ android:layout_height="200dp"
+ app:cardElevation="5dp"
+ app:cardUseCompatPadding="true">
+
+ <!-- This is mostly a copy of the same menu in activity_stream_contextmenu_bottomsheet.xml,
+ however for the popup menu we don't need to override the dividers, hence we omit the
+ android:theme override -->
+ <android.support.design.widget.NavigationView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/menu"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:itemTextAppearance="@style/ActivityStreamContextMenuText"
+ app:menu="@menu/activitystream_contextmenu"/>
+
+</android.support.v7.widget.CardView> \ No newline at end of file
diff --git a/mobile/android/base/resources/layout/activity_stream_main_highlightstitle.xml b/mobile/android/base/resources/layout/activity_stream_main_highlightstitle.xml
new file mode 100644
index 0000000000..7338c85968
--- /dev/null
+++ b/mobile/android/base/resources/layout/activity_stream_main_highlightstitle.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <View
+ android:id="@+id/divider"
+ android:layout_width="match_parent"
+ android:layout_height="0.5dp"
+ android:padding="4dp"
+ android:background="#ffe0e0e0" />
+
+ <TextView
+ android:id="@+id/title_highlights"
+ android:layout_marginLeft="@dimen/activity_stream_base_margin"
+ android:layout_marginStart="@dimen/activity_stream_base_margin"
+ android:layout_marginTop="@dimen/activity_stream_base_margin"
+ android:layout_marginBottom="@dimen/activity_stream_base_margin"
+ android:layout_marginRight="@dimen/activity_stream_base_margin"
+ android:layout_marginEnd="@dimen/activity_stream_base_margin"
+ android:text="@string/activity_stream_highlights"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textStyle="bold"
+ android:textSize="16sp"
+ android:textColor="#FF858585" />
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/activity_stream_main_toppanel.xml b/mobile/android/base/resources/layout/activity_stream_main_toppanel.xml
new file mode 100644
index 0000000000..60c420063c
--- /dev/null
+++ b/mobile/android/base/resources/layout/activity_stream_main_toppanel.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <android.support.v4.view.ViewPager
+ android:layout_marginTop="10dp"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/topsites_pager"
+ android:contentDescription="@string/activity_stream_topsites" />
+
+ <org.mozilla.gecko.home.activitystream.topsites.CirclePageIndicator
+ android:id="@+id/topsites_indicator"
+ android:padding="10dip"
+ app:fillColor="#ff9d9d9d"
+ app:pageColor="#FFFFFF"
+ app:strokeWidth="1dp"
+ app:radius="2dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/activity_stream_topsites_card.xml b/mobile/android/base/resources/layout/activity_stream_topsites_card.xml
new file mode 100644
index 0000000000..8cb288b2f1
--- /dev/null
+++ b/mobile/android/base/resources/layout/activity_stream_topsites_card.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.mozilla.gecko.widget.FilledCardView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <RelativeLayout
+ android:id="@+id/content"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground">
+
+ <org.mozilla.gecko.widget.FaviconView
+ android:id="@+id/favicon"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_above="@+id/title"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
+ android:layout_gravity="center"
+ gecko:enableRoundCorners="false"
+ tools:background="@drawable/favicon_globe" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="0.5dp"
+ android:layout_below="@id/favicon"
+ android:background="@color/activity_stream_divider" />
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentStart="true"
+ android:ellipsize="end"
+ android:gravity="center"
+ android:lines="1"
+ android:padding="4dp"
+ android:textSize="12sp"
+ android:textColor="@android:color/black"
+ tools:text="Lorem Ipsum here is a title" />
+
+ <ImageView
+ android:id="@+id/menu"
+ android:layout_width="wrap_content"
+ android:layout_height="28dp"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:layout_gravity="right|top"
+ android:padding="6dp"
+ android:contentDescription="@string/menu"
+ android:src="@drawable/menu" />
+
+ </RelativeLayout>
+</org.mozilla.gecko.widget.FilledCardView> \ No newline at end of file
diff --git a/mobile/android/base/resources/layout/activity_stream_topsites_page.xml b/mobile/android/base/resources/layout/activity_stream_topsites_page.xml
new file mode 100644
index 0000000000..78399f7fce
--- /dev/null
+++ b/mobile/android/base/resources/layout/activity_stream_topsites_page.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.mozilla.gecko.home.activitystream.topsites.TopSitesPage xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:paddingLeft="10dp"/>
diff --git a/mobile/android/base/resources/layout/anchored_popup.xml b/mobile/android/base/resources/layout/anchored_popup.xml
new file mode 100644
index 0000000000..bc4b61493c
--- /dev/null
+++ b/mobile/android/base/resources/layout/anchored_popup.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/dropshadow"
+ android:paddingLeft="3dp"
+ android:paddingRight="3dp"
+ android:paddingTop="3dp"
+ android:paddingBottom="4dp">
+
+ <ScrollView android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <org.mozilla.gecko.widget.RoundedCornerLayout android:id="@+id/content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/toolbar_grey"
+ android:orientation="vertical"/>
+
+ </ScrollView>
+</FrameLayout>
diff --git a/mobile/android/base/resources/layout/as_content.xml b/mobile/android/base/resources/layout/as_content.xml
new file mode 100644
index 0000000000..780d00beb7
--- /dev/null
+++ b/mobile/android/base/resources/layout/as_content.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/activity_stream_main_recyclerview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+</merge> \ No newline at end of file
diff --git a/mobile/android/base/resources/layout/autocomplete_list.xml b/mobile/android/base/resources/layout/autocomplete_list.xml
new file mode 100644
index 0000000000..78487d42c7
--- /dev/null
+++ b/mobile/android/base/resources/layout/autocomplete_list.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<ListView xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@android:style/Widget.Holo.ListView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/autocomplete_list_bg"
+ android:cacheColorHint="#ffffff"/>
diff --git a/mobile/android/base/resources/layout/autocomplete_list_item.xml b/mobile/android/base/resources/layout/autocomplete_list_item.xml
new file mode 100644
index 0000000000..b8bbf6fd51
--- /dev/null
+++ b/mobile/android/base/resources/layout/autocomplete_list_item.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="32dip"
+ android:textAppearance="@style/TextAppearance.Medium"
+ android:layout_gravity="center_vertical"
+ android:paddingLeft="10dp"
+ android:paddingRight="10dp"
+ android:paddingTop="3dp"
+ android:paddingBottom="3dp"/>
diff --git a/mobile/android/base/resources/layout/basic_color_picker_dialog.xml b/mobile/android/base/resources/layout/basic_color_picker_dialog.xml
new file mode 100644
index 0000000000..4efb341d52
--- /dev/null
+++ b/mobile/android/base/resources/layout/basic_color_picker_dialog.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:orientation="vertical">
+
+ <org.mozilla.gecko.widget.BasicColorPicker android:id="@+id/colorpicker"
+ android:layout_height="0dip"
+ android:layout_weight="1"
+ android:drawSelectorOnTop="true"
+ android:choiceMode="singleChoice"
+ android:divider="@android:color/transparent"
+ android:dividerHeight="0dip"
+ android:listSelector="#22FFFFFF"
+ android:layout_width="match_parent"/>
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/bookmark_edit.xml b/mobile/android/base/resources/layout/bookmark_edit.xml
new file mode 100644
index 0000000000..cf59096c84
--- /dev/null
+++ b/mobile/android/base/resources/layout/bookmark_edit.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:orientation="vertical"
+ android:layout_height="match_parent">
+
+ <android.support.design.widget.TextInputLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <EditText
+ android:id="@+id/edit_bookmark_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:hint="@string/bookmark_edit_name"
+ />
+ </android.support.design.widget.TextInputLayout>
+
+ <android.support.design.widget.TextInputLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <EditText
+ android:id="@+id/edit_bookmark_location"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:hint="@string/bookmark_edit_location"
+ android:inputType="textNoSuggestions"/>
+ </android.support.design.widget.TextInputLayout>
+
+ <android.support.design.widget.TextInputLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <EditText
+ android:id="@+id/edit_bookmark_keyword"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:hint="@string/bookmark_edit_keyword"/>
+ </android.support.design.widget.TextInputLayout>
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/bookmark_folder_row.xml b/mobile/android/base/resources/layout/bookmark_folder_row.xml
new file mode 100644
index 0000000000..dcf9620cd0
--- /dev/null
+++ b/mobile/android/base/resources/layout/bookmark_folder_row.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<org.mozilla.gecko.home.BookmarkFolderView xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/Widget.FolderView"
+ android:layout_width="match_parent"
+ android:paddingLeft="0dp"
+ android:paddingTop="0dp"
+ android:paddingBottom="0dp"
+ android:paddingRight="16dp"
+ android:gravity="center_vertical"/>
diff --git a/mobile/android/base/resources/layout/bookmark_item_row.xml b/mobile/android/base/resources/layout/bookmark_item_row.xml
new file mode 100644
index 0000000000..71d4f532ff
--- /dev/null
+++ b/mobile/android/base/resources/layout/bookmark_item_row.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<org.mozilla.gecko.home.TwoLinePageRow xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/Widget.BookmarkItemView"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/page_row_height"
+ android:minHeight="@dimen/page_row_height"/>
diff --git a/mobile/android/base/resources/layout/bookmark_screenshot_row.xml b/mobile/android/base/resources/layout/bookmark_screenshot_row.xml
new file mode 100644
index 0000000000..cdb3912c2b
--- /dev/null
+++ b/mobile/android/base/resources/layout/bookmark_screenshot_row.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<org.mozilla.gecko.home.BookmarkScreenshotRow
+ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:gecko="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/page_row_height"
+ android:minHeight="@dimen/page_row_height"
+ android:orientation="vertical"
+ android:gravity="center_vertical"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+>
+
+ <org.mozilla.gecko.widget.FadedSingleColorTextView
+ android:id="@+id/title"
+ style="@style/Widget.TwoLinePageRow.Title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ gecko:fadeWidth="30dp"
+ />
+
+ <org.mozilla.gecko.widget.FadedSingleColorTextView
+ android:id="@+id/date"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.Widget.Home.ItemDescription"
+ android:textColor="@color/link_blue"
+ android:singleLine="true"
+ gecko:fadeWidth="30dp"
+ />
+
+</org.mozilla.gecko.home.BookmarkScreenshotRow>
diff --git a/mobile/android/base/resources/layout/browser_search.xml b/mobile/android/base/resources/layout/browser_search.xml
new file mode 100644
index 0000000000..44ff340cf1
--- /dev/null
+++ b/mobile/android/base/resources/layout/browser_search.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ViewStub android:id="@+id/suggestions_opt_in_prompt"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout="@layout/home_suggestion_prompt" />
+
+ <view class="org.mozilla.gecko.home.BrowserSearch$HomeSearchListView"
+ android:id="@+id/home_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+
+ <!-- listSelector is too slow for showing pressed state
+ so we set the pressed colors on the child. -->
+ <org.mozilla.gecko.home.SearchEngineBar
+ android:id="@+id/search_engine_bar"
+ android:layout_width="match_parent"
+ android:layout_height="48dp"
+ android:paddingTop="1dp"
+ android:orientation="horizontal"
+ android:layout_gravity="center_horizontal"
+ android:choiceMode="singleChoice"
+ android:listSelector="@android:color/transparent"
+ android:cacheColorHint="@android:color/transparent" />
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/browser_toolbar.xml b/mobile/android/base/resources/layout/browser_toolbar.xml
new file mode 100644
index 0000000000..0413215f88
--- /dev/null
+++ b/mobile/android/base/resources/layout/browser_toolbar.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!-- Note: any layout parameters setting the right edge of
+ this View should be matched in the url_bar_translating_edge. -->
+ <ImageView android:id="@+id/url_bar_entry"
+ style="@style/UrlBar.Button"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="-6dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:layout_toLeftOf="@+id/tabs"
+ android:duplicateParentState="true"
+ android:clickable="false"
+ android:focusable="false"
+ android:src="@drawable/url_bar_entry"
+ android:scaleType="fitXY"/>
+
+ <!-- A View that clips with url_bar_entry and translates
+ around it to animate growing the url bar,
+ which occurs in the display/editing mode transitions. -->
+ <ImageView android:id="@+id/url_bar_translating_edge"
+ style="@style/UrlBar.Button"
+ android:layout_alignLeft="@id/url_bar_entry"
+ android:layout_alignRight="@+id/url_bar_entry"
+ android:layout_alignTop="@id/url_bar_entry"
+ android:layout_alignBottom="@id/url_bar_entry"
+ android:duplicateParentState="true"
+ android:clickable="false"
+ android:focusable="false"
+ android:visibility="invisible"
+ android:src="@drawable/url_bar_translating_edge"
+ android:scaleType="fitXY"/>
+
+ <org.mozilla.gecko.toolbar.ShapedButtonFrameLayout
+ android:id="@+id/menu"
+ style="@style/UrlBar.ImageButton"
+ android:layout_alignParentRight="true"
+ android:contentDescription="@string/menu"
+ android:background="@drawable/shaped_button">
+
+ <org.mozilla.gecko.widget.themed.ThemedImageView
+ android:id="@+id/menu_icon"
+ style="@style/UrlBar.ImageButton"
+ android:layout_height="@dimen/browser_toolbar_menu_icon_height"
+ android:layout_width="wrap_content"
+ android:scaleType="centerInside"
+ android:layout_gravity="center"
+ android:src="@drawable/menu"
+ android:tint="@color/tabs_tray_icon_grey"/>
+
+ </org.mozilla.gecko.toolbar.ShapedButtonFrameLayout>
+
+ <org.mozilla.gecko.toolbar.PhoneTabsButton android:id="@+id/tabs"
+ style="@style/UrlBar.ImageButton"
+ android:layout_width="64dip"
+ android:layout_toLeftOf="@id/menu"
+ android:layout_alignWithParentIfMissing="true"
+ android:background="@drawable/shaped_button"/>
+
+ <!-- The TextSwitcher should be shifted 24dp on the left, to avoid
+ the curve. On a 48dp space, centering 24dp image will leave
+ 12dp on all sides. However this image has a perception of
+ 2 layers. Hence to center this, an additional 4dp is added to the left.
+ The margins will be 40dp on left, 8dp on right, instead of ideal 30dp
+ and 12dp. -->
+ <org.mozilla.gecko.toolbar.TabCounter android:id="@+id/tabs_counter"
+ style="@style/UrlBar.ImageButton"
+ android:layout_width="24dip"
+ android:layout_height="24dip"
+ android:layout_centerVertical="true"
+ android:layout_marginRight="8dip"
+ android:layout_alignRight="@id/tabs"
+ android:background="@drawable/tabs_count"
+ android:gravity="center_horizontal"
+ android:clipChildren="false"
+ android:clipToPadding="false"/>
+
+ <!-- Note that the edit components are invisible so that the views
+ depending on their location can properly layout. -->
+ <org.mozilla.gecko.widget.themed.ThemedImageView
+ android:id="@+id/edit_cancel"
+ style="@style/UrlBar.ImageButton"
+ android:layout_alignParentRight="true"
+ android:src="@drawable/close_edit_mode_selector"
+ android:contentDescription="@string/edit_mode_cancel"
+ android:background="@drawable/action_bar_button"
+ android:visibility="invisible"/>
+
+ <!-- The space to the left of the cancel button would be larger than the right because
+ the url bar drawable contains some whitespace, so we compensate by removing
+ some padding from the right (value determined through experimentation). -->
+ <org.mozilla.gecko.toolbar.ToolbarEditLayout android:id="@+id/edit_layout"
+ style="@style/UrlBar.Button"
+ android:layout_alignLeft="@id/url_bar_entry"
+ android:layout_toLeftOf="@id/edit_cancel"
+ android:visibility="invisible"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"/>
+
+ <org.mozilla.gecko.toolbar.ToolbarDisplayLayout android:id="@+id/display_layout"
+ style="@style/UrlBar.Button"
+ android:layout_alignLeft="@id/url_bar_entry"
+ android:layout_alignRight="@id/url_bar_entry"
+ android:paddingLeft="1dip"
+ android:paddingRight="4dip"/>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/button_toast.xml b/mobile/android/base/resources/layout/button_toast.xml
new file mode 100644
index 0000000000..0ca5903656
--- /dev/null
+++ b/mobile/android/base/resources/layout/button_toast.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/toast"
+ style="@style/Toast">
+
+ <TextView android:id="@+id/toast_message"
+ style="@style/ToastMessage" />
+
+ <View android:id="@+id/toast_divider"
+ style="@style/ToastDivider" />
+
+ <Button android:id="@+id/toast_button"
+ style="@style/ToastButton" />
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/color_picker_row.xml b/mobile/android/base/resources/layout/color_picker_row.xml
new file mode 100644
index 0000000000..716383f501
--- /dev/null
+++ b/mobile/android/base/resources/layout/color_picker_row.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.Widget.TextView"
+ style="@style/Widget.ListItem"
+ android:background="@drawable/color_picker_row_bg"
+ android:checkMark="@android:color/transparent"/>
diff --git a/mobile/android/base/resources/layout/customtabs_activity.xml b/mobile/android/base/resources/layout/customtabs_activity.xml
new file mode 100644
index 0000000000..7ba9c07f68
--- /dev/null
+++ b/mobile/android/base/resources/layout/customtabs_activity.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/root_layout"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <!--
+ This layout is quite complex because GeckoApp accesses all view groups
+ in this tree. In a perfect world this should just include a GeckoView.
+ -->
+
+ <android.support.v7.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ android:elevation="4dp"
+ android:background="@color/text_and_tabs_tray_grey"
+ app:layout_scrollFlags="scroll|enterAlways"/>
+
+ <view class="org.mozilla.gecko.GeckoApp$MainLayout"
+ android:id="@+id/main_layout"
+ android:layout_width="match_parent"
+ android:layout_below="@+id/toolbar"
+ android:layout_height="match_parent"
+ android:background="@android:color/transparent">
+
+ <RelativeLayout android:id="@+id/gecko_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_below="@+id/tablet_tab_strip"
+ android:layout_above="@+id/find_in_page">
+
+ <fragment class="org.mozilla.gecko.GeckoViewFragment"
+ android:id="@+id/layer_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbars="none"/>
+
+ </RelativeLayout>
+
+ </view>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/mobile/android/base/resources/layout/datetime_picker.xml b/mobile/android/base/resources/layout/datetime_picker.xml
new file mode 100644
index 0000000000..66826f5896
--- /dev/null
+++ b/mobile/android/base/resources/layout/datetime_picker.xml
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 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.
+*/
+-->
+
+<!-- Layout of date picker-->
+
+<!-- Warning: everything within the "pickers" layout is removed and re-ordered
+ depending on the date format selected by the user.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/datetime_picker"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:orientation="vertical"
+ android:gravity="center">
+
+ <LinearLayout android:id="@+id/date_spinners"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_marginLeft="1dip"
+ android:layout_marginRight="1dip"
+ android:gravity="center">
+
+ <!-- Month -->
+ <android.widget.NumberPicker
+ android:id="@+id/month"
+ android:layout_width="60dip"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="1dip"
+ android:layout_marginRight="1dip"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ />
+
+ <!-- Week -->
+ <android.widget.NumberPicker
+ android:id="@+id/week"
+ android:layout_width="60dip"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="1dip"
+ android:layout_marginRight="1dip"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ />
+
+ <!-- Day -->
+ <android.widget.NumberPicker
+ android:id="@+id/day"
+ android:layout_width="60dip"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="1dip"
+ android:layout_marginRight="1dip"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ />
+
+ <!-- Year -->
+ <android.widget.NumberPicker
+ android:id="@+id/year"
+ android:layout_width="75dip"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="1dip"
+ android:layout_marginRight="1dip"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ />
+
+ </LinearLayout>
+
+ <LinearLayout android:id="@+id/time_spinners"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_marginLeft="1dip"
+ android:layout_marginRight="1dip"
+ android:gravity="center">
+
+ <!-- Hour -->
+ <android.widget.NumberPicker
+ android:id="@+id/hour"
+ android:layout_width="60dip"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="1dip"
+ android:layout_marginRight="1dip"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ />
+
+ <TextView android:id="@+id/mincolon"
+ android:text="@string/colon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="1dip"
+ android:layout_marginRight="1dip"/>
+
+ <!-- Minute -->
+ <android.widget.NumberPicker
+ android:id="@+id/minute"
+ android:layout_width="60dip"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="1dip"
+ android:layout_marginRight="1dip"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ />
+
+ <!-- AMPM -->
+ <android.widget.NumberPicker
+ android:id="@+id/ampm"
+ android:layout_width="60dip"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="1dip"
+ android:layout_marginRight="1dip"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ />
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/default_doorhanger.xml b/mobile/android/base/resources/layout/default_doorhanger.xml
new file mode 100644
index 0000000000..fd8bf09aaf
--- /dev/null
+++ b/mobile/android/base/resources/layout/default_doorhanger.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView android:id="@+id/doorhanger_message"
+ android:focusable="true"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.DoorHanger.Medium"/>
+
+ <LinearLayout android:id="@+id/doorhanger_inputs"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_marginTop="@dimen/doorhanger_section_padding_medium"
+ android:gravity="right"
+ android:visibility="gone"/>
+
+ <android.support.v7.widget.AppCompatCheckBox
+ android:id="@+id/doorhanger_checkbox"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/doorhanger_section_padding_medium"
+ android:checked="true"
+ android:textColor="@color/placeholder_active_grey"
+ android:visibility="gone"/>
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/doorhanger.xml b/mobile/android/base/resources/layout/doorhanger.xml
new file mode 100644
index 0000000000..f3ab4e1fe1
--- /dev/null
+++ b/mobile/android/base/resources/layout/doorhanger.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <LinearLayout android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingLeft="@dimen/doorhanger_section_padding_small"
+ android:paddingRight="@dimen/doorhanger_section_padding_small"
+ android:paddingBottom="@dimen/doorhanger_section_padding_medium"
+ android:paddingTop="@dimen/doorhanger_section_padding_medium">
+
+ <ImageView android:id="@+id/doorhanger_icon"
+ android:layout_width="@dimen/doorhanger_icon_size"
+ android:layout_height="@dimen/doorhanger_icon_size"
+ android:layout_gravity="center_horizontal"
+ android:padding="@dimen/doorhanger_section_padding_small"
+ android:layout_marginRight="@dimen/doorhanger_section_padding_small"
+ android:visibility="gone"/>
+
+ <LinearLayout android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView android:id="@+id/doorhanger_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/doorhanger_section_padding_medium"
+ android:textAppearance="@style/TextAppearance.DoorHanger.Medium.Light"
+ android:visibility="gone"/>
+
+ <ViewStub android:id="@+id/content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <TextView android:id="@+id/doorhanger_link"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.DoorHanger.Medium"
+ android:textColor="@color/link_blue"
+ android:layout_marginTop="@dimen/doorhanger_section_padding_large"
+ android:layout_marginBottom="@dimen/doorhanger_section_padding_small"
+ android:visibility="gone"/>
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <LinearLayout android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <Button android:id="@+id/doorhanger_button_negative"
+ style="@style/Widget.Doorhanger.Button"
+ android:textColor="@android:color/black"
+ android:background="@drawable/action_bar_button_negative"
+ android:visibility="gone"/>
+
+ <Button android:id="@+id/doorhanger_button_positive"
+ style="@style/Widget.Doorhanger.Button"
+ android:textColor="@android:color/white"
+ android:background="@drawable/action_bar_button_positive"
+ android:visibility="gone"/>
+
+ </LinearLayout>
+
+ <View android:id="@+id/divider_doorhanger"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/toolbar_divider_grey"
+ android:visibility="gone"/>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/doorhanger_security.xml b/mobile/android/base/resources/layout/doorhanger_security.xml
new file mode 100644
index 0000000000..8f4ddc9638
--- /dev/null
+++ b/mobile/android/base/resources/layout/doorhanger_security.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView android:id="@+id/security_title"
+ android:focusable="true"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="@dimen/doorhanger_subsection_padding"
+ android:textAppearance="@style/TextAppearance.DoorHanger.Medium"
+ android:visibility="gone"/>
+
+ <TextView android:id="@+id/security_state"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="@dimen/doorhanger_section_padding_small"
+ android:textAppearance="@style/TextAppearance.DoorHanger.Medium.Bold"
+ android:visibility="gone"/>
+
+ <TextView android:id="@+id/security_message"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.DoorHanger.Medium"/>
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/find_in_page_content.xml b/mobile/android/base/resources/layout/find_in_page_content.xml
new file mode 100644
index 0000000000..5505f90bcf
--- /dev/null
+++ b/mobile/android/base/resources/layout/find_in_page_content.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <view class="org.mozilla.gecko.CustomEditText"
+ android:id="@+id/find_text"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1.0"
+ android:layout_marginLeft="@dimen/find_in_page_text_margin_left"
+ android:layout_marginRight="@dimen/find_in_page_text_margin_right"
+ android:contentDescription="@string/find_text"
+ android:background="@drawable/url_bar_entry"
+ android:singleLine="true"
+ android:textColor="#000000"
+ android:textCursorDrawable="@null"
+ android:inputType="text"
+ android:paddingLeft="@dimen/find_in_page_text_padding_left"
+ android:paddingRight="@dimen/find_in_page_text_padding_right"
+ android:textColorHighlight="@color/fennec_ui_orange"
+ android:imeOptions="actionSearch"
+ android:selectAllOnFocus="true"
+ android:gravity="center_vertical|left"/>
+
+ <TextView android:id="@+id/find_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="@dimen/find_in_page_status_margin_right"
+ android:textColor="@color/tabs_tray_icon_grey"
+ android:visibility="gone"/>
+
+ <ImageButton android:id="@+id/find_prev"
+ style="@style/FindBar.ImageButton"
+ android:contentDescription="@string/find_prev"
+ android:layout_marginTop="@dimen/find_in_page_control_margin_top"
+ android:src="@drawable/find_prev"/>
+
+ <ImageButton android:id="@+id/find_next"
+ style="@style/FindBar.ImageButton"
+ android:contentDescription="@string/find_next"
+ android:layout_marginTop="@dimen/find_in_page_control_margin_top"
+ android:src="@drawable/find_next"/>
+
+ <ImageButton android:id="@+id/find_close"
+ style="@style/FindBar.ImageButton"
+ android:contentDescription="@string/find_close"
+ android:layout_marginTop="@dimen/find_in_page_control_margin_top"
+ android:src="@drawable/find_close"/>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/firstrun_animation_container.xml b/mobile/android/base/resources/layout/firstrun_animation_container.xml
new file mode 100644
index 0000000000..3e7225365c
--- /dev/null
+++ b/mobile/android/base/resources/layout/firstrun_animation_container.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<org.mozilla.gecko.firstrun.FirstrunAnimationContainer xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:background="@color/dark_transparent_overlay">
+
+ <org.mozilla.gecko.firstrun.FirstrunPager
+ android:id="@+id/firstrun_pager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/white">
+
+ <org.mozilla.gecko.home.TabMenuStrip android:layout_width="match_parent"
+ android:layout_height="@dimen/tabs_strip_height"
+ android:background="@color/firstrun_pager_header"
+ android:visibility="visible"
+ android:layout_gravity="top"
+ gecko:strip="@drawable/home_tab_menu_strip"
+ gecko:activeTextColor="@color/placeholder_grey"
+ gecko:inactiveTextColor="@color/tab_text_color"
+ gecko:tabsMarginLeft="@dimen/firstrun_tab_strip_content_start" />
+
+ </org.mozilla.gecko.firstrun.FirstrunPager>
+</org.mozilla.gecko.firstrun.FirstrunAnimationContainer>
diff --git a/mobile/android/base/resources/layout/firstrun_basepanel_checkable_fragment.xml b/mobile/android/base/resources/layout/firstrun_basepanel_checkable_fragment.xml
new file mode 100644
index 0000000000..feedab735c
--- /dev/null
+++ b/mobile/android/base/resources/layout/firstrun_basepanel_checkable_fragment.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:orientation="vertical"
+ android:fillViewport="true">
+
+ <LinearLayout android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="@dimen/firstrun_min_height"
+ android:background="@color/about_page_header_grey"
+ android:gravity="center_horizontal"
+ android:orientation="vertical">
+
+ <ImageView android:id="@+id/firstrun_image"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/firstrun_background_height"
+ android:layout_marginTop="40dp"
+ android:layout_marginBottom="40dp"
+ android:scaleType="fitCenter"
+ android:layout_gravity="center"
+ android:adjustViewBounds="true"/>
+
+ <TextView android:id="@+id/firstrun_text"
+ android:layout_width="@dimen/firstrun_content_width"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:textAppearance="@style/TextAppearance.FirstrunLight.Main"/>
+
+ <TextView android:id="@+id/firstrun_subtext"
+ android:layout_width="@dimen/firstrun_content_width"
+ android:layout_height="wrap_content"
+ android:paddingTop="20dp"
+ android:paddingBottom="30dp"
+ android:gravity="center"
+ android:textAppearance="@style/TextAppearance.FirstrunRegular.Body"/>
+
+ <android.support.v7.widget.SwitchCompat
+ android:id="@+id/firstrun_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:visibility="invisible"/>
+
+ <TextView android:id="@+id/firstrun_link"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="30dp"
+ android:gravity="center"
+ android:textAppearance="@style/TextAppearance.FirstrunRegular.Link"
+ android:text="@string/firstrun_button_next"/>
+ </LinearLayout>
+</ScrollView>
diff --git a/mobile/android/base/resources/layout/firstrun_sync_fragment.xml b/mobile/android/base/resources/layout/firstrun_sync_fragment.xml
new file mode 100644
index 0000000000..46fec6d5d5
--- /dev/null
+++ b/mobile/android/base/resources/layout/firstrun_sync_fragment.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:orientation="vertical"
+ android:fillViewport="true">
+
+ <LinearLayout android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="@dimen/firstrun_min_height"
+ android:background="@color/about_page_header_grey"
+ android:gravity="center_horizontal"
+ android:orientation="vertical">
+
+
+ <ImageView android:id="@+id/firstrun_image"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/firstrun_background_height"
+ android:layout_marginTop="40dp"
+ android:layout_marginBottom="40dp"
+ android:scaleType="fitCenter"
+ android:layout_gravity="center"
+ android:adjustViewBounds="true"/>
+
+ <TextView android:id="@+id/firstrun_text"
+ android:layout_width="@dimen/firstrun_content_width"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:paddingBottom="40dp"
+ android:textAppearance="@style/TextAppearance.FirstrunLight.Main"/>
+
+ <Button android:id="@+id/welcome_account"
+ style="@style/Widget.Firstrun.Button"
+ android:background="@drawable/button_background_action_orange_round"
+ android:layout_gravity="center"
+ android:text="@string/firstrun_signin_button"/>
+
+ <View android:layout_weight="1"
+ android:layout_height="0dp"
+ android:layout_width="match_parent"/>
+
+ <TextView android:id="@+id/welcome_browse"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="30dp"
+ android:gravity="center"
+ android:textAppearance="@style/TextAppearance.FirstrunRegular.Link"/>
+ </LinearLayout>
+</ScrollView>
diff --git a/mobile/android/base/resources/layout/font_size_preference.xml b/mobile/android/base/resources/layout/font_size_preference.xml
new file mode 100644
index 0000000000..b5af5e588c
--- /dev/null
+++ b/mobile/android/base/resources/layout/font_size_preference.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout android:id="@+id/button_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:layout_marginRight="4dp"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:orientation="horizontal">
+
+ <Button android:id="@+id/decrease_preview_font_button"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:text="@string/pref_font_size_adjust_char"
+ android:textSize="8sp"/>
+
+ <Button android:id="@+id/increase_preview_font_button"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:text="@string/pref_font_size_adjust_char"
+ android:textSize="16sp"/>
+
+ </LinearLayout>
+
+ <ScrollView android:id="@+id/scrolling_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_above="@id/button_container"
+ android:layout_margin="8dp"
+ android:padding="8dp"
+ android:scrollbars="vertical"
+ android:scrollbarStyle="outsideOverlay"
+ android:fadeScrollbars="true"
+ android:requiresFadingEdge="vertical">
+
+ <TextView android:id="@+id/preview"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/pref_font_size_preview_text"
+ android:textColor="#ff000000"/>
+
+ </ScrollView>
+
+</RelativeLayout>
diff --git a/mobile/android/base/resources/layout/gecko_app.xml b/mobile/android/base/resources/layout/gecko_app.xml
new file mode 100644
index 0000000000..ec43b341f2
--- /dev/null
+++ b/mobile/android/base/resources/layout/gecko_app.xml
@@ -0,0 +1,177 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/root_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ViewStub android:id="@+id/tabs_panel"
+ android:layout="@layout/tabs_panel_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <view class="org.mozilla.gecko.GeckoApp$MainLayout"
+ android:id="@+id/main_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/transparent">
+
+ <RelativeLayout android:id="@+id/gecko_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_below="@+id/tablet_tab_strip"
+ android:layout_above="@+id/find_in_page">
+
+ <fragment class="org.mozilla.gecko.GeckoViewFragment"
+ android:id="@+id/layer_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbars="none"/>
+
+ <AbsoluteLayout android:id="@+id/plugin_container"
+ android:background="@android:color/transparent"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <org.mozilla.gecko.FormAssistPopup android:id="@+id/form_assist_popup"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="gone"/>
+
+ <view class="org.mozilla.gecko.media.VideoPlayer" android:id="@+id/video_player"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent">
+ </view>
+
+ <ViewStub android:id="@+id/zoomed_view_stub"
+ android:inflatedId="@+id/zoomed_view"
+ android:layout="@layout/zoomed_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ <FrameLayout android:id="@+id/home_screen_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="gone">
+
+ <ViewStub android:id="@+id/home_pager_stub"
+ android:layout="@layout/home_pager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <ViewStub android:id="@+id/activity_stream_stub"
+ android:layout="@layout/activity_stream"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <ViewStub android:id="@+id/home_banner_stub"
+ android:layout="@layout/home_banner"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/home_banner_height"
+ android:layout_gravity="bottom"/>
+
+ <ViewStub android:id="@+id/firstrun_pager_stub"
+ android:layout="@layout/firstrun_animation_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ </FrameLayout>
+
+ <View android:id="@+id/doorhanger_overlay"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/dark_transparent_overlay"
+ android:alpha="0"
+ android:layerType="hardware"/>
+
+ </RelativeLayout>
+
+ <org.mozilla.gecko.FindInPageBar android:id="@+id/find_in_page"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ style="@style/FindBar"
+ android:visibility="gone"/>
+
+ <org.mozilla.gecko.MediaCastingBar android:id="@+id/media_casting"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ style="@style/FindBar"
+ android:visibility="gone"/>
+
+ <FrameLayout android:id="@+id/search_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_below="@+id/browser_chrome"
+ android:visibility="invisible"/>
+
+ <!-- When focus is cleared from from BrowserToolbar's EditText to
+ lower the virtual keyboard, focus will be returned to the root
+ view. To make sure the EditText is not the first focusable view in
+ the root view, BrowserToolbar should be specified as low in the
+ view hierarchy as possible. -->
+
+ <LinearLayout android:id="@id/browser_chrome"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <ViewStub android:id="@+id/tablet_tab_strip"
+ android:inflatedId="@id/tablet_tab_strip"
+ android:layout="@layout/tab_strip"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/tablet_tab_strip_height"
+ android:visibility="gone"/>
+
+ <ViewFlipper
+ android:id="@+id/browser_actionbar"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/browser_toolbar_height_flipper"
+ android:clickable="true"
+ android:focusable="true">
+
+ <org.mozilla.gecko.toolbar.BrowserToolbar
+ android:id="@+id/browser_toolbar"
+ style="@style/BrowserToolbar"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clickable="true"
+ android:focusable="true"
+ android:background="@drawable/url_bar_bg"/>
+
+ <org.mozilla.gecko.ActionModeCompatView android:id="@+id/actionbar"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ style="@style/GeckoActionBar.ActionMode"/>
+
+ </ViewFlipper>
+
+ </LinearLayout>
+
+ <org.mozilla.gecko.toolbar.ToolbarProgressView android:id="@+id/progress"
+ android:layout_width="match_parent"
+ android:layout_height="14dp"
+ android:layout_marginTop="-8dp"
+ android:layout_below="@id/browser_chrome"
+ android:src="@drawable/progress"
+ android:background="@null"
+ android:visibility="gone" />
+
+ </view>
+
+ <FrameLayout android:id="@+id/tab_history_panel"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentBottom="true"
+ android:visibility="gone" />
+
+ <ViewStub android:id="@+id/toast_stub"
+ android:layout="@layout/button_toast"
+ style="@style/Toast"/>
+
+</RelativeLayout>
diff --git a/mobile/android/base/resources/layout/history_sync_setup.xml b/mobile/android/base/resources/layout/history_sync_setup.xml
new file mode 100644
index 0000000000..d8ebfeebfd
--- /dev/null
+++ b/mobile/android/base/resources/layout/history_sync_setup.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <LinearLayout
+ style="@style/RemoteTabsPanelFrame">
+
+ <TextView
+ style="@style/RemoteTabsPanelItem.TextAppearance.Header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/fxaccount_getting_started_welcome_to_sync" />
+
+ <TextView
+ style="@style/RemoteTabsPanelItem.TextAppearance"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/fxaccount_getting_started_description" />
+
+ <Button
+ android:id="@+id/sync_setup_button"
+ style="@style/RemoteTabsPanelItem.Button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/fxaccount_getting_started_get_started" />
+ </LinearLayout>
+
+</ScrollView> \ No newline at end of file
diff --git a/mobile/android/base/resources/layout/home_banner.xml b/mobile/android/base/resources/layout/home_banner.xml
new file mode 100644
index 0000000000..46152c7116
--- /dev/null
+++ b/mobile/android/base/resources/layout/home_banner.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<org.mozilla.gecko.home.HomeBanner xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/home_banner"
+ style="@style/Widget.HomeBanner"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/home_banner"
+ android:gravity="center_vertical"
+ android:visibility="gone"
+ android:clickable="true"
+ android:focusable="true"/>
diff --git a/mobile/android/base/resources/layout/home_banner_content.xml b/mobile/android/base/resources/layout/home_banner_content.xml
new file mode 100644
index 0000000000..5cbdc47e27
--- /dev/null
+++ b/mobile/android/base/resources/layout/home_banner_content.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <ImageView android:id="@+id/icon"
+ android:layout_width="@dimen/home_banner_icon_width"
+ android:layout_height="@dimen/home_banner_icon_height"
+ android:layout_marginLeft="10dp"
+ android:scaleType="centerInside"/>
+
+ <org.mozilla.gecko.widget.EllipsisTextView
+ android:id="@+id/text"
+ android:layout_width="0dip"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:layout_marginLeft="10dp"
+ android:paddingTop="7dp"
+ android:paddingBottom="7dp"
+ android:textAppearance="@style/TextAppearance.Widget.HomeBanner"
+ android:layout_gravity="bottom"
+ android:gravity="center_vertical"
+ gecko:ellipsizeAtLine="3"/>
+
+ <ImageButton android:id="@+id/close"
+ android:layout_width="@dimen/home_banner_close_width"
+ android:layout_height="match_parent"
+ android:background="@drawable/home_banner"
+ android:scaleType="center"
+ android:contentDescription="@string/close_tab"
+ android:src="@drawable/tab_close"/>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/home_bookmarks_panel.xml b/mobile/android/base/resources/layout/home_bookmarks_panel.xml
new file mode 100644
index 0000000000..c4c08b93a8
--- /dev/null
+++ b/mobile/android/base/resources/layout/home_bookmarks_panel.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ViewStub android:id="@+id/home_empty_view_stub"
+ android:layout="@layout/home_empty_panel"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <org.mozilla.gecko.home.BookmarksListView
+ android:id="@+id/bookmarks_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+</FrameLayout>
diff --git a/mobile/android/base/resources/layout/home_combined_back_item.xml b/mobile/android/base/resources/layout/home_combined_back_item.xml
new file mode 100644
index 0000000000..e6d4437086
--- /dev/null
+++ b/mobile/android/base/resources/layout/home_combined_back_item.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/Widget.FolderView"
+ android:layout_width="match_parent"
+ android:text="@string/home_history_back_to"
+ android:paddingLeft="24dp"
+ android:drawablePadding="24dp"
+ android:drawableLeft="@drawable/arrow_up"
+ android:gravity="center_vertical"/>
diff --git a/mobile/android/base/resources/layout/home_combined_history_panel.xml b/mobile/android/base/resources/layout/home_combined_history_panel.xml
new file mode 100644
index 0000000000..fe64b028e6
--- /dev/null
+++ b/mobile/android/base/resources/layout/home_combined_history_panel.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <android.support.v4.widget.SwipeRefreshLayout
+ android:id="@+id/refresh_layout"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1.2">
+
+ <org.mozilla.gecko.home.CombinedHistoryRecyclerView
+ android:id="@+id/combined_recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbars="vertical"/>
+
+ </android.support.v4.widget.SwipeRefreshLayout>
+
+ <include android:id="@+id/home_history_empty_view"
+ layout="@layout/home_empty_panel"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="3"
+ android:visibility="gone"/>
+
+ <include android:id="@+id/home_clients_empty_view"
+ layout="@layout/history_sync_setup"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="3"
+ android:visibility="gone"/>
+
+ <include android:id="@+id/home_recent_tabs_empty_view"
+ layout="@layout/home_empty_panel"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="3"
+ android:visibility="gone"/>
+
+ <Button android:id="@+id/history_panel_footer_button"
+ style="@style/Widget.Home.ActionButton"
+ android:layout_width="match_parent"
+ android:layout_height="48dp"
+ android:visibility="gone" />
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/home_empty_panel.xml b/mobile/android/base/resources/layout/home_empty_panel.xml
new file mode 100644
index 0000000000..e845e7765d
--- /dev/null
+++ b/mobile/android/base/resources/layout/home_empty_panel.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/home_empty_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:gravity="center"
+ android:paddingLeft="50dp"
+ android:paddingRight="50dp">
+
+ <!-- Empty spacer view -->
+ <View android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1"/>
+
+ <ImageView android:id="@+id/home_empty_image"
+ android:layout_width="90dp"
+ android:layout_height="90dp"
+ android:layout_marginBottom="10dp"
+ android:gravity="top|center"
+ android:scaleType="fitCenter"/>
+
+ <TextView android:id="@+id/home_empty_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="top|center"
+ android:textAppearance="@style/TextAppearance.EmptyMessage"/>
+
+ <TextView android:id="@+id/home_empty_hint"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:gravity="top|center"
+ android:textAppearance="@style/TextAppearance.EmptyHint"
+ android:textColorLink="#FFA62F" />
+ <!-- Empty spacer view -->
+ <View android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="2"/>
+
+</LinearLayout>
+
diff --git a/mobile/android/base/resources/layout/home_header_row.xml b/mobile/android/base/resources/layout/home_header_row.xml
new file mode 100644
index 0000000000..f3ca9322b2
--- /dev/null
+++ b/mobile/android/base/resources/layout/home_header_row.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/Widget.Home.HeaderItem"/>
diff --git a/mobile/android/base/resources/layout/home_item_row.xml b/mobile/android/base/resources/layout/home_item_row.xml
new file mode 100644
index 0000000000..86754225f6
--- /dev/null
+++ b/mobile/android/base/resources/layout/home_item_row.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<org.mozilla.gecko.home.TwoLinePageRow xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/Widget.TwoLinePageRow"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/page_row_height"
+ android:minHeight="@dimen/page_row_height"/>
diff --git a/mobile/android/base/resources/layout/home_pager.xml b/mobile/android/base/resources/layout/home_pager.xml
new file mode 100644
index 0000000000..c858cefbc1
--- /dev/null
+++ b/mobile/android/base/resources/layout/home_pager.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- This file is used to include the home pager in gecko app
+ layout based on screen size -->
+
+<org.mozilla.gecko.home.HomePager xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/home_pager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/white">
+
+ <org.mozilla.gecko.home.TabMenuStrip android:layout_width="match_parent"
+ android:layout_height="@dimen/tabs_strip_height"
+ android:background="@color/about_page_header_grey"
+ android:layout_gravity="top"
+ gecko:strip="@drawable/home_tab_menu_strip"
+ gecko:activeTextColor="@color/placeholder_grey"
+ gecko:inactiveTextColor="@color/tab_text_color"
+ gecko:tabsMarginLeft="@dimen/tab_strip_content_start" />
+
+</org.mozilla.gecko.home.HomePager>
diff --git a/mobile/android/base/resources/layout/home_remote_tabs_group.xml b/mobile/android/base/resources/layout/home_remote_tabs_group.xml
new file mode 100644
index 0000000000..60e6597f79
--- /dev/null
+++ b/mobile/android/base/resources/layout/home_remote_tabs_group.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto"
+ style="@style/Widget.RemoteTabsClientView"
+ android:gravity="center_vertical"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/home_header_item_height"
+ android:minHeight="@dimen/home_header_item_height">
+
+ <ImageView
+ android:id="@+id/device_type"
+ android:layout_width="@dimen/favicon_bg"
+ android:layout_height="@dimen/favicon_bg"
+ android:layout_marginLeft="12dp"
+ android:layout_marginRight="12dp"
+ android:layout_gravity="center_vertical"
+ android:scaleType="center"
+ tools:src="@drawable/sync_mobile"/>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical">
+
+ <org.mozilla.gecko.widget.FadedSingleColorTextView
+ android:id="@+id/client"
+ style="@style/Widget.FolderTitle.TwoLine"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ gecko:fadeWidth="30dp"
+ tools:text="Firefox on Nexus 5"/>
+
+ <TextView
+ android:id="@+id/last_synced"
+ style="@style/Widget.TwoLinePageRow.Url"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="4dp"
+ android:maxLength="1024"
+ tools:text="Last synced: 5 minutes ago"/>
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/device_expanded"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_marginLeft="15dip"
+ android:layout_marginRight="15dip"
+ android:scaleType="center"
+ tools:src="@drawable/arrow_down"/>
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/home_remote_tabs_hidden_devices.xml b/mobile/android/base/resources/layout/home_remote_tabs_hidden_devices.xml
new file mode 100644
index 0000000000..9397cfedc4
--- /dev/null
+++ b/mobile/android/base/resources/layout/home_remote_tabs_hidden_devices.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/hidden_devices"
+ style="@style/Widget.Home.ActionItem"
+ android:background="@drawable/action_bar_button"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/home_remote_tabs_hidden_footer_height"
+ android:gravity="center"
+ android:maxLength="1024"
+ android:textSize="12dp"
+ android:textColor="@color/disabled_grey" />
diff --git a/mobile/android/base/resources/layout/home_search_item_row.xml b/mobile/android/base/resources/layout/home_search_item_row.xml
new file mode 100644
index 0000000000..d12239550c
--- /dev/null
+++ b/mobile/android/base/resources/layout/home_search_item_row.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<org.mozilla.gecko.home.SearchEngineRow xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="@dimen/search_row_height"
+ android:duplicateParentState="true"
+ android:paddingTop="5dp"
+ android:paddingBottom="5dp"/>
diff --git a/mobile/android/base/resources/layout/home_smartfolder.xml b/mobile/android/base/resources/layout/home_smartfolder.xml
new file mode 100644
index 0000000000..484c30eab9
--- /dev/null
+++ b/mobile/android/base/resources/layout/home_smartfolder.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto"
+ style="@style/Widget.RemoteTabsClientView"
+ android:gravity="center_vertical"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/home_header_item_height"
+ android:minHeight="@dimen/home_header_item_height">
+
+ <ImageView
+ android:id="@+id/device_type"
+ android:layout_width="26dp"
+ android:layout_height="18dp"
+ android:layout_margin="20dp"
+ android:scaleType="fitCenter"
+ android:layout_gravity="center_vertical"
+ tools:src="@drawable/cloud"/>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical">
+
+ <org.mozilla.gecko.widget.FadedSingleColorTextView
+ android:id="@+id/title"
+ style="@style/Widget.FolderTitle.TwoLine"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ gecko:fadeWidth="30dp"
+ tools:text="Firefox on Nexus 5"/>
+
+ <TextView
+ android:id="@+id/subtext"
+ style="@style/Widget.TwoLinePageRow.Url"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="4dp"
+ android:maxLength="1024"
+ tools:text="Last synced: 5 minutes ago"/>
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/home_suggestion_prompt.xml b/mobile/android/base/resources/layout/home_suggestion_prompt.xml
new file mode 100644
index 0000000000..818671e1c2
--- /dev/null
+++ b/mobile/android/base/resources/layout/home_suggestion_prompt.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/about_page_header_grey">
+
+ <LinearLayout android:id="@+id/prompt"
+ android:focusable="true"
+ android:layout_width="match_parent"
+ android:minHeight="48dp"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:paddingLeft="15dp"
+ android:paddingRight="15dp"
+ android:textSize="12sp">
+
+ <TextView android:id="@+id/suggestions_prompt_title"
+ android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:fontFamily="sans-serif"
+ android:textColor="@color/text_and_tabs_tray_grey"
+ android:layout_marginRight="10dp"/>
+
+ <TextView android:id="@+id/suggestions_prompt_no"
+ android:layout_height="32dp"
+ android:minWidth="72dp"
+ android:layout_width="wrap_content"
+ android:layout_marginRight="10dp"
+ android:textColor="@color/text_and_tabs_tray_grey"
+ android:gravity="center"
+ android:background="@drawable/search_suggestion_prompt_no"
+ android:nextFocusRight="@+id/suggestions_prompt_yes"
+ android:focusable="true"
+ android:text="@string/button_no"/>
+
+ <TextView android:id="@+id/suggestions_prompt_yes"
+ android:layout_height="32dp"
+ android:minWidth="72dp"
+ android:layout_width="wrap_content"
+ android:gravity="center"
+ android:textColor="@android:color/white"
+ android:background="@drawable/search_suggestion_prompt_yes"
+ android:focusable="true"
+ android:text="@string/button_yes"/>
+
+
+
+ </LinearLayout>
+
+</FrameLayout>
diff --git a/mobile/android/base/resources/layout/home_top_sites_panel.xml b/mobile/android/base/resources/layout/home_top_sites_panel.xml
new file mode 100644
index 0000000000..a08e680e6e
--- /dev/null
+++ b/mobile/android/base/resources/layout/home_top_sites_panel.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<org.mozilla.gecko.home.HomeListView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/list"
+ style="@style/Widget.TopSitesListView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
diff --git a/mobile/android/base/resources/layout/icon_grid.xml b/mobile/android/base/resources/layout/icon_grid.xml
new file mode 100644
index 0000000000..33c9aee312
--- /dev/null
+++ b/mobile/android/base/resources/layout/icon_grid.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<GridView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:choiceMode="singleChoice"
+ android:padding="@dimen/icongrid_padding"/>
diff --git a/mobile/android/base/resources/layout/icon_grid_item.xml b/mobile/android/base/resources/layout/icon_grid_item.xml
new file mode 100644
index 0000000000..5c1a4bc9b7
--- /dev/null
+++ b/mobile/android/base/resources/layout/icon_grid_item.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ //device/apps/common/res/any/layout/resolve_list_item.xml
+ Copyright 2006, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:background="@drawable/icon_grid_item_bg"
+ android:padding="16dp">
+
+ <!-- Extended activity info to distinguish between duplicate activity names -->
+ <TextView android:id="@android:id/text2"
+ android:textAppearance="?android:attr/textAppearance"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:minLines="2"
+ android:maxLines="2"
+ android:paddingTop="4dip"
+ android:paddingBottom="4dip" />
+
+ <!-- Activity icon when presenting dialog
+ Size will be filled in by ResolverActivity -->
+ <ImageView android:id="@+id/icon"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:scaleType="fitCenter" />
+
+ <!-- Activity name -->
+ <TextView android:id="@android:id/text1"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:minLines="2"
+ android:maxLines="2"
+ android:paddingTop="4dip"
+ android:paddingBottom="4dip" />
+</LinearLayout>
+
diff --git a/mobile/android/base/resources/layout/list_item_header.xml b/mobile/android/base/resources/layout/list_item_header.xml
new file mode 100644
index 0000000000..2d91f4e020
--- /dev/null
+++ b/mobile/android/base/resources/layout/list_item_header.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/text1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="2dip"
+ android:paddingBottom="2dip"
+ android:paddingLeft="5dip"
+ style="?android:attr/listSeparatorTextViewStyle" />
diff --git a/mobile/android/base/resources/layout/login_doorhanger.xml b/mobile/android/base/resources/layout/login_doorhanger.xml
new file mode 100644
index 0000000000..8e8c005afd
--- /dev/null
+++ b/mobile/android/base/resources/layout/login_doorhanger.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView android:id="@+id/doorhanger_message"
+ android:focusable="true"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.DoorHanger.Medium"/>
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/login_edit_dialog.xml b/mobile/android/base/resources/layout/login_edit_dialog.xml
new file mode 100644
index 0000000000..8d42d5d95b
--- /dev/null
+++ b/mobile/android/base/resources/layout/login_edit_dialog.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="@dimen/doorhanger_section_padding_medium"
+ android:orientation="vertical">
+
+ <EditText android:id="@+id/username_edit"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textNoSuggestions"
+ android:hint="@string/doorhanger_login_edit_username_hint"/>
+
+ <EditText android:id="@+id/password_edit"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textPassword"
+ android:hint="@string/doorhanger_login_edit_password_hint"/>
+
+ <android.support.v7.widget.AppCompatCheckBox
+ android:id="@+id/checkbox_toggle_password"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/doorhanger_login_edit_toggle"
+ android:layout_marginTop="@dimen/doorhanger_subsection_padding"/>
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/media_casting.xml b/mobile/android/base/resources/layout/media_casting.xml
new file mode 100644
index 0000000000..008cf36a0f
--- /dev/null
+++ b/mobile/android/base/resources/layout/media_casting.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <RelativeLayout android:id="@+id/media_controls"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true">
+
+ <ImageButton android:id="@+id/media_play"
+ style="@style/FindBar.ImageButton"
+ android:contentDescription="@string/media_play"
+ android:src="@drawable/media_bar_play"
+ android:visibility="gone"/>
+
+ <ImageButton android:id="@+id/media_pause"
+ style="@style/FindBar.ImageButton"
+ android:contentDescription="@string/media_pause"
+ android:src="@drawable/media_bar_pause"/>
+
+ </RelativeLayout>
+
+ <TextView android:id="@+id/media_sending_to"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="5dip"
+ android:layout_marginRight="5dip"
+ android:layout_alignParentLeft="true"
+ android:layout_toLeftOf="@id/media_controls"
+ android:layout_centerVertical="true"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:textColor="#FFFFFFFF"
+ android:contentDescription="@string/media_sending_to"/>
+
+ <ImageButton android:id="@+id/media_stop"
+ style="@style/FindBar.ImageButton"
+ android:contentDescription="@string/media_stop"
+ android:layout_alignParentRight="true"
+ android:src="@drawable/media_bar_stop"/>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/menu_action_bar.xml b/mobile/android/base/resources/layout/menu_action_bar.xml
new file mode 100644
index 0000000000..0b9476f200
--- /dev/null
+++ b/mobile/android/base/resources/layout/menu_action_bar.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!--
+ Note: This layout is intended to be used only above 11+.
+ android:showDividers are available only 11+
+-->
+<view xmlns:android="http://schemas.android.com/apk/res/android"
+ class="org.mozilla.gecko.menu.GeckoMenu$DefaultActionItemBar"
+ android:layout_width="@dimen/menu_item_row_width"
+ android:layout_height="@dimen/browser_toolbar_height"
+ android:orientation="horizontal"
+ android:background="@color/toolbar_menu_dark_grey"/>
diff --git a/mobile/android/base/resources/layout/menu_item_switcher_layout.xml b/mobile/android/base/resources/layout/menu_item_switcher_layout.xml
new file mode 100644
index 0000000000..04b70bde8f
--- /dev/null
+++ b/mobile/android/base/resources/layout/menu_item_switcher_layout.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!-- Application icons will be added dynamically -->
+
+ <FrameLayout android:layout_width="0dip"
+ android:layout_height="@dimen/menu_item_row_height"
+ android:layout_weight="1.0">
+
+ <org.mozilla.gecko.menu.MenuItemDefault
+ android:id="@+id/menu_item"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/menu_item_action_bar_bg"
+ android:clickable="true"
+ android:focusable="true"/>
+
+ <org.mozilla.gecko.menu.MenuItemActionBar
+ style="@style/Widget.MenuItemSecondaryActionBar"
+ android:id="@+id/menu_item_button"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center_vertical"
+ android:visibility="gone"/>
+
+ </FrameLayout>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/menu_popup.xml b/mobile/android/base/resources/layout/menu_popup.xml
new file mode 100644
index 0000000000..494b727e46
--- /dev/null
+++ b/mobile/android/base/resources/layout/menu_popup.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<org.mozilla.gecko.widget.FilledCardView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/menu_panel"
+ android:layout_width="@dimen/menu_popup_width"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:minWidth="@dimen/menu_popup_width"
+ app:cardBackgroundColor="@color/toolbar_grey"
+ app:cardUseCompatPadding="true">
+
+ <!-- MenuPanel will be added here dynamically -->
+
+</org.mozilla.gecko.widget.FilledCardView> \ No newline at end of file
diff --git a/mobile/android/base/resources/layout/menu_secondary_action_bar.xml b/mobile/android/base/resources/layout/menu_secondary_action_bar.xml
new file mode 100644
index 0000000000..c5be6f51ee
--- /dev/null
+++ b/mobile/android/base/resources/layout/menu_secondary_action_bar.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!--
+ Note: This layout is intended to be used only above 11+.
+ android:showDividers are available only 11+
+-->
+<view xmlns:android="http://schemas.android.com/apk/res/android"
+ class="org.mozilla.gecko.menu.GeckoMenu$DefaultActionItemBar"
+ android:layout_width="@dimen/menu_item_row_width"
+ android:layout_height="@dimen/browser_toolbar_height"
+ android:orientation="horizontal"/>
diff --git a/mobile/android/base/resources/layout/overlay_share_button.xml b/mobile/android/base/resources/layout/overlay_share_button.xml
new file mode 100644
index 0000000000..d40303a938
--- /dev/null
+++ b/mobile/android/base/resources/layout/overlay_share_button.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <ImageView
+ android:layout_width="60dp"
+ android:layout_height="match_parent"
+ android:id="@+id/overlaybtn_icon"
+ android:padding="30dp"
+ android:scaleType="center"/>
+
+ <TextView
+ android:textAppearance="@style/TextAppearance.ShareOverlay"
+ android:id="@+id/overlaybtn_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clickable="false"
+ android:enabled="false"
+ android:maxLines="1"
+ android:textSize="14sp"
+ android:textColor="@color/primary_text_selector"/>
+</merge>
diff --git a/mobile/android/base/resources/layout/overlay_share_dialog.xml b/mobile/android/base/resources/layout/overlay_share_dialog.xml
new file mode 100644
index 0000000000..b7fcd8747b
--- /dev/null
+++ b/mobile/android/base/resources/layout/overlay_share_dialog.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- Serves to position the content on the screen (bottom, centered) and provide the drop-shadow -->
+
+<merge
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+ <LinearLayout
+ android:id="@+id/sharedialog"
+ android:layout_width="300dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|center"
+ android:layout_marginLeft="15dp"
+ android:layout_marginRight="15dp"
+ android:paddingTop="8dp"
+ android:orientation="vertical">
+
+ <!-- Title -->
+ <TextView
+ android:id="@+id/title"
+ style="@style/ShareOverlayTitle"
+ android:textAppearance="@style/TextAppearance.ShareOverlay.Header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:maxLines="2"
+ android:textSize="20sp"
+ android:ellipsize="end"/>
+
+ <!-- Subtitle (url) -->
+ <TextView
+ android:id="@+id/subtitle"
+ style="@style/ShareOverlayTitle"
+ android:textAppearance="@style/TextAppearance.ShareOverlay.Header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="20dp"
+ android:textSize="12sp"
+ android:scrollHorizontally="true"/>
+
+ <!-- TODO: Add back drop shadow (bug 1146488)? -->
+ <!-- Buttons -->
+ <!-- "Send to Firefox Sync" -->
+ <org.mozilla.gecko.overlays.ui.SendTabList
+ android:id="@+id/overlay_send_tab_btn"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:divider="@null"/>
+
+ <!-- "Add bookmark" -->
+ <org.mozilla.gecko.overlays.ui.OverlayDialogButton
+ style="@style/ShareOverlayRow"
+ android:id="@+id/overlay_share_bookmark_btn"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:enabled="false"
+ gecko:drawable="@drawable/overlay_share_bookmark_button"
+ gecko:enabledText="@string/overlay_share_bookmark_btn_label"
+ gecko:disabledText="@string/overlay_share_bookmark_btn_label_already"/>
+
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/check"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="center"
+ android:src="@drawable/overlay_check"
+ android:visibility="invisible"/>
+
+ <!-- This transparent View is used to overlay the
+ entire Activity with an onClickListener. -->
+ <View
+ android:id="@+id/fullscreen_click_target"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:visibility="gone"/>
+</merge>
diff --git a/mobile/android/base/resources/layout/overlay_share_send_tab_item.xml b/mobile/android/base/resources/layout/overlay_share_send_tab_item.xml
new file mode 100644
index 0000000000..99d7238663
--- /dev/null
+++ b/mobile/android/base/resources/layout/overlay_share_send_tab_item.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- The first item's background is unique and these views are reused
+ so the background is set dynamically. -->
+<org.mozilla.gecko.overlays.ui.OverlayDialogButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/ShareOverlayRow"
+ android:id="@+id/overlay_send_tab_item"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
diff --git a/mobile/android/base/resources/layout/panel_article_item.xml b/mobile/android/base/resources/layout/panel_article_item.xml
new file mode 100644
index 0000000000..a4234e018e
--- /dev/null
+++ b/mobile/android/base/resources/layout/panel_article_item.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <ImageView android:id="@+id/image"
+ android:layout_width="@dimen/panel_article_item_height"
+ android:layout_height="@dimen/panel_article_item_height"
+ android:scaleType="centerCrop"/>
+
+ <LinearLayout android:id="@+id/title_desc_container"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/panel_article_item_height"
+ android:paddingLeft="15dip"
+ android:paddingRight="15dip"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView android:id="@+id/title"
+ style="@style/Widget.PanelItemView.Title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <TextView android:id="@+id/description"
+ style="@style/Widget.PanelItemView.Description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="1dp"
+ android:maxLength="1024"/>
+
+ </LinearLayout>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/panel_auth_layout.xml b/mobile/android/base/resources/layout/panel_auth_layout.xml
new file mode 100644
index 0000000000..549dc4191f
--- /dev/null
+++ b/mobile/android/base/resources/layout/panel_auth_layout.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!-- Empty spacer view -->
+ <View android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1"/>
+
+ <ImageView android:id="@+id/image"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="top|center"
+ android:scaleType="center"
+ android:paddingBottom="10dp"/>
+
+ <TextView android:id="@+id/message"
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:gravity="top|center"
+ android:textAppearance="@style/TextAppearance.EmptyMessage"
+ android:paddingLeft="50dp"
+ android:paddingRight="50dp"
+ android:layout_weight="3"/>
+
+ <Button android:id="@+id/button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="25dp"
+ android:layout_marginRight="25dp"
+ android:layout_marginBottom="25dp"
+ android:gravity="bottom|center"
+ android:textAppearance="@style/TextAppearance.EmptyMessage"
+ android:background="@drawable/panel_auth_button"
+ android:padding="20dp"/>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/panel_back_item.xml b/mobile/android/base/resources/layout/panel_back_item.xml
new file mode 100644
index 0000000000..d7f32fff3e
--- /dev/null
+++ b/mobile/android/base/resources/layout/panel_back_item.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <ImageView android:id="@+id/image"
+ android:layout_width="54dp"
+ android:layout_height="44dp"
+ android:layout_marginTop="10dip"
+ android:layout_marginLeft="10dip"
+ android:scaleType="centerCrop"/>
+
+ <TextView android:id="@+id/title"
+ style="@style/Widget.PanelItemView.Title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="5dip"
+ android:paddingBottom="5dip"
+ android:paddingLeft="10dip"
+ android:paddingRight="10dip"
+ android:minHeight="@dimen/page_row_height"
+ android:gravity="center_vertical"/>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/panel_icon_item.xml b/mobile/android/base/resources/layout/panel_icon_item.xml
new file mode 100644
index 0000000000..7f3e554b36
--- /dev/null
+++ b/mobile/android/base/resources/layout/panel_icon_item.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<org.mozilla.gecko.widget.SquaredRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ImageView
+ android:id="@+id/background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="centerCrop" />
+
+ <ImageView
+ android:id="@+id/image"
+ android:layout_width="50dp"
+ android:layout_height="50dp"
+ android:scaleType="centerInside"
+ android:layout_centerInParent="true" />
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:textColor="@android:color/white"
+ android:layout_alignParentBottom="true"
+ android:textSize="12sp"
+ android:padding="8dp"
+ android:background="@color/panel_icon_item_title_background"
+ android:layout_gravity="center_horizontal"/>
+
+</org.mozilla.gecko.widget.SquaredRelativeLayout>
diff --git a/mobile/android/base/resources/layout/panel_image_item.xml b/mobile/android/base/resources/layout/panel_image_item.xml
new file mode 100644
index 0000000000..15131dfafb
--- /dev/null
+++ b/mobile/android/base/resources/layout/panel_image_item.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <org.mozilla.gecko.widget.SquaredImageView
+ android:id="@+id/image"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:scaleType="centerCrop"
+ android:adjustViewBounds="true"
+ android:background="@color/panel_image_item_background"/>
+
+ <LinearLayout android:id="@+id/title_desc_container"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/page_row_height"
+ android:paddingTop="7dip"
+ android:paddingBottom="7dip"
+ android:paddingLeft="5dip"
+ android:paddingRight="5dip"
+ android:orientation="vertical">
+
+ <TextView android:id="@+id/title"
+ style="@style/Widget.PanelItemView.Title"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:gravity="center_vertical"
+ android:singleLine="true"/>
+
+ <TextView android:id="@+id/description"
+ style="@style/Widget.PanelItemView.Description"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:gravity="center_vertical"
+ android:singleLine="true"
+ android:maxLength="1024"/>
+
+ </LinearLayout>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/panel_item_container.xml b/mobile/android/base/resources/layout/panel_item_container.xml
new file mode 100644
index 0000000000..2d767c64db
--- /dev/null
+++ b/mobile/android/base/resources/layout/panel_item_container.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clickable="true"
+ android:foreground="@color/recyclerview_selector">
+
+</FrameLayout>
diff --git a/mobile/android/base/resources/layout/pin_site_dialog.xml b/mobile/android/base/resources/layout/pin_site_dialog.xml
new file mode 100644
index 0000000000..07eb152d77
--- /dev/null
+++ b/mobile/android/base/resources/layout/pin_site_dialog.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout android:layout_width="match_parent"
+ android:layout_height="@dimen/browser_toolbar_height"
+ android:orientation="vertical"
+ android:background="@color/toolbar_grey"
+ android:padding="4dip">
+
+ <EditText android:id="@+id/search"
+ style="@style/UrlBar.Button"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="6dip"
+ android:hint="@string/pin_site_dialog_hint"
+ android:background="@drawable/url_bar_entry"
+ android:textColor="@color/url_bar_title"
+ android:textColorHint="@color/url_bar_title_hint"
+ android:textColorHighlight="@color/fennec_ui_orange"
+ android:textSelectHandle="@drawable/handle_middle"
+ android:textSelectHandleLeft="@drawable/handle_start"
+ android:textSelectHandleRight="@drawable/handle_end"
+ android:textCursorDrawable="@null"
+ android:inputType="textUri|textNoSuggestions"
+ android:imeOptions="actionGo|flagNoExtractUi|flagNoFullscreen"
+ android:singleLine="true"
+ android:gravity="center_vertical|left"/>
+
+ </LinearLayout>
+
+ <org.mozilla.gecko.home.HomeListView android:id="@+id/list"
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1.0"/>
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/preference_checkbox.xml b/mobile/android/base/resources/layout/preference_checkbox.xml
new file mode 100644
index 0000000000..e16f5b623d
--- /dev/null
+++ b/mobile/android/base/resources/layout/preference_checkbox.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2006 The Android Open Source Project
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Layout used by CheckBoxPreference for the checkbox style. This is inflated
+ inside android.R.layout.preference. -->
+<CheckBox xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:focusable="false"
+ android:clickable="false" />
diff --git a/mobile/android/base/resources/layout/preference_panels.xml b/mobile/android/base/resources/layout/preference_panels.xml
new file mode 100644
index 0000000000..baeb99ce8d
--- /dev/null
+++ b/mobile/android/base/resources/layout/preference_panels.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:gravity="center_vertical"
+ android:orientation="vertical"
+ android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingRight="?android:attr/scrollbarSize">
+
+ <TextView android:id="@+android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal" />
+
+ <TextView android:id="@+android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:maxLines="2" />
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/preference_rightalign_icon.xml b/mobile/android/base/resources/layout/preference_rightalign_icon.xml
new file mode 100644
index 0000000000..413faf2aa1
--- /dev/null
+++ b/mobile/android/base/resources/layout/preference_rightalign_icon.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- This custom layout matches the Android layout for preferences in
+ padding and margins, so it is visually consistent. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingRight="?android:attr/scrollbarSize">
+
+ <RelativeLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:gravity="right"
+ android:layout_marginLeft="15dip"
+ android:layout_marginRight="6dip"
+ android:layout_marginTop="6dip"
+ android:layout_marginBottom="6dip"
+ android:paddingRight="6dip"
+ android:layout_weight="1">
+
+ <TextView android:id="@+android:id/title"
+ android:layout_gravity="right"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal" />
+
+ </RelativeLayout>
+
+ <ImageView android:src="@drawable/menu_item_more"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:paddingRight="16dp" />
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/preference_search_engine.xml b/mobile/android/base/resources/layout/preference_search_engine.xml
new file mode 100644
index 0000000000..97ce95a3fc
--- /dev/null
+++ b/mobile/android/base/resources/layout/preference_search_engine.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:gravity="center_vertical"
+ android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingRight="?android:attr/scrollbarSize">
+
+ <org.mozilla.gecko.widget.FaviconView
+ android:id="@+id/search_engine_icon"
+ android:layout_width="@dimen/favicon_bg"
+ android:layout_height="@dimen/favicon_bg"
+ android:layout_gravity="center"
+ android:minWidth="@dimen/favicon_bg"
+ android:minHeight="@dimen/favicon_bg" />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_marginLeft="15dip"
+ android:layout_marginRight="6dip"
+ android:layout_marginTop="6dip"
+ android:layout_marginBottom="6dip">
+
+ <TextView android:id="@+android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal" />
+
+ <TextView android:id="@+android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:maxLines="2" />
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/preference_search_tip.xml b/mobile/android/base/resources/layout/preference_search_tip.xml
new file mode 100644
index 0000000000..e03b171b46
--- /dev/null
+++ b/mobile/android/base/resources/layout/preference_search_tip.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- Ignore UseCompoundDrawables because they caused a regression in bug 1208790. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:ignore="UseCompoundDrawables"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingRight="?android:attr/scrollbarSize">
+
+ <TextView android:id="@+id/label_search_hint"
+ android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:text="@string/pref_search_hint"
+ android:layout_marginTop="5dip"
+ android:layout_marginBottom="6dip"
+ android:layout_marginLeft="15dip"
+ android:layout_marginRight="6dip"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:paddingRight="6dip"
+ android:layout_weight="1"/>
+
+ <ImageView android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:paddingRight="8dp"
+ android:paddingTop="12dip"
+ android:src="@drawable/tip_addsearch"/>
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/preference_set_homepage.xml b/mobile/android/base/resources/layout/preference_set_homepage.xml
new file mode 100644
index 0000000000..d30221a230
--- /dev/null
+++ b/mobile/android/base/resources/layout/preference_set_homepage.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<RadioGroup xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/homepage_layout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingLeft="20dp"
+ android:paddingRight="20dp"
+ android:orientation="vertical">
+
+ <RadioButton android:id="@+id/radio_default"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:text="@string/home_homepage_radio_default"
+ android:textColor="@color/text_and_tabs_tray_grey"/>
+
+ <RadioButton android:id="@+id/radio_user_address"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:text="@string/home_homepage_radio_user_address"
+ android:textColor="@color/text_and_tabs_tray_grey"/>
+
+ <!-- RadioGroup is a LinearLayout under the hood, so including this View is fine.
+ The visibility changes with RadioButton state so we hide it to start. -->
+ <EditText android:id="@+id/edittext_user_address"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:inputType="textUri"
+ android:hint="@string/home_homepage_hint_user_address"
+ android:textColorHint="@color/disabled_grey"
+ android:visibility="gone"/>
+
+</RadioGroup>
diff --git a/mobile/android/base/resources/layout/private_tabs_panel.xml b/mobile/android/base/resources/layout/private_tabs_panel.xml
new file mode 100644
index 0000000000..2c553abd70
--- /dev/null
+++ b/mobile/android/base/resources/layout/private_tabs_panel.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <ImageView android:id="@+id/private_tabs_empty"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:src="@drawable/private_masq"
+ android:layout_gravity="center"/>
+
+ <!-- Note: for an unknown reason, scrolling in the TabsLayout
+ does not work unless it is laid out after the empty view. -->
+ <view class="org.mozilla.gecko.tabs.TabsPanel$TabsLayout"
+ android:id="@+id/private_tabs_layout"
+ style="@style/TabsLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:choiceMode="singleChoice"
+ gecko:tabs="tabs_private"/>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/restricted_firstrun_welcome_fragment.xml b/mobile/android/base/resources/layout/restricted_firstrun_welcome_fragment.xml
new file mode 100644
index 0000000000..94ee43b7b4
--- /dev/null
+++ b/mobile/android/base/resources/layout/restricted_firstrun_welcome_fragment.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:fillViewport="true"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/restricted_profile_background_gold"
+ android:minHeight="@dimen/firstrun_min_height"
+ android:orientation="vertical"
+ android:weightSum="50">
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="21"
+ android:padding="30dp">
+
+ <TextView
+ android:layout_width="320dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:background="@color/restricted_profile_background_green"
+ android:gravity="center"
+ android:paddingBottom="40dp"
+ android:paddingLeft="30dp"
+ android:paddingRight="30dp"
+ android:paddingTop="40dp"
+ android:text="@string/firstrun_welcome_restricted"
+ android:textColor="#ffffff"
+ android:textSize="22sp" />
+
+ </FrameLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="29"
+ android:background="@color/android:white"
+ android:gravity="center_horizontal"
+ android:orientation="vertical"
+ android:padding="40dp">
+
+ <Button
+ android:id="@+id/welcome_browse"
+ style="@style/Widget.Firstrun.Button"
+ android:layout_gravity="center"
+ android:layout_marginBottom="20dp"
+ android:background="@drawable/button_background_action_blue_round"
+ android:text="@string/firstrun_welcome_button_browser" />
+
+ <TextView
+ android:id="@+id/learn_more_link"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:padding="20dp"
+ android:text="@string/pref_learn_more"
+ android:textAppearance="@style/TextAppearance.FirstrunRegular.Link" />
+
+ </LinearLayout>
+ </LinearLayout>
+
+</ScrollView>
diff --git a/mobile/android/base/resources/layout/search_activity_main.xml b/mobile/android/base/resources/layout/search_activity_main.xml
new file mode 100644
index 0000000000..93da2e0835
--- /dev/null
+++ b/mobile/android/base/resources/layout/search_activity_main.xml
@@ -0,0 +1,65 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:context=".SearchActivity">
+
+ <org.mozilla.search.autocomplete.SearchBar
+ android:id="@+id/search_bar"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/search_bar_height"
+ android:paddingTop="@dimen/search_bar_padding_y"
+ android:paddingBottom="@dimen/search_bar_padding_y"
+ android:paddingLeft="@dimen/search_row_padding"
+ android:paddingRight="@dimen/search_row_padding"
+ android:layout_gravity="top"/>
+
+ <fragment
+ android:id="@+id/postsearch"
+ android:name="org.mozilla.search.PostSearchFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="@dimen/search_bar_height"
+ android:layout_gravity="top"
+ android:visibility="invisible"/>
+
+ <fragment
+ android:id="@+id/presearch"
+ android:name="org.mozilla.search.PreSearchFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="@dimen/search_bar_height"
+ android:layout_gravity="top"/>
+
+ <fragment
+ android:id="@+id/suggestions"
+ android:name="org.mozilla.search.autocomplete.SuggestionsFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="@dimen/search_bar_height"
+ android:layout_gravity="top"/>
+
+
+ <ImageButton
+ android:id="@+id/settings_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@android:color/transparent"
+ android:padding="15dp"
+ android:src="@drawable/ic_action_settings"
+ android:layout_gravity="bottom|right"
+ android:contentDescription="@string/search_pref_button_content_description"/>
+
+ <View
+ android:id="@+id/animation_card"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="@dimen/search_bar_height"
+ android:background="@color/row_background"
+ android:visibility="invisible"
+ android:layout_gravity="top"/>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/search_bar.xml b/mobile/android/base/resources/layout/search_bar.xml
new file mode 100644
index 0000000000..69232ff07f
--- /dev/null
+++ b/mobile/android/base/resources/layout/search_bar.xml
@@ -0,0 +1,43 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <org.mozilla.search.ui.BackCaptureEditText
+ android:id="@+id/edit_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:imeOptions="actionSearch"
+ android:inputType="textNoSuggestions"
+ android:drawableLeft="@drawable/search_icon_inactive"
+ android:drawablePadding="5dp"
+ android:textSize="@dimen/query_text_size"
+ android:focusable="false"
+ android:focusableInTouchMode="false"
+ android:textColorHighlight="@color/fennec_ui_orange"
+ android:textSelectHandle="@drawable/handle_middle"
+ android:textSelectHandleLeft="@drawable/handle_start"
+ android:textSelectHandleRight="@drawable/handle_end" />
+
+ <ImageButton
+ android:id="@+id/clear_button"
+ android:layout_width="30dp"
+ android:layout_height="30dp"
+ android:paddingLeft="10dp"
+ android:layout_gravity="right|center_vertical"
+ android:background="@android:color/transparent"
+ android:src="@drawable/search_clear"
+ android:scaleType="centerInside"
+ android:visibility="gone"/>
+
+ <ImageView
+ android:id="@+id/engine_icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_gravity="right|center_vertical"
+ android:background="@android:color/transparent"
+ android:visibility="gone"/>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/search_empty.xml b/mobile/android/base/resources/layout/search_empty.xml
new file mode 100644
index 0000000000..9177a9dde9
--- /dev/null
+++ b/mobile/android/base/resources/layout/search_empty.xml
@@ -0,0 +1,51 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:gravity="center"
+ android:paddingLeft="50dp"
+ android:paddingRight="50dp">
+
+ <!-- Empty spacer view -->
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1"/>
+
+ <ImageView
+ android:id="@+id/empty_image"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="15dp"
+ android:gravity="top|center"
+ android:scaleType="fitCenter"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:orientation="vertical"
+ android:layout_weight="3">
+
+ <TextView
+ android:id="@+id/empty_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="15dp"
+ style="@style/TextAppearance.EmptyView.Title"
+ android:gravity="center"/>
+
+ <TextView
+ android:id="@+id/empty_message"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/TextAppearance.EmptyView.Message"
+ android:gravity="center"/>
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/search_engine_bar_item.xml b/mobile/android/base/resources/layout/search_engine_bar_item.xml
new file mode 100644
index 0000000000..f8c546d938
--- /dev/null
+++ b/mobile/android/base/resources/layout/search_engine_bar_item.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- TwoWayView doesn't let us set the margin around items (except as
+ gecko:itemMargin, but that doesn't increase the hit area) so we
+ have to surround the main View by a ViewGroup to create a pressable margin.
+
+ Note: the layout_height values are shared with the parent
+ View (browser_search at the time of this writing).
+
+ The actual width of the FrameLayout is calculated at runtime by the
+ SearchEngineBar class to spread the icons across the device's width. -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/search_engine_icon_container"
+ android:layout_width="72dp"
+ android:layout_height="match_parent"
+ android:clickable="true"
+ android:background="@color/pressed_about_page_header_grey">
+
+ <!-- Width & height are set to make the Favicons as sharp as possible
+ based on asset size. -->
+ <ImageView
+ android:id="@+id/search_engine_icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_gravity="center"
+ android:scaleType="fitCenter"/>
+
+</FrameLayout>
diff --git a/mobile/android/base/resources/layout/search_engine_bar_label.xml b/mobile/android/base/resources/layout/search_engine_bar_label.xml
new file mode 100644
index 0000000000..057e9bf31c
--- /dev/null
+++ b/mobile/android/base/resources/layout/search_engine_bar_label.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- TwoWayView doesn't let us set the margin around items (except as
+ gecko:itemMargin, but that doesn't increase the hit area) so we
+ have to surround the main View by a ViewGroup to create a pressable margin. -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="48dp"
+ android:layout_height="match_parent"
+ android:clickable="true"
+ android:background="@color/pressed_about_page_header_grey">
+
+ <ImageView
+ android:id="@+id/search_engine_label"
+ android:layout_width="16dp"
+ android:layout_height="16dp"
+ android:layout_gravity="center"
+ android:scaleType="fitXY"/>
+
+</FrameLayout>
diff --git a/mobile/android/base/resources/layout/search_engine_row.xml b/mobile/android/base/resources/layout/search_engine_row.xml
new file mode 100644
index 0000000000..5c2f2dfb91
--- /dev/null
+++ b/mobile/android/base/resources/layout/search_engine_row.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <org.mozilla.gecko.widget.FaviconView android:id="@+id/suggestion_icon"
+ android:layout_width="@dimen/favicon_bg"
+ android:layout_height="@dimen/favicon_bg"
+ android:layout_marginLeft="16dip"
+ android:layout_marginRight="16dip"
+ android:layout_centerVertical="true"
+ android:minWidth="@dimen/favicon_bg"
+ android:minHeight="@dimen/favicon_bg"/>
+
+ <org.mozilla.gecko.widget.FlowLayout android:id="@+id/suggestion_layout"
+ android:layout_toRightOf="@id/suggestion_icon"
+ android:layout_centerVertical="true"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="15dp"
+ android:duplicateParentState="true">
+
+ <include layout="@layout/suggestion_item"
+ android:id="@+id/suggestion_user_entered"/>
+
+ </org.mozilla.gecko.widget.FlowLayout>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/search_fragment_post_search.xml b/mobile/android/base/resources/layout/search_fragment_post_search.xml
new file mode 100644
index 0000000000..4bbfd36b3a
--- /dev/null
+++ b/mobile/android/base/resources/layout/search_fragment_post_search.xml
@@ -0,0 +1,30 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <ProgressBar
+ android:id="@+id/progress_bar"
+ style="@android:style/Widget.ProgressBar.Horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/progress_bar_height"
+ android:progressDrawable="@drawable/progressbar"/>
+
+ <WebView
+ android:id="@+id/webview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <ViewStub
+ android:id="@+id/error_view_stub"
+ android:layout="@layout/search_empty"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="gone"/>
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/search_fragment_pre_search.xml b/mobile/android/base/resources/layout/search_fragment_pre_search.xml
new file mode 100644
index 0000000000..82ce15b1be
--- /dev/null
+++ b/mobile/android/base/resources/layout/search_fragment_pre_search.xml
@@ -0,0 +1,27 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:background="@color/toolbar_grey">
+
+ <ViewStub android:id="@+id/empty_view_stub"
+ android:layout="@layout/search_empty"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"/>
+
+ <ListView
+ android:id="@+id/list_view"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:scrollbarStyle="outsideOverlay"
+ android:paddingLeft="@dimen/search_row_padding"
+ android:paddingRight="@dimen/search_row_padding"/>
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/search_history_row.xml b/mobile/android/base/resources/layout/search_history_row.xml
new file mode 100644
index 0000000000..d47185bcc7
--- /dev/null
+++ b/mobile/android/base/resources/layout/search_history_row.xml
@@ -0,0 +1,14 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<TextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/site_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/search_row_background"
+ android:drawableLeft="@drawable/search_history"
+ android:drawablePadding="@dimen/search_history_drawable_padding"
+ android:padding="@dimen/search_row_padding"
+ android:textSize="@dimen/query_text_size"/>
diff --git a/mobile/android/base/resources/layout/search_sugestions.xml b/mobile/android/base/resources/layout/search_sugestions.xml
new file mode 100644
index 0000000000..1fb989fe8b
--- /dev/null
+++ b/mobile/android/base/resources/layout/search_sugestions.xml
@@ -0,0 +1,12 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<ListView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/toolbar_grey"
+ android:paddingLeft="@dimen/search_row_padding"
+ android:paddingRight="@dimen/search_row_padding"
+ android:visibility="invisible"/>
diff --git a/mobile/android/base/resources/layout/search_suggestions_row.xml b/mobile/android/base/resources/layout/search_suggestions_row.xml
new file mode 100644
index 0000000000..8c3f3e7a1c
--- /dev/null
+++ b/mobile/android/base/resources/layout/search_suggestions_row.xml
@@ -0,0 +1,31 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/search_row_background"
+ android:padding="@dimen/search_row_padding"
+ android:descendantFocusability="blocksDescendants"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/auto_complete_row_text"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:textSize="@dimen/query_text_size"/>
+
+ <ImageButton
+ android:id="@+id/auto_complete_row_jump_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ android:paddingLeft="@dimen/search_row_padding"
+ android:src="@drawable/search_plus"
+ android:contentDescription="@string/search_plus_content_description"
+ android:background="@android:color/transparent" />
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/search_widget.xml b/mobile/android/base/resources/layout/search_widget.xml
new file mode 100644
index 0000000000..e5db2d7746
--- /dev/null
+++ b/mobile/android/base/resources/layout/search_widget.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- A homescreen widget for launching Fennec or the search activity. We can't use styles in here
+ so make sure any changes you make are also made to launch_widget.xml which doesn't have
+ the search widget button. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/widget_header_height"
+ android:orientation="horizontal"
+ android:background="@drawable/widget_bg">
+
+ <ImageView android:id="@+id/logo_button"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:padding="@dimen/widget_padding"
+ android:background="@drawable/widget_button_left"
+ android:layout_height="match_parent"
+ android:src="@drawable/widget_icon"/>
+
+ <LinearLayout android:id="@+id/search_button"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="center"
+ android:contentDescription="@string/search_widget_button_label"
+ android:orientation="horizontal"
+ android:background="@drawable/widget_button_middle">
+
+ <TextView android:id="@+id/search_button_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawableLeft="@drawable/ic_widget_search"
+ android:drawablePadding="@dimen/widget_padding"
+ android:text="@string/search_widget_button_label"
+ android:gravity="center"
+ android:fontFamily="sans-serif"
+ android:textSize="@dimen/widget_text_size"
+ android:textColor="@color/toolbar_icon_grey"/>
+
+ </LinearLayout>
+
+ <LinearLayout android:id="@+id/new_tab_button"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/new_tab"
+ android:gravity="center"
+ android:orientation="horizontal"
+ android:background="@drawable/widget_button_right">
+
+ <TextView android:id="@+id/new_tab_button_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawableLeft="@drawable/ic_widget_new_tab"
+ android:drawablePadding="@dimen/widget_padding"
+ android:gravity="center"
+ android:text="@string/new_tab"
+ android:fontFamily="sans-serif"
+ android:textSize="@dimen/widget_text_size"
+ android:textColor="@color/toolbar_icon_grey"/>
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/select_dialog_list.xml b/mobile/android/base/resources/layout/select_dialog_list.xml
new file mode 100644
index 0000000000..ed6a7ecea3
--- /dev/null
+++ b/mobile/android/base/resources/layout/select_dialog_list.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- This is used for select lists for multiple selection enabled -->
+<ListView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/select_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:cacheColorHint="@null"
+ android:scrollbars="vertical"
+ android:overScrollMode="ifContentScrolls" />
diff --git a/mobile/android/base/resources/layout/select_dialog_multichoice.xml b/mobile/android/base/resources/layout/select_dialog_multichoice.xml
new file mode 100644
index 0000000000..0fc9f94947
--- /dev/null
+++ b/mobile/android/base/resources/layout/select_dialog_multichoice.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/text1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.Widget.TextView"
+ style="@style/Widget.ListItem"/>
diff --git a/mobile/android/base/resources/layout/select_dialog_singlechoice.xml b/mobile/android/base/resources/layout/select_dialog_singlechoice.xml
new file mode 100644
index 0000000000..0bffa830d5
--- /dev/null
+++ b/mobile/android/base/resources/layout/select_dialog_singlechoice.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/text1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.Widget.TextView"
+ style="@style/Widget.ListItem"
+ android:checkMark="?android:attr/listChoiceIndicatorSingle"
+ android:ellipsize="marquee"/>
diff --git a/mobile/android/base/resources/layout/site_identity.xml b/mobile/android/base/resources/layout/site_identity.xml
new file mode 100644
index 0000000000..b74c97c832
--- /dev/null
+++ b/mobile/android/base/resources/layout/site_identity.xml
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingLeft="@dimen/doorhanger_section_padding_small"
+ android:paddingRight="@dimen/doorhanger_section_padding_small"
+ android:paddingBottom="@dimen/doorhanger_section_padding_large"
+ android:paddingTop="@dimen/doorhanger_section_padding_medium">
+
+ <ImageView android:id="@+id/site_identity_icon"
+ android:layout_width="@dimen/doorhanger_icon_size"
+ android:layout_height="@dimen/doorhanger_icon_size"
+ android:gravity="center_horizontal"
+ android:padding="@dimen/doorhanger_section_padding_small"
+ android:layout_marginRight="@dimen/doorhanger_section_padding_small"/>
+
+ <LinearLayout android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_weight="1.0">
+
+ <TextView android:id="@+id/site_identity_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.DoorHanger.Medium.Light"/>
+
+ <TextView android:id="@+id/site_identity_state"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/doorhanger_subsection_padding"
+ android:textAppearance="@style/TextAppearance.DoorHanger.Medium.Bold"/>
+
+ <TextView android:id="@+id/mixed_content_activity"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/doorhanger_section_padding_medium"
+ android:textAppearance="@style/TextAppearance.DoorHanger.Medium"
+ android:visibility="gone"/>
+
+ <LinearLayout android:id="@+id/site_identity_known_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:orientation="vertical">
+
+ <TextView android:id="@+id/owner"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/doorhanger_section_padding_small"
+ android:textAppearance="@style/TextAppearance.DoorHanger.Medium"
+ android:textStyle="bold"/>
+
+ <TextView android:id="@+id/owner_supplemental"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.DoorHanger.Medium"/>
+
+ <TextView android:id="@+id/verifier"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/doorhanger_section_padding_medium"
+ android:textAppearance="@style/TextAppearance.DoorHanger.Medium.Light"/>
+
+ </LinearLayout>
+
+ <TextView android:id="@+id/site_identity_link"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.DoorHanger.Medium"
+ android:textColor="@color/link_blue"
+ android:layout_marginTop="@dimen/doorhanger_section_padding_large"
+ android:text="@string/learn_more"
+ android:visibility="gone"/>
+
+ <TextView android:id="@+id/site_settings_link"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.DoorHanger.Medium"
+ android:textColor="@color/link_blue"
+ android:layout_marginTop="@dimen/doorhanger_section_padding_large"
+ android:text="@string/contextmenu_site_settings"
+ android:visibility="gone"/>
+ </LinearLayout>
+ </LinearLayout>
+
+ <View android:id="@+id/divider_doorhanger"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/toolbar_divider_grey"
+ android:visibility="gone"/>
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/site_setting_item.xml b/mobile/android/base/resources/layout/site_setting_item.xml
new file mode 100644
index 0000000000..a8feba7e3e
--- /dev/null
+++ b/mobile/android/base/resources/layout/site_setting_item.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<org.mozilla.gecko.widget.CheckableLinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:paddingLeft="16dip"
+ android:paddingRight="12dip"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:focusable="false">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center_vertical"
+ android:focusable="false">
+
+ <TextView
+ android:id="@+id/setting"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ style="Widget.ListItem"
+ android:gravity="center_vertical|left"
+ android:singleLine="true"
+ android:ellipsize="marquee"/>
+
+ <TextView
+ android:id="@+id/value"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ style="Widget.ListItem"
+ android:gravity="center_vertical|left"
+ android:singleLine="true"
+ android:ellipsize="marquee"/>
+
+ </LinearLayout>
+
+ <android.support.v7.widget.AppCompatCheckBox
+ android:id="@+id/checkbox"
+ android:layout_width="35dip"
+ android:layout_height="wrap_content"
+ android:paddingRight="12dip"
+ android:gravity="center_vertical"
+ android:focusable="false"
+ android:clickable="false"/>
+
+</org.mozilla.gecko.widget.CheckableLinearLayout>
diff --git a/mobile/android/base/resources/layout/suggestion_item.xml b/mobile/android/base/resources/layout/suggestion_item.xml
new file mode 100644
index 0000000000..8dc0dbe6c4
--- /dev/null
+++ b/mobile/android/base/resources/layout/suggestion_item.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:minHeight="32dp"
+ android:orientation="horizontal"
+ android:background="@drawable/search_suggestion_button"
+ android:gravity="center_vertical"
+ android:clickable="true"
+ android:padding="5dp">
+
+ <ImageView android:id="@+id/suggestion_item_icon"
+ android:src="@drawable/icon_most_recent_empty"
+ android:layout_marginRight="3dip"
+ android:layout_width="18dip"
+ android:layout_height="18dip"
+ android:visibility="gone"/>
+
+ <TextView android:id="@+id/suggestion_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/text_and_tabs_tray_grey"
+ android:textSize="14sp"
+ android:gravity="center_vertical"
+ android:layout_gravity="center_vertical"/>
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/tab_history_item_row.xml b/mobile/android/base/resources/layout/tab_history_item_row.xml
new file mode 100644
index 0000000000..c9de095061
--- /dev/null
+++ b/mobile/android/base/resources/layout/tab_history_item_row.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko= "http://schemas.android.com/apk/res-auto"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:background="@color/state_pressed_toolbar_grey_pressed" >
+
+ <LinearLayout android:id="@+id/tab_history_timeline_combo"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_marginLeft="@dimen/tab_history_combo_margin_left"
+ android:layout_marginRight="@dimen/tab_history_combo_margin_right" >
+
+ <ImageView android:id="@+id/tab_history_timeline_top"
+ android:layout_width="@dimen/tab_history_timeline_width"
+ android:layout_height="@dimen/tab_history_timeline_height"
+ android:layout_gravity="center_horizontal"
+ android:background="@color/tab_history_timeline_separator" />
+
+ <org.mozilla.gecko.widget.FaviconView android:id="@+id/tab_history_icon"
+ android:layout_width="@dimen/tab_history_favicon_bg"
+ android:layout_height="@dimen/tab_history_favicon_bg"
+ android:background="@drawable/tab_history_icon_state"
+ android:padding="@dimen/tab_history_favicon_padding"
+ android:scaleType="centerInside"
+ gecko:overrideScaleType="false"
+ gecko:dominantBorderEnabled="false" />
+
+ <ImageView android:id="@+id/tab_history_timeline_bottom"
+ android:layout_width="@dimen/tab_history_timeline_width"
+ android:layout_height="@dimen/tab_history_timeline_height"
+ android:layout_gravity="center_horizontal"
+ android:background="@color/tab_history_timeline_separator" />
+
+ </LinearLayout>
+
+ <!-- HACK: Widget.TwoLinePageRow overrides the background attr but really shouldn't
+ (bug 1271797). So we override the override and set the background equal to null. -->
+ <org.mozilla.gecko.widget.FadedSingleColorTextView
+ android:id="@+id/tab_history_title"
+ style="@style/Widget.TwoLinePageRow.Title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:background="@null"
+ android:paddingRight="@dimen/tab_history_title_margin_right"
+ android:text="@+id/tab_history_title"
+ android:textSize="@dimen/tab_history_title_text_size"
+ android:textColor="@color/text_and_tabs_tray_grey"
+ gecko:fadeWidth="@dimen/tab_history_title_fading_width"/>
+
+</LinearLayout>
diff --git a/mobile/android/base/resources/layout/tab_history_layout.xml b/mobile/android/base/resources/layout/tab_history_layout.xml
new file mode 100644
index 0000000000..010ab6c500
--- /dev/null
+++ b/mobile/android/base/resources/layout/tab_history_layout.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <ListView android:id="@+id/tab_history_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:background="@drawable/tab_history_bg"
+ android:divider="@null" />
+</RelativeLayout>
diff --git a/mobile/android/base/resources/layout/tab_menu_strip.xml b/mobile/android/base/resources/layout/tab_menu_strip.xml
new file mode 100644
index 0000000000..b3d3952b8f
--- /dev/null
+++ b/mobile/android/base/resources/layout/tab_menu_strip.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:minWidth="@dimen/tabs_strip_button_width"
+ android:background="@drawable/tabs_strip_indicator"
+ android:paddingLeft="@dimen/tabs_strip_button_padding"
+ android:paddingRight="@dimen/tabs_strip_button_padding"
+ android:gravity="center"
+ android:focusable="true"
+ style="@style/TextAppearance.Widget.HomePagerTabMenuStrip"/>
diff --git a/mobile/android/base/resources/layout/tab_prompt_input.xml b/mobile/android/base/resources/layout/tab_prompt_input.xml
new file mode 100644
index 0000000000..72a73b7028
--- /dev/null
+++ b/mobile/android/base/resources/layout/tab_prompt_input.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<TabHost xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/tabhost"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:minHeight="100dp">
+
+ <TabWidget
+ android:id="@android:id/tabs"
+ style="@style/TabInput.TabWidget"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" >
+ </TabWidget>
+
+ <FrameLayout
+ android:id="@android:id/tabcontent"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+ </FrameLayout>
+
+ </LinearLayout>
+
+</TabHost>
diff --git a/mobile/android/base/resources/layout/tab_queue_prompt.xml b/mobile/android/base/resources/layout/tab_queue_prompt.xml
new file mode 100644
index 0000000000..d37b431d3e
--- /dev/null
+++ b/mobile/android/base/resources/layout/tab_queue_prompt.xml
@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipChildren="false"
+ android:clipToPadding="false">
+
+ <LinearLayout
+ android:id="@+id/tab_queue_container"
+ android:layout_width="@dimen/overlay_prompt_container_width"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|center"
+ android:background="@android:color/white"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="@dimen/overlay_prompt_content_width"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:fontFamily="sans-serif-light"
+ android:gravity="center_horizontal"
+ android:paddingTop="40dp"
+ android:text="@string/tab_queue_prompt_title"
+ android:textColor="@color/text_and_tabs_tray_grey"
+ android:textSize="20sp"
+
+ tools:text="Opening multiple links?" />
+
+ <TextView
+ android:id="@+id/text"
+ android:layout_width="@dimen/overlay_prompt_content_width"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:lineSpacingMultiplier="1.25"
+ android:paddingTop="20dp"
+ android:text="@string/tab_queue_prompt_text"
+ android:textColor="@color/placeholder_grey"
+ android:textSize="16sp"
+ tools:text="Save them until the next time you open Firefox." />
+
+ <TextView
+ android:id="@+id/tip_text"
+ android:layout_width="@dimen/overlay_prompt_content_width"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:paddingBottom="30dp"
+ android:paddingTop="20dp"
+ android:text="@string/tab_queue_prompt_tip_text"
+ android:textColor="@color/action_orange"
+ android:textSize="14sp"
+ android:textStyle="italic"
+ tools:text="you can change this later in Settings" />
+
+ <TextView
+ android:id="@+id/settings_permit_text"
+ android:layout_width="@dimen/overlay_prompt_content_width"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:paddingBottom="30dp"
+ android:paddingTop="20dp"
+ android:text="@string/tab_queue_prompt_permit_drawing_over_apps"
+ android:textColor="@color/action_orange"
+ android:textSize="14sp"
+ android:textStyle="italic"
+ tools:text="Turn on Permit drawing over other apps" />
+
+ <FrameLayout
+ android:id="@+id/bottom_container"
+ android:layout_width="match_parent"
+ android:layout_height="52dp"
+ android:layout_gravity="center"
+ android:layout_marginBottom="40dp">
+
+ <ImageView
+ android:id="@+id/enabled_confirmation"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:src="@drawable/img_check"
+ android:visibility="gone" />
+
+ <LinearLayout
+ android:id="@+id/button_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_horizontal"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/cancel_button"
+ style="@style/Widget.BaseButton"
+ android:layout_width="@dimen/overlay_prompt_button_width"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:background="@color/android:white"
+ android:text="@string/tab_queue_prompt_negative_action_button"
+ android:textColor="@drawable/tab_queue_dismiss_button_foreground"
+ android:textSize="16sp"
+
+ tools:text="Not now" />
+
+ <Button
+ android:id="@+id/ok_button"
+ style="@style/Widget.BaseButton"
+ android:layout_width="@dimen/overlay_prompt_button_width"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:background="@drawable/button_background_action_orange_round"
+ android:text="@string/tab_queue_prompt_positive_action_button"
+ android:textColor="@android:color/white"
+ android:textSize="16sp"
+ tools:text="Enable" />
+
+ <Button
+ android:id="@+id/settings_button"
+ style="@style/Widget.BaseButton"
+ android:layout_width="@dimen/overlay_prompt_button_width"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:background="@drawable/button_background_action_orange_round"
+ android:text="@string/tab_queue_prompt_settings_button"
+ android:textColor="@android:color/white"
+ android:textSize="16sp"
+ tools:text="Go to settings" />
+
+ </LinearLayout>
+
+ </FrameLayout>
+
+ </LinearLayout>
+</merge>
diff --git a/mobile/android/base/resources/layout/tab_queue_toast.xml b/mobile/android/base/resources/layout/tab_queue_toast.xml
new file mode 100644
index 0000000000..9aaf60f943
--- /dev/null
+++ b/mobile/android/base/resources/layout/tab_queue_toast.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- (lint: UselessParent) The second-outermost layout doesn't have a parent to position itself
+ against and would take up the whole screen without the outermost layout. -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ tools:ignore="UselessParent">
+
+ <LinearLayout
+ android:id="@+id/toast"
+ style="@style/Toast">
+
+ <TextView
+ android:id="@+id/toast_message"
+ style="@style/ToastMessage"
+
+ tools:text="Tab queued in firefox" />
+
+ <View
+ android:id="@+id/toast_divider"
+ style="@style/ToastDivider" />
+
+ <Button
+ android:id="@+id/toast_button"
+ style="@style/ToastButton"
+ android:drawableLeft="@drawable/switch_button_icon"
+
+ tools:text="Open" />
+
+ </LinearLayout>
+</FrameLayout> \ No newline at end of file
diff --git a/mobile/android/base/resources/layout/tab_strip.xml b/mobile/android/base/resources/layout/tab_strip.xml
new file mode 100644
index 0000000000..fd9d885559
--- /dev/null
+++ b/mobile/android/base/resources/layout/tab_strip.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<org.mozilla.gecko.tabs.TabStrip
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/text_and_tabs_tray_grey"/>
diff --git a/mobile/android/base/resources/layout/tab_strip_inner.xml b/mobile/android/base/resources/layout/tab_strip_inner.xml
new file mode 100644
index 0000000000..8e78248184
--- /dev/null
+++ b/mobile/android/base/resources/layout/tab_strip_inner.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <org.mozilla.gecko.tabs.TabStripView
+ android:id="@+id/tab_strip"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:paddingTop="8dp"/>
+
+ <!-- The right margin creates a "dead area" on the right side of the button
+ which we compensate for with a touch delegate. See TabStrip -->
+ <org.mozilla.gecko.widget.themed.ThemedImageButton
+ android:id="@+id/add_tab"
+ style="@style/UrlBar.ImageButton"
+ android:layout_width="@dimen/tablet_tab_strip_height"
+ android:src="@drawable/tab_new"
+ gecko:drawableTintList="@color/tab_new_tab_strip_colors"
+ android:contentDescription="@string/new_tab"
+ android:layout_marginRight="9dp"
+ android:background="@drawable/tab_strip_button"/>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/tab_strip_item.xml b/mobile/android/base/resources/layout/tab_strip_item.xml
new file mode 100644
index 0000000000..73412458dd
--- /dev/null
+++ b/mobile/android/base/resources/layout/tab_strip_item.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- The paddings are asymmetric here to compensate the padding around the
+ the close button within the TabStripItemView -->
+<org.mozilla.gecko.tabs.TabStripItemView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/tablet_tab_strip_item_width"
+ android:layout_height="match_parent"
+ android:paddingLeft="28dp"
+ android:paddingRight="12dp"/>
diff --git a/mobile/android/base/resources/layout/tab_strip_item_view.xml b/mobile/android/base/resources/layout/tab_strip_item_view.xml
new file mode 100644
index 0000000000..2f56b7702f
--- /dev/null
+++ b/mobile/android/base/resources/layout/tab_strip_item_view.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <ImageView
+ android:id="@+id/favicon"
+ android:layout_width="@dimen/browser_toolbar_favicon_size"
+ android:layout_height="match_parent"
+ android:layout_marginRight="8dp"
+ android:scaleType="centerInside"
+ android:duplicateParentState="true"/>
+
+ <org.mozilla.gecko.widget.FadedSingleColorTextView
+ android:id="@+id/title"
+ android:layout_width="0dip"
+ android:layout_height="match_parent"
+ android:layout_weight="1.0"
+ android:layout_marginRight="-5dp"
+ android:drawablePadding="6dp"
+ android:gravity="center_vertical"
+ android:textSize="14sp"
+ android:ellipsize="end"
+ android:textColor="@color/tab_strip_item_title"
+ android:maxLines="1"
+ gecko:fadeWidth="30dip"
+ android:duplicateParentState="true"/>
+
+ <org.mozilla.gecko.widget.themed.ThemedImageButton
+ android:id="@+id/close"
+ android:layout_width="40dip"
+ android:layout_height="match_parent"
+ android:background="@android:color/transparent"
+ android:scaleType="center"
+ android:contentDescription="@string/close_tab"
+ android:src="@drawable/tab_close"
+ android:duplicateParentState="true"/>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/tabs_counter.xml b/mobile/android/base/resources/layout/tabs_counter.xml
new file mode 100644
index 0000000000..e410812513
--- /dev/null
+++ b/mobile/android/base/resources/layout/tabs_counter.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<org.mozilla.gecko.widget.themed.ThemedTextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="24dip"
+ android:layout_height="24dip"
+ android:layout_margin="12dip"
+ android:paddingTop="2dip"
+ android:paddingLeft="4dip"
+ android:background="@drawable/tabs_count_foreground"
+ android:textAppearance="@style/TextAppearance.Micro"
+ android:textColor="@color/text_and_tabs_tray_grey"
+ android:textStyle="bold"
+ android:duplicateParentState="true"
+ android:gravity="center"/>
diff --git a/mobile/android/base/resources/layout/tabs_layout_item_view.xml b/mobile/android/base/resources/layout/tabs_layout_item_view.xml
new file mode 100644
index 0000000000..75f35e4679
--- /dev/null
+++ b/mobile/android/base/resources/layout/tabs_layout_item_view.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<org.mozilla.gecko.tabs.TabsLayoutItemView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto"
+ style="@style/TabsItem"
+ android:id="@+id/info"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="vertical">
+
+ <LinearLayout android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:duplicateParentState="true"
+ android:paddingLeft="@dimen/tab_highlight_stroke_width"
+ android:paddingRight="@dimen/tab_highlight_stroke_width"
+ android:paddingBottom="@dimen/tab_highlight_stroke_width">
+
+ <org.mozilla.gecko.widget.FadedSingleColorTextView
+ android:id="@+id/title"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1.0"
+ style="@style/TabLayoutItemTextAppearance"
+ android:textSize="14sp"
+ android:textColor="@color/tab_item_title"
+ android:singleLine="true"
+ android:duplicateParentState="true"
+ gecko:fadeWidth="15dp"
+ android:paddingRight="5dp"
+ android:drawablePadding="6dp"/>
+
+ <!-- Use of baselineAlignBottom only supported from API 11+ - if this needs to work on lower API versions
+ we'll need to override getBaseLine() and return image height, but we assume this won't happen -->
+ <ImageView android:id="@+id/close"
+ style="@style/TabsItemClose"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:scaleType="center"
+ android:baselineAlignBottom="true"
+ android:background="@android:color/transparent"
+ android:contentDescription="@string/close_tab"
+ android:src="@drawable/tab_item_close_button"
+ android:duplicateParentState="true"/>
+
+ </LinearLayout>
+
+ <!-- We set state_private on this View dynamically in TabsGridLayout. -->
+ <org.mozilla.gecko.widget.TabThumbnailWrapper
+ android:id="@+id/wrapper"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/tab_highlight_stroke_width"
+ android:background="@drawable/tab_thumbnail"
+ android:duplicateParentState="true"
+ android:clipToPadding="false">
+
+ <org.mozilla.gecko.tabs.TabsPanelThumbnailView android:id="@+id/thumbnail"
+ android:layout_width="@dimen/tab_thumbnail_width"
+ android:layout_height="@dimen/tab_thumbnail_height"
+ android:elevation="2dp"
+ android:outlineProvider="bounds"
+ />
+
+ </org.mozilla.gecko.widget.TabThumbnailWrapper>
+
+</org.mozilla.gecko.tabs.TabsLayoutItemView>
diff --git a/mobile/android/base/resources/layout/tabs_list_item_view.xml b/mobile/android/base/resources/layout/tabs_list_item_view.xml
new file mode 100644
index 0000000000..8c81e8095a
--- /dev/null
+++ b/mobile/android/base/resources/layout/tabs_list_item_view.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<org.mozilla.gecko.tabs.TabsLayoutItemView xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/TabsItem"
+ android:focusable="true"
+ android:id="@+id/info"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="12dip"
+ android:paddingTop="6dip"
+ android:paddingBottom="6dip"
+ android:background="@drawable/tab_row">
+
+ <!-- We set state_private on this View dynamically in TabsListLayout. -->
+ <org.mozilla.gecko.widget.TabThumbnailWrapper
+ android:id="@+id/wrapper"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="4dip"
+ android:background="@drawable/tab_thumbnail"
+ android:duplicateParentState="true"
+ android:clipToPadding="false">
+
+ <org.mozilla.gecko.tabs.TabsPanelThumbnailView android:id="@+id/thumbnail"
+ android:layout_width="@dimen/tab_thumbnail_width"
+ android:layout_height="@dimen/tab_thumbnail_height"
+ android:elevation="2dp"
+ android:outlineProvider="bounds"/>
+
+ </org.mozilla.gecko.widget.TabThumbnailWrapper>
+
+ <LinearLayout android:layout_width="0dip"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:layout_weight="1.0"
+ android:paddingTop="4dip"
+ android:paddingLeft="8dip"
+ android:paddingRight="4dip"
+ android:duplicateParentState="true">
+
+ <TextView android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1.0"
+ style="@style/TabLayoutItemTextAppearance"
+ android:textColor="@color/tab_item_title"
+ android:textSize="14sp"
+ android:gravity="center_vertical"
+ android:singleLine="false"
+ android:maxLines="4"
+ android:drawablePadding="6dp"
+ android:duplicateParentState="true"/>
+
+ </LinearLayout>
+
+ <ImageView android:id="@+id/close"
+ style="@style/TabsItemClose"
+ android:layout_width="34dip"
+ android:layout_height="match_parent"
+ android:scaleType="center"
+ android:contentDescription="@string/close_tab"
+ android:src="@drawable/tab_item_close_button"
+ android:duplicateParentState="true"/>
+
+</org.mozilla.gecko.tabs.TabsLayoutItemView> \ No newline at end of file
diff --git a/mobile/android/base/resources/layout/tabs_panel_default.xml b/mobile/android/base/resources/layout/tabs_panel_default.xml
new file mode 100644
index 0000000000..a5ef871e86
--- /dev/null
+++ b/mobile/android/base/resources/layout/tabs_panel_default.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <!-- The layout_height value is used in TabsPanel.getTabsLayoutContainerHeight
+ and as an offset in PrivateTabsPanel: if you change it here,
+ change it there! -->
+ <RelativeLayout android:id="@+id/tabs_panel_header"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/browser_toolbar_height">
+
+ <view class="org.mozilla.gecko.tabs.TabsPanel$TabsPanelToolbar"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/browser_toolbar_height"
+ android:background="@color/text_and_tabs_tray_grey">
+
+ <org.mozilla.gecko.tabs.TabPanelBackButton
+ android:id="@+id/nav_back"
+ android:layout_width="@dimen/tabs_panel_button_width"
+ android:layout_height="match_parent"
+ android:minWidth="@dimen/tabs_panel_button_width"
+ android:src="@drawable/tabs_panel_nav_back"
+ android:contentDescription="@string/back"
+ android:background="@drawable/action_bar_button_inverse"
+ gecko:dividerVerticalPadding="@dimen/tab_panel_divider_vertical_padding"
+ gecko:rightDivider="@drawable/tab_indicator_divider"/>
+
+ <org.mozilla.gecko.widget.IconTabWidget android:id="@+id/tab_widget"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:tabStripEnabled="false"
+ android:divider="@drawable/tab_indicator_divider"
+ android:dividerPadding="@dimen/tab_panel_divider_vertical_padding"
+ android:layout="@layout/tabs_panel_indicator"/>
+
+ <View android:layout_width="0dip"
+ android:layout_height="match_parent"
+ android:layout_weight="1.0"/>
+
+ <ImageButton android:id="@+id/add_tab"
+ style="@style/UrlBar.ImageButton"
+ android:layout_width="@dimen/tabs_panel_button_width"
+ android:padding="@dimen/browser_toolbar_button_padding"
+ android:src="@drawable/tab_new"
+ android:contentDescription="@string/new_tab"
+ android:background="@drawable/action_bar_button_inverse"/>
+
+ <FrameLayout android:id="@+id/menu"
+ style="@style/UrlBar.ImageButton"
+ android:layout_width="@dimen/tabs_panel_button_width"
+ android:background="@drawable/action_bar_button_inverse"
+ android:contentDescription="@string/menu">
+
+ <ImageView
+ style="@style/UrlBar.ImageButton"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/browser_toolbar_menu_icon_height"
+ android:layout_gravity="center"
+ android:scaleType="centerInside"
+ android:src="@drawable/menu"
+ android:tint="@color/tabs_tray_icon_grey"/>
+
+ </FrameLayout>
+
+ </view>
+
+ <View android:layout_width="match_parent"
+ android:layout_height="2dp"
+ android:layout_alignParentBottom="true"
+ android:background="#1A000000"/>
+
+ </RelativeLayout>
+
+ <FrameLayout
+ android:id="@+id/tabs_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <view class="org.mozilla.gecko.tabs.TabsPanel$TabsLayout"
+ android:id="@+id/normal_tabs"
+ style="@style/TabsLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:choiceMode="singleChoice"
+ android:visibility="gone"
+ gecko:tabs="tabs_normal"/>
+
+ <org.mozilla.gecko.tabs.PrivateTabsPanel
+ android:id="@+id/private_tabs_panel"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="gone"/>
+
+ </FrameLayout>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/tabs_panel_indicator.xml b/mobile/android/base/resources/layout/tabs_panel_indicator.xml
new file mode 100644
index 0000000000..64c9d4afc1
--- /dev/null
+++ b/mobile/android/base/resources/layout/tabs_panel_indicator.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<org.mozilla.gecko.widget.themed.ThemedImageButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/tabs_panel_indicator_width"
+ android:layout_height="@dimen/browser_toolbar_height"
+ android:minWidth="@dimen/tabs_panel_indicator_width"
+ android:paddingTop="@dimen/browser_toolbar_button_padding"
+ android:paddingBottom="@dimen/browser_toolbar_button_padding"
+ android:background="@drawable/tabs_panel_indicator"/>
diff --git a/mobile/android/base/resources/layout/tabs_panel_view.xml b/mobile/android/base/resources/layout/tabs_panel_view.xml
new file mode 100644
index 0000000000..1f70b28f57
--- /dev/null
+++ b/mobile/android/base/resources/layout/tabs_panel_view.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- The tabs panel is initial hidden on startup. It will be made
+ visible when the user activates it. See TabsPanel.show() -->
+<org.mozilla.gecko.tabs.TabsPanel xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/tabs_panel"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/text_and_tabs_tray_grey"
+ android:orientation="vertical"
+ android:visibility="invisible"/>
diff --git a/mobile/android/base/resources/layout/toolbar_display_layout.xml b/mobile/android/base/resources/layout/toolbar_display_layout.xml
new file mode 100644
index 0000000000..4caacb3797
--- /dev/null
+++ b/mobile/android/base/resources/layout/toolbar_display_layout.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <!-- The site security icon is misaligned with the page title so
+ we add a bottom margin to align their bottoms.
+ Site security icon must have exact position and size as search icon in
+ edit layout -->
+ <ImageButton android:id="@+id/site_security"
+ style="@style/UrlBar.ImageButton"
+ android:layout_width="@dimen/browser_toolbar_site_security_width"
+ android:layout_height="@dimen/browser_toolbar_site_security_height"
+ android:scaleType="fitCenter"
+ android:layout_marginRight="@dimen/browser_toolbar_site_security_margin_right"
+ android:layout_marginBottom="@dimen/browser_toolbar_site_security_margin_bottom"
+ android:paddingLeft="@dimen/browser_toolbar_site_security_padding_horizontal"
+ android:paddingRight="@dimen/browser_toolbar_site_security_padding_horizontal"
+ android:paddingTop="@dimen/browser_toolbar_site_security_padding_vertical"
+ android:paddingBottom="@dimen/browser_toolbar_site_security_padding_vertical"
+ android:src="@drawable/site_security_level"
+ android:contentDescription="@string/site_security"
+ android:layout_gravity="center_vertical" />
+
+ <org.mozilla.gecko.widget.FadedMultiColorTextView
+ android:id="@+id/url_bar_title"
+ style="@style/UrlBar.Title"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1.0"
+ gecko:fadeWidth="40dip"
+ gecko:fadeBackgroundColor="@color/toolbar_display_layout_bg"
+ gecko:autoUpdateTheme="false"/>
+
+ <org.mozilla.gecko.toolbar.PageActionLayout android:id="@+id/page_action_layout"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:visibility="gone"
+ android:orientation="horizontal"/>
+
+ <ImageButton android:id="@+id/stop"
+ android:layout_width="@dimen/page_action_button_width"
+ android:layout_height="match_parent"
+ android:src="@drawable/urlbar_stop"
+ android:contentDescription="@string/stop"
+ android:background="#00ffffff"
+ android:visibility="gone"/>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/toolbar_edit_layout.xml b/mobile/android/base/resources/layout/toolbar_edit_layout.xml
new file mode 100644
index 0000000000..c3d27fadfe
--- /dev/null
+++ b/mobile/android/base/resources/layout/toolbar_edit_layout.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <!-- Search icon must have exact position and size as site security in
+ display layout -->
+ <ImageView android:id="@+id/search_icon"
+ android:layout_width="@dimen/browser_toolbar_site_security_width"
+ android:layout_height="@dimen/browser_toolbar_site_security_height"
+ android:layout_marginBottom="@dimen/browser_toolbar_site_security_margin_bottom"
+ android:layout_marginRight="@dimen/browser_toolbar_site_security_margin_right"
+ android:paddingBottom="@dimen/browser_toolbar_site_security_padding_vertical"
+ android:paddingLeft="@dimen/browser_toolbar_site_security_padding_horizontal"
+ android:paddingRight="@dimen/browser_toolbar_site_security_padding_horizontal"
+ android:paddingTop="@dimen/browser_toolbar_site_security_padding_vertical"
+ android:scaleType="fitCenter"
+ android:src="@drawable/search_icon_inactive"
+ android:visibility="gone"/>
+
+ <org.mozilla.gecko.toolbar.ToolbarEditText
+ android:id="@+id/url_edit_text"
+ style="@style/UrlBar.Title"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1.0"
+ android:inputType="textUri|textNoSuggestions"
+ android:imeOptions="actionGo|flagNoExtractUi|flagNoFullscreen"
+ android:selectAllOnFocus="true"
+ android:contentDescription="@string/url_bar_default_text"
+ android:paddingRight="8dp"
+ gecko:autoUpdateTheme="false"/>
+
+ <ImageButton android:id="@+id/qrcode"
+ android:layout_width="@dimen/page_action_button_width"
+ android:layout_height="match_parent"
+ android:src="@drawable/ab_qrcode"
+ android:background="@android:color/transparent"/>
+
+ <ImageButton android:id="@+id/mic"
+ android:layout_width="@dimen/page_action_button_width"
+ android:layout_height="match_parent"
+ android:src="@drawable/ab_mic"
+ android:background="@android:color/transparent"/>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/top_sites_grid_item_view.xml b/mobile/android/base/resources/layout/top_sites_grid_item_view.xml
new file mode 100644
index 0000000000..6a76514a33
--- /dev/null
+++ b/mobile/android/base/resources/layout/top_sites_grid_item_view.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+ <org.mozilla.gecko.home.TopSitesThumbnailView
+ android:id="@+id/thumbnail"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"/>
+
+ <org.mozilla.gecko.widget.FadedSingleColorTextView
+ android:id="@+id/title"
+ style="@style/Widget.TopSitesGridItemTitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/thumbnail"
+ android:duplicateParentState="true"
+ android:drawablePadding="4dip"
+ gecko:fadeWidth="20dip"/>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/tracking_protection_prompt.xml b/mobile/android/base/resources/layout/tracking_protection_prompt.xml
new file mode 100644
index 0000000000..5fc993b09a
--- /dev/null
+++ b/mobile/android/base/resources/layout/tracking_protection_prompt.xml
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/tracking_protection_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipChildren="false"
+ android:clipToPadding="false">
+
+ <LinearLayout
+ android:id="@+id/tracking_protection_inner_container"
+ android:layout_width="@dimen/overlay_prompt_container_width"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|center"
+ android:background="@android:color/white"
+ android:orientation="vertical"
+ android:paddingBottom="40dp">
+
+ <ScrollView android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:fillViewport="true"
+ android:fadeScrollbars="false">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <ImageView android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/tracking_protection_toolbar_illustration"
+ android:layout_gravity="center"
+ android:layout_marginTop="40dp"
+ android:layout_marginBottom="20dp"/>
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="@dimen/overlay_prompt_content_width"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:fontFamily="sans-serif-light"
+ android:gravity="center_horizontal"
+ android:text="@string/tracking_protection_prompt_title"
+ android:textColor="@color/text_and_tabs_tray_grey"
+ android:textSize="20sp"
+
+ tools:text="Now with Tracking Protection"/>
+
+ <TextView
+ android:id="@+id/text"
+ android:layout_width="@dimen/overlay_prompt_content_width"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:lineSpacingMultiplier="1.25"
+ android:paddingTop="20dp"
+ android:text="@string/tracking_protection_prompt_text"
+ android:textColor="@color/placeholder_grey"
+ android:textSize="16sp"
+
+ tools:text="Actively block tracking elements so you don't have to worry."/>
+
+ <TextView
+ android:id="@+id/link_text"
+ android:layout_width="@dimen/overlay_prompt_content_width"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:paddingBottom="30dp"
+ android:paddingTop="20dp"
+ android:text="@string/tracking_protection_prompt_tip_text"
+ android:textColor="@color/link_blue"
+ android:textSize="14sp"
+
+ tools:text="Visit Privacy settings to learn more"/>
+
+ </LinearLayout>
+
+ </ScrollView>
+
+ <Button
+ android:id="@+id/ok_button"
+ style="@style/Widget.BaseButton"
+ android:layout_width="match_parent"
+
+ android:layout_height="52dp"
+ android:layout_gravity="center"
+ android:background="@drawable/button_background_action_orange_round"
+ android:text="@string/tracking_protection_prompt_action_button"
+ android:textColor="@android:color/white"
+ android:textSize="16sp"
+
+ android:layout_marginLeft="32dp"
+ android:layout_marginRight="32dp"
+ tools:text="Got it"/>
+
+ </LinearLayout>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/two_line_folder_row.xml b/mobile/android/base/resources/layout/two_line_folder_row.xml
new file mode 100644
index 0000000000..3ca52749f9
--- /dev/null
+++ b/mobile/android/base/resources/layout/two_line_folder_row.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto"
+ tools:context=".BrowserApp">
+
+ <ImageView android:id="@+id/icon"
+ android:src="@drawable/folder_closed"
+ android:layout_width="24dp"
+ android:layout_height="18dp"
+ android:scaleType="fitXY"
+ android:layout_margin="20dp"/>
+
+ <LinearLayout android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="center_vertical"
+ android:paddingRight="10dp"
+ android:orientation="vertical">
+
+ <org.mozilla.gecko.widget.FadedSingleColorTextView
+ android:id="@+id/title"
+ style="@style/Widget.TwoLinePageRow.Title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ gecko:fadeWidth="90dp"
+ tools:text="This is a long test title"/>
+
+ <org.mozilla.gecko.widget.FadedSingleColorTextView android:id="@+id/subtitle"
+ style="@style/Widget.TwoLinePageRow.Url"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ gecko:fadeWidth="90dp"
+ tools:text="1 items"/>
+
+ </LinearLayout>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/two_line_page_row.xml b/mobile/android/base/resources/layout/two_line_page_row.xml
new file mode 100644
index 0000000000..344f717d66
--- /dev/null
+++ b/mobile/android/base/resources/layout/two_line_page_row.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto"
+ tools:context=".BrowserApp">
+
+ <org.mozilla.gecko.widget.FaviconView android:id="@+id/icon"
+ android:layout_width="@dimen/favicon_bg"
+ android:layout_height="@dimen/favicon_bg"
+ android:layout_margin="16dp"
+ tools:background="@drawable/favicon_globe"/>
+
+ <LinearLayout android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="center_vertical"
+ android:paddingRight="10dp"
+ android:orientation="vertical">
+
+ <org.mozilla.gecko.widget.FadedSingleColorTextView
+ android:id="@+id/title"
+ style="@style/Widget.TwoLinePageRow.Title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ gecko:fadeWidth="90dp"
+ tools:text="This is a long test title"/>
+
+ <org.mozilla.gecko.widget.FadedSingleColorTextView android:id="@+id/url"
+ style="@style/Widget.TwoLinePageRow.Url"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawablePadding="5dp"
+ android:maxLength="1024"
+ gecko:fadeWidth="90dp"
+ tools:text="http://test.com/test"
+ tools:drawableLeft="@drawable/ic_url_bar_tab"/>
+
+ </LinearLayout>
+
+ <ImageView android:id="@+id/status_icon_bookmark"
+ android:layout_width="20dp"
+ android:layout_height="20dp"
+ android:layout_gravity="center"
+ android:visibility="gone"
+ android:src="@drawable/star_blue"/>
+
+</merge>
diff --git a/mobile/android/base/resources/layout/validation_message.xml b/mobile/android/base/resources/layout/validation_message.xml
new file mode 100644
index 0000000000..abcc2ef1e4
--- /dev/null
+++ b/mobile/android/base/resources/layout/validation_message.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="50dip">
+
+ <TextView android:id="@+id/validation_message_text"
+ android:layout_width="wrap_content"
+ android:layout_height="40dip"
+ android:layout_marginTop="6dip"
+ android:paddingLeft="20dp"
+ android:paddingRight="20dp"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="@color/validation_message_text"
+ android:background="@drawable/validation_bg"
+ android:gravity="center"
+ android:singleLine="true"
+ android:scrollHorizontally="true"
+ android:ellipsize="marquee"/>
+
+ <ImageView android:id="@+id/validation_message_arrow"
+ android:layout_width="24dip"
+ android:layout_height="10dip"
+ android:layout_centerHorizontal="true"
+ android:layout_alignParentTop="true"
+ android:src="@drawable/validation_arrow"
+ android:scaleType="fitXY"/>
+
+ <ImageView android:id="@+id/validation_message_arrow_inverted"
+ android:layout_width="24dip"
+ android:layout_height="10dip"
+ android:layout_centerHorizontal="true"
+ android:layout_marginTop="35dip"
+ android:layout_alignParentTop="true"
+ android:src="@drawable/validation_arrow_inverted"
+ android:scaleType="fitXY"
+ android:visibility="gone"/>
+
+</RelativeLayout>
diff --git a/mobile/android/base/resources/layout/zoomed_view.xml b/mobile/android/base/resources/layout/zoomed_view.xml
new file mode 100644
index 0000000000..fd5684303a
--- /dev/null
+++ b/mobile/android/base/resources/layout/zoomed_view.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+
+<org.mozilla.gecko.ZoomedView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/zoomed_view_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/dropshadow"
+ android:padding="@dimen/drawable_dropshadow_size"
+ android:visibility="gone">
+
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:background="@drawable/toolbar_grey_round">
+ <!--
+ Zoom factor button is invisible by default. Set ui.zoomedview.simplified to false
+ in order to display the button in the zoomed view tool bar
+ -->
+ <TextView
+ android:id="@+id/change_zoom_factor"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/zoomed_view_toolbar_height"
+ android:background="@android:color/transparent"
+ android:padding="12dip"
+ android:layout_alignLeft="@+id/zoomed_image_view"
+ android:textSize="16sp"
+ android:textColor="@color/text_and_tabs_tray_grey"
+ android:visibility="invisible"/>
+ <ImageView
+ android:id="@+id/dialog_close"
+ android:scaleType="center"
+ android:layout_width="@dimen/zoomed_view_toolbar_height"
+ android:layout_height="@dimen/zoomed_view_toolbar_height"
+ android:layout_alignRight="@id/zoomed_image_view"
+ android:src="@drawable/close_edit_mode_selector"/>
+ <ImageView
+ android:id="@id/zoomed_image_view"
+ android:layout_below="@id/change_zoom_factor"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+ </RelativeLayout>
+
+</org.mozilla.gecko.ZoomedView> \ No newline at end of file
diff --git a/mobile/android/base/resources/menu-large/browser_app_menu.xml b/mobile/android/base/resources/menu-large/browser_app_menu.xml
new file mode 100644
index 0000000000..a4a8dbd4ac
--- /dev/null
+++ b/mobile/android/base/resources/menu-large/browser_app_menu.xml
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- We disable AlwaysShowAction because we interpret the menu
+ attributes ourselves and thus the warning isn't relevant to us. -->
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:ignore="AlwaysShowAction">
+
+ <item android:id="@+id/reload"
+ android:icon="@drawable/ic_menu_reload"
+ android:title="@string/reload"
+ android:showAsAction="always"/>
+
+ <item android:id="@+id/back"
+ android:icon="@drawable/ic_menu_back"
+ android:title="@string/back"
+ android:visible="false"/>
+
+ <item android:id="@+id/forward"
+ android:icon="@drawable/ic_menu_forward"
+ android:title="@string/forward"
+ android:visible="false"/>
+
+ <item android:id="@+id/bookmark"
+ android:icon="@drawable/ic_menu_bookmark_add"
+ android:title="@string/bookmark"
+ android:showAsAction="ifRoom"/>
+
+ <item android:id="@+id/share"
+ android:icon="@drawable/ic_menu_share"
+ android:title="@string/share"
+ android:showAsAction="ifRoom"/>
+
+ <item android:id="@+id/new_tab"
+ android:title="@string/new_tab"/>
+
+ <item android:id="@+id/new_private_tab"
+ android:title="@string/new_private_tab"/>
+
+ <item android:id="@+id/bookmarks_list"
+ android:title="@string/bookmarks_title"/>
+
+ <item android:id="@+id/history_list"
+ android:title="@string/history_title"/>
+
+ <item android:id="@+id/find_in_page"
+ android:title="@string/find_in_page" />
+
+ <item android:id="@+id/desktop_mode"
+ android:title="@string/desktop_mode"
+ android:checkable="true" />
+
+ <item android:id="@+id/page"
+ android:title="@string/page">
+
+ <menu>
+
+ <item android:id="@+id/subscribe"
+ android:title="@string/contextmenu_subscribe"/>
+
+ <item android:id="@+id/save_as_pdf"
+ android:title="@string/save_as_pdf"/>
+
+ <item android:id="@+id/print"
+ android:title="@string/print"/>
+
+ <item android:id="@+id/add_search_engine"
+ android:title="@string/contextmenu_add_search_engine"/>
+
+ <item android:id="@+id/add_to_launcher"
+ android:title="@string/contextmenu_add_to_launcher"/>
+
+ </menu>
+
+ </item>
+
+ <item android:id="@+id/tools"
+ android:title="@string/tools">
+
+ <menu>
+
+ <item android:id="@+id/downloads"
+ android:title="@string/downloads"/>
+
+ <item android:id="@+id/addons"
+ android:title="@string/addons"/>
+
+ <item android:id="@+id/logins"
+ android:title="@string/logins"/>
+
+ <item android:id="@+id/new_guest_session"
+ android:visible="false"
+ android:title="@string/new_guest_session"/>
+
+ <item android:id="@+id/exit_guest_session"
+ android:visible="false"
+ android:title="@string/exit_guest_session"/>
+
+ </menu>
+
+ </item>
+
+ <item android:id="@+id/char_encoding"
+ android:visible="false"
+ android:title="@string/char_encoding"/>
+
+ <item android:id="@+id/settings"
+ android:title="@string/settings" />
+
+ <item android:id="@+id/help"
+ android:title="@string/help_menu" />
+
+</menu>
diff --git a/mobile/android/base/resources/menu-v11/preferences_search_menu.xml b/mobile/android/base/resources/menu-v11/preferences_search_menu.xml
new file mode 100644
index 0000000000..11fe2cadd3
--- /dev/null
+++ b/mobile/android/base/resources/menu-v11/preferences_search_menu.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@+id/restore_defaults"
+ android:drawable="@drawable/menu"
+ android:showAsAction="never"
+ android:title="@string/pref_search_restore_defaults" />
+</menu>
diff --git a/mobile/android/base/resources/menu-v11/tabs_menu.xml b/mobile/android/base/resources/menu-v11/tabs_menu.xml
new file mode 100644
index 0000000000..bd974e25b0
--- /dev/null
+++ b/mobile/android/base/resources/menu-v11/tabs_menu.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:id="@+id/new_tab"
+ android:title="@string/new_tab"/>
+
+ <item android:id="@+id/new_private_tab"
+ android:title="@string/new_private_tab"/>
+
+ <item android:id="@+id/close_all_tabs"
+ android:title="@string/close_all_tabs"/>
+
+ <item android:id="@+id/close_private_tabs"
+ android:title="@string/close_private_tabs"/>
+
+</menu>
diff --git a/mobile/android/base/resources/menu-v11/titlebar_contextmenu.xml b/mobile/android/base/resources/menu-v11/titlebar_contextmenu.xml
new file mode 100644
index 0000000000..0027f0542f
--- /dev/null
+++ b/mobile/android/base/resources/menu-v11/titlebar_contextmenu.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:id="@+id/pasteandgo"
+ android:title="@string/contextmenu_pasteandgo"/>
+
+ <item android:id="@+id/paste"
+ android:title="@string/contextmenu_paste"/>
+
+ <item android:id="@+id/copyurl"
+ android:title="@string/contextmenu_copyurl"/>
+
+ <item android:id="@+id/add_to_launcher"
+ android:title="@string/contextmenu_add_to_launcher"/>
+
+</menu>
diff --git a/mobile/android/base/resources/menu-xlarge/browser_app_menu.xml b/mobile/android/base/resources/menu-xlarge/browser_app_menu.xml
new file mode 100644
index 0000000000..817a8690a9
--- /dev/null
+++ b/mobile/android/base/resources/menu-xlarge/browser_app_menu.xml
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- We disable AlwaysShowAction because we interpret the menu
+ attributes ourselves and thus the warning isn't relevant to us. -->
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:ignore="AlwaysShowAction">
+
+ <item android:id="@+id/reload"
+ android:icon="@drawable/ic_menu_reload"
+ android:title="@string/reload"
+ android:showAsAction="always"/>
+
+ <item android:id="@+id/back"
+ android:icon="@drawable/ic_menu_back"
+ android:title="@string/back"
+ android:visible="false"/>
+
+ <item android:id="@+id/forward"
+ android:icon="@drawable/ic_menu_forward"
+ android:title="@string/forward"
+ android:visible="false"/>
+
+ <item android:id="@+id/bookmark"
+ android:icon="@drawable/ic_menu_bookmark_add"
+ android:title="@string/bookmark"
+ android:showAsAction="always"/>
+
+ <item android:id="@+id/share"
+ android:icon="@drawable/ic_menu_share"
+ android:title="@string/share"
+ android:showAsAction="ifRoom"/>
+
+ <item android:id="@+id/new_tab"
+ android:title="@string/new_tab"/>
+
+ <item android:id="@+id/new_private_tab"
+ android:title="@string/new_private_tab"/>
+
+ <item android:id="@+id/bookmarks_list"
+ android:title="@string/bookmarks_title"/>
+
+ <item android:id="@+id/history_list"
+ android:title="@string/history_title"/>
+
+ <item android:id="@+id/find_in_page"
+ android:title="@string/find_in_page" />
+
+ <item android:id="@+id/desktop_mode"
+ android:title="@string/desktop_mode"
+ android:checkable="true" />
+
+
+ <item android:id="@+id/page"
+ android:title="@string/page">
+
+ <menu>
+
+ <item android:id="@+id/subscribe"
+ android:title="@string/contextmenu_subscribe"/>
+
+ <item android:id="@+id/save_as_pdf"
+ android:title="@string/save_as_pdf"/>
+
+ <item android:id="@+id/print"
+ android:title="@string/print"/>
+
+ <item android:id="@+id/add_search_engine"
+ android:title="@string/contextmenu_add_search_engine"/>
+
+ <item android:id="@+id/add_to_launcher"
+ android:title="@string/contextmenu_add_to_launcher"/>
+
+ </menu>
+
+ </item>
+
+ <item android:id="@+id/tools"
+ android:title="@string/tools">
+
+ <menu>
+
+ <item android:id="@+id/downloads"
+ android:title="@string/downloads"/>
+
+ <item android:id="@+id/addons"
+ android:title="@string/addons"/>
+
+ <item android:id="@+id/logins"
+ android:title="@string/logins"/>
+
+ <item android:id="@+id/new_guest_session"
+ android:visible="false"
+ android:title="@string/new_guest_session"/>
+
+ <item android:id="@+id/exit_guest_session"
+ android:visible="false"
+ android:title="@string/exit_guest_session"/>
+
+ </menu>
+
+ </item>
+
+ <item android:id="@+id/char_encoding"
+ android:visible="false"
+ android:title="@string/char_encoding"/>
+
+ <item android:id="@+id/settings"
+ android:title="@string/settings" />
+
+ <item android:id="@+id/help"
+ android:title="@string/help_menu" />
+
+</menu>
diff --git a/mobile/android/base/resources/menu/activitystream_contextmenu.xml b/mobile/android/base/resources/menu/activitystream_contextmenu.xml
new file mode 100644
index 0000000000..ce22f14dc8
--- /dev/null
+++ b/mobile/android/base/resources/menu/activitystream_contextmenu.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- Group ID's are required, otherwise NavigationView won't show any dividers. The ID's are unused, but still required. -->
+ <group android:id="@+id/group0">
+ <item
+ android:id="@+id/bookmark"
+ android:icon="@drawable/as_bookmark"
+ android:title="@string/bookmark"/>
+ <item
+ android:id="@+id/share"
+ android:icon="@drawable/as_share"
+ android:title="@string/share"/>
+ <item
+ android:id="@+id/copy_url"
+ android:icon="@drawable/as_copy"
+ android:title="@string/contextmenu_copyurl"/>
+ <item
+ android:id="@+id/add_homescreen"
+ android:icon="@drawable/as_home"
+ android:title="@string/contextmenu_add_to_launcher"/>
+ </group>
+
+ <group android:id="@+id/group1">
+ <item
+ android:id="@+id/open_new_tab"
+ android:icon="@drawable/as_tab"
+ android:title="@string/contextmenu_open_new_tab"/>
+ <item
+ android:id="@+id/open_new_private_tab"
+ android:icon="@drawable/as_private"
+ android:title="@string/contextmenu_open_private_tab"/>
+ </group>
+
+
+ <group android:id="@+id/group2">
+ <item
+ android:id="@+id/dismiss"
+ android:icon="@drawable/as_dimiss"
+ android:title="@string/activity_stream_dismiss"/>
+
+ <item
+ android:id="@+id/delete"
+ android:icon="@drawable/as_bin"
+ android:visible="false"
+ android:title="@string/activity_stream_delete_history"/>
+ </group>
+</menu> \ No newline at end of file
diff --git a/mobile/android/base/resources/menu/browser_app_menu.xml b/mobile/android/base/resources/menu/browser_app_menu.xml
new file mode 100644
index 0000000000..f831ee7838
--- /dev/null
+++ b/mobile/android/base/resources/menu/browser_app_menu.xml
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- We disable AlwaysShowAction because we interpret the menu
+ attributes ourselves and thus the warning isn't relevant to us. -->
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:ignore="AlwaysShowAction">
+
+ <item android:id="@+id/back"
+ android:icon="@drawable/ic_menu_back"
+ android:title="@string/back"
+ android:showAsAction="always"/>
+
+ <item android:id="@+id/forward"
+ android:icon="@drawable/ic_menu_forward"
+ android:title="@string/forward"
+ android:showAsAction="always"/>
+
+ <item android:id="@+id/bookmark"
+ android:icon="@drawable/ic_menu_bookmark_add"
+ android:title="@string/bookmark"
+ android:showAsAction="always"/>
+
+ <item android:id="@+id/reload"
+ android:icon="@drawable/ic_menu_reload"
+ android:title="@string/reload"
+ android:showAsAction="always"/>
+
+ <item android:id="@+id/share"
+ android:icon="@drawable/ic_menu_share"
+ android:title="@string/share"
+ android:showAsAction="ifRoom"/>
+
+ <item android:id="@+id/new_tab"
+ android:title="@string/new_tab"/>
+
+ <item android:id="@+id/new_private_tab"
+ android:title="@string/new_private_tab"/>
+
+ <item android:id="@+id/bookmarks_list"
+ android:title="@string/bookmarks_title"/>
+
+ <item android:id="@+id/history_list"
+ android:title="@string/history_title"/>
+
+ <item android:id="@+id/find_in_page"
+ android:title="@string/find_in_page" />
+
+ <item android:id="@+id/desktop_mode"
+ android:title="@string/desktop_mode"
+ android:checkable="true" />
+
+ <item android:id="@+id/page"
+ android:title="@string/page">
+
+ <menu>
+
+ <item android:id="@+id/subscribe"
+ android:title="@string/contextmenu_subscribe"/>
+
+ <item android:id="@+id/save_as_pdf"
+ android:title="@string/save_as_pdf"/>
+
+ <item android:id="@+id/print"
+ android:title="@string/print"/>
+
+ <item android:id="@+id/add_search_engine"
+ android:title="@string/contextmenu_add_search_engine"/>
+
+ <item android:id="@+id/add_to_launcher"
+ android:title="@string/contextmenu_add_to_launcher"/>
+
+ </menu>
+
+ </item>
+
+ <item android:id="@+id/tools"
+ android:title="@string/tools">
+
+ <menu>
+
+ <item android:id="@+id/downloads"
+ android:title="@string/downloads"/>
+
+ <item android:id="@+id/addons"
+ android:title="@string/addons"/>
+
+ <item android:id="@+id/logins"
+ android:title="@string/logins"/>
+
+ <item android:id="@+id/new_guest_session"
+ android:visible="false"
+ android:title="@string/new_guest_session"/>
+
+ <item android:id="@+id/exit_guest_session"
+ android:visible="false"
+ android:title="@string/exit_guest_session"/>
+
+ </menu>
+
+ </item>
+
+ <item android:id="@+id/char_encoding"
+ android:visible="false"
+ android:title="@string/char_encoding"/>
+
+ <item android:id="@+id/settings"
+ android:title="@string/settings" />
+
+ <item android:id="@+id/help"
+ android:title="@string/help_menu" />
+
+</menu>
diff --git a/mobile/android/base/resources/menu/browsersearch_contextmenu.xml b/mobile/android/base/resources/menu/browsersearch_contextmenu.xml
new file mode 100644
index 0000000000..3354908252
--- /dev/null
+++ b/mobile/android/base/resources/menu/browsersearch_contextmenu.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:id="@+id/browsersearch_remove"
+ android:title="@string/contextmenu_remove"/>
+
+</menu>
diff --git a/mobile/android/base/resources/menu/gecko_app_menu.xml b/mobile/android/base/resources/menu/gecko_app_menu.xml
new file mode 100644
index 0000000000..a55804fcbc
--- /dev/null
+++ b/mobile/android/base/resources/menu/gecko_app_menu.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@+id/quit"
+ android:title="@string/quit"
+ android:orderInCategory="10" />
+</menu>
diff --git a/mobile/android/base/resources/menu/home_contextmenu.xml b/mobile/android/base/resources/menu/home_contextmenu.xml
new file mode 100644
index 0000000000..294b8aee50
--- /dev/null
+++ b/mobile/android/base/resources/menu/home_contextmenu.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:id="@+id/home_open_new_tab"
+ android:title="@string/contextmenu_open_new_tab"/>
+
+ <item android:id="@+id/home_open_private_tab"
+ android:title="@string/contextmenu_open_private_tab"/>
+
+ <item android:id="@+id/home_copyurl"
+ android:title="@string/contextmenu_copyurl"/>
+
+ <item android:id="@+id/home_share"
+ android:title="@string/contextmenu_share"/>
+
+ <item android:id="@+id/top_sites_edit"
+ android:title="@string/contextmenu_top_sites_edit"/>
+
+ <item android:id="@+id/top_sites_pin"
+ android:title="@string/contextmenu_top_sites_pin"/>
+
+ <item android:id="@+id/top_sites_unpin"
+ android:title="@string/contextmenu_top_sites_unpin"/>
+
+ <item android:id="@+id/home_edit_bookmark"
+ android:title="@string/contextmenu_edit_bookmark"/>
+
+ <item android:id="@+id/home_remove"
+ android:title="@string/contextmenu_remove"/>
+
+ <item android:id="@+id/home_add_to_launcher"
+ android:title="@string/contextmenu_add_to_launcher"/>
+
+</menu>
diff --git a/mobile/android/base/resources/menu/home_remote_tabs_client_contextmenu.xml b/mobile/android/base/resources/menu/home_remote_tabs_client_contextmenu.xml
new file mode 100644
index 0000000000..cd6310bdbb
--- /dev/null
+++ b/mobile/android/base/resources/menu/home_remote_tabs_client_contextmenu.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:id="@+id/home_remote_tabs_hide_client"
+ android:title="@string/pref_panels_hide"/>
+
+</menu>
diff --git a/mobile/android/base/resources/menu/preferences_search_menu.xml b/mobile/android/base/resources/menu/preferences_search_menu.xml
new file mode 100644
index 0000000000..f3b2453d16
--- /dev/null
+++ b/mobile/android/base/resources/menu/preferences_search_menu.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- Stub to preserve IDs. -->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@+id/restore_defaults"
+ android:title="" />
+</menu>
diff --git a/mobile/android/base/resources/menu/tabs_menu.xml b/mobile/android/base/resources/menu/tabs_menu.xml
new file mode 100644
index 0000000000..bd974e25b0
--- /dev/null
+++ b/mobile/android/base/resources/menu/tabs_menu.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:id="@+id/new_tab"
+ android:title="@string/new_tab"/>
+
+ <item android:id="@+id/new_private_tab"
+ android:title="@string/new_private_tab"/>
+
+ <item android:id="@+id/close_all_tabs"
+ android:title="@string/close_all_tabs"/>
+
+ <item android:id="@+id/close_private_tabs"
+ android:title="@string/close_private_tabs"/>
+
+</menu>
diff --git a/mobile/android/base/resources/menu/titlebar_contextmenu.xml b/mobile/android/base/resources/menu/titlebar_contextmenu.xml
new file mode 100644
index 0000000000..72c41cdc5b
--- /dev/null
+++ b/mobile/android/base/resources/menu/titlebar_contextmenu.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:id="@+id/pasteandgo"
+ android:title="@string/contextmenu_pasteandgo"/>
+
+ <item android:id="@+id/paste"
+ android:title="@string/contextmenu_paste"/>
+
+ <item android:id="@+id/subscribe"
+ android:title="@string/contextmenu_subscribe"/>
+
+ <item android:id="@+id/add_search_engine"
+ android:title="@string/contextmenu_add_search_engine"/>
+
+ <item android:id="@+id/copyurl"
+ android:title="@string/contextmenu_copyurl"/>
+
+ <item android:id="@+id/add_to_launcher"
+ android:title="@string/contextmenu_add_to_launcher"/>
+
+</menu>
diff --git a/mobile/android/base/resources/raw/bookmarkdefaults_favicon_addons.png b/mobile/android/base/resources/raw/bookmarkdefaults_favicon_addons.png
new file mode 100644
index 0000000000..2fbc630e66
--- /dev/null
+++ b/mobile/android/base/resources/raw/bookmarkdefaults_favicon_addons.png
Binary files differ
diff --git a/mobile/android/base/resources/raw/bookmarkdefaults_favicon_restricted_support.png b/mobile/android/base/resources/raw/bookmarkdefaults_favicon_restricted_support.png
new file mode 100644
index 0000000000..ea7d6a3aeb
--- /dev/null
+++ b/mobile/android/base/resources/raw/bookmarkdefaults_favicon_restricted_support.png
Binary files differ
diff --git a/mobile/android/base/resources/raw/bookmarkdefaults_favicon_restricted_webmaker.png b/mobile/android/base/resources/raw/bookmarkdefaults_favicon_restricted_webmaker.png
new file mode 100644
index 0000000000..ad177aa868
--- /dev/null
+++ b/mobile/android/base/resources/raw/bookmarkdefaults_favicon_restricted_webmaker.png
Binary files differ
diff --git a/mobile/android/base/resources/raw/bookmarkdefaults_favicon_support.png b/mobile/android/base/resources/raw/bookmarkdefaults_favicon_support.png
new file mode 100644
index 0000000000..3255e4cfdf
--- /dev/null
+++ b/mobile/android/base/resources/raw/bookmarkdefaults_favicon_support.png
Binary files differ
diff --git a/mobile/android/base/resources/raw/fake_home_items.json b/mobile/android/base/resources/raw/fake_home_items.json
new file mode 100644
index 0000000000..59b4ab74e3
--- /dev/null
+++ b/mobile/android/base/resources/raw/fake_home_items.json
@@ -0,0 +1,15 @@
+[{
+ "id": 1,
+ "dataset_id": "fake-dataset",
+ "url": "http://example.com/first",
+ "title": "First Example",
+ "description": "This is an example",
+ "image_url": "http://lorempixel.com/64/64?id=1"
+}, {
+ "id": 2,
+ "dataset_id": "fake-dataset",
+ "url": "http://example.com/second",
+ "title": "Second Example",
+ "description": "This is a second example",
+ "image_url": "http://lorempixel.com/64/64?id=2"
+}]
diff --git a/mobile/android/base/resources/raw/topdomains.txt b/mobile/android/base/resources/raw/topdomains.txt
new file mode 100644
index 0000000000..de1c19f4b3
--- /dev/null
+++ b/mobile/android/base/resources/raw/topdomains.txt
@@ -0,0 +1,455 @@
+google.com
+facebook.com
+amazon.com
+youtube.com
+yahoo.com
+ebay.com
+wikipedia.org
+twitter.com
+reddit.com
+go.com
+craigslist.org
+live.com
+netflix.com
+pinterest.com
+bing.com
+linkedin.com
+imgur.com
+espn.go.com
+walmart.com
+tumblr.com
+target.com
+paypal.com
+cnn.com
+chase.com
+instagram.com
+bestbuy.com
+blogspot.com
+nytimes.com
+msn.com
+imdb.com
+apple.com
+bankofamerica.com
+diply.com
+huffingtonpost.com
+yelp.com
+wellsfargo.com
+etsy.com
+weather.com
+wordpress.com
+buzzfeed.com
+zillow.com
+kohls.com
+aol.com
+homedepot.com
+foxnews.com
+microsoft.com
+comcast.net
+wikia.com
+groupon.com
+macys.com
+washingtonpost.com
+outbrain.com
+microsoftonline.com
+xfinity.com
+usps.com
+hulu.com
+americanexpress.com
+slickdeals.net
+pandora.com
+office.com
+cnet.com
+indeed.com
+capitalone.com
+nfl.com
+ups.com
+ask.com
+verizonwireless.com
+newegg.com
+usatoday.com
+forbes.com
+dailymail.co.uk
+dropbox.com
+att.com
+costco.com
+gfycat.com
+lowes.com
+gap.com
+about.com
+tripadvisor.com
+fedex.com
+baidu.com
+vice.com
+nordstrom.com
+adobe.com
+bbc.com
+twitch.tv
+allrecipes.com
+retailmenot.com
+stackoverflow.com
+citi.com
+sears.com
+jcpenney.com
+webmd.com
+ijreview.com
+nih.gov
+answers.com
+foodnetwork.com
+discovercard.com
+cbssports.com
+overstock.com
+businessinsider.com
+office365.com
+theguardian.com
+staples.com
+bleacherreport.com
+toysrus.com
+verizon.com
+github.com
+wayfair.com
+salesforce.com
+zulily.com
+wsj.com
+flickr.com
+goodreads.com
+realtor.com
+nbcnews.com
+ebates.com
+ancestry.com
+wunderground.com
+instructure.com
+people.com
+stackexchange.com
+drudgereport.com
+fidelity.com
+southwest.com
+deviantart.com
+thesaurus.com
+intuit.com
+woot.com
+pch.com
+soundcloud.com
+force.com
+samsclub.com
+ign.com
+qvc.com
+npr.org
+patch.com
+dell.com
+accuweather.com
+vimeo.com
+expedia.com
+trulia.com
+ca.gov
+swagbucks.com
+spotify.com
+bedbathandbeyond.com
+nypost.com
+aliexpress.com
+blackboard.com
+ticketmaster.com
+ikea.com
+feedly.com
+usaa.com
+tmz.com
+quora.com
+lifehacker.com
+kayak.com
+reference.com
+zappos.com
+gizmodo.com
+slate.com
+faithtap.com
+adp.com
+abcnews.go.com
+sephora.com
+cbs.com
+latimes.com
+shutterfly.com
+t-mobile.com
+littlethings.com
+glassdoor.com
+bloomberg.com
+cbsnews.com
+wikihow.com
+walgreens.com
+usbank.com
+blogger.com
+weebly.com
+gamestop.com
+food.com
+time.com
+kickstarter.com
+okcupid.com
+aa.com
+weather.gov
+nametests.com
+fandango.com
+engadget.com
+steamcommunity.com
+thekitchn.com
+nba.com
+mashable.com
+hp.com
+gamefaqs.com
+delta.com
+breitbart.com
+coupons.com
+eonline.com
+surveymonkey.com
+kmart.com
+barnesandnoble.com
+meetup.com
+bhphotovideo.com
+fanduel.com
+quizlet.com
+nydailynews.com
+sbnation.com
+nbcsports.com
+likes.com
+bbc.co.uk
+ew.com
+nike.com
+rottentomatoes.com
+steampowered.com
+reuters.com
+qq.com
+today.com
+mapquest.com
+audible.com
+priceline.com
+whitepages.com
+united.com
+myfitnesspal.com
+icloud.com
+forever21.com
+theatlantic.com
+microsoftstore.com
+theverge.com
+gawker.com
+houzz.com
+mayoclinic.org
+rei.com
+sfgate.com
+lifebuzz.com
+discover.com
+pnc.com
+pof.com
+iflscience.com
+popsugar.com
+creditkarma.com
+telegraph.co.uk
+airbnb.com
+buzzlie.com
+cnbc.com
+deadspin.com
+sina.com.cn
+legacy.com
+thedailybeast.com
+samsung.com
+nextdoor.com
+evite.com
+shopify.com
+yellowpages.com
+pcmag.com
+redfin.com
+emgn.com
+weibo.com
+alibaba.com
+cabelas.com
+battle.net
+foxsports.com
+taobao.com
+eventbrite.com
+victoriassecret.com
+theblaze.com
+dealnews.com
+cbslocal.com
+cvs.com
+dailymotion.com
+ecollege.com
+gofundme.com
+fitbit.com
+instructables.com
+godaddy.com
+babycenter.com
+squarespace.com
+llbean.com
+dickssportinggoods.com
+6pm.com
+myway.com
+hsn.com
+wired.com
+officedepot.com
+ozztube.com
+usmagazine.com
+match.com
+cracked.com
+evernote.com
+box.com
+starbucks.com
+kbb.com
+mlb.com
+marriott.com
+si.com
+jezebel.com
+pbs.org
+consumerreports.org
+roblox.com
+urbandictionary.com
+kotaku.com
+xbox.com
+marketwatch.com
+refinery29.com
+wikimedia.org
+tvguide.com
+politico.com
+barclaycardus.com
+abc.go.com
+mint.com
+topix.com
+theblackfriday.com
+aarp.org
+hotnewhiphop.com
+yourdailydish.com
+sprint.com
+vox.com
+cafemom.com
+nbc.com
+dailykos.com
+azlyrics.com
+autotrader.com
+hilton.com
+irs.gov
+monster.com
+fatwallet.com
+mailchimp.com
+webex.com
+landsend.com
+wix.com
+usnews.com
+jcrew.com
+jet.com
+capitalone360.com
+sharepoint.com
+schwab.com
+ulta.com
+vistaprint.com
+rollingstone.com
+biblegateway.com
+gamespot.com
+io9.com
+opentable.com
+hm.com
+duckduckgo.com
+chron.com
+photobucket.com
+shareasale.com
+directv.com
+avg.com
+oracle.com
+hotels.com
+timewarnercable.com
+chicagotribune.com
+ehow.com
+primewire.ag
+abs-cbnnews.com
+salon.com
+greatergood.com
+epicurious.com
+fool.com
+patheos.com
+custhelp.com
+purdue.edu
+tickld.com
+frys.com
+indiatimes.com
+amazon.co.uk
+zendesk.com
+tigerdirect.com
+stubhub.com
+healthcare.gov
+archive.org
+qualtrics.com
+ravelry.com
+cars.com
+redbox.com
+jalopnik.com
+speedtest.net
+harvard.edu
+slideshare.net
+kinja.com
+nesn.com
+michaels.com
+mit.edu
+bodybuilding.com
+edmunds.com
+nhl.com
+zergnet.com
+terraclicks.com
+techcrunch.com
+regnok.com
+pogo.com
+backpage.com
+mozilla.org
+naver.com
+giphy.com
+bankrate.com
+msnbc.com
+digitaltrends.com
+fanfiction.net
+skype.com
+disney.go.com
+norton.com
+androidcentral.com
+tomshardware.com
+thefreedictionary.com
+liveleak.com
+247sports.com
+merriam-webster.com
+wnd.com
+earthlink.net
+conservativetribune.com
+independent.co.uk
+drugs.com
+rotoworld.com
+nationalgeographic.com
+ae.com
+noaa.gov
+arstechnica.com
+thinkgeek.com
+stanford.edu
+bizjournals.com
+hootsuite.com
+genius.com
+goodhousekeeping.com
+vanguard.com
+ny.gov
+citibankonline.com
+booking.com
+mic.com
+orbitz.com
+dominos.com
+medium.com
+wow.com
+urbanoutfitters.com
+douban.com
+timeanddate.com
+draftkings.com
+livestrong.com
+livingsocial.com
+cox.net
+theonion.com
+marthastewart.com
+comenity.net
+worldlifestyle.com
+disney.com
+realsimple.com
+vrbo.com
+playstation.com
+potterybarn.com
+zazzle.com
+ksl.com
+tdbank.com
+sourceforge.net
+careerbuilder.com
diff --git a/mobile/android/base/resources/values-land/dimens.xml b/mobile/android/base/resources/values-land/dimens.xml
new file mode 100644
index 0000000000..561b81b76f
--- /dev/null
+++ b/mobile/android/base/resources/values-land/dimens.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <!-- Remote Tabs static view top padding. Less in landscape on phones. -->
+ <dimen name="home_remote_tabs_top_padding">16dp</dimen>
+ <dimen name="tab_panel_grid_padding">48dp</dimen>
+</resources>
diff --git a/mobile/android/base/resources/values-land/integers.xml b/mobile/android/base/resources/values-land/integers.xml
new file mode 100644
index 0000000000..c70badd26e
--- /dev/null
+++ b/mobile/android/base/resources/values-land/integers.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <integer name="number_of_top_sites_cols">3</integer>
+ <integer name="panel_icon_grid_view_columns">6</integer>
+
+</resources>
diff --git a/mobile/android/base/resources/values-land/styles.xml b/mobile/android/base/resources/values-land/styles.xml
new file mode 100644
index 0000000000..b9362223a9
--- /dev/null
+++ b/mobile/android/base/resources/values-land/styles.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <style name="TabsLayout" parent="TabsLayoutBase">
+ <item name="android:orientation">horizontal</item>
+ <item name="android:scrollbars">horizontal</item>
+ </style>
+
+ <style name="TabsItem">
+ <item name="android:nextFocusDown">@+id/close</item>
+ </style>
+
+ <style name="TabsItemClose">
+ <item name="android:nextFocusUp">@+id/info</item>
+ </style>
+
+ <!-- Tabs panel -->
+ <style name="TabsPanelSection" parent="TabsPanelSectionBase">
+ <item name="android:layout_weight">1</item>
+ </style>
+
+ <style name="TabsPanelItem">
+ <item name="android:layout_marginBottom">20dp</item>
+ <item name="android:layout_gravity">left</item>
+ <item name="android:gravity">left</item>
+ </style>
+
+</resources>
diff --git a/mobile/android/base/resources/values-large-land-v11/dimens.xml b/mobile/android/base/resources/values-large-land-v11/dimens.xml
new file mode 100644
index 0000000000..4d14ba8356
--- /dev/null
+++ b/mobile/android/base/resources/values-large-land-v11/dimens.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <!-- Remote Tabs static view top padding. Less in landscape on phones. -->
+ <dimen name="home_remote_tabs_top_padding">48dp</dimen>
+ <dimen name="home_header_item_height">64dp</dimen>
+
+</resources>
diff --git a/mobile/android/base/resources/values-large-land-v11/styles.xml b/mobile/android/base/resources/values-large-land-v11/styles.xml
new file mode 100644
index 0000000000..14fbdf3429
--- /dev/null
+++ b/mobile/android/base/resources/values-large-land-v11/styles.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <style name="TabsLayout" parent="TabsLayoutBase">
+ <item name="android:scrollbars">vertical</item>
+ </style>
+
+ <style name="Widget.BookmarksListView" parent="Widget.HomeListView">
+ <item name="android:scrollbarStyle">outsideOverlay</item>
+ </style>
+
+ <style name="Widget.TopSitesGridView" parent="Widget.GridView">
+ <item name="android:paddingLeft">55dp</item>
+ <item name="android:paddingRight">55dp</item>
+ <item name="android:paddingBottom">30dp</item>
+ <item name="android:horizontalSpacing">20dp</item>
+ <item name="android:verticalSpacing">20dp</item>
+ </style>
+
+ <!-- Tabs panel -->
+ <style name="TabsPanelSection" parent="TabsPanelSectionBase">
+ <item name="android:layout_marginLeft">20dp</item>
+ <item name="android:layout_marginRight">20dp</item>
+ </style>
+
+ <style name="TabsPanelItem" parent="TabsPanelItemBase">
+ <!-- To override the values-land style. -->
+ </style>
+
+</resources>
diff --git a/mobile/android/base/resources/values-large-v16/dimens.xml b/mobile/android/base/resources/values-large-v16/dimens.xml
new file mode 100644
index 0000000000..80ce7118d7
--- /dev/null
+++ b/mobile/android/base/resources/values-large-v16/dimens.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+ <dimen name="doorhanger_offsetY">124dp</dimen>
+</resources>
diff --git a/mobile/android/base/resources/values-large-v16/styles.xml b/mobile/android/base/resources/values-large-v16/styles.xml
new file mode 100644
index 0000000000..1c57a046e7
--- /dev/null
+++ b/mobile/android/base/resources/values-large-v16/styles.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <style name="TextAppearance.UrlBar.Title" parent="TextAppearance.Small">
+ <item name="android:textSize">16sp</item>
+ <item name="android:fontFamily">sans-serif-light</item>
+ </style>
+
+</resources>
diff --git a/mobile/android/base/resources/values-large/bool.xml b/mobile/android/base/resources/values-large/bool.xml
new file mode 100644
index 0000000000..73fde40be7
--- /dev/null
+++ b/mobile/android/base/resources/values-large/bool.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ This Source Code Form is subject to the terms of the Mozilla Public
+ ~ License, v. 2.0. If a copy of the MPL was not distributed with this
+ ~ file, you can obtain one at http://mozilla.org/MPL/2.0/.
+ -->
+
+<resources>
+ <!-- See definition in values/ for explanation. -->
+ <bool name="is_large_resource">true</bool>
+</resources>
diff --git a/mobile/android/base/resources/values-large/dimens.xml b/mobile/android/base/resources/values-large/dimens.xml
new file mode 100644
index 0000000000..a94ef6fb18
--- /dev/null
+++ b/mobile/android/base/resources/values-large/dimens.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <dimen name="doorhanger_offsetY">100dp</dimen>
+
+ <dimen name="browser_toolbar_height">56dp</dimen>
+ <!-- This value is the height of the Tabs Panel header view
+ (browser_toolbar_height) minus the height of the indicator
+ (6dp). This value should change when the height of the view changes. -->
+ <dimen name="tabs_panel_indicator_selected_padding_top">50dp</dimen>
+
+ <dimen name="browser_toolbar_height_flipper">60dp</dimen>
+ <dimen name="browser_toolbar_button_padding">16dp</dimen>
+ <dimen name="browser_toolbar_favicon_size">16dp</dimen>
+
+ <dimen name="browser_toolbar_site_security_height">60dp</dimen>
+ <dimen name="browser_toolbar_site_security_width">34dp</dimen>
+ <dimen name="browser_toolbar_site_security_margin_right">1dp</dimen>
+ <!-- We primarily use padding (instead of margins) to increase the hit area. -->
+ <dimen name="browser_toolbar_site_security_padding_vertical">21dp</dimen>
+ <dimen name="browser_toolbar_site_security_padding_horizontal">8dp</dimen>
+
+ <dimen name="firstrun_background_height">300dp</dimen>
+
+ <dimen name="tabs_panel_indicator_width">72dp</dimen>
+ <dimen name="tabs_panel_button_width">60dp</dimen>
+ <dimen name="panel_grid_view_column_width">200dp</dimen>
+
+ <dimen name="overlay_prompt_container_width">360dp</dimen>
+
+ <item name="tab_strip_content_start" type="dimen">72dp</item>
+
+</resources>
diff --git a/mobile/android/base/resources/values-large/integers.xml b/mobile/android/base/resources/values-large/integers.xml
new file mode 100644
index 0000000000..2688e5de50
--- /dev/null
+++ b/mobile/android/base/resources/values-large/integers.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <integer name="number_of_inline_share_devices">3</integer>
+ <integer name="max_search_suggestions">4</integer>
+ <integer name="max_saved_suggestions">4</integer>
+
+</resources>
diff --git a/mobile/android/base/resources/values-large/styles.xml b/mobile/android/base/resources/values-large/styles.xml
new file mode 100644
index 0000000000..79867a8021
--- /dev/null
+++ b/mobile/android/base/resources/values-large/styles.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <style name="UrlBar.ImageButton" parent="UrlBar.ImageButtonBase">
+ <item name="android:layout_width">@dimen/tablet_browser_toolbar_menu_item_width</item>
+ </style>
+
+ <!-- If this style wasn't actually shared outside the
+ url bar, this name could be improved (bug 1197424). -->
+ <style name="UrlBar.ImageButton.BrowserToolbarColors">
+ <item name="drawableTintList">@color/action_bar_menu_item_colors</item>
+ </style>
+
+ <style name="UrlBar.Button.Container">
+ <item name="android:layout_marginTop">6dp</item>
+ <item name="android:layout_marginBottom">6dp</item>
+ <!-- Start with forward hidden -->
+ <item name="android:orientation">horizontal</item>
+ </style>
+
+ <style name="TabsLayout" parent="TabsLayoutBase">
+ <item name="android:scrollbars">vertical</item>
+ </style>
+
+ <style name="TabsItem">
+ <item name="android:nextFocusDown">@+id/close</item>
+ </style>
+
+ <style name="TabsItemClose">
+ <item name="android:nextFocusUp">@+id/info</item>
+ </style>
+
+ <style name="Toast" parent="ToastBase">
+ <item name="android:layout_width">400dp</item>
+
+ <!-- Same as pre-19 Toast style, but with no left and right margins.
+ They're removed since large tablets are never going to be only 400dp wide. -->
+ </style>
+
+ <style name="Widget.MenuItemActionBar">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:background">@drawable/browser_toolbar_action_bar_button</item>
+ <item name="drawableTintList">@color/action_bar_menu_item_colors</item>
+ <item name="android:scaleType">center</item>
+
+ <!-- layout_width/height doesn't work here, likely because it's only
+ added programmatically, so we use padding for the width instead.
+ layout_height is set to MATCH_PARENT programmatically in
+ org.mozilla.gecko.toolbar.BrowserToolbarTabletBase.addActionItem(View) -->
+
+ <item name="android:paddingLeft">@dimen/tablet_browser_toolbar_menu_item_padding_horizontal</item>
+ <item name="android:paddingRight">@dimen/tablet_browser_toolbar_menu_item_padding_horizontal</item>
+ </style>
+
+ <style name="Widget.BookmarksListView" parent="Widget.HomeListView">
+ <item name="android:scrollbarStyle">outsideOverlay</item>
+ </style>
+
+ <style name="Widget.TopSitesGridView" parent="Widget.GridView">
+ <item name="android:paddingLeft">5dp</item>
+ <item name="android:paddingRight">5dp</item>
+ <item name="android:paddingBottom">30dp</item>
+ <item name="android:horizontalSpacing">10dp</item>
+ <item name="android:verticalSpacing">10dp</item>
+ </style>
+
+ <style name="Widget.TopSitesListView" parent="Widget.BookmarksListView">
+ <item name="android:paddingTop">30dp</item>
+ <item name="android:paddingLeft">32dp</item>
+ <item name="android:paddingRight">32dp</item>
+ <item name="android:clipToPadding">false</item>
+ <item name="topDivider">false</item>
+ </style>
+
+ <style name="Widget.HomeBanner">
+ <item name="android:paddingLeft">32dp</item>
+ <item name="android:paddingRight">32dp</item>
+ </style>
+
+ <style name="TextAppearance.UrlBar.Title" parent="TextAppearance.Medium">
+ <item name="android:textSize">16sp</item>
+ </style>
+
+</resources>
diff --git a/mobile/android/base/resources/values-sw240dp/dimens.xml b/mobile/android/base/resources/values-sw240dp/dimens.xml
new file mode 100644
index 0000000000..ee2bf0e8d0
--- /dev/null
+++ b/mobile/android/base/resources/values-sw240dp/dimens.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+ <dimen name="tab_panel_column_width">143dip</dimen>
+ <dimen name="tab_thumbnail_height">100dip</dimen>
+ <dimen name="tab_thumbnail_width">135dip</dimen>
+</resources>
diff --git a/mobile/android/base/resources/values-sw360dp/dimens.xml b/mobile/android/base/resources/values-sw360dp/dimens.xml
new file mode 100644
index 0000000000..da14a185ca
--- /dev/null
+++ b/mobile/android/base/resources/values-sw360dp/dimens.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+ <dimen name="tab_panel_column_width">156dip</dimen>
+ <dimen name="tab_thumbnail_height">110dip</dimen>
+ <dimen name="tab_thumbnail_width">148dip</dimen>
+
+ <dimen name="firstrun_background_height">180dp</dimen>
+ <dimen name="firstrun_min_height">180dp</dimen>
+</resources>
diff --git a/mobile/android/base/resources/values-sw400dp/dimens.xml b/mobile/android/base/resources/values-sw400dp/dimens.xml
new file mode 100644
index 0000000000..94567a2201
--- /dev/null
+++ b/mobile/android/base/resources/values-sw400dp/dimens.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+ <dimen name="tab_panel_column_width">176dip</dimen>
+ <dimen name="tab_thumbnail_height">120dip</dimen>
+ <dimen name="tab_thumbnail_width">168dip</dimen>
+</resources>
diff --git a/mobile/android/base/resources/values-v11/dimens.xml b/mobile/android/base/resources/values-v11/dimens.xml
new file mode 100644
index 0000000000..fbcb27bd1a
--- /dev/null
+++ b/mobile/android/base/resources/values-v11/dimens.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <!-- This is chosen to be close to Android's listPreferredItemHeightSmall.
+ TODO: We should inherit these from the system.
+ http://androidxref.com/4.2.2_r1/xref/frameworks/base/core/res/res/values/themes.xml#1287 -->
+ <dimen name="menu_item_row_height">48dp</dimen>
+
+</resources>
diff --git a/mobile/android/base/resources/values-v11/styles.xml b/mobile/android/base/resources/values-v11/styles.xml
new file mode 100644
index 0000000000..1550c18b88
--- /dev/null
+++ b/mobile/android/base/resources/values-v11/styles.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!--
+ Only overriden styles for Honeycomb/Ice cream sandwich are specified here.
+ Please refer to values/styles.xml for default styles.
+ -->
+
+ <!--
+ Base application styles. This could be overridden in other res/values-XXX/themes.xml.
+ -->
+ <style name="Widget.BaseButton" parent="android:style/Widget.Holo.Light.Button"/>
+
+ <style name="Widget.BaseDropDownItem" parent="android:style/Widget.Holo.Light.DropDownItem"/>
+
+ <style name="Widget.BaseEditText" parent="android:style/Widget.Holo.Light.EditText"/>
+
+ <style name="Widget.BaseListView" parent="android:style/Widget.Holo.ListView"/>
+
+ <style name="Widget.BaseGridView" parent="android:style/Widget.Holo.GridView"/>
+
+ <style name="Widget.BaseTextView" parent="android:style/Widget.Holo.Light.TextView"/>
+
+ <style name="Widget.ProgressBar.Horizontal" parent="android:style/Widget.Holo.ProgressBar.Horizontal"/>
+
+
+ <!--
+ Application styles. All customizations that are not specific
+ to a particular API level can go here.
+ -->
+ <style name="Widget.ListItem">
+ <item name="android:textColor">@color/select_item_multichoice</item>
+ <item name="android:minHeight">?android:attr/listPreferredItemHeight</item>
+ <item name="android:textAppearance">?android:attr/textAppearanceLarge</item>
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:paddingLeft">12dip</item>
+ <item name="android:paddingRight">7dip</item>
+ <item name="android:checkMark">?android:attr/listChoiceIndicatorMultiple</item>
+ <item name="android:ellipsize">marquee</item>
+ </style>
+
+ <!-- ActionBar -->
+ <style name="ActionBar" parent="android:style/Widget.Holo.ActionBar" />
+
+ <!-- TabsLayout ActionBar -->
+ <style name="ActionBar.TabsLayout">
+ <item name="android:visibility">gone</item>
+ </style>
+
+ <!-- DropDown List View -->
+ <style name="DropDownListView" parent="@android:style/Widget.Holo.ListView.DropDown">
+ <item name="android:listSelector">@drawable/action_bar_button</item>
+ <item name="android:divider">@color/toolbar_divider_grey</item>
+ <item name="android:dividerHeight">@dimen/page_row_divider_height</item>
+ </style>
+
+ <!-- Spinner DropDown Item -->
+ <style name="Widget.DropDownItem.Spinner" parent="@android:style/Widget.Holo.Light.DropDownItem.Spinner">
+ <item name="android:textColor">#FF000000</item>
+ </style>
+
+ <style name="Widget.Spinner" parent="android:style/Widget.Holo.Light.Spinner">
+ <item name="android:minWidth">@dimen/doorhanger_input_width</item>
+ </style>
+
+ <style name="Widget.TextView.SpinnerItem" parent="android:style/Widget.Holo.Light.TextView.SpinnerItem">
+ <item name="android:textColor">#FF000000</item>
+ </style>
+
+ <style name="TextAppearance.Widget.ActionBar.Title" parent="@android:style/TextAppearance.Medium"/>
+
+ <style name="GeckoActionBar.Title" parent="TextAppearance.Widget.ActionBar.Title">
+ <item name="android:drawableLeft">@drawable/ab_done</item>
+ <item name="android:background">@android:color/transparent</item>
+ <item name="android:paddingLeft">15dp</item>
+ <item name="android:paddingRight">15dp</item>
+ <!-- gravity and minWidth are added here to more resemble our values/styles.xml
+ counterpart. This is solely to correct bug 1233709 -->
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:minWidth">0dp</item>
+ </style>
+
+ <style name="GeckoActionBar.Button" parent="android:style/Widget.Holo.Light.ActionButton">
+ <item name="android:padding">8dip</item>
+ <!-- The default implementation doesn't do any image scaling. Our custom menus mean we can't just use the same image
+ in both menus and the actionbar without doing some scaling though. -->
+ <item name="android:scaleType">centerInside</item>
+ </style>
+
+ <style name="GeckoActionBar.Button.MenuButton" parent="android:style/Widget.Holo.Light.ActionButton.Overflow">
+ <item name="android:scaleType">centerInside</item>
+ <item name="android:background">@android:color/transparent</item>
+ <item name="android:src">@drawable/menu</item>
+ <item name="android:tint">@color/toolbar_icon_grey</item>
+ <item name="android:layout_marginTop">16dp</item>
+ <item name="android:layout_marginBottom">16dp</item>
+ </style>
+
+ <style name="TabInput"></style>
+
+ <style name="TabInput.TabWidget" parent="android:style/Widget.Holo.Light.TabWidget"/>
+
+ <style name="TabInput.Tab" parent="android:style/Widget.Holo.Light.Tab">
+ <item name="android:minHeight">@dimen/menu_item_row_height</item>
+ <item name="android:textAllCaps">true</item>
+ </style>
+
+</resources>
diff --git a/mobile/android/base/resources/values-v11/themes.xml b/mobile/android/base/resources/values-v11/themes.xml
new file mode 100644
index 0000000000..af8145226e
--- /dev/null
+++ b/mobile/android/base/resources/values-v11/themes.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!--
+ Base application theme. This could be overridden by GeckoBaseTheme
+ in other res/values-XXX/themes.xml.
+ -->
+ <style name="GeckoBase" parent="Theme.AppCompat.Light.DarkActionBar">
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="windowActionBar">false</item>
+ <item name="windowNoTitle">true</item>
+ </style>
+
+ <style name="GeckoDialogBase" parent="@android:style/Theme.Holo.Light.Dialog">
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:windowActionBar">false</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowBackground">@android:color/transparent</item>
+ </style>
+
+ <style name="GeckoTitleDialogBase" parent="@android:style/Theme.Holo.Light.Dialog" />
+
+ <!--
+ Activity based themes for API 11+. This theme completely replaces
+ GeckoAppBase from res/values/themes.xml on API 11+ devices.
+ -->
+ <style name="GeckoAppBase" parent="Gecko">
+ <item name="android:actionButtonStyle">@style/GeckoActionBar.Button</item>
+ <item name="android:actionModeCopyDrawable">@drawable/ab_copy</item>
+ <item name="android:actionModeCutDrawable">@drawable/ab_cut</item>
+ <item name="android:actionModePasteDrawable">@drawable/ab_paste</item>
+ <item name="android:listViewStyle">@style/Widget.ListView</item>
+ <item name="android:spinnerDropDownItemStyle">@style/Widget.DropDownItem.Spinner</item>
+ <item name="android:spinnerItemStyle">@style/Widget.TextView.SpinnerItem</item>
+ <item name="menuItemSwitcherLayoutStyle">@style/Widget.MenuItemSwitcherLayout</item>
+ <item name="menuItemDefaultStyle">@style/Widget.MenuItemDefault</item>
+ <item name="menuItemSecondaryActionBarStyle">@style/Widget.MenuItemSecondaryActionBar</item>
+ <item name="tabGridLayoutViewStyle">@style/Widget.TabsGridLayout</item>
+ </style>
+
+</resources>
diff --git a/mobile/android/base/resources/values-v13/search_styles.xml b/mobile/android/base/resources/values-v13/search_styles.xml
new file mode 100644
index 0000000000..f493891e80
--- /dev/null
+++ b/mobile/android/base/resources/values-v13/search_styles.xml
@@ -0,0 +1,20 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="@android:style/Theme.Holo.Light.NoActionBar">
+ <item name="android:windowBackground">@color/toolbar_grey</item>
+ <item name="android:colorBackground">@color/toolbar_grey</item>
+
+ <!--This attribute is required so that we can create a facet button-->
+ <!--pragmatically. The defStyle param used in the View constructor-->
+ <!--must be an attr, see: https://code.google.com/p/android/issues/detail?id=12683-->
+ <item name="facetButtonStyle">@style/FacetButtonStyle</item>
+ </style>
+
+ <style name="SettingsTheme" parent="@android:style/Theme.Holo.Light"/>
+
+</resources>
diff --git a/mobile/android/base/resources/values-v13/styles.xml b/mobile/android/base/resources/values-v13/styles.xml
new file mode 100644
index 0000000000..307f571807
--- /dev/null
+++ b/mobile/android/base/resources/values-v13/styles.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <style name="GeckoDialogTitle">
+ <!-- Override this to use a Holo theme on v13+ -->
+ <item name="android:textAppearance">@android:style/TextAppearance.Holo.DialogWindowTitle</item>
+ </style>
+
+ <style name="TextAppearance.Widget.ActionBar.Title" parent="@android:style/TextAppearance.Holo.Widget.ActionBar.Title"/>
+
+</resources>
diff --git a/mobile/android/base/resources/values-v14/themes.xml b/mobile/android/base/resources/values-v14/themes.xml
new file mode 100644
index 0000000000..a04cf54d89
--- /dev/null
+++ b/mobile/android/base/resources/values-v14/themes.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!--
+ Activity based themes for API 14+. This theme completely replaces
+ GeckoAppBase from res/values/themes.xml on API 14+ devices.
+ -->
+ <style name="GeckoAppBase" parent="Gecko">
+ <item name="android:actionButtonStyle">@style/GeckoActionBar.Button</item>
+ <item name="android:actionModeCopyDrawable">@drawable/ab_copy</item>
+ <item name="android:actionModeCutDrawable">@drawable/ab_cut</item>
+ <item name="android:actionModePasteDrawable">@drawable/ab_paste</item>
+ <item name="android:actionModeSelectAllDrawable">@drawable/ab_select_all</item>
+ <item name="android:actionModeStyle">@style/GeckoActionBar</item>
+ <item name="android:listViewStyle">@style/Widget.ListView</item>
+ <item name="android:spinnerDropDownItemStyle">@style/Widget.DropDownItem.Spinner</item>
+ <item name="android:spinnerItemStyle">@style/Widget.TextView.SpinnerItem</item>
+ <item name="menuItemSwitcherLayoutStyle">@style/Widget.MenuItemSwitcherLayout</item>
+ <item name="menuItemDefaultStyle">@style/Widget.MenuItemDefault</item>
+ <item name="menuItemSecondaryActionBarStyle">@style/Widget.MenuItemSecondaryActionBar</item>
+ </style>
+
+</resources>
diff --git a/mobile/android/base/resources/values-v16/search_styles.xml b/mobile/android/base/resources/values-v16/search_styles.xml
new file mode 100644
index 0000000000..3da95f06a3
--- /dev/null
+++ b/mobile/android/base/resources/values-v16/search_styles.xml
@@ -0,0 +1,19 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <style name="TextAppearance.EmptyView.Title" parent="@android:style/TextAppearance.Small">
+ <item name="android:textColor">@color/text_and_tabs_tray_grey</item>
+ <item name="android:textSize">20sp</item>
+ <item name="android:fontFamily">sans-serif-light</item>
+ </style>
+
+ <style name="TextAppearance.EmptyView.Message" parent="@android:style/TextAppearance.Small">
+ <item name="android:textColor">@color/placeholder_grey</item>
+ <item name="android:textSize">16sp</item>
+ <item name="android:lineSpacingExtra">4sp</item>
+ </style>
+
+</resources>
diff --git a/mobile/android/base/resources/values-v16/styles.xml b/mobile/android/base/resources/values-v16/styles.xml
new file mode 100644
index 0000000000..15243891e5
--- /dev/null
+++ b/mobile/android/base/resources/values-v16/styles.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <style name="TextAppearance.EmptyMessage" parent="TextAppearance.Large">
+ <item name="android:fontFamily">sans-serif-light</item>
+ </style>
+
+ <style name="TextAppearance.Widget.Home.ItemTitle" parent="TextAppearance.Medium">
+ <item name="android:fontFamily">sans-serif-light</item>
+ </style>
+
+ <style name="TextAppearance.FirstrunTextLight">
+ <item name="android:fontFamily">sans-serif-light</item>
+ </style>
+
+ <style name="TextAppearance.FirstrunTextRegular">
+ <item name="android:fontFamily">sans-serif</item>
+ </style>
+
+ <style name="TextAppearance.UrlBar.Title" parent="TextAppearance.Small">
+ <item name="android:textSize">15sp</item>
+ <item name="android:fontFamily">sans-serif-light</item>
+ </style>
+
+</resources>
diff --git a/mobile/android/base/resources/values-v19/dimens.xml b/mobile/android/base/resources/values-v19/dimens.xml
new file mode 100644
index 0000000000..e2c964d825
--- /dev/null
+++ b/mobile/android/base/resources/values-v19/dimens.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+ <dimen name="toast_button_corner_radius">24dp</dimen>
+</resources>
diff --git a/mobile/android/base/resources/values-v19/styles.xml b/mobile/android/base/resources/values-v19/styles.xml
new file mode 100644
index 0000000000..6114c82291
--- /dev/null
+++ b/mobile/android/base/resources/values-v19/styles.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <style name="Toast" parent="ToastBase">
+ <item name="android:layout_marginLeft">8dp</item>
+ <item name="android:layout_marginRight">8dp</item>
+ </style>
+
+ <style name="ToastElementBase">
+ <item name="android:background">@null</item>
+ <item name="android:paddingLeft">24dp</item>
+ <item name="android:paddingRight">24dp</item>
+ <item name="android:paddingTop">11dp</item>
+ <item name="android:paddingBottom">11dp</item>
+ </style>
+
+ <style name="ToastDivider" parent="ToastDividerBase">
+ <item name="android:layout_marginTop">12dp</item>
+ <item name="android:layout_marginBottom">12dp</item>
+ </style>
+
+ <style name="ToastMessage" parent="ToastMessageBase">
+ <item name="android:textSize">16sp</item>
+ <item name="android:fontFamily">sans-serif-condensed</item>
+ <item name="android:shadowColor">#BB000000</item>
+ <item name="android:shadowRadius">2.75</item>
+ </style>
+
+ <style name="ToastButton" parent="ToastButtonBase">
+ <item name="android:textSize">12sp</item>
+ <item name="android:textStyle">bold</item>
+ <item name="android:fontFamily">sans-serif-condensed</item>
+ <item name="android:shadowColor">#BB000000</item>
+ <item name="android:shadowRadius">2.75</item>
+ </style>
+
+</resources>
diff --git a/mobile/android/base/resources/values-v21/dimens.xml b/mobile/android/base/resources/values-v21/dimens.xml
new file mode 100644
index 0000000000..dbf33ea28e
--- /dev/null
+++ b/mobile/android/base/resources/values-v21/dimens.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+ <dimen name="context_menu_item_horizontal_padding">17dp</dimen>
+</resources>
diff --git a/mobile/android/base/resources/values-v21/integers.xml b/mobile/android/base/resources/values-v21/integers.xml
new file mode 100644
index 0000000000..76fd5f483a
--- /dev/null
+++ b/mobile/android/base/resources/values-v21/integers.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <integer name="search_assist_launch_res">@drawable/search_launcher</integer>
+
+</resources>
diff --git a/mobile/android/base/resources/values-v21/styles.xml b/mobile/android/base/resources/values-v21/styles.xml
new file mode 100644
index 0000000000..5449c23347
--- /dev/null
+++ b/mobile/android/base/resources/values-v21/styles.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <style name="ActionBarTitleTextStyle" parent="@android:style/TextAppearance.Material.Widget.ActionBar.Title">
+ <item name="android:textColor">#fff</item>
+ </style>
+
+</resources>
diff --git a/mobile/android/base/resources/values-v21/themes.xml b/mobile/android/base/resources/values-v21/themes.xml
new file mode 100644
index 0000000000..ddb08d052a
--- /dev/null
+++ b/mobile/android/base/resources/values-v21/themes.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <!--
+ Base application theme.
+ -->
+ <style name="GeckoBase" parent="Theme.AppCompat.Light.DarkActionBar">
+ <item name="colorPrimary">@color/text_and_tabs_tray_grey</item>
+ <item name="colorPrimaryDark">@color/text_and_tabs_tray_grey</item>
+ <item name="windowActionBar">false</item>
+ <item name="windowNoTitle">true</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:alertDialogTheme">@style/GeckoAlertDialog</item>
+ </style>
+
+ <style name="GeckoAlertDialog" parent="@android:style/Theme.Material.Light.Dialog.Alert">
+ <item name="android:colorAccent">@color/fennec_ui_orange</item>
+ </style>
+
+ <style name="ActionBar.FxAccountStatusActivity" parent="@android:style/Widget.Material.ActionBar.Solid">
+ <item name="android:displayOptions">homeAsUp|showTitle</item>
+ <item name="android:titleTextStyle">@style/ActionBarTitleTextStyle</item>
+ </style>
+
+ <style name="GeckoAppBase" parent="Gecko">
+ <item name="android:actionButtonStyle">@style/GeckoActionBar.Button</item>
+ <item name="android:listViewStyle">@style/Widget.ListView</item>
+ <item name="android:spinnerDropDownItemStyle">@style/Widget.DropDownItem.Spinner</item>
+ <item name="android:spinnerItemStyle">@style/Widget.TextView.SpinnerItem</item>
+ <item name="menuItemSwitcherLayoutStyle">@style/Widget.MenuItemSwitcherLayout</item>
+ <item name="menuItemDefaultStyle">@style/Widget.MenuItemDefault</item>
+ <item name="menuItemSecondaryActionBarStyle">@style/Widget.MenuItemSecondaryActionBar</item>
+ <item name="tabGridLayoutViewStyle">@style/Widget.TabsGridLayout</item>
+ </style>
+
+</resources>
diff --git a/mobile/android/base/resources/values-w400dp/styles.xml b/mobile/android/base/resources/values-w400dp/styles.xml
new file mode 100644
index 0000000000..5308af9caa
--- /dev/null
+++ b/mobile/android/base/resources/values-w400dp/styles.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <style name="Toast" parent="ToastBase">
+ <item name="android:layout_width">400dp</item>
+ </style>
+
+</resources>
diff --git a/mobile/android/base/resources/values-xlarge-land-v11/dimens.xml b/mobile/android/base/resources/values-xlarge-land-v11/dimens.xml
new file mode 100644
index 0000000000..ca605a31bc
--- /dev/null
+++ b/mobile/android/base/resources/values-xlarge-land-v11/dimens.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <dimen name="tab_panel_grid_padding">64dp</dimen>
+
+</resources>
diff --git a/mobile/android/base/resources/values-xlarge-land-v11/styles.xml b/mobile/android/base/resources/values-xlarge-land-v11/styles.xml
new file mode 100644
index 0000000000..1b8666f29c
--- /dev/null
+++ b/mobile/android/base/resources/values-xlarge-land-v11/styles.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+ <style name="Widget.TopSitesListView" parent="Widget.BookmarksListView">
+ <item name="android:paddingTop">30dp</item>
+ <item name="android:paddingLeft">120dp</item>
+ <item name="android:paddingRight">120dp</item>
+ <item name="android:clipToPadding">false</item>
+ <item name="topDivider">false</item>
+ </style>
+
+ <style name="Widget.TopSitesGridView" parent="Widget.GridView">
+ <item name="android:paddingLeft">55dp</item>
+ <item name="android:paddingRight">55dp</item>
+ <item name="android:paddingBottom">30dp</item>
+ <item name="android:horizontalSpacing">56dp</item>
+ <item name="android:verticalSpacing">20dp</item>
+ </style>
+
+</resources>
diff --git a/mobile/android/base/resources/values-xlarge-v11/dimens.xml b/mobile/android/base/resources/values-xlarge-v11/dimens.xml
new file mode 100644
index 0000000000..a666854f17
--- /dev/null
+++ b/mobile/android/base/resources/values-xlarge-v11/dimens.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <dimen name="panel_grid_view_column_width">250dp</dimen>
+ <dimen name="tab_panel_grid_padding">48dp</dimen>
+
+</resources>
diff --git a/mobile/android/base/resources/values-xlarge-v11/integers.xml b/mobile/android/base/resources/values-xlarge-v11/integers.xml
new file mode 100644
index 0000000000..55e1babea8
--- /dev/null
+++ b/mobile/android/base/resources/values-xlarge-v11/integers.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <integer name="number_of_top_sites">9</integer>
+ <integer name="number_of_top_sites_cols">3</integer>
+ <integer name="panel_icon_grid_view_columns">6</integer>
+ <integer name="number_of_inline_share_devices">4</integer>
+
+</resources>
diff --git a/mobile/android/base/resources/values-xlarge-v11/styles.xml b/mobile/android/base/resources/values-xlarge-v11/styles.xml
new file mode 100644
index 0000000000..d0bcbae31f
--- /dev/null
+++ b/mobile/android/base/resources/values-xlarge-v11/styles.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <!--
+ Only overriden styles for Honeycomb/Ice cream sandwich XLARGE tablets are specified here.
+ Please refer to values/styles.xml for default styles.
+ -->
+
+ <!-- TabWidget -->
+ <style name="TabWidget">
+ <item name="android:layout_width">300dip</item>
+ <item name="android:layout_height">48dip</item>
+ </style>
+
+ <style name="Widget.TopSitesListView" parent="Widget.BookmarksListView">
+ <item name="android:paddingTop">30dp</item>
+ <item name="android:paddingLeft">32dp</item>
+ <item name="android:paddingRight">32dp</item>
+ <item name="android:clipToPadding">false</item>
+ <item name="topDivider">false</item>
+ </style>
+
+</resources>
diff --git a/mobile/android/base/resources/values/arrays.xml b/mobile/android/base/resources/values/arrays.xml
new file mode 100644
index 0000000000..d220ca9bb4
--- /dev/null
+++ b/mobile/android/base/resources/values/arrays.xml
@@ -0,0 +1,176 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<resources>
+ <string-array name="pref_home_updates_entries">
+ <item>@string/pref_home_updates_enabled</item>
+ <item>@string/pref_home_updates_wifi</item>
+ </string-array>
+ <string-array name="pref_home_updates_values">
+ <item>0</item>
+ <item>1</item>
+ </string-array>
+ <string-array name="pref_plugins_entries">
+ <item>@string/pref_plugins_enabled</item>
+ <item>@string/pref_plugins_tap_to_play</item>
+ <item>@string/pref_plugins_disabled</item>
+ </string-array>
+ <string-array name="pref_plugins_values">
+ <item>1</item>
+ <item>2</item>
+ <item>0</item>
+ </string-array>
+ <string-array name="pref_font_size_entries">
+ <item>@string/pref_font_size_tiny</item>
+ <item>@string/pref_font_size_small</item>
+ <item>@string/pref_font_size_medium</item>
+ <item>@string/pref_font_size_large</item>
+ <item>@string/pref_font_size_xlarge</item>
+ </string-array>
+ <string-array name="pref_font_size_values">
+ <item>0</item>
+ <item>80</item>
+ <item>120</item>
+ <item>160</item>
+ <item>240</item>
+ </string-array>
+ <string-array name="pref_char_encoding_entries">
+ <item>@string/pref_char_encoding_on</item>
+ <item>@string/pref_char_encoding_off</item>
+ </string-array>
+ <string-array name="pref_char_encoding_values">
+ <item>true</item>
+ <item>false</item>
+ </string-array>
+ <string-array name="pref_cookies_entries">
+ <item>@string/pref_cookies_accept_all</item>
+ <item>@string/pref_cookies_not_accept_foreign</item>
+ <item>@string/pref_cookies_disabled</item>
+ </string-array>
+ <string-array name="pref_tracking_protection_values">
+ <item>2</item>
+ <item>1</item>
+ <item>0</item>
+ </string-array>
+ <string-array name="pref_tracking_protection_entries">
+ <item>@string/pref_tracking_protection_enabled</item>
+ <item>@string/pref_tracking_protection_enabled_pb</item>
+ <item>@string/pref_tracking_protection_disabled</item>
+ </string-array>
+ <string-array name="pref_cookies_values">
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ </string-array>
+ <string-array name="pref_import_android_entries">
+ <item>@string/bookmarks_title</item>
+ <item>@string/history_title</item>
+ </string-array>
+ <string-array name="pref_import_android_defaults">
+ <item>true</item>
+ <item>true</item>
+ </string-array>
+ <string-array name="pref_import_android_values">
+ <item>android_import.data.bookmarks</item>
+ <item>android_import.data.history</item>
+ </string-array>
+ <string-array name="pref_private_data_entries">
+ <item>@string/pref_private_data_history2</item>
+ <item>@string/pref_private_data_searchHistory</item>
+ <item>@string/pref_private_data_downloadFiles2</item>
+ <item>@string/pref_private_data_formdata2</item>
+ <item>@string/pref_private_data_cookies2</item>
+ <item>@string/pref_private_data_cache</item>
+ <item>@string/pref_private_data_offlineApps</item>
+ <item>@string/pref_private_data_siteSettings</item>
+ <item>@string/pref_private_data_syncedTabs</item>
+ <item>@string/pref_private_data_passwords</item>
+ </string-array>
+ <string-array name="pref_private_data_defaults">
+ <item>true</item>
+ <item>true</item>
+ <item>true</item>
+ <item>true</item>
+ <item>true</item>
+ <item>true</item>
+ <item>true</item>
+ <item>true</item>
+ <item>true</item>
+ <item>false</item>
+ </string-array>
+ <string-array name="pref_private_data_values">
+ <item>private.data.history</item>
+ <item>private.data.searchHistory</item>
+ <item>private.data.downloadFiles</item>
+ <item>private.data.formdata</item>
+ <item>private.data.cookies_sessions</item>
+ <item>private.data.cache</item>
+ <item>private.data.offlineApps</item>
+ <item>private.data.siteSettings</item>
+ <item>private.data.syncedTabs</item>
+ <item>private.data.passwords</item>
+ </string-array>
+ <string-array name="pref_private_data_keys">
+ <item>private.data.history</item>
+ <item>private.data.searchHistory</item>
+ <item>private.data.downloadFiles</item>
+ <item>private.data.formdata</item>
+ <item>private.data.cookies_sessions</item>
+ <item>private.data.cache</item>
+ <item>private.data.offlineApps</item>
+ <item>private.data.siteSettings</item>
+ <item>private.data.syncedTabs</item>
+ <item>private.data.passwords</item>
+ </string-array>
+ <string-array name="pref_clear_on_exit_defaults">
+ <item>false</item>
+ <item>false</item>
+ <item>false</item>
+ <item>false</item>
+ <item>false</item>
+ <item>false</item>
+ <item>false</item>
+ <item>false</item>
+ <item>false</item>
+ <item>false</item>
+ </string-array>
+ <string-array name="pref_restore_entries">
+ <item>@string/pref_restore_always</item>
+ <item>@string/pref_restore_quit</item>
+ </string-array>
+ <string-array name="pref_restore_values">
+ <item>always</item>
+ <item>quit</item>
+ </string-array>
+ <string-array name="pref_update_autodownload_entries">
+ <item>@string/pref_update_autodownload_enabled</item>
+ <item>@string/pref_update_autodownload_wifi</item>
+ <item>@string/pref_update_autodownload_disabled</item>
+ </string-array>
+ <string-array name="pref_update_autodownload_values">
+ <item>enabled</item>
+ <item>wifi</item>
+ <item>disabled</item>
+ </string-array>
+ <!-- This value is similar to config_longPressVibePattern in android frameworks/base/core/res/res/values/config.xml-->
+ <integer-array name="long_press_vibrate_msec">
+ <item>0</item>
+ <item>1</item>
+ <item>20</item>
+ <item>21</item>
+ </integer-array>
+ <!-- browser.image_blocking -->
+ <string-array name="pref_browser_image_blocking_entries">
+ <item>@string/pref_tap_to_load_images_enabled</item>
+ <item>@string/pref_tap_to_load_images_data</item>
+ <item>@string/pref_tap_to_load_images_disabled2</item>
+ </string-array>
+ <string-array name="pref_browser_image_blocking_values">
+ <item>1</item> <!-- Always -->
+ <item>2</item> <!-- Wifi-only -->
+ <item>0</item> <!-- Blocked -->
+ </string-array>
+</resources>
diff --git a/mobile/android/base/resources/values/attrs.xml b/mobile/android/base/resources/values/attrs.xml
new file mode 100644
index 0000000000..0a75f98847
--- /dev/null
+++ b/mobile/android/base/resources/values/attrs.xml
@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- Theme level attributes -->
+ <declare-styleable name="GeckoTheme">
+
+ <!-- Style for GeckoMenu ListView -->
+ <attr name="geckoMenuListViewStyle" format="reference"/>
+
+ <!-- Style for MenuItemActionBar -->
+ <attr name="menuItemActionBarStyle" format="reference"/>
+
+ <!-- Style for MenuItemActionBar -->
+ <attr name="menuItemActionModeStyle" format="reference"/>
+
+ <!-- Style for MenuItemSwitcherLayout -->
+ <attr name="menuItemSwitcherLayoutStyle" format="reference"/>
+
+ <!-- Style for MenuItemDefault -->
+ <attr name="menuItemDefaultStyle" format="reference"/>
+
+ <!-- Style for MenuItemActionBar when shown in SecondaryActionBar -->
+ <attr name="menuItemSecondaryActionBarStyle" format="reference"/>
+
+ <!-- Default style for the BookmarksListView -->
+ <attr name="bookmarksListViewStyle" format="reference" />
+
+ <!-- Default style for the TopSitesGridItemView -->
+ <attr name="topSitesGridItemViewStyle" format="reference" />
+
+ <!-- Styles for dynamic panel grid views -->
+ <attr name="panelIconViewStyle" format="reference" />
+
+ <!-- Style for the TabsGridLayout -->
+ <attr name="tabGridLayoutViewStyle" format="reference" />
+
+ <!-- Default style for the TopSitesGridView -->
+ <attr name="topSitesGridViewStyle" format="reference" />
+
+ <!-- Default style for the TopSitesThumbnailView -->
+ <attr name="topSitesThumbnailViewStyle" format="reference" />
+
+ <!-- Default style for the HomeListView -->
+ <attr name="homeListViewStyle" format="reference" />
+
+ </declare-styleable>
+
+ <declare-styleable name="MenuItem">
+ <attr name="android:id"/>
+ <attr name="android:orderInCategory"/>
+ <attr name="android:title"/>
+ <attr name="android:icon"/>
+ <attr name="android:checkable"/>
+ <attr name="android:checked"/>
+ <attr name="android:visible"/>
+ <attr name="android:enabled"/>
+ <attr name="android:showAsAction"/>
+ </declare-styleable>
+
+ <declare-styleable name="MenuItemDefault">
+ <attr name="state_more" format="boolean"/>
+ </declare-styleable>
+
+ <declare-styleable name="TabThumbnailWrapper">
+ <attr name="state_recording" format="boolean"/>
+ </declare-styleable>
+
+ <declare-styleable name="FlowLayout">
+ <attr name="spacing" format="dimension"/>
+ </declare-styleable>
+
+ <declare-styleable name="MultiChoicePreference">
+ <attr name="entries" format="string"/>
+ <attr name="entryValues" format="string"/>
+ <attr name="initialValues" format="string"/>
+ </declare-styleable>
+
+ <declare-styleable name="MultiPrefMultiChoicePreference">
+ <attr name="entryKeys" format="string"/>
+ </declare-styleable>
+
+ <declare-styleable name="TabsLayout">
+ <attr name="tabs">
+ <flag name="tabs_normal" value="0x00" />
+ <flag name="tabs_private" value ="0x01" />
+ </attr>
+ </declare-styleable>
+
+ <declare-styleable name="TabCounter">
+ <attr name="android:layout"/>
+ </declare-styleable>
+
+ <declare-styleable name="PrivateBrowsing">
+ <attr name="state_private" format="boolean"/>
+ </declare-styleable>
+
+ <declare-styleable name="LightweightTheme">
+ <attr name="state_light" format="boolean"/>
+ <attr name="state_dark" format="boolean"/>
+ <attr name="autoUpdateTheme" format="boolean"/>
+ </declare-styleable>
+
+ <declare-styleable name="TwoWayView">
+ <attr name="android:orientation"/>
+ <attr name="android:choiceMode"/>
+ <attr name="android:listSelector"/>
+ <attr name="android:drawSelectorOnTop"/>
+ </declare-styleable>
+
+ <declare-styleable name="HomeListView">
+ <!-- Draws a divider on top of the list, if true. Defaults to false. -->
+ <attr name="topDivider" format="boolean"/>
+ </declare-styleable>
+
+ <declare-styleable name="FadedTextView">
+ <attr name="fadeWidth" format="dimension"/>
+ </declare-styleable>
+
+ <declare-styleable name="FadedMultiColorTextView">
+ <!-- The background color we should be fading over. Useful because the
+ background is full alpha and we need to copy the background underneath. -->
+ <attr name="fadeBackgroundColor" format="dimension"/>
+ </declare-styleable>
+
+ <declare-styleable name="IconTabWidget">
+ <attr name="android:layout"/>
+
+ <!-- Sets the tab's content type. Defaults to icon. -->
+ <attr name="display">
+ <enum name="icon" value="0x00" />
+ <enum name="text" value="0x01" />
+ </attr>
+ </declare-styleable>
+
+ <declare-styleable name="TopSitesGridView">
+ <attr name="android:horizontalSpacing"/>
+ <attr name="android:verticalSpacing"/>
+ </declare-styleable>
+
+ <declare-styleable name="TabMenuStrip">
+ <attr name="strip" format="reference"/>
+ <attr name="tabsMarginLeft" format="dimension" />
+ <attr name="activeTextColor" format="color" />
+ <attr name="inactiveTextColor" format="color" />
+ <attr name="titlebarFill" format="boolean" />
+ </declare-styleable>
+
+ <declare-styleable name="CustomColorPreference">
+ <attr name="titleColor" format="color" />
+ <attr name="summaryColor" format="color" />
+ </declare-styleable>
+
+ <declare-styleable name="TabPanelBackButton">
+ <attr name="rightDivider" format="reference"/>
+ <attr name="dividerVerticalPadding" format="dimension"/>
+ </declare-styleable>
+
+ <declare-styleable name="EllipsisTextView">
+ <attr name="ellipsizeAtLine" format="integer"/>
+ </declare-styleable>
+
+ <declare-styleable name="FaviconView">
+ <attr name="dominantBorderEnabled" format="boolean" />
+ <attr name="overrideScaleType" format="boolean" />
+ <attr name="enableRoundCorners" format="boolean"/>
+ </declare-styleable>
+
+ <declare-styleable name="OverlayDialogButton">
+ <attr name="drawable" format="reference" />
+ <attr name="enabledText" format="string" />
+ <attr name="disabledText" format="string" />
+ </declare-styleable>
+
+ <declare-styleable name="ThemedView">
+ <!-- A reimplementation of android:tintList which is
+ otherwise only available on API 21+.
+
+ Using this attribute is mutually exclusive with android:tint
+ and setting colorFilters in code. This is because on pre-Lollipop,
+ android:tint and DrawableCompat.tint* uses colorFilters under the hood. -->
+ <attr name="drawableTintList" format="color" />
+ </declare-styleable>
+
+</resources>
+
diff --git a/mobile/android/base/resources/values/bool.xml b/mobile/android/base/resources/values/bool.xml
new file mode 100644
index 0000000000..6ef7090c64
--- /dev/null
+++ b/mobile/android/base/resources/values/bool.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ This Source Code Form is subject to the terms of the Mozilla Public
+ ~ License, v. 2.0. If a copy of the MPL was not distributed with this
+ ~ file, you can obtain one at http://mozilla.org/MPL/2.0/.
+ -->
+
+<resources>
+ <!-- Some devices use resources based on configuration (e.g. large, xlarge) that are inconsistent
+ with the configuration retrieved by HardwareUtils (e.g. some custom ROMs allow the user to
+ choose a phone or tablet version of the UI even though the hardware stays the same). This
+ can cause crashes when we branch on the value returned by HardwareUtils.
+
+ In order to work around this, we define the resource size in resources with the expectation that
+ we branch on that value, rather than HardwareUtils, so our code is consistent with the used resources.
+ See bug 1277379 for a initiative to move all of the HardwareUtils code over. -->
+ <bool name="is_large_resource">false</bool>
+</resources>
diff --git a/mobile/android/base/resources/values/colors.xml b/mobile/android/base/resources/values/colors.xml
new file mode 100644
index 0000000000..318ad50265
--- /dev/null
+++ b/mobile/android/base/resources/values/colors.xml
@@ -0,0 +1,148 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+ <!-- Fennec color palette (bug 1127517) -->
+ <color name="fennec_ui_orange">#FF9500</color>
+ <color name="affirmative_green">#6FBE4A</color>
+ <color name="rejection_red">#D23228</color>
+ <color name="action_orange">#E66000</color>
+ <color name="action_orange_pressed">#DC5600</color>
+ <color name="link_blue">#0096DD</color>
+ <color name="link_blue_pressed">#0082C6</color>
+ <color name="private_browsing_purple">#CF68FF</color>
+
+ <color name="placeholder_active_grey">#222222</color>
+ <color name="placeholder_grey">#777777</color>
+ <color name="private_toolbar_grey">#292C29</color>
+ <color name="text_and_tabs_tray_grey">#363B40</color>
+ <color name="tabs_tray_grey_pressed">#45494E</color>
+ <color name="toolbar_icon_grey">#5F6368</color>
+
+ <color name="tabs_tray_icon_grey">#AFB1B3</color>
+ <color name="disabled_grey">#BFBFBF</color>
+ <color name="toolbar_grey_pressed">#D7D7DC</color>
+ <color name="toolbar_menu_dark_grey">#E1E1E6</color>
+ <color name="toolbar_grey">#EBEBF0</color>
+ <color name="about_page_header_grey">#F5F5F5</color>
+
+ <color name="url_bar_shadow_private">#7878A5</color>
+
+ <!-- Restricted profiles palette -->
+
+ <color name="restricted_profile_background_gold">#ffffcb51</color>
+ <color name="restricted_profile_background_green">#1aaa86</color>
+
+ <!-- Non-palette colors -->
+
+ <!-- Synced w/ toolbar_grey -->
+ <color name="background_normal_lwt">#DDEBEBF0</color>
+
+ <color name="highlight">#33000000</color>
+ <color name="highlight_focused">#1A000000</color>
+ <color name="highlight_dark">#33FFFFFF</color>
+ <color name="highlight_dark_focused">#1AFFFFFF</color>
+
+ <!-- Synced w/ toolbar_grey_pressed -->
+ <color name="tablet_highlight_lwt">#AAD7D7DC</color>
+
+ <!-- Synced w/ tabs_tray_grey_pressed -->
+ <color name="tablet_highlight_dark_lwt">#AA45494E</color>
+
+ <!-- (bug 1077195) Focused state values are temporary. -->
+ <color name="tablet_highlight_focused">#C0C9D0</color>
+
+ <!-- highlight on shaped button: 20% white over text_and_tabs_tray_grey -->
+ <color name="highlight_shaped">#FF696D71</color>
+
+ <!-- highlight-focused on shaped button: 10% white over text_and_tabs_tray_grey -->
+ <color name="highlight_shaped_focused">#FF565B60</color>
+
+ <!-- highlight on private nav button: 20% white over private_toolbar_grey -->
+ <color name="highlight_nav_pb">#FF545654</color>
+
+ <color name="dark_transparent_overlay">#99000000</color>
+
+ <!-- Firstrun tour -->
+ <color name="firstrun_pager_header">#E8E8E8</color>
+
+ <!-- Tab Queue -->
+ <color name="tab_queue_dismiss_button_foreground">#16A3DF</color>
+ <color name="tab_queue_dismiss_button_foreground_pressed">#1193CB</color>
+
+ <!--
+ Application theme colors
+ -->
+ <!-- Default colors -->
+ <color name="text_color_tertiary">#9198A1</color>
+
+ <!-- Default inverse colors -->
+ <color name="text_color_secondary_inverse">#DDDDDD</color>
+
+ <!-- Disabled colors -->
+ <color name="text_color_primary_disable_only">#999999</color>
+
+ <!-- Hint colors -->
+ <color name="text_color_hint">#666666</color>
+ <color name="text_color_hint_inverse">#7F828A</color>
+
+ <!-- Highlight colors -->
+ <color name="text_color_highlight_inverse">#D06BFF</color>
+
+ <!-- Link colors -->
+ <color name="text_color_link">#22629E</color>
+
+ <!-- Divider colors -->
+ <color name="toolbar_divider_grey">#D7D9DB</color>
+
+ <color name="doorhanger_link">#FF2AA1FE</color>
+
+ <color name="validation_message_text">#ffffff</color>
+ <color name="url_bar_text_highlight_pb">#FFD06BFF</color>
+ <color name="tab_row_pressed">#4D000000</color>
+
+ <color name="url_bar_urltext">#AFB1B3</color>
+ <color name="url_bar_urltext_private">#777777</color>
+ <color name="url_bar_domaintext">#363B40</color>
+ <color name="url_bar_domaintext_private">#FFFFFF</color>
+ <color name="url_bar_blockedtext">#b14646</color>
+ <color name="url_bar_shadow">#12000000</color>
+
+ <color name="panel_image_item_background">#D1D9E1</color>
+ <color name="panel_icon_item_title_background">#32000000</color>
+ <color name="panel_tab_text_normal">#FFBFBFBF</color>
+
+ <!-- Remote tabs setup -->
+ <color name="remote_tabs_setup_button_background_hit">#D95300</color>
+
+ <!-- Button toast colors. -->
+ <color name="toast_background">#DD222222</color>
+ <color name="toast_button_background">#00000000</color>
+ <color name="toast_button_pressed">#DD2C3136</color>
+ <color name="toast_button_text">#FFFFFFFF</color>
+
+ <!-- Tab History colors. -->
+ <color name="tab_history_timeline_separator">#D7D9DB</color>
+ <color name="tab_history_favicon_border">#D7D9DB</color>
+ <color name="tab_history_favicon_background">#FFFFFF</color>
+ <color name="tab_history_border_color">#DADADF</color>
+
+ <!-- Canvas delegate paint color -->
+ <color name="canvas_delegate_paint">#FFFF0000</color>
+
+ <!-- Top sites thumbnail colors -->
+ <color name="top_site_default">#FFECF0F3</color>
+ <color name="top_site_border">#FFCFD9E1</color>
+
+ <color name="private_active_text">#FFFFFF</color>
+
+ <color name="action_bar_bg_color">@color/toolbar_grey</color>
+
+ <color name="activity_stream_divider">#FFD2D2D2</color>
+ <color name="activity_stream_subtitle">#FF919191</color>
+ <color name="activity_stream_timestamp">#FFD3D3D3</color>
+ <color name="activity_stream_icon">#FF919191</color>
+
+</resources>
diff --git a/mobile/android/base/resources/values/dimens.xml b/mobile/android/base/resources/values/dimens.xml
new file mode 100644
index 0000000000..b730f96717
--- /dev/null
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -0,0 +1,227 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <dimen name="standard_corner_radius">4dp</dimen>
+
+ <dimen name="autocomplete_min_width">200dp</dimen>
+ <dimen name="autocomplete_row_height">32dp</dimen>
+
+ <dimen name="browser_toolbar_height">48dp</dimen>
+ <!-- This value is the height of the Tabs Panel header view
+ (browser_toolbar_height) minus the height of the indicator
+ (6dp). This value should change when the height of the view changes. -->
+ <dimen name="tabs_panel_indicator_selected_padding_top">42dp</dimen>
+
+ <!-- We use two different values for browser_toolbar_height on tablet
+ which is inconsistent. Temporary value until bug 1150730 is fixed. -->
+ <dimen name="browser_toolbar_height_flipper">48dp</dimen>
+ <dimen name="browser_toolbar_button_padding">12dp</dimen>
+ <dimen name="browser_toolbar_icon_width">48dp</dimen>
+ <dimen name="browser_toolbar_menu_icon_height">16dp</dimen>
+
+ <!-- favicon_size includes 4dp of right padding. We can't use margin (which would allow us to
+ specify the actual size) because that would decrease the size of our hit target. -->
+ <dimen name="browser_toolbar_favicon_size">21.33dip</dimen>
+ <dimen name="browser_toolbar_shadow_size">2dp</dimen>
+
+ <!-- If you update one of these values, update the others. -->
+ <dimen name="tablet_nav_button_width">42dp</dimen>
+ <dimen name="tablet_nav_button_width_half">21dp</dimen>
+ <dimen name="tablet_nav_button_width_plus_half">63dp</dimen>
+
+ <!-- This is the system default for the vertical padding for the divider of the TabWidget.
+ Used to mimic the divider padding on the tablet tabs panel back button. -->
+ <dimen name="tab_panel_divider_vertical_padding">12dp</dimen>
+
+ <dimen name="tablet_tab_strip_height">48dp</dimen>
+ <dimen name="tablet_tab_strip_item_width">208dp</dimen>
+ <dimen name="tablet_tab_strip_item_margin">-28dp</dimen>
+ <dimen name="tablet_tab_strip_fading_edge_size">15dp</dimen>
+ <dimen name="tablet_browser_toolbar_menu_item_width">56dp</dimen>
+ <!-- Padding combines with an 18dp image to form the menu item width and height. -->
+ <dimen name="tablet_browser_toolbar_menu_item_padding_horizontal">19dp</dimen>
+ <dimen name="tablet_browser_toolbar_menu_item_inset_vertical">5dp</dimen>
+ <dimen name="tablet_browser_toolbar_menu_item_inset_horizontal">3dp</dimen>
+ <dimen name="tablet_tab_strip_button_inset">5dp</dimen>
+
+ <!-- Dimensions used by Favicons and FaviconView -->
+ <dimen name="favicon_bg">32dp</dimen>
+ <dimen name="favicon_corner_radius">4dp</dimen>
+ <!-- Set the upper limit on the size of favicon that will be processed. Favicons larger than
+ this will be downscaled to this value. If you need to use larger Favicons (Due to a UI
+ redesign sometime after this is written) you should increase this value to the largest
+ commonly-used size of favicon and, performance permitting, fetch the remainder from the
+ database. The largest available size is always stored in the database, regardless of this
+ value.-->
+ <dimen name="favicon_largest_interesting_size">32dp</dimen>
+
+ <dimen name="firstrun_content_width">300dp</dimen>
+ <dimen name="firstrun_min_height">120dp</dimen>
+ <dimen name="firstrun_background_height">120dp</dimen>
+
+ <dimen name="overlay_prompt_content_width">260dp</dimen>
+ <dimen name="overlay_prompt_button_width">148dp</dimen>
+ <dimen name="overlay_prompt_container_width">@dimen/match_parent</dimen>
+
+ <!-- Site security icon -->
+ <dimen name="browser_toolbar_site_security_height">32dp</dimen>
+ <dimen name="browser_toolbar_site_security_width">32dp</dimen>
+ <dimen name="browser_toolbar_site_security_margin_right">0dp</dimen>
+ <dimen name="browser_toolbar_site_security_padding_vertical">7dp</dimen>
+ <dimen name="browser_toolbar_site_security_padding_horizontal">7dp</dimen>
+
+ <!-- If one of these values changes, they all should. -->
+ <dimen name="browser_toolbar_site_security_margin_bottom">.5dp</dimen>
+ <dimen name="site_security_unknown_inset_top">1dp</dimen>
+ <dimen name="site_security_unknown_inset_bottom">-1dp</dimen>
+
+ <dimen name="home_folder_title_oneline_textsize">16sp</dimen>
+ <dimen name="home_folder_title_twoline_textsize">14sp</dimen>
+ <dimen name="home_twolinepagerow_title_textsize">16sp</dimen>
+
+ <dimen name="page_row_edge_padding">16dp</dimen>
+
+ <!-- Regular page row on about:home -->
+ <dimen name="page_row_height">64dp</dimen>
+
+ <!-- Group/heading page row on about:home -->
+ <dimen name="page_group_height">56dp</dimen>
+ <dimen name="home_header_item_height">56dp</dimen>
+ <dimen name="page_row_divider_height">1dp</dimen>
+
+ <!-- Remote Tabs static view top padding. Less in landscape on phones. -->
+ <dimen name="home_remote_tabs_top_padding">48dp</dimen>
+
+ <!-- Remote Tabs Hidden devices row height -->
+ <dimen name="home_remote_tabs_hidden_footer_height">40dp</dimen>
+
+ <!-- Search Engine Row height -->
+ <dimen name="search_row_height">48dp</dimen>
+
+ <dimen name="doorhanger_width">300dp</dimen>
+ <dimen name="doorhanger_input_width">250dp</dimen>
+ <dimen name="doorhanger_offsetX">12dp</dimen>
+ <dimen name="doorhanger_offsetY">67dp</dimen>
+ <dimen name="doorhanger_drawable_padding">5dp</dimen>
+ <dimen name="doorhanger_subsection_padding">8dp</dimen>
+ <dimen name="doorhanger_section_padding_small">10dp</dimen>
+ <dimen name="doorhanger_section_padding_medium">20dp</dimen>
+ <dimen name="doorhanger_section_padding_large">30dp</dimen>
+ <dimen name="doorhanger_icon_size">60dp</dimen>
+ <dimen name="doorhanger_rounded_corner_radius">4dp</dimen>
+
+ <dimen name="context_menu_item_horizontal_padding">10dp</dimen>
+
+ <dimen name="flow_layout_spacing">6dp</dimen>
+ <dimen name="menu_item_icon">21dp</dimen>
+ <dimen name="menu_item_textsize">16sp</dimen>
+ <dimen name="menu_item_state_icon">18dp</dimen>
+ <!-- This is chosen to match Android's listPreferredItemHeight.
+ TODO: We should inherit these from the system.
+ http://androidxref.com/4.2.2_r1/xref/frameworks/base/core/res/res/values/themes.xml#123 -->
+ <dimen name="menu_item_row_height">64dip</dimen>
+ <dimen name="menu_item_row_width">240dp</dimen>
+ <dimen name="menu_popup_width">256dp</dimen>
+ <dimen name="nav_button_border_width">1dp</dimen>
+ <dimen name="prompt_service_group_padding_size">32dp</dimen>
+ <dimen name="prompt_service_icon_size">36dp</dimen>
+ <dimen name="prompt_service_icon_text_padding">10dp</dimen>
+ <dimen name="prompt_service_inputs_padding">16dp</dimen>
+ <dimen name="prompt_service_left_right_text_with_icon_padding">10dp</dimen>
+ <dimen name="prompt_service_top_bottom_text_with_icon_padding">8dp</dimen>
+ <dimen name="tabs_panel_indicator_width">60dp</dimen>
+ <dimen name="tabs_panel_button_width">48dp</dimen>
+ <dimen name="tabs_strip_height">48dp</dimen>
+ <dimen name="tabs_strip_button_width">100dp</dimen>
+ <dimen name="tabs_strip_button_padding">18dp</dimen>
+ <dimen name="tabs_strip_shadow_size">1dp</dimen>
+ <dimen name="validation_message_height">50dp</dimen>
+ <dimen name="validation_message_margin_top">6dp</dimen>
+
+ <dimen name="tab_thumbnail_width">121dp</dimen>
+ <dimen name="tab_thumbnail_height">90dp</dimen>
+ <dimen name="tab_panel_column_width">129dp</dimen>
+ <dimen name="tab_panel_grid_padding">20dp</dimen>
+ <dimen name="tab_panel_grid_vspacing">20dp</dimen>
+ <dimen name="tab_panel_grid_padding_top">19dp</dimen>
+
+ <dimen name="tab_highlight_stroke_width">4dp</dimen>
+
+ <!-- PageActionButtons dimensions -->
+ <dimen name="page_action_button_width">32dp</dimen>
+
+ <!-- Banner -->
+ <dimen name="home_banner_height">72dp</dimen>
+ <dimen name="home_banner_close_width">42dp</dimen>
+ <dimen name="home_banner_icon_height">48dip</dimen>
+ <dimen name="home_banner_icon_width">48dip</dimen>
+
+ <!-- Icon Grid -->
+ <dimen name="icongrid_columnwidth">128dp</dimen>
+ <dimen name="icongrid_padding">16dp</dimen>
+
+ <!-- PanelRecyclerView dimensions -->
+ <dimen name="panel_grid_view_column_width">150dp</dimen>
+ <dimen name="panel_grid_view_horizontal_spacing">3dp</dimen>
+ <dimen name="panel_grid_view_vertical_spacing">3dp</dimen>
+ <dimen name="panel_grid_view_outer_spacing">3dp</dimen>
+
+ <!-- PanelItemView dimensions -->
+ <dimen name="panel_article_item_height">95dp</dimen>
+
+ <!-- Button toast dimenstions. -->
+ <dimen name="toast_button_corner_radius">2dp</dimen>
+
+ <!-- TabHistoryItemRow dimensions. -->
+ <dimen name="tab_history_timeline_width">3dp</dimen>
+ <dimen name="tab_history_timeline_height">14dp</dimen>
+ <dimen name="tab_history_favicon_bg">32dp</dimen>
+ <dimen name="tab_history_favicon_padding">5dp</dimen>
+ <dimen name="tab_history_favicon_border_enabled">3dp</dimen>
+ <dimen name="tab_history_favicon_border_disabled">1dp</dimen>
+ <dimen name="tab_history_combo_margin_left">15dp</dimen>
+ <dimen name="tab_history_combo_margin_right">15dp</dimen>
+ <dimen name="tab_history_title_fading_width">50dp</dimen>
+ <dimen name="tab_history_title_margin_right">15dp</dimen>
+ <dimen name="tab_history_title_text_size">14sp</dimen>
+ <dimen name="tab_history_bg_width">2dp</dimen>
+ <dimen name="tab_history_border_padding">2dp</dimen>
+
+ <!-- ZoomedView dimensions. -->
+ <dimen name="zoomed_view_toolbar_height">44dp</dimen>
+ <dimen name="drawable_dropshadow_size">3dp</dimen>
+
+ <!-- Find-In-Page dialog dimensions. -->
+ <dimen name="find_in_page_text_margin_left">5dip</dimen>
+ <dimen name="find_in_page_text_margin_right">12dip</dimen>
+ <dimen name="find_in_page_text_padding_left">10dip</dimen>
+ <dimen name="find_in_page_text_padding_right">10dip</dimen>
+ <dimen name="find_in_page_status_margin_right">10dip</dimen>
+ <dimen name="find_in_page_control_margin_top">2dip</dimen>
+ <dimen name="progress_bar_scroll_offset">1.5dp</dimen>
+
+ <!-- Matches the built-in divider height. fwiw, in the framework
+ I suspect this is a drawable rather than a dimen. -->
+ <dimen name="action_bar_divider_height">2dp</dimen>
+
+ <!-- http://blog.danlew.net/2015/01/06/handling-android-resources-with-non-standard-formats/ -->
+ <item name="match_parent" type="dimen">-1</item>
+ <item name="wrap_content" type="dimen">-2</item>
+
+ <item name="tab_strip_content_start" type="dimen">12dp</item>
+ <item name="firstrun_tab_strip_content_start" type="dimen">15dp</item>
+
+ <item name="notification_media_cover" type="dimen">128dp</item>
+
+ <item name="activity_stream_base_margin" type="dimen">10dp</item>
+ <item name="activity_stream_desired_tile_width" type="dimen">90dp</item>
+ <item name="activity_stream_desired_tile_height" type="dimen">70dp</item>
+ <item name="activity_stream_top_sites_text_height" type="dimen">30dp</item>
+
+ <!-- Default touch target size for buttons/imageviews that might be of small size -->
+ <item name="touch_target_size" type="dimen">48dp</item>
+</resources>
diff --git a/mobile/android/base/resources/values/ids.xml b/mobile/android/base/resources/values/ids.xml
new file mode 100644
index 0000000000..2fe1ca7938
--- /dev/null
+++ b/mobile/android/base/resources/values/ids.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <item type="id" name="tabQueueNotification"/>
+ <item type="id" name="tabQueueSettingsNotification" />
+ <item type="id" name="guestNotification"/>
+ <item type="id" name="original_height"/>
+ <item type="id" name="menu_items"/>
+ <item type="id" name="menu_margin"/>
+ <item type="id" name="recycler_view_click_support" />
+ <item type="id" name="range_list"/>
+ <item type="id" name="pref_header_general"/>
+ <item type="id" name="pref_header_privacy"/>
+ <item type="id" name="pref_header_search"/>
+ <item type="id" name="updateServicePermissionNotification" />
+ <item type="id" name="websiteContentNotification" />
+ <item type="id" name="foregroundNotification" />
+
+</resources>
diff --git a/mobile/android/base/resources/values/integers.xml b/mobile/android/base/resources/values/integers.xml
new file mode 100644
index 0000000000..b3451e05d4
--- /dev/null
+++ b/mobile/android/base/resources/values/integers.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <integer name="number_of_top_sites">6</integer>
+ <integer name="number_of_top_sites_cols">2</integer>
+ <integer name="max_icon_grid_columns">4</integer>
+ <integer name="panel_icon_grid_view_columns">3</integer>
+ <integer name="number_of_inline_share_devices">2</integer>
+ <integer name="max_search_suggestions">2</integer>
+ <integer name="max_saved_suggestions">2</integer>
+ <integer name="search_assist_launch_res">0</integer>
+
+</resources>
diff --git a/mobile/android/base/resources/values/search_attrs.xml b/mobile/android/base/resources/values/search_attrs.xml
new file mode 100644
index 0000000000..d1b0ded598
--- /dev/null
+++ b/mobile/android/base/resources/values/search_attrs.xml
@@ -0,0 +1,12 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+ <declare-styleable name="FacetButton"/>
+
+ <!--This attribute is required so that we can create a facet button-->
+ <!--pragmatically. The defStyle param used in the View constructor-->
+ <!--must be an attr, see: https://code.google.com/p/android/issues/detail?id=12683-->
+ <attr name="facetButtonStyle" />
+</resources>
diff --git a/mobile/android/base/resources/values/search_colors.xml b/mobile/android/base/resources/values/search_colors.xml
new file mode 100644
index 0000000000..8718d1b514
--- /dev/null
+++ b/mobile/android/base/resources/values/search_colors.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <!-- card colors -->
+ <color name="row_background">#ffffff</color>
+ <color name="row_background_pressed">#DCDCE1</color>
+
+ <color name="widget_button_pressed">#33000000</color>
+
+ <!--Facet button colors-->
+ <color name="facet_button_background_color_default">@android:color/white</color>
+ <color name="facet_button_background_color_pressed">#FAFAFA</color>
+
+ <color name="facet_button_text_color_default">#ADB0B1</color>
+ <color name="facet_button_text_color_selected">#383E42</color>
+
+ <color name="network_error_link">#0092DB</color>
+
+ <!-- Suggestion highlight color -->
+ <color name="suggestion_highlight">#FF999999</color>
+</resources>
diff --git a/mobile/android/base/resources/values/search_dimens.xml b/mobile/android/base/resources/values/search_dimens.xml
new file mode 100644
index 0000000000..d35641a168
--- /dev/null
+++ b/mobile/android/base/resources/values/search_dimens.xml
@@ -0,0 +1,30 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+ <!-- The height of the search bar is also used to offset the PreSearchFragment
+ and PostSearchFragment contents -->
+ <dimen name="search_bar_height">65dp</dimen>
+ <dimen name="progress_bar_height">3dp</dimen>
+
+ <!-- Size of the text for query input and suggestions -->
+ <dimen name="query_text_size">16sp</dimen>
+
+ <dimen name="search_row_padding">15dp</dimen>
+ <dimen name="search_bar_padding_y">10dp</dimen>
+
+ <!-- Padding to account for search engine icon/clear button -->
+ <dimen name="search_bar_padding_right">25dp</dimen>
+
+ <dimen name="search_history_drawable_padding">10dp</dimen>
+
+ <!-- Widget Buttons -->
+ <dimen name="widget_header_height">70dp</dimen>
+ <dimen name="widget_text_size">14sp</dimen>
+ <dimen name="widget_padding">7dp</dimen>
+ <dimen name="widget_drawable_corner_radius">4dp</dimen>
+ <dimen name="widget_bg_border_offset">3dp</dimen>
+
+ <dimen name="facet_button_underline_thickness">5dp</dimen>
+</resources>
diff --git a/mobile/android/base/resources/values/search_styles.xml b/mobile/android/base/resources/values/search_styles.xml
new file mode 100644
index 0000000000..bc5adcd2ed
--- /dev/null
+++ b/mobile/android/base/resources/values/search_styles.xml
@@ -0,0 +1,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/. -->
+
+<resources>
+
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="@android:style/Theme.Light.NoTitleBar">
+ <item name="android:windowBackground">@color/toolbar_grey</item>
+ <item name="android:colorBackground">@color/toolbar_grey</item>
+
+ <!--This attribute is required so that we can create a facet button-->
+ <!--pragmatically. The defStyle param used in the View constructor-->
+ <!--must be an attr, see: https://code.google.com/p/android/issues/detail?id=12683-->
+ <item name="facetButtonStyle">@style/FacetButtonStyle</item>
+ </style>
+
+ <style name="SettingsTheme" parent="@android:style/Theme.Light" />
+
+ <style name="FacetButtonStyle">
+ <!--Since we're not inflating xml, we have to apply the layout params -->
+ <!--after instantiation. See FacetBar.addFacet.-->
+ <item name="android:textSize">15sp</item>
+ <item name="android:textColor">@color/facet_button_text_color</item>
+ <item name="android:background">@drawable/facet_button_background</item>
+ <item name="android:gravity">center</item>
+ <item name="android:clickable">true</item>
+ </style>
+
+ <style name="TextAppearance.EmptyView.Title" parent="@android:style/TextAppearance.Small">
+ <item name="android:textColor">@color/text_and_tabs_tray_grey</item>
+ <item name="android:textSize">20sp</item>
+ </style>
+
+ <style name="TextAppearance.EmptyView.Message" parent="@android:style/TextAppearance.Small">
+ <item name="android:textColor">@color/placeholder_grey</item>
+ <item name="android:textSize">16sp</item>
+ </style>
+
+</resources>
diff --git a/mobile/android/base/resources/values/styles.xml b/mobile/android/base/resources/values/styles.xml
new file mode 100644
index 0000000000..dd22ef00bd
--- /dev/null
+++ b/mobile/android/base/resources/values/styles.xml
@@ -0,0 +1,832 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
+
+ <!--
+ Base application styles. This could be overridden in other res/values-XXX/themes.xml.
+ -->
+ <style name="Widget"/>
+
+ <style name="Widget.BaseButton" parent="android:style/Widget.Button"/>
+
+ <style name="Widget.BaseDropDownItem" parent="android:style/Widget.DropDownItem"/>
+
+ <style name="Widget.BaseEditText" parent="android:style/Widget.EditText"/>
+
+ <style name="Widget.BaseListView" parent="android:style/Widget.ListView"/>
+
+ <style name="Widget.BaseGridView" parent="android:style/Widget.GridView"/>
+
+ <style name="Widget.BaseTextView" parent="android:style/Widget.TextView"/>
+
+ <style name="Widget.ProgressBar.Horizontal" parent="android:style/Widget.ProgressBar.Horizontal"/>
+
+ <!--
+ Application styles. All customizations that are not specific
+ to a particular API level can go here.
+ -->
+ <style name="Widget.Button" parent="Widget.BaseButton">
+ <item name="android:textAppearance">@style/TextAppearance.Widget.Button</item>
+ </style>
+
+ <style name="Widget.DropDownItem" parent="Widget.BaseDropDownItem">
+ <item name="android:textAppearance">@style/TextAppearance.Widget.DropDownItem</item>
+ </style>
+
+ <style name="Widget.EditText" parent="Widget.BaseEditText">
+ <item name="android:textAppearance">@style/TextAppearance.Widget.EditText</item>
+ </style>
+
+ <style name="Widget.TextView" parent="Widget.BaseTextView">
+ <item name="android:textAppearance">@style/TextAppearance.Widget.TextView</item>
+ </style>
+
+ <style name="Widget.ListView" parent="Widget.BaseListView">
+ <item name="android:divider">@color/toolbar_divider_grey</item>
+ <item name="android:dividerHeight">@dimen/page_row_divider_height</item>
+ <item name="android:cacheColorHint">@android:color/transparent</item>
+ <item name="android:listSelector">@drawable/action_bar_button</item>
+ </style>
+
+ <style name="Widget.ExpandableListView" parent="Widget.ListView">
+ <item name="android:groupIndicator">@android:color/transparent</item>
+ </style>
+
+ <style name="Widget.GridView" parent="Widget.BaseGridView">
+ <item name="android:verticalSpacing">0dip</item>
+ <item name="android:horizontalSpacing">0dip</item>
+ <item name="android:cacheColorHint">@android:color/transparent</item>
+ <item name="android:listSelector">@drawable/action_bar_button</item>
+ </style>
+
+ <style name="Widget.Home.HomeList">
+ <item name="android:scrollbarStyle">outsideOverlay</item>
+ </style>
+
+ <style name="Widget.ListItem">
+ <item name="android:minHeight">?android:attr/listPreferredItemHeight</item>
+ <item name="android:textAppearance">?android:attr/textAppearanceLargeInverse</item>
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:paddingLeft">12dip</item>
+ <item name="android:paddingRight">7dip</item>
+ <item name="android:checkMark">?android:attr/listChoiceIndicatorMultiple</item>
+ <item name="android:ellipsize">marquee</item>
+ </style>
+
+ <style name="Widget.Spinner" parent="android:style/Widget.Spinner">
+ <item name="android:minWidth">@dimen/doorhanger_input_width</item>
+ </style>
+
+ <style name="Widget.GeckoMenuListView" parent="Widget.ListView">
+ <item name="android:listSelector">@drawable/menu_item_action_bar_bg</item>
+ <item name="android:divider">@null</item>
+ <item name="android:dividerHeight">0dp</item>
+ </style>
+
+ <style name="Widget.MenuItemActionBar">
+ <item name="android:padding">10dip</item>
+ <item name="android:background">@drawable/menu_item_action_bar_bg</item>
+ <item name="android:scaleType">fitCenter</item>
+ <item name="drawableTintList">@color/action_bar_menu_item_colors</item>"
+ </style>
+
+ <style name="Widget.MenuItemSecondaryActionBar">
+ <item name="android:padding">8dip</item>
+ <item name="android:background">@drawable/menu_item_action_bar_bg</item>
+ <item name="android:scaleType">centerInside</item>
+ <item name="drawableTintList">@color/action_bar_secondary_menu_item_colors</item>
+ </style>
+
+ <style name="Widget.MenuItemSwitcherLayout">
+ <item name="android:gravity">left</item>
+ </style>
+
+ <style name="Widget.MenuItemDefault">
+ <item name="android:paddingLeft">15dip</item>
+ <item name="android:paddingRight">10dip</item>
+ <item name="android:drawablePadding">6dip</item>
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:textAppearance">@style/TextAppearance</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:ellipsize">middle</item>
+ <item name="android:textSize">@dimen/menu_item_textsize</item>
+ </style>
+
+ <style name="Widget.FolderTitle">
+ <item name="android:textAppearance">@style/TextAppearance.Widget.Home.ItemTitle</item>
+ </style>
+
+ <style name="Widget.FolderTitle.OneLine">
+ <item name="android:textSize">@dimen/home_folder_title_oneline_textsize</item>
+ </style>
+
+ <style name="Widget.FolderTitle.TwoLine">
+ <item name="android:textSize">@dimen/home_folder_title_twoline_textsize</item>
+ </style>
+
+ <style name="Widget.TwoLinePageRow" >
+ <item name="android:background">@color/pressed_about_page_header_grey</item>
+ </style>
+
+ <style name="Widget.TwoLinePageRow.Title">
+ <item name="android:textAppearance">@style/TextAppearance.Widget.Home.ItemTitle</item>
+ <item name="android:textSize">@dimen/home_twolinepagerow_title_textsize</item>
+ </style>
+
+ <style name="Widget.TwoLinePageRow.Url">
+ <item name="android:textAppearance">@style/TextAppearance.Widget.Home.ItemDescription</item>
+ <item name="android:includeFontPadding">false</item>
+ <item name="android:singleLine">true</item>
+ </style>
+
+ <style name="Widget.FolderView" parent="Widget.FolderTitle.OneLine">
+ <item name="android:layout_height">@dimen/page_group_height</item>
+ <item name="android:minHeight">@dimen/page_group_height</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:ellipsize">none</item>
+ <item name="android:background">@color/about_page_header_grey</item>
+ <item name="android:paddingLeft">20dp</item>
+ <item name="android:drawablePadding">20dp</item>
+ </style>
+
+ <style name="Widget.PanelItemView" />
+
+ <style name="Widget.PanelItemView.Title">
+ <item name="android:textAppearance">@style/TextAppearance.Widget.Home.ItemTitle</item>
+ <item name="android:maxLines">2</item>
+ <item name="android:ellipsize">end</item>
+ </style>
+
+ <style name="Widget.PanelItemView.Description">
+ <item name="android:textAppearance">@style/TextAppearance.Widget.Home.ItemDescription</item>
+ <item name="android:includeFontPadding">false</item>
+ <item name="android:maxLines">2</item>
+ <item name="android:ellipsize">end</item>
+ </style>
+
+ <style name="Widget.TopSitesGridView" parent="Widget.GridView">
+ <item name="android:padding">7dp</item>
+ <item name="android:horizontalSpacing">0dp</item>
+ <item name="android:verticalSpacing">7dp</item>
+ </style>
+
+ <style name="Widget.TopSitesGridItemView">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">match_parent</item>
+ <item name="android:padding">5dip</item>
+ <item name="android:orientation">vertical</item>
+ </style>
+
+ <style name="Widget.TabsGridLayout" parent="Widget.GridView">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">match_parent</item>
+ <item name="android:paddingTop">0dp</item>
+ <item name="android:stretchMode">spacingWidth</item>
+ <item name="android:scrollbarStyle">outsideOverlay</item>
+ <item name="android:gravity">center</item>
+ <item name="android:numColumns">auto_fit</item>
+ <item name="android:columnWidth">@dimen/tab_panel_column_width</item>
+ <item name="android:horizontalSpacing">2dp</item>
+ <item name="android:verticalSpacing">@dimen/tab_panel_grid_vspacing</item>
+ <item name="android:drawSelectorOnTop">true</item>
+ <item name="android:clipToPadding">false</item>
+ </style>
+
+ <style name="Widget.BookmarkItemView" parent="Widget.TwoLinePageRow"/>
+
+ <style name="Widget.BookmarksListView" parent="Widget.HomeListView"/>
+
+ <style name="Widget.TopSitesThumbnailView">
+ <item name="android:padding">0dip</item>
+ <item name="android:scaleType">centerCrop</item>
+ </style>
+
+ <style name="Widget.TopSitesGridItemPin">
+ <item name="android:minWidth">30dip</item>
+ <item name="android:minHeight">30dip</item>
+ <item name="android:padding">0dip</item>
+ </style>
+
+ <style name="Widget.TopSitesGridItemTitle">
+ <item name="android:textColor">@color/top_sites_grid_item_title</item>
+ <item name="android:textSize">12sp</item>
+ <item name="android:paddingTop">5dip</item>
+ <item name="android:gravity">left</item>
+ </style>
+
+ <style name="Widget.HomeListView" parent="Widget.ListView">
+ <item name="android:divider">@color/toolbar_divider_grey</item>
+ </style>
+
+ <style name="Widget.TopSitesListView" parent="Widget.BookmarksListView"/>
+
+ <style name="Widget.HomeBanner"/>
+
+ <style name="Widget.Home" />
+
+ <style name="Widget.Home.HeaderItem">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">36dp</item>
+ <item name="android:textAppearance">@style/TextAppearance.Widget.Home.Header</item>
+ <item name="android:background">@android:color/white</item>
+ <item name="android:focusable">false</item>
+ <item name="android:gravity">center|left</item>
+ <item name="android:paddingLeft">16dp</item>
+ <item name="android:paddingRight">16dp</item>
+ <item name="android:paddingTop">11dp</item>
+ <item name="android:paddingBottom">11dp</item>
+ <item name="android:includeFontPadding">false</item>
+ </style>
+
+ <style name="Widget.Home.ActionButton">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">48dp</item>
+ <item name="android:textColor">@color/text_and_tabs_tray_grey</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:background">@drawable/home_history_clear_button_bg</item>
+ <item name="android:focusable">true</item>
+ <item name="android:gravity">center</item>
+ <item name="android:paddingLeft">10dip</item>
+ <item name="android:paddingRight">10dip</item>
+ </style>
+
+ <style name="Widget.Home.ActionItem">
+ <item name="android:layout_width">fill_parent</item>
+ <item name="android:layout_height">40dip</item>
+ <item name="android:textColor">#000000</item>
+ <item name="android:gravity">center</item>
+ </style>
+
+ <style name="Widget.Firstrun.Button" parent="Widget.BaseButton">
+ <item name="android:layout_width">@dimen/firstrun_content_width</item>
+ <item name="android:layout_height">60dp</item>
+ <item name="android:textColor">@color/android:white</item>
+ <item name="android:background">@color/action_orange</item>
+ <item name="android:textSize">18sp</item>
+ </style>
+
+ <style name="Widget.Doorhanger.Button" parent="Widget.BaseButton">
+ <item name="android:layout_width">0dp</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_weight">1</item>
+ <item name="android:minHeight">48dp</item>
+ <item name="android:textSize">14sp</item>
+ </style>
+
+ <!--
+ We are overriding the snackbar message style to guarantee a consistent style across Android versions (bug 1217416).
+ -->
+ <style name="TextAppearance.Design.Snackbar.Message" parent="android:TextAppearance" tools:override="true">
+ <item name="android:textSize">@dimen/design_snackbar_text_size</item>
+ <item name="android:textColor">@android:color/white</item>
+ </style>
+
+ <!--
+ TextAppearance
+ Note: Gecko uses light theme as default, while Android uses dark.
+ If Android convention has to be followd, the list of colors specified
+ in themes.xml would be inverse, and things would get confusing.
+ Hence, Gecko's TextAppearance is based on text over light theme and
+ TextAppearance.Inverse is based on text over dark theme.
+ -->
+ <style name="TextAppearance">
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:textColorHighlight">@color/fennec_ui_orange</item>
+ <item name="android:textColorHint">?android:attr/textColorHint</item>
+ <item name="android:textColorLink">?android:attr/textColorLink</item>
+ <item name="android:textSize">@dimen/menu_item_textsize</item>
+ <item name="android:textStyle">normal</item>
+ </style>
+
+ <style name="TextAppearance.Inverse">
+ <item name="android:textColor">?android:attr/textColorPrimaryInverse</item>
+ <item name="android:textColorHint">?android:attr/textColorHintInverse</item>
+ <item name="android:textColorHighlight">@color/text_color_highlight_inverse</item>
+ <item name="android:textColorLink">?android:attr/textColorLink</item>
+ </style>
+
+ <style name="TextAppearance.Large">
+ <item name="android:textSize">22sp</item>
+ </style>
+
+ <style name="TextAppearance.Large.Inverse">
+ <item name="android:textColor">?android:attr/textColorPrimaryInverse</item>
+ <item name="android:textColorHint">?android:attr/textColorHintInverse</item>
+ <item name="android:textColorHighlight">@color/text_color_highlight_inverse</item>
+ <item name="android:textColorLink">?android:attr/textColorLink</item>
+ </style>
+
+ <style name="TextAppearance.Medium">
+ <item name="android:textSize">18sp</item>
+ </style>
+
+ <style name="TextAppearance.Medium.Inverse">
+ <item name="android:textColor">?android:attr/textColorPrimaryInverse</item>
+ <item name="android:textColorHint">?android:attr/textColorHintInverse</item>
+ <item name="android:textColorHighlight">@color/text_color_highlight_inverse</item>
+ <item name="android:textColorLink">?android:attr/textColorLink</item>
+ </style>
+
+ <style name="TextAppearance.Small">
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">?android:attr/textColorSecondary</item>
+ </style>
+
+ <style name="TextAppearance.Small.Inverse">
+ <item name="android:textColor">?android:attr/textColorSecondaryInverse</item>
+ <item name="android:textColorHint">?android:attr/textColorHintInverse</item>
+ <item name="android:textColorHighlight">@color/text_color_highlight_inverse</item>
+ <item name="android:textColorLink">?android:attr/textColorLink</item>
+ </style>
+
+ <style name="TextAppearance.EmptyMessage" parent="TextAppearance.Large"/>
+
+ <style name="TextAppearance.EmptyHint" parent="TextAppearance.Medium">
+ <item name="android:textColor">#FFA62F</item>
+ <item name="android:textStyle">italic</item>
+ </style>
+
+ <style name="TextAppearance.Micro">
+ <item name="android:textSize">12sp</item>
+ <item name="android:textColor">?android:attr/textColorTertiary</item>
+ </style>
+
+ <style name="TextAppearance.Micro.Inverse">
+ <item name="android:textColor">?android:attr/textColorTertiaryInverse</item>
+ <item name="android:textColorHint">?android:attr/textColorHintInverse</item>
+ <item name="android:textColorHighlight">@color/text_color_highlight_inverse</item>
+ <item name="android:textColorLink">?android:attr/textColorLink</item>
+ </style>
+
+ <style name="TextAppearance.Widget" />
+
+ <style name="TextAppearance.Widget.Button" parent="TextAppearance.Small">
+ <item name="android:textColor">@color/primary_text</item>
+ </style>
+
+ <style name="TextAppearance.Widget.DropDownItem">
+ <item name="android:textColor">@color/primary_text</item>
+ </style>
+
+ <style name="TextAppearance.Widget.EditText">
+ <item name="android:textColor">@color/primary_text</item>
+ </style>
+
+ <style name="TextAppearance.Widget.TextView">
+ <item name="android:textColor">@color/primary_text</item>
+ </style>
+
+ <style name="TextAppearance.Widget.HomePagerTabMenuStrip" parent="TextAppearance.Small">
+ <item name="android:textColor">?android:attr/textColorHint</item>
+ <item name="android:textSize">14sp</item>
+ </style>
+
+ <style name="TextAppearance.Widget.Home" />
+
+ <style name="TextAppearance.Widget.Home.Header" parent="TextAppearance.Small">
+ <item name="android:textColor">@color/disabled_grey</item>
+ <item name="android:textSize">12sp</item>
+ </style>
+
+ <style name="TextAppearance.Widget.Home.ItemTitle" parent="TextAppearance">
+ <item name="android:textSize">16dp</item>
+ </style>
+
+ <style name="TextAppearance.Widget.Home.ItemDescription" parent="TextAppearance.Micro">
+ <item name="android:textColor">@color/tabs_tray_icon_grey</item>
+ </style>
+
+ <style name="TextAppearance.Widget.HomeBanner" parent="TextAppearance.Small">
+ <item name="android:textColor">?android:attr/textColorHint</item>
+ </style>
+
+ <style name="TextAppearance.DoorHanger">
+ <item name="android:textColor">@color/placeholder_active_grey</item>
+ <item name="android:textColorLink">@color/doorhanger_link</item>
+ </style>
+
+ <style name="TextAppearance.DoorHanger.Medium">
+ <item name="android:textSize">16dp</item>
+ </style>
+
+ <style name="TextAppearance.DoorHanger.Medium.Bold">
+ <item name="android:fontFamily">sans-serif-medium</item>
+ </style>
+
+ <style name="TextAppearance.DoorHanger.Medium.Light">
+ <item name="android:fontFamily">sans-serif-light</item>
+ </style>
+
+ <style name="TextAppearance.DoorHanger.Small">
+ <item name="android:textSize">14sp</item>
+ </style>
+
+ <style name="TextAppearance.UrlBar.Title" parent="TextAppearance.Small">
+ <item name="android:textSize">15sp</item>
+ </style>
+
+ <!-- BrowserToolbar -->
+ <style name="BrowserToolbar">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">@dimen/browser_toolbar_height</item>
+ <item name="android:orientation">horizontal</item>
+ </style>
+
+ <!-- URL bar -->
+ <style name="UrlBar">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">match_parent</item>
+ <item name="android:orientation">horizontal</item>
+ </style>
+
+ <!-- URL bar - Button -->
+ <style name="UrlBar.Button">
+ <item name="android:layout_height">match_parent</item>
+ <item name="android:background">@android:color/transparent</item>
+ </style>
+
+ <!-- URL bar - Button -->
+ <style name="UrlBar.Title" parent="UrlBar.Button">
+ <item name="android:textAppearance">@style/TextAppearance.UrlBar.Title</item>
+ <item name="android:textColor">@color/url_bar_title</item>
+ <item name="android:textColorHint">@color/url_bar_title_hint</item>
+ <item name="android:textColorHighlight">@color/fennec_ui_orange</item>
+ <item name="android:textSelectHandle">@drawable/handle_middle</item>
+ <item name="android:textSelectHandleLeft">@drawable/handle_start</item>
+ <item name="android:textSelectHandleRight">@drawable/handle_end</item>
+ <item name="android:textCursorDrawable">@null</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:gravity">center_vertical|left</item>
+ <item name="android:hint">@string/url_bar_default_text</item>
+ </style>
+
+ <!-- URL bar - Image Button -->
+ <style name="UrlBar.ImageButtonBase" parent="UrlBar.Button">
+ <item name="android:scaleType">center</item>
+ <item name="android:layout_gravity">center_vertical</item>
+ <item name="android:background">@android:color/transparent</item>
+ </style>
+
+ <style name="UrlBar.ImageButton" parent="UrlBar.ImageButtonBase">
+ <item name="android:layout_width">@dimen/browser_toolbar_height</item>
+ </style>
+
+ <!-- TabsLayout -->
+ <style name="TabsLayoutBase">
+ <item name="android:background">@android:color/transparent</item>
+ <item name="android:listSelector">@android:color/transparent</item>
+ </style>
+
+ <style name="TabsLayout" parent="TabsLayoutBase">
+ <item name="android:orientation">vertical</item>
+ <item name="android:scrollbars">vertical</item>
+ </style>
+
+ <style name="TabsItem">
+ <item name="android:nextFocusRight">@+id/close</item>
+ </style>
+
+ <style name="TabsItemClose">
+ <item name="android:nextFocusLeft">@+id/info</item>
+ </style>
+
+ <!-- Tabs panel -->
+ <style name="TabsPanelSectionBase">
+ <item name="android:orientation">vertical</item>
+ <item name="android:layout_marginLeft">40dp</item>
+ <item name="android:layout_marginRight">40dp</item>
+ </style>
+
+ <style name="TabsPanelSection" parent="TabsPanelSectionBase">
+ <!-- We set values in landscape. -->
+ </style>
+
+ <style name="TabsPanelItemBase">
+ <item name="android:layout_marginBottom">28dp</item>
+ <item name="android:layout_gravity">center</item>
+ <item name="android:gravity">center</item>
+ </style>
+
+ <style name="TabsPanelItem" parent="TabsPanelItemBase">
+ <!-- We set values in landscape. -->
+ </style>
+
+ <style name="TabsPanelItem.TextAppearance">
+ <item name="android:textColor">#C0C9D0</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:lineSpacingMultiplier">1.35</item>
+ </style>
+
+ <style name="TabsPanelItem.TextAppearance.Header">
+ <item name="android:textSize">18sp</item>
+ <item name="android:layout_marginBottom">16dp</item>
+ </style>
+
+ <style name="TabsPanelItem.TextAppearance.Linkified">
+ <item name="android:clickable">true</item>
+ <item name="android:focusable">true</item>
+ <item name="android:textColor">#0292D6</item>
+ </style>
+
+ <style name="Widget.RemoteTabsItemView" parent="Widget.TwoLinePageRow"/>
+
+ <style name="Widget.RemoteTabsClientView" parent="Widget.TwoLinePageRow">
+ <item name="android:background">@color/about_page_header_grey</item>
+ </style>
+
+ <style name="Widget.RemoteTabsListView" parent="Widget.HomeListView">
+ <item name="android:childDivider">@color/toolbar_divider_grey</item>
+ </style>
+
+ <style name="Widget.HistoryListView" parent="Widget.HomeListView">
+ <item name="android:childDivider">@color/toolbar_divider_grey</item>
+ <item name="android:drawSelectorOnTop">true</item>
+ </style>
+
+ <!-- TabsLayout Row -->
+ <style name="TabLayoutItemTextAppearance">
+ <item name="android:textColor">#FFFFFFFF</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:ellipsize">middle</item>
+ </style>
+
+ <!-- TabsLayout RemoteTabs Row Url -->
+ <style name="TabLayoutItemTextAppearance.Url">
+ <item name="android:textColor">#FFA4A7A9</item>
+ </style>
+
+ <!-- TabWidget -->
+ <style name="TabWidget">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">40dip</item>
+ <item name="android:layout_weight">1.0</item>
+ </style>
+
+ <!-- Find bar -->
+ <style name="FindBar">
+ <item name="android:background">@color/text_and_tabs_tray_grey</item>
+ <item name="android:paddingLeft">3dip</item>
+ <item name="android:paddingRight">3dip</item>
+ <item name="android:paddingTop">6dip</item>
+ <item name="android:paddingBottom">6dip</item>
+ </style>
+
+ <!-- Find bar - Image Button -->
+ <style name="FindBar.ImageButton">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_marginLeft">5dip</item>
+ <item name="android:layout_marginRight">5dip</item>
+ <item name="android:scaleType">fitCenter</item>
+ <item name="android:layout_centerVertical">true</item>
+ <item name="android:background">@drawable/action_bar_button_inverse</item>
+ </style>
+
+ <style name="GeckoDialogTitle">
+ <item name="android:textAppearance">@android:style/TextAppearance.DialogWindowTitle</item>
+ </style>
+
+ <style name="GeckoDialogTitle.SubTitle" />
+
+ <style name="PopupAnimation">
+ <item name="@android:windowEnterAnimation">@anim/popup_show</item>
+ <item name="@android:windowExitAnimation">@anim/popup_hide</item>
+ </style>
+
+ <style name="ToastBase">
+ <item name="android:background">@drawable/toast_background</item>
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_alignParentBottom">true</item>
+ <item name="android:layout_centerHorizontal">true</item>
+ <item name="android:layout_gravity">bottom|center_horizontal</item>
+ <item name="android:layout_marginBottom">64dp</item>
+ <item name="android:layout_marginTop">0dp</item>
+ <item name="android:orientation">horizontal</item>
+ <item name="android:clickable">true</item>
+ </style>
+
+ <style name="Toast" parent="ToastBase">
+ <item name="android:layout_marginLeft">16dp</item>
+ <item name="android:layout_marginRight">16dp</item>
+ </style>
+
+ <style name="ToastElementBase">
+ <item name="android:background">@null</item>
+ <item name="android:paddingLeft">12dp</item>
+ <item name="android:paddingRight">12dp</item>
+ <item name="android:paddingTop">11dp</item>
+ <item name="android:paddingBottom">11dp</item>
+ </style>
+
+ <style name="ToastDividerBase">
+ <item name="android:background">@color/toolbar_divider_grey</item>
+ <item name="android:layout_width">1dp</item>
+ <item name="android:layout_height">match_parent</item>
+ </style>
+
+ <style name="ToastMessageBase" parent="ToastElementBase">
+ <item name="android:textColor">@color/toast_button_text</item>
+ <item name="android:layout_width">0dp</item>
+ <item name="android:layout_weight">1</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_gravity">center_vertical</item>
+ <item name="android:ellipsize">end</item>
+ <item name="android:maxLines">1</item>
+ <item name="android:clickable">false</item>
+ <item name="android:focusable">false</item>
+ </style>
+
+ <style name="ToastButtonBase" parent="ToastElementBase">
+ <item name="android:background">@drawable/toast_button_background</item>
+ <item name="android:textColor">@color/toast_button_text</item>
+ <item name="android:minHeight">0dp</item>
+ <item name="android:minWidth">0dp</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_gravity">center_vertical</item>
+ <item name="android:drawablePadding">12dp</item>
+ <item name="android:maxWidth">160dp</item>
+ </style>
+
+ <style name="ToastDivider" parent="ToastDividerBase">
+ <item name="android:layout_marginTop">14dp</item>
+ <item name="android:layout_marginBottom">14dp</item>
+ </style>
+
+ <style name="ToastMessage" parent="ToastMessageBase">
+ <item name="android:textAppearance">?android:textAppearanceSmall</item>
+ </style>
+
+ <style name="ToastButton" parent="ToastButtonBase">
+ <item name="android:textAppearance">?android:textAppearanceSmall</item>
+ <item name="android:textStyle">bold</item>
+ </style>
+
+ <!-- Ideally, we use the same style for the action bar & action mode, but unfortunately
+ some attrs that share a purpose have different names so instead we inherit. -->
+ <style name="GeckoActionBar" parent="ThemeOverlay.AppCompat.ActionBar">
+ <item name="android:colorBackground">@color/action_bar_bg_color</item>
+ <item name="colorAccent">@color/fennec_ui_orange</item>
+ <item name="colorControlNormal">@color/toolbar_icon_grey</item>
+ </style>
+ <style name="GeckoActionBar.ActionMode">
+ <item name="android:background">@color/action_bar_bg_color</item>
+ <!-- Note: the bottom divider is drawn in code. -->
+ </style>
+
+ <style name="PreferencesActionBar" parent="Widget.AppCompat.ActionBar.Solid">
+ <item name="displayOptions">showHome|homeAsUp|showTitle</item>
+ </style>
+
+ <style name="GeckoActionBar.Title">
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:minWidth">0dp</item>
+ <item name="android:background">@android:color/transparent</item>
+ <item name="android:textAppearance">@style/TextAppearance.Medium</item>
+ <item name="android:drawableLeft">@drawable/ab_done</item>
+ <item name="android:paddingLeft">15dp</item>
+ <item name="android:paddingRight">15dp</item>
+ <item name="android:contentDescription">@string/actionbar_done</item>
+ </style>
+
+ <style name="GeckoActionBar.Button" parent="Widget.MenuItemActionBar">
+ <item name="android:padding">8dp</item>
+ </style>
+
+ <style name="GeckoActionBar.Button.MenuButton">
+ <item name="android:scaleType">center</item>
+ <item name="android:src">@drawable/menu</item>
+ <item name="android:tint">@color/toolbar_icon_grey</item>
+ <item name="android:contentDescription">@string/actionbar_menu</item>
+ <item name="android:background">@android:color/transparent</item>
+ </style>
+
+ <style name="GeckoActionBar.Buttons">
+ <item name="android:background">@android:color/transparent</item>
+ <item name="android:textColor">@color/placeholder_active_grey</item>
+ <item name="android:gravity">right</item>
+ </style>
+
+ <style name="ShareOverlayTitle">
+ <item name="android:gravity">center_horizontal</item>
+ <item name="android:paddingLeft">15dp</item>
+ <item name="android:paddingRight">15dp</item>
+ </style>
+
+ <style name="TextAppearance.ShareOverlay">
+ <item name="android:fontFamily">sans-serif</item>
+ </style>
+
+ <style name="TextAppearance.ShareOverlay.Header">
+ <item name="android:textColor">@android:color/white</item>
+ <item name="android:fontFamily">sans-serif-light</item>
+ <item name="android:textStyle">normal</item>
+ </style>
+
+ <style name="ShareOverlayRow">
+ <item name="android:minHeight">60dp</item>
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:background">@drawable/overlay_share_button_background</item>
+ <item name="android:focusableInTouchMode">false</item>
+ </style>
+
+ <style name="TabInput"></style>
+
+ <style name="TabInput.TabWidget">
+ <item name="android:divider">@drawable/divider_vertical</item>
+ <item name="android:background">@drawable/tab_indicator_background</item>
+ </style>
+
+ <style name="TabInput.Tab">
+ <item name="android:background">@drawable/tabs_strip_indicator</item>
+ <item name="android:gravity">center</item>
+ <item name="android:minHeight">@dimen/menu_item_row_height</item>
+ </style>
+
+ <style name="TextAppearance.FirstrunLight"/>
+ <style name="TextAppearance.FirstrunRegular"/>
+
+ <style name="TextAppearance.FirstrunLight.Main">
+ <item name="android:textSize">20sp</item>
+ <item name="android:textColor">@color/text_and_tabs_tray_grey</item>
+ </style>
+
+ <style name="TextAppearance.FirstrunRegular.Body">
+ <item name="android:textSize">16sp</item>
+ <item name="android:textColor">@color/placeholder_grey</item>
+ <item name="android:lineSpacingMultiplier">1.25</item>
+ </style>
+
+ <style name="TextAppearance.FirstrunRegular.Link">
+ <item name="android:textSize">16sp</item>
+ <item name="android:textColor">@color/link_blue</item>
+ </style>
+
+ <!-- Remote Tabs home panel -->
+ <style name="RemoteTabsPanelFrame">
+ <item name="android:paddingLeft">32dp</item>
+ <item name="android:paddingRight">32dp</item>
+ <item name="android:paddingTop">@dimen/home_remote_tabs_top_padding</item>
+ <item name="android:orientation">vertical</item>
+ <item name="android:layout_gravity">center_horizontal</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ </style>
+
+ <style name="RemoteTabsPanelItem">
+ <item name="android:layout_gravity">center</item>
+ <item name="android:gravity">center</item>
+ <item name="android:layout_marginBottom">16dp</item>
+ <item name="android:maxWidth">320dp</item>
+ </style>
+
+ <style name="RemoteTabsPanelItem.TextAppearance">
+ <item name="android:textColor">@color/placeholder_grey</item>
+ <item name="android:textSize">16sp</item>
+ <item name="android:lineSpacingMultiplier">1.25</item>
+ <item name="android:layout_marginLeft">8dp</item>
+ <item name="android:layout_marginRight">8dp</item>
+ </style>
+
+ <style name="RemoteTabsPanelItem.TextAppearance.Header">
+ <item name="android:textColor">@color/placeholder_active_grey</item>
+ <item name="android:textSize">20sp</item>
+ <item name="android:layout_marginBottom">8dp</item>
+ </style>
+
+ <style name="RemoteTabsPanelItem.TextAppearance.Linkified">
+ <item name="android:clickable">true</item>
+ <item name="android:focusable">true</item>
+ <item name="android:textColor">#0092DB</item>
+ </style>
+
+ <style name="RemoteTabsPanelItem.Button">
+ <item name="android:background">@drawable/remote_tabs_setup_button_background</item>
+ <item name="android:textColor">#FFFFFF</item>
+ <item name="android:textSize">20sp</item>
+ <item name="android:gravity">center</item>
+ <item name="android:paddingTop">16dp</item>
+ <item name="android:paddingBottom">16dp</item>
+ <item name="android:paddingLeft">8dp</item>
+ <item name="android:paddingRight">8dp</item>
+
+ <!-- AppCompat sets Button text to all caps so we override that here. -->
+ <item name="textAllCaps">false</item>
+ </style>
+
+ <style name="TabQueueActivity" parent="android:style/Theme.NoDisplay" />
+
+ <style name="ActivityStreamContextMenuText">
+ <item name="android:textSize">16sp</item>
+ </style>
+
+ <!-- We use this style to provide our own divider that has an inset on the left side -->
+ <style name="ActivityStreamContextMenuStyle">
+ <item name="android:listDivider">@drawable/as_contextmenu_divider</item>
+ </style>
+
+</resources>
diff --git a/mobile/android/base/resources/values/themes.xml b/mobile/android/base/resources/values/themes.xml
new file mode 100644
index 0000000000..f62a9d4547
--- /dev/null
+++ b/mobile/android/base/resources/values/themes.xml
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+
+ <!--
+ Base application theme. This could be overridden by GeckoBaseTheme
+ in other res/values-XXX/themes.xml.
+ -->
+ <style name="GeckoBase" parent="Theme.AppCompat.Light.DarkActionBar">
+ <item name="windowNoTitle">true</item>
+ <item name="windowActionBar">false</item>
+ <item name="android:windowContentOverlay">@null</item>
+ </style>
+
+ <style name="GeckoDialogBase" parent="@android:style/Theme.Dialog">
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowContentOverlay">@null</item>
+ </style>
+
+ <style name="GeckoTitleDialogBase" parent="@android:style/Theme.Dialog" />
+
+ <style name="Gecko.Preferences">
+ <item name="windowActionBar">true</item>
+ <item name="windowNoTitle">false</item>
+ <item name="actionBarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>
+ <item name="actionBarStyle">@style/PreferencesActionBar</item>
+ </style>
+
+ <!--
+ Application Theme. All customizations that are not specific
+ to a particular API level can go here.
+ -->
+ <style name="Gecko" parent="GeckoBase">
+ <!-- Default colors -->
+ <item name="android:textColorPrimary">@color/primary_text</item>
+ <item name="android:textColorSecondary">@color/secondary_text</item>
+ <item name="android:textColorTertiary">@color/tertiary_text</item>
+
+ <!-- Default inverse colors -->
+ <item name="android:textColorPrimaryInverse">@color/primary_text</item>
+ <item name="android:textColorSecondaryInverse">@color/secondary_text</item>
+ <item name="android:textColorTertiaryInverse">@color/tertiary_text</item>
+
+ <!-- Disabled colors -->
+ <item name="android:textColorPrimaryDisableOnly">@color/text_color_primary_disable_only</item>
+
+ <!-- Hint colors -->
+ <item name="android:textColorHint">@color/text_color_hint</item>
+ <item name="android:textColorHintInverse">@color/text_color_hint_inverse</item>
+
+ <!-- Highlight colors -->
+ <item name="android:textColorHighlight">@color/fennec_ui_orange</item>
+ <item name="android:textColorHighlightInverse">@color/text_color_highlight_inverse</item>
+
+ <!-- Link colors -->
+ <item name="android:textColorLink">@color/text_color_link</item>
+
+ <!-- TextAppearances -->
+ <item name="android:textAppearance">@style/TextAppearance</item>
+ <item name="android:textAppearanceInverse">@style/TextAppearance.Inverse</item>
+ <item name="android:textAppearanceLarge">@style/TextAppearance.Large</item>
+ <item name="android:textAppearanceMedium">@style/TextAppearance.Medium</item>
+ <item name="android:textAppearanceSmall">@style/TextAppearance.Small</item>
+ <item name="android:textAppearanceLargeInverse">@style/TextAppearance.Large.Inverse</item>
+ <item name="android:textAppearanceMediumInverse">@style/TextAppearance.Medium.Inverse</item>
+ <item name="android:textAppearanceSmallInverse">@style/TextAppearance.Small.Inverse</item>
+
+ <item name="colorAccent">@color/action_orange</item>
+
+ <item name="actionBarTheme">@style/GeckoActionBar</item>
+ </style>
+
+ <style name="Gecko.Dialog" parent="GeckoDialogBase"/>
+
+ <style name="Gecko.TitleDialog" parent="GeckoTitleDialogBase"/>
+
+ <!--
+ Activity based themes, dependent on API level. This theme is replaced
+ by GeckoAppBase from res/values-vXX/themes.xml on newer devices.
+ -->
+ <style name="GeckoAppBase" parent="Gecko">
+ <item name="android:buttonStyle">@style/Widget.Button</item>
+ <item name="android:dropDownItemStyle">@style/Widget.DropDownItem</item>
+ <item name="android:editTextStyle">@style/Widget.EditText</item>
+ <item name="android:textViewStyle">@style/Widget.TextView</item>
+ <item name="menuItemDefaultStyle">@style/Widget.MenuItemDefault</item>
+ </style>
+
+ <!-- All customizations that are NOT specific to a particular API-level can go here. -->
+ <style name="Gecko.App" parent="GeckoAppBase">
+ <item name="android:gridViewStyle">@style/Widget.GridView</item>
+ <item name="android:spinnerStyle">@style/Widget.Spinner</item>
+ <item name="android:windowBackground">@android:color/white</item>
+ <item name="bookmarksListViewStyle">@style/Widget.BookmarksListView</item>
+ <item name="tabGridLayoutViewStyle">@style/Widget.TabsGridLayout</item>
+ <item name="geckoMenuListViewStyle">@style/Widget.GeckoMenuListView</item>
+ <item name="homeListViewStyle">@style/Widget.HomeListView</item>
+ <item name="menuItemActionBarStyle">@style/Widget.MenuItemActionBar</item>
+ <item name="menuItemActionModeStyle">@style/GeckoActionBar.Button</item>
+ <item name="topSitesGridItemViewStyle">@style/Widget.TopSitesGridItemView</item>
+ <item name="topSitesGridViewStyle">@style/Widget.TopSitesGridView</item>
+ <item name="topSitesThumbnailViewStyle">@style/Widget.TopSitesThumbnailView</item>
+ </style>
+
+ <!-- Make an activity appear like an overlay. -->
+ <style name="OverlayActivity" parent="Gecko">
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:backgroundDimEnabled">true</item>
+
+ <!-- Set the app's title bar color in the recent app switcher.
+
+ Note: We'd prefer not to show up in the recent app switcher (bug 1137928). -->
+ <item name="android:colorPrimary">@color/text_and_tabs_tray_grey</item>
+ <!-- We display the overlay on top of other Activities so show their status bar. -->
+ <item name="android:statusBarColor">@android:color/transparent</item>
+ </style>
+
+</resources>
diff --git a/mobile/android/base/resources/values/vpi__attrs.xml b/mobile/android/base/resources/values/vpi__attrs.xml
new file mode 100644
index 0000000000..ffe895f222
--- /dev/null
+++ b/mobile/android/base/resources/values/vpi__attrs.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 Jake Wharton
+ Copyright (C) 2011 Patrik Ã…kerfeldt
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <declare-styleable name="ViewPagerIndicator">
+ <!-- Style of the circle indicator. -->
+ <attr name="vpiCirclePageIndicatorStyle" format="reference"/>
+ <!-- Style of the icon indicator's views. -->
+ <attr name="vpiIconPageIndicatorStyle" format="reference"/>
+ <!-- Style of the line indicator. -->
+ <attr name="vpiLinePageIndicatorStyle" format="reference"/>
+ <!-- Style of the title indicator. -->
+ <attr name="vpiTitlePageIndicatorStyle" format="reference"/>
+ <!-- Style of the tab indicator's tabs. -->
+ <attr name="vpiTabPageIndicatorStyle" format="reference"/>
+ <!-- Style of the underline indicator. -->
+ <attr name="vpiUnderlinePageIndicatorStyle" format="reference"/>
+ </declare-styleable>
+
+ <attr name="centered" format="boolean" />
+ <attr name="selectedColor" format="color" />
+ <attr name="strokeWidth" format="dimension" />
+ <attr name="unselectedColor" format="color" />
+
+ <declare-styleable name="CirclePageIndicator">
+ <!-- Whether or not the indicators should be centered. -->
+ <attr name="centered" />
+ <!-- Color of the filled circle that represents the current page. -->
+ <attr name="fillColor" format="color" />
+ <!-- Color of the filled circles that represents pages. -->
+ <attr name="pageColor" format="color" />
+ <!-- Orientation of the indicator. -->
+ <attr name="android:orientation"/>
+ <!-- Radius of the circles. This is also the spacing between circles. -->
+ <attr name="radius" format="dimension" />
+ <!-- Whether or not the selected indicator snaps to the circles. -->
+ <attr name="snap" format="boolean" />
+ <!-- Color of the open circles. -->
+ <attr name="strokeColor" format="color" />
+ <!-- Width of the stroke used to draw the circles. -->
+ <attr name="strokeWidth" />
+ <!-- View background -->
+ <attr name="android:background"/>
+ </declare-styleable>
+</resources>
diff --git a/mobile/android/base/resources/values/vpi__defaults.xml b/mobile/android/base/resources/values/vpi__defaults.xml
new file mode 100644
index 0000000000..f902f48a86
--- /dev/null
+++ b/mobile/android/base/resources/values/vpi__defaults.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+
+<resources>
+ <bool name="default_circle_indicator_centered">true</bool>
+ <color name="default_circle_indicator_fill_color">#FFFFFFFF</color>
+ <color name="default_circle_indicator_page_color">#00000000</color>
+ <integer name="default_circle_indicator_orientation">0</integer>
+ <dimen name="default_circle_indicator_radius">3dp</dimen>
+ <bool name="default_circle_indicator_snap">false</bool>
+ <color name="default_circle_indicator_stroke_color">#FFDDDDDD</color>
+ <dimen name="default_circle_indicator_stroke_width">1dp</dimen>
+</resources> \ No newline at end of file
diff --git a/mobile/android/base/resources/xml-v11/preference_headers.xml b/mobile/android/base/resources/xml-v11/preference_headers.xml
new file mode 100644
index 0000000000..dedd908066
--- /dev/null
+++ b/mobile/android/base/resources/xml-v11/preference_headers.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- Tablet only: Sync is nested within the "General" header on tablets,
+ instead of being a top-level menu item.
+ See xml-v11/preferences.xml for single-pane v11+ phone layout. -->
+
+<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <header android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment"
+ android:title="@string/pref_header_general"
+ android:id="@+id/pref_header_general">
+ <extra android:name="resource"
+ android:value="preferences_general_tablet"/>
+ </header>
+
+ <header android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment"
+ android:title="@string/pref_header_search"
+ android:id="@+id/pref_header_search">
+ <extra android:name="resource"
+ android:value="preferences_search"/>
+ </header>
+
+ <header android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment"
+ android:title="@string/pref_header_privacy_short"
+ android:id="@+id/pref_header_privacy">
+ <extra android:name="resource"
+ android:value="preferences_privacy"/>
+ </header>
+
+ <header android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment"
+ android:title="@string/pref_header_accessibility"
+ android:id="@+id/pref_header_accessibility">
+ <extra android:name="resource"
+ android:value="preferences_accessibility"/>
+ </header>
+
+ <header android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment"
+ android:title="@string/pref_header_notifications"
+ android:id="@+id/pref_header_notifications">
+ <extra android:name="resource"
+ android:value="preferences_notifications"/>
+ </header>
+
+ <header android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment"
+ android:title="@string/pref_header_advanced"
+ android:id="@+id/pref_header_advanced">
+ <extra android:name="resource"
+ android:value="preferences_advanced"/>
+ </header>
+
+ <header android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment"
+ android:title="@string/pref_clear_private_data_now"
+ android:id="@+id/pref_header_clear_private_data">
+ <extra android:name="resource"
+ android:value="preferences_privacy_clear_tablet"/>
+ </header>
+
+ <header android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment"
+ android:title="@string/pref_default_browser"
+ android:id="@+id/pref_header_default_browser">
+ <extra android:name="resource"
+ android:value="preferences_default_browser_tablet"/>
+ </header>
+
+ <header android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment"
+ android:title="@string/pref_header_vendor">
+ <extra android:name="resource"
+ android:value="preferences_vendor"/>
+ </header>
+
+</preference-headers>
diff --git a/mobile/android/base/resources/xml-v11/preferences_default_browser_tablet.xml b/mobile/android/base/resources/xml-v11/preferences_default_browser_tablet.xml
new file mode 100644
index 0000000000..53c2c2b565
--- /dev/null
+++ b/mobile/android/base/resources/xml-v11/preferences_default_browser_tablet.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+- License, v. 2.0. If a copy of the MPL was not distributed with this
+- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+ <org.mozilla.gecko.preferences.LinkPreference android:key="android.not_a_preference.default_browser.link"
+ android:title="@string/pref_default_browser_mozilla_support_tablet"
+ android:persistent="false"
+ url="https://support.mozilla.org/kb/make-firefox-default-browser-android?utm_source=inproduct&amp;utm_medium=settings&amp;utm_campaign=mobileandroid"/>
+</PreferenceScreen> \ No newline at end of file
diff --git a/mobile/android/base/resources/xml-v11/preferences_privacy_clear_tablet.xml b/mobile/android/base/resources/xml-v11/preferences_privacy_clear_tablet.xml
new file mode 100644
index 0000000000..49cc895c4c
--- /dev/null
+++ b/mobile/android/base/resources/xml-v11/preferences_privacy_clear_tablet.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+- License, v. 2.0. If a copy of the MPL was not distributed with this
+- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto">
+ <org.mozilla.gecko.preferences.PrivateDataPreference
+ android:key="android.not_a_preference.privacy.clear"
+ android:title="@string/pref_clear_private_data_now_tablet"
+ android:persistent="true"
+ android:positiveButtonText="@string/button_clear_data"
+ gecko:entries="@array/pref_private_data_entries"
+ gecko:entryValues="@array/pref_private_data_values"
+ gecko:entryKeys="@array/pref_private_data_keys"
+ gecko:initialValues="@array/pref_private_data_defaults" />
+</PreferenceScreen>
diff --git a/mobile/android/base/resources/xml-v11/preferences_search.xml b/mobile/android/base/resources/xml-v11/preferences_search.xml
new file mode 100644
index 0000000000..937b05b617
--- /dev/null
+++ b/mobile/android/base/resources/xml-v11/preferences_search.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto"
+ android:title="@string/pref_category_search"
+ android:enabled="false">
+
+ <PreferenceCategory android:title="@string/pref_category_add_search_providers">
+
+ <org.mozilla.gecko.preferences.ModifiableHintPreference
+ android:layout="@layout/preference_search_tip"
+ android:enabled="false"
+ android:selectable="false"/>
+
+ </PreferenceCategory>
+
+ <org.mozilla.gecko.preferences.SearchPreferenceCategory
+ android:title="@string/pref_category_installed_search_engines"/>
+
+ <CheckBoxPreference android:key="browser.search.suggest.enabled"
+ android:title="@string/pref_search_suggestions"
+ android:defaultValue="false"
+ android:persistent="false" />
+
+ <CheckBoxPreference android:key="android.not_a_preference.search.search_history.enabled"
+ android:title="@string/pref_history_search_suggestions"
+ android:defaultValue="true"
+ android:persistent="true" />
+
+</PreferenceScreen>
diff --git a/mobile/android/base/resources/xml/preference_headers.xml b/mobile/android/base/resources/xml/preference_headers.xml
new file mode 100644
index 0000000000..e17c9bd77b
--- /dev/null
+++ b/mobile/android/base/resources/xml/preference_headers.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- This file is a stub to allow IDs to be used in code
+ even for a version-limited build. -->
+
+<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
+ <header android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment"
+ android:id="@+id/pref_header_search">
+ </header>
+ <header android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment"
+ android:id="@+id/pref_header_notifications">
+ </header>
+ <header android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment"
+ android:id="@+id/pref_header_advanced">
+ </header>
+ <header android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment"
+ android:id="@+id/pref_header_accessibility">
+ </header>
+ <header android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment"
+ android:id="@+id/pref_header_clear_private_data">
+ </header>
+</preference-headers>
diff --git a/mobile/android/base/resources/xml/preferences.xml b/mobile/android/base/resources/xml/preferences.xml
new file mode 100644
index 0000000000..06716cd9af
--- /dev/null
+++ b/mobile/android/base/resources/xml/preferences.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- See xml-v11/preference_headers.xml for tablet layout. -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto"
+ android:enabled="false">
+
+ <org.mozilla.gecko.preferences.SyncPreference android:key="android.not_a_preference.sync"
+ android:title="@string/pref_sync"
+ android:icon="@drawable/sync_avatar_default"
+ android:summary="@string/pref_sync_summary"
+ android:persistent="false" />
+
+ <PreferenceScreen android:title="@string/pref_category_general"
+ android:summary="@string/pref_category_general_summary"
+ android:key="android.not_a_preference.general_screen"
+ android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment" >
+ <extra android:name="resource"
+ android:value="preferences_general"/>
+ </PreferenceScreen>
+
+ <PreferenceScreen android:title="@string/pref_category_search"
+ android:summary="@string/pref_category_search_summary"
+ android:key="android.not_a_preference.search_screen"
+ android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment" >
+ <extra android:name="resource"
+ android:value="preferences_search"/>
+ </PreferenceScreen>
+
+ <PreferenceScreen android:title="@string/pref_category_privacy_short"
+ android:summary="@string/pref_category_privacy_summary"
+ android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment" >
+ <extra android:name="resource"
+ android:value="preferences_privacy" />
+ </PreferenceScreen>
+
+ <PreferenceScreen android:title="@string/pref_category_accessibility"
+ android:summary="@string/pref_category_accessibility_summary"
+ android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment" >
+ <extra android:name="resource"
+ android:value="preferences_accessibility" />
+ </PreferenceScreen>
+
+ <PreferenceScreen android:title="@string/pref_category_notifications"
+ android:summary="@string/pref_category_notifications_summary"
+ android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment">
+ <extra android:name="resource"
+ android:value="preferences_notifications"/>
+ </PreferenceScreen>
+
+ <PreferenceScreen android:title="@string/pref_category_advanced"
+ android:summary="@string/pref_category_advanced_summary"
+ android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment"
+ android:key="android.not_a_preference.advanced_screen" >
+ <extra android:name="resource"
+ android:value="preferences_advanced"/>
+ </PreferenceScreen>
+
+ <org.mozilla.gecko.preferences.PrivateDataPreference
+ android:key="android.not_a_preference.privacy.clear"
+ android:title="@string/pref_clear_private_data_now"
+ android:persistent="true"
+ android:positiveButtonText="@string/button_clear_data"
+ gecko:entries="@array/pref_private_data_entries"
+ gecko:entryValues="@array/pref_private_data_values"
+ gecko:entryKeys="@array/pref_private_data_keys"
+ gecko:initialValues="@array/pref_private_data_defaults" />
+
+ <org.mozilla.gecko.preferences.LinkPreference android:key="android.not_a_preference.default_browser.link"
+ android:title="@string/pref_default_browser"
+ android:persistent="false"
+ url="https://support.mozilla.org/kb/make-firefox-default-browser-android?utm_source=inproduct&amp;utm_medium=settings&amp;utm_campaign=mobileandroid"/>
+
+ <PreferenceScreen android:title="@string/pref_category_vendor"
+ android:summary="@string/pref_category_vendor_summary"
+ android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment" >
+ <extra android:name="resource"
+ android:value="preferences_vendor"/>
+ </PreferenceScreen>
+
+</PreferenceScreen>
diff --git a/mobile/android/base/resources/xml/preferences_accessibility.xml b/mobile/android/base/resources/xml/preferences_accessibility.xml
new file mode 100644
index 0000000000..8e352f1fd8
--- /dev/null
+++ b/mobile/android/base/resources/xml/preferences_accessibility.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ android:title="@string/pref_category_accessibility"
+ android:enabled="false">
+
+ <org.mozilla.gecko.preferences.FontSizePreference
+ android:key="font.size.inflation.minTwips"
+ android:title="@string/pref_text_size"
+ android:positiveButtonText="@string/pref_font_size_set"
+ android:negativeButtonText="@string/button_cancel"
+ android:persistent="false" />
+
+ <SwitchPreference android:key="browser.ui.zoom.force-user-scalable"
+ android:title="@string/pref_zoom_force_enabled"
+ android:summary="@string/pref_zoom_force_enabled_summary" />
+
+ <SwitchPreference android:key="ui.zoomedview.enabled"
+ android:title="@string/pref_magnifying_glass_enabled"
+ android:summary="@string/pref_magnifying_glass_enabled_summary" />
+
+ <SwitchPreference android:key="android.not_a_preference.voice_input_enabled"
+ android:title="@string/pref_voice_input"
+ android:summary="@string/pref_voice_input_summary"
+ android:defaultValue="true"/>
+
+ <SwitchPreference android:key="android.not_a_preference.qrcode_enabled"
+ android:title="@string/pref_qrcode_enabled"
+ android:summary="@string/pref_qrcode_enabled_summary"
+ android:defaultValue="true"/>
+
+</PreferenceScreen>
diff --git a/mobile/android/base/resources/xml/preferences_advanced.xml b/mobile/android/base/resources/xml/preferences_advanced.xml
new file mode 100644
index 0000000000..32cdf0b915
--- /dev/null
+++ b/mobile/android/base/resources/xml/preferences_advanced.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto"
+ android:title="@string/pref_category_advanced"
+ android:enabled="false">
+
+ <org.mozilla.gecko.preferences.AndroidImportPreference
+ android:key="android.not_a_preference.import_android"
+ gecko:entries="@array/pref_import_android_entries"
+ gecko:entryValues="@array/pref_import_android_values"
+ gecko:initialValues="@array/pref_import_android_defaults"
+ android:title="@string/pref_import_android"
+ android:summary="@string/pref_import_android_summary"
+ android:positiveButtonText="@string/bookmarkhistory_button_import"
+ android:negativeButtonText="@string/button_cancel"
+ android:persistent="false" />
+
+ <ListPreference android:key="app.update.autodownload"
+ android:title="@string/pref_update_autodownload"
+ android:entries="@array/pref_update_autodownload_entries"
+ android:entryValues="@array/pref_update_autodownload_values"
+ android:persistent="false" />
+
+ <ListPreference android:key="android.not_a_preference.restoreSession3"
+ android:title="@string/pref_restore"
+ android:defaultValue="always"
+ android:entries="@array/pref_restore_entries"
+ android:entryValues="@array/pref_restore_values"
+ android:persistent="true" />
+
+ <ListPreference android:key="browser.menu.showCharacterEncoding"
+ android:title="@string/pref_char_encoding"
+ android:entries="@array/pref_char_encoding_entries"
+ android:entryValues="@array/pref_char_encoding_values"
+ android:persistent="false" />
+
+ <PreferenceCategory android:title="@string/pref_category_data_saver">
+
+ <ListPreference android:key="browser.image_blocking"
+ android:title="@string/pref_tap_to_load_images_title2"
+ android:entries="@array/pref_browser_image_blocking_entries"
+ android:entryValues="@array/pref_browser_image_blocking_values"
+ android:persistent="false" />
+
+ <SwitchPreference android:key="browser.display.use_document_fonts"
+ android:title="@string/pref_show_web_fonts"
+ android:summary="@string/pref_show_web_fonts_summary"/>
+
+ </PreferenceCategory>
+
+ <PreferenceCategory android:title="@string/pref_category_media">
+
+ <ListPreference android:key="plugin.enable"
+ android:title="@string/pref_plugins"
+ android:entries="@array/pref_plugins_entries"
+ android:entryValues="@array/pref_plugins_values"
+ android:persistent="false" />
+
+ <SwitchPreference android:key="media.autoplay.enabled"
+ android:title="@string/pref_media_autoplay_enabled"
+ android:summary="@string/pref_media_autoplay_enabled_summary" />
+
+ </PreferenceCategory>
+
+ <PreferenceCategory android:title="@string/pref_category_developer_tools">
+
+ <SwitchPreference android:key="devtools.remote.usb.enabled"
+ android:title="@string/pref_developer_remotedebugging_usb" />
+
+ <SwitchPreference android:key="devtools.remote.wifi.enabled"
+ android:title="@string/pref_developer_remotedebugging_wifi" />
+
+ <org.mozilla.gecko.preferences.AlignRightLinkPreference android:key="android.not_a_preference.remote_debugging.link"
+ android:title="@string/pref_learn_more"
+ android:persistent="false"
+ url="https://developer.mozilla.org/docs/Tools/Remote_Debugging/Debugging_Firefox_for_Android_with_WebIDE" />
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:key="android.not_a_preference.category_experimental"
+ android:title="@string/pref_category_experimental">
+
+ <SwitchPreference android:key="android.not_a_preference.activitystream"
+ android:title="@string/pref_activity_stream"
+ android:summary="@string/pref_activity_stream_summary"
+ android:defaultValue="false" />
+
+
+ <SwitchPreference android:key="android.not_a_preference.customtabs"
+ android:title="@string/pref_custom_tabs"
+ android:summary="@string/pref_custom_tabs_summary"
+ android:defaultValue="false" />
+
+ </PreferenceCategory>
+
+</PreferenceScreen>
diff --git a/mobile/android/base/resources/xml/preferences_general.xml b/mobile/android/base/resources/xml/preferences_general.xml
new file mode 100644
index 0000000000..f50bceb554
--- /dev/null
+++ b/mobile/android/base/resources/xml/preferences_general.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- Changes should be mirrored to preferences_general_tablet.xml. -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto"
+ android:enabled="false">
+
+ <PreferenceScreen android:key="android.not_a_preference.general_home"
+ android:title="@string/pref_category_home"
+ android:summary="@string/pref_category_home_summary"
+ android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment" >
+ <extra android:name="resource"
+ android:value="preferences_home" />
+ </PreferenceScreen>
+
+ <PreferenceScreen android:title="@string/pref_category_language"
+ android:summary="@string/pref_category_language_summary"
+ android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment" >
+ <extra android:name="resource"
+ android:value="preferences_locale" />
+ </PreferenceScreen>
+
+ <SwitchPreference android:key="browser.chrome.dynamictoolbar"
+ android:title="@string/pref_scroll_title_bar2"
+ android:summary="@string/pref_scroll_title_bar_summary" />
+
+ <SwitchPreference android:key="android.not_a_preference.tab_queue"
+ android:title="@string/pref_tab_queue_title"
+ android:summary="@string/pref_tab_queue_summary"
+ android:defaultValue="false" />
+
+</PreferenceScreen>
+
diff --git a/mobile/android/base/resources/xml/preferences_general_tablet.xml b/mobile/android/base/resources/xml/preferences_general_tablet.xml
new file mode 100644
index 0000000000..f05be98277
--- /dev/null
+++ b/mobile/android/base/resources/xml/preferences_general_tablet.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- Tablet only: The contents under the "General" header for tablets,
+ See xml-v11/preferences.xml for single-pane v11+ phone layout.
+ Changes to preferences should be mirrored to preferences_general.xml. -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ android:title="@string/pref_category_general"
+ android:enabled="false">
+
+ <org.mozilla.gecko.preferences.SyncPreference android:key="android.not_a_preference.sync"
+ android:title="@string/pref_sync"
+ android:persistent="false" />
+
+ <PreferenceScreen android:key="android.not_a_preference.general_home"
+ android:title="@string/pref_category_home"
+ android:summary="@string/pref_category_home_summary"
+ android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment" >
+ <extra android:name="resource"
+ android:value="preferences_home" />
+ </PreferenceScreen>
+
+ <PreferenceScreen android:title="@string/pref_category_language"
+ android:summary="@string/pref_category_language_summary"
+ android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment" >
+ <extra android:name="resource"
+ android:value="preferences_locale" />
+ </PreferenceScreen>
+
+ <SwitchPreference android:key="browser.chrome.dynamictoolbar"
+ android:title="@string/pref_scroll_title_bar2"
+ android:summary="@string/pref_scroll_title_bar_summary" />
+
+ <SwitchPreference android:key="android.not_a_preference.tab_queue"
+ android:title="@string/pref_tab_queue_title"
+ android:summary="@string/pref_tab_queue_summary"
+ android:defaultValue="false" />
+
+</PreferenceScreen>
+
diff --git a/mobile/android/base/resources/xml/preferences_home.xml b/mobile/android/base/resources/xml/preferences_home.xml
new file mode 100644
index 0000000000..ecc90e093a
--- /dev/null
+++ b/mobile/android/base/resources/xml/preferences_home.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ android:title="@string/pref_category_home"
+ android:enabled="false">
+
+ <PreferenceCategory android:title="@string/pref_category_home_homepage"
+ android:key="android.not_a_preference.category_homepage">
+
+ <org.mozilla.gecko.preferences.SetHomepagePreference
+ android:key="android.not_a_preference.homepage"
+ android:title="@string/home_homepage_title"
+ android:persistent="false"
+ android:dialogLayout="@layout/preference_set_homepage"/>
+
+ </PreferenceCategory>
+
+ <org.mozilla.gecko.preferences.PanelsPreferenceCategory
+ android:title="@string/pref_category_home_panels"/>
+
+ <PreferenceCategory android:title="@string/pref_category_home_add_ons">
+
+ <ListPreference android:key="home.sync.updateMode"
+ android:title="@string/pref_home_updates"
+ android:entries="@array/pref_home_updates_entries"
+ android:entryValues="@array/pref_home_updates_values"
+ android:persistent="false" />
+
+ </PreferenceCategory>
+
+</PreferenceScreen>
diff --git a/mobile/android/base/resources/xml/preferences_locale.xml b/mobile/android/base/resources/xml/preferences_locale.xml
new file mode 100644
index 0000000000..80173c8e0c
--- /dev/null
+++ b/mobile/android/base/resources/xml/preferences_locale.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto"
+ android:title="@string/pref_category_language"
+ android:enabled="false">
+ <PreferenceCategory android:title="@string/pref_browser_locale">
+ <!-- No title set here. We set the title to the current locale in
+ GeckoPreferences. -->
+ <org.mozilla.gecko.preferences.LocaleListPreference
+ android:key="locale"
+ android:persistent="true"
+ android:defaultValue=""
+ />
+ </PreferenceCategory>
+</PreferenceScreen>
diff --git a/mobile/android/base/resources/xml/preferences_notifications.xml b/mobile/android/base/resources/xml/preferences_notifications.xml
new file mode 100644
index 0000000000..b9080d1100
--- /dev/null
+++ b/mobile/android/base/resources/xml/preferences_notifications.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+ <SwitchPreference android:key="android.not_a_preference.notifications.content"
+ android:title="@string/pref_content_notifications"
+ android:summary="@string/pref_content_notifications_summary"
+ android:defaultValue="true" />
+ <org.mozilla.gecko.preferences.AlignRightLinkPreference
+ android:key="android.not_a_preference.notifications.content.learn_more"
+ android:title="@string/pref_learn_more"
+ android:persistent="false"
+ url="https://support.mozilla.org/kb/notifications-firefox-android?utm_source=inproduct&amp;utm_medium=notifications&amp;utm_campaign=mobileandroid" />
+ <SwitchPreference android:key="android.not_a_preference.notifications.whats_new"
+ android:title="@string/pref_whats_new_notification"
+ android:summary="@string/pref_whats_new_notification_summary"
+ android:defaultValue="true" />
+</PreferenceScreen> \ No newline at end of file
diff --git a/mobile/android/base/resources/xml/preferences_privacy.xml b/mobile/android/base/resources/xml/preferences_privacy.xml
new file mode 100644
index 0000000000..7b3215cb26
--- /dev/null
+++ b/mobile/android/base/resources/xml/preferences_privacy.xml
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto"
+ android:title="@string/pref_category_privacy_short"
+ android:enabled="false">
+
+ <SwitchPreference android:key="privacy.donottrackheader.enabled"
+ android:title="@string/pref_donottrack_title"
+ android:summary="@string/pref_donottrack_summary"
+ android:persistent="false" />
+
+ <org.mozilla.gecko.preferences.AlignRightLinkPreference
+ android:key="android.not_a_preference.donottrackheader.learn_more"
+ android:title="@string/pref_learn_more"
+ android:persistent="false"
+ url="https://www.mozilla.org/firefox/dnt/" />
+
+ <CheckBoxPreference android:key="privacy.trackingprotection.pbmode.enabled"
+ android:title="@string/pref_tracking_protection_title"
+ android:summary="@string/pref_tracking_protection_summary"
+ android:persistent="false" />
+
+ <ListPreference android:key="privacy.trackingprotection.state"
+ android:title="@string/pref_tracking_protection_title"
+ android:entries="@array/pref_tracking_protection_entries"
+ android:entryValues="@array/pref_tracking_protection_values"
+ android:persistent="false" />
+
+ <org.mozilla.gecko.preferences.AlignRightLinkPreference
+ android:key="android.not_a_preference.trackingprotection.learn_more"
+ android:title="@string/pref_learn_more"
+ android:persistent="false"
+ url="https://support.mozilla.org/kb/firefox-android-tracking-protection" />
+
+ <ListPreference android:key="network.cookie.cookieBehavior"
+ android:title="@string/pref_cookies_menu"
+ android:entries="@array/pref_cookies_entries"
+ android:entryValues="@array/pref_cookies_values"
+ android:persistent="false" />
+
+
+ <!-- This pref is persisted in both Gecko and Java -->
+ <org.mozilla.gecko.preferences.ListCheckboxPreference
+ android:key="android.not_a_preference.history.clear_on_exit"
+ gecko:entries="@array/pref_private_data_entries"
+ gecko:entryValues="@array/pref_private_data_values"
+ gecko:initialValues="@array/pref_clear_on_exit_defaults"
+
+ android:title="@string/pref_clear_on_exit_title"
+ android:summary="@string/pref_clear_on_exit_summary2"
+
+ android:dialogTitle="@string/pref_clear_on_exit_dialog_title"
+ android:positiveButtonText="@string/button_set"/>
+
+ <PreferenceCategory android:title="@string/pref_category_logins">
+
+ <org.mozilla.gecko.preferences.LinkPreference
+ android:key="android.not_a_preference.signon.manage"
+ android:title="@string/pref_manage_logins"
+ url="about:logins"/>
+
+ <CheckBoxPreference
+ android:key="signon.rememberSignons"
+ android:title="@string/pref_remember_signons"
+ android:persistent="false" />
+
+ <CheckBoxPreference
+ android:key="privacy.masterpassword.enabled"
+ android:title="@string/pref_use_master_password"
+ android:persistent="false" />
+
+ </PreferenceCategory>
+
+ <PreferenceCategory android:key="android.not_a_preference.datareporting.preferences"
+ android:title="@string/pref_category_datareporting">
+
+ <CheckBoxPreference android:key="toolkit.telemetry.enabled"
+ android:title="@string/datareporting_telemetry_title"
+ android:summary="@string/datareporting_telemetry_summary" />
+
+ <CheckBoxPreference android:key="datareporting.crashreporter.submitEnabled"
+ android:title="@string/datareporting_crashreporter_title_short"
+ android:summary="@string/datareporting_crashreporter_summary"
+ android:defaultValue="false" />
+
+ <CheckBoxPreference android:key="android.not_a_preference.app.geo.reportdata"
+ android:title="@string/datareporting_wifi_title"
+ android:summary="@string/datareporting_wifi_geolocation_summary" />
+
+ <org.mozilla.gecko.preferences.AlignRightLinkPreference android:key="android.not_a_preference.geo.learn_more"
+ android:title="@string/pref_learn_more"
+ android:persistent="false"
+ url="https://location.services.mozilla.com/" />
+
+ <CheckBoxPreference android:key="android.not_a_preference.healthreport.uploadEnabled"
+ android:title="@string/datareporting_fhr_title"
+ android:summary="@string/datareporting_fhr_summary2"
+ android:defaultValue="true" />
+
+ <org.mozilla.gecko.preferences.AlignRightLinkPreference android:key="android.not_a_preference.healthreport.link"
+ android:title="@string/datareporting_abouthr_title"
+ android:persistent="false"
+ url="about:healthreport" />
+
+ </PreferenceCategory>
+
+</PreferenceScreen>
+
+
diff --git a/mobile/android/base/resources/xml/preferences_search.xml b/mobile/android/base/resources/xml/preferences_search.xml
new file mode 100644
index 0000000000..440167fe5e
--- /dev/null
+++ b/mobile/android/base/resources/xml/preferences_search.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto"
+ android:title="@string/pref_category_search"
+ android:enabled="false">
+
+ <PreferenceCategory android:title="@string/pref_category_add_search_providers">
+
+ <org.mozilla.gecko.preferences.ModifiableHintPreference
+ android:layout="@layout/preference_search_tip"
+ android:enabled="false"
+ android:selectable="false"/>
+
+ </PreferenceCategory>
+
+ <org.mozilla.gecko.preferences.SearchPreferenceCategory
+ android:title="@string/pref_category_installed_search_engines"/>
+
+ <PreferenceCategory android:title="@string/pref_category_search_restore_defaults">
+
+ <Preference android:key="android.not_a_preference.search.restore_defaults"
+ android:title="@string/pref_search_restore_defaults_summary" />
+
+ </PreferenceCategory>
+
+ <CheckBoxPreference android:key="browser.search.suggest.enabled"
+ android:title="@string/pref_search_suggestions"
+ android:defaultValue="false"
+ android:persistent="false" />
+
+ <CheckBoxPreference android:key="android.not_a_preference.search.search_history.enabled"
+ android:title="@string/pref_history_search_suggestions"
+ android:defaultValue="true"
+ android:persistent="true" />
+
+</PreferenceScreen>
diff --git a/mobile/android/base/resources/xml/preferences_vendor.xml b/mobile/android/base/resources/xml/preferences_vendor.xml
new file mode 100644
index 0000000000..38eba9d303
--- /dev/null
+++ b/mobile/android/base/resources/xml/preferences_vendor.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto"
+ android:title="@string/pref_category_vendor"
+ android:enabled="false">
+
+ <org.mozilla.gecko.preferences.LinkPreference android:key="android.not_a_preference.about.link"
+ android:title="@string/pref_about_firefox"
+ android:persistent="false"
+ url="about:" />
+
+ <org.mozilla.gecko.preferences.LinkPreference android:key="android.not_a_preference.faq.link"
+ android:title="@string/pref_vendor_faqs"
+ android:persistent="false"/>
+
+ <org.mozilla.gecko.preferences.LinkPreference android:key="android.not_a_preference.feedback.link"
+ android:title="@string/pref_vendor_feedback"
+ android:persistent="false"/>
+
+</PreferenceScreen>
diff --git a/mobile/android/base/resources/xml/search_preferences.xml b/mobile/android/base/resources/xml/search_preferences.xml
new file mode 100644
index 0000000000..510e3f4250
--- /dev/null
+++ b/mobile/android/base/resources/xml/search_preferences.xml
@@ -0,0 +1,9 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+ <Preference
+ android:key="search.not_a_preference.clear_history"
+ android:title="@string/pref_clearHistory_title"/>
+</PreferenceScreen>
diff --git a/mobile/android/base/resources/xml/search_widget_info.xml b/mobile/android/base/resources/xml/search_widget_info.xml
new file mode 100644
index 0000000000..7fd66f7ed5
--- /dev/null
+++ b/mobile/android/base/resources/xml/search_widget_info.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+ android:minWidth="250dp"
+ android:minHeight="40dp"
+ android:label="@string/search_widget_name"
+ android:widgetCategory="home_screen"
+ android:previewImage="@drawable/launcher_widget"
+ android:initialLayout="@layout/search_widget"/>
diff --git a/mobile/android/base/resources/xml/searchable.xml b/mobile/android/base/resources/xml/searchable.xml
new file mode 100644
index 0000000000..3cd7333bd5
--- /dev/null
+++ b/mobile/android/base/resources/xml/searchable.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<searchable xmlns:android="http://schemas.android.com/apk/res/android"
+ android:label="@string/moz_app_displayname"
+ android:searchSuggestAuthority="@string/content_authority_db_browser"
+ android:searchSuggestIntentAction="android.intent.action.SEARCH"
+ android:searchSettingsDescription="@string/searchable_description"
+ android:includeInGlobalSearch="true"/>
diff --git a/mobile/android/base/strings.xml.in b/mobile/android/base/strings.xml.in
new file mode 100644
index 0000000000..3511a4eca6
--- /dev/null
+++ b/mobile/android/base/strings.xml.in
@@ -0,0 +1,639 @@
+#filter substitution
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE resources [
+#includesubst @BRANDPATH@
+#includesubst @STRINGSPATH@
+#includesubst @SYNCSTRINGSPATH@
+#includesubst @SEARCHSTRINGSPATH@
+
+<!-- C-style format strings. -->
+<!ENTITY formatI "&#037;I">
+<!ENTITY formatS "&#037;s">
+<!ENTITY formatS1 "&#037;1&#036;s">
+<!ENTITY formatS2 "&#037;2&#036;s">
+<!ENTITY formatS3 "&#037;3&#036;s">
+<!ENTITY formatD "&#037;d">
+]>
+
+<resources>
+ <string name="moz_app_displayname">@MOZ_APP_DISPLAYNAME@</string>
+ <string name="content_authority_db_browser">@ANDROID_PACKAGE_NAME@.db.browser</string>
+ <string name="moz_android_shared_fxaccount_type">@ANDROID_PACKAGE_NAME@_fxaccount</string>
+ <string name="android_package_name_for_ui">@ANDROID_PACKAGE_NAME@</string>
+
+#include ../search/strings/search_strings.xml.in
+
+#include ../services/strings.xml.in
+
+ <string name="firstrun_panel_title_welcome">&firstrun_panel_title_welcome;</string>
+
+ <string name="firstrun_urlbar_message">&firstrun_urlbar_message;</string>
+ <string name="firstrun_urlbar_subtext">&firstrun_urlbar_subtext;</string>
+ <string name="firstrun_bookmarks_title">&firstrun_bookmarks_title;</string>
+ <string name="firstrun_bookmarks_message">&firstrun_bookmarks_message;</string>
+ <string name="firstrun_bookmarks_subtext">&firstrun_bookmarks_subtext;</string>
+ <string name="firstrun_data_title">&firstrun_data_title;</string>
+ <string name="firstrun_data_message">&firstrun_data_message;</string>
+ <string name="firstrun_data_subtext">&firstrun_data_subtext2;</string>
+ <string name="firstrun_sync_title">&firstrun_sync_title;</string>
+ <string name="firstrun_sync_message">&firstrun_sync_message;</string>
+ <string name="firstrun_sync_subtext">&firstrun_sync_subtext;</string>
+ <string name="firstrun_signin_message">&firstrun_signin_message;</string>
+ <string name="firstrun_signin_button">&firstrun_signin_button;</string>
+ <string name="firstrun_welcome_button_browser">&onboard_start_button_browser;</string>
+ <string name="firstrun_button_notnow">&firstrun_button_notnow;</string>
+ <string name="firstrun_button_next">&firstrun_button_next;</string>
+
+ <string name="firstrun_tabqueue_title">&firstrun_tabqueue_title;</string>
+ <string name="firstrun_tabqueue_message_off">&firstrun_tabqueue_message_off;</string>
+ <string name="firstrun_tabqueue_subtext_off">&firstrun_tabqueue_subtext_off;</string>
+ <string name="firstrun_tabqueue_message_on">&firstrun_tabqueue_message_on;</string>
+ <string name="firstrun_tabqueue_subtext_on">&firstrun_tabqueue_subtext_on;</string>
+
+ <string name="firstrun_readerview_title">&firstrun_readerview_title;</string>
+ <string name="firstrun_readerview_message">&firstrun_readerview_message;</string>
+ <string name="firstrun_readerview_subtext">&firstrun_readerview_subtext;</string>
+
+ <string name="firstrun_account_title">&firstrun_account_title;</string>
+ <string name="firstrun_account_message">&firstrun_account_message;</string>
+
+ <string name="firstrun_welcome_restricted">&onboard_start_restricted1;</string>
+
+ <string name="bookmarks_title">&bookmarks_title;</string>
+ <string name="history_title">&history_title;</string>
+
+ <string name="switch_to_tab">&switch_to_tab;</string>
+
+ <string name="tab_offline_version">&tab_offline_version;</string>
+
+ <string name="crash_reporter_title">&crash_reporter_title;</string>
+ <string name="crash_message2">&crash_message2;</string>
+ <string name="crash_send_report_message3">&crash_send_report_message3;</string>
+ <string name="crash_include_url2">&crash_include_url2;</string>
+ <string name="crash_sorry">&crash_sorry;</string>
+ <string name="crash_comment">&crash_comment;</string>
+ <string name="crash_allow_contact2">&crash_allow_contact2;</string>
+ <string name="crash_email">&crash_email;</string>
+ <string name="crash_closing_alert">&crash_closing_alert;</string>
+ <string name="sending_crash_report">&sending_crash_report;</string>
+ <string name="crash_close_label">&crash_close_label;</string>
+ <string name="crash_restart_label">&crash_restart_label;</string>
+
+ <string name="url_bar_default_text">&url_bar_default_text2;</string>
+
+ <!-- https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/mobile-help -->
+ <string name="help_link">https://support.mozilla.org/1/mobile/&formatS1;/&formatS2;/&formatS3;/mobile-help</string>
+ <string name="help_menu">&help_menu;</string>
+
+ <string name="quit">&quit;</string>
+ <string name="bookmark">&bookmark;</string>
+ <string name="bookmark_remove">&bookmark_remove;</string>
+ <string name="bookmark_added">&bookmark_added;</string>
+ <string name="bookmark_already_added">&bookmark_already_added;</string>
+ <string name="bookmark_removed">&bookmark_removed;</string>
+ <string name="bookmark_updated">&bookmark_updated;</string>
+ <string name="bookmark_options">&bookmark_options;</string>
+ <string name="screenshot_added_to_bookmarks">&screenshot_added_to_bookmarks;</string>
+ <string name="screenshot_folder_label_in_bookmarks">&screenshot_folder_label_in_bookmarks;</string>
+ <string name="readinglist_smartfolder_label_in_bookmarks">&readinglist_smartfolder_label_in_bookmarks;</string>
+ <string name="bookmark_folder_items">&bookmark_folder_items;</string>
+ <string name="bookmark_folder_one_item">&bookmark_folder_one_item;</string>
+
+ <string name="reader_saved_offline">&reader_saved_offline;</string>
+ <string name="reader_switch_to_bookmarks">&reader_switch_to_bookmarks;</string>
+
+ <string name="history_today_section">&history_today_section;</string>
+ <string name="history_yesterday_section">&history_yesterday_section;</string>
+ <string name="history_week_section">&history_week_section3;</string>
+ <string name="history_older_section">&history_older_section3;</string>
+
+ <string name="share">&share;</string>
+ <string name="share_title">&share_title;</string>
+ <string name="share_image_failed">&share_image_failed;</string>
+ <string name="save_as_pdf">&save_as_pdf;</string>
+ <string name="print">&print;</string>
+ <string name="find_in_page">&find_in_page;</string>
+ <string name="desktop_mode">&desktop_mode;</string>
+ <string name="page">&page;</string>
+ <string name="tools">&tools;</string>
+
+ <string name="find_text">&find_text;</string>
+ <string name="find_prev">&find_prev;</string>
+ <string name="find_next">&find_next;</string>
+ <string name="find_close">&find_close;</string>
+
+ <string name="media_sending_to">&media_sending_to;</string>
+ <string name="media_play">&media_play;</string>
+ <string name="media_pause">&media_pause;</string>
+ <string name="media_stop">&media_stop;</string>
+
+ <string name="overlay_share_send_other">&overlay_share_send_other;</string>
+ <string name="overlay_share_label">&overlay_share_label;</string>
+ <string name="overlay_share_bookmark_btn_label">&overlay_share_bookmark_btn_label;</string>
+ <string name="overlay_share_bookmark_btn_label_already">&overlay_share_bookmark_btn_label_already;</string>
+ <string name="overlay_share_send_tab_btn_label">&overlay_share_send_tab_btn_label;</string>
+ <string name="overlay_share_no_url">&overlay_share_no_url;</string>
+ <string name="overlay_share_select_device">&overlay_share_select_device;</string>
+ <string name="overlay_no_synced_devices">&overlay_no_synced_devices;</string>
+
+ <string name="settings">&settings;</string>
+ <string name="settings_title">&settings_title;</string>
+ <string name="pref_category_general">&pref_category_general;</string>
+ <string name="pref_category_general_summary">&pref_category_general_summary3;</string>
+
+ <string name="pref_category_search">&pref_category_search3;</string>
+ <string name="pref_category_search_summary">&pref_category_search_summary2;</string>
+ <string name="pref_category_accessibility">&pref_category_accessibility;</string>
+ <string name="pref_category_accessibility_summary">&pref_category_accessibility_summary2;</string>
+ <string name="pref_category_privacy_short">&pref_category_privacy_short;</string>
+ <string name="pref_category_privacy_summary">&pref_category_privacy_summary4;</string>
+ <string name="pref_category_vendor">&pref_category_vendor2;</string>
+ <string name="pref_category_vendor_summary">&pref_category_vendor_summary2;</string>
+ <string name="pref_category_datareporting">&pref_category_datareporting;</string>
+ <string name="pref_category_logins">&pref_category_logins;</string>
+ <string name="pref_category_installed_search_engines">&pref_category_installed_search_engines;</string>
+ <string name="pref_category_add_search_providers">&pref_category_add_search_providers;</string>
+ <string name="pref_category_search_restore_defaults">&pref_category_search_restore_defaults;</string>
+ <string name="pref_search_restore_defaults">&pref_search_restore_defaults;</string>
+ <string name="pref_search_restore_defaults_summary">&pref_search_restore_defaults_summary;</string>
+ <string name="pref_search_hint">&pref_search_hint2;</string>
+
+ <string name="pref_category_language">&pref_category_language;</string>
+ <string name="pref_category_language_summary">&pref_category_language_summary;</string>
+ <string name="pref_browser_locale">&pref_browser_locale;</string>
+ <string name="locale_system_default">&locale_system_default;</string>
+
+ <string name="pref_category_advanced">&pref_category_advanced;</string>
+ <string name="pref_category_advanced_summary">&pref_category_advanced_summary3;</string>
+ <string name="pref_developer_remotedebugging_usb">&pref_developer_remotedebugging_usb;</string>
+ <string name="pref_developer_remotedebugging_wifi">&pref_developer_remotedebugging_wifi;</string>
+ <string name="pref_developer_remotedebugging_wifi_disabled_summary">&pref_developer_remotedebugging_wifi_disabled_summary;</string>
+
+ <string name="pref_category_notifications">&pref_category_notifications;</string>
+ <string name="pref_category_notifications_summary">&pref_category_notifications_summary;</string>
+ <string name="pref_content_notifications">&pref_content_notifications;</string>
+ <string name="pref_content_notifications_summary">&pref_content_notifications_summary2;</string>
+
+ <string name="pref_category_home">&pref_category_home;</string>
+ <string name="pref_category_home_summary">&pref_category_home_summary;</string>
+ <string name="pref_category_home_panels">&pref_category_home_panels;</string>
+ <string name="pref_home_updates_wifi">&pref_home_updates_wifi;</string>
+ <string name="pref_category_home_add_ons">&pref_category_home_add_ons;</string>
+ <string name="pref_home_updates">&pref_home_updates2;</string>
+ <string name="pref_home_updates_enabled">&pref_home_updates_enabled;</string>
+ <string name="pref_category_home_homepage">&pref_category_home_homepage;</string>
+ <string name="home_homepage_title">&home_homepage_title;</string>
+ <string name="home_homepage_radio_default">&home_homepage_radio_default;</string>
+ <string name="home_homepage_radio_user_address">&home_homepage_radio_user_address;</string>
+ <string name="home_homepage_hint_user_address">&home_homepage_hint_user_address;</string>
+
+ <string name="pref_header_general">&pref_header_general;</string>
+ <string name="pref_header_search">&pref_header_search;</string>
+ <string name="pref_header_accessibility">&pref_header_accessibility;</string>
+ <string name="pref_header_privacy_short">&pref_header_privacy_short;</string>
+ <string name="pref_header_notifications">&pref_header_notifications;</string>
+ <string name="pref_header_advanced">&pref_header_advanced;</string>
+ <string name="pref_header_vendor">&pref_header_vendor;</string>
+
+ <string name="pref_learn_more">&pref_learn_more;</string>
+
+ <string name="pref_remember_signons">&pref_remember_signons2;</string>
+
+ <string name="pref_manage_logins">&pref_manage_logins;</string>
+
+ <string name="pref_cookies_menu">&pref_cookies_menu;</string>
+ <string name="pref_cookies_accept_all">&pref_cookies_accept_all;</string>
+ <string name="pref_cookies_not_accept_foreign">&pref_cookies_not_accept_foreign;</string>
+ <string name="pref_cookies_disabled">&pref_cookies_disabled;</string>
+
+ <string name="pref_category_data_saver">&pref_category_data_saver;</string>
+ <string name="pref_category_media">&pref_category_media;</string>
+ <string name="pref_category_developer_tools">&pref_category_developer_tools;</string>
+
+ <string name="pref_tap_to_load_images_title2">&pref_tap_to_load_images_title2;</string>
+ <string name="pref_tap_to_load_images_enabled">&pref_tap_to_load_images_enabled;</string>
+ <string name="pref_tap_to_load_images_data">&pref_tap_to_load_images_data;</string>
+ <string name="pref_tap_to_load_images_disabled2">&pref_tap_to_load_images_disabled2;</string>
+
+ <string name="pref_show_web_fonts">&pref_show_web_fonts;</string>
+ <string name="pref_show_web_fonts_summary">&pref_show_web_fonts_summary2;</string>
+
+ <string name="pref_tracking_protection_title">&pref_tracking_protection_title2;</string>
+ <string name="pref_tracking_protection_summary">&pref_tracking_protection_summary3;</string>
+ <string name="pref_donottrack_title">&pref_donottrack_title;</string>
+ <string name="pref_donottrack_summary">&pref_donottrack_summary;</string>
+
+ <string name="pref_tracking_protection_enabled">&pref_tracking_protection_enabled;</string>
+ <string name="pref_tracking_protection_enabled_pb">&pref_tracking_protection_enabled_pb;</string>
+ <string name="pref_tracking_protection_disabled">&pref_tracking_protection_disabled;</string>
+
+ <string name="pref_whats_new_notification">&pref_whats_new_notification;</string>
+ <string name="pref_whats_new_notification_summary">&pref_whats_new_notification_summary;</string>
+
+ <string name="pref_category_experimental">&pref_category_experimental;</string>
+
+ <string name="pref_custom_tabs">&pref_custom_tabs;</string>
+ <string name="pref_custom_tabs_summary">&pref_custom_tabs_summary3;</string>
+
+ <string name="pref_activity_stream">&pref_activity_stream;</string>
+ <string name="pref_activity_stream_summary">&pref_activity_stream_summary;</string>
+
+ <string name="pref_char_encoding">&pref_char_encoding;</string>
+ <string name="pref_char_encoding_on">&pref_char_encoding_on;</string>
+ <string name="pref_char_encoding_off">&pref_char_encoding_off;</string>
+ <string name="pref_clear_private_data_now">&pref_clear_private_data2;</string>
+ <string name="pref_clear_private_data_now_tablet">&pref_clear_private_data_now_tablet;</string>
+ <string name="pref_clear_on_exit_title">&pref_clear_on_exit_title3;</string>
+ <string name="pref_clear_on_exit_summary2">&pref_clear_on_exit_summary2;</string>
+ <string name="pref_clear_on_exit_dialog_title">&pref_clear_on_exit_dialog_title;</string>
+ <string name="pref_plugins">&pref_plugins;</string>
+ <string name="pref_plugins_enabled">&pref_plugins_enabled;</string>
+ <string name="pref_plugins_tap_to_play">&pref_plugins_tap_to_play2;</string>
+ <string name="pref_plugins_disabled">&pref_plugins_disabled;</string>
+ <string name="pref_text_size">&pref_text_size;</string>
+ <string name="pref_font_size_tiny">&pref_font_size_tiny;</string>
+ <string name="pref_font_size_small">&pref_font_size_small;</string>
+ <string name="pref_font_size_medium">&pref_font_size_medium;</string>
+ <string name="pref_font_size_large">&pref_font_size_large;</string>
+ <string name="pref_font_size_xlarge">&pref_font_size_xlarge;</string>
+ <string name="pref_font_size_set">&pref_font_size_set;</string>
+ <string name="pref_font_size_adjust_char">&pref_font_size_adjust_char;</string>
+ <string name="pref_font_size_preview_text">&pref_font_size_preview_text;</string>
+ <string name="pref_media_autoplay_enabled">&pref_media_autoplay_enabled;</string>
+ <string name="pref_media_autoplay_enabled_summary">&pref_media_autoplay_enabled_summary;</string>
+ <string name="pref_zoom_force_enabled">&pref_zoom_force_enabled;</string>
+ <string name="pref_zoom_force_enabled_summary">&pref_zoom_force_enabled_summary;</string>
+ <string name="pref_voice_input">&pref_voice_input;</string>
+ <string name="pref_voice_input_summary">&pref_voice_input_summary2;</string>
+ <string name="pref_qrcode_enabled">&pref_qrcode_enabled;</string>
+ <string name="pref_qrcode_enabled_summary">&pref_qrcode_enabled_summary2;</string>
+ <string name="pref_restore">&pref_restore_tabs;</string>
+ <string name="pref_restore_always">&pref_restore_always;</string>
+ <string name="pref_restore_quit">&pref_restore_quit;</string>
+ <string name="pref_sync">&pref_sync2;</string>
+ <string name="pref_sync_summary">&pref_sync_summary2;</string>
+ <string name="pref_search_suggestions">&pref_search_suggestions;</string>
+ <string name="pref_history_search_suggestions">&pref_history_search_suggestions;</string>
+ <string name="pref_private_data_history2">&pref_private_data_history2;</string>
+ <string name="pref_private_data_searchHistory">&pref_private_data_searchHistory;</string>
+ <string name="pref_private_data_formdata2">&pref_private_data_formdata2;</string>
+ <string name="pref_private_data_cookies2">&pref_private_data_cookies2;</string>
+ <string name="pref_private_data_passwords">&pref_private_data_passwords2;</string>
+ <string name="pref_private_data_cache">&pref_private_data_cache;</string>
+ <string name="pref_private_data_offlineApps">&pref_private_data_offlineApps;</string>
+ <string name="pref_private_data_siteSettings">&pref_private_data_siteSettings2;</string>
+ <string name="pref_private_data_downloadFiles2">&pref_private_data_downloadFiles2;</string>
+ <string name="pref_private_data_syncedTabs">&pref_private_data_syncedTabs;</string>
+ <string name="pref_import_android">&pref_import_options;</string>
+ <string name="pref_import_android_summary">&pref_import_android_summary;</string>
+ <string name="pref_update_autodownload">&pref_update_autodownload3;</string>
+ <string name="pref_update_autodownload_wifi">&pref_update_autodownload_wifi;</string>
+ <string name="pref_update_autodownload_disabled">&pref_update_autodownload_never;</string>
+ <string name="pref_update_autodownload_enabled">&pref_update_autodownload_always;</string>
+
+ <string name="tracking_protection_prompt_title">&tracking_protection_prompt_title;</string>
+ <string name="tracking_protection_prompt_text">&tracking_protection_prompt_text;</string>
+ <string name="tracking_protection_prompt_tip_text">&tracking_protection_prompt_tip_text;</string>
+ <string name="tracking_protection_prompt_action_button">&tracking_protection_prompt_action_button;</string>
+
+ <string name="pref_tab_queue_title">&pref_tab_queue_title3;</string>
+ <string name="pref_tab_queue_summary">&pref_tab_queue_summary4;</string>
+ <string name="tab_queue_prompt_title">&tab_queue_prompt_title;</string>
+ <string name="tab_queue_prompt_text">&tab_queue_prompt_text4;</string>
+ <string name="tab_queue_prompt_tip_text">&tab_queue_prompt_tip_text2;</string>
+ <string name="tab_queue_prompt_positive_action_button">&tab_queue_prompt_positive_action_button;</string>
+ <string name="tab_queue_prompt_negative_action_button">&tab_queue_prompt_negative_action_button;</string>
+ <string name="tab_queue_prompt_permit_drawing_over_apps">&tab_queue_prompt_permit_drawing_over_apps;</string>
+ <string name="tab_queue_prompt_settings_button">&tab_queue_prompt_settings_button;</string>
+ <string name="tab_queue_toast_message">&tab_queue_toast_message3;</string>
+ <string name="tab_queue_toast_action">&tab_queue_toast_action;</string>
+ <string name="tab_queue_notification_text_singular">&tab_queue_notification_text_singular2;</string>
+ <string name="tab_queue_notification_text_plural">&tab_queue_notification_text_plural2;</string>
+ <string name="tab_queue_notification_title">&tab_queue_notification_title;</string>
+ <string name="tab_queue_notification_settings">&tab_queue_notification_settings;</string>
+
+ <string name="content_notification_summary">&content_notification_summary;</string>
+ <string name="content_notification_title_plural">&content_notification_title_plural;</string>
+ <string name="content_notification_action_settings">&content_notification_action_settings2;</string>
+ <string name="content_notification_action_read_now">&content_notification_action_read_now;</string>
+ <string name="content_notification_updated_on">&content_notification_updated_on;</string>
+
+ <string name="pref_default_browser">&pref_default_browser;</string>
+ <string name="pref_default_browser_mozilla_support_tablet">&pref_default_browser_mozilla_support_tablet;</string>
+
+ <string name="pref_about_firefox">&pref_about_firefox;</string>
+
+ <string name="pref_vendor_faqs">&pref_vendor_faqs;</string>
+ <!-- https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/faq -->
+ <string name="faq_link">https://support.mozilla.org/1/mobile/&formatS1;/&formatS2;/&formatS3;/faq</string>
+
+ <string name="pref_vendor_feedback">&pref_vendor_feedback;</string>
+ <!-- https://input.mozilla.org/feedback/android/%VERSION%/%CHANNEL%/?utm_source=feedback-settings
+ This should be kept in sync with the "app.feedbackURL" pref defined in mobile.js -->
+ <string name="feedback_link">https://input.mozilla.org/feedback/android/&formatS1;/&formatS2;/?utm_source=feedback-settings</string>
+
+ <string name="pref_dialog_set_default">&pref_dialog_set_default;</string>
+ <string name="pref_default">&pref_dialog_default;</string>
+ <string name="pref_dialog_remove">&pref_dialog_remove;</string>
+
+ <string name="pref_search_last_toast">&pref_search_last_toast;</string>
+
+ <string name="pref_panels_show">&pref_panels_show;</string>
+ <string name="pref_panels_hide">&pref_panels_hide;</string>
+ <string name="pref_panels_reorder">&pref_panels_reorder;</string>
+ <string name="pref_panels_move_up">&pref_panels_move_up;</string>
+ <string name="pref_panels_move_down">&pref_panels_move_down;</string>
+
+ <string name="datareporting_notification_title">&datareporting_notification_title;</string>
+ <string name="datareporting_notification_action">&datareporting_notification_action;</string>
+ <string name="datareporting_notification_summary">&datareporting_notification_summary;</string>
+ <string name="datareporting_notification_ticker_text">&datareporting_notification_ticker_text;</string>
+
+ <string name="datareporting_telemetry_title">&datareporting_telemetry_title;</string>
+ <string name="datareporting_telemetry_summary">&datareporting_telemetry_summary;</string>
+ <string name="datareporting_fhr_title">&datareporting_fhr_title;</string>
+ <string name="datareporting_fhr_summary2">&datareporting_fhr_summary2;</string>
+ <string name="datareporting_abouthr_title">&datareporting_abouthr_title;</string>
+ <string name="datareporting_crashreporter_title_short">&datareporting_crashreporter_title_short;</string>
+ <string name="datareporting_crashreporter_summary">&datareporting_crashreporter_summary;</string>
+ <string name="datareporting_wifi_title">&datareporting_wifi_title2;</string>
+ <string name="datareporting_wifi_geolocation_summary">&datareporting_wifi_geolocation_summary4;</string>
+
+ <string name="search">&search;</string>
+ <string name="reload">&reload;</string>
+ <string name="forward">&forward;</string>
+ <string name="menu">&menu;</string>
+ <string name="back">&back;</string>
+ <string name="stop">&stop;</string>
+ <string name="site_security">&site_security;</string>
+ <string name="close_tab">&close_tab;</string>
+ <string name="new_tab_opened">&new_tab_opened;</string>
+ <string name="new_private_tab_opened">&new_private_tab_opened;</string>
+ <string name="switch_button_message">&switch_button_message;</string>
+ <string name="tab_title_prefix_is_playing_audio">&tab_title_prefix_is_playing_audio;</string>
+ <string name="one_tab">&one_tab;</string>
+ <string name="num_tabs">&num_tabs2;</string>
+ <string name="addons">&addons;</string>
+ <string name="logins">&logins;</string>
+ <string name="downloads">&downloads;</string>
+ <string name="char_encoding">&char_encoding;</string>
+ <string name="new_tab">&new_tab;</string>
+ <string name="new_private_tab">&new_private_tab;</string>
+ <string name="close_all_tabs">&close_all_tabs;</string>
+ <string name="close_private_tabs">&close_private_tabs;</string>
+ <string name="tabs_normal">&tabs_normal;</string>
+ <string name="tabs_private">&tabs_private;</string>
+ <string name="edit_mode_cancel">&edit_mode_cancel;</string>
+
+ <string name="site_settings_title">&site_settings_title3;</string>
+ <string name="site_settings_cancel">&site_settings_cancel;</string>
+ <string name="site_settings_clear">&site_settings_clear;</string>
+
+ <string name="page_action_dropmarker_description">&page_action_dropmarker_description;</string>
+
+ <string name="contextmenu_open_new_tab">&contextmenu_open_new_tab;</string>
+ <string name="contextmenu_open_private_tab">&contextmenu_open_private_tab;</string>
+ <string name="contextmenu_remove">&contextmenu_remove;</string>
+ <string name="contextmenu_add_to_launcher">&contextmenu_add_to_launcher;</string>
+ <string name="contextmenu_share">&contextmenu_share;</string>
+ <string name="contextmenu_pasteandgo">&contextmenu_pasteandgo;</string>
+ <string name="contextmenu_paste">&contextmenu_paste;</string>
+ <string name="contextmenu_copyurl">&contextmenu_copyurl;</string>
+ <string name="contextmenu_edit_bookmark">&contextmenu_edit_bookmark;</string>
+ <string name="contextmenu_subscribe">&contextmenu_subscribe;</string>
+ <string name="contextmenu_site_settings">&contextmenu_site_settings;</string>
+ <string name="contextmenu_top_sites_edit">&contextmenu_top_sites_edit;</string>
+ <string name="contextmenu_top_sites_pin">&contextmenu_top_sites_pin;</string>
+ <string name="contextmenu_top_sites_unpin">&contextmenu_top_sites_unpin;</string>
+ <string name="contextmenu_add_search_engine">&contextmenu_add_search_engine;</string>
+
+ <string name="doorhanger_login_no_username">&doorhanger_login_no_username;</string>
+ <string name="doorhanger_login_edit_title">&doorhanger_login_edit_title;</string>
+ <string name="doorhanger_login_edit_username_hint">&doorhanger_login_edit_username_hint;</string>
+ <string name="doorhanger_login_edit_password_hint">&doorhanger_login_edit_password_hint;</string>
+ <string name="doorhanger_login_edit_toggle">&doorhanger_login_edit_toggle;</string>
+ <string name="doorhanger_login_edit_toast_error">&doorhanger_login_edit_toast_error;</string>
+ <string name="doorhanger_login_select_message">&doorhanger_login_select_message;</string>
+ <string name="doorhanger_login_select_toast_copy">&doorhanger_login_select_toast_copy;</string>
+ <string name="doorhanger_login_select_toast_copy_error">&doorhanger_login_select_toast_copy_error;</string>
+ <string name="doorhanger_login_select_action_text">&doorhanger_login_select_action_text;</string>
+ <string name="doorhanger_login_select_title">&doorhanger_login_select_title;</string>
+
+ <string name="pref_magnifying_glass_enabled">&pref_magnifying_glass_enabled;</string>
+ <string name="pref_magnifying_glass_enabled_summary">&pref_magnifying_glass_enabled_summary2;</string>
+
+ <string name="pref_scroll_title_bar2">&pref_scroll_title_bar2;</string>
+ <string name="pref_scroll_title_bar_summary">&pref_scroll_title_bar_summary2;</string>
+
+ <string name="page_removed">&page_removed;</string>
+
+ <string name="bookmark_edit_title">&bookmark_edit_title;</string>
+ <string name="bookmark_edit_name">&bookmark_edit_name;</string>
+ <string name="bookmark_edit_location">&bookmark_edit_location;</string>
+ <string name="bookmark_edit_keyword">&bookmark_edit_keyword;</string>
+
+ <string name="pref_use_master_password">&pref_use_master_password;</string>
+ <string name="masterpassword_create_title">&masterpassword_create_title;</string>
+ <string name="masterpassword_remove_title">&masterpassword_remove_title;</string>
+ <string name="masterpassword_password">&masterpassword_password;</string>
+ <string name="masterpassword_confirm">&masterpassword_confirm;</string>
+
+ <string name="button_ok">&button_ok;</string>
+ <string name="button_cancel">&button_cancel;</string>
+ <string name="button_clear_data">&button_clear_data;</string>
+ <string name="button_set">&button_set;</string>
+ <string name="button_clear">&button_clear;</string>
+ <string name="button_yes">&button_yes;</string>
+ <string name="button_no">&button_no;</string>
+ <string name="button_copy">&button_copy;</string>
+
+ <string name="home_title">&home_title;</string>
+ <string name="home_top_sites_title">&home_top_sites_title;</string>
+ <string name="home_top_sites_add">&home_top_sites_add;</string>
+ <string name="home_history_title">&home_history_title;</string>
+ <string name="home_synced_devices_smartfolder">&home_synced_devices_smartfolder;</string>
+ <string name="home_synced_devices_number">&home_synced_devices_number;</string>
+ <string name="home_synced_devices_one">&home_synced_devices_one;</string>
+ <string name="home_history_back_to">&home_history_back_to2;</string>
+ <string name="home_clear_history_button">&home_clear_history_button;</string>
+ <string name="home_clear_history_confirm">&home_clear_history_confirm;</string>
+ <string name="home_bookmarks_empty">&home_bookmarks_empty;</string>
+ <string name="home_closed_tabs_title2">&home_closed_tabs_title2;</string>
+ <string name="home_last_tabs_empty">&home_last_tabs_empty;</string>
+ <string name="home_restore_all">&home_restore_all;</string>
+ <string name="home_closed_tabs_number">&home_closed_tabs_number;</string>
+ <string name="home_closed_tabs_one">&home_closed_tabs_one;</string>
+ <string name="home_most_recent_empty">&home_most_recent_empty;</string>
+ <string name="home_most_recent_emptyhint">&home_most_recent_emptyhint2;</string>
+ <string name="home_default_empty">&home_default_empty;</string>
+ <string name="home_move_back_to_filter">&home_move_back_to_filter;</string>
+ <string name="home_remote_tabs_many_hidden_devices">&home_remote_tabs_many_hidden_devices;</string>
+ <string name="home_remote_tabs_hidden_devices_title">&home_remote_tabs_hidden_devices_title;</string>
+ <string name="home_remote_tabs_unhide_selected_devices">&home_remote_tabs_unhide_selected_devices;</string>
+ <string name="pin_site_dialog_hint">&pin_site_dialog_hint;</string>
+
+ <string name="remote_tabs_never_synced">&remote_tabs_never_synced;</string>
+
+ <string name="filepicker_title">&filepicker_title;</string>
+ <string name="filepicker_audio_title">&filepicker_audio_title;</string>
+ <string name="filepicker_image_title">&filepicker_image_title;</string>
+ <string name="filepicker_video_title">&filepicker_video_title;</string>
+
+ <!-- Default bookmarks. We used to use bookmark titles shared with XUL from mobile's
+ profile/bookmarks.inc (see bug 964946). Don't expose the URLs to L10N. -->
+ <string name="bookmarkdefaults_title_aboutfirefox">&bookmarks_about_browser;</string>
+ <string name="bookmarkdefaults_url_aboutfirefox">about:firefox</string>
+
+ <!-- Icon is automatically generated from R.drawable.bookmarkdefaults_favicon_addons -->
+ <string name="bookmarkdefaults_title_addons">&bookmarks_addons;</string>
+ <string name="bookmarkdefaults_url_addons">https://addons.mozilla.org/android?utm_source=inproduct&amp;utm_medium=default-bookmarks&amp;utm_campaign=mobileandroid</string>
+
+ <!-- Icon is automatically generated from R.drawable.bookmarkdefaults_favicon_support -->
+ <string name="bookmarkdefaults_title_support">&bookmarks_support;</string>
+ <string name="bookmarkdefaults_url_support">https://support.mozilla.org/products/mobile?utm_source=inproduct&amp;utm_medium=default-bookmarks&amp;utm_campaign=mobileandroid</string>
+
+ <string name="bookmarkdefaults_title_restricted_webmaker">&bookmarks_restricted_webmaker;</string>
+ <string name="bookmarkdefaults_url_restricted_webmaker">https://webmaker.org/</string>
+
+ <string name="bookmarkdefaults_title_restricted_support">&bookmarks_restricted_support2;</string>
+ <string name="bookmarkdefaults_url_restricted_support">https://support.mozilla.org/kb/controlledaccess?utm_source=inproduct&amp;utm_medium=default-bookmarks&amp;utm_campaign=mobileandroid</string>
+
+ <!-- Site identity popup -->
+ <string name="identity_connection_secure">&identity_connection_secure;</string>
+ <string name="identity_connection_insecure">&identity_connection_insecure;</string>
+ <string name="identity_connection_chromeui">&identity_connection_chromeui;</string>
+
+ <string name="mixed_content_blocked_all">&mixed_content_blocked_all1;</string>
+ <string name="mixed_content_blocked_some">&mixed_content_blocked_some1;</string>
+ <string name="mixed_content_display_loaded">&mixed_content_display_loaded1;</string>
+ <string name="mixed_content_protection_disabled">&mixed_content_protection_disabled1;</string>
+
+ <string name="doorhanger_tracking_title">&doorhanger_tracking_title2;</string>
+ <string name="doorhanger_tracking_state_enabled">&doorhanger_tracking_state_enabled;</string>
+ <string name="doorhanger_tracking_state_disabled">&doorhanger_tracking_state_disabled;</string>
+ <string name="doorhanger_tracking_message_enabled">&doorhanger_tracking_message_enabled1;</string>
+ <string name="doorhanger_tracking_message_disabled">&doorhanger_tracking_message_disabled2;</string>
+
+ <string name="learn_more">&learn_more;</string>
+ <string name="enable_protection">&enable_protection;</string>
+ <string name="disable_protection">&disable_protection;</string>
+
+ <!-- Clear private data -->
+ <string name="private_data_success">&private_data_success;</string>
+ <string name="private_data_fail">&private_data_fail;</string>
+
+ <!-- Bookmark import/export -->
+ <string name="bookmarkhistory_button_import">&bookmarkhistory_button_import;</string>
+ <string name="bookmarkhistory_import_both">&bookmarkhistory_import_both;</string>
+ <string name="bookmarkhistory_import_bookmarks">&bookmarkhistory_import_bookmarks;</string>
+ <string name="bookmarkhistory_import_history">&bookmarkhistory_import_history;</string>
+ <string name="bookmarkhistory_import_wait">&bookmarkhistory_import_wait;</string>
+
+ <string name="searchable_description">&searchable_description;</string>
+
+ <!-- Updater notifications -->
+ <string name="updater_start_title">&updater_start_title2;</string>
+ <string name="updater_start_select">&updater_start_select2;</string>
+
+ <string name="updater_downloading_title">&updater_downloading_title2;</string>
+ <string name="updater_downloading_title_failed">&updater_downloading_title_failed2;</string>
+ <string name="updater_downloading_select">&updater_downloading_select2;</string>
+ <string name="updater_downloading_retry">&updater_downloading_retry2;</string>
+
+ <string name="updater_apply_title">&updater_apply_title2;</string>
+ <string name="updater_apply_select">&updater_apply_select2;</string>
+
+ <string name="updater_permission_title">&brandShortName;</string>
+ <string name="updater_permission_text">&updater_permission_text;</string>
+ <string name="updater_permission_allow">&updater_permission_allow;</string>
+
+ <!-- Awesomescreen screen -->
+ <string name="suggestions_prompt">&suggestions_prompt3;</string>
+ <string name="search_bar_item_desc">&search_bar_item_desc;</string>
+
+ <string name="suggestion_for_engine">&suggestion_for_engine;</string>
+
+ <!-- Set Image Notifications -->
+ <string name="set_image_fail">&set_image_fail;</string>
+ <string name="set_image_path_fail">&set_image_path_fail;</string>
+ <string name="set_image_chooser_title">&set_image_chooser_title;</string>
+
+ <!-- Guest mode -->
+ <string name="new_guest_session">&new_guest_session;</string>
+ <string name="exit_guest_session">&exit_guest_session;</string>
+ <string name="guest_session_dialog_continue">&guest_session_dialog_continue;</string>
+ <string name="guest_session_dialog_cancel">&guest_session_dialog_cancel;</string>
+ <string name="new_guest_session_title">&new_guest_session_title;</string>
+ <string name="new_guest_session_text">&new_guest_session_text2;</string>
+ <string name="guest_browsing_notification_title">&guest_browsing_notification_title;</string>
+ <string name="guest_browsing_notification_text">&guest_browsing_notification_text;</string>
+
+ <string name="exit_guest_session_title">&exit_guest_session_title;</string>
+ <string name="exit_guest_session_text">&exit_guest_session_text;</string>
+
+ <string name="actionbar_menu">&actionbar_menu;</string>
+ <string name="actionbar_done">&actionbar_done;</string>
+
+ <!-- Voice search from the Awesome Bar -->
+ <string name="voicesearch_prompt">&voicesearch_prompt;</string>
+
+ <!-- Restrictable features -->
+ <string name="restrictable_feature_addons_installation">&restrictable_feature_addons_installation;</string>
+ <string name="restrictable_feature_addons_installation_description">&restrictable_feature_addons_installation_description;</string>
+ <string name="restrictable_feature_private_browsing">&restrictable_feature_private_browsing;</string>
+ <string name="restrictable_feature_private_browsing_description">&restrictable_feature_private_browsing_description;</string>
+ <string name="restrictable_feature_clear_history">&restrictable_feature_clear_history;</string>
+ <string name="restrictable_feature_clear_history_description">&restrictable_feature_clear_history_description;</string>
+ <string name="restrictable_feature_advanced_settings">&restrictable_feature_advanced_settings;</string>
+ <string name="restrictable_feature_advanced_settings_description">&restrictable_feature_advanced_settings_description;</string>
+ <string name="restrictable_feature_camera_microphone">&restrictable_feature_camera_microphone;</string>
+ <string name="restrictable_feature_camera_microphone_description">&restrictable_feature_camera_microphone_description;</string>
+ <string name="restrictable_feature_block_list">&restrictable_feature_block_list;</string>
+ <string name="restrictable_feature_block_list_description">&restrictable_feature_block_list_description;</string>
+
+ <!-- Miscellaneous -->
+ <string name="ellipsis">&ellipsis;</string>
+
+ <string name="colon">&colon;</string>
+
+ <string name="percent">&percent;</string>
+
+ <string name="remote_tabs_last_synced">&remote_tabs_last_synced;</string>
+
+ <string name="intent_uri_private_browsing_prompt">&intent_uri_private_browsing_prompt;</string>
+ <string name="intent_uri_private_browsing_multiple_match_title">&intent_uri_private_browsing_multiple_match_title;</string>
+
+ <string name="devtools_auth_scan_header">&devtools_auth_scan_header;</string>
+
+ <string name="unsupported_sdk_version">&unsupported_sdk_version;</string>
+ <string name="eol_notification_title">&eol_notification_title2;</string>
+ <string name="eol_notification_summary">&eol_notification_summary;</string>
+ <!-- https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/honeycomb -->
+ <string name="eol_notification_url">https://support.mozilla.org/1/mobile/&formatS1;/&formatS2;/&formatS3;/unsupported-version</string>
+
+ <string name="whatsnew_notification_title">&whatsnew_notification_title;</string>
+ <string name="whatsnew_notification_summary">&whatsnew_notification_summary;</string>
+ <!-- https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/new-android -->
+ <string name="whatsnew_notification_url">https://support.mozilla.org/1/mobile/&formatS1;/&formatS2;/&formatS3;/new-android</string>
+
+ <string name="promotion_add_to_homescreen">&promotion_add_to_homescreen;</string>
+
+ <string name="helper_first_offline_bookmark_title">&helper_first_offline_bookmark_title;</string>
+ <string name="helper_first_offline_bookmark_message">&helper_first_offline_bookmark_message;</string>
+ <string name="helper_first_offline_bookmark_button">&helper_first_offline_bookmark_button;</string>
+
+ <string name="helper_triple_readerview_open_title">&helper_triple_readerview_open_title;</string>
+ <string name="helper_triple_readerview_open_message">&helper_triple_readerview_open_message;</string>
+ <string name="helper_triple_readerview_open_button">&helper_triple_readerview_open_button;</string>
+
+ <string name="activity_stream_topsites">&activity_stream_topsites;</string>
+ <string name="activity_stream_highlights">&activity_stream_highlights;</string>
+ <string name="activity_stream_highlight_label_bookmarked">&activity_stream_highlight_label_bookmarked;</string>
+ <string name="activity_stream_highlight_label_visited">&activity_stream_highlight_label_visited;</string>
+ <string name="activity_stream_dismiss">&activity_stream_dismiss;</string>
+ <string name="activity_stream_delete_history">&activity_stream_delete_history;</string>
+</resources>
diff --git a/mobile/android/bouncer/AndroidManifest.xml.in b/mobile/android/bouncer/AndroidManifest.xml.in
new file mode 100644
index 0000000000..b265e73942
--- /dev/null
+++ b/mobile/android/bouncer/AndroidManifest.xml.in
@@ -0,0 +1,96 @@
+#filter substitution
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="@ANDROID_PACKAGE_NAME@"
+ android:installLocation="auto"
+ android:versionCode="@ANDROID_VERSION_CODE@"
+ android:versionName="@MOZ_APP_VERSION@"
+#ifdef MOZ_ANDROID_SHARED_ID
+ android:sharedUserId="@MOZ_ANDROID_SHARED_ID@"
+#endif
+ >
+ <uses-sdk android:minSdkVersion="@MOZ_ANDROID_MIN_SDK_VERSION@"
+#ifdef MOZ_ANDROID_MAX_SDK_VERSION
+ android:maxSdkVersion="@MOZ_ANDROID_MAX_SDK_VERSION@"
+#endif
+ android:targetSdkVersion="23"/>
+
+<!-- The bouncer APK and the main APK should define the same set of
+ <permission>, <uses-permission>, and <uses-feature> elements. This reduces
+ the likelihood of permission-related surprises when installing the main APK
+ on top of a pre-installed bouncer APK. Add such shared elements in the
+ fileincluded here, so that they can be referenced by both APKs. -->
+#include ../base/FennecManifest_permissions.xml.in
+
+ <application android:label="@MOZ_APP_DISPLAYNAME@"
+ android:icon="@drawable/icon"
+ android:logo="@drawable/logo"
+ android:hardwareAccelerated="true"
+ android:allowBackup="false"
+# The preprocessor does not yet support arbitrary parentheses, so this cannot
+# be parenthesized thus to clarify that the logical AND operator has precedence:
+# !defined(MOZILLA_OFFICIAL) || (defined(NIGHTLY_BUILD) && defined(MOZ_DEBUG))
+#if !defined(MOZILLA_OFFICIAL) || defined(NIGHTLY_BUILD) && defined(MOZ_DEBUG)
+ android:debuggable="true">
+#else
+ android:debuggable="false">
+#endif
+
+ <activity
+ android:name="@MOZ_ANDROID_BROWSER_INTENT_CLASS@"
+ android:label="@MOZ_APP_DISPLAYNAME@"
+ android:theme="@android:style/Theme.Translucent">
+
+ <!-- Aping org.mozilla.gecko.BrowserApp. -->
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER"/>
+ <category android:name="android.intent.category.APP_BROWSER" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+
+ <meta-data android:name="com.sec.minimode.icon.portrait.normal"
+ android:resource="@drawable/icon"/>
+
+ <meta-data android:name="com.sec.minimode.icon.landscape.normal"
+ android:resource="@drawable/icon" />
+
+ <intent-filter>
+ <action android:name="android.intent.action.WEB_SEARCH" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="" />
+ <data android:scheme="http" />
+ <data android:scheme="https" />
+ </intent-filter>
+
+ <!-- Aping org.mozilla.gecko.tabqueue.TabQueueDispatcher. -->
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="http" />
+ <data android:scheme="https" />
+ <data android:scheme="about" />
+ <data android:scheme="javascript" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:scheme="file" />
+ <data android:scheme="http" />
+ <data android:scheme="https" />
+ <data android:mimeType="text/html"/>
+ <data android:mimeType="text/plain"/>
+ <data android:mimeType="application/xhtml+xml"/>
+ </intent-filter>
+ </activity>
+
+ <service
+ android:name="org.mozilla.bouncer.BouncerService"
+ android:exported="false" />
+
+ </application>
+</manifest>
diff --git a/mobile/android/bouncer/Makefile.in b/mobile/android/bouncer/Makefile.in
new file mode 100644
index 0000000000..0a971d5d66
--- /dev/null
+++ b/mobile/android/bouncer/Makefile.in
@@ -0,0 +1,20 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+include $(topsrcdir)/config/config.mk
+
+JAVAFILES := \
+ java/org/mozilla/bouncer/BouncerService.java \
+ java/org/mozilla/gecko/BrowserApp.java \
+ $(NULL)
+
+ANDROID_EXTRA_JARS := \
+ $(NULL)
+
+# Targets built very early during a Gradle build.
+gradle-targets: $(abspath AndroidManifest.xml)
+
+.PHONY: gradle-targets
+
+libs:: $(ANDROID_APK_NAME).apk
diff --git a/mobile/android/bouncer/assets/example_asset.txt b/mobile/android/bouncer/assets/example_asset.txt
new file mode 100644
index 0000000000..34338f983e
--- /dev/null
+++ b/mobile/android/bouncer/assets/example_asset.txt
@@ -0,0 +1 @@
+This is an example asset.
diff --git a/mobile/android/bouncer/build.gradle b/mobile/android/bouncer/build.gradle
new file mode 100644
index 0000000000..210aa7ece0
--- /dev/null
+++ b/mobile/android/bouncer/build.gradle
@@ -0,0 +1,76 @@
+buildDir "${topobjdir}/gradle/build/mobile/android/bouncer"
+
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 23
+ buildToolsVersion mozconfig.substs.ANDROID_BUILD_TOOLS_VERSION
+
+ defaultConfig {
+ targetSdkVersion 23
+ minSdkVersion 15
+ applicationId mozconfig.substs.ANDROID_PACKAGE_NAME
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_7
+ targetCompatibility JavaVersion.VERSION_1_7
+ }
+
+ dexOptions {
+ javaMaxHeapSize "2g"
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ }
+
+ sourceSets {
+ main {
+ manifest.srcFile "${topobjdir}/mobile/android/bouncer/AndroidManifest.xml"
+ assets {
+ if (mozconfig.substs.MOZ_ANDROID_DISTRIBUTION_DIRECTORY) {
+ srcDir "${mozconfig.substs.MOZ_ANDROID_DISTRIBUTION_DIRECTORY}/assets"
+ }
+ }
+ java {
+ srcDir 'java'
+ }
+ res {
+ srcDir "${topsrcdir}/${mozconfig.substs.MOZ_BRANDING_DIRECTORY}/res" // For the icon.
+ srcDir 'res'
+ }
+ }
+ }
+}
+
+task generateCodeAndResources(type:Exec) {
+ workingDir "${topobjdir}"
+
+ commandLine mozconfig.substs.GMAKE
+ args '-C'
+ args "${topobjdir}/mobile/android/bouncer"
+ args 'gradle-targets'
+
+ // Only show the output if something went wrong.
+ ignoreExitValue = true
+ standardOutput = new ByteArrayOutputStream()
+ errorOutput = standardOutput
+ doLast {
+ if (execResult.exitValue != 0) {
+ throw new GradleException("Process '${commandLine}' finished with non-zero exit value ${execResult.exitValue}:\n\n${standardOutput.toString()}")
+ }
+ }
+}
+
+afterEvaluate {
+ android.applicationVariants.all {
+ preBuild.dependsOn generateCodeAndResources
+ }
+}
diff --git a/mobile/android/bouncer/java/org/mozilla/bouncer/BouncerService.java b/mobile/android/bouncer/java/org/mozilla/bouncer/BouncerService.java
new file mode 100644
index 0000000000..b33d1a9ca5
--- /dev/null
+++ b/mobile/android/bouncer/java/org/mozilla/bouncer/BouncerService.java
@@ -0,0 +1,129 @@
+/* -*- 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.bouncer;
+
+import android.app.IntentService;
+import android.content.Intent;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+public class BouncerService extends IntentService {
+
+ private static final String LOGTAG = "GeckoBouncerService";
+
+ public BouncerService() {
+ super("BouncerService");
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ final byte[] buffer = new byte[8192];
+
+ Log.d(LOGTAG, "Preparing to copy distribution files");
+
+ final List<String> files;
+ try {
+ files = getFiles("distribution");
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Error getting distribution files from assets/distribution/**", e);
+ return;
+ }
+
+ InputStream in = null;
+ for (String path : files) {
+ try {
+ Log.d(LOGTAG, "Copying distribution file: " + path);
+
+ in = getAssets().open(path);
+
+ final File outFile = getDataFile(path);
+ writeStream(in, outFile, buffer);
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Error opening distribution input stream from assets", e);
+ } finally {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Error closing distribution input stream", e);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Recursively traverse a directory to list paths to all files.
+ *
+ * @param path Directory to traverse.
+ * @return List of all files in given directory.
+ * @throws IOException
+ */
+ private List<String> getFiles(String path) throws IOException {
+ List<String> paths = new ArrayList<>();
+ getFiles(path, paths);
+ return paths;
+ }
+
+ /**
+ * Recursively traverse a directory to list paths to all files.
+ *
+ * @param path Directory to traverse.
+ * @param acc Accumulator of paths seen.
+ * @throws IOException
+ */
+ private void getFiles(String path, List<String> acc) throws IOException {
+ final String[] list = getAssets().list(path);
+ if (list.length > 0) {
+ // We're a directory -- recurse.
+ for (final String file : list) {
+ getFiles(path + "/" + file, acc);
+ }
+ } else {
+ // We're a file -- accumulate.
+ acc.add(path);
+ }
+ }
+
+ private String getDataDir() {
+ return getApplicationInfo().dataDir;
+ }
+
+ private File getDataFile(final String path) {
+ File outFile = new File(getDataDir(), path);
+ File dir = outFile.getParentFile();
+
+ if (dir != null && !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 void writeStream(InputStream fileStream, File outFile, byte[] buffer)
+ throws IOException {
+ final OutputStream outStream = new FileOutputStream(outFile);
+ try {
+ int count;
+ while ((count = fileStream.read(buffer)) > 0) {
+ outStream.write(buffer, 0, count);
+ }
+ } finally {
+ outStream.close();
+ }
+ }
+}
diff --git a/mobile/android/bouncer/java/org/mozilla/gecko/BrowserApp.java b/mobile/android/bouncer/java/org/mozilla/gecko/BrowserApp.java
new file mode 100644
index 0000000000..8a462822f5
--- /dev/null
+++ b/mobile/android/bouncer/java/org/mozilla/gecko/BrowserApp.java
@@ -0,0 +1,46 @@
+/* -*- 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.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+import org.mozilla.bouncer.BouncerService;
+
+/**
+ * Bouncer activity version of BrowserApp.
+ *
+ * This class has the same name as org.mozilla.gecko.BrowserApp to preserve
+ * shortcuts created by the bouncer app.
+ */
+public class BrowserApp extends Activity {
+ private static final String LOGTAG = "GeckoBouncerActivity";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // This races distribution installation against the Play Store killing our process to
+ // install the update. We'll live with it. To do better, consider using an Intent to
+ // notify when the service has completed.
+ startService(new Intent(this, BouncerService.class));
+
+ final String appPackageName = Uri.encode(getPackageName());
+ final Uri uri = Uri.parse("market://details?id=" + appPackageName);
+ Log.i(LOGTAG, "Lanching activity with URL: " + uri.toString());
+
+ // It might be more correct to catch failure in case the Play Store isn't installed. The
+ // fallback action is to open the Play Store website... but doing so may offer Firefox as
+ // browser (since even the bouncer offers to view URLs), which will be very confusing.
+ // Therefore, we don't try to be fancy here, and we just fail (silently).
+ startActivity(new Intent(Intent.ACTION_VIEW, uri));
+
+ finish();
+ }
+}
diff --git a/mobile/android/bouncer/moz.build b/mobile/android/bouncer/moz.build
new file mode 100644
index 0000000000..2749fa9277
--- /dev/null
+++ b/mobile/android/bouncer/moz.build
@@ -0,0 +1,45 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEFINES['ANDROID_VERSION_CODE'] = '1'
+
+for var in ('ANDROID_PACKAGE_NAME',
+ 'MOZ_ANDROID_BROWSER_INTENT_CLASS',
+ 'MOZ_APP_DISPLAYNAME',
+ 'MOZ_APP_VERSION'):
+ DEFINES[var] = CONFIG[var]
+
+for var in ('MOZ_ANDROID_GCM',
+ 'MOZ_ANDROID_DOWNLOADS_INTEGRATION',
+ 'MOZ_ANDROID_BEAM',
+ 'MOZ_ANDROID_SEARCH_ACTIVITY',
+ 'MOZ_ANDROID_MLS_STUMBLER'):
+ if CONFIG[var]:
+ DEFINES[var] = CONFIG[var]
+
+ANDROID_APK_NAME = 'bouncer'
+ANDROID_APK_PACKAGE = CONFIG['ANDROID_PACKAGE_NAME']
+
+# Putting branding earlier allows branders to override default resources.
+ANDROID_RES_DIRS += [
+ '/' + CONFIG['MOZ_BRANDING_DIRECTORY'] + '/res', # For the icon.
+ 'res',
+]
+
+ANDROID_ASSETS_DIRS += [
+ 'assets',
+]
+
+if CONFIG['MOZ_ANDROID_DISTRIBUTION_DIRECTORY']:
+ # If you change this, also change its equivalent in mobile/android/base.
+ ANDROID_ASSETS_DIRS += [
+ '%' + CONFIG['MOZ_ANDROID_DISTRIBUTION_DIRECTORY'] + '/assets',
+ ]
+
+DEFINES['MOZ_ANDROID_SHARED_ID'] = CONFIG['MOZ_ANDROID_SHARED_ID']
+OBJDIR_PP_FILES.mobile.android.bouncer += [
+ 'AndroidManifest.xml.in',
+]
diff --git a/mobile/android/bouncer/res/drawable-v21/logo.xml b/mobile/android/bouncer/res/drawable-v21/logo.xml
new file mode 100644
index 0000000000..568cbec00e
--- /dev/null
+++ b/mobile/android/bouncer/res/drawable-v21/logo.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- The action bar scales the application icon to be too large (bug 1132751)
+ so add some padding to prevent it from scaling so much. -->
+<inset
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:drawable="@drawable/icon"
+ android:insetTop="6dp"
+ android:insetBottom="6dp"
+ android:insetLeft="6dp"
+ android:insetRight="6dp"
+ />
diff --git a/mobile/android/bouncer/res/drawable/logo.xml b/mobile/android/bouncer/res/drawable/logo.xml
new file mode 100644
index 0000000000..e188f80dce
--- /dev/null
+++ b/mobile/android/bouncer/res/drawable/logo.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- Overidden. -->
+<bitmap
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/icon"/>
diff --git a/mobile/android/branding/aurora/configure.sh b/mobile/android/branding/aurora/configure.sh
new file mode 100644
index 0000000000..39b9e15c31
--- /dev/null
+++ b/mobile/android/branding/aurora/configure.sh
@@ -0,0 +1,10 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MOZ_APP_DISPLAYNAME="Firefox Aurora"
+ANDROID_PACKAGE_NAME=org.mozilla.fennec_aurora
+MOZ_UPDATER=1
+MOZ_ANDROID_ANR_REPORTER=1
+MOZ_ANDROID_SHARED_ID=org.mozilla.fennec.sharedID
+MOZ_ANDROID_GCM_SENDERID=965234145045
diff --git a/mobile/android/branding/aurora/content/about.png b/mobile/android/branding/aurora/content/about.png
new file mode 100644
index 0000000000..3779bd120c
--- /dev/null
+++ b/mobile/android/branding/aurora/content/about.png
Binary files differ
diff --git a/mobile/android/branding/aurora/content/favicon32.png b/mobile/android/branding/aurora/content/favicon32.png
new file mode 100644
index 0000000000..defb22717d
--- /dev/null
+++ b/mobile/android/branding/aurora/content/favicon32.png
Binary files differ
diff --git a/mobile/android/branding/aurora/content/favicon64.png b/mobile/android/branding/aurora/content/favicon64.png
new file mode 100644
index 0000000000..163782abf0
--- /dev/null
+++ b/mobile/android/branding/aurora/content/favicon64.png
Binary files differ
diff --git a/mobile/android/branding/aurora/content/jar.mn b/mobile/android/branding/aurora/content/jar.mn
new file mode 100644
index 0000000000..e49cc40416
--- /dev/null
+++ b/mobile/android/branding/aurora/content/jar.mn
@@ -0,0 +1,9 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+chrome.jar:
+% content branding %content/branding/ contentaccessible=yes
+ content/branding/about.png (about.png)
+ content/branding/favicon32.png (favicon32.png)
+ content/branding/favicon64.png (favicon64.png)
diff --git a/mobile/android/branding/aurora/content/moz.build b/mobile/android/branding/aurora/content/moz.build
new file mode 100644
index 0000000000..eb4454d28f
--- /dev/null
+++ b/mobile/android/branding/aurora/content/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file
diff --git a/mobile/android/branding/aurora/locales/en-US/brand.dtd b/mobile/android/branding/aurora/locales/en-US/brand.dtd
new file mode 100644
index 0000000000..a6cada676f
--- /dev/null
+++ b/mobile/android/branding/aurora/locales/en-US/brand.dtd
@@ -0,0 +1,7 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY brandShortName "Firefox Aurora">
+<!ENTITY brandFullName "Mozilla Firefox Aurora">
+<!ENTITY vendorShortName "Mozilla"> \ No newline at end of file
diff --git a/mobile/android/branding/aurora/locales/en-US/brand.properties b/mobile/android/branding/aurora/locales/en-US/brand.properties
new file mode 100644
index 0000000000..fc4afd6d59
--- /dev/null
+++ b/mobile/android/branding/aurora/locales/en-US/brand.properties
@@ -0,0 +1,6 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+brandShortName=Aurora
+brandFullName=Mozilla Aurora
diff --git a/mobile/android/branding/aurora/locales/jar.mn b/mobile/android/branding/aurora/locales/jar.mn
new file mode 100644
index 0000000000..5a77695c91
--- /dev/null
+++ b/mobile/android/branding/aurora/locales/jar.mn
@@ -0,0 +1,11 @@
+#filter substitution
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+@AB_CD@.jar:
+% locale branding @AB_CD@ %locale/branding/
+# Nightly branding only exists in en-US
+ locale/branding/brand.dtd (en-US/brand.dtd)
+ locale/branding/brand.properties (en-US/brand.properties)
diff --git a/mobile/android/branding/aurora/locales/moz.build b/mobile/android/branding/aurora/locales/moz.build
new file mode 100644
index 0000000000..eb4454d28f
--- /dev/null
+++ b/mobile/android/branding/aurora/locales/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file
diff --git a/mobile/android/branding/aurora/moz.build b/mobile/android/branding/aurora/moz.build
new file mode 100644
index 0000000000..04084b1ace
--- /dev/null
+++ b/mobile/android/branding/aurora/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += ['content', 'locales']
diff --git a/mobile/android/branding/aurora/res/drawable-hdpi/icon.png b/mobile/android/branding/aurora/res/drawable-hdpi/icon.png
new file mode 100644
index 0000000000..17748ed5e9
--- /dev/null
+++ b/mobile/android/branding/aurora/res/drawable-hdpi/icon.png
Binary files differ
diff --git a/mobile/android/branding/aurora/res/drawable-hdpi/large_icon.png b/mobile/android/branding/aurora/res/drawable-hdpi/large_icon.png
new file mode 100644
index 0000000000..229ad5e0fd
--- /dev/null
+++ b/mobile/android/branding/aurora/res/drawable-hdpi/large_icon.png
Binary files differ
diff --git a/mobile/android/branding/aurora/res/drawable-hdpi/launcher_widget.png b/mobile/android/branding/aurora/res/drawable-hdpi/launcher_widget.png
new file mode 100644
index 0000000000..8e55b0c113
--- /dev/null
+++ b/mobile/android/branding/aurora/res/drawable-hdpi/launcher_widget.png
Binary files differ
diff --git a/mobile/android/branding/aurora/res/drawable-hdpi/widget_icon.png b/mobile/android/branding/aurora/res/drawable-hdpi/widget_icon.png
new file mode 100644
index 0000000000..39fdb8204b
--- /dev/null
+++ b/mobile/android/branding/aurora/res/drawable-hdpi/widget_icon.png
Binary files differ
diff --git a/mobile/android/branding/aurora/res/drawable-xhdpi/icon.png b/mobile/android/branding/aurora/res/drawable-xhdpi/icon.png
new file mode 100644
index 0000000000..51c54bf37e
--- /dev/null
+++ b/mobile/android/branding/aurora/res/drawable-xhdpi/icon.png
Binary files differ
diff --git a/mobile/android/branding/aurora/res/drawable-xhdpi/large_icon.png b/mobile/android/branding/aurora/res/drawable-xhdpi/large_icon.png
new file mode 100644
index 0000000000..78e94d30a8
--- /dev/null
+++ b/mobile/android/branding/aurora/res/drawable-xhdpi/large_icon.png
Binary files differ
diff --git a/mobile/android/branding/aurora/res/drawable-xhdpi/launcher_widget.png b/mobile/android/branding/aurora/res/drawable-xhdpi/launcher_widget.png
new file mode 100644
index 0000000000..56f47446a6
--- /dev/null
+++ b/mobile/android/branding/aurora/res/drawable-xhdpi/launcher_widget.png
Binary files differ
diff --git a/mobile/android/branding/aurora/res/drawable-xhdpi/widget_icon.png b/mobile/android/branding/aurora/res/drawable-xhdpi/widget_icon.png
new file mode 100644
index 0000000000..74e6f8aeaa
--- /dev/null
+++ b/mobile/android/branding/aurora/res/drawable-xhdpi/widget_icon.png
Binary files differ
diff --git a/mobile/android/branding/aurora/res/drawable-xxhdpi/icon.png b/mobile/android/branding/aurora/res/drawable-xxhdpi/icon.png
new file mode 100644
index 0000000000..229ad5e0fd
--- /dev/null
+++ b/mobile/android/branding/aurora/res/drawable-xxhdpi/icon.png
Binary files differ
diff --git a/mobile/android/branding/aurora/res/drawable-xxhdpi/large_icon.png b/mobile/android/branding/aurora/res/drawable-xxhdpi/large_icon.png
new file mode 100644
index 0000000000..5b439392e4
--- /dev/null
+++ b/mobile/android/branding/aurora/res/drawable-xxhdpi/large_icon.png
Binary files differ
diff --git a/mobile/android/branding/aurora/res/drawable-xxhdpi/launcher_widget.png b/mobile/android/branding/aurora/res/drawable-xxhdpi/launcher_widget.png
new file mode 100644
index 0000000000..eb88984dbf
--- /dev/null
+++ b/mobile/android/branding/aurora/res/drawable-xxhdpi/launcher_widget.png
Binary files differ
diff --git a/mobile/android/branding/aurora/res/drawable-xxhdpi/widget_icon.png b/mobile/android/branding/aurora/res/drawable-xxhdpi/widget_icon.png
new file mode 100644
index 0000000000..bb651922fc
--- /dev/null
+++ b/mobile/android/branding/aurora/res/drawable-xxhdpi/widget_icon.png
Binary files differ
diff --git a/mobile/android/branding/aurora/res/drawable-xxxhdpi/icon.png b/mobile/android/branding/aurora/res/drawable-xxxhdpi/icon.png
new file mode 100644
index 0000000000..27f63fc249
--- /dev/null
+++ b/mobile/android/branding/aurora/res/drawable-xxxhdpi/icon.png
Binary files differ
diff --git a/mobile/android/branding/beta/configure.sh b/mobile/android/branding/beta/configure.sh
new file mode 100644
index 0000000000..b8c442a7c2
--- /dev/null
+++ b/mobile/android/branding/beta/configure.sh
@@ -0,0 +1,10 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MOZ_APP_DISPLAYNAME="Firefox Beta"
+ANDROID_PACKAGE_NAME=org.mozilla.firefox_beta
+MOZ_UPDATER=
+MOZ_ANDROID_ANR_REPORTER=
+MOZ_ANDROID_SHARED_ID=org.mozilla.firefox.sharedID
+MOZ_ANDROID_GCM_SENDERID=965234145045
diff --git a/mobile/android/branding/beta/content/about.png b/mobile/android/branding/beta/content/about.png
new file mode 100644
index 0000000000..dc52e843bd
--- /dev/null
+++ b/mobile/android/branding/beta/content/about.png
Binary files differ
diff --git a/mobile/android/branding/beta/content/favicon32.png b/mobile/android/branding/beta/content/favicon32.png
new file mode 100644
index 0000000000..63b54a2473
--- /dev/null
+++ b/mobile/android/branding/beta/content/favicon32.png
Binary files differ
diff --git a/mobile/android/branding/beta/content/favicon64.png b/mobile/android/branding/beta/content/favicon64.png
new file mode 100644
index 0000000000..2a9eb30d82
--- /dev/null
+++ b/mobile/android/branding/beta/content/favicon64.png
Binary files differ
diff --git a/mobile/android/branding/beta/content/jar.mn b/mobile/android/branding/beta/content/jar.mn
new file mode 100644
index 0000000000..e49cc40416
--- /dev/null
+++ b/mobile/android/branding/beta/content/jar.mn
@@ -0,0 +1,9 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+chrome.jar:
+% content branding %content/branding/ contentaccessible=yes
+ content/branding/about.png (about.png)
+ content/branding/favicon32.png (favicon32.png)
+ content/branding/favicon64.png (favicon64.png)
diff --git a/mobile/android/branding/beta/content/moz.build b/mobile/android/branding/beta/content/moz.build
new file mode 100644
index 0000000000..eb4454d28f
--- /dev/null
+++ b/mobile/android/branding/beta/content/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file
diff --git a/mobile/android/branding/beta/locales/en-US/brand.dtd b/mobile/android/branding/beta/locales/en-US/brand.dtd
new file mode 100644
index 0000000000..4eb8febf0b
--- /dev/null
+++ b/mobile/android/branding/beta/locales/en-US/brand.dtd
@@ -0,0 +1,7 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY brandShortName "Firefox Beta">
+<!ENTITY brandFullName "Mozilla Firefox Beta">
+<!ENTITY vendorShortName "Mozilla"> \ No newline at end of file
diff --git a/mobile/android/branding/beta/locales/en-US/brand.properties b/mobile/android/branding/beta/locales/en-US/brand.properties
new file mode 100644
index 0000000000..8d8a8b8e3e
--- /dev/null
+++ b/mobile/android/branding/beta/locales/en-US/brand.properties
@@ -0,0 +1,6 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+brandShortName=Firefox Beta
+brandFullName=Mozilla Firefox Beta
diff --git a/mobile/android/branding/beta/locales/jar.mn b/mobile/android/branding/beta/locales/jar.mn
new file mode 100644
index 0000000000..2ea47e1684
--- /dev/null
+++ b/mobile/android/branding/beta/locales/jar.mn
@@ -0,0 +1,11 @@
+#filter substitution
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+@AB_CD@.jar:
+% locale branding @AB_CD@ %locale/branding/
+# Branding only exists in en-US
+ locale/branding/brand.dtd (en-US/brand.dtd)
+ locale/branding/brand.properties (en-US/brand.properties)
diff --git a/mobile/android/branding/beta/locales/moz.build b/mobile/android/branding/beta/locales/moz.build
new file mode 100644
index 0000000000..eb4454d28f
--- /dev/null
+++ b/mobile/android/branding/beta/locales/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file
diff --git a/mobile/android/branding/beta/moz.build b/mobile/android/branding/beta/moz.build
new file mode 100644
index 0000000000..04084b1ace
--- /dev/null
+++ b/mobile/android/branding/beta/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += ['content', 'locales']
diff --git a/mobile/android/branding/beta/res/drawable-hdpi/icon.png b/mobile/android/branding/beta/res/drawable-hdpi/icon.png
new file mode 100644
index 0000000000..fc450d0fdd
--- /dev/null
+++ b/mobile/android/branding/beta/res/drawable-hdpi/icon.png
Binary files differ
diff --git a/mobile/android/branding/beta/res/drawable-hdpi/large_icon.png b/mobile/android/branding/beta/res/drawable-hdpi/large_icon.png
new file mode 100644
index 0000000000..8b7f313fbe
--- /dev/null
+++ b/mobile/android/branding/beta/res/drawable-hdpi/large_icon.png
Binary files differ
diff --git a/mobile/android/branding/beta/res/drawable-hdpi/launcher_widget.png b/mobile/android/branding/beta/res/drawable-hdpi/launcher_widget.png
new file mode 100644
index 0000000000..ba8ba8eed0
--- /dev/null
+++ b/mobile/android/branding/beta/res/drawable-hdpi/launcher_widget.png
Binary files differ
diff --git a/mobile/android/branding/beta/res/drawable-hdpi/widget_icon.png b/mobile/android/branding/beta/res/drawable-hdpi/widget_icon.png
new file mode 100644
index 0000000000..8c60639e21
--- /dev/null
+++ b/mobile/android/branding/beta/res/drawable-hdpi/widget_icon.png
Binary files differ
diff --git a/mobile/android/branding/beta/res/drawable-xhdpi/icon.png b/mobile/android/branding/beta/res/drawable-xhdpi/icon.png
new file mode 100644
index 0000000000..7c2af7a0cf
--- /dev/null
+++ b/mobile/android/branding/beta/res/drawable-xhdpi/icon.png
Binary files differ
diff --git a/mobile/android/branding/beta/res/drawable-xhdpi/large_icon.png b/mobile/android/branding/beta/res/drawable-xhdpi/large_icon.png
new file mode 100644
index 0000000000..638d6b5d7a
--- /dev/null
+++ b/mobile/android/branding/beta/res/drawable-xhdpi/large_icon.png
Binary files differ
diff --git a/mobile/android/branding/beta/res/drawable-xhdpi/launcher_widget.png b/mobile/android/branding/beta/res/drawable-xhdpi/launcher_widget.png
new file mode 100644
index 0000000000..d15d5b91ea
--- /dev/null
+++ b/mobile/android/branding/beta/res/drawable-xhdpi/launcher_widget.png
Binary files differ
diff --git a/mobile/android/branding/beta/res/drawable-xhdpi/widget_icon.png b/mobile/android/branding/beta/res/drawable-xhdpi/widget_icon.png
new file mode 100644
index 0000000000..5ec6b038e7
--- /dev/null
+++ b/mobile/android/branding/beta/res/drawable-xhdpi/widget_icon.png
Binary files differ
diff --git a/mobile/android/branding/beta/res/drawable-xxhdpi/icon.png b/mobile/android/branding/beta/res/drawable-xxhdpi/icon.png
new file mode 100644
index 0000000000..8b7f313fbe
--- /dev/null
+++ b/mobile/android/branding/beta/res/drawable-xxhdpi/icon.png
Binary files differ
diff --git a/mobile/android/branding/beta/res/drawable-xxhdpi/large_icon.png b/mobile/android/branding/beta/res/drawable-xxhdpi/large_icon.png
new file mode 100644
index 0000000000..6a25ee0f0b
--- /dev/null
+++ b/mobile/android/branding/beta/res/drawable-xxhdpi/large_icon.png
Binary files differ
diff --git a/mobile/android/branding/beta/res/drawable-xxhdpi/launcher_widget.png b/mobile/android/branding/beta/res/drawable-xxhdpi/launcher_widget.png
new file mode 100644
index 0000000000..df75a5b2f7
--- /dev/null
+++ b/mobile/android/branding/beta/res/drawable-xxhdpi/launcher_widget.png
Binary files differ
diff --git a/mobile/android/branding/beta/res/drawable-xxhdpi/widget_icon.png b/mobile/android/branding/beta/res/drawable-xxhdpi/widget_icon.png
new file mode 100644
index 0000000000..c4d71316e0
--- /dev/null
+++ b/mobile/android/branding/beta/res/drawable-xxhdpi/widget_icon.png
Binary files differ
diff --git a/mobile/android/branding/beta/res/drawable-xxxhdpi/icon.png b/mobile/android/branding/beta/res/drawable-xxxhdpi/icon.png
new file mode 100644
index 0000000000..57c81f22cb
--- /dev/null
+++ b/mobile/android/branding/beta/res/drawable-xxxhdpi/icon.png
Binary files differ
diff --git a/mobile/android/branding/nightly/configure.sh b/mobile/android/branding/nightly/configure.sh
new file mode 100644
index 0000000000..399de4c88f
--- /dev/null
+++ b/mobile/android/branding/nightly/configure.sh
@@ -0,0 +1,9 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MOZ_APP_DISPLAYNAME=Nightly
+MOZ_UPDATER=1
+MOZ_ANDROID_ANR_REPORTER=1
+MOZ_ANDROID_SHARED_ID=org.mozilla.fennec.sharedID
+MOZ_ANDROID_GCM_SENDERID=965234145045
diff --git a/mobile/android/branding/nightly/content/about.png b/mobile/android/branding/nightly/content/about.png
new file mode 100644
index 0000000000..d6602ec625
--- /dev/null
+++ b/mobile/android/branding/nightly/content/about.png
Binary files differ
diff --git a/mobile/android/branding/nightly/content/favicon32.png b/mobile/android/branding/nightly/content/favicon32.png
new file mode 100644
index 0000000000..6cf3db69a4
--- /dev/null
+++ b/mobile/android/branding/nightly/content/favicon32.png
Binary files differ
diff --git a/mobile/android/branding/nightly/content/favicon64.png b/mobile/android/branding/nightly/content/favicon64.png
new file mode 100644
index 0000000000..ab340837ee
--- /dev/null
+++ b/mobile/android/branding/nightly/content/favicon64.png
Binary files differ
diff --git a/mobile/android/branding/nightly/content/jar.mn b/mobile/android/branding/nightly/content/jar.mn
new file mode 100644
index 0000000000..e49cc40416
--- /dev/null
+++ b/mobile/android/branding/nightly/content/jar.mn
@@ -0,0 +1,9 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+chrome.jar:
+% content branding %content/branding/ contentaccessible=yes
+ content/branding/about.png (about.png)
+ content/branding/favicon32.png (favicon32.png)
+ content/branding/favicon64.png (favicon64.png)
diff --git a/mobile/android/branding/nightly/content/moz.build b/mobile/android/branding/nightly/content/moz.build
new file mode 100644
index 0000000000..eb4454d28f
--- /dev/null
+++ b/mobile/android/branding/nightly/content/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file
diff --git a/mobile/android/branding/nightly/locales/en-US/brand.dtd b/mobile/android/branding/nightly/locales/en-US/brand.dtd
new file mode 100644
index 0000000000..3d3a3a10fb
--- /dev/null
+++ b/mobile/android/branding/nightly/locales/en-US/brand.dtd
@@ -0,0 +1,7 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY brandShortName "Nightly">
+<!ENTITY brandFullName "Mozilla Nightly">
+<!ENTITY vendorShortName "Mozilla"> \ No newline at end of file
diff --git a/mobile/android/branding/nightly/locales/en-US/brand.properties b/mobile/android/branding/nightly/locales/en-US/brand.properties
new file mode 100644
index 0000000000..d060536147
--- /dev/null
+++ b/mobile/android/branding/nightly/locales/en-US/brand.properties
@@ -0,0 +1,6 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+brandShortName=Nightly
+brandFullName=Mozilla Nightly
diff --git a/mobile/android/branding/nightly/locales/jar.mn b/mobile/android/branding/nightly/locales/jar.mn
new file mode 100644
index 0000000000..5a77695c91
--- /dev/null
+++ b/mobile/android/branding/nightly/locales/jar.mn
@@ -0,0 +1,11 @@
+#filter substitution
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+@AB_CD@.jar:
+% locale branding @AB_CD@ %locale/branding/
+# Nightly branding only exists in en-US
+ locale/branding/brand.dtd (en-US/brand.dtd)
+ locale/branding/brand.properties (en-US/brand.properties)
diff --git a/mobile/android/branding/nightly/locales/moz.build b/mobile/android/branding/nightly/locales/moz.build
new file mode 100644
index 0000000000..eb4454d28f
--- /dev/null
+++ b/mobile/android/branding/nightly/locales/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file
diff --git a/mobile/android/branding/nightly/moz.build b/mobile/android/branding/nightly/moz.build
new file mode 100644
index 0000000000..04084b1ace
--- /dev/null
+++ b/mobile/android/branding/nightly/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += ['content', 'locales']
diff --git a/mobile/android/branding/nightly/res/drawable-hdpi/icon.png b/mobile/android/branding/nightly/res/drawable-hdpi/icon.png
new file mode 100644
index 0000000000..bb87f783b7
--- /dev/null
+++ b/mobile/android/branding/nightly/res/drawable-hdpi/icon.png
Binary files differ
diff --git a/mobile/android/branding/nightly/res/drawable-hdpi/large_icon.png b/mobile/android/branding/nightly/res/drawable-hdpi/large_icon.png
new file mode 100644
index 0000000000..0907bbcf1e
--- /dev/null
+++ b/mobile/android/branding/nightly/res/drawable-hdpi/large_icon.png
Binary files differ
diff --git a/mobile/android/branding/nightly/res/drawable-hdpi/launcher_widget.png b/mobile/android/branding/nightly/res/drawable-hdpi/launcher_widget.png
new file mode 100644
index 0000000000..8e55b0c113
--- /dev/null
+++ b/mobile/android/branding/nightly/res/drawable-hdpi/launcher_widget.png
Binary files differ
diff --git a/mobile/android/branding/nightly/res/drawable-hdpi/widget_icon.png b/mobile/android/branding/nightly/res/drawable-hdpi/widget_icon.png
new file mode 100644
index 0000000000..f46e29c755
--- /dev/null
+++ b/mobile/android/branding/nightly/res/drawable-hdpi/widget_icon.png
Binary files differ
diff --git a/mobile/android/branding/nightly/res/drawable-xhdpi/icon.png b/mobile/android/branding/nightly/res/drawable-xhdpi/icon.png
new file mode 100644
index 0000000000..bb78ad6238
--- /dev/null
+++ b/mobile/android/branding/nightly/res/drawable-xhdpi/icon.png
Binary files differ
diff --git a/mobile/android/branding/nightly/res/drawable-xhdpi/large_icon.png b/mobile/android/branding/nightly/res/drawable-xhdpi/large_icon.png
new file mode 100644
index 0000000000..3a1f003ba6
--- /dev/null
+++ b/mobile/android/branding/nightly/res/drawable-xhdpi/large_icon.png
Binary files differ
diff --git a/mobile/android/branding/nightly/res/drawable-xhdpi/launcher_widget.png b/mobile/android/branding/nightly/res/drawable-xhdpi/launcher_widget.png
new file mode 100644
index 0000000000..56f47446a6
--- /dev/null
+++ b/mobile/android/branding/nightly/res/drawable-xhdpi/launcher_widget.png
Binary files differ
diff --git a/mobile/android/branding/nightly/res/drawable-xhdpi/widget_icon.png b/mobile/android/branding/nightly/res/drawable-xhdpi/widget_icon.png
new file mode 100644
index 0000000000..4413e0c781
--- /dev/null
+++ b/mobile/android/branding/nightly/res/drawable-xhdpi/widget_icon.png
Binary files differ
diff --git a/mobile/android/branding/nightly/res/drawable-xxhdpi/icon.png b/mobile/android/branding/nightly/res/drawable-xxhdpi/icon.png
new file mode 100644
index 0000000000..92c3df0369
--- /dev/null
+++ b/mobile/android/branding/nightly/res/drawable-xxhdpi/icon.png
Binary files differ
diff --git a/mobile/android/branding/nightly/res/drawable-xxhdpi/large_icon.png b/mobile/android/branding/nightly/res/drawable-xxhdpi/large_icon.png
new file mode 100644
index 0000000000..cae9ea2891
--- /dev/null
+++ b/mobile/android/branding/nightly/res/drawable-xxhdpi/large_icon.png
Binary files differ
diff --git a/mobile/android/branding/nightly/res/drawable-xxhdpi/launcher_widget.png b/mobile/android/branding/nightly/res/drawable-xxhdpi/launcher_widget.png
new file mode 100644
index 0000000000..eb88984dbf
--- /dev/null
+++ b/mobile/android/branding/nightly/res/drawable-xxhdpi/launcher_widget.png
Binary files differ
diff --git a/mobile/android/branding/nightly/res/drawable-xxhdpi/widget_icon.png b/mobile/android/branding/nightly/res/drawable-xxhdpi/widget_icon.png
new file mode 100644
index 0000000000..43b679b655
--- /dev/null
+++ b/mobile/android/branding/nightly/res/drawable-xxhdpi/widget_icon.png
Binary files differ
diff --git a/mobile/android/branding/nightly/res/drawable-xxxhdpi/icon.png b/mobile/android/branding/nightly/res/drawable-xxxhdpi/icon.png
new file mode 100644
index 0000000000..abd62f1fcb
--- /dev/null
+++ b/mobile/android/branding/nightly/res/drawable-xxxhdpi/icon.png
Binary files differ
diff --git a/mobile/android/branding/official/configure.sh b/mobile/android/branding/official/configure.sh
new file mode 100644
index 0000000000..b36e083f9d
--- /dev/null
+++ b/mobile/android/branding/official/configure.sh
@@ -0,0 +1,10 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MOZ_APP_DISPLAYNAME=Firefox
+ANDROID_PACKAGE_NAME=org.mozilla.firefox
+MOZ_UPDATER=
+MOZ_ANDROID_ANR_REPORTER=
+MOZ_ANDROID_SHARED_ID=org.mozilla.firefox.sharedID
+MOZ_ANDROID_GCM_SENDERID=965234145045
diff --git a/mobile/android/branding/official/content/about.png b/mobile/android/branding/official/content/about.png
new file mode 100644
index 0000000000..dc52e843bd
--- /dev/null
+++ b/mobile/android/branding/official/content/about.png
Binary files differ
diff --git a/mobile/android/branding/official/content/favicon32.png b/mobile/android/branding/official/content/favicon32.png
new file mode 100644
index 0000000000..63b54a2473
--- /dev/null
+++ b/mobile/android/branding/official/content/favicon32.png
Binary files differ
diff --git a/mobile/android/branding/official/content/favicon64.png b/mobile/android/branding/official/content/favicon64.png
new file mode 100644
index 0000000000..e11693d6c6
--- /dev/null
+++ b/mobile/android/branding/official/content/favicon64.png
Binary files differ
diff --git a/mobile/android/branding/official/content/jar.mn b/mobile/android/branding/official/content/jar.mn
new file mode 100644
index 0000000000..e49cc40416
--- /dev/null
+++ b/mobile/android/branding/official/content/jar.mn
@@ -0,0 +1,9 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+chrome.jar:
+% content branding %content/branding/ contentaccessible=yes
+ content/branding/about.png (about.png)
+ content/branding/favicon32.png (favicon32.png)
+ content/branding/favicon64.png (favicon64.png)
diff --git a/mobile/android/branding/official/content/moz.build b/mobile/android/branding/official/content/moz.build
new file mode 100644
index 0000000000..eb4454d28f
--- /dev/null
+++ b/mobile/android/branding/official/content/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file
diff --git a/mobile/android/branding/official/locales/en-US/brand.dtd b/mobile/android/branding/official/locales/en-US/brand.dtd
new file mode 100644
index 0000000000..b2b28a49aa
--- /dev/null
+++ b/mobile/android/branding/official/locales/en-US/brand.dtd
@@ -0,0 +1,7 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY brandShortName "Firefox">
+<!ENTITY brandFullName "Mozilla Firefox">
+<!ENTITY vendorShortName "Mozilla"> \ No newline at end of file
diff --git a/mobile/android/branding/official/locales/en-US/brand.properties b/mobile/android/branding/official/locales/en-US/brand.properties
new file mode 100644
index 0000000000..d0203e35a4
--- /dev/null
+++ b/mobile/android/branding/official/locales/en-US/brand.properties
@@ -0,0 +1,6 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+brandShortName=Firefox
+brandFullName=Mozilla Firefox
diff --git a/mobile/android/branding/official/locales/jar.mn b/mobile/android/branding/official/locales/jar.mn
new file mode 100644
index 0000000000..2ea47e1684
--- /dev/null
+++ b/mobile/android/branding/official/locales/jar.mn
@@ -0,0 +1,11 @@
+#filter substitution
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+@AB_CD@.jar:
+% locale branding @AB_CD@ %locale/branding/
+# Branding only exists in en-US
+ locale/branding/brand.dtd (en-US/brand.dtd)
+ locale/branding/brand.properties (en-US/brand.properties)
diff --git a/mobile/android/branding/official/locales/moz.build b/mobile/android/branding/official/locales/moz.build
new file mode 100644
index 0000000000..eb4454d28f
--- /dev/null
+++ b/mobile/android/branding/official/locales/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file
diff --git a/mobile/android/branding/official/moz.build b/mobile/android/branding/official/moz.build
new file mode 100644
index 0000000000..04084b1ace
--- /dev/null
+++ b/mobile/android/branding/official/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += ['content', 'locales']
diff --git a/mobile/android/branding/official/res/drawable-hdpi/icon.png b/mobile/android/branding/official/res/drawable-hdpi/icon.png
new file mode 100644
index 0000000000..de9052a107
--- /dev/null
+++ b/mobile/android/branding/official/res/drawable-hdpi/icon.png
Binary files differ
diff --git a/mobile/android/branding/official/res/drawable-hdpi/large_icon.png b/mobile/android/branding/official/res/drawable-hdpi/large_icon.png
new file mode 100644
index 0000000000..b14cd3d810
--- /dev/null
+++ b/mobile/android/branding/official/res/drawable-hdpi/large_icon.png
Binary files differ
diff --git a/mobile/android/branding/official/res/drawable-hdpi/launcher_widget.png b/mobile/android/branding/official/res/drawable-hdpi/launcher_widget.png
new file mode 100644
index 0000000000..2528f4b350
--- /dev/null
+++ b/mobile/android/branding/official/res/drawable-hdpi/launcher_widget.png
Binary files differ
diff --git a/mobile/android/branding/official/res/drawable-hdpi/widget_icon.png b/mobile/android/branding/official/res/drawable-hdpi/widget_icon.png
new file mode 100644
index 0000000000..02c88369bb
--- /dev/null
+++ b/mobile/android/branding/official/res/drawable-hdpi/widget_icon.png
Binary files differ
diff --git a/mobile/android/branding/official/res/drawable-xhdpi/icon.png b/mobile/android/branding/official/res/drawable-xhdpi/icon.png
new file mode 100644
index 0000000000..baee75b77e
--- /dev/null
+++ b/mobile/android/branding/official/res/drawable-xhdpi/icon.png
Binary files differ
diff --git a/mobile/android/branding/official/res/drawable-xhdpi/large_icon.png b/mobile/android/branding/official/res/drawable-xhdpi/large_icon.png
new file mode 100644
index 0000000000..20a8c28af5
--- /dev/null
+++ b/mobile/android/branding/official/res/drawable-xhdpi/large_icon.png
Binary files differ
diff --git a/mobile/android/branding/official/res/drawable-xhdpi/launcher_widget.png b/mobile/android/branding/official/res/drawable-xhdpi/launcher_widget.png
new file mode 100644
index 0000000000..a4d8bdaf57
--- /dev/null
+++ b/mobile/android/branding/official/res/drawable-xhdpi/launcher_widget.png
Binary files differ
diff --git a/mobile/android/branding/official/res/drawable-xhdpi/widget_icon.png b/mobile/android/branding/official/res/drawable-xhdpi/widget_icon.png
new file mode 100644
index 0000000000..66b933d634
--- /dev/null
+++ b/mobile/android/branding/official/res/drawable-xhdpi/widget_icon.png
Binary files differ
diff --git a/mobile/android/branding/official/res/drawable-xxhdpi/icon.png b/mobile/android/branding/official/res/drawable-xxhdpi/icon.png
new file mode 100644
index 0000000000..b14cd3d810
--- /dev/null
+++ b/mobile/android/branding/official/res/drawable-xxhdpi/icon.png
Binary files differ
diff --git a/mobile/android/branding/official/res/drawable-xxhdpi/large_icon.png b/mobile/android/branding/official/res/drawable-xxhdpi/large_icon.png
new file mode 100644
index 0000000000..a37b9a83df
--- /dev/null
+++ b/mobile/android/branding/official/res/drawable-xxhdpi/large_icon.png
Binary files differ
diff --git a/mobile/android/branding/official/res/drawable-xxhdpi/launcher_widget.png b/mobile/android/branding/official/res/drawable-xxhdpi/launcher_widget.png
new file mode 100644
index 0000000000..7a5470cb49
--- /dev/null
+++ b/mobile/android/branding/official/res/drawable-xxhdpi/launcher_widget.png
Binary files differ
diff --git a/mobile/android/branding/official/res/drawable-xxhdpi/widget_icon.png b/mobile/android/branding/official/res/drawable-xxhdpi/widget_icon.png
new file mode 100644
index 0000000000..843ce03a04
--- /dev/null
+++ b/mobile/android/branding/official/res/drawable-xxhdpi/widget_icon.png
Binary files differ
diff --git a/mobile/android/branding/official/res/drawable-xxxhdpi/icon.png b/mobile/android/branding/official/res/drawable-xxxhdpi/icon.png
new file mode 100644
index 0000000000..659a33ede9
--- /dev/null
+++ b/mobile/android/branding/official/res/drawable-xxxhdpi/icon.png
Binary files differ
diff --git a/mobile/android/branding/unofficial/configure.sh b/mobile/android/branding/unofficial/configure.sh
new file mode 100644
index 0000000000..0920151a53
--- /dev/null
+++ b/mobile/android/branding/unofficial/configure.sh
@@ -0,0 +1,9 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+ANDROID_PACKAGE_NAME=org.mozilla.fennec_`echo $USER | sed 's/-/_/g'`
+MOZ_APP_DISPLAYNAME="Fennec `echo $USER | sed 's/-/_/g'`"
+MOZ_UPDATER=
+MOZ_ANDROID_ANR_REPORTER=
+MOZ_ANDROID_GCM_SENDERID=965234145045
diff --git a/mobile/android/branding/unofficial/content/about.png b/mobile/android/branding/unofficial/content/about.png
new file mode 100644
index 0000000000..e3f697e8d5
--- /dev/null
+++ b/mobile/android/branding/unofficial/content/about.png
Binary files differ
diff --git a/mobile/android/branding/unofficial/content/favicon32.png b/mobile/android/branding/unofficial/content/favicon32.png
new file mode 100644
index 0000000000..c43d5a2295
--- /dev/null
+++ b/mobile/android/branding/unofficial/content/favicon32.png
Binary files differ
diff --git a/mobile/android/branding/unofficial/content/favicon64.png b/mobile/android/branding/unofficial/content/favicon64.png
new file mode 100644
index 0000000000..895f60c15b
--- /dev/null
+++ b/mobile/android/branding/unofficial/content/favicon64.png
Binary files differ
diff --git a/mobile/android/branding/unofficial/content/jar.mn b/mobile/android/branding/unofficial/content/jar.mn
new file mode 100644
index 0000000000..e49cc40416
--- /dev/null
+++ b/mobile/android/branding/unofficial/content/jar.mn
@@ -0,0 +1,9 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+chrome.jar:
+% content branding %content/branding/ contentaccessible=yes
+ content/branding/about.png (about.png)
+ content/branding/favicon32.png (favicon32.png)
+ content/branding/favicon64.png (favicon64.png)
diff --git a/mobile/android/branding/unofficial/content/moz.build b/mobile/android/branding/unofficial/content/moz.build
new file mode 100644
index 0000000000..eb4454d28f
--- /dev/null
+++ b/mobile/android/branding/unofficial/content/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file
diff --git a/mobile/android/branding/unofficial/locales/en-US/brand.dtd b/mobile/android/branding/unofficial/locales/en-US/brand.dtd
new file mode 100644
index 0000000000..3d998d15c6
--- /dev/null
+++ b/mobile/android/branding/unofficial/locales/en-US/brand.dtd
@@ -0,0 +1,7 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY brandShortName "Fennec">
+<!ENTITY brandFullName "Mozilla Fennec">
+<!ENTITY vendorShortName "Mozilla"> \ No newline at end of file
diff --git a/mobile/android/branding/unofficial/locales/en-US/brand.properties b/mobile/android/branding/unofficial/locales/en-US/brand.properties
new file mode 100644
index 0000000000..9cedd01afc
--- /dev/null
+++ b/mobile/android/branding/unofficial/locales/en-US/brand.properties
@@ -0,0 +1,6 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+brandShortName=Fennec
+brandFullName=Mozilla Fennec
diff --git a/mobile/android/branding/unofficial/locales/jar.mn b/mobile/android/branding/unofficial/locales/jar.mn
new file mode 100644
index 0000000000..5a77695c91
--- /dev/null
+++ b/mobile/android/branding/unofficial/locales/jar.mn
@@ -0,0 +1,11 @@
+#filter substitution
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+@AB_CD@.jar:
+% locale branding @AB_CD@ %locale/branding/
+# Nightly branding only exists in en-US
+ locale/branding/brand.dtd (en-US/brand.dtd)
+ locale/branding/brand.properties (en-US/brand.properties)
diff --git a/mobile/android/branding/unofficial/locales/moz.build b/mobile/android/branding/unofficial/locales/moz.build
new file mode 100644
index 0000000000..eb4454d28f
--- /dev/null
+++ b/mobile/android/branding/unofficial/locales/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file
diff --git a/mobile/android/branding/unofficial/moz.build b/mobile/android/branding/unofficial/moz.build
new file mode 100644
index 0000000000..04084b1ace
--- /dev/null
+++ b/mobile/android/branding/unofficial/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += ['content', 'locales']
diff --git a/mobile/android/branding/unofficial/res/drawable-hdpi/icon.png b/mobile/android/branding/unofficial/res/drawable-hdpi/icon.png
new file mode 100644
index 0000000000..786894f770
--- /dev/null
+++ b/mobile/android/branding/unofficial/res/drawable-hdpi/icon.png
Binary files differ
diff --git a/mobile/android/branding/unofficial/res/drawable-hdpi/large_icon.png b/mobile/android/branding/unofficial/res/drawable-hdpi/large_icon.png
new file mode 100644
index 0000000000..f596042266
--- /dev/null
+++ b/mobile/android/branding/unofficial/res/drawable-hdpi/large_icon.png
Binary files differ
diff --git a/mobile/android/branding/unofficial/res/drawable-hdpi/launcher_widget.png b/mobile/android/branding/unofficial/res/drawable-hdpi/launcher_widget.png
new file mode 100644
index 0000000000..8e55b0c113
--- /dev/null
+++ b/mobile/android/branding/unofficial/res/drawable-hdpi/launcher_widget.png
Binary files differ
diff --git a/mobile/android/branding/unofficial/res/drawable-xhdpi/icon.png b/mobile/android/branding/unofficial/res/drawable-xhdpi/icon.png
new file mode 100644
index 0000000000..7ebffe3a51
--- /dev/null
+++ b/mobile/android/branding/unofficial/res/drawable-xhdpi/icon.png
Binary files differ
diff --git a/mobile/android/branding/unofficial/res/drawable-xhdpi/large_icon.png b/mobile/android/branding/unofficial/res/drawable-xhdpi/large_icon.png
new file mode 100644
index 0000000000..f596042266
--- /dev/null
+++ b/mobile/android/branding/unofficial/res/drawable-xhdpi/large_icon.png
Binary files differ
diff --git a/mobile/android/branding/unofficial/res/drawable-xhdpi/launcher_widget.png b/mobile/android/branding/unofficial/res/drawable-xhdpi/launcher_widget.png
new file mode 100644
index 0000000000..56f47446a6
--- /dev/null
+++ b/mobile/android/branding/unofficial/res/drawable-xhdpi/launcher_widget.png
Binary files differ
diff --git a/mobile/android/branding/unofficial/res/drawable-xhdpi/widget_icon.png b/mobile/android/branding/unofficial/res/drawable-xhdpi/widget_icon.png
new file mode 100644
index 0000000000..d5bcf96979
--- /dev/null
+++ b/mobile/android/branding/unofficial/res/drawable-xhdpi/widget_icon.png
Binary files differ
diff --git a/mobile/android/branding/unofficial/res/drawable-xxhdpi/icon.png b/mobile/android/branding/unofficial/res/drawable-xxhdpi/icon.png
new file mode 100644
index 0000000000..f596042266
--- /dev/null
+++ b/mobile/android/branding/unofficial/res/drawable-xxhdpi/icon.png
Binary files differ
diff --git a/mobile/android/branding/unofficial/res/drawable-xxhdpi/large_icon.png b/mobile/android/branding/unofficial/res/drawable-xxhdpi/large_icon.png
new file mode 100644
index 0000000000..f596042266
--- /dev/null
+++ b/mobile/android/branding/unofficial/res/drawable-xxhdpi/large_icon.png
Binary files differ
diff --git a/mobile/android/branding/unofficial/res/drawable-xxhdpi/launcher_widget.png b/mobile/android/branding/unofficial/res/drawable-xxhdpi/launcher_widget.png
new file mode 100644
index 0000000000..eb88984dbf
--- /dev/null
+++ b/mobile/android/branding/unofficial/res/drawable-xxhdpi/launcher_widget.png
Binary files differ
diff --git a/mobile/android/build.mk b/mobile/android/build.mk
new file mode 100644
index 0000000000..a5d7d8eb27
--- /dev/null
+++ b/mobile/android/build.mk
@@ -0,0 +1,66 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+include $(topsrcdir)/toolkit/mozapps/installer/package-name.mk
+
+installer:
+ @$(MAKE) -C mobile/android/installer installer
+
+package:
+ @$(MAKE) -C mobile/android/installer
+
+ifeq ($(OS_TARGET),Android)
+ifneq ($(MOZ_ANDROID_INSTALL_TARGET),)
+ANDROID_SERIAL = $(MOZ_ANDROID_INSTALL_TARGET)
+endif
+ifneq ($(ANDROID_SERIAL),)
+export ANDROID_SERIAL
+else
+# Determine if there's more than one device connected
+android_devices=$(words $(filter device,$(shell $(ADB) devices)))
+define no_device
+ @echo 'No devices are connected. Connect a device or start an emulator.'
+ @exit 1
+endef
+define multiple_devices
+ @echo 'Multiple devices are connected. Define ANDROID_SERIAL to specify the install target.'
+ $(ADB) devices
+ @exit 1
+endef
+
+install::
+ @# Use foreach to avoid running adb multiple times here
+ $(foreach val,$(android_devices),\
+ $(if $(filter 0,$(val)),$(no_device),\
+ $(if $(filter-out 1,$(val)),$(multiple_devices))))
+endif
+
+install::
+ $(ADB) install -r $(DIST)/$(PKG_PATH)$(PKG_BASENAME).apk
+else
+ @echo 'Mobile can't be installed directly.'
+ @exit 1
+endif
+
+deb: package
+ @$(MAKE) -C mobile/android/installer deb
+
+upload::
+ @$(MAKE) -C mobile/android/installer upload
+
+ifdef ENABLE_TESTS
+# Implemented in testing/testsuite-targets.mk
+
+mochitest-browser-chrome:
+ $(RUN_MOCHITEST) --flavor=browser
+ $(CHECK_TEST_ERROR)
+
+mochitest:: mochitest-browser-chrome
+
+.PHONY: mochitest-browser-chrome
+endif
+
+ifeq ($(OS_TARGET),Linux)
+deb: installer
+endif
diff --git a/mobile/android/build/classycle/LICENSE.txt b/mobile/android/build/classycle/LICENSE.txt
new file mode 100644
index 0000000000..c50c581044
--- /dev/null
+++ b/mobile/android/build/classycle/LICENSE.txt
@@ -0,0 +1,22 @@
+Copyright (c) 2003-2008, Franz-Josef Elmer, All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+- Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+- Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/mobile/android/build/classycle/classycle-1.4.1.jar b/mobile/android/build/classycle/classycle-1.4.1.jar
new file mode 100644
index 0000000000..bc8f2ddaac
--- /dev/null
+++ b/mobile/android/build/classycle/classycle-1.4.1.jar
Binary files differ
diff --git a/mobile/android/chrome/content/.eslintrc b/mobile/android/chrome/content/.eslintrc
new file mode 100644
index 0000000000..32513189ae
--- /dev/null
+++ b/mobile/android/chrome/content/.eslintrc
@@ -0,0 +1,23 @@
+globals:
+ # TODO: Maybe this should be by file
+ BrowserApp: false
+ Cc: false
+ Ci: false
+ Cu: false
+ NativeWindow: false
+ PageActions: false
+ ReaderMode: false
+ SimpleServiceDiscovery: false
+ TabMirror: false
+ MediaPlayerApp: false
+ RokuApp: false
+ SearchEngines: false
+ ConsoleAPI: true
+ Point: false
+ Rect: false
+
+rules:
+ # Disabled stuff
+ no-console: 0 # TODO: Can we use console?
+ no-cond-assign: 0
+ no-fallthrough: 0
diff --git a/mobile/android/chrome/content/ActionBarHandler.js b/mobile/android/chrome/content/ActionBarHandler.js
new file mode 100644
index 0000000000..1900210432
--- /dev/null
+++ b/mobile/android/chrome/content/ActionBarHandler.js
@@ -0,0 +1,731 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+
+const PHONE_REGEX = /^\+?[0-9\s,-.\(\)*#pw]{1,30}$/; // Are we a phone #?
+
+
+/**
+ * ActionBarHandler Object and methods. Interface between Gecko Text Selection code
+ * (AccessibleCaret, etc) and the Mobile ActionBar UI.
+ */
+var ActionBarHandler = {
+ // Error codes returned from _init().
+ START_TOUCH_ERROR: {
+ NO_CONTENT_WINDOW: "No valid content Window found.",
+ NONE: "",
+ },
+
+ _nextSelectionID: 1, // Next available.
+ _selectionID: null, // Unique Selection ID, assigned each time we _init().
+
+ _boundingClientRect: null, // Current selections boundingClientRect.
+ _actionBarActions: null, // Most-recent set of actions sent to ActionBar.
+
+ /**
+ * Receive and act on AccessibleCarets caret state-change
+ * (mozcaretstatechanged) events.
+ */
+ caretStateChangedHandler: function(e) {
+ // Close an open ActionBar, if carets no longer logically visible.
+ if (this._selectionID && !e.caretVisible) {
+ this._uninit(false);
+ return;
+ }
+
+ if (!this._selectionID && e.collapsed) {
+ switch (e.reason) {
+ case 'longpressonemptycontent':
+ case 'taponcaret':
+ // Show ActionBar when long pressing on an empty input or single
+ // tapping on the caret.
+ this._init(e.boundingClientRect);
+ break;
+
+ case 'updateposition':
+ // Do not show ActionBar when single tapping on an non-empty editable
+ // input.
+ break;
+
+ default:
+ break;
+ }
+ return;
+ }
+
+ // Open a closed ActionBar if carets actually visible.
+ if (!this._selectionID && e.caretVisuallyVisible) {
+ this._init(e.boundingClientRect);
+ return;
+ }
+
+ // Else, update an open ActionBar.
+ if (this._selectionID) {
+ if (!this._selectionHasChanged()) {
+ // Still the same active selection.
+ if (e.reason == 'presscaret' || e.reason == 'scroll') {
+ // boundingClientRect doesn't matter since we are hiding the floating
+ // toolbar.
+ this._updateVisibility();
+ } else {
+ // Selection changes update boundingClientRect.
+ this._boundingClientRect = e.boundingClientRect;
+ let forceUpdate = e.reason == 'updateposition' || e.reason == 'releasecaret';
+ this._sendActionBarActions(forceUpdate);
+ }
+ } else {
+ // We've started a new selection entirely.
+ this._uninit(false);
+ this._init(e.boundingClientRect);
+ }
+ }
+ },
+
+ /**
+ * ActionBarHandler notification observers.
+ */
+ observe: function(subject, topic, data) {
+ switch (topic) {
+ // User click an ActionBar button.
+ case "TextSelection:Action": {
+ if (!this._selectionID) {
+ break;
+ }
+ for (let type in this.actions) {
+ let action = this.actions[type];
+ if (action.id == data) {
+ action.action(this._targetElement, this._contentWindow);
+ break;
+ }
+ }
+ break;
+ }
+
+ // Provide selected text to FindInPageBar on request.
+ case "TextSelection:Get": {
+ Messaging.sendRequest({
+ type: "TextSelection:Data",
+ requestId: data,
+ text: this._getSelectedText(),
+ });
+
+ this._uninit();
+ break;
+ }
+
+ // User closed ActionBar by clicking "checkmark" button.
+ case "TextSelection:End": {
+ // End the requested selection only.
+ if (this._selectionID == JSON.parse(data).selectionID) {
+ this._uninit();
+ }
+ break;
+ }
+ }
+ },
+
+ /**
+ * Called when Gecko AccessibleCaret becomes visible.
+ */
+ _init: function(boundingClientRect) {
+ let [element, win] = this._getSelectionTargets();
+ if (!win) {
+ return this.START_TOUCH_ERROR.NO_CONTENT_WINDOW;
+ }
+
+ // Hold the ActionBar ID provided by Gecko.
+ this._selectionID = this._nextSelectionID++;
+ [this._targetElement, this._contentWindow] = [element, win];
+ this._boundingClientRect = boundingClientRect;
+
+ // Open the ActionBar, send it's actions list.
+ Messaging.sendRequest({
+ type: "TextSelection:ActionbarInit",
+ selectionID: this._selectionID,
+ });
+ this._sendActionBarActions(true);
+
+ return this.START_TOUCH_ERROR.NONE;
+ },
+
+ /**
+ * Called when content is scrolled and handles are hidden.
+ */
+ _updateVisibility: function() {
+ Messaging.sendRequest({
+ type: "TextSelection:Visibility",
+ selectionID: this._selectionID,
+ });
+ },
+
+ /**
+ * Determines the window containing the selection, and its
+ * editable element if present.
+ */
+ _getSelectionTargets: function() {
+ let [element, win] = [Services.focus.focusedElement, Services.focus.focusedWindow];
+ if (!element) {
+ // No focused editable.
+ return [null, win];
+ }
+
+ // Return focused editable text element and its window.
+ if (((element instanceof HTMLInputElement) && element.mozIsTextField(false)) ||
+ (element instanceof HTMLTextAreaElement) ||
+ element.isContentEditable) {
+ return [element, win];
+ }
+
+ // Focused element can't contain text.
+ return [null, win];
+ },
+
+ /**
+ * The active Selection has changed, if the current focused element / win,
+ * pair, or state of the win's designMode changes.
+ */
+ _selectionHasChanged: function() {
+ let [element, win] = this._getSelectionTargets();
+ return (this._targetElement !== element ||
+ this._contentWindow !== win ||
+ this._isInDesignMode(this._contentWindow) !== this._isInDesignMode(win));
+ },
+
+ /**
+ * Called when Gecko AccessibleCaret becomes hidden,
+ * ActionBar is closed by user "close" request, or as a result of object
+ * methods such as SELECT_ALL, PASTE, etc.
+ */
+ _uninit: function(clearSelection = true) {
+ // Bail if there's no active selection.
+ if (!this._selectionID) {
+ return;
+ }
+
+ // Close the ActionBar.
+ Messaging.sendRequest({
+ type: "TextSelection:ActionbarUninit",
+ });
+
+ // Clear the selection ID to complete the uninit(), but leave our reference
+ // to selectionTargets (_targetElement, _contentWindow) in case we need
+ // a final clearSelection().
+ this._selectionID = null;
+ this._boundingClientRect = null;
+
+ // Clear selection required if triggered by self, or TextSelection icon
+ // actions. If called by Gecko CaretStateChangedEvent,
+ // visibility state is already correct.
+ if (clearSelection) {
+ this._clearSelection();
+ }
+ },
+
+ /**
+ * Final UI cleanup when Actionbar is closed by icon click, or where
+ * we terminate selection state after before/after actionbar actions
+ * (Cut, Copy, Paste, Search, Share, Call).
+ */
+ _clearSelection: function(element = this._targetElement, win = this._contentWindow) {
+ // Commit edit compositions, and clear focus from editables.
+ if (element) {
+ let imeSupport = this._getEditor(element, win).QueryInterface(Ci.nsIEditorIMESupport);
+ if (imeSupport.composing) {
+ imeSupport.forceCompositionEnd();
+ }
+ element.blur();
+ }
+
+ // Remove Selection from non-editables and now-unfocused contentEditables.
+ if (!element || element.isContentEditable) {
+ this._getSelection().removeAllRanges();
+ }
+ },
+
+ /**
+ * Called to determine current ActionBar actions and send to TextSelection
+ * handler. By default we only send if current action state differs from
+ * the previous.
+ * @param By default we only send an ActionBarStatus update message if
+ * there is a change from the previous state. sendAlways can be
+ * set by init() for example, where we want to always send the
+ * current state.
+ */
+ _sendActionBarActions: function(sendAlways) {
+ let actions = this._getActionBarActions();
+
+ let actionCountUnchanged = this._actionBarActions &&
+ actions.length === this._actionBarActions.length;
+ let actionsMatch = actionCountUnchanged &&
+ this._actionBarActions.every((e,i) => {
+ return e.id === actions[i].id;
+ });
+
+ if (sendAlways || !actionsMatch) {
+ Messaging.sendRequest({
+ type: "TextSelection:ActionbarStatus",
+ selectionID: this._selectionID,
+ actions: actions,
+ x: this._boundingClientRect.x,
+ y: this._boundingClientRect.y,
+ width: this._boundingClientRect.width,
+ height: this._boundingClientRect.height
+ });
+ }
+
+ this._actionBarActions = actions;
+ },
+
+ /**
+ * Determine and return current ActionBar state.
+ */
+ _getActionBarActions: function(element = this._targetElement, win = this._contentWindow) {
+ let actions = [];
+
+ for (let type in this.actions) {
+ let action = this.actions[type];
+ if (action.selector.matches(element, win)) {
+ let a = {
+ id: action.id,
+ label: this._getActionValue(action, "label", "", element),
+ icon: this._getActionValue(action, "icon", "drawable://ic_status_logo", element),
+ order: this._getActionValue(action, "order", 0, element),
+ floatingOrder: this._getActionValue(action, "floatingOrder", 9, element),
+ showAsAction: this._getActionValue(action, "showAsAction", true, element),
+ };
+ actions.push(a);
+ }
+ }
+ actions.sort((a, b) => b.order - a.order);
+
+ return actions;
+ },
+
+ /**
+ * Provides a value from an action. If the action defines the value as a function,
+ * we return the result of calling the function. Otherwise, we return the value
+ * itself. If the value isn't defined for this action, will return a default.
+ */
+ _getActionValue: function(obj, name, defaultValue, element) {
+ if (!(name in obj))
+ return defaultValue;
+
+ if (typeof obj[name] == "function")
+ return obj[name](element);
+
+ return obj[name];
+ },
+
+ /**
+ * Actionbar callback methods.
+ */
+ actions: {
+
+ SELECT_ALL: {
+ id: "selectall_action",
+ label: Strings.browser.GetStringFromName("contextmenu.selectAll"),
+ icon: "drawable://ab_select_all",
+ order: 5,
+ floatingOrder: 5,
+
+ selector: {
+ matches: function(element, win) {
+ // For editable, check its length. For default contentWindow, assume
+ // true, else there'd been nothing to long-press to open ActionBar.
+ return (element) ? element.textLength != 0 : true;
+ },
+ },
+
+ action: function(element, win) {
+ // Some Mobile keyboards such as SwiftKeyboard, provide auto-suggest
+ // style highlights via composition selections in editables.
+ if (element) {
+ // If we have an active composition string, commit it, and
+ // ensure proper element focus.
+ let imeSupport = ActionBarHandler._getEditor(element, win).
+ QueryInterface(Ci.nsIEditorIMESupport);
+ if (imeSupport.composing) {
+ element.blur();
+ element.focus();
+ }
+ }
+
+ // Close ActionBarHandler, then selectAll, and display handles.
+ ActionBarHandler._getSelectAllController(element, win).selectAll();
+ UITelemetry.addEvent("action.1", "actionbar", null, "select_all");
+ },
+ },
+
+ CUT: {
+ id: "cut_action",
+ label: Strings.browser.GetStringFromName("contextmenu.cut"),
+ icon: "drawable://ab_cut",
+ order: 4,
+ floatingOrder: 1,
+
+ selector: {
+ matches: function(element, win) {
+ // Can cut from editable, or design-mode document.
+ if (!element && !ActionBarHandler._isInDesignMode(win)) {
+ return false;
+ }
+ // Don't allow "cut" from password fields.
+ if (element instanceof Ci.nsIDOMHTMLInputElement &&
+ !element.mozIsTextField(true)) {
+ return false;
+ }
+ // Don't allow "cut" from disabled/readonly fields.
+ if (element && (element.disabled || element.readOnly)) {
+ return false;
+ }
+ // Allow if selected text exists.
+ return (ActionBarHandler._getSelectedText().length > 0);
+ },
+ },
+
+ action: function(element, win) {
+ // First copy the selection text to the clipboard.
+ let selectedText = ActionBarHandler._getSelectedText();
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(selectedText);
+
+ let msg = Strings.browser.GetStringFromName("selectionHelper.textCopied");
+ Snackbars.show(msg, Snackbars.LENGTH_LONG);
+
+ // Then cut the selection text.
+ ActionBarHandler._getSelection(element, win).deleteFromDocument();
+
+ ActionBarHandler._uninit();
+ UITelemetry.addEvent("action.1", "actionbar", null, "cut");
+ },
+ },
+
+ COPY: {
+ id: "copy_action",
+ label: Strings.browser.GetStringFromName("contextmenu.copy"),
+ icon: "drawable://ab_copy",
+ order: 3,
+ floatingOrder: 2,
+
+ selector: {
+ matches: function(element, win) {
+ // Don't allow "copy" from password fields.
+ if (element instanceof Ci.nsIDOMHTMLInputElement &&
+ !element.mozIsTextField(true)) {
+ return false;
+ }
+ // Allow if selected text exists.
+ return (ActionBarHandler._getSelectedText().length > 0);
+ },
+ },
+
+ action: function(element, win) {
+ let selectedText = ActionBarHandler._getSelectedText();
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(selectedText);
+
+ let msg = Strings.browser.GetStringFromName("selectionHelper.textCopied");
+ Snackbars.show(msg, Snackbars.LENGTH_LONG);
+
+ ActionBarHandler._uninit();
+ UITelemetry.addEvent("action.1", "actionbar", null, "copy");
+ },
+ },
+
+ PASTE: {
+ id: "paste_action",
+ label: Strings.browser.GetStringFromName("contextmenu.paste"),
+ icon: "drawable://ab_paste",
+ order: 2,
+ floatingOrder: 3,
+
+ selector: {
+ matches: function(element, win) {
+ // Can paste to editable, or design-mode document.
+ if (!element && !ActionBarHandler._isInDesignMode(win)) {
+ return false;
+ }
+ // Can't paste into disabled/readonly fields.
+ if (element && (element.disabled || element.readOnly)) {
+ return false;
+ }
+ // Can't paste if Clipboard empty.
+ let flavors = ["text/unicode"];
+ return Services.clipboard.hasDataMatchingFlavors(flavors, flavors.length,
+ Ci.nsIClipboard.kGlobalClipboard);
+ },
+ },
+
+ action: function(element, win) {
+ // Paste the clipboard, then close the ActionBarHandler and ActionBar.
+ ActionBarHandler._getEditor(element, win).
+ paste(Ci.nsIClipboard.kGlobalClipboard);
+ ActionBarHandler._uninit();
+ UITelemetry.addEvent("action.1", "actionbar", null, "paste");
+ },
+ },
+
+ CALL: {
+ id: "call_action",
+ label: Strings.browser.GetStringFromName("contextmenu.call"),
+ icon: "drawable://phone",
+ order: 1,
+ floatingOrder: 0,
+
+ selector: {
+ matches: function(element, win) {
+ return (ActionBarHandler._getSelectedPhoneNumber() != null);
+ },
+ },
+
+ action: function(element, win) {
+ BrowserApp.loadURI("tel:" +
+ ActionBarHandler._getSelectedPhoneNumber());
+
+ ActionBarHandler._uninit();
+ UITelemetry.addEvent("action.1", "actionbar", null, "call");
+ },
+ },
+
+ SEARCH: {
+ id: "search_action",
+ label: () => Strings.browser.formatStringFromName("contextmenu.search",
+ [Services.search.defaultEngine.name], 1),
+ icon: "drawable://ab_search",
+ order: 1,
+ floatingOrder: 6,
+
+ selector: {
+ matches: function(element, win) {
+ // Allow if selected text exists.
+ return (ActionBarHandler._getSelectedText().length > 0);
+ },
+ },
+
+ action: function(element, win) {
+ let selectedText = ActionBarHandler._getSelectedText();
+ ActionBarHandler._uninit();
+
+ // Set current tab as parent of new tab,
+ // and set new tab as private if the parent is.
+ let searchSubmission = Services.search.defaultEngine.getSubmission(selectedText);
+ let parent = BrowserApp.selectedTab;
+ let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(parent.browser);
+ BrowserApp.addTab(searchSubmission.uri.spec,
+ { parentId: parent.id,
+ selected: true,
+ isPrivate: isPrivate,
+ }
+ );
+
+ UITelemetry.addEvent("action.1", "actionbar", null, "search");
+ },
+ },
+
+ SEARCH_ADD: {
+ id: "search_add_action",
+ label: Strings.browser.GetStringFromName("contextmenu.addSearchEngine3"),
+ icon: "drawable://ab_add_search_engine",
+ order: 0,
+ floatingOrder: 8,
+
+ selector: {
+ matches: function(element, win) {
+ if(!(element instanceof HTMLInputElement)) {
+ return false;
+ }
+ let form = element.form;
+ if (!form || element.type == "password") {
+ return false;
+ }
+
+ let method = form.method.toUpperCase();
+ let canAddEngine = (method == "GET") ||
+ (method == "POST" && (form.enctype != "text/plain" && form.enctype != "multipart/form-data"));
+ if (!canAddEngine) {
+ return false;
+ }
+
+ // If SearchEngine query finds it, then we don't want action to add displayed.
+ if (SearchEngines.visibleEngineExists(element)) {
+ return false;
+ }
+
+ return true;
+ },
+ },
+
+ action: function(element, win) {
+ UITelemetry.addEvent("action.1", "actionbar", null, "add_search_engine");
+
+ // Engines are added asynch. If required, update SelectionUI on callback.
+ SearchEngines.addEngine(element, (result) => {
+ if (result) {
+ ActionBarHandler._sendActionBarActions(true);
+ }
+ });
+ },
+ },
+
+ SHARE: {
+ id: "share_action",
+ label: Strings.browser.GetStringFromName("contextmenu.share"),
+ icon: "drawable://ic_menu_share",
+ order: 0,
+ floatingOrder: 4,
+
+ selector: {
+ matches: function(element, win) {
+ if (!ParentalControls.isAllowed(ParentalControls.SHARE)) {
+ return false;
+ }
+ // Allow if selected text exists.
+ return (ActionBarHandler._getSelectedText().length > 0);
+ },
+ },
+
+ action: function(element, win) {
+ Messaging.sendRequest({
+ type: "Share:Text",
+ text: ActionBarHandler._getSelectedText(),
+ });
+
+ ActionBarHandler._uninit();
+ UITelemetry.addEvent("action.1", "actionbar", null, "share");
+ },
+ },
+ },
+
+ /**
+ * Provides UUID service for generating action ID's.
+ */
+ get _idService() {
+ delete this._idService;
+ return this._idService = Cc["@mozilla.org/uuid-generator;1"].
+ getService(Ci.nsIUUIDGenerator);
+ },
+
+ /**
+ * The targetElement holds an editable element containing a
+ * selection or a caret.
+ */
+ get _targetElement() {
+ if (this._targetElementRef)
+ return this._targetElementRef.get();
+ return null;
+ },
+
+ set _targetElement(element) {
+ this._targetElementRef = Cu.getWeakReference(element);
+ },
+
+ /**
+ * The contentWindow holds the selection, or the targetElement
+ * if it's an editable.
+ */
+ get _contentWindow() {
+ if (this._contentWindowRef)
+ return this._contentWindowRef.get();
+ return null;
+ },
+
+ set _contentWindow(aContentWindow) {
+ this._contentWindowRef = Cu.getWeakReference(aContentWindow);
+ },
+
+ /**
+ * If we have an active selection, is it part of a designMode document?
+ */
+ _isInDesignMode: function(win) {
+ return this._selectionID && (win.document.designMode === "on");
+ },
+
+ /**
+ * Provides the currently selected text, for either an editable,
+ * or for the default contentWindow.
+ */
+ _getSelectedText: function() {
+ // Can be called from FindInPageBar "TextSelection:Get", when there
+ // is no active selection.
+ if (!this._selectionID) {
+ return "";
+ }
+
+ let selection = this._getSelection();
+
+ // Textarea can contain LF, etc.
+ if (this._targetElement instanceof Ci.nsIDOMHTMLTextAreaElement) {
+ let flags = Ci.nsIDocumentEncoder.OutputPreformatted |
+ Ci.nsIDocumentEncoder.OutputRaw;
+ return selection.QueryInterface(Ci.nsISelectionPrivate).
+ toStringWithFormat("text/plain", flags, 0);
+ }
+
+ // Return explicitly selected text.
+ return selection.toString();
+ },
+
+ /**
+ * Provides the nsISelection for either an editor, or from the
+ * default window.
+ */
+ _getSelection: function(element = this._targetElement, win = this._contentWindow) {
+ return (element instanceof Ci.nsIDOMNSEditableElement) ?
+ this._getEditor(element).selection :
+ win.getSelection();
+ },
+
+ /**
+ * Returns an nsEditor or nsHTMLEditor.
+ */
+ _getEditor: function(element = this._targetElement, win = this._contentWindow) {
+ if (element instanceof Ci.nsIDOMNSEditableElement) {
+ return element.QueryInterface(Ci.nsIDOMNSEditableElement).editor;
+ }
+
+ return win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation).
+ QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession).
+ getEditorForWindow(win);
+ },
+
+ /**
+ * Returns a selection controller.
+ */
+ _getSelectionController: function(element = this._targetElement, win = this._contentWindow) {
+ if (element instanceof Ci.nsIDOMNSEditableElement) {
+ return this._getEditor(element, win).selectionController;
+ }
+
+ return win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation).
+ QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay).
+ QueryInterface(Ci.nsISelectionController);
+ },
+
+ /**
+ * For selectAll(), provides the editor, or the default window selection Controller.
+ */
+ _getSelectAllController: function(element = this._targetElement, win = this._contentWindow) {
+ let editor = this._getEditor(element, win);
+ return (editor) ?
+ editor : this._getSelectionController(element, win);
+ },
+
+ /**
+ * Call / Phone Helper methods.
+ */
+ _getSelectedPhoneNumber: function() {
+ let selectedText = this._getSelectedText().trim();
+ return this._isPhoneNumber(selectedText) ?
+ selectedText : null;
+ },
+
+ _isPhoneNumber: function(selectedText) {
+ return (PHONE_REGEX.test(selectedText));
+ },
+};
diff --git a/mobile/android/chrome/content/CastingApps.js b/mobile/android/chrome/content/CastingApps.js
new file mode 100644
index 0000000000..76773c4d84
--- /dev/null
+++ b/mobile/android/chrome/content/CastingApps.js
@@ -0,0 +1,850 @@
+// -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "PageActions",
+ "resource://gre/modules/PageActions.jsm");
+
+// Define service devices. We should consider moving these to their respective
+// JSM files, but we left them here to allow for better lazy JSM loading.
+var rokuDevice = {
+ id: "roku:ecp",
+ target: "roku:ecp",
+ factory: function(aService) {
+ Cu.import("resource://gre/modules/RokuApp.jsm");
+ return new RokuApp(aService);
+ },
+ types: ["video/mp4"],
+ extensions: ["mp4"]
+};
+
+var mediaPlayerDevice = {
+ id: "media:router",
+ target: "media:router",
+ factory: function(aService) {
+ Cu.import("resource://gre/modules/MediaPlayerApp.jsm");
+ return new MediaPlayerApp(aService);
+ },
+ types: ["video/mp4", "video/webm", "application/x-mpegurl"],
+ extensions: ["mp4", "webm", "m3u", "m3u8"],
+ init: function() {
+ Services.obs.addObserver(this, "MediaPlayer:Added", false);
+ Services.obs.addObserver(this, "MediaPlayer:Changed", false);
+ Services.obs.addObserver(this, "MediaPlayer:Removed", false);
+ },
+ observe: function(subject, topic, data) {
+ if (topic === "MediaPlayer:Added") {
+ let service = this.toService(JSON.parse(data));
+ SimpleServiceDiscovery.addService(service);
+ } else if (topic === "MediaPlayer:Changed") {
+ let service = this.toService(JSON.parse(data));
+ SimpleServiceDiscovery.updateService(service);
+ } else if (topic === "MediaPlayer:Removed") {
+ SimpleServiceDiscovery.removeService(data);
+ }
+ },
+ toService: function(display) {
+ // Convert the native data into something matching what is created in _processService()
+ return {
+ location: display.location,
+ target: "media:router",
+ friendlyName: display.friendlyName,
+ uuid: display.uuid,
+ manufacturer: display.manufacturer,
+ modelName: display.modelName,
+ mirror: display.mirror
+ };
+ }
+};
+
+var fxOSTVDevice = {
+ id: "app://fling-player.gaiamobile.org",
+ target: "app://fling-player.gaiamobile.org/index.html",
+ factory: function(aService) {
+ Cu.import("resource://gre/modules/PresentationApp.jsm");
+ let request = new window.PresentationRequest(this.target);
+ return new PresentationApp(aService, request);
+ },
+ init: function() {
+ Services.obs.addObserver(this, "presentation-device-change", false);
+ SimpleServiceDiscovery.addExternalDiscovery(this);
+ },
+ observe: function(subject, topic, data) {
+ let device = subject.QueryInterface(Ci.nsIPresentationDevice);
+ let service = this.toService(device);
+ switch (data) {
+ case "add":
+ SimpleServiceDiscovery.addService(service);
+ break;
+ case "update":
+ SimpleServiceDiscovery.updateService(service);
+ break;
+ case "remove":
+ if(SimpleServiceDiscovery.findServiceForID(device.id)) {
+ SimpleServiceDiscovery.removeService(device.id);
+ }
+ break;
+ }
+ },
+ toService: function(device) {
+ return {
+ location: device.id,
+ target: fxOSTVDevice.target,
+ friendlyName: device.name,
+ uuid: device.id,
+ manufacturer: "Firefox OS TV",
+ modelName: "Firefox OS TV",
+ };
+ },
+ startDiscovery: function() {
+ window.navigator.mozPresentationDeviceInfo.forceDiscovery();
+
+ // need to update the lastPing time for known device.
+ window.navigator.mozPresentationDeviceInfo.getAll()
+ .then(function(devices) {
+ for (let device of devices) {
+ let service = fxOSTVDevice.toService(device);
+ SimpleServiceDiscovery.addService(service);
+ }
+ });
+ },
+ stopDiscovery: function() {
+ // do nothing
+ },
+ types: ["video/mp4", "video/webm"],
+ extensions: ["mp4", "webm"],
+};
+
+var CastingApps = {
+ _castMenuId: -1,
+ mirrorStartMenuId: -1,
+ mirrorStopMenuId: -1,
+ _blocked: null,
+ _bound: null,
+ _interval: 120 * 1000, // 120 seconds
+
+ init: function ca_init() {
+ if (!this.isCastingEnabled()) {
+ return;
+ }
+
+ // Register targets
+ SimpleServiceDiscovery.registerDevice(rokuDevice);
+
+ // MediaPlayerDevice will notify us any time the native device list changes.
+ mediaPlayerDevice.init();
+ SimpleServiceDiscovery.registerDevice(mediaPlayerDevice);
+
+ // Presentation Device will notify us any time the available device list changes.
+ if (window.PresentationRequest) {
+ fxOSTVDevice.init();
+ SimpleServiceDiscovery.registerDevice(fxOSTVDevice);
+ }
+
+ // Search for devices continuously
+ SimpleServiceDiscovery.search(this._interval);
+
+ this._castMenuId = NativeWindow.contextmenus.add(
+ Strings.browser.GetStringFromName("contextmenu.sendToDevice"),
+ this.filterCast,
+ this.handleContextMenu.bind(this)
+ );
+
+ Services.obs.addObserver(this, "Casting:Play", false);
+ Services.obs.addObserver(this, "Casting:Pause", false);
+ Services.obs.addObserver(this, "Casting:Stop", false);
+ Services.obs.addObserver(this, "Casting:Mirror", false);
+ Services.obs.addObserver(this, "ssdp-service-found", false);
+ Services.obs.addObserver(this, "ssdp-service-lost", false);
+ Services.obs.addObserver(this, "application-background", false);
+ Services.obs.addObserver(this, "application-foreground", false);
+
+ BrowserApp.deck.addEventListener("TabSelect", this, true);
+ BrowserApp.deck.addEventListener("pageshow", this, true);
+ BrowserApp.deck.addEventListener("playing", this, true);
+ BrowserApp.deck.addEventListener("ended", this, true);
+ BrowserApp.deck.addEventListener("MozAutoplayMediaBlocked", this, true);
+ // Note that the XBL binding is untrusted
+ BrowserApp.deck.addEventListener("MozNoControlsVideoBindingAttached", this, true, true);
+ },
+
+ _mirrorStarted: function(stopMirrorCallback) {
+ this.stopMirrorCallback = stopMirrorCallback;
+ NativeWindow.menu.update(this.mirrorStartMenuId, { visible: false });
+ NativeWindow.menu.update(this.mirrorStopMenuId, { visible: true });
+ },
+
+ serviceAdded: function(aService) {
+ if (this.isMirroringEnabled() && aService.mirror && this.mirrorStartMenuId == -1) {
+ this.mirrorStartMenuId = NativeWindow.menu.add({
+ name: Strings.browser.GetStringFromName("casting.mirrorTab"),
+ callback: function() {
+ let callbackFunc = function(aService) {
+ let app = SimpleServiceDiscovery.findAppForService(aService);
+ if (app) {
+ app.mirror(function() {}, window, BrowserApp.selectedTab.getViewport(), this._mirrorStarted.bind(this), window.BrowserApp.selectedBrowser.contentWindow);
+ }
+ }.bind(this);
+
+ this.prompt(callbackFunc, aService => aService.mirror);
+ }.bind(this),
+ parent: NativeWindow.menu.toolsMenuID
+ });
+
+ this.mirrorStopMenuId = NativeWindow.menu.add({
+ name: Strings.browser.GetStringFromName("casting.mirrorTabStop"),
+ callback: function() {
+ if (this.tabMirror) {
+ this.tabMirror.stop();
+ this.tabMirror = null;
+ } else if (this.stopMirrorCallback) {
+ this.stopMirrorCallback();
+ this.stopMirrorCallback = null;
+ }
+ NativeWindow.menu.update(this.mirrorStartMenuId, { visible: true });
+ NativeWindow.menu.update(this.mirrorStopMenuId, { visible: false });
+ }.bind(this),
+ });
+ }
+ if (this.mirrorStartMenuId != -1) {
+ NativeWindow.menu.update(this.mirrorStopMenuId, { visible: false });
+ }
+ },
+
+ serviceLost: function(aService) {
+ if (aService.mirror && this.mirrorStartMenuId != -1) {
+ let haveMirror = false;
+ SimpleServiceDiscovery.services.forEach(function(service) {
+ if (service.mirror) {
+ haveMirror = true;
+ }
+ });
+ if (!haveMirror) {
+ NativeWindow.menu.remove(this.mirrorStartMenuId);
+ this.mirrorStartMenuId = -1;
+ }
+ }
+ },
+
+ isCastingEnabled: function isCastingEnabled() {
+ return Services.prefs.getBoolPref("browser.casting.enabled");
+ },
+
+ isMirroringEnabled: function isMirroringEnabled() {
+ return Services.prefs.getBoolPref("browser.mirroring.enabled");
+ },
+
+ observe: function (aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "Casting:Play":
+ if (this.session && this.session.remoteMedia.status == "paused") {
+ this.session.remoteMedia.play();
+ }
+ break;
+ case "Casting:Pause":
+ if (this.session && this.session.remoteMedia.status == "started") {
+ this.session.remoteMedia.pause();
+ }
+ break;
+ case "Casting:Stop":
+ if (this.session) {
+ this.closeExternal();
+ }
+ break;
+ case "Casting:Mirror":
+ {
+ Cu.import("resource://gre/modules/TabMirror.jsm");
+ this.tabMirror = new TabMirror(aData, window);
+ NativeWindow.menu.update(this.mirrorStartMenuId, { visible: false });
+ NativeWindow.menu.update(this.mirrorStopMenuId, { visible: true });
+ }
+ break;
+ case "ssdp-service-found":
+ this.serviceAdded(SimpleServiceDiscovery.findServiceForID(aData));
+ break;
+ case "ssdp-service-lost":
+ this.serviceLost(SimpleServiceDiscovery.findServiceForID(aData));
+ break;
+ case "application-background":
+ // Turn off polling while in the background
+ this._interval = SimpleServiceDiscovery.search(0);
+ SimpleServiceDiscovery.stopSearch();
+ break;
+ case "application-foreground":
+ // Turn polling on when app comes back to foreground
+ SimpleServiceDiscovery.search(this._interval);
+ break;
+ }
+ },
+
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case "TabSelect": {
+ let tab = BrowserApp.getTabForBrowser(aEvent.target);
+ this._updatePageActionForTab(tab, aEvent);
+ break;
+ }
+ case "pageshow": {
+ let tab = BrowserApp.getTabForWindow(aEvent.originalTarget.defaultView);
+ this._updatePageActionForTab(tab, aEvent);
+ break;
+ }
+ case "playing":
+ case "ended": {
+ let video = aEvent.target;
+ if (video instanceof HTMLVideoElement) {
+ // If playing, send the <video>, but if ended we send nothing to shutdown the pageaction
+ this._updatePageActionForVideo(aEvent.type === "playing" ? video : null);
+ }
+ break;
+ }
+ case "MozAutoplayMediaBlocked": {
+ if (this._bound && this._bound.has(aEvent.target)) {
+ aEvent.target.dispatchEvent(new CustomEvent("MozNoControlsBlockedVideo"));
+ } else {
+ if (!this._blocked) {
+ this._blocked = new WeakMap;
+ }
+ this._blocked.set(aEvent.target, true);
+ }
+ break;
+ }
+ case "MozNoControlsVideoBindingAttached": {
+ if (!this._bound) {
+ this._bound = new WeakMap;
+ }
+ this._bound.set(aEvent.target, true);
+ if (this._blocked && this._blocked.has(aEvent.target)) {
+ this._blocked.delete(aEvent.target);
+ aEvent.target.dispatchEvent(new CustomEvent("MozNoControlsBlockedVideo"));
+ }
+ break;
+ }
+ }
+ },
+
+ _sendEventToVideo: function _sendEventToVideo(aElement, aData) {
+ let event = aElement.ownerDocument.createEvent("CustomEvent");
+ event.initCustomEvent("media-videoCasting", false, true, JSON.stringify(aData));
+ aElement.dispatchEvent(event);
+ },
+
+ handleVideoBindingAttached: function handleVideoBindingAttached(aTab, aEvent) {
+ // Let's figure out if we have everything needed to cast a video. The binding
+ // defaults to |false| so we only need to send an event if |true|.
+ let video = aEvent.target;
+ if (!(video instanceof HTMLVideoElement)) {
+ return;
+ }
+
+ if (SimpleServiceDiscovery.services.length == 0) {
+ return;
+ }
+
+ this.getVideo(video, 0, 0, (aBundle) => {
+ // Let the binding know casting is allowed
+ if (aBundle) {
+ this._sendEventToVideo(aBundle.element, { allow: true });
+ }
+ });
+ },
+
+ handleVideoBindingCast: function handleVideoBindingCast(aTab, aEvent) {
+ // The binding wants to start a casting session
+ let video = aEvent.target;
+ if (!(video instanceof HTMLVideoElement)) {
+ return;
+ }
+
+ // Close an existing session first. closeExternal has checks for an exsting
+ // session and handles remote and video binding shutdown.
+ this.closeExternal();
+
+ // Start the new session
+ UITelemetry.addEvent("cast.1", "button", null);
+ this.openExternal(video, 0, 0);
+ },
+
+ makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) {
+ return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
+ },
+
+ allowableExtension: function(aURI, aExtensions) {
+ return (aURI instanceof Ci.nsIURL) && aExtensions.indexOf(aURI.fileExtension) != -1;
+ },
+
+ allowableMimeType: function(aType, aTypes) {
+ return aTypes.indexOf(aType) != -1;
+ },
+
+ // This method will look at the aElement (or try to find a video at aX, aY) that has
+ // a castable source. If found, aCallback will be called with a JSON meta bundle. If
+ // no castable source was found, aCallback is called with null.
+ getVideo: function(aElement, aX, aY, aCallback) {
+ let extensions = SimpleServiceDiscovery.getSupportedExtensions();
+ let types = SimpleServiceDiscovery.getSupportedMimeTypes();
+
+ // Fast path: Is the given element a video element?
+ if (aElement instanceof HTMLVideoElement) {
+ // If we found a video element, no need to look further, even if no
+ // castable video source is found.
+ this._getVideo(aElement, types, extensions, aCallback);
+ return;
+ }
+
+ // Maybe this is an overlay, with the video element under it.
+ // Use the (x, y) location to guess at a <video> element.
+
+ // The context menu system will keep walking up the DOM giving us a chance
+ // to find an element we match. When it hits <html> things can go BOOM.
+ try {
+ let elements = aElement.ownerDocument.querySelectorAll("video");
+ for (let element of elements) {
+ // Look for a video element contained in the overlay bounds
+ let rect = element.getBoundingClientRect();
+ if (aY >= rect.top && aX >= rect.left && aY <= rect.bottom && aX <= rect.right) {
+ // Once we find a <video> under the overlay, we check it and exit.
+ this._getVideo(element, types, extensions, aCallback);
+ return;
+ }
+ }
+ } catch(e) {}
+ },
+
+ _getContentTypeForURI: function(aURI, aElement, aCallback) {
+ let channel;
+ try {
+ let secFlags = Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS;
+ if (aElement.crossOrigin) {
+ secFlags = Ci.nsILoadInfo.SEC_REQUIRE_CORS_DATA_INHERITS;
+ if (aElement.crossOrigin === "use-credentials") {
+ secFlags |= Ci.nsILoadInfo.SEC_COOKIES_INCLUDE;
+ }
+ }
+ channel = NetUtil.newChannel({
+ uri: aURI,
+ loadingNode: aElement,
+ securityFlags: secFlags,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_VIDEO
+ });
+ } catch(e) {
+ aCallback(null);
+ return;
+ }
+
+ let listener = {
+ onStartRequest: function(request, context) {
+ switch (channel.responseStatus) {
+ case 301:
+ case 302:
+ case 303:
+ request.cancel(0);
+ let location = channel.getResponseHeader("Location");
+ CastingApps._getContentTypeForURI(CastingApps.makeURI(location), aElement, aCallback);
+ break;
+ default:
+ aCallback(channel.contentType);
+ request.cancel(0);
+ break;
+ }
+ },
+ onStopRequest: function(request, context, statusCode) {},
+ onDataAvailable: function(request, context, stream, offset, count) {}
+ };
+
+ if (channel) {
+ channel.asyncOpen2(listener);
+ } else {
+ aCallback(null);
+ }
+ },
+
+ // Because this method uses a callback, make sure we return ASAP if we know
+ // we have a castable video source.
+ _getVideo: function(aElement, aTypes, aExtensions, aCallback) {
+ // Keep a list of URIs we need for an async mimetype check
+ let asyncURIs = [];
+
+ // Grab the poster attribute from the <video>
+ let posterURL = aElement.poster;
+
+ // First, look to see if the <video> has a src attribute
+ let sourceURL = aElement.src;
+
+ // If empty, try the currentSrc
+ if (!sourceURL) {
+ sourceURL = aElement.currentSrc;
+ }
+
+ if (sourceURL) {
+ // Use the file extension to guess the mime type
+ let sourceURI = this.makeURI(sourceURL, null, this.makeURI(aElement.baseURI));
+ if (this.allowableExtension(sourceURI, aExtensions)) {
+ aCallback({ element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI});
+ return;
+ }
+
+ if (aElement.type) {
+ // Fast sync check
+ if (this.allowableMimeType(aElement.type, aTypes)) {
+ aCallback({ element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI, type: aElement.type });
+ return;
+ }
+ }
+
+ // Delay the async check until we sync scan all possible URIs
+ asyncURIs.push(sourceURI);
+ }
+
+ // Next, look to see if there is a <source> child element that meets
+ // our needs
+ let sourceNodes = aElement.getElementsByTagName("source");
+ for (let sourceNode of sourceNodes) {
+ let sourceURI = this.makeURI(sourceNode.src, null, this.makeURI(sourceNode.baseURI));
+
+ // Using the type attribute is our ideal way to guess the mime type. Otherwise,
+ // fallback to using the file extension to guess the mime type
+ if (this.allowableExtension(sourceURI, aExtensions)) {
+ aCallback({ element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI, type: sourceNode.type });
+ return;
+ }
+
+ if (sourceNode.type) {
+ // Fast sync check
+ if (this.allowableMimeType(sourceNode.type, aTypes)) {
+ aCallback({ element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI, type: sourceNode.type });
+ return;
+ }
+ }
+
+ // Delay the async check until we sync scan all possible URIs
+ asyncURIs.push(sourceURI);
+ }
+
+ // Helper method that walks the array of possible URIs, fetching the mimetype as we go.
+ // As soon as we find a good sourceURL, avoid firing the callback any further
+ var _getContentTypeForURIs = (aURIs) => {
+ // Do an async fetch to figure out the mimetype of the source video
+ let sourceURI = aURIs.pop();
+ this._getContentTypeForURI(sourceURI, aElement, (aType) => {
+ if (this.allowableMimeType(aType, aTypes)) {
+ // We found a supported mimetype.
+ aCallback({ element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI, type: aType });
+ } else {
+ // This URI was not a supported mimetype, so let's try the next, if we have more.
+ if (aURIs.length > 0) {
+ _getContentTypeForURIs(aURIs);
+ } else {
+ // We were not able to find a supported mimetype.
+ aCallback(null);
+ }
+ }
+ });
+ }
+
+ // If we didn't find a good URI directly, let's look using async methods.
+ if (asyncURIs.length > 0) {
+ _getContentTypeForURIs(asyncURIs);
+ }
+ },
+
+ // This code depends on handleVideoBindingAttached setting mozAllowCasting
+ // so we can quickly figure out if the video is castable
+ isVideoCastable: function(aElement, aX, aY) {
+ // Use the flag set when the <video> binding was created as the check
+ if (aElement instanceof HTMLVideoElement) {
+ return aElement.mozAllowCasting;
+ }
+
+ // This is called by the context menu system and the system will keep
+ // walking up the DOM giving us a chance to find an element we match.
+ // When it hits <html> things can go BOOM.
+ try {
+ // Maybe this is an overlay, with the video element under it
+ // Use the (x, y) location to guess at a <video> element
+ let elements = aElement.ownerDocument.querySelectorAll("video");
+ for (let element of elements) {
+ // Look for a video element contained in the overlay bounds
+ let rect = element.getBoundingClientRect();
+ if (aY >= rect.top && aX >= rect.left && aY <= rect.bottom && aX <= rect.right) {
+ // Use the flag set when the <video> binding was created as the check
+ return element.mozAllowCasting;
+ }
+ }
+ } catch(e) {}
+
+ return false;
+ },
+
+ filterCast: {
+ matches: function(aElement, aX, aY) {
+ // This behavior matches the pageaction: As long as a video is castable,
+ // we can cast it, even if it's already being cast to a device.
+ if (SimpleServiceDiscovery.services.length == 0)
+ return false;
+ return CastingApps.isVideoCastable(aElement, aX, aY);
+ }
+ },
+
+ pageAction: {
+ click: function() {
+ // Since this is a pageaction, we use the selected browser
+ let browser = BrowserApp.selectedBrowser;
+ if (!browser) {
+ return;
+ }
+
+ // Look for a castable <video> that is playing, and start casting it
+ let videos = browser.contentDocument.querySelectorAll("video");
+ for (let video of videos) {
+ if (!video.paused && video.mozAllowCasting) {
+ UITelemetry.addEvent("cast.1", "pageaction", null);
+ CastingApps.openExternal(video, 0, 0);
+ return;
+ }
+ }
+ }
+ },
+
+ _findCastableVideo: function _findCastableVideo(aBrowser) {
+ if (!aBrowser) {
+ return null;
+ }
+
+ // Scan for a <video> being actively cast. Also look for a castable <video>
+ // on the page.
+ let castableVideo = null;
+ let videos = aBrowser.contentDocument.querySelectorAll("video");
+ for (let video of videos) {
+ if (video.mozIsCasting) {
+ // This <video> is cast-active. Break out of loop.
+ return video;
+ }
+
+ if (!video.paused && video.mozAllowCasting) {
+ // This <video> is cast-ready. Keep looking so cast-active could be found.
+ castableVideo = video;
+ }
+ }
+
+ // Could be null
+ return castableVideo;
+ },
+
+ _updatePageActionForTab: function _updatePageActionForTab(aTab, aEvent) {
+ // We only care about events on the selected tab
+ if (aTab != BrowserApp.selectedTab) {
+ return;
+ }
+
+ // Update the page action, scanning for a castable <video>
+ this._updatePageAction();
+ },
+
+ _updatePageActionForVideo: function _updatePageActionForVideo(aVideo) {
+ this._updatePageAction(aVideo);
+ },
+
+ _updatePageAction: function _updatePageAction(aVideo) {
+ // Remove any exising pageaction first, in case state changes or we don't have
+ // a castable video
+ if (this.pageAction.id) {
+ PageActions.remove(this.pageAction.id);
+ delete this.pageAction.id;
+ }
+
+ if (!aVideo) {
+ aVideo = this._findCastableVideo(BrowserApp.selectedBrowser);
+ if (!aVideo) {
+ return;
+ }
+ }
+
+ // We only show pageactions if the <video> is from the selected tab
+ if (BrowserApp.selectedTab != BrowserApp.getTabForWindow(aVideo.ownerDocument.defaultView.top)) {
+ return;
+ }
+
+ // We check for two state here:
+ // 1. The video is actively being cast
+ // 2. The video is allowed to be cast and is currently playing
+ // Both states have the same action: Show the cast page action
+ if (aVideo.mozIsCasting) {
+ this.pageAction.id = PageActions.add({
+ title: Strings.browser.GetStringFromName("contextmenu.sendToDevice"),
+ icon: "drawable://casting_active",
+ clickCallback: this.pageAction.click,
+ important: true
+ });
+ } else if (aVideo.mozAllowCasting) {
+ this.pageAction.id = PageActions.add({
+ title: Strings.browser.GetStringFromName("contextmenu.sendToDevice"),
+ icon: "drawable://casting",
+ clickCallback: this.pageAction.click,
+ important: true
+ });
+ }
+ },
+
+ prompt: function(aCallback, aFilterFunc) {
+ let items = [];
+ let filteredServices = [];
+ SimpleServiceDiscovery.services.forEach(function(aService) {
+ let item = {
+ label: aService.friendlyName,
+ selected: false
+ };
+ if (!aFilterFunc || aFilterFunc(aService)) {
+ filteredServices.push(aService);
+ items.push(item);
+ }
+ });
+
+ if (items.length == 0) {
+ return;
+ }
+
+ let prompt = new Prompt({
+ title: Strings.browser.GetStringFromName("casting.sendToDevice")
+ }).setSingleChoiceItems(items).show(function(data) {
+ let selected = data.button;
+ let service = selected == -1 ? null : filteredServices[selected];
+ if (aCallback)
+ aCallback(service);
+ });
+ },
+
+ handleContextMenu: function(aElement, aX, aY) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_cast");
+ UITelemetry.addEvent("cast.1", "contextmenu", null);
+ this.openExternal(aElement, aX, aY);
+ },
+
+ openExternal: function(aElement, aX, aY) {
+ // Start a second screen media service
+ this.getVideo(aElement, aX, aY, this._openExternal.bind(this));
+ },
+
+ _openExternal: function(aVideo) {
+ if (!aVideo) {
+ return;
+ }
+
+ function filterFunc(aService) {
+ return this.allowableExtension(aVideo.sourceURI, aService.extensions) || this.allowableMimeType(aVideo.type, aService.types);
+ }
+
+ this.prompt(function(aService) {
+ if (!aService)
+ return;
+
+ // Make sure we have a player app for the given service
+ let app = SimpleServiceDiscovery.findAppForService(aService);
+ if (!app)
+ return;
+
+ if (aVideo.element) {
+ aVideo.title = aVideo.element.ownerDocument.defaultView.top.document.title;
+
+ // If the video is currently playing on the device, pause it
+ if (!aVideo.element.paused) {
+ aVideo.element.pause();
+ }
+ }
+
+ app.stop(function() {
+ app.start(function(aStarted) {
+ if (!aStarted) {
+ dump("CastingApps: Unable to start app");
+ return;
+ }
+
+ app.remoteMedia(function(aRemoteMedia) {
+ if (!aRemoteMedia) {
+ dump("CastingApps: Failed to create remotemedia");
+ return;
+ }
+
+ this.session = {
+ service: aService,
+ app: app,
+ remoteMedia: aRemoteMedia,
+ data: {
+ title: aVideo.title,
+ source: aVideo.source,
+ poster: aVideo.poster
+ },
+ videoRef: Cu.getWeakReference(aVideo.element)
+ };
+ }.bind(this), this);
+ }.bind(this));
+ }.bind(this));
+ }.bind(this), filterFunc.bind(this));
+ },
+
+ closeExternal: function() {
+ if (!this.session) {
+ return;
+ }
+
+ this.session.remoteMedia.shutdown();
+ this._shutdown();
+ },
+
+ _shutdown: function() {
+ if (!this.session) {
+ return;
+ }
+
+ this.session.app.stop();
+ let video = this.session.videoRef.get();
+ if (video) {
+ this._sendEventToVideo(video, { active: false });
+ this._updatePageAction();
+ }
+
+ delete this.session;
+ },
+
+ // RemoteMedia callback API methods
+ onRemoteMediaStart: function(aRemoteMedia) {
+ if (!this.session) {
+ return;
+ }
+
+ aRemoteMedia.load(this.session.data);
+ Messaging.sendRequest({ type: "Casting:Started", device: this.session.service.friendlyName });
+
+ let video = this.session.videoRef.get();
+ if (video) {
+ this._sendEventToVideo(video, { active: true });
+ this._updatePageAction(video);
+ }
+ },
+
+ onRemoteMediaStop: function(aRemoteMedia) {
+ Messaging.sendRequest({ type: "Casting:Stopped" });
+ this._shutdown();
+ },
+
+ onRemoteMediaStatus: function(aRemoteMedia) {
+ if (!this.session) {
+ return;
+ }
+
+ let status = aRemoteMedia.status;
+ switch (status) {
+ case "started":
+ Messaging.sendRequest({ type: "Casting:Playing" });
+ break;
+ case "paused":
+ Messaging.sendRequest({ type: "Casting:Paused" });
+ break;
+ case "completed":
+ this.closeExternal();
+ break;
+ }
+ }
+};
diff --git a/mobile/android/chrome/content/ConsoleAPI.js b/mobile/android/chrome/content/ConsoleAPI.js
new file mode 100644
index 0000000000..6ba4c11950
--- /dev/null
+++ b/mobile/android/chrome/content/ConsoleAPI.js
@@ -0,0 +1,96 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var ConsoleAPI = {
+ observe: function observe(aMessage, aTopic, aData) {
+ aMessage = aMessage.wrappedJSObject;
+
+ let mappedArguments = Array.map(aMessage.arguments, this.formatResult, this);
+ let joinedArguments = Array.join(mappedArguments, " ");
+
+ if (aMessage.level == "error" || aMessage.level == "warn") {
+ let flag = (aMessage.level == "error" ? Ci.nsIScriptError.errorFlag : Ci.nsIScriptError.warningFlag);
+ let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError);
+ consoleMsg.init(joinedArguments, null, null, 0, 0, flag, "content javascript");
+ Services.console.logMessage(consoleMsg);
+ } else if (aMessage.level == "trace") {
+ let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+ let args = aMessage.arguments;
+ let filename = this.abbreviateSourceURL(args[0].filename);
+ let functionName = args[0].functionName || bundle.GetStringFromName("stacktrace.anonymousFunction");
+ let lineNumber = args[0].lineNumber;
+
+ let body = bundle.formatStringFromName("stacktrace.outputMessage", [filename, functionName, lineNumber], 3);
+ body += "\n";
+ args.forEach(function(aFrame) {
+ let functionName = aFrame.functionName || bundle.GetStringFromName("stacktrace.anonymousFunction");
+ body += " " + aFrame.filename + " :: " + functionName + " :: " + aFrame.lineNumber + "\n";
+ });
+
+ Services.console.logStringMessage(body);
+ } else if (aMessage.level == "time" && aMessage.arguments) {
+ let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+ let body = bundle.formatStringFromName("timer.start", [aMessage.arguments.name], 1);
+ Services.console.logStringMessage(body);
+ } else if (aMessage.level == "timeEnd" && aMessage.arguments) {
+ let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+ let body = bundle.formatStringFromName("timer.end", [aMessage.arguments.name, aMessage.arguments.duration], 2);
+ Services.console.logStringMessage(body);
+ } else if (["group", "groupCollapsed", "groupEnd"].indexOf(aMessage.level) != -1) {
+ // Do nothing yet
+ } else {
+ Services.console.logStringMessage(joinedArguments);
+ }
+ },
+
+ getResultType: function getResultType(aResult) {
+ let type = aResult === null ? "null" : typeof aResult;
+ if (type == "object" && aResult.constructor && aResult.constructor.name)
+ type = aResult.constructor.name;
+ return type.toLowerCase();
+ },
+
+ formatResult: function formatResult(aResult) {
+ let output = "";
+ let type = this.getResultType(aResult);
+ switch (type) {
+ case "string":
+ case "boolean":
+ case "date":
+ case "error":
+ case "number":
+ case "regexp":
+ output = aResult.toString();
+ break;
+ case "null":
+ case "undefined":
+ output = type;
+ break;
+ default:
+ output = aResult.toString();
+ break;
+ }
+
+ return output;
+ },
+
+ abbreviateSourceURL: function abbreviateSourceURL(aSourceURL) {
+ // Remove any query parameters.
+ let hookIndex = aSourceURL.indexOf("?");
+ if (hookIndex > -1)
+ aSourceURL = aSourceURL.substring(0, hookIndex);
+
+ // Remove a trailing "/".
+ if (aSourceURL[aSourceURL.length - 1] == "/")
+ aSourceURL = aSourceURL.substring(0, aSourceURL.length - 1);
+
+ // Remove all but the last path component.
+ let slashIndex = aSourceURL.lastIndexOf("/");
+ if (slashIndex > -1)
+ aSourceURL = aSourceURL.substring(slashIndex + 1);
+
+ return aSourceURL;
+ }
+};
diff --git a/mobile/android/chrome/content/EmbedRT.js b/mobile/android/chrome/content/EmbedRT.js
new file mode 100644
index 0000000000..8e35a3b634
--- /dev/null
+++ b/mobile/android/chrome/content/EmbedRT.js
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPI",
+ "resource://gre/modules/Console.jsm");
+
+/*
+ * Collection of methods and features specific to using a GeckoView instance.
+ * The code is isolated from browser.js for code size and performance reasons.
+ */
+var EmbedRT = {
+ _scopes: {},
+
+ observe: function(subject, topic, data) {
+ switch(topic) {
+ case "GeckoView:ImportScript":
+ this.importScript(data);
+ break;
+ }
+ },
+
+ /*
+ * Loads a script file into a sandbox and calls an optional load function
+ */
+ importScript: function(scriptURL) {
+ if (scriptURL in this._scopes) {
+ return;
+ }
+
+ let principal = Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal);
+
+ let sandbox = new Cu.Sandbox(principal,
+ {
+ sandboxName: scriptURL,
+ wantGlobalProperties: ["indexedDB"]
+ }
+ );
+
+ sandbox["console"] = new ConsoleAPI({ consoleID: "script/" + scriptURL });
+ sandbox["GeckoView"] = {
+ sendRequest: function(data) {
+ if (!data) {
+ throw new Error("Invalid parameter: 'data' can't be null.");
+ }
+
+ let message = { type: "GeckoView:Message", data: data };
+ Messaging.sendRequest(message);
+ },
+ sendRequestForResult: function(data) {
+ if (!data) {
+ throw new Error("Invalid parameter: 'data' can't be null.");
+ }
+
+ let message = { type: "GeckoView:Message", data: data };
+ return Messaging.sendRequestForResult(message);
+ }
+ };
+
+ // As we don't want our caller to control the JS version used for the
+ // script file, we run loadSubScript within the context of the
+ // sandbox with the latest JS version set explicitly.
+ sandbox.__SCRIPT_URI_SPEC__ = scriptURL;
+ Cu.evalInSandbox("Components.classes['@mozilla.org/moz/jssubscript-loader;1'].createInstance(Components.interfaces.mozIJSSubScriptLoader).loadSubScript(__SCRIPT_URI_SPEC__);", sandbox, "ECMAv5");
+
+ this._scopes[scriptURL] = sandbox;
+
+ if ("load" in sandbox) {
+ let params = {
+ window: window,
+ resourceURI: scriptURL,
+ };
+
+ try {
+ sandbox["load"](params);
+ } catch(e) {
+ dump("Exception calling 'load' method in script: " + scriptURL + "\n" + e);
+ }
+ }
+ }
+};
diff --git a/mobile/android/chrome/content/FeedHandler.js b/mobile/android/chrome/content/FeedHandler.js
new file mode 100644
index 0000000000..91d73ee8dc
--- /dev/null
+++ b/mobile/android/chrome/content/FeedHandler.js
@@ -0,0 +1,120 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var FeedHandler = {
+ PREF_CONTENTHANDLERS_BRANCH: "browser.contentHandlers.types.",
+ TYPE_MAYBE_FEED: "application/vnd.mozilla.maybe.feed",
+
+ _contentTypes: null,
+
+ getContentHandlers: function fh_getContentHandlers(contentType) {
+ if (!this._contentTypes)
+ this.loadContentHandlers();
+
+ if (!(contentType in this._contentTypes))
+ return [];
+
+ return this._contentTypes[contentType];
+ },
+
+ loadContentHandlers: function fh_loadContentHandlers() {
+ this._contentTypes = {};
+
+ let kids = Services.prefs.getBranch(this.PREF_CONTENTHANDLERS_BRANCH).getChildList("");
+
+ // First get the numbers of the providers by getting all ###.uri prefs
+ let nums = [];
+ for (let i = 0; i < kids.length; i++) {
+ let match = /^(\d+)\.uri$/.exec(kids[i]);
+ if (!match)
+ continue;
+ else
+ nums.push(match[1]);
+ }
+
+ // Sort them, to get them back in order
+ nums.sort(function(a, b) { return a - b; });
+
+ // Now register them
+ for (let i = 0; i < nums.length; i++) {
+ let branch = Services.prefs.getBranch(this.PREF_CONTENTHANDLERS_BRANCH + nums[i] + ".");
+ let vals = branch.getChildList("");
+ if (vals.length == 0)
+ return;
+
+ try {
+ let type = branch.getCharPref("type");
+ let uri = branch.getComplexValue("uri", Ci.nsIPrefLocalizedString).data;
+ let title = branch.getComplexValue("title", Ci.nsIPrefLocalizedString).data;
+
+ if (!(type in this._contentTypes))
+ this._contentTypes[type] = [];
+ this._contentTypes[type].push({ contentType: type, uri: uri, name: title });
+ }
+ catch(ex) {}
+ }
+ },
+
+ observe: function fh_observe(aSubject, aTopic, aData) {
+ if (aTopic === "Feeds:Subscribe") {
+ let args = JSON.parse(aData);
+ let tab = BrowserApp.getTabForId(args.tabId);
+ if (!tab)
+ return;
+
+ let browser = tab.browser;
+ let feeds = browser.feeds;
+ if (feeds == null)
+ return;
+
+ // First, let's decide on which feed to subscribe
+ let feedIndex = -1;
+ if (feeds.length > 1) {
+ let p = new Prompt({
+ window: browser.contentWindow,
+ title: Strings.browser.GetStringFromName("feedHandler.chooseFeed")
+ }).setSingleChoiceItems(feeds.map(function(feed) {
+ return { label: feed.title || feed.href }
+ })).show((function(data) {
+ feedIndex = data.button;
+ if (feedIndex == -1)
+ return;
+
+ this.loadFeed(feeds[feedIndex], browser);
+ }).bind(this));
+ return;
+ }
+
+ this.loadFeed(feeds[0], browser);
+ }
+ },
+
+ loadFeed: function fh_loadFeed(aFeed, aBrowser) {
+ let feedURL = aFeed.href;
+
+ // Next, we decide on which service to send the feed
+ let handlers = this.getContentHandlers(this.TYPE_MAYBE_FEED);
+ if (handlers.length == 0)
+ return;
+
+ // JSON for Prompt
+ let p = new Prompt({
+ window: aBrowser.contentWindow,
+ title: Strings.browser.GetStringFromName("feedHandler.subscribeWith")
+ }).setSingleChoiceItems(handlers.map(function(handler) {
+ return { label: handler.name };
+ })).show(function(data) {
+ if (data.button == -1)
+ return;
+
+ // Merge the handler URL and the feed URL
+ let readerURL = handlers[data.button].uri;
+ readerURL = readerURL.replace(/%s/gi, encodeURIComponent(feedURL));
+
+ // Open the resultant URL in a new tab
+ BrowserApp.addTab(readerURL, { parentId: BrowserApp.selectedTab.id });
+ });
+ }
+};
diff --git a/mobile/android/chrome/content/Feedback.js b/mobile/android/chrome/content/Feedback.js
new file mode 100644
index 0000000000..8727c46c39
--- /dev/null
+++ b/mobile/android/chrome/content/Feedback.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var Feedback = {
+
+ get _feedbackURL() {
+ delete this._feedbackURL;
+ return this._feedbackURL = Services.urlFormatter.formatURLPref("app.feedbackURL");
+ },
+
+ observe: function(aMessage, aTopic, aData) {
+ if (aTopic !== "Feedback:Show") {
+ return;
+ }
+
+ // Don't prompt for feedback in distribution builds.
+ try {
+ Services.prefs.getCharPref("distribution.id");
+ return;
+ } catch (e) {}
+
+ let url = this._feedbackURL;
+ let browser = BrowserApp.selectOrAddTab(url, { parentId: BrowserApp.selectedTab.id }).browser;
+
+ browser.addEventListener("FeedbackClose", this, false, true);
+ browser.addEventListener("FeedbackMaybeLater", this, false, true);
+
+ // Dispatch a custom event to the page content when feedback is prompted by the browser.
+ // This will be used by the page to determine it's being loaded directly by the browser,
+ // instead of by the user visiting the page, e.g. through browser history.
+ function loadListener(event) {
+ browser.removeEventListener("DOMContentLoaded", loadListener, false);
+ browser.contentDocument.dispatchEvent(new CustomEvent("FeedbackPrompted"));
+ }
+ browser.addEventListener("DOMContentLoaded", loadListener, false);
+ },
+
+ handleEvent: function(event) {
+ if (!this._isAllowed(event.target)) {
+ return;
+ }
+
+ switch (event.type) {
+ case "FeedbackClose":
+ // Do nothing.
+ break;
+
+ case "FeedbackMaybeLater":
+ Messaging.sendRequest({ type: "Feedback:MaybeLater" });
+ break;
+ }
+
+ let win = event.target.ownerDocument.defaultView.top;
+ BrowserApp.closeTab(BrowserApp.getTabForWindow(win));
+ },
+
+ _isAllowed: function(node) {
+ let uri = node.ownerDocument.documentURIObject;
+ let feedbackURI = Services.io.newURI(this._feedbackURL, null, null);
+ return uri.prePath === feedbackURI.prePath;
+ }
+};
diff --git a/mobile/android/chrome/content/FindHelper.js b/mobile/android/chrome/content/FindHelper.js
new file mode 100644
index 0000000000..037b182d64
--- /dev/null
+++ b/mobile/android/chrome/content/FindHelper.js
@@ -0,0 +1,197 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var FindHelper = {
+ _finder: null,
+ _targetTab: null,
+ _initialViewport: null,
+ _viewportChanged: false,
+ _result: null,
+
+ // Start of nsIObserver implementation.
+
+ observe: function(aMessage, aTopic, aData) {
+ switch(aTopic) {
+ case "FindInPage:Opened": {
+ this._findOpened();
+ break;
+ }
+
+ case "Tab:Selected": {
+ // Allow for page switching.
+ this._uninit();
+ break;
+ }
+
+ case "FindInPage:Closed":
+ this._uninit();
+ this._findClosed();
+ break;
+ }
+ },
+
+ /**
+ * When the FindInPageBar opens/ becomes visible, it's time to:
+ * 1. Add listeners for other message types sent from the FindInPageBar
+ * 2. initialize the Finder instance, if necessary.
+ */
+ _findOpened: function() {
+ Messaging.addListener(data => this.doFind(data), "FindInPage:Find");
+ Messaging.addListener(data => this.findAgain(data, false), "FindInPage:Next");
+ Messaging.addListener(data => this.findAgain(data, true), "FindInPage:Prev");
+
+ // Initialize the finder component for the current page by performing a fake find.
+ this._init();
+ this._finder.requestMatchesCount("");
+ },
+
+ /**
+ * Fetch the Finder instance from the active tabs' browser and start tracking
+ * the active viewport.
+ */
+ _init: function() {
+ // If there's no find in progress, start one.
+ if (this._finder) {
+ return;
+ }
+
+ this._targetTab = BrowserApp.selectedTab;
+ try {
+ this._finder = this._targetTab.browser.finder;
+ } catch (e) {
+ throw new Error("FindHelper: " + e + "\n" +
+ "JS stack: \n" + (e.stack || Components.stack.formattedStack));
+ }
+
+ this._finder.addResultListener(this);
+ this._initialViewport = JSON.stringify(this._targetTab.getViewport());
+ this._viewportChanged = false;
+ },
+
+ /**
+ * Detach from the Finder instance (so stop listening for messages) and stop
+ * tracking the active viewport.
+ */
+ _uninit: function() {
+ // If there's no find in progress, there's nothing to clean up.
+ if (!this._finder) {
+ return;
+ }
+
+ this._finder.removeSelection();
+ this._finder.removeResultListener(this);
+ this._finder = null;
+ this._targetTab = null;
+ this._initialViewport = null;
+ this._viewportChanged = false;
+ },
+
+ /**
+ * When the FindInPageBar closes, it's time to stop listening for its messages.
+ */
+ _findClosed: function() {
+ Messaging.removeListener("FindInPage:Find");
+ Messaging.removeListener("FindInPage:Next");
+ Messaging.removeListener("FindInPage:Prev");
+ },
+
+ /**
+ * Start an asynchronous find-in-page operation, using the current Finder
+ * instance and request to count the amount of matches.
+ * If no Finder instance is currently active, we'll lazily initialize it here.
+ *
+ * @param {String} searchString Word to search for in the current document
+ * @return {Object} Echo of the current find action
+ */
+ doFind: function(searchString) {
+ if (!this._finder) {
+ this._init();
+ }
+
+ this._finder.fastFind(searchString, false);
+ return { searchString, findBackwards: false };
+ },
+
+ /**
+ * Restart the same find-in-page operation as before via `doFind()`. If we
+ * haven't called `doFind()`, we simply kick off a regular find.
+ *
+ * @param {String} searchString Word to search for in the current document
+ * @param {Boolean} findBackwards Direction to search in
+ * @return {Object} Echo of the current find action
+ */
+ findAgain: function(searchString, findBackwards) {
+ // This always happens if the user taps next/previous after re-opening the
+ // search bar, and not only forces _init() but also an initial fastFind(STRING)
+ // before any findAgain(DIRECTION).
+ if (!this._finder) {
+ return this.doFind(searchString);
+ }
+
+ this._finder.findAgain(findBackwards, false, false);
+ return { searchString, findBackwards };
+ },
+
+ // Start of Finder.jsm listener implementation.
+
+ /**
+ * Pass along the count results to FindInPageBar for display. The result that
+ * is sent to the FindInPageBar is augmented with the current find-in-page count
+ * limit.
+ *
+ * @param {Object} result Result coming from the Finder instance that contains
+ * the following properties:
+ * - {Number} total The total amount of matches found
+ * - {Number} current The index of current found range
+ * in the document
+ */
+ onMatchesCountResult: function(result) {
+ this._result = result;
+
+ Messaging.sendRequest(Object.assign({
+ type: "FindInPage:MatchesCountResult"
+ }, this._result));
+ },
+
+ /**
+ * When a find-in-page action finishes, this method is invoked. This is mainly
+ * used at the moment to detect if the current viewport has changed, which might
+ * be indicated by not finding a string in the current page.
+ *
+ * @param {Object} aData A dictionary, representing the find result, which
+ * contains the following properties:
+ * - {String} searchString Word that was searched for
+ * in the current document
+ * - {Number} result One of the following
+ * Ci.nsITypeAheadFind.* result
+ * indicators: FIND_FOUND,
+ * FIND_NOTFOUND, FIND_WRAPPED,
+ * FIND_PENDING
+ * - {Boolean} findBackwards Whether the search direction
+ * was backwards
+ * - {Boolean} findAgain Whether the previous search
+ * was repeated
+ * - {Boolean} drawOutline Whether we may (re-)draw the
+ * outline of a hyperlink
+ * - {Boolean} linksOnly Whether links-only mode was
+ * active
+ */
+ onFindResult: function(aData) {
+ if (aData.result == Ci.nsITypeAheadFind.FIND_NOTFOUND) {
+ if (this._viewportChanged) {
+ if (this._targetTab != BrowserApp.selectedTab) {
+ // this should never happen
+ Cu.reportError("Warning: selected tab changed during find!");
+ // fall through and restore viewport on the initial tab anyway
+ }
+ this._targetTab.sendViewportUpdate();
+ }
+ } else {
+ // Disabled until bug 1014113 is fixed
+ // ZoomHelper.zoomToRect(aData.rect);
+ this._viewportChanged = true;
+ }
+ }
+};
diff --git a/mobile/android/chrome/content/InputWidgetHelper.js b/mobile/android/chrome/content/InputWidgetHelper.js
new file mode 100644
index 0000000000..9c753bd7bb
--- /dev/null
+++ b/mobile/android/chrome/content/InputWidgetHelper.js
@@ -0,0 +1,98 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var InputWidgetHelper = {
+ _uiBusy: false,
+
+ handleEvent: function(aEvent) {
+ this.handleClick(aEvent.target);
+ },
+
+ handleClick: function(aTarget) {
+ // if we're busy looking at a InputWidget we want to eat any clicks that
+ // come to us, but not to process them
+ if (this._uiBusy || !this.hasInputWidget(aTarget) || this._isDisabledElement(aTarget))
+ return;
+
+ this._uiBusy = true;
+ this.show(aTarget);
+ this._uiBusy = false;
+ },
+
+ show: function(aElement) {
+ let type = aElement.getAttribute('type');
+ let p = new Prompt({
+ window: aElement.ownerDocument.defaultView,
+ title: Strings.browser.GetStringFromName("inputWidgetHelper." + aElement.getAttribute('type')),
+ buttons: [
+ Strings.browser.GetStringFromName("inputWidgetHelper.set"),
+ Strings.browser.GetStringFromName("inputWidgetHelper.clear"),
+ Strings.browser.GetStringFromName("inputWidgetHelper.cancel")
+ ],
+ }).addDatePicker({
+ value: aElement.value,
+ type: type,
+ min: aElement.min,
+ max: aElement.max,
+ }).show((function(data) {
+ let changed = false;
+ if (data.button == -1) {
+ // This type is not supported with this android version.
+ return;
+ }
+ if (data.button == 1) {
+ // The user cleared the value.
+ if (aElement.value != "") {
+ aElement.value = "";
+ changed = true;
+ }
+ } else if (data.button == 0) {
+ // Commit the new value.
+ if (aElement.value != data[type]) {
+ aElement.value = data[type + "0"];
+ changed = true;
+ }
+ }
+ // Else the user canceled the input.
+
+ if (changed)
+ this.fireOnChange(aElement);
+ }).bind(this));
+ },
+
+ hasInputWidget: function(aElement) {
+ if (!aElement instanceof HTMLInputElement)
+ return false;
+
+ let type = aElement.getAttribute('type');
+ if (type == "date" || type == "datetime" || type == "datetime-local" ||
+ type == "week" || type == "month" || type == "time") {
+ return true;
+ }
+
+ return false;
+ },
+
+ fireOnChange: function(aElement) {
+ let evt = aElement.ownerDocument.createEvent("Events");
+ evt.initEvent("change", true, true, aElement.defaultView, 0,
+ false, false,
+ false, false, null);
+ setTimeout(function() {
+ aElement.dispatchEvent(evt);
+ }, 0);
+ },
+
+ _isDisabledElement : function(aElement) {
+ let currentElement = aElement;
+ while (currentElement) {
+ if (currentElement.disabled)
+ return true;
+
+ currentElement = currentElement.parentElement;
+ }
+ return false;
+ }
+};
diff --git a/mobile/android/chrome/content/Linkify.js b/mobile/android/chrome/content/Linkify.js
new file mode 100644
index 0000000000..3c757cc180
--- /dev/null
+++ b/mobile/android/chrome/content/Linkify.js
@@ -0,0 +1,108 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const LINKIFY_TIMEOUT = 0;
+
+function Linkifier() {
+ this._linkifyTimer = null;
+ this._phoneRegex = /(?:\s|^)[\+]?(\(?\d{1,8}\)?)?([- ]+\(?\d{1,8}\)?)+( ?(x|ext) ?\d{1,3})?(?:\s|$)/g;
+}
+
+Linkifier.prototype = {
+ _buildAnchor : function(aDoc, aNumberText) {
+ let anchorNode = aDoc.createElement("a");
+ let cleanedText = "";
+ for (let i = 0; i < aNumberText.length; i++) {
+ let c = aNumberText.charAt(i);
+ if ((c >= '0' && c <= '9') || c == '+') //assuming there is only the leading '+'.
+ cleanedText += c;
+ }
+ anchorNode.setAttribute("href", "tel:" + cleanedText);
+ let nodeText = aDoc.createTextNode(aNumberText);
+ anchorNode.appendChild(nodeText);
+ return anchorNode;
+ },
+
+ _linkifyNodeNumbers : function(aNodeToProcess, aDoc) {
+ let parent = aNodeToProcess.parentNode;
+ let nodeText = aNodeToProcess.nodeValue;
+
+ // Replacing the original text node with a sequence of
+ // |text before number|anchor with number|text after number nodes.
+ // Each step a couple of (optional) text node and anchor node are appended.
+ let anchorNode = null;
+ let m = null;
+ let startIndex = 0;
+ let prevNode = null;
+ while (m = this._phoneRegex.exec(nodeText)) {
+ anchorNode = this._buildAnchor(aDoc, nodeText.substr(m.index, m[0].length));
+
+ let textExistsBeforeNumber = (m.index > startIndex);
+ let nodeToAdd = null;
+ if (textExistsBeforeNumber)
+ nodeToAdd = aDoc.createTextNode(nodeText.substr(startIndex, m.index - startIndex));
+ else
+ nodeToAdd = anchorNode;
+
+ if (!prevNode) // first time, need to replace the whole node with the first new one.
+ parent.replaceChild(nodeToAdd, aNodeToProcess);
+ else
+ parent.insertBefore(nodeToAdd, prevNode.nextSibling); //inserts after.
+
+ if (textExistsBeforeNumber) // if we added the text node before the anchor, we still need to add the anchor node.
+ parent.insertBefore(anchorNode, nodeToAdd.nextSibling);
+
+ // next nodes need to be appended to this node.
+ prevNode = anchorNode;
+ startIndex = m.index + m[0].length;
+ }
+
+ // if some text is remaining after the last anchor.
+ if (startIndex > 0 && startIndex < nodeText.length) {
+ let lastNode = aDoc.createTextNode(nodeText.substr(startIndex));
+ parent.insertBefore(lastNode, prevNode.nextSibling);
+ return lastNode;
+ }
+ return anchorNode;
+ },
+
+ linkifyNumbers: function(aDoc) {
+ // Removing any installed timer in case the page has changed and a previous timer is still running.
+ if (this._linkifyTimer) {
+ clearTimeout(this._linkifyTimer);
+ this._linkifyTimer = null;
+ }
+
+ let filterNode = function (node) {
+ if (node.parentNode.tagName != 'A' &&
+ node.parentNode.tagName != 'SCRIPT' &&
+ node.parentNode.tagName != 'NOSCRIPT' &&
+ node.parentNode.tagName != 'STYLE' &&
+ node.parentNode.tagName != 'APPLET' &&
+ node.parentNode.tagName != 'TEXTAREA')
+ return NodeFilter.FILTER_ACCEPT;
+ else
+ return NodeFilter.FILTER_REJECT;
+ }
+
+ let nodeWalker = aDoc.createTreeWalker(aDoc.body, NodeFilter.SHOW_TEXT, filterNode, false);
+ let parseNode = function() {
+ let node = nodeWalker.nextNode();
+ if (!node) {
+ this._linkifyTimer = null;
+ return;
+ }
+ let lastAddedNode = this._linkifyNodeNumbers(node, aDoc);
+ // we assign a different timeout whether the node was processed or not.
+ if (lastAddedNode) {
+ nodeWalker.currentNode = lastAddedNode;
+ this._linkifyTimer = setTimeout(parseNode, LINKIFY_TIMEOUT);
+ } else {
+ this._linkifyTimer = setTimeout(parseNode, LINKIFY_TIMEOUT);
+ }
+ }.bind(this);
+
+ this._linkifyTimer = setTimeout(parseNode, LINKIFY_TIMEOUT);
+ }
+};
diff --git a/mobile/android/chrome/content/MasterPassword.js b/mobile/android/chrome/content/MasterPassword.js
new file mode 100644
index 0000000000..d85fa928d8
--- /dev/null
+++ b/mobile/android/chrome/content/MasterPassword.js
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+
+var MasterPassword = {
+ pref: "privacy.masterpassword.enabled",
+ _tokenName: "",
+
+ get _secModuleDB() {
+ delete this._secModuleDB;
+ return this._secModuleDB = Cc["@mozilla.org/security/pkcs11moduledb;1"].getService(Ci.nsIPKCS11ModuleDB);
+ },
+
+ get _pk11DB() {
+ delete this._pk11DB;
+ return this._pk11DB = Cc["@mozilla.org/security/pk11tokendb;1"].getService(Ci.nsIPK11TokenDB);
+ },
+
+ get enabled() {
+ let slot = this._secModuleDB.findSlotByName(this._tokenName);
+ if (slot) {
+ let status = slot.status;
+ return status != Ci.nsIPKCS11Slot.SLOT_UNINITIALIZED && status != Ci.nsIPKCS11Slot.SLOT_READY;
+ }
+ return false;
+ },
+
+ setPassword: function setPassword(aPassword) {
+ try {
+ let status;
+ let slot = this._secModuleDB.findSlotByName(this._tokenName);
+ if (slot)
+ status = slot.status;
+ else
+ return false;
+
+ let token = this._pk11DB.findTokenByName(this._tokenName);
+
+ if (status == Ci.nsIPKCS11Slot.SLOT_UNINITIALIZED)
+ token.initPassword(aPassword);
+ else if (status == Ci.nsIPKCS11Slot.SLOT_READY)
+ token.changePassword("", aPassword);
+
+ return true;
+ } catch(e) {
+ dump("MasterPassword.setPassword: " + e);
+ }
+ return false;
+ },
+
+ removePassword: function removePassword(aOldPassword) {
+ try {
+ let token = this._pk11DB.getInternalKeyToken();
+ if (token.checkPassword(aOldPassword)) {
+ token.changePassword(aOldPassword, "");
+ return true;
+ }
+ } catch(e) {
+ dump("MasterPassword.removePassword: " + e + "\n");
+ }
+ Snackbars.show(Strings.browser.GetStringFromName("masterPassword.incorrect"), Snackbars.LENGTH_LONG);
+ return false;
+ }
+};
diff --git a/mobile/android/chrome/content/MemoryObserver.js b/mobile/android/chrome/content/MemoryObserver.js
new file mode 100644
index 0000000000..2bb3ae8425
--- /dev/null
+++ b/mobile/android/chrome/content/MemoryObserver.js
@@ -0,0 +1,88 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var MemoryObserver = {
+ observe: function mo_observe(aSubject, aTopic, aData) {
+ if (aTopic == "memory-pressure") {
+ if (aData != "heap-minimize") {
+ this.handleLowMemory();
+ }
+ // The JS engine would normally GC on this notification, but since we
+ // disabled that in favor of this method (bug 669346), we should gc here.
+ // See bug 784040 for when this code was ported from XUL to native Fennec.
+ this.gc();
+ } else if (aTopic == "Memory:Dump") {
+ this.dumpMemoryStats(aData);
+ }
+ },
+
+ handleLowMemory: function() {
+ // do things to reduce memory usage here
+ if (!Services.prefs.getBoolPref("browser.tabs.disableBackgroundZombification")) {
+ let tabs = BrowserApp.tabs;
+ let selected = BrowserApp.selectedTab;
+ for (let i = 0; i < tabs.length; i++) {
+ if (tabs[i] != selected && !tabs[i].playingAudio) {
+ this.zombify(tabs[i]);
+ }
+ }
+ }
+
+ // Change some preferences temporarily for only this session
+ let defaults = Services.prefs.getDefaultBranch(null);
+
+ // Reduce the amount of decoded image data we keep around
+ defaults.setIntPref("image.mem.max_decoded_image_kb", 0);
+
+ // Stop using the bfcache
+ if (!Services.prefs.getBoolPref("browser.sessionhistory.bfcacheIgnoreMemoryPressure")) {
+ defaults.setIntPref("browser.sessionhistory.max_total_viewers", 0);
+ }
+ },
+
+ zombify: function(tab) {
+ let browser = tab.browser;
+ let data = browser.__SS_data;
+ let extra = browser.__SS_extdata;
+
+ // Notify any interested parties (e.g. the session store)
+ // that the original tab object is going to be destroyed
+ let evt = document.createEvent("UIEvents");
+ evt.initUIEvent("TabPreZombify", true, false, window, null);
+ browser.dispatchEvent(evt);
+
+ // We need this data to correctly create and position the new browser
+ // If this browser is already a zombie, fallback to the session data
+ let currentURL = browser.__SS_restore ? data.entries[0].url : browser.currentURI.spec;
+ let sibling = browser.nextSibling;
+ let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(browser);
+
+ tab.destroy();
+ tab.create(currentURL, { sibling: sibling, zombifying: true, delayLoad: true, isPrivate: isPrivate });
+
+ // Reattach session store data and flag this browser so it is restored on select
+ browser = tab.browser;
+ browser.__SS_data = data;
+ browser.__SS_extdata = extra;
+ browser.__SS_restore = true;
+ browser.setAttribute("pending", "true");
+
+ // Notify the session store to reattach its listeners to the new tab object
+ evt = document.createEvent("UIEvents");
+ evt.initUIEvent("TabPostZombify", true, false, window, null);
+ browser.dispatchEvent(evt);
+ },
+
+ gc: function() {
+ window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).garbageCollect();
+ Cu.forceGC();
+ },
+
+ dumpMemoryStats: function(aLabel) {
+ let memDumper = Cc["@mozilla.org/memory-info-dumper;1"].getService(Ci.nsIMemoryInfoDumper);
+ memDumper.dumpMemoryInfoToTempDir(aLabel, /* anonymize = */ false,
+ /* minimize = */ false);
+ },
+};
diff --git a/mobile/android/chrome/content/OfflineApps.js b/mobile/android/chrome/content/OfflineApps.js
new file mode 100644
index 0000000000..e11b3c645d
--- /dev/null
+++ b/mobile/android/chrome/content/OfflineApps.js
@@ -0,0 +1,77 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var OfflineApps = {
+ offlineAppRequested: function(aContentWindow) {
+ if (!Services.prefs.getBoolPref("browser.offline-apps.notify"))
+ return;
+
+ let tab = BrowserApp.getTabForWindow(aContentWindow);
+ let currentURI = aContentWindow.document.documentURIObject;
+
+ // Don't bother showing UI if the user has already made a decision
+ if (Services.perms.testExactPermission(currentURI, "offline-app") != Services.perms.UNKNOWN_ACTION)
+ return;
+
+ try {
+ if (Services.prefs.getBoolPref("offline-apps.allow_by_default")) {
+ // All pages can use offline capabilities, no need to ask the user
+ return;
+ }
+ } catch(e) {
+ // This pref isn't set by default, ignore failures
+ }
+
+ let host = currentURI.asciiHost;
+ let notificationID = "offline-app-requested-" + host;
+
+ let strings = Strings.browser;
+ let buttons = [{
+ label: strings.GetStringFromName("offlineApps.dontAllow2"),
+ callback: function(aChecked) {
+ if (aChecked)
+ OfflineApps.disallowSite(aContentWindow.document);
+ }
+ },
+ {
+ label: strings.GetStringFromName("offlineApps.allow"),
+ callback: function() {
+ OfflineApps.allowSite(aContentWindow.document);
+ },
+ positive: true
+ }];
+
+ let requestor = BrowserApp.manifest ? "'" + BrowserApp.manifest.name + "'" : host;
+ let message = strings.formatStringFromName("offlineApps.ask", [requestor], 1);
+ let options = { checkbox: Strings.browser.GetStringFromName("offlineApps.dontAskAgain") };
+ NativeWindow.doorhanger.show(message, notificationID, buttons, tab.id, options);
+ },
+
+ allowSite: function(aDocument) {
+ Services.perms.add(aDocument.documentURIObject, "offline-app", Services.perms.ALLOW_ACTION);
+
+ // When a site is enabled while loading, manifest resources will
+ // start fetching immediately. This one time we need to do it
+ // ourselves.
+ this._startFetching(aDocument);
+ },
+
+ disallowSite: function(aDocument) {
+ Services.perms.add(aDocument.documentURIObject, "offline-app", Services.perms.DENY_ACTION);
+ },
+
+ _startFetching: function(aDocument) {
+ if (!aDocument.documentElement)
+ return;
+
+ let manifest = aDocument.documentElement.getAttribute("manifest");
+ if (!manifest)
+ return;
+
+ let manifestURI = Services.io.newURI(manifest, aDocument.characterSet, aDocument.documentURIObject);
+ let updateService = Cc["@mozilla.org/offlinecacheupdate-service;1"].getService(Ci.nsIOfflineCacheUpdateService);
+ updateService.scheduleUpdate(manifestURI, aDocument.documentURIObject, aDocument.nodePrincipal, window);
+ }
+};
diff --git a/mobile/android/chrome/content/PermissionsHelper.js b/mobile/android/chrome/content/PermissionsHelper.js
new file mode 100644
index 0000000000..ad1eb760aa
--- /dev/null
+++ b/mobile/android/chrome/content/PermissionsHelper.js
@@ -0,0 +1,188 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var PermissionsHelper = {
+ _permissonTypes: ["password", "geolocation", "popup", "indexedDB",
+ "offline-app", "desktop-notification", "plugins", "native-intent",
+ "flyweb-publish-server"],
+ _permissionStrings: {
+ "password": {
+ label: "password.logins",
+ allowed: "password.save",
+ denied: "password.dontSave"
+ },
+ "geolocation": {
+ label: "geolocation.location",
+ allowed: "geolocation.allow",
+ denied: "geolocation.dontAllow"
+ },
+ "flyweb-publish-server": {
+ label: "flyWebPublishServer.publishServer",
+ allowed: "flyWebPublishServer.allow",
+ denied: "flyWebPublishServer.dontAllow"
+ },
+ "popup": {
+ label: "blockPopups.label2",
+ allowed: "popup.show",
+ denied: "popup.dontShow"
+ },
+ "indexedDB": {
+ label: "offlineApps.offlineData",
+ allowed: "offlineApps.allow",
+ denied: "offlineApps.dontAllow2"
+ },
+ "offline-app": {
+ label: "offlineApps.offlineData",
+ allowed: "offlineApps.allow",
+ denied: "offlineApps.dontAllow2"
+ },
+ "desktop-notification": {
+ label: "desktopNotification.notifications",
+ allowed: "desktopNotification2.allow",
+ denied: "desktopNotification2.dontAllow"
+ },
+ "plugins": {
+ label: "clickToPlayPlugins.plugins",
+ allowed: "clickToPlayPlugins.activate",
+ denied: "clickToPlayPlugins.dontActivate"
+ },
+ "native-intent": {
+ label: "helperapps.openWithList2",
+ allowed: "helperapps.always",
+ denied: "helperapps.never"
+ }
+ },
+
+ observe: function observe(aSubject, aTopic, aData) {
+ let uri = BrowserApp.selectedBrowser.currentURI;
+ let check = false;
+
+ switch (aTopic) {
+ case "Permissions:Check":
+ check = true;
+ case "Permissions:Get":
+ let permissions = [];
+ for (let i = 0; i < this._permissonTypes.length; i++) {
+ let type = this._permissonTypes[i];
+ let value = this.getPermission(uri, type);
+
+ // Only add the permission if it was set by the user
+ if (value == Services.perms.UNKNOWN_ACTION)
+ continue;
+
+ if (check) {
+ Messaging.sendRequest({
+ type: "Permissions:CheckResult",
+ hasPermissions: true
+ });
+ return;
+ }
+ // Get the strings that correspond to the permission type
+ let typeStrings = this._permissionStrings[type];
+ let label = Strings.browser.GetStringFromName(typeStrings["label"]);
+
+ // Get the key to look up the appropriate string entity
+ let valueKey = value == Services.perms.ALLOW_ACTION ?
+ "allowed" : "denied";
+ let valueString = Strings.browser.GetStringFromName(typeStrings[valueKey]);
+
+ permissions.push({
+ type: type,
+ setting: label,
+ value: valueString
+ });
+ }
+
+ if (check) {
+ Messaging.sendRequest({
+ type: "Permissions:CheckResult",
+ hasPermissions: false
+ });
+ return;
+ }
+
+ // Keep track of permissions, so we know which ones to clear
+ this._currentPermissions = permissions;
+
+ Messaging.sendRequest({
+ type: "Permissions:Data",
+ permissions: permissions
+ });
+ break;
+
+ case "Permissions:Clear":
+ // An array of the indices of the permissions we want to clear
+ let permissionsToClear = JSON.parse(aData);
+ let privacyContext = BrowserApp.selectedBrowser.docShell
+ .QueryInterface(Ci.nsILoadContext);
+
+ for (let i = 0; i < permissionsToClear.length; i++) {
+ let indexToClear = permissionsToClear[i];
+ let permissionType = this._currentPermissions[indexToClear]["type"];
+ this.clearPermission(uri, permissionType, privacyContext);
+ }
+ break;
+ }
+ },
+
+ /**
+ * Gets the permission value stored for a specified permission type.
+ *
+ * @param aType
+ * The permission type string stored in permission manager.
+ * e.g. "geolocation", "indexedDB", "popup"
+ *
+ * @return A permission value defined in nsIPermissionManager.
+ */
+ getPermission: function getPermission(aURI, aType) {
+ // Password saving isn't a nsIPermissionManager permission type, so handle
+ // it seperately.
+ if (aType == "password") {
+ // By default, login saving is enabled, so if it is disabled, the
+ // user selected the never remember option
+ if (!Services.logins.getLoginSavingEnabled(aURI.prePath))
+ return Services.perms.DENY_ACTION;
+
+ // Check to see if the user ever actually saved a login
+ if (Services.logins.countLogins(aURI.prePath, "", ""))
+ return Services.perms.ALLOW_ACTION;
+
+ return Services.perms.UNKNOWN_ACTION;
+ }
+
+ // Geolocation consumers use testExactPermission
+ if (aType == "geolocation")
+ return Services.perms.testExactPermission(aURI, aType);
+
+ return Services.perms.testPermission(aURI, aType);
+ },
+
+ /**
+ * Clears a user-set permission value for the site given a permission type.
+ *
+ * @param aType
+ * The permission type string stored in permission manager.
+ * e.g. "geolocation", "indexedDB", "popup"
+ */
+ clearPermission: function clearPermission(aURI, aType, aContext) {
+ // Password saving isn't a nsIPermissionManager permission type, so handle
+ // it seperately.
+ if (aType == "password") {
+ // Get rid of exisiting stored logings
+ let logins = Services.logins.findLogins({}, aURI.prePath, "", "");
+ for (let i = 0; i < logins.length; i++) {
+ Services.logins.removeLogin(logins[i]);
+ }
+ // Re-set login saving to enabled
+ Services.logins.setLoginSavingEnabled(aURI.prePath, true);
+ } else {
+ Services.perms.remove(aURI, aType);
+ // Clear content prefs set in ContentPermissionPrompt.js
+ Cc["@mozilla.org/content-pref/service;1"]
+ .getService(Ci.nsIContentPrefService2)
+ .removeByDomainAndName(aURI.spec, aType + ".request.remember", aContext);
+ }
+ }
+};
diff --git a/mobile/android/chrome/content/PluginHelper.js b/mobile/android/chrome/content/PluginHelper.js
new file mode 100644
index 0000000000..59d87fa7c6
--- /dev/null
+++ b/mobile/android/chrome/content/PluginHelper.js
@@ -0,0 +1,221 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var PluginHelper = {
+ showDoorHanger: function(aTab) {
+ if (!aTab.browser)
+ return;
+
+ // Even though we may not end up showing a doorhanger, this flag
+ // lets us know that we've tried to show a doorhanger.
+ aTab.shouldShowPluginDoorhanger = false;
+
+ let uri = aTab.browser.currentURI;
+
+ // If the user has previously set a plugins permission for this website,
+ // either play or don't play the plugins instead of showing a doorhanger.
+ let permValue = Services.perms.testPermission(uri, "plugins");
+ if (permValue != Services.perms.UNKNOWN_ACTION) {
+ if (permValue == Services.perms.ALLOW_ACTION)
+ PluginHelper.playAllPlugins(aTab.browser.contentWindow);
+
+ return;
+ }
+
+ let message = Strings.browser.formatStringFromName("clickToPlayPlugins.message2",
+ [uri.host], 1);
+ let buttons = [
+ {
+ label: Strings.browser.GetStringFromName("clickToPlayPlugins.dontActivate"),
+ callback: function(aChecked) {
+ // If the user checked "Don't ask again", make a permanent exception
+ if (aChecked)
+ Services.perms.add(uri, "plugins", Ci.nsIPermissionManager.DENY_ACTION);
+
+ // Other than that, do nothing
+ }
+ },
+ {
+ label: Strings.browser.GetStringFromName("clickToPlayPlugins.activate"),
+ callback: function(aChecked) {
+ // If the user checked "Don't ask again", make a permanent exception
+ if (aChecked)
+ Services.perms.add(uri, "plugins", Ci.nsIPermissionManager.ALLOW_ACTION);
+
+ PluginHelper.playAllPlugins(aTab.browser.contentWindow);
+ },
+ positive: true
+ }
+ ];
+
+ // Add a checkbox with a "Don't ask again" message if the uri contains a
+ // host. Adding a permanent exception will fail if host is not present.
+ let options = uri.host ? { checkbox: Strings.browser.GetStringFromName("clickToPlayPlugins.dontAskAgain") } : {};
+
+ NativeWindow.doorhanger.show(message, "ask-to-play-plugins", buttons, aTab.id, options);
+ },
+
+ delayAndShowDoorHanger: function(aTab) {
+ // To avoid showing the doorhanger if there are also visible plugin
+ // overlays on the page, delay showing the doorhanger to check if
+ // visible plugins get added in the near future.
+ if (!aTab.pluginDoorhangerTimeout) {
+ aTab.pluginDoorhangerTimeout = setTimeout(function() {
+ if (this.shouldShowPluginDoorhanger) {
+ PluginHelper.showDoorHanger(this);
+ }
+ }.bind(aTab), 500);
+ }
+ },
+
+ playAllPlugins: function(aContentWindow) {
+ let cwu = aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ // XXX not sure if we should enable plugins for the parent documents...
+ let plugins = cwu.plugins;
+ if (!plugins || !plugins.length)
+ return;
+
+ plugins.forEach(this.playPlugin);
+ },
+
+ playPlugin: function(plugin) {
+ let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ if (!objLoadingContent.activated)
+ objLoadingContent.playPlugin();
+ },
+
+ getPluginPreference: function getPluginPreference() {
+ let pluginDisable = Services.prefs.getBoolPref("plugin.disable");
+ if (pluginDisable)
+ return "0";
+
+ let state = Services.prefs.getIntPref("plugin.default.state");
+ return state == Ci.nsIPluginTag.STATE_CLICKTOPLAY ? "2" : "1";
+ },
+
+ setPluginPreference: function setPluginPreference(aValue) {
+ switch (aValue) {
+ case "0": // Enable Plugins = No
+ Services.prefs.setBoolPref("plugin.disable", true);
+ Services.prefs.clearUserPref("plugin.default.state");
+ break;
+ case "1": // Enable Plugins = Yes
+ Services.prefs.clearUserPref("plugin.disable");
+ Services.prefs.setIntPref("plugin.default.state", Ci.nsIPluginTag.STATE_ENABLED);
+ break;
+ case "2": // Enable Plugins = Tap to Play (default)
+ Services.prefs.clearUserPref("plugin.disable");
+ Services.prefs.clearUserPref("plugin.default.state");
+ break;
+ }
+ },
+
+ // Copied from /browser/base/content/browser.js
+ isTooSmall : function (plugin, overlay) {
+ // Is the <object>'s size too small to hold what we want to show?
+ let pluginRect = plugin.getBoundingClientRect();
+ // XXX bug 446693. The text-shadow on the submitted-report text at
+ // the bottom causes scrollHeight to be larger than it should be.
+ let overflows = (overlay.scrollWidth > pluginRect.width) ||
+ (overlay.scrollHeight - 5 > pluginRect.height);
+
+ return overflows;
+ },
+
+ getPluginMimeType: function (plugin) {
+ var tagMimetype = plugin.actualType;
+
+ if (tagMimetype == "") {
+ tagMimetype = plugin.type;
+ }
+
+ return tagMimetype;
+ },
+
+ handlePluginBindingAttached: function (aTab, aEvent) {
+ let plugin = aEvent.target;
+ let doc = plugin.ownerDocument;
+ let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+ if (!overlay || overlay._bindingHandled) {
+ return;
+ }
+ overlay._bindingHandled = true;
+
+ let eventType = PluginHelper._getBindingType(plugin);
+ if (!eventType) {
+ // Not all bindings have handlers
+ return;
+ }
+
+ switch (eventType) {
+ case "PluginClickToPlay": {
+ // Check if plugins have already been activated for this page, or if
+ // the user has set a permission to always play plugins on the site
+ if (aTab.clickToPlayPluginsActivated ||
+ Services.perms.testPermission(aTab.browser.currentURI, "plugins") ==
+ Services.perms.ALLOW_ACTION) {
+ PluginHelper.playPlugin(plugin);
+ return;
+ }
+
+ // If the plugin is hidden, or if the overlay is too small, show a
+ // doorhanger notification
+ if (PluginHelper.isTooSmall(plugin, overlay)) {
+ PluginHelper.delayAndShowDoorHanger(aTab);
+ } else {
+ // There's a large enough visible overlay that we don't need to show
+ // the doorhanger.
+ aTab.shouldShowPluginDoorhanger = false;
+ overlay.classList.add("visible");
+ }
+
+ // Add click to play listener to the overlay
+ overlay.addEventListener("click", function(e) {
+ if (!e.isTrusted)
+ return;
+ e.preventDefault();
+ let win = e.target.ownerDocument.defaultView.top;
+ let tab = BrowserApp.getTabForWindow(win);
+ tab.clickToPlayPluginsActivated = true;
+ PluginHelper.playAllPlugins(win);
+
+ NativeWindow.doorhanger.hide("ask-to-play-plugins", tab.id);
+ }, true);
+
+ // Add handlers for over- and underflow in case the plugin gets resized
+ plugin.addEventListener("overflow", function(event) {
+ overlay.classList.remove("visible");
+ PluginHelper.delayAndShowDoorHanger(aTab);
+ });
+ plugin.addEventListener("underflow", function(event) {
+ // This is also triggered if only one dimension underflows,
+ // the other dimension might still overflow
+ if (!PluginHelper.isTooSmall(plugin, overlay)) {
+ overlay.classList.add("visible");
+ }
+ });
+
+ break;
+ }
+ }
+ },
+
+ // Helper to get the binding handler type from a plugin object
+ _getBindingType: function(plugin) {
+ if (!(plugin instanceof Ci.nsIObjectLoadingContent))
+ return null;
+
+ switch (plugin.pluginFallbackType) {
+ case Ci.nsIObjectLoadingContent.PLUGIN_UNSUPPORTED:
+ return "PluginNotFound";
+ case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY:
+ return "PluginClickToPlay";
+ default:
+ // Not all states map to a handler
+ return null;
+ }
+ }
+};
diff --git a/mobile/android/chrome/content/PresentationView.js b/mobile/android/chrome/content/PresentationView.js
new file mode 100644
index 0000000000..4f7e02870c
--- /dev/null
+++ b/mobile/android/chrome/content/PresentationView.js
@@ -0,0 +1,63 @@
+/* -*- Mode: tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+const TOPIC_PRESENTATION_VIEW_READY = "presentation-view-ready";
+const TOPIC_PRESENTATION_RECEIVER_LAUNCH = "presentation-receiver:launch";
+const TOPIC_PRESENTATION_RECEIVER_LAUNCH_RESPONSE = "presentation-receiver:launch:response";
+
+// globals Services
+Cu.import("resource://gre/modules/Services.jsm");
+
+function log(str) {
+ // dump("-*- PresentationView.js -*-: " + str + "\n");
+}
+
+let PresentationView = {
+ _id: null,
+
+ startup: function startup() {
+ // use hash as the ID of this top level window
+ this._id = window.location.hash.substr(1);
+
+ // Listen "presentation-receiver:launch" sent from
+ // PresentationRequestUIGlue.
+ Services.obs.addObserver(this,TOPIC_PRESENTATION_RECEIVER_LAUNCH, false);
+
+ // Notify PresentationView is ready.
+ Services.obs.notifyObservers(null, TOPIC_PRESENTATION_VIEW_READY, this._id);
+ },
+
+ stop: function stop() {
+ Services.obs.removeObserver(this, TOPIC_PRESENTATION_RECEIVER_LAUNCH);
+ },
+
+ observe: function observe(aSubject, aTopic, aData) {
+ log("Got observe: aTopic=" + aTopic);
+
+ let requestData = JSON.parse(aData);
+ if (this._id != requestData.windowId) {
+ return;
+ }
+
+ let browser = document.getElementById("content");
+ browser.setAttribute("mozpresentation", requestData.url);
+ try {
+ browser.loadURI(requestData.url);
+ Services.obs.notifyObservers(browser,
+ TOPIC_PRESENTATION_RECEIVER_LAUNCH_RESPONSE,
+ JSON.stringify({ result: "success",
+ requestId: requestData.requestId }));
+ } catch (e) {
+ Services.obs.notifyObservers(null,
+ TOPIC_PRESENTATION_RECEIVER_LAUNCH_RESPONSE,
+ JSON.stringify({ result: "error",
+ reason: e.message }));
+ }
+ }
+};
diff --git a/mobile/android/chrome/content/PresentationView.xul b/mobile/android/chrome/content/PresentationView.xul
new file mode 100644
index 0000000000..00440453c2
--- /dev/null
+++ b/mobile/android/chrome/content/PresentationView.xul
@@ -0,0 +1,15 @@
+<?xml version="1.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/. -->
+
+<window id="presentation-window"
+ onload="PresentationView.startup();"
+ onunload="PresentationView.stop();"
+ windowtype="navigator:browser"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <browser id="content" type="content-targetable" src="about:blank" flex="1"/>
+
+ <script type="application/javascript" src="chrome://browser/content/PresentationView.js"/>
+</window>
diff --git a/mobile/android/chrome/content/PrintHelper.js b/mobile/android/chrome/content/PrintHelper.js
new file mode 100644
index 0000000000..9b071ee928
--- /dev/null
+++ b/mobile/android/chrome/content/PrintHelper.js
@@ -0,0 +1,73 @@
+// -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+
+var PrintHelper = {
+ init: function() {
+ Services.obs.addObserver(this, "Print:PDF", false);
+ },
+
+ observe: function (aSubject, aTopic, aData) {
+ let browser = BrowserApp.selectedBrowser;
+
+ switch (aTopic) {
+ case "Print:PDF":
+ Messaging.handleRequest(aTopic, aData, (data) => {
+ return this.generatePDF(browser);
+ });
+ break;
+ }
+ },
+
+ generatePDF: function(aBrowser) {
+ // Create the final destination file location
+ let fileName = ContentAreaUtils.getDefaultFileName(aBrowser.contentTitle, aBrowser.currentURI, null, null);
+ fileName = fileName.trim() + ".pdf";
+
+ let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append(fileName);
+ file.createUnique(file.NORMAL_FILE_TYPE, parseInt("666", 8));
+
+ let printSettings = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(Ci.nsIPrintSettingsService).newPrintSettings;
+ printSettings.printSilent = true;
+ printSettings.showPrintProgress = false;
+ printSettings.printBGImages = false;
+ printSettings.printBGColors = false;
+ printSettings.printToFile = true;
+ printSettings.toFileName = file.path;
+ printSettings.printFrameType = Ci.nsIPrintSettings.kFramesAsIs;
+ printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
+
+ let webBrowserPrint = aBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebBrowserPrint);
+
+ return new Promise((resolve, reject) => {
+ webBrowserPrint.print(printSettings, {
+ onStateChange: function(webProgress, request, stateFlags, status) {
+ // We get two STATE_START calls, one for STATE_IS_DOCUMENT and one for STATE_IS_NETWORK
+ if (stateFlags & Ci.nsIWebProgressListener.STATE_START && stateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
+ // Let the user know something is happening. Generating the PDF can take some time.
+ Snackbars.show(Strings.browser.GetStringFromName("alertPrintjobToast"), Snackbars.LENGTH_LONG);
+ }
+
+ // We get two STATE_STOP calls, one for STATE_IS_DOCUMENT and one for STATE_IS_NETWORK
+ if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP && stateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
+ if (Components.isSuccessCode(status)) {
+ // Send the details to Java
+ resolve({ file: file.path, title: fileName });
+ } else {
+ reject();
+ }
+ }
+ },
+ onProgressChange: function () {},
+ onLocationChange: function () {},
+ onStatusChange: function () {},
+ onSecurityChange: function () {},
+ });
+ });
+ }
+};
diff --git a/mobile/android/chrome/content/Reader.js b/mobile/android/chrome/content/Reader.js
new file mode 100644
index 0000000000..d0f3d7801a
--- /dev/null
+++ b/mobile/android/chrome/content/Reader.js
@@ -0,0 +1,290 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+
+/*globals MAX_URI_LENGTH, MAX_TITLE_LENGTH */
+
+var Reader = {
+ // These values should match those defined in BrowserContract.java.
+ STATUS_UNFETCHED: 0,
+ STATUS_FETCH_FAILED_TEMPORARY: 1,
+ STATUS_FETCH_FAILED_PERMANENT: 2,
+ STATUS_FETCH_FAILED_UNSUPPORTED_FORMAT: 3,
+ STATUS_FETCHED_ARTICLE: 4,
+
+ get _hasUsedToolbar() {
+ delete this._hasUsedToolbar;
+ return this._hasUsedToolbar = Services.prefs.getBoolPref("reader.has_used_toolbar");
+ },
+
+ /**
+ * BackPressListener (listeners / ReaderView Ids).
+ */
+ _backPressListeners: [],
+ _backPressViewIds: [],
+
+ /**
+ * Set a backPressListener for this tabId / ReaderView Id pair.
+ */
+ _addBackPressListener: function(tabId, viewId, listener) {
+ this._backPressListeners[tabId] = listener;
+ this._backPressViewIds[viewId] = tabId;
+ },
+
+ /**
+ * Remove a backPressListener for this ReaderView Id.
+ */
+ _removeBackPressListener: function(viewId) {
+ let tabId = this._backPressViewIds[viewId];
+ if (tabId != undefined) {
+ this._backPressListeners[tabId] = null;
+ delete this._backPressViewIds[viewId];
+ }
+ },
+
+ /**
+ * If the requested tab has a backPress listener, return its results, else false.
+ */
+ onBackPress: function(tabId) {
+ let listener = this._backPressListeners[tabId];
+ return { handled: (listener ? listener() : false) };
+ },
+
+ observe: function Reader_observe(aMessage, aTopic, aData) {
+ switch (aTopic) {
+ case "Reader:RemoveFromCache": {
+ ReaderMode.removeArticleFromCache(aData).catch(e => Cu.reportError("Error removing article from cache: " + e));
+ break;
+ }
+
+ case "Reader:AddToCache": {
+ let tab = BrowserApp.getTabForId(aData);
+ if (!tab) {
+ throw new Error("No tab for tabID = " + aData + " when trying to save reader view article");
+ }
+
+ // If the article is coming from reader mode, we must have fetched it already.
+ this._getArticleData(tab.browser).then((article) => {
+ ReaderMode.storeArticleInCache(article);
+ }).catch(e => Cu.reportError("Error storing article in cache: " + e));
+ break;
+ }
+ }
+ },
+
+ receiveMessage: function(message) {
+ switch (message.name) {
+ case "Reader:ArticleGet":
+ this._getArticle(message.data.url).then((article) => {
+ // Make sure the target browser is still alive before trying to send data back.
+ if (message.target.messageManager) {
+ message.target.messageManager.sendAsyncMessage("Reader:ArticleData", { article: article });
+ }
+ }, e => {
+ if (e && e.newURL) {
+ message.target.loadURI("about:reader?url=" + encodeURIComponent(e.newURL));
+ }
+ });
+ break;
+
+ // On DropdownClosed in ReaderView, we cleanup / clear existing BackPressListener.
+ case "Reader:DropdownClosed": {
+ this._removeBackPressListener(message.data);
+ break;
+ }
+
+ // On DropdownOpened in ReaderView, we add BackPressListener to handle a subsequent BACK request.
+ case "Reader:DropdownOpened": {
+ let tabId = BrowserApp.selectedTab.id;
+ this._addBackPressListener(tabId, message.data, () => {
+ // User hit BACK key while ReaderView has the banner font-dropdown opened.
+ // Close it and return prevent-default.
+ if (message.target.messageManager) {
+ message.target.messageManager.sendAsyncMessage("Reader:CloseDropdown");
+ return true;
+ }
+ // We can assume ReaderView banner's font-dropdown doesn't need to be closed.
+ return false;
+ });
+
+ break;
+ }
+
+ case "Reader:FaviconRequest": {
+ Messaging.sendRequestForResult({
+ type: "Reader:FaviconRequest",
+ url: message.data.url
+ }).then(data => {
+ message.target.messageManager.sendAsyncMessage("Reader:FaviconReturn", JSON.parse(data));
+ });
+ break;
+ }
+
+ case "Reader:SystemUIVisibility":
+ Messaging.sendRequest({
+ type: "SystemUI:Visibility",
+ visible: message.data.visible
+ });
+ break;
+
+ case "Reader:ToolbarHidden":
+ if (!this._hasUsedToolbar) {
+ Snackbars.show(Strings.browser.GetStringFromName("readerMode.toolbarTip"), Snackbars.LENGTH_LONG);
+ Services.prefs.setBoolPref("reader.has_used_toolbar", true);
+ this._hasUsedToolbar = true;
+ }
+ break;
+
+ case "Reader:UpdateReaderButton": {
+ let tab = BrowserApp.getTabForBrowser(message.target);
+ tab.browser.isArticle = message.data.isArticle;
+ this.updatePageAction(tab);
+ break;
+ }
+ }
+ },
+
+ pageAction: {
+ readerModeCallback: function(browser) {
+ let url = browser.currentURI.spec;
+ if (url.startsWith("about:reader")) {
+ UITelemetry.addEvent("action.1", "button", null, "reader_exit");
+ } else {
+ UITelemetry.addEvent("action.1", "button", null, "reader_enter");
+ }
+ browser.messageManager.sendAsyncMessage("Reader:ToggleReaderMode");
+ },
+ },
+
+ updatePageAction: function(tab) {
+ if (!tab.getActive()) {
+ return;
+ }
+
+ if (this.pageAction.id) {
+ PageActions.remove(this.pageAction.id);
+ delete this.pageAction.id;
+ }
+
+ let showPageAction = (icon, title) => {
+ this.pageAction.id = PageActions.add({
+ icon: icon,
+ title: title,
+ clickCallback: () => this.pageAction.readerModeCallback(browser),
+ important: true
+ });
+ };
+
+ let browser = tab.browser;
+ if (browser.currentURI.spec.startsWith("about:reader")) {
+ showPageAction("drawable://reader_active", Strings.reader.GetStringFromName("readerView.close"));
+ // Only start a reader session if the viewer is in the foreground. We do
+ // not track background reader viewers.
+ UITelemetry.startSession("reader.1", null);
+ return;
+ }
+
+ // Only stop a reader session if the foreground viewer is not visible.
+ UITelemetry.stopSession("reader.1", "", null);
+
+ if (browser.isArticle) {
+ showPageAction("drawable://reader", Strings.reader.GetStringFromName("readerView.enter"));
+ UITelemetry.addEvent("show.1", "button", null, "reader_available");
+ } else {
+ UITelemetry.addEvent("show.1", "button", null, "reader_unavailable");
+ }
+ },
+
+ /**
+ * Gets an article for a given URL. This method will download and parse a document
+ * if it does not find the article in the cache.
+ *
+ * @param url The article URL.
+ * @return {Promise}
+ * @resolves JS object representing the article, or null if no article is found.
+ */
+ _getArticle: Task.async(function* (url) {
+ // First try to find a parsed article in the cache.
+ let article = yield ReaderMode.getArticleFromCache(url);
+ if (article) {
+ return article;
+ }
+
+ // Article hasn't been found in the cache, we need to
+ // download the page and parse the article out of it.
+ return yield ReaderMode.downloadAndParseDocument(url).catch(e => {
+ if (e && e.newURL) {
+ // Pass up the error so we can navigate the browser in question to the new URL:
+ throw e;
+ }
+ Cu.reportError("Error downloading and parsing document: " + e);
+ return null;
+ });
+ }),
+
+ _getArticleData: function(browser) {
+ return new Promise((resolve, reject) => {
+ if (browser == null) {
+ reject("_getArticleData needs valid browser");
+ }
+
+ let mm = browser.messageManager;
+ let listener = (message) => {
+ mm.removeMessageListener("Reader:StoredArticleData", listener);
+ resolve(message.data.article);
+ };
+ mm.addMessageListener("Reader:StoredArticleData", listener);
+ mm.sendAsyncMessage("Reader:GetStoredArticleData");
+ });
+ },
+
+
+ /**
+ * Migrates old indexedDB reader mode cache to new JSON cache.
+ */
+ migrateCache: Task.async(function* () {
+ let cacheDB = yield new Promise((resolve, reject) => {
+ let request = window.indexedDB.open("about:reader", 1);
+ request.onsuccess = event => resolve(event.target.result);
+ request.onerror = event => reject(request.error);
+
+ // If there is no DB to migrate, don't do anything.
+ request.onupgradeneeded = event => resolve(null);
+ });
+
+ if (!cacheDB) {
+ return;
+ }
+
+ let articles = yield new Promise((resolve, reject) => {
+ let articles = [];
+
+ let transaction = cacheDB.transaction(cacheDB.objectStoreNames);
+ let store = transaction.objectStore(cacheDB.objectStoreNames[0]);
+
+ let request = store.openCursor();
+ request.onsuccess = event => {
+ let cursor = event.target.result;
+ if (!cursor) {
+ resolve(articles);
+ } else {
+ articles.push(cursor.value);
+ cursor.continue();
+ }
+ };
+ request.onerror = event => reject(request.error);
+ });
+
+ for (let article of articles) {
+ yield ReaderMode.storeArticleInCache(article);
+ }
+
+ // Delete the database.
+ window.indexedDB.deleteDatabase("about:reader");
+ }),
+};
diff --git a/mobile/android/chrome/content/RemoteDebugger.js b/mobile/android/chrome/content/RemoteDebugger.js
new file mode 100644
index 0000000000..a5a3a43dee
--- /dev/null
+++ b/mobile/android/chrome/content/RemoteDebugger.js
@@ -0,0 +1,355 @@
+// -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* globals DebuggerServer */
+"use strict";
+
+XPCOMUtils.defineLazyGetter(this, "DebuggerServer", () => {
+ let { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ let { DebuggerServer } = require("devtools/server/main");
+ return DebuggerServer;
+});
+
+var RemoteDebugger = {
+ init() {
+ USBRemoteDebugger.init();
+ WiFiRemoteDebugger.init();
+ },
+
+ get isAnyEnabled() {
+ return USBRemoteDebugger.isEnabled || WiFiRemoteDebugger.isEnabled;
+ },
+
+ /**
+ * Prompt the user to accept or decline the incoming connection.
+ *
+ * @param session object
+ * The session object will contain at least the following fields:
+ * {
+ * authentication,
+ * client: {
+ * host,
+ * port
+ * },
+ * server: {
+ * host,
+ * port
+ * }
+ * }
+ * Specific authentication modes may include additional fields. Check
+ * the different |allowConnection| methods in
+ * devtools/shared/security/auth.js.
+ * @return An AuthenticationResult value.
+ * A promise that will be resolved to the above is also allowed.
+ */
+ allowConnection(session) {
+ if (this._promptingForAllow) {
+ // Don't stack connection prompts if one is already open
+ return DebuggerServer.AuthenticationResult.DENY;
+ }
+
+ if (!session.server.port) {
+ this._promptingForAllow = this._promptForUSB(session);
+ } else {
+ this._promptingForAllow = this._promptForTCP(session);
+ }
+ this._promptingForAllow.then(() => this._promptingForAllow = null);
+
+ return this._promptingForAllow;
+ },
+
+ _promptForUSB(session) {
+ if (session.authentication !== 'PROMPT') {
+ // This dialog is not prepared for any other authentication method at
+ // this time.
+ return DebuggerServer.AuthenticationResult.DENY;
+ }
+
+ return new Promise(resolve => {
+ let title = Strings.browser.GetStringFromName("remoteIncomingPromptTitle");
+ let msg = Strings.browser.GetStringFromName("remoteIncomingPromptUSB");
+ let allow = Strings.browser.GetStringFromName("remoteIncomingPromptAllow");
+ let deny = Strings.browser.GetStringFromName("remoteIncomingPromptDeny");
+
+ // Make prompt. Note: button order is in reverse.
+ let prompt = new Prompt({
+ window: null,
+ hint: "remotedebug",
+ title: title,
+ message: msg,
+ buttons: [ allow, deny ],
+ priority: 1
+ });
+
+ prompt.show(data => {
+ let result = data.button;
+ if (result === 0) {
+ resolve(DebuggerServer.AuthenticationResult.ALLOW);
+ } else {
+ resolve(DebuggerServer.AuthenticationResult.DENY);
+ }
+ });
+ });
+ },
+
+ _promptForTCP(session) {
+ if (session.authentication !== 'OOB_CERT' || !session.client.cert) {
+ // This dialog is not prepared for any other authentication method at
+ // this time.
+ return DebuggerServer.AuthenticationResult.DENY;
+ }
+
+ return new Promise(resolve => {
+ let title = Strings.browser.GetStringFromName("remoteIncomingPromptTitle");
+ let msg = Strings.browser.formatStringFromName("remoteIncomingPromptTCP", [
+ session.client.host,
+ session.client.port
+ ], 2);
+ let scan = Strings.browser.GetStringFromName("remoteIncomingPromptScan");
+ let scanAndRemember = Strings.browser.GetStringFromName("remoteIncomingPromptScanAndRemember");
+ let deny = Strings.browser.GetStringFromName("remoteIncomingPromptDeny");
+
+ // Make prompt. Note: button order is in reverse.
+ let prompt = new Prompt({
+ window: null,
+ hint: "remotedebug",
+ title: title,
+ message: msg,
+ buttons: [ scan, scanAndRemember, deny ],
+ priority: 1
+ });
+
+ prompt.show(data => {
+ let result = data.button;
+ if (result === 0) {
+ resolve(DebuggerServer.AuthenticationResult.ALLOW);
+ } else if (result === 1) {
+ resolve(DebuggerServer.AuthenticationResult.ALLOW_PERSIST);
+ } else {
+ resolve(DebuggerServer.AuthenticationResult.DENY);
+ }
+ });
+ });
+ },
+
+ /**
+ * During OOB_CERT authentication, the user must transfer some data through
+ * some out of band mechanism from the client to the server to authenticate
+ * the devices.
+ *
+ * This implementation instructs Fennec to invoke a QR decoder and return the
+ * the data it contains back here.
+ *
+ * @return An object containing:
+ * * sha256: hash(ClientCert)
+ * * k : K(random 128-bit number)
+ * A promise that will be resolved to the above is also allowed.
+ */
+ receiveOOB() {
+ if (this._receivingOOB) {
+ return this._receivingOOB;
+ }
+
+ this._receivingOOB = Messaging.sendRequestForResult({
+ type: "DevToolsAuth:Scan"
+ }).then(data => {
+ return JSON.parse(data);
+ }, () => {
+ let title = Strings.browser.GetStringFromName("remoteQRScanFailedPromptTitle");
+ let msg = Strings.browser.GetStringFromName("remoteQRScanFailedPromptMessage");
+ let ok = Strings.browser.GetStringFromName("remoteQRScanFailedPromptOK");
+ let prompt = new Prompt({
+ window: null,
+ hint: "remotedebug",
+ title: title,
+ message: msg,
+ buttons: [ ok ],
+ priority: 1
+ });
+ prompt.show();
+ });
+
+ this._receivingOOB.then(() => this._receivingOOB = null);
+
+ return this._receivingOOB;
+ },
+
+ initServer: function() {
+ if (DebuggerServer.initialized) {
+ return;
+ }
+
+ DebuggerServer.init();
+
+ // Add browser and Fennec specific actors
+ DebuggerServer.addBrowserActors();
+ DebuggerServer.registerModule("resource://gre/modules/dbg-browser-actors.js");
+
+ // Allow debugging of chrome for any process
+ DebuggerServer.allowChromeProcess = true;
+ }
+};
+
+RemoteDebugger.allowConnection =
+ RemoteDebugger.allowConnection.bind(RemoteDebugger);
+RemoteDebugger.receiveOOB =
+ RemoteDebugger.receiveOOB.bind(RemoteDebugger);
+
+var USBRemoteDebugger = {
+
+ init() {
+ Services.prefs.addObserver("devtools.", this, false);
+
+ if (this.isEnabled) {
+ this.start();
+ }
+ },
+
+ observe(subject, topic, data) {
+ if (topic != "nsPref:changed") {
+ return;
+ }
+
+ switch (data) {
+ case "devtools.remote.usb.enabled":
+ Services.prefs.setBoolPref("devtools.debugger.remote-enabled",
+ RemoteDebugger.isAnyEnabled);
+ if (this.isEnabled) {
+ this.start();
+ } else {
+ this.stop();
+ }
+ break;
+
+ case "devtools.debugger.remote-port":
+ case "devtools.debugger.unix-domain-socket":
+ if (this.isEnabled) {
+ this.stop();
+ this.start();
+ }
+ break;
+ }
+ },
+
+ get isEnabled() {
+ return Services.prefs.getBoolPref("devtools.remote.usb.enabled");
+ },
+
+ start: function() {
+ if (this._listener) {
+ return;
+ }
+
+ RemoteDebugger.initServer();
+
+ let portOrPath =
+ Services.prefs.getCharPref("devtools.debugger.unix-domain-socket") ||
+ Services.prefs.getIntPref("devtools.debugger.remote-port");
+
+ try {
+ dump("Starting USB debugger on " + portOrPath);
+ let AuthenticatorType = DebuggerServer.Authenticators.get("PROMPT");
+ let authenticator = new AuthenticatorType.Server();
+ authenticator.allowConnection = RemoteDebugger.allowConnection;
+ this._listener = DebuggerServer.createListener();
+ this._listener.portOrPath = portOrPath;
+ this._listener.authenticator = authenticator;
+ this._listener.open();
+ } catch (e) {
+ dump("Unable to start USB debugger server: " + e);
+ }
+ },
+
+ stop: function() {
+ if (!this._listener) {
+ return;
+ }
+
+ try {
+ this._listener.close();
+ this._listener = null;
+ } catch (e) {
+ dump("Unable to stop USB debugger server: " + e);
+ }
+ }
+
+};
+
+var WiFiRemoteDebugger = {
+
+ init() {
+ Services.prefs.addObserver("devtools.", this, false);
+
+ if (this.isEnabled) {
+ this.start();
+ }
+ },
+
+ observe(subject, topic, data) {
+ if (topic != "nsPref:changed") {
+ return;
+ }
+
+ switch (data) {
+ case "devtools.remote.wifi.enabled":
+ Services.prefs.setBoolPref("devtools.debugger.remote-enabled",
+ RemoteDebugger.isAnyEnabled);
+ // Allow remote debugging on non-local interfaces when WiFi debug is
+ // enabled
+ // TODO: Bug 1034411: Lock down to WiFi interface only
+ Services.prefs.setBoolPref("devtools.debugger.force-local",
+ !this.isEnabled);
+ if (this.isEnabled) {
+ this.start();
+ } else {
+ this.stop();
+ }
+ break;
+ }
+ },
+
+ get isEnabled() {
+ return Services.prefs.getBoolPref("devtools.remote.wifi.enabled");
+ },
+
+ start: function() {
+ if (this._listener) {
+ return;
+ }
+
+ RemoteDebugger.initServer();
+
+ try {
+ dump("Starting WiFi debugger");
+ let AuthenticatorType = DebuggerServer.Authenticators.get("OOB_CERT");
+ let authenticator = new AuthenticatorType.Server();
+ authenticator.allowConnection = RemoteDebugger.allowConnection;
+ authenticator.receiveOOB = RemoteDebugger.receiveOOB;
+ this._listener = DebuggerServer.createListener();
+ this._listener.portOrPath = -1 /* any available port */;
+ this._listener.authenticator = authenticator;
+ this._listener.discoverable = true;
+ this._listener.encryption = true;
+ this._listener.open();
+ let port = this._listener.port;
+ dump("Started WiFi debugger on " + port);
+ } catch (e) {
+ dump("Unable to start WiFi debugger server: " + e);
+ }
+ },
+
+ stop: function() {
+ if (!this._listener) {
+ return;
+ }
+
+ try {
+ this._listener.close();
+ this._listener = null;
+ } catch (e) {
+ dump("Unable to stop WiFi debugger server: " + e);
+ }
+ }
+
+};
diff --git a/mobile/android/chrome/content/SelectHelper.js b/mobile/android/chrome/content/SelectHelper.js
new file mode 100644
index 0000000000..41d0193d4c
--- /dev/null
+++ b/mobile/android/chrome/content/SelectHelper.js
@@ -0,0 +1,161 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var SelectHelper = {
+ _uiBusy: false,
+
+ handleEvent: function(event) {
+ this.handleClick(event.target);
+ },
+
+ handleClick: function(target) {
+ // if we're busy looking at a select we want to eat any clicks that
+ // come to us, but not to process them
+ if (this._uiBusy || !this._isMenu(target) || this._isDisabledElement(target)) {
+ return;
+ }
+
+ this._uiBusy = true;
+ this.show(target);
+ this._uiBusy = false;
+ },
+
+ // This is a callback function to be provided to prompt.show(callBack).
+ // It will update which Option elements in a Select have been selected
+ // or unselected and fire the onChange event.
+ _promptCallBack: function(data, element) {
+ let selected = data.list;
+
+ if (element instanceof Ci.nsIDOMXULMenuListElement) {
+ if (element.selectedIndex != selected[0]) {
+ element.selectedIndex = selected[0];
+ this.fireOnCommand(element);
+ }
+ } else if (element instanceof HTMLSelectElement) {
+ let changed = false;
+ let i = 0; // The index for the element from `data.list` that we are currently examining.
+ this.forVisibleOptions(element, function(node) {
+ if (node.selected && selected.indexOf(i) == -1) {
+ changed = true;
+ node.selected = false;
+ } else if (!node.selected && selected.indexOf(i) != -1) {
+ changed = true;
+ node.selected = true;
+ }
+ i++;
+ });
+
+ if (changed) {
+ this.fireOnChange(element);
+ }
+ }
+ },
+
+ show: function(element) {
+ let list = this.getListForElement(element);
+ let p = new Prompt({
+ window: element.ownerDocument.defaultView
+ });
+
+ if (element.multiple) {
+ p.addButton({
+ label: Strings.browser.GetStringFromName("selectHelper.closeMultipleSelectDialog")
+ }).setMultiChoiceItems(list);
+ } else {
+ p.setSingleChoiceItems(list);
+ }
+
+ p.show((data) => {
+ this._promptCallBack(data,element)
+ });
+ },
+
+ _isMenu: function(element) {
+ return (element instanceof HTMLSelectElement || element instanceof Ci.nsIDOMXULMenuListElement);
+ },
+
+ // Return a list of Option elements within a Select excluding
+ // any that were not visible.
+ getListForElement: function(element) {
+ let index = 0;
+ let items = [];
+ this.forVisibleOptions(element, function(node, options,parent) {
+ let item = {
+ label: node.text || node.label,
+ header: options.isGroup,
+ disabled: node.disabled,
+ id: index,
+ selected: node.selected,
+ };
+
+ if (parent) {
+ item.child = true;
+ item.disabled = item.disabled || parent.disabled;
+ }
+ items.push(item);
+ index++;
+ });
+ return items;
+ },
+
+ // Apply a function to all visible Option elements in a Select
+ forVisibleOptions: function(element, aFunction, parent = null) {
+ if (element instanceof Ci.nsIDOMXULMenuListElement) {
+ element = element.menupopup;
+ }
+ let children = element.children;
+ let numChildren = children.length;
+
+
+ // if there are no children in this select, we add a dummy row so that at least something appears
+ if (numChildren == 0) {
+ aFunction.call(this, {label: ""}, {isGroup: false}, parent);
+ }
+
+ for (let i = 0; i < numChildren; i++) {
+ let child = children[i];
+ let style = window.getComputedStyle(child, null);
+ if (style.display !== "none") {
+ if (child instanceof HTMLOptionElement ||
+ child instanceof Ci.nsIDOMXULSelectControlItemElement) {
+ aFunction.call(this, child, {isGroup: false}, parent);
+ } else if (child instanceof HTMLOptGroupElement) {
+ aFunction.call(this, child, {isGroup: true});
+ this.forVisibleOptions(child, aFunction, child);
+ }
+ }
+ }
+ },
+
+ fireOnChange: function(element) {
+ let event = element.ownerDocument.createEvent("Events");
+ event.initEvent("change", true, true, element.defaultView, 0,
+ false, false, false, false, null);
+ setTimeout(function() {
+ element.dispatchEvent(event);
+ }, 0);
+ },
+
+ fireOnCommand: function(element) {
+ let event = element.ownerDocument.createEvent("XULCommandEvent");
+ event.initCommandEvent("command", true, true, element.defaultView, 0,
+ false, false, false, false, null);
+ setTimeout(function() {
+ element.dispatchEvent(event);
+ }, 0);
+ },
+
+ _isDisabledElement : function(element) {
+ let currentElement = element;
+ while (currentElement) {
+ // Must test with === in case a form has a field named "disabled". See bug 1263589.
+ if (currentElement.disabled === true) {
+ return true;
+ }
+ currentElement = currentElement.parentElement;
+ }
+ return false;
+ }
+};
diff --git a/mobile/android/chrome/content/WebcompatReporter.js b/mobile/android/chrome/content/WebcompatReporter.js
new file mode 100644
index 0000000000..66aefdda04
--- /dev/null
+++ b/mobile/android/chrome/content/WebcompatReporter.js
@@ -0,0 +1,144 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+
+var WebcompatReporter = {
+ menuItem: null,
+ menuItemEnabled: null,
+ init: function() {
+ Services.obs.addObserver(this, "DesktopMode:Change", false);
+ Services.obs.addObserver(this, "chrome-document-global-created", false);
+ Services.obs.addObserver(this, "content-document-global-created", false);
+
+ let visible = true;
+ if ("@mozilla.org/parental-controls-service;1" in Cc) {
+ let pc = Cc["@mozilla.org/parental-controls-service;1"].createInstance(Ci.nsIParentalControlsService);
+ visible = !pc.parentalControlsEnabled;
+ }
+
+ this.addMenuItem(visible);
+ },
+
+ observe: function(subject, topic, data) {
+ if (topic == "content-document-global-created" || topic == "chrome-document-global-created") {
+ let win = subject;
+ let currentURI = win.document.documentURI;
+
+ // Ignore non top-level documents
+ if (currentURI !== win.top.location.href) {
+ return;
+ }
+
+ if (!this.menuItemEnabled && this.isReportableUrl(currentURI)) {
+ NativeWindow.menu.update(this.menuItem, {enabled: true});
+ this.menuItemEnabled = true;
+ } else if (this.menuItemEnabled && !this.isReportableUrl(currentURI)) {
+ NativeWindow.menu.update(this.menuItem, {enabled: false});
+ this.menuItemEnabled = false;
+ }
+ } else if (topic === "DesktopMode:Change") {
+ let args = JSON.parse(data);
+ let tab = BrowserApp.getTabForId(args.tabId);
+ let currentURI = tab.browser.currentURI.spec;
+ if (args.desktopMode && this.isReportableUrl(currentURI)) {
+ this.reportDesktopModePrompt(tab);
+ }
+ }
+ },
+
+ addMenuItem: function(visible) {
+ this.menuItem = NativeWindow.menu.add({
+ name: this.strings.GetStringFromName("webcompat.menu.name"),
+ callback: () => {
+ Promise.resolve(BrowserApp.selectedTab).then(this.getScreenshot)
+ .then(this.reportIssue)
+ .catch(Cu.reportError);
+ },
+ enabled: false,
+ visible: visible,
+ });
+ },
+
+ getScreenshot: (tab) => {
+ return new Promise((resolve) => {
+ try {
+ let win = tab.window;
+ let dpr = win.devicePixelRatio;
+ let canvas = win.document.createElement("canvas");
+ let ctx = canvas.getContext("2d");
+ // Grab the visible viewport coordinates
+ let x = win.document.documentElement.scrollLeft;
+ let y = win.document.documentElement.scrollTop;
+ let w = win.innerWidth;
+ let h = win.innerHeight;
+ // Scale according to devicePixelRatio and coordinates
+ canvas.width = dpr * w;
+ canvas.height = dpr * h;
+ ctx.scale(dpr, dpr);
+ ctx.drawWindow(win, x, y, w, h, '#ffffff');
+ let screenshot = canvas.toDataURL();
+ resolve({tab: tab, data: screenshot});
+ } catch (e) {
+ // drawWindow can fail depending on memory or surface size. Rather than reject here,
+ // we resolve the URL so the user can continue to file an issue without a screenshot.
+ Cu.reportError("WebCompatReporter: getting a screenshot failed: " + e);
+ resolve({tab: tab});
+ }
+ });
+ },
+
+ isReportableUrl: function(url) {
+ return url && !(url.startsWith("about") ||
+ url.startsWith("chrome") ||
+ url.startsWith("file") ||
+ url.startsWith("resource"));
+ },
+
+ reportDesktopModePrompt: function(tab) {
+ let message = this.strings.GetStringFromName("webcompat.reportDesktopMode.message");
+ let options = {
+ action: {
+ label: this.strings.GetStringFromName("webcompat.reportDesktopModeYes.label"),
+ callback: () => this.reportIssue({tab: tab})
+ }
+ };
+ Snackbars.show(message, Snackbars.LENGTH_LONG, options);
+ },
+
+ reportIssue: (tabData) => {
+ return new Promise((resolve) => {
+ const WEBCOMPAT_ORIGIN = "https://webcompat.com";
+ let url = tabData.tab.browser.currentURI.spec
+ let webcompatURL = `${WEBCOMPAT_ORIGIN}/issues/new?url=${url}`;
+
+ if (tabData.data && typeof tabData.data === "string") {
+ BrowserApp.deck.addEventListener("DOMContentLoaded", function sendDataToTab(event) {
+ BrowserApp.deck.removeEventListener("DOMContentLoaded", sendDataToTab, false);
+
+ if (event.target.defaultView.location.origin === WEBCOMPAT_ORIGIN) {
+ // Waive Xray vision so event.origin is not chrome://browser on the other side.
+ let win = Cu.waiveXrays(event.target.defaultView);
+ win.postMessage(tabData.data, WEBCOMPAT_ORIGIN);
+ }
+ }, false);
+ }
+
+ let isPrivateTab = PrivateBrowsingUtils.isBrowserPrivate(tabData.tab.browser);
+ BrowserApp.addTab(webcompatURL, {parentId: tabData.tab.id, isPrivate: isPrivateTab});
+ resolve();
+ });
+ }
+};
+
+XPCOMUtils.defineLazyGetter(WebcompatReporter, "strings", function() {
+ return Services.strings.createBundle("chrome://browser/locale/webcompatReporter.properties");
+});
diff --git a/mobile/android/chrome/content/WebrtcUI.js b/mobile/android/chrome/content/WebrtcUI.js
new file mode 100644
index 0000000000..475d05bd2c
--- /dev/null
+++ b/mobile/android/chrome/content/WebrtcUI.js
@@ -0,0 +1,302 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["WebrtcUI"];
+
+XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls", "@mozilla.org/parental-controls-service;1", "nsIParentalControlsService");
+XPCOMUtils.defineLazyModuleGetter(this, "RuntimePermissions", "resource://gre/modules/RuntimePermissions.jsm");
+
+var WebrtcUI = {
+ _notificationId: null,
+
+ // Add-ons can override stock permission behavior by doing:
+ //
+ // var stockObserve = WebrtcUI.observe;
+ //
+ // webrtcUI.observe = function(aSubject, aTopic, aData) {
+ // switch (aTopic) {
+ // case "PeerConnection:request": {
+ // // new code.
+ // break;
+ // ...
+ // default:
+ // return stockObserve.call(this, aSubject, aTopic, aData);
+ //
+ // See browser/modules/webrtcUI.jsm for details.
+
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic === "getUserMedia:request") {
+ RuntimePermissions
+ .waitForPermissions(this._determineNeededRuntimePermissions(aSubject))
+ .then((permissionGranted) => {
+ if (permissionGranted) {
+ WebrtcUI.handleGumRequest(aSubject, aTopic, aData);
+ } else {
+ Services.obs.notifyObservers(null, "getUserMedia:response:deny", aSubject.callID);
+ }});
+ } else if (aTopic === "PeerConnection:request") {
+ this.handlePCRequest(aSubject, aTopic, aData);
+ } else if (aTopic === "recording-device-events") {
+ switch (aData) {
+ case "shutdown":
+ case "starting":
+ this.notify();
+ break;
+ }
+ } else if (aTopic === "VideoCapture:Paused") {
+ if (this._notificationId) {
+ Notifications.cancel(this._notificationId);
+ this._notificationId = null;
+ }
+ } else if (aTopic === "VideoCapture:Resumed") {
+ this.notify();
+ }
+ },
+
+ notify: function() {
+ let windows = MediaManagerService.activeMediaCaptureWindows;
+ let count = windows.length;
+ let msg = {};
+ if (count == 0) {
+ if (this._notificationId) {
+ Notifications.cancel(this._notificationId);
+ this._notificationId = null;
+ }
+ } else {
+ let notificationOptions = {
+ title: Strings.brand.GetStringFromName("brandShortName"),
+ when: null, // hide the date row
+ light: [0xFF9500FF, 1000, 1000],
+ ongoing: true
+ };
+
+ let cameraActive = false;
+ let audioActive = false;
+ for (let i = 0; i < count; i++) {
+ let win = windows.queryElementAt(i, Ci.nsIDOMWindow);
+ let hasAudio = {};
+ let hasVideo = {};
+ MediaManagerService.mediaCaptureWindowState(win, hasVideo, hasAudio);
+ if (hasVideo.value) cameraActive = true;
+ if (hasAudio.value) audioActive = true;
+ }
+
+ if (cameraActive && audioActive) {
+ notificationOptions.message = Strings.browser.GetStringFromName("getUserMedia.sharingCameraAndMicrophone.message2");
+ notificationOptions.icon = "drawable:alert_mic_camera";
+ } else if (cameraActive) {
+ notificationOptions.message = Strings.browser.GetStringFromName("getUserMedia.sharingCamera.message2");
+ notificationOptions.icon = "drawable:alert_camera";
+ } else if (audioActive) {
+ notificationOptions.message = Strings.browser.GetStringFromName("getUserMedia.sharingMicrophone.message2");
+ notificationOptions.icon = "drawable:alert_mic";
+ } else {
+ // somethings wrong. lets throw
+ throw "Couldn't find any cameras or microphones being used"
+ }
+
+ if (this._notificationId)
+ Notifications.update(this._notificationId, notificationOptions);
+ else
+ this._notificationId = Notifications.create(notificationOptions);
+ if (count > 1)
+ msg.count = count;
+ }
+ },
+
+ handlePCRequest: function handlePCRequest(aSubject, aTopic, aData) {
+ aSubject = aSubject.wrappedJSObject;
+ let { callID } = aSubject;
+ // Also available: windowID, isSecure, innerWindowID. For contentWindow do:
+ //
+ // let contentWindow = Services.wm.getOuterWindowWithId(windowID);
+
+ Services.obs.notifyObservers(null, "PeerConnection:response:allow", callID);
+ },
+
+ handleGumRequest: function handleGumRequest(aSubject, aTopic, aData) {
+ let constraints = aSubject.getConstraints();
+ let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID);
+
+ contentWindow.navigator.mozGetUserMediaDevices(
+ constraints,
+ function (devices) {
+ if (!ParentalControls.isAllowed(ParentalControls.CAMERA_MICROPHONE)) {
+ Services.obs.notifyObservers(null, "getUserMedia:response:deny", aSubject.callID);
+ WebrtcUI.showBlockMessage(devices);
+ return;
+ }
+
+ WebrtcUI.prompt(contentWindow, aSubject.callID, constraints.audio,
+ constraints.video, devices);
+ },
+ function (error) {
+ Cu.reportError(error);
+ },
+ aSubject.innerWindowID,
+ aSubject.callID);
+ },
+
+ getDeviceButtons: function(audioDevices, videoDevices, aCallID, aUri) {
+ return [{
+ label: Strings.browser.GetStringFromName("getUserMedia.denyRequest.label"),
+ callback: function() {
+ Services.obs.notifyObservers(null, "getUserMedia:response:deny", aCallID);
+ }
+ },
+ {
+ label: Strings.browser.GetStringFromName("getUserMedia.shareRequest.label"),
+ callback: function(checked /* ignored */, inputs) {
+ let allowedDevices = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+
+ let audioId = 0;
+ if (inputs && inputs.audioDevice != undefined)
+ audioId = inputs.audioDevice;
+ if (audioDevices[audioId])
+ allowedDevices.appendElement(audioDevices[audioId], /*weak =*/ false);
+
+ let videoId = 0;
+ if (inputs && inputs.videoSource != undefined)
+ videoId = inputs.videoSource;
+ if (videoDevices[videoId]) {
+ allowedDevices.appendElement(videoDevices[videoId], /*weak =*/ false);
+ let perms = Services.perms;
+ // Although the lifetime is "session" it will be removed upon
+ // use so it's more of a one-shot.
+ perms.add(aUri, "MediaManagerVideo", perms.ALLOW_ACTION, perms.EXPIRE_SESSION);
+ }
+
+ Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", aCallID);
+ },
+ positive: true
+ }];
+ },
+
+ _determineNeededRuntimePermissions: function(aSubject) {
+ let permissions = [];
+
+ let constraints = aSubject.getConstraints();
+ if (constraints.video) {
+ permissions.push(RuntimePermissions.CAMERA);
+ }
+ if (constraints.audio) {
+ permissions.push(RuntimePermissions.RECORD_AUDIO);
+ }
+
+ return permissions;
+ },
+
+ // Get a list of string names for devices. Ensures that none of the strings are blank
+ _getList: function(aDevices, aType) {
+ let defaultCount = 0;
+ return aDevices.map(function(device) {
+ // if this is a Camera input, convert the name to something readable
+ let res = /Camera\ \d+,\ Facing (front|back)/.exec(device.name);
+ if (res)
+ return Strings.browser.GetStringFromName("getUserMedia." + aType + "." + res[1] + "Camera");
+
+ if (device.name.startsWith("&") && device.name.endsWith(";"))
+ return Strings.browser.GetStringFromName(device.name.substring(1, device.name.length -1));
+
+ if (device.name.trim() == "") {
+ defaultCount++;
+ return Strings.browser.formatStringFromName("getUserMedia." + aType + ".default", [defaultCount], 1);
+ }
+ return device.name
+ }, this);
+ },
+
+ _addDevicesToOptions: function(aDevices, aType, aOptions) {
+ if (aDevices.length) {
+
+ // Filter out empty items from the list
+ let list = this._getList(aDevices, aType);
+
+ if (list.length > 0) {
+ aOptions.inputs.push({
+ id: aType,
+ type: "menulist",
+ label: Strings.browser.GetStringFromName("getUserMedia." + aType + ".prompt"),
+ values: list
+ });
+
+ }
+ }
+ },
+
+ showBlockMessage: function(aDevices) {
+ let microphone = false;
+ let camera = false;
+
+ for (let device of aDevices) {
+ device = device.QueryInterface(Ci.nsIMediaDevice);
+ if (device.type == "audio") {
+ microphone = true;
+ } else if (device.type == "video") {
+ camera = true;
+ }
+ }
+
+ let message;
+ if (microphone && !camera) {
+ message = Strings.browser.GetStringFromName("getUserMedia.blockedMicrophoneAccess");
+ } else if (camera && !microphone) {
+ message = Strings.browser.GetStringFromName("getUserMedia.blockedCameraAccess");
+ } else {
+ message = Strings.browser.GetStringFromName("getUserMedia.blockedCameraAndMicrophoneAccess");
+ }
+
+ NativeWindow.doorhanger.show(message, "webrtc-blocked", [], BrowserApp.selectedTab.id, {});
+ },
+
+ prompt: function prompt(aContentWindow, aCallID, aAudioRequested,
+ aVideoRequested, aDevices) {
+ let audioDevices = [];
+ let videoDevices = [];
+ for (let device of aDevices) {
+ device = device.QueryInterface(Ci.nsIMediaDevice);
+ switch (device.type) {
+ case "audio":
+ if (aAudioRequested)
+ audioDevices.push(device);
+ break;
+ case "video":
+ if (aVideoRequested)
+ videoDevices.push(device);
+ break;
+ }
+ }
+
+ let requestType;
+ if (audioDevices.length && videoDevices.length)
+ requestType = "CameraAndMicrophone";
+ else if (audioDevices.length)
+ requestType = "Microphone";
+ else if (videoDevices.length)
+ requestType = "Camera";
+ else
+ return;
+
+ let uri = aContentWindow.document.documentURIObject;
+ let host = uri.host;
+ let requestor = BrowserApp.manifest ? "'" + BrowserApp.manifest.name + "'" : host;
+ let message = Strings.browser.formatStringFromName("getUserMedia.share" + requestType + ".message", [ requestor ], 1);
+
+ let options = { inputs: [] };
+ if (videoDevices.length > 1 || audioDevices.length > 0) {
+ // videoSource is both the string used for l10n lookup and the object that will be returned
+ this._addDevicesToOptions(videoDevices, "videoSource", options);
+ }
+
+ if (audioDevices.length > 1 || videoDevices.length > 0) {
+ this._addDevicesToOptions(audioDevices, "audioDevice", options);
+ }
+
+ let buttons = this.getDeviceButtons(audioDevices, videoDevices, aCallID, uri);
+
+ NativeWindow.doorhanger.show(message, "webrtc-request", buttons, BrowserApp.selectedTab.id, options, "WEBRTC");
+ }
+}
diff --git a/mobile/android/chrome/content/about.js b/mobile/android/chrome/content/about.js
new file mode 100644
index 0000000000..8c9acdf8ae
--- /dev/null
+++ b/mobile/android/chrome/content/about.js
@@ -0,0 +1,151 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils, Cr = Components.results;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function init() {
+ // Include the build date and a warning about Telemetry
+ // if this is an "a#" (nightly or aurora) build
+#expand const version = "__MOZ_APP_VERSION_DISPLAY__";
+ if (/a\d+$/.test(version)) {
+ let buildID = Services.appinfo.appBuildID;
+ let buildDate = buildID.slice(0, 4) + "-" + buildID.slice(4, 6) + "-" + buildID.slice(6, 8);
+ let br = document.createElement("br");
+ let versionPara = document.getElementById("version");
+ versionPara.appendChild(br);
+ let date = document.createTextNode("(" + buildDate + ")");
+ versionPara.appendChild(date);
+ document.getElementById("telemetry").hidden = false;
+ }
+
+ // Include the Distribution information if available
+ try {
+ let distroId = Services.prefs.getCharPref("distribution.id");
+ if (distroId) {
+ let distroVersion = Services.prefs.getCharPref("distribution.version");
+ let distroIdField = document.getElementById("distributionID");
+ distroIdField.textContent = distroId + " - " + distroVersion;
+ distroIdField.hidden = false;
+
+ let distroAbout = Services.prefs.getComplexValue("distribution.about", Ci.nsISupportsString);
+ let distroField = document.getElementById("distributionAbout");
+ distroField.textContent = distroAbout;
+ distroField.hidden = false;
+ }
+ } catch (e) {
+ // Pref is unset
+ }
+
+ // get URLs from prefs
+ try {
+ let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter);
+
+ let links = [
+ {id: "releaseNotesURL", pref: "app.releaseNotesURL"},
+ {id: "supportURL", pref: "app.supportURL"},
+ {id: "faqURL", pref: "app.faqURL"},
+ {id: "privacyURL", pref: "app.privacyURL"},
+ {id: "creditsURL", pref: "app.creditsURL"},
+ ];
+
+ links.forEach(function(link) {
+ let url = formatter.formatURLPref(link.pref);
+ let element = document.getElementById(link.id);
+ element.setAttribute("href", url);
+ });
+ } catch (ex) {}
+
+#ifdef MOZ_UPDATER
+ let Updater = {
+ update: null,
+
+ init: function() {
+ Services.obs.addObserver(this, "Update:CheckResult", false);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "Update:CheckResult") {
+ showUpdateMessage(aData);
+ }
+ },
+ };
+
+ Updater.init();
+
+ function checkForUpdates() {
+ showCheckingMessage();
+
+ Services.androidBridge.handleGeckoMessage({ type: "Update:Check" });
+ }
+
+ function downloadUpdate() {
+ Services.androidBridge.handleGeckoMessage({ type: "Update:Download" });
+ }
+
+ function installUpdate() {
+ showCheckAction();
+
+ Services.androidBridge.handleGeckoMessage({ type: "Update:Install" });
+ }
+
+ let updateLink = document.getElementById("updateLink");
+ let checkingSpan = document.getElementById("update-message-checking");
+ let noneSpan = document.getElementById("update-message-none");
+ let foundSpan = document.getElementById("update-message-found");
+ let downloadingSpan = document.getElementById("update-message-downloading");
+ let downloadedSpan = document.getElementById("update-message-downloaded");
+
+ updateLink.onclick = checkForUpdates;
+ foundSpan.onclick = downloadUpdate;
+ downloadedSpan.onclick = installUpdate;
+
+ function showCheckAction() {
+ checkingSpan.style.display = "none";
+ noneSpan.style.display = "none";
+ foundSpan.style.display = "none";
+ downloadingSpan.style.display = "none";
+ downloadedSpan.style.display = "none";
+ updateLink.style.display = "block";
+ }
+
+ function showCheckingMessage() {
+ updateLink.style.display = "none";
+ noneSpan.style.display = "none";
+ foundSpan.style.display = "none";
+ downloadingSpan.style.display = "none";
+ downloadedSpan.style.display = "none";
+ checkingSpan.style.display = "block";
+ }
+
+ function showUpdateMessage(aResult) {
+ updateLink.style.display = "none";
+ checkingSpan.style.display = "none";
+ noneSpan.style.display = "none";
+ foundSpan.style.display = "none";
+ downloadingSpan.style.display = "none";
+ downloadedSpan.style.display = "none";
+
+ // the aResult values come from mobile/android/base/UpdateServiceHelper.java
+ switch (aResult) {
+ case "NOT_AVAILABLE":
+ noneSpan.style.display = "block";
+ setTimeout(showCheckAction, 2000);
+ break;
+ case "AVAILABLE":
+ foundSpan.style.display = "block";
+ break;
+ case "DOWNLOADING":
+ downloadingSpan.style.display = "block";
+ break;
+ case "DOWNLOADED":
+ downloadedSpan.style.display = "block";
+ break;
+ }
+ }
+#endif
+}
+
+document.addEventListener("DOMContentLoaded", init, false);
diff --git a/mobile/android/chrome/content/about.xhtml b/mobile/android/chrome/content/about.xhtml
new file mode 100644
index 0000000000..8a4c283571
--- /dev/null
+++ b/mobile/android/chrome/content/about.xhtml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+%globalDTD;
+<!ENTITY % fennecDTD SYSTEM "chrome://browser/locale/about.dtd">
+%fennecDTD;
+]>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <meta name="viewport" content="width=480; initial-scale=.6667; user-scalable=no"/>
+ <title>&aboutPage.title;</title>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutPage.css" type="text/css"/>
+ <link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
+</head>
+
+<body dir="&locale.dir;">
+ <div id="header">
+ <div id="wordmark"></div>
+#expand <p id="version">__MOZ_APP_VERSION_DISPLAY__</p>
+ </div>
+
+ <div id="banner">
+ <div id="logo"/>
+#ifdef MOZ_UPDATER
+ <div id="updateBox">
+ <a id="updateLink" href="">&aboutPage.checkForUpdates.link;</a>
+ <span id="update-message-checking">&aboutPage.checkForUpdates.checking;</span>
+ <span id="update-message-none">&aboutPage.checkForUpdates.none;</span>
+ <span id="update-message-found">&aboutPage.checkForUpdates.available2;</span>
+ <span id="update-message-downloading">&aboutPage.checkForUpdates.downloading;</span>
+ <span id="update-message-downloaded">&aboutPage.checkForUpdates.downloaded2;</span>
+ </div>
+#endif
+
+ <div id="messages">
+ <p id="distributionAbout" hidden="true"/>
+ <p id="distributionID" hidden="true"/>
+ <p id="telemetry" hidden="true">
+ &aboutPage.warningVersion;
+#ifdef MOZ_TELEMETRY_ON_BY_DEFAULT
+ &aboutPage.telemetryStart;<a href="https://www.mozilla.org/">&aboutPage.telemetryMozillaLink;</a>&aboutPage.telemetryEnd;
+#endif
+ </p>
+ </div>
+
+ </div>
+
+ <ul id="aboutLinks">
+ <div class="top-border"></div>
+ <li><a id="faqURL">&aboutPage.faq.label;</a></li>
+ <li><a id="supportURL">&aboutPage.support.label;</a></li>
+ <li><a id="privacyURL">&aboutPage.privacyPolicy.label;</a></li>
+ <li><a href="about:rights">&aboutPage.rights.label;</a></li>
+ <li><a id="releaseNotesURL">&aboutPage.relNotes.label;</a></li>
+ <li><a id="creditsURL">&aboutPage.credits.label;</a></li>
+ <li><a href="about:license">&aboutPage.license.label;</a></li>
+ <div class="bottom-border"></div>
+ </ul>
+
+#ifdef RELEASE_OR_BETA
+ <div id="aboutDetails">
+ <p>&aboutPage.logoTrademark;</p>
+ </div>
+#endif
+
+ <script type="application/javascript;version=1.8" src="chrome://browser/content/about.js" />
+
+</body>
+</html>
diff --git a/mobile/android/chrome/content/aboutAccounts.js b/mobile/android/chrome/content/aboutAccounts.js
new file mode 100644
index 0000000000..4801a76a1e
--- /dev/null
+++ b/mobile/android/chrome/content/aboutAccounts.js
@@ -0,0 +1,351 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Wrap a remote fxa-content-server.
+ *
+ * An about:accounts tab loads and displays an fxa-content-server page,
+ * depending on the current Android Account status and an optional 'action'
+ * parameter.
+ *
+ * We show a spinner while the remote iframe is loading. We expect the
+ * WebChannel message listening to the fxa-content-server to send this tab's
+ * <browser>'s messageManager a LOADED message when the remote iframe provides
+ * the WebChannel LOADED message. See the messageManager registration and the
+ * |loadedDeferred| promise. This loosely couples the WebChannel implementation
+ * and about:accounts! (We need this coupling in order to distinguish
+ * WebChannel LOADED messages produced by multiple about:accounts tabs.)
+ *
+ * We capture error conditions by accessing the inner nsIWebNavigation of the
+ * iframe directly.
+ */
+
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components; /*global Components */
+
+Cu.import("resource://gre/modules/Accounts.jsm"); /*global Accounts */
+Cu.import("resource://gre/modules/PromiseUtils.jsm"); /*global PromiseUtils */
+Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
+Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /*global XPCOMUtils */
+
+const ACTION_URL_PARAM = "action";
+
+const COMMAND_LOADED = "fxaccounts:loaded";
+
+const log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccounts");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls",
+ "@mozilla.org/parental-controls-service;1", "nsIParentalControlsService");
+
+// Shows the toplevel element with |id| to be shown - all other top-level
+// elements are hidden.
+// If |id| is 'spinner', then 'remote' is also shown, with opacity 0.
+function show(id) {
+ let allTop = document.querySelectorAll(".toplevel");
+ for (let elt of allTop) {
+ if (elt.getAttribute("id") == id) {
+ elt.style.display = 'block';
+ } else {
+ elt.style.display = 'none';
+ }
+ }
+ if (id == 'spinner') {
+ document.getElementById('remote').style.display = 'block';
+ document.getElementById('remote').style.opacity = 0;
+ }
+}
+
+// Each time we try to load the remote <iframe>, loadedDeferred is replaced. It
+// is resolved by a LOADED message, and rejected by a failure to load.
+var loadedDeferred = null;
+
+// We have a new load starting. Replace the existing promise with a new one,
+// and queue up the transition to remote content.
+function deferTransitionToRemoteAfterLoaded() {
+ log.d('Waiting for LOADED message.');
+
+ loadedDeferred = PromiseUtils.defer();
+ loadedDeferred.promise.then(() => {
+ log.d('Got LOADED message!');
+ document.getElementById("remote").style.opacity = 0;
+ show("remote");
+ document.getElementById("remote").style.opacity = 1;
+ })
+ .catch((e) => {
+ log.w('Did not get LOADED message: ' + e.toString());
+ });
+}
+
+function handleLoadedMessage(message) {
+ loadedDeferred.resolve();
+};
+
+var wrapper = {
+ iframe: null,
+
+ url: null,
+
+ init: function (url) {
+ this.url = url;
+ deferTransitionToRemoteAfterLoaded();
+
+ let iframe = document.getElementById("remote");
+ this.iframe = iframe;
+ this.iframe.QueryInterface(Ci.nsIFrameLoaderOwner);
+ let docShell = this.iframe.frameLoader.docShell;
+ docShell.QueryInterface(Ci.nsIWebProgress);
+ docShell.addProgressListener(this.iframeListener, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
+
+ // Set the iframe's location with loadURI/LOAD_FLAGS_BYPASS_HISTORY to
+ // avoid having a new history entry being added.
+ let webNav = iframe.frameLoader.docShell.QueryInterface(Ci.nsIWebNavigation);
+ webNav.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null);
+ },
+
+ retry: function () {
+ deferTransitionToRemoteAfterLoaded();
+
+ let webNav = this.iframe.frameLoader.docShell.QueryInterface(Ci.nsIWebNavigation);
+ webNav.loadURI(this.url, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null);
+ },
+
+ iframeListener: {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference,
+ Ci.nsISupports]),
+
+ onStateChange: function(aWebProgress, aRequest, aState, aStatus) {
+ let failure = false;
+
+ // Captive portals sometimes redirect users
+ if ((aState & Ci.nsIWebProgressListener.STATE_REDIRECTING)) {
+ failure = true;
+ } else if ((aState & Ci.nsIWebProgressListener.STATE_STOP)) {
+ if (aRequest instanceof Ci.nsIHttpChannel) {
+ try {
+ failure = aRequest.responseStatus != 200;
+ } catch (e) {
+ failure = aStatus != Components.results.NS_OK;
+ }
+ }
+ }
+
+ // Calling cancel() will raise some OnStateChange notifications by itself,
+ // so avoid doing that more than once
+ if (failure && aStatus != Components.results.NS_BINDING_ABORTED) {
+ aRequest.cancel(Components.results.NS_BINDING_ABORTED);
+ // Since after a promise is fulfilled, subsequent fulfillments are
+ // treated as no-ops, we don't care that we might see multiple failures
+ // due to multiple listener callbacks. (It's not easy to extract this
+ // from the Promises spec, but it is widely quoted. Start with
+ // http://stackoverflow.com/a/18218542.)
+ loadedDeferred.reject(new Error("Failed in onStateChange!"));
+ show("networkError");
+ }
+ },
+
+ onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
+ if (aRequest && aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
+ aRequest.cancel(Components.results.NS_BINDING_ABORTED);
+ // As above, we're not concerned by multiple listener callbacks.
+ loadedDeferred.reject(new Error("Failed in onLocationChange!"));
+ show("networkError");
+ }
+ },
+
+ onProgressChange: function() {},
+ onStatusChange: function() {},
+ onSecurityChange: function() {},
+ },
+};
+
+
+function retry() {
+ log.i("Retrying.");
+ show("spinner");
+ wrapper.retry();
+}
+
+function openPrefs() {
+ log.i("Opening Sync preferences.");
+ // If an Android Account exists, this will open the Status Activity.
+ // Otherwise, it will begin the Get Started flow. This should only be shown
+ // when an Account actually exists.
+ Accounts.launchSetup();
+}
+
+function getURLForAction(action, urlParams) {
+ let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri");
+ url = url + (url.endsWith("/") ? "" : "/") + action;
+ const CONTEXT = "fx_fennec_v1";
+ // The only service managed by Fennec, to date, is Firefox Sync.
+ const SERVICE = "sync";
+ urlParams = urlParams || new URLSearchParams("");
+ urlParams.set('service', SERVICE);
+ urlParams.set('context', CONTEXT);
+ // Ideally we'd just merge urlParams with new URL(url).searchParams, but our
+ // URLSearchParams implementation doesn't support iteration (bug 1085284).
+ let urlParamStr = urlParams.toString();
+ if (urlParamStr) {
+ url += (url.includes("?") ? "&" : "?") + urlParamStr;
+ }
+ return url;
+}
+
+function updateDisplayedEmail(user) {
+ let emailDiv = document.getElementById("email");
+ if (emailDiv && user) {
+ emailDiv.textContent = user.email;
+ }
+}
+
+function init() {
+ // Test for restrictions before getFirefoxAccount(), since that will fail if
+ // we are restricted.
+ if (!ParentalControls.isAllowed(ParentalControls.MODIFY_ACCOUNTS)) {
+ // It's better to log and show an error message than to invite user
+ // confusion by removing about:accounts entirely. That is, if the user is
+ // restricted, this way they'll discover as much and may be able to get
+ // out of their restricted profile. If we remove about:accounts entirely,
+ // it will look like Fennec is buggy, and the user will be very confused.
+ log.e("This profile cannot connect to Firefox Accounts: showing restricted error.");
+ show("restrictedError");
+ return;
+ }
+
+ Accounts.getFirefoxAccount().then(user => {
+ // It's possible for the window to start closing before getting the user
+ // completes. Tests in particular can cause this.
+ if (window.closed) {
+ return;
+ }
+
+ updateDisplayedEmail(user);
+
+ // Ideally we'd use new URL(document.URL).searchParams, but for about: URIs,
+ // searchParams is empty.
+ let urlParams = new URLSearchParams(document.URL.split("?")[1] || "");
+ let action = urlParams.get(ACTION_URL_PARAM);
+ urlParams.delete(ACTION_URL_PARAM);
+
+ switch (action) {
+ case "signup":
+ if (user) {
+ // Asking to sign-up when already signed in just shows prefs.
+ show("prefs");
+ } else {
+ show("spinner");
+ wrapper.init(getURLForAction("signup", urlParams));
+ }
+ break;
+ case "signin":
+ if (user) {
+ // Asking to sign-in when already signed in just shows prefs.
+ show("prefs");
+ } else {
+ show("spinner");
+ wrapper.init(getURLForAction("signin", urlParams));
+ }
+ break;
+ case "force_auth":
+ if (user) {
+ show("spinner");
+ urlParams.set("email", user.email); // In future, pin using the UID.
+ wrapper.init(getURLForAction("force_auth", urlParams));
+ } else {
+ show("spinner");
+ wrapper.init(getURLForAction("signup", urlParams));
+ }
+ break;
+ case "manage":
+ if (user) {
+ show("spinner");
+ urlParams.set("email", user.email); // In future, pin using the UID.
+ wrapper.init(getURLForAction("settings", urlParams));
+ } else {
+ show("spinner");
+ wrapper.init(getURLForAction("signup", urlParams));
+ }
+ break;
+ case "avatar":
+ if (user) {
+ show("spinner");
+ urlParams.set("email", user.email); // In future, pin using the UID.
+ wrapper.init(getURLForAction("settings/avatar/change", urlParams));
+ } else {
+ show("spinner");
+ wrapper.init(getURLForAction("signup", urlParams));
+ }
+ break;
+ default:
+ // Unrecognized or no action specified.
+ if (action) {
+ log.w("Ignoring unrecognized action: " + action);
+ }
+ if (user) {
+ show("prefs");
+ } else {
+ show("spinner");
+ wrapper.init(getURLForAction("signup", urlParams));
+ }
+ break;
+ }
+ }).catch(e => {
+ log.e("Failed to get the signed in user: " + e.toString());
+ });
+}
+
+document.addEventListener("DOMContentLoaded", function onload() {
+ document.removeEventListener("DOMContentLoaded", onload, true);
+ init();
+ var buttonRetry = document.getElementById('buttonRetry');
+ buttonRetry.addEventListener('click', retry);
+
+ var buttonOpenPrefs = document.getElementById('buttonOpenPrefs');
+ buttonOpenPrefs.addEventListener('click', openPrefs);
+}, true);
+
+// This window is contained in a XUL <browser> element. Return the
+// messageManager of that <browser> element, or null.
+function getBrowserMessageManager() {
+ let browser = window
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow)
+ .QueryInterface(Ci.nsIDOMChromeWindow)
+ .BrowserApp
+ .getBrowserForDocument(document);
+ if (browser) {
+ return browser.messageManager;
+ }
+ return null;
+}
+
+// Add a single listener for 'loaded' messages from the iframe in this
+// <browser>. These 'loaded' messages are ferried from the WebChannel to just
+// this <browser>.
+var mm = getBrowserMessageManager();
+if (mm) {
+ mm.addMessageListener(COMMAND_LOADED, handleLoadedMessage);
+} else {
+ log.e('No messageManager, not listening for LOADED message!');
+}
+
+window.addEventListener("unload", function(event) {
+ try {
+ let mm = getBrowserMessageManager();
+ if (mm) {
+ mm.removeMessageListener(COMMAND_LOADED, handleLoadedMessage);
+ }
+ } catch (e) {
+ // This could fail if the page is being torn down, the tab is being
+ // destroyed, etc.
+ log.w('Not removing listener for LOADED message: ' + e.toString());
+ }
+});
diff --git a/mobile/android/chrome/content/aboutAccounts.xhtml b/mobile/android/chrome/content/aboutAccounts.xhtml
new file mode 100644
index 0000000000..b988741d5c
--- /dev/null
+++ b/mobile/android/chrome/content/aboutAccounts.xhtml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
+%globalDTD;
+<!ENTITY % aboutDTD SYSTEM "chrome://browser/locale/aboutAccounts.dtd">
+%aboutDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml" dir="&locale.dir;">
+ <head>
+ <title>Firefox Sync</title>
+ <meta name="viewport" content="width=device-width; user-scalable=0" />
+ <link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
+ <link rel="stylesheet" href="chrome://browser/skin/spinner.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutBase.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutAccounts.css" type="text/css"/>
+ </head>
+ <body>
+ <div id="spinner" class="toplevel">
+ <div class="container flex-column">
+ <!-- Empty text-container for spacing. -->
+ <div class="text-container flex-column" />
+
+ <div class="mui-refresh-main">
+ <div class="mui-refresh-wrapper">
+ <div class="mui-spinner-wrapper">
+ <div class="mui-spinner-main">
+ <div class="mui-spinner-left">
+ <div class="mui-half-circle-left" />
+ </div>
+ <div class="mui-spinner-right">
+ <div class="mui-half-circle-right" />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ </div>
+ </div>
+
+ <iframe mozframetype="content" id="remote" class="toplevel" />
+
+ <div id="prefs" class="toplevel">
+ <div class="container flex-column">
+ <div class="text-container flex-column">
+ <div class="text">&aboutAccounts.connected.title;</div>
+ <div class="hint">&aboutAccounts.connected.description;</div>
+ <div id="email" class="hint"></div>
+ </div>
+ <a id="buttonOpenPrefs" tabindex="0" href="#">&aboutAccounts.syncPreferences.label;</a>
+ </div>
+ </div>
+
+ <div id="networkError" class="toplevel">
+ <div class="container flex-column">
+ <div class="text-container flex-column">
+ <div class="text">&aboutAccounts.noConnection.title;</div>
+ </div>
+ <div class="button-row">
+ <button id="buttonRetry" class="button" tabindex="1">&aboutAccounts.retry.label;</button>
+ </div>
+ </div>
+ </div>
+
+ <div id="restrictedError" class="toplevel">
+ <div class="container flex-column">
+ <div class="text-container flex-column">
+ <div class="text">&aboutAccounts.restrictedError.title;</div>
+ <div class="hint">&aboutAccounts.restrictedError.description;</div>
+ </div>
+ </div>
+ </div>
+
+ <script type="application/javascript;version=1.8" src="chrome://browser/content/aboutAccounts.js"></script>
+ </body>
+</html>
diff --git a/mobile/android/chrome/content/aboutAddons.js b/mobile/android/chrome/content/aboutAddons.js
new file mode 100644
index 0000000000..becf56a327
--- /dev/null
+++ b/mobile/android/chrome/content/aboutAddons.js
@@ -0,0 +1,609 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/*globals gChromeWin */
+
+var Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm")
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const AMO_ICON = "chrome://browser/skin/images/amo-logo.png";
+
+var gStringBundle = Services.strings.createBundle("chrome://browser/locale/aboutAddons.properties");
+
+XPCOMUtils.defineLazyGetter(window, "gChromeWin", function() {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow)
+ .QueryInterface(Ci.nsIDOMChromeWindow);
+});
+XPCOMUtils.defineLazyModuleGetter(window, "Preferences",
+ "resource://gre/modules/Preferences.jsm");
+
+var ContextMenus = {
+ target: null,
+
+ init: function() {
+ document.addEventListener("contextmenu", this, false);
+
+ document.getElementById("contextmenu-enable").addEventListener("click", ContextMenus.enable.bind(this), false);
+ document.getElementById("contextmenu-disable").addEventListener("click", ContextMenus.disable.bind(this), false);
+ document.getElementById("contextmenu-uninstall").addEventListener("click", ContextMenus.uninstall.bind(this), false);
+
+ // XXX - Hack to fix bug 985867 for now
+ document.addEventListener("touchstart", function() { });
+ },
+
+ handleEvent: function(event) {
+ // store the target of context menu events so that we know which app to act on
+ this.target = event.target;
+ while (!this.target.hasAttribute("contextmenu")) {
+ this.target = this.target.parentNode;
+ }
+
+ if (!this.target) {
+ document.getElementById("contextmenu-enable").setAttribute("hidden", "true");
+ document.getElementById("contextmenu-disable").setAttribute("hidden", "true");
+ document.getElementById("contextmenu-uninstall").setAttribute("hidden", "true");
+ return;
+ }
+
+ let addon = this.target.addon;
+ if (addon.scope == AddonManager.SCOPE_APPLICATION) {
+ document.getElementById("contextmenu-uninstall").setAttribute("hidden", "true");
+ } else {
+ document.getElementById("contextmenu-uninstall").removeAttribute("hidden");
+ }
+
+ // Hide the enable/disable context menu items if the add-on was disabled by
+ // Firefox (e.g. unsigned or blocklisted add-on).
+ if (addon.appDisabled) {
+ document.getElementById("contextmenu-enable").setAttribute("hidden", "true");
+ document.getElementById("contextmenu-disable").setAttribute("hidden", "true");
+ return;
+ }
+
+ let enabled = this.target.getAttribute("isDisabled") != "true";
+ if (enabled) {
+ document.getElementById("contextmenu-enable").setAttribute("hidden", "true");
+ document.getElementById("contextmenu-disable").removeAttribute("hidden");
+ } else {
+ document.getElementById("contextmenu-enable").removeAttribute("hidden");
+ document.getElementById("contextmenu-disable").setAttribute("hidden", "true");
+ }
+ },
+
+ enable: function(event) {
+ Addons.setEnabled(true, this.target.addon);
+ this.target = null;
+ },
+
+ disable: function (event) {
+ Addons.setEnabled(false, this.target.addon);
+ this.target = null;
+ },
+
+ uninstall: function (event) {
+ Addons.uninstall(this.target.addon);
+ this.target = null;
+ }
+}
+
+function init() {
+ window.addEventListener("popstate", onPopState, false);
+
+ AddonManager.addInstallListener(Addons);
+ AddonManager.addAddonListener(Addons);
+ Addons.init();
+ showList();
+ ContextMenus.init();
+}
+
+
+function uninit() {
+ AddonManager.removeInstallListener(Addons);
+ AddonManager.removeAddonListener(Addons);
+}
+
+function openLink(url) {
+ let BrowserApp = gChromeWin.BrowserApp;
+ BrowserApp.addTab(url, { selected: true, parentId: BrowserApp.selectedTab.id });
+}
+
+function onPopState(aEvent) {
+ // Called when back/forward is used to change the state of the page
+ if (aEvent.state) {
+ // Show the detail page for an addon
+ Addons.showDetails(Addons._getElementForAddon(aEvent.state.id));
+ } else {
+ // Clear any previous detail addon
+ let detailItem = document.querySelector("#addons-details > .addon-item");
+ detailItem.addon = null;
+
+ showList();
+ }
+}
+
+function showList() {
+ // Hide the detail page and show the list
+ let details = document.querySelector("#addons-details");
+ details.style.display = "none";
+ let list = document.querySelector("#addons-list");
+ list.style.display = "block";
+ document.documentElement.removeAttribute("details");
+}
+
+var Addons = {
+ _restartCount: 0,
+
+ _createItem: function _createItem(aAddon) {
+ let outer = document.createElement("div");
+ outer.setAttribute("addonID", aAddon.id);
+ outer.className = "addon-item list-item";
+ outer.setAttribute("role", "button");
+ outer.setAttribute("contextmenu", "addonmenu");
+ outer.addEventListener("click", function() {
+ this.showDetails(outer);
+ history.pushState({ id: aAddon.id }, document.title);
+ }.bind(this), true);
+
+ let img = document.createElement("img");
+ img.className = "icon";
+ img.setAttribute("src", aAddon.iconURL || AMO_ICON);
+ outer.appendChild(img);
+
+ let inner = document.createElement("div");
+ inner.className = "inner";
+
+ let details = document.createElement("div");
+ details.className = "details";
+ inner.appendChild(details);
+
+ let titlePart = document.createElement("div");
+ titlePart.textContent = aAddon.name;
+ titlePart.className = "title";
+ details.appendChild(titlePart);
+
+ let versionPart = document.createElement("div");
+ versionPart.textContent = aAddon.version;
+ versionPart.className = "version";
+ details.appendChild(versionPart);
+
+ if ("description" in aAddon) {
+ let descPart = document.createElement("div");
+ descPart.textContent = aAddon.description;
+ descPart.className = "description";
+ inner.appendChild(descPart);
+ }
+
+ outer.appendChild(inner);
+ return outer;
+ },
+
+ _createBrowseItem: function _createBrowseItem() {
+ let outer = document.createElement("div");
+ outer.className = "addon-item list-item";
+ outer.setAttribute("role", "button");
+ outer.addEventListener("click", function(event) {
+ try {
+ let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter);
+ openLink(formatter.formatURLPref("extensions.getAddons.browseAddons"));
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }, true);
+
+ let img = document.createElement("img");
+ img.className = "icon";
+ img.setAttribute("src", AMO_ICON);
+ outer.appendChild(img);
+
+ let inner = document.createElement("div");
+ inner.className = "inner";
+
+ let title = document.createElement("div");
+ title.id = "browse-title";
+ title.className = "title";
+ title.textContent = gStringBundle.GetStringFromName("addons.browseAll");
+ inner.appendChild(title);
+
+ outer.appendChild(inner);
+ return outer;
+ },
+
+ _createItemForAddon: function _createItemForAddon(aAddon) {
+ let appManaged = (aAddon.scope == AddonManager.SCOPE_APPLICATION);
+ let opType = this._getOpTypeForOperations(aAddon.pendingOperations);
+ let updateable = (aAddon.permissions & AddonManager.PERM_CAN_UPGRADE) > 0;
+ let uninstallable = (aAddon.permissions & AddonManager.PERM_CAN_UNINSTALL) > 0;
+
+ // TODO(matt): Add support for OPTIONS_TYPE_INLINE_BROWSER once bug 1302504 lands.
+ let optionsURL;
+ switch (aAddon.optionsType) {
+ case AddonManager.OPTIONS_TYPE_INLINE:
+ optionsURL = aAddon.optionsURL || "";
+ break;
+ default:
+ optionsURL = "";
+ }
+
+ let blocked = "";
+ switch(aAddon.blocklistState) {
+ case Ci.nsIBlocklistService.STATE_BLOCKED:
+ blocked = "blocked";
+ break;
+ case Ci.nsIBlocklistService.STATE_SOFTBLOCKED:
+ blocked = "softBlocked";
+ break;
+ case Ci.nsIBlocklistService.STATE_OUTDATED:
+ blocked = "outdated";
+ break;
+ }
+
+ let item = this._createItem(aAddon);
+ item.setAttribute("isDisabled", !aAddon.isActive);
+ item.setAttribute("isUnsigned", aAddon.signedState <= AddonManager.SIGNEDSTATE_MISSING);
+ item.setAttribute("opType", opType);
+ item.setAttribute("updateable", updateable);
+ if (blocked)
+ item.setAttribute("blockedStatus", blocked);
+ item.setAttribute("optionsURL", optionsURL);
+ item.addon = aAddon;
+
+ return item;
+ },
+
+ _getElementForAddon: function(aKey) {
+ let list = document.getElementById("addons-list");
+ let element = list.querySelector("div[addonID=\"" + CSS.escape(aKey) + "\"]");
+ return element;
+ },
+
+ init: function init() {
+ let self = this;
+ AddonManager.getAllAddons(function(aAddons) {
+ // Clear all content before filling the addons
+ let list = document.getElementById("addons-list");
+ list.innerHTML = "";
+
+ aAddons.sort(function(a,b) {
+ return a.name.localeCompare(b.name);
+ });
+ for (let i=0; i<aAddons.length; i++) {
+ // Don't create item for system add-ons.
+ if (aAddons[i].isSystem)
+ continue;
+
+ let item = self._createItemForAddon(aAddons[i]);
+ list.appendChild(item);
+ }
+
+ // Add a "Browse all Firefox Add-ons" item to the bottom of the list.
+ let browseItem = self._createBrowseItem();
+ list.appendChild(browseItem);
+ });
+
+ document.getElementById("uninstall-btn").addEventListener("click", Addons.uninstallCurrent.bind(this), false);
+ document.getElementById("cancel-btn").addEventListener("click", Addons.cancelUninstall.bind(this), false);
+ document.getElementById("disable-btn").addEventListener("click", Addons.disable.bind(this), false);
+ document.getElementById("enable-btn").addEventListener("click", Addons.enable.bind(this), false);
+
+ document.getElementById("unsigned-learn-more").addEventListener("click", function() {
+ openLink(Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons");
+ }, false);
+ },
+
+ _getOpTypeForOperations: function _getOpTypeForOperations(aOperations) {
+ if (aOperations & AddonManager.PENDING_UNINSTALL)
+ return "needs-uninstall";
+ if (aOperations & AddonManager.PENDING_ENABLE)
+ return "needs-enable";
+ if (aOperations & AddonManager.PENDING_DISABLE)
+ return "needs-disable";
+ return "";
+ },
+
+ showDetails: function showDetails(aListItem) {
+ // This function removes and returns the text content of aNode without
+ // removing any child elements. Removing the text nodes ensures any XBL
+ // bindings apply properly.
+ function stripTextNodes(aNode) {
+ var text = "";
+ for (var i = 0; i < aNode.childNodes.length; i++) {
+ if (aNode.childNodes[i].nodeType != document.ELEMENT_NODE) {
+ text += aNode.childNodes[i].textContent;
+ aNode.removeChild(aNode.childNodes[i--]);
+ } else {
+ text += stripTextNodes(aNode.childNodes[i]);
+ }
+ }
+ return text;
+ }
+
+ let detailItem = document.querySelector("#addons-details > .addon-item");
+ detailItem.setAttribute("isDisabled", aListItem.getAttribute("isDisabled"));
+ detailItem.setAttribute("isUnsigned", aListItem.getAttribute("isUnsigned"));
+ detailItem.setAttribute("opType", aListItem.getAttribute("opType"));
+ detailItem.setAttribute("optionsURL", aListItem.getAttribute("optionsURL"));
+ let addon = detailItem.addon = aListItem.addon;
+
+ let favicon = document.querySelector("#addons-details > .addon-item .icon");
+ favicon.setAttribute("src", addon.iconURL || AMO_ICON);
+
+ detailItem.querySelector(".title").textContent = addon.name;
+ detailItem.querySelector(".version").textContent = addon.version;
+ detailItem.querySelector(".description-full").textContent = addon.description;
+ detailItem.querySelector(".status-uninstalled").textContent =
+ gStringBundle.formatStringFromName("addonStatus.uninstalled", [addon.name], 1);
+
+ let enableBtn = document.getElementById("enable-btn");
+ if (addon.appDisabled) {
+ enableBtn.setAttribute("disabled", "true");
+ } else {
+ enableBtn.removeAttribute("disabled");
+ }
+
+ let uninstallBtn = document.getElementById("uninstall-btn");
+ if (addon.scope == AddonManager.SCOPE_APPLICATION) {
+ uninstallBtn.setAttribute("disabled", "true");
+ } else {
+ uninstallBtn.removeAttribute("disabled");
+ }
+
+ let box = document.querySelector("#addons-details > .addon-item .options-box");
+ box.innerHTML = "";
+
+ // Retrieve the extensions preferences
+ try {
+ let optionsURL = aListItem.getAttribute("optionsURL");
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", optionsURL, true);
+ xhr.onload = function(e) {
+ if (xhr.responseXML) {
+ // Only allow <setting> for now
+ let settings = xhr.responseXML.querySelectorAll(":root > setting");
+ if (settings.length > 0) {
+ for (let i = 0; i < settings.length; i++) {
+ var setting = settings[i];
+ var desc = stripTextNodes(setting).trim();
+ if (!setting.hasAttribute("desc")) {
+ setting.setAttribute("desc", desc);
+ }
+ box.appendChild(setting);
+ }
+ // Send an event so add-ons can prepopulate any non-preference based
+ // settings
+ let event = document.createEvent("Events");
+ event.initEvent("AddonOptionsLoad", true, false);
+ window.dispatchEvent(event);
+ } else {
+ // Reset the options URL to hide the options header if there are no
+ // valid settings to show.
+ detailItem.setAttribute("optionsURL", "");
+ }
+
+ // Also send a notification to match the behavior of desktop Firefox
+ let id = aListItem.getAttribute("addonID");
+ Services.obs.notifyObservers(document, AddonManager.OPTIONS_NOTIFICATION_DISPLAYED, id);
+ }
+ }
+ xhr.send(null);
+ } catch (e) { }
+
+ let list = document.querySelector("#addons-list");
+ list.style.display = "none";
+ let details = document.querySelector("#addons-details");
+ details.style.display = "block";
+ document.documentElement.setAttribute("details", "true");
+ },
+
+ setEnabled: function setEnabled(aValue, aAddon) {
+ let detailItem = document.querySelector("#addons-details > .addon-item");
+ let addon = aAddon || detailItem.addon;
+ if (!addon)
+ return;
+
+ let listItem = this._getElementForAddon(addon.id);
+
+ let opType;
+ if (addon.type == "theme") {
+ if (aValue) {
+ // We can have only one theme enabled, so disable the current one if any
+ let list = document.getElementById("addons-list");
+ let item = list.firstElementChild;
+ while (item) {
+ if (item.addon && (item.addon.type == "theme") && (item.addon.isActive)) {
+ item.addon.userDisabled = true;
+ item.setAttribute("isDisabled", true);
+ break;
+ }
+ item = item.nextSibling;
+ }
+ }
+ addon.userDisabled = !aValue;
+ } else if (addon.type == "locale") {
+ addon.userDisabled = !aValue;
+ } else {
+ addon.userDisabled = !aValue;
+ opType = this._getOpTypeForOperations(addon.pendingOperations);
+
+ if ((addon.pendingOperations & AddonManager.PENDING_ENABLE) ||
+ (addon.pendingOperations & AddonManager.PENDING_DISABLE)) {
+ this.showRestart();
+ } else if (listItem && /needs-(enable|disable)/.test(listItem.getAttribute("opType"))) {
+ this.hideRestart();
+ }
+ }
+
+ if (addon == detailItem.addon) {
+ detailItem.setAttribute("isDisabled", !aValue);
+ if (opType)
+ detailItem.setAttribute("opType", opType);
+ else
+ detailItem.removeAttribute("opType");
+ }
+
+ // Sync to the list item
+ if (listItem) {
+ listItem.setAttribute("isDisabled", !aValue);
+ if (opType)
+ listItem.setAttribute("opType", opType);
+ else
+ listItem.removeAttribute("opType");
+ }
+ },
+
+ enable: function enable() {
+ this.setEnabled(true);
+ },
+
+ disable: function disable() {
+ this.setEnabled(false);
+ },
+
+ uninstallCurrent: function uninstallCurrent() {
+ let detailItem = document.querySelector("#addons-details > .addon-item");
+
+ let addon = detailItem.addon;
+ if (!addon)
+ return;
+
+ this.uninstall(addon);
+ },
+
+ uninstall: function uninstall(aAddon) {
+ if (!aAddon) {
+ return;
+ }
+
+ let listItem = this._getElementForAddon(aAddon.id);
+ aAddon.uninstall();
+
+ if (aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL) {
+ this.showRestart();
+
+ // A disabled addon doesn't need a restart so it has no pending ops and
+ // can't be cancelled
+ let opType = this._getOpTypeForOperations(aAddon.pendingOperations);
+ if (!aAddon.isActive && opType == "")
+ opType = "needs-uninstall";
+
+ detailItem.setAttribute("opType", opType);
+ listItem.setAttribute("opType", opType);
+ }
+ },
+
+ cancelUninstall: function ev_cancelUninstall() {
+ let detailItem = document.querySelector("#addons-details > .addon-item");
+ let addon = detailItem.addon;
+ if (!addon)
+ return;
+
+ addon.cancelUninstall();
+ this.hideRestart();
+
+ let opType = this._getOpTypeForOperations(addon.pendingOperations);
+ detailItem.setAttribute("opType", opType);
+
+ let listItem = this._getElementForAddon(addon.id);
+ listItem.setAttribute("opType", opType);
+ },
+
+ showRestart: function showRestart() {
+ this._restartCount++;
+ gChromeWin.XPInstallObserver.showRestartPrompt();
+ },
+
+ hideRestart: function hideRestart() {
+ this._restartCount--;
+ if (this._restartCount == 0)
+ gChromeWin.XPInstallObserver.hideRestartPrompt();
+ },
+
+ onEnabled: function(aAddon) {
+ let listItem = this._getElementForAddon(aAddon.id);
+ if (!listItem)
+ return;
+
+ // Reload the details to pick up any options now that it's enabled.
+ listItem.setAttribute("optionsURL", aAddon.optionsURL || "");
+ let detailItem = document.querySelector("#addons-details > .addon-item");
+ if (aAddon == detailItem.addon)
+ this.showDetails(listItem);
+ },
+
+ onInstallEnded: function(aInstall, aAddon) {
+ let needsRestart = false;
+ if (aInstall.existingAddon && (aInstall.existingAddon.pendingOperations & AddonManager.PENDING_UPGRADE))
+ needsRestart = true;
+ else if (aAddon.pendingOperations & AddonManager.PENDING_INSTALL)
+ needsRestart = true;
+
+ let list = document.getElementById("addons-list");
+ let element = this._getElementForAddon(aAddon.id);
+ if (!element) {
+ element = this._createItemForAddon(aAddon);
+ list.insertBefore(element, list.firstElementChild);
+ }
+
+ if (needsRestart)
+ element.setAttribute("opType", "needs-restart");
+ },
+
+ onInstalled: function(aAddon) {
+ let list = document.getElementById("addons-list");
+ let element = this._getElementForAddon(aAddon.id);
+ if (!element) {
+ element = this._createItemForAddon(aAddon);
+
+ // Themes aren't considered active on install, so set existing as disabled, and new one enabled.
+ if (aAddon.type == "theme") {
+ let item = list.firstElementChild;
+ while (item) {
+ if (item.addon && (item.addon.type == "theme")) {
+ item.setAttribute("isDisabled", true);
+ }
+ item = item.nextSibling;
+ }
+ element.setAttribute("isDisabled", false);
+ }
+
+ list.insertBefore(element, list.firstElementChild);
+ }
+ },
+
+ onUninstalled: function(aAddon) {
+ let list = document.getElementById("addons-list");
+ let element = this._getElementForAddon(aAddon.id);
+ list.removeChild(element);
+
+ // Go back if we're in the detail view of the add-on that was uninstalled.
+ let detailItem = document.querySelector("#addons-details > .addon-item");
+ if (detailItem.addon.id == aAddon.id) {
+ history.back();
+ }
+ },
+
+ onInstallFailed: function(aInstall) {
+ },
+
+ onDownloadProgress: function xpidm_onDownloadProgress(aInstall) {
+ },
+
+ onDownloadFailed: function(aInstall) {
+ },
+
+ onDownloadCancelled: function(aInstall) {
+ }
+}
+
+window.addEventListener("load", init, false);
+window.addEventListener("unload", uninit, false);
diff --git a/mobile/android/chrome/content/aboutAddons.xhtml b/mobile/android/chrome/content/aboutAddons.xhtml
new file mode 100644
index 0000000000..42d9cfa9c1
--- /dev/null
+++ b/mobile/android/chrome/content/aboutAddons.xhtml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
+%globalDTD;
+<!ENTITY % aboutDTD SYSTEM "chrome://browser/locale/aboutAddons.dtd" >
+%aboutDTD;
+]>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>&aboutAddons.title2;</title>
+ <meta name="viewport" content="width=device-width; user-scalable=0" />
+ <link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
+ <link rel="stylesheet" href="chrome://browser/skin/aboutBase.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutAddons.css" type="text/css"/>
+</head>
+
+<body dir="&locale.dir;">
+ <menu type="context" id="addonmenu">
+ <menuitem id="contextmenu-enable" label="&addonAction.enable;"></menuitem>
+ <menuitem id="contextmenu-disable" label="&addonAction.disable;" ></menuitem>
+ <menuitem id="contextmenu-uninstall" label="&addonAction.uninstall;" ></menuitem>
+ </menu>
+
+ <div id="addons-header" class="header">
+ <div>&aboutAddons.header2;</div>
+ </div>
+ <div id="addons-list" class="list">
+ </div>
+
+ <div id="addons-details" class="list">
+ <div class="addon-item list-item">
+ <img class="icon"/>
+ <div class="inner">
+ <div class="details">
+ <div class="title"></div><div class="version"></div>
+ </div>
+ <div class="description-full"></div>
+ <div class="options-header">&aboutAddons.options;</div>
+ <div class="options-box"></div>
+ </div>
+ <div class="warn-unsigned">&addonUnsigned.message; <a id="unsigned-learn-more">&addonUnsigned.learnMore;</a></div>
+ <div class="status status-uninstalled show-on-uninstall"></div>
+ <div class="buttons">
+ <button id="enable-btn" class="show-on-disable hide-on-enable hide-on-uninstall" >&addonAction.enable;</button>
+ <button id="disable-btn" class="show-on-enable hide-on-disable hide-on-uninstall" >&addonAction.disable;</button>
+ <button id="uninstall-btn" class="hide-on-uninstall" >&addonAction.uninstall;</button>
+ <button id="cancel-btn" class="show-on-uninstall" >&addonAction.undo;</button>
+ </div>
+ </div>
+ </div>
+
+ <script type="application/javascript;version=1.8" src="chrome://browser/content/aboutAddons.js"></script>
+</body>
+</html>
diff --git a/mobile/android/chrome/content/aboutCertError.xhtml b/mobile/android/chrome/content/aboutCertError.xhtml
new file mode 100644
index 0000000000..c5922e2fe9
--- /dev/null
+++ b/mobile/android/chrome/content/aboutCertError.xhtml
@@ -0,0 +1,264 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD
+ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % globalDTD
+ SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+ <!ENTITY % certerrorDTD
+ SYSTEM "chrome://browser/locale/aboutCertError.dtd">
+ %certerrorDTD;
+]>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>&certerror.pagetitle;</title>
+ <meta name="viewport" content="width=device-width; user-scalable=false" />
+ <link rel="stylesheet" href="chrome://global/skin/netError.css" type="text/css" media="all" />
+ <!-- This page currently uses the same favicon as neterror.xhtml.
+ If the location of the favicon is changed for both pages, the
+ FAVICON_ERRORPAGE_URL symbol in toolkit/components/places/src/nsFaviconService.h
+ should be updated. If this page starts using a different favicon
+ than neterrorm nsFaviconService->SetAndLoadFaviconForPage
+ should be updated to ignore this one as well. -->
+ <link rel="icon" type="image/png" id="favicon" sizes="64x64" href="chrome://browser/skin/images/certerror-warning.png"/>
+
+ <script type="application/javascript"><![CDATA[
+ // Error url MUST be formatted like this:
+ // about:certerror?e=error&u=url&d=desc
+
+ // Note that this file uses document.documentURI to get
+ // the URL (with the format from above). This is because
+ // document.location.href gets the current URI off the docshell,
+ // which is the URL displayed in the location bar, i.e.
+ // the URI that the user attempted to load.
+
+ function getCSSClass()
+ {
+ var url = document.documentURI;
+ var matches = url.match(/s\=([^&]+)\&/);
+ // s is optional, if no match just return nothing
+ if (!matches || matches.length < 2)
+ return "";
+
+ // parenthetical match is the second entry
+ return decodeURIComponent(matches[1]);
+ }
+
+ function getDescription()
+ {
+ var url = document.documentURI;
+ var desc = url.search(/d\=/);
+
+ // desc == -1 if not found; if so, return an empty string
+ // instead of what would turn out to be portions of the URI
+ if (desc == -1)
+ return "";
+
+ return decodeURIComponent(url.slice(desc + 2));
+ }
+
+ function initPage()
+ {
+ // Replace the "#1" string in the intro with the hostname. Trickier
+ // than it might seem since we want to preserve the <b> tags, but
+ // not allow for any injection by just using innerHTML. Instead,
+ // just find the right target text node.
+ var intro = document.getElementById('introContentP1');
+ function replaceWithHost(node) {
+ if (node.textContent == "#1")
+ node.textContent = location.host;
+ else
+ for(var i = 0; i < node.childNodes.length; i++)
+ replaceWithHost(node.childNodes[i]);
+ };
+ replaceWithHost(intro);
+
+ if (getCSSClass() == "expertBadCert") {
+ toggle('technicalContent');
+ toggle('expertContent');
+ }
+
+ // Disallow overrides if this is a Strict-Transport-Security
+ // host and the cert is bad (STS Spec section 7.3) or if the
+ // certerror is in a frame (bug 633691).
+ if (getCSSClass() == "badStsCert" || window != top)
+ document.getElementById("expertContent").setAttribute("hidden", "true");
+
+ var tech = document.getElementById("technicalContentText");
+ if (tech)
+ tech.textContent = getDescription();
+
+ addDomainErrorLinks();
+ }
+
+ /* Try to preserve the links contained in the error description, like
+ the error code.
+
+ Also, in the case of SSL error pages about domain mismatch, see if
+ we can hyperlink the user to the correct site. We don't want
+ to do this generically since it allows MitM attacks to redirect
+ users to a site under attacker control, but in certain cases
+ it is safe (and helpful!) to do so. Bug 402210
+ */
+ function addDomainErrorLinks() {
+ // Rather than textContent, we need to treat description as HTML
+ var sd = document.getElementById("technicalContentText");
+ if (sd) {
+ var desc = getDescription();
+
+ // sanitize description text - see bug 441169
+
+ // First, find the index of the <a> tags we care about, being
+ // careful not to use an over-greedy regex.
+ var codeRe = /<a id="errorCode" title="([^"]+)">/;
+ var codeResult = codeRe.exec(desc);
+ var domainRe = /<a id="cert_domain_link" title="([^"]+)">/;
+ var domainResult = domainRe.exec(desc);
+
+ // The order of these links in the description is fixed in
+ // TransportSecurityInfo.cpp:formatOverridableCertErrorMessage.
+ var firstResult = domainResult;
+ if (!domainResult)
+ firstResult = codeResult;
+ if (!firstResult)
+ return;
+
+ // Remove sd's existing children
+ sd.textContent = "";
+
+ // Everything up to the first link should be text content.
+ sd.appendChild(document.createTextNode(desc.slice(0, firstResult.index)));
+
+ // Now create the actual links.
+ if (domainResult) {
+ createLink(sd, "cert_domain_link", domainResult[1])
+ // Append text for anything between the two links.
+ sd.appendChild(document.createTextNode(desc.slice(desc.indexOf("</a>") + "</a>".length, codeResult.index)));
+ }
+ createLink(sd, "errorCode", codeResult[1])
+
+ // Finally, append text for anything after the last closing </a>.
+ sd.appendChild(document.createTextNode(desc.slice(desc.lastIndexOf("</a>") + "</a>".length)));
+ }
+
+ // Then initialize the cert domain link.
+ var link = document.getElementById('cert_domain_link');
+ if (!link)
+ return;
+
+ var okHost = link.getAttribute("title");
+ var thisHost = document.location.hostname;
+ var proto = document.location.protocol;
+
+ // If okHost is a wildcard domain ("*.example.com") let's
+ // use "www" instead. "*.example.com" isn't going to
+ // get anyone anywhere useful. bug 432491
+ okHost = okHost.replace(/^\*\./, "www.");
+
+ /* case #1:
+ * example.com uses an invalid security certificate.
+ *
+ * The certificate is only valid for www.example.com
+ *
+ * Make sure to include the "." ahead of thisHost so that
+ * a MitM attack on paypal.com doesn't hyperlink to "notpaypal.com"
+ *
+ * We'd normally just use a RegExp here except that we lack a
+ * library function to escape them properly (bug 248062), and
+ * domain names are famous for having '.' characters in them,
+ * which would allow spurious and possibly hostile matches.
+ */
+ if (okHost.endsWith("." + thisHost))
+ link.href = proto + okHost;
+
+ /* case #2:
+ * browser.garage.maemo.org uses an invalid security certificate.
+ *
+ * The certificate is only valid for garage.maemo.org
+ */
+ if (thisHost.endsWith("." + okHost))
+ link.href = proto + okHost;
+
+ // If we set a link, meaning there's something helpful for
+ // the user here, expand the section by default
+ if (link.href && getCSSClass() != "expertBadCert")
+ toggle("technicalContent");
+ }
+
+ function createLink(el, id, text) {
+ var anchorEl = document.createElement("a");
+ anchorEl.setAttribute("id", id);
+ anchorEl.setAttribute("title", text);
+ anchorEl.appendChild(document.createTextNode(text));
+ el.appendChild(anchorEl);
+ }
+
+ function toggle(id) {
+ var el = document.getElementById(id);
+ if (el.hasAttribute("collapsed"))
+ el.removeAttribute("collapsed");
+ else
+ el.setAttribute("collapsed", true);
+ }
+ ]]></script>
+ </head>
+
+ <body id="errorPage" class="certerror" dir="&locale.dir;">
+
+ <!-- PAGE CONTAINER (for styling purposes only) -->
+ <div id="errorPageContainer">
+
+ <!-- Error Title -->
+ <div id="errorTitle">
+ <h1 class="errorTitleText">&certerror.longpagetitle;</h1>
+ </div>
+
+ <!-- LONG CONTENT (the section most likely to require scrolling) -->
+ <div id="errorLongContent">
+ <div id="introContent">
+ <p id="introContentP1">&certerror.introPara1;</p>
+ </div>
+
+ <div id="whatShouldIDoContent">
+ <h2>&certerror.whatShouldIDo.heading;</h2>
+ <div id="whatShouldIDoContentText">
+ <p>&certerror.whatShouldIDo.content;</p>
+ <button id="getMeOutOfHereButton">&certerror.getMeOutOfHere.label;</button>
+ </div>
+ </div>
+
+ <!-- The following sections can be unhidden by default by setting the
+ "browser.xul.error_pages.expert_bad_cert" pref to true -->
+ <div id="technicalContent" collapsed="true">
+ <h2 class="expander" onclick="toggle('technicalContent');" id="technicalContentHeading">&certerror.technical.heading;</h2>
+ <p id="technicalContentText"/>
+ </div>
+
+ <div id="expertContent" collapsed="true">
+ <h2 class="expander" onclick="toggle('expertContent');" id="expertContentHeading">&certerror.expert.heading;</h2>
+ <div>
+ <p>&certerror.expert.content;</p>
+ <p>&certerror.expert.contentPara2;</p>
+ <button id="temporaryExceptionButton">&certerror.addTemporaryException.label;</button>
+ <button id="permanentExceptionButton">&certerror.addPermanentException.label;</button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!--
+ - Note: It is important to run the script this way, instead of using
+ - an onload handler. This is because error pages are loaded as
+ - LOAD_BACKGROUND, which means that onload handlers will not be executed.
+ -->
+ <script type="application/javascript">initPage();</script>
+
+ </body>
+</html>
diff --git a/mobile/android/chrome/content/aboutDownloads.js b/mobile/android/chrome/content/aboutDownloads.js
new file mode 100644
index 0000000000..add0a48e66
--- /dev/null
+++ b/mobile/android/chrome/content/aboutDownloads.js
@@ -0,0 +1,373 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", "resource://gre/modules/DownloadUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
+
+var gStrings = Services.strings.createBundle("chrome://browser/locale/aboutDownloads.properties");
+XPCOMUtils.defineLazyGetter(this, "strings",
+ () => Services.strings.createBundle("chrome://browser/locale/aboutDownloads.properties"));
+
+function deleteDownload(download) {
+ download.finalize(true).then(null, Cu.reportError);
+ OS.File.remove(download.target.path).then(null, ex => {
+ if (!(ex instanceof OS.File.Error && ex.becauseNoSuchFile)) {
+ Cu.reportError(ex);
+ }
+ });
+}
+
+var contextMenu = {
+ _items: [],
+ _targetDownload: null,
+
+ init: function () {
+ let element = document.getElementById("downloadmenu");
+ element.addEventListener("click",
+ event => event.download = this._targetDownload,
+ true);
+ this._items = [
+ new ContextMenuItem("open",
+ download => download.succeeded,
+ download => download.launch().then(null, Cu.reportError)),
+ new ContextMenuItem("retry",
+ download => download.error ||
+ (download.canceled && !download.hasPartialData),
+ download => download.start().then(null, Cu.reportError)),
+ new ContextMenuItem("remove",
+ download => download.stopped,
+ download => {
+ Downloads.getList(Downloads.ALL)
+ .then(list => list.remove(download))
+ .then(null, Cu.reportError);
+ deleteDownload(download);
+ }),
+ new ContextMenuItem("pause",
+ download => !download.stopped && download.hasPartialData,
+ download => download.cancel().then(null, Cu.reportError)),
+ new ContextMenuItem("resume",
+ download => download.canceled && download.hasPartialData,
+ download => download.start().then(null, Cu.reportError)),
+ new ContextMenuItem("cancel",
+ download => !download.stopped ||
+ (download.canceled && download.hasPartialData),
+ download => {
+ download.cancel().then(null, Cu.reportError);
+ download.removePartialData().then(null, Cu.reportError);
+ }),
+ // following menu item is a global action
+ new ContextMenuItem("removeall",
+ () => downloadLists.finished.length > 0,
+ () => downloadLists.removeFinished())
+ ];
+ },
+
+ addContextMenuEventListener: function (element) {
+ element.addEventListener("contextmenu", this.onContextMenu.bind(this));
+ },
+
+ onContextMenu: function (event) {
+ let target = event.target;
+ while (target && !target.download) {
+ target = target.parentNode;
+ }
+ if (!target) {
+ Cu.reportError("No download found for context menu target");
+ event.preventDefault();
+ return;
+ }
+
+ // capture the target download for menu items to use in a click event
+ this._targetDownload = target.download;
+ for (let item of this._items) {
+ item.updateVisibility(target.download);
+ }
+ }
+};
+
+function ContextMenuItem(name, isVisible, action) {
+ this.element = document.getElementById("contextmenu-" + name);
+ this.isVisible = isVisible;
+
+ this.element.addEventListener("click", event => action(event.download));
+}
+
+ContextMenuItem.prototype = {
+ updateVisibility: function (download) {
+ this.element.hidden = !this.isVisible(download);
+ }
+};
+
+function DownloadListView(type, listElementId) {
+ this.listElement = document.getElementById(listElementId);
+ contextMenu.addContextMenuEventListener(this.listElement);
+
+ this.items = new Map();
+
+ Downloads.getList(type)
+ .then(list => list.addView(this))
+ .then(null, Cu.reportError);
+
+ window.addEventListener("unload", event => {
+ Downloads.getList(type)
+ .then(list => list.removeView(this))
+ .then(null, Cu.reportError);
+ });
+}
+
+DownloadListView.prototype = {
+ get finished() {
+ let finished = [];
+ for (let download of this.items.keys()) {
+ if (download.stopped && (!download.hasPartialData || download.error)) {
+ finished.push(download);
+ }
+ }
+
+ return finished;
+ },
+
+ insertOrMoveItem: function (item) {
+ var compare = (a, b) => {
+ // active downloads always before stopped downloads
+ if (a.stopped != b.stopped) {
+ return b.stopped ? -1 : 1
+ }
+ // most recent downloads first
+ return b.startTime - a.startTime;
+ };
+
+ let insertLocation = this.listElement.firstChild;
+ while (insertLocation && compare(item.download, insertLocation.download) > 0) {
+ insertLocation = insertLocation.nextElementSibling;
+ }
+ this.listElement.insertBefore(item.element, insertLocation);
+ },
+
+ onDownloadAdded: function (download) {
+ let item = new DownloadItem(download);
+ this.items.set(download, item);
+ this.insertOrMoveItem(item);
+ },
+
+ onDownloadChanged: function (download) {
+ let item = this.items.get(download);
+ if (!item) {
+ Cu.reportError("No DownloadItem found for download");
+ return;
+ }
+
+ if (item.stateChanged) {
+ this.insertOrMoveItem(item);
+ }
+
+ item.onDownloadChanged();
+ },
+
+ onDownloadRemoved: function (download) {
+ let item = this.items.get(download);
+ if (!item) {
+ Cu.reportError("No DownloadItem found for download");
+ return;
+ }
+
+ this.items.delete(download);
+ this.listElement.removeChild(item.element);
+
+ Messaging.sendRequest({
+ type: "Download:Remove",
+ path: download.target.path
+ });
+ }
+};
+
+var downloadLists = {
+ init: function () {
+ this.publicDownloads = new DownloadListView(Downloads.PUBLIC, "public-downloads-list");
+ this.privateDownloads = new DownloadListView(Downloads.PRIVATE, "private-downloads-list");
+ },
+
+ get finished() {
+ return this.publicDownloads.finished.concat(this.privateDownloads.finished);
+ },
+
+ removeFinished: function () {
+ let finished = this.finished;
+ if (finished.length == 0) {
+ return;
+ }
+
+ let title = strings.GetStringFromName("downloadAction.deleteAll");
+ let messageForm = strings.GetStringFromName("downloadMessage.deleteAll");
+ let message = PluralForm.get(finished.length, messageForm).replace("#1", finished.length);
+
+ if (Services.prompt.confirm(null, title, message)) {
+ Downloads.getList(Downloads.ALL)
+ .then(list => {
+ for (let download of finished) {
+ list.remove(download).then(null, Cu.reportError);
+ deleteDownload(download);
+ }
+ }, Cu.reportError);
+ }
+ }
+};
+
+function DownloadItem(download) {
+ this._download = download;
+ this._updateFromDownload();
+
+ this._domain = DownloadUtils.getURIHost(download.source.url)[0];
+ this._fileName = this._htmlEscape(OS.Path.basename(download.target.path));
+ this._iconUrl = "moz-icon://" + this._fileName + "?size=64";
+ this._startDate = this._htmlEscape(DownloadUtils.getReadableDates(download.startTime)[0]);
+
+ this._element = this.createElement();
+}
+
+const kDownloadStatePropertyNames = [
+ "stopped",
+ "succeeded",
+ "canceled",
+ "error",
+ "startTime"
+];
+
+DownloadItem.prototype = {
+ _htmlEscape : function (s) {
+ s = s.replace(/&/g, "&amp;");
+ s = s.replace(/>/g, "&gt;");
+ s = s.replace(/</g, "&lt;");
+ s = s.replace(/"/g, "&quot;");
+ s = s.replace(/'/g, "&apos;");
+ return s;
+ },
+
+ _updateFromDownload: function () {
+ this._state = {};
+ kDownloadStatePropertyNames.forEach(
+ name => this._state[name] = this._download[name],
+ this);
+ },
+
+ get stateChanged() {
+ return kDownloadStatePropertyNames.some(
+ name => this._state[name] != this._download[name],
+ this);
+ },
+
+ get download() {
+ return this._download;
+ },
+ get element() {
+ return this._element;
+ },
+
+ createElement: function() {
+ let template = document.getElementById("download-item");
+ // TODO: use this once <template> is working
+ // let element = document.importNode(template.content, true);
+
+ // simulate a <template> node...
+ let element = template.cloneNode(true);
+ element.removeAttribute("id");
+ element.removeAttribute("style");
+
+ // launch the download if clicked
+ element.addEventListener("click", this.onClick.bind(this));
+
+ // set download as an expando property for the context menu
+ element.download = this.download;
+
+ // fill in template placeholders
+ this.updateElement(element);
+
+ return element;
+ },
+
+ updateElement: function (element) {
+ element.querySelector(".date").textContent = this.startDate;
+ element.querySelector(".domain").textContent = this.domain;
+ element.querySelector(".icon").src = this.iconUrl;
+ element.querySelector(".size").textContent = this.size;
+ element.querySelector(".state").textContent = this.stateDescription;
+ element.querySelector(".title").setAttribute("value", this.fileName);
+ },
+
+ onClick: function (event) {
+ if (this.download.succeeded) {
+ this.download.launch().then(null, Cu.reportError);
+ }
+ },
+
+ onDownloadChanged: function () {
+ this._updateFromDownload();
+ this.updateElement(this.element);
+ },
+
+ // template properties below
+ get domain() {
+ return this._domain;
+ },
+ get fileName() {
+ return this._fileName;
+ },
+ get id() {
+ return this._id;
+ },
+ get iconUrl() {
+ return this._iconUrl;
+ },
+
+ get size() {
+ if (this.download.succeeded && this.download.target.exists) {
+ return DownloadUtils.convertByteUnits(this.download.target.size).join("");
+ } else if (this.download.hasProgress) {
+ return DownloadUtils.convertByteUnits(this.download.totalBytes).join("");
+ }
+ return strings.GetStringFromName("downloadState.unknownSize");
+ },
+
+ get startDate() {
+ return this._startDate;
+ },
+
+ get stateDescription() {
+ let name;
+ if (this.download.error) {
+ name = "downloadState.failed";
+ } else if (this.download.canceled) {
+ if (this.download.hasPartialData) {
+ name = "downloadState.paused";
+ } else {
+ name = "downloadState.canceled";
+ }
+ } else if (!this.download.stopped) {
+ if (this.download.currentBytes > 0) {
+ name = "downloadState.downloading";
+ } else {
+ name = "downloadState.starting";
+ }
+ }
+
+ if (name) {
+ return strings.GetStringFromName(name);
+ }
+ return "";
+ }
+};
+
+window.addEventListener("DOMContentLoaded", event => {
+ contextMenu.init();
+ downloadLists.init()
+}); \ No newline at end of file
diff --git a/mobile/android/chrome/content/aboutDownloads.xhtml b/mobile/android/chrome/content/aboutDownloads.xhtml
new file mode 100644
index 0000000000..6b90256941
--- /dev/null
+++ b/mobile/android/chrome/content/aboutDownloads.xhtml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
+%globalDTD;
+<!ENTITY % downloadsDTD SYSTEM "chrome://browser/locale/aboutDownloads.dtd" >
+%downloadsDTD;
+]>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<head>
+ <title>&aboutDownloads.title;</title>
+ <meta name="viewport" content="width=device-width; user-scalable=0" />
+ <link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
+ <link rel="stylesheet" href="chrome://browser/skin/aboutBase.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutDownloads.css" type="text/css"/>
+</head>
+
+<body dir="&locale.dir;">
+ <menu type="context" id="downloadmenu">
+ <menuitem id="contextmenu-open" label="&aboutDownloads.open;"></menuitem>
+ <menuitem id="contextmenu-retry" label="&aboutDownloads.retry;"></menuitem>
+ <menuitem id="contextmenu-remove" label="&aboutDownloads.remove;"></menuitem>
+ <menuitem id="contextmenu-pause" label="&aboutDownloads.pause;"></menuitem>
+ <menuitem id="contextmenu-resume" label="&aboutDownloads.resume;"></menuitem>
+ <menuitem id="contextmenu-cancel" label="&aboutDownloads.cancel;"></menuitem>
+ <menuitem id="contextmenu-removeall" label="&aboutDownloads.removeAll;"></menuitem>
+ </menu>
+
+ <!--template id="download-item"-->
+ <li id="download-item" class="list-item" role="button" contextmenu="downloadmenu" style="display: none">
+ <img class="icon" src=""/>
+ <div class="details">
+ <div class="row">
+ <!-- This is a hack so that we can crop this label in its center -->
+ <xul:label class="title" crop="center" value=""/>
+ <div class="date"></div>
+ </div>
+ <div class="size"></div>
+ <div class="domain"></div>
+ <div class="state"></div>
+ </div>
+ </li>
+ <!--/template-->
+
+ <div class="header">
+ <div>&aboutDownloads.header;</div>
+ </div>
+ <ul id="private-downloads-list" class="list"></ul>
+ <ul id="public-downloads-list" class="list"></ul>
+ <span id="no-downloads-indicator">&aboutDownloads.empty;</span>
+ <script type="application/javascript;version=1.8" src="chrome://browser/content/aboutDownloads.js"/>
+</body>
+</html>
diff --git a/mobile/android/chrome/content/aboutHealthReport.js b/mobile/android/chrome/content/aboutHealthReport.js
new file mode 100644
index 0000000000..070eb821df
--- /dev/null
+++ b/mobile/android/chrome/content/aboutHealthReport.js
@@ -0,0 +1,192 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+Cu.import("resource://gre/modules/SharedPreferences.jsm");
+
+// Name of Android SharedPreference controlling whether to upload
+// health reports.
+const PREF_UPLOAD_ENABLED = "android.not_a_preference.healthreport.uploadEnabled";
+
+// Name of Gecko Pref specifying report content location.
+const PREF_REPORTURL = "datareporting.healthreport.about.reportUrl";
+
+// Monotonically increasing wrapper API version number.
+const WRAPPER_VERSION = 1;
+
+const EVENT_HEALTH_REQUEST = "HealthReport:Request";
+const EVENT_HEALTH_RESPONSE = "HealthReport:Response";
+
+// about:healthreport prefs are stored in Firefox's default Android
+// SharedPreferences.
+var sharedPrefs = SharedPreferences.forApp();
+
+var healthReportWrapper = {
+ init: function () {
+ let iframe = document.getElementById("remote-report");
+ iframe.addEventListener("load", healthReportWrapper.initRemotePage, false);
+ let report = this._getReportURI();
+ iframe.src = report.spec;
+ console.log("AboutHealthReport: loading content from " + report.spec);
+
+ sharedPrefs.addObserver(PREF_UPLOAD_ENABLED, this, false);
+ Services.obs.addObserver(this, EVENT_HEALTH_RESPONSE, false);
+ },
+
+ observe: function (subject, topic, data) {
+ if (topic == PREF_UPLOAD_ENABLED) {
+ this.updatePrefState();
+ } else if (topic == EVENT_HEALTH_RESPONSE) {
+ this.updatePayload(data);
+ }
+ },
+
+ uninit: function () {
+ sharedPrefs.removeObserver(PREF_UPLOAD_ENABLED, this);
+ Services.obs.removeObserver(this, EVENT_HEALTH_RESPONSE);
+ },
+
+ _getReportURI: function () {
+ let url = Services.urlFormatter.formatURLPref(PREF_REPORTURL);
+ // This handles URLs that already have query parameters.
+ let uri = Services.io.newURI(url, null, null).QueryInterface(Ci.nsIURL);
+ uri.query += ((uri.query != "") ? "&v=" : "v=") + WRAPPER_VERSION;
+ return uri;
+ },
+
+ onOptIn: function () {
+ console.log("AboutHealthReport: page sent opt-in command.");
+ sharedPrefs.setBoolPref(PREF_UPLOAD_ENABLED, true);
+ this.updatePrefState();
+ },
+
+ onOptOut: function () {
+ console.log("AboutHealthReport: page sent opt-out command.");
+ sharedPrefs.setBoolPref(PREF_UPLOAD_ENABLED, false);
+ this.updatePrefState();
+ },
+
+ updatePrefState: function () {
+ console.log("AboutHealthReport: sending pref state to page.");
+ try {
+ let prefs = {
+ enabled: sharedPrefs.getBoolPref(PREF_UPLOAD_ENABLED),
+ };
+ this.injectData("prefs", prefs);
+ } catch (e) {
+ this.reportFailure(this.ERROR_PREFS_FAILED);
+ }
+ },
+
+ refreshPayload: function () {
+ console.log("AboutHealthReport: page requested fresh payload.");
+ Messaging.sendRequest({
+ type: EVENT_HEALTH_REQUEST,
+ });
+ },
+
+ updatePayload: function (data) {
+ healthReportWrapper.injectData("payload", data);
+ // Data is supposed to be a string, so the length should be
+ // defined. Just in case, we do this after injecting the data.
+ console.log("AboutHealthReport: sending payload to page " +
+ "(" + typeof(data) + " of length " + data.length + ").");
+ },
+
+ injectData: function (type, content) {
+ let report = this._getReportURI();
+
+ // file: URIs can't be used for targetOrigin, so we use "*" for
+ // this special case. In all other cases, pass in the URL to the
+ // report so we properly restrict the message dispatch.
+ let reportUrl = (report.scheme == "file") ? "*" : report.spec;
+
+ let data = {
+ type: type,
+ content: content,
+ };
+
+ let iframe = document.getElementById("remote-report");
+ iframe.contentWindow.postMessage(data, reportUrl);
+ },
+
+ showSettings: function () {
+ console.log("AboutHealthReport: showing settings.");
+ Messaging.sendRequest({
+ type: "Settings:Show",
+ resource: "preferences_vendor",
+ });
+ },
+
+ launchUpdater: function () {
+ console.log("AboutHealthReport: launching updater.");
+ Messaging.sendRequest({
+ type: "Updater:Launch",
+ });
+ },
+
+ handleRemoteCommand: function (evt) {
+ switch (evt.detail.command) {
+ case "DisableDataSubmission":
+ this.onOptOut();
+ break;
+ case "EnableDataSubmission":
+ this.onOptIn();
+ break;
+ case "RequestCurrentPrefs":
+ this.updatePrefState();
+ break;
+ case "RequestCurrentPayload":
+ this.refreshPayload();
+ break;
+ case "ShowSettings":
+ this.showSettings();
+ break;
+ case "LaunchUpdater":
+ this.launchUpdater();
+ break;
+ default:
+ Cu.reportError("Unexpected remote command received: " + evt.detail.command +
+ ". Ignoring command.");
+ break;
+ }
+ },
+
+ initRemotePage: function () {
+ let iframe = document.getElementById("remote-report").contentDocument;
+ iframe.addEventListener("RemoteHealthReportCommand",
+ function onCommand(e) {healthReportWrapper.handleRemoteCommand(e);},
+ false);
+ healthReportWrapper.injectData("begin", null);
+ },
+
+ // error handling
+ ERROR_INIT_FAILED: 1,
+ ERROR_PAYLOAD_FAILED: 2,
+ ERROR_PREFS_FAILED: 3,
+
+ reportFailure: function (error) {
+ let details = {
+ errorType: error,
+ };
+ healthReportWrapper.injectData("error", details);
+ },
+
+ handleInitFailure: function () {
+ healthReportWrapper.reportFailure(healthReportWrapper.ERROR_INIT_FAILED);
+ },
+
+ handlePayloadFailure: function () {
+ healthReportWrapper.reportFailure(healthReportWrapper.ERROR_PAYLOAD_FAILED);
+ },
+};
+
+window.addEventListener("load", healthReportWrapper.init.bind(healthReportWrapper), false);
+window.addEventListener("unload", healthReportWrapper.uninit.bind(healthReportWrapper), false);
diff --git a/mobile/android/chrome/content/aboutHealthReport.xhtml b/mobile/android/chrome/content/aboutHealthReport.xhtml
new file mode 100644
index 0000000000..73dae0380b
--- /dev/null
+++ b/mobile/android/chrome/content/aboutHealthReport.xhtml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+%globalDTD;
+<!ENTITY % aboutHealthReportDTD SYSTEM "chrome://browser/locale/aboutHealthReport.dtd" >
+%aboutHealthReportDTD;
+]>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>&abouthealth.pagetitle;</title>
+ <link rel="icon" type="image/png" sizes="64x64"
+ href="chrome://branding/content/favicon64.png" />
+ <link rel="stylesheet"
+ href="chrome://browser/skin/aboutHealthReport.css"
+ type="text/css" />
+ <script type="text/javascript;version=1.8"
+ src="chrome://browser/content/aboutHealthReport.js" />
+ </head>
+ <body>
+ <iframe id="remote-report"/>
+ </body>
+</html>
diff --git a/mobile/android/chrome/content/aboutHome.xhtml b/mobile/android/chrome/content/aboutHome.xhtml
new file mode 100644
index 0000000000..692eac2ec9
--- /dev/null
+++ b/mobile/android/chrome/content/aboutHome.xhtml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+ %brandDTD;
+ <!ENTITY % abouthomeDTD SYSTEM "chrome://browser/locale/aboutHome.dtd">
+ %abouthomeDTD;
+]>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>&abouthome.title;</title>
+</head>
+<body>
+</body>
+</html>
diff --git a/mobile/android/chrome/content/aboutLogins.js b/mobile/android/chrome/content/aboutLogins.js
new file mode 100644
index 0000000000..99e2af841a
--- /dev/null
+++ b/mobile/android/chrome/content/aboutLogins.js
@@ -0,0 +1,518 @@
+/* Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this file,
+* You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils;
+
+Cu.import("resource://services-common/utils.js"); /*global: CommonUtils */
+Cu.import("resource://gre/modules/Messaging.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/TelemetryStopwatch.jsm");
+
+XPCOMUtils.defineLazyGetter(window, "gChromeWin", () =>
+ window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow)
+ .QueryInterface(Ci.nsIDOMChromeWindow));
+
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
+ "resource://gre/modules/Prompt.jsm");
+
+var debug = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.d.bind(null, "AboutLogins");
+
+var gStringBundle = Services.strings.createBundle("chrome://browser/locale/aboutLogins.properties");
+
+function copyStringShowSnackbar(string, notifyString) {
+ try {
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(string);
+ Snackbars.show(notifyString, Snackbars.LENGTH_LONG);
+ } catch (e) {
+ debug("Error copying from about:logins");
+ Snackbars.show(gStringBundle.GetStringFromName("loginsDetails.copyFailed"), Snackbars.LENGTH_LONG);
+ }
+}
+
+// Delay filtering while typing in MS
+const FILTER_DELAY = 500;
+
+var Logins = {
+ _logins: [],
+ _filterTimer: null,
+ _selectedLogin: null,
+
+ // Load the logins list, displaying interstitial UI (see
+ // #logins-list-loading-body) while loading. There are careful
+ // jank-avoiding measures taken in this function; be careful when
+ // modifying it!
+ //
+ // Returns a Promise that resolves to the list of logins, ordered by
+ // hostname.
+ _promiseLogins: function() {
+ let contentBody = document.getElementById("content-body");
+ let emptyBody = document.getElementById("empty-body");
+ let filterIcon = document.getElementById("filter-button");
+
+ let showSpinner = () => {
+ this._toggleListBody(true);
+ emptyBody.classList.add("hidden");
+ };
+
+ let getAllLogins = () => {
+ let logins = [];
+ try {
+ logins = Services.logins.getAllLogins();
+ } catch(e) {
+ // It's likely that the Master Password was not entered; give
+ // a hint to the next person.
+ throw new Error("Possible Master Password permissions error: " + e.toString());
+ }
+
+ logins.sort((a, b) => a.hostname.localeCompare(b.hostname));
+
+ return logins;
+ };
+
+ let hideSpinner = (logins) => {
+ this._toggleListBody(false);
+
+ if (!logins.length) {
+ contentBody.classList.add("hidden");
+ filterIcon.classList.add("hidden");
+ emptyBody.classList.remove("hidden");
+ } else {
+ contentBody.classList.remove("hidden");
+ emptyBody.classList.add("hidden");
+ }
+
+ return logins;
+ };
+
+ // Return a promise that is resolved after a paint.
+ let waitForPaint = () => {
+ // We're changing 'display'. We need to wait for the new value to take
+ // effect; otherwise, we'll block and never paint a change. Since
+ // requestAnimationFrame callback is generally triggered *before* any
+ // style flush and layout, we wait for two animation frames. This
+ // approach was cribbed from
+ // https://dxr.mozilla.org/mozilla-central/rev/5abe3c4deab94270440422c850bbeaf512b1f38d/browser/base/content/browser-fullScreen.js?offset=0#469.
+ return new Promise(function(resolve, reject) {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ resolve();
+ });
+ });
+ });
+ };
+
+ // getAllLogins janks the main-thread. We need to paint before that jank;
+ // by throwing the janky load onto the next tick, we paint the spinner; the
+ // spinner is CSS animated off-main-thread.
+ return Promise.resolve()
+ .then(showSpinner)
+ .then(waitForPaint)
+ .then(getAllLogins)
+ .then(hideSpinner);
+ },
+
+ // Reload the logins list, displaying interstitial UI while loading.
+ // Update the stored and displayed list upon completion.
+ _reloadList: function() {
+ this._promiseLogins()
+ .then((logins) => {
+ this._logins = logins;
+ this._loadList(logins);
+ })
+ .catch((e) => {
+ // There's no way to recover from errors, sadly. Log and make
+ // it obvious that something is up.
+ this._logins = [];
+ debug("Failed to _reloadList!");
+ Cu.reportError(e);
+ });
+ },
+
+ _toggleListBody: function(isLoading) {
+ let contentBody = document.getElementById("content-body");
+ let loadingBody = document.getElementById("logins-list-loading-body");
+
+ if (isLoading) {
+ contentBody.classList.add("hidden");
+ loadingBody.classList.remove("hidden");
+ } else {
+ loadingBody.classList.add("hidden");
+ contentBody.classList.remove("hidden");
+ }
+ },
+
+ init: function () {
+ window.addEventListener("popstate", this , false);
+
+ Services.obs.addObserver(this, "passwordmgr-storage-changed", false);
+ document.getElementById("update-btn").addEventListener("click", this._onSaveEditLogin.bind(this), false);
+ document.getElementById("password-btn").addEventListener("click", this._onPasswordBtn.bind(this), false);
+
+ let filterInput = document.getElementById("filter-input");
+ let filterContainer = document.getElementById("filter-input-container");
+
+ filterInput.addEventListener("input", (event) => {
+ // Stop any in-progress filter timer
+ if (this._filterTimer) {
+ clearTimeout(this._filterTimer);
+ this._filterTimer = null;
+ }
+
+ // Start a new timer
+ this._filterTimer = setTimeout(() => {
+ this._filter(event);
+ }, FILTER_DELAY);
+ }, false);
+
+ filterInput.addEventListener("blur", (event) => {
+ filterContainer.setAttribute("hidden", true);
+ });
+
+ document.getElementById("filter-button").addEventListener("click", (event) => {
+ filterContainer.removeAttribute("hidden");
+ filterInput.focus();
+ }, false);
+
+ document.getElementById("filter-clear").addEventListener("click", (event) => {
+ // Stop any in-progress filter timer
+ if (this._filterTimer) {
+ clearTimeout(this._filterTimer);
+ this._filterTimer = null;
+ }
+
+ filterInput.blur();
+ filterInput.value = "";
+ this._loadList(this._logins);
+ }, false);
+
+ this._showList();
+
+ this._updatePasswordBtn(true);
+
+ this._reloadList();
+ },
+
+ uninit: function () {
+ Services.obs.removeObserver(this, "passwordmgr-storage-changed");
+ window.removeEventListener("popstate", this, false);
+ },
+
+ _loadList: function (logins) {
+ let list = document.getElementById("logins-list");
+ let newList = list.cloneNode(false);
+
+ logins.forEach(login => {
+ let item = this._createItemForLogin(login);
+ newList.appendChild(item);
+ });
+
+ list.parentNode.replaceChild(newList, list);
+ },
+
+ _showList: function () {
+ let loginsListPage = document.getElementById("logins-list-page");
+ loginsListPage.classList.remove("hidden");
+
+ let editLoginPage = document.getElementById("edit-login-page");
+ editLoginPage.classList.add("hidden");
+
+ // If the Show/Hide password button has been flipped, reset it
+ if (this._isPasswordBtnInHideMode()) {
+ this._updatePasswordBtn(true);
+ }
+ },
+
+ _onPopState: function (event) {
+ // Called when back/forward is used to change the state of the page
+ if (event.state) {
+ this._showEditLoginDialog(event.state.id);
+ } else {
+ this._selectedLogin = null;
+ this._showList();
+ }
+ },
+ _showEditLoginDialog: function (login) {
+ let listPage = document.getElementById("logins-list-page");
+ listPage.classList.add("hidden");
+
+ let editLoginPage = document.getElementById("edit-login-page");
+ editLoginPage.classList.remove("hidden");
+
+ let usernameField = document.getElementById("username");
+ usernameField.value = login.username;
+ let passwordField = document.getElementById("password");
+ passwordField.value = login.password;
+ let domainField = document.getElementById("hostname");
+ domainField.value = login.hostname;
+
+ let img = document.getElementById("favicon");
+ this._loadFavicon(img, login.hostname);
+
+ let headerText = document.getElementById("edit-login-header-text");
+ if (login.hostname && (login.hostname != "")) {
+ headerText.textContent = login.hostname;
+ }
+ else {
+ headerText.textContent = gStringBundle.GetStringFromName("editLogin.fallbackTitle");
+ }
+
+ passwordField.addEventListener("input", (event) => {
+ let newPassword = passwordField.value;
+ let updateBtn = document.getElementById("update-btn");
+
+ if (newPassword === "") {
+ updateBtn.disabled = true;
+ updateBtn.classList.add("disabled-btn");
+ } else if ((newPassword !== "") && (updateBtn.disabled === true)) {
+ updateBtn.disabled = false;
+ updateBtn.classList.remove("disabled-btn");
+ }
+ }, false);
+ },
+
+ _onSaveEditLogin: function() {
+ let newUsername = document.getElementById("username").value;
+ let newPassword = document.getElementById("password").value;
+ let newDomain = document.getElementById("hostname").value;
+ let origUsername = this._selectedLogin.username;
+ let origPassword = this._selectedLogin.password;
+ let origDomain = this._selectedLogin.hostname;
+
+ try {
+ if ((newUsername === origUsername) &&
+ (newPassword === origPassword) &&
+ (newDomain === origDomain) ) {
+ Snackbars.show(gStringBundle.GetStringFromName("editLogin.saved1"), Snackbars.LENGTH_LONG);
+ this._showList();
+ return;
+ }
+
+ let logins = Services.logins.findLogins({}, origDomain, origDomain, null);
+
+ for (let i = 0; i < logins.length; i++) {
+ if (logins[i].username == origUsername) {
+ let clone = logins[i].clone();
+ clone.username = newUsername;
+ clone.password = newPassword;
+ clone.hostname = newDomain;
+ Services.logins.removeLogin(logins[i]);
+ Services.logins.addLogin(clone);
+ break;
+ }
+ }
+ } catch (e) {
+ Snackbars.show(gStringBundle.GetStringFromName("editLogin.couldNotSave"), Snackbars.LENGTH_LONG);
+ return;
+ }
+ Snackbars.show(gStringBundle.GetStringFromName("editLogin.saved1"), Snackbars.LENGTH_LONG);
+ this._showList();
+ },
+
+ _onPasswordBtn: function () {
+ this._updatePasswordBtn(this._isPasswordBtnInHideMode());
+ },
+
+ _updatePasswordBtn: function (aShouldShow) {
+ let passwordField = document.getElementById("password");
+ let button = document.getElementById("password-btn");
+ let show = gStringBundle.GetStringFromName("password-btn.show");
+ let hide = gStringBundle.GetStringFromName("password-btn.hide");
+ if (aShouldShow) {
+ passwordField.type = "password";
+ button.textContent = show;
+ button.classList.remove("password-btn-hide");
+ } else {
+ passwordField.type = "text";
+ button.textContent= hide;
+ button.classList.add("password-btn-hide");
+ }
+ },
+
+ _isPasswordBtnInHideMode: function () {
+ let button = document.getElementById("password-btn");
+ return button.classList.contains("password-btn-hide");
+ },
+
+ _showPassword: function(password) {
+ let passwordPrompt = new Prompt({
+ window: window,
+ message: password,
+ buttons: [
+ gStringBundle.GetStringFromName("loginsDialog.copy"),
+ gStringBundle.GetStringFromName("loginsDialog.cancel") ]
+ }).show((data) => {
+ switch (data.button) {
+ case 0:
+ // Corresponds to "Copy password" button.
+ copyStringShowSnackbar(password, gStringBundle.GetStringFromName("loginsDetails.passwordCopied"));
+ }
+ });
+ },
+
+ _onLoginClick: function (event) {
+ let loginItem = event.currentTarget;
+ let login = loginItem.login;
+ if (!login) {
+ debug("No login!");
+ return;
+ }
+
+ let prompt = new Prompt({
+ window: window,
+ });
+ let menuItems = [
+ { label: gStringBundle.GetStringFromName("loginsMenu.showPassword") },
+ { label: gStringBundle.GetStringFromName("loginsMenu.copyPassword") },
+ { label: gStringBundle.GetStringFromName("loginsMenu.copyUsername") },
+ { label: gStringBundle.GetStringFromName("loginsMenu.editLogin") },
+ { label: gStringBundle.GetStringFromName("loginsMenu.delete") }
+ ];
+
+ prompt.setSingleChoiceItems(menuItems);
+ prompt.show((data) => {
+ // Switch on indices of buttons, as they were added when creating login item.
+ switch (data.button) {
+ case 0:
+ this._showPassword(login.password);
+ break;
+ case 1:
+ copyStringShowSnackbar(login.password, gStringBundle.GetStringFromName("loginsDetails.passwordCopied"));
+ break;
+ case 2:
+ copyStringShowSnackbar(login.username, gStringBundle.GetStringFromName("loginsDetails.usernameCopied"));
+ break;
+ case 3:
+ this._selectedLogin = login;
+ this._showEditLoginDialog(login);
+ history.pushState({ id: login.guid }, document.title);
+ break;
+ case 4:
+ let confirmPrompt = new Prompt({
+ window: window,
+ message: gStringBundle.GetStringFromName("loginsDialog.confirmDelete"),
+ buttons: [
+ gStringBundle.GetStringFromName("loginsDialog.confirm"),
+ gStringBundle.GetStringFromName("loginsDialog.cancel") ]
+ });
+ confirmPrompt.show((data) => {
+ switch (data.button) {
+ case 0:
+ // Corresponds to "confirm" button.
+ Services.logins.removeLogin(login);
+ }
+ });
+ }
+ });
+ },
+
+ _loadFavicon: function (aImg, aHostname) {
+ // Load favicon from cache.
+ Messaging.sendRequestForResult({
+ type: "Favicon:CacheLoad",
+ url: aHostname,
+ }).then(function(faviconUrl) {
+ aImg.style.backgroundImage= "url('" + faviconUrl + "')";
+ aImg.style.visibility = "visible";
+ }, function(data) {
+ debug("Favicon cache failure : " + data);
+ aImg.style.visibility = "visible";
+ });
+ },
+
+ _createItemForLogin: function (login) {
+ let loginItem = document.createElement("div");
+
+ loginItem.setAttribute("loginID", login.guid);
+ loginItem.className = "login-item list-item";
+
+ loginItem.addEventListener("click", this, true);
+
+ // Create item icon.
+ let img = document.createElement("div");
+ img.className = "icon";
+
+ this._loadFavicon(img, login.hostname);
+ loginItem.appendChild(img);
+
+ // Create item details.
+ let inner = document.createElement("div");
+ inner.className = "inner";
+
+ let details = document.createElement("div");
+ details.className = "details";
+ inner.appendChild(details);
+
+ let titlePart = document.createElement("div");
+ titlePart.className = "hostname";
+ titlePart.textContent = login.hostname;
+ details.appendChild(titlePart);
+
+ let versionPart = document.createElement("div");
+ versionPart.textContent = login.httpRealm;
+ versionPart.className = "realm";
+ details.appendChild(versionPart);
+
+ let descPart = document.createElement("div");
+ descPart.textContent = login.username;
+ descPart.className = "username";
+ inner.appendChild(descPart);
+
+ loginItem.appendChild(inner);
+ loginItem.login = login;
+ return loginItem;
+ },
+
+ handleEvent: function (event) {
+ switch (event.type) {
+ case "popstate": {
+ this._onPopState(event);
+ break;
+ }
+ case "click": {
+ this._onLoginClick(event);
+ break;
+ }
+ }
+ },
+
+ observe: function (subject, topic, data) {
+ switch(topic) {
+ case "passwordmgr-storage-changed": {
+ this._reloadList();
+ break;
+ }
+ }
+ },
+
+ _filter: function(event) {
+ let value = event.target.value.toLowerCase();
+ let logins = this._logins.filter((login) => {
+ if (login.hostname.toLowerCase().indexOf(value) != -1) {
+ return true;
+ }
+ if (login.username &&
+ login.username.toLowerCase().indexOf(value) != -1) {
+ return true;
+ }
+ if (login.httpRealm &&
+ login.httpRealm.toLowerCase().indexOf(value) != -1) {
+ return true;
+ }
+ return false;
+ });
+
+ this._loadList(logins);
+ }
+};
+
+window.addEventListener("load", Logins.init.bind(Logins), false);
+window.addEventListener("unload", Logins.uninit.bind(Logins), false);
diff --git a/mobile/android/chrome/content/aboutLogins.xhtml b/mobile/android/chrome/content/aboutLogins.xhtml
new file mode 100644
index 0000000000..02225d43a7
--- /dev/null
+++ b/mobile/android/chrome/content/aboutLogins.xhtml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
+<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
+%globalDTD;
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % aboutDTD SYSTEM "chrome://browser/locale/aboutLogins.dtd" >
+%aboutDTD;
+]>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+- License, v. 2.0. If a copy of the MPL was not distributed with this
+- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>&aboutLogins.title;</title>
+ <meta name="viewport" content="width=device-width; user-scalable=0" />
+ <link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
+ <link rel="stylesheet" href="chrome://browser/skin/spinner.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutBase.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutLogins.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://browser/content/aboutLogins.js"></script>
+ </head>
+ <body dir="&locale.dir;">
+
+ <div id="logins-list-page">
+ <div id="logins-header" class="header">
+ <div>&aboutLogins.title;</div>
+ <ul class="toolbar-buttons">
+ <li id="filter-button"></li>
+ </ul>
+ </div>
+ <div id="content-body">
+ <div id="logins-list" class="list"/>
+ <div id="filter-input-container" hidden="true">
+ <input id="filter-input" type="search"/>
+ <div id="filter-clear"/>
+ </div>
+ </div>
+ <div id="logins-list-loading-body" class="hidden">
+ <div id="loading-img-container">
+
+ <div id="spinner" class="mui-refresh-main">
+ <div class="mui-refresh-wrapper">
+ <div class="mui-spinner-wrapper">
+ <div class="mui-spinner-main">
+ <div class="mui-spinner-left">
+ <div class="mui-half-circle-left" />
+ </div>
+ <div class="mui-spinner-right">
+ <div class="mui-half-circle-right" />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ </div>
+ </div>
+ <div id="empty-body" class="hidden">
+ <div id="empty-obj-text-container">
+ <object type="image/svg+xml" id="empty-icon" data="chrome://browser/skin/images/icon_key_emptypage.svg"/>
+ <div class="empty-text">&aboutLogins.emptyLoginText;</div>
+ <div class="empty-hint">&aboutLogins.emptyLoginHint;</div>
+ </div>
+ </div>
+ </div>
+
+ <div id="edit-login-page" class="hidden">
+ <div id="edit-login-header" class="header">
+ <div id="edit-login-header-text"/>
+ </div>
+ <div class="edit-login-div">
+ <div id="favicon" class="edit-login-icon"/>
+ <input type="text" name="hostname" id="hostname" class="edit-login-input"/>
+ </div>
+ <div class="edit-login-div">
+ <input type="text" name="username" id="username" class="edit-login-input" autocomplete="off"/>
+ </div>
+ <div class="edit-login-div">
+ <input type="password" id="password" name="password" value="password" class="edit-login-input" />
+ <button id="password-btn"></button>
+ </div>
+ <div class="edit-login-div">
+ <button id="update-btn" class="update-button">&aboutLogins.update;</button>
+ </div>
+ </div>
+
+ </body>
+</html>
diff --git a/mobile/android/chrome/content/aboutPrivateBrowsing.js b/mobile/android/chrome/content/aboutPrivateBrowsing.js
new file mode 100644
index 0000000000..782abfb5d9
--- /dev/null
+++ b/mobile/android/chrome/content/aboutPrivateBrowsing.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(window, "gChromeWin", () =>
+ window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow)
+ .QueryInterface(Ci.nsIDOMChromeWindow));
+
+document.addEventListener("DOMContentLoaded", function() {
+ let BrowserApp = gChromeWin.BrowserApp;
+
+ if (!PrivateBrowsingUtils.isContentWindowPrivate(window)) {
+ document.body.setAttribute("class", "normal");
+ document.getElementById("newPrivateTabLink").addEventListener("click", function() {
+ BrowserApp.addTab("about:privatebrowsing", { selected: true, parentId: BrowserApp.selectedTab.id, isPrivate: true });
+ }, false);
+ }
+ }, false);
diff --git a/mobile/android/chrome/content/aboutPrivateBrowsing.xhtml b/mobile/android/chrome/content/aboutPrivateBrowsing.xhtml
new file mode 100644
index 0000000000..7075bd11e6
--- /dev/null
+++ b/mobile/android/chrome/content/aboutPrivateBrowsing.xhtml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+ %brandDTD;
+ <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+ <!ENTITY % privatebrowsingpageDTD SYSTEM "chrome://browser/locale/aboutPrivateBrowsing.dtd">
+ %privatebrowsingpageDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>&privatebrowsingpage.title;</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1; user-scalable=no"/>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutPrivateBrowsing.css" type="text/css" media="all"/>
+ <link rel="icon" type="image/png" href="chrome://branding/content/favicon32.png" />
+ <script type="application/javascript;version=1.8" src="chrome://browser/content/aboutPrivateBrowsing.js"></script>
+ </head>
+
+ <body class="private">
+ <img class="showPrivate masq" src="chrome://browser/skin/images/privatebrowsing-mask-and-shield.svg" />
+ <img class="showNormal masq" src="chrome://browser/skin/images/privatebrowsing-mask.png" />
+
+ <h1 class="showPrivate">&privatebrowsingpage.title;<br />&privatebrowsingpage.title.private;</h1>
+ <h1 class="showNormal">&privatebrowsingpage.title.normal1;</h1>
+
+ <div class="contentSection">
+ <p class="showPrivate">&privatebrowsingpage.description.trackingProtection;<br /><br />&privatebrowsingpage.description.privateDetails;</p>
+ <p class="showNormal">&privatebrowsingpage.description.normal2;</p>
+
+ <p class="showPrivate"><a href="https://support.mozilla.org/kb/private-browsing-firefox-android">&privatebrowsingpage.link.private;</a></p>
+ <p class="showNormal"><a href="#" id="newPrivateTabLink">&privatebrowsingpage.link.normal;</a></p>
+ </div>
+
+ </body>
+</html>
diff --git a/mobile/android/chrome/content/aboutRights.xhtml b/mobile/android/chrome/content/aboutRights.xhtml
new file mode 100644
index 0000000000..8172788f4d
--- /dev/null
+++ b/mobile/android/chrome/content/aboutRights.xhtml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+ %brandDTD;
+ <!ENTITY % aboutRightsDTD SYSTEM "chrome://global/locale/aboutRights.dtd">
+ %aboutRightsDTD;
+]>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+ <title>&rights.pagetitle;</title>
+ <link rel="stylesheet" href="chrome://global/skin/about.css" type="text/css"/>
+</head>
+
+<body id="your-rights" dir="&rights.locale-direction;" class="aboutPageWideContainer">
+
+<h1>&rights.intro-header;</h1>
+
+<p>&rights.intro;</p>
+
+<ul>
+ <li>&rights.intro-point1a;<a href="https://www.mozilla.org/MPL/">&rights.intro-point1b;</a>&rights.intro-point1c;</li>
+ <!-- Point 2 discusses Mozilla trademarks, and isn't needed when the build is unbranded.
+ Point 3 discusses privacy policy, unbranded builds get a placeholder (for the vendor to replace)
+ Point 4 discusses web service terms, unbranded builds gets a placeholder (for the vendor to replace) -->
+ <li>&rights.intro-point2-a;<a href="https://www.mozilla.org/foundation/trademarks/policy.html">&rights.intro-point2-b;</a>&rights.intro-point2-c;</li>
+ <li>&rights.intro-point2.5;</li>
+ <li>&rights2.intro-point3a;<a href="https://www.mozilla.org/privacy/firefox/">&rights2.intro-point3b;</a>&rights.intro-point3c;</li>
+ <li>&rights2.intro-point4a;<a href="about:rights#webservices" onclick="showServices();">&rights.intro-point4b;</a>&rights.intro-point4c;</li>
+</ul>
+
+<div id="webservices-container">
+ <a name="webservices"/>
+ <h3>&rights2.webservices-header;</h3>
+
+ <p>&rights2.webservices-a;<a href="about:rights#disabling-webservices" onclick="showDisablingServices();">&rights2.webservices-b;</a>&rights3.webservices-c;</p>
+
+ <div id="disabling-webservices-container" style="margin-left:40px;">
+ <a name="disabling-webservices"/>
+ <!-- Safe Browsing cannot be disabled in Firefox Mobile; these instructions show how to do it on desktop
+ <p><strong>&rights.safebrowsing-a;</strong>&rights.safebrowsing-b;</p>
+ <ul>
+ <li>&rights.safebrowsing-term1;</li>
+ <li>&rights.safebrowsing-term2;</li>
+ <li>&rights2.safebrowsing-term3;</li>
+ <li>&rights.safebrowsing-term4;</li>
+ </ul>
+ -->
+
+ <p><strong>&rights.locationawarebrowsing-a;</strong>&rights.locationawarebrowsing-b;</p>
+ <ul>
+ <li>&rights.locationawarebrowsing-term1a;<code>&rights.locationawarebrowsing-term1b;</code></li>
+ <li>&rights.locationawarebrowsing-term2;</li>
+ <li>&rights.locationawarebrowsing-term3;</li>
+ <li>&rights.locationawarebrowsing-term4;</li>
+ </ul>
+ </div>
+
+ <ol>
+ <!-- Terms only apply to official builds, unbranded builds get a placeholder. -->
+ <li>&rights2.webservices-term1;</li>
+ <li>&rights.webservices-term2;</li>
+ <li>&rights2.webservices-term3;</li>
+ <li><strong>&rights.webservices-term4;</strong></li>
+ <li><strong>&rights.webservices-term5;</strong></li>
+ <li>&rights.webservices-term6;</li>
+ <li>&rights.webservices-term7;</li>
+ </ol>
+</div>
+
+<script type="application/javascript"><![CDATA[
+ var servicesDiv = document.getElementById("webservices-container");
+ servicesDiv.style.display = "none";
+
+ function showServices() {
+ servicesDiv.style.display = "";
+ }
+
+ var disablingServicesDiv = document.getElementById("disabling-webservices-container");
+ disablingServicesDiv.style.display = "none";
+
+ function showDisablingServices() {
+ disablingServicesDiv.style.display = "";
+ }
+]]></script>
+
+</body>
+</html>
diff --git a/mobile/android/chrome/content/bindings/checkbox.xml b/mobile/android/chrome/content/bindings/checkbox.xml
new file mode 100644
index 0000000000..ec5c7828b3
--- /dev/null
+++ b/mobile/android/chrome/content/bindings/checkbox.xml
@@ -0,0 +1,75 @@
+<?xml version="1.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/. -->
+
+<!DOCTYPE bindings [
+ <!ENTITY % checkboxDTD SYSTEM "chrome://browser/locale/checkbox.dtd">
+ %checkboxDTD;
+]>
+
+<bindings
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="checkbox-radio" display="xul:box" extends="chrome://global/content/bindings/checkbox.xml#checkbox-baseline">
+ <content>
+ <xul:radiogroup anonid="group" xbl:inherits="disabled">
+ <xul:radio anonid="on" class="checkbox-radio-on" label="&checkbox.yes.label;" xbl:inherits="label=onlabel"/>
+ <xul:radio anonid="off" class="checkbox-radio-off" label="&checkbox.no.label;" xbl:inherits="label=offlabel"/>
+ </xul:radiogroup>
+ </content>
+ <implementation>
+ <constructor><![CDATA[
+ this.setChecked(this.checked);
+ ]]></constructor>
+
+ <field name="_group">
+ document.getAnonymousElementByAttribute(this, "anonid", "group");
+ </field>
+
+ <field name="_on">
+ document.getAnonymousElementByAttribute(this, "anonid", "on");
+ </field>
+
+ <field name="_off">
+ document.getAnonymousElementByAttribute(this, "anonid", "off");
+ </field>
+
+ <property name="onlabel"
+ onget="return this._on.label"
+ onset="this._on.label=val"/>
+
+ <property name="offlabel"
+ onget="return this._off.label"
+ onset="this._off.label=val"/>
+
+ <method name="setChecked">
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ var change = (aValue != this.checked);
+ if (aValue) {
+ this.setAttribute("checked", "true");
+ this._group.selectedItem = this._on;
+ }
+ else {
+ this.removeAttribute("checked");
+ this._group.selectedItem = this._off;
+ }
+
+ if (change) {
+ var event = document.createEvent("Events");
+ event.initEvent("CheckboxStateChange", true, true);
+ this.dispatchEvent(event);
+ }
+ return aValue;
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+</bindings>
diff --git a/mobile/android/chrome/content/bindings/settings.xml b/mobile/android/chrome/content/bindings/settings.xml
new file mode 100644
index 0000000000..0019e9d3b2
--- /dev/null
+++ b/mobile/android/chrome/content/bindings/settings.xml
@@ -0,0 +1,66 @@
+<?xml version="1.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/. -->
+
+
+<bindings
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="setting-fulltoggle-bool" extends="chrome://mozapps/content/extensions/setting.xml#setting-bool">
+ <handlers>
+ <handler event="command" button="0" phase="capturing">
+ <![CDATA[
+ event.stopPropagation();
+ ]]>
+ </handler>
+ <handler event="click" button="0" phase="capturing">
+ <![CDATA[
+ event.stopPropagation();
+ this.input.checked = !this.input.checked;
+ this.inputChanged();
+ this.fireEvent("oncommand");
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="setting-fulltoggle-boolint" extends="chrome://mozapps/content/extensions/setting.xml#setting-boolint">
+ <handlers>
+ <handler event="command" button="0" phase="capturing">
+ <![CDATA[
+ event.stopPropagation();
+ ]]>
+ </handler>
+ <handler event="click" button="0" phase="capturing">
+ <![CDATA[
+ event.stopPropagation();
+ this.input.checked = !this.input.checked;
+ this.inputChanged();
+ this.fireEvent("oncommand");
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="setting-fulltoggle-localized-bool" extends="chrome://mozapps/content/extensions/setting.xml#setting-localized-bool">
+ <handlers>
+ <handler event="command" button="0" phase="capturing">
+ <![CDATA[
+ event.stopPropagation();
+ ]]>
+ </handler>
+ <handler event="click" button="0" phase="capturing">
+ <![CDATA[
+ event.stopPropagation();
+ this.input.checked = !this.input.checked;
+ this.inputChanged();
+ this.fireEvent("oncommand");
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+</bindings>
diff --git a/mobile/android/chrome/content/blockedSite.xhtml b/mobile/android/chrome/content/blockedSite.xhtml
new file mode 100644
index 0000000000..5f04edbefd
--- /dev/null
+++ b/mobile/android/chrome/content/blockedSite.xhtml
@@ -0,0 +1,195 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+ %brandDTD;
+ <!ENTITY % blockedSiteDTD SYSTEM "chrome://browser/locale/phishing.dtd">
+ %blockedSiteDTD;
+]>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml" class="blacklist">
+ <head>
+ <meta name="viewport" content="width=device-width; user-scalable=false" />
+ <link rel="stylesheet" href="chrome://global/skin/netError.css" type="text/css" media="all" />
+ <link rel="icon" type="image/png" id="favicon" sizes="64x64" href="chrome://browser/skin/images/blocked-warning.png"/>
+
+ <script type="application/javascript"><![CDATA[
+ // Error url MUST be formatted like this:
+ // about:blocked?e=error_code&u=url(&o=1)?
+ // (o=1 when user overrides are allowed)
+
+ // Note that this file uses document.documentURI to get
+ // the URL (with the format from above). This is because
+ // document.location.href gets the current URI off the docshell,
+ // which is the URL displayed in the location bar, i.e.
+ // the URI that the user attempted to load.
+
+ function getErrorCode()
+ {
+ var url = document.documentURI;
+ var error = url.search(/e\=/);
+ var duffUrl = url.search(/\&u\=/);
+ return decodeURIComponent(url.slice(error + 2, duffUrl));
+ }
+
+ function getURL()
+ {
+ var url = document.documentURI;
+ var match = url.match(/&u=([^&]+)&/);
+
+ // match == null if not found; if so, return an empty string
+ // instead of what would turn out to be portions of the URI
+ if (!match)
+ return "";
+
+ url = decodeURIComponent(match[1]);
+
+ // If this is a view-source page, then get then real URI of the page
+ if (/^view-source\:/.test(url))
+ url = url.slice(12);
+ return url;
+ }
+
+ /**
+ * Check whether this warning page should be overridable or whether
+ * the "ignore warning" button should be hidden.
+ */
+ function getOverride()
+ {
+ var url = document.documentURI;
+ var match = url.match(/&o=1&/);
+ return !!match;
+ }
+
+ /**
+ * Attempt to get the hostname via document.location. Fail back
+ * to getURL so that we always return something meaningful.
+ */
+ function getHostString()
+ {
+ try {
+ return document.location.hostname;
+ } catch (e) {
+ return getURL();
+ }
+ }
+
+ function initPage()
+ {
+ var error = "";
+ switch (getErrorCode()) {
+ case "malwareBlocked" :
+ error = "malware";
+ break;
+ case "deceptiveBlocked" :
+ error = "phishing";
+ break;
+ case "unwantedBlocked" :
+ error = "unwanted";
+ break;
+ default:
+ return;
+ }
+
+ var el;
+
+ if (error !== "malware") {
+ el = document.getElementById("errorTitleText_malware");
+ el.parentNode.removeChild(el);
+ el = document.getElementById("errorShortDescText_malware");
+ el.parentNode.removeChild(el);
+ el = document.getElementById("errorLongDescText_malware");
+ el.parentNode.removeChild(el);
+ }
+
+ if (error !== "phishing") {
+ el = document.getElementById("errorTitleText_phishing");
+ el.parentNode.removeChild(el);
+ el = document.getElementById("errorShortDescText_phishing");
+ el.parentNode.removeChild(el);
+ el = document.getElementById("errorLongDescText_phishing");
+ el.parentNode.removeChild(el);
+ }
+
+ if (error !== "unwanted") {
+ el = document.getElementById("errorTitleText_unwanted");
+ el.parentNode.removeChild(el);
+ el = document.getElementById("errorShortDescText_unwanted");
+ el.parentNode.removeChild(el);
+ el = document.getElementById("errorLongDescText_unwanted");
+ el.parentNode.removeChild(el);
+ }
+
+ if (!getOverride()) {
+ var btn = document.getElementById("ignoreWarningButton");
+ if (btn) {
+ btn.parentNode.removeChild(btn);
+ }
+ }
+
+ // Set sitename
+ document.getElementById(error + "_sitename").textContent = getHostString();
+ document.title = document.getElementById("errorTitleText_" + error)
+ .innerHTML;
+
+ // Inform the test harness that we're done loading the page
+ var event = new CustomEvent("AboutBlockedLoaded");
+ document.dispatchEvent(event);
+ }
+ ]]></script>
+ </head>
+
+ <body id="errorPage" class="blockedsite" dir="&locale.dir;">
+
+ <div id="errorPageContainer">
+
+ <!-- Error Title -->
+ <div id="errorTitle">
+ <h1 id="errorTitleText_phishing" class="errorTitleText">&safeb.blocked.phishingPage.title3;</h1>
+ <h1 id="errorTitleText_malware" class="errorTitleText">&safeb.blocked.malwarePage.title;</h1>
+ <h1 id="errorTitleText_unwanted" class="errorTitleText">&safeb.blocked.unwantedPage.title;</h1>
+ </div>
+
+ <div id="errorLongContent">
+
+ <!-- Short Description -->
+ <div id="errorShortDesc">
+ <p id="errorShortDescText_phishing">&safeb.blocked.phishingPage.shortDesc3;</p>
+ <p id="errorShortDescText_malware">&safeb.blocked.malwarePage.shortDesc;</p>
+ <p id="errorShortDescText_unwanted">&safeb.blocked.unwantedPage.shortDesc;</p>
+ </div>
+
+ <!-- Long Description -->
+ <div id="errorLongDesc">
+ <p id="errorLongDescText_phishing">&safeb.blocked.phishingPage.longDesc3;</p>
+ <p id="errorLongDescText_malware">&safeb.blocked.malwarePage.longDesc;</p>
+ <p id="errorLongDescText_unwanted">&safeb.blocked.unwantedPage.longDesc;</p>
+ </div>
+
+ <!-- Action buttons -->
+ <div id="buttons">
+ <!-- Commands handled in browser.js -->
+ <button id="getMeOutButton">&safeb.palm.accept.label;</button>
+ <button id="reportButton">&safeb.palm.reportPage.label;</button>
+ </div>
+ </div>
+ <div id="ignoreWarning">
+ <button id="ignoreWarningButton">&safeb.palm.decline.label;</button>
+ </div>
+ </div>
+ <!--
+ - Note: It is important to run the script this way, instead of using
+ - an onload handler. This is because error pages are loaded as
+ - LOAD_BACKGROUND, which means that onload handlers will not be executed.
+ -->
+ <script type="application/javascript">initPage();</script>
+ </body>
+</html>
diff --git a/mobile/android/chrome/content/browser.css b/mobile/android/chrome/content/browser.css
new file mode 100644
index 0000000000..fdc35d961d
--- /dev/null
+++ b/mobile/android/chrome/content/browser.css
@@ -0,0 +1,7 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+browser[remote="true"] {
+ -moz-binding: url("chrome://global/content/bindings/remote-browser.xml#remote-browser");
+} \ No newline at end of file
diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js
new file mode 100644
index 0000000000..b00e1af150
--- /dev/null
+++ b/mobile/android/chrome/content/browser.js
@@ -0,0 +1,6999 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/AsyncPrefs.jsm");
+Cu.import("resource://gre/modules/DelayedInit.jsm");
+
+if (AppConstants.ACCESSIBILITY) {
+ XPCOMUtils.defineLazyModuleGetter(this, "AccessFu",
+ "resource://gre/modules/accessibility/AccessFu.jsm");
+}
+
+XPCOMUtils.defineLazyModuleGetter(this, "SpatialNavigation",
+ "resource://gre/modules/SpatialNavigation.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadNotifications",
+ "resource://gre/modules/DownloadNotifications.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "JNI",
+ "resource://gre/modules/JNI.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
+ "resource://gre/modules/UITelemetry.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+ "resource://gre/modules/PluralForm.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging",
+ "resource://gre/modules/Messaging.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "UserAgentOverrides",
+ "resource://gre/modules/UserAgentOverrides.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
+ "resource://gre/modules/LoginManagerContent.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerParent",
+ "resource://gre/modules/LoginManagerParent.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing",
+ "resource://gre/modules/SafeBrowsing.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+ "resource://gre/modules/BrowserUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Sanitizer",
+ "resource://gre/modules/Sanitizer.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
+ "resource://gre/modules/Prompt.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "HelperApps",
+ "resource://gre/modules/HelperApps.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "SSLExceptions",
+ "resource://gre/modules/SSLExceptions.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
+ "resource://gre/modules/FormHistory.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+
+XPCOMUtils.defineLazyServiceGetter(this, "Profiler",
+ "@mozilla.org/tools/profiler;1",
+ "nsIProfiler");
+
+XPCOMUtils.defineLazyModuleGetter(this, "SimpleServiceDiscovery",
+ "resource://gre/modules/SimpleServiceDiscovery.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu",
+ "resource://gre/modules/CharsetMenu.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetErrorHelper",
+ "resource://gre/modules/NetErrorHelper.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils",
+ "resource://gre/modules/PermissionsUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+ "resource://gre/modules/Preferences.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "SharedPreferences",
+ "resource://gre/modules/SharedPreferences.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Notifications",
+ "resource://gre/modules/Notifications.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "RuntimePermissions", "resource://gre/modules/RuntimePermissions.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "WebsiteMetadata", "resource://gre/modules/WebsiteMetadata.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "FontEnumerator",
+ "@mozilla.org/gfx/fontenumerator;1",
+ "nsIFontEnumerator");
+
+var lazilyLoadedBrowserScripts = [
+ ["SelectHelper", "chrome://browser/content/SelectHelper.js"],
+ ["InputWidgetHelper", "chrome://browser/content/InputWidgetHelper.js"],
+ ["MasterPassword", "chrome://browser/content/MasterPassword.js"],
+ ["PluginHelper", "chrome://browser/content/PluginHelper.js"],
+ ["OfflineApps", "chrome://browser/content/OfflineApps.js"],
+ ["Linkifier", "chrome://browser/content/Linkify.js"],
+ ["CastingApps", "chrome://browser/content/CastingApps.js"],
+ ["RemoteDebugger", "chrome://browser/content/RemoteDebugger.js"],
+];
+if (!AppConstants.RELEASE_OR_BETA) {
+ lazilyLoadedBrowserScripts.push(
+ ["WebcompatReporter", "chrome://browser/content/WebcompatReporter.js"]);
+}
+
+lazilyLoadedBrowserScripts.forEach(function (aScript) {
+ let [name, script] = aScript;
+ XPCOMUtils.defineLazyGetter(window, name, function() {
+ let sandbox = {};
+ Services.scriptloader.loadSubScript(script, sandbox);
+ return sandbox[name];
+ });
+});
+
+var lazilyLoadedObserverScripts = [
+ ["MemoryObserver", ["memory-pressure", "Memory:Dump"], "chrome://browser/content/MemoryObserver.js"],
+ ["ConsoleAPI", ["console-api-log-event"], "chrome://browser/content/ConsoleAPI.js"],
+ ["FindHelper", ["FindInPage:Opened", "FindInPage:Closed", "Tab:Selected"], "chrome://browser/content/FindHelper.js"],
+ ["PermissionsHelper", ["Permissions:Check", "Permissions:Get", "Permissions:Clear"], "chrome://browser/content/PermissionsHelper.js"],
+ ["FeedHandler", ["Feeds:Subscribe"], "chrome://browser/content/FeedHandler.js"],
+ ["Feedback", ["Feedback:Show"], "chrome://browser/content/Feedback.js"],
+ ["EmbedRT", ["GeckoView:ImportScript"], "chrome://browser/content/EmbedRT.js"],
+ ["Reader", ["Reader:AddToCache", "Reader:RemoveFromCache"], "chrome://browser/content/Reader.js"],
+ ["PrintHelper", ["Print:PDF"], "chrome://browser/content/PrintHelper.js"],
+];
+
+lazilyLoadedObserverScripts.push(
+["ActionBarHandler", ["TextSelection:Get", "TextSelection:Action", "TextSelection:End"],
+ "chrome://browser/content/ActionBarHandler.js"]
+);
+
+if (AppConstants.MOZ_WEBRTC) {
+ lazilyLoadedObserverScripts.push(
+ ["WebrtcUI", ["getUserMedia:request",
+ "PeerConnection:request",
+ "recording-device-events",
+ "VideoCapture:Paused",
+ "VideoCapture:Resumed"], "chrome://browser/content/WebrtcUI.js"])
+}
+
+lazilyLoadedObserverScripts.forEach(function (aScript) {
+ let [name, notifications, script] = aScript;
+ XPCOMUtils.defineLazyGetter(window, name, function() {
+ let sandbox = {};
+ Services.scriptloader.loadSubScript(script, sandbox);
+ return sandbox[name];
+ });
+ let observer = (s, t, d) => {
+ Services.obs.removeObserver(observer, t);
+ Services.obs.addObserver(window[name], t, false);
+ window[name].observe(s, t, d); // Explicitly notify new observer
+ };
+ notifications.forEach((notification) => {
+ Services.obs.addObserver(observer, notification, false);
+ });
+});
+
+// Lazily-loaded browser scripts that use message listeners.
+[
+ ["Reader", [
+ ["Reader:AddToCache", false],
+ ["Reader:RemoveFromCache", false],
+ ["Reader:ArticleGet", false],
+ ["Reader:DropdownClosed", true], // 'true' allows us to survive mid-air cycle-collection.
+ ["Reader:DropdownOpened", false],
+ ["Reader:FaviconRequest", false],
+ ["Reader:ToolbarHidden", false],
+ ["Reader:SystemUIVisibility", false],
+ ["Reader:UpdateReaderButton", false],
+ ], "chrome://browser/content/Reader.js"],
+].forEach(aScript => {
+ let [name, messages, script] = aScript;
+ XPCOMUtils.defineLazyGetter(window, name, function() {
+ let sandbox = {};
+ Services.scriptloader.loadSubScript(script, sandbox);
+ return sandbox[name];
+ });
+
+ let mm = window.getGroupMessageManager("browsers");
+ let listener = (message) => {
+ mm.removeMessageListener(message.name, listener);
+ let listenAfterClose = false;
+ for (let [name, laClose] of messages) {
+ if (message.name === name) {
+ listenAfterClose = laClose;
+ break;
+ }
+ }
+
+ mm.addMessageListener(message.name, window[name], listenAfterClose);
+ window[name].receiveMessage(message);
+ };
+
+ messages.forEach((message) => {
+ let [name, listenAfterClose] = message;
+ mm.addMessageListener(name, listener, listenAfterClose);
+ });
+});
+
+// Lazily-loaded JS modules that use observer notifications
+[
+ ["Home", ["HomeBanner:Get", "HomePanels:Get", "HomePanels:Authenticate", "HomePanels:RefreshView",
+ "HomePanels:Installed", "HomePanels:Uninstalled"], "resource://gre/modules/Home.jsm"],
+].forEach(module => {
+ let [name, notifications, resource] = module;
+ XPCOMUtils.defineLazyModuleGetter(this, name, resource);
+ let observer = (s, t, d) => {
+ Services.obs.removeObserver(observer, t);
+ Services.obs.addObserver(this[name], t, false);
+ this[name].observe(s, t, d); // Explicitly notify new observer
+ };
+ notifications.forEach(notification => {
+ Services.obs.addObserver(observer, notification, false);
+ });
+});
+
+XPCOMUtils.defineLazyServiceGetter(this, "Haptic",
+ "@mozilla.org/widget/hapticfeedback;1", "nsIHapticFeedback");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls",
+ "@mozilla.org/parental-controls-service;1", "nsIParentalControlsService");
+
+XPCOMUtils.defineLazyServiceGetter(this, "DOMUtils",
+ "@mozilla.org/inspector/dom-utils;1", "inIDOMUtils");
+
+XPCOMUtils.defineLazyServiceGetter(window, "URIFixup",
+ "@mozilla.org/docshell/urifixup;1", "nsIURIFixup");
+
+if (AppConstants.MOZ_WEBRTC) {
+ XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
+ "@mozilla.org/mediaManagerService;1", "nsIMediaManagerService");
+}
+
+XPCOMUtils.defineLazyModuleGetter(this, "Log",
+ "resource://gre/modules/AndroidLog.jsm", "AndroidLog");
+
+// Define the "dump" function as a binding of the Log.d function so it specifies
+// the "debug" priority and a log tag.
+function dump(msg) {
+ Log.d("Browser", msg);
+}
+
+const kStateActive = 0x00000001; // :active pseudoclass for elements
+
+const kXLinkNamespace = "http://www.w3.org/1999/xlink";
+
+function fuzzyEquals(a, b) {
+ return (Math.abs(a - b) < 1e-6);
+}
+
+XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() {
+ let ContentAreaUtils = {};
+ Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils);
+ return ContentAreaUtils;
+});
+
+XPCOMUtils.defineLazyModuleGetter(this, "Rect", "resource://gre/modules/Geometry.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Point", "resource://gre/modules/Geometry.jsm");
+
+function resolveGeckoURI(aURI) {
+ if (!aURI)
+ throw "Can't resolve an empty uri";
+
+ if (aURI.startsWith("chrome://")) {
+ let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]);
+ return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec;
+ } else if (aURI.startsWith("resource://")) {
+ let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
+ return handler.resolveURI(Services.io.newURI(aURI, null, null));
+ }
+ return aURI;
+}
+
+/**
+ * Cache of commonly used string bundles.
+ */
+var Strings = {
+ init: function () {
+ XPCOMUtils.defineLazyGetter(Strings, "brand", () => Services.strings.createBundle("chrome://branding/locale/brand.properties"));
+ XPCOMUtils.defineLazyGetter(Strings, "browser", () => Services.strings.createBundle("chrome://browser/locale/browser.properties"));
+ XPCOMUtils.defineLazyGetter(Strings, "reader", () => Services.strings.createBundle("chrome://global/locale/aboutReader.properties"));
+ },
+
+ flush: function () {
+ Services.strings.flushBundles();
+ this.init();
+ },
+};
+
+Strings.init();
+
+const kFormHelperModeDisabled = 0;
+const kFormHelperModeEnabled = 1;
+const kFormHelperModeDynamic = 2; // disabled on tablets
+const kMaxHistoryListSize = 50;
+
+function InitLater(fn, object, name) {
+ return DelayedInit.schedule(fn, object, name, 15000 /* 15s max wait */);
+}
+
+var BrowserApp = {
+ _tabs: [],
+ _selectedTab: null,
+
+ get isTablet() {
+ let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
+ delete this.isTablet;
+ return this.isTablet = sysInfo.get("tablet");
+ },
+
+ get isOnLowMemoryPlatform() {
+ let memory = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory);
+ delete this.isOnLowMemoryPlatform;
+ return this.isOnLowMemoryPlatform = memory.isLowMemoryPlatform();
+ },
+
+ deck: null,
+
+ startup: function startup() {
+ window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess();
+ dump("zerdatime " + Date.now() + " - browser chrome startup finished.");
+ Services.obs.notifyObservers(this.browser, "BrowserChrome:Ready", null);
+
+ this.deck = document.getElementById("browsers");
+
+ BrowserEventHandler.init();
+
+ ViewportHandler.init();
+
+ Services.androidBridge.browserApp = this;
+
+ Services.obs.addObserver(this, "Locale:OS", false);
+ Services.obs.addObserver(this, "Locale:Changed", false);
+ Services.obs.addObserver(this, "Tab:Load", false);
+ Services.obs.addObserver(this, "Tab:Selected", false);
+ Services.obs.addObserver(this, "Tab:Closed", false);
+ Services.obs.addObserver(this, "Session:Back", false);
+ Services.obs.addObserver(this, "Session:Forward", false);
+ Services.obs.addObserver(this, "Session:Navigate", false);
+ Services.obs.addObserver(this, "Session:Reload", false);
+ Services.obs.addObserver(this, "Session:Stop", false);
+ Services.obs.addObserver(this, "SaveAs:PDF", false);
+ Services.obs.addObserver(this, "Browser:Quit", false);
+ Services.obs.addObserver(this, "ScrollTo:FocusedInput", false);
+ Services.obs.addObserver(this, "Sanitize:ClearData", false);
+ Services.obs.addObserver(this, "FullScreen:Exit", false);
+ Services.obs.addObserver(this, "Passwords:Init", false);
+ Services.obs.addObserver(this, "FormHistory:Init", false);
+ Services.obs.addObserver(this, "android-get-pref", false);
+ Services.obs.addObserver(this, "android-set-pref", false);
+ Services.obs.addObserver(this, "gather-telemetry", false);
+ Services.obs.addObserver(this, "keyword-search", false);
+ Services.obs.addObserver(this, "sessionstore-state-purge-complete", false);
+ Services.obs.addObserver(this, "Fonts:Reload", false);
+ Services.obs.addObserver(this, "Vibration:Request", false);
+
+ Messaging.addListener(this.getHistory.bind(this), "Session:GetHistory");
+
+ window.addEventListener("fullscreen", function() {
+ Messaging.sendRequest({
+ type: window.fullScreen ? "ToggleChrome:Hide" : "ToggleChrome:Show"
+ });
+ }, false);
+
+ window.addEventListener("fullscreenchange", (e) => {
+ // This event gets fired on the document and its entire ancestor chain
+ // of documents. When enabling fullscreen, it is fired on the top-level
+ // document first and goes down; when disabling the order is reversed
+ // (per spec). This means the last event on enabling will be for the innermost
+ // document, which will have fullscreenElement set correctly.
+ let doc = e.target;
+ Messaging.sendRequest({
+ type: doc.fullscreenElement ? "DOMFullScreen:Start" : "DOMFullScreen:Stop",
+ rootElement: doc.fullscreenElement == doc.documentElement
+ });
+
+ if (this.fullscreenTransitionTab) {
+ // Tab selection has changed during a fullscreen transition, handle it now.
+ let tab = this.fullscreenTransitionTab;
+ this.fullscreenTransitionTab = null;
+ this._handleTabSelected(tab);
+ }
+ }, false);
+
+ NativeWindow.init();
+ FormAssistant.init();
+ IndexedDB.init();
+ XPInstallObserver.init();
+ CharacterEncoding.init();
+ ActivityObserver.init();
+ RemoteDebugger.init();
+ UserAgentOverrides.init();
+ DesktopUserAgent.init();
+ Distribution.init();
+ Tabs.init();
+ SearchEngines.init();
+ Experiments.init();
+
+ // XXX maybe we don't do this if the launch was kicked off from external
+ Services.io.offline = false;
+
+ // Broadcast a UIReady message so add-ons know we are finished with startup
+ let event = document.createEvent("Events");
+ event.initEvent("UIReady", true, false);
+ window.dispatchEvent(event);
+
+ if (this._startupStatus) {
+ this.onAppUpdated();
+ }
+
+ if (!ParentalControls.isAllowed(ParentalControls.INSTALL_EXTENSION)) {
+ // Disable extension installs
+ Services.prefs.setIntPref("extensions.enabledScopes", 1);
+ Services.prefs.setIntPref("extensions.autoDisableScopes", 1);
+ Services.prefs.setBoolPref("xpinstall.enabled", false);
+ } else if (ParentalControls.parentalControlsEnabled) {
+ Services.prefs.clearUserPref("extensions.enabledScopes");
+ Services.prefs.clearUserPref("extensions.autoDisableScopes");
+ Services.prefs.setBoolPref("xpinstall.enabled", true);
+ }
+
+ if (ParentalControls.parentalControlsEnabled) {
+ let isBlockListEnabled = ParentalControls.isAllowed(ParentalControls.BLOCK_LIST);
+ Services.prefs.setBoolPref("browser.safebrowsing.forbiddenURIs.enabled", isBlockListEnabled);
+ Services.prefs.setBoolPref("browser.safebrowsing.allowOverride", !isBlockListEnabled);
+
+ let isTelemetryEnabled = ParentalControls.isAllowed(ParentalControls.TELEMETRY);
+ Services.prefs.setBoolPref("toolkit.telemetry.enabled", isTelemetryEnabled);
+
+ let isHealthReportEnabled = ParentalControls.isAllowed(ParentalControls.HEALTH_REPORT);
+ SharedPreferences.forApp().setBoolPref("android.not_a_preference.healthreport.uploadEnabled", isHealthReportEnabled);
+ }
+
+ let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
+ if (sysInfo.get("version") < 16) {
+ let defaults = Services.prefs.getDefaultBranch(null);
+ defaults.setBoolPref("media.autoplay.enabled", false);
+ }
+
+ InitLater(() => {
+ // The order that context menu items are added is important
+ // Make sure the "Open in App" context menu item appears at the bottom of the list
+ this.initContextMenu();
+ ExternalApps.init();
+ }, NativeWindow, "contextmenus");
+
+ if (AppConstants.ACCESSIBILITY) {
+ InitLater(() => AccessFu.attach(window), window, "AccessFu");
+ }
+
+ // Don't delay loading content.js because when we restore reader mode tabs,
+ // we require the reader mode scripts in content.js right away.
+ let mm = window.getGroupMessageManager("browsers");
+ mm.loadFrameScript("chrome://browser/content/content.js", true);
+
+ // We can't delay registering WebChannel listeners: if the first page is
+ // about:accounts, which can happen when starting the Firefox Account flow
+ // from the first run experience, or via the Firefox Account Status
+ // Activity, we can and do miss messages from the fxa-content-server.
+ // However, we never allow suitably restricted profiles from listening to
+ // fxa-content-server messages.
+ if (ParentalControls.isAllowed(ParentalControls.MODIFY_ACCOUNTS)) {
+ console.log("browser.js: loading Firefox Accounts WebChannel");
+ Cu.import("resource://gre/modules/FxAccountsWebChannel.jsm");
+ EnsureFxAccountsWebChannel();
+ } else {
+ console.log("browser.js: not loading Firefox Accounts WebChannel; this profile cannot connect to Firefox Accounts.");
+ }
+
+ // Notify Java that Gecko has loaded.
+ Messaging.sendRequest({ type: "Gecko:Ready" });
+
+ this.deck.addEventListener("DOMContentLoaded", function BrowserApp_delayedStartup() {
+ BrowserApp.deck.removeEventListener("DOMContentLoaded", BrowserApp_delayedStartup, false);
+
+ InitLater(() => Cu.import("resource://gre/modules/NotificationDB.jsm"));
+ InitLater(() => Cu.import("resource://gre/modules/PresentationDeviceInfoManager.jsm"));
+
+ InitLater(() => Services.obs.notifyObservers(window, "browser-delayed-startup-finished", ""));
+ InitLater(() => Messaging.sendRequest({ type: "Gecko:DelayedStartup" }));
+
+ if (!AppConstants.RELEASE_OR_BETA) {
+ InitLater(() => WebcompatReporter.init());
+ }
+
+ // Collect telemetry data.
+ // We do this at startup because we want to move away from "gather-telemetry" (bug 1127907)
+ InitLater(() => {
+ Telemetry.addData("FENNEC_TRACKING_PROTECTION_STATE", parseInt(BrowserApp.getTrackingProtectionState()));
+ Telemetry.addData("ZOOMED_VIEW_ENABLED", Services.prefs.getBoolPref("ui.zoomedview.enabled"));
+ });
+
+ InitLater(() => LightWeightThemeWebInstaller.init());
+ InitLater(() => SpatialNavigation.init(BrowserApp.deck, null), window, "SpatialNavigation");
+ InitLater(() => CastingApps.init(), window, "CastingApps");
+ InitLater(() => Services.search.init(), Services, "search");
+ InitLater(() => DownloadNotifications.init(), window, "DownloadNotifications");
+
+ // Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008.
+ InitLater(() => SafeBrowsing.init(), window, "SafeBrowsing");
+
+ InitLater(() => Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager));
+ InitLater(() => LoginManagerParent.init(), window, "LoginManagerParent");
+
+ }, false);
+
+ // Pass caret StateChanged events to ActionBarHandler.
+ window.addEventListener("mozcaretstatechanged", e => {
+ ActionBarHandler.caretStateChangedHandler(e);
+ }, /* useCapture = */ true, /* wantsUntrusted = */ false);
+ },
+
+ get _startupStatus() {
+ delete this._startupStatus;
+
+ let savedMilestone = null;
+ try {
+ savedMilestone = Services.prefs.getCharPref("browser.startup.homepage_override.mstone");
+ } catch (e) {
+ }
+ let ourMilestone = AppConstants.MOZ_APP_VERSION;
+ this._startupStatus = "";
+ if (ourMilestone != savedMilestone) {
+ Services.prefs.setCharPref("browser.startup.homepage_override.mstone", ourMilestone);
+ this._startupStatus = savedMilestone ? "upgrade" : "new";
+ }
+
+ return this._startupStatus;
+ },
+
+ /**
+ * Pass this a locale string, such as "fr" or "es_ES".
+ */
+ setLocale: function (locale) {
+ console.log("browser.js: requesting locale set: " + locale);
+ Messaging.sendRequest({ type: "Locale:Set", locale: locale });
+ },
+
+ initContextMenu: function () {
+ // We pass a thunk in place of a raw label string. This allows the
+ // context menu to automatically accommodate locale changes without
+ // having to be rebuilt.
+ let stringGetter = name => () => Strings.browser.GetStringFromName(name);
+
+ // TODO: These should eventually move into more appropriate classes
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.openInNewTab"),
+ NativeWindow.contextmenus.linkOpenableNonPrivateContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_open_new_tab");
+ UITelemetry.addEvent("loadurl.1", "contextmenu", null);
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal);
+ let tab = BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id });
+
+ let newtabStrings = Strings.browser.GetStringFromName("newtabpopup.opened");
+ let label = PluralForm.get(1, newtabStrings).replace("#1", 1);
+ let buttonLabel = Strings.browser.GetStringFromName("newtabpopup.switch");
+
+ Snackbars.show(label, Snackbars.LENGTH_LONG, {
+ action: {
+ label: buttonLabel,
+ callback: () => { BrowserApp.selectTab(tab); },
+ }
+ });
+ });
+
+ let showOpenInPrivateTab = true;
+ if ("@mozilla.org/parental-controls-service;1" in Cc) {
+ let pc = Cc["@mozilla.org/parental-controls-service;1"].createInstance(Ci.nsIParentalControlsService);
+ showOpenInPrivateTab = pc.isAllowed(Ci.nsIParentalControlsService.PRIVATE_BROWSING);
+ }
+
+ if (showOpenInPrivateTab) {
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.openInPrivateTab"),
+ NativeWindow.contextmenus.linkOpenableContext,
+ function (aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_open_new_tab");
+ UITelemetry.addEvent("loadurl.1", "contextmenu", null);
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal);
+ let tab = BrowserApp.addTab(url, {selected: false, parentId: BrowserApp.selectedTab.id, isPrivate: true});
+
+ let newtabStrings = Strings.browser.GetStringFromName("newprivatetabpopup.opened");
+ let label = PluralForm.get(1, newtabStrings).replace("#1", 1);
+ let buttonLabel = Strings.browser.GetStringFromName("newtabpopup.switch");
+ Snackbars.show(label, Snackbars.LENGTH_LONG, {
+ action: {
+ label: buttonLabel,
+ callback: () => { BrowserApp.selectTab(tab); },
+ }
+ });
+ });
+ }
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.copyLink"),
+ NativeWindow.contextmenus.linkCopyableContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_link");
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ NativeWindow.contextmenus._copyStringToDefaultClipboard(url);
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.copyEmailAddress"),
+ NativeWindow.contextmenus.emailLinkContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_email");
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ let emailAddr = NativeWindow.contextmenus._stripScheme(url);
+ NativeWindow.contextmenus._copyStringToDefaultClipboard(emailAddr);
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.copyPhoneNumber"),
+ NativeWindow.contextmenus.phoneNumberLinkContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_phone");
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ let phoneNumber = NativeWindow.contextmenus._stripScheme(url);
+ NativeWindow.contextmenus._copyStringToDefaultClipboard(phoneNumber);
+ });
+
+ NativeWindow.contextmenus.add({
+ label: stringGetter("contextmenu.shareLink"),
+ order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items
+ selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.linkShareableContext),
+ showAsActions: function(aElement) {
+ return {
+ title: aElement.textContent.trim() || aElement.title.trim(),
+ uri: NativeWindow.contextmenus._getLinkURL(aElement),
+ };
+ },
+ icon: "drawable://ic_menu_share",
+ callback: function(aTarget) {
+ // share.1 telemetry is handled in Java via PromptList
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_link");
+ }
+ });
+
+ NativeWindow.contextmenus.add({
+ label: stringGetter("contextmenu.shareEmailAddress"),
+ order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
+ selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.emailLinkContext),
+ showAsActions: function(aElement) {
+ let url = NativeWindow.contextmenus._getLinkURL(aElement);
+ let emailAddr = NativeWindow.contextmenus._stripScheme(url);
+ let title = aElement.textContent || aElement.title;
+ return {
+ title: title,
+ uri: emailAddr,
+ };
+ },
+ icon: "drawable://ic_menu_share",
+ callback: function(aTarget) {
+ // share.1 telemetry is handled in Java via PromptList
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_email");
+ }
+ });
+
+ NativeWindow.contextmenus.add({
+ label: stringGetter("contextmenu.sharePhoneNumber"),
+ order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
+ selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.phoneNumberLinkContext),
+ showAsActions: function(aElement) {
+ let url = NativeWindow.contextmenus._getLinkURL(aElement);
+ let phoneNumber = NativeWindow.contextmenus._stripScheme(url);
+ let title = aElement.textContent || aElement.title;
+ return {
+ title: title,
+ uri: phoneNumber,
+ };
+ },
+ icon: "drawable://ic_menu_share",
+ callback: function(aTarget) {
+ // share.1 telemetry is handled in Java via PromptList
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_phone");
+ }
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.addToContacts"),
+ NativeWindow.contextmenus._disableRestricted("ADD_CONTACT", NativeWindow.contextmenus.emailLinkContext),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_email");
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ Messaging.sendRequest({
+ type: "Contact:Add",
+ email: url
+ });
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.addToContacts"),
+ NativeWindow.contextmenus._disableRestricted("ADD_CONTACT", NativeWindow.contextmenus.phoneNumberLinkContext),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_phone");
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ Messaging.sendRequest({
+ type: "Contact:Add",
+ phone: url
+ });
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.bookmarkLink"),
+ NativeWindow.contextmenus._disableRestricted("BOOKMARK", NativeWindow.contextmenus.linkBookmarkableContext),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_bookmark");
+ UITelemetry.addEvent("save.1", "contextmenu", null, "bookmark");
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ let title = aTarget.textContent || aTarget.title || url;
+ Messaging.sendRequest({
+ type: "Bookmark:Insert",
+ url: url,
+ title: title
+ });
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.playMedia"),
+ NativeWindow.contextmenus.mediaContext("media-paused"),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_play");
+ aTarget.play();
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.pauseMedia"),
+ NativeWindow.contextmenus.mediaContext("media-playing"),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_pause");
+ aTarget.pause();
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.showControls2"),
+ NativeWindow.contextmenus.mediaContext("media-hidingcontrols"),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_controls_media");
+ aTarget.setAttribute("controls", true);
+ });
+
+ NativeWindow.contextmenus.add({
+ label: stringGetter("contextmenu.shareMedia"),
+ order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
+ selector: NativeWindow.contextmenus._disableRestricted(
+ "SHARE", NativeWindow.contextmenus.videoContext()),
+ showAsActions: function(aElement) {
+ let url = (aElement.currentSrc || aElement.src);
+ let title = aElement.textContent || aElement.title;
+ return {
+ title: title,
+ uri: url,
+ type: "video/*",
+ };
+ },
+ icon: "drawable://ic_menu_share",
+ callback: function(aTarget) {
+ // share.1 telemetry is handled in Java via PromptList
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_media");
+ }
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.fullScreen"),
+ NativeWindow.contextmenus.videoContext("not-fullscreen"),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_fullscreen");
+ aTarget.requestFullscreen();
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.mute"),
+ NativeWindow.contextmenus.mediaContext("media-unmuted"),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_mute");
+ aTarget.muted = true;
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.unmute"),
+ NativeWindow.contextmenus.mediaContext("media-muted"),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_unmute");
+ aTarget.muted = false;
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.viewImage"),
+ NativeWindow.contextmenus.imageLocationCopyableContext,
+ function(aTarget) {
+ let url = aTarget.src;
+ ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_view_image");
+ UITelemetry.addEvent("loadurl.1", "contextmenu", null);
+ BrowserApp.selectedBrowser.loadURI(url);
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.copyImageLocation"),
+ NativeWindow.contextmenus.imageLocationCopyableContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_image");
+
+ let url = aTarget.src;
+ NativeWindow.contextmenus._copyStringToDefaultClipboard(url);
+ });
+
+ NativeWindow.contextmenus.add({
+ label: stringGetter("contextmenu.shareImage"),
+ selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.imageShareableContext),
+ order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items
+ showAsActions: function(aTarget) {
+ let doc = aTarget.ownerDocument;
+ let imageCache = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools)
+ .getImgCacheForDocument(doc);
+ let props = imageCache.findEntryProperties(aTarget.currentURI, doc);
+ let src = aTarget.src;
+ return {
+ title: src,
+ uri: src,
+ type: "image/*",
+ };
+ },
+ icon: "drawable://ic_menu_share",
+ menu: true,
+ callback: function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_image");
+ }
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.saveImage"),
+ NativeWindow.contextmenus.imageSaveableContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_image");
+ UITelemetry.addEvent("save.1", "contextmenu", null, "image");
+
+ RuntimePermissions.waitForPermissions(RuntimePermissions.WRITE_EXTERNAL_STORAGE).then(function(permissionGranted) {
+ if (!permissionGranted) {
+ return;
+ }
+
+ ContentAreaUtils.saveImageURL(aTarget.currentURI.spec, null, "SaveImageTitle",
+ false, true, aTarget.ownerDocument.documentURIObject,
+ aTarget.ownerDocument);
+ });
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.setImageAs"),
+ NativeWindow.contextmenus._disableRestricted("SET_IMAGE", NativeWindow.contextmenus.imageSaveableContext),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_background_image");
+
+ let src = aTarget.src;
+ Messaging.sendRequest({
+ type: "Image:SetAs",
+ url: src
+ });
+ });
+
+ NativeWindow.contextmenus.add(
+ function(aTarget) {
+ if (aTarget instanceof HTMLVideoElement) {
+ // If a video element is zero width or height, its essentially
+ // an HTMLAudioElement.
+ if (aTarget.videoWidth == 0 || aTarget.videoHeight == 0 )
+ return Strings.browser.GetStringFromName("contextmenu.saveAudio");
+ return Strings.browser.GetStringFromName("contextmenu.saveVideo");
+ } else if (aTarget instanceof HTMLAudioElement) {
+ return Strings.browser.GetStringFromName("contextmenu.saveAudio");
+ }
+ return Strings.browser.GetStringFromName("contextmenu.saveVideo");
+ }, NativeWindow.contextmenus.mediaSaveableContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_media");
+ UITelemetry.addEvent("save.1", "contextmenu", null, "media");
+
+ let url = aTarget.currentSrc || aTarget.src;
+ let filePickerTitleKey = (aTarget instanceof HTMLVideoElement &&
+ (aTarget.videoWidth != 0 && aTarget.videoHeight != 0))
+ ? "SaveVideoTitle" : "SaveAudioTitle";
+ // Skipped trying to pull MIME type out of cache for now
+ ContentAreaUtils.internalSave(url, null, null, null, null, false,
+ filePickerTitleKey, null, aTarget.ownerDocument.documentURIObject,
+ aTarget.ownerDocument, true, null);
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.showImage"),
+ NativeWindow.contextmenus.imageBlockingPolicyContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_show_image");
+ aTarget.setAttribute("data-ctv-show", "true");
+ aTarget.setAttribute("src", aTarget.getAttribute("data-ctv-src"));
+
+ // Shows a snackbar to unblock all images if browser.image_blocking.enabled is enabled.
+ let blockedImgs = aTarget.ownerDocument.querySelectorAll("[data-ctv-src]");
+ if (blockedImgs.length == 0) {
+ return;
+ }
+ let message = Strings.browser.GetStringFromName("imageblocking.downloadedImage");
+ Snackbars.show(message, Snackbars.LENGTH_LONG, {
+ action: {
+ label: Strings.browser.GetStringFromName("imageblocking.showAllImages"),
+ callback: () => {
+ UITelemetry.addEvent("action.1", "toast", null, "web_show_all_image");
+ for (let i = 0; i < blockedImgs.length; ++i) {
+ blockedImgs[i].setAttribute("data-ctv-show", "true");
+ blockedImgs[i].setAttribute("src", blockedImgs[i].getAttribute("data-ctv-src"));
+ }
+ },
+ }
+ });
+ });
+ },
+
+ onAppUpdated: function() {
+ // initialize the form history and passwords databases on upgrades
+ Services.obs.notifyObservers(null, "FormHistory:Init", "");
+ Services.obs.notifyObservers(null, "Passwords:Init", "");
+
+ if (this._startupStatus === "upgrade") {
+ this._migrateUI();
+ }
+ },
+
+ _migrateUI: function() {
+ const UI_VERSION = 3;
+ let currentUIVersion = 0;
+ try {
+ currentUIVersion = Services.prefs.getIntPref("browser.migration.version");
+ } catch(ex) {}
+ if (currentUIVersion >= UI_VERSION) {
+ return;
+ }
+
+ if (currentUIVersion < 1) {
+ // Migrate user-set "plugins.click_to_play" pref. See bug 884694.
+ // Because the default value is true, a user-set pref means that the pref was set to false.
+ if (Services.prefs.prefHasUserValue("plugins.click_to_play")) {
+ Services.prefs.setIntPref("plugin.default.state", Ci.nsIPluginTag.STATE_ENABLED);
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ }
+
+ // Migrate the "privacy.donottrackheader.value" pref. See bug 1042135.
+ if (Services.prefs.prefHasUserValue("privacy.donottrackheader.value")) {
+ // Make sure the doNotTrack value conforms to the conversion from
+ // three-state to two-state. (This reverts a setting of "please track me"
+ // to the default "don't say anything").
+ if (Services.prefs.getBoolPref("privacy.donottrackheader.enabled") &&
+ (Services.prefs.getIntPref("privacy.donottrackheader.value") != 1)) {
+ Services.prefs.clearUserPref("privacy.donottrackheader.enabled");
+ }
+
+ // This pref has been removed, so always clear it.
+ Services.prefs.clearUserPref("privacy.donottrackheader.value");
+ }
+
+ // Set the search activity default pref on app upgrade if it has not been set already.
+ if (!Services.prefs.prefHasUserValue("searchActivity.default.migrated")) {
+ Services.prefs.setBoolPref("searchActivity.default.migrated", true);
+ SearchEngines.migrateSearchActivityDefaultPref();
+ }
+
+ Reader.migrateCache().catch(e => Cu.reportError("Error migrating Reader cache: " + e));
+
+ // We removed this pref from user visible settings, so we should reset it.
+ // Power users can go into about:config to re-enable this if they choose.
+ if (Services.prefs.prefHasUserValue("nglayout.debug.paint_flashing")) {
+ Services.prefs.clearUserPref("nglayout.debug.paint_flashing");
+ }
+ }
+
+ if (currentUIVersion < 2) {
+ let name;
+ if (Services.prefs.prefHasUserValue("browser.search.defaultenginename")) {
+ name = Services.prefs.getCharPref("browser.search.defaultenginename");
+ }
+ if (!name && Services.prefs.prefHasUserValue("browser.search.defaultenginename.US")) {
+ name = Services.prefs.getCharPref("browser.search.defaultenginename.US");
+ }
+ if (name) {
+ Services.search.init(() => {
+ let engine = Services.search.getEngineByName(name);
+ if (engine) {
+ Services.search.defaultEngine = engine;
+ Services.obs.notifyObservers(null, "default-search-engine-migrated", "");
+ }
+ });
+ }
+ }
+
+ if (currentUIVersion < 3) {
+ const kOldSafeBrowsingPref = "browser.safebrowsing.enabled";
+ // Default value is set to true, a user pref means that the pref was
+ // set to false.
+ if (Services.prefs.prefHasUserValue(kOldSafeBrowsingPref) &&
+ !Services.prefs.getBoolPref(kOldSafeBrowsingPref)) {
+ Services.prefs.setBoolPref("browser.safebrowsing.phishing.enabled",
+ false);
+ // Should just remove support for the pref entirely, even if it's
+ // only in about:config
+ Services.prefs.clearUserPref(kOldSafeBrowsingPref);
+ }
+ }
+
+ // Update the migration version.
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
+ },
+
+ // This function returns false during periods where the browser displayed document is
+ // different from the browser content document, so user actions and some kinds of viewport
+ // updates should be ignored. This period starts when we start loading a new page or
+ // switch tabs, and ends when the new browser content document has been drawn and handed
+ // off to the compositor.
+ isBrowserContentDocumentDisplayed: function() {
+ try {
+ if (!Services.androidBridge.isContentDocumentDisplayed(window))
+ return false;
+ } catch (e) {
+ return false;
+ }
+
+ let tab = this.selectedTab;
+ if (!tab)
+ return false;
+ return tab.contentDocumentIsDisplayed;
+ },
+
+ contentDocumentChanged: function() {
+ window.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).isFirstPaint = true;
+ Services.androidBridge.contentDocumentChanged(window);
+ },
+
+ get tabs() {
+ return this._tabs;
+ },
+
+ set selectedTab(aTab) {
+ if (this._selectedTab == aTab)
+ return;
+
+ if (this._selectedTab) {
+ this._selectedTab.setActive(false);
+ }
+
+ this._selectedTab = aTab;
+ if (!aTab)
+ return;
+
+ aTab.setActive(true);
+ this.contentDocumentChanged();
+ this.deck.selectedPanel = aTab.browser;
+ // Focus the browser so that things like selection will be styled correctly.
+ aTab.browser.focus();
+ },
+
+ get selectedBrowser() {
+ if (this._selectedTab)
+ return this._selectedTab.browser;
+ return null;
+ },
+
+ getTabForId: function getTabForId(aId) {
+ let tabs = this._tabs;
+ for (let i=0; i < tabs.length; i++) {
+ if (tabs[i].id == aId)
+ return tabs[i];
+ }
+ return null;
+ },
+
+ getTabForBrowser: function getTabForBrowser(aBrowser) {
+ let tabs = this._tabs;
+ for (let i = 0; i < tabs.length; i++) {
+ if (tabs[i].browser == aBrowser)
+ return tabs[i];
+ }
+ return null;
+ },
+
+ getTabForWindow: function getTabForWindow(aWindow) {
+ let tabs = this._tabs;
+ for (let i = 0; i < tabs.length; i++) {
+ if (tabs[i].browser.contentWindow == aWindow)
+ return tabs[i];
+ }
+ return null;
+ },
+
+ getBrowserForWindow: function getBrowserForWindow(aWindow) {
+ let tabs = this._tabs;
+ for (let i = 0; i < tabs.length; i++) {
+ if (tabs[i].browser.contentWindow == aWindow)
+ return tabs[i].browser;
+ }
+ return null;
+ },
+
+ getBrowserForDocument: function getBrowserForDocument(aDocument) {
+ let tabs = this._tabs;
+ for (let i = 0; i < tabs.length; i++) {
+ if (tabs[i].browser.contentDocument == aDocument)
+ return tabs[i].browser;
+ }
+ return null;
+ },
+
+ loadURI: function loadURI(aURI, aBrowser, aParams) {
+ aBrowser = aBrowser || this.selectedBrowser;
+ if (!aBrowser)
+ return;
+
+ aParams = aParams || {};
+
+ let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ let postData = ("postData" in aParams && aParams.postData) ? aParams.postData : null;
+ let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null;
+ let charset = "charset" in aParams ? aParams.charset : null;
+
+ let tab = this.getTabForBrowser(aBrowser);
+ if (tab) {
+ if ("userRequested" in aParams) tab.userRequested = aParams.userRequested;
+ tab.isSearch = ("isSearch" in aParams) ? aParams.isSearch : false;
+ }
+
+ try {
+ aBrowser.loadURIWithFlags(aURI, flags, referrerURI, charset, postData);
+ } catch(e) {
+ if (tab) {
+ let message = {
+ type: "Content:LoadError",
+ tabID: tab.id
+ };
+ Messaging.sendRequest(message);
+ dump("Handled load error: " + e)
+ }
+ }
+ },
+
+ addTab: function addTab(aURI, aParams) {
+ aParams = aParams || {};
+
+ let newTab = new Tab(aURI, aParams);
+
+ if (typeof aParams.tabIndex == "number") {
+ this._tabs.splice(aParams.tabIndex, 0, newTab);
+ } else {
+ this._tabs.push(newTab);
+ }
+
+ let selected = "selected" in aParams ? aParams.selected : true;
+ if (selected)
+ this.selectedTab = newTab;
+
+ let pinned = "pinned" in aParams ? aParams.pinned : false;
+ if (pinned) {
+ let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+ ss.setTabValue(newTab, "appOrigin", aURI);
+ }
+
+ let evt = document.createEvent("UIEvents");
+ evt.initUIEvent("TabOpen", true, false, window, null);
+ newTab.browser.dispatchEvent(evt);
+
+ return newTab;
+ },
+
+ // Use this method to close a tab from JS. This method sends a message
+ // to Java to close the tab in the Java UI (we'll get a Tab:Closed message
+ // back from Java when that happens).
+ closeTab: function closeTab(aTab) {
+ if (!aTab) {
+ Cu.reportError("Error trying to close tab (tab doesn't exist)");
+ return;
+ }
+
+ let message = {
+ type: "Tab:Close",
+ tabID: aTab.id
+ };
+ Messaging.sendRequest(message);
+ },
+
+ // Calling this will update the state in BrowserApp after a tab has been
+ // closed in the Java UI.
+ _handleTabClosed: function _handleTabClosed(aTab, aShowUndoSnackbar) {
+ if (aTab == this.selectedTab)
+ this.selectedTab = null;
+
+ let tabIndex = this._tabs.indexOf(aTab);
+
+ let evt = document.createEvent("UIEvents");
+ evt.initUIEvent("TabClose", true, false, window, tabIndex);
+ aTab.browser.dispatchEvent(evt);
+
+ if (aShowUndoSnackbar) {
+ // Get a title for the undo close snackbar. Fall back to the URL if there is no title.
+ let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+ let closedTabData = ss.getClosedTabs(window)[0];
+
+ let message;
+ let title = closedTabData.entries[closedTabData.index - 1].title;
+ let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(aTab.browser);
+
+ if (isPrivate) {
+ message = Strings.browser.GetStringFromName("privateClosedMessage.message");
+ } else if (title) {
+ message = Strings.browser.formatStringFromName("undoCloseToast.message", [title], 1);
+ } else {
+ message = Strings.browser.GetStringFromName("undoCloseToast.messageDefault");
+ }
+
+ Snackbars.show(message, Snackbars.LENGTH_LONG, {
+ action: {
+ label: Strings.browser.GetStringFromName("undoCloseToast.action2"),
+ callback: function() {
+ UITelemetry.addEvent("undo.1", "toast", null, "closetab");
+ ss.undoCloseTab(window, closedTabData);
+ }
+ }
+ });
+ }
+
+ aTab.destroy();
+ this._tabs.splice(tabIndex, 1);
+ },
+
+ // Use this method to select a tab from JS. This method sends a message
+ // to Java to select the tab in the Java UI (we'll get a Tab:Selected message
+ // back from Java when that happens).
+ selectTab: function selectTab(aTab) {
+ if (!aTab) {
+ Cu.reportError("Error trying to select tab (tab doesn't exist)");
+ return;
+ }
+
+ // There's nothing to do if the tab is already selected
+ if (aTab == this.selectedTab)
+ return;
+
+ let doc = this.selectedBrowser.contentDocument;
+ if (doc.fullscreenElement) {
+ // We'll finish the tab selection once the fullscreen transition has ended,
+ // remember the new tab for this.
+ this.fullscreenTransitionTab = aTab;
+ doc.exitFullscreen();
+ }
+
+ let message = {
+ type: "Tab:Select",
+ tabID: aTab.id
+ };
+ Messaging.sendRequest(message);
+ },
+
+ /**
+ * Gets an open tab with the given URL.
+ *
+ * @param aURL URL to look for
+ * @param aOptions Options for the search. Currently supports:
+ ** @option startsWith a Boolean indicating whether to search for a tab who's url starts with the
+ * requested url. Useful if you want to ignore hash codes on the end of a url. For instance
+ * to have about:downloads match about:downloads#123.
+ * @return the tab with the given URL, or null if no such tab exists
+ */
+ getTabWithURL: function getTabWithURL(aURL, aOptions) {
+ aOptions = aOptions || {};
+ let uri = Services.io.newURI(aURL, null, null);
+ for (let i = 0; i < this._tabs.length; ++i) {
+ let tab = this._tabs[i];
+ if (aOptions.startsWith) {
+ if (tab.browser.currentURI.spec.startsWith(aURL)) {
+ return tab;
+ }
+ } else {
+ if (tab.browser.currentURI.equals(uri)) {
+ return tab;
+ }
+ }
+ }
+ return null;
+ },
+
+ /**
+ * If a tab with the given URL already exists, that tab is selected.
+ * Otherwise, a new tab is opened with the given URL.
+ *
+ * @param aURL URL to open
+ * @param aParam Options used if a tab is created
+ * @param aFlags Options for the search. Currently supports:
+ ** @option startsWith a Boolean indicating whether to search for a tab who's url starts with the
+ * requested url. Useful if you want to ignore hash codes on the end of a url. For instance
+ * to have about:downloads match about:downloads#123.
+ */
+ selectOrAddTab: function selectOrAddTab(aURL, aParams, aFlags) {
+ let tab = this.getTabWithURL(aURL, aFlags);
+ if (tab == null) {
+ tab = this.addTab(aURL, aParams);
+ } else {
+ this.selectTab(tab);
+ }
+
+ return tab;
+ },
+
+ // This method updates the state in BrowserApp after a tab has been selected
+ // in the Java UI.
+ _handleTabSelected: function _handleTabSelected(aTab) {
+ if (this.fullscreenTransitionTab) {
+ // Defer updating to "fullscreenchange" if tab selection happened during
+ // a fullscreen transition.
+ return;
+ }
+ this.selectedTab = aTab;
+
+ let evt = document.createEvent("UIEvents");
+ evt.initUIEvent("TabSelect", true, false, window, null);
+ aTab.browser.dispatchEvent(evt);
+ },
+
+ quit: function quit(aClear = { sanitize: {}, dontSaveSession: false }) {
+ // Notify all windows that an application quit has been requested.
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested", null);
+
+ // Quit aborted.
+ if (cancelQuit.data) {
+ return;
+ }
+
+ Services.obs.notifyObservers(null, "quit-application-proceeding", null);
+
+ // Tell session store to forget about this window
+ if (aClear.dontSaveSession) {
+ let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+ ss.removeWindow(window);
+ }
+
+ BrowserApp.sanitize(aClear.sanitize, function() {
+ let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
+ appStartup.quit(Ci.nsIAppStartup.eForceQuit);
+ }, true);
+ },
+
+ saveAsPDF: function saveAsPDF(aBrowser) {
+ RuntimePermissions.waitForPermissions(RuntimePermissions.WRITE_EXTERNAL_STORAGE).then(function(permissionGranted) {
+ if (!permissionGranted) {
+ return;
+ }
+
+ Task.spawn(function* () {
+ let fileName = ContentAreaUtils.getDefaultFileName(aBrowser.contentTitle, aBrowser.currentURI, null, null);
+ fileName = fileName.trim() + ".pdf";
+
+ let downloadsDir = yield Downloads.getPreferredDownloadsDirectory();
+ let file = OS.Path.join(downloadsDir, fileName);
+
+ // Force this to have a unique name.
+ let openedFile = yield OS.File.openUnique(file, { humanReadable: true });
+ file = openedFile.path;
+ yield openedFile.file.close();
+
+ let download = yield Downloads.createDownload({
+ source: aBrowser.contentWindow,
+ target: file,
+ saver: "pdf",
+ startTime: Date.now(),
+ });
+
+ let list = yield Downloads.getList(download.source.isPrivate ? Downloads.PRIVATE : Downloads.PUBLIC)
+ yield list.add(download);
+ yield download.start();
+ });
+ });
+ },
+
+ // These values come from pref_tracking_protection_entries in arrays.xml.
+ PREF_TRACKING_PROTECTION_ENABLED: "2",
+ PREF_TRACKING_PROTECTION_ENABLED_PB: "1",
+ PREF_TRACKING_PROTECTION_DISABLED: "0",
+
+ /**
+ * Returns the current state of the tracking protection pref.
+ * (0 = Disabled, 1 = Enabled in PB, 2 = Enabled)
+ */
+ getTrackingProtectionState: function() {
+ if (Services.prefs.getBoolPref("privacy.trackingprotection.enabled")) {
+ return this.PREF_TRACKING_PROTECTION_ENABLED;
+ }
+ if (Services.prefs.getBoolPref("privacy.trackingprotection.pbmode.enabled")) {
+ return this.PREF_TRACKING_PROTECTION_ENABLED_PB;
+ }
+ return this.PREF_TRACKING_PROTECTION_DISABLED;
+ },
+
+ sanitize: function (aItems, callback, aShutdown) {
+ let success = true;
+ var promises = [];
+
+ for (let key in aItems) {
+ if (!aItems[key])
+ continue;
+
+ key = key.replace("private.data.", "");
+
+ switch (key) {
+ case "cookies_sessions":
+ promises.push(Sanitizer.clearItem("cookies"));
+ promises.push(Sanitizer.clearItem("sessions"));
+ break;
+ default:
+ promises.push(Sanitizer.clearItem(key));
+ }
+ }
+
+ Promise.all(promises).then(function() {
+ Messaging.sendRequest({
+ type: "Sanitize:Finished",
+ success: true,
+ shutdown: aShutdown === true
+ });
+
+ if (callback) {
+ callback();
+ }
+ }).catch(function(err) {
+ Messaging.sendRequest({
+ type: "Sanitize:Finished",
+ error: err,
+ success: false,
+ shutdown: aShutdown === true
+ });
+
+ if (callback) {
+ callback();
+ }
+ })
+ },
+
+ getFocusedInput: function(aBrowser, aOnlyInputElements = false) {
+ if (!aBrowser)
+ return null;
+
+ let doc = aBrowser.contentDocument;
+ if (!doc)
+ return null;
+
+ let focused = doc.activeElement;
+ while (focused instanceof HTMLFrameElement || focused instanceof HTMLIFrameElement) {
+ doc = focused.contentDocument;
+ focused = doc.activeElement;
+ }
+
+ if (focused instanceof HTMLInputElement &&
+ (focused.mozIsTextField(false) || focused.type === "number")) {
+ return focused;
+ }
+
+ if (aOnlyInputElements)
+ return null;
+
+ if (focused && (focused instanceof HTMLTextAreaElement || focused.isContentEditable)) {
+
+ if (focused instanceof HTMLBodyElement) {
+ // we are putting focus into a contentEditable frame. scroll the frame into
+ // view instead of the contentEditable document contained within, because that
+ // results in a better user experience
+ focused = focused.ownerDocument.defaultView.frameElement;
+ }
+ return focused;
+ }
+ return null;
+ },
+
+ scrollToFocusedInput: function(aBrowser, aAllowZoom = true) {
+ let formHelperMode = Services.prefs.getIntPref("formhelper.mode");
+ if (formHelperMode == kFormHelperModeDisabled)
+ return;
+
+ let dwu = aBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ if (!dwu) {
+ return;
+ }
+
+ let apzFlushDone = function() {
+ Services.obs.removeObserver(apzFlushDone, "apz-repaints-flushed", false);
+ dwu.zoomToFocusedInput();
+ };
+
+ let paintDone = function() {
+ window.removeEventListener("MozAfterPaint", paintDone, false);
+ if (dwu.flushApzRepaints()) {
+ Services.obs.addObserver(apzFlushDone, "apz-repaints-flushed", false);
+ } else {
+ apzFlushDone();
+ }
+ };
+
+ let gotResizeWindow = false;
+ let resizeWindow = function(e) {
+ gotResizeWindow = true;
+ aBrowser.contentWindow.removeEventListener("resize", resizeWindow, false);
+ if (dwu.isMozAfterPaintPending) {
+ window.addEventListener("MozAfterPaint", paintDone, false);
+ } else {
+ paintDone();
+ }
+ }
+
+ aBrowser.contentWindow.addEventListener("resize", resizeWindow, false);
+
+ // The "resize" event sometimes fails to fire, so set a timer to catch that case
+ // and unregister the event listener. See Bug 1253469
+ setTimeout(function(e) {
+ if (!gotResizeWindow) {
+ aBrowser.contentWindow.removeEventListener("resize", resizeWindow, false);
+ dwu.zoomToFocusedInput();
+ }
+ }, 500);
+ },
+
+ getUALocalePref: function () {
+ try {
+ return Services.prefs.getComplexValue("general.useragent.locale", Ci.nsIPrefLocalizedString).data;
+ } catch (e) {
+ try {
+ return Services.prefs.getCharPref("general.useragent.locale");
+ } catch (ee) {
+ return undefined;
+ }
+ }
+ },
+
+ getOSLocalePref: function () {
+ try {
+ return Services.prefs.getCharPref("intl.locale.os");
+ } catch (e) {
+ return undefined;
+ }
+ },
+
+ setLocalizedPref: function (pref, value) {
+ let pls = Cc["@mozilla.org/pref-localizedstring;1"]
+ .createInstance(Ci.nsIPrefLocalizedString);
+ pls.data = value;
+ Services.prefs.setComplexValue(pref, Ci.nsIPrefLocalizedString, pls);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ let browser = this.selectedBrowser;
+
+ switch (aTopic) {
+
+ case "Session:Back":
+ browser.goBack();
+ break;
+
+ case "Session:Forward":
+ browser.goForward();
+ break;
+
+ case "Session:Navigate":
+ let index = JSON.parse(aData);
+ let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
+ let historySize = webNav.sessionHistory.count;
+
+ if (index < 0) {
+ index = 0;
+ Log.e("Browser", "Negative index truncated to zero");
+ } else if (index >= historySize) {
+ Log.e("Browser", "Incorrect index " + index + " truncated to " + historySize - 1);
+ index = historySize - 1;
+ }
+
+ browser.gotoIndex(index);
+ break;
+
+ case "Session:Reload": {
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+
+ // Check to see if this is a message to enable/disable mixed content blocking.
+ if (aData) {
+ let data = JSON.parse(aData);
+
+ if (data.bypassCache) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE |
+ Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY;
+ }
+
+ if (data.contentType === "tracking") {
+ // Convert document URI into the format used by
+ // nsChannelClassifier::ShouldEnableTrackingProtection
+ // (any scheme turned into https is correct)
+ let normalizedUrl = Services.io.newURI("https://" + browser.currentURI.hostPort, null, null);
+ if (data.allowContent) {
+ // Add the current host in the 'trackingprotection' consumer of
+ // the permission manager using a normalized URI. This effectively
+ // places this host on the tracking protection white list.
+ if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
+ PrivateBrowsingUtils.addToTrackingAllowlist(normalizedUrl);
+ } else {
+ Services.perms.add(normalizedUrl, "trackingprotection", Services.perms.ALLOW_ACTION);
+ Telemetry.addData("TRACKING_PROTECTION_EVENTS", 1);
+ }
+ } else {
+ // Remove the current host from the 'trackingprotection' consumer
+ // of the permission manager. This effectively removes this host
+ // from the tracking protection white list (any list actually).
+ if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
+ PrivateBrowsingUtils.removeFromTrackingAllowlist(normalizedUrl);
+ } else {
+ Services.perms.remove(normalizedUrl, "trackingprotection");
+ Telemetry.addData("TRACKING_PROTECTION_EVENTS", 2);
+ }
+ }
+ }
+ }
+
+ // Try to use the session history to reload so that framesets are
+ // handled properly. If the window has no session history, fall back
+ // to using the web navigation's reload method.
+ let webNav = browser.webNavigation;
+ try {
+ let sh = webNav.sessionHistory;
+ if (sh)
+ webNav = sh.QueryInterface(Ci.nsIWebNavigation);
+ } catch (e) {}
+ webNav.reload(flags);
+ break;
+ }
+
+ case "Session:Stop":
+ browser.stop();
+ break;
+
+ case "Tab:Load": {
+ let data = JSON.parse(aData);
+ let url = data.url;
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP
+ | Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
+
+ // Pass LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL to prevent any loads from
+ // inheriting the currently loaded document's principal.
+ if (data.userEntered) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
+ }
+
+ let delayLoad = ("delayLoad" in data) ? data.delayLoad : false;
+ let params = {
+ selected: ("selected" in data) ? data.selected : !delayLoad,
+ parentId: ("parentId" in data) ? data.parentId : -1,
+ flags: flags,
+ tabID: data.tabID,
+ isPrivate: (data.isPrivate === true),
+ pinned: (data.pinned === true),
+ delayLoad: (delayLoad === true),
+ desktopMode: (data.desktopMode === true)
+ };
+
+ params.userRequested = url;
+
+ if (data.engine) {
+ let engine = Services.search.getEngineByName(data.engine);
+ if (engine) {
+ let submission = engine.getSubmission(url);
+ url = submission.uri.spec;
+ params.postData = submission.postData;
+ params.isSearch = true;
+ }
+ }
+
+ if (data.newTab) {
+ this.addTab(url, params);
+ } else {
+ if (data.tabId) {
+ // Use a specific browser instead of the selected browser, if it exists
+ let specificBrowser = this.getTabForId(data.tabId).browser;
+ if (specificBrowser)
+ browser = specificBrowser;
+ }
+ this.loadURI(url, browser, params);
+ }
+ break;
+ }
+
+ case "Tab:Selected":
+ this._handleTabSelected(this.getTabForId(parseInt(aData)));
+ break;
+
+ case "Tab:Closed": {
+ let data = JSON.parse(aData);
+ this._handleTabClosed(this.getTabForId(data.tabId), data.showUndoToast);
+ break;
+ }
+
+ case "keyword-search":
+ // This event refers to a search via the URL bar, not a bookmarks
+ // keyword search. Note that this code assumes that the user can only
+ // perform a keyword search on the selected tab.
+ this.selectedTab.isSearch = true;
+
+ // Don't store queries in private browsing mode.
+ let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.selectedTab.browser);
+ let query = isPrivate ? "" : aData;
+
+ let engine = aSubject.QueryInterface(Ci.nsISearchEngine);
+ Messaging.sendRequest({
+ type: "Search:Keyword",
+ identifier: engine.identifier,
+ name: engine.name,
+ query: query
+ });
+ break;
+
+ case "Browser:Quit":
+ // Add-ons like QuitNow and CleanQuit provide aData as an empty-string ("").
+ // Pass undefined to invoke the methods default parms.
+ this.quit(aData ? JSON.parse(aData) : undefined);
+ break;
+
+ case "SaveAs:PDF":
+ this.saveAsPDF(browser);
+ break;
+
+ case "ScrollTo:FocusedInput":
+ // these messages come from a change in the viewable area and not user interaction
+ // we allow scrolling to the selected input, but not zooming the page
+ this.scrollToFocusedInput(browser, false);
+ break;
+
+ case "Sanitize:ClearData":
+ this.sanitize(JSON.parse(aData));
+ break;
+
+ case "FullScreen:Exit":
+ browser.contentDocument.exitFullscreen();
+ break;
+
+ case "Passwords:Init": {
+ let storage = Cc["@mozilla.org/login-manager/storage/mozStorage;1"].
+ getService(Ci.nsILoginManagerStorage);
+ storage.initialize();
+ Services.obs.removeObserver(this, "Passwords:Init");
+ break;
+ }
+
+ case "FormHistory:Init": {
+ // Force creation/upgrade of formhistory.sqlite
+ FormHistory.count({});
+ Services.obs.removeObserver(this, "FormHistory:Init");
+ break;
+ }
+
+ case "android-get-pref": {
+ // These pref names are not "real" pref names. They are used in the
+ // setting menu, and these are passed when initializing the setting
+ // menu. aSubject is a nsIWritableVariant to hold the pref value.
+ aSubject.QueryInterface(Ci.nsIWritableVariant);
+
+ switch (aData) {
+ // The plugin pref is actually two separate prefs, so
+ // we need to handle it differently
+ case "plugin.enable":
+ aSubject.setAsAString(PluginHelper.getPluginPreference());
+ break;
+
+ // Handle master password
+ case "privacy.masterpassword.enabled":
+ aSubject.setAsBool(MasterPassword.enabled);
+ break;
+
+ case "privacy.trackingprotection.state": {
+ aSubject.setAsAString(this.getTrackingProtectionState());
+ break;
+ }
+
+ // Crash reporter submit pref must be fetched from nsICrashReporter
+ // service.
+ case "datareporting.crashreporter.submitEnabled":
+ let crashReporterBuilt = "nsICrashReporter" in Ci &&
+ Services.appinfo instanceof Ci.nsICrashReporter;
+ if (crashReporterBuilt) {
+ aSubject.setAsBool(Services.appinfo.submitReports);
+ }
+ break;
+ }
+ break;
+ }
+
+ case "android-set-pref": {
+ // Pseudo-prefs. aSubject is an nsIWritableVariant that holds the pref
+ // value. Set to empty to signal the pref was handled.
+ aSubject.QueryInterface(Ci.nsIWritableVariant);
+ let value = aSubject.QueryInterface(Ci.nsIVariant);
+
+ switch (aData) {
+ // The plugin pref is actually two separate prefs, so we need to
+ // handle it differently.
+ case "plugin.enable":
+ PluginHelper.setPluginPreference(value);
+ aSubject.setAsEmpty();
+ break;
+
+ // MasterPassword pref is not real, we just need take action and leave
+ case "privacy.masterpassword.enabled":
+ if (MasterPassword.enabled) {
+ MasterPassword.removePassword(value);
+ } else {
+ MasterPassword.setPassword(value);
+ }
+ aSubject.setAsEmpty();
+ break;
+
+ // "privacy.trackingprotection.state" is not a "real" pref name, but
+ // it's used in the setting menu. By default
+ // "privacy.trackingprotection.pbmode.enabled" is true, and
+ // "privacy.trackingprotection.enabled" is false.
+ case "privacy.trackingprotection.state": {
+ switch (value) {
+ // Tracking protection disabled.
+ case this.PREF_TRACKING_PROTECTION_DISABLED:
+ Services.prefs.setBoolPref("privacy.trackingprotection.pbmode.enabled", false);
+ Services.prefs.setBoolPref("privacy.trackingprotection.enabled", false);
+ break;
+ // Tracking protection only in private browsing,
+ case this.PREF_TRACKING_PROTECTION_ENABLED_PB:
+ Services.prefs.setBoolPref("privacy.trackingprotection.pbmode.enabled", true);
+ Services.prefs.setBoolPref("privacy.trackingprotection.enabled", false);
+ break;
+ // Tracking protection everywhere.
+ case this.PREF_TRACKING_PROTECTION_ENABLED:
+ Services.prefs.setBoolPref("privacy.trackingprotection.pbmode.enabled", true);
+ Services.prefs.setBoolPref("privacy.trackingprotection.enabled", true);
+ break;
+ }
+ aSubject.setAsEmpty();
+ break;
+ }
+
+ // Crash reporter preference is in a service; set and return.
+ case "datareporting.crashreporter.submitEnabled":
+ let crashReporterBuilt = "nsICrashReporter" in Ci &&
+ Services.appinfo instanceof Ci.nsICrashReporter;
+ if (crashReporterBuilt) {
+ Services.appinfo.submitReports = value;
+ aSubject.setAsEmpty();
+ }
+ break;
+ }
+ break;
+ }
+
+ case "sessionstore-state-purge-complete":
+ Messaging.sendRequest({ type: "Session:StatePurged" });
+ break;
+
+ case "gather-telemetry":
+ Messaging.sendRequest({ type: "Telemetry:Gather" });
+ break;
+
+ case "Locale:OS":
+ // We know the system locale. We use this for generating Accept-Language headers.
+ console.log("Locale:OS: " + aData);
+ let currentOSLocale = this.getOSLocalePref();
+ if (currentOSLocale == aData) {
+ break;
+ }
+
+ console.log("New OS locale.");
+
+ // Ensure that this choice is immediately persisted, because
+ // Gecko won't be told again if it forgets.
+ Services.prefs.setCharPref("intl.locale.os", aData);
+ Services.prefs.savePrefFile(null);
+
+ let appLocale = this.getUALocalePref();
+
+ this.computeAcceptLanguages(aData, appLocale);
+ break;
+
+ case "Locale:Changed":
+ if (aData) {
+ // The value provided to Locale:Changed should be a BCP47 language tag
+ // understood by Gecko -- for example, "es-ES" or "de".
+ console.log("Locale:Changed: " + aData);
+
+ // We always write a localized pref, even though sometimes the value is a char pref.
+ // (E.g., on desktop single-locale builds.)
+ this.setLocalizedPref("general.useragent.locale", aData);
+ } else {
+ // Resetting.
+ console.log("Switching to system locale.");
+ Services.prefs.clearUserPref("general.useragent.locale");
+ }
+
+ Services.prefs.setBoolPref("intl.locale.matchOS", !aData);
+
+ // Ensure that this choice is immediately persisted, because
+ // Gecko won't be told again if it forgets.
+ Services.prefs.savePrefFile(null);
+
+ // Blow away the string cache so that future lookups get the
+ // correct locale.
+ Strings.flush();
+
+ // Make sure we use the right Accept-Language header.
+ let osLocale;
+ try {
+ // This should never not be set at this point, but better safe than sorry.
+ osLocale = Services.prefs.getCharPref("intl.locale.os");
+ } catch (e) {
+ }
+
+ this.computeAcceptLanguages(osLocale, aData);
+ break;
+
+ case "Fonts:Reload":
+ FontEnumerator.updateFontList();
+ break;
+
+ case "Vibration:Request":
+ if (aSubject instanceof Navigator) {
+ let navigator = aSubject;
+ let buttons = [
+ {
+ label: Strings.browser.GetStringFromName("vibrationRequest.denyButton"),
+ callback: function() {
+ navigator.setVibrationPermission(false);
+ }
+ },
+ {
+ label: Strings.browser.GetStringFromName("vibrationRequest.allowButton"),
+ callback: function() {
+ navigator.setVibrationPermission(true);
+ },
+ positive: true
+ }
+ ];
+ let message = Strings.browser.GetStringFromName("vibrationRequest.message");
+ let options = {};
+ NativeWindow.doorhanger.show(message, "vibration-request", buttons,
+ BrowserApp.selectedTab.id, options, "VIBRATION");
+ }
+ break;
+
+ default:
+ dump('BrowserApp.observe: unexpected topic "' + aTopic + '"\n');
+ break;
+
+ }
+ },
+
+ /**
+ * Set intl.accept_languages accordingly.
+ *
+ * After Bug 881510 this will also accept a real Accept-Language choice as
+ * input; all Accept-Language logic lives here.
+ *
+ * osLocale should never be null, but this method is safe regardless.
+ * appLocale may explicitly be null.
+ */
+ computeAcceptLanguages(osLocale, appLocale) {
+ let defaultBranch = Services.prefs.getDefaultBranch(null);
+ let defaultAccept = defaultBranch.getComplexValue("intl.accept_languages", Ci.nsIPrefLocalizedString).data;
+ console.log("Default intl.accept_languages = " + defaultAccept);
+
+ // A guard for potential breakage. Bug 438031.
+ // This should not be necessary, because we're reading from the default branch,
+ // but better safe than sorry.
+ if (defaultAccept && defaultAccept.startsWith("chrome://")) {
+ defaultAccept = null;
+ } else {
+ // Ensure lowercase everywhere so we can compare.
+ defaultAccept = defaultAccept.toLowerCase();
+ }
+
+ if (appLocale) {
+ appLocale = appLocale.toLowerCase();
+ }
+
+ if (osLocale) {
+ osLocale = osLocale.toLowerCase();
+ }
+
+ // Eliminate values if they're present in the default.
+ let chosen;
+ if (defaultAccept) {
+ // intl.accept_languages is a comma-separated list, with no q-value params. Those
+ // are added when the header is generated.
+ chosen = defaultAccept.split(",")
+ .map(String.trim)
+ .filter((x) => (x != appLocale && x != osLocale));
+ } else {
+ chosen = [];
+ }
+
+ if (osLocale) {
+ chosen.unshift(osLocale);
+ }
+
+ if (appLocale && appLocale != osLocale) {
+ chosen.unshift(appLocale);
+ }
+
+ let result = chosen.join(",");
+ console.log("Setting intl.accept_languages to " + result);
+ this.setLocalizedPref("intl.accept_languages", result);
+ },
+
+ // nsIAndroidBrowserApp
+ get selectedTab() {
+ return this._selectedTab;
+ },
+
+ // nsIAndroidBrowserApp
+ getBrowserTab: function(tabId) {
+ return this.getTabForId(tabId);
+ },
+
+ getUITelemetryObserver: function() {
+ return UITelemetry;
+ },
+
+ // This method will return a list of history items and toIndex based on the action provided from the fromIndex to toIndex,
+ // optionally selecting selIndex (if fromIndex <= selIndex <= toIndex)
+ getHistory: function(data) {
+ let action = data.action;
+ let webNav = BrowserApp.getTabForId(data.tabId).window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
+ let historyIndex = webNav.sessionHistory.index;
+ let historySize = webNav.sessionHistory.count;
+ let canGoBack = webNav.canGoBack;
+ let canGoForward = webNav.canGoForward;
+ let listitems = [];
+ let fromIndex = 0;
+ let toIndex = historySize - 1;
+ let selIndex = historyIndex;
+
+ if (action == "BACK" && canGoBack) {
+ fromIndex = Math.max(historyIndex - kMaxHistoryListSize, 0);
+ toIndex = historyIndex;
+ selIndex = historyIndex;
+ } else if (action == "FORWARD" && canGoForward) {
+ fromIndex = historyIndex;
+ toIndex = Math.min(historySize - 1, historyIndex + kMaxHistoryListSize);
+ selIndex = historyIndex;
+ } else if (action == "ALL" && (canGoBack || canGoForward)){
+ fromIndex = historyIndex - kMaxHistoryListSize / 2;
+ toIndex = historyIndex + kMaxHistoryListSize / 2;
+ if (fromIndex < 0) {
+ toIndex -= fromIndex;
+ }
+
+ if (toIndex > historySize - 1) {
+ fromIndex -= toIndex - (historySize - 1);
+ toIndex = historySize - 1;
+ }
+
+ fromIndex = Math.max(fromIndex, 0);
+ selIndex = historyIndex;
+ } else {
+ // return empty list immediately.
+ return {
+ "historyItems": listitems,
+ "toIndex": toIndex
+ };
+ }
+
+ let browser = this.selectedBrowser;
+ let hist = browser.sessionHistory;
+ for (let i = toIndex; i >= fromIndex; i--) {
+ let entry = hist.getEntryAtIndex(i, false);
+ let item = {
+ title: entry.title || entry.URI.spec,
+ url: entry.URI.spec,
+ selected: (i == selIndex)
+ };
+ listitems.push(item);
+ }
+
+ return {
+ "historyItems": listitems,
+ "toIndex": toIndex
+ };
+ },
+};
+
+var NativeWindow = {
+ init: function() {
+ Services.obs.addObserver(this, "Menu:Clicked", false);
+ Services.obs.addObserver(this, "Doorhanger:Reply", false);
+ this.contextmenus.init();
+ },
+
+ loadDex: function(zipFile, implClass) {
+ Messaging.sendRequest({
+ type: "Dex:Load",
+ zipfile: zipFile,
+ impl: implClass || "Main"
+ });
+ },
+
+ unloadDex: function(zipFile) {
+ Messaging.sendRequest({
+ type: "Dex:Unload",
+ zipfile: zipFile
+ });
+ },
+
+ menu: {
+ _callbacks: [],
+ _menuId: 1,
+ toolsMenuID: -1,
+ add: function() {
+ let options;
+ if (arguments.length == 1) {
+ options = arguments[0];
+ } else if (arguments.length == 3) {
+ Log.w("Browser", "This menu addon API has been deprecated. Instead, use the options object API.");
+ options = {
+ name: arguments[0],
+ callback: arguments[2]
+ };
+ } else {
+ throw "Incorrect number of parameters";
+ }
+
+ options.type = "Menu:Add";
+ options.id = this._menuId;
+
+ Messaging.sendRequest(options);
+ this._callbacks[this._menuId] = options.callback;
+ this._menuId++;
+ return this._menuId - 1;
+ },
+
+ remove: function(aId) {
+ Messaging.sendRequest({ type: "Menu:Remove", id: aId });
+ },
+
+ update: function(aId, aOptions) {
+ if (!aOptions)
+ return;
+
+ Messaging.sendRequest({
+ type: "Menu:Update",
+ id: aId,
+ options: aOptions
+ });
+ }
+ },
+
+ doorhanger: {
+ _callbacks: {},
+ _callbacksId: 0,
+ _promptId: 0,
+
+ /**
+ * @param aOptions
+ * An options JavaScript object holding additional properties for the
+ * notification. The following properties are currently supported:
+ * persistence: An integer. The notification will not automatically
+ * dismiss for this many page loads. If persistence is set
+ * to -1, the doorhanger will never automatically dismiss.
+ * persistWhileVisible:
+ * A boolean. If true, a visible notification will always
+ * persist across location changes.
+ * timeout: A time in milliseconds. The notification will not
+ * automatically dismiss before this time.
+ *
+ * checkbox: A string to appear next to a checkbox under the notification
+ * message. The button callback functions will be called with
+ * the checked state as an argument.
+ *
+ * actionText: An object that specifies a clickable string, a type of action,
+ * and a bundle blob for the consumer to create a click action.
+ * { text: <text>,
+ * type: <type>,
+ * bundle: <blob-object> }
+ *
+ * @param aCategory
+ * Doorhanger type to display (e.g., LOGIN)
+ */
+ show: function(aMessage, aValue, aButtons, aTabID, aOptions, aCategory) {
+ if (aButtons == null) {
+ aButtons = [];
+ }
+
+ if (aButtons.length > 2) {
+ console.log("Doorhanger can have a maximum of two buttons!");
+ aButtons.length = 2;
+ }
+
+ aButtons.forEach((function(aButton) {
+ this._callbacks[this._callbacksId] = { cb: aButton.callback, prompt: this._promptId };
+ aButton.callback = this._callbacksId;
+ this._callbacksId++;
+ }).bind(this));
+
+ this._promptId++;
+ let json = {
+ type: "Doorhanger:Add",
+ message: aMessage,
+ value: aValue,
+ buttons: aButtons,
+ // use the current tab if none is provided
+ tabID: aTabID || BrowserApp.selectedTab.id,
+ options: aOptions || {},
+ category: aCategory
+ };
+ Messaging.sendRequest(json);
+ },
+
+ hide: function(aValue, aTabID) {
+ Messaging.sendRequest({
+ type: "Doorhanger:Remove",
+ value: aValue,
+ tabID: aTabID
+ });
+ }
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "Menu:Clicked") {
+ if (this.menu._callbacks[aData])
+ this.menu._callbacks[aData]();
+ } else if (aTopic == "Doorhanger:Reply") {
+ let data = JSON.parse(aData);
+ let reply_id = data["callback"];
+
+ if (this.doorhanger._callbacks[reply_id]) {
+ // Pass the value of the optional checkbox to the callback
+ let checked = data["checked"];
+ this.doorhanger._callbacks[reply_id].cb(checked, data.inputs);
+
+ let prompt = this.doorhanger._callbacks[reply_id].prompt;
+ for (let id in this.doorhanger._callbacks) {
+ if (this.doorhanger._callbacks[id].prompt == prompt) {
+ delete this.doorhanger._callbacks[id];
+ }
+ }
+ }
+ }
+ },
+
+ contextmenus: {
+ items: {}, // a list of context menu items that we may show
+ DEFAULT_HTML5_ORDER: -1, // Sort order for HTML5 context menu items
+
+ init: function() {
+ // Accessing "NativeWindow.contextmenus" initializes context menus if needed.
+ BrowserApp.deck.addEventListener(
+ "contextmenu", (e) => NativeWindow.contextmenus.show(e), false);
+ },
+
+ add: function() {
+ let args;
+ if (arguments.length == 1) {
+ args = arguments[0];
+ } else if (arguments.length == 3) {
+ args = {
+ label : arguments[0],
+ selector: arguments[1],
+ callback: arguments[2]
+ };
+ } else {
+ throw "Incorrect number of parameters";
+ }
+
+ if (!args.label)
+ throw "Menu items must have a name";
+
+ let cmItem = new ContextMenuItem(args);
+ this.items[cmItem.id] = cmItem;
+ return cmItem.id;
+ },
+
+ remove: function(aId) {
+ delete this.items[aId];
+ },
+
+ // Although we do not use this ourselves anymore, add-ons may still
+ // need it as it has been documented, so we shouldn't remove it.
+ SelectorContext: function(aSelector) {
+ return {
+ matches: function(aElt) {
+ if (aElt.matches)
+ return aElt.matches(aSelector);
+ return false;
+ }
+ };
+ },
+
+ linkOpenableNonPrivateContext: {
+ matches: function linkOpenableNonPrivateContextMatches(aElement) {
+ let doc = aElement.ownerDocument;
+ if (!doc || PrivateBrowsingUtils.isContentWindowPrivate(doc.defaultView)) {
+ return false;
+ }
+
+ return NativeWindow.contextmenus.linkOpenableContext.matches(aElement);
+ }
+ },
+
+ linkOpenableContext: {
+ matches: function linkOpenableContextMatches(aElement) {
+ let uri = NativeWindow.contextmenus._getLink(aElement);
+ if (uri) {
+ let scheme = uri.scheme;
+ let dontOpen = /^(javascript|mailto|news|snews|tel)$/;
+ return (scheme && !dontOpen.test(scheme));
+ }
+ return false;
+ }
+ },
+
+ linkCopyableContext: {
+ matches: function linkCopyableContextMatches(aElement) {
+ let uri = NativeWindow.contextmenus._getLink(aElement);
+ if (uri) {
+ let scheme = uri.scheme;
+ let dontCopy = /^(mailto|tel)$/;
+ return (scheme && !dontCopy.test(scheme));
+ }
+ return false;
+ }
+ },
+
+ linkShareableContext: {
+ matches: function linkShareableContextMatches(aElement) {
+ let uri = NativeWindow.contextmenus._getLink(aElement);
+ if (uri) {
+ let scheme = uri.scheme;
+ let dontShare = /^(about|chrome|file|javascript|mailto|resource|tel)$/;
+ return (scheme && !dontShare.test(scheme));
+ }
+ return false;
+ }
+ },
+
+ linkBookmarkableContext: {
+ matches: function linkBookmarkableContextMatches(aElement) {
+ let uri = NativeWindow.contextmenus._getLink(aElement);
+ if (uri) {
+ let scheme = uri.scheme;
+ let dontBookmark = /^(mailto|tel)$/;
+ return (scheme && !dontBookmark.test(scheme));
+ }
+ return false;
+ }
+ },
+
+ emailLinkContext: {
+ matches: function emailLinkContextMatches(aElement) {
+ let uri = NativeWindow.contextmenus._getLink(aElement);
+ if (uri)
+ return uri.schemeIs("mailto");
+ return false;
+ }
+ },
+
+ phoneNumberLinkContext: {
+ matches: function phoneNumberLinkContextMatches(aElement) {
+ let uri = NativeWindow.contextmenus._getLink(aElement);
+ if (uri)
+ return uri.schemeIs("tel");
+ return false;
+ }
+ },
+
+ imageLocationCopyableContext: {
+ matches: function imageLinkCopyableContextMatches(aElement) {
+ if (aElement instanceof Ci.nsIDOMHTMLImageElement) {
+ // The image is blocked by Tap-to-load Images
+ if (aElement.hasAttribute("data-ctv-src") && !aElement.hasAttribute("data-ctv-show")) {
+ return false;
+ }
+ }
+ return (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI);
+ }
+ },
+
+ imageSaveableContext: {
+ matches: function imageSaveableContextMatches(aElement) {
+ if (aElement instanceof Ci.nsIDOMHTMLImageElement) {
+ // The image is blocked by Tap-to-load Images
+ if (aElement.hasAttribute("data-ctv-src") && !aElement.hasAttribute("data-ctv-show")) {
+ return false;
+ }
+ }
+ if (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI) {
+ // The image must be loaded to allow saving
+ let request = aElement.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
+ return (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE));
+ }
+ return false;
+ }
+ },
+
+ imageShareableContext: {
+ matches: function imageShareableContextMatches(aElement) {
+ let imgSrc = '';
+ if (aElement instanceof Ci.nsIDOMHTMLImageElement) {
+ imgSrc = aElement.src;
+ } else if (aElement instanceof Ci.nsIImageLoadingContent &&
+ aElement.currentURI &&
+ aElement.currentURI.spec) {
+ imgSrc = aElement.currentURI.spec;
+ }
+
+ // In order to share an image, we need to pass the image src over IPC via an Intent (in
+ // `ApplicationPackageManager.queryIntentActivities`). However, the transaction has a 1MB limit
+ // (shared by all transactions in progress) - otherwise we crash! (bug 1243305)
+ // https://developer.android.com/reference/android/os/TransactionTooLargeException.html
+ //
+ // The transaction limit is 1MB and we arbitrarily choose to cap this transaction at 1/4 of that = 250,000 bytes.
+ // In Java, a UTF-8 character is 1-4 bytes so, 250,000 bytes / 4 bytes/char = 62,500 char
+ let MAX_IMG_SRC_LEN = 62500;
+ let isTooLong = imgSrc.length >= MAX_IMG_SRC_LEN;
+ return !isTooLong && this.NativeWindow.contextmenus.imageSaveableContext.matches(aElement);
+ }.bind(this)
+ },
+
+ mediaSaveableContext: {
+ matches: function mediaSaveableContextMatches(aElement) {
+ return (aElement instanceof HTMLVideoElement ||
+ aElement instanceof HTMLAudioElement);
+ }
+ },
+
+ imageBlockingPolicyContext: {
+ matches: function imageBlockingPolicyContextMatches(aElement) {
+ if (aElement instanceof Ci.nsIDOMHTMLImageElement && aElement.getAttribute("data-ctv-src")) {
+ // Only show the menuitem if we are blocking the image
+ if (aElement.getAttribute("data-ctv-show") == "true") {
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+ },
+
+ mediaContext: function(aMode) {
+ return {
+ matches: function(aElt) {
+ if (aElt instanceof Ci.nsIDOMHTMLMediaElement) {
+ let hasError = aElt.error != null || aElt.networkState == aElt.NETWORK_NO_SOURCE;
+ if (hasError)
+ return false;
+
+ let paused = aElt.paused || aElt.ended;
+ if (paused && aMode == "media-paused")
+ return true;
+ if (!paused && aMode == "media-playing")
+ return true;
+ let controls = aElt.controls;
+ if (!controls && aMode == "media-hidingcontrols")
+ return true;
+
+ let muted = aElt.muted;
+ if (muted && aMode == "media-muted")
+ return true;
+ else if (!muted && aMode == "media-unmuted")
+ return true;
+ }
+ return false;
+ }
+ };
+ },
+
+ videoContext: function(aMode) {
+ return {
+ matches: function(aElt) {
+ if (aElt instanceof HTMLVideoElement) {
+ if (!aMode) {
+ return true;
+ }
+ var isFullscreen = aElt.ownerDocument.fullscreenElement == aElt;
+ if (aMode == "not-fullscreen") {
+ return !isFullscreen;
+ }
+ if (aMode == "fullscreen") {
+ return isFullscreen;
+ }
+ }
+ return false;
+ }
+ };
+ },
+
+ /* Holds a WeakRef to the original target element this context menu was shown for.
+ * Most API's will have to walk up the tree from this node to find the correct element
+ * to act on
+ */
+ get _target() {
+ if (this._targetRef)
+ return this._targetRef.get();
+ return null;
+ },
+
+ set _target(aTarget) {
+ if (aTarget)
+ this._targetRef = Cu.getWeakReference(aTarget);
+ else this._targetRef = null;
+ },
+
+ get defaultContext() {
+ delete this.defaultContext;
+ return this.defaultContext = Strings.browser.GetStringFromName("browser.menu.context.default");
+ },
+
+ /* Gets menuitems for an arbitrary node
+ * Parameters:
+ * element - The element to look at. If this element has a contextmenu attribute, the
+ * corresponding contextmenu will be used.
+ */
+ _getHTMLContextMenuItemsForElement: function(element) {
+ let htmlMenu = element.contextMenu;
+ if (!htmlMenu) {
+ return [];
+ }
+
+ htmlMenu.QueryInterface(Components.interfaces.nsIHTMLMenu);
+ htmlMenu.sendShowEvent();
+
+ return this._getHTMLContextMenuItemsForMenu(htmlMenu, element);
+ },
+
+ /* Add a menuitem for an HTML <menu> node
+ * Parameters:
+ * menu - The <menu> element to iterate through for menuitems
+ * target - The target element these context menu items are attached to
+ */
+ _getHTMLContextMenuItemsForMenu: function(menu, target) {
+ let items = [];
+ for (let i = 0; i < menu.childNodes.length; i++) {
+ let elt = menu.childNodes[i];
+ if (!elt.label)
+ continue;
+
+ items.push(new HTMLContextMenuItem(elt, target));
+ }
+
+ return items;
+ },
+
+ // Searches the current list of menuitems to show for any that match this id
+ _findMenuItem: function(aId) {
+ if (!this.menus) {
+ return null;
+ }
+
+ for (let context in this.menus) {
+ let menu = this.menus[context];
+ for (let i = 0; i < menu.length; i++) {
+ if (menu[i].id === aId) {
+ return menu[i];
+ }
+ }
+ }
+ return null;
+ },
+
+ // Returns true if there are any context menu items to show
+ _shouldShow: function() {
+ for (let context in this.menus) {
+ let menu = this.menus[context];
+ if (menu.length > 0) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /* Returns a label to be shown in a tabbed ui if there are multiple "contexts". For instance, if this
+ * is an image inside an <a> tag, we may have a "link" context and an "image" one.
+ */
+ _getContextType: function(element) {
+ // For anchor nodes, we try to use the scheme to pick a string
+ if (element instanceof Ci.nsIDOMHTMLAnchorElement) {
+ let uri = this.makeURI(this._getLinkURL(element));
+ try {
+ return Strings.browser.GetStringFromName("browser.menu.context." + uri.scheme);
+ } catch(ex) { }
+ }
+
+ // Otherwise we try the nodeName
+ try {
+ return Strings.browser.GetStringFromName("browser.menu.context." + element.nodeName.toLowerCase());
+ } catch(ex) { }
+
+ // Fallback to the default
+ return this.defaultContext;
+ },
+
+ // Adds context menu items added through the add-on api
+ _getNativeContextMenuItems: function(element, x, y) {
+ let res = [];
+ for (let itemId of Object.keys(this.items)) {
+ let item = this.items[itemId];
+
+ if (!this._findMenuItem(item.id) && item.matches(element, x, y)) {
+ res.push(item);
+ }
+ }
+
+ return res;
+ },
+
+ /* Checks if there are context menu items to show, and if it finds them
+ * sends a contextmenu event to content. We also send showing events to
+ * any html5 context menus we are about to show, and fire some local notifications
+ * for chrome consumers to do lazy menuitem construction
+ */
+ show: function(event) {
+ // Android Long-press / contextmenu event provides clientX/Y data. This is not provided
+ // by mochitest: test_browserElement_inproc_ContextmenuEvents.html.
+ if (!event.clientX || !event.clientY) {
+ return;
+ }
+
+ // If the event was already defaultPrevented by somebody (web content, or
+ // some other part of gecko), then don't do anything with it.
+ if (event.defaultPrevented) {
+ return;
+ }
+
+ // Use the highlighted element for the context menu target. When accessibility is
+ // enabled, elements may not be highlighted so use the event target instead.
+ this._target = BrowserEventHandler._highlightElement || event.target;
+ if (!this._target) {
+ return;
+ }
+
+ // Try to build a list of contextmenu items. If successful, actually show the
+ // native context menu by passing the list to Java.
+ this._buildMenu(event.clientX, event.clientY);
+ if (this._shouldShow()) {
+ BrowserEventHandler._cancelTapHighlight();
+
+ // Consume / preventDefault the event, and show the contextmenu.
+ event.preventDefault();
+ this._innerShow(this._target, event.clientX, event.clientY);
+ this._target = null;
+
+ return;
+ }
+
+ // If no context-menu for long-press event, it may be meant to trigger text-selection.
+ this.menus = null;
+ Services.obs.notifyObservers(
+ {target: this._target, x: event.clientX, y: event.clientY}, "context-menu-not-shown", "");
+ },
+
+ // Returns a title for a context menu. If no title attribute exists, will fall back to looking for a url
+ _getTitle: function(node) {
+ if (node.hasAttribute && node.hasAttribute("title")) {
+ return node.getAttribute("title");
+ }
+ return this._getUrl(node);
+ },
+
+ // Returns a url associated with a node
+ _getUrl: function(node) {
+ if ((node instanceof Ci.nsIDOMHTMLAnchorElement && node.href) ||
+ (node instanceof Ci.nsIDOMHTMLAreaElement && node.href)) {
+ return this._getLinkURL(node);
+ } else if (node instanceof Ci.nsIImageLoadingContent && node.currentURI) {
+ // The image is blocked by Tap-to-load Images
+ let originalURL = node.getAttribute("data-ctv-src");
+ if (originalURL) {
+ return originalURL;
+ }
+ return node.currentURI.spec;
+ } else if (node instanceof Ci.nsIDOMHTMLMediaElement) {
+ let srcUrl = node.currentSrc || node.src;
+ // If URL prepended with blob or mediasource, we'll remove it.
+ return srcUrl.replace(/^(?:blob|mediasource):/, '');
+ }
+
+ return "";
+ },
+
+ // Adds an array of menuitems to the current list of items to show, in the correct context
+ _addMenuItems: function(items, context) {
+ if (!this.menus[context]) {
+ this.menus[context] = [];
+ }
+ this.menus[context] = this.menus[context].concat(items);
+ },
+
+ /* Does the basic work of building a context menu to show. Will combine HTML and Native
+ * context menus items, as well as sorting menuitems into different menus based on context.
+ */
+ _buildMenu: function(x, y) {
+ // now walk up the tree and for each node look for any context menu items that apply
+ let element = this._target;
+
+ // this.menus holds a hashmap of "contexts" to menuitems associated with that context
+ // For instance, if the user taps an image inside a link, we'll have something like:
+ // {
+ // link: [ ContextMenuItem, ContextMenuItem ]
+ // image: [ ContextMenuItem, ContextMenuItem ]
+ // }
+ this.menus = {};
+
+ while (element) {
+ let context = this._getContextType(element);
+
+ // First check for any html5 context menus that might exist...
+ var items = this._getHTMLContextMenuItemsForElement(element);
+ if (items.length > 0) {
+ this._addMenuItems(items, context);
+ }
+
+ // then check for any context menu items registered in the ui.
+ items = this._getNativeContextMenuItems(element, x, y);
+ if (items.length > 0) {
+ this._addMenuItems(items, context);
+ }
+
+ // walk up the tree and find more items to show
+ element = element.parentNode;
+ }
+ },
+
+ // Walks the DOM tree to find a title from a node
+ _findTitle: function(node) {
+ let title = "";
+ while(node && !title) {
+ title = this._getTitle(node);
+ node = node.parentNode;
+ }
+ return title;
+ },
+
+ /* Reformats the list of menus to show into an object that can be sent to Prompt.jsm
+ * If there is one menu, will return a flat array of menuitems. If there are multiple
+ * menus, will return an array with appropriate tabs/items inside it. i.e. :
+ * [
+ * { label: "link", items: [...] },
+ * { label: "image", items: [...] }
+ * ]
+ */
+ _reformatList: function(target) {
+ let contexts = Object.keys(this.menus);
+
+ if (contexts.length === 1) {
+ // If there's only one context, we'll only show a single flat single select list
+ return this._reformatMenuItems(target, this.menus[contexts[0]]);
+ }
+
+ // If there are multiple contexts, we'll only show a tabbed ui with multiple lists
+ return this._reformatListAsTabs(target, this.menus);
+ },
+
+ /* Reformats the list of menus to show into an object that can be sent to Prompt.jsm's
+ * addTabs method. i.e. :
+ * { link: [...], image: [...] } becomes
+ * [ { label: "link", items: [...] } ]
+ *
+ * Also reformats items and resolves any parmaeters that aren't known until display time
+ * (for instance Helper app menu items adjust their title to reflect what Helper App can be used for this link).
+ */
+ _reformatListAsTabs: function(target, menus) {
+ let itemArray = [];
+
+ // Sort the keys so that "link" is always first
+ let contexts = Object.keys(this.menus);
+ contexts.sort((context1, context2) => {
+ if (context1 === this.defaultContext) {
+ return -1;
+ } else if (context2 === this.defaultContext) {
+ return 1;
+ }
+ return 0;
+ });
+
+ contexts.forEach(context => {
+ itemArray.push({
+ label: context,
+ items: this._reformatMenuItems(target, menus[context])
+ });
+ });
+
+ return itemArray;
+ },
+
+ /* Reformats an array of ContextMenuItems into an array that can be handled by Prompt.jsm. Also reformats items
+ * and resolves any parmaeters that aren't known until display time
+ * (for instance Helper app menu items adjust their title to reflect what Helper App can be used for this link).
+ */
+ _reformatMenuItems: function(target, menuitems) {
+ let itemArray = [];
+
+ for (let i = 0; i < menuitems.length; i++) {
+ let t = target;
+ while(t) {
+ if (menuitems[i].matches(t)) {
+ let val = menuitems[i].getValue(t);
+
+ // hidden menu items will return null from getValue
+ if (val) {
+ itemArray.push(val);
+ break;
+ }
+ }
+
+ t = t.parentNode;
+ }
+ }
+
+ return itemArray;
+ },
+
+ // Called where we're finally ready to actually show the contextmenu. Sorts the items and shows a prompt.
+ _innerShow: function(target, x, y) {
+ Haptic.performSimpleAction(Haptic.LongPress);
+
+ // spin through the tree looking for a title for this context menu
+ let title = this._findTitle(target);
+
+ for (let context in this.menus) {
+ let menu = this.menus[context];
+ menu.sort((a,b) => {
+ if (a.order === b.order) {
+ return 0;
+ }
+ return (a.order > b.order) ? 1 : -1;
+ });
+ }
+
+ let useTabs = Object.keys(this.menus).length > 1;
+ let prompt = new Prompt({
+ window: target.ownerDocument.defaultView,
+ title: useTabs ? undefined : title
+ });
+
+ let items = this._reformatList(target);
+ if (useTabs) {
+ prompt.addTabs({
+ id: "tabs",
+ items: items
+ });
+ } else {
+ prompt.setSingleChoiceItems(items);
+ }
+
+ prompt.show(this._promptDone.bind(this, target, x, y, items));
+ },
+
+ // Called when the contextmenu prompt is closed
+ _promptDone: function(target, x, y, items, data) {
+ if (data.button == -1) {
+ // Prompt was cancelled, or an ActionView was used.
+ return;
+ }
+
+ let selectedItemId;
+ if (data.tabs) {
+ let menu = items[data.tabs.tab];
+ selectedItemId = menu.items[data.tabs.item].id;
+ } else {
+ selectedItemId = items[data.list[0]].id
+ }
+
+ let selectedItem = this._findMenuItem(selectedItemId);
+ this.menus = null;
+
+ if (!selectedItem || !selectedItem.matches || !selectedItem.callback) {
+ return;
+ }
+
+ // for menuitems added using the native UI, pass the dom element that matched that item to the callback
+ while (target) {
+ if (selectedItem.matches(target, x, y)) {
+ selectedItem.callback(target, x, y);
+ break;
+ }
+ target = target.parentNode;
+ }
+ },
+
+ // XXX - These are stolen from Util.js, we should remove them if we bring it back
+ makeURLAbsolute: function makeURLAbsolute(base, url) {
+ // Note: makeURI() will throw if url is not a valid URI
+ return this.makeURI(url, null, this.makeURI(base)).spec;
+ },
+
+ makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) {
+ return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
+ },
+
+ _getLink: function(aElement) {
+ if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
+ ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) ||
+ (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href) ||
+ aElement instanceof Ci.nsIDOMHTMLLinkElement ||
+ aElement.getAttributeNS(kXLinkNamespace, "type") == "simple")) {
+ try {
+ let url = this._getLinkURL(aElement);
+ return Services.io.newURI(url, null, null);
+ } catch (e) {}
+ }
+ return null;
+ },
+
+ _disableRestricted: function _disableRestricted(restriction, selector) {
+ return {
+ matches: function _disableRestrictedMatches(aElement, aX, aY) {
+ if (!ParentalControls.isAllowed(ParentalControls[restriction])) {
+ return false;
+ }
+
+ return selector.matches(aElement, aX, aY);
+ }
+ };
+ },
+
+ _getLinkURL: function ch_getLinkURL(aLink) {
+ let href = aLink.href;
+ if (href)
+ return href;
+
+ href = aLink.getAttribute("href") ||
+ aLink.getAttributeNS(kXLinkNamespace, "href");
+ if (!href || !href.match(/\S/)) {
+ // Without this we try to save as the current doc,
+ // for example, HTML case also throws if empty
+ throw "Empty href";
+ }
+
+ return this.makeURLAbsolute(aLink.baseURI, href);
+ },
+
+ _copyStringToDefaultClipboard: function(aString) {
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(aString);
+ Snackbars.show(Strings.browser.GetStringFromName("selectionHelper.textCopied"), Snackbars.LENGTH_LONG);
+ },
+
+ _stripScheme: function(aString) {
+ let index = aString.indexOf(":");
+ return aString.slice(index + 1);
+ }
+ }
+};
+
+XPCOMUtils.defineLazyModuleGetter(this, "PageActions",
+ "resource://gre/modules/PageActions.jsm");
+
+// These alias to the old, deprecated NativeWindow interfaces
+[
+ ["pageactions", "resource://gre/modules/PageActions.jsm", "PageActions"],
+ ["toast", "resource://gre/modules/Snackbars.jsm", "Snackbars"]
+].forEach(item => {
+ let [name, script, exprt] = item;
+
+ XPCOMUtils.defineLazyGetter(NativeWindow, name, () => {
+ var err = Strings.browser.formatStringFromName("nativeWindow.deprecated", ["NativeWindow." + name, script], 2);
+ Cu.reportError(err);
+
+ let sandbox = {};
+ Cu.import(script, sandbox);
+ return sandbox[exprt];
+ });
+});
+
+var LightWeightThemeWebInstaller = {
+ init: function sh_init() {
+ let temp = {};
+ Cu.import("resource://gre/modules/LightweightThemeConsumer.jsm", temp);
+ let theme = new temp.LightweightThemeConsumer(document);
+ BrowserApp.deck.addEventListener("InstallBrowserTheme", this, false, true);
+ BrowserApp.deck.addEventListener("PreviewBrowserTheme", this, false, true);
+ BrowserApp.deck.addEventListener("ResetBrowserThemePreview", this, false, true);
+
+ if (ParentalControls.parentalControlsEnabled &&
+ !this._manager.currentTheme &&
+ ParentalControls.isAllowed(ParentalControls.DEFAULT_THEME)) {
+ // We are using the DEFAULT_THEME restriction to differentiate between restricted profiles & guest mode - Bug 1199596
+ this._installParentalControlsTheme();
+ }
+ },
+
+ handleEvent: function (event) {
+ switch (event.type) {
+ case "InstallBrowserTheme":
+ case "PreviewBrowserTheme":
+ case "ResetBrowserThemePreview":
+ // ignore requests from background tabs
+ if (event.target.ownerDocument.defaultView.top != content)
+ return;
+ }
+
+ switch (event.type) {
+ case "InstallBrowserTheme":
+ this._installRequest(event);
+ break;
+ case "PreviewBrowserTheme":
+ this._preview(event);
+ break;
+ case "ResetBrowserThemePreview":
+ this._resetPreview(event);
+ break;
+ case "pagehide":
+ case "TabSelect":
+ this._resetPreview();
+ break;
+ }
+ },
+
+ get _manager () {
+ let temp = {};
+ Cu.import("resource://gre/modules/LightweightThemeManager.jsm", temp);
+ delete this._manager;
+ return this._manager = temp.LightweightThemeManager;
+ },
+
+ _installParentalControlsTheme: function() {
+ let mgr = this._manager;
+ let parentalControlsTheme = {
+ "headerURL": "resource://android/assets/parental_controls_theme.png",
+ "name": "Parental Controls Theme",
+ "id": "parental-controls-theme@mozilla.org"
+ };
+
+ mgr.addBuiltInTheme(parentalControlsTheme);
+ mgr.themeChanged(parentalControlsTheme);
+ },
+
+ _installRequest: function (event) {
+ let node = event.target;
+ let data = this._getThemeFromNode(node);
+ if (!data)
+ return;
+
+ if (this._isAllowed(node)) {
+ this._install(data);
+ return;
+ }
+
+ let allowButtonText = Strings.browser.GetStringFromName("lwthemeInstallRequest.allowButton");
+ let message = Strings.browser.formatStringFromName("lwthemeInstallRequest.message", [node.ownerDocument.location.hostname], 1);
+ let buttons = [{
+ label: allowButtonText,
+ callback: function () {
+ LightWeightThemeWebInstaller._install(data);
+ },
+ positive: true
+ }];
+
+ NativeWindow.doorhanger.show(message, "Personas", buttons, BrowserApp.selectedTab.id);
+ },
+
+ _install: function (newLWTheme) {
+ this._manager.currentTheme = newLWTheme;
+ },
+
+ _previewWindow: null,
+ _preview: function (event) {
+ if (!this._isAllowed(event.target))
+ return;
+ let data = this._getThemeFromNode(event.target);
+ if (!data)
+ return;
+ this._resetPreview();
+
+ this._previewWindow = event.target.ownerDocument.defaultView;
+ this._previewWindow.addEventListener("pagehide", this, true);
+ BrowserApp.deck.addEventListener("TabSelect", this, false);
+ this._manager.previewTheme(data);
+ },
+
+ _resetPreview: function (event) {
+ if (!this._previewWindow ||
+ event && !this._isAllowed(event.target))
+ return;
+
+ this._previewWindow.removeEventListener("pagehide", this, true);
+ this._previewWindow = null;
+ BrowserApp.deck.removeEventListener("TabSelect", this, false);
+
+ this._manager.resetPreview();
+ },
+
+ _isAllowed: function (node) {
+ // Make sure the whitelist has been imported to permissions
+ PermissionsUtils.importFromPrefs("xpinstall.", "install");
+
+ let pm = Services.perms;
+
+ let uri = node.ownerDocument.documentURIObject;
+ if (!uri.schemeIs("https")) {
+ return false;
+ }
+
+ return pm.testPermission(uri, "install") == pm.ALLOW_ACTION;
+ },
+
+ _getThemeFromNode: function (node) {
+ return this._manager.parseTheme(node.getAttribute("data-browsertheme"), node.baseURI);
+ }
+};
+
+var DesktopUserAgent = {
+ DESKTOP_UA: null,
+ TCO_DOMAIN: "t.co",
+ TCO_REPLACE: / Gecko.*/,
+
+ init: function ua_init() {
+ Services.obs.addObserver(this, "DesktopMode:Change", false);
+ UserAgentOverrides.addComplexOverride(this.onRequest.bind(this));
+
+ // See https://developer.mozilla.org/en/Gecko_user_agent_string_reference
+ this.DESKTOP_UA = Cc["@mozilla.org/network/protocol;1?name=http"]
+ .getService(Ci.nsIHttpProtocolHandler).userAgent
+ .replace(/Android \d.+?; [a-zA-Z]+/, "X11; Linux x86_64")
+ .replace(/Gecko\/[0-9\.]+/, "Gecko/20100101");
+ },
+
+ onRequest: function(channel, defaultUA) {
+ if (AppConstants.NIGHTLY_BUILD && this.TCO_DOMAIN == channel.URI.host) {
+ // Force the referrer
+ channel.referrer = channel.URI;
+
+ // Send a bot-like UA to t.co to get a real redirect. We strip off the
+ // "Gecko/x.y Firefox/x.y" part
+ return defaultUA.replace(this.TCO_REPLACE, "");
+ }
+
+ let channelWindow = this._getWindowForRequest(channel);
+ let tab = BrowserApp.getTabForWindow(channelWindow);
+ if (tab) {
+ return this.getUserAgentForTab(tab);
+ }
+
+ return null;
+ },
+
+ getUserAgentForWindow: function ua_getUserAgentForWindow(aWindow) {
+ let tab = BrowserApp.getTabForWindow(aWindow.top);
+ if (tab) {
+ return this.getUserAgentForTab(tab);
+ }
+
+ return null;
+ },
+
+ getUserAgentForTab: function ua_getUserAgentForTab(aTab) {
+ // Send desktop UA if "Request Desktop Site" is enabled.
+ if (aTab.desktopMode) {
+ return this.DESKTOP_UA;
+ }
+
+ return null;
+ },
+
+ _getRequestLoadContext: function ua_getRequestLoadContext(aRequest) {
+ if (aRequest && aRequest.notificationCallbacks) {
+ try {
+ return aRequest.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch (ex) { }
+ }
+
+ if (aRequest && aRequest.loadGroup && aRequest.loadGroup.notificationCallbacks) {
+ try {
+ return aRequest.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch (ex) { }
+ }
+
+ return null;
+ },
+
+ _getWindowForRequest: function ua_getWindowForRequest(aRequest) {
+ let loadContext = this._getRequestLoadContext(aRequest);
+ if (loadContext) {
+ try {
+ return loadContext.associatedWindow;
+ } catch (e) {
+ // loadContext.associatedWindow can throw when there's no window
+ }
+ }
+ return null;
+ },
+
+ observe: function ua_observe(aSubject, aTopic, aData) {
+ if (aTopic === "DesktopMode:Change") {
+ let args = JSON.parse(aData);
+ let tab = BrowserApp.getTabForId(args.tabId);
+ if (tab) {
+ tab.reloadWithMode(args.desktopMode);
+ }
+ }
+ }
+};
+
+
+function nsBrowserAccess() {
+}
+
+nsBrowserAccess.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow]),
+
+ _getBrowser: function _getBrowser(aURI, aOpener, aWhere, aFlags) {
+ let isExternal = !!(aFlags & Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL);
+ if (isExternal && aURI && aURI.schemeIs("chrome"))
+ return null;
+
+ let loadflags = isExternal ?
+ Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL :
+ Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW) {
+ if (isExternal) {
+ aWhere = Services.prefs.getIntPref("browser.link.open_external");
+ } else {
+ aWhere = Services.prefs.getIntPref("browser.link.open_newwindow");
+ }
+ }
+
+ Services.io.offline = false;
+
+ let referrer;
+ if (aOpener) {
+ try {
+ let location = aOpener.location;
+ referrer = Services.io.newURI(location, null, null);
+ } catch(e) { }
+ }
+
+ let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+ let pinned = false;
+
+ if (aURI && aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB) {
+ pinned = true;
+ let spec = aURI.spec;
+ let tabs = BrowserApp.tabs;
+ for (let i = 0; i < tabs.length; i++) {
+ let appOrigin = ss.getTabValue(tabs[i], "appOrigin");
+ if (appOrigin == spec) {
+ let tab = tabs[i];
+ BrowserApp.selectTab(tab);
+ return tab.browser;
+ }
+ }
+ }
+
+ // If OPEN_SWITCHTAB was not handled above, we need to open a new tab,
+ // along with other OPEN_ values that create a new tab.
+ let newTab = (aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW ||
+ aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB ||
+ aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB);
+ let isPrivate = false;
+
+ if (newTab) {
+ let parentId = -1;
+ if (!isExternal && aOpener) {
+ let parent = BrowserApp.getTabForWindow(aOpener.top);
+ if (parent) {
+ parentId = parent.id;
+ isPrivate = PrivateBrowsingUtils.isBrowserPrivate(parent.browser);
+ }
+ }
+
+ let openerWindow = (aFlags & Ci.nsIBrowserDOMWindow.OPEN_NO_OPENER) ? null : aOpener;
+ // BrowserApp.addTab calls loadURIWithFlags with the appropriate params
+ let tab = BrowserApp.addTab(aURI ? aURI.spec : "about:blank", { flags: loadflags,
+ referrerURI: referrer,
+ external: isExternal,
+ parentId: parentId,
+ opener: openerWindow,
+ selected: true,
+ isPrivate: isPrivate,
+ pinned: pinned });
+
+ return tab.browser;
+ }
+
+ // OPEN_CURRENTWINDOW and illegal values
+ let browser = BrowserApp.selectedBrowser;
+ if (aURI && browser) {
+ browser.loadURIWithFlags(aURI.spec, loadflags, referrer, null, null);
+ }
+
+ return browser;
+ },
+
+ openURI: function browser_openURI(aURI, aOpener, aWhere, aFlags) {
+ let browser = this._getBrowser(aURI, aOpener, aWhere, aFlags);
+ return browser ? browser.contentWindow : null;
+ },
+
+ openURIInFrame: function browser_openURIInFrame(aURI, aParams, aWhere, aFlags) {
+ let browser = this._getBrowser(aURI, null, aWhere, aFlags);
+ return browser ? browser.QueryInterface(Ci.nsIFrameLoaderOwner) : null;
+ },
+
+ isTabContentWindow: function(aWindow) {
+ return BrowserApp.getBrowserForWindow(aWindow) != null;
+ },
+
+ canClose() {
+ return BrowserUtils.canCloseWindow(window);
+ },
+};
+
+
+function Tab(aURL, aParams) {
+ this.filter = null;
+ this.browser = null;
+ this.id = 0;
+ this.lastTouchedAt = Date.now();
+ this._zoom = 1.0;
+ this._drawZoom = 1.0;
+ this._restoreZoom = false;
+ this.userScrollPos = { x: 0, y: 0 };
+ this.contentDocumentIsDisplayed = true;
+ this.pluginDoorhangerTimeout = null;
+ this.shouldShowPluginDoorhanger = true;
+ this.clickToPlayPluginsActivated = false;
+ this.desktopMode = false;
+ this.originalURI = null;
+ this.hasTouchListener = false;
+ this.playingAudio = false;
+
+ this.create(aURL, aParams);
+}
+
+/*
+ * Sanity limit for URIs passed to UI code.
+ *
+ * 2000 is the typical industry limit, largely due to older IE versions.
+ *
+ * We use 25000, so we'll allow almost any value through.
+ *
+ * Still, this truncation doesn't affect history, so this is only a practical
+ * concern in two ways: the truncated value is used when editing URIs, and as
+ * the key for favicon fetches.
+ */
+const MAX_URI_LENGTH = 25000;
+
+/*
+ * Similar restriction for titles. This is only a display concern.
+ */
+const MAX_TITLE_LENGTH = 255;
+
+/**
+ * Ensure that a string is of a sane length.
+ */
+function truncate(text, max) {
+ if (!text || !max) {
+ return text;
+ }
+
+ if (text.length <= max) {
+ return text;
+ }
+
+ return text.slice(0, max) + "…";
+}
+
+Tab.prototype = {
+ create: function(aURL, aParams) {
+ if (this.browser)
+ return;
+
+ aParams = aParams || {};
+
+ this.browser = document.createElement("browser");
+ this.browser.setAttribute("type", "content-targetable");
+ this.browser.setAttribute("messagemanagergroup", "browsers");
+
+ if (Preferences.get("browser.tabs.remote.force-enable", false)) {
+ this.browser.setAttribute("remote", "true");
+ }
+
+ this.browser.permanentKey = {};
+
+ // Check if we have a "parent" window which we need to set as our opener
+ if ("opener" in aParams) {
+ this.browser.presetOpenerWindow(aParams.opener);
+ }
+
+ // Make sure the previously selected panel remains selected. The selected panel of a deck is
+ // not stable when panels are added.
+ let selectedPanel = BrowserApp.deck.selectedPanel;
+ BrowserApp.deck.insertBefore(this.browser, aParams.sibling || null);
+ BrowserApp.deck.selectedPanel = selectedPanel;
+
+ let attrs = {};
+ if (BrowserApp.manifestUrl) {
+ let appsService = Cc["@mozilla.org/AppsService;1"].getService(Ci.nsIAppsService);
+ let manifest = appsService.getAppByManifestURL(BrowserApp.manifestUrl);
+ if (manifest) {
+ let app = manifest.QueryInterface(Ci.mozIApplication);
+ this.browser.docShell.frameType = Ci.nsIDocShell.FRAME_TYPE_APP;
+ attrs['appId'] = app.localId;
+ }
+ }
+
+ // Must be called after appendChild so the docShell has been created.
+ this.setActive(false);
+
+ let isPrivate = ("isPrivate" in aParams) && aParams.isPrivate;
+ if (isPrivate) {
+ attrs['privateBrowsingId'] = 1;
+ }
+
+ this.browser.docShell.setOriginAttributes(attrs);
+
+ // Set the new docShell load flags based on network state.
+ if (Tabs.useCache) {
+ this.browser.docShell.defaultLoadFlags |= Ci.nsIRequest.LOAD_FROM_CACHE;
+ }
+
+ this.browser.stop();
+
+ // Only set tab uri if uri is valid
+ let uri = null;
+ let title = aParams.title || aURL;
+ try {
+ uri = Services.io.newURI(aURL, null, null).spec;
+ } catch (e) {}
+
+ // When the tab is stubbed from Java, there's a window between the stub
+ // creation and the tab creation in Gecko where the stub could be removed
+ // or the selected tab can change (which is easiest to hit during startup).
+ // To prevent these races, we need to differentiate between tab stubs from
+ // Java and new tabs from Gecko.
+ let stub = false;
+
+ if (!aParams.zombifying) {
+ if ("tabID" in aParams) {
+ this.id = aParams.tabID;
+ stub = true;
+ } else {
+ let jenv = JNI.GetForThread();
+ let jTabs = JNI.LoadClass(jenv, "org.mozilla.gecko.Tabs", {
+ static_methods: [
+ { name: "getNextTabId", sig: "()I" }
+ ],
+ });
+ this.id = jTabs.getNextTabId();
+ JNI.UnloadClasses(jenv);
+ }
+
+ this.desktopMode = ("desktopMode" in aParams) ? aParams.desktopMode : false;
+
+ let message = {
+ type: "Tab:Added",
+ tabID: this.id,
+ uri: truncate(uri, MAX_URI_LENGTH),
+ parentId: ("parentId" in aParams) ? aParams.parentId : -1,
+ tabIndex: ("tabIndex" in aParams) ? aParams.tabIndex : -1,
+ external: ("external" in aParams) ? aParams.external : false,
+ selected: ("selected" in aParams || aParams.cancelEditMode === true) ? aParams.selected : true,
+ cancelEditMode: aParams.cancelEditMode === true,
+ title: truncate(title, MAX_TITLE_LENGTH),
+ delayLoad: aParams.delayLoad || false,
+ desktopMode: this.desktopMode,
+ isPrivate: isPrivate,
+ stub: stub
+ };
+ Messaging.sendRequest(message);
+ }
+
+ let flags = Ci.nsIWebProgress.NOTIFY_STATE_ALL |
+ Ci.nsIWebProgress.NOTIFY_LOCATION |
+ Ci.nsIWebProgress.NOTIFY_SECURITY;
+ this.filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"].createInstance(Ci.nsIWebProgress);
+ this.filter.addProgressListener(this, flags)
+ this.browser.addProgressListener(this.filter, flags);
+ this.browser.sessionHistory.addSHistoryListener(this);
+
+ this.browser.addEventListener("DOMContentLoaded", this, true);
+ this.browser.addEventListener("DOMFormHasPassword", this, true);
+ this.browser.addEventListener("DOMInputPasswordAdded", this, true);
+ this.browser.addEventListener("DOMLinkAdded", this, true);
+ this.browser.addEventListener("DOMLinkChanged", this, true);
+ this.browser.addEventListener("DOMMetaAdded", this, false);
+ this.browser.addEventListener("DOMTitleChanged", this, true);
+ this.browser.addEventListener("DOMAudioPlaybackStarted", this, true);
+ this.browser.addEventListener("DOMAudioPlaybackStopped", this, true);
+ this.browser.addEventListener("DOMWindowClose", this, true);
+ this.browser.addEventListener("DOMWillOpenModalDialog", this, true);
+ this.browser.addEventListener("DOMAutoComplete", this, true);
+ this.browser.addEventListener("blur", this, true);
+ this.browser.addEventListener("pageshow", this, true);
+ this.browser.addEventListener("MozApplicationManifest", this, true);
+ this.browser.addEventListener("TabPreZombify", this, true);
+
+ // Note that the XBL binding is untrusted
+ this.browser.addEventListener("PluginBindingAttached", this, true, true);
+ this.browser.addEventListener("VideoBindingAttached", this, true, true);
+ this.browser.addEventListener("VideoBindingCast", this, true, true);
+
+ Services.obs.addObserver(this, "before-first-paint", false);
+ Services.obs.addObserver(this, "media-playback", false);
+ Services.obs.addObserver(this, "media-playback-resumed", false);
+
+ // Always intialise new tabs with basic session store data to avoid
+ // problems with functions that always expect it to be present
+ this.browser.__SS_data = {
+ entries: [{
+ url: aURL,
+ title: truncate(title, MAX_TITLE_LENGTH)
+ }],
+ index: 1,
+ desktopMode: this.desktopMode,
+ isPrivate: isPrivate
+ };
+
+ if (aParams.delayLoad) {
+ // If this is a zombie tab, mark the browser for delay loading, which will
+ // restore the tab when selected using the session data added above
+ this.browser.__SS_restore = true;
+ } else {
+ let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ let postData = ("postData" in aParams && aParams.postData) ? aParams.postData.value : null;
+ let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null;
+ let charset = "charset" in aParams ? aParams.charset : null;
+
+ // The search term the user entered to load the current URL
+ this.userRequested = "userRequested" in aParams ? aParams.userRequested : "";
+ this.isSearch = "isSearch" in aParams ? aParams.isSearch : false;
+
+ try {
+ this.browser.loadURIWithFlags(aURL, flags, referrerURI, charset, postData);
+ } catch(e) {
+ let message = {
+ type: "Content:LoadError",
+ tabID: this.id
+ };
+ Messaging.sendRequest(message);
+ dump("Handled load error: " + e);
+ }
+ }
+ },
+
+ /**
+ * Reloads the tab with the desktop mode setting.
+ */
+ reloadWithMode: function (aDesktopMode) {
+ // notify desktopmode for PIDOMWindow
+ let win = this.browser.contentWindow;
+ let dwi = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ dwi.setDesktopModeViewport(aDesktopMode);
+
+ // Set desktop mode for tab and send change to Java
+ if (this.desktopMode != aDesktopMode) {
+ this.desktopMode = aDesktopMode;
+ Messaging.sendRequest({
+ type: "DesktopMode:Changed",
+ desktopMode: aDesktopMode,
+ tabID: this.id
+ });
+ }
+
+ // Only reload the page for http/https schemes
+ let currentURI = this.browser.currentURI;
+ if (!currentURI.schemeIs("http") && !currentURI.schemeIs("https"))
+ return;
+
+ let url = currentURI.spec;
+ // We need LOAD_FLAGS_BYPASS_CACHE here since we're changing the User-Agent
+ // string, and servers typically don't use the Vary: User-Agent header, so
+ // not doing this means that we'd get some of the previously cached content.
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE |
+ Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
+ if (this.originalURI && !this.originalURI.equals(currentURI)) {
+ // We were redirected; reload the original URL
+ url = this.originalURI.spec;
+ }
+
+ this.browser.docShell.loadURI(url, flags, null, null, null);
+ },
+
+ destroy: function() {
+ if (!this.browser)
+ return;
+
+ this.browser.removeProgressListener(this.filter);
+ this.filter.removeProgressListener(this);
+ this.filter = null;
+ this.browser.sessionHistory.removeSHistoryListener(this);
+
+ this.browser.removeEventListener("DOMContentLoaded", this, true);
+ this.browser.removeEventListener("DOMFormHasPassword", this, true);
+ this.browser.removeEventListener("DOMInputPasswordAdded", this, true);
+ this.browser.removeEventListener("DOMLinkAdded", this, true);
+ this.browser.removeEventListener("DOMLinkChanged", this, true);
+ this.browser.removeEventListener("DOMMetaAdded", this, false);
+ this.browser.removeEventListener("DOMTitleChanged", this, true);
+ this.browser.removeEventListener("DOMAudioPlaybackStarted", this, true);
+ this.browser.removeEventListener("DOMAudioPlaybackStopped", this, true);
+ this.browser.removeEventListener("DOMWindowClose", this, true);
+ this.browser.removeEventListener("DOMWillOpenModalDialog", this, true);
+ this.browser.removeEventListener("DOMAutoComplete", this, true);
+ this.browser.removeEventListener("blur", this, true);
+ this.browser.removeEventListener("pageshow", this, true);
+ this.browser.removeEventListener("MozApplicationManifest", this, true);
+ this.browser.removeEventListener("TabPreZombify", this, true);
+
+ this.browser.removeEventListener("PluginBindingAttached", this, true, true);
+ this.browser.removeEventListener("VideoBindingAttached", this, true, true);
+ this.browser.removeEventListener("VideoBindingCast", this, true, true);
+
+ Services.obs.removeObserver(this, "before-first-paint");
+ Services.obs.removeObserver(this, "media-playback", false);
+ Services.obs.removeObserver(this, "media-playback-resumed", false);
+
+ // Make sure the previously selected panel remains selected. The selected panel of a deck is
+ // not stable when panels are removed.
+ let selectedPanel = BrowserApp.deck.selectedPanel;
+ BrowserApp.deck.removeChild(this.browser);
+ BrowserApp.deck.selectedPanel = selectedPanel;
+
+ this.browser = null;
+ },
+
+ // This should be called to update the browser when the tab gets selected/unselected
+ setActive: function setActive(aActive) {
+ if (!this.browser || !this.browser.docShell)
+ return;
+
+ this.lastTouchedAt = Date.now();
+
+ if (aActive) {
+ this.browser.setAttribute("type", "content-primary");
+ this.browser.focus();
+ this.browser.docShellIsActive = true;
+ Reader.updatePageAction(this);
+ ExternalApps.updatePageAction(this.browser.currentURI, this.browser.contentDocument);
+ } else {
+ this.browser.setAttribute("type", "content-targetable");
+ this.browser.docShellIsActive = false;
+ this.browser.blur();
+ }
+ },
+
+ getActive: function getActive() {
+ return this.browser.docShellIsActive;
+ },
+
+ // These constants are used to prioritize high quality metadata over low quality data, so that
+ // we can collect data as we find meta tags, and replace low quality metadata with higher quality
+ // matches. For instance a msApplicationTile icon is a better tile image than an og:image tag.
+ METADATA_GOOD_MATCH: 10,
+ METADATA_NORMAL_MATCH: 1,
+
+ addMetadata: function(type, value, quality = 1) {
+ if (!this.metatags) {
+ this.metatags = {
+ url: this.browser.currentURI.specIgnoringRef
+ };
+ }
+
+ if (type == "touchIconList") {
+ if (!this.metatags['touchIconList']) {
+ this.metatags['touchIconList'] = {};
+ }
+ this.metatags.touchIconList[quality] = value;
+ } else if (!this.metatags[type] || this.metatags[type + "_quality"] < quality) {
+ this.metatags[type] = value;
+ this.metatags[type + "_quality"] = quality;
+ }
+ },
+
+ sanitizeRelString: function(linkRel) {
+ // Sanitize the rel string
+ let list = [];
+ if (linkRel) {
+ list = linkRel.toLowerCase().split(/\s+/);
+ let hash = {};
+ list.forEach(function(value) { hash[value] = true; });
+ list = [];
+ for (let rel in hash)
+ list.push("[" + rel + "]");
+ }
+ return list;
+ },
+
+ makeFaviconMessage: function(eventTarget) {
+ // We want to get the largest icon size possible for our UI.
+ let maxSize = 0;
+
+ // We use the sizes attribute if available
+ // see http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#rel-icon
+ if (eventTarget.hasAttribute("sizes")) {
+ let sizes = eventTarget.getAttribute("sizes").toLowerCase();
+
+ if (sizes == "any") {
+ // Since Java expects an integer, use -1 to represent icons with sizes="any"
+ maxSize = -1;
+ } else {
+ let tokens = sizes.split(" ");
+ tokens.forEach(function(token) {
+ // TODO: check for invalid tokens
+ let [w, h] = token.split("x");
+ maxSize = Math.max(maxSize, Math.max(w, h));
+ });
+ }
+ }
+ return {
+ type: "Link:Favicon",
+ tabID: this.id,
+ href: resolveGeckoURI(eventTarget.href),
+ size: maxSize,
+ mime: eventTarget.getAttribute("type") || ""
+ };
+ },
+
+ makeFeedMessage: function(eventTarget, targetType) {
+ try {
+ // urlSecurityCeck will throw if things are not OK
+ ContentAreaUtils.urlSecurityCheck(eventTarget.href,
+ eventTarget.ownerDocument.nodePrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
+
+ if (!this.browser.feeds)
+ this.browser.feeds = [];
+
+ this.browser.feeds.push({
+ href: eventTarget.href,
+ title: eventTarget.title,
+ type: targetType
+ });
+
+ return {
+ type: "Link:Feed",
+ tabID: this.id
+ };
+ } catch (e) {
+ return null;
+ }
+ },
+
+ sendOpenSearchMessage: function(eventTarget) {
+ let type = eventTarget.type && eventTarget.type.toLowerCase();
+ // Replace all starting or trailing spaces or spaces before "*;" globally w/ "".
+ type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
+
+ // Check that type matches opensearch.
+ let isOpenSearch = (type == "application/opensearchdescription+xml");
+ if (isOpenSearch && eventTarget.title && /^(?:https?|ftp):/i.test(eventTarget.href)) {
+ Services.search.init(() => {
+ let visibleEngines = Services.search.getVisibleEngines();
+ // NOTE: Engines are currently identified by name, but this can be changed
+ // when Engines are identified by URL (see bug 335102).
+ if (visibleEngines.some(function(e) {
+ return e.name == eventTarget.title;
+ })) {
+ // This engine is already present, do nothing.
+ return null;
+ }
+
+ if (this.browser.engines) {
+ // This engine has already been handled, do nothing.
+ if (this.browser.engines.some(function(e) {
+ return e.url == eventTarget.href;
+ })) {
+ return null;
+ }
+ } else {
+ this.browser.engines = [];
+ }
+
+ // Get favicon.
+ let iconURL = eventTarget.ownerDocument.documentURIObject.prePath + "/favicon.ico";
+
+ let newEngine = {
+ title: eventTarget.title,
+ url: eventTarget.href,
+ iconURL: iconURL
+ };
+
+ this.browser.engines.push(newEngine);
+
+ // Don't send a message to display engines if we've already handled an engine.
+ if (this.browser.engines.length > 1)
+ return null;
+
+ // Broadcast message that this tab contains search engines that should be visible.
+ Messaging.sendRequest({
+ type: "Link:OpenSearch",
+ tabID: this.id,
+ visible: true
+ });
+ });
+ }
+ },
+
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case "DOMContentLoaded": {
+ let target = aEvent.originalTarget;
+
+ // ignore on frames and other documents
+ if (target != this.browser.contentDocument)
+ return;
+
+ // Sample the background color of the page and pass it along. (This is used to draw the
+ // checkerboard.) Right now we don't detect changes in the background color after this
+ // event fires; it's not clear that doing so is worth the effort.
+ var backgroundColor = null;
+ try {
+ let { contentDocument, contentWindow } = this.browser;
+ let computedStyle = contentWindow.getComputedStyle(contentDocument.body);
+ backgroundColor = computedStyle.backgroundColor;
+ } catch (e) {
+ // Ignore. Catching and ignoring exceptions here ensures that Talos succeeds.
+ }
+
+ let docURI = target.documentURI;
+ let errorType = "";
+ if (docURI.startsWith("about:certerror")) {
+ errorType = "certerror";
+ }
+ else if (docURI.startsWith("about:blocked")) {
+ errorType = "blocked";
+ }
+ else if (docURI.startsWith("about:neterror")) {
+ let error = docURI.search(/e\=/);
+ let duffUrl = docURI.search(/\&u\=/);
+ let errorExtra = decodeURIComponent(docURI.slice(error + 2, duffUrl));
+ // Here is a list of errorExtra types (et_*)
+ // http://mxr.mozilla.org/mozilla-central/source/mobile/android/chrome/content/netError.xhtml#287
+ UITelemetry.addEvent("neterror.1", "content", null, errorExtra);
+ errorType = "neterror";
+ }
+
+ // Attach a listener to watch for "click" events bubbling up from error
+ // pages and other similar page. This lets us fix bugs like 401575 which
+ // require error page UI to do privileged things, without letting error
+ // pages have any privilege themselves.
+ if (docURI.startsWith("about:neterror")) {
+ NetErrorHelper.attachToBrowser(this.browser);
+ }
+
+ Messaging.sendRequest({
+ type: "DOMContentLoaded",
+ tabID: this.id,
+ bgColor: backgroundColor,
+ errorType: errorType,
+ metadata: this.metatags,
+ });
+
+ // Reset isSearch so that the userRequested term will be erased on next page load
+ this.metatags = null;
+
+ if (docURI.startsWith("about:certerror") || docURI.startsWith("about:blocked")) {
+ this.browser.addEventListener("click", ErrorPageEventHandler, true);
+ let listener = function() {
+ this.browser.removeEventListener("click", ErrorPageEventHandler, true);
+ this.browser.removeEventListener("pagehide", listener, true);
+ }.bind(this);
+
+ this.browser.addEventListener("pagehide", listener, true);
+ }
+
+ if (AppConstants.NIGHTLY_BUILD || AppConstants.MOZ_ANDROID_ACTIVITY_STREAM) {
+ WebsiteMetadata.parseAsynchronously(this.browser.contentDocument);
+ }
+
+ break;
+ }
+
+ case "DOMFormHasPassword": {
+ LoginManagerContent.onDOMFormHasPassword(aEvent,
+ this.browser.contentWindow);
+
+ // Send logins for this hostname to Java.
+ let hostname = aEvent.target.baseURIObject.prePath;
+ let foundLogins = Services.logins.findLogins({}, hostname, "", "");
+ if (foundLogins.length > 0) {
+ let displayHost = IdentityHandler.getEffectiveHost();
+ let title = { text: displayHost, resource: hostname };
+ let selectObj = { title: title, logins: foundLogins };
+ Messaging.sendRequest({ type: "Doorhanger:Logins", data: selectObj });
+ }
+ break;
+ }
+
+ case "DOMInputPasswordAdded": {
+ LoginManagerContent.onDOMInputPasswordAdded(aEvent,
+ this.browser.contentWindow);
+ }
+
+ case "DOMMetaAdded":
+ let target = aEvent.originalTarget;
+ let browser = BrowserApp.getBrowserForDocument(target.ownerDocument);
+
+ switch (target.name) {
+ case "msapplication-TileImage":
+ this.addMetadata("tileImage", browser.currentURI.resolve(target.content), this.METADATA_GOOD_MATCH);
+ break;
+ case "msapplication-TileColor":
+ this.addMetadata("tileColor", target.content, this.METADATA_GOOD_MATCH);
+ break;
+ }
+
+ break;
+
+ case "DOMLinkAdded":
+ case "DOMLinkChanged": {
+ let jsonMessage = null;
+ let target = aEvent.originalTarget;
+ if (!target.href || target.disabled)
+ return;
+
+ // Ignore on frames and other documents
+ if (target.ownerDocument != this.browser.contentDocument)
+ return;
+
+ // Sanitize rel link
+ let list = this.sanitizeRelString(target.rel);
+ if (list.indexOf("[icon]") != -1) {
+ jsonMessage = this.makeFaviconMessage(target);
+ } else if (list.indexOf("[apple-touch-icon]") != -1 ||
+ list.indexOf("[apple-touch-icon-precomposed]") != -1) {
+ jsonMessage = this.makeFaviconMessage(target);
+ jsonMessage['type'] = 'Link:Touchicon';
+ this.addMetadata("touchIconList", jsonMessage.href, jsonMessage.size);
+ } else if (list.indexOf("[alternate]") != -1 && aEvent.type == "DOMLinkAdded") {
+ let type = target.type.toLowerCase().replace(/^\s+|\s*(?:;.*)?$/g, "");
+ let isFeed = (type == "application/rss+xml" || type == "application/atom+xml");
+
+ if (!isFeed)
+ return;
+
+ jsonMessage = this.makeFeedMessage(target, type);
+ } else if (list.indexOf("[search]") != -1 && aEvent.type == "DOMLinkAdded") {
+ this.sendOpenSearchMessage(target);
+ }
+ if (!jsonMessage)
+ return;
+
+ Messaging.sendRequest(jsonMessage);
+ break;
+ }
+
+ case "DOMTitleChanged": {
+ if (!aEvent.isTrusted)
+ return;
+
+ // ignore on frames and other documents
+ if (aEvent.originalTarget != this.browser.contentDocument)
+ return;
+
+ Messaging.sendRequest({
+ type: "DOMTitleChanged",
+ tabID: this.id,
+ title: truncate(aEvent.target.title, MAX_TITLE_LENGTH)
+ });
+ break;
+ }
+
+ case "TabPreZombify": {
+ if (!this.playingAudio) {
+ return;
+ }
+ // Fall through to the DOMAudioPlayback events, so the
+ // audio playback indicator gets reset upon zombification.
+ }
+ case "DOMAudioPlaybackStarted":
+ case "DOMAudioPlaybackStopped": {
+ if (!Services.prefs.getBoolPref("browser.tabs.showAudioPlayingIcon") ||
+ !aEvent.isTrusted) {
+ return;
+ }
+
+ let browser = aEvent.originalTarget;
+ if (browser != this.browser) {
+ return;
+ }
+
+ this.playingAudio = aEvent.type === "DOMAudioPlaybackStarted";
+
+ Messaging.sendRequest({
+ type: "Tab:AudioPlayingChange",
+ tabID: this.id,
+ isAudioPlaying: this.playingAudio
+ });
+ return;
+ }
+
+ case "DOMWindowClose": {
+ if (!aEvent.isTrusted)
+ return;
+
+ // Find the relevant tab, and close it from Java
+ if (this.browser.contentWindow == aEvent.target) {
+ aEvent.preventDefault();
+
+ Messaging.sendRequest({
+ type: "Tab:Close",
+ tabID: this.id
+ });
+ }
+ break;
+ }
+
+ case "DOMWillOpenModalDialog": {
+ if (!aEvent.isTrusted)
+ return;
+
+ // We're about to open a modal dialog, make sure the opening
+ // tab is brought to the front.
+ let tab = BrowserApp.getTabForWindow(aEvent.target.top);
+ BrowserApp.selectTab(tab);
+ break;
+ }
+
+ case "DOMAutoComplete":
+ case "blur": {
+ LoginManagerContent.onUsernameInput(aEvent);
+ break;
+ }
+
+ case "PluginBindingAttached": {
+ PluginHelper.handlePluginBindingAttached(this, aEvent);
+ break;
+ }
+
+ case "VideoBindingAttached": {
+ CastingApps.handleVideoBindingAttached(this, aEvent);
+ break;
+ }
+
+ case "VideoBindingCast": {
+ CastingApps.handleVideoBindingCast(this, aEvent);
+ break;
+ }
+
+ case "MozApplicationManifest": {
+ OfflineApps.offlineAppRequested(aEvent.originalTarget.defaultView);
+ break;
+ }
+
+ case "pageshow": {
+ LoginManagerContent.onPageShow(aEvent, this.browser.contentWindow);
+
+ // The rest of this only handles pageshow for the top-level document.
+ if (aEvent.originalTarget.defaultView != this.browser.contentWindow)
+ return;
+
+ let target = aEvent.originalTarget;
+ let docURI = target.documentURI;
+ if (!docURI.startsWith("about:neterror") && !this.isSearch) {
+ // If this wasn't an error page and the user isn't search, don't retain the typed entry
+ this.userRequested = "";
+ }
+
+ Messaging.sendRequest({
+ type: "Content:PageShow",
+ tabID: this.id,
+ userRequested: this.userRequested,
+ fromCache: Tabs.useCache
+ });
+
+ this.isSearch = false;
+
+ if (!aEvent.persisted && Services.prefs.getBoolPref("browser.ui.linkify.phone")) {
+ if (!this._linkifier)
+ this._linkifier = new Linkifier();
+ this._linkifier.linkifyNumbers(this.browser.contentWindow.document);
+ }
+
+ // Update page actions for helper apps.
+ let uri = this.browser.currentURI;
+ if (BrowserApp.selectedTab == this) {
+ if (ExternalApps.shouldCheckUri(uri)) {
+ ExternalApps.updatePageAction(uri, this.browser.contentDocument);
+ } else {
+ ExternalApps.clearPageAction();
+ }
+ }
+ }
+ }
+ },
+
+ onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
+ let contentWin = aWebProgress.DOMWindow;
+ if (contentWin != contentWin.top)
+ return;
+
+ // Filter optimization: Only really send NETWORK state changes to Java listener
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
+ if (AppConstants.NIGHTLY_BUILD && (aStateFlags & Ci.nsIWebProgressListener.STATE_START)) {
+ Profiler.AddMarker("Load start: " + aRequest.QueryInterface(Ci.nsIChannel).originalURI.spec);
+ } else if (AppConstants.NIGHTLY_BUILD && (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) && !aWebProgress.isLoadingDocument) {
+ Profiler.AddMarker("Load stop: " + aRequest.QueryInterface(Ci.nsIChannel).originalURI.spec);
+ }
+
+ if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) && aWebProgress.isLoadingDocument) {
+ // We may receive a document stop event while a document is still loading
+ // (such as when doing URI fixup). Don't notify Java UI in these cases.
+ return;
+ }
+
+ // Clear page-specific opensearch engines and feeds for a new request.
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_START && aRequest && aWebProgress.isTopLevel) {
+ this.browser.engines = null;
+ this.browser.feeds = null;
+ }
+
+ // true if the page loaded successfully (i.e., no 404s or other errors)
+ let success = false;
+ let uri = "";
+ try {
+ // Remember original URI for UA changes on redirected pages
+ this.originalURI = aRequest.QueryInterface(Components.interfaces.nsIChannel).originalURI;
+
+ if (this.originalURI != null)
+ uri = this.originalURI.spec;
+ } catch (e) { }
+ try {
+ success = aRequest.QueryInterface(Components.interfaces.nsIHttpChannel).requestSucceeded;
+ } catch (e) {
+ // If the request does not handle the nsIHttpChannel interface, use nsIRequest's success
+ // status. Used for local files. See bug 948849.
+ success = aRequest.status == 0;
+ }
+
+ // Check to see if we restoring the content from a previous presentation (session)
+ // since there should be no real network activity
+ let restoring = (aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) > 0;
+
+ let message = {
+ type: "Content:StateChange",
+ tabID: this.id,
+ uri: truncate(uri, MAX_URI_LENGTH),
+ state: aStateFlags,
+ restoring: restoring,
+ success: success
+ };
+ Messaging.sendRequest(message);
+ }
+ },
+
+ onLocationChange: function(aWebProgress, aRequest, aLocationURI, aFlags) {
+ let contentWin = aWebProgress.DOMWindow;
+
+ // Browser webapps may load content inside iframes that can not reach across the app/frame boundary
+ // i.e. even though the page is loaded in an iframe window.top != webapp
+ // Make cure this window is a top level tab before moving on.
+ if (BrowserApp.getBrowserForWindow(contentWin) == null)
+ return;
+
+ this._hostChanged = true;
+
+ let fixedURI = aLocationURI;
+ try {
+ fixedURI = URIFixup.createExposableURI(aLocationURI);
+ } catch (ex) { }
+
+ // In restricted profiles, we refuse to let you open various urls.
+ if (!ParentalControls.isAllowed(ParentalControls.BROWSE, fixedURI)) {
+ aRequest.cancel(Cr.NS_BINDING_ABORTED);
+
+ this.browser.docShell.displayLoadError(Cr.NS_ERROR_UNKNOWN_PROTOCOL, fixedURI, null);
+ }
+
+ let contentType = contentWin.document.contentType;
+
+ // If fixedURI matches browser.lastURI, we assume this isn't a real location
+ // change but rather a spurious addition like a wyciwyg URI prefix. See Bug 747883.
+ // Note that we have to ensure fixedURI is not the same as aLocationURI so we
+ // don't false-positive page reloads as spurious additions.
+ let sameDocument = (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) != 0 ||
+ ((this.browser.lastURI != null) && fixedURI.equals(this.browser.lastURI) && !fixedURI.equals(aLocationURI));
+ this.browser.lastURI = fixedURI;
+
+ // Let the reader logic know about same document changes because we won't get a DOMContentLoaded
+ // or pageshow event, but we'll still want to update the reader view button to account for this change.
+ // This mirrors the desktop logic in TabsProgressListener.
+ if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
+ this.browser.messageManager.sendAsyncMessage("Reader:PushState", {isArticle: this.browser.isArticle});
+ }
+
+ // Reset state of click-to-play plugin notifications.
+ clearTimeout(this.pluginDoorhangerTimeout);
+ this.pluginDoorhangerTimeout = null;
+ this.shouldShowPluginDoorhanger = true;
+ this.clickToPlayPluginsActivated = false;
+
+ let documentURI = contentWin.document.documentURIObject.spec;
+
+ // If reader mode, get the base domain for the original url.
+ let strippedURI = this._stripAboutReaderURL(documentURI);
+
+ // Borrowed from desktop Firefox: http://hg.mozilla.org/mozilla-central/annotate/72835344333f/browser/base/content/urlbarBindings.xml#l236
+ let matchedURL = strippedURI.match(/^((?:[a-z]+:\/\/)?(?:[^\/]+@)?)(.+?)(?::\d+)?(?:\/|$)/);
+ let baseDomain = "";
+ if (matchedURL) {
+ var domain = "";
+ [, , domain] = matchedURL;
+
+ try {
+ baseDomain = Services.eTLD.getBaseDomainFromHost(domain);
+ if (!domain.endsWith(baseDomain)) {
+ // getBaseDomainFromHost converts its resultant to ACE.
+ let IDNService = Cc["@mozilla.org/network/idn-service;1"].getService(Ci.nsIIDNService);
+ baseDomain = IDNService.convertACEtoUTF8(baseDomain);
+ }
+ } catch (e) {}
+ }
+
+ // If we are navigating to a new location with a different host,
+ // clear any URL origin that might have been pinned to this tab.
+ let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+ let appOrigin = ss.getTabValue(this, "appOrigin");
+ if (appOrigin) {
+ let originHost = "";
+ try {
+ originHost = Services.io.newURI(appOrigin, null, null).host;
+ } catch (e if (e.result == Cr.NS_ERROR_FAILURE)) {
+ // NS_ERROR_FAILURE can be thrown by nsIURI.host if the URI scheme does not possess a host - in this case
+ // we just act as if we have an empty host.
+ }
+ if (originHost != aLocationURI.host) {
+ // Note: going 'back' will not make this tab pinned again
+ ss.deleteTabValue(this, "appOrigin");
+ }
+ }
+
+ // Update the page actions URI for helper apps.
+ if (BrowserApp.selectedTab == this) {
+ ExternalApps.updatePageActionUri(fixedURI);
+ }
+
+ let webNav = contentWin.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
+
+ let message = {
+ type: "Content:LocationChange",
+ tabID: this.id,
+ uri: truncate(fixedURI.spec, MAX_URI_LENGTH),
+ userRequested: this.userRequested || "",
+ baseDomain: baseDomain,
+ contentType: (contentType ? contentType : ""),
+ sameDocument: sameDocument,
+
+ historyIndex: webNav.sessionHistory.index,
+ historySize: webNav.sessionHistory.count,
+ canGoBack: webNav.canGoBack,
+ canGoForward: webNav.canGoForward,
+ };
+
+ Messaging.sendRequest(message);
+
+ if (!sameDocument) {
+ // XXX This code assumes that this is the earliest hook we have at which
+ // browser.contentDocument is changed to the new document we're loading
+ this.contentDocumentIsDisplayed = false;
+ this.hasTouchListener = false;
+ Services.obs.notifyObservers(this.browser, "Session:NotifyLocationChange", null);
+ }
+ },
+
+ _stripAboutReaderURL: function (url) {
+ return ReaderMode.getOriginalUrl(url) || url;
+ },
+
+ // Properties used to cache security state used to update the UI
+ _state: null,
+ _hostChanged: false, // onLocationChange will flip this bit
+
+ onSecurityChange: function(aWebProgress, aRequest, aState) {
+ // Don't need to do anything if the data we use to update the UI hasn't changed
+ if (this._state == aState && !this._hostChanged)
+ return;
+
+ this._state = aState;
+ this._hostChanged = false;
+
+ let identity = IdentityHandler.checkIdentity(aState, this.browser);
+
+ let message = {
+ type: "Content:SecurityChange",
+ tabID: this.id,
+ identity: identity
+ };
+
+ Messaging.sendRequest(message);
+ },
+
+ onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) {
+ // Note: aWebProgess and aRequest will be NULL since we are filtering webprogress
+ // notifications using nsBrowserStatusFilter.
+ },
+
+ onStatusChange: function(aBrowser, aWebProgress, aRequest, aStatus, aMessage) {
+ // Note: aWebProgess and aRequest will be NULL since we are filtering webprogress
+ // notifications using nsBrowserStatusFilter.
+ },
+
+ _getGeckoZoom: function() {
+ let res = {};
+ let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ cwu.getResolution(res);
+ let zoom = res.value * window.devicePixelRatio;
+ return zoom;
+ },
+
+ saveSessionZoom: function(aZoom) {
+ let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ cwu.setResolutionAndScaleTo(aZoom / window.devicePixelRatio);
+ },
+
+ restoredSessionZoom: function() {
+ let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+
+ if (this._restoreZoom && cwu.isResolutionSet) {
+ return this._getGeckoZoom();
+ }
+ return null;
+ },
+
+ _updateZoomFromHistoryEvent: function(aHistoryEventName) {
+ // Restore zoom only when moving in session history, not for new page loads.
+ this._restoreZoom = aHistoryEventName !== "New";
+ },
+
+ OnHistoryNewEntry: function(aUri) {
+ this._updateZoomFromHistoryEvent("New");
+ },
+
+ OnHistoryGoBack: function(aUri) {
+ this._updateZoomFromHistoryEvent("Back");
+ return true;
+ },
+
+ OnHistoryGoForward: function(aUri) {
+ this._updateZoomFromHistoryEvent("Forward");
+ return true;
+ },
+
+ OnHistoryReload: function(aUri, aFlags) {
+ // we don't do anything with this, so don't propagate it
+ // for now anyway
+ return true;
+ },
+
+ OnHistoryGotoIndex: function(aIndex, aUri) {
+ this._updateZoomFromHistoryEvent("Goto");
+ return true;
+ },
+
+ OnHistoryPurge: function(aNumEntries) {
+ this._updateZoomFromHistoryEvent("Purge");
+ return true;
+ },
+
+ OnHistoryReplaceEntry: function(aIndex) {
+ // we don't do anything with this, so don't propogate it
+ // for now anyway.
+ },
+
+ ShouldNotifyMediaPlaybackChange: function(activeState) {
+ // If the media is active, we would check it's duration, because we don't
+ // want to show the media control interface for the short sound which
+ // duration is smaller than the threshold. The basic unit is second.
+ // Note : the streaming format's duration is infinite.
+ if (activeState === "inactive") {
+ return true;
+ }
+
+ const mediaDurationThreshold = 1.0;
+
+ let audioElements = this.browser.contentDocument.getElementsByTagName("audio");
+ for each (let audio in audioElements) {
+ if (!audio.paused && audio.duration < mediaDurationThreshold) {
+ return false;
+ }
+ }
+
+ let videoElements = this.browser.contentDocument.getElementsByTagName("video");
+ for each (let video in videoElements) {
+ if (!video.paused && video.duration < mediaDurationThreshold) {
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "before-first-paint":
+ // Is it on the top level?
+ let contentDocument = aSubject;
+ if (contentDocument == this.browser.contentDocument) {
+ if (BrowserApp.selectedTab == this) {
+ BrowserApp.contentDocumentChanged();
+ }
+ this.contentDocumentIsDisplayed = true;
+
+ if (contentDocument instanceof Ci.nsIImageDocument) {
+ contentDocument.shrinkToFit();
+ }
+ }
+ break;
+
+ case "media-playback":
+ case "media-playback-resumed":
+ if (!aSubject) {
+ return;
+ }
+
+ let winId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ if (this.browser.outerWindowID != winId) {
+ return;
+ }
+
+ if (!this.ShouldNotifyMediaPlaybackChange(aData)) {
+ return;
+ }
+
+ let status;
+ if (aTopic == "media-playback") {
+ status = (aData === "inactive") ? "end" : "start";
+ } else if (aTopic == "media-playback-resumed") {
+ status = "resume";
+ }
+
+ Messaging.sendRequest({
+ type: "Tab:MediaPlaybackChange",
+ tabID: this.id,
+ status: status
+ });
+ break;
+ }
+ },
+
+ // nsIBrowserTab
+ get window() {
+ if (!this.browser)
+ return null;
+ return this.browser.contentWindow;
+ },
+
+ get scale() {
+ return this._zoom;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIWebProgressListener,
+ Ci.nsISHistoryListener,
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference,
+ Ci.nsIBrowserTab
+ ])
+};
+
+var BrowserEventHandler = {
+ init: function init() {
+ this._clickInZoomedView = false;
+ Services.obs.addObserver(this, "Gesture:SingleTap", false);
+ Services.obs.addObserver(this, "Gesture:ClickInZoomedView", false);
+
+ BrowserApp.deck.addEventListener("touchend", this, true);
+
+ BrowserApp.deck.addEventListener("DOMUpdatePageReport", PopupBlockerObserver.onUpdatePageReport, false);
+ BrowserApp.deck.addEventListener("MozMouseHittest", this, true);
+ BrowserApp.deck.addEventListener("OpenMediaWithExternalApp", this, true);
+
+ InitLater(() => BrowserApp.deck.addEventListener("click", InputWidgetHelper, true));
+ InitLater(() => BrowserApp.deck.addEventListener("click", SelectHelper, true));
+
+ // ReaderViews support backPress listeners.
+ Messaging.addListener(() => {
+ return Reader.onBackPress(BrowserApp.selectedTab.id);
+ }, "Browser:OnBackPressed");
+ },
+
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case 'touchend':
+ if (this._inCluster) {
+ aEvent.preventDefault();
+ }
+ break;
+ case 'MozMouseHittest':
+ this._handleRetargetedTouchStart(aEvent);
+ break;
+ case 'OpenMediaWithExternalApp': {
+ let mediaSrc = aEvent.target.currentSrc || aEvent.target.src;
+ let uuid = uuidgen.generateUUID().toString();
+ Services.androidBridge.handleGeckoMessage({
+ type: "Video:Play",
+ uri: mediaSrc,
+ uuid: uuid
+ });
+ break;
+ }
+ }
+ },
+
+ _handleRetargetedTouchStart: function(aEvent) {
+ // we should only get this called just after a new touchstart with a single
+ // touch point.
+ if (!BrowserApp.isBrowserContentDocumentDisplayed() || aEvent.defaultPrevented) {
+ return;
+ }
+
+ let target = aEvent.target;
+ if (!target) {
+ return;
+ }
+
+ this._inCluster = aEvent.hitCluster;
+ if (this._inCluster) {
+ return; // No highlight for a cluster of links
+ }
+
+ let uri = this._getLinkURI(target);
+ if (uri) {
+ try {
+ Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
+ } catch (e) {}
+ }
+ this._doTapHighlight(target);
+ },
+
+ _getLinkURI: function(aElement) {
+ if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
+ ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) ||
+ (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href))) {
+ try {
+ return Services.io.newURI(aElement.href, null, null);
+ } catch (e) {}
+ }
+ return null;
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ // the remaining events are all dependent on the browser content document being the
+ // same as the browser displayed document. if they are not the same, we should ignore
+ // the event.
+ if (BrowserApp.isBrowserContentDocumentDisplayed()) {
+ this.handleUserEvent(aTopic, aData);
+ }
+ },
+
+ handleUserEvent: function(aTopic, aData) {
+ switch (aTopic) {
+
+ case "Gesture:ClickInZoomedView":
+ this._clickInZoomedView = true;
+ break;
+
+ case "Gesture:SingleTap": {
+ let focusedElement = BrowserApp.getFocusedInput(BrowserApp.selectedBrowser);
+ let data = JSON.parse(aData);
+ let {x, y} = data;
+
+ if (this._inCluster && this._clickInZoomedView != true) {
+ // If there is a focused element, the display of the zoomed view won't remove the focus.
+ // In this case, the form assistant linked to the focused element will never be closed.
+ // To avoid this situation, the focus is moved and the form assistant is closed.
+ if (focusedElement) {
+ try {
+ Services.focus.moveFocus(BrowserApp.selectedBrowser.contentWindow, null, Services.focus.MOVEFOCUS_ROOT, 0);
+ } catch(e) {
+ Cu.reportError(e);
+ }
+ Messaging.sendRequest({ type: "FormAssist:Hide" });
+ }
+ this._clusterClicked(x, y);
+ } else {
+ if (this._clickInZoomedView != true) {
+ this._closeZoomedView();
+ }
+ }
+ this._clickInZoomedView = false;
+ this._cancelTapHighlight();
+ break;
+ }
+
+ default:
+ dump('BrowserEventHandler.handleUserEvent: unexpected topic "' + aTopic + '"');
+ break;
+ }
+ },
+
+ _closeZoomedView: function() {
+ Messaging.sendRequest({
+ type: "Gesture:CloseZoomedView"
+ });
+ },
+
+ _clusterClicked: function(aX, aY) {
+ Messaging.sendRequest({
+ type: "Gesture:clusteredLinksClicked",
+ clickPosition: {
+ x: aX,
+ y: aY
+ }
+ });
+ },
+
+ _highlightElement: null,
+
+ _doTapHighlight: function _doTapHighlight(aElement) {
+ this._highlightElement = aElement;
+ },
+
+ _cancelTapHighlight: function _cancelTapHighlight() {
+ if (!this._highlightElement)
+ return;
+
+ this._highlightElement = null;
+ }
+};
+
+const ElementTouchHelper = {
+ getBoundingContentRect: function(aElement) {
+ if (!aElement)
+ return {x: 0, y: 0, w: 0, h: 0};
+
+ let document = aElement.ownerDocument;
+ while (document.defaultView.frameElement)
+ document = document.defaultView.frameElement.ownerDocument;
+
+ let cwu = document.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ let scrollX = {}, scrollY = {};
+ cwu.getScrollXY(false, scrollX, scrollY);
+
+ let r = aElement.getBoundingClientRect();
+
+ // step out of iframes and frames, offsetting scroll values
+ for (let frame = aElement.ownerDocument.defaultView; frame.frameElement && frame != content; frame = frame.parent) {
+ // adjust client coordinates' origin to be top left of iframe viewport
+ let rect = frame.frameElement.getBoundingClientRect();
+ let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
+ let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
+ scrollX.value += rect.left + parseInt(left);
+ scrollY.value += rect.top + parseInt(top);
+ }
+
+ return {x: r.left + scrollX.value,
+ y: r.top + scrollY.value,
+ w: r.width,
+ h: r.height };
+ }
+};
+
+var ErrorPageEventHandler = {
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case "click": {
+ // Don't trust synthetic events
+ if (!aEvent.isTrusted)
+ return;
+
+ let target = aEvent.originalTarget;
+ let errorDoc = target.ownerDocument;
+
+ // If the event came from an ssl error page, it is probably either the "Add
+ // Exception…" or "Get me out of here!" button
+ if (errorDoc.documentURI.startsWith("about:certerror?e=nssBadCert")) {
+ let perm = errorDoc.getElementById("permanentExceptionButton");
+ let temp = errorDoc.getElementById("temporaryExceptionButton");
+ if (target == temp || target == perm) {
+ // Handle setting an cert exception and reloading the page
+ try {
+ // Add a new SSL exception for this URL
+ let uri = Services.io.newURI(errorDoc.location.href, null, null);
+ let sslExceptions = new SSLExceptions();
+
+ if (target == perm)
+ sslExceptions.addPermanentException(uri, errorDoc.defaultView);
+ else
+ sslExceptions.addTemporaryException(uri, errorDoc.defaultView);
+ } catch (e) {
+ dump("Failed to set cert exception: " + e + "\n");
+ }
+ errorDoc.location.reload();
+ } else if (target == errorDoc.getElementById("getMeOutOfHereButton")) {
+ errorDoc.location = "about:home";
+ }
+ } else if (errorDoc.documentURI.startsWith("about:blocked")) {
+ // The event came from a button on a malware/phishing block page
+ // First check whether it's malware, phishing or unwanted, so that we
+ // can use the right strings/links
+ let bucketName = "";
+ let sendTelemetry = false;
+ if (errorDoc.documentURI.includes("e=malwareBlocked")) {
+ sendTelemetry = true;
+ bucketName = "WARNING_MALWARE_PAGE_";
+ } else if (errorDoc.documentURI.includes("e=deceptiveBlocked")) {
+ sendTelemetry = true;
+ bucketName = "WARNING_PHISHING_PAGE_";
+ } else if (errorDoc.documentURI.includes("e=unwantedBlocked")) {
+ sendTelemetry = true;
+ bucketName = "WARNING_UNWANTED_PAGE_";
+ }
+ let nsISecTel = Ci.nsISecurityUITelemetry;
+ let isIframe = (errorDoc.defaultView.parent === errorDoc.defaultView);
+ bucketName += isIframe ? "TOP_" : "FRAME_";
+
+ let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter);
+
+ if (target == errorDoc.getElementById("getMeOutButton")) {
+ if (sendTelemetry) {
+ Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "GET_ME_OUT_OF_HERE"]);
+ }
+ errorDoc.location = "about:home";
+ } else if (target == errorDoc.getElementById("reportButton")) {
+ // We log even if malware/phishing info URL couldn't be found:
+ // the measurement is for how many users clicked the WHY BLOCKED button
+ if (sendTelemetry) {
+ Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "WHY_BLOCKED"]);
+ }
+
+ // This is the "Why is this site blocked" button. We redirect
+ // to the generic page describing phishing/malware protection.
+ let url = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ BrowserApp.selectedBrowser.loadURI(url + "phishing-malware");
+ } else if (target == errorDoc.getElementById("ignoreWarningButton") &&
+ Services.prefs.getBoolPref("browser.safebrowsing.allowOverride")) {
+ if (sendTelemetry) {
+ Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "IGNORE_WARNING"]);
+ }
+
+ // Allow users to override and continue through to the site,
+ let webNav = BrowserApp.selectedBrowser.docShell.QueryInterface(Ci.nsIWebNavigation);
+ let location = BrowserApp.selectedBrowser.contentWindow.location;
+ webNav.loadURI(location, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER, null, null, null);
+
+ // ....but add a notify bar as a reminder, so that they don't lose
+ // track after, e.g., tab switching.
+ NativeWindow.doorhanger.show(Strings.browser.GetStringFromName("safeBrowsingDoorhanger"), "safebrowsing-warning", [], BrowserApp.selectedTab.id);
+ }
+ }
+ break;
+ }
+ }
+ }
+};
+
+var FormAssistant = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]),
+
+ // Used to keep track of the element that corresponds to the current
+ // autocomplete suggestions
+ _currentInputElement: null,
+
+ // The value of the currently focused input
+ _currentInputValue: null,
+
+ // Whether we're in the middle of an autocomplete
+ _doingAutocomplete: false,
+
+ // Keep track of whether or not an invalid form has been submitted
+ _invalidSubmit: false,
+
+ init: function() {
+ Services.obs.addObserver(this, "FormAssist:AutoComplete", false);
+ Services.obs.addObserver(this, "FormAssist:Hidden", false);
+ Services.obs.addObserver(this, "FormAssist:Remove", false);
+ Services.obs.addObserver(this, "invalidformsubmit", false);
+ Services.obs.addObserver(this, "PanZoom:StateChange", false);
+
+ // We need to use a capturing listener for focus events
+ BrowserApp.deck.addEventListener("focus", this, true);
+ BrowserApp.deck.addEventListener("blur", this, true);
+ BrowserApp.deck.addEventListener("click", this, true);
+ BrowserApp.deck.addEventListener("input", this, false);
+ BrowserApp.deck.addEventListener("pageshow", this, false);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "PanZoom:StateChange":
+ // If the user is just touching the screen and we haven't entered a pan or zoom state yet do nothing
+ if (aData == "TOUCHING" || aData == "WAITING_LISTENERS")
+ break;
+ if (aData == "NOTHING") {
+ // only look for input elements, not contentEditable or multiline text areas
+ let focused = BrowserApp.getFocusedInput(BrowserApp.selectedBrowser, true);
+ if (!focused)
+ break;
+
+ if (this._showValidationMessage(focused))
+ break;
+ let checkResultsClick = hasResults => {
+ if (!hasResults) {
+ this._hideFormAssistPopup();
+ }
+ };
+ this._showAutoCompleteSuggestions(focused, checkResultsClick);
+ } else {
+ // temporarily hide the form assist popup while we're panning or zooming the page
+ this._hideFormAssistPopup();
+ }
+ break;
+ case "FormAssist:AutoComplete":
+ if (!this._currentInputElement)
+ break;
+
+ let editableElement = this._currentInputElement.QueryInterface(Ci.nsIDOMNSEditableElement);
+
+ this._doingAutocomplete = true;
+
+ // If we have an active composition string, commit it before sending
+ // the autocomplete event with the text that will replace it.
+ try {
+ let imeEditor = editableElement.editor.QueryInterface(Ci.nsIEditorIMESupport);
+ if (imeEditor.composing)
+ imeEditor.forceCompositionEnd();
+ } catch (e) {}
+
+ editableElement.setUserInput(aData);
+ this._currentInputValue = aData;
+
+ let event = this._currentInputElement.ownerDocument.createEvent("Events");
+ event.initEvent("DOMAutoComplete", true, true);
+ this._currentInputElement.dispatchEvent(event);
+
+ this._doingAutocomplete = false;
+
+ break;
+
+ case "FormAssist:Hidden":
+ this._currentInputElement = null;
+ break;
+
+ case "FormAssist:Remove":
+ if (!this._currentInputElement) {
+ break;
+ }
+
+ FormHistory.update({
+ op: "remove",
+ fieldname: this._currentInputElement.name,
+ value: aData
+ });
+ break;
+ }
+ },
+
+ notifyInvalidSubmit: function notifyInvalidSubmit(aFormElement, aInvalidElements) {
+ if (!aInvalidElements.length)
+ return;
+
+ // Ignore this notificaiton if the current tab doesn't contain the invalid element
+ let currentElement = aInvalidElements.queryElementAt(0, Ci.nsISupports);
+ if (BrowserApp.selectedBrowser.contentDocument !=
+ currentElement.ownerDocument.defaultView.top.document)
+ return;
+
+ this._invalidSubmit = true;
+
+ // Our focus listener will show the element's validation message
+ currentElement.focus();
+ },
+
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case "focus": {
+ let currentElement = aEvent.target;
+
+ // Only show a validation message on focus.
+ this._showValidationMessage(currentElement);
+ break;
+ }
+
+ case "blur": {
+ this._currentInputValue = null;
+ break;
+ }
+
+ case "click": {
+ let currentElement = aEvent.target;
+
+ // Prioritize a form validation message over autocomplete suggestions
+ // when the element is first focused (a form validation message will
+ // only be available if an invalid form was submitted)
+ if (this._showValidationMessage(currentElement))
+ break;
+
+ let checkResultsClick = hasResults => {
+ if (!hasResults) {
+ this._hideFormAssistPopup();
+ }
+ };
+
+ this._showAutoCompleteSuggestions(currentElement, checkResultsClick);
+ break;
+ }
+
+ case "input": {
+ let currentElement = aEvent.target;
+
+ // If this element isn't focused, we're already in middle of an
+ // autocomplete, or its value hasn't changed, don't show the
+ // autocomplete popup.
+ if (currentElement !== BrowserApp.getFocusedInput(BrowserApp.selectedBrowser) ||
+ this._doingAutocomplete ||
+ currentElement.value === this._currentInputValue) {
+ break;
+ }
+
+ this._currentInputValue = currentElement.value;
+
+ // Since we can only show one popup at a time, prioritze autocomplete
+ // suggestions over a form validation message
+ let checkResultsInput = hasResults => {
+ if (hasResults)
+ return;
+
+ if (this._showValidationMessage(currentElement))
+ return;
+
+ // If we're not showing autocomplete suggestions, hide the form assist popup
+ this._hideFormAssistPopup();
+ };
+
+ this._showAutoCompleteSuggestions(currentElement, checkResultsInput);
+ break;
+ }
+
+ // Reset invalid submit state on each pageshow
+ case "pageshow": {
+ if (!this._invalidSubmit)
+ return;
+
+ let selectedBrowser = BrowserApp.selectedBrowser;
+ if (selectedBrowser) {
+ let selectedDocument = selectedBrowser.contentDocument;
+ let target = aEvent.originalTarget;
+ if (target == selectedDocument || target.ownerDocument == selectedDocument)
+ this._invalidSubmit = false;
+ }
+ break;
+ }
+ }
+ },
+
+ // We only want to show autocomplete suggestions for certain elements
+ _isAutoComplete: function _isAutoComplete(aElement) {
+ if (!(aElement instanceof HTMLInputElement) || aElement.readOnly || aElement.disabled ||
+ (aElement.getAttribute("type") == "password") ||
+ (aElement.hasAttribute("autocomplete") &&
+ aElement.getAttribute("autocomplete").toLowerCase() == "off"))
+ return false;
+
+ return true;
+ },
+
+ // Retrieves autocomplete suggestions for an element from the form autocomplete service.
+ // aCallback(array_of_suggestions) is called when results are available.
+ _getAutoCompleteSuggestions: function _getAutoCompleteSuggestions(aSearchString, aElement, aCallback) {
+ // Cache the form autocomplete service for future use
+ if (!this._formAutoCompleteService) {
+ this._formAutoCompleteService = Cc["@mozilla.org/satchel/form-autocomplete;1"]
+ .getService(Ci.nsIFormAutoComplete);
+ }
+
+ let resultsAvailable = function (results) {
+ let suggestions = [];
+ for (let i = 0; i < results.matchCount; i++) {
+ let value = results.getValueAt(i);
+
+ // Do not show the value if it is the current one in the input field
+ if (value == aSearchString)
+ continue;
+
+ // Supply a label and value, since they can differ for datalist suggestions
+ suggestions.push({ label: value, value: value });
+ }
+ aCallback(suggestions);
+ };
+
+ this._formAutoCompleteService.autoCompleteSearchAsync(aElement.name || aElement.id,
+ aSearchString, aElement, null,
+ null, resultsAvailable);
+ },
+
+ /**
+ * (Copied from mobile/xul/chrome/content/forms.js)
+ * This function is similar to getListSuggestions from
+ * components/satchel/src/nsInputListAutoComplete.js but sadly this one is
+ * used by the autocomplete.xml binding which is not in used in fennec
+ */
+ _getListSuggestions: function _getListSuggestions(aElement) {
+ if (!(aElement instanceof HTMLInputElement) || !aElement.list)
+ return [];
+
+ let suggestions = [];
+ let filter = !aElement.hasAttribute("mozNoFilter");
+ let lowerFieldValue = aElement.value.toLowerCase();
+
+ let options = aElement.list.options;
+ let length = options.length;
+ for (let i = 0; i < length; i++) {
+ let item = options.item(i);
+
+ let label = item.value;
+ if (item.label)
+ label = item.label;
+ else if (item.text)
+ label = item.text;
+
+ if (filter && !(label.toLowerCase().includes(lowerFieldValue)) )
+ continue;
+ suggestions.push({ label: label, value: item.value });
+ }
+
+ return suggestions;
+ },
+
+ // Retrieves autocomplete suggestions for an element from the form autocomplete service
+ // and sends the suggestions to the Java UI, along with element position data. As
+ // autocomplete queries are asynchronous, calls aCallback when done with a true
+ // argument if results were found and false if no results were found.
+ _showAutoCompleteSuggestions: function _showAutoCompleteSuggestions(aElement, aCallback) {
+ if (!this._isAutoComplete(aElement)) {
+ aCallback(false);
+ return;
+ }
+ if (this._isDisabledElement(aElement)) {
+ aCallback(false);
+ return;
+ }
+
+ let isEmpty = (aElement.value.length === 0);
+
+ let resultsAvailable = autoCompleteSuggestions => {
+ // On desktop, we show datalist suggestions below autocomplete suggestions,
+ // without duplicates removed.
+ let listSuggestions = this._getListSuggestions(aElement);
+ let suggestions = autoCompleteSuggestions.concat(listSuggestions);
+
+ // Return false if there are no suggestions to show
+ if (!suggestions.length) {
+ aCallback(false);
+ return;
+ }
+
+ Messaging.sendRequest({
+ type: "FormAssist:AutoComplete",
+ suggestions: suggestions,
+ rect: ElementTouchHelper.getBoundingContentRect(aElement),
+ isEmpty: isEmpty,
+ });
+
+ // Keep track of input element so we can fill it in if the user
+ // selects an autocomplete suggestion
+ this._currentInputElement = aElement;
+ aCallback(true);
+ };
+
+ this._getAutoCompleteSuggestions(aElement.value, aElement, resultsAvailable);
+ },
+
+ // Only show a validation message if the user submitted an invalid form,
+ // there's a non-empty message string, and the element is the correct type
+ _isValidateable: function _isValidateable(aElement) {
+ if (!this._invalidSubmit ||
+ !aElement.validationMessage ||
+ !(aElement instanceof HTMLInputElement ||
+ aElement instanceof HTMLTextAreaElement ||
+ aElement instanceof HTMLSelectElement ||
+ aElement instanceof HTMLButtonElement))
+ return false;
+
+ return true;
+ },
+
+ // Sends a validation message and position data for an element to the Java UI.
+ // Returns true if there's a validation message to show, false otherwise.
+ _showValidationMessage: function _sendValidationMessage(aElement) {
+ if (!this._isValidateable(aElement))
+ return false;
+
+ Messaging.sendRequest({
+ type: "FormAssist:ValidationMessage",
+ validationMessage: aElement.validationMessage,
+ rect: ElementTouchHelper.getBoundingContentRect(aElement)
+ });
+
+ return true;
+ },
+
+ _hideFormAssistPopup: function _hideFormAssistPopup() {
+ Messaging.sendRequest({ type: "FormAssist:Hide" });
+ },
+
+ _isDisabledElement : function(aElement) {
+ let currentElement = aElement;
+ while (currentElement) {
+ if(currentElement.disabled)
+ return true;
+
+ currentElement = currentElement.parentElement;
+ }
+ return false;
+ }
+};
+
+var XPInstallObserver = {
+ init: function() {
+ Services.obs.addObserver(this, "addon-install-origin-blocked", false);
+ Services.obs.addObserver(this, "addon-install-disabled", false);
+ Services.obs.addObserver(this, "addon-install-blocked", false);
+ Services.obs.addObserver(this, "addon-install-started", false);
+ Services.obs.addObserver(this, "xpi-signature-changed", false);
+ Services.obs.addObserver(this, "browser-delayed-startup-finished", false);
+
+ AddonManager.addInstallListener(this);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ let installInfo, tab, host;
+ if (aSubject && aSubject instanceof Ci.amIWebInstallInfo) {
+ installInfo = aSubject;
+ tab = BrowserApp.getTabForBrowser(installInfo.browser);
+ if (installInfo.originatingURI) {
+ host = installInfo.originatingURI.host;
+ }
+ }
+
+ let strings = Strings.browser;
+ let brandShortName = Strings.brand.GetStringFromName("brandShortName");
+
+ switch (aTopic) {
+ case "addon-install-started":
+ Snackbars.show(strings.GetStringFromName("alertAddonsDownloading"), Snackbars.LENGTH_LONG);
+ break;
+ case "addon-install-disabled": {
+ if (!tab)
+ return;
+
+ let enabled = true;
+ try {
+ enabled = Services.prefs.getBoolPref("xpinstall.enabled");
+ } catch (e) {}
+
+ let buttons, message, callback;
+ if (!enabled) {
+ message = strings.GetStringFromName("xpinstallDisabledMessageLocked");
+ buttons = [strings.GetStringFromName("unsignedAddonsDisabled.dismiss")];
+ callback: (data) => {};
+ } else {
+ message = strings.formatStringFromName("xpinstallDisabledMessage2", [brandShortName, host], 2);
+ buttons = [
+ strings.GetStringFromName("xpinstallDisabledButton"),
+ strings.GetStringFromName("unsignedAddonsDisabled.dismiss")
+ ];
+ callback: (data) => {
+ if (data.button === 1) {
+ Services.prefs.setBoolPref("xpinstall.enabled", true)
+ }
+ };
+ }
+
+ new Prompt({
+ title: Strings.browser.GetStringFromName("addonError.titleError"),
+ message: message,
+ buttons: buttons
+ }).show(callback);
+ break;
+ }
+ case "addon-install-blocked": {
+ if (!tab)
+ return;
+
+ let message;
+ if (host) {
+ // We have a host which asked for the install.
+ message = strings.formatStringFromName("xpinstallPromptWarning2", [brandShortName, host], 2);
+ } else {
+ // Without a host we address the add-on as the initiator of the install.
+ let addon = null;
+ if (installInfo.installs.length > 0) {
+ addon = installInfo.installs[0].name;
+ }
+ if (addon) {
+ // We have an addon name, show the regular message.
+ message = strings.formatStringFromName("xpinstallPromptWarningLocal", [brandShortName, addon], 2);
+ } else {
+ // We don't have an addon name, show an alternative message.
+ message = strings.formatStringFromName("xpinstallPromptWarningDirect", [brandShortName], 1);
+ }
+ }
+
+ let buttons = [
+ strings.GetStringFromName("xpinstallPromptAllowButton"),
+ strings.GetStringFromName("unsignedAddonsDisabled.dismiss")
+ ];
+ new Prompt({
+ title: Strings.browser.GetStringFromName("addonError.titleBlocked"),
+ message: message,
+ buttons: buttons
+ }).show((data) => {
+ if (data.button === 0) {
+ // Kick off the install
+ installInfo.install();
+ }
+ });
+ break;
+ }
+ case "addon-install-origin-blocked": {
+ if (!tab)
+ return;
+
+ new Prompt({
+ title: Strings.browser.GetStringFromName("addonError.titleBlocked"),
+ message: strings.formatStringFromName("xpinstallPromptWarningDirect", [brandShortName], 1),
+ buttons: [strings.GetStringFromName("unsignedAddonsDisabled.dismiss")]
+ }).show((data) => {});
+ break;
+ }
+ case "xpi-signature-changed": {
+ if (JSON.parse(aData).disabled.length) {
+ this._notifyUnsignedAddonsDisabled();
+ }
+ break;
+ }
+ case "browser-delayed-startup-finished": {
+ let disabledAddons = AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_DISABLED);
+ for (let id of disabledAddons) {
+ if (AddonManager.getAddonByID(id).signedState <= AddonManager.SIGNEDSTATE_MISSING) {
+ this._notifyUnsignedAddonsDisabled();
+ break;
+ }
+ }
+ break;
+ }
+ }
+ },
+
+ _notifyUnsignedAddonsDisabled: function() {
+ new Prompt({
+ window: window,
+ title: Strings.browser.GetStringFromName("unsignedAddonsDisabled.title"),
+ message: Strings.browser.GetStringFromName("unsignedAddonsDisabled.message"),
+ buttons: [
+ Strings.browser.GetStringFromName("unsignedAddonsDisabled.viewAddons"),
+ Strings.browser.GetStringFromName("unsignedAddonsDisabled.dismiss")
+ ]
+ }).show((data) => {
+ if (data.button === 0) {
+ // TODO: Open about:addons to show only unsigned add-ons?
+ BrowserApp.selectOrAddTab("about:addons", { parentId: BrowserApp.selectedTab.id });
+ }
+ });
+ },
+
+ onInstallEnded: function(aInstall, aAddon) {
+ // Don't create a notification for distribution add-ons.
+ if (Distribution.pendingAddonInstalls.has(aInstall)) {
+ Distribution.pendingAddonInstalls.delete(aInstall);
+ return;
+ }
+
+ let needsRestart = false;
+ if (aInstall.existingAddon && (aInstall.existingAddon.pendingOperations & AddonManager.PENDING_UPGRADE))
+ needsRestart = true;
+ else if (aAddon.pendingOperations & AddonManager.PENDING_INSTALL)
+ needsRestart = true;
+
+ if (needsRestart) {
+ this.showRestartPrompt();
+ } else {
+ // Display completion message for new installs or updates not done Automatically
+ if (!aInstall.existingAddon || !AddonManager.shouldAutoUpdate(aInstall.existingAddon)) {
+ let message = Strings.browser.GetStringFromName("alertAddonsInstalledNoRestart.message");
+ Snackbars.show(message, Snackbars.LENGTH_LONG, {
+ action: {
+ label: Strings.browser.GetStringFromName("alertAddonsInstalledNoRestart.action2"),
+ callback: () => {
+ UITelemetry.addEvent("show.1", "toast", null, "addons");
+ BrowserApp.selectOrAddTab("about:addons", { parentId: BrowserApp.selectedTab.id });
+ },
+ }
+ });
+ }
+ }
+ },
+
+ onInstallFailed: function(aInstall) {
+ this._showErrorMessage(aInstall);
+ },
+
+ onDownloadProgress: function(aInstall) {},
+
+ onDownloadFailed: function(aInstall) {
+ this._showErrorMessage(aInstall);
+ },
+
+ onDownloadCancelled: function(aInstall) {},
+
+ _showErrorMessage: function(aInstall) {
+ // Don't create a notification for distribution add-ons.
+ if (Distribution.pendingAddonInstalls.has(aInstall)) {
+ Cu.reportError("Error installing distribution add-on: " + aInstall.addon.id);
+ Distribution.pendingAddonInstalls.delete(aInstall);
+ return;
+ }
+
+ let host = (aInstall.originatingURI instanceof Ci.nsIStandardURL) && aInstall.originatingURI.host;
+ if (!host) {
+ host = (aInstall.sourceURI instanceof Ci.nsIStandardURL) && aInstall.sourceURI.host;
+ }
+
+ let error = (host || aInstall.error == 0) ? "addonError" : "addonLocalError";
+ if (aInstall.error < 0) {
+ error += aInstall.error;
+ } else if (aInstall.addon && aInstall.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) {
+ error += "Blocklisted";
+ } else {
+ error += "Incompatible";
+ }
+
+ let msg = Strings.browser.GetStringFromName(error);
+ // TODO: formatStringFromName
+ msg = msg.replace("#1", aInstall.name);
+ if (host) {
+ msg = msg.replace("#2", host);
+ }
+ msg = msg.replace("#3", Strings.brand.GetStringFromName("brandShortName"));
+ msg = msg.replace("#4", Services.appinfo.version);
+
+ if (aInstall.error == AddonManager.ERROR_SIGNEDSTATE_REQUIRED) {
+ new Prompt({
+ window: window,
+ title: Strings.browser.GetStringFromName("addonError.titleBlocked"),
+ message: msg,
+ buttons: [Strings.browser.GetStringFromName("addonError.learnMore")]
+ }).show((data) => {
+ if (data.button === 0) {
+ let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons";
+ BrowserApp.addTab(url, { parentId: BrowserApp.selectedTab.id });
+ }
+ });
+ } else {
+ Services.prompt.alert(null, Strings.browser.GetStringFromName("addonError.titleError"), msg);
+ }
+ },
+
+ showRestartPrompt: function() {
+ let buttons = [{
+ label: Strings.browser.GetStringFromName("notificationRestart.button"),
+ callback: function() {
+ // Notify all windows that an application quit has been requested
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
+
+ // If nothing aborted, quit the app
+ if (cancelQuit.data == false) {
+ Services.obs.notifyObservers(null, "quit-application-proceeding", null);
+ let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
+ appStartup.quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit);
+ }
+ },
+ positive: true
+ }];
+
+ let message = Strings.browser.GetStringFromName("notificationRestart.normal");
+ NativeWindow.doorhanger.show(message, "addon-app-restart", buttons, BrowserApp.selectedTab.id, { persistence: -1 });
+ },
+
+ hideRestartPrompt: function() {
+ NativeWindow.doorhanger.hide("addon-app-restart", BrowserApp.selectedTab.id);
+ }
+};
+
+var ViewportHandler = {
+ init: function init() {
+ Services.obs.addObserver(this, "Window:Resize", false);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "Window:Resize" && aData) {
+ let scrollChange = JSON.parse(aData);
+ let windowUtils = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ windowUtils.setNextPaintSyncId(scrollChange.id);
+ }
+ }
+};
+
+/**
+ * Handler for blocked popups, triggered by DOMUpdatePageReport events in browser.xml
+ */
+var PopupBlockerObserver = {
+ onUpdatePageReport: function onUpdatePageReport(aEvent) {
+ let browser = BrowserApp.selectedBrowser;
+ if (aEvent.originalTarget != browser)
+ return;
+
+ if (!browser.pageReport)
+ return;
+
+ let result = Services.perms.testExactPermission(BrowserApp.selectedBrowser.currentURI, "popup");
+ if (result == Ci.nsIPermissionManager.DENY_ACTION)
+ return;
+
+ // Only show the notification again if we've not already shown it. Since
+ // notifications are per-browser, we don't need to worry about re-adding
+ // it.
+ if (!browser.pageReport.reported) {
+ if (Services.prefs.getBoolPref("privacy.popups.showBrowserMessage")) {
+ let brandShortName = Strings.brand.GetStringFromName("brandShortName");
+ let popupCount = browser.pageReport.length;
+
+ let strings = Strings.browser;
+ let message = PluralForm.get(popupCount, strings.GetStringFromName("popup.message"))
+ .replace("#1", brandShortName)
+ .replace("#2", popupCount);
+
+ let buttons = [
+ {
+ label: strings.GetStringFromName("popup.dontShow"),
+ callback: function(aChecked) {
+ if (aChecked)
+ PopupBlockerObserver.allowPopupsForSite(false);
+ }
+ },
+ {
+ label: strings.GetStringFromName("popup.show"),
+ callback: function(aChecked) {
+ // Set permission before opening popup windows
+ if (aChecked)
+ PopupBlockerObserver.allowPopupsForSite(true);
+
+ PopupBlockerObserver.showPopupsForSite();
+ },
+ positive: true
+ }
+ ];
+
+ let options = { checkbox: Strings.browser.GetStringFromName("popup.dontAskAgain") };
+ NativeWindow.doorhanger.show(message, "popup-blocked", buttons, null, options);
+ }
+ // Record the fact that we've reported this blocked popup, so we don't
+ // show it again.
+ browser.pageReport.reported = true;
+ }
+ },
+
+ allowPopupsForSite: function allowPopupsForSite(aAllow) {
+ let currentURI = BrowserApp.selectedBrowser.currentURI;
+ Services.perms.add(currentURI, "popup", aAllow
+ ? Ci.nsIPermissionManager.ALLOW_ACTION
+ : Ci.nsIPermissionManager.DENY_ACTION);
+ dump("Allowing popups for: " + currentURI);
+ },
+
+ showPopupsForSite: function showPopupsForSite() {
+ let uri = BrowserApp.selectedBrowser.currentURI;
+ let pageReport = BrowserApp.selectedBrowser.pageReport;
+ if (pageReport) {
+ for (let i = 0; i < pageReport.length; ++i) {
+ let popupURIspec = pageReport[i].popupWindowURIspec;
+
+ // Sometimes the popup URI that we get back from the pageReport
+ // isn't useful (for instance, netscape.com's popup URI ends up
+ // being "http://www.netscape.com", which isn't really the URI of
+ // the popup they're trying to show). This isn't going to be
+ // useful to the user, so we won't create a menu item for it.
+ if (popupURIspec == "" || popupURIspec == "about:blank" || popupURIspec == uri.spec)
+ continue;
+
+ let popupFeatures = pageReport[i].popupWindowFeatures;
+ let popupName = pageReport[i].popupWindowName;
+
+ let parent = BrowserApp.selectedTab;
+ let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(parent.browser);
+ BrowserApp.addTab(popupURIspec, { parentId: parent.id, isPrivate: isPrivate });
+ }
+ }
+ }
+};
+
+
+var IndexedDB = {
+ _permissionsPrompt: "indexedDB-permissions-prompt",
+ _permissionsResponse: "indexedDB-permissions-response",
+
+ init: function IndexedDB_init() {
+ Services.obs.addObserver(this, this._permissionsPrompt, false);
+ },
+
+ observe: function IndexedDB_observe(subject, topic, data) {
+ if (topic != this._permissionsPrompt) {
+ throw new Error("Unexpected topic!");
+ }
+
+ let requestor = subject.QueryInterface(Ci.nsIInterfaceRequestor);
+
+ let browser = requestor.getInterface(Ci.nsIDOMNode);
+ let tab = BrowserApp.getTabForBrowser(browser);
+ if (!tab)
+ return;
+
+ let host = browser.currentURI.asciiHost;
+
+ let strings = Strings.browser;
+
+ let message, responseTopic;
+ if (topic == this._permissionsPrompt) {
+ message = strings.formatStringFromName("offlineApps.ask", [host], 1);
+ responseTopic = this._permissionsResponse;
+ }
+
+ const firstTimeoutDuration = 300000; // 5 minutes
+
+ let timeoutId;
+
+ let notificationID = responseTopic + host;
+ let observer = requestor.getInterface(Ci.nsIObserver);
+
+ // This will be set to the result of PopupNotifications.show() below, or to
+ // the result of PopupNotifications.getNotification() if this is a
+ // quotaCancel notification.
+ let notification;
+
+ function timeoutNotification() {
+ // Remove the notification.
+ NativeWindow.doorhanger.hide(notificationID, tab.id);
+
+ // Clear all of our timeout stuff. We may be called directly, not just
+ // when the timeout actually elapses.
+ clearTimeout(timeoutId);
+
+ // And tell the page that the popup timed out.
+ observer.observe(null, responseTopic, Ci.nsIPermissionManager.UNKNOWN_ACTION);
+ }
+
+ let buttons = [
+ {
+ label: strings.GetStringFromName("offlineApps.dontAllow2"),
+ callback: function(aChecked) {
+ clearTimeout(timeoutId);
+ let action = aChecked ? Ci.nsIPermissionManager.DENY_ACTION : Ci.nsIPermissionManager.UNKNOWN_ACTION;
+ observer.observe(null, responseTopic, action);
+ }
+ },
+ {
+ label: strings.GetStringFromName("offlineApps.allow"),
+ callback: function() {
+ clearTimeout(timeoutId);
+ observer.observe(null, responseTopic, Ci.nsIPermissionManager.ALLOW_ACTION);
+ },
+ positive: true
+ }];
+
+ let options = { checkbox: Strings.browser.GetStringFromName("offlineApps.dontAskAgain") };
+ NativeWindow.doorhanger.show(message, notificationID, buttons, tab.id, options);
+
+ // Set the timeoutId after the popup has been created, and use the long
+ // timeout value. If the user doesn't notice the popup after this amount of
+ // time then it is most likely not visible and we want to alert the page.
+ timeoutId = setTimeout(timeoutNotification, firstTimeoutDuration);
+ }
+};
+
+var CharacterEncoding = {
+ _charsets: [],
+
+ init: function init() {
+ Services.obs.addObserver(this, "CharEncoding:Get", false);
+ Services.obs.addObserver(this, "CharEncoding:Set", false);
+ InitLater(() => this.sendState());
+ },
+
+ observe: function observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "CharEncoding:Get":
+ this.getEncoding();
+ break;
+ case "CharEncoding:Set":
+ this.setEncoding(aData);
+ break;
+ }
+ },
+
+ sendState: function sendState() {
+ let showCharEncoding = "false";
+ try {
+ showCharEncoding = Services.prefs.getComplexValue("browser.menu.showCharacterEncoding", Ci.nsIPrefLocalizedString).data;
+ } catch (e) { /* Optional */ }
+
+ Messaging.sendRequest({
+ type: "CharEncoding:State",
+ visible: showCharEncoding
+ });
+ },
+
+ getEncoding: function getEncoding() {
+ function infoToCharset(info) {
+ return { code: info.value, title: info.label };
+ }
+
+ if (!this._charsets.length) {
+ let data = CharsetMenu.getData();
+
+ // In the desktop UI, the pinned charsets are shown above the rest.
+ let pinnedCharsets = data.pinnedCharsets.map(infoToCharset);
+ let otherCharsets = data.otherCharsets.map(infoToCharset)
+
+ this._charsets = pinnedCharsets.concat(otherCharsets);
+ }
+
+ // Look for the index of the selected charset. Default to -1 if the
+ // doc charset isn't found in the list of available charsets.
+ let docCharset = BrowserApp.selectedBrowser.contentDocument.characterSet;
+ let selected = -1;
+ let charsetCount = this._charsets.length;
+
+ for (let i = 0; i < charsetCount; i++) {
+ if (this._charsets[i].code === docCharset) {
+ selected = i;
+ break;
+ }
+ }
+
+ Messaging.sendRequest({
+ type: "CharEncoding:Data",
+ charsets: this._charsets,
+ selected: selected
+ });
+ },
+
+ setEncoding: function setEncoding(aEncoding) {
+ let browser = BrowserApp.selectedBrowser;
+ browser.docShell.gatherCharsetMenuTelemetry();
+ browser.docShell.charset = aEncoding;
+ browser.reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
+ }
+};
+
+var IdentityHandler = {
+ // No trusted identity information. No site identity icon is shown.
+ IDENTITY_MODE_UNKNOWN: "unknown",
+
+ // Domain-Validation SSL CA-signed domain verification (DV).
+ IDENTITY_MODE_IDENTIFIED: "identified",
+
+ // Extended-Validation SSL CA-signed identity information (EV). A more rigorous validation process.
+ IDENTITY_MODE_VERIFIED: "verified",
+
+ // Part of the product's UI (built in about: pages)
+ IDENTITY_MODE_CHROMEUI: "chromeUI",
+
+ // The following mixed content modes are only used if "security.mixed_content.block_active_content"
+ // is enabled. Our Java frontend coalesces them into one indicator.
+
+ // No mixed content information. No mixed content icon is shown.
+ MIXED_MODE_UNKNOWN: "unknown",
+
+ // Blocked active mixed content.
+ MIXED_MODE_CONTENT_BLOCKED: "blocked",
+
+ // Loaded active mixed content.
+ MIXED_MODE_CONTENT_LOADED: "loaded",
+
+ // The following tracking content modes are only used if tracking protection
+ // is enabled. Our Java frontend coalesces them into one indicator.
+
+ // No tracking content information. No tracking content icon is shown.
+ TRACKING_MODE_UNKNOWN: "unknown",
+
+ // Blocked active tracking content. Shield icon is shown, with a popup option to load content.
+ TRACKING_MODE_CONTENT_BLOCKED: "tracking_content_blocked",
+
+ // Loaded active tracking content. Yellow triangle icon is shown.
+ TRACKING_MODE_CONTENT_LOADED: "tracking_content_loaded",
+
+ // Cache the most recent SSLStatus and Location seen in getIdentityStrings
+ _lastStatus : null,
+ _lastLocation : null,
+
+ /**
+ * Helper to parse out the important parts of _lastStatus (of the SSL cert in
+ * particular) for use in constructing identity UI strings
+ */
+ getIdentityData : function() {
+ let result = {};
+ let status = this._lastStatus.QueryInterface(Components.interfaces.nsISSLStatus);
+ let cert = status.serverCert;
+
+ // Human readable name of Subject
+ result.subjectOrg = cert.organization;
+
+ // SubjectName fields, broken up for individual access
+ if (cert.subjectName) {
+ result.subjectNameFields = {};
+ cert.subjectName.split(",").forEach(function(v) {
+ let field = v.split("=");
+ this[field[0]] = field[1];
+ }, result.subjectNameFields);
+
+ // Call out city, state, and country specifically
+ result.city = result.subjectNameFields.L;
+ result.state = result.subjectNameFields.ST;
+ result.country = result.subjectNameFields.C;
+ }
+
+ // Human readable name of Certificate Authority
+ result.caOrg = cert.issuerOrganization || cert.issuerCommonName;
+ result.cert = cert;
+
+ return result;
+ },
+
+ /**
+ * Determines the identity mode corresponding to the icon we show in the urlbar.
+ */
+ getIdentityMode: function getIdentityMode(aState, uri) {
+ if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) {
+ return this.IDENTITY_MODE_VERIFIED;
+ }
+
+ if (aState & Ci.nsIWebProgressListener.STATE_IS_SECURE) {
+ return this.IDENTITY_MODE_IDENTIFIED;
+ }
+
+ // We also allow "about:" by allowing the selector to be empty (i.e. '(|.....|...|...)'
+ let whitelist = /^about:($|about|accounts|addons|buildconfig|cache|config|crashes|devices|downloads|fennec|firefox|feedback|healthreport|home|license|logins|logo|memory|mozilla|networking|plugins|privatebrowsing|rights|serviceworkers|support|telemetry|webrtc)($|\?)/i;
+ if (uri.schemeIs("about") && whitelist.test(uri.spec)) {
+ return this.IDENTITY_MODE_CHROMEUI;
+ }
+
+ return this.IDENTITY_MODE_UNKNOWN;
+ },
+
+ getMixedDisplayMode: function getMixedDisplayMode(aState) {
+ if (aState & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT) {
+ return this.MIXED_MODE_CONTENT_LOADED;
+ }
+
+ if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT) {
+ return this.MIXED_MODE_CONTENT_BLOCKED;
+ }
+
+ return this.MIXED_MODE_UNKNOWN;
+ },
+
+ getMixedActiveMode: function getActiveDisplayMode(aState) {
+ // Only show an indicator for loaded mixed content if the pref to block it is enabled
+ if ((aState & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) &&
+ !Services.prefs.getBoolPref("security.mixed_content.block_active_content")) {
+ return this.MIXED_MODE_CONTENT_LOADED;
+ }
+
+ if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT) {
+ return this.MIXED_MODE_CONTENT_BLOCKED;
+ }
+
+ return this.MIXED_MODE_UNKNOWN;
+ },
+
+ getTrackingMode: function getTrackingMode(aState, aBrowser) {
+ if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT) {
+ this.shieldHistogramAdd(aBrowser, 2);
+ return this.TRACKING_MODE_CONTENT_BLOCKED;
+ }
+
+ // Only show an indicator for loaded tracking content if the pref to block it is enabled
+ let tpEnabled = Services.prefs.getBoolPref("privacy.trackingprotection.enabled") ||
+ (Services.prefs.getBoolPref("privacy.trackingprotection.pbmode.enabled") &&
+ PrivateBrowsingUtils.isBrowserPrivate(aBrowser));
+
+ if ((aState & Ci.nsIWebProgressListener.STATE_LOADED_TRACKING_CONTENT) && tpEnabled) {
+ this.shieldHistogramAdd(aBrowser, 1);
+ return this.TRACKING_MODE_CONTENT_LOADED;
+ }
+
+ this.shieldHistogramAdd(aBrowser, 0);
+ return this.TRACKING_MODE_UNKNOWN;
+ },
+
+ shieldHistogramAdd: function(browser, value) {
+ if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
+ return;
+ }
+ Telemetry.addData("TRACKING_PROTECTION_SHIELD", value);
+ },
+
+ /**
+ * Determine the identity of the page being displayed by examining its SSL cert
+ * (if available). Return the data needed to update the UI.
+ */
+ checkIdentity: function checkIdentity(aState, aBrowser) {
+ this._lastStatus = aBrowser.securityUI
+ .QueryInterface(Components.interfaces.nsISSLStatusProvider)
+ .SSLStatus;
+
+ // Don't pass in the actual location object, since it can cause us to
+ // hold on to the window object too long. Just pass in the fields we
+ // care about. (bug 424829)
+ let locationObj = {};
+ try {
+ let location = aBrowser.contentWindow.location;
+ locationObj.host = location.host;
+ locationObj.hostname = location.hostname;
+ locationObj.port = location.port;
+ locationObj.origin = location.origin;
+ } catch (ex) {
+ // Can sometimes throw if the URL being visited has no host/hostname,
+ // e.g. about:blank. The _state for these pages means we won't need these
+ // properties anyways, though.
+ }
+ this._lastLocation = locationObj;
+
+ let uri = aBrowser.currentURI;
+ try {
+ uri = Services.uriFixup.createExposableURI(uri);
+ } catch (e) {}
+
+ let identityMode = this.getIdentityMode(aState, uri);
+ let mixedDisplay = this.getMixedDisplayMode(aState);
+ let mixedActive = this.getMixedActiveMode(aState);
+ let trackingMode = this.getTrackingMode(aState, aBrowser);
+ let result = {
+ origin: locationObj.origin,
+ mode: {
+ identity: identityMode,
+ mixed_display: mixedDisplay,
+ mixed_active: mixedActive,
+ tracking: trackingMode
+ }
+ };
+
+ // Don't show identity data for pages with an unknown identity or if any
+ // mixed content is loaded (mixed display content is loaded by default).
+ // We also return for CHROMEUI pages since they don't have any certificate
+ // information to load either. result.secure specifically refers to connection
+ // security, which is irrelevant for about: pages, as they're loaded locally.
+ if (identityMode == this.IDENTITY_MODE_UNKNOWN ||
+ identityMode == this.IDENTITY_MODE_CHROMEUI ||
+ aState & Ci.nsIWebProgressListener.STATE_IS_BROKEN) {
+ result.secure = false;
+ return result;
+ }
+
+ result.secure = true;
+
+ result.host = this.getEffectiveHost();
+
+ let iData = this.getIdentityData();
+ result.verifier = Strings.browser.formatStringFromName("identity.identified.verifier", [iData.caOrg], 1);
+
+ // If the cert is identified, then we can populate the results with credentials
+ if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) {
+ result.owner = iData.subjectOrg;
+
+ // Build an appropriate supplemental block out of whatever location data we have
+ let supplemental = "";
+ if (iData.city) {
+ supplemental += iData.city + "\n";
+ }
+ if (iData.state && iData.country) {
+ supplemental += Strings.browser.formatStringFromName("identity.identified.state_and_country", [iData.state, iData.country], 2);
+ result.country = iData.country;
+ } else if (iData.state) { // State only
+ supplemental += iData.state;
+ } else if (iData.country) { // Country only
+ supplemental += iData.country;
+ result.country = iData.country;
+ }
+ result.supplemental = supplemental;
+
+ return result;
+ }
+
+ // Cache the override service the first time we need to check it
+ if (!this._overrideService)
+ this._overrideService = Cc["@mozilla.org/security/certoverride;1"].getService(Ci.nsICertOverrideService);
+
+ // Check whether this site is a security exception. XPConnect does the right
+ // thing here in terms of converting _lastLocation.port from string to int, but
+ // the overrideService doesn't like undefined ports, so make sure we have
+ // something in the default case (bug 432241).
+ // .hostname can return an empty string in some exceptional cases -
+ // hasMatchingOverride does not handle that, so avoid calling it.
+ // Updating the tooltip value in those cases isn't critical.
+ // FIXME: Fixing bug 646690 would probably makes this check unnecessary
+ if (this._lastLocation.hostname &&
+ this._overrideService.hasMatchingOverride(this._lastLocation.hostname,
+ (this._lastLocation.port || 443),
+ iData.cert, {}, {}))
+ result.verifier = Strings.browser.GetStringFromName("identity.identified.verified_by_you");
+
+ return result;
+ },
+
+ /**
+ * Attempt to provide proper IDN treatment for host names
+ */
+ getEffectiveHost: function getEffectiveHost() {
+ if (!this._IDNService)
+ this._IDNService = Cc["@mozilla.org/network/idn-service;1"]
+ .getService(Ci.nsIIDNService);
+ try {
+ return this._IDNService.convertToDisplayIDN(this._uri.host, {});
+ } catch (e) {
+ // If something goes wrong (e.g. hostname is an IP address) just fail back
+ // to the full domain.
+ return this._lastLocation.hostname;
+ }
+ }
+};
+
+var SearchEngines = {
+ _contextMenuId: null,
+ PREF_SUGGEST_ENABLED: "browser.search.suggest.enabled",
+ PREF_SUGGEST_PROMPTED: "browser.search.suggest.prompted",
+
+ // Shared preference key used for search activity default engine.
+ PREF_SEARCH_ACTIVITY_ENGINE_KEY: "search.engines.defaultname",
+
+ init: function init() {
+ Services.obs.addObserver(this, "SearchEngines:Add", false);
+ Services.obs.addObserver(this, "SearchEngines:GetVisible", false);
+ Services.obs.addObserver(this, "SearchEngines:Remove", false);
+ Services.obs.addObserver(this, "SearchEngines:RestoreDefaults", false);
+ Services.obs.addObserver(this, "SearchEngines:SetDefault", false);
+ Services.obs.addObserver(this, "browser-search-engine-modified", false);
+ },
+
+ // Fetch list of search engines. all ? All engines : Visible engines only.
+ _handleSearchEnginesGetVisible: function _handleSearchEnginesGetVisible(rv, all) {
+ if (!Components.isSuccessCode(rv)) {
+ Cu.reportError("Could not initialize search service, bailing out.");
+ return;
+ }
+
+ let engineData = Services.search.getVisibleEngines({});
+
+ // Our Java UI assumes that the default engine is the first item in the array,
+ // so we need to make sure that's the case.
+ if (engineData[0] !== Services.search.defaultEngine) {
+ engineData = engineData.filter(engine => engine !== Services.search.defaultEngine);
+ engineData.unshift(Services.search.defaultEngine);
+ }
+
+ let searchEngines = engineData.map(function (engine) {
+ return {
+ name: engine.name,
+ identifier: engine.identifier,
+ iconURI: (engine.iconURI ? engine.iconURI.spec : null),
+ hidden: engine.hidden
+ };
+ });
+
+ let suggestTemplate = null;
+ let suggestEngine = null;
+
+ // Check to see if the default engine supports search suggestions. We only need to check
+ // the default engine because we only show suggestions for the default engine in the UI.
+ let engine = Services.search.defaultEngine;
+ if (engine.supportsResponseType("application/x-suggestions+json")) {
+ suggestEngine = engine.name;
+ suggestTemplate = engine.getSubmission("__searchTerms__", "application/x-suggestions+json").uri.spec;
+ }
+
+ // By convention, the currently configured default engine is at position zero in searchEngines.
+ Messaging.sendRequest({
+ type: "SearchEngines:Data",
+ searchEngines: searchEngines,
+ suggest: {
+ engine: suggestEngine,
+ template: suggestTemplate,
+ enabled: Services.prefs.getBoolPref(this.PREF_SUGGEST_ENABLED),
+ prompted: Services.prefs.getBoolPref(this.PREF_SUGGEST_PROMPTED)
+ }
+ });
+
+ // Send a speculative connection to the default engine.
+ Services.search.defaultEngine.speculativeConnect({window: window});
+ },
+
+ // Helper method to extract the engine name from a JSON. Simplifies the observe function.
+ _extractEngineFromJSON: function _extractEngineFromJSON(aData) {
+ let data = JSON.parse(aData);
+ return Services.search.getEngineByName(data.engine);
+ },
+
+ observe: function observe(aSubject, aTopic, aData) {
+ let engine;
+ switch(aTopic) {
+ case "SearchEngines:Add":
+ this.displaySearchEnginesList(aData);
+ break;
+ case "SearchEngines:GetVisible":
+ Services.search.init(this._handleSearchEnginesGetVisible.bind(this));
+ break;
+ case "SearchEngines:Remove":
+ // Make sure the engine isn't hidden before removing it, to make sure it's
+ // visible if the user later re-adds it (works around bug 341833)
+ engine = this._extractEngineFromJSON(aData);
+ engine.hidden = false;
+ Services.search.removeEngine(engine);
+ break;
+ case "SearchEngines:RestoreDefaults":
+ // Un-hides all default engines.
+ Services.search.restoreDefaultEngines();
+ break;
+ case "SearchEngines:SetDefault":
+ engine = this._extractEngineFromJSON(aData);
+ // Move the new default search engine to the top of the search engine list.
+ Services.search.moveEngine(engine, 0);
+ Services.search.defaultEngine = engine;
+ break;
+ case "browser-search-engine-modified":
+ if (aData == "engine-default") {
+ this._setSearchActivityDefaultPref(aSubject.QueryInterface(Ci.nsISearchEngine));
+ }
+ break;
+ default:
+ dump("Unexpected message type observed: " + aTopic);
+ break;
+ }
+ },
+
+ migrateSearchActivityDefaultPref: function migrateSearchActivityDefaultPref() {
+ Services.search.init(() => this._setSearchActivityDefaultPref(Services.search.defaultEngine));
+ },
+
+ // Updates the search activity pref when the default engine changes.
+ _setSearchActivityDefaultPref: function _setSearchActivityDefaultPref(engine) {
+ SharedPreferences.forApp().setCharPref(this.PREF_SEARCH_ACTIVITY_ENGINE_KEY, engine.name);
+ },
+
+ // Display context menu listing names of the search engines available to be added.
+ displaySearchEnginesList: function displaySearchEnginesList(aData) {
+ let data = JSON.parse(aData);
+ let tab = BrowserApp.getTabForId(data.tabId);
+
+ if (!tab)
+ return;
+
+ let browser = tab.browser;
+ let engines = browser.engines;
+
+ let p = new Prompt({
+ window: browser.contentWindow
+ }).setSingleChoiceItems(engines.map(function(e) {
+ return { label: e.title };
+ })).show((function(data) {
+ if (data.button == -1)
+ return;
+
+ this.addOpenSearchEngine(engines[data.button]);
+ engines.splice(data.button, 1);
+
+ if (engines.length < 1) {
+ // Broadcast message that there are no more add-able search engines.
+ let newEngineMessage = {
+ type: "Link:OpenSearch",
+ tabID: tab.id,
+ visible: false
+ };
+
+ Messaging.sendRequest(newEngineMessage);
+ }
+ }).bind(this));
+ },
+
+ addOpenSearchEngine: function addOpenSearchEngine(engine) {
+ Services.search.addEngine(engine.url, Ci.nsISearchEngine.DATA_XML, engine.iconURL, false, {
+ onSuccess: function() {
+ // Display a toast confirming addition of new search engine.
+ Snackbars.show(Strings.browser.formatStringFromName("alertSearchEngineAddedToast", [engine.title], 1), Snackbars.LENGTH_LONG);
+ },
+
+ onError: function(aCode) {
+ let errorMessage;
+ if (aCode == 2) {
+ // Engine is a duplicate.
+ errorMessage = "alertSearchEngineDuplicateToast";
+
+ } else {
+ // Unknown failure. Display general error message.
+ errorMessage = "alertSearchEngineErrorToast";
+ }
+
+ Snackbars.show(Strings.browser.formatStringFromName(errorMessage, [engine.title], 1), Snackbars.LENGTH_LONG);
+ }
+ });
+ },
+
+ /**
+ * Build and return an array of sorted form data / Query Parameters
+ * for an element in a submission form.
+ *
+ * @param element
+ * A valid submission element of a form.
+ */
+ _getSortedFormData: function(element) {
+ let formData = [];
+
+ for (let formElement of element.form.elements) {
+ if (!formElement.type) {
+ continue;
+ }
+
+ // Make this text field a generic search parameter.
+ if (element == formElement) {
+ formData.push({ name: formElement.name, value: "{searchTerms}" });
+ continue;
+ }
+
+ // Add other form elements as parameters.
+ switch (formElement.type.toLowerCase()) {
+ case "checkbox":
+ case "radio":
+ if (!formElement.checked) {
+ break;
+ }
+ case "text":
+ case "hidden":
+ case "textarea":
+ formData.push({ name: escape(formElement.name), value: escape(formElement.value) });
+ break;
+
+ case "select-one": {
+ for (let option of formElement.options) {
+ if (option.selected) {
+ formData.push({ name: escape(formElement.name), value: escape(formElement.value) });
+ break;
+ }
+ }
+ }
+ }
+ };
+
+ // Return valid, pre-sorted queryParams.
+ return formData.filter(a => a.name && a.value).sort((a, b) => {
+ // nsIBrowserSearchService.hasEngineWithURL() ensures sort, but this helps.
+ if (a.name > b.name) {
+ return 1;
+ }
+ if (b.name > a.name) {
+ return -1;
+ }
+
+ if (a.value > b.value) {
+ return 1;
+ }
+ if (b.value > a.value) {
+ return -1;
+ }
+
+ return 0;
+ });
+ },
+
+ /**
+ * Check if any search engines already handle an EngineURL of type
+ * URLTYPE_SEARCH_HTML, matching this request-method, formURL, and queryParams.
+ */
+ visibleEngineExists: function(element) {
+ let formData = this._getSortedFormData(element);
+
+ let form = element.form;
+ let method = form.method.toUpperCase();
+
+ let charset = element.ownerDocument.characterSet;
+ let docURI = Services.io.newURI(element.ownerDocument.URL, charset, null);
+ let formURL = Services.io.newURI(form.getAttribute("action"), charset, docURI).spec;
+
+ return Services.search.hasEngineWithURL(method, formURL, formData);
+ },
+
+ /**
+ * Adds a new search engine to the BrowserSearchService, based on its provided element. Prompts for an engine
+ * name, and appends a simple version-number in case of collision with an existing name.
+ *
+ * @return callback to handle success value. Currently used for ActionBarHandler.js and UI updates.
+ */
+ addEngine: function addEngine(aElement, resultCallback) {
+ let form = aElement.form;
+ let charset = aElement.ownerDocument.characterSet;
+ let docURI = Services.io.newURI(aElement.ownerDocument.URL, charset, null);
+ let formURL = Services.io.newURI(form.getAttribute("action"), charset, docURI).spec;
+ let method = form.method.toUpperCase();
+ let formData = this._getSortedFormData(aElement);
+
+ // prompt user for name of search engine
+ let promptTitle = Strings.browser.GetStringFromName("contextmenu.addSearchEngine3");
+ let title = { value: (aElement.ownerDocument.title || docURI.host) };
+ if (!Services.prompt.prompt(null, promptTitle, null, title, null, {})) {
+ if (resultCallback) {
+ resultCallback(false);
+ };
+ return;
+ }
+
+ // fetch the favicon for this page
+ let dbFile = FileUtils.getFile("ProfD", ["browser.db"]);
+ let mDBConn = Services.storage.openDatabase(dbFile);
+ let stmts = [];
+ stmts[0] = mDBConn.createStatement("SELECT favicon FROM history_with_favicons WHERE url = ?");
+ stmts[0].bindByIndex(0, docURI.spec);
+ let favicon = null;
+
+ Services.search.init(function addEngine_cb(rv) {
+ if (!Components.isSuccessCode(rv)) {
+ Cu.reportError("Could not initialize search service, bailing out.");
+ if (resultCallback) {
+ resultCallback(false);
+ };
+ return;
+ }
+
+ mDBConn.executeAsync(stmts, stmts.length, {
+ handleResult: function (results) {
+ let bytes = results.getNextRow().getResultByName("favicon");
+ if (bytes && bytes.length) {
+ favicon = "data:image/x-icon;base64," + btoa(String.fromCharCode.apply(null, bytes));
+ }
+ },
+ handleCompletion: function (reason) {
+ // if there's already an engine with this name, add a number to
+ // make the name unique (e.g., "Google" becomes "Google 2")
+ let name = title.value;
+ for (let i = 2; Services.search.getEngineByName(name); i++)
+ name = title.value + " " + i;
+
+ Services.search.addEngineWithDetails(name, favicon, null, null, method, formURL);
+ Snackbars.show(Strings.browser.formatStringFromName("alertSearchEngineAddedToast", [name], 1), Snackbars.LENGTH_LONG);
+
+ let engine = Services.search.getEngineByName(name);
+ engine.wrappedJSObject._queryCharset = charset;
+ formData.forEach(param => { engine.addParam(param.name, param.value, null); });
+
+ if (resultCallback) {
+ return resultCallback(true);
+ };
+ }
+ });
+ });
+ }
+};
+
+var ActivityObserver = {
+ init: function ao_init() {
+ Services.obs.addObserver(this, "application-background", false);
+ Services.obs.addObserver(this, "application-foreground", false);
+ },
+
+ observe: function ao_observe(aSubject, aTopic, aData) {
+ let isForeground = false;
+ let tab = BrowserApp.selectedTab;
+
+ UITelemetry.addEvent("show.1", "system", null, aTopic);
+
+ switch (aTopic) {
+ case "application-background" :
+ let doc = (tab ? tab.browser.contentDocument : null);
+ if (doc && doc.fullscreenElement) {
+ doc.exitFullscreen();
+ }
+ isForeground = false;
+ break;
+ case "application-foreground" :
+ isForeground = true;
+ break;
+ }
+
+ if (tab && tab.getActive() != isForeground) {
+ tab.setActive(isForeground);
+ }
+ }
+};
+
+var Telemetry = {
+ addData: function addData(aHistogramId, aValue) {
+ let histogram = Services.telemetry.getHistogramById(aHistogramId);
+ histogram.add(aValue);
+ },
+};
+
+var Experiments = {
+ // Enable malware download protection (bug 936041)
+ MALWARE_DOWNLOAD_PROTECTION: "malware-download-protection",
+
+ // Try to load pages from disk cache when network is offline (bug 935190)
+ OFFLINE_CACHE: "offline-cache",
+
+ init() {
+ Messaging.sendRequestForResult({
+ type: "Experiments:GetActive"
+ }).then(experiments => {
+ let names = JSON.parse(experiments);
+ for (let name of names) {
+ switch (name) {
+ case this.MALWARE_DOWNLOAD_PROTECTION: {
+ // Apply experiment preferences on the default branch. This allows
+ // us to avoid migrating user prefs when experiments are enabled/disabled,
+ // and it also allows users to override these prefs in about:config.
+ let defaults = Services.prefs.getDefaultBranch(null);
+ defaults.setBoolPref("browser.safebrowsing.downloads.enabled", true);
+ defaults.setBoolPref("browser.safebrowsing.downloads.remote.enabled", true);
+ continue;
+ }
+
+ case this.OFFLINE_CACHE: {
+ let defaults = Services.prefs.getDefaultBranch(null);
+ defaults.setBoolPref("browser.tabs.useCache", true);
+ continue;
+ }
+ }
+ }
+ });
+ },
+
+ setOverride(name, isEnabled) {
+ Messaging.sendRequest({
+ type: "Experiments:SetOverride",
+ name: name,
+ isEnabled: isEnabled
+ });
+ },
+
+ clearOverride(name) {
+ Messaging.sendRequest({
+ type: "Experiments:ClearOverride",
+ name: name
+ });
+ }
+};
+
+var ExternalApps = {
+ _contextMenuId: null,
+
+ // extend _getLink to pickup html5 media links.
+ _getMediaLink: function(aElement) {
+ let uri = NativeWindow.contextmenus._getLink(aElement);
+ if (uri == null && aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE && (aElement instanceof Ci.nsIDOMHTMLMediaElement)) {
+ try {
+ let mediaSrc = aElement.currentSrc || aElement.src;
+ uri = ContentAreaUtils.makeURI(mediaSrc, null, null);
+ } catch (e) {}
+ }
+ return uri;
+ },
+
+ init: function helper_init() {
+ this._contextMenuId = NativeWindow.contextmenus.add(function(aElement) {
+ let uri = null;
+ var node = aElement;
+ while (node && !uri) {
+ uri = ExternalApps._getMediaLink(node);
+ node = node.parentNode;
+ }
+ let apps = [];
+ if (uri)
+ apps = HelperApps.getAppsForUri(uri);
+
+ return apps.length == 1 ? Strings.browser.formatStringFromName("helperapps.openWithApp2", [apps[0].name], 1) :
+ Strings.browser.GetStringFromName("helperapps.openWithList2");
+ }, this.filter, this.openExternal);
+ },
+
+ filter: {
+ matches: function(aElement) {
+ let uri = ExternalApps._getMediaLink(aElement);
+ let apps = [];
+ if (uri) {
+ apps = HelperApps.getAppsForUri(uri);
+ }
+ return apps.length > 0;
+ }
+ },
+
+ openExternal: function(aElement) {
+ if (aElement.pause) {
+ aElement.pause();
+ }
+ let uri = ExternalApps._getMediaLink(aElement);
+ HelperApps.launchUri(uri);
+ },
+
+ shouldCheckUri: function(uri) {
+ if (!(uri.schemeIs("http") || uri.schemeIs("https") || uri.schemeIs("file"))) {
+ return false;
+ }
+
+ return true;
+ },
+
+ updatePageAction: function updatePageAction(uri, contentDocument) {
+ HelperApps.getAppsForUri(uri, { filterHttp: true }, (apps) => {
+ this.clearPageAction();
+ if (apps.length > 0)
+ this._setUriForPageAction(uri, apps, contentDocument);
+ });
+ },
+
+ updatePageActionUri: function updatePageActionUri(uri) {
+ this._pageActionUri = uri;
+ },
+
+ _getMediaContentElement(contentDocument) {
+ if (!contentDocument.contentType.startsWith("video/") &&
+ !contentDocument.contentType.startsWith("audio/")) {
+ return null;
+ }
+
+ let element = contentDocument.activeElement;
+
+ if (element instanceof HTMLBodyElement) {
+ element = element.firstChild;
+ }
+
+ if (element instanceof HTMLMediaElement) {
+ return element;
+ }
+
+ return null;
+ },
+
+ _setUriForPageAction: function setUriForPageAction(uri, apps, contentDocument) {
+ this.updatePageActionUri(uri);
+
+ // If the pageaction is already added, simply update the URI to be launched when 'onclick' is triggered.
+ if (this._pageActionId != undefined)
+ return;
+
+ let mediaElement = this._getMediaContentElement(contentDocument);
+
+ this._pageActionId = PageActions.add({
+ title: Strings.browser.GetStringFromName("openInApp.pageAction"),
+ icon: "drawable://icon_openinapp",
+
+ clickCallback: () => {
+ UITelemetry.addEvent("launch.1", "pageaction", null, "helper");
+
+ let wasPlaying = mediaElement && !mediaElement.paused && !mediaElement.ended;
+ if (wasPlaying) {
+ mediaElement.pause();
+ }
+
+ if (apps.length > 1) {
+ // Use the HelperApps prompt here to filter out any Http handlers
+ HelperApps.prompt(apps, {
+ title: Strings.browser.GetStringFromName("openInApp.pageAction"),
+ buttons: [
+ Strings.browser.GetStringFromName("openInApp.ok"),
+ Strings.browser.GetStringFromName("openInApp.cancel")
+ ]
+ }, (result) => {
+ if (result.button != 0) {
+ if (wasPlaying) {
+ mediaElement.play();
+ }
+
+ return;
+ }
+ apps[result.icongrid0].launch(this._pageActionUri);
+ });
+ } else {
+ apps[0].launch(this._pageActionUri);
+ }
+ }
+ });
+ },
+
+ clearPageAction: function clearPageAction() {
+ if(!this._pageActionId)
+ return;
+
+ PageActions.remove(this._pageActionId);
+ delete this._pageActionId;
+ },
+};
+
+var Distribution = {
+ // File used to store campaign data
+ _file: null,
+
+ _preferencesJSON: null,
+
+ init: function dc_init() {
+ Services.obs.addObserver(this, "Distribution:Changed", false);
+ Services.obs.addObserver(this, "Distribution:Set", false);
+ Services.obs.addObserver(this, "prefservice:after-app-defaults", false);
+ Services.obs.addObserver(this, "Campaign:Set", false);
+
+ // Look for file outside the APK:
+ // /data/data/org.mozilla.xxx/distribution.json
+ this._file = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
+ this._file.append("distribution.json");
+ this.readJSON(this._file, this.update);
+ },
+
+ observe: function dc_observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "Distribution:Changed":
+ // Re-init the search service.
+ try {
+ Services.search._asyncReInit();
+ } catch (e) {
+ console.log("Unable to reinit search service.");
+ }
+ // Fall through.
+
+ case "Distribution:Set":
+ if (aData) {
+ try {
+ this._preferencesJSON = JSON.parse(aData);
+ } catch (e) {
+ console.log("Invalid distribution JSON.");
+ }
+ }
+ // Reload the default prefs so we can observe "prefservice:after-app-defaults"
+ Services.prefs.QueryInterface(Ci.nsIObserver).observe(null, "reload-default-prefs", null);
+ this.installDistroAddons();
+ break;
+
+ case "prefservice:after-app-defaults":
+ this.getPrefs();
+ break;
+
+ case "Campaign:Set": {
+ // Update the prefs for this session
+ try {
+ this.update(JSON.parse(aData));
+ } catch (ex) {
+ Cu.reportError("Distribution: Could not parse JSON: " + ex);
+ return;
+ }
+
+ // Asynchronously copy the data to the file.
+ let array = new TextEncoder().encode(aData);
+ OS.File.writeAtomic(this._file.path, array, { tmpPath: this._file.path + ".tmp" });
+ break;
+ }
+ }
+ },
+
+ update: function dc_update(aData) {
+ // Force the distribution preferences on the default branch
+ let defaults = Services.prefs.getDefaultBranch(null);
+ defaults.setCharPref("distribution.id", aData.id);
+ defaults.setCharPref("distribution.version", aData.version);
+ },
+
+ getPrefs: function dc_getPrefs() {
+ if (this._preferencesJSON) {
+ this.applyPrefs(this._preferencesJSON);
+ this._preferencesJSON = null;
+ return;
+ }
+
+ // Get the distribution directory, and bail if it doesn't exist.
+ let file = FileUtils.getDir("XREAppDist", [], false);
+ if (!file.exists())
+ return;
+
+ file.append("preferences.json");
+ this.readJSON(file, this.applyPrefs);
+ },
+
+ applyPrefs: function dc_applyPrefs(aData) {
+ // Check for required Global preferences
+ let global = aData["Global"];
+ if (!(global && global["id"] && global["version"] && global["about"])) {
+ Cu.reportError("Distribution: missing or incomplete Global preferences");
+ return;
+ }
+
+ // Force the distribution preferences on the default branch
+ let defaults = Services.prefs.getDefaultBranch(null);
+ defaults.setCharPref("distribution.id", global["id"]);
+ defaults.setCharPref("distribution.version", global["version"]);
+
+ let locale = BrowserApp.getUALocalePref();
+ let aboutString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+ aboutString.data = global["about." + locale] || global["about"];
+ defaults.setComplexValue("distribution.about", Ci.nsISupportsString, aboutString);
+
+ let prefs = aData["Preferences"];
+ for (let key in prefs) {
+ try {
+ let value = prefs[key];
+ switch (typeof value) {
+ case "boolean":
+ defaults.setBoolPref(key, value);
+ break;
+ case "number":
+ defaults.setIntPref(key, value);
+ break;
+ case "string":
+ case "undefined":
+ defaults.setCharPref(key, value);
+ break;
+ }
+ } catch (e) { /* ignore bad prefs and move on */ }
+ }
+
+ // Apply a lightweight theme if necessary
+ if (prefs && prefs["lightweightThemes.selectedThemeID"]) {
+ Services.obs.notifyObservers(null, "lightweight-theme-apply", "");
+ }
+
+ let localizedString = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(Ci.nsIPrefLocalizedString);
+ let localizeablePrefs = aData["LocalizablePreferences"];
+ for (let key in localizeablePrefs) {
+ try {
+ let value = localizeablePrefs[key];
+ value = value.replace(/%LOCALE%/g, locale);
+ localizedString.data = "data:text/plain," + key + "=" + value;
+ defaults.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedString);
+ } catch (e) { /* ignore bad prefs and move on */ }
+ }
+
+ let localizeablePrefsOverrides = aData["LocalizablePreferences." + locale];
+ for (let key in localizeablePrefsOverrides) {
+ try {
+ let value = localizeablePrefsOverrides[key];
+ localizedString.data = "data:text/plain," + key + "=" + value;
+ defaults.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedString);
+ } catch (e) { /* ignore bad prefs and move on */ }
+ }
+
+ Messaging.sendRequest({ type: "Distribution:Set:OK" });
+ },
+
+ // aFile is an nsIFile
+ // aCallback takes the parsed JSON object as a parameter
+ readJSON: function dc_readJSON(aFile, aCallback) {
+ Task.spawn(function() {
+ let bytes = yield OS.File.read(aFile.path);
+ let raw = new TextDecoder().decode(bytes) || "";
+
+ try {
+ aCallback(JSON.parse(raw));
+ } catch (e) {
+ Cu.reportError("Distribution: Could not parse JSON: " + e);
+ }
+ }).then(null, function onError(reason) {
+ if (!(reason instanceof OS.File.Error && reason.becauseNoSuchFile)) {
+ Cu.reportError("Distribution: Could not read from " + aFile.leafName + " file");
+ }
+ });
+ },
+
+ // Track pending installs so we can avoid showing notifications for them.
+ pendingAddonInstalls: new Set(),
+
+ installDistroAddons: Task.async(function* () {
+ const PREF_ADDONS_INSTALLED = "distribution.addonsInstalled";
+ try {
+ let installed = Services.prefs.getBoolPref(PREF_ADDONS_INSTALLED);
+ if (installed) {
+ return;
+ }
+ } catch (e) {
+ Services.prefs.setBoolPref(PREF_ADDONS_INSTALLED, true);
+ }
+
+ let distroPath;
+ try {
+ distroPath = FileUtils.getDir("XREAppDist", ["extensions"]).path;
+
+ let info = yield OS.File.stat(distroPath);
+ if (!info.isDir) {
+ return;
+ }
+ } catch (e) {
+ return;
+ }
+
+ let it = new OS.File.DirectoryIterator(distroPath);
+ try {
+ yield it.forEach(entry => {
+ // Only support extensions that are zipped in .xpi files.
+ if (entry.isDir || !entry.name.endsWith(".xpi")) {
+ dump("Ignoring distribution add-on that isn't an XPI: " + entry.path);
+ return;
+ }
+
+ new Promise((resolve, reject) => {
+ AddonManager.getInstallForFile(new FileUtils.File(entry.path), resolve);
+ }).then(install => {
+ let id = entry.name.substring(0, entry.name.length - 4);
+ if (install.addon.id !== id) {
+ Cu.reportError("File entry " + entry.path + " contains an add-on with an incorrect ID");
+ return;
+ }
+ this.pendingAddonInstalls.add(install);
+ install.install();
+ }).catch(e => {
+ Cu.reportError("Error installing distribution add-on: " + entry.path + ": " + e);
+ });
+ });
+ } finally {
+ it.close();
+ }
+ })
+};
+
+var Tabs = {
+ _enableTabExpiration: false,
+ _useCache: false,
+ _domains: new Set(),
+
+ init: function() {
+ // On low-memory platforms, always allow tab expiration. On high-mem
+ // platforms, allow it to be turned on once we hit a low-mem situation.
+ if (BrowserApp.isOnLowMemoryPlatform) {
+ this._enableTabExpiration = true;
+ } else {
+ Services.obs.addObserver(this, "memory-pressure", false);
+ }
+
+ // Watch for opportunities to pre-connect to high probability targets.
+ Services.obs.addObserver(this, "Session:Prefetch", false);
+
+ // Track the network connection so we can efficiently use the cache
+ // for possible offline rendering.
+ Services.obs.addObserver(this, "network:link-status-changed", false);
+ let network = Cc["@mozilla.org/network/network-link-service;1"].getService(Ci.nsINetworkLinkService);
+ this.useCache = !network.isLinkUp;
+
+ BrowserApp.deck.addEventListener("pageshow", this, false);
+ BrowserApp.deck.addEventListener("TabOpen", this, false);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "memory-pressure":
+ if (aData != "heap-minimize") {
+ // We received a low-memory related notification. This will enable
+ // expirations.
+ this._enableTabExpiration = true;
+ Services.obs.removeObserver(this, "memory-pressure");
+ } else {
+ // Use "heap-minimize" as a trigger to expire the most stale tab.
+ this.expireLruTab();
+ }
+ break;
+ case "Session:Prefetch":
+ if (aData) {
+ try {
+ let uri = Services.io.newURI(aData, null, null);
+ if (uri && !this._domains.has(uri.host)) {
+ Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
+ this._domains.add(uri.host);
+ }
+ } catch (e) {}
+ }
+ break;
+ case "network:link-status-changed":
+ if (["down", "unknown", "up"].indexOf(aData) == -1) {
+ return;
+ }
+ this.useCache = (aData === "down");
+ break;
+ }
+ },
+
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case "pageshow":
+ // Clear the domain cache whenever a page is loaded into any browser.
+ this._domains.clear();
+
+ break;
+ case "TabOpen":
+ // Use opening a new tab as a trigger to expire the most stale tab.
+ this.expireLruTab();
+ break;
+ }
+ },
+
+ // Manage the most-recently-used list of tabs. Each tab has a timestamp
+ // associated with it that indicates when it was last touched.
+ expireLruTab: function() {
+ if (!this._enableTabExpiration) {
+ return false;
+ }
+ let expireTimeMs = Services.prefs.getIntPref("browser.tabs.expireTime") * 1000;
+ if (expireTimeMs < 0) {
+ // This behaviour is disabled.
+ return false;
+ }
+ let tabs = BrowserApp.tabs;
+ let selected = BrowserApp.selectedTab;
+ let lruTab = null;
+ // Find the least recently used non-zombie tab.
+ for (let i = 0; i < tabs.length; i++) {
+ if (tabs[i] == selected ||
+ tabs[i].browser.__SS_restore ||
+ tabs[i].playingAudio) {
+ // This tab is selected, is already a zombie, or is currently playing
+ // audio, skip it.
+ continue;
+ }
+ if (lruTab == null || tabs[i].lastTouchedAt < lruTab.lastTouchedAt) {
+ lruTab = tabs[i];
+ }
+ }
+ // If the tab was last touched more than browser.tabs.expireTime seconds ago,
+ // zombify it.
+ if (lruTab) {
+ if (Date.now() - lruTab.lastTouchedAt > expireTimeMs) {
+ MemoryObserver.zombify(lruTab);
+ return true;
+ }
+ }
+ return false;
+ },
+
+ get useCache() {
+ if (!Services.prefs.getBoolPref("browser.tabs.useCache")) {
+ return false;
+ }
+ return this._useCache;
+ },
+
+ set useCache(aUseCache) {
+ if (!Services.prefs.getBoolPref("browser.tabs.useCache")) {
+ return;
+ }
+
+ if (this._useCache == aUseCache) {
+ return;
+ }
+
+ BrowserApp.tabs.forEach(function(tab) {
+ if (tab.browser && tab.browser.docShell) {
+ if (aUseCache) {
+ tab.browser.docShell.defaultLoadFlags |= Ci.nsIRequest.LOAD_FROM_CACHE;
+ } else {
+ tab.browser.docShell.defaultLoadFlags &= ~Ci.nsIRequest.LOAD_FROM_CACHE;
+ }
+ }
+ });
+ this._useCache = aUseCache;
+ },
+
+ // For debugging
+ dump: function(aPrefix) {
+ let tabs = BrowserApp.tabs;
+ for (let i = 0; i < tabs.length; i++) {
+ dump(aPrefix + " | " + "Tab [" + tabs[i].browser.contentWindow.location.href + "]: lastTouchedAt:" + tabs[i].lastTouchedAt + ", zombie:" + tabs[i].browser.__SS_restore);
+ }
+ },
+};
+
+function ContextMenuItem(args) {
+ this.id = uuidgen.generateUUID().toString();
+ this.args = args;
+}
+
+ContextMenuItem.prototype = {
+ get order() {
+ return this.args.order || 0;
+ },
+
+ matches: function(elt, x, y) {
+ return this.args.selector.matches(elt, x, y);
+ },
+
+ callback: function(elt) {
+ this.args.callback(elt);
+ },
+
+ addVal: function(name, elt, defaultValue) {
+ if (!(name in this.args))
+ return defaultValue;
+
+ if (typeof this.args[name] == "function")
+ return this.args[name](elt);
+
+ return this.args[name];
+ },
+
+ getValue: function(elt) {
+ return {
+ id: this.id,
+ label: this.addVal("label", elt),
+ showAsActions: this.addVal("showAsActions", elt),
+ icon: this.addVal("icon", elt),
+ isGroup: this.addVal("isGroup", elt, false),
+ inGroup: this.addVal("inGroup", elt, false),
+ disabled: this.addVal("disabled", elt, false),
+ selected: this.addVal("selected", elt, false),
+ isParent: this.addVal("isParent", elt, false),
+ };
+ }
+}
+
+function HTMLContextMenuItem(elt, target) {
+ ContextMenuItem.call(this, { });
+
+ this.menuElementRef = Cu.getWeakReference(elt);
+ this.targetElementRef = Cu.getWeakReference(target);
+}
+
+HTMLContextMenuItem.prototype = Object.create(ContextMenuItem.prototype, {
+ order: {
+ value: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER
+ },
+
+ matches: {
+ value: function(target) {
+ let t = this.targetElementRef.get();
+ return t === target;
+ },
+ },
+
+ callback: {
+ value: function(target) {
+ let elt = this.menuElementRef.get();
+ if (!elt) {
+ return;
+ }
+
+ // If this is a menu item, show a new context menu with the submenu in it
+ if (elt instanceof Ci.nsIDOMHTMLMenuElement) {
+ try {
+ NativeWindow.contextmenus.menus = {};
+
+ let elt = this.menuElementRef.get();
+ let target = this.targetElementRef.get();
+ if (!elt) {
+ return;
+ }
+
+ var items = NativeWindow.contextmenus._getHTMLContextMenuItemsForMenu(elt, target);
+ // This menu will always only have one context, but we still make sure its the "right" one.
+ var context = NativeWindow.contextmenus._getContextType(target);
+ if (items.length > 0) {
+ NativeWindow.contextmenus._addMenuItems(items, context);
+ }
+
+ } catch(ex) {
+ Cu.reportError(ex);
+ }
+ } else {
+ // otherwise just click the menu item
+ elt.click();
+ }
+ },
+ },
+
+ getValue: {
+ value: function(target) {
+ let elt = this.menuElementRef.get();
+ if (!elt) {
+ return null;
+ }
+
+ if (elt.hasAttribute("hidden")) {
+ return null;
+ }
+
+ return {
+ id: this.id,
+ icon: elt.icon,
+ label: elt.label,
+ disabled: elt.disabled,
+ menu: elt instanceof Ci.nsIDOMHTMLMenuElement
+ };
+ }
+ },
+});
+
diff --git a/mobile/android/chrome/content/browser.xul b/mobile/android/chrome/content/browser.xul
new file mode 100644
index 0000000000..8072a7a1c6
--- /dev/null
+++ b/mobile/android/chrome/content/browser.xul
@@ -0,0 +1,17 @@
+<?xml version="1.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/. -->
+
+<?xml-stylesheet href="chrome://browser/content/browser.css" type="text/css"?>
+
+<window id="main-window"
+ onload="BrowserApp.startup();"
+ windowtype="navigator:browser"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript" src="chrome://browser/content/browser.js"/>
+
+ <deck id="browsers" flex="1"/>
+
+</window>
diff --git a/mobile/android/chrome/content/config.js b/mobile/android/chrome/content/config.js
new file mode 100644
index 0000000000..2c868f175e
--- /dev/null
+++ b/mobile/android/chrome/content/config.js
@@ -0,0 +1,673 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var {classes: Cc, interfaces: Ci, manager: Cm, utils: Cu} = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+
+const VKB_ENTER_KEY = 13; // User press of VKB enter key
+const INITIAL_PAGE_DELAY = 500; // Initial pause on program start for scroll alignment
+const PREFS_BUFFER_MAX = 30; // Max prefs buffer size for getPrefsBuffer()
+const PAGE_SCROLL_TRIGGER = 200; // Triggers additional getPrefsBuffer() on user scroll-to-bottom
+const FILTER_CHANGE_TRIGGER = 200; // Delay between responses to filterInput changes
+const INNERHTML_VALUE_DELAY = 100; // Delay before providing prefs innerHTML value
+
+var gStringBundle = Services.strings.createBundle("chrome://browser/locale/config.properties");
+var gClipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
+
+
+/* ============================== NewPrefDialog ==============================
+ *
+ * New Preference Dialog Object and methods
+ *
+ * Implements User Interfaces for creation of a single(new) Preference setting
+ *
+ */
+var NewPrefDialog = {
+
+ _prefsShield: null,
+
+ _newPrefsDialog: null,
+ _newPrefItem: null,
+ _prefNameInputElt: null,
+ _prefTypeSelectElt: null,
+
+ _booleanValue: null,
+ _booleanToggle: null,
+ _stringValue: null,
+ _intValue: null,
+
+ _positiveButton: null,
+
+ get type() {
+ return this._prefTypeSelectElt.value;
+ },
+
+ set type(aType) {
+ this._prefTypeSelectElt.value = aType;
+ switch(this._prefTypeSelectElt.value) {
+ case "boolean":
+ this._prefTypeSelectElt.selectedIndex = 0;
+ break;
+ case "string":
+ this._prefTypeSelectElt.selectedIndex = 1;
+ break;
+ case "int":
+ this._prefTypeSelectElt.selectedIndex = 2;
+ break;
+ }
+
+ this._newPrefItem.setAttribute("typestyle", aType);
+ },
+
+ // Init the NewPrefDialog
+ init: function AC_init() {
+ this._prefsShield = document.getElementById("prefs-shield");
+
+ this._newPrefsDialog = document.getElementById("new-pref-container");
+ this._newPrefItem = document.getElementById("new-pref-item");
+ this._prefNameInputElt = document.getElementById("new-pref-name");
+ this._prefTypeSelectElt = document.getElementById("new-pref-type");
+
+ this._booleanValue = document.getElementById("new-pref-value-boolean");
+ this._stringValue = document.getElementById("new-pref-value-string");
+ this._intValue = document.getElementById("new-pref-value-int");
+
+ this._positiveButton = document.getElementById("positive-button");
+ },
+
+ // Called to update positive button to display text ("Create"/"Change), and enabled/disabled status
+ // As new pref name is initially displayed, re-focused, or modifed during user input
+ _updatePositiveButton: function AC_updatePositiveButton(aPrefName) {
+ this._positiveButton.textContent = gStringBundle.GetStringFromName("newPref.createButton");
+ this._positiveButton.setAttribute("disabled", true);
+ if (aPrefName == "") {
+ return;
+ }
+
+ // If item already in list, it's being changed, else added
+ let item = AboutConfig._list.filter(i => { return i.name == aPrefName });
+ if (item.length) {
+ this._positiveButton.textContent = gStringBundle.GetStringFromName("newPref.changeButton");
+ } else {
+ this._positiveButton.removeAttribute("disabled");
+ }
+ },
+
+ // When we want to cancel/hide an existing, or show a new pref dialog
+ toggleShowHide: function AC_toggleShowHide() {
+ if (this._newPrefsDialog.classList.contains("show")) {
+ this.hide();
+ } else {
+ this._show();
+ }
+ },
+
+ // When we want to show the new pref dialog / shield the prefs list
+ _show: function AC_show() {
+ this._newPrefsDialog.classList.add("show");
+ this._prefsShield.setAttribute("shown", true);
+
+ // Initial default field values
+ this._prefNameInputElt.value = "";
+ this._updatePositiveButton(this._prefNameInputElt.value);
+
+ this.type = "boolean";
+ this._booleanValue.value = "false";
+ this._stringValue.value = "";
+ this._intValue.value = "";
+
+ this._prefNameInputElt.focus();
+
+ window.addEventListener("keypress", this.handleKeypress, false);
+ },
+
+ // When we want to cancel/hide the new pref dialog / un-shield the prefs list
+ hide: function AC_hide() {
+ this._newPrefsDialog.classList.remove("show");
+ this._prefsShield.removeAttribute("shown");
+
+ window.removeEventListener("keypress", this.handleKeypress, false);
+ },
+
+ // Watch user key input so we can provide Enter key action, commit input values
+ handleKeypress: function AC_handleKeypress(aEvent) {
+ // Close our VKB on new pref enter key press
+ if (aEvent.keyCode == VKB_ENTER_KEY)
+ aEvent.target.blur();
+ },
+
+ // New prefs create dialog only allows creating a non-existing preference, doesn't allow for
+ // Changing an existing one on-the-fly, tap existing/displayed line item pref for that
+ create: function AC_create(aEvent) {
+ if (this._positiveButton.getAttribute("disabled") == "true") {
+ return;
+ }
+
+ switch(this.type) {
+ case "boolean":
+ Services.prefs.setBoolPref(this._prefNameInputElt.value, (this._booleanValue.value == "true") ? true : false);
+ break;
+ case "string":
+ Services.prefs.setCharPref(this._prefNameInputElt.value, this._stringValue.value);
+ break;
+ case "int":
+ Services.prefs.setIntPref(this._prefNameInputElt.value, this._intValue.value);
+ break;
+ }
+
+ // Ensure pref adds flushed to disk immediately
+ Services.prefs.savePrefFile(null);
+
+ this.hide();
+ },
+
+ // Display proper positive button text/state on new prefs name input focus
+ focusName: function AC_focusName(aEvent) {
+ this._updatePositiveButton(aEvent.target.value);
+ },
+
+ // Display proper positive button text/state as user changes new prefs name
+ updateName: function AC_updateName(aEvent) {
+ this._updatePositiveButton(aEvent.target.value);
+ },
+
+ // In new prefs dialog, bool prefs are <input type="text">, as they aren't yet tied to an
+ // Actual Services.prefs.*etBoolPref()
+ toggleBoolValue: function AC_toggleBoolValue() {
+ this._booleanValue.value = (this._booleanValue.value == "true" ? "false" : "true");
+ }
+}
+
+
+/* ============================== AboutConfig ==============================
+ *
+ * Main AboutConfig object and methods
+ *
+ * Implements User Interfaces for maintenance of a list of Preference settings
+ *
+ */
+var AboutConfig = {
+
+ contextMenuLINode: null,
+ filterInput: null,
+ _filterPrevInput: null,
+ _filterChangeTimer: null,
+ _prefsContainer: null,
+ _loadingContainer: null,
+ _list: null,
+
+ // Init the main AboutConfig dialog
+ init: function AC_init() {
+ this.filterInput = document.getElementById("filter-input");
+ this._prefsContainer = document.getElementById("prefs-container");
+ this._loadingContainer = document.getElementById("loading-container");
+
+ let list = Services.prefs.getChildList("");
+ this._list = list.sort().map( function AC_getMapPref(aPref) {
+ return new Pref(aPref);
+ }, this);
+
+ // Support filtering about:config via a ?filter=<string> param
+ let match = /[?&]filter=([^&]+)/i.exec(window.location.href);
+ if (match) {
+ this.filterInput.value = decodeURIComponent(match[1]);
+ }
+
+ // Display the current prefs list (retains searchFilter value)
+ this.bufferFilterInput();
+
+ // Setup the prefs observers
+ Services.prefs.addObserver("", this, false);
+ },
+
+ // Uninit the main AboutConfig dialog
+ uninit: function AC_uninit() {
+ // Remove the prefs observer
+ Services.prefs.removeObserver("", this);
+ },
+
+ // Clear the filterInput value, to display the entire list
+ clearFilterInput: function AC_clearFilterInput() {
+ this.filterInput.value = "";
+ this.bufferFilterInput();
+ },
+
+ // Buffer down rapid changes in filterInput value from keyboard
+ bufferFilterInput: function AC_bufferFilterInput() {
+ if (this._filterChangeTimer) {
+ clearTimeout(this._filterChangeTimer);
+ }
+
+ this._filterChangeTimer = setTimeout((function() {
+ this._filterChangeTimer = null;
+ // Display updated prefs list when filterInput value settles
+ this._displayNewList();
+ }).bind(this), FILTER_CHANGE_TRIGGER);
+ },
+
+ // Update displayed list when filterInput value changes
+ _displayNewList: function AC_displayNewList() {
+ // This survives the search filter value past a page refresh
+ this.filterInput.setAttribute("value", this.filterInput.value);
+
+ // Don't start new filter search if same as last
+ if (this.filterInput.value == this._filterPrevInput) {
+ return;
+ }
+ this._filterPrevInput = this.filterInput.value;
+
+ // Clear list item selection / context menu, prefs list, get first buffer, set scrolling on
+ this.selected = "";
+ this._clearPrefsContainer();
+ this._addMorePrefsToContainer();
+ window.onscroll = this.onScroll.bind(this);
+
+ // Pause for screen to settle, then ensure at top
+ setTimeout((function() {
+ window.scrollTo(0, 0);
+ }).bind(this), INITIAL_PAGE_DELAY);
+ },
+
+ // Clear the displayed preferences list
+ _clearPrefsContainer: function AC_clearPrefsContainer() {
+ // Quick clear the prefsContainer list
+ let empty = this._prefsContainer.cloneNode(false);
+ this._prefsContainer.parentNode.replaceChild(empty, this._prefsContainer);
+ this._prefsContainer = empty;
+
+ // Quick clear the prefs li.HTML list
+ this._list.forEach(function(item) {
+ delete item.li;
+ });
+ },
+
+ // Get a small manageable block of prefs items, and add them to the displayed list
+ _addMorePrefsToContainer: function AC_addMorePrefsToContainer() {
+ // Create filter regex
+ let filterExp = this.filterInput.value ?
+ new RegExp(this.filterInput.value, "i") : null;
+
+ // Get a new block for the display list
+ let prefsBuffer = [];
+ for (let i = 0; i < this._list.length && prefsBuffer.length < PREFS_BUFFER_MAX; i++) {
+ if (!this._list[i].li && this._list[i].test(filterExp)) {
+ prefsBuffer.push(this._list[i]);
+ }
+ }
+
+ // Add the new block to the displayed list
+ for (let i = 0; i < prefsBuffer.length; i++) {
+ this._prefsContainer.appendChild(prefsBuffer[i].getOrCreateNewLINode());
+ }
+
+ // Determine if anything left to add later by scrolling
+ let anotherPrefsBufferRemains = false;
+ for (let i = 0; i < this._list.length; i++) {
+ if (!this._list[i].li && this._list[i].test(filterExp)) {
+ anotherPrefsBufferRemains = true;
+ break;
+ }
+ }
+
+ if (anotherPrefsBufferRemains) {
+ // If still more could be displayed, show the throbber
+ this._loadingContainer.style.display = "block";
+ } else {
+ // If no more could be displayed, hide the throbber, and stop noticing scroll events
+ this._loadingContainer.style.display = "none";
+ window.onscroll = null;
+ }
+ },
+
+ // If scrolling at the bottom, maybe add some more entries
+ onScroll: function AC_onScroll(aEvent) {
+ if (this._prefsContainer.scrollHeight - (window.pageYOffset + window.innerHeight) < PAGE_SCROLL_TRIGGER) {
+ if (!this._filterChangeTimer) {
+ this._addMorePrefsToContainer();
+ }
+ }
+ },
+
+
+ // Return currently selected list item node
+ get selected() {
+ return document.querySelector(".pref-item.selected");
+ },
+
+ // Set list item node as selected
+ set selected(aSelection) {
+ let currentSelection = this.selected;
+ if (aSelection == currentSelection) {
+ return;
+ }
+
+ // Clear any previous selection
+ if (currentSelection) {
+ currentSelection.classList.remove("selected");
+ currentSelection.removeEventListener("keypress", this.handleKeypress, false);
+ }
+
+ // Set any current selection
+ if (aSelection) {
+ aSelection.classList.add("selected");
+ aSelection.addEventListener("keypress", this.handleKeypress, false);
+ }
+ },
+
+ // Watch user key input so we can provide Enter key action, commit input values
+ handleKeypress: function AC_handleKeypress(aEvent) {
+ if (aEvent.keyCode == VKB_ENTER_KEY)
+ aEvent.target.blur();
+ },
+
+ // Return the target list item node of an action event
+ getLINodeForEvent: function AC_getLINodeForEvent(aEvent) {
+ let node = aEvent.target;
+ while (node && node.nodeName != "li") {
+ node = node.parentNode;
+ }
+
+ return node;
+ },
+
+ // Return a pref of a list item node
+ _getPrefForNode: function AC_getPrefForNode(aNode) {
+ let pref = aNode.getAttribute("name");
+
+ return new Pref(pref);
+ },
+
+ // When list item name or value are tapped
+ selectOrToggleBoolPref: function AC_selectOrToggleBoolPref(aEvent) {
+ let node = this.getLINodeForEvent(aEvent);
+
+ // If not already selected, just do so
+ if (this.selected != node) {
+ this.selected = node;
+ return;
+ }
+
+ // If already selected, and value is boolean, toggle it
+ let pref = this._getPrefForNode(node);
+ if (pref.type != Services.prefs.PREF_BOOL) {
+ return;
+ }
+
+ this.toggleBoolPref(aEvent);
+ },
+
+ // When finalizing list input values due to blur
+ setIntOrStringPref: function AC_setIntOrStringPref(aEvent) {
+ let node = this.getLINodeForEvent(aEvent);
+
+ // Skip if locked
+ let pref = this._getPrefForNode(node);
+ if (pref.locked) {
+ return;
+ }
+
+ // Boolean inputs blur to remove focus from "button"
+ if (pref.type == Services.prefs.PREF_BOOL) {
+ return;
+ }
+
+ // String and Int inputs change / commit on blur
+ pref.value = aEvent.target.value;
+ },
+
+ // When we reset a pref to it's default value (note resetting a user created pref will delete it)
+ resetDefaultPref: function AC_resetDefaultPref(aEvent) {
+ let node = this.getLINodeForEvent(aEvent);
+
+ // If not already selected, do so
+ if (this.selected != node) {
+ this.selected = node;
+ }
+
+ // Reset will handle any locked condition
+ let pref = this._getPrefForNode(node);
+ pref.reset();
+
+ // Ensure pref reset flushed to disk immediately
+ Services.prefs.savePrefFile(null);
+ },
+
+ // When we want to toggle a bool pref
+ toggleBoolPref: function AC_toggleBoolPref(aEvent) {
+ let node = this.getLINodeForEvent(aEvent);
+
+ // Skip if locked, or not boolean
+ let pref = this._getPrefForNode(node);
+ if (pref.locked) {
+ return;
+ }
+
+ // Toggle, and blur to remove field focus
+ pref.value = !pref.value;
+ aEvent.target.blur();
+ },
+
+ // When Int inputs have their Up or Down arrows toggled
+ incrOrDecrIntPref: function AC_incrOrDecrIntPref(aEvent, aInt) {
+ let node = this.getLINodeForEvent(aEvent);
+
+ // Skip if locked
+ let pref = this._getPrefForNode(node);
+ if (pref.locked) {
+ return;
+ }
+
+ pref.value += aInt;
+ },
+
+ // Observe preference changes
+ observe: function AC_observe(aSubject, aTopic, aPrefName) {
+ let pref = new Pref(aPrefName);
+
+ // Ignore uninteresting changes, and avoid "private" preferences
+ if (aTopic != "nsPref:changed") {
+ return;
+ }
+
+ // If pref type invalid, refresh display as user reset/removed an item from the list
+ if (pref.type == Services.prefs.PREF_INVALID) {
+ document.location.reload();
+ return;
+ }
+
+ // If pref onscreen, update in place.
+ let item = document.querySelector(".pref-item[name=\"" + CSS.escape(pref.name) + "\"]");
+ if (item) {
+ item.setAttribute("value", pref.value);
+ let input = item.querySelector("input");
+ input.setAttribute("value", pref.value);
+ input.value = pref.value;
+
+ pref.default ?
+ item.querySelector(".reset").setAttribute("disabled", "true") :
+ item.querySelector(".reset").removeAttribute("disabled");
+ return;
+ }
+
+ // If pref not already in list, refresh display as it's being added
+ let anyWhere = this._list.filter(i => { return i.name == pref.name });
+ if (!anyWhere.length) {
+ document.location.reload();
+ }
+ },
+
+ // Quick context menu helpers for about:config
+ clipboardCopy: function AC_clipboardCopy(aField) {
+ let pref = this._getPrefForNode(this.contextMenuLINode);
+ if (aField == 'name') {
+ gClipboardHelper.copyString(pref.name);
+ } else {
+ gClipboardHelper.copyString(pref.value);
+ }
+ }
+}
+
+
+/* ============================== Pref ==============================
+ *
+ * Individual Preference object / methods
+ *
+ * Defines a Pref object, a document list item tied to Preferences Services
+ * And the methods by which they interact.
+ *
+ */
+function Pref(aName) {
+ this.name = aName;
+}
+
+Pref.prototype = {
+ get type() {
+ return Services.prefs.getPrefType(this.name);
+ },
+
+ get value() {
+ switch (this.type) {
+ case Services.prefs.PREF_BOOL:
+ return Services.prefs.getBoolPref(this.name);
+ case Services.prefs.PREF_INT:
+ return Services.prefs.getIntPref(this.name);
+ case Services.prefs.PREF_STRING:
+ default:
+ return Services.prefs.getCharPref(this.name);
+ }
+
+ },
+ set value(aPrefValue) {
+ switch (this.type) {
+ case Services.prefs.PREF_BOOL:
+ Services.prefs.setBoolPref(this.name, aPrefValue);
+ break;
+ case Services.prefs.PREF_INT:
+ Services.prefs.setIntPref(this.name, aPrefValue);
+ break;
+ case Services.prefs.PREF_STRING:
+ default:
+ Services.prefs.setCharPref(this.name, aPrefValue);
+ }
+
+ // Ensure pref change flushed to disk immediately
+ Services.prefs.savePrefFile(null);
+ },
+
+ get default() {
+ return !Services.prefs.prefHasUserValue(this.name);
+ },
+
+ get locked() {
+ return Services.prefs.prefIsLocked(this.name);
+ },
+
+ reset: function AC_reset() {
+ Services.prefs.clearUserPref(this.name);
+ },
+
+ test: function AC_test(aValue) {
+ return aValue ? aValue.test(this.name) : true;
+ },
+
+ // Get existing or create new LI node for the pref
+ getOrCreateNewLINode: function AC_getOrCreateNewLINode() {
+ if (!this.li) {
+ this.li = document.createElement("li");
+
+ this.li.className = "pref-item";
+ this.li.setAttribute("name", this.name);
+
+ // Click callback to ensure list item selected even on no-action tap events
+ this.li.addEventListener("click",
+ function(aEvent) {
+ AboutConfig.selected = AboutConfig.getLINodeForEvent(aEvent);
+ },
+ false
+ );
+
+ // Contextmenu callback to identify selected list item
+ this.li.addEventListener("contextmenu",
+ function(aEvent) {
+ AboutConfig.contextMenuLINode = AboutConfig.getLINodeForEvent(aEvent);
+ },
+ false
+ );
+
+ this.li.setAttribute("contextmenu", "prefs-context-menu");
+
+ // Create list item outline, bind to object actions
+ this.li.innerHTML =
+ "<div class='pref-name' " +
+ "onclick='AboutConfig.selectOrToggleBoolPref(event);'>" +
+ this.name +
+ "</div>" +
+ "<div class='pref-item-line'>" +
+ "<input class='pref-value' value='' " +
+ "onblur='AboutConfig.setIntOrStringPref(event);' " +
+ "onclick='AboutConfig.selectOrToggleBoolPref(event);'>" +
+ "</input>" +
+ "<div class='pref-button reset' " +
+ "onclick='AboutConfig.resetDefaultPref(event);'>" +
+ gStringBundle.GetStringFromName("pref.resetButton") +
+ "</div>" +
+ "<div class='pref-button toggle' " +
+ "onclick='AboutConfig.toggleBoolPref(event);'>" +
+ gStringBundle.GetStringFromName("pref.toggleButton") +
+ "</div>" +
+ "<div class='pref-button up' " +
+ "onclick='AboutConfig.incrOrDecrIntPref(event, 1);'>" +
+ "</div>" +
+ "<div class='pref-button down' " +
+ "onclick='AboutConfig.incrOrDecrIntPref(event, -1);'>" +
+ "</div>" +
+ "</div>";
+
+ // Delay providing the list item values, until the LI is returned and added to the document
+ setTimeout(this._valueSetup.bind(this), INNERHTML_VALUE_DELAY);
+ }
+
+ return this.li;
+ },
+
+ // Initialize list item object values
+ _valueSetup: function AC_valueSetup() {
+
+ this.li.setAttribute("type", this.type);
+ this.li.setAttribute("value", this.value);
+
+ let valDiv = this.li.querySelector(".pref-value");
+ valDiv.value = this.value;
+
+ switch(this.type) {
+ case Services.prefs.PREF_BOOL:
+ valDiv.setAttribute("type", "button");
+ this.li.querySelector(".up").setAttribute("disabled", true);
+ this.li.querySelector(".down").setAttribute("disabled", true);
+ break;
+ case Services.prefs.PREF_STRING:
+ valDiv.setAttribute("type", "text");
+ this.li.querySelector(".up").setAttribute("disabled", true);
+ this.li.querySelector(".down").setAttribute("disabled", true);
+ this.li.querySelector(".toggle").setAttribute("disabled", true);
+ break;
+ case Services.prefs.PREF_INT:
+ valDiv.setAttribute("type", "number");
+ this.li.querySelector(".toggle").setAttribute("disabled", true);
+ break;
+ }
+
+ this.li.setAttribute("default", this.default);
+ if (this.default) {
+ this.li.querySelector(".reset").setAttribute("disabled", true);
+ }
+
+ if (this.locked) {
+ valDiv.setAttribute("disabled", this.locked);
+ this.li.querySelector(".pref-name").setAttribute("locked", true);
+ }
+ }
+}
+
diff --git a/mobile/android/chrome/content/config.xhtml b/mobile/android/chrome/content/config.xhtml
new file mode 100644
index 0000000000..fd40bb5173
--- /dev/null
+++ b/mobile/android/chrome/content/config.xhtml
@@ -0,0 +1,86 @@
+<?xml version="1.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/. -->
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
+<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
+%globalDTD;
+<!ENTITY % configDTD SYSTEM "chrome://browser/locale/config.dtd">
+%configDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+ <meta name="viewport" content="width=device-width; user-scalable=0" />
+
+ <link rel="stylesheet" href="chrome://browser/skin/config.css" type="text/css"/>
+ <script type="text/javascript;version=1.8" src="chrome://browser/content/config.js"></script>
+</head>
+
+<body dir="&locale.dir;" onload="NewPrefDialog.init(); AboutConfig.init();"
+ onunload="AboutConfig.uninit();">
+
+ <div class="toolbar">
+ <div class="toolbar-container">
+ <div id="new-pref-toggle-button" onclick="NewPrefDialog.toggleShowHide();"/>
+
+ <div class="toolbar-item" id="filter-container">
+ <div id="filter-search-button"/>
+ <input id="filter-input" type="search" placeholder="&toolbar.searchPlaceholder;" value=""
+ oninput="AboutConfig.bufferFilterInput();"/>
+ <div id="filter-input-clear-button" onclick="AboutConfig.clearFilterInput();"/>
+ </div>
+ </div>
+ </div>
+
+ <div id="content" ontouchstart="AboutConfig.filterInput.blur();">
+
+ <div id="new-pref-container">
+ <li class="pref-item" id="new-pref-item">
+ <div class="pref-item-line">
+ <input class="pref-name" id="new-pref-name" type="text" placeholder="&newPref.namePlaceholder;"
+ onfocus="NewPrefDialog.focusName(event);"
+ oninput="NewPrefDialog.updateName(event);"/>
+ <select class="pref-value" id="new-pref-type" onchange="NewPrefDialog.type = event.target.value;">
+ <option value="boolean">&newPref.valueBoolean;</option>
+ <option value="string">&newPref.valueString;</option>
+ <option value="int">&newPref.valueInteger;</option>
+ </select>
+ </div>
+
+ <div class="pref-item-line" id="new-pref-line-boolean">
+ <input class="pref-value" id="new-pref-value-boolean" disabled="disabled"/>
+ <div class="pref-button toggle" onclick="NewPrefDialog.toggleBoolValue();">&newPref.toggleButton;</div>
+ </div>
+
+ <div class="pref-item-line">
+ <input class="pref-value" id="new-pref-value-string" placeholder="&newPref.stringPlaceholder;"/>
+ <input class="pref-value" id="new-pref-value-int" placeholder="&newPref.numberPlaceholder;" type="number"/>
+ </div>
+
+ <div class="pref-item-line">
+ <div class="pref-button cancel" id="negative-button" onclick="NewPrefDialog.hide();">&newPref.cancelButton;</div>
+ <div class="pref-button create" id="positive-button" onclick="NewPrefDialog.create(event);"></div>
+ </div>
+ </li>
+ </div>
+
+ <div id="prefs-shield"></div>
+
+ <ul id="prefs-container"/>
+
+ <ul id="loading-container"><li></li></ul>
+
+ </div>
+
+ <menu type="context" id="prefs-context-menu">
+ <menuitem label="&contextMenu.copyPrefName;" onclick="AboutConfig.clipboardCopy('name');"></menuitem>
+ <menuitem label="&contextMenu.copyPrefValue;" onclick="AboutConfig.clipboardCopy('value');"></menuitem>
+ </menu>
+
+</body>
+</html>
diff --git a/mobile/android/chrome/content/content.js b/mobile/android/chrome/content/content.js
new file mode 100644
index 0000000000..7cac22bd14
--- /dev/null
+++ b/mobile/android/chrome/content/content.js
@@ -0,0 +1,159 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/ExtensionContent.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AboutReader", "resource://gre/modules/AboutReader.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent", "resource://gre/modules/LoginManagerContent.jsm");
+
+var dump = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.d.bind(null, "Content");
+
+var global = this;
+
+// This is copied from desktop's tab-content.js. See bug 1153485 about sharing this code somehow.
+var AboutReaderListener = {
+
+ _articlePromise: null,
+
+ _isLeavingReaderMode: false,
+
+ init: function() {
+ addEventListener("AboutReaderContentLoaded", this, false, true);
+ addEventListener("DOMContentLoaded", this, false);
+ addEventListener("pageshow", this, false);
+ addEventListener("pagehide", this, false);
+ addMessageListener("Reader:ToggleReaderMode", this);
+ addMessageListener("Reader:PushState", this);
+ },
+
+ receiveMessage: function(message) {
+ switch (message.name) {
+ case "Reader:ToggleReaderMode":
+ let url = content.document.location.href;
+ if (!this.isAboutReader) {
+ this._articlePromise = ReaderMode.parseDocument(content.document).catch(Cu.reportError);
+ ReaderMode.enterReaderMode(docShell, content);
+ } else {
+ this._isLeavingReaderMode = true;
+ ReaderMode.leaveReaderMode(docShell, content);
+ }
+ break;
+
+ case "Reader:PushState":
+ this.updateReaderButton(!!(message.data && message.data.isArticle));
+ break;
+ }
+ },
+
+ get isAboutReader() {
+ return content.document.documentURI.startsWith("about:reader");
+ },
+
+ handleEvent: function(aEvent) {
+ if (aEvent.originalTarget.defaultView != content) {
+ return;
+ }
+
+ switch (aEvent.type) {
+ case "AboutReaderContentLoaded":
+ if (!this.isAboutReader) {
+ return;
+ }
+
+ // If we are restoring multiple reader mode tabs during session restore, duplicate "DOMContentLoaded"
+ // events may be fired for the visible tab. The inital "DOMContentLoaded" may be received before the
+ // document body is available, so we avoid instantiating an AboutReader object, expecting that a
+ // valid message will follow. See bug 925983.
+ if (content.document.body) {
+ new AboutReader(global, content, this._articlePromise);
+ this._articlePromise = null;
+ }
+ break;
+
+ case "pagehide":
+ // this._isLeavingReaderMode is used here to keep the Reader Mode icon
+ // visible in the location bar when transitioning from reader-mode page
+ // back to the source page.
+ sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: this._isLeavingReaderMode });
+ if (this._isLeavingReaderMode) {
+ this._isLeavingReaderMode = false;
+ }
+ break;
+
+ case "pageshow":
+ // If a page is loaded from the bfcache, we won't get a "DOMContentLoaded"
+ // event, so we need to rely on "pageshow" in this case.
+ if (aEvent.persisted) {
+ this.updateReaderButton();
+ }
+ break;
+ case "DOMContentLoaded":
+ this.updateReaderButton();
+ break;
+ }
+ },
+ updateReaderButton: function(forceNonArticle) {
+ if (!ReaderMode.isEnabledForParseOnLoad || this.isAboutReader ||
+ !(content.document instanceof content.HTMLDocument) ||
+ content.document.mozSyntheticDocument) {
+ return;
+ }
+
+ this.scheduleReadabilityCheckPostPaint(forceNonArticle);
+ },
+
+ cancelPotentialPendingReadabilityCheck: function() {
+ if (this._pendingReadabilityCheck) {
+ removeEventListener("MozAfterPaint", this._pendingReadabilityCheck);
+ delete this._pendingReadabilityCheck;
+ }
+ },
+
+ scheduleReadabilityCheckPostPaint: function(forceNonArticle) {
+ if (this._pendingReadabilityCheck) {
+ // We need to stop this check before we re-add one because we don't know
+ // if forceNonArticle was true or false last time.
+ this.cancelPotentialPendingReadabilityCheck();
+ }
+ this._pendingReadabilityCheck = this.onPaintWhenWaitedFor.bind(this, forceNonArticle);
+ addEventListener("MozAfterPaint", this._pendingReadabilityCheck);
+ },
+
+ onPaintWhenWaitedFor: function(forceNonArticle, event) {
+ // In non-e10s, we'll get called for paints other than ours, and so it's
+ // possible that this page hasn't been laid out yet, in which case we
+ // should wait until we get an event that does relate to our layout. We
+ // determine whether any of our content got painted by checking if there
+ // are any painted rects.
+ if (!event.clientRects.length) {
+ return;
+ }
+
+ this.cancelPotentialPendingReadabilityCheck();
+
+ // Only send updates when there are articles; there's no point updating with
+ // |false| all the time.
+ if (ReaderMode.isProbablyReaderable(content.document)) {
+ sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: true });
+ } else if (forceNonArticle) {
+ sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: false });
+ }
+ },
+};
+AboutReaderListener.init();
+
+addMessageListener("RemoteLogins:fillForm", function(message) {
+ LoginManagerContent.receiveMessage(message, content);
+});
+
+ExtensionContent.init(this);
+addEventListener("unload", () => {
+ ExtensionContent.uninit(this);
+});
diff --git a/mobile/android/chrome/content/geckoview.js b/mobile/android/chrome/content/geckoview.js
new file mode 100644
index 0000000000..b4685a8d3a
--- /dev/null
+++ b/mobile/android/chrome/content/geckoview.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Log",
+ "resource://gre/modules/AndroidLog.jsm", "AndroidLog");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging",
+ "resource://gre/modules/Messaging.jsm", "Messaging");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm", "Services");
+
+function dump(msg) {
+ Log.d("View", msg);
+}
+
+function startup() {
+ dump("zerdatime " + Date.now() + " - geckoivew chrome startup finished.");
+
+ // Notify Java that Gecko has loaded.
+ Messaging.sendRequest({ type: "Gecko:Ready" });
+}
diff --git a/mobile/android/chrome/content/geckoview.xul b/mobile/android/chrome/content/geckoview.xul
new file mode 100644
index 0000000000..a3d4d12905
--- /dev/null
+++ b/mobile/android/chrome/content/geckoview.xul
@@ -0,0 +1,16 @@
+<?xml version="1.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/. -->
+
+<?xml-stylesheet href="chrome://browser/content/browser.css" type="text/css"?>
+
+<window id="main-window"
+ onload="startup();"
+ windowtype="navigator:browser"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <browser id="content" type="content-primary" src="https://mozilla.com" flex="1" remote="true"/>
+
+ <script type="application/javascript" src="chrome://browser/content/geckoview.js"/>
+</window>
diff --git a/mobile/android/chrome/content/healthreport-prefs.js b/mobile/android/chrome/content/healthreport-prefs.js
new file mode 100644
index 0000000000..5c4a50d388
--- /dev/null
+++ b/mobile/android/chrome/content/healthreport-prefs.js
@@ -0,0 +1,6 @@
+#filter substitution
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+pref("datareporting.healthreport.about.reportUrl", "https://fhr.cdn.mozilla.net/%LOCALE%/mobile/");
diff --git a/mobile/android/chrome/content/languages.properties b/mobile/android/chrome/content/languages.properties
new file mode 100644
index 0000000000..53d30d1259
--- /dev/null
+++ b/mobile/android/chrome/content/languages.properties
@@ -0,0 +1,114 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# LOCALIZATION NOTE: do not localize
+af=Afrikaans
+ak=Akan
+ar=عربي
+as=অসমীয়া
+ast-ES=Asturianu
+be=БеларуÑкаÑ
+bg=БългарÑки
+bn-BD=বাংলা (বাংলাদেশ)
+bn-IN=বাংলা (ভারত)
+br-FR=Brezhoneg
+ca=català
+ca-valencia=català (valencià)
+cs=Čeština
+cy=Cymraeg
+da=Dansk
+de=Deutsch
+de-AT=Deutsch (Österreich)
+de-CH=Deutsch (Schweiz)
+de-DE=Deutsch (Deutschland)
+el=Ελληνικά
+en-AU=English (Australian)
+en-CA=English (Canadian)
+en-GB=English (British)
+en-NZ=English (New Zealand)
+en-US=English (US)
+en-ZA=English (South African)
+eo=Esperanto
+es-AR=Español (de Argentina)
+es-CL=Español (de Chile)
+es-ES=Español (de España)
+es-MX=Español (de México)
+et=Eesti keel
+eu=Euskara
+fa=Ùارسی
+fi=suomi
+fr=Français
+fur-IT=Furlan
+fy-NL=Frysk
+ga-IE=Gaeilge
+gl=Galego
+gu-IN=ગà«àªœàª°àª¾àª¤à«€
+he=עברית
+hi=हिनà¥à¤¦à¥€
+hi-IN=हिनà¥à¤¦à¥€ (भारत)
+hr=Hrvatski
+hsb=Hornjoserbsce
+hu=Magyar
+hy-AM=Õ€Õ¡ÕµÕ¥Ö€Õ¥Õ¶
+id=Bahasa Indonesia
+is=íslenska
+it=Italiano
+ja=日本語
+ka=ქáƒáƒ áƒ—ული
+kk=Қазақ
+kn=ಕನà³à²¨à²¡
+ko=한국어
+ku=Kurdî
+la=Latina
+lt=lietuvių
+lv=Latviešu
+mg=Malagasy
+mi=MÄori (Aotearoa)
+mk=МакедонÑки
+ml=മലയാളം
+mn=Монгол
+mr=मराठी
+nb-NO=Norsk bokmål
+ne-NP=नेपाली
+nl=Nederlands
+nn-NO=Norsk nynorsk
+nr=isiNdebele Sepumalanga
+nso=Sepedi
+oc=occitan (lengadocian)
+or=ଓଡ଼ିଆ
+pa-IN=ਪੰਜਾਬੀ
+pl=Polski
+pt-BR=Português (do Brasil)
+pt-PT=Português (Europeu)
+rm=rumantsch
+ro=română
+ru=РуÑÑкий
+rw=Ikinyarwanda
+si=සිංහල
+sk=slovenÄina
+sl=slovensko
+sq=Shqip
+sr=СрпÑки
+sr-Latn=Srpski
+ss=Siswati
+st=Sesotho
+sv-SE=Svenska
+ta=தமிழà¯
+ta-IN=தமிழ௠(இநà¯à®¤à®¿à®¯à®¾)
+ta-LK=தமிழ௠(இலஙà¯à®•à¯ˆ)
+te=తెలà±à°—à±
+th=ไทย
+tn=Setswana
+tr=Türkçe
+ts=Mutsonga
+tt-RU=Tatarça
+uk=УкраїнÑька
+ur=اÙردو
+ve=Tshivenḓa
+vi=Tiếng Việt
+wo=Wolof
+xh=isiXhosa
+zh-CN=中文 (简体)
+zh-TW=正體中文 (ç¹é«”)
+zu=isiZulu
diff --git a/mobile/android/chrome/content/netError.xhtml b/mobile/android/chrome/content/netError.xhtml
new file mode 100644
index 0000000000..f4c727c061
--- /dev/null
+++ b/mobile/android/chrome/content/netError.xhtml
@@ -0,0 +1,406 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD
+ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % netErrorDTD
+ SYSTEM "chrome://global/locale/netError.dtd">
+ %netErrorDTD;
+ <!ENTITY % globalDTD
+ SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+]>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta name="viewport" content="width=device-width; user-scalable=false;" />
+ <title>&loadError.label;</title>
+ <link rel="stylesheet" href="chrome://global/skin/netError.css" type="text/css" media="all" />
+ <!-- If the location of the favicon is changed here, the FAVICON_ERRORPAGE_URL symbol in
+ toolkit/components/places/src/nsFaviconService.h should be updated. -->
+ <link rel="icon" type="image/png" id="favicon" sizes="64x64" href="chrome://browser/skin/images/errorpage-warning.png"/>
+
+ <script type="application/javascript"><![CDATA[
+ // Error url MUST be formatted like this:
+ // moz-neterror:page?e=error&u=url&d=desc
+ //
+ // or optionally, to specify an alternate CSS class to allow for
+ // custom styling and favicon:
+ //
+ // moz-neterror:page?e=error&u=url&s=classname&d=desc
+
+ // Note that this file uses document.documentURI to get
+ // the URL (with the format from above). This is because
+ // document.location.href gets the current URI off the docshell,
+ // which is the URL displayed in the location bar, i.e.
+ // the URI that the user attempted to load.
+
+ function getErrorCode()
+ {
+ var url = document.documentURI;
+ var error = url.search(/e\=/);
+ var duffUrl = url.search(/\&u\=/);
+ return decodeURIComponent(url.slice(error + 2, duffUrl));
+ }
+
+ function getCSSClass()
+ {
+ var url = document.documentURI;
+ var matches = url.match(/s\=([^&]+)\&/);
+ // s is optional, if no match just return nothing
+ if (!matches || matches.length < 2)
+ return "";
+
+ // parenthetical match is the second entry
+ return decodeURIComponent(matches[1]);
+ }
+
+ function getDescription()
+ {
+ var url = document.documentURI;
+ var desc = url.search(/d\=/);
+
+ // desc == -1 if not found; if so, return an empty string
+ // instead of what would turn out to be portions of the URI
+ if (desc == -1)
+ return "";
+
+ return decodeURIComponent(url.slice(desc + 2));
+ }
+
+ function retryThis(buttonEl)
+ {
+ // Note: The application may wish to handle switching off "offline mode"
+ // before this event handler runs, but using a capturing event handler.
+
+ // Session history has the URL of the page that failed
+ // to load, not the one of the error page. So, just call
+ // reload(), which will also repost POST data correctly.
+ try {
+ location.reload();
+ } catch (e) {
+ // We probably tried to reload a URI that caused an exception to
+ // occur; e.g. a nonexistent file.
+ }
+ }
+
+ function initPage()
+ {
+ var err = getErrorCode();
+
+ // if it's an unknown error or there's no title or description
+ // defined, get the generic message
+ var errTitle = document.getElementById("et_" + err);
+ var errDesc = document.getElementById("ed_" + err);
+ if (!errTitle || !errDesc)
+ {
+ errTitle = document.getElementById("et_generic");
+ errDesc = document.getElementById("ed_generic");
+ }
+
+ var title = document.getElementsByClassName("errorTitleText")[0];
+ if (title)
+ {
+ title.parentNode.replaceChild(errTitle, title);
+ // change id to the replaced child's id so styling works
+ errTitle.classList.add("errorTitleText");
+ }
+
+ var sd = document.getElementById("errorShortDescText");
+ if (sd)
+ sd.textContent = getDescription();
+
+ var ld = document.getElementById("errorLongDesc");
+ if (ld)
+ {
+ ld.parentNode.replaceChild(errDesc, ld);
+ // change id to the replaced child's id so styling works
+ errDesc.id = "errorLongDesc";
+ }
+
+ // remove undisplayed errors to avoid bug 39098
+ var errContainer = document.getElementById("errorContainer");
+ errContainer.parentNode.removeChild(errContainer);
+
+ var className = getCSSClass();
+ if (className && className != "expertBadCert") {
+ // Associate a CSS class with the root of the page, if one was passed in,
+ // to allow custom styling.
+ // Not "expertBadCert" though, don't want to deal with the favicon
+ document.documentElement.className = className;
+
+ // Also, if they specified a CSS class, they must supply their own
+ // favicon. In order to trigger the browser to repaint though, we
+ // need to remove/add the link element.
+ var favicon = document.getElementById("favicon");
+ var faviconParent = favicon.parentNode;
+ faviconParent.removeChild(favicon);
+ favicon.setAttribute("href", "chrome://global/skin/icons/" + className + "_favicon.png");
+ faviconParent.appendChild(favicon);
+ }
+ if (className == "expertBadCert") {
+ showSecuritySection();
+ }
+
+ if (err == "remoteXUL") {
+ // Remove the "Try again" button for remote XUL errors given that
+ // it is useless.
+ document.getElementById("errorTryAgain").style.display = "none";
+ }
+
+ if (err == "cspBlocked") {
+ // Remove the "Try again" button for CSP violations, since it's
+ // almost certainly useless. (Bug 553180)
+ document.getElementById("errorTryAgain").style.display = "none";
+ }
+
+ if (err == "nssBadCert") {
+ // Remove the "Try again" button for security exceptions, since it's
+ // almost certainly useless.
+ document.getElementById("errorTryAgain").style.display = "none";
+ document.getElementById("errorPage").setAttribute("class", "certerror");
+ }
+ else {
+ // Remove the override block for non-certificate errors. CSS-hiding
+ // isn't good enough here, because of bug 39098
+ var secOverride = document.getElementById("securityOverrideDiv");
+ secOverride.parentNode.removeChild(secOverride);
+ }
+
+ if (err == "inadequateSecurityError") {
+ // Remove the "Try again" button for HTTP/2 inadequate security as it
+ // is useless.
+ document.getElementById("errorTryAgain").style.display = "none";
+
+ var container = document.getElementById("errorLongDesc");
+ for (var span of container.querySelectorAll("span.hostname")) {
+ span.textContent = document.location.hostname;
+ }
+ }
+
+ addDomainErrorLinks();
+ }
+
+ function showSecuritySection() {
+ // Swap link out, content in
+ document.getElementById('securityOverrideContent').style.display = '';
+ document.getElementById('securityOverrideLink').style.display = 'none';
+ }
+
+ /* Try to preserve the links contained in the error description, like
+ the error code.
+
+ Also, in the case of SSL error pages about domain mismatch, see if
+ we can hyperlink the user to the correct site. We don't want
+ to do this generically since it allows MitM attacks to redirect
+ users to a site under attacker control, but in certain cases
+ it is safe (and helpful!) to do so. Bug 402210
+ */
+ function addDomainErrorLinks() {
+ // Rather than textContent, we need to treat description as HTML
+ var sd = document.getElementById("errorShortDescText");
+ if (sd) {
+ var desc = getDescription();
+
+ // sanitize description text - see bug 441169
+
+ // First, find the index of the <a> tags we care about, being
+ // careful not to use an over-greedy regex.
+ var codeRe = /<a id="errorCode" title="([^"]+)">/;
+ var codeResult = codeRe.exec(desc);
+ var domainRe = /<a id="cert_domain_link" title="([^"]+)">/;
+ var domainResult = domainRe.exec(desc);
+
+ // The order of these links in the description is fixed in
+ // TransportSecurityInfo.cpp:formatOverridableCertErrorMessage.
+ var firstResult = domainResult;
+ if(!domainResult)
+ firstResult = codeResult;
+ if (!firstResult)
+ return;
+ // Remove sd's existing children
+ sd.textContent = "";
+
+ // Everything up to the first link should be text content.
+ sd.appendChild(document.createTextNode(desc.slice(0, firstResult.index)));
+
+ // Now create the actual links.
+ if (domainResult) {
+ createLink(sd, "cert_domain_link", domainResult[1])
+ // Append text for anything between the two links.
+ sd.appendChild(document.createTextNode(desc.slice(desc.indexOf("</a>") + "</a>".length, codeResult.index)));
+ }
+ createLink(sd, "errorCode", codeResult[1])
+
+ // Finally, append text for anything after the last closing </a>.
+ sd.appendChild(document.createTextNode(desc.slice(desc.lastIndexOf("</a>") + "</a>".length)));
+ }
+
+ // Initialize the cert domain link.
+ var link = document.getElementById('cert_domain_link');
+ if (!link)
+ return;
+
+ var okHost = link.getAttribute("title");
+ var thisHost = document.location.hostname;
+ var proto = document.location.protocol;
+
+ // If okHost is a wildcard domain ("*.example.com") let's
+ // use "www" instead. "*.example.com" isn't going to
+ // get anyone anywhere useful. bug 432491
+ okHost = okHost.replace(/^\*\./, "www.");
+
+ /* case #1:
+ * example.com uses an invalid security certificate.
+ *
+ * The certificate is only valid for www.example.com
+ *
+ * Make sure to include the "." ahead of thisHost so that
+ * a MitM attack on paypal.com doesn't hyperlink to "notpaypal.com"
+ *
+ * We'd normally just use a RegExp here except that we lack a
+ * library function to escape them properly (bug 248062), and
+ * domain names are famous for having '.' characters in them,
+ * which would allow spurious and possibly hostile matches.
+ */
+ if (okHost.endsWith("." + thisHost))
+ link.href = proto + okHost;
+
+ /* case #2:
+ * browser.garage.maemo.org uses an invalid security certificate.
+ *
+ * The certificate is only valid for garage.maemo.org
+ */
+ if (thisHost.endsWith("." + okHost))
+ link.href = proto + okHost;
+ }
+
+ function createLink(el, id, text) {
+ var anchorEl = document.createElement("a");
+ anchorEl.setAttribute("id", id);
+ anchorEl.setAttribute("title", text);
+ anchorEl.appendChild(document.createTextNode(text));
+ el.appendChild(anchorEl);
+ }
+ ]]></script>
+ </head>
+
+ <body id="errorPage" dir="&locale.dir;">
+
+ <!-- ERROR ITEM CONTAINER (removed during loading to avoid bug 39098) -->
+ <div id="errorContainer">
+ <div id="errorTitlesContainer">
+ <h1 id="et_generic">&generic.title;</h1>
+ <h1 id="et_dnsNotFound">&dnsNotFound.title;</h1>
+ <h1 id="et_fileNotFound">&fileNotFound.title;</h1>
+ <h1 id="et_fileAccessDenied">&fileAccessDenied.title;</h1>
+ <h1 id="et_malformedURI">&malformedURI.title;</h1>
+ <h1 id="et_unknownProtocolFound">&unknownProtocolFound.title;</h1>
+ <h1 id="et_connectionFailure">&connectionFailure.title;</h1>
+ <h1 id="et_netTimeout">&netTimeout.title;</h1>
+ <h1 id="et_redirectLoop">&redirectLoop.title;</h1>
+ <h1 id="et_unknownSocketType">&unknownSocketType.title;</h1>
+ <h1 id="et_netReset">&netReset.title;</h1>
+ <h1 id="et_notCached">&notCached.title;</h1>
+
+ <!-- Since Fennec not yet have offline mode, change the title to
+ connectionFailure to prevent confusion -->
+ <h1 id="et_netOffline">&connectionFailure.title;</h1>
+
+ <h1 id="et_netInterrupt">&netInterrupt.title;</h1>
+ <h1 id="et_deniedPortAccess">&deniedPortAccess.title;</h1>
+ <h1 id="et_proxyResolveFailure">&proxyResolveFailure.title;</h1>
+ <h1 id="et_proxyConnectFailure">&proxyConnectFailure.title;</h1>
+ <h1 id="et_contentEncodingError">&contentEncodingError.title;</h1>
+ <h1 id="et_unsafeContentType">&unsafeContentType.title;</h1>
+ <h1 id="et_nssFailure2">&nssFailure2.title;</h1>
+ <h1 id="et_nssBadCert">&nssBadCert.title;</h1>
+ <h1 id="et_cspBlocked">&cspBlocked.title;</h1>
+ <h1 id="et_remoteXUL">&remoteXUL.title;</h1>
+ <h1 id="et_corruptedContentErrorv2">&corruptedContentErrorv2.title;</h1>
+ <h1 id="et_sslv3Used">&sslv3Used.title;</h1>
+ <h1 id="et_weakCryptoUsed">&weakCryptoUsed.title;</h1>
+ <h1 id="et_inadequateSecurityError">&inadequateSecurityError.title;</h1>
+ </div>
+ <div id="errorDescriptionsContainer">
+ <div id="ed_generic">&generic.longDesc;</div>
+ <div id="ed_dnsNotFound">&dnsNotFound.longDesc4;</div>
+ <div id="ed_fileNotFound">&fileNotFound.longDesc;</div>
+ <div id="ed_fileAccessDenied">&fileAccessDenied.longDesc;</div>
+ <div id="ed_malformedURI">&malformedURI.longDesc2;</div>
+ <div id="ed_unknownProtocolFound">&unknownProtocolFound.longDesc;</div>
+ <div id="ed_connectionFailure">&connectionFailure.longDesc2;</div>
+ <div id="ed_netTimeout">&netTimeout.longDesc2;</div>
+ <div id="ed_redirectLoop">&redirectLoop.longDesc;</div>
+ <div id="ed_unknownSocketType">&unknownSocketType.longDesc;</div>
+ <div id="ed_netReset">&netReset.longDesc2;</div>
+ <div id="ed_notCached">&notCached.longDesc;</div>
+
+ <!-- Change longDesc from netOffline to connectionFailure,
+ suggesting user to check their wifi/cell_data connection -->
+ <div id="ed_netOffline">&connectionFailure.longDesc2;</div>
+
+ <div id="ed_netInterrupt">&netInterrupt.longDesc2;</div>
+ <div id="ed_deniedPortAccess">&deniedPortAccess.longDesc;</div>
+ <div id="ed_proxyResolveFailure">&proxyResolveFailure.longDesc3;</div>
+ <div id="ed_proxyConnectFailure">&proxyConnectFailure.longDesc;</div>
+ <div id="ed_contentEncodingError">&contentEncodingError.longDesc;</div>
+ <div id="ed_unsafeContentType">&unsafeContentType.longDesc;</div>
+ <div id="ed_nssFailure2">&nssFailure2.longDesc2;</div>
+ <div id="ed_nssBadCert">&nssBadCert.longDesc2;</div>
+ <div id="ed_cspBlocked">&cspBlocked.longDesc;</div>
+ <div id="ed_remoteXUL">&remoteXUL.longDesc;</div>
+ <div id="ed_corruptedContentErrorv2">&corruptedContentErrorv2.longDesc;</div>
+ <div id="ed_sslv3Used">&sslv3Used.longDesc;</div>
+ <div id="ed_weakCryptoUsed">&weakCryptoUsed.longDesc;</div>
+ <div id="ed_inadequateSecurityError">&inadequateSecurityError.longDesc;</div>
+ </div>
+ </div>
+
+ <!-- PAGE CONTAINER (for styling purposes only) -->
+ <div id="errorPageContainer">
+
+ <!-- Error Title -->
+ <div id="errorTitle">
+ <h1 class="errorTitleText" />
+ </div>
+
+ <!-- LONG CONTENT (the section most likely to require scrolling) -->
+ <div id="errorLongContent">
+
+ <!-- Short Description -->
+ <div id="errorShortDesc">
+ <p id="errorShortDescText" />
+ </div>
+
+ <!-- Long Description (Note: See netError.dtd for used XHTML tags) -->
+ <div id="errorLongDesc" />
+
+ <!-- Override section - For ssl errors only. Removed on init for other
+ error types. -->
+ <div id="securityOverrideDiv">
+ <a id="securityOverrideLink" href="javascript:showSecuritySection();" >&securityOverride.linkText;</a>
+ <div id="securityOverrideContent" style="display: none;">&securityOverride.warningContent;</div>
+ </div>
+ </div>
+
+ <!-- Retry Button -->
+ <button id="errorTryAgain" onclick="retryThis(this);">&retry.label;</button>
+
+ </div>
+
+ <!--
+ - Note: It is important to run the script this way, instead of using
+ - an onload handler. This is because error pages are loaded as
+ - LOAD_BACKGROUND, which means that onload handlers will not be executed.
+ -->
+ <script type="application/javascript">initPage();</script>
+
+ </body>
+</html>
diff --git a/mobile/android/chrome/jar.mn b/mobile/android/chrome/jar.mn
new file mode 100644
index 0000000000..538a025bd0
--- /dev/null
+++ b/mobile/android/chrome/jar.mn
@@ -0,0 +1,71 @@
+#filter substitution
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+chrome.jar:
+% content browser %content/ contentaccessible=yes
+
+* content/about.xhtml (content/about.xhtml)
+* content/about.js (content/about.js)
+ content/config.xhtml (content/config.xhtml)
+ content/config.js (content/config.js)
+ content/content.js (content/content.js)
+ content/aboutAddons.xhtml (content/aboutAddons.xhtml)
+ content/aboutAddons.js (content/aboutAddons.js)
+ content/aboutCertError.xhtml (content/aboutCertError.xhtml)
+ content/aboutDownloads.xhtml (content/aboutDownloads.xhtml)
+ content/aboutDownloads.js (content/aboutDownloads.js)
+ content/aboutPrivateBrowsing.xhtml (content/aboutPrivateBrowsing.xhtml)
+ content/aboutPrivateBrowsing.js (content/aboutPrivateBrowsing.js)
+ content/Reader.js (content/Reader.js)
+ content/aboutHome.xhtml (content/aboutHome.xhtml)
+ content/aboutRights.xhtml (content/aboutRights.xhtml)
+ content/blockedSite.xhtml (content/blockedSite.xhtml)
+ content/languages.properties (content/languages.properties)
+ content/browser.xul (content/browser.xul)
+ content/browser.css (content/browser.css)
+ content/browser.js (content/browser.js)
+ content/geckoview.xul (content/geckoview.xul)
+ content/geckoview.js (content/geckoview.js)
+ content/PresentationView.xul (content/PresentationView.xul)
+ content/PresentationView.js (content/PresentationView.js)
+ content/bindings/checkbox.xml (content/bindings/checkbox.xml)
+ content/bindings/settings.xml (content/bindings/settings.xml)
+ content/netError.xhtml (content/netError.xhtml)
+ content/SelectHelper.js (content/SelectHelper.js)
+ content/ActionBarHandler.js (content/ActionBarHandler.js)
+ content/EmbedRT.js (content/EmbedRT.js)
+ content/InputWidgetHelper.js (content/InputWidgetHelper.js)
+ content/WebrtcUI.js (content/WebrtcUI.js)
+ content/MemoryObserver.js (content/MemoryObserver.js)
+ content/ConsoleAPI.js (content/ConsoleAPI.js)
+ content/PluginHelper.js (content/PluginHelper.js)
+ content/PrintHelper.js (content/PrintHelper.js)
+ content/OfflineApps.js (content/OfflineApps.js)
+ content/MasterPassword.js (content/MasterPassword.js)
+ content/FindHelper.js (content/FindHelper.js)
+ content/PermissionsHelper.js (content/PermissionsHelper.js)
+ content/FeedHandler.js (content/FeedHandler.js)
+ content/Feedback.js (content/Feedback.js)
+ content/Linkify.js (content/Linkify.js)
+ content/CastingApps.js (content/CastingApps.js)
+ content/RemoteDebugger.js (content/RemoteDebugger.js)
+#ifdef MOZ_SERVICES_HEALTHREPORT
+ content/aboutHealthReport.xhtml (content/aboutHealthReport.xhtml)
+ content/aboutHealthReport.js (content/aboutHealthReport.js)
+#endif
+ content/aboutAccounts.xhtml (content/aboutAccounts.xhtml)
+ content/aboutAccounts.js (content/aboutAccounts.js)
+ content/aboutLogins.xhtml (content/aboutLogins.xhtml)
+ content/aboutLogins.js (content/aboutLogins.js)
+#ifndef RELEASE_OR_BETA
+ content/WebcompatReporter.js (content/WebcompatReporter.js)
+#endif
+
+% content branding %content/branding/
+
+% override chrome://global/content/config.xul chrome://browser/content/config.xhtml
+% override chrome://global/content/netError.xhtml chrome://browser/content/netError.xhtml
+% override chrome://mozapps/content/extensions/extensions.xul chrome://browser/content/aboutAddons.xhtml
diff --git a/mobile/android/chrome/moz.build b/mobile/android/chrome/moz.build
new file mode 100644
index 0000000000..e1610c2c7f
--- /dev/null
+++ b/mobile/android/chrome/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEFINES['AB_CD'] = CONFIG['MOZ_UI_LOCALE']
+DEFINES['PACKAGE'] = 'browser'
+DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
+DEFINES['MOZ_APP_VERSION_DISPLAY'] = CONFIG['MOZ_APP_VERSION_DISPLAY']
+DEFINES['ANDROID_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME']
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/mobile/android/components/AboutRedirector.js b/mobile/android/components/AboutRedirector.js
new file mode 100644
index 0000000000..df50864dd2
--- /dev/null
+++ b/mobile/android/components/AboutRedirector.js
@@ -0,0 +1,132 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+var modules = {
+ // about:
+ "": {
+ uri: "chrome://browser/content/about.xhtml",
+ privileged: true
+ },
+
+ // about:fennec and about:firefox are aliases for about:,
+ // but hidden from about:about
+ fennec: {
+ uri: "chrome://browser/content/about.xhtml",
+ privileged: true,
+ hide: true
+ },
+ get firefox() {
+ return this.fennec
+ },
+
+ // about:blank has some bad loading behavior we can avoid, if we use an alias
+ empty: {
+ uri: "about:blank",
+ privileged: false,
+ hide: true
+ },
+
+ rights: {
+ uri: "chrome://browser/content/aboutRights.xhtml",
+ privileged: false
+ },
+ blocked: {
+ uri: "chrome://browser/content/blockedSite.xhtml",
+ privileged: false,
+ hide: true
+ },
+ certerror: {
+ uri: "chrome://browser/content/aboutCertError.xhtml",
+ privileged: false,
+ hide: true
+ },
+ home: {
+ uri: "chrome://browser/content/aboutHome.xhtml",
+ privileged: false
+ },
+ downloads: {
+ uri: "chrome://browser/content/aboutDownloads.xhtml",
+ privileged: true
+ },
+ reader: {
+ uri: "chrome://global/content/reader/aboutReader.html",
+ privileged: false,
+ hide: true
+ },
+ feedback: {
+ uri: "chrome://browser/content/aboutFeedback.xhtml",
+ privileged: true
+ },
+ privatebrowsing: {
+ uri: "chrome://browser/content/aboutPrivateBrowsing.xhtml",
+ privileged: true
+ },
+ logins: {
+ uri: "chrome://browser/content/aboutLogins.xhtml",
+ privileged: true
+ },
+ accounts: {
+ uri: "chrome://browser/content/aboutAccounts.xhtml",
+ privileged: true
+ },
+};
+
+if (AppConstants.MOZ_SERVICES_HEALTHREPORT) {
+ modules['healthreport'] = {
+ uri: "chrome://browser/content/aboutHealthReport.xhtml",
+ privileged: true
+ };
+}
+
+function AboutRedirector() {}
+AboutRedirector.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]),
+ classID: Components.ID("{322ba47e-7047-4f71-aebf-cb7d69325cd9}"),
+
+ _getModuleInfo: function (aURI) {
+ let moduleName = aURI.path.replace(/[?#].*/, "").toLowerCase();
+ return modules[moduleName];
+ },
+
+ // nsIAboutModule
+ getURIFlags: function(aURI) {
+ let flags;
+ let moduleInfo = this._getModuleInfo(aURI);
+ if (moduleInfo.hide)
+ flags = Ci.nsIAboutModule.HIDE_FROM_ABOUTABOUT;
+
+ return flags | Ci.nsIAboutModule.ALLOW_SCRIPT;
+ },
+
+ newChannel: function(aURI, aLoadInfo) {
+ let moduleInfo = this._getModuleInfo(aURI);
+
+ var ios = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+
+ var newURI = ios.newURI(moduleInfo.uri, null, null);
+
+ var channel = ios.newChannelFromURIWithLoadInfo(newURI, aLoadInfo);
+
+ if (!moduleInfo.privileged) {
+ // Setting the owner to null means that we'll go through the normal
+ // path in GetChannelPrincipal and create a codebase principal based
+ // on the channel's originalURI
+ channel.owner = null;
+ }
+
+ channel.originalURI = aURI;
+
+ return channel;
+ }
+};
+
+const components = [AboutRedirector];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
diff --git a/mobile/android/components/AddonUpdateService.js b/mobile/android/components/AddonUpdateService.js
new file mode 100644
index 0000000000..2505e27962
--- /dev/null
+++ b/mobile/android/components/AddonUpdateService.js
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate",
+ "resource://gre/modules/AddonManager.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
+ "resource://gre/modules/addons/AddonRepository.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "GMPInstallManager",
+ "resource://gre/modules/GMPInstallManager.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging",
+ "resource://gre/modules/Messaging.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+
+function getPref(func, preference, defaultValue) {
+ try {
+ return Services.prefs[func](preference);
+ }
+ catch (e) {}
+ return defaultValue;
+}
+
+// -----------------------------------------------------------------------
+// Add-on auto-update management service
+// -----------------------------------------------------------------------
+
+const PREF_ADDON_UPDATE_ENABLED = "extensions.autoupdate.enabled";
+const PREF_ADDON_UPDATE_INTERVAL = "extensions.autoupdate.interval";
+
+var gNeedsRestart = false;
+
+function AddonUpdateService() {}
+
+AddonUpdateService.prototype = {
+ classDescription: "Add-on auto-update management",
+ classID: Components.ID("{93c8824c-9b87-45ae-bc90-5b82a1e4d877}"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback]),
+
+ notify: function aus_notify(aTimer) {
+ if (aTimer && !getPref("getBoolPref", PREF_ADDON_UPDATE_ENABLED, true))
+ return;
+
+ // If we already auto-upgraded and installed new versions, ignore this check
+ if (gNeedsRestart)
+ return;
+
+ AddonManagerPrivate.backgroundUpdateCheck();
+
+ let gmp = new GMPInstallManager();
+ gmp.simpleCheckAndInstall().then(null, () => {});
+
+ let interval = 1000 * getPref("getIntPref", PREF_ADDON_UPDATE_INTERVAL, 86400);
+ Messaging.sendRequest({
+ type: "Gecko:ScheduleRun",
+ action: "update-addons",
+ trigger: interval,
+ interval: interval,
+ });
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AddonUpdateService]);
+
diff --git a/mobile/android/components/BlocklistPrompt.js b/mobile/android/components/BlocklistPrompt.js
new file mode 100644
index 0000000000..ce7b8e011e
--- /dev/null
+++ b/mobile/android/components/BlocklistPrompt.js
@@ -0,0 +1,61 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cc = Components.classes;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+// -----------------------------------------------------------------------
+// BlocklistPrompt Service
+// -----------------------------------------------------------------------
+
+
+function BlocklistPrompt() { }
+
+BlocklistPrompt.prototype = {
+ prompt: function(aAddons, aCount) {
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+ if (win.ExtensionsView.visible) {
+ win.ExtensionsView.showRestart("blocked");
+ } else {
+ let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+ let notifyBox = win.getNotificationBox();
+ let restartCallback = function(aNotification, aDescription) {
+ // Notify all windows that an application quit has been requested
+ var cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
+
+ // If nothing aborted, quit the app
+ if (cancelQuit.data == false) {
+ let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
+ appStartup.quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit);
+ }
+ };
+
+ let buttons = [{accessKey: null,
+ label: bundle.GetStringFromName("notificationRestart.button"),
+ callback: restartCallback}];
+ notifyBox.appendNotification(bundle.GetStringFromName("notificationRestart.blocked"),
+ "blocked-add-on",
+ "",
+ "PRIORITY_CRITICAL_HIGH",
+ buttons);
+ }
+ // Disable softblocked items automatically
+ for (let i = 0; i < aAddons.length; i++) {
+ if (aAddons[i].item instanceof Ci.nsIPluginTag)
+ aAddons[i].item.disabled = true;
+ else
+ aAddons[i].item.userDisabled = true;
+ }
+ },
+ classID: Components.ID("{4e6ea350-b09a-11df-94e2-0800200c9a66}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIBlocklistPrompt])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([BlocklistPrompt]);
+
diff --git a/mobile/android/components/BrowserCLH.js b/mobile/android/components/BrowserCLH.js
new file mode 100644
index 0000000000..4cbf03554e
--- /dev/null
+++ b/mobile/android/components/BrowserCLH.js
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function BrowserCLH() {}
+
+BrowserCLH.prototype = {
+ /**
+ * Register resource://android as the APK root.
+ *
+ * Consumers can access Android assets using resource://android/assets/FILENAME.
+ */
+ setResourceSubstitutions: function () {
+ let registry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci["nsIChromeRegistry"]);
+ // Like jar:jar:file:///data/app/org.mozilla.fennec-2.apk!/assets/omni.ja!/chrome/chrome/content/aboutHome.xhtml
+ let url = registry.convertChromeURL(Services.io.newURI("chrome://browser/content/aboutHome.xhtml", null, null)).spec;
+ // Like jar:file:///data/app/org.mozilla.fennec-2.apk!/
+ url = url.substring(4, url.indexOf("!/") + 2);
+
+ let protocolHandler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
+ protocolHandler.setSubstitution("android", Services.io.newURI(url, null, null));
+ },
+
+ observe: function (subject, topic, data) {
+ switch (topic) {
+ case "app-startup":
+ this.setResourceSubstitutions();
+ break;
+ }
+ },
+
+ // QI
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ // XPCOMUtils factory
+ classID: Components.ID("{be623d20-d305-11de-8a39-0800200c9a66}")
+};
+
+var components = [ BrowserCLH ];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
diff --git a/mobile/android/components/ColorPicker.js b/mobile/android/components/ColorPicker.js
new file mode 100644
index 0000000000..7d478da803
--- /dev/null
+++ b/mobile/android/components/ColorPicker.js
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cc = Components.classes;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
+ "resource://gre/modules/Prompt.jsm");
+
+function ColorPicker() {
+}
+
+ColorPicker.prototype = {
+ _initial: 0,
+ _domWin: null,
+ _title: "",
+
+ get strings() {
+ if (!this._strings) {
+ this._strings = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+ }
+ return this._strings;
+ },
+
+ init: function(aParent, aTitle, aInitial) {
+ this._domWin = aParent;
+ this._initial = aInitial;
+ this._title = aTitle;
+ },
+
+ open: function(aCallback) {
+ let p = new Prompt({ title: this._title,
+ buttons: [
+ this.strings.GetStringFromName("inputWidgetHelper.set"),
+ this.strings.GetStringFromName("inputWidgetHelper.cancel")
+ ] })
+ .addColorPicker({ value: this._initial })
+ .show((data) => {
+ if (data.button == 0)
+ aCallback.done(data.color0);
+ else
+ aCallback.done(this._initial);
+ });
+ },
+
+ classID: Components.ID("{430b987f-bb9f-46a3-99a5-241749220b29}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIColorPicker])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ColorPicker]);
diff --git a/mobile/android/components/ContentDispatchChooser.js b/mobile/android/components/ContentDispatchChooser.js
new file mode 100644
index 0000000000..b28e356e02
--- /dev/null
+++ b/mobile/android/components/ContentDispatchChooser.js
@@ -0,0 +1,83 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+
+function ContentDispatchChooser() {}
+
+ContentDispatchChooser.prototype =
+{
+ classID: Components.ID("5a072a22-1e66-4100-afc1-07aed8b62fc5"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentDispatchChooser]),
+
+ get protoSvc() {
+ if (!this._protoSvc) {
+ this._protoSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"].getService(Ci.nsIExternalProtocolService);
+ }
+ return this._protoSvc;
+ },
+
+ _getChromeWin: function getChromeWin() {
+ try {
+ return Services.wm.getMostRecentWindow("navigator:browser");
+ } catch (e) {
+ throw Cr.NS_ERROR_FAILURE;
+ }
+ },
+
+ ask: function ask(aHandler, aWindowContext, aURI, aReason) {
+ let window = null;
+ try {
+ if (aWindowContext)
+ window = aWindowContext.getInterface(Ci.nsIDOMWindow);
+ } catch (e) { /* it's OK to not have a window */ }
+
+ // The current list is based purely on the scheme. Redo the query using the url to get more
+ // specific results.
+ aHandler = this.protoSvc.getProtocolHandlerInfoFromOS(aURI.spec, {});
+
+ // The first handler in the set is the Android Application Chooser (which will fall back to a default if one is set)
+ // If we have more than one option, let the OS handle showing a list (if needed).
+ if (aHandler.possibleApplicationHandlers.length > 1) {
+ aHandler.launchWithURI(aURI, aWindowContext);
+ } else {
+ // xpcshell tests do not have an Android Bridge but we require Android
+ // Bridge when using Messaging so we guard against this case. xpcshell
+ // tests also do not have a window, so we use this state to guard.
+ let win = this._getChromeWin();
+ if (!win) {
+ return;
+ }
+
+ let msg = {
+ type: "Intent:OpenNoHandler",
+ uri: aURI.spec,
+ };
+
+ Messaging.sendRequestForResult(msg).then(() => {
+ // Java opens an app on success: take no action.
+ }, (uri) => {
+ // We couldn't open this. If this was from a click, it's likely that we just
+ // want this to fail silently. If the user entered this on the address bar, though,
+ // we want to show the neterror page.
+
+ let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ let millis = dwu.millisSinceLastUserInput;
+ if (millis > 0 && millis >= 1000) {
+ window.location.href = uri;
+ }
+ });
+ }
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentDispatchChooser]);
diff --git a/mobile/android/components/ContentPermissionPrompt.js b/mobile/android/components/ContentPermissionPrompt.js
new file mode 100644
index 0000000000..fd13ce26b3
--- /dev/null
+++ b/mobile/android/components/ContentPermissionPrompt.js
@@ -0,0 +1,146 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+const Cc = Components.classes;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const kEntities = {
+ "contacts": "contacts",
+ "desktop-notification": "desktopNotification2",
+ "geolocation": "geolocation",
+ "flyweb-publish-server": "flyWebPublishServer",
+};
+
+// For these types, prompt for permission if action is unknown.
+const PROMPT_FOR_UNKNOWN = [
+ "desktop-notification",
+ "geolocation",
+ "flyweb-publish-server",
+];
+
+function ContentPermissionPrompt() {}
+
+ContentPermissionPrompt.prototype = {
+ classID: Components.ID("{C6E8C44D-9F39-4AF7-BCC0-76E38A8310F5}"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionPrompt]),
+
+ handleExistingPermission: function handleExistingPermission(request, type, denyUnknown) {
+ let result = Services.perms.testExactPermissionFromPrincipal(request.principal, type);
+ if (result == Ci.nsIPermissionManager.ALLOW_ACTION) {
+ request.allow();
+ return true;
+ }
+
+ if (result == Ci.nsIPermissionManager.DENY_ACTION) {
+ request.cancel();
+ return true;
+ }
+
+ if (denyUnknown && result == Ci.nsIPermissionManager.UNKNOWN_ACTION) {
+ request.cancel();
+ return true;
+ }
+
+ return false;
+ },
+
+ getChromeWindow: function getChromeWindow(aWindow) {
+ let chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow)
+ .QueryInterface(Ci.nsIDOMChromeWindow);
+ return chromeWin;
+ },
+
+ getChromeForRequest: function getChromeForRequest(request) {
+ if (request.window) {
+ let requestingWindow = request.window.top;
+ return this.getChromeWindow(requestingWindow).wrappedJSObject;
+ }
+ return request.element.ownerDocument.defaultView;
+ },
+
+ prompt: function(request) {
+ let isApp = request.principal.appId !== Ci.nsIScriptSecurityManager.NO_APP_ID && request.principal.appId !== Ci.nsIScriptSecurityManager.UNKNOWN_APP_ID;
+
+ // Only allow exactly one permission rquest here.
+ let types = request.types.QueryInterface(Ci.nsIArray);
+ if (types.length != 1) {
+ request.cancel();
+ return;
+ }
+ let perm = types.queryElementAt(0, Ci.nsIContentPermissionType);
+
+ // Returns true if the request was handled
+ let access = (perm.access && perm.access !== "unused") ?
+ (perm.type + "-" + perm.access) : perm.type;
+ if (this.handleExistingPermission(request, access,
+ /* denyUnknown */ isApp || PROMPT_FOR_UNKNOWN.indexOf(perm.type) < 0))
+ return;
+
+ let chromeWin = this.getChromeForRequest(request);
+ let tab = chromeWin.BrowserApp.getTabForWindow(request.window.top);
+ if (!tab)
+ return;
+
+ let browserBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+ let entityName = kEntities[perm.type];
+
+ let buttons = [{
+ label: browserBundle.GetStringFromName(entityName + ".dontAllow"),
+ callback: function(aChecked) {
+ // If the user checked "Don't ask again" or this is a desktopNotification, make a permanent exception
+ if (aChecked || entityName == "desktopNotification2")
+ Services.perms.addFromPrincipal(request.principal, access, Ci.nsIPermissionManager.DENY_ACTION);
+
+ request.cancel();
+ }
+ },
+ {
+ label: browserBundle.GetStringFromName(entityName + ".allow"),
+ callback: function(aChecked) {
+ // If the user checked "Don't ask again" or this is a desktopNotification, make a permanent exception
+ if (aChecked || entityName == "desktopNotification2") {
+ Services.perms.addFromPrincipal(request.principal, access, Ci.nsIPermissionManager.ALLOW_ACTION);
+ } else if (isApp) {
+ // Otherwise allow the permission for the current session if the request comes from an app
+ Services.perms.addFromPrincipal(request.principal, access, Ci.nsIPermissionManager.ALLOW_ACTION, Ci.nsIPermissionManager.EXPIRE_SESSION);
+ }
+
+ request.allow();
+ },
+ positive: true
+ }];
+
+ let requestor = chromeWin.BrowserApp.manifest ? "'" + chromeWin.BrowserApp.manifest.name + "'" : request.principal.URI.host;
+ let message = browserBundle.formatStringFromName(entityName + ".ask", [requestor], 1);
+ // desktopNotification doesn't have a checkbox
+ let options;
+ if (entityName == "desktopNotification2") {
+ options = {
+ link: {
+ label: browserBundle.GetStringFromName("doorhanger.learnMore"),
+ url: "https://www.mozilla.org/firefox/push/"
+ }
+ };
+ } else {
+ options = { checkbox: browserBundle.GetStringFromName(entityName + ".dontAskAgain") };
+ }
+
+ chromeWin.NativeWindow.doorhanger.show(message, entityName + request.principal.URI.host, buttons, tab.id, options, entityName.toUpperCase());
+ }
+};
+
+
+//module initialization
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentPermissionPrompt]);
diff --git a/mobile/android/components/DirectoryProvider.js b/mobile/android/components/DirectoryProvider.js
new file mode 100644
index 0000000000..5d0f7974c9
--- /dev/null
+++ b/mobile/android/components/DirectoryProvider.js
@@ -0,0 +1,214 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "JNI", "resource://gre/modules/JNI.jsm");
+
+// -----------------------------------------------------------------------
+// Directory Provider for special browser folders and files
+// -----------------------------------------------------------------------
+
+const NS_APP_CACHE_PARENT_DIR = "cachePDir";
+const NS_APP_SEARCH_DIR = "SrchPlugns";
+const NS_APP_SEARCH_DIR_LIST = "SrchPluginsDL";
+const NS_APP_DISTRIBUTION_SEARCH_DIR_LIST = "SrchPluginsDistDL";
+const NS_APP_USER_SEARCH_DIR = "UsrSrchPlugns";
+const NS_XPCOM_CURRENT_PROCESS_DIR = "XCurProcD";
+const XRE_APP_DISTRIBUTION_DIR = "XREAppDist";
+const XRE_UPDATE_ROOT_DIR = "UpdRootD";
+const ENVVAR_UPDATE_DIR = "UPDATES_DIRECTORY";
+const WEBAPPS_DIR = "webappsDir";
+
+const SYSTEM_DIST_PATH = `/system/${AppConstants.ANDROID_PACKAGE_NAME}/distribution`;
+
+function DirectoryProvider() {}
+
+DirectoryProvider.prototype = {
+ classID: Components.ID("{ef0f7a87-c1ee-45a8-8d67-26f586e46a4b}"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider,
+ Ci.nsIDirectoryServiceProvider2]),
+
+ getFile: function(prop, persistent) {
+ if (prop == NS_APP_CACHE_PARENT_DIR) {
+ let dirsvc = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties);
+ let profile = dirsvc.get("ProfD", Ci.nsIFile);
+ return profile;
+ } else if (prop == WEBAPPS_DIR) {
+ // returns the folder that should hold the webapps database file
+ // For fennec we will store that in the root profile folder so that all
+ // webapps can easily access it
+ let dirsvc = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties);
+ let profile = dirsvc.get("ProfD", Ci.nsIFile);
+ return profile.parent;
+ } else if (prop == XRE_APP_DISTRIBUTION_DIR) {
+ let distributionDirectories = this._getDistributionDirectories();
+ for (let i = 0; i < distributionDirectories.length; i++) {
+ if (distributionDirectories[i].exists()) {
+ return distributionDirectories[i];
+ }
+ }
+ // Fallback: Return default data distribution directory
+ return FileUtils.getDir(NS_XPCOM_CURRENT_PROCESS_DIR, ["distribution"], false);
+ } else if (prop == XRE_UPDATE_ROOT_DIR) {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
+ if (env.exists(ENVVAR_UPDATE_DIR)) {
+ let path = env.get(ENVVAR_UPDATE_DIR);
+ if (path) {
+ return new FileUtils.File(path);
+ }
+ }
+ return new FileUtils.File(env.get("DOWNLOADS_DIRECTORY"));
+ }
+
+ // We are retuning null to show failure instead for throwing an error. The
+ // interface is called quite a bit and throwing an error is noisy. Returning
+ // null works with the way the interface is called [see bug 529077]
+ return null;
+ },
+
+ /**
+ * Appends the distribution-specific search engine directories to the array.
+ * The distribution directory structure is as follows:
+ *
+ * \- distribution/
+ * \- searchplugins/
+ * |- common/
+ * \- locale/
+ * |- <locale 1>/
+ * ...
+ * \- <locale N>/
+ *
+ * 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.
+ */
+ _appendDistroSearchDirs: function(array) {
+ let distro = this.getFile(XRE_APP_DISTRIBUTION_DIR);
+ if (!distro.exists())
+ return;
+
+ let searchPlugins = distro.clone();
+ searchPlugins.append("searchplugins");
+ if (!searchPlugins.exists())
+ return;
+
+ let commonPlugins = searchPlugins.clone();
+ commonPlugins.append("common");
+ if (commonPlugins.exists())
+ array.push(commonPlugins);
+
+ let localePlugins = searchPlugins.clone();
+ localePlugins.append("locale");
+ if (!localePlugins.exists())
+ return;
+
+ let curLocale = "";
+ try {
+ curLocale = Services.prefs.getComplexValue("general.useragent.locale", Ci.nsIPrefLocalizedString).data;
+ } catch (e) {
+ try {
+ curLocale = Services.prefs.getCharPref("general.useragent.locale");
+ } catch (ee) {
+ }
+ }
+
+ if (curLocale) {
+ let curLocalePlugins = localePlugins.clone();
+ curLocalePlugins.append(curLocale);
+ if (curLocalePlugins.exists()) {
+ array.push(curLocalePlugins);
+ return;
+ }
+ }
+
+ // We didn't append the locale dir - try the default one.
+ try {
+ let defLocale = Services.prefs.getCharPref("distribution.searchplugins.defaultLocale");
+ let defLocalePlugins = localePlugins.clone();
+ defLocalePlugins.append(defLocale);
+ if (defLocalePlugins.exists())
+ array.push(defLocalePlugins);
+ } catch(e) {
+ }
+ },
+
+ getFiles: function(prop) {
+ if (prop != NS_APP_SEARCH_DIR_LIST &&
+ prop != NS_APP_DISTRIBUTION_SEARCH_DIR_LIST)
+ return null;
+
+ let result = [];
+
+ if (prop == NS_APP_DISTRIBUTION_SEARCH_DIR_LIST) {
+ this._appendDistroSearchDirs(result);
+ }
+ else {
+ /**
+ * We want to preserve the following order, since the search service
+ * loads engines in first-loaded-wins order.
+ * - distro search plugin locations (loaded separately by the search
+ * service)
+ * - user search plugin locations (profile)
+ * - app search plugin location (shipped engines)
+ */
+ let appUserSearchDir = FileUtils.getDir(NS_APP_USER_SEARCH_DIR, [], false);
+ if (appUserSearchDir.exists())
+ result.push(appUserSearchDir);
+
+ let appSearchDir = FileUtils.getDir(NS_APP_SEARCH_DIR, [], false);
+ if (appSearchDir.exists())
+ result.push(appSearchDir);
+ }
+
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator]),
+ hasMoreElements: function() {
+ return result.length > 0;
+ },
+ getNext: function() {
+ return result.shift();
+ }
+ };
+ },
+
+ _getDistributionDirectories: function() {
+ let directories = [];
+ let jenv = null;
+
+ try {
+ jenv = JNI.GetForThread();
+
+ let jDistribution = JNI.LoadClass(jenv, "org.mozilla.gecko.distribution.Distribution", {
+ static_methods: [
+ { name: "getDistributionDirectories", sig: "()[Ljava/lang/String;" }
+ ],
+ });
+
+ let jDirectories = jDistribution.getDistributionDirectories();
+
+ for (let i = 0; i < jDirectories.length; i++) {
+ directories.push(new FileUtils.File(
+ JNI.ReadString(jenv, jDirectories.get(i))
+ ));
+ }
+ } finally {
+ if (jenv) {
+ JNI.UnloadClasses(jenv);
+ }
+ }
+
+ return directories;
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DirectoryProvider]);
diff --git a/mobile/android/components/FilePicker.js b/mobile/android/components/FilePicker.js
new file mode 100644
index 0000000000..2de81ca465
--- /dev/null
+++ b/mobile/android/components/FilePicker.js
@@ -0,0 +1,302 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cc = Components.classes;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+
+Cu.importGlobalProperties(['File']);
+
+function FilePicker() {
+}
+
+FilePicker.prototype = {
+ _mimeTypeFilter: 0,
+ _extensionsFilter: "",
+ _defaultString: "",
+ _domWin: null,
+ _defaultExtension: null,
+ _displayDirectory: null,
+ _filePath: null,
+ _promptActive: false,
+ _filterIndex: 0,
+ _addToRecentDocs: false,
+ _title: "",
+
+ init: function(aParent, aTitle, aMode) {
+ this._domWin = aParent;
+ this._mode = aMode;
+ this._title = aTitle;
+ Services.obs.addObserver(this, "FilePicker:Result", false);
+
+ let idService = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+ this.guid = idService.generateUUID().toString();
+
+ if (aMode != Ci.nsIFilePicker.modeOpen && aMode != Ci.nsIFilePicker.modeOpenMultiple)
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ appendFilters: function(aFilterMask) {
+ if (aFilterMask & Ci.nsIFilePicker.filterAudio) {
+ this._mimeTypeFilter = "audio/*";
+ return;
+ }
+
+ if (aFilterMask & Ci.nsIFilePicker.filterImages) {
+ this._mimeTypeFilter = "image/*";
+ return;
+ }
+
+ if (aFilterMask & Ci.nsIFilePicker.filterVideo) {
+ this._mimeTypeFilter = "video/*";
+ return;
+ }
+
+ if (aFilterMask & Ci.nsIFilePicker.filterAll) {
+ this._mimeTypeFilter = "*/*";
+ return;
+ }
+
+ /* From BaseFilePicker.cpp */
+ if (aFilterMask & Ci.nsIFilePicker.filterHTML) {
+ this.appendFilter("*.html; *.htm; *.shtml; *.xhtml");
+ }
+ if (aFilterMask & Ci.nsIFilePicker.filterText) {
+ this.appendFilter("*.txt; *.text");
+ }
+
+ if (aFilterMask & Ci.nsIFilePicker.filterXML) {
+ this.appendFilter("*.xml");
+ }
+
+ if (aFilterMask & Ci.nsIFilePicker.xulFilter) {
+ this.appendFilter("*.xul");
+ }
+
+ if (aFilterMask & Ci.nsIFilePicker.xulFilter) {
+ this.appendFilter("..apps");
+ }
+ },
+
+ appendFilter: function(title, filter) {
+ if (this._extensionsFilter)
+ this._extensionsFilter += ", ";
+ this._extensionsFilter += filter;
+ },
+
+ get defaultString() {
+ return this._defaultString;
+ },
+
+ set defaultString(defaultString) {
+ this._defaultString = defaultString;
+ },
+
+ get defaultExtension() {
+ return this._defaultExtension;
+ },
+
+ set defaultExtension(defaultExtension) {
+ this._defaultExtension = defaultExtension;
+ },
+
+ get filterIndex() {
+ return this._filterIndex;
+ },
+
+ set filterIndex(val) {
+ this._filterIndex = val;
+ },
+
+ get displayDirectory() {
+ return this._displayDirectory;
+ },
+
+ set displayDirectory(dir) {
+ this._displayDirectory = dir;
+ },
+
+ get file() {
+ if (!this._filePath) {
+ return null;
+ }
+
+ return new FileUtils.File(this._filePath);
+ },
+
+ get fileURL() {
+ let file = this.getFile();
+ return Services.io.newFileURI(file);
+ },
+
+ get files() {
+ return this.getEnumerator([this.file], function(file) {
+ return file;
+ });
+ },
+
+ // We don't support directory selection yet.
+ get domFileOrDirectory() {
+ let f = this.file;
+ if (!f) {
+ return null;
+ }
+
+ let win = this._domWin;
+ if (win) {
+ let utils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ return utils.wrapDOMFile(f);
+ }
+
+ return File.createFromNsIFile(f);
+ },
+
+ get domFileOrDirectoryEnumerator() {
+ let win = this._domWin;
+ return this.getEnumerator([this.file], function(file) {
+ if (win) {
+ let utils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ return utils.wrapDOMFile(file);
+ }
+
+ return File.createFromNsIFile(file);
+ });
+ },
+
+ get addToRecentDocs() {
+ return this._addToRecentDocs;
+ },
+
+ set addToRecentDocs(val) {
+ this._addToRecentDocs = val;
+ },
+
+ get mode() {
+ return this._mode;
+ },
+
+ show: function() {
+ if (this._domWin) {
+ this.fireDialogEvent(this._domWin, "DOMWillOpenModalDialog");
+ let winUtils = this._domWin.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ winUtils.enterModalState();
+ }
+
+ this._promptActive = true;
+ this._sendMessage();
+
+ let thread = Services.tm.currentThread;
+ while (this._promptActive)
+ thread.processNextEvent(true);
+ delete this._promptActive;
+
+ if (this._domWin) {
+ let winUtils = this._domWin.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ winUtils.leaveModalState();
+ this.fireDialogEvent(this._domWin, "DOMModalDialogClosed");
+ }
+
+ if (this._filePath)
+ return Ci.nsIFilePicker.returnOK;
+
+ return Ci.nsIFilePicker.returnCancel;
+ },
+
+ open: function(callback) {
+ this._callback = callback;
+ this._sendMessage();
+ },
+
+ _sendMessage: function() {
+ let msg = {
+ type: "FilePicker:Show",
+ guid: this.guid,
+ title: this._title,
+ };
+
+ // Knowing the window lets us destroy any temp files when the tab is closed
+ // Other consumers of the file picker may have to either wait for Android
+ // to clean up the temp dir (not guaranteed) or clean up after themselves.
+ let win = Services.wm.getMostRecentWindow('navigator:browser');
+ let tab = win.BrowserApp.getTabForWindow(this._domWin.top)
+ if (tab) {
+ msg.tabId = tab.id;
+ }
+
+ if (!this._extensionsFilter && !this._mimeTypeFilter) {
+ // If neither filters is set show anything we can.
+ msg.mode = "mimeType";
+ msg.mimeType = "*/*";
+ } else if (this._extensionsFilter) {
+ msg.mode = "extension";
+ msg.extensions = this._extensionsFilter;
+ } else {
+ msg.mode = "mimeType";
+ msg.mimeType = this._mimeTypeFilter;
+ }
+
+ this.sendMessageToJava(msg);
+ },
+
+ sendMessageToJava: function(aMsg) {
+ Services.androidBridge.handleGeckoMessage(aMsg);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ let data = JSON.parse(aData);
+ if (data.guid != this.guid)
+ return;
+
+ this._filePath = null;
+ if (data.file)
+ this._filePath = data.file;
+
+ this._promptActive = false;
+
+ if (this._callback) {
+ this._callback.done(this._filePath ? Ci.nsIFilePicker.returnOK : Ci.nsIFilePicker.returnCancel);
+ }
+ delete this._callback;
+ },
+
+ getEnumerator: function(files, mapFunction) {
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator]),
+ mFiles: files,
+ mIndex: 0,
+ hasMoreElements: function() {
+ return (this.mIndex < this.mFiles.length);
+ },
+ getNext: function() {
+ if (this.mIndex >= this.mFiles.length) {
+ throw Components.results.NS_ERROR_FAILURE;
+ }
+ return mapFunction(this.mFiles[this.mIndex++]);
+ }
+ };
+ },
+
+ fireDialogEvent: function(aDomWin, aEventName) {
+ // accessing the document object can throw if this window no longer exists. See bug 789888.
+ try {
+ if (!aDomWin.document)
+ return;
+ let event = aDomWin.document.createEvent("Events");
+ event.initEvent(aEventName, true, true);
+ let winUtils = aDomWin.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ winUtils.dispatchEventToChromeOnly(aDomWin, event);
+ } catch(ex) {
+ }
+ },
+
+ classID: Components.ID("{18a4e042-7c7c-424b-a583-354e68553a7f}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIFilePicker, Ci.nsIObserver])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FilePicker]);
diff --git a/mobile/android/components/FxAccountsPush.js b/mobile/android/components/FxAccountsPush.js
new file mode 100644
index 0000000000..e6054a2de6
--- /dev/null
+++ b/mobile/android/components/FxAccountsPush.js
@@ -0,0 +1,164 @@
+/* jshint moz: true, esnext: true */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+const {
+ PushCrypto,
+ getCryptoParams,
+} = Cu.import("resource://gre/modules/PushCrypto.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "PushService",
+ "@mozilla.org/push/Service;1", "nsIPushService");
+XPCOMUtils.defineLazyGetter(this, "_decoder", () => new TextDecoder());
+
+const FXA_PUSH_SCOPE = "chrome://fxa-push";
+const Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccountsPush");
+
+function FxAccountsPush() {
+ Services.obs.addObserver(this, "FxAccountsPush:ReceivedPushMessageToDecode", false);
+
+ Messaging.sendRequestForResult({
+ type: "FxAccountsPush:Initialized"
+ });
+}
+
+FxAccountsPush.prototype = {
+ observe: function (subject, topic, data) {
+ switch (topic) {
+ case "android-push-service":
+ if (data === "android-fxa-subscribe") {
+ this._subscribe();
+ } else if (data === "android-fxa-unsubscribe") {
+ this._unsubscribe();
+ }
+ break;
+ case "FxAccountsPush:ReceivedPushMessageToDecode":
+ this._decodePushMessage(data);
+ break;
+ }
+ },
+
+ _subscribe() {
+ Log.i("FxAccountsPush _subscribe");
+ return new Promise((resolve, reject) => {
+ PushService.subscribe(FXA_PUSH_SCOPE,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, subscription) => {
+ if (Components.isSuccessCode(result)) {
+ Log.d("FxAccountsPush got subscription");
+ resolve(subscription);
+ } else {
+ Log.w("FxAccountsPush failed to subscribe", result);
+ reject(new Error("FxAccountsPush failed to subscribe"));
+ }
+ });
+ })
+ .then(subscription => {
+ Messaging.sendRequest({
+ type: "FxAccountsPush:Subscribe:Response",
+ subscription: {
+ pushCallback: subscription.endpoint,
+ pushPublicKey: urlsafeBase64Encode(subscription.getKey('p256dh')),
+ pushAuthKey: urlsafeBase64Encode(subscription.getKey('auth'))
+ }
+ });
+ })
+ .catch(err => {
+ Log.i("Error when registering FxA push endpoint " + err);
+ });
+ },
+
+ _unsubscribe() {
+ Log.i("FxAccountsPush _unsubscribe");
+ return new Promise((resolve) => {
+ PushService.unsubscribe(FXA_PUSH_SCOPE,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, ok) => {
+ if (Components.isSuccessCode(result)) {
+ if (ok === true) {
+ Log.d("FxAccountsPush unsubscribed");
+ } else {
+ Log.d("FxAccountsPush had no subscription to unsubscribe");
+ }
+ } else {
+ Log.w("FxAccountsPush failed to unsubscribe", result);
+ }
+ return resolve(ok);
+ });
+ }).catch(err => {
+ Log.e("Error during unsubscribe", err);
+ });
+ },
+
+ _decodePushMessage(data) {
+ Log.i("FxAccountsPush _decodePushMessage");
+ data = JSON.parse(data);
+ let { headers, message } = this._messageAndHeaders(data);
+ return new Promise((resolve, reject) => {
+ PushService.getSubscription(FXA_PUSH_SCOPE,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, subscription) => {
+ if (!subscription) {
+ return reject(new Error("No subscription found"));
+ }
+ return resolve(subscription);
+ });
+ }).then(subscription => {
+ return PushCrypto.decrypt(subscription.p256dhPrivateKey,
+ new Uint8Array(subscription.getKey("p256dh")),
+ new Uint8Array(subscription.getKey("auth")),
+ headers, message);
+ })
+ .then(plaintext => {
+ let decryptedMessage = plaintext ? _decoder.decode(plaintext) : "";
+ Messaging.sendRequestForResult({
+ type: "FxAccountsPush:ReceivedPushMessageToDecode:Response",
+ message: decryptedMessage
+ });
+ })
+ .catch(err => {
+ Log.d("Error while decoding incoming message : " + err);
+ });
+ },
+
+ // Copied from PushServiceAndroidGCM
+ _messageAndHeaders(data) {
+ // Default is no data (and no encryption).
+ let message = null;
+ let headers = null;
+
+ if (data.message && data.enc && (data.enckey || data.cryptokey)) {
+ headers = {
+ encryption_key: data.enckey,
+ crypto_key: data.cryptokey,
+ encryption: data.enc,
+ encoding: data.con,
+ };
+ // Ciphertext is (urlsafe) Base 64 encoded.
+ message = ChromeUtils.base64URLDecode(data.message, {
+ // The Push server may append padding.
+ padding: "ignore",
+ });
+ }
+ return { headers, message };
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ classID: Components.ID("{d1bbb0fd-1d47-4134-9c12-d7b1be20b721}")
+};
+
+function urlsafeBase64Encode(key) {
+ return ChromeUtils.base64URLEncode(new Uint8Array(key), { pad: false });
+}
+
+var components = [ FxAccountsPush ];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
diff --git a/mobile/android/components/HelperAppDialog.js b/mobile/android/components/HelperAppDialog.js
new file mode 100644
index 0000000000..f127fb0b3b
--- /dev/null
+++ b/mobile/android/components/HelperAppDialog.js
@@ -0,0 +1,373 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*globals ContentAreaUtils */
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+const APK_MIME_TYPE = "application/vnd.android.package-archive";
+
+const OMA_DOWNLOAD_DESCRIPTOR_MIME_TYPE = "application/vnd.oma.dd+xml";
+const OMA_DRM_MESSAGE_MIME = "application/vnd.oma.drm.message";
+const OMA_DRM_CONTENT_MIME = "application/vnd.oma.drm.content";
+const OMA_DRM_RIGHTS_MIME = "application/vnd.oma.drm.rights+wbxml";
+
+const PREF_BD_USEDOWNLOADDIR = "browser.download.useDownloadDir";
+const URI_GENERIC_ICON_DOWNLOAD = "drawable://alert_download";
+
+Cu.import("resource://gre/modules/Downloads.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/HelperApps.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "RuntimePermissions", "resource://gre/modules/RuntimePermissions.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+
+// -----------------------------------------------------------------------
+// HelperApp Launcher Dialog
+// -----------------------------------------------------------------------
+
+XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() {
+ let ContentAreaUtils = {};
+ Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils);
+ return ContentAreaUtils;
+});
+
+function HelperAppLauncherDialog() { }
+
+HelperAppLauncherDialog.prototype = {
+ classID: Components.ID("{e9d277a0-268a-4ec2-bb8c-10fdf3e44611}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]),
+
+ /**
+ * Returns false if `url` represents a local or special URL that we don't
+ * wish to ever download.
+ *
+ * Returns true otherwise.
+ */
+ _canDownload: function (url, alreadyResolved=false) {
+ // The common case.
+ if (url.schemeIs("http") ||
+ url.schemeIs("https") ||
+ url.schemeIs("ftp")) {
+ return true;
+ }
+
+ // The less-common opposite case.
+ if (url.schemeIs("chrome") ||
+ url.schemeIs("jar") ||
+ url.schemeIs("resource") ||
+ url.schemeIs("wyciwyg") ||
+ url.schemeIs("file")) {
+ return false;
+ }
+
+ // For all other URIs, try to resolve them to an inner URI, and check that.
+ if (!alreadyResolved) {
+ let innerURI = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true
+ }).URI;
+
+ if (!url.equals(innerURI)) {
+ return this._canDownload(innerURI, true);
+ }
+ }
+
+ // Anything else is fine to download.
+ return true;
+ },
+
+ /**
+ * Returns true if `launcher` represents a download for which we wish
+ * to prompt.
+ */
+ _shouldPrompt: function (launcher) {
+ let mimeType = this._getMimeTypeFromLauncher(launcher);
+
+ // Straight equality: nsIMIMEInfo normalizes.
+ return APK_MIME_TYPE == mimeType || OMA_DOWNLOAD_DESCRIPTOR_MIME_TYPE == mimeType;
+ },
+
+ /**
+ * Returns true if `launcher` represents a download for which we wish to
+ * offer a "Save to disk" option.
+ */
+ _shouldAddSaveToDiskIntent: function(launcher) {
+ let mimeType = this._getMimeTypeFromLauncher(launcher);
+
+ // We can't handle OMA downloads. So don't even try. (Bug 1219078)
+ return mimeType != OMA_DOWNLOAD_DESCRIPTOR_MIME_TYPE;
+ },
+
+ /**
+ * Returns true if `launcher`represents a download that should not be handled by Firefox
+ * or a third-party app and instead be forwarded to Android's download manager.
+ */
+ _shouldForwardToAndroidDownloadManager: function(aLauncher) {
+ let forwardDownload = Services.prefs.getBoolPref('browser.download.forward_oma_android_download_manager');
+ if (!forwardDownload) {
+ return false;
+ }
+
+ let mimeType = aLauncher.MIMEInfo.MIMEType;
+ if (!mimeType) {
+ mimeType = ContentAreaUtils.getMIMETypeForURI(aLauncher.source) || "";
+ }
+
+ return [
+ OMA_DOWNLOAD_DESCRIPTOR_MIME_TYPE,
+ OMA_DRM_MESSAGE_MIME,
+ OMA_DRM_CONTENT_MIME,
+ OMA_DRM_RIGHTS_MIME
+ ].indexOf(mimeType) != -1;
+ },
+
+ show: function hald_show(aLauncher, aContext, aReason) {
+ if (!this._canDownload(aLauncher.source)) {
+ this._refuseDownload(aLauncher);
+ return;
+ }
+
+ if (this._shouldForwardToAndroidDownloadManager(aLauncher)) {
+ Task.spawn(function* () {
+ try {
+ let hasPermission = yield RuntimePermissions.waitForPermissions(RuntimePermissions.WRITE_EXTERNAL_STORAGE);
+ if (hasPermission) {
+ this._downloadWithAndroidDownloadManager(aLauncher);
+ aLauncher.cancel(Cr.NS_BINDING_ABORTED);
+ }
+ } finally {
+ }
+ }.bind(this)).catch(Cu.reportError);
+ return;
+ }
+
+ let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+
+ let defaultHandler = new Object();
+ let apps = HelperApps.getAppsForUri(aLauncher.source, {
+ mimeType: aLauncher.MIMEInfo.MIMEType,
+ });
+
+ if (this._shouldAddSaveToDiskIntent(aLauncher)) {
+ // Add a fake intent for save to disk at the top of the list.
+ apps.unshift({
+ name: bundle.GetStringFromName("helperapps.saveToDisk"),
+ packageName: "org.mozilla.gecko.Download",
+ iconUri: "drawable://icon",
+ selected: true, // Default to download for files
+ launch: function() {
+ // Reset the preferredAction here.
+ aLauncher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.saveToDisk;
+ aLauncher.saveToDisk(null, false);
+ return true;
+ }
+ });
+ }
+
+ // We do not handle this download and there are no apps that want to do it
+ if (apps.length === 0) {
+ this._refuseDownload(aLauncher);
+ return;
+ }
+
+ let callback = function(app) {
+ aLauncher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
+ if (!app.launch(aLauncher.source)) {
+ // Once the app is done we need to get rid of the temp file. This shouldn't
+ // get run in the saveToDisk case.
+ aLauncher.cancel(Cr.NS_BINDING_ABORTED);
+ }
+ }
+
+ // See if the user already marked something as the default for this mimetype,
+ // and if that app is still installed.
+ let preferredApp = this._getPreferredApp(aLauncher);
+ if (preferredApp) {
+ let pref = apps.filter(function(app) {
+ return app.packageName === preferredApp;
+ });
+
+ if (pref.length > 0) {
+ callback(pref[0]);
+ return;
+ }
+ }
+
+ // If there's only one choice, and we don't want to prompt, go right ahead
+ // and choose that app automatically.
+ if (!this._shouldPrompt(aLauncher) && (apps.length === 1)) {
+ callback(apps[0]);
+ return;
+ }
+
+ // Otherwise, let's go through the prompt.
+ HelperApps.prompt(apps, {
+ title: bundle.GetStringFromName("helperapps.pick"),
+ buttons: [
+ bundle.GetStringFromName("helperapps.alwaysUse"),
+ bundle.GetStringFromName("helperapps.useJustOnce")
+ ],
+ // Tapping an app twice should choose "Just once".
+ doubleTapButton: 1
+ }, (data) => {
+ if (data.button < 0) {
+ return;
+ }
+
+ callback(apps[data.icongrid0]);
+
+ if (data.button === 0) {
+ this._setPreferredApp(aLauncher, apps[data.icongrid0]);
+ }
+ });
+ },
+
+ _refuseDownload: function(aLauncher) {
+ aLauncher.cancel(Cr.NS_BINDING_ABORTED);
+
+ Services.console.logStringMessage("Refusing download of non-downloadable file.");
+
+ let bundle = Services.strings.createBundle("chrome://browser/locale/handling.properties");
+ let failedText = bundle.GetStringFromName("download.blocked");
+
+ Snackbars.show(failedText, Snackbars.LENGTH_LONG);
+ },
+
+ _downloadWithAndroidDownloadManager(aLauncher) {
+ let mimeType = aLauncher.MIMEInfo.MIMEType;
+ if (!mimeType) {
+ mimeType = ContentAreaUtils.getMIMETypeForURI(aLauncher.source) || "";
+ }
+
+ Messaging.sendRequest({
+ 'type': 'Download:AndroidDownloadManager',
+ 'uri': aLauncher.source.spec,
+ 'mimeType': mimeType,
+ 'filename': aLauncher.suggestedFileName
+ });
+ },
+
+ _getPrefName: function getPrefName(mimetype) {
+ return "browser.download.preferred." + mimetype.replace("\\", ".");
+ },
+
+ _getMimeTypeFromLauncher: function (launcher) {
+ let mime = launcher.MIMEInfo.MIMEType;
+ if (!mime)
+ mime = ContentAreaUtils.getMIMETypeForURI(launcher.source) || "";
+ return mime;
+ },
+
+ _getPreferredApp: function getPreferredApp(launcher) {
+ let mime = this._getMimeTypeFromLauncher(launcher);
+ if (!mime)
+ return;
+
+ try {
+ return Services.prefs.getCharPref(this._getPrefName(mime));
+ } catch(ex) {
+ Services.console.logStringMessage("Error getting pref for " + mime + ".");
+ }
+ return null;
+ },
+
+ _setPreferredApp: function setPreferredApp(launcher, app) {
+ let mime = this._getMimeTypeFromLauncher(launcher);
+ if (!mime)
+ return;
+
+ if (app)
+ Services.prefs.setCharPref(this._getPrefName(mime), app.packageName);
+ else
+ Services.prefs.clearUserPref(this._getPrefName(mime));
+ },
+
+ promptForSaveToFileAsync: function (aLauncher, aContext, aDefaultFile,
+ aSuggestedFileExt, aForcePrompt) {
+ Task.spawn(function* () {
+ let file = null;
+ try {
+ let hasPermission = yield RuntimePermissions.waitForPermissions(RuntimePermissions.WRITE_EXTERNAL_STORAGE);
+ if (hasPermission) {
+ // If we do have the STORAGE permission then pick the public downloads directory as destination
+ // for this file. Without the permission saveDestinationAvailable(null) will be called which
+ // will effectively cancel the download.
+ let preferredDir = yield Downloads.getPreferredDownloadsDirectory();
+ file = this.validateLeafName(new FileUtils.File(preferredDir),
+ aDefaultFile, aSuggestedFileExt);
+ }
+ } finally {
+ // The file argument will be null in case any exception occurred.
+ aLauncher.saveDestinationAvailable(file);
+ }
+ }.bind(this)).catch(Cu.reportError);
+ },
+
+ validateLeafName: function hald_validateLeafName(aLocalFile, aLeafName, aFileExt) {
+ if (!(aLocalFile && this.isUsableDirectory(aLocalFile)))
+ return null;
+
+ // Remove any leading periods, since we don't want to save hidden files
+ // automatically.
+ aLeafName = aLeafName.replace(/^\.+/, "");
+
+ if (aLeafName == "")
+ aLeafName = "unnamed" + (aFileExt ? "." + aFileExt : "");
+ aLocalFile.append(aLeafName);
+
+ this.makeFileUnique(aLocalFile);
+ return aLocalFile;
+ },
+
+ makeFileUnique: function hald_makeFileUnique(aLocalFile) {
+ try {
+ // Note - this code is identical to that in
+ // toolkit/content/contentAreaUtils.js.
+ // If you are updating this code, update that code too! We can't share code
+ // here since this is called in a js component.
+ let collisionCount = 0;
+ while (aLocalFile.exists()) {
+ collisionCount++;
+ if (collisionCount == 1) {
+ // Append "(2)" before the last dot in (or at the end of) the filename
+ // special case .ext.gz etc files so we don't wind up with .tar(2).gz
+ if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i))
+ aLocalFile.leafName = aLocalFile.leafName.replace(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i, "(2)$&");
+ else
+ aLocalFile.leafName = aLocalFile.leafName.replace(/(\.[^\.]*)?$/, "(2)$&");
+ }
+ else {
+ // replace the last (n) in the filename with (n+1)
+ aLocalFile.leafName = aLocalFile.leafName.replace(/^(.*\()\d+\)/, "$1" + (collisionCount+1) + ")");
+ }
+ }
+ aLocalFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+ }
+ catch (e) {
+ dump("*** exception in validateLeafName: " + e + "\n");
+
+ if (e.result == Cr.NS_ERROR_FILE_ACCESS_DENIED)
+ throw e;
+
+ if (aLocalFile.leafName == "" || aLocalFile.isDirectory()) {
+ aLocalFile.append("unnamed");
+ if (aLocalFile.exists())
+ aLocalFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+ }
+ }
+ },
+
+ isUsableDirectory: function hald_isUsableDirectory(aDirectory) {
+ return aDirectory.exists() && aDirectory.isDirectory() && aDirectory.isWritable();
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([HelperAppLauncherDialog]);
diff --git a/mobile/android/components/ImageBlockingPolicy.js b/mobile/android/components/ImageBlockingPolicy.js
new file mode 100644
index 0000000000..2444bda066
--- /dev/null
+++ b/mobile/android/components/ImageBlockingPolicy.js
@@ -0,0 +1,125 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { classes: Cc, interfaces: Ci, manager: Cm, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+
+////////////////////////////////////////////////////////////////////////////////
+//// Constants
+
+//// SVG placeholder image for blocked image content
+const PLACEHOLDER_IMG = "chrome://browser/skin/images/placeholder_image.svg";
+
+//// Telemetry
+const TELEMETRY_TAP_TO_LOAD_ENABLED = "TAP_TO_LOAD_ENABLED";
+const TELEMETRY_SHOW_IMAGE_SIZE = "TAP_TO_LOAD_IMAGE_SIZE";
+const TOPIC_GATHER_TELEMETRY = "gather-telemetry";
+
+//// Gecko preference
+const PREF_IMAGEBLOCKING = "browser.image_blocking";
+
+//// Enabled options
+const OPTION_NEVER = 0;
+const OPTION_ALWAYS = 1;
+const OPTION_WIFI_ONLY = 2;
+
+
+/**
+ * Content policy for blocking images
+ */
+function ImageBlockingPolicy() {
+ Services.obs.addObserver(this, TOPIC_GATHER_TELEMETRY, false);
+}
+
+ImageBlockingPolicy.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPolicy, Ci.nsIObserver]),
+ classDescription: "Click-To-Play Image",
+ classID: Components.ID("{f55f77f9-d33d-4759-82fc-60db3ee0bb91}"),
+ contractID: "@mozilla.org/browser/blockimages-policy;1",
+ xpcom_categories: [{category: "content-policy", service: true}],
+
+ // nsIContentPolicy interface implementation
+ shouldLoad: function(contentType, contentLocation, requestOrigin, node, mimeTypeGuess, extra) {
+ // When enabled or when on cellular, and option for cellular-only is selected
+ if (this._enabled() == OPTION_NEVER || (this._enabled() == OPTION_WIFI_ONLY && this._usingCellular())) {
+ if (contentType === Ci.nsIContentPolicy.TYPE_IMAGE || contentType === Ci.nsIContentPolicy.TYPE_IMAGESET) {
+ // Accept any non-http(s) image URLs
+ if (!contentLocation.schemeIs("http") && !contentLocation.schemeIs("https")) {
+ return Ci.nsIContentPolicy.ACCEPT;
+ }
+
+ if (node instanceof Ci.nsIDOMHTMLImageElement) {
+ // Accept if the user has asked to view the image
+ if (node.getAttribute("data-ctv-show") == "true") {
+ sendImageSizeTelemetry(node.getAttribute("data-ctv-src"));
+ return Ci.nsIContentPolicy.ACCEPT;
+ }
+
+ setTimeout(() => {
+ // Cache the original image URL and swap in our placeholder
+ node.setAttribute("data-ctv-src", contentLocation.spec);
+ node.setAttribute("src", PLACEHOLDER_IMG);
+
+ // For imageset (img + srcset) the "srcset" is used even after we reset the "src" causing a loop.
+ // We are given the final image URL anyway, so it's OK to just remove the "srcset" value.
+ node.removeAttribute("srcset");
+ }, 0);
+ }
+
+ // Reject any image that is not associated with a DOM element
+ return Ci.nsIContentPolicy.REJECT;
+ }
+ }
+
+ // Accept all other content types
+ return Ci.nsIContentPolicy.ACCEPT;
+ },
+
+ shouldProcess: function(contentType, contentLocation, requestOrigin, node, mimeTypeGuess, extra) {
+ return Ci.nsIContentPolicy.ACCEPT;
+ },
+
+ _usingCellular: function() {
+ let network = Cc["@mozilla.org/network/network-link-service;1"].getService(Ci.nsINetworkLinkService);
+ return !(network.linkType == Ci.nsINetworkLinkService.LINK_TYPE_UNKNOWN ||
+ network.linkType == Ci.nsINetworkLinkService.LINK_TYPE_ETHERNET ||
+ network.linkType == Ci.nsINetworkLinkService.LINK_TYPE_USB ||
+ network.linkType == Ci.nsINetworkLinkService.LINK_TYPE_WIFI);
+ },
+
+ _enabled: function() {
+ return Services.prefs.getIntPref(PREF_IMAGEBLOCKING);
+ },
+
+ observe : function (subject, topic, data) {
+ if (topic == TOPIC_GATHER_TELEMETRY) {
+ Services.telemetry.getHistogramById(TELEMETRY_TAP_TO_LOAD_ENABLED).add(this._enabled());
+ }
+ },
+};
+
+function sendImageSizeTelemetry(imageURL) {
+ let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
+ xhr.open("HEAD", imageURL, true);
+ xhr.onreadystatechange = function (e) {
+ if (xhr.readyState != 4) {
+ return;
+ }
+ if (xhr.status != 200) {
+ return;
+ }
+ let contentLength = xhr.getResponseHeader("Content-Length");
+ if (!contentLength) {
+ return;
+ }
+ let imageSize = contentLength / 1024;
+ Services.telemetry.getHistogramById(TELEMETRY_SHOW_IMAGE_SIZE).add(imageSize);
+ };
+ xhr.send(null);
+}
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ImageBlockingPolicy]);
diff --git a/mobile/android/components/LoginManagerPrompter.js b/mobile/android/components/LoginManagerPrompter.js
new file mode 100644
index 0000000000..e70afbe147
--- /dev/null
+++ b/mobile/android/components/LoginManagerPrompter.js
@@ -0,0 +1,413 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this
+* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+/* Constants for password prompt telemetry.
+* Mirrored in nsLoginManagerPrompter.js */
+const PROMPT_DISPLAYED = 0;
+
+const PROMPT_ADD = 1;
+const PROMPT_NOTNOW = 2;
+const PROMPT_NEVER = 3;
+
+const PROMPT_UPDATE = 1;
+
+/* ==================== LoginManagerPrompter ==================== */
+/*
+ * LoginManagerPrompter
+ *
+ * Implements interfaces for prompting the user to enter/save/change auth info.
+ *
+ * nsILoginManagerPrompter: Used by Login Manager for saving/changing logins
+ * found in HTML forms.
+ */
+function LoginManagerPrompter() {
+}
+
+LoginManagerPrompter.prototype = {
+ classID : Components.ID("97d12931-abe2-11df-94e2-0800200c9a66"),
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerPrompter]),
+
+ _factory : null,
+ _window : null,
+ _debug : false, // mirrors signon.debug
+
+ __pwmgr : null, // Password Manager service
+ get _pwmgr() {
+ if (!this.__pwmgr)
+ this.__pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+ return this.__pwmgr;
+ },
+
+ __promptService : null, // Prompt service for user interaction
+ get _promptService() {
+ if (!this.__promptService)
+ this.__promptService = Cc["@mozilla.org/embedcomp/prompt-service;1"].
+ getService(Ci.nsIPromptService2);
+ return this.__promptService;
+ },
+
+ __strBundle : null, // String bundle for L10N
+ get _strBundle() {
+ if (!this.__strBundle) {
+ let bunService = Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService);
+ this.__strBundle = {
+ pwmgr : bunService.createBundle("chrome://passwordmgr/locale/passwordmgr.properties"),
+ brand : bunService.createBundle("chrome://branding/locale/brand.properties")
+ };
+
+ if (!this.__strBundle)
+ throw "String bundle for Login Manager not present!";
+ }
+
+ return this.__strBundle;
+ },
+
+ __ellipsis : null,
+ get _ellipsis() {
+ if (!this.__ellipsis) {
+ this.__ellipsis = "\u2026";
+ try {
+ this.__ellipsis = Services.prefs.getComplexValue(
+ "intl.ellipsis", Ci.nsIPrefLocalizedString).data;
+ } catch (e) { }
+ }
+ return this.__ellipsis;
+ },
+
+ /*
+ * log
+ *
+ * Internal function for logging debug messages to the Error Console window.
+ */
+ log : function (message) {
+ if (!this._debug)
+ return;
+
+ dump("Pwmgr Prompter: " + message + "\n");
+ Services.console.logStringMessage("Pwmgr Prompter: " + message);
+ },
+
+ /* ---------- nsILoginManagerPrompter prompts ---------- */
+
+ /*
+ * init
+ *
+ */
+ init : function (aWindow, aFactory) {
+ this._chromeWindow = this._getChromeWindow(aWindow).wrappedJSObject;
+ this._factory = aFactory || null;
+ this._browser = null;
+
+ var prefBranch = Services.prefs.getBranch("signon.");
+ this._debug = prefBranch.getBoolPref("debug");
+ this.log("===== initialized =====");
+ },
+
+ set browser(aBrowser) {
+ this._browser = aBrowser;
+ },
+
+ // setting this attribute is ignored because Android does not consider
+ // opener windows when displaying login notifications
+ set opener(aOpener) { },
+
+ /*
+ * promptToSavePassword
+ *
+ */
+ promptToSavePassword : function (aLogin) {
+ this._showSaveLoginNotification(aLogin);
+ Services.telemetry.getHistogramById("PWMGR_PROMPT_REMEMBER_ACTION").add(PROMPT_DISPLAYED);
+ Services.obs.notifyObservers(aLogin, "passwordmgr-prompt-save", null);
+ },
+
+ /*
+ * _showLoginNotification
+ *
+ * Displays a notification doorhanger.
+ * @param aBody
+ * String message to be displayed in the doorhanger
+ * @param aButtons
+ * Buttons to display with the doorhanger
+ * @param aUsername
+ * Username string used in creating a doorhanger action
+ * @param aPassword
+ * Password string used in creating a doorhanger action
+ */
+ _showLoginNotification : function (aBody, aButtons, aUsername, aPassword) {
+ let tabID = this._chromeWindow.BrowserApp.getTabForBrowser(this._browser).id;
+
+ let actionText = {
+ text: aUsername,
+ type: "EDIT",
+ bundle: { username: aUsername,
+ password: aPassword }
+ };
+
+ // The page we're going to hasn't loaded yet, so we want to persist
+ // across the first location change.
+
+ // Sites like Gmail perform a funky redirect dance before you end up
+ // at the post-authentication page. I don't see a good way to
+ // heuristically determine when to ignore such location changes, so
+ // we'll try ignoring location changes based on a time interval.
+ let options = {
+ persistWhileVisible: true,
+ timeout: Date.now() + 10000,
+ actionText: actionText
+ }
+
+ var nativeWindow = this._getNativeWindow();
+ if (nativeWindow)
+ nativeWindow.doorhanger.show(aBody, "password", aButtons, tabID, options, "LOGIN");
+ },
+
+ /*
+ * _showSaveLoginNotification
+ *
+ * Displays a notification doorhanger (rather than a popup), to allow the user to
+ * save the specified login. This allows the user to see the results of
+ * their login, and only save a login which they know worked.
+ *
+ */
+ _showSaveLoginNotification : function (aLogin) {
+ let brandShortName = this._strBundle.brand.GetStringFromName("brandShortName");
+ let notificationText = this._getLocalizedString("saveLogin", [brandShortName]);
+
+ let username = aLogin.username ? this._sanitizeUsername(aLogin.username) : "";
+
+ // The callbacks in |buttons| have a closure to access the variables
+ // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
+ // without a getService() call.
+ var pwmgr = this._pwmgr;
+ let promptHistogram = Services.telemetry.getHistogramById("PWMGR_PROMPT_REMEMBER_ACTION");
+
+ var buttons = [
+ {
+ label: this._getLocalizedString("neverButton"),
+ callback: function() {
+ promptHistogram.add(PROMPT_NEVER);
+ pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
+ }
+ },
+ {
+ label: this._getLocalizedString("rememberButton"),
+ callback: function(checked, response) {
+ if (response) {
+ aLogin.username = response["username"] || aLogin.username;
+ aLogin.password = response["password"] || aLogin.password;
+ }
+ pwmgr.addLogin(aLogin);
+ promptHistogram.add(PROMPT_ADD);
+ },
+ positive: true
+ }
+ ];
+
+ this._showLoginNotification(notificationText, buttons, aLogin.username, aLogin.password);
+ },
+
+ /*
+ * promptToChangePassword
+ *
+ * Called when we think we detect a password change for an existing
+ * login, when the form being submitted contains multiple password
+ * fields.
+ *
+ */
+ promptToChangePassword : function (aOldLogin, aNewLogin) {
+ this._showChangeLoginNotification(aOldLogin, aNewLogin.password);
+ Services.telemetry.getHistogramById("PWMGR_PROMPT_UPDATE_ACTION").add(PROMPT_DISPLAYED);
+ let oldGUID = aOldLogin.QueryInterface(Ci.nsILoginMetaInfo).guid;
+ Services.obs.notifyObservers(aNewLogin, "passwordmgr-prompt-change", oldGUID);
+ },
+
+ /*
+ * _showChangeLoginNotification
+ *
+ * Shows the Change Password notification doorhanger.
+ *
+ */
+ _showChangeLoginNotification : function (aOldLogin, aNewPassword) {
+ var notificationText;
+ if (aOldLogin.username) {
+ let displayUser = this._sanitizeUsername(aOldLogin.username);
+ notificationText = this._getLocalizedString("updatePassword", [displayUser]);
+ } else {
+ notificationText = this._getLocalizedString("updatePasswordNoUser");
+ }
+
+ // The callbacks in |buttons| have a closure to access the variables
+ // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
+ // without a getService() call.
+ var self = this;
+ let promptHistogram = Services.telemetry.getHistogramById("PWMGR_PROMPT_UPDATE_ACTION");
+
+ var buttons = [
+ {
+ label: this._getLocalizedString("dontUpdateButton"),
+ callback: function() {
+ promptHistogram.add(PROMPT_NOTNOW);
+ // do nothing
+ }
+ },
+ {
+ label: this._getLocalizedString("updateButton"),
+ callback: function(checked, response) {
+ let password = response ? response["password"] : aNewPassword;
+ self._updateLogin(aOldLogin, password);
+
+ promptHistogram.add(PROMPT_UPDATE);
+ },
+ positive: true
+ }
+ ];
+
+ this._showLoginNotification(notificationText, buttons, aOldLogin.username, aNewPassword);
+ },
+
+ /*
+ * promptToChangePasswordWithUsernames
+ *
+ * Called when we detect a password change in a form submission, but we
+ * don't know which existing login (username) it's for. Asks the user
+ * to select a username and confirm the password change.
+ *
+ * Note: The caller doesn't know the username for aNewLogin, so this
+ * function fills in .username and .usernameField with the values
+ * from the login selected by the user.
+ *
+ * Note; XPCOM stupidity: |count| is just |logins.length|.
+ */
+ promptToChangePasswordWithUsernames : function (logins, count, aNewLogin) {
+ const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS;
+
+ var usernames = logins.map(l => l.username);
+ var dialogText = this._getLocalizedString("userSelectText");
+ var dialogTitle = this._getLocalizedString("passwordChangeTitle");
+ var selectedIndex = { value: null };
+
+ // If user selects ok, outparam.value is set to the index
+ // of the selected username.
+ var ok = this._promptService.select(null,
+ dialogTitle, dialogText,
+ usernames.length, usernames,
+ selectedIndex);
+ if (ok) {
+ // Now that we know which login to use, modify its password.
+ let selectedLogin = logins[selectedIndex.value];
+ this.log("Updating password for user " + selectedLogin.username);
+ this._updateLogin(selectedLogin, aNewLogin.password);
+ }
+ },
+
+ /* ---------- Internal Methods ---------- */
+
+ /*
+ * _updateLogin
+ */
+ _updateLogin : function (login, newPassword) {
+ var now = Date.now();
+ var propBag = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+ if (newPassword) {
+ propBag.setProperty("password", newPassword);
+ // Explicitly set the password change time here (even though it would
+ // be changed automatically), to ensure that it's exactly the same
+ // value as timeLastUsed.
+ propBag.setProperty("timePasswordChanged", now);
+ }
+ propBag.setProperty("timeLastUsed", now);
+ propBag.setProperty("timesUsedIncrement", 1);
+ this._pwmgr.modifyLogin(login, propBag);
+ },
+
+ /*
+ * _getChromeWindow
+ *
+ * Given a content DOM window, returns the chrome window it's in.
+ */
+ _getChromeWindow: function (aWindow) {
+ if (aWindow instanceof Ci.nsIDOMChromeWindow)
+ return aWindow;
+ var chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .chromeEventHandler.ownerDocument.defaultView;
+ return chromeWin;
+ },
+
+ /*
+ * _getNativeWindow
+ *
+ * Returns the NativeWindow to this prompter, or null if there isn't
+ * a NativeWindow available (w/ error sent to logcat).
+ */
+ _getNativeWindow : function () {
+ let nativeWindow = null;
+ try {
+ let chromeWin = this._chromeWindow;
+ if (chromeWin.NativeWindow) {
+ nativeWindow = chromeWin.NativeWindow;
+ } else {
+ Cu.reportError("NativeWindow not available on window");
+ }
+
+ } catch (e) {
+ // If any errors happen, just assume no native window helper.
+ Cu.reportError("No NativeWindow available: " + e);
+ }
+ return nativeWindow;
+ },
+
+ /*
+ * _getLocalizedString
+ *
+ * Can be called as:
+ * _getLocalizedString("key1");
+ * _getLocalizedString("key2", ["arg1"]);
+ * _getLocalizedString("key3", ["arg1", "arg2"]);
+ * (etc)
+ *
+ * Returns the localized string for the specified key,
+ * formatted if required.
+ *
+ */
+ _getLocalizedString : function (key, formatArgs) {
+ if (formatArgs)
+ return this._strBundle.pwmgr.formatStringFromName(
+ key, formatArgs, formatArgs.length);
+ else
+ return this._strBundle.pwmgr.GetStringFromName(key);
+ },
+
+ /*
+ * _sanitizeUsername
+ *
+ * Sanitizes the specified username, by stripping quotes and truncating if
+ * it's too long. This helps prevent an evil site from messing with the
+ * "save password?" prompt too much.
+ */
+ _sanitizeUsername : function (username) {
+ if (username.length > 30) {
+ username = username.substring(0, 30);
+ username += this._ellipsis;
+ }
+ return username.replace(/['"]/g, "");
+ },
+}; // end of LoginManagerPrompter implementation
+
+
+var component = [LoginManagerPrompter];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
diff --git a/mobile/android/components/MobileComponents.manifest b/mobile/android/components/MobileComponents.manifest
new file mode 100644
index 0000000000..8cf8f9a27e
--- /dev/null
+++ b/mobile/android/components/MobileComponents.manifest
@@ -0,0 +1,126 @@
+# AboutRedirector.js
+component {322ba47e-7047-4f71-aebf-cb7d69325cd9} AboutRedirector.js
+contract @mozilla.org/network/protocol/about;1?what= {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=fennec {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=firefox {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=empty {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=rights {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=certerror {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=home {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=downloads {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=reader {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=feedback {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=privatebrowsing {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+#ifdef MOZ_SERVICES_HEALTHREPORT
+contract @mozilla.org/network/protocol/about;1?what=healthreport {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+#endif
+contract @mozilla.org/network/protocol/about;1?what=blocked {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=accounts {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=logins {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+
+# DirectoryProvider.js
+component {ef0f7a87-c1ee-45a8-8d67-26f586e46a4b} DirectoryProvider.js
+contract @mozilla.org/browser/directory-provider;1 {ef0f7a87-c1ee-45a8-8d67-26f586e46a4b}
+category xpcom-directory-providers browser-directory-provider @mozilla.org/browser/directory-provider;1
+
+# stylesheets
+category agent-style-sheets browser-content-stylesheet chrome://browser/skin/content.css
+
+# SessionStore.js
+component {8c1f07d6-cba3-4226-a315-8bd43d67d032} SessionStore.js
+contract @mozilla.org/browser/sessionstore;1 {8c1f07d6-cba3-4226-a315-8bd43d67d032}
+category app-startup SessionStore service,@mozilla.org/browser/sessionstore;1
+
+# ContentPermissionPrompt.js
+component {C6E8C44D-9F39-4AF7-BCC0-76E38A8310F5} ContentPermissionPrompt.js
+contract @mozilla.org/content-permission/prompt;1 {C6E8C44D-9F39-4AF7-BCC0-76E38A8310F5}
+
+# PromptService.js
+component {9a61149b-2276-4a0a-b79c-be994ad106cf} PromptService.js
+contract @mozilla.org/prompter;1 {9a61149b-2276-4a0a-b79c-be994ad106cf}
+contract @mozilla.org/embedcomp/prompt-service;1 {9a61149b-2276-4a0a-b79c-be994ad106cf}
+component {80dae1e9-e0d2-4974-915f-f97050fa8068} PromptService.js
+contract @mozilla.org/network/authprompt-adapter-factory;1 {80dae1e9-e0d2-4974-915f-f97050fa8068}
+
+# PresentationDevicePrompt.js
+component {388bd149-c919-4a43-b646-d7ec57877689} PresentationDevicePrompt.js
+contract @mozilla.org/presentation-device/prompt;1 {388bd149-c919-4a43-b646-d7ec57877689}
+
+# PresentationRequestUIGlue.js
+component {9c550ef7-3ff6-4bd1-9ad1-5a3735b90d21} PresentationRequestUIGlue.js
+contract @mozilla.org/presentation/requestuiglue;1 {9c550ef7-3ff6-4bd1-9ad1-5a3735b90d21}
+
+# ImageBlockingPolicy.js
+component {f55f77f9-d33d-4759-82fc-60db3ee0bb91} ImageBlockingPolicy.js
+contract @mozilla.org/browser/blockimages-policy;1 {f55f77f9-d33d-4759-82fc-60db3ee0bb91}
+category content-policy ImageBlockingPolicy @mozilla.org/browser/blockimages-policy;1
+
+# XPIDialogService.js
+component {c1242012-27d8-477e-a0f1-0b098ffc329b} XPIDialogService.js
+contract @mozilla.org/addons/web-install-prompt;1 {c1242012-27d8-477e-a0f1-0b098ffc329b}
+
+# HelperAppDialog.js
+component {e9d277a0-268a-4ec2-bb8c-10fdf3e44611} HelperAppDialog.js
+contract @mozilla.org/helperapplauncherdialog;1 {e9d277a0-268a-4ec2-bb8c-10fdf3e44611}
+
+# BrowserCLH.js
+component {be623d20-d305-11de-8a39-0800200c9a66} BrowserCLH.js application={aa3c5121-dab2-40e2-81ca-7ea25febc110}
+contract @mozilla.org/browser/browser-clh;1 {be623d20-d305-11de-8a39-0800200c9a66}
+category app-startup BrowserCLH @mozilla.org/browser/browser-clh;1
+
+# ContentDispatchChooser.js
+component {5a072a22-1e66-4100-afc1-07aed8b62fc5} ContentDispatchChooser.js
+contract @mozilla.org/content-dispatch-chooser;1 {5a072a22-1e66-4100-afc1-07aed8b62fc5}
+
+# AddonUpdateService.js
+component {93c8824c-9b87-45ae-bc90-5b82a1e4d877} AddonUpdateService.js
+contract @mozilla.org/browser/addon-update-service;1 {93c8824c-9b87-45ae-bc90-5b82a1e4d877}
+category update-timer AddonUpdateService @mozilla.org/browser/addon-update-service;1,getService,auto-addon-background-update-timer,extensions.autoupdate.interval,86400
+
+# LoginManagerPrompter.js
+component {97d12931-abe2-11df-94e2-0800200c9a66} LoginManagerPrompter.js
+contract @mozilla.org/login-manager/prompter;1 {97d12931-abe2-11df-94e2-0800200c9a66}
+
+# BlocklistPrompt.js
+component {4e6ea350-b09a-11df-94e2-0800200c9a66} BlocklistPrompt.js
+contract @mozilla.org/addons/blocklist-prompt;1 {4e6ea350-b09a-11df-94e2-0800200c9a66}
+
+# NSSDialogService.js
+component {cbc08081-49b6-4561-9c18-a7707a50bda1} NSSDialogService.js
+contract @mozilla.org/nsCertificateDialogs;1 {cbc08081-49b6-4561-9c18-a7707a50bda1}
+contract @mozilla.org/nsClientAuthDialogs;1 {cbc08081-49b6-4561-9c18-a7707a50bda1}
+
+# SiteSpecificUserAgent.js
+component {d5234c9d-0ee2-4b3c-9da3-18be9e5cf7e6} SiteSpecificUserAgent.js
+contract @mozilla.org/dom/site-specific-user-agent;1 {d5234c9d-0ee2-4b3c-9da3-18be9e5cf7e6}
+
+# FilePicker.js
+component {18a4e042-7c7c-424b-a583-354e68553a7f} FilePicker.js
+contract @mozilla.org/filepicker;1 {18a4e042-7c7c-424b-a583-354e68553a7f}
+
+# FxAccountsPush.js
+component {d1bbb0fd-1d47-4134-9c12-d7b1be20b721} FxAccountsPush.js
+contract @mozilla.org/fxa-push;1 {d1bbb0fd-1d47-4134-9c12-d7b1be20b721}
+category android-push-service FxAccountsPush @mozilla.org/fxa-push;1
+
+#ifndef RELEASE_OR_BETA
+# TabSource.js
+component {5850c76e-b916-4218-b99a-31f004e0a7e7} TabSource.js
+contract @mozilla.org/tab-source-service;1 {5850c76e-b916-4218-b99a-31f004e0a7e7}
+#endif
+
+# Snippets.js
+component {a78d7e59-b558-4321-a3d6-dffe2f1e76dd} Snippets.js
+contract @mozilla.org/snippets;1 {a78d7e59-b558-4321-a3d6-dffe2f1e76dd}
+category browser-delayed-startup-finished Snippets @mozilla.org/snippets;1
+category update-timer Snippets @mozilla.org/snippets;1,getService,snippets-update-timer,browser.snippets.updateInterval,86400
+
+# ColorPicker.js
+component {430b987f-bb9f-46a3-99a5-241749220b29} ColorPicker.js
+contract @mozilla.org/colorpicker;1 {430b987f-bb9f-46a3-99a5-241749220b29}
+
+# PersistentNotificationHandler.js
+component {75390fe7-f8a3-423a-b3b1-258d7eabed40} PersistentNotificationHandler.js
+contract @mozilla.org/persistent-notification-handler;1 {75390fe7-f8a3-423a-b3b1-258d7eabed40}
+category persistent-notification-click PersistentNotificationHandler @mozilla.org/persistent-notification-handler;1
+category persistent-notification-close PersistentNotificationHandler @mozilla.org/persistent-notification-handler;1
diff --git a/mobile/android/components/NSSDialogService.js b/mobile/android/components/NSSDialogService.js
new file mode 100644
index 0000000000..671cc8c351
--- /dev/null
+++ b/mobile/android/components/NSSDialogService.js
@@ -0,0 +1,276 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cc = Components.classes;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
+ "resource://gre/modules/Prompt.jsm");
+
+// -----------------------------------------------------------------------
+// NSS Dialog Service
+// -----------------------------------------------------------------------
+
+function NSSDialogs() { }
+
+NSSDialogs.prototype = {
+ classID: Components.ID("{cbc08081-49b6-4561-9c18-a7707a50bda1}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICertificateDialogs, Ci.nsIClientAuthDialogs]),
+
+ /**
+ * Escapes the given input via HTML entity encoding. Used to prevent HTML
+ * injection when the input is to be placed inside an HTML body, but not in
+ * any other context.
+ *
+ * @param {String} input The input to interpret as a plain string.
+ * @returns {String} The escaped input.
+ */
+ escapeHTML: function(input) {
+ return input.replace(/&/g, "&amp;")
+ .replace(/</g, "&lt;")
+ .replace(/>/g, "&gt;")
+ .replace(/"/g, "&quot;")
+ .replace(/'/g, "&#x27;")
+ .replace(/\//g, "&#x2F;");
+ },
+
+ getString: function(aName) {
+ if (!this.bundle) {
+ this.bundle = Services.strings.createBundle("chrome://browser/locale/pippki.properties");
+ }
+ return this.bundle.GetStringFromName(aName);
+ },
+
+ formatString: function(aName, argList) {
+ if (!this.bundle) {
+ this.bundle =
+ Services.strings.createBundle("chrome://browser/locale/pippki.properties");
+ }
+ let escapedArgList = Array.from(argList, x => this.escapeHTML(x));
+ return this.bundle.formatStringFromName(aName, escapedArgList,
+ escapedArgList.length);
+ },
+
+ getPrompt: function(aTitle, aText, aButtons) {
+ return new Prompt({
+ title: aTitle,
+ text: aText,
+ buttons: aButtons,
+ });
+ },
+
+ showPrompt: function(aPrompt) {
+ let response = null;
+ aPrompt.show(function(data) {
+ response = data;
+ });
+
+ // Spin this thread while we wait for a result
+ let thread = Services.tm.currentThread;
+ while (response === null)
+ thread.processNextEvent(true);
+
+ return response;
+ },
+
+ confirmDownloadCACert: function(aCtx, aCert, aTrust) {
+ while (true) {
+ let prompt = this.getPrompt(this.getString("downloadCert.title"),
+ this.getString("downloadCert.message1"),
+ [ this.getString("nssdialogs.ok.label"),
+ this.getString("downloadCert.viewCert.label"),
+ this.getString("nssdialogs.cancel.label")
+ ]);
+
+ prompt.addCheckbox({ id: "trustSSL", label: this.getString("downloadCert.trustSSL"), checked: false })
+ .addCheckbox({ id: "trustEmail", label: this.getString("downloadCert.trustEmail"), checked: false })
+ .addCheckbox({ id: "trustSign", label: this.getString("downloadCert.trustObjSign"), checked: false });
+ let response = this.showPrompt(prompt);
+
+ // they hit the "view cert" button, so show the cert and try again
+ if (response.button == 1) {
+ this.viewCert(aCtx, aCert);
+ continue;
+ } else if (response.button != 0) {
+ return false;
+ }
+
+ aTrust.value = Ci.nsIX509CertDB.UNTRUSTED;
+ if (response.trustSSL) aTrust.value |= Ci.nsIX509CertDB.TRUSTED_SSL;
+ if (response.trustEmail) aTrust.value |= Ci.nsIX509CertDB.TRUSTED_EMAIL;
+ if (response.trustSign) aTrust.value |= Ci.nsIX509CertDB.TRUSTED_OBJSIGN;
+ return true;
+ }
+ },
+
+ setPKCS12FilePassword: function(aCtx, aPassword) {
+ // this dialog is never shown in Fennec; in Desktop it is shown while backing up a personal
+ // certificate to a file via Preferences->Advanced->Encryption->View Certificates->Your Certificates
+ throw "Unimplemented";
+ },
+
+ getPKCS12FilePassword: function(aCtx, aPassword) {
+ let prompt = this.getPrompt(this.getString("pkcs12.getpassword.title"),
+ this.getString("pkcs12.getpassword.message"),
+ [ this.getString("nssdialogs.ok.label"),
+ this.getString("nssdialogs.cancel.label")
+ ]).addPassword({id: "pw"});
+ let response = this.showPrompt(prompt);
+ if (response.button != 0) {
+ return false;
+ }
+
+ aPassword.value = response.pw;
+ return true;
+ },
+
+ certInfoSection: function(aHeading, aDataPairs, aTrailingNewline = true) {
+ let certInfoStrings = [
+ "<big>" + this.getString(aHeading) + "</big>",
+ ];
+
+ for (let i = 0; i < aDataPairs.length; i += 2) {
+ let key = aDataPairs[i];
+ let value = aDataPairs[i + 1];
+ certInfoStrings.push(this.formatString(key, [value]));
+ }
+
+ if (aTrailingNewline) {
+ certInfoStrings.push("<br/>");
+ }
+
+ return certInfoStrings.join("<br/>");
+ },
+
+ viewCert: function(aCtx, aCert) {
+ let p = this.getPrompt(this.getString("certmgr.title"), "", [
+ this.getString("nssdialogs.ok.label"),
+ ]);
+ p.addLabel({ label: this.certInfoSection("certmgr.subjectinfo.label",
+ ["certdetail.cn", aCert.commonName,
+ "certdetail.o", aCert.organization,
+ "certdetail.ou", aCert.organizationalUnit,
+ "certdetail.serialnumber", aCert.serialNumber])})
+ .addLabel({ label: this.certInfoSection("certmgr.issuerinfo.label",
+ ["certdetail.cn", aCert.issuerCommonName,
+ "certdetail.o", aCert.issuerOrganization,
+ "certdetail.ou", aCert.issuerOrganizationUnit])})
+ .addLabel({ label: this.certInfoSection("certmgr.periodofvalidity.label",
+ ["certdetail.notBefore", aCert.validity.notBeforeLocalDay,
+ "certdetail.notAfter", aCert.validity.notAfterLocalDay])})
+ .addLabel({ label: this.certInfoSection("certmgr.fingerprints.label",
+ ["certdetail.sha256fingerprint", aCert.sha256Fingerprint,
+ "certdetail.sha1fingerprint", aCert.sha1Fingerprint],
+ false) });
+ this.showPrompt(p);
+ },
+
+ /**
+ * Returns a list of details of the given cert relevant for TLS client
+ * authentication.
+ *
+ * @param {nsIX509Cert} cert Cert to get the details of.
+ * @returns {String} <br/> delimited list of details.
+ */
+ getCertDetails: function(cert) {
+ let detailLines = [
+ this.formatString("clientAuthAsk.issuedTo", [cert.subjectName]),
+ this.formatString("clientAuthAsk.serial", [cert.serialNumber]),
+ this.formatString("clientAuthAsk.validityPeriod",
+ [cert.validity.notBeforeLocalTime,
+ cert.validity.notAfterLocalTime]),
+ ];
+ let keyUsages = cert.keyUsages;
+ if (keyUsages) {
+ detailLines.push(this.formatString("clientAuthAsk.keyUsages",
+ [keyUsages]));
+ }
+ let emailAddresses = cert.getEmailAddresses({});
+ if (emailAddresses.length > 0) {
+ let joinedAddresses = emailAddresses.join(", ");
+ detailLines.push(this.formatString("clientAuthAsk.emailAddresses",
+ [joinedAddresses]));
+ }
+ detailLines.push(this.formatString("clientAuthAsk.issuedBy",
+ [cert.issuerName]));
+ detailLines.push(this.formatString("clientAuthAsk.storedOn",
+ [cert.tokenName]));
+
+ return detailLines.join("<br/>");
+ },
+
+ viewCertDetails: function(details) {
+ let p = this.getPrompt(this.getString("clientAuthAsk.message3"),
+ '',
+ [ this.getString("nssdialogs.ok.label") ]);
+ p.addLabel({ label: details });
+ this.showPrompt(p);
+ },
+
+ chooseCertificate: function(ctx, hostname, port, organization, issuerOrg,
+ certList, selectedIndex) {
+ let rememberSetting =
+ Services.prefs.getBoolPref("security.remember_cert_checkbox_default_setting");
+
+ let serverRequestedDetails = [
+ this.formatString("clientAuthAsk.hostnameAndPort",
+ [hostname, port.toString()]),
+ this.formatString("clientAuthAsk.organization", [organization]),
+ this.formatString("clientAuthAsk.issuer", [issuerOrg]),
+ ].join("<br/>");
+
+ let certNickList = [];
+ let certDetailsList = [];
+ for (let i = 0; i < certList.length; i++) {
+ let cert = certList.queryElementAt(i, Ci.nsIX509Cert);
+ certNickList.push(this.formatString("clientAuthAsk.nickAndSerial",
+ [cert.nickname, cert.serialNumber]));
+ certDetailsList.push(this.getCertDetails(cert));
+ }
+
+ selectedIndex.value = 0;
+ while (true) {
+ let buttons = [
+ this.getString("nssdialogs.ok.label"),
+ this.getString("clientAuthAsk.viewCert.label"),
+ this.getString("nssdialogs.cancel.label"),
+ ];
+ let prompt = this.getPrompt(this.getString("clientAuthAsk.title"),
+ this.getString("clientAuthAsk.message1"),
+ buttons)
+ .addLabel({ id: "requestedDetails", label: serverRequestedDetails } )
+ .addMenulist({
+ id: "nicknames",
+ label: this.getString("clientAuthAsk.message2"),
+ values: certNickList,
+ selected: selectedIndex.value,
+ }).addCheckbox({
+ id: "rememberBox",
+ label: this.getString("clientAuthAsk.remember.label"),
+ checked: rememberSetting
+ });
+ let response = this.showPrompt(prompt);
+ selectedIndex.value = response.nicknames;
+ if (response.button == 1 /* buttons[1] */) {
+ this.viewCertDetails(certDetailsList[selectedIndex.value]);
+ continue;
+ } else if (response.button == 0 /* buttons[0] */) {
+ if (response.rememberBox == true) {
+ let caud = ctx.QueryInterface(Ci.nsIClientAuthUserDecision);
+ if (caud) {
+ caud.rememberClientAuthCertificate = true;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([NSSDialogs]);
diff --git a/mobile/android/components/PersistentNotificationHandler.js b/mobile/android/components/PersistentNotificationHandler.js
new file mode 100644
index 0000000000..2a3529f5f6
--- /dev/null
+++ b/mobile/android/components/PersistentNotificationHandler.js
@@ -0,0 +1,78 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, 'Services', // jshint ignore:line
+ 'resource://gre/modules/Services.jsm');
+XPCOMUtils.defineLazyServiceGetter(this, "notificationStorage",
+ "@mozilla.org/notificationStorage;1",
+ "nsINotificationStorage");
+XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager",
+ "@mozilla.org/serviceworkers/manager;1",
+ "nsIServiceWorkerManager");
+
+function PersistentNotificationHandler() {
+}
+
+PersistentNotificationHandler.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+ classID: Components.ID("{75390fe7-f8a3-423a-b3b1-258d7eabed40}"),
+
+ observe(subject, topic, data) {
+ if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ Cu.import("resource://gre/modules/NotificationDB.jsm");
+ }
+ const persistentInfo = JSON.parse(data);
+
+ if (topic === 'persistent-notification-click') {
+ notificationStorage.getByID(persistentInfo.origin, persistentInfo.id, {
+ handle(id, title, dir, lang, body, tag, icon, data, behavior, serviceWorkerRegistrationScope) {
+ serviceWorkerManager.sendNotificationClickEvent(
+ persistentInfo.originSuffix,
+ serviceWorkerRegistrationScope,
+ id,
+ title,
+ dir,
+ lang,
+ body,
+ tag,
+ icon,
+ data,
+ behavior
+ );
+ notificationStorage.delete(persistentInfo.origin, persistentInfo.id);
+ }
+ });
+ } else if (topic === 'persistent-notification-close') {
+ notificationStorage.getByID(persistentInfo.origin, persistentInfo.id, {
+ handle(id, title, dir, lang, body, tag, icon, data, behavior, serviceWorkerRegistrationScope) {
+ serviceWorkerManager.sendNotificationCloseEvent(
+ persistentInfo.originSuffix,
+ serviceWorkerRegistrationScope,
+ id,
+ title,
+ dir,
+ lang,
+ body,
+ tag,
+ icon,
+ data,
+ behavior
+ );
+ notificationStorage.delete(persistentInfo.origin, persistentInfo.id);
+ }
+ });
+ }
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([
+ PersistentNotificationHandler
+]);
diff --git a/mobile/android/components/PresentationDevicePrompt.js b/mobile/android/components/PresentationDevicePrompt.js
new file mode 100644
index 0000000000..e3e0633736
--- /dev/null
+++ b/mobile/android/components/PresentationDevicePrompt.js
@@ -0,0 +1,134 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+
+XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
+ "resource://gre/modules/Prompt.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
+ "resource://gre/modules/UITelemetry.jsm");
+
+const kPRESENTATIONDEVICEPROMPT_CONTRACTID = "@mozilla.org/presentation-device/prompt;1";
+const kPRESENTATIONDEVICEPROMPT_CID = Components.ID("{388bd149-c919-4a43-b646-d7ec57877689}");
+
+function debug(aMsg) {
+ // dump("-*- PresentationDevicePrompt: " + aMsg + "\n");
+}
+
+// nsIPresentationDevicePrompt
+function PresentationDevicePrompt() {
+ debug("PresentationDevicePrompt init");
+}
+
+PresentationDevicePrompt.prototype = {
+ classID: kPRESENTATIONDEVICEPROMPT_CID,
+ contractID: kPRESENTATIONDEVICEPROMPT_CONTRACTID,
+ classDescription: "Fennec Presentation Device Prompt",
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevicePrompt]),
+
+ _devices: [], // Store all available presentation devices
+ _request: null, // Store the request from presentation api
+
+ _getString: function(aName) {
+ debug("_getString");
+
+ if (!this.bundle) {
+ this.bundle = Services.strings.createBundle("chrome://browser/locale/devicePrompt.properties");
+ }
+ return this.bundle.GetStringFromName(aName);
+ },
+
+ _loadDevices: function(requestURLs) {
+ debug("_loadDevices");
+
+ let deviceManager = Cc["@mozilla.org/presentation-device/manager;1"]
+ .getService(Ci.nsIPresentationDeviceManager);
+ let devices = deviceManager.getAvailableDevices(requestURLs).QueryInterface(Ci.nsIArray);
+
+ // Re-load the available devices
+ this._devices = [];
+ for (let i = 0; i < devices.length; i++) {
+ let device = devices.queryElementAt(i, Ci.nsIPresentationDevice);
+ this._devices.push(device);
+ }
+ },
+
+ _getPromptMenu: function(aDevices) {
+ debug("_getPromptMenu");
+
+ return aDevices.map(function(device) {
+ return { label: device.name };
+ });
+ },
+
+ _getPrompt: function(aTitle, aMenu) {
+ debug("_getPrompt");
+
+ let p = new Prompt({
+ title: aTitle,
+ });
+
+ p.setSingleChoiceItems(aMenu);
+
+ return p;
+ },
+
+ _showPrompt: function(aPrompt, aCallback) {
+ debug("_showPrompt");
+
+ aPrompt.show(function(data) {
+ let buttonIndex = data.button;
+ aCallback(buttonIndex);
+ });
+ },
+
+ _selectDevice: function(aIndex) {
+ debug("_selectDevice");
+
+ if (!this._request) {
+ return;
+ }
+
+ if (aIndex < 0) { // Cancel request if no selected device,
+ this._request.cancel(Cr.NS_ERROR_DOM_NOT_ALLOWED_ERR);
+ return;
+ } else if (!this._devices.length) { // or there is no available devices
+ this._request.cancel(Cr.NS_ERROR_DOM_NOT_FOUND_ERR);
+ return;
+ }
+
+ this._request.select(this._devices[aIndex]);
+ },
+
+ // This will be fired when window.PresentationRequest(URL).start() is called
+ promptDeviceSelection: function(aRequest) {
+ debug("promptDeviceSelection");
+
+ // Load available presentation devices into this._devices
+ this._loadDevices(aRequest.requestURLs);
+
+ if (!this._devices.length) { // Cancel request if no available device
+ aRequest.cancel(Cr.NS_ERROR_DOM_NOT_FOUND_ERR);
+ return;
+ }
+
+ this._request = aRequest;
+
+ let prompt = this._getPrompt(this._getString("deviceMenu.title"),
+ this._getPromptMenu(this._devices));
+
+ this._showPrompt(prompt, this._selectDevice.bind(this));
+
+ UITelemetry.addEvent("show.1", "dialog", null, "prompt_device_selection");
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PresentationDevicePrompt]);
diff --git a/mobile/android/components/PresentationRequestUIGlue.js b/mobile/android/components/PresentationRequestUIGlue.js
new file mode 100644
index 0000000000..af252c8759
--- /dev/null
+++ b/mobile/android/components/PresentationRequestUIGlue.js
@@ -0,0 +1,86 @@
+/* -*- Mode: tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict"
+
+const { interfaces: Ci, utils: Cu, classes: Cc } = Components;
+
+const TOPIC_PRESENTATION_RECEIVER_LAUNCH = "presentation-receiver:launch";
+const TOPIC_PRESENTATION_RECEIVER_LAUNCH_RESPONSE = "presentation-receiver:launch:response";
+
+// globals XPCOMUtils
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+// globals Services
+Cu.import("resource://gre/modules/Services.jsm");
+
+function log(str) {
+ // dump("-*- PresentationRequestUIGlue.js -*-: " + str + "\n");
+}
+
+function PresentationRequestUIGlue() { }
+
+PresentationRequestUIGlue.prototype = {
+ sendRequest: function sendRequest(aURL, aSessionId, aDevice) {
+ log("PresentationRequestUIGlue - sendRequest aURL=" + aURL +
+ " aSessionId=" + aSessionId);
+
+ let localDevice;
+ try {
+ localDevice = aDevice.QueryInterface(Ci.nsIPresentationLocalDevice);
+ } catch (e) {
+ /* XXX: Currently, Fennec only support 1-UA devices. Remove this
+ * Promise.reject() when it starts to support 2-UA devices.
+ */
+ log("Not an 1-UA device.")
+ return new Promise.reject();
+ }
+
+ return new Promise((aResolve, aReject) => {
+
+ let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"]
+ .getService(Ci.nsIUUIDGenerator);
+ let requestId = uuidGenerator.generateUUID().toString();
+
+ let handleObserve = (aSubject, aTopic, aData) => {
+ log("Got observe: aTopic=" + aTopic);
+
+ let data = JSON.parse(aData);
+ if (data.requestId != requestId) {
+ return;
+ }
+
+ Services.obs.removeObserver(handleObserve,
+ TOPIC_PRESENTATION_RECEIVER_LAUNCH_RESPONSE);
+ switch(data.result) {
+ case "success":
+ aResolve(aSubject);
+ break;
+ case "error":
+ aReject();
+ break;
+ };
+ };
+
+ Services.obs.addObserver(handleObserve,
+ TOPIC_PRESENTATION_RECEIVER_LAUNCH_RESPONSE,
+ false);
+
+ let data = {
+ url: aURL,
+ windowId: localDevice.windowId,
+ requestId: requestId
+ };
+ Services.obs.notifyObservers(null,
+ TOPIC_PRESENTATION_RECEIVER_LAUNCH,
+ JSON.stringify(data));
+ })
+ },
+
+ classID: Components.ID("9c550ef7-3ff6-4bd1-9ad1-5a3735b90d21"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationRequestUIGlue])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PresentationRequestUIGlue]);
diff --git a/mobile/android/components/PromptService.js b/mobile/android/components/PromptService.js
new file mode 100644
index 0000000000..93aff67eec
--- /dev/null
+++ b/mobile/android/components/PromptService.js
@@ -0,0 +1,878 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
+ "resource://gre/modules/Prompt.jsm");
+
+var gPromptService = null;
+
+function PromptService() {
+ gPromptService = this;
+}
+
+PromptService.prototype = {
+ classID: Components.ID("{9a61149b-2276-4a0a-b79c-be994ad106cf}"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPromptFactory, Ci.nsIPromptService, Ci.nsIPromptService2]),
+
+ /* ---------- nsIPromptFactory ---------- */
+ // XXX Copied from nsPrompter.js.
+ getPrompt: function getPrompt(domWin, iid) {
+ // This is still kind of dumb; the C++ code delegated to login manager
+ // here, which in turn calls back into us via nsIPromptService2.
+ if (iid.equals(Ci.nsIAuthPrompt2) || iid.equals(Ci.nsIAuthPrompt)) {
+ try {
+ let pwmgr = Cc["@mozilla.org/passwordmanager/authpromptfactory;1"].getService(Ci.nsIPromptFactory);
+ return pwmgr.getPrompt(domWin, iid);
+ } catch (e) {
+ Cu.reportError("nsPrompter: Delegation to password manager failed: " + e);
+ }
+ }
+
+ let p = new InternalPrompt(domWin);
+ p.QueryInterface(iid);
+ return p;
+ },
+
+ /* ---------- private memebers ---------- */
+
+ // nsIPromptService and nsIPromptService2 methods proxy to our Prompt class
+ callProxy: function(aMethod, aArguments) {
+ let prompt;
+ let domWin = aArguments[0];
+ prompt = new InternalPrompt(domWin);
+ return prompt[aMethod].apply(prompt, Array.prototype.slice.call(aArguments, 1));
+ },
+
+ /* ---------- nsIPromptService ---------- */
+
+ alert: function() {
+ return this.callProxy("alert", arguments);
+ },
+ alertCheck: function() {
+ return this.callProxy("alertCheck", arguments);
+ },
+ confirm: function() {
+ return this.callProxy("confirm", arguments);
+ },
+ confirmCheck: function() {
+ return this.callProxy("confirmCheck", arguments);
+ },
+ confirmEx: function() {
+ return this.callProxy("confirmEx", arguments);
+ },
+ prompt: function() {
+ return this.callProxy("prompt", arguments);
+ },
+ promptUsernameAndPassword: function() {
+ return this.callProxy("promptUsernameAndPassword", arguments);
+ },
+ promptPassword: function() {
+ return this.callProxy("promptPassword", arguments);
+ },
+ select: function() {
+ return this.callProxy("select", arguments);
+ },
+
+ /* ---------- nsIPromptService2 ---------- */
+ promptAuth: function() {
+ return this.callProxy("promptAuth", arguments);
+ },
+ asyncPromptAuth: function() {
+ return this.callProxy("asyncPromptAuth", arguments);
+ }
+};
+
+function InternalPrompt(aDomWin) {
+ this._domWin = aDomWin;
+}
+
+InternalPrompt.prototype = {
+ _domWin: null,
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt, Ci.nsIAuthPrompt, Ci.nsIAuthPrompt2]),
+
+ /* ---------- internal methods ---------- */
+ _getPrompt: function _getPrompt(aTitle, aText, aButtons, aCheckMsg, aCheckState) {
+ let p = new Prompt({
+ window: this._domWin,
+ title: aTitle,
+ message: aText,
+ buttons: aButtons || [
+ PromptUtils.getLocaleString("OK"),
+ PromptUtils.getLocaleString("Cancel")
+ ]
+ });
+ return p;
+ },
+
+ addCheckbox: function addCheckbox(aPrompt, aCheckMsg, aCheckState) {
+ // Don't bother to check for aCheckSate. For nsIPomptService interfaces, aCheckState is an
+ // out param and is required to be defined. If we've gotten here without it, something
+ // has probably gone wrong and we should fail
+ if (aCheckMsg) {
+ aPrompt.addCheckbox({
+ label: PromptUtils.cleanUpLabel(aCheckMsg),
+ checked: aCheckState.value
+ });
+ }
+
+ return aPrompt;
+ },
+
+ addTextbox: function(prompt, value, autofocus, hint) {
+ prompt.addTextbox({
+ value: (value !== null) ? value : "",
+ autofocus: autofocus,
+ hint: hint
+ });
+ },
+
+ addPassword: function(prompt, value, autofocus, hint) {
+ prompt.addPassword({
+ value: (value !== null) ? value : "",
+ autofocus: autofocus,
+ hint: hint
+ });
+ },
+
+ /* Shows a native prompt, and then spins the event loop for this thread while we wait
+ * for a response
+ */
+ showPrompt: function showPrompt(aPrompt) {
+ if (this._domWin) {
+ PromptUtils.fireDialogEvent(this._domWin, "DOMWillOpenModalDialog");
+ let winUtils = this._domWin.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ winUtils.enterModalState();
+ }
+
+ let retval = null;
+ aPrompt.show(function(data) {
+ retval = data;
+ });
+
+ // Spin this thread while we wait for a result
+ let thread = Services.tm.currentThread;
+ while (retval == null)
+ thread.processNextEvent(true);
+
+ if (this._domWin) {
+ let winUtils = this._domWin.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ winUtils.leaveModalState();
+ PromptUtils.fireDialogEvent(this._domWin, "DOMModalDialogClosed");
+ }
+
+ return retval;
+ },
+
+ /*
+ * ---------- interface disambiguation ----------
+ *
+ * XXX Copied from nsPrompter.js.
+ *
+ * nsIPrompt and nsIAuthPrompt share 3 method names with slightly
+ * different arguments. All but prompt() have the same number of
+ * arguments, so look at the arg types to figure out how we're being
+ * called. :-(
+ */
+ prompt: function prompt() {
+ if (gPromptService.inContentProcess)
+ return gPromptService.callProxy("prompt", [null].concat(Array.prototype.slice.call(arguments)));
+
+ // also, the nsIPrompt flavor has 5 args instead of 6.
+ if (typeof arguments[2] == "object")
+ return this.nsIPrompt_prompt.apply(this, arguments);
+ else
+ return this.nsIAuthPrompt_prompt.apply(this, arguments);
+ },
+
+ promptUsernameAndPassword: function promptUsernameAndPassword() {
+ // Both have 6 args, so use types.
+ if (typeof arguments[2] == "object")
+ return this.nsIPrompt_promptUsernameAndPassword.apply(this, arguments);
+ else
+ return this.nsIAuthPrompt_promptUsernameAndPassword.apply(this, arguments);
+ },
+
+ promptPassword: function promptPassword() {
+ // Both have 5 args, so use types.
+ if (typeof arguments[2] == "object")
+ return this.nsIPrompt_promptPassword.apply(this, arguments);
+ else
+ return this.nsIAuthPrompt_promptPassword.apply(this, arguments);
+ },
+
+ /* ---------- nsIPrompt ---------- */
+
+ alert: function alert(aTitle, aText) {
+ let p = this._getPrompt(aTitle, aText, [ PromptUtils.getLocaleString("OK") ]);
+ p.setHint("alert");
+ this.showPrompt(p);
+ },
+
+ alertCheck: function alertCheck(aTitle, aText, aCheckMsg, aCheckState) {
+ let p = this._getPrompt(aTitle, aText, [ PromptUtils.getLocaleString("OK") ]);
+ this.addCheckbox(p, aCheckMsg, aCheckState);
+ let data = this.showPrompt(p);
+ if (aCheckState && data.button > -1)
+ aCheckState.value = data.checkbox0;
+ },
+
+ confirm: function confirm(aTitle, aText) {
+ let p = this._getPrompt(aTitle, aText);
+ p.setHint("confirm");
+ let data = this.showPrompt(p);
+ return (data.button == 0);
+ },
+
+ confirmCheck: function confirmCheck(aTitle, aText, aCheckMsg, aCheckState) {
+ let p = this._getPrompt(aTitle, aText, null);
+ this.addCheckbox(p, aCheckMsg, aCheckState);
+ let data = this.showPrompt(p);
+ let ok = data.button == 0;
+ if (aCheckState && data.button > -1)
+ aCheckState.value = data.checkbox0;
+ return ok;
+ },
+
+ confirmEx: function confirmEx(aTitle, aText, aButtonFlags, aButton0,
+ aButton1, aButton2, aCheckMsg, aCheckState) {
+ let buttons = [];
+ let titles = [aButton0, aButton1, aButton2];
+ for (let i = 0; i < 3; i++) {
+ let bTitle = null;
+ switch (aButtonFlags & 0xff) {
+ case Ci.nsIPromptService.BUTTON_TITLE_OK :
+ bTitle = PromptUtils.getLocaleString("OK");
+ break;
+ case Ci.nsIPromptService.BUTTON_TITLE_CANCEL :
+ bTitle = PromptUtils.getLocaleString("Cancel");
+ break;
+ case Ci.nsIPromptService.BUTTON_TITLE_YES :
+ bTitle = PromptUtils.getLocaleString("Yes");
+ break;
+ case Ci.nsIPromptService.BUTTON_TITLE_NO :
+ bTitle = PromptUtils.getLocaleString("No");
+ break;
+ case Ci.nsIPromptService.BUTTON_TITLE_SAVE :
+ bTitle = PromptUtils.getLocaleString("Save");
+ break;
+ case Ci.nsIPromptService.BUTTON_TITLE_DONT_SAVE :
+ bTitle = PromptUtils.getLocaleString("DontSave");
+ break;
+ case Ci.nsIPromptService.BUTTON_TITLE_REVERT :
+ bTitle = PromptUtils.getLocaleString("Revert");
+ break;
+ case Ci.nsIPromptService.BUTTON_TITLE_IS_STRING :
+ bTitle = PromptUtils.cleanUpLabel(titles[i]);
+ break;
+ }
+
+ if (bTitle)
+ buttons.push(bTitle);
+
+ aButtonFlags >>= 8;
+ }
+
+ let p = this._getPrompt(aTitle, aText, buttons);
+ this.addCheckbox(p, aCheckMsg, aCheckState);
+ let data = this.showPrompt(p);
+ if (aCheckState && data.button > -1)
+ aCheckState.value = data.checkbox0;
+ return data.button;
+ },
+
+ nsIPrompt_prompt: function nsIPrompt_prompt(aTitle, aText, aValue, aCheckMsg, aCheckState) {
+ let p = this._getPrompt(aTitle, aText, null, aCheckMsg, aCheckState);
+ p.setHint("prompt");
+ this.addTextbox(p, aValue.value, true);
+ this.addCheckbox(p, aCheckMsg, aCheckState);
+ let data = this.showPrompt(p);
+
+ let ok = data.button == 0;
+ if (aCheckState && data.button > -1)
+ aCheckState.value = data.checkbox0;
+ if (ok)
+ aValue.value = data.textbox0;
+ return ok;
+ },
+
+ nsIPrompt_promptPassword: function nsIPrompt_promptPassword(
+ aTitle, aText, aPassword, aCheckMsg, aCheckState) {
+ let p = this._getPrompt(aTitle, aText, null);
+ this.addPassword(p, aPassword.value, true, PromptUtils.getLocaleString("password", "passwdmgr"));
+ this.addCheckbox(p, aCheckMsg, aCheckState);
+ let data = this.showPrompt(p);
+
+ let ok = data.button == 0;
+ if (aCheckState && data.button > -1)
+ aCheckState.value = data.checkbox0;
+ if (ok)
+ aPassword.value = data.password0;
+ return ok;
+ },
+
+ nsIPrompt_promptUsernameAndPassword: function nsIPrompt_promptUsernameAndPassword(
+ aTitle, aText, aUsername, aPassword, aCheckMsg, aCheckState) {
+ let p = this._getPrompt(aTitle, aText, null);
+ this.addTextbox(p, aUsername.value, true, PromptUtils.getLocaleString("username", "passwdmgr"));
+ this.addPassword(p, aPassword.value, false, PromptUtils.getLocaleString("password", "passwdmgr"));
+ this.addCheckbox(p, aCheckMsg, aCheckState);
+ let data = this.showPrompt(p);
+
+ let ok = data.button == 0;
+ if (aCheckState && data.button > -1)
+ aCheckState.value = data.checkbox0;
+
+ if (ok) {
+ aUsername.value = data.textbox0;
+ aPassword.value = data.password0;
+ }
+ return ok;
+ },
+
+ select: function select(aTitle, aText, aCount, aSelectList, aOutSelection) {
+ let p = this._getPrompt(aTitle, aText, [ PromptUtils.getLocaleString("OK") ]);
+ p.addMenulist({ values: aSelectList });
+ let data = this.showPrompt(p);
+
+ let ok = data.button == 0;
+ if (ok)
+ aOutSelection.value = data.menulist0;
+
+ return ok;
+ },
+
+ /* ---------- nsIAuthPrompt ---------- */
+
+ nsIAuthPrompt_prompt : function (title, text, passwordRealm, savePassword, defaultText, result) {
+ // TODO: Port functions from nsLoginManagerPrompter.js to here
+ if (defaultText)
+ result.value = defaultText;
+ return this.nsIPrompt_prompt(title, text, result, null, {});
+ },
+
+ nsIAuthPrompt_promptUsernameAndPassword : function(aTitle, aText, aPasswordRealm, aSavePassword, aUser, aPass) {
+ return this.nsIAuthPrompt_loginPrompt(aTitle, aText, aPasswordRealm, aSavePassword, aUser, aPass);
+ },
+
+ nsIAuthPrompt_promptPassword : function(aTitle, aText, aPasswordRealm, aSavePassword, aPass) {
+ return this.nsIAuthPrompt_loginPrompt(aTitle, aText, aPasswordRealm, aSavePassword, null, aPass);
+ },
+
+ nsIAuthPrompt_loginPrompt: function(aTitle, aPasswordRealm, aSavePassword, aUser, aPass) {
+ let checkMsg = null;
+ let check = { value: false };
+ let hostname, realm;
+ [hostname, realm, aUser] = PromptUtils.getHostnameAndRealm(aPasswordRealm);
+
+ let canSave = PromptUtils.canSaveLogin(hostname, aSavePassword);
+ if (canSave) {
+ // Look for existing logins.
+ let foundLogins = PromptUtils.pwmgr.findLogins({}, hostname, null, realm);
+ [checkMsg, check] = PromptUtils.getUsernameAndPassword(foundLogins, aUser, aPass);
+ }
+
+ // (eslint-disable: see bug 1177904)
+ let ok = false;
+ if (aUser)
+ ok = this.nsIPrompt_promptUsernameAndPassword(aTitle, aText, aUser, aPass, checkMsg, check); // eslint-disable-line no-undef
+ else
+ ok = this.nsIPrompt_promptPassword(aTitle, aText, aPass, checkMsg, check); // eslint-disable-line no-undef
+
+ if (ok && canSave && check.value)
+ PromptUtils.savePassword(hostname, realm, aUser, aPass);
+
+ return ok;
+ },
+
+ /* ---------- nsIAuthPrompt2 ---------- */
+
+ promptAuth: function promptAuth(aChannel, aLevel, aAuthInfo) {
+ let checkMsg = null;
+ let check = { value: false };
+ let message = PromptUtils.makeDialogText(aChannel, aAuthInfo);
+ let [username, password] = PromptUtils.getAuthInfo(aAuthInfo);
+ let [hostname, httpRealm] = PromptUtils.getAuthTarget(aChannel, aAuthInfo);
+ let foundLogins = PromptUtils.pwmgr.findLogins({}, hostname, null, httpRealm);
+
+ let canSave = PromptUtils.canSaveLogin(hostname, null);
+ if (canSave)
+ [checkMsg, check] = PromptUtils.getUsernameAndPassword(foundLogins, username, password);
+
+ if (username.value && password.value) {
+ PromptUtils.setAuthInfo(aAuthInfo, username.value, password.value);
+ }
+
+ let canAutologin = false;
+ if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY &&
+ !(aAuthInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) &&
+ Services.prefs.getBoolPref("signon.autologin.proxy"))
+ canAutologin = true;
+
+ let ok = canAutologin;
+ if (!ok && aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD)
+ ok = this.nsIPrompt_promptPassword(null, message, password, checkMsg, check);
+ else if (!ok)
+ ok = this.nsIPrompt_promptUsernameAndPassword(null, message, username, password, checkMsg, check);
+
+ PromptUtils.setAuthInfo(aAuthInfo, username.value, password.value);
+
+ if (ok && canSave && check.value)
+ PromptUtils.savePassword(foundLogins, username, password, hostname, httpRealm);
+
+ return ok;
+ },
+
+ _asyncPrompts: {},
+ _asyncPromptInProgress: false,
+
+ _doAsyncPrompt : function() {
+ if (this._asyncPromptInProgress)
+ return;
+
+ // Find the first prompt key we have in the queue
+ let hashKey = null;
+ for (hashKey in this._asyncPrompts)
+ break;
+
+ if (!hashKey)
+ return;
+
+ // If login manger has logins for this host, defer prompting if we're
+ // already waiting on a master password entry.
+ let prompt = this._asyncPrompts[hashKey];
+ let prompter = prompt.prompter;
+ let [hostname, httpRealm] = PromptUtils.getAuthTarget(prompt.channel, prompt.authInfo);
+ let foundLogins = PromptUtils.pwmgr.findLogins({}, hostname, null, httpRealm);
+ if (foundLogins.length > 0 && PromptUtils.pwmgr.uiBusy)
+ return;
+
+ this._asyncPromptInProgress = true;
+ prompt.inProgress = true;
+
+ let self = this;
+
+ let runnable = {
+ run: function() {
+ let ok = false;
+ try {
+ ok = prompter.promptAuth(prompt.channel, prompt.level, prompt.authInfo);
+ } catch (e) {
+ Cu.reportError("_doAsyncPrompt:run: " + e + "\n");
+ }
+
+ delete self._asyncPrompts[hashKey];
+ prompt.inProgress = false;
+ self._asyncPromptInProgress = false;
+
+ for (let consumer of prompt.consumers) {
+ if (!consumer.callback)
+ // Not having a callback means that consumer didn't provide it
+ // or canceled the notification
+ continue;
+
+ try {
+ if (ok)
+ consumer.callback.onAuthAvailable(consumer.context, prompt.authInfo);
+ else
+ consumer.callback.onAuthCancelled(consumer.context, true);
+ } catch (e) { /* Throw away exceptions caused by callback */ }
+ }
+ self._doAsyncPrompt();
+ }
+ }
+
+ Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL);
+ },
+
+ asyncPromptAuth: function asyncPromptAuth(aChannel, aCallback, aContext, aLevel, aAuthInfo) {
+ let cancelable = null;
+ try {
+ // If the user submits a login but it fails, we need to remove the
+ // notification bar that was displayed. Conveniently, the user will
+ // be prompted for authentication again, which brings us here.
+ //this._removeLoginNotifications();
+
+ cancelable = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+ callback: aCallback,
+ context: aContext,
+ cancel: function() {
+ this.callback.onAuthCancelled(this.context, false);
+ this.callback = null;
+ this.context = null;
+ }
+ };
+ let [hostname, httpRealm] = PromptUtils.getAuthTarget(aChannel, aAuthInfo);
+ let hashKey = aLevel + "|" + hostname + "|" + httpRealm;
+ let asyncPrompt = this._asyncPrompts[hashKey];
+ if (asyncPrompt) {
+ asyncPrompt.consumers.push(cancelable);
+ return cancelable;
+ }
+
+ asyncPrompt = {
+ consumers: [cancelable],
+ channel: aChannel,
+ authInfo: aAuthInfo,
+ level: aLevel,
+ inProgress : false,
+ prompter: this
+ }
+
+ this._asyncPrompts[hashKey] = asyncPrompt;
+ this._doAsyncPrompt();
+ } catch (e) {
+ Cu.reportError("PromptService: " + e + "\n");
+ throw e;
+ }
+ return cancelable;
+ }
+};
+
+var PromptUtils = {
+ getLocaleString: function pu_getLocaleString(aKey, aService) {
+ if (aService == "passwdmgr")
+ return this.cleanUpLabel(this.passwdBundle.GetStringFromName(aKey));
+
+ return this.cleanUpLabel(this.bundle.GetStringFromName(aKey));
+ },
+
+ //
+ // Copied from chrome://global/content/commonDialog.js
+ //
+ cleanUpLabel: function cleanUpLabel(aLabel) {
+ // This is for labels which may contain embedded access keys.
+ // If we end in (&X) where X represents the access key, optionally preceded
+ // by spaces and/or followed by the ':' character,
+ // remove the access key placeholder + leading spaces from the label.
+ // Otherwise a character preceded by one but not two &s is the access key.
+
+ // Note that if you change the following code, see the comment of
+ // nsTextBoxFrame::UpdateAccessTitle.
+ if (!aLabel)
+ return "";
+
+ if (/ *\(\&([^&])\)(:?)$/.test(aLabel)) {
+ aLabel = RegExp.leftContext + RegExp.$2;
+ } else if (/^([^&]*)\&(([^&]).*$)/.test(aLabel)) {
+ aLabel = RegExp.$1 + RegExp.$2;
+ }
+
+ // Special code for using that & symbol
+ aLabel = aLabel.replace(/\&\&/g, "&");
+
+ return aLabel;
+ },
+
+ get pwmgr() {
+ delete this.pwmgr;
+ return this.pwmgr = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
+ },
+
+ getHostnameAndRealm: function pu_getHostnameAndRealm(aRealmString) {
+ let httpRealm = /^.+ \(.+\)$/;
+ if (httpRealm.test(aRealmString))
+ return [null, null, null];
+
+ let uri = Services.io.newURI(aRealmString, null, null);
+ let pathname = "";
+
+ if (uri.path != "/")
+ pathname = uri.path;
+
+ let formattedHostname = this._getFormattedHostname(uri);
+ return [formattedHostname, formattedHostname + pathname, uri.username];
+ },
+
+ canSaveLogin: function pu_canSaveLogin(aHostname, aSavePassword) {
+ let canSave = !this._inPrivateBrowsing && this.pwmgr.getLoginSavingEnabled(aHostname)
+ if (aSavePassword)
+ canSave = canSave && (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY)
+ return canSave;
+ },
+
+ getUsernameAndPassword: function pu_getUsernameAndPassword(aFoundLogins, aUser, aPass) {
+ let checkLabel = null;
+ let check = { value: false };
+ let selectedLogin;
+
+ checkLabel = this.getLocaleString("rememberButton", "passwdmgr");
+
+ // XXX Like the original code, we can't deal with multiple
+ // account selection. (bug 227632)
+ if (aFoundLogins.length > 0) {
+ selectedLogin = aFoundLogins[0];
+
+ // If the caller provided a username, try to use it. If they
+ // provided only a password, this will try to find a password-only
+ // login (or return null if none exists).
+ if (aUser.value)
+ selectedLogin = this.findLogin(aFoundLogins, "username", aUser.value);
+
+ if (selectedLogin) {
+ check.value = true;
+ aUser.value = selectedLogin.username;
+ // If the caller provided a password, prefer it.
+ if (!aPass.value)
+ aPass.value = selectedLogin.password;
+ }
+ }
+
+ return [checkLabel, check];
+ },
+
+ findLogin: function pu_findLogin(aLogins, aName, aValue) {
+ for (let i = 0; i < aLogins.length; i++)
+ if (aLogins[i][aName] == aValue)
+ return aLogins[i];
+ return null;
+ },
+
+ savePassword: function pu_savePassword(aLogins, aUser, aPass, aHostname, aRealm) {
+ let selectedLogin = this.findLogin(aLogins, "username", aUser.value);
+
+ // If we didn't find an existing login, or if the username
+ // changed, save as a new login.
+ if (!selectedLogin) {
+ // add as new
+ var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
+ newLogin.init(aHostname, null, aRealm, aUser.value, aPass.value, "", "");
+ this.pwmgr.addLogin(newLogin);
+ } else if (aPass.value != selectedLogin.password) {
+ // update password
+ this.updateLogin(selectedLogin, aPass.value);
+ } else {
+ this.updateLogin(selectedLogin);
+ }
+ },
+
+ updateLogin: function pu_updateLogin(aLogin, aPassword) {
+ let now = Date.now();
+ let propBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag);
+ if (aPassword) {
+ propBag.setProperty("password", aPassword);
+ // Explicitly set the password change time here (even though it would
+ // be changed automatically), to ensure that it's exactly the same
+ // value as timeLastUsed.
+ propBag.setProperty("timePasswordChanged", now);
+ }
+ propBag.setProperty("timeLastUsed", now);
+ propBag.setProperty("timesUsedIncrement", 1);
+
+ this.pwmgr.modifyLogin(aLogin, propBag);
+ },
+
+ // JS port of http://mxr.mozilla.org/mozilla-central/source/embedding/components/windowwatcher/nsPrompt.cpp#388
+ makeDialogText: function pu_makeDialogText(aChannel, aAuthInfo) {
+ let isProxy = (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY);
+ let isPassOnly = (aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD);
+ let isCrossOrig = (aAuthInfo.flags &
+ Ci.nsIAuthInformation.CROSS_ORIGIN_SUB_RESOURCE);
+
+ let username = aAuthInfo.username;
+ let [displayHost, realm] = this.getAuthTarget(aChannel, aAuthInfo);
+
+ // Suppress "the site says: $realm" when we synthesized a missing realm.
+ if (!aAuthInfo.realm && !isProxy)
+ realm = "";
+
+ // Trim obnoxiously long realms.
+ if (realm.length > 150) {
+ realm = realm.substring(0, 150);
+ // Append "..." (or localized equivalent).
+ realm += this.ellipsis;
+ }
+
+ let text;
+ if (isProxy) {
+ text = this.bundle.formatStringFromName("EnterLoginForProxy3", [realm, displayHost], 2);
+ } else if (isPassOnly) {
+ text = this.bundle.formatStringFromName("EnterPasswordFor", [username, displayHost], 2);
+ } else if (isCrossOrig) {
+ text = this.bundle.formatStringFromName("EnterUserPasswordForCrossOrigin2", [displayHost], 1);
+ } else if (!realm) {
+ text = this.bundle.formatStringFromName("EnterUserPasswordFor2", [displayHost], 1);
+ } else {
+ text = this.bundle.formatStringFromName("EnterLoginForRealm3", [realm, displayHost], 2);
+ }
+
+ return text;
+ },
+
+ // JS port of http://mxr.mozilla.org/mozilla-central/source/embedding/components/windowwatcher/nsPromptUtils.h#89
+ getAuthHostPort: function pu_getAuthHostPort(aChannel, aAuthInfo) {
+ let uri = aChannel.URI;
+ let res = { host: null, port: -1 };
+ if (aAuthInfo.flags & aAuthInfo.AUTH_PROXY) {
+ let proxy = aChannel.QueryInterface(Ci.nsIProxiedChannel);
+ res.host = proxy.proxyInfo.host;
+ res.port = proxy.proxyInfo.port;
+ } else {
+ res.host = uri.host;
+ res.port = uri.port;
+ }
+ return res;
+ },
+
+ getAuthTarget : function pu_getAuthTarget(aChannel, aAuthInfo) {
+ let hostname, realm;
+ // If our proxy is demanding authentication, don't use the
+ // channel's actual destination.
+ if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) {
+ if (!(aChannel instanceof Ci.nsIProxiedChannel))
+ throw "proxy auth needs nsIProxiedChannel";
+
+ let info = aChannel.proxyInfo;
+ if (!info)
+ throw "proxy auth needs nsIProxyInfo";
+
+ // Proxies don't have a scheme, but we'll use "moz-proxy://"
+ // so that it's more obvious what the login is for.
+ let idnService = Cc["@mozilla.org/network/idn-service;1"].getService(Ci.nsIIDNService);
+ hostname = "moz-proxy://" + idnService.convertUTF8toACE(info.host) + ":" + info.port;
+ realm = aAuthInfo.realm;
+ if (!realm)
+ realm = hostname;
+
+ return [hostname, realm];
+ }
+ hostname = this.getFormattedHostname(aChannel.URI);
+
+ // If a HTTP WWW-Authenticate header specified a realm, that value
+ // will be available here. If it wasn't set or wasn't HTTP, we'll use
+ // the formatted hostname instead.
+ realm = aAuthInfo.realm;
+ if (!realm)
+ realm = hostname;
+
+ return [hostname, realm];
+ },
+
+ getAuthInfo : function pu_getAuthInfo(aAuthInfo) {
+ let flags = aAuthInfo.flags;
+ let username = {value: ""};
+ let password = {value: ""};
+
+ if (flags & Ci.nsIAuthInformation.NEED_DOMAIN && aAuthInfo.domain)
+ username.value = aAuthInfo.domain + "\\" + aAuthInfo.username;
+ else
+ username.value = aAuthInfo.username;
+
+ password.value = aAuthInfo.password
+
+ return [username, password];
+ },
+
+ setAuthInfo : function (aAuthInfo, username, password) {
+ var flags = aAuthInfo.flags;
+ if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) {
+ // Domain is separated from username by a backslash
+ var idx = username.indexOf("\\");
+ if (idx == -1) {
+ aAuthInfo.username = username;
+ } else {
+ aAuthInfo.domain = username.substring(0, idx);
+ aAuthInfo.username = username.substring(idx+1);
+ }
+ } else {
+ aAuthInfo.username = username;
+ }
+ aAuthInfo.password = password;
+ },
+
+ /**
+ * Strip out things like userPass and path for display.
+ */
+ getFormattedHostname : function pu_getFormattedHostname(uri) {
+ return uri.scheme + "://" + uri.hostPort;
+ },
+
+ fireDialogEvent: function(aDomWin, aEventName) {
+ // accessing the document object can throw if this window no longer exists. See bug 789888.
+ try {
+ if (!aDomWin.document)
+ return;
+ let event = aDomWin.document.createEvent("Events");
+ event.initEvent(aEventName, true, true);
+ let winUtils = aDomWin.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ winUtils.dispatchEventToChromeOnly(aDomWin, event);
+ } catch(ex) {
+ }
+ }
+};
+
+XPCOMUtils.defineLazyGetter(PromptUtils, "passwdBundle", function () {
+ return Services.strings.createBundle("chrome://passwordmgr/locale/passwordmgr.properties");
+});
+
+XPCOMUtils.defineLazyGetter(PromptUtils, "bundle", function () {
+ return Services.strings.createBundle("chrome://global/locale/commonDialogs.properties");
+});
+
+
+// Factory for wrapping nsIAuthPrompt interfaces to make them usable via an nsIAuthPrompt2 interface.
+// XXX Copied from nsPrompter.js.
+function AuthPromptAdapterFactory() {
+}
+
+AuthPromptAdapterFactory.prototype = {
+ classID: Components.ID("{80dae1e9-e0d2-4974-915f-f97050fa8068}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAuthPromptAdapterFactory]),
+
+ /* ---------- nsIAuthPromptAdapterFactory ---------- */
+
+ createAdapter: function(aPrompt) {
+ return new AuthPromptAdapter(aPrompt);
+ }
+};
+
+
+// Takes an nsIAuthPrompt implementation, wraps it with a nsIAuthPrompt2 shell.
+// XXX Copied from nsPrompter.js.
+function AuthPromptAdapter(aPrompt) {
+ this.prompt = aPrompt;
+}
+
+AuthPromptAdapter.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAuthPrompt2]),
+ prompt: null,
+
+ /* ---------- nsIAuthPrompt2 ---------- */
+
+ promptAuth: function(aChannel, aLevel, aAuthInfo, aCheckLabel, aCheckValue) {
+ let message = PromptUtils.makeDialogText(aChannel, aAuthInfo);
+
+ let [username, password] = PromptUtils.getAuthInfo(aAuthInfo);
+ let [host, realm] = PromptUtils.getAuthTarget(aChannel, aAuthInfo);
+ let authTarget = host + " (" + realm + ")";
+
+ let ok;
+ if (aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD) {
+ ok = this.prompt.promptPassword(null, message, authTarget, Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, password);
+ } else {
+ ok = this.prompt.promptUsernameAndPassword(null, message, authTarget, Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, username, password);
+ }
+
+ if (ok) {
+ PromptUtils.setAuthInfo(aAuthInfo, username.value, password.value);
+ }
+ return ok;
+ },
+
+ asyncPromptAuth: function(aChannel, aCallback, aContext, aLevel, aAuthInfo, aCheckLabel, aCheckValue) {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PromptService, AuthPromptAdapterFactory]);
diff --git a/mobile/android/components/SessionStore.idl b/mobile/android/components/SessionStore.idl
new file mode 100644
index 0000000000..14ddd5834f
--- /dev/null
+++ b/mobile/android/components/SessionStore.idl
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIDOMWindow;
+interface nsIDOMNode;
+
+/**
+ * nsISessionStore keeps track of the current browsing state.
+ *
+ * The nsISessionStore API operates mostly on browser windows and the browser
+ * tabs contained in them.
+ */
+
+[scriptable, uuid(da9ffc70-d444-47d4-b4ab-df3fb0fd24d0)]
+interface nsISessionStore : nsISupports
+{
+ /**
+ * Get the current browsing state.
+ * @returns a JSON string representing the session state.
+ */
+ AString getBrowserState();
+
+ /**
+ * Get the number of restore-able tabs for a browser window
+ */
+ unsigned long getClosedTabCount(in nsIDOMWindow aWindow);
+
+ /**
+ * Get closed tab data
+ *
+ * @param aWindow is the browser window for which to get closed tab data
+ * @returns a JS array of closed tabs.
+ */
+ jsval getClosedTabs(in nsIDOMWindow aWindow);
+
+ /**
+ * @param aWindow is the browser window to reopen a closed tab in.
+ * @param aCloseTabData is the data of the tab to be restored.
+ * @returns a reference to the reopened tab.
+ */
+ nsIDOMNode undoCloseTab(in nsIDOMWindow aWindow, in jsval aCloseTabData);
+
+ /**
+ * @param aWindow is the browser window associated with the closed tab.
+ * @param aIndex is the index of the closed tab to be removed (FIFO ordered).
+ */
+ nsIDOMNode forgetClosedTab(in nsIDOMWindow aWindow, in unsigned long aIndex);
+
+ /**
+ * @param aTab is the browser tab to get the value for.
+ * @param aKey is the value's name.
+ *
+ * @returns A string value or an empty string if none is set.
+ */
+ AString getTabValue(in jsval aTab, in AString aKey);
+
+ /**
+ * @param aTab is the browser tab to set the value for.
+ * @param aKey is the value's name.
+ * @param aStringValue is the value itself (use JSON.stringify/parse before setting JS objects).
+ */
+ void setTabValue(in jsval aTab, in AString aKey, in AString aStringValue);
+
+ /**
+ * @param aTab is the browser tab to get the value for.
+ * @param aKey is the value's name.
+ */
+ void deleteTabValue(in jsval aTab, in AString aKey);
+
+ /**
+ * Restores the previous browser session using a fast, lightweight strategy
+ * @param aSessionString The session string to restore from. If null, the
+ * backup session file is read from.
+ */
+ void restoreLastSession(in AString aSessionString);
+
+ /**
+ * Removes a window from the current session history. Data from this window
+ * won't be saved when its closed.
+ * @param aWindow The window to remove
+ */
+ void removeWindow(in nsIDOMWindow aWindow);
+};
diff --git a/mobile/android/components/SessionStore.js b/mobile/android/components/SessionStore.js
new file mode 100644
index 0000000000..18ac6bf940
--- /dev/null
+++ b/mobile/android/components/SessionStore.js
@@ -0,0 +1,1794 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FormData", "resource://gre/modules/FormData.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition", "resource://gre/modules/ScrollPosition.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", "resource://gre/modules/TelemetryStopwatch.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Log", "resource://gre/modules/AndroidLog.jsm", "AndroidLog");
+XPCOMUtils.defineLazyModuleGetter(this, "SharedPreferences", "resource://gre/modules/SharedPreferences.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Utils", "resource://gre/modules/sessionstore/Utils.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "serializationHelper",
+ "@mozilla.org/network/serialization-helper;1",
+ "nsISerializationHelper");
+
+function dump(a) {
+ Services.console.logStringMessage(a);
+}
+
+let loggingEnabled = false;
+
+function log(a) {
+ if (!loggingEnabled) {
+ return;
+ }
+ Log.d("SessionStore", a);
+}
+
+// -----------------------------------------------------------------------
+// Session Store
+// -----------------------------------------------------------------------
+
+const STATE_STOPPED = 0;
+const STATE_RUNNING = 1;
+const STATE_QUITTING = -1;
+const STATE_QUITTING_FLUSHED = -2;
+
+const PRIVACY_NONE = 0;
+const PRIVACY_ENCRYPTED = 1;
+const PRIVACY_FULL = 2;
+
+const PREFS_RESTORE_FROM_CRASH = "browser.sessionstore.resume_from_crash";
+const PREFS_MAX_CRASH_RESUMES = "browser.sessionstore.max_resumed_crashes";
+
+const MINIMUM_SAVE_DELAY = 2000;
+// We reduce the delay in background because we could be killed at any moment,
+// however we don't set it to 0 in order to allow for multiple events arriving
+// one after the other to be batched together in one write operation.
+const MINIMUM_SAVE_DELAY_BACKGROUND = 200;
+
+function SessionStore() { }
+
+SessionStore.prototype = {
+ classID: Components.ID("{8c1f07d6-cba3-4226-a315-8bd43d67d032}"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISessionStore,
+ Ci.nsIDOMEventListener,
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+
+ _windows: {},
+ _lastSaveTime: 0,
+ _lastBackupTime: 0,
+ _interval: 10000,
+ _backupInterval: 120000, // 2 minutes
+ _minSaveDelay: MINIMUM_SAVE_DELAY,
+ _maxTabsUndo: 5,
+ _pendingWrite: 0,
+ _scrollSavePending: null,
+ _writeInProgress: false,
+
+ // We only want to start doing backups if we've successfully
+ // written the session data at least once.
+ _sessionDataIsGood: false,
+
+ // The index where the most recently closed tab was in the tabs array
+ // when it was closed.
+ _lastClosedTabIndex: -1,
+
+ // Whether or not to send notifications for changes to the closed tabs.
+ _notifyClosedTabs: false,
+
+ // If we're simultaneously closing both a tab and Firefox, we don't want
+ // to bother reloading the newly selected tab if it is zombified.
+ // The Java UI will tell us which tab to watch out for.
+ _keepAsZombieTabId: -1,
+
+ init: function ss_init() {
+ loggingEnabled = Services.prefs.getBoolPref("browser.sessionstore.debug_logging");
+
+ // Get file references
+ this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
+ this._sessionFileBackup = this._sessionFile.clone();
+ this._sessionFilePrevious = this._sessionFile.clone();
+ this._sessionFileTemp = this._sessionFile.clone();
+ this._sessionFile.append("sessionstore.js"); // The main session store save file.
+ this._sessionFileBackup.append("sessionstore.bak"); // A backup copy to guard against interrupted writes.
+ this._sessionFilePrevious.append("sessionstore.old"); // The previous session's file, used for what used to be the "Tabs from last time".
+ this._sessionFileTemp.append(this._sessionFile.leafName + ".tmp"); // Temporary file for writing changes to disk.
+
+ this._loadState = STATE_STOPPED;
+ this._startupRestoreFinished = false;
+
+ this._interval = Services.prefs.getIntPref("browser.sessionstore.interval");
+ this._backupInterval = Services.prefs.getIntPref("browser.sessionstore.backupInterval");
+ this._maxTabsUndo = Services.prefs.getIntPref("browser.sessionstore.max_tabs_undo");
+
+ // Copy changes in Gecko settings to their Java counterparts,
+ // so the startup code can access them
+ Services.prefs.addObserver(PREFS_RESTORE_FROM_CRASH, function() {
+ SharedPreferences.forApp().setBoolPref(PREFS_RESTORE_FROM_CRASH,
+ Services.prefs.getBoolPref(PREFS_RESTORE_FROM_CRASH));
+ }, false);
+ Services.prefs.addObserver(PREFS_MAX_CRASH_RESUMES, function() {
+ SharedPreferences.forApp().setIntPref(PREFS_MAX_CRASH_RESUMES,
+ Services.prefs.getIntPref(PREFS_MAX_CRASH_RESUMES));
+ }, false);
+ },
+
+ _clearDisk: function ss_clearDisk() {
+ this._sessionDataIsGood = false;
+
+ if (this._loadState > STATE_QUITTING) {
+ OS.File.remove(this._sessionFile.path);
+ OS.File.remove(this._sessionFileBackup.path);
+ OS.File.remove(this._sessionFilePrevious.path);
+ OS.File.remove(this._sessionFileTemp.path);
+ } else { // We're shutting down and must delete synchronously
+ if (this._sessionFile.exists()) { this._sessionFile.remove(false); }
+ if (this._sessionFileBackup.exists()) { this._sessionFileBackup.remove(false); }
+ if (this._sessionFileBackup.exists()) { this._sessionFilePrevious.remove(false); }
+ if (this._sessionFileBackup.exists()) { this._sessionFileTemp.remove(false); }
+ }
+ },
+
+ observe: function ss_observe(aSubject, aTopic, aData) {
+ let self = this;
+ let observerService = Services.obs;
+ switch (aTopic) {
+ case "app-startup":
+ observerService.addObserver(this, "final-ui-startup", true);
+ observerService.addObserver(this, "domwindowopened", true);
+ observerService.addObserver(this, "domwindowclosed", true);
+ observerService.addObserver(this, "browser:purge-session-history", true);
+ observerService.addObserver(this, "quit-application-requested", true);
+ observerService.addObserver(this, "quit-application-proceeding", true);
+ observerService.addObserver(this, "quit-application", true);
+ observerService.addObserver(this, "Session:Restore", true);
+ observerService.addObserver(this, "Session:NotifyLocationChange", true);
+ observerService.addObserver(this, "Tab:KeepZombified", true);
+ observerService.addObserver(this, "application-background", true);
+ observerService.addObserver(this, "application-foreground", true);
+ observerService.addObserver(this, "ClosedTabs:StartNotifications", true);
+ observerService.addObserver(this, "ClosedTabs:StopNotifications", true);
+ observerService.addObserver(this, "last-pb-context-exited", true);
+ observerService.addObserver(this, "Session:RestoreRecentTabs", true);
+ observerService.addObserver(this, "Tabs:OpenMultiple", true);
+ break;
+ case "final-ui-startup":
+ observerService.removeObserver(this, "final-ui-startup");
+ this.init();
+ break;
+ case "domwindowopened": {
+ let window = aSubject;
+ window.addEventListener("load", function() {
+ self.onWindowOpen(window);
+ window.removeEventListener("load", arguments.callee, false);
+ }, false);
+ break;
+ }
+ case "domwindowclosed": // catch closed windows
+ this.onWindowClose(aSubject);
+ break;
+ case "quit-application-requested":
+ log("quit-application-requested");
+ // Get a current snapshot of all windows
+ if (this._pendingWrite) {
+ this._forEachBrowserWindow(function(aWindow) {
+ self._collectWindowData(aWindow);
+ });
+ }
+ break;
+ case "quit-application-proceeding":
+ log("quit-application-proceeding");
+ // Freeze the data at what we've got (ignoring closing windows)
+ this._loadState = STATE_QUITTING;
+ break;
+ case "quit-application":
+ log("quit-application");
+ observerService.removeObserver(this, "domwindowopened");
+ observerService.removeObserver(this, "domwindowclosed");
+ observerService.removeObserver(this, "quit-application-requested");
+ observerService.removeObserver(this, "quit-application-proceeding");
+ observerService.removeObserver(this, "quit-application");
+
+ // Flush all pending writes to disk now
+ this.flushPendingState();
+ this._loadState = STATE_QUITTING_FLUSHED;
+
+ break;
+ case "browser:purge-session-history": // catch sanitization
+ log("browser:purge-session-history");
+ this._clearDisk();
+
+ // Clear all data about closed tabs
+ for (let [ssid, win] of Object.entries(this._windows))
+ win.closedTabs = [];
+
+ this._lastClosedTabIndex = -1;
+
+ if (this._loadState == STATE_RUNNING) {
+ // Save the purged state immediately
+ this.saveState();
+ } else if (this._loadState <= STATE_QUITTING) {
+ this.saveStateDelayed();
+ if (this._loadState == STATE_QUITTING_FLUSHED) {
+ this.flushPendingState();
+ }
+ }
+
+ Services.obs.notifyObservers(null, "sessionstore-state-purge-complete", "");
+ if (this._notifyClosedTabs) {
+ this._sendClosedTabsToJava(Services.wm.getMostRecentWindow("navigator:browser"));
+ }
+ break;
+ case "timer-callback":
+ if (this._loadState == STATE_RUNNING) {
+ // Timer call back for delayed saving
+ this._saveTimer = null;
+ log("timer-callback, pendingWrite = " + this._pendingWrite);
+ if (this._pendingWrite) {
+ this.saveState();
+ }
+ }
+ break;
+ case "Session:Restore": {
+ Services.obs.removeObserver(this, "Session:Restore");
+ if (aData) {
+ // Be ready to handle any restore failures by making sure we have a valid tab opened
+ let window = Services.wm.getMostRecentWindow("navigator:browser");
+ let restoreCleanup = {
+ observe: function (aSubject, aTopic, aData) {
+ Services.obs.removeObserver(restoreCleanup, "sessionstore-windows-restored");
+
+ if (window.BrowserApp.tabs.length == 0) {
+ window.BrowserApp.addTab("about:home", {
+ selected: true
+ });
+ }
+ // Normally, _restoreWindow() will have set this to true already,
+ // but we want to make sure it's set even in case of a restore failure.
+ this._startupRestoreFinished = true;
+ log("startupRestoreFinished = true (through notification)");
+ }.bind(this)
+ };
+ Services.obs.addObserver(restoreCleanup, "sessionstore-windows-restored", false);
+
+ // Do a restore, triggered by Java
+ let data = JSON.parse(aData);
+ this.restoreLastSession(data.sessionString);
+ } else {
+ // Not doing a restore; just send restore message
+ this._startupRestoreFinished = true;
+ log("startupRestoreFinished = true");
+ Services.obs.notifyObservers(null, "sessionstore-windows-restored", "");
+ }
+ break;
+ }
+ case "Session:NotifyLocationChange": {
+ let browser = aSubject;
+
+ if (browser.__SS_restoreReloadPending && this._startupRestoreFinished) {
+ delete browser.__SS_restoreReloadPending;
+ log("remove restoreReloadPending");
+ }
+
+ if (browser.__SS_restoreDataOnLocationChange) {
+ delete browser.__SS_restoreDataOnLocationChange;
+ this._restoreZoom(browser.__SS_data.scrolldata, browser);
+ }
+ break;
+ }
+ case "Tabs:OpenMultiple": {
+ let data = JSON.parse(aData);
+
+ this._openTabs(data);
+
+ if (data.shouldNotifyTabsOpenedToJava) {
+ Messaging.sendRequest({
+ type: "Tabs:TabsOpened"
+ });
+ }
+ break;
+ }
+ case "Tab:KeepZombified": {
+ if (aData >= 0) {
+ this._keepAsZombieTabId = aData;
+ log("Tab:KeepZombified " + aData);
+ }
+ break;
+ }
+ case "application-background":
+ // We receive this notification when Android's onPause callback is
+ // executed. After onPause, the application may be terminated at any
+ // point without notice; therefore, we must synchronously write out any
+ // pending save state to ensure that this data does not get lost.
+ log("application-background");
+ // Tab events dispatched immediately before the application was backgrounded
+ // might actually arrive after this point, therefore save them without delay.
+ if (this._loadState == STATE_RUNNING) {
+ this._interval = 0;
+ this._minSaveDelay = MINIMUM_SAVE_DELAY_BACKGROUND; // A small delay allows successive tab events to be batched together.
+ this.flushPendingState();
+ }
+ break;
+ case "application-foreground":
+ // Reset minimum interval between session store writes back to default.
+ log("application-foreground");
+ this._interval = Services.prefs.getIntPref("browser.sessionstore.interval");
+ this._minSaveDelay = MINIMUM_SAVE_DELAY;
+
+ // If we skipped restoring a zombified tab before backgrounding,
+ // we might have to do it now instead.
+ let window = Services.wm.getMostRecentWindow("navigator:browser");
+ if (window) { // Might not yet be ready during a cold startup.
+ let tab = window.BrowserApp.selectedTab;
+ if (tab.browser.__SS_restore) {
+ this._restoreZombieTab(tab.browser, tab.id);
+ }
+ }
+ break;
+ case "ClosedTabs:StartNotifications":
+ this._notifyClosedTabs = true;
+ log("ClosedTabs:StartNotifications");
+ this._sendClosedTabsToJava(Services.wm.getMostRecentWindow("navigator:browser"));
+ break;
+ case "ClosedTabs:StopNotifications":
+ this._notifyClosedTabs = false;
+ log("ClosedTabs:StopNotifications");
+ break;
+ case "last-pb-context-exited":
+ // Clear private closed tab data when we leave private browsing.
+ for (let window of Object.values(this._windows)) {
+ window.closedTabs = window.closedTabs.filter(tab => !tab.isPrivate);
+ }
+ this._lastClosedTabIndex = -1;
+ break;
+ case "Session:RestoreRecentTabs": {
+ let data = JSON.parse(aData);
+ this._restoreTabs(data);
+ break;
+ }
+ }
+ },
+
+ handleEvent: function ss_handleEvent(aEvent) {
+ let window = aEvent.currentTarget.ownerDocument.defaultView;
+ switch (aEvent.type) {
+ case "TabOpen": {
+ let browser = aEvent.target;
+ log("TabOpen for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+ this.onTabAdd(window, browser);
+ break;
+ }
+ case "TabClose": {
+ let browser = aEvent.target;
+ log("TabClose for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+ this.onTabClose(window, browser, aEvent.detail);
+ this.onTabRemove(window, browser);
+ break;
+ }
+ case "TabPreZombify": {
+ let browser = aEvent.target;
+ log("TabPreZombify for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+ this.onTabRemove(window, browser, true);
+ break;
+ }
+ case "TabPostZombify": {
+ let browser = aEvent.target;
+ log("TabPostZombify for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+ this.onTabAdd(window, browser, true);
+ break;
+ }
+ case "TabSelect": {
+ let browser = aEvent.target;
+ log("TabSelect for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+ this.onTabSelect(window, browser);
+ break;
+ }
+ case "DOMTitleChanged": {
+ // Use DOMTitleChanged to detect page loads over alternatives.
+ // onLocationChange happens too early, so we don't have the page title
+ // yet; pageshow happens too late, so we could lose session data if the
+ // browser were killed.
+ let browser = aEvent.currentTarget;
+ log("DOMTitleChanged for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+ this.onTabLoad(window, browser);
+ break;
+ }
+ case "load": {
+ let browser = aEvent.currentTarget;
+
+ // Skip subframe loads.
+ if (browser.contentDocument !== aEvent.originalTarget) {
+ return;
+ }
+
+ // Handle restoring the text data into the content and frames.
+ // We wait until the main content and all frames are loaded
+ // before trying to restore this data.
+ log("load for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+ if (browser.__SS_restoreDataOnLoad) {
+ delete browser.__SS_restoreDataOnLoad;
+ this._restoreTextData(browser.__SS_data.formdata, browser);
+ }
+ break;
+ }
+ case "pageshow":
+ case "AboutReaderContentReady": {
+ let browser = aEvent.currentTarget;
+
+ // Skip subframe pageshows.
+ if (browser.contentDocument !== aEvent.originalTarget) {
+ return;
+ }
+
+ if (browser.currentURI.spec.startsWith("about:reader") &&
+ !browser.contentDocument.body.classList.contains("loaded")) {
+ // Don't restore the scroll position of an about:reader page at this point;
+ // wait for the custom event dispatched from AboutReader.jsm instead.
+ return;
+ }
+
+ // Restoring the scroll position needs to happen after the zoom level has been
+ // restored, which is done by the MobileViewportManager either on first paint
+ // or on load, whichever comes first.
+ // In the latter case, our load handler runs before the MVM's one, which is the
+ // wrong way around, so we have to use a later event instead.
+ log(aEvent.type + " for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+ if (browser.__SS_restoreDataOnPageshow) {
+ delete browser.__SS_restoreDataOnPageshow;
+ this._restoreScrollPosition(browser.__SS_data.scrolldata, browser);
+ } else {
+ // We're not restoring, capture the initial scroll position on pageshow.
+ this.onTabScroll(window, browser);
+ }
+ break;
+ }
+ case "change":
+ case "input":
+ case "DOMAutoComplete": {
+ let browser = aEvent.currentTarget;
+ log("TabInput for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+ this.onTabInput(window, browser);
+ break;
+ }
+ case "resize":
+ case "scroll": {
+ let browser = aEvent.currentTarget;
+ // Duplicated logging check to avoid calling getTabForBrowser on each scroll event.
+ if (loggingEnabled) {
+ log(aEvent.type + " for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+ }
+ if (!this._scrollSavePending) {
+ this._scrollSavePending =
+ window.setTimeout(() => {
+ this._scrollSavePending = null;
+ this.onTabScroll(window, browser);
+ }, 500);
+ }
+ break;
+ }
+ }
+ },
+
+ onWindowOpen: function ss_onWindowOpen(aWindow) {
+ // Return if window has already been initialized
+ if (aWindow && aWindow.__SSID && this._windows[aWindow.__SSID]) {
+ return;
+ }
+
+ // Ignore non-browser windows and windows opened while shutting down
+ if (aWindow.document.documentElement.getAttribute("windowtype") != "navigator:browser" || this._loadState <= STATE_QUITTING) {
+ return;
+ }
+
+ // Assign it a unique identifier (timestamp) and create its data object
+ aWindow.__SSID = "window" + Date.now();
+ this._windows[aWindow.__SSID] = { tabs: [], selected: 0, closedTabs: [] };
+
+ // Perform additional initialization when the first window is loading
+ if (this._loadState == STATE_STOPPED) {
+ this._loadState = STATE_RUNNING;
+ this._lastSaveTime = Date.now();
+ }
+
+ // Add tab change listeners to all already existing tabs
+ let tabs = aWindow.BrowserApp.tabs;
+ for (let i = 0; i < tabs.length; i++)
+ this.onTabAdd(aWindow, tabs[i].browser, true);
+
+ // Notification of tab add/remove/selection/zombification
+ let browsers = aWindow.document.getElementById("browsers");
+ browsers.addEventListener("TabOpen", this, true);
+ browsers.addEventListener("TabClose", this, true);
+ browsers.addEventListener("TabSelect", this, true);
+ browsers.addEventListener("TabPreZombify", this, true);
+ browsers.addEventListener("TabPostZombify", this, true);
+ },
+
+ onWindowClose: function ss_onWindowClose(aWindow) {
+ // Ignore windows not tracked by SessionStore
+ if (!aWindow.__SSID || !this._windows[aWindow.__SSID]) {
+ return;
+ }
+
+ let browsers = aWindow.document.getElementById("browsers");
+ browsers.removeEventListener("TabOpen", this, true);
+ browsers.removeEventListener("TabClose", this, true);
+ browsers.removeEventListener("TabSelect", this, true);
+ browsers.removeEventListener("TabPreZombify", this, true);
+ browsers.removeEventListener("TabPostZombify", this, true);
+
+ if (this._loadState == STATE_RUNNING) {
+ // Update all window data for a last time
+ this._collectWindowData(aWindow);
+
+ // Clear this window from the list
+ delete this._windows[aWindow.__SSID];
+
+ // Save the state without this window to disk
+ this.saveStateDelayed();
+ }
+
+ let tabs = aWindow.BrowserApp.tabs;
+ for (let i = 0; i < tabs.length; i++)
+ this.onTabRemove(aWindow, tabs[i].browser, true);
+
+ delete aWindow.__SSID;
+ },
+
+ onTabAdd: function ss_onTabAdd(aWindow, aBrowser, aNoNotification) {
+ // Use DOMTitleChange to catch the initial load and restore history
+ aBrowser.addEventListener("DOMTitleChanged", this, true);
+
+ // Use load to restore text data
+ aBrowser.addEventListener("load", this, true);
+
+ // Gecko might set the initial zoom level after the JS "load" event,
+ // so we have to restore zoom and scroll position after that.
+ aBrowser.addEventListener("pageshow", this, true);
+ aBrowser.addEventListener("AboutReaderContentReady", this, true);
+
+ // Use a combination of events to watch for text data changes
+ aBrowser.addEventListener("change", this, true);
+ aBrowser.addEventListener("input", this, true);
+ aBrowser.addEventListener("DOMAutoComplete", this, true);
+
+ // Record the current scroll position and zoom level.
+ aBrowser.addEventListener("scroll", this, true);
+ aBrowser.addEventListener("resize", this, true);
+
+ log("onTabAdd() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id +
+ ", aNoNotification = " + aNoNotification);
+ if (!aNoNotification) {
+ this.saveStateDelayed();
+ }
+ this._updateCrashReportURL(aWindow);
+ },
+
+ onTabRemove: function ss_onTabRemove(aWindow, aBrowser, aNoNotification) {
+ // Cleanup event listeners
+ aBrowser.removeEventListener("DOMTitleChanged", this, true);
+ aBrowser.removeEventListener("load", this, true);
+ aBrowser.removeEventListener("pageshow", this, true);
+ aBrowser.removeEventListener("AboutReaderContentReady", this, true);
+ aBrowser.removeEventListener("change", this, true);
+ aBrowser.removeEventListener("input", this, true);
+ aBrowser.removeEventListener("DOMAutoComplete", this, true);
+ aBrowser.removeEventListener("scroll", this, true);
+ aBrowser.removeEventListener("resize", this, true);
+
+ delete aBrowser.__SS_data;
+
+ log("onTabRemove() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id +
+ ", aNoNotification = " + aNoNotification);
+ if (!aNoNotification) {
+ this.saveStateDelayed();
+ }
+ },
+
+ onTabClose: function ss_onTabClose(aWindow, aBrowser, aTabIndex) {
+ if (this._maxTabsUndo == 0) {
+ return;
+ }
+
+ if (aWindow.BrowserApp.tabs.length > 0) {
+ // Bundle this browser's data and extra data and save in the closedTabs
+ // window property
+ let data = aBrowser.__SS_data || {};
+ data.extData = aBrowser.__SS_extdata || {};
+
+ this._windows[aWindow.__SSID].closedTabs.unshift(data);
+ let length = this._windows[aWindow.__SSID].closedTabs.length;
+ if (length > this._maxTabsUndo) {
+ this._windows[aWindow.__SSID].closedTabs.splice(this._maxTabsUndo, length - this._maxTabsUndo);
+ }
+
+ this._lastClosedTabIndex = aTabIndex;
+
+ if (this._notifyClosedTabs) {
+ this._sendClosedTabsToJava(aWindow);
+ }
+
+ log("onTabClose() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id);
+ let evt = new Event("SSTabCloseProcessed", {"bubbles":true, "cancelable":false});
+ aBrowser.dispatchEvent(evt);
+ }
+ },
+
+ onTabLoad: function ss_onTabLoad(aWindow, aBrowser) {
+ // If this browser belongs to a zombie tab or the initial restore hasn't yet finished,
+ // skip any session save activity.
+ if (aBrowser.__SS_restore || !this._startupRestoreFinished || aBrowser.__SS_restoreReloadPending) {
+ return;
+ }
+
+ // Ignore a transient "about:blank"
+ if (!aBrowser.canGoBack && aBrowser.currentURI.spec == "about:blank") {
+ return;
+ }
+
+ let history = aBrowser.sessionHistory;
+
+ // Serialize the tab data
+ let entries = [];
+ let index = history.index + 1;
+ for (let i = 0; i < history.count; i++) {
+ let historyEntry = history.getEntryAtIndex(i, false);
+ // Don't try to restore wyciwyg URLs
+ if (historyEntry.URI.schemeIs("wyciwyg")) {
+ // Adjust the index to account for skipped history entries
+ if (i <= history.index) {
+ index--;
+ }
+ continue;
+ }
+ let entry = this._serializeHistoryEntry(historyEntry);
+ entries.push(entry);
+ }
+ let data = { entries: entries, index: index };
+
+ let formdata;
+ let scrolldata;
+ if (aBrowser.__SS_data) {
+ formdata = aBrowser.__SS_data.formdata;
+ scrolldata = aBrowser.__SS_data.scrolldata;
+ }
+ delete aBrowser.__SS_data;
+
+ this._collectTabData(aWindow, aBrowser, data);
+ if (aBrowser.__SS_restoreDataOnLoad || aBrowser.__SS_restoreDataOnPageshow) {
+ // If the tab has been freshly restored and the "load" or "pageshow"
+ // events haven't yet fired, we need to preserve any form data and
+ // scroll positions that might have been present.
+ aBrowser.__SS_data.formdata = formdata;
+ aBrowser.__SS_data.scrolldata = scrolldata;
+ } else {
+ // When navigating via the forward/back buttons, Gecko restores
+ // the form data all by itself and doesn't invoke any input events.
+ // As _collectTabData() doesn't save any form data, we need to manually
+ // capture it to bridge the time until the next input event arrives.
+ this.onTabInput(aWindow, aBrowser);
+ }
+
+ log("onTabLoad() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id);
+ let evt = new Event("SSTabDataUpdated", {"bubbles":true, "cancelable":false});
+ aBrowser.dispatchEvent(evt);
+ this.saveStateDelayed();
+
+ this._updateCrashReportURL(aWindow);
+ },
+
+ onTabSelect: function ss_onTabSelect(aWindow, aBrowser) {
+ if (this._loadState != STATE_RUNNING) {
+ return;
+ }
+
+ let browsers = aWindow.document.getElementById("browsers");
+ let index = browsers.selectedIndex;
+ this._windows[aWindow.__SSID].selected = parseInt(index) + 1; // 1-based
+
+ let tabId = aWindow.BrowserApp.getTabForBrowser(aBrowser).id;
+
+ // Restore the resurrected browser
+ if (aBrowser.__SS_restore) {
+ if (tabId != this._keepAsZombieTabId) {
+ this._restoreZombieTab(aBrowser, tabId);
+ } else {
+ log("keeping as zombie tab " + tabId);
+ }
+ }
+ // The tab id passed through Tab:KeepZombified is valid for one TabSelect only.
+ this._keepAsZombieTabId = -1;
+
+ log("onTabSelect() ran for tab " + tabId);
+ this.saveStateDelayed();
+ this._updateCrashReportURL(aWindow);
+
+ // If the selected tab has changed while listening for closed tab
+ // notifications, we may have switched between different private browsing
+ // modes.
+ if (this._notifyClosedTabs) {
+ this._sendClosedTabsToJava(aWindow);
+ }
+ },
+
+ _restoreZombieTab: function ss_restoreZombieTab(aBrowser, aTabId) {
+ let data = aBrowser.__SS_data;
+ this._restoreTab(data, aBrowser);
+
+ delete aBrowser.__SS_restore;
+ aBrowser.removeAttribute("pending");
+ log("restoring zombie tab " + aTabId);
+ },
+
+ onTabInput: function ss_onTabInput(aWindow, aBrowser) {
+ // If this browser belongs to a zombie tab or the initial restore hasn't yet finished,
+ // skip any session save activity.
+ if (aBrowser.__SS_restore || !this._startupRestoreFinished || aBrowser.__SS_restoreReloadPending) {
+ return;
+ }
+
+ // Don't bother trying to save text data if we don't have history yet
+ let data = aBrowser.__SS_data;
+ if (!data || data.entries.length == 0) {
+ return;
+ }
+
+ // Start with storing the main content
+ let content = aBrowser.contentWindow;
+
+ // If the main content document has an associated URL that we are not
+ // allowed to store data for, bail out. We explicitly discard data for any
+ // children as well even if storing data for those frames would be allowed.
+ if (!this.checkPrivacyLevel(content.document.documentURI)) {
+ return;
+ }
+
+ // Store the main content
+ let formdata = FormData.collect(content) || {};
+
+ // Loop over direct child frames, and store the text data
+ let children = [];
+ for (let i = 0; i < content.frames.length; i++) {
+ let frame = content.frames[i];
+ if (!this.checkPrivacyLevel(frame.document.documentURI)) {
+ continue;
+ }
+
+ let result = FormData.collect(frame);
+ if (result && Object.keys(result).length) {
+ children[i] = result;
+ }
+ }
+
+ // If any frame had text data, add it to the main form data
+ if (children.length) {
+ formdata.children = children;
+ }
+
+ // If we found any form data, main content or frames, let's save it
+ if (Object.keys(formdata).length) {
+ data.formdata = formdata;
+ log("onTabInput() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id);
+ this.saveStateDelayed();
+ }
+ },
+
+ onTabScroll: function ss_onTabScroll(aWindow, aBrowser) {
+ // If we've been called directly, cancel any pending timeouts.
+ if (this._scrollSavePending) {
+ aWindow.clearTimeout(this._scrollSavePending);
+ this._scrollSavePending = null;
+ log("onTabScroll() clearing pending timeout");
+ }
+
+ // If this browser belongs to a zombie tab or the initial restore hasn't yet finished,
+ // skip any session save activity.
+ if (aBrowser.__SS_restore || !this._startupRestoreFinished || aBrowser.__SS_restoreReloadPending) {
+ return;
+ }
+
+ // Don't bother trying to save scroll positions if we don't have history yet.
+ let data = aBrowser.__SS_data;
+ if (!data || data.entries.length == 0) {
+ return;
+ }
+
+ // Neither bother if we're yet to restore the previous scroll position.
+ if (aBrowser.__SS_restoreDataOnLoad || aBrowser.__SS_restoreDataOnPageshow) {
+ return;
+ }
+
+ // Start with storing the main content.
+ let content = aBrowser.contentWindow;
+
+ // Store the main content.
+ let scrolldata = ScrollPosition.collect(content) || {};
+
+ // Loop over direct child frames, and store the scroll positions.
+ let children = [];
+ for (let i = 0; i < content.frames.length; i++) {
+ let frame = content.frames[i];
+
+ let result = ScrollPosition.collect(frame);
+ if (result && Object.keys(result).length) {
+ children[i] = result;
+ }
+ }
+
+ // If any frame had scroll positions, add them to the main scroll data.
+ if (children.length) {
+ scrolldata.children = children;
+ }
+
+ // Save the current document resolution.
+ let zoom = { value: 1 };
+ content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(
+ Ci.nsIDOMWindowUtils).getResolution(zoom);
+ scrolldata.zoom = {};
+ scrolldata.zoom.resolution = zoom.value;
+ log("onTabScroll() zoom level: " + zoom.value);
+
+ // Save some data that'll help in adjusting the zoom level
+ // when restoring in a different screen orientation.
+ scrolldata.zoom.displaySize = this._getContentViewerSize(content);
+ log("onTabScroll() displayWidth: " + scrolldata.zoom.displaySize.width);
+
+ // Save zoom and scroll data.
+ data.scrolldata = scrolldata;
+ log("onTabScroll() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id);
+ let evt = new Event("SSTabScrollCaptured", {"bubbles":true, "cancelable":false});
+ aBrowser.dispatchEvent(evt);
+ this.saveStateDelayed();
+ },
+
+ _getContentViewerSize: function ss_getContentViewerSize(aWindow) {
+ let displaySize = {};
+ let width = {}, height = {};
+ aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(
+ Ci.nsIDOMWindowUtils).getContentViewerSize(width, height);
+
+ displaySize.width = width.value;
+ displaySize.height = height.value;
+
+ return displaySize;
+ },
+
+ saveStateDelayed: function ss_saveStateDelayed() {
+ if (!this._saveTimer) {
+ // Interval until the next disk operation is allowed
+ let currentDelay = this._lastSaveTime + this._interval - Date.now();
+
+ // If we have to wait, set a timer, otherwise saveState directly
+ let delay = Math.max(currentDelay, this._minSaveDelay);
+ if (delay > 0) {
+ this._pendingWrite++;
+ this._saveTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._saveTimer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT);
+ log("saveStateDelayed() timer delay = " + delay +
+ ", incrementing _pendingWrite to " + this._pendingWrite);
+ } else {
+ log("saveStateDelayed() no delay");
+ this.saveState();
+ }
+ } else {
+ log("saveStateDelayed() timer already running, taking no action");
+ }
+ },
+
+ saveState: function ss_saveState() {
+ this._pendingWrite++;
+ log("saveState(), incrementing _pendingWrite to " + this._pendingWrite);
+ this._saveState(true);
+ },
+
+ // Immediately and synchronously writes any pending state to disk.
+ flushPendingState: function ss_flushPendingState() {
+ log("flushPendingState(), _pendingWrite = " + this._pendingWrite);
+ if (this._pendingWrite) {
+ this._saveState(false);
+ }
+ },
+
+ _saveState: function ss_saveState(aAsync) {
+ log("_saveState(aAsync = " + aAsync + ")");
+ // Kill any queued timer and save immediately
+ if (this._saveTimer) {
+ this._saveTimer.cancel();
+ this._saveTimer = null;
+ log("_saveState() killed queued timer");
+ }
+
+ // Periodically save a "known good" copy of the session store data.
+ if (!this._writeInProgress && Date.now() - this._lastBackupTime > this._backupInterval &&
+ this._sessionDataIsGood && this._sessionFile.exists()) {
+ if (this._sessionFileBackup.exists()) {
+ this._sessionFileBackup.remove(false);
+ }
+
+ log("_saveState() backing up session data");
+ this._sessionFile.copyTo(null, this._sessionFileBackup.leafName);
+ this._lastBackupTime = Date.now();
+ }
+
+ let data = this._getCurrentState();
+ let normalData = { windows: [] };
+ let privateData = { windows: [] };
+ log("_saveState() current state collected");
+
+ for (let winIndex = 0; winIndex < data.windows.length; ++winIndex) {
+ let win = data.windows[winIndex];
+ let normalWin = {};
+ for (let prop in win) {
+ normalWin[prop] = data[prop];
+ }
+ normalWin.tabs = [];
+
+ // Save normal closed tabs. Forget about private closed tabs.
+ normalWin.closedTabs = win.closedTabs.filter(tab => !tab.isPrivate);
+
+ normalData.windows.push(normalWin);
+ privateData.windows.push({ tabs: [] });
+
+ // Split the session data into private and non-private data objects.
+ // Non-private session data will be saved to disk, and private session
+ // data will be sent to Java for Android to hold it in memory.
+ for (let i = 0; i < win.tabs.length; ++i) {
+ let tab = win.tabs[i];
+ let savedWin = tab.isPrivate ? privateData.windows[winIndex] : normalData.windows[winIndex];
+ savedWin.tabs.push(tab);
+ if (win.selected == i + 1) {
+ savedWin.selected = savedWin.tabs.length;
+ }
+ }
+ }
+
+ // Write only non-private data to disk
+ if (normalData.windows[0] && normalData.windows[0].tabs) {
+ log("_saveState() writing normal data, " +
+ normalData.windows[0].tabs.length + " tabs in window[0]");
+ } else {
+ log("_saveState() writing empty normal data");
+ }
+ this._writeFile(this._sessionFile, this._sessionFileTemp, normalData, aAsync);
+
+ // If we have private data, send it to Java; otherwise, send null to
+ // indicate that there is no private data
+ Messaging.sendRequest({
+ type: "PrivateBrowsing:Data",
+ session: (privateData.windows.length > 0 && privateData.windows[0].tabs.length > 0) ? JSON.stringify(privateData) : null
+ });
+
+ this._lastSaveTime = Date.now();
+ },
+
+ _getCurrentState: function ss_getCurrentState() {
+ let self = this;
+ this._forEachBrowserWindow(function(aWindow) {
+ self._collectWindowData(aWindow);
+ });
+
+ let data = { windows: [] };
+ for (let index in this._windows) {
+ data.windows.push(this._windows[index]);
+ }
+
+ return data;
+ },
+
+ _collectTabData: function ss__collectTabData(aWindow, aBrowser, aHistory) {
+ // If this browser is being restored, skip any session save activity
+ if (aBrowser.__SS_restore) {
+ return;
+ }
+
+ aHistory = aHistory || { entries: [{ url: aBrowser.currentURI.spec, title: aBrowser.contentTitle }], index: 1 };
+
+ let tabData = {};
+ tabData.entries = aHistory.entries;
+ tabData.index = aHistory.index;
+ tabData.attributes = { image: aBrowser.mIconURL };
+ tabData.desktopMode = aWindow.BrowserApp.getTabForBrowser(aBrowser).desktopMode;
+ tabData.isPrivate = aBrowser.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing;
+
+ aBrowser.__SS_data = tabData;
+ },
+
+ _collectWindowData: function ss__collectWindowData(aWindow) {
+ // Ignore windows not tracked by SessionStore
+ if (!aWindow.__SSID || !this._windows[aWindow.__SSID]) {
+ return;
+ }
+
+ let winData = this._windows[aWindow.__SSID];
+ winData.tabs = [];
+
+ let browsers = aWindow.document.getElementById("browsers");
+ let index = browsers.selectedIndex;
+ winData.selected = parseInt(index) + 1; // 1-based
+
+ let tabs = aWindow.BrowserApp.tabs;
+ for (let i = 0; i < tabs.length; i++) {
+ let browser = tabs[i].browser;
+ if (browser.__SS_data) {
+ let tabData = browser.__SS_data;
+ if (browser.__SS_extdata) {
+ tabData.extData = browser.__SS_extdata;
+ }
+ winData.tabs.push(tabData);
+ }
+ }
+ },
+
+ _forEachBrowserWindow: function ss_forEachBrowserWindow(aFunc) {
+ let windowsEnum = Services.wm.getEnumerator("navigator:browser");
+ while (windowsEnum.hasMoreElements()) {
+ let window = windowsEnum.getNext();
+ if (window.__SSID && !window.closed) {
+ aFunc.call(this, window);
+ }
+ }
+ },
+
+ /**
+ * Writes the session state to a disk file, while doing some telemetry and notification
+ * bookkeeping.
+ * @param aFile nsIFile used for saving the session
+ * @param aFileTemp nsIFile used as a temporary file in writing the data
+ * @param aData JSON session state
+ * @param aAsync boolelan used to determine the method of saving the state
+ */
+ _writeFile: function ss_writeFile(aFile, aFileTemp, aData, aAsync) {
+ TelemetryStopwatch.start("FX_SESSION_RESTORE_SERIALIZE_DATA_MS");
+ let state = JSON.stringify(aData);
+ TelemetryStopwatch.finish("FX_SESSION_RESTORE_SERIALIZE_DATA_MS");
+
+ // Convert data string to a utf-8 encoded array buffer
+ let buffer = new TextEncoder().encode(state);
+ Services.telemetry.getHistogramById("FX_SESSION_RESTORE_FILE_SIZE_BYTES").add(buffer.byteLength);
+
+ Services.obs.notifyObservers(null, "sessionstore-state-write", "");
+ let startWriteMs = Cu.now();
+
+ log("_writeFile(aAsync = " + aAsync + "), _pendingWrite = " + this._pendingWrite);
+ this._writeInProgress = true;
+ let pendingWrite = this._pendingWrite;
+ this._write(aFile, aFileTemp, buffer, aAsync).then(() => {
+ let stopWriteMs = Cu.now();
+
+ // Make sure this._pendingWrite is the same value it was before we
+ // fired off the async write. If the count is different, another write
+ // is pending, so we shouldn't reset this._pendingWrite yet.
+ if (pendingWrite === this._pendingWrite) {
+ this._pendingWrite = 0;
+ this._writeInProgress = false;
+ }
+
+ log("_writeFile() _write() returned, _pendingWrite = " + this._pendingWrite);
+
+ // We don't use a stopwatch here since the calls are async and stopwatches can only manage
+ // a single timer per histogram.
+ Services.telemetry.getHistogramById("FX_SESSION_RESTORE_WRITE_FILE_MS").add(Math.round(stopWriteMs - startWriteMs));
+ Services.obs.notifyObservers(null, "sessionstore-state-write-complete", "");
+ this._sessionDataIsGood = true;
+ });
+ },
+
+ /**
+ * Writes the session state to a disk file, using async or sync methods
+ * @param aFile nsIFile used for saving the session
+ * @param aFileTemp nsIFile used as a temporary file in writing the data
+ * @param aBuffer UTF-8 encoded ArrayBuffer of the session state
+ * @param aAsync boolelan used to determine the method of saving the state
+ * @return Promise that resolves when the file has been written
+ */
+ _write: function ss_write(aFile, aFileTemp, aBuffer, aAsync) {
+ // Use async file writer and just return it's promise
+ if (aAsync) {
+ log("_write() writing asynchronously");
+ return OS.File.writeAtomic(aFile.path, aBuffer, { tmpPath: aFileTemp.path });
+ }
+
+ // Convert buffer to an encoded string and sync write to disk
+ let bytes = String.fromCharCode.apply(null, new Uint16Array(aBuffer));
+ let stream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
+ stream.init(aFileTemp, 0x02 | 0x08 | 0x20, 0o666, 0);
+ stream.write(bytes, bytes.length);
+ stream.close();
+ // Mimic writeAtomic behaviour when tmpPath is set and write
+ // to a temp file which is then renamed at the end.
+ aFileTemp.renameTo(null, aFile.leafName);
+ log("_write() writing synchronously");
+
+ // Return a resolved promise to make the caller happy
+ return Promise.resolve();
+ },
+
+ _updateCrashReportURL: function ss_updateCrashReportURL(aWindow) {
+ let crashReporterBuilt = "nsICrashReporter" in Ci && Services.appinfo instanceof Ci.nsICrashReporter;
+ if (!crashReporterBuilt) {
+ return;
+ }
+
+ if (!aWindow.BrowserApp.selectedBrowser) {
+ return;
+ }
+
+ try {
+ let currentURI = aWindow.BrowserApp.selectedBrowser.currentURI.clone();
+ // if the current URI contains a username/password, remove it
+ try {
+ currentURI.userPass = "";
+ } catch (ex) { } // ignore failures on about: URIs
+
+ Services.appinfo.annotateCrashReport("URL", currentURI.spec);
+ } catch (ex) {
+ // don't make noise when crashreporter is built but not enabled
+ if (ex.result != Cr.NS_ERROR_NOT_INITIALIZED) {
+ Cu.reportError("SessionStore:" + ex);
+ }
+ }
+ },
+
+ /**
+ * Determines whether a given session history entry has been added dynamically.
+ */
+ isDynamic: function(aEntry) {
+ // aEntry.isDynamicallyAdded() is true for dynamically added
+ // <iframe> and <frameset>, but also for <html> (the root of the
+ // document) so we use aEntry.parent to ensure that we're not looking
+ // at the root of the document
+ return aEntry.parent && aEntry.isDynamicallyAdded();
+ },
+
+ /**
+ * Get an object that is a serialized representation of a History entry.
+ */
+ _serializeHistoryEntry: function _serializeHistoryEntry(aEntry) {
+ let entry = { url: aEntry.URI.spec };
+
+ if (aEntry.title && aEntry.title != entry.url) {
+ entry.title = aEntry.title;
+ }
+
+ if (!(aEntry instanceof Ci.nsISHEntry)) {
+ return entry;
+ }
+
+ let cacheKey = aEntry.cacheKey;
+ if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 && cacheKey.data != 0) {
+ entry.cacheKey = cacheKey.data;
+ }
+
+ entry.ID = aEntry.ID;
+ entry.docshellID = aEntry.docshellID;
+
+ if (aEntry.referrerURI) {
+ entry.referrer = aEntry.referrerURI.spec;
+ }
+
+ if (aEntry.originalURI) {
+ entry.originalURI = aEntry.originalURI.spec;
+ }
+
+ if (aEntry.loadReplace) {
+ entry.loadReplace = aEntry.loadReplace;
+ }
+
+ if (aEntry.contentType) {
+ entry.contentType = aEntry.contentType;
+ }
+
+ if (aEntry.scrollRestorationIsManual) {
+ entry.scrollRestorationIsManual = true;
+ } else {
+ let x = {}, y = {};
+ aEntry.getScrollPosition(x, y);
+ if (x.value != 0 || y.value != 0) {
+ entry.scroll = x.value + "," + y.value;
+ }
+ }
+
+ // Collect triggeringPrincipal data for the current history entry.
+ // Please note that before Bug 1297338 there was no concept of a
+ // principalToInherit. To remain backward/forward compatible we
+ // serialize the principalToInherit as triggeringPrincipal_b64.
+ // Once principalToInherit is well established (within FF55)
+ // we can update this code, remove triggeringPrincipal_b64 and
+ // just keep triggeringPrincipal_base64 as well as
+ // principalToInherit_base64; see Bug 1301666.
+ if (aEntry.principalToInherit) {
+ try {
+ let principalToInherit = Utils.serializePrincipal(aEntry.principalToInherit);
+ if (principalToInherit) {
+ entry.triggeringPrincipal_b64 = principalToInherit;
+ entry.principalToInherit_base64 = principalToInherit;
+ }
+ } catch (e) {
+ dump(e);
+ }
+ }
+
+ if (aEntry.triggeringPrincipal) {
+ try {
+ let triggeringPrincipal = Utils.serializePrincipal(aEntry.triggeringPrincipal);
+ if (triggeringPrincipal) {
+ entry.triggeringPrincipal_base64 = triggeringPrincipal;
+ }
+ } catch (e) {
+ dump(e);
+ }
+ }
+
+ entry.docIdentifier = aEntry.BFCacheEntry.ID;
+
+ if (aEntry.stateData != null) {
+ entry.structuredCloneState = aEntry.stateData.getDataAsBase64();
+ entry.structuredCloneVersion = aEntry.stateData.formatVersion;
+ }
+
+ if (!(aEntry instanceof Ci.nsISHContainer)) {
+ return entry;
+ }
+
+ if (aEntry.childCount > 0) {
+ let children = [];
+ for (let i = 0; i < aEntry.childCount; i++) {
+ let child = aEntry.GetChildAt(i);
+
+ if (child && !this.isDynamic(child)) {
+ // don't try to restore framesets containing wyciwyg URLs (cf. bug 424689 and bug 450595)
+ if (child.URI.schemeIs("wyciwyg")) {
+ children = [];
+ break;
+ }
+ children.push(this._serializeHistoryEntry(child));
+ }
+ }
+
+ if (children.length) {
+ entry.children = children;
+ }
+ }
+
+ return entry;
+ },
+
+ _deserializeHistoryEntry: function _deserializeHistoryEntry(aEntry, aIdMap, aDocIdentMap) {
+ let shEntry = Cc["@mozilla.org/browser/session-history-entry;1"].createInstance(Ci.nsISHEntry);
+
+ shEntry.setURI(Services.io.newURI(aEntry.url, null, null));
+ shEntry.setTitle(aEntry.title || aEntry.url);
+ if (aEntry.subframe) {
+ shEntry.setIsSubFrame(aEntry.subframe || false);
+ }
+ shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory;
+ if (aEntry.contentType) {
+ shEntry.contentType = aEntry.contentType;
+ }
+ if (aEntry.referrer) {
+ shEntry.referrerURI = Services.io.newURI(aEntry.referrer, null, null);
+ }
+
+ if (aEntry.originalURI) {
+ shEntry.originalURI = Services.io.newURI(aEntry.originalURI, null, null);
+ }
+
+ if (aEntry.loadReplace) {
+ shEntry.loadReplace = aEntry.loadReplace;
+ }
+
+ if (aEntry.cacheKey) {
+ let cacheKey = Cc["@mozilla.org/supports-PRUint32;1"].createInstance(Ci.nsISupportsPRUint32);
+ cacheKey.data = aEntry.cacheKey;
+ shEntry.cacheKey = cacheKey;
+ }
+
+ if (aEntry.ID) {
+ // get a new unique ID for this frame (since the one from the last
+ // start might already be in use)
+ let id = aIdMap[aEntry.ID] || 0;
+ if (!id) {
+ for (id = Date.now(); id in aIdMap.used; id++);
+ aIdMap[aEntry.ID] = id;
+ aIdMap.used[id] = true;
+ }
+ shEntry.ID = id;
+ }
+
+ if (aEntry.docshellID) {
+ shEntry.docshellID = aEntry.docshellID;
+ }
+
+ if (aEntry.structuredCloneState && aEntry.structuredCloneVersion) {
+ shEntry.stateData =
+ Cc["@mozilla.org/docshell/structured-clone-container;1"].
+ createInstance(Ci.nsIStructuredCloneContainer);
+
+ shEntry.stateData.initFromBase64(aEntry.structuredCloneState, aEntry.structuredCloneVersion);
+ }
+
+ if (aEntry.scrollRestorationIsManual) {
+ shEntry.scrollRestorationIsManual = true;
+ } else if (aEntry.scroll) {
+ let scrollPos = aEntry.scroll.split(",");
+ scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0];
+ shEntry.setScrollPosition(scrollPos[0], scrollPos[1]);
+ }
+
+ let childDocIdents = {};
+ if (aEntry.docIdentifier) {
+ // If we have a serialized document identifier, try to find an SHEntry
+ // which matches that doc identifier and adopt that SHEntry's
+ // BFCacheEntry. If we don't find a match, insert shEntry as the match
+ // for the document identifier.
+ let matchingEntry = aDocIdentMap[aEntry.docIdentifier];
+ if (!matchingEntry) {
+ matchingEntry = {shEntry: shEntry, childDocIdents: childDocIdents};
+ aDocIdentMap[aEntry.docIdentifier] = matchingEntry;
+ } else {
+ shEntry.adoptBFCacheEntry(matchingEntry.shEntry);
+ childDocIdents = matchingEntry.childDocIdents;
+ }
+ }
+
+ // The field aEntry.owner_b64 got renamed to aEntry.triggeringPricipal_b64 in
+ // Bug 1286472. To remain backward compatible we still have to support that
+ // field for a few cycles before we can remove it within Bug 1289785.
+ if (aEntry.owner_b64) {
+ aEntry.triggeringPricipal_b64 = aEntry.owner_b64;
+ delete aEntry.owner_b64;
+ }
+
+ // Before introducing the concept of principalToInherit we only had
+ // a triggeringPrincipal within every entry which basically is the
+ // equivalent of the new principalToInherit. To avoid compatibility
+ // issues, we first check if the entry has entries for
+ // triggeringPrincipal_base64 and principalToInherit_base64. If not
+ // we fall back to using the principalToInherit (which is stored
+ // as triggeringPrincipal_b64) as the triggeringPrincipal and
+ // the principalToInherit.
+ // FF55 will remove the triggeringPrincipal_b64, see Bug 1301666.
+ if (aEntry.triggeringPrincipal_base64 || aEntry.principalToInherit_base64) {
+ if (aEntry.triggeringPrincipal_base64) {
+ shEntry.triggeringPrincipal =
+ Utils.deserializePrincipal(aEntry.triggeringPrincipal_base64);
+ }
+ if (aEntry.principalToInherit_base64) {
+ shEntry.principalToInherit =
+ Utils.deserializePrincipal(aEntry.principalToInherit_base64);
+ }
+ } else if (aEntry.triggeringPrincipal_b64) {
+ shEntry.triggeringPrincipal = Utils.deserializePrincipal(aEntry.triggeringPrincipal_b64);
+ shEntry.principalToInherit = shEntry.triggeringPrincipal;
+ }
+
+ if (aEntry.children && shEntry instanceof Ci.nsISHContainer) {
+ for (let i = 0; i < aEntry.children.length; i++) {
+ if (!aEntry.children[i].url) {
+ continue;
+ }
+
+ // We're getting sessionrestore.js files with a cycle in the
+ // doc-identifier graph, likely due to bug 698656. (That is, we have
+ // an entry where doc identifier A is an ancestor of doc identifier B,
+ // and another entry where doc identifier B is an ancestor of A.)
+ //
+ // If we were to respect these doc identifiers, we'd create a cycle in
+ // the SHEntries themselves, which causes the docshell to loop forever
+ // when it looks for the root SHEntry.
+ //
+ // So as a hack to fix this, we restrict the scope of a doc identifier
+ // to be a node's siblings and cousins, and pass childDocIdents, not
+ // aDocIdents, to _deserializeHistoryEntry. That is, we say that two
+ // SHEntries with the same doc identifier have the same document iff
+ // they have the same parent or their parents have the same document.
+
+ shEntry.AddChild(this._deserializeHistoryEntry(aEntry.children[i], aIdMap, childDocIdents), i);
+ }
+ }
+
+ return shEntry;
+ },
+
+ // This function iterates through a list of urls opening a new tab for each.
+ _openTabs: function ss_openTabs(aData) {
+ let window = Services.wm.getMostRecentWindow("navigator:browser");
+ for (let i = 0; i < aData.urls.length; i++) {
+ let url = aData.urls[i];
+ let params = {
+ selected: (i == aData.urls.length - 1),
+ isPrivate: false,
+ desktopMode: false,
+ };
+
+ let tab = window.BrowserApp.addTab(url, params);
+ }
+ },
+
+ // This function iterates through a list of tab data restoring session for each of them.
+ _restoreTabs: function ss_restoreTabs(aData) {
+ let window = Services.wm.getMostRecentWindow("navigator:browser");
+ for (let i = 0; i < aData.tabs.length; i++) {
+ let tabData = JSON.parse(aData.tabs[i]);
+ let isSelectedTab = (i == aData.tabs.length - 1);
+ let params = {
+ selected: isSelectedTab,
+ isPrivate: tabData.isPrivate,
+ desktopMode: tabData.desktopMode,
+ cancelEditMode: isSelectedTab
+ };
+
+ let tab = window.BrowserApp.addTab(tabData.entries[tabData.index - 1].url, params);
+ tab.browser.__SS_data = tabData;
+ tab.browser.__SS_extdata = tabData.extData;
+ this._restoreTab(tabData, tab.browser);
+ }
+ },
+
+ /**
+ * Don't save sensitive data if the user doesn't want to
+ * (distinguishes between encrypted and non-encrypted sites)
+ */
+ checkPrivacyLevel: function ss_checkPrivacyLevel(aURL) {
+ let isHTTPS = aURL.startsWith("https:");
+ let pref = "browser.sessionstore.privacy_level";
+ return Services.prefs.getIntPref(pref) < (isHTTPS ? PRIVACY_ENCRYPTED : PRIVACY_FULL);
+ },
+
+ /**
+ * Starts the restoration process for a browser. History is restored at this
+ * point, but text data must be delayed until the content loads.
+ */
+ _restoreTab: function ss_restoreTab(aTabData, aBrowser) {
+ // aTabData shouldn't be empty here, but if it is,
+ // _restoreHistory() will crash otherwise.
+ if (!aTabData || aTabData.entries.length == 0) {
+ Cu.reportError("SessionStore.js: Error trying to restore tab with empty tabdata");
+ return;
+ }
+ this._restoreHistory(aTabData, aBrowser.sessionHistory);
+
+ // Various bits of state can only be restored if page loading has progressed far enough:
+ // The MobileViewportManager needs to be told as early as possible about
+ // our desired zoom level so it can take it into account during the
+ // initial document resolution calculation.
+ aBrowser.__SS_restoreDataOnLocationChange = true;
+ // Restoring saved form data requires the input fields to be available,
+ // so we have to wait for the content to load.
+ aBrowser.__SS_restoreDataOnLoad = true;
+ // Restoring the scroll position depends on the document resolution having been set,
+ // which is only guaranteed to have happened *after* we receive the load event.
+ aBrowser.__SS_restoreDataOnPageshow = true;
+ },
+
+ /**
+ * Takes serialized history data and create news entries into the given
+ * nsISessionHistory object.
+ */
+ _restoreHistory: function ss_restoreHistory(aTabData, aHistory) {
+ if (aHistory.count > 0) {
+ aHistory.PurgeHistory(aHistory.count);
+ }
+ aHistory.QueryInterface(Ci.nsISHistoryInternal);
+
+ // Helper hashes for ensuring unique frame IDs and unique document
+ // identifiers.
+ let idMap = { used: {} };
+ let docIdentMap = {};
+
+ for (let i = 0; i < aTabData.entries.length; i++) {
+ if (!aTabData.entries[i].url) {
+ continue;
+ }
+ aHistory.addEntry(this._deserializeHistoryEntry(aTabData.entries[i], idMap, docIdentMap), true);
+ }
+
+ // We need to force set the active history item and cause it to reload since
+ // we stop the load above
+ let activeIndex = (aTabData.index || aTabData.entries.length) - 1;
+ aHistory.getEntryAtIndex(activeIndex, true);
+
+ try {
+ aHistory.QueryInterface(Ci.nsISHistory).reloadCurrentEntry();
+ } catch (e) {
+ // This will throw if the current entry is an error page.
+ }
+ },
+
+ /**
+ * Takes serialized form text data and restores it into the given browser.
+ */
+ _restoreTextData: function ss_restoreTextData(aFormData, aBrowser) {
+ if (aFormData) {
+ log("_restoreTextData()");
+ FormData.restoreTree(aBrowser.contentWindow, aFormData);
+ }
+ },
+
+ /**
+ * Restores the zoom level of the window. This needs to be called before
+ * first paint/load (whichever comes first) to take any effect.
+ */
+ _restoreZoom: function ss_restoreZoom(aScrollData, aBrowser) {
+ if (aScrollData && aScrollData.zoom && aScrollData.zoom.displaySize) {
+ log("_restoreZoom(), resolution: " + aScrollData.zoom.resolution +
+ ", old displayWidth: " + aScrollData.zoom.displaySize.width);
+
+ let utils = aBrowser.contentWindow.QueryInterface(
+ Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ // Restore zoom level.
+ utils.setRestoreResolution(aScrollData.zoom.resolution,
+ aScrollData.zoom.displaySize.width,
+ aScrollData.zoom.displaySize.height);
+ }
+ },
+
+ /**
+ * Takes serialized scroll positions and restores them into the given browser.
+ */
+ _restoreScrollPosition: function ss_restoreScrollPosition(aScrollData, aBrowser) {
+ if (aScrollData) {
+ log("_restoreScrollPosition()");
+ ScrollPosition.restoreTree(aBrowser.contentWindow, aScrollData);
+ }
+ },
+
+ getBrowserState: function ss_getBrowserState() {
+ return this._getCurrentState();
+ },
+
+ _restoreWindow: function ss_restoreWindow(aData) {
+ let state;
+ try {
+ state = JSON.parse(aData);
+ } catch (e) {
+ throw "Invalid session JSON: " + aData;
+ }
+
+ // To do a restore, we must have at least one window with one tab
+ if (!state || state.windows.length == 0 || !state.windows[0].tabs || state.windows[0].tabs.length == 0) {
+ throw "Invalid session JSON: " + aData;
+ }
+
+ let window = Services.wm.getMostRecentWindow("navigator:browser");
+
+ let tabs = state.windows[0].tabs;
+ let selected = state.windows[0].selected;
+ log("_restoreWindow() selected tab in aData is " + selected + " of " + tabs.length)
+ if (selected == null || selected > tabs.length) { // Clamp the selected index if it's bogus
+ log("_restoreWindow() resetting selected tab");
+ selected = 1;
+ }
+ log("restoreWindow() window.BrowserApp.selectedTab is " + window.BrowserApp.selectedTab.id);
+
+ for (let i = 0; i < tabs.length; i++) {
+ let tabData = tabs[i];
+ let entry = tabData.entries[tabData.index - 1];
+
+ // Use stubbed tab if we've already created it; otherwise, make a new tab
+ let tab;
+ if (tabData.tabId == null) {
+ let params = {
+ selected: (selected == i+1),
+ delayLoad: true,
+ title: entry.title,
+ desktopMode: (tabData.desktopMode == true),
+ isPrivate: (tabData.isPrivate == true)
+ };
+ tab = window.BrowserApp.addTab(entry.url, params);
+ } else {
+ tab = window.BrowserApp.getTabForId(tabData.tabId);
+ delete tabData.tabId;
+
+ // Don't restore tab if user has closed it
+ if (tab == null) {
+ continue;
+ }
+ }
+
+ tab.browser.__SS_data = tabData;
+ tab.browser.__SS_extdata = tabData.extData;
+
+ if (window.BrowserApp.selectedTab == tab) {
+ this._restoreTab(tabData, tab.browser);
+
+ // We can now lift the general ban on tab data capturing,
+ // but we still need to protect the foreground tab until we're
+ // sure it's actually reloading after history restoring has finished.
+ tab.browser.__SS_restoreReloadPending = true;
+ this._startupRestoreFinished = true;
+ log("startupRestoreFinished = true");
+
+ delete tab.browser.__SS_restore;
+ tab.browser.removeAttribute("pending");
+ } else {
+ // Mark the browser for delay loading
+ tab.browser.__SS_restore = true;
+ tab.browser.setAttribute("pending", "true");
+ }
+ }
+
+ // Restore the closed tabs array on the current window.
+ if (state.windows[0].closedTabs) {
+ this._windows[window.__SSID].closedTabs = state.windows[0].closedTabs;
+ log("_restoreWindow() loaded " + state.windows[0].closedTabs.length + " closed tabs");
+ }
+ },
+
+ getClosedTabCount: function ss_getClosedTabCount(aWindow) {
+ if (!aWindow || !aWindow.__SSID || !this._windows[aWindow.__SSID]) {
+ return 0; // not a browser window, or not otherwise tracked by SS.
+ }
+
+ return this._windows[aWindow.__SSID].closedTabs.length;
+ },
+
+ getClosedTabs: function ss_getClosedTabs(aWindow) {
+ if (!aWindow.__SSID) {
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ return this._windows[aWindow.__SSID].closedTabs;
+ },
+
+ undoCloseTab: function ss_undoCloseTab(aWindow, aCloseTabData) {
+ if (!aWindow.__SSID) {
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ let closedTabs = this._windows[aWindow.__SSID].closedTabs;
+ if (!closedTabs) {
+ return null;
+ }
+
+ // If the tab data is in the closedTabs array, remove it.
+ closedTabs.find(function (tabData, i) {
+ if (tabData == aCloseTabData) {
+ closedTabs.splice(i, 1);
+ return true;
+ }
+ });
+
+ // create a new tab and bring to front
+ let params = {
+ selected: true,
+ isPrivate: aCloseTabData.isPrivate,
+ desktopMode: aCloseTabData.desktopMode,
+ tabIndex: this._lastClosedTabIndex
+ };
+ let tab = aWindow.BrowserApp.addTab(aCloseTabData.entries[aCloseTabData.index - 1].url, params);
+ tab.browser.__SS_data = aCloseTabData;
+ tab.browser.__SS_extdata = aCloseTabData.extData;
+ this._restoreTab(aCloseTabData, tab.browser);
+
+ this._lastClosedTabIndex = -1;
+
+ if (this._notifyClosedTabs) {
+ this._sendClosedTabsToJava(aWindow);
+ }
+
+ return tab.browser;
+ },
+
+ forgetClosedTab: function ss_forgetClosedTab(aWindow, aIndex) {
+ if (!aWindow.__SSID) {
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ let closedTabs = this._windows[aWindow.__SSID].closedTabs;
+
+ // default to the most-recently closed tab
+ aIndex = aIndex || 0;
+ if (!(aIndex in closedTabs)) {
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ // remove closed tab from the array
+ closedTabs.splice(aIndex, 1);
+
+ // Forget the last closed tab index if we're forgetting the last closed tab.
+ if (aIndex == 0) {
+ this._lastClosedTabIndex = -1;
+ }
+ if (this._notifyClosedTabs) {
+ this._sendClosedTabsToJava(aWindow);
+ }
+ },
+
+ _sendClosedTabsToJava: function ss_sendClosedTabsToJava(aWindow) {
+ if (!aWindow.__SSID) {
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ let closedTabs = this._windows[aWindow.__SSID].closedTabs;
+ let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(aWindow.BrowserApp.selectedBrowser);
+
+ let tabs = closedTabs
+ .filter(tab => tab.isPrivate == isPrivate)
+ .map(function (tab) {
+ // Get the url and title for the last entry in the session history.
+ let lastEntry = tab.entries[tab.entries.length - 1];
+ return {
+ url: lastEntry.url,
+ title: lastEntry.title || "",
+ data: tab
+ };
+ });
+
+ log("sending " + tabs.length + " closed tabs to Java");
+ Messaging.sendRequest({
+ type: "ClosedTabs:Data",
+ tabs: tabs
+ });
+ },
+
+ getTabValue: function ss_getTabValue(aTab, aKey) {
+ let browser = aTab.browser;
+ let data = browser.__SS_extdata || {};
+ return data[aKey] || "";
+ },
+
+ setTabValue: function ss_setTabValue(aTab, aKey, aStringValue) {
+ let browser = aTab.browser;
+ if (!browser.__SS_extdata) {
+ browser.__SS_extdata = {};
+ }
+ browser.__SS_extdata[aKey] = aStringValue;
+ this.saveStateDelayed();
+ },
+
+ deleteTabValue: function ss_deleteTabValue(aTab, aKey) {
+ let browser = aTab.browser;
+ if (browser.__SS_extdata && aKey in browser.__SS_extdata) {
+ delete browser.__SS_extdata[aKey];
+ this.saveStateDelayed();
+ }
+ },
+
+ restoreLastSession: Task.async(function* (aSessionString) {
+ let notifyMessage = "";
+
+ try {
+ this._restoreWindow(aSessionString);
+ } catch (e) {
+ Cu.reportError("SessionStore: " + e);
+ notifyMessage = "fail";
+ }
+
+ Services.obs.notifyObservers(null, "sessionstore-windows-restored", notifyMessage);
+ }),
+
+ removeWindow: function ss_removeWindow(aWindow) {
+ if (!aWindow || !aWindow.__SSID || !this._windows[aWindow.__SSID]) {
+ return;
+ }
+
+ delete this._windows[aWindow.__SSID];
+ delete aWindow.__SSID;
+
+ if (this._loadState == STATE_RUNNING) {
+ // Save the purged state immediately
+ this.saveState();
+ } else if (this._loadState <= STATE_QUITTING) {
+ this.saveStateDelayed();
+ }
+ }
+
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStore]);
diff --git a/mobile/android/components/SiteSpecificUserAgent.js b/mobile/android/components/SiteSpecificUserAgent.js
new file mode 100644
index 0000000000..f95d7ab163
--- /dev/null
+++ b/mobile/android/components/SiteSpecificUserAgent.js
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/UserAgentOverrides.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const DEFAULT_UA = Cc["@mozilla.org/network/protocol;1?name=http"]
+ .getService(Ci.nsIHttpProtocolHandler)
+ .userAgent;
+
+function SiteSpecificUserAgent() {}
+
+SiteSpecificUserAgent.prototype = {
+ getUserAgentForURIAndWindow: function ssua_getUserAgentForURIAndWindow(aURI, aWindow) {
+ let UA;
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+ if (win && win.DesktopUserAgent) {
+ UA = win.DesktopUserAgent.getUserAgentForWindow(aWindow);
+ }
+ return UA || UserAgentOverrides.getOverrideForURI(aURI) || DEFAULT_UA;
+ },
+
+ classID: Components.ID("{d5234c9d-0ee2-4b3c-9da3-18be9e5cf7e6}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISiteSpecificUserAgent])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SiteSpecificUserAgent]);
diff --git a/mobile/android/components/Snippets.js b/mobile/android/components/Snippets.js
new file mode 100644
index 0000000000..92639236f2
--- /dev/null
+++ b/mobile/android/components/Snippets.js
@@ -0,0 +1,446 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Accounts.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Home", "resource://gre/modules/Home.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", "resource://gre/modules/UITelemetry.jsm");
+
+
+XPCOMUtils.defineLazyGetter(this, "gEncoder", function() { return new gChromeWin.TextEncoder(); });
+XPCOMUtils.defineLazyGetter(this, "gDecoder", function() { return new gChromeWin.TextDecoder(); });
+
+// URL to fetch snippets, in the urlFormatter service format.
+const SNIPPETS_UPDATE_URL_PREF = "browser.snippets.updateUrl";
+
+// URL to send stats data to metrics.
+const SNIPPETS_STATS_URL_PREF = "browser.snippets.statsUrl";
+
+// URL to fetch country code, a value that's cached and refreshed once per month.
+const SNIPPETS_GEO_URL_PREF = "browser.snippets.geoUrl";
+
+// Timestamp when we last updated the user's country code.
+const SNIPPETS_GEO_LAST_UPDATE_PREF = "browser.snippets.geoLastUpdate";
+
+// Pref where we'll cache the user's country.
+const SNIPPETS_COUNTRY_CODE_PREF = "browser.snippets.countryCode";
+
+// Pref where we store an array IDs of snippets that should not be shown again
+const SNIPPETS_REMOVED_IDS_PREF = "browser.snippets.removedIds";
+
+// How frequently we update the user's country code from the server (30 days).
+const SNIPPETS_GEO_UPDATE_INTERVAL_MS = 86400000*30;
+
+// Should be bumped up if the snippets content format changes.
+const SNIPPETS_VERSION = 1;
+
+XPCOMUtils.defineLazyGetter(this, "gSnippetsURL", function() {
+ let updateURL = Services.prefs.getCharPref(SNIPPETS_UPDATE_URL_PREF).replace("%SNIPPETS_VERSION%", SNIPPETS_VERSION);
+ return Services.urlFormatter.formatURL(updateURL);
+});
+
+// Where we cache snippets data
+XPCOMUtils.defineLazyGetter(this, "gSnippetsPath", function() {
+ return OS.Path.join(OS.Constants.Path.profileDir, "snippets.json");
+});
+
+XPCOMUtils.defineLazyGetter(this, "gStatsURL", function() {
+ return Services.prefs.getCharPref(SNIPPETS_STATS_URL_PREF);
+});
+
+// Where we store stats about which snippets have been shown
+XPCOMUtils.defineLazyGetter(this, "gStatsPath", function() {
+ return OS.Path.join(OS.Constants.Path.profileDir, "snippets-stats.txt");
+});
+
+XPCOMUtils.defineLazyGetter(this, "gGeoURL", function() {
+ return Services.prefs.getCharPref(SNIPPETS_GEO_URL_PREF);
+});
+
+XPCOMUtils.defineLazyGetter(this, "gCountryCode", function() {
+ try {
+ return Services.prefs.getCharPref(SNIPPETS_COUNTRY_CODE_PREF);
+ } catch (e) {
+ // Return an empty string if the country code pref isn't set yet.
+ return "";
+ }
+});
+
+XPCOMUtils.defineLazyGetter(this, "gChromeWin", function() {
+ return Services.wm.getMostRecentWindow("navigator:browser");
+});
+
+/**
+ * Updates snippet data and country code (if necessary).
+ */
+function update() {
+ // Check to see if we should update the user's country code from the geo server.
+ let lastUpdate = 0;
+ try {
+ lastUpdate = parseFloat(Services.prefs.getCharPref(SNIPPETS_GEO_LAST_UPDATE_PREF));
+ } catch (e) {}
+
+ if (Date.now() - lastUpdate > SNIPPETS_GEO_UPDATE_INTERVAL_MS) {
+ // We should update the snippets after updating the country code,
+ // so that we can filter snippets to add to the banner.
+ updateCountryCode(updateSnippets);
+ } else {
+ updateSnippets();
+ }
+}
+
+/**
+ * Fetches the user's country code from the geo server and stores the value in a pref.
+ *
+ * @param callback function called once country code is updated
+ */
+function updateCountryCode(callback) {
+ _httpGetRequest(gGeoURL, function(responseText) {
+ // Store the country code in a pref.
+ let data = JSON.parse(responseText);
+ Services.prefs.setCharPref(SNIPPETS_COUNTRY_CODE_PREF, data.country_code);
+
+ // Set last update time.
+ Services.prefs.setCharPref(SNIPPETS_GEO_LAST_UPDATE_PREF, Date.now());
+
+ callback();
+ });
+}
+
+/**
+ * Loads snippets from snippets server, caches the response, and
+ * updates the home banner with the new set of snippets.
+ */
+function updateSnippets() {
+ _httpGetRequest(gSnippetsURL, function(responseText) {
+ try {
+ let messages = JSON.parse(responseText);
+ updateBanner(messages);
+
+ // Only cache the response if it is valid JSON.
+ cacheSnippets(responseText);
+ } catch (e) {
+ Cu.reportError("Error parsing snippets responseText: " + e);
+ }
+ });
+}
+
+/**
+ * Caches snippets server response text to `snippets.json` in profile directory.
+ *
+ * @param response responseText returned from snippets server
+ */
+function cacheSnippets(response) {
+ let data = gEncoder.encode(response);
+ let promise = OS.File.writeAtomic(gSnippetsPath, data, { tmpPath: gSnippetsPath + ".tmp" });
+ promise.then(null, e => Cu.reportError("Error caching snippets: " + e));
+}
+
+/**
+ * Loads snippets from cached `snippets.json`.
+ */
+function loadSnippetsFromCache() {
+ let promise = OS.File.read(gSnippetsPath);
+ promise.then(array => {
+ let messages = JSON.parse(gDecoder.decode(array));
+ updateBanner(messages);
+ }, e => {
+ if (e instanceof OS.File.Error && e.becauseNoSuchFile) {
+ Services.console.logStringMessage("Couldn't show snippets because cache does not exist yet.");
+ } else {
+ Cu.reportError("Error loading snippets from cache: " + e);
+ }
+ });
+}
+
+// Array of the message ids added to the home banner, used to remove
+// older set of snippets when new ones are available.
+var gMessageIds = [];
+
+/**
+ * Updates set of snippets in the home banner message rotation.
+ *
+ * @param messages JSON array of message data JSON objects.
+ * Each message object should have the following properties:
+ * - id (?): Unique identifier for this snippets message
+ * - text (string): Text to show as banner message
+ * - url (string): URL to open when banner is clicked
+ * - icon (data URI): Icon to appear in banner
+ * - countries (list of strings): Country codes for where this message should be shown (e.g. ["US", "GR"])
+ */
+function updateBanner(messages) {
+ // Remove the current messages, if there are any.
+ gMessageIds.forEach(function(id) {
+ Home.banner.remove(id);
+ })
+ gMessageIds = [];
+
+ try {
+ let removedSnippetIds = JSON.parse(Services.prefs.getCharPref(SNIPPETS_REMOVED_IDS_PREF));
+ messages = messages.filter(function(message) {
+ // Only include the snippet if it has not been previously removed.
+ return removedSnippetIds.indexOf(message.id) === -1;
+ });
+ } catch (e) {
+ // If the pref doesn't exist, there aren't any snippets to filter out.
+ }
+
+ messages.forEach(function(message) {
+ // Don't add this message to the banner if it's not supposed to be shown in this country.
+ if ("countries" in message && message.countries.indexOf(gCountryCode) === -1) {
+ return;
+ }
+
+ let id = Home.banner.add({
+ text: message.text,
+ icon: message.icon,
+ weight: message.weight,
+ onclick: function() {
+ gChromeWin.BrowserApp.loadURI(message.url);
+ removeSnippet(id, message.id);
+ UITelemetry.addEvent("action.1", "banner", null, message.id);
+ },
+ ondismiss: function() {
+ removeSnippet(id, message.id);
+ UITelemetry.addEvent("cancel.1", "banner", null, message.id);
+ },
+ onshown: function() {
+ // 10% of the time, record the snippet id and a timestamp
+ if (Math.random() < .1) {
+ writeStat(message.id, new Date().toISOString());
+ }
+ }
+ });
+ // Keep track of the message we added so that we can remove it later.
+ gMessageIds.push(id);
+ });
+}
+
+/**
+ * Removes a snippet message from the home banner rotation, and stores its
+ * snippet id in a pref so we'll never show it again.
+ *
+ * @param messageId unique id for home banner message, returned from Home.banner API
+ * @param snippetId unique id for snippet, sent from snippets server
+ */
+function removeSnippet(messageId, snippetId) {
+ // Remove the message from the home banner rotation.
+ Home.banner.remove(messageId);
+
+ // Remove the message from the stored message ids.
+ gMessageIds.splice(gMessageIds.indexOf(messageId), 1);
+
+ let removedSnippetIds;
+ try {
+ removedSnippetIds = JSON.parse(Services.prefs.getCharPref(SNIPPETS_REMOVED_IDS_PREF));
+ } catch (e) {
+ removedSnippetIds = [];
+ }
+
+ removedSnippetIds.push(snippetId);
+ Services.prefs.setCharPref(SNIPPETS_REMOVED_IDS_PREF, JSON.stringify(removedSnippetIds));
+}
+
+/**
+ * Appends snippet id and timestamp to the end of `snippets-stats.txt`.
+ *
+ * @param snippetId unique id for snippet, sent from snippets server
+ * @param timestamp in ISO8601
+ */
+function writeStat(snippetId, timestamp) {
+ let data = gEncoder.encode(snippetId + "," + timestamp + ";");
+
+ Task.spawn(function() {
+ try {
+ let file = yield OS.File.open(gStatsPath, { append: true, write: true });
+ try {
+ yield file.write(data);
+ } finally {
+ yield file.close();
+ }
+ } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
+ // If the file doesn't exist yet, create it.
+ yield OS.File.writeAtomic(gStatsPath, data, { tmpPath: gStatsPath + ".tmp" });
+ }
+ }).then(null, e => Cu.reportError("Error writing snippets stats: " + e));
+}
+
+/**
+ * Reads snippets stats data from `snippets-stats.txt` and sends the data to metrics.
+ */
+function sendStats() {
+ let promise = OS.File.read(gStatsPath);
+ promise.then(array => sendStatsRequest(gDecoder.decode(array)), e => {
+ if (e instanceof OS.File.Error && e.becauseNoSuchFile) {
+ // If the file doesn't exist, there aren't any stats to send.
+ } else {
+ Cu.reportError("Error eading snippets stats: " + e);
+ }
+ });
+}
+
+/**
+ * Sends stats to metrics about which snippets have been shown.
+ * Appends snippet ids and timestamps as parameters to a GET request.
+ * e.g. https://snippets-stats.mozilla.org/mobile?s1=3825&t1=2013-11-17T18:27Z&s2=6326&t2=2013-11-18T18:27Z
+ *
+ * @param data contents of stats data file
+ */
+function sendStatsRequest(data) {
+ let params = [];
+ let stats = data.split(";");
+
+ // The last item in the array will be an empty string, so stop before then.
+ for (let i = 0; i < stats.length - 1; i++) {
+ let stat = stats[i].split(",");
+ params.push("s" + i + "=" + encodeURIComponent(stat[0]));
+ params.push("t" + i + "=" + encodeURIComponent(stat[1]));
+ }
+
+ let url = gStatsURL + "?" + params.join("&");
+
+ // Remove the file after succesfully sending the data.
+ _httpGetRequest(url, removeStats);
+}
+
+/**
+ * Removes text file where we store snippets stats.
+ */
+function removeStats() {
+ let promise = OS.File.remove(gStatsPath);
+ promise.then(null, e => Cu.reportError("Error removing snippets stats: " + e));
+}
+
+/**
+ * Helper function to make HTTP GET requests.
+ *
+ * @param url where we send the request
+ * @param callback function that is called with the xhr responseText
+ */
+function _httpGetRequest(url, callback) {
+ let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
+ try {
+ xhr.open("GET", url, true);
+ } catch (e) {
+ Cu.reportError("Error opening request to " + url + ": " + e);
+ return;
+ }
+ xhr.onerror = function onerror(e) {
+ Cu.reportError("Error making request to " + url + ": " + e.error);
+ }
+ xhr.onload = function onload(event) {
+ if (xhr.status !== 200) {
+ Cu.reportError("Request to " + url + " returned status " + xhr.status);
+ return;
+ }
+ if (callback) {
+ callback(xhr.responseText);
+ }
+ }
+ xhr.send(null);
+}
+
+function loadSyncPromoBanner() {
+ Accounts.anySyncAccountsExist().then(
+ (exist) => {
+ // Don't show the banner if sync accounts exist.
+ if (exist) {
+ return;
+ }
+
+ let stringBundle = Services.strings.createBundle("chrome://browser/locale/sync.properties");
+ let text = stringBundle.GetStringFromName("promoBanner.message.text");
+ let link = stringBundle.GetStringFromName("promoBanner.message.link");
+
+ let id = Home.banner.add({
+ text: text + "<a href=\"#\">" + link + "</a>",
+ icon: "drawable://sync_promo",
+ onclick: function() {
+ // Remove the message, so that it won't show again for the rest of the app lifetime.
+ Home.banner.remove(id);
+ Accounts.launchSetup();
+
+ UITelemetry.addEvent("action.1", "banner", null, "syncpromo");
+ },
+ ondismiss: function() {
+ // Remove the sync promo message from the banner and never try to show it again.
+ Home.banner.remove(id);
+ Services.prefs.setBoolPref("browser.snippets.syncPromo.enabled", false);
+
+ UITelemetry.addEvent("cancel.1", "banner", null, "syncpromo");
+ }
+ });
+ },
+ (err) => {
+ Cu.reportError("Error checking whether sync account exists: " + err);
+ }
+ );
+}
+
+function loadHomePanelsBanner() {
+ let stringBundle = Services.strings.createBundle("chrome://browser/locale/aboutHome.properties");
+ let text = stringBundle.GetStringFromName("banner.firstrunHomepage.text");
+
+ let id = Home.banner.add({
+ text: text,
+ icon: "drawable://homepage_banner_firstrun",
+ onclick: function() {
+ // Remove the message, so that it won't show again for the rest of the app lifetime.
+ Home.banner.remove(id);
+ // User has interacted with this snippet so don't show it again.
+ Services.prefs.setBoolPref("browser.snippets.firstrunHomepage.enabled", false);
+
+ UITelemetry.addEvent("action.1", "banner", null, "firstrun-homepage");
+ },
+ ondismiss: function() {
+ Home.banner.remove(id);
+ Services.prefs.setBoolPref("browser.snippets.firstrunHomepage.enabled", false);
+
+ UITelemetry.addEvent("cancel.1", "banner", null, "firstrun-homepage");
+ }
+ });
+}
+
+function Snippets() {}
+
+Snippets.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsITimerCallback]),
+ classID: Components.ID("{a78d7e59-b558-4321-a3d6-dffe2f1e76dd}"),
+
+ observe: function(subject, topic, data) {
+ switch(topic) {
+ case "browser-delayed-startup-finished":
+ // Add snippets to be cycled through.
+ if (Services.prefs.getBoolPref("browser.snippets.firstrunHomepage.enabled")) {
+ loadHomePanelsBanner();
+ }
+
+ if (Services.prefs.getBoolPref("browser.snippets.syncPromo.enabled")) {
+ loadSyncPromoBanner();
+ }
+
+ if (Services.prefs.getBoolPref("browser.snippets.enabled")) {
+ loadSnippetsFromCache();
+ }
+ break;
+ }
+ },
+
+ // By default, this timer fires once every 24 hours. See the "browser.snippets.updateInterval" pref.
+ notify: function(timer) {
+ if (!Services.prefs.getBoolPref("browser.snippets.enabled")) {
+ return;
+ }
+ update();
+ sendStats();
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Snippets]);
diff --git a/mobile/android/components/TabSource.js b/mobile/android/components/TabSource.js
new file mode 100644
index 0000000000..c35a54438a
--- /dev/null
+++ b/mobile/android/components/TabSource.js
@@ -0,0 +1,91 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict"
+
+const { classes: Cc, interfaces: Ci, manager: Cm, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
+ "resource://gre/modules/Prompt.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging",
+ "resource://gre/modules/Messaging.jsm");
+
+function TabSource() {
+}
+
+TabSource.prototype = {
+ classID: Components.ID("{5850c76e-b916-4218-b99a-31f004e0a7e7}"),
+ classDescription: "Fennec Tab Source",
+ contractID: "@mozilla.org/tab-source-service;1",
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITabSource]),
+
+ getTabToStream: function() {
+ let app = Services.wm.getMostRecentWindow("navigator:browser").BrowserApp;
+ let tabs = app.tabs;
+ if (tabs == null || tabs.length == 0) {
+ Services.console.logStringMessage("ERROR: No tabs");
+ return null;
+ }
+
+ let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+ let title = bundle.GetStringFromName("tabshare.title")
+
+ let prompt = new Prompt({
+ title: title,
+ window: null
+ }).setSingleChoiceItems(tabs.map(function(tab) {
+ let label;
+ if (tab.browser.contentTitle)
+ label = tab.browser.contentTitle;
+ else if (tab.browser.contentURI)
+ label = tab.browser.contentURI.spec;
+ else
+ label = tab.originalURI.spec;
+ return { label: label,
+ icon: "thumbnail:" + tab.id }
+ }));
+
+ let result = null;
+ prompt.show(function(data) {
+ result = data.button;
+ });
+
+ // Spin this thread while we wait for a result.
+ let thread = Services.tm.currentThread;
+ while (result == null) {
+ thread.processNextEvent(true);
+ }
+
+ if (result == -1) {
+ return null;
+ }
+ return tabs[result].browser.contentWindow;
+ },
+
+ notifyStreamStart: function(window) {
+ let app = Services.wm.getMostRecentWindow("navigator:browser").BrowserApp;
+ let tabs = app.tabs;
+ for (var i in tabs) {
+ if (tabs[i].browser.contentWindow == window) {
+ Messaging.sendRequest({ type: "Tab:StreamStart", tabID: tabs[i].id });
+ }
+ }
+ },
+
+ notifyStreamStop: function(window) {
+ let app = Services.wm.getMostRecentWindow("navigator:browser").BrowserApp;
+ let tabs = app.tabs;
+ for (let i in tabs) {
+ if (tabs[i].browser.contentWindow == window) {
+ Messaging.sendRequest({ type: "Tab:StreamStop", tabID: tabs[i].id });
+ }
+ }
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TabSource]);
diff --git a/mobile/android/components/XPIDialogService.js b/mobile/android/components/XPIDialogService.js
new file mode 100644
index 0000000000..2a33d4ddff
--- /dev/null
+++ b/mobile/android/components/XPIDialogService.js
@@ -0,0 +1,49 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
+
+// -----------------------------------------------------------------------
+// Web Install Prompt service
+// -----------------------------------------------------------------------
+
+function WebInstallPrompt() { }
+
+WebInstallPrompt.prototype = {
+ classID: Components.ID("{c1242012-27d8-477e-a0f1-0b098ffc329b}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallPrompt]),
+
+ confirm: function(aBrowser, aURL, aInstalls) {
+ let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+
+ let prompt = Services.prompt;
+ let flags = prompt.BUTTON_POS_0 * prompt.BUTTON_TITLE_IS_STRING + prompt.BUTTON_POS_1 * prompt.BUTTON_TITLE_CANCEL;
+ let title = bundle.GetStringFromName("addonsConfirmInstall.title");
+ let button = bundle.GetStringFromName("addonsConfirmInstall.install");
+
+ aInstalls.forEach(function(install) {
+ let message;
+ if (install.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) {
+ title = bundle.GetStringFromName("addonsConfirmInstallUnsigned.title")
+ message = bundle.GetStringFromName("addonsConfirmInstallUnsigned.message") + "\n\n" + install.name;
+ } else {
+ message = install.name;
+ }
+
+ let result = (prompt.confirmEx(aBrowser.contentWindow, title, message, flags, button, null, null, null, {value: false}) == 0);
+ if (result)
+ install.install();
+ else
+ install.cancel();
+ });
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WebInstallPrompt]);
diff --git a/mobile/android/components/build/moz.build b/mobile/android/components/build/moz.build
new file mode 100644
index 0000000000..7a5c439e72
--- /dev/null
+++ b/mobile/android/components/build/moz.build
@@ -0,0 +1,31 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPIDL_SOURCES += [
+ 'nsIShellService.idl',
+]
+
+XPIDL_MODULE = 'browsercomps'
+
+EXPORTS += [
+ 'nsBrowserComponents.h',
+]
+
+SOURCES += [
+ 'nsBrowserModule.cpp',
+ 'nsShellService.cpp',
+]
+
+if CONFIG['MOZ_ANDROID_HISTORY']:
+ SOURCES += [
+ 'nsAndroidHistory.cpp',
+ ]
+ LOCAL_INCLUDES += [
+ '/docshell/base',
+ '/dom/base',
+ ]
+
+FINAL_LIBRARY = 'xul'
diff --git a/mobile/android/components/build/nsAndroidHistory.cpp b/mobile/android/components/build/nsAndroidHistory.cpp
new file mode 100644
index 0000000000..2610781c07
--- /dev/null
+++ b/mobile/android/components/build/nsAndroidHistory.cpp
@@ -0,0 +1,395 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsThreadUtils.h"
+#include "nsAndroidHistory.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIURI.h"
+#include "nsIObserverService.h"
+#include "GeneratedJNIWrappers.h"
+#include "Link.h"
+
+#include "mozilla/Services.h"
+#include "mozilla/Preferences.h"
+
+#define NS_LINK_VISITED_EVENT_TOPIC "link-visited"
+
+// We copy Places here.
+// Note that we don't yet observe this pref at runtime.
+#define PREF_HISTORY_ENABLED "places.history.enabled"
+
+// Time we wait to see if a pending visit is really a redirect
+#define PENDING_REDIRECT_TIMEOUT 3000
+
+using namespace mozilla;
+using mozilla::dom::Link;
+
+NS_IMPL_ISUPPORTS(nsAndroidHistory, IHistory, nsIRunnable, nsITimerCallback)
+
+nsAndroidHistory* nsAndroidHistory::sHistory = nullptr;
+
+/*static*/
+nsAndroidHistory*
+nsAndroidHistory::GetSingleton()
+{
+ if (!sHistory) {
+ sHistory = new nsAndroidHistory();
+ NS_ENSURE_TRUE(sHistory, nullptr);
+ }
+
+ NS_ADDREF(sHistory);
+ return sHistory;
+}
+
+nsAndroidHistory::nsAndroidHistory()
+ : mHistoryEnabled(true)
+{
+ LoadPrefs();
+
+ mTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
+}
+
+NS_IMETHODIMP
+nsAndroidHistory::RegisterVisitedCallback(nsIURI *aURI, Link *aContent)
+{
+ if (!aContent || !aURI)
+ return NS_OK;
+
+ // Silently return if URI is something we would never add to DB.
+ bool canAdd;
+ nsresult rv = CanAddURI(aURI, &canAdd);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!canAdd) {
+ return NS_OK;
+ }
+
+ nsAutoCString uri;
+ rv = aURI->GetSpec(uri);
+ if (NS_FAILED(rv)) return rv;
+ NS_ConvertUTF8toUTF16 uriString(uri);
+
+ nsTArray<Link*>* list = mListeners.Get(uriString);
+ if (! list) {
+ list = new nsTArray<Link*>();
+ mListeners.Put(uriString, list);
+ }
+ list->AppendElement(aContent);
+
+ if (jni::IsAvailable()) {
+ java::GeckoAppShell::CheckURIVisited(uriString);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAndroidHistory::UnregisterVisitedCallback(nsIURI *aURI, Link *aContent)
+{
+ if (!aContent || !aURI)
+ return NS_OK;
+
+ nsAutoCString uri;
+ nsresult rv = aURI->GetSpec(uri);
+ if (NS_FAILED(rv)) return rv;
+ NS_ConvertUTF8toUTF16 uriString(uri);
+
+ nsTArray<Link*>* list = mListeners.Get(uriString);
+ if (! list)
+ return NS_OK;
+
+ list->RemoveElement(aContent);
+ if (list->IsEmpty()) {
+ mListeners.Remove(uriString);
+ delete list;
+ }
+ return NS_OK;
+}
+
+void
+nsAndroidHistory::AppendToRecentlyVisitedURIs(nsIURI* aURI) {
+ if (mRecentlyVisitedURIs.Length() < RECENTLY_VISITED_URI_SIZE) {
+ // Append a new element while the array is not full.
+ mRecentlyVisitedURIs.AppendElement(aURI);
+ } else {
+ // Otherwise, replace the oldest member.
+ mRecentlyVisitedURIsNextIndex %= RECENTLY_VISITED_URI_SIZE;
+ mRecentlyVisitedURIs.ElementAt(mRecentlyVisitedURIsNextIndex) = aURI;
+ mRecentlyVisitedURIsNextIndex++;
+ }
+}
+
+bool
+nsAndroidHistory::ShouldRecordHistory() {
+ return mHistoryEnabled;
+}
+
+void
+nsAndroidHistory::LoadPrefs() {
+ mHistoryEnabled = Preferences::GetBool(PREF_HISTORY_ENABLED, true);
+}
+
+inline bool
+nsAndroidHistory::IsRecentlyVisitedURI(nsIURI* aURI) {
+ bool equals = false;
+ RecentlyVisitedArray::index_type i;
+ RecentlyVisitedArray::size_type length = mRecentlyVisitedURIs.Length();
+ for (i = 0; i < length && !equals; ++i) {
+ aURI->Equals(mRecentlyVisitedURIs.ElementAt(i), &equals);
+ }
+ return equals;
+}
+
+void
+nsAndroidHistory::AppendToEmbedURIs(nsIURI* aURI) {
+ if (mEmbedURIs.Length() < EMBED_URI_SIZE) {
+ // Append a new element while the array is not full.
+ mEmbedURIs.AppendElement(aURI);
+ } else {
+ // Otherwise, replace the oldest member.
+ mEmbedURIsNextIndex %= EMBED_URI_SIZE;
+ mEmbedURIs.ElementAt(mEmbedURIsNextIndex) = aURI;
+ mEmbedURIsNextIndex++;
+ }
+}
+
+inline bool
+nsAndroidHistory::IsEmbedURI(nsIURI* aURI) {
+ bool equals = false;
+ EmbedArray::index_type i;
+ EmbedArray::size_type length = mEmbedURIs.Length();
+ for (i = 0; i < length && !equals; ++i) {
+ aURI->Equals(mEmbedURIs.ElementAt(i), &equals);
+ }
+ return equals;
+}
+
+inline bool
+nsAndroidHistory::RemovePendingVisitURI(nsIURI* aURI) {
+ // Remove the first pending URI that matches. Return a boolean to
+ // let the caller know if we removed a URI or not.
+ bool equals = false;
+ PendingVisitArray::index_type i;
+ for (i = 0; i < mPendingVisitURIs.Length(); ++i) {
+ aURI->Equals(mPendingVisitURIs.ElementAt(i), &equals);
+ if (equals) {
+ mPendingVisitURIs.RemoveElementAt(i);
+ return true;
+ }
+ }
+ return false;
+}
+
+NS_IMETHODIMP
+nsAndroidHistory::Notify(nsITimer *timer)
+{
+ // Any pending visits left in the queue have exceeded our threshold for
+ // redirects, so save them
+ PendingVisitArray::index_type i;
+ for (i = 0; i < mPendingVisitURIs.Length(); ++i) {
+ SaveVisitURI(mPendingVisitURIs.ElementAt(i));
+ }
+ mPendingVisitURIs.Clear();
+
+ return NS_OK;
+}
+
+void
+nsAndroidHistory::SaveVisitURI(nsIURI* aURI) {
+ // Add the URI to our cache so we can take a fast path later
+ AppendToRecentlyVisitedURIs(aURI);
+
+ if (jni::IsAvailable()) {
+ // Save this URI in our history
+ nsAutoCString spec;
+ (void)aURI->GetSpec(spec);
+ java::GeckoAppShell::MarkURIVisited(NS_ConvertUTF8toUTF16(spec));
+ }
+
+ // Finally, notify that we've been visited.
+ nsCOMPtr<nsIObserverService> obsService = mozilla::services::GetObserverService();
+ if (obsService) {
+ obsService->NotifyObservers(aURI, NS_LINK_VISITED_EVENT_TOPIC, nullptr);
+ }
+}
+
+NS_IMETHODIMP
+nsAndroidHistory::VisitURI(nsIURI *aURI, nsIURI *aLastVisitedURI, uint32_t aFlags)
+{
+ if (!aURI) {
+ return NS_OK;
+ }
+
+ if (!(aFlags & VisitFlags::TOP_LEVEL)) {
+ return NS_OK;
+ }
+
+ if (aFlags & VisitFlags::UNRECOVERABLE_ERROR) {
+ return NS_OK;
+ }
+
+ // Silently return if URI is something we shouldn't add to DB.
+ bool canAdd;
+ nsresult rv = CanAddURI(aURI, &canAdd);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!canAdd) {
+ return NS_OK;
+ }
+
+ if (aLastVisitedURI) {
+ if (aFlags & VisitFlags::REDIRECT_SOURCE ||
+ aFlags & VisitFlags::REDIRECT_PERMANENT ||
+ aFlags & VisitFlags::REDIRECT_TEMPORARY) {
+ // aLastVisitedURI redirected to aURI. We want to ignore aLastVisitedURI,
+ // so remove the pending visit. We want to give aURI a chance to be saved,
+ // so don't return early.
+ RemovePendingVisitURI(aLastVisitedURI);
+ }
+
+ bool same;
+ rv = aURI->Equals(aLastVisitedURI, &same);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (same && IsRecentlyVisitedURI(aURI)) {
+ // Do not save refresh visits if we have visited this URI recently.
+ return NS_OK;
+ }
+
+ // Since we have a last visited URI and we were not redirected, it is
+ // safe to save the visit if it's still pending.
+ if (RemovePendingVisitURI(aLastVisitedURI)) {
+ SaveVisitURI(aLastVisitedURI);
+ }
+ }
+
+ // Let's wait and see if this visit is not a redirect.
+ mPendingVisitURIs.AppendElement(aURI);
+ mTimer->InitWithCallback(this, PENDING_REDIRECT_TIMEOUT, nsITimer::TYPE_ONE_SHOT);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAndroidHistory::SetURITitle(nsIURI *aURI, const nsAString& aTitle)
+{
+ // Silently return if URI is something we shouldn't add to DB.
+ bool canAdd;
+ nsresult rv = CanAddURI(aURI, &canAdd);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!canAdd) {
+ return NS_OK;
+ }
+
+ if (IsEmbedURI(aURI)) {
+ return NS_OK;
+ }
+
+ if (jni::IsAvailable()) {
+ nsAutoCString uri;
+ nsresult rv = aURI->GetSpec(uri);
+ if (NS_FAILED(rv)) return rv;
+ if (RemovePendingVisitURI(aURI)) {
+ // We have a title, so aURI isn't a redirect, so save the visit now before setting the title.
+ SaveVisitURI(aURI);
+ }
+ NS_ConvertUTF8toUTF16 uriString(uri);
+ java::GeckoAppShell::SetURITitle(uriString, aTitle);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAndroidHistory::NotifyVisited(nsIURI *aURI)
+{
+ if (aURI && sHistory) {
+ nsAutoCString spec;
+ (void)aURI->GetSpec(spec);
+ sHistory->mPendingLinkURIs.Push(NS_ConvertUTF8toUTF16(spec));
+ NS_DispatchToMainThread(sHistory);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAndroidHistory::Run()
+{
+ while (! mPendingLinkURIs.IsEmpty()) {
+ nsString uriString = mPendingLinkURIs.Pop();
+ nsTArray<Link*>* list = sHistory->mListeners.Get(uriString);
+ if (list) {
+ for (unsigned int i = 0; i < list->Length(); i++) {
+ list->ElementAt(i)->SetLinkState(eLinkState_Visited);
+ }
+ // as per the IHistory interface contract, remove the
+ // Link pointers once they have been notified
+ mListeners.Remove(uriString);
+ delete list;
+ }
+ }
+ return NS_OK;
+}
+
+// Filter out unwanted URIs such as "chrome:", "mailbox:", etc.
+//
+// The model is if we don't know differently then add which basically means
+// we are suppose to try all the things we know not to allow in and then if
+// we don't bail go on and allow it in.
+//
+// Logic ported from nsNavHistory::CanAddURI.
+
+NS_IMETHODIMP
+nsAndroidHistory::CanAddURI(nsIURI* aURI, bool* canAdd)
+{
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+ NS_ENSURE_ARG(aURI);
+ NS_ENSURE_ARG_POINTER(canAdd);
+
+ // See if we're disabled.
+ if (!ShouldRecordHistory()) {
+ *canAdd = false;
+ return NS_OK;
+ }
+
+ nsAutoCString scheme;
+ nsresult rv = aURI->GetScheme(scheme);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // first check the most common cases (HTTP, HTTPS) to allow in to avoid most
+ // of the work
+ if (scheme.EqualsLiteral("http")) {
+ *canAdd = true;
+ return NS_OK;
+ }
+ if (scheme.EqualsLiteral("https")) {
+ *canAdd = true;
+ return NS_OK;
+ }
+ if (scheme.EqualsLiteral("about")) {
+ nsAutoCString path;
+ rv = aURI->GetPath(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (StringBeginsWith(path, NS_LITERAL_CSTRING("reader"))) {
+ *canAdd = true;
+ return NS_OK;
+ }
+ }
+
+ // now check for all bad things
+ if (scheme.EqualsLiteral("about") ||
+ scheme.EqualsLiteral("imap") ||
+ scheme.EqualsLiteral("news") ||
+ scheme.EqualsLiteral("mailbox") ||
+ scheme.EqualsLiteral("moz-anno") ||
+ scheme.EqualsLiteral("view-source") ||
+ scheme.EqualsLiteral("chrome") ||
+ scheme.EqualsLiteral("resource") ||
+ scheme.EqualsLiteral("data") ||
+ scheme.EqualsLiteral("wyciwyg") ||
+ scheme.EqualsLiteral("javascript") ||
+ scheme.EqualsLiteral("blob")) {
+ *canAdd = false;
+ return NS_OK;
+ }
+ *canAdd = true;
+ return NS_OK;
+}
diff --git a/mobile/android/components/build/nsAndroidHistory.h b/mobile/android/components/build/nsAndroidHistory.h
new file mode 100644
index 0000000000..382fbcd2ec
--- /dev/null
+++ b/mobile/android/components/build/nsAndroidHistory.h
@@ -0,0 +1,97 @@
+/* -*- Mode: c++; 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/. */
+
+#ifndef NS_ANDROIDHISTORY_H
+#define NS_ANDROIDHISTORY_H
+
+#include "IHistory.h"
+#include "nsDataHashtable.h"
+#include "nsTPriorityQueue.h"
+#include "nsIRunnable.h"
+#include "nsIURI.h"
+#include "nsITimer.h"
+
+
+#define NS_ANDROIDHISTORY_CID \
+ {0xCCAA4880, 0x44DD, 0x40A7, {0xA1, 0x3F, 0x61, 0x56, 0xFC, 0x88, 0x2C, 0x0B}}
+
+// Max size of History::mRecentlyVisitedURIs
+#define RECENTLY_VISITED_URI_SIZE 8
+
+// Max size of History::mEmbedURIs
+#define EMBED_URI_SIZE 128
+
+class nsAndroidHistory final : public mozilla::IHistory,
+ public nsIRunnable,
+ public nsITimerCallback
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_IHISTORY
+ NS_DECL_NSIRUNNABLE
+ NS_DECL_NSITIMERCALLBACK
+
+ /**
+ * Obtains a pointer that has had AddRef called on it. Used by the service
+ * manager only.
+ */
+ static nsAndroidHistory* GetSingleton();
+
+ nsAndroidHistory();
+
+private:
+ ~nsAndroidHistory() {}
+
+ static nsAndroidHistory* sHistory;
+
+ // Will mimic the value of the places.history.enabled preference.
+ bool mHistoryEnabled;
+
+ void LoadPrefs();
+ bool ShouldRecordHistory();
+ nsresult CanAddURI(nsIURI* aURI, bool* canAdd);
+
+ /**
+ * We need to manage data used to determine a:visited status.
+ */
+ nsDataHashtable<nsStringHashKey, nsTArray<mozilla::dom::Link *> *> mListeners;
+ nsTPriorityQueue<nsString> mPendingLinkURIs;
+
+ /**
+ * Redirection (temporary and permanent) flags are sent with the redirected
+ * URI, not the original URI. Since we want to ignore the original URI, we
+ * need to cache the pending visit and make sure it doesn't redirect.
+ */
+ RefPtr<nsITimer> mTimer;
+ typedef AutoTArray<nsCOMPtr<nsIURI>, RECENTLY_VISITED_URI_SIZE> PendingVisitArray;
+ PendingVisitArray mPendingVisitURIs;
+
+ bool RemovePendingVisitURI(nsIURI* aURI);
+ void SaveVisitURI(nsIURI* aURI);
+
+ /**
+ * mRecentlyVisitedURIs remembers URIs which are recently added to the DB,
+ * to avoid saving these locations repeatedly in a short period.
+ */
+ typedef AutoTArray<nsCOMPtr<nsIURI>, RECENTLY_VISITED_URI_SIZE> RecentlyVisitedArray;
+ RecentlyVisitedArray mRecentlyVisitedURIs;
+ RecentlyVisitedArray::index_type mRecentlyVisitedURIsNextIndex;
+
+ void AppendToRecentlyVisitedURIs(nsIURI* aURI);
+ bool IsRecentlyVisitedURI(nsIURI* aURI);
+
+ /**
+ * mEmbedURIs remembers URIs which are explicitly not added to the DB,
+ * to avoid wasting time on these locations.
+ */
+ typedef AutoTArray<nsCOMPtr<nsIURI>, EMBED_URI_SIZE> EmbedArray;
+ EmbedArray::index_type mEmbedURIsNextIndex;
+ EmbedArray mEmbedURIs;
+
+ void AppendToEmbedURIs(nsIURI* aURI);
+ bool IsEmbedURI(nsIURI* aURI);
+};
+
+#endif
diff --git a/mobile/android/components/build/nsBrowserComponents.h b/mobile/android/components/build/nsBrowserComponents.h
new file mode 100644
index 0000000000..c9830d9c5d
--- /dev/null
+++ b/mobile/android/components/build/nsBrowserComponents.h
@@ -0,0 +1,7 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Needed for building our components as part of libxul
+#define APP_COMPONENT_MODULES MODULE(nsBrowserCompsModule)
diff --git a/mobile/android/components/build/nsBrowserModule.cpp b/mobile/android/components/build/nsBrowserModule.cpp
new file mode 100644
index 0000000000..6f9fe67bf1
--- /dev/null
+++ b/mobile/android/components/build/nsBrowserModule.cpp
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/ModuleUtils.h"
+
+#include "nsShellService.h"
+
+#ifdef MOZ_ANDROID_HISTORY
+#include "nsDocShellCID.h"
+#include "nsAndroidHistory.h"
+#define NS_ANDROIDHISTORY_CID \
+ {0xCCAA4880, 0x44DD, 0x40A7, {0xA1, 0x3F, 0x61, 0x56, 0xFC, 0x88, 0x2C, 0x0B}}
+#endif
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsShellService)
+NS_DEFINE_NAMED_CID(nsShellService_CID);
+
+#ifdef MOZ_ANDROID_HISTORY
+NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsAndroidHistory, nsAndroidHistory::GetSingleton)
+NS_DEFINE_NAMED_CID(NS_ANDROIDHISTORY_CID);
+#endif
+
+static const mozilla::Module::CIDEntry kBrowserCIDs[] = {
+ { &knsShellService_CID, false, nullptr, nsShellServiceConstructor },
+#ifdef MOZ_ANDROID_HISTORY
+ { &kNS_ANDROIDHISTORY_CID, false, nullptr, nsAndroidHistoryConstructor },
+#endif
+ { nullptr }
+};
+
+static const mozilla::Module::ContractIDEntry kBrowserContracts[] = {
+ { nsShellService_ContractID, &knsShellService_CID },
+#ifdef MOZ_ANDROID_HISTORY
+ { NS_IHISTORY_CONTRACTID, &kNS_ANDROIDHISTORY_CID },
+#endif
+ { nullptr }
+};
+
+static const mozilla::Module kBrowserModule = {
+ mozilla::Module::kVersion,
+ kBrowserCIDs,
+ kBrowserContracts
+};
+
+NSMODULE_DEFN(nsBrowserCompsModule) = &kBrowserModule;
diff --git a/mobile/android/components/build/nsIShellService.idl b/mobile/android/components/build/nsIShellService.idl
new file mode 100644
index 0000000000..e7f8d92776
--- /dev/null
+++ b/mobile/android/components/build/nsIShellService.idl
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(fd2450a3-966b-44a9-a8eb-316256bb80b4)]
+interface nsIShellService : nsISupports
+{
+ /**
+ * This method displays a UI to switch to (or launch) a different task
+ */
+ void switchTask();
+
+ /**
+ * This method creates a shortcut on a desktop or homescreen that opens in
+ * the our application.
+ *
+ * @param aTitle the user-friendly name of the shortcut.
+ * @param aURI the URI to open.
+ * @param aIconData obsolete and ignored, but remains for backward compatibility; pass an empty string
+ * @param aIntent obsolete and ignored, but remains for backward compatibility; pass an empty string
+ */
+ void createShortcut(in AString aTitle, in AString aURI, in AString aIconData, in AString aIntent);
+};
diff --git a/mobile/android/components/build/nsShellService.cpp b/mobile/android/components/build/nsShellService.cpp
new file mode 100644
index 0000000000..86cac86b44
--- /dev/null
+++ b/mobile/android/components/build/nsShellService.cpp
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsShellService.h"
+#include "nsString.h"
+
+#include "GeneratedJNIWrappers.h"
+
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS(nsShellService, nsIShellService)
+
+NS_IMETHODIMP
+nsShellService::SwitchTask()
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsShellService::CreateShortcut(const nsAString& aTitle, const nsAString& aURI,
+ const nsAString& aIcondata, const nsAString& aIntent)
+{
+ if (!aTitle.Length() || !aURI.Length())
+ return NS_ERROR_FAILURE;
+
+ java::GeckoAppShell::CreateShortcut(aTitle, aURI);
+ return NS_OK;
+}
diff --git a/mobile/android/components/build/nsShellService.h b/mobile/android/components/build/nsShellService.h
new file mode 100644
index 0000000000..ba56cbcaec
--- /dev/null
+++ b/mobile/android/components/build/nsShellService.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __NS_SHELLSERVICE_H__
+#define __NS_SHELLSERVICE_H__
+
+#include "nsIShellService.h"
+
+class nsShellService final : public nsIShellService
+{
+public:
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISHELLSERVICE
+
+ nsShellService() {}
+
+private:
+ ~nsShellService() {}
+};
+
+#define nsShellService_CID \
+{0xae9ebe1c, 0x61e9, 0x45fa, {0x8f, 0x34, 0xc1, 0x07, 0x80, 0x3a, 0x5b, 0x44}}
+
+#define nsShellService_ContractID "@mozilla.org/browser/shell-service;1"
+
+#endif
diff --git a/mobile/android/components/extensions/.eslintrc.js b/mobile/android/components/extensions/.eslintrc.js
new file mode 100644
index 0000000000..4b67e27b88
--- /dev/null
+++ b/mobile/android/components/extensions/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ "extends": "../../../../toolkit/components/extensions/.eslintrc.js",
+};
diff --git a/mobile/android/components/extensions/ext-pageAction.js b/mobile/android/components/extensions/ext-pageAction.js
new file mode 100644
index 0000000000..fb1c3a3f37
--- /dev/null
+++ b/mobile/android/components/extensions/ext-pageAction.js
@@ -0,0 +1,169 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
+ "resource://devtools/shared/event-emitter.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+// Import the android PageActions module.
+XPCOMUtils.defineLazyModuleGetter(this, "PageActions",
+ "resource://gre/modules/PageActions.jsm");
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+var {
+ IconDetails,
+ SingletonEventManager,
+} = ExtensionUtils;
+
+// WeakMap[Extension -> PageAction]
+var pageActionMap = new WeakMap();
+
+function PageAction(options, extension) {
+ this.id = null;
+
+ this.extension = extension;
+ this.icons = IconDetails.normalize({path: options.default_icon}, extension);
+
+ this.popupUrl = options.default_popup;
+
+ this.options = {
+ title: options.default_title || extension.name,
+ id: `{${extension.uuid}}`,
+ clickCallback: () => {
+ if (this.popupUrl) {
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+ win.BrowserApp.addTab(this.popupUrl, {
+ selected: true,
+ parentId: win.BrowserApp.selectedTab.id,
+ });
+ } else {
+ this.emit("click");
+ }
+ },
+ };
+
+ this.shouldShow = false;
+
+ EventEmitter.decorate(this);
+}
+
+PageAction.prototype = {
+ show(tabId, context) {
+ if (this.id) {
+ return Promise.resolve();
+ }
+
+ if (this.options.icon) {
+ this.id = PageActions.add(this.options);
+ return Promise.resolve();
+ }
+
+ this.shouldShow = true;
+
+ // TODO(robwu): Remove dependency on contentWindow from this file. It should
+ // be put in a separate file called ext-c-pageAction.js.
+ // Note: Fennec is not going to be multi-process for the foreseaable future,
+ // so this layering violation has no immediate impact. However, it is should
+ // be done at some point.
+ let {contentWindow} = context.xulBrowser;
+
+ // TODO(robwu): Why is this contentWindow.devicePixelRatio, while
+ // convertImageURLToDataURL uses browserWindow.devicePixelRatio?
+ let {icon} = IconDetails.getPreferredIcon(this.icons, this.extension,
+ 18 * contentWindow.devicePixelRatio);
+
+ let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ return IconDetails.convertImageURLToDataURL(icon, contentWindow, browserWindow).then(dataURI => {
+ if (this.shouldShow) {
+ this.options.icon = dataURI;
+ this.id = PageActions.add(this.options);
+ }
+ }).catch(() => {
+ return Promise.reject({
+ message: "Failed to load PageAction icon",
+ });
+ });
+ },
+
+ hide(tabId) {
+ this.shouldShow = false;
+ if (this.id) {
+ PageActions.remove(this.id);
+ this.id = null;
+ }
+ },
+
+ setPopup(tab, url) {
+ // TODO: Only set the popup for the specified tab once we have Tabs API support.
+ this.popupUrl = url;
+ },
+
+ getPopup(tab) {
+ // TODO: Only return the popup for the specified tab once we have Tabs API support.
+ return this.popupUrl;
+ },
+
+ shutdown() {
+ this.hide();
+ },
+};
+
+/* eslint-disable mozilla/balanced-listeners */
+extensions.on("manifest_page_action", (type, directive, extension, manifest) => {
+ let pageAction = new PageAction(manifest.page_action, extension);
+ pageActionMap.set(extension, pageAction);
+});
+
+extensions.on("shutdown", (type, extension) => {
+ if (pageActionMap.has(extension)) {
+ pageActionMap.get(extension).shutdown();
+ pageActionMap.delete(extension);
+ }
+});
+/* eslint-enable mozilla/balanced-listeners */
+
+extensions.registerSchemaAPI("pageAction", "addon_parent", context => {
+ let {extension} = context;
+ return {
+ pageAction: {
+ onClicked: new SingletonEventManager(context, "pageAction.onClicked", fire => {
+ let listener = (event) => {
+ fire();
+ };
+ pageActionMap.get(extension).on("click", listener);
+ return () => {
+ pageActionMap.get(extension).off("click", listener);
+ };
+ }).api(),
+
+ show(tabId) {
+ return pageActionMap.get(extension)
+ .show(tabId, context)
+ .then(() => {});
+ },
+
+ hide(tabId) {
+ pageActionMap.get(extension).hide(tabId);
+ return Promise.resolve();
+ },
+
+ setPopup(details) {
+ // TODO: Use the Tabs API to get the tab from details.tabId.
+ let tab = null;
+ let url = details.popup && context.uri.resolve(details.popup);
+ pageActionMap.get(extension).setPopup(tab, url);
+ },
+
+ getPopup(details) {
+ // TODO: Use the Tabs API to get the tab from details.tabId.
+ let tab = null;
+ let popup = pageActionMap.get(extension).getPopup(tab);
+ return Promise.resolve(popup);
+ },
+ },
+ };
+});
diff --git a/mobile/android/components/extensions/extensions-mobile.manifest b/mobile/android/components/extensions/extensions-mobile.manifest
new file mode 100644
index 0000000000..f15540d628
--- /dev/null
+++ b/mobile/android/components/extensions/extensions-mobile.manifest
@@ -0,0 +1,5 @@
+# scripts
+category webextension-scripts pageAction chrome://browser/content/ext-pageAction.js
+
+# schemas
+category webextension-schemas page_action chrome://browser/content/schemas/page_action.json \ No newline at end of file
diff --git a/mobile/android/components/extensions/jar.mn b/mobile/android/components/extensions/jar.mn
new file mode 100644
index 0000000000..a3d2b8de8a
--- /dev/null
+++ b/mobile/android/components/extensions/jar.mn
@@ -0,0 +1,6 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+chrome.jar:
+ content/ext-pageAction.js \ No newline at end of file
diff --git a/mobile/android/components/extensions/moz.build b/mobile/android/components/extensions/moz.build
new file mode 100644
index 0000000000..0953fcefc7
--- /dev/null
+++ b/mobile/android/components/extensions/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ['jar.mn']
+
+EXTRA_COMPONENTS += [
+ 'extensions-mobile.manifest',
+]
+
+DIRS += ['schemas']
+
+MOCHITEST_MANIFESTS += ['test/mochitest/mochitest.ini']
+MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini']
diff --git a/mobile/android/components/extensions/schemas/jar.mn b/mobile/android/components/extensions/schemas/jar.mn
new file mode 100644
index 0000000000..1a587ce207
--- /dev/null
+++ b/mobile/android/components/extensions/schemas/jar.mn
@@ -0,0 +1,6 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+chrome.jar:
+ content/schemas/page_action.json \ No newline at end of file
diff --git a/mobile/android/components/extensions/schemas/moz.build b/mobile/android/components/extensions/schemas/moz.build
new file mode 100644
index 0000000000..eb4454d28f
--- /dev/null
+++ b/mobile/android/components/extensions/schemas/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file
diff --git a/mobile/android/components/extensions/schemas/page_action.json b/mobile/android/components/extensions/schemas/page_action.json
new file mode 100644
index 0000000000..5e92809229
--- /dev/null
+++ b/mobile/android/components/extensions/schemas/page_action.json
@@ -0,0 +1,239 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+ {
+ "namespace": "manifest",
+ "types": [
+ {
+ "$extend": "WebExtensionManifest",
+ "properties": {
+ "page_action": {
+ "type": "object",
+ "additionalProperties": { "$ref": "UnrecognizedProperty" },
+ "properties": {
+ "default_title": {
+ "type": "string",
+ "optional": true,
+ "preprocess": "localize"
+ },
+ "default_icon": {
+ "$ref": "IconPath",
+ "optional": true
+ },
+ "default_popup": {
+ "type": "string",
+ "format": "relativeUrl",
+ "optional": true,
+ "preprocess": "localize"
+ },
+ "browser_style": {
+ "type": "boolean",
+ "optional": true
+ }
+ },
+ "optional": true
+ }
+ }
+ }
+ ]
+ },
+ {
+ "namespace": "pageAction",
+ "description": "Use the <code>browser.pageAction</code> API to put icons inside the address bar. Page actions represent actions that can be taken on the current page, but that aren't applicable to all pages.",
+ "permissions": ["manifest:page_action"],
+ "types": [
+ {
+ "id": "ImageDataType",
+ "type": "object",
+ "isInstanceOf": "ImageData",
+ "additionalProperties": { "type": "any" },
+ "description": "Pixel data for an image. Must be an ImageData object (for example, from a <code>canvas</code> element)."
+ }
+ ],
+ "functions": [
+ {
+ "name": "show",
+ "type": "function",
+ "description": "Shows the page action. The page action is shown whenever the tab is selected.",
+ "async": "callback",
+ "parameters": [
+ {"type": "integer", "name": "tabId", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
+ {
+ "type": "function",
+ "name": "callback",
+ "optional": true,
+ "parameters": []
+ }
+ ]
+ },
+ {
+ "name": "hide",
+ "type": "function",
+ "description": "Hides the page action.",
+ "async": "callback",
+ "parameters": [
+ {"type": "integer", "name": "tabId", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
+ {
+ "type": "function",
+ "name": "callback",
+ "optional": true,
+ "parameters": []
+ }
+ ]
+ },
+ {
+ "name": "setTitle",
+ "unsupported": true,
+ "type": "function",
+ "description": "Sets the title of the page action. This is displayed in a tooltip over the page action.",
+ "parameters": [
+ {
+ "name": "details",
+ "type": "object",
+ "properties": {
+ "tabId": {"type": "integer", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
+ "title": {"type": "string", "description": "The tooltip string."}
+ }
+ }
+ ]
+ },
+ {
+ "name": "getTitle",
+ "unsupported": true,
+ "type": "function",
+ "description": "Gets the title of the page action.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "details",
+ "type": "object",
+ "properties": {
+ "tabId": {
+ "type": "integer",
+ "description": "Specify the tab to get the title from."
+ }
+ }
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ {
+ "name": "result",
+ "type": "string"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "setIcon",
+ "unsupported": true,
+ "type": "function",
+ "description": "Sets the icon for the page action. The icon can be specified either as the path to an image file or as the pixel data from a canvas element, or as dictionary of either one of those. Either the <b>path</b> or the <b>imageData</b> property must be specified.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "details",
+ "type": "object",
+ "properties": {
+ "tabId": {"type": "integer", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
+ "imageData": {
+ "choices": [
+ { "$ref": "ImageDataType" },
+ {
+ "type": "object",
+ "additionalProperties": {"$ref": "ImageDataType"}
+ }
+ ],
+ "optional": true,
+ "description": "Either an ImageData object or a dictionary {size -> ImageData} representing icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals <code>scale</code>, then image with size <code>scale</code> * 19 will be selected. Initially only scales 1 and 2 will be supported. At least one image must be specified. Note that 'details.imageData = foo' is equivalent to 'details.imageData = {'19': foo}'"
+ },
+ "path": {
+ "choices": [
+ { "type": "string" },
+ {
+ "type": "object",
+ "additionalProperties": {"type": "string"}
+ }
+ ],
+ "optional": true,
+ "description": "Either a relative image path or a dictionary {size -> relative image path} pointing to icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals <code>scale</code>, then image with size <code>scale</code> * 19 will be selected. Initially only scales 1 and 2 will be supported. At least one image must be specified. Note that 'details.path = foo' is equivalent to 'details.imageData = {'19': foo}'"
+ }
+ }
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "optional": true,
+ "parameters": []
+ }
+ ]
+ },
+ {
+ "name": "setPopup",
+ "type": "function",
+ "async": "callback",
+ "description": "Sets the html document to be opened as a popup when the user clicks on the page action's icon.",
+ "parameters": [
+ {
+ "name": "details",
+ "type": "object",
+ "properties": {
+ "tabId": {"type": "integer", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
+ "popup": {
+ "type": "string",
+ "description": "The html file to show in a popup. If set to the empty string (''), no popup is shown."
+ }
+ }
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "optional": true,
+ "parameters": []
+ }
+ ]
+ },
+ {
+ "name": "getPopup",
+ "type": "function",
+ "description": "Gets the html document set as the popup for this page action.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "details",
+ "type": "object",
+ "properties": {
+ "tabId": {
+ "type": "integer",
+ "description": "Specify the tab to get the popup from."
+ }
+ }
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "optional": true,
+ "parameters": []
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "onClicked",
+ "type": "function",
+ "description": "Fired when a page action icon is clicked. This event will not fire if the page action has a popup.",
+ "parameters": [
+ {
+ "name": "tab",
+ "$ref": "tabs.Tab"
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/mobile/android/components/extensions/test/mochitest/.eslintrc.js b/mobile/android/components/extensions/test/mochitest/.eslintrc.js
new file mode 100644
index 0000000000..5f9059e18e
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/.eslintrc.js
@@ -0,0 +1,10 @@
+"use strict";
+
+module.exports = {
+ "extends": "../../../../../../toolkit/components/extensions/test/mochitest/.eslintrc.js",
+
+ "globals": {
+ "isPageActionShown": true,
+ "clickPageAction": true,
+ },
+};
diff --git a/mobile/android/components/extensions/test/mochitest/chrome.ini b/mobile/android/components/extensions/test/mochitest/chrome.ini
new file mode 100644
index 0000000000..e19ddf3933
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/chrome.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+support-files =
+ head.js
+tags = webextensions
+
+[test_ext_pageAction.html]
+[test_ext_pageAction_popup.html]
diff --git a/mobile/android/components/extensions/test/mochitest/head.js b/mobile/android/components/extensions/test/mochitest/head.js
new file mode 100644
index 0000000000..be9683682f
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/head.js
@@ -0,0 +1,15 @@
+"use strict";
+
+/* exported isPageActionShown clickPageAction */
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/PageActions.jsm");
+
+function isPageActionShown(uuid) {
+ return PageActions.isShown(uuid);
+}
+
+function clickPageAction(uuid) {
+ PageActions.synthesizeClick(uuid);
+}
diff --git a/mobile/android/components/extensions/test/mochitest/mochitest.ini b/mobile/android/components/extensions/test/mochitest/mochitest.ini
new file mode 100644
index 0000000000..59ef4bd20a
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/mochitest.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+support-files =
+ ../../../../../../toolkit/components/extensions/test/mochitest/test_ext_all_apis.js
+tags = webextensions
+
+[test_ext_all_apis.html]
diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_all_apis.html b/mobile/android/components/extensions/test/mochitest/test_ext_all_apis.html
new file mode 100644
index 0000000000..aec3eb7c13
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_all_apis.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <meta charset="utf-8">
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<script>
+"use strict";
+/* exported expectedContentApisTargetSpecific, expectedBackgroundApisTargetSpecific */
+let expectedContentApisTargetSpecific = [
+];
+
+let expectedBackgroundApisTargetSpecific = [
+];
+</script>
+<script src="test_ext_all_apis.js"></script>
+</body>
+</html>
diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_pageAction.html b/mobile/android/components/extensions/test/mochitest/test_ext_pageAction.html
new file mode 100644
index 0000000000..b13c551bdc
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_pageAction.html
@@ -0,0 +1,99 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>PageAction Test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+let dataURI = "iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAC4klEQVRYhdWXLWzbQBSADQtDAwsHC1tUhUxqfL67lk2tdn+OJg0ODU0rLByqgqINBY6tmlbn7LMTJ5FaFVVBk1G0oUGjG2jT2Y7jxmmcbU/6iJ+f36fz+e5sGP9riCGm9hB37RG+scd4Yo/wsDXCZyIE2xuXsce4bY+wXkAsQtzYmExrfFgvkJkRbkzo1ehoxx5iXcgI/9iYUGt8WH9MqDXEcmNChmEYrRCf2SHWeYgQx3x0tLNRIeKQLTtEFyJEep4NTuhk8BC+yMrwEE3+iozo42d8gK7FAOkMsRiiN8QhW2ttSK5QTfRRV4QoymVeJMvPvDp7gCZigD613MN6yRFA3SWarow9QB9LCfG+NeF9qCtjAKOSQjCqVKhfVsiHEQ+grgx/lRGqUihAc1uL8EFD+KCRO+GrF4J61phcoRoPoEzkYhZYpykh5sMb7kOdIeY+jHKur4QI4Feh4AFX1nVeLxrAvQchGsBz5ls6wa2QdwcvIcE2863bTH79KOvsz/uUYJsp+J0pSzNlDckVqqVGUAF+n6uS7txcOl6wot4JVy70ufDLy4pWLUQVPE81pRI0mGe9oxLMHSeohHvMs/STUNaUK6vDPCvOyxMFDx4achehRDJmHnydnkPww5OFfLxrGIZBFDyYl4LpMzlTQFIP6AQx86w2UeYBccFpJrcKv5L9eGDtUAU6RIELqsB74uynjy/UBRF1gS5BTFxwQT1wTiXoUg9MH7m/3NZRRoi5IJytUbMgzv4Wc832+oQkiKgEehmyMkkpKsFkQV11QsRJL5rJYBLItQgRaUZEmnoZXsomz3vGiWw+I9KMF9SVFOqZEemZekli1jN3U/UOqhHHvC6oWWGElhfSpGdOk6+O9prdwvtLj5BjRsQxdRnot+Zeifpy/2/0stktKTRNLmbk0mwXyl8253fyojj+8rxOHNAhjjm5n0/5OOCGOKBzkrMO0Z75lvSAzKlrF32Z/3z8BqLAn+yMV7VhAAAAAElFTkSuQmCC";
+
+let image = atob(dataURI);
+const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer;
+
+function background() {
+ browser.test.assertTrue("pageAction" in browser, "Namespace 'pageAction' exists in browser");
+ browser.test.assertTrue("show" in browser.pageAction, "API method 'show' exists in browser.pageAction");
+
+ // TODO: Use the Tabs API to obtain the tab ids for showing pageActions.
+ let tabId = 1;
+ browser.test.onMessage.addListener(msg => {
+ if (msg === "pageAction-show") {
+ browser.pageAction.show(tabId).then(() => {
+ browser.test.sendMessage("page-action-shown");
+ });
+ } else if (msg === "pageAction-hide") {
+ browser.pageAction.hide(tabId).then(() => {
+ browser.test.sendMessage("page-action-hidden");
+ });
+ }
+ });
+
+ browser.pageAction.onClicked.addListener(tab => {
+ // TODO: Make sure we get the correct tab once basic tabs support is added.
+ browser.test.sendMessage("page-action-clicked");
+ });
+
+ let extensionInfo = {
+ // Extract the assigned uuid from the background page url.
+ uuid: `{${window.location.hostname}}`,
+ };
+
+ browser.test.sendMessage("ready", extensionInfo);
+}
+
+add_task(function* test_pageAction() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ "name": "PageAction Extension",
+ "page_action": {
+ "default_title": "Page Action",
+ "default_icon": {
+ "18": "extension.png",
+ },
+ },
+ "applications": {
+ "gecko": {
+ "id": "foo@bar.com",
+ },
+ },
+ },
+ files: {
+ "extension.png": IMAGE_ARRAYBUFFER,
+ },
+ });
+
+ yield extension.startup();
+ let {uuid} = yield extension.awaitMessage("ready");
+
+ extension.sendMessage("pageAction-show");
+ yield extension.awaitMessage("page-action-shown");
+ ok(isPageActionShown(uuid), "The PageAction should be shown");
+
+ extension.sendMessage("pageAction-hide");
+ yield extension.awaitMessage("page-action-hidden");
+ ok(!isPageActionShown(uuid), "The PageAction should be hidden");
+
+ extension.sendMessage("pageAction-show");
+ yield extension.awaitMessage("page-action-shown");
+ ok(isPageActionShown(uuid), "The PageAction should be shown");
+
+ clickPageAction(uuid);
+ yield extension.awaitMessage("page-action-clicked");
+ ok(isPageActionShown(uuid), "The PageAction should still be shown after being clicked");
+
+ yield extension.unload();
+ ok(!isPageActionShown(uuid), "The PageAction should be removed after unload");
+});
+</script>
+
+</body>
+</html>
diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_pageAction_popup.html b/mobile/android/components/extensions/test/mochitest/test_ext_pageAction_popup.html
new file mode 100644
index 0000000000..89edc7c298
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_pageAction_popup.html
@@ -0,0 +1,169 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>PageAction Test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+let dataURI = "iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAC4klEQVRYhdWXLWzbQBSADQtDAwsHC1tUhUxqfL67lk2tdn+OJg0ODU0rLByqgqINBY6tmlbn7LMTJ5FaFVVBk1G0oUGjG2jT2Y7jxmmcbU/6iJ+f36fz+e5sGP9riCGm9hB37RG+scd4Yo/wsDXCZyIE2xuXsce4bY+wXkAsQtzYmExrfFgvkJkRbkzo1ehoxx5iXcgI/9iYUGt8WH9MqDXEcmNChmEYrRCf2SHWeYgQx3x0tLNRIeKQLTtEFyJEep4NTuhk8BC+yMrwEE3+iozo42d8gK7FAOkMsRiiN8QhW2ttSK5QTfRRV4QoymVeJMvPvDp7gCZigD613MN6yRFA3SWarow9QB9LCfG+NeF9qCtjAKOSQjCqVKhfVsiHEQ+grgx/lRGqUihAc1uL8EFD+KCRO+GrF4J61phcoRoPoEzkYhZYpykh5sMb7kOdIeY+jHKur4QI4Feh4AFX1nVeLxrAvQchGsBz5ls6wa2QdwcvIcE2863bTH79KOvsz/uUYJsp+J0pSzNlDckVqqVGUAF+n6uS7txcOl6wot4JVy70ufDLy4pWLUQVPE81pRI0mGe9oxLMHSeohHvMs/STUNaUK6vDPCvOyxMFDx4achehRDJmHnydnkPww5OFfLxrGIZBFDyYl4LpMzlTQFIP6AQx86w2UeYBccFpJrcKv5L9eGDtUAU6RIELqsB74uynjy/UBRF1gS5BTFxwQT1wTiXoUg9MH7m/3NZRRoi5IJytUbMgzv4Wc832+oQkiKgEehmyMkkpKsFkQV11QsRJL5rJYBLItQgRaUZEmnoZXsomz3vGiWw+I9KMF9SVFOqZEemZekli1jN3U/UOqhHHvC6oWWGElhfSpGdOk6+O9prdwvtLj5BjRsQxdRnot+Zeifpy/2/0stktKTRNLmbk0mwXyl8253fyojj+8rxOHNAhjjm5n0/5OOCGOKBzkrMO0Z75lvSAzKlrF32Z/3z8BqLAn+yMV7VhAAAAAElFTkSuQmCC";
+
+let image = atob(dataURI);
+const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer;
+
+add_task(function* test_contentscript() {
+ function background() {
+ // TODO: Use the Tabs API to obtain the tab ids for showing pageActions.
+ let tabId = 1;
+ let onClickedListenerEnabled = false;
+
+ browser.test.onMessage.addListener((msg, details) => {
+ if (msg === "page-action-show") {
+ // TODO: switch to using .show(tabId).then(...) once bug 1270742 lands.
+ browser.pageAction.show(tabId).then(() => {
+ browser.test.sendMessage("page-action-shown");
+ });
+ } else if (msg == "page-action-set-popup") {
+ browser.pageAction.setPopup({popup: details.name, tabId: tabId}).then(() => {
+ browser.test.sendMessage("page-action-popup-set");
+ });
+ } else if (msg == "page-action-get-popup") {
+ browser.pageAction.getPopup({tabId: tabId}).then(url => {
+ browser.test.sendMessage("page-action-got-popup", url);
+ });
+ } else if (msg == "page-action-enable-onClicked-listener") {
+ onClickedListenerEnabled = true;
+ browser.test.sendMessage("page-action-onClicked-listener-enabled");
+ } else if (msg == "page-action-disable-onClicked-listener") {
+ onClickedListenerEnabled = false;
+ browser.test.sendMessage("page-action-onClicked-listener-disabled");
+ }
+ });
+
+ browser.pageAction.onClicked.addListener(tab => {
+ browser.test.assertTrue(onClickedListenerEnabled, "The onClicked listener should only fire when it is enabled.");
+ browser.test.sendMessage("page-action-onClicked-fired");
+ });
+
+ let extensionInfo = {
+ // Extract the assigned uuid from the background page url.
+ uuid: `{${window.location.hostname}}`,
+ };
+
+ browser.test.sendMessage("ready", extensionInfo);
+ }
+
+ function popupScript() {
+ window.onload = () => {
+ browser.test.sendMessage("page-action-from-popup", location.href);
+ };
+ browser.test.onMessage.addListener((msg, details) => {
+ if (msg == "page-action-close-popup") {
+ if (details.location == location.href) {
+ window.close();
+ }
+ }
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ "name": "PageAction Extension",
+ "page_action": {
+ "default_title": "Page Action",
+ "default_popup": "default.html",
+ "default_icon": {
+ "18": "extension.png",
+ },
+ },
+ },
+ files: {
+ "default.html": `<html><head><meta charset="utf-8"><script src="popup.js"><\/script></head></html>`,
+ "extension.png": IMAGE_ARRAYBUFFER,
+ "a.html": `<html><head><meta charset="utf-8"><script src="popup.js"><\/script></head></html>`,
+ "b.html": `<html><head><meta charset="utf-8"><script src="popup.js"><\/script></head></html>`,
+ "popup.js": popupScript,
+ },
+ });
+
+ let tabClosedPromise = () => {
+ return new Promise(resolve => {
+ let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let BrowserApp = chromeWin.BrowserApp;
+
+ let tabCloseListener = (event) => {
+ BrowserApp.deck.removeEventListener("TabClose", tabCloseListener, false);
+ let browser = event.target;
+ let url = browser.currentURI.spec;
+ resolve(url);
+ };
+
+ BrowserApp.deck.addEventListener("TabClose", tabCloseListener, false);
+ });
+ };
+
+ function* testPopup(name, uuid) {
+ // We don't need to set the popup when testing default_popup.
+ if (name != "default.html") {
+ extension.sendMessage("page-action-set-popup", {name});
+ yield extension.awaitMessage("page-action-popup-set");
+ }
+
+ extension.sendMessage("page-action-get-popup");
+ let url = yield extension.awaitMessage("page-action-got-popup");
+
+ if (name == "") {
+ ok(url == name, "Calling pageAction.getPopup should return an empty string when the popup is not set.");
+
+ // The onClicked listener should get called when the popup is set to an empty string.
+ extension.sendMessage("page-action-enable-onClicked-listener");
+ yield extension.awaitMessage("page-action-onClicked-listener-enabled");
+
+ clickPageAction(uuid);
+ yield extension.awaitMessage("page-action-onClicked-fired");
+
+ extension.sendMessage("page-action-disable-onClicked-listener");
+ yield extension.awaitMessage("page-action-onClicked-listener-disabled");
+ } else {
+ ok(url.includes(name), "Calling pageAction.getPopup should return the correct popup URL when the popup is set.");
+
+ clickPageAction(uuid);
+ let location = yield extension.awaitMessage("page-action-from-popup");
+ ok(location.includes(name), "The popup with the correct URL should be shown.");
+
+ extension.sendMessage("page-action-close-popup", {location});
+
+ url = yield tabClosedPromise();
+ ok(url.includes(name), "The tab for the popup should be closed.");
+ }
+ }
+
+ yield extension.startup();
+ let {uuid} = yield extension.awaitMessage("ready");
+
+ extension.sendMessage("page-action-show");
+ yield extension.awaitMessage("page-action-shown");
+ ok(isPageActionShown(uuid), "The PageAction should be shown.");
+
+ yield testPopup("default.html", uuid);
+ yield testPopup("a.html", uuid);
+ yield testPopup("", uuid);
+ yield testPopup("b.html", uuid);
+
+ yield extension.unload();
+ ok(!isPageActionShown(uuid), "The PageAction should be removed after unload.");
+});
+</script>
+
+</body>
+</html>
diff --git a/mobile/android/components/moz.build b/mobile/android/components/moz.build
new file mode 100644
index 0000000000..cac34b603c
--- /dev/null
+++ b/mobile/android/components/moz.build
@@ -0,0 +1,48 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPIDL_SOURCES += [
+ 'SessionStore.idl',
+]
+
+XPIDL_MODULE = 'MobileComponents'
+
+EXTRA_COMPONENTS += [
+ 'AboutRedirector.js',
+ 'AddonUpdateService.js',
+ 'BlocklistPrompt.js',
+ 'BrowserCLH.js',
+ 'ColorPicker.js',
+ 'ContentDispatchChooser.js',
+ 'ContentPermissionPrompt.js',
+ 'DirectoryProvider.js',
+ 'FilePicker.js',
+ 'FxAccountsPush.js',
+ 'HelperAppDialog.js',
+ 'ImageBlockingPolicy.js',
+ 'LoginManagerPrompter.js',
+ 'NSSDialogService.js',
+ 'PersistentNotificationHandler.js',
+ 'PresentationDevicePrompt.js',
+ 'PresentationRequestUIGlue.js',
+ 'PromptService.js',
+ 'SessionStore.js',
+ 'SiteSpecificUserAgent.js',
+ 'Snippets.js',
+ 'TabSource.js',
+ 'XPIDialogService.js',
+]
+
+# Keep it this way if at all possible. If you need preprocessing,
+# consider adding fields to AppConstants.jsm.
+EXTRA_PP_COMPONENTS += [
+ 'MobileComponents.manifest',
+]
+
+DIRS += [
+ 'extensions',
+ 'build',
+]
diff --git a/mobile/android/config/js_wrapper.sh b/mobile/android/config/js_wrapper.sh
new file mode 100755
index 0000000000..464d5c63c9
--- /dev/null
+++ b/mobile/android/config/js_wrapper.sh
@@ -0,0 +1,20 @@
+#! /bin/sh
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Wrapper for running SpiderMonkey js shell in automation with correct
+# LD_LIBRARY_PATH.
+
+# We don't have a reference to topsrcdir at this point, but we are invoked as
+# "$topsrcdir/mobile/android/config/js_wrapper.sh" so we can extract topsrcdir
+# from $0.
+topsrcdir=`cd \`dirname $0\`/../../..; pwd`
+
+JS_BINARY="$topsrcdir/jsshell/js"
+
+LD_LIBRARY_PATH="$topsrcdir/jsshell${LD_LIBRARY_PATH+:$LD_LIBRARY_PATH}"
+export LD_LIBRARY_PATH
+
+# Pass through all arguments and exit with status from js shell.
+exec "$JS_BINARY" "$@"
diff --git a/mobile/android/config/mozconfigs/android-api-15-frontend/nightly b/mobile/android/config/mozconfigs/android-api-15-frontend/nightly
new file mode 100644
index 0000000000..6fb88b0f52
--- /dev/null
+++ b/mobile/android/config/mozconfigs/android-api-15-frontend/nightly
@@ -0,0 +1,43 @@
+# Many things aren't appropriate for a frontend-only build.
+MOZ_AUTOMATION_BUILD_SYMBOLS=0
+MOZ_AUTOMATION_INSTALLER=0
+MOZ_AUTOMATION_L10N_CHECK=0
+MOZ_AUTOMATION_PACKAGE=0
+MOZ_AUTOMATION_PACKAGE_TESTS=0
+MOZ_AUTOMATION_SDK=0
+MOZ_AUTOMATION_UPDATE_PACKAGING=0
+MOZ_AUTOMATION_UPLOAD=0
+MOZ_AUTOMATION_UPLOAD_SYMBOLS=0
+
+NO_CACHE=1
+NO_NDK=1
+
+. "$topsrcdir/mobile/android/config/mozconfigs/common"
+
+ac_add_options --with-gradle="$topsrcdir/gradle-dist/bin/gradle"
+export GRADLE_MAVEN_REPOSITORY="file://$topsrcdir/jcentral"
+
+unset HOST_CC
+unset HOST_CXX
+
+ac_add_options --disable-compile-environment
+ac_add_options --disable-tests
+
+# From here on, like ../android-api-15/nightly.
+
+ac_add_options --enable-profiling
+
+# Android
+ac_add_options --with-android-min-sdk=15
+ac_add_options --target=arm-linux-androideabi
+
+ac_add_options --with-branding=mobile/android/branding/nightly
+
+# This will overwrite the default of stripping everything and keep the symbol table.
+# This is useful for profiling with eideticker. See bug 788680
+STRIP_FLAGS="--strip-debug"
+
+export MOZILLA_OFFICIAL=1
+export MOZ_TELEMETRY_REPORTING=1
+
+. "$topsrcdir/mobile/android/config/mozconfigs/common.override"
diff --git a/mobile/android/config/mozconfigs/android-api-15-gradle-dependencies/nightly b/mobile/android/config/mozconfigs/android-api-15-gradle-dependencies/nightly
new file mode 100644
index 0000000000..99789543c5
--- /dev/null
+++ b/mobile/android/config/mozconfigs/android-api-15-gradle-dependencies/nightly
@@ -0,0 +1,45 @@
+# Many things aren't appropriate for a frontend-only build.
+MOZ_AUTOMATION_BUILD_SYMBOLS=0
+MOZ_AUTOMATION_INSTALLER=0
+MOZ_AUTOMATION_L10N_CHECK=0
+MOZ_AUTOMATION_PACKAGE=0
+MOZ_AUTOMATION_PACKAGE_TESTS=0
+MOZ_AUTOMATION_SDK=0
+MOZ_AUTOMATION_UPDATE_PACKAGING=0
+MOZ_AUTOMATION_UPLOAD=0
+MOZ_AUTOMATION_UPLOAD_SYMBOLS=0
+
+NO_CACHE=1
+NO_NDK=1
+
+. "$topsrcdir/mobile/android/config/mozconfigs/common"
+
+# We want to download Gradle.
+ac_add_options --with-gradle
+# We want to use (and populate!) the local Nexus repository.
+export GRADLE_MAVEN_REPOSITORY="http://localhost:8081/nexus/content/repositories/central/"
+
+# From here on, just like ../android-api-15-frontend/nightly.
+
+ac_add_options --disable-compile-environment
+unset HOST_CC
+unset HOST_CXX
+
+ac_add_options --disable-tests
+
+ac_add_options --enable-profiling
+
+# Android
+ac_add_options --with-android-min-sdk=15
+ac_add_options --target=arm-linux-androideabi
+
+ac_add_options --with-branding=mobile/android/branding/nightly
+
+# This will overwrite the default of stripping everything and keep the symbol table.
+# This is useful for profiling with eideticker. See bug 788680
+STRIP_FLAGS="--strip-debug"
+
+export MOZILLA_OFFICIAL=1
+export MOZ_TELEMETRY_REPORTING=1
+
+. "$topsrcdir/mobile/android/config/mozconfigs/common.override"
diff --git a/mobile/android/config/mozconfigs/android-api-15-gradle/nightly b/mobile/android/config/mozconfigs/android-api-15-gradle/nightly
new file mode 100644
index 0000000000..d1bca2d674
--- /dev/null
+++ b/mobile/android/config/mozconfigs/android-api-15-gradle/nightly
@@ -0,0 +1,23 @@
+. "$topsrcdir/mobile/android/config/mozconfigs/common"
+
+ac_add_options --enable-profiling
+
+# Android
+ac_add_options --with-android-min-sdk=15
+ac_add_options --target=arm-linux-androideabi
+
+ac_add_options --with-branding=mobile/android/branding/nightly
+
+ac_add_options --with-gradle="$topsrcdir/gradle-dist/bin/gradle"
+export GRADLE_MAVEN_REPOSITORY="file://$topsrcdir/jcentral"
+
+# From here on, just like ../android-api-15/nightly.
+
+# This will overwrite the default of stripping everything and keep the symbol table.
+# This is useful for profiling with eideticker. See bug 788680
+STRIP_FLAGS="--strip-debug"
+
+export MOZILLA_OFFICIAL=1
+export MOZ_TELEMETRY_REPORTING=1
+
+. "$topsrcdir/mobile/android/config/mozconfigs/common.override"
diff --git a/mobile/android/config/mozconfigs/android-api-15/debug b/mobile/android/config/mozconfigs/android-api-15/debug
new file mode 100644
index 0000000000..90093edcb0
--- /dev/null
+++ b/mobile/android/config/mozconfigs/android-api-15/debug
@@ -0,0 +1,16 @@
+. "$topsrcdir/mobile/android/config/mozconfigs/common"
+
+# Global options
+ac_add_options --enable-debug
+ENABLE_MARIONETTE=1
+
+# Android
+ac_add_options --with-android-min-sdk=15
+ac_add_options --target=arm-linux-androideabi
+
+export MOZILLA_OFFICIAL=1
+export MOZ_TELEMETRY_REPORTING=1
+
+ac_add_options --with-branding=mobile/android/branding/official
+
+. "$topsrcdir/mobile/android/config/mozconfigs/common.override"
diff --git a/mobile/android/config/mozconfigs/android-api-15/l10n-nightly b/mobile/android/config/mozconfigs/android-api-15/l10n-nightly
new file mode 100644
index 0000000000..cdebcf1748
--- /dev/null
+++ b/mobile/android/config/mozconfigs/android-api-15/l10n-nightly
@@ -0,0 +1,27 @@
+. "$topsrcdir/mobile/android/config/mozconfigs/common"
+
+# L10n
+ac_add_options --with-l10n-base=..
+
+# Global options
+ac_add_options --disable-tests
+
+# Android
+ac_add_options --with-android-min-sdk=15
+ac_add_options --target=arm-linux-androideabi
+
+ac_add_options --with-system-zlib
+ac_add_options --enable-updater
+ac_add_options --enable-update-channel=${MOZ_UPDATE_CHANNEL}
+
+export MOZILLA_OFFICIAL=1
+
+ac_add_options --with-branding=mobile/android/branding/official
+
+ac_add_options --disable-stdcxx-compat
+
+# Don't autoclobber l10n, as this can lead to missing binaries and broken builds
+# Bug 1283438
+mk_add_options AUTOCLOBBER=
+
+. "$topsrcdir/mobile/android/config/mozconfigs/common.override"
diff --git a/mobile/android/config/mozconfigs/android-api-15/l10n-release b/mobile/android/config/mozconfigs/android-api-15/l10n-release
new file mode 100644
index 0000000000..e72e3bb849
--- /dev/null
+++ b/mobile/android/config/mozconfigs/android-api-15/l10n-release
@@ -0,0 +1,28 @@
+. "$topsrcdir/mobile/android/config/mozconfigs/common"
+
+# L10n
+ac_add_options --with-l10n-base=..
+
+# Global options
+ac_add_options --disable-tests
+
+# Android
+ac_add_options --with-android-min-sdk=15
+ac_add_options --target=arm-linux-androideabi
+
+ac_add_options --with-system-zlib
+ac_add_options --enable-updater
+ac_add_options --enable-update-channel=${MOZ_UPDATE_CHANNEL}
+
+export MOZILLA_OFFICIAL=1
+
+ac_add_options --enable-official-branding
+ac_add_options --with-branding=mobile/android/branding/official
+
+ac_add_options --disable-stdcxx-compat
+
+# Don't autoclobber l10n, as this can lead to missing binaries and broken builds
+# Bug 1283438
+mk_add_options AUTOCLOBBER=
+
+. "$topsrcdir/mobile/android/config/mozconfigs/common.override"
diff --git a/mobile/android/config/mozconfigs/android-api-15/nightly b/mobile/android/config/mozconfigs/android-api-15/nightly
new file mode 100644
index 0000000000..0ad80cb8b6
--- /dev/null
+++ b/mobile/android/config/mozconfigs/android-api-15/nightly
@@ -0,0 +1,18 @@
+. "$topsrcdir/mobile/android/config/mozconfigs/common"
+
+
+
+# Android
+ac_add_options --with-android-min-sdk=15
+ac_add_options --target=arm-linux-androideabi
+
+ac_add_options --with-branding=mobile/android/branding/official
+
+# This will overwrite the default of stripping everything and keep the symbol table.
+# This is useful for profiling with eideticker. See bug 788680
+STRIP_FLAGS="--strip-debug"
+
+export MOZILLA_OFFICIAL=1
+export MOZ_TELEMETRY_REPORTING=1
+
+. "$topsrcdir/mobile/android/config/mozconfigs/common.override"
diff --git a/mobile/android/config/mozconfigs/android-api-15/release b/mobile/android/config/mozconfigs/android-api-15/release
new file mode 100644
index 0000000000..edb662825d
--- /dev/null
+++ b/mobile/android/config/mozconfigs/android-api-15/release
@@ -0,0 +1,16 @@
+. "$topsrcdir/mobile/android/config/mozconfigs/common"
+
+# Android
+ac_add_options --with-android-min-sdk=15
+ac_add_options --target=arm-linux-androideabi
+
+ac_add_options --with-branding=mobile/android/branding/official
+
+ac_add_options --enable-updater
+
+export MOZILLA_OFFICIAL=1
+export MOZ_TELEMETRY_REPORTING=1
+
+ac_add_options --enable-official-branding
+
+. "$topsrcdir/mobile/android/config/mozconfigs/common.override"
diff --git a/mobile/android/config/mozconfigs/android-x86/debug b/mobile/android/config/mozconfigs/android-x86/debug
new file mode 100644
index 0000000000..e2d090f98f
--- /dev/null
+++ b/mobile/android/config/mozconfigs/android-x86/debug
@@ -0,0 +1,15 @@
+. "$topsrcdir/mobile/android/config/mozconfigs/common"
+
+# Global options
+ac_add_options --enable-debug
+
+# Android
+ac_add_options --target=i386-linux-android
+ac_add_options --with-android-min-sdk=15
+
+export MOZILLA_OFFICIAL=1
+export MOZ_TELEMETRY_REPORTING=1
+
+ac_add_options --with-branding=mobile/android/branding/official
+
+. "$topsrcdir/mobile/android/config/mozconfigs/common.override"
diff --git a/mobile/android/config/mozconfigs/android-x86/l10n-nightly b/mobile/android/config/mozconfigs/android-x86/l10n-nightly
new file mode 100644
index 0000000000..f725f2db10
--- /dev/null
+++ b/mobile/android/config/mozconfigs/android-x86/l10n-nightly
@@ -0,0 +1,26 @@
+. "$topsrcdir/mobile/android/config/mozconfigs/common"
+
+# L10n
+ac_add_options --with-l10n-base=..
+
+# Global options
+ac_add_options --disable-tests
+
+# Android
+ac_add_options --target=i386-linux-android
+ac_add_options --with-android-min-sdk=15
+
+ac_add_options --enable-updater
+ac_add_options --enable-update-channel=${MOZ_UPDATE_CHANNEL}
+
+export MOZILLA_OFFICIAL=1
+
+ac_add_options --with-branding=mobile/android/branding/official
+
+ac_add_options --disable-stdcxx-compat
+
+# Don't autoclobber l10n, as this can lead to missing binaries and broken builds
+# Bug 1283438
+mk_add_options AUTOCLOBBER=
+
+. "$topsrcdir/mobile/android/config/mozconfigs/common.override"
diff --git a/mobile/android/config/mozconfigs/android-x86/l10n-release b/mobile/android/config/mozconfigs/android-x86/l10n-release
new file mode 100644
index 0000000000..61f871f6dd
--- /dev/null
+++ b/mobile/android/config/mozconfigs/android-x86/l10n-release
@@ -0,0 +1,27 @@
+. "$topsrcdir/mobile/android/config/mozconfigs/common"
+
+# L10n
+ac_add_options --with-l10n-base=..
+
+# Global options
+ac_add_options --disable-tests
+
+# Android
+ac_add_options --target=i386-linux-android
+ac_add_options --with-android-min-sdk=15
+
+ac_add_options --enable-updater
+ac_add_options --enable-update-channel=${MOZ_UPDATE_CHANNEL}
+
+export MOZILLA_OFFICIAL=1
+
+ac_add_options --enable-official-branding
+ac_add_options --with-branding=mobile/android/branding/official
+
+ac_add_options --disable-stdcxx-compat
+
+# Don't autoclobber l10n, as this can lead to missing binaries and broken builds
+# Bug 1283438
+mk_add_options AUTOCLOBBER=
+
+. "$topsrcdir/mobile/android/config/mozconfigs/common.override"
diff --git a/mobile/android/config/mozconfigs/android-x86/nightly b/mobile/android/config/mozconfigs/android-x86/nightly
new file mode 100644
index 0000000000..c2f73febb3
--- /dev/null
+++ b/mobile/android/config/mozconfigs/android-x86/nightly
@@ -0,0 +1,17 @@
+. "$topsrcdir/mobile/android/config/mozconfigs/common"
+
+ac_add_options --target=i386-linux-android
+ac_add_options --with-android-min-sdk=15
+
+
+
+ac_add_options --with-branding=mobile/android/branding/official
+
+# This will overwrite the default of stripping everything and keep the symbol table.
+# This is useful for profiling with eideticker. See bug 788680
+STRIP_FLAGS="--strip-debug"
+
+export MOZILLA_OFFICIAL=1
+export MOZ_TELEMETRY_REPORTING=1
+
+. "$topsrcdir/mobile/android/config/mozconfigs/common.override"
diff --git a/mobile/android/config/mozconfigs/android-x86/release b/mobile/android/config/mozconfigs/android-x86/release
new file mode 100644
index 0000000000..7622151c24
--- /dev/null
+++ b/mobile/android/config/mozconfigs/android-x86/release
@@ -0,0 +1,16 @@
+. "$topsrcdir/mobile/android/config/mozconfigs/common"
+
+# Android
+ac_add_options --target=i386-linux-android
+ac_add_options --with-android-min-sdk=15
+
+ac_add_options --enable-updater
+
+ac_add_options --with-branding=mobile/android/branding/official
+
+export MOZILLA_OFFICIAL=1
+export MOZ_TELEMETRY_REPORTING=1
+
+ac_add_options --enable-official-branding
+
+. "$topsrcdir/mobile/android/config/mozconfigs/common.override"
diff --git a/mobile/android/config/mozconfigs/common b/mobile/android/config/mozconfigs/common
new file mode 100644
index 0000000000..1acb91ebbe
--- /dev/null
+++ b/mobile/android/config/mozconfigs/common
@@ -0,0 +1,83 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This file is included at the top of all native android mozconfigs
+if [ "x$IS_NIGHTLY" = "xyes" ]; then
+ MOZ_AUTOMATION_UPLOAD_SYMBOLS=${MOZ_AUTOMATION_UPLOAD_SYMBOLS-1}
+fi
+
+MOZ_AUTOMATION_L10N_CHECK=0
+. "$topsrcdir/build/mozconfig.common"
+
+# In TaskCluster, the Java JRE/JDK are installed from tooltool, but that
+# install doesn't work on the old Buildbot mock builders (CentOS 6.2), so
+# the relevant env vars are not set up in that case, leaving the build to
+# run from the JRE/JDK in /usr/lib/jvm.
+if [ ! -f /etc/redhat-release ] || [ "$(< /etc/redhat-release)" != "CentOS release 6.2 (Final)" ]; then
+ # set JAVA_HOME to find the JRE/JDK from tooltool. Several scripts in the JDK
+ # assume `java` is in PATH, so set that too. To see how this tarball is built,
+ # see taskcluster/scripts/misc/repackage-jdk.sh
+ export JAVA_HOME="$topsrcdir/java_home"
+ export PATH="$PATH:$topsrcdir/java_home/bin"
+
+ mk_add_options "export JAVA_HOME=$topsrcdir/java_home"
+ mk_add_options "export PATH=$PATH:$topsrcdir/java_home/bin"
+fi
+
+ac_add_options --enable-elf-hack
+
+ANDROID_NDK_VERSION="r10e"
+ANDROID_NDK_VERSION_32BIT="r8c"
+
+# Build Fennec
+ac_add_options --enable-application=mobile/android
+ac_add_options --with-android-sdk="$topsrcdir/android-sdk-linux"
+
+if [ -z "$NO_NDK" ]; then
+ ac_add_options --with-android-ndk="$topsrcdir/android-ndk"
+ ac_add_options --with-android-gnu-compiler-version=4.9
+fi
+
+ac_add_options --with-system-zlib
+ac_add_options --enable-update-channel=${MOZ_UPDATE_CHANNEL}
+
+ac_add_options --with-mozilla-api-keyfile=/builds/mozilla-fennec-geoloc-api.key
+
+# MOZ_INSTALL_TRACKING does not guarantee MOZ_UPDATE_CHANNEL will be set so we
+# provide a default state. Currently, the default state provides a default
+# keyfile because an assertion will be thrown if MOZ_INSTALL_TRACKING is
+# specified but a keyfile is not. This assertion can catch if we misconfigure a
+# release or beta build and it does not have a valid keyfile.
+#
+# However, by providing a default keyfile, if we misconfigure beta or release,
+# the default keyfile may be used instead and the assertion won't catch the
+# error. Therefore, it would be ideal to have MOZ_INSTALL_TRACKING guarantee
+# MOZ_UPDATE_CHANNEL was set so we can remove the default case. This may occur
+# when release promotion is implemented on Android.
+#
+# In all cases, we don't upload Adjust pings in automation.
+if test "$MOZ_UPDATE_CHANNEL" = "release" ; then
+ ac_add_options --with-adjust-sdk-keyfile=/builds/adjust-sdk.token
+elif test "$MOZ_UPDATE_CHANNEL" = "beta" ; then
+ ac_add_options --with-adjust-sdk-keyfile=/builds/adjust-sdk-beta.token
+else
+ ac_add_options --with-adjust-sdk-keyfile="$topsrcdir/mobile/android/base/adjust-sdk-sandbox.token"
+fi
+export SOCORRO_SYMBOL_UPLOAD_TOKEN_FILE=/builds/crash-stats-api.token
+
+# Package js shell.
+export MOZ_PACKAGE_JSSHELL=1
+
+# Use ccache
+. "$topsrcdir/build/mozconfig.cache"
+
+HOST_CC="$topsrcdir/gcc/bin/gcc"
+HOST_CXX="$topsrcdir/gcc/bin/g++"
+
+. "$topsrcdir/build/unix/mozconfig.stdcxx"
+
+# Use libc++ as our C++ standard library
+ac_add_options --with-android-cxx-stl=libc++
+
+JS_BINARY="$topsrcdir/mobile/android/config/js_wrapper.sh"
diff --git a/mobile/android/config/mozconfigs/common.override b/mobile/android/config/mozconfigs/common.override
new file mode 100644
index 0000000000..e2382d21e8
--- /dev/null
+++ b/mobile/android/config/mozconfigs/common.override
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This file is included at the bottom of all native android mozconfigs
+#
+# Disable enforcing that add-ons are signed by the trusted root
+MOZ_REQUIRE_SIGNING=0
+
+. "$topsrcdir/build/mozconfig.common.override"
+. "$topsrcdir/build/mozconfig.cache"
diff --git a/mobile/android/config/mozconfigs/public-partner/distribution_sample/mozconfig1 b/mobile/android/config/mozconfigs/public-partner/distribution_sample/mozconfig1
new file mode 100644
index 0000000000..b790769c69
--- /dev/null
+++ b/mobile/android/config/mozconfigs/public-partner/distribution_sample/mozconfig1
@@ -0,0 +1,21 @@
+# currently a copy of mobile/android/config/mozconfigs/android-api-15/nightly
+. "$topsrcdir/mobile/android/config/mozconfigs/common"
+
+ac_add_options --enable-profiling
+
+# Android
+ac_add_options --with-android-min-sdk=15
+ac_add_options --target=arm-linux-androideabi
+
+ac_add_options --with-branding=mobile/android/branding/nightly
+
+ac_add_options --with-android-distribution-directory=/home/worker/workspace/build/partner
+
+# This will overwrite the default of stripping everything and keep the symbol table.
+# This is useful for profiling with eideticker. See bug 788680
+STRIP_FLAGS="--strip-debug"
+
+export MOZILLA_OFFICIAL=1
+export MOZ_TELEMETRY_REPORTING=1
+
+. "$topsrcdir/mobile/android/config/mozconfigs/common.override"
diff --git a/mobile/android/config/proguard/adjust-keeps.cfg b/mobile/android/config/proguard/adjust-keeps.cfg
new file mode 100644
index 0000000000..0c0fc2158d
--- /dev/null
+++ b/mobile/android/config/proguard/adjust-keeps.cfg
@@ -0,0 +1,20 @@
+# Rules to make the Adjust install tracking library work.
+# via https://github.com/adjust/android_sdk#5-add-permissions
+
+-keep class com.adjust.sdk.plugin.MacAddressUtil {
+ java.lang.String getMacAddress(android.content.Context);
+}
+-keep class com.adjust.sdk.plugin.AndroidIdUtil {
+ java.lang.String getAndroidId(android.content.Context);
+}
+-keep class com.google.android.gms.common.ConnectionResult {
+ int SUCCESS;
+}
+-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient {
+ com.google.android.gms.ads.identifier.AdvertisingIdClient$Info
+ getAdvertisingIdInfo (android.content.Context);
+}
+-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient$Info {
+ java.lang.String getId ();
+ boolean isLimitAdTrackingEnabled();
+}
diff --git a/mobile/android/config/proguard/appcompat-v7-keeps.cfg b/mobile/android/config/proguard/appcompat-v7-keeps.cfg
new file mode 100644
index 0000000000..fdaa2a4aa9
--- /dev/null
+++ b/mobile/android/config/proguard/appcompat-v7-keeps.cfg
@@ -0,0 +1,11 @@
+# Avoid https://code.google.com/p/android/issues/detail?id=187611 and
+# http://stackoverflow.com/q/32813894 when building with Gradle. Why
+# these aren't defined in appcompat-v7.aar/proguard.txt is beyond me.
+
+-keep public class android.support.v7.widget.** { *; }
+-keep public class android.support.v7.internal.widget.** { *; }
+-keep public class android.support.v7.internal.view.menu.** { *; }
+
+-keep public class * extends android.support.v4.view.ActionProvider {
+ public <init>(android.content.Context);
+}
diff --git a/mobile/android/config/proguard/leakcanary-keeps.cfg b/mobile/android/config/proguard/leakcanary-keeps.cfg
new file mode 100644
index 0000000000..f9e5df87c1
--- /dev/null
+++ b/mobile/android/config/proguard/leakcanary-keeps.cfg
@@ -0,0 +1,7 @@
+# LeakCanary
+-keep class org.eclipse.mat.** { *; }
+-keep class com.squareup.leakcanary.** { *; }
+-keep class com.squareup.haha.** { *; }
+
+# With LeakCanary 1.4-beta1 this creates a pile of warnings
+-dontwarn com.squareup.haha.**
diff --git a/mobile/android/config/proguard/play-services-keeps.cfg b/mobile/android/config/proguard/play-services-keeps.cfg
new file mode 100644
index 0000000000..b3aaf80aa9
--- /dev/null
+++ b/mobile/android/config/proguard/play-services-keeps.cfg
@@ -0,0 +1,19 @@
+# Rules to prevent Google Play Services from exploding
+# (From http://developer.android.com/google/play-services/setup.html#Proguard
+# With the reference to "Object" changed so it'll actually *work*...)
+-keep class * extends java.util.ListResourceBundle {
+ protected java.lang.Object[][] getContents();
+}
+
+-keep public class com.google.android.gms.common.internal.safeparcel.SafeParcelable {
+ public static final *** NULL;
+}
+
+-keepnames @com.google.android.gms.common.annotation.KeepName class *
+-keepclassmembernames class * {
+ @com.google.android.gms.common.annotation.KeepName *;
+}
+
+-keepnames class * implements android.os.Parcelable {
+ public static final ** CREATOR;
+}
diff --git a/mobile/android/config/proguard/proguard-android.cfg b/mobile/android/config/proguard/proguard-android.cfg
new file mode 100644
index 0000000000..93acf28d10
--- /dev/null
+++ b/mobile/android/config/proguard/proguard-android.cfg
@@ -0,0 +1,78 @@
+# This is a configuration file for ProGuard.
+# http://proguard.sourceforge.net/index.html#manual/usage.html
+#
+# Starting with version 2.2 of the Android plugin for Gradle, these files are no longer used. Newer
+# versions are distributed with the plugin and unpacked at build time. Files in this directory are
+# no longer maintained.
+
+-dontusemixedcaseclassnames
+-dontskipnonpubliclibraryclasses
+-verbose
+
+# Optimization is turned off by default. Dex does not like code run
+# through the ProGuard optimize and preverify steps (and performs some
+# of these optimizations on its own).
+-dontoptimize
+-dontpreverify
+# Note that if you want to enable optimization, you cannot just
+# include optimization flags in your own project configuration file;
+# instead you will need to point to the
+# "proguard-android-optimize.txt" file instead of this one from your
+# project.properties file.
+
+-keepattributes *Annotation*
+-keep public class com.google.vending.licensing.ILicensingService
+-keep public class com.android.vending.licensing.ILicensingService
+
+# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
+-keepclasseswithmembernames class * {
+ native <methods>;
+}
+
+# keep setters in Views so that animations can still work.
+# see http://proguard.sourceforge.net/manual/examples.html#beans
+-keepclassmembers public class * extends android.view.View {
+ void set*(***);
+ *** get*();
+}
+
+# We want to keep methods in Activity that could be used in the XML attribute onClick
+-keepclassmembers class * extends android.app.Activity {
+ public void *(android.view.View);
+}
+
+# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
+-keepclassmembers enum * {
+ public static **[] values();
+ public static ** valueOf(java.lang.String);
+}
+
+-keepclassmembers class * implements android.os.Parcelable {
+ public static final android.os.Parcelable$Creator CREATOR;
+}
+
+-keepclassmembers class **.R$* {
+ public static <fields>;
+}
+
+# The support library contains references to newer platform versions.
+# Don't warn about those in case this app is linking against an older
+# platform version. We know about them, and they are safe.
+-dontwarn android.support.**
+
+# Understand the @Keep support annotation.
+-keep class android.support.annotation.Keep
+
+-keep @android.support.annotation.Keep class * {*;}
+
+-keepclasseswithmembers class * {
+ @android.support.annotation.Keep <methods>;
+}
+
+-keepclasseswithmembers class * {
+ @android.support.annotation.Keep <fields>;
+}
+
+-keepclasseswithmembers class * {
+ @android.support.annotation.Keep <init>(...);
+}
diff --git a/mobile/android/config/proguard/proguard.cfg b/mobile/android/config/proguard/proguard.cfg
new file mode 100644
index 0000000000..f44730e72c
--- /dev/null
+++ b/mobile/android/config/proguard/proguard.cfg
@@ -0,0 +1,185 @@
+# Dalvik renders preverification unuseful (Would just slightly bloat the file).
+-dontpreverify
+
+# Uncomment to have Proguard list dead code detected during the run - useful for cleaning up the codebase.
+# -printusage
+
+-dontskipnonpubliclibraryclassmembers
+-verbose
+-allowaccessmodification
+
+# Preserve all fundamental application classes.
+-keep public class * extends android.app.Activity
+-keep public class * extends android.app.Application
+-keep public class * extends android.app.Service
+-keep public class * extends android.app.backup.BackupAgentHelper
+-keep public class * extends android.content.BroadcastReceiver
+-keep public class * extends android.content.ContentProvider
+-keep public class * extends android.preference.Preference
+-keep public class * extends org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter
+-keep class org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter
+
+-keep public class * extends android.support.v4.app.Fragment
+
+# Preserve all native method names and the names of their classes.
+-keepclasseswithmembernames class * {
+ native <methods>;
+}
+
+-keepclasseswithmembers class * {
+ public <init>(android.content.Context, android.util.AttributeSet, int);
+}
+
+-keepclassmembers class * extends android.app.Activity {
+ public void *(android.view.View);
+}
+
+
+# Keep setters in Views so that animations can still work.
+# See http://proguard.sourceforge.net/manual/examples.html#beans
+# From tools/proguard/proguard-android.txt.
+-keepclassmembers public class * extends android.view.View {
+ void set*(***);
+ *** get*();
+}
+
+# Keep setters in support Fragment so that Robocop tests work,
+# specifically testBrowserSearchVisibility.
+-keepclassmembers public class * extends android.support.v4.app.Fragment {
+ void set*(***);
+ *** get*();
+}
+
+# Preserve enums. (For awful reasons, the runtime accesses them using introspection...)
+-keepclassmembers enum * {
+ *;
+}
+
+#
+# Rules from ProGuard's Android example:
+# http://proguard.sourceforge.net/manual/examples.html#androidapplication
+#
+
+# Keep a fixed source file attribute and all line number tables to get line
+# numbers in the stack traces.
+# You can comment this out if you're not interested in stack traces.
+
+-renamesourcefileattribute SourceFile
+-keepattributes SourceFile,LineNumberTable
+
+# RemoteViews might need annotations.
+
+-keepattributes *Annotation*
+
+# Preserve all View implementations, their special context constructors, and
+# their setters.
+
+-keep public class * extends android.view.View {
+ public <init>(android.content.Context);
+ public <init>(android.content.Context, android.util.AttributeSet);
+ public <init>(android.content.Context, android.util.AttributeSet, int);
+ public void set*(...);
+}
+
+# Preserve all classes that have special context constructors, and the
+# constructors themselves.
+
+-keepclasseswithmembers class * {
+ public <init>(android.content.Context, android.util.AttributeSet);
+}
+
+# Preserve the special fields of all Parcelable implementations.
+
+-keepclassmembers class * implements android.os.Parcelable {
+ static android.os.Parcelable$Creator CREATOR;
+}
+
+# Preserve static fields of inner classes of R classes that might be accessed
+# through introspection.
+
+-keepclassmembers class **.R$* {
+ public static <fields>;
+}
+
+# Preserve the required interface from the License Verification Library
+# (but don't nag the developer if the library is not used at all).
+
+-keep public interface com.android.vending.licensing.ILicensingService
+
+-dontnote com.android.vending.licensing.ILicensingService
+
+# The Android Compatibility library references some classes that may not be
+# present in all versions of the API, but we know that's ok.
+
+-dontwarn android.support.**
+
+# Preserve all native method names and the names of their classes.
+
+-keepclasseswithmembernames class * {
+ native <methods>;
+}
+
+#
+# Mozilla-specific rules
+#
+# Merging classes can generate dex warnings about anonymous inner classes.
+-optimizations !class/merging/horizontal
+-optimizations !class/merging/vertical
+
+# This optimisation causes corrupt bytecode if we run more than two passes.
+# Testing shows that running the extra passes of everything else saves us
+# more than this optimisation does, so bye bye!
+-optimizations !code/allocation/variable
+
+# Keep miscellaneous targets.
+
+# Keep Robocop targets. TODO: Can omit these from release builds. Also, Bug 916507.
+
+# Same formula as above...
+-keep @interface org.mozilla.gecko.annotation.RobocopTarget
+-keep @org.mozilla.gecko.annotation.RobocopTarget class *
+-keepclassmembers class * {
+ @org.mozilla.gecko.annotation.RobocopTarget *;
+}
+-keepclassmembers @org.mozilla.gecko.annotation.RobocopTarget class * {
+ *;
+}
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.annotation.RobocopTarget <methods>;
+}
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.annotation.RobocopTarget <fields>;
+}
+
+-keep class **.R$*
+
+# Keep all interfaces that might be dynamically required by Java Addons.
+-keep class org.mozilla.javaaddons.* {
+ *;
+}
+
+-keep class org.mozilla.javaaddons.*$* {
+ *;
+}
+
+# Disable obfuscation because it makes exception stack traces more difficult to read.
+-dontobfuscate
+
+# Suppress warnings about missing descriptor classes.
+#-dontnote **,!ch.boye.**,!org.mozilla.gecko.sync.**
+
+-include "play-services-keeps.cfg"
+
+# Don't print spurious warnings from the support library.
+# See: http://stackoverflow.com/questions/22441366/note-android-support-v4-text-icucompatics-cant-find-dynamically-referenced-cl
+-dontnote android.support.**
+
+-include "adjust-keeps.cfg"
+
+-include "leakcanary-keeps.cfg"
+
+-include "appcompat-v7-keeps.cfg"
+
+-include "proguard-android.cfg"
+
+-include "../../geckoview/proguard-rules.txt"
diff --git a/mobile/android/config/proguard/strip-libs.cfg b/mobile/android/config/proguard/strip-libs.cfg
new file mode 100644
index 0000000000..80a1f151c9
--- /dev/null
+++ b/mobile/android/config/proguard/strip-libs.cfg
@@ -0,0 +1,41 @@
+# Proguard step for stripping debug information.
+#
+# This is useful to work around a bug in the way Proguard handles debug information: it
+# sometimes corrupts it. Classes with corrupt debug information cannot be dexed, but
+# classes with *no* debug information can be. There's no way to configure Proguard to
+# delete debug information on a per-class basis, so we need this special extra step for
+# stripping debug information only from those classes for which the Proguard bug is
+# encountered.
+#
+# Currently, this pass is applied to all bundled library jars for which we are not
+# compiling the source. This is slightly more than is strictly necessary to work around
+# the Proguard bug, but such debug information is of negligible value and stripping it
+# too slightly simplifies the makefile and saves us a handful of kilobytes of binary size.
+#
+# Configuring Proguard to do nothing except strip metadata is done by having it run only
+# the obfuscation pass, but with a configuration that prevents it from renaming any classes.
+# It then attempts to delete class metadata, so we further configure it not to do so for
+# anything except the problematic debug information.
+
+# Run only the obfuscator.
+-dontoptimize
+-dontshrink
+-dontpreverify
+-verbose
+
+# Don't rename anything.
+-keeppackagenames
+
+# Seriously, don't rename anything.
+-keep class *
+-keepclassmembers class * {
+ *;
+}
+
+# Don't delete other useful metadata.
+-keepattributes Exceptions,InnerClasses,Signature,Deprecated,*Annotation*,EnclosingMethod
+
+# Don't print spurious warnings from the support library.
+# See: http://stackoverflow.com/questions/22441366/note-android-support-v4-text-icucompatics-cant-find-dynamically-referenced-cl
+-dontnote android.support.**
+-dontwarn android.support.**
diff --git a/mobile/android/config/tooltool-manifests/android-frontend/releng.manifest b/mobile/android/config/tooltool-manifests/android-frontend/releng.manifest
new file mode 100644
index 0000000000..ef4d299c14
--- /dev/null
+++ b/mobile/android/config/tooltool-manifests/android-frontend/releng.manifest
@@ -0,0 +1,57 @@
+[
+{
+"versions": [
+ "Android SDK 6.0 / API 23",
+ "Android tools r24.4",
+ "Android build tools 23.0.3",
+ "Android Support Repository (Support Library 23.0.1)",
+ "Google Support Repository (Google Play Services 8.1.0)"
+],
+"size": 573952124,
+"visibility": "internal",
+"digest": "1d495d7a7386af3f27b14982e0ff7b0963fd1a63a08040b9b1db0e94c9681fa3704c195ba8be23b5f73e15101b2b767293bc8f96e0584e17867ef13b074e5038",
+"algorithm": "sha512",
+"filename": "android-sdk-linux.tar.xz",
+"unpack": true
+},
+{
+"size": 167175,
+"visibility": "public",
+"digest": "0b71a936edf5bd70cf274aaa5d7abc8f77fe8e7b5593a208f805cc9436fac646b9c4f0b43c2b10de63ff3da671497d35536077ecbc72dba7f8159a38b580f831",
+"algorithm": "sha512",
+"filename": "sccache.tar.bz2",
+"unpack": true
+},
+{
+"size": 30899096,
+"visibility": "public",
+"digest": "ac9f5f95d11580d3dbeff87e80a585fe4d324b270dabb91b1165686acab47d99fa6651074ab0be09420239a5d6af38bb2c539506962a7b44e0ed4d080bba2953",
+"algorithm": "sha512",
+"filename": "java_home-1.7.0-openjdk-1.7.0.85.x86_64.tar.xz",
+"unpack": true
+},
+{
+"algorithm": "sha512",
+"visibility": "public",
+"filename": "jcentral.tar.xz",
+"unpack": true,
+"digest": "8e50f0993e129d3447b228d7da77d661d4ae3d490d791630dabb73e7d8021920f765317a258fd6e819aca48daaa8d0d86ec07cb6c30736199bbf2c4f92270cb5",
+"size": 47164284
+},
+{
+"algorithm": "sha512",
+"visibility": "public",
+"filename": "gradle-dist.tar.xz",
+"unpack": true,
+"digest": "e3cfe7f8259ad97722243d4e873d5a05c014bfc24d637427f89d804bf5073290229c778ea303142cf06c2dc79e0492f23521f57d3a73825f55b8db587317646f",
+"size": 51753660
+},
+{
+"algorithm": "sha512",
+"visibility": "public",
+"filename": "dotgradle.tar.xz",
+"unpack": true,
+"digest": "9f082ccd71ad18991eb71fcad355c6990f50a72a09ab9b79696521485656083a72faf5a8d4714de9c4b901ee2319b6786a51964846bb7075061642a8505501c2",
+"size": 512
+}
+]
diff --git a/mobile/android/config/tooltool-manifests/android-gradle-dependencies/releng.manifest b/mobile/android/config/tooltool-manifests/android-gradle-dependencies/releng.manifest
new file mode 100644
index 0000000000..03b6b5bb8e
--- /dev/null
+++ b/mobile/android/config/tooltool-manifests/android-gradle-dependencies/releng.manifest
@@ -0,0 +1,41 @@
+[
+{
+"versions": [
+ "Android SDK 6.0 / API 23",
+ "Android tools r24.4",
+ "Android build tools 23.0.3",
+ "Android Support Repository (Support Library 23.0.1)",
+ "Google Support Repository (Google Play Services 8.1.0)"
+],
+"size": 573952124,
+"visibility": "internal",
+"digest": "1d495d7a7386af3f27b14982e0ff7b0963fd1a63a08040b9b1db0e94c9681fa3704c195ba8be23b5f73e15101b2b767293bc8f96e0584e17867ef13b074e5038",
+"algorithm": "sha512",
+"filename": "android-sdk-linux.tar.xz",
+"unpack": true
+},
+{
+"size": 167175,
+"visibility": "public",
+"digest": "0b71a936edf5bd70cf274aaa5d7abc8f77fe8e7b5593a208f805cc9436fac646b9c4f0b43c2b10de63ff3da671497d35536077ecbc72dba7f8159a38b580f831",
+"algorithm": "sha512",
+"filename": "sccache.tar.bz2",
+"unpack": true
+},
+{
+"size": 30899096,
+"visibility": "public",
+"digest": "ac9f5f95d11580d3dbeff87e80a585fe4d324b270dabb91b1165686acab47d99fa6651074ab0be09420239a5d6af38bb2c539506962a7b44e0ed4d080bba2953",
+"algorithm": "sha512",
+"unpack": true,
+"filename": "java_home-1.7.0-openjdk-1.7.0.85.x86_64.tar.xz"
+},
+{
+"algorithm": "sha512",
+"visibility": "public",
+"filename": "dotgradle-online.tar.xz",
+"unpack": true,
+"digest": "0979eb1dcd9349a9c3f51f24747bb6e19e803226d7150fcf6846889ae24a8df925d03edfac038a5330822703c51130d4f5757d9f4caff7bcb2b6f71858c024d3",
+"size": 512
+}
+]
diff --git a/mobile/android/config/tooltool-manifests/android-x86/releng.manifest b/mobile/android/config/tooltool-manifests/android-x86/releng.manifest
new file mode 100644
index 0000000000..523e23e0a8
--- /dev/null
+++ b/mobile/android/config/tooltool-manifests/android-x86/releng.manifest
@@ -0,0 +1,72 @@
+[
+{
+"version": "Android NDK r11c",
+"size": 138388708,
+"visibility": "internal",
+"digest": "2dc605644e84fadf399a4e5cb60dfb3b42518803f80de15ef310ead4d0084a01dce3052bbf7d9dfb8bbe027019250235afe0a1e4ffdc87babc0f2bdbf2403232",
+"algorithm": "sha512",
+"filename": "android-ndk.tar.bz2",
+"unpack": true
+},
+{
+"versions": [
+ "Android SDK 6.0 / API 23",
+ "Android tools r24.4",
+ "Android build tools 23.0.3",
+ "Android Support Repository (Support Library 23.0.1)",
+ "Google Support Repository (Google Play Services 8.1.0)"
+],
+"size": 573952124,
+"visibility": "internal",
+"digest": "1d495d7a7386af3f27b14982e0ff7b0963fd1a63a08040b9b1db0e94c9681fa3704c195ba8be23b5f73e15101b2b767293bc8f96e0584e17867ef13b074e5038",
+"algorithm": "sha512",
+"filename": "android-sdk-linux.tar.xz",
+"unpack": true
+},
+{
+"size": 167175,
+"digest": "0b71a936edf5bd70cf274aaa5d7abc8f77fe8e7b5593a208f805cc9436fac646b9c4f0b43c2b10de63ff3da671497d35536077ecbc72dba7f8159a38b580f831",
+"algorithm": "sha512",
+"filename": "sccache.tar.bz2",
+"unpack": true
+},
+{
+"size": 4906080,
+"digest": "d735544e039da89382c53b2302b7408d4610247b4f8b5cdc5a4d5a8ec5470947b19e8ea7f7a37e78222e661347e394e0030d81f41534138b527b14e9c4e55634",
+"algorithm": "sha512",
+"filename": "jsshell.tar.xz",
+"unpack": true
+},
+{
+"version": "gcc 4.8.5 + PR64905",
+"size": 80160264,
+"digest": "c1a9dc9da289b8528874d16300b9d13a997cec99195bb0bc46ff665216d8535d6d6cb5af6b4b1f2749af6815dab12e703fdb3849014e5c23a70eff351a0baf4e",
+"algorithm": "sha512",
+"filename": "gcc.tar.xz",
+"unpack": true
+},
+{
+"algorithm": "sha512",
+"visibility": "public",
+"filename": "jcentral.tar.xz",
+"unpack": true,
+"digest": "8e50f0993e129d3447b228d7da77d661d4ae3d490d791630dabb73e7d8021920f765317a258fd6e819aca48daaa8d0d86ec07cb6c30736199bbf2c4f92270cb5",
+"size": 47164284
+},
+{
+"algorithm": "sha512",
+"visibility": "public",
+"filename": "gradle-dist.tar.xz",
+"unpack": true,
+"digest": "e3cfe7f8259ad97722243d4e873d5a05c014bfc24d637427f89d804bf5073290229c778ea303142cf06c2dc79e0492f23521f57d3a73825f55b8db587317646f",
+"size": 51753660
+},
+{
+"size": 30899096,
+"visibility": "public",
+"digest": "ac9f5f95d11580d3dbeff87e80a585fe4d324b270dabb91b1165686acab47d99fa6651074ab0be09420239a5d6af38bb2c539506962a7b44e0ed4d080bba2953",
+"algorithm": "sha512",
+"filename": "java_home-1.7.0-openjdk-1.7.0.85.x86_64.tar.xz",
+"unpack": true
+}
+]
diff --git a/mobile/android/config/tooltool-manifests/android/releng.manifest b/mobile/android/config/tooltool-manifests/android/releng.manifest
new file mode 100644
index 0000000000..7516299793
--- /dev/null
+++ b/mobile/android/config/tooltool-manifests/android/releng.manifest
@@ -0,0 +1,90 @@
+[
+{
+"version": "Android NDK r11c",
+"size": 138388708,
+"visibility": "internal",
+"digest": "2dc605644e84fadf399a4e5cb60dfb3b42518803f80de15ef310ead4d0084a01dce3052bbf7d9dfb8bbe027019250235afe0a1e4ffdc87babc0f2bdbf2403232",
+"algorithm": "sha512",
+"filename": "android-ndk.tar.bz2",
+"unpack": true
+},
+{
+"versions": [
+ "Android SDK 6.0 / API 23",
+ "Android tools r24.4",
+ "Android build tools 23.0.3",
+ "Android Support Repository (Support Library 23.0.1)",
+ "Google Support Repository (Google Play Services 8.1.0)"
+],
+"size": 573952124,
+"visibility": "internal",
+"digest": "1d495d7a7386af3f27b14982e0ff7b0963fd1a63a08040b9b1db0e94c9681fa3704c195ba8be23b5f73e15101b2b767293bc8f96e0584e17867ef13b074e5038",
+"algorithm": "sha512",
+"filename": "android-sdk-linux.tar.xz",
+"unpack": true
+},
+{
+"size": 167175,
+"visibility": "public",
+"digest": "0b71a936edf5bd70cf274aaa5d7abc8f77fe8e7b5593a208f805cc9436fac646b9c4f0b43c2b10de63ff3da671497d35536077ecbc72dba7f8159a38b580f831",
+"algorithm": "sha512",
+"filename": "sccache.tar.bz2",
+"unpack": true
+},
+{
+"size": 4906080,
+"visibility": "public",
+"unpack": true,
+"digest": "d735544e039da89382c53b2302b7408d4610247b4f8b5cdc5a4d5a8ec5470947b19e8ea7f7a37e78222e661347e394e0030d81f41534138b527b14e9c4e55634",
+"algorithm": "sha512",
+"filename": "jsshell.tar.xz"
+},
+{
+"version": "gcc 4.8.5 + PR64905",
+"size": 80160264,
+"digest": "c1a9dc9da289b8528874d16300b9d13a997cec99195bb0bc46ff665216d8535d6d6cb5af6b4b1f2749af6815dab12e703fdb3849014e5c23a70eff351a0baf4e",
+"algorithm": "sha512",
+"filename": "gcc.tar.xz",
+"unpack": true
+},
+{
+"size": 30899096,
+"visibility": "public",
+"digest": "ac9f5f95d11580d3dbeff87e80a585fe4d324b270dabb91b1165686acab47d99fa6651074ab0be09420239a5d6af38bb2c539506962a7b44e0ed4d080bba2953",
+"algorithm": "sha512",
+"filename": "java_home-1.7.0-openjdk-1.7.0.85.x86_64.tar.xz",
+"unpack": true
+},
+{
+"algorithm": "sha512",
+"visibility": "public",
+"filename": "jcentral.tar.xz",
+"unpack": true,
+"digest": "8e50f0993e129d3447b228d7da77d661d4ae3d490d791630dabb73e7d8021920f765317a258fd6e819aca48daaa8d0d86ec07cb6c30736199bbf2c4f92270cb5",
+"size": 47164284
+},
+{
+"algorithm": "sha512",
+"visibility": "public",
+"filename": "gradle-dist.tar.xz",
+"unpack": true,
+"digest": "e3cfe7f8259ad97722243d4e873d5a05c014bfc24d637427f89d804bf5073290229c778ea303142cf06c2dc79e0492f23521f57d3a73825f55b8db587317646f",
+"size": 51753660
+},
+{
+"version": "rustc 1.12.0 (3191fbae9 2016-09-23) repack",
+"size": 102120320,
+"digest": "f821e5ef00d758e45ecf4e10fea20b59035737d83b2e6a0f399932795ba27212883256c0c1bf23e55fcad05e377214d646290c6c7f78d5a6fe8149c0ac0ad0a9",
+"algorithm": "sha512",
+"filename": "rustc.tar.xz",
+"unpack": true
+},
+{
+"algorithm": "sha512",
+"visibility": "public",
+"filename": "dotgradle.tar.xz",
+"unpack": true,
+"digest": "9f082ccd71ad18991eb71fcad355c6990f50a72a09ab9b79696521485656083a72faf5a8d4714de9c4b901ee2319b6786a51964846bb7075061642a8505501c2",
+"size": 512
+}
+]
diff --git a/mobile/android/confvars.sh b/mobile/android/confvars.sh
new file mode 100644
index 0000000000..b4215ca89a
--- /dev/null
+++ b/mobile/android/confvars.sh
@@ -0,0 +1,60 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MOZ_APP_BASENAME=Fennec
+MOZ_APP_VENDOR=Mozilla
+
+MOZ_APP_VERSION=$FIREFOX_VERSION
+MOZ_APP_VERSION_DISPLAY=$FIREFOX_VERSION_DISPLAY
+MOZ_APP_UA_NAME=Firefox
+
+MOZ_BRANDING_DIRECTORY=mobile/android/branding/unofficial
+MOZ_OFFICIAL_BRANDING_DIRECTORY=mobile/android/branding/official
+# MOZ_APP_DISPLAYNAME is set by branding/configure.sh
+
+# We support Android SDK version 15 and up by default.
+# See the --enable-android-min-sdk and --enable-android-max-sdk arguments in configure.in.
+MOZ_ANDROID_MIN_SDK_VERSION=15
+
+# There are several entry points into the Firefox application. These are the names of some of the classes that are
+# listed in the Android manifest. They are specified in here to avoid hard-coding them in source code files.
+MOZ_ANDROID_APPLICATION_CLASS=org.mozilla.gecko.GeckoApplication
+MOZ_ANDROID_BROWSER_INTENT_CLASS=org.mozilla.gecko.BrowserApp
+MOZ_ANDROID_SEARCH_INTENT_CLASS=org.mozilla.search.SearchActivity
+
+MOZ_NO_SMART_CARDS=1
+
+MOZ_XULRUNNER=
+
+MOZ_CAPTURE=1
+MOZ_RAW=1
+
+MOZ_RUST_MP4PARSE=1
+
+# use custom widget for html:select
+MOZ_USE_NATIVE_POPUP_WINDOWS=1
+
+MOZ_APP_ID={aa3c5121-dab2-40e2-81ca-7ea25febc110}
+
+MOZ_APP_STATIC_INI=1
+
+# Enable second screen using native Android libraries.
+MOZ_NATIVE_DEVICES=1
+
+# Enable install tracking SDK if we have Google Play support; MOZ_NATIVE_DEVICES
+# is a proxy flag for that support.
+if test "$RELEASE_OR_BETA"; then
+if test "$MOZ_NATIVE_DEVICES"; then
+ MOZ_INSTALL_TRACKING=1
+fi
+fi
+
+# Mark as WebGL conformant
+MOZ_WEBGL_CONFORMANT=1
+
+# Use the low-memory GC tuning.
+export JS_GC_SMALL_CHUNK_SIZE=1
+
+# Enable checking that add-ons are signed by the trusted root
+MOZ_ADDON_SIGNING=1
diff --git a/mobile/android/debug_sign_tool.py b/mobile/android/debug_sign_tool.py
new file mode 100755
index 0000000000..2d3e2099f8
--- /dev/null
+++ b/mobile/android/debug_sign_tool.py
@@ -0,0 +1,187 @@
+#!/usr/bin/env python
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+Sign Android packages using an Android debug keystore, creating the
+keystore if it does not exist.
+
+This and |zip| can be combined to replace the Android |apkbuilder|
+tool, which was deprecated in SDK r22.
+
+Exits with code 0 if creating the keystore and every signing succeeded,
+or with code 1 if any creation or signing failed.
+"""
+
+from argparse import ArgumentParser
+import errno
+import logging
+import os
+import subprocess
+import sys
+
+
+log = logging.getLogger(os.path.basename(__file__))
+log.setLevel(logging.INFO)
+sh = logging.StreamHandler(stream=sys.stdout)
+sh.setFormatter(logging.Formatter('%(name)s: %(message)s'))
+log.addHandler(sh)
+
+
+class DebugKeystore:
+ """
+ A thin abstraction on top of an Android debug key store.
+ """
+ def __init__(self, keystore):
+ self._keystore = os.path.abspath(os.path.expanduser(keystore))
+ self._alias = 'androiddebugkey'
+ self.verbose = False
+ self.keytool = 'keytool'
+ self.jarsigner = 'jarsigner'
+
+ @property
+ def keystore(self):
+ return self._keystore
+
+ @property
+ def alias(self):
+ return self._alias
+
+ def _check(self, args):
+ try:
+ if self.verbose:
+ subprocess.check_call(args)
+ else:
+ subprocess.check_output(args)
+ except OSError as ex:
+ if ex.errno != errno.ENOENT:
+ raise
+ raise Exception("Could not find executable '%s'" % args[0])
+
+ def keystore_contains_alias(self):
+ args = [ self.keytool,
+ '-list',
+ '-keystore', self.keystore,
+ '-storepass', 'android',
+ '-alias', self.alias,
+ ]
+ if self.verbose:
+ args.append('-v')
+ contains = True
+ try:
+ self._check(args)
+ except subprocess.CalledProcessError as e:
+ contains = False
+ if self.verbose:
+ log.info('Keystore %s %s alias %s' %
+ (self.keystore,
+ 'contains' if contains else 'does not contain',
+ self.alias))
+ return contains
+
+ def create_alias_in_keystore(self):
+ try:
+ path = os.path.dirname(self.keystore)
+ os.makedirs(path)
+ except OSError as exception:
+ if exception.errno != errno.EEXIST:
+ raise
+
+ args = [ self.keytool,
+ '-genkeypair',
+ '-keystore', self.keystore,
+ '-storepass', 'android',
+ '-alias', self.alias,
+ '-keypass', 'android',
+ '-dname', 'CN=Android Debug,O=Android,C=US',
+ '-keyalg', 'RSA',
+ '-validity', '365',
+ ]
+ if self.verbose:
+ args.append('-v')
+ self._check(args)
+ if self.verbose:
+ log.info('Created alias %s in keystore %s' %
+ (self.alias, self.keystore))
+
+ def sign(self, apk):
+ if not self.keystore_contains_alias():
+ self.create_alias_in_keystore()
+
+ args = [ self.jarsigner,
+ '-digestalg', 'SHA1',
+ '-sigalg', 'MD5withRSA',
+ '-keystore', self.keystore,
+ '-storepass', 'android',
+ apk,
+ self.alias,
+ ]
+ if self.verbose:
+ args.append('-verbose')
+ self._check(args)
+ if self.verbose:
+ log.info('Signed %s with alias %s from keystore %s' %
+ (apk, self.alias, self.keystore))
+
+
+def parse_args(argv):
+ parser = ArgumentParser(description='Sign Android packages using an Android debug keystore.')
+ parser.add_argument('apks', nargs='+',
+ metavar='APK',
+ help='Android packages to be signed')
+ parser.add_argument('-v', '--verbose',
+ dest='verbose',
+ default=False,
+ action='store_true',
+ help='verbose output')
+ parser.add_argument('--keytool',
+ metavar='PATH',
+ default='keytool',
+ help='path to Java keytool')
+ parser.add_argument('--jarsigner',
+ metavar='PATH',
+ default='jarsigner',
+ help='path to Java jarsigner')
+ parser.add_argument('--keystore',
+ metavar='PATH',
+ default='~/.android/debug.keystore',
+ help='path to keystore (default: ~/.android/debug.keystore)')
+ parser.add_argument('-f', '--force-create-keystore',
+ dest='force',
+ default=False,
+ action='store_true',
+ help='force creating keystore')
+ return parser.parse_args(argv)
+
+
+def main():
+ args = parse_args(sys.argv[1:])
+
+ keystore = DebugKeystore(args.keystore)
+ keystore.verbose = args.verbose
+ keystore.keytool = args.keytool
+ keystore.jarsigner = args.jarsigner
+
+ if args.force:
+ try:
+ keystore.create_alias_in_keystore()
+ except subprocess.CalledProcessError as e:
+ log.error('Failed to force-create alias %s in keystore %s' %
+ (keystore.alias, keystore.keystore))
+ log.error(e)
+ return 1
+
+ for apk in args.apks:
+ try:
+ keystore.sign(apk)
+ except subprocess.CalledProcessError as e:
+ log.error('Failed to sign %s', apk)
+ log.error(e)
+ return 1
+
+ return 0
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/mobile/android/docs/Makefile b/mobile/android/docs/Makefile
new file mode 100644
index 0000000000..c5ddb90806
--- /dev/null
+++ b/mobile/android/docs/Makefile
@@ -0,0 +1,177 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " xml to make Docutils-native XML files"
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/FirefoxforAndroid.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/FirefoxforAndroid.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/FirefoxforAndroid"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/FirefoxforAndroid"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
+
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
diff --git a/mobile/android/docs/adjust.rst b/mobile/android/docs/adjust.rst
new file mode 100644
index 0000000000..16cb85b2bb
--- /dev/null
+++ b/mobile/android/docs/adjust.rst
@@ -0,0 +1,179 @@
+.. -*- Mode: rst; fill-column: 100; -*-
+
+======================================
+ Install tracking with the Adjust SDK
+======================================
+
+Fennec (Firefox for Android) tracks certain types of installs using a third party install tracking
+framework called Adjust. The intention is to determine the origin of Fennec installs by answering
+the question, "Did this user on this device install Fennec in response to a specific advertising
+campaign performed by Mozilla?"
+
+Mozilla is using a third party framework in order to answer this question for the Firefox for
+Android 38.0.5 release. We hope to remove the framework from Fennec in the future.
+
+The framework consists of a software development kit (SDK) built into Fennec and a
+data-collecting Internet service backend run by the German company `adjust GmbH`_. The Adjust SDK
+is open source and MIT licensed: see the `github repository`_. Fennec ships a copy of the SDK
+(currently not modified from upstream) in ``mobile/android/thirdparty/com/adjust/sdk``. The SDK is
+documented at https://docs.adjust.com.
+
+Data collection
+~~~~~~~~~~~~~~~
+
+When is data collected and sent to the Adjust backend?
+======================================================
+
+Data is never collected (or sent to the Adjust backend) unless
+
+* the Fennec binary is an official Mozilla binary [#official]_; and
+* the release channel is Release or Beta [#channel]_.
+
+If both of the above conditions are true, then data is collected and sent to the Adjust backend in
+the following two circumstances: first, when
+
+* Fennec is started on the device [#started]_.
+
+Second, when
+
+* the Fennec binary was installed from the Google Play Store; and
+* the Google Play Store sends the installed Fennec binary an `INSTALL_REFERRER Intent`_, and the
+ received Intent includes Google Play Store campaign tracking information. This happens when thea
+ Google Play Store install is in response to a campaign-specific Google Play Store link. For
+ details, see the developer documentation at
+ https://developers.google.com/analytics/devguides/collection/android/v4/campaigns.
+
+In these two limited circumstances, data is collected and sent to the Adjust backend.
+
+Where does data sent to the Adjust backend go?
+==============================================
+
+The Adjust SDK is hard-coded to send data to the endpoint https://app.adjust.com. The endpoint is
+defined by ``com.adjust.sdk.Constants.BASE_URL`` at
+https://hg.mozilla.org/mozilla-central/file/f76f02793f7a/mobile/android/thirdparty/com/adjust/sdk/Constants.java#l27.
+
+The Adjust backend then sends a limited subset of the collected data -- limited but sufficient to
+uniquely identify the submitting device -- to a set of advertising network providers that Mozilla
+elects to share the collected data with. Those advertising networks then confirm or deny that the
+identifying information corresponds to a specific advertising campaign performed by Mozilla.
+
+What data is collected and sent to the Adjust backend?
+======================================================
+
+The Adjust SDK collects and sends two messages to the Adjust backend. The messages have the
+following parameters::
+
+ V/Adjust ( 6508): Parameters:
+ V/Adjust ( 6508): screen_format normal
+ V/Adjust ( 6508): device_manufacturer samsung
+ V/Adjust ( 6508): session_count 1
+ V/Adjust ( 6508): device_type phone
+ V/Adjust ( 6508): screen_size normal
+ V/Adjust ( 6508): package_name org.mozilla.firefox
+ V/Adjust ( 6508): app_version 39.0a1
+ V/Adjust ( 6508): android_uuid <guid>
+ V/Adjust ( 6508): display_width 720
+ V/Adjust ( 6508): country GB
+ V/Adjust ( 6508): os_version 18
+ V/Adjust ( 6508): needs_attribution_data 0
+ V/Adjust ( 6508): environment sandbox
+ V/Adjust ( 6508): device_name Galaxy Nexus
+ V/Adjust ( 6508): os_name android
+ V/Adjust ( 6508): tracking_enabled 1
+ V/Adjust ( 6508): created_at 2015-03-24T17:53:38.452Z-0400
+ V/Adjust ( 6508): app_token <private>
+ V/Adjust ( 6508): screen_density high
+ V/Adjust ( 6508): language en
+ V/Adjust ( 6508): display_height 1184
+ V/Adjust ( 6508): gps_adid <guid>
+
+ V/Adjust ( 6508): Parameters:
+ V/Adjust ( 6508): needs_attribution_data 0
+ V/Adjust ( 6508): app_token <private>
+ V/Adjust ( 6508): environment production
+ V/Adjust ( 6508): android_uuid <guid>
+ V/Adjust ( 6508): tracking_enabled 1
+ V/Adjust ( 6508): gps_adid <guid>
+
+The available parameters (including ones not exposed to Mozilla) are documented at
+https://partners.adjust.com/placeholders/.
+
+Notes on what data is collected
+-------------------------------
+
+The *android_uuid* uniquely identifies the device.
+
+The *gps_adid* is a Google Advertising ID. It is capable of uniquely identifying a device to any
+advertiser, across all applications. If a Google Advertising ID is not available, Adjust may fall
+back to an Android ID, or, as a last resort, the device's WiFi MAC address.
+
+The *tracking_enabled* flag is only used to allow or disallow contextual advertising to be sent to a
+user. It can be, and is, ignored for general install tracking of the type Mozilla is using the
+Adjust SDK for. (This flag might be used by consumers using the Adjust SDK to provide in-App
+advertising.)
+
+It is not clear how much entropy their is in the set of per-device parameters that do not
+*explicitly* uniquely identify the device. That is, it is not known if the device parameters are
+likely to uniquely fingerprint the device, in the way that user agent capabilities are likely to
+uniquely fingerprint the user.
+
+Technical notes
+~~~~~~~~~~~~~~~
+
+Build flags controlling the Adjust SDK integration
+==================================================
+
+Add the following to your mozconfig to compile with the Adjust SDK::
+
+ export MOZ_INSTALL_TRACKING=1
+ export MOZ_NATIVE_DEVICES=1
+ export RELEASE_OR_BETA=1
+ ac_add_options --with-adjust-sdk-keyfile="$topsrcdir/mobile/android/base/adjust-sdk-sandbox.token"
+
+``MOZ_NATIVE_DEVICES`` && ``RELEASE_OR_BETA`` are required for an unknown
+reason. If you build without them, the ``StubAdjustHelper`` will be
+returned.
+
+No trace of the Adjust SDK should be present in Fennec if
+``MOZ_INSTALL_TRACKING`` is not defined.
+
+Access to the Adjust backend is controlled by a private App-specific
+token. Fennec's token is managed by Release Engineering and should not
+be exposed if at all possible; for example, it should *not* leak to build
+logs. The value of the token is read from the file specified using the
+``configure`` flag ``--with-adjust-sdk-keyfile=KEYFILE`` and stored in
+the build variable ``MOZ_ADJUST_SDK_KEY``. The mozconfig specified above
+defaults to submitting data to a special Adjust sandbox allowing a
+developer to test Adjust without submitting false data to our backend.
+
+We throw an assertion if ``MOZ_INSTALL_TRACKING`` is specified but
+``--with-adjust-sdk-keyfile`` is not to ensure our builders have a proper
+adjust token for release and beta builds. It's great to catch some
+errors at compile-time rather than in release. That being said, ideally
+we'd specify a default ``--with-adjust-sdk-keyfile`` for developer builds
+but I don't know how to do that.
+
+Technical notes on the Adjust SDK integration
+=============================================
+
+The *Adjust install tracking SDK* is a pure-Java library that is conditionally compiled into Fennec.
+It's not trivial to integrate such conditional feature libraries into Fennec without pre-processing.
+To minimize such pre-processing, we define a trivial ``AdjustHelperInterface`` and define two
+implementations: the real ``AdjustHelper``, which requires the Adjust SDK, and a no-op
+``StubAdjustHelper``, which has no additional requirements. We use the existing pre-processed
+``AppConstants.java.in`` to switch, at build-time, between the two implementations.
+
+Notes and links
+===============
+
+.. _adjust GmbH: http://www.adjust.com
+.. _github repository: https://github.com/adjust/android_sdk
+.. [#official] Data is not sent for builds not produced by Mozilla: this would include
+ redistributors such as the Palemoon project.
+.. [#channel] Data is not sent for Aurora, Nightly, or custom builds.
+.. [#started] *Started* means more than just when the user taps the Fennec icon or otherwise causes
+ the Fennec user interface to appear directly. It includes, for example, when a Fennec service
+ (like the Update Service, or Background Sync), starts and Fennec was not previously running on the
+ device. See http://developer.android.com/reference/android/app/Application.html#onCreate%28%29
+ for details.
+.. _INSTALL_REFERRER Intent: https://developer.android.com/reference/com/google/android/gms/tagmanager/InstallReferrerReceiver.html
diff --git a/mobile/android/docs/bouncer.rst b/mobile/android/docs/bouncer.rst
new file mode 100644
index 0000000000..6ba4a5f303
--- /dev/null
+++ b/mobile/android/docs/bouncer.rst
@@ -0,0 +1,38 @@
+.. -*- Mode: rst; fill-column: 100; -*-
+
+=========================================
+ The Firefox for Android install bouncer
+=========================================
+
+`Bug 1234629 <https://bugzilla.mozilla.org/show_bug.cgi?id=1234629>`_ and `Bug 1163082
+<https://bugzilla.mozilla.org/show_bug.cgi?id=1163082>`_ combine to allow building a very small
+Fennec-like "bouncer" APK that redirects (bounces) a potential Fennec user to the marketplace of
+their choice -- usually the Google Play Store -- to install the real Firefox for Android application
+APK.
+
+The real APK should install seamlessly over top of the bouncer APK. Care is taken to keep the
+bouncer and application APK <permission> manifest definitions identical, and to have the bouncer APK
+<activity> manifest definitions look similar to the application APK <activity> manifest definitions.
+
+In addition, the bouncer APK can carry a Fennec distribution, which it copies onto the device before
+redirecting to the marketplace. The application APK recognizes the installed distribution and
+customizes itself accordingly on first run.
+
+The motivation is to allow partners to pre-install the very small bouncer APK on shipping devices
+and to have a smooth path to upgrade to the full application APK, with a partner-specific
+distribution in place.
+
+Technical details
+=================
+
+To build the bouncer APK, define ``MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER``. To pack a distribution
+into the bouncer APK (and *not* into the application APK), add a line like::
+
+ ac_add_options --with-android-distribution-directory=/path/to/fennec-distribution-sample
+
+to your ``mozconfig`` file. See the `general distribution documentation on the wiki
+<https://wiki.mozilla.org/Mobile/Distribution_Files>`_ for more information.
+
+The ``distribution`` directory should end up in the ``assets/distribution`` directory of the bouncer
+APK. It will be copied into ``/data/data/$ANDROID_PACKAGE_NAME/distribution`` when the bouncer
+executes.
diff --git a/mobile/android/docs/conf.py b/mobile/android/docs/conf.py
new file mode 100644
index 0000000000..8eda58960d
--- /dev/null
+++ b/mobile/android/docs/conf.py
@@ -0,0 +1,258 @@
+# -*- coding: utf-8 -*-
+#
+# Firefox for Android documentation build configuration file, created by
+# sphinx-quickstart on Fri Dec 4 22:51:57 2015.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys
+import os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = []
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'Firefox for Android'
+copyright = u'2015, mobile team'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '1.0'
+# The full version, including alpha/beta/rc tags.
+release = '1.0'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+#keep_warnings = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#html_extra_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'FirefoxforAndroiddoc'
+
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ ('index', 'FirefoxforAndroid.tex', u'Firefox for Android Documentation',
+ u'mobile team', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'firefoxforandroid', u'Firefox for Android Documentation',
+ [u'mobile team'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'FirefoxforAndroid', u'Firefox for Android Documentation',
+ u'mobile team', 'FirefoxforAndroid', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#texinfo_no_detailmenu = False
diff --git a/mobile/android/docs/defaultdomains.rst b/mobile/android/docs/defaultdomains.rst
new file mode 100644
index 0000000000..470c2c8ec2
--- /dev/null
+++ b/mobile/android/docs/defaultdomains.rst
@@ -0,0 +1,90 @@
+.. -*- Mode: rst; fill-column: 100; -*-
+
+==========================
+ Shipping Default Domains
+==========================
+
+Firefox for Mobile (Android and iOS) ships sets of default content in order to improve the
+first-run experience. There are two primary places where default sets of domains are used: URLBar
+domain auto-completion, and Top Sites suggested thumbnails.
+
+The source of these domains is typically the Alexa top sites lists, global and by-country. Before
+shipping the sets of domains, the lists are sanitized.
+
+Domain Auto-completion
+======================
+
+As you type in the URLBar, Firefox will scan your history and auto-complete previously visited
+domains that match what you have entered. This can make navigating to web sites faster because it
+can avoid significant amounts of typing. During your first few uses, Firefox does not have any
+history and you are forced to type full URLs. Shipping a set of top domains provides a fallback.
+
+The top domains list can be localized, but Firefox will fallback to using en-US as the default for all
+locales that do not provide a specific set. The list can have several hundred domains, but due to
+size concerns, is usually capped to five hundred or less.
+
+Sanitizing Methods
+------------------
+
+After getting a source list, e.g. Alexa top global sites, we apply some simple guidelines to the
+list of domains:
+
+
+* Remove any sites in the Alexa adult site list.
+* Remove any locale-specific domain duplicates. We assume primary URLs (.com) will redirect to the
+ correct locale (.co.jp) at run-time.
+* Remove any explicit adult content* domains.
+* Remove any sites that use explicit or adult advertising*.
+* Remove any URL shorteners and redirecters.
+* Remove any content/CDN domains. Some sites use separate domains to store images and other static content.
+* Remove any sites primarily used for advertising or management of advertising.
+* Remove any sites that fail to load in mobile browsers.
+* Remove any time/date specific sites that may have appeared on the list due to seasonal spikes.
+
+Suggested Sites
+===============
+
+Suggested sites are default thumbnails, displayed on the Top Sites home panel. A suggested site
+consists of a title, thumbnail image, background color and URL. Multiple images are usually
+required to handle the variety of device DPIs.
+
+Suggested sites can be localized, but Firefox will fallback to using en-US as the default for all
+locales that do not provide a specific set. The list is usually small, with perhaps fewer than ten
+sites.
+
+Sanitizing Methods
+------------------
+
+After getting a source list, e.g. Alexa top global sites, we apply some simple guidelines to the
+list of domains:
+
+* Remove pure search engines. We handle search engines differently and don't consider them to be
+ suggested sites.
+* Remove any locale-specific domain duplicates. We assume primary URLs (.com) will redirect to the
+ correct locale (.co.jp) at run-time.
+* Remove any explicit adult content domains.
+* Remove any sites that use explicit or adult advertising.
+* Remove any URL shorteners and redirecters.
+* Remove any content/CDN domains. Some sites use separate domains to store images and other static
+ content.
+
+Guidelines for Adult Content
+============================
+
+Generally the Adult category includes sites whose dominant theme is either:
+
+* To appeal to the prurient interest in sex without any serious literary, artistic, political, or
+ scientific value
+* The depiction or description of nudity, including sexual or excretory activities or organs in a
+ lascivious way
+* The depiction or description of sexually explicit conduct in a lascivious way (e.g. for
+ entertainment purposes)
+
+For a more complete definition and guidelines of adult content, use the full DMOZ guidelines at
+http://www.dmoz.org/docs/en/guidelines/adult/general.html.
+
+Updating Lists
+==============
+
+After approximately every two releases, Product (with Legal) will review current lists and
+sanitizing methods, and update the lists accordingly.
diff --git a/mobile/android/docs/index.rst b/mobile/android/docs/index.rst
new file mode 100644
index 0000000000..8295f29546
--- /dev/null
+++ b/mobile/android/docs/index.rst
@@ -0,0 +1,26 @@
+.. Firefox for Android documentation master file, created by
+ sphinx-quickstart on Fri Dec 4 22:51:57 2015.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+Firefox for Android
+===================
+
+Contents:
+
+.. toctree::
+ :maxdepth: 2
+
+ localeswitching
+ uitelemetry
+ adjust
+ defaultdomains
+ bouncer
+ shutdown
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/mobile/android/docs/localeswitching.rst b/mobile/android/docs/localeswitching.rst
new file mode 100644
index 0000000000..8fcb419764
--- /dev/null
+++ b/mobile/android/docs/localeswitching.rst
@@ -0,0 +1,97 @@
+.. -*- Mode: rst; fill-column: 80; -*-
+
+====================================
+ Runtime locale switching in Fennec
+====================================
+
+`Bug 917480 <https://bugzilla.mozilla.org/show_bug.cgi?id=917480>`_ built on `Bug 936756 <https://bugzilla.mozilla.org/show_bug.cgi?id=936756>`_ to allow users to switch between supported locales at runtime, within Fennec, without altering the system locale.
+
+This document aims to describe the overall architecture of the solution, along with guidelines for Fennec developers.
+
+Overview
+========
+
+There are two places that locales are relevant to an Android application: the Java ``Locale`` object and the Android configuration itself.
+
+Locale switching involves manipulating these values (to affect future UI), persisting them for future activities, and selectively redisplaying existing UI elements to give the appearance of responsive switching.
+
+The user's choice of locale is stored in a per-app pref, ``"locale"``. If missing, the system default locale is used. If set, it should be a locale code like ``"es"`` or ``"en-US"``.
+
+``BrowserLocaleManager`` takes care of updating the active locale when asked to do so. It also manages persistence and retrieval of the locale preference.
+
+The question, then, is when to do so.
+
+Locale events
+=============
+
+One might imagine that we need only set the locale when our Application is instantiated, and when a new locale is set. Alas, that's not the case: whenever there's a configuration change (*e.g.*, screen rotation), when a new activity is started, and at other apparently random times, Android will supply our activities with a configuration that's been reset to the system locale.
+
+For this reason, each starting activity must ask ``BrowserLocaleManager`` to fix its locale.
+
+Ideally, we also need to perform some amount of work when our configuration changes, when our activity is resumed, and perhaps when a result is returned from another activity, if that activity can change the app locale (as is the case for any activity that calls out to ``GeckoPreferences`` -- see ``BrowserApp#onActivityResult``).
+
+``GeckoApp`` itself does some additional work, because it has particular performance constraints, and also is the typical root of the preferences activity.
+
+Here's an example of the work that a typical activity should do::
+
+ // This is cribbed from o.m.g.sync.setup.activities.LocaleAware.
+ public static void initializeLocale(Context context) {
+ final LocaleManager localeManager = BrowserLocaleManager.getInstance();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
+ localeManager.getAndApplyPersistedLocale(context);
+ } else {
+ final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+ StrictMode.allowThreadDiskWrites();
+ try {
+ localeManager.getAndApplyPersistedLocale(context);
+ } finally {
+ StrictMode.setThreadPolicy(savedPolicy);
+ }
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ final LocaleManager localeManager = BrowserLocaleManager.getInstance();
+ final Locale changed = localeManager.onSystemConfigurationChanged(this, getResources(), newConfig, mLastLocale);
+ if (changed != null) {
+ // Redisplay to match the locale.
+ onLocaleChanged(BrowserLocaleManager.getLanguageTag(changed));
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ // Note that we don't do this in onResume. We should,
+ // but it's an edge case that we feel free to ignore.
+ // We also don't have a hook in this example for when
+ // the user picks a new locale.
+ initializeLocale(this);
+
+ super.onCreate(icicle);
+ }
+
+``GeckoApplication`` itself handles correcting locales when the configuration changes; your activity shouldn't need to do this itself. See ``GeckoApplication``'s and ``GeckoApp``'s ``onConfigurationChanged`` methods.
+
+System locale changes
+=====================
+
+Fennec can be in one of two states.
+
+If the user has not explicitly chosen a Fennec-specific locale, we say
+we are "mirroring" the system locale.
+
+When we are not mirroring, system locale changes do not impact Fennec
+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 Fennec
+to reflect the new system locale, as if the user picked the new locale.
+
+When the system locale changes when we're mirroring, your activity will receive an ``onConfigurationChanged`` call. Simply pass this on to ``BrowserLocaleManager``, and then handle the response appropriately.
+
+Further reference
+=================
+
+``GeckoPreferences``, ``GeckoApp``, and ``BrowserApp`` are excellent resources for figuring out what you should do.
diff --git a/mobile/android/docs/make.bat b/mobile/android/docs/make.bat
new file mode 100644
index 0000000000..d916a4c8f9
--- /dev/null
+++ b/mobile/android/docs/make.bat
@@ -0,0 +1,242 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+set I18NSPHINXOPTS=%SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+ set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+ set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+ :help
+ echo.Please use `make ^<target^>` where ^<target^> is one of
+ echo. html to make standalone HTML files
+ echo. dirhtml to make HTML files named index.html in directories
+ echo. singlehtml to make a single large HTML file
+ echo. pickle to make pickle files
+ echo. json to make JSON files
+ echo. htmlhelp to make HTML files and a HTML help project
+ echo. qthelp to make HTML files and a qthelp project
+ echo. devhelp to make HTML files and a Devhelp project
+ echo. epub to make an epub
+ echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+ echo. text to make text files
+ echo. man to make manual pages
+ echo. texinfo to make Texinfo files
+ echo. gettext to make PO message catalogs
+ echo. changes to make an overview over all changed/added/deprecated items
+ echo. xml to make Docutils-native XML files
+ echo. pseudoxml to make pseudoxml-XML files for display purposes
+ echo. linkcheck to check all external links for integrity
+ echo. doctest to run all doctests embedded in the documentation if enabled
+ goto end
+)
+
+if "%1" == "clean" (
+ for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+ del /q /s %BUILDDIR%\*
+ goto end
+)
+
+
+%SPHINXBUILD% 2> nul
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+if "%1" == "html" (
+ %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+ goto end
+)
+
+if "%1" == "dirhtml" (
+ %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+ goto end
+)
+
+if "%1" == "singlehtml" (
+ %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+ goto end
+)
+
+if "%1" == "pickle" (
+ %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the pickle files.
+ goto end
+)
+
+if "%1" == "json" (
+ %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the JSON files.
+ goto end
+)
+
+if "%1" == "htmlhelp" (
+ %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+ goto end
+)
+
+if "%1" == "qthelp" (
+ %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+ echo.^> qcollectiongenerator %BUILDDIR%\qthelp\FirefoxforAndroid.qhcp
+ echo.To view the help file:
+ echo.^> assistant -collectionFile %BUILDDIR%\qthelp\FirefoxforAndroid.ghc
+ goto end
+)
+
+if "%1" == "devhelp" (
+ %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished.
+ goto end
+)
+
+if "%1" == "epub" (
+ %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub file is in %BUILDDIR%/epub.
+ goto end
+)
+
+if "%1" == "latex" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdf" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf
+ cd %BUILDDIR%/..
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdfja" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf-ja
+ cd %BUILDDIR%/..
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "text" (
+ %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The text files are in %BUILDDIR%/text.
+ goto end
+)
+
+if "%1" == "man" (
+ %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The manual pages are in %BUILDDIR%/man.
+ goto end
+)
+
+if "%1" == "texinfo" (
+ %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+ goto end
+)
+
+if "%1" == "gettext" (
+ %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+ goto end
+)
+
+if "%1" == "changes" (
+ %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.The overview file is in %BUILDDIR%/changes.
+ goto end
+)
+
+if "%1" == "linkcheck" (
+ %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+ goto end
+)
+
+if "%1" == "doctest" (
+ %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+ goto end
+)
+
+if "%1" == "xml" (
+ %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The XML files are in %BUILDDIR%/xml.
+ goto end
+)
+
+if "%1" == "pseudoxml" (
+ %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
+ goto end
+)
+
+:end
diff --git a/mobile/android/docs/shutdown.rst b/mobile/android/docs/shutdown.rst
new file mode 100644
index 0000000000..9ec0655f9d
--- /dev/null
+++ b/mobile/android/docs/shutdown.rst
@@ -0,0 +1,77 @@
+.. -*- Mode: rst; fill-column: 100; -*-
+
+===================================
+ Shutting down Firefox for Android
+===================================
+
+Background
+==========
+
+Normally, apps on Android don't need to provide any support for explicit quitting, instead they are
+just sent into the background where they eventually get killed by the OS's low-memory killer.
+
+Nevertheless, Firefox on Android allows explicit quitting to support the use case of users wanting
+to clear part or all of their private data after finishing a browsing session. When this option to
+"Clear private data on exit" is activated from the settings, a "Quit" button is provided in the menu.
+
+Because Firefox on Android uses a native UI (written in Java), which also holds some of the user's
+browsing data, this creates some additional complications when compared to quitting on desktop.
+
+Technical details
+=================
+
+When the "Quit" button is used, the UI sends a ``Browser:Quit`` notification to Gecko's ``BrowserApp``,
+which initiates the normal Gecko shutdown procedure. At the same time however, the native UI needs to
+shutdown as well, so as to
+
+1) provide an immediate visual feedback to the user that Firefox is indeed quitting
+
+2) avoid a state where the UI is still running "normally" while the rendering engine is already
+ shutting down, which could lead to loosing incoming external tabs if they were to arrive within
+ that period.
+
+Therefore, shutdown of the native UI was originally started simultaneously with notifying Gecko.
+Because the clearing of private data during shutdown is handled by Gecko's ``Sanitizer``, while some
+private data, e.g. the browsing history, is held in a database by the native UI, this means that
+Gecko needs to message the native UI during shutdown if the user wants the browsing history to be
+cleared on quitting.
+Shutting down the UI simultaneously with Gecko therefore introduced a race condition where the data
+clearing could fail because the native UI thread responsible for receiving Gecko's sanitization
+messages had already exited by the time Gecko's ``Sanitizer`` was attempting to e.g. clear the
+user's browsing history (for further reading, compare `bug 1266594
+<https://bugzilla.mozilla.org/show_bug.cgi?id=1266594>`_).
+
+To fix this issue, the native UI (in ``GeckoApp``) now waits for the ``Sanitizer`` to run and
+message all necessary sanitization handlers and only starts its shutdown after receiving a
+``Sanitize:Finished`` message with a ``shutdown: true`` parameter set. While this introduces a
+certain delay in closing the UI, it is still faster than having to wait for Gecko to exit completely
+before starting to close the UI.
+
+Currently, quitting Firefox therefore proceeds roughly as follows:
+
+1) The user presses the "Quit" button in the main menu, which sends a ``Browser:Quit`` notification
+ to ``BrowserApp``. This notification also contains additional parameters indicating which types
+ of private user data - if any - to clear during shutdown.
+
+2) ``BrowserApp.quit`` runs, which initiates Gecko shutdown by sending out a
+ ``quit-application-requested`` notification.
+
+3) If nobody cancelled shutdown in response to the ``quit-application-requested`` notification,
+ quitting proceeds and the ``SessionStore`` enters shutdown mode (``STATE_QUITTING``), which
+ basically means that no new (asynchronous) writes are started to prevent any interference with
+ the final flushing of data.
+
+4) ``BrowserApp`` calls the ``Sanitizer`` to clear up any private user data that might need cleaning.
+ After the ``Sanitizer`` has invoked all required sanitization handlers (including any on the
+ native Java UI side, e.g. for the browsing history) and finished running, it sends a
+ ``Sanitize:Finished`` message back to the native UI.
+
+5) On receiving the ``Sanitize:Finished`` message, ``GeckoApp`` starts the shutdown of the native UI
+ as well by calling ``doShutdown()``.
+
+6) After sending the ``Sanitize:Finished`` message, Gecko's ``Sanitizer`` runs the callback provided
+ by ``BrowserApp.quit``, which is ``appStartup.quit(Ci.nsIAppStartup.eForceQuit)``, thereby
+ starting the actual and final shutting down of Gecko.
+
+7) On receiving the final ``quit-application`` notification, the ``SessionStore`` synchronously
+ writes its current state to disk.
diff --git a/mobile/android/docs/uitelemetry.rst b/mobile/android/docs/uitelemetry.rst
new file mode 100644
index 0000000000..d592acbb65
--- /dev/null
+++ b/mobile/android/docs/uitelemetry.rst
@@ -0,0 +1,271 @@
+.. -*- Mode: rst; fill-column: 80; -*-
+
+==============
+ UI Telemetry
+==============
+
+Fennec records UI events using a telemetry framework called UITelemetry.
+
+Some links:
+
+- `Project page <https://wiki.mozilla.org/Mobile/Projects/Telemetry_probes_for_Fennec_UI_elements>`_
+- `Wiki page <https://wiki.mozilla.org/Mobile/Fennec/Android/UITelemetry>`_
+- `User research notes <https://wiki.mozilla.org/Mobile/User_Experience/Research>`_
+
+Sessions
+========
+
+**Sessions** are essentially scopes. They are meant to provide context to
+events; this allows events to be simpler and more reusable. Sessions are
+usually bound to some component of the UI, some user action with a duration, or
+some transient state.
+
+For example, a session might be begun when a user begins interacting with a
+menu, and stopped when the interaction ends. Or a session might encapsulate
+period of no network connectivity, the first five seconds after the browser
+launched, the time spent with an active download, or a guest mode session.
+
+Sessions implicitly record the duration of the interaction.
+
+A simple use-case for sessions is the bookmarks panel in about:home. We start a
+session when the user swipes into the panel, and stop it when they swipe away.
+This bookmarks session does two things: firstly, it gives scope to any generic
+event that may occur within the panel (*e.g.*, loading a URL). Secondly, it
+allows us to figure out how much time users are spending in the bookmarks
+panel.
+
+To start a session, call ``Telemetry.startUISession(String sessionName)``.
+
+``sessionName``
+ The name of the session. Session names should be brief, lowercase, and should describe which UI
+ component the user is interacting with. In certain cases where the UI component is dynamic, they could include an ID, essential to identifying that component. An example of this is dynamic home panels: we use session names of the format ``homepanel:<panel_id>`` to identify home panel sessions.
+
+To stop a session, call ``Telemetry.stopUISession(String sessionName, String reason)``.
+
+``sessionName``
+ The name of the open session
+
+``reason`` (Optional)
+ A descriptive cause for ending the session. It should be brief, lowercase, and generic so it can be reused in different places. Examples reasons are:
+
+ ``switched``
+ The user transitioned to a UI element of equal level.
+
+ ``exit``
+ The user left for an entirely different element.
+
+Events
+======
+
+Events capture key occurrences. They should be brief and simple, and should not contain sensitive or excess information. Context for events should come from the session (scope). An event can be created with four fields (via ``Telemetry.sendUIEvent``): ``action``, ``method``, ``extras``, and ``timestamp``.
+
+``action``
+ The name of the event. Should be brief and lowercase. If needed, you can make use of namespacing with a '``.``' separator. Example event names: ``panel.switch``, ``panel.enable``, ``panel.disable``, ``panel.install``.
+
+``method`` (Optional)
+ Used for user actions that can be performed in many ways. This field specifies the method by which the action was performed. For example, users can add an item to their reading list either by long-tapping the reader icon in the address bar, or from within reader mode. We would use the same event name for both user actions but specify two methods: ``addressbar`` and ``readermode``.
+
+``extras`` (Optional)
+ For extra information that may be useful in understanding the event. Make an effort to keep this brief.
+
+``timestamp`` (Optional)
+ The time at which the event occurred. If not specified, this field defaults to the current value of the realtime clock.
+
+Versioning
+==========
+
+As a we improve on our Telemetry methods, it is foreseeable that our probes will change over time. Different versions of a probe could carry different data or have different interpretations on the server-side. To make it easier for the server to handle these changes, you should add version numbers to your event and session names. An example of a versioned session is ``homepanel.1``; this is version 1 of the ``homepanel`` session. This approach should also be applied to event names, an example being: ``panel.enable.1`` and ``panel.enable.2``.
+
+
+Clock
+=====
+
+Times are relative to either elapsed realtime (an arbitrary monotonically increasing clock that continues to tick when the device is asleep), or elapsed uptime (which doesn't tick when the device is in deep sleep). We default to elapsed realtime.
+
+See the documentation in `the source <http://dxr.mozilla.org/mozilla-central/source/mobile/android/base/Telemetry.java>`_ for more details.
+
+Dictionary
+==========
+
+Events
+------
+``action.1``
+ Generic action, usually for tracking menu and toolbar actions.
+
+``cancel.1``
+ Cancel a state, action, etc.
+
+``cast.1``
+ Start casting a video.
+
+``edit.1``
+ Sent when the user edits a top site.
+
+``launch.1``
+ Launching (opening) an external application.
+ Note: Only used in JavaScript for now.
+
+``loadurl.1``
+ Loading a URL.
+
+``locale.browser.reset.1``
+ When the user chooses "System default" in the browser locale picker.
+
+``locale.browser.selected.1``
+ When the user chooses a locale in the browser locale picker. The selected
+ locale is provided as the extra.
+
+``locale.browser.unselected.1``
+ When the user chose a different locale in the browser locale picker, this
+ event is fired with the previous locale as the extra. If the previous locale
+ could not be determined, "unknown" is provided.
+
+``neterror.1``
+ When the user performs actions on the in-content network error page. This should probably be a ``Session``, but it's difficult to start and stop the session reliably.
+
+``panel.hide.1``
+ Hide a built-in home panel.
+
+``panel.move.1``
+ Move a home panel up or down.
+
+``panel.remove.1``
+ Remove a custom home panel.
+
+``panel.setdefault.1``
+ Set default home panel.
+
+``panel.show.1``
+ Show a hidden built-in home panel.
+
+``pin.1``, ``unpin.1``
+ Sent when the user pinned or unpinned a top site.
+
+``policynotification.success.1:true``
+ Sent when a user has accepted the data notification policy. Can be ``false``
+ instead of ``true`` if an error occurs.
+
+``sanitize.1``
+ Sent when the user chooses to clear private data.
+
+``save.1``, ``unsave.1``
+ Saving or unsaving a resource (reader, bookmark, etc.) for viewing later.
+
+``search.1``
+ Sent when the user performs a search. Currently used in the search activity.
+
+``search.remove.1``
+ Sent when the user removes a search engine.
+
+``search.restore.1``
+ Sent when the user restores the search engine configuration back to the built-in configuration.
+
+``search.setdefault.1``
+ Sent when the user sets a search engine to be the default.
+
+``share.1``
+ Sharing content.
+
+``show.1``
+ Sent when a contextual UI element is shown to the user.
+
+``undo.1``
+ Sent when performing an undo-style action, like undoing a closed tab.
+
+Methods
+-------
+``actionbar``
+ Action triggered from an ActionBar UI.
+
+``back``
+ Action triggered from the back button.
+
+``banner``
+ Action triggered from a banner (such as HomeBanner).
+
+``button``
+ Action triggered from a button.
+ Note: Only used in JavaScript for now.
+
+``content``
+ Action triggered from a content page.
+
+``contextmenu``
+ Action triggered from a contextmenu. Could be from chrome or content.
+
+``dialog``
+ Action triggered from a dialog.
+
+``doorhanger``
+ Action triggered from a doorhanger popup prompt.
+
+``griditem``
+ Action triggered from a griditem, such as those used in Top Sites panel.
+
+``homescreen``
+ Action triggered from a homescreen shortcut icon.
+
+``intent``
+ Action triggered from a system Intent, usually sent from the OS.
+
+``list``
+ Action triggered from an unmanaged list of items, usually provided by the OS.
+
+``listitem``
+ Action triggered from a listitem.
+
+``menu``
+ Action triggered from the main menu.
+
+``notification``
+ Action triggered from a system notification.
+
+``pageaction``
+ Action triggered from a pageaction, displayed in the URL bar.
+
+``service``
+ Action triggered from an automatic system making a decision.
+
+``settings``
+ Action triggered from a content page.
+
+``shareoverlay``
+ Action triggered from a content page.
+
+``suggestion``
+ Action triggered from a suggested result, like those from search engines or default tiles.
+
+``system``
+ Action triggered from an OS level action, like application foreground / background.
+
+``toast``
+ Action triggered from an unobtrusive, temporary notification.
+
+``widget``
+ Action triggered from a widget placed on the homescreen.
+
+Sessions
+--------
+``awesomescreen.1``
+ Awesomescreen (including frecency search) is active.
+
+``firstrun.1``
+ Started the very first time we believe the application has been launched.
+
+``frecency.1``
+ Awesomescreen frecency search is active.
+
+``homepanel.1``
+ Started when a user enters a given home panel.
+ Session name is dynamic, encoded as "homepanel.1:<panel_id>"
+ Built-in home panels have fixed IDs
+
+``reader.1``
+ Reader viewer becomes active in the foreground.
+
+``searchactivity.1``
+ Started when the user launches the search activity (onStart) and stopped
+ when they leave the search activity.
+
+``settings.1``
+ Settings activity is active.
diff --git a/mobile/android/extensions/flyweb/bootstrap.js b/mobile/android/extensions/flyweb/bootstrap.js
new file mode 100644
index 0000000000..017cb4763d
--- /dev/null
+++ b/mobile/android/extensions/flyweb/bootstrap.js
@@ -0,0 +1,154 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {classes: Cc, interfaces: Ci, manager: Cm, results: Cr, utils: Cu, Constructor: CC} = Components;
+
+Cm.QueryInterface(Ci.nsIComponentRegistrar);
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Console",
+ "resource://gre/modules/Console.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "gFlyWebBundle", function() {
+ return Services.strings.createBundle("chrome://flyweb/locale/flyweb.properties");
+});
+
+const FLYWEB_ENABLED_PREF = "dom.flyweb.enabled";
+
+let factory, menuID;
+
+function AboutFlyWeb() {}
+
+AboutFlyWeb.prototype = Object.freeze({
+ classDescription: "About page for displaying nearby FlyWeb services",
+ contractID: "@mozilla.org/network/protocol/about;1?what=flyweb",
+ classID: Components.ID("{baa04ff0-08b5-11e6-a837-0800200c9a66}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]),
+
+ getURIFlags: function(aURI) {
+ return Ci.nsIAboutModule.ALLOW_SCRIPT;
+ },
+
+ newChannel: function(aURI, aLoadInfo) {
+ let uri = Services.io.newURI("chrome://flyweb/content/aboutFlyWeb.xhtml", null, null);
+ let channel = Services.io.newChannelFromURIWithLoadInfo(uri, aLoadInfo);
+ channel.originalURI = aURI;
+ return channel;
+ }
+});
+
+function Factory(component) {
+ this.createInstance = function(outer, iid) {
+ if (outer) {
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+ return new component();
+ };
+ this.register = function() {
+ Cm.registerFactory(component.prototype.classID, component.prototype.classDescription, component.prototype.contractID, this);
+ };
+ this.unregister = function() {
+ Cm.unregisterFactory(component.prototype.classID, this);
+ }
+ Object.freeze(this);
+ this.register();
+}
+
+let windowListener = {
+ onOpenWindow: function(aWindow) {
+ // Wait for the window to finish loading
+ let domWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
+ domWindow.addEventListener("UIReady", function onLoad() {
+ domWindow.removeEventListener("UIReady", onLoad, false);
+ loadIntoWindow(domWindow);
+ }, false);
+ },
+
+ onCloseWindow: function(aWindow) {},
+ onWindowTitleChange: function(aWindow, aTitle) {}
+};
+
+let FlyWebUI = {
+ init() {
+ factory = new Factory(AboutFlyWeb);
+
+ // Load into any existing windows
+ let windows = Services.wm.getEnumerator("navigator:browser");
+ while (windows.hasMoreElements()) {
+ let domWindow = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
+ loadIntoWindow(domWindow);
+ }
+
+ // Load into any new windows
+ Services.wm.addListener(windowListener);
+ },
+
+ uninit() {
+ factory.unregister();
+
+ // Stop listening for new windows
+ Services.wm.removeListener(windowListener);
+
+ // Unload from any existing windows
+ let windows = Services.wm.getEnumerator("navigator:browser");
+ while (windows.hasMoreElements()) {
+ let domWindow = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
+ unloadFromWindow(domWindow);
+ }
+ }
+};
+
+function loadIntoWindow(aWindow) {
+ menuID = aWindow.NativeWindow.menu.add({
+ name: gFlyWebBundle.GetStringFromName("flyweb-menu.name"),
+ callback() {
+ aWindow.BrowserApp.addTab("about:flyweb");
+ }
+ });
+}
+
+function unloadFromWindow(aWindow) {
+ if (!aWindow) {
+ return;
+ }
+
+ aWindow.NativeWindow.menu.remove(menuID);
+}
+
+function prefObserver(aSubject, aTopic, aData) {
+ let enabled = Services.prefs.getBoolPref(FLYWEB_ENABLED_PREF);
+ if (enabled) {
+ FlyWebUI.init();
+ } else {
+ FlyWebUI.uninit();
+ }
+}
+
+function install(aData, aReason) {}
+
+function uninstall(aData, aReason) {}
+
+function startup(aData, aReason) {
+ // Observe pref changes and enable/disable as necessary.
+ Services.prefs.addObserver(FLYWEB_ENABLED_PREF, prefObserver, false);
+
+ // Only initialize if pref is enabled.
+ let enabled = Services.prefs.getBoolPref(FLYWEB_ENABLED_PREF);
+ if (enabled) {
+ FlyWebUI.init();
+ }
+}
+
+function shutdown(aData, aReason) {
+ Services.prefs.removeObserver(FLYWEB_ENABLED_PREF, prefObserver);
+
+ let enabled = Services.prefs.getBoolPref(FLYWEB_ENABLED_PREF);
+ if (enabled) {
+ FlyWebUI.uninit();
+ }
+}
diff --git a/mobile/android/extensions/flyweb/content/aboutFlyWeb.css b/mobile/android/extensions/flyweb/content/aboutFlyWeb.css
new file mode 100644
index 0000000000..0c751b53f7
--- /dev/null
+++ b/mobile/android/extensions/flyweb/content/aboutFlyWeb.css
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include "defines.css"
+
+.list-item > a {
+ color: inherit;
+ text-decoration: none;
+}
+
+.details {
+ -moz-margin-start: calc(var(--icon-size) + var(--icon-margin) * 2 - 1em);
+ padding: 1em;
+}
+
+#flyweb-item-template {
+ display: none;
+}
+
+#flyweb-list-empty {
+ display: none;
+}
+
+#flyweb-list:empty + #flyweb-list-empty {
+ display: block;
+ text-align: center;
+ padding-top: 3.9em;
+}
diff --git a/mobile/android/extensions/flyweb/content/aboutFlyWeb.js b/mobile/android/extensions/flyweb/content/aboutFlyWeb.js
new file mode 100644
index 0000000000..48b7ea4b7e
--- /dev/null
+++ b/mobile/android/extensions/flyweb/content/aboutFlyWeb.js
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Console.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "gFlyWebBundle", function() {
+ return Services.strings.createBundle("chrome://flyweb/locale/flyweb.properties");
+});
+
+let discoveryManager = new FlyWebDiscoveryManager();
+
+let discoveryCallback = {
+ onDiscoveredServicesChanged(services) {
+ if (!this.id) {
+ return;
+ }
+
+ let list = document.getElementById("flyweb-list");
+ while (list.firstChild) {
+ list.firstChild.remove();
+ }
+
+ let template = document.getElementById("flyweb-item-template");
+
+ for (let service of services) {
+ let item = template.cloneNode(true);
+ item.removeAttribute("id");
+
+ item.setAttribute("data-service-id", service.serviceId);
+ item.querySelector(".title").setAttribute("value", service.displayName);
+ item.querySelector(".icon").src = "chrome://flyweb/content/icon-64.png";
+
+ list.appendChild(item);
+ }
+ },
+ start() {
+ this.id = discoveryManager.startDiscovery(this);
+ },
+ stop() {
+ discoveryManager.stopDiscovery(this.id);
+ this.id = undefined;
+ }
+};
+
+window.addEventListener("DOMContentLoaded", () => {
+ let list = document.getElementById("flyweb-list");
+ list.addEventListener("click", (evt) => {
+ let serviceId = evt.target.closest("[data-service-id]").getAttribute("data-service-id");
+
+ discoveryManager.pairWithService(serviceId, {
+ pairingSucceeded(service) {
+ window.open(service.uiUrl, "FlyWebWindow_" + serviceId);
+ },
+
+ pairingFailed(error) {
+ console.error("FlyWeb failed to connect to service " + serviceId, error);
+ }
+ });
+ });
+
+ discoveryCallback.start();
+});
+
+window.addEventListener("unload", () => {
+ discoveryCallback.stop();
+});
diff --git a/mobile/android/extensions/flyweb/content/aboutFlyWeb.xhtml b/mobile/android/extensions/flyweb/content/aboutFlyWeb.xhtml
new file mode 100644
index 0000000000..85e92ddf8f
--- /dev/null
+++ b/mobile/android/extensions/flyweb/content/aboutFlyWeb.xhtml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
+%globalDTD;
+<!ENTITY % flywebDTD SYSTEM "chrome://flyweb/locale/aboutFlyWeb.dtd" >
+%flywebDTD;
+]>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<head>
+ <title>&aboutFlyWeb.title;</title>
+ <meta name="viewport" content="width=device-width; user-scalable=0" />
+ <link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
+ <link rel="stylesheet" href="chrome://browser/skin/aboutBase.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://flyweb/content/aboutFlyWeb.css" type="text/css"/>
+</head>
+
+<body dir="&locale.dir;">
+ <!--template id="flyweb-item-template"-->
+ <li id="flyweb-item-template" class="list-item" role="button">
+ <img class="icon" src=""/>
+ <div class="details">
+ <div class="row">
+ <!-- This is a hack so that we can crop this label in its center -->
+ <xul:label class="title" crop="center" value=""/>
+ </div>
+ </div>
+ </li>
+ <!--/template-->
+
+ <div class="header">
+ <div>&aboutFlyWeb.header;</div>
+ </div>
+ <ul id="flyweb-list" class="list"></ul>
+ <span id="flyweb-list-empty">&aboutFlyWeb.empty;</span>
+ <script type="application/javascript;version=1.8" src="chrome://flyweb/content/aboutFlyWeb.js"/>
+</body>
+</html>
diff --git a/mobile/android/extensions/flyweb/content/icon-64.png b/mobile/android/extensions/flyweb/content/icon-64.png
new file mode 100644
index 0000000000..be8ece467b
--- /dev/null
+++ b/mobile/android/extensions/flyweb/content/icon-64.png
Binary files differ
diff --git a/mobile/android/extensions/flyweb/install.rdf.in b/mobile/android/extensions/flyweb/install.rdf.in
new file mode 100644
index 0000000000..76430412cc
--- /dev/null
+++ b/mobile/android/extensions/flyweb/install.rdf.in
@@ -0,0 +1,31 @@
+<?xml version="1.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/. -->
+
+#filter substitution
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>flyweb@mozilla.org</em:id>
+ <em:version>1.0.0</em:version>
+ <em:type>2</em:type>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Target Application this theme can install into,
+ with minimum and maximum supported versions. -->
+ <em:targetApplication>
+ <Description>
+ <em:id>{aa3c5121-dab2-40e2-81ca-7ea25febc110}</em:id>
+ <em:minVersion>@FIREFOX_VERSION@</em:minVersion>
+ <em:maxVersion>@FIREFOX_VERSION@</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>FlyWeb</em:name>
+ <em:description>Discover nearby services in the browser</em:description>
+ </Description>
+</RDF>
diff --git a/mobile/android/extensions/flyweb/jar.mn b/mobile/android/extensions/flyweb/jar.mn
new file mode 100644
index 0000000000..c0aba080ba
--- /dev/null
+++ b/mobile/android/extensions/flyweb/jar.mn
@@ -0,0 +1,10 @@
+#filter substitution
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[features/flyweb@mozilla.org] chrome.jar:
+% content flyweb %content/ contentaccessible=yes
+ content/ (content/*)
+% locale flyweb en-US %locale/en-US/
+ locale/ (locale/*) \ No newline at end of file
diff --git a/mobile/android/extensions/flyweb/locale/en-US/aboutFlyWeb.dtd b/mobile/android/extensions/flyweb/locale/en-US/aboutFlyWeb.dtd
new file mode 100644
index 0000000000..9366ea19ca
--- /dev/null
+++ b/mobile/android/extensions/flyweb/locale/en-US/aboutFlyWeb.dtd
@@ -0,0 +1,7 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY aboutFlyWeb.title "FlyWeb">
+<!ENTITY aboutFlyWeb.header "Nearby FlyWeb Services">
+<!ENTITY aboutFlyWeb.empty "No FlyWeb Services Found">
diff --git a/mobile/android/extensions/flyweb/locale/en-US/flyweb.properties b/mobile/android/extensions/flyweb/locale/en-US/flyweb.properties
new file mode 100644
index 0000000000..556e646d37
--- /dev/null
+++ b/mobile/android/extensions/flyweb/locale/en-US/flyweb.properties
@@ -0,0 +1,5 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+flyweb-menu.name = FlyWeb
diff --git a/mobile/android/extensions/flyweb/moz.build b/mobile/android/extensions/flyweb/moz.build
new file mode 100644
index 0000000000..f453297e56
--- /dev/null
+++ b/mobile/android/extensions/flyweb/moz.build
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+FINAL_TARGET_FILES.features['flyweb@mozilla.org'] += [
+ 'bootstrap.js'
+]
+
+FINAL_TARGET_PP_FILES.features['flyweb@mozilla.org'] += [
+ 'install.rdf.in'
+]
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/mobile/android/extensions/moz.build b/mobile/android/extensions/moz.build
new file mode 100644
index 0000000000..24b6ca936b
--- /dev/null
+++ b/mobile/android/extensions/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Only include the following system add-ons if building Aurora or Nightly
+if 'a' in CONFIG['GRE_MILESTONE']:
+ DIRS += [
+ 'flyweb',
+ ]
diff --git a/mobile/android/fonts/CharisSILCompact-B.ttf b/mobile/android/fonts/CharisSILCompact-B.ttf
new file mode 100644
index 0000000000..eade6d0240
--- /dev/null
+++ b/mobile/android/fonts/CharisSILCompact-B.ttf
Binary files differ
diff --git a/mobile/android/fonts/CharisSILCompact-BI.ttf b/mobile/android/fonts/CharisSILCompact-BI.ttf
new file mode 100644
index 0000000000..b7ef2b2acf
--- /dev/null
+++ b/mobile/android/fonts/CharisSILCompact-BI.ttf
Binary files differ
diff --git a/mobile/android/fonts/CharisSILCompact-I.ttf b/mobile/android/fonts/CharisSILCompact-I.ttf
new file mode 100644
index 0000000000..6148b6542f
--- /dev/null
+++ b/mobile/android/fonts/CharisSILCompact-I.ttf
Binary files differ
diff --git a/mobile/android/fonts/CharisSILCompact-R.ttf b/mobile/android/fonts/CharisSILCompact-R.ttf
new file mode 100644
index 0000000000..167a6901c7
--- /dev/null
+++ b/mobile/android/fonts/CharisSILCompact-R.ttf
Binary files differ
diff --git a/mobile/android/fonts/ClearSans-Bold.ttf b/mobile/android/fonts/ClearSans-Bold.ttf
new file mode 100644
index 0000000000..b06f84dccd
--- /dev/null
+++ b/mobile/android/fonts/ClearSans-Bold.ttf
Binary files differ
diff --git a/mobile/android/fonts/ClearSans-BoldItalic.ttf b/mobile/android/fonts/ClearSans-BoldItalic.ttf
new file mode 100644
index 0000000000..debf556071
--- /dev/null
+++ b/mobile/android/fonts/ClearSans-BoldItalic.ttf
Binary files differ
diff --git a/mobile/android/fonts/ClearSans-Italic.ttf b/mobile/android/fonts/ClearSans-Italic.ttf
new file mode 100644
index 0000000000..c47df80eeb
--- /dev/null
+++ b/mobile/android/fonts/ClearSans-Italic.ttf
Binary files differ
diff --git a/mobile/android/fonts/ClearSans-Light.ttf b/mobile/android/fonts/ClearSans-Light.ttf
new file mode 100644
index 0000000000..c87978a1e9
--- /dev/null
+++ b/mobile/android/fonts/ClearSans-Light.ttf
Binary files differ
diff --git a/mobile/android/fonts/ClearSans-Medium.ttf b/mobile/android/fonts/ClearSans-Medium.ttf
new file mode 100644
index 0000000000..40dfd82681
--- /dev/null
+++ b/mobile/android/fonts/ClearSans-Medium.ttf
Binary files differ
diff --git a/mobile/android/fonts/ClearSans-MediumItalic.ttf b/mobile/android/fonts/ClearSans-MediumItalic.ttf
new file mode 100644
index 0000000000..952bcb5751
--- /dev/null
+++ b/mobile/android/fonts/ClearSans-MediumItalic.ttf
Binary files differ
diff --git a/mobile/android/fonts/ClearSans-Regular.ttf b/mobile/android/fonts/ClearSans-Regular.ttf
new file mode 100644
index 0000000000..fe686f8d2a
--- /dev/null
+++ b/mobile/android/fonts/ClearSans-Regular.ttf
Binary files differ
diff --git a/mobile/android/fonts/ClearSans-Thin.ttf b/mobile/android/fonts/ClearSans-Thin.ttf
new file mode 100644
index 0000000000..6f25224f5d
--- /dev/null
+++ b/mobile/android/fonts/ClearSans-Thin.ttf
Binary files differ
diff --git a/mobile/android/fonts/moz.build b/mobile/android/fonts/moz.build
new file mode 100644
index 0000000000..55c114f79e
--- /dev/null
+++ b/mobile/android/fonts/moz.build
@@ -0,0 +1,21 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+if not CONFIG['MOZ_ANDROID_EXCLUDE_FONTS']:
+ RESOURCE_FILES.fonts += [
+ 'CharisSILCompact-B.ttf',
+ 'CharisSILCompact-BI.ttf',
+ 'CharisSILCompact-I.ttf',
+ 'CharisSILCompact-R.ttf',
+ 'ClearSans-Bold.ttf',
+ 'ClearSans-BoldItalic.ttf',
+ 'ClearSans-Italic.ttf',
+ 'ClearSans-Light.ttf',
+ 'ClearSans-Medium.ttf',
+ 'ClearSans-MediumItalic.ttf',
+ 'ClearSans-Regular.ttf',
+ 'ClearSans-Thin.ttf',
+ ]
diff --git a/mobile/android/geckoview/build.gradle b/mobile/android/geckoview/build.gradle
new file mode 100644
index 0000000000..cc46530cbb
--- /dev/null
+++ b/mobile/android/geckoview/build.gradle
@@ -0,0 +1,176 @@
+buildDir "${topobjdir}/gradle/build/mobile/android/geckoview"
+
+apply plugin: 'android-sdk-manager' // Must come before 'com.android.*'.
+apply plugin: 'com.android.library'
+
+def VERSION_NAME = '0.0.1'
+
+android {
+ compileSdkVersion 23
+ buildToolsVersion mozconfig.substs.ANDROID_BUILD_TOOLS_VERSION
+
+ defaultConfig {
+ // TODO: version GeckoView explicitly. We'd like to avoid
+ // mozconfig.substs.ANDROID_VERSION_CODE, which won't be intuitive to
+ // consumer (and advances very quickly on pre-release channels).
+ versionCode 1
+ versionName VERSION_NAME
+ targetSdkVersion 23
+ minSdkVersion 15
+ consumerProguardFiles 'proguard-rules.txt'
+ }
+
+ buildTypes {
+ withGeckoBinaries {
+ initWith release
+ }
+ withoutGeckoBinaries { // For clarity and consistency throughout the tree.
+ initWith release
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_7
+ targetCompatibility JavaVersion.VERSION_1_7
+ }
+
+ dexOptions {
+ javaMaxHeapSize "2g"
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+
+ sourceSets {
+ main {
+ java {
+ srcDir "${topsrcdir}/mobile/android/geckoview/src/thirdparty/java"
+
+ // TODO: support WebRTC.
+ // if (mozconfig.substs.MOZ_WEBRTC) {
+ // srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/audio_device/android/java/src"
+ // srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/video_capture/android/java/src"
+ // srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/video_render/android/java/src"
+ // }
+
+ // TODO: don't use AppConstants.
+ srcDir "${project.buildDir}/generated/source/preprocessed_code" // See syncPreprocessedCode.
+ }
+
+ assets {
+ }
+ }
+ }
+}
+
+dependencies {
+ compile "com.android.support:support-v4:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+}
+
+task syncPreprocessedCode(type: Sync, dependsOn: rootProject.generateCodeAndResources) {
+ into("${project.buildDir}/generated/source/preprocessed_code")
+ from("${topobjdir}/mobile/android/base/generated/preprocessed") {
+ // AdjustConstants is included in the main app project.
+ exclude '**/AdjustConstants.java'
+ }
+}
+
+apply from: "${topsrcdir}/mobile/android/gradle/with_gecko_binaries.gradle"
+
+android.libraryVariants.all { variant ->
+ variant.preBuild.dependsOn syncPreprocessedCode
+
+ // Like 'debug', 'release', or 'withGeckoBinaries'.
+ def buildType = variant.buildType.name
+
+ // It would be most natural for :geckoview to always include the Gecko
+ // binaries, but that's difficult; see the notes in
+ // mobile/android/gradle/with_gecko_binaries.gradle. Instead :app uses
+ // :geckoview:release and handles it's own Gecko binary inclusion.
+ if (buildType.equals('withGeckoBinaries')) {
+ configureVariantWithGeckoBinaries(variant)
+ }
+
+ // Javadoc and Sources JAR configuration cribbed from
+ // https://github.com/mapbox/mapbox-gl-native/blob/d169ea55c1cfa85cd8bf19f94c5f023569f71810/platform/android/MapboxGLAndroidSDK/build.gradle#L85
+ // informed by
+ // https://code.tutsplus.com/tutorials/creating-and-publishing-an-android-library--cms-24582,
+ // and amended from numerous Stackoverflow posts.
+ def name = variant.name
+ def javadoc = task "javadoc${name.capitalize()}"(type: Javadoc) {
+ description = "Generate Javadoc for build variant $name"
+ failOnError = false
+ destinationDir = new File(destinationDir, variant.baseName)
+ source = files(variant.javaCompile.source)
+ classpath = files(variant.javaCompile.classpath.files) + files(android.bootClasspath)
+ options.windowTitle("Mozilla GeckoView Android API $VERSION_NAME Reference")
+ options.docTitle("Mozilla GeckoView Android API $VERSION_NAME")
+ options.header("Mozilla GeckoView Android API $VERSION_NAME Reference")
+ options.bottom("&copy; 2016 Mozilla. All rights reserved.")
+ options.links("http://docs.oracle.com/javase/7/docs/api/")
+ options.linksOffline("http://d.android.com/reference/", "$System.env.ANDROID_HOME/docs/reference")
+ // TODO: options.overview("src/main/java/overview.html")
+ options.group("Mozilla GeckoView", "org.mozilla.gecko*") // TODO: narrow this down.
+ exclude '**/R.java', '**/BuildConfig.java', 'com/googlecode/**'
+ }
+
+ task "javadocJar${name.capitalize()}"(type: Jar, dependsOn: javadoc) {
+ classifier = 'javadoc'
+ from javadoc.destinationDir
+ }
+
+ task "sourcesJar${name.capitalize()}"(type: Jar) {
+ classifier 'sources'
+ description = "Generate Javadoc for build variant $name"
+ destinationDir = new File(destinationDir, variant.baseName)
+ from files(variant.javaCompile.source)
+ }
+}
+
+apply plugin: 'maven'
+
+uploadArchives {
+ repositories.mavenDeployer {
+ pom.groupId = 'org.mozilla'
+ pom.artifactId = 'geckoview'
+ pom.version = VERSION_NAME
+ pom.project {
+ licenses {
+ license {
+ name 'The Mozilla Public License, v. 2.0'
+ url 'http://mozilla.org/MPL/2.0/'
+ distribution 'repo'
+ }
+ }
+ }
+ repository(url: "file://${project.buildDir}/maven")
+ }
+}
+
+// This is all related to the withGeckoBinaries approach; see
+// mobile/android/gradle/with_gecko_binaries.gradle.
+afterEvaluate {
+ // The bundle tasks are only present when the particular configuration is
+ // being built, so this task might not exist. (This is due to the way the
+ // Android Gradle plugin defines things during configuration.)
+ def bundleWithGeckoBinaries = tasks.findByName('bundleWithGeckoBinaries')
+ if (!bundleWithGeckoBinaries) {
+ return
+ }
+
+ // Remove default configuration, which is the release configuration, when
+ // we're actually building withGeckoBinaries. This makes `gradle install`
+ // install the withGeckoBinaries artifacts, not the release artifacts (which
+ // are withoutGeckoBinaries and not suitable for distribution.)
+ def Configuration archivesConfig = project.getConfigurations().getByName('archives')
+ archivesConfig.artifacts.removeAll { it.extension.equals('aar') }
+
+ artifacts {
+ // Instead of default (release) configuration, publish one with Gecko binaries.
+ archives bundleWithGeckoBinaries
+ // Javadoc and sources for developer ergononomics.
+ archives javadocJarWithGeckoBinaries
+ archives sourcesJarWithGeckoBinaries
+ }
+}
diff --git a/mobile/android/geckoview/proguard-rules.txt b/mobile/android/geckoview/proguard-rules.txt
new file mode 100644
index 0000000000..6dbc4260b3
--- /dev/null
+++ b/mobile/android/geckoview/proguard-rules.txt
@@ -0,0 +1,175 @@
+# Modified from https://robotsandpencils.com/blog/use-proguard-android-library/.
+
+# Preserve all annotations.
+
+-keepattributes *Annotation*
+
+# Preserve all public classes, and their public and protected fields and
+# methods.
+
+-keep public class * {
+ public protected *;
+}
+
+# Preserve all .class method names.
+
+-keepclassmembernames class * {
+ java.lang.Class class$(java.lang.String);
+ java.lang.Class class$(java.lang.String, boolean);
+}
+
+# Preserve all native method names and the names of their classes.
+
+-keepclasseswithmembernames class * {
+ native <methods>;
+}
+
+# Preserve the special static methods that are required in all enumeration
+# classes.
+
+-keepclassmembers class * extends java.lang.Enum {
+ public static **[] values();
+ public static ** valueOf(java.lang.String);
+}
+
+# Explicitly preserve all serialization members. The Serializable interface
+# is only a marker interface, so it wouldn't save them.
+# You can comment this out if your library doesn't use serialization.
+# If your code contains serializable classes that have to be backward
+# compatible, please refer to the manual.
+
+-keepclassmembers class * implements java.io.Serializable {
+ static final long serialVersionUID;
+ static final java.io.ObjectStreamField[] serialPersistentFields;
+ private void writeObject(java.io.ObjectOutputStream);
+ private void readObject(java.io.ObjectInputStream);
+ java.lang.Object writeReplace();
+ java.lang.Object readResolve();
+}
+
+# Preserve all View implementations and their special context constructors.
+
+-keep public class * extends android.view.View {
+ public <init>(android.content.Context);
+ public <init>(android.content.Context, android.util.AttributeSet);
+ public <init>(android.content.Context, android.util.AttributeSet, int);
+ public void set*(...);
+}
+
+# Keep setters in Views so that animations can still work.
+# See http://proguard.sourceforge.net/manual/examples.html#beans
+# From tools/proguard/proguard-android.txt.
+-keepclassmembers public class * extends android.view.View {
+ void set*(***);
+ *** get*();
+}
+
+# Preserve all classes that have special context constructors, and the
+# constructors themselves.
+
+-keepclasseswithmembers class * {
+ public <init>(android.content.Context, android.util.AttributeSet);
+}
+
+# Preserve the special fields of all Parcelable implementations.
+
+-keepclassmembers class * implements android.os.Parcelable {
+ static android.os.Parcelable$Creator CREATOR;
+}
+
+# Preserve static fields of inner classes of R classes that might be accessed
+# through introspection.
+
+-keepclassmembers class **.R$* {
+ public static <fields>;
+}
+
+# GeckoView specific rules.
+
+# Keep classes, and all their contents, compiled before annotation.*.
+-keep class org.mozilla.gecko.AppConstants {
+ *;
+}
+-keep class org.mozilla.gecko.AppConstants$Versions {
+ *;
+}
+-keep class org.mozilla.gecko.SysInfo {
+ *;
+}
+
+# Keep the annotation.
+-keep @interface org.mozilla.gecko.annotation.JNITarget
+
+# Keep classes tagged with the annotation.
+-keep @org.mozilla.gecko.annotation.JNITarget class *
+
+# Keep all members of an annotated class.
+-keepclassmembers @org.mozilla.gecko.annotation.JNITarget class * {
+ *;
+}
+
+# Keep annotated members of any class.
+-keepclassmembers class * {
+ @org.mozilla.gecko.annotation.JNITarget *;
+}
+
+# Keep classes which contain at least one annotated element. Split over two directives
+# because, according to the developer of ProGuard, "the option -keepclasseswithmembers
+# doesn't combine well with the '*' wildcard" (And, indeed, using it causes things to
+# be deleted that we want to keep.)
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.annotation.JNITarget <methods>;
+}
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.annotation.JNITarget <fields>;
+}
+
+# Keep WebRTC targets.
+-keep @interface org.mozilla.gecko.annotation.WebRTCJNITarget
+-keep @org.mozilla.gecko.annotation.WebRTCJNITarget class *
+-keepclassmembers class * {
+ @org.mozilla.gecko.annotation.WebRTCJNITarget *;
+}
+-keepclassmembers @org.mozilla.gecko.annotation.WebRTCJNITarget class * {
+ *;
+}
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.annotation.WebRTCJNITarget <methods>;
+}
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.annotation.WebRTCJNITarget <fields>;
+}
+
+# Keep generator-targeted entry points.
+-keep @interface org.mozilla.gecko.annotation.WrapForJNI
+-keep @org.mozilla.gecko.annotation.WrapForJNI class *
+-keepclassmembers class * {
+ @org.mozilla.gecko.annotation.WrapForJNI *;
+}
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.annotation.WrapForJNI <methods>;
+}
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.annotation.WrapForJNI <fields>;
+}
+
+# Keep all members of an annotated class.
+-keepclassmembers @org.mozilla.gecko.annotation.WrapForJNI class * {
+ *;
+}
+
+# Keep Reflection targets.
+-keep @interface org.mozilla.gecko.annotation.ReflectionTarget
+-keep @org.mozilla.gecko.annotation.ReflectionTarget class *
+-keepclassmembers class * {
+ @org.mozilla.gecko.annotation.ReflectionTarget *;
+}
+-keepclassmembers @org.mozilla.gecko.annotation.ReflectionTarget class * {
+ *;
+}
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.annotation.ReflectionTarget <methods>;
+}
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.annotation.ReflectionTarget <fields>;
+}
diff --git a/mobile/android/geckoview/src/main/AndroidManifest.xml b/mobile/android/geckoview/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..4e2aaf4471
--- /dev/null
+++ b/mobile/android/geckoview/src/main/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.mozilla.geckoview">
+
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+ <!-- READ_EXTERNAL_STORAGE was added in API 16, and is only enforced in API
+ 19+. We declare it so that the bouncer APK and the main APK have the
+ same set of permissions. -->
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
+ <uses-permission android:name="com.android.launcher.permission.UNINSTALL_SHORTCUT"/>
+
+ <uses-permission android:name="android.permission.WAKE_LOCK"/>
+ <uses-permission android:name="android.permission.VIBRATE"/>
+
+ <uses-feature android:name="android.hardware.location" android:required="false"/>
+ <uses-feature android:name="android.hardware.location.gps" android:required="false"/>
+ <uses-feature android:name="android.hardware.touchscreen"/>
+
+ <!--#ifdef MOZ_WEBRTC-->
+ <!--<uses-permission android:name="android.permission.RECORD_AUDIO"/>-->
+ <!--<uses-feature android:name="android.hardware.audio.low_latency" android:required="false"/>-->
+ <!--<uses-feature android:name="android.hardware.camera.any" android:required="false"/>-->
+ <!--<uses-feature android:name="android.hardware.microphone" android:required="false"/>-->
+ <!--#endif-->
+
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-feature android:name="android.hardware.camera" android:required="false"/>
+ <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
+
+ <!-- App requires OpenGL ES 2.0 -->
+ <uses-feature android:glEsVersion="0x00020000" android:required="true" />
+
+</manifest>
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/AlarmReceiver.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/AlarmReceiver.java
new file mode 100644
index 0000000000..a098113fab
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/AlarmReceiver.java
@@ -0,0 +1,42 @@
+/* -*- 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 android.app.IntentService;
+import android.content.Context;
+import android.content.Intent;
+import android.content.BroadcastReceiver;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.util.Log;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class AlarmReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ PowerManager powerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
+ final WakeLock wakeLock = powerManager.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, "GeckoAlarm");
+ wakeLock.acquire();
+
+ AlarmReceiver.notifyAlarmFired();
+ TimerTask releaseLockTask = new TimerTask() {
+ @Override
+ public void run() {
+ wakeLock.release();
+ }
+ };
+ Timer timer = new Timer();
+ // 5 seconds ought to be enough for anybody
+ timer.schedule(releaseLockTask, 5 * 1000);
+ }
+
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+ private static native void notifyAlarmFired();
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/AndroidGamepadManager.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/AndroidGamepadManager.java
new file mode 100644
index 0000000000..54e1b09312
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/AndroidGamepadManager.java
@@ -0,0 +1,425 @@
+/* -*- 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.util.ArrayList;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.util.GamepadUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.content.Context;
+import android.hardware.input.InputManager;
+import android.util.SparseArray;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+
+public class AndroidGamepadManager {
+ // This is completely arbitrary.
+ private static final float TRIGGER_PRESSED_THRESHOLD = 0.25f;
+ private static final long POLL_TIMER_PERIOD = 1000; // milliseconds
+
+ private static enum Axis {
+ X(MotionEvent.AXIS_X),
+ Y(MotionEvent.AXIS_Y),
+ Z(MotionEvent.AXIS_Z),
+ RZ(MotionEvent.AXIS_RZ);
+
+ public final int axis;
+
+ private Axis(int axis) {
+ this.axis = axis;
+ }
+ }
+
+ // A list of gamepad button mappings. Axes are determined at
+ // runtime, as they vary by Android version.
+ private static enum Trigger {
+ Left(6),
+ Right(7);
+
+ public final int button;
+
+ private Trigger(int button) {
+ this.button = button;
+ }
+ }
+
+ private static final int FIRST_DPAD_BUTTON = 12;
+ // A list of axis number, gamepad button mappings for negative, positive.
+ // Button mappings are added to FIRST_DPAD_BUTTON.
+ private static enum DpadAxis {
+ UpDown(MotionEvent.AXIS_HAT_Y, 0, 1),
+ LeftRight(MotionEvent.AXIS_HAT_X, 2, 3);
+
+ public final int axis;
+ public final int negativeButton;
+ public final int positiveButton;
+
+ private DpadAxis(int axis, int negativeButton, int positiveButton) {
+ this.axis = axis;
+ this.negativeButton = negativeButton;
+ this.positiveButton = positiveButton;
+ }
+ }
+
+ private static enum Button {
+ A(KeyEvent.KEYCODE_BUTTON_A),
+ B(KeyEvent.KEYCODE_BUTTON_B),
+ X(KeyEvent.KEYCODE_BUTTON_X),
+ Y(KeyEvent.KEYCODE_BUTTON_Y),
+ L1(KeyEvent.KEYCODE_BUTTON_L1),
+ R1(KeyEvent.KEYCODE_BUTTON_R1),
+ L2(KeyEvent.KEYCODE_BUTTON_L2),
+ R2(KeyEvent.KEYCODE_BUTTON_R2),
+ SELECT(KeyEvent.KEYCODE_BUTTON_SELECT),
+ START(KeyEvent.KEYCODE_BUTTON_START),
+ THUMBL(KeyEvent.KEYCODE_BUTTON_THUMBL),
+ THUMBR(KeyEvent.KEYCODE_BUTTON_THUMBR),
+ DPAD_UP(KeyEvent.KEYCODE_DPAD_UP),
+ DPAD_DOWN(KeyEvent.KEYCODE_DPAD_DOWN),
+ DPAD_LEFT(KeyEvent.KEYCODE_DPAD_LEFT),
+ DPAD_RIGHT(KeyEvent.KEYCODE_DPAD_RIGHT);
+
+ public final int button;
+
+ private Button(int button) {
+ this.button = button;
+ }
+ }
+
+ private static class Gamepad {
+ // ID from GamepadService
+ public int id;
+ // Retain axis state so we can determine changes.
+ public float axes[];
+ public boolean dpad[];
+ public int triggerAxes[];
+ public float triggers[];
+
+ public Gamepad(int serviceId, int deviceId) {
+ id = serviceId;
+ axes = new float[Axis.values().length];
+ dpad = new boolean[4];
+ triggers = new float[2];
+
+ InputDevice device = InputDevice.getDevice(deviceId);
+ if (device != null) {
+ // LTRIGGER/RTRIGGER don't seem to be exposed on older
+ // versions of Android.
+ if (device.getMotionRange(MotionEvent.AXIS_LTRIGGER) != null && device.getMotionRange(MotionEvent.AXIS_RTRIGGER) != null) {
+ triggerAxes = new int[]{MotionEvent.AXIS_LTRIGGER,
+ MotionEvent.AXIS_RTRIGGER};
+ } else if (device.getMotionRange(MotionEvent.AXIS_BRAKE) != null && device.getMotionRange(MotionEvent.AXIS_GAS) != null) {
+ triggerAxes = new int[]{MotionEvent.AXIS_BRAKE,
+ MotionEvent.AXIS_GAS};
+ } else {
+ triggerAxes = null;
+ }
+ }
+ }
+ }
+
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+ private static native void onGamepadChange(int id, boolean added);
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+ private static native void onButtonChange(int id, int button, boolean pressed, float value);
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+ private static native void onAxisChange(int id, boolean[] valid, float[] values);
+
+ private static boolean sStarted;
+ private static final SparseArray<Gamepad> sGamepads = new SparseArray<>();
+ private static final SparseArray<List<KeyEvent>> sPendingGamepads = new SparseArray<>();
+ private static InputManager.InputDeviceListener sListener;
+ private static Timer sPollTimer;
+
+ private AndroidGamepadManager() {
+ }
+
+ @WrapForJNI
+ private static void start() {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doStart();
+ }
+ });
+ }
+
+ /* package */ static void doStart() {
+ ThreadUtils.assertOnUiThread();
+ if (!sStarted) {
+ scanForGamepads();
+ addDeviceListener();
+ sStarted = true;
+ }
+ }
+
+ @WrapForJNI
+ private static void stop() {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doStop();
+ }
+ });
+ }
+
+ /* package */ static void doStop() {
+ ThreadUtils.assertOnUiThread();
+ if (sStarted) {
+ removeDeviceListener();
+ sPendingGamepads.clear();
+ sGamepads.clear();
+ sStarted = false;
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void onGamepadAdded(final int device_id, final int service_id) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ handleGamepadAdded(device_id, service_id);
+ }
+ });
+ }
+
+ /* package */ static void handleGamepadAdded(int deviceId, int serviceId) {
+ ThreadUtils.assertOnUiThread();
+ if (!sStarted) {
+ return;
+ }
+
+ final List<KeyEvent> pending = sPendingGamepads.get(deviceId);
+ if (pending == null) {
+ removeGamepad(deviceId);
+ return;
+ }
+
+ sPendingGamepads.remove(deviceId);
+ sGamepads.put(deviceId, new Gamepad(serviceId, deviceId));
+ // Handle queued KeyEvents
+ for (KeyEvent ev : pending) {
+ handleKeyEvent(ev);
+ }
+ }
+
+ private static float deadZone(MotionEvent ev, int axis) {
+ if (GamepadUtils.isValueInDeadZone(ev, axis)) {
+ return 0.0f;
+ }
+ return ev.getAxisValue(axis);
+ }
+
+ private static void mapDpadAxis(Gamepad gamepad,
+ boolean pressed,
+ float value,
+ int which) {
+ if (pressed != gamepad.dpad[which]) {
+ gamepad.dpad[which] = pressed;
+ onButtonChange(gamepad.id, FIRST_DPAD_BUTTON + which, pressed, Math.abs(value));
+ }
+ }
+
+ public static boolean handleMotionEvent(MotionEvent ev) {
+ ThreadUtils.assertOnUiThread();
+ if (!sStarted) {
+ return false;
+ }
+
+ final Gamepad gamepad = sGamepads.get(ev.getDeviceId());
+ if (gamepad == null) {
+ // Not a device we care about.
+ return false;
+ }
+
+ // First check the analog stick axes
+ boolean[] valid = new boolean[Axis.values().length];
+ float[] axes = new float[Axis.values().length];
+ boolean anyValidAxes = false;
+ for (Axis axis : Axis.values()) {
+ float value = deadZone(ev, axis.axis);
+ int i = axis.ordinal();
+ if (value != gamepad.axes[i]) {
+ axes[i] = value;
+ gamepad.axes[i] = value;
+ valid[i] = true;
+ anyValidAxes = true;
+ }
+ }
+ if (anyValidAxes) {
+ // Send an axismove event.
+ onAxisChange(gamepad.id, valid, axes);
+ }
+
+ // Map triggers to buttons.
+ if (gamepad.triggerAxes != null) {
+ for (Trigger trigger : Trigger.values()) {
+ int i = trigger.ordinal();
+ int axis = gamepad.triggerAxes[i];
+ float value = deadZone(ev, axis);
+ if (value != gamepad.triggers[i]) {
+ gamepad.triggers[i] = value;
+ boolean pressed = value > TRIGGER_PRESSED_THRESHOLD;
+ onButtonChange(gamepad.id, trigger.button, pressed, value);
+ }
+ }
+ }
+ // Map d-pad to buttons.
+ for (DpadAxis dpadaxis : DpadAxis.values()) {
+ float value = deadZone(ev, dpadaxis.axis);
+ mapDpadAxis(gamepad, value < 0.0f, value, dpadaxis.negativeButton);
+ mapDpadAxis(gamepad, value > 0.0f, value, dpadaxis.positiveButton);
+ }
+ return true;
+ }
+
+ public static boolean handleKeyEvent(KeyEvent ev) {
+ ThreadUtils.assertOnUiThread();
+ if (!sStarted) {
+ return false;
+ }
+
+ int deviceId = ev.getDeviceId();
+ final List<KeyEvent> pendingGamepad = sPendingGamepads.get(deviceId);
+ if (pendingGamepad != null) {
+ // Queue up key events for pending devices.
+ pendingGamepad.add(ev);
+ return true;
+ }
+
+ if (sGamepads.get(deviceId) == null) {
+ InputDevice device = ev.getDevice();
+ if (device != null &&
+ (device.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
+ // This is a gamepad we haven't seen yet.
+ addGamepad(device);
+ sPendingGamepads.get(deviceId).add(ev);
+ return true;
+ }
+ // Not a device we care about.
+ return false;
+ }
+
+ int key = -1;
+ for (Button button : Button.values()) {
+ if (button.button == ev.getKeyCode()) {
+ key = button.ordinal();
+ break;
+ }
+ }
+ if (key == -1) {
+ // Not a key we know how to handle.
+ return false;
+ }
+ if (ev.getRepeatCount() > 0) {
+ // We would handle this key, but we're not interested in
+ // repeats. Eat it.
+ return true;
+ }
+
+ Gamepad gamepad = sGamepads.get(deviceId);
+ boolean pressed = ev.getAction() == KeyEvent.ACTION_DOWN;
+ onButtonChange(gamepad.id, key, pressed, pressed ? 1.0f : 0.0f);
+ return true;
+ }
+
+ private static void scanForGamepads() {
+ int[] deviceIds = InputDevice.getDeviceIds();
+ if (deviceIds == null) {
+ return;
+ }
+ for (int i = 0; i < deviceIds.length; i++) {
+ InputDevice device = InputDevice.getDevice(deviceIds[i]);
+ if (device == null) {
+ continue;
+ }
+ if ((device.getSources() & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD) {
+ continue;
+ }
+ addGamepad(device);
+ }
+ }
+
+ private static void addGamepad(InputDevice device) {
+ sPendingGamepads.put(device.getId(), new ArrayList<KeyEvent>());
+ onGamepadChange(device.getId(), true);
+ }
+
+ private static void removeGamepad(int deviceId) {
+ Gamepad gamepad = sGamepads.get(deviceId);
+ onGamepadChange(gamepad.id, false);
+ sGamepads.remove(deviceId);
+ }
+
+ private static void addDeviceListener() {
+ if (Versions.preJB) {
+ // Poll known gamepads to see if they've disappeared.
+ sPollTimer = new Timer();
+ sPollTimer.scheduleAtFixedRate(new TimerTask() {
+ @Override
+ public void run() {
+ for (int i = 0; i < sGamepads.size(); ++i) {
+ final int deviceId = sGamepads.keyAt(i);
+ if (InputDevice.getDevice(deviceId) == null) {
+ removeGamepad(deviceId);
+ }
+ }
+ }
+ }, POLL_TIMER_PERIOD, POLL_TIMER_PERIOD);
+ return;
+ }
+ sListener = new InputManager.InputDeviceListener() {
+ @Override
+ public void onInputDeviceAdded(int deviceId) {
+ InputDevice device = InputDevice.getDevice(deviceId);
+ if (device == null) {
+ return;
+ }
+ if ((device.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
+ addGamepad(device);
+ }
+ }
+
+ @Override
+ public void onInputDeviceRemoved(int deviceId) {
+ if (sPendingGamepads.get(deviceId) != null) {
+ // Got removed before Gecko's ack reached us.
+ // gamepadAdded will deal with it.
+ sPendingGamepads.remove(deviceId);
+ return;
+ }
+ if (sGamepads.get(deviceId) != null) {
+ removeGamepad(deviceId);
+ }
+ }
+
+ @Override
+ public void onInputDeviceChanged(int deviceId) {
+ }
+ };
+ ((InputManager)GeckoAppShell.getContext().getSystemService(Context.INPUT_SERVICE)).registerInputDeviceListener(sListener, ThreadUtils.getUiHandler());
+ }
+
+ private static void removeDeviceListener() {
+ if (Versions.preJB) {
+ if (sPollTimer != null) {
+ sPollTimer.cancel();
+ sPollTimer = null;
+ }
+ return;
+ }
+ ((InputManager)GeckoAppShell.getContext().getSystemService(Context.INPUT_SERVICE)).unregisterInputDeviceListener(sListener);
+ sListener = null;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/BaseGeckoInterface.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/BaseGeckoInterface.java
new file mode 100644
index 0000000000..c4f64fd3d5
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/BaseGeckoInterface.java
@@ -0,0 +1,169 @@
+/* -*- 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.ActivityUtils;
+import org.mozilla.gecko.util.HardwareUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.RectF;
+import android.hardware.SensorEventListener;
+import android.location.LocationListener;
+import android.view.View;
+import android.widget.AbsoluteLayout;
+
+public class BaseGeckoInterface implements GeckoAppShell.GeckoInterface {
+ // Bug 908744: Implement GeckoEventListener
+ // Bug 908752: Implement SensorEventListener
+ // Bug 908755: Implement LocationListener
+ // Bug 908756: Implement Tabs.OnTabsChangedListener
+ // Bug 908760: Implement GeckoEventResponder
+
+ private final Context mContext;
+ private GeckoProfile mProfile;
+ private final EventDispatcher eventDispatcher;
+
+ public BaseGeckoInterface(Context context) {
+ mContext = context;
+ eventDispatcher = new EventDispatcher();
+ }
+
+ @Override
+ public EventDispatcher getAppEventDispatcher() {
+ return eventDispatcher;
+ }
+
+ @Override
+ public GeckoProfile getProfile() {
+ // Fall back to default profile if we didn't load a specific one
+ if (mProfile == null) {
+ mProfile = GeckoProfile.get(mContext);
+ }
+ return mProfile;
+ }
+
+ @Override
+ public Activity getActivity() {
+ return (Activity)mContext;
+ }
+
+ @Override
+ public String getDefaultUAString() {
+ return HardwareUtils.isTablet() ? AppConstants.USER_AGENT_FENNEC_TABLET :
+ AppConstants.USER_AGENT_FENNEC_MOBILE;
+ }
+
+ // Bug 908775: Implement this
+ @Override
+ public void doRestart() {}
+
+ @Override
+ public void setFullScreen(final boolean fullscreen) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ActivityUtils.setFullScreen(getActivity(), fullscreen);
+ }
+ });
+ }
+
+ // Bug 908779: Implement this
+ @Override
+ public void addPluginView(final View view) {}
+
+ // Bug 908781: Implement this
+ @Override
+ public void removePluginView(final View view) {}
+
+ @Override
+ public void enableOrientationListener() {}
+
+ @Override
+ public void disableOrientationListener() {}
+
+ // Bug 908786: Implement this
+ @Override
+ public void addAppStateListener(GeckoAppShell.AppStateListener listener) {}
+
+ // Bug 908787: Implement this
+ @Override
+ public void removeAppStateListener(GeckoAppShell.AppStateListener listener) {}
+
+ // Bug 908789: Implement this
+ @Override
+ public void notifyWakeLockChanged(String topic, String state) {}
+
+ @Override
+ public boolean areTabsShown() {
+ return false;
+ }
+
+ // Bug 908791: Implement this
+ @Override
+ public AbsoluteLayout getPluginContainer() {
+ return null;
+ }
+
+ @Override
+ public void notifyCheckUpdateResult(String result) {
+ GeckoAppShell.notifyObservers("Update:CheckResult", result);
+ }
+
+ // Bug 908792: Implement this
+ @Override
+ public void invalidateOptionsMenu() {}
+
+ @Override
+ public void createShortcut(String title, String URI) {
+ // By default, do nothing.
+ }
+
+ @Override
+ public void checkUriVisited(String uri) {
+ // By default, no URIs are considered visited.
+ }
+
+ @Override
+ public void markUriVisited(final String uri) {
+ // By default, no URIs are marked as visited.
+ }
+
+ @Override
+ public void setUriTitle(final String uri, final String title) {
+ // By default, no titles are associated with URIs.
+ }
+
+ @Override
+ public void setAccessibilityEnabled(boolean enabled) {
+ // By default, take no action when accessibility is toggled on or off.
+ }
+
+ @Override
+ public boolean openUriExternal(String targetURI, String mimeType, String packageName, String className, String action, String title) {
+ // By default, never open external URIs.
+ return false;
+ }
+
+ @Override
+ public String[] getHandlersForMimeType(String mimeType, String action) {
+ // By default, offer no handlers for any MIME type.
+ return new String[] {};
+ }
+
+ @Override
+ public String[] getHandlersForURL(String url, String action) {
+ // By default, offer no handlers for any URL.
+ return new String[] {};
+ }
+
+ @Override
+ public String getDefaultChromeURI() {
+ // By default, use the GeckoView-specific chrome URI.
+ return "chrome://browser/content/geckoview.xul";
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/ContextGetter.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/ContextGetter.java
new file mode 100644
index 0000000000..3158546334
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/ContextGetter.java
@@ -0,0 +1,15 @@
+/* -*- 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.content.Context;
+import android.content.SharedPreferences;
+
+public interface ContextGetter {
+ Context getContext();
+ SharedPreferences getSharedPreferences();
+}
+
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/CrashHandler.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/CrashHandler.java
new file mode 100644
index 0000000000..15df273368
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/CrashHandler.java
@@ -0,0 +1,478 @@
+/* -*- 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.BufferedWriter;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Arrays;
+import java.util.UUID;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Process;
+import android.util.Log;
+
+public class CrashHandler implements Thread.UncaughtExceptionHandler {
+
+ private static final String LOGTAG = "GeckoCrashHandler";
+ private static final Thread MAIN_THREAD = Thread.currentThread();
+ private static final String DEFAULT_SERVER_URL =
+ "https://crash-reports.mozilla.com/submit?id=%1$s&version=%2$s&buildid=%3$s";
+
+ // Context for getting device information
+ protected final Context appContext;
+ // Thread that this handler applies to, or null for a global handler
+ protected final Thread handlerThread;
+ protected final Thread.UncaughtExceptionHandler systemUncaughtHandler;
+
+ protected boolean crashing;
+ protected boolean unregistered;
+
+ /**
+ * Get the root exception from the 'cause' chain of an exception.
+ *
+ * @param exc An exception
+ * @return The root exception
+ */
+ public static Throwable getRootException(Throwable exc) {
+ for (Throwable cause = exc; cause != null; cause = cause.getCause()) {
+ exc = cause;
+ }
+ return exc;
+ }
+
+ /**
+ * Get the standard stack trace string of an exception.
+ *
+ * @param exc An exception
+ * @return The exception stack trace.
+ */
+ public static String getExceptionStackTrace(final Throwable exc) {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ exc.printStackTrace(pw);
+ pw.flush();
+ return sw.toString();
+ }
+
+ /**
+ * Terminate the current process.
+ */
+ public static void terminateProcess() {
+ Process.killProcess(Process.myPid());
+ }
+
+ /**
+ * Create and register a CrashHandler for all threads and thread groups.
+ */
+ public CrashHandler() {
+ this((Context) null);
+ }
+
+ /**
+ * Create and register a CrashHandler for all threads and thread groups.
+ *
+ * @param appContext A Context for retrieving application information.
+ */
+ public CrashHandler(final Context appContext) {
+ this.appContext = appContext;
+ this.handlerThread = null;
+ this.systemUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler();
+ Thread.setDefaultUncaughtExceptionHandler(this);
+ }
+
+ /**
+ * Create and register a CrashHandler for a particular thread.
+ *
+ * @param thread A thread to register the CrashHandler
+ */
+ public CrashHandler(final Thread thread) {
+ this(thread, null);
+ }
+
+ /**
+ * Create and register a CrashHandler for a particular thread.
+ *
+ * @param thread A thread to register the CrashHandler
+ * @param appContext A Context for retrieving application information.
+ */
+ public CrashHandler(final Thread thread, final Context appContext) {
+ this.appContext = appContext;
+ this.handlerThread = thread;
+ this.systemUncaughtHandler = thread.getUncaughtExceptionHandler();
+ thread.setUncaughtExceptionHandler(this);
+ }
+
+ /**
+ * Unregister this CrashHandler for exception handling.
+ */
+ public void unregister() {
+ unregistered = true;
+
+ // Restore the previous handler if we are still the topmost handler.
+ // If not, we are part of a chain of handlers, and we cannot just restore the previous
+ // handler, because that would replace whatever handler that's above us in the chain.
+
+ if (handlerThread != null) {
+ if (handlerThread.getUncaughtExceptionHandler() == this) {
+ handlerThread.setUncaughtExceptionHandler(systemUncaughtHandler);
+ }
+ } else {
+ if (Thread.getDefaultUncaughtExceptionHandler() == this) {
+ Thread.setDefaultUncaughtExceptionHandler(systemUncaughtHandler);
+ }
+ }
+ }
+
+ /**
+ * Record an exception stack in logs.
+ *
+ * @param thread The exception thread
+ * @param exc An exception
+ */
+ public static void logException(final Thread thread, final Throwable exc) {
+ try {
+ Log.e(LOGTAG, ">>> REPORTING UNCAUGHT EXCEPTION FROM THREAD "
+ + thread.getId() + " (\"" + thread.getName() + "\")", exc);
+
+ if (MAIN_THREAD != thread) {
+ Log.e(LOGTAG, "Main thread (" + MAIN_THREAD.getId() + ") stack:");
+ for (StackTraceElement ste : MAIN_THREAD.getStackTrace()) {
+ Log.e(LOGTAG, " " + ste.toString());
+ }
+ }
+ } catch (final Throwable e) {
+ // If something throws here, we want to continue to report the exception,
+ // so we catch all exceptions and ignore them.
+ }
+ }
+
+ private static long getCrashTime() {
+ return System.currentTimeMillis() / 1000;
+ }
+
+ private static long getStartupTime() {
+ // Process start time is also the proc file modified time.
+ final long uptimeMins = (new File("/proc/self/cmdline")).lastModified();
+ if (uptimeMins == 0L) {
+ return getCrashTime();
+ }
+ return uptimeMins / 1000;
+ }
+
+ private static String getJavaPackageName() {
+ return CrashHandler.class.getPackage().getName();
+ }
+
+ private static String getProcessName() {
+ try {
+ final FileReader reader = new FileReader("/proc/self/cmdline");
+ final char[] buffer = new char[64];
+ try {
+ if (reader.read(buffer) > 0) {
+ // cmdline is delimited by '\0', and we want the first token.
+ final int nul = Arrays.asList(buffer).indexOf('\0');
+ return (new String(buffer, 0, nul < 0 ? buffer.length : nul)).trim();
+ }
+ } finally {
+ reader.close();
+ }
+ } catch (final IOException e) {
+ }
+
+ return null;
+ }
+
+ protected String getAppPackageName() {
+ final Context context = getAppContext();
+
+ if (context != null) {
+ return context.getPackageName();
+ }
+
+ // Package name is also the process name in most cases.
+ String processName = getProcessName();
+ if (processName != null) {
+ return processName;
+ }
+
+ // Fallback to using CrashHandler's package name.
+ return getJavaPackageName();
+ }
+
+ protected Context getAppContext() {
+ return appContext;
+ }
+
+ /**
+ * Get the crash "extras" to be reported.
+ *
+ * @param thread The exception thread
+ * @param exc An exception
+ * @return "Extras" in the from of a Bundle
+ */
+ protected Bundle getCrashExtras(final Thread thread, final Throwable exc) {
+ final Context context = getAppContext();
+ final Bundle extras = new Bundle();
+ final String pkgName = getAppPackageName();
+ final String processName = getProcessName();
+
+ extras.putString("ProductName", pkgName);
+ extras.putLong("CrashTime", getCrashTime());
+ extras.putLong("StartupTime", getStartupTime());
+ extras.putString("AndroidProcessName", getProcessName());
+
+ if (context != null) {
+ final PackageManager pkgMgr = context.getPackageManager();
+ try {
+ final PackageInfo pkgInfo = pkgMgr.getPackageInfo(pkgName, 0);
+ extras.putString("Version", pkgInfo.versionName);
+ extras.putInt("BuildID", pkgInfo.versionCode);
+ extras.putLong("InstallTime", pkgInfo.lastUpdateTime / 1000);
+ } catch (final PackageManager.NameNotFoundException e) {
+ Log.i(LOGTAG, "Error getting package info", e);
+ }
+ }
+
+ extras.putString("JavaStackTrace", getExceptionStackTrace(exc));
+ return extras;
+ }
+
+ /**
+ * Get the crash minidump content to be reported.
+ *
+ * @param thread The exception thread
+ * @param exc An exception
+ * @return Minidump content
+ */
+ protected byte[] getCrashDump(final Thread thread, final Throwable exc) {
+ return new byte[0]; // No minidump.
+ }
+
+ protected static String normalizeUrlString(final String str) {
+ if (str == null) {
+ return "";
+ }
+ return Uri.encode(str);
+ }
+
+ /**
+ * Get the server URL to send the crash report to.
+ *
+ * @param extras The crash extras Bundle
+ */
+ protected String getServerUrl(final Bundle extras) {
+ return String.format(DEFAULT_SERVER_URL,
+ normalizeUrlString(extras.getString("ProductID")),
+ normalizeUrlString(extras.getString("Version")),
+ normalizeUrlString(extras.getString("BuildID")));
+ }
+
+ /**
+ * Launch the crash reporter activity that sends the crash report to the server.
+ *
+ * @param dumpFile Path for the minidump file
+ * @param extraFile Path for the crash extra file
+ * @return Whether the crash reporter was successfully launched
+ */
+ protected boolean launchCrashReporter(final String dumpFile, final String extraFile) {
+ try {
+ final Context context = getAppContext();
+ final String javaPkg = getJavaPackageName();
+ final String pkg = getAppPackageName();
+ final String component = javaPkg + ".CrashReporter";
+ final String action = javaPkg + ".reportCrash";
+ final ProcessBuilder pb;
+
+ if (context != null) {
+ final Intent intent = new Intent(action);
+ intent.setComponent(new ComponentName(pkg, component));
+ intent.putExtra("minidumpPath", dumpFile);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ return true;
+ }
+
+ // Avoid AppConstants dependency for SDK version constants,
+ // because CrashHandler could be used outside of Fennec code.
+ if (Build.VERSION.SDK_INT < 17) {
+ pb = new ProcessBuilder(
+ "/system/bin/am", "start",
+ "-a", action,
+ "-n", pkg + '/' + component,
+ "--es", "minidumpPath", dumpFile);
+ } else {
+ pb = new ProcessBuilder(
+ "/system/bin/am", "start",
+ "--user", /* USER_CURRENT_OR_SELF */ "-3",
+ "-a", action,
+ "-n", pkg + '/' + component,
+ "--es", "minidumpPath", dumpFile);
+ }
+
+ pb.start().waitFor();
+
+ } catch (final IOException e) {
+ Log.e(LOGTAG, "Error launching crash reporter", e);
+ return false;
+
+ } catch (final InterruptedException e) {
+ Log.i(LOGTAG, "Interrupted while waiting to launch crash reporter", e);
+ // Fall-through
+ }
+ return true;
+ }
+
+ /**
+ * Report an exception to Socorro.
+ *
+ * @param thread The exception thread
+ * @param exc An exception
+ * @return Whether the exception was successfully reported
+ */
+ protected boolean reportException(final Thread thread, final Throwable exc) {
+ final Context context = getAppContext();
+ final String id = UUID.randomUUID().toString();
+
+ // Use the cache directory under the app directory to store crash files.
+ final File dir;
+ if (context != null) {
+ dir = context.getCacheDir();
+ } else {
+ dir = new File("/data/data/" + getAppPackageName() + "/cache");
+ }
+
+ dir.mkdirs();
+ if (!dir.exists()) {
+ return false;
+ }
+
+ final File dmpFile = new File(dir, id + ".dmp");
+ final File extraFile = new File(dir, id + ".extra");
+
+ try {
+ // Write out minidump file as binary.
+
+ final byte[] minidump = getCrashDump(thread, exc);
+ final FileOutputStream dmpStream = new FileOutputStream(dmpFile);
+ try {
+ dmpStream.write(minidump);
+ } finally {
+ dmpStream.close();
+ }
+
+ } catch (final IOException e) {
+ Log.e(LOGTAG, "Error writing minidump file", e);
+ return false;
+ }
+
+ try {
+ // Write out crash extra file as text.
+
+ final Bundle extras = getCrashExtras(thread, exc);
+ final String url = getServerUrl(extras);
+ extras.putString("ServerURL", url);
+
+ final BufferedWriter extraWriter = new BufferedWriter(new FileWriter(extraFile));
+ try {
+ for (String key : extras.keySet()) {
+ // Each extra line is in the format, key=value, with newlines escaped.
+ extraWriter.write(key);
+ extraWriter.write('=');
+ extraWriter.write(String.valueOf(extras.get(key)).replace("\n", "\\n"));
+ extraWriter.write('\n');
+ }
+ } finally {
+ extraWriter.close();
+ }
+
+ } catch (final IOException e) {
+ Log.e(LOGTAG, "Error writing extra file", e);
+ return false;
+ }
+
+ return launchCrashReporter(dmpFile.getAbsolutePath(), extraFile.getAbsolutePath());
+ }
+
+ /**
+ * Implements the default behavior for handling uncaught exceptions.
+ *
+ * @param thread The exception thread
+ * @param exc An uncaught exception
+ */
+ @Override
+ public void uncaughtException(Thread thread, Throwable exc) {
+ if (this.crashing) {
+ // Prevent possible infinite recusions.
+ return;
+ }
+
+ if (thread == null) {
+ // Gecko may pass in null for thread to denote the current thread.
+ thread = Thread.currentThread();
+ }
+
+ try {
+ if (!this.unregistered) {
+ // Only process crash ourselves if we have not been unregistered.
+
+ this.crashing = true;
+ exc = getRootException(exc);
+ logException(thread, exc);
+
+ if (reportException(thread, exc)) {
+ // Reporting succeeded; we can terminate our process now.
+ return;
+ }
+ }
+
+ if (systemUncaughtHandler != null) {
+ // Follow the chain of uncaught handlers.
+ systemUncaughtHandler.uncaughtException(thread, exc);
+ }
+ } finally {
+ terminateProcess();
+ }
+ }
+
+ public static CrashHandler createDefaultCrashHandler(final Context context) {
+ return new CrashHandler(context) {
+ @Override
+ protected Bundle getCrashExtras(final Thread thread, final Throwable exc) {
+ final Bundle extras = super.getCrashExtras(thread, exc);
+
+ extras.putString("ProductName", AppConstants.MOZ_APP_BASENAME);
+ extras.putString("ProductID", AppConstants.MOZ_APP_ID);
+ extras.putString("Version", AppConstants.MOZ_APP_VERSION);
+ extras.putString("BuildID", AppConstants.MOZ_APP_BUILDID);
+ extras.putString("Vendor", AppConstants.MOZ_APP_VENDOR);
+ extras.putString("ReleaseChannel", AppConstants.MOZ_UPDATE_CHANNEL);
+ return extras;
+ }
+
+ @Override
+ public boolean reportException(final Thread thread, final Throwable exc) {
+ if (AppConstants.MOZ_CRASHREPORTER && AppConstants.MOZILLA_OFFICIAL) {
+ // Only use Java crash reporter if enabled on official build.
+ return super.reportException(thread, exc);
+ }
+ return false;
+ }
+ };
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java
new file mode 100644
index 0000000000..6c4e67b433
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java
@@ -0,0 +1,503 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.ReflectionTarget;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.util.BundleEventListener;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoEventListener;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSContainer;
+import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+@RobocopTarget
+public final class EventDispatcher {
+ private static final String LOGTAG = "GeckoEventDispatcher";
+ /* package */ static final String GUID = "__guid__";
+ private static final String STATUS_ERROR = "error";
+ private static final String STATUS_SUCCESS = "success";
+
+ private static final EventDispatcher INSTANCE = new EventDispatcher();
+
+ /**
+ * The capacity of a HashMap is rounded up to the next power-of-2. Every time the size
+ * of the map goes beyond 75% of the capacity, the map is rehashed. Therefore, to
+ * empirically determine the initial capacity that avoids rehashing, we need to
+ * determine the initial size, divide it by 75%, and round up to the next power-of-2.
+ */
+ private static final int DEFAULT_GECKO_NATIVE_EVENTS_COUNT = 0; // Default for HashMap
+ private static final int DEFAULT_GECKO_JSON_EVENTS_COUNT = 256; // Empirically measured
+ private static final int DEFAULT_UI_EVENTS_COUNT = 0; // Default for HashMap
+ private static final int DEFAULT_BACKGROUND_EVENTS_COUNT = 0; // Default for HashMap
+
+ private final Map<String, List<NativeEventListener>> mGeckoThreadNativeListeners =
+ new HashMap<String, List<NativeEventListener>>(DEFAULT_GECKO_NATIVE_EVENTS_COUNT);
+ private final Map<String, List<GeckoEventListener>> mGeckoThreadJSONListeners =
+ new HashMap<String, List<GeckoEventListener>>(DEFAULT_GECKO_JSON_EVENTS_COUNT);
+ private final Map<String, List<BundleEventListener>> mUiThreadListeners =
+ new HashMap<String, List<BundleEventListener>>(DEFAULT_UI_EVENTS_COUNT);
+ private final Map<String, List<BundleEventListener>> mBackgroundThreadListeners =
+ new HashMap<String, List<BundleEventListener>>(DEFAULT_BACKGROUND_EVENTS_COUNT);
+
+ @ReflectionTarget
+ public static EventDispatcher getInstance() {
+ return INSTANCE;
+ }
+
+ public EventDispatcher() {
+ }
+
+ private <T> void registerListener(final Class<?> listType,
+ final Map<String, List<T>> listenersMap,
+ final T listener,
+ final String[] events) {
+ try {
+ synchronized (listenersMap) {
+ for (final String event : events) {
+ List<T> listeners = listenersMap.get(event);
+ if (listeners == null) {
+ // Java doesn't let us put Class<? extends List<T>> as the type for listType.
+ @SuppressWarnings("unchecked")
+ final Class<? extends List<T>> type = (Class) listType;
+ listeners = type.newInstance();
+ listenersMap.put(event, listeners);
+ }
+ if (!AppConstants.RELEASE_OR_BETA && listeners.contains(listener)) {
+ throw new IllegalStateException("Already registered " + event);
+ }
+ listeners.add(listener);
+ }
+ }
+ } catch (final IllegalAccessException | InstantiationException e) {
+ throw new IllegalArgumentException("Invalid new list type", e);
+ }
+ }
+
+ private void checkNotRegisteredElsewhere(final Map<String, ?> allowedMap,
+ final String[] events) {
+ if (AppConstants.RELEASE_OR_BETA) {
+ // for performance reasons, we only check for
+ // already-registered listeners in non-release builds.
+ return;
+ }
+ for (final Map<String, ?> listenersMap : Arrays.asList(mGeckoThreadNativeListeners,
+ mGeckoThreadJSONListeners,
+ mUiThreadListeners,
+ mBackgroundThreadListeners)) {
+ if (listenersMap == allowedMap) {
+ continue;
+ }
+ synchronized (listenersMap) {
+ for (final String event : events) {
+ if (listenersMap.get(event) != null) {
+ throw new IllegalStateException(
+ "Already registered " + event + " under a different type");
+ }
+ }
+ }
+ }
+ }
+
+ private <T> void unregisterListener(final Map<String, List<T>> listenersMap,
+ final T listener,
+ final String[] events) {
+ synchronized (listenersMap) {
+ for (final String event : events) {
+ List<T> listeners = listenersMap.get(event);
+ if ((listeners == null ||
+ !listeners.remove(listener)) && !AppConstants.RELEASE_OR_BETA) {
+ throw new IllegalArgumentException(event + " was not registered");
+ }
+ }
+ }
+ }
+
+ public void registerGeckoThreadListener(final NativeEventListener listener,
+ final String... events) {
+ checkNotRegisteredElsewhere(mGeckoThreadNativeListeners, events);
+
+ // For listeners running on the Gecko thread, we want to notify the listeners
+ // outside of our synchronized block, because the listeners may take an
+ // indeterminate amount of time to run. Therefore, to ensure concurrency when
+ // iterating the list outside of the synchronized block, we use a
+ // CopyOnWriteArrayList.
+ registerListener(CopyOnWriteArrayList.class,
+ mGeckoThreadNativeListeners, listener, events);
+ }
+
+ @Deprecated // Use NativeEventListener instead
+ public void registerGeckoThreadListener(final GeckoEventListener listener,
+ final String... events) {
+ checkNotRegisteredElsewhere(mGeckoThreadJSONListeners, events);
+
+ registerListener(CopyOnWriteArrayList.class,
+ mGeckoThreadJSONListeners, listener, events);
+ }
+
+ public void registerUiThreadListener(final BundleEventListener listener,
+ final String... events) {
+ checkNotRegisteredElsewhere(mUiThreadListeners, events);
+
+ registerListener(ArrayList.class,
+ mUiThreadListeners, listener, events);
+ }
+
+ @ReflectionTarget
+ public void registerBackgroundThreadListener(final BundleEventListener listener,
+ final String... events) {
+ checkNotRegisteredElsewhere(mBackgroundThreadListeners, events);
+
+ registerListener(ArrayList.class,
+ mBackgroundThreadListeners, listener, events);
+ }
+
+ public void unregisterGeckoThreadListener(final NativeEventListener listener,
+ final String... events) {
+ unregisterListener(mGeckoThreadNativeListeners, listener, events);
+ }
+
+ @Deprecated // Use NativeEventListener instead
+ public void unregisterGeckoThreadListener(final GeckoEventListener listener,
+ final String... events) {
+ unregisterListener(mGeckoThreadJSONListeners, listener, events);
+ }
+
+ public void unregisterUiThreadListener(final BundleEventListener listener,
+ final String... events) {
+ unregisterListener(mUiThreadListeners, listener, events);
+ }
+
+ public void unregisterBackgroundThreadListener(final BundleEventListener listener,
+ final String... events) {
+ unregisterListener(mBackgroundThreadListeners, listener, events);
+ }
+
+ private List<NativeEventListener> getNativeListeners(final String type) {
+ final List<NativeEventListener> listeners;
+ synchronized (mGeckoThreadNativeListeners) {
+ listeners = mGeckoThreadNativeListeners.get(type);
+ }
+ return listeners;
+ }
+
+ private List<GeckoEventListener> getGeckoListeners(final String type) {
+ final List<GeckoEventListener> listeners;
+ synchronized (mGeckoThreadJSONListeners) {
+ listeners = mGeckoThreadJSONListeners.get(type);
+ }
+ return listeners;
+ }
+
+ public boolean dispatchEvent(final NativeJSContainer message) {
+ // First try native listeners.
+ final String type = message.optString("type", null);
+ if (type == null) {
+ Log.e(LOGTAG, "JSON message must have a type property");
+ return true; // It may seem odd to return true here, but it's necessary to preserve the correct behavior.
+ }
+
+ final List<NativeEventListener> listeners = getNativeListeners(type);
+
+ final String guid = message.optString(GUID, null);
+ EventCallback callback = null;
+ if (guid != null) {
+ callback = new GeckoEventCallback(guid, type);
+ }
+
+ if (listeners != null) {
+ if (listeners.isEmpty()) {
+ Log.w(LOGTAG, "No listeners for " + type);
+
+ // There were native listeners, and they're gone. Return a failure rather than
+ // looking for JSON listeners. This is an optimization, as we can safely assume
+ // that an event which previously had native listeners will never have JSON
+ // listeners.
+ return false;
+ }
+ try {
+ for (final NativeEventListener listener : listeners) {
+ listener.handleMessage(type, message, callback);
+ }
+ } catch (final NativeJSObject.InvalidPropertyException e) {
+ Log.e(LOGTAG, "Exception occurred while handling " + type, e);
+ }
+ // If we found native listeners, we assume we don't have any other types of listeners
+ // and return early. This assumption is checked when registering listeners.
+ return true;
+ }
+
+ // Check for thread event listeners before checking for JSON event listeners,
+ // because checking for thread listeners is very fast and doesn't require us to
+ // serialize into JSON and construct a JSONObject.
+ if (dispatchToThreads(type, message, /* bundle */ null, callback)) {
+ // If we found thread listeners, we assume we don't have any other types of listeners
+ // and return early. This assumption is checked when registering listeners.
+ return true;
+ }
+
+ try {
+ // If we didn't find native listeners, try JSON listeners.
+ return dispatchEvent(new JSONObject(message.toString()), callback);
+ } catch (final JSONException e) {
+ Log.e(LOGTAG, "Cannot parse JSON", e);
+ } catch (final UnsupportedOperationException e) {
+ Log.e(LOGTAG, "Cannot convert message to JSON", e);
+ }
+
+ return true;
+ }
+
+ /**
+ * Dispatch event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * @param message Bundle message with "type" value specifying the event type.
+ */
+ public void dispatch(final Bundle message) {
+ dispatch(message, /* callback */ null);
+ }
+
+ /**
+ * Dispatch event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * @param message Bundle message with "type" value specifying the event type.
+ * @param callback Optional object for callbacks from events.
+ */
+ public void dispatch(final Bundle message, final EventCallback callback) {
+ if (message == null) {
+ throw new IllegalArgumentException("Null message");
+ }
+
+ final String type = message.getCharSequence("type").toString();
+ if (type == null) {
+ Log.e(LOGTAG, "Bundle message must have a type property");
+ return;
+ }
+ dispatchToThreads(type, /* js */ null, message, /* callback */ callback);
+ }
+
+ /**
+ * Dispatch event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * @param type Event type
+ * @param message Bundle message
+ */
+ public void dispatch(final String type, final Bundle message) {
+ dispatch(type, message, /* callback */ null);
+ }
+
+ /**
+ * Dispatch event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * @param type Event type
+ * @param message Bundle message
+ * @param callback Optional object for callbacks from events.
+ */
+ public void dispatch(final String type, final Bundle message, final EventCallback callback) {
+ dispatchToThreads(type, /* js */ null, message, /* callback */ callback);
+ }
+
+ private boolean dispatchToThreads(final String type,
+ final NativeJSObject jsMessage,
+ final Bundle bundleMessage,
+ final EventCallback callback) {
+ if (dispatchToThread(type, jsMessage, bundleMessage, callback,
+ mUiThreadListeners, ThreadUtils.getUiHandler())) {
+ return true;
+ }
+
+ if (dispatchToThread(type, jsMessage, bundleMessage, callback,
+ mBackgroundThreadListeners, ThreadUtils.getBackgroundHandler())) {
+ return true;
+ }
+
+ if (jsMessage == null) {
+ Log.w(LOGTAG, "No listeners for " + type + " in dispatchToThreads");
+ }
+
+ if (!AppConstants.RELEASE_OR_BETA && jsMessage == null) {
+ // We're dispatching a Bundle message. Because Gecko thread listeners are not
+ // supported for Bundle messages, do a sanity check to make sure we don't have
+ // matching Gecko thread listeners.
+ boolean hasGeckoListener = false;
+ synchronized (mGeckoThreadNativeListeners) {
+ hasGeckoListener |= mGeckoThreadNativeListeners.containsKey(type);
+ }
+ synchronized (mGeckoThreadJSONListeners) {
+ hasGeckoListener |= mGeckoThreadJSONListeners.containsKey(type);
+ }
+ if (hasGeckoListener) {
+ throw new IllegalStateException(
+ "Dispatching Bundle message to Gecko listener " + type);
+ }
+ }
+
+ return false;
+ }
+
+ private boolean dispatchToThread(final String type,
+ final NativeJSObject jsMessage,
+ final Bundle bundleMessage,
+ final EventCallback callback,
+ final Map<String, List<BundleEventListener>> listenersMap,
+ final Handler thread) {
+ // We need to hold the lock throughout dispatching, to ensure the listeners list
+ // is consistent, while we iterate over it. We don't have to worry about listeners
+ // running for a long time while we have the lock, because the listeners will run
+ // on a separate thread.
+ synchronized (listenersMap) {
+ final List<BundleEventListener> listeners = listenersMap.get(type);
+ if (listeners == null) {
+ return false;
+ }
+
+ if (listeners.isEmpty()) {
+ Log.w(LOGTAG, "No listeners for " + type + " in dispatchToThread");
+
+ // There were native listeners, and they're gone.
+ return false;
+ }
+
+ final Bundle messageAsBundle;
+ try {
+ messageAsBundle = jsMessage != null ? jsMessage.toBundle() : bundleMessage;
+ } catch (final NativeJSObject.InvalidPropertyException e) {
+ Log.e(LOGTAG, "Exception occurred while handling " + type, e);
+ return true;
+ }
+
+ // Event listeners will call | callback.sendError | if applicable.
+ for (final BundleEventListener listener : listeners) {
+ thread.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.handleMessage(type, messageAsBundle, callback);
+ }
+ });
+ }
+ return true;
+ }
+ }
+
+ public boolean dispatchEvent(final JSONObject message, final EventCallback callback) {
+ // {
+ // "type": "value",
+ // "event_specific": "value",
+ // ...
+ try {
+ final String type = message.getString("type");
+
+ final List<GeckoEventListener> listeners = getGeckoListeners(type);
+
+ if (listeners == null || listeners.isEmpty()) {
+ Log.w(LOGTAG, "No listeners for " + type + " in dispatchEvent");
+
+ return false;
+ }
+
+ for (final GeckoEventListener listener : listeners) {
+ listener.handleMessage(type, message);
+ }
+ } catch (final JSONException e) {
+ Log.e(LOGTAG, "handleGeckoMessage throws " + e, e);
+ }
+
+ return true;
+ }
+
+ @RobocopTarget
+ @Deprecated
+ public static void sendResponse(JSONObject message, Object response) {
+ sendResponseHelper(STATUS_SUCCESS, message, response);
+ }
+
+ @Deprecated
+ public static void sendError(JSONObject message, Object response) {
+ sendResponseHelper(STATUS_ERROR, message, response);
+ }
+
+ @Deprecated
+ private static void sendResponseHelper(String status, JSONObject message, Object response) {
+ try {
+ final String topic = message.getString("type") + ":Response";
+ final JSONObject wrapper = new JSONObject();
+ wrapper.put(GUID, message.getString(GUID));
+ wrapper.put("status", status);
+ wrapper.put("response", response);
+
+ if (ThreadUtils.isOnGeckoThread()) {
+ GeckoAppShell.syncNotifyObservers(topic, wrapper.toString());
+ } else {
+ GeckoAppShell.notifyObservers(topic, wrapper.toString(),
+ GeckoThread.State.PROFILE_READY);
+ }
+ } catch (final JSONException e) {
+ Log.e(LOGTAG, "Unable to send response", e);
+ }
+ }
+
+ /* package */ static class GeckoEventCallback implements EventCallback {
+ private final String guid;
+ private final String type;
+ private boolean sent;
+
+ public GeckoEventCallback(final String guid, final String type) {
+ this.guid = guid;
+ this.type = type;
+ }
+
+ @Override
+ public void sendSuccess(final Object response) {
+ sendResponse(STATUS_SUCCESS, response);
+ }
+
+ @Override
+ public void sendError(final Object response) {
+ sendResponse(STATUS_ERROR, response);
+ }
+
+ private void sendResponse(final String status, final Object response) {
+ if (sent) {
+ throw new IllegalStateException("Callback has already been executed for type=" +
+ type + ", guid=" + guid);
+ }
+
+ sent = true;
+
+ try {
+ final String topic = type + ":Response";
+ final JSONObject wrapper = new JSONObject();
+ wrapper.put(GUID, guid);
+ wrapper.put("status", status);
+ wrapper.put("response", response);
+
+ if (ThreadUtils.isOnGeckoThread()) {
+ GeckoAppShell.syncNotifyObservers(topic, wrapper.toString());
+ } else {
+ GeckoAppShell.notifyObservers(topic, wrapper.toString(),
+ GeckoThread.State.PROFILE_READY);
+ }
+ } catch (final JSONException e) {
+ Log.e(LOGTAG, "Unable to send response for: " + type, e);
+ }
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAccessibility.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAccessibility.java
new file mode 100644
index 0000000000..8d4c0fb2a1
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAccessibility.java
@@ -0,0 +1,410 @@
+/* -*- 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.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.UIAsyncTask;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
+
+import com.googlecode.eyesfree.braille.selfbraille.SelfBrailleClient;
+import com.googlecode.eyesfree.braille.selfbraille.WriteData;
+
+public class GeckoAccessibility {
+ private static final String LOGTAG = "GeckoAccessibility";
+ private static final int VIRTUAL_ENTRY_POINT_BEFORE = 1;
+ private static final int VIRTUAL_CURSOR_POSITION = 2;
+ private static final int VIRTUAL_ENTRY_POINT_AFTER = 3;
+
+ private static boolean sEnabled;
+ // Used to store the JSON message and populate the event later in the code path.
+ private static JSONObject sHoverEnter;
+ private static AccessibilityNodeInfo sVirtualCursorNode;
+ private static int sCurrentNode;
+
+ // This is the number Brailleback uses to start indexing routing keys.
+ private static final int BRAILLE_CLICK_BASE_INDEX = -275000000;
+ private static SelfBrailleClient sSelfBrailleClient;
+
+ public static void updateAccessibilitySettings (final Context context) {
+ new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
+ @Override
+ public Void doInBackground() {
+ JSONObject ret = new JSONObject();
+ sEnabled = false;
+ AccessibilityManager accessibilityManager =
+ (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ sEnabled = accessibilityManager.isEnabled() && accessibilityManager.isTouchExplorationEnabled();
+ if (Versions.feature16Plus && sEnabled && sSelfBrailleClient == null) {
+ sSelfBrailleClient = new SelfBrailleClient(context, false);
+ }
+
+ try {
+ ret.put("enabled", sEnabled);
+ } catch (Exception ex) {
+ Log.e(LOGTAG, "Error building JSON arguments for Accessibility:Settings:", ex);
+ }
+
+ GeckoAppShell.notifyObservers("Accessibility:Settings", ret.toString());
+ return null;
+ }
+
+ @Override
+ public void onPostExecute(Void args) {
+ final GeckoAppShell.GeckoInterface geckoInterface = GeckoAppShell.getGeckoInterface();
+ if (geckoInterface == null) {
+ return;
+ }
+ geckoInterface.setAccessibilityEnabled(sEnabled);
+ }
+ }.execute();
+ }
+
+ private static void populateEventFromJSON (AccessibilityEvent event, JSONObject message) {
+ final JSONArray textArray = message.optJSONArray("text");
+ if (textArray != null) {
+ for (int i = 0; i < textArray.length(); i++)
+ event.getText().add(textArray.optString(i));
+ }
+
+ event.setContentDescription(message.optString("description"));
+ event.setEnabled(message.optBoolean("enabled", true));
+ event.setChecked(message.optBoolean("checked"));
+ event.setPassword(message.optBoolean("password"));
+ event.setAddedCount(message.optInt("addedCount", -1));
+ event.setRemovedCount(message.optInt("removedCount", -1));
+ event.setFromIndex(message.optInt("fromIndex", -1));
+ event.setItemCount(message.optInt("itemCount", -1));
+ event.setCurrentItemIndex(message.optInt("currentItemIndex", -1));
+ event.setBeforeText(message.optString("beforeText"));
+ event.setToIndex(message.optInt("toIndex", -1));
+ event.setScrollable(message.optBoolean("scrollable"));
+ event.setScrollX(message.optInt("scrollX", -1));
+ event.setScrollY(message.optInt("scrollY", -1));
+ event.setMaxScrollX(message.optInt("maxScrollX", -1));
+ event.setMaxScrollY(message.optInt("maxScrollY", -1));
+ }
+
+ private static void sendDirectAccessibilityEvent(int eventType, JSONObject message) {
+ final Context context = GeckoAppShell.getApplicationContext();
+ final AccessibilityEvent accEvent = AccessibilityEvent.obtain(eventType);
+ accEvent.setClassName(GeckoAccessibility.class.getName());
+ accEvent.setPackageName(context.getPackageName());
+ populateEventFromJSON(accEvent, message);
+ AccessibilityManager accessibilityManager =
+ (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ try {
+ accessibilityManager.sendAccessibilityEvent(accEvent);
+ } catch (IllegalStateException e) {
+ // Accessibility is off.
+ }
+ }
+
+ public static boolean isEnabled() {
+ return sEnabled;
+ }
+
+ public static void sendAccessibilityEvent (final JSONObject message) {
+ if (!sEnabled)
+ return;
+
+ final int eventType = message.optInt("eventType", -1);
+ if (eventType < 0) {
+ Log.e(LOGTAG, "No accessibility event type provided");
+ return;
+ }
+
+ sendAccessibilityEvent(message, eventType);
+ }
+
+ public static void sendAccessibilityEvent (final JSONObject message, final int eventType) {
+ if (!sEnabled)
+ return;
+
+ final String exitView = message.optString("exitView");
+ if (exitView.equals("moveNext")) {
+ sCurrentNode = VIRTUAL_ENTRY_POINT_AFTER;
+ } else if (exitView.equals("movePrevious")) {
+ sCurrentNode = VIRTUAL_ENTRY_POINT_BEFORE;
+ } else {
+ sCurrentNode = VIRTUAL_CURSOR_POSITION;
+ }
+
+ if (Versions.preJB) {
+ // Before Jelly Bean we send events directly from here while spoofing the source by setting
+ // the package and class name manually.
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ public void run() {
+ sendDirectAccessibilityEvent(eventType, message);
+ }
+ });
+ } else {
+ // In Jelly Bean we populate an AccessibilityNodeInfo with the minimal amount of data to have
+ // it work with TalkBack.
+ final View view = GeckoAppShell.getLayerView();
+ if (view == null)
+ return;
+
+ if (sVirtualCursorNode == null)
+ sVirtualCursorNode = AccessibilityNodeInfo.obtain(view, VIRTUAL_CURSOR_POSITION);
+ sVirtualCursorNode.setEnabled(message.optBoolean("enabled", true));
+ sVirtualCursorNode.setClickable(message.optBoolean("clickable"));
+ sVirtualCursorNode.setCheckable(message.optBoolean("checkable"));
+ sVirtualCursorNode.setChecked(message.optBoolean("checked"));
+ sVirtualCursorNode.setPassword(message.optBoolean("password"));
+
+ final JSONArray textArray = message.optJSONArray("text");
+ StringBuilder sb = new StringBuilder();
+ if (textArray != null && textArray.length() > 0) {
+ sb.append(textArray.optString(0));
+ for (int i = 1; i < textArray.length(); i++) {
+ sb.append(" ").append(textArray.optString(i));
+ }
+ sVirtualCursorNode.setText(sb.toString());
+ }
+ sVirtualCursorNode.setContentDescription(message.optString("description"));
+
+ JSONObject bounds = message.optJSONObject("bounds");
+ if (bounds != null) {
+ Rect relativeBounds = new Rect(bounds.optInt("left"), bounds.optInt("top"),
+ bounds.optInt("right"), bounds.optInt("bottom"));
+ sVirtualCursorNode.setBoundsInParent(relativeBounds);
+ int[] locationOnScreen = new int[2];
+ view.getLocationOnScreen(locationOnScreen);
+ Rect screenBounds = new Rect(relativeBounds);
+ screenBounds.offset(locationOnScreen[0], locationOnScreen[1]);
+ sVirtualCursorNode.setBoundsInScreen(screenBounds);
+ }
+
+ final JSONObject braille = message.optJSONObject("brailleOutput");
+ if (braille != null) {
+ sendBrailleText(view, braille.optString("text"),
+ braille.optInt("selectionStart"), braille.optInt("selectionEnd"));
+ }
+
+ if (eventType == AccessibilityEvent.TYPE_VIEW_HOVER_ENTER) {
+ sHoverEnter = message;
+ }
+
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
+ event.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
+ event.setClassName(GeckoAccessibility.class.getName());
+ if (eventType == AccessibilityEvent.TYPE_ANNOUNCEMENT ||
+ eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
+ event.setSource(view, View.NO_ID);
+ } else {
+ event.setSource(view, VIRTUAL_CURSOR_POSITION);
+ }
+ populateEventFromJSON(event, message);
+ ((ViewParent) view).requestSendAccessibilityEvent(view, event);
+ }
+ });
+
+ }
+ }
+
+ private static void sendBrailleText(final View view, final String text, final int selectionStart, final int selectionEnd) {
+ AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(view, VIRTUAL_CURSOR_POSITION);
+ WriteData data = WriteData.forInfo(info);
+ data.setText(text);
+ // Set either the focus blink or the current caret position/selection
+ data.setSelectionStart(selectionStart);
+ data.setSelectionEnd(selectionEnd);
+ sSelfBrailleClient.write(data);
+ }
+
+ public static void setDelegate(View view) {
+ // Only use this delegate in Jelly Bean.
+ if (Versions.feature16Plus) {
+ view.setAccessibilityDelegate(new GeckoAccessibilityDelegate());
+ view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+ }
+
+ public static void setAccessibilityManagerListeners(final Context context) {
+ AccessibilityManager accessibilityManager =
+ (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+
+ accessibilityManager.addAccessibilityStateChangeListener(new AccessibilityManager.AccessibilityStateChangeListener() {
+ @Override
+ public void onAccessibilityStateChanged(boolean enabled) {
+ updateAccessibilitySettings(context);
+ }
+ });
+
+ if (Versions.feature19Plus) {
+ accessibilityManager.addTouchExplorationStateChangeListener(new AccessibilityManager.TouchExplorationStateChangeListener() {
+ @Override
+ public void onTouchExplorationStateChanged(boolean enabled) {
+ updateAccessibilitySettings(context);
+ }
+ });
+ }
+ }
+
+ public static void onLayerViewFocusChanged(boolean gainFocus) {
+ if (sEnabled)
+ GeckoAppShell.notifyObservers("Accessibility:Focus", gainFocus ? "true" : "false");
+ }
+
+ public static class GeckoAccessibilityDelegate extends View.AccessibilityDelegate {
+ AccessibilityNodeProvider mAccessibilityNodeProvider;
+
+ @Override
+ public AccessibilityNodeProvider getAccessibilityNodeProvider(final View host) {
+ if (mAccessibilityNodeProvider == null)
+ // The accessibility node structure for web content consists of 3 LayerView child nodes:
+ // 1. VIRTUAL_ENTRY_POINT_BEFORE: Represents the entry point before the LayerView.
+ // 2. VIRTUAL_CURSOR_POSITION: Represents the current position of the virtual cursor.
+ // 3. VIRTUAL_ENTRY_POINT_AFTER: Represents the entry point after the LayerView.
+ mAccessibilityNodeProvider = new AccessibilityNodeProvider() {
+ @Override
+ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualDescendantId) {
+ AccessibilityNodeInfo info = (virtualDescendantId == VIRTUAL_CURSOR_POSITION && sVirtualCursorNode != null) ?
+ AccessibilityNodeInfo.obtain(sVirtualCursorNode) :
+ AccessibilityNodeInfo.obtain(host, virtualDescendantId);
+
+ switch (virtualDescendantId) {
+ case View.NO_ID:
+ // This is the parent LayerView node, populate it with children.
+ onInitializeAccessibilityNodeInfo(host, info);
+ info.addChild(host, VIRTUAL_ENTRY_POINT_BEFORE);
+ info.addChild(host, VIRTUAL_CURSOR_POSITION);
+ info.addChild(host, VIRTUAL_ENTRY_POINT_AFTER);
+ break;
+ default:
+ info.setParent(host);
+ info.setSource(host, virtualDescendantId);
+ info.setVisibleToUser(host.isShown());
+ info.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
+ info.setClassName(host.getClass().getName());
+ info.setEnabled(true);
+ info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
+ info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
+ info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
+ info.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
+ info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
+ info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
+ info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
+ info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+ info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
+ info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT);
+ info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER |
+ AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD |
+ AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE |
+ AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH);
+ break;
+ }
+ return info;
+ }
+
+ @Override
+ public boolean performAction (int virtualViewId, int action, Bundle arguments) {
+ if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) {
+ // The accessibility focus is permanently on the middle node, VIRTUAL_CURSOR_POSITION.
+ // When we enter the view forward or backward we just ask Gecko to get focus, keeping the current position.
+ if (virtualViewId == VIRTUAL_CURSOR_POSITION && sHoverEnter != null) {
+ GeckoAccessibility.sendAccessibilityEvent(sHoverEnter, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
+ } else {
+ GeckoAppShell.notifyObservers("Accessibility:Focus", "true");
+ }
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_CLICK && virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ GeckoAppShell.notifyObservers("Accessibility:ActivateObject", null);
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_LONG_CLICK && virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ GeckoAppShell.notifyObservers("Accessibility:LongPress", null);
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD && virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ GeckoAppShell.notifyObservers("Accessibility:ScrollForward", null);
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD && virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ GeckoAppShell.notifyObservers("Accessibility:ScrollBackward", null);
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT && virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ String traversalRule = "";
+ if (arguments != null) {
+ traversalRule = arguments.getString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING);
+ }
+ GeckoAppShell.notifyObservers("Accessibility:NextObject", traversalRule);
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT && virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ String traversalRule = "";
+ if (arguments != null) {
+ traversalRule = arguments.getString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING);
+ }
+ GeckoAppShell.notifyObservers("Accessibility:PreviousObject", traversalRule);
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY &&
+ virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ // XXX: Self brailling gives this action with a bogus argument instead of an actual click action;
+ // the argument value is the BRAILLE_CLICK_BASE_INDEX - the index of the routing key that was hit.
+ // Other negative values are used by ChromeVox, but we don't support them.
+ // FAKE_GRANULARITY_READ_CURRENT = -1
+ // FAKE_GRANULARITY_READ_TITLE = -2
+ // FAKE_GRANULARITY_STOP_SPEECH = -3
+ // FAKE_GRANULARITY_CHANGE_SHIFTER = -4
+ int granularity = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
+ if (granularity <= BRAILLE_CLICK_BASE_INDEX) {
+ int keyIndex = BRAILLE_CLICK_BASE_INDEX - granularity;
+ JSONObject activationData = new JSONObject();
+ try {
+ activationData.put("keyIndex", keyIndex);
+ } catch (JSONException e) {
+ return true;
+ }
+ GeckoAppShell.notifyObservers("Accessibility:ActivateObject", activationData.toString());
+ } else if (granularity > 0) {
+ JSONObject movementData = new JSONObject();
+ try {
+ movementData.put("direction", "Next");
+ movementData.put("granularity", granularity);
+ } catch (JSONException e) {
+ return true;
+ }
+ GeckoAppShell.notifyObservers("Accessibility:MoveByGranularity", movementData.toString());
+ }
+ return true;
+ } else if (action == AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY &&
+ virtualViewId == VIRTUAL_CURSOR_POSITION) {
+ JSONObject movementData = new JSONObject();
+ int granularity = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
+ try {
+ movementData.put("direction", "Previous");
+ movementData.put("granularity", granularity);
+ } catch (JSONException e) {
+ return true;
+ }
+ if (granularity > 0) {
+ GeckoAppShell.notifyObservers("Accessibility:MoveByGranularity", movementData.toString());
+ }
+ return true;
+ }
+ return host.performAccessibilityAction(action, arguments);
+ }
+ };
+
+ return mAccessibilityNodeProvider;
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java
new file mode 100644
index 0000000000..a802126395
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java
@@ -0,0 +1,2239 @@
+/* -*- 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.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.net.MalformedURLException;
+import java.net.Proxy;
+import java.net.URLConnection;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.TreeMap;
+import java.util.concurrent.ConcurrentHashMap;
+
+import android.annotation.SuppressLint;
+import org.mozilla.gecko.annotation.JNITarget;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.gfx.BitmapUtils;
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.gfx.PanZoomController;
+import org.mozilla.gecko.permissions.Permissions;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoRequest;
+import org.mozilla.gecko.util.HardwareCodecCapabilityUtils;
+import org.mozilla.gecko.util.HardwareUtils;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSContainer;
+import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.gecko.util.ProxySelector;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.pm.Signature;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.ImageFormat;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.SurfaceTexture;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.hardware.Camera;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.location.Criteria;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.Display;
+import android.view.HapticFeedbackConstants;
+import android.view.Surface;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+import android.webkit.MimeTypeMap;
+import android.widget.AbsoluteLayout;
+
+public class GeckoAppShell
+{
+ private static final String LOGTAG = "GeckoAppShell";
+
+ // We have static members only.
+ private GeckoAppShell() { }
+
+ private static final CrashHandler CRASH_HANDLER = new CrashHandler() {
+ @Override
+ protected String getAppPackageName() {
+ return AppConstants.ANDROID_PACKAGE_NAME;
+ }
+
+ @Override
+ protected Context getAppContext() {
+ return sContextGetter != null ? getApplicationContext() : null;
+ }
+
+ @Override
+ protected Bundle getCrashExtras(final Thread thread, final Throwable exc) {
+ final Bundle extras = super.getCrashExtras(thread, exc);
+
+ extras.putString("ProductName", AppConstants.MOZ_APP_BASENAME);
+ extras.putString("ProductID", AppConstants.MOZ_APP_ID);
+ extras.putString("Version", AppConstants.MOZ_APP_VERSION);
+ extras.putString("BuildID", AppConstants.MOZ_APP_BUILDID);
+ extras.putString("Vendor", AppConstants.MOZ_APP_VENDOR);
+ extras.putString("ReleaseChannel", AppConstants.MOZ_UPDATE_CHANNEL);
+ return extras;
+ }
+
+ @Override
+ public void uncaughtException(final Thread thread, final Throwable exc) {
+ if (GeckoThread.isState(GeckoThread.State.EXITING) ||
+ GeckoThread.isState(GeckoThread.State.EXITED)) {
+ // We've called System.exit. All exceptions after this point are Android
+ // berating us for being nasty to it.
+ return;
+ }
+
+ super.uncaughtException(thread, exc);
+ }
+
+ @Override
+ public boolean reportException(final Thread thread, final Throwable exc) {
+ try {
+ if (exc instanceof OutOfMemoryError) {
+ SharedPreferences prefs = getSharedPreferences();
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(PREFS_OOM_EXCEPTION, true);
+
+ // Synchronously write to disk so we know it's done before we
+ // shutdown
+ editor.commit();
+ }
+
+ reportJavaCrash(exc, getExceptionStackTrace(exc));
+
+ } catch (final Throwable e) {
+ }
+
+ // reportJavaCrash should have caused us to hard crash. If we're still here,
+ // it probably means Gecko is not loaded, and we should do something else.
+ if (AppConstants.MOZ_CRASHREPORTER && AppConstants.MOZILLA_OFFICIAL) {
+ // Only use Java crash reporter if enabled on official build.
+ return super.reportException(thread, exc);
+ }
+ return false;
+ }
+ };
+
+ public static CrashHandler ensureCrashHandling() {
+ // Crash handling is automatically enabled when GeckoAppShell is loaded.
+ return CRASH_HANDLER;
+ }
+
+ private static volatile boolean locationHighAccuracyEnabled;
+
+ // See also HardwareUtils.LOW_MEMORY_THRESHOLD_MB.
+ private static final int HIGH_MEMORY_DEVICE_THRESHOLD_MB = 768;
+
+ static private int sDensityDpi;
+ static private int sScreenDepth;
+
+ /* Is the value in sVibrationEndTime valid? */
+ private static boolean sVibrationMaybePlaying;
+
+ /* Time (in System.nanoTime() units) when the currently-playing vibration
+ * is scheduled to end. This value is valid only when
+ * sVibrationMaybePlaying is true. */
+ private static long sVibrationEndTime;
+
+ private static Sensor gAccelerometerSensor;
+ private static Sensor gLinearAccelerometerSensor;
+ private static Sensor gGyroscopeSensor;
+ private static Sensor gOrientationSensor;
+ private static Sensor gProximitySensor;
+ private static Sensor gLightSensor;
+ private static Sensor gRotationVectorSensor;
+ private static Sensor gGameRotationVectorSensor;
+
+ private static final String GECKOREQUEST_RESPONSE_KEY = "response";
+ private static final String GECKOREQUEST_ERROR_KEY = "error";
+
+ /*
+ * Keep in sync with constants found here:
+ * http://dxr.mozilla.org/mozilla-central/source/uriloader/base/nsIWebProgressListener.idl
+ */
+ static public final int WPL_STATE_START = 0x00000001;
+ static public final int WPL_STATE_STOP = 0x00000010;
+ static public final int WPL_STATE_IS_DOCUMENT = 0x00020000;
+ static public final int WPL_STATE_IS_NETWORK = 0x00040000;
+
+ /* Keep in sync with constants found here:
+ http://dxr.mozilla.org/mozilla-central/source/netwerk/base/nsINetworkLinkService.idl
+ */
+ static public final int LINK_TYPE_UNKNOWN = 0;
+ static public final int LINK_TYPE_ETHERNET = 1;
+ static public final int LINK_TYPE_USB = 2;
+ static public final int LINK_TYPE_WIFI = 3;
+ static public final int LINK_TYPE_WIMAX = 4;
+ static public final int LINK_TYPE_2G = 5;
+ static public final int LINK_TYPE_3G = 6;
+ static public final int LINK_TYPE_4G = 7;
+
+ public static final String PREFS_OOM_EXCEPTION = "OOMException";
+
+ /* The Android-side API: API methods that Android calls */
+
+ // helper methods
+ @WrapForJNI
+ /* package */ static native void reportJavaCrash(Throwable exc, String stackTrace);
+
+ @WrapForJNI(dispatchTo = "gecko")
+ public static native void notifyUriVisited(String uri);
+
+ private static LayerView sLayerView;
+ private static Rect sScreenSize;
+
+ public static void setLayerView(LayerView lv) {
+ if (sLayerView == lv) {
+ return;
+ }
+ sLayerView = lv;
+ }
+
+ @RobocopTarget
+ public static LayerView getLayerView() {
+ return sLayerView;
+ }
+
+ /**
+ * Sends an asynchronous request to Gecko.
+ *
+ * The response data will be passed to {@link GeckoRequest#onResponse(NativeJSObject)} if the
+ * request succeeds; otherwise, {@link GeckoRequest#onError()} will fire.
+ *
+ * It can be called from any thread. The GeckoRequest callbacks will be executed on the Gecko thread.
+ *
+ * @param request The request to dispatch. Cannot be null.
+ */
+ @RobocopTarget
+ public static void sendRequestToGecko(final GeckoRequest request) {
+ final String responseMessage = "Gecko:Request" + request.getId();
+
+ EventDispatcher.getInstance().registerGeckoThreadListener(new NativeEventListener() {
+ @Override
+ public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
+ EventDispatcher.getInstance().unregisterGeckoThreadListener(this, event);
+ if (!message.has(GECKOREQUEST_RESPONSE_KEY)) {
+ request.onError(message.getObject(GECKOREQUEST_ERROR_KEY));
+ return;
+ }
+ request.onResponse(message.getObject(GECKOREQUEST_RESPONSE_KEY));
+ }
+ }, responseMessage);
+
+ notifyObservers(request.getName(), request.getData());
+ }
+
+ // Synchronously notify a Gecko observer; must be called from Gecko thread.
+ @WrapForJNI(calledFrom = "gecko")
+ public static native void syncNotifyObservers(String topic, String data);
+
+ @WrapForJNI(stubName = "NotifyObservers", dispatchTo = "gecko")
+ private static native void nativeNotifyObservers(String topic, String data);
+
+ @RobocopTarget
+ public static void notifyObservers(final String topic, final String data) {
+ notifyObservers(topic, data, GeckoThread.State.RUNNING);
+ }
+
+ public static void notifyObservers(final String topic, final String data, final GeckoThread.State state) {
+ if (GeckoThread.isStateAtLeast(state)) {
+ nativeNotifyObservers(topic, data);
+ } else {
+ GeckoThread.queueNativeCallUntil(
+ state, GeckoAppShell.class, "nativeNotifyObservers",
+ String.class, topic, String.class, data);
+ }
+ }
+
+ /*
+ * The Gecko-side API: API methods that Gecko calls
+ */
+
+ @WrapForJNI(exceptionMode = "ignore")
+ private static String getExceptionStackTrace(Throwable e) {
+ return CrashHandler.getExceptionStackTrace(CrashHandler.getRootException(e));
+ }
+
+ @WrapForJNI(exceptionMode = "ignore")
+ private static void handleUncaughtException(Throwable e) {
+ CRASH_HANDLER.uncaughtException(null, e);
+ }
+
+ @WrapForJNI
+ public static void openWindowForNotification() {
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+ intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
+
+ getApplicationContext().startActivity(intent);
+ }
+
+ private static float getLocationAccuracy(Location location) {
+ float radius = location.getAccuracy();
+ return (location.hasAccuracy() && radius > 0) ? radius : 1001;
+ }
+
+ @SuppressLint("MissingPermission") // Permissions are explicitly checked for in enableLocation()
+ private static Location getLastKnownLocation(LocationManager lm) {
+ Location lastKnownLocation = null;
+ List<String> providers = lm.getAllProviders();
+
+ for (String provider : providers) {
+ Location location = lm.getLastKnownLocation(provider);
+ if (location == null) {
+ continue;
+ }
+
+ if (lastKnownLocation == null) {
+ lastKnownLocation = location;
+ continue;
+ }
+
+ long timeDiff = location.getTime() - lastKnownLocation.getTime();
+ if (timeDiff > 0 ||
+ (timeDiff == 0 &&
+ getLocationAccuracy(location) < getLocationAccuracy(lastKnownLocation))) {
+ lastKnownLocation = location;
+ }
+ }
+
+ return lastKnownLocation;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ @SuppressLint("MissingPermission") // Permissions are explicitly checked for within this method
+ private static void enableLocation(final boolean enable) {
+ final Runnable requestLocation = new Runnable() {
+ @Override
+ public void run() {
+ LocationManager lm = getLocationManager(getApplicationContext());
+ if (lm == null) {
+ return;
+ }
+
+ if (!enable) {
+ lm.removeUpdates(getLocationListener());
+ return;
+ }
+
+ Location lastKnownLocation = getLastKnownLocation(lm);
+ if (lastKnownLocation != null) {
+ getLocationListener().onLocationChanged(lastKnownLocation);
+ }
+
+ Criteria criteria = new Criteria();
+ criteria.setSpeedRequired(false);
+ criteria.setBearingRequired(false);
+ criteria.setAltitudeRequired(false);
+ if (locationHighAccuracyEnabled) {
+ criteria.setAccuracy(Criteria.ACCURACY_FINE);
+ criteria.setCostAllowed(true);
+ criteria.setPowerRequirement(Criteria.POWER_HIGH);
+ } else {
+ criteria.setAccuracy(Criteria.ACCURACY_COARSE);
+ criteria.setCostAllowed(false);
+ criteria.setPowerRequirement(Criteria.POWER_LOW);
+ }
+
+ String provider = lm.getBestProvider(criteria, true);
+ if (provider == null)
+ return;
+
+ Looper l = Looper.getMainLooper();
+ lm.requestLocationUpdates(provider, 100, 0.5f, getLocationListener(), l);
+ }
+ };
+
+ Permissions
+ .from((Activity) getContext())
+ .withPermissions(Manifest.permission.ACCESS_FINE_LOCATION)
+ .onUIThread()
+ .doNotPromptIf(!enable)
+ .run(requestLocation);
+ }
+
+ private static LocationManager getLocationManager(Context context) {
+ try {
+ return (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
+ } catch (NoSuchFieldError e) {
+ // Some Tegras throw exceptions about missing the CONTROL_LOCATION_UPDATES permission,
+ // which allows enabling/disabling location update notifications from the cell radio.
+ // CONTROL_LOCATION_UPDATES is not for use by normal applications, but we might be
+ // hitting this problem if the Tegras are confused about missing cell radios.
+ Log.e(LOGTAG, "LOCATION_SERVICE not found?!", e);
+ return null;
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void enableLocationHighAccuracy(final boolean enable) {
+ locationHighAccuracyEnabled = enable;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean setAlarm(int aSeconds, int aNanoSeconds) {
+ AlarmManager am = (AlarmManager)
+ getApplicationContext().getSystemService(Context.ALARM_SERVICE);
+
+ Intent intent = new Intent(getApplicationContext(), AlarmReceiver.class);
+ PendingIntent pi = PendingIntent.getBroadcast(
+ getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ // AlarmManager only supports millisecond precision
+ long time = ((long) aSeconds * 1000) + ((long) aNanoSeconds / 1_000_000L);
+ am.setExact(AlarmManager.RTC_WAKEUP, time, pi);
+
+ return true;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void disableAlarm() {
+ AlarmManager am = (AlarmManager)
+ getApplicationContext().getSystemService(Context.ALARM_SERVICE);
+
+ Intent intent = new Intent(getApplicationContext(), AlarmReceiver.class);
+ PendingIntent pi = PendingIntent.getBroadcast(
+ getApplicationContext(), 0, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ am.cancel(pi);
+ }
+
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+ /* package */ static native void onSensorChanged(int hal_type, float x, float y, float z,
+ float w, int accuracy, long time);
+
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+ /* package */ static native void onLocationChanged(double latitude, double longitude,
+ double altitude, float accuracy,
+ float bearing, float speed, long time);
+
+ private static class DefaultListeners
+ implements SensorEventListener, LocationListener, NotificationListener {
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ }
+
+ private static int HalSensorAccuracyFor(int androidAccuracy) {
+ switch (androidAccuracy) {
+ case SensorManager.SENSOR_STATUS_UNRELIABLE:
+ return GeckoHalDefines.SENSOR_ACCURACY_UNRELIABLE;
+ case SensorManager.SENSOR_STATUS_ACCURACY_LOW:
+ return GeckoHalDefines.SENSOR_ACCURACY_LOW;
+ case SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM:
+ return GeckoHalDefines.SENSOR_ACCURACY_MED;
+ case SensorManager.SENSOR_STATUS_ACCURACY_HIGH:
+ return GeckoHalDefines.SENSOR_ACCURACY_HIGH;
+ }
+ return GeckoHalDefines.SENSOR_ACCURACY_UNKNOWN;
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent s) {
+ int sensor_type = s.sensor.getType();
+ int hal_type = 0;
+ float x = 0.0f, y = 0.0f, z = 0.0f, w = 0.0f;
+ final int accuracy = HalSensorAccuracyFor(s.accuracy);
+ // SensorEvent timestamp is in nanoseconds, Gecko expects microseconds.
+ final long time = s.timestamp / 1000;
+
+ switch (sensor_type) {
+ case Sensor.TYPE_ACCELEROMETER:
+ case Sensor.TYPE_LINEAR_ACCELERATION:
+ case Sensor.TYPE_ORIENTATION:
+ if (sensor_type == Sensor.TYPE_ACCELEROMETER) {
+ hal_type = GeckoHalDefines.SENSOR_ACCELERATION;
+ } else if (sensor_type == Sensor.TYPE_LINEAR_ACCELERATION) {
+ hal_type = GeckoHalDefines.SENSOR_LINEAR_ACCELERATION;
+ } else {
+ hal_type = GeckoHalDefines.SENSOR_ORIENTATION;
+ }
+ x = s.values[0];
+ y = s.values[1];
+ z = s.values[2];
+ break;
+
+ case Sensor.TYPE_GYROSCOPE:
+ hal_type = GeckoHalDefines.SENSOR_GYROSCOPE;
+ x = (float) Math.toDegrees(s.values[0]);
+ y = (float) Math.toDegrees(s.values[1]);
+ z = (float) Math.toDegrees(s.values[2]);
+ break;
+
+ case Sensor.TYPE_PROXIMITY:
+ hal_type = GeckoHalDefines.SENSOR_PROXIMITY;
+ x = s.values[0];
+ z = s.sensor.getMaximumRange();
+ break;
+
+ case Sensor.TYPE_LIGHT:
+ hal_type = GeckoHalDefines.SENSOR_LIGHT;
+ x = s.values[0];
+ break;
+
+ case Sensor.TYPE_ROTATION_VECTOR:
+ case Sensor.TYPE_GAME_ROTATION_VECTOR: // API >= 18
+ hal_type = (sensor_type == Sensor.TYPE_ROTATION_VECTOR ?
+ GeckoHalDefines.SENSOR_ROTATION_VECTOR :
+ GeckoHalDefines.SENSOR_GAME_ROTATION_VECTOR);
+ x = s.values[0];
+ y = s.values[1];
+ z = s.values[2];
+ if (s.values.length >= 4) {
+ w = s.values[3];
+ } else {
+ // s.values[3] was optional in API <= 18, so we need to compute it
+ // The values form a unit quaternion, so we can compute the angle of
+ // rotation purely based on the given 3 values.
+ w = 1.0f - s.values[0] * s.values[0] -
+ s.values[1] * s.values[1] - s.values[2] * s.values[2];
+ w = (w > 0.0f) ? (float) Math.sqrt(w) : 0.0f;
+ }
+ break;
+ }
+
+ GeckoAppShell.onSensorChanged(hal_type, x, y, z, w, accuracy, time);
+ }
+
+ // Geolocation.
+ @Override
+ public void onLocationChanged(Location location) {
+ // No logging here: user-identifying information.
+ GeckoAppShell.onLocationChanged(location.getLatitude(), location.getLongitude(),
+ location.getAltitude(), location.getAccuracy(),
+ location.getBearing(), location.getSpeed(),
+ location.getTime());
+ }
+
+ @Override
+ public void onProviderDisabled(String provider)
+ {
+ }
+
+ @Override
+ public void onProviderEnabled(String provider)
+ {
+ }
+
+ @Override
+ public void onStatusChanged(String provider, int status, Bundle extras)
+ {
+ }
+
+ @Override // NotificationListener
+ public void showNotification(String name, String cookie, String host,
+ String title, String text, String imageUrl) {
+ // Default is to not show the notification, and immediate send close message.
+ GeckoAppShell.onNotificationClose(name, cookie);
+ }
+
+ @Override // NotificationListener
+ public void showPersistentNotification(String name, String cookie, String host,
+ String title, String text, String imageUrl,
+ String data) {
+ // Default is to not show the notification, and immediate send close message.
+ GeckoAppShell.onNotificationClose(name, cookie);
+ }
+
+ @Override // NotificationListener
+ public void closeNotification(String name) {
+ // Do nothing.
+ }
+ }
+
+ private static final DefaultListeners DEFAULT_LISTENERS = new DefaultListeners();
+ private static SensorEventListener sSensorListener = DEFAULT_LISTENERS;
+ private static LocationListener sLocationListener = DEFAULT_LISTENERS;
+ private static NotificationListener sNotificationListener = DEFAULT_LISTENERS;
+
+ public static SensorEventListener getSensorListener() {
+ return sSensorListener;
+ }
+
+ public static void setSensorListener(final SensorEventListener listener) {
+ sSensorListener = (listener != null) ? listener : DEFAULT_LISTENERS;
+ }
+
+ public static LocationListener getLocationListener() {
+ return sLocationListener;
+ }
+
+ public static void setLocationListener(final LocationListener listener) {
+ sLocationListener = (listener != null) ? listener : DEFAULT_LISTENERS;
+ }
+
+ public static NotificationListener getNotificationListener() {
+ return sNotificationListener;
+ }
+
+ public static void setNotificationListener(final NotificationListener listener) {
+ sNotificationListener = (listener != null) ? listener : DEFAULT_LISTENERS;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void enableSensor(int aSensortype) {
+ GeckoInterface gi = getGeckoInterface();
+ if (gi == null) {
+ return;
+ }
+ SensorManager sm = (SensorManager)
+ getApplicationContext().getSystemService(Context.SENSOR_SERVICE);
+
+ switch (aSensortype) {
+ case GeckoHalDefines.SENSOR_GAME_ROTATION_VECTOR:
+ if (gGameRotationVectorSensor == null) {
+ gGameRotationVectorSensor = sm.getDefaultSensor(15);
+ // sm.getDefaultSensor(
+ // Sensor.TYPE_GAME_ROTATION_VECTOR); // API >= 18
+ }
+ if (gGameRotationVectorSensor != null) {
+ sm.registerListener(getSensorListener(),
+ gGameRotationVectorSensor,
+ SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ if (gGameRotationVectorSensor != null) {
+ break;
+ }
+ // Fallthrough
+
+ case GeckoHalDefines.SENSOR_ROTATION_VECTOR:
+ if (gRotationVectorSensor == null) {
+ gRotationVectorSensor = sm.getDefaultSensor(
+ Sensor.TYPE_ROTATION_VECTOR);
+ }
+ if (gRotationVectorSensor != null) {
+ sm.registerListener(getSensorListener(),
+ gRotationVectorSensor,
+ SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ if (gRotationVectorSensor != null) {
+ break;
+ }
+ // Fallthrough
+
+ case GeckoHalDefines.SENSOR_ORIENTATION:
+ if (gOrientationSensor == null) {
+ gOrientationSensor = sm.getDefaultSensor(
+ Sensor.TYPE_ORIENTATION);
+ }
+ if (gOrientationSensor != null) {
+ sm.registerListener(getSensorListener(),
+ gOrientationSensor,
+ SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_ACCELERATION:
+ if (gAccelerometerSensor == null) {
+ gAccelerometerSensor = sm.getDefaultSensor(
+ Sensor.TYPE_ACCELEROMETER);
+ }
+ if (gAccelerometerSensor != null) {
+ sm.registerListener(getSensorListener(),
+ gAccelerometerSensor,
+ SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_PROXIMITY:
+ if (gProximitySensor == null) {
+ gProximitySensor = sm.getDefaultSensor(Sensor.TYPE_PROXIMITY);
+ }
+ if (gProximitySensor != null) {
+ sm.registerListener(getSensorListener(),
+ gProximitySensor,
+ SensorManager.SENSOR_DELAY_NORMAL);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_LIGHT:
+ if (gLightSensor == null) {
+ gLightSensor = sm.getDefaultSensor(Sensor.TYPE_LIGHT);
+ }
+ if (gLightSensor != null) {
+ sm.registerListener(getSensorListener(),
+ gLightSensor,
+ SensorManager.SENSOR_DELAY_NORMAL);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_LINEAR_ACCELERATION:
+ if (gLinearAccelerometerSensor == null) {
+ gLinearAccelerometerSensor = sm.getDefaultSensor(
+ Sensor.TYPE_LINEAR_ACCELERATION);
+ }
+ if (gLinearAccelerometerSensor != null) {
+ sm.registerListener(getSensorListener(),
+ gLinearAccelerometerSensor,
+ SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_GYROSCOPE:
+ if (gGyroscopeSensor == null) {
+ gGyroscopeSensor = sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
+ }
+ if (gGyroscopeSensor != null) {
+ sm.registerListener(getSensorListener(),
+ gGyroscopeSensor,
+ SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ break;
+
+ default:
+ Log.w(LOGTAG, "Error! Can't enable unknown SENSOR type " +
+ aSensortype);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void disableSensor(int aSensortype) {
+ GeckoInterface gi = getGeckoInterface();
+ if (gi == null)
+ return;
+
+ SensorManager sm = (SensorManager)
+ getApplicationContext().getSystemService(Context.SENSOR_SERVICE);
+
+ switch (aSensortype) {
+ case GeckoHalDefines.SENSOR_GAME_ROTATION_VECTOR:
+ if (gGameRotationVectorSensor != null) {
+ sm.unregisterListener(getSensorListener(), gGameRotationVectorSensor);
+ break;
+ }
+ // Fallthrough
+
+ case GeckoHalDefines.SENSOR_ROTATION_VECTOR:
+ if (gRotationVectorSensor != null) {
+ sm.unregisterListener(getSensorListener(), gRotationVectorSensor);
+ break;
+ }
+ // Fallthrough
+
+ case GeckoHalDefines.SENSOR_ORIENTATION:
+ if (gOrientationSensor != null) {
+ sm.unregisterListener(getSensorListener(), gOrientationSensor);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_ACCELERATION:
+ if (gAccelerometerSensor != null) {
+ sm.unregisterListener(getSensorListener(), gAccelerometerSensor);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_PROXIMITY:
+ if (gProximitySensor != null) {
+ sm.unregisterListener(getSensorListener(), gProximitySensor);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_LIGHT:
+ if (gLightSensor != null) {
+ sm.unregisterListener(getSensorListener(), gLightSensor);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_LINEAR_ACCELERATION:
+ if (gLinearAccelerometerSensor != null) {
+ sm.unregisterListener(getSensorListener(), gLinearAccelerometerSensor);
+ }
+ break;
+
+ case GeckoHalDefines.SENSOR_GYROSCOPE:
+ if (gGyroscopeSensor != null) {
+ sm.unregisterListener(getSensorListener(), gGyroscopeSensor);
+ }
+ break;
+ default:
+ Log.w(LOGTAG, "Error! Can't disable unknown SENSOR type " + aSensortype);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void moveTaskToBack() {
+ if (getGeckoInterface() != null)
+ getGeckoInterface().getActivity().moveTaskToBack(true);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static void scheduleRestart() {
+ getGeckoInterface().doRestart();
+ }
+
+ // Creates a homescreen shortcut for a web page.
+ // This is the entry point from nsIShellService.
+ @WrapForJNI(calledFrom = "gecko")
+ public static void createShortcut(final String aTitle, final String aURI) {
+ final GeckoInterface geckoInterface = getGeckoInterface();
+ if (geckoInterface == null) {
+ return;
+ }
+ geckoInterface.createShortcut(aTitle, aURI);
+ }
+
+ @JNITarget
+ static public int getPreferredIconSize() {
+ ActivityManager am = (ActivityManager)
+ getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
+ return am.getLauncherLargeIconSize();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static String[] getHandlersForMimeType(String aMimeType, String aAction) {
+ final GeckoInterface geckoInterface = getGeckoInterface();
+ if (geckoInterface == null) {
+ return new String[] {};
+ }
+ return geckoInterface.getHandlersForMimeType(aMimeType, aAction);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static String[] getHandlersForURL(String aURL, String aAction) {
+ final GeckoInterface geckoInterface = getGeckoInterface();
+ if (geckoInterface == null) {
+ return new String[] {};
+ }
+ return geckoInterface.getHandlersForURL(aURL, aAction);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean getHWEncoderCapability() {
+ return HardwareCodecCapabilityUtils.getHWEncoderCapability();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean getHWDecoderCapability() {
+ return HardwareCodecCapabilityUtils.getHWDecoderCapability();
+ }
+
+ static List<ResolveInfo> queryIntentActivities(Intent intent) {
+ final PackageManager pm = getApplicationContext().getPackageManager();
+
+ // Exclude any non-exported activities: we can't open them even if we want to!
+ // Bug 1031569 has some details.
+ final ArrayList<ResolveInfo> list = new ArrayList<>();
+ for (ResolveInfo ri: pm.queryIntentActivities(intent, 0)) {
+ if (ri.activityInfo.exported) {
+ list.add(ri);
+ }
+ }
+
+ return list;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static String getExtensionFromMimeType(String aMimeType) {
+ return MimeTypeMap.getSingleton().getExtensionFromMimeType(aMimeType);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static String getMimeTypeFromExtensions(String aFileExt) {
+ StringTokenizer st = new StringTokenizer(aFileExt, ".,; ");
+ String type = null;
+ String subType = null;
+ while (st.hasMoreElements()) {
+ String ext = st.nextToken();
+ String mt = getMimeTypeFromExtension(ext);
+ if (mt == null)
+ continue;
+ int slash = mt.indexOf('/');
+ String tmpType = mt.substring(0, slash);
+ if (!tmpType.equalsIgnoreCase(type))
+ type = type == null ? tmpType : "*";
+ String tmpSubType = mt.substring(slash + 1);
+ if (!tmpSubType.equalsIgnoreCase(subType))
+ subType = subType == null ? tmpSubType : "*";
+ }
+ if (type == null)
+ type = "*";
+ if (subType == null)
+ subType = "*";
+ return type + "/" + subType;
+ }
+
+ static boolean isUriSafeForScheme(Uri aUri) {
+ // Bug 794034 - We don't want to pass MWI or USSD codes to the
+ // dialer, and ensure the Uri class doesn't parse a URI
+ // containing a fragment ('#')
+ final String scheme = aUri.getScheme();
+ if ("tel".equals(scheme) || "sms".equals(scheme)) {
+ final String number = aUri.getSchemeSpecificPart();
+ if (number.contains("#") || number.contains("*") || aUri.getFragment() != null) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean openUriExternal(String targetURI,
+ String mimeType,
+ String packageName,
+ String className,
+ String action,
+ String title) {
+ final GeckoInterface geckoInterface = getGeckoInterface();
+ if (geckoInterface == null) {
+ return false;
+ }
+ return geckoInterface.openUriExternal(targetURI, mimeType, packageName, className, action, title);
+ }
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void notifyAlertListener(String name, String topic, String cookie);
+
+ /**
+ * Called by the NotificationListener to notify Gecko that a notification has been
+ * shown.
+ */
+ public static void onNotificationShow(final String name, final String cookie) {
+ if (GeckoThread.isRunning()) {
+ notifyAlertListener(name, "alertshow", cookie);
+ }
+ }
+
+ /**
+ * Called by the NotificationListener to notify Gecko that a previously shown
+ * notification has been closed.
+ */
+ public static void onNotificationClose(final String name, final String cookie) {
+ if (GeckoThread.isRunning()) {
+ notifyAlertListener(name, "alertfinished", cookie);
+ }
+ }
+
+ /**
+ * Called by the NotificationListener to notify Gecko that a previously shown
+ * notification has been clicked on.
+ */
+ public static void onNotificationClick(final String name, final String cookie) {
+ if (GeckoThread.isRunning()) {
+ notifyAlertListener(name, "alertclickcallback", cookie);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void showNotification(String name, String cookie, String title,
+ String text, String host, String imageUrl,
+ String persistentData) {
+ if (persistentData == null) {
+ getNotificationListener().showNotification(name, cookie, title, text, host, imageUrl);
+ return;
+ }
+
+ getNotificationListener().showPersistentNotification(
+ name, cookie, title, text, host, imageUrl, persistentData);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void closeNotification(String name) {
+ getNotificationListener().closeNotification(name);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static int getDpi() {
+ if (sDensityDpi == 0) {
+ sDensityDpi = getApplicationContext().getResources().getDisplayMetrics().densityDpi;
+ }
+
+ return sDensityDpi;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static float getDensity() {
+ return getApplicationContext().getResources().getDisplayMetrics().density;
+ }
+
+ private static boolean isHighMemoryDevice() {
+ return HardwareUtils.getMemSize() > HIGH_MEMORY_DEVICE_THRESHOLD_MB;
+ }
+
+ /**
+ * Returns the colour depth of the default screen. This will either be
+ * 24 or 16.
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ public static synchronized int getScreenDepth() {
+ if (sScreenDepth == 0) {
+ sScreenDepth = 16;
+ PixelFormat info = new PixelFormat();
+ final WindowManager wm = (WindowManager)
+ getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
+ PixelFormat.getPixelFormatInfo(wm.getDefaultDisplay().getPixelFormat(), info);
+ if (info.bitsPerPixel >= 24 && isHighMemoryDevice()) {
+ sScreenDepth = 24;
+ }
+ }
+
+ return sScreenDepth;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static synchronized void setScreenDepthOverride(int aScreenDepth) {
+ if (sScreenDepth != 0) {
+ Log.e(LOGTAG, "Tried to override screen depth after it's already been set");
+ return;
+ }
+
+ sScreenDepth = aScreenDepth;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void setFullScreen(boolean fullscreen) {
+ if (getGeckoInterface() != null)
+ getGeckoInterface().setFullScreen(fullscreen);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void performHapticFeedback(boolean aIsLongPress) {
+ // Don't perform haptic feedback if a vibration is currently playing,
+ // because the haptic feedback will nuke the vibration.
+ if (!sVibrationMaybePlaying || System.nanoTime() >= sVibrationEndTime) {
+ LayerView layerView = getLayerView();
+ layerView.performHapticFeedback(aIsLongPress ?
+ HapticFeedbackConstants.LONG_PRESS :
+ HapticFeedbackConstants.VIRTUAL_KEY);
+ }
+ }
+
+ private static Vibrator vibrator() {
+ return (Vibrator) getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE);
+ }
+
+ // Helper method to convert integer array to long array.
+ private static long[] convertIntToLongArray(int[] input) {
+ long[] output = new long[input.length];
+ for (int i = 0; i < input.length; i++) {
+ output[i] = input[i];
+ }
+ return output;
+ }
+
+ // Vibrate only if haptic feedback is enabled.
+ public static void vibrateOnHapticFeedbackEnabled(int[] milliseconds) {
+ if (Settings.System.getInt(getApplicationContext().getContentResolver(),
+ Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) > 0) {
+ vibrate(convertIntToLongArray(milliseconds), -1);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void vibrate(long milliseconds) {
+ sVibrationEndTime = System.nanoTime() + milliseconds * 1000000;
+ sVibrationMaybePlaying = true;
+ vibrator().vibrate(milliseconds);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void vibrate(long[] pattern, int repeat) {
+ // If pattern.length is even, the last element in the pattern is a
+ // meaningless delay, so don't include it in vibrationDuration.
+ long vibrationDuration = 0;
+ int iterLen = pattern.length - (pattern.length % 2 == 0 ? 1 : 0);
+ for (int i = 0; i < iterLen; i++) {
+ vibrationDuration += pattern[i];
+ }
+
+ sVibrationEndTime = System.nanoTime() + vibrationDuration * 1000000;
+ sVibrationMaybePlaying = true;
+ vibrator().vibrate(pattern, repeat);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void cancelVibrate() {
+ sVibrationMaybePlaying = false;
+ sVibrationEndTime = 0;
+ vibrator().cancel();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void setKeepScreenOn(final boolean on) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // TODO
+ }
+ });
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean isNetworkLinkUp() {
+ ConnectivityManager cm = (ConnectivityManager)
+ getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+ try {
+ NetworkInfo info = cm.getActiveNetworkInfo();
+ if (info == null || !info.isConnected())
+ return false;
+ } catch (SecurityException se) {
+ return false;
+ }
+ return true;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean isNetworkLinkKnown() {
+ ConnectivityManager cm = (ConnectivityManager)
+ getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+ try {
+ if (cm.getActiveNetworkInfo() == null)
+ return false;
+ } catch (SecurityException se) {
+ return false;
+ }
+ return true;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static int getNetworkLinkType() {
+ ConnectivityManager cm = (ConnectivityManager)
+ getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo info = cm.getActiveNetworkInfo();
+ if (info == null) {
+ return LINK_TYPE_UNKNOWN;
+ }
+
+ switch (info.getType()) {
+ case ConnectivityManager.TYPE_ETHERNET:
+ return LINK_TYPE_ETHERNET;
+ case ConnectivityManager.TYPE_WIFI:
+ return LINK_TYPE_WIFI;
+ case ConnectivityManager.TYPE_WIMAX:
+ return LINK_TYPE_WIMAX;
+ case ConnectivityManager.TYPE_MOBILE:
+ break; // We will handle sub-types after the switch.
+ default:
+ Log.w(LOGTAG, "Ignoring the current network type.");
+ return LINK_TYPE_UNKNOWN;
+ }
+
+ TelephonyManager tm = (TelephonyManager)
+ getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE);
+ if (tm == null) {
+ Log.e(LOGTAG, "Telephony service does not exist");
+ return LINK_TYPE_UNKNOWN;
+ }
+
+ switch (tm.getNetworkType()) {
+ case TelephonyManager.NETWORK_TYPE_IDEN:
+ case TelephonyManager.NETWORK_TYPE_CDMA:
+ case TelephonyManager.NETWORK_TYPE_GPRS:
+ return LINK_TYPE_2G;
+ case TelephonyManager.NETWORK_TYPE_1xRTT:
+ case TelephonyManager.NETWORK_TYPE_EDGE:
+ return LINK_TYPE_2G; // 2.5G
+ case TelephonyManager.NETWORK_TYPE_UMTS:
+ case TelephonyManager.NETWORK_TYPE_EVDO_0:
+ return LINK_TYPE_3G;
+ case TelephonyManager.NETWORK_TYPE_HSPA:
+ case TelephonyManager.NETWORK_TYPE_HSDPA:
+ case TelephonyManager.NETWORK_TYPE_HSUPA:
+ case TelephonyManager.NETWORK_TYPE_EVDO_A:
+ case TelephonyManager.NETWORK_TYPE_EVDO_B:
+ case TelephonyManager.NETWORK_TYPE_EHRPD:
+ return LINK_TYPE_3G; // 3.5G
+ case TelephonyManager.NETWORK_TYPE_HSPAP:
+ return LINK_TYPE_3G; // 3.75G
+ case TelephonyManager.NETWORK_TYPE_LTE:
+ return LINK_TYPE_4G; // 3.9G
+ case TelephonyManager.NETWORK_TYPE_UNKNOWN:
+ default:
+ Log.w(LOGTAG, "Connected to an unknown mobile network!");
+ return LINK_TYPE_UNKNOWN;
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static int[] getSystemColors() {
+ // attrsAppearance[] must correspond to AndroidSystemColors structure in android/AndroidBridge.h
+ final int[] attrsAppearance = {
+ android.R.attr.textColor,
+ android.R.attr.textColorPrimary,
+ android.R.attr.textColorPrimaryInverse,
+ android.R.attr.textColorSecondary,
+ android.R.attr.textColorSecondaryInverse,
+ android.R.attr.textColorTertiary,
+ android.R.attr.textColorTertiaryInverse,
+ android.R.attr.textColorHighlight,
+ android.R.attr.colorForeground,
+ android.R.attr.colorBackground,
+ android.R.attr.panelColorForeground,
+ android.R.attr.panelColorBackground
+ };
+
+ int[] result = new int[attrsAppearance.length];
+
+ final ContextThemeWrapper contextThemeWrapper =
+ new ContextThemeWrapper(getApplicationContext(), android.R.style.TextAppearance);
+
+ final TypedArray appearance = contextThemeWrapper.getTheme().obtainStyledAttributes(attrsAppearance);
+
+ if (appearance != null) {
+ for (int i = 0; i < appearance.getIndexCount(); i++) {
+ int idx = appearance.getIndex(i);
+ int color = appearance.getColor(idx, 0);
+ result[idx] = color;
+ }
+ appearance.recycle();
+ }
+
+ return result;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static void killAnyZombies() {
+ GeckoProcessesVisitor visitor = new GeckoProcessesVisitor() {
+ @Override
+ public boolean callback(int pid) {
+ if (pid != android.os.Process.myPid())
+ android.os.Process.killProcess(pid);
+ return true;
+ }
+ };
+
+ EnumerateGeckoProcesses(visitor);
+ }
+
+ interface GeckoProcessesVisitor {
+ boolean callback(int pid);
+ }
+
+ private static void EnumerateGeckoProcesses(GeckoProcessesVisitor visiter) {
+ int pidColumn = -1;
+ int userColumn = -1;
+
+ try {
+ // run ps and parse its output
+ java.lang.Process ps = Runtime.getRuntime().exec("ps");
+ BufferedReader in = new BufferedReader(new InputStreamReader(ps.getInputStream()),
+ 2048);
+
+ String headerOutput = in.readLine();
+
+ // figure out the column offsets. We only care about the pid and user fields
+ StringTokenizer st = new StringTokenizer(headerOutput);
+
+ int tokenSoFar = 0;
+ while (st.hasMoreTokens()) {
+ String next = st.nextToken();
+ if (next.equalsIgnoreCase("PID"))
+ pidColumn = tokenSoFar;
+ else if (next.equalsIgnoreCase("USER"))
+ userColumn = tokenSoFar;
+ tokenSoFar++;
+ }
+
+ // alright, the rest are process entries.
+ String psOutput = null;
+ while ((psOutput = in.readLine()) != null) {
+ String[] split = psOutput.split("\\s+");
+ if (split.length <= pidColumn || split.length <= userColumn)
+ continue;
+ int uid = android.os.Process.getUidForName(split[userColumn]);
+ if (uid == android.os.Process.myUid() &&
+ !split[split.length - 1].equalsIgnoreCase("ps")) {
+ int pid = Integer.parseInt(split[pidColumn]);
+ boolean keepGoing = visiter.callback(pid);
+ if (keepGoing == false)
+ break;
+ }
+ }
+ in.close();
+ }
+ catch (Exception e) {
+ Log.w(LOGTAG, "Failed to enumerate Gecko processes.", e);
+ }
+ }
+
+ public static String getAppNameByPID(int pid) {
+ BufferedReader cmdlineReader = null;
+ String path = "/proc/" + pid + "/cmdline";
+ try {
+ File cmdlineFile = new File(path);
+ if (!cmdlineFile.exists())
+ return "";
+ cmdlineReader = new BufferedReader(new FileReader(cmdlineFile));
+ return cmdlineReader.readLine().trim();
+ } catch (Exception ex) {
+ return "";
+ } finally {
+ if (null != cmdlineReader) {
+ try {
+ cmdlineReader.close();
+ } catch (Exception e) { }
+ }
+ }
+ }
+
+ public static void listOfOpenFiles() {
+ int pidColumn = -1;
+ int nameColumn = -1;
+
+ try {
+ String filter = GeckoProfile.get(getApplicationContext()).getDir().toString();
+ Log.d(LOGTAG, "[OPENFILE] Filter: " + filter);
+
+ // run lsof and parse its output
+ java.lang.Process lsof = Runtime.getRuntime().exec("lsof");
+ BufferedReader in = new BufferedReader(new InputStreamReader(lsof.getInputStream()), 2048);
+
+ String headerOutput = in.readLine();
+ StringTokenizer st = new StringTokenizer(headerOutput);
+ int token = 0;
+ while (st.hasMoreTokens()) {
+ String next = st.nextToken();
+ if (next.equalsIgnoreCase("PID"))
+ pidColumn = token;
+ else if (next.equalsIgnoreCase("NAME"))
+ nameColumn = token;
+ token++;
+ }
+
+ // alright, the rest are open file entries.
+ Map<Integer, String> pidNameMap = new TreeMap<Integer, String>();
+ String output = null;
+ while ((output = in.readLine()) != null) {
+ String[] split = output.split("\\s+");
+ if (split.length <= pidColumn || split.length <= nameColumn)
+ continue;
+ final Integer pid = Integer.valueOf(split[pidColumn]);
+ String name = pidNameMap.get(pid);
+ if (name == null) {
+ name = getAppNameByPID(pid.intValue());
+ pidNameMap.put(pid, name);
+ }
+ String file = split[nameColumn];
+ if (!TextUtils.isEmpty(name) && !TextUtils.isEmpty(file) && file.startsWith(filter))
+ Log.d(LOGTAG, "[OPENFILE] " + name + "(" + split[pidColumn] + ") : " + file);
+ }
+ in.close();
+ } catch (Exception e) { }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static byte[] getIconForExtension(String aExt, int iconSize) {
+ try {
+ if (iconSize <= 0)
+ iconSize = 16;
+
+ if (aExt != null && aExt.length() > 1 && aExt.charAt(0) == '.')
+ aExt = aExt.substring(1);
+
+ PackageManager pm = getApplicationContext().getPackageManager();
+ Drawable icon = getDrawableForExtension(pm, aExt);
+ if (icon == null) {
+ // Use a generic icon
+ icon = pm.getDefaultActivityIcon();
+ }
+
+ Bitmap bitmap = ((BitmapDrawable)icon).getBitmap();
+ if (bitmap.getWidth() != iconSize || bitmap.getHeight() != iconSize)
+ bitmap = Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, true);
+
+ ByteBuffer buf = ByteBuffer.allocate(iconSize * iconSize * 4);
+ bitmap.copyPixelsToBuffer(buf);
+
+ return buf.array();
+ }
+ catch (Exception e) {
+ Log.w(LOGTAG, "getIconForExtension failed.", e);
+ return null;
+ }
+ }
+
+ public static String getMimeTypeFromExtension(String ext) {
+ final MimeTypeMap mtm = MimeTypeMap.getSingleton();
+ return mtm.getMimeTypeFromExtension(ext);
+ }
+
+ private static Drawable getDrawableForExtension(PackageManager pm, String aExt) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ final String mimeType = getMimeTypeFromExtension(aExt);
+ if (mimeType != null && mimeType.length() > 0)
+ intent.setType(mimeType);
+ else
+ return null;
+
+ List<ResolveInfo> list = pm.queryIntentActivities(intent, 0);
+ if (list.size() == 0)
+ return null;
+
+ ResolveInfo resolveInfo = list.get(0);
+
+ if (resolveInfo == null)
+ return null;
+
+ ActivityInfo activityInfo = resolveInfo.activityInfo;
+
+ return activityInfo.loadIcon(pm);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean getShowPasswordSetting() {
+ try {
+ int showPassword =
+ Settings.System.getInt(getApplicationContext().getContentResolver(),
+ Settings.System.TEXT_SHOW_PASSWORD, 1);
+ return (showPassword > 0);
+ }
+ catch (Exception e) {
+ return true;
+ }
+ }
+
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+ public static native void onFullScreenPluginHidden(View view);
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void addFullScreenPluginView(View view) {
+ if (getGeckoInterface() != null)
+ getGeckoInterface().addPluginView(view);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void removeFullScreenPluginView(View view) {
+ if (getGeckoInterface() != null)
+ getGeckoInterface().removePluginView(view);
+ }
+
+ /**
+ * A plugin that wish to be loaded in the WebView must provide this permission
+ * in their AndroidManifest.xml.
+ */
+ public static final String PLUGIN_ACTION = "android.webkit.PLUGIN";
+ public static final String PLUGIN_PERMISSION = "android.webkit.permission.PLUGIN";
+
+ private static final String PLUGIN_SYSTEM_LIB = "/system/lib/plugins/";
+
+ private static final String PLUGIN_TYPE = "type";
+ private static final String TYPE_NATIVE = "native";
+ public static final ArrayList<PackageInfo> mPackageInfoCache = new ArrayList<>();
+
+ // Returns null if plugins are blocked on the device.
+ static String[] getPluginDirectories() {
+
+ // Block on Pixel C.
+ if ((new File("/system/lib/hw/power.dragon.so")).exists()) {
+ Log.w(LOGTAG, "Blocking plugins because of Pixel C device (bug 1255122)");
+ return null;
+ }
+ // An awful hack to detect Tegra devices. Easiest way to do it without spinning up a EGL context.
+ boolean isTegra = (new File("/system/lib/hw/gralloc.tegra.so")).exists() ||
+ (new File("/system/lib/hw/gralloc.tegra3.so")).exists() ||
+ (new File("/sys/class/nvidia-gpu")).exists();
+ if (isTegra) {
+ // disable on KitKat (bug 957694)
+ if (Versions.feature19Plus) {
+ Log.w(LOGTAG, "Blocking plugins because of Tegra (bug 957694)");
+ return null;
+ }
+
+ // disable Flash on Tegra ICS with CM9 and other custom firmware (bug 736421)
+ final File vfile = new File("/proc/version");
+ try {
+ if (vfile.canRead()) {
+ final BufferedReader reader = new BufferedReader(new FileReader(vfile));
+ try {
+ final String version = reader.readLine();
+ if (version.indexOf("CM9") != -1 ||
+ version.indexOf("cyanogen") != -1 ||
+ version.indexOf("Nova") != -1) {
+ Log.w(LOGTAG, "Blocking plugins because of Tegra 2 + unofficial ICS bug (bug 736421)");
+ return null;
+ }
+ } finally {
+ reader.close();
+ }
+ }
+ } catch (IOException ex) {
+ // Do nothing.
+ }
+ }
+
+ ArrayList<String> directories = new ArrayList<String>();
+ PackageManager pm = getApplicationContext().getPackageManager();
+ List<ResolveInfo> plugins = pm.queryIntentServices(new Intent(PLUGIN_ACTION),
+ PackageManager.GET_META_DATA);
+
+ synchronized (mPackageInfoCache) {
+
+ // clear the list of existing packageInfo objects
+ mPackageInfoCache.clear();
+
+
+ for (ResolveInfo info : plugins) {
+
+ // retrieve the plugin's service information
+ ServiceInfo serviceInfo = info.serviceInfo;
+ if (serviceInfo == null) {
+ Log.w(LOGTAG, "Ignoring bad plugin.");
+ continue;
+ }
+
+ // Blacklist HTC's flash lite.
+ // See bug #704516 - We're not quite sure what Flash Lite does,
+ // but loading it causes Flash to give errors and fail to draw.
+ if (serviceInfo.packageName.equals("com.htc.flashliteplugin")) {
+ Log.w(LOGTAG, "Skipping HTC's flash lite plugin");
+ continue;
+ }
+
+
+ // Retrieve information from the plugin's manifest.
+ PackageInfo pkgInfo;
+ try {
+ pkgInfo = pm.getPackageInfo(serviceInfo.packageName,
+ PackageManager.GET_PERMISSIONS
+ | PackageManager.GET_SIGNATURES);
+ } catch (Exception e) {
+ Log.w(LOGTAG, "Can't find plugin: " + serviceInfo.packageName);
+ continue;
+ }
+
+ if (pkgInfo == null) {
+ Log.w(LOGTAG, "Not loading plugin: " + serviceInfo.packageName + ". Could not load package information.");
+ continue;
+ }
+
+ /*
+ * find the location of the plugin's shared library. The default
+ * is to assume the app is either a user installed app or an
+ * updated system app. In both of these cases the library is
+ * stored in the app's data directory.
+ */
+ String directory = pkgInfo.applicationInfo.dataDir + "/lib";
+ final int appFlags = pkgInfo.applicationInfo.flags;
+ final int updatedSystemFlags = ApplicationInfo.FLAG_SYSTEM |
+ ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
+
+ // preloaded system app with no user updates
+ if ((appFlags & updatedSystemFlags) == ApplicationInfo.FLAG_SYSTEM) {
+ directory = PLUGIN_SYSTEM_LIB + pkgInfo.packageName;
+ }
+
+ // check if the plugin has the required permissions
+ String permissions[] = pkgInfo.requestedPermissions;
+ if (permissions == null) {
+ Log.w(LOGTAG, "Not loading plugin: " + serviceInfo.packageName + ". Does not have required permission.");
+ continue;
+ }
+ boolean permissionOk = false;
+ for (String permit : permissions) {
+ if (PLUGIN_PERMISSION.equals(permit)) {
+ permissionOk = true;
+ break;
+ }
+ }
+ if (!permissionOk) {
+ Log.w(LOGTAG, "Not loading plugin: " + serviceInfo.packageName + ". Does not have required permission (2).");
+ continue;
+ }
+
+ // check to ensure the plugin is properly signed
+ Signature signatures[] = pkgInfo.signatures;
+ if (signatures == null) {
+ Log.w(LOGTAG, "Not loading plugin: " + serviceInfo.packageName + ". Not signed.");
+ continue;
+ }
+
+ // determine the type of plugin from the manifest
+ if (serviceInfo.metaData == null) {
+ Log.e(LOGTAG, "The plugin '" + serviceInfo.name + "' has no defined type.");
+ continue;
+ }
+
+ String pluginType = serviceInfo.metaData.getString(PLUGIN_TYPE);
+ if (!TYPE_NATIVE.equals(pluginType)) {
+ Log.e(LOGTAG, "Unrecognized plugin type: " + pluginType);
+ continue;
+ }
+
+ try {
+ Class<?> cls = getPluginClass(serviceInfo.packageName, serviceInfo.name);
+
+ //TODO implement any requirements of the plugin class here!
+ boolean classFound = true;
+
+ if (!classFound) {
+ Log.e(LOGTAG, "The plugin's class' " + serviceInfo.name + "' does not extend the appropriate class.");
+ continue;
+ }
+
+ } catch (NameNotFoundException e) {
+ Log.e(LOGTAG, "Can't find plugin: " + serviceInfo.packageName);
+ continue;
+ } catch (ClassNotFoundException e) {
+ Log.e(LOGTAG, "Can't find plugin's class: " + serviceInfo.name);
+ continue;
+ }
+
+ // if all checks have passed then make the plugin available
+ mPackageInfoCache.add(pkgInfo);
+ directories.add(directory);
+ }
+ }
+
+ return directories.toArray(new String[directories.size()]);
+ }
+
+ static String getPluginPackage(String pluginLib) {
+
+ if (pluginLib == null || pluginLib.length() == 0) {
+ return null;
+ }
+
+ synchronized (mPackageInfoCache) {
+ for (PackageInfo pkgInfo : mPackageInfoCache) {
+ if (pluginLib.contains(pkgInfo.packageName)) {
+ return pkgInfo.packageName;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ static Class<?> getPluginClass(String packageName, String className)
+ throws NameNotFoundException, ClassNotFoundException {
+ Context pluginContext = getApplicationContext().createPackageContext(packageName,
+ Context.CONTEXT_INCLUDE_CODE |
+ Context.CONTEXT_IGNORE_SECURITY);
+ ClassLoader pluginCL = pluginContext.getClassLoader();
+ return pluginCL.loadClass(className);
+ }
+
+ @WrapForJNI
+ private static Class<?> loadPluginClass(String className, String libName) {
+ if (getGeckoInterface() == null)
+ return null;
+ try {
+ final String packageName = getPluginPackage(libName);
+ final int contextFlags = Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY;
+ final Context pluginContext = getApplicationContext().createPackageContext(
+ packageName, contextFlags);
+ return pluginContext.getClassLoader().loadClass(className);
+ } catch (java.lang.ClassNotFoundException cnfe) {
+ Log.w(LOGTAG, "Couldn't find plugin class " + className, cnfe);
+ return null;
+ } catch (android.content.pm.PackageManager.NameNotFoundException nnfe) {
+ Log.w(LOGTAG, "Couldn't find package.", nnfe);
+ return null;
+ }
+ }
+
+ private static Context sApplicationContext;
+ private static ContextGetter sContextGetter;
+
+ @Deprecated
+ @WrapForJNI
+ public static Context getContext() {
+ return sContextGetter.getContext();
+ }
+
+ public static void setContextGetter(ContextGetter cg) {
+ sContextGetter = cg;
+ }
+
+ @WrapForJNI
+ public static Context getApplicationContext() {
+ return sApplicationContext;
+ }
+
+ public static void setApplicationContext(final Context context) {
+ sApplicationContext = context;
+ }
+
+ public static SharedPreferences getSharedPreferences() {
+ if (sContextGetter == null) {
+ throw new IllegalStateException("No ContextGetter; cannot fetch prefs.");
+ }
+ return sContextGetter.getSharedPreferences();
+ }
+
+ public interface AppStateListener {
+ public void onPause();
+ public void onResume();
+ public void onOrientationChanged();
+ }
+
+ public interface GeckoInterface {
+ public EventDispatcher getAppEventDispatcher();
+ public GeckoProfile getProfile();
+ public Activity getActivity();
+ public String getDefaultUAString();
+ public void doRestart();
+ public void setFullScreen(boolean fullscreen);
+ public void addPluginView(View view);
+ public void removePluginView(final View view);
+ public void enableOrientationListener();
+ public void disableOrientationListener();
+ public void addAppStateListener(AppStateListener listener);
+ public void removeAppStateListener(AppStateListener listener);
+ public void notifyWakeLockChanged(String topic, String state);
+ public boolean areTabsShown();
+ public AbsoluteLayout getPluginContainer();
+ public void notifyCheckUpdateResult(String result);
+ public void invalidateOptionsMenu();
+
+ /**
+ * Create a shortcut -- generally a home-screen icon -- linking the given title to the given URI.
+ * <p>
+ * This method is always invoked on the Gecko thread.
+ *
+ * @param title of URI to link to.
+ * @param URI to link to.
+ */
+ public void createShortcut(String title, String URI);
+
+ /**
+ * Check if the given URI is visited.
+ * <p/>
+ * If it has been visited, call {@link GeckoAppShell#notifyUriVisited(String)}. (If it
+ * has not been visited, do nothing.)
+ * <p/>
+ * This method is always invoked on the Gecko thread.
+ *
+ * @param uri to check.
+ */
+ public void checkUriVisited(String uri);
+
+ /**
+ * Mark the given URI as visited in Gecko.
+ * <p/>
+ * Implementors may maintain some local store of visited URIs in order to be able to
+ * answer {@link #checkUriVisited(String)} requests affirmatively.
+ * <p/>
+ * This method is always invoked on the Gecko thread.
+ *
+ * @param uri to mark.
+ */
+ public void markUriVisited(final String uri);
+
+ /**
+ * Set the title of the given URI, as determined by Gecko.
+ * <p/>
+ * This method is always invoked on the Gecko thread.
+ *
+ * @param uri given.
+ * @param title to associate with the given URI.
+ */
+ public void setUriTitle(final String uri, final String title);
+
+ public void setAccessibilityEnabled(boolean enabled);
+
+ public boolean openUriExternal(String targetURI, String mimeType, String packageName, String className, String action, String title);
+
+ public String[] getHandlersForMimeType(String mimeType, String action);
+ public String[] getHandlersForURL(String url, String action);
+
+ /**
+ * URI of the underlying chrome window to be opened, or null to use the default GeckoView
+ * XUL container <tt>chrome://browser/content/geckoview.xul</tt>. See
+ * <a href="https://developer.mozilla.org/en/docs/toolkit.defaultChromeURI">https://developer.mozilla.org/en/docs/toolkit.defaultChromeURI</a>
+ *
+ * @return URI or null.
+ */
+ String getDefaultChromeURI();
+ };
+
+ private static GeckoInterface sGeckoInterface;
+
+ public static GeckoInterface getGeckoInterface() {
+ return sGeckoInterface;
+ }
+
+ public static void setGeckoInterface(GeckoInterface aGeckoInterface) {
+ sGeckoInterface = aGeckoInterface;
+ }
+
+ /* package */ static Camera sCamera;
+
+ private static final int kPreferredFPS = 25;
+ private static byte[] sCameraBuffer;
+
+ private static class CameraCallback implements Camera.PreviewCallback {
+ @WrapForJNI(calledFrom = "gecko")
+ private static native void onFrameData(int camera, byte[] data);
+
+ private final int mCamera;
+
+ public CameraCallback(int camera) {
+ mCamera = camera;
+ }
+
+ @Override
+ public void onPreviewFrame(byte[] data, Camera camera) {
+ onFrameData(mCamera, data);
+
+ if (sCamera != null) {
+ sCamera.addCallbackBuffer(sCameraBuffer);
+ }
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static int[] initCamera(String aContentType, int aCamera, int aWidth, int aHeight) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (getGeckoInterface() != null)
+ getGeckoInterface().enableOrientationListener();
+ } catch (Exception e) { }
+ }
+ });
+
+ // [0] = 0|1 (failure/success)
+ // [1] = width
+ // [2] = height
+ // [3] = fps
+ int[] result = new int[4];
+ result[0] = 0;
+
+ if (Camera.getNumberOfCameras() == 0) {
+ return result;
+ }
+
+ try {
+ sCamera = Camera.open(aCamera);
+
+ Camera.Parameters params = sCamera.getParameters();
+ params.setPreviewFormat(ImageFormat.NV21);
+
+ // use the preview fps closest to 25 fps.
+ int fpsDelta = 1000;
+ try {
+ Iterator<Integer> it = params.getSupportedPreviewFrameRates().iterator();
+ while (it.hasNext()) {
+ int nFps = it.next();
+ if (Math.abs(nFps - kPreferredFPS) < fpsDelta) {
+ fpsDelta = Math.abs(nFps - kPreferredFPS);
+ params.setPreviewFrameRate(nFps);
+ }
+ }
+ } catch (Exception e) {
+ params.setPreviewFrameRate(kPreferredFPS);
+ }
+
+ // set up the closest preview size available
+ Iterator<Camera.Size> sit = params.getSupportedPreviewSizes().iterator();
+ int sizeDelta = 10000000;
+ int bufferSize = 0;
+ while (sit.hasNext()) {
+ Camera.Size size = sit.next();
+ if (Math.abs(size.width * size.height - aWidth * aHeight) < sizeDelta) {
+ sizeDelta = Math.abs(size.width * size.height - aWidth * aHeight);
+ params.setPreviewSize(size.width, size.height);
+ bufferSize = size.width * size.height;
+ }
+ }
+
+ sCamera.setParameters(params);
+ sCameraBuffer = new byte[(bufferSize * 12) / 8];
+ sCamera.addCallbackBuffer(sCameraBuffer);
+ sCamera.setPreviewCallbackWithBuffer(new CameraCallback(aCamera));
+ sCamera.startPreview();
+ params = sCamera.getParameters();
+ result[0] = 1;
+ result[1] = params.getPreviewSize().width;
+ result[2] = params.getPreviewSize().height;
+ result[3] = params.getPreviewFrameRate();
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "initCamera RuntimeException.", e);
+ result[0] = result[1] = result[2] = result[3] = 0;
+ }
+ return result;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static synchronized void closeCamera() {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (getGeckoInterface() != null)
+ getGeckoInterface().disableOrientationListener();
+ } catch (Exception e) { }
+ }
+ });
+ if (sCamera != null) {
+ sCamera.stopPreview();
+ sCamera.release();
+ sCamera = null;
+ sCameraBuffer = null;
+ }
+ }
+
+ /*
+ * Battery API related methods.
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ private static void enableBatteryNotifications() {
+ GeckoBatteryManager.enableNotifications();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void handleGeckoMessage(final NativeJSContainer message) {
+ boolean success = EventDispatcher.getInstance().dispatchEvent(message);
+ if (getGeckoInterface() != null && getGeckoInterface().getAppEventDispatcher() != null) {
+ success |= getGeckoInterface().getAppEventDispatcher().dispatchEvent(message);
+ }
+
+ if (!success) {
+ final String type = message.optString("type", null);
+ final String guid = message.optString(EventDispatcher.GUID, null);
+ if (type != null && guid != null) {
+ (new EventDispatcher.GeckoEventCallback(guid, type)).sendError("No listeners for request");
+ }
+ }
+ message.disposeNative();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void disableBatteryNotifications() {
+ GeckoBatteryManager.disableNotifications();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static double[] getCurrentBatteryInformation() {
+ return GeckoBatteryManager.getCurrentInformation();
+ }
+
+ @WrapForJNI(stubName = "CheckURIVisited", calledFrom = "gecko")
+ private static void checkUriVisited(String uri) {
+ final GeckoInterface geckoInterface = getGeckoInterface();
+ if (geckoInterface == null) {
+ return;
+ }
+ geckoInterface.checkUriVisited(uri);
+ }
+
+ @WrapForJNI(stubName = "MarkURIVisited", calledFrom = "gecko")
+ private static void markUriVisited(final String uri) {
+ final GeckoInterface geckoInterface = getGeckoInterface();
+ if (geckoInterface == null) {
+ return;
+ }
+ geckoInterface.markUriVisited(uri);
+ }
+
+ @WrapForJNI(stubName = "SetURITitle", calledFrom = "gecko")
+ private static void setUriTitle(final String uri, final String title) {
+ final GeckoInterface geckoInterface = getGeckoInterface();
+ if (geckoInterface == null) {
+ return;
+ }
+ geckoInterface.setUriTitle(uri, title);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void hideProgressDialog() {
+ // unused stub
+ }
+
+ /* Called by JNI from AndroidBridge, and by reflection from tests/BaseTest.java.in */
+ @WrapForJNI(calledFrom = "gecko")
+ @RobocopTarget
+ public static boolean isTablet() {
+ return HardwareUtils.isTablet();
+ }
+
+ private static boolean sImeWasEnabledOnLastResize = false;
+ public static void viewSizeChanged() {
+ GeckoView v = (GeckoView) getLayerView();
+ if (v == null) {
+ return;
+ }
+ boolean imeIsEnabled = v.isIMEEnabled();
+ if (imeIsEnabled && !sImeWasEnabledOnLastResize) {
+ // The IME just came up after not being up, so let's scroll
+ // to the focused input.
+ notifyObservers("ScrollTo:FocusedInput", "");
+ }
+ sImeWasEnabledOnLastResize = imeIsEnabled;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static double[] getCurrentNetworkInformation() {
+ return GeckoNetworkManager.getInstance().getCurrentInformation();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void enableNetworkNotifications() {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ GeckoNetworkManager.getInstance().enableNotifications();
+ }
+ });
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void disableNetworkNotifications() {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ GeckoNetworkManager.getInstance().disableNotifications();
+ }
+ });
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static short getScreenOrientation() {
+ return GeckoScreenOrientation.getInstance().getScreenOrientation().value;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static int getScreenAngle() {
+ return GeckoScreenOrientation.getInstance().getAngle();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void enableScreenOrientationNotifications() {
+ GeckoScreenOrientation.getInstance().enableNotifications();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void disableScreenOrientationNotifications() {
+ GeckoScreenOrientation.getInstance().disableNotifications();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void lockScreenOrientation(int aOrientation) {
+ GeckoScreenOrientation.getInstance().lock(aOrientation);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void unlockScreenOrientation() {
+ GeckoScreenOrientation.getInstance().unlock();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void notifyWakeLockChanged(String topic, String state) {
+ if (getGeckoInterface() != null)
+ getGeckoInterface().notifyWakeLockChanged(topic, state);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean unlockProfile() {
+ // Try to kill any zombie Fennec's that might be running
+ GeckoAppShell.killAnyZombies();
+
+ // Then force unlock this profile
+ if (getGeckoInterface() != null) {
+ GeckoProfile profile = getGeckoInterface().getProfile();
+ File lock = profile.getFile(".parentlock");
+ return lock.exists() && lock.delete();
+ }
+ return false;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static String getProxyForURI(String spec, String scheme, String host, int port) {
+ final ProxySelector ps = new ProxySelector();
+
+ Proxy proxy = ps.select(scheme, host);
+ if (Proxy.NO_PROXY.equals(proxy)) {
+ return "DIRECT";
+ }
+
+ switch (proxy.type()) {
+ case HTTP:
+ return "PROXY " + proxy.address().toString();
+ case SOCKS:
+ return "SOCKS " + proxy.address().toString();
+ }
+
+ return "DIRECT";
+ }
+
+ @WrapForJNI
+ private static InputStream createInputStream(URLConnection connection) throws IOException {
+ return connection.getInputStream();
+ }
+
+ private static class BitmapConnection extends URLConnection {
+ private Bitmap bitmap;
+
+ BitmapConnection(Bitmap b) throws MalformedURLException, IOException {
+ super(null);
+ bitmap = b;
+ }
+
+ @Override
+ public void connect() {}
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return new BitmapInputStream();
+ }
+
+ @Override
+ public String getContentType() {
+ return "image/png";
+ }
+
+ private final class BitmapInputStream extends PipedInputStream {
+ private boolean mHaveConnected = false;
+
+ @Override
+ public synchronized int read(byte[] buffer, int byteOffset, int byteCount)
+ throws IOException {
+ if (mHaveConnected) {
+ return super.read(buffer, byteOffset, byteCount);
+ }
+
+ final PipedOutputStream output = new PipedOutputStream();
+ connect(output);
+ ThreadUtils.postToBackgroundThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ try {
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, output);
+ output.close();
+ } catch (IOException ioe) { }
+ }
+ });
+ mHaveConnected = true;
+ return super.read(buffer, byteOffset, byteCount);
+ }
+ }
+ }
+
+ @WrapForJNI
+ private static URLConnection getConnection(String url) {
+ try {
+ String spec;
+ if (url.startsWith("android://")) {
+ spec = url.substring(10);
+ } else {
+ spec = url.substring(8);
+ }
+
+ // Check if we are loading a package icon.
+ try {
+ if (spec.startsWith("icon/")) {
+ String[] splits = spec.split("/");
+ if (splits.length != 2) {
+ return null;
+ }
+ final String pkg = splits[1];
+ final PackageManager pm = getApplicationContext().getPackageManager();
+ final Drawable d = pm.getApplicationIcon(pkg);
+ final Bitmap bitmap = BitmapUtils.getBitmapFromDrawable(d);
+ return new BitmapConnection(bitmap);
+ }
+ } catch (Exception ex) {
+ Log.e(LOGTAG, "error", ex);
+ }
+
+ // if the colon got stripped, put it back
+ int colon = spec.indexOf(':');
+ if (colon == -1 || colon > spec.indexOf('/')) {
+ spec = spec.replaceFirst("/", ":/");
+ }
+ } catch (Exception ex) {
+ return null;
+ }
+ return null;
+ }
+
+ @WrapForJNI
+ private static String connectionGetMimeType(URLConnection connection) {
+ return connection.getContentType();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static int getMaxTouchPoints() {
+ PackageManager pm = getApplicationContext().getPackageManager();
+ if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_JAZZHAND)) {
+ // at least, 5+ fingers.
+ return 5;
+ } else if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
+ // at least, 2+ fingers.
+ return 2;
+ } else if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH)) {
+ // 2 fingers
+ return 2;
+ } else if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) {
+ // 1 finger
+ return 1;
+ }
+ return 0;
+ }
+
+ public static synchronized void resetScreenSize() {
+ sScreenSize = null;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static synchronized Rect getScreenSize() {
+ if (sScreenSize == null) {
+ final WindowManager wm = (WindowManager)
+ getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
+ final Display disp = wm.getDefaultDisplay();
+ sScreenSize = new Rect(0, 0, disp.getWidth(), disp.getHeight());
+ }
+ return sScreenSize;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoBatteryManager.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoBatteryManager.java
new file mode 100644
index 0000000000..1a41c390a6
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoBatteryManager.java
@@ -0,0 +1,202 @@
+/* -*- 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.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.BatteryManager;
+import android.os.Build;
+import android.os.SystemClock;
+import android.util.Log;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+public class GeckoBatteryManager extends BroadcastReceiver {
+ private static final String LOGTAG = "GeckoBatteryManager";
+
+ // Those constants should be keep in sync with the ones in:
+ // dom/battery/Constants.h
+ private final static double kDefaultLevel = 1.0;
+ private final static boolean kDefaultCharging = true;
+ private final static double kDefaultRemainingTime = 0.0;
+ private final static double kUnknownRemainingTime = -1.0;
+
+ private static long sLastLevelChange;
+ private static boolean sNotificationsEnabled;
+ private static double sLevel = kDefaultLevel;
+ private static boolean sCharging = kDefaultCharging;
+ private static double sRemainingTime = kDefaultRemainingTime;
+
+ private static final GeckoBatteryManager sInstance = new GeckoBatteryManager();
+
+ private final IntentFilter mFilter;
+ private Context mApplicationContext;
+ private boolean mIsEnabled;
+
+ public static GeckoBatteryManager getInstance() {
+ return sInstance;
+ }
+
+ private GeckoBatteryManager() {
+ mFilter = new IntentFilter();
+ mFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
+ }
+
+ public synchronized void start(final Context context) {
+ if (mIsEnabled) {
+ Log.w(LOGTAG, "Already started!");
+ return;
+ }
+
+ mApplicationContext = context.getApplicationContext();
+ // registerReceiver will return null if registering fails.
+ if (mApplicationContext.registerReceiver(this, mFilter) == null) {
+ Log.e(LOGTAG, "Registering receiver failed");
+ } else {
+ mIsEnabled = true;
+ }
+ }
+
+ public synchronized void stop() {
+ if (!mIsEnabled) {
+ Log.w(LOGTAG, "Already stopped!");
+ return;
+ }
+
+ mApplicationContext.unregisterReceiver(this);
+ mApplicationContext = null;
+ mIsEnabled = false;
+ }
+
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+ private static native void onBatteryChange(double level, boolean charging,
+ double remainingTime);
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!intent.getAction().equals(Intent.ACTION_BATTERY_CHANGED)) {
+ Log.e(LOGTAG, "Got an unexpected intent!");
+ return;
+ }
+
+ boolean previousCharging = isCharging();
+ double previousLevel = getLevel();
+
+ // NOTE: it might not be common (in 2012) but technically, Android can run
+ // on a device that has no battery so we want to make sure it's not the case
+ // before bothering checking for battery state.
+ // However, the Galaxy Nexus phone advertises itself as battery-less which
+ // force us to special-case the logic.
+ // See the Google bug: https://code.google.com/p/android/issues/detail?id=22035
+ if (intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, false) ||
+ Build.MODEL.equals("Galaxy Nexus")) {
+ int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
+ if (plugged == -1) {
+ sCharging = kDefaultCharging;
+ Log.e(LOGTAG, "Failed to get the plugged status!");
+ } else {
+ // Likely, if plugged > 0, it's likely plugged and charging but the doc
+ // isn't clear about that.
+ sCharging = plugged != 0;
+ }
+
+ if (sCharging != previousCharging) {
+ sRemainingTime = kUnknownRemainingTime;
+ // The new remaining time is going to take some time to show up but
+ // it's the best way to show a not too wrong value.
+ sLastLevelChange = 0;
+ }
+
+ // We need two doubles because sLevel is a double.
+ double current = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
+ double max = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
+ if (current == -1 || max == -1) {
+ Log.e(LOGTAG, "Failed to get battery level!");
+ sLevel = kDefaultLevel;
+ } else {
+ sLevel = current / max;
+ }
+
+ if (sLevel == 1.0 && sCharging) {
+ sRemainingTime = kDefaultRemainingTime;
+ } else if (sLevel != previousLevel) {
+ // Estimate remaining time.
+ if (sLastLevelChange != 0) {
+ // Use elapsedRealtime() because we want to track time across device sleeps.
+ long currentTime = SystemClock.elapsedRealtime();
+ long dt = (currentTime - sLastLevelChange) / 1000;
+ double dLevel = sLevel - previousLevel;
+
+ if (sCharging) {
+ if (dLevel < 0) {
+ sRemainingTime = kUnknownRemainingTime;
+ } else {
+ sRemainingTime = Math.round(dt / dLevel * (1.0 - sLevel));
+ }
+ } else {
+ if (dLevel > 0) {
+ Log.w(LOGTAG, "When discharging, level should decrease!");
+ sRemainingTime = kUnknownRemainingTime;
+ } else {
+ sRemainingTime = Math.round(dt / -dLevel * sLevel);
+ }
+ }
+
+ sLastLevelChange = currentTime;
+ } else {
+ // That's the first time we got an update, we can't do anything.
+ sLastLevelChange = SystemClock.elapsedRealtime();
+ }
+ }
+ } else {
+ sLevel = kDefaultLevel;
+ sCharging = kDefaultCharging;
+ sRemainingTime = kDefaultRemainingTime;
+ }
+
+ /*
+ * We want to inform listeners if the following conditions are fulfilled:
+ * - we have at least one observer;
+ * - the charging state or the level has changed.
+ *
+ * Note: no need to check for a remaining time change given that it's only
+ * updated if there is a level change or a charging change.
+ *
+ * The idea is to prevent doing all the way to the DOM code in the child
+ * process to finally not send an event.
+ */
+ if (sNotificationsEnabled &&
+ (previousCharging != isCharging() || previousLevel != getLevel())) {
+ onBatteryChange(getLevel(), isCharging(), getRemainingTime());
+ }
+ }
+
+ public static boolean isCharging() {
+ return sCharging;
+ }
+
+ public static double getLevel() {
+ return sLevel;
+ }
+
+ public static double getRemainingTime() {
+ return sRemainingTime;
+ }
+
+ public static void enableNotifications() {
+ sNotificationsEnabled = true;
+ }
+
+ public static void disableNotifications() {
+ sNotificationsEnabled = false;
+ }
+
+ public static double[] getCurrentInformation() {
+ return new double[] { getLevel(), isCharging() ? 1.0 : 0.0, getRemainingTime() };
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditable.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditable.java
new file mode 100644
index 0000000000..695cff4439
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditable.java
@@ -0,0 +1,1589 @@
+/* -*- 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.lang.reflect.Array;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import org.json.JSONObject;
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.ThreadUtils.AssertBehavior;
+
+import android.graphics.RectF;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.NoCopySpan;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.style.CharacterStyle;
+import android.util.Log;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+
+/*
+ GeckoEditable implements only some functions of Editable
+ The field mText contains the actual underlying
+ SpannableStringBuilder/Editable that contains our text.
+*/
+final class GeckoEditable extends JNIObject
+ implements InvocationHandler, Editable, GeckoEditableClient {
+
+ private static final boolean DEBUG = false;
+ private static final String LOGTAG = "GeckoEditable";
+
+ // Filters to implement Editable's filtering functionality
+ private InputFilter[] mFilters;
+
+ private final AsyncText mText;
+ private final Editable mProxy;
+ private final ConcurrentLinkedQueue<Action> mActions;
+ private KeyCharacterMap mKeyMap;
+
+ // mIcRunHandler is the Handler that currently runs Gecko-to-IC Runnables
+ // mIcPostHandler is the Handler to post Gecko-to-IC Runnables to
+ // The two can be different when switching from one handler to another
+ private Handler mIcRunHandler;
+ private Handler mIcPostHandler;
+
+ /* package */ GeckoEditableListener mListener;
+ /* package */ GeckoView mView;
+
+ /* package */ boolean mInBatchMode; // Used by IC thread
+ /* package */ boolean mNeedSync; // Used by IC thread
+ // Gecko side needs an updated composition from Java;
+ private boolean mNeedUpdateComposition; // Used by IC thread
+ private boolean mSuppressKeyUp; // Used by IC thread
+
+ private boolean mGeckoFocused; // Used by Gecko thread
+ private boolean mIgnoreSelectionChange; // Used by Gecko thread
+
+ private static final int IME_RANGE_CARETPOSITION = 1;
+ private static final int IME_RANGE_RAWINPUT = 2;
+ private static final int IME_RANGE_SELECTEDRAWTEXT = 3;
+ private static final int IME_RANGE_CONVERTEDTEXT = 4;
+ private static final int IME_RANGE_SELECTEDCONVERTEDTEXT = 5;
+
+ private static final int IME_RANGE_LINE_NONE = 0;
+ private static final int IME_RANGE_LINE_DOTTED = 1;
+ private static final int IME_RANGE_LINE_DASHED = 2;
+ private static final int IME_RANGE_LINE_SOLID = 3;
+ private static final int IME_RANGE_LINE_DOUBLE = 4;
+ private static final int IME_RANGE_LINE_WAVY = 5;
+
+ private static final int IME_RANGE_UNDERLINE = 1;
+ private static final int IME_RANGE_FORECOLOR = 2;
+ private static final int IME_RANGE_BACKCOLOR = 4;
+ private static final int IME_RANGE_LINECOLOR = 8;
+
+ @WrapForJNI(dispatchTo = "proxy")
+ private native void onKeyEvent(int action, int keyCode, int scanCode, int metaState,
+ long time, int unicodeChar, int baseUnicodeChar,
+ int domPrintableKeyValue, int repeatCount, int flags,
+ boolean isSynthesizedImeKey, KeyEvent event);
+
+ private void onKeyEvent(KeyEvent event, int action, int savedMetaState,
+ boolean isSynthesizedImeKey) {
+ // Use a separate action argument so we can override the key's original action,
+ // e.g. change ACTION_MULTIPLE to ACTION_DOWN. That way we don't have to allocate
+ // a new key event just to change its action field.
+ //
+ // Normally we expect event.getMetaState() to reflect the current meta-state; however,
+ // some software-generated key events may not have event.getMetaState() set, e.g. key
+ // events from Swype. Therefore, it's necessary to combine the key's meta-states
+ // with the meta-states that we keep separately in KeyListener
+ final int metaState = event.getMetaState() | savedMetaState;
+ final int unmodifiedMetaState = metaState &
+ ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK | KeyEvent.META_META_MASK);
+ final int unicodeChar = event.getUnicodeChar(metaState);
+ final int domPrintableKeyValue =
+ unicodeChar >= ' ' ? unicodeChar :
+ unmodifiedMetaState != metaState ? event.getUnicodeChar(unmodifiedMetaState) :
+ 0;
+ onKeyEvent(action, event.getKeyCode(), event.getScanCode(),
+ metaState, event.getEventTime(), unicodeChar,
+ // e.g. for Ctrl+A, Android returns 0 for unicodeChar,
+ // but Gecko expects 'a', so we return that in baseUnicodeChar.
+ event.getUnicodeChar(0), domPrintableKeyValue, event.getRepeatCount(),
+ event.getFlags(), isSynthesizedImeKey, event);
+ }
+
+ @WrapForJNI(dispatchTo = "proxy")
+ private native void onImeSynchronize();
+
+ @WrapForJNI(dispatchTo = "proxy")
+ private native void onImeReplaceText(int start, int end, String text);
+
+ @WrapForJNI(dispatchTo = "proxy")
+ private native void onImeAddCompositionRange(int start, int end, int rangeType,
+ int rangeStyles, int rangeLineStyle,
+ boolean rangeBoldLine, int rangeForeColor,
+ int rangeBackColor, int rangeLineColor);
+
+ @WrapForJNI(dispatchTo = "proxy")
+ private native void onImeUpdateComposition(int start, int end);
+
+ @WrapForJNI(dispatchTo = "proxy")
+ private native void onImeRequestCursorUpdates(int requestMode);
+
+ /**
+ * Class that encapsulates asynchronous text editing. There are two copies of the
+ * text, a current copy and a shadow copy. Both can be modified independently through
+ * the current*** and shadow*** methods, respectively. The current copy can only be
+ * modified on the Gecko side and reflects the authoritative version of the text. The
+ * shadow copy can only be modified on the IC side and reflects what we think the
+ * current text is. Periodically, the shadow copy can be synced to the current copy
+ * through syncShadowText, so the shadow copy once again refers to the same text as
+ * the current copy.
+ */
+ private final class AsyncText {
+ // The current text is the update-to-date version of the text, and is only updated
+ // on the Gecko side.
+ private final SpannableStringBuilder mCurrentText = new SpannableStringBuilder();
+ // Track changes on the current side for syncing purposes.
+ // Start of the changed range in current text since last sync.
+ private int mCurrentStart = Integer.MAX_VALUE;
+ // End of the changed range (before the change) in current text since last sync.
+ private int mCurrentOldEnd;
+ // End of the changed range (after the change) in current text since last sync.
+ private int mCurrentNewEnd;
+ // Track selection changes separately.
+ private boolean mCurrentSelectionChanged;
+
+ // The shadow text is what we think the current text is on the Java side, and is
+ // periodically synced with the current text.
+ private final SpannableStringBuilder mShadowText = new SpannableStringBuilder();
+ // Track changes on the shadow side for syncing purposes.
+ // Start of the changed range in shadow text since last sync.
+ private int mShadowStart = Integer.MAX_VALUE;
+ // End of the changed range (before the change) in shadow text since last sync.
+ private int mShadowOldEnd;
+ // End of the changed range (after the change) in shadow text since last sync.
+ private int mShadowNewEnd;
+
+ private void addCurrentChangeLocked(final int start, final int oldEnd, final int newEnd) {
+ // Merge the new change into any existing change.
+ mCurrentStart = Math.min(mCurrentStart, start);
+ mCurrentOldEnd += Math.max(0, oldEnd - mCurrentNewEnd);
+ mCurrentNewEnd = newEnd + Math.max(0, mCurrentNewEnd - oldEnd);
+ }
+
+ public synchronized void currentReplace(final int start, final int end,
+ final CharSequence newText) {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ mCurrentText.replace(start, end, newText);
+ addCurrentChangeLocked(start, end, start + newText.length());
+ }
+
+ public synchronized void currentSetSelection(final int start, final int end) {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ Selection.setSelection(mCurrentText, start, end);
+ mCurrentSelectionChanged = true;
+ }
+
+ public synchronized void currentSetSpan(final Object obj, final int start,
+ final int end, final int flags) {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ mCurrentText.setSpan(obj, start, end, flags);
+ addCurrentChangeLocked(start, end, end);
+ }
+
+ public synchronized void currentRemoveSpan(final Object obj) {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ if (obj == null) {
+ mCurrentText.clearSpans();
+ addCurrentChangeLocked(0, mCurrentText.length(), mCurrentText.length());
+ return;
+ }
+ final int start = mCurrentText.getSpanStart(obj);
+ final int end = mCurrentText.getSpanEnd(obj);
+ if (start < 0 || end < 0) {
+ return;
+ }
+ mCurrentText.removeSpan(obj);
+ addCurrentChangeLocked(start, end, end);
+ }
+
+ // Return Spanned instead of Editable because the returned object is supposed to
+ // be read-only. Editing should be done through one of the current*** methods.
+ public Spanned getCurrentText() {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ return mCurrentText;
+ }
+
+ private void addShadowChange(final int start, final int oldEnd, final int newEnd) {
+ // Merge the new change into any existing change.
+ mShadowStart = Math.min(mShadowStart, start);
+ mShadowOldEnd += Math.max(0, oldEnd - mShadowNewEnd);
+ mShadowNewEnd = newEnd + Math.max(0, mShadowNewEnd - oldEnd);
+ }
+
+ public void shadowReplace(final int start, final int end,
+ final CharSequence newText)
+ {
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+ mShadowText.replace(start, end, newText);
+ addShadowChange(start, end, start + newText.length());
+ }
+
+ public void shadowSetSpan(final Object obj, final int start,
+ final int end, final int flags) {
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+ mShadowText.setSpan(obj, start, end, flags);
+ addShadowChange(start, end, end);
+ }
+
+ public void shadowRemoveSpan(final Object obj) {
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+ if (obj == null) {
+ mShadowText.clearSpans();
+ addShadowChange(0, mShadowText.length(), mShadowText.length());
+ return;
+ }
+ final int start = mShadowText.getSpanStart(obj);
+ final int end = mShadowText.getSpanEnd(obj);
+ if (start < 0 || end < 0) {
+ return;
+ }
+ mShadowText.removeSpan(obj);
+ addShadowChange(start, end, end);
+ }
+
+ // Return Spanned instead of Editable because the returned object is supposed to
+ // be read-only. Editing should be done through one of the shadow*** methods.
+ public Spanned getShadowText() {
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+ return mShadowText;
+ }
+
+ public synchronized void syncShadowText(final GeckoEditableListener listener) {
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+
+ if (mCurrentStart > mCurrentOldEnd && mShadowStart > mShadowOldEnd) {
+ // Still check selection changes.
+ if (!mCurrentSelectionChanged) {
+ return;
+ }
+ final int start = Selection.getSelectionStart(mCurrentText);
+ final int end = Selection.getSelectionEnd(mCurrentText);
+ Selection.setSelection(mShadowText, start, end);
+ mCurrentSelectionChanged = false;
+
+ if (listener != null) {
+ listener.onSelectionChange();
+ }
+ return;
+ }
+
+ // Copy the portion of the current text that has changed over to the shadow
+ // text, with consideration for any concurrent changes in the shadow text.
+ final int start = Math.min(mShadowStart, mCurrentStart);
+ final int shadowEnd = mShadowNewEnd + Math.max(0, mCurrentOldEnd - mShadowOldEnd);
+ final int currentEnd = mCurrentNewEnd + Math.max(0, mShadowOldEnd - mCurrentOldEnd);
+
+ // Perform replacement in two steps (delete and insert) so that old spans are
+ // properly deleted before identical new spans are inserted. Otherwise the new
+ // spans won't be inserted due to the text already having the old spans.
+ mShadowText.delete(start, shadowEnd);
+ mShadowText.insert(start, mCurrentText, start, currentEnd);
+
+ // SpannableStringBuilder has some internal logic to fix up selections, but we
+ // don't want that, so we always fix up the selection a second time.
+ final int selStart = Selection.getSelectionStart(mCurrentText);
+ final int selEnd = Selection.getSelectionEnd(mCurrentText);
+ Selection.setSelection(mShadowText, selStart, selEnd);
+
+ if (DEBUG && !mShadowText.equals(mCurrentText)) {
+ // Sanity check.
+ throw new IllegalStateException("Failed to sync: " +
+ mShadowStart + '-' + mShadowOldEnd + '-' + mShadowNewEnd + '/' +
+ mCurrentStart + '-' + mCurrentOldEnd + '-' + mCurrentNewEnd);
+ }
+
+ if (listener != null) {
+ // Call onTextChange after selection fix-up but before we call
+ // onSelectionChange.
+ listener.onTextChange();
+
+ if (mCurrentSelectionChanged || (mCurrentOldEnd != mCurrentNewEnd &&
+ (selStart >= mCurrentStart || selEnd >= mCurrentStart))) {
+ listener.onSelectionChange();
+ }
+ }
+
+ // These values ensure the first change is properly added.
+ mCurrentStart = mShadowStart = Integer.MAX_VALUE;
+ mCurrentOldEnd = mShadowOldEnd = 0;
+ mCurrentNewEnd = mShadowNewEnd = 0;
+ mCurrentSelectionChanged = false;
+ }
+ }
+
+ /* An action that alters the Editable
+
+ Each action corresponds to a Gecko event. While the Gecko event is being sent to the Gecko
+ thread, the action stays on top of mActions queue. After the Gecko event is processed and
+ replied, the action is removed from the queue
+ */
+ private static final class Action {
+ // For input events (keypress, etc.); use with onImeSynchronize
+ static final int TYPE_EVENT = 0;
+ // For Editable.replace() call; use with onImeReplaceText
+ static final int TYPE_REPLACE_TEXT = 1;
+ // For Editable.setSpan() call; use with onImeSynchronize
+ static final int TYPE_SET_SPAN = 2;
+ // For Editable.removeSpan() call; use with onImeSynchronize
+ static final int TYPE_REMOVE_SPAN = 3;
+ // For switching handler; use with onImeSynchronize
+ static final int TYPE_SET_HANDLER = 4;
+
+ final int mType;
+ int mStart;
+ int mEnd;
+ CharSequence mSequence;
+ Object mSpanObject;
+ int mSpanFlags;
+ Handler mHandler;
+
+ Action(int type) {
+ mType = type;
+ }
+
+ static Action newReplaceText(CharSequence text, int start, int end) {
+ if (start < 0 || start > end) {
+ Log.e(LOGTAG, "invalid replace text offsets: " + start + " to " + end);
+ throw new IllegalArgumentException("invalid replace text offsets");
+ }
+
+ final Action action = new Action(TYPE_REPLACE_TEXT);
+ action.mSequence = text;
+ action.mStart = start;
+ action.mEnd = end;
+ return action;
+ }
+
+ static Action newSetSpan(Object object, int start, int end, int flags) {
+ if (start < 0 || start > end) {
+ Log.e(LOGTAG, "invalid span offsets: " + start + " to " + end);
+ throw new IllegalArgumentException("invalid span offsets");
+ }
+ final Action action = new Action(TYPE_SET_SPAN);
+ action.mSpanObject = object;
+ action.mStart = start;
+ action.mEnd = end;
+ action.mSpanFlags = flags;
+ return action;
+ }
+
+ static Action newRemoveSpan(Object object) {
+ final Action action = new Action(TYPE_REMOVE_SPAN);
+ action.mSpanObject = object;
+ return action;
+ }
+
+ static Action newSetHandler(Handler handler) {
+ final Action action = new Action(TYPE_SET_HANDLER);
+ action.mHandler = handler;
+ return action;
+ }
+ }
+
+ private void icOfferAction(final Action action) {
+ if (DEBUG) {
+ assertOnIcThread();
+ Log.d(LOGTAG, "offer: Action(" +
+ getConstantName(Action.class, "TYPE_", action.mType) + ")");
+ }
+
+ if (mListener == null) {
+ // We haven't initialized or we've been destroyed.
+ return;
+ }
+
+ mActions.offer(action);
+
+ switch (action.mType) {
+ case Action.TYPE_EVENT:
+ case Action.TYPE_SET_HANDLER:
+ onImeSynchronize();
+ break;
+
+ case Action.TYPE_SET_SPAN:
+ mText.shadowSetSpan(action.mSpanObject, action.mStart,
+ action.mEnd, action.mSpanFlags);
+ action.mSequence = TextUtils.substring(
+ mText.getShadowText(), action.mStart, action.mEnd);
+
+ mNeedUpdateComposition |= (action.mSpanFlags & Spanned.SPAN_INTERMEDIATE) == 0 &&
+ ((action.mSpanFlags & Spanned.SPAN_COMPOSING) != 0 ||
+ action.mSpanObject == Selection.SELECTION_START ||
+ action.mSpanObject == Selection.SELECTION_END);
+
+ onImeSynchronize();
+ break;
+
+ case Action.TYPE_REMOVE_SPAN:
+ final int flags = mText.getShadowText().getSpanFlags(action.mSpanObject);
+ mText.shadowRemoveSpan(action.mSpanObject);
+
+ mNeedUpdateComposition |= (flags & Spanned.SPAN_INTERMEDIATE) == 0 &&
+ (flags & Spanned.SPAN_COMPOSING) != 0;
+
+ onImeSynchronize();
+ break;
+
+ case Action.TYPE_REPLACE_TEXT:
+ // Always sync text after a replace action, so that if the Gecko
+ // text is not changed, we will revert the shadow text to before.
+ mNeedSync = true;
+
+ // Because we get composition styling here essentially for free,
+ // we don't need to check if we're in batch mode.
+ if (!icMaybeSendComposition(
+ action.mSequence, /* useEntireText */ true, /* notifyGecko */ false)) {
+ // Since we don't have a composition, we can try sending key events.
+ sendCharKeyEvents(action);
+ }
+ mText.shadowReplace(action.mStart, action.mEnd, action.mSequence);
+ onImeReplaceText(action.mStart, action.mEnd, action.mSequence.toString());
+ break;
+
+ default:
+ throw new IllegalStateException("Action not processed");
+ }
+ }
+
+ private KeyEvent [] synthesizeKeyEvents(CharSequence cs) {
+ try {
+ if (mKeyMap == null) {
+ mKeyMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
+ }
+ } catch (Exception e) {
+ // KeyCharacterMap.UnavailableException is not found on Gingerbread;
+ // besides, it seems like HC and ICS will throw something other than
+ // KeyCharacterMap.UnavailableException; so use a generic Exception here
+ return null;
+ }
+ KeyEvent [] keyEvents = mKeyMap.getEvents(cs.toString().toCharArray());
+ if (keyEvents == null || keyEvents.length == 0) {
+ return null;
+ }
+ return keyEvents;
+ }
+
+ private void sendCharKeyEvents(Action action) {
+ if (action.mSequence.length() != 1 ||
+ (action.mSequence instanceof Spannable &&
+ ((Spannable)action.mSequence).nextSpanTransition(
+ -1, Integer.MAX_VALUE, null) < Integer.MAX_VALUE)) {
+ // Spans are not preserved when we use key events,
+ // so we need the sequence to not have any spans
+ return;
+ }
+ KeyEvent [] keyEvents = synthesizeKeyEvents(action.mSequence);
+ if (keyEvents == null) {
+ return;
+ }
+ for (KeyEvent event : keyEvents) {
+ if (KeyEvent.isModifierKey(event.getKeyCode())) {
+ continue;
+ }
+ if (event.getAction() == KeyEvent.ACTION_UP && mSuppressKeyUp) {
+ continue;
+ }
+ if (DEBUG) {
+ Log.d(LOGTAG, "sending: " + event);
+ }
+ onKeyEvent(event, event.getAction(),
+ /* metaState */ 0, /* isSynthesizedImeKey */ true);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ GeckoEditable(final GeckoView v) {
+ if (DEBUG) {
+ // Called by nsWindow.
+ ThreadUtils.assertOnGeckoThread();
+ }
+
+ mText = new AsyncText();
+ mActions = new ConcurrentLinkedQueue<Action>();
+
+ final Class<?>[] PROXY_INTERFACES = { Editable.class };
+ mProxy = (Editable)Proxy.newProxyInstance(
+ Editable.class.getClassLoader(),
+ PROXY_INTERFACES, this);
+
+ mIcRunHandler = mIcPostHandler = ThreadUtils.getUiHandler();
+
+ onViewChange(v);
+ }
+
+ @WrapForJNI(dispatchTo = "proxy") @Override
+ protected native void disposeNative();
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void onViewChange(final GeckoView v) {
+ if (DEBUG) {
+ // Called by nsWindow.
+ ThreadUtils.assertOnGeckoThread();
+ Log.d(LOGTAG, "onViewChange(" + v + ")");
+ }
+
+ final GeckoEditableListener newListener =
+ v != null ? GeckoInputConnection.create(v, this) : null;
+
+ final Runnable setListenerRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG) {
+ Log.d(LOGTAG, "onViewChange (set listener)");
+ }
+
+ mListener = newListener;
+
+ if (newListener == null) {
+ // We're being destroyed. By this point, we should have cleared all
+ // pending Runnables on the IC thread, so it's safe to call
+ // disposeNative here.
+ GeckoEditable.this.disposeNative();
+ }
+ }
+ };
+
+ // Post to UI thread first to make sure any code that is using the old input
+ // connection has finished running, before we switch to a new input connection or
+ // before we clear the input connection on destruction.
+ final Handler icHandler = mIcPostHandler;
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG) {
+ Log.d(LOGTAG, "onViewChange (set IC)");
+ }
+
+ if (mView != null) {
+ // Detach the previous view.
+ mView.setInputConnectionListener(null);
+ }
+ if (v != null) {
+ // And attach the new view.
+ v.setInputConnectionListener((InputConnectionListener) newListener);
+ }
+
+ mView = v;
+ icHandler.post(setListenerRunnable);
+ }
+ });
+ }
+
+ private boolean onIcThread() {
+ return mIcRunHandler.getLooper() == Looper.myLooper();
+ }
+
+ private void assertOnIcThread() {
+ ThreadUtils.assertOnThread(mIcRunHandler.getLooper().getThread(), AssertBehavior.THROW);
+ }
+
+ private void geckoPostToIc(Runnable runnable) {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ mIcPostHandler.post(runnable);
+ }
+
+ private Object getField(Object obj, String field, Object def) {
+ try {
+ return obj.getClass().getField(field).get(obj);
+ } catch (Exception e) {
+ return def;
+ }
+ }
+
+ /**
+ * Send composition ranges to Gecko for the entire shadow text.
+ */
+ private void icMaybeSendComposition() {
+ if (!mNeedUpdateComposition) {
+ return;
+ }
+
+ icMaybeSendComposition(mText.getShadowText(),
+ /* useEntireText */ false, /* notifyGecko */ true);
+ }
+
+ /**
+ * Send composition ranges to Gecko if the text has composing spans.
+ *
+ * @param sequence Text with possible composing spans
+ * @param useEntireText If text has composing spans, treat the entire text as
+ * a Gecko composition, instead of just the spanned part.
+ * @param notifyGecko Notify Gecko of the new composition ranges;
+ * otherwise, the caller is responsible for notifying Gecko.
+ * @return Whether there was a composition
+ */
+ private boolean icMaybeSendComposition(final CharSequence sequence,
+ final boolean useEntireText,
+ final boolean notifyGecko) {
+ mNeedUpdateComposition = false;
+
+ int selStart = Selection.getSelectionStart(sequence);
+ int selEnd = Selection.getSelectionEnd(sequence);
+
+ if (sequence instanceof Spanned) {
+ final Spanned text = (Spanned) sequence;
+ final Object[] spans = text.getSpans(0, text.length(), Object.class);
+ boolean found = false;
+ int composingStart = useEntireText ? 0 : Integer.MAX_VALUE;
+ int composingEnd = useEntireText ? text.length() : 0;
+
+ // Find existence and range of any composing spans (spans with the
+ // SPAN_COMPOSING flag set).
+ for (Object span : spans) {
+ if ((text.getSpanFlags(span) & Spanned.SPAN_COMPOSING) == 0) {
+ continue;
+ }
+ found = true;
+ if (useEntireText) {
+ break;
+ }
+ composingStart = Math.min(composingStart, text.getSpanStart(span));
+ composingEnd = Math.max(composingEnd, text.getSpanEnd(span));
+ }
+
+ if (useEntireText && (selStart < 0 || selEnd < 0)) {
+ selStart = composingEnd;
+ selEnd = composingEnd;
+ }
+
+ if (found) {
+ icSendComposition(text, selStart, selEnd, composingStart, composingEnd);
+ if (notifyGecko) {
+ onImeUpdateComposition(composingStart, composingEnd);
+ }
+ return true;
+ }
+ }
+
+ if (notifyGecko) {
+ // Set the selection by using a composition without ranges
+ onImeUpdateComposition(selStart, selEnd);
+ }
+
+ if (DEBUG) {
+ Log.d(LOGTAG, "icSendComposition(): no composition");
+ }
+ return false;
+ }
+
+ private void icSendComposition(final Spanned text,
+ final int selStart, final int selEnd,
+ final int composingStart, final int composingEnd) {
+ if (DEBUG) {
+ assertOnIcThread();
+ Log.d(LOGTAG, "icSendComposition(\"" + text + "\", " +
+ composingStart + ", " + composingEnd + ")");
+ }
+ if (DEBUG) {
+ Log.d(LOGTAG, " range = " + composingStart + "-" + composingEnd);
+ Log.d(LOGTAG, " selection = " + selStart + "-" + selEnd);
+ }
+
+ if (selEnd >= composingStart && selEnd <= composingEnd) {
+ onImeAddCompositionRange(
+ selEnd - composingStart, selEnd - composingStart,
+ IME_RANGE_CARETPOSITION, 0, 0, false, 0, 0, 0);
+ }
+
+ int rangeStart = composingStart;
+ TextPaint tp = new TextPaint();
+ TextPaint emptyTp = new TextPaint();
+ // set initial foreground color to 0, because we check for tp.getColor() == 0
+ // below to decide whether to pass a foreground color to Gecko
+ emptyTp.setColor(0);
+ do {
+ int rangeType, rangeStyles = 0, rangeLineStyle = IME_RANGE_LINE_NONE;
+ boolean rangeBoldLine = false;
+ int rangeForeColor = 0, rangeBackColor = 0, rangeLineColor = 0;
+ int rangeEnd = text.nextSpanTransition(rangeStart, composingEnd, Object.class);
+
+ if (selStart > rangeStart && selStart < rangeEnd) {
+ rangeEnd = selStart;
+ } else if (selEnd > rangeStart && selEnd < rangeEnd) {
+ rangeEnd = selEnd;
+ }
+ CharacterStyle[] styleSpans =
+ text.getSpans(rangeStart, rangeEnd, CharacterStyle.class);
+
+ if (DEBUG) {
+ Log.d(LOGTAG, " found " + styleSpans.length + " spans @ " +
+ rangeStart + "-" + rangeEnd);
+ }
+
+ if (styleSpans.length == 0) {
+ rangeType = (selStart == rangeStart && selEnd == rangeEnd)
+ ? IME_RANGE_SELECTEDRAWTEXT
+ : IME_RANGE_RAWINPUT;
+ } else {
+ rangeType = (selStart == rangeStart && selEnd == rangeEnd)
+ ? IME_RANGE_SELECTEDCONVERTEDTEXT
+ : IME_RANGE_CONVERTEDTEXT;
+ tp.set(emptyTp);
+ for (CharacterStyle span : styleSpans) {
+ span.updateDrawState(tp);
+ }
+ int tpUnderlineColor = 0;
+ float tpUnderlineThickness = 0.0f;
+
+ // These TextPaint fields only exist on Android ICS+ and are not in the SDK.
+ tpUnderlineColor = (Integer)getField(tp, "underlineColor", 0);
+ tpUnderlineThickness = (Float)getField(tp, "underlineThickness", 0.0f);
+ if (tpUnderlineColor != 0) {
+ rangeStyles |= IME_RANGE_UNDERLINE | IME_RANGE_LINECOLOR;
+ rangeLineColor = tpUnderlineColor;
+ // Approximately translate underline thickness to what Gecko understands
+ if (tpUnderlineThickness <= 0.5f) {
+ rangeLineStyle = IME_RANGE_LINE_DOTTED;
+ } else {
+ rangeLineStyle = IME_RANGE_LINE_SOLID;
+ if (tpUnderlineThickness >= 2.0f) {
+ rangeBoldLine = true;
+ }
+ }
+ } else if (tp.isUnderlineText()) {
+ rangeStyles |= IME_RANGE_UNDERLINE;
+ rangeLineStyle = IME_RANGE_LINE_SOLID;
+ }
+ if (tp.getColor() != 0) {
+ rangeStyles |= IME_RANGE_FORECOLOR;
+ rangeForeColor = tp.getColor();
+ }
+ if (tp.bgColor != 0) {
+ rangeStyles |= IME_RANGE_BACKCOLOR;
+ rangeBackColor = tp.bgColor;
+ }
+ }
+ onImeAddCompositionRange(
+ rangeStart - composingStart, rangeEnd - composingStart,
+ rangeType, rangeStyles, rangeLineStyle, rangeBoldLine,
+ rangeForeColor, rangeBackColor, rangeLineColor);
+ rangeStart = rangeEnd;
+
+ if (DEBUG) {
+ Log.d(LOGTAG, " added " + rangeType +
+ " : " + Integer.toHexString(rangeStyles) +
+ " : " + Integer.toHexString(rangeForeColor) +
+ " : " + Integer.toHexString(rangeBackColor));
+ }
+ } while (rangeStart < composingEnd);
+ }
+
+ // GeckoEditableClient interface
+
+ @Override
+ public void sendKeyEvent(final KeyEvent event, int action, int metaState) {
+ if (DEBUG) {
+ assertOnIcThread();
+ Log.d(LOGTAG, "sendKeyEvent(" + event + ", " + action + ", " + metaState + ")");
+ }
+ /*
+ We are actually sending two events to Gecko here,
+ 1. Event from the event parameter (key event)
+ 2. Sync event from the icOfferAction call
+ The first event is a normal event that does not reply back to us,
+ the second sync event will have a reply, during which we see that there is a pending
+ event-type action, and update the shadow text accordingly.
+ */
+ icMaybeSendComposition();
+ onKeyEvent(event, action, metaState, /* isSynthesizedImeKey */ false);
+ icOfferAction(new Action(Action.TYPE_EVENT));
+ }
+
+ @Override
+ public Editable getEditable() {
+ if (!onIcThread()) {
+ // Android may be holding an old InputConnection; ignore
+ if (DEBUG) {
+ Log.i(LOGTAG, "getEditable() called on non-IC thread");
+ }
+ return null;
+ }
+ if (mListener == null) {
+ // We haven't initialized or we've been destroyed.
+ return null;
+ }
+ return mProxy;
+ }
+
+ @Override
+ public void setBatchMode(boolean inBatchMode) {
+ if (!onIcThread()) {
+ // Android may be holding an old InputConnection; ignore
+ if (DEBUG) {
+ Log.i(LOGTAG, "setBatchMode() called on non-IC thread");
+ }
+ return;
+ }
+
+ mInBatchMode = inBatchMode;
+
+ if (!inBatchMode && mNeedSync) {
+ icSyncShadowText();
+ }
+ }
+
+ /* package */ void icSyncShadowText() {
+ if (mListener == null) {
+ // Not yet attached or already destroyed.
+ return;
+ }
+
+ if (mInBatchMode || !mActions.isEmpty()) {
+ mNeedSync = true;
+ return;
+ }
+
+ mNeedSync = false;
+ mText.syncShadowText(mListener);
+ }
+
+ private void geckoScheduleSyncShadowText() {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ geckoPostToIc(new Runnable() {
+ @Override
+ public void run() {
+ icSyncShadowText();
+ }
+ });
+ }
+
+ @Override
+ public void setSuppressKeyUp(boolean suppress) {
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+ // Suppress key up event generated as a result of
+ // translating characters to key events
+ mSuppressKeyUp = suppress;
+ }
+
+ @Override // GeckoEditableClient
+ public Handler setInputConnectionHandler(final Handler handler) {
+ if (handler == mIcRunHandler) {
+ return mIcRunHandler;
+ }
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+
+ // There are three threads at this point: Gecko thread, old IC thread, and new IC
+ // thread, and we want to safely switch from old IC thread to new IC thread.
+ // We first send a TYPE_SET_HANDLER action to the Gecko thread; this ensures that
+ // the Gecko thread is stopped at a known point. At the same time, the old IC
+ // thread blocks on the action; this ensures that the old IC thread is stopped at
+ // a known point. Finally, inside the Gecko thread, we post a Runnable to the old
+ // IC thread; this Runnable switches from old IC thread to new IC thread. We
+ // switch IC thread on the old IC thread to ensure any pending Runnables on the
+ // old IC thread are processed before we switch over. Inside the Gecko thread, we
+ // also post a Runnable to the new IC thread; this Runnable blocks until the
+ // switch is complete; this ensures that the new IC thread won't accept
+ // InputConnection calls until after the switch.
+
+ handler.post(new Runnable() { // Make the new IC thread wait.
+ @Override
+ public void run() {
+ synchronized (handler) {
+ while (mIcRunHandler != handler) {
+ try {
+ handler.wait();
+ } catch (final InterruptedException e) {
+ }
+ }
+ }
+ }
+ });
+
+ icOfferAction(Action.newSetHandler(handler));
+ return handler;
+ }
+
+ @Override // GeckoEditableClient
+ public void postToInputConnection(final Runnable runnable) {
+ mIcPostHandler.post(runnable);
+ }
+
+ @Override // GeckoEditableClient
+ public void requestCursorUpdates(int requestMode) {
+ onImeRequestCursorUpdates(requestMode);
+ }
+
+ private void geckoSetIcHandler(final Handler newHandler) {
+ geckoPostToIc(new Runnable() { // posting to old IC thread
+ @Override
+ public void run() {
+ synchronized (newHandler) {
+ mIcRunHandler = newHandler;
+ newHandler.notify();
+ }
+ }
+ });
+
+ // At this point, all future Runnables should be posted to the new IC thread, but
+ // we don't switch mIcRunHandler yet because there may be pending Runnables on the
+ // old IC thread still waiting to run.
+ mIcPostHandler = newHandler;
+ }
+
+ private void geckoActionReply(final Action action) {
+ if (!mGeckoFocused) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "discarding stale reply");
+ }
+ return;
+ }
+
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ Log.d(LOGTAG, "reply: Action(" +
+ getConstantName(Action.class, "TYPE_", action.mType) + ")");
+ }
+ switch (action.mType) {
+ case Action.TYPE_SET_SPAN:
+ final int len = mText.getCurrentText().length();
+ if (action.mStart > len || action.mEnd > len ||
+ !TextUtils.substring(mText.getCurrentText(), action.mStart,
+ action.mEnd).equals(action.mSequence)) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "discarding stale set span call");
+ }
+ break;
+ }
+ mText.currentSetSpan(action.mSpanObject, action.mStart, action.mEnd, action.mSpanFlags);
+ break;
+
+ case Action.TYPE_REMOVE_SPAN:
+ mText.currentRemoveSpan(action.mSpanObject);
+ break;
+
+ case Action.TYPE_SET_HANDLER:
+ geckoSetIcHandler(action.mHandler);
+ break;
+ }
+ }
+
+ private void notifyCommitComposition() {
+ // Gecko already committed its composition. However, Android keyboards
+ // have trouble dealing with us removing the composition manually on
+ // the Java side. Therefore, we keep the composition intact on the Java
+ // side. The text content should still be in-sync on both sides.
+ }
+
+ private void notifyCancelComposition() {
+ // Composition should have been canceled on our side
+ // through text update notifications; verify that here.
+ if (DEBUG) {
+ final Spanned text = mText.getCurrentText();
+ final Object[] spans = text.getSpans(0, text.length(), Object.class);
+ for (Object span : spans) {
+ if ((text.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) {
+ throw new IllegalStateException("composition not cancelled");
+ }
+ }
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void notifyIME(final int type) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ // NOTIFY_IME_REPLY_EVENT is logged separately, inside geckoActionReply()
+ if (type != GeckoEditableListener.NOTIFY_IME_REPLY_EVENT) {
+ Log.d(LOGTAG, "notifyIME(" +
+ getConstantName(GeckoEditableListener.class, "NOTIFY_IME_", type) +
+ ")");
+ }
+ }
+
+ if (type == GeckoEditableListener.NOTIFY_IME_REPLY_EVENT) {
+ geckoActionReply(mActions.poll());
+ if (!mGeckoFocused || !mActions.isEmpty()) {
+ // Only post to IC thread below when the queue is empty.
+ return;
+ }
+ } else if (type == GeckoEditableListener.NOTIFY_IME_TO_COMMIT_COMPOSITION) {
+ notifyCommitComposition();
+ return;
+ } else if (type == GeckoEditableListener.NOTIFY_IME_TO_CANCEL_COMPOSITION) {
+ notifyCancelComposition();
+ return;
+ }
+
+ geckoPostToIc(new Runnable() {
+ @Override
+ public void run() {
+ if (type == GeckoEditableListener.NOTIFY_IME_REPLY_EVENT) {
+ if (mNeedSync) {
+ icSyncShadowText();
+ }
+ return;
+ }
+
+ if (type == GeckoEditableListener.NOTIFY_IME_OF_FOCUS && mListener != null) {
+ mNeedSync = false;
+ mText.syncShadowText(/* listener */ null);
+ }
+
+ if (mListener != null) {
+ mListener.notifyIME(type);
+ }
+ }
+ });
+
+ // Update the mGeckoFocused flag.
+ if (type == GeckoEditableListener.NOTIFY_IME_OF_BLUR) {
+ mGeckoFocused = false;
+ } else if (type == GeckoEditableListener.NOTIFY_IME_OF_FOCUS) {
+ mGeckoFocused = true;
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void notifyIMEContext(final int state, final String typeHint,
+ final String modeHint, final String actionHint) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ Log.d(LOGTAG, "notifyIMEContext(" +
+ getConstantName(GeckoEditableListener.class, "IME_STATE_", state) +
+ ", \"" + typeHint + "\", \"" + modeHint + "\", \"" + actionHint + "\")");
+ }
+ geckoPostToIc(new Runnable() {
+ @Override
+ public void run() {
+ if (mListener == null) {
+ return;
+ }
+ mListener.notifyIMEContext(state, typeHint, modeHint, actionHint);
+ }
+ });
+ }
+
+ @WrapForJNI(calledFrom = "gecko", exceptionMode = "ignore")
+ private void onSelectionChange(final int start, final int end) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ Log.d(LOGTAG, "onSelectionChange(" + start + ", " + end + ")");
+ }
+
+ final int currentLength = mText.getCurrentText().length();
+ if (start < 0 || start > currentLength || end < 0 || end > currentLength) {
+ Log.e(LOGTAG, "invalid selection notification range: " +
+ start + " to " + end + ", length: " + currentLength);
+ throw new IllegalArgumentException("invalid selection notification range");
+ }
+
+ if (mIgnoreSelectionChange) {
+ mIgnoreSelectionChange = false;
+ } else {
+ mText.currentSetSelection(start, end);
+ }
+
+ geckoScheduleSyncShadowText();
+ }
+
+ private boolean geckoIsSameText(int start, int oldEnd, CharSequence newText) {
+ return oldEnd - start == newText.length() &&
+ TextUtils.regionMatches(mText.getCurrentText(), start, newText, 0, oldEnd - start);
+ }
+
+ @WrapForJNI(calledFrom = "gecko", exceptionMode = "ignore")
+ private void onTextChange(final CharSequence text, final int start,
+ final int unboundedOldEnd, final int unboundedNewEnd) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ StringBuilder sb = new StringBuilder("onTextChange(");
+ debugAppend(sb, text);
+ sb.append(", ").append(start).append(", ")
+ .append(unboundedOldEnd).append(", ")
+ .append(unboundedNewEnd).append(")");
+ Log.d(LOGTAG, sb.toString());
+ }
+ if (start < 0 || start > unboundedOldEnd) {
+ Log.e(LOGTAG, "invalid text notification range: " +
+ start + " to " + unboundedOldEnd);
+ throw new IllegalArgumentException("invalid text notification range");
+ }
+
+ final int currentLength = mText.getCurrentText().length();
+
+ /* For the "end" parameters, Gecko can pass in a large
+ number to denote "end of the text". Fix that here */
+ final int oldEnd = unboundedOldEnd > currentLength ? currentLength : unboundedOldEnd;
+ // new end should always match text
+ if (unboundedOldEnd <= currentLength && unboundedNewEnd != (start + text.length())) {
+ Log.e(LOGTAG, "newEnd does not match text: " + unboundedNewEnd + " vs " +
+ (start + text.length()));
+ throw new IllegalArgumentException("newEnd does not match text");
+ }
+
+ final int newEnd = start + text.length();
+ final Action action = mActions.peek();
+
+ if (start == 0 && unboundedOldEnd > currentLength) {
+ // Simply replace the text for newly-focused editors. Replace in two steps to
+ // properly clear composing spans that span the whole range.
+ mText.currentReplace(0, currentLength, "");
+ mText.currentReplace(0, 0, text);
+
+ // Don't ignore the next selection change because we are re-syncing with Gecko
+ mIgnoreSelectionChange = false;
+
+ } else if (action != null &&
+ action.mType == Action.TYPE_REPLACE_TEXT &&
+ start <= action.mStart &&
+ oldEnd >= action.mEnd &&
+ newEnd >= action.mStart + action.mSequence.length()) {
+
+ // Try to preserve both old spans and new spans in action.mSequence.
+ // indexInText is where we can find waction.mSequence within the passed in text.
+ final int startWithinText = action.mStart - start;
+ int indexInText = TextUtils.indexOf(text, action.mSequence, startWithinText);
+ if (indexInText < 0 && startWithinText >= action.mSequence.length()) {
+ indexInText = text.toString().lastIndexOf(action.mSequence.toString(),
+ startWithinText);
+ }
+
+ if (indexInText < 0) {
+ // Text was changed from under us. We are forced to discard any new spans.
+ mText.currentReplace(start, oldEnd, text);
+
+ // Don't ignore the next selection change because we are forced to re-sync
+ // with Gecko here.
+ mIgnoreSelectionChange = false;
+
+ } else if (indexInText == 0 && text.length() == action.mSequence.length() &&
+ oldEnd - start == action.mEnd - action.mStart) {
+ // The new change exactly matches our saved change, so do a direct replace.
+ mText.currentReplace(start, oldEnd, action.mSequence);
+
+ // Ignore the next selection change because the selection change is a
+ // side-effect of the replace-text event we sent.
+ mIgnoreSelectionChange = true;
+
+ } else {
+ // The sequence is embedded within the changed text, so we have to perform
+ // replacement in parts. First replace part of text before the sequence.
+ mText.currentReplace(start, action.mStart, text.subSequence(0, indexInText));
+
+ // Then replace part of the text after the sequence.
+ final int actionStart = indexInText + start;
+ final int delta = actionStart - action.mStart;
+ final int actionEnd = delta + action.mEnd;
+
+ final Spanned currentText = mText.getCurrentText();
+ final boolean resetSelStart = Selection.getSelectionStart(currentText) == actionEnd;
+ final boolean resetSelEnd = Selection.getSelectionEnd(currentText) == actionEnd;
+
+ mText.currentReplace(actionEnd, delta + oldEnd, text.subSequence(
+ indexInText + action.mSequence.length(), text.length()));
+
+ // The replacement above may have shifted our selection, if the selection
+ // was at the start of the replacement range. If so, we need to reset
+ // our selection to the previous position.
+ if (resetSelStart || resetSelEnd) {
+ mText.currentSetSelection(
+ resetSelStart ? actionEnd : Selection.getSelectionStart(currentText),
+ resetSelEnd ? actionEnd : Selection.getSelectionEnd(currentText));
+ }
+
+ // Finally replace the sequence itself to preserve new spans.
+ mText.currentReplace(actionStart, actionEnd, action.mSequence);
+
+ // Ignore the next selection change because the selection change is a
+ // side-effect of the replace-text event we sent.
+ mIgnoreSelectionChange = true;
+ }
+
+ } else if (geckoIsSameText(start, oldEnd, text)) {
+ // Nothing to do because the text is the same. This could happen when
+ // the composition is updated for example, in which case we want to keep the
+ // Java selection.
+ mIgnoreSelectionChange = mIgnoreSelectionChange ||
+ (action != null && action.mType == Action.TYPE_REPLACE_TEXT);
+ return;
+
+ } else {
+ // Gecko side initiated the text change. Replace in two steps to properly
+ // clear composing spans that span the whole range.
+ mText.currentReplace(start, oldEnd, "");
+ mText.currentReplace(start, start, text);
+
+ // Don't ignore the next selection change because we are forced to re-sync
+ // with Gecko here.
+ mIgnoreSelectionChange = false;
+ }
+
+ // onTextChange is always followed by onSelectionChange, so we let
+ // onSelectionChange schedule a shadow text sync.
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void onDefaultKeyEvent(final KeyEvent event) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ StringBuilder sb = new StringBuilder("onDefaultKeyEvent(");
+ sb.append("action=").append(event.getAction()).append(", ")
+ .append("keyCode=").append(event.getKeyCode()).append(", ")
+ .append("metaState=").append(event.getMetaState()).append(", ")
+ .append("time=").append(event.getEventTime()).append(", ")
+ .append("repeatCount=").append(event.getRepeatCount()).append(")");
+ Log.d(LOGTAG, sb.toString());
+ }
+
+ geckoPostToIc(new Runnable() {
+ @Override
+ public void run() {
+ if (mListener == null) {
+ return;
+ }
+ mListener.onDefaultKeyEvent(event);
+ }
+ });
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void updateCompositionRects(final RectF[] aRects) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ Log.d(LOGTAG, "updateCompositionRects(aRects.length = " + aRects.length + ")");
+ }
+ geckoPostToIc(new Runnable() {
+ @Override
+ public void run() {
+ if (mListener == null) {
+ return;
+ }
+ mListener.updateCompositionRects(aRects);
+ }
+ });
+ }
+
+ // InvocationHandler interface
+
+ static String getConstantName(Class<?> cls, String prefix, Object value) {
+ for (Field fld : cls.getDeclaredFields()) {
+ try {
+ if (fld.getName().startsWith(prefix) &&
+ fld.get(null).equals(value)) {
+ return fld.getName();
+ }
+ } catch (IllegalAccessException e) {
+ }
+ }
+ return String.valueOf(value);
+ }
+
+ static StringBuilder debugAppend(StringBuilder sb, Object obj) {
+ if (obj == null) {
+ sb.append("null");
+ } else if (obj instanceof GeckoEditable) {
+ sb.append("GeckoEditable");
+ } else if (Proxy.isProxyClass(obj.getClass())) {
+ debugAppend(sb, Proxy.getInvocationHandler(obj));
+ } else if (obj instanceof CharSequence) {
+ sb.append('"').append(obj.toString().replace('\n', '\u21b2')).append('"');
+ } else if (obj.getClass().isArray()) {
+ sb.append(obj.getClass().getComponentType().getSimpleName()).append('[')
+ .append(Array.getLength(obj)).append(']');
+ } else {
+ sb.append(obj);
+ }
+ return sb;
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args)
+ throws Throwable {
+ Object target;
+ final Class<?> methodInterface = method.getDeclaringClass();
+ if (DEBUG) {
+ // Editable methods should all be called from the IC thread
+ assertOnIcThread();
+ }
+ if (methodInterface == Editable.class ||
+ methodInterface == Appendable.class ||
+ methodInterface == Spannable.class) {
+ // Method alters the Editable; route calls to our implementation
+ target = this;
+ } else {
+ target = mText.getShadowText();
+ }
+ Object ret;
+ try {
+ ret = method.invoke(target, args);
+ } catch (InvocationTargetException e) {
+ // Bug 817386
+ // Most likely Gecko has changed the text while GeckoInputConnection is
+ // trying to access the text. If we pass through the exception here, Fennec
+ // will crash due to a lack of exception handler. Log the exception and
+ // return an empty value instead.
+ if (!(e.getCause() instanceof IndexOutOfBoundsException)) {
+ // Only handle IndexOutOfBoundsException for now,
+ // as other exceptions might signal other bugs
+ throw e;
+ }
+ Log.w(LOGTAG, "Exception in GeckoEditable." + method.getName(), e.getCause());
+ Class<?> retClass = method.getReturnType();
+ if (retClass == Character.TYPE) {
+ ret = '\0';
+ } else if (retClass == Integer.TYPE) {
+ ret = 0;
+ } else if (retClass == String.class) {
+ ret = "";
+ } else {
+ ret = null;
+ }
+ }
+ if (DEBUG) {
+ StringBuilder log = new StringBuilder(method.getName());
+ log.append("(");
+ if (args != null) {
+ for (Object arg : args) {
+ debugAppend(log, arg).append(", ");
+ }
+ if (args.length > 0) {
+ log.setLength(log.length() - 2);
+ }
+ }
+ if (method.getReturnType().equals(Void.TYPE)) {
+ log.append(")");
+ } else {
+ debugAppend(log.append(") = "), ret);
+ }
+ Log.d(LOGTAG, log.toString());
+ }
+ return ret;
+ }
+
+ // Spannable interface
+
+ @Override
+ public void removeSpan(Object what) {
+ if (what == null) {
+ return;
+ }
+
+ if (what == Selection.SELECTION_START ||
+ what == Selection.SELECTION_END) {
+ Log.w(LOGTAG, "selection removed with removeSpan()");
+ }
+
+ icOfferAction(Action.newRemoveSpan(what));
+ }
+
+ @Override
+ public void setSpan(Object what, int start, int end, int flags) {
+ icOfferAction(Action.newSetSpan(what, start, end, flags));
+ }
+
+ // Appendable interface
+
+ @Override
+ public Editable append(CharSequence text) {
+ return replace(mProxy.length(), mProxy.length(), text, 0, text.length());
+ }
+
+ @Override
+ public Editable append(CharSequence text, int start, int end) {
+ return replace(mProxy.length(), mProxy.length(), text, start, end);
+ }
+
+ @Override
+ public Editable append(char text) {
+ return replace(mProxy.length(), mProxy.length(), String.valueOf(text), 0, 1);
+ }
+
+ // Editable interface
+
+ @Override
+ public InputFilter[] getFilters() {
+ return mFilters;
+ }
+
+ @Override
+ public void setFilters(InputFilter[] filters) {
+ mFilters = filters;
+ }
+
+ @Override
+ public void clearSpans() {
+ /* XXX this clears the selection spans too,
+ but there is no way to clear the corresponding selection in Gecko */
+ Log.w(LOGTAG, "selection cleared with clearSpans()");
+ icOfferAction(Action.newRemoveSpan(/* what */ null));
+ }
+
+ @Override
+ public Editable replace(int st, int en,
+ CharSequence source, int start, int end) {
+
+ CharSequence text = source;
+ if (start < 0 || start > end || end > text.length()) {
+ Log.e(LOGTAG, "invalid replace offsets: " +
+ start + " to " + end + ", length: " + text.length());
+ throw new IllegalArgumentException("invalid replace offsets");
+ }
+ if (start != 0 || end != text.length()) {
+ text = text.subSequence(start, end);
+ }
+ if (mFilters != null) {
+ // Filter text before sending the request to Gecko
+ for (int i = 0; i < mFilters.length; ++i) {
+ final CharSequence cs = mFilters[i].filter(
+ text, 0, text.length(), mProxy, st, en);
+ if (cs != null) {
+ text = cs;
+ }
+ }
+ }
+ if (text == source) {
+ // Always create a copy
+ text = new SpannableString(source);
+ }
+ icOfferAction(Action.newReplaceText(text, Math.min(st, en), Math.max(st, en)));
+ return mProxy;
+ }
+
+ @Override
+ public void clear() {
+ replace(0, mProxy.length(), "", 0, 0);
+ }
+
+ @Override
+ public Editable delete(int st, int en) {
+ return replace(st, en, "", 0, 0);
+ }
+
+ @Override
+ public Editable insert(int where, CharSequence text,
+ int start, int end) {
+ return replace(where, where, text, start, end);
+ }
+
+ @Override
+ public Editable insert(int where, CharSequence text) {
+ return replace(where, where, text, 0, text.length());
+ }
+
+ @Override
+ public Editable replace(int st, int en, CharSequence text) {
+ return replace(st, en, text, 0, text.length());
+ }
+
+ /* GetChars interface */
+
+ @Override
+ public void getChars(int start, int end, char[] dest, int destoff) {
+ /* overridden Editable interface methods in GeckoEditable must not be called directly
+ outside of GeckoEditable. Instead, the call must go through mProxy, which ensures
+ that Java is properly synchronized with Gecko */
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ /* Spanned interface */
+
+ @Override
+ public int getSpanEnd(Object tag) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public int getSpanFlags(Object tag) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public int getSpanStart(Object tag) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public <T> T[] getSpans(int start, int end, Class<T> type) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ @SuppressWarnings("rawtypes") // nextSpanTransition uses raw Class in its Android declaration
+ public int nextSpanTransition(int start, int limit, Class type) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ /* CharSequence interface */
+
+ @Override
+ public char charAt(int index) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public int length() {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public CharSequence subSequence(int start, int end) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public String toString() {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+}
+
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableClient.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableClient.java
new file mode 100644
index 0000000000..5e721b3af2
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableClient.java
@@ -0,0 +1,33 @@
+/* -*- 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.Handler;
+import android.text.Editable;
+import android.view.KeyEvent;
+
+/**
+ * Interface for the IC thread.
+ */
+interface GeckoEditableClient {
+ void sendKeyEvent(KeyEvent event, int action, int metaState);
+ Editable getEditable();
+ void setBatchMode(boolean isBatchMode);
+ void setSuppressKeyUp(boolean suppress);
+ Handler setInputConnectionHandler(Handler handler);
+ void postToInputConnection(Runnable runnable);
+
+ // The following value is used by requestCursorUpdates
+
+ // ONE_SHOT calls updateCompositionRects() after getting current composing character rects.
+ public static final int ONE_SHOT = 1;
+ // START_MONITOR start the monitor for composing character rects. If is is updaed, call updateCompositionRects()
+ public static final int START_MONITOR = 2;
+ // ENDT_MONITOR stops the monitor for composing character rects.
+ public static final int END_MONITOR = 3;
+
+ void requestCursorUpdates(int requestMode);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableListener.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableListener.java
new file mode 100644
index 0000000000..db594aaf77
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableListener.java
@@ -0,0 +1,43 @@
+/* -*- 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 android.graphics.RectF;
+import android.view.KeyEvent;
+
+/**
+ * Interface for the Editable to listen on the Gecko thread, as well as for the IC thread to listen
+ * to the Editable.
+ */
+interface GeckoEditableListener {
+ // IME notification type for notifyIME(), corresponding to NotificationToIME enum in Gecko
+ @WrapForJNI
+ int NOTIFY_IME_OPEN_VKB = -2;
+ @WrapForJNI
+ int NOTIFY_IME_REPLY_EVENT = -1;
+ @WrapForJNI
+ int NOTIFY_IME_OF_FOCUS = 1;
+ @WrapForJNI
+ int NOTIFY_IME_OF_BLUR = 2;
+ @WrapForJNI
+ int NOTIFY_IME_TO_COMMIT_COMPOSITION = 8;
+ @WrapForJNI
+ int NOTIFY_IME_TO_CANCEL_COMPOSITION = 9;
+ // IME enabled state for notifyIMEContext()
+ int IME_STATE_DISABLED = 0;
+ int IME_STATE_ENABLED = 1;
+ int IME_STATE_PASSWORD = 2;
+ int IME_STATE_PLUGIN = 3;
+
+ void notifyIME(int type);
+ void notifyIMEContext(int state, String typeHint, String modeHint, String actionHint);
+ void onSelectionChange();
+ void onTextChange();
+ void onDefaultKeyEvent(KeyEvent event);
+ void updateCompositionRects(final RectF[] aRects);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoHalDefines.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoHalDefines.java
new file mode 100644
index 0000000000..3d9b974279
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoHalDefines.java
@@ -0,0 +1,27 @@
+/* -*- 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;
+
+public class GeckoHalDefines
+{
+ /*
+ * Keep these values consistent with |SensorType| in HalSensor.h
+ */
+ public static final int SENSOR_ORIENTATION = 0;
+ public static final int SENSOR_ACCELERATION = 1;
+ public static final int SENSOR_PROXIMITY = 2;
+ public static final int SENSOR_LINEAR_ACCELERATION = 3;
+ public static final int SENSOR_GYROSCOPE = 4;
+ public static final int SENSOR_LIGHT = 5;
+ public static final int SENSOR_ROTATION_VECTOR = 6;
+ public static final int SENSOR_GAME_ROTATION_VECTOR = 7;
+
+ public static final int SENSOR_ACCURACY_UNKNOWN = -1;
+ public static final int SENSOR_ACCURACY_UNRELIABLE = 0;
+ public static final int SENSOR_ACCURACY_LOW = 1;
+ public static final int SENSOR_ACCURACY_MED = 2;
+ public static final int SENSOR_ACCURACY_HIGH = 3;
+};
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java
new file mode 100644
index 0000000000..a80be0bce7
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java
@@ -0,0 +1,1060 @@
+/* -*- 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.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.concurrent.SynchronousQueue;
+
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
+import org.mozilla.gecko.util.Clipboard;
+import org.mozilla.gecko.util.GamepadUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.ThreadUtils.AssertBehavior;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.media.AudioManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.Selection;
+import android.text.SpannableString;
+import android.text.method.KeyListener;
+import android.text.method.TextKeyListener;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.CursorAnchorInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodManager;
+
+class GeckoInputConnection
+ extends BaseInputConnection
+ implements InputConnectionListener, GeckoEditableListener {
+
+ private static final boolean DEBUG = false;
+ protected static final String LOGTAG = "GeckoInputConnection";
+
+ private static final String CUSTOM_HANDLER_TEST_METHOD = "testInputConnection";
+ private static final String CUSTOM_HANDLER_TEST_CLASS =
+ "org.mozilla.gecko.tests.components.GeckoViewComponent$TextInput";
+
+ private static final int INLINE_IME_MIN_DISPLAY_SIZE = 480;
+
+ private static Handler sBackgroundHandler;
+
+ // Managed only by notifyIMEContext; see comments in notifyIMEContext
+ private int mIMEState;
+ private String mIMETypeHint = "";
+ private String mIMEModeHint = "";
+ private String mIMEActionHint = "";
+ private boolean mFocused;
+
+ private String mCurrentInputMethod = "";
+
+ private final View mView;
+ private final GeckoEditableClient mEditableClient;
+ protected int mBatchEditCount;
+ private ExtractedTextRequest mUpdateRequest;
+ private final ExtractedText mUpdateExtract = new ExtractedText();
+ private final InputConnection mKeyInputConnection;
+ private CursorAnchorInfo.Builder mCursorAnchorInfoBuilder;
+
+ // Prevent showSoftInput and hideSoftInput from causing reentrant calls on some devices.
+ private volatile boolean mSoftInputReentrancyGuard;
+
+ public static GeckoEditableListener create(View targetView,
+ GeckoEditableClient editable) {
+ if (DEBUG)
+ return DebugGeckoInputConnection.create(targetView, editable);
+ else
+ return new GeckoInputConnection(targetView, editable);
+ }
+
+ protected GeckoInputConnection(View targetView,
+ GeckoEditableClient editable) {
+ super(targetView, true);
+ mView = targetView;
+ mEditableClient = editable;
+ mIMEState = IME_STATE_DISABLED;
+ // InputConnection that sends keys for plugins, which don't have full editors
+ mKeyInputConnection = new BaseInputConnection(targetView, false);
+ }
+
+ @Override
+ public synchronized boolean beginBatchEdit() {
+ mBatchEditCount++;
+ if (mBatchEditCount == 1) {
+ mEditableClient.setBatchMode(true);
+ }
+ return true;
+ }
+
+ @Override
+ public synchronized boolean endBatchEdit() {
+ if (mBatchEditCount <= 0) {
+ Log.w(LOGTAG, "endBatchEdit() called, but mBatchEditCount <= 0?!");
+ return true;
+ }
+
+ mBatchEditCount--;
+ if (mBatchEditCount != 0) {
+ return true;
+ }
+
+ // setBatchMode will call onTextChange and/or onSelectionChange for us.
+ mEditableClient.setBatchMode(false);
+ return true;
+ }
+
+ @Override
+ public Editable getEditable() {
+ return mEditableClient.getEditable();
+ }
+
+ @Override
+ public boolean performContextMenuAction(int id) {
+ Editable editable = getEditable();
+ if (editable == null) {
+ return false;
+ }
+ int selStart = Selection.getSelectionStart(editable);
+ int selEnd = Selection.getSelectionEnd(editable);
+
+ switch (id) {
+ case android.R.id.selectAll:
+ setSelection(0, editable.length());
+ break;
+ case android.R.id.cut:
+ // If selection is empty, we'll select everything
+ if (selStart == selEnd) {
+ // Fill the clipboard
+ Clipboard.setText(editable);
+ editable.clear();
+ } else {
+ Clipboard.setText(
+ editable.toString().substring(
+ Math.min(selStart, selEnd),
+ Math.max(selStart, selEnd)));
+ editable.delete(selStart, selEnd);
+ }
+ break;
+ case android.R.id.paste:
+ commitText(Clipboard.getText(), 1);
+ break;
+ case android.R.id.copy:
+ // Copy the current selection or the empty string if nothing is selected.
+ String copiedText = selStart == selEnd ? "" :
+ editable.toString().substring(
+ Math.min(selStart, selEnd),
+ Math.max(selStart, selEnd));
+ Clipboard.setText(copiedText);
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ public ExtractedText getExtractedText(ExtractedTextRequest req, int flags) {
+ if (req == null)
+ return null;
+
+ if ((flags & GET_EXTRACTED_TEXT_MONITOR) != 0)
+ mUpdateRequest = req;
+
+ Editable editable = getEditable();
+ if (editable == null) {
+ return null;
+ }
+ int selStart = Selection.getSelectionStart(editable);
+ int selEnd = Selection.getSelectionEnd(editable);
+
+ ExtractedText extract = new ExtractedText();
+ extract.flags = 0;
+ extract.partialStartOffset = -1;
+ extract.partialEndOffset = -1;
+ extract.selectionStart = selStart;
+ extract.selectionEnd = selEnd;
+ extract.startOffset = 0;
+ if ((req.flags & GET_TEXT_WITH_STYLES) != 0) {
+ extract.text = new SpannableString(editable);
+ } else {
+ extract.text = editable.toString();
+ }
+ return extract;
+ }
+
+ private View getView() {
+ return mView;
+ }
+
+ private InputMethodManager getInputMethodManager() {
+ View view = getView();
+ if (view == null) {
+ return null;
+ }
+ Context context = view.getContext();
+ return InputMethods.getInputMethodManager(context);
+ }
+
+ private void showSoftInput() {
+ if (mSoftInputReentrancyGuard) {
+ return;
+ }
+ final View v = getView();
+ final InputMethodManager imm = getInputMethodManager();
+ if (v == null || imm == null) {
+ return;
+ }
+
+ v.post(new Runnable() {
+ @Override
+ public void run() {
+ if (v.hasFocus() && !imm.isActive(v)) {
+ // Marshmallow workaround: The view has focus but it is not the active
+ // view for the input method. (Bug 1211848)
+ v.clearFocus();
+ v.requestFocus();
+ }
+ GeckoAppShell.getLayerView().getDynamicToolbarAnimator().showToolbar(/*immediately*/true);
+ mSoftInputReentrancyGuard = true;
+ imm.showSoftInput(v, 0);
+ mSoftInputReentrancyGuard = false;
+ }
+ });
+ }
+
+ private void hideSoftInput() {
+ if (mSoftInputReentrancyGuard) {
+ return;
+ }
+ final InputMethodManager imm = getInputMethodManager();
+ if (imm != null) {
+ final View v = getView();
+ mSoftInputReentrancyGuard = true;
+ imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
+ mSoftInputReentrancyGuard = false;
+ }
+ }
+
+ private void restartInput() {
+
+ final InputMethodManager imm = getInputMethodManager();
+ if (imm == null) {
+ return;
+ }
+ final View v = getView();
+ // InputMethodManager has internal logic to detect if we are restarting input
+ // in an already focused View, which is the case here because all content text
+ // fields are inside one LayerView. When this happens, InputMethodManager will
+ // tell the input method to soft reset instead of hard reset. Stock latin IME
+ // on Android 4.2+ has a quirk that when it soft resets, it does not clear the
+ // composition. The following workaround tricks the IME into clearing the
+ // composition when soft resetting.
+ if (InputMethods.needsSoftResetWorkaround(mCurrentInputMethod)) {
+ // Fake a selection change, because the IME clears the composition when
+ // the selection changes, even if soft-resetting. Offsets here must be
+ // different from the previous selection offsets, and -1 seems to be a
+ // reasonable, deterministic value
+ notifySelectionChange(-1, -1);
+ }
+ try {
+ imm.restartInput(v);
+ } catch (RuntimeException e) {
+ Log.e(LOGTAG, "Error restarting input", e);
+ }
+ }
+
+ private void resetInputConnection() {
+ if (mBatchEditCount != 0) {
+ Log.w(LOGTAG, "resetting with mBatchEditCount = " + mBatchEditCount);
+ mBatchEditCount = 0;
+ }
+
+ // Do not reset mIMEState here; see comments in notifyIMEContext
+
+ restartInput();
+ }
+
+ @Override // GeckoEditableListener
+ public void onTextChange() {
+
+ if (mUpdateRequest == null) {
+ return;
+ }
+
+ final InputMethodManager imm = getInputMethodManager();
+ final View v = getView();
+ final Editable editable = getEditable();
+ if (imm == null || v == null || editable == null) {
+ return;
+ }
+ mUpdateExtract.flags = 0;
+ // Update the entire Editable range
+ mUpdateExtract.partialStartOffset = -1;
+ mUpdateExtract.partialEndOffset = -1;
+ mUpdateExtract.selectionStart = Selection.getSelectionStart(editable);
+ mUpdateExtract.selectionEnd = Selection.getSelectionEnd(editable);
+ mUpdateExtract.startOffset = 0;
+ if ((mUpdateRequest.flags & GET_TEXT_WITH_STYLES) != 0) {
+ mUpdateExtract.text = new SpannableString(editable);
+ } else {
+ mUpdateExtract.text = editable.toString();
+ }
+ imm.updateExtractedText(v, mUpdateRequest.token, mUpdateExtract);
+ }
+
+ @Override // GeckoEditableListener
+ public void onSelectionChange() {
+
+ final Editable editable = getEditable();
+ if (editable != null) {
+ notifySelectionChange(Selection.getSelectionStart(editable),
+ Selection.getSelectionEnd(editable));
+ }
+ }
+
+ private void notifySelectionChange(int start, int end) {
+
+ final InputMethodManager imm = getInputMethodManager();
+ final View v = getView();
+ final Editable editable = getEditable();
+ if (imm == null || v == null || editable == null) {
+ return;
+ }
+ imm.updateSelection(v, start, end, getComposingSpanStart(editable),
+ getComposingSpanEnd(editable));
+ }
+
+ @Override
+ public void updateCompositionRects(final RectF[] aRects) {
+ if (!Versions.feature21Plus) {
+ return;
+ }
+
+ if (mCursorAnchorInfoBuilder == null) {
+ mCursorAnchorInfoBuilder = new CursorAnchorInfo.Builder();
+ }
+ mCursorAnchorInfoBuilder.reset();
+
+ // Calculate Gecko logical coords to screen coords
+ final View v = getView();
+ if (v == null) {
+ return;
+ }
+
+ int[] viewCoords = new int[2];
+ v.getLocationOnScreen(viewCoords);
+
+ DynamicToolbarAnimator animator = GeckoAppShell.getLayerView().getDynamicToolbarAnimator();
+ float toolbarHeight = animator.getMaxTranslation() - animator.getToolbarTranslation();
+
+ Matrix matrix = GeckoAppShell.getLayerView().getMatrixForLayerRectToViewRect();
+ if (matrix == null) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "Cannot get Matrix to convert from Gecko coords to layer view coords");
+ }
+ return;
+ }
+ matrix.postTranslate(viewCoords[0], viewCoords[1] + toolbarHeight);
+ mCursorAnchorInfoBuilder.setMatrix(matrix);
+
+ final Editable content = getEditable();
+ if (content == null) {
+ return;
+ }
+ int composingStart = getComposingSpanStart(content);
+ int composingEnd = getComposingSpanEnd(content);
+ if (composingStart < 0 || composingEnd < 0) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "No composition for updates");
+ }
+ return;
+ }
+
+ for (int i = 0; i < aRects.length; i++) {
+ mCursorAnchorInfoBuilder.addCharacterBounds(i,
+ aRects[i].left,
+ aRects[i].top,
+ aRects[i].right,
+ aRects[i].bottom,
+ CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION);
+ }
+
+ mCursorAnchorInfoBuilder.setComposingText(0, content.subSequence(composingStart, composingEnd));
+
+ updateCursor();
+ }
+
+ @TargetApi(21)
+ private void updateCursor() {
+ if (mCursorAnchorInfoBuilder == null) {
+ return;
+ }
+
+ final InputMethodManager imm = getInputMethodManager();
+ final View v = getView();
+ if (imm == null || v == null) {
+ return;
+ }
+
+ imm.updateCursorAnchorInfo(v, mCursorAnchorInfoBuilder.build());
+ }
+
+ @Override
+ public boolean requestCursorUpdates(int cursorUpdateMode) {
+
+ if ((cursorUpdateMode & InputConnection.CURSOR_UPDATE_IMMEDIATE) != 0) {
+ mEditableClient.requestCursorUpdates(GeckoEditableClient.ONE_SHOT);
+ }
+
+ if ((cursorUpdateMode & InputConnection.CURSOR_UPDATE_MONITOR) != 0) {
+ mEditableClient.requestCursorUpdates(GeckoEditableClient.START_MONITOR);
+ } else {
+ mEditableClient.requestCursorUpdates(GeckoEditableClient.END_MONITOR);
+ }
+ return true;
+ }
+
+ @Override
+ public void onDefaultKeyEvent(final KeyEvent event) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ GeckoInputConnection.this.performDefaultKeyAction(event);
+ }
+ });
+ }
+
+ private static synchronized Handler getBackgroundHandler() {
+ if (sBackgroundHandler != null) {
+ return sBackgroundHandler;
+ }
+ // Don't use GeckoBackgroundThread because Gecko thread may block waiting on
+ // GeckoBackgroundThread. If we were to use GeckoBackgroundThread, due to IME,
+ // GeckoBackgroundThread may end up also block waiting on Gecko thread and a
+ // deadlock occurs
+ Thread backgroundThread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ Looper.prepare();
+ synchronized (GeckoInputConnection.class) {
+ sBackgroundHandler = new Handler();
+ GeckoInputConnection.class.notify();
+ }
+ Looper.loop();
+ // We should never be exiting the thread loop.
+ throw new IllegalThreadStateException("unreachable code");
+ }
+ }, LOGTAG);
+ backgroundThread.setDaemon(true);
+ backgroundThread.start();
+ while (sBackgroundHandler == null) {
+ try {
+ // wait for new thread to set sBackgroundHandler
+ GeckoInputConnection.class.wait();
+ } catch (InterruptedException e) {
+ }
+ }
+ return sBackgroundHandler;
+ }
+
+ private boolean canReturnCustomHandler() {
+ if (mIMEState == IME_STATE_DISABLED) {
+ return false;
+ }
+ for (StackTraceElement frame : Thread.currentThread().getStackTrace()) {
+ // We only return our custom Handler to InputMethodManager's InputConnection
+ // proxy. For all other purposes, we return the regular Handler.
+ // InputMethodManager retrieves the Handler for its InputConnection proxy
+ // inside its method startInputInner(), so we check for that here. This is
+ // valid from Android 2.2 to at least Android 4.2. If this situation ever
+ // changes, we gracefully fall back to using the regular Handler.
+ if ("startInputInner".equals(frame.getMethodName()) &&
+ "android.view.inputmethod.InputMethodManager".equals(frame.getClassName())) {
+ // only return our own Handler to InputMethodManager
+ return true;
+ }
+ if (CUSTOM_HANDLER_TEST_METHOD.equals(frame.getMethodName()) &&
+ CUSTOM_HANDLER_TEST_CLASS.equals(frame.getClassName())) {
+ // InputConnection tests should also run on the custom handler
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isPhysicalKeyboardPresent() {
+ final View v = getView();
+ if (v == null) {
+ return false;
+ }
+ final Configuration config = v.getContext().getResources().getConfiguration();
+ return config.keyboard != Configuration.KEYBOARD_NOKEYS;
+ }
+
+ // Android N: @Override // InputConnection
+ // We need to suppress lint complaining about the lack override here in the meantime: it wants us to build
+ // against sdk 24, even though we're using 23, and therefore complains about the lack of override.
+ // Once we update to 24, we can use the actual override annotation and remove the lint suppression.
+ @SuppressLint("Override")
+ public Handler getHandler() {
+ if (isPhysicalKeyboardPresent()) {
+ return ThreadUtils.getUiHandler();
+ }
+
+ return getBackgroundHandler();
+ }
+
+ @Override // InputConnectionListener
+ public Handler getHandler(Handler defHandler) {
+ if (!canReturnCustomHandler()) {
+ return defHandler;
+ }
+
+ return mEditableClient.setInputConnectionHandler(getHandler());
+ }
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ // Some keyboards require us to fill out outAttrs even if we return null.
+ outAttrs.inputType = InputType.TYPE_CLASS_TEXT;
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_NONE;
+ outAttrs.actionLabel = null;
+
+ if (mIMEState == IME_STATE_DISABLED) {
+ hideSoftInput();
+ return null;
+ }
+
+ if (mIMEState == IME_STATE_PASSWORD ||
+ "password".equalsIgnoreCase(mIMETypeHint))
+ outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_PASSWORD;
+ else if (mIMEState == IME_STATE_PLUGIN)
+ outAttrs.inputType = InputType.TYPE_NULL; // "send key events" mode
+ else if (mIMETypeHint.equalsIgnoreCase("url"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_URI;
+ else if (mIMETypeHint.equalsIgnoreCase("email"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
+ else if (mIMETypeHint.equalsIgnoreCase("tel"))
+ outAttrs.inputType = InputType.TYPE_CLASS_PHONE;
+ else if (mIMETypeHint.equalsIgnoreCase("number") ||
+ mIMETypeHint.equalsIgnoreCase("range"))
+ outAttrs.inputType = InputType.TYPE_CLASS_NUMBER
+ | InputType.TYPE_NUMBER_FLAG_SIGNED
+ | InputType.TYPE_NUMBER_FLAG_DECIMAL;
+ else if (mIMETypeHint.equalsIgnoreCase("week") ||
+ mIMETypeHint.equalsIgnoreCase("month"))
+ outAttrs.inputType = InputType.TYPE_CLASS_DATETIME
+ | InputType.TYPE_DATETIME_VARIATION_DATE;
+ else if (mIMEModeHint.equalsIgnoreCase("numeric"))
+ outAttrs.inputType = InputType.TYPE_CLASS_NUMBER |
+ InputType.TYPE_NUMBER_FLAG_SIGNED |
+ InputType.TYPE_NUMBER_FLAG_DECIMAL;
+ else if (mIMEModeHint.equalsIgnoreCase("digit"))
+ outAttrs.inputType = InputType.TYPE_CLASS_NUMBER;
+ else {
+ // TYPE_TEXT_FLAG_IME_MULTI_LINE flag makes the fullscreen IME line wrap
+ outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT |
+ InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE;
+ if (mIMETypeHint.equalsIgnoreCase("textarea") ||
+ mIMETypeHint.length() == 0) {
+ // empty mIMETypeHint indicates contentEditable/designMode documents
+ outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE;
+ }
+ if (mIMEModeHint.equalsIgnoreCase("uppercase"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
+ else if (mIMEModeHint.equalsIgnoreCase("titlecase"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
+ else if (mIMETypeHint.equalsIgnoreCase("text") &&
+ !mIMEModeHint.equalsIgnoreCase("autocapitalized"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_NORMAL;
+ else if (!mIMEModeHint.equalsIgnoreCase("lowercase"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
+ // auto-capitalized mode is the default for types other than text
+ }
+
+ if (mIMEActionHint.equalsIgnoreCase("go"))
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_GO;
+ else if (mIMEActionHint.equalsIgnoreCase("done"))
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE;
+ else if (mIMEActionHint.equalsIgnoreCase("next"))
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_NEXT;
+ else if (mIMEActionHint.equalsIgnoreCase("search") ||
+ mIMETypeHint.equalsIgnoreCase("search"))
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_SEARCH;
+ else if (mIMEActionHint.equalsIgnoreCase("send"))
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_SEND;
+ else if (mIMEActionHint.length() > 0) {
+ if (DEBUG)
+ Log.w(LOGTAG, "Unexpected mIMEActionHint=\"" + mIMEActionHint + "\"");
+ outAttrs.actionLabel = mIMEActionHint;
+ }
+
+ Context context = getView().getContext();
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ if (Math.min(metrics.widthPixels, metrics.heightPixels) > INLINE_IME_MIN_DISPLAY_SIZE) {
+ // prevent showing full-screen keyboard only when the screen is tall enough
+ // to show some reasonable amount of the page (see bug 752709)
+ outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI
+ | EditorInfo.IME_FLAG_NO_FULLSCREEN;
+ }
+
+ if (DEBUG) {
+ Log.d(LOGTAG, "mapped IME states to: inputType = " +
+ Integer.toHexString(outAttrs.inputType) + ", imeOptions = " +
+ Integer.toHexString(outAttrs.imeOptions));
+ }
+
+ String prevInputMethod = mCurrentInputMethod;
+ mCurrentInputMethod = InputMethods.getCurrentInputMethod(context);
+ if (DEBUG) {
+ Log.d(LOGTAG, "IME: CurrentInputMethod=" + mCurrentInputMethod);
+ }
+
+ if (mIMEState == IME_STATE_PLUGIN) {
+ // Since we are using a temporary string as the editable, the selection is at 0
+ outAttrs.initialSelStart = 0;
+ outAttrs.initialSelEnd = 0;
+ return mKeyInputConnection;
+ }
+ Editable editable = getEditable();
+ outAttrs.initialSelStart = Selection.getSelectionStart(editable);
+ outAttrs.initialSelEnd = Selection.getSelectionEnd(editable);
+
+ showSoftInput();
+ return this;
+ }
+
+ private boolean replaceComposingSpanWithSelection() {
+ final Editable content = getEditable();
+ if (content == null) {
+ return false;
+ }
+ int a = getComposingSpanStart(content),
+ b = getComposingSpanEnd(content);
+ if (a != -1 && b != -1) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "removing composition at " + a + "-" + b);
+ }
+ removeComposingSpans(content);
+ Selection.setSelection(content, a, b);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean commitText(CharSequence text, int newCursorPosition) {
+ if (InputMethods.shouldCommitCharAsKey(mCurrentInputMethod) &&
+ text.length() == 1 && newCursorPosition > 0) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "committing \"" + text + "\" as key");
+ }
+ // mKeyInputConnection is a BaseInputConnection that commits text as keys;
+ // but we first need to replace any composing span with a selection,
+ // so that the new key events will generate characters to replace
+ // text from the old composing span
+ return replaceComposingSpanWithSelection() &&
+ mKeyInputConnection.commitText(text, newCursorPosition);
+ }
+ return super.commitText(text, newCursorPosition);
+ }
+
+ @Override
+ public boolean setSelection(int start, int end) {
+ if (start < 0 || end < 0) {
+ // Some keyboards (e.g. Samsung) can call setSelection with
+ // negative offsets. In that case we ignore the call, similar to how
+ // BaseInputConnection.setSelection ignores offsets that go past the length.
+ return true;
+ }
+ return super.setSelection(start, end);
+ }
+
+ /* package */ void sendKeyEvent(final int action, KeyEvent event) {
+ final Editable editable = getEditable();
+ if (editable == null) {
+ return;
+ }
+
+ final KeyListener keyListener = TextKeyListener.getInstance();
+ event = translateKey(event.getKeyCode(), event);
+
+ // We only let TextKeyListener do UI things on the UI thread.
+ final View v = ThreadUtils.isOnUiThread() ? getView() : null;
+ final int keyCode = event.getKeyCode();
+ final boolean handled;
+
+ if (shouldSkipKeyListener(keyCode, event)) {
+ handled = false;
+ } else if (action == KeyEvent.ACTION_DOWN) {
+ mEditableClient.setSuppressKeyUp(true);
+ handled = keyListener.onKeyDown(v, editable, keyCode, event);
+ } else if (action == KeyEvent.ACTION_UP) {
+ handled = keyListener.onKeyUp(v, editable, keyCode, event);
+ } else {
+ handled = keyListener.onKeyOther(v, editable, event);
+ }
+
+ if (!handled) {
+ mEditableClient.sendKeyEvent(event, action, TextKeyListener.getMetaState(editable));
+ }
+
+ if (action == KeyEvent.ACTION_DOWN) {
+ if (!handled) {
+ // Usually, the down key listener call above adjusts meta states for us.
+ // However, if the call didn't handle the event, we have to manually
+ // adjust meta states so the meta states remain consistent.
+ TextKeyListener.adjustMetaAfterKeypress(editable);
+ }
+ mEditableClient.setSuppressKeyUp(false);
+ }
+ }
+
+ @Override
+ public boolean sendKeyEvent(KeyEvent event) {
+ sendKeyEvent(event.getAction(), event);
+ return false; // seems to always return false
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ private boolean shouldProcessKey(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_MENU:
+ case KeyEvent.KEYCODE_BACK:
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ case KeyEvent.KEYCODE_SEARCH:
+ // ignore HEADSETHOOK to allow hold-for-voice-search to work
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ return false;
+ }
+ return true;
+ }
+
+ private boolean shouldSkipKeyListener(int keyCode, KeyEvent event) {
+ if (mIMEState == IME_STATE_DISABLED ||
+ mIMEState == IME_STATE_PLUGIN) {
+ return true;
+ }
+ // Preserve enter and tab keys for the browser
+ if (keyCode == KeyEvent.KEYCODE_ENTER ||
+ keyCode == KeyEvent.KEYCODE_TAB) {
+ return true;
+ }
+ // BaseKeyListener returns false even if it handled these keys for us,
+ // so we skip the key listener entirely and handle these ourselves
+ if (keyCode == KeyEvent.KEYCODE_DEL ||
+ keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
+ return true;
+ }
+ return false;
+ }
+
+ private KeyEvent translateKey(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ENTER:
+ if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0 &&
+ mIMEActionHint.equalsIgnoreCase("next")) {
+ return new KeyEvent(event.getAction(), KeyEvent.KEYCODE_TAB);
+ }
+ break;
+ }
+
+ if (GamepadUtils.isSonyXperiaGamepadKeyEvent(event)) {
+ return GamepadUtils.translateSonyXperiaGamepadKeys(keyCode, event);
+ }
+
+ return event;
+ }
+
+ // Called by OnDefaultKeyEvent handler, up from Gecko
+ /* package */ void performDefaultKeyAction(KeyEvent event) {
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_MUTE:
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ case KeyEvent.KEYCODE_MEDIA_PLAY:
+ case KeyEvent.KEYCODE_MEDIA_PAUSE:
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ case KeyEvent.KEYCODE_MEDIA_STOP:
+ case KeyEvent.KEYCODE_MEDIA_NEXT:
+ case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+ case KeyEvent.KEYCODE_MEDIA_REWIND:
+ case KeyEvent.KEYCODE_MEDIA_RECORD:
+ case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
+ case KeyEvent.KEYCODE_MEDIA_CLOSE:
+ case KeyEvent.KEYCODE_MEDIA_EJECT:
+ case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK:
+ // Forward media keypresses to the registered handler so headset controls work
+ // Does the same thing as Chromium
+ // https://chromium.googlesource.com/chromium/src/+/49.0.2623.67/chrome/android/java/src/org/chromium/chrome/browser/tab/TabWebContentsDelegateAndroid.java#445
+ // These are all the keys dispatchMediaKeyEvent supports.
+ if (AppConstants.Versions.feature19Plus) {
+ // dispatchMediaKeyEvent is only available on Android 4.4+
+ Context viewContext = getView().getContext();
+ AudioManager am = (AudioManager)viewContext.getSystemService(Context.AUDIO_SERVICE);
+ am.dispatchMediaKeyEvent(event);
+ }
+ break;
+ }
+ }
+
+ private boolean processKey(final int action, final int keyCode, final KeyEvent event) {
+
+ if (keyCode > KeyEvent.getMaxKeyCode() || !shouldProcessKey(keyCode, event)) {
+ return false;
+ }
+
+ mEditableClient.postToInputConnection(new Runnable() {
+ @Override
+ public void run() {
+ sendKeyEvent(action, event);
+ }
+ });
+ return true;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return processKey(KeyEvent.ACTION_DOWN, keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return processKey(KeyEvent.ACTION_UP, keyCode, event);
+ }
+
+ /**
+ * Get a key that represents a given character.
+ */
+ private KeyEvent getCharKeyEvent(final char c) {
+ final long time = SystemClock.uptimeMillis();
+ return new KeyEvent(time, time, KeyEvent.ACTION_MULTIPLE,
+ KeyEvent.KEYCODE_UNKNOWN, /* repeat */ 0) {
+ @Override
+ public int getUnicodeChar() {
+ return c;
+ }
+
+ @Override
+ public int getUnicodeChar(int metaState) {
+ return c;
+ }
+ };
+ }
+
+ @Override
+ public boolean onKeyMultiple(int keyCode, int repeatCount, final KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_UNKNOWN) {
+ // KEYCODE_UNKNOWN means the characters are in KeyEvent.getCharacters()
+ final String str = event.getCharacters();
+ for (int i = 0; i < str.length(); i++) {
+ final KeyEvent charEvent = getCharKeyEvent(str.charAt(i));
+ if (!processKey(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_UNKNOWN, charEvent) ||
+ !processKey(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_UNKNOWN, charEvent)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ while ((repeatCount--) != 0) {
+ if (!processKey(KeyEvent.ACTION_DOWN, keyCode, event) ||
+ !processKey(KeyEvent.ACTION_UP, keyCode, event)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+ View v = getView();
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_MENU:
+ InputMethodManager imm = getInputMethodManager();
+ imm.toggleSoftInputFromWindow(v.getWindowToken(),
+ InputMethodManager.SHOW_FORCED, 0);
+ return true;
+ default:
+ break;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isIMEEnabled() {
+ // make sure this picks up PASSWORD and PLUGIN states as well
+ return mIMEState != IME_STATE_DISABLED;
+ }
+
+ @Override
+ public void notifyIME(int type) {
+ switch (type) {
+
+ case NOTIFY_IME_OF_FOCUS:
+ // Showing/hiding vkb is done in notifyIMEContext
+ mFocused = true;
+ resetInputConnection();
+ break;
+
+ case NOTIFY_IME_OF_BLUR:
+ // Showing/hiding vkb is done in notifyIMEContext
+ mFocused = false;
+ break;
+
+ case NOTIFY_IME_OPEN_VKB:
+ showSoftInput();
+ break;
+
+ default:
+ if (DEBUG) {
+ throw new IllegalArgumentException("Unexpected NOTIFY_IME=" + type);
+ }
+ break;
+ }
+ }
+
+ @Override
+ public void notifyIMEContext(int state, String typeHint, String modeHint, String actionHint) {
+ // For some input type we will use a widget to display the ui, for those we must not
+ // display the ime. We can display a widget for date and time types and, if the sdk version
+ // is 11 or greater, for datetime/month/week as well.
+ if (typeHint != null &&
+ (typeHint.equalsIgnoreCase("date") ||
+ typeHint.equalsIgnoreCase("time") ||
+ typeHint.equalsIgnoreCase("datetime") ||
+ typeHint.equalsIgnoreCase("month") ||
+ typeHint.equalsIgnoreCase("week") ||
+ typeHint.equalsIgnoreCase("datetime-local"))) {
+ state = IME_STATE_DISABLED;
+ }
+
+ // mIMEState and the mIME*Hint fields should only be changed by notifyIMEContext,
+ // and not reset anywhere else. Usually, notifyIMEContext is called right after a
+ // focus or blur, so resetting mIMEState during the focus or blur seems harmless.
+ // However, this behavior is not guaranteed. Gecko may call notifyIMEContext
+ // independent of focus change; that is, a focus change may not be accompanied by
+ // a notifyIMEContext call. So if we reset mIMEState inside focus, there may not
+ // be another notifyIMEContext call to set mIMEState to a proper value (bug 829318)
+ /* When IME is 'disabled', IME processing is disabled.
+ In addition, the IME UI is hidden */
+ mIMEState = state;
+ mIMETypeHint = (typeHint == null) ? "" : typeHint;
+ mIMEModeHint = (modeHint == null) ? "" : modeHint;
+ mIMEActionHint = (actionHint == null) ? "" : actionHint;
+
+ // These fields are reset here and will be updated when restartInput is called below
+ mUpdateRequest = null;
+ mCurrentInputMethod = "";
+
+ View v = getView();
+ if (v == null || !v.hasFocus()) {
+ // When using Find In Page, we can still receive notifyIMEContext calls due to the
+ // selection changing when highlighting. However in this case we don't want to reset/
+ // show/hide the keyboard because the find box has the focus and is taking input from
+ // the keyboard.
+ return;
+ }
+
+ // On focus, the notifyIMEContext call comes *before* the
+ // notifyIME(NOTIFY_IME_OF_FOCUS) call, but we need to call restartInput during
+ // notifyIME, so we skip restartInput here. On blur, the notifyIMEContext call
+ // comes *after* the notifyIME(NOTIFY_IME_OF_BLUR) call, and we need to call
+ // restartInput here.
+ if (mIMEState == IME_STATE_DISABLED || mFocused) {
+ restartInput();
+ }
+ }
+}
+
+final class DebugGeckoInputConnection
+ extends GeckoInputConnection
+ implements InvocationHandler {
+
+ private InputConnection mProxy;
+ private final StringBuilder mCallLevel;
+
+ private DebugGeckoInputConnection(View targetView,
+ GeckoEditableClient editable) {
+ super(targetView, editable);
+ mCallLevel = new StringBuilder();
+ }
+
+ public static GeckoEditableListener create(View targetView,
+ GeckoEditableClient editable) {
+ final Class<?>[] PROXY_INTERFACES = { InputConnection.class,
+ InputConnectionListener.class,
+ GeckoEditableListener.class };
+ DebugGeckoInputConnection dgic =
+ new DebugGeckoInputConnection(targetView, editable);
+ dgic.mProxy = (InputConnection)Proxy.newProxyInstance(
+ GeckoInputConnection.class.getClassLoader(),
+ PROXY_INTERFACES, dgic);
+ return (GeckoEditableListener)dgic.mProxy;
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args)
+ throws Throwable {
+
+ StringBuilder log = new StringBuilder(mCallLevel);
+ log.append("> ").append(method.getName()).append("(");
+ if (args != null) {
+ for (Object arg : args) {
+ // translate argument values to constant names
+ if ("notifyIME".equals(method.getName()) && arg == args[0]) {
+ log.append(GeckoEditable.getConstantName(
+ GeckoEditableListener.class, "NOTIFY_IME_", arg));
+ } else if ("notifyIMEContext".equals(method.getName()) && arg == args[0]) {
+ log.append(GeckoEditable.getConstantName(
+ GeckoEditableListener.class, "IME_STATE_", arg));
+ } else {
+ GeckoEditable.debugAppend(log, arg);
+ }
+ log.append(", ");
+ }
+ if (args.length > 0) {
+ log.setLength(log.length() - 2);
+ }
+ }
+ log.append(")");
+ Log.d(LOGTAG, log.toString());
+
+ mCallLevel.append(' ');
+ Object ret = method.invoke(this, args);
+ if (ret == this) {
+ ret = mProxy;
+ }
+ mCallLevel.setLength(Math.max(0, mCallLevel.length() - 1));
+
+ log.setLength(mCallLevel.length());
+ log.append("< ").append(method.getName());
+ if (!method.getReturnType().equals(Void.TYPE)) {
+ GeckoEditable.debugAppend(log.append(": "), ret);
+ }
+ Log.d(LOGTAG, log.toString());
+ return ret;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java
new file mode 100644
index 0000000000..0cb56a7d28
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java
@@ -0,0 +1,491 @@
+/* -*- 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.JNITarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.NetworkUtils;
+import org.mozilla.gecko.util.NetworkUtils.ConnectionSubType;
+import org.mozilla.gecko.util.NetworkUtils.ConnectionType;
+import org.mozilla.gecko.util.NetworkUtils.NetworkStatus;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.DhcpInfo;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.telephony.TelephonyManager;
+import android.text.format.Formatter;
+import android.util.Log;
+
+/**
+ * Provides connection type, subtype and general network status (up/down).
+ *
+ * According to spec of Network Information API version 3, connection types include:
+ * bluetooth, cellular, ethernet, none, wifi and other. The objective of providing such general
+ * connection is due to some security concerns. In short, we don't want to expose exact network type,
+ * especially the cellular network type.
+ *
+ * Specific mobile subtypes are mapped to general 2G, 3G and 4G buckets.
+ *
+ * Logic is implemented as a state machine, so see the transition matrix to figure out what happens when.
+ * This class depends on access to the context, so only use after GeckoAppShell has been initialized.
+ */
+public class GeckoNetworkManager extends BroadcastReceiver implements NativeEventListener {
+ private static final String LOGTAG = "GeckoNetworkManager";
+
+ private static final String LINK_DATA_CHANGED = "changed";
+
+ private static GeckoNetworkManager instance;
+
+ // We hackishly (yet harmlessly, in this case) keep a Context reference passed in via the start method.
+ // See context handling notes in handleManagerEvent, and Bug 1277333.
+ private Context context;
+
+ public static void destroy() {
+ if (instance != null) {
+ instance.onDestroy();
+ instance = null;
+ }
+ }
+
+ public enum ManagerState {
+ OffNoListeners,
+ OffWithListeners,
+ OnNoListeners,
+ OnWithListeners
+ }
+
+ public enum ManagerEvent {
+ start,
+ stop,
+ enableNotifications,
+ disableNotifications,
+ receivedUpdate
+ }
+
+ private ManagerState currentState = ManagerState.OffNoListeners;
+ private ConnectionType currentConnectionType = ConnectionType.NONE;
+ private ConnectionType previousConnectionType = ConnectionType.NONE;
+ private ConnectionSubType currentConnectionSubtype = ConnectionSubType.UNKNOWN;
+ private ConnectionSubType previousConnectionSubtype = ConnectionSubType.UNKNOWN;
+ private NetworkStatus currentNetworkStatus = NetworkStatus.UNKNOWN;
+ private NetworkStatus previousNetworkStatus = NetworkStatus.UNKNOWN;
+
+ private enum InfoType {
+ MCC,
+ MNC
+ }
+
+ private GeckoNetworkManager() {
+ EventDispatcher.getInstance().registerGeckoThreadListener(this,
+ "Wifi:Enable",
+ "Wifi:GetIPAddress");
+ }
+
+ private void onDestroy() {
+ handleManagerEvent(ManagerEvent.stop);
+ EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
+ "Wifi:Enable",
+ "Wifi:GetIPAddress");
+ }
+
+ public static GeckoNetworkManager getInstance() {
+ if (instance == null) {
+ instance = new GeckoNetworkManager();
+ }
+
+ return instance;
+ }
+
+ public double[] getCurrentInformation() {
+ final Context applicationContext = GeckoAppShell.getApplicationContext();
+ final ConnectionType connectionType = currentConnectionType;
+ return new double[] {
+ connectionType.value,
+ connectionType == ConnectionType.WIFI ? 1.0 : 0.0,
+ connectionType == ConnectionType.WIFI ? wifiDhcpGatewayAddress(applicationContext) : 0.0
+ };
+ }
+
+ @Override
+ public void onReceive(Context aContext, Intent aIntent) {
+ handleManagerEvent(ManagerEvent.receivedUpdate);
+ }
+
+ public void start(final Context context) {
+ this.context = context;
+ handleManagerEvent(ManagerEvent.start);
+ }
+
+ public void stop() {
+ handleManagerEvent(ManagerEvent.stop);
+ }
+
+ public void enableNotifications() {
+ handleManagerEvent(ManagerEvent.enableNotifications);
+ }
+
+ public void disableNotifications() {
+ handleManagerEvent(ManagerEvent.disableNotifications);
+ }
+
+ /**
+ * For a given event, figure out the next state, run any transition by-product actions, and switch
+ * current state to the next state. If event is invalid for the current state, this is a no-op.
+ *
+ * @param event Incoming event
+ * @return Boolean indicating if transition was performed.
+ */
+ private synchronized boolean handleManagerEvent(ManagerEvent event) {
+ final ManagerState nextState = getNextState(currentState, event);
+
+ Log.d(LOGTAG, "Incoming event " + event + " for state " + currentState + " -> " + nextState);
+ if (nextState == null) {
+ Log.w(LOGTAG, "Invalid event " + event + " for state " + currentState);
+ return false;
+ }
+
+ // We're being deliberately careful about handling context here; it's possible that in some
+ // rare cases and possibly related to timing of when this is called (seems to be early in the startup phase),
+ // GeckoAppShell.getApplicationContext() will be null, and .start() wasn't called yet,
+ // so we don't have a local Context reference either. If both of these are true, we have to drop the event.
+ // NB: this is hacky (and these checks attempt to isolate the hackiness), and root cause
+ // seems to be how this class fits into the larger ecosystem and general flow of events.
+ // See Bug 1277333.
+ final Context contextForAction;
+ if (context != null) {
+ contextForAction = context;
+ } else {
+ contextForAction = GeckoAppShell.getApplicationContext();
+ }
+
+ if (contextForAction == null) {
+ Log.w(LOGTAG, "Context is not available while processing event " + event + " for state " + currentState);
+ return false;
+ }
+
+ performActionsForStateEvent(contextForAction, currentState, event);
+ currentState = nextState;
+
+ return true;
+ }
+
+ /**
+ * Defines a transition matrix for our state machine. For a given state/event pair, returns nextState.
+ *
+ * @param currentState Current state against which we have an incoming event
+ * @param event Incoming event for which we'd like to figure out the next state
+ * @return State into which we should transition as result of given event
+ */
+ @Nullable
+ public static ManagerState getNextState(@NonNull ManagerState currentState, @NonNull ManagerEvent event) {
+ switch (currentState) {
+ case OffNoListeners:
+ switch (event) {
+ case start:
+ return ManagerState.OnNoListeners;
+ case enableNotifications:
+ return ManagerState.OffWithListeners;
+ default:
+ return null;
+ }
+ case OnNoListeners:
+ switch (event) {
+ case stop:
+ return ManagerState.OffNoListeners;
+ case enableNotifications:
+ return ManagerState.OnWithListeners;
+ case receivedUpdate:
+ return ManagerState.OnNoListeners;
+ default:
+ return null;
+ }
+ case OnWithListeners:
+ switch (event) {
+ case stop:
+ return ManagerState.OffWithListeners;
+ case disableNotifications:
+ return ManagerState.OnNoListeners;
+ case receivedUpdate:
+ return ManagerState.OnWithListeners;
+ default:
+ return null;
+ }
+ case OffWithListeners:
+ switch (event) {
+ case start:
+ return ManagerState.OnWithListeners;
+ case disableNotifications:
+ return ManagerState.OffNoListeners;
+ default:
+ return null;
+ }
+ default:
+ throw new IllegalStateException("Unknown current state: " + currentState.name());
+ }
+ }
+
+ /**
+ * For a given state/event combination, run any actions which are by-products of leaving the state
+ * because of a given event. Since this is a deterministic state machine, we can easily do that
+ * without any additional information.
+ *
+ * @param currentState State which we are leaving
+ * @param event Event which is causing us to leave the state
+ */
+ private void performActionsForStateEvent(final Context context, final ManagerState currentState, final ManagerEvent event) {
+ // NB: network state might be queried via getCurrentInformation at any time; pre-rewrite behaviour was
+ // that network state was updated whenever enableNotifications was called. To avoid deviating
+ // from previous behaviour and causing weird side-effects, we call updateNetworkStateAndConnectionType
+ // whenever notifications are enabled.
+ switch (currentState) {
+ case OffNoListeners:
+ if (event == ManagerEvent.start) {
+ updateNetworkStateAndConnectionType(context);
+ registerBroadcastReceiver(context, this);
+ }
+ if (event == ManagerEvent.enableNotifications) {
+ updateNetworkStateAndConnectionType(context);
+ }
+ break;
+ case OnNoListeners:
+ if (event == ManagerEvent.receivedUpdate) {
+ updateNetworkStateAndConnectionType(context);
+ sendNetworkStateToListeners(context);
+ }
+ if (event == ManagerEvent.enableNotifications) {
+ updateNetworkStateAndConnectionType(context);
+ registerBroadcastReceiver(context, this);
+ }
+ if (event == ManagerEvent.stop) {
+ unregisterBroadcastReceiver(context, this);
+ }
+ break;
+ case OnWithListeners:
+ if (event == ManagerEvent.receivedUpdate) {
+ updateNetworkStateAndConnectionType(context);
+ sendNetworkStateToListeners(context);
+ }
+ if (event == ManagerEvent.stop) {
+ unregisterBroadcastReceiver(context, this);
+ }
+ /* no-op event: ManagerEvent.disableNotifications */
+ break;
+ case OffWithListeners:
+ if (event == ManagerEvent.start) {
+ registerBroadcastReceiver(context, this);
+ }
+ /* no-op event: ManagerEvent.disableNotifications */
+ break;
+ default:
+ throw new IllegalStateException("Unknown current state: " + currentState.name());
+ }
+ }
+
+ /**
+ * Update current network state and connection types.
+ */
+ private void updateNetworkStateAndConnectionType(final Context context) {
+ final ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(
+ Context.CONNECTIVITY_SERVICE);
+ // Type/status getters below all have a defined behaviour for when connectivityManager == null
+ if (connectivityManager == null) {
+ Log.e(LOGTAG, "ConnectivityManager does not exist.");
+ }
+ currentConnectionType = NetworkUtils.getConnectionType(connectivityManager);
+ currentNetworkStatus = NetworkUtils.getNetworkStatus(connectivityManager);
+ currentConnectionSubtype = NetworkUtils.getConnectionSubType(connectivityManager);
+ Log.d(LOGTAG, "New network state: " + currentNetworkStatus + ", " + currentConnectionType + ", " + currentConnectionSubtype);
+ }
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void onConnectionChanged(int type, String subType,
+ boolean isWifi, int DHCPGateway);
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void onStatusChanged(String status);
+
+ /**
+ * Send current network state and connection type to whomever is listening.
+ */
+ private void sendNetworkStateToListeners(final Context context) {
+ if (currentConnectionType != previousConnectionType ||
+ currentConnectionSubtype != previousConnectionSubtype) {
+ previousConnectionType = currentConnectionType;
+ previousConnectionSubtype = currentConnectionSubtype;
+
+ final boolean isWifi = currentConnectionType == ConnectionType.WIFI;
+ final int gateway = !isWifi ? 0 :
+ wifiDhcpGatewayAddress(context);
+
+ if (GeckoThread.isRunning()) {
+ onConnectionChanged(currentConnectionType.value,
+ currentConnectionSubtype.value, isWifi, gateway);
+ } else {
+ GeckoThread.queueNativeCall(GeckoNetworkManager.class, "onConnectionChanged",
+ currentConnectionType.value,
+ String.class, currentConnectionSubtype.value,
+ isWifi, gateway);
+ }
+ }
+
+ final String status;
+
+ if (currentNetworkStatus != previousNetworkStatus) {
+ previousNetworkStatus = currentNetworkStatus;
+ status = currentNetworkStatus.value;
+ } else {
+ status = LINK_DATA_CHANGED;
+ }
+
+ if (GeckoThread.isRunning()) {
+ onStatusChanged(status);
+ } else {
+ GeckoThread.queueNativeCall(GeckoNetworkManager.class, "onStatusChanged",
+ String.class, status);
+ }
+ }
+
+ /**
+ * Stop listening for network state updates.
+ */
+ private static void unregisterBroadcastReceiver(final Context context, final BroadcastReceiver receiver) {
+ context.unregisterReceiver(receiver);
+ }
+
+ /**
+ * Start listening for network state updates.
+ */
+ private static void registerBroadcastReceiver(final Context context, final BroadcastReceiver receiver) {
+ final IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
+ context.registerReceiver(receiver, filter);
+ }
+
+ private static int wifiDhcpGatewayAddress(final Context context) {
+ if (context == null) {
+ return 0;
+ }
+
+ try {
+ WifiManager mgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ DhcpInfo d = mgr.getDhcpInfo();
+ if (d == null) {
+ return 0;
+ }
+
+ return d.gateway;
+
+ } catch (Exception ex) {
+ // getDhcpInfo() is not documented to require any permissions, but on some devices
+ // requires android.permission.ACCESS_WIFI_STATE. Just catch the generic exception
+ // here and returning 0. Not logging because this could be noisy.
+ return 0;
+ }
+ }
+
+ @Override
+ /**
+ * Handles native messages, not part of the state machine flow.
+ */
+ public void handleMessage(final String event, final NativeJSObject message,
+ final EventCallback callback) {
+ final Context applicationContext = GeckoAppShell.getApplicationContext();
+ switch (event) {
+ case "Wifi:Enable":
+ final WifiManager mgr = (WifiManager) applicationContext.getSystemService(Context.WIFI_SERVICE);
+
+ if (!mgr.isWifiEnabled()) {
+ mgr.setWifiEnabled(true);
+ } else {
+ // If Wifi is enabled, maybe you need to select a network
+ Intent intent = new Intent(android.provider.Settings.ACTION_WIFI_SETTINGS);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ applicationContext.startActivity(intent);
+ }
+ break;
+ case "Wifi:GetIPAddress":
+ getWifiIPAddress(callback);
+ break;
+ }
+ }
+
+ // This function only works for IPv4; not part of the state machine flow.
+ private void getWifiIPAddress(final EventCallback callback) {
+ final WifiManager mgr = (WifiManager) GeckoAppShell.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+
+ if (mgr == null) {
+ callback.sendError("Cannot get WifiManager");
+ return;
+ }
+
+ final WifiInfo info = mgr.getConnectionInfo();
+ if (info == null) {
+ callback.sendError("Cannot get connection info");
+ return;
+ }
+
+ int ip = info.getIpAddress();
+ if (ip == 0) {
+ callback.sendError("Cannot get IPv4 address");
+ return;
+ }
+ callback.sendSuccess(Formatter.formatIpAddress(ip));
+ }
+
+ private static int getNetworkOperator(InfoType type, Context context) {
+ if (null == context) {
+ return -1;
+ }
+
+ TelephonyManager tel = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ if (tel == null) {
+ Log.e(LOGTAG, "Telephony service does not exist");
+ return -1;
+ }
+
+ String networkOperator = tel.getNetworkOperator();
+ if (networkOperator == null || networkOperator.length() <= 3) {
+ return -1;
+ }
+
+ if (type == InfoType.MNC) {
+ return Integer.parseInt(networkOperator.substring(3));
+ }
+
+ if (type == InfoType.MCC) {
+ return Integer.parseInt(networkOperator.substring(0, 3));
+ }
+
+ return -1;
+ }
+
+ /**
+ * These are called from JavaScript ctypes. Avoid letting ProGuard delete them.
+ *
+ * Note that these methods must only be called after GeckoAppShell has been
+ * initialized: they depend on access to the context.
+ *
+ * Not part of the state machine flow.
+ */
+ @JNITarget
+ public static int getMCC() {
+ return getNetworkOperator(InfoType.MCC, GeckoAppShell.getApplicationContext());
+ }
+
+ @JNITarget
+ public static int getMNC() {
+ return getNetworkOperator(InfoType.MNC, GeckoAppShell.getApplicationContext());
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfile.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfile.java
new file mode 100644
index 0000000000..27ec4f1dd6
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfile.java
@@ -0,0 +1,1002 @@
+/* -*- 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.SharedPreferences;
+import android.os.Bundle;
+import android.support.annotation.WorkerThread;
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
+import org.mozilla.gecko.GeckoProfileDirectories.NoSuchProfileException;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.util.FileUtils;
+import org.mozilla.gecko.util.INIParser;
+import org.mozilla.gecko.util.INISection;
+import org.mozilla.gecko.util.IntentUtils;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.nio.charset.Charset;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class GeckoProfile {
+ private static final String LOGTAG = "GeckoProfile";
+
+ // The path in the profile to the file containing the client ID.
+ private static final String CLIENT_ID_FILE_PATH = "datareporting/state.json";
+ private static final String FHR_CLIENT_ID_FILE_PATH = "healthreport/state.json";
+ // In the client ID file, the attribute title in the JSON object containing the client ID value.
+ private static final String CLIENT_ID_JSON_ATTR = "clientID";
+
+ private static final String TIMES_PATH = "times.json";
+ private static final String PROFILE_CREATION_DATE_JSON_ATTR = "created";
+
+ // Only tests should need to do this.
+ // We can default this to AppConstants.RELEASE_OR_BETA once we fix Bug 1069687.
+ private static volatile boolean sAcceptDirectoryChanges = true;
+
+ @RobocopTarget
+ public static void enableDirectoryChanges() {
+ Log.w(LOGTAG, "Directory changes should only be enabled for tests. And even then it's a bad idea.");
+ sAcceptDirectoryChanges = true;
+ }
+
+ public static final String DEFAULT_PROFILE = "default";
+ // Profile is using a custom directory outside of the Mozilla directory.
+ public static final String CUSTOM_PROFILE = "";
+
+ public static final String GUEST_PROFILE_DIR = "guest";
+ public static final String GUEST_MODE_PREF = "guestMode";
+
+ // Session store
+ private static final String SESSION_FILE = "sessionstore.js";
+ private static final String SESSION_FILE_BACKUP = "sessionstore.bak";
+ private static final String SESSION_FILE_PREVIOUS = "sessionstore.old";
+ private static final long MAX_PREVIOUS_FILE_AGE = 1000 * 3600 * 24; // 24 hours
+
+ private boolean mOldSessionDataProcessed = false;
+
+ private static final ConcurrentHashMap<String, GeckoProfile> sProfileCache =
+ new ConcurrentHashMap<String, GeckoProfile>(
+ /* capacity */ 4, /* load factor */ 0.75f, /* concurrency */ 2);
+ private static String sDefaultProfileName;
+
+ private final String mName;
+ private final File mMozillaDir;
+ private final Context mApplicationContext;
+
+ private Object mData;
+
+ /**
+ * Access to this member should be synchronized to avoid
+ * races during creation -- particularly between getDir and GeckoView#init.
+ *
+ * Not final because this is lazily computed.
+ */
+ private File mProfileDir;
+
+ private Boolean mInGuestMode;
+
+ public static boolean shouldUseGuestMode(final Context context) {
+ return GeckoSharedPrefs.forApp(context).getBoolean(GUEST_MODE_PREF, false);
+ }
+
+ public static void enterGuestMode(final Context context) {
+ GeckoSharedPrefs.forApp(context).edit().putBoolean(GUEST_MODE_PREF, true).commit();
+ }
+
+ public static void leaveGuestMode(final Context context) {
+ GeckoSharedPrefs.forApp(context).edit().putBoolean(GUEST_MODE_PREF, false).commit();
+ }
+
+ public static GeckoProfile initFromArgs(final Context context, final String args) {
+ if (shouldUseGuestMode(context)) {
+ final GeckoProfile guestProfile = getGuestProfile(context);
+ if (guestProfile != null) {
+ return guestProfile;
+ }
+ // Failed to create guest profile; leave guest mode.
+ leaveGuestMode(context);
+ }
+
+ // We never want to use the guest mode profile concurrently with a normal profile
+ // -- no syncing to it, no dual-profile usage, nothing. GeckoThread startup with
+ // a conventional GeckoProfile will cause the guest profile to be deleted and
+ // guest mode to reset.
+ if (getGuestDir(context).isDirectory()) {
+ final GeckoProfile guestProfile = getGuestProfile(context);
+ if (guestProfile != null) {
+ removeProfile(context, guestProfile);
+ }
+ }
+
+ String profileName = null;
+ String profilePath = null;
+
+ if (args != null && args.contains("-P")) {
+ final Pattern p = Pattern.compile("(?:-P\\s*)(\\w*)(\\s*)");
+ final Matcher m = p.matcher(args);
+ if (m.find()) {
+ profileName = m.group(1);
+ }
+ }
+
+ if (args != null && args.contains("-profile")) {
+ final Pattern p = Pattern.compile("(?:-profile\\s*)(\\S*)(\\s*)");
+ final Matcher m = p.matcher(args);
+ if (m.find()) {
+ profilePath = m.group(1);
+ }
+ }
+
+ if (profileName == null && profilePath == null) {
+ // Get the default profile for the Activity.
+ return getDefaultProfile(context);
+ }
+
+ return GeckoProfile.get(context, profileName, profilePath);
+ }
+
+ private static GeckoProfile getDefaultProfile(Context context) {
+ try {
+ return get(context, getDefaultProfileName(context));
+
+ } catch (final NoMozillaDirectoryException e) {
+ // If this failed, we're screwed.
+ Log.wtf(LOGTAG, "Unable to get default profile name.", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static GeckoProfile get(Context context) {
+ return get(context, null, (File) null);
+ }
+
+ public static GeckoProfile get(Context context, String profileName) {
+ if (profileName != null) {
+ GeckoProfile profile = sProfileCache.get(profileName);
+ if (profile != null)
+ return profile;
+ }
+ return get(context, profileName, (File)null);
+ }
+
+ @RobocopTarget
+ public static GeckoProfile get(Context context, String profileName, String profilePath) {
+ File dir = null;
+ if (!TextUtils.isEmpty(profilePath)) {
+ dir = new File(profilePath);
+ if (!dir.exists() || !dir.isDirectory()) {
+ Log.w(LOGTAG, "requested profile directory missing: " + profilePath);
+ }
+ }
+ return get(context, profileName, dir);
+ }
+
+ // Note that the profile cache respects only the profile name!
+ // If the directory changes, the returned GeckoProfile instance will be mutated.
+ @RobocopTarget
+ public static GeckoProfile get(Context context, String profileName, File profileDir) {
+ if (context == null) {
+ throw new IllegalArgumentException("context must be non-null");
+ }
+
+ // Null name? | Null dir? | Returned profile
+ // ------------------------------------------
+ // Yes | Yes | Active profile or default profile.
+ // No | Yes | Profile with specified name at default dir.
+ // Yes | No | Custom (anonymous) profile with specified dir.
+ // No | No | Profile with specified name at specified dir.
+
+ if (profileName == null && profileDir == null) {
+ // If no profile info was passed in, look for the active profile or a default profile.
+ final GeckoProfile profile = GeckoThread.getActiveProfile();
+ if (profile != null) {
+ return profile;
+ }
+
+ final String args;
+ if (context instanceof Activity) {
+ args = IntentUtils.getStringExtraSafe(((Activity) context).getIntent(), "args");
+ } else {
+ args = null;
+ }
+
+ return GeckoProfile.initFromArgs(context, args);
+
+ } else if (profileName == null) {
+ // If only profile dir was passed in, use custom (anonymous) profile.
+ profileName = CUSTOM_PROFILE;
+
+ } else if (AppConstants.DEBUG_BUILD) {
+ Log.v(LOGTAG, "Fetching profile: '" + profileName + "', '" + profileDir + "'");
+ }
+
+ // We require the profile dir to exist if specified, so create it here if needed.
+ final boolean init = profileDir != null && profileDir.mkdirs();
+
+ // Actually try to look up the profile.
+ GeckoProfile profile = sProfileCache.get(profileName);
+ GeckoProfile newProfile = null;
+
+ if (profile == null) {
+ try {
+ newProfile = new GeckoProfile(context, profileName, profileDir);
+ } catch (NoMozillaDirectoryException e) {
+ // We're unable to do anything sane here.
+ throw new RuntimeException(e);
+ }
+
+ profile = sProfileCache.putIfAbsent(profileName, newProfile);
+ }
+
+ if (profile == null) {
+ profile = newProfile;
+
+ } else if (profileDir != null) {
+ // We have an existing profile but was given an alternate directory.
+ boolean consistent = false;
+ try {
+ consistent = profile.mProfileDir != null &&
+ profile.mProfileDir.getCanonicalPath().equals(profileDir.getCanonicalPath());
+ } catch (final IOException e) {
+ }
+
+ if (!consistent) {
+ if (!sAcceptDirectoryChanges || !profileDir.isDirectory()) {
+ throw new IllegalStateException(
+ "Refusing to reuse profile with a different directory.");
+ }
+
+ if (AppConstants.RELEASE_OR_BETA) {
+ Log.e(LOGTAG, "Release build trying to switch out profile dir. " +
+ "This is an error, but let's do what we can.");
+ }
+ profile.setDir(profileDir);
+ }
+ }
+
+ if (init) {
+ // Initialize the profile directory if we had to create it.
+ profile.enqueueInitialization(profileDir);
+ }
+
+ return profile;
+ }
+
+ // Currently unused outside of testing.
+ @RobocopTarget
+ public static boolean removeProfile(final Context context, final GeckoProfile profile) {
+ final boolean success = profile.remove();
+
+ if (success) {
+ // Clear all shared prefs for the given profile.
+ GeckoSharedPrefs.forProfileName(context, profile.getName())
+ .edit().clear().apply();
+ }
+
+ return success;
+ }
+
+ private static File getGuestDir(final Context context) {
+ return context.getFileStreamPath(GUEST_PROFILE_DIR);
+ }
+
+ @RobocopTarget
+ public static GeckoProfile getGuestProfile(final Context context) {
+ return get(context, CUSTOM_PROFILE, getGuestDir(context));
+ }
+
+ public static boolean isGuestProfile(final Context context, final String profileName,
+ final File profileDir) {
+ // Guest profile is just a custom profile with a special path.
+ if (profileDir == null || !CUSTOM_PROFILE.equals(profileName)) {
+ return false;
+ }
+
+ try {
+ return profileDir.getCanonicalPath().equals(getGuestDir(context).getCanonicalPath());
+ } catch (final IOException e) {
+ return false;
+ }
+ }
+
+ private GeckoProfile(Context context, String profileName, File profileDir) throws NoMozillaDirectoryException {
+ if (profileName == null) {
+ throw new IllegalArgumentException("Unable to create GeckoProfile for empty profile name.");
+ } else if (CUSTOM_PROFILE.equals(profileName) && profileDir == null) {
+ throw new IllegalArgumentException("Custom profile must have a directory");
+ }
+
+ mApplicationContext = context.getApplicationContext();
+ mName = profileName;
+ mMozillaDir = GeckoProfileDirectories.getMozillaDirectory(context);
+
+ mProfileDir = profileDir;
+ if (profileDir != null && !profileDir.isDirectory()) {
+ throw new IllegalArgumentException("Profile directory must exist if specified.");
+ }
+ }
+
+ /**
+ * Return the custom data object associated with this profile, which was set by the
+ * previous {@link #setData(Object)} call. This association is valid for the duration
+ * of the process lifetime. The caller must ensure proper synchronization, typically
+ * by synchronizing on the object returned by {@link #getLock()}.
+ *
+ * The data object is usually a database object that stores per-profile data such as
+ * page history. However, it can be any other object that needs to maintain
+ * profile-specific state.
+ *
+ * @return Associated data object
+ */
+ public Object getData() {
+ return mData;
+ }
+
+ /**
+ * Associate this profile with a custom data object, which can be retrieved by
+ * subsequent {@link #getData()} calls. The caller must ensure proper
+ * synchronization, typically by synchronizing on the object returned by {@link
+ * #getLock()}.
+ *
+ * @param data Custom data object
+ */
+ public void setData(final Object data) {
+ mData = data;
+ }
+
+ private void setDir(File dir) {
+ if (dir != null && dir.exists() && dir.isDirectory()) {
+ synchronized (this) {
+ mProfileDir = dir;
+ mInGuestMode = null;
+ }
+ }
+ }
+
+ @RobocopTarget
+ public String getName() {
+ return mName;
+ }
+
+ public boolean isCustomProfile() {
+ return CUSTOM_PROFILE.equals(mName);
+ }
+
+ @RobocopTarget
+ public boolean inGuestMode() {
+ if (mInGuestMode == null) {
+ mInGuestMode = isGuestProfile(GeckoAppShell.getApplicationContext(),
+ mName, mProfileDir);
+ }
+ return mInGuestMode;
+ }
+
+ /**
+ * Return an Object that can be used with a synchronized statement to allow
+ * exclusive access to the profile.
+ */
+ public Object getLock() {
+ return this;
+ }
+
+ /**
+ * Retrieves the directory backing the profile. This method acts
+ * as a lazy initializer for the GeckoProfile instance.
+ */
+ @RobocopTarget
+ public synchronized File getDir() {
+ forceCreateLocked();
+ return mProfileDir;
+ }
+
+ /**
+ * Forces profile creation. Consider using {@link #getDir()} to initialize the profile instead - it is the
+ * lazy initializer and, for our code reasoning abilities, we should initialize the profile in one place.
+ */
+ private void forceCreateLocked() {
+ if (mProfileDir != null) {
+ return;
+ }
+
+ try {
+ // Check if a profile with this name already exists.
+ try {
+ mProfileDir = findProfileDir();
+ Log.d(LOGTAG, "Found profile dir.");
+ } catch (NoSuchProfileException noSuchProfile) {
+ // If it doesn't exist, create it.
+ mProfileDir = createProfileDir();
+ }
+ } catch (IOException ioe) {
+ Log.e(LOGTAG, "Error getting profile dir", ioe);
+ }
+ }
+
+ public File getFile(String aFile) {
+ File f = getDir();
+ if (f == null)
+ return null;
+
+ return new File(f, aFile);
+ }
+
+ /**
+ * Retrieves the Gecko client ID from the filesystem. If the client ID does not exist, we attempt to migrate and
+ * persist it from FHR and, if that fails, we attempt to create a new one ourselves.
+ *
+ * This method assumes the client ID is located in a file at a hard-coded path within the profile. The format of
+ * this file is a JSONObject which at the bottom level contains a String -> String mapping containing the client ID.
+ *
+ * WARNING: the platform provides a JSM to retrieve the client ID [1] and this would be a
+ * robust way to access it. However, we don't want to rely on Gecko running in order to get
+ * the client ID so instead we access the file this module accesses directly. However, it's
+ * possible the format of this file (and the access calls in the jsm) will change, leaving
+ * this code to fail. There are tests in TestGeckoProfile to verify the file format but be
+ * warned: THIS IS NOT FOOLPROOF.
+ *
+ * [1]: https://dxr.mozilla.org/mozilla-central/source/toolkit/modules/ClientID.jsm
+ *
+ * @throws IOException if the client ID could not be retrieved.
+ */
+ // Mimics ClientID.jsm – _doLoadClientID.
+ @WorkerThread
+ public String getClientId() throws IOException {
+ try {
+ return getValidClientIdFromDisk(CLIENT_ID_FILE_PATH);
+ } catch (final IOException e) {
+ // Avoid log spam: don't log the full Exception w/ the stack trace.
+ Log.d(LOGTAG, "Could not get client ID - attempting to migrate ID from FHR: " + e.getLocalizedMessage());
+ }
+
+ String clientIdToWrite;
+ try {
+ clientIdToWrite = getValidClientIdFromDisk(FHR_CLIENT_ID_FILE_PATH);
+ } catch (final IOException e) {
+ // Avoid log spam: don't log the full Exception w/ the stack trace.
+ Log.d(LOGTAG, "Could not migrate client ID from FHR – creating a new one: " + e.getLocalizedMessage());
+ clientIdToWrite = generateNewClientId();
+ }
+
+ // There is a possibility Gecko is running and the Gecko telemetry implementation decided it's time to generate
+ // the client ID, writing client ID underneath us. Since it's highly unlikely (e.g. we run in onStart before
+ // Gecko is started), we don't handle that possibility besides writing the ID and then reading from the file
+ // again (rather than just returning the value we generated before writing).
+ //
+ // In the event it does happen, any discrepancy will be resolved after a restart. In the mean time, both this
+ // implementation and the Gecko implementation could upload documents with inconsistent IDs.
+ //
+ // In any case, if we get an exception, intentionally throw - there's nothing more to do here.
+ persistClientId(clientIdToWrite);
+ return getValidClientIdFromDisk(CLIENT_ID_FILE_PATH);
+ }
+
+ protected static String generateNewClientId() {
+ return UUID.randomUUID().toString();
+ }
+
+ /**
+ * @return a valid client ID
+ * @throws IOException if a valid client ID could not be retrieved
+ */
+ @WorkerThread
+ private String getValidClientIdFromDisk(final String filePath) throws IOException {
+ final JSONObject obj = readJSONObjectFromFile(filePath);
+ final String clientId = obj.optString(CLIENT_ID_JSON_ATTR);
+ if (isClientIdValid(clientId)) {
+ return clientId;
+ }
+ throw new IOException("Received client ID is invalid: " + clientId);
+ }
+
+ /**
+ * Persists the given client ID to disk. This will overwrite any existing files.
+ */
+ @WorkerThread
+ private void persistClientId(final String clientId) throws IOException {
+ if (!ensureParentDirs(CLIENT_ID_FILE_PATH)) {
+ throw new IOException("Could not create client ID parent directories");
+ }
+
+ final JSONObject obj = new JSONObject();
+ try {
+ obj.put(CLIENT_ID_JSON_ATTR, clientId);
+ } catch (final JSONException e) {
+ throw new IOException("Could not create client ID JSON object", e);
+ }
+
+ // ClientID.jsm overwrites the file to store the client ID so it's okay if we do it too.
+ Log.d(LOGTAG, "Attempting to write new client ID");
+ writeFile(CLIENT_ID_FILE_PATH, obj.toString()); // Logs errors within function: ideally we'd throw.
+ }
+
+ // From ClientID.jsm - isValidClientID.
+ public static boolean isClientIdValid(final String clientId) {
+ // We could use UUID.fromString but, for consistency, we take the implementation from ClientID.jsm.
+ if (TextUtils.isEmpty(clientId)) {
+ return false;
+ }
+ return clientId.matches("(?i:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})");
+ }
+
+ /**
+ * Gets the profile creation date and persists it if it had to be generated.
+ *
+ * To get this value, we first look in times.json. If that could not be accessed, we
+ * return the package's first install date. This is not a perfect solution because a
+ * user may have large gap between install time and first use.
+ *
+ * A more correct algorithm could be the one performed by the JS code in ProfileAge.jsm
+ * getOldestProfileTimestamp: walk the tree and return the oldest timestamp on the files
+ * within the profile. However, since times.json will only not exist for the small
+ * number of really old profiles, we're okay with the package install date compromise for
+ * simplicity.
+ *
+ * @return the profile creation date in the format returned by {@link System#currentTimeMillis()}
+ * or -1 if the value could not be persisted.
+ */
+ @WorkerThread
+ public long getAndPersistProfileCreationDate(final Context context) {
+ try {
+ return getProfileCreationDateFromTimesFile();
+ } catch (final IOException e) {
+ Log.d(LOGTAG, "Unable to retrieve profile creation date from times.json. Getting from system...");
+ final long packageInstallMillis = org.mozilla.gecko.util.ContextUtils.getCurrentPackageInfo(context).firstInstallTime;
+ try {
+ persistProfileCreationDateToTimesFile(packageInstallMillis);
+ } catch (final IOException ioEx) {
+ // We return -1 to ensure the profileCreationDate
+ // will either be an error (-1) or a consistent value.
+ Log.w(LOGTAG, "Unable to persist profile creation date - returning -1");
+ return -1;
+ }
+
+ return packageInstallMillis;
+ }
+ }
+
+ @WorkerThread
+ private long getProfileCreationDateFromTimesFile() throws IOException {
+ final JSONObject obj = readJSONObjectFromFile(TIMES_PATH);
+ try {
+ return obj.getLong(PROFILE_CREATION_DATE_JSON_ATTR);
+ } catch (final JSONException e) {
+ // Don't log to avoid leaking data in JSONObject.
+ throw new IOException("Profile creation does not exist in JSONObject");
+ }
+ }
+
+ @WorkerThread
+ private void persistProfileCreationDateToTimesFile(final long profileCreationMillis) throws IOException {
+ final JSONObject obj = new JSONObject();
+ try {
+ obj.put(PROFILE_CREATION_DATE_JSON_ATTR, profileCreationMillis);
+ } catch (final JSONException e) {
+ // Don't log to avoid leaking data in JSONObject.
+ throw new IOException("Unable to persist profile creation date to times file");
+ }
+ Log.d(LOGTAG, "Attempting to write new profile creation date");
+ writeFile(TIMES_PATH, obj.toString()); // Ideally we'd throw here too.
+ }
+
+ /**
+ * Updates the state of the old session data file.
+ *
+ * sessionstore.js should hold the current session, and sessionstore.old should
+ * hold the previous session (where it is used to read the "tabs from last time").
+ * If we're not restoring tabs automatically, sessionstore.js needs to be moved to
+ * sessionstore.old, so we can display the correct "tabs from last time".
+ * If we *are* restoring tabs, we need to delete outdated copies of sessionstore.old,
+ * so we don't continue showing stale "tabs from last time" indefinitely.
+ *
+ * @param shouldRestore Pass true if we are automatically restoring last session's tabs.
+ */
+ public void updateSessionFile(boolean shouldRestore) {
+ File sessionFilePrevious = getFile(SESSION_FILE_PREVIOUS);
+ if (!shouldRestore) {
+ File sessionFile = getFile(SESSION_FILE);
+ if (sessionFile != null && sessionFile.exists()) {
+ sessionFile.renameTo(sessionFilePrevious);
+ }
+ } else {
+ if (sessionFilePrevious != null && sessionFilePrevious.exists() &&
+ System.currentTimeMillis() - sessionFilePrevious.lastModified() > MAX_PREVIOUS_FILE_AGE) {
+ sessionFilePrevious.delete();
+ }
+ }
+ synchronized (this) {
+ mOldSessionDataProcessed = true;
+ notifyAll();
+ }
+ }
+
+ public void waitForOldSessionDataProcessing() {
+ synchronized (this) {
+ while (!mOldSessionDataProcessed) {
+ try {
+ wait();
+ } catch (final InterruptedException e) {
+ // Ignore and wait again.
+ }
+ }
+ }
+ }
+
+ /**
+ * Get the string from a session file.
+ *
+ * The session can either be read from sessionstore.js or sessionstore.bak.
+ * In general, sessionstore.js holds the current session, and
+ * sessionstore.bak holds a backup copy in case of interrupted writes.
+ *
+ * @param readBackup if true, the session is read from sessionstore.bak;
+ * otherwise, the session is read from sessionstore.js
+ *
+ * @return the session string
+ */
+ public String readSessionFile(boolean readBackup) {
+ return readSessionFile(readBackup ? SESSION_FILE_BACKUP : SESSION_FILE);
+ }
+
+ /**
+ * Get the string from last session's session file.
+ *
+ * If we are not restoring tabs automatically, sessionstore.old will contain
+ * the previous session.
+ *
+ * @return the session string
+ */
+ public String readPreviousSessionFile() {
+ return readSessionFile(SESSION_FILE_PREVIOUS);
+ }
+
+ private String readSessionFile(String fileName) {
+ File sessionFile = getFile(fileName);
+
+ try {
+ if (sessionFile != null && sessionFile.exists()) {
+ return readFile(sessionFile);
+ }
+ } catch (IOException ioe) {
+ Log.e(LOGTAG, "Unable to read session file", ioe);
+ }
+ return null;
+ }
+
+ /**
+ * Checks whether the session store file exists.
+ */
+ public boolean sessionFileExists() {
+ File sessionFile = getFile(SESSION_FILE);
+
+ return sessionFile != null && sessionFile.exists();
+ }
+
+ /**
+ * Ensures the parent director(y|ies) of the given filename exist by making them
+ * if they don't already exist..
+ *
+ * @param filename The path to the file whose parents should be made directories
+ * @return true if the parent directory exists, false otherwise
+ */
+ @WorkerThread
+ protected boolean ensureParentDirs(final String filename) {
+ final File file = new File(getDir(), filename);
+ final File parentFile = file.getParentFile();
+ return parentFile.mkdirs() || parentFile.isDirectory();
+ }
+
+ public void writeFile(final String filename, final String data) {
+ File file = new File(getDir(), filename);
+ BufferedWriter bufferedWriter = null;
+ try {
+ bufferedWriter = new BufferedWriter(new FileWriter(file, false));
+ bufferedWriter.write(data);
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Unable to write to file", e);
+ } finally {
+ try {
+ if (bufferedWriter != null) {
+ bufferedWriter.close();
+ }
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Error closing writer while writing to file", e);
+ }
+ }
+ }
+
+ @WorkerThread
+ public JSONObject readJSONObjectFromFile(final String filename) throws IOException {
+ final String fileContents;
+ try {
+ fileContents = readFile(filename);
+ } catch (final IOException e) {
+ // Don't log exception to avoid leaking profile path.
+ throw new IOException("Could not access given file to retrieve JSONObject");
+ }
+
+ try {
+ return new JSONObject(fileContents);
+ } catch (final JSONException e) {
+ // Don't log exception to avoid leaking profile path.
+ throw new IOException("Could not parse JSON to retrieve JSONObject");
+ }
+ }
+
+ public JSONArray readJSONArrayFromFile(final String filename) {
+ String fileContent;
+ try {
+ fileContent = readFile(filename);
+ } catch (IOException expected) {
+ return new JSONArray();
+ }
+
+ JSONArray jsonArray;
+ try {
+ jsonArray = new JSONArray(fileContent);
+ } catch (JSONException e) {
+ jsonArray = new JSONArray();
+ }
+ return jsonArray;
+ }
+
+ public String readFile(String filename) throws IOException {
+ File dir = getDir();
+ if (dir == null) {
+ throw new IOException("No profile directory found");
+ }
+ File target = new File(dir, filename);
+ return readFile(target);
+ }
+
+ private String readFile(File target) throws IOException {
+ FileReader fr = new FileReader(target);
+ try {
+ StringBuilder sb = new StringBuilder();
+ char[] buf = new char[8192];
+ int read = fr.read(buf);
+ while (read >= 0) {
+ sb.append(buf, 0, read);
+ read = fr.read(buf);
+ }
+ return sb.toString();
+ } finally {
+ fr.close();
+ }
+ }
+
+ public boolean deleteFileFromProfileDir(String fileName) throws IllegalArgumentException {
+ if (TextUtils.isEmpty(fileName)) {
+ throw new IllegalArgumentException("Filename cannot be empty.");
+ }
+ File file = new File(getDir(), fileName);
+ return file.delete();
+ }
+
+ private boolean remove() {
+ try {
+ synchronized (this) {
+ if (mProfileDir != null && mProfileDir.exists()) {
+ FileUtils.delete(mProfileDir);
+ }
+
+ if (isCustomProfile()) {
+ // Custom profiles don't have profile.ini sections that we need to remove.
+ return true;
+ }
+
+ try {
+ // If findProfileDir() succeeds, it means the profile was created
+ // through forceCreate(), so we set mProfileDir to null to enable
+ // forceCreate() to create the profile again.
+ findProfileDir();
+ mProfileDir = null;
+
+ } catch (final NoSuchProfileException e) {
+ // If findProfileDir() throws, it means the profile was not created
+ // through forceCreate(), and we have to preserve mProfileDir because
+ // it was given to us. In that case, there's nothing left to do here.
+ return true;
+ }
+ }
+
+ final INIParser parser = GeckoProfileDirectories.getProfilesINI(mMozillaDir);
+ final Hashtable<String, INISection> sections = parser.getSections();
+ for (Enumeration<INISection> e = sections.elements(); e.hasMoreElements();) {
+ final INISection section = e.nextElement();
+ String name = section.getStringProperty("Name");
+
+ if (name == null || !name.equals(mName)) {
+ continue;
+ }
+
+ if (section.getName().startsWith("Profile")) {
+ // ok, we have stupid Profile#-named things. Rename backwards.
+ try {
+ int sectionNumber = Integer.parseInt(section.getName().substring("Profile".length()));
+ String curSection = "Profile" + sectionNumber;
+ String nextSection = "Profile" + (sectionNumber + 1);
+
+ sections.remove(curSection);
+
+ while (sections.containsKey(nextSection)) {
+ parser.renameSection(nextSection, curSection);
+ sectionNumber++;
+
+ curSection = nextSection;
+ nextSection = "Profile" + (sectionNumber + 1);
+ }
+ } catch (NumberFormatException nex) {
+ // uhm, malformed Profile thing; we can't do much.
+ Log.e(LOGTAG, "Malformed section name in profiles.ini: " + section.getName());
+ return false;
+ }
+ } else {
+ // this really shouldn't be the case, but handle it anyway
+ parser.removeSection(mName);
+ }
+
+ break;
+ }
+
+ parser.write();
+ return true;
+ } catch (IOException ex) {
+ Log.w(LOGTAG, "Failed to remove profile.", ex);
+ return false;
+ }
+ }
+
+ /**
+ * @return the default profile name for this application, or
+ * {@link GeckoProfile#DEFAULT_PROFILE} if none could be found.
+ *
+ * @throws NoMozillaDirectoryException
+ * if the Mozilla directory did not exist and could not be
+ * created.
+ */
+ public static String getDefaultProfileName(final Context context) throws NoMozillaDirectoryException {
+ // Have we read the default profile from the INI already?
+ // Changing the default profile requires a restart, so we don't
+ // need to worry about runtime changes.
+ if (sDefaultProfileName != null) {
+ return sDefaultProfileName;
+ }
+
+ final String profileName = GeckoProfileDirectories.findDefaultProfileName(context);
+ if (profileName == null) {
+ // Note that we don't persist this back to profiles.ini.
+ sDefaultProfileName = DEFAULT_PROFILE;
+ return DEFAULT_PROFILE;
+ }
+
+ sDefaultProfileName = profileName;
+ return sDefaultProfileName;
+ }
+
+ private File findProfileDir() throws NoSuchProfileException {
+ if (isCustomProfile()) {
+ return mProfileDir;
+ }
+ return GeckoProfileDirectories.findProfileDir(mMozillaDir, mName);
+ }
+
+ @WorkerThread
+ private File createProfileDir() throws IOException {
+ if (isCustomProfile()) {
+ // Custom profiles must already exist.
+ return mProfileDir;
+ }
+
+ INIParser parser = GeckoProfileDirectories.getProfilesINI(mMozillaDir);
+
+ // Salt the name of our requested profile
+ String saltedName;
+ File profileDir;
+ do {
+ saltedName = GeckoProfileDirectories.saltProfileName(mName);
+ profileDir = new File(mMozillaDir, saltedName);
+ } while (profileDir.exists());
+
+ // Attempt to create the salted profile dir
+ if (!profileDir.mkdirs()) {
+ throw new IOException("Unable to create profile.");
+ }
+ Log.d(LOGTAG, "Created new profile dir.");
+
+ // Now update profiles.ini
+ // If this is the first time its created, we also add a General section
+ // look for the first profile number that isn't taken yet
+ int profileNum = 0;
+ boolean isDefaultSet = false;
+ INISection profileSection;
+ while ((profileSection = parser.getSection("Profile" + profileNum)) != null) {
+ profileNum++;
+ if (profileSection.getProperty("Default") != null) {
+ isDefaultSet = true;
+ }
+ }
+
+ profileSection = new INISection("Profile" + profileNum);
+ profileSection.setProperty("Name", mName);
+ profileSection.setProperty("IsRelative", 1);
+ profileSection.setProperty("Path", saltedName);
+
+ if (parser.getSection("General") == null) {
+ INISection generalSection = new INISection("General");
+ generalSection.setProperty("StartWithLastProfile", 1);
+ parser.addSection(generalSection);
+ }
+
+ if (!isDefaultSet) {
+ // only set as default if this is the first profile we're creating
+ profileSection.setProperty("Default", 1);
+ }
+
+ parser.addSection(profileSection);
+ parser.write();
+
+ enqueueInitialization(profileDir);
+
+ // Write out profile creation time, mirroring the logic in nsToolkitProfileService.
+ try {
+ FileOutputStream stream = new FileOutputStream(profileDir.getAbsolutePath() + File.separator + TIMES_PATH);
+ OutputStreamWriter writer = new OutputStreamWriter(stream, Charset.forName("UTF-8"));
+ try {
+ writer.append("{\"created\": " + System.currentTimeMillis() + "}\n");
+ } finally {
+ writer.close();
+ }
+ } catch (Exception e) {
+ // Best-effort.
+ Log.w(LOGTAG, "Couldn't write " + TIMES_PATH, e);
+ }
+
+ // Create the client ID file before Gecko starts (we assume this method
+ // is called before Gecko starts). If we let Gecko start, the JS telemetry
+ // code may try to write to the file at the same time Java does.
+ persistClientId(generateNewClientId());
+
+ return profileDir;
+ }
+
+ /**
+ * This method is called once, immediately before creation of the profile
+ * directory completes.
+ *
+ * It queues up work to be done in the background to prepare the profile,
+ * such as adding default bookmarks.
+ *
+ * This is public for use *from tests only*!
+ */
+ @RobocopTarget
+ public void enqueueInitialization(final File profileDir) {
+ Log.i(LOGTAG, "Enqueuing profile init.");
+
+ final Bundle message = new Bundle(2);
+ message.putCharSequence("name", getName());
+ message.putCharSequence("path", profileDir.getAbsolutePath());
+ EventDispatcher.getInstance().dispatch("Profile:Create", message);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfileDirectories.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfileDirectories.java
new file mode 100644
index 0000000000..2afb54bc4d
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfileDirectories.java
@@ -0,0 +1,230 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.util.INIParser;
+import org.mozilla.gecko.util.INISection;
+
+import android.content.Context;
+
+/**
+ * <code>GeckoProfileDirectories</code> manages access to mappings from profile
+ * names to salted profile directory paths, as well as the default profile name.
+ *
+ * This class will eventually come to encapsulate the remaining logic embedded
+ * in profiles.ini; for now it's a read-only wrapper.
+ */
+public class GeckoProfileDirectories {
+ @SuppressWarnings("serial")
+ public static class NoMozillaDirectoryException extends Exception {
+ public NoMozillaDirectoryException(Throwable cause) {
+ super(cause);
+ }
+
+ public NoMozillaDirectoryException(String reason) {
+ super(reason);
+ }
+
+ public NoMozillaDirectoryException(String reason, Throwable cause) {
+ super(reason, cause);
+ }
+ }
+
+ @SuppressWarnings("serial")
+ public static class NoSuchProfileException extends Exception {
+ public NoSuchProfileException(String detailMessage, Throwable cause) {
+ super(detailMessage, cause);
+ }
+
+ public NoSuchProfileException(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ private interface INISectionPredicate {
+ public boolean matches(INISection section);
+ }
+
+ private static final String MOZILLA_DIR_NAME = "mozilla";
+
+ /**
+ * Returns true if the supplied profile entry represents the default profile.
+ */
+ private static final INISectionPredicate sectionIsDefault = new INISectionPredicate() {
+ @Override
+ public boolean matches(INISection section) {
+ return section.getIntProperty("Default") == 1;
+ }
+ };
+
+ /**
+ * Returns true if the supplied profile entry has a 'Name' field.
+ */
+ private static final INISectionPredicate sectionHasName = new INISectionPredicate() {
+ @Override
+ public boolean matches(INISection section) {
+ final String name = section.getStringProperty("Name");
+ return name != null;
+ }
+ };
+
+ @RobocopTarget
+ public static INIParser getProfilesINI(File mozillaDir) {
+ return new INIParser(new File(mozillaDir, "profiles.ini"));
+ }
+
+ /**
+ * Utility method to compute a salted profile name: eight random alphanumeric
+ * characters, followed by a period, followed by the profile name.
+ */
+ public static String saltProfileName(final String name) {
+ if (name == null) {
+ throw new IllegalArgumentException("Cannot salt null profile name.");
+ }
+
+ final String allowedChars = "abcdefghijklmnopqrstuvwxyz0123456789";
+ final int scale = allowedChars.length();
+ final int saltSize = 8;
+
+ final StringBuilder saltBuilder = new StringBuilder(saltSize + 1 + name.length());
+ for (int i = 0; i < saltSize; i++) {
+ saltBuilder.append(allowedChars.charAt((int)(Math.random() * scale)));
+ }
+ saltBuilder.append('.');
+ saltBuilder.append(name);
+ return saltBuilder.toString();
+ }
+
+ /**
+ * Return the Mozilla directory within the files directory of the provided
+ * context. This should always be the same within a running application.
+ *
+ * This method is package-scoped so that new {@link GeckoProfile} instances can
+ * contextualize themselves.
+ *
+ * @return a new File object for the Mozilla directory.
+ * @throws NoMozillaDirectoryException
+ * if the directory did not exist and could not be created.
+ */
+ @RobocopTarget
+ public static File getMozillaDirectory(Context context) throws NoMozillaDirectoryException {
+ final File mozillaDir = new File(context.getFilesDir(), MOZILLA_DIR_NAME);
+ if (mozillaDir.mkdirs() || mozillaDir.isDirectory()) {
+ return mozillaDir;
+ }
+
+ // Although this leaks a path to the system log, the path is
+ // predictable (unlike a profile directory), so this is fine.
+ throw new NoMozillaDirectoryException("Unable to create mozilla directory at " + mozillaDir.getAbsolutePath());
+ }
+
+ /**
+ * Discover the default profile name by examining profiles.ini.
+ *
+ * Package-scoped because {@link GeckoProfile} needs access to it.
+ *
+ * @return null if there is no "Default" entry in profiles.ini, or the profile
+ * name if there is.
+ * @throws NoMozillaDirectoryException
+ * if the Mozilla directory did not exist and could not be created.
+ */
+ static String findDefaultProfileName(final Context context) throws NoMozillaDirectoryException {
+ final INIParser parser = GeckoProfileDirectories.getProfilesINI(getMozillaDirectory(context));
+ if (parser.getSections() != null) {
+ for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements(); ) {
+ final INISection section = e.nextElement();
+ if (section.getIntProperty("Default") == 1) {
+ return section.getStringProperty("Name");
+ }
+ }
+ }
+ return null;
+ }
+
+ static Map<String, String> getDefaultProfile(final File mozillaDir) {
+ return getMatchingProfiles(mozillaDir, sectionIsDefault, true);
+ }
+
+ static Map<String, String> getProfilesNamed(final File mozillaDir, final String name) {
+ final INISectionPredicate predicate = new INISectionPredicate() {
+ @Override
+ public boolean matches(final INISection section) {
+ return name.equals(section.getStringProperty("Name"));
+ }
+ };
+ return getMatchingProfiles(mozillaDir, predicate, true);
+ }
+
+ /**
+ * Calls {@link GeckoProfileDirectories#getMatchingProfiles(File, INISectionPredicate, boolean)}
+ * with a filter to ensure that all profiles are named.
+ */
+ static Map<String, String> getAllProfiles(final File mozillaDir) {
+ return getMatchingProfiles(mozillaDir, sectionHasName, false);
+ }
+
+ /**
+ * Return a mapping from the names of all matching profiles (that is,
+ * profiles appearing in profiles.ini that match the supplied predicate) to
+ * their absolute paths on disk.
+ *
+ * @param mozillaDir
+ * a directory containing profiles.ini.
+ * @param predicate
+ * a predicate to use when evaluating whether to include a
+ * particular INI section.
+ * @param stopOnSuccess
+ * if true, this method will return with the first result that
+ * matches the predicate; if false, all matching results are
+ * included.
+ * @return a {@link Map} from name to path.
+ */
+ public static Map<String, String> getMatchingProfiles(final File mozillaDir, INISectionPredicate predicate, boolean stopOnSuccess) {
+ final HashMap<String, String> result = new HashMap<String, String>();
+ final INIParser parser = GeckoProfileDirectories.getProfilesINI(mozillaDir);
+
+ if (parser.getSections() != null) {
+ for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements(); ) {
+ final INISection section = e.nextElement();
+ if (predicate == null || predicate.matches(section)) {
+ final String name = section.getStringProperty("Name");
+ final String pathString = section.getStringProperty("Path");
+ final boolean isRelative = section.getIntProperty("IsRelative") == 1;
+ final File path = isRelative ? new File(mozillaDir, pathString) : new File(pathString);
+ result.put(name, path.getAbsolutePath());
+
+ if (stopOnSuccess) {
+ return result;
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ public static File findProfileDir(final File mozillaDir, final String profileName) throws NoSuchProfileException {
+ // Open profiles.ini to find the correct path.
+ final INIParser parser = GeckoProfileDirectories.getProfilesINI(mozillaDir);
+ if (parser.getSections() != null) {
+ for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements(); ) {
+ final INISection section = e.nextElement();
+ final String name = section.getStringProperty("Name");
+ if (name != null && name.equals(profileName)) {
+ if (section.getIntProperty("IsRelative") == 1) {
+ return new File(mozillaDir, section.getStringProperty("Path"));
+ }
+ return new File(section.getStringProperty("Path"));
+ }
+ }
+ }
+ throw new NoSuchProfileException("No profile " + profileName);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoScreenOrientation.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoScreenOrientation.java
new file mode 100644
index 0000000000..23f84f52ac
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoScreenOrientation.java
@@ -0,0 +1,423 @@
+/* -*- 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 android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.util.Log;
+import android.view.Surface;
+import android.app.Activity;
+
+import java.util.Arrays;
+import java.util.List;
+
+/*
+ * Updates, locks and unlocks the screen orientation.
+ *
+ * Note: Replaces the OnOrientationChangeListener to avoid redundant rotation
+ * event handling.
+ */
+public class GeckoScreenOrientation {
+ private static final String LOGTAG = "GeckoScreenOrientation";
+
+ // Make sure that any change in dom/base/ScreenOrientation.h happens here too.
+ public enum ScreenOrientation {
+ NONE(0),
+ PORTRAIT_PRIMARY(1 << 0),
+ PORTRAIT_SECONDARY(1 << 1),
+ PORTRAIT(PORTRAIT_PRIMARY.value | PORTRAIT_SECONDARY.value),
+ LANDSCAPE_PRIMARY(1 << 2),
+ LANDSCAPE_SECONDARY(1 << 3),
+ LANDSCAPE(LANDSCAPE_PRIMARY.value | LANDSCAPE_SECONDARY.value),
+ DEFAULT(1 << 4);
+
+ public final short value;
+
+ private ScreenOrientation(int value) {
+ this.value = (short)value;
+ }
+
+ private final static ScreenOrientation[] sValues = ScreenOrientation.values();
+
+ public static ScreenOrientation get(int value) {
+ for (ScreenOrientation orient: sValues) {
+ if (orient.value == value) {
+ return orient;
+ }
+ }
+ return NONE;
+ }
+ }
+
+ // Singleton instance.
+ private static GeckoScreenOrientation sInstance;
+ // Default screen orientation, used for initialization and unlocking.
+ private static final ScreenOrientation DEFAULT_SCREEN_ORIENTATION = ScreenOrientation.DEFAULT;
+ // Default rotation, used when device rotation is unknown.
+ private static final int DEFAULT_ROTATION = Surface.ROTATION_0;
+ // Default orientation, used if screen orientation is unspecified.
+ private ScreenOrientation mDefaultScreenOrientation;
+ // Last updated screen orientation.
+ private ScreenOrientation mScreenOrientation;
+ // Whether the update should notify Gecko about screen orientation changes.
+ private boolean mShouldNotify = true;
+ // Configuration screen orientation preference path.
+ private static final String DEFAULT_SCREEN_ORIENTATION_PREF = "app.orientation.default";
+
+ public GeckoScreenOrientation() {
+ PrefsHelper.getPref(DEFAULT_SCREEN_ORIENTATION_PREF, new PrefsHelper.PrefHandlerBase() {
+ @Override public void prefValue(String pref, String value) {
+ // Read and update the configuration default preference.
+ mDefaultScreenOrientation = screenOrientationFromArrayString(value);
+ setRequestedOrientation(mDefaultScreenOrientation);
+ }
+ });
+
+ mDefaultScreenOrientation = DEFAULT_SCREEN_ORIENTATION;
+ update();
+ }
+
+ public static GeckoScreenOrientation getInstance() {
+ if (sInstance == null) {
+ sInstance = new GeckoScreenOrientation();
+ }
+ return sInstance;
+ }
+
+ /*
+ * Enable Gecko screen orientation events on update.
+ */
+ public void enableNotifications() {
+ update();
+ mShouldNotify = true;
+ }
+
+ /*
+ * Disable Gecko screen orientation events on update.
+ */
+ public void disableNotifications() {
+ mShouldNotify = false;
+ }
+
+ /*
+ * Update screen orientation.
+ * Retrieve orientation and rotation via GeckoAppShell.
+ *
+ * @return Whether the screen orientation has changed.
+ */
+ public boolean update() {
+ Activity activity = getActivity();
+ if (activity == null) {
+ return false;
+ }
+ Configuration config = activity.getResources().getConfiguration();
+ return update(config.orientation);
+ }
+
+ /*
+ * Update screen orientation given the android orientation.
+ * Retrieve rotation via GeckoAppShell.
+ *
+ * @param aAndroidOrientation
+ * Android screen orientation from Configuration.orientation.
+ *
+ * @return Whether the screen orientation has changed.
+ */
+ public boolean update(int aAndroidOrientation) {
+ return update(getScreenOrientation(aAndroidOrientation, getRotation()));
+ }
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void onOrientationChange(short screenOrientation, short angle);
+
+ /*
+ * Update screen orientation given the screen orientation.
+ *
+ * @param aScreenOrientation
+ * Gecko screen orientation based on android orientation and rotation.
+ *
+ * @return Whether the screen orientation has changed.
+ */
+ public boolean update(ScreenOrientation aScreenOrientation) {
+ if (mScreenOrientation == aScreenOrientation) {
+ return false;
+ }
+ mScreenOrientation = aScreenOrientation;
+ Log.d(LOGTAG, "updating to new orientation " + mScreenOrientation);
+ if (mShouldNotify) {
+ // Gecko expects a definite screen orientation, so we default to the
+ // primary orientations.
+ if (aScreenOrientation == ScreenOrientation.PORTRAIT) {
+ aScreenOrientation = ScreenOrientation.PORTRAIT_PRIMARY;
+ } else if (aScreenOrientation == ScreenOrientation.LANDSCAPE) {
+ aScreenOrientation = ScreenOrientation.LANDSCAPE_PRIMARY;
+ }
+
+ if (GeckoThread.isRunning()) {
+ onOrientationChange(aScreenOrientation.value, getAngle());
+ } else {
+ GeckoThread.queueNativeCall(GeckoScreenOrientation.class, "onOrientationChange",
+ aScreenOrientation.value, getAngle());
+ }
+ }
+ GeckoAppShell.resetScreenSize();
+ return true;
+ }
+
+ /*
+ * @return The Android orientation (Configuration.orientation).
+ */
+ public int getAndroidOrientation() {
+ return screenOrientationToAndroidOrientation(getScreenOrientation());
+ }
+
+ /*
+ * @return The Gecko screen orientation derived from Android orientation and
+ * rotation.
+ */
+ public ScreenOrientation getScreenOrientation() {
+ return mScreenOrientation;
+ }
+
+ /*
+ * Lock screen orientation given the Gecko screen orientation.
+ *
+ * @param aGeckoOrientation
+ * The Gecko orientation provided.
+ */
+ public void lock(int aGeckoOrientation) {
+ lock(ScreenOrientation.get(aGeckoOrientation));
+ }
+
+ /*
+ * Lock screen orientation given the Gecko screen orientation.
+ * Retrieve rotation via GeckoAppShell.
+ *
+ * @param aScreenOrientation
+ * Gecko screen orientation derived from Android orientation and
+ * rotation.
+ *
+ * @return Whether the locking was successful.
+ */
+ public boolean lock(ScreenOrientation aScreenOrientation) {
+ Log.d(LOGTAG, "locking to " + aScreenOrientation);
+ update(aScreenOrientation);
+ return setRequestedOrientation(aScreenOrientation);
+ }
+
+ /*
+ * Unlock and update screen orientation.
+ *
+ * @return Whether the unlocking was successful.
+ */
+ public boolean unlock() {
+ Log.d(LOGTAG, "unlocking");
+ setRequestedOrientation(mDefaultScreenOrientation);
+ return update();
+ }
+
+ private Activity getActivity() {
+ if (GeckoAppShell.getGeckoInterface() == null) {
+ return null;
+ }
+ return GeckoAppShell.getGeckoInterface().getActivity();
+ }
+
+ /*
+ * Set the given requested orientation for the current activity.
+ * This is essentially an unlock without an update.
+ *
+ * @param aScreenOrientation
+ * Gecko screen orientation.
+ *
+ * @return Whether the requested orientation was set. This can only fail if
+ * the current activity cannot be retrieved via GeckoAppShell.
+ *
+ */
+ private boolean setRequestedOrientation(ScreenOrientation aScreenOrientation) {
+ int activityOrientation = screenOrientationToActivityInfoOrientation(aScreenOrientation);
+ Activity activity = getActivity();
+ if (activity == null) {
+ Log.w(LOGTAG, "setRequestOrientation: failed to get activity");
+ return false;
+ }
+ if (activity.getRequestedOrientation() == activityOrientation) {
+ return false;
+ }
+ activity.setRequestedOrientation(activityOrientation);
+ return true;
+ }
+
+ /*
+ * Combine the Android orientation and rotation to the Gecko orientation.
+ *
+ * @param aAndroidOrientation
+ * Android orientation from Configuration.orientation.
+ * @param aRotation
+ * Device rotation from Display.getRotation().
+ *
+ * @return Gecko screen orientation.
+ */
+ private ScreenOrientation getScreenOrientation(int aAndroidOrientation, int aRotation) {
+ boolean isPrimary = aRotation == Surface.ROTATION_0 || aRotation == Surface.ROTATION_90;
+ if (aAndroidOrientation == Configuration.ORIENTATION_PORTRAIT) {
+ if (isPrimary) {
+ // Non-rotated portrait device or landscape device rotated
+ // to primary portrait mode counter-clockwise.
+ return ScreenOrientation.PORTRAIT_PRIMARY;
+ }
+ return ScreenOrientation.PORTRAIT_SECONDARY;
+ }
+ if (aAndroidOrientation == Configuration.ORIENTATION_LANDSCAPE) {
+ if (isPrimary) {
+ // Non-rotated landscape device or portrait device rotated
+ // to primary landscape mode counter-clockwise.
+ return ScreenOrientation.LANDSCAPE_PRIMARY;
+ }
+ return ScreenOrientation.LANDSCAPE_SECONDARY;
+ }
+ return ScreenOrientation.NONE;
+ }
+
+ /*
+ * @return Device rotation converted to an angle.
+ */
+ public short getAngle() {
+ switch (getRotation()) {
+ case Surface.ROTATION_0:
+ return 0;
+ case Surface.ROTATION_90:
+ return 90;
+ case Surface.ROTATION_180:
+ return 180;
+ case Surface.ROTATION_270:
+ return 270;
+ default:
+ Log.w(LOGTAG, "getAngle: unexpected rotation value");
+ return 0;
+ }
+ }
+
+ /*
+ * @return Device rotation from Display.getRotation().
+ */
+ private int getRotation() {
+ Activity activity = getActivity();
+ if (activity == null) {
+ Log.w(LOGTAG, "getRotation: failed to get activity");
+ return DEFAULT_ROTATION;
+ }
+ return activity.getWindowManager().getDefaultDisplay().getRotation();
+ }
+
+ /*
+ * Retrieve the screen orientation from an array string.
+ *
+ * @param aArray
+ * String containing comma-delimited strings.
+ *
+ * @return First parsed Gecko screen orientation.
+ */
+ public static ScreenOrientation screenOrientationFromArrayString(String aArray) {
+ List<String> orientations = Arrays.asList(aArray.split(","));
+ if (orientations.size() == 0) {
+ // If nothing is listed, return default.
+ Log.w(LOGTAG, "screenOrientationFromArrayString: no orientation in string");
+ return DEFAULT_SCREEN_ORIENTATION;
+ }
+
+ // We don't support multiple orientations yet. To avoid developer
+ // confusion, just take the first one listed.
+ return screenOrientationFromString(orientations.get(0));
+ }
+
+ /*
+ * Retrieve the screen orientation from a string.
+ *
+ * @param aStr
+ * String hopefully containing a screen orientation name.
+ * @return Gecko screen orientation if matched, DEFAULT_SCREEN_ORIENTATION
+ * otherwise.
+ */
+ public static ScreenOrientation screenOrientationFromString(String aStr) {
+ switch (aStr) {
+ case "portrait":
+ return ScreenOrientation.PORTRAIT;
+ case "landscape":
+ return ScreenOrientation.LANDSCAPE;
+ case "portrait-primary":
+ return ScreenOrientation.PORTRAIT_PRIMARY;
+ case "portrait-secondary":
+ return ScreenOrientation.PORTRAIT_SECONDARY;
+ case "landscape-primary":
+ return ScreenOrientation.LANDSCAPE_PRIMARY;
+ case "landscape-secondary":
+ return ScreenOrientation.LANDSCAPE_SECONDARY;
+ }
+
+ Log.w(LOGTAG, "screenOrientationFromString: unknown orientation string: " + aStr);
+ return DEFAULT_SCREEN_ORIENTATION;
+ }
+
+ /*
+ * Convert Gecko screen orientation to Android orientation.
+ *
+ * @param aScreenOrientation
+ * Gecko screen orientation.
+ * @return Android orientation. This conversion is lossy, the Android
+ * orientation does not differentiate between primary and secondary
+ * orientations.
+ */
+ public static int screenOrientationToAndroidOrientation(ScreenOrientation aScreenOrientation) {
+ switch (aScreenOrientation) {
+ case PORTRAIT:
+ case PORTRAIT_PRIMARY:
+ case PORTRAIT_SECONDARY:
+ return Configuration.ORIENTATION_PORTRAIT;
+ case LANDSCAPE:
+ case LANDSCAPE_PRIMARY:
+ case LANDSCAPE_SECONDARY:
+ return Configuration.ORIENTATION_LANDSCAPE;
+ case NONE:
+ case DEFAULT:
+ default:
+ return Configuration.ORIENTATION_UNDEFINED;
+ }
+ }
+
+
+ /*
+ * Convert Gecko screen orientation to Android ActivityInfo orientation.
+ * This is yet another orientation used by Android, but it's more detailed
+ * than the Android orientation.
+ * It is required for screen orientation locking and unlocking.
+ *
+ * @param aScreenOrientation
+ * Gecko screen orientation.
+ * @return Android ActivityInfo orientation.
+ */
+ public static int screenOrientationToActivityInfoOrientation(ScreenOrientation aScreenOrientation) {
+ switch (aScreenOrientation) {
+ case PORTRAIT:
+ case PORTRAIT_PRIMARY:
+ return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+ case PORTRAIT_SECONDARY:
+ return ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
+ case LANDSCAPE:
+ case LANDSCAPE_PRIMARY:
+ return ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+ case LANDSCAPE_SECONDARY:
+ return ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+ case DEFAULT:
+ case NONE:
+ return ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+ default:
+ return ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSharedPrefs.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSharedPrefs.java
new file mode 100644
index 0000000000..ec928dd86a
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSharedPrefs.java
@@ -0,0 +1,318 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.Arrays;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.os.StrictMode;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+/**
+ * {@code GeckoSharedPrefs} provides scoped SharedPreferences instances.
+ * You should use this API instead of using Context.getSharedPreferences()
+ * directly. There are four methods to get scoped SharedPreferences instances:
+ *
+ * forApp()
+ * Use it for app-wide, cross-profile pref keys.
+ * forCrashReporter()
+ * For the crash reporter, which runs in its own process.
+ * forProfile()
+ * Use it to fetch and store keys for the current profile.
+ * forProfileName()
+ * Use it to fetch and store keys from/for a specific profile.
+ *
+ * {@code GeckoSharedPrefs} has a notion of migrations. Migrations can used to
+ * migrate keys from one scope to another. You can trigger a new migration by
+ * incrementing PREFS_VERSION and updating migrateIfNecessary() accordingly.
+ *
+ * Migration history:
+ * 1: Move all PreferenceManager keys to app/profile scopes
+ * 2: Move the crash reporter's private preferences into their own scope
+ */
+@RobocopTarget
+public final class GeckoSharedPrefs {
+ private static final String LOGTAG = "GeckoSharedPrefs";
+
+ // Increment it to trigger a new migration
+ public static final int PREFS_VERSION = 2;
+
+ // Name for app-scoped prefs
+ public static final String APP_PREFS_NAME = "GeckoApp";
+
+ // Name for crash reporter prefs
+ public static final String CRASH_PREFS_NAME = "CrashReporter";
+
+ // Used when fetching profile-scoped prefs.
+ public static final String PROFILE_PREFS_NAME_PREFIX = "GeckoProfile-";
+
+ // The prefs key that holds the current migration
+ private static final String PREFS_VERSION_KEY = "gecko_shared_prefs_migration";
+
+ // For disabling migration when getting a SharedPreferences instance
+ private static final EnumSet<Flags> disableMigrations = EnumSet.of(Flags.DISABLE_MIGRATIONS);
+
+ // The keys that have to be moved from ProfileManager's default
+ // shared prefs to the profile from version 0 to 1.
+ private static final String[] PROFILE_MIGRATIONS_0_TO_1 = {
+ "home_panels",
+ "home_locale"
+ };
+
+ // The keys that have to be moved from the app prefs
+ // into the crash reporter's own prefs.
+ private static final String[] PROFILE_MIGRATIONS_1_TO_2 = {
+ "sendReport",
+ "includeUrl",
+ "allowContact",
+ "contactEmail"
+ };
+
+ // For optimizing the migration check in subsequent get() calls
+ private static volatile boolean migrationDone;
+
+ public enum Flags {
+ DISABLE_MIGRATIONS
+ }
+
+ public static SharedPreferences forApp(Context context) {
+ return forApp(context, EnumSet.noneOf(Flags.class));
+ }
+
+ /**
+ * Returns an app-scoped SharedPreferences instance. You can disable
+ * migrations by using the DISABLE_MIGRATIONS flag.
+ */
+ public static SharedPreferences forApp(Context context, EnumSet<Flags> flags) {
+ if (flags != null && !flags.contains(Flags.DISABLE_MIGRATIONS)) {
+ migrateIfNecessary(context);
+ }
+
+ return context.getSharedPreferences(APP_PREFS_NAME, 0);
+ }
+
+ public static SharedPreferences forCrashReporter(Context context) {
+ return forCrashReporter(context, EnumSet.noneOf(Flags.class));
+ }
+
+ /**
+ * Returns a crash-reporter-scoped SharedPreferences instance. You can disable
+ * migrations by using the DISABLE_MIGRATIONS flag.
+ */
+ public static SharedPreferences forCrashReporter(Context context, EnumSet<Flags> flags) {
+ if (flags != null && !flags.contains(Flags.DISABLE_MIGRATIONS)) {
+ migrateIfNecessary(context);
+ }
+
+ return context.getSharedPreferences(CRASH_PREFS_NAME, 0);
+ }
+
+ public static SharedPreferences forProfile(Context context) {
+ return forProfile(context, EnumSet.noneOf(Flags.class));
+ }
+
+ /**
+ * Returns a SharedPreferences instance scoped to the current profile
+ * in the app. You can disable migrations by using the DISABLE_MIGRATIONS
+ * flag.
+ */
+ public static SharedPreferences forProfile(Context context, EnumSet<Flags> flags) {
+ String profileName = GeckoProfile.get(context).getName();
+ if (profileName == null) {
+ throw new IllegalStateException("Could not get current profile name");
+ }
+
+ return forProfileName(context, profileName, flags);
+ }
+
+ public static SharedPreferences forProfileName(Context context, String profileName) {
+ return forProfileName(context, profileName, EnumSet.noneOf(Flags.class));
+ }
+
+ /**
+ * Returns an SharedPreferences instance scoped to the given profile name.
+ * You can disable migrations by using the DISABLE_MIGRATION flag.
+ */
+ public static SharedPreferences forProfileName(Context context, String profileName,
+ EnumSet<Flags> flags) {
+ if (flags != null && !flags.contains(Flags.DISABLE_MIGRATIONS)) {
+ migrateIfNecessary(context);
+ }
+
+ final String prefsName = PROFILE_PREFS_NAME_PREFIX + profileName;
+ return context.getSharedPreferences(prefsName, 0);
+ }
+
+ /**
+ * Returns the current version of the prefs.
+ */
+ public static int getVersion(Context context) {
+ return forApp(context, disableMigrations).getInt(PREFS_VERSION_KEY, 0);
+ }
+
+ /**
+ * Resets migration flag. Should only be used in tests.
+ */
+ public static synchronized void reset() {
+ migrationDone = false;
+ }
+
+ /**
+ * Performs all prefs migrations in the background thread to avoid StrictMode
+ * exceptions from reading/writing in the UI thread. This method will block
+ * the current thread until the migration is finished.
+ */
+ private static synchronized void migrateIfNecessary(final Context context) {
+ if (migrationDone) {
+ return;
+ }
+
+ // We deliberately perform the migration in the current thread (which
+ // is likely the UI thread) as this is actually cheaper than enforcing a
+ // context switch to another thread (see bug 940575).
+ // Avoid strict mode warnings when doing so.
+ final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+ StrictMode.allowThreadDiskWrites();
+ try {
+ performMigration(context);
+ } finally {
+ StrictMode.setThreadPolicy(savedPolicy);
+ }
+
+ migrationDone = true;
+ }
+
+ private static void performMigration(Context context) {
+ final SharedPreferences appPrefs = forApp(context, disableMigrations);
+
+ final int currentVersion = appPrefs.getInt(PREFS_VERSION_KEY, 0);
+ Log.d(LOGTAG, "Current version = " + currentVersion + ", prefs version = " + PREFS_VERSION);
+
+ if (currentVersion == PREFS_VERSION) {
+ return;
+ }
+
+ Log.d(LOGTAG, "Performing migration");
+
+ final Editor appEditor = appPrefs.edit();
+
+ // The migration always moves prefs to the default profile, not
+ // the current one. We might have to revisit this if we ever support
+ // multiple profiles.
+ final String defaultProfileName;
+ try {
+ defaultProfileName = GeckoProfile.getDefaultProfileName(context);
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to get default profile name for migration");
+ }
+
+ final Editor profileEditor = forProfileName(context, defaultProfileName, disableMigrations).edit();
+ final Editor crashEditor = forCrashReporter(context, disableMigrations).edit();
+
+ List<String> profileKeys;
+ Editor pmEditor = null;
+
+ for (int v = currentVersion + 1; v <= PREFS_VERSION; v++) {
+ Log.d(LOGTAG, "Migrating to version = " + v);
+
+ switch (v) {
+ case 1:
+ profileKeys = Arrays.asList(PROFILE_MIGRATIONS_0_TO_1);
+ pmEditor = migrateFromPreferenceManager(context, appEditor, profileEditor, profileKeys);
+ break;
+ case 2:
+ profileKeys = Arrays.asList(PROFILE_MIGRATIONS_1_TO_2);
+ migrateCrashReporterSettings(appPrefs, appEditor, crashEditor, profileKeys);
+ break;
+ }
+ }
+
+ // Update prefs version accordingly.
+ appEditor.putInt(PREFS_VERSION_KEY, PREFS_VERSION);
+
+ appEditor.apply();
+ profileEditor.apply();
+ crashEditor.apply();
+ if (pmEditor != null) {
+ pmEditor.apply();
+ }
+
+ Log.d(LOGTAG, "All keys have been migrated");
+ }
+
+ /**
+ * Moves all preferences stored in PreferenceManager's default prefs
+ * to either app or profile scopes. The profile-scoped keys are defined
+ * in given profileKeys list, all other keys are moved to the app scope.
+ */
+ public static Editor migrateFromPreferenceManager(Context context, Editor appEditor,
+ Editor profileEditor, List<String> profileKeys) {
+ Log.d(LOGTAG, "Migrating from PreferenceManager");
+
+ final SharedPreferences pmPrefs =
+ PreferenceManager.getDefaultSharedPreferences(context);
+
+ for (Map.Entry<String, ?> entry : pmPrefs.getAll().entrySet()) {
+ final String key = entry.getKey();
+
+ final Editor to;
+ if (profileKeys.contains(key)) {
+ to = profileEditor;
+ } else {
+ to = appEditor;
+ }
+
+ putEntry(to, key, entry.getValue());
+ }
+
+ // Clear PreferenceManager's prefs once we're done
+ // and return the Editor to be committed.
+ return pmPrefs.edit().clear();
+ }
+
+ /**
+ * Moves the crash reporter's preferences from the app-wide prefs
+ * into its own shared prefs to avoid cross-process pref accesses.
+ */
+ public static void migrateCrashReporterSettings(SharedPreferences appPrefs, Editor appEditor,
+ Editor crashEditor, List<String> profileKeys) {
+ Log.d(LOGTAG, "Migrating crash reporter settings");
+
+ for (Map.Entry<String, ?> entry : appPrefs.getAll().entrySet()) {
+ final String key = entry.getKey();
+
+ if (profileKeys.contains(key)) {
+ putEntry(crashEditor, key, entry.getValue());
+ appEditor.remove(key);
+ }
+ }
+ }
+
+ private static void putEntry(Editor to, String key, Object value) {
+ Log.d(LOGTAG, "Migrating key = " + key + " with value = " + value);
+
+ if (value instanceof String) {
+ to.putString(key, (String) value);
+ } else if (value instanceof Boolean) {
+ to.putBoolean(key, (Boolean) value);
+ } else if (value instanceof Long) {
+ to.putLong(key, (Long) value);
+ } else if (value instanceof Float) {
+ to.putFloat(key, (Float) value);
+ } else if (value instanceof Integer) {
+ to.putInt(key, (Integer) value);
+ } else {
+ throw new IllegalStateException("Unrecognized value type for key: " + key);
+ }
+ }
+} \ No newline at end of file
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java
new file mode 100644
index 0000000000..b57222a314
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java
@@ -0,0 +1,677 @@
+/* -*- 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.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.GeckoLoader;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Locale;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+public class GeckoThread extends Thread {
+ private static final String LOGTAG = "GeckoThread";
+
+ public enum State {
+ // After being loaded by class loader.
+ @WrapForJNI INITIAL(0),
+ // After launching Gecko thread
+ @WrapForJNI LAUNCHED(1),
+ // After loading the mozglue library.
+ @WrapForJNI MOZGLUE_READY(2),
+ // After loading the libxul library.
+ @WrapForJNI LIBS_READY(3),
+ // After initializing nsAppShell and JNI calls.
+ @WrapForJNI JNI_READY(4),
+ // After initializing profile and prefs.
+ @WrapForJNI PROFILE_READY(5),
+ // After initializing frontend JS
+ @WrapForJNI RUNNING(6),
+ // After leaving Gecko event loop
+ @WrapForJNI EXITING(3),
+ // After exiting GeckoThread (corresponding to "Gecko:Exited" event)
+ @WrapForJNI EXITED(0);
+
+ /* The rank is an arbitrary value reflecting the amount of components or features
+ * that are available for use. During startup and up to the RUNNING state, the
+ * rank value increases because more components are initialized and available for
+ * use. During shutdown and up to the EXITED state, the rank value decreases as
+ * components are shut down and become unavailable. EXITING has the same rank as
+ * LIBS_READY because both states have a similar amount of components available.
+ */
+ private final int rank;
+
+ private State(int rank) {
+ this.rank = rank;
+ }
+
+ public boolean is(final State other) {
+ return this == other;
+ }
+
+ public boolean isAtLeast(final State other) {
+ return this.rank >= other.rank;
+ }
+
+ public boolean isAtMost(final State other) {
+ return this.rank <= other.rank;
+ }
+
+ // Inclusive
+ public boolean isBetween(final State min, final State max) {
+ return this.rank >= min.rank && this.rank <= max.rank;
+ }
+ }
+
+ public static final State MIN_STATE = State.INITIAL;
+ public static final State MAX_STATE = State.EXITED;
+
+ private static volatile State sState = State.INITIAL;
+
+ private static class QueuedCall {
+ public Method method;
+ public Object target;
+ public Object[] args;
+ public State state;
+
+ public QueuedCall(final Method method, final Object target,
+ final Object[] args, final State state) {
+ this.method = method;
+ this.target = target;
+ this.args = args;
+ this.state = state;
+ }
+ }
+
+ private static final int QUEUED_CALLS_COUNT = 16;
+ private static final ArrayList<QueuedCall> QUEUED_CALLS = new ArrayList<>(QUEUED_CALLS_COUNT);
+
+ private static final Runnable UI_THREAD_CALLBACK = new Runnable() {
+ @Override
+ public void run() {
+ ThreadUtils.assertOnUiThread();
+ long nextDelay = runUiThreadCallback();
+ if (nextDelay >= 0) {
+ ThreadUtils.getUiHandler().postDelayed(this, nextDelay);
+ }
+ }
+ };
+
+ private static GeckoThread sGeckoThread;
+
+ @WrapForJNI
+ private static final ClassLoader clsLoader = GeckoThread.class.getClassLoader();
+ @WrapForJNI
+ private static MessageQueue msgQueue;
+
+ private GeckoProfile mProfile;
+
+ private final String mArgs;
+ private final String mAction;
+ private final boolean mDebugging;
+
+ GeckoThread(GeckoProfile profile, String args, String action, boolean debugging) {
+ mProfile = profile;
+ mArgs = args;
+ mAction = action;
+ mDebugging = debugging;
+
+ setName("Gecko");
+ }
+
+ public static boolean init(GeckoProfile profile, String args, String action, boolean debugging) {
+ ThreadUtils.assertOnUiThread();
+ if (isState(State.INITIAL) && sGeckoThread == null) {
+ sGeckoThread = new GeckoThread(profile, args, action, debugging);
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean canUseProfile(final Context context, final GeckoProfile profile,
+ final String profileName, final File profileDir) {
+ if (profileDir != null && !profileDir.isDirectory()) {
+ return false;
+ }
+
+ if (profile == null) {
+ // We haven't initialized; any profile is okay as long as we follow the guest mode setting.
+ return GeckoProfile.shouldUseGuestMode(context) ==
+ GeckoProfile.isGuestProfile(context, profileName, profileDir);
+ }
+
+ // We already initialized and have a profile; see if it matches ours.
+ try {
+ return profileDir == null ? profileName.equals(profile.getName()) :
+ profile.getDir().getCanonicalPath().equals(profileDir.getCanonicalPath());
+ } catch (final IOException e) {
+ Log.e(LOGTAG, "Cannot compare profile " + profileName);
+ return false;
+ }
+ }
+
+ public static boolean canUseProfile(final String profileName, final File profileDir) {
+ if (profileName == null) {
+ throw new IllegalArgumentException("Null profile name");
+ }
+ return canUseProfile(GeckoAppShell.getApplicationContext(), getActiveProfile(),
+ profileName, profileDir);
+ }
+
+ public static boolean initWithProfile(final String profileName, final File profileDir) {
+ if (profileName == null) {
+ throw new IllegalArgumentException("Null profile name");
+ }
+
+ final Context context = GeckoAppShell.getApplicationContext();
+ final GeckoProfile profile = getActiveProfile();
+
+ if (!canUseProfile(context, profile, profileName, profileDir)) {
+ // Profile is incompatible with current profile.
+ return false;
+ }
+
+ if (profile != null) {
+ // We already have a compatible profile.
+ return true;
+ }
+
+ // We haven't initialized yet; okay to initialize now.
+ return init(GeckoProfile.get(context, profileName, profileDir),
+ /* args */ null, /* action */ null, /* debugging */ false);
+ }
+
+ public static boolean launch() {
+ ThreadUtils.assertOnUiThread();
+ if (checkAndSetState(State.INITIAL, State.LAUNCHED)) {
+ sGeckoThread.start();
+ return true;
+ }
+ return false;
+ }
+
+ public static boolean isLaunched() {
+ return !isState(State.INITIAL);
+ }
+
+ @RobocopTarget
+ public static boolean isRunning() {
+ return isState(State.RUNNING);
+ }
+
+ // Invoke the given Method and handle checked Exceptions.
+ private static void invokeMethod(final Method method, final Object obj, final Object[] args) {
+ try {
+ method.setAccessible(true);
+ method.invoke(obj, args);
+ } catch (final IllegalAccessException e) {
+ throw new IllegalStateException("Unexpected exception", e);
+ } catch (final InvocationTargetException e) {
+ throw new UnsupportedOperationException("Cannot make call", e.getCause());
+ }
+ }
+
+ // Queue a call to the given method.
+ private static void queueNativeCallLocked(final Class<?> cls, final String methodName,
+ final Object obj, final Object[] args,
+ final State state) {
+ final ArrayList<Class<?>> argTypes = new ArrayList<>(args.length);
+ final ArrayList<Object> argValues = new ArrayList<>(args.length);
+
+ for (int i = 0; i < args.length; i++) {
+ if (args[i] instanceof Class) {
+ argTypes.add((Class<?>) args[i]);
+ argValues.add(args[++i]);
+ continue;
+ }
+ Class<?> argType = args[i].getClass();
+ if (argType == Boolean.class) argType = Boolean.TYPE;
+ else if (argType == Byte.class) argType = Byte.TYPE;
+ else if (argType == Character.class) argType = Character.TYPE;
+ else if (argType == Double.class) argType = Double.TYPE;
+ else if (argType == Float.class) argType = Float.TYPE;
+ else if (argType == Integer.class) argType = Integer.TYPE;
+ else if (argType == Long.class) argType = Long.TYPE;
+ else if (argType == Short.class) argType = Short.TYPE;
+ argTypes.add(argType);
+ argValues.add(args[i]);
+ }
+ final Method method;
+ try {
+ method = cls.getDeclaredMethod(
+ methodName, argTypes.toArray(new Class<?>[argTypes.size()]));
+ } catch (final NoSuchMethodException e) {
+ throw new IllegalArgumentException("Cannot find method", e);
+ }
+
+ if (!Modifier.isNative(method.getModifiers())) {
+ // As a precaution, we disallow queuing non-native methods. Queuing non-native
+ // methods is dangerous because the method could end up being called on either
+ // the original thread or the Gecko thread depending on timing. Native methods
+ // usually handle this by posting an event to the Gecko thread automatically,
+ // but there is no automatic mechanism for non-native methods.
+ throw new UnsupportedOperationException("Not allowed to queue non-native methods");
+ }
+
+ if (isStateAtLeast(state)) {
+ invokeMethod(method, obj, argValues.toArray());
+ return;
+ }
+
+ QUEUED_CALLS.add(new QueuedCall(
+ method, obj, argValues.toArray(), state));
+ }
+
+ /**
+ * Queue a call to the given static method until Gecko is in the given state.
+ *
+ * @param state The Gecko state in which the native call could be executed.
+ * Default is State.RUNNING, which means this queued call will
+ * run when Gecko is at or after RUNNING state.
+ * @param cls Class that declares the static method.
+ * @param methodName Name of the static method.
+ * @param args Args to call the static method with; to specify a parameter type,
+ * pass in a Class instance first, followed by the value.
+ */
+ public static void queueNativeCallUntil(final State state, final Class<?> cls,
+ final String methodName, final Object... args) {
+ synchronized (QUEUED_CALLS) {
+ queueNativeCallLocked(cls, methodName, null, args, state);
+ }
+ }
+
+ /**
+ * Queue a call to the given static method until Gecko is in the RUNNING state.
+ */
+ public static void queueNativeCall(final Class<?> cls, final String methodName,
+ final Object... args) {
+ synchronized (QUEUED_CALLS) {
+ queueNativeCallLocked(cls, methodName, null, args, State.RUNNING);
+ }
+ }
+
+ /**
+ * Queue a call to the given instance method until Gecko is in the given state.
+ *
+ * @param state The Gecko state in which the native call could be executed.
+ * @param obj Object that declares the instance method.
+ * @param methodName Name of the instance method.
+ * @param args Args to call the instance method with; to specify a parameter type,
+ * pass in a Class instance first, followed by the value.
+ */
+ public static void queueNativeCallUntil(final State state, final Object obj,
+ final String methodName, final Object... args) {
+ synchronized (QUEUED_CALLS) {
+ queueNativeCallLocked(obj.getClass(), methodName, obj, args, state);
+ }
+ }
+
+ /**
+ * Queue a call to the given instance method until Gecko is in the RUNNING state.
+ */
+ public static void queueNativeCall(final Object obj, final String methodName,
+ final Object... args) {
+ synchronized (QUEUED_CALLS) {
+ queueNativeCallLocked(obj.getClass(), methodName, obj, args, State.RUNNING);
+ }
+ }
+
+ // Run all queued methods
+ private static void flushQueuedNativeCallsLocked(final State state) {
+ int lastSkipped = -1;
+ for (int i = 0; i < QUEUED_CALLS.size(); i++) {
+ final QueuedCall call = QUEUED_CALLS.get(i);
+ if (call == null) {
+ // We already handled the call.
+ continue;
+ }
+ if (!state.isAtLeast(call.state)) {
+ // The call is not ready yet; skip it.
+ lastSkipped = i;
+ continue;
+ }
+ // Mark as handled.
+ QUEUED_CALLS.set(i, null);
+
+ invokeMethod(call.method, call.target, call.args);
+ }
+ if (lastSkipped < 0) {
+ // We're done here; release the memory
+ QUEUED_CALLS.clear();
+ QUEUED_CALLS.trimToSize();
+ } else if (lastSkipped < QUEUED_CALLS.size() - 1) {
+ // We skipped some; free up null entries at the end,
+ // but keep all the previous entries for later.
+ QUEUED_CALLS.subList(lastSkipped + 1, QUEUED_CALLS.size()).clear();
+ }
+ }
+
+ private static String initGeckoEnvironment() {
+ final Context context = GeckoAppShell.getApplicationContext();
+ GeckoLoader.loadMozGlue(context);
+ setState(State.MOZGLUE_READY);
+
+ final Locale locale = Locale.getDefault();
+ final Resources res = context.getResources();
+ if (locale.toString().equalsIgnoreCase("zh_hk")) {
+ final Locale mappedLocale = Locale.TRADITIONAL_CHINESE;
+ Locale.setDefault(mappedLocale);
+ Configuration config = res.getConfiguration();
+ config.locale = mappedLocale;
+ res.updateConfiguration(config, null);
+ }
+
+ String[] pluginDirs = null;
+ try {
+ pluginDirs = GeckoAppShell.getPluginDirectories();
+ } catch (Exception e) {
+ Log.w(LOGTAG, "Caught exception getting plugin dirs.", e);
+ }
+
+ final String resourcePath = context.getPackageResourcePath();
+ GeckoLoader.setupGeckoEnvironment(context, pluginDirs, context.getFilesDir().getPath());
+
+ GeckoLoader.loadSQLiteLibs(context, resourcePath);
+ GeckoLoader.loadNSSLibs(context, resourcePath);
+ GeckoLoader.loadGeckoLibs(context, resourcePath);
+ setState(State.LIBS_READY);
+
+ return resourcePath;
+ }
+
+ private String addCustomProfileArg(String args) {
+ String profileArg = "";
+
+ // Make sure a profile exists.
+ final GeckoProfile profile = getProfile();
+ profile.getDir(); // call the lazy initializer
+
+ // If args don't include the profile, make sure it's included.
+ if (args == null || !args.matches(".*\\B-(P|profile)\\s+\\S+.*")) {
+ if (profile.isCustomProfile()) {
+ profileArg = " -profile " + profile.getDir().getAbsolutePath();
+ } else {
+ profileArg = " -P " + profile.getName();
+ }
+ }
+
+ return (args != null ? args : "") + profileArg;
+ }
+
+ private String getGeckoArgs(final String apkPath) {
+ // argv[0] is the program name, which for us is the package name.
+ final Context context = GeckoAppShell.getApplicationContext();
+ final StringBuilder args = new StringBuilder(context.getPackageName());
+ args.append(" -greomni ").append(apkPath);
+
+ final String userArgs = addCustomProfileArg(mArgs);
+ if (userArgs != null) {
+ args.append(' ').append(userArgs);
+ }
+
+ // In un-official builds, we want to load Javascript resources fresh
+ // with each build. In official builds, the startup cache is purged by
+ // the buildid mechanism, but most un-official builds don't bump the
+ // buildid, so we purge here instead.
+ if (!AppConstants.MOZILLA_OFFICIAL) {
+ Log.w(LOGTAG, "STARTUP PERFORMANCE WARNING: un-official build: purging the " +
+ "startup (JavaScript) caches.");
+ args.append(" -purgecaches");
+ }
+
+ return args.toString();
+ }
+
+ public static GeckoProfile getActiveProfile() {
+ if (sGeckoThread == null) {
+ return null;
+ }
+ final GeckoProfile profile = sGeckoThread.mProfile;
+ if (profile != null) {
+ return profile;
+ }
+ return sGeckoThread.getProfile();
+ }
+
+ public synchronized GeckoProfile getProfile() {
+ if (mProfile == null) {
+ final Context context = GeckoAppShell.getApplicationContext();
+ mProfile = GeckoProfile.initFromArgs(context, mArgs);
+ }
+ return mProfile;
+ }
+
+ @Override
+ public void run() {
+ Log.i(LOGTAG, "preparing to run Gecko");
+
+ Looper.prepare();
+ GeckoThread.msgQueue = Looper.myQueue();
+ ThreadUtils.sGeckoThread = this;
+ ThreadUtils.sGeckoHandler = new Handler();
+
+ // Preparation for pumpMessageLoop()
+ final MessageQueue.IdleHandler idleHandler = new MessageQueue.IdleHandler() {
+ @Override public boolean queueIdle() {
+ final Handler geckoHandler = ThreadUtils.sGeckoHandler;
+ Message idleMsg = Message.obtain(geckoHandler);
+ // Use |Message.obj == GeckoHandler| to identify our "queue is empty" message
+ idleMsg.obj = geckoHandler;
+ geckoHandler.sendMessageAtFrontOfQueue(idleMsg);
+ // Keep this IdleHandler
+ return true;
+ }
+ };
+ Looper.myQueue().addIdleHandler(idleHandler);
+
+ if (mDebugging) {
+ try {
+ Thread.sleep(5 * 1000 /* 5 seconds */);
+ } catch (final InterruptedException e) {
+ }
+ }
+
+ final String args = getGeckoArgs(initGeckoEnvironment());
+
+ // This can only happen after the call to initGeckoEnvironment
+ // above, because otherwise the JNI code hasn't been loaded yet.
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override public void run() {
+ registerUiThread();
+ }
+ });
+
+ Log.w(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - runGecko");
+
+ if (!AppConstants.MOZILLA_OFFICIAL) {
+ Log.i(LOGTAG, "RunGecko - args = " + args);
+ }
+
+ // And go.
+ GeckoLoader.nativeRun(args);
+
+ // And... we're done.
+ setState(State.EXITED);
+
+ try {
+ final JSONObject msg = new JSONObject();
+ msg.put("type", "Gecko:Exited");
+ GeckoAppShell.getGeckoInterface().getAppEventDispatcher().dispatchEvent(msg, null);
+ EventDispatcher.getInstance().dispatchEvent(msg, null);
+ } catch (final JSONException e) {
+ Log.e(LOGTAG, "unable to dispatch event", e);
+ }
+
+ // Remove pumpMessageLoop() idle handler
+ Looper.myQueue().removeIdleHandler(idleHandler);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean pumpMessageLoop(final Message msg) {
+ final Handler geckoHandler = ThreadUtils.sGeckoHandler;
+
+ if (msg.obj == geckoHandler && msg.getTarget() == geckoHandler) {
+ // Our "queue is empty" message; see runGecko()
+ return false;
+ }
+
+ if (msg.getTarget() == null) {
+ Looper.myLooper().quit();
+ } else {
+ msg.getTarget().dispatchMessage(msg);
+ }
+
+ return true;
+ }
+
+ /**
+ * Check that the current Gecko thread state matches the given state.
+ *
+ * @param state State to check
+ * @return True if the current Gecko thread state matches
+ */
+ public static boolean isState(final State state) {
+ return sState.is(state);
+ }
+
+ /**
+ * Check that the current Gecko thread state is at the given state or further along,
+ * according to the order defined in the State enum.
+ *
+ * @param state State to check
+ * @return True if the current Gecko thread state matches
+ */
+ public static boolean isStateAtLeast(final State state) {
+ return sState.isAtLeast(state);
+ }
+
+ /**
+ * Check that the current Gecko thread state is at the given state or prior,
+ * according to the order defined in the State enum.
+ *
+ * @param state State to check
+ * @return True if the current Gecko thread state matches
+ */
+ public static boolean isStateAtMost(final State state) {
+ return sState.isAtMost(state);
+ }
+
+ /**
+ * Check that the current Gecko thread state falls into an inclusive range of states,
+ * according to the order defined in the State enum.
+ *
+ * @param minState Lower range of allowable states
+ * @param maxState Upper range of allowable states
+ * @return True if the current Gecko thread state matches
+ */
+ public static boolean isStateBetween(final State minState, final State maxState) {
+ return sState.isBetween(minState, maxState);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void setState(final State newState) {
+ ThreadUtils.assertOnGeckoThread();
+ synchronized (QUEUED_CALLS) {
+ flushQueuedNativeCallsLocked(newState);
+ sState = newState;
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean checkAndSetState(final State currentState, final State newState) {
+ synchronized (QUEUED_CALLS) {
+ if (sState == currentState) {
+ flushQueuedNativeCallsLocked(newState);
+ sState = newState;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @WrapForJNI(stubName = "SpeculativeConnect")
+ private static native void speculativeConnectNative(String uri);
+
+ public static void speculativeConnect(final String uri) {
+ // This is almost always called before Gecko loads, so we don't
+ // bother checking here if Gecko is actually loaded or not.
+ // Speculative connection depends on proxy settings,
+ // so the earliest it can happen is after profile is ready.
+ queueNativeCallUntil(State.PROFILE_READY, GeckoThread.class,
+ "speculativeConnectNative", uri);
+ }
+
+ @WrapForJNI @RobocopTarget
+ public static native void waitOnGecko();
+
+ @WrapForJNI(stubName = "OnPause", dispatchTo = "gecko")
+ private static native void nativeOnPause();
+
+ public static void onPause() {
+ if (isStateAtLeast(State.PROFILE_READY)) {
+ nativeOnPause();
+ } else {
+ queueNativeCallUntil(State.PROFILE_READY, GeckoThread.class,
+ "nativeOnPause");
+ }
+ }
+
+ @WrapForJNI(stubName = "OnResume", dispatchTo = "gecko")
+ private static native void nativeOnResume();
+
+ public static void onResume() {
+ if (isStateAtLeast(State.PROFILE_READY)) {
+ nativeOnResume();
+ } else {
+ queueNativeCallUntil(State.PROFILE_READY, GeckoThread.class,
+ "nativeOnResume");
+ }
+ }
+
+ @WrapForJNI(stubName = "CreateServices", dispatchTo = "gecko")
+ private static native void nativeCreateServices(String category, String data);
+
+ public static void createServices(final String category, final String data) {
+ if (isStateAtLeast(State.PROFILE_READY)) {
+ nativeCreateServices(category, data);
+ } else {
+ queueNativeCallUntil(State.PROFILE_READY, GeckoThread.class, "nativeCreateServices",
+ String.class, category, String.class, data);
+ }
+ }
+
+ // Implemented in mozglue/android/APKOpen.cpp.
+ /* package */ static native void registerUiThread();
+
+ @WrapForJNI(calledFrom = "ui")
+ /* package */ static native long runUiThreadCallback();
+
+ @WrapForJNI
+ private static void requestUiThreadCallback(long delay) {
+ ThreadUtils.getUiHandler().postDelayed(UI_THREAD_CALLBACK, delay);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoView.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoView.java
new file mode 100644
index 0000000000..93d7383614
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoView.java
@@ -0,0 +1,736 @@
+/* -*- 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 java.util.Set;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.annotation.ReflectionTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoEventListener;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+public class GeckoView extends LayerView
+ implements ContextGetter, GeckoEventListener, NativeEventListener {
+
+ private static final String DEFAULT_SHARED_PREFERENCES_FILE = "GeckoView";
+ private static final String LOGTAG = "GeckoView";
+
+ private ChromeDelegate mChromeDelegate;
+ private ContentDelegate mContentDelegate;
+
+ private InputConnectionListener mInputConnectionListener;
+
+ protected boolean onAttachedToWindowCalled;
+ protected String chromeURI = getGeckoInterface().getDefaultChromeURI();
+ protected int screenId = 0; // default to the primary screen
+
+ @Override
+ public void handleMessage(final String event, final JSONObject message) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (event.equals("Gecko:Ready")) {
+ handleReady(message);
+ } else if (event.equals("Content:StateChange")) {
+ handleStateChange(message);
+ } else if (event.equals("Content:LoadError")) {
+ handleLoadError(message);
+ } else if (event.equals("Content:PageShow")) {
+ handlePageShow(message);
+ } else if (event.equals("DOMTitleChanged")) {
+ handleTitleChanged(message);
+ } else if (event.equals("Link:Favicon")) {
+ handleLinkFavicon(message);
+ } else if (event.equals("Prompt:Show") || event.equals("Prompt:ShowTop")) {
+ handlePrompt(message);
+ } else if (event.equals("Accessibility:Event")) {
+ int mode = getImportantForAccessibility();
+ if (mode == View.IMPORTANT_FOR_ACCESSIBILITY_YES ||
+ mode == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ GeckoAccessibility.sendAccessibilityEvent(message);
+ }
+ }
+ } catch (Exception e) {
+ Log.e(LOGTAG, "handleMessage threw for " + event, e);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) {
+ try {
+ if ("Accessibility:Ready".equals(event)) {
+ GeckoAccessibility.updateAccessibilitySettings(getContext());
+ } else if ("GeckoView:Message".equals(event)) {
+ // We need to pull out the bundle while on the Gecko thread.
+ NativeJSObject json = message.optObject("data", null);
+ if (json == null) {
+ // Must have payload to call the message handler.
+ return;
+ }
+ final Bundle data = json.toBundle();
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ handleScriptMessage(data, callback);
+ }
+ });
+ }
+ } catch (Exception e) {
+ Log.w(LOGTAG, "handleMessage threw for " + event, e);
+ }
+ }
+
+ @WrapForJNI(dispatchTo = "proxy")
+ protected static final class Window extends JNIObject {
+ @WrapForJNI(skip = true)
+ /* package */ Window() {}
+
+ static native void open(Window instance, GeckoView view, Object compositor,
+ String chromeURI, int screenId);
+
+ @Override protected native void disposeNative();
+ native void close();
+ native void reattach(GeckoView view, Object compositor);
+ native void loadUri(String uri, int flags);
+ }
+
+ // Object to hold onto our nsWindow connection when GeckoView gets destroyed.
+ private static class StateBinder extends Binder implements Parcelable {
+ public final Parcelable superState;
+ public final Window window;
+
+ public StateBinder(Parcelable superState, Window window) {
+ this.superState = superState;
+ this.window = window;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ // Always write out the super-state, so that even if we lose this binder, we
+ // will still have something to pass into super.onRestoreInstanceState.
+ out.writeParcelable(superState, flags);
+ out.writeStrongBinder(this);
+ }
+
+ @ReflectionTarget
+ public static final Parcelable.Creator<StateBinder> CREATOR
+ = new Parcelable.Creator<StateBinder>() {
+ @Override
+ public StateBinder createFromParcel(Parcel in) {
+ final Parcelable superState = in.readParcelable(null);
+ final IBinder binder = in.readStrongBinder();
+ if (binder instanceof StateBinder) {
+ return (StateBinder) binder;
+ }
+ // Not the original object we saved; return null state.
+ return new StateBinder(superState, null);
+ }
+
+ @Override
+ public StateBinder[] newArray(int size) {
+ return new StateBinder[size];
+ }
+ };
+ }
+
+ protected Window window;
+ private boolean stateSaved;
+
+ public GeckoView(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public GeckoView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ private void init(Context context) {
+ if (GeckoAppShell.getApplicationContext() == null) {
+ GeckoAppShell.setApplicationContext(context.getApplicationContext());
+ }
+
+ // Set the GeckoInterface if the context is an activity and the GeckoInterface
+ // has not already been set
+ if (context instanceof Activity && getGeckoInterface() == null) {
+ setGeckoInterface(new BaseGeckoInterface(context));
+ GeckoAppShell.setContextGetter(this);
+ }
+
+ // Perform common initialization for Fennec/GeckoView.
+ GeckoAppShell.setLayerView(this);
+
+ initializeView(EventDispatcher.getInstance());
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState()
+ {
+ final Parcelable superState = super.onSaveInstanceState();
+ stateSaved = true;
+ return new StateBinder(superState, this.window);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(final Parcelable state)
+ {
+ final StateBinder stateBinder = (StateBinder) state;
+
+ if (stateBinder.window != null) {
+ this.window = stateBinder.window;
+ }
+ stateSaved = false;
+
+ if (onAttachedToWindowCalled) {
+ reattachWindow();
+ }
+
+ // We have to always call super.onRestoreInstanceState because View keeps
+ // track of these calls and throws an exception when we don't call it.
+ super.onRestoreInstanceState(stateBinder.superState);
+ }
+
+ protected void openWindow() {
+
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ Window.open(window, this, getCompositor(),
+ chromeURI, screenId);
+ } else {
+ GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY, Window.class,
+ "open", window, GeckoView.class, this, Object.class, getCompositor(),
+ String.class, chromeURI, screenId);
+ }
+ }
+
+ protected void reattachWindow() {
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ window.reattach(this, getCompositor());
+ } else {
+ GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
+ window, "reattach", GeckoView.class, this, Object.class, getCompositor());
+ }
+ }
+
+ @Override
+ public void onAttachedToWindow()
+ {
+ final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
+
+ if (window == null) {
+ // Open a new nsWindow if we didn't have one from before.
+ window = new Window();
+ openWindow();
+ } else {
+ reattachWindow();
+ }
+
+ super.onAttachedToWindow();
+
+ onAttachedToWindowCalled = true;
+ }
+
+ @Override
+ public void onDetachedFromWindow()
+ {
+ super.onDetachedFromWindow();
+ super.destroy();
+
+ if (stateSaved) {
+ // If we saved state earlier, we don't want to close the nsWindow.
+ return;
+ }
+
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ window.close();
+ window.disposeNative();
+ } else {
+ GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
+ window, "close");
+ GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
+ window, "disposeNative");
+ }
+
+ onAttachedToWindowCalled = false;
+ }
+
+ @WrapForJNI public static final int LOAD_DEFAULT = 0;
+ @WrapForJNI public static final int LOAD_NEW_TAB = 1;
+ @WrapForJNI public static final int LOAD_SWITCH_TAB = 2;
+
+ public void loadUri(String uri, int flags) {
+ if (window == null) {
+ throw new IllegalStateException("Not attached to window");
+ }
+
+ if (GeckoThread.isRunning()) {
+ window.loadUri(uri, flags);
+ } else {
+ GeckoThread.queueNativeCall(window, "loadUri", String.class, uri, flags);
+ }
+ }
+
+ /* package */ void setInputConnectionListener(final InputConnectionListener icl) {
+ mInputConnectionListener = icl;
+ }
+
+ @Override
+ public Handler getHandler() {
+ if (mInputConnectionListener != null) {
+ return mInputConnectionListener.getHandler(super.getHandler());
+ }
+ return super.getHandler();
+ }
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ if (mInputConnectionListener != null) {
+ return mInputConnectionListener.onCreateInputConnection(outAttrs);
+ }
+ return null;
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ if (super.onKeyPreIme(keyCode, event)) {
+ return true;
+ }
+ return mInputConnectionListener != null &&
+ mInputConnectionListener.onKeyPreIme(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (super.onKeyUp(keyCode, event)) {
+ return true;
+ }
+ return mInputConnectionListener != null &&
+ mInputConnectionListener.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (super.onKeyDown(keyCode, event)) {
+ return true;
+ }
+ return mInputConnectionListener != null &&
+ mInputConnectionListener.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+ if (super.onKeyLongPress(keyCode, event)) {
+ return true;
+ }
+ return mInputConnectionListener != null &&
+ mInputConnectionListener.onKeyLongPress(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ if (super.onKeyMultiple(keyCode, repeatCount, event)) {
+ return true;
+ }
+ return mInputConnectionListener != null &&
+ mInputConnectionListener.onKeyMultiple(keyCode, repeatCount, event);
+ }
+
+ /* package */ boolean isIMEEnabled() {
+ return mInputConnectionListener != null &&
+ mInputConnectionListener.isIMEEnabled();
+ }
+
+ public void importScript(final String url) {
+ if (url.startsWith("resource://android/assets/")) {
+ GeckoAppShell.notifyObservers("GeckoView:ImportScript", url);
+ return;
+ }
+
+ throw new IllegalArgumentException("Must import script from 'resources://android/assets/' location.");
+ }
+
+ private void handleReady(final JSONObject message) {
+ if (mChromeDelegate != null) {
+ mChromeDelegate.onReady(this);
+ }
+ }
+
+ private void handleStateChange(final JSONObject message) throws JSONException {
+ int state = message.getInt("state");
+ if ((state & GeckoAppShell.WPL_STATE_IS_NETWORK) != 0) {
+ if ((state & GeckoAppShell.WPL_STATE_START) != 0) {
+ if (mContentDelegate != null) {
+ int id = message.getInt("tabID");
+ mContentDelegate.onPageStart(this, new Browser(id), message.getString("uri"));
+ }
+ } else if ((state & GeckoAppShell.WPL_STATE_STOP) != 0) {
+ if (mContentDelegate != null) {
+ int id = message.getInt("tabID");
+ mContentDelegate.onPageStop(this, new Browser(id), message.getBoolean("success"));
+ }
+ }
+ }
+ }
+
+ private void handleLoadError(final JSONObject message) throws JSONException {
+ if (mContentDelegate != null) {
+ int id = message.getInt("tabID");
+ mContentDelegate.onPageStop(GeckoView.this, new Browser(id), false);
+ }
+ }
+
+ private void handlePageShow(final JSONObject message) throws JSONException {
+ if (mContentDelegate != null) {
+ int id = message.getInt("tabID");
+ mContentDelegate.onPageShow(GeckoView.this, new Browser(id));
+ }
+ }
+
+ private void handleTitleChanged(final JSONObject message) throws JSONException {
+ if (mContentDelegate != null) {
+ int id = message.getInt("tabID");
+ mContentDelegate.onReceivedTitle(GeckoView.this, new Browser(id), message.getString("title"));
+ }
+ }
+
+ private void handleLinkFavicon(final JSONObject message) throws JSONException {
+ if (mContentDelegate != null) {
+ int id = message.getInt("tabID");
+ mContentDelegate.onReceivedFavicon(GeckoView.this, new Browser(id), message.getString("href"), message.getInt("size"));
+ }
+ }
+
+ private void handlePrompt(final JSONObject message) throws JSONException {
+ if (mChromeDelegate != null) {
+ String hint = message.optString("hint");
+ if ("alert".equals(hint)) {
+ String text = message.optString("text");
+ mChromeDelegate.onAlert(GeckoView.this, null, text, new PromptResult(message));
+ } else if ("confirm".equals(hint)) {
+ String text = message.optString("text");
+ mChromeDelegate.onConfirm(GeckoView.this, null, text, new PromptResult(message));
+ } else if ("prompt".equals(hint)) {
+ String text = message.optString("text");
+ String defaultValue = message.optString("textbox0");
+ mChromeDelegate.onPrompt(GeckoView.this, null, text, defaultValue, new PromptResult(message));
+ } else if ("remotedebug".equals(hint)) {
+ mChromeDelegate.onDebugRequest(GeckoView.this, new PromptResult(message));
+ }
+ }
+ }
+
+ private void handleScriptMessage(final Bundle data, final EventCallback callback) {
+ if (mChromeDelegate != null) {
+ MessageResult result = null;
+ if (callback != null) {
+ result = new MessageResult(callback);
+ }
+ mChromeDelegate.onScriptMessage(GeckoView.this, data, result);
+ }
+ }
+
+ /**
+ * Set the chrome callback handler.
+ * This will replace the current handler.
+ * @param chrome An implementation of GeckoViewChrome.
+ */
+ public void setChromeDelegate(ChromeDelegate chrome) {
+ mChromeDelegate = chrome;
+ }
+
+ /**
+ * Set the content callback handler.
+ * This will replace the current handler.
+ * @param content An implementation of ContentDelegate.
+ */
+ public void setContentDelegate(ContentDelegate content) {
+ mContentDelegate = content;
+ }
+
+ public static void setGeckoInterface(final BaseGeckoInterface geckoInterface) {
+ GeckoAppShell.setGeckoInterface(geckoInterface);
+ }
+
+ public static GeckoAppShell.GeckoInterface getGeckoInterface() {
+ return GeckoAppShell.getGeckoInterface();
+ }
+
+ protected String getSharedPreferencesFile() {
+ return DEFAULT_SHARED_PREFERENCES_FILE;
+ }
+
+ @Override
+ public SharedPreferences getSharedPreferences() {
+ return getContext().getSharedPreferences(getSharedPreferencesFile(), 0);
+ }
+
+ /**
+ * Wrapper for a browser in the GeckoView container. Associated with a browser
+ * element in the Gecko system.
+ */
+ public class Browser {
+ private final int mId;
+ private Browser(int Id) {
+ mId = Id;
+ }
+
+ /**
+ * Get the ID of the Browser. This is the same ID used by Gecko for it's underlying
+ * browser element.
+ * @return The integer ID of the Browser.
+ */
+ private int getId() {
+ return mId;
+ }
+
+ /**
+ * Load a URL resource into the Browser.
+ * @param url The URL string.
+ */
+ public void loadUrl(String url) {
+ JSONObject args = new JSONObject();
+ try {
+ args.put("url", url);
+ args.put("parentId", -1);
+ args.put("newTab", false);
+ args.put("tabID", mId);
+ } catch (Exception e) {
+ Log.w(LOGTAG, "Error building JSON arguments for loadUrl.", e);
+ }
+ GeckoAppShell.notifyObservers("Tab:Load", args.toString());
+ }
+ }
+
+ /* Provides a means for the client to indicate whether a JavaScript
+ * dialog request should proceed. An instance of this class is passed to
+ * various GeckoViewChrome callback actions.
+ */
+ public class PromptResult {
+ private final int RESULT_OK = 0;
+ private final int RESULT_CANCEL = 1;
+
+ private final JSONObject mMessage;
+
+ public PromptResult(JSONObject message) {
+ mMessage = message;
+ }
+
+ private JSONObject makeResult(int resultCode) {
+ JSONObject result = new JSONObject();
+ try {
+ result.put("button", resultCode);
+ } catch (JSONException ex) { }
+ return result;
+ }
+
+ /**
+ * Handle a confirmation response from the user.
+ */
+ public void confirm() {
+ JSONObject result = makeResult(RESULT_OK);
+ EventDispatcher.sendResponse(mMessage, result);
+ }
+
+ /**
+ * Handle a confirmation response from the user.
+ * @param value String value to return to the browser context.
+ */
+ public void confirmWithValue(String value) {
+ JSONObject result = makeResult(RESULT_OK);
+ try {
+ result.put("textbox0", value);
+ } catch (JSONException ex) { }
+ EventDispatcher.sendResponse(mMessage, result);
+ }
+
+ /**
+ * Handle a cancellation response from the user.
+ */
+ public void cancel() {
+ JSONObject result = makeResult(RESULT_CANCEL);
+ EventDispatcher.sendResponse(mMessage, result);
+ }
+ }
+
+ /* Provides a means for the client to respond to a script message with some data.
+ * An instance of this class is passed to GeckoViewChrome.onScriptMessage.
+ */
+ public class MessageResult {
+ private final EventCallback mCallback;
+
+ public MessageResult(EventCallback callback) {
+ if (callback == null) {
+ throw new IllegalArgumentException("EventCallback should not be null.");
+ }
+ mCallback = callback;
+ }
+
+ private JSONObject bundleToJSON(Bundle data) {
+ JSONObject result = new JSONObject();
+ if (data == null) {
+ return result;
+ }
+
+ final Set<String> keys = data.keySet();
+ for (String key : keys) {
+ try {
+ result.put(key, data.get(key));
+ } catch (JSONException e) {
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Handle a successful response to a script message.
+ * @param value Bundle value to return to the script context.
+ */
+ public void success(Bundle data) {
+ mCallback.sendSuccess(bundleToJSON(data));
+ }
+
+ /**
+ * Handle a failure response to a script message.
+ */
+ public void failure(Bundle data) {
+ mCallback.sendError(bundleToJSON(data));
+ }
+ }
+
+ public interface ChromeDelegate {
+ /**
+ * Tell the host application that Gecko is ready to handle requests.
+ * @param view The GeckoView that initiated the callback.
+ */
+ public void onReady(GeckoView view);
+
+ /**
+ * Tell the host application to display an alert dialog.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is loading the content.
+ * @param message The string to display in the dialog.
+ * @param result A PromptResult used to send back the result without blocking.
+ * Defaults to cancel requests.
+ */
+ public void onAlert(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result);
+
+ /**
+ * Tell the host application to display a confirmation dialog.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is loading the content.
+ * @param message The string to display in the dialog.
+ * @param result A PromptResult used to send back the result without blocking.
+ * Defaults to cancel requests.
+ */
+ public void onConfirm(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result);
+
+ /**
+ * Tell the host application to display an input prompt dialog.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is loading the content.
+ * @param message The string to display in the dialog.
+ * @param defaultValue The string to use as default input.
+ * @param result A PromptResult used to send back the result without blocking.
+ * Defaults to cancel requests.
+ */
+ public void onPrompt(GeckoView view, GeckoView.Browser browser, String message, String defaultValue, GeckoView.PromptResult result);
+
+ /**
+ * Tell the host application to display a remote debugging request dialog.
+ * @param view The GeckoView that initiated the callback.
+ * @param result A PromptResult used to send back the result without blocking.
+ * Defaults to cancel requests.
+ */
+ public void onDebugRequest(GeckoView view, GeckoView.PromptResult result);
+
+ /**
+ * Receive a message from an imported script.
+ * @param view The GeckoView that initiated the callback.
+ * @param data Bundle of data sent with the message. Never null.
+ * @param result A MessageResult used to send back a response without blocking. Can be null.
+ * Defaults to do nothing.
+ */
+ public void onScriptMessage(GeckoView view, Bundle data, GeckoView.MessageResult result);
+ }
+
+ public interface ContentDelegate {
+ /**
+ * A Browser has started loading content from the network.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is loading the content.
+ * @param url The resource being loaded.
+ */
+ public void onPageStart(GeckoView view, GeckoView.Browser browser, String url);
+
+ /**
+ * A Browser has finished loading content from the network.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that was loading the content.
+ * @param success Whether the page loaded successfully or an error occurred.
+ */
+ public void onPageStop(GeckoView view, GeckoView.Browser browser, boolean success);
+
+ /**
+ * A Browser is displaying content. This page could have been loaded via
+ * network or from the session history.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is showing the content.
+ */
+ public void onPageShow(GeckoView view, GeckoView.Browser browser);
+
+ /**
+ * A page title was discovered in the content or updated after the content
+ * loaded.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is showing the content.
+ * @param title The title sent from the content.
+ */
+ public void onReceivedTitle(GeckoView view, GeckoView.Browser browser, String title);
+
+ /**
+ * A link element was discovered in the content or updated after the content
+ * loaded that specifies a favicon.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is showing the content.
+ * @param url The href of the link element specifying the favicon.
+ * @param size The maximum size specified for the favicon, or -1 for any size.
+ */
+ public void onReceivedFavicon(GeckoView view, GeckoView.Browser browser, String url, int size);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewChrome.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewChrome.java
new file mode 100644
index 0000000000..403c6dbca0
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewChrome.java
@@ -0,0 +1,81 @@
+/* -*- 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.os.Bundle;
+
+public class GeckoViewChrome implements GeckoView.ChromeDelegate {
+ /**
+ * Tell the host application that Gecko is ready to handle requests.
+ * @param view The GeckoView that initiated the callback.
+ */
+ @Override
+ public void onReady(GeckoView view) {}
+
+ /**
+ * Tell the host application to display an alert dialog.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is loading the content.
+ * @param message The string to display in the dialog.
+ * @param result A PromptResult used to send back the result without blocking.
+ * Defaults to cancel requests.
+ */
+ @Override
+ public void onAlert(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result) {
+ result.cancel();
+ }
+
+ /**
+ * Tell the host application to display a confirmation dialog.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is loading the content.
+ * @param message The string to display in the dialog.
+ * @param result A PromptResult used to send back the result without blocking.
+ * Defaults to cancel requests.
+ */
+ @Override
+ public void onConfirm(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result) {
+ result.cancel();
+ }
+
+ /**
+ * Tell the host application to display an input prompt dialog.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is loading the content.
+ * @param message The string to display in the dialog.
+ * @param defaultValue The string to use as default input.
+ * @param result A PromptResult used to send back the result without blocking.
+ * Defaults to cancel requests.
+ */
+ @Override
+ public void onPrompt(GeckoView view, GeckoView.Browser browser, String message, String defaultValue, GeckoView.PromptResult result) {
+ result.cancel();
+ }
+
+ /**
+ * Tell the host application to display a remote debugging request dialog.
+ * @param view The GeckoView that initiated the callback.
+ * @param result A PromptResult used to send back the result without blocking.
+ * Defaults to cancel requests.
+ */
+ @Override
+ public void onDebugRequest(GeckoView view, GeckoView.PromptResult result) {
+ result.cancel();
+ }
+
+ /**
+ * Receive a message from an imported script.
+ * @param view The GeckoView that initiated the callback.
+ * @param data Bundle of data sent with the message. Never null.
+ * @param result A MessageResult used to send back a response without blocking. Can be null.
+ * Defaults to cancel requests with a failed response.
+ */
+ public void onScriptMessage(GeckoView view, Bundle data, GeckoView.MessageResult result) {
+ if (result != null) {
+ result.failure(null);
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewContent.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewContent.java
new file mode 100644
index 0000000000..22d0ede75c
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewContent.java
@@ -0,0 +1,56 @@
+/* -*- 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;
+
+public class GeckoViewContent implements GeckoView.ContentDelegate {
+ /**
+ * A Browser has started loading content from the network.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is loading the content.
+ * @param url The resource being loaded.
+ */
+ @Override
+ public void onPageStart(GeckoView view, GeckoView.Browser browser, String url) {}
+
+ /**
+ * A Browser has finished loading content from the network.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that was loading the content.
+ * @param success Whether the page loaded successfully or an error occurred.
+ */
+ @Override
+ public void onPageStop(GeckoView view, GeckoView.Browser browser, boolean success) {}
+
+ /**
+ * A Browser is displaying content. This page could have been loaded via
+ * network or from the session history.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is showing the content.
+ */
+ @Override
+ public void onPageShow(GeckoView view, GeckoView.Browser browser) {}
+
+ /**
+ * A page title was discovered in the content or updated after the content
+ * loaded.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is showing the content.
+ * @param title The title sent from the content.
+ */
+ @Override
+ public void onReceivedTitle(GeckoView view, GeckoView.Browser browser, String title) {}
+
+ /**
+ * A link element was discovered in the content or updated after the content
+ * loaded that specifies a favicon.
+ * @param view The GeckoView that initiated the callback.
+ * @param browser The Browser that is showing the content.
+ * @param url The href of the link element specifying the favicon.
+ * @param size The maximum size specified for the favicon, or -1 for any size.
+ */
+ @Override
+ public void onReceivedFavicon(GeckoView view, GeckoView.Browser browser, String url, int size) {}
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewFragment.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewFragment.java
new file mode 100644
index 0000000000..51320636e5
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoViewFragment.java
@@ -0,0 +1,52 @@
+/* -*- 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.app.Fragment;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+public class GeckoViewFragment extends android.support.v4.app.Fragment {
+ private static final String LOGTAG = "GeckoViewFragment";
+
+ private static Parcelable state = null;
+ private static GeckoViewFragment lastUsed = null;
+ private GeckoView geckoView = null;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ setRetainInstance(true);
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ geckoView = new GeckoView(getContext());
+ return geckoView;
+ }
+
+ @Override
+ public void onResume() {
+ if (state != null && lastUsed != this) {
+ // "Restore" the window from the previously used GeckoView to this GeckoView and attach it
+ geckoView.onRestoreInstanceState(state);
+ state = null;
+ }
+ super.onResume();
+ }
+
+ @Override
+ public void onPause() {
+ state = geckoView.onSaveInstanceState();
+ lastUsed = this;
+ super.onPause();
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputConnectionListener.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputConnectionListener.java
new file mode 100644
index 0000000000..baddc4ed25
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputConnectionListener.java
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.Handler;
+import android.view.KeyEvent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+/**
+ * Interface for interacting with GeckoInputConnection from GeckoView.
+ */
+interface InputConnectionListener
+{
+ Handler getHandler(Handler defHandler);
+ InputConnection onCreateInputConnection(EditorInfo outAttrs);
+ boolean onKeyPreIme(int keyCode, KeyEvent event);
+ boolean onKeyDown(int keyCode, KeyEvent event);
+ boolean onKeyLongPress(int keyCode, KeyEvent event);
+ boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event);
+ boolean onKeyUp(int keyCode, KeyEvent event);
+ boolean isIMEEnabled();
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputMethods.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputMethods.java
new file mode 100644
index 0000000000..57649b0da3
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputMethods.java
@@ -0,0 +1,76 @@
+/* -*- 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.Collection;
+
+import org.mozilla.gecko.AppConstants.Versions;
+
+import android.content.Context;
+import android.provider.Settings.Secure;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodManager;
+
+final public class InputMethods {
+ public static final String METHOD_ANDROID_LATINIME = "com.android.inputmethod.latin/.LatinIME";
+ public static final String METHOD_ATOK = "com.justsystems.atokmobile.service/.AtokInputMethodService";
+ public static final String METHOD_GOOGLE_JAPANESE_INPUT = "com.google.android.inputmethod.japanese/.MozcService";
+ public static final String METHOD_GOOGLE_LATINIME = "com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME";
+ public static final String METHOD_HTC_TOUCH_INPUT = "com.htc.android.htcime/.HTCIMEService";
+ public static final String METHOD_IWNN = "jp.co.omronsoft.iwnnime.ml/.standardcommon.IWnnLanguageSwitcher";
+ public static final String METHOD_OPENWNN_PLUS = "com.owplus.ime.openwnnplus/.OpenWnnJAJP";
+ public static final String METHOD_SAMSUNG = "com.sec.android.inputmethod/.SamsungKeypad";
+ public static final String METHOD_SIMEJI = "com.adamrocker.android.input.simeji/.OpenWnnSimeji";
+ public static final String METHOD_SWIFTKEY = "com.touchtype.swiftkey/com.touchtype.KeyboardService";
+ public static final String METHOD_SWYPE = "com.swype.android.inputmethod/.SwypeInputMethod";
+ public static final String METHOD_SWYPE_BETA = "com.nuance.swype.input/.IME";
+ public static final String METHOD_TOUCHPAL_KEYBOARD = "com.cootek.smartinputv5/com.cootek.smartinput5.TouchPalIME";
+
+ private InputMethods() {}
+
+ public static String getCurrentInputMethod(Context context) {
+ String inputMethod = Secure.getString(context.getContentResolver(), Secure.DEFAULT_INPUT_METHOD);
+ return (inputMethod != null ? inputMethod : "");
+ }
+
+ public static InputMethodInfo getInputMethodInfo(Context context, String inputMethod) {
+ InputMethodManager imm = getInputMethodManager(context);
+ Collection<InputMethodInfo> infos = imm.getEnabledInputMethodList();
+ for (InputMethodInfo info : infos) {
+ if (info.getId().equals(inputMethod)) {
+ return info;
+ }
+ }
+ return null;
+ }
+
+ public static InputMethodManager getInputMethodManager(Context context) {
+ return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ }
+
+ public static boolean needsSoftResetWorkaround(String inputMethod) {
+ // Stock latin IME on Android 4.2 and above
+ return Versions.feature17Plus &&
+ (METHOD_ANDROID_LATINIME.equals(inputMethod) ||
+ METHOD_GOOGLE_LATINIME.equals(inputMethod));
+ }
+
+ public static boolean shouldCommitCharAsKey(String inputMethod) {
+ return METHOD_HTC_TOUCH_INPUT.equals(inputMethod);
+ }
+
+ public static boolean isGestureKeyboard(Context context) {
+ // SwiftKey is a gesture keyboard, but it doesn't seem to need any special-casing
+ // to do AwesomeBar auto-spacing.
+ String inputMethod = getCurrentInputMethod(context);
+ return (Versions.feature17Plus &&
+ (METHOD_ANDROID_LATINIME.equals(inputMethod) ||
+ METHOD_GOOGLE_LATINIME.equals(inputMethod))) ||
+ METHOD_SWYPE.equals(inputMethod) ||
+ METHOD_SWYPE_BETA.equals(inputMethod) ||
+ METHOD_TOUCHPAL_KEYBOARD.equals(inputMethod);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/NSSBridge.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/NSSBridge.java
new file mode 100644
index 0000000000..8d525b0ba6
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/NSSBridge.java
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.mozglue.GeckoLoader;
+
+import android.content.Context;
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+public class NSSBridge {
+ private static final String LOGTAG = "NSSBridge";
+
+ private static native String nativeEncrypt(String aDb, String aValue);
+ private static native String nativeDecrypt(String aDb, String aValue);
+
+ @RobocopTarget
+ static public String encrypt(Context context, String aValue)
+ throws Exception {
+ String resourcePath = context.getPackageResourcePath();
+ GeckoLoader.loadNSSLibs(context, resourcePath);
+
+ String path = GeckoProfile.get(context).getDir().toString();
+ return nativeEncrypt(path, aValue);
+ }
+
+ @RobocopTarget
+ static public String encrypt(Context context, String profilePath, String aValue)
+ throws Exception {
+ String resourcePath = context.getPackageResourcePath();
+ GeckoLoader.loadNSSLibs(context, resourcePath);
+
+ return nativeEncrypt(profilePath, aValue);
+ }
+
+ @RobocopTarget
+ static public String decrypt(Context context, String aValue)
+ throws Exception {
+ String resourcePath = context.getPackageResourcePath();
+ GeckoLoader.loadNSSLibs(context, resourcePath);
+
+ String path = GeckoProfile.get(context).getDir().toString();
+ return nativeDecrypt(path, aValue);
+ }
+
+ @RobocopTarget
+ static public String decrypt(Context context, String profilePath, String aValue)
+ throws Exception {
+ String resourcePath = context.getPackageResourcePath();
+ GeckoLoader.loadNSSLibs(context, resourcePath);
+
+ return nativeDecrypt(profilePath, aValue);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/NotificationListener.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/NotificationListener.java
new file mode 100644
index 0000000000..85a68768fa
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/NotificationListener.java
@@ -0,0 +1,17 @@
+/* -*- 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;
+
+public interface NotificationListener
+{
+ void showNotification(String name, String cookie, String title, String text,
+ String host, String imageUrl);
+
+ void showPersistentNotification(String name, String cookie, String title, String text,
+ String host, String imageUrl, String data);
+
+ void closeNotification(String name);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/PrefsHelper.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/PrefsHelper.java
new file mode 100644
index 0000000000..b60f6fd885
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/PrefsHelper.java
@@ -0,0 +1,308 @@
+/* -*- 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 android.support.v4.util.SimpleArrayMap;
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Helper class to get/set gecko prefs.
+ */
+public final class PrefsHelper {
+ private static final String LOGTAG = "GeckoPrefsHelper";
+
+ // Map pref name to ArrayList for multiple observers or PrefHandler for single observer.
+ private static final SimpleArrayMap<String, Object> OBSERVERS = new SimpleArrayMap<>();
+ private static final HashSet<String> INT_TO_STRING_PREFS = new HashSet<>(8);
+ private static final HashSet<String> INT_TO_BOOL_PREFS = new HashSet<>(2);
+
+ static {
+ INT_TO_STRING_PREFS.add("browser.chrome.titlebarMode");
+ INT_TO_STRING_PREFS.add("network.cookie.cookieBehavior");
+ INT_TO_STRING_PREFS.add("font.size.inflation.minTwips");
+ INT_TO_STRING_PREFS.add("home.sync.updateMode");
+ INT_TO_STRING_PREFS.add("browser.image_blocking");
+ INT_TO_BOOL_PREFS.add("browser.display.use_document_fonts");
+ }
+
+ @WrapForJNI
+ private static final int PREF_INVALID = -1;
+ @WrapForJNI
+ private static final int PREF_FINISH = 0;
+ @WrapForJNI
+ private static final int PREF_BOOL = 1;
+ @WrapForJNI
+ private static final int PREF_INT = 2;
+ @WrapForJNI
+ private static final int PREF_STRING = 3;
+
+ @WrapForJNI(stubName = "GetPrefs", dispatchTo = "gecko")
+ private static native void nativeGetPrefs(String[] prefNames, PrefHandler handler);
+ @WrapForJNI(stubName = "SetPref", dispatchTo = "gecko")
+ private static native void nativeSetPref(String prefName, boolean flush, int type,
+ boolean boolVal, int intVal, String strVal);
+ @WrapForJNI(stubName = "AddObserver", dispatchTo = "gecko")
+ private static native void nativeAddObserver(String[] prefNames, PrefHandler handler,
+ String[] prefsToObserve);
+ @WrapForJNI(stubName = "RemoveObserver", dispatchTo = "gecko")
+ private static native void nativeRemoveObserver(String[] prefToUnobserve);
+
+ @RobocopTarget
+ public static void getPrefs(final String[] prefNames, final PrefHandler callback) {
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ nativeGetPrefs(prefNames, callback);
+ } else {
+ GeckoThread.queueNativeCallUntil(
+ GeckoThread.State.PROFILE_READY, PrefsHelper.class, "nativeGetPrefs",
+ String[].class, prefNames, PrefHandler.class, callback);
+ }
+ }
+
+ public static void getPref(final String prefName, final PrefHandler callback) {
+ getPrefs(new String[] { prefName }, callback);
+ }
+
+ public static void getPrefs(final ArrayList<String> prefNames, final PrefHandler callback) {
+ getPrefs(prefNames.toArray(new String[prefNames.size()]), callback);
+ }
+
+ @RobocopTarget
+ public static void setPref(final String pref, final Object value, final boolean flush) {
+ final int type;
+ boolean boolVal = false;
+ int intVal = 0;
+ String strVal = null;
+
+ if (INT_TO_STRING_PREFS.contains(pref)) {
+ // When sending to Java, we normalized special preferences that use integers
+ // and strings to represent booleans. Here, we convert them back to their
+ // actual types so we can store them.
+ type = PREF_INT;
+ intVal = Integer.parseInt(String.valueOf(value));
+ } else if (INT_TO_BOOL_PREFS.contains(pref)) {
+ type = PREF_INT;
+ intVal = (Boolean) value ? 1 : 0;
+ } else if (value instanceof Boolean) {
+ type = PREF_BOOL;
+ boolVal = (Boolean) value;
+ } else if (value instanceof Integer) {
+ type = PREF_INT;
+ intVal = (Integer) value;
+ } else {
+ type = PREF_STRING;
+ strVal = String.valueOf(value);
+ }
+
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ nativeSetPref(pref, flush, type, boolVal, intVal, strVal);
+ } else {
+ GeckoThread.queueNativeCallUntil(
+ GeckoThread.State.PROFILE_READY, PrefsHelper.class, "nativeSetPref",
+ String.class, pref, flush, type, boolVal, intVal, String.class, strVal);
+ }
+ }
+
+ public static void setPref(final String pref, final Object value) {
+ setPref(pref, value, /* flush */ false);
+ }
+
+ @RobocopTarget
+ public synchronized static void addObserver(final String[] prefNames,
+ final PrefHandler handler) {
+ List<String> prefsToObserve = null;
+
+ for (String pref : prefNames) {
+ final Object existing = OBSERVERS.get(pref);
+
+ if (existing == null) {
+ // Not observing yet, so add observer.
+ if (prefsToObserve == null) {
+ prefsToObserve = new ArrayList<>(prefNames.length);
+ }
+ prefsToObserve.add(pref);
+ OBSERVERS.put(pref, handler);
+
+ } else if (existing instanceof PrefHandler) {
+ // Already observing one, so turn it into an array.
+ final List<PrefHandler> handlerList = new ArrayList<>(2);
+ handlerList.add((PrefHandler) existing);
+ handlerList.add(handler);
+ OBSERVERS.put(pref, handlerList);
+
+ } else {
+ // Already observing multiple, so add to existing array.
+ @SuppressWarnings("unchecked")
+ final List<PrefHandler> handlerList = (List) existing;
+ handlerList.add(handler);
+ }
+ }
+
+ final String[] namesToObserve = prefsToObserve == null ? null :
+ prefsToObserve.toArray(new String[prefsToObserve.size()]);
+
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ nativeAddObserver(prefNames, handler, namesToObserve);
+ } else {
+ GeckoThread.queueNativeCallUntil(
+ GeckoThread.State.PROFILE_READY, PrefsHelper.class, "nativeAddObserver",
+ String[].class, prefNames, PrefHandler.class, handler,
+ String[].class, namesToObserve);
+ }
+ }
+
+ @RobocopTarget
+ public synchronized static void removeObserver(final PrefHandler handler) {
+ List<String> prefsToUnobserve = null;
+
+ for (int i = OBSERVERS.size() - 1; i >= 0; i--) {
+ final Object existing = OBSERVERS.valueAt(i);
+ boolean removeObserver = false;
+
+ if (existing == handler) {
+ removeObserver = true;
+
+ } else if (!(existing instanceof PrefHandler)) {
+ // Removing existing handler from list.
+ @SuppressWarnings("unchecked")
+ final List<PrefHandler> handlerList = (List) existing;
+ if (handlerList.remove(handler) && handlerList.isEmpty()) {
+ removeObserver = true;
+ }
+ }
+
+ if (removeObserver) {
+ // Removed last handler, so remove observer.
+ if (prefsToUnobserve == null) {
+ prefsToUnobserve = new ArrayList<>();
+ }
+ prefsToUnobserve.add(OBSERVERS.keyAt(i));
+ OBSERVERS.removeAt(i);
+ }
+ }
+
+ if (prefsToUnobserve == null) {
+ return;
+ }
+
+ final String[] namesToUnobserve =
+ prefsToUnobserve.toArray(new String[prefsToUnobserve.size()]);
+
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ nativeRemoveObserver(namesToUnobserve);
+ } else {
+ GeckoThread.queueNativeCallUntil(
+ GeckoThread.State.PROFILE_READY, PrefsHelper.class, "nativeRemoveObserver",
+ String[].class, namesToUnobserve);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void callPrefHandler(final PrefHandler handler, int type, final String pref,
+ boolean boolVal, int intVal, String strVal) {
+
+ // Some Gecko preferences use integers or strings to reference state instead of
+ // directly representing the value. Since the Java UI uses the type to determine
+ // which ui elements to show and how to handle them, we need to normalize these
+ // preferences to the correct type.
+ if (INT_TO_STRING_PREFS.contains(pref)) {
+ type = PREF_STRING;
+ strVal = String.valueOf(intVal);
+ } else if (INT_TO_BOOL_PREFS.contains(pref)) {
+ type = PREF_BOOL;
+ boolVal = intVal == 1;
+ }
+
+ switch (type) {
+ case PREF_FINISH:
+ handler.finish();
+ return;
+ case PREF_BOOL:
+ handler.prefValue(pref, boolVal);
+ return;
+ case PREF_INT:
+ handler.prefValue(pref, intVal);
+ return;
+ case PREF_STRING:
+ handler.prefValue(pref, strVal);
+ return;
+ }
+ throw new IllegalArgumentException();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private synchronized static void onPrefChange(final String pref, final int type,
+ final boolean boolVal, final int intVal,
+ final String strVal) {
+ final Object existing = OBSERVERS.get(pref);
+
+ if (existing == null) {
+ return;
+ }
+
+ final Iterator<PrefHandler> itor;
+ PrefHandler handler;
+
+ if (existing instanceof PrefHandler) {
+ itor = null;
+ handler = (PrefHandler) existing;
+ } else {
+ @SuppressWarnings("unchecked")
+ final List<PrefHandler> handlerList = (List) existing;
+ if (handlerList.isEmpty()) {
+ return;
+ }
+ itor = handlerList.iterator();
+ handler = itor.next();
+ }
+
+ do {
+ callPrefHandler(handler, type, pref, boolVal, intVal, strVal);
+ handler.finish();
+
+ handler = itor != null && itor.hasNext() ? itor.next() : null;
+ } while (handler != null);
+ }
+
+ public interface PrefHandler {
+ void prefValue(String pref, boolean value);
+ void prefValue(String pref, int value);
+ void prefValue(String pref, String value);
+ void finish();
+ }
+
+ public static abstract class PrefHandlerBase implements PrefHandler {
+ @Override
+ public void prefValue(String pref, boolean value) {
+ throw new UnsupportedOperationException(
+ "Unhandled boolean pref " + pref + "; wrong type?");
+ }
+
+ @Override
+ public void prefValue(String pref, int value) {
+ throw new UnsupportedOperationException(
+ "Unhandled int pref " + pref + "; wrong type?");
+ }
+
+ @Override
+ public void prefValue(String pref, String value) {
+ throw new UnsupportedOperationException(
+ "Unhandled String pref " + pref + "; wrong type?");
+ }
+
+ @Override
+ public void finish() {
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SysInfo.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SysInfo.java
new file mode 100644
index 0000000000..5c53ef465d
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SysInfo.java
@@ -0,0 +1,237 @@
+/* -*- 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.os.StrictMode;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import java.util.regex.Pattern;
+
+/**
+ * A collection of system info values, broadly mirroring a subset of
+ * nsSystemInfo. See also the constants in AppConstants, which reflect
+ * much of nsIXULAppInfo.
+ */
+// Normally, we'd annotate with @RobocopTarget. Since SysInfo is compiled
+// before RobocopTarget, we instead add o.m.g.SysInfo directly to the Proguard
+// configuration.
+public final class SysInfo {
+ private static final String LOG_TAG = "GeckoSysInfo";
+
+ // Number of bytes of /proc/meminfo to read in one go.
+ private static final int MEMINFO_BUFFER_SIZE_BYTES = 256;
+
+ // We don't mind an instant of possible duplicate work, we only wish to
+ // avoid inconsistency, so we don't bother with synchronization for
+ // these.
+ private static volatile int cpuCount = -1;
+
+ private static volatile int totalRAM = -1;
+
+ /**
+ * Get the number of cores on the device.
+ *
+ * We can't use a nice tidy API call, because they're all wrong:
+ *
+ * <http://stackoverflow.com/questions/7962155/how-can-you-detect-a-dual-core-
+ * cpu-on-an-android-device-from-code>
+ *
+ * This method is based on that code.
+ *
+ * @return the number of CPU cores, or 1 if the number could not be
+ * determined.
+ */
+ public static int getCPUCount() {
+ if (cpuCount > 0) {
+ return cpuCount;
+ }
+
+ // Avoid a strict mode warning.
+ StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+ try {
+ return readCPUCount();
+ } finally {
+ StrictMode.setThreadPolicy(savedPolicy);
+ }
+ }
+
+ private static int readCPUCount() {
+ class CpuFilter implements FileFilter {
+ @Override
+ public boolean accept(File pathname) {
+ return Pattern.matches("cpu[0-9]+", pathname.getName());
+ }
+ }
+ try {
+ final File dir = new File("/sys/devices/system/cpu/");
+ return cpuCount = dir.listFiles(new CpuFilter()).length;
+ } catch (Exception e) {
+ Log.w(LOG_TAG, "Assuming 1 CPU; got exception.", e);
+ return cpuCount = 1;
+ }
+ }
+
+ /**
+ * Helper functions used to extract key/value data from /proc/meminfo
+ * Pulled from:
+ * http://androidxref.com/4.2_r1/xref/frameworks/base/core/java/com/android/internal/util/MemInfoReader.java
+ */
+ private static boolean matchMemText(byte[] buffer, int index, int bufferLength, byte[] text) {
+ final int N = text.length;
+ if ((index + N) >= bufferLength) {
+ return false;
+ }
+ for (int i = 0; i < N; i++) {
+ if (buffer[index + i] != text[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Parses a line like:
+ *
+ * MemTotal: 1605324 kB
+ *
+ * into 1605324.
+ *
+ * @return the first uninterrupted sequence of digits following the
+ * specified index, parsed as an integer value in KB.
+ */
+ private static int extractMemValue(byte[] buffer, int offset, int length) {
+ if (offset >= length) {
+ return 0;
+ }
+
+ while (offset < length && buffer[offset] != '\n') {
+ if (buffer[offset] >= '0' && buffer[offset] <= '9') {
+ int start = offset++;
+ while (offset < length &&
+ buffer[offset] >= '0' &&
+ buffer[offset] <= '9') {
+ ++offset;
+ }
+ return Integer.parseInt(new String(buffer, start, offset - start), 10);
+ }
+ ++offset;
+ }
+ return 0;
+ }
+
+ /**
+ * Fetch the total memory of the device in MB by parsing /proc/meminfo.
+ *
+ * Of course, Android doesn't have a neat and tidy way to find total
+ * RAM, so we do it by parsing /proc/meminfo.
+ *
+ * @return 0 if a problem occurred, or memory size in MB.
+ */
+ public static int getMemSize() {
+ if (totalRAM >= 0) {
+ return totalRAM;
+ }
+
+ // This is the string "MemTotal" that we're searching for in the buffer.
+ final byte[] MEMTOTAL = {'M', 'e', 'm', 'T', 'o', 't', 'a', 'l'};
+
+ // `/proc/meminfo` is not a real file and thus safe to read on the main thread.
+ final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+ try {
+ final byte[] buffer = new byte[MEMINFO_BUFFER_SIZE_BYTES];
+ final FileInputStream is = new FileInputStream("/proc/meminfo");
+ try {
+ final int length = is.read(buffer);
+
+ for (int i = 0; i < length; i++) {
+ if (matchMemText(buffer, i, length, MEMTOTAL)) {
+ i += 8;
+ totalRAM = extractMemValue(buffer, i, length) / 1024;
+ Log.d(LOG_TAG, "System memory: " + totalRAM + "MB.");
+ return totalRAM;
+ }
+ }
+ } finally {
+ is.close();
+ }
+
+ Log.w(LOG_TAG, "Did not find MemTotal line in /proc/meminfo.");
+ return totalRAM = 0;
+ } catch (FileNotFoundException f) {
+ return totalRAM = 0;
+ } catch (IOException e) {
+ return totalRAM = 0;
+ } finally {
+ StrictMode.setThreadPolicy(savedPolicy);
+ }
+ }
+
+ /**
+ * @return the SDK version supported by this device, such as '16'.
+ */
+ public static int getVersion() {
+ return android.os.Build.VERSION.SDK_INT;
+ }
+
+ /**
+ * @return the release version string, such as "4.1.2".
+ */
+ public static String getReleaseVersion() {
+ return android.os.Build.VERSION.RELEASE;
+ }
+
+ /**
+ * @return the kernel version string, such as "3.4.10-geb45596".
+ */
+ public static String getKernelVersion() {
+ return System.getProperty("os.version", "");
+ }
+
+ /**
+ * @return the device manufacturer, such as "HTC".
+ */
+ public static String getManufacturer() {
+ return android.os.Build.MANUFACTURER;
+ }
+
+ /**
+ * @return the device name, such as "HTC One".
+ */
+ public static String getDevice() {
+ // No, not android.os.Build.DEVICE.
+ return android.os.Build.MODEL;
+ }
+
+ /**
+ * @return the Android "hardware" identifier, such as "m7".
+ */
+ public static String getHardware() {
+ return android.os.Build.HARDWARE;
+ }
+
+ /**
+ * @return the system OS name. Hardcoded to "Android".
+ */
+ public static String getName() {
+ // We deliberately differ from PR_SI_SYSNAME, which is "Linux".
+ return "Android";
+ }
+
+ /**
+ * @return the Android architecture string, including ABI.
+ */
+ public static String getArchABI() {
+ // Android likes to include the ABI, too ("armeabiv7"), so we
+ // differ to add value.
+ return android.os.Build.CPU_ABI;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/TouchEventInterceptor.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/TouchEventInterceptor.java
new file mode 100644
index 0000000000..41a71dfa5f
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/TouchEventInterceptor.java
@@ -0,0 +1,14 @@
+/* -*- 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 TouchEventInterceptor extends View.OnTouchListener {
+ /** Override this method for a chance to consume events before the view or its children */
+ public boolean onInterceptTouchEvent(View view, MotionEvent event);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/JNITarget.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/JNITarget.java
new file mode 100644
index 0000000000..d6140a1ffb
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/JNITarget.java
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
+@Retention(RetentionPolicy.CLASS)
+public @interface JNITarget {}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/ReflectionTarget.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/ReflectionTarget.java
new file mode 100644
index 0000000000..e873ebeb96
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/ReflectionTarget.java
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/*
+ * Used to indicate to ProGuard that this definition is accessed
+ * via reflection and should not be stripped from the source.
+ */
+@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
+@Retention(RetentionPolicy.CLASS)
+public @interface ReflectionTarget {}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/RobocopTarget.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/RobocopTarget.java
new file mode 100644
index 0000000000..e151306748
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/RobocopTarget.java
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
+@Retention(RetentionPolicy.CLASS)
+public @interface RobocopTarget {}
+
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WebRTCJNITarget.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WebRTCJNITarget.java
new file mode 100644
index 0000000000..f58dea1487
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WebRTCJNITarget.java
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
+@Retention(RetentionPolicy.CLASS)
+public @interface WebRTCJNITarget {}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WrapForJNI.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WrapForJNI.java
new file mode 100644
index 0000000000..358ed5d568
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WrapForJNI.java
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation is used to tag methods that are to have wrapper methods generated.
+ * Such methods will be protected from destruction by ProGuard, and allow us to avoid
+ * writing by hand large amounts of boring boilerplate.
+ */
+@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface WrapForJNI {
+ /**
+ * Skip this member when generating wrappers for a whole class.
+ */
+ boolean skip() default false;
+
+ /**
+ * Optional parameter specifying the name of the generated method stub. If omitted,
+ * the capitalized name of the Java method will be used.
+ */
+ String stubName() default "";
+
+ /**
+ * Action to take if member access returns an exception.
+ * One of "abort", "ignore", or "nsresult". "nsresult" is not supported for native
+ * methods.
+ */
+ String exceptionMode() default "abort";
+
+ /**
+ * The thread that the method will be called from.
+ * One of "any", "gecko", or "ui". Not supported for fields.
+ */
+ String calledFrom() default "any";
+
+ /**
+ * The thread that the method call will be dispatched to.
+ * One of "current", "gecko", or "proxy". Not supported for non-native methods,
+ * fields, and constructors. Only void-return methods are supported for anything other
+ * than current thread.
+ */
+ String dispatchTo() default "current";
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BitmapUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BitmapUtils.java
new file mode 100644
index 0000000000..a4b5165198
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BitmapUtils.java
@@ -0,0 +1,290 @@
+/* -*- 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.gfx;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.util.Base64;
+import android.util.Log;
+
+public final class BitmapUtils {
+ private static final String LOGTAG = "GeckoBitmapUtils";
+
+ private BitmapUtils() {}
+
+ public static Bitmap decodeByteArray(byte[] bytes) {
+ return decodeByteArray(bytes, null);
+ }
+
+ public static Bitmap decodeByteArray(byte[] bytes, BitmapFactory.Options options) {
+ return decodeByteArray(bytes, 0, bytes.length, options);
+ }
+
+ public static Bitmap decodeByteArray(byte[] bytes, int offset, int length) {
+ return decodeByteArray(bytes, offset, length, null);
+ }
+
+ public static Bitmap decodeByteArray(byte[] bytes, int offset, int length, BitmapFactory.Options options) {
+ if (bytes.length <= 0) {
+ throw new IllegalArgumentException("bytes.length " + bytes.length
+ + " must be a positive number");
+ }
+
+ Bitmap bitmap = null;
+ try {
+ bitmap = BitmapFactory.decodeByteArray(bytes, offset, length, options);
+ } catch (OutOfMemoryError e) {
+ Log.e(LOGTAG, "decodeByteArray(bytes.length=" + bytes.length
+ + ", options= " + options + ") OOM!", e);
+ return null;
+ }
+
+ if (bitmap == null) {
+ Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned null");
+ return null;
+ }
+
+ if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
+ Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned "
+ + "a bitmap with dimensions " + bitmap.getWidth()
+ + "x" + bitmap.getHeight());
+ return null;
+ }
+
+ return bitmap;
+ }
+
+ public static Bitmap decodeStream(InputStream inputStream) {
+ try {
+ return BitmapFactory.decodeStream(inputStream);
+ } catch (OutOfMemoryError e) {
+ Log.e(LOGTAG, "decodeStream() OOM!", e);
+ return null;
+ }
+ }
+
+ public static Bitmap decodeUrl(Uri uri) {
+ return decodeUrl(uri.toString());
+ }
+
+ public static Bitmap decodeUrl(String urlString) {
+ URL url;
+
+ try {
+ url = new URL(urlString);
+ } catch (MalformedURLException e) {
+ Log.w(LOGTAG, "decodeUrl: malformed URL " + urlString);
+ return null;
+ }
+
+ return decodeUrl(url);
+ }
+
+ public static Bitmap decodeUrl(URL url) {
+ InputStream stream = null;
+
+ try {
+ stream = url.openStream();
+ } catch (IOException e) {
+ Log.w(LOGTAG, "decodeUrl: IOException downloading " + url);
+ return null;
+ }
+
+ if (stream == null) {
+ Log.w(LOGTAG, "decodeUrl: stream not found downloading " + url);
+ return null;
+ }
+
+ Bitmap bitmap = decodeStream(stream);
+
+ try {
+ stream.close();
+ } catch (IOException e) {
+ Log.w(LOGTAG, "decodeUrl: IOException closing stream " + url, e);
+ }
+
+ return bitmap;
+ }
+
+ public static Bitmap decodeResource(Context context, int id) {
+ return decodeResource(context, id, null);
+ }
+
+ public static Bitmap decodeResource(Context context, int id, BitmapFactory.Options options) {
+ Resources resources = context.getResources();
+ try {
+ return BitmapFactory.decodeResource(resources, id, options);
+ } catch (OutOfMemoryError e) {
+ Log.e(LOGTAG, "decodeResource() OOM! Resource id=" + id, e);
+ return null;
+ }
+ }
+
+ public static int getDominantColor(Bitmap source) {
+ return getDominantColor(source, true);
+ }
+
+ public static int getDominantColor(Bitmap source, boolean applyThreshold) {
+ if (source == null)
+ return Color.argb(255, 255, 255, 255);
+
+ // Keep track of how many times a hue in a given bin appears in the image.
+ // Hue values range [0 .. 360), so dividing by 10, we get 36 bins.
+ int[] colorBins = new int[36];
+
+ // The bin with the most colors. Initialize to -1 to prevent accidentally
+ // thinking the first bin holds the dominant color.
+ int maxBin = -1;
+
+ // Keep track of sum hue/saturation/value per hue bin, which we'll use to
+ // compute an average to for the dominant color.
+ float[] sumHue = new float[36];
+ float[] sumSat = new float[36];
+ float[] sumVal = new float[36];
+ float[] hsv = new float[3];
+
+ int height = source.getHeight();
+ int width = source.getWidth();
+ int[] pixels = new int[width * height];
+ source.getPixels(pixels, 0, width, 0, 0, width, height);
+ for (int row = 0; row < height; row++) {
+ for (int col = 0; col < width; col++) {
+ int c = pixels[col + row * width];
+ // Ignore pixels with a certain transparency.
+ if (Color.alpha(c) < 128)
+ continue;
+
+ Color.colorToHSV(c, hsv);
+
+ // If a threshold is applied, ignore arbitrarily chosen values for "white" and "black".
+ if (applyThreshold && (hsv[1] <= 0.35f || hsv[2] <= 0.35f))
+ continue;
+
+ // We compute the dominant color by putting colors in bins based on their hue.
+ int bin = (int) Math.floor(hsv[0] / 10.0f);
+
+ // Update the sum hue/saturation/value for this bin.
+ sumHue[bin] = sumHue[bin] + hsv[0];
+ sumSat[bin] = sumSat[bin] + hsv[1];
+ sumVal[bin] = sumVal[bin] + hsv[2];
+
+ // Increment the number of colors in this bin.
+ colorBins[bin]++;
+
+ // Keep track of the bin that holds the most colors.
+ if (maxBin < 0 || colorBins[bin] > colorBins[maxBin])
+ maxBin = bin;
+ }
+ }
+
+ // maxBin may never get updated if the image holds only transparent and/or black/white pixels.
+ if (maxBin < 0)
+ return Color.argb(255, 255, 255, 255);
+
+ // Return a color with the average hue/saturation/value of the bin with the most colors.
+ hsv[0] = sumHue[maxBin] / colorBins[maxBin];
+ hsv[1] = sumSat[maxBin] / colorBins[maxBin];
+ hsv[2] = sumVal[maxBin] / colorBins[maxBin];
+ return Color.HSVToColor(hsv);
+ }
+
+ /**
+ * Decodes a bitmap from a Base64 data URI.
+ *
+ * @param dataURI a Base64-encoded data URI string
+ * @return the decoded bitmap, or null if the data URI is invalid
+ */
+ public static Bitmap getBitmapFromDataURI(String dataURI) {
+ if (dataURI == null) {
+ return null;
+ }
+
+ byte[] raw = getBytesFromDataURI(dataURI);
+ if (raw == null || raw.length == 0) {
+ return null;
+ }
+
+ return decodeByteArray(raw);
+ }
+
+ /**
+ * Return a byte[] containing the bytes in a given base64 string, or null if this is not a valid
+ * base64 string.
+ */
+ public static byte[] getBytesFromBase64(String base64) {
+ try {
+ return Base64.decode(base64, Base64.DEFAULT);
+ } catch (Exception e) {
+ Log.e(LOGTAG, "exception decoding bitmap from data URI: " + base64, e);
+ }
+
+ return null;
+ }
+
+ public static byte[] getBytesFromDataURI(String dataURI) {
+ final String base64 = dataURI.substring(dataURI.indexOf(',') + 1);
+ return getBytesFromBase64(base64);
+ }
+
+ public static Bitmap getBitmapFromDrawable(Drawable drawable) {
+ if (drawable instanceof BitmapDrawable) {
+ return ((BitmapDrawable) drawable).getBitmap();
+ }
+
+ int width = drawable.getIntrinsicWidth();
+ width = width > 0 ? width : 1;
+ int height = drawable.getIntrinsicHeight();
+ height = height > 0 ? height : 1;
+
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+
+ return bitmap;
+ }
+
+ public static int getResource(final Context context, final Uri resourceUrl) {
+ final String scheme = resourceUrl.getScheme();
+ if (!"drawable".equals(scheme)) {
+ // Return a "not found" default icon that's easy to spot.
+ return android.R.drawable.sym_def_app_icon;
+ }
+
+ String resource = resourceUrl.getSchemeSpecificPart();
+ if (resource.startsWith("//")) {
+ resource = resource.substring(2);
+ }
+
+ final Resources res = context.getResources();
+ int id = res.getIdentifier(resource, "drawable", context.getPackageName());
+ if (id != 0) {
+ return id;
+ }
+
+ // For backwards compatibility, we also search in system resources.
+ id = res.getIdentifier(resource, "drawable", "android");
+ if (id != 0) {
+ return id;
+ }
+
+ Log.w(LOGTAG, "Cannot find drawable/" + resource);
+ // Return a "not found" default icon that's easy to spot.
+ return android.R.drawable.sym_def_app_icon;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BufferedImage.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BufferedImage.java
new file mode 100644
index 0000000000..4dbcf61bbc
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BufferedImage.java
@@ -0,0 +1,94 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.mozglue.DirectBufferAllocator;
+
+import android.graphics.Bitmap;
+import android.util.Log;
+
+import java.nio.ByteBuffer;
+
+/** A buffered image that simply saves a buffer of pixel data. */
+public class BufferedImage {
+ private ByteBuffer mBuffer;
+ private Bitmap mBitmap;
+ private IntSize mSize;
+ private int mFormat;
+
+ private static final String LOGTAG = "GeckoBufferedImage";
+
+ /** Creates an empty buffered image */
+ public BufferedImage() {
+ mSize = new IntSize(0, 0);
+ }
+
+ /** Creates a buffered image from an Android bitmap. */
+ public BufferedImage(Bitmap bitmap) {
+ mFormat = bitmapConfigToFormat(bitmap.getConfig());
+ mSize = new IntSize(bitmap.getWidth(), bitmap.getHeight());
+ mBitmap = bitmap;
+ }
+
+ private synchronized void freeBuffer() {
+ if (mBuffer != null) {
+ mBuffer = DirectBufferAllocator.free(mBuffer);
+ }
+ }
+
+ public void destroy() {
+ try {
+ freeBuffer();
+ } catch (Exception ex) {
+ Log.e(LOGTAG, "error clearing buffer: ", ex);
+ }
+ }
+
+ public ByteBuffer getBuffer() {
+ if (mBuffer == null) {
+ int bpp = bitsPerPixelForFormat(mFormat);
+ mBuffer = DirectBufferAllocator.allocate(mSize.getArea() * bpp);
+ mBitmap.copyPixelsToBuffer(mBuffer.asIntBuffer());
+ mBitmap = null;
+ }
+ return mBuffer;
+ }
+
+ public IntSize getSize() { return mSize; }
+ public int getFormat() { return mFormat; }
+
+ public static final int FORMAT_INVALID = -1;
+ public static final int FORMAT_ARGB32 = 0;
+ public static final int FORMAT_RGB24 = 1;
+ public static final int FORMAT_A8 = 2;
+ public static final int FORMAT_A1 = 3;
+ public static final int FORMAT_RGB16_565 = 4;
+
+ private static int bitsPerPixelForFormat(int format) {
+ switch (format) {
+ case FORMAT_A1: return 1;
+ case FORMAT_A8: return 8;
+ case FORMAT_RGB16_565: return 16;
+ case FORMAT_RGB24: return 24;
+ case FORMAT_ARGB32: return 32;
+ default:
+ throw new RuntimeException("Unknown Cairo format");
+ }
+ }
+
+ private static int bitmapConfigToFormat(Bitmap.Config config) {
+ if (config == null)
+ return FORMAT_ARGB32; /* Droid Pro fix. */
+
+ switch (config) {
+ case ALPHA_8: return FORMAT_A8;
+ case ARGB_4444: throw new RuntimeException("ARGB_444 unsupported");
+ case ARGB_8888: return FORMAT_ARGB32;
+ case RGB_565: return FORMAT_RGB16_565;
+ default: throw new RuntimeException("Unknown Skia bitmap config");
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BufferedImageGLInfo.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BufferedImageGLInfo.java
new file mode 100644
index 0000000000..41f38e1ba0
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/BufferedImageGLInfo.java
@@ -0,0 +1,35 @@
+/* -*- 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.gfx;
+
+import javax.microedition.khronos.opengles.GL10;
+
+/** Information needed to render buffered bitmaps using OpenGL ES. */
+public class BufferedImageGLInfo {
+ public final int internalFormat;
+ public final int format;
+ public final int type;
+
+ public BufferedImageGLInfo(int bufferedImageFormat) {
+ switch (bufferedImageFormat) {
+ case BufferedImage.FORMAT_ARGB32:
+ internalFormat = format = GL10.GL_RGBA; type = GL10.GL_UNSIGNED_BYTE;
+ break;
+ case BufferedImage.FORMAT_RGB24:
+ internalFormat = format = GL10.GL_RGB; type = GL10.GL_UNSIGNED_BYTE;
+ break;
+ case BufferedImage.FORMAT_RGB16_565:
+ internalFormat = format = GL10.GL_RGB; type = GL10.GL_UNSIGNED_SHORT_5_6_5;
+ break;
+ case BufferedImage.FORMAT_A8:
+ case BufferedImage.FORMAT_A1:
+ throw new RuntimeException("BufferedImage FORMAT_A1 and FORMAT_A8 unsupported");
+ default:
+ throw new RuntimeException("Unknown BufferedImage format");
+ }
+ }
+}
+
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/DynamicToolbarAnimator.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/DynamicToolbarAnimator.java
new file mode 100644
index 0000000000..e299b5744e
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/DynamicToolbarAnimator.java
@@ -0,0 +1,605 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.PrefsHelper;
+import org.mozilla.gecko.util.FloatUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.graphics.PointF;
+import android.support.v4.view.ViewCompat;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.animation.LinearInterpolator;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+
+public class DynamicToolbarAnimator {
+ private static final String LOGTAG = "GeckoDynamicToolbarAnimator";
+ private static final String PREF_SCROLL_TOOLBAR_THRESHOLD = "browser.ui.scroll-toolbar-threshold";
+
+ public static enum PinReason {
+ RELAYOUT,
+ ACTION_MODE,
+ FULL_SCREEN,
+ CARET_DRAG
+ }
+
+ private final Set<PinReason> pinFlags = Collections.synchronizedSet(EnumSet.noneOf(PinReason.class));
+
+ // The duration of the animation in ns
+ private static final long ANIMATION_DURATION = 150000000;
+
+ private final GeckoLayerClient mTarget;
+ private final List<LayerView.DynamicToolbarListener> mListeners;
+
+ /* The translation to be applied to the toolbar UI view. This is the
+ * distance from the default/initial location (at the top of the screen,
+ * visible to the user) to where we want it to be. This variable should
+ * always be between 0 (toolbar fully visible) and the height of the toolbar
+ * (toolbar fully hidden), inclusive.
+ */
+ private float mToolbarTranslation;
+
+ /* The translation to be applied to the LayerView. This is the distance from
+ * the default/initial location (just below the toolbar, with the bottom
+ * extending past the bottom of the screen) to where we want it to be.
+ * This variable should always be between 0 and the height of the toolbar,
+ * inclusive.
+ */
+ private float mLayerViewTranslation;
+
+ /* This stores the maximum translation that can be applied to the toolbar
+ * and layerview when scrolling. This is populated with the height of the
+ * toolbar. */
+ private float mMaxTranslation;
+
+ /* This interpolator is used for the above mentioned animation */
+ private LinearInterpolator mInterpolator;
+
+ /* This is the proportion of the viewport rect that needs to be travelled
+ * while scrolling before the translation will start taking effect.
+ */
+ private float SCROLL_TOOLBAR_THRESHOLD = 0.20f;
+ /* The ID of the prefs listener for the scroll-toolbar threshold */
+ private final PrefsHelper.PrefHandler mPrefObserver;
+
+ /* While we are resizing the viewport to account for the toolbar, the Java
+ * code and painted layer metrics in the compositor have different notions
+ * of the CSS viewport height. The Java value is stored in the
+ * GeckoLayerClient's viewport metrics, and the Gecko one is stored here.
+ * This allows us to adjust fixed-pos items correctly.
+ * You must synchronize on mTarget.getLock() to read/write this. */
+ private Integer mHeightDuringResize;
+
+ /* This tracks if we should trigger a "snap" on the next composite. A "snap"
+ * is when we simultaneously move the LayerView and change the scroll offset
+ * in the compositor so that everything looks the same on the screen but
+ * has really been shifted.
+ * You must synchronize on |this| to read/write this. */
+ private boolean mSnapRequired = false;
+
+ /* The task that handles showing/hiding toolbar */
+ private DynamicToolbarAnimationTask mAnimationTask;
+
+ /* The start point of a drag, used for scroll-based dynamic toolbar
+ * behaviour. */
+ private PointF mTouchStart;
+ private float mLastTouch;
+
+ /* Set to true when root content is being scrolled */
+ private boolean mScrollingRootContent;
+
+ public DynamicToolbarAnimator(GeckoLayerClient aTarget) {
+ mTarget = aTarget;
+ mListeners = new ArrayList<LayerView.DynamicToolbarListener>();
+
+ mInterpolator = new LinearInterpolator();
+
+ // Listen to the dynamic toolbar pref
+ mPrefObserver = new PrefsHelper.PrefHandlerBase() {
+ @Override
+ public void prefValue(String pref, int value) {
+ SCROLL_TOOLBAR_THRESHOLD = value / 100.0f;
+ }
+ };
+ PrefsHelper.addObserver(new String[] { PREF_SCROLL_TOOLBAR_THRESHOLD }, mPrefObserver);
+ }
+
+ public void destroy() {
+ PrefsHelper.removeObserver(mPrefObserver);
+ }
+
+ public void addTranslationListener(LayerView.DynamicToolbarListener aListener) {
+ mListeners.add(aListener);
+ }
+
+ public void removeTranslationListener(LayerView.DynamicToolbarListener aListener) {
+ mListeners.remove(aListener);
+ }
+
+ private void fireListeners() {
+ for (LayerView.DynamicToolbarListener listener : mListeners) {
+ listener.onTranslationChanged(mToolbarTranslation, mLayerViewTranslation);
+ }
+ }
+
+ void onPanZoomStopped() {
+ for (LayerView.DynamicToolbarListener listener : mListeners) {
+ listener.onPanZoomStopped();
+ }
+ }
+
+ void onMetricsChanged(ImmutableViewportMetrics aMetrics) {
+ for (LayerView.DynamicToolbarListener listener : mListeners) {
+ listener.onMetricsChanged(aMetrics);
+ }
+ }
+
+ public void setMaxTranslation(float maxTranslation) {
+ ThreadUtils.assertOnUiThread();
+ if (maxTranslation < 0) {
+ Log.e(LOGTAG, "Got a negative max-translation value: " + maxTranslation + "; clamping to zero");
+ mMaxTranslation = 0;
+ } else {
+ mMaxTranslation = maxTranslation;
+ }
+ }
+
+ public float getMaxTranslation() {
+ return mMaxTranslation;
+ }
+
+ public float getToolbarTranslation() {
+ return mToolbarTranslation;
+ }
+
+ /**
+ * If true, scroll changes will not affect translation.
+ */
+ public boolean isPinned() {
+ return !pinFlags.isEmpty();
+ }
+
+ public boolean isPinnedBy(PinReason reason) {
+ return pinFlags.contains(reason);
+ }
+
+ public void setPinned(boolean pinned, PinReason reason) {
+ if (pinned) {
+ pinFlags.add(reason);
+ } else {
+ pinFlags.remove(reason);
+ }
+ }
+
+ public void showToolbar(boolean immediately) {
+ animateToolbar(true, immediately);
+ }
+
+ public void hideToolbar(boolean immediately) {
+ animateToolbar(false, immediately);
+ }
+
+ public void setScrollingRootContent(boolean isRootContent) {
+ mScrollingRootContent = isRootContent;
+ }
+
+ private void animateToolbar(final boolean showToolbar, boolean immediately) {
+ ThreadUtils.assertOnUiThread();
+
+ if (mAnimationTask != null) {
+ mTarget.getView().removeRenderTask(mAnimationTask);
+ mAnimationTask = null;
+ }
+
+ float desiredTranslation = (showToolbar ? 0 : mMaxTranslation);
+ Log.v(LOGTAG, "Requested " + (immediately ? "immediate " : "") + "toolbar animation to translation " + desiredTranslation);
+ if (FloatUtils.fuzzyEquals(mToolbarTranslation, desiredTranslation)) {
+ // If we're already pretty much in the desired position, don't bother
+ // with a full animation; do an immediate jump
+ immediately = true;
+ Log.v(LOGTAG, "Changing animation to immediate jump");
+ }
+
+ if (showToolbar && immediately) {
+ // Special case for showing the toolbar immediately: some of the call
+ // sites expect this to happen synchronously, so let's do that. This
+ // is safe because if we are showing the toolbar from a hidden state
+ // there is no chance of showing garbage
+ mToolbarTranslation = desiredTranslation;
+ fireListeners();
+ // And then proceed with the normal flow (some of which will be
+ // a no-op now)...
+ }
+
+ if (!showToolbar) {
+ // If we are hiding the toolbar, we need to move the LayerView first,
+ // so that we don't end up showing garbage under the toolbar when
+ // it is hidden. In the case that we are showing the toolbar, we
+ // move the LayerView after the toolbar is shown - the
+ // DynamicToolbarAnimationTask calls that upon completion.
+ shiftLayerView(desiredTranslation);
+ }
+
+ mAnimationTask = new DynamicToolbarAnimationTask(desiredTranslation, immediately, showToolbar);
+ mTarget.getView().postRenderTask(mAnimationTask);
+ }
+
+ private synchronized void shiftLayerView(float desiredTranslation) {
+ float layerViewTranslationNeeded = desiredTranslation - mLayerViewTranslation;
+ mLayerViewTranslation = desiredTranslation;
+ synchronized (mTarget.getLock()) {
+ if (layerViewTranslationNeeded == 0 && isResizing()) {
+ // We're already in the middle of a snap, so this new call is
+ // redundant as it's snapping to the same place. Ignore it.
+ return;
+ }
+ mHeightDuringResize = new Integer(mTarget.getViewportMetrics().viewportRectHeight);
+ mSnapRequired = mTarget.setViewportSize(
+ mTarget.getView().getWidth(),
+ mTarget.getView().getHeight() - Math.round(mMaxTranslation - mLayerViewTranslation),
+ new PointF(0, -layerViewTranslationNeeded));
+ if (!mSnapRequired) {
+ mHeightDuringResize = null;
+ ThreadUtils.postToUiThread(new Runnable() {
+ // Post to run it outside of the synchronize blocks. The
+ // delay shouldn't hurt.
+ @Override
+ public void run() {
+ fireListeners();
+ }
+ });
+ }
+ // Request a composite, which will trigger the snap.
+ mTarget.getView().requestRender();
+ }
+ }
+
+ IntSize getViewportSize() {
+ ThreadUtils.assertOnUiThread();
+
+ int viewWidth = mTarget.getView().getWidth();
+ int viewHeight = mTarget.getView().getHeight();
+ float toolbarTranslation = mToolbarTranslation;
+ if (mAnimationTask != null) {
+ // If we have an animation going, mToolbarTranslation may be in flux
+ // and we should use the final value it will settle on.
+ toolbarTranslation = mAnimationTask.getFinalToolbarTranslation();
+ }
+ int viewHeightVisible = viewHeight - Math.round(mMaxTranslation - toolbarTranslation);
+ return new IntSize(viewWidth, viewHeightVisible);
+ }
+
+ boolean isResizing() {
+ return mHeightDuringResize != null;
+ }
+
+ private final Runnable mSnapRunnable = new Runnable() {
+ private int mFrame = 0;
+
+ @Override
+ public final void run() {
+ // It takes 2 frames for the view translation to take effect, at
+ // least on a Nexus 4 device running Android 4.2.2. So we wait for
+ // two frames before doing the notifyAll(), otherwise we get a
+ // short user-visible glitch.
+ // TODO: find a better way to do this, if possible.
+ if (mFrame == 1) {
+ synchronized (this) {
+ this.notifyAll();
+ }
+ mFrame = 0;
+ return;
+ }
+
+ if (mFrame == 0) {
+ fireListeners();
+ }
+
+ ViewCompat.postOnAnimation(mTarget.getView(), this);
+ mFrame++;
+ }
+ };
+
+ void scrollChangeResizeCompleted() {
+ synchronized (mTarget.getLock()) {
+ Log.v(LOGTAG, "Scrollchange resize completed");
+ mHeightDuringResize = null;
+ }
+ }
+
+ /**
+ * "Shrinks" the absolute value of aValue by moving it closer to zero by
+ * aShrinkAmount, but prevents it from crossing over zero. If aShrinkAmount
+ * is negative it is ignored.
+ * @return The shrunken value.
+ */
+ private static float shrinkAbs(float aValue, float aShrinkAmount) {
+ if (aShrinkAmount <= 0) {
+ return aValue;
+ }
+ float shrinkBy = Math.min(Math.abs(aValue), aShrinkAmount);
+ return (aValue < 0 ? aValue + shrinkBy : aValue - shrinkBy);
+ }
+
+ /**
+ * This function takes in a scroll amount and decides how much of that
+ * should be used up to translate things on screen because of the dynamic
+ * toolbar behaviour. It returns the maximum amount that could be used
+ * for translation purposes; the rest must be used for scrolling.
+ */
+ private float decideTranslation(float aDelta,
+ ImmutableViewportMetrics aMetrics,
+ float aTouchTravelDistance) {
+
+ float exposeThreshold = aMetrics.getHeight() * SCROLL_TOOLBAR_THRESHOLD;
+ float translation = aDelta;
+
+ if (translation < 0) { // finger moving upwards
+ translation = shrinkAbs(translation, aMetrics.getOverscroll().top);
+
+ // If the toolbar is in a state between fully hidden and fully shown
+ // (i.e. the user is actively translating it), then we want the
+ // translation to take effect right away. Or if the user has moved
+ // their finger past the required threshold (and is not trying to
+ // scroll past the bottom of the page) then also we want the touch
+ // to cause translation. If the toolbar is fully visible, we only
+ // want the toolbar to hide if the user is scrolling the root content.
+ boolean inBetween = (mToolbarTranslation != 0 && mToolbarTranslation != mMaxTranslation);
+ boolean reachedThreshold = -aTouchTravelDistance >= exposeThreshold;
+ boolean atBottomOfPage = aMetrics.viewportRectBottom() >= aMetrics.pageRectBottom;
+ if (inBetween || (mScrollingRootContent && reachedThreshold && !atBottomOfPage)) {
+ return translation;
+ }
+ } else { // finger moving downwards
+ translation = shrinkAbs(translation, aMetrics.getOverscroll().bottom);
+
+ // Ditto above comment, but in this case if they reached the top and
+ // the toolbar is not shown, then we do want to allow translation
+ // right away.
+ boolean inBetween = (mToolbarTranslation != 0 && mToolbarTranslation != mMaxTranslation);
+ boolean reachedThreshold = aTouchTravelDistance >= exposeThreshold;
+ boolean atTopOfPage = aMetrics.viewportRectTop <= aMetrics.pageRectTop;
+ boolean isToolbarTranslated = (mToolbarTranslation != 0);
+ if (inBetween || reachedThreshold || (atTopOfPage && isToolbarTranslated)) {
+ return translation;
+ }
+ }
+
+ return 0;
+ }
+
+ // Timestamp of the start of the touch event used to calculate toolbar velocity
+ private long mLastEventTime;
+ // Current velocity of the toolbar. Used to populate the velocity queue in C++APZ.
+ private float mVelocity;
+
+ boolean onInterceptTouchEvent(MotionEvent event) {
+ if (isPinned()) {
+ return false;
+ }
+
+ // Animations should never co-exist with the user touching the screen.
+ if (mAnimationTask != null) {
+ mTarget.getView().removeRenderTask(mAnimationTask);
+ mAnimationTask = null;
+ }
+
+ // we only care about single-finger drags here; any other kind of event
+ // should reset and cause us to start over.
+ if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE ||
+ event.getActionMasked() != MotionEvent.ACTION_MOVE ||
+ event.getPointerCount() != 1)
+ {
+ if (mTouchStart != null) {
+ Log.v(LOGTAG, "Resetting touch sequence due to non-move");
+ mTouchStart = null;
+ mVelocity = 0.0f;
+ }
+
+ if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+ // We need to do this even if the toolbar is already fully
+ // visible or fully hidden, because this is what triggers the
+ // viewport resize in content and updates the viewport metrics.
+ boolean toolbarMostlyVisible = mToolbarTranslation < (mMaxTranslation / 2);
+ Log.v(LOGTAG, "All fingers lifted, completing " + (toolbarMostlyVisible ? "show" : "hide"));
+ animateToolbar(toolbarMostlyVisible, false);
+ }
+ return false;
+ }
+
+ if (mTouchStart != null) {
+ float prevDir = mLastTouch - mTouchStart.y;
+ float newDir = event.getRawY() - mLastTouch;
+ if (prevDir != 0 && newDir != 0 && ((prevDir < 0) != (newDir < 0))) {
+ // If the direction of movement changed, reset the travel
+ // distance properties.
+ mTouchStart = null;
+ mVelocity = 0.0f;
+ }
+ }
+
+ if (mTouchStart == null) {
+ mTouchStart = new PointF(event.getRawX(), event.getRawY());
+ mLastTouch = event.getRawY();
+ mLastEventTime = event.getEventTime();
+ return false;
+ }
+
+ float deltaY = event.getRawY() - mLastTouch;
+ long currentTime = event.getEventTime();
+ float deltaTime = (float)(currentTime - mLastEventTime);
+ mLastEventTime = currentTime;
+ if (deltaTime > 0.0f) {
+ mVelocity = -deltaY / deltaTime;
+ } else {
+ mVelocity = 0.0f;
+ }
+ mLastTouch = event.getRawY();
+ float travelDistance = event.getRawY() - mTouchStart.y;
+
+ ImmutableViewportMetrics metrics = mTarget.getViewportMetrics();
+
+ if (metrics.getPageHeight() <= mTarget.getView().getHeight() &&
+ mToolbarTranslation == 0) {
+ // If the page is short and the toolbar is already visible, don't
+ // allow translating it out of view.
+ return false;
+ }
+
+ float translation = decideTranslation(deltaY, metrics, travelDistance);
+
+ float oldToolbarTranslation = mToolbarTranslation;
+ float oldLayerViewTranslation = mLayerViewTranslation;
+ mToolbarTranslation = FloatUtils.clamp(mToolbarTranslation - translation, 0, mMaxTranslation);
+ mLayerViewTranslation = FloatUtils.clamp(mLayerViewTranslation - translation, 0, mMaxTranslation);
+
+ if (oldToolbarTranslation == mToolbarTranslation &&
+ oldLayerViewTranslation == mLayerViewTranslation) {
+ return false;
+ }
+
+ if (mToolbarTranslation == mMaxTranslation) {
+ Log.v(LOGTAG, "Toolbar at maximum translation, calling shiftLayerView(" + mMaxTranslation + ")");
+ shiftLayerView(mMaxTranslation);
+ } else if (mToolbarTranslation == 0) {
+ Log.v(LOGTAG, "Toolbar at minimum translation, calling shiftLayerView(0)");
+ shiftLayerView(0);
+ }
+
+ fireListeners();
+ mTarget.getView().requestRender();
+ return true;
+ }
+
+ // Get the current velocity of the toolbar.
+ float getVelocity() {
+ return mVelocity;
+ }
+
+ public PointF getVisibleEndOfLayerView() {
+ return new PointF(mTarget.getView().getWidth(),
+ mTarget.getView().getHeight() - mMaxTranslation + mLayerViewTranslation);
+ }
+
+ private float bottomOfCssViewport(ImmutableViewportMetrics aMetrics) {
+ return (isResizing() ? mHeightDuringResize : aMetrics.getHeight())
+ + mMaxTranslation - mLayerViewTranslation;
+ }
+
+ private synchronized boolean getAndClearSnapRequired() {
+ boolean snapRequired = mSnapRequired;
+ mSnapRequired = false;
+ return snapRequired;
+ }
+
+ void populateViewTransform(ViewTransform aTransform, ImmutableViewportMetrics aMetrics) {
+ if (getAndClearSnapRequired()) {
+ synchronized (mSnapRunnable) {
+ ViewCompat.postOnAnimation(mTarget.getView(), mSnapRunnable);
+ try {
+ // hold the in-progress composite until the views have been
+ // translated because otherwise there is a visible glitch.
+ // don't hold for more than 100ms just in case.
+ mSnapRunnable.wait(100);
+ } catch (InterruptedException ie) {
+ }
+ }
+ }
+
+ aTransform.x = aMetrics.viewportRectLeft;
+ aTransform.y = aMetrics.viewportRectTop;
+ aTransform.width = aMetrics.viewportRectWidth;
+ aTransform.height = aMetrics.viewportRectHeight;
+ aTransform.scale = aMetrics.zoomFactor;
+
+ aTransform.fixedLayerMarginTop = mLayerViewTranslation - mToolbarTranslation;
+ float bottomOfScreen = mTarget.getView().getHeight();
+ // We want to move a fixed item from "bottomOfCssViewport" to
+ // "bottomOfScreen". But also the bottom margin > 0 means that bottom
+ // fixed-pos items will move upwards.
+ aTransform.fixedLayerMarginBottom = bottomOfCssViewport(aMetrics) - bottomOfScreen;
+ //Log.v(LOGTAG, "ViewTransform is x=" + aTransform.x + " y=" + aTransform.y
+ // + " z=" + aTransform.scale + " t=" + aTransform.fixedLayerMarginTop
+ // + " b=" + aTransform.fixedLayerMarginBottom);
+ }
+
+ class DynamicToolbarAnimationTask extends RenderTask {
+ private final float mStartTranslation;
+ private final float mEndTranslation;
+ private final boolean mImmediate;
+ private final boolean mShiftLayerView;
+ private boolean mContinueAnimation;
+
+ public DynamicToolbarAnimationTask(float aTranslation, boolean aImmediate, boolean aShiftLayerView) {
+ super(false);
+ mContinueAnimation = true;
+ mStartTranslation = mToolbarTranslation;
+ mEndTranslation = aTranslation;
+ mImmediate = aImmediate;
+ mShiftLayerView = aShiftLayerView;
+ }
+
+ float getFinalToolbarTranslation() {
+ return mEndTranslation;
+ }
+
+ @Override
+ public boolean internalRun(long timeDelta, long currentFrameStartTime) {
+ if (!mContinueAnimation) {
+ return false;
+ }
+
+ // Calculate the progress (between 0 and 1)
+ final float progress = mImmediate
+ ? 1.0f
+ : mInterpolator.getInterpolation(
+ Math.min(1.0f, (System.nanoTime() - getStartTime())
+ / (float)ANIMATION_DURATION));
+
+ // This runs on the compositor thread, so we need to post the
+ // actual work to the UI thread.
+ ThreadUtils.assertNotOnUiThread();
+
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // Move the toolbar as per the animation
+ mToolbarTranslation = FloatUtils.interpolate(mStartTranslation, mEndTranslation, progress);
+ fireListeners();
+
+ if (mShiftLayerView && progress >= 1.0f) {
+ shiftLayerView(mEndTranslation);
+ }
+ }
+ });
+
+ mTarget.getView().requestRender();
+ if (progress >= 1.0f) {
+ mContinueAnimation = false;
+ }
+ return mContinueAnimation;
+ }
+ }
+
+ class SnapMetrics {
+ public final int viewportWidth;
+ public final int viewportHeight;
+ public final float scrollChangeY;
+
+ SnapMetrics(ImmutableViewportMetrics aMetrics, float aScrollChange) {
+ viewportWidth = aMetrics.viewportRectWidth;
+ viewportHeight = aMetrics.viewportRectHeight;
+ scrollChangeY = aScrollChange;
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/FloatSize.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/FloatSize.java
new file mode 100644
index 0000000000..4b495ab77e
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/FloatSize.java
@@ -0,0 +1,54 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.util.FloatUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class FloatSize {
+ public final float width, height;
+
+ public FloatSize(FloatSize size) { width = size.width; height = size.height; }
+ public FloatSize(IntSize size) { width = size.width; height = size.height; }
+ public FloatSize(float aWidth, float aHeight) { width = aWidth; height = aHeight; }
+
+ public FloatSize(JSONObject json) {
+ try {
+ width = (float)json.getDouble("width");
+ height = (float)json.getDouble("height");
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public String toString() { return "(" + width + "," + height + ")"; }
+
+ public boolean isPositive() {
+ return (width > 0 && height > 0);
+ }
+
+ public boolean fuzzyEquals(FloatSize size) {
+ return (FloatUtils.fuzzyEquals(size.width, width) &&
+ FloatUtils.fuzzyEquals(size.height, height));
+ }
+
+ public FloatSize scale(float factor) {
+ return new FloatSize(width * factor, height * factor);
+ }
+
+ /*
+ * Returns the size that represents a linear transition between this size and `to` at time `t`,
+ * which is on the scale [0, 1).
+ */
+ public FloatSize interpolate(FloatSize to, float t) {
+ return new FloatSize(FloatUtils.interpolate(width, to.width, t),
+ FloatUtils.interpolate(height, to.height, t));
+ }
+}
+
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/FullScreenState.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/FullScreenState.java
new file mode 100644
index 0000000000..9574bbe0e5
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/FullScreenState.java
@@ -0,0 +1,12 @@
+/* -*- 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.gfx;
+
+public enum FullScreenState {
+ NONE,
+ ROOT_ELEMENT,
+ NON_ROOT_ELEMENT
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
new file mode 100644
index 0000000000..d504fe13eb
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
@@ -0,0 +1,694 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.gfx.LayerView.DrawListener;
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.util.FloatUtils;
+import org.mozilla.gecko.AppConstants;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.SystemClock;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class GeckoLayerClient implements LayerView.Listener, PanZoomTarget
+{
+ private static final String LOGTAG = "GeckoLayerClient";
+ private static int sPaintSyncId = 1;
+
+ private LayerRenderer mLayerRenderer;
+ private boolean mLayerRendererInitialized;
+
+ private final Context mContext;
+ private IntSize mScreenSize;
+ private IntSize mWindowSize;
+
+ /*
+ * The viewport metrics being used to draw the current frame. This is only
+ * accessed by the compositor thread, and so needs no synchronisation.
+ */
+ private ImmutableViewportMetrics mFrameMetrics;
+
+ private final List<DrawListener> mDrawListeners;
+
+ /* Used as temporaries by syncViewportInfo */
+ private final ViewTransform mCurrentViewTransform;
+
+ private boolean mForceRedraw;
+
+ /* The current viewport metrics.
+ * This is volatile so that we can read and write to it from different threads.
+ * We avoid synchronization to make getting the viewport metrics from
+ * the compositor as cheap as possible. The viewport is immutable so
+ * we don't need to worry about anyone mutating it while we're reading from it.
+ * Specifically:
+ * 1) reading mViewportMetrics from any thread is fine without synchronization
+ * 2) writing to mViewportMetrics requires synchronizing on the layer controller object
+ * 3) whenever reading multiple fields from mViewportMetrics without synchronization (i.e. in
+ * case 1 above) you should always first grab a local copy of the reference, and then use
+ * that because mViewportMetrics might get reassigned in between reading the different
+ * fields. */
+ private volatile ImmutableViewportMetrics mViewportMetrics;
+
+ private volatile boolean mGeckoIsReady;
+
+ /* package */ final PanZoomController mPanZoomController;
+ private final DynamicToolbarAnimator mToolbarAnimator;
+ /* package */ final LayerView mView;
+
+ /* This flag is true from the time that browser.js detects a first-paint is about to start,
+ * to the time that we receive the first-paint composite notification from the compositor.
+ * Note that there is a small race condition with this; if there are two paints that both
+ * have the first-paint flag set, and the second paint happens concurrently with the
+ * composite for the first paint, then this flag may be set to true prematurely. Fixing this
+ * is possible but risky; see https://bugzilla.mozilla.org/show_bug.cgi?id=797615#c751
+ */
+ private volatile boolean mContentDocumentIsDisplayed;
+
+ private SynthesizedEventState mPointerState;
+
+ @WrapForJNI(stubName = "ClearColor")
+ private volatile int mClearColor = Color.WHITE;
+
+ public GeckoLayerClient(Context context, LayerView view, EventDispatcher eventDispatcher) {
+ // we can fill these in with dummy values because they are always written
+ // to before being read
+ mContext = context;
+ mScreenSize = new IntSize(0, 0);
+ mWindowSize = new IntSize(0, 0);
+ mCurrentViewTransform = new ViewTransform(0, 0, 1);
+
+ mForceRedraw = true;
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ mViewportMetrics = new ImmutableViewportMetrics(displayMetrics)
+ .setViewportSize(view.getWidth(), view.getHeight());
+
+ mFrameMetrics = mViewportMetrics;
+
+ mDrawListeners = new ArrayList<DrawListener>();
+ mToolbarAnimator = new DynamicToolbarAnimator(this);
+ mPanZoomController = PanZoomController.Factory.create(this, view, eventDispatcher);
+ mView = view;
+ mView.setListener(this);
+ mContentDocumentIsDisplayed = true;
+ }
+
+ public void setOverscrollHandler(final Overscroll listener) {
+ mPanZoomController.setOverscrollHandler(listener);
+ }
+
+ public void setGeckoReady(boolean ready) {
+ mGeckoIsReady = ready;
+ }
+
+ @Override // PanZoomTarget
+ public boolean isGeckoReady() {
+ return mGeckoIsReady;
+ }
+
+ /** Attaches to root layer so that Gecko appears. */
+ @WrapForJNI(calledFrom = "gecko")
+ private void onGeckoReady() {
+ mGeckoIsReady = true;
+
+ mLayerRenderer = mView.getRenderer();
+
+ sendResizeEventIfNecessary(true, null);
+
+ // Gecko being ready is one of the two conditions (along with having an available
+ // surface) that cause us to create the compositor. So here, now that we know gecko
+ // is ready, call updateCompositor() to see if we can actually do the creation.
+ // This needs to run on the UI thread so that the surface validity can't change on
+ // us while we're in the middle of creating the compositor.
+ mView.post(new Runnable() {
+ @Override
+ public void run() {
+ mPanZoomController.attach();
+ mView.updateCompositor();
+ }
+ });
+ }
+
+ public void destroy() {
+ mPanZoomController.destroy();
+ mToolbarAnimator.destroy();
+ mDrawListeners.clear();
+ mGeckoIsReady = false;
+ }
+
+ public LayerView getView() {
+ return mView;
+ }
+
+ public FloatSize getViewportSize() {
+ return mViewportMetrics.getSize();
+ }
+
+ /**
+ * The view calls this function to indicate that the viewport changed size. It must hold the
+ * monitor while calling it.
+ *
+ * TODO: Refactor this to use an interface. Expose that interface only to the view and not
+ * to the layer client. That way, the layer client won't be tempted to call this, which might
+ * result in an infinite loop.
+ */
+ boolean setViewportSize(int width, int height, PointF scrollChange) {
+ if (mViewportMetrics.viewportRectWidth == width &&
+ mViewportMetrics.viewportRectHeight == height &&
+ (scrollChange == null || (scrollChange.x == 0 && scrollChange.y == 0))) {
+ return false;
+ }
+ mViewportMetrics = mViewportMetrics.setViewportSize(width, height);
+ if (scrollChange != null) {
+ mViewportMetrics = mPanZoomController.adjustScrollForSurfaceShift(mViewportMetrics, scrollChange);
+ }
+
+ if (mGeckoIsReady) {
+ // here we send gecko a resize message. The code in browser.js is responsible for
+ // picking up on that resize event, modifying the viewport as necessary, and informing
+ // us of the new viewport.
+ sendResizeEventIfNecessary(true, scrollChange);
+
+ // the following call also sends gecko a message, which will be processed after the resize
+ // message above has updated the viewport. this message ensures that if we have just put
+ // focus in a text field, we scroll the content so that the text field is in view.
+ GeckoAppShell.viewSizeChanged();
+ }
+ return true;
+ }
+
+ PanZoomController getPanZoomController() {
+ return mPanZoomController;
+ }
+
+ DynamicToolbarAnimator getDynamicToolbarAnimator() {
+ return mToolbarAnimator;
+ }
+
+ /* Informs Gecko that the screen size has changed. */
+ private void sendResizeEventIfNecessary(boolean force, PointF scrollChange) {
+ DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
+
+ IntSize newScreenSize = new IntSize(metrics.widthPixels, metrics.heightPixels);
+ IntSize newWindowSize = new IntSize(mViewportMetrics.viewportRectWidth,
+ mViewportMetrics.viewportRectHeight);
+
+ boolean screenSizeChanged = !mScreenSize.equals(newScreenSize);
+ boolean windowSizeChanged = !mWindowSize.equals(newWindowSize);
+
+ if (!force && !screenSizeChanged && !windowSizeChanged) {
+ return;
+ }
+
+ mScreenSize = newScreenSize;
+ mWindowSize = newWindowSize;
+
+ if (screenSizeChanged) {
+ Log.d(LOGTAG, "Screen-size changed to " + mScreenSize);
+ }
+
+ if (windowSizeChanged) {
+ Log.d(LOGTAG, "Window-size changed to " + mWindowSize);
+ }
+
+ if (mView != null) {
+ mView.notifySizeChanged(mWindowSize.width, mWindowSize.height,
+ mScreenSize.width, mScreenSize.height);
+ }
+
+ String json = "";
+ try {
+ if (scrollChange != null) {
+ int id = ++sPaintSyncId;
+ if (id == 0) {
+ // never use 0 as that is the default value for "this is not
+ // a special transaction"
+ id = ++sPaintSyncId;
+ }
+ JSONObject jsonObj = new JSONObject();
+ jsonObj.put("x", scrollChange.x / mViewportMetrics.zoomFactor);
+ jsonObj.put("y", scrollChange.y / mViewportMetrics.zoomFactor);
+ jsonObj.put("id", id);
+ json = jsonObj.toString();
+ }
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Unable to convert point to JSON", e);
+ }
+ GeckoAppShell.notifyObservers("Window:Resize", json);
+ }
+
+ /**
+ * The different types of Viewport messages handled. All viewport events
+ * expect a display-port to be returned, but can handle one not being
+ * returned.
+ */
+ private enum ViewportMessageType {
+ UPDATE, // The viewport has changed and should be entirely updated
+ PAGE_SIZE // The viewport's page-size has changed
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ void contentDocumentChanged() {
+ mContentDocumentIsDisplayed = false;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ boolean isContentDocumentDisplayed() {
+ return mContentDocumentIsDisplayed;
+ }
+
+ /** The compositor invokes this function just before compositing a frame where the document
+ * is different from the document composited on the last frame. In these cases, the viewport
+ * information we have in Java is no longer valid and needs to be replaced with the new
+ * viewport information provided.
+ */
+ @WrapForJNI
+ public void setFirstPaintViewport(float offsetX, float offsetY, float zoom,
+ float cssPageLeft, float cssPageTop, float cssPageRight, float cssPageBottom) {
+ synchronized (getLock()) {
+ ImmutableViewportMetrics currentMetrics = getViewportMetrics();
+
+ RectF cssPageRect = new RectF(cssPageLeft, cssPageTop, cssPageRight, cssPageBottom);
+ RectF pageRect = RectUtils.scaleAndRound(cssPageRect, zoom);
+
+ final ImmutableViewportMetrics newMetrics = currentMetrics
+ .setViewportOrigin(offsetX, offsetY)
+ .setZoomFactor(zoom)
+ .setPageRect(pageRect, cssPageRect);
+ // Since we have switched to displaying a different document, we need to update any
+ // viewport-related state we have lying around (i.e. mViewportMetrics).
+ // Usually this information is updated via handleViewportMessage
+ // while we remain on the same document.
+ setViewportMetrics(newMetrics, true);
+
+ // Indicate that the document is about to be composited so the
+ // LayerView background can be removed.
+ if (mView.getPaintState() == LayerView.PAINT_START) {
+ mView.setPaintState(LayerView.PAINT_BEFORE_FIRST);
+ }
+ }
+
+ mContentDocumentIsDisplayed = true;
+ }
+
+ /** The compositor invokes this function on every frame to figure out what part of the
+ * page to display, and to inform Java of the current display port. Since it is called
+ * on every frame, it needs to be ultra-fast.
+ * It avoids taking any locks or allocating any objects. We keep around a
+ * mCurrentViewTransform so we don't need to allocate a new ViewTransform
+ * every time we're called. NOTE: we might be able to return a ImmutableViewportMetrics
+ * which would avoid the copy into mCurrentViewTransform.
+ */
+ private ViewTransform syncViewportInfo(int x, int y, int width, int height, float resolution, boolean layersUpdated,
+ int paintSyncId) {
+ // getViewportMetrics is thread safe so we don't need to synchronize.
+ // We save the viewport metrics here, so we later use it later in
+ // createFrame (which will be called by nsWindow::DrawWindowUnderlay on
+ // the native side, by the compositor). The viewport
+ // metrics can change between here and there, as it's accessed outside
+ // of the compositor thread.
+ mFrameMetrics = getViewportMetrics();
+
+ if (paintSyncId == sPaintSyncId) {
+ mToolbarAnimator.scrollChangeResizeCompleted();
+ }
+ mToolbarAnimator.populateViewTransform(mCurrentViewTransform, mFrameMetrics);
+
+ if (layersUpdated) {
+ for (DrawListener listener : mDrawListeners) {
+ listener.drawFinished();
+ }
+ }
+
+ return mCurrentViewTransform;
+ }
+
+ @WrapForJNI
+ public ViewTransform syncFrameMetrics(float scrollX, float scrollY, float zoom,
+ float cssPageLeft, float cssPageTop, float cssPageRight, float cssPageBottom,
+ int dpX, int dpY, int dpWidth, int dpHeight, float paintedResolution,
+ boolean layersUpdated, int paintSyncId)
+ {
+ // TODO: optimize this so it doesn't create so much garbage - it's a
+ // hot path
+ RectF cssPageRect = new RectF(cssPageLeft, cssPageTop, cssPageRight, cssPageBottom);
+ synchronized (getLock()) {
+ mViewportMetrics = mViewportMetrics.setViewportOrigin(scrollX, scrollY)
+ .setZoomFactor(zoom)
+ .setPageRect(RectUtils.scale(cssPageRect, zoom), cssPageRect);
+ }
+ return syncViewportInfo(dpX, dpY, dpWidth, dpHeight, paintedResolution,
+ layersUpdated, paintSyncId);
+ }
+
+ class PointerInfo {
+ // We reserve one pointer ID for the mouse, so that tests don't have
+ // to worry about tracking pointer IDs if they just want to test mouse
+ // event synthesization. If somebody tries to use this ID for a
+ // synthesized touch event we'll throw an exception.
+ public static final int RESERVED_MOUSE_POINTER_ID = 100000;
+
+ public int pointerId;
+ public int source;
+ public int screenX;
+ public int screenY;
+ public double pressure;
+ public int orientation;
+
+ public MotionEvent.PointerCoords getCoords() {
+ MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+ coords.orientation = orientation;
+ coords.pressure = (float)pressure;
+ coords.x = screenX;
+ coords.y = screenY;
+ return coords;
+ }
+ }
+
+ class SynthesizedEventState {
+ public final ArrayList<PointerInfo> pointers;
+ public long downTime;
+
+ SynthesizedEventState() {
+ pointers = new ArrayList<PointerInfo>();
+ }
+
+ int getPointerIndex(int pointerId) {
+ for (int i = 0; i < pointers.size(); i++) {
+ if (pointers.get(i).pointerId == pointerId) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ int addPointer(int pointerId, int source) {
+ PointerInfo info = new PointerInfo();
+ info.pointerId = pointerId;
+ info.source = source;
+ pointers.add(info);
+ return pointers.size() - 1;
+ }
+
+ int getPointerCount(int source) {
+ int count = 0;
+ for (int i = 0; i < pointers.size(); i++) {
+ if (pointers.get(i).source == source) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ MotionEvent.PointerProperties[] getPointerProperties(int source) {
+ MotionEvent.PointerProperties[] props = new MotionEvent.PointerProperties[getPointerCount(source)];
+ int index = 0;
+ for (int i = 0; i < pointers.size(); i++) {
+ if (pointers.get(i).source == source) {
+ MotionEvent.PointerProperties p = new MotionEvent.PointerProperties();
+ p.id = pointers.get(i).pointerId;
+ switch (source) {
+ case InputDevice.SOURCE_TOUCHSCREEN:
+ p.toolType = MotionEvent.TOOL_TYPE_FINGER;
+ break;
+ case InputDevice.SOURCE_MOUSE:
+ p.toolType = MotionEvent.TOOL_TYPE_MOUSE;
+ break;
+ }
+ props[index++] = p;
+ }
+ }
+ return props;
+ }
+
+ MotionEvent.PointerCoords[] getPointerCoords(int source) {
+ MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[getPointerCount(source)];
+ int index = 0;
+ for (int i = 0; i < pointers.size(); i++) {
+ if (pointers.get(i).source == source) {
+ coords[index++] = pointers.get(i).getCoords();
+ }
+ }
+ return coords;
+ }
+ }
+
+ private void synthesizeNativePointer(int source, int pointerId,
+ int eventType, int screenX, int screenY, double pressure,
+ int orientation)
+ {
+ Log.d(LOGTAG, "Synthesizing pointer from " + source + " id " + pointerId + " at " + screenX + ", " + screenY);
+
+ if (mPointerState == null) {
+ mPointerState = new SynthesizedEventState();
+ }
+
+ // Find the pointer if it already exists
+ int pointerIndex = mPointerState.getPointerIndex(pointerId);
+
+ // Event-specific handling
+ switch (eventType) {
+ case MotionEvent.ACTION_POINTER_UP:
+ if (pointerIndex < 0) {
+ Log.d(LOGTAG, "Requested synthesis of a pointer-up for a pointer that doesn't exist!");
+ return;
+ }
+ if (mPointerState.pointers.size() == 1) {
+ // Last pointer is going up
+ eventType = MotionEvent.ACTION_UP;
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ if (pointerIndex < 0) {
+ Log.d(LOGTAG, "Requested synthesis of a pointer-cancel for a pointer that doesn't exist!");
+ return;
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ if (pointerIndex < 0) {
+ // Adding a new pointer
+ pointerIndex = mPointerState.addPointer(pointerId, source);
+ if (pointerIndex == 0) {
+ // first pointer
+ eventType = MotionEvent.ACTION_DOWN;
+ mPointerState.downTime = SystemClock.uptimeMillis();
+ }
+ } else {
+ // We're moving an existing pointer
+ eventType = MotionEvent.ACTION_MOVE;
+ }
+ break;
+ case MotionEvent.ACTION_HOVER_MOVE:
+ if (pointerIndex < 0) {
+ // Mouse-move a pointer without it going "down". However
+ // in order to send the right MotionEvent without a lot of
+ // duplicated code, we add the pointer to mPointerState,
+ // and then remove it at the bottom of this function.
+ pointerIndex = mPointerState.addPointer(pointerId, source);
+ } else {
+ // We're moving an existing mouse pointer that went down.
+ eventType = MotionEvent.ACTION_MOVE;
+ }
+ break;
+ }
+
+ // Update the pointer with the new info
+ PointerInfo info = mPointerState.pointers.get(pointerIndex);
+ info.screenX = screenX;
+ info.screenY = screenY;
+ info.pressure = pressure;
+ info.orientation = orientation;
+
+ // Dispatch the event
+ int action = (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
+ action &= MotionEvent.ACTION_POINTER_INDEX_MASK;
+ action |= (eventType & MotionEvent.ACTION_MASK);
+ boolean isButtonDown = (source == InputDevice.SOURCE_MOUSE) &&
+ (eventType == MotionEvent.ACTION_DOWN || eventType == MotionEvent.ACTION_MOVE);
+ final MotionEvent event = MotionEvent.obtain(
+ /*downTime*/ mPointerState.downTime,
+ /*eventTime*/ SystemClock.uptimeMillis(),
+ /*action*/ action,
+ /*pointerCount*/ mPointerState.getPointerCount(source),
+ /*pointerProperties*/ mPointerState.getPointerProperties(source),
+ /*pointerCoords*/ mPointerState.getPointerCoords(source),
+ /*metaState*/ 0,
+ /*buttonState*/ (isButtonDown ? MotionEvent.BUTTON_PRIMARY : 0),
+ /*xPrecision*/ 0,
+ /*yPrecision*/ 0,
+ /*deviceId*/ 0,
+ /*edgeFlags*/ 0,
+ /*source*/ source,
+ /*flags*/ 0);
+ mView.post(new Runnable() {
+ @Override
+ public void run() {
+ mView.dispatchTouchEvent(event);
+ }
+ });
+
+ // Forget about removed pointers
+ if (eventType == MotionEvent.ACTION_POINTER_UP ||
+ eventType == MotionEvent.ACTION_UP ||
+ eventType == MotionEvent.ACTION_CANCEL ||
+ eventType == MotionEvent.ACTION_HOVER_MOVE)
+ {
+ mPointerState.pointers.remove(pointerIndex);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public void synthesizeNativeTouchPoint(int pointerId, int eventType, int screenX,
+ int screenY, double pressure, int orientation)
+ {
+ if (pointerId == PointerInfo.RESERVED_MOUSE_POINTER_ID) {
+ throw new IllegalArgumentException("Use a different pointer ID in your test, this one is reserved for mouse");
+ }
+ synthesizeNativePointer(InputDevice.SOURCE_TOUCHSCREEN, pointerId,
+ eventType, screenX, screenY, pressure, orientation);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public void synthesizeNativeMouseEvent(int eventType, int screenX, int screenY) {
+ synthesizeNativePointer(InputDevice.SOURCE_MOUSE, PointerInfo.RESERVED_MOUSE_POINTER_ID,
+ eventType, screenX, screenY, 0, 0);
+ }
+
+ @WrapForJNI
+ public LayerRenderer.Frame createFrame() {
+ // Create the shaders and textures if necessary.
+ if (!mLayerRendererInitialized) {
+ if (mLayerRenderer == null) {
+ return null;
+ }
+ mLayerRenderer.createDefaultProgram();
+ mLayerRendererInitialized = true;
+ }
+
+ try {
+ return mLayerRenderer.createFrame(mFrameMetrics);
+ } catch (Exception e) {
+ Log.w(LOGTAG, e);
+ return null;
+ }
+ }
+
+ private void geometryChanged() {
+ /* Let Gecko know if the screensize has changed */
+ sendResizeEventIfNecessary(false, null);
+ }
+
+ /** Implementation of LayerView.Listener */
+ @Override
+ public void surfaceChanged(int width, int height) {
+ IntSize viewportSize = mToolbarAnimator.getViewportSize();
+ setViewportSize(viewportSize.width, viewportSize.height, null);
+ }
+
+ ImmutableViewportMetrics getViewportMetrics() {
+ return mViewportMetrics;
+ }
+
+ /*
+ * You must hold the monitor while calling this.
+ */
+ private void setViewportMetrics(ImmutableViewportMetrics metrics, boolean notifyGecko) {
+ // This class owns the viewport size and the fixed layer margins; don't let other pieces
+ // of code clobber either of them. The only place the viewport size should ever be
+ // updated is in GeckoLayerClient.setViewportSize, and the only place the margins should
+ // ever be updated is in GeckoLayerClient.setFixedLayerMargins; both of these assign to
+ // mViewportMetrics directly.
+ metrics = metrics.setViewportSize(mViewportMetrics.viewportRectWidth, mViewportMetrics.viewportRectHeight);
+ mViewportMetrics = metrics;
+
+ viewportMetricsChanged(notifyGecko);
+ }
+
+ /*
+ * You must hold the monitor while calling this.
+ */
+ private void viewportMetricsChanged(boolean notifyGecko) {
+ mToolbarAnimator.onMetricsChanged(mViewportMetrics);
+
+ mView.requestRender();
+ if (notifyGecko && mGeckoIsReady) {
+ geometryChanged();
+ }
+ }
+
+ /*
+ * Updates the viewport metrics, overriding the viewport size and margins
+ * which are normally retained when calling setViewportMetrics.
+ * You must hold the monitor while calling this.
+ */
+ void forceViewportMetrics(ImmutableViewportMetrics metrics, boolean notifyGecko, boolean forceRedraw) {
+ if (forceRedraw) {
+ mForceRedraw = true;
+ }
+ mViewportMetrics = metrics;
+ viewportMetricsChanged(notifyGecko);
+ }
+
+ /** Implementation of PanZoomTarget */
+ @Override
+ public void panZoomStopped() {
+ mToolbarAnimator.onPanZoomStopped();
+ }
+
+ Object getLock() {
+ return this;
+ }
+
+ Matrix getMatrixForLayerRectToViewRect() {
+ if (!mGeckoIsReady) {
+ return null;
+ }
+
+ ImmutableViewportMetrics viewportMetrics = mViewportMetrics;
+ PointF origin = viewportMetrics.getOrigin();
+ float zoom = viewportMetrics.zoomFactor;
+ ImmutableViewportMetrics geckoViewport = mViewportMetrics;
+ PointF geckoOrigin = geckoViewport.getOrigin();
+ float geckoZoom = geckoViewport.zoomFactor;
+
+ Matrix matrix = new Matrix();
+ matrix.postTranslate(geckoOrigin.x / geckoZoom, geckoOrigin.y / geckoZoom);
+ matrix.postScale(zoom, zoom);
+ matrix.postTranslate(-origin.x, -origin.y);
+ return matrix;
+ }
+
+ @Override
+ public void setScrollingRootContent(boolean isRootContent) {
+ mToolbarAnimator.setScrollingRootContent(isRootContent);
+ }
+
+ public void addDrawListener(DrawListener listener) {
+ mDrawListeners.add(listener);
+ }
+
+ public void removeDrawListener(DrawListener listener) {
+ mDrawListeners.remove(listener);
+ }
+
+ public void setClearColor(int color) {
+ mClearColor = color;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java
new file mode 100644
index 0000000000..8072deecce
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java
@@ -0,0 +1,282 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.util.FloatUtils;
+
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.util.DisplayMetrics;
+
+/**
+ * ImmutableViewportMetrics are used to store the viewport metrics
+ * in way that we can access a version of them from multiple threads
+ * without having to take a lock
+ */
+public class ImmutableViewportMetrics {
+
+ // We need to flatten the RectF and FloatSize structures
+ // because Java doesn't have the concept of const classes
+ public final float pageRectLeft;
+ public final float pageRectTop;
+ public final float pageRectRight;
+ public final float pageRectBottom;
+ public final float cssPageRectLeft;
+ public final float cssPageRectTop;
+ public final float cssPageRectRight;
+ public final float cssPageRectBottom;
+ public final float viewportRectLeft;
+ public final float viewportRectTop;
+ public final int viewportRectWidth;
+ public final int viewportRectHeight;
+
+ public final float zoomFactor;
+
+ public ImmutableViewportMetrics(DisplayMetrics metrics) {
+ viewportRectLeft = pageRectLeft = cssPageRectLeft = 0;
+ viewportRectTop = pageRectTop = cssPageRectTop = 0;
+ viewportRectWidth = metrics.widthPixels;
+ viewportRectHeight = metrics.heightPixels;
+ pageRectRight = cssPageRectRight = metrics.widthPixels;
+ pageRectBottom = cssPageRectBottom = metrics.heightPixels;
+ zoomFactor = 1.0f;
+ }
+
+ /** This constructor is used by native code in AndroidJavaWrappers.cpp, be
+ * careful when modifying the signature.
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ private ImmutableViewportMetrics(float aPageRectLeft, float aPageRectTop,
+ float aPageRectRight, float aPageRectBottom, float aCssPageRectLeft,
+ float aCssPageRectTop, float aCssPageRectRight, float aCssPageRectBottom,
+ float aViewportRectLeft, float aViewportRectTop, int aViewportRectWidth,
+ int aViewportRectHeight, float aZoomFactor)
+ {
+ pageRectLeft = aPageRectLeft;
+ pageRectTop = aPageRectTop;
+ pageRectRight = aPageRectRight;
+ pageRectBottom = aPageRectBottom;
+ cssPageRectLeft = aCssPageRectLeft;
+ cssPageRectTop = aCssPageRectTop;
+ cssPageRectRight = aCssPageRectRight;
+ cssPageRectBottom = aCssPageRectBottom;
+ viewportRectLeft = aViewportRectLeft;
+ viewportRectTop = aViewportRectTop;
+ viewportRectWidth = aViewportRectWidth;
+ viewportRectHeight = aViewportRectHeight;
+ zoomFactor = aZoomFactor;
+ }
+
+ public float getWidth() {
+ return viewportRectWidth;
+ }
+
+ public float getHeight() {
+ return viewportRectHeight;
+ }
+
+ public float viewportRectRight() {
+ return viewportRectLeft + viewportRectWidth;
+ }
+
+ public float viewportRectBottom() {
+ return viewportRectTop + viewportRectHeight;
+ }
+
+ public PointF getOrigin() {
+ return new PointF(viewportRectLeft, viewportRectTop);
+ }
+
+ public FloatSize getSize() {
+ return new FloatSize(viewportRectWidth, viewportRectHeight);
+ }
+
+ public RectF getViewport() {
+ return new RectF(viewportRectLeft,
+ viewportRectTop,
+ viewportRectRight(),
+ viewportRectBottom());
+ }
+
+ public RectF getCssViewport() {
+ return RectUtils.scale(getViewport(), 1 / zoomFactor);
+ }
+
+ public RectF getPageRect() {
+ return new RectF(pageRectLeft, pageRectTop, pageRectRight, pageRectBottom);
+ }
+
+ public float getPageWidth() {
+ return pageRectRight - pageRectLeft;
+ }
+
+ public float getPageHeight() {
+ return pageRectBottom - pageRectTop;
+ }
+
+ public RectF getCssPageRect() {
+ return new RectF(cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom);
+ }
+
+ public RectF getOverscroll() {
+ return new RectF(Math.max(0, pageRectLeft - viewportRectLeft),
+ Math.max(0, pageRectTop - viewportRectTop),
+ Math.max(0, viewportRectRight() - pageRectRight),
+ Math.max(0, viewportRectBottom() - pageRectBottom));
+ }
+
+ /*
+ * Returns the viewport metrics that represent a linear transition between "this" and "to" at
+ * time "t", which is on the scale [0, 1). This function interpolates all values stored in
+ * the viewport metrics.
+ */
+ public ImmutableViewportMetrics interpolate(ImmutableViewportMetrics to, float t) {
+ return new ImmutableViewportMetrics(
+ FloatUtils.interpolate(pageRectLeft, to.pageRectLeft, t),
+ FloatUtils.interpolate(pageRectTop, to.pageRectTop, t),
+ FloatUtils.interpolate(pageRectRight, to.pageRectRight, t),
+ FloatUtils.interpolate(pageRectBottom, to.pageRectBottom, t),
+ FloatUtils.interpolate(cssPageRectLeft, to.cssPageRectLeft, t),
+ FloatUtils.interpolate(cssPageRectTop, to.cssPageRectTop, t),
+ FloatUtils.interpolate(cssPageRectRight, to.cssPageRectRight, t),
+ FloatUtils.interpolate(cssPageRectBottom, to.cssPageRectBottom, t),
+ FloatUtils.interpolate(viewportRectLeft, to.viewportRectLeft, t),
+ FloatUtils.interpolate(viewportRectTop, to.viewportRectTop, t),
+ (int)FloatUtils.interpolate(viewportRectWidth, to.viewportRectWidth, t),
+ (int)FloatUtils.interpolate(viewportRectHeight, to.viewportRectHeight, t),
+ FloatUtils.interpolate(zoomFactor, to.zoomFactor, t));
+ }
+
+ public ImmutableViewportMetrics setViewportSize(int width, int height) {
+ if (width == viewportRectWidth && height == viewportRectHeight) {
+ return this;
+ }
+
+ return new ImmutableViewportMetrics(
+ pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ viewportRectLeft, viewportRectTop, width, height,
+ zoomFactor);
+ }
+
+ public ImmutableViewportMetrics setViewportOrigin(float newOriginX, float newOriginY) {
+ return new ImmutableViewportMetrics(
+ pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ newOriginX, newOriginY, viewportRectWidth, viewportRectHeight,
+ zoomFactor);
+ }
+
+ public ImmutableViewportMetrics setZoomFactor(float newZoomFactor) {
+ return new ImmutableViewportMetrics(
+ pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ viewportRectLeft, viewportRectTop, viewportRectWidth, viewportRectHeight,
+ newZoomFactor);
+ }
+
+ public ImmutableViewportMetrics offsetViewportBy(float dx, float dy) {
+ return setViewportOrigin(viewportRectLeft + dx, viewportRectTop + dy);
+ }
+
+ public ImmutableViewportMetrics offsetViewportByAndClamp(float dx, float dy) {
+ return setViewportOrigin(
+ Math.max(pageRectLeft, Math.min(viewportRectLeft + dx, pageRectRight - getWidth())),
+ Math.max(pageRectTop, Math.min(viewportRectTop + dy, pageRectBottom - getHeight())));
+ }
+
+ public ImmutableViewportMetrics setPageRect(RectF pageRect, RectF cssPageRect) {
+ return new ImmutableViewportMetrics(
+ pageRect.left, pageRect.top, pageRect.right, pageRect.bottom,
+ cssPageRect.left, cssPageRect.top, cssPageRect.right, cssPageRect.bottom,
+ viewportRectLeft, viewportRectTop, viewportRectWidth, viewportRectHeight,
+ zoomFactor);
+ }
+
+ public ImmutableViewportMetrics setPageRectFrom(ImmutableViewportMetrics aMetrics) {
+ if (aMetrics.cssPageRectLeft == cssPageRectLeft &&
+ aMetrics.cssPageRectTop == cssPageRectTop &&
+ aMetrics.cssPageRectRight == cssPageRectRight &&
+ aMetrics.cssPageRectBottom == cssPageRectBottom) {
+ return this;
+ }
+ RectF css = aMetrics.getCssPageRect();
+ return setPageRect(RectUtils.scale(css, zoomFactor), css);
+ }
+
+ /* This will set the zoom factor and re-scale page-size and viewport offset
+ * accordingly. The given focus will remain at the same point on the screen
+ * after scaling.
+ */
+ public ImmutableViewportMetrics scaleTo(float newZoomFactor, PointF focus) {
+ // cssPageRect* is invariant, since we're setting the scale factor
+ // here. The page rect is based on the CSS page rect.
+ float newPageRectLeft = cssPageRectLeft * newZoomFactor;
+ float newPageRectTop = cssPageRectTop * newZoomFactor;
+ float newPageRectRight = cssPageRectLeft + ((cssPageRectRight - cssPageRectLeft) * newZoomFactor);
+ float newPageRectBottom = cssPageRectTop + ((cssPageRectBottom - cssPageRectTop) * newZoomFactor);
+
+ PointF origin = getOrigin();
+ origin.offset(focus.x, focus.y);
+ origin = PointUtils.scale(origin, newZoomFactor / zoomFactor);
+ origin.offset(-focus.x, -focus.y);
+
+ return new ImmutableViewportMetrics(
+ newPageRectLeft, newPageRectTop, newPageRectRight, newPageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ origin.x, origin.y, viewportRectWidth, viewportRectHeight,
+ newZoomFactor);
+ }
+
+ /** Clamps the viewport to remain within the page rect. */
+ public ImmutableViewportMetrics clamp() {
+ RectF newViewport = getViewport();
+
+ // The viewport bounds ought to never exceed the page bounds.
+ if (newViewport.right > pageRectRight)
+ newViewport.offset((pageRectRight) - newViewport.right, 0);
+ if (newViewport.left < pageRectLeft)
+ newViewport.offset(pageRectLeft - newViewport.left, 0);
+
+ if (newViewport.bottom > pageRectBottom)
+ newViewport.offset(0, (pageRectBottom) - newViewport.bottom);
+ if (newViewport.top < pageRectTop)
+ newViewport.offset(0, pageRectTop - newViewport.top);
+
+ // Note that since newViewport is only translated around, the viewport's
+ // width and height are unchanged.
+ return new ImmutableViewportMetrics(
+ pageRectLeft, pageRectTop, pageRectRight, pageRectBottom,
+ cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom,
+ newViewport.left, newViewport.top, viewportRectWidth, viewportRectHeight,
+ zoomFactor);
+ }
+
+ public boolean fuzzyEquals(ImmutableViewportMetrics other) {
+ // Don't bother checking the pageRectXXX values because they are a product
+ // of the cssPageRectXXX values and the zoomFactor, except with more rounding
+ // error. Checking those is both inefficient and can lead to false negatives.
+ return FloatUtils.fuzzyEquals(cssPageRectLeft, other.cssPageRectLeft)
+ && FloatUtils.fuzzyEquals(cssPageRectTop, other.cssPageRectTop)
+ && FloatUtils.fuzzyEquals(cssPageRectRight, other.cssPageRectRight)
+ && FloatUtils.fuzzyEquals(cssPageRectBottom, other.cssPageRectBottom)
+ && FloatUtils.fuzzyEquals(viewportRectLeft, other.viewportRectLeft)
+ && FloatUtils.fuzzyEquals(viewportRectTop, other.viewportRectTop)
+ && viewportRectWidth == other.viewportRectWidth
+ && viewportRectHeight == other.viewportRectHeight
+ && FloatUtils.fuzzyEquals(zoomFactor, other.zoomFactor);
+ }
+
+ @Override
+ public String toString() {
+ return "ImmutableViewportMetrics v=(" + viewportRectLeft + "," + viewportRectTop + ","
+ + viewportRectWidth + "x" + viewportRectHeight + ") p=(" + pageRectLeft + ","
+ + pageRectTop + "," + pageRectRight + "," + pageRectBottom + ") c=("
+ + cssPageRectLeft + "," + cssPageRectTop + "," + cssPageRectRight + ","
+ + cssPageRectBottom + ") z=" + zoomFactor;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/IntSize.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/IntSize.java
new file mode 100644
index 0000000000..0e847158d7
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/IntSize.java
@@ -0,0 +1,89 @@
+/* -*- 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.gfx;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class IntSize {
+ public final int width, height;
+
+ public IntSize(IntSize size) { width = size.width; height = size.height; }
+ public IntSize(int inWidth, int inHeight) { width = inWidth; height = inHeight; }
+
+ public IntSize(FloatSize size) {
+ width = Math.round(size.width);
+ height = Math.round(size.height);
+ }
+
+ public IntSize(JSONObject json) {
+ try {
+ width = json.getInt("width");
+ height = json.getInt("height");
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public int getArea() {
+ return width * height;
+ }
+
+ public boolean equals(IntSize size) {
+ return ((size.width == width) && (size.height == height));
+ }
+
+ public boolean isPositive() {
+ return (width > 0 && height > 0);
+ }
+
+ @Override
+ public String toString() { return "(" + width + "," + height + ")"; }
+
+ public IntSize scale(float factor) {
+ return new IntSize(Math.round(width * factor),
+ Math.round(height * factor));
+ }
+
+ /* Returns the power of two that is greater than or equal to value */
+ public static int nextPowerOfTwo(int value) {
+ // code taken from http://acius2.blogspot.com/2007/11/calculating-next-power-of-2.html
+ if (0 == value--) {
+ return 1;
+ }
+ value = (value >> 1) | value;
+ value = (value >> 2) | value;
+ value = (value >> 4) | value;
+ value = (value >> 8) | value;
+ value = (value >> 16) | value;
+ return value + 1;
+ }
+
+ public IntSize nextPowerOfTwo() {
+ return new IntSize(nextPowerOfTwo(width), nextPowerOfTwo(height));
+ }
+
+ public static boolean isPowerOfTwo(int value) {
+ if (value == 0)
+ return false;
+ return (value & (value - 1)) == 0;
+ }
+
+ public static int largestPowerOfTwoLessThan(float value) {
+ int val = (int) Math.floor(value);
+ if (val <= 0) {
+ throw new IllegalArgumentException("Error: value must be > 0");
+ }
+ // keep dropping the least-significant set bits until only one is left
+ int bestVal = val;
+ while (val != 0) {
+ bestVal = val;
+ val &= (val - 1);
+ }
+ return bestVal;
+ }
+}
+
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerRenderer.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerRenderer.java
new file mode 100644
index 0000000000..1a087cc2ac
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerRenderer.java
@@ -0,0 +1,275 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.mozglue.DirectBufferAllocator;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.opengl.GLES20;
+import android.os.SystemClock;
+import android.util.Log;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.microedition.khronos.egl.EGLConfig;
+
+/**
+ * The layer renderer implements the rendering logic for a layer view.
+ */
+public class LayerRenderer {
+ private static final String LOGTAG = "GeckoLayerRenderer";
+
+ /*
+ * The amount of time a frame is allowed to take to render before we declare it a dropped
+ * frame.
+ */
+ private static final int MAX_FRAME_TIME = 16; /* 1000 ms / 60 FPS */
+ private static final long NANOS_PER_MS = 1000000;
+ private static final int MAX_SCROLL_SPEED_TO_REQUEST_ZOOM_RENDER = 5;
+
+ private final LayerView mView;
+ private ByteBuffer mCoordByteBuffer;
+ private FloatBuffer mCoordBuffer;
+ private int mMaxTextureSize;
+
+ private long mLastFrameTime;
+ private final CopyOnWriteArrayList<RenderTask> mTasks;
+
+ // Dropped frames display
+ private final int[] mFrameTimings;
+ private int mCurrentFrame, mFrameTimingsSum, mDroppedFrames;
+
+ private IntBuffer mPixelBuffer;
+ private List<LayerView.ZoomedViewListener> mZoomedViewListeners;
+ private float mLastViewLeft;
+ private float mLastViewTop;
+
+ public LayerRenderer(LayerView view) {
+ mView = view;
+
+ mTasks = new CopyOnWriteArrayList<RenderTask>();
+ mLastFrameTime = System.nanoTime();
+
+ mFrameTimings = new int[60];
+ mCurrentFrame = mFrameTimingsSum = mDroppedFrames = 0;
+
+ mZoomedViewListeners = new ArrayList<LayerView.ZoomedViewListener>();
+ }
+
+ public void destroy() {
+ if (mCoordByteBuffer != null) {
+ DirectBufferAllocator.free(mCoordByteBuffer);
+ mCoordByteBuffer = null;
+ mCoordBuffer = null;
+ }
+ mZoomedViewListeners.clear();
+ }
+
+ void onSurfaceCreated(EGLConfig config) {
+ createDefaultProgram();
+ }
+
+ public void createDefaultProgram() {
+ int maxTextureSizeResult[] = new int[1];
+ GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, maxTextureSizeResult, 0);
+ mMaxTextureSize = maxTextureSizeResult[0];
+ }
+
+ public int getMaxTextureSize() {
+ return mMaxTextureSize;
+ }
+
+ public void postRenderTask(RenderTask aTask) {
+ mTasks.add(aTask);
+ mView.requestRender();
+ }
+
+ public void removeRenderTask(RenderTask aTask) {
+ mTasks.remove(aTask);
+ }
+
+ private void runRenderTasks(CopyOnWriteArrayList<RenderTask> tasks, boolean after, long frameStartTime) {
+ for (RenderTask task : tasks) {
+ if (task.runAfter != after) {
+ continue;
+ }
+
+ boolean stillRunning = task.run(frameStartTime - mLastFrameTime, frameStartTime);
+
+ // Remove the task from the list if its finished
+ if (!stillRunning) {
+ tasks.remove(task);
+ }
+ }
+ }
+
+ /** Used by robocop for testing purposes. Not for production use! */
+ IntBuffer getPixels() {
+ IntBuffer pixelBuffer = IntBuffer.allocate(mView.getWidth() * mView.getHeight());
+ synchronized (pixelBuffer) {
+ mPixelBuffer = pixelBuffer;
+ mView.requestRender();
+ try {
+ pixelBuffer.wait();
+ } catch (InterruptedException ie) {
+ }
+ mPixelBuffer = null;
+ }
+ return pixelBuffer;
+ }
+
+ private void updateDroppedFrames(long frameStartTime) {
+ int frameElapsedTime = (int)((System.nanoTime() - frameStartTime) / NANOS_PER_MS);
+
+ /* Update the running statistics. */
+ mFrameTimingsSum -= mFrameTimings[mCurrentFrame];
+ mFrameTimingsSum += frameElapsedTime;
+ mDroppedFrames -= (mFrameTimings[mCurrentFrame] + 1) / MAX_FRAME_TIME;
+ mDroppedFrames += (frameElapsedTime + 1) / MAX_FRAME_TIME;
+
+ mFrameTimings[mCurrentFrame] = frameElapsedTime;
+ mCurrentFrame = (mCurrentFrame + 1) % mFrameTimings.length;
+
+ int averageTime = mFrameTimingsSum / mFrameTimings.length;
+ }
+
+ public Frame createFrame(ImmutableViewportMetrics metrics) {
+ return new Frame(metrics);
+ }
+
+ public class Frame {
+ // The timestamp recording the start of this frame.
+ private long mFrameStartTime;
+ // A fixed snapshot of the viewport metrics that this frame is using to render content.
+ private final ImmutableViewportMetrics mFrameMetrics;
+
+ public Frame(ImmutableViewportMetrics metrics) {
+ mFrameMetrics = metrics;
+ }
+
+ /** This function is invoked via JNI; be careful when modifying signature. */
+ @WrapForJNI
+ public void beginDrawing() {
+ mFrameStartTime = System.nanoTime();
+
+ // Run through pre-render tasks
+ runRenderTasks(mTasks, false, mFrameStartTime);
+ }
+
+
+ private void maybeRequestZoomedViewRender() {
+ // Concurrently update of mZoomedViewListeners should not be an issue here
+ // because the following line is just a short-circuit
+ if (mZoomedViewListeners.size() == 0) {
+ return;
+ }
+
+ // When scrolling fast, do not request zoomed view render to avoid to slow down
+ // the scroll in the main view.
+ // Speed is estimated using the offset changes between 2 display frame calls
+ final float viewLeft = Math.round(mFrameMetrics.getViewport().left);
+ final float viewTop = Math.round(mFrameMetrics.getViewport().top);
+ boolean shouldWaitToRender = false;
+
+ if (Math.abs(mLastViewLeft - viewLeft) > MAX_SCROLL_SPEED_TO_REQUEST_ZOOM_RENDER ||
+ Math.abs(mLastViewTop - viewTop) > MAX_SCROLL_SPEED_TO_REQUEST_ZOOM_RENDER) {
+ shouldWaitToRender = true;
+ }
+
+ mLastViewLeft = viewLeft;
+ mLastViewTop = viewTop;
+
+ if (shouldWaitToRender) {
+ return;
+ }
+
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ for (LayerView.ZoomedViewListener listener : mZoomedViewListeners) {
+ listener.requestZoomedViewRender();
+ }
+ }
+ });
+ }
+
+
+ /** This function is invoked via JNI; be careful when modifying signature. */
+ @WrapForJNI
+ public void endDrawing() {
+ PanningPerfAPI.recordFrameTime();
+
+ runRenderTasks(mTasks, true, mFrameStartTime);
+ maybeRequestZoomedViewRender();
+
+ /* Used by robocop for testing purposes */
+ IntBuffer pixelBuffer = mPixelBuffer;
+ if (pixelBuffer != null) {
+ synchronized (pixelBuffer) {
+ pixelBuffer.position(0);
+ GLES20.glReadPixels(0, 0, Math.round(mFrameMetrics.getWidth()),
+ Math.round(mFrameMetrics.getHeight()), GLES20.GL_RGBA,
+ GLES20.GL_UNSIGNED_BYTE, pixelBuffer);
+ pixelBuffer.notify();
+ }
+ }
+
+ // Remove background color once we've painted. GeckoLayerClient is
+ // responsible for setting this flag before current document is
+ // composited.
+ if (mView.getPaintState() == LayerView.PAINT_BEFORE_FIRST) {
+ mView.post(new Runnable() {
+ @Override
+ public void run() {
+ mView.setSurfaceBackgroundColor(Color.TRANSPARENT);
+ }
+ });
+ mView.setPaintState(LayerView.PAINT_AFTER_FIRST);
+ }
+ mLastFrameTime = mFrameStartTime;
+ }
+ }
+
+ public void updateZoomedView(final ByteBuffer data) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ for (LayerView.ZoomedViewListener listener : mZoomedViewListeners) {
+ data.position(0);
+ listener.updateView(data);
+ }
+ }
+ });
+ }
+
+ public void addZoomedViewListener(LayerView.ZoomedViewListener listener) {
+ ThreadUtils.assertOnUiThread();
+ mZoomedViewListeners.add(listener);
+ }
+
+ public void removeZoomedViewListener(LayerView.ZoomedViewListener listener) {
+ ThreadUtils.assertOnUiThread();
+ mZoomedViewListeners.remove(listener);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java
new file mode 100644
index 0000000000..969aa3f248
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java
@@ -0,0 +1,711 @@
+/* -*- 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.gfx;
+
+import java.nio.ByteBuffer;
+import java.nio.IntBuffer;
+
+import org.mozilla.gecko.AndroidGamepadManager;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.GeckoAccessibility;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.InputDevice;
+import android.widget.FrameLayout;
+
+/**
+ * A view rendered by the layer compositor.
+ */
+public class LayerView extends FrameLayout {
+ private static final String LOGTAG = "GeckoLayerView";
+
+ private GeckoLayerClient mLayerClient;
+ private PanZoomController mPanZoomController;
+ private DynamicToolbarAnimator mToolbarAnimator;
+ private LayerRenderer mRenderer;
+ /* Must be a PAINT_xxx constant */
+ private int mPaintState;
+ private FullScreenState mFullScreenState;
+
+ private SurfaceView mSurfaceView;
+ private TextureView mTextureView;
+
+ private Listener mListener;
+
+ /* This should only be modified on the Java UI thread. */
+ private final Overscroll mOverscroll;
+
+ private boolean mServerSurfaceValid;
+ private int mWidth, mHeight;
+
+ private boolean onAttachedToWindowCalled;
+
+ /* This is written by the Gecko thread and the UI thread, and read by the UI thread. */
+ @WrapForJNI(stubName = "CompositorCreated", calledFrom = "ui")
+ /* package */ volatile boolean mCompositorCreated;
+
+ private class Compositor extends JNIObject {
+ public Compositor() {
+ }
+
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+ @Override protected native void disposeNative();
+
+ // Gecko thread sets its Java instances; does not block UI thread.
+ @WrapForJNI(calledFrom = "any", dispatchTo = "gecko")
+ /* package */ native void attachToJava(GeckoLayerClient layerClient,
+ NativePanZoomController npzc);
+
+ @WrapForJNI(calledFrom = "any", dispatchTo = "gecko")
+ /* package */ native void onSizeChanged(int windowWidth, int windowHeight,
+ int screenWidth, int screenHeight);
+
+ // Gecko thread creates compositor; blocks UI thread.
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "proxy")
+ /* package */ native void createCompositor(int width, int height, Object surface);
+
+ // Gecko thread pauses compositor; blocks UI thread.
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "current")
+ /* package */ native void syncPauseCompositor();
+
+ // UI thread resumes compositor and notifies Gecko thread; does not block UI thread.
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "current")
+ /* package */ native void syncResumeResizeCompositor(int width, int height, Object surface);
+
+ @WrapForJNI(calledFrom = "any", dispatchTo = "current")
+ /* package */ native void syncInvalidateAndScheduleComposite();
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void reattach() {
+ mCompositorCreated = true;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void destroy() {
+ // The nsWindow has been closed. First mark our compositor as destroyed.
+ LayerView.this.mCompositorCreated = false;
+
+ LayerView.this.mLayerClient.setGeckoReady(false);
+
+ // Then clear out any pending calls on the UI thread by disposing on the UI thread.
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ disposeNative();
+ }
+ });
+ }
+ }
+
+ private final Compositor mCompositor = new Compositor();
+
+ /* Flags used to determine when to show the painted surface. */
+ public static final int PAINT_START = 0;
+ public static final int PAINT_BEFORE_FIRST = 1;
+ public static final int PAINT_AFTER_FIRST = 2;
+
+ public boolean shouldUseTextureView() {
+ // Disable TextureView support for now as it causes panning/zooming
+ // performance regressions (see bug 792259). Uncomment the code below
+ // once this bug is fixed.
+ return false;
+
+ /*
+ // we can only use TextureView on ICS or higher
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ Log.i(LOGTAG, "Not using TextureView: not on ICS+");
+ return false;
+ }
+
+ try {
+ // and then we can only use it if we have a hardware accelerated window
+ Method m = View.class.getMethod("isHardwareAccelerated", (Class[]) null);
+ return (Boolean) m.invoke(this);
+ } catch (Exception e) {
+ Log.i(LOGTAG, "Not using TextureView: caught exception checking for hw accel: " + e.toString());
+ return false;
+ } */
+ }
+
+ public LayerView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mPaintState = PAINT_START;
+ mFullScreenState = FullScreenState.NONE;
+
+ mOverscroll = new OverscrollEdgeEffect(this);
+ }
+
+ public LayerView(Context context) {
+ this(context, null);
+ }
+
+ public void initializeView(EventDispatcher eventDispatcher) {
+ mLayerClient = new GeckoLayerClient(getContext(), this, eventDispatcher);
+ if (mOverscroll != null) {
+ mLayerClient.setOverscrollHandler(mOverscroll);
+ }
+
+ mPanZoomController = mLayerClient.getPanZoomController();
+ mToolbarAnimator = mLayerClient.getDynamicToolbarAnimator();
+
+ mRenderer = new LayerRenderer(this);
+
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+
+ GeckoAccessibility.setDelegate(this);
+ }
+
+ /**
+ * MotionEventHelper dragAsync() robocop tests can instruct
+ * PanZoomController not to generate longpress events.
+ */
+ public void setIsLongpressEnabled(boolean isLongpressEnabled) {
+ mPanZoomController.setIsLongpressEnabled(isLongpressEnabled);
+ }
+
+ private static Point getEventRadius(MotionEvent event) {
+ return new Point((int)event.getToolMajor() / 2,
+ (int)event.getToolMinor() / 2);
+ }
+
+ public void showSurface() {
+ // Fix this if TextureView support is turned back on above
+ mSurfaceView.setVisibility(View.VISIBLE);
+ }
+
+ public void hideSurface() {
+ // Fix this if TextureView support is turned back on above
+ mSurfaceView.setVisibility(View.INVISIBLE);
+ }
+
+ public void destroy() {
+ if (mLayerClient != null) {
+ mLayerClient.destroy();
+ }
+ if (mRenderer != null) {
+ mRenderer.destroy();
+ }
+ }
+
+ @Override
+ public void dispatchDraw(final Canvas canvas) {
+ super.dispatchDraw(canvas);
+
+ // We must have a layer client to get valid viewport metrics
+ if (mLayerClient != null && mOverscroll != null) {
+ mOverscroll.draw(canvas, getViewportMetrics());
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ requestFocus();
+ }
+
+ if (mToolbarAnimator != null && mToolbarAnimator.onInterceptTouchEvent(event)) {
+ if (mPanZoomController != null) {
+ mPanZoomController.onMotionEventVelocity(event.getEventTime(), mToolbarAnimator.getVelocity());
+ }
+ return true;
+ }
+ if (!mLayerClient.isGeckoReady()) {
+ // If gecko isn't loaded yet, don't try sending events to the
+ // native code because it's just going to crash
+ return true;
+ }
+ if (mPanZoomController != null && mPanZoomController.onTouchEvent(event)) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onHoverEvent(MotionEvent event) {
+ // If we get a touchscreen hover event, and accessibility is not enabled,
+ // don't send it to gecko.
+ if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN &&
+ !GeckoAccessibility.isEnabled()) {
+ return false;
+ }
+
+ if (!mLayerClient.isGeckoReady()) {
+ // If gecko isn't loaded yet, don't try sending events to the
+ // native code because it's just going to crash
+ return true;
+ } else if (mPanZoomController != null && mPanZoomController.onMotionEvent(event)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ if (AndroidGamepadManager.handleMotionEvent(event)) {
+ return true;
+ }
+ if (!mLayerClient.isGeckoReady()) {
+ // If gecko isn't loaded yet, don't try sending events to the
+ // native code because it's just going to crash
+ return true;
+ }
+ if (mPanZoomController != null && mPanZoomController.onMotionEvent(event)) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(final Parcelable state) {
+ if (onAttachedToWindowCalled) {
+ attachCompositor();
+ }
+ super.onRestoreInstanceState(state);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ // We are adding descendants to this LayerView, but we don't want the
+ // descendants to affect the way LayerView retains its focus.
+ setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);
+
+ // This check should not be done before the view is attached to a window
+ // as hardware acceleration will not be enabled at that point.
+ // We must create and add the SurfaceView instance before the view tree
+ // is fully created to avoid flickering (see bug 801477).
+ if (shouldUseTextureView()) {
+ mTextureView = new TextureView(getContext());
+ mTextureView.setSurfaceTextureListener(new SurfaceTextureListener());
+
+ // The background is set to this color when the LayerView is
+ // created, and it will be shown immediately at startup. Shortly
+ // after, the tab's background color will be used before any content
+ // is shown.
+ mTextureView.setBackgroundColor(Color.WHITE);
+ addView(mTextureView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+ } else {
+ // This will stop PropertyAnimator from creating a drawing cache (i.e. a bitmap)
+ // from a SurfaceView, which is just not possible (the bitmap will be transparent).
+ setWillNotCacheDrawing(false);
+
+ mSurfaceView = new LayerSurfaceView(getContext(), this);
+ mSurfaceView.setBackgroundColor(Color.WHITE);
+ addView(mSurfaceView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+
+ SurfaceHolder holder = mSurfaceView.getHolder();
+ holder.addCallback(new SurfaceListener());
+ }
+
+ attachCompositor();
+
+ onAttachedToWindowCalled = true;
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ onAttachedToWindowCalled = false;
+ }
+
+ // Don't expose GeckoLayerClient to things outside this package; only expose it as an Object
+ GeckoLayerClient getLayerClient() { return mLayerClient; }
+
+ public PanZoomController getPanZoomController() { return mPanZoomController; }
+ public DynamicToolbarAnimator getDynamicToolbarAnimator() { return mToolbarAnimator; }
+
+ public ImmutableViewportMetrics getViewportMetrics() {
+ return mLayerClient.getViewportMetrics();
+ }
+
+ public Matrix getMatrixForLayerRectToViewRect() {
+ return mLayerClient.getMatrixForLayerRectToViewRect();
+ }
+
+ public void setSurfaceBackgroundColor(int newColor) {
+ if (mSurfaceView != null) {
+ mSurfaceView.setBackgroundColor(newColor);
+ }
+ }
+
+ public void requestRender() {
+ if (mCompositorCreated) {
+ mCompositor.syncInvalidateAndScheduleComposite();
+ }
+ }
+
+ public void postRenderTask(RenderTask task) {
+ mRenderer.postRenderTask(task);
+ }
+
+ public void removeRenderTask(RenderTask task) {
+ mRenderer.removeRenderTask(task);
+ }
+
+ public int getMaxTextureSize() {
+ return mRenderer.getMaxTextureSize();
+ }
+
+ /** Used by robocop for testing purposes. Not for production use! */
+ @RobocopTarget
+ public IntBuffer getPixels() {
+ return mRenderer.getPixels();
+ }
+
+ /* paintState must be a PAINT_xxx constant. */
+ public void setPaintState(int paintState) {
+ mPaintState = paintState;
+ }
+
+ public int getPaintState() {
+ return mPaintState;
+ }
+
+ public LayerRenderer getRenderer() {
+ return mRenderer;
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ Listener getListener() {
+ return mListener;
+ }
+
+ private void attachCompositor() {
+ final NativePanZoomController npzc = (NativePanZoomController) mPanZoomController;
+
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ mCompositor.attachToJava(mLayerClient, npzc);
+ } else {
+ GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
+ mCompositor, "attachToJava",
+ GeckoLayerClient.class, mLayerClient,
+ NativePanZoomController.class, npzc);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "ui")
+ protected Object getCompositor() {
+ return mCompositor;
+ }
+
+ void serverSurfaceChanged(int newWidth, int newHeight) {
+ ThreadUtils.assertOnUiThread();
+
+ mWidth = newWidth;
+ mHeight = newHeight;
+ mServerSurfaceValid = true;
+
+ updateCompositor();
+ }
+
+ void updateCompositor() {
+ ThreadUtils.assertOnUiThread();
+
+ if (mCompositorCreated) {
+ // If the compositor has already been created, just resume it instead. We don't need
+ // to block here because if the surface is destroyed before the compositor grabs it,
+ // we can handle that gracefully (i.e. the compositor will remain paused).
+ if (!mServerSurfaceValid) {
+ return;
+ }
+ // Asking Gecko to resume the compositor takes too long (see
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=735230#c23), so we
+ // resume the compositor directly. We still need to inform Gecko about
+ // the compositor resuming, so that Gecko knows that it can now draw.
+ // It is important to not notify Gecko until after the compositor has
+ // been resumed, otherwise Gecko may send updates that get dropped.
+ mCompositor.syncResumeResizeCompositor(mWidth, mHeight, getSurface());
+ return;
+ }
+
+ // Only try to create the compositor if we have a valid surface and gecko is up. When these
+ // two conditions are satisfied, we can be relatively sure that the compositor creation will
+ // happen without needing to block anywhere.
+ if (mServerSurfaceValid && getLayerClient().isGeckoReady()) {
+ mCompositorCreated = true;
+ mCompositor.createCompositor(mWidth, mHeight, getSurface());
+ }
+ }
+
+ /* When using a SurfaceView (mSurfaceView != null), resizing happens in two
+ * phases. First, the LayerView changes size, then, often some frames later,
+ * the SurfaceView changes size. Because of this, we need to split the
+ * resize into two phases to avoid jittering.
+ *
+ * The first phase is the LayerView size change. mListener is notified so
+ * that a synchronous draw can be performed (otherwise a blank frame will
+ * appear).
+ *
+ * The second phase is the SurfaceView size change. At this point, the
+ * backing GL surface is resized and another synchronous draw is performed.
+ * Gecko is also sent the new window size, and this will likely cause an
+ * extra draw a few frames later, after it's re-rendered and caught up.
+ *
+ * In the case that there is no valid GL surface (for example, when
+ * resuming, or when coming back from the awesomescreen), or we're using a
+ * TextureView instead of a SurfaceView, the first phase is skipped.
+ */
+ private void onSizeChanged(int width, int height) {
+ if (!mServerSurfaceValid || mSurfaceView == null) {
+ surfaceChanged(width, height);
+ return;
+ }
+
+ if (mCompositorCreated) {
+ mCompositor.syncResumeResizeCompositor(width, height, getSurface());
+ }
+
+ if (mOverscroll != null) {
+ mOverscroll.setSize(width, height);
+ }
+ }
+
+ private void surfaceChanged(int width, int height) {
+ serverSurfaceChanged(width, height);
+
+ if (mListener != null) {
+ mListener.surfaceChanged(width, height);
+ }
+
+ if (mOverscroll != null) {
+ mOverscroll.setSize(width, height);
+ }
+ }
+
+ void notifySizeChanged(int windowWidth, int windowHeight, int screenWidth, int screenHeight) {
+ mCompositor.onSizeChanged(windowWidth, windowHeight, screenWidth, screenHeight);
+ }
+
+ void serverSurfaceDestroyed() {
+ ThreadUtils.assertOnUiThread();
+
+ // We need to coordinate with Gecko when pausing composition, to ensure
+ // that Gecko never executes a draw event while the compositor is paused.
+ // This is sent synchronously to make sure that we don't attempt to use
+ // any outstanding Surfaces after we call this (such as from a
+ // serverSurfaceDestroyed notification), and to make sure that any in-flight
+ // Gecko draw events have been processed. When this returns, composition is
+ // definitely paused -- it'll synchronize with the Gecko event loop, which
+ // in turn will synchronize with the compositor thread.
+ if (mCompositorCreated) {
+ mCompositor.syncPauseCompositor();
+ }
+
+ mServerSurfaceValid = false;
+ }
+
+ private void onDestroyed() {
+ serverSurfaceDestroyed();
+ }
+
+ public Object getNativeWindow() {
+ if (mSurfaceView != null)
+ return mSurfaceView.getHolder();
+
+ return mTextureView.getSurfaceTexture();
+ }
+
+ public Object getSurface() {
+ if (mSurfaceView != null) {
+ return mSurfaceView.getHolder().getSurface();
+ }
+ return null;
+ }
+
+ // This method is called on the Gecko main thread.
+ @WrapForJNI(calledFrom = "gecko")
+ private static void updateZoomedView(ByteBuffer data) {
+ LayerView layerView = GeckoAppShell.getLayerView();
+ if (layerView != null) {
+ LayerRenderer layerRenderer = layerView.getRenderer();
+ if (layerRenderer != null) {
+ layerRenderer.updateZoomedView(data);
+ }
+ }
+ }
+
+ public interface Listener {
+ void surfaceChanged(int width, int height);
+ }
+
+ private class SurfaceListener implements SurfaceHolder.Callback {
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width,
+ int height) {
+ onSizeChanged(width, height);
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ onDestroyed();
+ }
+ }
+
+ /* A subclass of SurfaceView to listen to layout changes, as
+ * View.OnLayoutChangeListener requires API level 11.
+ */
+ private class LayerSurfaceView extends SurfaceView {
+ private LayerView mParent;
+
+ public LayerSurfaceView(Context aContext, LayerView aParent) {
+ super(aContext);
+ mParent = aParent;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (changed && mParent.mServerSurfaceValid) {
+ mParent.surfaceChanged(right - left, bottom - top);
+ }
+ }
+ }
+
+ private class SurfaceTextureListener implements TextureView.SurfaceTextureListener {
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+ // We don't do this for surfaceCreated above because it is always followed by a surfaceChanged,
+ // but that is not the case here.
+ onSizeChanged(width, height);
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+ onDestroyed();
+ return true; // allow Android to call release() on the SurfaceTexture, we are done drawing to it
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+ onSizeChanged(width, height);
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+
+ }
+ }
+
+ @RobocopTarget
+ public void addDrawListener(DrawListener listener) {
+ mLayerClient.addDrawListener(listener);
+ }
+
+ @RobocopTarget
+ public void removeDrawListener(DrawListener listener) {
+ mLayerClient.removeDrawListener(listener);
+ }
+
+ @RobocopTarget
+ public static interface DrawListener {
+ public void drawFinished();
+ }
+
+ @Override
+ public void setOverScrollMode(int overscrollMode) {
+ super.setOverScrollMode(overscrollMode);
+ }
+
+ @Override
+ public int getOverScrollMode() {
+ return super.getOverScrollMode();
+ }
+
+ public float getZoomFactor() {
+ return getLayerClient().getViewportMetrics().zoomFactor;
+ }
+
+ @Override
+ public void onFocusChanged (boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+ GeckoAccessibility.onLayerViewFocusChanged(gainFocus);
+ }
+
+ public void setFullScreenState(FullScreenState state) {
+ mFullScreenState = state;
+ }
+
+ public boolean isFullScreen() {
+ return mFullScreenState != FullScreenState.NONE;
+ }
+
+ public void setMaxTranslation(float aMaxTranslation) {
+ mToolbarAnimator.setMaxTranslation(aMaxTranslation);
+ }
+
+ public void setSurfaceTranslation(float translation) {
+ setTranslationY(translation);
+ }
+
+ public float getSurfaceTranslation() {
+ return getTranslationY();
+ }
+
+ // Public hooks for dynamic toolbar translation
+
+ public interface DynamicToolbarListener {
+ public void onTranslationChanged(float aToolbarTranslation, float aLayerViewTranslation);
+ public void onPanZoomStopped();
+ public void onMetricsChanged(ImmutableViewportMetrics viewport);
+ }
+
+ // Public hooks for zoomed view
+
+ public interface ZoomedViewListener {
+ public void requestZoomedViewRender();
+ public void updateView(ByteBuffer data);
+ }
+
+ public void addZoomedViewListener(ZoomedViewListener listener) {
+ mRenderer.addZoomedViewListener(listener);
+ }
+
+ public void removeZoomedViewListener(ZoomedViewListener listener) {
+ mRenderer.removeZoomedViewListener(listener);
+ }
+
+ public void setClearColor(int color) {
+ if (mLayerClient != null) {
+ mLayerClient.setClearColor(color);
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/NativePanZoomController.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/NativePanZoomController.java
new file mode 100644
index 0000000000..6b643c85b4
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/NativePanZoomController.java
@@ -0,0 +1,300 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.PrefsHelper;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import org.json.JSONObject;
+
+import android.graphics.PointF;
+import android.util.TypedValue;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.InputDevice;
+
+class NativePanZoomController extends JNIObject implements PanZoomController {
+ private final PanZoomTarget mTarget;
+ private final LayerView mView;
+ private boolean mDestroyed;
+ private Overscroll mOverscroll;
+ boolean mNegateWheelScroll;
+ private float mPointerScrollFactor;
+ private PrefsHelper.PrefHandler mPrefsObserver;
+ private long mLastDownTime;
+ private static final float MAX_SCROLL = 0.075f * GeckoAppShell.getDpi();
+
+ @WrapForJNI(calledFrom = "ui")
+ private native boolean handleMotionEvent(
+ int action, int actionIndex, long time, int metaState,
+ int pointerId[], float x[], float y[], float orientation[], float pressure[],
+ float toolMajor[], float toolMinor[]);
+
+ @WrapForJNI(calledFrom = "ui")
+ private native boolean handleScrollEvent(
+ long time, int metaState,
+ float x, float y,
+ float hScroll, float vScroll);
+
+ @WrapForJNI(calledFrom = "ui")
+ private native boolean handleMouseEvent(
+ int action, long time, int metaState,
+ float x, float y, int buttons);
+
+ @WrapForJNI(calledFrom = "ui")
+ private native void handleMotionEventVelocity(long time, float ySpeed);
+
+ private boolean handleMotionEvent(MotionEvent event) {
+ if (mDestroyed) {
+ return false;
+ }
+
+ final int action = event.getActionMasked();
+ final int count = event.getPointerCount();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mLastDownTime = event.getDownTime();
+ } else if (mLastDownTime != event.getDownTime()) {
+ return false;
+ }
+
+ final int[] pointerId = new int[count];
+ final float[] x = new float[count];
+ final float[] y = new float[count];
+ final float[] orientation = new float[count];
+ final float[] pressure = new float[count];
+ final float[] toolMajor = new float[count];
+ final float[] toolMinor = new float[count];
+
+ final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+
+ for (int i = 0; i < count; i++) {
+ pointerId[i] = event.getPointerId(i);
+ event.getPointerCoords(i, coords);
+
+ x[i] = coords.x;
+ y[i] = coords.y;
+
+ orientation[i] = coords.orientation;
+ pressure[i] = coords.pressure;
+
+ // If we are converting to CSS pixels, we should adjust the radii as well.
+ toolMajor[i] = coords.toolMajor;
+ toolMinor[i] = coords.toolMinor;
+ }
+
+ return handleMotionEvent(action, event.getActionIndex(), event.getEventTime(),
+ event.getMetaState(), pointerId, x, y, orientation, pressure,
+ toolMajor, toolMinor);
+ }
+
+ private boolean handleScrollEvent(MotionEvent event) {
+ if (mDestroyed) {
+ return false;
+ }
+
+ final int count = event.getPointerCount();
+
+ if (count <= 0) {
+ return false;
+ }
+
+ final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+ event.getPointerCoords(0, coords);
+ final float x = coords.x;
+ final float y = coords.y;
+
+ final float flipFactor = mNegateWheelScroll ? -1.0f : 1.0f;
+ final float hScroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL) * flipFactor * mPointerScrollFactor;
+ final float vScroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL) * flipFactor * mPointerScrollFactor;
+
+ return handleScrollEvent(event.getEventTime(), event.getMetaState(), x, y, hScroll, vScroll);
+ }
+
+ private boolean handleMouseEvent(MotionEvent event) {
+ if (mDestroyed) {
+ return false;
+ }
+
+ final int count = event.getPointerCount();
+
+ if (count <= 0) {
+ return false;
+ }
+
+ final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+ event.getPointerCoords(0, coords);
+ final float x = coords.x;
+ final float y = coords.y;
+
+ return handleMouseEvent(event.getActionMasked(), event.getEventTime(), event.getMetaState(), x, y, event.getButtonState());
+ }
+
+
+ NativePanZoomController(PanZoomTarget target, View view) {
+ mTarget = target;
+ mView = (LayerView) view;
+ mDestroyed = true;
+
+ String[] prefs = { "ui.scrolling.negate_wheel_scroll" };
+ mPrefsObserver = new PrefsHelper.PrefHandlerBase() {
+ @Override public void prefValue(String pref, boolean value) {
+ if (pref.equals("ui.scrolling.negate_wheel_scroll")) {
+ mNegateWheelScroll = value;
+ }
+ }
+ };
+ PrefsHelper.addObserver(prefs, mPrefsObserver);
+
+ TypedValue outValue = new TypedValue();
+ if (view.getContext().getTheme().resolveAttribute(android.R.attr.listPreferredItemHeight, outValue, true)) {
+ mPointerScrollFactor = outValue.getDimension(view.getContext().getResources().getDisplayMetrics());
+ } else {
+ mPointerScrollFactor = MAX_SCROLL;
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+// NOTE: This commented out block of code allows Fennec to generate
+// mouse event instead of converting them to touch events.
+// This gives Fennec similar behaviour to desktop when using
+// a mouse.
+//
+// if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
+// return handleMouseEvent(event);
+// } else {
+// return handleMotionEvent(event);
+// }
+ return handleMotionEvent(event);
+ }
+
+ @Override
+ public boolean onMotionEvent(MotionEvent event) {
+ final int action = event.getActionMasked();
+ if (action == MotionEvent.ACTION_SCROLL) {
+ if (event.getDownTime() >= mLastDownTime) {
+ mLastDownTime = event.getDownTime();
+ } else if ((InputDevice.getDevice(event.getDeviceId()).getSources() & InputDevice.SOURCE_TOUCHPAD) == InputDevice.SOURCE_TOUCHPAD) {
+ return false;
+ }
+ return handleScrollEvent(event);
+ } else if ((action == MotionEvent.ACTION_HOVER_MOVE) ||
+ (action == MotionEvent.ACTION_HOVER_ENTER) ||
+ (action == MotionEvent.ACTION_HOVER_EXIT)) {
+ return handleMouseEvent(event);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void onMotionEventVelocity(final long aEventTime, final float aSpeedY) {
+ handleMotionEventVelocity(aEventTime, aSpeedY);
+ }
+
+ @Override @WrapForJNI(calledFrom = "ui") // PanZoomController
+ public void destroy() {
+ if (mPrefsObserver != null) {
+ PrefsHelper.removeObserver(mPrefsObserver);
+ mPrefsObserver = null;
+ }
+ if (mDestroyed || !mTarget.isGeckoReady()) {
+ return;
+ }
+ mDestroyed = true;
+ disposeNative();
+ }
+
+ @Override
+ public void attach() {
+ mDestroyed = false;
+ }
+
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko") @Override // JNIObject
+ protected native void disposeNative();
+
+ @Override
+ public void setOverscrollHandler(final Overscroll handler) {
+ mOverscroll = handler;
+ }
+
+ @WrapForJNI(stubName = "SetIsLongpressEnabled") // Called from test thread.
+ private native void nativeSetIsLongpressEnabled(boolean isLongpressEnabled);
+
+ @Override // PanZoomController
+ public void setIsLongpressEnabled(boolean isLongpressEnabled) {
+ if (!mDestroyed) {
+ nativeSetIsLongpressEnabled(isLongpressEnabled);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "ui")
+ private native void adjustScrollForSurfaceShift(float aX, float aY);
+
+ @Override // PanZoomController
+ public ImmutableViewportMetrics adjustScrollForSurfaceShift(ImmutableViewportMetrics aMetrics, PointF aShift) {
+ adjustScrollForSurfaceShift(aShift.x, aShift.y);
+ return aMetrics.offsetViewportByAndClamp(aShift.x, aShift.y);
+ }
+
+ @WrapForJNI
+ private void updateOverscrollVelocity(final float x, final float y) {
+ if (mOverscroll != null) {
+ if (ThreadUtils.isOnUiThread() == true) {
+ mOverscroll.setVelocity(x * 1000.0f, Overscroll.Axis.X);
+ mOverscroll.setVelocity(y * 1000.0f, Overscroll.Axis.Y);
+ } else {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // Multiply the velocity by 1000 to match what was done in JPZ.
+ mOverscroll.setVelocity(x * 1000.0f, Overscroll.Axis.X);
+ mOverscroll.setVelocity(y * 1000.0f, Overscroll.Axis.Y);
+ }
+ });
+ }
+ }
+ }
+
+ @WrapForJNI
+ private void updateOverscrollOffset(final float x, final float y) {
+ if (mOverscroll != null) {
+ if (ThreadUtils.isOnUiThread() == true) {
+ mOverscroll.setDistance(x, Overscroll.Axis.X);
+ mOverscroll.setDistance(y, Overscroll.Axis.Y);
+ } else {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mOverscroll.setDistance(x, Overscroll.Axis.X);
+ mOverscroll.setDistance(y, Overscroll.Axis.Y);
+ }
+ });
+ }
+ }
+ }
+
+ @WrapForJNI(calledFrom = "ui")
+ private void setScrollingRootContent(final boolean isRootContent) {
+ mTarget.setScrollingRootContent(isRootContent);
+ }
+
+ /**
+ * Active SelectionCaretDrag requires DynamicToolbarAnimator to be pinned
+ * to avoid unwanted scroll interactions.
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ private void onSelectionDragState(boolean state) {
+ mView.getDynamicToolbarAnimator().setPinned(state, PinReason.CARET_DRAG);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/Overscroll.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/Overscroll.java
new file mode 100644
index 0000000000..e442444d5a
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/Overscroll.java
@@ -0,0 +1,21 @@
+/* -*- 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.gfx;
+
+import android.graphics.Canvas;
+
+public interface Overscroll {
+ // The axis to show overscroll on.
+ public enum Axis {
+ X,
+ Y,
+ };
+
+ public void draw(final Canvas canvas, final ImmutableViewportMetrics metrics);
+ public void setSize(final int width, final int height);
+ public void setVelocity(final float velocity, final Axis axis);
+ public void setDistance(final float distance, final Axis axis);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/OverscrollEdgeEffect.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/OverscrollEdgeEffect.java
new file mode 100644
index 0000000000..85e04d9f26
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/OverscrollEdgeEffect.java
@@ -0,0 +1,162 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.AppConstants.Versions;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.widget.EdgeEffect;
+
+import java.lang.reflect.Field;
+
+public class OverscrollEdgeEffect implements Overscroll {
+ // Used to index particular edges in the edges array
+ private static final int TOP = 0;
+ private static final int BOTTOM = 1;
+ private static final int LEFT = 2;
+ private static final int RIGHT = 3;
+
+ // All four edges of the screen
+ private final EdgeEffect[] mEdges = new EdgeEffect[4];
+
+ // The view we're showing this overscroll on.
+ private final LayerView mView;
+
+ public OverscrollEdgeEffect(final LayerView v) {
+ Field paintField = null;
+ if (Versions.feature21Plus) {
+ try {
+ paintField = EdgeEffect.class.getDeclaredField("mPaint");
+ paintField.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ }
+ }
+
+ mView = v;
+ Context context = v.getContext();
+ for (int i = 0; i < 4; i++) {
+ mEdges[i] = new EdgeEffect(context);
+
+ try {
+ if (paintField != null) {
+ final Paint p = (Paint) paintField.get(mEdges[i]);
+
+ // The Android EdgeEffect class uses a mode of SRC_ATOP here, which means it will only
+ // draw the effect where there are non-transparent pixels in the destination. Since the LayerView
+ // itself is fully transparent, it doesn't display at all. We need to use SRC instead.
+ p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
+ }
+ } catch (IllegalAccessException e) {
+ }
+ }
+ }
+
+ @Override
+ public void setSize(final int width, final int height) {
+ mEdges[LEFT].setSize(height, width);
+ mEdges[RIGHT].setSize(height, width);
+ mEdges[TOP].setSize(width, height);
+ mEdges[BOTTOM].setSize(width, height);
+ }
+
+ private EdgeEffect getEdgeForAxisAndSide(final Axis axis, final float side) {
+ if (axis == Axis.Y) {
+ if (side < 0) {
+ return mEdges[TOP];
+ } else {
+ return mEdges[BOTTOM];
+ }
+ } else {
+ if (side < 0) {
+ return mEdges[LEFT];
+ } else {
+ return mEdges[RIGHT];
+ }
+ }
+ }
+
+ private void invalidate() {
+ if (Versions.feature16Plus) {
+ mView.postInvalidateOnAnimation();
+ } else {
+ mView.postInvalidateDelayed(10);
+ }
+ }
+
+ @Override
+ public void setVelocity(final float velocity, final Axis axis) {
+ final EdgeEffect edge = getEdgeForAxisAndSide(axis, velocity);
+
+ // If we're showing overscroll already, start fading it out.
+ if (!edge.isFinished()) {
+ edge.onRelease();
+ } else {
+ // Otherwise, show an absorb effect
+ edge.onAbsorb((int)velocity);
+ }
+
+ invalidate();
+ }
+
+ @Override
+ public void setDistance(final float distance, final Axis axis) {
+ // The first overscroll event often has zero distance. Throw it out
+ if (distance == 0.0f) {
+ return;
+ }
+
+ final EdgeEffect edge = getEdgeForAxisAndSide(axis, (int)distance);
+ edge.onPull(distance / (axis == Axis.X ? mView.getWidth() : mView.getHeight()));
+ invalidate();
+ }
+
+ @Override
+ public void draw(final Canvas canvas, final ImmutableViewportMetrics metrics) {
+ if (metrics == null) {
+ return;
+ }
+
+ PointF visibleEnd = mView.getDynamicToolbarAnimator().getVisibleEndOfLayerView();
+
+ // If we're pulling an edge, or fading it out, draw!
+ boolean invalidate = false;
+ if (!mEdges[TOP].isFinished()) {
+ invalidate |= draw(mEdges[TOP], canvas, 0, 0, 0);
+ }
+
+ if (!mEdges[BOTTOM].isFinished()) {
+ invalidate |= draw(mEdges[BOTTOM], canvas, visibleEnd.x, visibleEnd.y, 180);
+ }
+
+ if (!mEdges[LEFT].isFinished()) {
+ invalidate |= draw(mEdges[LEFT], canvas, 0, visibleEnd.y, 270);
+ }
+
+ if (!mEdges[RIGHT].isFinished()) {
+ invalidate |= draw(mEdges[RIGHT], canvas, visibleEnd.x, 0, 90);
+ }
+
+ // If the edge effect is animating off screen, invalidate.
+ if (invalidate) {
+ invalidate();
+ }
+ }
+
+ private static boolean draw(final EdgeEffect edge, final Canvas canvas, final float translateX, final float translateY, final float rotation) {
+ final int state = canvas.save();
+ canvas.translate(translateX, translateY);
+ canvas.rotate(rotation);
+ boolean invalidate = edge.draw(canvas);
+ canvas.restoreToCount(state);
+
+ return invalidate;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomController.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomController.java
new file mode 100644
index 0000000000..fbd07c69bf
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomController.java
@@ -0,0 +1,38 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.EventDispatcher;
+
+import android.graphics.PointF;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+
+public interface PanZoomController {
+ // Threshold for sending touch move events to content
+ public static final float CLICK_THRESHOLD = 1 / 50f * GeckoAppShell.getDpi();
+
+ static class Factory {
+ static PanZoomController create(PanZoomTarget target, View view, EventDispatcher dispatcher) {
+ return new NativePanZoomController(target, view);
+ }
+ }
+
+ public void destroy();
+ public void attach();
+
+ public boolean onTouchEvent(MotionEvent event);
+ public boolean onMotionEvent(MotionEvent event);
+ public void onMotionEventVelocity(final long aEventTime, final float aSpeedY);
+
+ public void setOverscrollHandler(final Overscroll controller);
+
+ public void setIsLongpressEnabled(boolean isLongpressEnabled);
+
+ public ImmutableViewportMetrics adjustScrollForSurfaceShift(ImmutableViewportMetrics aMetrics, PointF aShift);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomTarget.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomTarget.java
new file mode 100644
index 0000000000..0896674fc8
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanZoomTarget.java
@@ -0,0 +1,15 @@
+/* -*- 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.gfx;
+
+import android.graphics.Matrix;
+import android.graphics.PointF;
+
+public interface PanZoomTarget {
+ public void panZoomStopped();
+ public boolean isGeckoReady();
+ public void setScrollingRootContent(boolean isRootContent);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanningPerfAPI.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanningPerfAPI.java
new file mode 100644
index 0000000000..42eb2b88b8
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanningPerfAPI.java
@@ -0,0 +1,73 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PanningPerfAPI {
+ private static final String LOGTAG = "GeckoPanningPerfAPI";
+
+ // make this large enough to avoid having to resize the frame time
+ // list, as that may be expensive and impact the thing we're trying
+ // to measure.
+ private static final int EXPECTED_FRAME_COUNT = 2048;
+
+ private static boolean mRecordingFrames;
+ private static List<Long> mFrameTimes;
+ private static long mFrameStartTime;
+
+ private static void initialiseRecordingArrays() {
+ if (mFrameTimes == null) {
+ mFrameTimes = new ArrayList<Long>(EXPECTED_FRAME_COUNT);
+ } else {
+ mFrameTimes.clear();
+ }
+ }
+
+ @RobocopTarget
+ public static void startFrameTimeRecording() {
+ if (mRecordingFrames) {
+ Log.e(LOGTAG, "Error: startFrameTimeRecording() called while already recording!");
+ return;
+ }
+ mRecordingFrames = true;
+ initialiseRecordingArrays();
+ mFrameStartTime = SystemClock.uptimeMillis();
+ }
+
+ @RobocopTarget
+ public static List<Long> stopFrameTimeRecording() {
+ if (!mRecordingFrames) {
+ Log.e(LOGTAG, "Error: stopFrameTimeRecording() called when not recording!");
+ return null;
+ }
+ mRecordingFrames = false;
+ return mFrameTimes;
+ }
+
+ public static void recordFrameTime() {
+ // this will be called often, so try to make it as quick as possible
+ if (mRecordingFrames) {
+ mFrameTimes.add(SystemClock.uptimeMillis() - mFrameStartTime);
+ }
+ }
+
+ @RobocopTarget
+ public static void startCheckerboardRecording() {
+ throw new UnsupportedOperationException();
+ }
+
+ @RobocopTarget
+ public static List<Float> stopCheckerboardRecording() {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PointUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PointUtils.java
new file mode 100644
index 0000000000..8db329c9fe
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PointUtils.java
@@ -0,0 +1,51 @@
+/* -*- 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.gfx;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.graphics.Point;
+import android.graphics.PointF;
+
+public final class PointUtils {
+ public static PointF add(PointF one, PointF two) {
+ return new PointF(one.x + two.x, one.y + two.y);
+ }
+
+ public static PointF subtract(PointF one, PointF two) {
+ return new PointF(one.x - two.x, one.y - two.y);
+ }
+
+ public static PointF scale(PointF point, float factor) {
+ return new PointF(point.x * factor, point.y * factor);
+ }
+
+ public static Point round(PointF point) {
+ return new Point(Math.round(point.x), Math.round(point.y));
+ }
+
+ /* Computes the magnitude of the given vector. */
+ public static float distance(PointF point) {
+ return (float)Math.sqrt(point.x * point.x + point.y * point.y);
+ }
+
+ /** Computes the scalar distance between two points. */
+ public static float distance(PointF one, PointF two) {
+ return PointF.length(one.x - two.x, one.y - two.y);
+ }
+
+ public static JSONObject toJSON(PointF point) throws JSONException {
+ // Ensure we put ints, not longs, because Gecko message handlers call getInt().
+ int x = Math.round(point.x);
+ int y = Math.round(point.y);
+ JSONObject json = new JSONObject();
+ json.put("x", x);
+ json.put("y", y);
+ return json;
+ }
+}
+
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ProgressiveUpdateData.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ProgressiveUpdateData.java
new file mode 100644
index 0000000000..d961a2569f
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ProgressiveUpdateData.java
@@ -0,0 +1,29 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+/**
+ * This is the data structure that's returned by the progressive tile update
+ * callback function. It encompasses the current viewport and a boolean value
+ * representing whether the front-end is interested in the current progressive
+ * update continuing.
+ */
+@WrapForJNI
+public class ProgressiveUpdateData {
+ public float x;
+ public float y;
+ public float scale;
+ public boolean abort;
+
+ public void setViewport(ImmutableViewportMetrics viewport) {
+ this.x = viewport.viewportRectLeft;
+ this.y = viewport.viewportRectTop;
+ this.scale = viewport.zoomFactor;
+ }
+}
+
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RectUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RectUtils.java
new file mode 100644
index 0000000000..22151db769
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RectUtils.java
@@ -0,0 +1,126 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.util.FloatUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+public final class RectUtils {
+ private RectUtils() {}
+
+ public static Rect create(JSONObject json) {
+ try {
+ int x = json.getInt("x");
+ int y = json.getInt("y");
+ int width = json.getInt("width");
+ int height = json.getInt("height");
+ return new Rect(x, y, x + width, y + height);
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static String toJSON(RectF rect) {
+ StringBuilder sb = new StringBuilder(256);
+ sb.append("{ \"left\": ").append(rect.left)
+ .append(", \"top\": ").append(rect.top)
+ .append(", \"right\": ").append(rect.right)
+ .append(", \"bottom\": ").append(rect.bottom)
+ .append('}');
+ return sb.toString();
+ }
+
+ public static RectF expand(RectF rect, float moreWidth, float moreHeight) {
+ float halfMoreWidth = moreWidth / 2;
+ float halfMoreHeight = moreHeight / 2;
+ return new RectF(rect.left - halfMoreWidth,
+ rect.top - halfMoreHeight,
+ rect.right + halfMoreWidth,
+ rect.bottom + halfMoreHeight);
+ }
+
+ public static RectF contract(RectF rect, float lessWidth, float lessHeight) {
+ float halfLessWidth = lessWidth / 2.0f;
+ float halfLessHeight = lessHeight / 2.0f;
+ return new RectF(rect.left + halfLessWidth,
+ rect.top + halfLessHeight,
+ rect.right - halfLessWidth,
+ rect.bottom - halfLessHeight);
+ }
+
+ public static RectF intersect(RectF one, RectF two) {
+ float left = Math.max(one.left, two.left);
+ float top = Math.max(one.top, two.top);
+ float right = Math.min(one.right, two.right);
+ float bottom = Math.min(one.bottom, two.bottom);
+ return new RectF(left, top, Math.max(right, left), Math.max(bottom, top));
+ }
+
+ public static RectF scale(RectF rect, float scale) {
+ float x = rect.left * scale;
+ float y = rect.top * scale;
+ return new RectF(x, y,
+ x + (rect.width() * scale),
+ y + (rect.height() * scale));
+ }
+
+ public static RectF scaleAndRound(RectF rect, float scale) {
+ float left = rect.left * scale;
+ float top = rect.top * scale;
+ return new RectF(Math.round(left),
+ Math.round(top),
+ Math.round(left + (rect.width() * scale)),
+ Math.round(top + (rect.height() * scale)));
+ }
+
+ /** Returns the nearest integer rect of the given rect. */
+ public static Rect round(RectF rect) {
+ Rect r = new Rect();
+ round(rect, r);
+ return r;
+ }
+
+ public static void round(RectF rect, Rect dest) {
+ dest.set(Math.round(rect.left), Math.round(rect.top),
+ Math.round(rect.right), Math.round(rect.bottom));
+ }
+
+ public static Rect roundIn(RectF rect) {
+ return new Rect((int)Math.ceil(rect.left), (int)Math.ceil(rect.top),
+ (int)Math.floor(rect.right), (int)Math.floor(rect.bottom));
+ }
+
+ public static IntSize getSize(Rect rect) {
+ return new IntSize(rect.width(), rect.height());
+ }
+
+ public static Point getOrigin(Rect rect) {
+ return new Point(rect.left, rect.top);
+ }
+
+ public static PointF getOrigin(RectF rect) {
+ return new PointF(rect.left, rect.top);
+ }
+
+ public static boolean fuzzyEquals(RectF a, RectF b) {
+ if (a == null && b == null)
+ return true;
+ else if ((a == null && b != null) || (a != null && b == null))
+ return false;
+ else
+ return FloatUtils.fuzzyEquals(a.top, b.top)
+ && FloatUtils.fuzzyEquals(a.left, b.left)
+ && FloatUtils.fuzzyEquals(a.right, b.right)
+ && FloatUtils.fuzzyEquals(a.bottom, b.bottom);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RenderTask.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RenderTask.java
new file mode 100644
index 0000000000..80cbf77f0a
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RenderTask.java
@@ -0,0 +1,80 @@
+/* -*- 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.gfx;
+
+/**
+ * A class used to schedule a callback to occur when the next frame is drawn.
+ * Subclasses must redefine the internalRun method, not the run method.
+ */
+public abstract class RenderTask {
+ /**
+ * Whether to run the task after the render, or before.
+ */
+ public final boolean runAfter;
+
+ /**
+ * Time when this task has first run, in ns. Useful for tasks which run for a specific duration.
+ */
+ private long mStartTime;
+
+ /**
+ * Whether we should initialise mStartTime on the next frame run.
+ */
+ private boolean mResetStartTime = true;
+
+ /**
+ * The callback to run on each frame. timeDelta is the time elapsed since
+ * the last call, in nanoseconds. Returns true if it should continue
+ * running, or false if it should be removed from the task queue. Returning
+ * true implicitly schedules a redraw.
+ *
+ * This method first initializes the start time if resetStartTime has been invoked,
+ * then calls internalRun.
+ *
+ * Note : subclasses should override internalRun.
+ *
+ * @param timeDelta the time between the beginning of last frame and the beginning of this frame, in ns.
+ * @param currentFrameStartTime the startTime of the current frame, in ns.
+ * @return true if animation should be run at the next frame, false otherwise
+ * @see RenderTask#internalRun(long, long)
+ */
+ public final boolean run(long timeDelta, long currentFrameStartTime) {
+ if (mResetStartTime) {
+ mStartTime = currentFrameStartTime;
+ mResetStartTime = false;
+ }
+ return internalRun(timeDelta, currentFrameStartTime);
+ }
+
+ /**
+ * Abstract method to be overridden by subclasses.
+ * @param timeDelta the time between the beginning of last frame and the beginning of this frame, in ns
+ * @param currentFrameStartTime the startTime of the current frame, in ns.
+ * @return true if animation should be run at the next frame, false otherwise
+ */
+ protected abstract boolean internalRun(long timeDelta, long currentFrameStartTime);
+
+ public RenderTask(boolean aRunAfter) {
+ runAfter = aRunAfter;
+ }
+
+ /**
+ * Get the start time of this task.
+ * It is the start time of the first frame this task was run on.
+ * @return the start time in ns
+ */
+ public long getStartTime() {
+ return mStartTime;
+ }
+
+ /**
+ * Schedule a reset of the recorded start time next time {@link RenderTask#run(long, long)} is run.
+ * @see RenderTask#getStartTime()
+ */
+ public void resetStartTime() {
+ mResetStartTime = true;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/StackScroller.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/StackScroller.java
new file mode 100644
index 0000000000..293268cba1
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/StackScroller.java
@@ -0,0 +1,695 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.mozilla.gecko.gfx;
+
+import android.content.Context;
+import android.hardware.SensorManager;
+import android.util.Log;
+import android.view.ViewConfiguration;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+/**
+ * This class is vastly copied from {@link android.widget.OverScroller} but decouples the time
+ * from the app time so it can be specified manually.
+ */
+@WrapForJNI(exceptionMode = "nsresult")
+public class StackScroller {
+ private int mMode;
+
+ private final SplineStackScroller mScrollerX;
+ private final SplineStackScroller mScrollerY;
+
+ private final boolean mFlywheel;
+
+ private static final int SCROLL_MODE = 0;
+ private static final int FLING_MODE = 1;
+
+ private static float sViscousFluidScale;
+ private static float sViscousFluidNormalize;
+
+ /**
+ * Creates an StackScroller with a viscous fluid scroll interpolator and flywheel.
+ * @param context
+ */
+ public StackScroller(Context context) {
+ mFlywheel = true;
+ mScrollerX = new SplineStackScroller(context);
+ mScrollerY = new SplineStackScroller(context);
+ initContants();
+ }
+
+ private static void initContants() {
+ // This controls the viscous fluid effect (how much of it)
+ sViscousFluidScale = 8.0f;
+ // must be set to 1.0 (used in viscousFluid())
+ sViscousFluidNormalize = 1.0f;
+ sViscousFluidNormalize = 1.0f / viscousFluid(1.0f);
+ }
+
+ /**
+ *
+ * Returns whether the scroller has finished scrolling.
+ *
+ * @return True if the scroller has finished scrolling, false otherwise.
+ */
+ public final boolean isFinished() {
+ return mScrollerX.mFinished && mScrollerY.mFinished;
+ }
+
+ /**
+ * Force the finished field to a particular value. Contrary to
+ * {@link #abortAnimation()}, forcing the animation to finished
+ * does NOT cause the scroller to move to the final x and y
+ * position.
+ *
+ * @param finished The new finished value.
+ */
+ public final void forceFinished(boolean finished) {
+ mScrollerX.mFinished = mScrollerY.mFinished = finished;
+ }
+
+ /**
+ * Returns the current X offset in the scroll.
+ *
+ * @return The new X offset as an absolute distance from the origin.
+ */
+ public final int getCurrX() {
+ return mScrollerX.mCurrentPosition;
+ }
+
+ /**
+ * Returns the current Y offset in the scroll.
+ *
+ * @return The new Y offset as an absolute distance from the origin.
+ */
+ public final int getCurrY() {
+ return mScrollerY.mCurrentPosition;
+ }
+
+ /**
+ * Returns where the scroll will end. Valid only for "fling" scrolls.
+ *
+ * @return The final X offset as an absolute distance from the origin.
+ */
+ public final int getFinalX() {
+ return mScrollerX.mFinal;
+ }
+
+ public final float getCurrSpeedX() {
+ return mScrollerX.mCurrVelocity;
+ }
+
+ public final float getCurrSpeedY() {
+ return mScrollerY.mCurrVelocity;
+ }
+
+ /**
+ * Returns where the scroll will end. Valid only for "fling" scrolls.
+ *
+ * @return The final Y offset as an absolute distance from the origin.
+ */
+ public final int getFinalY() {
+ return mScrollerY.mFinal;
+ }
+
+ /**
+ * Sets where the scroll will end. Valid only for "fling" scrolls.
+ *
+ * @param x The final X offset as an absolute distance from the origin.
+ */
+ public final void setFinalX(int x) {
+ mScrollerX.setFinalPosition(x);
+ }
+
+ private static float viscousFluid(float x) {
+ x *= sViscousFluidScale;
+ if (x < 1.0f) {
+ x -= (1.0f - (float) Math.exp(-x));
+ } else {
+ float start = 0.36787944117f; // 1/e == exp(-1)
+ x = 1.0f - (float) Math.exp(1.0f - x);
+ x = start + x * (1.0f - start);
+ }
+ x *= sViscousFluidNormalize;
+ return x;
+ }
+
+ /**
+ * Call this when you want to know the new location. If it returns true, the
+ * animation is not yet finished.
+ */
+ public boolean computeScrollOffset(long time) {
+ if (isFinished()) {
+ return false;
+ }
+
+ switch (mMode) {
+ case SCROLL_MODE:
+ // Any scroller can be used for time, since they were started
+ // together in scroll mode. We use X here.
+ final long elapsedTime = time - mScrollerX.mStartTime;
+
+ final int duration = mScrollerX.mDuration;
+ if (elapsedTime < duration) {
+ float q = (float) (elapsedTime) / duration;
+ q = viscousFluid(q);
+ mScrollerX.updateScroll(q);
+ mScrollerY.updateScroll(q);
+ } else {
+ abortAnimation();
+ }
+ break;
+
+ case FLING_MODE:
+ if (!mScrollerX.mFinished) {
+ if (!mScrollerX.update(time)) {
+ if (!mScrollerX.continueWhenFinished(time)) {
+ mScrollerX.finish();
+ }
+ }
+ }
+
+ if (!mScrollerY.mFinished) {
+ if (!mScrollerY.update(time)) {
+ if (!mScrollerY.continueWhenFinished(time)) {
+ mScrollerY.finish();
+ }
+ }
+ }
+
+ break;
+
+ default:
+ break;
+ }
+
+ return true;
+ }
+
+ /**
+ * Start scrolling by providing a starting point and the distance to travel.
+ *
+ * @param startX Starting horizontal scroll offset in pixels. Positive
+ * numbers will scroll the content to the left.
+ * @param startY Starting vertical scroll offset in pixels. Positive numbers
+ * will scroll the content up.
+ * @param dx Horizontal distance to travel. Positive numbers will scroll the
+ * content to the left.
+ * @param dy Vertical distance to travel. Positive numbers will scroll the
+ * content up.
+ * @param duration Duration of the scroll in milliseconds.
+ */
+ public void startScroll(int startX, int startY, int dx, int dy, long startTime, int duration) {
+ mMode = SCROLL_MODE;
+ mScrollerX.startScroll(startX, dx, startTime, duration);
+ mScrollerY.startScroll(startY, dy, startTime, duration);
+ }
+
+ /**
+ * Call this when you want to 'spring back' into a valid coordinate range.
+ *
+ * @param startX Starting X coordinate
+ * @param startY Starting Y coordinate
+ * @param minX Minimum valid X value
+ * @param maxX Maximum valid X value
+ * @param minY Minimum valid Y value
+ * @param maxY Minimum valid Y value
+ * @return true if a springback was initiated, false if startX and startY were
+ * already within the valid range.
+ */
+ public boolean springBack(
+ int startX, int startY, int minX, int maxX, int minY, int maxY, long time) {
+ mMode = FLING_MODE;
+
+ // Make sure both methods are called.
+ final boolean spingbackX = mScrollerX.springback(startX, minX, maxX, time);
+ final boolean spingbackY = mScrollerY.springback(startY, minY, maxY, time);
+ return spingbackX || spingbackY;
+ }
+
+ /**
+ * Start scrolling based on a fling gesture. The distance traveled will
+ * depend on the initial velocity of the fling.
+ *
+ * @param startX Starting point of the scroll (X)
+ * @param startY Starting point of the scroll (Y)
+ * @param velocityX Initial velocity of the fling (X) measured in pixels per second.
+ * @param velocityY Initial velocity of the fling (Y) measured in pixels per second
+ * @param minX Minimum X value. The scroller will not scroll past this point
+ * unless overX > 0. If overfling is allowed, it will use minX as
+ * a springback boundary.
+ * @param maxX Maximum X value. The scroller will not scroll past this point
+ * unless overX > 0. If overfling is allowed, it will use maxX as
+ * a springback boundary.
+ * @param minY Minimum Y value. The scroller will not scroll past this point
+ * unless overY > 0. If overfling is allowed, it will use minY as
+ * a springback boundary.
+ * @param maxY Maximum Y value. The scroller will not scroll past this point
+ * unless overY > 0. If overfling is allowed, it will use maxY as
+ * a springback boundary.
+ * @param overX Overfling range. If > 0, horizontal overfling in either
+ * direction will be possible.
+ * @param overY Overfling range. If > 0, vertical overfling in either
+ * direction will be possible.
+ */
+ public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX,
+ int minY, int maxY, int overX, int overY, long time) {
+ // Continue a scroll or fling in progress
+ if (mFlywheel && !isFinished()) {
+ float oldVelocityX = mScrollerX.mCurrVelocity;
+ float oldVelocityY = mScrollerY.mCurrVelocity;
+ boolean sameXDirection = (velocityX == 0) || (oldVelocityX == 0) ||
+ ((velocityX < 0) == (oldVelocityX < 0));
+ boolean sameYDirection = (velocityY == 0) || (oldVelocityY == 0) ||
+ ((velocityY < 0) == (oldVelocityY < 0));
+ if (sameXDirection) {
+ velocityX += oldVelocityX;
+ }
+ if (sameYDirection) {
+ velocityY += oldVelocityY;
+ }
+ }
+
+ mMode = FLING_MODE;
+ mScrollerX.fling(startX, velocityX, minX, maxX, overX, time);
+ mScrollerY.fling(startY, velocityY, minY, maxY, overY, time);
+ }
+
+ /**
+ * Stops the animation. Contrary to {@link #forceFinished(boolean)},
+ * aborting the animating causes the scroller to move to the final x and y
+ * positions.
+ *
+ * @see #forceFinished(boolean)
+ */
+ public void abortAnimation() {
+ mScrollerX.finish();
+ mScrollerY.finish();
+ }
+
+ static class SplineStackScroller {
+ // Initial position
+ private int mStart;
+
+ // Current position
+ private int mCurrentPosition;
+
+ // Final position
+ private int mFinal;
+
+ // Initial velocity
+ private int mVelocity;
+
+ // Current velocity
+ private float mCurrVelocity;
+
+ // Constant current deceleration
+ private float mDeceleration;
+
+ // Animation starting time, in system milliseconds
+ private long mStartTime;
+
+ // Animation duration, in milliseconds
+ private int mDuration;
+
+ // Duration to complete spline component of animation
+ private int mSplineDuration;
+
+ // Distance to travel along spline animation
+ private int mSplineDistance;
+
+ // Whether the animation is currently in progress
+ private boolean mFinished;
+
+ // The allowed overshot distance before boundary is reached.
+ private int mOver;
+
+ // Fling friction
+ private final float mFlingFriction = ViewConfiguration.getScrollFriction();
+
+ // Current state of the animation.
+ private int mState = SPLINE;
+
+ // Constant gravity value, used in the deceleration phase.
+ private static final float GRAVITY = 2000.0f;
+
+ // A context-specific coefficient adjusted to physical values.
+ private final float mPhysicalCoeff;
+
+ private static final float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
+ private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
+ private static final float START_TENSION = 0.5f;
+ private static final float END_TENSION = 1.0f;
+ private static final float P1 = START_TENSION * INFLEXION;
+ private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);
+
+ private static final int NB_SAMPLES = 100;
+ private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
+ private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];
+
+ private static final int SPLINE = 0;
+ private static final int CUBIC = 1;
+ private static final int BALLISTIC = 2;
+
+ static {
+ float xMin = 0.0f;
+ float yMin = 0.0f;
+ for (int i = 0; i < NB_SAMPLES; i++) {
+ final float alpha = (float) i / NB_SAMPLES;
+
+ float xMax = 1.0f;
+ float x, tx, coef;
+ while (true) {
+ x = xMin + (xMax - xMin) / 2.0f;
+ coef = 3.0f * x * (1.0f - x);
+ tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
+ if (Math.abs(tx - alpha) < 1E-5) break;
+ if (tx > alpha) {
+ xMax = x;
+ } else {
+ xMin = x;
+ }
+ }
+ SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;
+
+ float yMax = 1.0f;
+ float y, dy;
+ while (true) {
+ y = yMin + (yMax - yMin) / 2.0f;
+ coef = 3.0f * y * (1.0f - y);
+ dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
+ if (Math.abs(dy - alpha) < 1E-5) break;
+ if (dy > alpha) {
+ yMax = y;
+ } else {
+ yMin = y;
+ }
+ }
+ SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
+ }
+ SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
+ }
+
+ SplineStackScroller(Context context) {
+ mFinished = true;
+ final float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
+ mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2)
+ * 39.37f // inch/meter
+ * ppi * 0.84f; // look and feel tuning
+ }
+
+ void updateScroll(float q) {
+ mCurrentPosition = mStart + Math.round(q * (mFinal - mStart));
+ }
+
+ /*
+ * Get a signed deceleration that will reduce the velocity.
+ */
+ private static float getDeceleration(int velocity) {
+ return velocity > 0 ? -GRAVITY : GRAVITY;
+ }
+
+ /*
+ * Modifies mDuration to the duration it takes to get from start to newFinal using the
+ * spline interpolation. The previous duration was needed to get to oldFinal.
+ */
+ private void adjustDuration(int start, int oldFinal, int newFinal) {
+ final int oldDistance = oldFinal - start;
+ final int newDistance = newFinal - start;
+ final float x = Math.abs((float) newDistance / oldDistance);
+ final int index = (int) (NB_SAMPLES * x);
+ if (index < NB_SAMPLES) {
+ final float xInf = (float) index / NB_SAMPLES;
+ final float xSup = (float) (index + 1) / NB_SAMPLES;
+ final float tInf = SPLINE_TIME[index];
+ final float tSup = SPLINE_TIME[index + 1];
+ final float timeCoef = tInf + (x - xInf) / (xSup - xInf) * (tSup - tInf);
+ mDuration *= timeCoef;
+ }
+ }
+
+ void startScroll(int start, int distance, long startTime, int duration) {
+ mFinished = false;
+
+ mStart = start;
+ mFinal = start + distance;
+
+ mStartTime = startTime;
+ mDuration = duration;
+
+ // Unused
+ mDeceleration = 0.0f;
+ mVelocity = 0;
+ }
+
+ void finish() {
+ mCurrentPosition = mFinal;
+ // Not reset since WebView relies on this value for fast fling.
+ // TODO: restore when WebView uses the fast fling implemented in this class.
+ // mCurrVelocity = 0.0f;
+ mFinished = true;
+ }
+
+ void setFinalPosition(int position) {
+ mFinal = position;
+ mFinished = false;
+ }
+
+ boolean springback(int start, int min, int max, long time) {
+ mFinished = true;
+
+ mStart = mFinal = start;
+ mVelocity = 0;
+
+ mStartTime = time;
+ mDuration = 0;
+
+ if (start < min) {
+ startSpringback(start, min, 0);
+ } else if (start > max) {
+ startSpringback(start, max, 0);
+ }
+
+ return !mFinished;
+ }
+
+ private void startSpringback(int start, int end, int velocity) {
+ // mStartTime has been set
+ mFinished = false;
+ mState = CUBIC;
+ mStart = start;
+ mFinal = end;
+ final int delta = start - end;
+ mDeceleration = getDeceleration(delta);
+ // TODO take velocity into account
+ mVelocity = -delta; // only sign is used
+ mOver = Math.abs(delta);
+ mDuration = (int) (1000.0 * Math.sqrt(-2.0 * delta / mDeceleration));
+ }
+
+ void fling(int start, int velocity, int min, int max, int over, long time) {
+ mOver = over;
+ mFinished = false;
+ mCurrVelocity = mVelocity = velocity;
+ mDuration = mSplineDuration = 0;
+ mStartTime = time;
+ mCurrentPosition = mStart = start;
+
+ if (start > max || start < min) {
+ startAfterEdge(start, min, max, velocity, time);
+ return;
+ }
+
+ mState = SPLINE;
+ double totalDistance = 0.0;
+
+ if (velocity != 0) {
+ mDuration = mSplineDuration = getSplineFlingDuration(velocity);
+ totalDistance = getSplineFlingDistance(velocity);
+ }
+
+ mSplineDistance = (int) (totalDistance * Math.signum(velocity));
+ mFinal = start + mSplineDistance;
+
+ // Clamp to a valid final position
+ if (mFinal < min) {
+ adjustDuration(mStart, mFinal, min);
+ mFinal = min;
+ }
+
+ if (mFinal > max) {
+ adjustDuration(mStart, mFinal, max);
+ mFinal = max;
+ }
+ }
+
+ private double getSplineDeceleration(int velocity) {
+ return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
+ }
+
+ private double getSplineFlingDistance(int velocity) {
+ final double l = getSplineDeceleration(velocity);
+ final double decelMinusOne = DECELERATION_RATE - 1.0;
+ return mFlingFriction * mPhysicalCoeff
+ * Math.exp(DECELERATION_RATE / decelMinusOne * l);
+ }
+
+ /* Returns the duration, expressed in milliseconds */
+ private int getSplineFlingDuration(int velocity) {
+ final double l = getSplineDeceleration(velocity);
+ final double decelMinusOne = DECELERATION_RATE - 1.0;
+ return (int) (1000.0 * Math.exp(l / decelMinusOne));
+ }
+
+ private void fitOnBounceCurve(int start, int end, int velocity) {
+ // Simulate a bounce that started from edge
+ final float durationToApex = -velocity / mDeceleration;
+ final float distanceToApex = velocity * velocity / 2.0f / Math.abs(mDeceleration);
+ final float distanceToEdge = Math.abs(end - start);
+ final float totalDuration = (float) Math.sqrt(
+ 2.0 * (distanceToApex + distanceToEdge) / Math.abs(mDeceleration));
+ mStartTime -= (int) (1000.0f * (totalDuration - durationToApex));
+ mStart = end;
+ mVelocity = (int) (-mDeceleration * totalDuration);
+ }
+
+ private void startBounceAfterEdge(int start, int end, int velocity) {
+ mDeceleration = getDeceleration(velocity == 0 ? start - end : velocity);
+ fitOnBounceCurve(start, end, velocity);
+ onEdgeReached();
+ }
+
+ private void startAfterEdge(int start, int min, int max, int velocity, long time) {
+ if (start > min && start < max) {
+ Log.e("StackScroller", "startAfterEdge called from a valid position");
+ mFinished = true;
+ return;
+ }
+ final boolean positive = start > max;
+ final int edge = positive ? max : min;
+ final int overDistance = start - edge;
+ boolean keepIncreasing = overDistance * velocity >= 0;
+ if (keepIncreasing) {
+ // Will result in a bounce or a to_boundary depending on velocity.
+ startBounceAfterEdge(start, edge, velocity);
+ } else {
+ final double totalDistance = getSplineFlingDistance(velocity);
+ if (totalDistance > Math.abs(overDistance)) {
+ fling(start, velocity, positive ? min : start, positive ? start : max, mOver,
+ time);
+ } else {
+ startSpringback(start, edge, velocity);
+ }
+ }
+ }
+
+ private void onEdgeReached() {
+ // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached.
+ float distance = mVelocity * mVelocity / (2.0f * Math.abs(mDeceleration));
+ final float sign = Math.signum(mVelocity);
+
+ if (distance > mOver) {
+ // Default deceleration is not sufficient to slow us down before boundary
+ mDeceleration = -sign * mVelocity * mVelocity / (2.0f * mOver);
+ distance = mOver;
+ }
+
+ mOver = (int) distance;
+ mState = BALLISTIC;
+ mFinal = mStart + (int) (mVelocity > 0 ? distance : -distance);
+ mDuration = -(int) (1000.0f * mVelocity / mDeceleration);
+ }
+
+ boolean continueWhenFinished(long time) {
+ switch (mState) {
+ case SPLINE:
+ // Duration from start to null velocity
+ if (mDuration < mSplineDuration) {
+ // If the animation was clamped, we reached the edge
+ mStart = mFinal;
+ // TODO Better compute speed when edge was reached
+ mVelocity = (int) mCurrVelocity;
+ mDeceleration = getDeceleration(mVelocity);
+ mStartTime += mDuration;
+ onEdgeReached();
+ } else {
+ // Normal stop, no need to continue
+ return false;
+ }
+ break;
+ case BALLISTIC:
+ mStartTime += mDuration;
+ startSpringback(mFinal, mStart, 0);
+ break;
+ case CUBIC:
+ return false;
+ }
+
+ update(time);
+ return true;
+ }
+
+ /*
+ * Update the current position and velocity for current time. Returns
+ * true if update has been done and false if animation duration has been
+ * reached.
+ */
+ boolean update(long time) {
+ final long currentTime = time - mStartTime;
+
+ if (((mState == SPLINE) && (mSplineDuration <= 0)) ||
+ ((mState == CUBIC) && (mDuration <= 0))) {
+ return false;
+ }
+
+ if (currentTime > mDuration) {
+ return false;
+ }
+
+ double distance = 0.0;
+ switch (mState) {
+ case SPLINE: {
+ final float t = (float) currentTime / mSplineDuration;
+ final int index = (int) (NB_SAMPLES * t);
+ float distanceCoef = 1.f;
+ float velocityCoef = 0.f;
+ if (index < NB_SAMPLES) {
+ final float tInf = (float) index / NB_SAMPLES;
+ final float tSup = (float) (index + 1) / NB_SAMPLES;
+ final float dInf = SPLINE_POSITION[index];
+ final float dSup = SPLINE_POSITION[index + 1];
+ velocityCoef = (dSup - dInf) / (tSup - tInf);
+ distanceCoef = dInf + (t - tInf) * velocityCoef;
+ }
+
+ distance = distanceCoef * mSplineDistance;
+ mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f;
+ break;
+ }
+
+ case BALLISTIC: {
+ final float t = currentTime / 1000.0f;
+ mCurrVelocity = mVelocity + mDeceleration * t;
+ distance = mVelocity * t + mDeceleration * t * t / 2.0f;
+ break;
+ }
+
+ case CUBIC: {
+ final float t = (float) (currentTime) / mDuration;
+ final float t2 = t * t;
+ final float sign = Math.signum(mVelocity);
+ distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2);
+ mCurrVelocity = sign * mOver * 6.0f * (-t + t2);
+ break;
+ }
+ }
+
+ mCurrentPosition = mStart + (int) Math.round(distance);
+
+ return true;
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceTextureListener.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceTextureListener.java
new file mode 100644
index 0000000000..560674e4fb
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceTextureListener.java
@@ -0,0 +1,38 @@
+/* -*- 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.mozglue.JNIObject;
+
+import android.graphics.SurfaceTexture;
+
+final class SurfaceTextureListener
+ extends JNIObject implements SurfaceTexture.OnFrameAvailableListener
+{
+ @WrapForJNI(calledFrom = "gecko")
+ private SurfaceTextureListener() {
+ }
+
+ @Override
+ protected void disposeNative() {
+ // SurfaceTextureListener is disposed inside AndroidSurfaceTexture.
+ throw new IllegalStateException("unreachable code");
+ }
+
+ @WrapForJNI(stubName = "OnFrameAvailable")
+ private native void nativeOnFrameAvailable();
+
+ @Override // SurfaceTexture.OnFrameAvailableListener
+ public void onFrameAvailable(SurfaceTexture surfaceTexture) {
+ try {
+ nativeOnFrameAvailable();
+ } catch (final NullPointerException e) {
+ // Ignore exceptions caused by a disposed object, i.e.
+ // getting a callback after this listener is no longer in use.
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ViewTransform.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ViewTransform.java
new file mode 100644
index 0000000000..e6685f0668
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/ViewTransform.java
@@ -0,0 +1,28 @@
+/* -*- 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.gfx;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+@WrapForJNI
+public class ViewTransform {
+ public float x;
+ public float y;
+ public float width;
+ public float height;
+ public float scale;
+ public float fixedLayerMarginLeft;
+ public float fixedLayerMarginTop;
+ public float fixedLayerMarginRight;
+ public float fixedLayerMarginBottom;
+
+ public ViewTransform(float inX, float inY, float inScale) {
+ x = inX;
+ y = inY;
+ scale = inScale;
+ }
+}
+
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/ByteBufferInputStream.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/ByteBufferInputStream.java
new file mode 100644
index 0000000000..bc9e0a143c
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/ByteBufferInputStream.java
@@ -0,0 +1,64 @@
+/* -*- 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.mozglue;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+class ByteBufferInputStream extends InputStream {
+
+ protected ByteBuffer mBuf;
+ // Reference to a native object holding the data backing the ByteBuffer.
+ private final NativeReference mNativeRef;
+
+ protected ByteBufferInputStream(ByteBuffer buffer, NativeReference ref) {
+ mBuf = buffer;
+ mNativeRef = ref;
+ }
+
+ @Override
+ public int available() {
+ return mBuf.remaining();
+ }
+
+ @Override
+ public void close() {
+ // Do nothing, we need to keep the native references around for child
+ // buffers.
+ }
+
+ @Override
+ public int read() {
+ if (!mBuf.hasRemaining() || mNativeRef.isReleased()) {
+ return -1;
+ }
+
+ return mBuf.get() & 0xff; // Avoid sign extension
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int length) {
+ if (!mBuf.hasRemaining() || mNativeRef.isReleased()) {
+ return -1;
+ }
+
+ length = Math.min(length, mBuf.remaining());
+ mBuf.get(buffer, offset, length);
+ return length;
+ }
+
+ @Override
+ public long skip(long byteCount) {
+ if (byteCount < 0 || mNativeRef.isReleased()) {
+ return 0;
+ }
+
+ byteCount = Math.min(byteCount, mBuf.remaining());
+ mBuf.position(mBuf.position() + (int)byteCount);
+ return byteCount;
+ }
+
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/DirectBufferAllocator.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/DirectBufferAllocator.java
new file mode 100644
index 0000000000..b3fb24291e
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/DirectBufferAllocator.java
@@ -0,0 +1,52 @@
+/* -*- 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.mozglue;
+
+import java.nio.ByteBuffer;
+
+//
+// We must manually allocate direct buffers in JNI to work around a bug where Honeycomb's
+// ByteBuffer.allocateDirect() grossly overallocates the direct buffer size.
+// https://code.google.com/p/android/issues/detail?id=16941
+//
+
+public final class DirectBufferAllocator {
+ private DirectBufferAllocator() {}
+
+ public static ByteBuffer allocate(int size) {
+ if (size <= 0) {
+ throw new IllegalArgumentException("Invalid size " + size);
+ }
+
+ ByteBuffer directBuffer = nativeAllocateDirectBuffer(size);
+ if (directBuffer == null) {
+ throw new OutOfMemoryError("allocateDirectBuffer() returned null");
+ }
+
+ if (!directBuffer.isDirect()) {
+ throw new AssertionError("allocateDirectBuffer() did not return a direct buffer");
+ }
+
+ return directBuffer;
+ }
+
+ public static ByteBuffer free(ByteBuffer buffer) {
+ if (buffer == null) {
+ return null;
+ }
+
+ if (!buffer.isDirect()) {
+ throw new IllegalArgumentException("buffer must be direct");
+ }
+
+ nativeFreeDirectBuffer(buffer);
+ return null;
+ }
+
+ // These JNI methods are implemented in mozglue/android/nsGeckoUtils.cpp.
+ private static native ByteBuffer nativeAllocateDirectBuffer(long size);
+ private static native void nativeFreeDirectBuffer(ByteBuffer buf);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/GeckoLoader.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/GeckoLoader.java
new file mode 100644
index 0000000000..0bef2435bb
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/GeckoLoader.java
@@ -0,0 +1,549 @@
+/* -*- 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.mozglue;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+import java.util.Locale;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Environment;
+import android.util.Log;
+
+import org.mozilla.gecko.annotation.JNITarget;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.AppConstants;
+
+public final class GeckoLoader {
+ private static final String LOGTAG = "GeckoLoader";
+
+ private static volatile SafeIntent sIntent;
+ private static File sCacheFile;
+ private static File sGREDir;
+
+ /* Synchronized on GeckoLoader.class. */
+ private static boolean sSQLiteLibsLoaded;
+ private static boolean sNSSLibsLoaded;
+ private static boolean sMozGlueLoaded;
+
+ private GeckoLoader() {
+ // prevent instantiation
+ }
+
+ public static File getCacheDir(Context context) {
+ if (sCacheFile == null) {
+ sCacheFile = context.getCacheDir();
+ }
+ return sCacheFile;
+ }
+
+ public static File getGREDir(Context context) {
+ if (sGREDir == null) {
+ sGREDir = new File(context.getApplicationInfo().dataDir);
+ }
+ return sGREDir;
+ }
+
+ private static void setupPluginEnvironment(Context context, String[] pluginDirs) {
+ // setup plugin path directories
+ try {
+ // Check to see if plugins were blocked.
+ if (pluginDirs == null) {
+ putenv("MOZ_PLUGINS_BLOCKED=1");
+ putenv("MOZ_PLUGIN_PATH=");
+ return;
+ }
+
+ StringBuilder pluginSearchPath = new StringBuilder();
+ for (int i = 0; i < pluginDirs.length; i++) {
+ pluginSearchPath.append(pluginDirs[i]);
+ pluginSearchPath.append(":");
+ }
+ putenv("MOZ_PLUGIN_PATH=" + pluginSearchPath);
+
+ File pluginDataDir = context.getDir("plugins", 0);
+ putenv("ANDROID_PLUGIN_DATADIR=" + pluginDataDir.getPath());
+
+ File pluginPrivateDataDir = context.getDir("plugins_private", 0);
+ putenv("ANDROID_PLUGIN_DATADIR_PRIVATE=" + pluginPrivateDataDir.getPath());
+
+ } catch (Exception ex) {
+ Log.w(LOGTAG, "Caught exception getting plugin dirs.", ex);
+ }
+ }
+
+ private static void setupDownloadEnvironment(final Context context) {
+ try {
+ File downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+ File updatesDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
+ if (downloadDir == null) {
+ downloadDir = new File(Environment.getExternalStorageDirectory().getPath(), "download");
+ }
+ if (updatesDir == null) {
+ updatesDir = downloadDir;
+ }
+ putenv("DOWNLOADS_DIRECTORY=" + downloadDir.getPath());
+ putenv("UPDATES_DIRECTORY=" + updatesDir.getPath());
+ } catch (Exception e) {
+ Log.w(LOGTAG, "No download directory found.", e);
+ }
+ }
+
+ private static void delTree(File file) {
+ if (file.isDirectory()) {
+ File children[] = file.listFiles();
+ for (File child : children) {
+ delTree(child);
+ }
+ }
+ file.delete();
+ }
+
+ private static File getTmpDir(Context context) {
+ File tmpDir = context.getDir("tmpdir", Context.MODE_PRIVATE);
+ // check if the old tmp dir is there
+ File oldDir = new File(tmpDir.getParentFile(), "app_tmp");
+ if (oldDir.exists()) {
+ delTree(oldDir);
+ }
+ return tmpDir;
+ }
+
+ public static void setLastIntent(SafeIntent intent) {
+ sIntent = intent;
+ }
+
+ public static void setupGeckoEnvironment(Context context, String[] pluginDirs, String profilePath) {
+ // if we have an intent (we're being launched by an activity)
+ // read in any environmental variables from it here
+ final SafeIntent intent = sIntent;
+ if (intent != null) {
+ String env = intent.getStringExtra("env0");
+ Log.d(LOGTAG, "Gecko environment env0: " + env);
+ for (int c = 1; env != null; c++) {
+ putenv(env);
+ env = intent.getStringExtra("env" + c);
+ Log.d(LOGTAG, "env" + c + ": " + env);
+ }
+ }
+
+ putenv("MOZ_ANDROID_PACKAGE_NAME=" + context.getPackageName());
+
+ setupPluginEnvironment(context, pluginDirs);
+ setupDownloadEnvironment(context);
+
+ // profile home path
+ putenv("HOME=" + profilePath);
+
+ // setup the tmp path
+ File f = getTmpDir(context);
+ if (!f.exists()) {
+ f.mkdirs();
+ }
+ putenv("TMPDIR=" + f.getPath());
+
+ // setup the downloads path
+ f = Environment.getDownloadCacheDirectory();
+ putenv("EXTERNAL_STORAGE=" + f.getPath());
+
+ // setup the app-specific cache path
+ f = context.getCacheDir();
+ putenv("CACHE_DIRECTORY=" + f.getPath());
+
+ if (AppConstants.Versions.feature17Plus) {
+ android.os.UserManager um = (android.os.UserManager)context.getSystemService(Context.USER_SERVICE);
+ if (um != null) {
+ putenv("MOZ_ANDROID_USER_SERIAL_NUMBER=" + um.getSerialNumberForUser(android.os.Process.myUserHandle()));
+ } else {
+ Log.d(LOGTAG, "Unable to obtain user manager service on a device with SDK version " + Build.VERSION.SDK_INT);
+ }
+ }
+ setupLocaleEnvironment();
+
+ // We don't need this any more.
+ sIntent = null;
+ }
+
+ private static void loadLibsSetupLocked(Context context) {
+ // The package data lib directory isn't placed in ld.so's
+ // search path, so we have to manually load libraries that
+ // libxul will depend on. Not ideal.
+
+ File cacheFile = getCacheDir(context);
+ putenv("GRE_HOME=" + getGREDir(context).getPath());
+
+ // setup the libs cache
+ String linkerCache = System.getenv("MOZ_LINKER_CACHE");
+ if (linkerCache == null) {
+ linkerCache = cacheFile.getPath();
+ putenv("MOZ_LINKER_CACHE=" + linkerCache);
+ }
+
+ // Disable on-demand decompression of the linker on devices where it
+ // is known to cause crashes.
+ String forced_ondemand = System.getenv("MOZ_LINKER_ONDEMAND");
+ if (forced_ondemand == null) {
+ if ("HTC".equals(android.os.Build.MANUFACTURER) &&
+ "HTC Vision".equals(android.os.Build.MODEL)) {
+ putenv("MOZ_LINKER_ONDEMAND=0");
+ }
+ }
+
+ putenv("MOZ_LINKER_EXTRACT=1");
+ }
+
+ @RobocopTarget
+ public synchronized static void loadSQLiteLibs(final Context context, final String apkName) {
+ if (sSQLiteLibsLoaded) {
+ return;
+ }
+
+ loadMozGlue(context);
+ loadLibsSetupLocked(context);
+ loadSQLiteLibsNative(apkName);
+ sSQLiteLibsLoaded = true;
+ }
+
+ public synchronized static void loadNSSLibs(final Context context, final String apkName) {
+ if (sNSSLibsLoaded) {
+ return;
+ }
+
+ loadMozGlue(context);
+ loadLibsSetupLocked(context);
+ loadNSSLibsNative(apkName);
+ sNSSLibsLoaded = true;
+ }
+
+ @SuppressWarnings("deprecation")
+ private static final String getCPUABI() {
+ return android.os.Build.CPU_ABI;
+ }
+
+ /**
+ * Copy a library out of our APK.
+ *
+ * @param context a Context.
+ * @param lib the name of the library; e.g., "mozglue".
+ * @param outDir the output directory for the .so. No trailing slash.
+ * @return true on success, false on failure.
+ */
+ private static boolean extractLibrary(final Context context, final String lib, final String outDir) {
+ final String apkPath = context.getApplicationInfo().sourceDir;
+
+ // Sanity check.
+ if (!apkPath.endsWith(".apk")) {
+ Log.w(LOGTAG, "sourceDir is not an APK.");
+ return false;
+ }
+
+ // Try to extract the named library from the APK.
+ File outDirFile = new File(outDir);
+ if (!outDirFile.isDirectory()) {
+ if (!outDirFile.mkdirs()) {
+ Log.e(LOGTAG, "Couldn't create " + outDir);
+ return false;
+ }
+ }
+
+ if (AppConstants.Versions.feature21Plus) {
+ String[] abis = Build.SUPPORTED_ABIS;
+ for (String abi : abis) {
+ if (tryLoadWithABI(lib, outDir, apkPath, abi)) {
+ return true;
+ }
+ }
+ return false;
+ } else {
+ final String abi = getCPUABI();
+ return tryLoadWithABI(lib, outDir, apkPath, abi);
+ }
+ }
+
+ private static boolean tryLoadWithABI(String lib, String outDir, String apkPath, String abi) {
+ try {
+ final ZipFile zipFile = new ZipFile(new File(apkPath));
+ try {
+ final String libPath = "lib/" + abi + "/lib" + lib + ".so";
+ final ZipEntry entry = zipFile.getEntry(libPath);
+ if (entry == null) {
+ Log.w(LOGTAG, libPath + " not found in APK " + apkPath);
+ return false;
+ }
+
+ final InputStream in = zipFile.getInputStream(entry);
+ try {
+ final String outPath = outDir + "/lib" + lib + ".so";
+ final FileOutputStream out = new FileOutputStream(outPath);
+ final byte[] bytes = new byte[1024];
+ int read;
+
+ Log.d(LOGTAG, "Copying " + libPath + " to " + outPath);
+ boolean failed = false;
+ try {
+ while ((read = in.read(bytes, 0, 1024)) != -1) {
+ out.write(bytes, 0, read);
+ }
+ } catch (Exception e) {
+ Log.w(LOGTAG, "Failing library copy.", e);
+ failed = true;
+ } finally {
+ out.close();
+ }
+
+ if (failed) {
+ // Delete the partial copy so we don't fail to load it.
+ // Don't bother to check the return value -- there's nothing
+ // we can do about a failure.
+ new File(outPath).delete();
+ } else {
+ // Mark the file as executable. This doesn't seem to be
+ // necessary for the loader, but it's the normal state of
+ // affairs.
+ Log.d(LOGTAG, "Marking " + outPath + " as executable.");
+ new File(outPath).setExecutable(true);
+ }
+
+ return !failed;
+ } finally {
+ in.close();
+ }
+ } finally {
+ zipFile.close();
+ }
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Failed to extract lib from APK.", e);
+ return false;
+ }
+ }
+
+ private static String getLoadDiagnostics(final Context context, final String lib) {
+ final String androidPackageName = context.getPackageName();
+
+ final StringBuilder message = new StringBuilder("LOAD ");
+ message.append(lib);
+
+ // These might differ. If so, we know why the library won't load!
+ message.append(": ABI: " + AppConstants.MOZ_APP_ABI + ", " + getCPUABI());
+ message.append(": Data: " + context.getApplicationInfo().dataDir);
+ try {
+ final boolean appLibExists = new File("/data/app-lib/" + androidPackageName + "/lib" + lib + ".so").exists();
+ final boolean dataDataExists = new File("/data/data/" + androidPackageName + "/lib/lib" + lib + ".so").exists();
+ message.append(", ax=" + appLibExists);
+ message.append(", ddx=" + dataDataExists);
+ } catch (Throwable e) {
+ message.append(": ax/ddx fail, ");
+ }
+
+ try {
+ final String dashOne = "/data/data/" + androidPackageName + "-1";
+ final String dashTwo = "/data/data/" + androidPackageName + "-2";
+ final boolean dashOneExists = new File(dashOne).exists();
+ final boolean dashTwoExists = new File(dashTwo).exists();
+ message.append(", -1x=" + dashOneExists);
+ message.append(", -2x=" + dashTwoExists);
+ } catch (Throwable e) {
+ message.append(", dash fail, ");
+ }
+
+ try {
+ if (Build.VERSION.SDK_INT >= 9) {
+ final String nativeLibPath = context.getApplicationInfo().nativeLibraryDir;
+ final boolean nativeLibDirExists = new File(nativeLibPath).exists();
+ final boolean nativeLibLibExists = new File(nativeLibPath + "/lib" + lib + ".so").exists();
+
+ message.append(", nativeLib: " + nativeLibPath);
+ message.append(", dirx=" + nativeLibDirExists);
+ message.append(", libx=" + nativeLibLibExists);
+ } else {
+ message.append(", <pre-9>");
+ }
+ } catch (Throwable e) {
+ message.append(", nativeLib fail.");
+ }
+
+ return message.toString();
+ }
+
+ private static final boolean attemptLoad(final String path) {
+ try {
+ System.load(path);
+ return true;
+ } catch (Throwable e) {
+ Log.wtf(LOGTAG, "Couldn't load " + path + ": " + e);
+ }
+
+ return false;
+ }
+
+ /**
+ * The first two attempts at loading a library: directly, and
+ * then using the app library path.
+ *
+ * Returns null or the cause exception.
+ */
+ private static final Throwable doLoadLibraryExpected(final Context context, final String lib) {
+ try {
+ // Attempt 1: the way that should work.
+ System.loadLibrary(lib);
+ return null;
+ } catch (Throwable e) {
+ Log.wtf(LOGTAG, "Couldn't load " + lib + ". Trying native library dir.");
+
+ if (Build.VERSION.SDK_INT < 9) {
+ // We can't use nativeLibraryDir.
+ return e;
+ }
+
+ // Attempt 2: use nativeLibraryDir, which should also work.
+ final String libDir = context.getApplicationInfo().nativeLibraryDir;
+ final String libPath = libDir + "/lib" + lib + ".so";
+
+ // Does it even exist?
+ if (new File(libPath).exists()) {
+ if (attemptLoad(libPath)) {
+ // Success!
+ return null;
+ }
+ Log.wtf(LOGTAG, "Library exists but couldn't load!");
+ } else {
+ Log.wtf(LOGTAG, "Library doesn't exist when it should.");
+ }
+
+ // We failed. Return the original cause.
+ return e;
+ }
+ }
+
+ public static void doLoadLibrary(final Context context, final String lib) {
+ final Throwable e = doLoadLibraryExpected(context, lib);
+ if (e == null) {
+ // Success.
+ return;
+ }
+
+ // If we're in a mismatched UID state (Bug 1042935 Comment 16) there's really
+ // nothing we can do.
+ if (Build.VERSION.SDK_INT >= 9) {
+ final String nativeLibPath = context.getApplicationInfo().nativeLibraryDir;
+ if (nativeLibPath.contains("mismatched_uid")) {
+ throw new RuntimeException("Fatal: mismatched UID: cannot load.");
+ }
+ }
+
+ // Attempt 3: try finding the path the pseudo-supported way using .dataDir.
+ final String dataLibPath = context.getApplicationInfo().dataDir + "/lib/lib" + lib + ".so";
+ if (attemptLoad(dataLibPath)) {
+ return;
+ }
+
+ // Attempt 4: use /data/app-lib directly. This is a last-ditch effort.
+ final String androidPackageName = context.getPackageName();
+ if (attemptLoad("/data/app-lib/" + androidPackageName + "/lib" + lib + ".so")) {
+ return;
+ }
+
+ // Attempt 5: even more optimistic.
+ if (attemptLoad("/data/data/" + androidPackageName + "/lib/lib" + lib + ".so")) {
+ return;
+ }
+
+ // Look in our files directory, copying from the APK first if necessary.
+ final String filesLibDir = context.getFilesDir() + "/lib";
+ final String filesLibPath = filesLibDir + "/lib" + lib + ".so";
+ if (new File(filesLibPath).exists()) {
+ if (attemptLoad(filesLibPath)) {
+ return;
+ }
+ } else {
+ // Try copying.
+ if (extractLibrary(context, lib, filesLibDir)) {
+ // Let's try it!
+ if (attemptLoad(filesLibPath)) {
+ return;
+ }
+ }
+ }
+
+ // Give up loudly, leaking information to debug the failure.
+ final String message = getLoadDiagnostics(context, lib);
+ Log.e(LOGTAG, "Load diagnostics: " + message);
+
+ // Throw the descriptive message, using the original library load
+ // failure as the cause.
+ throw new RuntimeException(message, e);
+ }
+
+ public synchronized static void loadMozGlue(final Context context) {
+ if (sMozGlueLoaded) {
+ return;
+ }
+
+ doLoadLibrary(context, "mozglue");
+ sMozGlueLoaded = true;
+ }
+
+ public synchronized static void loadGeckoLibs(final Context context, final String apkName) {
+ loadLibsSetupLocked(context);
+ loadGeckoLibsNative(apkName);
+ }
+
+ public synchronized static void extractGeckoLibs(final Context context, final String apkName) {
+ loadLibsSetupLocked(context);
+ try {
+ extractGeckoLibsNative(apkName);
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Failing library extraction.", e);
+ }
+ }
+
+ private static void setupLocaleEnvironment() {
+ putenv("LANG=" + Locale.getDefault().toString());
+ NumberFormat nf = NumberFormat.getInstance();
+ if (nf instanceof DecimalFormat) {
+ DecimalFormat df = (DecimalFormat)nf;
+ DecimalFormatSymbols dfs = df.getDecimalFormatSymbols();
+
+ putenv("LOCALE_DECIMAL_POINT=" + dfs.getDecimalSeparator());
+ putenv("LOCALE_THOUSANDS_SEP=" + dfs.getGroupingSeparator());
+ putenv("LOCALE_GROUPING=" + (char)df.getGroupingSize());
+ }
+ }
+
+ @SuppressWarnings("serial")
+ public static class AbortException extends Exception {
+ public AbortException(String msg) {
+ super(msg);
+ }
+ }
+
+ @JNITarget
+ public static void abort(final String msg) {
+ final Thread thread = Thread.currentThread();
+ final Thread.UncaughtExceptionHandler uncaughtHandler =
+ thread.getUncaughtExceptionHandler();
+ if (uncaughtHandler != null) {
+ uncaughtHandler.uncaughtException(thread, new AbortException(msg));
+ }
+ }
+
+ // These methods are implemented in mozglue/android/nsGeckoUtils.cpp
+ private static native void putenv(String map);
+
+ // These methods are implemented in mozglue/android/APKOpen.cpp
+ public static native void nativeRun(String args);
+ private static native void loadGeckoLibsNative(String apkName);
+ private static native void loadSQLiteLibsNative(String apkName);
+ private static native void loadNSSLibsNative(String apkName);
+ private static native void extractGeckoLibsNative(String apkName);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/JNIObject.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/JNIObject.java
new file mode 100644
index 0000000000..a3a127a1a0
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/JNIObject.java
@@ -0,0 +1,11 @@
+package org.mozilla.gecko.mozglue;
+
+// Class that all classes with native methods extend from.
+public abstract class JNIObject
+{
+ // Pointer to a WeakPtr object that refers to the native object.
+ private long mHandle;
+
+ // Dispose of any reference to a native object.
+ protected abstract void disposeNative();
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeReference.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeReference.java
new file mode 100644
index 0000000000..9d897d3842
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeReference.java
@@ -0,0 +1,13 @@
+/* -*- 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.mozglue;
+
+public interface NativeReference
+{
+ public void release();
+
+ public boolean isReleased();
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeZip.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeZip.java
new file mode 100644
index 0000000000..11241c5757
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeZip.java
@@ -0,0 +1,84 @@
+/* -*- 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.mozglue;
+
+import android.support.annotation.Keep;
+import org.mozilla.gecko.annotation.JNITarget;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+
+public class NativeZip implements NativeReference {
+ private static final int DEFLATE = 8;
+ private static final int STORE = 0;
+
+ private volatile long mObj;
+ @Keep
+ private InputStream mInput;
+
+ public NativeZip(String path) {
+ mObj = getZip(path);
+ }
+
+ public NativeZip(InputStream input) {
+ if (!(input instanceof ByteBufferInputStream)) {
+ throw new IllegalArgumentException("Got " + input.getClass()
+ + ", but expected ByteBufferInputStream!");
+ }
+ ByteBufferInputStream bbinput = (ByteBufferInputStream)input;
+ mObj = getZipFromByteBuffer(bbinput.mBuf);
+ mInput = input;
+ }
+
+ @Override
+ protected void finalize() {
+ release();
+ }
+
+ @Override
+ public void release() {
+ if (mObj != 0) {
+ _release(mObj);
+ mObj = 0;
+ }
+ mInput = null;
+ }
+
+ @Override
+ public boolean isReleased() {
+ return (mObj == 0);
+ }
+
+ public InputStream getInputStream(String path) {
+ if (isReleased()) {
+ throw new IllegalStateException("Can't get path \"" + path
+ + "\" because NativeZip is closed!");
+ }
+ return _getInputStream(mObj, path);
+ }
+
+ private static native long getZip(String path);
+ private static native long getZipFromByteBuffer(ByteBuffer buffer);
+ private static native void _release(long obj);
+ private native InputStream _getInputStream(long obj, String path);
+
+ @JNITarget
+ private InputStream createInputStream(ByteBuffer buffer, int compression) {
+ if (compression != STORE && compression != DEFLATE) {
+ throw new IllegalArgumentException("Unexpected compression: " + compression);
+ }
+
+ InputStream input = new ByteBufferInputStream(buffer, this);
+ if (compression == DEFLATE) {
+ Inflater inflater = new Inflater(true);
+ input = new InflaterInputStream(input, inflater);
+ }
+
+ return input;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/SafeIntent.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/SafeIntent.java
new file mode 100644
index 0000000000..6942962fe4
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/SafeIntent.java
@@ -0,0 +1,134 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+// This should be in util/, but is here because of build dependency issues.
+package org.mozilla.gecko.mozglue;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * External applications can pass values into Intents that can cause us to crash: in defense,
+ * we wrap {@link Intent} and catch the exceptions they may force us to throw. See bug 1090385
+ * for more.
+ */
+public class SafeIntent {
+ private static final String LOGTAG = "Gecko" + SafeIntent.class.getSimpleName();
+
+ private final Intent intent;
+
+ public SafeIntent(final Intent intent) {
+ this.intent = intent;
+ }
+
+ public boolean hasExtra(String name) {
+ try {
+ return intent.hasExtra(name);
+ } catch (OutOfMemoryError e) {
+ Log.w(LOGTAG, "Couldn't determine if intent had an extra: OOM. Malformed?");
+ return false;
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "Couldn't determine if intent had an extra.", e);
+ return false;
+ }
+ }
+
+ public boolean getBooleanExtra(final String name, final boolean defaultValue) {
+ try {
+ return intent.getBooleanExtra(name, defaultValue);
+ } catch (OutOfMemoryError e) {
+ Log.w(LOGTAG, "Couldn't get intent extras: OOM. Malformed?");
+ return defaultValue;
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "Couldn't get intent extras.", e);
+ return defaultValue;
+ }
+ }
+
+ public int getIntExtra(final String name, final int defaultValue) {
+ try {
+ return intent.getIntExtra(name, defaultValue);
+ } catch (OutOfMemoryError e) {
+ Log.w(LOGTAG, "Couldn't get intent extras: OOM. Malformed?");
+ return defaultValue;
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "Couldn't get intent extras.", e);
+ return defaultValue;
+ }
+ }
+
+ public String getStringExtra(final String name) {
+ try {
+ return intent.getStringExtra(name);
+ } catch (OutOfMemoryError e) {
+ Log.w(LOGTAG, "Couldn't get intent extras: OOM. Malformed?");
+ return null;
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "Couldn't get intent extras.", e);
+ return null;
+ }
+ }
+
+ public Bundle getBundleExtra(final String name) {
+ try {
+ return intent.getBundleExtra(name);
+ } catch (OutOfMemoryError e) {
+ Log.w(LOGTAG, "Couldn't get intent extras: OOM. Malformed?");
+ return null;
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "Couldn't get intent extras.", e);
+ return null;
+ }
+ }
+
+ public String getAction() {
+ return intent.getAction();
+ }
+
+ public String getDataString() {
+ try {
+ return intent.getDataString();
+ } catch (OutOfMemoryError e) {
+ Log.w(LOGTAG, "Couldn't get intent data string: OOM. Malformed?");
+ return null;
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "Couldn't get intent data string.", e);
+ return null;
+ }
+ }
+
+ public ArrayList<String> getStringArrayListExtra(final String name) {
+ try {
+ return intent.getStringArrayListExtra(name);
+ } catch (OutOfMemoryError e) {
+ Log.w(LOGTAG, "Couldn't get intent data string: OOM. Malformed?");
+ return null;
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "Couldn't get intent data string.", e);
+ return null;
+ }
+ }
+
+ public Uri getData() {
+ try {
+ return intent.getData();
+ } catch (OutOfMemoryError e) {
+ Log.w(LOGTAG, "Couldn't get intent data: OOM. Malformed?");
+ return null;
+ } catch (RuntimeException e) {
+ Log.w(LOGTAG, "Couldn't get intent data.", e);
+ return null;
+ }
+ }
+
+ public Intent getUnsafe() {
+ return intent;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionBlock.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionBlock.java
new file mode 100644
index 0000000000..a4d72f2584
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionBlock.java
@@ -0,0 +1,133 @@
+/* -*- 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.permissions;
+
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.support.annotation.NonNull;
+
+/**
+ * Helper class to run code blocks depending on whether a user has granted or denied certain runtime permissions.
+ */
+public class PermissionBlock {
+ private final PermissionsHelper helper;
+
+ private Context context;
+ private String[] permissions;
+ private boolean onUIThread;
+ private Runnable onPermissionsGranted;
+ private Runnable onPermissionsDenied;
+ private boolean doNotPrompt;
+
+ /* package-private */ PermissionBlock(Context context, PermissionsHelper helper) {
+ this.context = context;
+ this.helper = helper;
+ }
+
+ /**
+ * Determine whether the app has been granted the specified permissions.
+ */
+ public PermissionBlock withPermissions(@NonNull String... permissions) {
+ this.permissions = permissions;
+ return this;
+ }
+
+ /**
+ * Execute all callbacks on the UI thread.
+ */
+ public PermissionBlock onUIThread() {
+ this.onUIThread = true;
+ return this;
+ }
+
+ /**
+ * Do not prompt the user to accept the permission if it has not been granted yet.
+ */
+ public PermissionBlock doNotPrompt() {
+ doNotPrompt = true;
+ return this;
+ }
+
+ /**
+ * If the condition is true then do not prompt the user to accept the permission if it has not
+ * been granted yet.
+ */
+ public PermissionBlock doNotPromptIf(boolean condition) {
+ if (condition) {
+ doNotPrompt();
+ }
+
+ return this;
+ }
+
+ /**
+ * Execute this permission block. Calling this method will prompt the user if needed.
+ */
+ public void run() {
+ run(null);
+ }
+
+ /**
+ * Execute the specified runnable if the app has been granted all permissions. Calling this method will prompt the
+ * user if needed.
+ */
+ public void run(Runnable onPermissionsGranted) {
+ if (!doNotPrompt && !(context instanceof Activity)) {
+ throw new IllegalStateException("You need to either specify doNotPrompt() or pass in an Activity context");
+ }
+
+ this.onPermissionsGranted = onPermissionsGranted;
+
+ if (hasPermissions(context)) {
+ onPermissionsGranted();
+ } else if (doNotPrompt) {
+ onPermissionsDenied();
+ } else {
+ Permissions.prompt((Activity) context, this);
+ }
+
+ // This reference is no longer needed. Let's clear it now to avoid memory leaks.
+ context = null;
+ }
+
+ /**
+ * Execute this fallback if at least one permission has not been granted.
+ */
+ public PermissionBlock andFallback(@NonNull Runnable onPermissionsDenied) {
+ this.onPermissionsDenied = onPermissionsDenied;
+ return this;
+ }
+
+ /* package-private */ void onPermissionsGranted() {
+ executeRunnable(onPermissionsGranted);
+ }
+
+ /* package-private */ void onPermissionsDenied() {
+ executeRunnable(onPermissionsDenied);
+ }
+
+ private void executeRunnable(Runnable runnable) {
+ if (runnable == null) {
+ return;
+ }
+
+ if (onUIThread) {
+ ThreadUtils.postToUiThread(runnable);
+ } else {
+ runnable.run();
+ }
+ }
+
+ /* package-private */ String[] getPermissions() {
+ return permissions;
+ }
+
+ /* packacge-private */ boolean hasPermissions(Context context) {
+ return helper.hasPermissions(context, permissions);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/Permissions.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/Permissions.java
new file mode 100644
index 0000000000..c1b38f61cb
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/Permissions.java
@@ -0,0 +1,210 @@
+/* -*- 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.permissions;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.support.annotation.NonNull;
+
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+/**
+ * Convenience class for checking and prompting for runtime permissions.
+ *
+ * Example:
+ *
+ * Permissions.from(activity)
+ * .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ * .onUiThread()
+ * .andFallback(onPermissionDenied())
+ * .run(onPermissionGranted())
+ *
+ * This example will run the runnable returned by onPermissionGranted() if the WRITE_EXTERNAL_STORAGE permission is
+ * already granted. Otherwise it will prompt the user and run the runnable returned by onPermissionGranted() or
+ * onPermissionDenied() depending on whether the user accepted or not. If onUiThread() is specified then all callbacks
+ * will be run on the UI thread.
+ */
+public class Permissions {
+ private static final Queue<PermissionBlock> waiting = new LinkedList<>();
+ private static final Queue<PermissionBlock> prompt = new LinkedList<>();
+
+ private static PermissionsHelper permissionHelper = new PermissionsHelper();
+
+ /**
+ * Entry point for checking (and optionally prompting for) runtime permissions.
+ *
+ * Note: The provided context needs to be an Activity context in order to prompt. Use doNotPrompt()
+ * for all other contexts.
+ */
+ public static PermissionBlock from(@NonNull Context context) {
+ return new PermissionBlock(context, permissionHelper);
+ }
+
+ /**
+ * This method will block until the specified permissions have been granted or denied by the user.
+ * If needed the user will be prompted.
+ *
+ * @return true if all of the permissions have been granted. False if any of the permissions have been denied.
+ */
+ public static boolean waitFor(@NonNull Activity activity, String... permissions) {
+ ThreadUtils.assertNotOnUiThread(); // We do not want to block the UI thread.
+
+ // This task will block until all of the permissions have been granted
+ final FutureTask<Boolean> blockingTask = new FutureTask<>(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ return true;
+ }
+ });
+
+ // This runnable will cancel the task if any of the permissions have been denied
+ Runnable cancelBlockingTask = new Runnable() {
+ @Override
+ public void run() {
+ blockingTask.cancel(true);
+ }
+ };
+
+ Permissions.from(activity)
+ .withPermissions(permissions)
+ .andFallback(cancelBlockingTask)
+ .run(blockingTask);
+
+ try {
+ return blockingTask.get();
+ } catch (InterruptedException | ExecutionException | CancellationException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Determine whether you have been granted particular permissions.
+ */
+ public static boolean has(Context context, String... permissions) {
+ return permissionHelper.hasPermissions(context, permissions);
+ }
+
+ /* package-private */ static void setPermissionHelper(PermissionsHelper permissionHelper) {
+ Permissions.permissionHelper = permissionHelper;
+ }
+
+ /**
+ * Callback for Activity.onRequestPermissionsResult(). All activities that prompt for permissions using this class
+ * should implement onRequestPermissionsResult() and call this method.
+ */
+ public static synchronized void onRequestPermissionsResult(@NonNull Activity activity, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ processGrantResults(permissions, grantResults);
+
+ processQueue(activity, permissions, grantResults);
+ }
+
+ /* package-private */ static synchronized void prompt(Activity activity, PermissionBlock block) {
+ if (prompt.isEmpty()) {
+ prompt.add(block);
+ showPrompt(activity);
+ } else {
+ waiting.add(block);
+ }
+ }
+
+ private static synchronized void processGrantResults(@NonNull String[] permissions, @NonNull int[] grantResults) {
+ final HashSet<String> grantedPermissions = collectGrantedPermissions(permissions, grantResults);
+
+ while (!prompt.isEmpty()) {
+ final PermissionBlock block = prompt.poll();
+
+ if (allPermissionsGranted(block, grantedPermissions)) {
+ block.onPermissionsGranted();
+ } else {
+ block.onPermissionsDenied();
+ }
+ }
+ }
+
+ private static synchronized void processQueue(Activity activity, String[] permissions, int[] grantResults) {
+ final HashSet<String> deniedPermissions = collectDeniedPermissions(permissions, grantResults);
+
+ while (!waiting.isEmpty()) {
+ final PermissionBlock block = waiting.poll();
+
+ if (block.hasPermissions(activity)) {
+ block.onPermissionsGranted();
+ } else {
+ if (atLeastOnePermissionDenied(block, deniedPermissions)) {
+ // We just prompted the user and one of the permissions of this block has been denied:
+ // There's no reason to instantly prompt again; Just reject without prompting.
+ block.onPermissionsDenied();
+ } else {
+ prompt.add(block);
+ }
+ }
+ }
+
+ if (!prompt.isEmpty()) {
+ showPrompt(activity);
+ }
+ }
+
+ private static synchronized void showPrompt(Activity activity) {
+ HashSet<String> permissions = new HashSet<>();
+
+ for (PermissionBlock block : prompt) {
+ Collections.addAll(permissions, block.getPermissions());
+ }
+
+ permissionHelper.prompt(activity, permissions.toArray(new String[permissions.size()]));
+ }
+
+ private static HashSet<String> collectGrantedPermissions(@NonNull String[] permissions, @NonNull int[] grantResults) {
+ return filterPermissionsByResult(permissions, grantResults, PackageManager.PERMISSION_GRANTED);
+ }
+
+ private static HashSet<String> collectDeniedPermissions(@NonNull String[] permissions, @NonNull int[] grantResults) {
+ return filterPermissionsByResult(permissions, grantResults, PackageManager.PERMISSION_DENIED);
+ }
+
+ private static HashSet<String> filterPermissionsByResult(@NonNull String[] permissions, @NonNull int[] grantResults, int result) {
+ HashSet<String> grantedPermissions = new HashSet<>(permissions.length);
+ for (int i = 0; i < permissions.length; i++) {
+ if (grantResults[i] == result) {
+ grantedPermissions.add(permissions[i]);
+ }
+ }
+ return grantedPermissions;
+ }
+
+ private static boolean allPermissionsGranted(PermissionBlock block, HashSet<String> grantedPermissions) {
+ for (String permission : block.getPermissions()) {
+ if (!grantedPermissions.contains(permission)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static boolean atLeastOnePermissionDenied(PermissionBlock block, HashSet<String> deniedPermissions) {
+ for (String permission : block.getPermissions()) {
+ if (deniedPermissions.contains(permission)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionsHelper.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionsHelper.java
new file mode 100644
index 0000000000..945a81f43a
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/permissions/PermissionsHelper.java
@@ -0,0 +1,32 @@
+/* -*- 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.permissions;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+
+/* package-private */ class PermissionsHelper {
+ private static final int PERMISSIONS_REQUEST_CODE = 212;
+
+ public boolean hasPermissions(Context context, String... permissions) {
+ for (String permission : permissions) {
+ final int permissionCheck = ContextCompat.checkSelfPermission(context, permission);
+
+ if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public void prompt(Activity activity, String[] permissions) {
+ ActivityCompat.requestPermissions(activity, permissions, PERMISSIONS_REQUEST_CODE);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/ByteBufferInputStream.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/ByteBufferInputStream.java
new file mode 100644
index 0000000000..f6b16619f5
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/ByteBufferInputStream.java
@@ -0,0 +1,38 @@
+/* -*- 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.sqlite;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+/*
+ * Helper class to make the ByteBuffers returned by SQLite BLOB
+ * easier to use.
+ */
+public class ByteBufferInputStream extends InputStream {
+ private final ByteBuffer mByteBuffer;
+
+ public ByteBufferInputStream(ByteBuffer aByteBuffer) {
+ mByteBuffer = aByteBuffer;
+ }
+
+ @Override
+ public synchronized int read() throws IOException {
+ if (!mByteBuffer.hasRemaining()) {
+ return -1;
+ }
+ return mByteBuffer.get();
+ }
+
+ @Override
+ public synchronized int read(byte[] aBytes, int aOffset, int aLen)
+ throws IOException {
+ int toRead = Math.min(aLen, mByteBuffer.remaining());
+ mByteBuffer.get(aBytes, aOffset, toRead);
+ return toRead;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/MatrixBlobCursor.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/MatrixBlobCursor.java
new file mode 100644
index 0000000000..3e2023c863
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/MatrixBlobCursor.java
@@ -0,0 +1,366 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+/*
+ * 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.sqlite;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+
+import org.mozilla.gecko.annotation.JNITarget;
+import org.mozilla.gecko.AppConstants;
+
+import android.database.AbstractCursor;
+import android.database.CursorIndexOutOfBoundsException;
+import android.util.Log;
+
+/**
+ * A mutable cursor implementation backed by an array of {@code Object}s. Use
+ * {@link #newRow()} to add rows. Automatically expands internal capacity
+ * as needed.
+ *
+ * This class provides one missing feature from Android's MatrixCursor:
+ * the implementation of getBlob that was inadvertently omitted from API 9 (and
+ * perhaps later; it's present in 14).
+ *
+ * MatrixCursor is all private, so we entirely duplicate it here.
+ */
+public class MatrixBlobCursor extends AbstractCursor {
+ private static final String LOGTAG = "GeckoMatrixCursor";
+
+ private final String[] columnNames;
+ private final int columnCount;
+
+ private int rowCount;
+ private Throwable allocationStack;
+
+ Object[] data;
+
+ /**
+ * Constructs a new cursor with the given initial capacity.
+ *
+ * @param columnNames names of the columns, the ordering of which
+ * determines column ordering elsewhere in this cursor
+ * @param initialCapacity in rows
+ */
+ @JNITarget
+ public MatrixBlobCursor(String[] columnNames, int initialCapacity) {
+ this.columnNames = columnNames;
+ this.columnCount = columnNames.length;
+
+ if (initialCapacity < 1) {
+ initialCapacity = 1;
+ }
+
+ this.data = new Object[columnCount * initialCapacity];
+ if (AppConstants.DEBUG_BUILD) {
+ this.allocationStack = new Throwable("allocationStack");
+ }
+ }
+
+ /**
+ * Constructs a new cursor.
+ *
+ * @param columnNames names of the columns, the ordering of which
+ * determines column ordering elsewhere in this cursor
+ */
+ @JNITarget
+ public MatrixBlobCursor(String[] columnNames) {
+ this(columnNames, 16);
+ }
+
+ /**
+ * Closes the Cursor, releasing all of its resources.
+ */
+ public void close() {
+ this.allocationStack = null;
+ this.data = null;
+ super.close();
+ }
+
+ /**
+ * Gets value at the given column for the current row.
+ */
+ protected Object get(int column) {
+ if (column < 0 || column >= columnCount) {
+ throw new CursorIndexOutOfBoundsException("Requested column: "
+ + column + ", # of columns: " + columnCount);
+ }
+ if (mPos < 0) {
+ throw new CursorIndexOutOfBoundsException("Before first row.");
+ }
+ if (mPos >= rowCount) {
+ throw new CursorIndexOutOfBoundsException("After last row.");
+ }
+ return data[mPos * columnCount + column];
+ }
+
+ /**
+ * Adds a new row to the end and returns a builder for that row. Not safe
+ * for concurrent use.
+ *
+ * @return builder which can be used to set the column values for the new
+ * row
+ */
+ public RowBuilder newRow() {
+ rowCount++;
+ int endIndex = rowCount * columnCount;
+ ensureCapacity(endIndex);
+ int start = endIndex - columnCount;
+ return new RowBuilder(start, endIndex);
+ }
+
+ /**
+ * Adds a new row to the end with the given column values. Not safe
+ * for concurrent use.
+ *
+ * @throws IllegalArgumentException if {@code columnValues.length !=
+ * columnNames.length}
+ * @param columnValues in the same order as the the column names specified
+ * at cursor construction time
+ */
+ @JNITarget
+ public void addRow(Object[] columnValues) {
+ if (columnValues.length != columnCount) {
+ throw new IllegalArgumentException("columnNames.length = "
+ + columnCount + ", columnValues.length = "
+ + columnValues.length);
+ }
+
+ int start = rowCount++ * columnCount;
+ ensureCapacity(start + columnCount);
+ System.arraycopy(columnValues, 0, data, start, columnCount);
+ }
+
+ /**
+ * Adds a new row to the end with the given column values. Not safe
+ * for concurrent use.
+ *
+ * @throws IllegalArgumentException if {@code columnValues.size() !=
+ * columnNames.length}
+ * @param columnValues in the same order as the the column names specified
+ * at cursor construction time
+ */
+ @JNITarget
+ public void addRow(Iterable<?> columnValues) {
+ final int start = rowCount * columnCount;
+
+ if (columnValues instanceof ArrayList<?>) {
+ addRow((ArrayList<?>) columnValues, start);
+ return;
+ }
+
+ final int end = start + columnCount;
+ int current = start;
+
+ ensureCapacity(end);
+ final Object[] localData = data;
+ for (Object columnValue : columnValues) {
+ if (current == end) {
+ // TODO: null out row?
+ throw new IllegalArgumentException(
+ "columnValues.size() > columnNames.length");
+ }
+ localData[current++] = columnValue;
+ }
+
+ if (current != end) {
+ // TODO: null out row?
+ throw new IllegalArgumentException(
+ "columnValues.size() < columnNames.length");
+ }
+
+ // Increase row count here in case we encounter an exception.
+ rowCount++;
+ }
+
+ /** Optimization for {@link ArrayList}. */
+ @JNITarget
+ private void addRow(ArrayList<?> columnValues, int start) {
+ final int size = columnValues.size();
+ if (size != columnCount) {
+ throw new IllegalArgumentException("columnNames.length = "
+ + columnCount + ", columnValues.size() = " + size);
+ }
+
+ final int end = start + columnCount;
+ ensureCapacity(end);
+
+ // Take a reference just in case someone calls ensureCapacity
+ // and `data` gets replaced by a new array!
+ final Object[] localData = data;
+ for (int i = 0; i < size; i++) {
+ localData[start + i] = columnValues.get(i);
+ }
+
+ rowCount++;
+ }
+
+ /**
+ * Ensures that this cursor has enough capacity. If it needs to allocate
+ * a new array, the existing capacity will be at least doubled.
+ */
+ private void ensureCapacity(final int size) {
+ if (size <= data.length) {
+ return;
+ }
+
+ final Object[] oldData = this.data;
+ this.data = new Object[Math.max(size, data.length * 2)];
+ System.arraycopy(oldData, 0, this.data, 0, oldData.length);
+ }
+
+ /**
+ * Builds a row, starting from the left-most column and adding one column
+ * value at a time. Follows the same ordering as the column names specified
+ * at cursor construction time.
+ *
+ * Not thread-safe.
+ */
+ public class RowBuilder {
+ private int index;
+ private final int endIndex;
+
+ RowBuilder(int index, int endIndex) {
+ this.index = index;
+ this.endIndex = endIndex;
+ }
+
+ /**
+ * Sets the next column value in this row.
+ *
+ * @throws CursorIndexOutOfBoundsException if you try to add too many
+ * values
+ * @return this builder to support chaining
+ */
+ public RowBuilder add(final Object columnValue) {
+ if (index == endIndex) {
+ throw new CursorIndexOutOfBoundsException("No more columns left.");
+ }
+
+ data[index++] = columnValue;
+ return this;
+ }
+ }
+
+ /**
+ * Not thread safe.
+ */
+ public void set(int column, Object value) {
+ if (column < 0 || column >= columnCount) {
+ throw new CursorIndexOutOfBoundsException("Requested column: "
+ + column + ", # of columns: " + columnCount);
+ }
+ if (mPos < 0) {
+ throw new CursorIndexOutOfBoundsException("Before first row.");
+ }
+ if (mPos >= rowCount) {
+ throw new CursorIndexOutOfBoundsException("After last row.");
+ }
+ data[mPos * columnCount + column] = value;
+ }
+
+ // AbstractCursor implementation.
+ @Override
+ public int getCount() {
+ return rowCount;
+ }
+
+ @Override
+ public String[] getColumnNames() {
+ return columnNames;
+ }
+
+ @Override
+ public String getString(int column) {
+ Object value = get(column);
+ if (value == null) return null;
+ return value.toString();
+ }
+
+ @Override
+ public short getShort(int column) {
+ final Object value = get(column);
+ if (value == null) return 0;
+ if (value instanceof Number) return ((Number) value).shortValue();
+ return Short.parseShort(value.toString());
+ }
+
+ @Override
+ public int getInt(int column) {
+ Object value = get(column);
+ if (value == null) return 0;
+ if (value instanceof Number) return ((Number) value).intValue();
+ return Integer.parseInt(value.toString());
+ }
+
+ @Override
+ public long getLong(int column) {
+ Object value = get(column);
+ if (value == null) return 0;
+ if (value instanceof Number) return ((Number) value).longValue();
+ return Long.parseLong(value.toString());
+ }
+
+ @Override
+ public float getFloat(int column) {
+ Object value = get(column);
+ if (value == null) return 0.0f;
+ if (value instanceof Number) return ((Number) value).floatValue();
+ return Float.parseFloat(value.toString());
+ }
+
+ @Override
+ public double getDouble(int column) {
+ Object value = get(column);
+ if (value == null) return 0.0d;
+ if (value instanceof Number) return ((Number) value).doubleValue();
+ return Double.parseDouble(value.toString());
+ }
+
+ @Override
+ public byte[] getBlob(int column) {
+ Object value = get(column);
+ if (value == null) return null;
+ if (value instanceof byte[]) {
+ return (byte[]) value;
+ }
+
+ if (value instanceof ByteBuffer) {
+ final ByteBuffer bytes = (ByteBuffer) value;
+ byte[] byteArray = new byte[bytes.remaining()];
+ bytes.get(byteArray);
+ return byteArray;
+ }
+ throw new UnsupportedOperationException("BLOB Object not of known type");
+ }
+
+ @Override
+ public boolean isNull(int column) {
+ return get(column) == null;
+ }
+
+ @Override
+ protected void finalize() {
+ if (AppConstants.DEBUG_BUILD) {
+ if (!isClosed()) {
+ Log.e(LOGTAG, "Cursor finalized without being closed", this.allocationStack);
+ }
+ }
+
+ super.finalize();
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridge.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridge.java
new file mode 100644
index 0000000000..866b9e2867
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridge.java
@@ -0,0 +1,387 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sqlite;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Map.Entry;
+
+/*
+ * This class allows using the mozsqlite3 library included with Firefox
+ * to read SQLite databases, instead of the Android SQLiteDataBase API,
+ * which might use whatever outdated DB is present on the Android system.
+ */
+public class SQLiteBridge {
+ private static final String LOGTAG = "SQLiteBridge";
+
+ // Path to the database. If this database was not opened with openDatabase, we reopen it every query.
+ private final String mDb;
+
+ // Pointer to the database if it was opened with openDatabase. 0 implies closed.
+ protected volatile long mDbPointer;
+
+ // Values remembered after a query.
+ private long[] mQueryResults;
+
+ private boolean mTransactionSuccess;
+ private boolean mInTransaction;
+
+ private static final int RESULT_INSERT_ROW_ID = 0;
+ private static final int RESULT_ROWS_CHANGED = 1;
+
+ // Shamelessly cribbed from db/sqlite3/src/moz.build.
+ private static final int DEFAULT_PAGE_SIZE_BYTES = 32768;
+
+ // The same size we use elsewhere.
+ private static final int MAX_WAL_SIZE_BYTES = 524288;
+
+ // JNI code in $(topdir)/mozglue/android/..
+ private static native MatrixBlobCursor sqliteCall(String aDb, String aQuery,
+ String[] aParams,
+ long[] aUpdateResult)
+ throws SQLiteBridgeException;
+ private static native MatrixBlobCursor sqliteCallWithDb(long aDb, String aQuery,
+ String[] aParams,
+ long[] aUpdateResult)
+ throws SQLiteBridgeException;
+ private static native long openDatabase(String aDb)
+ throws SQLiteBridgeException;
+ private static native void closeDatabase(long aDb);
+
+ // Takes the path to the database we want to access.
+ @RobocopTarget
+ public SQLiteBridge(String aDb) throws SQLiteBridgeException {
+ mDb = aDb;
+ }
+
+ // Executes a simple line of sql.
+ public void execSQL(String sql)
+ throws SQLiteBridgeException {
+ Cursor cursor = internalQuery(sql, null);
+ cursor.close();
+ }
+
+ // Executes a simple line of sql. Allow you to bind arguments
+ public void execSQL(String sql, String[] bindArgs)
+ throws SQLiteBridgeException {
+ Cursor cursor = internalQuery(sql, bindArgs);
+ cursor.close();
+ }
+
+ // Executes a DELETE statement on the database
+ public int delete(String table, String whereClause, String[] whereArgs)
+ throws SQLiteBridgeException {
+ StringBuilder sb = new StringBuilder("DELETE from ");
+ sb.append(table);
+ if (whereClause != null) {
+ sb.append(" WHERE " + whereClause);
+ }
+
+ execSQL(sb.toString(), whereArgs);
+ return (int)mQueryResults[RESULT_ROWS_CHANGED];
+ }
+
+ public Cursor query(String table,
+ String[] columns,
+ String selection,
+ String[] selectionArgs,
+ String groupBy,
+ String having,
+ String orderBy,
+ String limit)
+ throws SQLiteBridgeException {
+ StringBuilder sb = new StringBuilder("SELECT ");
+ if (columns != null)
+ sb.append(TextUtils.join(", ", columns));
+ else
+ sb.append(" * ");
+
+ sb.append(" FROM ");
+ sb.append(table);
+
+ if (selection != null) {
+ sb.append(" WHERE " + selection);
+ }
+
+ if (groupBy != null) {
+ sb.append(" GROUP BY " + groupBy);
+ }
+
+ if (having != null) {
+ sb.append(" HAVING " + having);
+ }
+
+ if (orderBy != null) {
+ sb.append(" ORDER BY " + orderBy);
+ }
+
+ if (limit != null) {
+ sb.append(" " + limit);
+ }
+
+ return rawQuery(sb.toString(), selectionArgs);
+ }
+
+ @RobocopTarget
+ public Cursor rawQuery(String sql, String[] selectionArgs)
+ throws SQLiteBridgeException {
+ return internalQuery(sql, selectionArgs);
+ }
+
+ public long insert(String table, String nullColumnHack, ContentValues values)
+ throws SQLiteBridgeException {
+ if (values == null)
+ return 0;
+
+ ArrayList<String> valueNames = new ArrayList<String>();
+ ArrayList<String> valueBinds = new ArrayList<String>();
+ ArrayList<String> keyNames = new ArrayList<String>();
+
+ for (Entry<String, Object> value : values.valueSet()) {
+ keyNames.add(value.getKey());
+
+ Object val = value.getValue();
+ if (val == null) {
+ valueNames.add("NULL");
+ } else {
+ valueNames.add("?");
+ valueBinds.add(val.toString());
+ }
+ }
+
+ StringBuilder sb = new StringBuilder("INSERT into ");
+ sb.append(table);
+
+ sb.append(" (");
+ sb.append(TextUtils.join(", ", keyNames));
+ sb.append(")");
+
+ // XXX - Do we need to bind these values?
+ sb.append(" VALUES (");
+ sb.append(TextUtils.join(", ", valueNames));
+ sb.append(") ");
+
+ String[] binds = new String[valueBinds.size()];
+ valueBinds.toArray(binds);
+ execSQL(sb.toString(), binds);
+ return mQueryResults[RESULT_INSERT_ROW_ID];
+ }
+
+ public int update(String table, ContentValues values, String whereClause, String[] whereArgs)
+ throws SQLiteBridgeException {
+ if (values == null)
+ return 0;
+
+ ArrayList<String> valueNames = new ArrayList<String>();
+
+ StringBuilder sb = new StringBuilder("UPDATE ");
+ sb.append(table);
+ sb.append(" SET ");
+
+ boolean isFirst = true;
+
+ for (Entry<String, Object> value : values.valueSet()) {
+ if (isFirst)
+ isFirst = false;
+ else
+ sb.append(", ");
+
+ sb.append(value.getKey());
+
+ Object val = value.getValue();
+ if (val == null) {
+ sb.append(" = NULL");
+ } else {
+ sb.append(" = ?");
+ valueNames.add(val.toString());
+ }
+ }
+
+ if (!TextUtils.isEmpty(whereClause)) {
+ sb.append(" WHERE ");
+ sb.append(whereClause);
+ valueNames.addAll(Arrays.asList(whereArgs));
+ }
+
+ String[] binds = new String[valueNames.size()];
+ valueNames.toArray(binds);
+
+ execSQL(sb.toString(), binds);
+ return (int)mQueryResults[RESULT_ROWS_CHANGED];
+ }
+
+ public int getVersion()
+ throws SQLiteBridgeException {
+ Cursor cursor = internalQuery("PRAGMA user_version", null);
+ int ret = -1;
+ if (cursor != null) {
+ cursor.moveToFirst();
+ String version = cursor.getString(0);
+ ret = Integer.parseInt(version);
+ cursor.close();
+ }
+ return ret;
+ }
+
+ // Do an SQL query, substituting the parameters in the query with the passed
+ // parameters. The parameters are substituted in order: named parameters
+ // are not supported.
+ private Cursor internalQuery(String aQuery, String[] aParams)
+ throws SQLiteBridgeException {
+
+ mQueryResults = new long[2];
+ if (isOpen()) {
+ return sqliteCallWithDb(mDbPointer, aQuery, aParams, mQueryResults);
+ }
+ return sqliteCall(mDb, aQuery, aParams, mQueryResults);
+ }
+
+ /*
+ * The second two parameters here are just provided for compatibility with SQLiteDatabase
+ * Support for them is not currently implemented.
+ */
+ public static SQLiteBridge openDatabase(String path, SQLiteDatabase.CursorFactory factory, int flags)
+ throws SQLiteException {
+ if (factory != null) {
+ throw new RuntimeException("factory not supported.");
+ }
+ if (flags != 0) {
+ throw new RuntimeException("flags not supported.");
+ }
+
+ SQLiteBridge bridge = null;
+ try {
+ bridge = new SQLiteBridge(path);
+ bridge.mDbPointer = SQLiteBridge.openDatabase(path);
+ } catch (SQLiteBridgeException ex) {
+ // Catch and rethrow as a SQLiteException to match SQLiteDatabase.
+ throw new SQLiteException(ex.getMessage());
+ }
+
+ prepareWAL(bridge);
+
+ return bridge;
+ }
+
+ public void close() {
+ if (isOpen()) {
+ closeDatabase(mDbPointer);
+ }
+ mDbPointer = 0L;
+ }
+
+ public boolean isOpen() {
+ return mDbPointer != 0;
+ }
+
+ public void beginTransaction() throws SQLiteBridgeException {
+ if (inTransaction()) {
+ throw new SQLiteBridgeException("Nested transactions are not supported");
+ }
+ execSQL("BEGIN EXCLUSIVE");
+ mTransactionSuccess = false;
+ mInTransaction = true;
+ }
+
+ public void beginTransactionNonExclusive() throws SQLiteBridgeException {
+ if (inTransaction()) {
+ throw new SQLiteBridgeException("Nested transactions are not supported");
+ }
+ execSQL("BEGIN IMMEDIATE");
+ mTransactionSuccess = false;
+ mInTransaction = true;
+ }
+
+ public void endTransaction() {
+ if (!inTransaction())
+ return;
+
+ try {
+ if (mTransactionSuccess) {
+ execSQL("COMMIT TRANSACTION");
+ } else {
+ execSQL("ROLLBACK TRANSACTION");
+ }
+ } catch (SQLiteBridgeException ex) {
+ Log.e(LOGTAG, "Error ending transaction", ex);
+ }
+ mInTransaction = false;
+ mTransactionSuccess = false;
+ }
+
+ public void setTransactionSuccessful() throws SQLiteBridgeException {
+ if (!inTransaction()) {
+ throw new SQLiteBridgeException("setTransactionSuccessful called outside a transaction");
+ }
+ mTransactionSuccess = true;
+ }
+
+ public boolean inTransaction() {
+ return mInTransaction;
+ }
+
+ @Override
+ public void finalize() {
+ if (isOpen()) {
+ Log.e(LOGTAG, "Bridge finalized without closing the database");
+ close();
+ }
+ }
+
+ private static void prepareWAL(final SQLiteBridge bridge) {
+ // Prepare for WAL mode. If we can, we switch to journal_mode=WAL, then
+ // set the checkpoint size appropriately. If we can't, then we fall back
+ // to truncating and synchronous writes.
+ final Cursor cursor = bridge.internalQuery("PRAGMA journal_mode=WAL", null);
+ try {
+ if (cursor.moveToFirst()) {
+ String journalMode = cursor.getString(0);
+ Log.d(LOGTAG, "Journal mode: " + journalMode);
+ if ("wal".equals(journalMode)) {
+ // Success! Let's make sure we autocheckpoint at a reasonable interval.
+ final int pageSizeBytes = bridge.getPageSizeBytes();
+ final int checkpointPageCount = MAX_WAL_SIZE_BYTES / pageSizeBytes;
+ bridge.execSQL("PRAGMA wal_autocheckpoint=" + checkpointPageCount);
+ } else {
+ if (!"truncate".equals(journalMode)) {
+ Log.w(LOGTAG, "Unable to activate WAL journal mode. Using truncate instead.");
+ bridge.execSQL("PRAGMA journal_mode=TRUNCATE");
+ }
+ Log.w(LOGTAG, "Not using WAL mode: using synchronous=FULL instead.");
+ bridge.execSQL("PRAGMA synchronous=FULL");
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private int getPageSizeBytes() {
+ if (!isOpen()) {
+ throw new IllegalStateException("Database not open.");
+ }
+
+ final Cursor cursor = internalQuery("PRAGMA page_size", null);
+ try {
+ if (!cursor.moveToFirst()) {
+ Log.w(LOGTAG, "Unable to retrieve page size.");
+ return DEFAULT_PAGE_SIZE_BYTES;
+ }
+
+ return cursor.getInt(0);
+ } finally {
+ cursor.close();
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridgeException.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridgeException.java
new file mode 100644
index 0000000000..c7999fc5c4
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/sqlite/SQLiteBridgeException.java
@@ -0,0 +1,18 @@
+/* -*- 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.sqlite;
+
+import org.mozilla.gecko.annotation.JNITarget;
+
+@JNITarget
+public class SQLiteBridgeException extends RuntimeException {
+ static final long serialVersionUID = 1L;
+
+ public SQLiteBridgeException() {}
+ public SQLiteBridgeException(String msg) {
+ super(msg);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityResultHandler.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityResultHandler.java
new file mode 100644
index 0000000000..6f40ee96b5
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityResultHandler.java
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.Intent;
+
+public interface ActivityResultHandler {
+ void onActivityResult(int resultCode, Intent data);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityResultHandlerMap.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityResultHandlerMap.java
new file mode 100644
index 0000000000..dc1d26cecd
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityResultHandlerMap.java
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.util.SparseArray;
+
+public final class ActivityResultHandlerMap {
+ private final SparseArray<ActivityResultHandler> mMap = new SparseArray<ActivityResultHandler>();
+ private int mCounter;
+
+ public synchronized int put(ActivityResultHandler handler) {
+ mMap.put(mCounter, handler);
+ return mCounter++;
+ }
+
+ public synchronized ActivityResultHandler getAndRemove(int i) {
+ ActivityResultHandler handler = mMap.get(i);
+ mMap.delete(i);
+
+ return handler;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityUtils.java
new file mode 100644
index 0000000000..2f15e78683
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ActivityUtils.java
@@ -0,0 +1,72 @@
+/* -*- 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.util;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+
+import org.mozilla.gecko.AppConstants.Versions;
+
+public class ActivityUtils {
+ private ActivityUtils() {
+ }
+
+ public static void setFullScreen(Activity activity, boolean fullscreen) {
+ // Hide/show the system notification bar
+ Window window = activity.getWindow();
+
+ if (Versions.feature16Plus) {
+ int newVis;
+ if (fullscreen) {
+ newVis = View.SYSTEM_UI_FLAG_FULLSCREEN;
+ if (Versions.feature19Plus) {
+ newVis |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
+ View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+ } else {
+ newVis |= View.SYSTEM_UI_FLAG_LOW_PROFILE;
+ }
+ } else {
+ newVis = View.SYSTEM_UI_FLAG_VISIBLE;
+ }
+
+ window.getDecorView().setSystemUiVisibility(newVis);
+ } else {
+ window.setFlags(fullscreen ?
+ WindowManager.LayoutParams.FLAG_FULLSCREEN : 0,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
+ }
+
+ public static boolean isFullScreen(final Activity activity) {
+ final Window window = activity.getWindow();
+
+ if (Versions.feature16Plus) {
+ final int vis = window.getDecorView().getSystemUiVisibility();
+ return (vis & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0;
+ }
+
+ final int flags = window.getAttributes().flags;
+ return ((flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) != 0);
+ }
+
+ /**
+ * Finish this activity and launch the default home screen activity.
+ */
+ public static void goToHomeScreen(Activity activity) {
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+
+ intent.addCategory(Intent.CATEGORY_HOME);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ activity.startActivity(intent);
+
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/BundleEventListener.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/BundleEventListener.java
new file mode 100644
index 0000000000..9e9bb5a9e8
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/BundleEventListener.java
@@ -0,0 +1,25 @@
+/* -*- 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 org.mozilla.gecko.annotation.RobocopTarget;
+
+import android.os.Bundle;
+
+@RobocopTarget
+public interface BundleEventListener {
+ /**
+ * Handles a message sent from Gecko.
+ *
+ * @param event The name of the event being sent.
+ * @param message The message data.
+ * @param callback The callback interface for this message. A callback is provided only if the
+ * originating Messaging.sendRequest call included a callback argument;
+ * otherwise, callback will be null. All listeners for a given event are given
+ * the same callback object, and exactly one listener must handle the callback.
+ */
+ void handleMessage(String event, Bundle message, EventCallback callback);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/Clipboard.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/Clipboard.java
new file mode 100644
index 0000000000..02b07674f1
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/Clipboard.java
@@ -0,0 +1,117 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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 java.util.concurrent.SynchronousQueue;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.AppConstants.Versions;
+
+import android.content.ClipData;
+import android.content.Context;
+import android.util.Log;
+
+public final class Clipboard {
+ // Volatile but not synchronized: we don't care about the race condition in
+ // init, because both app contexts will be the same, but we do care about a
+ // thread having a stale null value of mContext.
+ volatile static Context mContext;
+ private final static String LOGTAG = "GeckoClipboard";
+ private final static SynchronousQueue<String> sClipboardQueue = new SynchronousQueue<String>();
+
+ private Clipboard() {
+ }
+
+ public static void init(final Context c) {
+ if (mContext != null) {
+ Log.w(LOGTAG, "Clipboard.init() called twice!");
+ return;
+ }
+ mContext = c.getApplicationContext();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static String getText() {
+ // If we're on the UI thread or the background thread, we have a looper on the thread
+ // and can just call this directly. For any other threads, post the call to the
+ // background thread.
+
+ if (ThreadUtils.isOnUiThread() || ThreadUtils.isOnBackgroundThread()) {
+ return getClipboardTextImpl();
+ }
+
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ public void run() {
+ String text = getClipboardTextImpl();
+ try {
+ sClipboardQueue.put(text != null ? text : "");
+ } catch (InterruptedException ie) { }
+ }
+ });
+
+ try {
+ return sClipboardQueue.take();
+ } catch (InterruptedException ie) {
+ return "";
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static void setText(final CharSequence text) {
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ public void run() {
+ // In API Level 11 and above, CLIPBOARD_SERVICE returns android.content.ClipboardManager,
+ // which is a subclass of android.text.ClipboardManager.
+ final android.content.ClipboardManager cm = (android.content.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+ final ClipData clip = ClipData.newPlainText("Text", text);
+ try {
+ cm.setPrimaryClip(clip);
+ } catch (NullPointerException e) {
+ // Bug 776223: This is a Samsung clipboard bug. setPrimaryClip() can throw
+ // a NullPointerException if Samsung's /data/clipboard directory is full.
+ // Fortunately, the text is still successfully copied to the clipboard.
+ }
+ return;
+ }
+ });
+ }
+
+ /**
+ * @return true if the clipboard is nonempty, false otherwise.
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ public static boolean hasText() {
+ android.content.ClipboardManager cm = (android.content.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+ return cm.hasPrimaryClip();
+ }
+
+ /**
+ * Deletes all text from the clipboard.
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ public static void clearText() {
+ setText(null);
+ }
+
+ /**
+ * On some devices, access to the clipboard service needs to happen
+ * on a thread with a looper, so this function requires a looper is
+ * present on the thread.
+ */
+ @SuppressWarnings("deprecation")
+ static String getClipboardTextImpl() {
+ android.content.ClipboardManager cm = (android.content.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+ if (cm.hasPrimaryClip()) {
+ ClipData clip = cm.getPrimaryClip();
+ if (clip != null) {
+ ClipData.Item item = clip.getItemAt(0);
+ return item.coerceToText(mContext).toString();
+ }
+ }
+ return null;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ContextUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ContextUtils.java
new file mode 100644
index 0000000000..3a37911b05
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ContextUtils.java
@@ -0,0 +1,51 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.text.TextUtils;
+
+public class ContextUtils {
+ private static final String INSTALLER_GOOGLE_PLAY = "com.android.vending";
+
+ private ContextUtils() {}
+
+ /**
+ * @return {@link android.content.pm.PackageInfo#firstInstallTime} for the context's package.
+ * @throws PackageManager.NameNotFoundException Unexpected - we get the package name from the context so
+ * it's expected to be found.
+ */
+ public static PackageInfo getCurrentPackageInfo(final Context context) {
+ try {
+ return context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new AssertionError("Should not happen: Can't get package info of own package");
+ }
+ }
+
+ public static boolean isPackageInstalled(final Context context, String packageName) {
+ try {
+ PackageManager pm = context.getPackageManager();
+ pm.getPackageInfo(packageName, 0);
+ return true;
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ public static boolean isInstalledFromGooglePlay(final Context context) {
+ final String installerPackageName = context.getPackageManager().getInstallerPackageName(context.getPackageName());
+
+ if (TextUtils.isEmpty(installerPackageName)) {
+ return false;
+ }
+
+ return INSTALLER_GOOGLE_PLAY.equals(installerPackageName);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/DateUtil.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/DateUtil.java
new file mode 100644
index 0000000000..9d34a0fe84
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/DateUtil.java
@@ -0,0 +1,55 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.support.annotation.NonNull;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Utilities to help with manipulating Java's dates and calendars.
+ */
+public class DateUtil {
+ private DateUtil() {}
+
+ /**
+ * @param date the date to convert to HTTP format
+ * @return the date as specified in rfc 1123, e.g. "Tue, 01 Feb 2011 14:00:00 GMT"
+ */
+ public static String getDateInHTTPFormat(@NonNull final Date date) {
+ final DateFormat df = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss z", Locale.US);
+ df.setTimeZone(TimeZone.getTimeZone("GMT"));
+ return df.format(date);
+ }
+
+ /**
+ * Returns the timezone offset for the current date in minutes. See
+ * {@link #getTimezoneOffsetInMinutesForGivenDate(Calendar)} for more details.
+ */
+ public static int getTimezoneOffsetInMinutes(@NonNull final TimeZone timezone) {
+ return getTimezoneOffsetInMinutesForGivenDate(Calendar.getInstance(timezone));
+ }
+
+ /**
+ * Returns the time zone offset for the given date in minutes. The date makes a difference due to daylight
+ * savings time in some regions. We return minutes because we can accurately represent time zones that are
+ * offset by non-integer hour values, e.g. parts of New Zealand at UTC+12:45.
+ *
+ * @param calendar A calendar with the appropriate time zone & date already set.
+ */
+ public static int getTimezoneOffsetInMinutesForGivenDate(@NonNull final Calendar calendar) {
+ // via Date.getTimezoneOffset deprecated docs (note: it had incorrect order of operations).
+ // Also, we cast to int because we should never overflow here - the max should be GMT+14 = 840.
+ return (int) TimeUnit.MILLISECONDS.toMinutes(calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET));
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/EventCallback.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/EventCallback.java
new file mode 100644
index 0000000000..0995426661
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/EventCallback.java
@@ -0,0 +1,29 @@
+package org.mozilla.gecko.util;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+/**
+ * Callback interface for Gecko requests.
+ *
+ * For each instance of EventCallback, exactly one of sendResponse, sendError, or sendCancel
+ * must be called to prevent observer leaks. If more than one send* method is called, or if a
+ * single send method is called multiple times, an {@link IllegalStateException} will be thrown.
+ */
+@RobocopTarget
+public interface EventCallback {
+ /**
+ * Sends a success response with the given data.
+ *
+ * @param response The response data to send to Gecko. Can be any of the types accepted by
+ * JSONObject#put(String, Object).
+ */
+ public void sendSuccess(Object response);
+
+ /**
+ * Sends an error response with the given data.
+ *
+ * @param response The response data to send to Gecko. Can be any of the types accepted by
+ * JSONObject#put(String, Object).
+ */
+ public void sendError(Object response);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/FileUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/FileUtils.java
new file mode 100644
index 0000000000..01cdd42bba
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/FileUtils.java
@@ -0,0 +1,259 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.util.Log;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.FilenameFilter;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.Charset;
+import java.util.Comparator;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+public class FileUtils {
+ private static final String LOGTAG = "GeckoFileUtils";
+
+ /*
+ * A basic Filter for checking a filename and age.
+ **/
+ static public class NameAndAgeFilter implements FilenameFilter {
+ final private String mName;
+ final private double mMaxAge;
+
+ public NameAndAgeFilter(String name, double age) {
+ mName = name;
+ mMaxAge = age;
+ }
+
+ @Override
+ public boolean accept(File dir, String filename) {
+ if (mName == null || mName.matches(filename)) {
+ File f = new File(dir, filename);
+
+ if (mMaxAge < 0 || System.currentTimeMillis() - f.lastModified() > mMaxAge) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ @RobocopTarget
+ public static void delTree(File dir, FilenameFilter filter, boolean recurse) {
+ String[] files = null;
+
+ if (filter != null) {
+ files = dir.list(filter);
+ } else {
+ files = dir.list();
+ }
+
+ if (files == null) {
+ return;
+ }
+
+ for (String file : files) {
+ File f = new File(dir, file);
+ delete(f, recurse);
+ }
+ }
+
+ public static boolean delete(File file) throws IOException {
+ return delete(file, true);
+ }
+
+ public static boolean delete(File file, boolean recurse) {
+ if (file.isDirectory() && recurse) {
+ // If the quick delete failed and this is a dir, recursively delete the contents of the dir
+ String files[] = file.list();
+ for (String temp : files) {
+ File fileDelete = new File(file, temp);
+ try {
+ delete(fileDelete);
+ } catch (IOException ex) {
+ Log.i(LOGTAG, "Error deleting " + fileDelete.getPath(), ex);
+ }
+ }
+ }
+
+ // Even if this is a dir, it should now be empty and delete should work
+ return file.delete();
+ }
+
+ /**
+ * A generic solution to read a JSONObject from a file. See
+ * {@link #readStringFromFile(File)} for more details.
+ *
+ * @throws IOException if the file is empty, or another IOException occurs
+ * @throws JSONException if the file could not be converted to a JSONObject.
+ */
+ public static JSONObject readJSONObjectFromFile(final File file) throws IOException, JSONException {
+ if (file.length() == 0) {
+ // Redirect this exception so it's clearer than when the JSON parser catches it.
+ throw new IOException("Given file is empty - the JSON parser cannot create an object from an empty file");
+ }
+ return new JSONObject(readStringFromFile(file));
+ }
+
+ /**
+ * A generic solution to read from a file. For more details,
+ * see {@link #readStringFromInputStreamAndCloseStream(InputStream, int)}.
+ *
+ * This method loads the entire file into memory so will have the expected performance impact.
+ * If you're trying to read a large file, you should be handling your own reading to avoid
+ * out-of-memory errors.
+ */
+ public static String readStringFromFile(final File file) throws IOException {
+ // FileInputStream will throw FileNotFoundException if the file does not exist, but
+ // File.length will return 0 if the file does not exist so we catch it sooner.
+ if (!file.exists()) {
+ throw new FileNotFoundException("Given file, " + file + ", does not exist");
+ } else if (file.length() == 0) {
+ return "";
+ }
+ final int len = (int) file.length(); // includes potential EOF character.
+ return readStringFromInputStreamAndCloseStream(new FileInputStream(file), len);
+ }
+
+ /**
+ * A generic solution to read from an input stream in UTF-8. This function will read from the stream until it
+ * is finished and close the stream - this is necessary to close the wrapping resources.
+ *
+ * For a higher-level method, see {@link #readStringFromFile(File)}.
+ *
+ * Since this is generic, it may not be the most performant for your use case.
+ *
+ * @param bufferSize Size of the underlying buffer for read optimizations - must be > 0.
+ */
+ public static String readStringFromInputStreamAndCloseStream(final InputStream inputStream, final int bufferSize)
+ throws IOException {
+ if (bufferSize <= 0) {
+ // Safe close: it's more important to alert the programmer of
+ // their error than to let them catch and continue on their way.
+ IOUtils.safeStreamClose(inputStream);
+ throw new IllegalArgumentException("Expected buffer size larger than 0. Got: " + bufferSize);
+ }
+
+ final StringBuilder stringBuilder = new StringBuilder(bufferSize);
+ final InputStreamReader reader = new InputStreamReader(inputStream, Charset.forName("UTF-8"));
+ try {
+ int charsRead;
+ final char[] buffer = new char[bufferSize];
+ while ((charsRead = reader.read(buffer, 0, bufferSize)) != -1) {
+ stringBuilder.append(buffer, 0, charsRead);
+ }
+ } finally {
+ reader.close();
+ }
+ return stringBuilder.toString();
+ }
+
+ /**
+ * A generic solution to write a JSONObject to a file.
+ * See {@link #writeStringToFile(File, String)} for more details.
+ */
+ public static void writeJSONObjectToFile(final File file, final JSONObject obj) throws IOException {
+ writeStringToFile(file, obj.toString());
+ }
+
+ /**
+ * A generic solution to write to a File - the given file will be overwritten. If it does not exist yet, it will
+ * be created. See {@link #writeStringToOutputStreamAndCloseStream(OutputStream, String)} for more details.
+ */
+ public static void writeStringToFile(final File file, final String str) throws IOException {
+ writeStringToOutputStreamAndCloseStream(new FileOutputStream(file, false), str);
+ }
+
+ /**
+ * A generic solution to write to an output stream in UTF-8. The stream will be closed at the
+ * completion of this method - it's necessary in order to close the wrapping resources.
+ *
+ * For a higher-level method, see {@link #writeStringToFile(File, String)}.
+ *
+ * Since this is generic, it may not be the most performant for your use case.
+ */
+ public static void writeStringToOutputStreamAndCloseStream(final OutputStream outputStream, final String str)
+ throws IOException {
+ try {
+ final OutputStreamWriter writer = new OutputStreamWriter(outputStream, Charset.forName("UTF-8"));
+ try {
+ writer.write(str);
+ } finally {
+ writer.close();
+ }
+ } finally {
+ // OutputStreamWriter.close can throw before closing the
+ // underlying stream. For safety, we close here too.
+ outputStream.close();
+ }
+ }
+
+ public static class FilenameWhitelistFilter implements FilenameFilter {
+ private final Set<String> mFilenameWhitelist;
+
+ public FilenameWhitelistFilter(final Set<String> filenameWhitelist) {
+ mFilenameWhitelist = filenameWhitelist;
+ }
+
+ @Override
+ public boolean accept(final File dir, final String filename) {
+ return mFilenameWhitelist.contains(filename);
+ }
+ }
+
+ public static class FilenameRegexFilter implements FilenameFilter {
+ private final Pattern mPattern;
+
+ // Each time `Pattern.matcher` is called, a new matcher is created. We can avoid the excessive object creation
+ // by caching the returned matcher and calling `Matcher.reset` on it. Since Matcher's are not thread safe,
+ // this assumes `FilenameFilter.accept` is not run in parallel (which, according to the source, it is not).
+ private Matcher mCachedMatcher;
+
+ public FilenameRegexFilter(final Pattern pattern) {
+ mPattern = pattern;
+ }
+
+ @Override
+ public boolean accept(final File dir, final String filename) {
+ if (mCachedMatcher == null) {
+ mCachedMatcher = mPattern.matcher(filename);
+ } else {
+ mCachedMatcher.reset(filename);
+ }
+ return mCachedMatcher.matches();
+ }
+ }
+
+ public static class FileLastModifiedComparator implements Comparator<File> {
+ @Override
+ public int compare(final File lhs, final File rhs) {
+ // Long.compare is API 19+.
+ final long lhsModified = lhs.lastModified();
+ final long rhsModified = rhs.lastModified();
+ if (lhsModified < rhsModified) {
+ return -1;
+ } else if (lhsModified == rhsModified) {
+ return 0;
+ } else {
+ return 1;
+ }
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/FloatUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/FloatUtils.java
new file mode 100644
index 0000000000..fbcd7254f6
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/FloatUtils.java
@@ -0,0 +1,43 @@
+/* -*- 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.PointF;
+
+import java.lang.IllegalArgumentException;
+
+public final class FloatUtils {
+ private FloatUtils() {}
+
+ public static boolean fuzzyEquals(float a, float b) {
+ return (Math.abs(a - b) < 1e-6);
+ }
+
+ public static boolean fuzzyEquals(PointF a, PointF b) {
+ return fuzzyEquals(a.x, b.x) && fuzzyEquals(a.y, b.y);
+ }
+
+ /*
+ * Returns the value that represents a linear transition between `from` and `to` at time `t`,
+ * which is on the scale [0, 1). Thus with t = 0.0f, this returns `from`; with t = 1.0f, this
+ * returns `to`; with t = 0.5f, this returns the value halfway from `from` to `to`.
+ */
+ public static float interpolate(float from, float to, float t) {
+ return from + (to - from) * t;
+ }
+
+ /**
+ * Returns 'value', clamped so that it isn't any lower than 'low', and it
+ * isn't any higher than 'high'.
+ */
+ public static float clamp(float value, float low, float high) {
+ if (high < low) {
+ throw new IllegalArgumentException(
+ "clamp called with invalid parameters (" + high + " < " + low + ")" );
+ }
+ return Math.max(low, Math.min(high, value));
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GamepadUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GamepadUtils.java
new file mode 100644
index 0000000000..e22be8fd8c
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GamepadUtils.java
@@ -0,0 +1,140 @@
+/* -*- 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.annotation.TargetApi;
+import android.os.Build;
+import android.view.InputDevice;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+
+public final class GamepadUtils {
+ private static final int SONY_XPERIA_GAMEPAD_DEVICE_ID = 196611;
+
+ private static View.OnKeyListener sClickDispatcher;
+ private static float sDeadZoneThresholdOverride = 1e-2f;
+
+ private GamepadUtils() {
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
+ private static boolean isGamepadKey(KeyEvent event) {
+ if (Build.VERSION.SDK_INT < 12) {
+ return false;
+ }
+ return (event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD;
+ }
+
+ public static boolean isActionKey(KeyEvent event) {
+ return (isGamepadKey(event) && (event.getKeyCode() == KeyEvent.KEYCODE_BUTTON_A));
+ }
+
+ public static boolean isActionKeyDown(KeyEvent event) {
+ return isActionKey(event) && event.getAction() == KeyEvent.ACTION_DOWN;
+ }
+
+ public static boolean isBackKey(KeyEvent event) {
+ return (isGamepadKey(event) && (event.getKeyCode() == KeyEvent.KEYCODE_BUTTON_B));
+ }
+
+ public static void overrideDeadZoneThreshold(float threshold) {
+ sDeadZoneThresholdOverride = threshold;
+ }
+
+ public static boolean isValueInDeadZone(MotionEvent event, int axis) {
+ float threshold;
+ if (sDeadZoneThresholdOverride >= 0) {
+ threshold = sDeadZoneThresholdOverride;
+ } else {
+ InputDevice.MotionRange range = event.getDevice().getMotionRange(axis);
+ threshold = range.getFlat() + range.getFuzz();
+ }
+ float value = event.getAxisValue(axis);
+ return (Math.abs(value) < threshold);
+ }
+
+ public static boolean isPanningControl(MotionEvent event) {
+ if (Build.VERSION.SDK_INT < 12) {
+ return false;
+ }
+ if ((event.getSource() & InputDevice.SOURCE_CLASS_MASK) != InputDevice.SOURCE_CLASS_JOYSTICK) {
+ return false;
+ }
+ if (isValueInDeadZone(event, MotionEvent.AXIS_X)
+ && isValueInDeadZone(event, MotionEvent.AXIS_Y)
+ && isValueInDeadZone(event, MotionEvent.AXIS_Z)
+ && isValueInDeadZone(event, MotionEvent.AXIS_RZ)) {
+ return false;
+ }
+ return true;
+ }
+
+ public static View.OnKeyListener getClickDispatcher() {
+ if (sClickDispatcher == null) {
+ sClickDispatcher = new View.OnKeyListener() {
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (isActionKeyDown(event)) {
+ return v.performClick();
+ }
+ return false;
+ }
+ };
+ }
+ return sClickDispatcher;
+ }
+
+ public static KeyEvent translateSonyXperiaGamepadKeys(int keyCode, KeyEvent event) {
+ // The cross and circle button mappings may be swapped in the different regions so
+ // determine if they are swapped so the proper key codes can be mapped to the keys
+ boolean areKeysSwapped = areSonyXperiaGamepadKeysSwapped();
+
+ // If a Sony Xperia, remap the cross and circle buttons to buttons
+ // A and B for the gamepad API
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_BACK:
+ keyCode = (areKeysSwapped ? KeyEvent.KEYCODE_BUTTON_A : KeyEvent.KEYCODE_BUTTON_B);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ keyCode = (areKeysSwapped ? KeyEvent.KEYCODE_BUTTON_B : KeyEvent.KEYCODE_BUTTON_A);
+ break;
+
+ default:
+ return event;
+ }
+
+ return new KeyEvent(event.getAction(), keyCode);
+ }
+
+ public static boolean isSonyXperiaGamepadKeyEvent(KeyEvent event) {
+ return (event.getDeviceId() == SONY_XPERIA_GAMEPAD_DEVICE_ID &&
+ "Sony Ericsson".equals(Build.MANUFACTURER) &&
+ ("R800".equals(Build.MODEL) || "R800i".equals(Build.MODEL)));
+ }
+
+ private static boolean areSonyXperiaGamepadKeysSwapped() {
+ // The cross and circle buttons on Sony Xperia phones are swapped
+ // in different regions
+ // http://developer.sonymobile.com/2011/02/13/xperia-play-game-keys/
+ final char DEFAULT_O_BUTTON_LABEL = 0x25CB;
+
+ boolean swapped = false;
+ int[] deviceIds = InputDevice.getDeviceIds();
+
+ for (int i = 0; deviceIds != null && i < deviceIds.length; i++) {
+ KeyCharacterMap keyCharacterMap = KeyCharacterMap.load(deviceIds[i]);
+ if (keyCharacterMap != null && DEFAULT_O_BUTTON_LABEL ==
+ keyCharacterMap.getDisplayLabel(KeyEvent.KEYCODE_DPAD_CENTER)) {
+ swapped = true;
+ break;
+ }
+ }
+ return swapped;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBackgroundThread.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBackgroundThread.java
new file mode 100644
index 0000000000..442f782e2f
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBackgroundThread.java
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.os.Handler;
+import android.os.Looper;
+
+import java.util.concurrent.SynchronousQueue;
+
+final class GeckoBackgroundThread extends Thread {
+ private static final String LOOPER_NAME = "GeckoBackgroundThread";
+
+ // Guarded by 'GeckoBackgroundThread.class'.
+ private static Handler handler;
+ private static Thread thread;
+
+ // The initial Runnable to run on the new thread. Its purpose
+ // is to avoid us having to wait for the new thread to start.
+ private Runnable initialRunnable;
+
+ // Singleton, so private constructor.
+ private GeckoBackgroundThread(final Runnable initialRunnable) {
+ this.initialRunnable = initialRunnable;
+ }
+
+ @Override
+ public void run() {
+ setName(LOOPER_NAME);
+ Looper.prepare();
+
+ synchronized (GeckoBackgroundThread.class) {
+ handler = new Handler();
+ GeckoBackgroundThread.class.notify();
+ }
+
+ if (initialRunnable != null) {
+ initialRunnable.run();
+ initialRunnable = null;
+ }
+
+ Looper.loop();
+ }
+
+ private static void startThread(final Runnable initialRunnable) {
+ thread = new GeckoBackgroundThread(initialRunnable);
+ ThreadUtils.setBackgroundThread(thread);
+
+ thread.setDaemon(true);
+ thread.start();
+ }
+
+ // Get a Handler for a looper thread, or create one if it doesn't yet exist.
+ /*package*/ static synchronized Handler getHandler() {
+ if (thread == null) {
+ startThread(null);
+ }
+
+ while (handler == null) {
+ try {
+ GeckoBackgroundThread.class.wait();
+ } catch (final InterruptedException e) {
+ }
+ }
+ return handler;
+ }
+
+ /*package*/ static synchronized void post(final Runnable runnable) {
+ if (thread == null) {
+ startThread(runnable);
+ return;
+ }
+ getHandler().post(runnable);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoEventListener.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoEventListener.java
new file mode 100644
index 0000000000..10336490b7
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoEventListener.java
@@ -0,0 +1,14 @@
+/* -*- 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 org.json.JSONObject;
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+@RobocopTarget
+public interface GeckoEventListener {
+ void handleMessage(String event, JSONObject message);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoJarReader.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoJarReader.java
new file mode 100644
index 0000000000..4e11592a4a
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoJarReader.java
@@ -0,0 +1,261 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.Log;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.mozglue.GeckoLoader;
+import org.mozilla.gecko.mozglue.NativeZip;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Stack;
+
+/* Reads out of a multiple level deep jar file such as
+ * jar:jar:file:///data/app/org.mozilla.fennec.apk!/omni.ja!/chrome/chrome/content/branding/favicon32.png
+ */
+public final class GeckoJarReader {
+ private static final String LOGTAG = "GeckoJarReader";
+
+ private GeckoJarReader() {}
+
+ public static Bitmap getBitmap(Context context, Resources resources, String url) {
+ BitmapDrawable drawable = getBitmapDrawable(context, resources, url);
+ return (drawable != null) ? drawable.getBitmap() : null;
+ }
+
+ public static BitmapDrawable getBitmapDrawable(Context context, Resources resources,
+ String url) {
+ Stack<String> jarUrls = parseUrl(url);
+ InputStream inputStream = null;
+ BitmapDrawable bitmap = null;
+
+ NativeZip zip = null;
+ try {
+ // Load the initial jar file as a zip
+ zip = getZipFile(context, jarUrls.pop());
+ inputStream = getStream(zip, jarUrls, url);
+ if (inputStream != null) {
+ bitmap = new BitmapDrawable(resources, inputStream);
+ // BitmapDrawable created from a stream does not set the correct target density from resources.
+ // In fact it discards the resources https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/graphics/java/android/graphics/drawable/BitmapDrawable.java#191
+ bitmap.setTargetDensity(resources.getDisplayMetrics());
+ }
+ } catch (IOException | URISyntaxException ex) {
+ Log.e(LOGTAG, "Exception ", ex);
+ } finally {
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException ex) {
+ Log.e(LOGTAG, "Error closing stream", ex);
+ }
+ }
+ }
+
+ return bitmap;
+ }
+
+ public static String getText(Context context, String url) {
+ Stack<String> jarUrls = parseUrl(url);
+
+ NativeZip zip = null;
+ BufferedReader reader = null;
+ String text = null;
+ try {
+ zip = getZipFile(context, jarUrls.pop());
+ InputStream input = getStream(zip, jarUrls, url);
+ if (input != null) {
+ reader = new BufferedReader(new InputStreamReader(input));
+ text = reader.readLine();
+ }
+ } catch (IOException | URISyntaxException ex) {
+ Log.e(LOGTAG, "Exception ", ex);
+ } finally {
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException ex) {
+ Log.e(LOGTAG, "Error closing reader", ex);
+ }
+ }
+ }
+
+ return text;
+ }
+
+ private static NativeZip getZipFile(Context context, String url)
+ throws IOException, URISyntaxException {
+ URI fileUrl = new URI(url);
+ GeckoLoader.loadMozGlue(context);
+ return new NativeZip(fileUrl.getPath());
+ }
+
+ @RobocopTarget
+ /**
+ * Extract a (possibly nested) file from an archive and write it to a temporary file.
+ *
+ * @param context Android context.
+ * @param url to open. Can include jar: to "reach into" nested archives.
+ * @param dir to write temporary file to.
+ * @return a <code>File</code>, if one could be written; otherwise null.
+ * @throws IOException if an error occured.
+ */
+ public static File extractStream(Context context, String url, File dir, String suffix) throws IOException {
+ InputStream input = null;
+ try {
+ try {
+ final URI fileURI = new URI(url);
+ // We don't check the scheme because we want to catch bare files, not just file:// URIs.
+ // If we let bare files through, we'd try to open them as ZIP files later -- and crash in native code.
+ if (fileURI != null && fileURI.getPath() != null) {
+ final File inputFile = new File(fileURI.getPath());
+ if (inputFile != null && inputFile.exists()) {
+ input = new FileInputStream(inputFile);
+ }
+ }
+ } catch (URISyntaxException e) {
+ // Not a file:// URI.
+ }
+ if (input == null) {
+ // No luck with file:// URI; maybe some other URI?
+ input = getStream(context, url);
+ }
+ if (input == null) {
+ // Not found!
+ return null;
+ }
+
+ // n.b.: createTempFile does not in fact delete the file.
+ final File file = File.createTempFile("extractStream", suffix, dir);
+ OutputStream output = null;
+ try {
+ output = new FileOutputStream(file);
+ byte[] buf = new byte[8192];
+ int len;
+ while ((len = input.read(buf)) >= 0) {
+ output.write(buf, 0, len);
+ }
+ return file;
+ } finally {
+ if (output != null) {
+ output.close();
+ }
+ }
+ } finally {
+ if (input != null) {
+ try {
+ input.close();
+ } catch (IOException e) {
+ Log.w(LOGTAG, "Got exception closing stream; ignoring.", e);
+ }
+ }
+ }
+ }
+
+ @RobocopTarget
+ public static InputStream getStream(Context context, String url) {
+ Stack<String> jarUrls = parseUrl(url);
+ try {
+ NativeZip zip = getZipFile(context, jarUrls.pop());
+ return getStream(zip, jarUrls, url);
+ } catch (Exception ex) {
+ // Some JNI code throws IllegalArgumentException on a bad file name;
+ // swallow the error and return null. We could also see legitimate
+ // IOExceptions here.
+ Log.e(LOGTAG, "Exception getting input stream from jar URL: " + url, ex);
+ return null;
+ }
+ }
+
+ private static InputStream getStream(NativeZip zip, Stack<String> jarUrls, String origUrl) {
+ InputStream inputStream = null;
+
+ // loop through children jar files until we reach the innermost one
+ while (!jarUrls.empty()) {
+ String fileName = jarUrls.pop();
+
+ if (inputStream != null) {
+ // intermediate NativeZips and InputStreams will be garbage collected.
+ try {
+ zip = new NativeZip(inputStream);
+ } catch (IllegalArgumentException e) {
+ String description = "!!! BUG 849589 !!! origUrl=" + origUrl;
+ Log.e(LOGTAG, description, e);
+ throw new IllegalArgumentException(description);
+ }
+ }
+
+ inputStream = zip.getInputStream(fileName);
+ if (inputStream == null) {
+ Log.d(LOGTAG, "No Entry for " + fileName);
+ return null;
+ }
+ }
+
+ return inputStream;
+ }
+
+ /* Returns a stack of strings breaking the url up into pieces. Each piece
+ * is assumed to point to a jar file except for the final one. Callers should
+ * pass in the url to parse, and null for the parent parameter (used for recursion)
+ * For example, jar:jar:file:///data/app/org.mozilla.fennec.apk!/omni.ja!/chrome/chrome/content/branding/favicon32.png
+ * will return:
+ * file:///data/app/org.mozilla.fennec.apk
+ * omni.ja
+ * chrome/chrome/content/branding/favicon32.png
+ */
+ private static Stack<String> parseUrl(String url) {
+ return parseUrl(url, null);
+ }
+
+ private static Stack<String> parseUrl(String url, Stack<String> results) {
+ if (results == null) {
+ results = new Stack<String>();
+ }
+
+ if (url.startsWith("jar:")) {
+ int jarEnd = url.lastIndexOf("!");
+ String subStr = url.substring(4, jarEnd);
+ results.push(url.substring(jarEnd + 2)); // remove the !/ characters
+ return parseUrl(subStr, results);
+ } else {
+ results.push(url);
+ return results;
+ }
+ }
+
+ public static String getJarURL(Context context, String pathInsideJAR) {
+ // We need to encode the package resource path, because it might contain illegal characters. For example:
+ // /mnt/asec2/[2]org.mozilla.fennec-1/pkg.apk
+ // The round-trip through a URI does this for us.
+ final String resourcePath = context.getPackageResourcePath();
+ return computeJarURI(resourcePath, pathInsideJAR);
+ }
+
+ /**
+ * Encodes its resource path correctly.
+ */
+ @RobocopTarget
+ public static String computeJarURI(String resourcePath, String pathInsideJAR) {
+ final String resURI = new File(resourcePath).toURI().toString();
+
+ // TODO: do we need to encode the file path, too?
+ return "jar:jar:" + resURI + "!/" + AppConstants.OMNIJAR_NAME + "!/" + pathInsideJAR;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoRequest.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoRequest.java
new file mode 100644
index 0000000000..a57ed7f082
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoRequest.java
@@ -0,0 +1,94 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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 java.util.concurrent.atomic.AtomicInteger;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+import android.util.Log;
+
+public abstract class GeckoRequest {
+ private static final String LOGTAG = "GeckoRequest";
+ private static final AtomicInteger currentId = new AtomicInteger(0);
+
+ private final int id = currentId.getAndIncrement();
+ private final String name;
+ private final String data;
+
+ /**
+ * Creates a request that can be dispatched using
+ * {@link GeckoAppShell#sendRequestToGecko(GeckoRequest)}.
+ *
+ * @param name The name of the event associated with this request, which must have a
+ * Gecko-side listener registered to respond to this request.
+ * @param data Data to send with this request, which can be any object serializable by
+ * {@link JSONObject#put(String, Object)}.
+ */
+ @RobocopTarget
+ public GeckoRequest(String name, Object data) {
+ this.name = name;
+ final JSONObject message = new JSONObject();
+ try {
+ message.put("id", id);
+ message.put("data", data);
+ } catch (JSONException e) {
+ Log.e(LOGTAG, "JSON error", e);
+ }
+ this.data = message.toString();
+ }
+
+ /**
+ * Gets the ID for this request.
+ *
+ * @return The request ID
+ */
+ public int getId() {
+ return id;
+ }
+
+ /**
+ * Gets the event name associated with this request.
+ *
+ * @return The name of the event sent to Gecko
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Gets the stringified data associated with this request.
+ *
+ * @return The data being sent with the request
+ */
+ public String getData() {
+ return data;
+ }
+
+ /**
+ * Callback executed when the request succeeds.
+ *
+ * @param nativeJSObject The response data from Gecko
+ */
+ @RobocopTarget
+ public abstract void onResponse(NativeJSObject nativeJSObject);
+
+ /**
+ * Callback executed when the request fails.
+ *
+ * By default, an exception is thrown. This should be overridden if the
+ * GeckoRequest is able to recover from the error.
+ *
+ * @throws RuntimeException
+ */
+ @RobocopTarget
+ public void onError(NativeJSObject error) {
+ final String message = error.optString("message", "<no message>");
+ final String stack = error.optString("stack", "<no stack>");
+ throw new RuntimeException("Unhandled error for GeckoRequest " + name + ": " + message + "\nJS stack:\n" + stack);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java
new file mode 100644
index 0000000000..864462d9b1
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java
@@ -0,0 +1,169 @@
+/* -*- 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 org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.AppConstants.Versions;
+
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecList;
+import android.util.Log;
+
+public final class HardwareCodecCapabilityUtils {
+ private static final String LOGTAG = "GeckoHardwareCodecCapabilityUtils";
+
+ // List of supported HW VP8 encoders.
+ private static final String[] supportedVp8HwEncCodecPrefixes =
+ {"OMX.qcom.", "OMX.Intel." };
+ // List of supported HW VP8 decoders.
+ private static final String[] supportedVp8HwDecCodecPrefixes =
+ {"OMX.qcom.", "OMX.Nvidia.", "OMX.Exynos.", "OMX.Intel." };
+ private static final String VP8_MIME_TYPE = "video/x-vnd.on2.vp8";
+ private static final String VP9_MIME_TYPE = "video/x-vnd.on2.vp9";
+ // NV12 color format supported by QCOM codec, but not declared in MediaCodec -
+ // see /hardware/qcom/media/mm-core/inc/OMX_QCOMExtns.h
+ private static final int
+ COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m = 0x7FA30C04;
+ // Allowable color formats supported by codec - in order of preference.
+ private static final int[] supportedColorList = {
+ CodecCapabilities.COLOR_FormatYUV420Planar,
+ CodecCapabilities.COLOR_FormatYUV420SemiPlanar,
+ CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar,
+ COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m
+ };
+
+ @WrapForJNI
+ public static boolean findDecoderCodecInfoForMimeType(String aMimeType) {
+ for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) {
+ MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
+ if (info.isEncoder()) {
+ continue;
+ }
+ for (String mimeType : info.getSupportedTypes()) {
+ if (mimeType.equals(aMimeType)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public static boolean getHWEncoderCapability() {
+ if (Versions.feature20Plus) {
+ for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) {
+ MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
+ if (!info.isEncoder()) {
+ continue;
+ }
+ String name = null;
+ for (String mimeType : info.getSupportedTypes()) {
+ if (mimeType.equals(VP8_MIME_TYPE)) {
+ name = info.getName();
+ break;
+ }
+ }
+ if (name == null) {
+ continue; // No HW support in this codec; try the next one.
+ }
+ Log.e(LOGTAG, "Found candidate encoder " + name);
+
+ // Check if this is supported encoder.
+ boolean supportedCodec = false;
+ for (String codecPrefix : supportedVp8HwEncCodecPrefixes) {
+ if (name.startsWith(codecPrefix)) {
+ supportedCodec = true;
+ break;
+ }
+ }
+ if (!supportedCodec) {
+ continue;
+ }
+
+ // Check if codec supports either yuv420 or nv12.
+ CodecCapabilities capabilities =
+ info.getCapabilitiesForType(VP8_MIME_TYPE);
+ for (int colorFormat : capabilities.colorFormats) {
+ Log.v(LOGTAG, " Color: 0x" + Integer.toHexString(colorFormat));
+ }
+ for (int supportedColorFormat : supportedColorList) {
+ for (int codecColorFormat : capabilities.colorFormats) {
+ if (codecColorFormat == supportedColorFormat) {
+ // Found supported HW Encoder.
+ Log.e(LOGTAG, "Found target encoder " + name +
+ ". Color: 0x" + Integer.toHexString(codecColorFormat));
+ return true;
+ }
+ }
+ }
+ }
+ }
+ // No HW encoder.
+ return false;
+ }
+
+ public static boolean getHWDecoderCapability() {
+ return getHWDecoderCapability(VP8_MIME_TYPE);
+ }
+
+ @WrapForJNI
+ public static boolean HasHWVP9() {
+ return getHWDecoderCapability(VP9_MIME_TYPE);
+ }
+
+ public static boolean getHWDecoderCapability(String aMimeType) {
+ if (Versions.feature20Plus) {
+ for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) {
+ MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
+ if (info.isEncoder()) {
+ continue;
+ }
+ String name = null;
+ for (String mimeType : info.getSupportedTypes()) {
+ if (mimeType.equals(aMimeType)) {
+ name = info.getName();
+ break;
+ }
+ }
+ if (name == null) {
+ continue; // No HW support in this codec; try the next one.
+ }
+ Log.e(LOGTAG, "Found candidate decoder " + name);
+
+ // Check if this is supported decoder.
+ boolean supportedCodec = false;
+ for (String codecPrefix : supportedVp8HwDecCodecPrefixes) {
+ if (name.startsWith(codecPrefix)) {
+ supportedCodec = true;
+ break;
+ }
+ }
+ if (!supportedCodec) {
+ continue;
+ }
+
+ // Check if codec supports either yuv420 or nv12.
+ CodecCapabilities capabilities =
+ info.getCapabilitiesForType(aMimeType);
+ for (int colorFormat : capabilities.colorFormats) {
+ Log.v(LOGTAG, " Color: 0x" + Integer.toHexString(colorFormat));
+ }
+ for (int supportedColorFormat : supportedColorList) {
+ for (int codecColorFormat : capabilities.colorFormats) {
+ if (codecColorFormat == supportedColorFormat) {
+ // Found supported HW decoder.
+ Log.e(LOGTAG, "Found target decoder " + name +
+ ". Color: 0x" + Integer.toHexString(codecColorFormat));
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false; // No HW decoder.
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareUtils.java
new file mode 100644
index 0000000000..ba92d08cb5
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareUtils.java
@@ -0,0 +1,117 @@
+/* -*- 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 org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.SysInfo;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.os.Build;
+import android.util.Log;
+import android.view.ViewConfiguration;
+
+public final class HardwareUtils {
+ private static final String LOGTAG = "GeckoHardwareUtils";
+
+ private static final boolean IS_AMAZON_DEVICE = Build.MANUFACTURER.equalsIgnoreCase("Amazon");
+ public static final boolean IS_KINDLE_DEVICE = IS_AMAZON_DEVICE &&
+ (Build.MODEL.equals("Kindle Fire") ||
+ Build.MODEL.startsWith("KF"));
+
+ private static volatile boolean sInited;
+
+ // These are all set once, during init.
+ private static volatile boolean sIsLargeTablet;
+ private static volatile boolean sIsSmallTablet;
+ private static volatile boolean sIsTelevision;
+
+ private HardwareUtils() {
+ }
+
+ public static void init(Context context) {
+ if (sInited) {
+ // This is unavoidable, given that HardwareUtils is called from background services.
+ Log.d(LOGTAG, "HardwareUtils already inited.");
+ return;
+ }
+
+ // Pre-populate common flags from the context.
+ final int screenLayoutSize = context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
+ if (Build.VERSION.SDK_INT >= 11) {
+ if (screenLayoutSize == Configuration.SCREENLAYOUT_SIZE_XLARGE) {
+ sIsLargeTablet = true;
+ } else if (screenLayoutSize == Configuration.SCREENLAYOUT_SIZE_LARGE) {
+ sIsSmallTablet = true;
+ }
+ if (Build.VERSION.SDK_INT >= 16) {
+ if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
+ sIsTelevision = true;
+ }
+ }
+ }
+
+ sInited = true;
+ }
+
+ public static boolean isTablet() {
+ return sIsLargeTablet || sIsSmallTablet;
+ }
+
+ public static boolean isLargeTablet() {
+ return sIsLargeTablet;
+ }
+
+ public static boolean isSmallTablet() {
+ return sIsSmallTablet;
+ }
+
+ public static boolean isTelevision() {
+ return sIsTelevision;
+ }
+
+ public static int getMemSize() {
+ return SysInfo.getMemSize();
+ }
+
+ public static boolean isARMSystem() {
+ return Build.CPU_ABI != null && Build.CPU_ABI.equals("armeabi-v7a");
+ }
+
+ public static boolean isX86System() {
+ return Build.CPU_ABI != null && Build.CPU_ABI.equals("x86");
+ }
+
+ /**
+ * @return false if the current system is not supported (e.g. APK/system ABI mismatch).
+ */
+ public static boolean isSupportedSystem() {
+ if (Build.VERSION.SDK_INT < AppConstants.Versions.MIN_SDK_VERSION ||
+ Build.VERSION.SDK_INT > AppConstants.Versions.MAX_SDK_VERSION) {
+ return false;
+ }
+
+ // See http://developer.android.com/ndk/guides/abis.html
+ final boolean isSystemARM = isARMSystem();
+ final boolean isSystemX86 = isX86System();
+
+ final boolean isAppARM = AppConstants.ANDROID_CPU_ARCH.startsWith("armeabi-v7a");
+ final boolean isAppX86 = AppConstants.ANDROID_CPU_ARCH.startsWith("x86");
+
+ // Only reject known incompatible ABIs. Better safe than sorry.
+ if ((isSystemX86 && isAppARM) || (isSystemARM && isAppX86)) {
+ return false;
+ }
+
+ if ((isSystemX86 && isAppX86) || (isSystemARM && isAppARM)) {
+ return true;
+ }
+
+ Log.w(LOGTAG, "Unknown app/system ABI combination: " + AppConstants.MOZ_APP_ABI + " / " + Build.CPU_ABI);
+ return true;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/INIParser.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/INIParser.java
new file mode 100644
index 0000000000..ed0706320d
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/INIParser.java
@@ -0,0 +1,176 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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 java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.Hashtable;
+
+public final class INIParser extends INISection {
+ // default file to read and write to
+ private final File mFile;
+
+ // List of sections in the current iniFile. null if the file has not been parsed yet
+ private Hashtable<String, INISection> mSections;
+
+ // create a parser. The file will not be read until you attempt to
+ // access sections or properties inside it. At that point its read synchronously
+ public INIParser(File iniFile) {
+ super("");
+ mFile = iniFile;
+ }
+
+ // write ini data to the default file. Will overwrite anything current inside
+ public void write() {
+ writeTo(mFile);
+ }
+
+ // write to the specified file. Will overwrite anything current inside
+ public void writeTo(File f) {
+ if (f == null)
+ return;
+
+ FileWriter outputStream = null;
+ try {
+ outputStream = new FileWriter(f);
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ }
+
+ BufferedWriter writer = new BufferedWriter(outputStream);
+ try {
+ write(writer);
+ writer.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void write(BufferedWriter writer) throws IOException {
+ super.write(writer);
+
+ if (mSections != null) {
+ for (Enumeration<INISection> e = mSections.elements(); e.hasMoreElements();) {
+ INISection section = e.nextElement();
+ section.write(writer);
+ writer.newLine();
+ }
+ }
+ }
+
+ // return all of the sections inside this file
+ public Hashtable<String, INISection> getSections() {
+ if (mSections == null) {
+ try {
+ parse();
+ } catch (IOException e) {
+ debug("Error parsing: " + e);
+ }
+ }
+ return mSections;
+ }
+
+ // parse the default file
+ @Override
+ protected void parse() throws IOException {
+ super.parse();
+ parse(mFile);
+ }
+
+ // parse a passed in file
+ private void parse(File f) throws IOException {
+ // Set up internal data members
+ mSections = new Hashtable<String, INISection>();
+
+ if (f == null || !f.exists())
+ return;
+
+ FileReader inputStream = null;
+ try {
+ inputStream = new FileReader(f);
+ } catch (FileNotFoundException e1) {
+ // If the file doesn't exist. Just return;
+ return;
+ }
+
+ BufferedReader buf = new BufferedReader(inputStream);
+ String line = null; // current line of text we are parsing
+ INISection currentSection = null; // section we are currently parsing
+
+ while ((line = buf.readLine()) != null) {
+
+ if (line != null)
+ line = line.trim();
+
+ // blank line or a comment. ignore it
+ if (line == null || line.length() == 0 || line.charAt(0) == ';') {
+ debug("Ignore line: " + line);
+ } else if (line.charAt(0) == '[') {
+ debug("Parse as section: " + line);
+ currentSection = new INISection(line.substring(1, line.length() - 1));
+ mSections.put(currentSection.getName(), currentSection);
+ } else {
+ debug("Parse as property: " + line);
+
+ String[] pieces = line.split("=");
+ if (pieces.length != 2)
+ continue;
+
+ String key = pieces[0].trim();
+ String value = pieces[1].trim();
+ if (currentSection != null) {
+ currentSection.setProperty(key, value);
+ } else {
+ mProperties.put(key, value);
+ }
+ }
+ }
+ buf.close();
+ }
+
+ // add a section to the file
+ public void addSection(INISection sect) {
+ // ensure that we have parsed the file
+ getSections();
+ mSections.put(sect.getName(), sect);
+ }
+
+ // get a section from the file. will return null if the section doesn't exist
+ public INISection getSection(String key) {
+ // ensure that we have parsed the file
+ getSections();
+ return mSections.get(key);
+ }
+
+ // remove an entire section from the file
+ public void removeSection(String name) {
+ // ensure that we have parsed the file
+ getSections();
+ mSections.remove(name);
+ }
+
+ // rename a section; nuking any previous section with the new
+ // name in the process
+ public void renameSection(String oldName, String newName) {
+ // ensure that we have parsed the file
+ getSections();
+
+ mSections.remove(newName);
+ INISection section = mSections.get(oldName);
+ if (section == null)
+ return;
+
+ section.setName(newName);
+ mSections.remove(oldName);
+ mSections.put(newName, section);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/INISection.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/INISection.java
new file mode 100644
index 0000000000..af91ad4108
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/INISection.java
@@ -0,0 +1,123 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.text.TextUtils;
+import android.util.Log;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.Hashtable;
+
+public class INISection {
+ private static final String LOGTAG = "INIParser";
+
+ // default file to read and write to
+ private String mName;
+ public String getName() { return mName; }
+ public void setName(String name) { mName = name; }
+
+ // show or hide debug logging
+ private boolean mDebug;
+
+ // Global properties that aren't inside a section in the file
+ protected Hashtable<String, Object> mProperties;
+
+ // create a parser. The file will not be read until you attempt to
+ // access sections or properties inside it. At that point its read synchronously
+ public INISection(String name) {
+ mName = name;
+ }
+
+ // log a debug string to the console
+ protected void debug(String msg) {
+ if (mDebug) {
+ Log.i(LOGTAG, msg);
+ }
+ }
+
+ // get a global property out of the hash table. will return null if the property doesn't exist
+ public Object getProperty(String key) {
+ getProperties(); // ensure that we have parsed the file
+ return mProperties.get(key);
+ }
+
+ // get a global property out of the hash table. will return null if the property doesn't exist
+ public int getIntProperty(String key) {
+ Object val = getProperty(key);
+ if (val == null)
+ return -1;
+
+ return Integer.parseInt(val.toString());
+ }
+
+ // get a global property out of the hash table. will return null if the property doesn't exist
+ public String getStringProperty(String key) {
+ Object val = getProperty(key);
+ if (val == null)
+ return null;
+
+ return val.toString();
+ }
+
+ // get a hashtable of all the global properties in this file
+ public Hashtable<String, Object> getProperties() {
+ if (mProperties == null) {
+ try {
+ parse();
+ } catch (IOException e) {
+ debug("Error parsing: " + e);
+ }
+ }
+ return mProperties;
+ }
+
+ // do nothing for generic sections
+ protected void parse() throws IOException {
+ mProperties = new Hashtable<String, Object>();
+ }
+
+ // set a property. Will erase the property if value = null
+ public void setProperty(String key, Object value) {
+ getProperties(); // ensure that we have parsed the file
+ if (value == null)
+ removeProperty(key);
+ else
+ mProperties.put(key.trim(), value);
+ }
+
+ // remove a property
+ public void removeProperty(String name) {
+ // ensure that we have parsed the file
+ getProperties();
+ mProperties.remove(name);
+ }
+
+ public void write(BufferedWriter writer) throws IOException {
+ if (!TextUtils.isEmpty(mName)) {
+ writer.write("[" + mName + "]");
+ writer.newLine();
+ }
+
+ if (mProperties != null) {
+ for (Enumeration<String> e = mProperties.keys(); e.hasMoreElements();) {
+ String key = e.nextElement();
+ writeProperty(writer, key, mProperties.get(key));
+ }
+ }
+ writer.newLine();
+ }
+
+ // Helper function to write out a property
+ private void writeProperty(BufferedWriter writer, String key, Object value) {
+ try {
+ writer.write(key + "=" + value);
+ writer.newLine();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IOUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IOUtils.java
new file mode 100644
index 0000000000..62eee51923
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IOUtils.java
@@ -0,0 +1,129 @@
+/* -*- 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.util;
+
+import android.util.Log;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Static helper class containing useful methods for manipulating IO objects.
+ */
+public class IOUtils {
+ private static final String LOGTAG = "GeckoIOUtils";
+
+ /**
+ * Represents the result of consuming an input stream, holding the returned data as well
+ * as the length of the data returned.
+ * The byte[] is not guaranteed to be trimmed to the size of the data acquired from the stream:
+ * hence the need for the length field. This strategy avoids the need to copy the data into a
+ * trimmed buffer after consumption.
+ */
+ public static class ConsumedInputStream {
+ public final int consumedLength;
+ // Only reassigned in getTruncatedData.
+ private byte[] consumedData;
+
+ public ConsumedInputStream(int consumedLength, byte[] consumedData) {
+ this.consumedLength = consumedLength;
+ this.consumedData = consumedData;
+ }
+
+ /**
+ * Get the data trimmed to the length of the actual payload read, caching the result.
+ */
+ public byte[] getTruncatedData() {
+ if (consumedData.length == consumedLength) {
+ return consumedData;
+ }
+
+ consumedData = truncateBytes(consumedData, consumedLength);
+ return consumedData;
+ }
+
+ public byte[] getData() {
+ return consumedData;
+ }
+ }
+
+ /**
+ * Fully read an InputStream into a byte array.
+ * @param iStream the InputStream to consume.
+ * @param bufferSize The initial size of the buffer to allocate. It will be grown as
+ * needed, but if the caller knows something about the InputStream then
+ * passing a good value here can improve performance.
+ */
+ public static ConsumedInputStream readFully(InputStream iStream, int bufferSize) {
+ // Allocate a buffer to hold the raw data downloaded.
+ byte[] buffer = new byte[bufferSize];
+
+ // The offset of the start of the buffer's free space.
+ int bPointer = 0;
+
+ // The quantity of bytes the last call to read yielded.
+ int lastRead = 0;
+ try {
+ // Fully read the data into the buffer.
+ while (lastRead != -1) {
+ // Read as many bytes as are currently available into the buffer.
+ lastRead = iStream.read(buffer, bPointer, buffer.length - bPointer);
+ bPointer += lastRead;
+
+ // If buffer has overflowed, double its size and carry on.
+ if (bPointer == buffer.length) {
+ bufferSize *= 2;
+ byte[] newBuffer = new byte[bufferSize];
+
+ // Copy the contents of the old buffer into the new buffer.
+ System.arraycopy(buffer, 0, newBuffer, 0, buffer.length);
+ buffer = newBuffer;
+ }
+ }
+
+ return new ConsumedInputStream(bPointer + 1, buffer);
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Error consuming input stream.", e);
+ } finally {
+ try {
+ iStream.close();
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Error closing input stream.", e);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Truncate a given byte[] to a given length. Returns a new byte[] with the first length many
+ * bytes of the input.
+ */
+ public static byte[] truncateBytes(byte[] bytes, int length) {
+ byte[] newBytes = new byte[length];
+ System.arraycopy(bytes, 0, newBytes, 0, length);
+
+ return newBytes;
+ }
+
+ public static void safeStreamClose(Closeable stream) {
+ try {
+ if (stream != null)
+ stream.close();
+ } catch (IOException e) { }
+ }
+
+ public static void copy(InputStream in, OutputStream out) throws IOException {
+ byte[] buffer = new byte[4096];
+ int len;
+
+ while ((len = in.read(buffer)) != -1) {
+ out.write(buffer, 0, len);
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/InputOptionsUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/InputOptionsUtils.java
new file mode 100644
index 0000000000..55c02e4daa
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/InputOptionsUtils.java
@@ -0,0 +1,45 @@
+/* -*- 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.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.speech.RecognizerIntent;
+
+public class InputOptionsUtils {
+ public static boolean supportsVoiceRecognizer(Context context, String prompt) {
+ final Intent intent = createVoiceRecognizerIntent(prompt);
+ return intent.resolveActivity(context.getPackageManager()) != null;
+ }
+
+ public static Intent createVoiceRecognizerIntent(String prompt) {
+ final Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
+ intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
+ intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1);
+ intent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
+ return intent;
+ }
+
+ public static boolean supportsIntent(Intent intent, Context context) {
+ return intent.resolveActivity(context.getPackageManager()) != null;
+ }
+
+ public static boolean supportsQrCodeReader(Context context) {
+ final Intent intent = createQRCodeReaderIntent();
+ return supportsIntent(intent, context);
+ }
+
+ public static Intent createQRCodeReaderIntent() {
+ // Bug 602818 enables QR code input if you have the particular app below installed in your device
+ final String appPackage = "com.google.zxing.client.android";
+
+ Intent intent = new Intent(appPackage + ".SCAN");
+ intent.setPackage(appPackage);
+ intent.putExtra("SCAN_MODE", "QR_CODE_MODE");
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ return intent;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java
new file mode 100644
index 0000000000..d4fe297da5
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java
@@ -0,0 +1,109 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.Intent;
+import android.os.Bundle;
+import android.support.annotation.CheckResult;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+import org.mozilla.gecko.mozglue.SafeIntent;
+
+import java.util.HashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utilities for Intents.
+ */
+public class IntentUtils {
+ public static final String ENV_VAR_IN_AUTOMATION = "MOZ_IN_AUTOMATION";
+
+ private static final String ENV_VAR_REGEX = "(.+)=(.*)";
+
+ private IntentUtils() {}
+
+ /**
+ * Returns a list of environment variables and their values. These are parsed from an Intent extra
+ * with the key -> value format:
+ * env# -> ENV_VAR=VALUE
+ *
+ * # in env# is expected to be increasing from 0.
+ *
+ * @return A Map of environment variable name to value, e.g. ENV_VAR -> VALUE
+ */
+ public static HashMap<String, String> getEnvVarMap(@NonNull final SafeIntent intent) {
+ // Optimization: get matcher for re-use. Pattern.matcher creates a new object every time so it'd be great
+ // to avoid the unnecessary allocation, particularly because we expect to be called on the startup path.
+ final Pattern envVarPattern = Pattern.compile(ENV_VAR_REGEX);
+ final Matcher matcher = envVarPattern.matcher(""); // argument does not matter here.
+
+ // This is expected to be an external intent so we should use SafeIntent to prevent crashing.
+ final HashMap<String, String> out = new HashMap<>();
+ int i = 0;
+ while (true) {
+ final String envKey = "env" + i;
+ i += 1;
+ if (!intent.hasExtra(envKey)) {
+ break;
+ }
+
+ maybeAddEnvVarToEnvVarMap(out, intent, envKey, matcher);
+ }
+ return out;
+ }
+
+ /**
+ * @param envVarMap the map to add the env var to
+ * @param intent the intent from which to extract the env var
+ * @param envKey the key at which the env var resides
+ * @param envVarMatcher a matcher initialized with the env var pattern to extract
+ */
+ private static void maybeAddEnvVarToEnvVarMap(@NonNull final HashMap<String, String> envVarMap,
+ @NonNull final SafeIntent intent, @NonNull final String envKey, @NonNull final Matcher envVarMatcher) {
+ final String envValue = intent.getStringExtra(envKey);
+ if (envValue == null) {
+ return; // nothing to do here!
+ }
+
+ envVarMatcher.reset(envValue);
+ if (envVarMatcher.matches()) {
+ final String envVarName = envVarMatcher.group(1);
+ final String envVarValue = envVarMatcher.group(2);
+ envVarMap.put(envVarName, envVarValue);
+ }
+ }
+
+ public static Bundle getBundleExtraSafe(final Intent intent, final String name) {
+ return new SafeIntent(intent).getBundleExtra(name);
+ }
+
+ public static String getStringExtraSafe(final Intent intent, final String name) {
+ return new SafeIntent(intent).getStringExtra(name);
+ }
+
+ public static boolean getBooleanExtraSafe(final Intent intent, final String name, final boolean defaultValue) {
+ return new SafeIntent(intent).getBooleanExtra(name, defaultValue);
+ }
+
+ /**
+ * Gets whether or not we're in automation from the passed in environment variables.
+ *
+ * We need to read environment variables from the intent string
+ * extra because environment variables from our test harness aren't set
+ * until Gecko is loaded, and we need to know this before then.
+ *
+ * The return value of this method should be used early since other
+ * initialization may depend on its results.
+ */
+ @CheckResult
+ public static boolean getIsInAutomationFromEnvironment(final SafeIntent intent) {
+ final HashMap<String, String> envVars = IntentUtils.getEnvVarMap(intent);
+ return !TextUtils.isEmpty(envVars.get(IntentUtils.ENV_VAR_IN_AUTOMATION));
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/JSONUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/JSONUtils.java
new file mode 100644
index 0000000000..4ec98ec9e7
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/JSONUtils.java
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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 org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.os.Bundle;
+import android.util.Log;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+
+public final class JSONUtils {
+ private static final String LOGTAG = "GeckoJSONUtils";
+
+ private JSONUtils() {}
+
+ public static UUID getUUID(String name, JSONObject json) {
+ String uuid = json.optString(name, null);
+ return (uuid != null) ? UUID.fromString(uuid) : null;
+ }
+
+ public static void putUUID(String name, UUID uuid, JSONObject json) {
+ String uuidString = uuid.toString();
+ try {
+ json.put(name, uuidString);
+ } catch (JSONException e) {
+ throw new IllegalArgumentException(name + "=" + uuidString, e);
+ }
+ }
+
+ public static JSONObject bundleToJSON(Bundle bundle) {
+ if (bundle == null || bundle.isEmpty()) {
+ return null;
+ }
+
+ JSONObject json = new JSONObject();
+ for (String key : bundle.keySet()) {
+ try {
+ json.put(key, bundle.get(key));
+ } catch (JSONException e) {
+ Log.w(LOGTAG, "Error building JSON response.", e);
+ }
+ }
+
+ return json;
+ }
+
+ // Handles conversions between a JSONArray and a Set<String>
+ public static Set<String> parseStringSet(JSONArray json) {
+ final Set<String> ret = new HashSet<String>();
+
+ for (int i = 0; i < json.length(); i++) {
+ try {
+ ret.add(json.getString(i));
+ } catch (JSONException ex) {
+ Log.i(LOGTAG, "Error parsing json", ex);
+ }
+ }
+
+ return ret;
+ }
+
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/MenuUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/MenuUtils.java
new file mode 100644
index 0000000000..e44fdd541a
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/MenuUtils.java
@@ -0,0 +1,33 @@
+/* -*- 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.view.Menu;
+import android.view.MenuItem;
+
+public class MenuUtils {
+ /*
+ * This method looks for a menuitem and sets it's visible state, if
+ * it exists.
+ */
+ public static void safeSetVisible(Menu menu, int id, boolean visible) {
+ MenuItem item = menu.findItem(id);
+ if (item != null) {
+ item.setVisible(visible);
+ }
+ }
+
+ /*
+ * This method looks for a menuitem and sets it's enabled state, if
+ * it exists.
+ */
+ public static void safeSetEnabled(Menu menu, int id, boolean enabled) {
+ MenuItem item = menu.findItem(id);
+ if (item != null) {
+ item.setEnabled(enabled);
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeEventListener.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeEventListener.java
new file mode 100644
index 0000000000..2a1b6e89a8
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeEventListener.java
@@ -0,0 +1,23 @@
+/* -*- 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 org.mozilla.gecko.annotation.RobocopTarget;
+
+@RobocopTarget
+public interface NativeEventListener {
+ /**
+ * Handles a message sent from Gecko.
+ *
+ * @param event The name of the event being sent.
+ * @param message The message data.
+ * @param callback The callback interface for this message. A callback is provided only if the
+ * originating Messaging.sendRequest call included a callback argument; otherwise,
+ * callback will be null. All listeners for a given event are given the same
+ * callback object, and exactly one listener must handle the callback.
+ */
+ void handleMessage(String event, NativeJSObject message, EventCallback callback);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeJSContainer.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeJSContainer.java
new file mode 100644
index 0000000000..daefe6de0d
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeJSContainer.java
@@ -0,0 +1,37 @@
+/* -*- 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.util;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+/**
+ * NativeJSContainer is a wrapper around the SpiderMonkey JSAPI to make it possible to
+ * access Javascript objects in Java.
+ *
+ * A container must only be used on the thread it is attached to. To use it on another
+ * thread, call {@link #clone()} to make a copy, and use the copy on the other thread.
+ * When a copy is first used, it becomes attached to the thread using it.
+ */
+@WrapForJNI(calledFrom = "gecko")
+public final class NativeJSContainer extends NativeJSObject
+{
+ private NativeJSContainer() {
+ }
+
+ /**
+ * Make a copy of this container for use by another thread. When the copy is first used,
+ * it becomes attached to the thread using it.
+ */
+ @Override
+ public native NativeJSContainer clone();
+
+ /**
+ * Dispose all associated native objects. Subsequent use of any objects derived from
+ * this container will throw a NullPointerException.
+ */
+ @Override
+ public native void disposeNative();
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeJSObject.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeJSObject.java
new file mode 100644
index 0000000000..0d1f0a0375
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NativeJSObject.java
@@ -0,0 +1,533 @@
+/* -*- 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.util;
+
+import org.mozilla.gecko.annotation.JNITarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.JNIObject;
+
+import android.os.Bundle;
+
+/**
+ * NativeJSObject is a wrapper around the SpiderMonkey JSAPI to make it possible to
+ * access Javascript objects in Java.
+ */
+@WrapForJNI(calledFrom = "gecko")
+public class NativeJSObject extends JNIObject
+{
+ @SuppressWarnings("serial")
+ @JNITarget
+ public static final class InvalidPropertyException extends RuntimeException {
+ public InvalidPropertyException(final String msg) {
+ super(msg);
+ }
+ }
+
+ protected NativeJSObject() {
+ }
+
+ @Override
+ protected void disposeNative() {
+ // NativeJSObject is disposed as part of NativeJSContainer disposal.
+ }
+
+ /**
+ * Returns the value of a boolean property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native boolean getBoolean(String name);
+
+ /**
+ * Returns the value of a boolean property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native boolean optBoolean(String name, boolean fallback);
+
+ /**
+ * Returns the value of a boolean array property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native boolean[] getBooleanArray(String name);
+
+ /**
+ * Returns the value of a boolean array property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native boolean[] optBooleanArray(String name, boolean[] fallback);
+
+ /**
+ * Returns the value of an object property as a Bundle.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native Bundle getBundle(String name);
+
+ /**
+ * Returns the value of an object property as a Bundle.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native Bundle optBundle(String name, Bundle fallback);
+
+ /**
+ * Returns the value of an object array property as a Bundle array.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native Bundle[] getBundleArray(String name);
+
+ /**
+ * Returns the value of an object array property as a Bundle array.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native Bundle[] optBundleArray(String name, Bundle[] fallback);
+
+ /**
+ * Returns the value of a double property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native double getDouble(String name);
+
+ /**
+ * Returns the value of a double property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native double optDouble(String name, double fallback);
+
+ /**
+ * Returns the value of a double array property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native double[] getDoubleArray(String name);
+
+ /**
+ * Returns the value of a double array property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native double[] optDoubleArray(String name, double[] fallback);
+
+ /**
+ * Returns the value of an int property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native int getInt(String name);
+
+ /**
+ * Returns the value of an int property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native int optInt(String name, int fallback);
+
+ /**
+ * Returns the value of an int array property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native int[] getIntArray(String name);
+
+ /**
+ * Returns the value of an int array property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native int[] optIntArray(String name, int[] fallback);
+
+ /**
+ * Returns the value of an object property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native NativeJSObject getObject(String name);
+
+ /**
+ * Returns the value of an object property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native NativeJSObject optObject(String name, NativeJSObject fallback);
+
+ /**
+ * Returns the value of an object array property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native NativeJSObject[] getObjectArray(String name);
+
+ /**
+ * Returns the value of an object array property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native NativeJSObject[] optObjectArray(String name, NativeJSObject[] fallback);
+
+ /**
+ * Returns the value of a string property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native String getString(String name);
+
+ /**
+ * Returns the value of a string property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native String optString(String name, String fallback);
+
+ /**
+ * Returns the value of a string array property.
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property does not exist or if its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native String[] getStringArray(String name);
+
+ /**
+ * Returns the value of a string array property.
+ *
+ * @param name
+ * Property name
+ * @param fallback
+ * Value to return if property does not exist
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws InvalidPropertyException
+ * If the property exists and its type does not match the return type
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native String[] optStringArray(String name, String[] fallback);
+
+ /**
+ * Returns whether a property exists in this object
+ *
+ * @param name
+ * Property name
+ * @throws IllegalArgumentException
+ * If name is null
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native boolean has(String name);
+
+ /**
+ * Returns the Bundle representation of this object.
+ *
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ public native Bundle toBundle();
+
+ /**
+ * Returns the JSON representation of this object.
+ *
+ * @throws NullPointerException
+ * If this JS object has been disposed
+ * @throws IllegalThreadStateException
+ * If not called on the thread this object is attached to
+ * @throws UnsupportedOperationException
+ * If an internal JSAPI call failed
+ */
+ @Override
+ public native String toString();
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NetworkUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NetworkUtils.java
new file mode 100644
index 0000000000..2210e43edf
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NetworkUtils.java
@@ -0,0 +1,177 @@
+/* -*- 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.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.support.annotation.Nullable;
+import android.support.annotation.NonNull;
+import android.telephony.TelephonyManager;
+
+public class NetworkUtils {
+ /*
+ * Keep the below constants in sync with
+ * http://dxr.mozilla.org/mozilla-central/source/netwerk/base/nsINetworkLinkService.idl
+ */
+ public enum ConnectionSubType {
+ CELL_2G("2g"),
+ CELL_3G("3g"),
+ CELL_4G("4g"),
+ ETHERNET("ethernet"),
+ WIFI("wifi"),
+ WIMAX("wimax"),
+ UNKNOWN("unknown");
+
+ public final String value;
+ ConnectionSubType(String value) {
+ this.value = value;
+ }
+ }
+
+ /*
+ * Keep the below constants in sync with
+ * http://dxr.mozilla.org/mozilla-central/source/netwerk/base/nsINetworkLinkService.idl
+ */
+ public enum NetworkStatus {
+ UP("up"),
+ DOWN("down"),
+ UNKNOWN("unknown");
+
+ public final String value;
+
+ NetworkStatus(String value) {
+ this.value = value;
+ }
+ }
+
+ // Connection Type defined in Network Information API v3.
+ // See Bug 1270401 - current W3C Spec (Editor's Draft) is different, it also contains wimax, mixed, unknown.
+ // W3C spec: http://w3c.github.io/netinfo/#the-connectiontype-enum
+ public enum ConnectionType {
+ CELLULAR(0),
+ BLUETOOTH(1),
+ ETHERNET(2),
+ WIFI(3),
+ OTHER(4),
+ NONE(5);
+
+ public final int value;
+
+ ConnectionType(int value) {
+ this.value = value;
+ }
+ }
+
+ /**
+ * Indicates whether network connectivity exists and it is possible to establish connections and pass data.
+ */
+ public static boolean isConnected(@NonNull Context context) {
+ return isConnected((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE));
+ }
+
+ public static boolean isConnected(ConnectivityManager connectivityManager) {
+ if (connectivityManager == null) {
+ return false;
+ }
+
+ final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+ return networkInfo != null && networkInfo.isConnected();
+ }
+
+ /**
+ * For mobile connections, maps particular connection subtype to a general 2G, 3G, 4G bucket.
+ */
+ public static ConnectionSubType getConnectionSubType(ConnectivityManager connectivityManager) {
+ if (connectivityManager == null) {
+ return ConnectionSubType.UNKNOWN;
+ }
+
+ final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+
+ if (networkInfo == null) {
+ return ConnectionSubType.UNKNOWN;
+ }
+
+ switch (networkInfo.getType()) {
+ case ConnectivityManager.TYPE_ETHERNET:
+ return ConnectionSubType.ETHERNET;
+ case ConnectivityManager.TYPE_MOBILE:
+ return getGenericMobileSubtype(networkInfo.getSubtype());
+ case ConnectivityManager.TYPE_WIMAX:
+ return ConnectionSubType.WIMAX;
+ case ConnectivityManager.TYPE_WIFI:
+ return ConnectionSubType.WIFI;
+ default:
+ return ConnectionSubType.UNKNOWN;
+ }
+ }
+
+ public static ConnectionType getConnectionType(ConnectivityManager connectivityManager) {
+ if (connectivityManager == null) {
+ return ConnectionType.NONE;
+ }
+
+ final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+ if (networkInfo == null) {
+ return ConnectionType.NONE;
+ }
+
+ switch (networkInfo.getType()) {
+ case ConnectivityManager.TYPE_BLUETOOTH:
+ return ConnectionType.BLUETOOTH;
+ case ConnectivityManager.TYPE_ETHERNET:
+ return ConnectionType.ETHERNET;
+ // Fallthrough, MOBILE and WIMAX both map to CELLULAR.
+ case ConnectivityManager.TYPE_MOBILE:
+ case ConnectivityManager.TYPE_WIMAX:
+ return ConnectionType.CELLULAR;
+ case ConnectivityManager.TYPE_WIFI:
+ return ConnectionType.WIFI;
+ default:
+ return ConnectionType.OTHER;
+ }
+ }
+
+ public static NetworkStatus getNetworkStatus(ConnectivityManager connectivityManager) {
+ if (connectivityManager == null) {
+ return NetworkStatus.UNKNOWN;
+ }
+
+ if (isConnected(connectivityManager)) {
+ return NetworkStatus.UP;
+ }
+ return NetworkStatus.DOWN;
+ }
+
+ private static ConnectionSubType getGenericMobileSubtype(int subtype) {
+ switch (subtype) {
+ // 2G types: fallthrough 5x
+ case TelephonyManager.NETWORK_TYPE_GPRS:
+ case TelephonyManager.NETWORK_TYPE_EDGE:
+ case TelephonyManager.NETWORK_TYPE_CDMA:
+ case TelephonyManager.NETWORK_TYPE_1xRTT:
+ case TelephonyManager.NETWORK_TYPE_IDEN:
+ return ConnectionSubType.CELL_2G;
+ // 3G types: fallthrough 9x
+ case TelephonyManager.NETWORK_TYPE_UMTS:
+ case TelephonyManager.NETWORK_TYPE_EVDO_0:
+ case TelephonyManager.NETWORK_TYPE_EVDO_A:
+ case TelephonyManager.NETWORK_TYPE_HSDPA:
+ case TelephonyManager.NETWORK_TYPE_HSUPA:
+ case TelephonyManager.NETWORK_TYPE_HSPA:
+ case TelephonyManager.NETWORK_TYPE_EVDO_B:
+ case TelephonyManager.NETWORK_TYPE_EHRPD:
+ case TelephonyManager.NETWORK_TYPE_HSPAP:
+ return ConnectionSubType.CELL_3G;
+ // 4G - just one type!
+ case TelephonyManager.NETWORK_TYPE_LTE:
+ return ConnectionSubType.CELL_4G;
+ default:
+ return ConnectionSubType.UNKNOWN;
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NonEvictingLruCache.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NonEvictingLruCache.java
new file mode 100644
index 0000000000..793b39b819
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NonEvictingLruCache.java
@@ -0,0 +1,44 @@
+/* -*- 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.util.LruCache;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * An LruCache that also supports a set of items that will never be evicted.
+ *
+ * Alas, LruCache is final, so we compose rather than inherit.
+ */
+public class NonEvictingLruCache<K, V> {
+ private final ConcurrentHashMap<K, V> permanent = new ConcurrentHashMap<K, V>();
+ private final LruCache<K, V> evictable;
+
+ public NonEvictingLruCache(final int evictableSize) {
+ evictable = new LruCache<K, V>(evictableSize);
+ }
+
+ public V get(K key) {
+ V val = permanent.get(key);
+ if (val == null) {
+ return evictable.get(key);
+ }
+ return val;
+ }
+
+ public void putWithoutEviction(K key, V value) {
+ permanent.put(key, value);
+ }
+
+ public void put(K key, V value) {
+ evictable.put(key, value);
+ }
+
+ public void evictAll() {
+ evictable.evictAll();
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/PrefUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/PrefUtils.java
new file mode 100644
index 0000000000..217e40b911
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/PrefUtils.java
@@ -0,0 +1,70 @@
+/* -*- 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 java.util.HashSet;
+import java.util.Set;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.mozilla.gecko.AppConstants.Versions;
+
+import android.content.SharedPreferences;
+import android.util.Log;
+
+
+public class PrefUtils {
+ private static final String LOGTAG = "GeckoPrefUtils";
+
+ // Cross version compatible way to get a string set from a pref
+ public static Set<String> getStringSet(final SharedPreferences prefs,
+ final String key,
+ final Set<String> defaultVal) {
+ if (!prefs.contains(key)) {
+ return defaultVal;
+ }
+
+ // If this is Android version >= 11, try to use a Set<String>.
+ try {
+ return prefs.getStringSet(key, new HashSet<String>());
+ } catch (ClassCastException ex) {
+ // A ClassCastException means we've upgraded from a pre-v11 Android to a new one
+ final Set<String> val = getFromJSON(prefs, key);
+ SharedPreferences.Editor edit = prefs.edit();
+ putStringSet(edit, key, val).apply();
+ return val;
+ }
+ }
+
+ private static Set<String> getFromJSON(SharedPreferences prefs, String key) {
+ try {
+ final String val = prefs.getString(key, "[]");
+ return JSONUtils.parseStringSet(new JSONArray(val));
+ } catch (JSONException ex) {
+ Log.i(LOGTAG, "Unable to parse JSON", ex);
+ }
+
+ return new HashSet<String>();
+ }
+
+ /**
+ * Cross version compatible way to save a set of strings.
+ * <p>
+ * This method <b>does not commit</b> any transaction. It is up to callers
+ * to commit.
+ *
+ * @param editor to write to.
+ * @param key to write.
+ * @param vals comprising string set.
+ * @return
+ */
+ public static SharedPreferences.Editor putStringSet(final SharedPreferences.Editor editor,
+ final String key,
+ final Set<String> vals) {
+ editor.putStringSet(key, vals);
+ return editor;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ProxySelector.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ProxySelector.java
new file mode 100644
index 0000000000..35010242b2
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ProxySelector.java
@@ -0,0 +1,155 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// This code is based on AOSP /libcore/luni/src/main/java/java/net/ProxySelectorImpl.java
+
+package org.mozilla.gecko.util;
+
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.URI;
+import java.net.URLConnection;
+import java.util.List;
+
+public class ProxySelector {
+ public static URLConnection openConnectionWithProxy(URI uri) throws IOException {
+ java.net.ProxySelector ps = java.net.ProxySelector.getDefault();
+ Proxy proxy = Proxy.NO_PROXY;
+ if (ps != null) {
+ List<Proxy> proxies = ps.select(uri);
+ if (proxies != null && !proxies.isEmpty()) {
+ proxy = proxies.get(0);
+ }
+ }
+
+ return uri.toURL().openConnection(proxy);
+ }
+
+ public ProxySelector() {
+ }
+
+ public Proxy select(String scheme, String host) {
+ int port = -1;
+ Proxy proxy = null;
+ String nonProxyHostsKey = null;
+ boolean httpProxyOkay = true;
+ if ("http".equalsIgnoreCase(scheme)) {
+ port = 80;
+ nonProxyHostsKey = "http.nonProxyHosts";
+ proxy = lookupProxy("http.proxyHost", "http.proxyPort", Proxy.Type.HTTP, port);
+ } else if ("https".equalsIgnoreCase(scheme)) {
+ port = 443;
+ nonProxyHostsKey = "https.nonProxyHosts"; // RI doesn't support this
+ proxy = lookupProxy("https.proxyHost", "https.proxyPort", Proxy.Type.HTTP, port);
+ } else if ("ftp".equalsIgnoreCase(scheme)) {
+ port = 80; // not 21 as you might guess
+ nonProxyHostsKey = "ftp.nonProxyHosts";
+ proxy = lookupProxy("ftp.proxyHost", "ftp.proxyPort", Proxy.Type.HTTP, port);
+ } else if ("socket".equalsIgnoreCase(scheme)) {
+ httpProxyOkay = false;
+ } else {
+ return Proxy.NO_PROXY;
+ }
+
+ if (nonProxyHostsKey != null
+ && isNonProxyHost(host, System.getProperty(nonProxyHostsKey))) {
+ return Proxy.NO_PROXY;
+ }
+
+ if (proxy != null) {
+ return proxy;
+ }
+
+ if (httpProxyOkay) {
+ proxy = lookupProxy("proxyHost", "proxyPort", Proxy.Type.HTTP, port);
+ if (proxy != null) {
+ return proxy;
+ }
+ }
+
+ proxy = lookupProxy("socksProxyHost", "socksProxyPort", Proxy.Type.SOCKS, 1080);
+ if (proxy != null) {
+ return proxy;
+ }
+
+ return Proxy.NO_PROXY;
+ }
+
+ /**
+ * Returns the proxy identified by the {@code hostKey} system property, or
+ * null.
+ */
+ @Nullable
+ private Proxy lookupProxy(String hostKey, String portKey, Proxy.Type type, int defaultPort) {
+ final String host = System.getProperty(hostKey);
+ if (TextUtils.isEmpty(host)) {
+ return null;
+ }
+
+ final int port = getSystemPropertyInt(portKey, defaultPort);
+ if (port == -1) {
+ // Port can be -1. See bug 1270529.
+ return null;
+ }
+
+ return new Proxy(type, InetSocketAddress.createUnresolved(host, port));
+ }
+
+ private int getSystemPropertyInt(String key, int defaultValue) {
+ String string = System.getProperty(key);
+ if (string != null) {
+ try {
+ return Integer.parseInt(string);
+ } catch (NumberFormatException ignored) {
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Returns true if the {@code nonProxyHosts} system property pattern exists
+ * and matches {@code host}.
+ */
+ private boolean isNonProxyHost(String host, String nonProxyHosts) {
+ if (host == null || nonProxyHosts == null) {
+ return false;
+ }
+
+ // construct pattern
+ StringBuilder patternBuilder = new StringBuilder();
+ for (int i = 0; i < nonProxyHosts.length(); i++) {
+ char c = nonProxyHosts.charAt(i);
+ switch (c) {
+ case '.':
+ patternBuilder.append("\\.");
+ break;
+ case '*':
+ patternBuilder.append(".*");
+ break;
+ default:
+ patternBuilder.append(c);
+ }
+ }
+ // check whether the host is the nonProxyHosts.
+ String pattern = patternBuilder.toString();
+ return host.matches(pattern);
+ }
+}
+
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/RawResource.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/RawResource.java
new file mode 100644
index 0000000000..5bcad1c602
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/RawResource.java
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.Resources;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringWriter;
+
+/**
+ * {@code RawResource} provides API to load raw resources in different
+ * forms. For now, we only load them as strings. We're using raw resources
+ * as localizable 'assets' as opposed to a string that can be directly
+ * translatable e.g. JSON file vs string.
+ *
+ * This is just a utility class to avoid code duplication for the different
+ * cases where need to read such assets.
+ */
+public final class RawResource {
+ public static String getAsString(Context context, int id) throws IOException {
+ InputStreamReader reader = null;
+
+ try {
+ final Resources res = context.getResources();
+ final InputStream is = res.openRawResource(id);
+ if (is == null) {
+ return null;
+ }
+
+ reader = new InputStreamReader(is);
+
+ final char[] buffer = new char[1024];
+ final StringWriter s = new StringWriter();
+
+ int n;
+ while ((n = reader.read(buffer, 0, buffer.length)) != -1) {
+ s.write(buffer, 0, n);
+ }
+
+ return s.toString();
+ } finally {
+ if (reader != null) {
+ reader.close();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/StringUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/StringUtils.java
new file mode 100644
index 0000000000..308168f439
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/StringUtils.java
@@ -0,0 +1,293 @@
+/* -*- 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.net.Uri;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+import org.mozilla.gecko.AppConstants.Versions;
+
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+public class StringUtils {
+ private static final String LOGTAG = "GeckoStringUtils";
+
+ private static final String FILTER_URL_PREFIX = "filter://";
+ private static final String USER_ENTERED_URL_PREFIX = "user-entered:";
+
+ /*
+ * This method tries to guess if the given string could be a search query or URL,
+ * and returns a previous result if there is ambiguity
+ *
+ * Search examples:
+ * foo
+ * foo bar.com
+ * foo http://bar.com
+ *
+ * URL examples
+ * foo.com
+ * foo.c
+ * :foo
+ * http://foo.com bar
+ *
+ * wasSearchQuery specifies whether text was a search query before the latest change
+ * in text. In ambiguous cases where the new text can be either a search or a URL,
+ * wasSearchQuery is returned
+ */
+ public static boolean isSearchQuery(String text, boolean wasSearchQuery) {
+ // We remove leading and trailing white spaces when decoding URLs
+ text = text.trim();
+ if (text.length() == 0)
+ return wasSearchQuery;
+
+ int colon = text.indexOf(':');
+ int dot = text.indexOf('.');
+ int space = text.indexOf(' ');
+
+ // If a space is found before any dot and colon, we assume this is a search query
+ if (space > -1 && (colon == -1 || space < colon) && (dot == -1 || space < dot)) {
+ return true;
+ }
+ // Otherwise, if a dot or a colon is found, we assume this is a URL
+ if (dot > -1 || colon > -1) {
+ return false;
+ }
+ // Otherwise, text is ambiguous, and we keep its status unchanged
+ return wasSearchQuery;
+ }
+
+ /**
+ * Strip the ref from a URL, if present
+ *
+ * @return The base URL, without the ref. The original String is returned if it has no ref,
+ * of if the input is malformed.
+ */
+ public static String stripRef(final String inputURL) {
+ if (inputURL == null) {
+ return null;
+ }
+
+ final int refIndex = inputURL.indexOf('#');
+
+ if (refIndex >= 0) {
+ return inputURL.substring(0, refIndex);
+ }
+
+ return inputURL;
+ }
+
+ public static class UrlFlags {
+ public static final int NONE = 0;
+ public static final int STRIP_HTTPS = 1;
+ }
+
+ public static String stripScheme(String url) {
+ return stripScheme(url, UrlFlags.NONE);
+ }
+
+ public static String stripScheme(String url, int flags) {
+ if (url == null) {
+ return url;
+ }
+
+ String newURL = url;
+
+ if (newURL.startsWith("http://")) {
+ newURL = newURL.replace("http://", "");
+ } else if (newURL.startsWith("https://") && flags == UrlFlags.STRIP_HTTPS) {
+ newURL = newURL.replace("https://", "");
+ }
+
+ if (newURL.endsWith("/")) {
+ newURL = newURL.substring(0, newURL.length()-1);
+ }
+
+ return newURL;
+ }
+
+ public static boolean isHttpOrHttps(String url) {
+ if (TextUtils.isEmpty(url)) {
+ return false;
+ }
+
+ return url.startsWith("http://") || url.startsWith("https://");
+ }
+
+ public static String stripCommonSubdomains(String host) {
+ if (host == null) {
+ return host;
+ }
+
+ // In contrast to desktop, we also strip mobile subdomains,
+ // since its unlikely users are intentionally typing them
+ int start = 0;
+
+ if (host.startsWith("www.")) {
+ start = 4;
+ } else if (host.startsWith("mobile.")) {
+ start = 7;
+ } else if (host.startsWith("m.")) {
+ start = 2;
+ }
+
+ return host.substring(start);
+ }
+
+ /**
+ * Searches the url query string for the first value with the given key.
+ */
+ public static String getQueryParameter(String url, String desiredKey) {
+ if (TextUtils.isEmpty(url) || TextUtils.isEmpty(desiredKey)) {
+ return null;
+ }
+
+ final String[] urlParts = url.split("\\?");
+ if (urlParts.length < 2) {
+ return null;
+ }
+
+ final String query = urlParts[1];
+ for (final String param : query.split("&")) {
+ final String pair[] = param.split("=");
+ final String key = Uri.decode(pair[0]);
+
+ // Key is empty or does not match the key we're looking for, discard
+ if (TextUtils.isEmpty(key) || !key.equals(desiredKey)) {
+ continue;
+ }
+ // No value associated with key, discard
+ if (pair.length < 2) {
+ continue;
+ }
+ final String value = Uri.decode(pair[1]);
+ if (TextUtils.isEmpty(value)) {
+ return null;
+ }
+ return value;
+ }
+
+ return null;
+ }
+
+ public static boolean isFilterUrl(String url) {
+ if (TextUtils.isEmpty(url)) {
+ return false;
+ }
+
+ return url.startsWith(FILTER_URL_PREFIX);
+ }
+
+ public static String getFilterFromUrl(String url) {
+ if (TextUtils.isEmpty(url)) {
+ return null;
+ }
+
+ return url.substring(FILTER_URL_PREFIX.length());
+ }
+
+ public static boolean isShareableUrl(final String url) {
+ final String scheme = Uri.parse(url).getScheme();
+ return !("about".equals(scheme) || "chrome".equals(scheme) ||
+ "file".equals(scheme) || "resource".equals(scheme));
+ }
+
+ public static boolean isUserEnteredUrl(String url) {
+ return (url != null && url.startsWith(USER_ENTERED_URL_PREFIX));
+ }
+
+ /**
+ * Given a url with a user-entered scheme, extract the
+ * scheme-specific component. For e.g, given "user-entered://www.google.com",
+ * this method returns "//www.google.com". If the passed url
+ * does not have a user-entered scheme, the same url will be returned.
+ *
+ * @param url to be decoded
+ * @return url component entered by user
+ */
+ public static String decodeUserEnteredUrl(String url) {
+ Uri uri = Uri.parse(url);
+ if ("user-entered".equals(uri.getScheme())) {
+ return uri.getSchemeSpecificPart();
+ }
+ return url;
+ }
+
+ public static String encodeUserEnteredUrl(String url) {
+ return Uri.fromParts("user-entered", url, null).toString();
+ }
+
+ /**
+ * Compatibility layer for API < 11.
+ *
+ * Returns a set of the unique names of all query parameters. Iterating
+ * over the set will return the names in order of their first occurrence.
+ *
+ * @param uri
+ * @throws UnsupportedOperationException if this isn't a hierarchical URI
+ *
+ * @return a set of decoded names
+ */
+ public static Set<String> getQueryParameterNames(Uri uri) {
+ return uri.getQueryParameterNames();
+ }
+
+ public static String safeSubstring(@NonNull final String str, final int start, final int end) {
+ return str.substring(
+ Math.max(0, start),
+ Math.min(end, str.length()));
+ }
+
+ /**
+ * Check if this might be a RTL (right-to-left) text by looking at the first character.
+ */
+ public static boolean isRTL(String text) {
+ if (TextUtils.isEmpty(text)) {
+ return false;
+ }
+
+ final char character = text.charAt(0);
+ final byte directionality = Character.getDirectionality(character);
+
+ return directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT
+ || directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC
+ || directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING
+ || directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE;
+ }
+
+ /**
+ * Force LTR (left-to-right) by prepending the text with the "left-to-right mark" (U+200E) if needed.
+ */
+ public static String forceLTR(String text) {
+ if (!isRTL(text)) {
+ return text;
+ }
+
+ return "\u200E" + text;
+ }
+
+ /**
+ * Joining together a sequence of strings with a separator.
+ */
+ public static String join(@NonNull String separator, @NonNull List<String> parts) {
+ if (parts.size() == 0) {
+ return "";
+ }
+
+ final StringBuilder builder = new StringBuilder();
+ builder.append(parts.get(0));
+
+ for (int i = 1; i < parts.size(); i++) {
+ builder.append(separator);
+ builder.append(parts.get(i));
+ }
+
+ return builder.toString();
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ThreadUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ThreadUtils.java
new file mode 100644
index 0000000000..884a56dc4d
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ThreadUtils.java
@@ -0,0 +1,247 @@
+/* -*- 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 org.mozilla.gecko.annotation.RobocopTarget;
+
+import java.util.Map;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.MessageQueue;
+import android.util.Log;
+
+public final class ThreadUtils {
+ private static final String LOGTAG = "ThreadUtils";
+
+ /**
+ * Controls the action taken when a method like
+ * {@link ThreadUtils#assertOnUiThread(AssertBehavior)} detects a problem.
+ */
+ public static enum AssertBehavior {
+ NONE,
+ THROW,
+ }
+
+ private static final Thread sUiThread = Looper.getMainLooper().getThread();
+ private static final Handler sUiHandler = new Handler(Looper.getMainLooper());
+
+ private static volatile Thread sBackgroundThread;
+
+ // Referenced directly from GeckoAppShell in highly performance-sensitive code (The extra
+ // function call of the getter was harming performance. (Bug 897123))
+ // Once Bug 709230 is resolved we should reconsider this as ProGuard should be able to optimise
+ // this out at compile time.
+ public static Handler sGeckoHandler;
+ public static volatile Thread sGeckoThread;
+
+ // Delayed Runnable that resets the Gecko thread priority.
+ private static final Runnable sPriorityResetRunnable = new Runnable() {
+ @Override
+ public void run() {
+ resetGeckoPriority();
+ }
+ };
+
+ private static boolean sIsGeckoPriorityReduced;
+
+ @SuppressWarnings("serial")
+ public static class UiThreadBlockedException extends RuntimeException {
+ public UiThreadBlockedException() {
+ super();
+ }
+
+ public UiThreadBlockedException(String msg) {
+ super(msg);
+ }
+
+ public UiThreadBlockedException(String msg, Throwable e) {
+ super(msg, e);
+ }
+
+ public UiThreadBlockedException(Throwable e) {
+ super(e);
+ }
+ }
+
+ public static void dumpAllStackTraces() {
+ Log.w(LOGTAG, "Dumping ALL the threads!");
+ Map<Thread, StackTraceElement[]> allStacks = Thread.getAllStackTraces();
+ for (Thread t : allStacks.keySet()) {
+ Log.w(LOGTAG, t.toString());
+ for (StackTraceElement ste : allStacks.get(t)) {
+ Log.w(LOGTAG, ste.toString());
+ }
+ Log.w(LOGTAG, "----");
+ }
+ }
+
+ public static void setBackgroundThread(Thread thread) {
+ sBackgroundThread = thread;
+ }
+
+ public static Thread getUiThread() {
+ return sUiThread;
+ }
+
+ public static Handler getUiHandler() {
+ return sUiHandler;
+ }
+
+ public static void postToUiThread(Runnable runnable) {
+ sUiHandler.post(runnable);
+ }
+
+ public static void postDelayedToUiThread(Runnable runnable, long timeout) {
+ sUiHandler.postDelayed(runnable, timeout);
+ }
+
+ public static void removeCallbacksFromUiThread(Runnable runnable) {
+ sUiHandler.removeCallbacks(runnable);
+ }
+
+ public static Thread getBackgroundThread() {
+ return sBackgroundThread;
+ }
+
+ public static Handler getBackgroundHandler() {
+ return GeckoBackgroundThread.getHandler();
+ }
+
+ public static void postToBackgroundThread(Runnable runnable) {
+ GeckoBackgroundThread.post(runnable);
+ }
+
+ public static void assertOnUiThread(final AssertBehavior assertBehavior) {
+ assertOnThread(getUiThread(), assertBehavior);
+ }
+
+ public static void assertOnUiThread() {
+ assertOnThread(getUiThread(), AssertBehavior.THROW);
+ }
+
+ public static void assertNotOnUiThread() {
+ assertNotOnThread(getUiThread(), AssertBehavior.THROW);
+ }
+
+ @RobocopTarget
+ public static void assertOnGeckoThread() {
+ assertOnThread(sGeckoThread, AssertBehavior.THROW);
+ }
+
+ public static void assertNotOnGeckoThread() {
+ if (sGeckoThread == null) {
+ // Cannot be on Gecko thread if Gecko thread is not live yet.
+ return;
+ }
+ assertNotOnThread(sGeckoThread, AssertBehavior.THROW);
+ }
+
+ public static void assertOnBackgroundThread() {
+ assertOnThread(getBackgroundThread(), AssertBehavior.THROW);
+ }
+
+ public static void assertOnThread(final Thread expectedThread) {
+ assertOnThread(expectedThread, AssertBehavior.THROW);
+ }
+
+ public static void assertOnThread(final Thread expectedThread, AssertBehavior behavior) {
+ assertOnThreadComparison(expectedThread, behavior, true);
+ }
+
+ public static void assertNotOnThread(final Thread expectedThread, AssertBehavior behavior) {
+ assertOnThreadComparison(expectedThread, behavior, false);
+ }
+
+ private static void assertOnThreadComparison(final Thread expectedThread, AssertBehavior behavior, boolean expected) {
+ final Thread currentThread = Thread.currentThread();
+ final long currentThreadId = currentThread.getId();
+ final long expectedThreadId = expectedThread.getId();
+
+ if ((currentThreadId == expectedThreadId) == expected) {
+ return;
+ }
+
+ final String message;
+ if (expected) {
+ message = "Expected thread " + expectedThreadId +
+ " (\"" + expectedThread.getName() + "\"), but running on thread " +
+ currentThreadId + " (\"" + currentThread.getName() + "\")";
+ } else {
+ message = "Expected anything but " + expectedThreadId +
+ " (\"" + expectedThread.getName() + "\"), but running there.";
+ }
+
+ final IllegalThreadStateException e = new IllegalThreadStateException(message);
+
+ switch (behavior) {
+ case THROW:
+ throw e;
+ default:
+ Log.e(LOGTAG, "Method called on wrong thread!", e);
+ }
+ }
+
+ public static boolean isOnGeckoThread() {
+ if (sGeckoThread != null) {
+ return isOnThread(sGeckoThread);
+ }
+ return false;
+ }
+
+ public static boolean isOnUiThread() {
+ return isOnThread(getUiThread());
+ }
+
+ @RobocopTarget
+ public static boolean isOnBackgroundThread() {
+ if (sBackgroundThread == null) {
+ return false;
+ }
+
+ return isOnThread(sBackgroundThread);
+ }
+
+ @RobocopTarget
+ public static boolean isOnThread(Thread thread) {
+ return (Thread.currentThread().getId() == thread.getId());
+ }
+
+ /**
+ * Reduces the priority of the Gecko thread, allowing other operations
+ * (such as those related to the UI and database) to take precedence.
+ *
+ * Note that there are no guards in place to prevent multiple calls
+ * to this method from conflicting with each other.
+ *
+ * @param timeout Timeout in ms after which the priority will be reset
+ */
+ public static void reduceGeckoPriority(long timeout) {
+ if (Runtime.getRuntime().availableProcessors() > 1) {
+ // Don't reduce priority for multicore devices. We use availableProcessors()
+ // for its fast performance. It may give false negatives (i.e. multicore
+ // detected as single-core), but we can tolerate this behavior.
+ return;
+ }
+ if (!sIsGeckoPriorityReduced && sGeckoThread != null) {
+ sIsGeckoPriorityReduced = true;
+ sGeckoThread.setPriority(Thread.MIN_PRIORITY);
+ getUiHandler().postDelayed(sPriorityResetRunnable, timeout);
+ }
+ }
+
+ /**
+ * Resets the priority of a thread whose priority has been reduced
+ * by reduceGeckoPriority.
+ */
+ public static void resetGeckoPriority() {
+ if (sIsGeckoPriorityReduced) {
+ sIsGeckoPriorityReduced = false;
+ sGeckoThread.setPriority(Thread.NORM_PRIORITY);
+ getUiHandler().removeCallbacks(sPriorityResetRunnable);
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/UIAsyncTask.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/UIAsyncTask.java
new file mode 100644
index 0000000000..26cc32a998
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/UIAsyncTask.java
@@ -0,0 +1,121 @@
+/* -*- 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.util;
+
+import android.os.Handler;
+import android.os.Looper;
+
+/**
+ * Executes a background task and publishes the result on the UI thread.
+ *
+ * The standard {@link android.os.AsyncTask} only runs onPostExecute on the
+ * thread it is constructed on, so this is a convenience class for creating
+ * tasks off the UI thread.
+ *
+ * We use generics differently to Android's AsyncTask.
+ * Android uses a "Params" type parameter to represent the type of all the parameters to this task.
+ * It then uses arguments of type Params... to permit arbitrarily-many of these to be passed
+ * fluently.
+ *
+ * Unfortunately, since Java does not support generic array types (and since varargs desugars to a
+ * single array parameter) that behaviour exposes a hole in the type system. See:
+ * http://docs.oracle.com/javase/tutorial/java/generics/nonReifiableVarargsType.html#vulnerabilities
+ *
+ * Instead, we equivalently have a single type parameter "Param". A UiAsyncTask may take exactly one
+ * parameter of type Param. Since Param can be an array type, this no more restrictive than the
+ * other approach, it just provides additional type safety.
+ */
+public abstract class UIAsyncTask<Param, Result> {
+ /**
+ * Provide a convenient API for parameter-free UiAsyncTasks by wrapping parameter-taking methods
+ * from UiAsyncTask in parameterless equivalents.
+ */
+ public static abstract class WithoutParams<InnerResult> extends UIAsyncTask<Void, InnerResult> {
+ public WithoutParams(Handler backgroundThreadHandler) {
+ super(backgroundThreadHandler);
+ }
+
+ public void execute() {
+ execute(null);
+ }
+
+ @Override
+ protected InnerResult doInBackground(Void unused) {
+ return doInBackground();
+ }
+
+ protected abstract InnerResult doInBackground();
+ }
+
+ final Handler mBackgroundThreadHandler;
+ private volatile boolean mCancelled;
+ private static Handler sHandler;
+
+ /**
+ * Creates a new asynchronous task.
+ *
+ * @param backgroundThreadHandler the handler to execute the background task on
+ */
+ public UIAsyncTask(Handler backgroundThreadHandler) {
+ mBackgroundThreadHandler = backgroundThreadHandler;
+ }
+
+ private static synchronized Handler getUiHandler() {
+ if (sHandler == null) {
+ sHandler = new Handler(Looper.getMainLooper());
+ }
+
+ return sHandler;
+ }
+
+ private final class BackgroundTaskRunnable implements Runnable {
+ private final Param mParam;
+
+ public BackgroundTaskRunnable(Param param) {
+ mParam = param;
+ }
+
+ @Override
+ public void run() {
+ final Result result = doInBackground(mParam);
+
+ getUiHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ if (mCancelled) {
+ onCancelled();
+ } else {
+ onPostExecute(result);
+ }
+ }
+ });
+ }
+ }
+
+ protected void execute(final Param param) {
+ getUiHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ onPreExecute();
+ mBackgroundThreadHandler.post(new BackgroundTaskRunnable(param));
+ }
+ });
+ }
+
+ public final boolean cancel() {
+ mCancelled = true;
+ return mCancelled;
+ }
+
+ public final boolean isCancelled() {
+ return mCancelled;
+ }
+
+ protected void onPreExecute() { }
+ protected void onPostExecute(Result result) { }
+ protected void onCancelled() { }
+ protected abstract Result doInBackground(Param param);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/UUIDUtil.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/UUIDUtil.java
new file mode 100644
index 0000000000..cef303a870
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/UUIDUtil.java
@@ -0,0 +1,19 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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 java.util.regex.Pattern;
+
+/**
+ * Utilities for UUIDs.
+ */
+public class UUIDUtil {
+ private UUIDUtil() {}
+
+ public static final String UUID_REGEX = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
+ public static final Pattern UUID_PATTERN = Pattern.compile(UUID_REGEX);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WeakReferenceHandler.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WeakReferenceHandler.java
new file mode 100644
index 0000000000..3e8508bceb
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WeakReferenceHandler.java
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.os.Handler;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * A Handler to help prevent memory leaks when using Handlers as inner classes.
+ *
+ * To use, extend the Handler, if it's an inner class, make it static,
+ * and reference `this` via the associated WeakReference.
+ *
+ * For additional context, see the "HandlerLeak" android lint item and this post by Romain Guy:
+ * https://groups.google.com/forum/#!msg/android-developers/1aPZXZG6kWk/lIYDavGYn5UJ
+ */
+public class WeakReferenceHandler<T> extends Handler {
+ public final WeakReference<T> mTarget;
+
+ public WeakReferenceHandler(final T that) {
+ super();
+ mTarget = new WeakReference<>(that);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WindowUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WindowUtils.java
new file mode 100644
index 0000000000..5298f846af
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WindowUtils.java
@@ -0,0 +1,59 @@
+/* -*- 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 org.mozilla.gecko.AppConstants.Versions;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.Display;
+import android.view.WindowManager;
+
+import java.lang.reflect.Method;
+
+public class WindowUtils {
+ private static final String LOGTAG = "Gecko" + WindowUtils.class.getSimpleName();
+
+ private WindowUtils() { /* To prevent instantiation */ }
+
+ /**
+ * Returns the best-guess physical device dimensions, including the system status bars. Note
+ * that DisplayMetrics.height/widthPixels does not include the system bars.
+ *
+ * via http://stackoverflow.com/a/23861333
+ *
+ * @param context the calling Activity's Context
+ * @return The number of pixels of the device's largest dimension, ignoring software status bars
+ */
+ public static int getLargestDimension(final Context context) {
+ final Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
+
+ if (Versions.feature17Plus) {
+ final DisplayMetrics realMetrics = new DisplayMetrics();
+ display.getRealMetrics(realMetrics);
+ return Math.max(realMetrics.widthPixels, realMetrics.heightPixels);
+
+ } else {
+ int tempWidth;
+ int tempHeight;
+ try {
+ final Method getRawH = Display.class.getMethod("getRawHeight");
+ final Method getRawW = Display.class.getMethod("getRawWidth");
+ tempWidth = (Integer) getRawW.invoke(display);
+ tempHeight = (Integer) getRawH.invoke(display);
+ } catch (Exception e) {
+ // This is the best we can do.
+ tempWidth = display.getWidth();
+ tempHeight = display.getHeight();
+ Log.w(LOGTAG, "Couldn't use reflection to get the real display metrics.");
+ }
+
+ return Math.max(tempWidth, tempHeight);
+
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/publicsuffix/PublicSuffix.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/publicsuffix/PublicSuffix.java
new file mode 100644
index 0000000000..6a146cfcf7
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/publicsuffix/PublicSuffix.java
@@ -0,0 +1,121 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.publicsuffix;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+
+import org.mozilla.gecko.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Helper methods for the public suffix part of a domain.
+ *
+ * A "public suffix" is one under which Internet users can (or historically could) directly register
+ * names. Some examples of public suffixes are .com, .co.uk and pvt.k12.ma.us.
+ *
+ * https://publicsuffix.org/
+ *
+ * Some parts of the implementation of this class are based on InternetDomainName class of the Guava
+ * project: https://github.com/google/guava
+ */
+public class PublicSuffix {
+ /**
+ * Strip the public suffix from the domain. Returns the original domain if no public suffix
+ * could be found.
+ *
+ * www.mozilla.org -> www.mozilla
+ * independent.co.uk -> independent
+ */
+ @NonNull
+ @WorkerThread // This method might need to load data from disk
+ public static String stripPublicSuffix(Context context, @NonNull String domain) {
+ if (domain.length() == 0) {
+ return domain;
+ }
+
+ final int index = findPublicSuffixIndex(context, domain);
+ if (index == -1) {
+ return domain;
+ }
+
+ return domain.substring(0, index);
+ }
+
+ /**
+ * Returns the index of the leftmost part of the public suffix, or -1 if not found.
+ */
+ @WorkerThread
+ private static int findPublicSuffixIndex(Context context, String domain) {
+ final List<String> parts = normalizeAndSplit(domain);
+ final int partsSize = parts.size();
+ final Set<String> exact = PublicSuffixPatterns.getExactSet(context);
+
+ for (int i = 0; i < partsSize; i++) {
+ String ancestorName = StringUtils.join(".", parts.subList(i, partsSize));
+
+ if (exact.contains(ancestorName)) {
+ return joinIndex(parts, i);
+ }
+
+ // Excluded domains (e.g. !nhs.uk) use the next highest
+ // domain as the effective public suffix (e.g. uk).
+ if (PublicSuffixPatterns.EXCLUDED.contains(ancestorName)) {
+ return joinIndex(parts, i + 1);
+ }
+
+ if (matchesWildcardPublicSuffix(ancestorName)) {
+ return joinIndex(parts, i);
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Normalize domain and split into domain parts (www.mozilla.org -> [www, mozilla, org]).
+ */
+ private static List<String> normalizeAndSplit(String domain) {
+ domain = domain.replaceAll("[.\u3002\uFF0E\uFF61]", "."); // All dot-like characters to '.'
+ domain = domain.toLowerCase();
+
+ if (domain.endsWith(".")) {
+ domain = domain.substring(0, domain.length() - 1); // Strip trailing '.'
+ }
+
+ List<String> parts = new ArrayList<>();
+ Collections.addAll(parts, domain.split("\\."));
+
+ return parts;
+ }
+
+ /**
+ * Translate the index of the leftmost part of the public suffix to the index of the domain string.
+ *
+ * [www, mozilla, org] and 2 => 12 (www.mozilla)
+ */
+ private static int joinIndex(List<String> parts, int index) {
+ int actualIndex = parts.get(0).length();
+
+ for (int i = 1; i < index; i++) {
+ actualIndex += parts.get(i).length() + 1; // Add one for the "." that is not part of the list elements
+ }
+
+ return actualIndex;
+ }
+
+ /**
+ * Does the domain name match one of the "wildcard" patterns (e.g. {@code "*.ar"})?
+ */
+ private static boolean matchesWildcardPublicSuffix(String domain) {
+ final String[] pieces = domain.split("\\.", 2);
+ return pieces.length == 2 && PublicSuffixPatterns.UNDER.contains(pieces[1]);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/publicsuffix/PublicSuffixPatterns.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/publicsuffix/PublicSuffixPatterns.java
new file mode 100644
index 0000000000..8c4b80ce1a
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/publicsuffix/PublicSuffixPatterns.java
@@ -0,0 +1,117 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.publicsuffix;
+
+import android.content.Context;
+import android.util.Log;
+
+import org.mozilla.gecko.util.IOUtils;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.HashSet;
+import java.util.Set;
+
+class PublicSuffixPatterns {
+ /** If a hostname is contained as a key in this map, it is a public suffix. */
+ private static Set<String> EXACT = null;
+
+ static synchronized Set<String> getExactSet(Context context) {
+ if (EXACT != null) {
+ return EXACT;
+ }
+
+ EXACT = new HashSet<>();
+
+ InputStream stream = null;
+
+ try {
+ stream = context.getAssets().open("publicsuffixlist");
+ BufferedReader reader = new BufferedReader(new InputStreamReader(
+ new BufferedInputStream(stream)));
+
+ String line;
+ while ((line = reader.readLine()) != null) {
+ EXACT.add(line);
+ }
+
+ } catch (IOException e) {
+ Log.e("Patterns", "IOException during loading public suffix list");
+ } finally {
+ IOUtils.safeStreamClose(stream);
+ }
+
+ return EXACT;
+ }
+
+
+ /**
+ * If a hostname is not a key in the EXCLUDE map, and if removing its
+ * leftmost component results in a name which is a key in this map, it is a
+ * public suffix.
+ */
+ static final Set<String> UNDER = new HashSet<>();
+ static {
+ UNDER.add("bd");
+ UNDER.add("magentosite.cloud");
+ UNDER.add("ke");
+ UNDER.add("triton.zone");
+ UNDER.add("compute.estate");
+ UNDER.add("ye");
+ UNDER.add("pg");
+ UNDER.add("kh");
+ UNDER.add("platform.sh");
+ UNDER.add("fj");
+ UNDER.add("ck");
+ UNDER.add("fk");
+ UNDER.add("alces.network");
+ UNDER.add("sch.uk");
+ UNDER.add("jm");
+ UNDER.add("mm");
+ UNDER.add("api.githubcloud.com");
+ UNDER.add("ext.githubcloud.com");
+ UNDER.add("0emm.com");
+ UNDER.add("githubcloudusercontent.com");
+ UNDER.add("cns.joyent.com");
+ UNDER.add("bn");
+ UNDER.add("yokohama.jp");
+ UNDER.add("nagoya.jp");
+ UNDER.add("kobe.jp");
+ UNDER.add("sendai.jp");
+ UNDER.add("kawasaki.jp");
+ UNDER.add("sapporo.jp");
+ UNDER.add("kitakyushu.jp");
+ UNDER.add("np");
+ UNDER.add("nom.br");
+ UNDER.add("er");
+ UNDER.add("cryptonomic.net");
+ UNDER.add("gu");
+ UNDER.add("kw");
+ UNDER.add("zw");
+ UNDER.add("mz");
+ }
+
+ /**
+ * The elements in this map would pass the UNDER test, but are known not to
+ * be public suffixes and are thus excluded from consideration. Since it
+ * refers to elements in UNDER of the same type, the type is actually not
+ * important here. The map is simply used for consistency reasons.
+ */
+ static final Set<String> EXCLUDED = new HashSet<>();
+ static {
+ EXCLUDED.add("www.ck");
+ EXCLUDED.add("city.yokohama.jp");
+ EXCLUDED.add("city.nagoya.jp");
+ EXCLUDED.add("city.kobe.jp");
+ EXCLUDED.add("city.sendai.jp");
+ EXCLUDED.add("city.kawasaki.jp");
+ EXCLUDED.add("city.sapporo.jp");
+ EXCLUDED.add("city.kitakyushu.jp");
+ EXCLUDED.add("teledata.mz");
+ }
+}
diff --git a/mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/ISelfBrailleService.java b/mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/ISelfBrailleService.java
new file mode 100644
index 0000000000..3bdd8c4502
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/ISelfBrailleService.java
@@ -0,0 +1,147 @@
+/* -*- 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 com.googlecode.eyesfree.braille.selfbraille;
+
+/**
+ * Interface for a client to control braille output for a part of the
+ * accessibility node tree.
+ */
+public interface ISelfBrailleService extends android.os.IInterface {
+ /** Local-side IPC implementation stub class. */
+ public static abstract class Stub extends android.os.Binder implements
+ com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService {
+ private static final java.lang.String DESCRIPTOR = "com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService";
+
+ /** Construct the stub at attach it to the interface. */
+ public Stub() {
+ this.attachInterface(this, DESCRIPTOR);
+ }
+
+ /**
+ * Cast an IBinder object into an
+ * com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService
+ * interface, generating a proxy if needed.
+ */
+ public static com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService asInterface(
+ android.os.IBinder obj) {
+ if ((obj == null)) {
+ return null;
+ }
+ android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
+ if (((iin != null) && (iin instanceof com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService))) {
+ return ((com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService) iin);
+ }
+ return new com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService.Stub.Proxy(
+ obj);
+ }
+
+ @Override
+ public android.os.IBinder asBinder() {
+ return this;
+ }
+
+ @Override
+ public boolean onTransact(int code, android.os.Parcel data,
+ android.os.Parcel reply, int flags)
+ throws android.os.RemoteException {
+ switch (code) {
+ case INTERFACE_TRANSACTION: {
+ reply.writeString(DESCRIPTOR);
+ return true;
+ }
+ case TRANSACTION_write: {
+ data.enforceInterface(DESCRIPTOR);
+ android.os.IBinder _arg0;
+ _arg0 = data.readStrongBinder();
+ com.googlecode.eyesfree.braille.selfbraille.WriteData _arg1;
+ if ((0 != data.readInt())) {
+ _arg1 = com.googlecode.eyesfree.braille.selfbraille.WriteData.CREATOR
+ .createFromParcel(data);
+ } else {
+ _arg1 = null;
+ }
+ this.write(_arg0, _arg1);
+ reply.writeNoException();
+ return true;
+ }
+ case TRANSACTION_disconnect: {
+ data.enforceInterface(DESCRIPTOR);
+ android.os.IBinder _arg0;
+ _arg0 = data.readStrongBinder();
+ this.disconnect(_arg0);
+ return true;
+ }
+ }
+ return super.onTransact(code, data, reply, flags);
+ }
+
+ private static class Proxy implements
+ com.googlecode.eyesfree.braille.selfbraille.ISelfBrailleService {
+ private android.os.IBinder mRemote;
+
+ Proxy(android.os.IBinder remote) {
+ mRemote = remote;
+ }
+
+ @Override
+ public android.os.IBinder asBinder() {
+ return mRemote;
+ }
+
+ public java.lang.String getInterfaceDescriptor() {
+ return DESCRIPTOR;
+ }
+
+ @Override
+ public void write(
+ android.os.IBinder clientToken,
+ com.googlecode.eyesfree.braille.selfbraille.WriteData writeData)
+ throws android.os.RemoteException {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeStrongBinder(clientToken);
+ if ((writeData != null)) {
+ _data.writeInt(1);
+ writeData.writeToParcel(_data, 0);
+ } else {
+ _data.writeInt(0);
+ }
+ mRemote.transact(Stub.TRANSACTION_write, _data, _reply, 0);
+ _reply.readException();
+ } finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+
+ @Override
+ public void disconnect(android.os.IBinder clientToken)
+ throws android.os.RemoteException {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeStrongBinder(clientToken);
+ mRemote.transact(Stub.TRANSACTION_disconnect, _data, null,
+ android.os.IBinder.FLAG_ONEWAY);
+ } finally {
+ _data.recycle();
+ }
+ }
+ }
+
+ static final int TRANSACTION_write = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
+ static final int TRANSACTION_disconnect = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
+ }
+
+ public void write(android.os.IBinder clientToken,
+ com.googlecode.eyesfree.braille.selfbraille.WriteData writeData)
+ throws android.os.RemoteException;
+
+ public void disconnect(android.os.IBinder clientToken)
+ throws android.os.RemoteException;
+}
diff --git a/mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/SelfBrailleClient.java b/mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/SelfBrailleClient.java
new file mode 100644
index 0000000000..e4a363acaf
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/SelfBrailleClient.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.googlecode.eyesfree.braille.selfbraille;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.Signature;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Client-side interface to the self brailling interface.
+ *
+ * Threading: Instances of this object should be created and shut down
+ * in a thread with a {@link Looper} associated with it. Other methods may
+ * be called on any thread.
+ */
+public class SelfBrailleClient {
+ private static final String LOG_TAG =
+ SelfBrailleClient.class.getSimpleName();
+ private static final String ACTION_SELF_BRAILLE_SERVICE =
+ "com.googlecode.eyesfree.braille.service.ACTION_SELF_BRAILLE_SERVICE";
+ private static final String BRAILLE_BACK_PACKAGE =
+ "com.googlecode.eyesfree.brailleback";
+ private static final Intent mServiceIntent =
+ new Intent(ACTION_SELF_BRAILLE_SERVICE)
+ .setPackage(BRAILLE_BACK_PACKAGE);
+ /**
+ * SHA-1 hash value of the Eyes-Free release key certificate, used to sign
+ * BrailleBack. It was generated from the keystore with:
+ * $ keytool -exportcert -keystore <keystorefile> -alias android.keystore \
+ * > cert
+ * $ keytool -printcert -file cert
+ */
+ // The typecasts are to silence a compiler warning about loss of precision
+ private static final byte[] EYES_FREE_CERT_SHA1 = new byte[] {
+ (byte) 0x9B, (byte) 0x42, (byte) 0x4C, (byte) 0x2D,
+ (byte) 0x27, (byte) 0xAD, (byte) 0x51, (byte) 0xA4,
+ (byte) 0x2A, (byte) 0x33, (byte) 0x7E, (byte) 0x0B,
+ (byte) 0xB6, (byte) 0x99, (byte) 0x1C, (byte) 0x76,
+ (byte) 0xEC, (byte) 0xA4, (byte) 0x44, (byte) 0x61
+ };
+ /**
+ * Delay before the first rebind attempt on bind error or service
+ * disconnect.
+ */
+ private static final int REBIND_DELAY_MILLIS = 500;
+ private static final int MAX_REBIND_ATTEMPTS = 5;
+
+ private final Binder mIdentity = new Binder();
+ private final Context mContext;
+ private final boolean mAllowDebugService;
+ private final SelfBrailleHandler mHandler = new SelfBrailleHandler();
+ private boolean mShutdown = false;
+
+ /**
+ * Written in handler thread, read in any thread calling methods on the
+ * object.
+ */
+ private volatile Connection mConnection;
+ /** Protected by synchronizing on mHandler. */
+ private int mNumFailedBinds = 0;
+
+ /**
+ * Constructs an instance of this class. {@code context} is used to bind
+ * to the self braille service. The current thread must have a Looper
+ * associated with it. If {@code allowDebugService} is true, this instance
+ * will connect to a BrailleBack service without requiring it to be signed
+ * by the release key used to sign BrailleBack.
+ */
+ public SelfBrailleClient(Context context, boolean allowDebugService) {
+ mContext = context;
+ mAllowDebugService = allowDebugService;
+ doBindService();
+ }
+
+ /**
+ * Shuts this instance down, deallocating any global resources it is using.
+ * This method must be called on the same thread that created this object.
+ */
+ public void shutdown() {
+ mShutdown = true;
+ doUnbindService();
+ }
+
+ public void write(WriteData writeData) {
+ writeData.validate();
+ ISelfBrailleService localService = getSelfBrailleService();
+ if (localService != null) {
+ try {
+ localService.write(mIdentity, writeData);
+ } catch (RemoteException ex) {
+ Log.e(LOG_TAG, "Self braille write failed", ex);
+ }
+ }
+ }
+
+ private void doBindService() {
+ Connection localConnection = new Connection();
+ if (!mContext.bindService(mServiceIntent, localConnection,
+ Context.BIND_AUTO_CREATE)) {
+ Log.e(LOG_TAG, "Failed to bind to service");
+ mHandler.scheduleRebind();
+ return;
+ }
+ mConnection = localConnection;
+ Log.i(LOG_TAG, "Bound to self braille service");
+ }
+
+ private void doUnbindService() {
+ if (mConnection != null) {
+ ISelfBrailleService localService = getSelfBrailleService();
+ if (localService != null) {
+ try {
+ localService.disconnect(mIdentity);
+ } catch (RemoteException ex) {
+ // Nothing to do.
+ }
+ }
+ mContext.unbindService(mConnection);
+ mConnection = null;
+ }
+ }
+
+ private ISelfBrailleService getSelfBrailleService() {
+ Connection localConnection = mConnection;
+ if (localConnection != null) {
+ return localConnection.mService;
+ }
+ return null;
+ }
+
+ private boolean verifyPackage() {
+ PackageManager pm = mContext.getPackageManager();
+ PackageInfo pi;
+ try {
+ pi = pm.getPackageInfo(BRAILLE_BACK_PACKAGE,
+ PackageManager.GET_SIGNATURES);
+ } catch (PackageManager.NameNotFoundException ex) {
+ Log.w(LOG_TAG, "Can't verify package " + BRAILLE_BACK_PACKAGE,
+ ex);
+ return false;
+ }
+ MessageDigest digest;
+ try {
+ digest = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException ex) {
+ Log.e(LOG_TAG, "SHA-1 not supported", ex);
+ return false;
+ }
+ // Check if any of the certificates match our hash.
+ for (Signature signature : pi.signatures) {
+ digest.update(signature.toByteArray());
+ if (MessageDigest.isEqual(EYES_FREE_CERT_SHA1, digest.digest())) {
+ return true;
+ }
+ digest.reset();
+ }
+ if (mAllowDebugService) {
+ Log.w(LOG_TAG, String.format(
+ "*** %s connected to BrailleBack with invalid (debug?) "
+ + "signature ***",
+ mContext.getPackageName()));
+ return true;
+ }
+ return false;
+ }
+ private class Connection implements ServiceConnection {
+ // Read in application threads, written in main thread.
+ private volatile ISelfBrailleService mService;
+
+ @Override
+ public void onServiceConnected(ComponentName className,
+ IBinder binder) {
+ if (!verifyPackage()) {
+ Log.w(LOG_TAG, String.format("Service certificate mismatch "
+ + "for %s, dropping connection",
+ BRAILLE_BACK_PACKAGE));
+ mHandler.unbindService();
+ return;
+ }
+ Log.i(LOG_TAG, "Connected to self braille service");
+ mService = ISelfBrailleService.Stub.asInterface(binder);
+ synchronized (mHandler) {
+ mNumFailedBinds = 0;
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName className) {
+ Log.e(LOG_TAG, "Disconnected from self braille service");
+ mService = null;
+ // Retry by rebinding.
+ mHandler.scheduleRebind();
+ }
+ }
+
+ private class SelfBrailleHandler extends Handler {
+ private static final int MSG_REBIND_SERVICE = 1;
+ private static final int MSG_UNBIND_SERVICE = 2;
+
+ public void scheduleRebind() {
+ synchronized (this) {
+ if (mNumFailedBinds < MAX_REBIND_ATTEMPTS) {
+ int delay = REBIND_DELAY_MILLIS << mNumFailedBinds;
+ sendEmptyMessageDelayed(MSG_REBIND_SERVICE, delay);
+ ++mNumFailedBinds;
+ }
+ }
+ }
+
+ public void unbindService() {
+ sendEmptyMessage(MSG_UNBIND_SERVICE);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_REBIND_SERVICE:
+ handleRebindService();
+ break;
+ case MSG_UNBIND_SERVICE:
+ handleUnbindService();
+ break;
+ }
+ }
+
+ private void handleRebindService() {
+ if (mShutdown) {
+ return;
+ }
+ if (mConnection != null) {
+ doUnbindService();
+ }
+ doBindService();
+ }
+
+ private void handleUnbindService() {
+ doUnbindService();
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/WriteData.java b/mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/WriteData.java
new file mode 100644
index 0000000000..ef81a29900
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/googlecode/eyesfree/braille/selfbraille/WriteData.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.googlecode.eyesfree.braille.selfbraille;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/**
+ * Represents what should be shown on the braille display for a
+ * part of the accessibility node tree.
+ */
+public class WriteData implements Parcelable {
+
+ private static final String PROP_SELECTION_START = "selectionStart";
+ private static final String PROP_SELECTION_END = "selectionEnd";
+
+ private AccessibilityNodeInfo mAccessibilityNodeInfo;
+ private CharSequence mText;
+ private Bundle mProperties = Bundle.EMPTY;
+
+ /**
+ * Returns a new {@link WriteData} instance for the given {@code view}.
+ */
+ public static WriteData forView(View view) {
+ AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(view);
+ WriteData writeData = new WriteData();
+ writeData.mAccessibilityNodeInfo = node;
+ return writeData;
+ }
+
+ public static WriteData forInfo(AccessibilityNodeInfo info){
+ WriteData writeData = new WriteData();
+ writeData.mAccessibilityNodeInfo = info;
+ return writeData;
+ }
+
+
+ public AccessibilityNodeInfo getAccessibilityNodeInfo() {
+ return mAccessibilityNodeInfo;
+ }
+
+ /**
+ * Sets the text to be displayed when the accessibility node associated
+ * with this instance has focus. If this method is not called (or
+ * {@code text} is {@code null}), this client relinquishes control over
+ * this node.
+ */
+ public WriteData setText(CharSequence text) {
+ mText = text;
+ return this;
+ }
+
+ public CharSequence getText() {
+ return mText;
+ }
+
+ /**
+ * Sets the start position in the text of a text selection or cursor that
+ * should be marked on the display. A negative value (the default) means
+ * no selection will be added.
+ */
+ public WriteData setSelectionStart(int v) {
+ writableProperties().putInt(PROP_SELECTION_START, v);
+ return this;
+ }
+
+ /**
+ * @see {@link #setSelectionStart}.
+ */
+ public int getSelectionStart() {
+ return mProperties.getInt(PROP_SELECTION_START, -1);
+ }
+
+ /**
+ * Sets the end of the text selection to be marked on the display. This
+ * value should only be non-negative if the selection start is
+ * non-negative. If this value is <= the selection start, the selection
+ * is a cursor. Otherwise, the selection covers the range from
+ * start(inclusive) to end (exclusive).
+ *
+ * @see {@link android.text.Selection}.
+ */
+ public WriteData setSelectionEnd(int v) {
+ writableProperties().putInt(PROP_SELECTION_END, v);
+ return this;
+ }
+
+ /**
+ * @see {@link #setSelectionEnd}.
+ */
+ public int getSelectionEnd() {
+ return mProperties.getInt(PROP_SELECTION_END, -1);
+ }
+
+ private Bundle writableProperties() {
+ if (mProperties == Bundle.EMPTY) {
+ mProperties = new Bundle();
+ }
+ return mProperties;
+ }
+
+ /**
+ * Checks constraints on the fields that must be satisfied before sending
+ * this instance to the self braille service.
+ * @throws IllegalStateException
+ */
+ public void validate() throws IllegalStateException {
+ if (mAccessibilityNodeInfo == null) {
+ throw new IllegalStateException(
+ "Accessibility node info can't be null");
+ }
+ int selectionStart = getSelectionStart();
+ int selectionEnd = getSelectionEnd();
+ if (mText == null) {
+ if (selectionStart > 0 || selectionEnd > 0) {
+ throw new IllegalStateException(
+ "Selection can't be set without text");
+ }
+ } else {
+ if (selectionStart < 0 && selectionEnd >= 0) {
+ throw new IllegalStateException(
+ "Selection end without start");
+ }
+ int textLength = mText.length();
+ if (selectionStart > textLength || selectionEnd > textLength) {
+ throw new IllegalStateException("Selection out of bounds");
+ }
+ }
+ }
+
+ // For Parcelable support.
+
+ public static final Parcelable.Creator<WriteData> CREATOR =
+ new Parcelable.Creator<WriteData>() {
+ @Override
+ public WriteData createFromParcel(Parcel in) {
+ return new WriteData(in);
+ }
+
+ @Override
+ public WriteData[] newArray(int size) {
+ return new WriteData[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <strong>Note:</strong> The {@link AccessibilityNodeInfo} will be
+ * recycled by this method, don't try to use this more than once.
+ */
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ mAccessibilityNodeInfo.writeToParcel(out, flags);
+ // The above call recycles the node, so make sure we don't use it
+ // anymore.
+ mAccessibilityNodeInfo = null;
+ out.writeString(mText.toString());
+ out.writeBundle(mProperties);
+ }
+
+ private WriteData() {
+ }
+
+ private WriteData(Parcel in) {
+ mAccessibilityNodeInfo =
+ AccessibilityNodeInfo.CREATOR.createFromParcel(in);
+ mText = in.readString();
+ mProperties = in.readBundle();
+ }
+}
diff --git a/mobile/android/geckoview_example/build.gradle b/mobile/android/geckoview_example/build.gradle
new file mode 100644
index 0000000000..33f8d85835
--- /dev/null
+++ b/mobile/android/geckoview_example/build.gradle
@@ -0,0 +1,63 @@
+buildDir "${topobjdir}/gradle/build/mobile/android/geckoview_example"
+
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 23
+ buildToolsVersion mozconfig.substs.ANDROID_BUILD_TOOLS_VERSION
+
+ defaultConfig {
+ applicationId "org.mozilla.geckoview_example"
+ minSdkVersion 15
+ targetSdkVersion 23
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+
+ // This is extremely frustrating, but the only way to do it automation for
+ // now. Without this, we only get a "debugAndroidTest" configuration; we
+ // have no "withoutGeckoBinariesAndroidTest" configuration.
+ testBuildType "withoutGeckoBinaries"
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ withGeckoBinaries { // For consistency with :geckoview project in Task Cluster invocations.
+ initWith debug
+ }
+ withoutGeckoBinaries { // Logical negation of withGeckoBinaries.
+ initWith debug
+ }
+ }
+}
+
+dependencies {
+ testCompile 'junit:junit:4.12'
+
+ compile 'com.android.support:support-annotations:23.4.0'
+
+ androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
+ androidTestCompile 'com.android.support.test:runner:0.5'
+ // Not defining this library again results in test-app assuming 23.1.1, and the following errors:
+ // "Conflict with dependency 'com.android.support:support-annotations'. Resolved versions for app (23.4.0) and test app (23.1.1) differ."
+ androidTestCompile 'com.android.support:support-annotations:23.4.0'
+
+ compile project(':geckoview')
+}
+
+apply from: "${topsrcdir}/mobile/android/gradle/with_gecko_binaries.gradle"
+
+android.applicationVariants.all { variant ->
+ // Like 'debug', 'release', or 'withoutGeckoBinaries'.
+ def buildType = variant.buildType.name
+
+ // It would be most natural for :geckoview to always include the Gecko
+ // binaries, but that's difficult; see the notes in
+ // mobile/android/gradle/with_gecko_binaries.gradle. Instead we handle our
+ // own Gecko binary inclusion.
+ if (!buildType.equals('withoutGeckoBinaries')) {
+ configureVariantWithGeckoBinaries(variant)
+ }
+}
diff --git a/mobile/android/geckoview_example/proguard-rules.pro b/mobile/android/geckoview_example/proguard-rules.pro
new file mode 100644
index 0000000000..46fbee5497
--- /dev/null
+++ b/mobile/android/geckoview_example/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/nalexander/.mozbuild/android-sdk-macosx/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/mobile/android/geckoview_example/src/androidTest/java/org/mozilla/geckoview_example/ApplicationTest.java b/mobile/android/geckoview_example/src/androidTest/java/org/mozilla/geckoview_example/ApplicationTest.java
new file mode 100644
index 0000000000..88630b1974
--- /dev/null
+++ b/mobile/android/geckoview_example/src/androidTest/java/org/mozilla/geckoview_example/ApplicationTest.java
@@ -0,0 +1,13 @@
+package org.mozilla.geckoview_example;
+
+import android.app.Application;
+import android.test.ApplicationTestCase;
+
+/**
+ * <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
+ */
+public class ApplicationTest extends ApplicationTestCase<Application> {
+ public ApplicationTest() {
+ super(Application.class);
+ }
+} \ No newline at end of file
diff --git a/mobile/android/geckoview_example/src/androidTest/java/org/mozilla/geckoview_example/GeckoViewActivityTest.java b/mobile/android/geckoview_example/src/androidTest/java/org/mozilla/geckoview_example/GeckoViewActivityTest.java
new file mode 100644
index 0000000000..aca1273518
--- /dev/null
+++ b/mobile/android/geckoview_example/src/androidTest/java/org/mozilla/geckoview_example/GeckoViewActivityTest.java
@@ -0,0 +1,32 @@
+/* -*- 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.geckoview_example;
+
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.assertion.ViewAssertions.matches;
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+
+@RunWith(AndroidJUnit4.class)
+public class GeckoViewActivityTest {
+
+ @Rule
+ public ActivityTestRule<GeckoViewActivity> mActivityRule = new ActivityTestRule(GeckoViewActivity.class);
+
+ @Test
+ public void testA() throws InterruptedException {
+ onView(withId(R.id.gecko_view))
+ .check(matches(isDisplayed()));
+ }
+}
diff --git a/mobile/android/geckoview_example/src/main/AndroidManifest.xml b/mobile/android/geckoview_example/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..551a4a7db7
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.mozilla.geckoview_example">
+
+ <application android:allowBackup="true"
+ android:label="@string/app_name"
+ android:supportsRtl="true">
+
+ <uses-library android:name="android.test.runner" />
+
+ <activity android:name="org.mozilla.geckoview_example.GeckoViewActivity"
+ android:label="GeckoViewActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ </application>
+
+</manifest>
diff --git a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
new file mode 100644
index 0000000000..071f7ed25e
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
@@ -0,0 +1,148 @@
+/* -*- 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.geckoview_example;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.Toast;
+
+import org.mozilla.gecko.BaseGeckoInterface;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.GeckoView;
+import org.mozilla.gecko.PrefsHelper;
+
+import static org.mozilla.gecko.GeckoView.setGeckoInterface;
+
+public class GeckoViewActivity extends Activity {
+ private static final String LOGTAG = "GeckoViewActivity";
+
+ GeckoView mGeckoView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setGeckoInterface(new BaseGeckoInterface(getApplicationContext()));
+
+ setContentView(R.layout.geckoview_activity);
+
+ mGeckoView = (GeckoView) findViewById(R.id.gecko_view);
+ mGeckoView.setChromeDelegate(new MyGeckoViewChrome());
+ mGeckoView.setContentDelegate(new MyGeckoViewContent());
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+
+ final GeckoProfile profile = GeckoProfile.get(getApplicationContext());
+
+ GeckoThread.init(profile, /* args */ null, /* action */ null, /* debugging */ false);
+ GeckoThread.launch();
+ }
+
+ private class MyGeckoViewChrome implements GeckoView.ChromeDelegate {
+ @Override
+ public void onReady(GeckoView view) {
+ Log.i(LOGTAG, "Gecko is ready");
+ // // Inject a script that adds some code to the content window
+ // mGeckoView.importScript("resource://android/assets/script.js");
+
+ // Set up remote debugging to a port number
+ PrefsHelper.setPref("layers.dump", true);
+ PrefsHelper.setPref("devtools.debugger.remote-port", 6000);
+ PrefsHelper.setPref("devtools.debugger.unix-domain-socket", "");
+ PrefsHelper.setPref("devtools.debugger.remote-enabled", true);
+
+ // The Gecko libraries have finished loading and we can use the rendering engine.
+ // Let's add a browser (required) and load a page into it.
+ // mGeckoView.addBrowser(getResources().getString(R.string.default_url));
+ }
+
+ @Override
+ public void onAlert(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result) {
+ Log.i(LOGTAG, "Alert!");
+ result.confirm();
+ Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
+ }
+
+ @Override
+ public void onConfirm(GeckoView view, GeckoView.Browser browser, String message, final GeckoView.PromptResult result) {
+ Log.i(LOGTAG, "Confirm!");
+ new AlertDialog.Builder(GeckoViewActivity.this)
+ .setTitle("javaScript dialog")
+ .setMessage(message)
+ .setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ result.confirm();
+ }
+ })
+ .setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ result.cancel();
+ }
+ })
+ .create()
+ .show();
+ }
+
+ @Override
+ public void onPrompt(GeckoView view, GeckoView.Browser browser, String message, String defaultValue, GeckoView.PromptResult result) {
+ result.cancel();
+ }
+
+ @Override
+ public void onDebugRequest(GeckoView view, GeckoView.PromptResult result) {
+ Log.i(LOGTAG, "Remote Debug!");
+ result.confirm();
+ }
+
+ @Override
+ public void onScriptMessage(GeckoView view, Bundle data, GeckoView.MessageResult result) {
+ Log.i(LOGTAG, "Got Script Message: " + data.toString());
+ String type = data.getString("type");
+ if ("fetch".equals(type)) {
+ Bundle ret = new Bundle();
+ ret.putString("name", "Mozilla");
+ ret.putString("url", "https://mozilla.org");
+ result.success(ret);
+ }
+ }
+ }
+
+ private class MyGeckoViewContent implements GeckoView.ContentDelegate {
+ @Override
+ public void onPageStart(GeckoView view, GeckoView.Browser browser, String url) {
+
+ }
+
+ @Override
+ public void onPageStop(GeckoView view, GeckoView.Browser browser, boolean success) {
+
+ }
+
+ @Override
+ public void onPageShow(GeckoView view, GeckoView.Browser browser) {
+
+ }
+
+ @Override
+ public void onReceivedTitle(GeckoView view, GeckoView.Browser browser, String title) {
+ Log.i(LOGTAG, "Received a title: " + title);
+ }
+
+ @Override
+ public void onReceivedFavicon(GeckoView view, GeckoView.Browser browser, String url, int size) {
+ Log.i(LOGTAG, "Received a favicon URL: " + url);
+ }
+ }
+}
diff --git a/mobile/android/geckoview_example/src/main/res/layout/geckoview_activity.xml b/mobile/android/geckoview_example/src/main/res/layout/geckoview_activity.xml
new file mode 100644
index 0000000000..eb4281fc6d
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/layout/geckoview_activity.xml
@@ -0,0 +1,13 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical">
+
+ <org.mozilla.gecko.GeckoView
+ android:id="@+id/gecko_view"
+ android:layout_width="fill_parent"
+ android:layout_height="match_parent"
+ android:scrollbars="none"
+ />
+
+</LinearLayout>
diff --git a/mobile/android/geckoview_example/src/main/res/values/colors.xml b/mobile/android/geckoview_example/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..3ab3e9cbce
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="colorPrimary">#3F51B5</color>
+ <color name="colorPrimaryDark">#303F9F</color>
+ <color name="colorAccent">#FF4081</color>
+</resources>
diff --git a/mobile/android/geckoview_example/src/main/res/values/strings.xml b/mobile/android/geckoview_example/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..1f5f447b23
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+<resources>
+ <string name="app_name">geckoview_example</string>
+</resources>
diff --git a/mobile/android/geckoview_example/src/test/java/org/mozilla/geckoview_example/ExampleUnitTest.java b/mobile/android/geckoview_example/src/test/java/org/mozilla/geckoview_example/ExampleUnitTest.java
new file mode 100644
index 0000000000..14f82340be
--- /dev/null
+++ b/mobile/android/geckoview_example/src/test/java/org/mozilla/geckoview_example/ExampleUnitTest.java
@@ -0,0 +1,15 @@
+package org.mozilla.geckoview_example;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * To work on unit tests, switch the Test Artifact in the Build Variants view.
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() throws Exception {
+ assertEquals(4, 2 + 2);
+ }
+} \ No newline at end of file
diff --git a/mobile/android/gradle.configure b/mobile/android/gradle.configure
new file mode 100644
index 0000000000..22e3b68054
--- /dev/null
+++ b/mobile/android/gradle.configure
@@ -0,0 +1,59 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# If --with-gradle is specified, build mobile/android with Gradle. If no
+# Gradle binary is specified, or if --without-gradle is specified, use the in
+# tree Gradle wrapper. The wrapper downloads and installs Gradle, which is
+# good for local developers but not good in automation.
+option('--with-gradle', nargs='?',
+ help='Enable building mobile/android with Gradle '
+ '(argument: location of binary or wrapper (gradle/gradlew))')
+
+@depends('--with-gradle')
+def with_gradle(value):
+ if value:
+ return True
+
+set_config('MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE', with_gradle)
+
+
+@depends('--with-gradle', check_build_environment)
+@imports(_from='os.path', _import='isfile')
+def gradle(value, build_env):
+ gradle = value[0] if len(value) else \
+ os.path.join(build_env.topsrcdir, 'gradlew')
+
+ # TODO: verify that $GRADLE is executable.
+ if not isfile(gradle):
+ die('GRADLE must be executable: %s', gradle)
+
+ return gradle
+
+set_config('GRADLE', gradle)
+
+
+# Automation uses this to change log levels, not use the daemon, and use
+# offline mode.
+option(env='GRADLE_FLAGS', default='', help='Flags to pass to Gradle.')
+
+@depends('GRADLE_FLAGS')
+def gradle_flags(value):
+ return value[0] if value else ''
+
+set_config('GRADLE_FLAGS', gradle_flags)
+
+
+# Automation will set this to file:///path/to/local via the mozconfig.
+# Local developer default is jcenter.
+option(env='GRADLE_MAVEN_REPOSITORY', default='https://jcenter.bintray.com/',
+ help='Path to Maven repository containing Gradle dependencies.')
+
+@depends('GRADLE_MAVEN_REPOSITORY')
+def gradle_maven_repository(value):
+ if value:
+ return value[0]
+
+set_config('GRADLE_MAVEN_REPOSITORY', gradle_maven_repository)
diff --git a/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/1.5.0-SNAPSHOT/gradle-plugin-1.5.0-SNAPSHOT-javadoc.jar b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/1.5.0-SNAPSHOT/gradle-plugin-1.5.0-SNAPSHOT-javadoc.jar
new file mode 100644
index 0000000000..d68a325962
--- /dev/null
+++ b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/1.5.0-SNAPSHOT/gradle-plugin-1.5.0-SNAPSHOT-javadoc.jar
Binary files differ
diff --git a/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/1.5.0-SNAPSHOT/gradle-plugin-1.5.0-SNAPSHOT-sources.jar b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/1.5.0-SNAPSHOT/gradle-plugin-1.5.0-SNAPSHOT-sources.jar
new file mode 100644
index 0000000000..ea22583c1d
--- /dev/null
+++ b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/1.5.0-SNAPSHOT/gradle-plugin-1.5.0-SNAPSHOT-sources.jar
Binary files differ
diff --git a/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/1.5.0-SNAPSHOT/gradle-plugin-1.5.0-SNAPSHOT.jar b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/1.5.0-SNAPSHOT/gradle-plugin-1.5.0-SNAPSHOT.jar
new file mode 100644
index 0000000000..b0c513916d
--- /dev/null
+++ b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/1.5.0-SNAPSHOT/gradle-plugin-1.5.0-SNAPSHOT.jar
Binary files differ
diff --git a/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/1.5.0-SNAPSHOT/gradle-plugin-1.5.0-SNAPSHOT.pom b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/1.5.0-SNAPSHOT/gradle-plugin-1.5.0-SNAPSHOT.pom
new file mode 100644
index 0000000000..b254be06b1
--- /dev/null
+++ b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/1.5.0-SNAPSHOT/gradle-plugin-1.5.0-SNAPSHOT.pom
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.jakewharton.sdkmanager</groupId>
+ <artifactId>gradle-plugin</artifactId>
+ <version>1.5.0-SNAPSHOT</version>
+ <dependencies>
+ <dependency>
+ <groupId>org.rauschig</groupId>
+ <artifactId>jarchivelib</artifactId>
+ <version>0.6.0</version>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.easytesting</groupId>
+ <artifactId>fest-assert-core</artifactId>
+ <version>2.0M10</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.android.tools.build</groupId>
+ <artifactId>gradle</artifactId>
+ <version>1.5.0</version>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ <version>2.4</version>
+ <scope>compile</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/1.5.0-SNAPSHOT/maven-metadata-local.xml b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/1.5.0-SNAPSHOT/maven-metadata-local.xml
new file mode 100644
index 0000000000..83eab73b95
--- /dev/null
+++ b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/1.5.0-SNAPSHOT/maven-metadata-local.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<metadata>
+ <groupId>com.jakewharton.sdkmanager</groupId>
+ <artifactId>gradle-plugin</artifactId>
+ <version>1.5.0-SNAPSHOT</version>
+ <versioning>
+ <snapshot>
+ <localCopy>true</localCopy>
+ </snapshot>
+ <lastUpdated>20160302034904</lastUpdated>
+ </versioning>
+</metadata>
diff --git a/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/maven-metadata-local.xml b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/maven-metadata-local.xml
new file mode 100644
index 0000000000..69ba601576
--- /dev/null
+++ b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/gradle-plugin/maven-metadata-local.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<metadata>
+ <groupId>com.jakewharton.sdkmanager</groupId>
+ <artifactId>gradle-plugin</artifactId>
+ <versioning>
+ <versions>
+ <version>1.5.0-SNAPSHOT</version>
+ </versions>
+ <lastUpdated>20160302034904</lastUpdated>
+ </versioning>
+</metadata>
diff --git a/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/1.5.0-SNAPSHOT/maven-metadata-local.xml b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/1.5.0-SNAPSHOT/maven-metadata-local.xml
new file mode 100644
index 0000000000..f231002c6b
--- /dev/null
+++ b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/1.5.0-SNAPSHOT/maven-metadata-local.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<metadata>
+ <groupId>com.jakewharton.sdkmanager</groupId>
+ <artifactId>sdk-manager-plugin</artifactId>
+ <version>1.5.0-SNAPSHOT</version>
+ <versioning>
+ <snapshot>
+ <localCopy>true</localCopy>
+ </snapshot>
+ <lastUpdated>20160302034904</lastUpdated>
+ </versioning>
+</metadata>
diff --git a/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/1.5.0-SNAPSHOT/sdk-manager-plugin-1.5.0-SNAPSHOT-javadoc.jar b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/1.5.0-SNAPSHOT/sdk-manager-plugin-1.5.0-SNAPSHOT-javadoc.jar
new file mode 100644
index 0000000000..d68a325962
--- /dev/null
+++ b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/1.5.0-SNAPSHOT/sdk-manager-plugin-1.5.0-SNAPSHOT-javadoc.jar
Binary files differ
diff --git a/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/1.5.0-SNAPSHOT/sdk-manager-plugin-1.5.0-SNAPSHOT-sources.jar b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/1.5.0-SNAPSHOT/sdk-manager-plugin-1.5.0-SNAPSHOT-sources.jar
new file mode 100644
index 0000000000..ea22583c1d
--- /dev/null
+++ b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/1.5.0-SNAPSHOT/sdk-manager-plugin-1.5.0-SNAPSHOT-sources.jar
Binary files differ
diff --git a/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/1.5.0-SNAPSHOT/sdk-manager-plugin-1.5.0-SNAPSHOT.jar b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/1.5.0-SNAPSHOT/sdk-manager-plugin-1.5.0-SNAPSHOT.jar
new file mode 100644
index 0000000000..b0c513916d
--- /dev/null
+++ b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/1.5.0-SNAPSHOT/sdk-manager-plugin-1.5.0-SNAPSHOT.jar
Binary files differ
diff --git a/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/1.5.0-SNAPSHOT/sdk-manager-plugin-1.5.0-SNAPSHOT.pom b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/1.5.0-SNAPSHOT/sdk-manager-plugin-1.5.0-SNAPSHOT.pom
new file mode 100644
index 0000000000..109d6d13b4
--- /dev/null
+++ b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/1.5.0-SNAPSHOT/sdk-manager-plugin-1.5.0-SNAPSHOT.pom
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.jakewharton.sdkmanager</groupId>
+ <artifactId>sdk-manager-plugin</artifactId>
+ <version>1.5.0-SNAPSHOT</version>
+ <name>SDK Manager</name>
+ <description>Gradle plugin which downloads and manages your Android SDK.</description>
+ <url>https://github.com/JakeWharton/sdk-manager-plugin</url>
+ <inceptionYear>2014</inceptionYear>
+ <licenses>
+ <license>
+ <name>The Apache Software License, Version 2.0</name>
+ <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+ <distribution>repo</distribution>
+ </license>
+ </licenses>
+ <developers>
+ <developer>
+ <id>jakewharton</id>
+ <name>Jake Wharton</name>
+ <email>jakewharton@gmail.com</email>
+ </developer>
+ </developers>
+ <scm>
+ <connection>scm:git:git://github.com/JakeWharton/sdk-manager-plugin.git</connection>
+ <developerConnection>scm:git:ssh://git@github.com/JakeWharton/sdk-manager-plugin.git</developerConnection>
+ <url>https://github.com/JakeWharton/sdk-manager-plugin</url>
+ </scm>
+ <dependencies>
+ <dependency>
+ <groupId>org.rauschig</groupId>
+ <artifactId>jarchivelib</artifactId>
+ <version>0.6.0</version>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.easytesting</groupId>
+ <artifactId>fest-assert-core</artifactId>
+ <version>2.0M10</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.android.tools.build</groupId>
+ <artifactId>gradle</artifactId>
+ <version>1.5.0</version>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ <version>2.4</version>
+ <scope>compile</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/maven-metadata-local.xml b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/maven-metadata-local.xml
new file mode 100644
index 0000000000..7b55c4835e
--- /dev/null
+++ b/mobile/android/gradle/m2repo/com/jakewharton/sdkmanager/sdk-manager-plugin/maven-metadata-local.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<metadata>
+ <groupId>com.jakewharton.sdkmanager</groupId>
+ <artifactId>sdk-manager-plugin</artifactId>
+ <versioning>
+ <versions>
+ <version>1.5.0-SNAPSHOT</version>
+ </versions>
+ <lastUpdated>20160302034904</lastUpdated>
+ </versioning>
+</metadata>
diff --git a/mobile/android/gradle/with_gecko_binaries.gradle b/mobile/android/gradle/with_gecko_binaries.gradle
new file mode 100644
index 0000000000..9048ab6fbf
--- /dev/null
+++ b/mobile/android/gradle/with_gecko_binaries.gradle
@@ -0,0 +1,105 @@
+// We run fairly hard into a fundamental limitation of the Android Gradle
+// plugin. There are many bugs filed about this, but
+// https://code.google.com/p/android/issues/detail?id=216978#c6 is a reason one.
+// The issue is that we need fine-grained control over when to include Gecko's
+// binary libraries into the GeckoView AAR and the Fennec APK, and that's hard
+// to achieve. In particular:
+//
+// * :app:automation wants :geckoview to not include Gecko binaries (automation
+// * build, before package)
+//
+// * :geckoview:withLibraries wants :geckoview to include Gecko binaries
+// * (automation build, after package)
+//
+// * non-:app:automation wants :geckoview to include Gecko binaries (local
+// * build, always after package)
+//
+// publishNonDefault (see
+// http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Library-Publication)
+// is intended to address this, but doesn't handle our case. That option always
+// builds *all* configurations, which fails when the required Gecko binaries
+// don't exist (automation build, before package). So instead, we make both
+// :app and :geckoview both know how to include the Gecko binaries, and use a
+// non-default, non-published :geckoview:withGeckoBinaries configuration to
+// handle automation's needs. Simple, right?
+
+// The omnijar inputs are listed as resource directory inputs to a dummy JAR.
+// That arrangement labels them nicely in IntelliJ. See the comment in the
+// :omnijar project for more context.
+evaluationDependsOn(':omnijar')
+
+task buildOmnijar(type:Exec) {
+ dependsOn rootProject.generateCodeAndResources
+
+ // See comment in :omnijar project regarding interface mismatches here.
+ inputs.source project(':omnijar').sourceSets.main.resources.srcDirs
+
+ // Produce a single output file.
+ outputs.file "${topobjdir}/dist/fennec/assets/omni.ja"
+
+ workingDir "${topobjdir}"
+
+ commandLine mozconfig.substs.GMAKE
+ args '-C'
+ args "${topobjdir}/mobile/android/base"
+ args 'gradle-omnijar'
+
+ // Only show the output if something went wrong.
+ ignoreExitValue = true
+ standardOutput = new ByteArrayOutputStream()
+ errorOutput = standardOutput
+ doLast {
+ if (execResult.exitValue != 0) {
+ throw new GradleException("Process '${commandLine}' finished with non-zero exit value ${execResult.exitValue}:\n\n${standardOutput.toString()}")
+ }
+ }
+}
+
+task syncOmnijarFromDistDir(type: Sync) {
+ into("${project.buildDir}/generated/omnijar")
+ from("${topobjdir}/dist/fennec/assets") {
+ include 'omni.ja'
+ }
+}
+
+task checkLibsExistInDistDir<< {
+ if (syncLibsFromDistDir.source.empty) {
+ throw new GradleException("Required JNI libraries not found in ${topobjdir}/dist/fennec/lib. Have you built and packaged?")
+ }
+}
+
+task syncLibsFromDistDir(type: Sync, dependsOn: checkLibsExistInDistDir) {
+ into("${project.buildDir}/generated/jniLibs")
+ from("${topobjdir}/dist/fennec/lib")
+}
+
+task checkAssetsExistInDistDir<< {
+ if (syncAssetsFromDistDir.source.empty) {
+ throw new GradleException("Required assets not found in ${topobjdir}/dist/fennec/assets. Have you built and packaged?")
+ }
+}
+
+task syncAssetsFromDistDir(type: Sync, dependsOn: checkAssetsExistInDistDir) {
+ into("${project.buildDir}/generated/assets")
+ from("${topobjdir}/dist/fennec/assets") {
+ exclude 'omni.ja'
+ }
+}
+
+ext.configureVariantWithGeckoBinaries = { variant ->
+ // Like 'local' or 'localOld'; may be null.
+ def productFlavor = variant.productFlavors ? variant.productFlavors[0].name : ""
+ // Like 'debug' or 'release'.
+ def buildType = variant.buildType.name
+
+ syncOmnijarFromDistDir.dependsOn buildOmnijar
+ def generateAssetsTask = tasks.findByName("generate${productFlavor.capitalize()}${buildType.capitalize()}Assets")
+ generateAssetsTask.dependsOn syncOmnijarFromDistDir
+ generateAssetsTask.dependsOn syncLibsFromDistDir
+ generateAssetsTask.dependsOn syncAssetsFromDistDir
+
+ def sourceSet = productFlavor ? "${productFlavor}${buildType.capitalize()}" : buildType
+ android.sourceSets."${sourceSet}".assets.srcDir syncOmnijarFromDistDir.destinationDir
+ android.sourceSets."${sourceSet}".assets.srcDir syncAssetsFromDistDir.destinationDir
+ android.sourceSets."${sourceSet}".jniLibs.srcDir syncLibsFromDistDir.destinationDir
+}
diff --git a/mobile/android/installer/Makefile.in b/mobile/android/installer/Makefile.in
new file mode 100644
index 0000000000..d550a22a2e
--- /dev/null
+++ b/mobile/android/installer/Makefile.in
@@ -0,0 +1,98 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+STANDALONE_MAKEFILE := 1
+
+# overwrite mobile-l10n.js with a matchOS=true one for multi-locale builds
+ifeq ($(AB_CD),multi)
+L10N_PREF_JS_EXPORTS = $(srcdir)/mobile-l10n.js
+L10N_PREF_JS_EXPORTS_PATH = $(FINAL_TARGET)/$(PREF_DIR)
+L10N_PREF_JS_EXPORTS_FLAGS = $(PREF_PPFLAGS) --silence-missing-directive-warnings
+PP_TARGETS += L10N_PREF_JS_EXPORTS
+endif
+
+include $(topsrcdir)/config/rules.mk
+
+MOZ_PKG_REMOVALS = $(srcdir)/removed-files.in
+
+MOZ_PKG_MANIFEST = $(srcdir)/package-manifest.in
+MOZ_PKG_DUPEFLAGS = -f $(srcdir)/allowed-dupes.mn
+
+ifdef MOZ_CHROME_MULTILOCALE
+MOZ_PKG_MANIFEST_DEPS = locale-manifest.in
+
+DEFINES += -DPKG_LOCALE_MANIFEST=$(CURDIR)/locale-manifest.in
+endif
+
+DEFINES += \
+ -DMOZ_APP_NAME=$(MOZ_APP_NAME) \
+ -DPREF_DIR=$(PREF_DIR) \
+ -DJAREXT= \
+ -DMOZ_CHILD_PROCESS_NAME=$(MOZ_CHILD_PROCESS_NAME) \
+ -DMOZ_CHILD_PROCESS_NAME_PIE=$(MOZ_CHILD_PROCESS_NAME_PIE) \
+ -DANDROID_CPU_ARCH=$(ANDROID_CPU_ARCH) \
+ $(NULL)
+
+ifdef MOZ_DEBUG
+DEFINES += -DMOZ_DEBUG=1
+endif
+
+ifdef MOZ_ANDROID_EXCLUDE_FONTS
+DEFINES += -DMOZ_ANDROID_EXCLUDE_FONTS=1
+endif
+
+ifdef MOZ_ANDROID_GCM
+DEFINES += -DMOZ_ANDROID_GCM=1
+endif
+
+ifdef MOZ_ARTIFACT_BUILDS
+DEFINES += -DMOZ_ARTIFACT_BUILDS=1
+endif
+
+MOZ_PACKAGER_MINIFY=1
+
+include $(topsrcdir)/toolkit/mozapps/installer/packager.mk
+
+# Note that JS_BINARY can be defined in packager.mk, so this test must come
+# after including that file. MOZ_PACKAGER_MINIFY_JS is used in packager.mk, but
+# since recipe evaluation is deferred, we can set it here after the inclusion.
+ifneq (,$(JS_BINARY))
+ifndef MOZ_DEBUG
+ifndef NIGHTLY_BUILD
+MOZ_PACKAGER_MINIFY_JS=1
+endif
+endif
+endif
+
+ifeq (bundle, $(MOZ_FS_LAYOUT))
+BINPATH = $(_BINPATH)
+DEFINES += -DAPPNAME=$(_APPNAME)
+else
+# Every other platform just winds up in dist/bin
+BINPATH = bin
+endif
+DEFINES += -DBINPATH=$(BINPATH)
+
+ifdef ENABLE_MARIONETTE
+DEFINES += -DENABLE_MARIONETTE=1
+endif
+
+
+ifdef MOZ_CHROME_MULTILOCALE
+# When MOZ_CHROME_MULTILOCALE is defined, we write multilocale.json like:
+# {"locales": ["en-US", "de", "ar", ...]}
+locale-manifest.in: $(GLOBAL_DEPS) FORCE
+ printf '\n[multilocale]\n' > $@
+ printf '@BINPATH@/res/multilocale.json\n' >> $@
+ for LOCALE in en-US $(MOZ_CHROME_MULTILOCALE) ;\
+ do \
+ printf '$(BINPATH)/chrome/'"$$LOCALE"'$(JAREXT)\n' >> $@; \
+ printf '$(BINPATH)/chrome/'"$$LOCALE"'.manifest\n' >> $@; \
+ done
+ COMMA=,
+ echo '{"locales": [$(foreach l,$(MOZ_CHROME_MULTILOCALE),"$(l)"$(COMMA)) "en-US"]}' \
+ > $(FINAL_TARGET)/res/multilocale.json
+
+GARBAGE += locale-manifest.in
+endif
diff --git a/mobile/android/installer/allowed-dupes.mn b/mobile/android/installer/allowed-dupes.mn
new file mode 100644
index 0000000000..514c6f7841
--- /dev/null
+++ b/mobile/android/installer/allowed-dupes.mn
@@ -0,0 +1,157 @@
+# Known duplicate files
+# This file is ideally removed, but some existing files will be grandfathered in
+# See bug 1303184
+#
+# PLEASE DO NOT ADD MORE EXCEPTIONS TO THIS LIST
+#
+
+# For android multilocale; see bug 1313702
+chrome/en-US/locale/branding/brand.dtd
+chrome/en-US/locale/branding/brand.properties
+chrome/en-US/locale/en-US/browser/aboutAccounts.dtd
+chrome/en-US/locale/en-US/browser/aboutAccounts.properties
+chrome/en-US/locale/en-US/browser/aboutAddons.dtd
+chrome/en-US/locale/en-US/browser/aboutAddons.properties
+chrome/en-US/locale/en-US/browser/aboutDownloads.dtd
+chrome/en-US/locale/en-US/browser/aboutDownloads.properties
+chrome/en-US/locale/en-US/browser/aboutHealthReport.dtd
+chrome/en-US/locale/en-US/browser/aboutHome.dtd
+chrome/en-US/locale/en-US/browser/aboutHome.properties
+chrome/en-US/locale/en-US/browser/aboutLogins.dtd
+chrome/en-US/locale/en-US/browser/aboutLogins.properties
+chrome/en-US/locale/en-US/browser/checkbox.dtd
+chrome/en-US/locale/en-US/browser/config.dtd
+chrome/en-US/locale/en-US/browser/config.properties
+chrome/en-US/locale/en-US/browser/devicePrompt.properties
+chrome/en-US/locale/en-US/browser/handling.properties
+chrome/en-US/locale/en-US/browser/passwordmgr.properties
+chrome/en-US/locale/en-US/browser/phishing.dtd
+chrome/en-US/locale/en-US/browser/sync.properties
+chrome/en-US/locale/en-US/browser/overrides/aboutAbout.dtd
+chrome/en-US/locale/en-US/browser/overrides/global.dtd
+chrome/en-US/locale/en-US/browser/overrides/global/mozilla.dtd
+chrome/en-US/locale/en-US/browser/overrides/intl.css
+chrome/en-US/locale/en-US/browser/region.properties
+chrome/en-US/locale/en-US/browser/webcompatReporter.properties
+chrome/en-US/locale/en-US/browser/searchplugins/amazon-co-uk.xml
+chrome/en-US/locale/en-US/browser/searchplugins/amazon-de.xml
+chrome/en-US/locale/en-US/browser/searchplugins/amazon-en-GB.xml
+chrome/en-US/locale/en-US/browser/searchplugins/amazon-in.xml
+chrome/en-US/locale/en-US/browser/searchplugins/amazondotcom.xml
+chrome/en-US/locale/en-US/browser/searchplugins/bolcom-fy-NL.xml
+chrome/en-US/locale/en-US/browser/searchplugins/bolcom-nl.xml
+chrome/en-US/locale/en-US/browser/searchplugins/bing.xml
+chrome/en-US/locale/en-US/browser/searchplugins/duckduckgo.xml
+chrome/en-US/locale/en-US/browser/searchplugins/google-nocodes.xml
+chrome/en-US/locale/en-US/browser/searchplugins/google.xml
+chrome/en-US/locale/en-US/browser/searchplugins/gulesider-mobile-NO.xml
+chrome/en-US/locale/en-US/browser/searchplugins/list.txt
+chrome/en-US/locale/en-US/browser/searchplugins/qwant.xml
+chrome/en-US/locale/en-US/browser/searchplugins/rediff.xml
+chrome/en-US/locale/en-US/browser/searchplugins/twitter.xml
+chrome/en-US/locale/en-US/browser/searchplugins/wikipedia.xml
+chrome/en-US/locale/en-US/browser/searchplugins/wikipedia-es.xml
+chrome/en-US/locale/en-US/browser/searchplugins/wikipedia-fr.xml
+chrome/en-US/locale/en-US/browser/searchplugins/wikipedia-hi.xml
+chrome/en-US/locale/en-US/browser/searchplugins/yahoo.xml
+chrome/en-US/locale/en-US/browser/searchplugins/yahoo-de.xml
+chrome/en-US/locale/en-US/browser/searchplugins/yahoo-en-GB.xml
+chrome/en-US/locale/en-US/browser/searchplugins/yahoo-es.xml
+chrome/en-US/locale/en-US/browser/searchplugins/yahoo-espanol.xml
+chrome/en-US/locale/en-US/browser/searchplugins/yahoo-france.xml
+chrome/en-US/locale/en-US/browser/searchplugins/yahoo-in.xml
+
+# Some of these are common with desktop
+chrome/en-US/locale/en-US/browser/overrides/AccessFu.properties
+chrome/en-US/locale/en-US/browser/overrides/about.dtd
+chrome/en-US/locale/en-US/browser/overrides/aboutReader.properties
+chrome/en-US/locale/en-US/browser/overrides/aboutRights.dtd
+chrome/en-US/locale/en-US/browser/overrides/charsetMenu.properties
+chrome/en-US/locale/en-US/browser/overrides/commonDialogs.properties
+chrome/en-US/locale/en-US/browser/overrides/crashreporter/crashes.dtd
+chrome/en-US/locale/en-US/browser/overrides/crashreporter/crashes.properties
+chrome/en-US/locale/en-US/browser/overrides/dom/dom.properties
+chrome/en-US/locale/en-US/browser/overrides/global/aboutSupport.dtd
+chrome/en-US/locale/en-US/browser/overrides/global/aboutSupport.properties
+chrome/en-US/locale/en-US/browser/overrides/global/aboutTelemetry.dtd
+chrome/en-US/locale/en-US/browser/overrides/global/aboutTelemetry.properties
+chrome/en-US/locale/en-US/browser/overrides/global/aboutWebrtc.properties
+chrome/en-US/locale/en-US/browser/overrides/intl.properties
+chrome/en-US/locale/en-US/browser/overrides/passwordmgr.properties
+chrome/en-US/locale/en-US/browser/overrides/plugins.properties
+chrome/en-US/locale/en-US/browser/overrides/plugins/pluginproblem.dtd
+chrome/en-US/locale/en-US/browser/overrides/search/search.properties
+chrome/en-US/locale/en-US/global-platform/mac/intl.properties
+chrome/en-US/locale/en-US/global-platform/unix/accessible.properties
+chrome/en-US/locale/en-US/global-platform/unix/intl.properties
+chrome/en-US/locale/en-US/global-platform/unix/platformKeys.properties
+chrome/en-US/locale/en-US/global-platform/win/accessible.properties
+chrome/en-US/locale/en-US/global-platform/win/intl.properties
+chrome/en-US/locale/en-US/global-platform/win/platformKeys.properties
+chrome/en-US/locale/en-US/global/AccessFu.properties
+chrome/en-US/locale/en-US/global/about.dtd
+chrome/en-US/locale/en-US/global/aboutAbout.dtd
+chrome/en-US/locale/en-US/global/aboutReader.properties
+chrome/en-US/locale/en-US/global/aboutRights.dtd
+chrome/en-US/locale/en-US/global/aboutSupport.dtd
+chrome/en-US/locale/en-US/global/aboutSupport.properties
+chrome/en-US/locale/en-US/global/aboutTelemetry.dtd
+chrome/en-US/locale/en-US/global/aboutTelemetry.properties
+chrome/en-US/locale/en-US/global/aboutWebrtc.properties
+chrome/en-US/locale/en-US/global/charsetMenu.properties
+chrome/en-US/locale/en-US/global/commonDialogs.properties
+chrome/en-US/locale/en-US/global/crashes.dtd
+chrome/en-US/locale/en-US/global/crashes.properties
+chrome/en-US/locale/en-US/global/dom/dom.properties
+chrome/en-US/locale/en-US/global/global.dtd
+chrome/en-US/locale/en-US/global/intl.css
+chrome/en-US/locale/en-US/global/intl.properties
+chrome/en-US/locale/en-US/global/mozilla.dtd
+chrome/en-US/locale/en-US/global/plugins.properties
+chrome/en-US/locale/en-US/global/search/search.properties
+chrome/en-US/locale/en-US/passwordmgr/passwordmgr.properties
+chrome/en-US/locale/en-US/pluginproblem/pluginproblem.dtd
+chrome/toolkit/skin/classic/global/autocomplete.css
+chrome/toolkit/skin/classic/global/button.css
+chrome/toolkit/skin/classic/global/checkbox.css
+chrome/toolkit/skin/classic/global/dialog.css
+chrome/toolkit/skin/classic/global/dropmarker.css
+chrome/toolkit/skin/classic/global/global.css
+chrome/toolkit/skin/classic/global/groupbox.css
+chrome/toolkit/skin/classic/global/listbox.css
+chrome/toolkit/skin/classic/global/menu.css
+chrome/toolkit/skin/classic/global/menulist.css
+chrome/toolkit/skin/classic/global/numberbox.css
+chrome/toolkit/skin/classic/global/popup.css
+chrome/toolkit/skin/classic/global/preferences.css
+chrome/toolkit/skin/classic/global/progressmeter.css
+chrome/toolkit/skin/classic/global/radio.css
+chrome/toolkit/skin/classic/global/resizer.css
+chrome/toolkit/skin/classic/global/richlistbox.css
+chrome/toolkit/skin/classic/global/scale.css
+chrome/toolkit/skin/classic/global/scrollbars.css
+chrome/toolkit/skin/classic/global/scrollbox.css
+chrome/toolkit/skin/classic/global/spinbuttons.css
+chrome/toolkit/skin/classic/global/splitter.css
+chrome/toolkit/skin/classic/global/tabbox.css
+chrome/toolkit/skin/classic/global/textbox.css
+chrome/toolkit/skin/classic/global/toolbar.css
+chrome/toolkit/skin/classic/global/toolbarbutton.css
+chrome/toolkit/skin/classic/global/tree.css
+chrome/toolkit/skin/classic/global/wizard.css
+modules/commonjs/sdk/ui/button/view/events.js
+modules/commonjs/sdk/ui/state/events.js
+modules/devtools/Console.jsm
+modules/devtools/Loader.jsm
+modules/devtools/Simulator.jsm
+modules/devtools/shared/Console.jsm
+modules/devtools/shared/Loader.jsm
+modules/devtools/shared/apps/Simulator.jsm
+res/table-remove-column-active.gif
+res/table-remove-column-hover.gif
+res/table-remove-column.gif
+res/table-remove-row-active.gif
+res/table-remove-row-hover.gif
+res/table-remove-row.gif
+modules/commonjs/index.js
+chrome/toolkit/content/global/XPCNativeWrapper.js
diff --git a/mobile/android/installer/mobile-l10n.js b/mobile/android/installer/mobile-l10n.js
new file mode 100644
index 0000000000..932843f89e
--- /dev/null
+++ b/mobile/android/installer/mobile-l10n.js
@@ -0,0 +1,6 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Inherit locale from the OS, used for multi-locale builds
+pref("intl.locale.matchOS", true);
diff --git a/mobile/android/installer/moz.build b/mobile/android/installer/moz.build
new file mode 100644
index 0000000000..28919c271d
--- /dev/null
+++ b/mobile/android/installer/moz.build
@@ -0,0 +1,6 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
diff --git a/mobile/android/installer/package-manifest.in b/mobile/android/installer/package-manifest.in
new file mode 100644
index 0000000000..a643ea243d
--- /dev/null
+++ b/mobile/android/installer/package-manifest.in
@@ -0,0 +1,561 @@
+; This Source Code Form is subject to the terms of the Mozilla Public
+; License, v. 2.0. If a copy of the MPL was not distributed with this
+; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+; Package file for the Fennec build.
+;
+; File format:
+;
+; [] designates a toplevel component. Example: [xpcom]
+; - in front of a file specifies it to be removed from the destination
+; * wildcard support to recursively copy the entire directory
+; ; file comment
+;
+
+#filter substitution
+
+[@AB_CD@]
+@BINPATH@/chrome/@AB_CD@@JAREXT@
+@BINPATH@/chrome/@AB_CD@.manifest
+@BINPATH@/@PREF_DIR@/mobile-l10n.js
+@BINPATH@/update.locale
+#ifdef MOZ_UPDATER
+@BINPATH@/updater.ini
+#endif
+@BINPATH@/dictionaries/*
+@BINPATH@/hyphenation/*
+
+[assets destdir="assets/@ANDROID_CPU_ARCH@"]
+#ifndef MOZ_STATIC_JS
+@BINPATH@/@DLL_PREFIX@mozjs@DLL_SUFFIX@
+#endif
+#ifdef MOZ_DMD
+@BINPATH@/@DLL_PREFIX@dmd@DLL_SUFFIX@
+#endif
+#ifndef MOZ_FOLD_LIBS
+@BINPATH@/@DLL_PREFIX@plc4@DLL_SUFFIX@
+@BINPATH@/@DLL_PREFIX@plds4@DLL_SUFFIX@
+@BINPATH@/@DLL_PREFIX@nspr4@DLL_SUFFIX@
+#endif
+@BINPATH@/@DLL_PREFIX@lgpllibs@DLL_SUFFIX@
+#ifdef MOZ_OMX_PLUGIN
+@BINPATH@/@DLL_PREFIX@omxplugin@DLL_SUFFIX@
+@BINPATH@/@DLL_PREFIX@omxpluginkk@DLL_SUFFIX@
+#endif
+@BINPATH@/@DLL_PREFIX@xul@DLL_SUFFIX@
+
+@BINPATH@/@DLL_PREFIX@nssckbi@DLL_SUFFIX@
+@BINPATH@/@DLL_PREFIX@nss3@DLL_SUFFIX@
+#ifndef MOZ_FOLD_LIBS
+@BINPATH@/@DLL_PREFIX@nssutil3@DLL_SUFFIX@
+@BINPATH@/@DLL_PREFIX@smime3@DLL_SUFFIX@
+@BINPATH@/@DLL_PREFIX@ssl3@DLL_SUFFIX@
+#endif
+@BINPATH@/@DLL_PREFIX@softokn3@DLL_SUFFIX@
+@BINPATH@/@DLL_PREFIX@freebl3@DLL_SUFFIX@
+#ifndef CROSS_COMPILE
+@BINPATH@/@DLL_PREFIX@freebl3.chk
+@BINPATH@/@DLL_PREFIX@softokn3.chk
+#endif
+#ifndef NSS_DISABLE_DBM
+@BINPATH@/@DLL_PREFIX@nssdbm3@DLL_SUFFIX@
+#ifndef CROSS_COMPILE
+@BINPATH@/@DLL_PREFIX@nssdbm3.chk
+#endif
+#endif
+
+#ifndef MOZ_FOLD_LIBS
+@BINPATH@/@DLL_PREFIX@mozsqlite3@DLL_SUFFIX@
+#endif
+
+[lib destdir="lib/@ANDROID_CPU_ARCH@"]
+@BINPATH@/@DLL_PREFIX@mozglue@DLL_SUFFIX@
+# This should be MOZ_CHILD_PROCESS_NAME, but that has a "lib/" prefix.
+@BINPATH@/@MOZ_CHILD_PROCESS_NAME@
+@BINPATH@/@MOZ_CHILD_PROCESS_NAME_PIE@
+
+[xpcom]
+@BINPATH@/package-name.txt
+@BINPATH@/classes.dex
+
+[browser]
+; [Base Browser Files]
+@BINPATH@/application.ini
+@BINPATH@/platform.ini
+@BINPATH@/blocklist.xml
+
+; [Components]
+#ifdef MOZ_ARTIFACT_BUILDS
+@BINPATH@/components/interfaces.xpt
+@BINPATH@/components/prebuilt-interfaces.manifest
+#endif
+@BINPATH@/components/components.manifest
+@BINPATH@/components/alerts.xpt
+#ifdef ACCESSIBILITY
+@BINPATH@/components/accessibility.xpt
+#endif
+@BINPATH@/components/appshell.xpt
+@BINPATH@/components/appstartup.xpt
+@BINPATH@/components/autocomplete.xpt
+@BINPATH@/components/autoconfig.xpt
+@BINPATH@/components/browser-element.xpt
+@BINPATH@/components/caps.xpt
+@BINPATH@/components/chrome.xpt
+@BINPATH@/components/commandhandler.xpt
+@BINPATH@/components/commandlines.xpt
+@BINPATH@/components/composer.xpt
+@BINPATH@/components/content_events.xpt
+@BINPATH@/components/content_geckomediaplugins.xpt
+@BINPATH@/components/content_html.xpt
+@BINPATH@/components/content_webrtc.xpt
+@BINPATH@/components/content_xslt.xpt
+@BINPATH@/components/cookie.xpt
+@BINPATH@/components/directory.xpt
+@BINPATH@/components/docshell.xpt
+@BINPATH@/components/dom.xpt
+@BINPATH@/components/dom_apps.xpt
+@BINPATH@/components/dom_base.xpt
+@BINPATH@/components/dom_canvas.xpt
+@BINPATH@/components/dom_core.xpt
+@BINPATH@/components/dom_css.xpt
+@BINPATH@/components/dom_events.xpt
+@BINPATH@/components/dom_geolocation.xpt
+@BINPATH@/components/dom_media.xpt
+@BINPATH@/components/dom_network.xpt
+@BINPATH@/components/dom_notification.xpt
+@BINPATH@/components/dom_html.xpt
+@BINPATH@/components/dom_offline.xpt
+@BINPATH@/components/dom_json.xpt
+@BINPATH@/components/dom_power.xpt
+#ifdef MOZ_ANDROID_GCM
+@BINPATH@/components/dom_push.xpt
+#endif
+@BINPATH@/components/dom_quota.xpt
+@BINPATH@/components/dom_range.xpt
+@BINPATH@/components/dom_security.xpt
+@BINPATH@/components/dom_settings.xpt
+@BINPATH@/components/dom_permissionsettings.xpt
+@BINPATH@/components/dom_sidebar.xpt
+@BINPATH@/components/dom_mobilemessage.xpt
+@BINPATH@/components/dom_storage.xpt
+@BINPATH@/components/dom_stylesheets.xpt
+@BINPATH@/components/dom_system.xpt
+@BINPATH@/components/dom_traversal.xpt
+@BINPATH@/components/dom_tv.xpt
+#ifdef MOZ_WEBSPEECH
+@BINPATH@/components/dom_webspeechrecognition.xpt
+#endif
+@BINPATH@/components/dom_xbl.xpt
+@BINPATH@/components/dom_xhr.xpt
+@BINPATH@/components/dom_xpath.xpt
+@BINPATH@/components/dom_xul.xpt
+@BINPATH@/components/dom_presentation.xpt
+@BINPATH@/components/downloads.xpt
+@BINPATH@/components/editor.xpt
+@BINPATH@/components/embed_base.xpt
+@BINPATH@/components/extensions.xpt
+@BINPATH@/components/exthandler.xpt
+@BINPATH@/components/exthelper.xpt
+@BINPATH@/components/fastfind.xpt
+@BINPATH@/components/feeds.xpt
+@BINPATH@/components/find.xpt
+@BINPATH@/components/gfx.xpt
+@BINPATH@/components/html5.xpt
+@BINPATH@/components/htmlparser.xpt
+@BINPATH@/components/imglib2.xpt
+@BINPATH@/components/inspector.xpt
+@BINPATH@/components/intl.xpt
+@BINPATH@/components/jar.xpt
+@BINPATH@/components/jsdebugger.xpt
+@BINPATH@/components/jsdownloads.xpt
+@BINPATH@/components/jsinspector.xpt
+@BINPATH@/components/layout_base.xpt
+#ifdef NS_PRINTING
+@BINPATH@/components/layout_printing.xpt
+#endif
+@BINPATH@/components/layout_xul_tree.xpt
+@BINPATH@/components/layout_xul.xpt
+@BINPATH@/components/locale.xpt
+@BINPATH@/components/lwbrk.xpt
+#ifdef MOZ_ENABLE_PROFILER_SPS
+@BINPATH@/components/memory_profiler.xpt
+#endif
+@BINPATH@/components/mimetype.xpt
+@BINPATH@/components/mozfind.xpt
+@BINPATH@/components/necko_about.xpt
+@BINPATH@/components/necko_cache.xpt
+@BINPATH@/components/necko_cache2.xpt
+@BINPATH@/components/necko_cookie.xpt
+@BINPATH@/components/necko_dns.xpt
+@BINPATH@/components/necko_file.xpt
+@BINPATH@/components/necko_ftp.xpt
+@BINPATH@/components/necko_http.xpt
+@BINPATH@/components/necko_mdns.xpt
+@BINPATH@/components/necko_res.xpt
+@BINPATH@/components/necko_socket.xpt
+@BINPATH@/components/necko_strconv.xpt
+@BINPATH@/components/necko_viewsource.xpt
+@BINPATH@/components/necko_websocket.xpt
+#ifdef NECKO_WIFI
+@BINPATH@/components/necko_wifi.xpt
+#endif
+@BINPATH@/components/necko_wyciwyg.xpt
+@BINPATH@/components/necko.xpt
+@BINPATH@/components/loginmgr.xpt
+@BINPATH@/components/parentalcontrols.xpt
+#ifdef MOZ_WEBRTC
+@BINPATH@/components/peerconnection.xpt
+#endif
+@BINPATH@/components/plugin.xpt
+@BINPATH@/components/pref.xpt
+@BINPATH@/components/prefetch.xpt
+#ifdef MOZ_ENABLE_PROFILER_SPS
+@BINPATH@/components/profiler.xpt
+#endif
+@BINPATH@/components/rdf.xpt
+@BINPATH@/components/satchel.xpt
+@BINPATH@/components/saxparser.xpt
+@BINPATH@/components/services-crypto-component.xpt
+@BINPATH@/components/captivedetect.xpt
+@BINPATH@/components/shistory.xpt
+@BINPATH@/components/spellchecker.xpt
+@BINPATH@/components/storage.xpt
+@BINPATH@/components/telemetry.xpt
+@BINPATH@/components/toolkit_asyncshutdown.xpt
+@BINPATH@/components/toolkit_filewatcher.xpt
+@BINPATH@/components/toolkit_finalizationwitness.xpt
+@BINPATH@/components/toolkit_formautofill.xpt
+@BINPATH@/components/toolkit_osfile.xpt
+@BINPATH@/components/toolkit_securityreporter.xpt
+@BINPATH@/components/toolkit_perfmonitoring.xpt
+@BINPATH@/components/toolkit_xulstore.xpt
+@BINPATH@/components/toolkitprofile.xpt
+#ifdef MOZ_ENABLE_XREMOTE
+@BINPATH@/components/toolkitremote.xpt
+#endif
+@BINPATH@/components/txtsvc.xpt
+@BINPATH@/components/txmgr.xpt
+@BINPATH@/components/uconv.xpt
+@BINPATH@/components/unicharutil.xpt
+@BINPATH@/components/update.xpt
+@BINPATH@/components/uriloader.xpt
+@BINPATH@/components/urlformatter.xpt
+@BINPATH@/components/webBrowser_core.xpt
+@BINPATH@/components/webbrowserpersist.xpt
+@BINPATH@/components/widget.xpt
+@BINPATH@/components/widget_android.xpt
+@BINPATH@/components/windowds.xpt
+@BINPATH@/components/windowwatcher.xpt
+@BINPATH@/components/xpcom_base.xpt
+@BINPATH@/components/xpcom_system.xpt
+@BINPATH@/components/xpcom_components.xpt
+@BINPATH@/components/xpcom_ds.xpt
+@BINPATH@/components/xpcom_io.xpt
+@BINPATH@/components/xpcom_threads.xpt
+@BINPATH@/components/xpcom_xpti.xpt
+@BINPATH@/components/xpconnect.xpt
+@BINPATH@/components/xulapp.xpt
+@BINPATH@/components/xul.xpt
+@BINPATH@/components/xultmpl.xpt
+@BINPATH@/components/zipwriter.xpt
+
+; JavaScript components
+@BINPATH@/components/ConsoleAPI.manifest
+@BINPATH@/components/ConsoleAPIStorage.js
+@BINPATH@/components/ContactManager.js
+@BINPATH@/components/ContactManager.manifest
+@BINPATH@/components/PhoneNumberService.js
+@BINPATH@/components/PhoneNumberService.manifest
+@BINPATH@/components/NotificationStorage.js
+@BINPATH@/components/NotificationStorage.manifest
+#ifdef MOZ_ANDROID_GCM
+@BINPATH@/components/Push.js
+@BINPATH@/components/Push.manifest
+@BINPATH@/components/PushComponents.js
+#endif
+@BINPATH@/components/SettingsManager.js
+@BINPATH@/components/SettingsManager.manifest
+@BINPATH@/components/BrowserElementParent.manifest
+@BINPATH@/components/BrowserElementParent.js
+@BINPATH@/components/BrowserElementProxy.manifest
+@BINPATH@/components/BrowserElementProxy.js
+@BINPATH@/components/FeedProcessor.manifest
+@BINPATH@/components/FeedProcessor.js
+@BINPATH@/components/WellKnownOpportunisticUtils.js
+@BINPATH@/components/WellKnownOpportunisticUtils.manifest
+@BINPATH@/components/mozProtocolHandler.js
+@BINPATH@/components/mozProtocolHandler.manifest
+@BINPATH@/components/PermissionSettings.js
+@BINPATH@/components/PermissionSettings.manifest
+@BINPATH@/components/PermissionPromptService.js
+@BINPATH@/components/PermissionPromptService.manifest
+@BINPATH@/components/nsDNSServiceDiscovery.manifest
+@BINPATH@/components/nsDNSServiceDiscovery.js
+@BINPATH@/components/toolkitsearch.manifest
+@BINPATH@/components/nsSearchService.js
+@BINPATH@/components/nsSidebar.js
+@BINPATH@/components/passwordmgr.manifest
+@BINPATH@/components/nsLoginInfo.js
+@BINPATH@/components/nsLoginManager.js
+@BINPATH@/components/nsLoginManagerPrompter.js
+@BINPATH@/components/storage-mozStorage.js
+@BINPATH@/components/crypto-SDR.js
+@BINPATH@/components/TooltipTextProvider.js
+@BINPATH@/components/TooltipTextProvider.manifest
+@BINPATH@/components/NetworkGeolocationProvider.manifest
+@BINPATH@/components/NetworkGeolocationProvider.js
+@BINPATH@/components/EditorUtils.manifest
+@BINPATH@/components/EditorUtils.js
+@BINPATH@/components/extensions.manifest
+@BINPATH@/components/utils.manifest
+@BINPATH@/components/simpleServices.js
+@BINPATH@/components/addonManager.js
+@BINPATH@/components/amContentHandler.js
+@BINPATH@/components/amInstallTrigger.js
+@BINPATH@/components/amWebAPI.js
+@BINPATH@/components/amWebInstallListener.js
+@BINPATH@/components/nsBlocklistService.js
+#ifndef RELEASE_OR_BETA
+@BINPATH@/components/TabSource.js
+#endif
+@BINPATH@/components/webvtt.xpt
+@BINPATH@/components/WebVTT.manifest
+@BINPATH@/components/WebVTTParserWrapper.js
+
+#ifdef MOZ_UPDATER
+@BINPATH@/components/nsUpdateService.manifest
+@BINPATH@/components/nsUpdateService.js
+@BINPATH@/components/nsUpdateServiceStub.js
+#endif
+@BINPATH@/components/nsUpdateTimerManager.manifest
+@BINPATH@/components/nsUpdateTimerManager.js
+@BINPATH@/components/pluginGlue.manifest
+@BINPATH@/components/ProcessSingleton.manifest
+@BINPATH@/components/MainProcessSingleton.js
+@BINPATH@/components/ContentProcessSingleton.js
+@BINPATH@/components/nsURLFormatter.manifest
+@BINPATH@/components/nsURLFormatter.js
+@BINPATH@/components/txEXSLTRegExFunctions.manifest
+@BINPATH@/components/txEXSLTRegExFunctions.js
+@BINPATH@/components/nsContentPrefService.manifest
+@BINPATH@/components/nsContentPrefService.js
+@BINPATH@/components/nsHandlerService.manifest
+@BINPATH@/components/nsHandlerService.js
+@BINPATH@/components/nsWebHandlerApp.manifest
+@BINPATH@/components/nsWebHandlerApp.js
+@BINPATH@/components/satchel.manifest
+@BINPATH@/components/nsFormAutoComplete.js
+@BINPATH@/components/nsFormHistory.js
+@BINPATH@/components/FormHistoryStartup.js
+@BINPATH@/components/nsInputListAutoComplete.js
+@BINPATH@/components/formautofill.manifest
+@BINPATH@/components/FormAutofillContentService.js
+@BINPATH@/components/FormAutofillStartup.js
+@BINPATH@/components/CSSUnprefixingService.js
+@BINPATH@/components/CSSUnprefixingService.manifest
+@BINPATH@/components/contentAreaDropListener.manifest
+@BINPATH@/components/contentAreaDropListener.js
+@BINPATH@/components/messageWakeupService.js
+@BINPATH@/components/messageWakeupService.manifest
+@BINPATH@/components/nsINIProcessor.manifest
+@BINPATH@/components/nsINIProcessor.js
+@BINPATH@/components/servicesComponents.manifest
+@BINPATH@/components/TelemetryStartup.js
+@BINPATH@/components/TelemetryStartup.manifest
+@BINPATH@/components/XULStore.js
+@BINPATH@/components/XULStore.manifest
+@BINPATH@/components/AppsService.js
+@BINPATH@/components/AppsService.manifest
+@BINPATH@/components/htmlMenuBuilder.js
+@BINPATH@/components/htmlMenuBuilder.manifest
+
+@BINPATH@/components/SystemMessageInternal.js
+@BINPATH@/components/SystemMessageManager.js
+@BINPATH@/components/SystemMessageCache.js
+@BINPATH@/components/SystemMessageManager.manifest
+
+#ifdef MOZ_WEBRTC
+@BINPATH@/components/PeerConnection.js
+@BINPATH@/components/PeerConnection.manifest
+#endif
+
+@BINPATH@/components/CaptivePortalDetectComponents.manifest
+@BINPATH@/components/captivedetect.js
+
+#ifdef MOZ_WEBSPEECH
+@BINPATH@/components/dom_webspeechsynth.xpt
+#endif
+
+#if defined(ENABLE_TESTS) && defined(MOZ_DEBUG)
+@BINPATH@/components/TestInterfaceJS.js
+@BINPATH@/components/TestInterfaceJS.manifest
+@BINPATH@/components/TestInterfaceJSMaplike.js
+#endif
+
+@BINPATH@/components/nsAsyncShutdown.manifest
+@BINPATH@/components/nsAsyncShutdown.js
+
+@BINPATH@/components/Downloads.manifest
+@BINPATH@/components/DownloadLegacy.js
+
+@BINPATH@/components/PresentationDeviceInfoManager.manifest
+@BINPATH@/components/PresentationDeviceInfoManager.js
+@BINPATH@/components/BuiltinProviders.manifest
+@BINPATH@/components/PresentationControlService.js
+@BINPATH@/components/PresentationNetworkHelper.js
+@BINPATH@/components/PresentationNetworkHelper.manifest
+@BINPATH@/components/PresentationDataChannelSessionTransport.js
+@BINPATH@/components/PresentationDataChannelSessionTransport.manifest
+@BINPATH@/components/LegacyProviders.manifest
+@BINPATH@/components/LegacyPresentationControlService.js
+@BINPATH@/components/AndroidCastDeviceProvider.manifest
+@BINPATH@/components/AndroidCastDeviceProvider.js
+
+@BINPATH@/components/TVSimulatorService.js
+@BINPATH@/components/TVSimulatorService.manifest
+
+; Modules
+@BINPATH@/modules/*
+
+; Safe Browsing
+@BINPATH@/components/nsURLClassifier.manifest
+@BINPATH@/components/nsUrlClassifierHashCompleter.js
+@BINPATH@/components/nsUrlClassifierListManager.js
+@BINPATH@/components/nsUrlClassifierLib.js
+@BINPATH@/components/url-classifier.xpt
+
+; Private Browsing
+@BINPATH@/components/privatebrowsing.xpt
+@BINPATH@/components/PrivateBrowsing.manifest
+@BINPATH@/components/PrivateBrowsingTrackingProtectionWhitelist.js
+
+; Security Reports
+@BINPATH@/components/SecurityReporter.manifest
+@BINPATH@/components/SecurityReporter.js
+
+; [Browser Chrome Files]
+@BINPATH@/chrome/toolkit@JAREXT@
+@BINPATH@/chrome/toolkit.manifest
+
+; [Extensions]
+@BINPATH@/components/extensions-toolkit.manifest
+@BINPATH@/components/extensions-mobile.manifest
+
+; Features
+@BINPATH@/features/*
+
+; DevTools
+@BINPATH@/chrome/devtools@JAREXT@
+@BINPATH@/chrome/devtools.manifest
+
+; [Default Preferences]
+; All the pref files must be part of base to prevent migration bugs
+@BINPATH@/@PREF_DIR@/mobile.js
+@BINPATH@/@PREF_DIR@/channel-prefs.js
+@BINPATH@/ua-update.json
+@BINPATH@/greprefs.js
+@BINPATH@/defaults/autoconfig/prefcalls.js
+
+; [Layout Engine Resources]
+; Style Sheets, Graphics and other Resources used by the layout engine.
+@BINPATH@/res/EditorOverride.css
+@BINPATH@/res/contenteditable.css
+@BINPATH@/res/designmode.css
+@BINPATH@/res/TopLevelImageDocument.css
+@BINPATH@/res/TopLevelVideoDocument.css
+@BINPATH@/res/table-add-column-after-active.gif
+@BINPATH@/res/table-add-column-after-hover.gif
+@BINPATH@/res/table-add-column-after.gif
+@BINPATH@/res/table-add-column-before-active.gif
+@BINPATH@/res/table-add-column-before-hover.gif
+@BINPATH@/res/table-add-column-before.gif
+@BINPATH@/res/table-add-row-after-active.gif
+@BINPATH@/res/table-add-row-after-hover.gif
+@BINPATH@/res/table-add-row-after.gif
+@BINPATH@/res/table-add-row-before-active.gif
+@BINPATH@/res/table-add-row-before-hover.gif
+@BINPATH@/res/table-add-row-before.gif
+@BINPATH@/res/table-remove-column-active.gif
+@BINPATH@/res/table-remove-column-hover.gif
+@BINPATH@/res/table-remove-column.gif
+@BINPATH@/res/table-remove-row-active.gif
+@BINPATH@/res/table-remove-row-hover.gif
+@BINPATH@/res/table-remove-row.gif
+@BINPATH@/res/grabber.gif
+@BINPATH@/res/dtd/*
+@BINPATH@/res/html/*
+@BINPATH@/res/language.properties
+@BINPATH@/res/entityTables/*
+
+#ifndef MOZ_ANDROID_EXCLUDE_FONTS
+@BINPATH@/res/fonts/*
+#else
+@BINPATH@/res/fonts/*.properties
+#endif
+
+; svg
+@BINPATH@/res/svg.css
+@BINPATH@/components/dom_svg.xpt
+@BINPATH@/components/dom_smil.xpt
+
+; [Personal Security Manager]
+;
+@BINPATH@/components/pipnss.xpt
+
+; For process sandboxing
+#if defined(MOZ_SANDBOX)
+@BINPATH@/@DLL_PREFIX@mozsandbox@DLL_SUFFIX@
+#endif
+
+; [Crash Reporter]
+; CrashService is not used on Android but the ini files are required for L10N
+; strings, see bug 1191351.
+#ifdef MOZ_CRASHREPORTER
+@BINPATH@/crashreporter.ini
+@BINPATH@/crashreporter-override.ini
+#endif
+
+[mobile]
+@BINPATH@/chrome/chrome@JAREXT@
+@BINPATH@/chrome/chrome.manifest
+@BINPATH@/components/AboutRedirector.js
+@BINPATH@/components/AddonUpdateService.js
+@BINPATH@/components/BlocklistPrompt.js
+@BINPATH@/components/BrowserCLH.js
+@BINPATH@/components/ColorPicker.js
+@BINPATH@/components/ContentDispatchChooser.js
+@BINPATH@/components/ContentPermissionPrompt.js
+@BINPATH@/components/ImageBlockingPolicy.js
+@BINPATH@/components/DirectoryProvider.js
+@BINPATH@/components/FilePicker.js
+@BINPATH@/components/FxAccountsPush.js
+@BINPATH@/components/HelperAppDialog.js
+@BINPATH@/components/LoginManagerPrompter.js
+@BINPATH@/components/MobileComponents.manifest
+@BINPATH@/components/MobileComponents.xpt
+@BINPATH@/components/NSSDialogService.js
+@BINPATH@/components/PersistentNotificationHandler.js
+@BINPATH@/components/PresentationDevicePrompt.js
+@BINPATH@/components/PresentationRequestUIGlue.js
+@BINPATH@/components/PromptService.js
+@BINPATH@/components/SessionStore.js
+@BINPATH@/components/SiteSpecificUserAgent.js
+@BINPATH@/components/Snippets.js
+
+@BINPATH@/components/XPIDialogService.js
+
+#ifdef ENABLE_MARIONETTE
+@BINPATH@/chrome/marionette@JAREXT@
+@BINPATH@/chrome/marionette.manifest
+@BINPATH@/components/marionette.manifest
+@BINPATH@/components/marionette.js
+#endif
+
+#ifdef PKG_LOCALE_MANIFEST
+#include @PKG_LOCALE_MANIFEST@
+#endif
+
+@BINPATH@/components/dom_audiochannel.xpt
+
+@BINPATH@/components/RemoteWebNavigation.js
+@BINPATH@/components/remotebrowserutils.manifest
diff --git a/mobile/android/installer/removed-files.in b/mobile/android/installer/removed-files.in
new file mode 100644
index 0000000000..855cb05388
--- /dev/null
+++ b/mobile/android/installer/removed-files.in
@@ -0,0 +1,4 @@
+update.locale
+README.txt
+defaults/preferences/healthreport-prefs.js
+components/dom_webspeech.xpt
diff --git a/mobile/android/javaaddons/Makefile.in b/mobile/android/javaaddons/Makefile.in
new file mode 100644
index 0000000000..a223d6e4eb
--- /dev/null
+++ b/mobile/android/javaaddons/Makefile.in
@@ -0,0 +1,9 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+include $(topsrcdir)/config/rules.mk
+
+include $(topsrcdir)/config/android-common.mk
+
+libs:: javaaddons-1.0.jar
diff --git a/mobile/android/javaaddons/java/org/mozilla/javaaddons/JavaAddonInterfaceV1.java b/mobile/android/javaaddons/java/org/mozilla/javaaddons/JavaAddonInterfaceV1.java
new file mode 100644
index 0000000000..caf31e7a54
--- /dev/null
+++ b/mobile/android/javaaddons/java/org/mozilla/javaaddons/JavaAddonInterfaceV1.java
@@ -0,0 +1,51 @@
+/* -*- 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.javaaddons;
+
+import android.content.Context;
+import org.json.JSONObject;
+
+public interface JavaAddonInterfaceV1 {
+ /**
+ * Callback interface for Gecko requests.
+ * <p/>
+ * For each instance of EventCallback, exactly one of sendResponse, sendError, must be called to prevent observer leaks.
+ * If more than one send* method is called, or if a single send method is called multiple times, an
+ * {@link IllegalStateException} will be thrown.
+ */
+ interface EventCallback {
+ /**
+ * Sends a success response with the given data.
+ *
+ * @param response The response data to send to Gecko. Can be any of the types accepted by
+ * JSONObject#put(String, Object).
+ */
+ public void sendSuccess(Object response);
+
+ /**
+ * Sends an error response with the given data.
+ *
+ * @param response The response data to send to Gecko. Can be any of the types accepted by
+ * JSONObject#put(String, Object).
+ */
+ public void sendError(Object response);
+ }
+
+ interface EventDispatcher {
+ void registerEventListener(EventListener listener, String... events);
+ void unregisterEventListener(EventListener listener);
+
+ void sendRequestToGecko(String event, JSONObject message, RequestCallback callback);
+ }
+
+ interface EventListener {
+ public void handleMessage(final Context context, final String event, final JSONObject message, final EventCallback callback);
+ }
+
+ interface RequestCallback {
+ void onResponse(final Context context, JSONObject jsonObject);
+ }
+}
diff --git a/mobile/android/javaaddons/moz.build b/mobile/android/javaaddons/moz.build
new file mode 100644
index 0000000000..c2f004cc19
--- /dev/null
+++ b/mobile/android/javaaddons/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+jar = add_java_jar('javaaddons-1.0')
+jar.sources = [
+ 'java/org/mozilla/javaaddons/JavaAddonInterfaceV1.java',
+]
+jar.javac_flags += ['-Xlint:all']
diff --git a/mobile/android/locales/Makefile.in b/mobile/android/locales/Makefile.in
new file mode 100644
index 0000000000..b8e541ba8d
--- /dev/null
+++ b/mobile/android/locales/Makefile.in
@@ -0,0 +1,87 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+include $(topsrcdir)/config/config.mk
+
+SUBMAKEFILES += \
+ $(DEPTH)/$(MOZ_BRANDING_DIRECTORY)/Makefile \
+ $(DEPTH)/$(MOZ_BRANDING_DIRECTORY)/locales/Makefile \
+ $(DEPTH)/mobile/locales/Makefile \
+ $(NULL)
+
+L10N_PREF_JS_EXPORTS = $(firstword $(wildcard $(LOCALE_SRCDIR)/mobile-l10n.js) \
+ $(srcdir)/en-US/mobile-l10n.js )
+L10N_PREF_JS_EXPORTS_PATH = $(FINAL_TARGET)/$(PREF_DIR)
+L10N_PREF_JS_EXPORTS_FLAGS = $(PREF_PPFLAGS) --silence-missing-directive-warnings
+PP_TARGETS += L10N_PREF_JS_EXPORTS
+
+include $(topsrcdir)/config/rules.mk
+
+include $(topsrcdir)/toolkit/locales/l10n.mk
+
+clobber-zip:
+ $(RM) $(STAGEDIST)/chrome/$(AB_CD).jar \
+ $(STAGEDIST)/chrome/$(AB_CD).manifest \
+ $(STAGEDIST)/defaults/pref/mobile-l10n.js
+ $(RM) -r $(STAGEDIST)/dictionaries \
+ $(STAGEDIST)/hyphenation \
+ $(STAGEDIST)/defaults/profile \
+ $(STAGEDIST)/chrome/$(AB_CD)
+
+# need to kill stage for repacks for now due to the library moves
+# in PACKAGE and UNPACKAGE
+# also clean up potential left-overs of multi-locale builds, notably
+# values-*/strings.xml and raw-*/suggestedsites.json.
+# Those would be in the way of a single locale build, which this
+# target is for
+clobber-stage:
+ $(RM) -rf $(STAGEDIST)
+ $(RM) $(DEPTH)/mobile/android/base/res/values-*/strings.xml
+ $(RM) $(DEPTH)/mobile/android/base/res/raw-*/suggestedsites.json
+
+libs-%:
+ @$(MAKE) -C $(DEPTH)/mobile/locales libs-$*
+ @$(MAKE) libs AB_CD=$* XPI_NAME=locale-$* PREF_DIR=defaults/pref
+ifeq ($(OS_TARGET),Android)
+ @$(MAKE) -C $(DEPTH)/mobile/android/base/locales AB_CD=$* XPI_NAME=locale-$*
+endif
+
+# Tailored target to just add the chrome processing for multi-locale builds
+chrome-%:
+ @$(MAKE) -C $(DEPTH)/mobile/locales chrome-$*
+ @$(MAKE) chrome AB_CD=$*
+ifeq ($(OS_TARGET),Android)
+ @$(MAKE) -C $(DEPTH)/mobile/android/base/locales chrome-$*
+endif
+
+# This is a generic target that will make a langpack and repack tarball
+# builds. It is called from the tinderbox scripts. Alter it with caution.
+
+installers-%: clobber-stage repackage-zip-%
+ @echo 'repackaging done'
+
+# When we unpack fennec on MacOS X the platform.ini and application.ini are in slightly
+# different locations that on all other platforms
+ifeq (Darwin, $(OS_ARCH))
+GECKO_PLATFORM_INI_PATH='$(STAGEDIST)/platform.ini'
+FENNEC_APPLICATION_INI_PATH='$(STAGEDIST)/application.ini'
+else
+GECKO_PLATFORM_INI_PATH='$(STAGEDIST)/platform.ini'
+FENNEC_APPLICATION_INI_PATH='$(STAGEDIST)/application.ini'
+endif
+
+ident:
+ @printf 'gecko_revision '
+ @$(PYTHON) $(topsrcdir)/config/printconfigsetting.py $(GECKO_PLATFORM_INI_PATH) Build SourceStamp
+ @printf 'fennec_revision '
+ @$(PYTHON) $(topsrcdir)/config/printconfigsetting.py $(FENNEC_APPLICATION_INI_PATH) App SourceStamp
+ @printf 'buildid '
+ @$(PYTHON) $(topsrcdir)/config/printconfigsetting.py $(FENNEC_APPLICATION_INI_PATH) App BuildID
+
+merge-%:
+ifdef LOCALE_MERGEDIR
+ $(RM) -rf $(LOCALE_MERGEDIR)
+ $(topsrcdir)/mach compare-locales --merge-dir $(LOCALE_MERGEDIR) $*
+endif
+ @echo
diff --git a/mobile/android/locales/all-locales b/mobile/android/locales/all-locales
new file mode 100644
index 0000000000..5f8dbbe2aa
--- /dev/null
+++ b/mobile/android/locales/all-locales
@@ -0,0 +1,89 @@
+an
+ar
+as
+ast
+az
+bg
+bn-BD
+bn-IN
+br
+ca
+cak
+cs
+cy
+da
+de
+dsb
+el
+en-GB
+en-ZA
+eo
+es-AR
+es-CL
+es-ES
+es-MX
+et
+eu
+fa
+ff
+fi
+fr
+fy-NL
+ga-IE
+gd
+gl
+gn
+gu-IN
+he
+hi-IN
+hr
+hsb
+hu
+hy-AM
+id
+is
+it
+ja
+ka
+kab
+kk
+kn
+ko
+lo
+lt
+lv
+mai
+ml
+mr
+ms
+my
+nb-NO
+ne-NP
+nl
+nn-NO
+or
+pa-IN
+pl
+pt-BR
+pt-PT
+rm
+ro
+ru
+sk
+sl
+son
+sq
+sr
+sv-SE
+ta
+te
+th
+tr
+tsz
+uk
+ur
+uz
+wo
+xh
+zh-CN
+zh-TW
diff --git a/mobile/android/locales/en-US/chrome/about.dtd b/mobile/android/locales/en-US/chrome/about.dtd
new file mode 100644
index 0000000000..6c493e4b4d
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/about.dtd
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!ENTITY aboutPage.title "About &brandShortName;">
+<!ENTITY aboutPage.warningVersion "&brandShortName; is experimental and may be unstable.">
+<!ENTITY aboutPage.telemetryStart " It automatically sends information about performance, hardware, usage and customizations back to ">
+<!ENTITY aboutPage.telemetryMozillaLink "&vendorShortName;">
+<!ENTITY aboutPage.telemetryEnd " to help make &brandShortName; better.">
+<!ENTITY aboutPage.checkForUpdates.link "Check for Updates »">
+<!ENTITY aboutPage.checkForUpdates.checking "Looking for updates…">
+<!ENTITY aboutPage.checkForUpdates.none "No updates available">
+<!ENTITY aboutPage.checkForUpdates.available2 "Download update">
+<!ENTITY aboutPage.checkForUpdates.downloading "Downloading update…">
+<!ENTITY aboutPage.checkForUpdates.downloaded2 "Install update">
+<!ENTITY aboutPage.faq.label "FAQ">
+<!ENTITY aboutPage.support.label "Support">
+<!ENTITY aboutPage.privacyPolicy.label "Privacy Policy">
+<!ENTITY aboutPage.rights.label "Know Your Rights">
+<!ENTITY aboutPage.relNotes.label "Release Notes">
+<!ENTITY aboutPage.credits.label "Credits">
+<!ENTITY aboutPage.license.label "Licensing Information">
+
+<!-- LOCALIZATION NOTE (aboutPage.logoTrademark): The message is explicitly about the word "Firefox" being trademarked, that's why we use it, instead of brandShortName. -->
+<!ENTITY aboutPage.logoTrademark "Firefox and the Firefox logos are trademarks of the Mozilla Foundation.">
diff --git a/mobile/android/locales/en-US/chrome/aboutAccounts.dtd b/mobile/android/locales/en-US/chrome/aboutAccounts.dtd
new file mode 100644
index 0000000000..0053759d29
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/aboutAccounts.dtd
@@ -0,0 +1,13 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY aboutAccounts.connected.title "Firefox Accounts">
+<!ENTITY aboutAccounts.connected.description "You are connected as">
+<!ENTITY aboutAccounts.syncPreferences.label "Tap here to check Sync settings">
+
+<!ENTITY aboutAccounts.noConnection.title "No Internet connection">
+<!ENTITY aboutAccounts.retry.label "Try again">
+
+<!ENTITY aboutAccounts.restrictedError.title "Restricted">
+<!ENTITY aboutAccounts.restrictedError.description "You cannot manage Firefox Accounts from this profile.">
diff --git a/mobile/android/locales/en-US/chrome/aboutAccounts.properties b/mobile/android/locales/en-US/chrome/aboutAccounts.properties
new file mode 100644
index 0000000000..67f20eb618
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/aboutAccounts.properties
@@ -0,0 +1,16 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# LOCALIZATION NOTE (relinkDenied.message): Ideally, this string is short (it's
+# a toast message).
+relinkDenied.message = Already signed in to Sync!
+# LOCALIZATION NOTE (relinkDenied.openPrefs): Ideally, this string is short (it's a
+# button label) and upper-case, to match Google and Android's convention.
+relinkDenied.openPrefs = PREFS
+
+relinkVerify.title = Are you sure you want to sign in to Sync?
+# LOCALIZATION NOTE (relinkVerify.message): Email address of a user previously signed in to Sync.
+relinkVerify.message = You were previously signed in to Sync with a different email address. Signing in will merge this browser’s bookmarks, passwords and other settings with %S
+relinkVerify.continue = Continue
+relinkVerify.cancel = Cancel
diff --git a/mobile/android/locales/en-US/chrome/aboutAddons.dtd b/mobile/android/locales/en-US/chrome/aboutAddons.dtd
new file mode 100644
index 0000000000..d4c89146b7
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/aboutAddons.dtd
@@ -0,0 +1,15 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY aboutAddons.title2 "Add-ons">
+<!ENTITY aboutAddons.header2 "Your Add-ons">
+<!ENTITY aboutAddons.options "Options">
+
+<!ENTITY addonAction.enable "Enable">
+<!ENTITY addonAction.disable "Disable">
+<!ENTITY addonAction.uninstall "Uninstall">
+<!ENTITY addonAction.undo "Undo">
+
+<!ENTITY addonUnsigned.message "This add-on could not be verified by &brandShortName;.">
+<!ENTITY addonUnsigned.learnMore "Learn more">
diff --git a/mobile/android/locales/en-US/chrome/aboutAddons.properties b/mobile/android/locales/en-US/chrome/aboutAddons.properties
new file mode 100644
index 0000000000..41ea1a80e0
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/aboutAddons.properties
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+addonType.extension=Extension
+addonType.theme=Theme
+addonType.locale=Locale
+
+addonStatus.uninstalled=%S will be uninstalled after restart.
+
+addons.browseAll=Browse all Firefox Add-ons
diff --git a/mobile/android/locales/en-US/chrome/aboutCertError.dtd b/mobile/android/locales/en-US/chrome/aboutCertError.dtd
new file mode 100644
index 0000000000..cd6c6ba632
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/aboutCertError.dtd
@@ -0,0 +1,38 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY % brandDTD
+ SYSTEM "chrome://branding/locale/brand.dtd">
+ %brandDTD;
+
+<!-- These strings are used by Firefox's custom about:certerror page,
+a replacement for the standard security certificate errors produced
+by NSS/PSM via netError.xhtml. -->
+
+<!ENTITY certerror.pagetitle "Untrusted Connection">
+<!ENTITY certerror.longpagetitle "This Connection is Untrusted">
+
+<!-- Localization note (certerror.introPara1) - The string "#1" will
+be replaced at runtime with the name of the server to which the user
+was trying to connect. -->
+<!ENTITY certerror.introPara1 "You have asked &brandShortName; to connect
+securely to <b>#1</b>, but we can't confirm that your connection is secure.">
+
+<!ENTITY certerror.whatShouldIDo.heading "What Should I Do?">
+<!ENTITY certerror.whatShouldIDo.content "If you usually connect to
+this site without problems, this error could mean that someone is
+trying to impersonate the site, and you shouldn't continue.">
+<!ENTITY certerror.getMeOutOfHere.label "Get me out of here!">
+
+<!ENTITY certerror.expert.heading "I Understand the Risks">
+<!ENTITY certerror.expert.content "If you understand what's going on, you
+can tell &brandShortName; to start trusting this site's identification.
+<b>Even if you trust the site, this error could mean that someone is
+tampering with your connection.</b>">
+<!ENTITY certerror.expert.contentPara2 "Don't add an exception unless
+you know there's a good reason why this site doesn't use trusted identification.">
+<!ENTITY certerror.addTemporaryException.label "Visit site">
+<!ENTITY certerror.addPermanentException.label "Add permanent exception">
+
+<!ENTITY certerror.technical.heading "Technical Details">
diff --git a/mobile/android/locales/en-US/chrome/aboutDevices.dtd b/mobile/android/locales/en-US/chrome/aboutDevices.dtd
new file mode 100644
index 0000000000..45d00393e7
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/aboutDevices.dtd
@@ -0,0 +1,14 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY aboutDevices.title "Devices">
+<!ENTITY aboutDevices.header "Your devices">
+<!ENTITY aboutDevices.refresh "Refresh">
+<!ENTITY aboutDevices.addDeviceHeader "Add a device">
+<!ENTITY aboutDevices.roku "Roku">
+<!ENTITY aboutDevices.chromecast "Chromecast">
+<!-- Localization note (aboutDevices.placeholder): this is the hint shown to the
+ user prompting them to input the IP address of a casting device. -->
+<!ENTITY aboutDevices.placeholder "IP address">
+<!ENTITY aboutDevices.connectManually "Connect manually">
diff --git a/mobile/android/locales/en-US/chrome/aboutDownloads.dtd b/mobile/android/locales/en-US/chrome/aboutDownloads.dtd
new file mode 100644
index 0000000000..fae992d4ff
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/aboutDownloads.dtd
@@ -0,0 +1,15 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY aboutDownloads.title "Downloads">
+<!ENTITY aboutDownloads.header "Your Downloads">
+<!ENTITY aboutDownloads.empty "No Downloads">
+
+<!ENTITY aboutDownloads.open "Open">
+<!ENTITY aboutDownloads.remove "Delete">
+<!ENTITY aboutDownloads.removeAll "Delete All">
+<!ENTITY aboutDownloads.pause "Pause">
+<!ENTITY aboutDownloads.resume "Resume">
+<!ENTITY aboutDownloads.cancel "Cancel">
+<!ENTITY aboutDownloads.retry "Retry">
diff --git a/mobile/android/locales/en-US/chrome/aboutDownloads.properties b/mobile/android/locales/en-US/chrome/aboutDownloads.properties
new file mode 100644
index 0000000000..59ca71f5cd
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/aboutDownloads.properties
@@ -0,0 +1,17 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# LOCALIZATION NOTE (downloadMessage.deleteAll):
+# Semicolon-separated list of plural forms. See:
+# http://developer.mozilla.org/en/docs/Localization_and_Plurals
+downloadMessage.deleteAll=Delete this download?;Delete #1 downloads?
+
+downloadAction.deleteAll=Delete All
+
+downloadState.downloading=Downloading…
+downloadState.canceled=Canceled
+downloadState.failed=Failed
+downloadState.paused=Paused
+downloadState.starting=Starting…
+downloadState.unknownSize=Unknown size
diff --git a/mobile/android/locales/en-US/chrome/aboutHealthReport.dtd b/mobile/android/locales/en-US/chrome/aboutHealthReport.dtd
new file mode 100644
index 0000000000..0cc20e38ef
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/aboutHealthReport.dtd
@@ -0,0 +1,6 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- LOCALIZATION NOTE (abouthealth.pagetitle): Firefox Health Report is a proper noun in en-US, please keep this in mind. -->
+<!ENTITY abouthealth.pagetitle "&brandShortName; Health Report">
diff --git a/mobile/android/locales/en-US/chrome/aboutHome.dtd b/mobile/android/locales/en-US/chrome/aboutHome.dtd
new file mode 100644
index 0000000000..cd79cfcb59
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/aboutHome.dtd
@@ -0,0 +1,7 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- This string should be kept in sync with the home_title string
+ in android_strings.dtd -->
+<!ENTITY abouthome.title "&brandShortName; Home">
diff --git a/mobile/android/locales/en-US/chrome/aboutHome.properties b/mobile/android/locales/en-US/chrome/aboutHome.properties
new file mode 100644
index 0000000000..f2c379590e
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/aboutHome.properties
@@ -0,0 +1,5 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+banner.firstrunHomepage.text=Welcome to your Homepage! Get back here every time you open a new tab.
diff --git a/mobile/android/locales/en-US/chrome/aboutLogins.dtd b/mobile/android/locales/en-US/chrome/aboutLogins.dtd
new file mode 100644
index 0000000000..2ae2cadcbd
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/aboutLogins.dtd
@@ -0,0 +1,10 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!ENTITY % brandDTD
+ SYSTEM "chrome://branding/locale/brand.dtd">
+ %brandDTD;
+<!ENTITY aboutLogins.title "Logins">
+<!ENTITY aboutLogins.update "Update">
+<!ENTITY aboutLogins.emptyLoginText "Keep your logins safe">
+<!ENTITY aboutLogins.emptyLoginHint "Logins and credentials you save using &brandShortName; will show up here.">
diff --git a/mobile/android/locales/en-US/chrome/aboutLogins.properties b/mobile/android/locales/en-US/chrome/aboutLogins.properties
new file mode 100644
index 0000000000..ba04b986d7
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/aboutLogins.properties
@@ -0,0 +1,27 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+loginsMenu.showPassword=Show password
+loginsMenu.copyPassword=Copy password
+loginsMenu.copyUsername=Copy username
+loginsMenu.editLogin=Edit login
+loginsMenu.delete=Delete
+
+loginsDialog.confirmDelete=Delete this login?
+loginsDialog.copy=Copy
+loginsDialog.confirm=OK
+loginsDialog.cancel=Cancel
+
+editLogin.fallbackTitle=Edit Login
+editLogin.saved1=Saved login
+editLogin.couldNotSave=Changes could not be saved
+
+loginsDetails.age=Age: %S days
+
+loginsDetails.copyFailed=Copy failed
+loginsDetails.passwordCopied=Password copied
+loginsDetails.usernameCopied=Username copied
+
+password-btn.show=Show
+password-btn.hide=Hide
diff --git a/mobile/android/locales/en-US/chrome/aboutPrivateBrowsing.dtd b/mobile/android/locales/en-US/chrome/aboutPrivateBrowsing.dtd
new file mode 100644
index 0000000000..8176940bb2
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/aboutPrivateBrowsing.dtd
@@ -0,0 +1,25 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY privatebrowsingpage.title "Private Browsing">
+
+<!-- Localisation note: the plus sign here is a shorthand way of expressing the word "and". Contextually the privatebrowsingpage.title.private string
+ is used as a title, with the privatebrowsingpage.title string preceding it but on a separate line.
+ So the final line will say "Private Browsing + Tracking Protection". -->
+<!ENTITY privatebrowsingpage.title.private "+ Tracking Protection">
+<!-- Localization note (privatebrowsingpage.title.normal1): "Private Browsing"
+ is capitalized in English to be consistent with our existing uses of the
+ term. -->
+<!ENTITY privatebrowsingpage.title.normal1 "You are not in Private Browsing">
+
+<!ENTITY privatebrowsingpage.description.trackingProtection "&brandShortName; blocks parts of the pages that may track your browsing activity.">
+<!ENTITY privatebrowsingpage.description.privateDetails "We won't remember any history, but downloaded files and new bookmarks will still be saved to your device.">
+
+<!-- Localization note (privatebrowsingpage.description.normal2): "Private
+ Browsing is capitalized in English to be consistent with our existing uses
+ of the term. -->
+<!ENTITY privatebrowsingpage.description.normal2 "In Private Browsing, we won't keep any of your browsing history or cookies. Bookmarks you add and files you download will still be saved on your device.">
+
+<!ENTITY privatebrowsingpage.link.private "Want to learn more?">
+<!ENTITY privatebrowsingpage.link.normal "Open a new private tab">
diff --git a/mobile/android/locales/en-US/chrome/browser.properties b/mobile/android/locales/en-US/chrome/browser.properties
new file mode 100644
index 0000000000..ae2fc3a8c1
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/browser.properties
@@ -0,0 +1,462 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+addonsConfirmInstall.title=Installing Add-on
+addonsConfirmInstall.install=Install
+
+addonsConfirmInstallUnsigned.title=Unverified add-on
+addonsConfirmInstallUnsigned.message=This site would like to install an unverified add-on. Proceed at your own risk.
+
+# Alerts
+alertAddonsDownloading=Downloading add-on
+alertAddonsInstalledNoRestart.message=Installation complete
+
+# LOCALIZATION NOTE (alertAddonsInstalledNoRestart.action2): Ideally, this string is short (it's a
+# button label) and upper-case, to match Google and Android's convention.
+alertAddonsInstalledNoRestart.action2=ADD-ONS
+
+alertDownloadsStart2=Download starting
+alertDownloadsDone2=Download complete
+alertCantOpenDownload=Can't open file. Tap to save it.
+alertDownloadsSize=Download too big
+alertDownloadsNoSpace=Not enough storage space
+alertDownloadsToast=Download started…
+alertDownloadsPause=Pause
+alertDownloadsResume=Resume
+alertDownloadsCancel=Cancel
+# LOCALIZATION NOTE (alertDownloadSucceeded): This text is shown as a snackbar inside the app after a
+# successful download. %S will be replaced by the file name of the download.
+alertDownloadSucceeded=%S downloaded
+# LOCALIZATION NOTE (downloads.disabledInGuest): This message appears in a toast
+# when the user tries to download something in Guest mode.
+downloads.disabledInGuest=Downloads are disabled in guest sessions
+
+# LOCALIZATION NOTE (alertSearchEngineAddedToast, alertSearchEngineErrorToast, alertSearchEngineDuplicateToast)
+# %S will be replaced by the name of the search engine (exposed by the current page)
+# that has been added; for example, 'Google'.
+alertSearchEngineAddedToast='%S' has been added as a search engine
+alertSearchEngineErrorToast=Couldn't add '%S' as a search engine
+alertSearchEngineDuplicateToast='%S' is already one of your search engines
+
+alertPrintjobToast=Printing…
+
+downloadCancelPromptTitle1=Abort Download
+downloadCancelPromptMessage1=Do you want to abort this download?
+
+addonError.titleError=Error
+addonError.titleBlocked=Blocked add-on
+addonError.learnMore=Learn more
+
+# LOCALIZATION NOTE (unsignedAddonsDisabled.title, unsignedAddonsDisabled.message):
+# These strings will appear in a dialog when Firefox detects that installed add-ons cannot be verified.
+unsignedAddonsDisabled.title=Unverified add-ons
+unsignedAddonsDisabled.message=One or more installed add-ons cannot be verified and have been disabled.
+unsignedAddonsDisabled.dismiss=Dismiss
+unsignedAddonsDisabled.viewAddons=View add-ons
+
+# LOCALIZATION NOTE (addonError-1, addonError-2, addonError-3, addonError-4, addonError-5):
+# #1 is the add-on name, #2 is the add-on host, #3 is the application name
+addonError-1=The add-on could not be downloaded because of a connection failure on #2.
+addonError-2=The add-on from #2 could not be installed because it does not match the add-on #3 expected.
+addonError-3=The add-on downloaded from #2 could not be installed because it appears to be corrupt.
+addonError-4=#1 could not be installed because #3 cannot modify the needed file.
+addonError-5=#3 has prevented #2 from installing an unverified add-on.
+
+# LOCALIZATION NOTE (addonLocalError-1, addonLocalError-2, addonLocalError-3, addonLocalError-4, addonLocalError-5, addonErrorIncompatible, addonErrorBlocklisted):
+# #1 is the add-on name, #3 is the application name, #4 is the application version
+addonLocalError-1=This add-on could not be installed because of a filesystem error.
+addonLocalError-2=This add-on could not be installed because it does not match the add-on #3 expected.
+addonLocalError-3=This add-on could not be installed because it appears to be corrupt.
+addonLocalError-4=#1 could not be installed because #3 cannot modify the needed file.
+addonLocalError-5=This add-on could not be installed because it has not been verified.
+addonErrorIncompatible=#1 could not be installed because it is not compatible with #3 #4.
+addonErrorBlocklisted=#1 could not be installed because it has a high risk of causing stability or security problems.
+
+# Notifications
+notificationRestart.normal=Restart to complete changes.
+notificationRestart.blocked=Unsafe add-ons installed. Restart to disable.
+notificationRestart.button=Restart
+doorhanger.learnMore=Learn more
+
+# Popup Blocker
+
+# LOCALIZATION NOTE (popup.message): Semicolon-separated list of plural forms.
+# #1 is brandShortName and #2 is the number of pop-ups blocked.
+popup.message=#1 prevented this site from opening a pop-up window. Would you like to show it?;#1 prevented this site from opening #2 pop-up windows. Would you like to show them?
+popup.dontAskAgain=Don't ask again for this site
+popup.show=Show
+popup.dontShow=Don't show
+
+# SafeBrowsing
+safeBrowsingDoorhanger=This site has been identified as containing malware or a phishing attempt. Be careful.
+
+# LOCALIZATION NOTE (blockPopups.label2): Label that will be used in
+# site settings dialog.
+blockPopups.label2=Popups
+
+# XPInstall
+xpinstallPromptWarning2=%S prevented this site (%S) from asking you to install software on your device.
+xpinstallPromptWarningLocal=%S prevented this add-on (%S) from installing on your device.
+xpinstallPromptWarningDirect=%S prevented an add-on from installing on your device.
+xpinstallPromptAllowButton=Allow
+xpinstallDisabledMessageLocked=Software installation has been disabled by your system administrator.
+xpinstallDisabledMessage2=Software installation is currently disabled. Press Enable and try again.
+xpinstallDisabledButton=Enable
+
+# Site Identity
+identity.identified.verifier=Verified by: %S
+identity.identified.verified_by_you=You have added a security exception for this site
+identity.identified.state_and_country=%S, %S
+identity.identified.title_with_country=%S (%S)
+
+# Geolocation UI
+geolocation.allow=Share
+geolocation.dontAllow=Don't share
+geolocation.ask=Share your location with %S?
+# LOCALIZATION NOTE (geolocation.location): Label that will be used in
+# site settings dialog.
+geolocation.location=Location
+# LOCALIZATION NOTE (geolocation.dontAskAgain): This label appears next to a
+# checkbox to indicate whether or not the user wants to make a permanent decision.
+geolocation.dontAskAgain=Don't ask again for this site
+
+# Desktop notification UI
+desktopNotification2.allow=Always
+desktopNotification2.dontAllow=Never
+desktopNotification2.ask=Would you like to receive notifications from this site?
+# LOCALIZATION NOTE (desktopNotification.notifications): Label that will be
+# used in site settings dialog.
+desktopNotification.notifications=Notifications
+
+# FlyWeb UI
+# LOCALIZATION NOTE (flyWebPublishServer.allow): This is an experimental feature only shipping in Nightly, and doesn't need translation.
+flyWebPublishServer.allow=Allow
+# LOCALIZATION NOTE (flyWebPublishServer.dontAllow): This is an experimental feature only shipping in Nightly, and doesn't need translation.
+flyWebPublishServer.dontAllow=Deny
+# LOCALIZATION NOTE (flyWebPublishServer.ask): This is an experimental feature only shipping in Nightly, and doesn't need translation.
+flyWebPublishServer.ask=Would you like to let this site start a server accessible to nearby devices and people?
+# LOCALIZATION NOTE (flyWebPublishServer.dontAskAgain): This is an experimental feature only shipping in Nightly, and doesn't need translation.
+flyWebPublishServer.dontAskAgain=Don't ask again for this site
+# LOCALIZATION NOTE (flyWebPublishServer.publishServer): This is an experimental feature only shipping in Nightly, and doesn't need translation.
+flyWebPublishServer.publishServer=Publish Server
+
+# Imageblocking
+imageblocking.downloadedImage=Image unblocked
+imageblocking.showAllImages=Show All
+
+# Device Storage API
+deviceStorageMusic.allow=Allow
+deviceStorageMusic.dontAllow=Don't allow
+deviceStorageMusic.ask=Allow %S access to your music?
+# LOCALIZATION NOTE (deviceStorageMusic.dontAskAgain): This label appears next to a
+# checkbox to indicate whether or not the user wants to make a permanent decision.
+deviceStorageMusic.dontAskAgain=Don't ask again for this site
+
+deviceStoragePictures.allow=Allow
+deviceStoragePictures.dontAllow=Don't allow
+deviceStoragePictures.ask=Allow %S access to your images?
+# LOCALIZATION NOTE (deviceStoragePictures.dontAskAgain): This label appears next to a
+# checkbox to indicate whether or not the user wants to make a permanent decision.
+deviceStoragePictures.dontAskAgain=Don't ask again for this site
+
+deviceStorageSdcard.allow=Allow
+deviceStorageSdcard.dontAllow=Don't allow
+deviceStorageSdcard.ask=Allow %S access to external storage?
+# LOCALIZATION NOTE (deviceStorageSdcard.dontAskAgain): This label appears next to a
+# checkbox to indicate whether or not the user wants to make a permanent decision.
+deviceStorageSdcard.dontAskAgain=Don't ask again for this site
+
+deviceStorageVideos.allow=Allow
+deviceStorageVideos.dontAllow=Don't allow
+deviceStorageVideos.ask=Allow %S access to your videos?
+# LOCALIZATION NOTE (deviceStorageVideos.dontAskAgain): This label appears next to a
+# checkbox to indicate whether or not the user wants to make a permanent decision.
+deviceStorageVideos.dontAskAgain=Don't ask again for this site
+
+# New Tab Popup
+# LOCALIZATION NOTE (newtabpopup, newprivatetabpopup): Semicolon-separated list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 number of tabs
+newtabpopup.opened=New tab opened;#1 new tabs opened
+newprivatetabpopup.opened=New private tab opened;#1 new private tabs opened
+
+# LOCALIZATION NOTE (newtabpopup.switch): Ideally, this string is short (it's a
+# button label) and upper-case, to match Google and Android's convention.
+newtabpopup.switch=SWITCH
+
+# Undo close tab toast
+# LOCALIZATION NOTE (undoCloseToast.message): This message appears in a toast
+# when the user closes a tab. %S is the title of the tab that was closed.
+undoCloseToast.message=Closed %S
+
+# Private Tab closed message
+# LOCALIZATION NOTE (privateClosedMessage.message): This message appears
+# when the user closes a private tab.
+privateClosedMessage.message=Closed Private Browsing
+
+# LOCALIZATION NOTE (undoCloseToast.messageDefault): This message appears in a
+# toast when the user closes a tab if there is no title to display.
+undoCloseToast.messageDefault=Closed tab
+
+# LOCALIZATION NOTE (undoCloseToast.action2): Ideally, this string is short (it's a
+# button label) and upper-case, to match Google and Android's convention.
+undoCloseToast.action2=UNDO
+
+# Offline web applications
+offlineApps.ask=Allow %S to store data on your device for offline use?
+offlineApps.dontAskAgain=Don't ask again for this site
+offlineApps.allow=Allow
+offlineApps.dontAllow2=Don't allow
+
+# LOCALIZATION NOTE (offlineApps.offlineData): Label that will be used in
+# site settings dialog.
+offlineApps.offlineData=Offline Data
+
+# LOCALIZATION NOTE (password.logins): Label that will be used in
+ # site settings dialog.
+password.logins=Logins
+# LOCALIZATION NOTE (password.save): This should match
+# saveButton in passwordmgr.properties
+password.save=Save
+# LOCALIZATION NOTE (password.dontSave): This should match
+# dontSaveButton in passwordmgr.properties
+password.dontSave=Don't save
+
+# LOCALIZATION NOTE (browser.menu.showCharacterEncoding): Set to the string
+# "true" (spelled and capitalized exactly that way) to show the "Character
+# Encoding" menu in the site menu. Any other value will hide it. Without this
+# setting, the "Character Encoding" menu must be enabled via Preferences.
+# This is not a string to translate. If users frequently use the "Character Encoding"
+# menu, set this to "true". Otherwise, you can leave it as "false".
+browser.menu.showCharacterEncoding=false
+
+# Text Selection
+selectionHelper.textCopied=Text copied to clipboard
+
+# Casting
+# LOCALIZATION NOTE (casting.sendToDevice): Label that will be used in the
+# dialog/prompt.
+casting.sendToDevice=Send to Device
+casting.mirrorTab=Mirror Tab
+casting.mirrorTabStop=Stop Mirror
+
+# Context menu
+contextmenu.openInNewTab=Open Link in New Tab
+contextmenu.openInPrivateTab=Open Link in Private Tab
+contextmenu.share=Share
+contextmenu.copyLink=Copy Link
+contextmenu.shareLink=Share Link
+contextmenu.bookmarkLink=Bookmark Link
+contextmenu.copyEmailAddress=Copy Email Address
+contextmenu.shareEmailAddress=Share Email Address
+contextmenu.copyPhoneNumber=Copy Phone Number
+contextmenu.sharePhoneNumber=Share Phone Number
+contextmenu.changeInputMethod=Select Input Method
+contextmenu.fullScreen=Full Screen
+contextmenu.viewImage=View Image
+contextmenu.copyImageLocation=Copy Image Location
+contextmenu.shareImage=Share Image
+# LOCALIZATION NOTE (contextmenu.search):
+# The label of the contextmenu item which allows you to search with your default search engine for
+# the text you have selected. %S is the name of the search engine. For example, "Google".
+contextmenu.search=%S Search
+contextmenu.saveImage=Save Image
+contextmenu.showImage=Show Image
+contextmenu.setImageAs=Set Image As
+# LOCALIZATION NOTE (contextmenu.addSearchEngine3): This string should be rather short. If it is
+# significantly longer than the translation for the "Paste" action then this might trigger an
+# Android bug positioning the floating text selection partially off the screen. This issue heavily
+# depends on the screen size and the specific translations. For English "Paste" / "Add search engine"
+# is working while "Paste" / "Add as search engine" triggers the bug. See bug 1262098 for more details.
+# Manual testing the scenario described in bug 1262098 is highly recommended.
+contextmenu.addSearchEngine3=Add Search Engine
+contextmenu.playMedia=Play
+contextmenu.pauseMedia=Pause
+contextmenu.shareMedia=Share Video
+contextmenu.showControls2=Show Controls
+contextmenu.mute=Mute
+contextmenu.unmute=Unmute
+contextmenu.saveVideo=Save Video
+contextmenu.saveAudio=Save Audio
+contextmenu.addToContacts=Add to Contacts
+# LOCALIZATION NOTE (contextmenu.sendToDevice):
+# The label that will be used in the contextmenu and the pageaction
+contextmenu.sendToDevice=Send to Device
+
+contextmenu.copy=Copy
+contextmenu.cut=Cut
+contextmenu.selectAll=Select All
+contextmenu.paste=Paste
+
+contextmenu.call=Call
+
+# Select UI
+selectHelper.closeMultipleSelectDialog=Done
+
+#Input widgets UI
+inputWidgetHelper.date=Pick a date
+inputWidgetHelper.datetime=Pick a date and a time
+inputWidgetHelper.datetime-local=Pick a date and a time
+inputWidgetHelper.time=Pick a time
+inputWidgetHelper.week=Pick a week
+inputWidgetHelper.month=Pick a month
+inputWidgetHelper.cancel=Cancel
+inputWidgetHelper.set=Set
+inputWidgetHelper.clear=Clear
+
+# Web Console API
+stacktrace.anonymousFunction=<anonymous>
+stacktrace.outputMessage=Stack trace from %S, function %S, line %S.
+timer.start=%S: timer started
+
+# LOCALIZATION NOTE (timer.end):
+# This string is used to display the result of the console.timeEnd() call.
+# %1$S=name of timer, %2$S=number of milliseconds
+timer.end=%1$S: %2$Sms
+
+# Click to play plugins
+clickToPlayPlugins.message2=%S contains plugin content. Would you like to activate it?
+clickToPlayPlugins.activate=Activate
+clickToPlayPlugins.dontActivate=Don't activate
+# LOCALIZATION NOTE (clickToPlayPlugins.dontAskAgain): This label appears next to a
+# checkbox to indicate whether or not the user wants to make a permanent decision.
+clickToPlayPlugins.dontAskAgain=Don't ask again for this site
+# LOCALIZATION NOTE (clickToPlayPlugins.plugins): Label that
+# will be used in site settings dialog.
+clickToPlayPlugins.plugins=Plugins
+
+# Site settings dialog
+# LOCALIZATION NOTE (siteSettings.labelToValue): This string will be used to
+# dislay a list of current permissions settings for a site.
+# Example: "Store Offline Data: Allow"
+siteSettings.labelToValue=%S: %S
+
+masterPassword.incorrect=Incorrect password
+
+# Debugger
+# LOCALIZATION NOTE (remoteIncomingPromptTitle): The title displayed on the
+# dialog that prompts the user to allow the incoming connection.
+remoteIncomingPromptTitle=Incoming Connection
+# LOCALIZATION NOTE (remoteIncomingPromptUSB): The message displayed on the
+# dialog that prompts the user to allow an incoming USB connection.
+remoteIncomingPromptUSB=Allow USB debugging connection?
+# LOCALIZATION NOTE (remoteIncomingPromptUSB): The message displayed on the
+# dialog that prompts the user to allow an incoming TCP connection.
+remoteIncomingPromptTCP=Allow remote debugging connection from %1$S:%2$S? This connection requires a QR code to be scanned in order to authenticate the remote device's certificate. You can avoid future scans by remembering the device.
+# LOCALIZATION NOTE (remoteIncomingPromptDeny): This button will deny an
+# an incoming remote debugger connection.
+remoteIncomingPromptDeny=Deny
+# LOCALIZATION NOTE (remoteIncomingPromptAllow): This button will allow an
+# an incoming remote debugger connection.
+remoteIncomingPromptAllow=Allow
+# LOCALIZATION NOTE (remoteIncomingPromptScan): This button will start a QR
+# code scanner to authenticate an incoming remote debugger connection. The
+# connection will be allowed assuming the scan succeeds.
+remoteIncomingPromptScan=Scan
+# LOCALIZATION NOTE (remoteIncomingPromptScanAndRemember): This button will
+# start a QR code scanner to authenticate an incoming remote debugger
+# connection. The connection will be allowed assuming the scan succeeds, and
+# the other endpoint's certificate will be saved to skip future scans for this
+# client.
+remoteIncomingPromptScanAndRemember=Scan and Remember
+# LOCALIZATION NOTE (remoteQRScanFailedPromptTitle): The title displayed in a
+# dialog when we are unable to complete the QR code scan for an incoming remote
+# debugging connection.
+remoteQRScanFailedPromptTitle=QR Scan Failed
+# LOCALIZATION NOTE (remoteQRScanFailedPromptMessage): The message displayed in
+# a dialog when we are unable to complete the QR code scan for an incoming
+# remote debugging connection.
+remoteQRScanFailedPromptMessage=Unable to scan QR code for remote debugging. Verify that the Barcode Scanner app is installed and retry connecting.
+# LOCALIZATION NOTE (remoteQRScanFailedPromptOK): This button dismisses the
+# dialog that appears when we are unable to complete the QR code scan for an
+# incoming remote debugging connection.
+remoteQRScanFailedPromptOK=OK
+
+# LOCALIZATION NOTE (remoteNotificationTitle): %S is the name of the app.
+remoteNotificationTitle=%S debugging enabled
+# LOCALIZATION NOTE (remoteNotificationGenericName): a generic name to use
+# if the name of the app is not available.
+remoteNotificationGenericName=App
+# LOCALIZATION NOTE (remoteNotificationMessage): %S is the port on which
+# the remote debugger server is listening.
+remoteNotificationMessage=Listening on port %S
+# LOCALIZATION NOTE (remoteStartNotificationTitle): %S is the name of the app.
+remoteStartNotificationTitle=Activate debugging for %S
+# LOCALIZATION NOTE (remoteStartNotificationMessage):
+remoteStartNotificationMessage=Touch to activate remote debugger
+
+# Helper apps
+helperapps.open=Open
+helperapps.ignore=Ignore
+helperapps.dontAskAgain=Don't ask again for this site
+helperapps.openWithApp2=Open With %S App
+helperapps.openWithList2=Open With an App
+helperapps.always=Always
+helperapps.never=Never
+helperapps.pick=Complete action using
+helperapps.saveToDisk=Download
+helperapps.alwaysUse=Always
+helperapps.useJustOnce=Just once
+
+#Lightweight themes
+# LOCALIZATION NOTE (lwthemeInstallRequest.message): %S will be replaced with
+# the host name of the site.
+lwthemeInstallRequest.message=This site (%S) attempted to install a theme.
+lwthemeInstallRequest.allowButton=Allow
+
+# LOCALIZATION NOTE (getUserMedia.shareCamera.message, getUserMedia.shareMicrophone.message, getUserMedia.shareCameraAndMicrophone.message, getUserMedia.sharingCamera.message, getUserMedia.sharingMicrophone.message, getUserMedia.sharingCameraAndMicrophone.message): %S is the website origin (e.g. www.mozilla.org)
+getUserMedia.shareCamera.message = Would you like to share your camera with %S?
+getUserMedia.shareMicrophone.message = Would you like to share your microphone with %S?
+getUserMedia.shareCameraAndMicrophone.message = Would you like to share your camera and microphone with %S?
+getUserMedia.denyRequest.label = Don't Share
+getUserMedia.shareRequest.label = Share
+getUserMedia.videoSource.default = Camera %S
+getUserMedia.videoSource.frontCamera = Front facing camera
+getUserMedia.videoSource.backCamera = Back facing camera
+getUserMedia.videoSource.none = No Video
+getUserMedia.videoSource.tabShare = Choose a tab to stream
+getUserMedia.videoSource.prompt = Video source
+getUserMedia.audioDevice.default = Microphone %S
+getUserMedia.audioDevice.none = No Audio
+getUserMedia.audioDevice.prompt = Microphone to use
+getUserMedia.sharingCamera.message2 = Camera is on
+getUserMedia.sharingMicrophone.message2 = Microphone is on
+getUserMedia.sharingCameraAndMicrophone.message2 = Camera and microphone are on
+getUserMedia.blockedCameraAccess = Camera has been blocked.
+getUserMedia.blockedMicrophoneAccess = Microphone has been blocked.
+getUserMedia.blockedCameraAndMicrophoneAccess = Camera and microphone have been blocked.
+
+# LOCALIZATION NOTE (readerMode.toolbarTip):
+# Tip shown to users the first time we hide the reader mode toolbar.
+readerMode.toolbarTip=Tap the screen to show reader options
+
+#Open in App
+openInApp.pageAction = Open in App
+openInApp.ok = OK
+openInApp.cancel = Cancel
+
+#Tab sharing
+tabshare.title = "Choose a tab to stream"
+#Tabs in context menus
+browser.menu.context.default = Link
+browser.menu.context.img = Image
+browser.menu.context.video = Video
+browser.menu.context.audio = Audio
+browser.menu.context.tel = Phone
+browser.menu.context.mailto = Mail
+
+# "Subscribe to page" prompts created in FeedHandler.js
+feedHandler.chooseFeed=Choose feed
+feedHandler.subscribeWith=Subscribe with
+
+# LOCALIZATION NOTE (nativeWindow.deprecated):
+# This string is shown in the console when someone uses deprecated NativeWindow apis.
+# %1$S=name of the api that's deprecated, %2$S=New API to use. This may be a url to
+# a file they should import or the name of an api.
+nativeWindow.deprecated=%1$S is deprecated. Please use %2$S instead
+
+# Vibration API permission prompt
+vibrationRequest.message = Allow this site to vibrate your device?
+vibrationRequest.denyButton = Don't allow
+vibrationRequest.allowButton = Allow
diff --git a/mobile/android/locales/en-US/chrome/checkbox.dtd b/mobile/android/locales/en-US/chrome/checkbox.dtd
new file mode 100644
index 0000000000..523375f7c5
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/checkbox.dtd
@@ -0,0 +1,6 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY checkbox.yes.label "Yes">
+<!ENTITY checkbox.no.label "No">
diff --git a/mobile/android/locales/en-US/chrome/config.dtd b/mobile/android/locales/en-US/chrome/config.dtd
new file mode 100644
index 0000000000..95502af40e
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/config.dtd
@@ -0,0 +1,21 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<!ENTITY toolbar.searchPlaceholder "Search">
+
+<!ENTITY newPref.namePlaceholder "Name">
+
+<!ENTITY newPref.valueBoolean "Boolean">
+<!ENTITY newPref.valueString "String">
+<!ENTITY newPref.valueInteger "Integer">
+
+<!ENTITY newPref.stringPlaceholder "Enter a string">
+<!ENTITY newPref.numberPlaceholder "Enter a number">
+
+<!ENTITY newPref.toggleButton "Toggle">
+<!ENTITY newPref.cancelButton "Cancel">
+
+<!ENTITY contextMenu.copyPrefName "Copy Name">
+<!ENTITY contextMenu.copyPrefValue "Copy Value">
diff --git a/mobile/android/locales/en-US/chrome/config.properties b/mobile/android/locales/en-US/chrome/config.properties
new file mode 100644
index 0000000000..61fdf73b32
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/config.properties
@@ -0,0 +1,9 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+newPref.createButton=Create
+newPref.changeButton=Change
+
+pref.toggleButton=Toggle
+pref.resetButton=Reset
diff --git a/mobile/android/locales/en-US/chrome/devicePrompt.properties b/mobile/android/locales/en-US/chrome/devicePrompt.properties
new file mode 100644
index 0000000000..493abd6c1b
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/devicePrompt.properties
@@ -0,0 +1,5 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+deviceMenu.title=Nearby Devices
diff --git a/mobile/android/locales/en-US/chrome/handling.properties b/mobile/android/locales/en-US/chrome/handling.properties
new file mode 100644
index 0000000000..d0aa08bb1f
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/handling.properties
@@ -0,0 +1,5 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+download.blocked=Unable to download file
diff --git a/mobile/android/locales/en-US/chrome/phishing.dtd b/mobile/android/locales/en-US/chrome/phishing.dtd
new file mode 100644
index 0000000000..3123556da8
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/phishing.dtd
@@ -0,0 +1,23 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY safeb.palm.accept.label "Get me out of here!">
+<!ENTITY safeb.palm.decline.label "Ignore this warning">
+<!ENTITY safeb.palm.reportPage.label "Why was this page blocked?">
+
+<!ENTITY safeb.blocked.malwarePage.title "Reported Attack Page!">
+<!-- Localization note (safeb.blocked.malware.shortDesc) - Please don't translate the contents of the <span id="malware_sitename"/> tag. It will be replaced at runtime with a domain name (e.g. www.badsite.com) -->
+<!ENTITY safeb.blocked.malwarePage.shortDesc "This web page at <span id='malware_sitename'/> has been reported as an attack page and has been blocked based on your security preferences.">
+<!ENTITY safeb.blocked.malwarePage.longDesc "<p>Attack pages try to install programs that steal private information, use your computer to attack others, or damage your system.</p><p>Some attack pages intentionally distribute harmful software, but many are compromised without the knowledge or permission of their owners.</p>">
+
+<!ENTITY safeb.blocked.phishingPage.title3 "Deceptive Site!">
+<!-- Localization note (safeb.blocked.phishingPage.shortDesc3) - Please don't translate the contents of the <span id="phishing_sitename"/> tag. It will be replaced at runtime with a domain name (e.g. www.badsite.com) -->
+<!ENTITY safeb.blocked.phishingPage.shortDesc3 "This web page at <span id='phishing_sitename'/> has been reported as a deceptive site and has been blocked based on your security preferences.">
+<!ENTITY safeb.blocked.phishingPage.longDesc3 "<p>Deceptive sites are designed to trick you into doing something dangerous, like installing software, or revealing your personal information, like passwords, phone numbers or credit cards.</p><p>Entering any information on this web page may result in identity theft or other fraud.</p>">
+
+<!ENTITY safeb.blocked.unwantedPage.title "Reported Unwanted Software Site!">
+<!-- Localization note (safeb.blocked.unwanted.shortDesc) - Please don't translate the contents of the <span id="unwanted_sitename"/> tag. It will be replaced at runtime with a domain name (e.g. www.badsite.com) -->
+<!ENTITY safeb.blocked.unwantedPage.shortDesc "This web page at <span id='unwanted_sitename'/> has been reported to contain unwanted software and has been blocked based on your security preferences.">
+<!ENTITY safeb.blocked.unwantedPage.longDesc "Unwanted software pages try to install software that can be deceptive and affect your system in unexpected ways.">
+
diff --git a/mobile/android/locales/en-US/chrome/pippki.properties b/mobile/android/locales/en-US/chrome/pippki.properties
new file mode 100644
index 0000000000..1102f0b00a
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/pippki.properties
@@ -0,0 +1,85 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+nssdialogs.ok.label=OK
+nssdialogs.cancel.label=Cancel
+
+# These strings are stolen from security/manager/locales/en-US/chrome/pippki/pippki.dtd
+downloadCert.title=Downloading Certificate
+downloadCert.message1=You have been asked to trust a new Certificate Authority (CA).
+downloadCert.viewCert.label=View
+downloadCert.trustSSL=Trust to identify websites.
+downloadCert.trustEmail=Trust to identify email users.
+downloadCert.trustObjSign=Trust to identify software developers.
+pkcs12.getpassword.title=Password Entry Dialog
+pkcs12.getpassword.message=Please enter the password that was used to encrypt this certificate backup.
+clientAuthAsk.title=User Identification Request
+clientAuthAsk.message1=This site has requested that you identify yourself with a certificate:
+clientAuthAsk.message2=Choose a certificate to present as identification:
+clientAuthAsk.message3=Details of selected certificate:
+clientAuthAsk.remember.label=Remember this decision
+# LOCALIZATION NOTE(clientAuthAsk.nickAndSerial): Represents a single cert when
+# the user is choosing from a list of certificates.
+# %1$S is the nickname of the cert.
+# %2$S is the serial number of the cert in AA:BB:CC hex format.
+clientAuthAsk.nickAndSerial=%1$S [%2$S]
+# LOCALIZATION NOTE(clientAuthAsk.hostnameAndPort):
+# %1$S is the hostname of the server.
+# %2$S is the port of the server.
+clientAuthAsk.hostnameAndPort=%1$S:%2$S
+# LOCALIZATION NOTE(clientAuthAsk.organization): %S is the Organization of the
+# server cert.
+clientAuthAsk.organization=Organization: "%S"
+# LOCALIZATION NOTE(clientAuthAsk.issuer): %S is the Organization of the
+# issuer cert of the server cert.
+clientAuthAsk.issuer=Issued Under: "%S"
+# LOCALIZATION NOTE(clientAuthAsk.issuedTo): %1$S is the Distinguished Name of
+# the currently selected client cert, such as "CN=John Doe,OU=Example" (without
+# quotes).
+clientAuthAsk.issuedTo=Issued to: %1$S
+# LOCALIZATION NOTE(clientAuthAsk.serial): %1$S is the serial number of the
+# selected cert in AA:BB:CC hex format.
+clientAuthAsk.serial=Serial number: %1$S
+# LOCALIZATION NOTE(clientAuthAsk.validityPeriod):
+# %1$S is the already localized notBefore date of the selected cert.
+# %2$S is the already localized notAfter date of the selected cert.
+clientAuthAsk.validityPeriod=Valid from %1$S to %2$S
+# LOCALIZATION NOTE(clientAuthAsk.keyUsages): %1$S is a comma separated list of
+# already localized key usages the selected cert is valid for.
+clientAuthAsk.keyUsages=Key Usages: %1$S
+# LOCALIZATION NOTE(clientAuthAsk.emailAddresses): %1$S is a comma separated
+# list of e-mail addresses the selected cert is valid for.
+clientAuthAsk.emailAddresses=Email addresses: %1$S
+# LOCALIZATION NOTE(clientAuthAsk.issuedBy): %1$S is the Distinguished Name of
+# the cert which issued the selected cert.
+clientAuthAsk.issuedBy=Issued by: %1$S
+# LOCALIZATION NOTE(clientAuthAsk.storedOn): %1$S is the name of the PKCS #11
+# token the selected cert is stored on.
+clientAuthAsk.storedOn=Stored on: %1$S
+clientAuthAsk.viewCert.label=View
+
+certmgr.title=Certificate Details
+# These strings are stolen from security/manager/locales/en-US/chrome/pippki/certManager.dtd
+certmgr.subjectinfo.label=Issued To
+certmgr.issuerinfo.label=Issued By
+certmgr.periodofvalidity.label=Period of Validity
+certmgr.fingerprints.label=Fingerprints
+certdetail.cn=Common Name (CN): %1$S
+certdetail.o=Organization (O): %1$S
+certdetail.ou=Organizational Unit (OU): %1$S
+# LOCALIZATION NOTE(certdetail.serialnumber): %1$S is the serial number of the
+# cert being viewed in AA:BB:CC hex format.
+certdetail.serialnumber=Serial Number: %1$S
+# LOCALIZATION NOTE(certdetail.sha256fingerprint): %1$S is the SHA-256
+# Fingerprint of the cert being viewed in AA:BB:CC hex format.
+certdetail.sha256fingerprint=SHA-256 Fingerprint: %1$S
+# LOCALIZATION NOTE(certdetail.sha1fingerprint): %1$S is the SHA-1 Fingerprint
+# of the cert being viewed in AA:BB:CC hex format.
+certdetail.sha1fingerprint=SHA1 Fingerprint: %1$S
+# LOCALIZATION NOTE(certdetail.notBefore): %1$S is the already localized
+# notBefore date of the cert being viewed.
+certdetail.notBefore=Begins On: %1$S
+# LOCALIZATION NOTE(certdetail.notAfter): %1$S is the already localized notAfter
+# date of the cert being viewed.
+certdetail.notAfter=Expires On: %1$S
diff --git a/mobile/android/locales/en-US/chrome/sync.properties b/mobile/android/locales/en-US/chrome/sync.properties
new file mode 100644
index 0000000000..fa853efa78
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/sync.properties
@@ -0,0 +1,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/.
+
+# Mobile Sync
+
+# %S is the date and time at which the last sync successfully completed
+lastSync2.label=Last sync: %S
+lastSyncInProgress2.label=Last sync: in progress…
+
+# %S is the username logged in
+account.label=Account: %S
+notconnected.label=Not connected
+connecting.label=Connecting…
+
+notificationDisconnect.label=Your Firefox Sync account has been removed
+notificationDisconnect.button=Undo
+
+# LOCALIZATION NOTE (sync.clientUpdate, sync.remoteUpdate):
+# #1 is the "application name"
+# #2 is the "version"
+sync.update.client=#1 #2 is not compatible with the latest version of Firefox Sync. Please update to the latest version.
+sync.update.remote=#1 #2 is not compatible with older versions of Firefox Sync. Please update Firefox on your other computer(s).
+sync.update.title=Firefox Sync
+sync.update.button=Learn More
+sync.update.close=Close
+sync.setup.error.title=Cannot Setup Sync
+sync.setup.error.network=No internet connection available
+sync.setup.error.nodata=%S could not connect to Sync. Would you like to try again?
+sync.setup.tryagain=Try again
+sync.setup.manual=Manual setup
+
+sync.message.notabs=No tabs from your other computers.
+
+# LOCALIZATION NOTE (promoBanner.message.text): First part of the message displayed in a
+# banner on about:home. The final space separates this text from the link.
+promoBanner.message.text=Sync is brand new and easier than ever.\u0020
+
+# LOCALIZATION NOTE (promoBanner.message.link): Second part of the message, styled as a link.
+promoBanner.message.link=Tap here to learn more
diff --git a/mobile/android/locales/en-US/chrome/webcompatReporter.properties b/mobile/android/locales/en-US/chrome/webcompatReporter.properties
new file mode 100644
index 0000000000..de20d762ac
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/webcompatReporter.properties
@@ -0,0 +1,12 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# LOCALIZATION NOTE (webcompat.menu.name): A "site issue" is a bug, display,
+# or functionality problem with a webpage in the browser.
+webcompat.menu.name=Report site issue
+
+# LOCALIZATION NOTE (webcompat.reportDesktopMode.message): A " site issue" is a
+# bug, display, or functionality problem with a webpage in the browser.
+webcompat.reportDesktopMode.message=Report site issue?
+webcompat.reportDesktopModeYes.label=Report
diff --git a/mobile/android/locales/en-US/defines.inc b/mobile/android/locales/en-US/defines.inc
new file mode 100644
index 0000000000..fb7fe4c937
--- /dev/null
+++ b/mobile/android/locales/en-US/defines.inc
@@ -0,0 +1,12 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#filter emptyLines
+
+#define MOZ_LANGPACK_CREATOR mozilla.org
+
+# If non-English locales wish to credit multiple contributors, uncomment this
+# variable definition and use the format specified.
+# #define MOZ_LANGPACK_CONTRIBUTORS <em:contributor>Joe Solon</em:contributor> <em:contributor>Suzy Solon</em:contributor>
+
+#unfilter emptyLines
diff --git a/mobile/android/locales/en-US/mobile-l10n.js b/mobile/android/locales/en-US/mobile-l10n.js
new file mode 100644
index 0000000000..642ad6534a
--- /dev/null
+++ b/mobile/android/locales/en-US/mobile-l10n.js
@@ -0,0 +1,7 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#filter substitution
+
+pref("general.useragent.locale", "@AB_CD@");
diff --git a/mobile/android/locales/filter.py b/mobile/android/locales/filter.py
new file mode 100644
index 0000000000..26f68d8f06
--- /dev/null
+++ b/mobile/android/locales/filter.py
@@ -0,0 +1,45 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""This routine controls which localizable files and entries are
+reported and l10n-merged.
+This needs to stay in sync with the copy in mobile/locales.
+"""
+
+def test(mod, path, entity = None):
+ import re
+ # ignore anything but mobile, which is our local repo checkout name
+ if mod not in ("netwerk", "dom", "toolkit", "security/manager",
+ "devtools/shared",
+ "services/sync", "mobile",
+ "mobile/android/base", "mobile/android"):
+ return "ignore"
+
+ if mod not in ("mobile", "mobile/android"):
+ # we only have exceptions for mobile*
+ return "error"
+ if mod == "mobile/android":
+ if not entity:
+ if (re.match(r"mobile-l10n.js", path) or
+ re.match(r"defines.inc", path)):
+ return "ignore"
+ if path == "defines.inc":
+ if entity == "MOZ_LANGPACK_CONTRIBUTORS":
+ return "ignore"
+ return "error"
+
+ # we're in mod == "mobile"
+ if re.match(r"searchplugins\/.+\.xml", path):
+ return "ignore"
+ if path == "chrome/region.properties":
+ # only region.properties exceptions remain
+ if (re.match(r"browser\.search\.order\.[1-9]", entity) or
+ re.match(r"browser\.search\.[a-zA-Z]+\.US", entity) or
+ re.match(r"browser\.contentHandlers\.types\.[0-5]", entity) or
+ re.match(r"gecko\.handlerService\.schemes\.", entity) or
+ re.match(r"gecko\.handlerService\.defaultHandlersVersion", entity) or
+ re.match(r"browser\.suggestedsites\.", entity)):
+ return "ignore"
+
+ return "error"
diff --git a/mobile/android/locales/jar.mn b/mobile/android/locales/jar.mn
new file mode 100644
index 0000000000..011e409c2c
--- /dev/null
+++ b/mobile/android/locales/jar.mn
@@ -0,0 +1,99 @@
+#filter substitution
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+@AB_CD@.jar:
+% locale browser @AB_CD@ %locale/@AB_CD@/browser/
+ locale/@AB_CD@/browser/about.dtd (%chrome/about.dtd)
+ locale/@AB_CD@/browser/aboutAccounts.dtd (%chrome/aboutAccounts.dtd)
+ locale/@AB_CD@/browser/aboutAccounts.properties (%chrome/aboutAccounts.properties)
+ locale/@AB_CD@/browser/aboutAddons.dtd (%chrome/aboutAddons.dtd)
+ locale/@AB_CD@/browser/aboutAddons.properties (%chrome/aboutAddons.properties)
+ locale/@AB_CD@/browser/aboutCertError.dtd (%chrome/aboutCertError.dtd)
+ locale/@AB_CD@/browser/aboutDownloads.dtd (%chrome/aboutDownloads.dtd)
+ locale/@AB_CD@/browser/aboutDownloads.properties (%chrome/aboutDownloads.properties)
+ locale/@AB_CD@/browser/aboutHome.dtd (%chrome/aboutHome.dtd)
+ locale/@AB_CD@/browser/aboutHome.properties (%chrome/aboutHome.properties)
+ locale/@AB_CD@/browser/aboutPrivateBrowsing.dtd (%chrome/aboutPrivateBrowsing.dtd)
+#ifdef MOZ_SERVICES_HEALTHREPORT
+ locale/@AB_CD@/browser/aboutHealthReport.dtd (%chrome/aboutHealthReport.dtd)
+#endif
+ locale/@AB_CD@/browser/browser.properties (%chrome/browser.properties)
+ locale/@AB_CD@/browser/config.dtd (%chrome/config.dtd)
+ locale/@AB_CD@/browser/config.properties (%chrome/config.properties)
+ locale/@AB_CD@/browser/checkbox.dtd (%chrome/checkbox.dtd)
+ locale/@AB_CD@/browser/devicePrompt.properties (%chrome/devicePrompt.properties)
+ locale/@AB_CD@/browser/pippki.properties (%chrome/pippki.properties)
+ locale/@AB_CD@/browser/sync.properties (%chrome/sync.properties)
+ locale/@AB_CD@/browser/phishing.dtd (%chrome/phishing.dtd)
+ locale/@AB_CD@/browser/handling.properties (%chrome/handling.properties)
+ locale/@AB_CD@/browser/aboutLogins.dtd (%chrome/aboutLogins.dtd)
+ locale/@AB_CD@/browser/aboutLogins.properties (%chrome/aboutLogins.properties)
+#ifndef RELEASE_OR_BETA
+ locale/@AB_CD@/browser/webcompatReporter.properties (%chrome/webcompatReporter.properties)
+#endif
+% resource search-plugins chrome://browser/locale/searchplugins/
+
+# overrides for toolkit l10n, also for en-US
+relativesrcdir toolkit/locales:
+ locale/@AB_CD@/browser/overrides/about.dtd (%chrome/global/about.dtd)
+ locale/@AB_CD@/browser/overrides/aboutAbout.dtd (%chrome/global/aboutAbout.dtd)
+ locale/@AB_CD@/browser/overrides/aboutReader.properties (%chrome/global/aboutReader.properties)
+ locale/@AB_CD@/browser/overrides/aboutRights.dtd (%chrome/global/aboutRights.dtd)
+ locale/@AB_CD@/browser/overrides/charsetMenu.properties (%chrome/global/charsetMenu.properties)
+ locale/@AB_CD@/browser/overrides/commonDialogs.properties (%chrome/global/commonDialogs.properties)
+ locale/@AB_CD@/browser/overrides/intl.properties (%chrome/global/intl.properties)
+ locale/@AB_CD@/browser/overrides/intl.css (%chrome/global/intl.css)
+ locale/@AB_CD@/browser/overrides/passwordmgr.properties (%chrome/passwordmgr/passwordmgr.properties)
+ locale/@AB_CD@/browser/overrides/search/search.properties (%chrome/search/search.properties)
+# plugins
+ locale/@AB_CD@/browser/overrides/plugins/pluginproblem.dtd (%chrome/pluginproblem/pluginproblem.dtd)
+# about:support
+ locale/@AB_CD@/browser/overrides/global/aboutSupport.dtd (%chrome/global/aboutSupport.dtd)
+ locale/@AB_CD@/browser/overrides/global/aboutSupport.properties (%chrome/global/aboutSupport.properties)
+#about:crashes
+ locale/@AB_CD@/browser/overrides/crashreporter/crashes.dtd (%crashreporter/crashes.dtd)
+ locale/@AB_CD@/browser/overrides/crashreporter/crashes.properties (%crashreporter/crashes.properties)
+#about:mozilla
+ locale/@AB_CD@/browser/overrides/global/mozilla.dtd (%chrome/global/mozilla.dtd)
+#about:telemetry
+ locale/@AB_CD@/browser/overrides/global/aboutTelemetry.dtd (%chrome/global/aboutTelemetry.dtd)
+ locale/@AB_CD@/browser/overrides/global/aboutTelemetry.properties (%chrome/global/aboutTelemetry.properties)
+#about:webrtc
+ locale/@AB_CD@/browser/overrides/global/aboutWebrtc.properties (%chrome/global/aboutWebrtc.properties)
+
+% override chrome://global/locale/about.dtd chrome://browser/locale/overrides/about.dtd
+% override chrome://global/locale/aboutAbout.dtd chrome://browser/locale/overrides/aboutAbout.dtd
+% override chrome://global/locale/aboutReader.properties chrome://browser/locale/overrides/aboutReader.properties
+% override chrome://global/locale/aboutRights.dtd chrome://browser/locale/overrides/aboutRights.dtd
+% override chrome://global/locale/charsetMenu.properties chrome://browser/locale/overrides/charsetMenu.properties
+% override chrome://global/locale/commonDialogs.properties chrome://browser/locale/overrides/commonDialogs.properties
+% override chrome://mozapps/locale/handling/handling.properties chrome://browser/locale/handling.properties
+% override chrome://global/locale/intl.properties chrome://browser/locale/overrides/intl.properties
+% override chrome://global/locale/intl.css chrome://browser/locale/overrides/intl.css
+% override chrome://passwordmgr/locale/passwordmgr.properties chrome://browser/locale/overrides/passwordmgr/passwordmgr.properties
+% override chrome://global/locale/search/search.properties chrome://browser/locale/overrides/search/search.properties
+% override chrome://pluginproblem/locale/pluginproblem.dtd chrome://browser/locale/overrides/plugins/pluginproblem.dtd
+% override chrome://global/locale/aboutSupport.dtd chrome://browser/locale/overrides/global/aboutSupport.dtd
+% override chrome://global/locale/aboutSupport.properties chrome://browser/locale/overrides/global/aboutSupport.properties
+% override chrome://global/locale/crashes.dtd chrome://browser/locale/overrides/crashreporter/crashes.dtd
+% override chrome://global/locale/crashes.properties chrome://browser/locale/overrides/crashreporter/crashes.properties
+% override chrome://global/locale/mozilla.dtd chrome://browser/locale/overrides/global/mozilla.dtd
+% override chrome://global/locale/aboutTelemetry.dtd chrome://browser/locale/overrides/global/aboutTelemetry.dtd
+% override chrome://global/locale/aboutTelemetry.properties chrome://browser/locale/overrides/global/aboutTelemetry.properties
+% override chrome://global/locale/aboutWebrtc.properties chrome://browser/locale/overrides/global/aboutWebrtc.properties
+
+# overrides for dom l10n, also for en-US
+relativesrcdir dom/locales:
+ locale/@AB_CD@/browser/overrides/global.dtd (%chrome/global.dtd)
+ locale/@AB_CD@/browser/overrides/AccessFu.properties (%chrome/accessibility/AccessFu.properties)
+ locale/@AB_CD@/browser/overrides/dom/dom.properties (%chrome/dom/dom.properties)
+#about:plugins
+ locale/@AB_CD@/browser/overrides/plugins.properties (%chrome/plugins.properties)
+
+% override chrome://global/locale/global.dtd chrome://browser/locale/overrides/global.dtd
+% override chrome://global/locale/AccessFu.properties chrome://browser/locale/overrides/AccessFu.properties
+% override chrome://global/locale/dom/dom.properties chrome://browser/locale/overrides/dom/dom.properties
+% override chrome://global/locale/plugins.properties chrome://browser/locale/overrides/plugins.properties
diff --git a/mobile/android/locales/l10n.ini b/mobile/android/locales/l10n.ini
new file mode 100644
index 0000000000..1d6afa4a15
--- /dev/null
+++ b/mobile/android/locales/l10n.ini
@@ -0,0 +1,18 @@
+; This Source Code Form is subject to the terms of the Mozilla Public
+; License, v. 2.0. If a copy of the MPL was not distributed with this
+; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Control which directories and modules are part of mobile/android.
+# Changes here should be reflected in mobile/locales/l10n.ini so
+# that the dashboard picks them up.
+
+[general]
+depth = ../../..
+all = mobile/android/locales/all-locales
+
+[compare]
+dirs = mobile mobile/android mobile/android/base
+
+[includes]
+toolkit = toolkit/locales/l10n.ini
+services_sync = services/sync/locales/l10n.ini
diff --git a/mobile/android/locales/maemo-locales b/mobile/android/locales/maemo-locales
new file mode 100644
index 0000000000..111c2aa70d
--- /dev/null
+++ b/mobile/android/locales/maemo-locales
@@ -0,0 +1,77 @@
+an
+as
+ast
+az
+bn-IN
+br
+ca
+cak
+cs
+cy
+da
+de
+dsb
+en-GB
+en-ZA
+eo
+es-AR
+es-CL
+es-ES
+es-MX
+et
+eu
+ff
+fi
+fr
+fy-NL
+ga-IE
+gd
+gl
+gn
+gu-IN
+hi-IN
+hr
+hsb
+hu
+hy-AM
+id
+is
+it
+ja
+ka
+kk
+kn
+ko
+lt
+lv
+mai
+ml
+mr
+ms
+my
+nb-NO
+nl
+nn-NO
+or
+pa-IN
+pl
+pt-BR
+pt-PT
+rm
+ro
+ru
+sk
+sl
+son
+sq
+sr
+sv-SE
+ta
+te
+th
+tr
+uk
+uz
+xh
+zh-CN
+zh-TW
diff --git a/mobile/android/locales/moz.build b/mobile/android/locales/moz.build
new file mode 100644
index 0000000000..eb4454d28f
--- /dev/null
+++ b/mobile/android/locales/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file
diff --git a/mobile/android/mach_commands.py b/mobile/android/mach_commands.py
new file mode 100644
index 0000000000..17628ad9fb
--- /dev/null
+++ b/mobile/android/mach_commands.py
@@ -0,0 +1,216 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+import argparse
+import logging
+import os
+
+import mozpack.path as mozpath
+
+from mozbuild.base import (
+ MachCommandBase,
+ MachCommandConditions as conditions,
+)
+
+from mozbuild.shellutil import (
+ split as shell_split,
+)
+
+from mach.decorators import (
+ CommandArgument,
+ CommandProvider,
+ Command,
+)
+
+
+# NOTE python/mach/mach/commands/commandinfo.py references this function
+# by name. If this function is renamed or removed, that file should
+# be updated accordingly as well.
+def REMOVED(cls):
+ """Command no longer exists! Use the Gradle configuration rooted in the top source directory instead.
+
+ See https://developer.mozilla.org/en-US/docs/Simple_Firefox_for_Android_build#Developing_Firefox_for_Android_in_Android_Studio_or_IDEA_IntelliJ.
+ """
+ return False
+
+
+@CommandProvider
+class MachCommands(MachCommandBase):
+ @Command('android', category='devenv',
+ description='Run the Android package manager tool.',
+ conditions=[conditions.is_android])
+ @CommandArgument('args', nargs=argparse.REMAINDER)
+ def android(self, args):
+ # Avoid logging the command
+ self.log_manager.terminal_handler.setLevel(logging.CRITICAL)
+
+ return self.run_process(
+ [os.path.join(self.substs['ANDROID_TOOLS'], 'android')] + args,
+ pass_thru=True, # Allow user to run gradle interactively.
+ ensure_exit_code=False, # Don't throw on non-zero exit code.
+ cwd=mozpath.join(self.topsrcdir))
+
+ @Command('gradle', category='devenv',
+ description='Run gradle.',
+ conditions=[conditions.is_android])
+ @CommandArgument('args', nargs=argparse.REMAINDER)
+ def gradle(self, args):
+ # Avoid logging the command
+ self.log_manager.terminal_handler.setLevel(logging.CRITICAL)
+
+
+ # In automation, JAVA_HOME is set via mozconfig, which needs
+ # to be specially handled in each mach command. This turns
+ # $JAVA_HOME/bin/java into $JAVA_HOME.
+ java_home = os.path.dirname(os.path.dirname(self.substs['JAVA']))
+
+ gradle_flags = shell_split(self.substs.get('GRADLE_FLAGS', ''))
+
+ # We force the Gradle JVM to run with the UTF-8 encoding, since we
+ # filter strings.xml, which is really UTF-8; the ellipsis character is
+ # replaced with ??? in some encodings (including ASCII). It's not yet
+ # possible to filter with encodings in Gradle
+ # (https://github.com/gradle/gradle/pull/520) and it's challenging to
+ # do our filtering with Gradle's Ant support. Moreover, all of the
+ # Android tools expect UTF-8: see
+ # http://tools.android.com/knownissues/encoding. See
+ # http://stackoverflow.com/a/21267635 for discussion of this approach.
+ return self.run_process([self.substs['GRADLE']] + gradle_flags + args,
+ append_env={
+ 'GRADLE_OPTS': '-Dfile.encoding=utf-8',
+ 'JAVA_HOME': java_home,
+ },
+ pass_thru=True, # Allow user to run gradle interactively.
+ ensure_exit_code=False, # Don't throw on non-zero exit code.
+ cwd=mozpath.join(self.topsrcdir))
+
+ @Command('gradle-install', category='devenv',
+ conditions=[REMOVED])
+ def gradle_install(self):
+ pass
+
+
+@CommandProvider
+class AndroidEmulatorCommands(MachCommandBase):
+ """
+ Run the Android emulator with one of the AVDs used in the Mozilla
+ automated test environment. If necessary, the AVD is fetched from
+ the tooltool server and installed.
+ """
+ @Command('android-emulator', category='devenv',
+ conditions=[],
+ description='Run the Android emulator with an AVD from test automation.')
+ @CommandArgument('--version', metavar='VERSION', choices=['4.3', '6.0', 'x86'],
+ help='Specify Android version to run in emulator. One of "4.3", "6.0", or "x86".',
+ default='4.3')
+ @CommandArgument('--wait', action='store_true',
+ help='Wait for emulator to be closed.')
+ @CommandArgument('--force-update', action='store_true',
+ help='Update AVD definition even when AVD is already installed.')
+ @CommandArgument('--verbose', action='store_true',
+ help='Log informative status messages.')
+ def emulator(self, version, wait=False, force_update=False, verbose=False):
+ from mozrunner.devices.android_device import AndroidEmulator
+
+ emulator = AndroidEmulator(version, verbose, substs=self.substs, device_serial='emulator-5554')
+ if emulator.is_running():
+ # It is possible to run multiple emulators simultaneously, but:
+ # - if more than one emulator is using the same avd, errors may
+ # occur due to locked resources;
+ # - additional parameters must be specified when running tests,
+ # to select a specific device.
+ # To avoid these complications, allow just one emulator at a time.
+ self.log(logging.ERROR, "emulator", {},
+ "An Android emulator is already running.\n"
+ "Close the existing emulator and re-run this command.")
+ return 1
+
+ if not emulator.is_available():
+ self.log(logging.WARN, "emulator", {},
+ "Emulator binary not found.\n"
+ "Install the Android SDK and make sure 'emulator' is in your PATH.")
+ return 2
+
+ if not emulator.check_avd(force_update):
+ self.log(logging.INFO, "emulator", {},
+ "Fetching and installing AVD. This may take a few minutes...")
+ emulator.update_avd(force_update)
+
+ self.log(logging.INFO, "emulator", {},
+ "Starting Android emulator running %s..." %
+ emulator.get_avd_description())
+ emulator.start()
+ if emulator.wait_for_start():
+ self.log(logging.INFO, "emulator", {},
+ "Android emulator is running.")
+ else:
+ # This is unusual but the emulator may still function.
+ self.log(logging.WARN, "emulator", {},
+ "Unable to verify that emulator is running.")
+
+ if conditions.is_android(self):
+ self.log(logging.INFO, "emulator", {},
+ "Use 'mach install' to install or update Firefox on your emulator.")
+ else:
+ self.log(logging.WARN, "emulator", {},
+ "No Firefox for Android build detected.\n"
+ "Switch to a Firefox for Android build context or use 'mach bootstrap'\n"
+ "to setup an Android build environment.")
+
+ if wait:
+ self.log(logging.INFO, "emulator", {},
+ "Waiting for Android emulator to close...")
+ rc = emulator.wait()
+ if rc is not None:
+ self.log(logging.INFO, "emulator", {},
+ "Android emulator completed with return code %d." % rc)
+ else:
+ self.log(logging.WARN, "emulator", {},
+ "Unable to retrieve Android emulator return code.")
+ return 0
+
+
+@CommandProvider
+class AutophoneCommands(MachCommandBase):
+ """
+ Run autophone, https://wiki.mozilla.org/Auto-tools/Projects/Autophone.
+
+ If necessary, autophone is cloned from github, installed, and configured.
+ """
+ @Command('autophone', category='devenv',
+ conditions=[],
+ description='Run autophone.')
+ @CommandArgument('--clean', action='store_true',
+ help='Delete an existing autophone installation.')
+ @CommandArgument('--verbose', action='store_true',
+ help='Log informative status messages.')
+ def autophone(self, clean=False, verbose=False):
+ import platform
+ from mozrunner.devices.autophone import AutophoneRunner
+
+ if platform.system() == "Windows":
+ # Autophone is normally run on Linux or OSX.
+ self.log(logging.ERROR, "autophone", {},
+ "This mach command is not supported on Windows!")
+ return -1
+
+ runner = AutophoneRunner(self, verbose)
+ runner.load_config()
+ if clean:
+ runner.reset_to_clean()
+ return 0
+ if not runner.setup_directory():
+ return 1
+ if not runner.install_requirements():
+ runner.save_config()
+ return 2
+ if not runner.configure():
+ runner.save_config()
+ return 3
+ runner.save_config()
+ runner.launch_autophone()
+ runner.command_prompts()
+ return 0
diff --git a/mobile/android/modules/Accounts.jsm b/mobile/android/modules/Accounts.jsm
new file mode 100644
index 0000000000..a611f3c58f
--- /dev/null
+++ b/mobile/android/modules/Accounts.jsm
@@ -0,0 +1,178 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["Accounts"];
+
+const { utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Deprecated.jsm"); /*global Deprecated */
+Cu.import("resource://gre/modules/Messaging.jsm"); /*global Messaging */
+Cu.import("resource://gre/modules/Promise.jsm"); /*global Promise */
+Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
+
+/**
+ * A promise-based API for querying the existence of Sync accounts,
+ * and accessing the Sync setup wizard.
+ *
+ * Usage:
+ *
+ * Cu.import("resource://gre/modules/Accounts.jsm");
+ * Accounts.anySyncAccountsExist().then(
+ * (exist) => {
+ * console.log("Accounts exist? " + exist);
+ * if (!exist) {
+ * Accounts.launchSetup();
+ * }
+ * },
+ * (err) => {
+ * console.log("We failed so hard.");
+ * }
+ * );
+ */
+var Accounts = Object.freeze({
+ _accountsExist: function (kind) {
+ return Messaging.sendRequestForResult({
+ type: "Accounts:Exist",
+ kind: kind
+ }).then(data => data.exists);
+ },
+
+ firefoxAccountsExist: function () {
+ return this._accountsExist("fxa");
+ },
+
+ syncAccountsExist: function () {
+ Deprecated.warning("The legacy Sync account type has been removed from Firefox for Android. " +
+ "Please use `firefoxAccountsExist` instead.",
+ "https://developer.mozilla.org/en-US/Add-ons/Firefox_for_Android/API/Accounts.jsm");
+ return Promise.resolve(false);
+ },
+
+ anySyncAccountsExist: function () {
+ return this._accountsExist("any");
+ },
+
+ /**
+ * Fire-and-forget: open the Firefox accounts activity, which
+ * will be the Getting Started screen if FxA isn't yet set up.
+ *
+ * Optional extras are passed, as a JSON string, to the Firefox
+ * Account Getting Started activity in the extras bundle of the
+ * activity launch intent, under the key "extras".
+ *
+ * There is no return value from this method.
+ */
+ launchSetup: function (extras) {
+ Messaging.sendRequest({
+ type: "Accounts:Create",
+ extras: extras
+ });
+ },
+
+ _addDefaultEndpoints: function (json) {
+ let newData = Cu.cloneInto(json, {}, { cloneFunctions: false });
+ let associations = {
+ authServerEndpoint: 'identity.fxaccounts.auth.uri',
+ profileServerEndpoint: 'identity.fxaccounts.remote.profile.uri',
+ tokenServerEndpoint: 'identity.sync.tokenserver.uri'
+ };
+ for (let key in associations) {
+ newData[key] = newData[key] || Services.urlFormatter.formatURLPref(associations[key]);
+ }
+ return newData;
+ },
+
+ /**
+ * Create a new Android Account corresponding to the given
+ * fxa-content-server "login" JSON datum. The new account will be
+ * in the "Engaged" state, and will start syncing immediately.
+ *
+ * It is an error if an Android Account already exists.
+ *
+ * Returns a Promise that resolves to a boolean indicating success.
+ */
+ createFirefoxAccountFromJSON: function (json) {
+ return Messaging.sendRequestForResult({
+ type: "Accounts:CreateFirefoxAccountFromJSON",
+ json: this._addDefaultEndpoints(json)
+ });
+ },
+
+ /**
+ * Move an existing Android Account to the "Engaged" state with the given
+ * fxa-content-server "login" JSON datum. The account will (re)start
+ * syncing immediately, unless the user has manually configured the account
+ * to not Sync.
+ *
+ * It is an error if no Android Account exists.
+ *
+ * Returns a Promise that resolves to a boolean indicating success.
+ */
+ updateFirefoxAccountFromJSON: function (json) {
+ return Messaging.sendRequestForResult({
+ type: "Accounts:UpdateFirefoxAccountFromJSON",
+ json: this._addDefaultEndpoints(json)
+ });
+ },
+
+ /**
+ * Notify that profile for Android Account has updated.
+ * The account will re-fetch the profile image.
+ *
+ * It is an error if no Android Account exists.
+ *
+ * There is no return value from this method.
+ */
+ notifyFirefoxAccountProfileChanged: function () {
+ Messaging.sendRequest({
+ type: "Accounts:ProfileUpdated",
+ });
+ },
+
+ /**
+ * Fetch information about an existing Android Firefox Account.
+ *
+ * Returns a Promise that resolves to null if no Android Firefox Account
+ * exists, or an object including at least a string-valued 'email' key.
+ */
+ getFirefoxAccount: function () {
+ return Messaging.sendRequestForResult({
+ type: "Accounts:Exist",
+ kind: "fxa",
+ }).then(data => {
+ if (!data || !data.exists) {
+ return null;
+ }
+ delete data.exists;
+ return data;
+ });
+ },
+
+ /**
+ * Delete an existing Android Firefox Account.
+ *
+ * It is an error if no Android Account exists.
+ *
+ * Returns a Promise that resolves to a boolean indicating success.
+ */
+ deleteFirefoxAccount: function () {
+ return Messaging.sendRequestForResult({
+ type: "Accounts:DeleteFirefoxAccount",
+ });
+ },
+
+ showSyncPreferences: function () {
+ // Only show Sync preferences of an existing Android Account.
+ return Accounts.getFirefoxAccount().then(account => {
+ if (!account) {
+ throw new Error("Can't show Sync preferences of non-existent Firefox Account!");
+ }
+ return Messaging.sendRequestForResult({
+ type: "Accounts:ShowSyncPreferences"
+ });
+ });
+ }
+});
diff --git a/mobile/android/modules/AndroidLog.jsm b/mobile/android/modules/AndroidLog.jsm
new file mode 100644
index 0000000000..be6cca4e80
--- /dev/null
+++ b/mobile/android/modules/AndroidLog.jsm
@@ -0,0 +1,92 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+/**
+ * Native Android logging for JavaScript. Lets you specify a priority and tag
+ * in addition to the message being logged. Resembles the android.util.Log API
+ * <http://developer.android.com/reference/android/util/Log.html>.
+ *
+ * // Import it as a JSM:
+ * let Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog;
+ *
+ * // Or require it in a chrome worker:
+ * importScripts("resource://gre/modules/workers/require.js");
+ * let Log = require("resource://gre/modules/AndroidLog.jsm");
+ *
+ * // Use Log.i, Log.v, Log.d, Log.w, and Log.e to log verbose, debug, info,
+ * // warning, and error messages, respectively.
+ * Log.v("MyModule", "This is a verbose message.");
+ * Log.d("MyModule", "This is a debug message.");
+ * Log.i("MyModule", "This is an info message.");
+ * Log.w("MyModule", "This is a warning message.");
+ * Log.e("MyModule", "This is an error message.");
+ *
+ * // Bind a function with a tag to replace a bespoke dump/log/debug function:
+ * let debug = Log.d.bind(null, "MyModule");
+ * debug("This is a debug message.");
+ * // Outputs "D/GeckoMyModule(#####): This is a debug message."
+ *
+ * // Or "bind" the module object to a tag to automatically tag messages:
+ * Log = Log.bind("MyModule");
+ * Log.d("This is a debug message.");
+ * // Outputs "D/GeckoMyModule(#####): This is a debug message."
+ *
+ * Note: the module automatically prepends "Gecko" to the tag you specify,
+ * since all tags used by Fennec code should start with that string; and it
+ * truncates tags longer than MAX_TAG_LENGTH characters (not including "Gecko").
+ */
+
+if (typeof Components != "undefined") {
+ // Specify exported symbols for JSM module loader.
+ this.EXPORTED_SYMBOLS = ["AndroidLog"];
+ Components.utils.import("resource://gre/modules/ctypes.jsm");
+}
+
+// From <https://android.googlesource.com/platform/system/core/+/master/include/android/log.h>.
+const ANDROID_LOG_VERBOSE = 2;
+const ANDROID_LOG_DEBUG = 3;
+const ANDROID_LOG_INFO = 4;
+const ANDROID_LOG_WARN = 5;
+const ANDROID_LOG_ERROR = 6;
+
+// android.util.Log.isLoggable throws IllegalArgumentException if a tag length
+// exceeds 23 characters, and we prepend five characters ("Gecko") to every tag,
+// so we truncate tags exceeding 18 characters (although __android_log_write
+// itself and other android.util.Log methods don't seem to mind longer tags).
+const MAX_TAG_LENGTH = 18;
+
+var liblog = ctypes.open("liblog.so"); // /system/lib/liblog.so
+var __android_log_write = liblog.declare("__android_log_write",
+ ctypes.default_abi,
+ ctypes.int, // return value: num bytes logged
+ ctypes.int, // priority (ANDROID_LOG_* constant)
+ ctypes.char.ptr, // tag
+ ctypes.char.ptr); // message
+
+var AndroidLog = {
+ MAX_TAG_LENGTH: MAX_TAG_LENGTH,
+ v: (tag, msg) => __android_log_write(ANDROID_LOG_VERBOSE, "Gecko" + tag.substring(0, MAX_TAG_LENGTH), msg),
+ d: (tag, msg) => __android_log_write(ANDROID_LOG_DEBUG, "Gecko" + tag.substring(0, MAX_TAG_LENGTH), msg),
+ i: (tag, msg) => __android_log_write(ANDROID_LOG_INFO, "Gecko" + tag.substring(0, MAX_TAG_LENGTH), msg),
+ w: (tag, msg) => __android_log_write(ANDROID_LOG_WARN, "Gecko" + tag.substring(0, MAX_TAG_LENGTH), msg),
+ e: (tag, msg) => __android_log_write(ANDROID_LOG_ERROR, "Gecko" + tag.substring(0, MAX_TAG_LENGTH), msg),
+
+ bind: function(tag) {
+ return {
+ MAX_TAG_LENGTH: MAX_TAG_LENGTH,
+ v: AndroidLog.v.bind(null, tag),
+ d: AndroidLog.d.bind(null, tag),
+ i: AndroidLog.i.bind(null, tag),
+ w: AndroidLog.w.bind(null, tag),
+ e: AndroidLog.e.bind(null, tag),
+ };
+ },
+};
+
+if (typeof Components == "undefined") {
+ // Specify exported symbols for require.js module loader.
+ module.exports = AndroidLog;
+}
diff --git a/mobile/android/modules/DelayedInit.jsm b/mobile/android/modules/DelayedInit.jsm
new file mode 100644
index 0000000000..7c33a3ede3
--- /dev/null
+++ b/mobile/android/modules/DelayedInit.jsm
@@ -0,0 +1,177 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict"
+
+/*globals MessageLoop */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+this.EXPORTED_SYMBOLS = ["DelayedInit"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "MessageLoop",
+ "@mozilla.org/message-loop;1",
+ "nsIMessageLoop");
+
+/**
+ * Use DelayedInit to schedule initializers to run some time after startup.
+ * Initializers are added to a list of pending inits. Whenever the main thread
+ * message loop is idle, DelayedInit will start running initializers from the
+ * pending list. To prevent monopolizing the message loop, every idling period
+ * has a maximum duration. When that's reached, we give up the message loop and
+ * wait for the next idle.
+ *
+ * DelayedInit is compatible with lazy getters like those from XPCOMUtils. When
+ * the lazy getter is first accessed, its corresponding initializer is run
+ * automatically if it hasn't been run already. Each initializer also has a
+ * maximum wait parameter that specifies a mandatory timeout; when the timeout
+ * is reached, the initializer is forced to run.
+ *
+ * DelayedInit.schedule(() => Foo.init(), null, null, 5000);
+ *
+ * In the example above, Foo.init will run automatically when the message loop
+ * becomes idle, or when 5000ms has elapsed, whichever comes first.
+ *
+ * DelayedInit.schedule(() => Foo.init(), this, "Foo", 5000);
+ *
+ * In the example above, Foo.init will run automatically when the message loop
+ * becomes idle, when |this.Foo| is accessed, or when 5000ms has elapsed,
+ * whichever comes first.
+ *
+ * It may be simpler to have a wrapper for DelayedInit.schedule. For example,
+ *
+ * function InitLater(fn, obj, name) {
+ * return DelayedInit.schedule(fn, obj, name, 5000); // constant max wait
+ * }
+ * InitLater(() => Foo.init());
+ * InitLater(() => Bar.init(), this, "Bar");
+ */
+var DelayedInit = {
+ schedule: function (fn, object, name, maxWait) {
+ return Impl.scheduleInit(fn, object, name, maxWait);
+ },
+};
+
+// Maximum duration for each idling period. Pending inits are run until this
+// duration is exceeded; then we wait for next idling period.
+const MAX_IDLE_RUN_MS = 50;
+
+var Impl = {
+ pendingInits: [],
+
+ onIdle: function () {
+ let startTime = Cu.now();
+ let time = startTime;
+ let nextDue;
+
+ // Go through all the pending inits. Even if we don't run them,
+ // we still need to find out when the next timeout should be.
+ for (let init of this.pendingInits) {
+ if (init.complete) {
+ continue;
+ }
+
+ if (time - startTime < MAX_IDLE_RUN_MS) {
+ init.maybeInit();
+ time = Cu.now();
+ } else {
+ // We ran out of time; find when the next closest due time is.
+ nextDue = nextDue ? Math.min(nextDue, init.due) : init.due;
+ }
+ }
+
+ // Get rid of completed ones.
+ this.pendingInits = this.pendingInits.filter((init) => !init.complete);
+
+ if (nextDue !== undefined) {
+ // Schedule the next idle, if we still have pending inits.
+ MessageLoop.postIdleTask(() => this.onIdle(),
+ Math.max(0, nextDue - time));
+ }
+ },
+
+ addPendingInit: function (fn, wait) {
+ let init = {
+ fn: fn,
+ due: Cu.now() + wait,
+ complete: false,
+ maybeInit: function () {
+ if (this.complete) {
+ return false;
+ }
+ this.complete = true;
+ this.fn.call();
+ this.fn = null;
+ return true;
+ },
+ };
+
+ if (!this.pendingInits.length) {
+ // Schedule for the first idle.
+ MessageLoop.postIdleTask(() => this.onIdle(), wait);
+ }
+ this.pendingInits.push(init);
+ return init;
+ },
+
+ scheduleInit: function (fn, object, name, wait) {
+ let init = this.addPendingInit(fn, wait);
+
+ if (!object || !name) {
+ // No lazy getter needed.
+ return;
+ }
+
+ // Get any existing information about the property.
+ let prop = Object.getOwnPropertyDescriptor(object, name) ||
+ { configurable: true, enumerable: true, writable: true };
+
+ if (!prop.configurable) {
+ // Object.defineProperty won't work, so just perform init here.
+ init.maybeInit();
+ return;
+ }
+
+ // Define proxy getter/setter that will call first initializer first,
+ // before delegating the get/set to the original target.
+ Object.defineProperty(object, name, {
+ get: function proxy_getter() {
+ init.maybeInit();
+
+ // If the initializer actually ran, it may have replaced our proxy
+ // property with a real one, so we need to reload he property.
+ let newProp = Object.getOwnPropertyDescriptor(object, name);
+ if (newProp.get !== proxy_getter) {
+ // Set prop if newProp doesn't refer to our proxy property.
+ prop = newProp;
+ } else {
+ // Otherwise, reset to the original property.
+ Object.defineProperty(object, name, prop);
+ }
+
+ if (prop.get) {
+ return prop.get.call(object);
+ }
+ return prop.value;
+ },
+ set: function (newVal) {
+ init.maybeInit();
+
+ // Since our initializer already ran,
+ // we can get rid of our proxy property.
+ if (prop.get || prop.set) {
+ Object.defineProperty(object, name, prop);
+ return prop.set.call(object);
+ }
+
+ prop.value = newVal;
+ Object.defineProperty(object, name, prop);
+ return newVal;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+ }
+};
diff --git a/mobile/android/modules/DownloadNotifications.jsm b/mobile/android/modules/DownloadNotifications.jsm
new file mode 100644
index 0000000000..39b5209791
--- /dev/null
+++ b/mobile/android/modules/DownloadNotifications.jsm
@@ -0,0 +1,291 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["DownloadNotifications"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", "resource://gre/modules/UITelemetry.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls",
+ "@mozilla.org/parental-controls-service;1", "nsIParentalControlsService");
+
+var Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.i.bind(null, "DownloadNotifications");
+
+XPCOMUtils.defineLazyGetter(this, "strings",
+ () => Services.strings.createBundle("chrome://browser/locale/browser.properties"));
+
+Object.defineProperty(this, "window",
+ { get: () => Services.wm.getMostRecentWindow("navigator:browser") });
+
+const kButtons = {
+ PAUSE: new DownloadNotificationButton("pause",
+ "drawable://pause",
+ "alertDownloadsPause"),
+ RESUME: new DownloadNotificationButton("resume",
+ "drawable://play",
+ "alertDownloadsResume"),
+ CANCEL: new DownloadNotificationButton("cancel",
+ "drawable://close",
+ "alertDownloadsCancel")
+};
+
+var notifications = new Map();
+
+var DownloadNotifications = {
+ _notificationKey: "downloads",
+
+ init: function () {
+ Downloads.getList(Downloads.ALL)
+ .then(list => list.addView(this))
+ .then(() => this._viewAdded = true, Cu.reportError);
+
+ // All click, cancel, and button presses will be handled by this handler as part of the Notifications callback API.
+ Notifications.registerHandler(this._notificationKey, this);
+ },
+
+ onDownloadAdded: function (download) {
+ // Don't create notifications for pre-existing succeeded downloads.
+ // We still add notifications for canceled downloads in case the
+ // user decides to retry the download.
+ if (download.succeeded && !this._viewAdded) {
+ return;
+ }
+
+ if (!ParentalControls.isAllowed(ParentalControls.DOWNLOAD)) {
+ download.cancel().catch(Cu.reportError);
+ download.removePartialData().catch(Cu.reportError);
+ Snackbars.show(strings.GetStringFromName("downloads.disabledInGuest"), Snackbars.LENGTH_LONG);
+ return;
+ }
+
+ let notification = new DownloadNotification(download);
+ notifications.set(download, notification);
+ notification.showOrUpdate();
+
+ // If this is a new download, show a snackbar as well.
+ if (this._viewAdded) {
+ Snackbars.show(strings.GetStringFromName("alertDownloadsToast"), Snackbars.LENGTH_LONG);
+ }
+ },
+
+ onDownloadChanged: function (download) {
+ let notification = notifications.get(download);
+
+ if (download.succeeded) {
+ let file = new FileUtils.File(download.target.path);
+
+ Snackbars.show(strings.formatStringFromName("alertDownloadSucceeded", [file.leafName], 1), Snackbars.LENGTH_LONG, {
+ action: {
+ label: strings.GetStringFromName("helperapps.open"),
+ callback: () => {
+ UITelemetry.addEvent("launch.1", "toast", null, "downloads");
+ try {
+ file.launch();
+ } catch (ex) {
+ this.showInAboutDownloads(download);
+ }
+ if (notification) {
+ notification.hide();
+ }
+ }
+ }});
+ }
+
+ if (notification) {
+ notification.showOrUpdate();
+ }
+ },
+
+ onDownloadRemoved: function (download) {
+ let notification = notifications.get(download);
+ if (!notification) {
+ Cu.reportError("Download doesn't have a notification.");
+ return;
+ }
+
+ notification.hide();
+ notifications.delete(download);
+ },
+
+ _findDownloadForCookie: function(cookie) {
+ return Downloads.getList(Downloads.ALL)
+ .then(list => list.getAll())
+ .then((downloads) => {
+ for (let download of downloads) {
+ let cookie2 = getCookieFromDownload(download);
+ if (cookie2 === cookie) {
+ return download;
+ }
+ }
+
+ throw "Couldn't find download for " + cookie;
+ });
+ },
+
+ onCancel: function(cookie) {
+ // TODO: I'm not sure what we do here...
+ },
+
+ showInAboutDownloads: function (download) {
+ let hash = "#" + window.encodeURIComponent(download.target.path);
+
+ // Force using string equality to find a tab
+ window.BrowserApp.selectOrAddTab("about:downloads" + hash, null, { startsWith: true });
+ },
+
+ onClick: function(cookie) {
+ this._findDownloadForCookie(cookie).then((download) => {
+ if (download.succeeded) {
+ // We don't call Download.launch(), because there's (currently) no way to
+ // tell if the file was actually launched or not, and we want to show
+ // about:downloads if the launch failed.
+ let file = new FileUtils.File(download.target.path);
+ try {
+ file.launch();
+ } catch (ex) {
+ this.showInAboutDownloads(download);
+ }
+ } else {
+ ConfirmCancelPrompt.show(download);
+ }
+ }).catch(Cu.reportError);
+ },
+
+ onButtonClick: function(button, cookie) {
+ this._findDownloadForCookie(cookie).then((download) => {
+ if (button === kButtons.PAUSE.buttonId) {
+ download.cancel().catch(Cu.reportError);
+ } else if (button === kButtons.RESUME.buttonId) {
+ download.start().catch(Cu.reportError);
+ } else if (button === kButtons.CANCEL.buttonId) {
+ download.cancel().catch(Cu.reportError);
+ download.removePartialData().catch(Cu.reportError);
+ }
+ }).catch(Cu.reportError);
+ },
+};
+
+function getCookieFromDownload(download) {
+ return download.target.path +
+ download.source.url +
+ download.startTime;
+}
+
+function DownloadNotification(download) {
+ this.download = download;
+ this._fileName = OS.Path.basename(download.target.path);
+
+ this.id = null;
+}
+
+DownloadNotification.prototype = {
+ _updateFromDownload: function () {
+ this._downloading = !this.download.stopped;
+ this._paused = this.download.canceled && this.download.hasPartialData;
+ this._succeeded = this.download.succeeded;
+
+ this._show = this._downloading || this._paused || this._succeeded;
+ },
+
+ get options() {
+ if (!this._show) {
+ return null;
+ }
+
+ let options = {
+ icon: "drawable://alert_download",
+ cookie: getCookieFromDownload(this.download),
+ handlerKey: DownloadNotifications._notificationKey
+ };
+
+ if (this._downloading) {
+ options.icon = "drawable://alert_download_animation";
+ if (this.download.currentBytes == 0) {
+ this._updateOptionsForStatic(options, "alertDownloadsStart2");
+ } else {
+ let buttons = this.download.hasPartialData ? [kButtons.PAUSE, kButtons.CANCEL] :
+ [kButtons.CANCEL]
+ this._updateOptionsForOngoing(options, buttons);
+ }
+ } else if (this._paused) {
+ this._updateOptionsForOngoing(options, [kButtons.RESUME, kButtons.CANCEL]);
+ } else if (this._succeeded) {
+ options.persistent = false;
+ this._updateOptionsForStatic(options, "alertDownloadsDone2");
+ }
+
+ return options;
+ },
+
+ _updateOptionsForStatic : function (options, titleName) {
+ options.title = strings.GetStringFromName(titleName);
+ options.message = this._fileName;
+ },
+
+ _updateOptionsForOngoing: function (options, buttons) {
+ options.title = this._fileName;
+ options.message = this.download.progress + "%";
+ options.buttons = buttons;
+ options.ongoing = true;
+ options.progress = this.download.progress;
+ options.persistent = true;
+ },
+
+ showOrUpdate: function () {
+ this._updateFromDownload();
+
+ if (this._show) {
+ if (!this.id) {
+ this.id = Notifications.create(this.options);
+ } else if (!this.options.ongoing) {
+ // We need to explictly cancel ongoing notifications,
+ // since updating them to be non-ongoing doesn't seem
+ // to work. See bug 1130834.
+ Notifications.cancel(this.id);
+ this.id = Notifications.create(this.options);
+ } else {
+ Notifications.update(this.id, this.options);
+ }
+ } else {
+ this.hide();
+ }
+ },
+
+ hide: function () {
+ if (this.id) {
+ Notifications.cancel(this.id);
+ this.id = null;
+ }
+ },
+};
+
+var ConfirmCancelPrompt = {
+ show: function (download) {
+ // Open a prompt that offers a choice to cancel the download
+ let title = strings.GetStringFromName("downloadCancelPromptTitle1");
+ let message = strings.GetStringFromName("downloadCancelPromptMessage1");
+
+ if (Services.prompt.confirm(null, title, message)) {
+ download.cancel().catch(Cu.reportError);
+ download.removePartialData().catch(Cu.reportError);
+ }
+ }
+};
+
+function DownloadNotificationButton(buttonId, iconUrl, titleStringName, onClicked) {
+ this.buttonId = buttonId;
+ this.title = strings.GetStringFromName(titleStringName);
+ this.icon = iconUrl;
+}
diff --git a/mobile/android/modules/FxAccountsWebChannel.jsm b/mobile/android/modules/FxAccountsWebChannel.jsm
new file mode 100644
index 0000000000..6ee8fd07f3
--- /dev/null
+++ b/mobile/android/modules/FxAccountsWebChannel.jsm
@@ -0,0 +1,394 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Firefox Accounts Web Channel.
+ *
+ * Use the WebChannel component to receive messages about account
+ * state changes.
+ */
+this.EXPORTED_SYMBOLS = ["EnsureFxAccountsWebChannel"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; /*global Components */
+
+Cu.import("resource://gre/modules/Accounts.jsm"); /*global Accounts */
+Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
+Cu.import("resource://gre/modules/WebChannel.jsm"); /*global WebChannel */
+Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /*global XPCOMUtils */
+
+const log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccounts");
+
+const WEBCHANNEL_ID = "account_updates";
+
+const COMMAND_LOADED = "fxaccounts:loaded";
+const COMMAND_CAN_LINK_ACCOUNT = "fxaccounts:can_link_account";
+const COMMAND_LOGIN = "fxaccounts:login";
+const COMMAND_CHANGE_PASSWORD = "fxaccounts:change_password";
+const COMMAND_DELETE_ACCOUNT = "fxaccounts:delete_account";
+const COMMAND_PROFILE_CHANGE = "profile:change";
+const COMMAND_SYNC_PREFERENCES = "fxaccounts:sync_preferences";
+
+const PREF_LAST_FXA_USER = "identity.fxaccounts.lastSignedInUserHash";
+
+XPCOMUtils.defineLazyGetter(this, "strings",
+ () => Services.strings.createBundle("chrome://browser/locale/aboutAccounts.properties")); /*global strings */
+
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Prompt", "resource://gre/modules/Prompt.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", "resource://gre/modules/UITelemetry.jsm");
+
+this.FxAccountsWebChannelHelpers = function() {
+};
+
+this.FxAccountsWebChannelHelpers.prototype = {
+ /**
+ * Get the hash of account name of the previously signed in account.
+ */
+ getPreviousAccountNameHashPref() {
+ try {
+ return Services.prefs.getComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString).data;
+ } catch (_) {
+ return "";
+ }
+ },
+
+ /**
+ * Given an account name, set the hash of the previously signed in account.
+ *
+ * @param acctName the account name of the user's account.
+ */
+ setPreviousAccountNameHashPref(acctName) {
+ let string = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ string.data = this.sha256(acctName);
+ Services.prefs.setComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString, string);
+ },
+
+ /**
+ * Given a string, returns the SHA265 hash in base64.
+ */
+ sha256(str) {
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ // Data is an array of bytes.
+ let data = converter.convertToByteArray(str, {});
+ let hasher = Cc["@mozilla.org/security/hash;1"]
+ .createInstance(Ci.nsICryptoHash);
+ hasher.init(hasher.SHA256);
+ hasher.update(data, data.length);
+
+ return hasher.finish(true);
+ },
+};
+
+/**
+ * Create a new FxAccountsWebChannel to listen for account updates.
+ *
+ * @param {Object} options Options
+ * @param {Object} options
+ * @param {String} options.content_uri
+ * The FxA Content server uri
+ * @param {String} options.channel_id
+ * The ID of the WebChannel
+ * @param {String} options.helpers
+ * Helpers functions. Should only be passed in for testing.
+ * @constructor
+ */
+this.FxAccountsWebChannel = function(options) {
+ if (!options) {
+ throw new Error("Missing configuration options");
+ }
+ if (!options["content_uri"]) {
+ throw new Error("Missing 'content_uri' option");
+ }
+ this._contentUri = options.content_uri;
+
+ if (!options["channel_id"]) {
+ throw new Error("Missing 'channel_id' option");
+ }
+ this._webChannelId = options.channel_id;
+
+ // options.helpers is only specified by tests.
+ this._helpers = options.helpers || new FxAccountsWebChannelHelpers(options);
+
+ this._setupChannel();
+};
+
+this.FxAccountsWebChannel.prototype = {
+ /**
+ * WebChannel that is used to communicate with content page
+ */
+ _channel: null,
+
+ /**
+ * WebChannel ID.
+ */
+ _webChannelId: null,
+ /**
+ * WebChannel origin, used to validate origin of messages
+ */
+ _webChannelOrigin: null,
+
+ /**
+ * Release all resources that are in use.
+ */
+ tearDown() {
+ this._channel.stopListening();
+ this._channel = null;
+ this._channelCallback = null;
+ },
+
+ /**
+ * Configures and registers a new WebChannel
+ *
+ * @private
+ */
+ _setupChannel() {
+ // if this.contentUri is present but not a valid URI, then this will throw an error.
+ try {
+ this._webChannelOrigin = Services.io.newURI(this._contentUri, null, null);
+ this._registerChannel();
+ } catch (e) {
+ log.e(e.toString());
+ throw e;
+ }
+ },
+
+ /**
+ * Create a new channel with the WebChannelBroker, setup a callback listener
+ * @private
+ */
+ _registerChannel() {
+ /**
+ * Processes messages that are called back from the FxAccountsChannel
+ *
+ * @param webChannelId {String}
+ * Command webChannelId
+ * @param message {Object}
+ * Command message
+ * @param sendingContext {Object}
+ * Message sending context.
+ * @param sendingContext.browser {browser}
+ * The <browser> object that captured the
+ * WebChannelMessageToChrome.
+ * @param sendingContext.eventTarget {EventTarget}
+ * The <EventTarget> where the message was sent.
+ * @param sendingContext.principal {Principal}
+ * The <Principal> of the EventTarget where the message was sent.
+ * @private
+ *
+ */
+ let listener = (webChannelId, message, sendingContext) => {
+ if (message) {
+ let command = message.command;
+ let data = message.data;
+ log.d("FxAccountsWebChannel message received, command: " + command);
+
+ // Respond to the message with true or false.
+ let respond = (data) => {
+ let response = {
+ command: command,
+ messageId: message.messageId,
+ data: data
+ };
+ log.d("Sending response to command: " + command);
+ this._channel.send(response, sendingContext);
+ };
+
+ switch (command) {
+ case COMMAND_LOADED:
+ let mm = sendingContext.browser.docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+ mm.sendAsyncMessage(COMMAND_LOADED);
+ break;
+
+ case COMMAND_CAN_LINK_ACCOUNT:
+ Accounts.getFirefoxAccount().then(account => {
+ if (account) {
+ // If we /have/ an Android Account, we never allow the user to
+ // login to a different account. They need to manually delete
+ // the first Android Account and then create a new one.
+ if (account.email == data.email) {
+ // In future, we should use a UID for this comparison.
+ log.d("Relinking existing Android Account: email addresses agree.");
+ respond({ok: true});
+ } else {
+ log.w("Not relinking existing Android Account: email addresses disagree!");
+ let message = strings.GetStringFromName("relinkDenied.message");
+ let buttonLabel = strings.GetStringFromName("relinkDenied.openPrefs");
+ Snackbars.show(message, Snackbars.LENGTH_LONG, {
+ action: {
+ label: buttonLabel,
+ callback: () => {
+ // We have an account, so this opens Sync native preferences.
+ Accounts.launchSetup();
+ },
+ }
+ });
+ respond({ok: false});
+ }
+ } else {
+ // If we /don't have/ an Android Account, we warn if we're
+ // connecting to a new Account. This is to minimize surprise;
+ // we never did this when changing accounts via the native UI.
+ let prevAcctHash = this._helpers.getPreviousAccountNameHashPref();
+ let shouldShowWarning = prevAcctHash && (prevAcctHash != this._helpers.sha256(data.email));
+
+ if (shouldShowWarning) {
+ log.w("Warning about creating a new Android Account: previously linked to different email address!");
+ let message = strings.formatStringFromName("relinkVerify.message", [data.email], 1);
+ new Prompt({
+ title: strings.GetStringFromName("relinkVerify.title"),
+ message: message,
+ buttons: [
+ // This puts Cancel on the right.
+ strings.GetStringFromName("relinkVerify.cancel"),
+ strings.GetStringFromName("relinkVerify.continue"),
+ ],
+ }).show(result => respond({ok: result && result.button == 1}));
+ } else {
+ log.d("Not warning about creating a new Android Account: no previously linked email address.");
+ respond({ok: true});
+ }
+ }
+ }).catch(e => {
+ log.e(e.toString());
+ respond({ok: false});
+ });
+ break;
+
+ case COMMAND_LOGIN:
+ // Either create a new Android Account or re-connect an existing
+ // Android Account here. There's not much to be done if we don't
+ // succeed or get an error.
+ Accounts.getFirefoxAccount().then(account => {
+ if (!account) {
+ return Accounts.createFirefoxAccountFromJSON(data).then(success => {
+ if (!success) {
+ throw new Error("Could not create Firefox Account!");
+ }
+ UITelemetry.addEvent("action.1", "content", null, "fxaccount-create");
+ return success;
+ });
+ } else {
+ return Accounts.updateFirefoxAccountFromJSON(data).then(success => {
+ if (!success) {
+ throw new Error("Could not update Firefox Account!");
+ }
+ UITelemetry.addEvent("action.1", "content", null, "fxaccount-login");
+ return success;
+ });
+ }
+ })
+ .then(success => {
+ if (!success) {
+ throw new Error("Could not create or update Firefox Account!");
+ }
+
+ // Remember who it is so we can show a relink warning when appropriate.
+ this._helpers.setPreviousAccountNameHashPref(data.email);
+
+ log.i("Created or updated Firefox Account.");
+ })
+ .catch(e => {
+ log.e(e.toString());
+ });
+ break;
+
+ case COMMAND_CHANGE_PASSWORD:
+ // Only update an existing Android Account.
+ Accounts.getFirefoxAccount().then(account => {
+ if (!account) {
+ throw new Error("Can't change password of non-existent Firefox Account!");
+ }
+ return Accounts.updateFirefoxAccountFromJSON(data);
+ })
+ .then(success => {
+ if (!success) {
+ throw new Error("Could not change Firefox Account password!");
+ }
+ UITelemetry.addEvent("action.1", "content", null, "fxaccount-changepassword");
+ log.i("Changed Firefox Account password.");
+ })
+ .catch(e => {
+ log.e(e.toString());
+ });
+ break;
+
+ case COMMAND_DELETE_ACCOUNT:
+ // The fxa-content-server has already confirmed the user's intent.
+ // Bombs away. There's no recovery from failure, and not even a
+ // real need to check an account exists (although we do, for error
+ // messaging only).
+ Accounts.getFirefoxAccount().then(account => {
+ if (!account) {
+ throw new Error("Can't delete non-existent Firefox Account!");
+ }
+ return Accounts.deleteFirefoxAccount().then(success => {
+ if (!success) {
+ throw new Error("Could not delete Firefox Account!");
+ }
+ UITelemetry.addEvent("action.1", "content", null, "fxaccount-delete");
+ log.i("Firefox Account deleted.");
+ });
+ }).catch(e => {
+ log.e(e.toString());
+ });
+ break;
+
+ case COMMAND_PROFILE_CHANGE:
+ // Only update an existing Android Account.
+ Accounts.getFirefoxAccount().then(account => {
+ if (!account) {
+ throw new Error("Can't change profile of non-existent Firefox Account!");
+ }
+ UITelemetry.addEvent("action.1", "content", null, "fxaccount-changeprofile");
+ return Accounts.notifyFirefoxAccountProfileChanged();
+ })
+ .catch(e => {
+ log.e(e.toString());
+ });
+ break;
+
+ case COMMAND_SYNC_PREFERENCES:
+ UITelemetry.addEvent("action.1", "content", null, "fxaccount-syncprefs");
+ Accounts.showSyncPreferences()
+ .catch(e => {
+ log.e(e.toString());
+ });
+ break;
+
+ default:
+ log.w("Ignoring unrecognized FxAccountsWebChannel command: " + JSON.stringify(command));
+ break;
+ }
+ }
+ };
+
+ this._channelCallback = listener;
+ this._channel = new WebChannel(this._webChannelId, this._webChannelOrigin);
+ this._channel.listen(listener);
+
+ log.d("FxAccountsWebChannel registered: " + this._webChannelId + " with origin " + this._webChannelOrigin.prePath);
+ }
+};
+
+var singleton;
+// The entry-point for this module, which ensures only one of our channels is
+// ever created - we require this because the WebChannel is global in scope and
+// allowing multiple channels would cause such notifications to be sent multiple
+// times.
+this.EnsureFxAccountsWebChannel = function() {
+ if (!singleton) {
+ let contentUri = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri");
+ // The FxAccountsWebChannel listens for events and updates the Java layer.
+ singleton = new this.FxAccountsWebChannel({
+ content_uri: contentUri,
+ channel_id: WEBCHANNEL_ID,
+ });
+ }
+};
diff --git a/mobile/android/modules/HelperApps.jsm b/mobile/android/modules/HelperApps.jsm
new file mode 100644
index 0000000000..0ac478da09
--- /dev/null
+++ b/mobile/android/modules/HelperApps.jsm
@@ -0,0 +1,229 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+/* globals ContentAreaUtils */
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
+ "resource://gre/modules/Prompt.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging",
+ "resource://gre/modules/Messaging.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() {
+ let ContentAreaUtils = {};
+ Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils);
+ return ContentAreaUtils;
+});
+
+this.EXPORTED_SYMBOLS = ["App","HelperApps"];
+
+function App(data) {
+ this.name = data.name;
+ this.isDefault = data.isDefault;
+ this.packageName = data.packageName;
+ this.activityName = data.activityName;
+ this.iconUri = "-moz-icon://" + data.packageName;
+}
+
+App.prototype = {
+ // callback will be null if a result is not requested
+ launch: function(uri, callback) {
+ HelperApps._launchApp(this, uri, callback);
+ return false;
+ }
+}
+
+var HelperApps = {
+ get defaultBrowsers() {
+ delete this.defaultBrowsers;
+ this.defaultBrowsers = this._getHandlers("http://www.example.com", {
+ filterBrowsers: false,
+ filterHtml: false
+ });
+ return this.defaultBrowsers;
+ },
+
+ // Finds handlers that have registered for text/html pages or urls ending in html. Some apps, like
+ // the Samsung Video player will only appear for these urls, while some Browsers (like Link Bubble)
+ // won't register here because of the text/html mime type.
+ get defaultHtmlHandlers() {
+ delete this.defaultHtmlHandlers;
+ return this.defaultHtmlHandlers = this._getHandlers("http://www.example.com/index.html", {
+ filterBrowsers: false,
+ filterHtml: false
+ });
+ },
+
+ _getHandlers: function(url, options) {
+ let values = {};
+
+ let handlers = this.getAppsForUri(Services.io.newURI(url, null, null), options);
+ handlers.forEach(function(app) {
+ values[app.name] = app;
+ }, this);
+
+ return values;
+ },
+
+ get protoSvc() {
+ delete this.protoSvc;
+ return this.protoSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"].getService(Ci.nsIExternalProtocolService);
+ },
+
+ get urlHandlerService() {
+ delete this.urlHandlerService;
+ return this.urlHandlerService = Cc["@mozilla.org/uriloader/external-url-handler-service;1"].getService(Ci.nsIExternalURLHandlerService);
+ },
+
+ prompt: function showPicker(apps, promptOptions, callback) {
+ let p = new Prompt(promptOptions).addIconGrid({ items: apps });
+ p.show(callback);
+ },
+
+ getAppsForProtocol: function getAppsForProtocol(scheme) {
+ let protoHandlers = this.protoSvc.getProtocolHandlerInfoFromOS(scheme, {}).possibleApplicationHandlers;
+
+ let results = {};
+ for (let i = 0; i < protoHandlers.length; i++) {
+ try {
+ let protoApp = protoHandlers.queryElementAt(i, Ci.nsIHandlerApp);
+ results[protoApp.name] = new App({
+ name: protoApp.name,
+ description: protoApp.detailedDescription,
+ });
+ } catch(e) {}
+ }
+
+ return results;
+ },
+
+ getAppsForUri: function getAppsForUri(uri, flags = { }, callback) {
+ // Return early for well-known internal schemes
+ if (!uri || uri.schemeIs("about") || uri.schemeIs("chrome")) {
+ if (callback) {
+ callback([]);
+ }
+ return [];
+ }
+
+ flags.filterBrowsers = "filterBrowsers" in flags ? flags.filterBrowsers : true;
+ flags.filterHtml = "filterHtml" in flags ? flags.filterHtml : true;
+
+ // Query for apps that can/can't handle the mimetype
+ let msg = this._getMessage("Intent:GetHandlers", uri, flags);
+ let parseData = (d) => {
+ let apps = []
+ if (!d) {
+ return apps;
+ }
+
+ apps = this._parseApps(d.apps);
+
+ if (flags.filterBrowsers) {
+ apps = apps.filter((app) => {
+ return app.name && !this.defaultBrowsers[app.name];
+ });
+ }
+
+ // Some apps will register for html files (the Samsung Video player) but should be shown
+ // for non-HTML files (like videos). This filters them only if the page has an htm of html
+ // file extension.
+ if (flags.filterHtml) {
+ // Matches from the first '.' to the end of the string, '?', or '#'
+ let ext = /\.([^\?#]*)/.exec(uri.path);
+ if (ext && (ext[1] === "html" || ext[1] === "htm")) {
+ apps = apps.filter(function(app) {
+ return app.name && !this.defaultHtmlHandlers[app.name];
+ }, this);
+ }
+ }
+
+ return apps;
+ };
+
+ if (!callback) {
+ let data = this._sendMessageSync(msg);
+ return parseData(data);
+ } else {
+ Messaging.sendRequestForResult(msg).then(function(data) {
+ callback(parseData(data));
+ });
+ }
+ },
+
+ launchUri: function launchUri(uri) {
+ let msg = this._getMessage("Intent:Open", uri);
+ Messaging.sendRequest(msg);
+ },
+
+ _parseApps: function _parseApps(appInfo) {
+ // appInfo -> {apps: [app1Label, app1Default, app1PackageName, app1ActivityName, app2Label, app2Defaut, ...]}
+ // see GeckoAppShell.java getHandlersForIntent function for details
+ const numAttr = 4; // 4 elements per ResolveInfo: label, default, package name, activity name.
+
+ let apps = [];
+ for (let i = 0; i < appInfo.length; i += numAttr) {
+ apps.push(new App({"name" : appInfo[i],
+ "isDefault" : appInfo[i+1],
+ "packageName" : appInfo[i+2],
+ "activityName" : appInfo[i+3]}));
+ }
+
+ return apps;
+ },
+
+ _getMessage: function(type, uri, options = {}) {
+ let mimeType = options.mimeType;
+ if (uri && mimeType == undefined) {
+ mimeType = ContentAreaUtils.getMIMETypeForURI(uri) || "";
+ }
+
+ return {
+ type: type,
+ mime: mimeType,
+ action: options.action || "", // empty action string defaults to android.intent.action.VIEW
+ url: uri ? uri.spec : "",
+ packageName: options.packageName || "",
+ className: options.className || ""
+ };
+ },
+
+ _launchApp: function launchApp(app, uri, callback) {
+ if (callback) {
+ let msg = this._getMessage("Intent:OpenForResult", uri, {
+ packageName: app.packageName,
+ className: app.activityName
+ });
+
+ Messaging.sendRequestForResult(msg).then(callback);
+ } else {
+ let msg = this._getMessage("Intent:Open", uri, {
+ packageName: app.packageName,
+ className: app.activityName
+ });
+
+ Messaging.sendRequest(msg);
+ }
+ },
+
+ _sendMessageSync: function(msg) {
+ let res = null;
+ Messaging.sendRequestForResult(msg).then(function(data) {
+ res = data;
+ });
+
+ let thread = Services.tm.currentThread;
+ while (res == null) {
+ thread.processNextEvent(true);
+ }
+
+ return res;
+ },
+};
diff --git a/mobile/android/modules/Home.jsm b/mobile/android/modules/Home.jsm
new file mode 100644
index 0000000000..e77d35dbda
--- /dev/null
+++ b/mobile/android/modules/Home.jsm
@@ -0,0 +1,487 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["Home"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/SharedPreferences.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+
+// Keep this in sync with the constant defined in PanelAuthCache.java
+const PREFS_PANEL_AUTH_PREFIX = "home_panels_auth_";
+
+// Default weight for a banner message.
+const DEFAULT_WEIGHT = 100;
+
+// See bug 915424
+function resolveGeckoURI(aURI) {
+ if (!aURI)
+ throw "Can't resolve an empty uri";
+
+ if (aURI.startsWith("chrome://")) {
+ let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]);
+ return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec;
+ } else if (aURI.startsWith("resource://")) {
+ let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
+ return handler.resolveURI(Services.io.newURI(aURI, null, null));
+ }
+ return aURI;
+}
+
+function BannerMessage(options) {
+ let uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+ this.id = uuidgen.generateUUID().toString();
+
+ if ("text" in options && options.text != null)
+ this.text = options.text;
+
+ if ("icon" in options && options.icon != null)
+ this.iconURI = resolveGeckoURI(options.icon);
+
+ if ("onshown" in options && typeof options.onshown === "function")
+ this.onshown = options.onshown;
+
+ if ("onclick" in options && typeof options.onclick === "function")
+ this.onclick = options.onclick;
+
+ if ("ondismiss" in options && typeof options.ondismiss === "function")
+ this.ondismiss = options.ondismiss;
+
+ let weight = parseInt(options.weight, 10);
+ this.weight = weight > 0 ? weight : DEFAULT_WEIGHT;
+}
+
+// We need this object to have access to the HomeBanner
+// private members without leaking it outside Home.jsm.
+var HomeBannerMessageHandlers;
+
+var HomeBanner = (function () {
+ // Whether there is a "HomeBanner:Get" request we couldn't fulfill.
+ let _pendingRequest = false;
+
+ // Functions used to handle messages sent from Java.
+ HomeBannerMessageHandlers = {
+ "HomeBanner:Get": function handleBannerGet(data) {
+ if (Object.keys(_messages).length > 0) {
+ _sendBannerData();
+ } else {
+ _pendingRequest = true;
+ }
+ }
+ };
+
+ // Holds the messages that will rotate through the banner.
+ let _messages = {};
+
+ // Choose a random message from the set of messages, biasing towards those with higher weight.
+ // Weight logic copied from desktop snippets:
+ // https://github.com/mozilla/snippets-service/blob/7d80edb8b1cddaed075275c2fc7cdf69a10f4003/snippets/base/templates/base/includes/snippet_js.html#L119
+ let _sendBannerData = function() {
+ let totalWeight = 0;
+ for (let key in _messages) {
+ let message = _messages[key];
+ totalWeight += message.weight;
+ message.totalWeight = totalWeight;
+ }
+
+ let threshold = Math.random() * totalWeight;
+ for (let key in _messages) {
+ let message = _messages[key];
+ if (threshold < message.totalWeight) {
+ Messaging.sendRequest({
+ type: "HomeBanner:Data",
+ id: message.id,
+ text: message.text,
+ iconURI: message.iconURI
+ });
+ return;
+ }
+ }
+ };
+
+ let _handleShown = function(id) {
+ let message = _messages[id];
+ if (message.onshown)
+ message.onshown();
+ };
+
+ let _handleClick = function(id) {
+ let message = _messages[id];
+ if (message.onclick)
+ message.onclick();
+ };
+
+ let _handleDismiss = function(id) {
+ let message = _messages[id];
+ if (message.ondismiss)
+ message.ondismiss();
+ };
+
+ return Object.freeze({
+ observe: function(subject, topic, data) {
+ switch(topic) {
+ case "HomeBanner:Shown":
+ _handleShown(data);
+ break;
+
+ case "HomeBanner:Click":
+ _handleClick(data);
+ break;
+
+ case "HomeBanner:Dismiss":
+ _handleDismiss(data);
+ break;
+ }
+ },
+
+ /**
+ * Adds a new banner message to the rotation.
+ *
+ * @return id Unique identifer for the message.
+ */
+ add: function(options) {
+ let message = new BannerMessage(options);
+ _messages[message.id] = message;
+
+ // If this is the first message we're adding, add
+ // observers to listen for requests from the Java UI.
+ if (Object.keys(_messages).length == 1) {
+ Services.obs.addObserver(this, "HomeBanner:Shown", false);
+ Services.obs.addObserver(this, "HomeBanner:Click", false);
+ Services.obs.addObserver(this, "HomeBanner:Dismiss", false);
+
+ // Send a message to Java if there's a pending "HomeBanner:Get" request.
+ if (_pendingRequest) {
+ _pendingRequest = false;
+ _sendBannerData();
+ }
+ }
+
+ return message.id;
+ },
+
+ /**
+ * Removes a banner message from the rotation.
+ *
+ * @param id The id of the message to remove.
+ */
+ remove: function(id) {
+ if (!(id in _messages)) {
+ throw "Home.banner: Can't remove message that doesn't exist: id = " + id;
+ }
+
+ delete _messages[id];
+
+ // If there are no more messages, remove the observers.
+ if (Object.keys(_messages).length == 0) {
+ Services.obs.removeObserver(this, "HomeBanner:Shown");
+ Services.obs.removeObserver(this, "HomeBanner:Click");
+ Services.obs.removeObserver(this, "HomeBanner:Dismiss");
+ }
+ }
+ });
+})();
+
+// We need this object to have access to the HomePanels
+// private members without leaking it outside Home.jsm.
+var HomePanelsMessageHandlers;
+
+var HomePanels = (function () {
+ // Functions used to handle messages sent from Java.
+ HomePanelsMessageHandlers = {
+
+ "HomePanels:Get": function handlePanelsGet(data) {
+ data = JSON.parse(data);
+
+ let requestId = data.requestId;
+ let ids = data.ids || null;
+
+ let panels = [];
+ for (let id in _registeredPanels) {
+ // Null ids means we want to fetch all available panels
+ if (ids == null || ids.indexOf(id) >= 0) {
+ try {
+ panels.push(_generatePanel(id));
+ } catch(e) {
+ Cu.reportError("Home.panels: Invalid options, panel.id = " + id + ": " + e);
+ }
+ }
+ }
+
+ Messaging.sendRequest({
+ type: "HomePanels:Data",
+ panels: panels,
+ requestId: requestId
+ });
+ },
+
+ "HomePanels:Authenticate": function handlePanelsAuthenticate(id) {
+ // Generate panel options to get auth handler.
+ let options = _registeredPanels[id]();
+ if (!options.auth) {
+ throw "Home.panels: Invalid auth for panel.id = " + id;
+ }
+ if (!options.auth.authenticate || typeof options.auth.authenticate !== "function") {
+ throw "Home.panels: Invalid auth authenticate function: panel.id = " + this.id;
+ }
+ options.auth.authenticate();
+ },
+
+ "HomePanels:RefreshView": function handlePanelsRefreshView(data) {
+ data = JSON.parse(data);
+
+ let options = _registeredPanels[data.panelId]();
+ let view = options.views[data.viewIndex];
+
+ if (!view) {
+ throw "Home.panels: Invalid view for panel.id = " + data.panelId
+ + ", view.index = " + data.viewIndex;
+ }
+
+ if (!view.onrefresh || typeof view.onrefresh !== "function") {
+ throw "Home.panels: Invalid onrefresh for panel.id = " + data.panelId
+ + ", view.index = " + data.viewIndex;
+ }
+
+ view.onrefresh();
+ },
+
+ "HomePanels:Installed": function handlePanelsInstalled(id) {
+ _assertPanelExists(id);
+
+ let options = _registeredPanels[id]();
+ if (!options.oninstall) {
+ return;
+ }
+ if (typeof options.oninstall !== "function") {
+ throw "Home.panels: Invalid oninstall function: panel.id = " + this.id;
+ }
+ options.oninstall();
+ },
+
+ "HomePanels:Uninstalled": function handlePanelsUninstalled(id) {
+ _assertPanelExists(id);
+
+ let options = _registeredPanels[id]();
+ if (!options.onuninstall) {
+ return;
+ }
+ if (typeof options.onuninstall !== "function") {
+ throw "Home.panels: Invalid onuninstall function: panel.id = " + this.id;
+ }
+ options.onuninstall();
+ }
+ };
+
+ // Holds the current set of registered panels that can be
+ // installed, updated, uninstalled, or unregistered. It maps
+ // panel ids with the functions that dynamically generate
+ // their respective panel options. This is used to retrieve
+ // the current list of available panels in the system.
+ // See HomePanels:Get handler.
+ let _registeredPanels = {};
+
+ // Valid layouts for a panel.
+ let Layout = Object.freeze({
+ FRAME: "frame"
+ });
+
+ // Valid types of views for a dataset.
+ let View = Object.freeze({
+ LIST: "list",
+ GRID: "grid"
+ });
+
+ // Valid item types for a panel view.
+ let Item = Object.freeze({
+ ARTICLE: "article",
+ IMAGE: "image",
+ ICON: "icon"
+ });
+
+ // Valid item handlers for a panel view.
+ let ItemHandler = Object.freeze({
+ BROWSER: "browser",
+ INTENT: "intent"
+ });
+
+ function Panel(id, options) {
+ this.id = id;
+ this.title = options.title;
+ this.layout = options.layout;
+ this.views = options.views;
+ this.default = !!options.default;
+
+ if (!this.id || !this.title) {
+ throw "Home.panels: Can't create a home panel without an id and title!";
+ }
+
+ if (!this.layout) {
+ // Use FRAME layout by default
+ this.layout = Layout.FRAME;
+ } else if (!_valueExists(Layout, this.layout)) {
+ throw "Home.panels: Invalid layout for panel: panel.id = " + this.id + ", panel.layout =" + this.layout;
+ }
+
+ for (let view of this.views) {
+ if (!_valueExists(View, view.type)) {
+ throw "Home.panels: Invalid view type: panel.id = " + this.id + ", view.type = " + view.type;
+ }
+
+ if (!view.itemType) {
+ if (view.type == View.LIST) {
+ // Use ARTICLE item type by default in LIST views
+ view.itemType = Item.ARTICLE;
+ } else if (view.type == View.GRID) {
+ // Use IMAGE item type by default in GRID views
+ view.itemType = Item.IMAGE;
+ }
+ } else if (!_valueExists(Item, view.itemType)) {
+ throw "Home.panels: Invalid item type: panel.id = " + this.id + ", view.itemType = " + view.itemType;
+ }
+
+ if (!view.itemHandler) {
+ // Use BROWSER item handler by default
+ view.itemHandler = ItemHandler.BROWSER;
+ } else if (!_valueExists(ItemHandler, view.itemHandler)) {
+ throw "Home.panels: Invalid item handler: panel.id = " + this.id + ", view.itemHandler = " + view.itemHandler;
+ }
+
+ if (!view.dataset) {
+ throw "Home.panels: No dataset provided for view: panel.id = " + this.id + ", view.type = " + view.type;
+ }
+
+ if (view.onrefresh) {
+ view.refreshEnabled = true;
+ }
+ }
+
+ if (options.auth) {
+ if (!options.auth.messageText) {
+ throw "Home.panels: Invalid auth messageText: panel.id = " + this.id;
+ }
+ if (!options.auth.buttonText) {
+ throw "Home.panels: Invalid auth buttonText: panel.id = " + this.id;
+ }
+
+ this.authConfig = {
+ messageText: options.auth.messageText,
+ buttonText: options.auth.buttonText
+ };
+
+ // Include optional image URL if it is specified.
+ if (options.auth.imageUrl) {
+ this.authConfig.imageUrl = options.auth.imageUrl;
+ }
+ }
+
+ if (options.position >= 0) {
+ this.position = options.position;
+ }
+ }
+
+ let _generatePanel = function(id) {
+ let options = _registeredPanels[id]();
+ return new Panel(id, options);
+ };
+
+ // Helper function used to see if a value is in an object.
+ let _valueExists = function(obj, value) {
+ for (let key in obj) {
+ if (obj[key] == value) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ let _assertPanelExists = function(id) {
+ if (!(id in _registeredPanels)) {
+ throw "Home.panels: Panel doesn't exist: id = " + id;
+ }
+ };
+
+ return Object.freeze({
+ Layout: Layout,
+ View: View,
+ Item: Item,
+ ItemHandler: ItemHandler,
+
+ register: function(id, optionsCallback) {
+ // Bail if the panel already exists
+ if (id in _registeredPanels) {
+ throw "Home.panels: Panel already exists: id = " + id;
+ }
+
+ if (!optionsCallback || typeof optionsCallback !== "function") {
+ throw "Home.panels: Panel callback must be a function: id = " + id;
+ }
+
+ _registeredPanels[id] = optionsCallback;
+ },
+
+ unregister: function(id) {
+ _assertPanelExists(id);
+
+ delete _registeredPanels[id];
+ },
+
+ install: function(id) {
+ _assertPanelExists(id);
+
+ Messaging.sendRequest({
+ type: "HomePanels:Install",
+ panel: _generatePanel(id)
+ });
+ },
+
+ uninstall: function(id) {
+ _assertPanelExists(id);
+
+ Messaging.sendRequest({
+ type: "HomePanels:Uninstall",
+ id: id
+ });
+ },
+
+ update: function(id) {
+ _assertPanelExists(id);
+
+ Messaging.sendRequest({
+ type: "HomePanels:Update",
+ panel: _generatePanel(id)
+ });
+ },
+
+ setAuthenticated: function(id, isAuthenticated) {
+ _assertPanelExists(id);
+
+ let authKey = PREFS_PANEL_AUTH_PREFIX + id;
+ let sharedPrefs = SharedPreferences.forProfile();
+ sharedPrefs.setBoolPref(authKey, isAuthenticated);
+ }
+ });
+})();
+
+// Public API
+this.Home = Object.freeze({
+ banner: HomeBanner,
+ panels: HomePanels,
+
+ // Lazy notification observer registered in browser.js
+ observe: function(subject, topic, data) {
+ if (topic in HomeBannerMessageHandlers) {
+ HomeBannerMessageHandlers[topic](data);
+ } else if (topic in HomePanelsMessageHandlers) {
+ HomePanelsMessageHandlers[topic](data);
+ } else {
+ Cu.reportError("Home.observe: message handler not found for topic: " + topic);
+ }
+ }
+});
diff --git a/mobile/android/modules/HomeProvider.jsm b/mobile/android/modules/HomeProvider.jsm
new file mode 100644
index 0000000000..bca8fa526e
--- /dev/null
+++ b/mobile/android/modules/HomeProvider.jsm
@@ -0,0 +1,407 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [ "HomeProvider" ];
+
+const { utils: Cu, classes: Cc, interfaces: Ci } = Components;
+
+Cu.import("resource://gre/modules/Messaging.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Sqlite.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/*
+ * SCHEMA_VERSION history:
+ * 1: Create HomeProvider (bug 942288)
+ * 2: Add filter column to items table (bug 942295/975841)
+ * 3: Add background_color and background_url columns (bug 1157539)
+ */
+const SCHEMA_VERSION = 3;
+
+// The maximum number of items you can attempt to save at once.
+const MAX_SAVE_COUNT = 100;
+
+XPCOMUtils.defineLazyGetter(this, "DB_PATH", function() {
+ return OS.Path.join(OS.Constants.Path.profileDir, "home.sqlite");
+});
+
+const PREF_STORAGE_LAST_SYNC_TIME_PREFIX = "home.storage.lastSyncTime.";
+const PREF_SYNC_UPDATE_MODE = "home.sync.updateMode";
+const PREF_SYNC_CHECK_INTERVAL_SECS = "home.sync.checkIntervalSecs";
+
+XPCOMUtils.defineLazyGetter(this, "gSyncCheckIntervalSecs", function() {
+ return Services.prefs.getIntPref(PREF_SYNC_CHECK_INTERVAL_SECS);
+});
+
+XPCOMUtils.defineLazyServiceGetter(this,
+ "gUpdateTimerManager", "@mozilla.org/updates/timer-manager;1", "nsIUpdateTimerManager");
+
+/**
+ * All SQL statements should be defined here.
+ */
+const SQL = {
+ createItemsTable:
+ "CREATE TABLE items (" +
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+ "dataset_id TEXT NOT NULL, " +
+ "url TEXT," +
+ "title TEXT," +
+ "description TEXT," +
+ "image_url TEXT," +
+ "background_color TEXT," +
+ "background_url TEXT," +
+ "filter TEXT," +
+ "created INTEGER" +
+ ")",
+
+ dropItemsTable:
+ "DROP TABLE items",
+
+ insertItem:
+ "INSERT INTO items (dataset_id, url, title, description, image_url, background_color, background_url, filter, created) " +
+ "VALUES (:dataset_id, :url, :title, :description, :image_url, :background_color, :background_url, :filter, :created)",
+
+ deleteFromDataset:
+ "DELETE FROM items WHERE dataset_id = :dataset_id",
+
+ addColumnBackgroundColor:
+ "ALTER TABLE items ADD COLUMN background_color TEXT",
+
+ addColumnBackgroundUrl:
+ "ALTER TABLE items ADD COLUMN background_url TEXT",
+}
+
+/**
+ * Technically this function checks to see if the user is on a local network,
+ * but we express this as "wifi" to the user.
+ */
+function isUsingWifi() {
+ let network = Cc["@mozilla.org/network/network-link-service;1"].getService(Ci.nsINetworkLinkService);
+ return (network.linkType === Ci.nsINetworkLinkService.LINK_TYPE_WIFI || network.linkType === Ci.nsINetworkLinkService.LINK_TYPE_ETHERNET);
+}
+
+function getNowInSeconds() {
+ return Math.round(Date.now() / 1000);
+}
+
+function getLastSyncPrefName(datasetId) {
+ return PREF_STORAGE_LAST_SYNC_TIME_PREFIX + datasetId;
+}
+
+// Whether or not we've registered an update timer.
+var gTimerRegistered = false;
+
+// Map of datasetId -> { interval: <integer>, callback: <function> }
+var gSyncCallbacks = {};
+
+/**
+ * nsITimerCallback implementation. Checks to see if it's time to sync any registered datasets.
+ *
+ * @param timer The timer which has expired.
+ */
+function syncTimerCallback(timer) {
+ for (let datasetId in gSyncCallbacks) {
+ let lastSyncTime = 0;
+ try {
+ lastSyncTime = Services.prefs.getIntPref(getLastSyncPrefName(datasetId));
+ } catch(e) { }
+
+ let now = getNowInSeconds();
+ let { interval: interval, callback: callback } = gSyncCallbacks[datasetId];
+
+ if (lastSyncTime < now - interval) {
+ let success = HomeProvider.requestSync(datasetId, callback);
+ if (success) {
+ Services.prefs.setIntPref(getLastSyncPrefName(datasetId), now);
+ }
+ }
+ }
+}
+
+this.HomeStorage = function(datasetId) {
+ this.datasetId = datasetId;
+};
+
+this.ValidationError = function(message) {
+ this.name = "ValidationError";
+ this.message = message;
+};
+ValidationError.prototype = new Error();
+ValidationError.prototype.constructor = ValidationError;
+
+this.HomeProvider = Object.freeze({
+ ValidationError: ValidationError,
+
+ /**
+ * Returns a storage associated with a given dataset identifer.
+ *
+ * @param datasetId
+ * (string) Unique identifier for the dataset.
+ *
+ * @return HomeStorage
+ */
+ getStorage: function(datasetId) {
+ return new HomeStorage(datasetId);
+ },
+
+ /**
+ * Checks to see if it's an appropriate time to sync.
+ *
+ * @param datasetId Unique identifier for the dataset to sync.
+ * @param callback Function to call when it's time to sync, called with datasetId as a parameter.
+ *
+ * @return boolean Whether or not we were able to sync.
+ */
+ requestSync: function(datasetId, callback) {
+ // Make sure it's a good time to sync.
+ if ((Services.prefs.getIntPref(PREF_SYNC_UPDATE_MODE) === 1) && !isUsingWifi()) {
+ Cu.reportError("HomeProvider: Failed to sync because device is not on a local network");
+ return false;
+ }
+
+ callback(datasetId);
+ return true;
+ },
+
+ /**
+ * Specifies that a sync should be requested for the given dataset and update interval.
+ *
+ * @param datasetId Unique identifier for the dataset to sync.
+ * @param interval Update interval in seconds. By default, this is throttled to 3600 seconds (1 hour).
+ * @param callback Function to call when it's time to sync, called with datasetId as a parameter.
+ */
+ addPeriodicSync: function(datasetId, interval, callback) {
+ // Warn developers if they're expecting more frequent notifications that we allow.
+ if (interval < gSyncCheckIntervalSecs) {
+ Cu.reportError("HomeProvider: Warning for dataset " + datasetId +
+ " : Sync notifications are throttled to " + gSyncCheckIntervalSecs + " seconds");
+ }
+
+ gSyncCallbacks[datasetId] = {
+ interval: interval,
+ callback: callback
+ };
+
+ if (!gTimerRegistered) {
+ gUpdateTimerManager.registerTimer("home-provider-sync-timer", syncTimerCallback, gSyncCheckIntervalSecs);
+ gTimerRegistered = true;
+ }
+ },
+
+ /**
+ * Removes a periodic sync timer.
+ *
+ * @param datasetId Dataset to sync.
+ */
+ removePeriodicSync: function(datasetId) {
+ delete gSyncCallbacks[datasetId];
+ Services.prefs.clearUserPref(getLastSyncPrefName(datasetId));
+ // You can't unregister a update timer, so we don't try to do that.
+ }
+});
+
+var gDatabaseEnsured = false;
+
+/**
+ * Creates the database schema.
+ */
+function createDatabase(db) {
+ return Task.spawn(function create_database_task() {
+ yield db.execute(SQL.createItemsTable);
+ });
+}
+
+/**
+ * Migrates the database schema to a new version.
+ */
+function upgradeDatabase(db, oldVersion, newVersion) {
+ return Task.spawn(function upgrade_database_task() {
+ switch (oldVersion) {
+ case 1:
+ // Migration from v1 to latest:
+ // Recreate the items table discarding any
+ // existing data.
+ yield db.execute(SQL.dropItemsTable);
+ yield db.execute(SQL.createItemsTable);
+ break;
+
+ case 2:
+ // Migration from v2 to latest:
+ // Add new columns: background_color, background_url
+ yield db.execute(SQL.addColumnBackgroundColor);
+ yield db.execute(SQL.addColumnBackgroundUrl);
+ break;
+ }
+ });
+}
+
+/**
+ * Opens a database connection and makes sure that the database schema version
+ * is correct, performing migrations if necessary. Consumers should be sure
+ * to close any database connections they open.
+ *
+ * @return Promise
+ * @resolves Handle on an opened SQLite database.
+ */
+function getDatabaseConnection() {
+ return Task.spawn(function get_database_connection_task() {
+ let db = yield Sqlite.openConnection({ path: DB_PATH });
+ if (gDatabaseEnsured) {
+ throw new Task.Result(db);
+ }
+
+ try {
+ // Check to see if we need to perform any migrations.
+ let dbVersion = parseInt(yield db.getSchemaVersion());
+
+ // getSchemaVersion() returns a 0 int if the schema
+ // version is undefined.
+ if (dbVersion === 0) {
+ yield createDatabase(db);
+ } else if (dbVersion < SCHEMA_VERSION) {
+ yield upgradeDatabase(db, dbVersion, SCHEMA_VERSION);
+ }
+
+ yield db.setSchemaVersion(SCHEMA_VERSION);
+ } catch(e) {
+ // Close the DB connection before passing the exception to the consumer.
+ yield db.close();
+ throw e;
+ }
+
+ gDatabaseEnsured = true;
+ throw new Task.Result(db);
+ });
+}
+
+/**
+ * Validates an item to be saved to the DB.
+ *
+ * @param item
+ * (object) item object to be validated.
+ */
+function validateItem(datasetId, item) {
+ if (!item.url) {
+ throw new ValidationError('HomeStorage: All rows must have an URL: datasetId = ' +
+ datasetId);
+ }
+
+ if (!item.image_url && !item.title && !item.description) {
+ throw new ValidationError('HomeStorage: All rows must have at least an image URL, ' +
+ 'or a title or a description: datasetId = ' + datasetId);
+ }
+}
+
+var gRefreshTimers = {};
+
+/**
+ * Sends a message to Java to refresh the given dataset. Delays sending
+ * messages to avoid successive refreshes, which can result in flashing views.
+ */
+function refreshDataset(datasetId) {
+ // Bail if there's already a refresh timer waiting to fire
+ if (gRefreshTimers[datasetId]) {
+ return;
+ }
+
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(function(timer) {
+ delete gRefreshTimers[datasetId];
+
+ Messaging.sendRequest({
+ type: "HomePanels:RefreshDataset",
+ datasetId: datasetId
+ });
+ }, 100, Ci.nsITimer.TYPE_ONE_SHOT);
+
+ gRefreshTimers[datasetId] = timer;
+}
+
+HomeStorage.prototype = {
+ /**
+ * Saves data rows to the DB.
+ *
+ * @param data
+ * An array of JS objects represnting row items to save.
+ * Each object may have the following properties:
+ * - url (string)
+ * - title (string)
+ * - description (string)
+ * - image_url (string)
+ * - filter (string)
+ * @param options
+ * A JS object holding additional cofiguration properties.
+ * The following properties are currently supported:
+ * - replace (boolean): Whether or not to replace existing items.
+ *
+ * @return Promise
+ * @resolves When the operation has completed.
+ */
+ save: function(data, options) {
+ if (data && data.length > MAX_SAVE_COUNT) {
+ throw "save failed for dataset = " + this.datasetId +
+ ": you cannot save more than " + MAX_SAVE_COUNT + " items at once";
+ }
+
+ return Task.spawn(function save_task() {
+ let db = yield getDatabaseConnection();
+ try {
+ yield db.executeTransaction(function save_transaction() {
+ if (options && options.replace) {
+ yield db.executeCached(SQL.deleteFromDataset, { dataset_id: this.datasetId });
+ }
+
+ // Insert data into DB.
+ for (let item of data) {
+ validateItem(this.datasetId, item);
+
+ // XXX: Directly pass item as params? More validation for item?
+ let params = {
+ dataset_id: this.datasetId,
+ url: item.url,
+ title: item.title,
+ description: item.description,
+ image_url: item.image_url,
+ background_color: item.background_color,
+ background_url: item.background_url,
+ filter: item.filter,
+ created: Date.now()
+ };
+ yield db.executeCached(SQL.insertItem, params);
+ }
+ }.bind(this));
+ } finally {
+ yield db.close();
+ }
+
+ refreshDataset(this.datasetId);
+ }.bind(this));
+ },
+
+ /**
+ * Deletes all rows associated with this storage.
+ *
+ * @return Promise
+ * @resolves When the operation has completed.
+ */
+ deleteAll: function() {
+ return Task.spawn(function delete_all_task() {
+ let db = yield getDatabaseConnection();
+ try {
+ let params = { dataset_id: this.datasetId };
+ yield db.executeCached(SQL.deleteFromDataset, params);
+ } finally {
+ yield db.close();
+ }
+
+ refreshDataset(this.datasetId);
+ }.bind(this));
+ }
+};
diff --git a/mobile/android/modules/JNI.jsm b/mobile/android/modules/JNI.jsm
new file mode 100644
index 0000000000..1e10b9cfb2
--- /dev/null
+++ b/mobile/android/modules/JNI.jsm
@@ -0,0 +1,1167 @@
+// JavaScript to Java bridge via the Java Native Interface
+// Allows calling into Android SDK from JavaScript in Firefox Add-On.
+// Released into the public domain.
+// C. Scott Ananian <cscott@laptop.org> (http://cscott.net)
+
+// NOTE: All changes to this file should first be pushed to the repo at:
+// https://github.com/cscott/skeleton-addon-fxandroid/tree/jni
+
+var EXPORTED_SYMBOLS = ["JNI","android_log"];
+
+Components.utils.import("resource://gre/modules/ctypes.jsm")
+
+var liblog = ctypes.open('liblog.so');
+var android_log = liblog.declare("__android_log_write",
+ ctypes.default_abi,
+ ctypes.int32_t,
+ ctypes.int32_t,
+ ctypes.char.ptr,
+ ctypes.char.ptr);
+
+var libxul = ctypes.open('libxul.so');
+
+var jenvptr = ctypes.voidptr_t;
+var jclass = ctypes.voidptr_t;
+var jobject = ctypes.voidptr_t;
+var jvalue = ctypes.voidptr_t;
+var jmethodid = ctypes.voidptr_t;
+var jfieldid = ctypes.voidptr_t;
+
+var jboolean = ctypes.uint8_t;
+var jbyte = ctypes.int8_t;
+var jchar = ctypes.uint16_t;
+var jshort = ctypes.int16_t;
+var jint = ctypes.int32_t;
+var jlong = ctypes.int64_t;
+var jfloat = ctypes.float32_t;
+var jdouble = ctypes.float64_t;
+
+var jsize = jint;
+var jstring = jobject;
+var jarray = jobject;
+var jthrowable = jobject;
+
+var JNINativeInterface = new ctypes.StructType(
+ "JNINativeInterface",
+ [{reserved0: ctypes.voidptr_t},
+ {reserved1: ctypes.voidptr_t},
+ {reserved2: ctypes.voidptr_t},
+ {reserved3: ctypes.voidptr_t},
+ {GetVersion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.int32_t,
+ [ctypes.voidptr_t]).ptr},
+ {DefineClass: new ctypes.FunctionType(ctypes.default_abi,
+ jclass,
+ [jenvptr, ctypes.char.ptr, jobject,
+ jbyte.array(), jsize]).ptr},
+ {FindClass: new ctypes.FunctionType(ctypes.default_abi,
+ jclass,
+ [jenvptr,
+ ctypes.char.ptr]).ptr},
+ {FromReflectedMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jmethodid,
+ [jenvptr, jobject]).ptr},
+ {FromReflectedField: new ctypes.FunctionType(ctypes.default_abi,
+ jfieldid,
+ [jenvptr, jobject]).ptr},
+ {ToReflectedMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jobject,
+ [jenvptr, jclass,
+ jmethodid]).ptr},
+ {GetSuperclass: new ctypes.FunctionType(ctypes.default_abi,
+ jclass, [jenvptr, jclass]).ptr},
+ {IsAssignableFrom: new ctypes.FunctionType(ctypes.default_abi,
+ jboolean,
+ [jenvptr, jclass, jclass]).ptr},
+ {ToReflectedField: new ctypes.FunctionType(ctypes.default_abi,
+ jobject,
+ [jenvptr, jclass,
+ jfieldid]).ptr},
+ {Throw: new ctypes.FunctionType(ctypes.default_abi,
+ jint, [jenvptr, jthrowable]).ptr},
+ {ThrowNew: new ctypes.FunctionType(ctypes.default_abi,
+ jint, [jenvptr, jclass,
+ ctypes.char.ptr]).ptr},
+ {ExceptionOccurred: new ctypes.FunctionType(ctypes.default_abi,
+ jthrowable, [jenvptr]).ptr},
+ {ExceptionDescribe: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t, [jenvptr]).ptr},
+ {ExceptionClear: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t, [jenvptr]).ptr},
+ {FatalError: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr,
+ ctypes.char.ptr]).ptr},
+ {PushLocalFrame: new ctypes.FunctionType(ctypes.default_abi,
+ jint,
+ [jenvptr, jint]).ptr},
+ {PopLocalFrame: new ctypes.FunctionType(ctypes.default_abi,
+ jobject,
+ [jenvptr, jobject]).ptr},
+ {NewGlobalRef: new ctypes.FunctionType(ctypes.default_abi,
+ jobject, [jenvptr, jobject]).ptr},
+ {DeleteGlobalRef: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr,
+ jobject]).ptr},
+ {DeleteLocalRef: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr,
+ jobject]).ptr},
+ {IsSameObject: new ctypes.FunctionType(ctypes.default_abi,
+ jboolean,
+ [jenvptr, jobject, jobject]).ptr},
+ {NewLocalRef: new ctypes.FunctionType(ctypes.default_abi,
+ jobject, [jenvptr, jobject]).ptr},
+ {EnsureLocalCapacity: new ctypes.FunctionType(ctypes.default_abi,
+ jint, [jenvptr, jint]).ptr},
+ {AllocObject: new ctypes.FunctionType(ctypes.default_abi,
+ jobject, [jenvptr, jclass]).ptr},
+ {NewObject: new ctypes.FunctionType(ctypes.default_abi,
+ jobject,
+ [jenvptr,
+ jclass,
+ jmethodid,
+ "..."]).ptr},
+ {NewObjectV: ctypes.voidptr_t},
+ {NewObjectA: ctypes.voidptr_t},
+ {GetObjectClass: new ctypes.FunctionType(ctypes.default_abi,
+ jclass,
+ [jenvptr, jobject]).ptr},
+ {IsInstanceOf: new ctypes.FunctionType(ctypes.default_abi,
+ jboolean,
+ [jenvptr, jobject, jclass]).ptr},
+ {GetMethodID: new ctypes.FunctionType(ctypes.default_abi,
+ jmethodid,
+ [jenvptr,
+ jclass,
+ ctypes.char.ptr,
+ ctypes.char.ptr]).ptr},
+ {CallObjectMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jobject,
+ [jenvptr, jobject, jmethodid,
+ "..."]).ptr},
+ {CallObjectMethodV: ctypes.voidptr_t},
+ {CallObjectMethodA: ctypes.voidptr_t},
+ {CallBooleanMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jboolean,
+ [jenvptr,
+ jobject,
+ jmethodid,
+ "..."]).ptr},
+ {CallBooleanMethodV: ctypes.voidptr_t},
+ {CallBooleanMethodA: ctypes.voidptr_t},
+ {CallByteMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jbyte,
+ [jenvptr,
+ jobject,
+ jmethodid,
+ "..."]).ptr},
+ {CallByteMethodV: ctypes.voidptr_t},
+ {CallByteMethodA: ctypes.voidptr_t},
+ {CallCharMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jchar,
+ [jenvptr,
+ jobject,
+ jmethodid,
+ "..."]).ptr},
+ {CallCharMethodV: ctypes.voidptr_t},
+ {CallCharMethodA: ctypes.voidptr_t},
+ {CallShortMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jshort,
+ [jenvptr,
+ jobject,
+ jmethodid,
+ "..."]).ptr},
+ {CallShortMethodV: ctypes.voidptr_t},
+ {CallShortMethodA: ctypes.voidptr_t},
+ {CallIntMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jint,
+ [jenvptr,
+ jobject,
+ jmethodid,
+ "..."]).ptr},
+ {CallIntMethodV: ctypes.voidptr_t},
+ {CallIntMethodA: ctypes.voidptr_t},
+ {CallLongMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jlong,
+ [jenvptr,
+ jobject,
+ jmethodid,
+ "..."]).ptr},
+ {CallLongMethodV: ctypes.voidptr_t},
+ {CallLongMethodA: ctypes.voidptr_t},
+ {CallFloatMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jfloat,
+ [jenvptr,
+ jobject,
+ jmethodid,
+ "..."]).ptr},
+ {CallFloatMethodV: ctypes.voidptr_t},
+ {CallFloatMethodA: ctypes.voidptr_t},
+ {CallDoubleMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jdouble,
+ [jenvptr,
+ jobject,
+ jmethodid,
+ "..."]).ptr},
+ {CallDoubleMethodV: ctypes.voidptr_t},
+ {CallDoubleMethodA: ctypes.voidptr_t},
+ {CallVoidMethod: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr,
+ jobject,
+ jmethodid,
+ "..."]).ptr},
+ {CallVoidMethodV: ctypes.voidptr_t},
+ {CallVoidMethodA: ctypes.voidptr_t},
+ {CallNonvirtualObjectMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jobject,
+ [jenvptr, jobject,
+ jclass, jmethodid,
+ "..."]).ptr},
+ {CallNonvirtualObjectMethodV: ctypes.voidptr_t},
+ {CallNonvirtualObjectMethodA: ctypes.voidptr_t},
+ {CallNonvirtualBooleanMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jboolean,
+ [jenvptr, jobject,
+ jclass, jmethodid,
+ "..."]).ptr},
+ {CallNonvirtualBooleanMethodV: ctypes.voidptr_t},
+ {CallNonvirtualBooleanMethodA: ctypes.voidptr_t},
+ {CallNonvirtualByteMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jbyte,
+ [jenvptr, jobject,
+ jclass, jmethodid,
+ "..."]).ptr},
+ {CallNonvirtualByteMethodV: ctypes.voidptr_t},
+ {CallNonvirtualByteMethodA: ctypes.voidptr_t},
+ {CallNonvirtualCharMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jchar,
+ [jenvptr, jobject,
+ jclass, jmethodid,
+ "..."]).ptr},
+ {CallNonvirtualCharMethodV: ctypes.voidptr_t},
+ {CallNonvirtualCharMethodA: ctypes.voidptr_t},
+ {CallNonvirtualShortMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jshort,
+ [jenvptr, jobject,
+ jclass, jmethodid,
+ "..."]).ptr},
+ {CallNonvirtualShortMethodV: ctypes.voidptr_t},
+ {CallNonvirtualShortMethodA: ctypes.voidptr_t},
+ {CallNonvirtualIntMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jint,
+ [jenvptr, jobject,
+ jclass, jmethodid,
+ "..."]).ptr},
+ {CallNonvirtualIntMethodV: ctypes.voidptr_t},
+ {CallNonvirtualIntMethodA: ctypes.voidptr_t},
+ {CallNonvirtualLongMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jlong,
+ [jenvptr, jobject,
+ jclass, jmethodid,
+ "..."]).ptr},
+ {CallNonvirtualLongMethodV: ctypes.voidptr_t},
+ {CallNonvirtualLongMethodA: ctypes.voidptr_t},
+ {CallNonvirtualFloatMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jfloat,
+ [jenvptr, jobject,
+ jclass, jmethodid,
+ "..."]).ptr},
+ {CallNonvirtualFloatMethodV: ctypes.voidptr_t},
+ {CallNonvirtualFloatMethodA: ctypes.voidptr_t},
+ {CallNonvirtualDoubleMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jdouble,
+ [jenvptr, jobject,
+ jclass, jmethodid,
+ "..."]).ptr},
+ {CallNonvirtualDoubleMethodV: ctypes.voidptr_t},
+ {CallNonvirtualDoubleMethodA: ctypes.voidptr_t},
+ {CallNonvirtualVoidMethod: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jobject,
+ jclass, jmethodid,
+ "..."]).ptr},
+ {CallNonvirtualVoidMethodV: ctypes.voidptr_t},
+ {CallNonvirtualVoidMethodA: ctypes.voidptr_t},
+ {GetFieldID: new ctypes.FunctionType(ctypes.default_abi,
+ jfieldid,
+ [jenvptr, jclass,
+ ctypes.char.ptr,
+ ctypes.char.ptr]).ptr},
+ {GetObjectField: new ctypes.FunctionType(ctypes.default_abi,
+ jobject,
+ [jenvptr, jobject,
+ jfieldid]).ptr},
+ {GetBooleanField: new ctypes.FunctionType(ctypes.default_abi,
+ jboolean,
+ [jenvptr, jobject,
+ jfieldid]).ptr},
+ {GetByteField: new ctypes.FunctionType(ctypes.default_abi,
+ jbyte,
+ [jenvptr, jobject,
+ jfieldid]).ptr},
+ {GetCharField: new ctypes.FunctionType(ctypes.default_abi,
+ jchar,
+ [jenvptr, jobject,
+ jfieldid]).ptr},
+ {GetShortField: new ctypes.FunctionType(ctypes.default_abi,
+ jshort,
+ [jenvptr, jobject,
+ jfieldid]).ptr},
+ {GetIntField: new ctypes.FunctionType(ctypes.default_abi,
+ jint,
+ [jenvptr, jobject,
+ jfieldid]).ptr},
+ {GetLongField: new ctypes.FunctionType(ctypes.default_abi,
+ jlong,
+ [jenvptr, jobject,
+ jfieldid]).ptr},
+ {GetFloatField: new ctypes.FunctionType(ctypes.default_abi,
+ jfloat,
+ [jenvptr, jobject,
+ jfieldid]).ptr},
+ {GetDoubleField: new ctypes.FunctionType(ctypes.default_abi,
+ jdouble,
+ [jenvptr, jobject,
+ jfieldid]).ptr},
+ {SetObjectField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jobject,
+ jfieldid, jobject]).ptr},
+ {SetBooleanField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jobject,
+ jfieldid, jboolean]).ptr},
+ {SetByteField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jobject,
+ jfieldid, jbyte]).ptr},
+ {SetCharField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jobject,
+ jfieldid, jchar]).ptr},
+ {SetShortField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jobject,
+ jfieldid, jshort]).ptr},
+ {SetIntField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jobject,
+ jfieldid, jint]).ptr},
+ {SetLongField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jobject,
+ jfieldid, jlong]).ptr},
+ {SetFloatField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jobject,
+ jfieldid, jfloat]).ptr},
+ {SetDoubleField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jobject,
+ jfieldid, jdouble]).ptr},
+ {GetStaticMethodID: new ctypes.FunctionType(ctypes.default_abi,
+ jmethodid,
+ [jenvptr,
+ jclass,
+ ctypes.char.ptr,
+ ctypes.char.ptr]).ptr},
+ {CallStaticObjectMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jobject,
+ [jenvptr, jclass,
+ jmethodid,
+ "..."]).ptr},
+ {CallStaticObjectMethodV: ctypes.voidptr_t},
+ {CallStaticObjectMethodA: ctypes.voidptr_t},
+ {CallStaticBooleanMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jboolean,
+ [jenvptr, jclass,
+ jmethodid,
+ "..."]).ptr},
+ {CallStaticBooleanMethodV: ctypes.voidptr_t},
+ {CallStaticBooleanMethodA: ctypes.voidptr_t},
+ {CallStaticByteMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jbyte,
+ [jenvptr, jclass,
+ jmethodid,
+ "..."]).ptr},
+ {CallStaticByteMethodV: ctypes.voidptr_t},
+ {CallStaticByteMethodA: ctypes.voidptr_t},
+ {CallStaticCharMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jchar,
+ [jenvptr, jclass,
+ jmethodid,
+ "..."]).ptr},
+ {CallStaticCharMethodV: ctypes.voidptr_t},
+ {CallStaticCharMethodA: ctypes.voidptr_t},
+ {CallStaticShortMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jshort,
+ [jenvptr, jclass,
+ jmethodid,
+ "..."]).ptr},
+ {CallStaticShortMethodV: ctypes.voidptr_t},
+ {CallStaticShortMethodA: ctypes.voidptr_t},
+ {CallStaticIntMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jint,
+ [jenvptr, jclass,
+ jmethodid,
+ "..."]).ptr},
+ {CallStaticIntMethodV: ctypes.voidptr_t},
+ {CallStaticIntMethodA: ctypes.voidptr_t},
+ {CallStaticLongMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jlong,
+ [jenvptr, jclass,
+ jmethodid,
+ "..."]).ptr},
+ {CallStaticLongMethodV: ctypes.voidptr_t},
+ {CallStaticLongMethodA: ctypes.voidptr_t},
+ {CallStaticFloatMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jfloat,
+ [jenvptr, jclass,
+ jmethodid,
+ "..."]).ptr},
+ {CallStaticFloatMethodV: ctypes.voidptr_t},
+ {CallStaticFloatMethodA: ctypes.voidptr_t},
+ {CallStaticDoubleMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jdouble,
+ [jenvptr, jclass,
+ jmethodid,
+ "..."]).ptr},
+ {CallStaticDoubleMethodV: ctypes.voidptr_t},
+ {CallStaticDoubleMethodA: ctypes.voidptr_t},
+ {CallStaticVoidMethod: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jclass,
+ jmethodid,
+ "..."]).ptr},
+ {CallStaticVoidMethodV: ctypes.voidptr_t},
+ {CallStaticVoidMethodA: ctypes.voidptr_t},
+ {GetStaticFieldID: new ctypes.FunctionType(ctypes.default_abi,
+ jfieldid,
+ [jenvptr, jclass,
+ ctypes.char.ptr,
+ ctypes.char.ptr]).ptr},
+ {GetStaticObjectField: new ctypes.FunctionType(ctypes.default_abi,
+ jobject,
+ [jenvptr, jclass,
+ jfieldid]).ptr},
+ {GetStaticBooleanField: new ctypes.FunctionType(ctypes.default_abi,
+ jboolean,
+ [jenvptr, jclass,
+ jfieldid]).ptr},
+ {GetStaticByteField: new ctypes.FunctionType(ctypes.default_abi,
+ jbyte,
+ [jenvptr, jclass,
+ jfieldid]).ptr},
+ {GetStaticCharField: new ctypes.FunctionType(ctypes.default_abi,
+ jchar,
+ [jenvptr, jclass,
+ jfieldid]).ptr},
+ {GetStaticShortField: new ctypes.FunctionType(ctypes.default_abi,
+ jshort,
+ [jenvptr, jclass,
+ jfieldid]).ptr},
+ {GetStaticIntField: new ctypes.FunctionType(ctypes.default_abi,
+ jint,
+ [jenvptr, jclass,
+ jfieldid]).ptr},
+ {GetStaticLongField: new ctypes.FunctionType(ctypes.default_abi,
+ jlong,
+ [jenvptr, jclass,
+ jfieldid]).ptr},
+ {GetStaticFloatField: new ctypes.FunctionType(ctypes.default_abi,
+ jfloat,
+ [jenvptr, jclass,
+ jfieldid]).ptr},
+ {GetStaticDoubleField: new ctypes.FunctionType(ctypes.default_abi,
+ jdouble,
+ [jenvptr, jclass,
+ jfieldid]).ptr},
+ {SetStaticObjectField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jclass,
+ jfieldid, jobject]).ptr},
+ {SetStaticBooleanField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jclass,
+ jfieldid, jboolean]).ptr},
+ {SetStaticByteField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jclass,
+ jfieldid, jbyte]).ptr},
+ {SetStaticCharField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jclass,
+ jfieldid, jchar]).ptr},
+ {SetStaticShortField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jclass,
+ jfieldid, jshort]).ptr},
+ {SetStaticIntField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jclass,
+ jfieldid, jint]).ptr},
+ {SetStaticLongField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jclass,
+ jfieldid, jlong]).ptr},
+ {SetStaticFloatField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jclass,
+ jfieldid, jfloat]).ptr},
+ {SetStaticDoubleField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jclass,
+ jfieldid, jdouble]).ptr},
+
+ {NewString: new ctypes.FunctionType(ctypes.default_abi,
+ jstring,
+ [jenvptr, jchar.ptr, jsize]).ptr},
+ {GetStringLength: new ctypes.FunctionType(ctypes.default_abi,
+ jsize,
+ [jenvptr, jstring]).ptr},
+ {GetStringChars: new ctypes.FunctionType(ctypes.default_abi,
+ jchar.ptr,
+ [jenvptr, jstring,
+ jboolean.ptr]).ptr},
+ {ReleaseStringChars: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jstring,
+ jchar.ptr]).ptr},
+
+ {NewStringUTF: new ctypes.FunctionType(ctypes.default_abi,
+ jstring,
+ [jenvptr,
+ ctypes.char.ptr]).ptr},
+ {GetStringUTFLength: new ctypes.FunctionType(ctypes.default_abi,
+ jsize,
+ [jenvptr, jstring]).ptr},
+ {GetStringUTFChars: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.char.ptr,
+ [jenvptr, jstring,
+ jboolean.ptr]).ptr},
+ {ReleaseStringUTFChars: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jstring,
+ ctypes.char.ptr]).ptr},
+ {GetArrayLength: new ctypes.FunctionType(ctypes.default_abi,
+ jsize,
+ [jenvptr, jarray]).ptr},
+ {NewObjectArray: new ctypes.FunctionType(ctypes.default_abi,
+ jarray,
+ [jenvptr, jsize,
+ jclass, jobject]).ptr},
+ {GetObjectArrayElement: new ctypes.FunctionType(ctypes.default_abi,
+ jobject,
+ [jenvptr, jarray,
+ jsize]).ptr},
+ {SetObjectArrayElement: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jobject]).ptr},
+ {NewBooleanArray: new ctypes.FunctionType(ctypes.default_abi,
+ jarray,
+ [jenvptr, jsize]).ptr},
+ {NewByteArray: new ctypes.FunctionType(ctypes.default_abi,
+ jarray,
+ [jenvptr, jsize]).ptr},
+ {NewCharArray: new ctypes.FunctionType(ctypes.default_abi,
+ jarray,
+ [jenvptr, jsize]).ptr},
+ {NewShortArray: new ctypes.FunctionType(ctypes.default_abi,
+ jarray,
+ [jenvptr, jsize]).ptr},
+ {NewIntArray: new ctypes.FunctionType(ctypes.default_abi,
+ jarray,
+ [jenvptr, jsize]).ptr},
+ {NewLongArray: new ctypes.FunctionType(ctypes.default_abi,
+ jarray,
+ [jenvptr, jsize]).ptr},
+ {NewFloatArray: new ctypes.FunctionType(ctypes.default_abi,
+ jarray,
+ [jenvptr, jsize]).ptr},
+ {NewDoubleArray: new ctypes.FunctionType(ctypes.default_abi,
+ jarray,
+ [jenvptr, jsize]).ptr},
+ {GetBooleanArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ jboolean.ptr,
+ [jenvptr, jarray,
+ jboolean.ptr]).ptr},
+ {GetByteArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ jbyte.ptr,
+ [jenvptr, jarray,
+ jboolean.ptr]).ptr},
+ {GetCharArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ jchar.ptr,
+ [jenvptr, jarray,
+ jboolean.ptr]).ptr},
+ {GetShortArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ jshort.ptr,
+ [jenvptr, jarray,
+ jboolean.ptr]).ptr},
+ {GetIntArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ jint.ptr,
+ [jenvptr, jarray,
+ jboolean.ptr]).ptr},
+ {GetLongArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ jlong.ptr,
+ [jenvptr, jarray,
+ jboolean.ptr]).ptr},
+ {GetFloatArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ jfloat.ptr,
+ [jenvptr, jarray,
+ jboolean.ptr]).ptr},
+ {GetDoubleArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ jdouble.ptr,
+ [jenvptr, jarray,
+ jboolean.ptr]).ptr},
+ {ReleaseBooleanArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jboolean.ptr,
+ jint]).ptr},
+ {ReleaseByteArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jbyte.ptr,
+ jint]).ptr},
+ {ReleaseCharArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jchar.ptr,
+ jint]).ptr},
+ {ReleaseShortArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jshort.ptr,
+ jint]).ptr},
+ {ReleaseIntArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jint.ptr,
+ jint]).ptr},
+ {ReleaseLongArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jlong.ptr,
+ jint]).ptr},
+ {ReleaseFloatArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jfloat.ptr,
+ jint]).ptr},
+ {ReleaseDoubleArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jdouble.ptr,
+ jint]).ptr},
+ {GetBooleanArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jboolean.array()]).ptr},
+ {GetByteArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jbyte.array()]).ptr},
+ {GetCharArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jchar.array()]).ptr},
+ {GetShortArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jshort.array()]).ptr},
+ {GetIntArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jint.array()]).ptr},
+ {GetLongArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jlong.array()]).ptr},
+ {GetFloatArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jfloat.array()]).ptr},
+ {GetDoubleArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jdouble.array()]).ptr},
+ {SetBooleanArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jboolean.array()]).ptr},
+ {SetByteArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jbyte.array()]).ptr},
+ {SetCharArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jchar.array()]).ptr},
+ {SetShortArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jshort.array()]).ptr},
+ {SetIntArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jint.array()]).ptr},
+ {SetLongArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jlong.array()]).ptr},
+ {SetFloatArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jfloat.array()]).ptr},
+ {SetDoubleArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jdouble.array()]).ptr},
+ {RegisterNatives: ctypes.voidptr_t},
+ {UnregisterNatives: ctypes.voidptr_t},
+ {MonitorEnter: new ctypes.FunctionType(ctypes.default_abi,
+ jint, [jenvptr, jobject]).ptr},
+ {MonitorExit: new ctypes.FunctionType(ctypes.default_abi,
+ jint, [jenvptr, jobject]).ptr},
+ {GetJavaVM: ctypes.voidptr_t},
+ {GetStringRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jstring,
+ jsize, jsize,
+ jchar.array()]).ptr},
+ {GetStringUTFRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jstring,
+ jsize, jsize,
+ ctypes.char.array()]).ptr},
+ {GetPrimitiveArrayCritical: ctypes.voidptr_t},
+ {ReleasePrimitiveArrayCritical: ctypes.voidptr_t},
+ {GetStringCritical: ctypes.voidptr_t},
+ {ReleaseStringCritical: ctypes.voidptr_t},
+ {NewWeakGlobalRef: ctypes.voidptr_t},
+ {DeleteWeakGlobalRef: ctypes.voidptr_t},
+ {ExceptionCheck: new ctypes.FunctionType(ctypes.default_abi,
+ jboolean, [jenvptr]).ptr},
+ {NewDirectByteBuffer: ctypes.voidptr_t},
+ {GetDirectBufferAddress: ctypes.voidptr_t},
+ {GetDirectBufferCapacity: ctypes.voidptr_t},
+ {GetObjectRefType: ctypes.voidptr_t}]
+);
+
+var GetJNIForThread = libxul.declare("GetJNIForThread",
+ ctypes.default_abi,
+ JNINativeInterface.ptr.ptr);
+
+var registry = Object.create(null);
+var classes = Object.create(null);
+
+function JNIUnloadClasses(jenv) {
+ Object.getOwnPropertyNames(registry).forEach(function(classname) {
+ var jcls = unwrap(registry[classname]);
+ jenv.contents.contents.DeleteGlobalRef(jenv, jcls);
+
+ // Purge the registry, so we don't try to reuse stale global references
+ // in JNI calls and we garbage-collect the JS global reference objects.
+ delete registry[classname];
+ });
+
+ // The refs also get added to the 'classes' object, so we should purge it too.
+ // That object is a hierarchical data structure organized by class path parts,
+ // but deleting its own properties should be sufficient to break its refs.
+ Object.getOwnPropertyNames(classes).forEach(function(topLevelPart) {
+ delete classes[topLevelPart];
+ });
+}
+
+var PREFIX = 'js#';
+// this regex matches one component of a type signature:
+// any number of array modifiers, followed by either a
+// primitive type character or L<classname>;
+var sigRegex = () => /\[*([VZBCSIJFD]|L([^.\/;]+(\/[^.\/;]+)*);)/g;
+var ensureSig = function(classname_or_signature) {
+ // convert a classname into a signature,
+ // leaving unchanged signatures. We assume that
+ // anything not a valid signature is a classname.
+ var m = sigRegex().exec(classname_or_signature);
+ return (m && m[0] === classname_or_signature) ? classname_or_signature :
+ 'L' + classname_or_signature.replace(/\./g, '/') + ';';
+};
+var wrap = function(obj, classSig) {
+ if (!classSig) { return obj; }
+ // don't wrap primitive types.
+ if (classSig.charAt(0)!=='L' &&
+ classSig.charAt(0)!=='[') { return obj; }
+ var proto = registry[classSig][PREFIX+'proto'];
+ return new proto(obj);
+};
+var unwrap = function(obj, opt_jenv, opt_ctype) {
+ if (obj && typeof(obj)==='object' && (PREFIX+'obj') in obj) {
+ return obj[PREFIX+'obj'];
+ } else if (opt_jenv && opt_ctype) {
+ if (opt_ctype !== jobject)
+ return opt_ctype(obj); // cast to given primitive ctype
+ if (typeof(obj)==='string')
+ return unwrap(JNINewString(opt_jenv, obj)); // create Java String
+ }
+ return obj;
+};
+var ensureLoaded = function(jenv, classSig) {
+ if (!Object.hasOwnProperty.call(registry, classSig)) {
+ JNILoadClass(jenv, classSig);
+ }
+ return registry[classSig];
+};
+
+function JNINewString(jenv, value) {
+ var s = jenv.contents.contents.NewStringUTF(jenv, ctypes.char.array()(value));
+ ensureLoaded(jenv, "Ljava/lang/String;");
+ return wrap(s, "Ljava/lang/String;");
+}
+
+function JNIReadString(jenv, jstring_value) {
+ var val = unwrap(jstring_value);
+ if ((!val) || val.isNull()) { return null; }
+ var chars = jenv.contents.contents.GetStringUTFChars(jenv, val, null);
+ var result = chars.readString();
+ jenv.contents.contents.ReleaseStringUTFChars(jenv, val, chars);
+ return result;
+}
+
+var sigInfo = {
+ 'V': { name: 'Void', longName: 'Void', ctype: ctypes.void_t },
+ 'Z': { name: 'Boolean', longName: 'Boolean', ctype: jboolean },
+ 'B': { name: 'Byte', longName: 'Byte', ctype: jbyte },
+ 'C': { name: 'Char', longName: 'Char', ctype: jchar },
+ 'S': { name: 'Short', longName: 'Short', ctype: jshort },
+ 'I': { name: 'Int', longName: 'Integer', ctype: jint },
+ 'J': { name: 'Long', longName: 'Long', ctype: jlong },
+ 'F': { name: 'Float', longName: 'Float', ctype: jfloat },
+ 'D': { name: 'Double', longName: 'Double', ctype: jdouble },
+ 'L': { name: 'Object', longName: 'Object', ctype: jobject },
+ '[': { name: 'Object', longName: 'Object', ctype: jarray }
+};
+
+var sig2type = function(sig) { return sigInfo[sig.charAt(0)].name; };
+var sig2ctype = function(sig) { return sigInfo[sig.charAt(0)].ctype; };
+var sig2prim = function(sig) { return sigInfo[sig.charAt(0)].longName; };
+
+// return the class object for a signature string.
+// allocates 1 or 2 local refs
+function JNIClassObj(jenv, classSig) {
+ var jenvpp = function() { return jenv.contents.contents; };
+ // Deal with funny calling convention of JNI FindClass method.
+ // Classes get the leading & trailing chars stripped; primitives
+ // have to be looked up via their wrapper type.
+ var prim = function(ty) {
+ var jcls = jenvpp().FindClass(jenv, "java/lang/"+ty);
+ var jfld = jenvpp().GetStaticFieldID(jenv, jcls, "TYPE",
+ "Ljava/lang/Class;");
+ return jenvpp().GetStaticObjectField(jenv, jcls, jfld);
+ };
+ switch (classSig.charAt(0)) {
+ case '[':
+ return jenvpp().FindClass(jenv, classSig);
+ case 'L':
+ classSig = classSig.substring(1, classSig.indexOf(';'));
+ return jenvpp().FindClass(jenv, classSig);
+ default:
+ return prim(sig2prim(classSig));
+ }
+}
+
+// return the signature string for a Class object.
+// allocates 2 local refs
+function JNIClassSig(jenv, jcls) {
+ var jenvpp = function() { return jenv.contents.contents; };
+ var jclscls = jenvpp().FindClass(jenv, "java/lang/Class");
+ var jmtd = jenvpp().GetMethodID(jenv, jclscls,
+ "getName", "()Ljava/lang/String;");
+ var name = jenvpp().CallObjectMethod(jenv, jcls, jmtd);
+ name = JNIReadString(jenv, name);
+ // API is weird. Make sure we're using slashes not dots
+ name = name.replace(/\./g, '/');
+ // special case primitives, arrays
+ if (name.charAt(0)==='[') return name;
+ switch(name) {
+ case 'void': return 'V';
+ case 'boolean': return 'Z';
+ case 'byte': return 'B';
+ case 'char': return 'C';
+ case 'short': return 'S';
+ case 'int': return 'I';
+ case 'long': return 'J';
+ case 'float': return 'F';
+ case 'double': return 'D';
+ default:
+ return 'L' + name + ';';
+ }
+}
+
+// create dispatch method
+// we resolve overloaded methods only by # of arguments. If you need
+// further resolution, use the 'long form' of the method name, ie:
+// obj['toString()Ljava/lang/String'].call(obj);
+var overloadFunc = function(basename) {
+ return function() {
+ return this[basename+'('+arguments.length+')'].apply(this, arguments);
+ };
+};
+
+// Create appropriate wrapper fields/methods for a Java class.
+function JNILoadClass(jenv, classSig, opt_props) {
+ var jenvpp = function() { return jenv.contents.contents; };
+ var props = opt_props || {};
+
+ // allocate a local reference frame with enough space
+ // this class (1 or 2 local refs) plus superclass (3 refs)
+ // plus array element class (1 or 2 local refs)
+ var numLocals = 7;
+ jenvpp().PushLocalFrame(jenv, numLocals);
+
+ var jcls;
+ if (Object.hasOwnProperty.call(registry, classSig)) {
+ jcls = unwrap(registry[classSig]);
+ } else {
+ jcls = jenvpp().NewGlobalRef(jenv, JNIClassObj(jenv, classSig));
+
+ // get name of superclass
+ var jsuper = jenvpp().GetSuperclass(jenv, jcls);
+ if (jsuper.isNull()) {
+ jsuper = null;
+ } else {
+ jsuper = JNIClassSig(jenv, jsuper);
+ }
+
+ registry[classSig] = Object.create(jsuper?ensureLoaded(jenv, jsuper):null);
+ registry[classSig][PREFIX+'obj'] = jcls; // global ref, persistent.
+ registry[classSig][PREFIX+'proto'] =
+ function(o) { this[PREFIX+'obj'] = o; };
+ registry[classSig][PREFIX+'proto'].prototype =
+ Object.create(jsuper ?
+ ensureLoaded(jenv, jsuper)[PREFIX+'proto'].prototype :
+ null);
+ // Add a __cast__ method to the wrapper corresponding to the class
+ registry[classSig].__cast__ = function(obj) {
+ return wrap(unwrap(obj), classSig);
+ };
+
+ // make wrapper accessible via the classes object.
+ var path = sig2type(classSig).toLowerCase();
+ if (classSig.charAt(0)==='L') {
+ path = classSig.substring(1, classSig.length-1);
+ }
+ if (classSig.charAt(0)!=='[') {
+ var root = classes, i;
+ var parts = path.split('/');
+ for (i = 0; i < parts.length-1; i++) {
+ if (!Object.hasOwnProperty.call(root, parts[i])) {
+ root[parts[i]] = Object.create(null);
+ }
+ root = root[parts[i]];
+ }
+ root[parts[parts.length-1]] = registry[classSig];
+ }
+ }
+
+ var r = registry[classSig];
+ var rpp = r[PREFIX+'proto'].prototype;
+
+ if (classSig.charAt(0)==='[') {
+ // add 'length' field for arrays
+ Object.defineProperty(rpp, 'length', {
+ get: function() {
+ return jenvpp().GetArrayLength(jenv, unwrap(this));
+ }
+ });
+ // add 'get' and 'set' methods, 'new' constructor
+ var elemSig = classSig.substring(1);
+ ensureLoaded(jenv, elemSig);
+
+ registry[elemSig].__array__ = r;
+ if (!Object.hasOwnProperty.call(registry[elemSig], 'array'))
+ registry[elemSig].array = r;
+
+ if (elemSig.charAt(0)==='L' || elemSig.charAt(0)==='[') {
+ var elemClass = unwrap(registry[elemSig]);
+
+ rpp.get = function(idx) {
+ return wrap(jenvpp().GetObjectArrayElement(jenv, unwrap(this), idx),
+ elemSig);
+ };
+ rpp.set = function(idx, value) {
+ jenvpp().SetObjectArrayElement(jenv, unwrap(this), idx,
+ unwrap(value, jenv, jobject));
+ };
+ rpp.getElements = function(start, len) {
+ var i, r=[];
+ for (i=0; i<len; i++) { r.push(this.get(start+i)); }
+ return r;
+ };
+ rpp.setElements = function(start, vals) {
+ vals.forEach(function(v, i) { this.set(start+i, v); }.bind(this));
+ };
+ r['new'] = function(length) {
+ return wrap(jenvpp().NewObjectArray(jenv, length, elemClass, null),
+ classSig);
+ };
+ } else {
+ var ty = sig2type(elemSig), ctype = sig2ctype(elemSig);
+ var constructor = "New"+ty+"Array";
+ var getter = "Get"+ty+"ArrayRegion";
+ var setter = "Set"+ty+"ArrayRegion";
+ rpp.get = function(idx) { return this.getElements(idx, 1)[0]; };
+ rpp.set = function(idx, val) { this.setElements(idx, [val]); };
+ rpp.getElements = function(start, len) {
+ var j = jenvpp();
+ var buf = new (ctype.array())(len);
+ j[getter].call(j, jenv, unwrap(this), start, len, buf);
+ return buf;
+ };
+ rpp.setElements = function(start, vals) {
+ var j = jenvpp();
+ j[setter].call(j, jenv, unwrap(this), start, vals.length,
+ ctype.array()(vals));
+ };
+ r['new'] = function(length) {
+ var j = jenvpp();
+ return wrap(j[constructor].call(j, jenv, length), classSig);
+ };
+ }
+ }
+
+ (props.static_fields || []).forEach(function(fld) {
+ var jfld = jenvpp().GetStaticFieldID(jenv, jcls, fld.name, fld.sig);
+ var ty = sig2type(fld.sig), nm = fld.sig;
+ var getter = "GetStatic"+ty+"Field", setter = "SetStatic"+ty+"Field";
+ ensureLoaded(jenv, nm);
+ var props = {
+ get: function() {
+ var j = jenvpp();
+ return wrap(j[getter].call(j, jenv, jcls, jfld), nm);
+ },
+ set: function(newValue) {
+ var j = jenvpp();
+ j[setter].call(j, jenv, jcls, jfld, unwrap(newValue));
+ }
+ };
+ Object.defineProperty(r, fld.name, props);
+ // add static fields to object instances, too.
+ Object.defineProperty(rpp, fld.name, props);
+ });
+ (props.static_methods || []).forEach(function(mtd) {
+ var jmtd = jenvpp().GetStaticMethodID(jenv, jcls, mtd.name, mtd.sig);
+ var argctypes = mtd.sig.match(sigRegex()).map(s => sig2ctype(s));
+ var returnSig = mtd.sig.substring(mtd.sig.indexOf(')')+1);
+ var ty = sig2type(returnSig), nm = returnSig;
+ var call = "CallStatic"+ty+"Method";
+ ensureLoaded(jenv, nm);
+ r[mtd.name] = rpp[mtd.name] = overloadFunc(mtd.name);
+ r[mtd.name + mtd.sig] = r[mtd.name+'('+(argctypes.length-1)+')'] =
+ // add static methods to object instances, too.
+ rpp[mtd.name + mtd.sig] = rpp[mtd.name+'('+(argctypes.length-1)+')'] = function() {
+ var i, j = jenvpp();
+ var args = [jenv, jcls, jmtd];
+ for (i=0; i<arguments.length; i++) {
+ args.push(unwrap(arguments[i], jenv, argctypes[i]));
+ }
+ return wrap(j[call].apply(j, args), nm);
+ };
+ });
+ (props.constructors || []).forEach(function(mtd) {
+ mtd.name = "<init>";
+ var jmtd = jenvpp().GetMethodID(jenv, jcls, mtd.name, mtd.sig);
+ var argctypes = mtd.sig.match(sigRegex()).map(s => sig2ctype(s));
+ var returnSig = mtd.sig.substring(mtd.sig.indexOf(')')+1);
+
+ r['new'] = overloadFunc('new');
+ r['new'+mtd.sig] = r['new('+(argctypes.length-1)+')'] = function() {
+ var i, j = jenvpp();
+ var args = [jenv, jcls, jmtd];
+ for (i=0; i<arguments.length; i++) {
+ args.push(unwrap(arguments[i], jenv, argctypes[i]));
+ }
+ return wrap(j.NewObject.apply(j, args), classSig);
+ };
+ });
+ (props.fields || []).forEach(function(fld) {
+ var jfld = jenvpp().GetFieldID(jenv, jcls, fld.name, fld.sig);
+ var ty = sig2type(fld.sig), nm = fld.sig;
+ var getter = "Get"+ty+"Field", setter = "Set"+ty+"Field";
+ ensureLoaded(jenv, nm);
+ Object.defineProperty(rpp, fld.name, {
+ get: function() {
+ var j = jenvpp();
+ return wrap(j[getter].call(j, jenv, unwrap(this), jfld), nm);
+ },
+ set: function(newValue) {
+ var j = jenvpp();
+ j[setter].call(j, jenv, unwrap(this), jfld, unwrap(newValue));
+ }
+ });
+ });
+ (props.methods || []).forEach(function(mtd) {
+ var jmtd = jenvpp().GetMethodID(jenv, jcls, mtd.name, mtd.sig);
+ var argctypes = mtd.sig.match(sigRegex()).map(s => sig2ctype(s));
+ var returnSig = mtd.sig.substring(mtd.sig.indexOf(')')+1);
+ var ty = sig2type(returnSig), nm = returnSig;
+ var call = "Call"+ty+"Method";
+ ensureLoaded(jenv, nm);
+ rpp[mtd.name] = overloadFunc(mtd.name);
+ rpp[mtd.name + mtd.sig] = rpp[mtd.name+'('+(argctypes.length-1)+')'] = function() {
+ var i, j = jenvpp();
+ var args = [jenv, unwrap(this), jmtd];
+ for (i=0; i<arguments.length; i++) {
+ args.push(unwrap(arguments[i], jenv, argctypes[i]));
+ }
+ return wrap(j[call].apply(j, args), nm);
+ };
+ });
+ jenvpp().PopLocalFrame(jenv, null);
+ return r;
+}
+
+// exported object
+var JNI = {
+ // primitive types
+ jboolean: jboolean,
+ jbyte: jbyte,
+ jchar: jchar,
+ jshort: jshort,
+ jint: jint,
+ jlong: jlong,
+ jfloat: jfloat,
+ jdouble: jdouble,
+ jsize: jsize,
+
+ // class registry
+ classes: classes,
+
+ // methods
+ GetForThread: GetJNIForThread,
+ NewString: JNINewString,
+ ReadString: JNIReadString,
+ LoadClass: function(jenv, classname_or_signature, props) {
+ return JNILoadClass(jenv, ensureSig(classname_or_signature), props);
+ },
+ UnloadClasses: JNIUnloadClasses
+};
diff --git a/mobile/android/modules/JavaAddonManager.jsm b/mobile/android/modules/JavaAddonManager.jsm
new file mode 100644
index 0000000000..a24535ede4
--- /dev/null
+++ b/mobile/android/modules/JavaAddonManager.jsm
@@ -0,0 +1,115 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["JavaAddonManager"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components; /*global Components */
+
+Cu.import("resource://gre/modules/Messaging.jsm"); /*global Messaging */
+Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
+
+function resolveGeckoURI(uri) {
+ if (!uri) {
+ throw new Error("Can't resolve an empty uri");
+ }
+ if (uri.startsWith("chrome://")) {
+ let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]);
+ return registry.convertChromeURL(Services.io.newURI(uri, null, null)).spec;
+ } else if (uri.startsWith("resource://")) {
+ let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
+ return handler.resolveURI(Services.io.newURI(uri, null, null));
+ }
+ return uri;
+}
+
+/**
+ * A promise-based API
+ */
+var JavaAddonManager = Object.freeze({
+ classInstanceFromFile: function(classname, filename) {
+ if (!classname) {
+ throw new Error("classname cannot be null");
+ }
+ if (!filename) {
+ throw new Error("filename cannot be null");
+ }
+ return Messaging.sendRequestForResult({
+ type: "JavaAddonManagerV1:Load",
+ classname: classname,
+ filename: resolveGeckoURI(filename)
+ })
+ .then((guid) => {
+ if (!guid) {
+ throw new Error("Internal error: guid should not be null");
+ }
+ return new JavaAddonV1({classname: classname, guid: guid});
+ });
+ }
+});
+
+function JavaAddonV1(options = {}) {
+ if (!(this instanceof JavaAddonV1)) {
+ return new JavaAddonV1(options);
+ }
+ if (!options.classname) {
+ throw new Error("options.classname cannot be null");
+ }
+ if (!options.guid) {
+ throw new Error("options.guid cannot be null");
+ }
+ this._classname = options.classname;
+ this._guid = options.guid;
+ this._loaded = true;
+ this._listeners = {};
+}
+
+JavaAddonV1.prototype = Object.freeze({
+ unload: function() {
+ if (!this._loaded) {
+ return;
+ }
+
+ Messaging.sendRequestForResult({
+ type: "JavaAddonManagerV1:Unload",
+ guid: this._guid
+ })
+ .then(() => {
+ this._loaded = false;
+ for (let listener of this._listeners) {
+ // If we use this.removeListener, we prefix twice.
+ Messaging.removeListener(listener);
+ }
+ this._listeners = {};
+ });
+ },
+
+ _prefix: function(message) {
+ let newMessage = Cu.cloneInto(message, {}, { cloneFunctions: false });
+ newMessage.type = this._guid + ":" + message.type;
+ return newMessage;
+ },
+
+ sendRequest: function(message) {
+ return Messaging.sendRequest(this._prefix(message));
+ },
+
+ sendRequestForResult: function(message) {
+ return Messaging.sendRequestForResult(this._prefix(message));
+ },
+
+ addListener: function(listener, message) {
+ let prefixedMessage = this._guid + ":" + message;
+ this._listeners[prefixedMessage] = listener;
+ return Messaging.addListener(listener, prefixedMessage);
+ },
+
+ removeListener: function(message) {
+ let prefixedMessage = this._guid + ":" + message;
+ delete this._listeners[prefixedMessage];
+ return Messaging.removeListener(prefixedMessage);
+ }
+});
diff --git a/mobile/android/modules/LightweightThemeConsumer.jsm b/mobile/android/modules/LightweightThemeConsumer.jsm
new file mode 100644
index 0000000000..3d3ca4c0b7
--- /dev/null
+++ b/mobile/android/modules/LightweightThemeConsumer.jsm
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ["LightweightThemeConsumer"];
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm");
+
+function LightweightThemeConsumer(aDocument) {
+ this._doc = aDocument;
+ Services.obs.addObserver(this, "lightweight-theme-styling-update", false);
+ Services.obs.addObserver(this, "lightweight-theme-apply", false);
+
+ this._update(LightweightThemeManager.currentThemeForDisplay);
+}
+
+LightweightThemeConsumer.prototype = {
+ observe: function (aSubject, aTopic, aData) {
+ if (aTopic == "lightweight-theme-styling-update")
+ this._update(JSON.parse(aData));
+ else if (aTopic == "lightweight-theme-apply")
+ this._update(LightweightThemeManager.currentThemeForDisplay);
+ },
+
+ destroy: function () {
+ Services.obs.removeObserver(this, "lightweight-theme-styling-update");
+ Services.obs.removeObserver(this, "lightweight-theme-apply");
+ this._doc = null;
+ },
+
+ _update: function (aData) {
+ if (!aData)
+ aData = { headerURL: "", footerURL: "", textcolor: "", accentcolor: "" };
+
+ let active = !!aData.headerURL;
+
+ let msg = active ? { type: "LightweightTheme:Update", data: aData } :
+ { type: "LightweightTheme:Disable" };
+ Services.androidBridge.handleGeckoMessage(msg);
+ }
+}
diff --git a/mobile/android/modules/MediaPlayerApp.jsm b/mobile/android/modules/MediaPlayerApp.jsm
new file mode 100644
index 0000000000..949863d1f3
--- /dev/null
+++ b/mobile/android/modules/MediaPlayerApp.jsm
@@ -0,0 +1,166 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["MediaPlayerApp"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+var log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.d.bind(null, "MediaPlayerApp");
+
+// Helper function for sending commands to Java.
+function send(type, data, callback) {
+ let msg = {
+ type: type
+ };
+
+ for (let i in data) {
+ msg[i] = data[i];
+ }
+
+ Messaging.sendRequestForResult(msg)
+ .then(result => callback(result, null),
+ error => callback(null, error));
+}
+
+/* These apps represent players supported natively by the platform. This class will proxy commands
+ * to native controls */
+function MediaPlayerApp(service) {
+ this.service = service;
+ this.location = service.location;
+ this.id = service.uuid;
+}
+
+MediaPlayerApp.prototype = {
+ start: function start(callback) {
+ send("MediaPlayer:Start", { id: this.id }, (result, err) => {
+ if (callback) {
+ callback(err == null);
+ }
+ });
+ },
+
+ stop: function stop(callback) {
+ send("MediaPlayer:Stop", { id: this.id }, (result, err) => {
+ if (callback) {
+ callback(err == null);
+ }
+ });
+ },
+
+ remoteMedia: function remoteMedia(callback, listener) {
+ if (callback) {
+ callback(new RemoteMedia(this.id, listener));
+ }
+ },
+
+ mirror: function mirror(callback) {
+ send("MediaPlayer:Mirror", { id: this.id }, (result, err) => {
+ if (callback) {
+ callback(err == null);
+ }
+ });
+ }
+}
+
+/* RemoteMedia provides a proxy to a native media player session.
+ */
+function RemoteMedia(id, listener) {
+ this._id = id;
+ this._listener = listener;
+
+ if ("onRemoteMediaStart" in this._listener) {
+ Services.tm.mainThread.dispatch((function() {
+ this._listener.onRemoteMediaStart(this);
+ }).bind(this), Ci.nsIThread.DISPATCH_NORMAL);
+ }
+}
+
+RemoteMedia.prototype = {
+ shutdown: function shutdown() {
+ Services.obs.removeObserver(this, "MediaPlayer:Playing");
+ Services.obs.removeObserver(this, "MediaPlayer:Paused");
+
+ this._send("MediaPlayer:End", {}, (result, err) => {
+ this._status = "shutdown";
+ if ("onRemoteMediaStop" in this._listener) {
+ this._listener.onRemoteMediaStop(this);
+ }
+ });
+ },
+
+ play: function play() {
+ this._send("MediaPlayer:Play", {}, (result, err) => {
+ if (err) {
+ Cu.reportError("Can't play " + err);
+ this.shutdown();
+ return;
+ }
+
+ this._status = "started";
+ });
+ },
+
+ pause: function pause() {
+ this._send("MediaPlayer:Pause", {}, (result, err) => {
+ if (err) {
+ Cu.reportError("Can't pause " + err);
+ this.shutdown();
+ return;
+ }
+
+ this._status = "paused";
+ });
+ },
+
+ load: function load(aData) {
+ this._send("MediaPlayer:Load", aData, (result, err) => {
+ if (err) {
+ Cu.reportError("Can't load " + err);
+ this.shutdown();
+ return;
+ }
+
+ Services.obs.addObserver(this, "MediaPlayer:Playing", false);
+ Services.obs.addObserver(this, "MediaPlayer:Paused", false);
+ this._status = "started";
+ })
+ },
+
+ get status() {
+ return this._status;
+ },
+
+ observe: function (aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "MediaPlayer:Playing":
+ if (this._status !== "started") {
+ this._status = "started";
+ if ("onRemoteMediaStatus" in this._listener) {
+ this._listener.onRemoteMediaStatus(this);
+ }
+ }
+ break;
+ case "MediaPlayer:Paused":
+ if (this._status !== "paused") {
+ this._status = "paused";
+ if ("onRemoteMediaStatus" in this._listener) {
+ this._listener.onRemoteMediaStatus(this);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ },
+
+ _send: function(msg, data, callback) {
+ data.id = this._id;
+ send(msg, data, callback);
+ }
+}
diff --git a/mobile/android/modules/Messaging.jsm b/mobile/android/modules/Messaging.jsm
new file mode 100644
index 0000000000..30b7f5a963
--- /dev/null
+++ b/mobile/android/modules/Messaging.jsm
@@ -0,0 +1,183 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict"
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+this.EXPORTED_SYMBOLS = ["sendMessageToJava", "Messaging"];
+
+XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+
+function sendMessageToJava(aMessage, aCallback) {
+ Cu.reportError("sendMessageToJava is deprecated. Use Messaging API instead.");
+
+ if (aCallback) {
+ Messaging.sendRequestForResult(aMessage)
+ .then(result => aCallback(result, null),
+ error => aCallback(null, error));
+ } else {
+ Messaging.sendRequest(aMessage);
+ }
+}
+
+var Messaging = {
+ /**
+ * Add a listener for the given message.
+ *
+ * Only one request listener can be registered for a given message.
+ *
+ * Example usage:
+ * // aData is data sent from Java with the request. The return value is
+ * // used to respond to the request. The return type *must* be an instance
+ * // of Object.
+ * let listener = function (aData) {
+ * if (aData == "foo") {
+ * return { response: "bar" };
+ * }
+ * return {};
+ * };
+ * Messaging.addListener(listener, "Demo:Request");
+ *
+ * The listener may also be a generator function, useful for performing a
+ * task asynchronously. For example:
+ * let listener = function* (aData) {
+ * // Respond with "bar" after 2 seconds.
+ * yield new Promise(resolve => setTimeout(resolve, 2000));
+ * return { response: "bar" };
+ * };
+ * Messaging.addListener(listener, "Demo:Request");
+ *
+ * @param aListener Listener callback taking a single data parameter (see
+ * example usage above).
+ * @param aMessage Event name that this listener should observe.
+ */
+ addListener: function (aListener, aMessage) {
+ requestHandler.addListener(aListener, aMessage);
+ },
+
+ /**
+ * Removes a listener for a given message.
+ *
+ * @param aMessage The event to stop listening for.
+ */
+ removeListener: function (aMessage) {
+ requestHandler.removeListener(aMessage);
+ },
+
+ /**
+ * Sends a request to Java.
+ *
+ * @param aMessage Message to send; must be an object with a "type" property
+ */
+ sendRequest: function (aMessage) {
+ Services.androidBridge.handleGeckoMessage(aMessage);
+ },
+
+ /**
+ * Sends a request to Java, returning a Promise that resolves to the response.
+ *
+ * @param aMessage Message to send; must be an object with a "type" property
+ * @returns A Promise resolving to the response
+ */
+ sendRequestForResult: function (aMessage) {
+ return new Promise((resolve, reject) => {
+ let id = uuidgen.generateUUID().toString();
+ let obs = {
+ observe: function (aSubject, aTopic, aData) {
+ let data = JSON.parse(aData);
+ if (data.__guid__ != id) {
+ return;
+ }
+
+ Services.obs.removeObserver(obs, aMessage.type + ":Response");
+
+ if (data.status === "success") {
+ resolve(data.response);
+ } else {
+ reject(data.response);
+ }
+ }
+ };
+
+ aMessage.__guid__ = id;
+ Services.obs.addObserver(obs, aMessage.type + ":Response", false);
+
+ this.sendRequest(aMessage);
+ });
+ },
+
+ /**
+ * Handles a request from Java, using the given listener method.
+ * This is mainly an internal method used by the RequestHandler object, but can be
+ * used in nsIObserver.observe implmentations that fall outside the normal usage
+ * patterns.
+ *
+ * @param aTopic The string name of the message
+ * @param aData The data sent to the observe method from Java
+ * @param aListener A function that takes a JSON data argument and returns a
+ * response which is sent to Java.
+ */
+ handleRequest: Task.async(function* (aTopic, aData, aListener) {
+ let wrapper = JSON.parse(aData);
+
+ try {
+ let response = yield aListener(wrapper.data);
+ if (typeof response !== "object" || response === null) {
+ throw new Error("Gecko request listener did not return an object");
+ }
+
+ Messaging.sendRequest({
+ type: "Gecko:Request" + wrapper.id,
+ response: response
+ });
+ } catch (e) {
+ Cu.reportError("Error in Messaging handler for " + aTopic + ": " + e);
+
+ Messaging.sendRequest({
+ type: "Gecko:Request" + wrapper.id,
+ error: {
+ message: e.message || (e && e.toString()),
+ stack: e.stack || Components.stack.formattedStack,
+ }
+ });
+ }
+ })
+};
+
+var requestHandler = {
+ _listeners: {},
+
+ addListener: function (aListener, aMessage) {
+ if (aMessage in this._listeners) {
+ throw new Error("Error in addListener: A listener already exists for message " + aMessage);
+ }
+
+ if (typeof aListener !== "function") {
+ throw new Error("Error in addListener: Listener must be a function for message " + aMessage);
+ }
+
+ this._listeners[aMessage] = aListener;
+ Services.obs.addObserver(this, aMessage, false);
+ },
+
+ removeListener: function (aMessage) {
+ if (!(aMessage in this._listeners)) {
+ throw new Error("Error in removeListener: There is no listener for message " + aMessage);
+ }
+
+ delete this._listeners[aMessage];
+ Services.obs.removeObserver(this, aMessage);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ let listener = this._listeners[aTopic];
+ Messaging.handleRequest(aTopic, aData, listener);
+ }
+};
diff --git a/mobile/android/modules/NetErrorHelper.jsm b/mobile/android/modules/NetErrorHelper.jsm
new file mode 100644
index 0000000000..9c74df8fe5
--- /dev/null
+++ b/mobile/android/modules/NetErrorHelper.jsm
@@ -0,0 +1,175 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+Cu.import("resource://gre/modules/UITelemetry.jsm");
+
+this.EXPORTED_SYMBOLS = ["NetErrorHelper"];
+
+const KEY_CODE_ENTER = 13;
+
+/* Handlers is a list of objects that will be notified when an error page is shown
+ * or when an event occurs on the page that they are registered to handle. Registration
+ * is done by just adding yourself to the dictionary.
+ *
+ * handlers.myKey = {
+ * onPageShown: function(browser) { },
+ * handleEvent: function(event) { },
+ * }
+ *
+ * The key that you register yourself with should match the ID of the element you want to
+ * watch for click events on.
+ */
+
+var handlers = {};
+
+function NetErrorHelper(browser) {
+ browser.addEventListener("click", this.handleClick, true);
+
+ let listener = () => {
+ browser.removeEventListener("click", this.handleClick, true);
+ browser.removeEventListener("pagehide", listener, true);
+ };
+ browser.addEventListener("pagehide", listener, true);
+
+ // Handlers may want to customize the page
+ for (let id in handlers) {
+ if (handlers[id].onPageShown) {
+ handlers[id].onPageShown(browser);
+ }
+ }
+}
+
+NetErrorHelper.attachToBrowser = function(browser) {
+ return new NetErrorHelper(browser);
+}
+
+NetErrorHelper.prototype = {
+ handleClick: function(event) {
+ let node = event.target;
+
+ while(node) {
+ if (node.id in handlers && handlers[node.id].handleClick) {
+ handlers[node.id].handleClick(event);
+ return;
+ }
+
+ node = node.parentNode;
+ }
+ },
+}
+
+handlers.searchbutton = {
+ onPageShown: function(browser) {
+ let search = browser.contentDocument.querySelector("#searchbox");
+ if (!search) {
+ return;
+ }
+
+ let browserWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let tab = browserWin.BrowserApp.getTabForBrowser(browser);
+
+ // If there is no stored userRequested, just hide the searchbox
+ if (!tab.userRequested) {
+ search.style.display = "none";
+ } else {
+ let text = browser.contentDocument.querySelector("#searchtext");
+ text.value = tab.userRequested;
+ text.addEventListener("keypress", (event) => {
+ if (event.keyCode === KEY_CODE_ENTER) {
+ this.doSearch(event.target.value);
+ }
+ });
+ }
+ },
+
+ handleClick: function(event) {
+ let value = event.target.previousElementSibling.value;
+ this.doSearch(value);
+ },
+
+ doSearch: function(value) {
+ UITelemetry.addEvent("neterror.1", "button", null, "search");
+ let engine = Services.search.defaultEngine;
+ let uri = engine.getSubmission(value).uri;
+
+ let browserWin = Services.wm.getMostRecentWindow("navigator:browser");
+ // Reset the user search to whatever the new search term was
+ browserWin.BrowserApp.loadURI(uri.spec, undefined, { isSearch: true, userRequested: value });
+ }
+};
+
+handlers.wifi = {
+ // This registers itself with the nsIObserverService as a weak ref,
+ // so we have to implement GetWeakReference as well.
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+
+ GetWeakReference: function() {
+ return Cu.getWeakReference(this);
+ },
+
+ onPageShown: function(browser) {
+ // If we have a connection, don't bother showing the wifi toggle.
+ let network = Cc["@mozilla.org/network/network-link-service;1"].getService(Ci.nsINetworkLinkService);
+ if (network.isLinkUp && network.linkStatusKnown) {
+ let nodes = browser.contentDocument.querySelectorAll("#wifi");
+ for (let i = 0; i < nodes.length; i++) {
+ nodes[i].style.display = "none";
+ }
+ }
+ },
+
+ handleClick: function(event) {
+ let node = event.target;
+ while(node && node.id !== "wifi") {
+ node = node.parentNode;
+ }
+
+ if (!node) {
+ return;
+ }
+
+ UITelemetry.addEvent("neterror.1", "button", null, "wifitoggle");
+ // Show indeterminate progress while we wait for the network.
+ node.disabled = true;
+ node.classList.add("inProgress");
+
+ this.node = Cu.getWeakReference(node);
+ Services.obs.addObserver(this, "network:link-status-changed", true);
+
+ Messaging.sendRequest({
+ type: "Wifi:Enable"
+ });
+ },
+
+ observe: function(subject, topic, data) {
+ let node = this.node.get();
+ if (!node) {
+ return;
+ }
+
+ // Remove the progress bar
+ node.disabled = false;
+ node.classList.remove("inProgress");
+
+ let network = Cc["@mozilla.org/network/network-link-service;1"].getService(Ci.nsINetworkLinkService);
+ if (network.isLinkUp && network.linkStatusKnown) {
+ // If everything worked, reload the page
+ UITelemetry.addEvent("neterror.1", "button", null, "wifitoggle.reload");
+ Services.obs.removeObserver(this, "network:link-status-changed");
+
+ // Even at this point, Android sometimes lies about the real state of the network and this reload request fails.
+ // Add a 500ms delay before refreshing the page.
+ node.ownerDocument.defaultView.setTimeout(function() {
+ node.ownerDocument.location.reload(false);
+ }, 500);
+ }
+ }
+}
+
diff --git a/mobile/android/modules/Notifications.jsm b/mobile/android/modules/Notifications.jsm
new file mode 100644
index 0000000000..a035bb2e35
--- /dev/null
+++ b/mobile/android/modules/Notifications.jsm
@@ -0,0 +1,259 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+this.EXPORTED_SYMBOLS = ["Notifications"];
+
+function log(msg) {
+ // Services.console.logStringMessage(msg);
+}
+
+var _notificationsMap = {};
+var _handlersMap = {};
+
+function Notification(aId, aOptions) {
+ this._id = aId;
+ this._when = (new Date()).getTime();
+ this.fillWithOptions(aOptions);
+}
+
+Notification.prototype = {
+ fillWithOptions: function(aOptions) {
+ if ("icon" in aOptions && aOptions.icon != null)
+ this._icon = aOptions.icon;
+ else
+ throw "Notification icon is mandatory";
+
+ if ("title" in aOptions && aOptions.title != null)
+ this._title = aOptions.title;
+ else
+ throw "Notification title is mandatory";
+
+ if ("message" in aOptions && aOptions.message != null)
+ this._message = aOptions.message;
+ else
+ this._message = null;
+
+ if ("priority" in aOptions && aOptions.priority != null)
+ this._priority = aOptions.priority;
+
+ if ("buttons" in aOptions && aOptions.buttons != null) {
+ if (aOptions.buttons.length > 3)
+ throw "Too many buttons provided. The max number is 3";
+
+ this._buttons = {};
+ for (let i = 0; i < aOptions.buttons.length; i++) {
+ let button_id = aOptions.buttons[i].buttonId;
+ this._buttons[button_id] = aOptions.buttons[i];
+ }
+ } else {
+ this._buttons = null;
+ }
+
+ if ("ongoing" in aOptions && aOptions.ongoing != null)
+ this._ongoing = aOptions.ongoing;
+ else
+ this._ongoing = false;
+
+ if ("progress" in aOptions && aOptions.progress != null)
+ this._progress = aOptions.progress;
+ else
+ this._progress = null;
+
+ if ("onCancel" in aOptions && aOptions.onCancel != null)
+ this._onCancel = aOptions.onCancel;
+ else
+ this._onCancel = null;
+
+ if ("onClick" in aOptions && aOptions.onClick != null)
+ this._onClick = aOptions.onClick;
+ else
+ this._onClick = null;
+
+ if ("cookie" in aOptions && aOptions.cookie != null)
+ this._cookie = aOptions.cookie;
+ else
+ this._cookie = null;
+
+ if ("handlerKey" in aOptions && aOptions.handlerKey != null)
+ this._handlerKey = aOptions.handlerKey;
+
+ if ("persistent" in aOptions && aOptions.persistent != null)
+ this._persistent = aOptions.persistent;
+ else
+ this._persistent = false;
+ },
+
+ show: function() {
+ let msg = {
+ type: "Notification:Show",
+ id: this._id,
+ title: this._title,
+ smallIcon: this._icon,
+ ongoing: this._ongoing,
+ when: this._when,
+ persistent: this._persistent,
+ };
+
+ if (this._message)
+ msg.text = this._message;
+
+ if (this._progress) {
+ msg.progress_value = this._progress;
+ msg.progress_max = 100;
+ msg.progress_indeterminate = false;
+ } else if (Number.isNaN(this._progress)) {
+ msg.progress_value = 0;
+ msg.progress_max = 0;
+ msg.progress_indeterminate = true;
+ }
+
+ if (this._cookie)
+ msg.cookie = JSON.stringify(this._cookie);
+
+ if (this._priority)
+ msg.priority = this._priority;
+
+ if (this._buttons) {
+ msg.actions = [];
+ let buttonName;
+ for (buttonName in this._buttons) {
+ let button = this._buttons[buttonName];
+ let obj = {
+ buttonId: button.buttonId,
+ title : button.title,
+ icon : button.icon
+ };
+ msg.actions.push(obj);
+ }
+ }
+
+ if (this._light)
+ msg.light = this._light;
+
+ if (this._handlerKey)
+ msg.handlerKey = this._handlerKey;
+
+ Services.androidBridge.handleGeckoMessage(msg);
+ return this;
+ },
+
+ cancel: function() {
+ let msg = {
+ type: "Notification:Hide",
+ id: this._id,
+ handlerKey: this._handlerKey,
+ cookie: JSON.stringify(this._cookie),
+ };
+ Services.androidBridge.handleGeckoMessage(msg);
+ }
+}
+
+var Notifications = {
+ get idService() {
+ delete this.idService;
+ return this.idService = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+ },
+
+ registerHandler: function(key, handler) {
+ if (!_handlersMap[key]) {
+ _handlersMap[key] = [];
+ }
+ _handlersMap[key].push(handler);
+ },
+
+ unregisterHandler: function(key, handler) {
+ let h = _handlersMap[key];
+ if (!h) {
+ return;
+ }
+ let i = h.indexOf(handler);
+ if (i > -1) {
+ h.splice(i, 1);
+ }
+ },
+
+ create: function notif_notify(aOptions) {
+ let id = this.idService.generateUUID().toString();
+
+ let notification = new Notification(id, aOptions);
+ _notificationsMap[id] = notification;
+ notification.show();
+
+ return id;
+ },
+
+ update: function notif_update(aId, aOptions) {
+ let notification = _notificationsMap[aId];
+ if (!notification)
+ throw "Unknown notification id";
+ notification.fillWithOptions(aOptions);
+ notification.show();
+ },
+
+ cancel: function notif_cancel(aId) {
+ let notification = _notificationsMap[aId];
+ if (notification)
+ notification.cancel();
+ },
+
+ observe: function notif_observe(aSubject, aTopic, aData) {
+ Services.console.logStringMessage(aTopic + " " + aData);
+
+ let data = JSON.parse(aData);
+ let id = data.id;
+ let handlerKey = data.handlerKey;
+ let cookie = data.cookie ? JSON.parse(data.cookie) : undefined;
+ let notification = _notificationsMap[id];
+
+ switch (data.eventType) {
+ case "notification-clicked":
+ if (notification && notification._onClick)
+ notification._onClick(id, notification._cookie);
+
+ if (handlerKey) {
+ _handlersMap[handlerKey].forEach(function(handler) {
+ handler.onClick(cookie);
+ });
+ }
+
+ break;
+ case "notification-button-clicked":
+ if (handlerKey) {
+ _handlersMap[handlerKey].forEach(function(handler) {
+ handler.onButtonClick(data.buttonId, cookie);
+ });
+ }
+
+ break;
+ case "notification-cleared":
+ case "notification-closed":
+ if (handlerKey) {
+ _handlersMap[handlerKey].forEach(function(handler) {
+ handler.onCancel(cookie);
+ });
+ }
+
+ if (notification && notification._onCancel)
+ notification._onCancel(id, notification._cookie);
+ delete _notificationsMap[id]; // since the notification was dismissed, we no longer need to hold a reference.
+ break;
+ }
+ },
+
+ QueryInterface: function (aIID) {
+ if (!aIID.equals(Ci.nsISupports) &&
+ !aIID.equals(Ci.nsIObserver) &&
+ !aIID.equals(Ci.nsISupportsWeakReference))
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ return this;
+ }
+};
+
+Services.obs.addObserver(Notifications, "Notification:Event", false);
diff --git a/mobile/android/modules/PageActions.jsm b/mobile/android/modules/PageActions.jsm
new file mode 100644
index 0000000000..a66268f826
--- /dev/null
+++ b/mobile/android/modules/PageActions.jsm
@@ -0,0 +1,113 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+
+this.EXPORTED_SYMBOLS = ["PageActions"];
+
+// Copied from browser.js
+// TODO: We should move this method to a common importable location
+function resolveGeckoURI(aURI) {
+ if (!aURI)
+ throw "Can't resolve an empty uri";
+
+ if (aURI.startsWith("chrome://")) {
+ let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]);
+ return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec;
+ } else if (aURI.startsWith("resource://")) {
+ let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
+ return handler.resolveURI(Services.io.newURI(aURI, null, null));
+ }
+ return aURI;
+}
+
+var PageActions = {
+ _items: { },
+
+ _inited: false,
+
+ _maybeInit: function() {
+ if (!this._inited && Object.keys(this._items).length > 0) {
+ this._inited = true;
+ Services.obs.addObserver(this, "PageActions:Clicked", false);
+ Services.obs.addObserver(this, "PageActions:LongClicked", false);
+ }
+ },
+
+ _maybeUninit: function() {
+ if (this._inited && Object.keys(this._items).length == 0) {
+ this._inited = false;
+ Services.obs.removeObserver(this, "PageActions:Clicked");
+ Services.obs.removeObserver(this, "PageActions:LongClicked");
+ }
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ let item = this._items[aData];
+ if (aTopic == "PageActions:Clicked") {
+ if (item.clickCallback) {
+ item.clickCallback();
+ }
+ } else if (aTopic == "PageActions:LongClicked") {
+ if (item.longClickCallback) {
+ item.longClickCallback();
+ }
+ }
+ },
+
+ isShown: function(id) {
+ return !!this._items[id];
+ },
+
+ synthesizeClick: function(id) {
+ let item = this._items[id];
+ if (item && item.clickCallback) {
+ item.clickCallback();
+ }
+ },
+
+ add: function(aOptions) {
+ let id = aOptions.id || uuidgen.generateUUID().toString()
+
+ Messaging.sendRequest({
+ type: "PageActions:Add",
+ id: id,
+ title: aOptions.title,
+ icon: resolveGeckoURI(aOptions.icon),
+ important: "important" in aOptions ? aOptions.important : false
+ });
+
+ this._items[id] = {};
+
+ if (aOptions.clickCallback) {
+ this._items[id].clickCallback = aOptions.clickCallback;
+ }
+
+ if (aOptions.longClickCallback) {
+ this._items[id].longClickCallback = aOptions.longClickCallback;
+ }
+
+ this._maybeInit();
+ return id;
+ },
+
+ remove: function(id) {
+ Messaging.sendRequest({
+ type: "PageActions:Remove",
+ id: id
+ });
+
+ delete this._items[id];
+ this._maybeUninit();
+ }
+}
diff --git a/mobile/android/modules/Prompt.jsm b/mobile/android/modules/Prompt.jsm
new file mode 100644
index 0000000000..5bed876507
--- /dev/null
+++ b/mobile/android/modules/Prompt.jsm
@@ -0,0 +1,234 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict"
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Messaging.jsm");
+
+this.EXPORTED_SYMBOLS = ["Prompt"];
+
+function log(msg) {
+ Services.console.logStringMessage(msg);
+}
+
+function Prompt(aOptions) {
+ this.window = "window" in aOptions ? aOptions.window : null;
+
+ this.msg = { async: true };
+
+ if (this.window) {
+ let window = Services.wm.getMostRecentWindow("navigator:browser");
+ var tab = window.BrowserApp.getTabForWindow(this.window);
+ if (tab) {
+ this.msg.tabId = tab.id;
+ }
+ }
+
+ if (aOptions.priority === 1)
+ this.msg.type = "Prompt:ShowTop"
+ else
+ this.msg.type = "Prompt:Show"
+
+ if ("title" in aOptions && aOptions.title != null)
+ this.msg.title = aOptions.title;
+
+ if ("message" in aOptions && aOptions.message != null)
+ this.msg.text = aOptions.message;
+
+ if ("buttons" in aOptions && aOptions.buttons != null)
+ this.msg.buttons = aOptions.buttons;
+
+ if ("doubleTapButton" in aOptions && aOptions.doubleTapButton != null)
+ this.msg.doubleTapButton = aOptions.doubleTapButton;
+
+ if ("hint" in aOptions && aOptions.hint != null)
+ this.msg.hint = aOptions.hint;
+}
+
+Prompt.prototype = {
+ setHint: function(aHint) {
+ if (!aHint)
+ delete this.msg.hint;
+ else
+ this.msg.hint = aHint;
+ return this;
+ },
+
+ addButton: function(aOptions) {
+ if (!this.msg.buttons)
+ this.msg.buttons = [];
+ this.msg.buttons.push(aOptions.label);
+ return this;
+ },
+
+ _addInput: function(aOptions) {
+ let obj = aOptions;
+ if (this[aOptions.type + "_count"] === undefined)
+ this[aOptions.type + "_count"] = 0;
+
+ obj.id = aOptions.id || (aOptions.type + this[aOptions.type + "_count"]);
+ this[aOptions.type + "_count"]++;
+
+ if (!this.msg.inputs)
+ this.msg.inputs = [];
+ this.msg.inputs.push(obj);
+ return this;
+ },
+
+ addCheckbox: function(aOptions) {
+ return this._addInput({
+ type: "checkbox",
+ label: aOptions.label,
+ checked: aOptions.checked,
+ id: aOptions.id
+ });
+ },
+
+ addTextbox: function(aOptions) {
+ return this._addInput({
+ type: "textbox",
+ value: aOptions.value,
+ hint: aOptions.hint,
+ autofocus: aOptions.autofocus,
+ id: aOptions.id
+ });
+ },
+
+ addNumber: function(aOptions) {
+ return this._addInput({
+ type: "number",
+ value: aOptions.value,
+ hint: aOptions.hint,
+ autofocus: aOptions.autofocus,
+ id: aOptions.id
+ });
+ },
+
+ addPassword: function(aOptions) {
+ return this._addInput({
+ type: "password",
+ value: aOptions.value,
+ hint: aOptions.hint,
+ autofocus: aOptions.autofocus,
+ id : aOptions.id
+ });
+ },
+
+ addDatePicker: function(aOptions) {
+ return this._addInput({
+ type: aOptions.type || "date",
+ value: aOptions.value,
+ id: aOptions.id,
+ max: aOptions.max,
+ min: aOptions.min
+ });
+ },
+
+ addColorPicker: function(aOptions) {
+ return this._addInput({
+ type: "color",
+ value: aOptions.value,
+ id: aOptions.id
+ });
+ },
+
+ addLabel: function(aOptions) {
+ return this._addInput({
+ type: "label",
+ label: aOptions.label,
+ id: aOptions.id
+ });
+ },
+
+ addMenulist: function(aOptions) {
+ return this._addInput({
+ type: "menulist",
+ values: aOptions.values,
+ id: aOptions.id
+ });
+ },
+
+ addIconGrid: function(aOptions) {
+ return this._addInput({
+ type: "icongrid",
+ items: aOptions.items,
+ id: aOptions.id
+ });
+ },
+
+ addTabs: function(aOptions) {
+ return this._addInput({
+ type: "tabs",
+ items: aOptions.items,
+ id: aOptions.id
+ });
+ },
+
+ show: function(callback) {
+ this.callback = callback;
+ log("Sending message");
+ this._innerShow();
+ },
+
+ _innerShow: function() {
+ Messaging.sendRequestForResult(this.msg).then((data) => {
+ if (this.callback)
+ this.callback(data);
+ });
+ },
+
+ _setListItems: function(aItems) {
+ let hasSelected = false;
+ this.msg.listitems = [];
+
+ aItems.forEach(function(item) {
+ let obj = { id: item.id };
+
+ obj.label = item.label;
+
+ if (item.disabled)
+ obj.disabled = true;
+
+ if (item.selected) {
+ if (!this.msg.choiceMode) {
+ this.msg.choiceMode = "single";
+ }
+ obj.selected = item.selected;
+ }
+
+ if (item.header)
+ obj.isGroup = true;
+
+ if (item.menu)
+ obj.isParent = true;
+
+ if (item.child)
+ obj.inGroup = true;
+
+ if (item.showAsActions)
+ obj.showAsActions = item.showAsActions;
+
+ if (item.icon)
+ obj.icon = item.icon;
+
+ this.msg.listitems.push(obj);
+
+ }, this);
+ return this;
+ },
+
+ setSingleChoiceItems: function(aItems) {
+ return this._setListItems(aItems);
+ },
+
+ setMultiChoiceItems: function(aItems) {
+ this.msg.choiceMode = "multiple";
+ return this._setListItems(aItems);
+ },
+
+}
diff --git a/mobile/android/modules/RuntimePermissions.jsm b/mobile/android/modules/RuntimePermissions.jsm
new file mode 100644
index 0000000000..42d8024b1a
--- /dev/null
+++ b/mobile/android/modules/RuntimePermissions.jsm
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+this.EXPORTED_SYMBOLS = ["RuntimePermissions"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
+
+// See: http://developer.android.com/reference/android/Manifest.permission.html
+const CAMERA = "android.permission.CAMERA";
+const WRITE_EXTERNAL_STORAGE = "android.permission.WRITE_EXTERNAL_STORAGE";
+const RECORD_AUDIO = "android.permission.RECORD_AUDIO";
+
+var RuntimePermissions = {
+ CAMERA: CAMERA,
+ RECORD_AUDIO: RECORD_AUDIO,
+ WRITE_EXTERNAL_STORAGE: WRITE_EXTERNAL_STORAGE,
+
+ /**
+ * Check whether the permissions have been granted or not. If needed prompt the user to accept the permissions.
+ *
+ * @returns A promise resolving to true if all the permissions have been granted or false if any of the
+ * permissions have been denied.
+ */
+ waitForPermissions: function(permission) {
+ let permissions = [].concat(permission);
+
+ let msg = {
+ type: 'RuntimePermissions:Prompt',
+ permissions: permissions
+ };
+
+ return Messaging.sendRequestForResult(msg);
+ }
+}; \ No newline at end of file
diff --git a/mobile/android/modules/SSLExceptions.jsm b/mobile/android/modules/SSLExceptions.jsm
new file mode 100644
index 0000000000..48dfe8d92d
--- /dev/null
+++ b/mobile/android/modules/SSLExceptions.jsm
@@ -0,0 +1,118 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict"
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+this.EXPORTED_SYMBOLS = ["SSLExceptions"];
+
+/**
+ A class to add exceptions to override SSL certificate problems. The functionality
+ itself is borrowed from exceptionDialog.js.
+*/
+function SSLExceptions() {
+ this._overrideService = Cc["@mozilla.org/security/certoverride;1"]
+ .getService(Ci.nsICertOverrideService);
+}
+
+
+SSLExceptions.prototype = {
+ _overrideService: null,
+ _sslStatus: null,
+
+ getInterface: function SSLE_getInterface(aIID) {
+ return this.QueryInterface(aIID);
+ },
+ QueryInterface: function SSLE_QueryInterface(aIID) {
+ if (aIID.equals(Ci.nsIBadCertListener2) ||
+ aIID.equals(Ci.nsISupports))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ },
+
+ /**
+ To collect the SSL status we intercept the certificate error here
+ and store the status for later use.
+ */
+ notifyCertProblem: function SSLE_notifyCertProblem(socketInfo, sslStatus, targetHost) {
+ this._sslStatus = sslStatus.QueryInterface(Ci.nsISSLStatus);
+ return true; // suppress error UI
+ },
+
+ /**
+ Attempt to download the certificate for the location specified to get the SSLState
+ for the certificate and the errors.
+ */
+ _checkCert: function SSLE_checkCert(aURI) {
+ this._sslStatus = null;
+
+ let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
+ try {
+ if (aURI) {
+ req.open("GET", aURI.prePath, false);
+ req.channel.notificationCallbacks = this;
+ req.send(null);
+ }
+ } catch (e) {
+ // We *expect* exceptions if there are problems with the certificate
+ // presented by the site. Log it, just in case, but we can proceed here,
+ // with appropriate sanity checks
+ Components.utils.reportError("Attempted to connect to a site with a bad certificate in the add exception dialog. " +
+ "This results in a (mostly harmless) exception being thrown. " +
+ "Logged for information purposes only: " + e);
+ }
+
+ return this._sslStatus;
+ },
+
+ /**
+ Internal method to create an override.
+ */
+ _addOverride: function SSLE_addOverride(aURI, aWindow, aTemporary) {
+ let SSLStatus = this._checkCert(aURI);
+ let certificate = SSLStatus.serverCert;
+
+ let flags = 0;
+
+ // in private browsing do not store exceptions permanently ever
+ if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
+ aTemporary = true;
+ }
+
+ if (SSLStatus.isUntrusted)
+ flags |= this._overrideService.ERROR_UNTRUSTED;
+ if (SSLStatus.isDomainMismatch)
+ flags |= this._overrideService.ERROR_MISMATCH;
+ if (SSLStatus.isNotValidAtThisTime)
+ flags |= this._overrideService.ERROR_TIME;
+
+ this._overrideService.rememberValidityOverride(
+ aURI.asciiHost,
+ aURI.port,
+ certificate,
+ flags,
+ aTemporary);
+ },
+
+ /**
+ Creates a permanent exception to override all overridable errors for
+ the given URL.
+ */
+ addPermanentException: function SSLE_addPermanentException(aURI, aWindow) {
+ this._addOverride(aURI, aWindow, false);
+ },
+
+ /**
+ Creates a temporary exception to override all overridable errors for
+ the given URL.
+ */
+ addTemporaryException: function SSLE_addTemporaryException(aURI, aWindow) {
+ this._addOverride(aURI, aWindow, true);
+ }
+};
diff --git a/mobile/android/modules/Sanitizer.jsm b/mobile/android/modules/Sanitizer.jsm
new file mode 100644
index 0000000000..014a896883
--- /dev/null
+++ b/mobile/android/modules/Sanitizer.jsm
@@ -0,0 +1,303 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 4 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*globals LoadContextInfo, FormHistory, Accounts */
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/LoadContextInfo.jsm");
+Cu.import("resource://gre/modules/FormHistory.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Downloads.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Accounts.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadIntegration",
+ "resource://gre/modules/DownloadIntegration.jsm");
+
+function dump(a) {
+ Services.console.logStringMessage(a);
+}
+
+this.EXPORTED_SYMBOLS = ["Sanitizer"];
+
+function Sanitizer() {}
+Sanitizer.prototype = {
+ clearItem: function (aItemName)
+ {
+ let item = this.items[aItemName];
+ let canClear = item.canClear;
+ if (typeof canClear == "function") {
+ canClear(function clearCallback(aCanClear) {
+ if (aCanClear)
+ item.clear();
+ });
+ } else if (canClear) {
+ item.clear();
+ }
+ },
+
+ items: {
+ cache: {
+ clear: function ()
+ {
+ return new Promise(function(resolve, reject) {
+ var cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"].getService(Ci.nsICacheStorageService);
+ try {
+ cache.clear();
+ } catch(er) {}
+
+ let imageCache = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools)
+ .getImgCacheForDocument(null);
+ try {
+ imageCache.clearCache(false); // true=chrome, false=content
+ } catch(er) {}
+
+ resolve();
+ });
+ },
+
+ get canClear()
+ {
+ return true;
+ }
+ },
+
+ cookies: {
+ clear: function ()
+ {
+ return new Promise(function(resolve, reject) {
+ Services.cookies.removeAll();
+ resolve();
+ });
+ },
+
+ get canClear()
+ {
+ return true;
+ }
+ },
+
+ siteSettings: {
+ clear: Task.async(function* () {
+ // Clear site-specific permissions like "Allow this site to open popups"
+ Services.perms.removeAll();
+
+ // Clear site-specific settings like page-zoom level
+ Cc["@mozilla.org/content-pref/service;1"]
+ .getService(Ci.nsIContentPrefService2)
+ .removeAllDomains(null);
+
+ // Clear site security settings
+ var sss = Cc["@mozilla.org/ssservice;1"]
+ .getService(Ci.nsISiteSecurityService);
+ sss.clearAll();
+
+ // Clear push subscriptions
+ yield new Promise((resolve, reject) => {
+ let push = Cc["@mozilla.org/push/Service;1"]
+ .getService(Ci.nsIPushService);
+ push.clearForDomain("*", status => {
+ if (Components.isSuccessCode(status)) {
+ resolve();
+ } else {
+ reject(new Error("Error clearing push subscriptions: " +
+ status));
+ }
+ });
+ });
+ }),
+
+ get canClear()
+ {
+ return true;
+ }
+ },
+
+ offlineApps: {
+ clear: function ()
+ {
+ return new Promise(function(resolve, reject) {
+ var cacheService = Cc["@mozilla.org/netwerk/cache-storage-service;1"].getService(Ci.nsICacheStorageService);
+ var appCacheStorage = cacheService.appCacheStorage(LoadContextInfo.default, null);
+ try {
+ appCacheStorage.asyncEvictStorage(null);
+ } catch(er) {}
+
+ resolve();
+ });
+ },
+
+ get canClear()
+ {
+ return true;
+ }
+ },
+
+ history: {
+ clear: function ()
+ {
+ return Messaging.sendRequestForResult({ type: "Sanitize:ClearHistory" })
+ .catch(e => Cu.reportError("Java-side history clearing failed: " + e))
+ .then(function() {
+ try {
+ Services.obs.notifyObservers(null, "browser:purge-session-history", "");
+ }
+ catch (e) { }
+
+ try {
+ var predictor = Cc["@mozilla.org/network/predictor;1"].getService(Ci.nsINetworkPredictor);
+ predictor.reset();
+ } catch (e) { }
+ });
+ },
+
+ get canClear()
+ {
+ // bug 347231: Always allow clearing history due to dependencies on
+ // the browser:purge-session-history notification. (like error console)
+ return true;
+ }
+ },
+
+ searchHistory: {
+ clear: function ()
+ {
+ return Messaging.sendRequestForResult({ type: "Sanitize:ClearHistory", clearSearchHistory: true })
+ .catch(e => Cu.reportError("Java-side search history clearing failed: " + e))
+ },
+
+ get canClear()
+ {
+ return true;
+ }
+ },
+
+ formdata: {
+ clear: function ()
+ {
+ return new Promise(function(resolve, reject) {
+ FormHistory.update({ op: "remove" });
+ resolve();
+ });
+ },
+
+ canClear: function (aCallback)
+ {
+ let count = 0;
+ let countDone = {
+ handleResult: function(aResult) { count = aResult; },
+ handleError: function(aError) { Cu.reportError(aError); },
+ handleCompletion: function(aReason) { aCallback(aReason == 0 && count > 0); }
+ };
+ FormHistory.count({}, countDone);
+ }
+ },
+
+ downloadFiles: {
+ clear: Task.async(function* () {
+ let list = yield Downloads.getList(Downloads.ALL);
+ let downloads = yield list.getAll();
+ var finalizePromises = [];
+
+ // Logic copied from DownloadList.removeFinished. Ideally, we would
+ // just use that method directly, but we want to be able to remove the
+ // downloaded files as well.
+ for (let download of downloads) {
+ // Remove downloads that have been canceled, even if the cancellation
+ // operation hasn't completed yet so we don't check "stopped" here.
+ // Failed downloads with partial data are also removed.
+ if (download.stopped && (!download.hasPartialData || download.error)) {
+ // Remove the download first, so that the views don't get the change
+ // notifications that may occur during finalization.
+ yield list.remove(download);
+ // Ensure that the download is stopped and no partial data is kept.
+ // This works even if the download state has changed meanwhile. We
+ // don't need to wait for the procedure to be complete before
+ // processing the other downloads in the list.
+ finalizePromises.push(download.finalize(true).then(() => null, Cu.reportError));
+
+ // Delete the downloaded files themselves.
+ OS.File.remove(download.target.path).then(() => null, ex => {
+ if (!(ex instanceof OS.File.Error && ex.becauseNoSuchFile)) {
+ Cu.reportError(ex);
+ }
+ });
+ }
+ }
+
+ yield Promise.all(finalizePromises);
+ yield DownloadIntegration.forceSave();
+ }),
+
+ get canClear()
+ {
+ return true;
+ }
+ },
+
+ passwords: {
+ clear: function ()
+ {
+ return new Promise(function(resolve, reject) {
+ Services.logins.removeAllLogins();
+ resolve();
+ });
+ },
+
+ get canClear()
+ {
+ let count = Services.logins.countLogins("", "", ""); // count all logins
+ return (count > 0);
+ }
+ },
+
+ sessions: {
+ clear: function ()
+ {
+ return new Promise(function(resolve, reject) {
+ // clear all auth tokens
+ var sdr = Cc["@mozilla.org/security/sdr;1"].getService(Ci.nsISecretDecoderRing);
+ sdr.logoutAndTeardown();
+
+ // clear FTP and plain HTTP auth sessions
+ Services.obs.notifyObservers(null, "net:clear-active-logins", null);
+
+ resolve();
+ });
+ },
+
+ get canClear()
+ {
+ return true;
+ }
+ },
+
+ syncedTabs: {
+ clear: function ()
+ {
+ return Messaging.sendRequestForResult({ type: "Sanitize:ClearSyncedTabs" })
+ .catch(e => Cu.reportError("Java-side synced tabs clearing failed: " + e));
+ },
+
+ canClear: function(aCallback)
+ {
+ Accounts.anySyncAccountsExist().then(aCallback)
+ .catch(function(err) {
+ Cu.reportError("Java-side synced tabs clearing failed: " + err)
+ aCallback(false);
+ });
+ }
+ }
+
+ }
+};
+
+this.Sanitizer = new Sanitizer();
diff --git a/mobile/android/modules/SharedPreferences.jsm b/mobile/android/modules/SharedPreferences.jsm
new file mode 100644
index 0000000000..3f32df6ea5
--- /dev/null
+++ b/mobile/android/modules/SharedPreferences.jsm
@@ -0,0 +1,254 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["SharedPreferences"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+// For adding observers.
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+
+var Scope = Object.freeze({
+ APP: "app",
+ PROFILE: "profile",
+ GLOBAL: "global"
+});
+
+/**
+ * Public API to getting a SharedPreferencesImpl instance. These scopes mirror GeckoSharedPrefs.
+ */
+var SharedPreferences = {
+ forApp: function() {
+ return new SharedPreferencesImpl({ scope: Scope.APP });
+ },
+
+ forProfile: function() {
+ return new SharedPreferencesImpl({ scope: Scope.PROFILE });
+ },
+
+ /**
+ * Get SharedPreferences for the named profile; if the profile name is null,
+ * returns the preferences for the current profile (just like |forProfile|).
+ */
+ forProfileName: function(profileName) {
+ return new SharedPreferencesImpl({ scope: Scope.PROFILE, profileName: profileName });
+ },
+
+ /**
+ * Get SharedPreferences for the given Android branch; if the branch is null,
+ * returns the default preferences branch for the application, which is the
+ * output of |PreferenceManager.getDefaultSharedPreferences|.
+ */
+ forAndroid: function(branch) {
+ return new SharedPreferencesImpl({ scope: Scope.GLOBAL, branch: branch });
+ }
+};
+
+/**
+ * Create an interface to an Android SharedPreferences branch.
+ *
+ * options {Object} with the following valid keys:
+ * - scope {String} (required) specifies the scope of preferences that should be accessed.
+ * - branch {String} (only when using Scope.GLOBAL) should be a string describing a preferences branch,
+ * like "UpdateService" or "background.data", or null to access the
+ * default preferences branch for the application.
+ * - profileName {String} (optional, only valid when using Scope.PROFILE)
+ */
+function SharedPreferencesImpl(options = {}) {
+ if (!(this instanceof SharedPreferencesImpl)) {
+ return new SharedPreferencesImpl(options);
+ }
+
+ if (options.scope == null || options.scope == undefined) {
+ throw "Shared Preferences must specifiy a scope.";
+ }
+
+ this._scope = options.scope;
+ this._profileName = options.profileName;
+ this._branch = options.branch;
+ this._observers = {};
+}
+
+SharedPreferencesImpl.prototype = Object.freeze({
+ _set: function _set(prefs) {
+ Messaging.sendRequest({
+ type: "SharedPreferences:Set",
+ preferences: prefs,
+ scope: this._scope,
+ profileName: this._profileName,
+ branch: this._branch,
+ });
+ },
+
+ _setOne: function _setOne(prefName, value, type) {
+ let prefs = [];
+ prefs.push({
+ name: prefName,
+ value: value,
+ type: type,
+ });
+ this._set(prefs);
+ },
+
+ setBoolPref: function setBoolPref(prefName, value) {
+ this._setOne(prefName, value, "bool");
+ },
+
+ setCharPref: function setCharPref(prefName, value) {
+ this._setOne(prefName, value, "string");
+ },
+
+ setIntPref: function setIntPref(prefName, value) {
+ this._setOne(prefName, value, "int");
+ },
+
+ _get: function _get(prefs, callback) {
+ let result = null;
+ Messaging.sendRequestForResult({
+ type: "SharedPreferences:Get",
+ preferences: prefs,
+ scope: this._scope,
+ profileName: this._profileName,
+ branch: this._branch,
+ }).then((data) => {
+ result = data.values;
+ });
+
+ let thread = Services.tm.currentThread;
+ while (result == null)
+ thread.processNextEvent(true);
+
+ return result;
+ },
+
+ _getOne: function _getOne(prefName, type) {
+ let prefs = [];
+ prefs.push({
+ name: prefName,
+ type: type,
+ });
+ let values = this._get(prefs);
+ if (values.length != 1) {
+ throw new Error("Got too many values: " + values.length);
+ }
+ return values[0].value;
+ },
+
+ getBoolPref: function getBoolPref(prefName) {
+ return this._getOne(prefName, "bool");
+ },
+
+ getCharPref: function getCharPref(prefName) {
+ return this._getOne(prefName, "string");
+ },
+
+ getIntPref: function getIntPref(prefName) {
+ return this._getOne(prefName, "int");
+ },
+
+ /**
+ * Invoke `observer` after a change to the preference `domain` in
+ * the current branch.
+ *
+ * `observer` should implement the nsIObserver.observe interface.
+ */
+ addObserver: function addObserver(domain, observer, holdWeak) {
+ if (!domain)
+ throw new Error("domain must not be null");
+ if (!observer)
+ throw new Error("observer must not be null");
+ if (holdWeak)
+ throw new Error("Weak references not yet implemented.");
+
+ if (!this._observers.hasOwnProperty(domain))
+ this._observers[domain] = [];
+ if (this._observers[domain].indexOf(observer) > -1)
+ return;
+
+ this._observers[domain].push(observer);
+
+ this._updateAndroidListener();
+ },
+
+ /**
+ * Do not invoke `observer` after a change to the preference
+ * `domain` in the current branch.
+ */
+ removeObserver: function removeObserver(domain, observer) {
+ if (!this._observers.hasOwnProperty(domain))
+ return;
+ let index = this._observers[domain].indexOf(observer);
+ if (index < 0)
+ return;
+
+ this._observers[domain].splice(index, 1);
+ if (this._observers[domain].length < 1)
+ delete this._observers[domain];
+
+ this._updateAndroidListener();
+ },
+
+ _updateAndroidListener: function _updateAndroidListener() {
+ if (this._listening && Object.keys(this._observers).length < 1)
+ this._uninstallAndroidListener();
+ if (!this._listening && Object.keys(this._observers).length > 0)
+ this._installAndroidListener();
+ },
+
+ _installAndroidListener: function _installAndroidListener() {
+ if (this._listening)
+ return;
+ this._listening = true;
+
+ Services.obs.addObserver(this, "SharedPreferences:Changed", false);
+ Messaging.sendRequest({
+ type: "SharedPreferences:Observe",
+ enable: true,
+ scope: this._scope,
+ profileName: this._profileName,
+ branch: this._branch,
+ });
+ },
+
+ observe: function observe(subject, topic, data) {
+ if (topic != "SharedPreferences:Changed") {
+ return;
+ }
+
+ let msg = JSON.parse(data);
+ if (msg.scope !== this._scope ||
+ ((this._scope === Scope.PROFILE) && (msg.profileName !== this._profileName)) ||
+ ((this._scope === Scope.GLOBAL) && (msg.branch !== this._branch))) {
+ return;
+ }
+
+ if (!this._observers.hasOwnProperty(msg.key)) {
+ return;
+ }
+
+ let observers = this._observers[msg.key];
+ for (let obs of observers) {
+ obs.observe(obs, msg.key, msg.value);
+ }
+ },
+
+ _uninstallAndroidListener: function _uninstallAndroidListener() {
+ if (!this._listening)
+ return;
+ this._listening = false;
+
+ Services.obs.removeObserver(this, "SharedPreferences:Changed");
+ Messaging.sendRequest({
+ type: "SharedPreferences:Observe",
+ enable: false,
+ scope: this._scope,
+ profileName: this._profileName,
+ branch: this._branch,
+ });
+ },
+});
diff --git a/mobile/android/modules/Snackbars.jsm b/mobile/android/modules/Snackbars.jsm
new file mode 100644
index 0000000000..066a28c567
--- /dev/null
+++ b/mobile/android/modules/Snackbars.jsm
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+this.EXPORTED_SYMBOLS = ["Snackbars"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
+
+const LENGTH_INDEFINITE = -2;
+const LENGTH_LONG = 0;
+const LENGTH_SHORT = -1;
+
+var Snackbars = {
+ LENGTH_INDEFINITE: LENGTH_INDEFINITE,
+ LENGTH_LONG: LENGTH_LONG,
+ LENGTH_SHORT: LENGTH_SHORT,
+
+ show: function(aMessage, aDuration, aOptions) {
+
+ // Takes care of the deprecated toast calls
+ if (typeof aDuration === "string") {
+ [aDuration, aOptions] = migrateToastIfNeeded(aDuration, aOptions);
+ }
+
+ let msg = {
+ type: 'Snackbar:Show',
+ message: aMessage,
+ duration: aDuration,
+ };
+
+ if (aOptions && aOptions.backgroundColor) {
+ msg.backgroundColor = aOptions.backgroundColor;
+ }
+
+ if (aOptions && aOptions.action) {
+ msg.action = {};
+
+ if (aOptions.action.label) {
+ msg.action.label = aOptions.action.label;
+ }
+
+ Messaging.sendRequestForResult(msg).then(result => aOptions.action.callback());
+ } else {
+ Messaging.sendRequest(msg);
+ }
+ }
+};
+
+function migrateToastIfNeeded(aDuration, aOptions) {
+ let duration;
+ if (aDuration === "long") {
+ duration = LENGTH_LONG;
+ }
+ else {
+ duration = LENGTH_SHORT;
+ }
+
+ let options = {};
+ if (aOptions && aOptions.button) {
+ options.action = {
+ label: aOptions.button.label,
+ callback: () => aOptions.button.callback(),
+ };
+ }
+ return [duration, options];
+} \ No newline at end of file
diff --git a/mobile/android/modules/TabMirror.jsm b/mobile/android/modules/TabMirror.jsm
new file mode 100644
index 0000000000..72a640ec8f
--- /dev/null
+++ b/mobile/android/modules/TabMirror.jsm
@@ -0,0 +1,153 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+
+const CONFIG = { iceServers: [{ "urls": ["stun:stun.services.mozilla.com"] }] };
+
+var log = Cu.import("resource://gre/modules/AndroidLog.jsm",
+ {}).AndroidLog.d.bind(null, "TabMirror");
+
+var failure = function(x) {
+ log("ERROR: " + JSON.stringify(x));
+};
+
+var TabMirror = function(deviceId, window) {
+
+ this.deviceId = deviceId;
+ // Save RTCSessionDescription and RTCIceCandidate for later when the window object is not available.
+ this.RTCSessionDescription = window.RTCSessionDescription;
+ this.RTCIceCandidate = window.RTCIceCandidate;
+
+ Services.obs.addObserver((aSubject, aTopic, aData) => this._processMessage(aData), "MediaPlayer:Response", false);
+ this._sendMessage({ start: true });
+ this._window = window;
+ this._pc = new window.RTCPeerConnection(CONFIG, {});
+ if (!this._pc) {
+ throw "Failure creating Webrtc object";
+ }
+
+};
+
+TabMirror.prototype = {
+ _window: null,
+ _screenSize: { width: 1280, height: 720 },
+ _pc: null,
+ _start: function() {
+ this._pc.onicecandidate = this._onIceCandidate.bind(this);
+
+ let windowId = this._window.BrowserApp.selectedBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
+ let constraints = {
+ video: {
+ mediaSource: "browser",
+ browserWindow: windowId,
+ scrollWithPage: true,
+ advanced: [
+ { width: { min: 0, max: this._screenSize.width },
+ height: { min: 0, max: this._screenSize.height }
+ },
+ { aspectRatio: this._screenSize.width / this._screenSize.height }
+ ]
+ }
+ };
+
+ this._window.navigator.mozGetUserMedia(constraints, this._onGumSuccess.bind(this), this._onGumFailure.bind(this));
+ },
+
+ _processMessage: function(data) {
+ if (!data) {
+ return;
+ }
+
+ let msg = JSON.parse(data);
+
+ if (!msg) {
+ return;
+ }
+
+ if (msg.sdp && msg.type === "answer") {
+ this._processAnswer(msg);
+ } else if (msg.type == "size") {
+ if (msg.height) {
+ this._screenSize.height = msg.height;
+ }
+ if (msg.width) {
+ this._screenSize.width = msg.width;
+ }
+ this._start();
+ } else if (msg.candidate) {
+ this._processIceCandidate(msg);
+ } else {
+ log("dropping unrecognized message: " + JSON.stringify(msg));
+ }
+ },
+
+ // Signaling methods
+ _processAnswer: function(msg) {
+ this._pc.setRemoteDescription(new this.RTCSessionDescription(msg),
+ this._setRemoteAnswerSuccess.bind(this), failure);
+ },
+
+ _processIceCandidate: function(msg) {
+ // WebRTC generates a warning if the success and fail callbacks are not passed in.
+ this._pc.addIceCandidate(new this.RTCIceCandidate(msg), () => log("Ice Candiated added successfuly"), () => log("Failed to add Ice Candidate"));
+ },
+
+ _setRemoteAnswerSuccess: function() {
+ },
+
+ _setLocalSuccessOffer: function(sdp) {
+ this._sendMessage(sdp);
+ },
+
+ _createOfferSuccess: function(sdp) {
+ this._pc.setLocalDescription(sdp, () => this._setLocalSuccessOffer(sdp), failure);
+ },
+
+ _onIceCandidate: function (msg) {
+ log("NEW Ice Candidate: " + JSON.stringify(msg.candidate));
+ this._sendMessage(msg.candidate);
+ },
+
+ _ready: function() {
+ this._pc.createOffer(this._createOfferSuccess.bind(this), failure);
+ },
+
+ _onGumSuccess: function(stream){
+ this._pc.addStream(stream);
+ this._ready();
+ },
+
+ _onGumFailure: function() {
+ log("Could not get video stream");
+ this._pc.close();
+ },
+
+ _sendMessage: function(msg) {
+ if (this.deviceId) {
+ let obj = {
+ type: "MediaPlayer:Message",
+ id: this.deviceId,
+ data: JSON.stringify(msg)
+ };
+ Messaging.sendRequest(obj);
+ }
+ },
+
+ stop: function() {
+ if (this.deviceId) {
+ let obj = {
+ type: "MediaPlayer:End",
+ id: this.deviceId
+ };
+ Services.androidBridge.handleGeckoMessage(obj);
+ }
+ },
+};
+
+
+this.EXPORTED_SYMBOLS = ["TabMirror"];
diff --git a/mobile/android/modules/WebsiteMetadata.jsm b/mobile/android/modules/WebsiteMetadata.jsm
new file mode 100644
index 0000000000..39af9ddebe
--- /dev/null
+++ b/mobile/android/modules/WebsiteMetadata.jsm
@@ -0,0 +1,475 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+'use strict';
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+this.EXPORTED_SYMBOLS = ["WebsiteMetadata"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
+
+var WebsiteMetadata = {
+ /**
+ * Asynchronously parse the document extract metadata. A 'Website:Metadata' event with the metadata
+ * will be sent.
+ */
+ parseAsynchronously: function(doc) {
+ Task.spawn(function() {
+ let metadata = getMetadata(doc, doc.location.href, {
+ image_url: metadataRules['image_url']
+ });
+
+ // No metadata was extracted, so don't bother sending it.
+ if (Object.keys(metadata).length === 0) {
+ return;
+ }
+
+ let msg = {
+ type: 'Website:Metadata',
+ location: doc.location.href,
+ metadata: metadata,
+ };
+
+ Messaging.sendRequest(msg);
+ });
+ }
+};
+
+// #################################################################################################
+// # Modified version of makeUrlAbsolute() to not import url parser library (and dependencies)
+// #################################################################################################
+
+function makeUrlAbsolute(context, relative) {
+ var a = context.doc.createElement('a');
+ a.href = relative;
+ return a.href;
+}
+
+// #################################################################################################
+// # page-metadata-parser
+// # https://github.com/mozilla/page-metadata-parser/
+// # 61c58cbd0f0bf2153df832a388a79c66b288b98c
+// #################################################################################################
+
+function buildRuleset(name, rules, processors) {
+ const reversedRules = Array.from(rules).reverse();
+ const builtRuleset = ruleset(...reversedRules.map(([query, handler], order) => rule(
+ dom(query),
+ node => [{
+ score: order,
+ flavor: name,
+ notes: handler(node),
+ }]
+ )));
+
+ return (doc, context) => {
+ const kb = builtRuleset.score(doc);
+ const maxNode = kb.max(name);
+
+ if (maxNode) {
+ let value = maxNode.flavors.get(name);
+
+ if (processors) {
+ processors.forEach(processor => {
+ value = processor(value, context);
+ });
+ }
+
+ if (value) {
+ if (value.trim) {
+ return value.trim();
+ }
+ return value;
+ }
+ }
+ };
+}
+
+const metadataRules = {
+ description: {
+ rules: [
+ ['meta[property="og:description"]', node => node.element.getAttribute('content')],
+ ['meta[name="description"]', node => node.element.getAttribute('content')],
+ ],
+ },
+
+ icon_url: {
+ rules: [
+ ['link[rel="apple-touch-icon"]', node => node.element.getAttribute('href')],
+ ['link[rel="apple-touch-icon-precomposed"]', node => node.element.getAttribute('href')],
+ ['link[rel="icon"]', node => node.element.getAttribute('href')],
+ ['link[rel="fluid-icon"]', node => node.element.getAttribute('href')],
+ ['link[rel="shortcut icon"]', node => node.element.getAttribute('href')],
+ ['link[rel="Shortcut Icon"]', node => node.element.getAttribute('href')],
+ ['link[rel="mask-icon"]', node => node.element.getAttribute('href')],
+ ],
+ processors: [
+ (icon_url, context) => makeUrlAbsolute(context, icon_url)
+ ]
+ },
+
+ image_url: {
+ rules: [
+ ['meta[property="og:image:secure_url"]', node => node.element.getAttribute('content')],
+ ['meta[property="og:image:url"]', node => node.element.getAttribute('content')],
+ ['meta[property="og:image"]', node => node.element.getAttribute('content')],
+ ['meta[property="twitter:image"]', node => node.element.getAttribute('content')],
+ ['meta[name="thumbnail"]', node => node.element.getAttribute('content')],
+ ],
+ processors: [
+ (image_url, context) => makeUrlAbsolute(context, image_url)
+ ],
+ },
+
+ keywords: {
+ rules: [
+ ['meta[name="keywords"]', node => node.element.getAttribute('content')],
+ ],
+ processors: [
+ (keywords) => keywords.split(',').map((keyword) => keyword.trim()),
+ ]
+ },
+
+ title: {
+ rules: [
+ ['meta[property="og:title"]', node => node.element.getAttribute('content')],
+ ['meta[property="twitter:title"]', node => node.element.getAttribute('content')],
+ ['meta[name="hdl"]', node => node.element.getAttribute('content')],
+ ['title', node => node.element.text],
+ ],
+ },
+
+ type: {
+ rules: [
+ ['meta[property="og:type"]', node => node.element.getAttribute('content')],
+ ],
+ },
+
+ url: {
+ rules: [
+ ['meta[property="og:url"]', node => node.element.getAttribute('content')],
+ ['link[rel="canonical"]', node => node.element.getAttribute('href')],
+ ],
+ },
+};
+
+function getMetadata(doc, url, rules) {
+ const metadata = {};
+ const context = {url,doc};
+ const ruleSet = rules || metadataRules;
+
+ Object.keys(ruleSet).map(metadataKey => {
+ const metadataRule = ruleSet[metadataKey];
+
+ if(Array.isArray(metadataRule.rules)) {
+ const builtRule = buildRuleset(metadataKey, metadataRule.rules, metadataRule.processors);
+ metadata[metadataKey] = builtRule(doc, context);
+ } else {
+ metadata[metadataKey] = getMetadata(doc, url, metadataRule);
+ }
+ });
+
+ return metadata;
+}
+
+// #################################################################################################
+// # Fathom dependencies resolved
+// #################################################################################################
+
+// const {forEach} = require('wu');
+function forEach(fn, obj) {
+ for (let x of obj) {
+ fn(x);
+ }
+}
+
+function best(iterable, by, isBetter) {
+ let bestSoFar, bestKeySoFar;
+ let isFirst = true;
+ forEach(
+ function (item) {
+ const key = by(item);
+ if (isBetter(key, bestKeySoFar) || isFirst) {
+ bestSoFar = item;
+ bestKeySoFar = key;
+ isFirst = false;
+ }
+ },
+ iterable);
+ if (isFirst) {
+ throw new Error('Tried to call best() on empty iterable');
+ }
+ return bestSoFar;
+}
+
+// const {max} = require('./utils');
+function max(iterable, by = identity) {
+ return best(iterable, by, (a, b) => a > b);
+}
+
+// #################################################################################################
+// # Fathom
+// # https://github.com/mozilla/fathom
+// # cac59e470816f17fc1efd4a34437b585e3e451cd
+// #################################################################################################
+
+// Get a key of a map, first setting it to a default value if it's missing.
+function getDefault(map, key, defaultMaker) {
+ if (map.has(key)) {
+ return map.get(key);
+ }
+ const defaultValue = defaultMaker();
+ map.set(key, defaultValue);
+ return defaultValue;
+}
+
+
+// Construct a filtration network of rules.
+function ruleset(...rules) {
+ const rulesByInputFlavor = new Map(); // [someInputFlavor: [rule, ...]]
+
+ // File each rule under its input flavor:
+ forEach(rule => getDefault(rulesByInputFlavor, rule.source.inputFlavor, () => []).push(rule),
+ rules);
+
+ return {
+ // Iterate over a DOM tree or subtree, building up a knowledgebase, a
+ // data structure holding scores and annotations for interesting
+ // elements. Return the knowledgebase.
+ //
+ // This is the "rank" portion of the rank-and-yank algorithm.
+ score: function (tree) {
+ const kb = knowledgebase();
+
+ // Introduce the whole DOM into the KB as flavor 'dom' to get
+ // things started:
+ const nonterminals = [[{tree}, 'dom']]; // [[node, flavor], [node, flavor], ...]
+
+ // While there are new facts, run the applicable rules over them to
+ // generate even newer facts. Repeat until everything's fully
+ // digested. Rules run in no particular guaranteed order.
+ while (nonterminals.length) {
+ const [inNode, inFlavor] = nonterminals.pop();
+ for (let rule of getDefault(rulesByInputFlavor, inFlavor, () => [])) {
+ const outFacts = resultsOf(rule, inNode, inFlavor, kb);
+ for (let fact of outFacts) {
+ const outNode = kb.nodeForElement(fact.element);
+
+ // No matter whether or not this flavor has been
+ // emitted before for this node, we multiply the score.
+ // We want to be able to add rules that refine the
+ // scoring of a node, without having to rewire the path
+ // of flavors that winds through the ruleset.
+ //
+ // 1 score per Node is plenty. That simplifies our
+ // data, our rankers, our flavor system (since we don't
+ // need to represent score axes), and our engine. If
+ // somebody wants more score axes, they can fake it
+ // themselves with notes, thus paying only for what
+ // they eat. (We can even provide functions that help
+ // with that.) Most rulesets will probably be concerned
+ // with scoring only 1 thing at a time anyway. So,
+ // rankers return a score multiplier + 0 or more new
+ // flavors with optional notes. Facts can never be
+ // deleted from the KB by rankers (or order would start
+ // to matter); after all, they're *facts*.
+ outNode.score *= fact.score;
+
+ // Add a new annotation to a node--but only if there
+ // wasn't already one of the given flavor already
+ // there; otherwise there's no point.
+ //
+ // You might argue that we might want to modify an
+ // existing note here, but that would be a bad
+ // idea. Notes of a given flavor should be
+ // considered immutable once laid down. Otherwise, the
+ // order of execution of same-flavored rules could
+ // matter, hurting pluggability. Emit a new flavor and
+ // a new note if you want to do that.
+ //
+ // Also, choosing not to add a new fact to nonterminals
+ // when we're not adding a new flavor saves the work of
+ // running the rules against it, which would be
+ // entirely redundant and perform no new work (unless
+ // the rankers were nondeterministic, but don't do
+ // that).
+ if (!outNode.flavors.has(fact.flavor)) {
+ outNode.flavors.set(fact.flavor, fact.notes);
+ kb.indexNodeByFlavor(outNode, fact.flavor); // TODO: better encapsulation rather than indexing explicitly
+ nonterminals.push([outNode, fact.flavor]);
+ }
+ }
+ }
+ }
+ return kb;
+ }
+ };
+}
+
+
+// Construct a container for storing and querying facts, where a fact has a
+// flavor (used to dispatch further rules upon), a corresponding DOM element, a
+// score, and some other arbitrary notes opaque to fathom.
+function knowledgebase() {
+ const nodesByFlavor = new Map(); // Map{'texty' -> [NodeA],
+ // 'spiffy' -> [NodeA, NodeB]}
+ // NodeA = {element: <someElement>,
+ //
+ // // Global nodewide score. Add
+ // // custom ones with notes if
+ // // you want.
+ // score: 8,
+ //
+ // // Flavors is a map of flavor names to notes:
+ // flavors: Map{'texty' -> {ownText: 'blah',
+ // someOtherNote: 'foo',
+ // someCustomScore: 10},
+ // // This is an empty note:
+ // 'fluffy' -> undefined}}
+ const nodesByElement = new Map();
+
+ return {
+ // Return the "node" (our own data structure that we control) that
+ // corresponds to a given DOM element, creating one if necessary.
+ nodeForElement: function (element) {
+ return getDefault(nodesByElement,
+ element,
+ () => ({element,
+ score: 1,
+ flavors: new Map()}));
+ },
+
+ // Return the highest-scored node of the given flavor, undefined if
+ // there is none.
+ max: function (flavor) {
+ const nodes = nodesByFlavor.get(flavor);
+ return nodes === undefined ? undefined : max(nodes, node => node.score);
+ },
+
+ // Let the KB know that a new flavor has been added to an element.
+ indexNodeByFlavor: function (node, flavor) {
+ getDefault(nodesByFlavor, flavor, () => []).push(node);
+ },
+
+ nodesOfFlavor: function (flavor) {
+ return getDefault(nodesByFlavor, flavor, () => []);
+ }
+ };
+}
+
+
+// Apply a rule (as returned by a call to rule()) to a fact, and return the
+// new facts that result.
+function resultsOf(rule, node, flavor, kb) {
+ // If more types of rule pop up someday, do fancier dispatching here.
+ return rule.source.flavor === 'flavor' ? resultsOfFlavorRule(rule, node, flavor) : resultsOfDomRule(rule, node, kb);
+}
+
+
+// Pull the DOM tree off the special property of the root "dom" fact, and query
+// against it.
+function *resultsOfDomRule(rule, specialDomNode, kb) {
+ // Use the special "tree" property of the special starting node:
+ const matches = specialDomNode.tree.querySelectorAll(rule.source.selector);
+
+ for (let i = 0; i < matches.length; i++) { // matches is a NodeList, which doesn't conform to iterator protocol
+ const element = matches[i];
+ const newFacts = explicitFacts(rule.ranker(kb.nodeForElement(element)));
+ for (let fact of newFacts) {
+ if (fact.element === undefined) {
+ fact.element = element;
+ }
+ if (fact.flavor === undefined) {
+ throw new Error('Rankers of dom() rules must return a flavor in each fact. Otherwise, there is no way for that fact to be used later.');
+ }
+ yield fact;
+ }
+ }
+}
+
+
+function *resultsOfFlavorRule(rule, node, flavor) {
+ const newFacts = explicitFacts(rule.ranker(node));
+
+ for (let fact of newFacts) {
+ // If the ranker didn't specify a different element, assume it's
+ // talking about the one we passed in:
+ if (fact.element === undefined) {
+ fact.element = node.element;
+ }
+ if (fact.flavor === undefined) {
+ fact.flavor = flavor;
+ }
+ yield fact;
+ }
+}
+
+
+// Take the possibly abbreviated output of a ranker function, and make it
+// explicitly an iterable with a defined score.
+//
+// Rankers can return undefined, which means "no facts", a single fact, or an
+// array of facts.
+function *explicitFacts(rankerResult) {
+ const array = (rankerResult === undefined) ? [] : (Array.isArray(rankerResult) ? rankerResult : [rankerResult]);
+ for (let fact of array) {
+ if (fact.score === undefined) {
+ fact.score = 1;
+ }
+ yield fact;
+ }
+}
+
+
+// TODO: For the moment, a lot of responsibility is on the rankers to return a
+// pretty big data structure of up to 4 properties. This is a bit verbose for
+// an arrow function (as I hope we can use most of the time) and the usual case
+// will probably be returning just a score multiplier. Make that case more
+// concise.
+
+// TODO: It is likely that rankers should receive the notes of their input type
+// as a 2nd arg, for brevity.
+
+
+// Return a condition that uses a DOM selector to find its matches from the
+// original DOM tree.
+//
+// For consistency, Nodes will still be delivered to the transformers, but
+// they'll have empty flavors and score = 1.
+//
+// Condition constructors like dom() and flavor() build stupid, introspectable
+// objects that the query engine can read. They don't actually do the query
+// themselves. That way, the query planner can be smarter than them, figuring
+// out which indices to use based on all of them. (We'll probably keep a heap
+// by each dimension's score and a hash by flavor name, for starters.) Someday,
+// fancy things like this may be possible: rule(and(tag('p'), klass('snork')),
+// ...)
+function dom(selector) {
+ return {
+ flavor: 'dom',
+ inputFlavor: 'dom',
+ selector
+ };
+}
+
+
+// Return a condition that discriminates on nodes of the knowledgebase by flavor.
+function flavor(inputFlavor) {
+ return {
+ flavor: 'flavor',
+ inputFlavor
+ };
+}
+
+
+function rule(source, ranker) {
+ return {
+ source,
+ ranker
+ };
+}
diff --git a/mobile/android/modules/dbg-browser-actors.js b/mobile/android/modules/dbg-browser-actors.js
new file mode 100644
index 0000000000..f96a391517
--- /dev/null
+++ b/mobile/android/modules/dbg-browser-actors.js
@@ -0,0 +1,77 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+/**
+ * Fennec-specific actors.
+ */
+
+const { RootActor } = require("devtools/server/actors/root");
+const { DebuggerServer } = require("devtools/server/main");
+const { BrowserTabList, BrowserAddonList, sendShutdownEvent } =
+ require("devtools/server/actors/webbrowser");
+
+/**
+ * Construct a root actor appropriate for use in a server running in a
+ * browser on Android. The returned root actor:
+ * - respects the factories registered with DebuggerServer.addGlobalActor,
+ * - uses a MobileTabList to supply tab actors,
+ * - sends all navigator:browser window documents a Debugger:Shutdown event
+ * when it exits.
+ *
+ * * @param aConnection DebuggerServerConnection
+ * The conection to the client.
+ */
+function createRootActor(aConnection)
+{
+ let parameters = {
+ tabList: new MobileTabList(aConnection),
+ addonList: new BrowserAddonList(aConnection),
+ globalActorFactories: DebuggerServer.globalActorFactories,
+ onShutdown: sendShutdownEvent
+ };
+ return new RootActor(aConnection, parameters);
+}
+
+/**
+ * A live list of BrowserTabActors representing the current browser tabs,
+ * to be provided to the root actor to answer 'listTabs' requests.
+ *
+ * This object also takes care of listening for TabClose events and
+ * onCloseWindow notifications, and exiting the BrowserTabActors concerned.
+ *
+ * (See the documentation for RootActor for the definition of the "live
+ * list" interface.)
+ *
+ * @param aConnection DebuggerServerConnection
+ * The connection in which this list's tab actors may participate.
+ *
+ * @see BrowserTabList for more a extensive description of how tab list objects
+ * work.
+ */
+function MobileTabList(aConnection)
+{
+ BrowserTabList.call(this, aConnection);
+}
+
+MobileTabList.prototype = Object.create(BrowserTabList.prototype);
+
+MobileTabList.prototype.constructor = MobileTabList;
+
+MobileTabList.prototype._getSelectedBrowser = function(aWindow) {
+ return aWindow.BrowserApp.selectedBrowser;
+};
+
+MobileTabList.prototype._getChildren = function(aWindow) {
+ return aWindow.BrowserApp.tabs.map(tab => tab.browser);
+};
+
+exports.register = function(handle) {
+ handle.setRootActor(createRootActor);
+};
+
+exports.unregister = function(handle) {
+ handle.setRootActor(null);
+};
diff --git a/mobile/android/modules/moz.build b/mobile/android/modules/moz.build
new file mode 100644
index 0000000000..479ff1f3f5
--- /dev/null
+++ b/mobile/android/modules/moz.build
@@ -0,0 +1,33 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXTRA_JS_MODULES += [
+ 'Accounts.jsm',
+ 'AndroidLog.jsm',
+ 'dbg-browser-actors.js',
+ 'DelayedInit.jsm',
+ 'DownloadNotifications.jsm',
+ 'FxAccountsWebChannel.jsm',
+ 'HelperApps.jsm',
+ 'Home.jsm',
+ 'HomeProvider.jsm',
+ 'JavaAddonManager.jsm',
+ 'JNI.jsm',
+ 'LightweightThemeConsumer.jsm',
+ 'MediaPlayerApp.jsm',
+ 'Messaging.jsm',
+ 'NetErrorHelper.jsm',
+ 'Notifications.jsm',
+ 'PageActions.jsm',
+ 'Prompt.jsm',
+ 'RuntimePermissions.jsm',
+ 'Sanitizer.jsm',
+ 'SharedPreferences.jsm',
+ 'Snackbars.jsm',
+ 'SSLExceptions.jsm',
+ 'TabMirror.jsm',
+ 'WebsiteMetadata.jsm'
+]
diff --git a/mobile/android/moz.build b/mobile/android/moz.build
new file mode 100644
index 0000000000..b3f25275a3
--- /dev/null
+++ b/mobile/android/moz.build
@@ -0,0 +1,36 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+CONFIGURE_SUBST_FILES += ['installer/Makefile']
+
+DIRS += [
+ '../locales',
+ 'locales',
+]
+
+if CONFIG['MOZ_ANDROID_MLS_STUMBLER']:
+ DIRS += ['stumbler']
+
+DIRS += [
+ 'javaaddons', # Must be built before base.
+ 'base',
+ 'chrome',
+ 'components',
+ 'extensions',
+ 'modules',
+ 'themes/core',
+ 'app',
+ 'fonts',
+]
+
+if CONFIG['MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER']:
+ DIRS += ['bouncer'] # No ordering implied with respect to base.
+
+TEST_DIRS += [
+ 'tests',
+]
+
+SPHINX_TREES['fennec'] = 'docs'
diff --git a/mobile/android/moz.configure b/mobile/android/moz.configure
new file mode 100644
index 0000000000..0ab0b113e2
--- /dev/null
+++ b/mobile/android/moz.configure
@@ -0,0 +1,86 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+project_flag('MOZ_ANDROID_EXCLUDE_FONTS',
+ help='Whether to exclude font files from the build',
+ default=True)
+
+project_flag('MOZ_LOCALE_SWITCHER',
+ help='Enable runtime locale switching',
+ default=True)
+
+project_flag('MOZ_ANDROID_GCM',
+ help='Enable GCM registration on Nightly builds only',
+ default=True,
+ set_for_old_configure=True)
+
+project_flag('MOZ_ANDROID_DOWNLOADS_INTEGRATION',
+ help='Enable system download manager on Android',
+ default=True)
+
+project_flag('MOZ_ANDROID_BEAM',
+ help='Enable NFC permission on Android',
+ default=True)
+
+project_flag('MOZ_ANDROID_SEARCH_ACTIVITY',
+ help='Include Search Activity on Android',
+ default=True)
+
+project_flag('MOZ_ANDROID_MLS_STUMBLER',
+ help='Include Mozilla Location Service Stumbler on Android',
+ default=True)
+
+project_flag('MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE',
+ help='Background service for downloading additional content at runtime',
+ default=True)
+
+project_flag('MOZ_ANDROID_CUSTOM_TABS',
+ help='Enable support for Android custom tabs',
+ default=delayed_getattr(milestone, 'is_nightly'))
+
+# Enable the Switchboard A/B framework code.
+# Note: The framework is always included in the app. This flag controls
+# usage of the framework.
+project_flag('MOZ_SWITCHBOARD',
+ help='Include Switchboard A/B framework on Android',
+ default=True)
+
+option(env='MOZ_ANDROID_ACTIVITY_STREAM',
+ help='Enable Activity Stream on Android (replacing the default HomePager)',
+ default=False)
+
+set_config('MOZ_ANDROID_ACTIVITY_STREAM',
+ depends_if('MOZ_ANDROID_ACTIVITY_STREAM')(lambda _: True))
+
+option(env='MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER',
+ help='Build and package the install bouncer APK',
+ default=True)
+
+set_config('MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER',
+ depends_if('MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER')(lambda _: True))
+
+imply_option('MOZ_SOCIAL', False)
+imply_option('MOZ_SERVICES_HEALTHREPORT', True)
+imply_option('MOZ_ANDROID_HISTORY', True)
+
+set_config('ANDROID_SUPPORT_LIBRARY_VERSION', '23.4.0')
+add_old_configure_assignment('ANDROID_SUPPORT_LIBRARY_VERSION', '23.4.0')
+
+set_config('ANDROID_GOOGLE_PLAY_SERVICES_VERSION', '8.4.0')
+add_old_configure_assignment('ANDROID_GOOGLE_PLAY_SERVICES_VERSION', '8.4.0')
+
+@depends(target)
+def check_target(target):
+ if target.os != 'Android':
+ log.error('You must specify --target=arm-linux-androideabi (or some '
+ 'other valid Android target) when building mobile/android.')
+ die('See https://developer.mozilla.org/docs/Mozilla/Developer_guide/'
+ 'Build_Instructions/Simple_Firefox_for_Android_build '
+ 'for more information about the necessary options.')
+
+include('../../toolkit/moz.configure')
+include('../../build/moz.configure/java.configure')
+include('gradle.configure')
diff --git a/mobile/android/search/java/org/mozilla/search/AcceptsSearchQuery.java b/mobile/android/search/java/org/mozilla/search/AcceptsSearchQuery.java
new file mode 100644
index 0000000000..e54b9a9fc9
--- /dev/null
+++ b/mobile/android/search/java/org/mozilla/search/AcceptsSearchQuery.java
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.search;
+
+import android.graphics.Rect;
+
+/**
+ * Allows fragments to pass a search event to the main activity.
+ */
+public interface AcceptsSearchQuery {
+
+ /**
+ * Shows search suggestions.
+ * @param query
+ */
+ void onSuggest(String query);
+
+ /**
+ * Starts a search.
+ *
+ * @param query
+ */
+ void onSearch(String query);
+
+ /**
+ * Starts a search and animates a suggestion.
+ *
+ * @param query
+ * @param suggestionAnimation
+ */
+ void onSearch(String query, SuggestionAnimation suggestionAnimation);
+
+ /**
+ * Handles a change to the current search query.
+ *
+ * @param query
+ */
+ void onQueryChange(String query);
+
+ /**
+ * Interface to specify search suggestion animation details.
+ */
+ public interface SuggestionAnimation {
+ public Rect getStartBounds();
+ }
+}
diff --git a/mobile/android/search/java/org/mozilla/search/Constants.java b/mobile/android/search/java/org/mozilla/search/Constants.java
new file mode 100644
index 0000000000..8e8a176005
--- /dev/null
+++ b/mobile/android/search/java/org/mozilla/search/Constants.java
@@ -0,0 +1,20 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.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.search;
+
+/**
+ * Key should not be stored here. For more info on storing keys, see
+ * https://github.com/ericedens/FirefoxSearch/issues/3
+ */
+public class Constants {
+
+ public static final String ABOUT_BLANK = "about:blank";
+}
diff --git a/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java b/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java
new file mode 100644
index 0000000000..8a26c49dd2
--- /dev/null
+++ b/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java
@@ -0,0 +1,243 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.search;
+
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+import android.support.v4.content.ContextCompat;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.GeckoApplication;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.search.SearchEngine;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
+
+import android.annotation.SuppressLint;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.support.v4.app.Fragment;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.webkit.WebChromeClient;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+public class PostSearchFragment extends Fragment {
+
+ private static final String LOG_TAG = "PostSearchFragment";
+
+ private SearchEngine engine;
+
+ private ProgressBar progressBar;
+ private WebView webview;
+ private View errorView;
+
+ private String resultsPageHost;
+
+ @SuppressLint("SetJavaScriptEnabled")
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View mainView = inflater.inflate(R.layout.search_fragment_post_search, container, false);
+
+ progressBar = (ProgressBar) mainView.findViewById(R.id.progress_bar);
+
+ webview = (WebView) mainView.findViewById(R.id.webview);
+ webview.setWebChromeClient(new ChromeClient());
+ webview.setWebViewClient(new ResultsWebViewClient());
+
+ // This is required for our greasemonkey terror script.
+ webview.getSettings().setJavaScriptEnabled(true);
+
+ return mainView;
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ webview.setWebChromeClient(null);
+ webview.setWebViewClient(null);
+ webview = null;
+ progressBar = null;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ GeckoApplication.watchReference(getActivity(), this);
+ }
+
+ public void startSearch(SearchEngine engine, String query) {
+ this.engine = engine;
+
+ final String url = engine.resultsUriForQuery(query);
+ // Only load urls if the url is different than the webview's current url.
+ if (!TextUtils.equals(webview.getUrl(), url)) {
+ resultsPageHost = null;
+ webview.loadUrl(Constants.ABOUT_BLANK);
+ webview.loadUrl(url);
+ }
+ }
+
+ /**
+ * A custom WebViewClient that intercepts every page load. This allows
+ * us to decide whether to load the url here, or send it to Android
+ * as an intent. It also handles network errors.
+ */
+ private class ResultsWebViewClient extends WebViewClient {
+
+ // Whether or not there is a network error.
+ private boolean networkError;
+
+ @Override
+ public void onPageStarted(WebView view, final String url, Bitmap favicon) {
+ // Reset the error state.
+ networkError = false;
+ }
+
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ // Ignore about:blank URL loads and the first results page we try to load.
+ if (TextUtils.equals(url, Constants.ABOUT_BLANK) || resultsPageHost == null) {
+ return false;
+ }
+
+ String host = null;
+ try {
+ host = new URL(url).getHost();
+ } catch (MalformedURLException e) {
+ Log.e(LOG_TAG, "Error getting host from URL loading in webview", e);
+ }
+
+ // If the host name is the same as the results page, don't override the URL load, but
+ // do update the query in the search bar if possible.
+ if (TextUtils.equals(resultsPageHost, host)) {
+ // This won't work for results pages that redirect (e.g. Google in different country)
+ final String query = engine.queryForResultsUrl(url);
+ if (!TextUtils.isEmpty(query)) {
+ ((AcceptsSearchQuery) getActivity()).onQueryChange(query);
+ }
+ return false;
+ }
+
+ try {
+ // If the url URI does not have an intent scheme, the intent data will be the entire
+ // URI and its action will be ACTION_VIEW.
+ final Intent i = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
+
+ // If the intent URI didn't specify a package, open this in Fennec.
+ if (i.getPackage() == null) {
+ i.setClassName(view.getContext().getPackageName(), AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
+ Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL,
+ TelemetryContract.Method.CONTENT, "search-result");
+ } else {
+ Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH,
+ TelemetryContract.Method.INTENT, "search-result");
+ }
+
+ i.addCategory(Intent.CATEGORY_BROWSABLE);
+ i.setComponent(null);
+ i.setSelector(null);
+
+ startActivity(i);
+ return true;
+ } catch (URISyntaxException e) {
+ Log.e(LOG_TAG, "Error parsing intent URI", e);
+ } catch (SecurityException e) {
+ Log.e(LOG_TAG, "SecurityException handling arbitrary intent content");
+ } catch (ActivityNotFoundException e) {
+ Log.e(LOG_TAG, "Intent not actionable");
+ }
+
+ return false;
+ }
+
+ // We are suppressing the 'deprecation' warning because the new method is only available starting with API
+ // level 23 and that's much higher than our current minSdkLevel (1208580).
+ @SuppressWarnings("deprecation")
+ @Override
+ public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
+ Log.e(LOG_TAG, "Error loading search results: " + description);
+
+ networkError = true;
+
+ if (errorView == null) {
+ final ViewStub errorViewStub = (ViewStub) getView().findViewById(R.id.error_view_stub);
+ errorView = errorViewStub.inflate();
+
+ ((ImageView) errorView.findViewById(R.id.empty_image)).setImageResource(R.drawable.network_error);
+ ((TextView) errorView.findViewById(R.id.empty_title)).setText(R.string.network_error_title);
+
+ final TextView message = (TextView) errorView.findViewById(R.id.empty_message);
+ message.setText(R.string.network_error_message);
+ message.setTextColor(ContextCompat.getColor(view.getContext(), R.color.network_error_link));
+ message.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startActivity(new Intent(Settings.ACTION_SETTINGS));
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ // Make sure the error view is hidden if the network error was fixed.
+ if (errorView != null) {
+ errorView.setVisibility(networkError ? View.VISIBLE : View.GONE);
+ webview.setVisibility(networkError ? View.GONE : View.VISIBLE);
+ }
+
+ if (!TextUtils.equals(url, Constants.ABOUT_BLANK) && resultsPageHost == null) {
+ try {
+ resultsPageHost = new URL(url).getHost();
+ } catch (MalformedURLException e) {
+ Log.e(LOG_TAG, "Error getting host from results page URL", e);
+ }
+ }
+ }
+ }
+
+ /**
+ * A custom WebChromeClient that allows us to inject CSS into
+ * the head of the HTML and to monitor pageload progress.
+ *
+ * We use the WebChromeClient because it provides a hook to the titleReceived
+ * event. Once the title is available, the page will have started parsing the
+ * head element. The script injects its CSS into the head element.
+ */
+ private class ChromeClient extends WebChromeClient {
+
+ @Override
+ public void onReceivedTitle(final WebView view, String title) {
+ view.loadUrl(engine.getInjectableJs());
+ }
+
+ @Override
+ public void onProgressChanged(WebView view, int newProgress) {
+ if (newProgress < 100) {
+ if (progressBar.getVisibility() == View.INVISIBLE) {
+ progressBar.setVisibility(View.VISIBLE);
+ }
+ progressBar.setProgress(newProgress);
+ } else {
+ progressBar.setVisibility(View.INVISIBLE);
+ }
+ }
+ }
+}
diff --git a/mobile/android/search/java/org/mozilla/search/PreSearchFragment.java b/mobile/android/search/java/org/mozilla/search/PreSearchFragment.java
new file mode 100644
index 0000000000..107b82c5c9
--- /dev/null
+++ b/mobile/android/search/java/org/mozilla/search/PreSearchFragment.java
@@ -0,0 +1,218 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.search;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.support.v4.widget.SimpleCursorAdapter;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.widget.AdapterView;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import org.mozilla.gecko.GeckoApplication;
+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.SearchHistory;
+import org.mozilla.gecko.widget.SwipeDismissListViewTouchListener;
+import org.mozilla.gecko.widget.SwipeDismissListViewTouchListener.OnDismissCallback;
+import org.mozilla.search.AcceptsSearchQuery.SuggestionAnimation;
+
+/**
+ * This fragment is responsible for managing the card stream.
+ */
+public class PreSearchFragment extends Fragment {
+
+ private static final String LOG_TAG = "PreSearchFragment";
+
+ private AcceptsSearchQuery searchListener;
+ private SimpleCursorAdapter cursorAdapter;
+
+ private ListView listView;
+ private View emptyView;
+
+ private static final String[] PROJECTION = new String[]{ SearchHistory.QUERY, SearchHistory._ID };
+
+ // Limit search history query results to 10 items.
+ private static final int SEARCH_HISTORY_LIMIT = 10;
+
+ private static final Uri SEARCH_HISTORY_URI = SearchHistory.CONTENT_URI.buildUpon().
+ appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(SEARCH_HISTORY_LIMIT)).build();
+
+ private static final int LOADER_ID_SEARCH_HISTORY = 1;
+
+ public PreSearchFragment() {
+ // Mandatory empty constructor for Android's Fragment.
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+
+ if (context instanceof AcceptsSearchQuery) {
+ searchListener = (AcceptsSearchQuery) context;
+ } else {
+ throw new ClassCastException(context.toString() + " must implement AcceptsSearchQuery.");
+ }
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ searchListener = null;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getLoaderManager().initLoader(LOADER_ID_SEARCH_HISTORY, null, new SearchHistoryLoaderCallbacks());
+ cursorAdapter = new SimpleCursorAdapter(getActivity(), R.layout.search_history_row, null,
+ PROJECTION, new int[]{R.id.site_name}, 0);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ getLoaderManager().destroyLoader(LOADER_ID_SEARCH_HISTORY);
+ cursorAdapter.swapCursor(null);
+ cursorAdapter = null;
+
+ GeckoApplication.watchReference(getActivity(), this);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+ final View mainView = inflater.inflate(R.layout.search_fragment_pre_search, container, false);
+
+ // Initialize listview.
+ listView = (ListView) mainView.findViewById(R.id.list_view);
+ listView.setAdapter(cursorAdapter);
+ listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ final String query = getQueryAtPosition(position);
+ if (!TextUtils.isEmpty(query)) {
+ final Rect startBounds = new Rect();
+ view.getGlobalVisibleRect(startBounds);
+
+ Telemetry.sendUIEvent(TelemetryContract.Event.SEARCH, TelemetryContract.Method.SUGGESTION, "history");
+
+ searchListener.onSearch(query, new SuggestionAnimation() {
+ @Override
+ public Rect getStartBounds() {
+ return startBounds;
+ }
+ });
+ }
+ }
+ });
+
+ // 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(listView, new OnDismissCallback() {
+ @Override
+ public void onDismiss(ListView listView, final int position) {
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ final String query = getQueryAtPosition(position);
+ final int deleted = getActivity().getContentResolver().delete(
+ SearchHistory.CONTENT_URI,
+ SearchHistory.QUERY + " = ?",
+ new String[] { query });
+
+ if (deleted < 1) {
+ Log.w(LOG_TAG, "Search query not deleted: " + query);
+ }
+ return null;
+ }
+ }.execute();
+ }
+ });
+ listView.setOnTouchListener(touchListener);
+
+ // Setting this scroll listener is required to ensure that during ListView scrolling,
+ // we don't look for swipes.
+ listView.setOnScrollListener(touchListener.makeScrollListener());
+
+ // Setting this recycler listener is required to make sure animated views are reset.
+ listView.setRecyclerListener(touchListener.makeRecyclerListener());
+
+ return mainView;
+ }
+
+ private String getQueryAtPosition(int position) {
+ final Cursor c = cursorAdapter.getCursor();
+ if (c == null || !c.moveToPosition(position)) {
+ return null;
+ }
+ return c.getString(c.getColumnIndexOrThrow(SearchHistory.QUERY));
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ listView.setAdapter(null);
+ listView = null;
+ emptyView = null;
+ }
+
+ private void updateUiFromCursor(Cursor c) {
+ if (c != null && c.getCount() > 0) {
+ return;
+ }
+
+ if (emptyView == null) {
+ final ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.empty_view_stub);
+ emptyView = emptyViewStub.inflate();
+
+ ((ImageView) emptyView.findViewById(R.id.empty_image)).setImageResource(R.drawable.icon_search_empty_firefox);
+ ((TextView) emptyView.findViewById(R.id.empty_title)).setText(R.string.search_empty_title);
+ ((TextView) emptyView.findViewById(R.id.empty_message)).setText(R.string.search_empty_message);
+
+ listView.setEmptyView(emptyView);
+ }
+ }
+
+ private class SearchHistoryLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ return new CursorLoader(getActivity(), SEARCH_HISTORY_URI, PROJECTION, null, null,
+ SearchHistory.DATE_LAST_VISITED + " DESC");
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
+ if (cursorAdapter != null) {
+ cursorAdapter.swapCursor(c);
+ }
+ updateUiFromCursor(c);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ if (cursorAdapter != null) {
+ cursorAdapter.swapCursor(null);
+ }
+ }
+ }
+}
diff --git a/mobile/android/search/java/org/mozilla/search/SearchActivity.java b/mobile/android/search/java/org/mozilla/search/SearchActivity.java
new file mode 100644
index 0000000000..b013d77b41
--- /dev/null
+++ b/mobile/android/search/java/org/mozilla/search/SearchActivity.java
@@ -0,0 +1,436 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.search;
+
+import android.support.annotation.NonNull;
+import org.mozilla.gecko.GeckoAppShell;
+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.BrowserContract.SearchHistory;
+import org.mozilla.gecko.distribution.Distribution;
+import org.mozilla.gecko.search.SearchEngine;
+import org.mozilla.gecko.search.SearchEngineManager;
+import org.mozilla.gecko.search.SearchEngineManager.SearchEngineCallback;
+import org.mozilla.search.autocomplete.SearchBar;
+import org.mozilla.search.autocomplete.SuggestionsFragment;
+
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+
+/**
+ * The main entrance for the Android search intent.
+ * <p/>
+ * State management is delegated to child fragments. Fragments communicate
+ * with each other by passing messages through this activity.
+ */
+public class SearchActivity extends Locales.LocaleAwareFragmentActivity
+ implements AcceptsSearchQuery, SearchEngineCallback {
+
+ private static final String LOGTAG = "GeckoSearchActivity";
+
+ private static final String KEY_SEARCH_STATE = "search_state";
+ private static final String KEY_EDIT_STATE = "edit_state";
+ private static final String KEY_QUERY = "query";
+
+ static enum SearchState {
+ PRESEARCH,
+ POSTSEARCH
+ }
+
+ static enum EditState {
+ WAITING,
+ EDITING
+ }
+
+ // Default states when activity is created.
+ private SearchState searchState = SearchState.PRESEARCH;
+ private EditState editState = EditState.WAITING;
+
+ @NonNull
+ private SearchEngineManager searchEngineManager; // Contains reference to Context - DO NOT LEAK!
+
+ // Only accessed on the main thread.
+ private SearchEngine engine;
+
+ private SuggestionsFragment suggestionsFragment;
+ private PostSearchFragment postSearchFragment;
+
+ private AsyncQueryHandler queryHandler;
+
+ // Main views in layout.
+ private SearchBar searchBar;
+ private View preSearch;
+ private View postSearch;
+
+ private View settingsButton;
+
+ private View suggestions;
+
+ private static final int SUGGESTION_TRANSITION_DURATION = 300;
+ private static final Interpolator SUGGESTION_TRANSITION_INTERPOLATOR =
+ new AccelerateDecelerateInterpolator();
+
+ // View used for suggestion animation.
+ private View animationCard;
+
+ // Suggestion card background padding.
+ private int cardPaddingX;
+ private int cardPaddingY;
+
+ /**
+ * An empty implementation of AsyncQueryHandler to avoid the "HandlerLeak" warning from Android
+ * Lint. See also {@see org.mozilla.gecko.util.WeakReferenceHandler}.
+ */
+ private static class AsyncQueryHandlerImpl extends AsyncQueryHandler {
+ public AsyncQueryHandlerImpl(final ContentResolver that) {
+ super(that);
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ GeckoAppShell.ensureCrashHandling();
+
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.search_activity_main);
+
+ suggestionsFragment = (SuggestionsFragment) getSupportFragmentManager().findFragmentById(R.id.suggestions);
+ postSearchFragment = (PostSearchFragment) getSupportFragmentManager().findFragmentById(R.id.postsearch);
+
+ searchEngineManager = new SearchEngineManager(this, Distribution.init(getApplicationContext()));
+ searchEngineManager.setChangeCallback(this);
+
+ // Initialize the fragments with the selected search engine.
+ searchEngineManager.getEngine(this);
+
+ queryHandler = new AsyncQueryHandlerImpl(getContentResolver());
+
+ searchBar = (SearchBar) findViewById(R.id.search_bar);
+ searchBar.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setEditState(EditState.EDITING);
+ }
+ });
+
+ searchBar.setTextListener(new SearchBar.TextListener() {
+ @Override
+ public void onChange(String text) {
+ // Only load suggestions if we're in edit mode.
+ if (editState == EditState.EDITING) {
+ suggestionsFragment.loadSuggestions(text);
+ }
+ }
+
+ @Override
+ public void onSubmit(String text) {
+ // Don't submit an empty query.
+ final String trimmedQuery = text.trim();
+ if (!TextUtils.isEmpty(trimmedQuery)) {
+ onSearch(trimmedQuery);
+ }
+ }
+
+ @Override
+ public void onFocusChange(boolean hasFocus) {
+ setEditState(hasFocus ? EditState.EDITING : EditState.WAITING);
+ }
+ });
+
+ preSearch = findViewById(R.id.presearch);
+ postSearch = findViewById(R.id.postsearch);
+
+ settingsButton = findViewById(R.id.settings_button);
+
+ // Apply click handler to settings button.
+ settingsButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startActivity(new Intent(SearchActivity.this, SearchPreferenceActivity.class));
+ }
+ });
+
+ suggestions = findViewById(R.id.suggestions);
+
+ animationCard = findViewById(R.id.animation_card);
+
+ cardPaddingX = getResources().getDimensionPixelSize(R.dimen.search_row_padding);
+ cardPaddingY = getResources().getDimensionPixelSize(R.dimen.search_row_padding);
+
+ if (savedInstanceState != null) {
+ setSearchState(SearchState.valueOf(savedInstanceState.getString(KEY_SEARCH_STATE)));
+ setEditState(EditState.valueOf(savedInstanceState.getString(KEY_EDIT_STATE)));
+
+ final String query = savedInstanceState.getString(KEY_QUERY);
+ searchBar.setText(query);
+
+ // If we're in the postsearch state, we need to re-do the query.
+ if (searchState == SearchState.POSTSEARCH) {
+ startSearch(query);
+ }
+ } else {
+ // If there isn't a state to restore, the activity will start in the presearch state,
+ // and we should enter editing mode to bring up the keyboard.
+ setEditState(EditState.EDITING);
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ searchEngineManager.unregisterListeners();
+ engine = null;
+ suggestionsFragment = null;
+ postSearchFragment = null;
+ queryHandler = null;
+ searchBar = null;
+ preSearch = null;
+ postSearch = null;
+ settingsButton = null;
+ suggestions = null;
+ animationCard = null;
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ Telemetry.startUISession(TelemetryContract.Session.SEARCH_ACTIVITY);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ Telemetry.stopUISession(TelemetryContract.Session.SEARCH_ACTIVITY);
+ }
+
+ @Override
+ public void onNewIntent(Intent intent) {
+ // Reset the activity in the presearch state if it was launched from a new intent.
+ setSearchState(SearchState.PRESEARCH);
+
+ // Enter editing mode and reset the query. We must reset the query after entering
+ // edit mode in order for the suggestions to update.
+ setEditState(EditState.EDITING);
+ searchBar.setText("");
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ outState.putString(KEY_SEARCH_STATE, searchState.toString());
+ outState.putString(KEY_EDIT_STATE, editState.toString());
+ outState.putString(KEY_QUERY, searchBar.getText());
+ }
+
+ @Override
+ public void onSuggest(String query) {
+ searchBar.setText(query);
+ }
+
+ @Override
+ public void onSearch(String query) {
+ onSearch(query, null);
+ }
+
+ @Override
+ public void onSearch(String query, SuggestionAnimation suggestionAnimation) {
+ storeQuery(query);
+
+ try {
+ //BrowserHealthRecorder.recordSearchDelayed("activity", engine.getIdentifier());
+ } catch (Exception e) {
+ // This should never happen: it'll only throw if the
+ // search location is wrong. But let's not tempt fate.
+ Log.w(LOGTAG, "Unable to record search.");
+ }
+
+ startSearch(query);
+
+ if (suggestionAnimation != null) {
+ searchBar.setText(query);
+ // Animate the suggestion card if start bounds are specified.
+ animateSuggestion(suggestionAnimation);
+ } else {
+ // Otherwise immediately switch to the results view.
+ setEditState(EditState.WAITING);
+ setSearchState(SearchState.POSTSEARCH);
+ }
+ }
+
+ @Override
+ public void onQueryChange(String query) {
+ searchBar.setText(query);
+ }
+
+ private void startSearch(final String query) {
+ if (engine != null) {
+ postSearchFragment.startSearch(engine, query);
+ return;
+ }
+
+ // engine will only be null if startSearch is called before the getEngine
+ // call in onCreate is completed.
+ searchEngineManager.getEngine(new SearchEngineCallback() {
+ @Override
+ public void execute(SearchEngine engine) {
+ // TODO: If engine is null, we should show an error message.
+ if (engine != null) {
+ postSearchFragment.startSearch(engine, query);
+ }
+ }
+ });
+ }
+
+ /**
+ * This method is called when we fetch the current engine in onCreate,
+ * as well as whenever the current engine changes. This method will only
+ * ever be called on the main thread.
+ *
+ * @param engine The current search engine.
+ */
+ @Override
+ public void execute(SearchEngine engine) {
+ // TODO: If engine is null, we should show an error message.
+ if (engine == null) {
+ return;
+ }
+ this.engine = engine;
+ suggestionsFragment.setEngine(engine);
+ searchBar.setEngine(engine);
+ }
+
+ /**
+ * Animates search suggestion item to fill the results view area.
+ *
+ * @param suggestionAnimation
+ */
+ private void animateSuggestion(final SuggestionAnimation suggestionAnimation) {
+ final Rect startBounds = suggestionAnimation.getStartBounds();
+ final Rect endBounds = new Rect();
+ animationCard.getGlobalVisibleRect(endBounds, null);
+
+ // Vertically translate the animated card to align with the start bounds.
+ final float cardStartY = startBounds.centerY() - endBounds.centerY();
+
+ // Account for card background padding when calculating start scale.
+ final float startScaleX = (float) (startBounds.width() - cardPaddingX * 2) / endBounds.width();
+ final float startScaleY = (float) (startBounds.height() - cardPaddingY * 2) / endBounds.height();
+
+ animationCard.setVisibility(View.VISIBLE);
+
+ final AnimatorSet set = new AnimatorSet();
+ set.playTogether(
+ ObjectAnimator.ofFloat(animationCard, "translationY", cardStartY, 0),
+ ObjectAnimator.ofFloat(animationCard, "alpha", 0.5f, 1),
+ ObjectAnimator.ofFloat(animationCard, "scaleX", startScaleX, 1f),
+ ObjectAnimator.ofFloat(animationCard, "scaleY", startScaleY, 1f)
+ );
+
+ set.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ // Don't do anything if the activity is destroyed before the animation ends.
+ if (searchEngineManager == null) {
+ return;
+ }
+
+ setEditState(EditState.WAITING);
+ setSearchState(SearchState.POSTSEARCH);
+
+ // We need to manually clear the animation for the views to be hidden on gingerbread.
+ animationCard.clearAnimation();
+ animationCard.setVisibility(View.INVISIBLE);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+ });
+
+ set.setDuration(SUGGESTION_TRANSITION_DURATION);
+ set.setInterpolator(SUGGESTION_TRANSITION_INTERPOLATOR);
+
+ set.start();
+ }
+
+ private void setEditState(EditState editState) {
+ if (this.editState == editState) {
+ return;
+ }
+ this.editState = editState;
+
+ updateSettingsButtonVisibility();
+
+ searchBar.setActive(editState == EditState.EDITING);
+ suggestions.setVisibility(editState == EditState.EDITING ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ private void setSearchState(SearchState searchState) {
+ if (this.searchState == searchState) {
+ return;
+ }
+ this.searchState = searchState;
+
+ updateSettingsButtonVisibility();
+
+ preSearch.setVisibility(searchState == SearchState.PRESEARCH ? View.VISIBLE : View.INVISIBLE);
+ postSearch.setVisibility(searchState == SearchState.POSTSEARCH ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ private void updateSettingsButtonVisibility() {
+ // Show button on launch screen when keyboard is down.
+ if (searchState == SearchState.PRESEARCH && editState == EditState.WAITING) {
+ settingsButton.setVisibility(View.VISIBLE);
+ } else {
+ settingsButton.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (editState == EditState.EDITING) {
+ setEditState(EditState.WAITING);
+ } else if (searchState == SearchState.POSTSEARCH) {
+ setSearchState(SearchState.PRESEARCH);
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ /**
+ * Store the search query in Fennec's search history database.
+ */
+ private void storeQuery(String query) {
+ final ContentValues cv = new ContentValues();
+ cv.put(SearchHistory.QUERY, query);
+ // Setting 0 for the token, since we only have one type of insert.
+ // Setting null for the cookie, since we don't handle the result of the insert.
+ queryHandler.startInsert(0, null, SearchHistory.CONTENT_URI, cv);
+ }
+}
diff --git a/mobile/android/search/java/org/mozilla/search/SearchPreferenceActivity.java b/mobile/android/search/java/org/mozilla/search/SearchPreferenceActivity.java
new file mode 100644
index 0000000000..6d33da1309
--- /dev/null
+++ b/mobile/android/search/java/org/mozilla/search/SearchPreferenceActivity.java
@@ -0,0 +1,118 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.search;
+
+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.db.BrowserContract;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.util.Log;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+/**
+ * This activity allows users to modify the settings for the search activity.
+ *
+ * A note on implementation: At the moment, we don't have tablet-specific designs.
+ * Therefore, this implementation uses the old-style PreferenceActivity. When
+ * we start optimizing for tablets, we can migrate to Fennec's PreferenceFragment
+ * implementation.
+ *
+ * TODO: Change this to PreferenceFragment when we stop supporting devices older than SDK 11.
+ */
+public class SearchPreferenceActivity extends PreferenceActivity {
+
+ private static final String LOG_TAG = "SearchPreferenceActivity";
+
+ public static final String PREF_CLEAR_HISTORY_KEY = "search.not_a_preference.clear_history";
+
+ @Override
+ @SuppressWarnings("deprecation")
+ protected void onCreate(Bundle savedInstanceState) {
+ Locales.initializeLocale(getApplicationContext());
+ super.onCreate(savedInstanceState);
+
+ getPreferenceManager().setSharedPreferencesName(GeckoSharedPrefs.APP_PREFS_NAME);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ if (getActionBar() != null) {
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+ }
+ }
+
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ setupPrefsScreen();
+ }
+
+ @SuppressWarnings("deprecation")
+ private void setupPrefsScreen() {
+ addPreferencesFromResource(R.xml.search_preferences);
+
+ // Attach click listener to clear history button.
+ final Preference clearHistoryButton = findPreference(PREF_CLEAR_HISTORY_KEY);
+ clearHistoryButton.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(SearchPreferenceActivity.this);
+ dialogBuilder.setNegativeButton(android.R.string.cancel, null);
+ dialogBuilder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Telemetry.sendUIEvent(TelemetryContract.Event.SANITIZE, TelemetryContract.Method.MENU, "search-history");
+ clearHistory();
+ }
+ });
+ dialogBuilder.setMessage(R.string.pref_clearHistory_dialogMessage);
+ dialogBuilder.show();
+ return false;
+ }
+ });
+ }
+
+ private void clearHistory() {
+ final AsyncTask<Void, Void, Boolean> clearHistoryTask = new AsyncTask<Void, Void, Boolean>() {
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ final int numDeleted = getContentResolver().delete(
+ BrowserContract.SearchHistory.CONTENT_URI, null, null);
+ return numDeleted >= 0;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean success) {
+ if (success) {
+ getContentResolver().notifyChange(BrowserContract.SearchHistory.CONTENT_URI, null);
+ Toast.makeText(SearchPreferenceActivity.this, SearchPreferenceActivity.this.getResources()
+ .getString(R.string.pref_clearHistory_confirmation), Toast.LENGTH_SHORT).show();
+ } else {
+ Log.e(LOG_TAG, "Error clearing search history.");
+ }
+ }
+ };
+ clearHistoryTask.execute();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/mobile/android/search/java/org/mozilla/search/SearchWidget.java b/mobile/android/search/java/org/mozilla/search/SearchWidget.java
new file mode 100644
index 0000000000..8f69cc22c4
--- /dev/null
+++ b/mobile/android/search/java/org/mozilla/search/SearchWidget.java
@@ -0,0 +1,135 @@
+/* -*- 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.search;
+
+import org.mozilla.gecko.AboutPages;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.widget.RemoteViews;
+
+/* Provides a really simple widget with two buttons, one to launch Fennec
+ * and one to launch the search activity. All intents are actually sent back
+ * here and then forwarded on to start the real activity. */
+public class SearchWidget extends AppWidgetProvider {
+ final private static String LOGTAG = "GeckoSearchWidget";
+
+ final public static String ACTION_LAUNCH_BROWSER = "org.mozilla.widget.LAUNCH_BROWSER";
+ final public static String ACTION_LAUNCH_SEARCH = "org.mozilla.widget.LAUNCH_SEARCH";
+ final public static String ACTION_LAUNCH_NEW_TAB = "org.mozilla.widget.LAUNCH_NEW_TAB";
+
+ @TargetApi(16)
+ @Override
+ public void onUpdate(final Context context, final AppWidgetManager manager, final int[] ids) {
+ for (int id : ids) {
+ final Bundle bundle;
+ if (AppConstants.Versions.feature16Plus) {
+ bundle = manager.getAppWidgetOptions(id);
+ } else {
+ bundle = null;
+ }
+ addView(manager, context, id, bundle);
+ }
+
+ super.onUpdate(context, manager, ids);
+ }
+
+ @TargetApi(16)
+ @Override
+ public void onAppWidgetOptionsChanged(final Context context,
+ final AppWidgetManager manager,
+ final int id,
+ final Bundle options) {
+ addView(manager, context, id, options);
+ if (AppConstants.Versions.feature16Plus) {
+ super.onAppWidgetOptionsChanged(context, manager, id, options);
+ }
+ }
+
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ // This will hold the intent to redispatch.
+ final Intent redirect;
+ switch (intent.getAction()) {
+ case ACTION_LAUNCH_BROWSER:
+ redirect = buildRedirectIntent(Intent.ACTION_MAIN,
+ context.getPackageName(),
+ AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS,
+ intent);
+ Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH,
+ TelemetryContract.Method.WIDGET, "browser");
+ break;
+ case ACTION_LAUNCH_NEW_TAB:
+ redirect = buildRedirectIntent(Intent.ACTION_VIEW,
+ context.getPackageName(),
+ AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS,
+ intent);
+ Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH,
+ TelemetryContract.Method.WIDGET, "new-tab");
+ break;
+ case ACTION_LAUNCH_SEARCH:
+ redirect = buildRedirectIntent(Intent.ACTION_VIEW,
+ context.getPackageName(),
+ AppConstants.MOZ_ANDROID_SEARCH_INTENT_CLASS,
+ intent);
+ Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH,
+ TelemetryContract.Method.WIDGET, "search");
+ break;
+ default:
+ redirect = null;
+ }
+
+ if (redirect != null) {
+ context.startActivity(redirect);
+ }
+
+ super.onReceive(context, intent);
+ }
+
+ // Utility to create the view for this widget and attach any event listeners to it
+ private void addView(final AppWidgetManager manager, final Context context, final int id, final Bundle options) {
+ final RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.search_widget);
+
+ addClickIntent(context, views, R.id.search_button, ACTION_LAUNCH_SEARCH);
+ addClickIntent(context, views, R.id.new_tab_button, ACTION_LAUNCH_NEW_TAB);
+ // Clicking the logo also launches the browser
+ addClickIntent(context, views, R.id.logo_button, ACTION_LAUNCH_BROWSER);
+
+ manager.updateAppWidget(id, views);
+ }
+
+ // Utility for adding a pending intent to be fired when a View is clicked.
+ private void addClickIntent(final Context context, final RemoteViews views, final int viewId, final String action) {
+ final Intent intent = new Intent(context, SearchWidget.class);
+ intent.setAction(action);
+ intent.setData(Uri.parse(AboutPages.HOME));
+ final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
+ views.setOnClickPendingIntent(viewId, pendingIntent);
+ }
+
+ // Utility for building an intent to be redispatched (i.e. to launch the browser or the search intent).
+ private Intent buildRedirectIntent(final String action, final String pkg, final String className, final Intent source) {
+ final Intent activity = new Intent(action);
+ if (pkg != null && className != null) {
+ activity.setClassName(pkg, className);
+ }
+ activity.setData(source.getData());
+ activity.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ return activity;
+ }
+
+}
diff --git a/mobile/android/search/java/org/mozilla/search/autocomplete/AutoCompleteAdapter.java b/mobile/android/search/java/org/mozilla/search/autocomplete/AutoCompleteAdapter.java
new file mode 100644
index 0000000000..5a0cc8fb65
--- /dev/null
+++ b/mobile/android/search/java/org/mozilla/search/autocomplete/AutoCompleteAdapter.java
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.search.autocomplete;
+
+import java.util.List;
+
+import org.mozilla.gecko.R;
+import org.mozilla.search.AcceptsSearchQuery;
+import org.mozilla.search.autocomplete.SuggestionsFragment.Suggestion;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+
+/**
+ * The adapter that is used to populate the autocomplete rows.
+ */
+class AutoCompleteAdapter extends ArrayAdapter<Suggestion> {
+
+ private final AcceptsSearchQuery searchListener;
+
+ private final LayoutInflater inflater;
+
+ public AutoCompleteAdapter(Context context) {
+ // Uses '0' for the template id since we are overriding getView
+ // and supplying our own view.
+ super(context, 0);
+
+ if (context instanceof AcceptsSearchQuery) {
+ searchListener = (AcceptsSearchQuery) context;
+ } else {
+ throw new ClassCastException(context.toString() + " must implement AcceptsSearchQuery.");
+ }
+
+ // Disable notifying on change. We will notify ourselves in update.
+ setNotifyOnChange(false);
+
+ inflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = inflater.inflate(R.layout.search_suggestions_row, null);
+ }
+
+ final Suggestion suggestion = getItem(position);
+
+ final TextView textView = (TextView) convertView.findViewById(R.id.auto_complete_row_text);
+ textView.setText(suggestion.display);
+
+ final View jumpButton = convertView.findViewById(R.id.auto_complete_row_jump_button);
+ jumpButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ searchListener.onSuggest(suggestion.value);
+ }
+ });
+
+ return convertView;
+ }
+
+ /**
+ * Updates adapter content with new list of search suggestions.
+ *
+ * @param suggestions List of search suggestions.
+ */
+ public void update(List<Suggestion> suggestions) {
+ clear();
+ if (suggestions != null) {
+ for (Suggestion s : suggestions) {
+ add(s);
+ }
+ }
+ notifyDataSetChanged();
+ }
+}
diff --git a/mobile/android/search/java/org/mozilla/search/autocomplete/SearchBar.java b/mobile/android/search/java/org/mozilla/search/autocomplete/SearchBar.java
new file mode 100644
index 0000000000..6225c050b8
--- /dev/null
+++ b/mobile/android/search/java/org/mozilla/search/autocomplete/SearchBar.java
@@ -0,0 +1,201 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.search.autocomplete;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.gfx.BitmapUtils;
+import org.mozilla.gecko.search.SearchEngine;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+public class SearchBar extends FrameLayout {
+
+ private final EditText editText;
+ private final ImageButton clearButton;
+ private final ImageView engineIcon;
+
+ private final Drawable focusedBackground;
+ private final Drawable defaultBackground;
+
+ private final InputMethodManager inputMethodManager;
+
+ private TextListener listener;
+
+ private boolean active;
+
+ public interface TextListener {
+ public void onChange(String text);
+ public void onSubmit(String text);
+ public void onFocusChange(boolean hasFocus);
+ }
+
+ // Deprecation warnings suppressed to allow building with API level 22
+ @SuppressWarnings("deprecation")
+ public SearchBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ LayoutInflater.from(context).inflate(R.layout.search_bar, this);
+
+ editText = (EditText) findViewById(R.id.edit_text);
+ editText.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (listener != null) {
+ listener.onChange(s.toString());
+ }
+
+ updateClearButtonVisibility();
+ }
+ });
+
+ // Attach a listener for the "search" key on the keyboard.
+ editText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (listener != null &&
+ (actionId == EditorInfo.IME_ACTION_UNSPECIFIED || actionId == EditorInfo.IME_ACTION_SEARCH)) {
+ // The user searched without using search engine suggestions.
+ Telemetry.sendUIEvent(TelemetryContract.Event.SEARCH, TelemetryContract.Method.ACTIONBAR, "text");
+ listener.onSubmit(v.getText().toString());
+ return true;
+ }
+ return false;
+ }
+ });
+
+ editText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (listener != null) {
+ listener.onFocusChange(hasFocus);
+ }
+ }
+ });
+
+ clearButton = (ImageButton) findViewById(R.id.clear_button);
+ clearButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ editText.setText("");
+ }
+ });
+ engineIcon = (ImageView) findViewById(R.id.engine_icon);
+
+ focusedBackground = getResources().getDrawable(R.drawable.edit_text_focused);
+ defaultBackground = getResources().getDrawable(R.drawable.edit_text_default);
+
+ inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ }
+
+ public void setText(String text) {
+ editText.setText(text);
+
+ // Move cursor to end of search input.
+ editText.setSelection(text.length());
+ }
+
+ public String getText() {
+ return editText.getText().toString();
+ }
+
+ public void setEngine(SearchEngine engine) {
+ final String iconURL = engine.getIconURL();
+ final Bitmap bitmap = BitmapUtils.getBitmapFromDataURI(iconURL);
+ final BitmapDrawable d = new BitmapDrawable(getResources(), bitmap);
+ engineIcon.setImageDrawable(d);
+ engineIcon.setContentDescription(engine.getName());
+
+ // Update the focused background color.
+ int color = BitmapUtils.getDominantColor(bitmap);
+
+ // BitmapUtils#getDominantColor ignores black and white pixels, but it will
+ // return white if no dominant color was found. We don't want to create a
+ // white underline for the search bar, so we default to black instead.
+ if (color == Color.WHITE) {
+ color = Color.BLACK;
+ }
+ focusedBackground.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY));
+
+ editText.setHint(getResources().getString(R.string.search_bar_hint, engine.getName()));
+ }
+
+ @SuppressWarnings("deprecation")
+ public void setActive(boolean active) {
+ if (this.active == active) {
+ return;
+ }
+ this.active = active;
+
+ updateClearButtonVisibility();
+
+ editText.setFocusable(active);
+ editText.setFocusableInTouchMode(active);
+
+ final int leftDrawable = active ? R.drawable.search_icon_active : R.drawable.search_icon_inactive;
+ editText.setCompoundDrawablesWithIntrinsicBounds(leftDrawable, 0, 0, 0);
+
+ // We can't use a selector drawable because we apply a color filter to the focused
+ // background at run time.
+ // TODO: setBackgroundDrawable is deprecated in API level 16
+ editText.setBackgroundDrawable(active ? focusedBackground : defaultBackground);
+
+ if (active) {
+ editText.requestFocus();
+ inputMethodManager.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
+ } else {
+ editText.clearFocus();
+ inputMethodManager.hideSoftInputFromWindow(editText.getWindowToken(), 0);
+ }
+ }
+
+ private void updateClearButtonVisibility() {
+ // Only show the clear button when there is text in the input.
+ final boolean visible = active && (editText.getText().length() > 0);
+ clearButton.setVisibility(visible ? View.VISIBLE : View.GONE);
+ engineIcon.setVisibility(visible ? View.GONE : View.VISIBLE);
+ }
+
+ public void setTextListener(TextListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent e) {
+ // When the view is active, pass touch events to child views.
+ // Otherwise, intercept touch events to allow click listeners on the view to
+ // fire no matter where the user clicks.
+ return !active;
+ }
+}
diff --git a/mobile/android/search/java/org/mozilla/search/autocomplete/SuggestionsFragment.java b/mobile/android/search/java/org/mozilla/search/autocomplete/SuggestionsFragment.java
new file mode 100644
index 0000000000..ce935e4371
--- /dev/null
+++ b/mobile/android/search/java/org/mozilla/search/autocomplete/SuggestionsFragment.java
@@ -0,0 +1,263 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.search.autocomplete;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.support.v4.content.ContextCompat;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.search.SearchEngine;
+import org.mozilla.gecko.SuggestClient;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.search.AcceptsSearchQuery;
+import org.mozilla.search.AcceptsSearchQuery.SuggestionAnimation;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.AsyncTaskLoader;
+import android.support.v4.content.Loader;
+import android.text.SpannableString;
+import android.text.style.ForegroundColorSpan;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ListView;
+
+/**
+ * A fragment to show search suggestions.
+ */
+public class SuggestionsFragment extends Fragment {
+
+ private static final String LOG_TAG = "SuggestionsFragment";
+
+ private static final int LOADER_ID_SUGGESTION = 0;
+ private static final String KEY_SEARCH_TERM = "search_term";
+
+ // Timeout for the suggestion client to respond
+ private static final int SUGGESTION_TIMEOUT = 3000;
+
+ // Number of search suggestions to show.
+ private static final int SUGGESTION_MAX = 5;
+
+ public static final String GECKO_SEARCH_TERMS_URL_PARAM = "__searchTerms__";
+
+ private AcceptsSearchQuery searchListener;
+
+ // Suggest client gets setup outside of the normal fragment lifecycle, therefore
+ // clients should ensure that this isn't null before using it.
+ private SuggestClient suggestClient;
+ private SuggestionLoaderCallbacks suggestionLoaderCallbacks;
+
+ private AutoCompleteAdapter autoCompleteAdapter;
+
+ // Holds the list of search suggestions.
+ private ListView suggestionsList;
+
+ public SuggestionsFragment() {
+ // Required empty public constructor
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+
+ if (context instanceof AcceptsSearchQuery) {
+ searchListener = (AcceptsSearchQuery) context;
+ } else {
+ throw new ClassCastException(context.toString() + " must implement AcceptsSearchQuery.");
+ }
+
+ suggestionLoaderCallbacks = new SuggestionLoaderCallbacks();
+ autoCompleteAdapter = new AutoCompleteAdapter(context);
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+
+ searchListener = null;
+ suggestionLoaderCallbacks = null;
+ autoCompleteAdapter = null;
+ suggestClient = null;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ suggestionsList = (ListView) inflater.inflate(R.layout.search_sugestions, container, false);
+ suggestionsList.setAdapter(autoCompleteAdapter);
+
+ // Attach listener for tapping on a suggestion.
+ suggestionsList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ final Suggestion suggestion = (Suggestion) suggestionsList.getItemAtPosition(position);
+
+ final Rect startBounds = new Rect();
+ view.getGlobalVisibleRect(startBounds);
+
+ // The user tapped on a suggestion from the search engine.
+ Telemetry.sendUIEvent(TelemetryContract.Event.SEARCH, TelemetryContract.Method.SUGGESTION, position);
+
+ searchListener.onSearch(suggestion.value, new SuggestionAnimation() {
+ @Override
+ public Rect getStartBounds() {
+ return startBounds;
+ }
+ });
+ }
+ });
+
+ return suggestionsList;
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+
+ if (null != suggestionsList) {
+ suggestionsList.setOnItemClickListener(null);
+ suggestionsList.setAdapter(null);
+ suggestionsList = null;
+ }
+ }
+
+ public void setEngine(SearchEngine engine) {
+ suggestClient = new SuggestClient(getActivity(), engine.getSuggestionTemplate(GECKO_SEARCH_TERMS_URL_PARAM),
+ SUGGESTION_TIMEOUT, SUGGESTION_MAX, true);
+ }
+
+ public void loadSuggestions(String query) {
+ final Bundle args = new Bundle();
+ args.putString(KEY_SEARCH_TERM, query);
+ final LoaderManager loaderManager = getLoaderManager();
+
+ // Ensure that we don't try to restart a loader that doesn't exist. This becomes
+ // an issue because SuggestionLoaderCallbacks.onCreateLoader can return null
+ // as a loader if we don't have a suggestClient available yet.
+ if (loaderManager.getLoader(LOADER_ID_SUGGESTION) == null) {
+ loaderManager.initLoader(LOADER_ID_SUGGESTION, args, suggestionLoaderCallbacks);
+ } else {
+ loaderManager.restartLoader(LOADER_ID_SUGGESTION, args, suggestionLoaderCallbacks);
+ }
+ }
+
+ public static class Suggestion {
+
+ public final String value;
+ public final SpannableString display;
+ public final ForegroundColorSpan colorSpan;
+
+ public Suggestion(String value, String searchTerm, int suggestionHighlightColor) {
+ this.value = value;
+
+ display = new SpannableString(value);
+
+ colorSpan = new ForegroundColorSpan(suggestionHighlightColor);
+
+ // Highlight mixed-case matches.
+ final int start = value.toLowerCase().indexOf(searchTerm.toLowerCase());
+ if (start >= 0) {
+ display.setSpan(colorSpan, start, start + searchTerm.length(), 0);
+ }
+ }
+ }
+
+ private class SuggestionLoaderCallbacks implements LoaderManager.LoaderCallbacks<List<Suggestion>> {
+ @Override
+ public Loader<List<Suggestion>> onCreateLoader(int id, Bundle args) {
+ // We drop the user's search if suggestclient isn't ready. This happens if the
+ // user is really fast and starts typing before we can read shared prefs.
+ if (suggestClient != null) {
+ return new SuggestionAsyncLoader(getActivity(), suggestClient, args.getString(KEY_SEARCH_TERM));
+ }
+ Log.e(LOG_TAG, "Autocomplete setup failed; suggestClient not ready yet.");
+ return null;
+ }
+
+ @Override
+ public void onLoadFinished(Loader<List<Suggestion>> loader, List<Suggestion> suggestions) {
+ // Only show the ListView if there are suggestions in it.
+ if (suggestions.size() > 0) {
+ autoCompleteAdapter.update(suggestions);
+ suggestionsList.setVisibility(View.VISIBLE);
+ } else {
+ suggestionsList.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<List<Suggestion>> loader) { }
+ }
+
+ private static class SuggestionAsyncLoader extends AsyncTaskLoader<List<Suggestion>> {
+ private final SuggestClient suggestClient;
+ private final String searchTerm;
+ private List<Suggestion> suggestions;
+ private final int suggestionHighlightColor;
+
+ public SuggestionAsyncLoader(Context context, SuggestClient suggestClient, String searchTerm) {
+ super(context);
+ this.suggestClient = suggestClient;
+ this.searchTerm = searchTerm;
+ this.suggestions = null;
+
+ // Color of search term match in search suggestion
+ suggestionHighlightColor = ContextCompat.getColor(context, R.color.suggestion_highlight);
+ }
+
+ @Override
+ public List<Suggestion> loadInBackground() {
+ final List<String> values = suggestClient.query(searchTerm);
+
+ final List<Suggestion> result = new ArrayList<Suggestion>(values.size());
+ for (String value : values) {
+ result.add(new Suggestion(value, searchTerm, suggestionHighlightColor));
+ }
+
+ return result;
+ }
+
+ @Override
+ public void deliverResult(List<Suggestion> suggestions) {
+ this.suggestions = suggestions;
+
+ if (isStarted()) {
+ super.deliverResult(suggestions);
+ }
+ }
+
+ @Override
+ protected void onStartLoading() {
+ if (suggestions != null) {
+ deliverResult(suggestions);
+ }
+
+ if (takeContentChanged() || suggestions == null) {
+ forceLoad();
+ }
+ }
+
+ @Override
+ protected void onStopLoading() {
+ cancelLoad();
+ }
+
+ @Override
+ protected void onReset() {
+ super.onReset();
+
+ onStopLoading();
+ suggestions = null;
+ }
+ }
+}
diff --git a/mobile/android/search/java/org/mozilla/search/ui/BackCaptureEditText.java b/mobile/android/search/java/org/mozilla/search/ui/BackCaptureEditText.java
new file mode 100644
index 0000000000..727ad81057
--- /dev/null
+++ b/mobile/android/search/java/org/mozilla/search/ui/BackCaptureEditText.java
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.search.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.widget.EditText;
+
+/**
+ * An EditText subclass that loses focus when the keyboard
+ * is dismissed.
+ */
+public class BackCaptureEditText extends EditText {
+ public BackCaptureEditText(Context context) {
+ super(context);
+ }
+
+ public BackCaptureEditText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public BackCaptureEditText(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
+ clearFocus();
+ }
+ return super.onKeyPreIme(keyCode, event);
+ }
+}
diff --git a/mobile/android/search/java/org/mozilla/search/ui/FacetBar.java b/mobile/android/search/java/org/mozilla/search/ui/FacetBar.java
new file mode 100644
index 0000000000..7fcf3dc9bb
--- /dev/null
+++ b/mobile/android/search/java/org/mozilla/search/ui/FacetBar.java
@@ -0,0 +1,124 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.search.ui;
+
+import org.mozilla.gecko.R;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+
+public class FacetBar extends RadioGroup {
+
+ // Ensure facets have equal width and match the bar's height. Supplying these
+ // in styles.xml/FacetButtonStyle does not work. See:
+ // http://stackoverflow.com/questions/24213193/android-ignores-layout-weight-parameter-from-styles-xml
+ private static final RadioGroup.LayoutParams FACET_LAYOUT_PARAMS =
+ new RadioGroup.LayoutParams(0, LayoutParams.MATCH_PARENT, 1.0f);
+
+ // A loud default color to make it obvious that setUnderlineColor should be called.
+ private int underlineColor = Color.RED;
+
+ // Used for assigning unique view ids when facet buttons are being created.
+ private int nextButtonId = 0;
+
+ public FacetBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ /**
+ * Add a new button to the facet bar.
+ *
+ * @param facetName The text to be used in the button.
+ */
+ public void addFacet(String facetName) {
+ addFacet(facetName, false);
+ }
+
+ /**
+ * Add a new button to the facet bar.
+ *
+ * @param facetName The text to be used in the button.
+ * @param checked Whether the button should be checked. If true, the
+ * onCheckChange listener *will* be fired.
+ */
+ public void addFacet(String facetName, boolean checked) {
+ final FacetButton button = new FacetButton(getContext(), facetName, underlineColor);
+
+ // The ids are used internally by RadioGroup to manage which button is
+ // currently checked. Since we are programmatically creating the buttons,
+ // we need to manually assign an id.
+ button.setId(nextButtonId++);
+
+ // Ensure the buttons are equally spaced.
+ button.setLayoutParams(FACET_LAYOUT_PARAMS);
+
+ // If true, this will fire the onCheckChange listener.
+ button.setChecked(checked);
+
+ addView(button);
+ }
+
+ /**
+ * Update the brand color for all of the buttons.
+ */
+ public void setUnderlineColor(int underlineColor) {
+ this.underlineColor = underlineColor;
+
+ if (getChildCount() > 0) {
+ for (int i = 0; i < getChildCount(); i++) {
+ ((FacetButton) getChildAt(i)).setUnderlineColor(underlineColor);
+ }
+ }
+ }
+
+ /**
+ * A custom TextView that includes a bottom border. The bottom border
+ * can have a custom color and thickness.
+ */
+ private static class FacetButton extends RadioButton {
+
+ private final Paint underlinePaint = new Paint();
+
+ public FacetButton(Context context, String text, int color) {
+ super(context, null, R.attr.facetButtonStyle);
+
+ setText(text);
+
+ underlinePaint.setStyle(Paint.Style.STROKE);
+ underlinePaint.setStrokeWidth(getResources().getDimension(R.dimen.facet_button_underline_thickness));
+ underlinePaint.setColor(color);
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ super.setChecked(checked);
+
+ // Force the button to redraw to update the underline state.
+ invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (isChecked()) {
+ // Translate the line upward so that it isn't clipped by the button's boundary.
+ // We divide by 2 since, without offset, the line would be drawn with its
+ // midpoint at the bottom of the button -- half of the stroke going up,
+ // and half of the stroke getting clipped.
+ final float yPos = getHeight() - underlinePaint.getStrokeWidth() / 2;
+ canvas.drawLine(0, yPos, getWidth(), yPos, underlinePaint);
+ }
+ }
+
+ public void setUnderlineColor(int color) {
+ underlinePaint.setColor(color);
+ }
+ }
+}
diff --git a/mobile/android/search/manifests/SearchAndroidManifest_activities.xml.in b/mobile/android/search/manifests/SearchAndroidManifest_activities.xml.in
new file mode 100644
index 0000000000..21aee71c0f
--- /dev/null
+++ b/mobile/android/search/manifests/SearchAndroidManifest_activities.xml.in
@@ -0,0 +1,61 @@
+ <activity
+ android:name="@MOZ_ANDROID_SEARCH_INTENT_CLASS@"
+ android:process=":search"
+ android:launchMode="singleTop"
+ android:taskAffinity="@ANDROID_PACKAGE_NAME@.SEARCH"
+ android:icon="@drawable/search_launcher"
+ android:label="@string/search_app_name"
+ android:configChanges="orientation|screenSize"
+ android:theme="@style/AppTheme">
+ <intent-filter>
+ <action android:name="android.intent.action.ASSIST"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+
+ <!-- Pre Lollipop devices display a generic search icon, if none is
+ provided here. To use this we need to set the resource to 0.
+ For Lollipop and later the search launcher icon ist used.
+ To retrieve the resource value the Bundle.getInt() method is
+ used, so we use integer resources instead of drawables, because
+ setting a drawable referenced to 0 results in errors when used
+ as a real drawable resource somewhere else. -->
+ <meta-data
+ android:name="com.android.systemui.action_assist_icon"
+ android:resource="@integer/search_assist_launch_res"/>
+ </activity>
+
+ <!-- Basic launcher widget. -->
+ <receiver android:name="org.mozilla.search.SearchWidget"
+ android:label="@string/search_widget_name">
+
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+ </intent-filter>
+
+ <intent-filter>
+ <action android:name="org.mozilla.widget.LAUNCH_BROWSER"/>
+ </intent-filter>
+
+ <intent-filter>
+ <action android:name="org.mozilla.widget.LAUNCH_SEARCH"/>
+ </intent-filter>
+
+ <intent-filter>
+ <action android:name="org.mozilla.widget.LAUNCH_NEW_TAB"/>
+ </intent-filter>
+
+ <meta-data android:name="android.appwidget.provider" android:resource="@xml/search_widget_info" />
+ </receiver>
+
+ <activity
+ android:name="org.mozilla.search.SearchPreferenceActivity"
+ android:process=":search"
+ android:logo="@drawable/search_launcher"
+ android:label="@string/search_pref_title"
+ android:parentActivityName="@MOZ_ANDROID_SEARCH_INTENT_CLASS@"
+ android:theme="@style/SettingsTheme" >
+ <meta-data
+ android:name="android.support.PARENT_ACTIVITY"
+ android:value="@MOZ_ANDROID_SEARCH_INTENT_CLASS@"/>
+ </activity>
diff --git a/mobile/android/search/manifests/SearchAndroidManifest_permissions.xml.in b/mobile/android/search/manifests/SearchAndroidManifest_permissions.xml.in
new file mode 100644
index 0000000000..a0abb99fae
--- /dev/null
+++ b/mobile/android/search/manifests/SearchAndroidManifest_permissions.xml.in
@@ -0,0 +1,3 @@
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.INTERNET" />
diff --git a/mobile/android/search/manifests/SearchAndroidManifest_services.xml.in b/mobile/android/search/manifests/SearchAndroidManifest_services.xml.in
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/mobile/android/search/manifests/SearchAndroidManifest_services.xml.in
diff --git a/mobile/android/search/search_activity_sources.mozbuild b/mobile/android/search/search_activity_sources.mozbuild
new file mode 100644
index 0000000000..4805a25ad8
--- /dev/null
+++ b/mobile/android/search/search_activity_sources.mozbuild
@@ -0,0 +1,20 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+search_activity_sources = [
+ 'java/org/mozilla/search/AcceptsSearchQuery.java',
+ 'java/org/mozilla/search/autocomplete/AutoCompleteAdapter.java',
+ 'java/org/mozilla/search/autocomplete/SearchBar.java',
+ 'java/org/mozilla/search/autocomplete/SuggestionsFragment.java',
+ 'java/org/mozilla/search/Constants.java',
+ 'java/org/mozilla/search/PostSearchFragment.java',
+ 'java/org/mozilla/search/PreSearchFragment.java',
+ 'java/org/mozilla/search/SearchActivity.java',
+ 'java/org/mozilla/search/SearchPreferenceActivity.java',
+ 'java/org/mozilla/search/SearchWidget.java',
+ 'java/org/mozilla/search/ui/BackCaptureEditText.java',
+ 'java/org/mozilla/search/ui/FacetBar.java',
+]
diff --git a/mobile/android/search/strings/search_strings.xml.in b/mobile/android/search/strings/search_strings.xml.in
new file mode 100644
index 0000000000..ad0e7e57db
--- /dev/null
+++ b/mobile/android/search/strings/search_strings.xml.in
@@ -0,0 +1,20 @@
+ <string name="search_plus_content_description">&search_plus_content_description;</string>
+
+ <string name="search_app_name">&search_app_name;</string>
+ <string name="search_bar_hint">&search_bar_hint;</string>
+
+ <string name="search_empty_title">&search_empty_title2;</string>
+ <string name="search_empty_message">&search_empty_message;</string>
+
+ <string name="search_pref_title">&search_pref_title;</string>
+ <string name="search_pref_button_content_description">&search_pref_button_content_description;</string>
+
+ <string name="pref_clearHistory_confirmation">&pref_clearHistory_confirmation;</string>
+ <string name="pref_clearHistory_dialogMessage">&pref_clearHistory_dialogMessage;</string>
+ <string name="pref_clearHistory_title">&pref_clearHistory_title;</string>
+
+ <string name="search_widget_name">&search_app_name;</string>
+ <string name="search_widget_button_label">&search_widget_button_label;</string>
+
+ <string name="network_error_title">&network_error_title;</string>
+ <string name="network_error_message">&network_error_message;</string>
diff --git a/mobile/android/services/README.txt b/mobile/android/services/README.txt
new file mode 100644
index 0000000000..cf4624ca4b
--- /dev/null
+++ b/mobile/android/services/README.txt
@@ -0,0 +1 @@
+These files are managed in the android-sync repo. Do not modify directly, or your changes will be lost.
diff --git a/mobile/android/services/manifests/FxAccountAndroidManifest_activities.xml.in b/mobile/android/services/manifests/FxAccountAndroidManifest_activities.xml.in
new file mode 100644
index 0000000000..ad9542ad3d
--- /dev/null
+++ b/mobile/android/services/manifests/FxAccountAndroidManifest_activities.xml.in
@@ -0,0 +1,63 @@
+ <activity
+ android:theme="@style/FxAccountTheme.FxAccountStatusActivity"
+ android:label="@string/fxaccount_status_activity_label"
+ android:clearTaskOnLaunch="true"
+ android:taskAffinity="@ANDROID_PACKAGE_NAME@.FXA"
+ android:name="org.mozilla.gecko.fxa.activities.FxAccountStatusActivity"
+ android:configChanges="locale|layoutDirection"
+ android:windowSoftInputMode="adjustResize">
+ <!-- Adding a launcher will make this activity appear on the
+ Apps screen, which we only want when testing. -->
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <!-- <category android:name="android.intent.category.LAUNCHER" /> -->
+ </intent-filter>
+ <intent-filter>
+ <action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_STATUS"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ </activity>
+
+ <receiver
+ android:name="org.mozilla.gecko.fxa.receivers.FxAccountUpgradeReceiver">
+ <intent-filter>
+ <action android:name="android.intent.action.PACKAGE_REPLACED" />
+ <data android:scheme="package"/>
+ </intent-filter>
+ </receiver>
+
+ <activity
+ android:exported="false"
+ android:name="org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivityWeb">
+ <intent-filter>
+ <action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_GET_STARTED"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:exported="false"
+ android:name="org.mozilla.gecko.fxa.activities.FxAccountUpdateCredentialsActivityWeb">
+ <intent-filter>
+ <action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_UPDATE_CREDENTIALS"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:exported="false"
+ android:name="org.mozilla.gecko.fxa.activities.FxAccountFinishMigratingActivityWeb">
+ <intent-filter>
+ <action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_FINISH_MIGRATING"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:exported="false"
+ android:name="org.mozilla.gecko.fxa.activities.FxAccountConfirmAccountActivityWeb">
+ <intent-filter>
+ <action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_CONFIRM_ACCOUNT"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ </activity>
diff --git a/mobile/android/services/manifests/FxAccountAndroidManifest_permissions.xml.in b/mobile/android/services/manifests/FxAccountAndroidManifest_permissions.xml.in
new file mode 100644
index 0000000000..d5c7e3e5c7
--- /dev/null
+++ b/mobile/android/services/manifests/FxAccountAndroidManifest_permissions.xml.in
@@ -0,0 +1,18 @@
+ <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+ <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
+ <uses-permission android:name="android.permission.USE_CREDENTIALS" />
+ <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
+ <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
+ <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+ <uses-permission android:name="android.permission.READ_SYNC_STATS" />
+ <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
+
+ <!-- A signature level permission granted only to the Firefox
+ channels sharing an Android Account type. -->
+ <permission
+ android:name="@ANDROID_PACKAGE_NAME@_fxaccount.permission.PER_ACCOUNT_TYPE"
+ android:protectionLevel="signature">
+ </permission>
+
+ <uses-permission android:name="@ANDROID_PACKAGE_NAME@_fxaccount.permission.PER_ACCOUNT_TYPE" />
diff --git a/mobile/android/services/manifests/FxAccountAndroidManifest_services.xml.in b/mobile/android/services/manifests/FxAccountAndroidManifest_services.xml.in
new file mode 100644
index 0000000000..a109d1ba30
--- /dev/null
+++ b/mobile/android/services/manifests/FxAccountAndroidManifest_services.xml.in
@@ -0,0 +1,34 @@
+ <service
+ android:exported="true"
+ android:name="org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticatorService" >
+ <intent-filter >
+ <action android:name="android.accounts.AccountAuthenticator" />
+ </intent-filter>
+
+ <meta-data
+ android:name="android.accounts.AccountAuthenticator"
+ android:resource="@xml/fxaccount_authenticator" />
+ </service>
+
+ <service
+ android:exported="false"
+ android:name="org.mozilla.gecko.fxa.receivers.FxAccountDeletedService" >
+ </service>
+
+ <service
+ android:exported="false"
+ android:name="org.mozilla.gecko.fxa.sync.FxAccountProfileService" >
+ </service>
+
+ <!-- Firefox Sync. -->
+ <service
+ android:exported="false"
+ android:name="org.mozilla.gecko.fxa.sync.FxAccountSyncService" >
+ <intent-filter >
+ <action android:name="android.content.SyncAdapter" />
+ </intent-filter>
+
+ <meta-data
+ android:name="android.content.SyncAdapter"
+ android:resource="@xml/fxaccount_syncadapter" />
+ </service> \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/ReadingListConstants.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/ReadingListConstants.java
new file mode 100644
index 0000000000..df603a58ee
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/ReadingListConstants.java
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background;
+
+import org.mozilla.gecko.AppConstants;
+
+/**
+ * This is in 'background' not 'reading' so that it's still usable even when the
+ * Reading List feature is build-time disabled.
+ */
+public class ReadingListConstants {
+ public static final String GLOBAL_LOG_TAG = "FxReadingList";
+ public static final String USER_AGENT = "Firefox-Android-FxReader/" + AppConstants.MOZ_APP_VERSION + " (" + AppConstants.MOZ_APP_UA_NAME + ")";
+ public static final String DEFAULT_DEV_ENDPOINT = "https://readinglist.dev.mozaws.net/v1/";
+ public static final String DEFAULT_PROD_ENDPOINT = "https://readinglist.services.mozilla.com/v1/";
+
+ public static final String OAUTH_SCOPE_READINGLIST = "readinglist";
+ public static final String AUTH_TOKEN_TYPE = "oauth::" + OAUTH_SCOPE_READINGLIST;
+
+ public static boolean DEBUG = false;
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/EditorBranch.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/EditorBranch.java
new file mode 100644
index 0000000000..1ead09afae
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/EditorBranch.java
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.common;
+
+import java.util.Set;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+
+public class EditorBranch implements Editor {
+
+ private final String prefix;
+ private Editor editor;
+
+ public EditorBranch(final SharedPreferences prefs, final String prefix) {
+ if (!prefix.endsWith(".")) {
+ throw new IllegalArgumentException("No trailing period in prefix.");
+ }
+ this.prefix = prefix;
+ this.editor = prefs.edit();
+ }
+
+ @Override
+ public void apply() {
+ this.editor.apply();
+ }
+
+ @Override
+ public Editor clear() {
+ this.editor = this.editor.clear();
+ return this;
+ }
+
+ @Override
+ public boolean commit() {
+ return this.editor.commit();
+ }
+
+ @Override
+ public Editor putBoolean(String key, boolean value) {
+ this.editor = this.editor.putBoolean(prefix + key, value);
+ return this;
+ }
+
+ @Override
+ public Editor putFloat(String key, float value) {
+ this.editor = this.editor.putFloat(prefix + key, value);
+ return this;
+ }
+
+ @Override
+ public Editor putInt(String key, int value) {
+ this.editor = this.editor.putInt(prefix + key, value);
+ return this;
+ }
+
+ @Override
+ public Editor putLong(String key, long value) {
+ this.editor = this.editor.putLong(prefix + key, value);
+ return this;
+ }
+
+ @Override
+ public Editor putString(String key, String value) {
+ this.editor = this.editor.putString(prefix + key, value);
+ return this;
+ }
+
+ // Not marking as Override, because Android <= 10 doesn't have
+ // putStringSet. Neither can we implement it.
+ public Editor putStringSet(String key, Set<String> value) {
+ throw new RuntimeException("putStringSet not available.");
+ }
+
+ @Override
+ public Editor remove(String key) {
+ this.editor = this.editor.remove(prefix + key);
+ return this;
+ }
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/GlobalConstants.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/GlobalConstants.java
new file mode 100644
index 0000000000..d661e62dc7
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/GlobalConstants.java
@@ -0,0 +1,90 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.common;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.AppConstants.Versions;
+
+/**
+ * Constant values common to all Android services.
+ */
+public class GlobalConstants {
+ public static final String BROWSER_INTENT_PACKAGE = AppConstants.ANDROID_PACKAGE_NAME;
+ public static final String BROWSER_INTENT_CLASS = AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS;
+
+ public static final int SHARED_PREFERENCES_MODE = 0;
+
+ // Common time values.
+ public static final long MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
+ public static final long MILLISECONDS_PER_SIX_MONTHS = 180 * MILLISECONDS_PER_DAY;
+
+ // Acceptable cipher suites.
+ /**
+ * We support only a very limited range of strong cipher suites and protocols:
+ * no SSLv3 or TLSv1.0 (if we can), no DHE ciphers that might be vulnerable to Logjam
+ * (https://weakdh.org/), no RC4.
+ *
+ * Backstory: Bug 717691 (we no longer support Android 2.2, so the name
+ * workaround is unnecessary), Bug 1081953, Bug 1061273, Bug 1166839.
+ *
+ * See <http://developer.android.com/reference/javax/net/ssl/SSLSocket.html> for
+ * supported Android versions for each set of protocols and cipher suites.
+ *
+ * Note that currently we need to support connections to Sync 1.1 on Mozilla-hosted infra,
+ * as well as connections to FxA and Sync 1.5 on AWS.
+ *
+ * ELB cipher suites:
+ * <http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/elb-security-policy-table.html>
+ */
+ public static final String[] DEFAULT_CIPHER_SUITES;
+ public static final String[] DEFAULT_PROTOCOLS;
+
+ static {
+ // Prioritize 128 over 256 as a tradeoff between device CPU/battery and the minor
+ // increase in strength.
+ if (Versions.feature20Plus) {
+ DEFAULT_CIPHER_SUITES = new String[]
+ {
+ "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", // 20+
+ "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", // 20+
+ "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", // 20+
+ "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", // 11+
+ "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", // 20+
+ "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", // 20+
+ "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", // 11+
+
+ // For Sync 1.1.
+ "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", // 9+
+ "TLS_RSA_WITH_AES_128_CBC_SHA", // 9+
+ };
+ } else {
+ DEFAULT_CIPHER_SUITES = new String[]
+ {
+ "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", // 11+
+ "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", // 11+
+ "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", // 11+
+
+ // For Sync 1.1.
+ "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", // 9+
+ "TLS_RSA_WITH_AES_128_CBC_SHA", // 9+
+ };
+ }
+
+ if (Versions.feature16Plus) {
+ DEFAULT_PROTOCOLS = new String[]
+ {
+ "TLSv1.2",
+ "TLSv1.1",
+ "TLSv1", // We would like to remove this, and will do so when we can.
+ };
+ } else {
+ // Fall back to TLSv1 if there's nothing better.
+ DEFAULT_PROTOCOLS = new String[]
+ {
+ "TLSv1",
+ };
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/PrefsBranch.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/PrefsBranch.java
new file mode 100644
index 0000000000..78d5f61a1f
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/PrefsBranch.java
@@ -0,0 +1,83 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.common;
+
+import java.util.Map;
+import java.util.Set;
+
+import android.content.SharedPreferences;
+
+/**
+ * A wrapper around a portion of the SharedPreferences space.
+ */
+public class PrefsBranch implements SharedPreferences {
+ private final SharedPreferences prefs;
+ private final String prefix; // Including trailing period.
+
+ public PrefsBranch(SharedPreferences prefs, String prefix) {
+ if (!prefix.endsWith(".")) {
+ throw new IllegalArgumentException("No trailing period in prefix.");
+ }
+ this.prefs = prefs;
+ this.prefix = prefix;
+ }
+
+ @Override
+ public boolean contains(String key) {
+ return prefs.contains(prefix + key);
+ }
+
+ @Override
+ public Editor edit() {
+ return new EditorBranch(prefs, prefix);
+ }
+
+ @Override
+ public Map<String, ?> getAll() {
+ // Not implemented. TODO
+ return null;
+ }
+
+ @Override
+ public boolean getBoolean(String key, boolean defValue) {
+ return prefs.getBoolean(prefix + key, defValue);
+ }
+
+ @Override
+ public float getFloat(String key, float defValue) {
+ return prefs.getFloat(prefix + key, defValue);
+ }
+
+ @Override
+ public int getInt(String key, int defValue) {
+ return prefs.getInt(prefix + key, defValue);
+ }
+
+ @Override
+ public long getLong(String key, long defValue) {
+ return prefs.getLong(prefix + key, defValue);
+ }
+
+ @Override
+ public String getString(String key, String defValue) {
+ return prefs.getString(prefix + key, defValue);
+ }
+
+ // Not marking as Override, because Android <= 10 doesn't have
+ // getStringSet. Neither can we implement it.
+ public Set<String> getStringSet(String key, Set<String> defValue) {
+ throw new RuntimeException("getStringSet not available.");
+ }
+
+ @Override
+ public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+ prefs.registerOnSharedPreferenceChangeListener(listener);
+ }
+
+ @Override
+ public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+ prefs.unregisterOnSharedPreferenceChangeListener(listener);
+ }
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/Logger.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/Logger.java
new file mode 100644
index 0000000000..2575717eb0
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/Logger.java
@@ -0,0 +1,232 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.common.log;
+
+import java.io.PrintWriter;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import org.mozilla.gecko.background.common.GlobalConstants;
+import org.mozilla.gecko.background.common.log.writers.AndroidLevelCachingLogWriter;
+import org.mozilla.gecko.background.common.log.writers.AndroidLogWriter;
+import org.mozilla.gecko.background.common.log.writers.LogWriter;
+import org.mozilla.gecko.background.common.log.writers.PrintLogWriter;
+import org.mozilla.gecko.background.common.log.writers.SimpleTagLogWriter;
+import org.mozilla.gecko.background.common.log.writers.ThreadLocalTagLogWriter;
+
+import android.util.Log;
+
+/**
+ * Logging helper class. Serializes all log operations (by synchronizing).
+ */
+public class Logger {
+ public static final String LOGGER_TAG = "Logger";
+ public static final String DEFAULT_LOG_TAG = "GeckoLogger";
+
+ // For extra debugging.
+ public static boolean LOG_PERSONAL_INFORMATION = false;
+
+ /**
+ * Allow each thread to use its own global log tag. This allows
+ * independent services to log as different sources.
+ *
+ * When your thread sets up logging, it should do something like the following:
+ *
+ * Logger.setThreadLogTag("MyTag");
+ *
+ * The value is inheritable, so worker threads and such do not need to
+ * set the same log tag as their parent.
+ */
+ private static final InheritableThreadLocal<String> logTag = new InheritableThreadLocal<String>() {
+ @Override
+ protected String initialValue() {
+ return DEFAULT_LOG_TAG;
+ }
+ };
+
+ public static void setThreadLogTag(final String logTag) {
+ Logger.logTag.set(logTag);
+ }
+ public static String getThreadLogTag() {
+ return Logger.logTag.get();
+ }
+
+ /**
+ * Current set of writers to which we will log.
+ * <p>
+ * We want logging to be available while running tests, so we initialize
+ * this set statically.
+ */
+ protected final static Set<LogWriter> logWriters;
+ static {
+ final Set<LogWriter> defaultWriters = Logger.defaultLogWriters();
+ logWriters = new LinkedHashSet<LogWriter>(defaultWriters);
+ }
+
+ /**
+ * Default set of log writers to log to.
+ */
+ public final static Set<LogWriter> defaultLogWriters() {
+ final String processedPackage = GlobalConstants.BROWSER_INTENT_PACKAGE.replace("org.mozilla.", "");
+
+ final Set<LogWriter> defaultLogWriters = new LinkedHashSet<LogWriter>();
+
+ final LogWriter log = new AndroidLogWriter();
+ final LogWriter cache = new AndroidLevelCachingLogWriter(log);
+
+ final LogWriter single = new SimpleTagLogWriter(processedPackage, new ThreadLocalTagLogWriter(Logger.logTag, cache));
+
+ defaultLogWriters.add(single);
+ return defaultLogWriters;
+ }
+
+ public static synchronized void startLoggingTo(LogWriter logWriter) {
+ logWriters.add(logWriter);
+ }
+
+ public static synchronized void startLoggingToWriters(Set<LogWriter> writers) {
+ logWriters.addAll(writers);
+ }
+
+ public static synchronized void stopLoggingTo(LogWriter logWriter) {
+ try {
+ logWriter.close();
+ } catch (Exception e) {
+ Log.e(LOGGER_TAG, "Got exception closing and removing LogWriter " + logWriter + ".", e);
+ }
+ logWriters.remove(logWriter);
+ }
+
+ public static synchronized void stopLoggingToAll() {
+ for (LogWriter logWriter : logWriters) {
+ try {
+ logWriter.close();
+ } catch (Exception e) {
+ Log.e(LOGGER_TAG, "Got exception closing and removing LogWriter " + logWriter + ".", e);
+ }
+ }
+ logWriters.clear();
+ }
+
+ /**
+ * Write to only the default log writers.
+ */
+ public static synchronized void resetLogging() {
+ stopLoggingToAll();
+ logWriters.addAll(Logger.defaultLogWriters());
+ }
+
+ /**
+ * Start writing log output to stdout.
+ * <p>
+ * Use <code>resetLogging</code> to stop logging to stdout.
+ */
+ public static synchronized void startLoggingToConsole() {
+ setThreadLogTag("Test");
+ startLoggingTo(new PrintLogWriter(new PrintWriter(System.out, true)));
+ }
+
+ // Synchronized version for other classes to use.
+ public static synchronized boolean shouldLogVerbose(String logTag) {
+ for (LogWriter logWriter : logWriters) {
+ if (logWriter.shouldLogVerbose(logTag)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static void error(String tag, String message) {
+ Logger.error(tag, message, null);
+ }
+
+ public static void warn(String tag, String message) {
+ Logger.warn(tag, message, null);
+ }
+
+ public static void info(String tag, String message) {
+ Logger.info(tag, message, null);
+ }
+
+ public static void debug(String tag, String message) {
+ Logger.debug(tag, message, null);
+ }
+
+ public static void trace(String tag, String message) {
+ Logger.trace(tag, message, null);
+ }
+
+ public static void pii(String tag, String message) {
+ if (LOG_PERSONAL_INFORMATION) {
+ Logger.debug(tag, "$$PII$$: " + message);
+ }
+ }
+
+ public static synchronized void error(String tag, String message, Throwable error) {
+ Iterator<LogWriter> it = logWriters.iterator();
+ while (it.hasNext()) {
+ LogWriter writer = it.next();
+ try {
+ writer.error(tag, message, error);
+ } catch (Exception e) {
+ Log.e(LOGGER_TAG, "Got exception logging; removing LogWriter " + writer + ".", e);
+ it.remove();
+ }
+ }
+ }
+
+ public static synchronized void warn(String tag, String message, Throwable error) {
+ Iterator<LogWriter> it = logWriters.iterator();
+ while (it.hasNext()) {
+ LogWriter writer = it.next();
+ try {
+ writer.warn(tag, message, error);
+ } catch (Exception e) {
+ Log.e(LOGGER_TAG, "Got exception logging; removing LogWriter " + writer + ".", e);
+ it.remove();
+ }
+ }
+ }
+
+ public static synchronized void info(String tag, String message, Throwable error) {
+ Iterator<LogWriter> it = logWriters.iterator();
+ while (it.hasNext()) {
+ LogWriter writer = it.next();
+ try {
+ writer.info(tag, message, error);
+ } catch (Exception e) {
+ Log.e(LOGGER_TAG, "Got exception logging; removing LogWriter " + writer + ".", e);
+ it.remove();
+ }
+ }
+ }
+
+ public static synchronized void debug(String tag, String message, Throwable error) {
+ Iterator<LogWriter> it = logWriters.iterator();
+ while (it.hasNext()) {
+ LogWriter writer = it.next();
+ try {
+ writer.debug(tag, message, error);
+ } catch (Exception e) {
+ Log.e(LOGGER_TAG, "Got exception logging; removing LogWriter " + writer + ".", e);
+ it.remove();
+ }
+ }
+ }
+
+ public static synchronized void trace(String tag, String message, Throwable error) {
+ Iterator<LogWriter> it = logWriters.iterator();
+ while (it.hasNext()) {
+ LogWriter writer = it.next();
+ try {
+ writer.trace(tag, message, error);
+ } catch (Exception e) {
+ Log.e(LOGGER_TAG, "Got exception logging; removing LogWriter " + writer + ".", e);
+ it.remove();
+ }
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/AndroidLevelCachingLogWriter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/AndroidLevelCachingLogWriter.java
new file mode 100644
index 0000000000..ac4250a03d
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/AndroidLevelCachingLogWriter.java
@@ -0,0 +1,132 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.common.log.writers;
+
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+import android.util.Log;
+
+/**
+ * Make a <code>LogWriter</code> only log when the Android log system says to.
+ */
+public class AndroidLevelCachingLogWriter extends LogWriter {
+ protected final LogWriter inner;
+
+ public AndroidLevelCachingLogWriter(LogWriter inner) {
+ this.inner = inner;
+ }
+
+ // I can't believe we have to implement this ourselves.
+ // These aren't synchronized (and neither are the setters) because
+ // the logging calls themselves are synchronized.
+ private Map<String, Boolean> isErrorLoggable = new IdentityHashMap<String, Boolean>();
+ private Map<String, Boolean> isWarnLoggable = new IdentityHashMap<String, Boolean>();
+ private Map<String, Boolean> isInfoLoggable = new IdentityHashMap<String, Boolean>();
+ private Map<String, Boolean> isDebugLoggable = new IdentityHashMap<String, Boolean>();
+ private Map<String, Boolean> isVerboseLoggable = new IdentityHashMap<String, Boolean>();
+
+ /**
+ * Empty the caches of log levels.
+ */
+ public void refreshLogLevels() {
+ isErrorLoggable = new IdentityHashMap<String, Boolean>();
+ isWarnLoggable = new IdentityHashMap<String, Boolean>();
+ isInfoLoggable = new IdentityHashMap<String, Boolean>();
+ isDebugLoggable = new IdentityHashMap<String, Boolean>();
+ isVerboseLoggable = new IdentityHashMap<String, Boolean>();
+ }
+
+ private boolean shouldLogError(String logTag) {
+ Boolean out = isErrorLoggable.get(logTag);
+ if (out != null) {
+ return out;
+ }
+ out = Log.isLoggable(logTag, Log.ERROR);
+ isErrorLoggable.put(logTag, out);
+ return out;
+ }
+
+ private boolean shouldLogWarn(String logTag) {
+ Boolean out = isWarnLoggable.get(logTag);
+ if (out != null) {
+ return out;
+ }
+ out = Log.isLoggable(logTag, Log.WARN);
+ isWarnLoggable.put(logTag, out);
+ return out;
+ }
+
+ private boolean shouldLogInfo(String logTag) {
+ Boolean out = isInfoLoggable.get(logTag);
+ if (out != null) {
+ return out;
+ }
+ out = Log.isLoggable(logTag, Log.INFO);
+ isInfoLoggable.put(logTag, out);
+ return out;
+ }
+
+ private boolean shouldLogDebug(String logTag) {
+ Boolean out = isDebugLoggable.get(logTag);
+ if (out != null) {
+ return out;
+ }
+ out = Log.isLoggable(logTag, Log.DEBUG);
+ isDebugLoggable.put(logTag, out);
+ return out;
+ }
+
+ @Override
+ public boolean shouldLogVerbose(String logTag) {
+ Boolean out = isVerboseLoggable.get(logTag);
+ if (out != null) {
+ return out;
+ }
+ out = Log.isLoggable(logTag, Log.VERBOSE);
+ isVerboseLoggable.put(logTag, out);
+ return out;
+ }
+
+ @Override
+ public void error(String tag, String message, Throwable error) {
+ if (shouldLogError(tag)) {
+ inner.error(tag, message, error);
+ }
+ }
+
+ @Override
+ public void warn(String tag, String message, Throwable error) {
+ if (shouldLogWarn(tag)) {
+ inner.warn(tag, message, error);
+ }
+ }
+
+ @Override
+ public void info(String tag, String message, Throwable error) {
+ if (shouldLogInfo(tag)) {
+ inner.info(tag, message, error);
+ }
+ }
+
+ @Override
+ public void debug(String tag, String message, Throwable error) {
+ if (shouldLogDebug(tag)) {
+ inner.debug(tag, message, error);
+ }
+ }
+
+ @Override
+ public void trace(String tag, String message, Throwable error) {
+ if (shouldLogVerbose(tag)) {
+ inner.trace(tag, message, error);
+ }
+ }
+
+ @Override
+ public void close() {
+ inner.close();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/AndroidLogWriter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/AndroidLogWriter.java
new file mode 100644
index 0000000000..9d309844da
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/AndroidLogWriter.java
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.common.log.writers;
+
+import android.util.Log;
+
+/**
+ * Log to the Android log.
+ */
+public class AndroidLogWriter extends LogWriter {
+ @Override
+ public boolean shouldLogVerbose(String logTag) {
+ return true;
+ }
+
+ @Override
+ public void error(String tag, String message, Throwable error) {
+ Log.e(tag, message, error);
+ }
+
+ @Override
+ public void warn(String tag, String message, Throwable error) {
+ Log.w(tag, message, error);
+ }
+
+ @Override
+ public void info(String tag, String message, Throwable error) {
+ Log.i(tag, message, error);
+ }
+
+ @Override
+ public void debug(String tag, String message, Throwable error) {
+ Log.d(tag, message, error);
+ }
+
+ @Override
+ public void trace(String tag, String message, Throwable error) {
+ Log.v(tag, message, error);
+ }
+
+ @Override
+ public void close() {
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/LevelFilteringLogWriter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/LevelFilteringLogWriter.java
new file mode 100644
index 0000000000..74c3608c45
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/LevelFilteringLogWriter.java
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.common.log.writers;
+
+import android.util.Log;
+
+/**
+ * A LogWriter that logs only if the message is as important as the specified
+ * level. For example, if the specified level is <code>Log.WARN</code>, only
+ * <code>warn</code> and <code>error</code> will log.
+ */
+public class LevelFilteringLogWriter extends LogWriter {
+ protected final LogWriter inner;
+ protected final int logLevel;
+
+ public LevelFilteringLogWriter(int logLevel, LogWriter inner) {
+ this.inner = inner;
+ this.logLevel = logLevel;
+ }
+
+ @Override
+ public void close() {
+ inner.close();
+ }
+
+ @Override
+ public void error(String tag, String message, Throwable error) {
+ if (logLevel <= Log.ERROR) {
+ inner.error(tag, message, error);
+ }
+ }
+
+ @Override
+ public void warn(String tag, String message, Throwable error) {
+ if (logLevel <= Log.WARN) {
+ inner.warn(tag, message, error);
+ }
+ }
+
+ @Override
+ public void info(String tag, String message, Throwable error) {
+ if (logLevel <= Log.INFO) {
+ inner.info(tag, message, error);
+ }
+ }
+
+ @Override
+ public void debug(String tag, String message, Throwable error) {
+ if (logLevel <= Log.DEBUG) {
+ inner.debug(tag, message, error);
+ }
+ }
+
+ @Override
+ public void trace(String tag, String message, Throwable error) {
+ if (logLevel <= Log.VERBOSE) {
+ inner.trace(tag, message, error);
+ }
+ }
+
+ @Override
+ public boolean shouldLogVerbose(String tag) {
+ return logLevel <= Log.VERBOSE;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/LogWriter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/LogWriter.java
new file mode 100644
index 0000000000..acfb099695
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/LogWriter.java
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.common.log.writers;
+
+/**
+ * An abstract object that logs information in some way.
+ * <p>
+ * Intended to be composed with other log writers, for example a log
+ * writer could make all log entries have the same single log tag, or
+ * could ignore certain log levels, before delegating to an inner log
+ * writer.
+ */
+public abstract class LogWriter {
+ public abstract void error(String tag, String message, Throwable error);
+ public abstract void warn(String tag, String message, Throwable error);
+ public abstract void info(String tag, String message, Throwable error);
+ public abstract void debug(String tag, String message, Throwable error);
+ public abstract void trace(String tag, String message, Throwable error);
+
+ /**
+ * We expect <code>close</code> to be called only by static
+ * synchronized methods in class <code>Logger</code>.
+ */
+ public abstract void close();
+
+ public abstract boolean shouldLogVerbose(String tag);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/PrintLogWriter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/PrintLogWriter.java
new file mode 100644
index 0000000000..6e1f63de32
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/PrintLogWriter.java
@@ -0,0 +1,77 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.common.log.writers;
+
+import java.io.PrintWriter;
+
+/**
+ * Log to a <code>PrintWriter</code>.
+ */
+public class PrintLogWriter extends LogWriter {
+ protected final PrintWriter pw;
+ protected boolean closed = false;
+
+ public static final String ERROR = " :: E :: ";
+ public static final String WARN = " :: W :: ";
+ public static final String INFO = " :: I :: ";
+ public static final String DEBUG = " :: D :: ";
+ public static final String VERBOSE = " :: V :: ";
+
+ public PrintLogWriter(PrintWriter pw) {
+ this.pw = pw;
+ }
+
+ protected void log(String tag, String message, Throwable error) {
+ if (closed) {
+ return;
+ }
+
+ pw.println(tag + message);
+ if (error != null) {
+ error.printStackTrace(pw);
+ }
+ }
+
+ @Override
+ public void error(String tag, String message, Throwable error) {
+ log(tag, ERROR + message, error);
+ }
+
+ @Override
+ public void warn(String tag, String message, Throwable error) {
+ log(tag, WARN + message, error);
+ }
+
+ @Override
+ public void info(String tag, String message, Throwable error) {
+ log(tag, INFO + message, error);
+ }
+
+ @Override
+ public void debug(String tag, String message, Throwable error) {
+ log(tag, DEBUG + message, error);
+ }
+
+ @Override
+ public void trace(String tag, String message, Throwable error) {
+ log(tag, VERBOSE + message, error);
+ }
+
+ @Override
+ public boolean shouldLogVerbose(String tag) {
+ return true;
+ }
+
+ @Override
+ public void close() {
+ if (closed) {
+ return;
+ }
+ if (pw != null) {
+ pw.close();
+ }
+ closed = true;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/SimpleTagLogWriter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/SimpleTagLogWriter.java
new file mode 100644
index 0000000000..a176543716
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/SimpleTagLogWriter.java
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.common.log.writers;
+
+/**
+ * Make a <code>LogWriter</code> only log with a single string tag.
+ */
+public class SimpleTagLogWriter extends TagLogWriter {
+ final String tag;
+ public SimpleTagLogWriter(String tag, LogWriter inner) {
+ super(inner);
+ this.tag = tag;
+ }
+
+ @Override
+ protected String getMainTag() {
+ return tag;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/StringLogWriter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/StringLogWriter.java
new file mode 100644
index 0000000000..d6a9f5eb88
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/StringLogWriter.java
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.common.log.writers;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+public class StringLogWriter extends LogWriter {
+ protected final StringWriter sw;
+ protected final PrintLogWriter inner;
+
+ public StringLogWriter() {
+ sw = new StringWriter();
+ inner = new PrintLogWriter(new PrintWriter(sw));
+ }
+
+ public String toString() {
+ return sw.toString();
+ }
+
+ @Override
+ public boolean shouldLogVerbose(String tag) {
+ return true;
+ }
+
+ @Override
+ public void error(String tag, String message, Throwable error) {
+ inner.error(tag, message, error);
+ }
+
+ @Override
+ public void warn(String tag, String message, Throwable error) {
+ inner.warn(tag, message, error);
+ }
+
+ @Override
+ public void info(String tag, String message, Throwable error) {
+ inner.info(tag, message, error);
+ }
+
+ @Override
+ public void debug(String tag, String message, Throwable error) {
+ inner.debug(tag, message, error);
+ }
+
+ @Override
+ public void trace(String tag, String message, Throwable error) {
+ inner.trace(tag, message, error);
+ }
+
+ @Override
+ public void close() {
+ inner.close();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/TagLogWriter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/TagLogWriter.java
new file mode 100644
index 0000000000..fbcd94a91b
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/TagLogWriter.java
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.common.log.writers;
+
+/**
+ * A @link{LogWriter} that logs each message under a parent tag.
+ */
+public abstract class TagLogWriter extends LogWriter {
+
+ protected final LogWriter inner;
+
+ public TagLogWriter(final LogWriter inner) {
+ super();
+ this.inner = inner;
+ }
+
+ protected abstract String getMainTag();
+
+ @Override
+ public void error(String tag, String message, Throwable error) {
+ inner.error(this.getMainTag(), tag + " :: " + message, error);
+ }
+
+ @Override
+ public void warn(String tag, String message, Throwable error) {
+ inner.warn(this.getMainTag(), tag + " :: " + message, error);
+ }
+
+ @Override
+ public void info(String tag, String message, Throwable error) {
+ inner.info(this.getMainTag(), tag + " :: " + message, error);
+ }
+
+ @Override
+ public void debug(String tag, String message, Throwable error) {
+ inner.debug(this.getMainTag(), tag + " :: " + message, error);
+ }
+
+ @Override
+ public void trace(String tag, String message, Throwable error) {
+ inner.trace(this.getMainTag(), tag + " :: " + message, error);
+ }
+
+ @Override
+ public boolean shouldLogVerbose(String tag) {
+ return inner.shouldLogVerbose(this.getMainTag());
+ }
+
+ @Override
+ public void close() {
+ inner.close();
+ }
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/ThreadLocalTagLogWriter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/ThreadLocalTagLogWriter.java
new file mode 100644
index 0000000000..0c83504a0b
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/ThreadLocalTagLogWriter.java
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.common.log.writers;
+
+/**
+ * Log with a single global tag… but that tag can be different for each thread.
+ *
+ * Takes a @link{ThreadLocal} as a constructor parameter.
+ */
+public class ThreadLocalTagLogWriter extends TagLogWriter {
+
+ private final ThreadLocal<String> tag;
+
+ public ThreadLocalTagLogWriter(ThreadLocal<String> tag, LogWriter inner) {
+ super(inner);
+ this.tag = tag;
+ }
+
+ @Override
+ protected String getMainTag() {
+ return this.tag.get();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/telemetry/TelemetryWrapper.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/telemetry/TelemetryWrapper.java
new file mode 100644
index 0000000000..6639b817d5
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/telemetry/TelemetryWrapper.java
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.common.telemetry;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import org.mozilla.gecko.background.common.log.Logger;
+
+/**
+ * Android Background Services are normally built into Fennec, but can also be
+ * built as a stand-alone APK for rapid local development. The current Telemetry
+ * implementation is coupled to Gecko, and Background Services should not
+ * interact with Gecko directly. To maintain this independence, Background
+ * Services lazily introspects the relevant Telemetry class from the enclosing
+ * package, warning but otherwise ignoring failures during introspection or
+ * invocation.
+ * <p>
+ * It is possible that Background Services will introspect and invoke the
+ * Telemetry implementation while Gecko is not running. In this case, the Fennec
+ * process itself buffers Telemetry events until such time as they can be
+ * flushed to disk and uploaded. <b>There is no guarantee that all Telemetry
+ * events will be uploaded!</b> Depending on the volume of data and the
+ * application lifecycle, Telemetry events may be dropped.
+ */
+public class TelemetryWrapper {
+ private static final String LOG_TAG = TelemetryWrapper.class.getSimpleName();
+
+ // Marking this volatile maintains thread safety cheaply.
+ private static volatile Method mAddToHistogram;
+
+ public static void addToHistogram(String key, int value) {
+ if (mAddToHistogram == null) {
+ try {
+ final Class<?> telemetry = Class.forName("org.mozilla.gecko.Telemetry");
+ mAddToHistogram = telemetry.getMethod("addToHistogram", String.class, int.class);
+ } catch (ClassNotFoundException e) {
+ Logger.warn(LOG_TAG, "org.mozilla.gecko.Telemetry class found!");
+ return;
+ } catch (NoSuchMethodException e) {
+ Logger.warn(LOG_TAG, "org.mozilla.gecko.Telemetry.addToHistogram(String, int) method not found!");
+ return;
+ }
+ }
+
+ if (mAddToHistogram != null) {
+ try {
+ mAddToHistogram.invoke(null, key, value);
+ } catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException e) {
+ Logger.warn(LOG_TAG, "Got exception invoking telemetry!");
+ }
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/db/CursorDumper.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/db/CursorDumper.java
new file mode 100644
index 0000000000..bce968b00e
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/db/CursorDumper.java
@@ -0,0 +1,99 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.db;
+
+import android.database.Cursor;
+
+/**
+ * A utility for dumping a cursor the debug log.
+ * <p>
+ * <b>For debugging only!</p>
+ */
+public class CursorDumper {
+ protected static String fixedWidth(int width, String s) {
+ if (s == null) {
+ return spaces(width);
+ }
+ int length = s.length();
+ if (width == length) {
+ return s;
+ }
+ if (width > length) {
+ return s + spaces(width - length);
+ }
+ return s.substring(0, width);
+ }
+
+ protected static String spaces(int i) {
+ return " ".substring(0, i);
+ }
+
+ protected static String dashes(int i) {
+ return "-------------------------------------".substring(0, i);
+ }
+
+ /**
+ * Dump a cursor to the debug log, ignoring any log level settings.
+ * <p>
+ * The position in the cursor is maintained. Caller is responsible for opening
+ * and closing cursor.
+ *
+ * @param cursor
+ * to dump.
+ */
+ public static void dumpCursor(Cursor cursor) {
+ dumpCursor(cursor, 18, "records");
+ }
+
+ /**
+ * Dump a cursor to the debug log, ignoring any log level settings.
+ * <p>
+ * The position in the cursor is maintained. Caller is responsible for opening
+ * and closing cursor.
+ *
+ * @param cursor
+ * to dump.
+ * @param columnWidth
+ * how many characters per cursor column.
+ * @param tags
+ * a descriptor, printed like "(10 tags)", in the header row.
+ */
+ protected static void dumpCursor(Cursor cursor, int columnWidth, String tags) {
+ int originalPosition = cursor.getPosition();
+ try {
+ String[] columnNames = cursor.getColumnNames();
+ int columnCount = cursor.getColumnCount();
+
+ for (int i = 0; i < columnCount; ++i) {
+ System.out.print(fixedWidth(columnWidth, columnNames[i]) + " | ");
+ }
+ System.out.println("(" + cursor.getCount() + " " + tags + ")");
+ for (int i = 0; i < columnCount; ++i) {
+ System.out.print(dashes(columnWidth) + " | ");
+ }
+ System.out.println("");
+ if (!cursor.moveToFirst()) {
+ System.out.println("EMPTY");
+ return;
+ }
+
+ cursor.moveToFirst();
+ while (!cursor.isAfterLast()) {
+ for (int i = 0; i < columnCount; ++i) {
+ System.out.print(fixedWidth(columnWidth, cursor.getString(i)) + " | ");
+ }
+ System.out.println("");
+ cursor.moveToNext();
+ }
+ for (int i = 0; i < columnCount-1; ++i) {
+ System.out.print(dashes(columnWidth + 3));
+ }
+ System.out.print(dashes(columnWidth + 3 - 1));
+ System.out.println("");
+ } finally {
+ cursor.moveToPosition(originalPosition);
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/db/Tab.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/db/Tab.java
new file mode 100644
index 0000000000..f38cfdf0ea
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/db/Tab.java
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.db;
+
+import org.json.simple.JSONArray;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserContract.Tabs;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.repositories.android.RepoUtils;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+
+// Immutable.
+public class Tab {
+ public final String title;
+ public final String icon;
+ public final JSONArray history;
+ public final long lastUsed;
+
+ public Tab(String title, String icon, JSONArray history, long lastUsed) {
+ this.title = title;
+ this.icon = icon;
+ this.history = history;
+ this.lastUsed = lastUsed;
+ }
+
+ public ContentValues toContentValues(String clientGUID, int position) {
+ ContentValues out = new ContentValues();
+ out.put(BrowserContract.Tabs.POSITION, position);
+ out.put(BrowserContract.Tabs.CLIENT_GUID, clientGUID);
+
+ out.put(BrowserContract.Tabs.FAVICON, this.icon);
+ out.put(BrowserContract.Tabs.LAST_USED, this.lastUsed);
+ out.put(BrowserContract.Tabs.TITLE, this.title);
+ out.put(BrowserContract.Tabs.URL, (String) this.history.get(0));
+ out.put(BrowserContract.Tabs.HISTORY, this.history.toJSONString());
+ return out;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Tab)) {
+ return false;
+ }
+ final Tab other = (Tab) o;
+
+ if (!RepoUtils.stringsEqual(this.title, other.title)) {
+ return false;
+ }
+ if (!RepoUtils.stringsEqual(this.icon, other.icon)) {
+ return false;
+ }
+
+ if (!(this.lastUsed == other.lastUsed)) {
+ return false;
+ }
+
+ return Utils.sameArrays(this.history, other.history);
+ }
+
+ @Override
+ public int hashCode() {
+ return super.hashCode();
+ }
+
+ /**
+ * Extract a <code>Tab</code> from a cursor row.
+ * <p>
+ * Caller is responsible for creating, positioning, and closing the cursor.
+ *
+ * @param cursor
+ * to inspect.
+ * @return <code>Tab</code> instance.
+ */
+ public static Tab fromCursor(final Cursor cursor) {
+ final String title = RepoUtils.getStringFromCursor(cursor, Tabs.TITLE);
+ final String icon = RepoUtils.getStringFromCursor(cursor, Tabs.FAVICON);
+ final JSONArray history = RepoUtils.getJSONArrayFromCursor(cursor, Tabs.HISTORY);
+ final long lastUsed = RepoUtils.getLongFromCursor(cursor, Tabs.LAST_USED);
+
+ return new Tab(title, icon, history, lastUsed);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccount20CreateDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccount20CreateDelegate.java
new file mode 100644
index 0000000000..98809137fb
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccount20CreateDelegate.java
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.fxa;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.Utils;
+
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+
+public class FxAccount20CreateDelegate {
+ protected final byte[] emailUTF8;
+ protected final byte[] authPW;
+ protected final boolean preVerified;
+
+ /**
+ * Make a new "create account" delegate.
+ *
+ * @param emailUTF8
+ * email as UTF-8 bytes.
+ * @param quickStretchedPW
+ * quick stretched password as bytes.
+ * @param preVerified
+ * true if account should be marked already verified; only effective
+ * for non-production auth servers.
+ * @throws UnsupportedEncodingException
+ * @throws GeneralSecurityException
+ */
+ public FxAccount20CreateDelegate(byte[] emailUTF8, byte[] quickStretchedPW, boolean preVerified) throws UnsupportedEncodingException, GeneralSecurityException {
+ this.emailUTF8 = emailUTF8;
+ this.authPW = FxAccountUtils.generateAuthPW(quickStretchedPW);
+ this.preVerified = preVerified;
+ }
+
+ public ExtendedJSONObject getCreateBody() throws FxAccountClientException {
+ final ExtendedJSONObject body = new ExtendedJSONObject();
+ try {
+ body.put("email", new String(emailUTF8, "UTF-8"));
+ body.put("authPW", Utils.byte2Hex(authPW));
+ if (preVerified) {
+ // Production endpoints do not allow preVerified; this assumes we only
+ // set it when it's okay to send it.
+ body.put("preVerified", preVerified);
+ }
+ return body;
+ } catch (UnsupportedEncodingException e) {
+ throw new FxAccountClientException(e);
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccount20LoginDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccount20LoginDelegate.java
new file mode 100644
index 0000000000..0266a6eab9
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccount20LoginDelegate.java
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.fxa;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.Utils;
+
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+
+/**
+ * An abstraction around providing an email and authorization token to the auth
+ * server.
+ */
+public class FxAccount20LoginDelegate {
+ protected final byte[] emailUTF8;
+ protected final byte[] authPW;
+
+ public FxAccount20LoginDelegate(byte[] emailUTF8, byte[] quickStretchedPW) throws UnsupportedEncodingException, GeneralSecurityException {
+ this.emailUTF8 = emailUTF8;
+ this.authPW = FxAccountUtils.generateAuthPW(quickStretchedPW);
+ }
+
+ public ExtendedJSONObject getCreateBody() throws FxAccountClientException {
+ final ExtendedJSONObject body = new ExtendedJSONObject();
+ try {
+ body.put("email", new String(emailUTF8, "UTF-8"));
+ body.put("authPW", Utils.byte2Hex(authPW));
+ return body;
+ } catch (UnsupportedEncodingException e) {
+ throw new FxAccountClientException(e);
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient.java
new file mode 100644
index 0000000000..ed959ff0ee
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient.java
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.fxa;
+
+import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse;
+import org.mozilla.gecko.background.fxa.FxAccountClient20.RecoveryEmailStatusResponse;
+import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate;
+import org.mozilla.gecko.background.fxa.FxAccountClient20.TwoKeys;
+import org.mozilla.gecko.fxa.FxAccountDevice;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+import java.util.List;
+
+public interface FxAccountClient {
+ public void accountStatus(String uid, RequestDelegate<AccountStatusResponse> requestDelegate);
+ public void recoveryEmailStatus(byte[] sessionToken, RequestDelegate<RecoveryEmailStatusResponse> requestDelegate);
+ public void keys(byte[] keyFetchToken, RequestDelegate<TwoKeys> requestDelegate);
+ public void sign(byte[] sessionToken, ExtendedJSONObject publicKey, long certificateDurationInMilliseconds, RequestDelegate<String> requestDelegate);
+ public void registerOrUpdateDevice(byte[] sessionToken, FxAccountDevice device, RequestDelegate<FxAccountDevice> requestDelegate);
+ public void deviceList(byte[] sessionToken, RequestDelegate<FxAccountDevice[]> requestDelegate);
+ public void notifyDevices(byte[] sessionToken, List<String> deviceIds, ExtendedJSONObject payload, Long TTL, RequestDelegate<ExtendedJSONObject> requestDelegate);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient20.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient20.java
new file mode 100644
index 0000000000..596f4525e7
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient20.java
@@ -0,0 +1,914 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.fxa;
+
+import android.support.annotation.NonNull;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientMalformedResponseException;
+import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.Locales;
+import org.mozilla.gecko.fxa.FxAccountDevice;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.crypto.HKDF;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BaseResourceDelegate;
+import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider;
+import org.mozilla.gecko.sync.net.Resource;
+import org.mozilla.gecko.sync.net.SyncResponse;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.Executor;
+
+import javax.crypto.Mac;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+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;
+
+/**
+ * An HTTP client for talking to an FxAccount server.
+ * <p>
+ * <p>
+ * The delegate structure used is a little different from the rest of the code
+ * base. We add a <code>RequestDelegate</code> layer that processes a typed
+ * value extracted from the body of a successful response.
+ */
+public class FxAccountClient20 implements FxAccountClient {
+ protected static final String LOG_TAG = FxAccountClient20.class.getSimpleName();
+
+ protected static final String ACCEPT_HEADER = "application/json;charset=utf-8";
+
+ public static final String JSON_KEY_EMAIL = "email";
+ public static final String JSON_KEY_KEYFETCHTOKEN = "keyFetchToken";
+ public static final String JSON_KEY_SESSIONTOKEN = "sessionToken";
+ public static final String JSON_KEY_UID = "uid";
+ public static final String JSON_KEY_VERIFIED = "verified";
+ public static final String JSON_KEY_ERROR = "error";
+ public static final String JSON_KEY_MESSAGE = "message";
+ public static final String JSON_KEY_INFO = "info";
+ public static final String JSON_KEY_CODE = "code";
+ public static final String JSON_KEY_ERRNO = "errno";
+ public static final String JSON_KEY_EXISTS = "exists";
+
+ protected static final String[] requiredErrorStringFields = { JSON_KEY_ERROR, JSON_KEY_MESSAGE, JSON_KEY_INFO };
+ protected static final String[] requiredErrorLongFields = { JSON_KEY_CODE, JSON_KEY_ERRNO };
+
+ /**
+ * The server's URI.
+ * <p>
+ * We assume throughout that this ends with a trailing slash (and guarantee as
+ * much in the constructor).
+ */
+ protected final String serverURI;
+
+ protected final Executor executor;
+
+ public FxAccountClient20(String serverURI, Executor executor) {
+ if (serverURI == null) {
+ throw new IllegalArgumentException("Must provide a server URI.");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("Must provide a non-null executor.");
+ }
+ this.serverURI = serverURI.endsWith("/") ? serverURI : serverURI + "/";
+ if (!this.serverURI.endsWith("/")) {
+ throw new IllegalArgumentException("Constructed serverURI must end with a trailing slash: " + this.serverURI);
+ }
+ this.executor = executor;
+ }
+
+ protected BaseResource getBaseResource(String path, Map<String, String> queryParameters) throws UnsupportedEncodingException, URISyntaxException {
+ if (queryParameters == null || queryParameters.isEmpty()) {
+ return getBaseResource(path);
+ }
+ final String[] array = new String[2 * queryParameters.size()];
+ int i = 0;
+ for (Entry<String, String> entry : queryParameters.entrySet()) {
+ array[i++] = entry.getKey();
+ array[i++] = entry.getValue();
+ }
+ return getBaseResource(path, array);
+ }
+
+ /**
+ * Create <code>BaseResource</code>, encoding query parameters carefully.
+ * <p>
+ * This is equivalent to <code>android.net.Uri.Builder</code>, which is not
+ * present in our JUnit 4 tests.
+ *
+ * @param path fragment.
+ * @param queryParameters list of key/value query parameter pairs. Must be even length!
+ * @return <code>BaseResource<instance>
+ * @throws URISyntaxException
+ * @throws UnsupportedEncodingException
+ */
+ protected BaseResource getBaseResource(String path, String... queryParameters) throws URISyntaxException, UnsupportedEncodingException {
+ final StringBuilder sb = new StringBuilder(serverURI);
+ sb.append(path);
+ if (queryParameters != null) {
+ int i = 0;
+ while (i < queryParameters.length) {
+ sb.append(i > 0 ? "&" : "?");
+ final String key = queryParameters[i++];
+ final String val = queryParameters[i++];
+ sb.append(URLEncoder.encode(key, "UTF-8"));
+ sb.append("=");
+ sb.append(URLEncoder.encode(val, "UTF-8"));
+ }
+ }
+ return new BaseResource(new URI(sb.toString()));
+ }
+
+ /**
+ * Process a typed value extracted from a successful response (in an
+ * endpoint-dependent way).
+ */
+ public interface RequestDelegate<T> {
+ public void handleError(Exception e);
+ public void handleFailure(FxAccountClientRemoteException e);
+ public void handleSuccess(T result);
+ }
+
+ /**
+ * Thin container for two cryptographic keys.
+ */
+ public static class TwoKeys {
+ public final byte[] kA;
+ public final byte[] wrapkB;
+ public TwoKeys(byte[] kA, byte[] wrapkB) {
+ this.kA = kA;
+ this.wrapkB = wrapkB;
+ }
+ }
+
+ protected <T> void invokeHandleError(final RequestDelegate<T> delegate, final Exception e) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ delegate.handleError(e);
+ }
+ });
+ }
+
+ enum ResponseType {
+ JSON_ARRAY,
+ JSON_OBJECT
+ }
+
+ /**
+ * Translate resource callbacks into request callbacks invoked on the provided
+ * executor.
+ * <p>
+ * Override <code>handleSuccess</code> to parse the body of the resource
+ * request and call the request callback. <code>handleSuccess</code> is
+ * invoked via the executor, so you don't need to delegate further.
+ */
+ protected abstract class ResourceDelegate<T> extends BaseResourceDelegate {
+
+ protected void handleSuccess(final int status, HttpResponse response, final ExtendedJSONObject body) throws Exception {
+ throw new UnsupportedOperationException();
+ }
+
+ protected void handleSuccess(final int status, HttpResponse response, final JSONArray body) throws Exception {
+ throw new UnsupportedOperationException();
+ }
+
+ protected final RequestDelegate<T> delegate;
+
+ protected final byte[] tokenId;
+ protected final byte[] reqHMACKey;
+ protected final SkewHandler skewHandler;
+ protected final ResponseType responseType;
+
+ /**
+ * Create a delegate for an un-authenticated resource.
+ */
+ public ResourceDelegate(final Resource resource, final RequestDelegate<T> delegate, ResponseType responseType) {
+ this(resource, delegate, responseType, null, null);
+ }
+
+ /**
+ * Create a delegate for a Hawk-authenticated resource.
+ * <p>
+ * Every Hawk request that encloses an entity (PATCH, POST, and PUT) will
+ * include the payload verification hash.
+ */
+ public ResourceDelegate(final Resource resource, final RequestDelegate<T> delegate, ResponseType responseType, final byte[] tokenId, final byte[] reqHMACKey) {
+ super(resource);
+ this.delegate = delegate;
+ this.reqHMACKey = reqHMACKey;
+ this.tokenId = tokenId;
+ this.skewHandler = SkewHandler.getSkewHandlerForResource(resource);
+ this.responseType = responseType;
+ }
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ if (tokenId != null && reqHMACKey != null) {
+ // We always include the payload verification hash for FxA Hawk-authenticated requests.
+ final boolean includePayloadVerificationHash = true;
+ return new HawkAuthHeaderProvider(Utils.byte2Hex(tokenId), reqHMACKey, includePayloadVerificationHash, skewHandler.getSkewInSeconds());
+ }
+ return super.getAuthHeaderProvider();
+ }
+
+ @Override
+ public String getUserAgent() {
+ return FxAccountConstants.USER_AGENT;
+ }
+
+ @Override
+ public void handleHttpResponse(HttpResponse response) {
+ try {
+ final int status = validateResponse(response);
+ skewHandler.updateSkew(response, now());
+ invokeHandleSuccess(status, response);
+ } catch (FxAccountClientRemoteException e) {
+ if (!skewHandler.updateSkew(response, now())) {
+ // If we couldn't update skew, but we got a failure, let's try clearing the skew.
+ skewHandler.resetSkew();
+ }
+ invokeHandleFailure(e);
+ }
+ }
+
+ protected void invokeHandleFailure(final FxAccountClientRemoteException e) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ delegate.handleFailure(e);
+ }
+ });
+ }
+
+ protected void invokeHandleSuccess(final int status, final HttpResponse response) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ SyncResponse syncResponse = new SyncResponse(response);
+ if (responseType == ResponseType.JSON_ARRAY) {
+ JSONArray body = syncResponse.jsonArrayBody();
+ ResourceDelegate.this.handleSuccess(status, response, body);
+ } else {
+ ExtendedJSONObject body = syncResponse.jsonObjectBody();
+ ResourceDelegate.this.handleSuccess(status, response, body);
+ }
+ } catch (Exception e) {
+ delegate.handleError(e);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void handleHttpProtocolException(final ClientProtocolException e) {
+ invokeHandleError(delegate, e);
+ }
+
+ @Override
+ public void handleHttpIOException(IOException e) {
+ invokeHandleError(delegate, e);
+ }
+
+ @Override
+ public void handleTransportException(GeneralSecurityException e) {
+ invokeHandleError(delegate, e);
+ }
+
+ @Override
+ public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
+ super.addHeaders(request, client);
+
+ // The basics.
+ final Locale locale = Locale.getDefault();
+ request.addHeader(HttpHeaders.ACCEPT_LANGUAGE, Locales.getLanguageTag(locale));
+ request.addHeader(HttpHeaders.ACCEPT, ACCEPT_HEADER);
+ }
+ }
+
+ protected <T> void post(BaseResource resource, final ExtendedJSONObject requestBody) {
+ if (requestBody == null) {
+ resource.post((HttpEntity) null);
+ } else {
+ resource.post(requestBody);
+ }
+ }
+
+ @SuppressWarnings("static-method")
+ public long now() {
+ return System.currentTimeMillis();
+ }
+
+ /**
+ * Intepret a response from the auth server.
+ * <p>
+ * Throw an appropriate exception on errors; otherwise, return the response's
+ * status code.
+ *
+ * @return response's HTTP status code.
+ * @throws FxAccountClientException
+ */
+ public static int validateResponse(HttpResponse response) throws FxAccountClientRemoteException {
+ final int status = response.getStatusLine().getStatusCode();
+ if (status == 200) {
+ return status;
+ }
+ int code;
+ int errno;
+ String error;
+ String message;
+ String info;
+ ExtendedJSONObject body;
+ try {
+ body = new SyncStorageResponse(response).jsonObjectBody();
+ body.throwIfFieldsMissingOrMisTyped(requiredErrorStringFields, String.class);
+ body.throwIfFieldsMissingOrMisTyped(requiredErrorLongFields, Long.class);
+ code = body.getLong(JSON_KEY_CODE).intValue();
+ errno = body.getLong(JSON_KEY_ERRNO).intValue();
+ error = body.getString(JSON_KEY_ERROR);
+ message = body.getString(JSON_KEY_MESSAGE);
+ info = body.getString(JSON_KEY_INFO);
+ } catch (Exception e) {
+ throw new FxAccountClientMalformedResponseException(response);
+ }
+ throw new FxAccountClientRemoteException(response, code, errno, error, message, info, body);
+ }
+
+ /**
+ * Don't call this directly. Use <code>unbundleBody</code> instead.
+ */
+ protected void unbundleBytes(byte[] bundleBytes, byte[] respHMACKey, byte[] respXORKey, byte[]... rest)
+ throws InvalidKeyException, NoSuchAlgorithmException, FxAccountClientException {
+ if (bundleBytes.length < 32) {
+ throw new IllegalArgumentException("input bundle must include HMAC");
+ }
+ int len = respXORKey.length;
+ if (bundleBytes.length != len + 32) {
+ throw new IllegalArgumentException("input bundle and XOR key with HMAC have different lengths");
+ }
+ int left = len;
+ for (byte[] array : rest) {
+ left -= array.length;
+ }
+ if (left != 0) {
+ throw new IllegalArgumentException("XOR key and total output arrays have different lengths");
+ }
+
+ byte[] ciphertext = new byte[len];
+ byte[] HMAC = new byte[32];
+ System.arraycopy(bundleBytes, 0, ciphertext, 0, len);
+ System.arraycopy(bundleBytes, len, HMAC, 0, 32);
+
+ Mac hmacHasher = HKDF.makeHMACHasher(respHMACKey);
+ byte[] computedHMAC = hmacHasher.doFinal(ciphertext);
+ if (!Arrays.equals(computedHMAC, HMAC)) {
+ throw new FxAccountClientException("Bad message HMAC");
+ }
+
+ int offset = 0;
+ for (byte[] array : rest) {
+ for (int i = 0; i < array.length; i++) {
+ array[i] = (byte) (respXORKey[offset + i] ^ ciphertext[offset + i]);
+ }
+ offset += array.length;
+ }
+ }
+
+ protected void unbundleBody(ExtendedJSONObject body, byte[] requestKey, byte[] ctxInfo, byte[]... rest) throws Exception {
+ int length = 0;
+ for (byte[] array : rest) {
+ length += array.length;
+ }
+
+ if (body == null) {
+ throw new FxAccountClientException("body must be non-null");
+ }
+ String bundle = body.getString("bundle");
+ if (bundle == null) {
+ throw new FxAccountClientException("bundle must be a non-null string");
+ }
+ byte[] bundleBytes = Utils.hex2Byte(bundle);
+
+ final byte[] respHMACKey = new byte[32];
+ final byte[] respXORKey = new byte[length];
+ HKDF.deriveMany(requestKey, new byte[0], ctxInfo, respHMACKey, respXORKey);
+ unbundleBytes(bundleBytes, respHMACKey, respXORKey, rest);
+ }
+
+ public void keys(byte[] keyFetchToken, final RequestDelegate<TwoKeys> delegate) {
+ final byte[] tokenId = new byte[32];
+ final byte[] reqHMACKey = new byte[32];
+ final byte[] requestKey = new byte[32];
+ try {
+ HKDF.deriveMany(keyFetchToken, new byte[0], FxAccountUtils.KW("keyFetchToken"), tokenId, reqHMACKey, requestKey);
+ } catch (Exception e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ BaseResource resource;
+ try {
+ resource = getBaseResource("account/keys");
+ } catch (URISyntaxException | UnsupportedEncodingException e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ resource.delegate = new ResourceDelegate<TwoKeys>(resource, delegate, ResponseType.JSON_OBJECT, tokenId, reqHMACKey) {
+ @Override
+ public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception {
+ byte[] kA = new byte[FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES];
+ byte[] wrapkB = new byte[FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES];
+ unbundleBody(body, requestKey, FxAccountUtils.KW("account/keys"), kA, wrapkB);
+ delegate.handleSuccess(new TwoKeys(kA, wrapkB));
+ }
+ };
+ resource.get();
+ }
+
+ /**
+ * Thin container for account status response.
+ */
+ public static class AccountStatusResponse {
+ public final boolean exists;
+ public AccountStatusResponse(boolean exists) {
+ this.exists = exists;
+ }
+ }
+
+ /**
+ * Query the account status of an account given a uid.
+ *
+ * @param uid to query.
+ * @param delegate to invoke callbacks.
+ */
+ public void accountStatus(String uid, final RequestDelegate<AccountStatusResponse> delegate) {
+ final BaseResource resource;
+ try {
+ final Map<String, String> params = new HashMap<>(1);
+ params.put("uid", uid);
+ resource = getBaseResource("account/status", params);
+ } catch (URISyntaxException | UnsupportedEncodingException e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ resource.delegate = new ResourceDelegate<AccountStatusResponse>(resource, delegate, ResponseType.JSON_OBJECT) {
+ @Override
+ public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception {
+ boolean exists = body.getBoolean(JSON_KEY_EXISTS);
+ delegate.handleSuccess(new AccountStatusResponse(exists));
+ }
+ };
+ resource.get();
+ }
+
+ /**
+ * Thin container for recovery email status response.
+ */
+ public static class RecoveryEmailStatusResponse {
+ public final String email;
+ public final boolean verified;
+ public RecoveryEmailStatusResponse(String email, boolean verified) {
+ this.email = email;
+ this.verified = verified;
+ }
+ }
+
+ /**
+ * Query the recovery email status of an account given a valid session token.
+ * <p>
+ * This API is a little odd: the auth server returns the email and
+ * verification state of the account that corresponds to the (opaque) session
+ * token. It might fail if the session token is unknown (or invalid, or
+ * revoked).
+ *
+ * @param sessionToken
+ * to query.
+ * @param delegate
+ * to invoke callbacks.
+ */
+ public void recoveryEmailStatus(byte[] sessionToken, final RequestDelegate<RecoveryEmailStatusResponse> delegate) {
+ final byte[] tokenId = new byte[32];
+ final byte[] reqHMACKey = new byte[32];
+ final byte[] requestKey = new byte[32];
+ try {
+ HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey);
+ } catch (Exception e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ BaseResource resource;
+ try {
+ resource = getBaseResource("recovery_email/status");
+ } catch (URISyntaxException | UnsupportedEncodingException e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ resource.delegate = new ResourceDelegate<RecoveryEmailStatusResponse>(resource, delegate, ResponseType.JSON_OBJECT, tokenId, reqHMACKey) {
+ @Override
+ public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception {
+ String[] requiredStringFields = new String[] { JSON_KEY_EMAIL };
+ body.throwIfFieldsMissingOrMisTyped(requiredStringFields, String.class);
+ String email = body.getString(JSON_KEY_EMAIL);
+ Boolean verified = body.getBoolean(JSON_KEY_VERIFIED);
+ delegate.handleSuccess(new RecoveryEmailStatusResponse(email, verified));
+ }
+ };
+ resource.get();
+ }
+
+ @SuppressWarnings("unchecked")
+ public void sign(final byte[] sessionToken, final ExtendedJSONObject publicKey, long durationInMilliseconds, final RequestDelegate<String> delegate) {
+ final ExtendedJSONObject body = new ExtendedJSONObject();
+ body.put("publicKey", publicKey);
+ body.put("duration", durationInMilliseconds);
+
+ final byte[] tokenId = new byte[32];
+ final byte[] reqHMACKey = new byte[32];
+ try {
+ HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey);
+ } catch (Exception e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ BaseResource resource;
+ try {
+ resource = getBaseResource("certificate/sign");
+ } catch (URISyntaxException | UnsupportedEncodingException e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ resource.delegate = new ResourceDelegate<String>(resource, delegate, ResponseType.JSON_OBJECT, tokenId, reqHMACKey) {
+ @Override
+ public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception {
+ String cert = body.getString("cert");
+ if (cert == null) {
+ delegate.handleError(new FxAccountClientException("cert must be a non-null string"));
+ return;
+ }
+ delegate.handleSuccess(cert);
+ }
+ };
+ post(resource, body);
+ }
+
+ protected static final String[] LOGIN_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_UID, JSON_KEY_SESSIONTOKEN };
+ protected static final String[] LOGIN_RESPONSE_REQUIRED_STRING_FIELDS_KEYS = new String[] { JSON_KEY_UID, JSON_KEY_SESSIONTOKEN, JSON_KEY_KEYFETCHTOKEN, };
+ protected static final String[] LOGIN_RESPONSE_REQUIRED_BOOLEAN_FIELDS = new String[] { JSON_KEY_VERIFIED };
+
+ /**
+ * Thin container for login response.
+ * <p>
+ * The <code>remoteEmail</code> field is the email address as normalized by the
+ * server, and is <b>not necessarily</b> the email address delivered to the
+ * <code>login</code> or <code>create</code> call.
+ */
+ public static class LoginResponse {
+ public final String remoteEmail;
+ public final String uid;
+ public final byte[] sessionToken;
+ public final boolean verified;
+ public final byte[] keyFetchToken;
+
+ public LoginResponse(String remoteEmail, String uid, boolean verified, byte[] sessionToken, byte[] keyFetchToken) {
+ this.remoteEmail = remoteEmail;
+ this.uid = uid;
+ this.verified = verified;
+ this.sessionToken = sessionToken;
+ this.keyFetchToken = keyFetchToken;
+ }
+ }
+
+ // Public for testing only; prefer login and loginAndGetKeys (without boolean parameter).
+ public void login(final byte[] emailUTF8, final byte[] quickStretchedPW, final boolean getKeys,
+ final Map<String, String> queryParameters,
+ final RequestDelegate<LoginResponse> delegate) {
+ final BaseResource resource;
+ final ExtendedJSONObject body;
+ try {
+ final String path = "account/login";
+ final Map<String, String> modifiedParameters = new HashMap<>();
+ if (queryParameters != null) {
+ modifiedParameters.putAll(queryParameters);
+ }
+ if (getKeys) {
+ modifiedParameters.put("keys", "true");
+ }
+ resource = getBaseResource(path, modifiedParameters);
+ body = new FxAccount20LoginDelegate(emailUTF8, quickStretchedPW).getCreateBody();
+ } catch (Exception e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ resource.delegate = new ResourceDelegate<LoginResponse>(resource, delegate, ResponseType.JSON_OBJECT) {
+ @Override
+ public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception {
+ final String[] requiredStringFields = getKeys ? LOGIN_RESPONSE_REQUIRED_STRING_FIELDS_KEYS : LOGIN_RESPONSE_REQUIRED_STRING_FIELDS;
+ body.throwIfFieldsMissingOrMisTyped(requiredStringFields, String.class);
+
+ final String[] requiredBooleanFields = LOGIN_RESPONSE_REQUIRED_BOOLEAN_FIELDS;
+ body.throwIfFieldsMissingOrMisTyped(requiredBooleanFields, Boolean.class);
+
+ String uid = body.getString(JSON_KEY_UID);
+ boolean verified = body.getBoolean(JSON_KEY_VERIFIED);
+ byte[] sessionToken = Utils.hex2Byte(body.getString(JSON_KEY_SESSIONTOKEN));
+ byte[] keyFetchToken = null;
+ if (getKeys) {
+ keyFetchToken = Utils.hex2Byte(body.getString(JSON_KEY_KEYFETCHTOKEN));
+ }
+ LoginResponse loginResponse = new LoginResponse(new String(emailUTF8, "UTF-8"), uid, verified, sessionToken, keyFetchToken);
+
+ delegate.handleSuccess(loginResponse);
+ }
+ };
+
+ post(resource, body);
+ }
+
+ public void createAccount(final byte[] emailUTF8, final byte[] quickStretchedPW,
+ final boolean getKeys,
+ final boolean preVerified,
+ final Map<String, String> queryParameters,
+ final RequestDelegate<LoginResponse> delegate) {
+ final BaseResource resource;
+ final ExtendedJSONObject body;
+ try {
+ final String path = "account/create";
+ final Map<String, String> modifiedParameters = new HashMap<>();
+ if (queryParameters != null) {
+ modifiedParameters.putAll(queryParameters);
+ }
+ if (getKeys) {
+ modifiedParameters.put("keys", "true");
+ }
+ resource = getBaseResource(path, modifiedParameters);
+ body = new FxAccount20CreateDelegate(emailUTF8, quickStretchedPW, preVerified).getCreateBody();
+ } catch (Exception e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ // This is very similar to login, except verified is not required.
+ resource.delegate = new ResourceDelegate<LoginResponse>(resource, delegate, ResponseType.JSON_OBJECT) {
+ @Override
+ public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception {
+ final String[] requiredStringFields = getKeys ? LOGIN_RESPONSE_REQUIRED_STRING_FIELDS_KEYS : LOGIN_RESPONSE_REQUIRED_STRING_FIELDS;
+ body.throwIfFieldsMissingOrMisTyped(requiredStringFields, String.class);
+
+ String uid = body.getString(JSON_KEY_UID);
+ boolean verified = false; // In production, we're definitely not verified immediately upon creation.
+ Boolean tempVerified = body.getBoolean(JSON_KEY_VERIFIED);
+ if (tempVerified != null) {
+ verified = tempVerified;
+ }
+ byte[] sessionToken = Utils.hex2Byte(body.getString(JSON_KEY_SESSIONTOKEN));
+ byte[] keyFetchToken = null;
+ if (getKeys) {
+ keyFetchToken = Utils.hex2Byte(body.getString(JSON_KEY_KEYFETCHTOKEN));
+ }
+ LoginResponse loginResponse = new LoginResponse(new String(emailUTF8, "UTF-8"), uid, verified, sessionToken, keyFetchToken);
+
+ delegate.handleSuccess(loginResponse);
+ }
+ };
+
+ post(resource, body);
+ }
+
+ /**
+ * We want users to be able to enter their email address case-insensitively.
+ * We stretch the password locally using the email address as a salt, to make
+ * dictionary attacks more expensive. This means that a client with a
+ * case-differing email address is unable to produce the correct
+ * authorization, even though it knows the password. In this case, the server
+ * returns the email that the account was created with, so that the client can
+ * re-stretch the password locally with the correct email salt. This version
+ * of <code>login</code> retries at most one time with a server provided email
+ * address.
+ * <p>
+ * Be aware that consumers will not see the initial error response from the
+ * server providing an alternate email (if there is one).
+ *
+ * @param emailUTF8
+ * user entered email address.
+ * @param stretcher
+ * delegate to stretch and re-stretch password.
+ * @param getKeys
+ * true if a <code>keyFetchToken</code> should be returned (in
+ * addition to the standard <code>sessionToken</code>).
+ * @param queryParameters
+ * @param delegate
+ * to invoke callbacks.
+ */
+ public void login(final byte[] emailUTF8, final PasswordStretcher stretcher, final boolean getKeys,
+ final Map<String, String> queryParameters,
+ final RequestDelegate<LoginResponse> delegate) {
+ byte[] quickStretchedPW;
+ try {
+ FxAccountUtils.pii(LOG_TAG, "Trying user provided email: '" + new String(emailUTF8, "UTF-8") + "'" );
+ quickStretchedPW = stretcher.getQuickStretchedPW(emailUTF8);
+ } catch (Exception e) {
+ delegate.handleError(e);
+ return;
+ }
+
+ this.login(emailUTF8, quickStretchedPW, getKeys, queryParameters, new RequestDelegate<LoginResponse>() {
+ @Override
+ public void handleSuccess(LoginResponse result) {
+ delegate.handleSuccess(result);
+ }
+
+ @Override
+ public void handleError(Exception e) {
+ delegate.handleError(e);
+ }
+
+ @Override
+ public void handleFailure(FxAccountClientRemoteException e) {
+ String alternateEmail = e.body.getString(JSON_KEY_EMAIL);
+ if (!e.isBadEmailCase() || alternateEmail == null) {
+ delegate.handleFailure(e);
+ return;
+ };
+
+ Logger.info(LOG_TAG, "Server returned alternate email; retrying login with provided email.");
+ FxAccountUtils.pii(LOG_TAG, "Trying server provided email: '" + alternateEmail + "'" );
+
+ try {
+ // Nota bene: this is not recursive, since we call the fixed password
+ // signature here, which invokes a non-retrying version.
+ byte[] alternateEmailUTF8 = alternateEmail.getBytes("UTF-8");
+ byte[] alternateQuickStretchedPW = stretcher.getQuickStretchedPW(alternateEmailUTF8);
+ login(alternateEmailUTF8, alternateQuickStretchedPW, getKeys, queryParameters, delegate);
+ } catch (Exception innerException) {
+ delegate.handleError(innerException);
+ return;
+ }
+ }
+ });
+ }
+
+ /**
+ * Registers a device given a valid session token.
+ *
+ * @param sessionToken to query.
+ * @param delegate to invoke callbacks.
+ */
+ @Override
+ public void registerOrUpdateDevice(byte[] sessionToken, FxAccountDevice device, RequestDelegate<FxAccountDevice> delegate) {
+ final byte[] tokenId = new byte[32];
+ final byte[] reqHMACKey = new byte[32];
+ final byte[] requestKey = new byte[32];
+ try {
+ HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey);
+ } catch (Exception e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ final BaseResource resource;
+ final ExtendedJSONObject body;
+ try {
+ resource = getBaseResource("account/device");
+ body = device.toJson();
+ } catch (URISyntaxException | UnsupportedEncodingException e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ resource.delegate = new ResourceDelegate<FxAccountDevice>(resource, delegate, ResponseType.JSON_OBJECT, tokenId, reqHMACKey) {
+ @Override
+ public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+ try {
+ delegate.handleSuccess(FxAccountDevice.fromJson(body));
+ } catch (Exception e) {
+ delegate.handleError(e);
+ }
+ }
+ };
+
+ post(resource, body);
+ }
+
+ @Override
+ public void deviceList(byte[] sessionToken, RequestDelegate<FxAccountDevice[]> delegate) {
+ final byte[] tokenId = new byte[32];
+ final byte[] reqHMACKey = new byte[32];
+ final byte[] requestKey = new byte[32];
+ try {
+ HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey);
+ } catch (Exception e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ final BaseResource resource;
+ try {
+ resource = getBaseResource("account/devices");
+ } catch (URISyntaxException | UnsupportedEncodingException e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ resource.delegate = new ResourceDelegate<FxAccountDevice[]>(resource, delegate, ResponseType.JSON_ARRAY, tokenId, reqHMACKey) {
+ @Override
+ public void handleSuccess(int status, HttpResponse response, JSONArray devicesJson) {
+ try {
+ FxAccountDevice[] devices = new FxAccountDevice[devicesJson.size()];
+ for (int i = 0; i < devices.length; i++) {
+ ExtendedJSONObject deviceJson = new ExtendedJSONObject((JSONObject) devicesJson.get(i));
+ devices[i] = FxAccountDevice.fromJson(deviceJson);
+ }
+ delegate.handleSuccess(devices);
+ } catch (Exception e) {
+ delegate.handleError(e);
+ }
+ }
+ };
+
+ resource.get();
+ }
+
+ @Override
+ public void notifyDevices(@NonNull byte[] sessionToken, @NonNull List<String> deviceIds, ExtendedJSONObject payload, Long TTL, RequestDelegate<ExtendedJSONObject> delegate) {
+ final byte[] tokenId = new byte[32];
+ final byte[] reqHMACKey = new byte[32];
+ final byte[] requestKey = new byte[32];
+ try {
+ HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey);
+ } catch (Exception e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ final BaseResource resource;
+ final ExtendedJSONObject body = createNotifyDevicesBody(deviceIds, payload, TTL);
+ try {
+ resource = getBaseResource("account/devices/notify");
+ } catch (URISyntaxException | UnsupportedEncodingException e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ resource.delegate = new ResourceDelegate<ExtendedJSONObject>(resource, delegate, ResponseType.JSON_OBJECT, tokenId, reqHMACKey) {
+ @Override
+ public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+ try {
+ delegate.handleSuccess(body);
+ } catch (Exception e) {
+ delegate.handleError(e);
+ }
+ }
+ };
+
+ post(resource, body);
+ }
+
+ @NonNull
+ @SuppressWarnings("unchecked")
+ private ExtendedJSONObject createNotifyDevicesBody(@NonNull List<String> deviceIds, ExtendedJSONObject payload, Long TTL) {
+ final ExtendedJSONObject body = new ExtendedJSONObject();
+ final JSONArray to = new JSONArray();
+ to.addAll(deviceIds);
+ body.put("to", to);
+ if (payload != null) {
+ body.put("payload", payload);
+ }
+ if (TTL != null) {
+ body.put("TTL", TTL);
+ }
+ return body;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClientException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClientException.java
new file mode 100644
index 0000000000..28ee5630ed
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClientException.java
@@ -0,0 +1,133 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.fxa;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.HTTPFailureException;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+
+/**
+ * From <a href="https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md">https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md</a>.
+ */
+public class FxAccountClientException extends Exception {
+ private static final long serialVersionUID = 7953459541558266597L;
+
+ public FxAccountClientException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ public FxAccountClientException(Exception e) {
+ super(e);
+ }
+
+ public static class FxAccountClientRemoteException extends FxAccountClientException {
+ private static final long serialVersionUID = 2209313149952001097L;
+
+ public final HttpResponse response;
+ public final long httpStatusCode;
+ public final long apiErrorNumber;
+ public final String error;
+ public final String message;
+ public final String info;
+ public final ExtendedJSONObject body;
+
+ public FxAccountClientRemoteException(HttpResponse response, long httpStatusCode, long apiErrorNumber, String error, String message, String info, ExtendedJSONObject body) {
+ super(new HTTPFailureException(new SyncStorageResponse(response)));
+ if (body == null) {
+ throw new IllegalArgumentException("body must not be null");
+ }
+ this.response = response;
+ this.httpStatusCode = httpStatusCode;
+ this.apiErrorNumber = apiErrorNumber;
+ this.error = error;
+ this.message = message;
+ this.info = info;
+ this.body = body;
+ }
+
+ @Override
+ public String toString() {
+ return "<FxAccountClientRemoteException " + this.httpStatusCode + " [" + this.apiErrorNumber + "]: " + this.message + ">";
+ }
+
+ public boolean isInvalidAuthentication() {
+ return httpStatusCode == HttpStatus.SC_UNAUTHORIZED;
+ }
+
+ public boolean isAccountAlreadyExists() {
+ return apiErrorNumber == FxAccountRemoteError.ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS;
+ }
+
+ public boolean isAccountDoesNotExist() {
+ return apiErrorNumber == FxAccountRemoteError.ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST;
+ }
+
+ public boolean isBadPassword() {
+ return apiErrorNumber == FxAccountRemoteError.INCORRECT_PASSWORD;
+ }
+
+ public boolean isUnverified() {
+ return apiErrorNumber == FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT;
+ }
+
+ public boolean isUpgradeRequired() {
+ return
+ apiErrorNumber == FxAccountRemoteError.ENDPOINT_IS_NO_LONGER_SUPPORTED ||
+ apiErrorNumber == FxAccountRemoteError.INCORRECT_LOGIN_METHOD_FOR_THIS_ACCOUNT ||
+ apiErrorNumber == FxAccountRemoteError.INCORRECT_KEY_RETRIEVAL_METHOD_FOR_THIS_ACCOUNT ||
+ apiErrorNumber == FxAccountRemoteError.INCORRECT_API_VERSION_FOR_THIS_ACCOUNT;
+ }
+
+ public boolean isTooManyRequests() {
+ return apiErrorNumber == FxAccountRemoteError.CLIENT_HAS_SENT_TOO_MANY_REQUESTS;
+ }
+
+ public boolean isServerUnavailable() {
+ return apiErrorNumber == FxAccountRemoteError.SERVICE_TEMPORARILY_UNAVAILABLE_DUE_TO_HIGH_LOAD;
+ }
+
+ public boolean isBadEmailCase() {
+ return apiErrorNumber == FxAccountRemoteError.INCORRECT_EMAIL_CASE;
+ }
+
+ public boolean isAccountLocked() {
+ return apiErrorNumber == FxAccountRemoteError.ACCOUNT_LOCKED;
+ }
+
+ public int getErrorMessageStringResource() {
+ if (isUpgradeRequired()) {
+ return R.string.fxaccount_remote_error_UPGRADE_REQUIRED;
+ } else if (isAccountAlreadyExists()) {
+ return R.string.fxaccount_remote_error_ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS;
+ } else if (isAccountDoesNotExist()) {
+ return R.string.fxaccount_remote_error_ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST;
+ } else if (isBadPassword()) {
+ return R.string.fxaccount_remote_error_INCORRECT_PASSWORD;
+ } else if (isUnverified()) {
+ return R.string.fxaccount_remote_error_ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT;
+ } else if (isTooManyRequests()) {
+ return R.string.fxaccount_remote_error_CLIENT_HAS_SENT_TOO_MANY_REQUESTS;
+ } else if (isServerUnavailable()) {
+ return R.string.fxaccount_remote_error_SERVICE_TEMPORARILY_UNAVAILABLE_TO_DUE_HIGH_LOAD;
+ } else if (isAccountLocked()) {
+ return R.string.fxaccount_remote_error_ACCOUNT_LOCKED;
+ } else {
+ return R.string.fxaccount_remote_error_UNKNOWN_ERROR;
+ }
+ }
+ }
+
+ public static class FxAccountClientMalformedResponseException extends FxAccountClientRemoteException {
+ private static final long serialVersionUID = 2209313149952001098L;
+
+ public FxAccountClientMalformedResponseException(HttpResponse response) {
+ super(response, 0, FxAccountRemoteError.UNKNOWN_ERROR, "Response malformed", "Response malformed", "Response malformed", new ExtendedJSONObject());
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountRemoteError.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountRemoteError.java
new file mode 100644
index 0000000000..5a89561cb0
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountRemoteError.java
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.fxa;
+
+public interface FxAccountRemoteError {
+ public static final int ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS = 101;
+ public static final int ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST = 102;
+ public static final int INCORRECT_PASSWORD = 103;
+ public static final int ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT = 104;
+ public static final int INVALID_VERIFICATION_CODE = 105;
+ public static final int REQUEST_BODY_WAS_NOT_VALID_JSON = 106;
+ public static final int REQUEST_BODY_CONTAINS_INVALID_PARAMETERS = 107;
+ public static final int REQUEST_BODY_MISSING_REQUIRED_PARAMETERS = 108;
+ public static final int INVALID_REQUEST_SIGNATURE = 109;
+ public static final int INVALID_AUTHENTICATION_TOKEN = 110;
+ public static final int INVALID_AUTHENTICATION_TIMESTAMP = 111;
+ public static final int CONTENT_LENGTH_HEADER_WAS_NOT_PROVIDED = 112;
+ public static final int REQUEST_BODY_TOO_LARGE = 113;
+ public static final int CLIENT_HAS_SENT_TOO_MANY_REQUESTS = 114;
+ public static final int INVALID_NONCE_IN_REQUEST_SIGNATURE = 115;
+ public static final int ENDPOINT_IS_NO_LONGER_SUPPORTED = 116;
+ public static final int INCORRECT_LOGIN_METHOD_FOR_THIS_ACCOUNT = 117;
+ public static final int INCORRECT_KEY_RETRIEVAL_METHOD_FOR_THIS_ACCOUNT = 118;
+ public static final int INCORRECT_API_VERSION_FOR_THIS_ACCOUNT = 119;
+ public static final int INCORRECT_EMAIL_CASE = 120;
+ public static final int ACCOUNT_LOCKED = 121;
+ public static final int UNKNOWN_DEVICE = 123;
+ public static final int DEVICE_SESSION_CONFLICT = 124;
+ public static final int SERVICE_TEMPORARILY_UNAVAILABLE_DUE_TO_HIGH_LOAD = 201;
+ public static final int UNKNOWN_ERROR = 999;
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountUtils.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountUtils.java
new file mode 100644
index 0000000000..2d29725a07
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountUtils.java
@@ -0,0 +1,217 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.fxa;
+
+import java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.nativecode.NativeCrypto;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.crypto.HKDF;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.crypto.PBKDF2;
+
+import android.content.Context;
+
+public class FxAccountUtils {
+ private static final String LOG_TAG = FxAccountUtils.class.getSimpleName();
+
+ public static final int SALT_LENGTH_BYTES = 32;
+ public static final int SALT_LENGTH_HEX = 2 * SALT_LENGTH_BYTES;
+
+ public static final int HASH_LENGTH_BYTES = 16;
+ public static final int HASH_LENGTH_HEX = 2 * HASH_LENGTH_BYTES;
+
+ public static final int CRYPTO_KEY_LENGTH_BYTES = 32;
+ public static final int CRYPTO_KEY_LENGTH_HEX = 2 * CRYPTO_KEY_LENGTH_BYTES;
+
+ public static final String KW_VERSION_STRING = "identity.mozilla.com/picl/v1/";
+
+ public static final int NUMBER_OF_QUICK_STRETCH_ROUNDS = 1000;
+
+ // For extra debugging. Not final so it can be changed from Fennec, or from
+ // an add-on.
+ public static boolean LOG_PERSONAL_INFORMATION = false;
+
+ public static void pii(String tag, String message) {
+ if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
+ Logger.info(tag, "$$FxA PII$$: " + message);
+ }
+ }
+
+ public static String bytes(String string) throws UnsupportedEncodingException {
+ return Utils.byte2Hex(string.getBytes("UTF-8"));
+ }
+
+ public static byte[] KW(String name) throws UnsupportedEncodingException {
+ return Utils.concatAll(
+ KW_VERSION_STRING.getBytes("UTF-8"),
+ name.getBytes("UTF-8"));
+ }
+
+ public static byte[] KWE(String name, byte[] emailUTF8) throws UnsupportedEncodingException {
+ return Utils.concatAll(
+ KW_VERSION_STRING.getBytes("UTF-8"),
+ name.getBytes("UTF-8"),
+ ":".getBytes("UTF-8"),
+ emailUTF8);
+ }
+
+ /**
+ * Calculate the SRP verifier <tt>x</tt> value.
+ */
+ public static BigInteger srpVerifierLowercaseX(byte[] emailUTF8, byte[] srpPWBytes, byte[] srpSaltBytes)
+ throws NoSuchAlgorithmException, UnsupportedEncodingException {
+ byte[] inner = Utils.sha256(Utils.concatAll(emailUTF8, ":".getBytes("UTF-8"), srpPWBytes));
+ byte[] outer = Utils.sha256(Utils.concatAll(srpSaltBytes, inner));
+ return new BigInteger(1, outer);
+ }
+
+ /**
+ * Calculate the SRP verifier <tt>v</tt> value.
+ */
+ public static BigInteger srpVerifierLowercaseV(byte[] emailUTF8, byte[] srpPWBytes, byte[] srpSaltBytes, BigInteger g, BigInteger N)
+ throws NoSuchAlgorithmException, UnsupportedEncodingException {
+ BigInteger x = srpVerifierLowercaseX(emailUTF8, srpPWBytes, srpSaltBytes);
+ BigInteger v = g.modPow(x, N);
+ return v;
+ }
+
+ /**
+ * Format x modulo N in hexadecimal, using as many characters as N takes (in hexadecimal).
+ * @param x to format.
+ * @param N modulus.
+ * @return x modulo N in hexadecimal.
+ */
+ public static String hexModN(BigInteger x, BigInteger N) {
+ int byteLength = (N.bitLength() + 7) / 8;
+ int hexLength = 2 * byteLength;
+ return Utils.byte2Hex(Utils.hex2Byte((x.mod(N)).toString(16), byteLength), hexLength);
+ }
+
+ /**
+ * The first engineering milestone of PICL (Profile-in-the-Cloud) was
+ * comprised of Sync 1.1 fronted by a Firefox Account. The sync key was
+ * generated from the Firefox Account password-derived kB value using this
+ * method.
+ */
+ public static KeyBundle generateSyncKeyBundle(final byte[] kB) throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
+ byte[] encryptionKey = new byte[32];
+ byte[] hmacKey = new byte[32];
+ byte[] derived = HKDF.derive(kB, new byte[0], FxAccountUtils.KW("oldsync"), 2*32);
+ System.arraycopy(derived, 0*32, encryptionKey, 0, 1*32);
+ System.arraycopy(derived, 1*32, hmacKey, 0, 1*32);
+ return new KeyBundle(encryptionKey, hmacKey);
+ }
+
+ /**
+ * Firefox Accounts are password authenticated, but clients should not store
+ * the plain-text password for any amount of time. Equivalent, but slightly
+ * more secure, is the quickly client-side stretched password.
+ * <p>
+ * We separate this since multiple login-time operations want it, and the
+ * PBKDF2 operation is computationally expensive.
+ */
+ public static byte[] generateQuickStretchedPW(byte[] emailUTF8, byte[] passwordUTF8) throws GeneralSecurityException, UnsupportedEncodingException {
+ byte[] S = FxAccountUtils.KWE("quickStretch", emailUTF8);
+ try {
+ return NativeCrypto.pbkdf2SHA256(passwordUTF8, S, NUMBER_OF_QUICK_STRETCH_ROUNDS, 32);
+ } catch (final LinkageError e) {
+ // This will throw UnsatisfiedLinkError (missing mozglue) the first time it is called, and
+ // ClassNotDefFoundError, for the uninitialized NativeCrypto class, each subsequent time this
+ // is called; LinkageError is their common ancestor.
+ Logger.warn(LOG_TAG, "Got throwable stretching password using native pbkdf2SHA256 " +
+ "implementation; ignoring and using Java implementation.", e);
+ return PBKDF2.pbkdf2SHA256(passwordUTF8, S, NUMBER_OF_QUICK_STRETCH_ROUNDS, 32);
+ }
+ }
+
+ /**
+ * The password-derived credential used to authenticate to the Firefox Account
+ * auth server.
+ */
+ public static byte[] generateAuthPW(byte[] quickStretchedPW) throws GeneralSecurityException, UnsupportedEncodingException {
+ return HKDF.derive(quickStretchedPW, new byte[0], FxAccountUtils.KW("authPW"), 32);
+ }
+
+ /**
+ * The password-derived credential used to unwrap keys managed by the Firefox
+ * Account auth server.
+ */
+ public static byte[] generateUnwrapBKey(byte[] quickStretchedPW) throws GeneralSecurityException, UnsupportedEncodingException {
+ return HKDF.derive(quickStretchedPW, new byte[0], FxAccountUtils.KW("unwrapBkey"), 32);
+ }
+
+ public static byte[] unwrapkB(byte[] unwrapkB, byte[] wrapkB) {
+ if (unwrapkB == null) {
+ throw new IllegalArgumentException("unwrapkB must not be null");
+ }
+ if (wrapkB == null) {
+ throw new IllegalArgumentException("wrapkB must not be null");
+ }
+ if (unwrapkB.length != CRYPTO_KEY_LENGTH_BYTES || wrapkB.length != CRYPTO_KEY_LENGTH_BYTES) {
+ throw new IllegalArgumentException("unwrapkB and wrapkB must be " + CRYPTO_KEY_LENGTH_BYTES + " bytes long");
+ }
+ byte[] kB = new byte[CRYPTO_KEY_LENGTH_BYTES];
+ for (int i = 0; i < wrapkB.length; i++) {
+ kB[i] = (byte) (wrapkB[i] ^ unwrapkB[i]);
+ }
+ return kB;
+ }
+
+ /**
+ * The token server accepts an X-Client-State header, which is the
+ * lowercase-hex-encoded first 16 bytes of the SHA-256 hash of the
+ * bytes of kB.
+ * @param kB a byte array, expected to be 32 bytes long.
+ * @return a 32-character string.
+ * @throws NoSuchAlgorithmException
+ */
+ public static String computeClientState(byte[] kB) throws NoSuchAlgorithmException {
+ if (kB == null ||
+ kB.length != 32) {
+ throw new IllegalArgumentException("Unexpected kB.");
+ }
+ byte[] sha256 = Utils.sha256(kB);
+ byte[] truncated = new byte[16];
+ System.arraycopy(sha256, 0, truncated, 0, 16);
+ return Utils.byte2Hex(truncated); // This is automatically lowercase.
+ }
+
+ /**
+ * Given an endpoint, calculate the corresponding BrowserID audience.
+ * <p>
+ * This is the domain, in web parlance.
+ *
+ * @param serverURI endpoint.
+ * @return BrowserID audience.
+ * @throws URISyntaxException
+ */
+ public static String getAudienceForURL(String serverURI) throws URISyntaxException {
+ URI uri = new URI(serverURI);
+ return new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), null, null, null).toString();
+ }
+
+ public static String defaultClientName(Context context) {
+ String name = AppConstants.MOZ_APP_DISPLAYNAME; // The display name is never translated.
+ // Change "Firefox Aurora" or similar into "Aurora".
+ if (name.contains("Aurora")) {
+ name = "Aurora";
+ } else if (name.contains("Beta")) {
+ name = "Beta";
+ } else if (name.contains("Nightly")) {
+ name = "Nightly";
+ }
+ return context.getResources().getString(R.string.sync_default_client_name, name, android.os.Build.MODEL);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/PasswordStretcher.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/PasswordStretcher.java
new file mode 100644
index 0000000000..2debf3c77e
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/PasswordStretcher.java
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.fxa;
+
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+
+public interface PasswordStretcher {
+ public byte[] getQuickStretchedPW(byte[] emailUTF8) throws UnsupportedEncodingException, GeneralSecurityException;
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/QuickPasswordStretcher.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/QuickPasswordStretcher.java
new file mode 100644
index 0000000000..bf4b1bc975
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/QuickPasswordStretcher.java
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.fxa;
+
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.mozilla.gecko.sync.Utils;
+
+public class QuickPasswordStretcher implements PasswordStretcher {
+ protected final String password;
+ protected final Map<String, String> cache = new HashMap<String, String>();
+
+ public QuickPasswordStretcher(String password) {
+ this.password = password;
+ }
+
+ @Override
+ public synchronized byte[] getQuickStretchedPW(byte[] emailUTF8) throws UnsupportedEncodingException, GeneralSecurityException {
+ if (emailUTF8 == null) {
+ throw new IllegalArgumentException("emailUTF8 must not be null");
+ }
+ String key = Utils.byte2Hex(emailUTF8);
+ if (!cache.containsKey(key)) {
+ byte[] value = FxAccountUtils.generateQuickStretchedPW(emailUTF8, password.getBytes("UTF-8"));
+ cache.put(key, Utils.byte2Hex(value));
+ return value;
+ }
+ return Utils.hex2Byte(cache.get(key));
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/SkewHandler.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/SkewHandler.java
new file mode 100644
index 0000000000..9d0ad5e030
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/SkewHandler.java
@@ -0,0 +1,111 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.fxa;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.net.Resource;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpHeaders;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.impl.cookie.DateParseException;
+import ch.boye.httpclientandroidlib.impl.cookie.DateUtils;
+
+public class SkewHandler {
+ private static final String LOG_TAG = "SkewHandler";
+ protected volatile long skewMillis = 0L;
+ protected final String hostname;
+
+ private static final HashMap<String, SkewHandler> skewHandlers = new HashMap<String, SkewHandler>();
+
+ public static SkewHandler getSkewHandlerForResource(final Resource resource) {
+ return getSkewHandlerForHostname(resource.getHostname());
+ }
+
+ public static SkewHandler getSkewHandlerFromEndpointString(final String url) throws URISyntaxException {
+ if (url == null) {
+ throw new IllegalArgumentException("url must not be null.");
+ }
+ URI u = new URI(url);
+ return getSkewHandlerForHostname(u.getHost());
+ }
+
+ public static synchronized SkewHandler getSkewHandlerForHostname(final String hostname) {
+ SkewHandler handler = skewHandlers.get(hostname);
+ if (handler == null) {
+ handler = new SkewHandler(hostname);
+ skewHandlers.put(hostname, handler);
+ }
+ return handler;
+ }
+
+ public static synchronized void clearSkewHandlers() {
+ skewHandlers.clear();
+ }
+
+ public SkewHandler(final String hostname) {
+ this.hostname = hostname;
+ }
+
+ public boolean updateSkewFromServerMillis(long millis, long now) {
+ skewMillis = millis - now;
+ Logger.debug(LOG_TAG, "Updated skew: " + skewMillis + "ms for hostname " + this.hostname);
+ return true;
+ }
+
+ public boolean updateSkewFromHTTPDateString(String date, long now) {
+ try {
+ final long millis = DateUtils.parseDate(date).getTime();
+ return updateSkewFromServerMillis(millis, now);
+ } catch (DateParseException e) {
+ Logger.warn(LOG_TAG, "Unexpected: invalid Date header from " + this.hostname);
+ return false;
+ }
+ }
+
+ public boolean updateSkewFromDateHeader(Header header, long now) {
+ String date = header.getValue();
+ if (null == date) {
+ Logger.warn(LOG_TAG, "Unexpected: null Date header from " + this.hostname);
+ return false;
+ }
+ return updateSkewFromHTTPDateString(date, now);
+ }
+
+ /**
+ * Update our tracked skew value to account for the local clock differing from
+ * the server's.
+ *
+ * @param response
+ * the received HTTP response.
+ * @param now
+ * the current time in milliseconds.
+ * @return true if the skew value was updated, false otherwise.
+ */
+ public boolean updateSkew(HttpResponse response, long now) {
+ Header header = response.getFirstHeader(HttpHeaders.DATE);
+ if (null == header) {
+ Logger.warn(LOG_TAG, "Unexpected: missing Date header from " + this.hostname);
+ return false;
+ }
+ return updateSkewFromDateHeader(header, now);
+ }
+
+ public long getSkewInMillis() {
+ return skewMillis;
+ }
+
+ public long getSkewInSeconds() {
+ return skewMillis / 1000;
+ }
+
+ public void resetSkew() {
+ skewMillis = 0L;
+ }
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountAbstractClient.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountAbstractClient.java
new file mode 100644
index 0000000000..4bdaa6690c
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountAbstractClient.java
@@ -0,0 +1,224 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.fxa.oauth;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.Locale;
+import java.util.concurrent.Executor;
+
+import org.mozilla.gecko.background.fxa.FxAccountClientException;
+import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientMalformedResponseException;
+import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.Locales;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+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.sync.net.SyncResponse;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+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;
+
+public abstract class FxAccountAbstractClient {
+ protected static final String LOG_TAG = FxAccountAbstractClient.class.getSimpleName();
+
+ protected static final String ACCEPT_HEADER = "application/json;charset=utf-8";
+ protected static final String AUTHORIZATION_RESPONSE_TYPE = "token";
+
+ public static final String JSON_KEY_ERROR = "error";
+ public static final String JSON_KEY_MESSAGE = "message";
+ public static final String JSON_KEY_CODE = "code";
+ public static final String JSON_KEY_ERRNO = "errno";
+
+ protected static final String[] requiredErrorStringFields = { JSON_KEY_ERROR, JSON_KEY_MESSAGE };
+ protected static final String[] requiredErrorLongFields = { JSON_KEY_CODE, JSON_KEY_ERRNO };
+
+ /**
+ * The server's URI.
+ * <p>
+ * We assume throughout that this ends with a trailing slash (and guarantee as
+ * much in the constructor).
+ */
+ protected final String serverURI;
+
+ protected final Executor executor;
+
+ public FxAccountAbstractClient(String serverURI, Executor executor) {
+ if (serverURI == null) {
+ throw new IllegalArgumentException("Must provide a server URI.");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("Must provide a non-null executor.");
+ }
+ this.serverURI = serverURI.endsWith("/") ? serverURI : serverURI + "/";
+ if (!this.serverURI.endsWith("/")) {
+ throw new IllegalArgumentException("Constructed serverURI must end with a trailing slash: " + this.serverURI);
+ }
+ this.executor = executor;
+ }
+
+ /**
+ * Process a typed value extracted from a successful response (in an
+ * endpoint-dependent way).
+ */
+ public interface RequestDelegate<T> {
+ public void handleError(Exception e);
+ public void handleFailure(FxAccountAbstractClientRemoteException e);
+ public void handleSuccess(T result);
+ }
+
+ /**
+ * Intepret a response from the auth server.
+ * <p>
+ * Throw an appropriate exception on errors; otherwise, return the response's
+ * status code.
+ *
+ * @return response's HTTP status code.
+ * @throws FxAccountClientException
+ */
+ public static int validateResponse(HttpResponse response) throws FxAccountAbstractClientRemoteException {
+ final int status = response.getStatusLine().getStatusCode();
+ if (status == 200) {
+ return status;
+ }
+ int code;
+ int errno;
+ String error;
+ String message;
+ ExtendedJSONObject body;
+ try {
+ body = new SyncStorageResponse(response).jsonObjectBody();
+ body.throwIfFieldsMissingOrMisTyped(requiredErrorStringFields, String.class);
+ body.throwIfFieldsMissingOrMisTyped(requiredErrorLongFields, Long.class);
+ code = body.getLong(JSON_KEY_CODE).intValue();
+ errno = body.getLong(JSON_KEY_ERRNO).intValue();
+ error = body.getString(JSON_KEY_ERROR);
+ message = body.getString(JSON_KEY_MESSAGE);
+ } catch (Exception e) {
+ throw new FxAccountAbstractClientMalformedResponseException(response);
+ }
+ throw new FxAccountAbstractClientRemoteException(response, code, errno, error, message, body);
+ }
+
+ protected <T> void invokeHandleError(final RequestDelegate<T> delegate, final Exception e) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ delegate.handleError(e);
+ }
+ });
+ }
+
+ protected <T> void post(BaseResource resource, final ExtendedJSONObject requestBody, final RequestDelegate<T> delegate) {
+ try {
+ if (requestBody == null) {
+ resource.post((HttpEntity) null);
+ } else {
+ resource.post(requestBody);
+ }
+ } catch (Exception e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+ }
+
+ /**
+ * Translate resource callbacks into request callbacks invoked on the provided
+ * executor.
+ * <p>
+ * Override <code>handleSuccess</code> to parse the body of the resource
+ * request and call the request callback. <code>handleSuccess</code> is
+ * invoked via the executor, so you don't need to delegate further.
+ */
+ protected abstract class ResourceDelegate<T> extends BaseResourceDelegate {
+ protected abstract void handleSuccess(final int status, HttpResponse response, final ExtendedJSONObject body);
+
+ protected final RequestDelegate<T> delegate;
+
+ /**
+ * Create a delegate for an un-authenticated resource.
+ */
+ public ResourceDelegate(final Resource resource, final RequestDelegate<T> delegate) {
+ super(resource);
+ this.delegate = delegate;
+ }
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return super.getAuthHeaderProvider();
+ }
+
+ @Override
+ public String getUserAgent() {
+ return FxAccountConstants.USER_AGENT;
+ }
+
+ @Override
+ public void handleHttpResponse(HttpResponse response) {
+ try {
+ final int status = validateResponse(response);
+ invokeHandleSuccess(status, response);
+ } catch (FxAccountAbstractClientRemoteException e) {
+ invokeHandleFailure(e);
+ }
+ }
+
+ protected void invokeHandleFailure(final FxAccountAbstractClientRemoteException e) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ delegate.handleFailure(e);
+ }
+ });
+ }
+
+ protected void invokeHandleSuccess(final int status, final HttpResponse response) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ ExtendedJSONObject body = new SyncResponse(response).jsonObjectBody();
+ ResourceDelegate.this.handleSuccess(status, response, body);
+ } catch (Exception e) {
+ delegate.handleError(e);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void handleHttpProtocolException(final ClientProtocolException e) {
+ invokeHandleError(delegate, e);
+ }
+
+ @Override
+ public void handleHttpIOException(IOException e) {
+ invokeHandleError(delegate, e);
+ }
+
+ @Override
+ public void handleTransportException(GeneralSecurityException e) {
+ invokeHandleError(delegate, e);
+ }
+
+ @Override
+ public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
+ super.addHeaders(request, client);
+
+ // The basics.
+ final Locale locale = Locale.getDefault();
+ request.addHeader(HttpHeaders.ACCEPT_LANGUAGE, Locales.getLanguageTag(locale));
+ request.addHeader(HttpHeaders.ACCEPT, ACCEPT_HEADER);
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountAbstractClientException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountAbstractClientException.java
new file mode 100644
index 0000000000..21025af0a7
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountAbstractClientException.java
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.fxa.oauth;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.HTTPFailureException;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+
+/**
+ * From <a href="https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md">https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md</a>.
+ */
+public class FxAccountAbstractClientException extends Exception {
+ private static final long serialVersionUID = 1953459541558266597L;
+
+ public FxAccountAbstractClientException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ public FxAccountAbstractClientException(Exception e) {
+ super(e);
+ }
+
+ public static class FxAccountAbstractClientRemoteException extends FxAccountAbstractClientException {
+ private static final long serialVersionUID = 1209313149952001097L;
+
+ public final HttpResponse response;
+ public final long httpStatusCode;
+ public final long apiErrorNumber;
+ public final String error;
+ public final String message;
+ public final ExtendedJSONObject body;
+
+ public FxAccountAbstractClientRemoteException(HttpResponse response, long httpStatusCode, long apiErrorNumber, String error, String message, ExtendedJSONObject body) {
+ super(new HTTPFailureException(new SyncStorageResponse(response)));
+ if (body == null) {
+ throw new IllegalArgumentException("body must not be null");
+ }
+ this.response = response;
+ this.httpStatusCode = httpStatusCode;
+ this.apiErrorNumber = apiErrorNumber;
+ this.error = error;
+ this.message = message;
+ this.body = body;
+ }
+
+ @Override
+ public String toString() {
+ return "<FxAccountAbstractClientRemoteException " + this.httpStatusCode + " [" + this.apiErrorNumber + "]: " + this.message + ">";
+ }
+
+ public boolean isInvalidAuthentication() {
+ return this.httpStatusCode == HttpStatus.SC_UNAUTHORIZED;
+ }
+ }
+
+ public static class FxAccountAbstractClientMalformedResponseException extends FxAccountAbstractClientRemoteException {
+ private static final long serialVersionUID = 1209313149952001098L;
+
+ public FxAccountAbstractClientMalformedResponseException(HttpResponse response) {
+ super(response, 0, FxAccountOAuthRemoteError.UNKNOWN_ERROR, "Response malformed", "Response malformed", new ExtendedJSONObject());
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountOAuthClient10.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountOAuthClient10.java
new file mode 100644
index 0000000000..4f233695bc
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountOAuthClient10.java
@@ -0,0 +1,129 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.fxa.oauth;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.concurrent.Executor;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.net.BaseResource;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+
+/**
+ * Talk to an fxa-oauth-server to get "implicitly granted" OAuth tokens.
+ * <p>
+ * To use this client, you will need a pre-allocated fxa-oauth-server
+ * "client_id" with special "implicit grant" permissions.
+ * <p>
+ * This client was written against the API documented at <a href="https://github.com/mozilla/fxa-oauth-server/blob/41538990df9e91158558ae5a8115194383ac3b05/docs/api.md">https://github.com/mozilla/fxa-oauth-server/blob/41538990df9e91158558ae5a8115194383ac3b05/docs/api.md</a>.
+ */
+public class FxAccountOAuthClient10 extends FxAccountAbstractClient {
+ protected static final String LOG_TAG = FxAccountOAuthClient10.class.getSimpleName();
+
+ protected static final String AUTHORIZATION_RESPONSE_TYPE = "token";
+
+ protected static final String JSON_KEY_ACCESS_TOKEN = "access_token";
+ protected static final String JSON_KEY_ASSERTION = "assertion";
+ protected static final String JSON_KEY_CLIENT_ID = "client_id";
+ protected static final String JSON_KEY_RESPONSE_TYPE = "response_type";
+ protected static final String JSON_KEY_SCOPE = "scope";
+ protected static final String JSON_KEY_STATE = "state";
+ protected static final String JSON_KEY_TOKEN = "token";
+ protected static final String JSON_KEY_TOKEN_TYPE = "token_type";
+
+ // access_token: A string that can be used for authorized requests to service providers.
+ // scope: A string of space-separated permissions that this token has. May differ from requested scopes, since user can deny permissions.
+ // token_type: A string representing the token type. Currently will always be "bearer".
+ protected static final String[] AUTHORIZATION_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_ACCESS_TOKEN, JSON_KEY_SCOPE, JSON_KEY_TOKEN_TYPE };
+
+ public FxAccountOAuthClient10(String serverURI, Executor executor) {
+ super(serverURI, executor);
+ }
+
+ /**
+ * Thin container for an authorization response.
+ */
+ public static class AuthorizationResponse {
+ public final String access_token;
+ public final String token_type;
+ public final String scope;
+
+ public AuthorizationResponse(String access_token, String token_type, String scope) {
+ this.access_token = access_token;
+ this.token_type = token_type;
+ this.scope = scope;
+ }
+ }
+
+ public void authorization(String client_id, String assertion, String state, String scope,
+ RequestDelegate<AuthorizationResponse> delegate) {
+ final BaseResource resource;
+ try {
+ resource = new BaseResource(new URI(serverURI + "authorization"));
+ } catch (URISyntaxException e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ resource.delegate = new ResourceDelegate<AuthorizationResponse>(resource, delegate) {
+ @Override
+ public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+ try {
+ body.throwIfFieldsMissingOrMisTyped(AUTHORIZATION_RESPONSE_REQUIRED_STRING_FIELDS, String.class);
+ String access_token = body.getString(JSON_KEY_ACCESS_TOKEN);
+ String token_type = body.getString(JSON_KEY_TOKEN_TYPE);
+ String scope = body.getString(JSON_KEY_SCOPE);
+ delegate.handleSuccess(new AuthorizationResponse(access_token, token_type, scope));
+ return;
+ } catch (Exception e) {
+ delegate.handleError(e);
+ return;
+ }
+ }
+ };
+
+ final ExtendedJSONObject requestBody = new ExtendedJSONObject();
+ requestBody.put(JSON_KEY_RESPONSE_TYPE, AUTHORIZATION_RESPONSE_TYPE);
+ requestBody.put(JSON_KEY_CLIENT_ID, client_id);
+ requestBody.put(JSON_KEY_ASSERTION, assertion);
+ if (scope != null) {
+ requestBody.put(JSON_KEY_SCOPE, scope);
+ }
+ if (state != null) {
+ requestBody.put(JSON_KEY_STATE, state);
+ }
+
+ post(resource, requestBody, delegate);
+ }
+
+ public void deleteToken(final String token, final RequestDelegate<Void> delegate) {
+ final BaseResource resource;
+ try {
+ resource = new BaseResource(new URI(serverURI + "destroy"));
+ } catch (URISyntaxException e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ resource.delegate = new ResourceDelegate<Void>(resource, delegate) {
+ @Override
+ public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+ try {
+ delegate.handleSuccess(null);
+ return;
+ } catch (Exception e) {
+ delegate.handleError(e);
+ return;
+ }
+ }
+ };
+
+ final ExtendedJSONObject requestBody = new ExtendedJSONObject();
+ requestBody.put(JSON_KEY_TOKEN, token);
+ post(resource, requestBody, delegate);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountOAuthRemoteError.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountOAuthRemoteError.java
new file mode 100644
index 0000000000..d949d316be
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountOAuthRemoteError.java
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.fxa.oauth;
+
+public interface FxAccountOAuthRemoteError {
+ public static final int ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS = 101;
+ public static final int UNKNOWN_CLIENT_ID = 101;
+ public static final int INCORRECT_CLIENT_SECRET = 102;
+ public static final int REDIRECT_URI_DOES_NOT_MATCH_REGISTERED_VALUE = 103;
+ public static final int INVALID_FXA_ASSERTION = 104;
+ public static final int UNKNOWN_CODE = 105;
+ public static final int INCORRECT_CODE = 106;
+ public static final int EXPIRED_CODE = 107;
+ public static final int INVALID_TOKEN = 108;
+ public static final int INVALID_REQUEST_PARAMETER = 109;
+ public static final int UNKNOWN_ERROR = 999;
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/profile/FxAccountProfileClient10.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/profile/FxAccountProfileClient10.java
new file mode 100644
index 0000000000..cb851a8db3
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/profile/FxAccountProfileClient10.java
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.fxa.profile;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.concurrent.Executor;
+
+import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BearerAuthHeaderProvider;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+
+
+/**
+ * Talk to an fxa-profile-server to get profile information like name, age, gender, and avatar image.
+ * <p>
+ * This client was written against the API documented at <a href="https://github.com/mozilla/fxa-profile-server/blob/0c065619f5a2e867f813a343b4c67da3fe2c82a4/docs/API.md">https://github.com/mozilla/fxa-profile-server/blob/0c065619f5a2e867f813a343b4c67da3fe2c82a4/docs/API.md</a>.
+ */
+public class FxAccountProfileClient10 extends FxAccountAbstractClient {
+ public FxAccountProfileClient10(String serverURI, Executor executor) {
+ super(serverURI, executor);
+ }
+
+ public void profile(final String token, RequestDelegate<ExtendedJSONObject> delegate) {
+ BaseResource resource;
+ try {
+ resource = new BaseResource(new URI(serverURI + "profile"));
+ } catch (URISyntaxException e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ resource.delegate = new ResourceDelegate<ExtendedJSONObject>(resource, delegate) {
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return new BearerAuthHeaderProvider(token);
+ }
+
+ @Override
+ public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+ try {
+ delegate.handleSuccess(body);
+ return;
+ } catch (Exception e) {
+ delegate.handleError(e);
+ return;
+ }
+ }
+ };
+
+ resource.get();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/nativecode/NativeCrypto.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/nativecode/NativeCrypto.java
new file mode 100644
index 0000000000..25f0f84d9b
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/nativecode/NativeCrypto.java
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.nativecode;
+
+import java.security.GeneralSecurityException;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.AppConstants;
+
+import android.util.Log;
+
+@RobocopTarget
+public class NativeCrypto {
+ static {
+ try {
+ System.loadLibrary("mozglue");
+ } catch (UnsatisfiedLinkError e) {
+ Log.wtf("NativeCrypto", "Couldn't load mozglue. Trying /data/app-lib path.");
+ try {
+ System.load("/data/app-lib/" + AppConstants.ANDROID_PACKAGE_NAME + "/libmozglue.so");
+ } catch (Throwable ee) {
+ try {
+ Log.wtf("NativeCrypto", "Couldn't load mozglue: " + ee + ". Trying /data/data path.");
+ System.load("/data/data/" + AppConstants.ANDROID_PACKAGE_NAME + "/lib/libmozglue.so");
+ } catch (UnsatisfiedLinkError eee) {
+ Log.wtf("NativeCrypto", "Failed every attempt to load mozglue. Giving up.");
+ throw new RuntimeException("Unable to load mozglue", eee);
+ }
+ }
+ }
+ }
+
+ /**
+ * Wrapper to perform PBKDF2-HMAC-SHA-256 in native code.
+ */
+ public native static byte[] pbkdf2SHA256(byte[] password, byte[] salt, int c, int dkLen)
+ throws GeneralSecurityException;
+
+ /**
+ * Wrapper to perform SHA-1 in native code.
+ */
+ public native static byte[] sha1(byte[] str);
+
+ /**
+ * Wrapper to perform SHA-256 init in native code. Returns a SHA-256 context.
+ */
+ public native static byte[] sha256init();
+
+ /**
+ * Wrapper to update a SHA-256 context in native code.
+ */
+ public native static void sha256update(byte[] ctx, byte[] str, int len);
+
+ /**
+ * Wrapper to finalize a SHA-256 context in native code. Returns digest.
+ */
+ public native static byte[] sha256finalize(byte[] ctx);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/preferences/PreferenceFragment.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/preferences/PreferenceFragment.java
new file mode 100644
index 0000000000..5bc5422c80
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/preferences/PreferenceFragment.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.background.preferences;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.util.WeakReferenceHandler;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.preference.Preference;
+import android.preference.PreferenceGroup;
+import android.preference.PreferenceManager;
+import android.preference.PreferenceScreen;
+import android.support.v4.app.Fragment;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnKeyListener;
+import android.view.ViewGroup;
+import android.widget.ListView;
+
+public abstract class PreferenceFragment extends Fragment implements PreferenceManagerCompat.OnPreferenceTreeClickListener {
+ private static final String PREFERENCES_TAG = "android:preferences";
+
+ private PreferenceManager mPreferenceManager;
+ private ListView mList;
+ private boolean mHavePrefs;
+ private boolean mInitDone;
+
+ /**
+ * The starting request code given out to preference framework.
+ */
+ private static final int FIRST_REQUEST_CODE = 100;
+
+ private static final int MSG_BIND_PREFERENCES = 1;
+
+ private static class PreferenceFragmentHandler extends WeakReferenceHandler<PreferenceFragment> {
+ public PreferenceFragmentHandler(final PreferenceFragment that) {
+ super(that);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ final PreferenceFragment that = mTarget.get();
+ if (that == null) {
+ return;
+ }
+
+ switch (msg.what) {
+
+ case MSG_BIND_PREFERENCES:
+ that.bindPreferences();
+ break;
+ }
+ }
+ }
+
+ private final Handler mHandler = new PreferenceFragmentHandler(this);
+
+ final private Runnable mRequestFocus = new Runnable() {
+ @Override
+ public void run() {
+ mList.focusableViewAvailable(mList);
+ }
+ };
+
+ /**
+ * Interface that PreferenceFragment's containing activity should
+ * implement to be able to process preference items that wish to
+ * switch to a new fragment.
+ */
+ public interface OnPreferenceStartFragmentCallback {
+ /**
+ * Called when the user has clicked on a Preference that has
+ * a fragment class name associated with it. The implementation
+ * to should instantiate and switch to an instance of the given
+ * fragment.
+ */
+ boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref);
+ }
+
+ @Override
+ public void onCreate(Bundle paramBundle) {
+ super.onCreate(paramBundle);
+ mPreferenceManager = PreferenceManagerCompat.newInstance(getActivity(), FIRST_REQUEST_CODE);
+ PreferenceManagerCompat.setFragment(mPreferenceManager, this);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater paramLayoutInflater, ViewGroup paramViewGroup, Bundle paramBundle) {
+ return paramLayoutInflater.inflate(R.layout.fxaccount_preference_list_fragment, paramViewGroup,
+ false);
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ if (mHavePrefs) {
+ bindPreferences();
+ }
+
+ mInitDone = true;
+
+ if (savedInstanceState != null) {
+ Bundle container = savedInstanceState.getBundle(PREFERENCES_TAG);
+ if (container != null) {
+ final PreferenceScreen preferenceScreen = getPreferenceScreen();
+ if (preferenceScreen != null) {
+ preferenceScreen.restoreHierarchyState(container);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ PreferenceManagerCompat.setOnPreferenceTreeClickListener(mPreferenceManager, this);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ PreferenceManagerCompat.dispatchActivityStop(mPreferenceManager);
+ PreferenceManagerCompat.setOnPreferenceTreeClickListener(mPreferenceManager, null);
+ }
+
+ @Override
+ public void onDestroyView() {
+ mList = null;
+ mHandler.removeCallbacks(mRequestFocus);
+ mHandler.removeMessages(MSG_BIND_PREFERENCES);
+ super.onDestroyView();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ PreferenceManagerCompat.dispatchActivityDestroy(mPreferenceManager);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ final PreferenceScreen preferenceScreen = getPreferenceScreen();
+ if (preferenceScreen != null) {
+ Bundle container = new Bundle();
+ preferenceScreen.saveHierarchyState(container);
+ outState.putBundle(PREFERENCES_TAG, container);
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ PreferenceManagerCompat.dispatchActivityResult(mPreferenceManager, requestCode, resultCode, data);
+ }
+
+ /**
+ * Returns the {@link PreferenceManager} used by this fragment.
+ * @return The {@link PreferenceManager}.
+ */
+ public PreferenceManager getPreferenceManager() {
+ return mPreferenceManager;
+ }
+
+ /**
+ * Sets the root of the preference hierarchy that this fragment is showing.
+ *
+ * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy.
+ */
+ public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
+ if (PreferenceManagerCompat.setPreferences(mPreferenceManager, preferenceScreen) && preferenceScreen != null) {
+ mHavePrefs = true;
+ if (mInitDone) {
+ postBindPreferences();
+ }
+ }
+ }
+
+ /**
+ * Gets the root of the preference hierarchy that this fragment is showing.
+ *
+ * @return The {@link PreferenceScreen} that is the root of the preference
+ * hierarchy.
+ */
+ public PreferenceScreen getPreferenceScreen() {
+ return PreferenceManagerCompat.getPreferenceScreen(mPreferenceManager);
+ }
+
+ /**
+ * Adds preferences from activities that match the given {@link Intent}.
+ *
+ * @param intent The {@link Intent} to query activities.
+ */
+ public void addPreferencesFromIntent(Intent intent) {
+ requirePreferenceManager();
+
+ setPreferenceScreen(PreferenceManagerCompat.inflateFromIntent(mPreferenceManager, intent, getPreferenceScreen()));
+ }
+
+ /**
+ * Inflates the given XML resource and adds the preference hierarchy to the current
+ * preference hierarchy.
+ *
+ * @param preferencesResId The XML resource ID to inflate.
+ */
+ public void addPreferencesFromResource(int preferencesResId) {
+ requirePreferenceManager();
+
+ setPreferenceScreen(PreferenceManagerCompat.inflateFromResource(mPreferenceManager, getActivity(),
+ preferencesResId, getPreferenceScreen()));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen,
+ Preference preference) {
+ //if (preference.getFragment() != null &&
+ if (
+ getActivity() instanceof OnPreferenceStartFragmentCallback) {
+ return ((OnPreferenceStartFragmentCallback)getActivity()).onPreferenceStartFragment(
+ this, preference);
+ }
+ return false;
+ }
+
+ /**
+ * Finds a {@link Preference} based on its key.
+ *
+ * @param key The key of the preference to retrieve.
+ * @return The {@link Preference} with the key, or null.
+ * @see PreferenceGroup#findPreference(CharSequence)
+ */
+ public Preference findPreference(CharSequence key) {
+ if (mPreferenceManager == null) {
+ return null;
+ }
+ return mPreferenceManager.findPreference(key);
+ }
+
+ private void requirePreferenceManager() {
+ if (mPreferenceManager == null) {
+ throw new RuntimeException("This should be called after super.onCreate.");
+ }
+ }
+
+ private void postBindPreferences() {
+ if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return;
+ mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget();
+ }
+
+ private void bindPreferences() {
+ final PreferenceScreen preferenceScreen = getPreferenceScreen();
+ if (preferenceScreen != null) {
+ preferenceScreen.bind(getListView());
+ }
+ }
+
+ public ListView getListView() {
+ ensureList();
+ return mList;
+ }
+
+ private void ensureList() {
+ if (mList != null) {
+ return;
+ }
+ View root = getView();
+ if (root == null) {
+ throw new IllegalStateException("Content view not yet created");
+ }
+ View rawListView = root.findViewById(android.R.id.list);
+ if (!(rawListView instanceof ListView)) {
+ throw new RuntimeException(
+ "Content has view with id attribute 'android.R.id.list' "
+ + "that is not a ListView class");
+ }
+ mList = (ListView)rawListView;
+ if (mList == null) {
+ throw new RuntimeException(
+ "Your content must have a ListView whose id attribute is " +
+ "'android.R.id.list'");
+ }
+ mList.setOnKeyListener(mListOnKeyListener);
+ mHandler.post(mRequestFocus);
+ }
+
+ private final OnKeyListener mListOnKeyListener = new OnKeyListener() {
+
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ Object selectedItem = mList.getSelectedItem();
+ if (selectedItem instanceof Preference) {
+ @SuppressWarnings("unused")
+ View selectedView = mList.getSelectedView();
+ //return ((Preference)selectedItem).onKey(
+ // selectedView, keyCode, event);
+ return false;
+ }
+ return false;
+ }
+
+ };
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/preferences/PreferenceManagerCompat.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/preferences/PreferenceManagerCompat.java
new file mode 100644
index 0000000000..22c62e4318
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/preferences/PreferenceManagerCompat.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.background.preferences;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.preference.Preference;
+import android.preference.PreferenceManager;
+import android.preference.PreferenceScreen;
+import android.util.Log;
+
+public class PreferenceManagerCompat {
+
+ private static final String TAG = PreferenceManagerCompat.class.getSimpleName();
+
+ /**
+ * Interface definition for a callback to be invoked when a {@link Preference} in the hierarchy
+ * rooted at this {@link PreferenceScreen} is clicked.
+ */
+ interface OnPreferenceTreeClickListener {
+ /**
+ * Called when a preference in the tree rooted at this {@link PreferenceScreen} has been
+ * clicked.
+ *
+ * @param preferenceScreen The {@link PreferenceScreen} that the preference is located in.
+ * @param preference The preference that was clicked.
+ *
+ * @return Whether the click was handled.
+ */
+ boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference);
+ }
+
+ static PreferenceManager newInstance(Activity activity, int firstRequestCode) {
+ try {
+ Constructor<PreferenceManager> c = PreferenceManager.class.getDeclaredConstructor(Activity.class, int.class);
+ c.setAccessible(true);
+ return c.newInstance(activity, firstRequestCode);
+ } catch (Exception e) {
+ Log.w(TAG, "Couldn't call constructor PreferenceManager by reflection", e);
+ }
+ return null;
+ }
+
+ /**
+ * Sets the owning preference fragment
+ */
+ static void setFragment(PreferenceManager manager, PreferenceFragment fragment) {
+ // stub
+ }
+
+ /**
+ * Sets the callback to be invoked when a {@link Preference} in the hierarchy rooted at this
+ * {@link PreferenceManager} is clicked.
+ *
+ * @param listener The callback to be invoked.
+ */
+ static void setOnPreferenceTreeClickListener(PreferenceManager manager, final OnPreferenceTreeClickListener listener) {
+ try {
+ Field onPreferenceTreeClickListener = PreferenceManager.class.getDeclaredField("mOnPreferenceTreeClickListener");
+ onPreferenceTreeClickListener.setAccessible(true);
+ if (listener != null) {
+ Object proxy = Proxy.newProxyInstance(
+ onPreferenceTreeClickListener.getType().getClassLoader(),
+ new Class<?>[] { onPreferenceTreeClickListener.getType() },
+ new InvocationHandler() {
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) {
+ if (method.getName().equals("onPreferenceTreeClick")) {
+ return listener.onPreferenceTreeClick((PreferenceScreen) args[0], (Preference) args[1]);
+ } else {
+ return null;
+ }
+ }
+ });
+ onPreferenceTreeClickListener.set(manager, proxy);
+ } else {
+ onPreferenceTreeClickListener.set(manager, null);
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Couldn't set PreferenceManager.mOnPreferenceTreeClickListener by reflection", e);
+ }
+ }
+
+ /**
+ * Inflates a preference hierarchy from the preference hierarchies of {@link Activity Activities}
+ * that match the given {@link Intent}. An {@link Activity} defines its preference hierarchy with
+ * meta-data using the {@link #METADATA_KEY_PREFERENCES} key.
+ * <p/>
+ * If a preference hierarchy is given, the new preference hierarchies will be merged in.
+ *
+ * @param queryIntent The intent to match activities.
+ * @param rootPreferences Optional existing hierarchy to merge the new hierarchies into.
+ *
+ * @return The root hierarchy (if one was not provided, the new hierarchy's root).
+ */
+ static PreferenceScreen inflateFromIntent(PreferenceManager manager, Intent intent, PreferenceScreen screen) {
+ try {
+ Method m = PreferenceManager.class.getDeclaredMethod("inflateFromIntent", Intent.class, PreferenceScreen.class);
+ m.setAccessible(true);
+ PreferenceScreen prefScreen = (PreferenceScreen) m.invoke(manager, intent, screen);
+ return prefScreen;
+ } catch (Exception e) {
+ Log.w(TAG, "Couldn't call PreferenceManager.inflateFromIntent by reflection", e);
+ }
+ return null;
+ }
+
+ /**
+ * Inflates a preference hierarchy from XML. If a preference hierarchy is given, the new
+ * preference hierarchies will be merged in.
+ *
+ * @param context The context of the resource.
+ * @param resId The resource ID of the XML to inflate.
+ * @param rootPreferences Optional existing hierarchy to merge the new hierarchies into.
+ *
+ * @return The root hierarchy (if one was not provided, the new hierarchy's root).
+ *
+ * @hide
+ */
+ static PreferenceScreen inflateFromResource(PreferenceManager manager, Activity activity, int resId, PreferenceScreen screen) {
+ try {
+ Method m = PreferenceManager.class.getDeclaredMethod("inflateFromResource", Context.class, int.class, PreferenceScreen.class);
+ m.setAccessible(true);
+ PreferenceScreen prefScreen = (PreferenceScreen) m.invoke(manager, activity, resId, screen);
+ return prefScreen;
+ } catch (Exception e) {
+ Log.w(TAG, "Couldn't call PreferenceManager.inflateFromResource by reflection", e);
+ }
+ return null;
+ }
+
+ /**
+ * Returns the root of the preference hierarchy managed by this class.
+ *
+ * @return The {@link PreferenceScreen} object that is at the root of the hierarchy.
+ */
+ static PreferenceScreen getPreferenceScreen(PreferenceManager manager) {
+ try {
+ Method m = PreferenceManager.class.getDeclaredMethod("getPreferenceScreen");
+ m.setAccessible(true);
+ return (PreferenceScreen) m.invoke(manager);
+ } catch (Exception e) {
+ Log.w(TAG, "Couldn't call PreferenceManager.getPreferenceScreen by reflection", e);
+ }
+ return null;
+ }
+
+ /**
+ * Called by the {@link PreferenceManager} to dispatch a subactivity result.
+ */
+ static void dispatchActivityResult(PreferenceManager manager, int requestCode, int resultCode, Intent data) {
+ try {
+ Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityResult", int.class, int.class, Intent.class);
+ m.setAccessible(true);
+ m.invoke(manager, requestCode, resultCode, data);
+ } catch (Exception e) {
+ Log.w(TAG, "Couldn't call PreferenceManager.dispatchActivityResult by reflection", e);
+ }
+ }
+
+ /**
+ * Called by the {@link PreferenceManager} to dispatch the activity stop event.
+ */
+ static void dispatchActivityStop(PreferenceManager manager) {
+ try {
+ Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityStop");
+ m.setAccessible(true);
+ m.invoke(manager);
+ } catch (Exception e) {
+ Log.w(TAG, "Couldn't call PreferenceManager.dispatchActivityStop by reflection", e);
+ }
+ }
+
+ /**
+ * Called by the {@link PreferenceManager} to dispatch the activity destroy event.
+ */
+ static void dispatchActivityDestroy(PreferenceManager manager) {
+ try {
+ Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityDestroy");
+ m.setAccessible(true);
+ m.invoke(manager);
+ } catch (Exception e) {
+ Log.w(TAG, "Couldn't call PreferenceManager.dispatchActivityDestroy by reflection", e);
+ }
+ }
+
+ /**
+ * Sets the root of the preference hierarchy.
+ *
+ * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy.
+ *
+ * @return Whether the {@link PreferenceScreen} given is different than the previous.
+ */
+ static boolean setPreferences(PreferenceManager manager, PreferenceScreen screen) {
+ try {
+ Method m = PreferenceManager.class.getDeclaredMethod("setPreferences", PreferenceScreen.class);
+ m.setAccessible(true);
+ return ((Boolean) m.invoke(manager, screen));
+ } catch (Exception e) {
+ Log.w(TAG, "Couldn't call PreferenceManager.setPreferences by reflection", e);
+ }
+ return false;
+ }
+
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/ASNUtils.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/ASNUtils.java
new file mode 100644
index 0000000000..b032067c5e
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/ASNUtils.java
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.browserid;
+
+
+/**
+ * Java produces signature in ASN.1 format. Here's some hard-coded encoding and decoding
+ * code, courtesy of a comment in
+ * <a href="http://stackoverflow.com/questions/10921733/how-sign-method-of-the-digital-signature-combines-the-r-s-values-in-to-array">http://stackoverflow.com/questions/10921733/how-sign-method-of-the-digital-signature-combines-the-r-s-values-in-to-array</a>.
+ */
+public class ASNUtils {
+ /**
+ * Decode two short arrays from ASN.1 bytes.
+ * @param input to extract.
+ * @return length 2 array of byte arrays.
+ */
+ public static byte[][] decodeTwoArraysFromASN1(byte[] input) throws IllegalArgumentException {
+ if (input == null) {
+ throw new IllegalArgumentException("input must not be null");
+ }
+ if (input.length <= 3)
+ throw new IllegalArgumentException("bad length");
+ if (input[0] != 0x30)
+ throw new IllegalArgumentException("bad encoding");
+ if ((input[1] & ((byte) 0x80)) != 0)
+ throw new IllegalArgumentException("bad length encoding");
+ if (input[2] != 0x02)
+ throw new IllegalArgumentException("bad encoding");
+ if ((input[3] & ((byte) 0x80)) != 0)
+ throw new IllegalArgumentException("bad length encoding");
+ byte rLength = input[3];
+ if (input.length <= 5 + rLength)
+ throw new IllegalArgumentException("bad length");
+ if (input[4 + rLength] != 0x02)
+ throw new IllegalArgumentException("bad encoding");
+ if ((input[5 + rLength] & (byte) 0x80) !=0)
+ throw new IllegalArgumentException("bad length encoding");
+ byte sLength = input[5 + rLength];
+ if (input.length != 6 + sLength + rLength)
+ throw new IllegalArgumentException("bad length");
+ byte[] rArr = new byte[rLength];
+ byte[] sArr = new byte[sLength];
+ System.arraycopy(input, 4, rArr, 0, rLength);
+ System.arraycopy(input, 6 + rLength, sArr, 0, sLength);
+ return new byte[][] { rArr, sArr };
+ }
+
+ /**
+ * Encode two short arrays into ASN.1 bytes.
+ * @param first array to encode.
+ * @param second array to encode.
+ * @return array.
+ */
+ public static byte[] encodeTwoArraysToASN1(byte[] first, byte[] second) throws IllegalArgumentException {
+ if (first == null) {
+ throw new IllegalArgumentException("first must not be null");
+ }
+ if (second == null) {
+ throw new IllegalArgumentException("second must not be null");
+ }
+ byte[] output = new byte[6 + first.length + second.length];
+ output[0] = 0x30;
+ if (4 + first.length + second.length > 255)
+ throw new IllegalArgumentException("bad length");
+ output[1] = (byte) (4 + first.length + second.length);
+ if ((output[1] & ((byte) 0x80)) != 0)
+ throw new IllegalArgumentException("bad length encoding");
+ output[2] = 0x02;
+ output[3] = (byte) first.length;
+ if ((output[3] & ((byte) 0x80)) != 0)
+ throw new IllegalArgumentException("bad length encoding");
+ System.arraycopy(first, 0, output, 4, first.length);
+ output[4 + first.length] = 0x02;
+ output[5 + first.length] = (byte) second.length;
+ if ((output[5 + first.length] & ((byte) 0x80)) != 0)
+ throw new IllegalArgumentException("bad length encoding");
+ System.arraycopy(second, 0, output, 6 + first.length, second.length);
+ return output;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/BrowserIDKeyPair.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/BrowserIDKeyPair.java
new file mode 100644
index 0000000000..7283a0299a
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/BrowserIDKeyPair.java
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.browserid;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+public class BrowserIDKeyPair {
+ public static final String JSON_KEY_PRIVATEKEY = "privateKey";
+ public static final String JSON_KEY_PUBLICKEY = "publicKey";
+
+ protected final SigningPrivateKey privateKey;
+ protected final VerifyingPublicKey publicKey;
+
+ public BrowserIDKeyPair(SigningPrivateKey privateKey, VerifyingPublicKey publicKey) {
+ this.privateKey = privateKey;
+ this.publicKey = publicKey;
+ }
+
+ public SigningPrivateKey getPrivate() {
+ return this.privateKey;
+ }
+
+ public VerifyingPublicKey getPublic() {
+ return this.publicKey;
+ }
+
+ public ExtendedJSONObject toJSONObject() {
+ ExtendedJSONObject o = new ExtendedJSONObject();
+ o.put(JSON_KEY_PRIVATEKEY, privateKey.toJSONObject());
+ o.put(JSON_KEY_PUBLICKEY, publicKey.toJSONObject());
+ return o;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/DSACryptoImplementation.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/DSACryptoImplementation.java
new file mode 100644
index 0000000000..a04a89c8e7
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/DSACryptoImplementation.java
@@ -0,0 +1,255 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.browserid;
+
+import android.annotation.SuppressLint;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.util.PRNGFixes;
+
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.Signature;
+import java.security.interfaces.DSAParams;
+import java.security.interfaces.DSAPrivateKey;
+import java.security.interfaces.DSAPublicKey;
+import java.security.spec.DSAPrivateKeySpec;
+import java.security.spec.DSAPublicKeySpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+
+public class DSACryptoImplementation {
+ private static final String LOG_TAG = DSACryptoImplementation.class.getSimpleName();
+
+ public static final String SIGNATURE_ALGORITHM = "SHA1withDSA";
+ public static final int SIGNATURE_LENGTH_BYTES = 40; // DSA signatures are always 40 bytes long.
+
+ /**
+ * Parameters are serialized as hex strings. Hex-versus-decimal was
+ * reverse-engineered from what the Persona public verifier accepted. We
+ * expect to follow the JOSE/JWT spec as it solidifies, and that will probably
+ * mean unifying this base.
+ */
+ protected static final int SERIALIZATION_BASE = 16;
+
+ protected static class DSAVerifyingPublicKey implements VerifyingPublicKey {
+ protected final DSAPublicKey publicKey;
+
+ public DSAVerifyingPublicKey(DSAPublicKey publicKey) {
+ this.publicKey = publicKey;
+ }
+
+ /**
+ * Serialize to a JSON object.
+ * <p>
+ * Parameters are serialized as hex strings. Hex-versus-decimal was
+ * reverse-engineered from what the Persona public verifier accepted.
+ */
+ @Override
+ public ExtendedJSONObject toJSONObject() {
+ DSAParams params = publicKey.getParams();
+ ExtendedJSONObject o = new ExtendedJSONObject();
+ o.put("algorithm", "DS");
+ o.put("y", publicKey.getY().toString(SERIALIZATION_BASE));
+ o.put("g", params.getG().toString(SERIALIZATION_BASE));
+ o.put("p", params.getP().toString(SERIALIZATION_BASE));
+ o.put("q", params.getQ().toString(SERIALIZATION_BASE));
+ return o;
+ }
+
+ @Override
+ public boolean verifyMessage(byte[] bytes, byte[] signature)
+ throws GeneralSecurityException {
+ if (bytes == null) {
+ throw new IllegalArgumentException("bytes must not be null");
+ }
+ if (signature == null) {
+ throw new IllegalArgumentException("signature must not be null");
+ }
+ if (signature.length != SIGNATURE_LENGTH_BYTES) {
+ return false;
+ }
+ byte[] first = new byte[signature.length / 2];
+ byte[] second = new byte[signature.length / 2];
+ System.arraycopy(signature, 0, first, 0, first.length);
+ System.arraycopy(signature, first.length, second, 0, second.length);
+ BigInteger r = new BigInteger(Utils.byte2Hex(first), 16);
+ BigInteger s = new BigInteger(Utils.byte2Hex(second), 16);
+ // This is awful, but encoding an extra 0 byte works better on devices.
+ byte[] encoded = ASNUtils.encodeTwoArraysToASN1(
+ Utils.hex2Byte(r.toString(16), 1 + SIGNATURE_LENGTH_BYTES / 2),
+ Utils.hex2Byte(s.toString(16), 1 + SIGNATURE_LENGTH_BYTES / 2));
+
+ final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM);
+ signer.initVerify(publicKey);
+ signer.update(bytes);
+ return signer.verify(encoded);
+ }
+ }
+
+ protected static class DSASigningPrivateKey implements SigningPrivateKey {
+ protected final DSAPrivateKey privateKey;
+
+ public DSASigningPrivateKey(DSAPrivateKey privateKey) {
+ this.privateKey = privateKey;
+ }
+
+ @Override
+ public String getAlgorithm() {
+ return "DS" + (privateKey.getParams().getP().bitLength() + 7)/8;
+ }
+
+ /**
+ * Serialize to a JSON object.
+ * <p>
+ * Parameters are serialized as decimal strings. Hex-versus-decimal was
+ * reverse-engineered from what the Persona public verifier accepted.
+ */
+ @Override
+ public ExtendedJSONObject toJSONObject() {
+ DSAParams params = privateKey.getParams();
+ ExtendedJSONObject o = new ExtendedJSONObject();
+ o.put("algorithm", "DS");
+ o.put("x", privateKey.getX().toString(SERIALIZATION_BASE));
+ o.put("g", params.getG().toString(SERIALIZATION_BASE));
+ o.put("p", params.getP().toString(SERIALIZATION_BASE));
+ o.put("q", params.getQ().toString(SERIALIZATION_BASE));
+ return o;
+ }
+
+ @SuppressLint("TrulyRandom")
+ @Override
+ public byte[] signMessage(byte[] bytes)
+ throws GeneralSecurityException {
+ if (bytes == null) {
+ throw new IllegalArgumentException("bytes must not be null");
+ }
+
+ try {
+ PRNGFixes.apply();
+ } catch (Exception e) {
+ // Not much to be done here: it was weak before, and we couldn't patch it, so it's weak now. Not worth aborting.
+ Logger.error(LOG_TAG, "Got exception applying PRNGFixes! Cryptographic data produced on this device may be weak. Ignoring.", e);
+ }
+
+ final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM);
+ signer.initSign(privateKey);
+ signer.update(bytes);
+ final byte[] signature = signer.sign();
+
+ final byte[][] arrays = ASNUtils.decodeTwoArraysFromASN1(signature);
+ BigInteger r = new BigInteger(arrays[0]);
+ BigInteger s = new BigInteger(arrays[1]);
+ // This is awful, but signatures are always 40 bytes long.
+ byte[] decoded = Utils.concatAll(
+ Utils.hex2Byte(r.toString(16), SIGNATURE_LENGTH_BYTES / 2),
+ Utils.hex2Byte(s.toString(16), SIGNATURE_LENGTH_BYTES / 2));
+ return decoded;
+ }
+ }
+
+ public static BrowserIDKeyPair generateKeyPair(int keysize)
+ throws NoSuchAlgorithmException {
+ final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
+ keyPairGenerator.initialize(keysize);
+ final KeyPair keyPair = keyPairGenerator.generateKeyPair();
+ DSAPrivateKey privateKey = (DSAPrivateKey) keyPair.getPrivate();
+ DSAPublicKey publicKey = (DSAPublicKey) keyPair.getPublic();
+ return new BrowserIDKeyPair(new DSASigningPrivateKey(privateKey), new DSAVerifyingPublicKey(publicKey));
+ }
+
+ public static SigningPrivateKey createPrivateKey(BigInteger x, BigInteger p, BigInteger q, BigInteger g) throws NoSuchAlgorithmException, InvalidKeySpecException {
+ if (x == null) {
+ throw new IllegalArgumentException("x must not be null");
+ }
+ if (p == null) {
+ throw new IllegalArgumentException("p must not be null");
+ }
+ if (q == null) {
+ throw new IllegalArgumentException("q must not be null");
+ }
+ if (g == null) {
+ throw new IllegalArgumentException("g must not be null");
+ }
+ KeySpec keySpec = new DSAPrivateKeySpec(x, p, q, g);
+ KeyFactory keyFactory = KeyFactory.getInstance("DSA");
+ DSAPrivateKey privateKey = (DSAPrivateKey) keyFactory.generatePrivate(keySpec);
+ return new DSASigningPrivateKey(privateKey);
+ }
+
+ public static VerifyingPublicKey createPublicKey(BigInteger y, BigInteger p, BigInteger q, BigInteger g) throws NoSuchAlgorithmException, InvalidKeySpecException {
+ if (y == null) {
+ throw new IllegalArgumentException("n must not be null");
+ }
+ if (p == null) {
+ throw new IllegalArgumentException("p must not be null");
+ }
+ if (q == null) {
+ throw new IllegalArgumentException("q must not be null");
+ }
+ if (g == null) {
+ throw new IllegalArgumentException("g must not be null");
+ }
+ KeySpec keySpec = new DSAPublicKeySpec(y, p, q, g);
+ KeyFactory keyFactory = KeyFactory.getInstance("DSA");
+ DSAPublicKey publicKey = (DSAPublicKey) keyFactory.generatePublic(keySpec);
+ return new DSAVerifyingPublicKey(publicKey);
+ }
+
+ public static SigningPrivateKey createPrivateKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
+ String algorithm = o.getString("algorithm");
+ if (!"DS".equals(algorithm)) {
+ throw new InvalidKeySpecException("algorithm must equal DS, was " + algorithm);
+ }
+ try {
+ BigInteger x = new BigInteger(o.getString("x"), SERIALIZATION_BASE);
+ BigInteger p = new BigInteger(o.getString("p"), SERIALIZATION_BASE);
+ BigInteger q = new BigInteger(o.getString("q"), SERIALIZATION_BASE);
+ BigInteger g = new BigInteger(o.getString("g"), SERIALIZATION_BASE);
+ return createPrivateKey(x, p, q, g);
+ } catch (NullPointerException | NumberFormatException e) {
+ throw new InvalidKeySpecException("x, p, q, and g must be integers encoded as strings, base " + SERIALIZATION_BASE);
+ }
+ }
+
+ public static VerifyingPublicKey createPublicKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
+ String algorithm = o.getString("algorithm");
+ if (!"DS".equals(algorithm)) {
+ throw new InvalidKeySpecException("algorithm must equal DS, was " + algorithm);
+ }
+ try {
+ BigInteger y = new BigInteger(o.getString("y"), SERIALIZATION_BASE);
+ BigInteger p = new BigInteger(o.getString("p"), SERIALIZATION_BASE);
+ BigInteger q = new BigInteger(o.getString("q"), SERIALIZATION_BASE);
+ BigInteger g = new BigInteger(o.getString("g"), SERIALIZATION_BASE);
+ return createPublicKey(y, p, q, g);
+ } catch (NullPointerException | NumberFormatException e) {
+ throw new InvalidKeySpecException("y, p, q, and g must be integers encoded as strings, base " + SERIALIZATION_BASE);
+ }
+ }
+
+ public static BrowserIDKeyPair fromJSONObject(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
+ try {
+ ExtendedJSONObject privateKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PRIVATEKEY);
+ ExtendedJSONObject publicKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PUBLICKEY);
+ if (privateKey == null) {
+ throw new InvalidKeySpecException("privateKey must not be null");
+ }
+ if (publicKey == null) {
+ throw new InvalidKeySpecException("publicKey must not be null");
+ }
+ return new BrowserIDKeyPair(createPrivateKey(privateKey), createPublicKey(publicKey));
+ } catch (NonObjectJSONException e) {
+ throw new InvalidKeySpecException("privateKey and publicKey must be JSON objects");
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/JSONWebTokenUtils.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/JSONWebTokenUtils.java
new file mode 100644
index 0000000000..207accc76d
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/JSONWebTokenUtils.java
@@ -0,0 +1,245 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.browserid;
+
+import org.json.simple.JSONObject;
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.apache.commons.codec.binary.StringUtils;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.Utils;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.TreeMap;
+
+/**
+ * Encode and decode JSON Web Tokens.
+ * <p>
+ * Reverse-engineered from the Node.js jwcrypto library at
+ * <a href="https://github.com/mozilla/jwcrypto">https://github.com/mozilla/jwcrypto</a>
+ * and informed by the informal draft standard "JSON Web Token (JWT)" at
+ * <a href="http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html">http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html</a>.
+ */
+public class JSONWebTokenUtils {
+ public static final long DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS = 60 * 60 * 1000;
+ public static final long DEFAULT_ASSERTION_DURATION_IN_MILLISECONDS = 60 * 60 * 1000;
+ public static final long DEFAULT_FUTURE_EXPIRES_AT_IN_MILLISECONDS = 9999999999999L;
+ public static final String DEFAULT_CERTIFICATE_ISSUER = "127.0.0.1";
+ public static final String DEFAULT_ASSERTION_ISSUER = "127.0.0.1";
+
+ public static String encode(String payload, SigningPrivateKey privateKey) throws UnsupportedEncodingException, GeneralSecurityException {
+ final ExtendedJSONObject header = new ExtendedJSONObject();
+ header.put("alg", privateKey.getAlgorithm());
+ String encodedHeader = Base64.encodeBase64URLSafeString(header.toJSONString().getBytes("UTF-8"));
+ String encodedPayload = Base64.encodeBase64URLSafeString(payload.getBytes("UTF-8"));
+ ArrayList<String> segments = new ArrayList<String>();
+ segments.add(encodedHeader);
+ segments.add(encodedPayload);
+ byte[] message = Utils.toDelimitedString(".", segments).getBytes("UTF-8");
+ byte[] signature = privateKey.signMessage(message);
+ segments.add(Base64.encodeBase64URLSafeString(signature));
+ return Utils.toDelimitedString(".", segments);
+ }
+
+ public static String decode(String token, VerifyingPublicKey publicKey) throws GeneralSecurityException, UnsupportedEncodingException {
+ if (token == null) {
+ throw new IllegalArgumentException("token must not be null");
+ }
+ String[] segments = token.split("\\.");
+ if (segments == null || segments.length != 3) {
+ throw new GeneralSecurityException("malformed token");
+ }
+ byte[] message = (segments[0] + "." + segments[1]).getBytes("UTF-8");
+ byte[] signature = Base64.decodeBase64(segments[2]);
+ boolean verifies = publicKey.verifyMessage(message, signature);
+ if (!verifies) {
+ throw new GeneralSecurityException("bad signature");
+ }
+ String payload = StringUtils.newStringUtf8(Base64.decodeBase64(segments[1]));
+ return payload;
+ }
+
+ /**
+ * Public for testing.
+ */
+ @SuppressWarnings("unchecked")
+ public static String getPayloadString(String payloadString, String audience, String issuer,
+ Long issuedAt, long expiresAt) throws NonObjectJSONException, IOException {
+ ExtendedJSONObject payload;
+ if (payloadString != null) {
+ payload = new ExtendedJSONObject(payloadString);
+ } else {
+ payload = new ExtendedJSONObject();
+ }
+ if (audience != null) {
+ payload.put("aud", audience);
+ }
+ payload.put("iss", issuer);
+ if (issuedAt != null) {
+ payload.put("iat", issuedAt);
+ }
+ payload.put("exp", expiresAt);
+ // TreeMap so that keys are sorted. A small attempt to keep output stable over time.
+ return JSONObject.toJSONString(new TreeMap<Object, Object>(payload.object));
+ }
+
+ protected static String getCertificatePayloadString(VerifyingPublicKey publicKeyToSign, String email) throws NonObjectJSONException, IOException {
+ ExtendedJSONObject payload = new ExtendedJSONObject();
+ ExtendedJSONObject principal = new ExtendedJSONObject();
+ principal.put("email", email);
+ payload.put("principal", principal);
+ payload.put("public-key", publicKeyToSign.toJSONObject());
+ return payload.toJSONString();
+ }
+
+ public static String createCertificate(VerifyingPublicKey publicKeyToSign, String email,
+ String issuer, long issuedAt, long expiresAt, SigningPrivateKey privateKey) throws NonObjectJSONException, IOException, GeneralSecurityException {
+ String certificatePayloadString = getCertificatePayloadString(publicKeyToSign, email);
+ String payloadString = getPayloadString(certificatePayloadString, null, issuer, issuedAt, expiresAt);
+ return JSONWebTokenUtils.encode(payloadString, privateKey);
+ }
+
+ /**
+ * Create a Browser ID assertion.
+ *
+ * @param privateKeyToSignWith
+ * private key to sign assertion with.
+ * @param certificate
+ * to include in assertion; no attempt is made to ensure the
+ * certificate is valid, or corresponds to the private key, or any
+ * other condition.
+ * @param audience
+ * to produce assertion for.
+ * @param issuer
+ * to produce assertion for.
+ * @param issuedAt
+ * timestamp for assertion, in milliseconds since the epoch; if null,
+ * no timestamp is included.
+ * @param expiresAt
+ * expiration timestamp for assertion, in milliseconds since the epoch.
+ * @return assertion.
+ * @throws NonObjectJSONException
+ * @throws IOException
+ * @throws GeneralSecurityException
+ */
+ public static String createAssertion(SigningPrivateKey privateKeyToSignWith, String certificate, String audience,
+ String issuer, Long issuedAt, long expiresAt) throws NonObjectJSONException, IOException, GeneralSecurityException {
+ String emptyAssertionPayloadString = "{}";
+ String payloadString = getPayloadString(emptyAssertionPayloadString, audience, issuer, issuedAt, expiresAt);
+ String signature = JSONWebTokenUtils.encode(payloadString, privateKeyToSignWith);
+ return certificate + "~" + signature;
+ }
+
+ /**
+ * For debugging only!
+ *
+ * @param input
+ * certificate to dump.
+ * @return non-null object with keys header, payload, signature if the
+ * certificate is well-formed.
+ */
+ public static ExtendedJSONObject parseCertificate(String input) {
+ try {
+ String[] parts = input.split("\\.");
+ if (parts.length != 3) {
+ return null;
+ }
+ String cHeader = new String(Base64.decodeBase64(parts[0]));
+ String cPayload = new String(Base64.decodeBase64(parts[1]));
+ String cSignature = Utils.byte2Hex(Base64.decodeBase64(parts[2]));
+ ExtendedJSONObject o = new ExtendedJSONObject();
+ o.put("header", new ExtendedJSONObject(cHeader));
+ o.put("payload", new ExtendedJSONObject(cPayload));
+ o.put("signature", cSignature);
+ return o;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * For debugging only!
+ *
+ * @param input certificate to dump.
+ * @return true if the certificate is well-formed.
+ */
+ public static boolean dumpCertificate(String input) {
+ ExtendedJSONObject c = parseCertificate(input);
+ try {
+ if (c == null) {
+ System.out.println("Malformed certificate -- got exception trying to dump contents.");
+ return false;
+ }
+ System.out.println("certificate header: " + c.getObject("header").toJSONString());
+ System.out.println("certificate payload: " + c.getObject("payload").toJSONString());
+ System.out.println("certificate signature: " + c.getString("signature"));
+ return true;
+ } catch (Exception e) {
+ System.out.println("Malformed certificate -- got exception trying to dump contents.");
+ return false;
+ }
+ }
+
+ /**
+ * For debugging only!
+ *
+ * @param input assertion to dump.
+ * @return true if the assertion is well-formed.
+ */
+ public static ExtendedJSONObject parseAssertion(String input) {
+ try {
+ String[] parts = input.split("~");
+ if (parts.length != 2) {
+ return null;
+ }
+ String certificate = parts[0];
+ String assertion = parts[1];
+ parts = assertion.split("\\.");
+ if (parts.length != 3) {
+ return null;
+ }
+ String aHeader = new String(Base64.decodeBase64(parts[0]));
+ String aPayload = new String(Base64.decodeBase64(parts[1]));
+ String aSignature = Utils.byte2Hex(Base64.decodeBase64(parts[2]));
+ // We do all the assertion parsing *before* dumping the certificate in
+ // case there's a malformed assertion.
+ ExtendedJSONObject o = new ExtendedJSONObject();
+ o.put("header", new ExtendedJSONObject(aHeader));
+ o.put("payload", new ExtendedJSONObject(aPayload));
+ o.put("signature", aSignature);
+ o.put("certificate", certificate);
+ return o;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * For debugging only!
+ *
+ * @param input assertion to dump.
+ * @return true if the assertion is well-formed.
+ */
+ public static boolean dumpAssertion(String input) {
+ ExtendedJSONObject a = parseAssertion(input);
+ try {
+ if (a == null) {
+ System.out.println("Malformed assertion -- got exception trying to dump contents.");
+ return false;
+ }
+ dumpCertificate(a.getString("certificate"));
+ System.out.println("assertion header: " + a.getObject("header").toJSONString());
+ System.out.println("assertion payload: " + a.getObject("payload").toJSONString());
+ System.out.println("assertion signature: " + a.getString("signature"));
+ return true;
+ } catch (Exception e) {
+ System.out.println("Malformed assertion -- got exception trying to dump contents.");
+ return false;
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/MockMyIDTokenFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/MockMyIDTokenFactory.java
new file mode 100644
index 0000000000..c807d4cbbd
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/MockMyIDTokenFactory.java
@@ -0,0 +1,128 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.browserid;
+
+import java.math.BigInteger;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+
+/**
+ * Generate certificates and assertions backed by mockmyid.com's private key.
+ * <p>
+ * These artifacts are for testing only.
+ */
+public class MockMyIDTokenFactory {
+ public static final BigInteger MOCKMYID_x = new BigInteger("385cb3509f086e110c5e24bdd395a84b335a09ae", 16);
+ public static final BigInteger MOCKMYID_y = new BigInteger("738ec929b559b604a232a9b55a5295afc368063bb9c20fac4e53a74970a4db7956d48e4c7ed523405f629b4cc83062f13029c4d615bbacb8b97f5e56f0c7ac9bc1d4e23809889fa061425c984061fca1826040c399715ce7ed385c4dd0d402256912451e03452d3c961614eb458f188e3e8d2782916c43dbe2e571251ce38262", 16);
+ public static final BigInteger MOCKMYID_p = new BigInteger("ff600483db6abfc5b45eab78594b3533d550d9f1bf2a992a7a8daa6dc34f8045ad4e6e0c429d334eeeaaefd7e23d4810be00e4cc1492cba325ba81ff2d5a5b305a8d17eb3bf4a06a349d392e00d329744a5179380344e82a18c47933438f891e22aeef812d69c8f75e326cb70ea000c3f776dfdbd604638c2ef717fc26d02e17", 16);
+ public static final BigInteger MOCKMYID_q = new BigInteger("e21e04f911d1ed7991008ecaab3bf775984309c3", 16);
+ public static final BigInteger MOCKMYID_g = new BigInteger("c52a4a0ff3b7e61fdf1867ce84138369a6154f4afa92966e3c827e25cfa6cf508b90e5de419e1337e07a2e9e2a3cd5dea704d175f8ebf6af397d69e110b96afb17c7a03259329e4829b0d03bbc7896b15b4ade53e130858cc34d96269aa89041f409136c7242a38895c9d5bccad4f389af1d7a4bd1398bd072dffa896233397a", 16);
+
+ // Computed lazily by static <code>getMockMyIDPrivateKey</code>.
+ protected static SigningPrivateKey cachedMockMyIDPrivateKey;
+
+ public static SigningPrivateKey getMockMyIDPrivateKey() throws NoSuchAlgorithmException, InvalidKeySpecException {
+ if (cachedMockMyIDPrivateKey == null) {
+ cachedMockMyIDPrivateKey = DSACryptoImplementation.createPrivateKey(MOCKMYID_x, MOCKMYID_p, MOCKMYID_q, MOCKMYID_g);
+ }
+ return cachedMockMyIDPrivateKey;
+ }
+
+ /**
+ * Sign a public key asserting ownership of username@mockmyid.com with
+ * mockmyid.com's private key.
+ *
+ * @param publicKeyToSign
+ * public key to sign.
+ * @param username
+ * sign username@mockmyid.com
+ * @param issuedAt
+ * timestamp for certificate, in milliseconds since the epoch.
+ * @param expiresAt
+ * expiration timestamp for certificate, in milliseconds since the epoch.
+ * @return encoded certificate string.
+ * @throws Exception
+ */
+ public String createMockMyIDCertificate(final VerifyingPublicKey publicKeyToSign, String username,
+ final long issuedAt, final long expiresAt)
+ throws Exception {
+ if (!username.endsWith("@mockmyid.com")) {
+ username = username + "@mockmyid.com";
+ }
+ SigningPrivateKey mockMyIdPrivateKey = getMockMyIDPrivateKey();
+ return JSONWebTokenUtils.createCertificate(publicKeyToSign, username, "mockmyid.com", issuedAt, expiresAt, mockMyIdPrivateKey);
+ }
+
+ /**
+ * Sign a public key asserting ownership of username@mockmyid.com with
+ * mockmyid.com's private key.
+ *
+ * @param publicKeyToSign
+ * public key to sign.
+ * @param username
+ * sign username@mockmyid.com
+ * @return encoded certificate string.
+ * @throws Exception
+ */
+ public String createMockMyIDCertificate(final VerifyingPublicKey publicKeyToSign, final String username)
+ throws Exception {
+ long ciat = System.currentTimeMillis();
+ long cexp = ciat + JSONWebTokenUtils.DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS;
+ return createMockMyIDCertificate(publicKeyToSign, username, ciat, cexp);
+ }
+
+ /**
+ * Generate an assertion asserting ownership of username@mockmyid.com to a
+ * relying party. The underlying certificate is signed by mockymid.com's
+ * private key.
+ *
+ * @param keyPair
+ * to sign with.
+ * @param username
+ * sign username@mockmyid.com.
+ * @param certificateIssuedAt
+ * timestamp for certificate, in milliseconds since the epoch.
+ * @param certificateExpiresAt
+ * expiration timestamp for certificate, in milliseconds since the epoch.
+ * @param assertionIssuedAt
+ * timestamp for assertion, in milliseconds since the epoch; if null,
+ * no timestamp is included.
+ * @param assertionExpiresAt
+ * expiration timestamp for assertion, in milliseconds since the epoch.
+ * @return encoded assertion string.
+ * @throws Exception
+ */
+ public String createMockMyIDAssertion(BrowserIDKeyPair keyPair, String username, String audience,
+ long certificateIssuedAt, long certificateExpiresAt,
+ Long assertionIssuedAt, long assertionExpiresAt)
+ throws Exception {
+ String certificate = createMockMyIDCertificate(keyPair.getPublic(), username,
+ certificateIssuedAt, certificateExpiresAt);
+ return JSONWebTokenUtils.createAssertion(keyPair.getPrivate(), certificate, audience,
+ JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER, assertionIssuedAt, assertionExpiresAt);
+ }
+
+ /**
+ * Generate an assertion asserting ownership of username@mockmyid.com to a
+ * relying party. The underlying certificate is signed by mockymid.com's
+ * private key.
+ *
+ * @param keyPair
+ * to sign with.
+ * @param username
+ * sign username@mockmyid.com.
+ * @return encoded assertion string.
+ * @throws Exception
+ */
+ public String createMockMyIDAssertion(BrowserIDKeyPair keyPair, String username, String audience)
+ throws Exception {
+ long ciat = System.currentTimeMillis();
+ long cexp = ciat + JSONWebTokenUtils.DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS;
+ long aiat = ciat + 1;
+ long aexp = aiat + JSONWebTokenUtils.DEFAULT_ASSERTION_DURATION_IN_MILLISECONDS;
+ return createMockMyIDAssertion(keyPair, username, audience,
+ ciat, cexp, aiat, aexp);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/RSACryptoImplementation.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/RSACryptoImplementation.java
new file mode 100644
index 0000000000..902f6fb4d6
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/RSACryptoImplementation.java
@@ -0,0 +1,182 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.browserid;
+
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.Signature;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+import java.security.spec.RSAPrivateKeySpec;
+import java.security.spec.RSAPublicKeySpec;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+
+public class RSACryptoImplementation {
+ public static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
+
+ /**
+ * Parameters are serialized as decimal strings. Hex-versus-decimal was
+ * reverse-engineered from what the Persona public verifier accepted. We
+ * expect to follow the JOSE/JWT spec as it solidifies, and that will probably
+ * mean unifying this base.
+ */
+ protected static final int SERIALIZATION_BASE = 10;
+
+ protected static class RSAVerifyingPublicKey implements VerifyingPublicKey {
+ protected final RSAPublicKey publicKey;
+
+ public RSAVerifyingPublicKey(RSAPublicKey publicKey) {
+ this.publicKey = publicKey;
+ }
+
+ /**
+ * Serialize to a JSON object.
+ * <p>
+ * Parameters are serialized as decimal strings. Hex-versus-decimal was
+ * reverse-engineered from what the Persona public verifier accepted.
+ */
+ @Override
+ public ExtendedJSONObject toJSONObject() {
+ ExtendedJSONObject o = new ExtendedJSONObject();
+ o.put("algorithm", "RS");
+ o.put("n", publicKey.getModulus().toString(SERIALIZATION_BASE));
+ o.put("e", publicKey.getPublicExponent().toString(SERIALIZATION_BASE));
+ return o;
+ }
+
+ @Override
+ public boolean verifyMessage(byte[] bytes, byte[] signature)
+ throws GeneralSecurityException {
+ final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM);
+ signer.initVerify(publicKey);
+ signer.update(bytes);
+ return signer.verify(signature);
+ }
+ }
+
+ protected static class RSASigningPrivateKey implements SigningPrivateKey {
+ protected final RSAPrivateKey privateKey;
+
+ public RSASigningPrivateKey(RSAPrivateKey privateKey) {
+ this.privateKey = privateKey;
+ }
+
+ @Override
+ public String getAlgorithm() {
+ return "RS" + (privateKey.getModulus().bitLength() + 7)/8;
+ }
+
+ /**
+ * Serialize to a JSON object.
+ * <p>
+ * Parameters are serialized as decimal strings. Hex-versus-decimal was
+ * reverse-engineered from what the Persona public verifier accepted.
+ */
+ @Override
+ public ExtendedJSONObject toJSONObject() {
+ ExtendedJSONObject o = new ExtendedJSONObject();
+ o.put("algorithm", "RS");
+ o.put("n", privateKey.getModulus().toString(SERIALIZATION_BASE));
+ o.put("d", privateKey.getPrivateExponent().toString(SERIALIZATION_BASE));
+ return o;
+ }
+
+ @Override
+ public byte[] signMessage(byte[] bytes)
+ throws GeneralSecurityException {
+ final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM);
+ signer.initSign(privateKey);
+ signer.update(bytes);
+ return signer.sign();
+ }
+ }
+
+ public static BrowserIDKeyPair generateKeyPair(final int keysize) throws NoSuchAlgorithmException {
+ final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+ keyPairGenerator.initialize(keysize);
+ final KeyPair keyPair = keyPairGenerator.generateKeyPair();
+ RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
+ RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
+ return new BrowserIDKeyPair(new RSASigningPrivateKey(privateKey), new RSAVerifyingPublicKey(publicKey));
+ }
+
+ public static SigningPrivateKey createPrivateKey(BigInteger n, BigInteger d) throws NoSuchAlgorithmException, InvalidKeySpecException {
+ if (n == null) {
+ throw new IllegalArgumentException("n must not be null");
+ }
+ if (d == null) {
+ throw new IllegalArgumentException("d must not be null");
+ }
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ KeySpec keySpec = new RSAPrivateKeySpec(n, d);
+ RSAPrivateKey privateKey = (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
+ return new RSASigningPrivateKey(privateKey);
+ }
+
+ public static VerifyingPublicKey createPublicKey(BigInteger n, BigInteger e) throws NoSuchAlgorithmException, InvalidKeySpecException {
+ if (n == null) {
+ throw new IllegalArgumentException("n must not be null");
+ }
+ if (e == null) {
+ throw new IllegalArgumentException("e must not be null");
+ }
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ KeySpec keySpec = new RSAPublicKeySpec(n, e);
+ RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(keySpec);
+ return new RSAVerifyingPublicKey(publicKey);
+ }
+
+ public static SigningPrivateKey createPrivateKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
+ String algorithm = o.getString("algorithm");
+ if (!"RS".equals(algorithm)) {
+ throw new InvalidKeySpecException("algorithm must equal RS, was " + algorithm);
+ }
+ try {
+ BigInteger n = new BigInteger(o.getString("n"), SERIALIZATION_BASE);
+ BigInteger d = new BigInteger(o.getString("d"), SERIALIZATION_BASE);
+ return createPrivateKey(n, d);
+ } catch (NullPointerException | NumberFormatException e) {
+ throw new InvalidKeySpecException("n and d must be integers encoded as strings, base " + SERIALIZATION_BASE);
+ }
+ }
+
+ public static VerifyingPublicKey createPublicKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
+ String algorithm = o.getString("algorithm");
+ if (!"RS".equals(algorithm)) {
+ throw new InvalidKeySpecException("algorithm must equal RS, was " + algorithm);
+ }
+ try {
+ BigInteger n = new BigInteger(o.getString("n"), SERIALIZATION_BASE);
+ BigInteger e = new BigInteger(o.getString("e"), SERIALIZATION_BASE);
+ return createPublicKey(n, e);
+ } catch (NullPointerException | NumberFormatException e) {
+ throw new InvalidKeySpecException("n and e must be integers encoded as strings, base " + SERIALIZATION_BASE);
+ }
+ }
+
+ public static BrowserIDKeyPair fromJSONObject(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
+ try {
+ ExtendedJSONObject privateKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PRIVATEKEY);
+ ExtendedJSONObject publicKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PUBLICKEY);
+ if (privateKey == null) {
+ throw new InvalidKeySpecException("privateKey must not be null");
+ }
+ if (publicKey == null) {
+ throw new InvalidKeySpecException("publicKey must not be null");
+ }
+ return new BrowserIDKeyPair(createPrivateKey(privateKey), createPublicKey(publicKey));
+ } catch (NonObjectJSONException e) {
+ throw new InvalidKeySpecException("privateKey and publicKey must be JSON objects");
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/SigningPrivateKey.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/SigningPrivateKey.java
new file mode 100644
index 0000000000..6c388d1673
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/SigningPrivateKey.java
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.browserid;
+
+import java.security.GeneralSecurityException;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+public interface SigningPrivateKey {
+ /**
+ * Return the JSON Web Token "alg" header corresponding to this private key.
+ * <p>
+ * The header is used when formatting web tokens, and generally denotes the
+ * algorithm and an ad-hoc encoding of the key size.
+ *
+ * @return header.
+ */
+ public String getAlgorithm();
+
+ /**
+ * Generate a JSON representation of a private key.
+ * <p>
+ * <b>This should only be used for debugging. No private keys should go over
+ * the wire at any time.</b>
+ *
+ * @param privateKey
+ * to represent.
+ * @return JSON representation.
+ */
+ public ExtendedJSONObject toJSONObject();
+
+ /**
+ * Sign a message.
+ * @param message to sign.
+ * @return signature.
+ * @throws GeneralSecurityException
+ */
+ public byte[] signMessage(byte[] message) throws GeneralSecurityException;
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/VerifyingPublicKey.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/VerifyingPublicKey.java
new file mode 100644
index 0000000000..74b534b90c
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/VerifyingPublicKey.java
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.browserid;
+
+import java.security.GeneralSecurityException;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+
+public interface VerifyingPublicKey {
+ /**
+ * Generate a JSON representation of a public key.
+ *
+ * @param publicKey
+ * to represent.
+ * @return JSON representation.
+ */
+ public ExtendedJSONObject toJSONObject();
+
+ /**
+ * Verify a signature.
+ *
+ * @param message
+ * to verify signature of.
+ * @param signature
+ * to verify.
+ * @return true if signature is a signature of message produced by the private
+ * key corresponding to this public key.
+ * @throws GeneralSecurityException
+ */
+ public boolean verifyMessage(byte[] message, byte[] signature) throws GeneralSecurityException;
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/AbstractBrowserIDRemoteVerifierClient.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/AbstractBrowserIDRemoteVerifierClient.java
new file mode 100644
index 0000000000..aa8db2d481
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/AbstractBrowserIDRemoteVerifierClient.java
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.browserid.verifier;
+
+import java.io.IOException;
+import java.net.URI;
+import java.security.GeneralSecurityException;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierException.BrowserIDVerifierErrorResponseException;
+import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierException.BrowserIDVerifierMalformedResponseException;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.net.BaseResourceDelegate;
+import org.mozilla.gecko.sync.net.Resource;
+import org.mozilla.gecko.sync.net.SyncResponse;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+
+public abstract class AbstractBrowserIDRemoteVerifierClient implements BrowserIDVerifierClient {
+ public static final String LOG_TAG = AbstractBrowserIDRemoteVerifierClient.class.getSimpleName();
+
+ protected static class RemoteVerifierResourceDelegate extends BaseResourceDelegate {
+ private final BrowserIDVerifierDelegate delegate;
+
+ protected RemoteVerifierResourceDelegate(Resource resource, BrowserIDVerifierDelegate delegate) {
+ super(resource);
+ this.delegate = delegate;
+ }
+
+ @Override
+ public String getUserAgent() {
+ return null;
+ }
+
+ @Override
+ public void handleHttpResponse(HttpResponse response) {
+ SyncResponse res = new SyncResponse(response);
+ int statusCode = res.getStatusCode();
+ Logger.debug(LOG_TAG, "Got response with status code " + statusCode + ".");
+
+ if (statusCode != 200) {
+ delegate.handleError(new BrowserIDVerifierErrorResponseException("Expected status code 200."));
+ return;
+ }
+
+ ExtendedJSONObject o = null;
+ try {
+ o = res.jsonObjectBody();
+ } catch (Exception e) {
+ delegate.handleError(new BrowserIDVerifierMalformedResponseException(e));
+ return;
+ }
+
+ String status = o.getString("status");
+ if ("failure".equals(status)) {
+ delegate.handleFailure(o);
+ return;
+ }
+
+ if (!("okay".equals(status))) {
+ delegate.handleError(new BrowserIDVerifierMalformedResponseException("Expected status okay, got '" + status + "'."));
+ return;
+ }
+
+ delegate.handleSuccess(o);
+ }
+
+ @Override
+ public void handleTransportException(GeneralSecurityException e) {
+ Logger.warn(LOG_TAG, "Got transport exception.", e);
+ delegate.handleError(e);
+ }
+
+ @Override
+ public void handleHttpProtocolException(ClientProtocolException e) {
+ Logger.warn(LOG_TAG, "Got protocol exception.", e);
+ delegate.handleError(e);
+ }
+
+ @Override
+ public void handleHttpIOException(IOException e) {
+ Logger.warn(LOG_TAG, "Got IO exception.", e);
+ delegate.handleError(e);
+ }
+ }
+
+ protected final URI verifierUri;
+
+ public AbstractBrowserIDRemoteVerifierClient(URI verifierUri) {
+ this.verifierUri = verifierUri;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDRemoteVerifierClient10.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDRemoteVerifierClient10.java
new file mode 100644
index 0000000000..f61a82323f
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDRemoteVerifierClient10.java
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.browserid.verifier;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.List;
+
+import org.mozilla.gecko.sync.net.BaseResource;
+
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.client.entity.UrlEncodedFormEntity;
+import ch.boye.httpclientandroidlib.message.BasicNameValuePair;
+
+/**
+ * The verifier protocol changed: version 1 posts form-encoded data; version 2
+ * posts JSON data.
+ */
+public class BrowserIDRemoteVerifierClient10 extends AbstractBrowserIDRemoteVerifierClient {
+ public static final String LOG_TAG = BrowserIDRemoteVerifierClient10.class.getSimpleName();
+
+ public static final String DEFAULT_VERIFIER_URL = "https://verifier.login.persona.org/verify";
+
+ public BrowserIDRemoteVerifierClient10() throws URISyntaxException {
+ super(new URI(DEFAULT_VERIFIER_URL));
+ }
+
+ public BrowserIDRemoteVerifierClient10(URI verifierUri) {
+ super(verifierUri);
+ }
+
+ @Override
+ public void verify(String audience, String assertion, final BrowserIDVerifierDelegate delegate) {
+ if (audience == null) {
+ throw new IllegalArgumentException("audience cannot be null.");
+ }
+ if (assertion == null) {
+ throw new IllegalArgumentException("assertion cannot be null.");
+ }
+ if (delegate == null) {
+ throw new IllegalArgumentException("delegate cannot be null.");
+ }
+
+ BaseResource r = new BaseResource(verifierUri);
+
+ r.delegate = new RemoteVerifierResourceDelegate(r, delegate);
+
+ List<NameValuePair> nvps = Arrays.asList(new NameValuePair[] {
+ new BasicNameValuePair("audience", audience),
+ new BasicNameValuePair("assertion", assertion) });
+
+ try {
+ r.post(new UrlEncodedFormEntity(nvps, "UTF-8"));
+ } catch (UnsupportedEncodingException e) {
+ delegate.handleError(e);
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDRemoteVerifierClient20.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDRemoteVerifierClient20.java
new file mode 100644
index 0000000000..013856576d
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDRemoteVerifierClient20.java
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.browserid.verifier;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.net.BaseResource;
+
+/**
+ * The verifier protocol changed: version 1 posts form-encoded data; version 2
+ * posts JSON data.
+ */
+public class BrowserIDRemoteVerifierClient20 extends AbstractBrowserIDRemoteVerifierClient {
+ public static final String LOG_TAG = BrowserIDRemoteVerifierClient20.class.getSimpleName();
+
+ public static final String DEFAULT_VERIFIER_URL = "https://verifier.accounts.firefox.com/v2";
+
+ protected static final String JSON_KEY_ASSERTION = "assertion";
+ protected static final String JSON_KEY_AUDIENCE = "audience";
+
+ public BrowserIDRemoteVerifierClient20() throws URISyntaxException {
+ super(new URI(DEFAULT_VERIFIER_URL));
+ }
+
+ public BrowserIDRemoteVerifierClient20(URI verifierUri) {
+ super(verifierUri);
+ }
+
+ @Override
+ public void verify(String audience, String assertion, final BrowserIDVerifierDelegate delegate) {
+ if (audience == null) {
+ throw new IllegalArgumentException("audience cannot be null.");
+ }
+ if (assertion == null) {
+ throw new IllegalArgumentException("assertion cannot be null.");
+ }
+ if (delegate == null) {
+ throw new IllegalArgumentException("delegate cannot be null.");
+ }
+
+ BaseResource r = new BaseResource(verifierUri);
+ r.delegate = new RemoteVerifierResourceDelegate(r, delegate);
+
+ final ExtendedJSONObject requestBody = new ExtendedJSONObject();
+ requestBody.put(JSON_KEY_AUDIENCE, audience);
+ requestBody.put(JSON_KEY_ASSERTION, assertion);
+
+ try {
+ r.post(requestBody);
+ } catch (Exception e) {
+ delegate.handleError(e);
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDVerifierClient.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDVerifierClient.java
new file mode 100644
index 0000000000..67a327f198
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDVerifierClient.java
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.browserid.verifier;
+
+public interface BrowserIDVerifierClient {
+ public abstract void verify(String audience, String assertion, BrowserIDVerifierDelegate delegate);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDVerifierDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDVerifierDelegate.java
new file mode 100644
index 0000000000..b58d032817
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDVerifierDelegate.java
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.browserid.verifier;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+public interface BrowserIDVerifierDelegate {
+ void handleSuccess(ExtendedJSONObject response);
+ void handleFailure(ExtendedJSONObject response);
+ void handleError(Exception e);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDVerifierException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDVerifierException.java
new file mode 100644
index 0000000000..dacaf61125
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDVerifierException.java
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.browserid.verifier;
+
+public class BrowserIDVerifierException extends Exception {
+ private static final long serialVersionUID = 2228946910754889975L;
+
+ public BrowserIDVerifierException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ public BrowserIDVerifierException(Throwable throwable) {
+ super(throwable);
+ }
+
+ public static class BrowserIDVerifierMalformedResponseException extends BrowserIDVerifierException {
+ private static final long serialVersionUID = 115377527009652839L;
+
+ public BrowserIDVerifierMalformedResponseException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ public BrowserIDVerifierMalformedResponseException(Throwable throwable) {
+ super(throwable);
+ }
+ }
+
+ public static class BrowserIDVerifierErrorResponseException extends BrowserIDVerifierException {
+ private static final long serialVersionUID = 115377527009652840L;
+
+ public BrowserIDVerifierErrorResponseException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ public BrowserIDVerifierErrorResponseException(Throwable throwable) {
+ super(throwable);
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/AccountLoader.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/AccountLoader.java
new file mode 100644
index 0000000000..8a31c1ce0a
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/AccountLoader.java
@@ -0,0 +1,227 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa;
+
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+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.content.AsyncTaskLoader;
+import android.support.v4.content.LocalBroadcastManager;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * A Loader that queries and updates based on the existence of Firefox and
+ * legacy Sync Android Accounts.
+ *
+ * The loader returns an Android Account (of either Account type) if an account
+ * exists, and null to indicate no Account is present.
+ *
+ * The loader listens for Accounts added and deleted, and also Accounts being
+ * updated by Sync or another Activity, via the use of
+ * {@link AndroidFxAccount#setState(org.mozilla.gecko.fxa.login.State)}.
+ * Be careful of message loops if you update the account state from an activity
+ * that uses this loader.
+ *
+ * This implementation is based on
+ * <a href="http://www.androiddesignpatterns.com/2012/08/implementing-loaders.html">http://www.androiddesignpatterns.com/2012/08/implementing-loaders.html</a>.
+ */
+public class AccountLoader extends AsyncTaskLoader<Account> {
+ protected Account account = null;
+ protected BroadcastReceiver broadcastReceiver = null;
+
+ // Hold a weak reference to AccountLoader instance in this Runnable to avoid potentially leaking it
+ // after posting to a Handler in the BroadcastReceiver returned from makeNewObserver.
+ private final BroadcastReceiverRunnable broadcastReceiverRunnable = new BroadcastReceiverRunnable(this);
+
+ public AccountLoader(final Context context) {
+ super(context);
+ }
+
+ // Task that performs the asynchronous load.
+ @Override
+ public Account loadInBackground() {
+ return FirefoxAccounts.getFirefoxAccount(getContext());
+ }
+
+ // Deliver the results to the registered listener.
+ @Override
+ public void deliverResult(Account data) {
+ if (isReset()) {
+ // The Loader has been reset; ignore the result and invalidate the data.
+ releaseResources(data);
+ return;
+ }
+
+ // Hold a reference to the old data so it doesn't get garbage collected.
+ // We must protect it until the new data has been delivered.
+ Account oldData = account;
+ account = data;
+
+ if (isStarted()) {
+ // If the Loader is in a started state, deliver the results to the
+ // client. The superclass method does this for us.
+ super.deliverResult(data);
+ }
+
+ // Invalidate the old data as we don't need it any more.
+ if (oldData != null && oldData != data) {
+ releaseResources(oldData);
+ }
+ }
+
+ // The Loader’s state-dependent behavior.
+ @Override
+ protected void onStartLoading() {
+ if (account != null) {
+ // Deliver any previously loaded data immediately.
+ deliverResult(account);
+ }
+
+ // Begin monitoring the underlying data source.
+ if (broadcastReceiver == null) {
+ broadcastReceiver = makeNewObserver();
+ registerLocalObserver(getContext(), broadcastReceiver);
+ registerSystemObserver(getContext(), broadcastReceiver);
+ }
+
+ if (takeContentChanged() || account == null) {
+ // When the observer detects a change, it should call onContentChanged()
+ // on the Loader, which will cause the next call to takeContentChanged()
+ // to return true. If this is ever the case (or if the current data is
+ // null), we force a new load.
+ forceLoad();
+ }
+ }
+
+ @Override
+ protected void onStopLoading() {
+ // The Loader is in a stopped state, so we should attempt to cancel the
+ // current load (if there is one).
+ cancelLoad();
+
+ // Note that we leave the observer as is. Loaders in a stopped state
+ // should still monitor the data source for changes so that the Loader
+ // will know to force a new load if it is ever started again.
+ }
+
+ @Override
+ protected void onReset() {
+ // Ensure the loader has been stopped. In CursorLoader and the template
+ // this code follows (see the class comment), this is onStopLoading, which
+ // appears to not set the started flag (see Loader itself).
+ stopLoading();
+
+ // At this point we can release the resources associated with 'mData'.
+ if (account != null) {
+ releaseResources(account);
+ account = null;
+ }
+
+ // The Loader is being reset, so we should stop monitoring for changes.
+ if (broadcastReceiver != null) {
+ final BroadcastReceiver observer = broadcastReceiver;
+ broadcastReceiver = null;
+ unregisterObserver(getContext(), observer);
+ }
+ }
+
+ @Override
+ public void onCanceled(final Account data) {
+ // Attempt to cancel the current asynchronous load.
+ super.onCanceled(data);
+
+ // The load has been canceled, so we should release the resources
+ // associated with 'data'.
+ releaseResources(data);
+ }
+
+ // Observer which receives notifications when the data changes.
+ protected BroadcastReceiver makeNewObserver() {
+ return new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // onContentChanged must be called on the main thread.
+ // If we're already on the main thread, call it directly.
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ onContentChanged();
+ return;
+ }
+
+ // Otherwise, post a Runnable to a Handler bound to the main thread's message loop.
+ final Handler mainHandler = new Handler(Looper.getMainLooper());
+ mainHandler.post(broadcastReceiverRunnable);
+ }
+ };
+ }
+
+ private static class BroadcastReceiverRunnable implements Runnable {
+ private final WeakReference<AccountLoader> accountLoaderWeakReference;
+
+ public BroadcastReceiverRunnable(final AccountLoader accountLoader) {
+ accountLoaderWeakReference = new WeakReference<>(accountLoader);
+ }
+
+ @Override
+ public void run() {
+ final AccountLoader accountLoader = accountLoaderWeakReference.get();
+ if (accountLoader != null) {
+ accountLoader.onContentChanged();
+ }
+ }
+ }
+
+ private void releaseResources(Account data) {
+ // For a simple List, there is nothing to do. For something like a Cursor, we
+ // would close it in this method. All resources associated with the Loader
+ // should be released here.
+ }
+
+ /**
+ * Register provided observer with the LocalBroadcastManager to listen for internal events.
+ *
+ * @param context <code>Context</code> to use for obtaining LocalBroadcastManager instance.
+ * @param observer <code>BroadcastReceiver</code> which will handle local events.
+ */
+ protected static void registerLocalObserver(final Context context, final BroadcastReceiver observer) {
+ final IntentFilter intentFilter = new IntentFilter();
+ // Firefox Account internal state changed.
+ intentFilter.addAction(FxAccountConstants.ACCOUNT_STATE_CHANGED_ACTION);
+ // Firefox Account profile state changed.
+ intentFilter.addAction(FxAccountConstants.ACCOUNT_PROFILE_JSON_UPDATED_ACTION);
+
+ LocalBroadcastManager.getInstance(context).registerReceiver(observer, intentFilter);
+ }
+
+ /**
+ * Register provided observer for handling system-wide broadcasts.
+ *
+ * @param context <code>Context</code> to use for registering a receiver.
+ * @param observer <code>BroadcastReceiver</code> which will handle system events.
+ */
+ protected static void registerSystemObserver(final Context context, final BroadcastReceiver observer) {
+ context.registerReceiver(observer,
+ // Android Account added or removed.
+ new IntentFilter(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION),
+ // No broadcast permissions required.
+ null,
+ // Null handler ensures that broadcasts will be handled on the main thread.
+ null
+ );
+ }
+
+ protected static void unregisterObserver(final Context context, final BroadcastReceiver observer) {
+ LocalBroadcastManager.getInstance(context).unregisterReceiver(observer);
+ context.unregisterReceiver(observer);
+ }
+}
+
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FirefoxAccounts.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FirefoxAccounts.java
new file mode 100644
index 0000000000..4184340ec1
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FirefoxAccounts.java
@@ -0,0 +1,222 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa;
+
+import java.io.File;
+import java.util.concurrent.CountDownLatch;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.fxa.authenticator.AccountPickler;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
+import org.mozilla.gecko.sync.ThreadPool;
+import org.mozilla.gecko.sync.Utils;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.Bundle;
+
+/**
+ * Simple public accessors for Firefox account objects.
+ */
+public class FirefoxAccounts {
+ private static final String LOG_TAG = FirefoxAccounts.class.getSimpleName();
+
+ /**
+ * Returns true if a FirefoxAccount exists, false otherwise.
+ *
+ * @param context Android context.
+ * @return true if at least one Firefox account exists.
+ */
+ public static boolean firefoxAccountsExist(final Context context) {
+ return getFirefoxAccounts(context).length > 0;
+ }
+
+ /**
+ * Return Firefox accounts.
+ * <p>
+ * If no accounts exist in the AccountManager, one may be created
+ * via a pickled FirefoxAccount, if available, and that account
+ * will be added to the AccountManager and returned.
+ * <p>
+ * Note that this can be called from any thread.
+ *
+ * @param context Android context.
+ * @return Firefox account objects.
+ */
+ public static Account[] getFirefoxAccounts(final Context context) {
+ final Account[] accounts =
+ AccountManager.get(context).getAccountsByType(FxAccountConstants.ACCOUNT_TYPE);
+ if (accounts.length > 0) {
+ return accounts;
+ }
+
+ final Account pickledAccount = getPickledAccount(context);
+ return (pickledAccount != null) ? new Account[] {pickledAccount} : new Account[0];
+ }
+
+ private static Account getPickledAccount(final Context context) {
+ // To avoid a StrictMode violation for disk access, we call this from a background thread.
+ // We do this every time, so the caller doesn't have to care.
+ final CountDownLatch latch = new CountDownLatch(1);
+ final Account[] accounts = new Account[1];
+ ThreadPool.run(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ final File file = context.getFileStreamPath(FxAccountConstants.ACCOUNT_PICKLE_FILENAME);
+ if (!file.exists()) {
+ accounts[0] = null;
+ return;
+ }
+
+ // There is a small race window here: if the user creates a new Firefox account
+ // between our checks, this could erroneously report that no Firefox accounts
+ // exist.
+ final AndroidFxAccount fxAccount =
+ AccountPickler.unpickle(context, FxAccountConstants.ACCOUNT_PICKLE_FILENAME);
+ accounts[0] = fxAccount != null ? fxAccount.getAndroidAccount() : null;
+ } finally {
+ latch.countDown();
+ }
+ }
+ });
+
+ try {
+ latch.await(); // Wait for the background thread to return.
+ } catch (InterruptedException e) {
+ Logger.warn(LOG_TAG,
+ "Foreground thread unexpectedly interrupted while getting pickled account", e);
+ return null;
+ }
+
+ return accounts[0];
+ }
+
+ /**
+ * @param context Android context.
+ * @return the configured Firefox account if one exists, or null otherwise.
+ */
+ public static Account getFirefoxAccount(final Context context) {
+ Account[] accounts = getFirefoxAccounts(context);
+ if (accounts.length > 0) {
+ return accounts[0];
+ }
+ return null;
+ }
+
+ /**
+ * @return
+ * the {@link State} instance associated with the current account, or <code>null</code> if
+ * no accounts exist.
+ */
+ public static State getFirefoxAccountState(final Context context) {
+ final Account account = getFirefoxAccount(context);
+ if (account == null) {
+ return null;
+ }
+
+ final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
+ try {
+ return fxAccount.getState();
+ } catch (final Exception ex) {
+ Logger.warn(LOG_TAG, "Could not get FX account state.", ex);
+ return null;
+ }
+ }
+
+ /*
+ * @param context Android context
+ * @return the email address associated with the configured Firefox account if one exists; null otherwise.
+ */
+ public static String getFirefoxAccountEmail(final Context context) {
+ final Account account = getFirefoxAccount(context);
+ if (account == null) {
+ return null;
+ }
+ return account.name;
+ }
+
+ public static void logSyncOptions(Bundle syncOptions) {
+ final boolean scheduleNow = syncOptions.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false);
+
+ Logger.info(LOG_TAG, "Sync options -- scheduling now: " + scheduleNow);
+ }
+
+ public static void requestImmediateSync(final Account account, String[] stagesToSync, String[] stagesToSkip) {
+ final Bundle syncOptions = new Bundle();
+ syncOptions.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true);
+ syncOptions.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
+ requestSync(account, syncOptions, stagesToSync, stagesToSkip);
+ }
+
+ public static void requestEventualSync(final Account account, String[] stagesToSync, String[] stagesToSkip) {
+ requestSync(account, Bundle.EMPTY, stagesToSync, stagesToSkip);
+ }
+
+ /**
+ * Request a sync for the given Android Account.
+ * <p>
+ * Any hints are strictly optional: the actual requested sync is scheduled by
+ * the Android sync scheduler, and the sync mechanism may ignore hints as it
+ * sees fit.
+ * <p>
+ * It is safe to call this method from any thread.
+ *
+ * @param account to sync.
+ * @param syncOptions to pass to sync.
+ * @param stagesToSync stage names to sync.
+ * @param stagesToSkip stage names to skip.
+ */
+ protected static void requestSync(final Account account, final Bundle syncOptions, String[] stagesToSync, String[] stagesToSkip) {
+ if (account == null) {
+ throw new IllegalArgumentException("account must not be null");
+ }
+ if (syncOptions == null) {
+ throw new IllegalArgumentException("syncOptions must not be null");
+ }
+
+ Utils.putStageNamesToSync(syncOptions, stagesToSync, stagesToSkip);
+
+ Logger.info(LOG_TAG, "Requesting sync.");
+ logSyncOptions(syncOptions);
+
+ // We get strict mode warnings on some devices, so make the request on a
+ // background thread.
+ ThreadPool.run(new Runnable() {
+ @Override
+ public void run() {
+ for (String authority : AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP.keySet()) {
+ ContentResolver.requestSync(account, authority, syncOptions);
+ }
+ }
+ });
+ }
+
+ /**
+ * Start notifying <code>syncStatusListener</code> of sync status changes.
+ * <p>
+ * Only a weak reference to <code>syncStatusListener</code> is held.
+ *
+ * @param syncStatusListener to start notifying.
+ */
+ public static void addSyncStatusListener(SyncStatusListener syncStatusListener) {
+ // startObserving null-checks its argument.
+ FxAccountSyncStatusHelper.getInstance().startObserving(syncStatusListener);
+ }
+
+ /**
+ * Stop notifying <code>syncStatusListener</code> of sync status changes.
+ *
+ * @param syncStatusListener to stop notifying.
+ */
+ public static void removeSyncStatusListener(SyncStatusListener syncStatusListener) {
+ // stopObserving null-checks its argument.
+ FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusListener);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountConstants.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountConstants.java
new file mode 100644
index 0000000000..c6147b3235
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountConstants.java
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa;
+
+import org.mozilla.gecko.AppConstants;
+
+public class FxAccountConstants {
+ public static final String GLOBAL_LOG_TAG = "FxAccounts";
+ public static final String ACCOUNT_TYPE = AppConstants.MOZ_ANDROID_SHARED_FXACCOUNT_TYPE;
+
+ // Must be a client ID allocated with "canGrant" privileges!
+ public static final String OAUTH_CLIENT_ID_FENNEC = "3332a18d142636cb";
+
+ public static final String DEFAULT_AUTH_SERVER_ENDPOINT = "https://api.accounts.firefox.com/v1";
+ public static final String DEFAULT_TOKEN_SERVER_ENDPOINT = "https://token.services.mozilla.com/1.0/sync/1.5";
+ public static final String DEFAULT_OAUTH_SERVER_ENDPOINT = "https://oauth.accounts.firefox.com/v1";
+ public static final String DEFAULT_PROFILE_SERVER_ENDPOINT = "https://profile.accounts.firefox.com/v1";
+
+ public static final String STAGE_AUTH_SERVER_ENDPOINT = "https://stable.dev.lcip.org/auth/v1";
+ public static final String STAGE_TOKEN_SERVER_ENDPOINT = "https://stable.dev.lcip.org/syncserver/token/1.0/sync/1.5";
+ public static final String STAGE_OAUTH_SERVER_ENDPOINT = "https://oauth-stable.dev.lcip.org/v1";
+ public static final String STAGE_PROFILE_SERVER_ENDPOINT = "https://latest.dev.lcip.org/profile/v1";
+
+ // Action to update on cached profile information.
+ public static final String ACCOUNT_PROFILE_JSON_UPDATED_ACTION = "org.mozilla.gecko.fxa.profile.JSON.updated";
+
+ // You must be at least 13 years old, on the day of creation, to create a Firefox Account.
+ public static final int MINIMUM_AGE_TO_CREATE_AN_ACCOUNT = 13;
+
+ // Key for avatar URI in profile JSON.
+ public static final String KEY_PROFILE_JSON_AVATAR = "avatar";
+ // Key for username in profile JSON.
+ public static final String KEY_PROFILE_JSON_USERNAME = "displayName";
+
+ // You must wait 15 minutes after failing an age check before trying to create a different account.
+ public static final long MINIMUM_TIME_TO_WAIT_AFTER_AGE_CHECK_FAILED_IN_MILLISECONDS = 15 * 60 * 1000;
+
+ public static final String USER_AGENT = "Firefox-Android-FxAccounts/" + AppConstants.MOZ_APP_VERSION + " (" + AppConstants.MOZ_APP_UA_NAME + ")";
+
+ public static final String ACCOUNT_PICKLE_FILENAME = "fxa.account.json";
+
+
+ /**
+ * Version number of contents of SYNC_ACCOUNT_DELETED_ACTION intent.
+ */
+ public static final long ACCOUNT_DELETED_INTENT_VERSION = 1;
+
+ public static final String ACCOUNT_DELETED_INTENT_VERSION_KEY = "account_deleted_intent_version";
+ public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_KEY = "account_deleted_intent_account";
+ public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_PROFILE = "account_deleted_intent_profile";
+ public static final String ACCOUNT_OAUTH_SERVICE_ENDPOINT_KEY = "account_oauth_service_endpoint";
+ public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_AUTH_TOKENS = "account_deleted_intent_auth_tokens";
+
+ /**
+ * This action is broadcast when an Android Firefox Account's internal state
+ * is changed.
+ * <p>
+ * It is protected by signing-level permission PER_ACCOUNT_TYPE_PERMISSION and
+ * can be received only by Firefox versions sharing the same Android Firefox
+ * Account type.
+ */
+ public static final String ACCOUNT_STATE_CHANGED_ACTION = AppConstants.MOZ_ANDROID_SHARED_FXACCOUNT_TYPE + ".accounts.ACCOUNT_STATE_CHANGED_ACTION";
+
+ public static final String ACTION_FXA_CONFIRM_ACCOUNT = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_CONFIRM_ACCOUNT";
+ public static final String ACTION_FXA_FINISH_MIGRATING = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_FINISH_MIGRATING";
+ public static final String ACTION_FXA_GET_STARTED = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_GET_STARTED";
+ public static final String ACTION_FXA_STATUS = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_STATUS";
+ public static final String ACTION_FXA_UPDATE_CREDENTIALS = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_UPDATE_CREDENTIALS";
+
+ public static final String ENDPOINT_PREFERENCES = "preferences";
+ public static final String ENDPOINT_NOTIFICATION = "notification";
+ public static final String ENDPOINT_FIRSTRUN = "firstrun";
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDevice.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDevice.java
new file mode 100644
index 0000000000..cd46ae2bd5
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDevice.java
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this
+* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+public class FxAccountDevice {
+
+ public static final String JSON_KEY_NAME = "name";
+ public static final String JSON_KEY_ID = "id";
+ public static final String JSON_KEY_TYPE = "type";
+ public static final String JSON_KEY_ISCURRENTDEVICE = "isCurrentDevice";
+ public static final String JSON_KEY_PUSH_CALLBACK = "pushCallback";
+ public static final String JSON_KEY_PUSH_PUBLICKEY = "pushPublicKey";
+ public static final String JSON_KEY_PUSH_AUTHKEY = "pushAuthKey";
+
+ public final String id;
+ public final String name;
+ public final String type;
+ public final Boolean isCurrentDevice;
+ public final String pushCallback;
+ public final String pushPublicKey;
+ public final String pushAuthKey;
+
+ public FxAccountDevice(String name, String id, String type, Boolean isCurrentDevice,
+ String pushCallback, String pushPublicKey, String pushAuthKey) {
+ this.name = name;
+ this.id = id;
+ this.type = type;
+ this.isCurrentDevice = isCurrentDevice;
+ this.pushCallback = pushCallback;
+ this.pushPublicKey = pushPublicKey;
+ this.pushAuthKey = pushAuthKey;
+ }
+
+ public static FxAccountDevice forRegister(String name, String type, String pushCallback,
+ String pushPublicKey, String pushAuthKey) {
+ return new FxAccountDevice(name, null, type, null, pushCallback, pushPublicKey, pushAuthKey);
+ }
+
+ public static FxAccountDevice forUpdate(String id, String name, String pushCallback,
+ String pushPublicKey, String pushAuthKey) {
+ return new FxAccountDevice(name, id, null, null, pushCallback, pushPublicKey, pushAuthKey);
+ }
+
+ public static FxAccountDevice fromJson(ExtendedJSONObject json) {
+ String name = json.getString(JSON_KEY_NAME);
+ String id = json.getString(JSON_KEY_ID);
+ String type = json.getString(JSON_KEY_TYPE);
+ Boolean isCurrentDevice = json.getBoolean(JSON_KEY_ISCURRENTDEVICE);
+ String pushCallback = json.getString(JSON_KEY_PUSH_CALLBACK);
+ String pushPublicKey = json.getString(JSON_KEY_PUSH_PUBLICKEY);
+ String pushAuthKey = json.getString(JSON_KEY_PUSH_AUTHKEY);
+ return new FxAccountDevice(name, id, type, isCurrentDevice, pushCallback, pushPublicKey, pushAuthKey);
+ }
+
+ public ExtendedJSONObject toJson() {
+ final ExtendedJSONObject body = new ExtendedJSONObject();
+ if (this.name != null) {
+ body.put(JSON_KEY_NAME, this.name);
+ }
+ if (this.id != null) {
+ body.put(JSON_KEY_ID, this.id);
+ }
+ if (this.type != null) {
+ body.put(JSON_KEY_TYPE, this.type);
+ }
+ if (this.pushCallback != null) {
+ body.put(JSON_KEY_PUSH_CALLBACK, this.pushCallback);
+ }
+ if (this.pushPublicKey != null) {
+ body.put(JSON_KEY_PUSH_PUBLICKEY, this.pushPublicKey);
+ }
+ if (this.pushAuthKey != null) {
+ body.put(JSON_KEY_PUSH_AUTHKEY, this.pushAuthKey);
+ }
+ return body;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDeviceRegistrator.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDeviceRegistrator.java
new file mode 100644
index 0000000000..66a8ad8433
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDeviceRegistrator.java
@@ -0,0 +1,282 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this
+* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.FxAccountClient;
+import org.mozilla.gecko.background.fxa.FxAccountClient20;
+import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse;
+import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate;
+import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
+import org.mozilla.gecko.background.fxa.FxAccountRemoteError;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount.InvalidFxAState;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
+import org.mozilla.gecko.util.BundleEventListener;
+import org.mozilla.gecko.util.EventCallback;
+
+import java.io.UnsupportedEncodingException;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.security.GeneralSecurityException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/* This class provides a way to register the current device against FxA
+ * and also stores the registration details in the Android FxAccount.
+ * This should be used in a state where we possess a sessionToken, most likely the Married state.
+ */
+public class FxAccountDeviceRegistrator implements BundleEventListener {
+ private static final String LOG_TAG = "FxADeviceRegistrator";
+
+ // The current version of the device registration, we use this to re-register
+ // devices after we update what we send on device registration.
+ public static final Integer DEVICE_REGISTRATION_VERSION = 2;
+
+ private static FxAccountDeviceRegistrator instance;
+ private final WeakReference<Context> context;
+
+ private FxAccountDeviceRegistrator(Context appContext) {
+ this.context = new WeakReference<Context>(appContext);
+ }
+
+ private static FxAccountDeviceRegistrator getInstance(Context appContext) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ if (instance == null) {
+ FxAccountDeviceRegistrator tempInstance = new FxAccountDeviceRegistrator(appContext);
+ tempInstance.setupListeners(); // Set up listener for FxAccountPush:Subscribe:Response
+ instance = tempInstance;
+ }
+ return instance;
+ }
+
+ public static void register(Context context) {
+ Context appContext = context.getApplicationContext();
+ try {
+ getInstance(appContext).beginRegistration(appContext);
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "Could not start FxA device registration", e);
+ }
+ }
+
+ private void beginRegistration(Context context) {
+ // Fire up gecko and send event
+ // We create the Intent ourselves instead of using GeckoService.getIntentToCreateServices
+ // because we can't import these modules (circular dependency between browser and services)
+ final Intent geckoIntent = new Intent();
+ geckoIntent.setAction("create-services");
+ geckoIntent.setClassName(context, "org.mozilla.gecko.GeckoService");
+ geckoIntent.putExtra("category", "android-push-service");
+ geckoIntent.putExtra("data", "android-fxa-subscribe");
+ final AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context);
+ geckoIntent.putExtra("org.mozilla.gecko.intent.PROFILE_NAME", fxAccount.getProfile());
+ context.startService(geckoIntent);
+ // -> handleMessage()
+ }
+
+ @Override
+ public void handleMessage(String event, Bundle message, EventCallback callback) {
+ if ("FxAccountsPush:Subscribe:Response".equals(event)) {
+ try {
+ doFxaRegistration(message.getBundle("subscription"));
+ } catch (InvalidFxAState e) {
+ Log.d(LOG_TAG, "Invalid state when trying to register with FxA ", e);
+ }
+ } else {
+ Log.e(LOG_TAG, "No action defined for " + event);
+ }
+ }
+
+ private void doFxaRegistration(Bundle subscription) throws InvalidFxAState {
+ final Context context = this.context.get();
+ if (this.context == null) {
+ throw new IllegalStateException("Application context has been gc'ed");
+ }
+ doFxaRegistration(context, subscription, true);
+ }
+
+ private static void doFxaRegistration(final Context context, final Bundle subscription, final boolean allowRecursion) throws InvalidFxAState {
+ String pushCallback = subscription.getString("pushCallback");
+ String pushPublicKey = subscription.getString("pushPublicKey");
+ String pushAuthKey = subscription.getString("pushAuthKey");
+
+ final AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context);
+ if (fxAccount == null) {
+ Log.e(LOG_TAG, "AndroidFxAccount is null");
+ return;
+ }
+ final byte[] sessionToken = fxAccount.getSessionToken();
+ final FxAccountDevice device;
+ String deviceId = fxAccount.getDeviceId();
+ String clientName = getClientName(fxAccount, context);
+ if (TextUtils.isEmpty(deviceId)) {
+ Log.i(LOG_TAG, "Attempting registration for a new device");
+ device = FxAccountDevice.forRegister(clientName, "mobile", pushCallback, pushPublicKey, pushAuthKey);
+ } else {
+ Log.i(LOG_TAG, "Attempting registration for an existing device");
+ Logger.pii(LOG_TAG, "Device ID: " + deviceId);
+ device = FxAccountDevice.forUpdate(deviceId, clientName, pushCallback, pushPublicKey, pushAuthKey);
+ }
+
+ ExecutorService executor = Executors.newSingleThreadExecutor(); // Not called often, it's okay to spawn another thread
+ final FxAccountClient20 fxAccountClient =
+ new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
+ fxAccountClient.registerOrUpdateDevice(sessionToken, device, new RequestDelegate<FxAccountDevice>() {
+ @Override
+ public void handleError(Exception e) {
+ Log.e(LOG_TAG, "Error while updating a device registration: ", e);
+ }
+
+ @Override
+ public void handleFailure(FxAccountClientRemoteException error) {
+ Log.e(LOG_TAG, "Error while updating a device registration: ", error);
+ if (error.httpStatusCode == 400) {
+ if (error.apiErrorNumber == FxAccountRemoteError.UNKNOWN_DEVICE) {
+ recoverFromUnknownDevice(fxAccount);
+ } else if (error.apiErrorNumber == FxAccountRemoteError.DEVICE_SESSION_CONFLICT) {
+ recoverFromDeviceSessionConflict(error, fxAccountClient, sessionToken, fxAccount, context,
+ subscription, allowRecursion);
+ }
+ } else
+ if (error.httpStatusCode == 401
+ && error.apiErrorNumber == FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN) {
+ handleTokenError(error, fxAccountClient, fxAccount);
+ } else {
+ logErrorAndResetDeviceRegistrationVersion(error, fxAccount);
+ }
+ }
+
+ @Override
+ public void handleSuccess(FxAccountDevice result) {
+ Log.i(LOG_TAG, "Device registration complete");
+ Logger.pii(LOG_TAG, "Registered device ID: " + result.id);
+ fxAccount.setFxAUserData(result.id, DEVICE_REGISTRATION_VERSION);
+ }
+ });
+ }
+
+ private static void logErrorAndResetDeviceRegistrationVersion(
+ final FxAccountClientRemoteException error, final AndroidFxAccount fxAccount) {
+ Log.e(LOG_TAG, "Device registration failed", error);
+ fxAccount.resetDeviceRegistrationVersion();
+ }
+
+ @Nullable
+ private static String getClientName(final AndroidFxAccount fxAccount, final Context context) {
+ try {
+ SharedPreferencesClientsDataDelegate clientsDataDelegate =
+ new SharedPreferencesClientsDataDelegate(fxAccount.getSyncPrefs(), context);
+ return clientsDataDelegate.getClientName();
+ } catch (UnsupportedEncodingException | GeneralSecurityException e) {
+ Log.e(LOG_TAG, "Unable to get client name.", e);
+ return null;
+ }
+ }
+
+ private static void handleTokenError(final FxAccountClientRemoteException error,
+ final FxAccountClient fxAccountClient,
+ final AndroidFxAccount fxAccount) {
+ Log.i(LOG_TAG, "Recovering from invalid token error: ", error);
+ logErrorAndResetDeviceRegistrationVersion(error, fxAccount);
+ fxAccountClient.accountStatus(fxAccount.getState().uid,
+ new RequestDelegate<AccountStatusResponse>() {
+ @Override
+ public void handleError(Exception e) {
+ }
+
+ @Override
+ public void handleFailure(FxAccountClientRemoteException e) {
+ }
+
+ @Override
+ public void handleSuccess(AccountStatusResponse result) {
+ State doghouseState = fxAccount.getState().makeDoghouseState();
+ if (!result.exists) {
+ Log.i(LOG_TAG, "token invalidated because the account no longer exists");
+ // TODO: Should be in a "I have an Android account, but the FxA is gone." State.
+ // This will do for now..
+ fxAccount.setState(doghouseState);
+ return;
+ }
+ Log.e(LOG_TAG, "sessionToken invalid");
+ fxAccount.setState(doghouseState);
+ }
+ });
+ }
+
+ private static void recoverFromUnknownDevice(final AndroidFxAccount fxAccount) {
+ Log.i(LOG_TAG, "unknown device id, clearing the cached device id");
+ fxAccount.setDeviceId(null);
+ }
+
+ /**
+ * Will call delegate#complete in all cases
+ */
+ private static void recoverFromDeviceSessionConflict(final FxAccountClientRemoteException error,
+ final FxAccountClient fxAccountClient,
+ final byte[] sessionToken,
+ final AndroidFxAccount fxAccount,
+ final Context context,
+ final Bundle subscription,
+ final boolean allowRecursion) {
+ Log.w(LOG_TAG, "device session conflict, attempting to ascertain the correct device id");
+ fxAccountClient.deviceList(sessionToken, new RequestDelegate<FxAccountDevice[]>() {
+ private void onError() {
+ Log.e(LOG_TAG, "failed to recover from device-session conflict");
+ logErrorAndResetDeviceRegistrationVersion(error, fxAccount);
+ }
+
+ @Override
+ public void handleError(Exception e) {
+ onError();
+ }
+
+ @Override
+ public void handleFailure(FxAccountClientRemoteException e) {
+ onError();
+ }
+
+ @Override
+ public void handleSuccess(FxAccountDevice[] devices) {
+ for (FxAccountDevice device : devices) {
+ if (device.isCurrentDevice) {
+ fxAccount.setFxAUserData(device.id, 0); // Reset device registration version
+ if (!allowRecursion) {
+ Log.d(LOG_TAG, "Failure to register a device on the second try");
+ break;
+ }
+ try {
+ doFxaRegistration(context, subscription, false);
+ return;
+ } catch (InvalidFxAState e) {
+ Log.d(LOG_TAG, "Invalid state when trying to recover from a session conflict ", e);
+ break;
+ }
+ }
+ }
+ onError();
+ }
+ });
+ }
+
+ private void setupListeners() throws ClassNotFoundException, NoSuchMethodException,
+ InvocationTargetException, IllegalAccessException {
+ // We have no choice but to use reflection here, sorry :(
+ Class<?> eventDispatcher = Class.forName("org.mozilla.gecko.EventDispatcher");
+ Method getInstance = eventDispatcher.getMethod("getInstance");
+ Object instance = getInstance.invoke(null);
+ Method registerBackgroundThreadListener = eventDispatcher.getMethod("registerBackgroundThreadListener",
+ BundleEventListener.class, String[].class);
+ registerBackgroundThreadListener.invoke(instance, this, new String[] { "FxAccountsPush:Subscribe:Response" });
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountPushHandler.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountPushHandler.java
new file mode 100644
index 0000000000..0117e63202
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountPushHandler.java
@@ -0,0 +1,95 @@
+package org.mozilla.gecko.fxa;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.Context;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+
+public class FxAccountPushHandler {
+ private static final String LOG_TAG = "FxAccountPush";
+
+ private static final String COMMAND_DEVICE_DISCONNECTED = "fxaccounts:device_disconnected";
+ private static final String COMMAND_COLLECTION_CHANGED = "sync:collection_changed";
+
+ private static final String CLIENTS_COLLECTION = "clients";
+
+ // Forbid instantiation
+ private FxAccountPushHandler() {}
+
+ public static void handleFxAPushMessage(Context context, Bundle bundle) {
+ Log.i(LOG_TAG, "Handling FxA Push Message");
+ String rawMessage = bundle.getString("message");
+ JSONObject message = null;
+ if (!TextUtils.isEmpty(rawMessage)) {
+ try {
+ message = new JSONObject(rawMessage);
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, "Could not parse JSON", e);
+ return;
+ }
+ }
+ if (message == null) {
+ // An empty body means we should check the verification state of the account (FxA sends this
+ // when the account email is verified for example).
+ // TODO: We're only registering the push endpoint when we are in the Married state, that's why we're skipping the message :(
+ Log.d(LOG_TAG, "Skipping empty message");
+ return;
+ }
+ try {
+ String command = message.getString("command");
+ JSONObject data = message.getJSONObject("data");
+ switch (command) {
+ case COMMAND_DEVICE_DISCONNECTED:
+ handleDeviceDisconnection(context, data);
+ break;
+ case COMMAND_COLLECTION_CHANGED:
+ handleCollectionChanged(context, data);
+ break;
+ default:
+ Log.d(LOG_TAG, "No handler defined for FxA Push command " + command);
+ break;
+ }
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, "Error while handling FxA push notification", e);
+ }
+ }
+
+ private static void handleCollectionChanged(Context context, JSONObject data) throws JSONException {
+ JSONArray collections = data.getJSONArray("collections");
+ int len = collections.length();
+ for (int i = 0; i < len; i++) {
+ if (collections.getString(i).equals(CLIENTS_COLLECTION)) {
+ final Account account = FirefoxAccounts.getFirefoxAccount(context);
+ if (account == null) {
+ Log.e(LOG_TAG, "The account does not exist anymore");
+ return;
+ }
+ final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
+ fxAccount.requestImmediateSync(new String[] { CLIENTS_COLLECTION }, null);
+ return;
+ }
+ }
+ }
+
+ private static void handleDeviceDisconnection(Context context, JSONObject data) throws JSONException {
+ final Account account = FirefoxAccounts.getFirefoxAccount(context);
+ if (account == null) {
+ Log.e(LOG_TAG, "The account does not exist anymore");
+ return;
+ }
+ final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
+ if (!fxAccount.getDeviceId().equals(data.getString("id"))) {
+ Log.e(LOG_TAG, "The device ID to disconnect doesn't match with the local device ID.\n"
+ + "Local: " + fxAccount.getDeviceId() + ", ID to disconnect: " + data.getString("id"));
+ return;
+ }
+ AccountManager.get(context).removeAccount(account, null, null);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/SyncStatusListener.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/SyncStatusListener.java
new file mode 100644
index 0000000000..2f70a363af
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/SyncStatusListener.java
@@ -0,0 +1,31 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa;
+
+import android.accounts.Account;
+import android.content.Context;
+import android.support.annotation.UiThread;
+
+/**
+ * Interface definition for a callback to be invoked when an sync status change.
+ */
+public interface SyncStatusListener {
+ public Context getContext();
+ public Account getAccount();
+
+ /**
+ * Called when sync has started.
+ * This is always called in UiThread.
+ */
+ @UiThread
+ public void onSyncStarted();
+
+ /**
+ * Called when sync has finished.
+ * This is always called in UiThread.
+ */
+ @UiThread
+ public void onSyncFinished();
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/CustomColorPreference.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/CustomColorPreference.java
new file mode 100644
index 0000000000..5c4d7f3cca
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/CustomColorPreference.java
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.activities;
+
+import org.mozilla.gecko.R;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.preference.Preference;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.TextView;
+
+ /**
+ * This preference is used to define custom colors for both title and summary texts.
+ * Color code #777777 (placeholder_grey) is used as the fallback color for both title and summary.
+ */
+public class CustomColorPreference extends Preference {
+ private int mTitleColor;
+ private int mSummaryColor;
+
+ public CustomColorPreference(Context context) {
+ super(context);
+ }
+
+ public CustomColorPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs);
+ }
+
+ public CustomColorPreference(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init(context, attrs);
+ }
+
+ public void init(Context context, AttributeSet attrs) {
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomColorPreference);
+ mTitleColor = a.getColor(R.styleable.CustomColorPreference_titleColor, R.color.placeholder_grey);
+ mSummaryColor = a.getColor(R.styleable.CustomColorPreference_summaryColor, R.color.placeholder_grey);
+ a.recycle();
+ }
+
+ @Override
+ protected void onBindView(View view) {
+ super.onBindView(view);
+ final TextView title = (TextView) view.findViewById(android.R.id.title);
+ final TextView summary = (TextView) view.findViewById(android.R.id.summary);
+ title.setTextColor(mTitleColor);
+ summary.setTextColor(mSummaryColor);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountAbstractActivity.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountAbstractActivity.java
new file mode 100644
index 0000000000..fc8cbf0dab
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountAbstractActivity.java
@@ -0,0 +1,80 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.activities;
+
+import android.accounts.Account;
+import android.app.Activity;
+import android.content.Intent;
+
+import org.mozilla.gecko.Locales.LocaleAwareActivity;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+
+public abstract class FxAccountAbstractActivity extends LocaleAwareActivity {
+ private static final String LOG_TAG = FxAccountAbstractActivity.class.getSimpleName();
+
+ protected final boolean cannotResumeWhenAccountsExist;
+ protected final boolean cannotResumeWhenNoAccountsExist;
+
+ public static final int CAN_ALWAYS_RESUME = 0;
+ public static final int CANNOT_RESUME_WHEN_ACCOUNTS_EXIST = 1 << 0;
+ public static final int CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST = 1 << 1;
+
+ public FxAccountAbstractActivity(int resume) {
+ super();
+ this.cannotResumeWhenAccountsExist = 0 != (resume & CANNOT_RESUME_WHEN_ACCOUNTS_EXIST);
+ this.cannotResumeWhenNoAccountsExist = 0 != (resume & CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST);
+ }
+
+ /**
+ * Many Firefox Accounts activities shouldn't display if an account already
+ * exists. This function redirects as appropriate.
+ *
+ * @return true if redirected.
+ */
+ protected boolean redirectIfAppropriate() {
+ if (cannotResumeWhenAccountsExist || cannotResumeWhenNoAccountsExist) {
+ final Account account = FirefoxAccounts.getFirefoxAccount(this);
+ if (cannotResumeWhenAccountsExist && account != null) {
+ redirectToAction(FxAccountConstants.ACTION_FXA_STATUS);
+ return true;
+ }
+ if (cannotResumeWhenNoAccountsExist && account == null) {
+ redirectToAction(FxAccountConstants.ACTION_FXA_GET_STARTED);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ redirectIfAppropriate();
+ }
+
+ @Override
+ public void onBackPressed() {
+ super.onBackPressed();
+ overridePendingTransition(0, 0);
+ }
+
+ protected void launchActivity(Class<? extends Activity> activityClass) {
+ Intent intent = new Intent(this, activityClass);
+ // 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.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+ startActivity(intent);
+ }
+
+ protected void redirectToAction(final String action) {
+ final 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.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+ startActivity(intent);
+ finish();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountConfirmAccountActivityWeb.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountConfirmAccountActivityWeb.java
new file mode 100644
index 0000000000..b2afd9c5ac
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountConfirmAccountActivityWeb.java
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.activities;
+
+public class FxAccountConfirmAccountActivityWeb extends FxAccountWebFlowActivity {
+ public FxAccountConfirmAccountActivityWeb() {
+ super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST, "manage");
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountFinishMigratingActivityWeb.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountFinishMigratingActivityWeb.java
new file mode 100644
index 0000000000..0e66f1d6c1
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountFinishMigratingActivityWeb.java
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.activities;
+
+public class FxAccountFinishMigratingActivityWeb extends FxAccountWebFlowActivity {
+ public FxAccountFinishMigratingActivityWeb() {
+ super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST, "signin", "migration=sync11");
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountGetStartedActivityWeb.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountGetStartedActivityWeb.java
new file mode 100644
index 0000000000..39a907a445
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountGetStartedActivityWeb.java
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.activities;
+
+public class FxAccountGetStartedActivityWeb extends FxAccountWebFlowActivity {
+ public FxAccountGetStartedActivityWeb() {
+ super(CANNOT_RESUME_WHEN_ACCOUNTS_EXIST, "signup");
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusActivity.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusActivity.java
new file mode 100644
index 0000000000..4bb929f0ae
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusActivity.java
@@ -0,0 +1,228 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.activities;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerCallback;
+import android.accounts.AccountManagerFuture;
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.widget.Toolbar;
+import android.util.TypedValue;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.widget.Toast;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.Locales.LocaleAwareAppCompatActivity;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.sync.Utils;
+
+/**
+ * Activity which displays account status.
+ */
+public class FxAccountStatusActivity extends LocaleAwareAppCompatActivity {
+ private static final String LOG_TAG = FxAccountStatusActivity.class.getSimpleName();
+
+ protected FxAccountStatusFragment statusFragment;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Display the fragment as the content.
+ statusFragment = new FxAccountStatusFragment();
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(android.R.id.content, statusFragment)
+ .commit();
+
+ maybeSetHomeButtonEnabled();
+ }
+
+ /**
+ * Sufficiently recent Android versions need additional code to receive taps
+ * on the status bar to go "up". See <a
+ * href="http://stackoverflow.com/a/8953148">this stackoverflow answer</a> for
+ * more information.
+ */
+ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+ protected void maybeSetHomeButtonEnabled() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ Logger.debug(LOG_TAG, "Not enabling home button; version too low.");
+ return;
+ }
+ final ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ Logger.debug(LOG_TAG, "Enabling home button.");
+ actionBar.setHomeButtonEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ return;
+ }
+ Logger.debug(LOG_TAG, "Not enabling home button.");
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ final AndroidFxAccount fxAccount = getAndroidFxAccount();
+ if (fxAccount == null) {
+ Logger.warn(LOG_TAG, "Could not get Firefox Account.");
+
+ // Gracefully redirect to get started.
+ final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
+ // 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.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+ startActivity(intent);
+
+ setResult(RESULT_CANCELED);
+ finish();
+ return;
+ }
+ statusFragment.refresh(fxAccount);
+ }
+
+ /**
+ * Helper to fetch (unique) Android Firefox Account if one exists, or return null.
+ */
+ protected AndroidFxAccount getAndroidFxAccount() {
+ Account account = FirefoxAccounts.getFirefoxAccount(this);
+ if (account == null) {
+ return null;
+ }
+ return new AndroidFxAccount(this, account);
+ }
+
+
+ /**
+ * Helper function to maybe remove the given Android account.
+ */
+ @SuppressLint("InlinedApi")
+ public static void maybeDeleteAndroidAccount(final Activity activity, final Account account, final Intent intent) {
+ if (account == null) {
+ Logger.warn(LOG_TAG, "Trying to delete null account; ignoring request.");
+ return;
+ }
+
+ final AccountManagerCallback<Boolean> callback = new AccountManagerCallback<Boolean>() {
+ @Override
+ public void run(AccountManagerFuture<Boolean> future) {
+ Logger.info(LOG_TAG, "Account " + Utils.obfuscateEmail(account.name) + " removed.");
+ final String text = activity.getResources().getString(R.string.fxaccount_remove_account_toast, account.name);
+ Toast.makeText(activity, text, Toast.LENGTH_LONG).show();
+ if (intent != null) {
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ activity.startActivity(intent);
+ }
+ activity.finish();
+ }
+ };
+
+ /*
+ * Get the best dialog icon from the theme on v11+.
+ * See http://stackoverflow.com/questions/14910536/android-dialog-theme-makes-icon-too-light/14910945#14910945.
+ */
+ final int icon;
+ final TypedValue typedValue = new TypedValue();
+ activity.getTheme().resolveAttribute(android.R.attr.alertDialogIcon, typedValue, true);
+ icon = typedValue.resourceId;
+
+ final AlertDialog dialog = new AlertDialog.Builder(activity)
+ .setTitle(R.string.fxaccount_remove_account_dialog_title)
+ .setIcon(icon)
+ .setMessage(R.string.fxaccount_remove_account_dialog_message)
+ .setPositiveButton(android.R.string.ok, new Dialog.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ AccountManager.get(activity).removeAccount(account, callback, null);
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, new Dialog.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ }
+ })
+ .create();
+
+ dialog.show();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int itemId = item.getItemId();
+ if (itemId == android.R.id.home) {
+ finish();
+ return true;
+ }
+
+ if (itemId == R.id.enable_debug_mode) {
+ FxAccountUtils.LOG_PERSONAL_INFORMATION = !FxAccountUtils.LOG_PERSONAL_INFORMATION;
+ Toast.makeText(this, (FxAccountUtils.LOG_PERSONAL_INFORMATION ? "Enabled" : "Disabled") +
+ " Firefox Account personal information!", Toast.LENGTH_LONG).show();
+ item.setChecked(!item.isChecked());
+ // Display or hide debug options.
+ statusFragment.hardRefresh();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ final MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.fxaccount_status_menu, menu);
+ // !defined(MOZILLA_OFFICIAL) || defined(NIGHTLY_BUILD) || defined(MOZ_DEBUG)
+ boolean enabled = !AppConstants.MOZILLA_OFFICIAL || AppConstants.NIGHTLY_BUILD || AppConstants.DEBUG_BUILD;
+ if (!enabled) {
+ menu.removeItem(R.id.enable_debug_mode);
+ } else {
+ final MenuItem debugModeItem = menu.findItem(R.id.enable_debug_mode);
+ if (debugModeItem != null) {
+ // Update checked state based on internal flag.
+ menu.findItem(R.id.enable_debug_mode).setChecked(FxAccountUtils.LOG_PERSONAL_INFORMATION);
+ }
+ }
+ return super.onCreateOptionsMenu(menu);
+ };
+
+ @Override
+ public void openOptionsMenu() {
+ // This is a workaround of an Android bug:
+ // https://code.google.com/p/android/issues/detail?id=185217
+ // openOptionsMenu isn't overriden by WindowDecorActionBar, which is used by AppCompatActivity,
+ // meaning getSupportActionbar().openOptionsMenu doesn't work.
+ // Based loosely on the code in:
+ // http://androidxref.com/6.0.1_r10/xref/frameworks/support/v7/appcompat/src/android/support/v7/internal/app/WindowDecorActionBar.java#getDecorToolbar
+
+ final Window window = getWindow();
+ final View decor = window.getDecorView();
+ final View view = decor.findViewById(R.id.action_bar);
+
+ if (view instanceof Toolbar) {
+ final Toolbar toolbar = (Toolbar) view;
+ toolbar.showOverflowMenu();
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusFragment.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusFragment.java
new file mode 100644
index 0000000000..a30b92e5f3
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusFragment.java
@@ -0,0 +1,949 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.activities;
+
+import android.accounts.Account;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.Handler;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceScreen;
+import android.support.v4.content.LocalBroadcastManager;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+
+import com.squareup.picasso.Picasso;
+import com.squareup.picasso.Target;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.background.preferences.PreferenceFragment;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.SyncStatusListener;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.Married;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
+import org.mozilla.gecko.sync.SyncConfiguration;
+import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
+import org.mozilla.gecko.util.HardwareUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A fragment that displays the status of an AndroidFxAccount.
+ * <p>
+ * The owning activity is responsible for providing an AndroidFxAccount at
+ * appropriate times.
+ */
+public class FxAccountStatusFragment
+ extends PreferenceFragment
+ implements OnPreferenceClickListener, OnPreferenceChangeListener {
+ private static final String LOG_TAG = FxAccountStatusFragment.class.getSimpleName();
+
+ /**
+ * If a device claims to have synced before this date, we will assume it has never synced.
+ */
+ private 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();
+ }
+
+ // When a checkbox is toggled, wait 5 seconds (for other checkbox actions)
+ // before trying to sync. Should we kill off the fragment before the sync
+ // request happens, that's okay: the runnable will run if the UI thread is
+ // still around to service it, and since we're not updating any UI, we'll just
+ // schedule the sync as usual. See also comment below about garbage
+ // collection.
+ private static final long DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC = 5 * 1000;
+ private static final long LAST_SYNCED_TIME_UPDATE_INTERVAL_IN_MILLISECONDS = 60 * 1000;
+ private static final long PROFILE_FETCH_RETRY_INTERVAL_IN_MILLISECONDS = 60 * 1000;
+
+ private static final String[] STAGES_TO_SYNC_ON_DEVICE_NAME_CHANGE = new String[] { "clients" };
+
+ // By default, the auth/account server preference is only shown when the
+ // account is configured to use a custom server. In debug mode, this is set.
+ private static boolean ALWAYS_SHOW_AUTH_SERVER = false;
+
+ // By default, the Sync server preference is only shown when the account is
+ // configured to use a custom Sync server. In debug mode, this is set.
+ private static boolean ALWAYS_SHOW_SYNC_SERVER = false;
+
+ protected PreferenceCategory accountCategory;
+ protected Preference profilePreference;
+ protected Preference manageAccountPreference;
+ protected Preference authServerPreference;
+ protected Preference removeAccountPreference;
+
+ protected Preference needsPasswordPreference;
+ protected Preference needsUpgradePreference;
+ protected Preference needsVerificationPreference;
+ protected Preference needsMasterSyncAutomaticallyEnabledPreference;
+ protected Preference needsFinishMigratingPreference;
+
+ protected PreferenceCategory syncCategory;
+
+ protected CheckBoxPreference bookmarksPreference;
+ protected CheckBoxPreference historyPreference;
+ protected CheckBoxPreference tabsPreference;
+ protected CheckBoxPreference passwordsPreference;
+ protected CheckBoxPreference readingListPreference;
+
+ protected EditTextPreference deviceNamePreference;
+ protected Preference syncServerPreference;
+ protected Preference morePreference;
+ protected Preference syncNowPreference;
+
+ protected volatile AndroidFxAccount fxAccount;
+ // The contract is: when fxAccount is non-null, then clientsDataDelegate is
+ // non-null. If violated then an IllegalStateException is thrown.
+ protected volatile SharedPreferencesClientsDataDelegate clientsDataDelegate;
+
+ // Used to post delayed sync requests.
+ protected Handler handler;
+
+ // Member variable so that re-posting pushes back the already posted instance.
+ // This Runnable references the fxAccount above, but it is not specific to a
+ // single account. (That is, it does not capture a single account instance.)
+ protected Runnable requestSyncRunnable;
+
+ // Runnable to update last synced time.
+ protected Runnable lastSyncedTimeUpdateRunnable;
+
+ // Broadcast Receiver to update profile Information.
+ protected FxAccountProfileInformationReceiver accountProfileInformationReceiver;
+
+ protected final InnerSyncStatusDelegate syncStatusDelegate = new InnerSyncStatusDelegate();
+ private Target profileAvatarTarget;
+
+ protected Preference ensureFindPreference(String key) {
+ Preference preference = findPreference(key);
+ if (preference == null) {
+ throw new IllegalStateException("Could not find preference with key: " + key);
+ }
+ return preference;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // We need to do this before we can query the hardware menu button state.
+ // We're guaranteed to have an activity at this point (onAttach is called
+ // before onCreate). It's okay to call this multiple times (with different
+ // contexts).
+ HardwareUtils.init(getActivity());
+
+ addPreferences();
+ }
+
+ protected void addPreferences() {
+ addPreferencesFromResource(R.xml.fxaccount_status_prefscreen);
+
+ accountCategory = (PreferenceCategory) ensureFindPreference("signed_in_as_category");
+ profilePreference = ensureFindPreference("profile");
+ manageAccountPreference = ensureFindPreference("manage_account");
+ authServerPreference = ensureFindPreference("auth_server");
+ removeAccountPreference = ensureFindPreference("remove_account");
+
+ needsPasswordPreference = ensureFindPreference("needs_credentials");
+ needsUpgradePreference = ensureFindPreference("needs_upgrade");
+ needsVerificationPreference = ensureFindPreference("needs_verification");
+ needsMasterSyncAutomaticallyEnabledPreference = ensureFindPreference("needs_master_sync_automatically_enabled");
+ needsFinishMigratingPreference = ensureFindPreference("needs_finish_migrating");
+
+ syncCategory = (PreferenceCategory) ensureFindPreference("sync_category");
+
+ bookmarksPreference = (CheckBoxPreference) ensureFindPreference("bookmarks");
+ historyPreference = (CheckBoxPreference) ensureFindPreference("history");
+ tabsPreference = (CheckBoxPreference) ensureFindPreference("tabs");
+ passwordsPreference = (CheckBoxPreference) ensureFindPreference("passwords");
+
+ if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) {
+ removeDebugButtons();
+ } else {
+ connectDebugButtons();
+ ALWAYS_SHOW_AUTH_SERVER = true;
+ ALWAYS_SHOW_SYNC_SERVER = true;
+ }
+
+ profilePreference.setOnPreferenceClickListener(this);
+ manageAccountPreference.setOnPreferenceClickListener(this);
+ removeAccountPreference.setOnPreferenceClickListener(this);
+
+ needsPasswordPreference.setOnPreferenceClickListener(this);
+ needsVerificationPreference.setOnPreferenceClickListener(this);
+ needsFinishMigratingPreference.setOnPreferenceClickListener(this);
+
+ bookmarksPreference.setOnPreferenceClickListener(this);
+ historyPreference.setOnPreferenceClickListener(this);
+ tabsPreference.setOnPreferenceClickListener(this);
+ passwordsPreference.setOnPreferenceClickListener(this);
+
+ deviceNamePreference = (EditTextPreference) ensureFindPreference("device_name");
+ deviceNamePreference.setOnPreferenceChangeListener(this);
+
+ syncServerPreference = ensureFindPreference("sync_server");
+ morePreference = ensureFindPreference("more");
+ morePreference.setOnPreferenceClickListener(this);
+
+ syncNowPreference = ensureFindPreference("sync_now");
+ syncNowPreference.setEnabled(true);
+ syncNowPreference.setOnPreferenceClickListener(this);
+
+ ensureFindPreference("linktos").setOnPreferenceClickListener(this);
+ ensureFindPreference("linkprivacy").setOnPreferenceClickListener(this);
+ }
+
+ /**
+ * We intentionally don't refresh here. Our owning activity is responsible for
+ * providing an AndroidFxAccount to our refresh method in its onResume method.
+ */
+ @Override
+ public void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ if (preference == profilePreference) {
+ ActivityUtils.openURLInFennec(getActivity().getApplicationContext(), "about:accounts?action=avatar");
+ return true;
+ }
+
+ if (preference == manageAccountPreference) {
+ ActivityUtils.openURLInFennec(getActivity().getApplicationContext(), "about:accounts?action=manage");
+ return true;
+ }
+
+ if (preference == removeAccountPreference) {
+ FxAccountStatusActivity.maybeDeleteAndroidAccount(getActivity(), fxAccount.getAndroidAccount(), null);
+ return true;
+ }
+
+ if (preference == needsPasswordPreference) {
+ final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_UPDATE_CREDENTIALS);
+ intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES);
+ // 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.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+ startActivity(intent);
+
+ return true;
+ }
+
+ if (preference == needsFinishMigratingPreference) {
+ final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_FINISH_MIGRATING);
+ intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES);
+ // 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.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+ startActivity(intent);
+
+ return true;
+ }
+
+ if (preference == needsVerificationPreference) {
+ final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_CONFIRM_ACCOUNT);
+ // 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.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+ intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES);
+ startActivity(intent);
+
+ return true;
+ }
+
+ if (preference == bookmarksPreference ||
+ preference == historyPreference ||
+ preference == passwordsPreference ||
+ preference == tabsPreference) {
+ saveEngineSelections();
+ return true;
+ }
+
+ if (preference == morePreference) {
+ getActivity().openOptionsMenu();
+ return true;
+ }
+
+ if (preference == syncNowPreference) {
+ if (fxAccount != null) {
+ fxAccount.requestImmediateSync(null, null);
+ }
+ return true;
+ }
+
+ if (TextUtils.equals("linktos", preference.getKey())) {
+ ActivityUtils.openURLInFennec(getActivity().getApplicationContext(), getResources().getString(R.string.fxaccount_link_tos));
+ return true;
+ }
+
+ if (TextUtils.equals("linkprivacy", preference.getKey())) {
+ ActivityUtils.openURLInFennec(getActivity().getApplicationContext(), getResources().getString(R.string.fxaccount_link_pn));
+ return true;
+ }
+
+ return false;
+ }
+
+ protected void setCheckboxesEnabled(boolean enabled) {
+ bookmarksPreference.setEnabled(enabled);
+ historyPreference.setEnabled(enabled);
+ tabsPreference.setEnabled(enabled);
+ passwordsPreference.setEnabled(enabled);
+ // Since we can't sync, we can't update our remote client record.
+ deviceNamePreference.setEnabled(enabled);
+ syncNowPreference.setEnabled(enabled);
+ }
+
+ /**
+ * Show at most one error preference, hiding all others.
+ *
+ * @param errorPreferenceToShow
+ * single error preference to show; if null, hide all error preferences
+ */
+ protected void showOnlyOneErrorPreference(Preference errorPreferenceToShow) {
+ final Preference[] errorPreferences = new Preference[] {
+ this.needsPasswordPreference,
+ this.needsUpgradePreference,
+ this.needsVerificationPreference,
+ this.needsMasterSyncAutomaticallyEnabledPreference,
+ this.needsFinishMigratingPreference,
+ };
+ for (Preference errorPreference : errorPreferences) {
+ final boolean currentlyShown = null != findPreference(errorPreference.getKey());
+ final boolean shouldBeShown = errorPreference == errorPreferenceToShow;
+ if (currentlyShown == shouldBeShown) {
+ continue;
+ }
+ if (shouldBeShown) {
+ syncCategory.addPreference(errorPreference);
+ } else {
+ syncCategory.removePreference(errorPreference);
+ }
+ }
+ }
+
+ protected void showNeedsPassword() {
+ syncCategory.setTitle(R.string.fxaccount_status_sync);
+ showOnlyOneErrorPreference(needsPasswordPreference);
+ setCheckboxesEnabled(false);
+ }
+
+ protected void showNeedsUpgrade() {
+ syncCategory.setTitle(R.string.fxaccount_status_sync);
+ showOnlyOneErrorPreference(needsUpgradePreference);
+ setCheckboxesEnabled(false);
+ }
+
+ protected void showNeedsVerification() {
+ syncCategory.setTitle(R.string.fxaccount_status_sync);
+ showOnlyOneErrorPreference(needsVerificationPreference);
+ setCheckboxesEnabled(false);
+ }
+
+ protected void showNeedsMasterSyncAutomaticallyEnabled() {
+ syncCategory.setTitle(R.string.fxaccount_status_sync);
+ needsMasterSyncAutomaticallyEnabledPreference.setTitle(AppConstants.Versions.preLollipop ?
+ R.string.fxaccount_status_needs_master_sync_automatically_enabled :
+ R.string.fxaccount_status_needs_master_sync_automatically_enabled_v21);
+ showOnlyOneErrorPreference(needsMasterSyncAutomaticallyEnabledPreference);
+ setCheckboxesEnabled(false);
+ }
+
+ protected void showNeedsFinishMigrating() {
+ syncCategory.setTitle(R.string.fxaccount_status_sync);
+ showOnlyOneErrorPreference(needsFinishMigratingPreference);
+ setCheckboxesEnabled(false);
+ }
+
+ protected void showConnected() {
+ syncCategory.setTitle(R.string.fxaccount_status_sync_enabled);
+ showOnlyOneErrorPreference(null);
+ setCheckboxesEnabled(true);
+ }
+
+ protected class InnerSyncStatusDelegate implements SyncStatusListener {
+ protected final Runnable refreshRunnable = new Runnable() {
+ @Override
+ public void run() {
+ refresh();
+ }
+ };
+
+ @Override
+ public Context getContext() {
+ return FxAccountStatusFragment.this.getActivity();
+ }
+
+ @Override
+ public Account getAccount() {
+ return fxAccount.getAndroidAccount();
+ }
+
+ @Override
+ public void onSyncStarted() {
+ if (fxAccount == null) {
+ return;
+ }
+ Logger.info(LOG_TAG, "Got sync started message; refreshing.");
+ getActivity().runOnUiThread(refreshRunnable);
+ }
+
+ @Override
+ public void onSyncFinished() {
+ if (fxAccount == null) {
+ return;
+ }
+ Logger.info(LOG_TAG, "Got sync finished message; refreshing.");
+ getActivity().runOnUiThread(refreshRunnable);
+ }
+ }
+
+ /**
+ * Notify the fragment that a new AndroidFxAccount instance is current.
+ * <p>
+ * <b>Important:</b> call this method on the UI thread!
+ * <p>
+ * In future, this might be a Loader.
+ *
+ * @param fxAccount new instance.
+ */
+ public void refresh(AndroidFxAccount fxAccount) {
+ if (fxAccount == null) {
+ throw new IllegalArgumentException("fxAccount must not be null");
+ }
+ this.fxAccount = fxAccount;
+ try {
+ this.clientsDataDelegate = new SharedPreferencesClientsDataDelegate(fxAccount.getSyncPrefs(), getActivity().getApplicationContext());
+ } catch (Exception e) {
+ Logger.error(LOG_TAG, "Got exception fetching Sync prefs associated to Firefox Account; aborting.", e);
+ // Something is terribly wrong; best to get a stack trace rather than
+ // continue with a null clients delegate.
+ throw new IllegalStateException(e);
+ }
+
+ handler = new Handler(); // Attached to current (assumed to be UI) thread.
+
+ // Runnable is not specific to one Firefox Account. This runnable will keep
+ // a reference to this fragment alive, but we expect posted runnables to be
+ // serviced very quickly, so this is not an issue.
+ requestSyncRunnable = new RequestSyncRunnable();
+ lastSyncedTimeUpdateRunnable = new LastSyncTimeUpdateRunnable();
+
+ // We would very much like register these status observers in bookended
+ // onResume/onPause calls, but because the Fragment gets onResume during the
+ // Activity's super.onResume, it hasn't yet been told its Firefox Account.
+ // So we register the observer here (and remove it in onPause), and open
+ // ourselves to the possibility that we don't have properly paired
+ // register/unregister calls.
+ FxAccountSyncStatusHelper.getInstance().startObserving(syncStatusDelegate);
+
+ // Register a local broadcast receiver to get profile cached notification.
+ final IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(FxAccountConstants.ACCOUNT_PROFILE_JSON_UPDATED_ACTION);
+ accountProfileInformationReceiver = new FxAccountProfileInformationReceiver();
+ LocalBroadcastManager.getInstance(getActivity()).registerReceiver(accountProfileInformationReceiver, intentFilter);
+
+ // profilePreference is set during onCreate, so it's definitely not null here.
+ final float cornerRadius = getResources().getDimension(R.dimen.fxaccount_profile_image_width) / 2;
+ profileAvatarTarget = new PicassoPreferenceIconTarget(getResources(), profilePreference, cornerRadius);
+
+ refresh();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusDelegate);
+
+ // Focus lost, remove scheduled update if any.
+ if (lastSyncedTimeUpdateRunnable != null) {
+ handler.removeCallbacks(lastSyncedTimeUpdateRunnable);
+ }
+
+ // Focus lost, unregister broadcast receiver.
+ if (accountProfileInformationReceiver != null) {
+ LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(accountProfileInformationReceiver);
+ }
+
+ if (profileAvatarTarget != null) {
+ Picasso.with(getActivity()).cancelRequest(profileAvatarTarget);
+ profileAvatarTarget = null;
+ }
+ }
+
+ protected void hardRefresh() {
+ // This is the only way to guarantee that the EditText dialogs created by
+ // EditTextPreferences are re-created. This works around the issue described
+ // at http://androiddev.orkitra.com/?p=112079.
+ final PreferenceScreen statusScreen = (PreferenceScreen) ensureFindPreference("status_screen");
+ statusScreen.removeAll();
+ addPreferences();
+
+ refresh();
+ }
+
+ protected void refresh() {
+ // refresh is called from our onResume, which can happen before the owning
+ // Activity tells us about an account (via our public
+ // refresh(AndroidFxAccount) method).
+ if (fxAccount == null) {
+ throw new IllegalArgumentException("fxAccount must not be null");
+ }
+
+ updateProfileInformation();
+ updateAuthServerPreference();
+ updateSyncServerPreference();
+
+ try {
+ // There are error states determined by Android, not the login state
+ // machine, and we have a chance to present these states here. We handle
+ // them specially, since we can't surface these states as part of syncing,
+ // because they generally stop syncs from happening regularly. Right now
+ // there are no such states.
+
+ // Interrogate the Firefox Account's state.
+ State state = fxAccount.getState();
+ switch (state.getNeededAction()) {
+ case NeedsUpgrade:
+ showNeedsUpgrade();
+ break;
+ case NeedsPassword:
+ showNeedsPassword();
+ break;
+ case NeedsVerification:
+ showNeedsVerification();
+ break;
+ case NeedsFinishMigrating:
+ showNeedsFinishMigrating();
+ break;
+ case None:
+ showConnected();
+ break;
+ }
+
+ // We check for the master setting last, since it is not strictly
+ // necessary for the user to address this error state: it's really a
+ // warning state. We surface it for the user's convenience, and to prevent
+ // confused folks wondering why Sync is not working at all.
+ final boolean masterSyncAutomatically = ContentResolver.getMasterSyncAutomatically();
+ if (!masterSyncAutomatically) {
+ showNeedsMasterSyncAutomaticallyEnabled();
+ return;
+ }
+ } finally {
+ // No matter our state, we should update the checkboxes.
+ updateSelectedEngines();
+ }
+
+ final String clientName = clientsDataDelegate.getClientName();
+ deviceNamePreference.setSummary(clientName);
+ deviceNamePreference.setText(clientName);
+
+ updateSyncNowPreference();
+ }
+
+ // This is a helper function similar to TabsAccessor.getLastSyncedString() to calculate relative "Last synced" time span.
+ private String getLastSyncedString(final long startTime) {
+ if (new Date(startTime).before(EARLIEST_VALID_SYNCED_DATE)) {
+ return getActivity().getString(R.string.fxaccount_status_never_synced);
+ }
+ final CharSequence relativeTimeSpanString = DateUtils.getRelativeTimeSpanString(startTime);
+ return getActivity().getResources().getString(R.string.fxaccount_status_last_synced, relativeTimeSpanString);
+ }
+
+ protected void updateSyncNowPreference() {
+ final boolean currentlySyncing = fxAccount.isCurrentlySyncing();
+ syncNowPreference.setEnabled(!currentlySyncing);
+ if (currentlySyncing) {
+ syncNowPreference.setTitle(R.string.fxaccount_status_syncing);
+ } else {
+ syncNowPreference.setTitle(R.string.fxaccount_status_sync_now);
+ }
+ scheduleAndUpdateLastSyncedTime();
+ }
+
+ private void updateProfileInformation() {
+
+ final ExtendedJSONObject profileJSON = fxAccount.getProfileJSON();
+ if (profileJSON == null) {
+ // Update the profile title with email as the fallback.
+ // Profile icon by default use the default avatar as the fallback.
+ profilePreference.setTitle(fxAccount.getEmail());
+ return;
+ }
+
+ updateProfileInformation(profileJSON);
+ }
+
+ /**
+ * Update profile information from json on UI thread.
+ *
+ * @param profileJSON json fetched from server.
+ */
+ protected void updateProfileInformation(final ExtendedJSONObject profileJSON) {
+ // View changes must always be done on UI thread.
+ ThreadUtils.assertOnUiThread();
+
+ FxAccountUtils.pii(LOG_TAG, "Profile JSON is: " + profileJSON.toJSONString());
+
+ final String userName = profileJSON.getString(FxAccountConstants.KEY_PROFILE_JSON_USERNAME);
+ // Update the profile username and email if available.
+ if (!TextUtils.isEmpty(userName)) {
+ profilePreference.setTitle(userName);
+ profilePreference.setSummary(fxAccount.getEmail());
+ } else {
+ profilePreference.setTitle(fxAccount.getEmail());
+ }
+
+ // Avatar URI empty, skip profile image fetch.
+ final String avatarURI = profileJSON.getString(FxAccountConstants.KEY_PROFILE_JSON_AVATAR);
+ if (TextUtils.isEmpty(avatarURI)) {
+ Logger.info(LOG_TAG, "AvatarURI is empty, skipping profile image fetch.");
+ return;
+ }
+
+ // Using noPlaceholder would avoid a pop of the default image, but it's not available in the version of Picasso
+ // we ship in the tree.
+ Picasso
+ .with(getActivity())
+ .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);
+ }
+
+ private void scheduleAndUpdateLastSyncedTime() {
+ final String lastSynced = getLastSyncedString(fxAccount.getLastSyncedTimestamp());
+ syncNowPreference.setSummary(lastSynced);
+ handler.postDelayed(lastSyncedTimeUpdateRunnable, LAST_SYNCED_TIME_UPDATE_INTERVAL_IN_MILLISECONDS);
+ }
+
+ protected void updateAuthServerPreference() {
+ final String authServer = fxAccount.getAccountServerURI();
+ final boolean shouldBeShown = ALWAYS_SHOW_AUTH_SERVER || !FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT.equals(authServer);
+ final boolean currentlyShown = null != findPreference(authServerPreference.getKey());
+ if (currentlyShown != shouldBeShown) {
+ if (shouldBeShown) {
+ accountCategory.addPreference(authServerPreference);
+ } else {
+ accountCategory.removePreference(authServerPreference);
+ }
+ }
+ // Always set the summary, because on first run, the preference is visible,
+ // and the above block will be skipped if there is a custom value.
+ authServerPreference.setSummary(authServer);
+ }
+
+ protected void updateSyncServerPreference() {
+ final String syncServer = fxAccount.getTokenServerURI();
+ final boolean shouldBeShown = ALWAYS_SHOW_SYNC_SERVER || !FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT.equals(syncServer);
+ final boolean currentlyShown = null != findPreference(syncServerPreference.getKey());
+ if (currentlyShown != shouldBeShown) {
+ if (shouldBeShown) {
+ syncCategory.addPreference(syncServerPreference);
+ } else {
+ syncCategory.removePreference(syncServerPreference);
+ }
+ }
+ // Always set the summary, because on first run, the preference is visible,
+ // and the above block will be skipped if there is a custom value.
+ syncServerPreference.setSummary(syncServer);
+ }
+
+ /**
+ * Query shared prefs for the current engine state, and update the UI
+ * accordingly.
+ * <p>
+ * In future, we might want this to be on a background thread, or implemented
+ * as a Loader.
+ */
+ protected void updateSelectedEngines() {
+ try {
+ SharedPreferences syncPrefs = fxAccount.getSyncPrefs();
+ Map<String, Boolean> engines = SyncConfiguration.getUserSelectedEngines(syncPrefs);
+ if (engines != null) {
+ bookmarksPreference.setChecked(engines.containsKey("bookmarks") && engines.get("bookmarks"));
+ historyPreference.setChecked(engines.containsKey("history") && engines.get("history"));
+ passwordsPreference.setChecked(engines.containsKey("passwords") && engines.get("passwords"));
+ tabsPreference.setChecked(engines.containsKey("tabs") && engines.get("tabs"));
+ return;
+ }
+
+ // We don't have user specified preferences. Perhaps we have seen a meta/global?
+ Set<String> enabledNames = SyncConfiguration.getEnabledEngineNames(syncPrefs);
+ if (enabledNames != null) {
+ bookmarksPreference.setChecked(enabledNames.contains("bookmarks"));
+ historyPreference.setChecked(enabledNames.contains("history"));
+ passwordsPreference.setChecked(enabledNames.contains("passwords"));
+ tabsPreference.setChecked(enabledNames.contains("tabs"));
+ return;
+ }
+
+ // Okay, we don't have userSelectedEngines or enabledEngines. That means
+ // the user hasn't specified to begin with, we haven't specified here, and
+ // we haven't already seen, Sync engines. We don't know our state, so
+ // let's check everything (the default) and disable everything.
+ bookmarksPreference.setChecked(true);
+ historyPreference.setChecked(true);
+ passwordsPreference.setChecked(true);
+ tabsPreference.setChecked(true);
+ setCheckboxesEnabled(false);
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Got exception getting engines to select; ignoring.", e);
+ return;
+ }
+ }
+
+ /**
+ * Persist engine selections to local shared preferences, and request a sync
+ * to persist selections to remote storage.
+ */
+ protected void saveEngineSelections() {
+ final Map<String, Boolean> engineSelections = new HashMap<String, Boolean>();
+ engineSelections.put("bookmarks", bookmarksPreference.isChecked());
+ engineSelections.put("history", historyPreference.isChecked());
+ engineSelections.put("passwords", passwordsPreference.isChecked());
+ engineSelections.put("tabs", tabsPreference.isChecked());
+
+ // No GlobalSession.config, so store directly to shared prefs. We do this on
+ // a background thread to avoid IO on the main thread and strict mode
+ // warnings.
+ new Thread(new PersistEngineSelectionsRunnable(engineSelections)).start();
+ }
+
+ protected void requestDelayedSync() {
+ Logger.info(LOG_TAG, "Posting a delayed request for a sync sometime soon.");
+ handler.removeCallbacks(requestSyncRunnable);
+ handler.postDelayed(requestSyncRunnable, DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC);
+ }
+
+ /**
+ * Remove all traces of debug buttons. By default, no debug buttons are shown.
+ */
+ protected void removeDebugButtons() {
+ final PreferenceScreen statusScreen = (PreferenceScreen) ensureFindPreference("status_screen");
+ final PreferenceCategory debugCategory = (PreferenceCategory) ensureFindPreference("debug_category");
+ statusScreen.removePreference(debugCategory);
+ }
+
+ /**
+ * A Runnable that persists engine selections to shared prefs, and then
+ * requests a delayed sync.
+ * <p>
+ * References the member <code>fxAccount</code> and is specific to the Android
+ * account associated to that account.
+ */
+ protected class PersistEngineSelectionsRunnable implements Runnable {
+ private final Map<String, Boolean> engineSelections;
+
+ protected PersistEngineSelectionsRunnable(Map<String, Boolean> engineSelections) {
+ this.engineSelections = engineSelections;
+ }
+
+ @Override
+ public void run() {
+ try {
+ // Name shadowing -- do you like it, or do you love it?
+ AndroidFxAccount fxAccount = FxAccountStatusFragment.this.fxAccount;
+ if (fxAccount == null) {
+ return;
+ }
+ Logger.info(LOG_TAG, "Persisting engine selections: " + engineSelections.toString());
+ SyncConfiguration.storeSelectedEnginesToPrefs(fxAccount.getSyncPrefs(), engineSelections);
+ requestDelayedSync();
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Got exception persisting selected engines; ignoring.", e);
+ return;
+ }
+ }
+ }
+
+ /**
+ * A Runnable that requests a sync.
+ * <p>
+ * References the member <code>fxAccount</code>, but is not specific to the
+ * Android account associated to that account.
+ */
+ protected class RequestSyncRunnable implements Runnable {
+ @Override
+ public void run() {
+ // Name shadowing -- do you like it, or do you love it?
+ AndroidFxAccount fxAccount = FxAccountStatusFragment.this.fxAccount;
+ if (fxAccount == null) {
+ return;
+ }
+ Logger.info(LOG_TAG, "Requesting a sync sometime soon.");
+ fxAccount.requestEventualSync(null, null);
+ }
+ }
+
+ /**
+ * The Runnable that schedules a future update and updates the last synced time.
+ */
+ protected class LastSyncTimeUpdateRunnable implements Runnable {
+ @Override
+ public void run() {
+ scheduleAndUpdateLastSyncedTime();
+ }
+ }
+
+ /**
+ * Broadcast receiver to receive updates for the cached profile action.
+ */
+ public class FxAccountProfileInformationReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!intent.getAction().equals(FxAccountConstants.ACCOUNT_PROFILE_JSON_UPDATED_ACTION)) {
+ return;
+ }
+
+ Logger.info(LOG_TAG, "Profile avatar cache update action broadcast received.");
+ // Update the UI from cached profile json on the main thread.
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ updateProfileInformation();
+ }
+ });
+ }
+ }
+
+ /**
+ * A separate listener to separate debug logic from main code paths.
+ */
+ protected class DebugPreferenceClickListener implements OnPreferenceClickListener {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ final String key = preference.getKey();
+ if ("debug_refresh".equals(key)) {
+ Logger.info(LOG_TAG, "Refreshing.");
+ refresh();
+ } else if ("debug_dump".equals(key)) {
+ fxAccount.dump();
+ } else if ("debug_force_sync".equals(key)) {
+ Logger.info(LOG_TAG, "Force syncing.");
+ fxAccount.requestImmediateSync(null, null);
+ // No sense refreshing, since the sync will complete in the future.
+ } else if ("debug_forget_certificate".equals(key)) {
+ State state = fxAccount.getState();
+ try {
+ Married married = (Married) state;
+ Logger.info(LOG_TAG, "Moving to Cohabiting state: Forgetting certificate.");
+ fxAccount.setState(married.makeCohabitingState());
+ refresh();
+ } catch (ClassCastException e) {
+ Logger.info(LOG_TAG, "Not in Married state; can't forget certificate.");
+ // Ignore.
+ }
+ } else if ("debug_invalidate_certificate".equals(key)) {
+ State state = fxAccount.getState();
+ try {
+ Married married = (Married) state;
+ Logger.info(LOG_TAG, "Invalidating certificate.");
+ fxAccount.setState(married.makeCohabitingState().withCertificate("INVALID CERTIFICATE"));
+ refresh();
+ } catch (ClassCastException e) {
+ Logger.info(LOG_TAG, "Not in Married state; can't invalidate certificate.");
+ // Ignore.
+ }
+ } else if ("debug_require_password".equals(key)) {
+ Logger.info(LOG_TAG, "Moving to Separated state: Forgetting password.");
+ State state = fxAccount.getState();
+ fxAccount.setState(state.makeSeparatedState());
+ refresh();
+ } else if ("debug_require_upgrade".equals(key)) {
+ Logger.info(LOG_TAG, "Moving to Doghouse state: Requiring upgrade.");
+ State state = fxAccount.getState();
+ fxAccount.setState(state.makeDoghouseState());
+ refresh();
+ } else if ("debug_migrated_from_sync11".equals(key)) {
+ Logger.info(LOG_TAG, "Moving to MigratedFromSync11 state: Requiring password.");
+ State state = fxAccount.getState();
+ fxAccount.setState(state.makeMigratedFromSync11State(null));
+ refresh();
+ } else if ("debug_make_account_stage".equals(key)) {
+ Logger.info(LOG_TAG, "Moving Account endpoints, in place, to stage. Deleting Sync and RL prefs and requiring password.");
+ fxAccount.unsafeTransitionToStageEndpoints();
+ refresh();
+ } else if ("debug_make_account_default".equals(key)) {
+ Logger.info(LOG_TAG, "Moving Account endpoints, in place, to default (production). Deleting Sync and RL prefs and requiring password.");
+ fxAccount.unsafeTransitionToDefaultEndpoints();
+ refresh();
+ } else {
+ return false;
+ }
+ return true;
+ }
+ }
+
+ /**
+ * Iterate through debug buttons, adding a special debug preference click
+ * listener to each of them.
+ */
+ protected void connectDebugButtons() {
+ // Separate listener to really separate debug logic from main code paths.
+ final OnPreferenceClickListener listener = new DebugPreferenceClickListener();
+
+ // We don't want to use Android resource strings for debug UI, so we just
+ // use the keys throughout.
+ final PreferenceCategory debugCategory = (PreferenceCategory) ensureFindPreference("debug_category");
+ debugCategory.setTitle(debugCategory.getKey());
+
+ for (int i = 0; i < debugCategory.getPreferenceCount(); i++) {
+ final Preference button = debugCategory.getPreference(i);
+ button.setTitle(button.getKey()); // Not very friendly, but this is for debugging only!
+ button.setOnPreferenceClickListener(listener);
+ }
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (preference == deviceNamePreference) {
+ String newClientName = (String) newValue;
+ if (TextUtils.isEmpty(newClientName)) {
+ newClientName = clientsDataDelegate.getDefaultClientName();
+ }
+ final long now = System.currentTimeMillis();
+ clientsDataDelegate.setClientName(newClientName, now);
+ // Force sync the client record, we want the user to see the device name change immediately
+ // on the FxA Device Manager if possible ( = we are online) to avoid confusion
+ // ("I changed my Android's device name but I don't see it on my computer").
+ fxAccount.requestImmediateSync(STAGES_TO_SYNC_ON_DEVICE_NAME_CHANGE, null);
+ hardRefresh(); // Updates the value displayed to the user, among other things.
+ return true;
+ }
+
+ // For everything else, accept the change.
+ return true;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountUpdateCredentialsActivityWeb.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountUpdateCredentialsActivityWeb.java
new file mode 100644
index 0000000000..5a2ea79c83
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountUpdateCredentialsActivityWeb.java
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.activities;
+
+public class FxAccountUpdateCredentialsActivityWeb extends FxAccountWebFlowActivity {
+ public FxAccountUpdateCredentialsActivityWeb() {
+ super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST, "force_auth");
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountWebFlowActivity.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountWebFlowActivity.java
new file mode 100644
index 0000000000..e33e9c5776
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountWebFlowActivity.java
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.activities;
+
+import android.content.Intent;
+import android.os.Bundle;
+import org.mozilla.gecko.Locales;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
+
+/**
+ * Activity which shows the status activity or passes through to web flow.
+ */
+public abstract class FxAccountWebFlowActivity extends FxAccountAbstractActivity {
+ protected static final String LOG_TAG = FxAccountWebFlowActivity.class.getSimpleName();
+
+ protected static final String ABOUT_ACCOUNTS = "about:accounts";
+
+ public static final String EXTRA_ENDPOINT = "entrypoint";
+
+ protected static final String[] EXTRAS_TO_PASSTHROUGH = new String[] {
+ EXTRA_ENDPOINT,
+ };
+
+ private final String action;
+ private final String extras;
+
+ public FxAccountWebFlowActivity(int resume, String action) {
+ this(resume, action, null);
+ }
+
+ public FxAccountWebFlowActivity(int resume, String action, String extras) {
+ super(resume);
+ this.action = action;
+ this.extras = (extras != null) ? ("&" + extras) : "";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(Bundle icicle) {
+ Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG);
+ Logger.debug(LOG_TAG, "onCreate(" + icicle + ")");
+
+ Locales.initializeLocale(getApplicationContext());
+
+ super.onCreate(icicle);
+ }
+
+ protected boolean redirectIfAppropriate() {
+ final boolean redirected = super.redirectIfAppropriate();
+ if (redirected) {
+ return true;
+ }
+
+ final StringBuilder sb = new StringBuilder();
+ sb.append(ABOUT_ACCOUNTS);
+ sb.append("?action=");
+ sb.append(action);
+ sb.append(extras);
+
+ // Pass through a set of known string values from intent extras to about:accounts.
+ final Intent intent = getIntent();
+ if (intent != null) {
+ for (String key : EXTRAS_TO_PASSTHROUGH) {
+ final String value = intent.getStringExtra(key);
+ if (value != null) {
+ sb.append("&");
+ sb.append(key);
+ sb.append("=");
+ sb.append(value);
+ }
+ }
+ }
+
+ ActivityUtils.openURLInFennec(getApplicationContext(), sb.toString());
+ return true;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ // We are always redirected.
+ this.finish();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/PicassoPreferenceIconTarget.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/PicassoPreferenceIconTarget.java
new file mode 100644
index 0000000000..f71d3ed1cd
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/PicassoPreferenceIconTarget.java
@@ -0,0 +1,63 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.activities;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.preference.Preference;
+import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
+import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
+import com.squareup.picasso.Picasso;
+import com.squareup.picasso.Target;
+import org.mozilla.gecko.AppConstants;
+
+/**
+ * A Picasso Target that updates a preference icon.
+ *
+ * Nota bene: Android grew support for updating preference icons programatically
+ * only in API 11. This class silently ignores requests before API 11.
+ */
+public class PicassoPreferenceIconTarget implements Target {
+ private final Preference preference;
+ private final Resources resources;
+ private final float cornerRadius;
+
+ public PicassoPreferenceIconTarget(Resources resources, Preference preference) {
+ this(resources, preference, 0);
+ }
+
+ public PicassoPreferenceIconTarget(Resources resources, Preference preference, float cornerRadius) {
+ this.resources = resources;
+ this.preference = preference;
+ this.cornerRadius = cornerRadius;
+ }
+
+ @Override
+ public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
+ final Drawable drawable;
+ if (cornerRadius > 0) {
+ final RoundedBitmapDrawable roundedBitmapDrawable;
+ roundedBitmapDrawable = RoundedBitmapDrawableFactory.create(resources, bitmap);
+ roundedBitmapDrawable.setCornerRadius(cornerRadius);
+ roundedBitmapDrawable.setAntiAlias(true);
+ drawable = roundedBitmapDrawable;
+ } else {
+ drawable = new BitmapDrawable(resources, bitmap);
+ }
+ preference.setIcon(drawable);
+ }
+
+ @Override
+ public void onBitmapFailed(Drawable errorDrawable) {
+ preference.setIcon(errorDrawable);
+ }
+
+ @Override
+ public void onPrepareLoad(Drawable placeHolderDrawable) {
+ preference.setIcon(placeHolderDrawable);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AccountPickler.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AccountPickler.java
new file mode 100644
index 0000000000..3f2c5620d5
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AccountPickler.java
@@ -0,0 +1,362 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.authenticator;
+
+import java.io.FileOutputStream;
+import java.io.PrintStream;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.fxa.login.State.StateLabel;
+import org.mozilla.gecko.fxa.login.StateFactory;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.Utils;
+
+import android.content.Context;
+
+/**
+ * Android deletes Account objects when the Authenticator that owns the Account
+ * disappears. This happens when an App is installed to the SD card and the SD
+ * card is un-mounted or the device is rebooted.
+ * <p>
+ * We work around this by pickling the current Firefox account data every sync
+ * and unpickling when we check if Firefox accounts exist (called from Fennec).
+ * <p>
+ * Android just doesn't support installing Apps that define long-lived Services
+ * and/or own Account types onto the SD card. The documentation says not to do
+ * it. There are hordes of developers who want to do it, and have tried to
+ * register for almost every "package installation changed" broadcast intent
+ * that Android supports. They all explicitly state that the package that has
+ * changed does *not* receive the broadcast intent, thereby preventing an App
+ * from re-establishing its state.
+ * <p>
+ * <a href="http://developer.android.com/guide/topics/data/install-location.html">Reference.</a>
+ * <p>
+ * <b>Quote</b>: Your AbstractThreadedSyncAdapter and all its sync functionality
+ * will not work until external storage is remounted.
+ * <p>
+ * <b>Quote</b>: Your running Service will be killed and will not be restarted
+ * when external storage is remounted. You can, however, register for the
+ * ACTION_EXTERNAL_APPLICATIONS_AVAILABLE broadcast Intent, which will notify
+ * your application when applications installed on external storage have become
+ * available to the system again. At which time, you can restart your Service.
+ * <p>
+ * Problem: <a href="http://code.google.com/p/android/issues/detail?id=8485">that intent doesn't work</a>!
+ * <p>
+ * See bug 768102 for more information in the context of Sync.
+ */
+public class AccountPickler {
+ public static final String LOG_TAG = AccountPickler.class.getSimpleName();
+
+ public static final long PICKLE_VERSION = 3;
+
+ public static final String KEY_PICKLE_VERSION = "pickle_version";
+ public static final String KEY_PICKLE_TIMESTAMP = "pickle_timestamp";
+
+ public static final String KEY_ACCOUNT_VERSION = "account_version";
+ public static final String KEY_ACCOUNT_TYPE = "account_type";
+ public static final String KEY_EMAIL = "email";
+ public static final String KEY_PROFILE = "profile";
+ public static final String KEY_IDP_SERVER_URI = "idpServerURI";
+ public static final String KEY_TOKEN_SERVER_URI = "tokenServerURI";
+ public static final String KEY_PROFILE_SERVER_URI = "profileServerURI";
+
+ public static final String KEY_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP = "authoritiesToSyncAutomaticallyMap";
+
+ // Deprecated, but maintained for migration purposes.
+ public static final String KEY_IS_SYNCING_ENABLED = "isSyncingEnabled";
+
+ public static final String KEY_BUNDLE = "bundle";
+
+ /**
+ * Remove Firefox account persisted to disk.
+ * This operation is synchronized to avoid race condition while deleting the account.
+ *
+ * @param context Android context.
+ * @param filename name of persisted pickle file; must not contain path separators.
+ * @return <code>true</code> if given pickle existed and was successfully deleted.
+ */
+ public synchronized static boolean deletePickle(final Context context, final String filename) {
+ return context.deleteFile(filename);
+ }
+
+ public static ExtendedJSONObject toJSON(final AndroidFxAccount account, final long now) {
+ final ExtendedJSONObject o = new ExtendedJSONObject();
+ o.put(KEY_PICKLE_VERSION, PICKLE_VERSION);
+ o.put(KEY_PICKLE_TIMESTAMP, now);
+
+ o.put(KEY_ACCOUNT_VERSION, AndroidFxAccount.CURRENT_ACCOUNT_VERSION);
+ o.put(KEY_ACCOUNT_TYPE, FxAccountConstants.ACCOUNT_TYPE);
+ o.put(KEY_EMAIL, account.getEmail());
+ o.put(KEY_PROFILE, account.getProfile());
+ o.put(KEY_IDP_SERVER_URI, account.getAccountServerURI());
+ o.put(KEY_TOKEN_SERVER_URI, account.getTokenServerURI());
+ o.put(KEY_PROFILE_SERVER_URI, account.getProfileServerURI());
+
+ final ExtendedJSONObject p = new ExtendedJSONObject();
+ for (Entry<String, Boolean> pair : account.getAuthoritiesToSyncAutomaticallyMap().entrySet()) {
+ p.put(pair.getKey(), pair.getValue());
+ }
+ o.put(KEY_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP, p);
+
+ // TODO: If prefs version changes under us, SyncPrefsPath will change, "clearing" prefs.
+
+ final ExtendedJSONObject bundle = account.unbundle();
+ if (bundle == null) {
+ Logger.warn(LOG_TAG, "Unable to obtain account bundle; aborting.");
+ return null;
+ }
+ o.put(KEY_BUNDLE, bundle);
+
+ return o;
+ }
+
+ /**
+ * Persist Firefox account to disk as a JSON object.
+ * This operation is synchronized to avoid race condition while deleting the account.
+ *
+ * @param account the AndroidFxAccount to persist to disk
+ * @param filename name of file to persist to; must not contain path separators.
+ */
+ public synchronized static void pickle(final AndroidFxAccount account, final String filename) {
+ final ExtendedJSONObject o = toJSON(account, System.currentTimeMillis());
+ writeToDisk(account.context, filename, o);
+ }
+
+ private static void writeToDisk(final Context context, final String filename,
+ final ExtendedJSONObject pickle) {
+ try {
+ final FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE);
+ try {
+ final PrintStream ps = new PrintStream(fos);
+ try {
+ ps.print(pickle.toJSONString());
+ Logger.debug(LOG_TAG, "Persisted " + pickle.keySet().size() +
+ " account settings to " + filename + ".");
+ } finally {
+ ps.close();
+ }
+ } finally {
+ fos.close();
+ }
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Caught exception persisting account settings to " + filename +
+ "; ignoring.", e);
+ }
+ }
+
+ /**
+ * Create Android account from saved JSON object. Assumes that an account does not exist.
+ * This operation is synchronized to avoid race condition while deleting the account.
+ *
+ * @param context
+ * Android context.
+ * @param filename
+ * name of file to read from; must not contain path separators.
+ * @return created Android account, or null on error.
+ */
+ public synchronized static AndroidFxAccount unpickle(final Context context, final String filename) {
+ final String jsonString = Utils.readFile(context, filename);
+ if (jsonString == null) {
+ Logger.info(LOG_TAG, "Pickle file '" + filename + "' not found; aborting.");
+ return null;
+ }
+
+ ExtendedJSONObject json = null;
+ try {
+ json = new ExtendedJSONObject(jsonString);
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Got exception reading pickle file '" + filename + "'; aborting.", e);
+ return null;
+ }
+
+ final UnpickleParams params;
+ try {
+ params = UnpickleParams.fromJSON(json);
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Got exception extracting unpickle json; aborting.", e);
+ return null;
+ }
+
+ final AndroidFxAccount account;
+ try {
+ account = AndroidFxAccount.addAndroidAccount(context, params.email, params.profile,
+ params.authServerURI, params.tokenServerURI, params.profileServerURI, params.state,
+ params.authoritiesToSyncAutomaticallyMap,
+ params.accountVersion,
+ true, params.bundle);
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Exception when adding Android Account; aborting.", e);
+ return null;
+ }
+
+ if (account == null) {
+ Logger.warn(LOG_TAG, "Failed to add Android Account; aborting.");
+ return null;
+ }
+
+ Long timestamp = json.getLong(KEY_PICKLE_TIMESTAMP);
+ if (timestamp == null) {
+ Logger.warn(LOG_TAG, "Did not find timestamp in pickle file; ignoring.");
+ timestamp = -1L;
+ }
+
+ Logger.info(LOG_TAG, "Un-pickled Android account named " + params.email + " (version " +
+ params.pickleVersion + ", pickled at " + timestamp + ").");
+
+ return account;
+ }
+
+ private static class UnpickleParams {
+ private Long pickleVersion;
+
+ private int accountVersion;
+ private String email;
+ private String profile;
+ private String authServerURI;
+ private String tokenServerURI;
+ private String profileServerURI;
+ private final Map<String, Boolean> authoritiesToSyncAutomaticallyMap = new HashMap<>();
+
+ private ExtendedJSONObject bundle;
+ private State state;
+
+ private UnpickleParams() {
+ }
+
+ private static UnpickleParams fromJSON(final ExtendedJSONObject json)
+ throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
+ final UnpickleParams params = new UnpickleParams();
+ params.pickleVersion = json.getLong(KEY_PICKLE_VERSION);
+ if (params.pickleVersion == null) {
+ throw new IllegalStateException("Pickle version not found.");
+ }
+
+ /*
+ * Version 1 and version 2 are identical, except version 2 throws if the
+ * internal Android Account type has changed. Version 1 used to throw in
+ * this case, but we intentionally used the pickle file to migrate across
+ * Account types, bumping the version simultaneously.
+ *
+ * Version 3 replaces "isSyncEnabled" with a map (String -> Boolean)
+ * associating Android authorities to whether or not they are configured
+ * to sync automatically.
+ */
+ switch (params.pickleVersion.intValue()) {
+ case 3: {
+ // Sanity check.
+ final String accountType = json.getString(KEY_ACCOUNT_TYPE);
+ if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) {
+ throw new IllegalStateException("Account type has changed from " + accountType + " to " + FxAccountConstants.ACCOUNT_TYPE + ".");
+ }
+
+ params.unpickleV3(json);
+ }
+ break;
+
+ case 2: {
+ // Sanity check.
+ final String accountType = json.getString(KEY_ACCOUNT_TYPE);
+ if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) {
+ throw new IllegalStateException("Account type has changed from " + accountType + " to " + FxAccountConstants.ACCOUNT_TYPE + ".");
+ }
+
+ params.unpickleV1(json);
+ }
+ break;
+
+ case 1: {
+ // Warn about account type changing, but don't throw over it.
+ final String accountType = json.getString(KEY_ACCOUNT_TYPE);
+ if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) {
+ Logger.warn(LOG_TAG, "Account type has changed from " + accountType + " to " + FxAccountConstants.ACCOUNT_TYPE + "; ignoring.");
+ }
+
+ params.unpickleV1(json);
+ }
+ break;
+
+ default:
+ throw new IllegalStateException("Unknown pickle version, " + params.pickleVersion + ".");
+ }
+
+ return params;
+ }
+
+ private void unpickleV1(final ExtendedJSONObject json)
+ throws NonObjectJSONException, NoSuchAlgorithmException, InvalidKeySpecException {
+
+ this.accountVersion = json.getIntegerSafely(KEY_ACCOUNT_VERSION);
+ this.email = json.getString(KEY_EMAIL);
+ this.profile = json.getString(KEY_PROFILE);
+ this.authServerURI = json.getString(KEY_IDP_SERVER_URI);
+ this.tokenServerURI = json.getString(KEY_TOKEN_SERVER_URI);
+ this.profileServerURI = json.getString(KEY_PROFILE_SERVER_URI);
+
+ // Fallback to default value when profile server URI was not pickled.
+ if (this.profileServerURI == null) {
+ this.profileServerURI = FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT.equals(this.authServerURI)
+ ? FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT
+ : FxAccountConstants.STAGE_PROFILE_SERVER_ENDPOINT;
+ }
+
+ // We get the default value for everything except syncing browser data.
+ this.authoritiesToSyncAutomaticallyMap.put(BrowserContract.AUTHORITY, json.getBoolean(KEY_IS_SYNCING_ENABLED));
+
+ this.bundle = json.getObject(KEY_BUNDLE);
+ if (bundle == null) {
+ throw new IllegalStateException("Pickle bundle is null.");
+ }
+ this.state = getState(bundle);
+ }
+
+ private void unpickleV3(final ExtendedJSONObject json)
+ throws NonObjectJSONException, NoSuchAlgorithmException, InvalidKeySpecException {
+ // We'll overwrite the extracted sync automatically map.
+ unpickleV1(json);
+
+ // Extract the map of authorities to sync automatically.
+ authoritiesToSyncAutomaticallyMap.clear();
+ final ExtendedJSONObject o = json.getObject(KEY_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP);
+ if (o == null) {
+ return;
+ }
+ for (String key : o.keySet()) {
+ final Boolean enabled = o.getBoolean(key);
+ if (enabled != null) {
+ authoritiesToSyncAutomaticallyMap.put(key, enabled);
+ }
+ }
+ }
+
+ private State getState(final ExtendedJSONObject bundle) throws InvalidKeySpecException,
+ NonObjectJSONException, NoSuchAlgorithmException {
+ // TODO: Should copy-pasta BUNDLE_KEY_STATE & LABEL to this file to ensure we maintain
+ // old versions?
+ final StateLabel stateLabelString = StateLabel.valueOf(
+ bundle.getString(AndroidFxAccount.BUNDLE_KEY_STATE_LABEL));
+ final String stateString = bundle.getString(AndroidFxAccount.BUNDLE_KEY_STATE);
+ if (stateLabelString == null || stateString == null) {
+ throw new IllegalStateException("stateLabel and stateString must not be null, but: " +
+ "(stateLabel == null) = " + (stateLabelString == null) +
+ " and (stateString == null) = " + (stateString == null));
+ }
+
+ try {
+ return StateFactory.fromJSONObject(stateLabelString, new ExtendedJSONObject(stateString));
+ } catch (Exception e) {
+ throw new IllegalStateException("could not get state", e);
+ }
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java
new file mode 100644
index 0000000000..d7ce7c47f7
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java
@@ -0,0 +1,929 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.authenticator;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.content.LocalBroadcastManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.mozilla.gecko.background.common.GlobalConstants;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.fxa.login.State.StateLabel;
+import org.mozilla.gecko.fxa.login.StateFactory;
+import org.mozilla.gecko.fxa.login.TokensAndKeysState;
+import org.mozilla.gecko.fxa.sync.FxAccountProfileService;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.setup.Constants;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Semaphore;
+
+/**
+ * A Firefox Account that stores its details and state as user data attached to
+ * an Android Account instance.
+ * <p>
+ * Account user data is accessible only to the Android App(s) that own the
+ * Account type. Account user data is not removed when the App's private data is
+ * cleared.
+ */
+public class AndroidFxAccount {
+ protected static final String LOG_TAG = AndroidFxAccount.class.getSimpleName();
+
+ public static final int CURRENT_SYNC_PREFS_VERSION = 1;
+ public static final int CURRENT_RL_PREFS_VERSION = 1;
+
+ // When updating the account, do not forget to update AccountPickler.
+ public static final int CURRENT_ACCOUNT_VERSION = 3;
+ public static final String ACCOUNT_KEY_ACCOUNT_VERSION = "version";
+ public static final String ACCOUNT_KEY_PROFILE = "profile";
+ public static final String ACCOUNT_KEY_IDP_SERVER = "idpServerURI";
+ private static final String ACCOUNT_KEY_PROFILE_SERVER = "profileServerURI";
+
+ public static final String ACCOUNT_KEY_TOKEN_SERVER = "tokenServerURI"; // Sync-specific.
+ public static final String ACCOUNT_KEY_DESCRIPTOR = "descriptor";
+
+ public static final int CURRENT_BUNDLE_VERSION = 2;
+ public static final String BUNDLE_KEY_BUNDLE_VERSION = "version";
+ public static final String BUNDLE_KEY_STATE_LABEL = "stateLabel";
+ public static final String BUNDLE_KEY_STATE = "state";
+ public static final String BUNDLE_KEY_PROFILE_JSON = "profile";
+
+ public static final String ACCOUNT_KEY_DEVICE_ID = "deviceId";
+ public static final String ACCOUNT_KEY_DEVICE_REGISTRATION_VERSION = "deviceRegistrationVersion";
+
+ // Account authentication token type for fetching account profile.
+ public static final String PROFILE_OAUTH_TOKEN_TYPE = "oauth::profile";
+
+ // Services may request OAuth tokens from the Firefox Account dynamically.
+ // Each such token is prefixed with "oauth::" and a service-dependent scope.
+ // Such tokens should be destroyed when the account is removed from the device.
+ // This list collects all the known "oauth::" token types in order to delete them when necessary.
+ private static final List<String> KNOWN_OAUTH_TOKEN_TYPES;
+
+ static {
+ final List<String> list = new ArrayList<>();
+ list.add(PROFILE_OAUTH_TOKEN_TYPE);
+ KNOWN_OAUTH_TOKEN_TYPES = Collections.unmodifiableList(list);
+ }
+
+ public static final Map<String, Boolean> DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP;
+ static {
+ final HashMap<String, Boolean> m = new HashMap<String, Boolean>();
+ // By default, Firefox Sync is enabled.
+ m.put(BrowserContract.AUTHORITY, true);
+ DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP = Collections.unmodifiableMap(m);
+ }
+
+ private static final String PREF_KEY_LAST_SYNCED_TIMESTAMP = "lastSyncedTimestamp";
+
+ protected final Context context;
+ protected final AccountManager accountManager;
+ protected final Account account;
+
+ /**
+ * A cache associating Account name (email address) to a representation of the
+ * account's internal bundle.
+ * <p>
+ * The cache is invalidated entirely when <it>any</it> new Account is added,
+ * because there is no reliable way to know that an Account has been removed
+ * and then re-added.
+ */
+ protected static final ConcurrentHashMap<String, ExtendedJSONObject> perAccountBundleCache =
+ new ConcurrentHashMap<>();
+
+ public static void invalidateCaches() {
+ perAccountBundleCache.clear();
+ }
+
+ /**
+ * Create an Android Firefox Account instance backed by an Android Account
+ * instance.
+ * <p>
+ * We expect a long-lived application context to avoid life-cycle issues that
+ * might arise if the internally cached AccountManager instance surfaces UI.
+ * <p>
+ * We take care to not install any listeners or observers that might outlive
+ * the AccountManager; and Android ensures the AccountManager doesn't outlive
+ * the associated context.
+ *
+ * @param applicationContext
+ * to use as long-lived ambient Android context.
+ * @param account
+ * Android account to use for storage.
+ */
+ public AndroidFxAccount(Context applicationContext, Account account) {
+ this.context = applicationContext;
+ this.account = account;
+ this.accountManager = AccountManager.get(this.context);
+ }
+
+ public static AndroidFxAccount fromContext(Context context) {
+ context = context.getApplicationContext();
+ Account account = FirefoxAccounts.getFirefoxAccount(context);
+ if (account == null) {
+ return null;
+ }
+ return new AndroidFxAccount(context, account);
+ }
+
+ /**
+ * Persist the Firefox account to disk as a JSON object. Note that this is a wrapper around
+ * {@link AccountPickler#pickle}, and is identical to calling it directly.
+ * <p>
+ * Note that pickling is different from bundling, which involves operations on a
+ * {@link android.os.Bundle Bundle} object of miscellaneous data associated with the account.
+ * See {@link #persistBundle} and {@link #unbundle} for more.
+ */
+ public void pickle(final String filename) {
+ AccountPickler.pickle(this, filename);
+ }
+
+ public Account getAndroidAccount() {
+ return this.account;
+ }
+
+ protected int getAccountVersion() {
+ String v = accountManager.getUserData(account, ACCOUNT_KEY_ACCOUNT_VERSION);
+ if (v == null) {
+ return 0; // Implicit.
+ }
+
+ try {
+ return Integer.parseInt(v, 10);
+ } catch (NumberFormatException ex) {
+ return 0;
+ }
+ }
+
+ /**
+ * Saves the given data as the internal bundle associated with this account.
+ * @param bundle to write to account.
+ */
+ protected synchronized void persistBundle(ExtendedJSONObject bundle) {
+ perAccountBundleCache.put(account.name, bundle);
+ accountManager.setUserData(account, ACCOUNT_KEY_DESCRIPTOR, bundle.toJSONString());
+ }
+
+ protected ExtendedJSONObject unbundle() {
+ return unbundle(true);
+ }
+
+ /**
+ * Retrieve the internal bundle associated with this account.
+ * @return bundle associated with account.
+ */
+ protected synchronized ExtendedJSONObject unbundle(boolean allowCachedBundle) {
+ if (allowCachedBundle) {
+ final ExtendedJSONObject cachedBundle = perAccountBundleCache.get(account.name);
+ if (cachedBundle != null) {
+ Logger.debug(LOG_TAG, "Returning cached account bundle.");
+ return cachedBundle;
+ }
+ }
+
+ final int version = getAccountVersion();
+ if (version < CURRENT_ACCOUNT_VERSION) {
+ // Needs upgrade. For now, do nothing. We'd like to just put your account
+ // into the Separated state here and have you update your credentials.
+ return null;
+ }
+
+ if (version > CURRENT_ACCOUNT_VERSION) {
+ // Oh dear.
+ return null;
+ }
+
+ String bundleString = accountManager.getUserData(account, ACCOUNT_KEY_DESCRIPTOR);
+ if (bundleString == null) {
+ return null;
+ }
+ final ExtendedJSONObject bundle = unbundleAccountV2(bundleString);
+ perAccountBundleCache.put(account.name, bundle);
+ Logger.info(LOG_TAG, "Account bundle persisted to cache.");
+ return bundle;
+ }
+
+ protected String getBundleData(String key) {
+ ExtendedJSONObject o = unbundle();
+ if (o == null) {
+ return null;
+ }
+ return o.getString(key);
+ }
+
+ protected boolean getBundleDataBoolean(String key, boolean def) {
+ ExtendedJSONObject o = unbundle();
+ if (o == null) {
+ return def;
+ }
+ Boolean b = o.getBoolean(key);
+ if (b == null) {
+ return def;
+ }
+ return b;
+ }
+
+ protected byte[] getBundleDataBytes(String key) {
+ ExtendedJSONObject o = unbundle();
+ if (o == null) {
+ return null;
+ }
+ return o.getByteArrayHex(key);
+ }
+
+ protected void updateBundleValues(String key, String value, String... more) {
+ if (more.length % 2 != 0) {
+ throw new IllegalArgumentException("more must be a list of key, value pairs");
+ }
+ ExtendedJSONObject descriptor = unbundle();
+ if (descriptor == null) {
+ return;
+ }
+ descriptor.put(key, value);
+ for (int i = 0; i + 1 < more.length; i += 2) {
+ descriptor.put(more[i], more[i+1]);
+ }
+ persistBundle(descriptor);
+ }
+
+ private ExtendedJSONObject unbundleAccountV1(String bundle) {
+ ExtendedJSONObject o;
+ try {
+ o = new ExtendedJSONObject(bundle);
+ } catch (Exception e) {
+ return null;
+ }
+ if (CURRENT_BUNDLE_VERSION == o.getIntegerSafely(BUNDLE_KEY_BUNDLE_VERSION)) {
+ return o;
+ }
+ return null;
+ }
+
+ private ExtendedJSONObject unbundleAccountV2(String bundle) {
+ return unbundleAccountV1(bundle);
+ }
+
+ /**
+ * Note that if the user clears data, an account will be left pointing to a
+ * deleted profile. Such is life.
+ */
+ public String getProfile() {
+ return accountManager.getUserData(account, ACCOUNT_KEY_PROFILE);
+ }
+
+ public String getAccountServerURI() {
+ return accountManager.getUserData(account, ACCOUNT_KEY_IDP_SERVER);
+ }
+
+ public String getTokenServerURI() {
+ return accountManager.getUserData(account, ACCOUNT_KEY_TOKEN_SERVER);
+ }
+
+ public String getProfileServerURI() {
+ String profileURI = accountManager.getUserData(account, ACCOUNT_KEY_PROFILE_SERVER);
+ if (profileURI == null) {
+ if (isStaging()) {
+ return FxAccountConstants.STAGE_PROFILE_SERVER_ENDPOINT;
+ }
+ return FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT;
+ }
+ return profileURI;
+ }
+
+ public String getOAuthServerURI() {
+ // Allow testing against stage.
+ if (isStaging()) {
+ return FxAccountConstants.STAGE_OAUTH_SERVER_ENDPOINT;
+ } else {
+ return FxAccountConstants.DEFAULT_OAUTH_SERVER_ENDPOINT;
+ }
+ }
+
+ private boolean isStaging() {
+ return FxAccountConstants.STAGE_AUTH_SERVER_ENDPOINT.equals(getAccountServerURI());
+ }
+
+ private String constructPrefsPath(String product, long version, String extra) throws GeneralSecurityException, UnsupportedEncodingException {
+ String profile = getProfile();
+ String username = account.name;
+
+ if (profile == null) {
+ throw new IllegalStateException("Missing profile. Cannot fetch prefs.");
+ }
+
+ if (username == null) {
+ throw new IllegalStateException("Missing username. Cannot fetch prefs.");
+ }
+
+ final String fxaServerURI = getAccountServerURI();
+ if (fxaServerURI == null) {
+ throw new IllegalStateException("No account server URI. Cannot fetch prefs.");
+ }
+
+ // This is unique for each syncing 'view' of the account.
+ final String serverURLThing = fxaServerURI + "!" + extra;
+ return Utils.getPrefsPath(product, username, serverURLThing, profile, version);
+ }
+
+ /**
+ * This needs to return a string because of the tortured prefs access in GlobalSession.
+ */
+ public String getSyncPrefsPath() throws GeneralSecurityException, UnsupportedEncodingException {
+ final String tokenServerURI = getTokenServerURI();
+ if (tokenServerURI == null) {
+ throw new IllegalStateException("No token server URI. Cannot fetch prefs.");
+ }
+
+ final String product = GlobalConstants.BROWSER_INTENT_PACKAGE + ".fxa";
+ final long version = CURRENT_SYNC_PREFS_VERSION;
+ return constructPrefsPath(product, version, tokenServerURI);
+ }
+
+ public String getReadingListPrefsPath() throws GeneralSecurityException, UnsupportedEncodingException {
+ final String product = GlobalConstants.BROWSER_INTENT_PACKAGE + ".reading";
+ final long version = CURRENT_RL_PREFS_VERSION;
+ return constructPrefsPath(product, version, "");
+ }
+
+ public SharedPreferences getSyncPrefs() throws UnsupportedEncodingException, GeneralSecurityException {
+ return context.getSharedPreferences(getSyncPrefsPath(), Utils.SHARED_PREFERENCES_MODE);
+ }
+
+ public SharedPreferences getReadingListPrefs() throws UnsupportedEncodingException, GeneralSecurityException {
+ return context.getSharedPreferences(getReadingListPrefsPath(), Utils.SHARED_PREFERENCES_MODE);
+ }
+
+ /**
+ * Extract a JSON dictionary of the string values associated to this account.
+ * <p>
+ * <b>For debugging use only!</b> The contents of this JSON object completely
+ * determine the user's Firefox Account status and yield access to whatever
+ * user data the device has access to.
+ *
+ * @return JSON-object of Strings.
+ */
+ public ExtendedJSONObject toJSONObject() {
+ ExtendedJSONObject o = unbundle();
+ o.put("email", account.name);
+ try {
+ o.put("emailUTF8", Utils.byte2Hex(account.name.getBytes("UTF-8")));
+ } catch (UnsupportedEncodingException e) {
+ // Ignore.
+ }
+ o.put("fxaDeviceId", getDeviceId());
+ o.put("fxaDeviceRegistrationVersion", getDeviceRegistrationVersion());
+ return o;
+ }
+
+ public static AndroidFxAccount addAndroidAccount(
+ Context context,
+ String email,
+ String profile,
+ String idpServerURI,
+ String tokenServerURI,
+ String profileServerURI,
+ State state,
+ final Map<String, Boolean> authoritiesToSyncAutomaticallyMap)
+ throws UnsupportedEncodingException, GeneralSecurityException, URISyntaxException {
+ return addAndroidAccount(context, email, profile, idpServerURI, tokenServerURI, profileServerURI, state,
+ authoritiesToSyncAutomaticallyMap,
+ CURRENT_ACCOUNT_VERSION, false, null);
+ }
+
+ public static AndroidFxAccount addAndroidAccount(
+ Context context,
+ String email,
+ String profile,
+ String idpServerURI,
+ String tokenServerURI,
+ String profileServerURI,
+ State state,
+ final Map<String, Boolean> authoritiesToSyncAutomaticallyMap,
+ final int accountVersion,
+ final boolean fromPickle,
+ ExtendedJSONObject bundle)
+ throws UnsupportedEncodingException, GeneralSecurityException, URISyntaxException {
+ if (email == null) {
+ throw new IllegalArgumentException("email must not be null");
+ }
+ if (profile == null) {
+ throw new IllegalArgumentException("profile must not be null");
+ }
+ if (idpServerURI == null) {
+ throw new IllegalArgumentException("idpServerURI must not be null");
+ }
+ if (tokenServerURI == null) {
+ throw new IllegalArgumentException("tokenServerURI must not be null");
+ }
+ if (profileServerURI == null) {
+ throw new IllegalArgumentException("profileServerURI must not be null");
+ }
+ if (state == null) {
+ throw new IllegalArgumentException("state must not be null");
+ }
+
+ // TODO: Add migration code.
+ if (accountVersion != CURRENT_ACCOUNT_VERSION) {
+ throw new IllegalStateException("Could not create account of version " + accountVersion +
+ ". Current version is " + CURRENT_ACCOUNT_VERSION + ".");
+ }
+
+ // Android has internal restrictions that require all values in this
+ // bundle to be strings. *sigh*
+ Bundle userdata = new Bundle();
+ userdata.putString(ACCOUNT_KEY_ACCOUNT_VERSION, "" + CURRENT_ACCOUNT_VERSION);
+ userdata.putString(ACCOUNT_KEY_IDP_SERVER, idpServerURI);
+ userdata.putString(ACCOUNT_KEY_TOKEN_SERVER, tokenServerURI);
+ userdata.putString(ACCOUNT_KEY_PROFILE_SERVER, profileServerURI);
+ userdata.putString(ACCOUNT_KEY_PROFILE, profile);
+
+ if (bundle == null) {
+ bundle = new ExtendedJSONObject();
+ // TODO: How to upgrade?
+ bundle.put(BUNDLE_KEY_BUNDLE_VERSION, CURRENT_BUNDLE_VERSION);
+ }
+ bundle.put(BUNDLE_KEY_STATE_LABEL, state.getStateLabel().name());
+ bundle.put(BUNDLE_KEY_STATE, state.toJSONObject().toJSONString());
+
+ userdata.putString(ACCOUNT_KEY_DESCRIPTOR, bundle.toJSONString());
+
+ Account account = new Account(email, FxAccountConstants.ACCOUNT_TYPE);
+ AccountManager accountManager = AccountManager.get(context);
+ // We don't set an Android password, because we don't want to persist the
+ // password (or anything else as powerful as the password). Instead, we
+ // internally manage a sessionToken with a remotely owned lifecycle.
+ boolean added = accountManager.addAccountExplicitly(account, null, userdata);
+ if (!added) {
+ return null;
+ }
+
+ // Try to work around an intermittent issue described at
+ // http://stackoverflow.com/a/11698139. What happens is that tests that
+ // delete and re-create the same account frequently will find the account
+ // missing all or some of the userdata bundle, possibly due to an Android
+ // AccountManager caching bug.
+ for (String key : userdata.keySet()) {
+ accountManager.setUserData(account, key, userdata.getString(key));
+ }
+
+ AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
+
+ if (!fromPickle) {
+ fxAccount.clearSyncPrefs();
+ }
+
+ fxAccount.setAuthoritiesToSyncAutomaticallyMap(authoritiesToSyncAutomaticallyMap);
+
+ return fxAccount;
+ }
+
+ public void clearSyncPrefs() throws UnsupportedEncodingException, GeneralSecurityException {
+ getSyncPrefs().edit().clear().commit();
+ }
+
+ public void setAuthoritiesToSyncAutomaticallyMap(Map<String, Boolean> authoritiesToSyncAutomaticallyMap) {
+ if (authoritiesToSyncAutomaticallyMap == null) {
+ throw new IllegalArgumentException("authoritiesToSyncAutomaticallyMap must not be null");
+ }
+
+ for (String authority : DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP.keySet()) {
+ boolean authorityEnabled = DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP.get(authority);
+ final Boolean enabled = authoritiesToSyncAutomaticallyMap.get(authority);
+ if (enabled != null) {
+ authorityEnabled = enabled.booleanValue();
+ }
+ // Accounts are always capable of being synced ...
+ ContentResolver.setIsSyncable(account, authority, 1);
+ // ... but not always automatically synced.
+ ContentResolver.setSyncAutomatically(account, authority, authorityEnabled);
+ }
+ }
+
+ public Map<String, Boolean> getAuthoritiesToSyncAutomaticallyMap() {
+ final Map<String, Boolean> authoritiesToSync = new HashMap<>();
+ for (String authority : DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP.keySet()) {
+ final boolean enabled = ContentResolver.getSyncAutomatically(account, authority);
+ authoritiesToSync.put(authority, enabled);
+ }
+ return authoritiesToSync;
+ }
+
+ /**
+ * Is a sync currently in progress?
+ *
+ * @return true if Android is currently syncing the underlying Android Account.
+ */
+ public boolean isCurrentlySyncing() {
+ boolean active = false;
+ for (String authority : AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP.keySet()) {
+ active |= ContentResolver.isSyncActive(account, authority);
+ }
+ return active;
+ }
+
+ /**
+ * Request an immediate sync. Use this to sync as soon as possible in response to user action.
+ *
+ * @param stagesToSync stage names to sync; can be null to sync <b>all</b> known stages.
+ * @param stagesToSkip stage names to skip; can be null to skip <b>no</b> known stages.
+ */
+ public void requestImmediateSync(String[] stagesToSync, String[] stagesToSkip) {
+ FirefoxAccounts.requestImmediateSync(getAndroidAccount(), stagesToSync, stagesToSkip);
+ }
+
+ /**
+ * Request an eventual sync. Use this to request the system queue a sync for some time in the
+ * future.
+ *
+ * @param stagesToSync stage names to sync; can be null to sync <b>all</b> known stages.
+ * @param stagesToSkip stage names to skip; can be null to skip <b>no</b> known stages.
+ */
+ public void requestEventualSync(String[] stagesToSync, String[] stagesToSkip) {
+ FirefoxAccounts.requestEventualSync(getAndroidAccount(), stagesToSync, stagesToSkip);
+ }
+
+ public synchronized void setState(State state) {
+ if (state == null) {
+ throw new IllegalArgumentException("state must not be null");
+ }
+ Logger.info(LOG_TAG, "Moving account named like " + getObfuscatedEmail() +
+ " to state " + state.getStateLabel().toString());
+ updateBundleValues(
+ BUNDLE_KEY_STATE_LABEL, state.getStateLabel().name(),
+ BUNDLE_KEY_STATE, state.toJSONObject().toJSONString());
+ broadcastAccountStateChangedIntent();
+ }
+
+ protected void broadcastAccountStateChangedIntent() {
+ final Intent intent = new Intent(FxAccountConstants.ACCOUNT_STATE_CHANGED_ACTION);
+ intent.putExtra(Constants.JSON_KEY_ACCOUNT, account.name);
+ LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
+ }
+
+ public synchronized State getState() {
+ String stateLabelString = getBundleData(BUNDLE_KEY_STATE_LABEL);
+ String stateString = getBundleData(BUNDLE_KEY_STATE);
+ if (stateLabelString == null || stateString == null) {
+ throw new IllegalStateException("stateLabelString and stateString must not be null, but: " +
+ "(stateLabelString == null) = " + (stateLabelString == null) +
+ " and (stateString == null) = " + (stateString == null));
+ }
+
+ try {
+ StateLabel stateLabel = StateLabel.valueOf(stateLabelString);
+ Logger.debug(LOG_TAG, "Account is in state " + stateLabel);
+ return StateFactory.fromJSONObject(stateLabel, new ExtendedJSONObject(stateString));
+ } catch (Exception e) {
+ throw new IllegalStateException("could not get state", e);
+ }
+ }
+
+ public byte[] getSessionToken() throws InvalidFxAState {
+ State state = getState();
+ StateLabel stateLabel = state.getStateLabel();
+ if (stateLabel == StateLabel.Cohabiting || stateLabel == StateLabel.Married) {
+ TokensAndKeysState tokensAndKeysState = (TokensAndKeysState) state;
+ return tokensAndKeysState.getSessionToken();
+ }
+ throw new InvalidFxAState("Cannot get sessionToken: not in a TokensAndKeysState state");
+ }
+
+ public static class InvalidFxAState extends Exception {
+ private static final long serialVersionUID = -8537626959811195978L;
+
+ public InvalidFxAState(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * <b>For debugging only!</b>
+ */
+ public void dump() {
+ if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) {
+ return;
+ }
+ ExtendedJSONObject o = toJSONObject();
+ ArrayList<String> list = new ArrayList<String>(o.keySet());
+ Collections.sort(list);
+ for (String key : list) {
+ FxAccountUtils.pii(LOG_TAG, key + ": " + o.get(key));
+ }
+ }
+
+ /**
+ * Return the Firefox Account's local email address.
+ * <p>
+ * It is important to note that this is the local email address, and not
+ * necessarily the normalized remote email address that the server expects.
+ *
+ * @return local email address.
+ */
+ public String getEmail() {
+ return account.name;
+ }
+
+ /**
+ * Return the Firefox Account's local email address, obfuscated.
+ * <p>
+ * Use this when logging.
+ *
+ * @return local email address, obfuscated.
+ */
+ public String getObfuscatedEmail() {
+ return Utils.obfuscateEmail(account.name);
+ }
+
+ /**
+ * Populate an intent used for starting FxAccountDeletedService service.
+ *
+ * @param intent Intent to populate with necessary extras
+ * @return <code>Intent</code> with a deleted action and account/OAuth information extras
+ */
+ public Intent populateDeletedAccountIntent(final Intent intent) {
+ final List<String> tokens = new ArrayList<>();
+
+ intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION_KEY,
+ Long.valueOf(FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION));
+ intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_KEY, account.name);
+ intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_PROFILE, getProfile());
+
+ // Get the tokens from AccountManager. Note: currently, only reading list service supports OAuth. The following logic will
+ // be extended in future to support OAuth for other services.
+ for (String tokenKey : KNOWN_OAUTH_TOKEN_TYPES) {
+ final String authToken = accountManager.peekAuthToken(account, tokenKey);
+ if (authToken != null) {
+ tokens.add(authToken);
+ }
+ }
+
+ // Update intent with tokens and service URI.
+ intent.putExtra(FxAccountConstants.ACCOUNT_OAUTH_SERVICE_ENDPOINT_KEY, getOAuthServerURI());
+ // Deleted broadcasts are package-private, so there's no security risk include the tokens in the extras
+ intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_AUTH_TOKENS, tokens.toArray(new String[tokens.size()]));
+ return intent;
+ }
+
+ /**
+ * Create an intent announcing that the profile JSON attached to this Firefox Account has been updated.
+ * <p>
+ * It is not guaranteed that the profile JSON has changed.
+ *
+ * @return <code>Intent</code> to broadcast.
+ */
+ private Intent makeProfileJSONUpdatedIntent() {
+ final Intent intent = new Intent();
+ intent.setAction(FxAccountConstants.ACCOUNT_PROFILE_JSON_UPDATED_ACTION);
+ return intent;
+ }
+
+ public void setLastSyncedTimestamp(long now) {
+ try {
+ getSyncPrefs().edit().putLong(PREF_KEY_LAST_SYNCED_TIMESTAMP, now).commit();
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Got exception setting last synced time; ignoring.", e);
+ }
+ }
+
+ public long getLastSyncedTimestamp() {
+ final long neverSynced = -1L;
+ try {
+ return getSyncPrefs().getLong(PREF_KEY_LAST_SYNCED_TIMESTAMP, neverSynced);
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Got exception getting last synced time; ignoring.", e);
+ return neverSynced;
+ }
+ }
+
+ // Debug only! This is dangerous!
+ public void unsafeTransitionToDefaultEndpoints() {
+ unsafeTransitionToStageEndpoints(
+ FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT,
+ FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT,
+ FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT);
+ }
+
+ // Debug only! This is dangerous!
+ public void unsafeTransitionToStageEndpoints() {
+ unsafeTransitionToStageEndpoints(
+ FxAccountConstants.STAGE_AUTH_SERVER_ENDPOINT,
+ FxAccountConstants.STAGE_TOKEN_SERVER_ENDPOINT,
+ FxAccountConstants.STAGE_PROFILE_SERVER_ENDPOINT);
+ }
+
+ protected void unsafeTransitionToStageEndpoints(String authServerEndpoint, String tokenServerEndpoint, String profileServerEndpoint) {
+ try {
+ getReadingListPrefs().edit().clear().commit();
+ } catch (UnsupportedEncodingException | GeneralSecurityException e) {
+ // Ignore.
+ }
+ try {
+ getSyncPrefs().edit().clear().commit();
+ } catch (UnsupportedEncodingException | GeneralSecurityException e) {
+ // Ignore.
+ }
+ State state = getState();
+ setState(state.makeSeparatedState());
+ accountManager.setUserData(account, ACCOUNT_KEY_IDP_SERVER, authServerEndpoint);
+ accountManager.setUserData(account, ACCOUNT_KEY_TOKEN_SERVER, tokenServerEndpoint);
+ accountManager.setUserData(account, ACCOUNT_KEY_PROFILE_SERVER, profileServerEndpoint);
+ ContentResolver.setIsSyncable(account, BrowserContract.READING_LIST_AUTHORITY, 1);
+ }
+
+ /**
+ * Returns the current profile JSON if available, or null.
+ *
+ * @return profile JSON object.
+ */
+ public ExtendedJSONObject getProfileJSON() {
+ final String profileString = getBundleData(BUNDLE_KEY_PROFILE_JSON);
+ if (profileString == null) {
+ return null;
+ }
+
+ try {
+ return new ExtendedJSONObject(profileString);
+ } catch (Exception e) {
+ Logger.error(LOG_TAG, "Failed to parse profile JSON; ignoring and returning null.", e);
+ }
+ return null;
+ }
+
+ /**
+ * Fetch the profile JSON associated to the underlying Firefox Account from the server and update the local store.
+ * <p>
+ * The LocalBroadcastManager is used to notify the receivers asynchronously after a successful fetch.
+ */
+ public void fetchProfileJSON() {
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ public void run() {
+ // Fetch profile information from server.
+ String authToken;
+ try {
+ authToken = accountManager.blockingGetAuthToken(account, AndroidFxAccount.PROFILE_OAUTH_TOKEN_TYPE, true);
+ if (authToken == null) {
+ throw new RuntimeException("Couldn't get oauth token! Aborting profile fetch.");
+ }
+ } catch (Exception e) {
+ Logger.error(LOG_TAG, "Error fetching profile information; ignoring.", e);
+ return;
+ }
+
+ Logger.info(LOG_TAG, "Intent service launched to fetch profile.");
+ final Intent intent = new Intent(context, FxAccountProfileService.class);
+ intent.putExtra(FxAccountProfileService.KEY_AUTH_TOKEN, authToken);
+ intent.putExtra(FxAccountProfileService.KEY_PROFILE_SERVER_URI, getProfileServerURI());
+ intent.putExtra(FxAccountProfileService.KEY_RESULT_RECEIVER, new ProfileResultReceiver(new Handler()));
+ context.startService(intent);
+ }
+ });
+ }
+
+ @Nullable
+ public synchronized String getDeviceId() {
+ return accountManager.getUserData(account, ACCOUNT_KEY_DEVICE_ID);
+ }
+
+ @NonNull
+ public synchronized int getDeviceRegistrationVersion() {
+ String versionStr = accountManager.getUserData(account, ACCOUNT_KEY_DEVICE_REGISTRATION_VERSION);
+ if (TextUtils.isEmpty(versionStr)) {
+ return 0;
+ } else {
+ try {
+ return Integer.parseInt(versionStr);
+ } catch (NumberFormatException ex) {
+ return 0;
+ }
+ }
+ }
+
+ public synchronized void setDeviceId(String id) {
+ accountManager.setUserData(account, ACCOUNT_KEY_DEVICE_ID, id);
+ }
+
+ public synchronized void setDeviceRegistrationVersion(int deviceRegistrationVersion) {
+ accountManager.setUserData(account, ACCOUNT_KEY_DEVICE_REGISTRATION_VERSION,
+ Integer.toString(deviceRegistrationVersion));
+ }
+
+ public synchronized void resetDeviceRegistrationVersion() {
+ setDeviceRegistrationVersion(0);
+ }
+
+ public synchronized void setFxAUserData(String id, int deviceRegistrationVersion) {
+ accountManager.setUserData(account, ACCOUNT_KEY_DEVICE_ID, id);
+ accountManager.setUserData(account, ACCOUNT_KEY_DEVICE_REGISTRATION_VERSION,
+ Integer.toString(deviceRegistrationVersion));
+ }
+
+ @SuppressLint("ParcelCreator") // The CREATOR field is defined in the super class.
+ private class ProfileResultReceiver extends ResultReceiver {
+ public ProfileResultReceiver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle bundle) {
+ super.onReceiveResult(resultCode, bundle);
+ switch (resultCode) {
+ case Activity.RESULT_OK:
+ final String resultData = bundle.getString(FxAccountProfileService.KEY_RESULT_STRING);
+ updateBundleValues(BUNDLE_KEY_PROFILE_JSON, resultData);
+ Logger.info(LOG_TAG, "Profile JSON fetch succeeeded!");
+ FxAccountUtils.pii(LOG_TAG, "Profile JSON fetch returned: " + resultData);
+ LocalBroadcastManager.getInstance(context).sendBroadcast(makeProfileJSONUpdatedIntent());
+ break;
+ case Activity.RESULT_CANCELED:
+ Logger.warn(LOG_TAG, "Failed to fetch profile JSON; ignoring.");
+ break;
+ default:
+ Logger.warn(LOG_TAG, "Invalid result code received; ignoring.");
+ break;
+ }
+ }
+ }
+
+ /**
+ * Take the lock to own updating any Firefox Account's internal state.
+ *
+ * We use a <code>Semaphore</code> rather than a <code>ReentrantLock</code>
+ * because the callback that needs to release the lock may not be invoked on
+ * the thread that initially acquired the lock. Be aware!
+ */
+ protected static final Semaphore sLock = new Semaphore(1, true /* fair */);
+
+ // Which consumer took the lock?
+ // Synchronized by this.
+ protected String lockTag = null;
+
+ // Are we locked? (It's not easy to determine who took the lock dynamically,
+ // so we maintain this flag internally.)
+ // Synchronized by this.
+ protected boolean locked = false;
+
+ // Block until we can take the shared state lock.
+ public synchronized void acquireSharedAccountStateLock(final String tag) throws InterruptedException {
+ final long id = Thread.currentThread().getId();
+ this.lockTag = tag;
+ Log.d(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id acquiring lock: " + lockTag + ", " + id + " ...");
+ sLock.acquire();
+ locked = true;
+ Log.d(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id acquiring lock: " + lockTag + ", " + id + " ... ACQUIRED");
+ }
+
+ // If we hold the shared state lock, release it. Otherwise, ignore the request.
+ public synchronized void releaseSharedAccountStateLock() {
+ final long id = Thread.currentThread().getId();
+ Log.d(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id releasing lock: " + lockTag + ", " + id + " ...");
+ if (locked) {
+ sLock.release();
+ locked = false;
+ Log.d(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id releasing lock: " + lockTag + ", " + id + " ... RELEASED");
+ } else {
+ Log.d(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id releasing lock: " + lockTag + ", " + id + " ... NOT LOCKED");
+ }
+ }
+
+ @Override
+ protected synchronized void finalize() {
+ if (locked) {
+ // Should never happen, but...
+ sLock.release();
+ locked = false;
+ final long id = Thread.currentThread().getId();
+ Log.e(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id releasing lock: " + lockTag + ", " + id + " ... RELEASED DURING FINALIZE");
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxADefaultLoginStateMachineDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxADefaultLoginStateMachineDelegate.java
new file mode 100644
index 0000000000..ff31223224
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxADefaultLoginStateMachineDelegate.java
@@ -0,0 +1,84 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.authenticator;
+
+import java.security.NoSuchAlgorithmException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.FxAccountClient;
+import org.mozilla.gecko.background.fxa.FxAccountClient20;
+import org.mozilla.gecko.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
+import org.mozilla.gecko.fxa.login.Married;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.fxa.login.State.StateLabel;
+import org.mozilla.gecko.fxa.login.StateFactory;
+import org.mozilla.gecko.fxa.sync.FxAccountNotificationManager;
+import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter;
+
+import android.content.Context;
+
+public abstract class FxADefaultLoginStateMachineDelegate implements LoginStateMachineDelegate {
+ protected final static String LOG_TAG = LoginStateMachineDelegate.class.getSimpleName();
+
+ protected final Context context;
+ protected final AndroidFxAccount fxAccount;
+ protected final Executor executor;
+ protected final FxAccountClient client;
+
+ public FxADefaultLoginStateMachineDelegate(Context context, AndroidFxAccount fxAccount) {
+ this.context = context;
+ this.fxAccount = fxAccount;
+ this.executor = Executors.newSingleThreadExecutor();
+ this.client = new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
+ }
+
+ abstract public void handleNotMarried(State notMarried);
+ abstract public void handleMarried(Married married);
+
+ @Override
+ public FxAccountClient getClient() {
+ return client;
+ }
+
+ @Override
+ public long getCertificateDurationInMilliseconds() {
+ return 12 * 60 * 60 * 1000;
+ }
+
+ @Override
+ public long getAssertionDurationInMilliseconds() {
+ return 15 * 60 * 1000;
+ }
+
+ @Override
+ public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
+ return StateFactory.generateKeyPair();
+ }
+
+ @Override
+ public void handleTransition(Transition transition, State state) {
+ Logger.info(LOG_TAG, "handleTransition: " + transition + " to " + state.getStateLabel());
+ }
+
+ @Override
+ public void handleFinal(State state) {
+ Logger.info(LOG_TAG, "handleFinal: in " + state.getStateLabel());
+ fxAccount.setState(state);
+ // Update any notifications displayed.
+ final FxAccountNotificationManager notificationManager = new FxAccountNotificationManager(FxAccountSyncAdapter.NOTIFICATION_ID);
+ notificationManager.update(context, fxAccount);
+
+ if (state.getStateLabel() != StateLabel.Married) {
+ handleNotMarried(state);
+ return;
+ } else {
+ handleMarried((Married) state);
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountAuthenticator.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountAuthenticator.java
new file mode 100644
index 0000000000..259b1cb887
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountAuthenticator.java
@@ -0,0 +1,385 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.authenticator;
+
+import android.accounts.AbstractAccountAuthenticator;
+import android.accounts.Account;
+import android.accounts.AccountAuthenticatorResponse;
+import android.accounts.AccountManager;
+import android.accounts.NetworkErrorException;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.FxAccountClient;
+import org.mozilla.gecko.background.fxa.FxAccountClient20;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient.RequestDelegate;
+import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException;
+import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10;
+import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10.AuthorizationResponse;
+import org.mozilla.gecko.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.browserid.JSONWebTokenUtils;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine;
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
+import org.mozilla.gecko.fxa.login.Married;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.fxa.login.State.StateLabel;
+import org.mozilla.gecko.fxa.login.StateFactory;
+import org.mozilla.gecko.fxa.receivers.FxAccountDeletedService;
+import org.mozilla.gecko.fxa.sync.FxAccountNotificationManager;
+import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.security.NoSuchAlgorithmException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class FxAccountAuthenticator extends AbstractAccountAuthenticator {
+ public static final String LOG_TAG = FxAccountAuthenticator.class.getSimpleName();
+ public static final int UNKNOWN_ERROR_CODE = 999;
+
+ protected final Context context;
+ protected final AccountManager accountManager;
+
+ public FxAccountAuthenticator(Context context) {
+ super(context);
+ this.context = context;
+ this.accountManager = AccountManager.get(context);
+ }
+
+ @Override
+ public Bundle addAccount(AccountAuthenticatorResponse response,
+ String accountType, String authTokenType, String[] requiredFeatures,
+ Bundle options)
+ throws NetworkErrorException {
+ Logger.debug(LOG_TAG, "addAccount");
+
+ // The data associated to each Account should be invalidated when we change
+ // the set of Firefox Accounts on the system.
+ AndroidFxAccount.invalidateCaches();
+
+ final Bundle res = new Bundle();
+
+ if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) {
+ res.putInt(AccountManager.KEY_ERROR_CODE, -1);
+ res.putString(AccountManager.KEY_ERROR_MESSAGE, "Not adding unknown account type.");
+ return res;
+ }
+
+ final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
+ res.putParcelable(AccountManager.KEY_INTENT, intent);
+ return res;
+ }
+
+ @Override
+ public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options)
+ throws NetworkErrorException {
+ Logger.debug(LOG_TAG, "confirmCredentials");
+
+ return null;
+ }
+
+ @Override
+ public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
+ Logger.debug(LOG_TAG, "editProperties");
+
+ return null;
+ }
+
+ protected static class Responder {
+ final AccountAuthenticatorResponse response;
+ final AndroidFxAccount fxAccount;
+
+ public Responder(AccountAuthenticatorResponse response, AndroidFxAccount fxAccount) {
+ this.response = response;
+ this.fxAccount = fxAccount;
+ }
+
+ public void fail(Exception e) {
+ Logger.warn(LOG_TAG, "Responding with error!", e);
+ fxAccount.releaseSharedAccountStateLock();
+ final Bundle result = new Bundle();
+ result.putInt(AccountManager.KEY_ERROR_CODE, UNKNOWN_ERROR_CODE);
+ result.putString(AccountManager.KEY_ERROR_MESSAGE, e.toString());
+ response.onResult(result);
+ }
+
+ public void succeed(String authToken) {
+ Logger.info(LOG_TAG, "Responding with success!");
+ fxAccount.releaseSharedAccountStateLock();
+ final Bundle result = new Bundle();
+ result.putString(AccountManager.KEY_ACCOUNT_NAME, fxAccount.account.name);
+ result.putString(AccountManager.KEY_ACCOUNT_TYPE, fxAccount.account.type);
+ result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
+ response.onResult(result);
+ }
+ }
+
+ public abstract static class FxADefaultLoginStateMachineDelegate implements LoginStateMachineDelegate {
+ protected final Context context;
+ protected final AndroidFxAccount fxAccount;
+ protected final Executor executor;
+ protected final FxAccountClient client;
+
+ public FxADefaultLoginStateMachineDelegate(Context context, AndroidFxAccount fxAccount) {
+ this.context = context;
+ this.fxAccount = fxAccount;
+ this.executor = Executors.newSingleThreadExecutor();
+ this.client = new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
+ }
+
+ @Override
+ public FxAccountClient getClient() {
+ return client;
+ }
+
+ @Override
+ public long getCertificateDurationInMilliseconds() {
+ return 12 * 60 * 60 * 1000;
+ }
+
+ @Override
+ public long getAssertionDurationInMilliseconds() {
+ return 15 * 60 * 1000;
+ }
+
+ @Override
+ public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
+ return StateFactory.generateKeyPair();
+ }
+
+ @Override
+ public void handleTransition(Transition transition, State state) {
+ Logger.info(LOG_TAG, "handleTransition: " + transition + " to " + state.getStateLabel());
+ }
+
+ abstract public void handleNotMarried(State notMarried);
+ abstract public void handleMarried(Married married);
+
+ @Override
+ public void handleFinal(State state) {
+ Logger.info(LOG_TAG, "handleFinal: in " + state.getStateLabel());
+ fxAccount.setState(state);
+ // Update any notifications displayed.
+ final FxAccountNotificationManager notificationManager = new FxAccountNotificationManager(FxAccountSyncAdapter.NOTIFICATION_ID);
+ notificationManager.update(context, fxAccount);
+
+ if (state.getStateLabel() != StateLabel.Married) {
+ handleNotMarried(state);
+ return;
+ } else {
+ handleMarried((Married) state);
+ }
+ }
+ }
+
+ protected void getOAuthToken(final AccountAuthenticatorResponse response, final AndroidFxAccount fxAccount, final String scope) throws NetworkErrorException {
+ Logger.info(LOG_TAG, "Fetching oauth token with scope: " + scope);
+
+ final Responder responder = new Responder(response, fxAccount);
+ final String oauthServerUri = fxAccount.getOAuthServerURI();
+
+ final String audience;
+ try {
+ audience = FxAccountUtils.getAudienceForURL(oauthServerUri); // The assertion gets traded in for an oauth bearer token.
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Got exception fetching oauth token.", e);
+ responder.fail(e);
+ return;
+ }
+
+ final FxAccountLoginStateMachine stateMachine = new FxAccountLoginStateMachine();
+
+ stateMachine.advance(fxAccount.getState(), StateLabel.Married, new FxADefaultLoginStateMachineDelegate(context, fxAccount) {
+ @Override
+ public void handleNotMarried(State state) {
+ final String message = "Cannot fetch oauth token from state: " + state.getStateLabel();
+ Logger.warn(LOG_TAG, message);
+ responder.fail(new RuntimeException(message));
+ }
+
+ @Override
+ public void handleMarried(final Married married) {
+ final String assertion;
+ try {
+ assertion = married.generateAssertion(audience, JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER);
+ if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
+ JSONWebTokenUtils.dumpAssertion(assertion);
+ }
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Got exception fetching oauth token.", e);
+ responder.fail(e);
+ return;
+ }
+
+ final FxAccountOAuthClient10 oauthClient = new FxAccountOAuthClient10(oauthServerUri, executor);
+ Logger.debug(LOG_TAG, "OAuth fetch for scope: " + scope);
+ oauthClient.authorization(FxAccountConstants.OAUTH_CLIENT_ID_FENNEC, assertion, null, scope, new RequestDelegate<FxAccountOAuthClient10.AuthorizationResponse>() {
+ @Override
+ public void handleSuccess(AuthorizationResponse result) {
+ Logger.debug(LOG_TAG, "OAuth success.");
+ FxAccountUtils.pii(LOG_TAG, "Fetched oauth token: " + result.access_token);
+ responder.succeed(result.access_token);
+ }
+
+ @Override
+ public void handleFailure(FxAccountAbstractClientRemoteException e) {
+ Logger.error(LOG_TAG, "OAuth failure.", e);
+ if (e.isInvalidAuthentication()) {
+ // We were married, generated an assertion, and our assertion was rejected by the
+ // oauth client. If it's a 401, we probably have a stale certificate. If instead of
+ // a stale certificate we have bad credentials, the state machine will fail to sign
+ // our public key and drive us back to Separated.
+ fxAccount.setState(married.makeCohabitingState());
+ }
+ responder.fail(e);
+ }
+
+ @Override
+ public void handleError(Exception e) {
+ Logger.error(LOG_TAG, "OAuth error.", e);
+ responder.fail(e);
+ }
+ });
+ }
+ });
+ }
+
+ @Override
+ public Bundle getAuthToken(final AccountAuthenticatorResponse response,
+ final Account account, final String authTokenType, final Bundle options)
+ throws NetworkErrorException {
+ Logger.debug(LOG_TAG, "getAuthToken: " + authTokenType);
+
+ // If we have a cached authToken, hand it over.
+ final String cachedAuthToken = AccountManager.get(context).peekAuthToken(account, authTokenType);
+ if (cachedAuthToken != null && !cachedAuthToken.isEmpty()) {
+ Logger.info(LOG_TAG, "Return cached token.");
+ final Bundle result = new Bundle();
+ result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
+ result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
+ result.putString(AccountManager.KEY_AUTHTOKEN, cachedAuthToken);
+ return result;
+ }
+
+ // If we're asked for an oauth::scope token, try to generate one.
+ final String oauthPrefix = "oauth::";
+ if (authTokenType != null && authTokenType.startsWith(oauthPrefix)) {
+ final String scope = authTokenType.substring(oauthPrefix.length());
+ final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
+ try {
+ fxAccount.acquireSharedAccountStateLock(LOG_TAG);
+ } catch (InterruptedException e) {
+ Logger.warn(LOG_TAG, "Could not acquire account state lock; return error bundle.");
+ final Bundle bundle = new Bundle();
+ bundle.putInt(AccountManager.KEY_ERROR_CODE, 1);
+ bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "Could not acquire account state lock.");
+ return bundle;
+ }
+ getOAuthToken(response, fxAccount, scope);
+ return null;
+ }
+
+ // Otherwise, fail.
+ Logger.warn(LOG_TAG, "Returning error bundle for getAuthToken with unknown token type.");
+ final Bundle bundle = new Bundle();
+ bundle.putInt(AccountManager.KEY_ERROR_CODE, 2);
+ bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "Unknown token type: " + authTokenType);
+ return bundle;
+ }
+
+ @Override
+ public String getAuthTokenLabel(String authTokenType) {
+ Logger.debug(LOG_TAG, "getAuthTokenLabel");
+
+ return null;
+ }
+
+ @Override
+ public Bundle hasFeatures(AccountAuthenticatorResponse response,
+ Account account, String[] features) throws NetworkErrorException {
+ Logger.debug(LOG_TAG, "hasFeatures");
+
+ return null;
+ }
+
+ @Override
+ public Bundle updateCredentials(AccountAuthenticatorResponse response,
+ Account account, String authTokenType, Bundle options)
+ throws NetworkErrorException {
+ Logger.debug(LOG_TAG, "updateCredentials");
+
+ return null;
+ }
+
+ /**
+ * If the account is going to be removed, broadcast an "account deleted"
+ * intent. This allows us to clean up the account.
+ * <p>
+ * It is preferable to receive Android's LOGIN_ACCOUNTS_CHANGED_ACTION broadcast
+ * than to create our own hacky broadcast here, but that doesn't include enough
+ * information about which Accounts changed to correctly identify whether a Sync
+ * account has been removed (when some Firefox channels are installed on the SD
+ * card). We can work around this by storing additional state but it's both messy
+ * and expensive because the broadcast is noisy.
+ * <p>
+ * Note that this is <b>not</b> called when an Android Account is blown away
+ * due to the SD card being unmounted.
+ */
+ @Override
+ public Bundle getAccountRemovalAllowed(final AccountAuthenticatorResponse response, Account account)
+ throws NetworkErrorException {
+ Bundle result = super.getAccountRemovalAllowed(response, account);
+
+ if (result == null ||
+ !result.containsKey(AccountManager.KEY_BOOLEAN_RESULT) ||
+ result.containsKey(AccountManager.KEY_INTENT)) {
+ return result;
+ }
+
+ final boolean removalAllowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT);
+ if (!removalAllowed) {
+ return result;
+ }
+
+ // Broadcast a message to all Firefox channels sharing this Android
+ // Account type telling that this Firefox account has been deleted.
+ //
+ // Broadcast intents protected with permissions are secure, so it's okay
+ // to include private information such as a password.
+ final AndroidFxAccount androidFxAccount = new AndroidFxAccount(context, account);
+
+ // Deleting the pickle file in a blocking manner will avoid race conditions that might happen when
+ // an account is unpickled while an FxAccount is being deleted.
+ // Also we have an assumption that this method is always called from a background thread, so we delete
+ // the pickle file directly without being afraid from a StrictMode violation.
+ ThreadUtils.assertNotOnUiThread();
+
+ final Intent serviceIntent = androidFxAccount.populateDeletedAccountIntent(
+ new Intent(context, FxAccountDeletedService.class)
+ );
+ Logger.info(LOG_TAG, "Account named " + account.name + " being removed; " +
+ "starting FxAccountDeletedService with action: " + serviceIntent.getAction() + ".");
+ context.startService(serviceIntent);
+
+ Logger.info(LOG_TAG, "Firefox account named " + account.name + " being removed; " +
+ "deleting saved pickle file '" + FxAccountConstants.ACCOUNT_PICKLE_FILENAME + "'.");
+ deletePickle();
+
+ return result;
+ }
+
+ private void deletePickle() {
+ try {
+ AccountPickler.deletePickle(context, FxAccountConstants.ACCOUNT_PICKLE_FILENAME);
+ } catch (Exception e) {
+ // This should never happen, but we really don't want to die in a background thread.
+ Logger.warn(LOG_TAG, "Got exception deleting saved pickle file; ignoring.", e);
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountAuthenticatorService.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountAuthenticatorService.java
new file mode 100644
index 0000000000..d138e6c45a
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountAuthenticatorService.java
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.authenticator;
+
+import org.mozilla.gecko.background.common.log.Logger;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+public class FxAccountAuthenticatorService extends Service {
+ public static final String LOG_TAG = FxAccountAuthenticatorService.class.getSimpleName();
+
+ // Lazily initialized by <code>getAuthenticator</code>.
+ protected FxAccountAuthenticator accountAuthenticator;
+
+ protected synchronized FxAccountAuthenticator getAuthenticator() {
+ if (accountAuthenticator == null) {
+ accountAuthenticator = new FxAccountAuthenticator(this);
+ }
+
+ return accountAuthenticator;
+ }
+
+ @Override
+ public void onCreate() {
+ Logger.debug(LOG_TAG, "onCreate");
+
+ accountAuthenticator = getAuthenticator();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ Logger.debug(LOG_TAG, "onBind");
+
+ if (intent == null) {
+ // Should never happen, but can -- Bug 1025937.
+ return null;
+ }
+
+ if (!android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT.equals(intent.getAction())) {
+ return null;
+ }
+
+ final FxAccountAuthenticator authenticator = getAuthenticator();
+ if (authenticator == null) {
+ // Should never happen.
+ return null;
+ }
+
+ return authenticator.getIBinder();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountLoginDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountLoginDelegate.java
new file mode 100644
index 0000000000..71006e79d0
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountLoginDelegate.java
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.authenticator;
+
+/**
+ * Abstraction around things that might need to be signalled to the user via UI,
+ * such as:
+ * <ul>
+ * <li>account not yet verified;</li>
+ * <li>account password needs to be updated;</li>
+ * <li>account key management required or changed;</li>
+ * <li>auth protocol has changed and Firefox needs to be upgraded;</li>
+ * </ul>
+ * etc.
+ * <p>
+ * Consumers of this code should differentiate error classes based on the types
+ * of the exceptions thrown. Exceptions that do not have special meaning are of
+ * type <code>FxAccountLoginException</code> with an appropriate
+ * <code>cause</code> inner exception.
+ */
+public interface FxAccountLoginDelegate {
+ public void handleError(FxAccountLoginException e);
+ public void handleSuccess(String assertion);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountLoginException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountLoginException.java
new file mode 100644
index 0000000000..56c0140b21
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountLoginException.java
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.authenticator;
+
+public class FxAccountLoginException extends Exception {
+ public FxAccountLoginException(String string) {
+ super(string);
+ }
+
+ public FxAccountLoginException(Exception e) {
+ super(e);
+ }
+
+ private static final long serialVersionUID = 397685959625820798L;
+
+ public static class FxAccountLoginBadPasswordException extends FxAccountLoginException {
+ public FxAccountLoginBadPasswordException(String string) {
+ super(string);
+ }
+
+ private static final long serialVersionUID = 397685959625820799L;
+ }
+
+ public static class FxAccountLoginAccountNotVerifiedException extends FxAccountLoginException {
+ public FxAccountLoginAccountNotVerifiedException(String string) {
+ super(string);
+ }
+
+ private static final long serialVersionUID = 397685959625820800L;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/BaseRequestDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/BaseRequestDelegate.java
new file mode 100644
index 0000000000..5d3e71ece0
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/BaseRequestDelegate.java
@@ -0,0 +1,49 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.login;
+
+import org.mozilla.gecko.background.fxa.FxAccountClient20;
+import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.AccountNeedsVerification;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LocalError;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.RemoteError;
+
+public abstract class BaseRequestDelegate<T> implements FxAccountClient20.RequestDelegate<T> {
+ protected final ExecuteDelegate delegate;
+ protected final State state;
+
+ public BaseRequestDelegate(State state, ExecuteDelegate delegate) {
+ this.delegate = delegate;
+ this.state = state;
+ }
+
+ @Override
+ public void handleFailure(FxAccountClientRemoteException e) {
+ // Order matters here: we don't want to ignore upgrade required responses
+ // even if the server tells us something else as well. We don't go directly
+ // to the Doghouse on upgrade required; we want the user to try to update
+ // their credentials, and then display UI telling them they need to upgrade.
+ // Then they go to the Doghouse.
+ if (e.isUpgradeRequired()) {
+ delegate.handleTransition(new RemoteError(e), new Separated(state.email, state.uid, state.verified));
+ return;
+ }
+ if (e.isInvalidAuthentication()) {
+ delegate.handleTransition(new RemoteError(e), new Separated(state.email, state.uid, state.verified));
+ return;
+ }
+ if (e.isUnverified()) {
+ delegate.handleTransition(new AccountNeedsVerification(), state);
+ return;
+ }
+ delegate.handleTransition(new RemoteError(e), state);
+ }
+
+ @Override
+ public void handleError(Exception e) {
+ delegate.handleTransition(new LocalError(e), state);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Cohabiting.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Cohabiting.java
new file mode 100644
index 0000000000..dd3477a79e
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Cohabiting.java
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.login;
+
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.browserid.JSONWebTokenUtils;
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+public class Cohabiting extends TokensAndKeysState {
+ private static final String LOG_TAG = Cohabiting.class.getSimpleName();
+
+ public Cohabiting(String email, String uid, byte[] sessionToken, byte[] kA, byte[] kB, BrowserIDKeyPair keyPair) {
+ super(StateLabel.Cohabiting, email, uid, sessionToken, kA, kB, keyPair);
+ }
+
+ public Married withCertificate(String certificate) {
+ return new Married(email, uid, sessionToken, kA, kB, keyPair, certificate);
+ }
+
+ @Override
+ public void execute(final ExecuteDelegate delegate) {
+ delegate.getClient().sign(sessionToken, keyPair.getPublic().toJSONObject(), delegate.getCertificateDurationInMilliseconds(),
+ new BaseRequestDelegate<String>(this, delegate) {
+ @Override
+ public void handleSuccess(String certificate) {
+ if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
+ try {
+ FxAccountUtils.pii(LOG_TAG, "Fetched certificate: " + certificate);
+ ExtendedJSONObject c = JSONWebTokenUtils.parseCertificate(certificate);
+ if (c != null) {
+ FxAccountUtils.pii(LOG_TAG, "Header : " + c.getObject("header"));
+ FxAccountUtils.pii(LOG_TAG, "Payload : " + c.getObject("payload"));
+ FxAccountUtils.pii(LOG_TAG, "Signature: " + c.getString("signature"));
+ } else {
+ FxAccountUtils.pii(LOG_TAG, "Could not parse certificate!");
+ }
+ } catch (Exception e) {
+ FxAccountUtils.pii(LOG_TAG, "Could not parse certificate!");
+ }
+ }
+ delegate.handleTransition(new LogMessage("sign succeeded"), withCertificate(certificate));
+ }
+ });
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Doghouse.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Doghouse.java
new file mode 100644
index 0000000000..57600577db
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Doghouse.java
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.login;
+
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
+
+
+public class Doghouse extends State {
+ public Doghouse(String email, String uid, boolean verified) {
+ super(StateLabel.Doghouse, email, uid, verified);
+ }
+
+ @Override
+ public void execute(final ExecuteDelegate delegate) {
+ delegate.handleTransition(new LogMessage("Upgraded Firefox clients might know what to do here."), this);
+ }
+
+ @Override
+ public Action getNeededAction() {
+ return Action.NeedsUpgrade;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Engaged.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Engaged.java
new file mode 100644
index 0000000000..f192cb58ba
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Engaged.java
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.login;
+
+import java.security.NoSuchAlgorithmException;
+
+import org.mozilla.gecko.background.fxa.FxAccountClient20.TwoKeys;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.AccountVerified;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LocalError;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.RemoteError;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.Utils;
+
+public class Engaged extends State {
+ private static final String LOG_TAG = Engaged.class.getSimpleName();
+
+ protected final byte[] sessionToken;
+ protected final byte[] keyFetchToken;
+ protected final byte[] unwrapkB;
+
+ public Engaged(String email, String uid, boolean verified, byte[] unwrapkB, byte[] sessionToken, byte[] keyFetchToken) {
+ super(StateLabel.Engaged, email, uid, verified);
+ Utils.throwIfNull(unwrapkB, sessionToken, keyFetchToken);
+ this.unwrapkB = unwrapkB;
+ this.sessionToken = sessionToken;
+ this.keyFetchToken = keyFetchToken;
+ }
+
+ @Override
+ public ExtendedJSONObject toJSONObject() {
+ ExtendedJSONObject o = super.toJSONObject();
+ // Fields are non-null by constructor.
+ o.put("unwrapkB", Utils.byte2Hex(unwrapkB));
+ o.put("sessionToken", Utils.byte2Hex(sessionToken));
+ o.put("keyFetchToken", Utils.byte2Hex(keyFetchToken));
+ return o;
+ }
+
+ @Override
+ public void execute(final ExecuteDelegate delegate) {
+ BrowserIDKeyPair theKeyPair;
+ try {
+ theKeyPair = delegate.generateKeyPair();
+ } catch (NoSuchAlgorithmException e) {
+ delegate.handleTransition(new LocalError(e), new Doghouse(email, uid, verified));
+ return;
+ }
+ final BrowserIDKeyPair keyPair = theKeyPair;
+
+ delegate.getClient().keys(keyFetchToken, new BaseRequestDelegate<TwoKeys>(this, delegate) {
+ @Override
+ public void handleSuccess(TwoKeys result) {
+ byte[] kB;
+ try {
+ kB = FxAccountUtils.unwrapkB(unwrapkB, result.wrapkB);
+ if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
+ FxAccountUtils.pii(LOG_TAG, "Fetched kA: " + Utils.byte2Hex(result.kA));
+ FxAccountUtils.pii(LOG_TAG, "And wrapkB: " + Utils.byte2Hex(result.wrapkB));
+ FxAccountUtils.pii(LOG_TAG, "Giving kB : " + Utils.byte2Hex(kB));
+ }
+ } catch (Exception e) {
+ delegate.handleTransition(new RemoteError(e), new Separated(email, uid, verified));
+ return;
+ }
+ Transition transition = verified
+ ? new LogMessage("keys succeeded")
+ : new AccountVerified();
+ delegate.handleTransition(transition, new Cohabiting(email, uid, sessionToken, result.kA, kB, keyPair));
+ }
+ });
+ }
+
+ @Override
+ public Action getNeededAction() {
+ if (!verified) {
+ return Action.NeedsVerification;
+ }
+ return Action.None;
+ }
+
+ public byte[] getSessionToken() {
+ return sessionToken;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/FxAccountLoginStateMachine.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/FxAccountLoginStateMachine.java
new file mode 100644
index 0000000000..34e5075419
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/FxAccountLoginStateMachine.java
@@ -0,0 +1,84 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.login;
+
+import java.security.NoSuchAlgorithmException;
+import java.util.EnumSet;
+import java.util.Set;
+
+import org.mozilla.gecko.background.fxa.FxAccountClient;
+import org.mozilla.gecko.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
+import org.mozilla.gecko.fxa.login.State.StateLabel;
+
+public class FxAccountLoginStateMachine {
+ public static final String LOG_TAG = FxAccountLoginStateMachine.class.getSimpleName();
+
+ public interface LoginStateMachineDelegate {
+ public FxAccountClient getClient();
+ public long getCertificateDurationInMilliseconds();
+ public long getAssertionDurationInMilliseconds();
+ public void handleTransition(Transition transition, State state);
+ public void handleFinal(State state);
+ public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException;
+ }
+
+ public static class ExecuteDelegate {
+ protected final LoginStateMachineDelegate delegate;
+ protected final StateLabel desiredStateLabel;
+ // It's as difficult to detect arbitrary cycles as repeated states.
+ protected final Set<StateLabel> stateLabelsSeen = EnumSet.noneOf(StateLabel.class);
+
+ protected ExecuteDelegate(StateLabel initialStateLabel, StateLabel desiredStateLabel, LoginStateMachineDelegate delegate) {
+ this.delegate = delegate;
+ this.desiredStateLabel = desiredStateLabel;
+ this.stateLabelsSeen.add(initialStateLabel);
+ }
+
+ public FxAccountClient getClient() {
+ return delegate.getClient();
+ }
+
+ public long getCertificateDurationInMilliseconds() {
+ return delegate.getCertificateDurationInMilliseconds();
+ }
+
+ public long getAssertionDurationInMilliseconds() {
+ return delegate.getAssertionDurationInMilliseconds();
+ }
+
+ public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
+ return delegate.generateKeyPair();
+ }
+
+ public void handleTransition(Transition transition, State state) {
+ // Always trigger the transition callback.
+ delegate.handleTransition(transition, state);
+
+ // Possibly trigger the final callback. We trigger if we're at our desired
+ // state, or if we've seen this state before.
+ StateLabel stateLabel = state.getStateLabel();
+ if (stateLabel == desiredStateLabel || stateLabelsSeen.contains(stateLabel)) {
+ delegate.handleFinal(state);
+ return;
+ }
+
+ // If this wasn't the last state, leave a bread crumb and move on to the
+ // next state.
+ stateLabelsSeen.add(stateLabel);
+ state.execute(this);
+ }
+ }
+
+ public void advance(State initialState, final StateLabel desiredStateLabel, final LoginStateMachineDelegate delegate) {
+ if (initialState.getStateLabel() == desiredStateLabel) {
+ // We're already where we want to be!
+ delegate.handleFinal(initialState);
+ return;
+ }
+ ExecuteDelegate executeDelegate = new ExecuteDelegate(initialState.getStateLabel(), desiredStateLabel, delegate);
+ initialState.execute(executeDelegate);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/FxAccountLoginTransition.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/FxAccountLoginTransition.java
new file mode 100644
index 0000000000..6832178533
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/FxAccountLoginTransition.java
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.login;
+
+
+public class FxAccountLoginTransition {
+ public interface Transition {
+ }
+
+ public static class LogMessage implements Transition {
+ public final String detailMessage;
+
+ public LogMessage(String detailMessage) {
+ this.detailMessage = detailMessage;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + (this.detailMessage == null ? "" : "('" + this.detailMessage + "')");
+ }
+ }
+
+ public static class AccountNeedsVerification extends LogMessage {
+ public AccountNeedsVerification() {
+ super(null);
+ }
+ }
+
+ public static class AccountVerified extends LogMessage {
+ public AccountVerified() {
+ super(null);
+ }
+ }
+
+ public static class PasswordRequired extends LogMessage {
+ public PasswordRequired() {
+ super(null);
+ }
+ }
+
+ public static class LocalError implements Transition {
+ public final Exception e;
+
+ public LocalError(Exception e) {
+ this.e = e;
+ }
+
+ @Override
+ public String toString() {
+ return "Log(" + this.e + ")";
+ }
+ }
+
+ public static class RemoteError implements Transition {
+ public final Exception e;
+
+ public RemoteError(Exception e) {
+ this.e = e;
+ }
+
+ @Override
+ public String toString() {
+ return "Log(" + (this.e == null ? "null" : this.e) + ")";
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Married.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Married.java
new file mode 100644
index 0000000000..1ec7b4051b
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Married.java
@@ -0,0 +1,117 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.login;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.browserid.JSONWebTokenUtils;
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+
+public class Married extends TokensAndKeysState {
+ private static final String LOG_TAG = Married.class.getSimpleName();
+
+ protected final String certificate;
+ protected final String clientState;
+
+ public Married(String email, String uid, byte[] sessionToken, byte[] kA, byte[] kB, BrowserIDKeyPair keyPair, String certificate) {
+ super(StateLabel.Married, email, uid, sessionToken, kA, kB, keyPair);
+ Utils.throwIfNull(certificate);
+ this.certificate = certificate;
+ try {
+ this.clientState = FxAccountUtils.computeClientState(kB);
+ } catch (NoSuchAlgorithmException e) {
+ // This should never occur.
+ throw new IllegalStateException("Unable to compute client state from kB.");
+ }
+ }
+
+ @Override
+ public ExtendedJSONObject toJSONObject() {
+ ExtendedJSONObject o = super.toJSONObject();
+ // Fields are non-null by constructor.
+ o.put("certificate", certificate);
+ return o;
+ }
+
+ @Override
+ public void execute(final ExecuteDelegate delegate) {
+ delegate.handleTransition(new LogMessage("staying married"), this);
+ }
+
+ public String generateAssertion(String audience, String issuer) throws NonObjectJSONException, IOException, GeneralSecurityException {
+ // We generate assertions with no iat and an exp after 2050 to avoid
+ // invalid-timestamp errors from the token server.
+ final long expiresAt = JSONWebTokenUtils.DEFAULT_FUTURE_EXPIRES_AT_IN_MILLISECONDS;
+ String assertion = JSONWebTokenUtils.createAssertion(keyPair.getPrivate(), certificate, audience, issuer, null, expiresAt);
+ if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) {
+ return assertion;
+ }
+
+ try {
+ FxAccountUtils.pii(LOG_TAG, "Generated assertion: " + assertion);
+ ExtendedJSONObject a = JSONWebTokenUtils.parseAssertion(assertion);
+ if (a != null) {
+ FxAccountUtils.pii(LOG_TAG, "aHeader : " + a.getObject("header"));
+ FxAccountUtils.pii(LOG_TAG, "aPayload : " + a.getObject("payload"));
+ FxAccountUtils.pii(LOG_TAG, "aSignature: " + a.getString("signature"));
+ String certificate = a.getString("certificate");
+ if (certificate != null) {
+ ExtendedJSONObject c = JSONWebTokenUtils.parseCertificate(certificate);
+ FxAccountUtils.pii(LOG_TAG, "cHeader : " + c.getObject("header"));
+ FxAccountUtils.pii(LOG_TAG, "cPayload : " + c.getObject("payload"));
+ FxAccountUtils.pii(LOG_TAG, "cSignature: " + c.getString("signature"));
+ // Print the relevant timestamps in sorted order with labels.
+ HashMap<Long, String> map = new HashMap<Long, String>();
+ map.put(a.getObject("payload").getLong("iat"), "aiat");
+ map.put(a.getObject("payload").getLong("exp"), "aexp");
+ map.put(c.getObject("payload").getLong("iat"), "ciat");
+ map.put(c.getObject("payload").getLong("exp"), "cexp");
+ ArrayList<Long> values = new ArrayList<Long>(map.keySet());
+ Collections.sort(values);
+ for (Long value : values) {
+ FxAccountUtils.pii(LOG_TAG, map.get(value) + ": " + value);
+ }
+ } else {
+ FxAccountUtils.pii(LOG_TAG, "Could not parse certificate!");
+ }
+ } else {
+ FxAccountUtils.pii(LOG_TAG, "Could not parse assertion!");
+ }
+ } catch (Exception e) {
+ FxAccountUtils.pii(LOG_TAG, "Got exception dumping assertion debug info.");
+ }
+ return assertion;
+ }
+
+ public KeyBundle getSyncKeyBundle() throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
+ // TODO Document this choice for deriving from kB.
+ return FxAccountUtils.generateSyncKeyBundle(kB);
+ }
+
+ public String getClientState() {
+ if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
+ FxAccountUtils.pii(LOG_TAG, "Client state: " + this.clientState);
+ }
+ return this.clientState;
+ }
+
+ public Cohabiting makeCohabitingState() {
+ return new Cohabiting(email, uid, sessionToken, kA, kB, keyPair);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/MigratedFromSync11.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/MigratedFromSync11.java
new file mode 100644
index 0000000000..c30ac2ff76
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/MigratedFromSync11.java
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.login;
+
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.PasswordRequired;
+
+public class MigratedFromSync11 extends State {
+ public final String password;
+
+ public MigratedFromSync11(String email, String uid, boolean verified, String password) {
+ super(StateLabel.MigratedFromSync11, email, uid, verified);
+ // Null password is allowed.
+ this.password = password;
+ }
+
+ @Override
+ public void execute(final ExecuteDelegate delegate) {
+ delegate.handleTransition(new PasswordRequired(), this);
+ }
+
+ @Override
+ public Action getNeededAction() {
+ return Action.NeedsFinishMigrating;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Separated.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Separated.java
new file mode 100644
index 0000000000..bda620df99
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Separated.java
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.login;
+
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.PasswordRequired;
+
+
+public class Separated extends State {
+ public Separated(String email, String uid, boolean verified) {
+ super(StateLabel.Separated, email, uid, verified);
+ }
+
+ @Override
+ public void execute(final ExecuteDelegate delegate) {
+ delegate.handleTransition(new PasswordRequired(), this);
+ }
+
+ @Override
+ public Action getNeededAction() {
+ return Action.NeedsPassword;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/State.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/State.java
new file mode 100644
index 0000000000..797011ec23
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/State.java
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.login;
+
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.Utils;
+
+public abstract class State {
+ public static final long CURRENT_VERSION = 3L;
+
+ public enum StateLabel {
+ Engaged,
+ Cohabiting,
+ Married,
+ Separated,
+ Doghouse,
+ MigratedFromSync11,
+ }
+
+ public enum Action {
+ NeedsUpgrade,
+ NeedsPassword,
+ NeedsVerification,
+ NeedsFinishMigrating,
+ None,
+ }
+
+ protected final StateLabel stateLabel;
+ public final String email;
+ public final String uid;
+ public final boolean verified;
+
+ public State(StateLabel stateLabel, String email, String uid, boolean verified) {
+ Utils.throwIfNull(email, uid);
+ this.stateLabel = stateLabel;
+ this.email = email;
+ this.uid = uid;
+ this.verified = verified;
+ }
+
+ public StateLabel getStateLabel() {
+ return this.stateLabel;
+ }
+
+ public ExtendedJSONObject toJSONObject() {
+ ExtendedJSONObject o = new ExtendedJSONObject();
+ o.put("version", State.CURRENT_VERSION);
+ o.put("email", email);
+ o.put("uid", uid);
+ o.put("verified", verified);
+ return o;
+ }
+
+ public State makeSeparatedState() {
+ return new Separated(email, uid, verified);
+ }
+
+ public State makeDoghouseState() {
+ return new Doghouse(email, uid, verified);
+ }
+
+ public State makeMigratedFromSync11State(String password) {
+ return new MigratedFromSync11(email, uid, verified, password);
+ }
+
+ public abstract void execute(ExecuteDelegate delegate);
+
+ public abstract Action getNeededAction();
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/StateFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/StateFactory.java
new file mode 100644
index 0000000000..a98f2fb276
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/StateFactory.java
@@ -0,0 +1,206 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.login;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.browserid.DSACryptoImplementation;
+import org.mozilla.gecko.browserid.RSACryptoImplementation;
+import org.mozilla.gecko.fxa.login.State.StateLabel;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.Utils;
+
+/**
+ * Create {@link State} instances from serialized representations.
+ * <p>
+ * Version 1 recognizes 5 state labels (Engaged, Cohabiting, Married, Separated,
+ * Doghouse). In the Cohabiting and Married states, the associated key pairs are
+ * always RSA key pairs.
+ * <p>
+ * Version 2 is identical to version 1, except that in the Cohabiting and
+ * Married states, the associated keypairs are always DSA key pairs.
+ */
+public class StateFactory {
+ private static final String LOG_TAG = StateFactory.class.getSimpleName();
+
+ private static final int KEY_PAIR_SIZE_IN_BITS_V1 = 1024;
+
+ public static BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
+ // New key pairs are always DSA.
+ return DSACryptoImplementation.generateKeyPair(KEY_PAIR_SIZE_IN_BITS_V1);
+ }
+
+ protected static BrowserIDKeyPair keyPairFromJSONObjectV1(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
+ // V1 key pairs are RSA.
+ return RSACryptoImplementation.fromJSONObject(o);
+ }
+
+ protected static BrowserIDKeyPair keyPairFromJSONObjectV2(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
+ // V2 key pairs are DSA.
+ return DSACryptoImplementation.fromJSONObject(o);
+ }
+
+ public static State fromJSONObject(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
+ Long version = o.getLong("version");
+ if (version == null) {
+ throw new IllegalStateException("version must not be null");
+ }
+
+ final int v = version.intValue();
+ if (v == 3) {
+ // The most common case is the most recent version.
+ return fromJSONObjectV3(stateLabel, o);
+ }
+ if (v == 2) {
+ return fromJSONObjectV2(stateLabel, o);
+ }
+ if (v == 1) {
+ final State state = fromJSONObjectV1(stateLabel, o);
+ return migrateV1toV2(stateLabel, state);
+ }
+ throw new IllegalStateException("version must be in {1, 2}");
+ }
+
+ protected static State fromJSONObjectV1(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
+ switch (stateLabel) {
+ case Engaged:
+ return new Engaged(
+ o.getString("email"),
+ o.getString("uid"),
+ o.getBoolean("verified"),
+ Utils.hex2Byte(o.getString("unwrapkB")),
+ Utils.hex2Byte(o.getString("sessionToken")),
+ Utils.hex2Byte(o.getString("keyFetchToken")));
+ case Cohabiting:
+ return new Cohabiting(
+ o.getString("email"),
+ o.getString("uid"),
+ Utils.hex2Byte(o.getString("sessionToken")),
+ Utils.hex2Byte(o.getString("kA")),
+ Utils.hex2Byte(o.getString("kB")),
+ keyPairFromJSONObjectV1(o.getObject("keyPair")));
+ case Married:
+ return new Married(
+ o.getString("email"),
+ o.getString("uid"),
+ Utils.hex2Byte(o.getString("sessionToken")),
+ Utils.hex2Byte(o.getString("kA")),
+ Utils.hex2Byte(o.getString("kB")),
+ keyPairFromJSONObjectV1(o.getObject("keyPair")),
+ o.getString("certificate"));
+ case Separated:
+ return new Separated(
+ o.getString("email"),
+ o.getString("uid"),
+ o.getBoolean("verified"));
+ case Doghouse:
+ return new Doghouse(
+ o.getString("email"),
+ o.getString("uid"),
+ o.getBoolean("verified"));
+ default:
+ throw new IllegalStateException("unrecognized state label: " + stateLabel);
+ }
+ }
+
+ /**
+ * Exactly the same as {@link fromJSONObjectV1}, except that all key pairs are DSA key pairs.
+ */
+ protected static State fromJSONObjectV2(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
+ switch (stateLabel) {
+ case Cohabiting:
+ return new Cohabiting(
+ o.getString("email"),
+ o.getString("uid"),
+ Utils.hex2Byte(o.getString("sessionToken")),
+ Utils.hex2Byte(o.getString("kA")),
+ Utils.hex2Byte(o.getString("kB")),
+ keyPairFromJSONObjectV2(o.getObject("keyPair")));
+ case Married:
+ return new Married(
+ o.getString("email"),
+ o.getString("uid"),
+ Utils.hex2Byte(o.getString("sessionToken")),
+ Utils.hex2Byte(o.getString("kA")),
+ Utils.hex2Byte(o.getString("kB")),
+ keyPairFromJSONObjectV2(o.getObject("keyPair")),
+ o.getString("certificate"));
+ default:
+ return fromJSONObjectV1(stateLabel, o);
+ }
+ }
+
+ /**
+ * Exactly the same as {@link fromJSONObjectV2}, except that there's a new
+ * MigratedFromSyncV11 state.
+ */
+ protected static State fromJSONObjectV3(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
+ switch (stateLabel) {
+ case MigratedFromSync11:
+ return new MigratedFromSync11(
+ o.getString("email"),
+ o.getString("uid"),
+ o.getBoolean("verified"),
+ o.getString("password"));
+ default:
+ return fromJSONObjectV2(stateLabel, o);
+ }
+ }
+
+ protected static void logMigration(State from, State to) {
+ if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) {
+ return;
+ }
+ try {
+ FxAccountUtils.pii(LOG_TAG, "V1 persisted state is: " + from.toJSONObject().toJSONString());
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Error producing JSON representation of V1 state.", e);
+ }
+ FxAccountUtils.pii(LOG_TAG, "Generated new V2 state: " + to.toJSONObject().toJSONString());
+ }
+
+ protected static State migrateV1toV2(StateLabel stateLabel, State state) throws NoSuchAlgorithmException {
+ if (state == null) {
+ // This should never happen, but let's be careful.
+ Logger.error(LOG_TAG, "Got null state in migrateV1toV2; returning null.");
+ return state;
+ }
+
+ Logger.info(LOG_TAG, "Migrating V1 persisted State to V2; stateLabel: " + stateLabel);
+
+ // In V1, we use an RSA keyPair. In V2, we use a DSA keyPair. Only
+ // Cohabiting and Married states have a persisted keyPair at all; all
+ // other states need no conversion at all.
+ switch (stateLabel) {
+ case Cohabiting: {
+ // In the Cohabiting state, we can just generate a new key pair and move on.
+ final Cohabiting cohabiting = (Cohabiting) state;
+ final BrowserIDKeyPair keyPair = generateKeyPair();
+ final State migrated = new Cohabiting(cohabiting.email, cohabiting.uid, cohabiting.sessionToken, cohabiting.kA, cohabiting.kB, keyPair);
+ logMigration(cohabiting, migrated);
+ return migrated;
+ }
+ case Married: {
+ // In the Married state, we cannot only change the key pair: the stored
+ // certificate signs the public key of the now obsolete key pair. We
+ // regress to the Cohabiting state; the next time we sync, we should
+ // advance back to Married.
+ final Married married = (Married) state;
+ final BrowserIDKeyPair keyPair = generateKeyPair();
+ final State migrated = new Cohabiting(married.email, married.uid, married.sessionToken, married.kA, married.kB, keyPair);
+ logMigration(married, migrated);
+ return migrated;
+ }
+ default:
+ // Otherwise, V1 and V2 states are identical.
+ return state;
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/TokensAndKeysState.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/TokensAndKeysState.java
new file mode 100644
index 0000000000..b5121a4d40
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/TokensAndKeysState.java
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.login;
+
+import org.mozilla.gecko.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.Utils;
+
+public abstract class TokensAndKeysState extends State {
+ protected final byte[] sessionToken;
+ protected final byte[] kA;
+ protected final byte[] kB;
+ protected final BrowserIDKeyPair keyPair;
+
+ public TokensAndKeysState(StateLabel stateLabel, String email, String uid, byte[] sessionToken, byte[] kA, byte[] kB, BrowserIDKeyPair keyPair) {
+ super(stateLabel, email, uid, true);
+ Utils.throwIfNull(sessionToken, kA, kB, keyPair);
+ this.sessionToken = sessionToken;
+ this.kA = kA;
+ this.kB = kB;
+ this.keyPair = keyPair;
+ }
+
+ @Override
+ public ExtendedJSONObject toJSONObject() {
+ ExtendedJSONObject o = super.toJSONObject();
+ // Fields are non-null by constructor.
+ o.put("sessionToken", Utils.byte2Hex(sessionToken));
+ o.put("kA", Utils.byte2Hex(kA));
+ o.put("kB", Utils.byte2Hex(kB));
+ o.put("keyPair", keyPair.toJSONObject());
+ return o;
+ }
+
+ public byte[] getSessionToken() {
+ return sessionToken;
+ }
+
+ @Override
+ public Action getNeededAction() {
+ return Action.None;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountDeletedService.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountDeletedService.java
new file mode 100644
index 0000000000..60a63a5e11
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountDeletedService.java
@@ -0,0 +1,154 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.receivers;
+
+import android.app.IntentService;
+import android.content.Context;
+import android.content.Intent;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient;
+import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException;
+import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.sync.FxAccountNotificationManager;
+import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter;
+import org.mozilla.gecko.sync.repositories.android.ClientsDatabase;
+import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository;
+
+import java.util.concurrent.Executor;
+
+/**
+ * A background service to clean up after a Firefox Account is deleted.
+ * <p>
+ * Note that we specifically handle deleting the pickle file using a Service and a
+ * BroadcastReceiver, rather than a background thread, to allow channels sharing a Firefox account
+ * to delete their respective pickle files (since, if one remains, the account will be restored
+ * when that channel is used).
+ */
+public class FxAccountDeletedService extends IntentService {
+ public static final String LOG_TAG = FxAccountDeletedService.class.getSimpleName();
+
+ public FxAccountDeletedService() {
+ super(LOG_TAG);
+ }
+
+ @Override
+ protected void onHandleIntent(final Intent intent) {
+ // We have an in-memory accounts cache which we use for a variety of tasks; it needs to be cleared.
+ // It should be fine to invalidate it before doing anything else, as the tasks below do not rely
+ // on this data.
+ AndroidFxAccount.invalidateCaches();
+
+ // Intent can, in theory, be null. Bug 1025937.
+ if (intent == null) {
+ Logger.debug(LOG_TAG, "Short-circuiting on null intent.");
+ return;
+ }
+
+ final Context context = this;
+
+ long intentVersion = intent.getLongExtra(
+ FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION_KEY, 0);
+ long expectedVersion = FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION;
+ if (intentVersion != expectedVersion) {
+ Logger.warn(LOG_TAG, "Intent malformed: version " + intentVersion + " given but " +
+ "version " + expectedVersion + "expected. Not cleaning up after deleted Account.");
+ return;
+ }
+
+ // Android Account name, not Sync encoded account name.
+ final String accountName = intent.getStringExtra(
+ FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_KEY);
+ if (accountName == null) {
+ Logger.warn(LOG_TAG, "Intent malformed: no account name given. Not cleaning up after " +
+ "deleted Account.");
+ return;
+ }
+
+
+ // Fire up gecko and unsubscribe push
+ final Intent geckoIntent = new Intent();
+ geckoIntent.setAction("create-services");
+ geckoIntent.setClassName(context, "org.mozilla.gecko.GeckoService");
+ geckoIntent.putExtra("category", "android-push-service");
+ geckoIntent.putExtra("data", "android-fxa-unsubscribe");
+ final AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context);
+ geckoIntent.putExtra("org.mozilla.gecko.intent.PROFILE_NAME",
+ intent.getStringExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_PROFILE));
+ context.startService(geckoIntent);
+
+ // Delete client database and non-local tabs.
+ Logger.info(LOG_TAG, "Deleting the entire Fennec clients database and non-local tabs");
+ FennecTabsRepository.deleteNonLocalClientsAndTabs(context);
+
+
+ // Clear Firefox Sync client tables.
+ try {
+ Logger.info(LOG_TAG, "Deleting the Firefox Sync clients database.");
+ ClientsDatabase db = null;
+ try {
+ db = new ClientsDatabase(context);
+ db.wipeClientsTable();
+ db.wipeCommandsTable();
+ } finally {
+ if (db != null) {
+ db.close();
+ }
+ }
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Got exception deleting the Firefox Sync clients database; ignoring.", e);
+ }
+
+ // Remove any displayed notifications.
+ new FxAccountNotificationManager(FxAccountSyncAdapter.NOTIFICATION_ID).clear(context);
+
+ // Bug 1147275: Delete cached oauth tokens. There's no way to query all
+ // oauth tokens from Android, so this is tricky to do comprehensively. We
+ // can query, individually, for specific oauth tokens to delete, however.
+ final String oauthServerURI = intent.getStringExtra(FxAccountConstants.ACCOUNT_OAUTH_SERVICE_ENDPOINT_KEY);
+ final String[] tokens = intent.getStringArrayExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_AUTH_TOKENS);
+ if (oauthServerURI != null && tokens != null) {
+ final Executor directExecutor = new Executor() {
+ @Override
+ public void execute(Runnable runnable) {
+ runnable.run();
+ }
+ };
+
+ final FxAccountOAuthClient10 oauthClient = new FxAccountOAuthClient10(oauthServerURI, directExecutor);
+
+ for (String token : tokens) {
+ if (token == null) {
+ Logger.error(LOG_TAG, "Cached OAuth token is null; should never happen. Ignoring.");
+ continue;
+ }
+ try {
+ oauthClient.deleteToken(token, new FxAccountAbstractClient.RequestDelegate<Void>() {
+ @Override
+ public void handleSuccess(Void result) {
+ Logger.info(LOG_TAG, "Successfully deleted cached OAuth token.");
+ }
+
+ @Override
+ public void handleError(Exception e) {
+ Logger.error(LOG_TAG, "Failed to delete cached OAuth token; ignoring.", e);
+ }
+
+ @Override
+ public void handleFailure(FxAccountAbstractClientRemoteException e) {
+ Logger.error(LOG_TAG, "Exception during cached OAuth token deletion; ignoring.", e);
+ }
+ });
+ } catch (Exception e) {
+ Logger.error(LOG_TAG, "Exception during cached OAuth token deletion; ignoring.", e);
+ }
+ }
+ } else {
+ Logger.error(LOG_TAG, "Cached OAuth server URI is null or cached OAuth tokens are null; ignoring.");
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountUpgradeReceiver.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountUpgradeReceiver.java
new file mode 100644
index 0000000000..ad81e04886
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountUpgradeReceiver.java
@@ -0,0 +1,133 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.receivers;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.fxa.login.State.StateLabel;
+import org.mozilla.gecko.sync.Utils;
+
+import android.accounts.Account;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * A receiver that takes action when our Android package is upgraded (replaced).
+ */
+public class FxAccountUpgradeReceiver extends BroadcastReceiver {
+ private static final String LOG_TAG = FxAccountUpgradeReceiver.class.getSimpleName();
+
+ /**
+ * Produce a list of Runnable instances to be executed sequentially on
+ * upgrade.
+ * <p>
+ * Each Runnable will be executed sequentially on a background thread. Any
+ * unchecked Exception thrown will be caught and ignored.
+ *
+ * @param context Android context.
+ * @return list of Runnable instances.
+ */
+ protected List<Runnable> onUpgradeRunnables(Context context) {
+ List<Runnable> runnables = new LinkedList<Runnable>();
+ runnables.add(new MaybeUnpickleRunnable(context));
+ // Recovering accounts that are in the Doghouse should happen *after* we
+ // unpickle any accounts saved to disk.
+ runnables.add(new AdvanceFromDoghouseRunnable(context));
+ return runnables;
+ }
+
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG);
+ Logger.info(LOG_TAG, "Upgrade broadcast received.");
+
+ // Iterate Runnable instances one at a time.
+ final Executor executor = Executors.newSingleThreadExecutor();
+ for (final Runnable runnable : onUpgradeRunnables(context)) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ runnable.run();
+ } catch (Exception e) {
+ // We really don't want to throw on a background thread, so we
+ // catch, log, and move on.
+ Logger.error(LOG_TAG, "Got exception executing background upgrade Runnable; ignoring.", e);
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * A Runnable that tries to unpickle any pickled Firefox Accounts.
+ */
+ protected static class MaybeUnpickleRunnable implements Runnable {
+ protected final Context context;
+
+ public MaybeUnpickleRunnable(Context context) {
+ this.context = context;
+ }
+
+ @Override
+ public void run() {
+ // Querying the accounts will unpickle any pickled Firefox Account.
+ Logger.info(LOG_TAG, "Trying to unpickle any pickled Firefox Account.");
+ FirefoxAccounts.getFirefoxAccounts(context);
+ }
+ }
+
+ /**
+ * A Runnable that tries to advance existing Firefox Accounts that are in the
+ * Doghouse state to the Separated state.
+ * <p>
+ * This is our main deprecation-and-upgrade mechanism: in some way, the
+ * Account gets moved to the Doghouse state. If possible, an upgraded version
+ * of the package advances to Separated, prompting the user to re-connect the
+ * Account.
+ */
+ protected static class AdvanceFromDoghouseRunnable implements Runnable {
+ protected final Context context;
+
+ public AdvanceFromDoghouseRunnable(Context context) {
+ this.context = context;
+ }
+
+ @Override
+ public void run() {
+ final Account[] accounts = FirefoxAccounts.getFirefoxAccounts(context);
+ Logger.info(LOG_TAG, "Trying to advance " + accounts.length + " existing Firefox Accounts from the Doghouse to Separated (if necessary).");
+ for (Account account : accounts) {
+ try {
+ final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
+ // For great debugging.
+ if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
+ fxAccount.dump();
+ }
+ State state = fxAccount.getState();
+ if (state == null || state.getStateLabel() != StateLabel.Doghouse) {
+ Logger.debug(LOG_TAG, "Account named like " + Utils.obfuscateEmail(account.name) + " is not in the Doghouse; skipping.");
+ continue;
+ }
+ Logger.debug(LOG_TAG, "Account named like " + Utils.obfuscateEmail(account.name) + " is in the Doghouse; advancing to Separated.");
+ fxAccount.setState(state.makeSeparatedState());
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Got exception trying to advance account named like " + Utils.obfuscateEmail(account.name) +
+ " from Doghouse to Separated state; ignoring.", e);
+ }
+ }
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountNotificationManager.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountNotificationManager.java
new file mode 100644
index 0000000000..b44da76fc5
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountNotificationManager.java
@@ -0,0 +1,114 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.sync;
+
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.NotificationCompat.Builder;
+import org.mozilla.gecko.Locales;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.common.telemetry.TelemetryWrapper;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.activities.FxAccountWebFlowActivity;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.fxa.login.State.Action;
+import org.mozilla.gecko.sync.telemetry.TelemetryContract;
+
+/**
+ * Abstraction that manages notifications shown or hidden for a Firefox Account.
+ * <p>
+ * In future, we anticipate this tracking things like:
+ * <ul>
+ * <li>new engines to offer to Sync;</li>
+ * <li>service interruption updates;</li>
+ * <li>messages from other clients.</li>
+ * </ul>
+ */
+public class FxAccountNotificationManager {
+ private static final String LOG_TAG = FxAccountNotificationManager.class.getSimpleName();
+
+ protected final int notificationId;
+
+ // We're lazy about updating our locale info, because most syncs don't notify.
+ private volatile boolean localeUpdated;
+
+ public FxAccountNotificationManager(int notificationId) {
+ this.notificationId = notificationId;
+ }
+
+ /**
+ * Remove all Firefox Account related notifications from the notification manager.
+ *
+ * @param context
+ * Android context.
+ */
+ public void clear(Context context) {
+ final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ notificationManager.cancel(notificationId);
+ }
+
+ /**
+ * Reflect new Firefox Account state to the notification manager: show or hide
+ * notifications reflecting the state of a Firefox Account.
+ *
+ * @param context
+ * Android context.
+ * @param fxAccount
+ * Firefox Account to reflect to the notification manager.
+ */
+ public void update(Context context, AndroidFxAccount fxAccount) {
+ final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ final State state = fxAccount.getState();
+ final Action action = state.getNeededAction();
+ if (action == Action.None) {
+ Logger.info(LOG_TAG, "State " + state.getStateLabel() + " needs no action; cancelling any existing notification.");
+ notificationManager.cancel(notificationId);
+ return;
+ }
+
+ if (!localeUpdated) {
+ localeUpdated = true;
+ Locales.getLocaleManager().getAndApplyPersistedLocale(context);
+ }
+
+ final String title;
+ final String text;
+ final Intent notificationIntent;
+ if (action == Action.NeedsFinishMigrating) {
+ TelemetryWrapper.addToHistogram(TelemetryContract.SYNC11_MIGRATION_NOTIFICATIONS_OFFERED, 1);
+
+ title = context.getResources().getString(R.string.fxaccount_sync_finish_migrating_notification_title);
+ text = context.getResources().getString(R.string.fxaccount_sync_finish_migrating_notification_text, state.email);
+ notificationIntent = new Intent(FxAccountConstants.ACTION_FXA_FINISH_MIGRATING);
+ } else {
+ title = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_title);
+ text = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_text, state.email);
+ notificationIntent = new Intent(FxAccountConstants.ACTION_FXA_STATUS);
+ }
+
+ notificationIntent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_NOTIFICATION);
+
+ Logger.info(LOG_TAG, "State " + state.getStateLabel() + " needs action; offering notification with title: " + title);
+ FxAccountUtils.pii(LOG_TAG, "And text: " + text);
+
+ final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
+
+ final Builder builder = new NotificationCompat.Builder(context);
+ builder
+ .setContentTitle(title)
+ .setContentText(text)
+ .setSmallIcon(R.drawable.ic_status_logo)
+ .setAutoCancel(true)
+ .setContentIntent(pendingIntent);
+ notificationManager.notify(notificationId, builder.build());
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountProfileService.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountProfileService.java
new file mode 100644
index 0000000000..7f03eff1c9
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountProfileService.java
@@ -0,0 +1,107 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.sync;
+
+import android.accounts.AccountManager;
+import android.app.Activity;
+import android.app.IntentService;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient;
+import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException;
+import org.mozilla.gecko.background.fxa.profile.FxAccountProfileClient10;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class FxAccountProfileService extends IntentService {
+ private static final String LOG_TAG = "FxAccountProfileService";
+ private static final Executor EXECUTOR_SERVICE = Executors.newSingleThreadExecutor();
+ public static final String KEY_AUTH_TOKEN = "auth_token";
+ public static final String KEY_PROFILE_SERVER_URI = "profileServerURI";
+ public static final String KEY_RESULT_RECEIVER = "resultReceiver";
+ public static final String KEY_RESULT_STRING = "RESULT_STRING";
+
+ public FxAccountProfileService() {
+ super("FxAccountProfileService");
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ final String authToken = intent.getStringExtra(KEY_AUTH_TOKEN);
+ final String profileServerURI = intent.getStringExtra(KEY_PROFILE_SERVER_URI);
+ final ResultReceiver resultReceiver = intent.getParcelableExtra(KEY_RESULT_RECEIVER);
+
+ if (resultReceiver == null) {
+ Logger.warn(LOG_TAG, "Result receiver must not be null; ignoring intent.");
+ return;
+ }
+
+ if (authToken == null || authToken.length() == 0) {
+ Logger.warn(LOG_TAG, "Invalid Auth Token");
+ sendResult("Invalid Auth Token", resultReceiver, Activity.RESULT_CANCELED);
+ return;
+ }
+
+ if (profileServerURI == null || profileServerURI.length() == 0) {
+ Logger.warn(LOG_TAG, "Invalid profile Server Endpoint");
+ sendResult("Invalid profile Server Endpoint", resultReceiver, Activity.RESULT_CANCELED);
+ return;
+ }
+
+ // This delegate fetches the profile avatar json.
+ FxAccountProfileClient10.RequestDelegate<ExtendedJSONObject> delegate = new FxAccountAbstractClient.RequestDelegate<ExtendedJSONObject>() {
+ @Override
+ public void handleError(Exception e) {
+ Logger.error(LOG_TAG, "Error fetching Account profile.", e);
+ sendResult("Error fetching Account profile.", resultReceiver, Activity.RESULT_CANCELED);
+ }
+
+ @Override
+ public void handleFailure(FxAccountAbstractClientException.FxAccountAbstractClientRemoteException e) {
+ Logger.warn(LOG_TAG, "Failed to fetch Account profile.", e);
+
+ if (e.isInvalidAuthentication()) {
+ // The profile server rejected the cached oauth token! Invalidate it.
+ // A new token will be generated upon next request.
+ Logger.info(LOG_TAG, "Invalidating oauth token after 401!");
+ AccountManager.get(FxAccountProfileService.this).invalidateAuthToken(FxAccountConstants.ACCOUNT_TYPE, authToken);
+ }
+
+ sendResult("Failed to fetch Account profile.", resultReceiver, Activity.RESULT_CANCELED);
+ }
+
+ @Override
+ public void handleSuccess(ExtendedJSONObject result) {
+ if (result != null){
+ FxAccountUtils.pii(LOG_TAG, "Profile server return profile: " + result.toJSONString());
+ sendResult(result.toJSONString(), resultReceiver, Activity.RESULT_OK);
+ }
+ }
+ };
+
+ FxAccountProfileClient10 client = new FxAccountProfileClient10(profileServerURI, EXECUTOR_SERVICE);
+ try {
+ client.profile(authToken, delegate);
+ } catch (Exception e) {
+ Logger.error(LOG_TAG, "Got exception fetching profile.", e);
+ delegate.handleError(e);
+ }
+ }
+
+ private void sendResult(final String result, final ResultReceiver resultReceiver, final int code) {
+ if (resultReceiver != null) {
+ final Bundle bundle = new Bundle();
+ bundle.putString(KEY_RESULT_STRING, result);
+ resultReceiver.send(code, bundle);
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSchedulePolicy.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSchedulePolicy.java
new file mode 100644
index 0000000000..708686e726
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSchedulePolicy.java
@@ -0,0 +1,178 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.sync;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.State.Action;
+import org.mozilla.gecko.sync.BackoffHandler;
+
+import android.accounts.Account;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.Bundle;
+
+public class FxAccountSchedulePolicy implements SchedulePolicy {
+ private static final String LOG_TAG = "FxAccountSchedulePolicy";
+
+ // Our poll intervals are used to trigger automatic background syncs
+ // in the absence of user activity.
+ //
+ // We also receive sync requests as a result of network tickles, so
+ // these intervals are long, with the exception of the rapid polling
+ // while we wait for verification: if we're waiting for the user to
+ // click on a verification link, we sync very often in order to detect
+ // a change in state.
+ //
+ // In the case of unverified -> unverified (no transition), this should be
+ // very close to a single HTTP request (with the SyncAdapter overhead, of
+ // course, but that's not wildly different from alarm manager overhead).
+ //
+ // The /account/status endpoint is HAWK authed by sessionToken, so we still
+ // have to do some crypto no matter what.
+
+ // TODO: only do this for a while...
+ public static final long POLL_INTERVAL_PENDING_VERIFICATION = 60; // 1 minute.
+
+ // If we're in some kind of error state, there's no point trying often.
+ // This is not the same as a server-imposed backoff, which will be
+ // reflected dynamically.
+ public static final long POLL_INTERVAL_ERROR_STATE_SEC = 24 * 60 * 60; // 24 hours.
+
+ // If we're the only device, just sync once or twice a day in case that
+ // changes.
+ public static final long POLL_INTERVAL_SINGLE_DEVICE_SEC = 18 * 60 * 60; // 18 hours.
+
+ // And if we know there are other devices, let's sync often enough that
+ // we'll be more likely to be caught up (even if not completely) by the
+ // time you next use this device. This is also achieved via Android's
+ // network tickles.
+ public static final long POLL_INTERVAL_MULTI_DEVICE_SEC = 12 * 60 * 60; // 12 hours.
+
+ // This is used solely as an optimization for backoff handling, so it's not
+ // persisted.
+ private static volatile long POLL_INTERVAL_CURRENT_SEC = POLL_INTERVAL_SINGLE_DEVICE_SEC;
+
+ // Never sync more frequently than this, unless forced.
+ // This is to avoid overly-frequent syncs during active browsing.
+ public static final long RATE_LIMIT_FUNDAMENTAL_SEC = 90; // 90 seconds.
+
+ /**
+ * We are prompted to sync by several inputs:
+ * * Periodic syncs that we schedule at long intervals. See the POLL constants.
+ * * Network-tickle-based syncs that Android starts.
+ * * Upload-only syncs that are caused by local database writes.
+ *
+ * We rate-limit periodic and network-sourced events with this constant.
+ * We rate limit <b>both</b> with {@link FxAccountSchedulePolicy#RATE_LIMIT_FUNDAMENTAL_SEC}.
+ */
+ public static final long RATE_LIMIT_BACKGROUND_SEC = 60 * 60; // 1 hour.
+
+ private final AndroidFxAccount account;
+ private final Context context;
+
+ public FxAccountSchedulePolicy(Context context, AndroidFxAccount account) {
+ this.account = account;
+ this.context = context;
+ }
+
+ /**
+ * Return a millisecond timestamp in the future, offset from the current
+ * time by the provided amount.
+ * @param millis the duration by which to delay
+ * @return a timestamp.
+ */
+ private static long delay(long millis) {
+ return System.currentTimeMillis() + millis;
+ }
+
+ /**
+ * Updates the existing system periodic sync interval to the specified duration.
+ *
+ * @param intervalSeconds the requested period, which Android will vary by up to 4%.
+ */
+ protected void requestPeriodicSync(final long intervalSeconds) {
+ final String authority = BrowserContract.AUTHORITY;
+ final Account account = this.account.getAndroidAccount();
+ this.context.getContentResolver();
+ Logger.info(LOG_TAG, "Scheduling periodic sync for " + intervalSeconds + ".");
+ ContentResolver.addPeriodicSync(account, authority, Bundle.EMPTY, intervalSeconds);
+ POLL_INTERVAL_CURRENT_SEC = intervalSeconds;
+ }
+
+ @Override
+ public void onSuccessfulSync(int otherClientsCount) {
+ this.account.setLastSyncedTimestamp(System.currentTimeMillis());
+ // This undoes the change made in observeBackoffMillis -- once we hit backoff we'll
+ // periodically sync at the backoff duration, but as soon as we succeed we'll switch
+ // into the client-count-dependent interval.
+ long interval = (otherClientsCount > 0) ? POLL_INTERVAL_MULTI_DEVICE_SEC : POLL_INTERVAL_SINGLE_DEVICE_SEC;
+ requestPeriodicSync(interval);
+ }
+
+ @Override
+ public void onHandleFinal(Action needed) {
+ switch (needed) {
+ case NeedsPassword:
+ case NeedsUpgrade:
+ case NeedsFinishMigrating:
+ requestPeriodicSync(POLL_INTERVAL_ERROR_STATE_SEC);
+ break;
+ case NeedsVerification:
+ requestPeriodicSync(POLL_INTERVAL_PENDING_VERIFICATION);
+ break;
+ case None:
+ // No action needed: we'll set the periodic sync interval
+ // when the sync finishes, via the SessionCallback.
+ break;
+ }
+ }
+
+ @Override
+ public void onUpgradeRequired() {
+ // TODO: this shouldn't occur in FxA, but when we upgrade we
+ // need to reduce the interval again.
+ requestPeriodicSync(POLL_INTERVAL_ERROR_STATE_SEC);
+ }
+
+ @Override
+ public void onUnauthorized() {
+ // TODO: this shouldn't occur in FxA, but when we fix our credentials
+ // we need to reduce the interval again.
+ requestPeriodicSync(POLL_INTERVAL_ERROR_STATE_SEC);
+ }
+
+ @Override
+ public void configureBackoffMillisOnBackoff(BackoffHandler backoffHandler, long backoffMillis, boolean onlyExtend) {
+ if (onlyExtend) {
+ backoffHandler.extendEarliestNextRequest(delay(backoffMillis));
+ } else {
+ backoffHandler.setEarliestNextRequest(delay(backoffMillis));
+ }
+
+ // Yes, we might be part-way through the interval, in which case the backoff
+ // code will do its job. But we certainly don't want to reduce the interval
+ // if we're given a small backoff instruction.
+ // We'll reset the poll interval next time we sync without a backoff instruction.
+ if (backoffMillis > (POLL_INTERVAL_CURRENT_SEC * 1000)) {
+ // Slightly inflate the backoff duration to ensure that a fuzzed
+ // periodic sync doesn't occur before our backoff has passed. Android
+ // 19+ default to a 4% fuzz factor.
+ requestPeriodicSync((long) Math.ceil((1.05 * backoffMillis) / 1000));
+ }
+ }
+
+ /**
+ * Accepts two {@link BackoffHandler} instances as input. These are used
+ * respectively to track fundamental rate limiting, and to separately
+ * rate-limit periodic and network-tickled syncs.
+ */
+ @Override
+ public void configureBackoffMillisBeforeSyncing(BackoffHandler fundamentalRateHandler, BackoffHandler backgroundRateHandler) {
+ fundamentalRateHandler.setEarliestNextRequest(delay(RATE_LIMIT_FUNDAMENTAL_SEC * 1000));
+ backgroundRateHandler.setEarliestNextRequest(delay(RATE_LIMIT_BACKGROUND_SEC * 1000));
+ }
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java
new file mode 100644
index 0000000000..30990cf7fb
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java
@@ -0,0 +1,568 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.sync;
+
+import android.accounts.Account;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SyncResult;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.text.TextUtils;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.common.telemetry.TelemetryWrapper;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.background.fxa.SkewHandler;
+import org.mozilla.gecko.browserid.JSONWebTokenUtils;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.FxAccountDeviceRegistrator;
+import org.mozilla.gecko.fxa.authenticator.AccountPickler;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.authenticator.FxADefaultLoginStateMachineDelegate;
+import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator;
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine;
+import org.mozilla.gecko.fxa.login.Married;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.fxa.login.State.StateLabel;
+import org.mozilla.gecko.fxa.sync.FxAccountSyncDelegate.Result;
+import org.mozilla.gecko.sync.BackoffHandler;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.PrefsBackoffHandler;
+import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
+import org.mozilla.gecko.sync.SyncConfiguration;
+import org.mozilla.gecko.sync.ThreadPool;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
+import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
+import org.mozilla.gecko.sync.telemetry.TelemetryContract;
+import org.mozilla.gecko.tokenserver.TokenServerClient;
+import org.mozilla.gecko.tokenserver.TokenServerClientDelegate;
+import org.mozilla.gecko.tokenserver.TokenServerException;
+import org.mozilla.gecko.tokenserver.TokenServerToken;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+
+public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
+ private static final String LOG_TAG = FxAccountSyncAdapter.class.getSimpleName();
+
+ public static final int NOTIFICATION_ID = LOG_TAG.hashCode();
+
+ // Tracks the last seen storage hostname for backoff purposes.
+ private static final String PREF_BACKOFF_STORAGE_HOST = "backoffStorageHost";
+
+ // Used to do cheap in-memory rate limiting. Don't sync again if we
+ // successfully synced within this duration.
+ private static final int MINIMUM_SYNC_DELAY_MILLIS = 15 * 1000; // 15 seconds.
+ private volatile long lastSyncRealtimeMillis;
+
+ protected final ExecutorService executor;
+ protected final FxAccountNotificationManager notificationManager;
+
+ public FxAccountSyncAdapter(Context context, boolean autoInitialize) {
+ super(context, autoInitialize);
+ this.executor = Executors.newSingleThreadExecutor();
+ this.notificationManager = new FxAccountNotificationManager(NOTIFICATION_ID);
+ }
+
+ protected static class SyncDelegate extends FxAccountSyncDelegate {
+ @Override
+ public void handleSuccess() {
+ Logger.info(LOG_TAG, "Sync succeeded.");
+ super.handleSuccess();
+ TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_COMPLETED, 1);
+ }
+
+ @Override
+ public void handleError(Exception e) {
+ Logger.error(LOG_TAG, "Got exception syncing.", e);
+ super.handleError(e);
+ TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_FAILED, 1);
+ }
+
+ @Override
+ public void handleCannotSync(State finalState) {
+ Logger.warn(LOG_TAG, "Cannot sync from state: " + finalState.getStateLabel());
+ super.handleCannotSync(finalState);
+ }
+
+ @Override
+ public void postponeSync(long millis) {
+ if (millis <= 0) {
+ Logger.debug(LOG_TAG, "Asked to postpone sync, but zero delay.");
+ }
+ super.postponeSync(millis);
+ }
+
+ @Override
+ public void rejectSync() {
+ super.rejectSync();
+ }
+
+ protected final Collection<String> stageNamesToSync;
+
+ public SyncDelegate(BlockingQueue<Result> latch, SyncResult syncResult, AndroidFxAccount fxAccount, Collection<String> stageNamesToSync) {
+ super(latch, syncResult);
+ this.stageNamesToSync = Collections.unmodifiableCollection(stageNamesToSync);
+ }
+
+ public Collection<String> getStageNamesToSync() {
+ return this.stageNamesToSync;
+ }
+ }
+
+ protected static class SessionCallback implements GlobalSessionCallback {
+ protected final SyncDelegate syncDelegate;
+ protected final SchedulePolicy schedulePolicy;
+ protected volatile BackoffHandler storageBackoffHandler;
+
+ public SessionCallback(SyncDelegate syncDelegate, SchedulePolicy schedulePolicy) {
+ this.syncDelegate = syncDelegate;
+ this.schedulePolicy = schedulePolicy;
+ }
+
+ public void setBackoffHandler(BackoffHandler backoffHandler) {
+ this.storageBackoffHandler = backoffHandler;
+ }
+
+ @Override
+ public boolean shouldBackOffStorage() {
+ return storageBackoffHandler.delayMilliseconds() > 0;
+ }
+
+ @Override
+ public void requestBackoff(long backoffMillis) {
+ final boolean onlyExtend = true; // Because we trust what the storage server says.
+ schedulePolicy.configureBackoffMillisOnBackoff(storageBackoffHandler, backoffMillis, onlyExtend);
+ }
+
+ @Override
+ public void informUpgradeRequiredResponse(GlobalSession session) {
+ schedulePolicy.onUpgradeRequired();
+ }
+
+ @Override
+ public void informUnauthorizedResponse(GlobalSession globalSession, URI oldClusterURL) {
+ schedulePolicy.onUnauthorized();
+ }
+
+ @Override
+ public void informMigrated(GlobalSession globalSession) {
+ // It's not possible to migrate a Firefox Account to another Account type
+ // yet. Yell loudly but otherwise ignore.
+ Logger.error(LOG_TAG,
+ "Firefox Account informMigrated called, but it's not yet possible to migrate. " +
+ "Ignoring even though something is terribly wrong.");
+ }
+
+ @Override
+ public void handleStageCompleted(Stage currentState, GlobalSession globalSession) {
+ }
+
+ @Override
+ public void handleSuccess(GlobalSession globalSession) {
+ Logger.info(LOG_TAG, "Global session succeeded.");
+
+ // Get the number of clients, so we can schedule the sync interval accordingly.
+ try {
+ int otherClientsCount = globalSession.getClientsDelegate().getClientsCount();
+ Logger.debug(LOG_TAG, "" + otherClientsCount + " other client(s).");
+ this.schedulePolicy.onSuccessfulSync(otherClientsCount);
+ } finally {
+ // Continue with the usual success flow.
+ syncDelegate.handleSuccess();
+ }
+ }
+
+ @Override
+ public void handleError(GlobalSession globalSession, Exception e) {
+ Logger.warn(LOG_TAG, "Global session failed."); // Exception will be dumped by delegate below.
+ syncDelegate.handleError(e);
+ // TODO: should we reduce the periodic sync interval?
+ }
+
+ @Override
+ public void handleAborted(GlobalSession globalSession, String reason) {
+ Logger.warn(LOG_TAG, "Global session aborted: " + reason);
+ syncDelegate.handleError(null);
+ // TODO: should we reduce the periodic sync interval?
+ }
+ };
+
+ /**
+ * Return true if the provided {@link BackoffHandler} isn't reporting that we're in
+ * a backoff state, or the provided {@link Bundle} contains flags that indicate
+ * we should force a sync.
+ */
+ private boolean shouldPerformSync(final BackoffHandler backoffHandler, final String kind, final Bundle extras) {
+ final long delay = backoffHandler.delayMilliseconds();
+ if (delay <= 0) {
+ return true;
+ }
+
+ if (extras == null) {
+ return false;
+ }
+
+ final boolean forced = extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false);
+ if (forced) {
+ Logger.info(LOG_TAG, "Forced sync (" + kind + "): overruling remaining backoff of " + delay + "ms.");
+ } else {
+ Logger.info(LOG_TAG, "Not syncing (" + kind + "): must wait another " + delay + "ms.");
+ }
+ return forced;
+ }
+
+ protected void syncWithAssertion(final String audience,
+ final String assertion,
+ final URI tokenServerEndpointURI,
+ final BackoffHandler tokenBackoffHandler,
+ final SharedPreferences sharedPrefs,
+ final KeyBundle syncKeyBundle,
+ final String clientState,
+ final SessionCallback callback,
+ final Bundle extras,
+ final AndroidFxAccount fxAccount) {
+ final TokenServerClientDelegate delegate = new TokenServerClientDelegate() {
+ private boolean didReceiveBackoff = false;
+
+ @Override
+ public String getUserAgent() {
+ return FxAccountConstants.USER_AGENT;
+ }
+
+ @Override
+ public void handleSuccess(final TokenServerToken token) {
+ FxAccountUtils.pii(LOG_TAG, "Got token! uid is " + token.uid + " and endpoint is " + token.endpoint + ".");
+ fxAccount.releaseSharedAccountStateLock();
+
+ if (!didReceiveBackoff) {
+ // We must be OK to touch this token server.
+ tokenBackoffHandler.setEarliestNextRequest(0L);
+ }
+
+ final URI storageServerURI;
+ try {
+ storageServerURI = new URI(token.endpoint);
+ } catch (URISyntaxException e) {
+ handleError(e);
+ return;
+ }
+ final String storageHostname = storageServerURI.getHost();
+
+ // We back off on a per-host basis. When we have an endpoint URI from a token, we
+ // can check on the backoff status for that host.
+ // If we're supposed to be backing off, we abort the not-yet-started session.
+ final BackoffHandler storageBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "sync.storage");
+ callback.setBackoffHandler(storageBackoffHandler);
+
+ String lastStorageHost = sharedPrefs.getString(PREF_BACKOFF_STORAGE_HOST, null);
+ final boolean storageHostIsUnchanged = lastStorageHost != null &&
+ lastStorageHost.equalsIgnoreCase(storageHostname);
+ if (storageHostIsUnchanged) {
+ Logger.debug(LOG_TAG, "Storage host is unchanged.");
+ if (!shouldPerformSync(storageBackoffHandler, "storage", extras)) {
+ Logger.info(LOG_TAG, "Not syncing: storage server requested backoff.");
+ callback.handleAborted(null, "Storage backoff");
+ return;
+ }
+ } else {
+ Logger.debug(LOG_TAG, "Received new storage host.");
+ }
+
+ // Invalidate the previous backoff, because our storage host has changed,
+ // or we never had one at all, or we're OK to sync.
+ storageBackoffHandler.setEarliestNextRequest(0L);
+
+ GlobalSession globalSession = null;
+ try {
+ final ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs, getContext());
+ if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
+ FxAccountUtils.pii(LOG_TAG, "Client device name is: '" + clientsDataDelegate.getClientName() + "'.");
+ FxAccountUtils.pii(LOG_TAG, "Client device data last modified: " + clientsDataDelegate.getLastModifiedTimestamp());
+ }
+
+ // We compute skew over time using SkewHandler. This yields an unchanging
+ // skew adjustment that the HawkAuthHeaderProvider uses to adjust its
+ // timestamps. Eventually we might want this to adapt within the scope of a
+ // global session.
+ final SkewHandler storageServerSkewHandler = SkewHandler.getSkewHandlerForHostname(storageHostname);
+ final long storageServerSkew = storageServerSkewHandler.getSkewInSeconds();
+ // We expect Sync to upload large sets of records. Calculating the
+ // payload verification hash for these record sets could be expensive,
+ // so we explicitly do not send payload verification hashes to the
+ // Sync storage endpoint.
+ final boolean includePayloadVerificationHash = false;
+ final AuthHeaderProvider authHeaderProvider = new HawkAuthHeaderProvider(token.id, token.key.getBytes("UTF-8"), includePayloadVerificationHash, storageServerSkew);
+
+ final Context context = getContext();
+ final SyncConfiguration syncConfig = new SyncConfiguration(token.uid, authHeaderProvider, sharedPrefs, syncKeyBundle);
+
+ Collection<String> knownStageNames = SyncConfiguration.validEngineNames();
+ syncConfig.stagesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras);
+ syncConfig.setClusterURL(storageServerURI);
+
+ globalSession = new GlobalSession(syncConfig, callback, context, clientsDataDelegate);
+ globalSession.start();
+ } catch (Exception e) {
+ callback.handleError(globalSession, e);
+ return;
+ }
+ }
+
+ @Override
+ public void handleFailure(TokenServerException e) {
+ Logger.error(LOG_TAG, "Failed to get token.", e);
+ try {
+ // We should only get here *after* we're locked into the married state.
+ State state = fxAccount.getState();
+ if (state.getStateLabel() == StateLabel.Married) {
+ Married married = (Married) state;
+ fxAccount.setState(married.makeCohabitingState());
+ }
+ } finally {
+ fxAccount.releaseSharedAccountStateLock();
+ }
+ callback.handleError(null, e);
+ }
+
+ @Override
+ public void handleError(Exception e) {
+ Logger.error(LOG_TAG, "Failed to get token.", e);
+ fxAccount.releaseSharedAccountStateLock();
+ callback.handleError(null, e);
+ }
+
+ @Override
+ public void handleBackoff(int backoffSeconds) {
+ // This is the token server telling us to back off.
+ Logger.info(LOG_TAG, "Token server requesting backoff of " + backoffSeconds + "s. Backoff handler: " + tokenBackoffHandler);
+ didReceiveBackoff = true;
+
+ // If we've already stored a backoff, overrule it: we only use the server
+ // value for token server scheduling.
+ tokenBackoffHandler.setEarliestNextRequest(delay(backoffSeconds * 1000));
+ }
+
+ private long delay(long delay) {
+ return System.currentTimeMillis() + delay;
+ }
+ };
+
+ TokenServerClient tokenServerclient = new TokenServerClient(tokenServerEndpointURI, executor);
+ tokenServerclient.getTokenFromBrowserIDAssertion(assertion, true, clientState, delegate);
+ }
+
+ /**
+ * A trivial Sync implementation that does not cache client keys,
+ * certificates, or tokens.
+ *
+ * This should be replaced with a full {@link FxAccountAuthenticator}-based
+ * token implementation.
+ */
+ @Override
+ public void onPerformSync(final Account account, final Bundle extras, final String authority, ContentProviderClient provider, final SyncResult syncResult) {
+ Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG);
+ Logger.resetLogging();
+
+ final Context context = getContext();
+ final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
+
+ Logger.info(LOG_TAG, "Syncing FxAccount" +
+ " account named like " + Utils.obfuscateEmail(account.name) +
+ " for authority " + authority +
+ " with instance " + this + ".");
+
+ Logger.info(LOG_TAG, "Account last synced at: " + fxAccount.getLastSyncedTimestamp());
+
+ if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
+ fxAccount.dump();
+ }
+
+ FirefoxAccounts.logSyncOptions(extras);
+
+ if (this.lastSyncRealtimeMillis > 0L &&
+ (this.lastSyncRealtimeMillis + MINIMUM_SYNC_DELAY_MILLIS) > SystemClock.elapsedRealtime() &&
+ !extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false)) {
+ Logger.info(LOG_TAG, "Not syncing FxAccount " + Utils.obfuscateEmail(account.name) +
+ ": minimum interval not met.");
+ TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_FAILED_BACKOFF, 1);
+ return;
+ }
+
+ // Pickle in a background thread to avoid strict mode warnings.
+ ThreadPool.run(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ AccountPickler.pickle(fxAccount, FxAccountConstants.ACCOUNT_PICKLE_FILENAME);
+ } catch (Exception e) {
+ // Should never happen, but we really don't want to die in a background thread.
+ Logger.warn(LOG_TAG, "Got exception pickling current account details; ignoring.", e);
+ }
+ }
+ });
+
+ final BlockingQueue<Result> latch = new LinkedBlockingQueue<>(1);
+
+ Collection<String> knownStageNames = SyncConfiguration.validEngineNames();
+ Collection<String> stageNamesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras);
+
+ final SyncDelegate syncDelegate = new SyncDelegate(latch, syncResult, fxAccount, stageNamesToSync);
+
+ try {
+ // This will be the same chunk of SharedPreferences that we pass through to GlobalSession/SyncConfiguration.
+ final SharedPreferences sharedPrefs = fxAccount.getSyncPrefs();
+
+ final BackoffHandler backgroundBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "background");
+ final BackoffHandler rateLimitBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "rate");
+
+ // If this sync was triggered by user action, this will be true.
+ final boolean isImmediate = (extras != null) &&
+ (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false) ||
+ extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false));
+
+ // If it's not an immediate sync, it must be either periodic or tickled.
+ // Check our background rate limiter.
+ if (!isImmediate) {
+ if (!shouldPerformSync(backgroundBackoffHandler, "background", extras)) {
+ syncDelegate.rejectSync();
+ return;
+ }
+ }
+
+ // Regardless, let's make sure we're not syncing too often.
+ if (!shouldPerformSync(rateLimitBackoffHandler, "rate", extras)) {
+ syncDelegate.postponeSync(rateLimitBackoffHandler.delayMilliseconds());
+ return;
+ }
+
+ final SchedulePolicy schedulePolicy = new FxAccountSchedulePolicy(context, fxAccount);
+
+ // Set a small scheduled 'backoff' to rate-limit the next sync,
+ // and extend the background delay even further into the future.
+ schedulePolicy.configureBackoffMillisBeforeSyncing(rateLimitBackoffHandler, backgroundBackoffHandler);
+
+ final String tokenServerEndpoint = fxAccount.getTokenServerURI();
+ final URI tokenServerEndpointURI = new URI(tokenServerEndpoint);
+ final String audience = FxAccountUtils.getAudienceForURL(tokenServerEndpoint);
+
+ try {
+ // The clock starts... now!
+ fxAccount.acquireSharedAccountStateLock(FxAccountSyncAdapter.LOG_TAG);
+ } catch (InterruptedException e) {
+ // OK, skip this sync.
+ syncDelegate.handleError(e);
+ return;
+ }
+
+ final State state;
+ try {
+ state = fxAccount.getState();
+ } catch (Exception e) {
+ fxAccount.releaseSharedAccountStateLock();
+ syncDelegate.handleError(e);
+ return;
+ }
+
+ TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_STARTED, 1);
+
+ final FxAccountLoginStateMachine stateMachine = new FxAccountLoginStateMachine();
+ stateMachine.advance(state, StateLabel.Married, new FxADefaultLoginStateMachineDelegate(context, fxAccount) {
+ @Override
+ public void handleNotMarried(State notMarried) {
+ Logger.info(LOG_TAG, "handleNotMarried: in " + notMarried.getStateLabel());
+ schedulePolicy.onHandleFinal(notMarried.getNeededAction());
+ syncDelegate.handleCannotSync(notMarried);
+ }
+
+ private boolean shouldRequestToken(final BackoffHandler tokenBackoffHandler, final Bundle extras) {
+ return shouldPerformSync(tokenBackoffHandler, "token", extras);
+ }
+
+ @Override
+ public void handleMarried(Married married) {
+ schedulePolicy.onHandleFinal(married.getNeededAction());
+ Logger.info(LOG_TAG, "handleMarried: in " + married.getStateLabel());
+
+ try {
+ final String assertion = married.generateAssertion(audience, JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER);
+
+ /*
+ * At this point we're in the correct state to sync, and we're ready to fetch
+ * a token and do some work.
+ *
+ * But first we need to do two things:
+ * 1. Check to see whether we're in a backoff situation for the token server.
+ * If we are, but we're not forcing a sync, then we go no further.
+ * 2. Clear an existing backoff (if we're syncing it doesn't matter, and if
+ * we're forcing we'll get a new backoff if things are still bad).
+ *
+ * Note that we don't check the storage backoff before the token dance: the token
+ * server tells us which server we're syncing to!
+ *
+ * That logic lives in the TokenServerClientDelegate elsewhere in this file.
+ */
+
+ // Strictly speaking this backoff check could be done prior to walking through
+ // the login state machine, allowing us to short-circuit sooner.
+ // We don't expect many token server backoffs, and most users will be sitting
+ // in the Married state, so instead we simply do this here, once.
+ final BackoffHandler tokenBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "token");
+ if (!shouldRequestToken(tokenBackoffHandler, extras)) {
+ Logger.info(LOG_TAG, "Not syncing (token server).");
+ syncDelegate.postponeSync(tokenBackoffHandler.delayMilliseconds());
+ return;
+ }
+
+ final SessionCallback sessionCallback = new SessionCallback(syncDelegate, schedulePolicy);
+ final KeyBundle syncKeyBundle = married.getSyncKeyBundle();
+ final String clientState = married.getClientState();
+ syncWithAssertion(audience, assertion, tokenServerEndpointURI, tokenBackoffHandler, sharedPrefs, syncKeyBundle, clientState, sessionCallback, extras, fxAccount);
+
+ // Register the device if necessary (asynchronous, in another thread)
+ if (fxAccount.getDeviceRegistrationVersion() != FxAccountDeviceRegistrator.DEVICE_REGISTRATION_VERSION
+ || TextUtils.isEmpty(fxAccount.getDeviceId())) {
+ FxAccountDeviceRegistrator.register(context);
+ }
+
+ // Force fetch the profile avatar information. (asynchronous, in another thread)
+ Logger.info(LOG_TAG, "Fetching profile avatar information.");
+ fxAccount.fetchProfileJSON();
+ } catch (Exception e) {
+ syncDelegate.handleError(e);
+ return;
+ }
+ }
+ });
+
+ latch.take();
+ } catch (Exception e) {
+ Logger.error(LOG_TAG, "Got error syncing.", e);
+ syncDelegate.handleError(e);
+ } finally {
+ fxAccount.releaseSharedAccountStateLock();
+ }
+
+ Logger.info(LOG_TAG, "Syncing done.");
+ lastSyncRealtimeMillis = SystemClock.elapsedRealtime();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncDelegate.java
new file mode 100644
index 0000000000..71148f66cf
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncDelegate.java
@@ -0,0 +1,110 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.sync;
+
+import java.util.concurrent.BlockingQueue;
+
+import org.mozilla.gecko.fxa.login.State;
+
+import android.content.SyncResult;
+
+public class FxAccountSyncDelegate {
+ public enum Result {
+ Success,
+ Error,
+ Postponed,
+ Rejected,
+ }
+
+ protected final BlockingQueue<Result> latch;
+ protected final SyncResult syncResult;
+
+ public FxAccountSyncDelegate(BlockingQueue<Result> latch, SyncResult syncResult) {
+ if (latch == null) {
+ throw new IllegalArgumentException("latch must not be null");
+ }
+ if (syncResult == null) {
+ throw new IllegalArgumentException("syncResult must not be null");
+ }
+ this.latch = latch;
+ this.syncResult = syncResult;
+ }
+
+ /**
+ * No error! Say that we made progress.
+ */
+ protected void setSyncResultSuccess() {
+ syncResult.stats.numUpdates += 1;
+ }
+
+ /**
+ * Soft error. Say that we made progress, so that Android will sync us again
+ * after exponential backoff.
+ */
+ protected void setSyncResultSoftError() {
+ syncResult.stats.numUpdates += 1;
+ syncResult.stats.numIoExceptions += 1;
+ }
+
+ /**
+ * Hard error. We don't want Android to sync us again, even if we make
+ * progress, until the user intervenes.
+ */
+ protected void setSyncResultHardError() {
+ syncResult.stats.numAuthExceptions += 1;
+ }
+
+ public void handleSuccess() {
+ setSyncResultSuccess();
+ latch.offer(Result.Success);
+ }
+
+ public void handleError(Exception e) {
+ setSyncResultSoftError();
+ latch.offer(Result.Error);
+ }
+
+ /**
+ * When the login machine terminates, we might not be in the
+ * <code>Married</code> state, and therefore we can't sync. This method
+ * messages as much to the user.
+ * <p>
+ * To avoid stopping us syncing altogether, we set a soft error rather than
+ * a hard error. In future, we would like to set a hard error if we are in,
+ * for example, the <code>Separated</code> state, and then have some user
+ * initiated activity mark the Android account as ready to sync again. This
+ * is tricky, though, so we play it safe for now.
+ *
+ * @param finalState
+ * that login machine ended in.
+ */
+ public void handleCannotSync(State finalState) {
+ setSyncResultSoftError();
+ latch.offer(Result.Error);
+ }
+
+ public void postponeSync(long millis) {
+ if (millis > 0) {
+ // delayUntil is broken: https://code.google.com/p/android/issues/detail?id=65669
+ // So we don't bother doing this. Instead, we rely on the periodic sync
+ // we schedule, and the backoff handler for the rest.
+ /*
+ Logger.warn(LOG_TAG, "Postponing sync by " + millis + "ms.");
+ syncResult.delayUntil = millis / 1000;
+ */
+ }
+ setSyncResultSoftError();
+ latch.offer(Result.Postponed);
+ }
+
+ /**
+ * Simply don't sync, without setting any error flags.
+ * This is the appropriate behavior when a routine backoff has not yet
+ * been met.
+ */
+ public void rejectSync() {
+ latch.offer(Result.Rejected);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncService.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncService.java
new file mode 100644
index 0000000000..59c06ca976
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncService.java
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.sync;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+public class FxAccountSyncService extends Service {
+ private static final Object syncAdapterLock = new Object();
+ private static FxAccountSyncAdapter syncAdapter;
+
+ @Override
+ public void onCreate() {
+ synchronized (syncAdapterLock) {
+ if (syncAdapter == null) {
+ syncAdapter = new FxAccountSyncAdapter(getApplicationContext(), true);
+ }
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return syncAdapter.getSyncAdapterBinder();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncStatusHelper.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncStatusHelper.java
new file mode 100644
index 0000000000..ca64d4f871
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncStatusHelper.java
@@ -0,0 +1,113 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.sync;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.WeakHashMap;
+
+import org.mozilla.gecko.fxa.SyncStatusListener;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.content.ContentResolver;
+import android.content.SyncStatusObserver;
+
+/**
+ * Abstract away some details of Android's SyncStatusObserver.
+ * <p>
+ * Provides a simplified sync started/sync finished delegate.
+ */
+public class FxAccountSyncStatusHelper implements SyncStatusObserver {
+ @SuppressWarnings("unused")
+ private static final String LOG_TAG = FxAccountSyncStatusHelper.class.getSimpleName();
+
+ protected static FxAccountSyncStatusHelper sInstance;
+
+ public synchronized static FxAccountSyncStatusHelper getInstance() {
+ if (sInstance == null) {
+ sInstance = new FxAccountSyncStatusHelper();
+ }
+ return sInstance;
+ }
+
+ // Used to unregister this as a listener.
+ protected Object handle;
+
+ // Maps delegates to whether their underlying Android account was syncing the
+ // last time we observed a status change.
+ protected Map<SyncStatusListener, Boolean> delegates = new WeakHashMap<SyncStatusListener, Boolean>();
+
+ @Override
+ public synchronized void onStatusChanged(int which) {
+ for (Entry<SyncStatusListener, Boolean> entry : delegates.entrySet()) {
+ final SyncStatusListener delegate = entry.getKey();
+ final AndroidFxAccount fxAccount = new AndroidFxAccount(delegate.getContext(), delegate.getAccount());
+ final boolean active = fxAccount.isCurrentlySyncing();
+ // Remember for later.
+ boolean wasActiveLastTime = entry.getValue();
+ // It's okay to update the value of an entry while iterating the entrySet.
+ entry.setValue(active);
+
+ if (active && !wasActiveLastTime) {
+ // We've started a sync.
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ delegate.onSyncStarted();
+ }
+ });
+ }
+
+ if (!active && wasActiveLastTime) {
+ // We've finished a sync.
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ delegate.onSyncFinished();
+ }
+ });
+ }
+ }
+ }
+
+ protected void addListener() {
+ final int mask = ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE;
+ if (this.handle != null) {
+ throw new IllegalStateException("Already registered this as an observer?");
+ }
+ this.handle = ContentResolver.addStatusChangeListener(mask, this);
+ }
+
+ protected void removeListener() {
+ Object handle = this.handle;
+ this.handle = null;
+ if (handle != null) {
+ ContentResolver.removeStatusChangeListener(handle);
+ }
+ }
+
+ public synchronized void startObserving(SyncStatusListener delegate) {
+ if (delegate == null) {
+ throw new IllegalArgumentException("delegate must not be null");
+ }
+ if (delegates.containsKey(delegate)) {
+ return;
+ }
+ // If we are the first delegate to the party, start listening.
+ if (delegates.isEmpty()) {
+ addListener();
+ }
+ delegates.put(delegate, Boolean.FALSE);
+ }
+
+ public synchronized void stopObserving(SyncStatusListener delegate) {
+ delegates.remove(delegate);
+ // If we are the last delegate leaving the party, stop listening.
+ if (delegates.isEmpty()) {
+ removeListener();
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/SchedulePolicy.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/SchedulePolicy.java
new file mode 100644
index 0000000000..809191f5e2
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/SchedulePolicy.java
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.sync;
+
+import org.mozilla.gecko.fxa.login.State.Action;
+import org.mozilla.gecko.sync.BackoffHandler;
+
+public interface SchedulePolicy {
+ /**
+ * Call this with the number of other clients syncing to the account.
+ */
+ public abstract void onSuccessfulSync(int otherClientsCount);
+ public abstract void onHandleFinal(Action needed);
+ public abstract void onUpgradeRequired();
+ public abstract void onUnauthorized();
+
+ /**
+ * Before a sync we typically wish to adjust our backoff policy. This cleans
+ * the slate prior to encountering a new backoff, and also functions as a rate
+ * limiter.
+ *
+ * The {@link SchedulePolicy} acts as a controller for the {@link BackoffHandler}.
+ * As a result of calling these two methods, the {@link BackoffHandler} will be
+ * mutated, and additional side-effects (such as scheduling periodic syncs) can
+ * occur.
+ *
+ * @param rateHandler the backoff handler to configure for basic rate limiting.
+ * @param backgroundHandler the backoff handler to configure for background operations.
+ */
+ public abstract void configureBackoffMillisBeforeSyncing(BackoffHandler rateHandler, BackoffHandler backgroundHandler);
+
+ /**
+ * We received an explicit backoff instruction, typically from a server.
+ *
+ * @param onlyExtend
+ * if <code>true</code>, the backoff handler will be asked to update
+ * its backoff only if the provided value is greater than the current
+ * backoff.
+ */
+ public abstract void configureBackoffMillisOnBackoff(BackoffHandler backoffHandler, long backoffMillis, boolean onlyExtend);
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/push/RegisterUserAgentResponse.java b/mobile/android/services/src/main/java/org/mozilla/gecko/push/RegisterUserAgentResponse.java
new file mode 100644
index 0000000000..3bbb7e8b42
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/push/RegisterUserAgentResponse.java
@@ -0,0 +1,19 @@
+/* -*- 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;
+
+/**
+ * Thin container for a register User-Agent response.
+ */
+public class RegisterUserAgentResponse {
+ public final String uaid;
+ public final String secret;
+
+ public RegisterUserAgentResponse(String uaid, String secret) {
+ this.uaid = uaid;
+ this.secret = secret;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/push/SubscribeChannelResponse.java b/mobile/android/services/src/main/java/org/mozilla/gecko/push/SubscribeChannelResponse.java
new file mode 100644
index 0000000000..009a7f8388
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/push/SubscribeChannelResponse.java
@@ -0,0 +1,19 @@
+/* -*- 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;
+
+/**
+ * Thin container for a subscribe channel response.
+ */
+public class SubscribeChannelResponse {
+ public final String channelID;
+ public final String endpoint;
+
+ public SubscribeChannelResponse(String channelID, String endpoint) {
+ this.channelID = channelID;
+ this.endpoint = endpoint;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClient.java b/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClient.java
new file mode 100644
index 0000000000..8edd92f9eb
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClient.java
@@ -0,0 +1,410 @@
+/* -*- 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.autopush;
+
+import android.text.TextUtils;
+
+import org.mozilla.gecko.Locales;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.push.RegisterUserAgentResponse;
+import org.mozilla.gecko.push.SubscribeChannelResponse;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BaseResourceDelegate;
+import org.mozilla.gecko.sync.net.BearerAuthHeaderProvider;
+import org.mozilla.gecko.sync.net.Resource;
+import org.mozilla.gecko.sync.net.SyncResponse;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+import java.util.Locale;
+import java.util.concurrent.Executor;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+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;
+
+
+/**
+ * Interact with the autopush endpoint HTTP API.
+ * <p/>
+ * The API is a Mozilla-proprietary interface, and not even specified to Mozilla's usual ad-hoc standards.
+ * This client is written against a work-in-progress, un-deployed upstream commit.
+ */
+public class AutopushClient {
+ protected static final String LOG_TAG = AutopushClient.class.getSimpleName();
+
+ protected static final String ACCEPT_HEADER = "application/json;charset=utf-8";
+ protected static final String TYPE = "gcm";
+
+ protected static final String JSON_KEY_UAID = "uaid";
+ protected static final String JSON_KEY_SECRET = "secret";
+ protected static final String JSON_KEY_CHANNEL_ID = "channelID";
+ protected static final String JSON_KEY_ENDPOINT = "endpoint";
+
+ protected static final String[] REGISTER_USER_AGENT_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_UAID, JSON_KEY_SECRET, JSON_KEY_CHANNEL_ID, JSON_KEY_ENDPOINT };
+ protected static final String[] REGISTER_CHANNEL_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_CHANNEL_ID, JSON_KEY_ENDPOINT };
+
+ public static final String JSON_KEY_CODE = "code";
+ public static final String JSON_KEY_ERRNO = "errno";
+ public static final String JSON_KEY_ERROR = "error";
+ public static final String JSON_KEY_MESSAGE = "message";
+
+ protected static final String[] requiredErrorStringFields = { JSON_KEY_ERROR, JSON_KEY_MESSAGE };
+ protected static final String[] requiredErrorLongFields = { JSON_KEY_CODE, JSON_KEY_ERRNO };
+
+ /**
+ * The server's URI.
+ * <p>
+ * We assume throughout that this ends with a trailing slash (and guarantee as
+ * much in the constructor).
+ */
+ public final String serverURI;
+
+ protected final Executor executor;
+
+ public AutopushClient(String serverURI, Executor executor) {
+ if (serverURI == null) {
+ throw new IllegalArgumentException("Must provide a server URI.");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("Must provide a non-null executor.");
+ }
+ this.serverURI = serverURI.endsWith("/") ? serverURI : serverURI + "/";
+ if (!this.serverURI.endsWith("/")) {
+ throw new IllegalArgumentException("Constructed serverURI must end with a trailing slash: " + this.serverURI);
+ }
+ this.executor = executor;
+ }
+
+ /**
+ * A legal autopush server URL includes a sender ID embedded into it. Extract it.
+ *
+ * @return a non-null non-empty sender ID.
+ * @throws AutopushClientException on failure.
+ */
+ public String getSenderIDFromServerURI() throws AutopushClientException {
+ // Turn "https://updates-autopush-dev.stage.mozaws.net/v1/gcm/829133274407/" into "829133274407".
+ final String[] parts = serverURI.split("/", -1); // The -1 keeps the trailing empty part.
+ if (parts.length < 3) {
+ throw new AutopushClientException("Could not get sender ID from autopush server URI: " + serverURI);
+ }
+ if (!TextUtils.isEmpty(parts[parts.length - 1])) {
+ // We guarantee a trailing slash, so we should always have an empty part at the tail.
+ throw new AutopushClientException("Could not get sender ID from autopush server URI: " + serverURI);
+ }
+ if (!TextUtils.equals("gcm", parts[parts.length - 3])) {
+ // We should always have /gcm/senderID/.
+ throw new AutopushClientException("Could not get sender ID from autopush server URI: " + serverURI);
+ }
+ final String senderID = parts[parts.length - 2];
+ if (TextUtils.isEmpty(senderID)) {
+ // Something is horribly wrong -- we have /gcm//. Abort.
+ throw new AutopushClientException("Could not get sender ID from autopush server URI: " + serverURI);
+ }
+ return senderID;
+ }
+
+ /**
+ * Process a typed value extracted from a successful response (in an
+ * endpoint-dependent way).
+ */
+ public interface RequestDelegate<T> {
+ void handleError(Exception e);
+ void handleFailure(AutopushClientException e);
+ void handleSuccess(T result);
+ }
+
+ /**
+ * Intepret a response from the autopush server.
+ * <p>
+ * Throw an appropriate exception on errors; otherwise, return the response's
+ * status code.
+ *
+ * @return response's HTTP status code.
+ * @throws AutopushClientException
+ */
+ public static int validateResponse(HttpResponse response) throws AutopushClientException {
+ final int status = response.getStatusLine().getStatusCode();
+ if (200 <= status && status <= 299) {
+ return status;
+ }
+ long code;
+ long errno;
+ String error;
+ String message;
+ String info;
+ ExtendedJSONObject body;
+ try {
+ body = new SyncStorageResponse(response).jsonObjectBody();
+ // TODO: The service doesn't do the right thing yet :(
+ // body.throwIfFieldsMissingOrMisTyped(requiredErrorStringFields, String.class);
+ body.throwIfFieldsMissingOrMisTyped(requiredErrorLongFields, Long.class);
+ // Would throw above if missing; the -1 defaults quiet NPE warnings.
+ code = body.getLong(JSON_KEY_CODE, -1);
+ errno = body.getLong(JSON_KEY_ERRNO, -1);
+ error = body.getString(JSON_KEY_ERROR);
+ message = body.getString(JSON_KEY_MESSAGE);
+ } catch (Exception e) {
+ throw new AutopushClientException.AutopushClientMalformedResponseException(response);
+ }
+ throw new AutopushClientException.AutopushClientRemoteException(response, code, errno, error, message, body);
+ }
+
+ protected <T> void invokeHandleError(final RequestDelegate<T> delegate, final Exception e) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ delegate.handleError(e);
+ }
+ });
+ }
+
+ protected <T> void post(BaseResource resource, final ExtendedJSONObject requestBody, final RequestDelegate<T> delegate) {
+ try {
+ if (requestBody == null) {
+ resource.post((HttpEntity) null);
+ } else {
+ resource.post(requestBody);
+ }
+ } catch (Exception e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+ }
+
+ /**
+ * Translate resource callbacks into request callbacks invoked on the provided
+ * executor.
+ * <p>
+ * Override <code>handleSuccess</code> to parse the body of the resource
+ * request and call the request callback. <code>handleSuccess</code> is
+ * invoked via the executor, so you don't need to delegate further.
+ */
+ protected abstract class ResourceDelegate<T> extends BaseResourceDelegate {
+ protected abstract void handleSuccess(final int status, HttpResponse response, final ExtendedJSONObject body);
+
+ protected final String secret;
+ protected final RequestDelegate<T> delegate;
+
+ /**
+ * Create a delegate for an un-authenticated resource.
+ */
+ public ResourceDelegate(final Resource resource, final String secret, final RequestDelegate<T> delegate) {
+ super(resource);
+ this.delegate = delegate;
+ this.secret = secret;
+ }
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ if (secret != null) {
+ return new BearerAuthHeaderProvider(secret);
+ }
+ return null;
+ }
+
+ @Override
+ public String getUserAgent() {
+ return FxAccountConstants.USER_AGENT;
+ }
+
+ @Override
+ public void handleHttpResponse(HttpResponse response) {
+ try {
+ final int status = validateResponse(response);
+ invokeHandleSuccess(status, response);
+ } catch (AutopushClientException e) {
+ invokeHandleFailure(e);
+ }
+ }
+
+ protected void invokeHandleFailure(final AutopushClientException e) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ delegate.handleFailure(e);
+ }
+ });
+ }
+
+ protected void invokeHandleSuccess(final int status, final HttpResponse response) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ ExtendedJSONObject body = new SyncResponse(response).jsonObjectBody();
+ ResourceDelegate.this.handleSuccess(status, response, body);
+ } catch (Exception e) {
+ delegate.handleError(e);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void handleHttpProtocolException(final ClientProtocolException e) {
+ invokeHandleError(delegate, e);
+ }
+
+ @Override
+ public void handleHttpIOException(IOException e) {
+ invokeHandleError(delegate, e);
+ }
+
+ @Override
+ public void handleTransportException(GeneralSecurityException e) {
+ invokeHandleError(delegate, e);
+ }
+
+ @Override
+ public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
+ super.addHeaders(request, client);
+
+ // The basics.
+ final Locale locale = Locale.getDefault();
+ request.addHeader(HttpHeaders.ACCEPT_LANGUAGE, Locales.getLanguageTag(locale));
+ request.addHeader(HttpHeaders.ACCEPT, ACCEPT_HEADER);
+ }
+ }
+
+ public void registerUserAgent(final String token, RequestDelegate<RegisterUserAgentResponse> delegate) {
+ BaseResource resource;
+ try {
+ resource = new BaseResource(new URI(serverURI + "registration"));
+ } catch (URISyntaxException e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ resource.delegate = new ResourceDelegate<RegisterUserAgentResponse>(resource, null, delegate) {
+ @Override
+ public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+ try {
+ body.throwIfFieldsMissingOrMisTyped(REGISTER_USER_AGENT_RESPONSE_REQUIRED_STRING_FIELDS, String.class);
+ final String uaid = body.getString(JSON_KEY_UAID);
+ final String secret = body.getString(JSON_KEY_SECRET);
+ delegate.handleSuccess(new RegisterUserAgentResponse(uaid, secret));
+ return;
+ } catch (Exception e) {
+ delegate.handleError(e);
+ return;
+ }
+ }
+ };
+
+ final ExtendedJSONObject body = new ExtendedJSONObject();
+ body.put("type", TYPE);
+ body.put("token", token);
+
+ resource.post(body);
+ }
+
+ public void reregisterUserAgent(final String uaid, final String secret, final String token, RequestDelegate<Void> delegate) {
+ final BaseResource resource;
+ try {
+ resource = new BaseResource(new URI(serverURI + "registration/" + uaid));
+ } catch (Exception e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ resource.delegate = new ResourceDelegate<Void>(resource, secret, delegate) {
+ @Override
+ public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+ try {
+ delegate.handleSuccess(null);
+ return;
+ } catch (Exception e) {
+ delegate.handleError(e);
+ return;
+ }
+ }
+ };
+
+ final ExtendedJSONObject body = new ExtendedJSONObject();
+ body.put("type", TYPE);
+ body.put("token", token);
+
+ resource.put(body);
+ }
+
+
+ public void subscribeChannel(final String uaid, final String secret, final String appServerKey, RequestDelegate<SubscribeChannelResponse> delegate) {
+ final BaseResource resource;
+ try {
+ resource = new BaseResource(new URI(serverURI + "registration/" + uaid + "/subscription"));
+ } catch (Exception e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ resource.delegate = new ResourceDelegate<SubscribeChannelResponse>(resource, secret, delegate) {
+ @Override
+ public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+ try {
+ body.throwIfFieldsMissingOrMisTyped(REGISTER_CHANNEL_RESPONSE_REQUIRED_STRING_FIELDS, String.class);
+ final String channelID = body.getString(JSON_KEY_CHANNEL_ID);
+ final String endpoint = body.getString(JSON_KEY_ENDPOINT);
+ delegate.handleSuccess(new SubscribeChannelResponse(channelID, endpoint));
+ return;
+ } catch (Exception e) {
+ delegate.handleError(e);
+ return;
+ }
+ }
+ };
+
+ final ExtendedJSONObject body = new ExtendedJSONObject();
+ body.put("key", appServerKey);
+ resource.post(body);
+ }
+
+ public void unsubscribeChannel(final String uaid, final String secret, final String channelID, RequestDelegate<Void> delegate) {
+ final BaseResource resource;
+ try {
+ resource = new BaseResource(new URI(serverURI + "registration/" + uaid + "/subscription/" + channelID));
+ } catch (Exception e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ resource.delegate = new ResourceDelegate<Void>(resource, secret, delegate) {
+ @Override
+ public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+ delegate.handleSuccess(null);
+ }
+ };
+
+ resource.delete();
+ }
+
+ public void unregisterUserAgent(final String uaid, final String secret, RequestDelegate<Void> delegate) {
+ final BaseResource resource;
+ try {
+ resource = new BaseResource(new URI(serverURI + "registration/" + uaid));
+ } catch (Exception e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ resource.delegate = new ResourceDelegate<Void>(resource, secret, delegate) {
+ @Override
+ public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+ delegate.handleSuccess(null);
+ }
+ };
+
+ resource.delete();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClientException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClientException.java
new file mode 100644
index 0000000000..e3fda7a45f
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClientException.java
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.autopush;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.HTTPFailureException;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+public class AutopushClientException extends Exception {
+ private static final long serialVersionUID = 7953459541558266500L;
+
+ public AutopushClientException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ public AutopushClientException(Exception e) {
+ super(e);
+ }
+
+ public boolean isTransientError() {
+ return false;
+ }
+
+ public static class AutopushClientRemoteException extends AutopushClientException {
+ private static final long serialVersionUID = 2209313149952001000L;
+
+ public final HttpResponse response;
+ public final long httpStatusCode;
+ public final long apiErrorNumber;
+ public final String error;
+ public final String message;
+ public final ExtendedJSONObject body;
+
+ public AutopushClientRemoteException(HttpResponse response, long httpStatusCode, long apiErrorNumber, String error, String message, ExtendedJSONObject body) {
+ super(new HTTPFailureException(new SyncStorageResponse(response)));
+ if (body == null) {
+ throw new IllegalArgumentException("body must not be null");
+ }
+ this.response = response;
+ this.httpStatusCode = httpStatusCode;
+ this.apiErrorNumber = apiErrorNumber;
+ this.error = error;
+ this.message = message;
+ this.body = body;
+ }
+
+ @Override
+ public String toString() {
+ return "<AutopushClientRemoteException " + this.httpStatusCode + " [" + this.apiErrorNumber + "]: " + this.message + ">";
+ }
+
+ public boolean isInvalidAuthentication() {
+ return httpStatusCode == HttpStatus.SC_UNAUTHORIZED;
+ }
+
+ public boolean isNotFound() {
+ return httpStatusCode == HttpStatus.SC_NOT_FOUND;
+ }
+
+ public boolean isGone() {
+ return httpStatusCode == HttpStatus.SC_GONE;
+ }
+
+ @Override
+ public boolean isTransientError() {
+ return httpStatusCode >= 500;
+ }
+ }
+
+ public static class AutopushClientMalformedResponseException extends AutopushClientRemoteException {
+ private static final long serialVersionUID = 2209313149952001909L;
+
+ public AutopushClientMalformedResponseException(HttpResponse response) {
+ super(response, 0, 999, "Response malformed", "Response malformed", new ExtendedJSONObject());
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/AlreadySyncingException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/AlreadySyncingException.java
new file mode 100644
index 0000000000..75eb5ad370
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/AlreadySyncingException.java
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
+
+import android.content.SyncResult;
+
+public class AlreadySyncingException extends SyncException {
+ Stage inState;
+ public AlreadySyncingException(Stage currentState) {
+ inState = currentState;
+ }
+
+ private static final long serialVersionUID = -5647548462539009893L;
+
+ @Override
+ public void updateStats(GlobalSession globalSession, SyncResult syncResult) {
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/BackoffHandler.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/BackoffHandler.java
new file mode 100644
index 0000000000..abb880621c
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/BackoffHandler.java
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+
+public interface BackoffHandler {
+ public long getEarliestNextRequest();
+
+ /**
+ * Provide a timestamp in millis before which we shouldn't sync again.
+ * Overrides any existing value.
+ *
+ * @param next
+ * a timestamp in milliseconds.
+ */
+ public void setEarliestNextRequest(long next);
+
+ /**
+ * Provide a timestamp in millis before which we shouldn't sync again. Only
+ * change our persisted value if it's later than the existing time.
+ *
+ * @param next
+ * a timestamp in milliseconds.
+ */
+ public void extendEarliestNextRequest(long next);
+
+ /**
+ * Return the number of milliseconds until we're allowed to sync again,
+ * or 0 if now is fine.
+ */
+ public long delayMilliseconds();
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/BadRequiredFieldJSONException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/BadRequiredFieldJSONException.java
new file mode 100644
index 0000000000..3db93652d7
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/BadRequiredFieldJSONException.java
@@ -0,0 +1,5 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CollectionKeys.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CollectionKeys.java
new file mode 100644
index 0000000000..1fd363bcb0
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CollectionKeys.java
@@ -0,0 +1,199 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.json.simple.JSONArray;
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+
+public class CollectionKeys {
+ private KeyBundle defaultKeyBundle = null;
+ private final HashMap<String, KeyBundle> collectionKeyBundles = new HashMap<String, KeyBundle>();
+
+ /**
+ * Randomly generate a basic CollectionKeys object.
+ * @throws CryptoException
+ */
+ public static CollectionKeys generateCollectionKeys() throws CryptoException {
+ CollectionKeys ck = new CollectionKeys();
+ ck.clear();
+ ck.defaultKeyBundle = KeyBundle.withRandomKeys();
+ // TODO: eventually we would like to keep per-collection keys, just generate
+ // new ones as appropriate.
+ return ck;
+ }
+
+ public KeyBundle defaultKeyBundle() throws NoCollectionKeysSetException {
+ if (this.defaultKeyBundle == null) {
+ throw new NoCollectionKeysSetException();
+ }
+ return this.defaultKeyBundle;
+ }
+
+ public boolean keyBundleForCollectionIsNotDefault(String collection) {
+ return collectionKeyBundles.containsKey(collection);
+ }
+
+ public KeyBundle keyBundleForCollection(String collection)
+ throws NoCollectionKeysSetException {
+ if (this.defaultKeyBundle == null) {
+ throw new NoCollectionKeysSetException();
+ }
+ if (keyBundleForCollectionIsNotDefault(collection)) {
+ return collectionKeyBundles.get(collection);
+ }
+ return this.defaultKeyBundle;
+ }
+
+ /**
+ * Take a pair of values in a JSON array, handing them off to KeyBundle to
+ * produce a usable keypair.
+ */
+ private static KeyBundle arrayToKeyBundle(JSONArray array) throws UnsupportedEncodingException {
+ String encKeyStr = (String) array.get(0);
+ String hmacKeyStr = (String) array.get(1);
+ return KeyBundle.fromBase64EncodedKeys(encKeyStr, hmacKeyStr);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static JSONArray keyBundleToArray(KeyBundle bundle) {
+ // Generate JSON.
+ JSONArray keysArray = new JSONArray();
+ keysArray.add(new String(Base64.encodeBase64(bundle.getEncryptionKey())));
+ keysArray.add(new String(Base64.encodeBase64(bundle.getHMACKey())));
+ return keysArray;
+ }
+
+ private ExtendedJSONObject asRecordContents() throws NoCollectionKeysSetException {
+ ExtendedJSONObject json = new ExtendedJSONObject();
+ json.put("id", "keys");
+ json.put("collection", "crypto");
+ json.put("default", keyBundleToArray(this.defaultKeyBundle()));
+ ExtendedJSONObject colls = new ExtendedJSONObject();
+ for (Entry<String, KeyBundle> collKey : collectionKeyBundles.entrySet()) {
+ colls.put(collKey.getKey(), keyBundleToArray(collKey.getValue()));
+ }
+ json.put("collections", colls);
+ return json;
+ }
+
+ public CryptoRecord asCryptoRecord() throws NoCollectionKeysSetException {
+ ExtendedJSONObject payload = this.asRecordContents();
+ CryptoRecord record = new CryptoRecord(payload);
+ record.collection = "crypto";
+ record.guid = "keys";
+ record.deleted = false;
+ return record;
+ }
+
+ /**
+ * Set my key bundle and collection keys with the given key bundle and data
+ * (possibly decrypted) from the given record.
+ *
+ * @param keys
+ * A "crypto/keys" <code>CryptoRecord</code>, encrypted with
+ * <code>syncKeyBundle</code> if <code>syncKeyBundle</code> is non-null.
+ * @param syncKeyBundle
+ * If non-null, the sync key bundle to decrypt <code>keys</code> with.
+ */
+ public void setKeyPairsFromWBO(CryptoRecord keys, KeyBundle syncKeyBundle)
+ throws CryptoException, IOException, NonObjectJSONException {
+ if (keys == null) {
+ throw new IllegalArgumentException("cannot set key pairs from null record");
+ }
+ if (syncKeyBundle != null) {
+ keys.keyBundle = syncKeyBundle;
+ keys.decrypt();
+ }
+ ExtendedJSONObject cleartext = keys.payload;
+ KeyBundle defaultKey = arrayToKeyBundle((JSONArray) cleartext.get("default"));
+
+ ExtendedJSONObject collections = cleartext.getObject("collections");
+ HashMap<String, KeyBundle> collectionKeys = new HashMap<String, KeyBundle>();
+ for (Entry<String, Object> pair : collections.entrySet()) {
+ KeyBundle bundle = arrayToKeyBundle((JSONArray) pair.getValue());
+ collectionKeys.put(pair.getKey(), bundle);
+ }
+
+ this.collectionKeyBundles.clear();
+ this.collectionKeyBundles.putAll(collectionKeys);
+ this.defaultKeyBundle = defaultKey;
+ }
+
+ public void setKeyBundleForCollection(String collection, KeyBundle keys) {
+ this.collectionKeyBundles.put(collection, keys);
+ }
+
+ public void setDefaultKeyBundle(KeyBundle keys) {
+ this.defaultKeyBundle = keys;
+ }
+
+ public void clear() {
+ this.defaultKeyBundle = null;
+ this.collectionKeyBundles.clear();
+ }
+
+ /**
+ * Return set of collections where key is either missing from one collection
+ * or not the same in both collections.
+ * <p>
+ * Does not check for different default keys.
+ */
+ public static Set<String> differences(CollectionKeys a, CollectionKeys b) {
+ Set<String> differences = new HashSet<String>();
+ Set<String> collections = new HashSet<String>(a.collectionKeyBundles.keySet());
+ collections.addAll(b.collectionKeyBundles.keySet());
+
+ // Iterate through one collection, collecting missing and differences.
+ for (String collection : collections) {
+ KeyBundle keyA;
+ KeyBundle keyB;
+ try {
+ keyA = a.keyBundleForCollection(collection); // Will return default key as appropriate.
+ keyB = b.keyBundleForCollection(collection); // Will return default key as appropriate.
+ } catch (NoCollectionKeysSetException e) {
+ differences.add(collection);
+ continue;
+ }
+ // keyA and keyB are not null at this point.
+ if (!keyA.equals(keyB)) {
+ differences.add(collection);
+ }
+ }
+
+ return differences;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof CollectionKeys)) {
+ return false;
+ }
+ CollectionKeys other = (CollectionKeys) o;
+ try {
+ // It would be nice to use map equality here, but there can be map entries
+ // where the key is the default key that should compare equal to a missing
+ // map entry. Therefore, we always compute the set of differences.
+ return defaultKeyBundle().equals(other.defaultKeyBundle()) &&
+ CollectionKeys.differences(this, other).isEmpty();
+ } catch (NoCollectionKeysSetException e) {
+ // If either default key bundle is not set, we'll say the bundles are not equal.
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return super.hashCode();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CommandProcessor.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CommandProcessor.java
new file mode 100644
index 0000000000..371603de59
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CommandProcessor.java
@@ -0,0 +1,261 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
+import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Process commands received from Sync clients.
+ * <p>
+ * We need a command processor at two different times:
+ * <ol>
+ * <li>We execute commands during the "clients" engine stage of a Sync. Each
+ * command takes a <code>GlobalSession</code> instance as a parameter.</li>
+ * <li>We queue commands to be executed or propagated to other Sync clients
+ * during an activity completely unrelated to a sync</li>
+ * </ol>
+ * To provide a processor for both these time frames, we maintain a static
+ * long-lived singleton.
+ */
+public class CommandProcessor {
+ private static final String LOG_TAG = "Command";
+ private static final AtomicInteger currentId = new AtomicInteger();
+ protected ConcurrentHashMap<String, CommandRunner> commands = new ConcurrentHashMap<String, CommandRunner>();
+
+ private final static CommandProcessor processor = new CommandProcessor();
+
+ /**
+ * Get the global singleton command processor.
+ *
+ * @return the singleton processor.
+ */
+ public static CommandProcessor getProcessor() {
+ return processor;
+ }
+
+ public static class Command {
+ public final String commandType;
+ public final JSONArray args;
+ private List<String> argsList;
+
+ public Command(String commandType, JSONArray args) {
+ this.commandType = commandType;
+ this.args = args;
+ }
+
+ /**
+ * Get list of arguments as strings. Individual arguments may be null.
+ *
+ * @return list of strings.
+ */
+ public synchronized List<String> getArgsList() {
+ if (argsList == null) {
+ ArrayList<String> argsList = new ArrayList<String>(args.size());
+
+ for (int i = 0; i < args.size(); i++) {
+ final Object arg = args.get(i);
+ if (arg == null) {
+ argsList.add(null);
+ continue;
+ }
+ argsList.add(arg.toString());
+ }
+ this.argsList = argsList;
+ }
+ return this.argsList;
+ }
+
+ @SuppressWarnings("unchecked")
+ public JSONObject asJSONObject() {
+ JSONObject out = new JSONObject();
+ out.put("command", this.commandType);
+ out.put("args", this.args);
+ return out;
+ }
+ }
+
+ /**
+ * Register a command.
+ * <p>
+ * Any existing registration is overwritten.
+ *
+ * @param commandType
+ * the name of the command, i.e., "displayURI".
+ * @param command
+ * the <code>CommandRunner</code> instance that should handle the
+ * command.
+ */
+ public void registerCommand(String commandType, CommandRunner command) {
+ commands.put(commandType, command);
+ }
+
+ /**
+ * Process a command in the context of the given global session.
+ *
+ * @param session
+ * the <code>GlobalSession</code> instance currently executing.
+ * @param unparsedCommand
+ * command as a <code>ExtendedJSONObject</code> instance.
+ */
+ public void processCommand(final GlobalSession session, ExtendedJSONObject unparsedCommand) {
+ Command command = parseCommand(unparsedCommand);
+ if (command == null) {
+ Logger.debug(LOG_TAG, "Invalid command: " + unparsedCommand + " will not be processed.");
+ return;
+ }
+
+ CommandRunner executableCommand = commands.get(command.commandType);
+ if (executableCommand == null) {
+ Logger.debug(LOG_TAG, "Command \"" + command.commandType + "\" not registered and will not be processed.");
+ return;
+ }
+
+ executableCommand.executeCommand(session, command.getArgsList());
+ }
+
+ /**
+ * Parse a JSON command into a ParsedCommand object for easier handling.
+ *
+ * @param unparsedCommand - command as ExtendedJSONObject
+ * @return - null if command is invalid, else return ParsedCommand with
+ * no null attributes.
+ */
+ protected static Command parseCommand(ExtendedJSONObject unparsedCommand) {
+ String type = (String) unparsedCommand.get("command");
+ if (type == null) {
+ return null;
+ }
+
+ try {
+ JSONArray unparsedArgs = unparsedCommand.getArray("args");
+ if (unparsedArgs == null) {
+ return null;
+ }
+
+ return new Command(type, unparsedArgs);
+ } catch (NonArrayJSONException e) {
+ Logger.debug(LOG_TAG, "Unable to parse args array. Invalid command");
+ return null;
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public void sendURIToClientForDisplay(String uri, String clientID, String title, String sender, Context context) {
+ Logger.info(LOG_TAG, "Sending URI to client " + clientID + ".");
+ if (Logger.LOG_PERSONAL_INFORMATION) {
+ Logger.pii(LOG_TAG, "URI is " + uri + "; title is '" + title + "'.");
+ }
+
+ final JSONArray args = new JSONArray();
+ args.add(uri);
+ args.add(sender);
+ args.add(title);
+
+ final Command displayURICommand = new Command("displayURI", args);
+ this.sendCommand(clientID, displayURICommand, context);
+ }
+
+ /**
+ * Validates and sends a command to a client or all clients.
+ *
+ * Calling this does not actually sync the command data to the server. If the
+ * client already has the command/args pair, it won't receive a duplicate
+ * command.
+ *
+ * @param clientID
+ * Client ID to send command to. If null, send to all remote
+ * clients.
+ * @param command
+ * Command to invoke on remote clients
+ */
+ public void sendCommand(String clientID, Command command, Context context) {
+ Logger.debug(LOG_TAG, "In sendCommand.");
+
+ CommandRunner commandData = commands.get(command.commandType);
+
+ // Don't send commands that we don't know about.
+ if (commandData == null) {
+ Logger.error(LOG_TAG, "Unknown command to send: " + command);
+ return;
+ }
+
+ // Don't send a command with the wrong number of arguments.
+ if (!commandData.argumentsAreValid(command.getArgsList())) {
+ Logger.error(LOG_TAG, "Expected " + commandData.argCount + " args for '" +
+ command + "', but got " + command.args);
+ return;
+ }
+
+ if (clientID != null) {
+ this.sendCommandToClient(clientID, command, context);
+ return;
+ }
+
+ ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(context);
+ try {
+ Map<String, ClientRecord> clientMap = db.fetchAllClients();
+ for (ClientRecord client : clientMap.values()) {
+ this.sendCommandToClient(client.guid, command, context);
+ }
+ } catch (NullCursorException e) {
+ Logger.error(LOG_TAG, "NullCursorException when fetching all GUIDs");
+ } finally {
+ db.close();
+ }
+ }
+
+ protected void sendCommandToClient(String clientID, Command command, Context context) {
+ Logger.info(LOG_TAG, "Sending " + command.commandType + " to " + clientID);
+
+ ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(context);
+ try {
+ db.store(clientID, command);
+ } catch (NullCursorException e) {
+ Logger.error(LOG_TAG, "NullCursorException: Unable to send command.");
+ } finally {
+ db.close();
+ }
+ }
+
+ public static void displayURI(final List<String> args, final Context context) {
+ // We trust the client sender that these exist.
+ final String uri = args.get(0);
+ final String clientId = args.get(1);
+ Logger.pii(LOG_TAG, "Received a URI for display: " + uri + " from " + clientId);
+
+ if (uri == null) {
+ Logger.pii(LOG_TAG, "URI is null – ignoring");
+ return;
+ }
+
+ String title = null;
+ if (args.size() == 3) {
+ title = args.get(2);
+ }
+
+ final Intent sendTabNotificationIntent = new Intent();
+ sendTabNotificationIntent.setClassName(context, BrowserContract.TAB_RECEIVED_SERVICE_CLASS_NAME);
+ sendTabNotificationIntent.setData(Uri.parse(uri));
+ sendTabNotificationIntent.putExtra(Intent.EXTRA_TITLE, title);
+ sendTabNotificationIntent.putExtra(BrowserContract.EXTRA_CLIENT_GUID, clientId);
+ final ComponentName componentName = context.startService(sendTabNotificationIntent);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CommandRunner.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CommandRunner.java
new file mode 100644
index 0000000000..c7a0f17623
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CommandRunner.java
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import java.util.List;
+
+public abstract class CommandRunner {
+ public final int argCount;
+
+ public CommandRunner(int argCount) {
+ this.argCount = argCount;
+ }
+
+ public abstract void executeCommand(GlobalSession session, List<String> args);
+
+ public boolean argumentsAreValid(List<String> args) {
+ return args != null &&
+ args.size() == argCount;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CredentialException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CredentialException.java
new file mode 100644
index 0000000000..f9004e14ca
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CredentialException.java
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import android.content.SyncResult;
+
+/**
+ * There was a problem with the Sync account's credentials: bad username,
+ * missing password, malformed sync key, etc.
+ */
+public abstract class CredentialException extends SyncException {
+ private static final long serialVersionUID = 833010553314100538L;
+
+ public CredentialException() {
+ super();
+ }
+
+ public CredentialException(final Throwable e) {
+ super(e);
+ }
+
+ @Override
+ public void updateStats(GlobalSession globalSession, SyncResult syncResult) {
+ syncResult.stats.numAuthExceptions += 1;
+ }
+
+ /**
+ * No credentials at all.
+ */
+ public static class MissingAllCredentialsException extends CredentialException {
+ private static final long serialVersionUID = 3763937096217604611L;
+
+ public MissingAllCredentialsException() {
+ super();
+ }
+
+ public MissingAllCredentialsException(final Throwable e) {
+ super(e);
+ }
+ }
+
+ /**
+ * Some credential is missing.
+ */
+ public static class MissingCredentialException extends CredentialException {
+ private static final long serialVersionUID = -7543031216547596248L;
+
+ public final String missingCredential;
+
+ public MissingCredentialException(final String missingCredential) {
+ this.missingCredential = missingCredential;
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CryptoRecord.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CryptoRecord.java
new file mode 100644
index 0000000000..65563d3447
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CryptoRecord.java
@@ -0,0 +1,255 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+import org.json.simple.JSONObject;
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.CryptoInfo;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.crypto.MissingCryptoInputException;
+import org.mozilla.gecko.sync.crypto.NoKeyBundleException;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+import org.mozilla.gecko.sync.repositories.domain.RecordParseException;
+
+/**
+ * A Sync crypto record has:
+ *
+ * <ul>
+ * <li>a collection of fields which are not encrypted (id and collection);</il>
+ * <li>a set of metadata fields (index, modified, ttl);</il>
+ * <li>a payload, which is encrypted and decrypted on request.</il>
+ * </ul>
+ *
+ * The payload flips between being a blob of JSON with hmac/IV/ciphertext
+ * attributes and the cleartext itself.
+ *
+ * Until there's some benefit to the abstraction, we're simply going to call
+ * this <code>CryptoRecord</code>.
+ *
+ * <code>CryptoRecord</code> uses <code>CryptoInfo</code> to do the actual
+ * encryption and decryption.
+ */
+public class CryptoRecord extends Record {
+
+ // JSON related constants.
+ private static final String KEY_ID = "id";
+ private static final String KEY_COLLECTION = "collection";
+ private static final String KEY_PAYLOAD = "payload";
+ private static final String KEY_MODIFIED = "modified";
+ private static final String KEY_SORTINDEX = "sortindex";
+ private static final String KEY_TTL = "ttl";
+ private static final String KEY_CIPHERTEXT = "ciphertext";
+ private static final String KEY_HMAC = "hmac";
+ private static final String KEY_IV = "IV";
+
+ /**
+ * Helper method for doing actual decryption.
+ *
+ * Input: JSONObject containing a valid payload (cipherText, IV, HMAC),
+ * KeyBundle with keys for decryption. Output: byte[] clearText
+ * @throws CryptoException
+ * @throws UnsupportedEncodingException
+ */
+ private static byte[] decryptPayload(ExtendedJSONObject payload, KeyBundle keybundle) throws CryptoException, UnsupportedEncodingException {
+ byte[] ciphertext = Base64.decodeBase64(((String) payload.get(KEY_CIPHERTEXT)).getBytes("UTF-8"));
+ byte[] iv = Base64.decodeBase64(((String) payload.get(KEY_IV)).getBytes("UTF-8"));
+ byte[] hmac = Utils.hex2Byte((String) payload.get(KEY_HMAC));
+
+ return CryptoInfo.decrypt(ciphertext, iv, hmac, keybundle).getMessage();
+ }
+
+ // The encrypted JSON body object.
+ // The decrypted JSON body object. Fields are copied from `body`.
+
+ public ExtendedJSONObject payload;
+ public KeyBundle keyBundle;
+
+ /**
+ * Don't forget to set cleartext or body!
+ */
+ public CryptoRecord() {
+ super(null, null, 0, false);
+ }
+
+ public CryptoRecord(ExtendedJSONObject payload) {
+ super(null, null, 0, false);
+ if (payload == null) {
+ throw new IllegalArgumentException(
+ "No payload provided to CryptoRecord constructor.");
+ }
+ this.payload = payload;
+ }
+
+ public CryptoRecord(String jsonString) throws IOException, NonObjectJSONException {
+
+ this(new ExtendedJSONObject(jsonString));
+ }
+
+ /**
+ * Create a new CryptoRecord with the same metadata as an existing record.
+ *
+ * @param source
+ */
+ public CryptoRecord(Record source) {
+ super(source.guid, source.collection, source.lastModified, source.deleted);
+ this.ttl = source.ttl;
+ }
+
+ @Override
+ public Record copyWithIDs(String guid, long androidID) {
+ CryptoRecord out = new CryptoRecord(this);
+ out.guid = guid;
+ out.androidID = androidID;
+ out.sortIndex = this.sortIndex;
+ out.ttl = this.ttl;
+ out.payload = (this.payload == null) ? null : new ExtendedJSONObject(this.payload.object);
+ out.keyBundle = this.keyBundle; // TODO: copy me?
+ return out;
+ }
+
+ /**
+ * Take a whole record as JSON -- i.e., something like
+ *
+ * {"payload": "{...}", "id":"foobarbaz"}
+ *
+ * and turn it into a CryptoRecord object.
+ *
+ * @param jsonRecord
+ * @return
+ * A CryptoRecord that encapsulates the provided record.
+ *
+ * @throws NonObjectJSONException
+ * @throws IOException
+ */
+ public static CryptoRecord fromJSONRecord(String jsonRecord)
+ throws NonObjectJSONException, IOException, RecordParseException {
+ byte[] bytes = jsonRecord.getBytes("UTF-8");
+ ExtendedJSONObject object = ExtendedJSONObject.parseUTF8AsJSONObject(bytes);
+
+ return CryptoRecord.fromJSONRecord(object);
+ }
+
+ // TODO: defensive programming.
+ public static CryptoRecord fromJSONRecord(ExtendedJSONObject jsonRecord)
+ throws IOException, NonObjectJSONException, RecordParseException {
+ String id = (String) jsonRecord.get(KEY_ID);
+ String collection = (String) jsonRecord.get(KEY_COLLECTION);
+ String jsonEncodedPayload = (String) jsonRecord.get(KEY_PAYLOAD);
+
+ ExtendedJSONObject payload = new ExtendedJSONObject(jsonEncodedPayload);
+
+ CryptoRecord record = new CryptoRecord(payload);
+ record.guid = id;
+ record.collection = collection;
+ if (jsonRecord.containsKey(KEY_MODIFIED)) {
+ Long timestamp = jsonRecord.getTimestamp(KEY_MODIFIED);
+ if (timestamp == null) {
+ throw new RecordParseException("timestamp could not be parsed");
+ }
+ record.lastModified = timestamp;
+ }
+ if (jsonRecord.containsKey(KEY_SORTINDEX)) {
+ // getLong tries to cast to Long, and might return null. We catch all
+ // exceptions, just to be safe.
+ try {
+ record.sortIndex = jsonRecord.getLong(KEY_SORTINDEX);
+ } catch (Exception e) {
+ throw new RecordParseException("timestamp could not be parsed");
+ }
+ }
+ if (jsonRecord.containsKey(KEY_TTL)) {
+ // TTLs are never returned by the sync server, so should never be true if
+ // the record was fetched.
+ try {
+ record.ttl = jsonRecord.getLong(KEY_TTL);
+ } catch (Exception e) {
+ throw new RecordParseException("TTL could not be parsed");
+ }
+ }
+ // TODO: deleted?
+ return record;
+ }
+
+ public void setKeyBundle(KeyBundle bundle) {
+ this.keyBundle = bundle;
+ }
+
+ public CryptoRecord decrypt() throws CryptoException, IOException, NonObjectJSONException {
+ if (keyBundle == null) {
+ throw new NoKeyBundleException();
+ }
+
+ // Check that payload contains all pieces for crypto.
+ if (!payload.containsKey(KEY_CIPHERTEXT) ||
+ !payload.containsKey(KEY_IV) ||
+ !payload.containsKey(KEY_HMAC)) {
+ throw new MissingCryptoInputException();
+ }
+
+ // There's no difference between handling the crypto/keys object and
+ // anything else; we just get this.keyBundle from a different source.
+ byte[] cleartext = decryptPayload(payload, keyBundle);
+ payload = ExtendedJSONObject.parseUTF8AsJSONObject(cleartext);
+ return this;
+ }
+
+ public CryptoRecord encrypt() throws CryptoException, UnsupportedEncodingException {
+ if (this.keyBundle == null) {
+ throw new NoKeyBundleException();
+ }
+ String cleartext = payload.toJSONString();
+ byte[] cleartextBytes = cleartext.getBytes("UTF-8");
+ CryptoInfo info = CryptoInfo.encrypt(cleartextBytes, keyBundle);
+ String message = new String(Base64.encodeBase64(info.getMessage()));
+ String iv = new String(Base64.encodeBase64(info.getIV()));
+ String hmac = Utils.byte2Hex(info.getHMAC());
+ ExtendedJSONObject ciphertext = new ExtendedJSONObject();
+ ciphertext.put(KEY_CIPHERTEXT, message);
+ ciphertext.put(KEY_HMAC, hmac);
+ ciphertext.put(KEY_IV, iv);
+ this.payload = ciphertext;
+ return this;
+ }
+
+ @Override
+ public void initFromEnvelope(CryptoRecord payload) {
+ throw new IllegalStateException("Can't do this with a CryptoRecord.");
+ }
+
+ @Override
+ public CryptoRecord getEnvelope() {
+ throw new IllegalStateException("Can't do this with a CryptoRecord.");
+ }
+
+ @Override
+ protected void populatePayload(ExtendedJSONObject payload) {
+ throw new IllegalStateException("Can't do this with a CryptoRecord.");
+ }
+
+ @Override
+ protected void initFromPayload(ExtendedJSONObject payload) {
+ throw new IllegalStateException("Can't do this with a CryptoRecord.");
+ }
+
+ // TODO: this only works with encrypted object, and has other limitations.
+ public JSONObject toJSONObject() {
+ ExtendedJSONObject o = new ExtendedJSONObject();
+ o.put(KEY_PAYLOAD, payload.toJSONString());
+ o.put(KEY_ID, this.guid);
+ if (this.ttl > 0) {
+ o.put(KEY_TTL, this.ttl);
+ }
+ return o.object;
+ }
+
+ @Override
+ public String toJSONString() {
+ return toJSONObject().toJSONString();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/DelayedWorkTracker.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/DelayedWorkTracker.java
new file mode 100644
index 0000000000..ddcb5411c0
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/DelayedWorkTracker.java
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import org.mozilla.gecko.background.common.log.Logger;
+
+/**
+ * A little class to allow us to maintain a count of extant
+ * things (in our case, callbacks that need to fire), and
+ * some work that we want done when that count hits 0.
+ *
+ * @author rnewman
+ *
+ */
+public class DelayedWorkTracker {
+ private static final String LOG_TAG = "DelayedWorkTracker";
+ protected Runnable workItem = null;
+ protected int outstandingCount = 0;
+
+ public int incrementOutstanding() {
+ Logger.trace(LOG_TAG, "Incrementing outstanding.");
+ synchronized(this) {
+ return ++outstandingCount;
+ }
+ }
+ public int decrementOutstanding() {
+ Logger.trace(LOG_TAG, "Decrementing outstanding.");
+ Runnable job = null;
+ int count;
+ synchronized(this) {
+ if ((count = --outstandingCount) == 0 &&
+ workItem != null) {
+ job = workItem;
+ workItem = null;
+ } else {
+ return count;
+ }
+ }
+ job.run();
+ // In case it's changed.
+ return getOutstandingOperations();
+ }
+ public int getOutstandingOperations() {
+ synchronized(this) {
+ return outstandingCount;
+ }
+ }
+ public void delayWorkItem(Runnable item) {
+ Logger.trace(LOG_TAG, "delayWorkItem.");
+ boolean runnableNow = false;
+ synchronized(this) {
+ Logger.trace(LOG_TAG, "outstandingCount: " + outstandingCount);
+ if (outstandingCount == 0) {
+ runnableNow = true;
+ } else {
+ if (workItem != null) {
+ throw new IllegalStateException("Work item already set!");
+ }
+ workItem = item;
+ }
+ }
+ if (runnableNow) {
+ Logger.trace(LOG_TAG, "Running item now.");
+ item.run();
+ }
+ }
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/EngineSettings.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/EngineSettings.java
new file mode 100644
index 0000000000..0358160886
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/EngineSettings.java
@@ -0,0 +1,31 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+public class EngineSettings {
+ public final String syncID;
+ public final int version;
+
+ public EngineSettings(final String syncID, final int version) {
+ this.syncID = syncID;
+ this.version = version;
+ }
+
+ public EngineSettings(ExtendedJSONObject object) {
+ try {
+ this.syncID = object.getString("syncID");
+ this.version = object.getIntegerSafely("version");
+ } catch (Exception e ) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public ExtendedJSONObject toJSONObject() {
+ ExtendedJSONObject json = new ExtendedJSONObject();
+ json.put("syncID", syncID);
+ json.put("version", version);
+ return json;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/ExtendedJSONObject.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/ExtendedJSONObject.java
new file mode 100644
index 0000000000..f5fac00099
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/ExtendedJSONObject.java
@@ -0,0 +1,426 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.sync.UnexpectedJSONException.BadRequiredFieldJSONException;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * Extend JSONObject to do little things, like, y'know, accessing members.
+ *
+ * @author rnewman
+ *
+ */
+public class ExtendedJSONObject {
+
+ public JSONObject object;
+
+ /**
+ * Return a <code>JSONParser</code> instance for immediate use.
+ * <p>
+ * <code>JSONParser</code> is not thread-safe, so we return a new instance
+ * each call. This is extremely inefficient in execution time and especially
+ * memory use -- each instance allocates a 16kb temporary buffer -- and we
+ * hope to improve matters eventually.
+ */
+ protected static JSONParser getJSONParser() {
+ return new JSONParser();
+ }
+
+ /**
+ * Parse a JSON encoded string.
+ *
+ * @param in <code>Reader</code> over a JSON-encoded input to parse; not
+ * necessarily a JSON object.
+ * @return a regular Java <code>Object</code>.
+ * @throws ParseException
+ * @throws IOException
+ */
+ protected static Object parseRaw(Reader in) throws ParseException, IOException {
+ try {
+ return getJSONParser().parse(in);
+ } catch (Error e) {
+ // Don't be stupid, org.json.simple. Bug 1042929.
+ throw new ParseException(ParseException.ERROR_UNEXPECTED_EXCEPTION, e);
+ }
+ }
+
+ /**
+ * Parse a JSON encoded string.
+ * <p>
+ * You should prefer the streaming interface {@link #parseRaw(Reader)}.
+ *
+ * @param input JSON-encoded input string to parse; not necessarily a JSON object.
+ * @return a regular Java <code>Object</code>.
+ * @throws ParseException
+ */
+ protected static Object parseRaw(String input) throws ParseException {
+ try {
+ return getJSONParser().parse(input);
+ } catch (Error e) {
+ // Don't be stupid, org.json.simple. Bug 1042929.
+ throw new ParseException(ParseException.ERROR_UNEXPECTED_EXCEPTION, e);
+ }
+ }
+
+ /**
+ * Helper method to get a JSON array from a stream.
+ *
+ * @param in <code>Reader</code> over a JSON-encoded array to parse.
+ * @throws ParseException
+ * @throws IOException
+ * @throws NonArrayJSONException if the object is valid JSON, but not an array.
+ */
+ public static JSONArray parseJSONArray(Reader in)
+ throws IOException, ParseException, NonArrayJSONException {
+ Object o = parseRaw(in);
+
+ if (o == null) {
+ return null;
+ }
+
+ if (o instanceof JSONArray) {
+ return (JSONArray) o;
+ }
+
+ throw new NonArrayJSONException("value must be a JSON array");
+ }
+
+ /**
+ * Helper method to get a JSON array from a string.
+ * <p>
+ * You should prefer the stream interface {@link #parseJSONArray(Reader)}.
+ *
+ * @param jsonString input.
+ * @throws IOException
+ * @throws NonArrayJSONException if the object is invalid JSON or not an array.
+ */
+ public static JSONArray parseJSONArray(String jsonString)
+ throws IOException, NonArrayJSONException {
+ Object o = null;
+ try {
+ o = parseRaw(jsonString);
+ } catch (ParseException e) {
+ throw new NonArrayJSONException(e);
+ }
+
+ if (o == null) {
+ return null;
+ }
+
+ if (o instanceof JSONArray) {
+ return (JSONArray) o;
+ }
+
+ throw new NonArrayJSONException("value must be a JSON array");
+ }
+
+ /**
+ * Helper method to get a JSON object from a UTF-8 byte array.
+ *
+ * @param in UTF-8 bytes.
+ * @throws NonObjectJSONException if the object is not valid JSON or not an object.
+ * @throws IOException
+ */
+ public static ExtendedJSONObject parseUTF8AsJSONObject(byte[] in)
+ throws NonObjectJSONException, IOException {
+ return new ExtendedJSONObject(new String(in, "UTF-8"));
+ }
+
+ public ExtendedJSONObject() {
+ this.object = new JSONObject();
+ }
+
+ public ExtendedJSONObject(JSONObject o) {
+ this.object = o;
+ }
+
+ public ExtendedJSONObject(Reader in) throws IOException, NonObjectJSONException {
+ if (in == null) {
+ this.object = new JSONObject();
+ return;
+ }
+
+ Object obj = null;
+ try {
+ obj = parseRaw(in);
+ } catch (ParseException e) {
+ throw new NonObjectJSONException(e);
+ }
+
+ if (obj instanceof JSONObject) {
+ this.object = ((JSONObject) obj);
+ } else {
+ throw new NonObjectJSONException("value must be a JSON object");
+ }
+ }
+
+ public ExtendedJSONObject(String jsonString) throws IOException, NonObjectJSONException {
+ this(jsonString == null ? null : new StringReader(jsonString));
+ }
+
+ @Override
+ public ExtendedJSONObject clone() {
+ return new ExtendedJSONObject((JSONObject) this.object.clone());
+ }
+
+ // Passthrough methods.
+ public Object get(String key) {
+ return this.object.get(key);
+ }
+
+ public long getLong(String key, long def) {
+ if (!object.containsKey(key)) {
+ return def;
+ }
+
+ Long val = getLong(key);
+ if (val == null) {
+ return def;
+ }
+ return val.longValue();
+ }
+
+ public Long getLong(String key) {
+ return (Long) this.get(key);
+ }
+
+ public String getString(String key) {
+ return (String) this.get(key);
+ }
+
+ public Boolean getBoolean(String key) {
+ return (Boolean) this.get(key);
+ }
+
+ /**
+ * Return an Integer if the value for this key is an Integer, Long, or String
+ * that can be parsed as a base 10 Integer.
+ * Passes through null.
+ *
+ * @throws NumberFormatException
+ */
+ public Integer getIntegerSafely(String key) throws NumberFormatException {
+ Object val = this.object.get(key);
+ if (val == null) {
+ return null;
+ }
+ if (val instanceof Integer) {
+ return (Integer) val;
+ }
+ if (val instanceof Long) {
+ return ((Long) val).intValue();
+ }
+ if (val instanceof String) {
+ return Integer.parseInt((String) val, 10);
+ }
+ throw new NumberFormatException("Expecting Integer, got " + val.getClass());
+ }
+
+ /**
+ * Return a server timestamp value as milliseconds since epoch.
+ *
+ * @param key
+ * @return A Long, or null if the value is non-numeric or doesn't exist.
+ */
+ public Long getTimestamp(String key) {
+ Object val = this.object.get(key);
+
+ // This is absurd.
+ if (val instanceof Double) {
+ double millis = ((Double) val) * 1000;
+ return Double.valueOf(millis).longValue();
+ }
+ if (val instanceof Float) {
+ double millis = ((Float) val).doubleValue() * 1000;
+ return Double.valueOf(millis).longValue();
+ }
+ if (val instanceof Number) {
+ // Must be an integral number.
+ return ((Number) val).longValue() * 1000;
+ }
+
+ return null;
+ }
+
+ public boolean containsKey(String key) {
+ return this.object.containsKey(key);
+ }
+
+ public String toJSONString() {
+ return this.object.toJSONString();
+ }
+
+ @Override
+ public String toString() {
+ return this.object.toString();
+ }
+
+ protected void putRaw(String key, Object value) {
+ @SuppressWarnings("unchecked")
+ Map<Object, Object> map = this.object;
+ map.put(key, value);
+ }
+
+ public void put(String key, String value) {
+ this.putRaw(key, value);
+ }
+
+ public void put(String key, boolean value) {
+ this.putRaw(key, value);
+ }
+
+ public void put(String key, long value) {
+ this.putRaw(key, value);
+ }
+
+ public void put(String key, int value) {
+ this.putRaw(key, value);
+ }
+
+ public void put(String key, ExtendedJSONObject value) {
+ this.putRaw(key, value);
+ }
+
+ public void put(String key, JSONArray value) {
+ this.putRaw(key, value);
+ }
+
+ @SuppressWarnings("unchecked")
+ public void putArray(String key, List<String> value) {
+ // Frustratingly inefficient, but there you have it.
+ final JSONArray jsonArray = new JSONArray();
+ jsonArray.addAll(value);
+ this.putRaw(key, jsonArray);
+ }
+
+ /**
+ * Remove key-value pair from JSONObject.
+ *
+ * @param key
+ * to be removed.
+ * @return true if key exists and was removed, false otherwise.
+ */
+ public boolean remove(String key) {
+ Object res = this.object.remove(key);
+ return (res != null);
+ }
+
+ public ExtendedJSONObject getObject(String key) throws NonObjectJSONException {
+ Object o = this.object.get(key);
+ if (o == null) {
+ return null;
+ }
+ if (o instanceof ExtendedJSONObject) {
+ return (ExtendedJSONObject) o;
+ }
+ if (o instanceof JSONObject) {
+ return new ExtendedJSONObject((JSONObject) o);
+ }
+ throw new NonObjectJSONException("value must be a JSON object for key: " + key);
+ }
+
+ @SuppressWarnings("unchecked")
+ public Set<Entry<String, Object>> entrySet() {
+ return this.object.entrySet();
+ }
+
+ @SuppressWarnings("unchecked")
+ public Set<String> keySet() {
+ return this.object.keySet();
+ }
+
+ public org.json.simple.JSONArray getArray(String key) throws NonArrayJSONException {
+ Object o = this.object.get(key);
+ if (o == null) {
+ return null;
+ }
+ if (o instanceof JSONArray) {
+ return (JSONArray) o;
+ }
+ throw new NonArrayJSONException("key must be a JSON array: " + key);
+ }
+
+ public int size() {
+ return this.object.size();
+ }
+
+ @Override
+ public int hashCode() {
+ if (this.object == null) {
+ return getClass().hashCode();
+ }
+ return this.object.hashCode() ^ getClass().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof ExtendedJSONObject)) {
+ return false;
+ }
+ if (o == this) {
+ return true;
+ }
+ ExtendedJSONObject other = (ExtendedJSONObject) o;
+ if (this.object == null) {
+ return other.object == null;
+ }
+ return this.object.equals(other.object);
+ }
+
+ /**
+ * Throw if keys are missing or values have wrong types.
+ *
+ * @param requiredFields list of required keys.
+ * @param requiredFieldClass class that values must be coercable to; may be null, which means don't check.
+ * @throws UnexpectedJSONException
+ */
+ public void throwIfFieldsMissingOrMisTyped(String[] requiredFields, Class<?> requiredFieldClass) throws BadRequiredFieldJSONException {
+ // Defensive as possible: verify object has expected key(s) with string value.
+ for (String k : requiredFields) {
+ Object value = get(k);
+ if (value == null) {
+ throw new BadRequiredFieldJSONException("Expected key not present in result: " + k);
+ }
+ if (requiredFieldClass != null && !(requiredFieldClass.isInstance(value))) {
+ throw new BadRequiredFieldJSONException("Value for key not an instance of " + requiredFieldClass + ": " + k);
+ }
+ }
+ }
+
+ /**
+ * Return a base64-encoded string value as a byte array.
+ */
+ public byte[] getByteArrayBase64(String key) {
+ String s = (String) this.object.get(key);
+ if (s == null) {
+ return null;
+ }
+ return Base64.decodeBase64(s);
+ }
+
+ /**
+ * Return a hex-encoded string value as a byte array.
+ */
+ public byte[] getByteArrayHex(String key) {
+ String s = (String) this.object.get(key);
+ if (s == null) {
+ return null;
+ }
+ return Utils.hex2Byte(s);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/GlobalSession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/GlobalSession.java
new file mode 100644
index 0000000000..e28bbe4cca
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/GlobalSession.java
@@ -0,0 +1,1167 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import android.content.Context;
+
+import org.json.simple.JSONArray;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
+import org.mozilla.gecko.sync.delegates.FreshStartDelegate;
+import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
+import org.mozilla.gecko.sync.delegates.JSONRecordFetchDelegate;
+import org.mozilla.gecko.sync.delegates.KeyUploadDelegate;
+import org.mozilla.gecko.sync.delegates.MetaGlobalDelegate;
+import org.mozilla.gecko.sync.delegates.WipeServerDelegate;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.HttpResponseObserver;
+import org.mozilla.gecko.sync.net.SyncResponse;
+import org.mozilla.gecko.sync.net.SyncStorageRecordRequest;
+import org.mozilla.gecko.sync.net.SyncStorageRequest;
+import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.sync.stage.AndroidBrowserBookmarksServerSyncStage;
+import org.mozilla.gecko.sync.stage.AndroidBrowserHistoryServerSyncStage;
+import org.mozilla.gecko.sync.stage.CheckPreconditionsStage;
+import org.mozilla.gecko.sync.stage.CompletedStage;
+import org.mozilla.gecko.sync.stage.EnsureCrypto5KeysStage;
+import org.mozilla.gecko.sync.stage.FennecTabsServerSyncStage;
+import org.mozilla.gecko.sync.stage.FetchInfoCollectionsStage;
+import org.mozilla.gecko.sync.stage.FetchInfoConfigurationStage;
+import org.mozilla.gecko.sync.stage.FetchMetaGlobalStage;
+import org.mozilla.gecko.sync.stage.FormHistoryServerSyncStage;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
+import org.mozilla.gecko.sync.stage.NoSuchStageException;
+import org.mozilla.gecko.sync.stage.PasswordsServerSyncStage;
+import org.mozilla.gecko.sync.stage.SyncClientsEngineStage;
+import org.mozilla.gecko.sync.stage.UploadMetaGlobalStage;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+
+public class GlobalSession implements HttpResponseObserver {
+ private static final String LOG_TAG = "GlobalSession";
+
+ public static final long STORAGE_VERSION = 5;
+
+ public SyncConfiguration config = null;
+
+ protected Map<Stage, GlobalSyncStage> stages;
+ public Stage currentState = Stage.idle;
+
+ public final GlobalSessionCallback callback;
+ protected final Context context;
+ protected final ClientsDataDelegate clientsDelegate;
+
+ /**
+ * Map from engine name to new settings for an updated meta/global record.
+ * Engines to remove will have <code>null</code> EngineSettings.
+ */
+ public final Map<String, EngineSettings> enginesToUpdate = new HashMap<String, EngineSettings>();
+
+ /*
+ * Key accessors.
+ */
+ public KeyBundle keyBundleForCollection(String collection) throws NoCollectionKeysSetException {
+ return config.getCollectionKeys().keyBundleForCollection(collection);
+ }
+
+ /*
+ * Config passthrough for convenience.
+ */
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return config.getAuthHeaderProvider();
+ }
+
+ public URI wboURI(String collection, String id) throws URISyntaxException {
+ return config.wboURI(collection, id);
+ }
+
+ public GlobalSession(SyncConfiguration config,
+ GlobalSessionCallback callback,
+ Context context,
+ ClientsDataDelegate clientsDelegate)
+ throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException {
+
+ if (callback == null) {
+ throw new IllegalArgumentException("Must provide a callback to GlobalSession constructor.");
+ }
+
+ this.callback = callback;
+ this.context = context;
+ this.clientsDelegate = clientsDelegate;
+
+ this.config = config;
+ registerCommands();
+ prepareStages();
+
+ if (config.stagesToSync == null) {
+ Logger.info(LOG_TAG, "No stages to sync specified; defaulting to all valid engine names.");
+ config.stagesToSync = Collections.unmodifiableCollection(SyncConfiguration.validEngineNames());
+ }
+
+ // TODO: data-driven plan for the sync, referring to prepareStages.
+ }
+
+ /**
+ * Register commands this global session knows how to process.
+ * <p>
+ * Re-registering a command overwrites any existing registration.
+ */
+ protected static void registerCommands() {
+ final CommandProcessor processor = CommandProcessor.getProcessor();
+
+ processor.registerCommand("resetEngine", new CommandRunner(1) {
+ @Override
+ public void executeCommand(final GlobalSession session, List<String> args) {
+ HashSet<String> names = new HashSet<String>();
+ names.add(args.get(0));
+ session.resetStagesByName(names);
+ }
+ });
+
+ processor.registerCommand("resetAll", new CommandRunner(0) {
+ @Override
+ public void executeCommand(final GlobalSession session, List<String> args) {
+ session.resetAllStages();
+ }
+ });
+
+ processor.registerCommand("wipeEngine", new CommandRunner(1) {
+ @Override
+ public void executeCommand(final GlobalSession session, List<String> args) {
+ HashSet<String> names = new HashSet<String>();
+ names.add(args.get(0));
+ session.wipeStagesByName(names);
+ }
+ });
+
+ processor.registerCommand("wipeAll", new CommandRunner(0) {
+ @Override
+ public void executeCommand(final GlobalSession session, List<String> args) {
+ session.wipeAllStages();
+ }
+ });
+
+ processor.registerCommand("displayURI", new CommandRunner(3) {
+ @Override
+ public void executeCommand(final GlobalSession session, List<String> args) {
+ CommandProcessor.displayURI(args, session.getContext());
+ }
+ });
+ }
+
+ protected void prepareStages() {
+ Map<Stage, GlobalSyncStage> stages = new EnumMap<Stage, GlobalSyncStage>(Stage.class);
+
+ stages.put(Stage.checkPreconditions, new CheckPreconditionsStage());
+ stages.put(Stage.fetchInfoCollections, new FetchInfoCollectionsStage());
+ stages.put(Stage.fetchMetaGlobal, new FetchMetaGlobalStage());
+ stages.put(Stage.fetchInfoConfiguration, new FetchInfoConfigurationStage(
+ config.infoConfigurationURL(), getAuthHeaderProvider()));
+ stages.put(Stage.ensureKeysStage, new EnsureCrypto5KeysStage());
+
+ stages.put(Stage.syncClientsEngine, new SyncClientsEngineStage());
+
+ stages.put(Stage.syncTabs, new FennecTabsServerSyncStage());
+ stages.put(Stage.syncPasswords, new PasswordsServerSyncStage());
+ stages.put(Stage.syncBookmarks, new AndroidBrowserBookmarksServerSyncStage());
+ stages.put(Stage.syncHistory, new AndroidBrowserHistoryServerSyncStage());
+ stages.put(Stage.syncFormHistory, new FormHistoryServerSyncStage());
+
+ stages.put(Stage.uploadMetaGlobal, new UploadMetaGlobalStage());
+ stages.put(Stage.completed, new CompletedStage());
+
+ this.stages = Collections.unmodifiableMap(stages);
+ }
+
+ public GlobalSyncStage getSyncStageByName(String name) throws NoSuchStageException {
+ return getSyncStageByName(Stage.byName(name));
+ }
+
+ public GlobalSyncStage getSyncStageByName(Stage next) throws NoSuchStageException {
+ GlobalSyncStage stage = stages.get(next);
+ if (stage == null) {
+ throw new NoSuchStageException(next);
+ }
+ return stage;
+ }
+
+ public Collection<GlobalSyncStage> getSyncStagesByEnum(Collection<Stage> enums) {
+ ArrayList<GlobalSyncStage> out = new ArrayList<GlobalSyncStage>();
+ for (Stage name : enums) {
+ try {
+ GlobalSyncStage stage = this.getSyncStageByName(name);
+ out.add(stage);
+ } catch (NoSuchStageException e) {
+ Logger.warn(LOG_TAG, "Unable to find stage with name " + name);
+ }
+ }
+ return out;
+ }
+
+ public Collection<GlobalSyncStage> getSyncStagesByName(Collection<String> names) {
+ ArrayList<GlobalSyncStage> out = new ArrayList<GlobalSyncStage>();
+ for (String name : names) {
+ try {
+ GlobalSyncStage stage = this.getSyncStageByName(name);
+ out.add(stage);
+ } catch (NoSuchStageException e) {
+ Logger.warn(LOG_TAG, "Unable to find stage with name " + name);
+ }
+ }
+ return out;
+ }
+
+ /**
+ * Advance and loop around the stages of a sync.
+ * @param current
+ * @return
+ * The next stage to execute.
+ */
+ public static Stage nextStage(Stage current) {
+ int index = current.ordinal() + 1;
+ int max = Stage.completed.ordinal() + 1;
+ return Stage.values()[index % max];
+ }
+
+ /**
+ * Move to the next stage in the syncing process.
+ */
+ public void advance() {
+ // If we have a backoff, request a backoff and don't advance to next stage.
+ long existingBackoff = largestBackoffObserved.get();
+ if (existingBackoff > 0) {
+ this.abort(null, "Aborting sync because of backoff of " + existingBackoff + " milliseconds.");
+ return;
+ }
+
+ this.callback.handleStageCompleted(this.currentState, this);
+ Stage next = nextStage(this.currentState);
+ GlobalSyncStage nextStage;
+ try {
+ nextStage = this.getSyncStageByName(next);
+ } catch (NoSuchStageException e) {
+ this.abort(e, "No such stage " + next);
+ return;
+ }
+ this.currentState = next;
+ Logger.info(LOG_TAG, "Running next stage " + next + " (" + nextStage + ")...");
+ try {
+ nextStage.execute(this);
+ } catch (Exception ex) {
+ Logger.warn(LOG_TAG, "Caught exception " + ex + " running stage " + next);
+ this.abort(ex, "Uncaught exception in stage.");
+ return;
+ }
+ }
+
+ public Context getContext() {
+ return this.context;
+ }
+
+ /**
+ * Begin a sync.
+ * <p>
+ * The caller is responsible for:
+ * <ul>
+ * <li>Verifying that any backoffs/minimum next sync requests are respected.</li>
+ * <li>Ensuring that the device is online.</li>
+ * <li>Ensuring that dependencies are ready.</li>
+ * </ul>
+ *
+ * @throws AlreadySyncingException
+ */
+ public void start() throws AlreadySyncingException {
+ if (this.currentState != GlobalSyncStage.Stage.idle) {
+ throw new AlreadySyncingException(this.currentState);
+ }
+ installAsHttpResponseObserver(); // Uninstalled by completeSync or abort.
+ this.advance();
+ }
+
+ /**
+ * Stop this sync and start again.
+ * @throws AlreadySyncingException
+ */
+ protected void restart() throws AlreadySyncingException {
+ this.currentState = GlobalSyncStage.Stage.idle;
+ if (callback.shouldBackOffStorage()) {
+ this.callback.handleAborted(this, "Told to back off.");
+ return;
+ }
+ this.start();
+ }
+
+ /**
+ * We're finished (aborted or succeeded): release resources.
+ */
+ protected void cleanUp() {
+ uninstallAsHttpResponseObserver();
+ this.stages = null;
+ }
+
+ public void completeSync() {
+ cleanUp();
+ this.currentState = GlobalSyncStage.Stage.idle;
+ this.callback.handleSuccess(this);
+ }
+
+ /**
+ * Record that an updated meta/global record should be uploaded with the given
+ * settings for the given engine.
+ *
+ * @param engineName engine to update.
+ * @param engineSettings new syncID and version.
+ */
+ public void recordForMetaGlobalUpdate(String engineName, EngineSettings engineSettings) {
+ enginesToUpdate.put(engineName, engineSettings);
+ }
+
+ /**
+ * Record that an updated meta/global record should be uploaded without the
+ * given engine name.
+ *
+ * @param engineName
+ * engine to remove.
+ */
+ public void removeEngineFromMetaGlobal(String engineName) {
+ enginesToUpdate.put(engineName, null);
+ }
+
+ public boolean hasUpdatedMetaGlobal() {
+ if (enginesToUpdate.isEmpty()) {
+ Logger.info(LOG_TAG, "Not uploading updated meta/global record since there are no engines requesting upload.");
+ return false;
+ }
+
+ if (Logger.shouldLogVerbose(LOG_TAG)) {
+ Logger.trace(LOG_TAG, "Uploading updated meta/global record since there are engine changes to meta/global.");
+ Logger.trace(LOG_TAG, "Engines requesting update [" + Utils.toCommaSeparatedString(enginesToUpdate.keySet()) + "]");
+ }
+
+ return true;
+ }
+
+ public void updateMetaGlobalInPlace() {
+ config.metaGlobal.declined = this.declinedEngineNames();
+ ExtendedJSONObject engines = config.metaGlobal.getEngines();
+ for (Entry<String, EngineSettings> pair : enginesToUpdate.entrySet()) {
+ if (pair.getValue() == null) {
+ engines.remove(pair.getKey());
+ } else {
+ engines.put(pair.getKey(), pair.getValue().toJSONObject());
+ }
+ }
+
+ enginesToUpdate.clear();
+ }
+
+ /**
+ * Synchronously upload an updated meta/global.
+ * <p>
+ * All problems are logged and ignored.
+ */
+ public void uploadUpdatedMetaGlobal() {
+ updateMetaGlobalInPlace();
+
+ Logger.debug(LOG_TAG, "Uploading updated meta/global record.");
+ final Object monitor = new Object();
+
+ Runnable doUpload = new Runnable() {
+ @Override
+ public void run() {
+ config.metaGlobal.upload(new MetaGlobalDelegate() {
+ @Override
+ public void handleSuccess(MetaGlobal global, SyncStorageResponse response) {
+ Logger.info(LOG_TAG, "Successfully uploaded updated meta/global record.");
+ // Engine changes are stored as diffs, so update enabled engines in config to match uploaded meta/global.
+ config.enabledEngineNames = config.metaGlobal.getEnabledEngineNames();
+ // Clear userSelectedEngines because they are updated in config and meta/global.
+ config.userSelectedEngines = null;
+
+ synchronized (monitor) {
+ monitor.notify();
+ }
+ }
+
+ @Override
+ public void handleMissing(MetaGlobal global, SyncStorageResponse response) {
+ Logger.warn(LOG_TAG, "Got 404 missing uploading updated meta/global record; shouldn't happen. Ignoring.");
+ synchronized (monitor) {
+ monitor.notify();
+ }
+ }
+
+ @Override
+ public void handleFailure(SyncStorageResponse response) {
+ Logger.warn(LOG_TAG, "Failed to upload updated meta/global record; ignoring.");
+ synchronized (monitor) {
+ monitor.notify();
+ }
+ }
+
+ @Override
+ public void handleError(Exception e) {
+ Logger.warn(LOG_TAG, "Got exception trying to upload updated meta/global record; ignoring.", e);
+ synchronized (monitor) {
+ monitor.notify();
+ }
+ }
+ });
+ }
+ };
+
+ final Thread upload = new Thread(doUpload);
+ synchronized (monitor) {
+ try {
+ upload.start();
+ monitor.wait();
+ Logger.debug(LOG_TAG, "Uploaded updated meta/global record.");
+ } catch (InterruptedException e) {
+ Logger.error(LOG_TAG, "Uploading updated meta/global interrupted; continuing.");
+ }
+ }
+ }
+
+
+ public void abort(Exception e, String reason) {
+ Logger.warn(LOG_TAG, "Aborting sync: " + reason, e);
+ cleanUp();
+ long existingBackoff = largestBackoffObserved.get();
+ if (existingBackoff > 0) {
+ callback.requestBackoff(existingBackoff);
+ }
+ if (!(e instanceof HTTPFailureException)) {
+ // e is null, or we aborted for a non-HTTP reason; okay to upload new meta/global record.
+ if (this.hasUpdatedMetaGlobal()) {
+ this.uploadUpdatedMetaGlobal(); // Only logs errors; does not call abort.
+ }
+ }
+ this.callback.handleError(this, e);
+ }
+
+ public void handleHTTPError(SyncStorageResponse response, String reason) {
+ // TODO: handling of 50x (backoff), 401 (node reassignment or auth error).
+ // Fall back to aborting.
+ Logger.warn(LOG_TAG, "Aborting sync due to HTTP " + response.getStatusCode());
+ this.interpretHTTPFailure(response.httpResponse());
+ this.abort(new HTTPFailureException(response), reason);
+ }
+
+ /**
+ * Perform appropriate backoff etc. extraction.
+ */
+ public void interpretHTTPFailure(HttpResponse response) {
+ // TODO: handle permanent rejection.
+ long responseBackoff = (new SyncResponse(response)).totalBackoffInMilliseconds();
+ if (responseBackoff > 0) {
+ callback.requestBackoff(responseBackoff);
+ }
+
+ if (response.getStatusLine() != null) {
+ final int statusCode = response.getStatusLine().getStatusCode();
+ switch(statusCode) {
+
+ case 400:
+ SyncStorageResponse storageResponse = new SyncStorageResponse(response);
+ this.interpretHTTPBadRequestBody(storageResponse);
+ break;
+
+ case 401:
+ /*
+ * Alert our callback we have a 401 on a cluster URL. This GlobalSession
+ * will fail, but the next one will fetch a new cluster URL and will
+ * distinguish between "node reassignment" and "user password changed".
+ */
+ callback.informUnauthorizedResponse(this, config.getClusterURL());
+ break;
+ }
+ }
+ }
+
+ protected void interpretHTTPBadRequestBody(final SyncStorageResponse storageResponse) {
+ try {
+ final String body = storageResponse.body();
+ if (body == null) {
+ return;
+ }
+ if (SyncStorageResponse.RESPONSE_CLIENT_UPGRADE_REQUIRED.equals(body)) {
+ callback.informUpgradeRequiredResponse(this);
+ return;
+ }
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Exception parsing HTTP 400 body.", e);
+ }
+ }
+
+ public void fetchInfoCollections(JSONRecordFetchDelegate callback) throws URISyntaxException {
+ final JSONRecordFetcher fetcher = new JSONRecordFetcher(config.infoCollectionsURL(), getAuthHeaderProvider());
+ fetcher.fetch(callback);
+ }
+
+ /**
+ * Upload new crypto/keys.
+ *
+ * @param keys
+ * new keys.
+ * @param keyUploadDelegate
+ * a delegate.
+ */
+ public void uploadKeys(final CollectionKeys keys,
+ final KeyUploadDelegate keyUploadDelegate) {
+ SyncStorageRecordRequest request;
+ try {
+ request = new SyncStorageRecordRequest(this.config.keysURI());
+ } catch (URISyntaxException e) {
+ keyUploadDelegate.onKeyUploadFailed(e);
+ return;
+ }
+
+ request.delegate = new SyncStorageRequestDelegate() {
+
+ @Override
+ public String ifUnmodifiedSince() {
+ return null;
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse response) {
+ Logger.debug(LOG_TAG, "Keys uploaded.");
+ BaseResource.consumeEntity(response); // We don't need the response at all.
+ keyUploadDelegate.onKeysUploaded();
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ Logger.debug(LOG_TAG, "Failed to upload keys.");
+ GlobalSession.this.interpretHTTPFailure(response.httpResponse());
+ BaseResource.consumeEntity(response); // The exception thrown should not need the body of the response.
+ keyUploadDelegate.onKeyUploadFailed(new HTTPFailureException(response));
+ }
+
+ @Override
+ public void handleRequestError(Exception ex) {
+ Logger.warn(LOG_TAG, "Got exception trying to upload keys", ex);
+ keyUploadDelegate.onKeyUploadFailed(ex);
+ }
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return GlobalSession.this.getAuthHeaderProvider();
+ }
+ };
+
+ // Convert keys to an encrypted crypto record.
+ CryptoRecord keysRecord;
+ try {
+ keysRecord = keys.asCryptoRecord();
+ keysRecord.setKeyBundle(config.syncKeyBundle);
+ keysRecord.encrypt();
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Got exception trying creating crypto record from keys", e);
+ keyUploadDelegate.onKeyUploadFailed(e);
+ return;
+ }
+
+ request.put(keysRecord);
+ }
+
+ /*
+ * meta/global callbacks.
+ */
+ public void processMetaGlobal(MetaGlobal global) {
+ config.metaGlobal = global;
+
+ Long storageVersion = global.getStorageVersion();
+ if (storageVersion == null) {
+ Logger.warn(LOG_TAG, "Malformed remote meta/global: could not retrieve remote storage version.");
+ freshStart();
+ return;
+ }
+ if (storageVersion < STORAGE_VERSION) {
+ Logger.warn(LOG_TAG, "Outdated server: reported " +
+ "remote storage version " + storageVersion + " < " +
+ "local storage version " + STORAGE_VERSION);
+ freshStart();
+ return;
+ }
+ if (storageVersion > STORAGE_VERSION) {
+ Logger.warn(LOG_TAG, "Outdated client: reported " +
+ "remote storage version " + storageVersion + " > " +
+ "local storage version " + STORAGE_VERSION);
+ requiresUpgrade();
+ return;
+ }
+ String remoteSyncID = global.getSyncID();
+ if (remoteSyncID == null) {
+ Logger.warn(LOG_TAG, "Malformed remote meta/global: could not retrieve remote syncID.");
+ freshStart();
+ return;
+ }
+ String localSyncID = config.syncID;
+ if (!remoteSyncID.equals(localSyncID)) {
+ Logger.warn(LOG_TAG, "Remote syncID different from local syncID: resetting client and assuming remote syncID.");
+ resetAllStages();
+ config.purgeCryptoKeys();
+ config.syncID = remoteSyncID;
+ }
+ // Compare lastModified timestamps for remote/local engine selection times.
+ Logger.debug(LOG_TAG, "Comparing local engine selection timestamp [" + config.userSelectedEnginesTimestamp + "] to server meta/global timestamp [" + config.persistedMetaGlobal().lastModified() + "].");
+ if (config.userSelectedEnginesTimestamp < config.persistedMetaGlobal().lastModified()) {
+ // Remote has later meta/global timestamp. Don't upload engine changes.
+ config.userSelectedEngines = null;
+ }
+ // Persist enabled engine names.
+ config.enabledEngineNames = global.getEnabledEngineNames();
+ if (config.enabledEngineNames == null) {
+ Logger.warn(LOG_TAG, "meta/global reported no enabled engine names!");
+ } else {
+ if (Logger.shouldLogVerbose(LOG_TAG)) {
+ Logger.trace(LOG_TAG, "Persisting enabled engine names '" +
+ Utils.toCommaSeparatedString(config.enabledEngineNames) + "' from meta/global.");
+ }
+ }
+
+ // Persist declined.
+ // Our declined engines at any point are:
+ // Whatever they were remotely, plus whatever they were locally, less any
+ // engines that were just enabled locally or remotely.
+ // If remote just 'won', our recently enabled list just got cleared.
+ final HashSet<String> allDeclined = new HashSet<String>();
+
+ final Set<String> newRemoteDeclined = global.getDeclinedEngineNames();
+ final Set<String> oldLocalDeclined = config.declinedEngineNames;
+
+ allDeclined.addAll(newRemoteDeclined);
+ allDeclined.addAll(oldLocalDeclined);
+
+ if (config.userSelectedEngines != null) {
+ for (Entry<String, Boolean> selection : config.userSelectedEngines.entrySet()) {
+ if (selection.getValue()) {
+ allDeclined.remove(selection.getKey());
+ }
+ }
+ }
+
+ config.declinedEngineNames = allDeclined;
+ if (config.declinedEngineNames.isEmpty()) {
+ Logger.debug(LOG_TAG, "meta/global reported no declined engine names, and we have none declined locally.");
+ } else {
+ if (Logger.shouldLogVerbose(LOG_TAG)) {
+ Logger.trace(LOG_TAG, "Persisting declined engine names '" +
+ Utils.toCommaSeparatedString(config.declinedEngineNames) + "' from meta/global.");
+ }
+ }
+
+ config.persistToPrefs();
+ advance();
+ }
+
+ public void processMissingMetaGlobal(MetaGlobal global) {
+ freshStart();
+ }
+
+ /**
+ * Do a fresh start then quietly finish the sync, starting another.
+ */
+ public void freshStart() {
+ final GlobalSession globalSession = this;
+ freshStart(this, new FreshStartDelegate() {
+
+ @Override
+ public void onFreshStartFailed(Exception e) {
+ globalSession.abort(e, "Fresh start failed.");
+ }
+
+ @Override
+ public void onFreshStart() {
+ try {
+ Logger.warn(LOG_TAG, "Fresh start succeeded; restarting global session.");
+ globalSession.config.persistToPrefs();
+ globalSession.restart();
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Got exception when restarting sync after freshStart.", e);
+ globalSession.abort(e, "Got exception after freshStart.");
+ }
+ }
+ });
+ }
+
+ /**
+ * Clean the server, aborting the current sync.
+ * <p>
+ * <ol>
+ * <li>Wipe the server storage.</li>
+ * <li>Reset all stages and purge cached state: (meta/global and crypto/keys records).</li>
+ * <li>Upload fresh meta/global record.</li>
+ * <li>Upload fresh crypto/keys record.</li>
+ * <li>Restart the sync entirely in order to re-download meta/global and crypto/keys record.</li>
+ * </ol>
+ * @param session the current session.
+ * @param freshStartDelegate delegate to notify on fresh start or failure.
+ */
+ protected static void freshStart(final GlobalSession session, final FreshStartDelegate freshStartDelegate) {
+ Logger.debug(LOG_TAG, "Fresh starting.");
+
+ final MetaGlobal mg = session.generateNewMetaGlobal();
+
+ session.wipeServer(session.getAuthHeaderProvider(), new WipeServerDelegate() {
+
+ @Override
+ public void onWiped(long timestamp) {
+ Logger.debug(LOG_TAG, "Successfully wiped server. Resetting all stages and purging cached meta/global and crypto/keys records.");
+
+ session.resetAllStages();
+ session.config.purgeMetaGlobal();
+ session.config.purgeCryptoKeys();
+ session.config.persistToPrefs();
+
+ Logger.info(LOG_TAG, "Uploading new meta/global with sync ID " + mg.syncID + ".");
+
+ // It would be good to set the X-If-Unmodified-Since header to `timestamp`
+ // for this PUT to ensure at least some level of transactionality.
+ // Unfortunately, the servers don't support it after a wipe right now
+ // (bug 693893), so we're going to defer this until bug 692700.
+ mg.upload(new MetaGlobalDelegate() {
+ @Override
+ public void handleSuccess(MetaGlobal uploadedGlobal, SyncStorageResponse uploadResponse) {
+ Logger.info(LOG_TAG, "Uploaded new meta/global with sync ID " + uploadedGlobal.syncID + ".");
+
+ // Generate new keys.
+ CollectionKeys keys = null;
+ try {
+ keys = session.generateNewCryptoKeys();
+ } catch (CryptoException e) {
+ Logger.warn(LOG_TAG, "Got exception generating new keys; failing fresh start.", e);
+ freshStartDelegate.onFreshStartFailed(e);
+ }
+ if (keys == null) {
+ Logger.warn(LOG_TAG, "Got null keys from generateNewKeys; failing fresh start.");
+ freshStartDelegate.onFreshStartFailed(null);
+ }
+
+ // Upload new keys.
+ Logger.info(LOG_TAG, "Uploading new crypto/keys.");
+ session.uploadKeys(keys, new KeyUploadDelegate() {
+ @Override
+ public void onKeysUploaded() {
+ Logger.info(LOG_TAG, "Uploaded new crypto/keys.");
+ freshStartDelegate.onFreshStart();
+ }
+
+ @Override
+ public void onKeyUploadFailed(Exception e) {
+ Logger.warn(LOG_TAG, "Got exception uploading new keys.", e);
+ freshStartDelegate.onFreshStartFailed(e);
+ }
+ });
+ }
+
+ @Override
+ public void handleMissing(MetaGlobal global, SyncStorageResponse response) {
+ // Shouldn't happen on upload.
+ Logger.warn(LOG_TAG, "Got 'missing' response uploading new meta/global.");
+ freshStartDelegate.onFreshStartFailed(new Exception("meta/global missing while uploading."));
+ }
+
+ @Override
+ public void handleFailure(SyncStorageResponse response) {
+ Logger.warn(LOG_TAG, "Got failure " + response.getStatusCode() + " uploading new meta/global.");
+ session.interpretHTTPFailure(response.httpResponse());
+ freshStartDelegate.onFreshStartFailed(new HTTPFailureException(response));
+ }
+
+ @Override
+ public void handleError(Exception e) {
+ Logger.warn(LOG_TAG, "Got error uploading new meta/global.", e);
+ freshStartDelegate.onFreshStartFailed(e);
+ }
+ });
+ }
+
+ @Override
+ public void onWipeFailed(Exception e) {
+ Logger.warn(LOG_TAG, "Wipe failed.");
+ freshStartDelegate.onFreshStartFailed(e);
+ }
+ });
+ }
+
+ // Note that we do not yet implement wipeRemote: it's only necessary for
+ // first sync options.
+ // -- reset local stages, wipe server for each stage *except* clients
+ // (stages only, not whole server!), send wipeEngine commands to each client.
+ //
+ // Similarly for startOver (because we don't receive that notification).
+ // -- remove client data from server, reset local stages, clear keys, reset
+ // backoff, clear all prefs, discard credentials.
+ //
+ // Change passphrase: wipe entire server, reset client to force upload, sync.
+ //
+ // When an engine is disabled: wipe its collections on the server, reupload
+ // meta/global.
+ //
+ // On syncing each stage: if server has engine version 0 or old, wipe server,
+ // reset client to prompt reupload.
+ // If sync ID mismatch: take that syncID and reset client.
+
+ protected void wipeServer(final AuthHeaderProvider authHeaderProvider, final WipeServerDelegate wipeDelegate) {
+ SyncStorageRequest request;
+ final GlobalSession self = this;
+
+ try {
+ request = new SyncStorageRequest(config.storageURL());
+ } catch (URISyntaxException ex) {
+ Logger.warn(LOG_TAG, "Invalid URI in wipeServer.");
+ wipeDelegate.onWipeFailed(ex);
+ return;
+ }
+
+ request.delegate = new SyncStorageRequestDelegate() {
+
+ @Override
+ public String ifUnmodifiedSince() {
+ return null;
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse response) {
+ BaseResource.consumeEntity(response);
+ wipeDelegate.onWiped(response.normalizedWeaveTimestamp());
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ Logger.warn(LOG_TAG, "Got request failure " + response.getStatusCode() + " in wipeServer.");
+ // Process HTTP failures here to pick up backoffs, etc.
+ self.interpretHTTPFailure(response.httpResponse());
+ BaseResource.consumeEntity(response); // The exception thrown should not need the body of the response.
+ wipeDelegate.onWipeFailed(new HTTPFailureException(response));
+ }
+
+ @Override
+ public void handleRequestError(Exception ex) {
+ Logger.warn(LOG_TAG, "Got exception in wipeServer.", ex);
+ wipeDelegate.onWipeFailed(ex);
+ }
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return GlobalSession.this.getAuthHeaderProvider();
+ }
+ };
+ request.delete();
+ }
+
+ public void wipeAllStages() {
+ Logger.info(LOG_TAG, "Wiping all stages.");
+ // Includes "clients".
+ this.wipeStagesByEnum(Stage.getNamedStages());
+ }
+
+ public void wipeStages(Collection<GlobalSyncStage> stages) {
+ for (GlobalSyncStage stage : stages) {
+ try {
+ Logger.info(LOG_TAG, "Wiping " + stage);
+ stage.wipeLocal(this);
+ } catch (Exception e) {
+ Logger.error(LOG_TAG, "Ignoring wipe failure for stage " + stage, e);
+ }
+ }
+ }
+
+ public void wipeStagesByEnum(Collection<Stage> stages) {
+ wipeStages(this.getSyncStagesByEnum(stages));
+ }
+
+ public void wipeStagesByName(Collection<String> names) {
+ wipeStages(this.getSyncStagesByName(names));
+ }
+
+ public void resetAllStages() {
+ Logger.info(LOG_TAG, "Resetting all stages.");
+ // Includes "clients".
+ this.resetStagesByEnum(Stage.getNamedStages());
+ }
+
+ public void resetStages(Collection<GlobalSyncStage> stages) {
+ for (GlobalSyncStage stage : stages) {
+ try {
+ Logger.info(LOG_TAG, "Resetting " + stage);
+ stage.resetLocal(this);
+ } catch (Exception e) {
+ Logger.error(LOG_TAG, "Ignoring reset failure for stage " + stage, e);
+ }
+ }
+ }
+
+ public void resetStagesByEnum(Collection<Stage> stages) {
+ resetStages(this.getSyncStagesByEnum(stages));
+ }
+
+ public void resetStagesByName(Collection<String> names) {
+ resetStages(this.getSyncStagesByName(names));
+ }
+
+ /**
+ * Engines to explicitly mark as declined in a fresh meta/global record.
+ * <p>
+ * Returns an empty array if the user hasn't elected to customize data types,
+ * or an array of engines that the user un-checked during customization.
+ * <p>
+ * Engines that Android Sync doesn't recognize are <b>not</b> included in
+ * the returned array.
+ *
+ * @return a new JSONArray of engine names.
+ */
+ @SuppressWarnings("unchecked")
+ protected JSONArray declinedEngineNames() {
+ final JSONArray declined = new JSONArray();
+ for (String engine : config.declinedEngineNames) {
+ declined.add(engine);
+ };
+
+ return declined;
+ }
+
+ /**
+ * Engines to include in a fresh meta/global record.
+ * <p>
+ * Returns either the persisted engine names (perhaps we have been node
+ * re-assigned and are initializing a clean server: we want to upload the
+ * persisted engine names so that we don't accidentally disable engines that
+ * Android Sync doesn't recognize), or the set of engines names that Android
+ * Sync implements.
+ *
+ * @return set of engine names.
+ */
+ protected Set<String> enabledEngineNames() {
+ if (config.enabledEngineNames != null) {
+ return config.enabledEngineNames;
+ }
+
+ // These are the default set of engine names.
+ Set<String> validEngineNames = SyncConfiguration.validEngineNames();
+
+ // If the user hasn't set any selected engines, that's okay -- default to
+ // everything.
+ if (config.userSelectedEngines == null) {
+ return validEngineNames;
+ }
+
+ // userSelectedEngines has keys that are engine names, and boolean values
+ // corresponding to whether the user asked for the engine to sync or not. If
+ // an engine is not present, that means the user didn't change its sync
+ // setting. Since we default to everything on, that means the user didn't
+ // turn it off; therefore, it's included in the set of engines to sync.
+ Set<String> validAndSelectedEngineNames = new HashSet<String>();
+ for (String engineName : validEngineNames) {
+ if (config.userSelectedEngines.containsKey(engineName) &&
+ !config.userSelectedEngines.get(engineName)) {
+ continue;
+ }
+ validAndSelectedEngineNames.add(engineName);
+ }
+ return validAndSelectedEngineNames;
+ }
+
+ /**
+ * Generate fresh crypto/keys collection.
+ * @return crypto/keys collection.
+ * @throws CryptoException
+ */
+ @SuppressWarnings("static-method")
+ public CollectionKeys generateNewCryptoKeys() throws CryptoException {
+ return CollectionKeys.generateCollectionKeys();
+ }
+
+ /**
+ * Generate a fresh meta/global record.
+ * @return meta/global record.
+ */
+ public MetaGlobal generateNewMetaGlobal() {
+ final String newSyncID = Utils.generateGuid();
+ final String metaURL = this.config.metaURL();
+
+ ExtendedJSONObject engines = new ExtendedJSONObject();
+ for (String engineName : enabledEngineNames()) {
+ EngineSettings engineSettings = null;
+ try {
+ GlobalSyncStage globalStage = this.getSyncStageByName(engineName);
+ Integer version = globalStage.getStorageVersion();
+ if (version == null) {
+ continue; // Don't want this stage to be included in meta/global.
+ }
+ engineSettings = new EngineSettings(Utils.generateGuid(), version);
+ } catch (NoSuchStageException e) {
+ // No trouble; Android Sync might not recognize this engine yet.
+ // By default, version 0. Other clients will see the 0 version and reset/wipe accordingly.
+ engineSettings = new EngineSettings(Utils.generateGuid(), 0);
+ }
+ engines.put(engineName, engineSettings.toJSONObject());
+ }
+
+ MetaGlobal metaGlobal = new MetaGlobal(metaURL, this.getAuthHeaderProvider());
+ metaGlobal.setSyncID(newSyncID);
+ metaGlobal.setStorageVersion(STORAGE_VERSION);
+ metaGlobal.setEngines(engines);
+
+ // We assume that the config's declined engines have been updated
+ // according to the user's selections.
+ metaGlobal.setDeclinedEngineNames(this.declinedEngineNames());
+
+ return metaGlobal;
+ }
+
+ /**
+ * Suggest that your Sync client needs to be upgraded to work
+ * with this server.
+ */
+ public void requiresUpgrade() {
+ Logger.info(LOG_TAG, "Client outdated storage version; requires update.");
+ // TODO: notify UI.
+ this.abort(null, "Requires upgrade");
+ }
+
+ /**
+ * If meta/global is missing or malformed, throws a MetaGlobalException.
+ * Otherwise, returns true if there is an entry for this engine in the
+ * meta/global "engines" object.
+ * <p>
+ * This is a global/permanent setting, not a local/temporary setting. For the
+ * latter, see {@link GlobalSession#isEngineLocallyEnabled(String)}.
+ *
+ * @param engineName the name to check (e.g., "bookmarks").
+ * @param engineSettings
+ * if non-null, verify that the server engine settings are congruent
+ * with this, throwing the appropriate MetaGlobalException if not.
+ * @return
+ * true if the engine with the provided name is present in the
+ * meta/global "engines" object, and verification passed.
+ *
+ * @throws MetaGlobalException
+ */
+ public boolean isEngineRemotelyEnabled(String engineName, EngineSettings engineSettings) throws MetaGlobalException {
+ if (this.config.metaGlobal == null) {
+ throw new MetaGlobalNotSetException();
+ }
+
+ // This should not occur.
+ if (this.config.enabledEngineNames == null) {
+ Logger.error(LOG_TAG, "No enabled engines in config. Giving up.");
+ throw new MetaGlobalMissingEnginesException();
+ }
+
+ if (!(this.config.enabledEngineNames.contains(engineName))) {
+ Logger.debug(LOG_TAG, "Engine " + engineName + " not enabled: no meta/global entry.");
+ return false;
+ }
+
+ // If we have a meta/global, check that it's safe for us to sync.
+ // (If we don't, we'll create one later, which is why we return `true` above.)
+ if (engineSettings != null) {
+ // Throws if there's a problem.
+ this.config.metaGlobal.verifyEngineSettings(engineName, engineSettings);
+ }
+
+ return true;
+ }
+
+
+ /**
+ * Return true if the named stage should be synced this session.
+ * <p>
+ * This is a local/temporary setting, in contrast to the meta/global record,
+ * which is a global/permanent setting. For the latter, see
+ * {@link GlobalSession#isEngineRemotelyEnabled(String, EngineSettings)}.
+ *
+ * @param stageName
+ * to query.
+ * @return true if named stage is enabled for this sync.
+ */
+ public boolean isEngineLocallyEnabled(String stageName) {
+ if (config.stagesToSync == null) {
+ return true;
+ }
+ return config.stagesToSync.contains(stageName);
+ }
+
+ public ClientsDataDelegate getClientsDelegate() {
+ return this.clientsDelegate;
+ }
+
+ /**
+ * The longest backoff observed to date; -1 means no backoff observed.
+ */
+ protected final AtomicLong largestBackoffObserved = new AtomicLong(-1);
+
+ /**
+ * Reset any observed backoff and start observing HTTP responses for backoff
+ * requests.
+ */
+ protected void installAsHttpResponseObserver() {
+ Logger.debug(LOG_TAG, "Adding " + this + " as a BaseResource HttpResponseObserver.");
+ BaseResource.addHttpResponseObserver(this);
+ largestBackoffObserved.set(-1);
+ }
+
+ /**
+ * Stop observing HttpResponses for backoff requests.
+ */
+ protected void uninstallAsHttpResponseObserver() {
+ Logger.debug(LOG_TAG, "Removing " + this + " as a BaseResource HttpResponseObserver.");
+ BaseResource.removeHttpResponseObserver(this);
+ }
+
+ /**
+ * Observe all HTTP response for backoff requests on all status codes, not just errors.
+ */
+ @Override
+ public void observeHttpResponse(HttpUriRequest request, HttpResponse response) {
+ // Ignore non-Sync storage requests.
+ final URI clusterURL = config.getClusterURL();
+ if (clusterURL != null && !clusterURL.getHost().equals(request.getURI().getHost())) {
+ // It's possible to see requests without a clusterURL (in particular,
+ // during testing); allow some extra backoffs in this case.
+ return;
+ }
+
+ long responseBackoff = (new SyncResponse(response)).totalBackoffInMilliseconds(); // TODO: don't allocate object?
+ if (responseBackoff <= 0) {
+ return;
+ }
+
+ Logger.debug(LOG_TAG, "Observed " + responseBackoff + " millisecond backoff request.");
+ while (true) {
+ long existingBackoff = largestBackoffObserved.get();
+ if (existingBackoff >= responseBackoff) {
+ return;
+ }
+ if (largestBackoffObserved.compareAndSet(existingBackoff, responseBackoff)) {
+ return;
+ }
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/HTTPFailureException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/HTTPFailureException.java
new file mode 100644
index 0000000000..69bba88419
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/HTTPFailureException.java
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+import android.content.SyncResult;
+
+public class HTTPFailureException extends SyncException {
+ private static final long serialVersionUID = -5415864029780770619L;
+ public SyncStorageResponse response;
+
+ public HTTPFailureException(SyncStorageResponse response) {
+ this.response = response;
+ }
+
+ @Override
+ public String toString() {
+ String errorMessage;
+ try {
+ errorMessage = this.response.getErrorMessage();
+ } catch (Exception e) {
+ // Oh well.
+ errorMessage = "[unknown error message]";
+ }
+ return "<HTTPFailureException " + this.response.getStatusCode() +
+ " :: (" + errorMessage + ")>";
+ }
+
+ @Override
+ public void updateStats(GlobalSession globalSession, SyncResult syncResult) {
+ switch (response.getStatusCode()) {
+ case 401:
+ // Node reassignment 401s get handled internally.
+ syncResult.stats.numAuthExceptions++;
+ return;
+ case 500:
+ case 501:
+ case 503:
+ // TODO: backoff.
+ syncResult.stats.numIoExceptions++;
+ return;
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/InfoCollections.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/InfoCollections.java
new file mode 100644
index 0000000000..374fa5cf5d
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/InfoCollections.java
@@ -0,0 +1,103 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.mozilla.gecko.background.common.log.Logger;
+
+/**
+ * Fetches the timestamp information in <code>info/collections</code> on the
+ * Sync server. Provides access to those timestamps, along with logic to check
+ * for whether a collection requires an update.
+ */
+public class InfoCollections {
+ private static final String LOG_TAG = "InfoCollections";
+
+ /**
+ * Fields fetched from the server, or <code>null</code> if not yet fetched.
+ * <p>
+ * Rather than storing decimal/double timestamps, as provided by the server,
+ * we convert immediately to milliseconds since epoch.
+ */
+ final Map<String, Long> timestamps;
+
+ public InfoCollections() {
+ this(new ExtendedJSONObject());
+ }
+
+ public InfoCollections(final ExtendedJSONObject record) {
+ Logger.debug(LOG_TAG, "info/collections is " + record.toJSONString());
+ HashMap<String, Long> map = new HashMap<String, Long>();
+
+ for (Entry<String, Object> entry : record.entrySet()) {
+ final String key = entry.getKey();
+ final Object value = entry.getValue();
+
+ // These objects are most likely going to be Doubles. Regardless, we
+ // want to get them in a more sane time format.
+ if (value instanceof Double) {
+ map.put(key, Utils.decimalSecondsToMilliseconds((Double) value));
+ continue;
+ }
+ if (value instanceof Long) {
+ map.put(key, Utils.decimalSecondsToMilliseconds((Long) value));
+ continue;
+ }
+ if (value instanceof Integer) {
+ map.put(key, Utils.decimalSecondsToMilliseconds((Integer) value));
+ continue;
+ }
+ Logger.warn(LOG_TAG, "Skipping info/collections entry for " + key);
+ }
+
+ this.timestamps = Collections.unmodifiableMap(map);
+ }
+
+ /**
+ * Return the timestamp for the given collection, or null if the timestamps
+ * have not been fetched or the given collection does not have a timestamp.
+ *
+ * @param collection
+ * The collection to inspect.
+ * @return the timestamp in milliseconds since epoch.
+ */
+ public Long getTimestamp(String collection) {
+ if (timestamps == null) {
+ return null;
+ }
+ return timestamps.get(collection);
+ }
+
+ /**
+ * Test if a given collection needs to be updated.
+ *
+ * @param collection
+ * The collection to test.
+ * @param lastModified
+ * Timestamp when local record was last modified.
+ */
+ public boolean updateNeeded(String collection, long lastModified) {
+ Logger.trace(LOG_TAG, "Testing " + collection + " for updateNeeded. Local last modified is " + lastModified + ".");
+
+ // No local record of modification time? Need an update.
+ if (lastModified <= 0) {
+ return true;
+ }
+
+ // No meta/global on the server? We need an update. The server fetch will fail and
+ // then we will upload a fresh meta/global.
+ Long serverLastModified = getTimestamp(collection);
+ if (serverLastModified == null) {
+ return true;
+ }
+
+ // Otherwise, we need an update if our modification time is stale.
+ return serverLastModified > lastModified;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/InfoConfiguration.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/InfoConfiguration.java
new file mode 100644
index 0000000000..eb24284332
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/InfoConfiguration.java
@@ -0,0 +1,93 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import android.util.Log;
+
+import org.mozilla.gecko.background.common.log.Logger;
+
+/**
+ * Wraps and provides access to configuration data returned from info/configuration.
+ * Docs: https://docs.services.mozilla.com/storage/apis-1.5.html#general-info
+ *
+ * - <bold>max_request_bytes</bold>: the maximum size in bytes of the overall
+ * HTTP request body that will be accepted by the server.
+ *
+ * - <bold>max_post_records</bold>: the maximum number of records that can be
+ * uploaded to a collection in a single POST request.
+ *
+ * - <bold>max_post_bytes</bold>: the maximum combined size in bytes of the
+ * record payloads that can be uploaded to a collection in a single
+ * POST request.
+ *
+ * - <bold>max_total_records</bold>: the maximum number of records that can be
+ * uploaded to a collection as part of a batched upload.
+ *
+ * - <bold>max_total_bytes</bold>: the maximum combined size in bytes of the
+ * record payloads that can be uploaded to a collection as part of
+ * a batched upload.
+ */
+public class InfoConfiguration {
+ private static final String LOG_TAG = "InfoConfiguration";
+
+ public static final String MAX_REQUEST_BYTES = "max_request_bytes";
+ public static final String MAX_POST_RECORDS = "max_post_records";
+ public static final String MAX_POST_BYTES = "max_post_bytes";
+ public static final String MAX_TOTAL_RECORDS = "max_total_records";
+ public static final String MAX_TOTAL_BYTES = "max_total_bytes";
+
+ private static final long DEFAULT_MAX_REQUEST_BYTES = 1048576;
+ private static final long DEFAULT_MAX_POST_RECORDS = 100;
+ private static final long DEFAULT_MAX_POST_BYTES = 1048576;
+ private static final long DEFAULT_MAX_TOTAL_RECORDS = 10000;
+ private static final long DEFAULT_MAX_TOTAL_BYTES = 104857600;
+
+ // While int's upper range is (2^31-1), which in bytes is equivalent to 2.147 GB, let's be optimistic
+ // about the future and use long here, so that this code works if the server decides its clients are
+ // all on fiber and have congress-library sized bookmark collections.
+ // Record counts are long for the sake of simplicity.
+ public final long maxRequestBytes;
+ public final long maxPostRecords;
+ public final long maxPostBytes;
+ public final long maxTotalRecords;
+ public final long maxTotalBytes;
+
+ public InfoConfiguration() {
+ Logger.debug(LOG_TAG, "info/configuration is unavailable, using defaults");
+
+ maxRequestBytes = DEFAULT_MAX_REQUEST_BYTES;
+ maxPostRecords = DEFAULT_MAX_POST_RECORDS;
+ maxPostBytes = DEFAULT_MAX_POST_BYTES;
+ maxTotalRecords = DEFAULT_MAX_TOTAL_RECORDS;
+ maxTotalBytes = DEFAULT_MAX_TOTAL_BYTES;
+ }
+
+ public InfoConfiguration(final ExtendedJSONObject record) {
+ Logger.debug(LOG_TAG, "info/configuration is " + record.toJSONString());
+
+ maxRequestBytes = getValueFromRecord(record, MAX_REQUEST_BYTES, DEFAULT_MAX_REQUEST_BYTES);
+ maxPostRecords = getValueFromRecord(record, MAX_POST_RECORDS, DEFAULT_MAX_POST_RECORDS);
+ maxPostBytes = getValueFromRecord(record, MAX_POST_BYTES, DEFAULT_MAX_POST_BYTES);
+ maxTotalRecords = getValueFromRecord(record, MAX_TOTAL_RECORDS, DEFAULT_MAX_TOTAL_RECORDS);
+ maxTotalBytes = getValueFromRecord(record, MAX_TOTAL_BYTES, DEFAULT_MAX_TOTAL_BYTES);
+ }
+
+ private static Long getValueFromRecord(ExtendedJSONObject record, String key, long defaultValue) {
+ if (!record.containsKey(key)) {
+ return defaultValue;
+ }
+
+ try {
+ Long val = record.getLong(key);
+ if (val == null) {
+ return defaultValue;
+ }
+ return val;
+ } catch (NumberFormatException e) {
+ Log.w(LOG_TAG, "Could not parse key " + key + " from record: " + record, e);
+ return defaultValue;
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/InfoCounts.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/InfoCounts.java
new file mode 100644
index 0000000000..832e97d102
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/InfoCounts.java
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.mozilla.gecko.background.common.log.Logger;
+
+public class InfoCounts {
+ static final String LOG_TAG = "InfoCounts";
+
+ /**
+ * Counts fetched from the server, or <code>null</code> if not yet fetched.
+ */
+ private Map<String, Integer> counts = null;
+
+ @SuppressWarnings("unchecked")
+ public InfoCounts(final ExtendedJSONObject record) {
+ Logger.debug(LOG_TAG, "info/collection_counts is " + record.toJSONString());
+ HashMap<String, Integer> map = new HashMap<String, Integer>();
+
+ Set<Entry<String, Object>> entrySet = record.object.entrySet();
+
+ String key;
+ Object value;
+
+ for (Entry<String, Object> entry : entrySet) {
+ key = entry.getKey();
+ value = entry.getValue();
+
+ if (value instanceof Integer) {
+ map.put(key, (Integer) value);
+ continue;
+ }
+
+ if (value instanceof Long) {
+ map.put(key, ((Long) value).intValue());
+ continue;
+ }
+
+ Logger.warn(LOG_TAG, "Skipping info/collection_counts entry for " + key);
+ }
+
+ this.counts = Collections.unmodifiableMap(map);
+ }
+
+ /**
+ * Return the server count for the given collection, or null if the counts
+ * have not been fetched or the given collection does not have a count.
+ *
+ * @param collection
+ * The collection to inspect.
+ * @return the number of elements in the named collection.
+ */
+ public Integer getCount(String collection) {
+ if (counts == null) {
+ return null;
+ }
+ return counts.get(collection);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/JSONRecordFetcher.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/JSONRecordFetcher.java
new file mode 100644
index 0000000000..982b5b0266
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/JSONRecordFetcher.java
@@ -0,0 +1,145 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.delegates.JSONRecordFetchDelegate;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.SyncStorageRecordRequest;
+import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+/**
+ * An object which fetches a chunk of JSON from a URI, using certain credentials,
+ * and informs its delegate of the result.
+ */
+public class JSONRecordFetcher {
+ private static final long DEFAULT_AWAIT_TIMEOUT_MSEC = 2 * 60 * 1000; // Two minutes.
+ private static final String LOG_TAG = "JSONRecordFetcher";
+
+ protected final AuthHeaderProvider authHeaderProvider;
+ protected final String uri;
+ protected JSONRecordFetchDelegate delegate;
+
+ public JSONRecordFetcher(final String uri, final AuthHeaderProvider authHeaderProvider) {
+ if (uri == null) {
+ throw new IllegalArgumentException("uri must not be null");
+ }
+ this.uri = uri;
+ this.authHeaderProvider = authHeaderProvider;
+ }
+
+ protected String getURI() {
+ return this.uri;
+ }
+
+ private class JSONFetchHandler implements SyncStorageRequestDelegate {
+
+ // SyncStorageRequestDelegate methods for fetching.
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return authHeaderProvider;
+ }
+
+ @Override
+ public String ifUnmodifiedSince() {
+ return null;
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse response) {
+ if (response.wasSuccessful()) {
+ try {
+ delegate.handleSuccess(response.jsonObjectBody());
+ } catch (Exception e) {
+ handleRequestError(e);
+ }
+ return;
+ }
+ handleRequestFailure(response);
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ delegate.handleFailure(response);
+ }
+
+ @Override
+ public void handleRequestError(Exception ex) {
+ delegate.handleError(ex);
+ }
+ }
+
+ public void fetch(final JSONRecordFetchDelegate delegate) {
+ this.delegate = delegate;
+ try {
+ final SyncStorageRecordRequest r = new SyncStorageRecordRequest(this.getURI());
+ r.delegate = new JSONFetchHandler();
+ r.get();
+ } catch (Exception e) {
+ delegate.handleError(e);
+ }
+ }
+
+ private class LatchedJSONRecordFetchDelegate implements JSONRecordFetchDelegate {
+ public ExtendedJSONObject body = null;
+ public Exception exception = null;
+ private final CountDownLatch latch;
+
+ public LatchedJSONRecordFetchDelegate(CountDownLatch latch) {
+ this.latch = latch;
+ }
+
+ @Override
+ public void handleFailure(SyncStorageResponse response) {
+ this.exception = new HTTPFailureException(response);
+ latch.countDown();
+ }
+
+ @Override
+ public void handleError(Exception e) {
+ this.exception = e;
+ latch.countDown();
+ }
+
+ @Override
+ public void handleSuccess(ExtendedJSONObject body) {
+ this.body = body;
+ latch.countDown();
+ }
+ }
+
+ /**
+ * Fetch the info record, blocking until it returns.
+ * @return the info record.
+ */
+ public ExtendedJSONObject fetchBlocking() throws HTTPFailureException, Exception {
+ CountDownLatch latch = new CountDownLatch(1);
+ LatchedJSONRecordFetchDelegate delegate = new LatchedJSONRecordFetchDelegate(latch);
+ this.delegate = delegate;
+ this.fetch(delegate);
+
+ // Sanity wait: the resource itself will time out and throw after two
+ // minutes, so we just want to avoid coding errors causing us to block
+ // endlessly.
+ if (!latch.await(DEFAULT_AWAIT_TIMEOUT_MSEC, TimeUnit.MILLISECONDS)) {
+ Logger.warn(LOG_TAG, "Interrupted fetching info record.");
+ throw new InterruptedException("info fetch timed out.");
+ }
+
+ if (delegate.body != null) {
+ return delegate.body;
+ }
+
+ if (delegate.exception != null) {
+ throw delegate.exception;
+ }
+
+ throw new Exception("Unknown error.");
+ }
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/KeyBundleProvider.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/KeyBundleProvider.java
new file mode 100644
index 0000000000..4a2be2a9b1
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/KeyBundleProvider.java
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+
+public interface KeyBundleProvider {
+ public abstract KeyBundle keyBundle();
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobal.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobal.java
new file mode 100644
index 0000000000..a90c0fee82
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobal.java
@@ -0,0 +1,372 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.json.simple.JSONArray;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.MetaGlobalException.MetaGlobalMalformedSyncIDException;
+import org.mozilla.gecko.sync.MetaGlobalException.MetaGlobalMalformedVersionException;
+import org.mozilla.gecko.sync.delegates.MetaGlobalDelegate;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.SyncStorageRecordRequest;
+import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+public class MetaGlobal implements SyncStorageRequestDelegate {
+ private static final String LOG_TAG = "MetaGlobal";
+ protected String metaURL;
+
+ // Fields.
+ protected ExtendedJSONObject engines;
+ protected JSONArray declined;
+ protected Long storageVersion;
+ protected String syncID;
+
+ // Lookup tables.
+ protected Map<String, String> syncIDs;
+ protected Map<String, Integer> versions;
+ protected Map<String, MetaGlobalException> exceptions;
+
+ // Temporary location to store our callback.
+ private MetaGlobalDelegate callback;
+
+ // A little hack so we can use the same delegate implementation for upload and download.
+ private boolean isUploading;
+ protected final AuthHeaderProvider authHeaderProvider;
+
+ public MetaGlobal(String metaURL, AuthHeaderProvider authHeaderProvider) {
+ this.metaURL = metaURL;
+ this.authHeaderProvider = authHeaderProvider;
+ }
+
+ public void fetch(MetaGlobalDelegate delegate) {
+ this.callback = delegate;
+ try {
+ this.isUploading = false;
+ SyncStorageRecordRequest r = new SyncStorageRecordRequest(this.metaURL);
+ r.delegate = this;
+ r.get();
+ } catch (URISyntaxException e) {
+ this.callback.handleError(e);
+ }
+ }
+
+ public void upload(MetaGlobalDelegate callback) {
+ try {
+ this.isUploading = true;
+ SyncStorageRecordRequest r = new SyncStorageRecordRequest(this.metaURL);
+
+ r.delegate = this;
+ this.callback = callback;
+ r.put(this.asCryptoRecord());
+ } catch (Exception e) {
+ callback.handleError(e);
+ }
+ }
+
+ protected ExtendedJSONObject asRecordContents() {
+ ExtendedJSONObject json = new ExtendedJSONObject();
+ json.put("storageVersion", storageVersion);
+ json.put("engines", engines);
+ json.put("syncID", syncID);
+ json.put("declined", declined);
+ return json;
+ }
+
+ /**
+ * Return a copy ready for upload.
+ * @return an unencrypted <code>CryptoRecord</code>.
+ */
+ public CryptoRecord asCryptoRecord() {
+ ExtendedJSONObject payload = this.asRecordContents();
+ CryptoRecord record = new CryptoRecord(payload);
+ record.collection = "meta";
+ record.guid = "global";
+ record.deleted = false;
+ return record;
+ }
+
+ public void setFromRecord(CryptoRecord record) throws IllegalStateException, IOException, NonObjectJSONException, NonArrayJSONException {
+ if (record == null) {
+ throw new IllegalArgumentException("Cannot set meta/global from null record");
+ }
+ Logger.debug(LOG_TAG, "meta/global is " + record.payload.toJSONString());
+ this.storageVersion = (Long) record.payload.get("storageVersion");
+ this.syncID = (String) record.payload.get("syncID");
+
+ setEngines(record.payload.getObject("engines"));
+
+ // Accepts null -- declined can be missing.
+ setDeclinedEngineNames(record.payload.getArray("declined"));
+ }
+
+ public Long getStorageVersion() {
+ return this.storageVersion;
+ }
+
+ public void setStorageVersion(Long version) {
+ this.storageVersion = version;
+ }
+
+ public ExtendedJSONObject getEngines() {
+ return engines;
+ }
+
+ @SuppressWarnings("unchecked")
+ public void declineEngine(String engine) {
+ if (this.declined == null) {
+ JSONArray replacement = new JSONArray();
+ replacement.add(engine);
+ setDeclinedEngineNames(replacement);
+ return;
+ }
+
+ this.declined.add(engine);
+ }
+
+ @SuppressWarnings("unchecked")
+ public void declineEngineNames(Collection<String> additional) {
+ if (this.declined == null) {
+ JSONArray replacement = new JSONArray();
+ replacement.addAll(additional);
+ setDeclinedEngineNames(replacement);
+ return;
+ }
+
+ for (String engine : additional) {
+ if (!this.declined.contains(engine)) {
+ this.declined.add(engine);
+ }
+ }
+ }
+
+ public void setDeclinedEngineNames(JSONArray declined) {
+ if (declined == null) {
+ this.declined = new JSONArray();
+ return;
+ }
+ this.declined = declined;
+ }
+
+ /**
+ * Return the set of engines that we support (given as an argument)
+ * but the user hasn't explicitly declined on another device.
+ *
+ * Can return the input if the user hasn't declined any engines.
+ */
+ public Set<String> getNonDeclinedEngineNames(Set<String> supported) {
+ if (this.declined == null ||
+ this.declined.isEmpty()) {
+ return supported;
+ }
+
+ final Set<String> result = new HashSet<String>(supported);
+ result.removeAll(this.declined);
+ return result;
+ }
+
+ public void setEngines(ExtendedJSONObject engines) {
+ if (engines == null) {
+ engines = new ExtendedJSONObject();
+ }
+ this.engines = engines;
+ final int count = engines.size();
+ versions = new HashMap<String, Integer>(count);
+ syncIDs = new HashMap<String, String>(count);
+ exceptions = new HashMap<String, MetaGlobalException>(count);
+ for (String engineName : engines.keySet()) {
+ try {
+ ExtendedJSONObject engineEntry = engines.getObject(engineName);
+ recordEngineState(engineName, engineEntry);
+ } catch (NonObjectJSONException e) {
+ Logger.error(LOG_TAG, "Engine field for " + engineName + " in meta/global is not an object.");
+ recordEngineState(engineName, new ExtendedJSONObject()); // Doesn't have a version or syncID, for example, so will be server wiped.
+ }
+ }
+ }
+
+ /**
+ * Take a JSON object corresponding to the 'engines' field for the provided engine name,
+ * updating {@link #syncIDs} and {@link #versions} accordingly.
+ *
+ * If the record is malformed, an entry is added to {@link #exceptions}, to be rethrown
+ * during validation.
+ */
+ protected void recordEngineState(String engineName, ExtendedJSONObject engineEntry) {
+ if (engineEntry == null) {
+ throw new IllegalArgumentException("engineEntry cannot be null.");
+ }
+
+ // Record syncID first, so that engines with bad versions are recorded.
+ try {
+ String syncID = engineEntry.getString("syncID");
+ if (syncID == null) {
+ Logger.warn(LOG_TAG, "No syncID for " + engineName + ". Recording exception.");
+ exceptions.put(engineName, new MetaGlobalMalformedSyncIDException());
+ }
+ syncIDs.put(engineName, syncID);
+ } catch (ClassCastException e) {
+ // Malformed syncID on the server. Wipe the server.
+ Logger.warn(LOG_TAG, "Malformed syncID " + engineEntry.get("syncID") +
+ " for " + engineName + ". Recording exception.");
+ exceptions.put(engineName, new MetaGlobalMalformedSyncIDException());
+ }
+
+ try {
+ Integer version = engineEntry.getIntegerSafely("version");
+ Logger.trace(LOG_TAG, "Engine " + engineName + " has server version " + version);
+ if (version == null ||
+ version == 0) {
+ // Invalid version. Wipe the server.
+ Logger.warn(LOG_TAG, "Malformed version " + version +
+ " for " + engineName + ". Recording exception.");
+ exceptions.put(engineName, new MetaGlobalMalformedVersionException());
+ return;
+ }
+ versions.put(engineName, version);
+ } catch (NumberFormatException e) {
+ // Invalid version. Wipe the server.
+ Logger.warn(LOG_TAG, "Malformed version " + engineEntry.get("version") +
+ " for " + engineName + ". Recording exception.");
+ exceptions.put(engineName, new MetaGlobalMalformedVersionException());
+ return;
+ }
+ }
+
+ /**
+ * Get enabled engine names.
+ *
+ * @return a collection of engine names or <code>null</code> if meta/global
+ * was malformed.
+ */
+ public Set<String> getEnabledEngineNames() {
+ if (engines == null) {
+ return null;
+ }
+ return new HashSet<String>(engines.keySet());
+ }
+
+ @SuppressWarnings("unchecked")
+ public Set<String> getDeclinedEngineNames() {
+ if (declined == null) {
+ return null;
+ }
+ return new HashSet<String>(declined);
+ }
+
+ /**
+ * Returns if the server settings and local settings match.
+ * Throws a specific MetaGlobalException if that's not the case.
+ */
+ public void verifyEngineSettings(String engineName, EngineSettings engineSettings)
+ throws MetaGlobalException {
+
+ // We use syncIDs as our canary.
+ if (syncIDs == null) {
+ throw new IllegalStateException("No meta/global record yet processed.");
+ }
+
+ if (engineSettings == null) {
+ throw new IllegalArgumentException("engineSettings cannot be null.");
+ }
+
+ // First, see if we had a parsing problem.
+ final MetaGlobalException exception = exceptions.get(engineName);
+ if (exception != null) {
+ throw exception;
+ }
+
+ final String syncID = syncIDs.get(engineName);
+ if (syncID == null) {
+ // We have checked engineName against enabled engine names before this, so
+ // we should either have a syncID or an exception for this engine already.
+ throw new IllegalArgumentException("Unknown engine " + engineName);
+ }
+
+ // Since we don't have an exception, and we do have a syncID, we should have a version.
+ final Integer version = versions.get(engineName);
+ if (version > engineSettings.version) {
+ // We're out of date.
+ throw new MetaGlobalException.MetaGlobalStaleClientVersionException(version);
+ }
+
+ if (!syncID.equals(engineSettings.syncID)) {
+ // Our syncID is wrong. Reset client and take the server syncID.
+ throw new MetaGlobalException.MetaGlobalStaleClientSyncIDException(syncID);
+ }
+ }
+
+ public String getSyncID() {
+ return syncID;
+ }
+
+ public void setSyncID(String syncID) {
+ this.syncID = syncID;
+ }
+
+ // SyncStorageRequestDelegate methods for fetching.
+ public String credentials() {
+ return null;
+ }
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return authHeaderProvider;
+ }
+
+ @Override
+ public String ifUnmodifiedSince() {
+ return null;
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse response) {
+ if (this.isUploading) {
+ this.handleUploadSuccess(response);
+ } else {
+ this.handleDownloadSuccess(response);
+ }
+ }
+
+ private void handleUploadSuccess(SyncStorageResponse response) {
+ this.callback.handleSuccess(this, response);
+ }
+
+ private void handleDownloadSuccess(SyncStorageResponse response) {
+ if (response.wasSuccessful()) {
+ try {
+ CryptoRecord record = CryptoRecord.fromJSONRecord(response.jsonObjectBody());
+ this.setFromRecord(record);
+ this.callback.handleSuccess(this, response);
+ } catch (Exception e) {
+ this.callback.handleError(e);
+ }
+ return;
+ }
+ this.callback.handleFailure(response);
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ if (response.getStatusCode() == 404) {
+ this.callback.handleMissing(this, response);
+ return;
+ }
+ this.callback.handleFailure(response);
+ }
+
+ @Override
+ public void handleRequestError(Exception e) {
+ this.callback.handleError(e);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobalException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobalException.java
new file mode 100644
index 0000000000..bec531d11f
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobalException.java
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+public class MetaGlobalException extends SyncException {
+ private static final long serialVersionUID = -6182315615113508925L;
+
+ public static class MetaGlobalMalformedSyncIDException extends MetaGlobalException {
+ private static final long serialVersionUID = 1L;
+ }
+
+ public static class MetaGlobalMalformedVersionException extends MetaGlobalException {
+ private static final long serialVersionUID = 1L;
+ }
+
+ public static class MetaGlobalOutdatedVersionException extends MetaGlobalException {
+ private static final long serialVersionUID = 1L;
+ }
+
+ public static class MetaGlobalStaleClientVersionException extends MetaGlobalException {
+ private static final long serialVersionUID = 1L;
+ public final int serverVersion;
+ public MetaGlobalStaleClientVersionException(final int version) {
+ this.serverVersion = version;
+ }
+ }
+
+ public static class MetaGlobalStaleClientSyncIDException extends MetaGlobalException {
+ private static final long serialVersionUID = 1L;
+ public final String serverSyncID;
+ public MetaGlobalStaleClientSyncIDException(final String syncID) {
+ this.serverSyncID = syncID;
+ }
+ }
+
+ public static class MetaGlobalEngineStateChangedException extends MetaGlobalException {
+ private static final long serialVersionUID = 1L;
+ public final boolean isEnabled;
+ public MetaGlobalEngineStateChangedException(boolean isEnabled) {
+ this.isEnabled = isEnabled;
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobalMissingEnginesException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobalMissingEnginesException.java
new file mode 100644
index 0000000000..91bfd2f76f
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobalMissingEnginesException.java
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+public class MetaGlobalMissingEnginesException extends MetaGlobalException {
+ private static final long serialVersionUID = -2662107402622277865L;
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobalNotSetException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobalNotSetException.java
new file mode 100644
index 0000000000..ef059c71d4
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobalNotSetException.java
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+public class MetaGlobalNotSetException extends MetaGlobalException {
+ private static final long serialVersionUID = 2959032409571832970L;
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NoCollectionKeysSetException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NoCollectionKeysSetException.java
new file mode 100644
index 0000000000..323e355b4e
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NoCollectionKeysSetException.java
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import android.content.SyncResult;
+
+public class NoCollectionKeysSetException extends SyncException {
+ private static final long serialVersionUID = -6185128075412771120L;
+
+ @Override
+ public void updateStats(GlobalSession globalSession, SyncResult syncResult) {
+ syncResult.stats.numAuthExceptions++;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NodeAuthenticationException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NodeAuthenticationException.java
new file mode 100644
index 0000000000..a5cd5f0eb4
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NodeAuthenticationException.java
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import android.content.SyncResult;
+
+public class NodeAuthenticationException extends SyncException {
+ private static final long serialVersionUID = 8156745873212364352L;
+
+ @Override
+ public void updateStats(GlobalSession globalSession, SyncResult syncResult) {
+ syncResult.stats.numAuthExceptions++;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonArrayJSONException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonArrayJSONException.java
new file mode 100644
index 0000000000..554645b118
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonArrayJSONException.java
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+public class NonArrayJSONException extends UnexpectedJSONException {
+ private static final long serialVersionUID = 5582918057432365749L;
+
+ public NonArrayJSONException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ public NonArrayJSONException(Throwable throwable) {
+ super(throwable);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonObjectJSONException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonObjectJSONException.java
new file mode 100644
index 0000000000..fd50d465e6
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonObjectJSONException.java
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+public class NonObjectJSONException extends UnexpectedJSONException {
+ private static final long serialVersionUID = 2214238763035650087L;
+
+ public NonObjectJSONException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ public NonObjectJSONException(Throwable throwable) {
+ super(throwable);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NullClusterURLException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NullClusterURLException.java
new file mode 100644
index 0000000000..c1d8833b62
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NullClusterURLException.java
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import android.content.SyncResult;
+
+public class NullClusterURLException extends SyncException {
+ private static final long serialVersionUID = 4277845518548393161L;
+
+ @Override
+ public void updateStats(GlobalSession globalSession, SyncResult syncResult) {
+ syncResult.stats.numAuthExceptions++;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/PersistedMetaGlobal.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/PersistedMetaGlobal.java
new file mode 100644
index 0000000000..d3467545c2
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/PersistedMetaGlobal.java
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+
+import android.content.SharedPreferences;
+
+public class PersistedMetaGlobal {
+ public static final String LOG_TAG = "PersistedMetaGlobal";
+
+ public static final String META_GLOBAL_SERVER_RESPONSE_BODY = "metaGlobalServerResponseBody";
+ public static final String META_GLOBAL_LAST_MODIFIED = "metaGlobalLastModified";
+
+ protected SharedPreferences prefs;
+
+ public PersistedMetaGlobal(SharedPreferences prefs) {
+ this.prefs = prefs;
+ }
+
+ /**
+ * Sets a <code>MetaGlobal</code> from persisted prefs.
+ *
+ * @param metaUrl
+ * meta/global server URL
+ * @param credentials
+ * Sync credentials
+ *
+ * @return <MetaGlobal> set from previously fetched meta/global record from
+ * server
+ */
+ public MetaGlobal metaGlobal(String metaUrl, AuthHeaderProvider authHeaderProvider) {
+ String json = prefs.getString(META_GLOBAL_SERVER_RESPONSE_BODY, null);
+ if (json == null) {
+ return null;
+ }
+ MetaGlobal metaGlobal = null;
+ try {
+ CryptoRecord cryptoRecord = CryptoRecord.fromJSONRecord(json);
+ MetaGlobal mg = new MetaGlobal(metaUrl, authHeaderProvider);
+ mg.setFromRecord(cryptoRecord);
+ metaGlobal = mg;
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Got exception decrypting persisted meta/global.", e);
+ }
+ return metaGlobal;
+ }
+
+ public void persistMetaGlobal(MetaGlobal metaGlobal) {
+ if (metaGlobal == null) {
+ Logger.debug(LOG_TAG, "Clearing persisted meta/global.");
+ prefs.edit().remove(META_GLOBAL_SERVER_RESPONSE_BODY).commit();
+ return;
+ }
+ try {
+ CryptoRecord cryptoRecord = metaGlobal.asCryptoRecord();
+ String json = cryptoRecord.toJSONString();
+ Logger.debug(LOG_TAG, "Persisting meta/global.");
+ prefs.edit().putString(META_GLOBAL_SERVER_RESPONSE_BODY, json).commit();
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Got exception encrypting while persisting meta/global.", e);
+ }
+ }
+
+ public long lastModified() {
+ return prefs.getLong(META_GLOBAL_LAST_MODIFIED, -1);
+ }
+
+ public void persistLastModified(long lastModified) {
+ if (lastModified <= 0) {
+ Logger.debug(LOG_TAG, "Clearing persisted meta/global last modified timestamp.");
+ prefs.edit().remove(META_GLOBAL_LAST_MODIFIED).commit();
+ return;
+ }
+ Logger.debug(LOG_TAG, "Persisting meta/global last modified timestamp " + lastModified + ".");
+ prefs.edit().putLong(META_GLOBAL_LAST_MODIFIED, lastModified).commit();
+ }
+
+ public void purge() {
+ persistLastModified(-1);
+ persistMetaGlobal(null);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/PrefsBackoffHandler.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/PrefsBackoffHandler.java
new file mode 100644
index 0000000000..63f6446da1
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/PrefsBackoffHandler.java
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+
+public class PrefsBackoffHandler implements BackoffHandler {
+ public static final String PREF_EARLIEST_NEXT = "earliestnext";
+
+ private final SharedPreferences prefs;
+ private final String prefEarliest;
+
+ public PrefsBackoffHandler(final SharedPreferences prefs, final String prefSuffix) {
+ if (prefs == null) {
+ throw new IllegalArgumentException("prefs must not be null.");
+ }
+ this.prefs = prefs;
+ this.prefEarliest = PREF_EARLIEST_NEXT + "." + prefSuffix;
+ }
+
+ @Override
+ public synchronized long getEarliestNextRequest() {
+ return prefs.getLong(prefEarliest, 0);
+ }
+
+ @Override
+ public synchronized void setEarliestNextRequest(final long next) {
+ final Editor edit = prefs.edit();
+ edit.putLong(prefEarliest, next);
+ edit.commit();
+ }
+
+ @Override
+ public synchronized void extendEarliestNextRequest(final long next) {
+ if (prefs.getLong(prefEarliest, 0) >= next) {
+ return;
+ }
+ final Editor edit = prefs.edit();
+ edit.putLong(prefEarliest, next);
+ edit.commit();
+ }
+
+ /**
+ * Return the number of milliseconds until we're allowed to touch the server again,
+ * or 0 if now is fine.
+ */
+ @Override
+ public long delayMilliseconds() {
+ long earliestNextRequest = getEarliestNextRequest();
+ if (earliestNextRequest <= 0) {
+ return 0;
+ }
+ long now = System.currentTimeMillis();
+ return Math.max(0, earliestNextRequest - now);
+ }
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/README.txt b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/README.txt
new file mode 100644
index 0000000000..cf4624ca4b
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/README.txt
@@ -0,0 +1 @@
+These files are managed in the android-sync repo. Do not modify directly, or your changes will be lost.
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Server11PreviousPostFailedException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Server11PreviousPostFailedException.java
new file mode 100644
index 0000000000..4ea77f37c8
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Server11PreviousPostFailedException.java
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+/**
+ * A previous POST failed, so we won't send any more records this session.
+ */
+public class Server11PreviousPostFailedException extends SyncException {
+ private static final long serialVersionUID = -3582490631414624310L;
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Server11RecordPostFailedException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Server11RecordPostFailedException.java
new file mode 100644
index 0000000000..d654d3116a
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Server11RecordPostFailedException.java
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+/**
+ * The server rejected a record in its "failure" array.
+ */
+public class Server11RecordPostFailedException extends SyncException {
+ private static final long serialVersionUID = -8517471217486190314L;
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SharedPreferencesClientsDataDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SharedPreferencesClientsDataDelegate.java
new file mode 100644
index 0000000000..4c1584d5a1
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SharedPreferencesClientsDataDelegate.java
@@ -0,0 +1,121 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
+import org.mozilla.gecko.fxa.FxAccountDeviceRegistrator;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
+import org.mozilla.gecko.util.HardwareUtils;
+
+import android.accounts.Account;
+import android.content.Context;
+import android.content.SharedPreferences;
+
+/**
+ * A <code>ClientsDataDelegate</code> implementation that persists to a
+ * <code>SharedPreferences</code> instance.
+ */
+public class SharedPreferencesClientsDataDelegate implements ClientsDataDelegate {
+ protected final SharedPreferences sharedPreferences;
+ protected final Context context;
+
+ public SharedPreferencesClientsDataDelegate(SharedPreferences sharedPreferences, Context context) {
+ this.sharedPreferences = sharedPreferences;
+ this.context = context;
+
+ // It's safe to init this multiple times.
+ HardwareUtils.init(context);
+ }
+
+ @Override
+ public synchronized String getAccountGUID() {
+ String accountGUID = sharedPreferences.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null);
+ if (accountGUID == null) {
+ accountGUID = Utils.generateGuid();
+ sharedPreferences.edit().putString(SyncConfiguration.PREF_ACCOUNT_GUID, accountGUID).commit();
+ }
+ return accountGUID;
+ }
+
+ private synchronized void saveClientNameToSharedPreferences(String clientName, long now) {
+ sharedPreferences
+ .edit()
+ .putString(SyncConfiguration.PREF_CLIENT_NAME, clientName)
+ .putLong(SyncConfiguration.PREF_CLIENT_DATA_TIMESTAMP, now)
+ .apply();
+ }
+
+ /**
+ * Set client name.
+ *
+ * @param clientName to change to.
+ */
+ @Override
+ public synchronized void setClientName(String clientName, long now) {
+ saveClientNameToSharedPreferences(clientName, now);
+
+ // Update the FxA device registration
+ final Account account = FirefoxAccounts.getFirefoxAccount(context);
+ if (account != null) {
+ final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
+ fxAccount.resetDeviceRegistrationVersion();
+ }
+ }
+
+ @Override
+ public String getDefaultClientName() {
+ return FxAccountUtils.defaultClientName(context);
+ }
+
+ @Override
+ public synchronized String getClientName() {
+ String clientName = sharedPreferences.getString(SyncConfiguration.PREF_CLIENT_NAME, null);
+ if (clientName == null) {
+ clientName = getDefaultClientName();
+ long now = System.currentTimeMillis();
+ saveClientNameToSharedPreferences(clientName, now); // Save locally only to avoid a recursion loop
+ }
+ return clientName;
+ }
+
+ @Override
+ public synchronized void setClientsCount(int clientsCount) {
+ sharedPreferences.edit().putLong(SyncConfiguration.PREF_NUM_CLIENTS, clientsCount).commit();
+ }
+
+ @Override
+ public boolean isLocalGUID(String guid) {
+ return getAccountGUID().equals(guid);
+ }
+
+ @Override
+ public synchronized int getClientsCount() {
+ return (int) sharedPreferences.getLong(SyncConfiguration.PREF_NUM_CLIENTS, 0);
+ }
+
+ @Override
+ public long getLastModifiedTimestamp() {
+ return sharedPreferences.getLong(SyncConfiguration.PREF_CLIENT_DATA_TIMESTAMP, 0);
+ }
+
+ @Override
+ public String getFormFactor() {
+ if (HardwareUtils.isLargeTablet()) {
+ return "largetablet";
+ }
+
+ if (HardwareUtils.isSmallTablet()) {
+ return "smalltablet";
+ }
+
+ if (HardwareUtils.isTelevision()) {
+ return "tv";
+ }
+
+ return "phone";
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Sync11Configuration.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Sync11Configuration.java
new file mode 100644
index 0000000000..4b22808955
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Sync11Configuration.java
@@ -0,0 +1,84 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import java.net.URI;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+
+/**
+ * Override SyncConfiguration to restore the old behavior of clusterURL --
+ * that is, a URL without the protocol version etc.
+ *
+ */
+public class Sync11Configuration extends SyncConfiguration {
+ private static final String LOG_TAG = "Sync11Configuration";
+ private static final String API_VERSION = "1.1";
+
+ public Sync11Configuration(String username,
+ AuthHeaderProvider authHeaderProvider,
+ SharedPreferences prefs) {
+ super(username, authHeaderProvider, prefs);
+ }
+
+ public Sync11Configuration(String username,
+ AuthHeaderProvider authHeaderProvider,
+ SharedPreferences prefs,
+ KeyBundle keyBundle) {
+ super(username, authHeaderProvider, prefs, keyBundle);
+ }
+
+ @Override
+ public String getAPIVersion() {
+ return API_VERSION;
+ }
+
+ @Override
+ public String storageURL() {
+ return clusterURL + API_VERSION + "/" + username + "/storage";
+ }
+
+ @Override
+ protected String infoBaseURL() {
+ return clusterURL + API_VERSION + "/" + username + "/info/";
+ }
+
+ protected void setAndPersistClusterURL(URI u, SharedPreferences prefs) {
+ boolean shouldPersist = (prefs != null) && (clusterURL == null);
+
+ Logger.trace(LOG_TAG, "Setting cluster URL to " + u.toASCIIString() +
+ (shouldPersist ? ". Persisting." : ". Not persisting."));
+ clusterURL = u;
+ if (shouldPersist) {
+ Editor edit = prefs.edit();
+ edit.putString(PREF_CLUSTER_URL, clusterURL.toASCIIString());
+ edit.commit();
+ }
+ }
+
+ protected void setClusterURL(URI u, SharedPreferences prefs) {
+ if (u == null) {
+ Logger.warn(LOG_TAG, "Refusing to set cluster URL to null.");
+ return;
+ }
+ URI uri = u.normalize();
+ if (uri.toASCIIString().endsWith("/")) {
+ setAndPersistClusterURL(u, prefs);
+ return;
+ }
+ setAndPersistClusterURL(uri.resolve("/"), prefs);
+ Logger.trace(LOG_TAG, "Set cluster URL to " + clusterURL.toASCIIString() + ", given input " + u.toASCIIString());
+ }
+
+ @Override
+ public void setClusterURL(URI u) {
+ setClusterURL(u, this.getPrefs());
+ }
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConfiguration.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConfiguration.java
new file mode 100644
index 0000000000..53edf5f846
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConfiguration.java
@@ -0,0 +1,480 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.mozilla.gecko.background.common.PrefsBranch;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.crypto.PersistedCrypto5Keys;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+
+public class SyncConfiguration {
+ private static final String LOG_TAG = "SyncConfiguration";
+
+ // These must be set in GlobalSession's constructor.
+ public URI clusterURL;
+ public KeyBundle syncKeyBundle;
+
+ public InfoConfiguration infoConfiguration;
+
+ public CollectionKeys collectionKeys;
+ public InfoCollections infoCollections;
+ public MetaGlobal metaGlobal;
+ public String syncID;
+
+ protected final String username;
+
+ /**
+ * Persisted collection of enabledEngineNames.
+ * <p>
+ * Can contain engines Android Sync is not currently aware of, such as "prefs"
+ * or "addons".
+ * <p>
+ * Copied from latest downloaded meta/global record and used to generate a
+ * fresh meta/global record for upload.
+ */
+ public Set<String> enabledEngineNames;
+ public Set<String> declinedEngineNames = new HashSet<String>();
+
+ /**
+ * Names of stages to sync <it>this sync</it>, or <code>null</code> to sync
+ * all known stages.
+ * <p>
+ * Generated <it>each sync</it> from extras bundle passed to
+ * <code>SyncAdapter.onPerformSync</code> and not persisted.
+ * <p>
+ * Not synchronized! Set this exactly once per global session and don't modify
+ * it -- especially not from multiple threads.
+ */
+ public Collection<String> stagesToSync;
+
+ /**
+ * Engines whose sync state has been modified by the user through
+ * SelectEnginesActivity, where each key-value pair is an engine name and
+ * its sync state.
+ *
+ * This differs from <code>enabledEngineNames</code> in that
+ * <code>enabledEngineNames</code> reflects the downloaded meta/global,
+ * whereas <code>userSelectedEngines</code> stores the differences in engines to
+ * sync that the user has selected.
+ *
+ * Each engine stage will check for engine changes at the beginning of the
+ * stage.
+ *
+ * If no engine sync state changes have been made by the user, userSelectedEngines
+ * will be null, and Sync will proceed normally.
+ *
+ * If the user has made changes to engine syncing state, each engine will sync
+ * according to the sync state specified in userSelectedEngines and propagate that
+ * state to meta/global, to be uploaded.
+ */
+ public Map<String, Boolean> userSelectedEngines;
+ public long userSelectedEnginesTimestamp;
+
+ public SharedPreferences prefs;
+
+ protected final AuthHeaderProvider authHeaderProvider;
+
+ public static final String PREF_PREFS_VERSION = "prefs.version";
+ public static final long CURRENT_PREFS_VERSION = 1;
+
+ public static final String CLIENTS_COLLECTION_TIMESTAMP = "serverClientsTimestamp"; // When the collection was touched.
+ public static final String CLIENT_RECORD_TIMESTAMP = "serverClientRecordTimestamp"; // When our record was touched.
+ public static final String MIGRATION_SENTINEL_CHECK_TIMESTAMP = "migrationSentinelCheckTimestamp"; // When we last looked in meta/fxa_credentials.
+
+ public static final String PREF_CLUSTER_URL = "clusterURL";
+ public static final String PREF_SYNC_ID = "syncID";
+
+ public static final String PREF_ENABLED_ENGINE_NAMES = "enabledEngineNames";
+ public static final String PREF_DECLINED_ENGINE_NAMES = "declinedEngineNames";
+ public static final String PREF_USER_SELECTED_ENGINES_TO_SYNC = "userSelectedEngines";
+ public static final String PREF_USER_SELECTED_ENGINES_TO_SYNC_TIMESTAMP = "userSelectedEnginesTimestamp";
+
+ public static final String PREF_CLUSTER_URL_IS_STALE = "clusterurlisstale";
+
+ public static final String PREF_ACCOUNT_GUID = "account.guid";
+ public static final String PREF_CLIENT_NAME = "account.clientName";
+ public static final String PREF_NUM_CLIENTS = "account.numClients";
+ public static final String PREF_CLIENT_DATA_TIMESTAMP = "account.clientDataTimestamp";
+
+ private static final String API_VERSION = "1.5";
+
+ public SyncConfiguration(String username, AuthHeaderProvider authHeaderProvider, SharedPreferences prefs) {
+ this.username = username;
+ this.authHeaderProvider = authHeaderProvider;
+ this.prefs = prefs;
+ this.loadFromPrefs(prefs);
+ }
+
+ public SyncConfiguration(String username, AuthHeaderProvider authHeaderProvider, SharedPreferences prefs, KeyBundle syncKeyBundle) {
+ this(username, authHeaderProvider, prefs);
+ this.syncKeyBundle = syncKeyBundle;
+ }
+
+ public String getAPIVersion() {
+ return API_VERSION;
+ }
+
+ public SharedPreferences getPrefs() {
+ return this.prefs;
+ }
+
+ /**
+ * Valid engines supported by Android Sync.
+ *
+ * @return Set<String> of valid engine names that Android Sync implements.
+ */
+ public static Set<String> validEngineNames() {
+ Set<String> engineNames = new HashSet<String>();
+ for (Stage stage : Stage.getNamedStages()) {
+ engineNames.add(stage.getRepositoryName());
+ }
+ return engineNames;
+ }
+
+ /**
+ * Return a convenient accessor for part of prefs.
+ * @return
+ * A PrefsBranch object representing this
+ * section of the preferences space.
+ */
+ public PrefsBranch getBranch(String prefix) {
+ return new PrefsBranch(this.getPrefs(), prefix);
+ }
+
+ /**
+ * Gets the engine names that are enabled, declined, or other (depending on pref) in meta/global.
+ *
+ * @param prefs
+ * SharedPreferences that the engines are associated with.
+ * @param pref
+ * The preference name to use. E.g, PREF_ENABLED_ENGINE_NAMES.
+ * @return Set<String> of the enabled engine names if they have been stored,
+ * or null otherwise.
+ */
+ protected static Set<String> getEngineNamesFromPref(SharedPreferences prefs, String pref) {
+ final String json = prefs.getString(pref, null);
+ if (json == null) {
+ return null;
+ }
+ try {
+ final ExtendedJSONObject o = new ExtendedJSONObject(json);
+ return new HashSet<String>(o.keySet());
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the set of engine names that the user has enabled. If none
+ * have been stored in prefs, <code>null</code> is returned.
+ */
+ public static Set<String> getEnabledEngineNames(SharedPreferences prefs) {
+ return getEngineNamesFromPref(prefs, PREF_ENABLED_ENGINE_NAMES);
+ }
+
+ /**
+ * Returns the set of engine names that the user has declined.
+ */
+ public static Set<String> getDeclinedEngineNames(SharedPreferences prefs) {
+ final Set<String> names = getEngineNamesFromPref(prefs, PREF_DECLINED_ENGINE_NAMES);
+ if (names == null) {
+ return new HashSet<String>();
+ }
+ return names;
+ }
+
+ /**
+ * Gets the engines whose sync states have been changed by the user through the
+ * SelectEnginesActivity.
+ *
+ * @param prefs
+ * SharedPreferences of account that the engines are associated with.
+ * @return Map<String, Boolean> of changed engines. Key is the lower-cased
+ * engine name, Value is the new sync state.
+ */
+ public static Map<String, Boolean> getUserSelectedEngines(SharedPreferences prefs) {
+ String json = prefs.getString(PREF_USER_SELECTED_ENGINES_TO_SYNC, null);
+ if (json == null) {
+ return null;
+ }
+ try {
+ ExtendedJSONObject o = new ExtendedJSONObject(json);
+ Map<String, Boolean> map = new HashMap<String, Boolean>();
+ for (Entry<String, Object> e : o.entrySet()) {
+ String key = e.getKey();
+ Boolean value = (Boolean) e.getValue();
+ map.put(key, value);
+ // Forms depends on history. Add forms if history is selected.
+ if ("history".equals(key)) {
+ map.put("forms", value);
+ }
+ }
+ // Sanity check: remove forms if history does not exist.
+ if (!map.containsKey("history")) {
+ map.remove("forms");
+ }
+ return map;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * Store a Map of engines and their sync states to prefs.
+ *
+ * Any engine that's disabled in the input is also recorded
+ * as a declined engine, overwriting the stored values.
+ *
+ * @param prefs
+ * SharedPreferences that the engines are associated with.
+ * @param selectedEngines
+ * Map<String, Boolean> of engine name to sync state
+ */
+ public static void storeSelectedEnginesToPrefs(SharedPreferences prefs, Map<String, Boolean> selectedEngines) {
+ ExtendedJSONObject jObj = new ExtendedJSONObject();
+ HashSet<String> declined = new HashSet<String>();
+ for (Entry<String, Boolean> e : selectedEngines.entrySet()) {
+ final Boolean enabled = e.getValue();
+ final String engine = e.getKey();
+ jObj.put(engine, enabled);
+ if (!enabled) {
+ declined.add(engine);
+ }
+ }
+
+ // Our history checkbox drives form history, too.
+ // We don't need to do this for enablement: that's done at retrieval time.
+ if (selectedEngines.containsKey("history") && !selectedEngines.get("history")) {
+ declined.add("forms");
+ }
+
+ String json = jObj.toJSONString();
+ long currentTime = System.currentTimeMillis();
+ Editor edit = prefs.edit();
+ edit.putString(PREF_USER_SELECTED_ENGINES_TO_SYNC, json);
+ edit.putString(PREF_DECLINED_ENGINE_NAMES, setToJSONObjectString(declined));
+ edit.putLong(PREF_USER_SELECTED_ENGINES_TO_SYNC_TIMESTAMP, currentTime);
+ Logger.error(LOG_TAG, "Storing user-selected engines at [" + currentTime + "].");
+ edit.commit();
+ }
+
+ public void loadFromPrefs(SharedPreferences prefs) {
+ if (prefs.contains(PREF_CLUSTER_URL)) {
+ String u = prefs.getString(PREF_CLUSTER_URL, null);
+ try {
+ clusterURL = new URI(u);
+ Logger.trace(LOG_TAG, "Set clusterURL from bundle: " + u);
+ } catch (URISyntaxException e) {
+ Logger.warn(LOG_TAG, "Ignoring bundle clusterURL (" + u + "): invalid URI.", e);
+ }
+ }
+ if (prefs.contains(PREF_SYNC_ID)) {
+ syncID = prefs.getString(PREF_SYNC_ID, null);
+ Logger.trace(LOG_TAG, "Set syncID from bundle: " + syncID);
+ }
+ enabledEngineNames = getEnabledEngineNames(prefs);
+ declinedEngineNames = getDeclinedEngineNames(prefs);
+ userSelectedEngines = getUserSelectedEngines(prefs);
+ userSelectedEnginesTimestamp = prefs.getLong(PREF_USER_SELECTED_ENGINES_TO_SYNC_TIMESTAMP, 0);
+ // We don't set crypto/keys here because we need the syncKeyBundle to decrypt the JSON
+ // and we won't have it on construction.
+ // TODO: MetaGlobal, password, infoCollections.
+ }
+
+ public void persistToPrefs() {
+ this.persistToPrefs(this.getPrefs());
+ }
+
+ private static String setToJSONObjectString(Set<String> set) {
+ ExtendedJSONObject o = new ExtendedJSONObject();
+ for (String name : set) {
+ o.put(name, 0);
+ }
+ return o.toJSONString();
+ }
+
+ public void persistToPrefs(SharedPreferences prefs) {
+ Editor edit = prefs.edit();
+ if (clusterURL == null) {
+ edit.remove(PREF_CLUSTER_URL);
+ } else {
+ edit.putString(PREF_CLUSTER_URL, clusterURL.toASCIIString());
+ }
+ if (syncID != null) {
+ edit.putString(PREF_SYNC_ID, syncID);
+ }
+ if (enabledEngineNames == null) {
+ edit.remove(PREF_ENABLED_ENGINE_NAMES);
+ } else {
+ edit.putString(PREF_ENABLED_ENGINE_NAMES, setToJSONObjectString(enabledEngineNames));
+ }
+ if (declinedEngineNames == null || declinedEngineNames.isEmpty()) {
+ edit.remove(PREF_DECLINED_ENGINE_NAMES);
+ } else {
+ edit.putString(PREF_DECLINED_ENGINE_NAMES, setToJSONObjectString(declinedEngineNames));
+ }
+ if (userSelectedEngines == null) {
+ edit.remove(PREF_USER_SELECTED_ENGINES_TO_SYNC);
+ edit.remove(PREF_USER_SELECTED_ENGINES_TO_SYNC_TIMESTAMP);
+ }
+ // Don't bother saving userSelectedEngines - these should only be changed by
+ // SelectEnginesActivity.
+ edit.commit();
+ // TODO: keys.
+ }
+
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return authHeaderProvider;
+ }
+
+ public CollectionKeys getCollectionKeys() {
+ return collectionKeys;
+ }
+
+ public void setCollectionKeys(CollectionKeys k) {
+ collectionKeys = k;
+ }
+
+ /**
+ * Return path to storage endpoint without trailing slash.
+ *
+ * @return storage endpoint without trailing slash.
+ */
+ public String storageURL() {
+ return clusterURL + "/storage";
+ }
+
+ protected String infoBaseURL() {
+ return clusterURL + "/info/";
+ }
+
+ public String infoCollectionsURL() {
+ return infoBaseURL() + "collections";
+ }
+
+ public String infoConfigurationURL() {
+ return infoBaseURL() + "configuration";
+ }
+
+ public String infoCollectionCountsURL() {
+ return infoBaseURL() + "collection_counts";
+ }
+
+ public String metaURL() {
+ return storageURL() + "/meta/global";
+ }
+
+ public URI collectionURI(String collection) throws URISyntaxException {
+ return new URI(storageURL() + "/" + collection);
+ }
+
+ public URI collectionURI(String collection, boolean full) throws URISyntaxException {
+ // Do it this way to make it easier to add more params later.
+ // It's pretty ugly, I'll grant.
+ boolean anyParams = full;
+ String uriParams = "";
+ if (anyParams) {
+ StringBuilder params = new StringBuilder("?");
+ if (full) {
+ params.append("full=1");
+ }
+ uriParams = params.toString();
+ }
+ String uri = storageURL() + "/" + collection + uriParams;
+ return new URI(uri);
+ }
+
+ public URI wboURI(String collection, String id) throws URISyntaxException {
+ return new URI(storageURL() + "/" + collection + "/" + id);
+ }
+
+ public URI keysURI() throws URISyntaxException {
+ return wboURI("crypto", "keys");
+ }
+
+ public URI getClusterURL() {
+ return clusterURL;
+ }
+
+ public String getClusterURLString() {
+ if (clusterURL == null) {
+ return null;
+ }
+ return clusterURL.toASCIIString();
+ }
+
+ public void setClusterURL(URI u) {
+ this.clusterURL = u;
+ }
+
+ /**
+ * Used for direct management of related prefs.
+ */
+ public Editor getEditor() {
+ return this.getPrefs().edit();
+ }
+
+ /**
+ * We persist two different clients timestamps: our own record's,
+ * and the timestamp for the collection.
+ */
+ public void persistServerClientRecordTimestamp(long timestamp) {
+ getEditor().putLong(SyncConfiguration.CLIENT_RECORD_TIMESTAMP, timestamp).commit();
+ }
+
+ public long getPersistedServerClientRecordTimestamp() {
+ return getPrefs().getLong(SyncConfiguration.CLIENT_RECORD_TIMESTAMP, 0L);
+ }
+
+ public void persistServerClientsTimestamp(long timestamp) {
+ getEditor().putLong(SyncConfiguration.CLIENTS_COLLECTION_TIMESTAMP, timestamp).commit();
+ }
+
+ public long getPersistedServerClientsTimestamp() {
+ return getPrefs().getLong(SyncConfiguration.CLIENTS_COLLECTION_TIMESTAMP, 0L);
+ }
+
+ public void persistLastMigrationSentinelCheckTimestamp(long timestamp) {
+ getEditor().putLong(SyncConfiguration.MIGRATION_SENTINEL_CHECK_TIMESTAMP, timestamp).commit();
+ }
+
+ public long getLastMigrationSentinelCheckTimestamp() {
+ return getPrefs().getLong(SyncConfiguration.MIGRATION_SENTINEL_CHECK_TIMESTAMP, 0L);
+ }
+
+ public void purgeCryptoKeys() {
+ if (collectionKeys != null) {
+ collectionKeys.clear();
+ }
+ persistedCryptoKeys().purge();
+ }
+
+ public void purgeMetaGlobal() {
+ metaGlobal = null;
+ persistedMetaGlobal().purge();
+ }
+
+ public PersistedCrypto5Keys persistedCryptoKeys() {
+ return new PersistedCrypto5Keys(getPrefs(), syncKeyBundle);
+ }
+
+ public PersistedMetaGlobal persistedMetaGlobal() {
+ return new PersistedMetaGlobal(getPrefs());
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConfigurationException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConfigurationException.java
new file mode 100644
index 0000000000..02ba118c56
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConfigurationException.java
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import android.content.SyncResult;
+
+public class SyncConfigurationException extends SyncException {
+ private static final long serialVersionUID = 1107080177269358381L;
+
+ @Override
+ public void updateStats(GlobalSession globalSession, SyncResult syncResult) {
+ syncResult.stats.numAuthExceptions++;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConstants.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConstants.java
new file mode 100644
index 0000000000..5dc7b289f6
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConstants.java
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import org.mozilla.gecko.AppConstants;
+
+public class SyncConstants {
+ public static final String GLOBAL_LOG_TAG = "FxSync";
+ public static final String SYNC_MAJOR_VERSION = "1";
+ public static final String SYNC_MINOR_VERSION = "0";
+ public static final String SYNC_VERSION_STRING = SYNC_MAJOR_VERSION + "." +
+ AppConstants.MOZ_APP_VERSION + "." +
+ SYNC_MINOR_VERSION;
+
+ public static final String USER_AGENT = "Firefox AndroidSync " +
+ SYNC_VERSION_STRING + " (" +
+ AppConstants.MOZ_APP_UA_NAME + ")";
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncException.java
new file mode 100644
index 0000000000..ee09025681
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncException.java
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import android.content.SyncResult;
+
+public abstract class SyncException extends Exception {
+ private static final long serialVersionUID = -6928990004393234738L;
+
+ public SyncException() {
+ super();
+ }
+
+ public SyncException(final Throwable e) {
+ super(e);
+ }
+
+ /**
+ * Update sync result statistics with information particular to this
+ * exception.
+ *
+ * @param globalSession
+ * current session, or null.
+ * @param syncResult
+ * Android sync result to update.
+ */
+ public void updateStats(GlobalSession globalSession, SyncResult syncResult) {
+ // Assume storage error.
+ // TODO: this logic is overly simplistic.
+ syncResult.databaseError = true;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SynchronizerConfiguration.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SynchronizerConfiguration.java
new file mode 100644
index 0000000000..2b08be9c4e
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SynchronizerConfiguration.java
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import android.content.SharedPreferences.Editor;
+
+import org.mozilla.gecko.background.common.PrefsBranch;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
+
+import java.io.IOException;
+
+public class SynchronizerConfiguration {
+ private static final String LOG_TAG = "SynczrConfiguration";
+
+ public String syncID;
+ public RepositorySessionBundle remoteBundle;
+ public RepositorySessionBundle localBundle;
+
+ public SynchronizerConfiguration(PrefsBranch config) throws NonObjectJSONException, IOException {
+ this.load(config);
+ }
+
+ public SynchronizerConfiguration(String syncID, RepositorySessionBundle remoteBundle, RepositorySessionBundle localBundle) {
+ this.syncID = syncID;
+ this.remoteBundle = remoteBundle;
+ this.localBundle = localBundle;
+ }
+
+ // This should get partly shuffled back into SyncConfiguration, I think.
+ public void load(PrefsBranch config) throws NonObjectJSONException, IOException {
+ if (config == null) {
+ throw new IllegalArgumentException("config cannot be null.");
+ }
+ String remoteJSON = config.getString("remote", null);
+ String localJSON = config.getString("local", null);
+ RepositorySessionBundle rB = new RepositorySessionBundle(remoteJSON);
+ RepositorySessionBundle lB = new RepositorySessionBundle(localJSON);
+ if (remoteJSON == null) {
+ rB.setTimestamp(0);
+ }
+ if (localJSON == null) {
+ lB.setTimestamp(0);
+ }
+ syncID = config.getString("syncID", null);
+ remoteBundle = rB;
+ localBundle = lB;
+ Logger.debug(LOG_TAG, "Loaded SynchronizerConfiguration. syncID: " + syncID + ", remoteBundle: " + remoteBundle + ", localBundle: " + localBundle);
+ }
+
+ public void persist(PrefsBranch config) {
+ if (config == null) {
+ throw new IllegalArgumentException("config cannot be null.");
+ }
+ String jsonRemote = remoteBundle.toJSONString();
+ String jsonLocal = localBundle.toJSONString();
+ Editor editor = config.edit();
+ editor.putString("remote", jsonRemote);
+ editor.putString("local", jsonLocal);
+ editor.putString("syncID", syncID);
+
+ // Synchronous.
+ editor.commit();
+ Logger.debug(LOG_TAG, "Persisted SynchronizerConfiguration. syncID: " + syncID + ", remoteBundle: " + remoteBundle + ", localBundle: " + localBundle);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/ThreadPool.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/ThreadPool.java
new file mode 100644
index 0000000000..7f2029566d
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/ThreadPool.java
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class ThreadPool {
+ public static ExecutorService executorService = Executors.newCachedThreadPool();
+ public static void run(Runnable runnable) {
+ executorService.submit(runnable);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/UnexpectedJSONException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/UnexpectedJSONException.java
new file mode 100644
index 0000000000..e5771452ca
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/UnexpectedJSONException.java
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+public class UnexpectedJSONException extends Exception {
+ private static final long serialVersionUID = 4797570033096443169L;
+
+ public UnexpectedJSONException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ public UnexpectedJSONException(Throwable throwable) {
+ super(throwable);
+ }
+
+ public static class BadRequiredFieldJSONException extends UnexpectedJSONException {
+ private static final long serialVersionUID = -9207736984784497612L;
+
+ public BadRequiredFieldJSONException(String string) {
+ super(string);
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/UnknownSynchronizerConfigurationVersionException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/UnknownSynchronizerConfigurationVersionException.java
new file mode 100644
index 0000000000..e2350095eb
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/UnknownSynchronizerConfigurationVersionException.java
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+public class UnknownSynchronizerConfigurationVersionException extends
+ SyncConfigurationException {
+ public int badVersion;
+ private static final long serialVersionUID = -8497255862099517395L;
+
+ public UnknownSynchronizerConfigurationVersionException(int version) {
+ super();
+ badVersion = version;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Utils.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Utils.java
new file mode 100644
index 0000000000..ef8859b4a4
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Utils.java
@@ -0,0 +1,575 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.net.URLDecoder;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.Executor;
+
+import org.json.simple.JSONArray;
+import org.mozilla.apache.commons.codec.binary.Base32;
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.nativecode.NativeCrypto;
+import org.mozilla.gecko.sync.setup.Constants;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+
+public class Utils {
+
+ private static final String LOG_TAG = "Utils";
+
+ private static final SecureRandom sharedSecureRandom = new SecureRandom();
+
+ // See <http://developer.android.com/reference/android/content/Context.html#getSharedPreferences%28java.lang.String,%20int%29>
+ public static final int SHARED_PREFERENCES_MODE = 0;
+
+ public static String generateGuid() {
+ byte[] encodedBytes = Base64.encodeBase64(generateRandomBytes(9), false);
+ return new String(encodedBytes).replace("+", "-").replace("/", "_");
+ }
+
+ /**
+ * Helper to generate secure random bytes.
+ *
+ * @param length
+ * Number of bytes to generate.
+ */
+ public static byte[] generateRandomBytes(int length) {
+ byte[] bytes = new byte[length];
+ sharedSecureRandom.nextBytes(bytes);
+ return bytes;
+ }
+
+ /**
+ * Helper to generate a random integer in a specified range.
+ *
+ * @param r
+ * Generate an integer between 0 and r-1 inclusive.
+ */
+ public static BigInteger generateBigIntegerLessThan(BigInteger r) {
+ int maxBytes = (int) Math.ceil(((double) r.bitLength()) / 8);
+ BigInteger randInt = new BigInteger(generateRandomBytes(maxBytes));
+ return randInt.mod(r);
+ }
+
+ /**
+ * Helper to convert a byte array to a hex-encoded string
+ */
+ public static String byte2Hex(final byte[] b) {
+ return byte2Hex(b, 2 * b.length);
+ }
+
+ public static String byte2Hex(final byte[] b, int hexLength) {
+ final StringBuilder hs = new StringBuilder(Math.max(2*b.length, hexLength));
+ String stmp;
+
+ for (int n = 0; n < hexLength - 2*b.length; n++) {
+ hs.append("0");
+ }
+
+ for (int n = 0; n < b.length; n++) {
+ stmp = Integer.toHexString(b[n] & 0XFF);
+
+ if (stmp.length() == 1) {
+ hs.append("0");
+ }
+ hs.append(stmp);
+ }
+
+ return hs.toString();
+ }
+
+ public static byte[] concatAll(byte[] first, byte[]... rest) {
+ int totalLength = first.length;
+ for (byte[] array : rest) {
+ totalLength += array.length;
+ }
+
+ byte[] result = new byte[totalLength];
+ int offset = first.length;
+
+ System.arraycopy(first, 0, result, 0, offset);
+
+ for (byte[] array : rest) {
+ System.arraycopy(array, 0, result, offset, array.length);
+ offset += array.length;
+ }
+ return result;
+ }
+
+ /**
+ * Utility for Base64 decoding. Should ensure that the correct
+ * Apache Commons version is used.
+ *
+ * @param base64
+ * An input string. Will be decoded as UTF-8.
+ * @return
+ * A byte array of decoded values.
+ * @throws UnsupportedEncodingException
+ * Should not occur.
+ */
+ public static byte[] decodeBase64(String base64) throws UnsupportedEncodingException {
+ return Base64.decodeBase64(base64.getBytes("UTF-8"));
+ }
+
+ public static byte[] decodeFriendlyBase32(String base32) {
+ Base32 converter = new Base32();
+ final String translated = base32.replace('8', 'l').replace('9', 'o');
+ return converter.decode(translated.toUpperCase(Locale.US));
+ }
+
+ public static byte[] hex2Byte(String str, int byteLength) {
+ byte[] second = hex2Byte(str);
+ if (second.length >= byteLength) {
+ return second;
+ }
+ // New Java arrays are zeroed:
+ // http://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.12.5
+ byte[] first = new byte[byteLength - second.length];
+ return Utils.concatAll(first, second);
+ }
+
+ public static byte[] hex2Byte(String str) {
+ if (str.length() % 2 == 1) {
+ str = "0" + str;
+ }
+
+ byte[] bytes = new byte[str.length() / 2];
+ for (int i = 0; i < bytes.length; i++) {
+ bytes[i] = (byte) Integer.parseInt(str.substring(2 * i, 2 * i + 2), 16);
+ }
+ return bytes;
+ }
+
+ public static String millisecondsToDecimalSecondsString(long ms) {
+ return millisecondsToDecimalSeconds(ms).toString();
+ }
+
+ // For dumping into JSON without quotes.
+ public static BigDecimal millisecondsToDecimalSeconds(long ms) {
+ return new BigDecimal(ms).movePointLeft(3);
+ }
+
+ // This lives until Bug 708956 lands, and we don't have to do it any more.
+ public static long decimalSecondsToMilliseconds(String decimal) {
+ try {
+ return new BigDecimal(decimal).movePointRight(3).longValue();
+ } catch (Exception e) {
+ return -1;
+ }
+ }
+
+ // Oh, Java.
+ public static long decimalSecondsToMilliseconds(Double decimal) {
+ // Truncates towards 0.
+ return (long)(decimal * 1000);
+ }
+
+ public static long decimalSecondsToMilliseconds(Long decimal) {
+ return decimal * 1000;
+ }
+
+ public static long decimalSecondsToMilliseconds(Integer decimal) {
+ return (decimal * 1000);
+ }
+
+ public static byte[] sha256(byte[] in)
+ throws NoSuchAlgorithmException {
+ MessageDigest sha1 = MessageDigest.getInstance("SHA-256");
+ return sha1.digest(in);
+ }
+
+ protected static byte[] sha1(final String utf8)
+ throws NoSuchAlgorithmException, UnsupportedEncodingException {
+ final byte[] bytes = utf8.getBytes("UTF-8");
+ try {
+ return NativeCrypto.sha1(bytes);
+ } catch (final LinkageError e) {
+ // This will throw UnsatisifiedLinkError (missing mozglue) the first time it is called, and
+ // ClassNotDefFoundError, for the uninitialized NativeCrypto class, each subsequent time this
+ // is called; LinkageError is their common ancestor.
+ Logger.warn(LOG_TAG, "Got throwable stretching password using native sha1 implementation; " +
+ "ignoring and using Java implementation.", e);
+ final MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
+ return sha1.digest(utf8.getBytes("UTF-8"));
+ }
+ }
+
+ protected static String sha1Base32(final String utf8)
+ throws NoSuchAlgorithmException, UnsupportedEncodingException {
+ return new Base32().encodeAsString(sha1(utf8)).toLowerCase(Locale.US);
+ }
+
+ /**
+ * If we encounter characters not allowed by the API (as found for
+ * instance in an email address), hash the value.
+ * @param account
+ * An account string.
+ * @return
+ * An acceptable string.
+ * @throws UnsupportedEncodingException
+ * @throws NoSuchAlgorithmException
+ */
+ public static String usernameFromAccount(final String account) throws NoSuchAlgorithmException, UnsupportedEncodingException {
+ if (account == null || account.equals("")) {
+ throw new IllegalArgumentException("No account name provided.");
+ }
+ if (account.matches("^[A-Za-z0-9._-]+$")) {
+ return account.toLowerCase(Locale.US);
+ }
+ return sha1Base32(account.toLowerCase(Locale.US));
+ }
+
+ public static SharedPreferences getSharedPreferences(final Context context, final String product, final String username, final String serverURL, final String profile, final long version)
+ throws NoSuchAlgorithmException, UnsupportedEncodingException {
+ String prefsPath = getPrefsPath(product, username, serverURL, profile, version);
+ return context.getSharedPreferences(prefsPath, SHARED_PREFERENCES_MODE);
+ }
+
+ /**
+ * Get shared preferences path for a Sync account.
+ *
+ * @param product the Firefox Sync product package name (like "org.mozilla.firefox").
+ * @param username the Sync account name, optionally encoded with <code>Utils.usernameFromAccount</code>.
+ * @param serverURL the Sync account server URL.
+ * @param profile the Firefox profile name.
+ * @param version the version of preferences to reference.
+ * @return the path.
+ * @throws NoSuchAlgorithmException
+ * @throws UnsupportedEncodingException
+ */
+ public static String getPrefsPath(final String product, final String username, final String serverURL, final String profile, final long version)
+ throws NoSuchAlgorithmException, UnsupportedEncodingException {
+ final String encodedAccount = sha1Base32(serverURL + ":" + usernameFromAccount(username));
+
+ if (version <= 0) {
+ return "sync.prefs." + encodedAccount;
+ } else {
+ final String sanitizedProduct = product.replace('.', '!').replace(' ', '!');
+ return "sync.prefs." + sanitizedProduct + "." + encodedAccount + "." + profile + "." + version;
+ }
+ }
+
+ public static void addToIndexBucketMap(TreeMap<Long, ArrayList<String>> map, long index, String value) {
+ ArrayList<String> bucket = map.get(index);
+ if (bucket == null) {
+ bucket = new ArrayList<String>();
+ }
+ bucket.add(value);
+ map.put(index, bucket);
+ }
+
+ /**
+ * Yes, an equality method that's null-safe.
+ */
+ private static boolean same(Object a, Object b) {
+ if (a == b) {
+ return true;
+ }
+ if (a == null || b == null) {
+ return false; // If both null, case above applies.
+ }
+ return a.equals(b);
+ }
+
+ /**
+ * Return true if the two arrays are both null, or are both arrays
+ * containing the same elements in the same order.
+ */
+ public static boolean sameArrays(JSONArray a, JSONArray b) {
+ if (a == b) {
+ return true;
+ }
+ if (a == null || b == null) {
+ return false;
+ }
+ final int size = a.size();
+ if (size != b.size()) {
+ return false;
+ }
+ for (int i = 0; i < size; ++i) {
+ if (!same(a.get(i), b.get(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Takes a URI, extracting URI components.
+ * @param scheme the URI scheme on which to match.
+ */
+ @SuppressWarnings("deprecation")
+ public static Map<String, String> extractURIComponents(String scheme, String uri) {
+ if (uri.indexOf(scheme) != 0) {
+ throw new IllegalArgumentException("URI scheme does not match: " + scheme);
+ }
+
+ // Do this the hard way to avoid taking a large dependency on
+ // HttpClient or getting all regex-tastic.
+ String components = uri.substring(scheme.length());
+ HashMap<String, String> out = new HashMap<String, String>();
+ String[] parts = components.split("&");
+ for (int i = 0; i < parts.length; ++i) {
+ String part = parts[i];
+ if (part.length() == 0) {
+ continue;
+ }
+ String[] pair = part.split("=", 2);
+ switch (pair.length) {
+ case 0:
+ continue;
+ case 1:
+ out.put(URLDecoder.decode(pair[0]), null);
+ break;
+ case 2:
+ out.put(URLDecoder.decode(pair[0]), URLDecoder.decode(pair[1]));
+ break;
+ }
+ }
+ return out;
+ }
+
+ // Because TextUtils.join is not stubbed.
+ public static String toDelimitedString(String delimiter, Collection<? extends Object> items) {
+ if (items == null || items.size() == 0) {
+ return "";
+ }
+
+ StringBuilder sb = new StringBuilder();
+ int i = 0;
+ int c = items.size();
+ for (Object object : items) {
+ sb.append(object.toString());
+ if (++i < c) {
+ sb.append(delimiter);
+ }
+ }
+ return sb.toString();
+ }
+
+ public static String toCommaSeparatedString(Collection<? extends Object> items) {
+ return toDelimitedString(", ", items);
+ }
+
+ /**
+ * Names of stages to sync: (ALL intersect SYNC) intersect (ALL minus SKIP).
+ *
+ * @param knownStageNames collection of known stage names (set ALL above).
+ * @param toSync set SYNC above, or <code>null</code> to sync all known stages.
+ * @param toSkip set SKIP above, or <code>null</code> to not skip any stages.
+ * @return stage names.
+ */
+ public static Collection<String> getStagesToSync(final Collection<String> knownStageNames, Collection<String> toSync, Collection<String> toSkip) {
+ if (toSkip == null) {
+ toSkip = new HashSet<String>();
+ } else {
+ toSkip = new HashSet<String>(toSkip);
+ }
+
+ if (toSync == null) {
+ toSync = new HashSet<String>(knownStageNames);
+ } else {
+ toSync = new HashSet<String>(toSync);
+ }
+ toSync.retainAll(knownStageNames);
+ toSync.removeAll(toSkip);
+ return toSync;
+ }
+
+ /**
+ * Get names of stages to sync: (ALL intersect SYNC) intersect (ALL minus SKIP).
+ *
+ * @param knownStageNames collection of known stage names (set ALL above).
+ * @param extras
+ * a <code>Bundle</code> instance (possibly null) optionally containing keys
+ * <code>EXTRAS_KEY_STAGES_TO_SYNC</code> (set SYNC above) and
+ * <code>EXTRAS_KEY_STAGES_TO_SKIP</code> (set SKIP above).
+ * @return stage names.
+ */
+ public static Collection<String> getStagesToSyncFromBundle(final Collection<String> knownStageNames, final Bundle extras) {
+ if (extras == null) {
+ return knownStageNames;
+ }
+ String toSyncString = extras.getString(Constants.EXTRAS_KEY_STAGES_TO_SYNC);
+ String toSkipString = extras.getString(Constants.EXTRAS_KEY_STAGES_TO_SKIP);
+ if (toSyncString == null && toSkipString == null) {
+ return knownStageNames;
+ }
+
+ ArrayList<String> toSync = null;
+ ArrayList<String> toSkip = null;
+ if (toSyncString != null) {
+ try {
+ toSync = new ArrayList<String>(new ExtendedJSONObject(toSyncString).keySet());
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Got exception parsing stages to sync: '" + toSyncString + "'.", e);
+ }
+ }
+ if (toSkipString != null) {
+ try {
+ toSkip = new ArrayList<String>(new ExtendedJSONObject(toSkipString).keySet());
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Got exception parsing stages to skip: '" + toSkipString + "'.", e);
+ }
+ }
+
+ Logger.info(LOG_TAG, "Asked to sync '" + Utils.toCommaSeparatedString(toSync) +
+ "' and to skip '" + Utils.toCommaSeparatedString(toSkip) + "'.");
+ return getStagesToSync(knownStageNames, toSync, toSkip);
+ }
+
+ /**
+ * Put names of stages to sync and to skip into sync extras bundle.
+ *
+ * @param bundle
+ * a <code>Bundle</code> instance (possibly null).
+ * @param stagesToSync
+ * collection of stage names to sync: key
+ * <code>EXTRAS_KEY_STAGES_TO_SYNC</code>; ignored if <code>null</code>.
+ * @param stagesToSkip
+ * collection of stage names to skip: key
+ * <code>EXTRAS_KEY_STAGES_TO_SKIP</code>; ignored if <code>null</code>.
+ */
+ public static void putStageNamesToSync(final Bundle bundle, final String[] stagesToSync, final String[] stagesToSkip) {
+ if (bundle == null) {
+ return;
+ }
+
+ if (stagesToSync != null) {
+ ExtendedJSONObject o = new ExtendedJSONObject();
+ for (String stageName : stagesToSync) {
+ o.put(stageName, 0);
+ }
+ bundle.putString(Constants.EXTRAS_KEY_STAGES_TO_SYNC, o.toJSONString());
+ }
+
+ if (stagesToSkip != null) {
+ ExtendedJSONObject o = new ExtendedJSONObject();
+ for (String stageName : stagesToSkip) {
+ o.put(stageName, 0);
+ }
+ bundle.putString(Constants.EXTRAS_KEY_STAGES_TO_SKIP, o.toJSONString());
+ }
+ }
+
+ /**
+ * Read contents of file as a string.
+ *
+ * @param context Android context.
+ * @param filename name of file to read; must not be null.
+ * @return <code>String</code> instance.
+ */
+ public static String readFile(final Context context, final String filename) {
+ if (filename == null) {
+ throw new IllegalArgumentException("Passed null filename in readFile.");
+ }
+
+ FileInputStream fis = null;
+ InputStreamReader isr = null;
+ BufferedReader br = null;
+
+ try {
+ fis = context.openFileInput(filename);
+ isr = new InputStreamReader(fis);
+ br = new BufferedReader(isr);
+ StringBuilder sb = new StringBuilder();
+ String line;
+ while ((line = br.readLine()) != null) {
+ sb.append(line);
+ }
+ return sb.toString();
+ } catch (Exception e) {
+ return null;
+ } finally {
+ if (isr != null) {
+ try {
+ isr.close();
+ } catch (IOException e) {
+ // Ignore.
+ }
+ }
+ if (fis != null) {
+ try {
+ fis.close();
+ } catch (IOException e) {
+ // Ignore.
+ }
+ }
+ }
+ }
+
+ /**
+ * Format a duration as a string, like "0.56 seconds".
+ *
+ * @param startMillis start time in milliseconds.
+ * @param endMillis end time in milliseconds.
+ * @return formatted string.
+ */
+ public static String formatDuration(long startMillis, long endMillis) {
+ final long duration = endMillis - startMillis;
+ return new DecimalFormat("#0.00 seconds").format(((double) duration) / 1000);
+ }
+
+ /**
+ * This will take a string containing a UTF-8 representation of a UTF-8
+ * byte array — e.g., "pïgéons1" — and return UTF-8 (e.g., "pïgéons1").
+ *
+ * This is the format produced by desktop Firefox when exchanging credentials
+ * containing non-ASCII characters.
+ */
+ public static String decodeUTF8(final String in) throws UnsupportedEncodingException {
+ final int length = in.length();
+ final byte[] asciiBytes = new byte[length];
+ for (int i = 0; i < length; ++i) {
+ asciiBytes[i] = (byte) in.codePointAt(i);
+ }
+ return new String(asciiBytes, "UTF-8");
+ }
+
+ /**
+ * Replace "foo@bar.com" with "XXX@XXX.XXX".
+ */
+ public static String obfuscateEmail(final String in) {
+ return in.replaceAll("[^@\\.]", "X");
+ }
+
+ public static void throwIfNull(Object... objects) {
+ for (Object object : objects) {
+ if (object == null) {
+ throw new IllegalArgumentException("object must not be null");
+ }
+ }
+ }
+
+ public static Executor newSynchronousExecutor() {
+ return new Executor() {
+ @Override
+ public void execute(Runnable runnable) {
+ runnable.run();
+ }
+ };
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/CryptoException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/CryptoException.java
new file mode 100644
index 0000000000..a8d0483c9b
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/CryptoException.java
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.crypto;
+
+import java.security.GeneralSecurityException;
+
+public class CryptoException extends Exception {
+ public GeneralSecurityException cause;
+ public CryptoException(GeneralSecurityException e) {
+ this();
+ this.cause = e;
+ }
+ public CryptoException() {
+
+ }
+ private static final long serialVersionUID = -5219310989960126830L;
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/CryptoInfo.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/CryptoInfo.java
new file mode 100644
index 0000000000..355571c6a1
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/CryptoInfo.java
@@ -0,0 +1,232 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.crypto;
+
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.Mac;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.mozilla.apache.commons.codec.binary.Base64;
+
+/*
+ * All info in these objects should be decoded (i.e. not BaseXX encoded).
+ */
+public class CryptoInfo {
+ private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding";
+ private static final String KEY_ALGORITHM_SPEC = "AES";
+
+ private byte[] message;
+ private byte[] iv;
+ private byte[] hmac;
+ private KeyBundle keys;
+
+ /**
+ * Return a CryptoInfo with given plaintext encrypted using given keys.
+ */
+ public static CryptoInfo encrypt(byte[] plaintextBytes, KeyBundle keys) throws CryptoException {
+ CryptoInfo info = new CryptoInfo(plaintextBytes, keys);
+ info.encrypt();
+ return info;
+ }
+
+ /**
+ * Return a CryptoInfo with given plaintext encrypted using given keys and initial vector.
+ */
+ public static CryptoInfo encrypt(byte[] plaintextBytes, byte[] iv, KeyBundle keys) throws CryptoException {
+ CryptoInfo info = new CryptoInfo(plaintextBytes, iv, null, keys);
+ info.encrypt();
+ return info;
+ }
+
+ /**
+ * Return a CryptoInfo with given ciphertext decrypted using given keys and initial vector, verifying that given HMAC validates.
+ */
+ public static CryptoInfo decrypt(byte[] ciphertext, byte[] iv, byte[] hmac, KeyBundle keys) throws CryptoException {
+ CryptoInfo info = new CryptoInfo(ciphertext, iv, hmac, keys);
+ info.decrypt();
+ return info;
+ }
+
+ /*
+ * Constructor typically used when encrypting.
+ */
+ public CryptoInfo(byte[] message, KeyBundle keys) {
+ this.setMessage(message);
+ this.setKeys(keys);
+ }
+
+ /*
+ * Constructor typically used when decrypting.
+ */
+ public CryptoInfo(byte[] message, byte[] iv, byte[] hmac, KeyBundle keys) {
+ this.setMessage(message);
+ this.setIV(iv);
+ this.setHMAC(hmac);
+ this.setKeys(keys);
+ }
+
+ public byte[] getMessage() {
+ return message;
+ }
+
+ public void setMessage(byte[] message) {
+ this.message = message;
+ }
+
+ public byte[] getIV() {
+ return iv;
+ }
+
+ public void setIV(byte[] iv) {
+ this.iv = iv;
+ }
+
+ public byte[] getHMAC() {
+ return hmac;
+ }
+
+ public void setHMAC(byte[] hmac) {
+ this.hmac = hmac;
+ }
+
+ public KeyBundle getKeys() {
+ return keys;
+ }
+
+ public void setKeys(KeyBundle keys) {
+ this.keys = keys;
+ }
+
+ /*
+ * Generate HMAC for given cipher text.
+ */
+ public static byte[] generatedHMACFor(byte[] message, KeyBundle keys) throws NoSuchAlgorithmException, InvalidKeyException {
+ Mac hmacHasher = HKDF.makeHMACHasher(keys.getHMACKey());
+ return hmacHasher.doFinal(Base64.encodeBase64(message));
+ }
+
+ /*
+ * Return true if generated HMAC is the same as the specified HMAC.
+ */
+ public boolean generatedHMACIsHMAC() throws NoSuchAlgorithmException, InvalidKeyException {
+ byte[] generatedHMAC = generatedHMACFor(getMessage(), getKeys());
+ byte[] expectedHMAC = getHMAC();
+ return Arrays.equals(generatedHMAC, expectedHMAC);
+ }
+
+ /**
+ * Performs functionality common to both encryption and decryption.
+ *
+ * @param cipher
+ * @param inputMessage non-BaseXX-encoded message
+ * @return encrypted/decrypted message
+ * @throws CryptoException
+ */
+ private static byte[] commonCrypto(Cipher cipher, byte[] inputMessage)
+ throws CryptoException {
+ byte[] outputMessage = null;
+ try {
+ outputMessage = cipher.doFinal(inputMessage);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ throw new CryptoException(e);
+ }
+ return outputMessage;
+ }
+
+ /**
+ * Encrypt a CryptoInfo in-place.
+ *
+ * @throws CryptoException
+ */
+ public void encrypt() throws CryptoException {
+
+ Cipher cipher = CryptoInfo.getCipher(TRANSFORMATION);
+ try {
+ byte[] encryptionKey = getKeys().getEncryptionKey();
+ SecretKeySpec spec = new SecretKeySpec(encryptionKey, KEY_ALGORITHM_SPEC);
+
+ // If no IV is provided, we allow the cipher to provide one.
+ if (getIV() == null || getIV().length == 0) {
+ cipher.init(Cipher.ENCRYPT_MODE, spec);
+ } else {
+ cipher.init(Cipher.ENCRYPT_MODE, spec, new IvParameterSpec(getIV()));
+ }
+ } catch (GeneralSecurityException ex) {
+ throw new CryptoException(ex);
+ }
+
+ // Encrypt.
+ byte[] encryptedBytes = commonCrypto(cipher, getMessage());
+ byte[] iv = cipher.getIV();
+
+ byte[] hmac;
+ // Generate HMAC.
+ try {
+ hmac = generatedHMACFor(encryptedBytes, keys);
+ } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+ throw new CryptoException(e);
+ }
+
+ // Update in place. keys is already set.
+ this.setHMAC(hmac);
+ this.setIV(iv);
+ this.setMessage(encryptedBytes);
+ }
+
+ /**
+ * Decrypt a CryptoInfo in-place.
+ *
+ * @throws CryptoException
+ */
+ public void decrypt() throws CryptoException {
+
+ // Check HMAC.
+ try {
+ if (!generatedHMACIsHMAC()) {
+ throw new HMACVerificationException();
+ }
+ } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+ throw new CryptoException(e);
+ }
+
+ Cipher cipher = CryptoInfo.getCipher(TRANSFORMATION);
+ try {
+ byte[] encryptionKey = getKeys().getEncryptionKey();
+ SecretKeySpec spec = new SecretKeySpec(encryptionKey, KEY_ALGORITHM_SPEC);
+ cipher.init(Cipher.DECRYPT_MODE, spec, new IvParameterSpec(getIV()));
+ } catch (GeneralSecurityException ex) {
+ throw new CryptoException(ex);
+ }
+ byte[] decryptedBytes = commonCrypto(cipher, getMessage());
+ byte[] iv = cipher.getIV();
+
+ // Update in place. keys is already set.
+ this.setHMAC(null);
+ this.setIV(iv);
+ this.setMessage(decryptedBytes);
+ }
+
+ /**
+ * Helper to get a Cipher object.
+ *
+ * @param transformation The type of Cipher to get.
+ */
+ private static Cipher getCipher(String transformation) throws CryptoException {
+ try {
+ return Cipher.getInstance(transformation);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ throw new CryptoException(e);
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/HKDF.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/HKDF.java
new file mode 100644
index 0000000000..16c0d8147d
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/HKDF.java
@@ -0,0 +1,128 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.crypto;
+
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.mozilla.gecko.sync.Utils;
+
+/*
+ * A standards-compliant implementation of RFC 5869
+ * for HMAC-based Key Derivation Function.
+ * HMAC uses HMAC SHA256 standard.
+ */
+public class HKDF {
+ public static String HMAC_ALGORITHM = "hmacSHA256";
+
+ /**
+ * Used for conversion in cases in which you *know* the encoding exists.
+ */
+ public static final byte[] bytes(String in) {
+ try {
+ return in.getBytes("UTF-8");
+ } catch (java.io.UnsupportedEncodingException e) {
+ return null;
+ }
+ }
+
+ public static final int BLOCKSIZE = 256 / 8;
+ public static final byte[] HMAC_INPUT = bytes("Sync-AES_256_CBC-HMAC256");
+
+ /*
+ * Step 1 of RFC 5869
+ * Get sha256HMAC Bytes
+ * Input: salt (message), IKM (input keyring material)
+ * Output: PRK (pseudorandom key)
+ */
+ public static byte[] hkdfExtract(byte[] salt, byte[] IKM) throws NoSuchAlgorithmException, InvalidKeyException {
+ return digestBytes(IKM, makeHMACHasher(salt));
+ }
+
+ /*
+ * Step 2 of RFC 5869.
+ * Input: PRK from step 1, info, length.
+ * Output: OKM (output keyring material).
+ */
+ public static byte[] hkdfExpand(byte[] prk, byte[] info, int len) throws NoSuchAlgorithmException, InvalidKeyException {
+ Mac hmacHasher = makeHMACHasher(prk);
+
+ byte[] T = {};
+ byte[] Tn = {};
+
+ int iterations = (int) Math.ceil(((double)len) / (BLOCKSIZE));
+ for (int i = 0; i < iterations; i++) {
+ Tn = digestBytes(Utils.concatAll(Tn, info, Utils.hex2Byte(Integer.toHexString(i + 1))),
+ hmacHasher);
+ T = Utils.concatAll(T, Tn);
+ }
+
+ byte[] result = new byte[len];
+ System.arraycopy(T, 0, result, 0, len);
+ return result;
+ }
+
+ /*
+ * Make HMAC key
+ * Input: key (salt)
+ * Output: Key HMAC-Key
+ */
+ public static Key makeHMACKey(byte[] key) {
+ if (key.length == 0) {
+ key = new byte[BLOCKSIZE];
+ }
+ return new SecretKeySpec(key, HMAC_ALGORITHM);
+ }
+
+ /*
+ * Make an HMAC hasher
+ * Input: Key hmacKey
+ * Ouput: An HMAC Hasher
+ */
+ public static Mac makeHMACHasher(byte[] key) throws NoSuchAlgorithmException, InvalidKeyException {
+ Mac hmacHasher = null;
+ hmacHasher = Mac.getInstance(HMAC_ALGORITHM);
+
+ // If Mac.getInstance doesn't throw NoSuchAlgorithmException, hmacHasher is
+ // non-null.
+ assert(hmacHasher != null);
+
+ hmacHasher.init(makeHMACKey(key));
+ return hmacHasher;
+ }
+
+ /*
+ * Hash bytes with given hasher
+ * Input: message to hash, HMAC hasher
+ * Output: hashed byte[].
+ */
+ public static byte[] digestBytes(byte[] message, Mac hasher) {
+ hasher.update(message);
+ byte[] ret = hasher.doFinal();
+ hasher.reset();
+ return ret;
+ }
+
+ public static byte[] derive(byte[] skm, byte[] xts, byte[] ctxInfo, int dkLen) throws InvalidKeyException, NoSuchAlgorithmException {
+ return hkdfExpand(hkdfExtract(xts, skm), ctxInfo, dkLen);
+ }
+
+ public static void deriveMany(byte[] skm, byte[] xts, byte[] ctxInfo, byte[]... keys) throws InvalidKeyException, NoSuchAlgorithmException {
+ int length = 0;
+ for (byte[] key : keys) {
+ length += key.length;
+ }
+ byte[] derived = hkdfExpand(hkdfExtract(xts, skm), ctxInfo, length);
+ int offset = 0;
+ for (byte[] key : keys) {
+ System.arraycopy(derived, offset, key, 0, key.length);
+ offset += key.length;
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/HMACVerificationException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/HMACVerificationException.java
new file mode 100644
index 0000000000..f33babd52e
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/HMACVerificationException.java
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.crypto;
+
+public class HMACVerificationException extends CryptoException {
+ private static final long serialVersionUID = 1235311303567074897L;
+ public HMACVerificationException() {
+ }
+
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/KeyBundle.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/KeyBundle.java
new file mode 100644
index 0000000000..2063b1e32a
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/KeyBundle.java
@@ -0,0 +1,135 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.crypto;
+
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import javax.crypto.KeyGenerator;
+import javax.crypto.Mac;
+
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.sync.Utils;
+
+public class KeyBundle {
+ private static final String KEY_ALGORITHM_SPEC = "AES";
+ private static final int KEY_SIZE = 256;
+
+ private byte[] encryptionKey;
+ private byte[] hmacKey;
+
+ // These are the same for every sync key bundle.
+ private static final byte[] EMPTY_BYTES = {};
+ private static final byte[] ENCR_INPUT_BYTES = {1};
+ private static final byte[] HMAC_INPUT_BYTES = {2};
+
+ /*
+ * Mozilla's use of HKDF for getting keys from the Sync Key string.
+ *
+ * We do exactly 2 HKDF iterations and make the first iteration the
+ * encryption key and the second iteration the HMAC key.
+ *
+ */
+ public KeyBundle(String username, String base32SyncKey) throws CryptoException {
+ if (base32SyncKey == null) {
+ throw new IllegalArgumentException("No sync key provided.");
+ }
+ if (username == null || username.equals("")) {
+ throw new IllegalArgumentException("No username provided.");
+ }
+ // Hash appropriately.
+ try {
+ username = Utils.usernameFromAccount(username);
+ } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
+ throw new IllegalArgumentException("Invalid username.");
+ }
+
+ byte[] syncKey = Utils.decodeFriendlyBase32(base32SyncKey);
+ byte[] user = username.getBytes();
+
+ Mac hmacHasher;
+ try {
+ hmacHasher = HKDF.makeHMACHasher(syncKey);
+ } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+ throw new CryptoException(e);
+ }
+ assert(hmacHasher != null); // If makeHMACHasher doesn't throw, then hmacHasher is non-null.
+
+ byte[] encrBytes = Utils.concatAll(EMPTY_BYTES, HKDF.HMAC_INPUT, user, ENCR_INPUT_BYTES);
+ byte[] encrKey = HKDF.digestBytes(encrBytes, hmacHasher);
+ byte[] hmacBytes = Utils.concatAll(encrKey, HKDF.HMAC_INPUT, user, HMAC_INPUT_BYTES);
+
+ this.hmacKey = HKDF.digestBytes(hmacBytes, hmacHasher);
+ this.encryptionKey = encrKey;
+ }
+
+ public KeyBundle(byte[] encryptionKey, byte[] hmacKey) {
+ this.setEncryptionKey(encryptionKey);
+ this.setHMACKey(hmacKey);
+ }
+
+ /**
+ * Make a KeyBundle with the specified base64-encoded keys.
+ *
+ * @return A KeyBundle with the specified keys.
+ */
+ public static KeyBundle fromBase64EncodedKeys(String base64EncryptionKey, String base64HmacKey) throws UnsupportedEncodingException {
+ return new KeyBundle(Base64.decodeBase64(base64EncryptionKey.getBytes("UTF-8")),
+ Base64.decodeBase64(base64HmacKey.getBytes("UTF-8")));
+ }
+
+ /**
+ * Make a KeyBundle with two random 256 bit keys (encryption and HMAC).
+ *
+ * @return A KeyBundle with random keys.
+ */
+ public static KeyBundle withRandomKeys() throws CryptoException {
+ KeyGenerator keygen;
+ try {
+ keygen = KeyGenerator.getInstance(KEY_ALGORITHM_SPEC);
+ } catch (NoSuchAlgorithmException e) {
+ throw new CryptoException(e);
+ }
+
+ keygen.init(KEY_SIZE);
+ byte[] encryptionKey = keygen.generateKey().getEncoded();
+ byte[] hmacKey = keygen.generateKey().getEncoded();
+
+ return new KeyBundle(encryptionKey, hmacKey);
+ }
+
+ public byte[] getEncryptionKey() {
+ return encryptionKey;
+ }
+
+ public void setEncryptionKey(byte[] encryptionKey) {
+ this.encryptionKey = encryptionKey;
+ }
+
+ public byte[] getHMACKey() {
+ return hmacKey;
+ }
+
+ public void setHMACKey(byte[] hmacKey) {
+ this.hmacKey = hmacKey;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof KeyBundle)) {
+ return false;
+ }
+ KeyBundle other = (KeyBundle) o;
+ return Arrays.equals(other.encryptionKey, this.encryptionKey) &&
+ Arrays.equals(other.hmacKey, this.hmacKey);
+ }
+
+ @Override
+ public int hashCode() {
+ throw new UnsupportedOperationException("No hashCode for KeyBundle.");
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/MissingCryptoInputException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/MissingCryptoInputException.java
new file mode 100644
index 0000000000..8add1cf11a
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/MissingCryptoInputException.java
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.crypto;
+
+public class MissingCryptoInputException extends CryptoException {
+ private static final long serialVersionUID = 5334412407012972445L;
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/NoKeyBundleException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/NoKeyBundleException.java
new file mode 100644
index 0000000000..00e0f8b185
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/NoKeyBundleException.java
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.crypto;
+
+public class NoKeyBundleException extends CryptoException {
+ private static final long serialVersionUID = -6627154503154040915L;
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/PBKDF2.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/PBKDF2.java
new file mode 100644
index 0000000000..636b2105c3
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/PBKDF2.java
@@ -0,0 +1,78 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.crypto;
+
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+
+import javax.crypto.Mac;
+import javax.crypto.ShortBufferException;
+import javax.crypto.spec.SecretKeySpec;
+
+public class PBKDF2 {
+ public static byte[] pbkdf2SHA256(byte[] password, byte[] salt, int c, int dkLen)
+ throws GeneralSecurityException {
+ final String algorithm = "HmacSHA256";
+ SecretKeySpec keyspec = new SecretKeySpec(password, algorithm);
+ Mac prf = Mac.getInstance(algorithm);
+ prf.init(keyspec);
+
+ int hLen = prf.getMacLength();
+
+ byte U_r[] = new byte[hLen];
+ byte U_i[] = new byte[salt.length + 4];
+ byte scratch[] = new byte[hLen];
+
+ int l = Math.max(dkLen, hLen);
+ int r = dkLen - (l - 1) * hLen;
+ byte T[] = new byte[l * hLen];
+ int ti_offset = 0;
+ for (int i = 1; i <= l; i++) {
+ Arrays.fill(U_r, (byte) 0);
+ F(T, ti_offset, prf, salt, c, i, U_r, U_i, scratch);
+ ti_offset += hLen;
+ }
+
+ if (r < hLen) {
+ // Incomplete last block.
+ byte DK[] = new byte[dkLen];
+ System.arraycopy(T, 0, DK, 0, dkLen);
+ return DK;
+ }
+
+ return T;
+ }
+
+ private static void F(byte[] dest, int offset, Mac prf, byte[] S, int c, int blockIndex, byte U_r[], byte U_i[], byte[] scratch)
+ throws ShortBufferException, IllegalStateException {
+ final int hLen = prf.getMacLength();
+
+ // U0 = S || INT (i);
+ System.arraycopy(S, 0, U_i, 0, S.length);
+ INT(U_i, S.length, blockIndex);
+
+ for (int i = 0; i < c; i++) {
+ prf.update(U_i);
+ prf.doFinal(scratch, 0);
+ U_i = scratch;
+ xor(U_r, U_i);
+ }
+
+ System.arraycopy(U_r, 0, dest, offset, hLen);
+ }
+
+ private static void xor(byte[] dest, byte[] src) {
+ for (int i = 0; i < dest.length; i++) {
+ dest[i] ^= src[i];
+ }
+ }
+
+ private static void INT(byte[] dest, int offset, int i) {
+ dest[offset + 0] = (byte) (i / (256 * 256 * 256));
+ dest[offset + 1] = (byte) (i / (256 * 256));
+ dest[offset + 2] = (byte) (i / (256));
+ dest[offset + 3] = (byte) (i);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/PersistedCrypto5Keys.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/PersistedCrypto5Keys.java
new file mode 100644
index 0000000000..4dba4f258c
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/PersistedCrypto5Keys.java
@@ -0,0 +1,103 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.crypto;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.CollectionKeys;
+import org.mozilla.gecko.sync.CryptoRecord;
+
+import android.content.SharedPreferences;
+
+public class PersistedCrypto5Keys {
+ public static final String LOG_TAG = "PersistedC5Keys";
+
+ public static final String CRYPTO5_KEYS_SERVER_RESPONSE_BODY = "crypto5KeysServerResponseBody";
+ public static final String CRYPTO5_KEYS_LAST_MODIFIED = "crypto5KeysLastModified";
+
+ protected SharedPreferences prefs;
+ protected KeyBundle syncKeyBundle;
+
+ public PersistedCrypto5Keys(SharedPreferences prefs, KeyBundle syncKeyBundle) {
+ if (syncKeyBundle == null) {
+ throw new IllegalArgumentException("Null syncKeyBundle passed in to PersistedCrypto5Keys constructor.");
+ }
+ this.prefs = prefs;
+ this.syncKeyBundle = syncKeyBundle;
+ }
+
+ /**
+ * Get persisted crypto/keys.
+ * <p>
+ * crypto/keys is fetched from an encrypted JSON-encoded <code>CryptoRecord</code>.
+ *
+ * @return A <code>CollectionKeys</code> instance or <code>null</code> if none
+ * is currently persisted.
+ */
+ public CollectionKeys keys() {
+ String keysJSON = prefs.getString(CRYPTO5_KEYS_SERVER_RESPONSE_BODY, null);
+ if (keysJSON == null) {
+ return null;
+ }
+ try {
+ CryptoRecord cryptoRecord = CryptoRecord.fromJSONRecord(keysJSON);
+ CollectionKeys keys = new CollectionKeys();
+ keys.setKeyPairsFromWBO(cryptoRecord, syncKeyBundle);
+ return keys;
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Got exception decrypting persisted crypto/keys.", e);
+ return null;
+ }
+ }
+
+ /**
+ * Persist crypto/keys.
+ * <p>
+ * crypto/keys is stored as an encrypted JSON-encoded <code>CryptoRecord</code>.
+ *
+ * @param keys
+ * The <code>CollectionKeys</code> object to persist, which should
+ * have the same default key bundle as the sync key bundle.
+ */
+ public void persistKeys(CollectionKeys keys) {
+ if (keys == null) {
+ Logger.debug(LOG_TAG, "Clearing persisted crypto/keys.");
+ prefs.edit().remove(CRYPTO5_KEYS_SERVER_RESPONSE_BODY).commit();
+ return;
+ }
+ try {
+ CryptoRecord cryptoRecord = keys.asCryptoRecord();
+ cryptoRecord.keyBundle = syncKeyBundle;
+ cryptoRecord.encrypt();
+ String keysJSON = cryptoRecord.toJSONString();
+ Logger.debug(LOG_TAG, "Persisting crypto/keys.");
+ prefs.edit().putString(CRYPTO5_KEYS_SERVER_RESPONSE_BODY, keysJSON).commit();
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Got exception encrypting while persisting crypto/keys.", e);
+ }
+ }
+
+ public boolean persistedKeysExist() {
+ return lastModified() > 0;
+ }
+
+ public long lastModified() {
+ return prefs.getLong(CRYPTO5_KEYS_LAST_MODIFIED, -1);
+ }
+
+ public void persistLastModified(long lastModified) {
+ if (lastModified <= 0) {
+ Logger.debug(LOG_TAG, "Clearing persisted crypto/keys last modified timestamp.");
+ prefs.edit().remove(CRYPTO5_KEYS_LAST_MODIFIED).commit();
+ return;
+ }
+ Logger.debug(LOG_TAG, "Persisting crypto/keys last modified timestamp " + lastModified + ".");
+ prefs.edit().putLong(CRYPTO5_KEYS_LAST_MODIFIED, lastModified).commit();
+ }
+
+ public void purge() {
+ persistLastModified(-1);
+ persistKeys(null);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/ClientsDataDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/ClientsDataDelegate.java
new file mode 100644
index 0000000000..07e9179f0f
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/ClientsDataDelegate.java
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.delegates;
+
+public interface ClientsDataDelegate {
+ public String getAccountGUID();
+ public String getDefaultClientName();
+ public void setClientName(String clientName, long now);
+ public String getClientName();
+ public void setClientsCount(int clientsCount);
+ public int getClientsCount();
+ public boolean isLocalGUID(String guid);
+ public String getFormFactor();
+
+ /**
+ * The last time the client's data was modified in a way that should be
+ * reflected remotely.
+ * <p>
+ * Changing the client's name should be reflected remotely, while changing the
+ * clients count should not (since that data is only used to inform local
+ * policy.)
+ *
+ * @return timestamp in milliseconds.
+ */
+ public long getLastModifiedTimestamp();
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/FreshStartDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/FreshStartDelegate.java
new file mode 100644
index 0000000000..2e53470611
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/FreshStartDelegate.java
@@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.delegates;
+
+public interface FreshStartDelegate {
+ void onFreshStart();
+ void onFreshStartFailed(Exception e);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/GlobalSessionCallback.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/GlobalSessionCallback.java
new file mode 100644
index 0000000000..9829f5b346
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/GlobalSessionCallback.java
@@ -0,0 +1,49 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.delegates;
+
+import java.net.URI;
+
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
+
+public interface GlobalSessionCallback {
+ /**
+ * Request that no further syncs occur within the next `backoff` milliseconds.
+ * @param backoff a duration in milliseconds.
+ */
+ void requestBackoff(long backoff);
+
+ /**
+ * Called on a 401 HTTP response.
+ */
+ void informUnauthorizedResponse(GlobalSession globalSession, URI oldClusterURL);
+
+
+ /**
+ * Called when an HTTP failure indicates that a software upgrade is required.
+ */
+ void informUpgradeRequiredResponse(GlobalSession session);
+
+ /**
+ * Called when a migration sentinel has been found and processed successfully.
+ * <p>
+ * This account should stop syncing immediately, and arrange to delete itself.
+ */
+ void informMigrated(GlobalSession session);
+
+ void handleAborted(GlobalSession globalSession, String reason);
+ void handleError(GlobalSession globalSession, Exception ex);
+ void handleSuccess(GlobalSession globalSession);
+ void handleStageCompleted(Stage currentState, GlobalSession globalSession);
+
+ /**
+ * Called when a {@link GlobalSession} wants to know if it should continue
+ * to make storage requests.
+ *
+ * @return false if the session should make no further requests.
+ */
+ boolean shouldBackOffStorage();
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/JSONRecordFetchDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/JSONRecordFetchDelegate.java
new file mode 100644
index 0000000000..90b73a33ae
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/JSONRecordFetchDelegate.java
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.delegates;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+/**
+ * A fairly generic delegate to handle fetches of single JSON object blobs, as
+ * provided by <code>info/configuration</code>, <code>info/collections</code>
+ * and <code>info/collection_counts</code>.
+ */
+public interface JSONRecordFetchDelegate {
+ public void handleSuccess(ExtendedJSONObject body);
+ public void handleFailure(SyncStorageResponse response);
+ public void handleError(Exception e);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/KeyUploadDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/KeyUploadDelegate.java
new file mode 100644
index 0000000000..0cd5ec732c
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/KeyUploadDelegate.java
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.delegates;
+
+public interface KeyUploadDelegate {
+ /**
+ * Called when keys have been successfully uploaded to the server.
+ * <p>
+ * The uploaded keys are intentionally not exposed. It is possible for two
+ * clients to simultaneously upload keys and for each client to conclude that
+ * its keys are current (since the server returned 200 on upload). To shorten
+ * the window wherein two such clients can race, all clients should upload and
+ * then immediately re-download the fetched keys.
+ * <p>
+ * See Bug 692700, Bug 693893.
+ */
+ void onKeysUploaded();
+ void onKeyUploadFailed(Exception e);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/MetaGlobalDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/MetaGlobalDelegate.java
new file mode 100644
index 0000000000..13854cb5a3
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/MetaGlobalDelegate.java
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.delegates;
+
+import org.mozilla.gecko.sync.MetaGlobal;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+public interface MetaGlobalDelegate {
+ public void handleSuccess(MetaGlobal global, SyncStorageResponse response);
+ public void handleMissing(MetaGlobal global, SyncStorageResponse response);
+ public void handleFailure(SyncStorageResponse response);
+ public void handleError(Exception e);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/WipeServerDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/WipeServerDelegate.java
new file mode 100644
index 0000000000..ef35658127
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/WipeServerDelegate.java
@@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.delegates;
+
+public interface WipeServerDelegate {
+ public void onWiped(long timestamp);
+ public void onWipeFailed(Exception e);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/Crypto5MiddlewareRepository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/Crypto5MiddlewareRepository.java
new file mode 100644
index 0000000000..79319aff50
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/Crypto5MiddlewareRepository.java
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.middleware;
+
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.repositories.IdentityRecordFactory;
+import org.mozilla.gecko.sync.repositories.RecordFactory;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCleanDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+
+import android.content.Context;
+
+/**
+ * Wrap an existing repository in middleware that encrypts and decrypts records
+ * passing through.
+ *
+ * @author rnewman
+ *
+ */
+public class Crypto5MiddlewareRepository extends MiddlewareRepository {
+
+ public RecordFactory recordFactory = new IdentityRecordFactory();
+
+ public class Crypto5MiddlewareRepositorySessionCreationDelegate extends MiddlewareRepository.SessionCreationDelegate {
+ private final Crypto5MiddlewareRepository repository;
+ private final RepositorySessionCreationDelegate outerDelegate;
+
+ public Crypto5MiddlewareRepositorySessionCreationDelegate(Crypto5MiddlewareRepository repository, RepositorySessionCreationDelegate outerDelegate) {
+ this.repository = repository;
+ this.outerDelegate = outerDelegate;
+ }
+
+ @Override
+ public void onSessionCreateFailed(Exception ex) {
+ this.outerDelegate.onSessionCreateFailed(ex);
+ }
+
+ @Override
+ public void onSessionCreated(RepositorySession session) {
+ // Do some work, then report success with the wrapping session.
+ Crypto5MiddlewareRepositorySession cryptoSession;
+ try {
+ // Synchronous, baby.
+ cryptoSession = new Crypto5MiddlewareRepositorySession(session, this.repository, recordFactory);
+ } catch (Exception ex) {
+ this.outerDelegate.onSessionCreateFailed(ex);
+ return;
+ }
+ this.outerDelegate.onSessionCreated(cryptoSession);
+ }
+ }
+
+ public KeyBundle keyBundle;
+ private final Repository inner;
+
+ public Crypto5MiddlewareRepository(Repository inner, KeyBundle keys) {
+ super();
+ this.inner = inner;
+ this.keyBundle = keys;
+ }
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate, Context context) {
+ Crypto5MiddlewareRepositorySessionCreationDelegate delegateWrapper = new Crypto5MiddlewareRepositorySessionCreationDelegate(this, delegate);
+ inner.createSession(delegateWrapper, context);
+ }
+
+ @Override
+ public void clean(boolean success, RepositorySessionCleanDelegate delegate,
+ Context context) {
+ this.inner.clean(success, delegate, context);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/Crypto5MiddlewareRepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/Crypto5MiddlewareRepositorySession.java
new file mode 100644
index 0000000000..46de7a2367
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/Crypto5MiddlewareRepositorySession.java
@@ -0,0 +1,172 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.middleware;
+
+import java.io.UnsupportedEncodingException;
+import java.util.concurrent.ExecutorService;
+
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.repositories.InactiveSessionException;
+import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
+import org.mozilla.gecko.sync.repositories.RecordFactory;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+/**
+ * It's a RepositorySession that accepts Records as input, producing CryptoRecords
+ * for submission to a remote service.
+ * Takes a RecordFactory as a parameter. This is in charge of taking decrypted CryptoRecords
+ * as input and producing some expected kind of Record as output for local use.
+ *
+ *
+
+
+
+ +------------------------------------+
+ | Server11RepositorySession |
+ +-------------------------+----------+
+ ^ |
+ | |
+ Encrypted CryptoRecords
+ | |
+ | v
+ +---------+--------------------------+
+ | Crypto5MiddlewareRepositorySession |
+ +------------------------------------+
+ ^ |
+ | | Decrypted CryptoRecords
+ | |
+ | +---------------+
+ | | RecordFactory |
+ | +--+------------+
+ | |
+ Local Record instances
+ | |
+ | v
+ +---------+--------------------------+
+ | Local RepositorySession instance |
+ +------------------------------------+
+
+
+ * @author rnewman
+ *
+ */
+public class Crypto5MiddlewareRepositorySession extends MiddlewareRepositorySession {
+ private final KeyBundle keyBundle;
+ private final RecordFactory recordFactory;
+
+ public Crypto5MiddlewareRepositorySession(RepositorySession session, Crypto5MiddlewareRepository repository, RecordFactory recordFactory) {
+ super(session, repository);
+ this.keyBundle = repository.keyBundle;
+ this.recordFactory = recordFactory;
+ }
+
+ public class DecryptingTransformingFetchDelegate implements RepositorySessionFetchRecordsDelegate {
+ private final RepositorySessionFetchRecordsDelegate next;
+ private final KeyBundle keyBundle;
+ private final RecordFactory recordFactory;
+
+ DecryptingTransformingFetchDelegate(RepositorySessionFetchRecordsDelegate next, KeyBundle bundle, RecordFactory recordFactory) {
+ this.next = next;
+ this.keyBundle = bundle;
+ this.recordFactory = recordFactory;
+ }
+
+ @Override
+ public void onFetchFailed(Exception ex, Record record) {
+ next.onFetchFailed(ex, record);
+ }
+
+ @Override
+ public void onFetchedRecord(Record record) {
+ CryptoRecord r;
+ try {
+ r = (CryptoRecord) record;
+ } catch (ClassCastException e) {
+ next.onFetchFailed(e, record);
+ return;
+ }
+ r.keyBundle = keyBundle;
+ try {
+ r.decrypt();
+ } catch (Exception e) {
+ next.onFetchFailed(e, r);
+ return;
+ }
+ Record transformed;
+ try {
+ transformed = this.recordFactory.createRecord(r);
+ } catch (Exception e) {
+ next.onFetchFailed(e, r);
+ return;
+ }
+ next.onFetchedRecord(transformed);
+ }
+
+ @Override
+ public void onFetchCompleted(final long fetchEnd) {
+ next.onFetchCompleted(fetchEnd);
+ }
+
+ @Override
+ public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService executor) {
+ // Synchronously perform *our* work, passing through appropriately.
+ RepositorySessionFetchRecordsDelegate deferredNext = next.deferredFetchDelegate(executor);
+ return new DecryptingTransformingFetchDelegate(deferredNext, keyBundle, recordFactory);
+ }
+ }
+
+ private DecryptingTransformingFetchDelegate makeUnwrappingDelegate(RepositorySessionFetchRecordsDelegate inner) {
+ if (inner == null) {
+ throw new IllegalArgumentException("Inner delegate cannot be null!");
+ }
+ return new DecryptingTransformingFetchDelegate(inner, this.keyBundle, this.recordFactory);
+ }
+
+ @Override
+ public void fetchSince(long timestamp,
+ RepositorySessionFetchRecordsDelegate delegate) {
+ inner.fetchSince(timestamp, makeUnwrappingDelegate(delegate));
+ }
+
+ @Override
+ public void fetch(String[] guids,
+ RepositorySessionFetchRecordsDelegate delegate) throws InactiveSessionException {
+ inner.fetch(guids, makeUnwrappingDelegate(delegate));
+ }
+
+ @Override
+ public void fetchAll(RepositorySessionFetchRecordsDelegate delegate) {
+ inner.fetchAll(makeUnwrappingDelegate(delegate));
+ }
+
+ @Override
+ public void setStoreDelegate(RepositorySessionStoreDelegate delegate) {
+ // TODO: it remains to be seen how this will work.
+ inner.setStoreDelegate(delegate);
+ this.delegate = delegate; // So we can handle errors without involving inner.
+ }
+
+ @Override
+ public void store(Record record) throws NoStoreDelegateException {
+ if (delegate == null) {
+ throw new NoStoreDelegateException();
+ }
+ CryptoRecord rec = record.getEnvelope();
+ rec.keyBundle = this.keyBundle;
+ try {
+ rec.encrypt();
+ } catch (UnsupportedEncodingException | CryptoException e) {
+ delegate.onRecordStoreFailed(e, record.guid);
+ return;
+ }
+ // Allow the inner session to do delegate handling.
+ inner.store(rec);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/MiddlewareRepository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/MiddlewareRepository.java
new file mode 100644
index 0000000000..d807aa5c09
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/MiddlewareRepository.java
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.middleware;
+
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+
+public abstract class MiddlewareRepository extends Repository {
+
+ public abstract class SessionCreationDelegate implements
+ RepositorySessionCreationDelegate {
+
+ // We call through to our inner repository, so we don't need our own
+ // deferral scheme.
+ @Override
+ public RepositorySessionCreationDelegate deferredCreationDelegate() {
+ return this;
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/MiddlewareRepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/MiddlewareRepositorySession.java
new file mode 100644
index 0000000000..e14ef52265
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/MiddlewareRepositorySession.java
@@ -0,0 +1,185 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.middleware;
+
+import java.util.concurrent.ExecutorService;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.repositories.InactiveSessionException;
+import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
+
+public abstract class MiddlewareRepositorySession extends RepositorySession {
+ private static final String LOG_TAG = "MiddlewareSession";
+ protected final RepositorySession inner;
+
+ public MiddlewareRepositorySession(RepositorySession innerSession, MiddlewareRepository repository) {
+ super(repository);
+ this.inner = innerSession;
+ }
+
+ @Override
+ public void wipe(RepositorySessionWipeDelegate delegate) {
+ inner.wipe(delegate);
+ }
+
+ public class MiddlewareRepositorySessionBeginDelegate implements RepositorySessionBeginDelegate {
+
+ private final MiddlewareRepositorySession outerSession;
+ private final RepositorySessionBeginDelegate next;
+
+ public MiddlewareRepositorySessionBeginDelegate(MiddlewareRepositorySession outerSession, RepositorySessionBeginDelegate next) {
+ this.outerSession = outerSession;
+ this.next = next;
+ }
+
+ @Override
+ public void onBeginFailed(Exception ex) {
+ next.onBeginFailed(ex);
+ }
+
+ @Override
+ public void onBeginSucceeded(RepositorySession session) {
+ next.onBeginSucceeded(outerSession);
+ }
+
+ @Override
+ public RepositorySessionBeginDelegate deferredBeginDelegate(ExecutorService executor) {
+ final RepositorySessionBeginDelegate deferred = next.deferredBeginDelegate(executor);
+ return new RepositorySessionBeginDelegate() {
+ @Override
+ public void onBeginSucceeded(RepositorySession session) {
+ if (inner != session) {
+ Logger.warn(LOG_TAG, "Got onBeginSucceeded for session " + session + ", not our inner session!");
+ }
+ deferred.onBeginSucceeded(outerSession);
+ }
+
+ @Override
+ public void onBeginFailed(Exception ex) {
+ deferred.onBeginFailed(ex);
+ }
+
+ @Override
+ public RepositorySessionBeginDelegate deferredBeginDelegate(ExecutorService executor) {
+ return this;
+ }
+ };
+ }
+ }
+
+ @Override
+ public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException {
+ inner.begin(new MiddlewareRepositorySessionBeginDelegate(this, delegate));
+ }
+
+ public class MiddlewareRepositorySessionFinishDelegate implements RepositorySessionFinishDelegate {
+ private final MiddlewareRepositorySession outerSession;
+ private final RepositorySessionFinishDelegate next;
+
+ public MiddlewareRepositorySessionFinishDelegate(MiddlewareRepositorySession outerSession, RepositorySessionFinishDelegate next) {
+ this.outerSession = outerSession;
+ this.next = next;
+ }
+
+ @Override
+ public void onFinishFailed(Exception ex) {
+ next.onFinishFailed(ex);
+ }
+
+ @Override
+ public void onFinishSucceeded(RepositorySession session, RepositorySessionBundle bundle) {
+ next.onFinishSucceeded(outerSession, bundle);
+ }
+
+ @Override
+ public RepositorySessionFinishDelegate deferredFinishDelegate(ExecutorService executor) {
+ return this;
+ }
+ }
+
+ @Override
+ public void finish(RepositorySessionFinishDelegate delegate) throws InactiveSessionException {
+ inner.finish(new MiddlewareRepositorySessionFinishDelegate(this, delegate));
+ }
+
+
+ @Override
+ public synchronized void ensureActive() throws InactiveSessionException {
+ inner.ensureActive();
+ }
+
+ @Override
+ public synchronized boolean isActive() {
+ return inner.isActive();
+ }
+
+ @Override
+ public synchronized SessionStatus getStatus() {
+ return inner.getStatus();
+ }
+
+ @Override
+ public synchronized void setStatus(SessionStatus status) {
+ inner.setStatus(status);
+ }
+
+ @Override
+ public synchronized void transitionFrom(SessionStatus from, SessionStatus to)
+ throws InvalidSessionTransitionException {
+ inner.transitionFrom(from, to);
+ }
+
+ @Override
+ public void abort() {
+ inner.abort();
+ }
+
+ @Override
+ public void abort(RepositorySessionFinishDelegate delegate) {
+ inner.abort(new MiddlewareRepositorySessionFinishDelegate(this, delegate));
+ }
+
+ @Override
+ public void guidsSince(long timestamp, RepositorySessionGuidsSinceDelegate delegate) {
+ // TODO: need to do anything here?
+ inner.guidsSince(timestamp, delegate);
+ }
+
+ @Override
+ public void storeDone() {
+ inner.storeDone();
+ }
+
+ @Override
+ public void storeDone(long storeEnd) {
+ inner.storeDone(storeEnd);
+ }
+
+ @Override
+ public boolean shouldSkip() {
+ return inner.shouldSkip();
+ }
+
+ @Override
+ public boolean dataAvailable() {
+ return inner.dataAvailable();
+ }
+
+ @Override
+ public void unbundle(RepositorySessionBundle bundle) {
+ inner.unbundle(bundle);
+ }
+
+ @Override
+ public long getLastSyncTimestamp() {
+ return inner.getLastSyncTimestamp();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/AbstractBearerTokenAuthHeaderProvider.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/AbstractBearerTokenAuthHeaderProvider.java
new file mode 100644
index 0000000000..e3b4f25b1e
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/AbstractBearerTokenAuthHeaderProvider.java
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
+import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
+import ch.boye.httpclientandroidlib.message.BasicHeader;
+import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
+
+/**
+ * An <code>AuthHeaderProvider</code> that returns an Authorization header for
+ * bearer tokens, adding a simple prefix.
+ */
+public abstract class AbstractBearerTokenAuthHeaderProvider implements AuthHeaderProvider {
+ protected final String header;
+
+ public AbstractBearerTokenAuthHeaderProvider(String token) {
+ if (token == null) {
+ throw new IllegalArgumentException("token must not be null.");
+ }
+
+ this.header = getPrefix() + " " + token;
+ }
+
+ protected abstract String getPrefix();
+
+ @Override
+ public Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client) {
+ return new BasicHeader("Authorization", header);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/AuthHeaderProvider.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/AuthHeaderProvider.java
new file mode 100644
index 0000000000..7be6fef3d2
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/AuthHeaderProvider.java
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import java.security.GeneralSecurityException;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
+import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
+import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
+
+/**
+ * An <code>AuthHeaderProvider</code> generates HTTP Authorization headers for
+ * HTTP requests.
+ */
+public interface AuthHeaderProvider {
+ /**
+ * Generate an HTTP Authorization header.
+ *
+ * @param request HTTP request.
+ * @param context HTTP context.
+ * @param client HTTP client.
+ * @return HTTP Authorization header.
+ * @throws GeneralSecurityException usually wrapping a more specific exception.
+ */
+ Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client)
+ throws GeneralSecurityException;
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BaseResource.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BaseResource.java
new file mode 100644
index 0000000000..60bbc86bbc
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BaseResource.java
@@ -0,0 +1,565 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.lang.ref.WeakReference;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import javax.net.ssl.SSLContext;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.mozilla.gecko.background.common.GlobalConstants;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.client.AuthCache;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+import ch.boye.httpclientandroidlib.client.entity.GzipCompressingEntity;
+import ch.boye.httpclientandroidlib.client.methods.HttpDelete;
+import ch.boye.httpclientandroidlib.client.methods.HttpGet;
+import ch.boye.httpclientandroidlib.client.methods.HttpPatch;
+import ch.boye.httpclientandroidlib.client.methods.HttpPost;
+import ch.boye.httpclientandroidlib.client.methods.HttpPut;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.client.protocol.ClientContext;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionManager;
+import ch.boye.httpclientandroidlib.conn.scheme.PlainSocketFactory;
+import ch.boye.httpclientandroidlib.conn.scheme.Scheme;
+import ch.boye.httpclientandroidlib.conn.scheme.SchemeRegistry;
+import ch.boye.httpclientandroidlib.conn.ssl.SSLSocketFactory;
+import ch.boye.httpclientandroidlib.entity.StringEntity;
+import ch.boye.httpclientandroidlib.impl.client.BasicAuthCache;
+import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
+import ch.boye.httpclientandroidlib.impl.conn.tsccm.ThreadSafeClientConnManager;
+import ch.boye.httpclientandroidlib.params.HttpConnectionParams;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.params.HttpProtocolParams;
+import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.EntityUtils;
+
+/**
+ * Provide simple HTTP access to a Sync server or similar.
+ * Implements Basic Auth by asking its delegate for credentials.
+ * Communicates with a ResourceDelegate to asynchronously return responses and errors.
+ * Exposes simple get/post/put/delete methods.
+ */
+@SuppressWarnings("deprecation")
+public class BaseResource implements Resource {
+ private static final String ANDROID_LOOPBACK_IP = "10.0.2.2";
+
+ private static final int MAX_TOTAL_CONNECTIONS = 20;
+ private static final int MAX_CONNECTIONS_PER_ROUTE = 10;
+
+ private boolean retryOnFailedRequest = true;
+
+ public static boolean rewriteLocalhost = true;
+
+ private static final String LOG_TAG = "BaseResource";
+
+ protected final URI uri;
+ protected BasicHttpContext context;
+ protected DefaultHttpClient client;
+ public ResourceDelegate delegate;
+ protected HttpRequestBase request;
+ public final String charset = "utf-8";
+
+ private boolean shouldGzipCompress = false;
+ // A hint whether uploaded payloads are chunked. Default true to use GzipCompressingEntity, which is built-in functionality.
+ private boolean shouldChunkUploadsHint = true;
+
+ /**
+ * We have very few writes (observers tend to be installed around sync
+ * sessions) and many iterations (every HTTP request iterates observers), so
+ * CopyOnWriteArrayList is a reasonable choice.
+ */
+ protected static final CopyOnWriteArrayList<WeakReference<HttpResponseObserver>>
+ httpResponseObservers = new CopyOnWriteArrayList<>();
+
+ public BaseResource(String uri) throws URISyntaxException {
+ this(uri, rewriteLocalhost);
+ }
+
+ public BaseResource(URI uri) {
+ this(uri, rewriteLocalhost);
+ }
+
+ public BaseResource(String uri, boolean rewrite) throws URISyntaxException {
+ this(new URI(uri), rewrite);
+ }
+
+ public BaseResource(URI uri, boolean rewrite) {
+ if (uri == null) {
+ throw new IllegalArgumentException("uri must not be null");
+ }
+ if (rewrite && "localhost".equals(uri.getHost())) {
+ // Rewrite localhost URIs to refer to the special Android emulator loopback passthrough interface.
+ Logger.debug(LOG_TAG, "Rewriting " + uri + " to point to " + ANDROID_LOOPBACK_IP + ".");
+ try {
+ this.uri = new URI(uri.getScheme(), uri.getUserInfo(), ANDROID_LOOPBACK_IP, uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment());
+ } catch (URISyntaxException e) {
+ Logger.error(LOG_TAG, "Got error rewriting URI for Android emulator.", e);
+ throw new IllegalArgumentException("Invalid URI", e);
+ }
+ } else {
+ this.uri = uri;
+ }
+ }
+
+ public static void addHttpResponseObserver(HttpResponseObserver newHttpResponseObserver) {
+ if (newHttpResponseObserver == null) {
+ return;
+ }
+ httpResponseObservers.add(new WeakReference<HttpResponseObserver>(newHttpResponseObserver));
+ }
+
+ public static boolean isHttpResponseObserver(HttpResponseObserver httpResponseObserver) {
+ for (WeakReference<HttpResponseObserver> weakReference : httpResponseObservers) {
+ HttpResponseObserver innerHttpResponseObserver = weakReference.get();
+ if (innerHttpResponseObserver == httpResponseObserver) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static boolean removeHttpResponseObserver(HttpResponseObserver httpResponseObserver) {
+ for (WeakReference<HttpResponseObserver> weakReference : httpResponseObservers) {
+ HttpResponseObserver innerHttpResponseObserver = weakReference.get();
+ if (innerHttpResponseObserver == httpResponseObserver) {
+ // It's safe to mutate the observers while iterating.
+ httpResponseObservers.remove(weakReference);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public URI getURI() {
+ return this.uri;
+ }
+
+ @Override
+ public String getURIString() {
+ return this.uri.toString();
+ }
+
+ @Override
+ public String getHostname() {
+ return this.getURI().getHost();
+ }
+
+ /**
+ * Causes the Resource to compress the uploaded entity payload in requests with payloads (e.g. post, put)
+ * @param shouldCompress true if the entity should be compressed, false otherwise
+ */
+ public void setShouldCompressUploadedEntity(final boolean shouldCompress) {
+ shouldGzipCompress = shouldCompress;
+ }
+
+ /**
+ * Causes the Resource to chunk the uploaded entity payload in requests with payloads (e.g. post, put).
+ * Note: this flag is only a hint - chunking is not guaranteed.
+ *
+ * Chunking is currently supported with gzip compression.
+ *
+ * @param shouldChunk true if the transfer should be chunked, false otherwise
+ */
+ public void setShouldChunkUploadsHint(final boolean shouldChunk) {
+ shouldChunkUploadsHint = shouldChunk;
+ }
+
+ private HttpEntity getMaybeCompressedEntity(final HttpEntity entity) {
+ if (!shouldGzipCompress) {
+ return entity;
+ }
+
+ return shouldChunkUploadsHint ? new GzipCompressingEntity(entity) : new GzipNonChunkedCompressingEntity(entity);
+ }
+
+ /**
+ * This shuts up HttpClient, which will otherwise debug log about there
+ * being no auth cache in the context.
+ */
+ private static void addAuthCacheToContext(HttpUriRequest request, HttpContext context) {
+ AuthCache authCache = new BasicAuthCache(); // Not thread safe.
+ context.setAttribute(ClientContext.AUTH_CACHE, authCache);
+ }
+
+ /**
+ * Invoke this after delegate and request have been set.
+ * @throws NoSuchAlgorithmException
+ * @throws KeyManagementException
+ */
+ protected void prepareClient() throws KeyManagementException, NoSuchAlgorithmException, GeneralSecurityException {
+ context = new BasicHttpContext();
+
+ // We could reuse these client instances, except that we mess around
+ // with their parameters… so we'd need a pool of some kind.
+ client = new DefaultHttpClient(getConnectionManager());
+
+ // TODO: Eventually we should use Apache HttpAsyncClient. It's not out of alpha yet.
+ // Until then, we synchronously make the request, then invoke our delegate's callback.
+ AuthHeaderProvider authHeaderProvider = delegate.getAuthHeaderProvider();
+ if (authHeaderProvider != null) {
+ Header authHeader = authHeaderProvider.getAuthHeader(request, context, client);
+ if (authHeader != null) {
+ request.addHeader(authHeader);
+ Logger.debug(LOG_TAG, "Added auth header.");
+ }
+ }
+
+ addAuthCacheToContext(request, context);
+
+ HttpParams params = client.getParams();
+ HttpConnectionParams.setConnectionTimeout(params, delegate.connectionTimeout());
+ HttpConnectionParams.setSoTimeout(params, delegate.socketTimeout());
+ HttpConnectionParams.setStaleCheckingEnabled(params, false);
+ HttpProtocolParams.setContentCharset(params, charset);
+ HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
+ final String userAgent = delegate.getUserAgent();
+ if (userAgent != null) {
+ HttpProtocolParams.setUserAgent(params, userAgent);
+ }
+ delegate.addHeaders(request, client);
+ }
+
+ private static final Object connManagerMonitor = new Object();
+ private static ClientConnectionManager connManager;
+
+ // Call within a synchronized block on connManagerMonitor.
+ private static ClientConnectionManager enableTLSConnectionManager() throws KeyManagementException, NoSuchAlgorithmException {
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, null, new SecureRandom());
+
+ Logger.debug(LOG_TAG, "Using protocols and cipher suites for Android API " + android.os.Build.VERSION.SDK_INT);
+ SSLSocketFactory sf = new SSLSocketFactory(sslContext, GlobalConstants.DEFAULT_PROTOCOLS, GlobalConstants.DEFAULT_CIPHER_SUITES, null);
+ SchemeRegistry schemeRegistry = new SchemeRegistry();
+ schemeRegistry.register(new Scheme("https", 443, sf));
+ schemeRegistry.register(new Scheme("http", 80, new PlainSocketFactory()));
+ ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(schemeRegistry);
+
+ cm.setMaxTotal(MAX_TOTAL_CONNECTIONS);
+ cm.setDefaultMaxPerRoute(MAX_CONNECTIONS_PER_ROUTE);
+ connManager = cm;
+ return cm;
+ }
+
+ public static ClientConnectionManager getConnectionManager() throws KeyManagementException, NoSuchAlgorithmException
+ {
+ // TODO: shutdown.
+ synchronized (connManagerMonitor) {
+ if (connManager != null) {
+ return connManager;
+ }
+ return enableTLSConnectionManager();
+ }
+ }
+
+ /**
+ * Do some cleanup, so we don't need the stale connection check.
+ */
+ public static void closeExpiredConnections() {
+ ClientConnectionManager connectionManager;
+ synchronized (connManagerMonitor) {
+ connectionManager = connManager;
+ }
+ if (connectionManager == null) {
+ return;
+ }
+ Logger.trace(LOG_TAG, "Closing expired connections.");
+ connectionManager.closeExpiredConnections();
+ }
+
+ public static void shutdownConnectionManager() {
+ ClientConnectionManager connectionManager;
+ synchronized (connManagerMonitor) {
+ connectionManager = connManager;
+ connManager = null;
+ }
+ if (connectionManager == null) {
+ return;
+ }
+ Logger.debug(LOG_TAG, "Shutting down connection manager.");
+ connectionManager.shutdown();
+ }
+
+ private void execute() {
+ HttpResponse response;
+ try {
+ response = client.execute(request, context);
+ Logger.debug(LOG_TAG, "Response: " + response.getStatusLine().toString());
+ } catch (ClientProtocolException e) {
+ delegate.handleHttpProtocolException(e);
+ return;
+ } catch (IOException e) {
+ Logger.debug(LOG_TAG, "I/O exception returned from execute.");
+ if (!retryOnFailedRequest) {
+ delegate.handleHttpIOException(e);
+ } else {
+ retryRequest();
+ }
+ return;
+ } catch (Exception e) {
+ // Bug 740731: Don't let an exception fall through. Wrapping isn't
+ // optimal, but often the exception is treated as an Exception anyway.
+ if (!retryOnFailedRequest) {
+ // Bug 769671: IOException(Throwable cause) was added only in API level 9.
+ final IOException ex = new IOException();
+ ex.initCause(e);
+ delegate.handleHttpIOException(ex);
+ } else {
+ retryRequest();
+ }
+ return;
+ }
+
+ // Don't retry if the observer or delegate throws!
+ for (WeakReference<HttpResponseObserver> weakReference : httpResponseObservers) {
+ HttpResponseObserver observer = weakReference.get();
+ if (observer != null) {
+ observer.observeHttpResponse(request, response);
+ }
+ }
+ delegate.handleHttpResponse(response);
+ }
+
+ private void retryRequest() {
+ // Only retry once.
+ retryOnFailedRequest = false;
+ Logger.debug(LOG_TAG, "Retrying request...");
+ this.execute();
+ }
+
+ private void go(HttpRequestBase request) {
+ if (delegate == null) {
+ throw new IllegalArgumentException("No delegate provided.");
+ }
+ this.request = request;
+ try {
+ this.prepareClient();
+ } catch (KeyManagementException e) {
+ Logger.error(LOG_TAG, "Couldn't prepare client.", e);
+ delegate.handleTransportException(e);
+ return;
+ } catch (GeneralSecurityException e) {
+ Logger.error(LOG_TAG, "Couldn't prepare client.", e);
+ delegate.handleTransportException(e);
+ return;
+ } catch (Exception e) {
+ // Bug 740731: Don't let an exception fall through. Wrapping isn't
+ // optimal, but often the exception is treated as an Exception anyway.
+ delegate.handleTransportException(new GeneralSecurityException(e));
+ return;
+ }
+ this.execute();
+ }
+
+ @Override
+ public void get() {
+ Logger.debug(LOG_TAG, "HTTP GET " + this.uri.toASCIIString());
+ this.go(new HttpGet(this.uri));
+ }
+
+ /**
+ * Perform an HTTP GET as with {@link BaseResource#get()}, returning only
+ * after callbacks have been invoked.
+ */
+ public void getBlocking() {
+ // Until we use the asynchronous Apache HttpClient, we can simply call
+ // through.
+ this.get();
+ }
+
+ @Override
+ public void delete() {
+ Logger.debug(LOG_TAG, "HTTP DELETE " + this.uri.toASCIIString());
+ this.go(new HttpDelete(this.uri));
+ }
+
+ @Override
+ public void post(HttpEntity body) {
+ Logger.debug(LOG_TAG, "HTTP POST " + this.uri.toASCIIString());
+ body = getMaybeCompressedEntity(body);
+ HttpPost request = new HttpPost(this.uri);
+ request.setEntity(body);
+ this.go(request);
+ }
+
+ @Override
+ public void patch(HttpEntity body) {
+ Logger.debug(LOG_TAG, "HTTP PATCH " + this.uri.toASCIIString());
+ body = getMaybeCompressedEntity(body);
+ HttpPatch request = new HttpPatch(this.uri);
+ request.setEntity(body);
+ this.go(request);
+ }
+
+ @Override
+ public void put(HttpEntity body) {
+ Logger.debug(LOG_TAG, "HTTP PUT " + this.uri.toASCIIString());
+ body = getMaybeCompressedEntity(body);
+ HttpPut request = new HttpPut(this.uri);
+ request.setEntity(body);
+ this.go(request);
+ }
+
+ protected static StringEntity stringEntityWithContentTypeApplicationJSON(String s) {
+ StringEntity e = new StringEntity(s, "UTF-8");
+ e.setContentType("application/json");
+ return e;
+ }
+
+ /**
+ * Helper for turning a JSON object into a payload.
+ * @throws UnsupportedEncodingException
+ */
+ protected static StringEntity jsonEntity(JSONObject body) {
+ return stringEntityWithContentTypeApplicationJSON(body.toJSONString());
+ }
+
+ /**
+ * Helper for turning an extended JSON object into a payload.
+ * @throws UnsupportedEncodingException
+ */
+ protected static StringEntity jsonEntity(ExtendedJSONObject body) {
+ return stringEntityWithContentTypeApplicationJSON(body.toJSONString());
+ }
+
+ /**
+ * Helper for turning a JSON array into a payload.
+ * @throws UnsupportedEncodingException
+ */
+ protected static HttpEntity jsonEntity(JSONArray toPOST) throws UnsupportedEncodingException {
+ return stringEntityWithContentTypeApplicationJSON(toPOST.toJSONString());
+ }
+
+ /**
+ * Best-effort attempt to ensure that the entity has been fully consumed and
+ * that the underlying stream has been closed.
+ *
+ * This releases the connection back to the connection pool.
+ *
+ * @param entity The HttpEntity to be consumed.
+ */
+ public static void consumeEntity(HttpEntity entity) {
+ try {
+ EntityUtils.consume(entity);
+ } catch (IOException e) {
+ // Doesn't matter.
+ }
+ }
+
+ /**
+ * Best-effort attempt to ensure that the entity corresponding to the given
+ * HTTP response has been fully consumed and that the underlying stream has
+ * been closed.
+ *
+ * This releases the connection back to the connection pool.
+ *
+ * @param response
+ * The HttpResponse to be consumed.
+ */
+ public static void consumeEntity(HttpResponse response) {
+ if (response == null) {
+ return;
+ }
+ try {
+ EntityUtils.consume(response.getEntity());
+ } catch (IOException e) {
+ }
+ }
+
+ /**
+ * Best-effort attempt to ensure that the entity corresponding to the given
+ * Sync storage response has been fully consumed and that the underlying
+ * stream has been closed.
+ *
+ * This releases the connection back to the connection pool.
+ *
+ * @param response
+ * The SyncStorageResponse to be consumed.
+ */
+ public static void consumeEntity(SyncStorageResponse response) {
+ if (response.httpResponse() == null) {
+ return;
+ }
+ consumeEntity(response.httpResponse());
+ }
+
+ /**
+ * Best-effort attempt to ensure that the reader has been fully consumed, so
+ * that the underlying stream will be closed.
+ *
+ * This should allow the connection to be released back to the connection pool.
+ *
+ * @param reader The BufferedReader to be consumed.
+ */
+ public static void consumeReader(BufferedReader reader) {
+ try {
+ reader.close();
+ } catch (IOException e) {
+ // Do nothing.
+ }
+ }
+
+ public void post(JSONArray jsonArray) throws UnsupportedEncodingException {
+ post(jsonEntity(jsonArray));
+ }
+
+ public void put(JSONObject jsonObject) throws UnsupportedEncodingException {
+ put(jsonEntity(jsonObject));
+ }
+
+ public void put(ExtendedJSONObject o) {
+ put(jsonEntity(o));
+ }
+
+ public void post(ExtendedJSONObject o) {
+ post(jsonEntity(o));
+ }
+
+ /**
+ * Perform an HTTP POST as with {@link BaseResource#post(ExtendedJSONObject)}, returning only
+ * after callbacks have been invoked.
+ */
+ public void postBlocking(final ExtendedJSONObject o) {
+ // Until we use the asynchronous Apache HttpClient, we can simply call
+ // through.
+ post(jsonEntity(o));
+ }
+
+ public void post(JSONObject jsonObject) throws UnsupportedEncodingException {
+ post(jsonEntity(jsonObject));
+ }
+
+ public void patch(JSONArray jsonArray) throws UnsupportedEncodingException {
+ patch(jsonEntity(jsonArray));
+ }
+
+ public void patch(ExtendedJSONObject o) {
+ patch(jsonEntity(o));
+ }
+
+ public void patch(JSONObject jsonObject) throws UnsupportedEncodingException {
+ patch(jsonEntity(jsonObject));
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BaseResourceDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BaseResourceDelegate.java
new file mode 100644
index 0000000000..84ae7a3d5c
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BaseResourceDelegate.java
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
+import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
+
+/**
+ * Shared abstract class for resource delegate that use the same timeouts
+ * and no credentials.
+ *
+ * @author rnewman
+ *
+ */
+public abstract class BaseResourceDelegate implements ResourceDelegate {
+ public static int connectionTimeoutInMillis = 1000 * 30; // Wait 30s for a connection to open.
+ public static int socketTimeoutInMillis = 1000 * 2 * 60; // Wait 2 minutes for data.
+
+ protected Resource resource;
+ public BaseResourceDelegate(Resource resource) {
+ this.resource = resource;
+ }
+
+ @Override
+ public int connectionTimeout() {
+ return connectionTimeoutInMillis;
+ }
+
+ @Override
+ public int socketTimeout() {
+ return socketTimeoutInMillis;
+ }
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return null;
+ }
+
+ @Override
+ public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BasicAuthHeaderProvider.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BasicAuthHeaderProvider.java
new file mode 100644
index 0000000000..d8a371ddca
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BasicAuthHeaderProvider.java
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.auth.Credentials;
+import ch.boye.httpclientandroidlib.auth.UsernamePasswordCredentials;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
+import ch.boye.httpclientandroidlib.impl.auth.BasicScheme;
+import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
+import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
+
+/**
+ * An <code>AuthHeaderProvider</code> that returns an HTTP Basic auth header.
+ */
+public class BasicAuthHeaderProvider implements AuthHeaderProvider {
+ protected final String credentials;
+
+ /**
+ * Constructor.
+ *
+ * @param credentials string in form "user:pass".
+ */
+ public BasicAuthHeaderProvider(String credentials) {
+ this.credentials = credentials;
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param user username.
+ * @param pass password.
+ */
+ public BasicAuthHeaderProvider(String user, String pass) {
+ this(user + ":" + pass);
+ }
+
+ /**
+ * Return a Header object representing an Authentication header for HTTP
+ * Basic.
+ */
+ @Override
+ public Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client) {
+ Credentials creds = new UsernamePasswordCredentials(credentials);
+
+ // This must be UTF-8 to generate the same Basic Auth headers as desktop for non-ASCII passwords.
+ return BasicScheme.authenticate(creds, "UTF-8", false);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BearerAuthHeaderProvider.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BearerAuthHeaderProvider.java
new file mode 100644
index 0000000000..d142d50d95
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BearerAuthHeaderProvider.java
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+/**
+ * An <code>AuthHeaderProvider</code> that returns an Authorization header for
+ * Bearer tokens in the format expected by a Mozilla Firefox Accounts Profile Server.
+ * <p>
+ * See <a href="https://github.com/mozilla/fxa-profile-server/blob/master/docs/API.md">https://github.com/mozilla/fxa-profile-server/blob/master/docs/API.md</a>.
+ */
+public class BearerAuthHeaderProvider extends AbstractBearerTokenAuthHeaderProvider {
+ public BearerAuthHeaderProvider(String token) {
+ super(token);
+ }
+
+ @Override
+ protected String getPrefix() {
+ return "Bearer";
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BrowserIDAuthHeaderProvider.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BrowserIDAuthHeaderProvider.java
new file mode 100644
index 0000000000..5004673b35
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BrowserIDAuthHeaderProvider.java
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+/**
+ * An <code>AuthHeaderProvider</code> that returns an Authorization header for
+ * BrowserID assertions in the format expected by a Mozilla Services Token
+ * Server.
+ * <p>
+ * See <a href="http://docs.services.mozilla.com/token/apis.html">http://docs.services.mozilla.com/token/apis.html</a>.
+ */
+public class BrowserIDAuthHeaderProvider extends AbstractBearerTokenAuthHeaderProvider {
+ public BrowserIDAuthHeaderProvider(String assertion) {
+ super(assertion);
+ }
+
+ @Override
+ protected String getPrefix() {
+ return "BrowserID";
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/ConnectionMonitorThread.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/ConnectionMonitorThread.java
new file mode 100644
index 0000000000..1a20117718
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/ConnectionMonitorThread.java
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import org.mozilla.gecko.background.common.log.Logger;
+
+/**
+ * Every <code>REAP_INTERVAL</code> milliseconds, wake up
+ * and expire any connections that need cleaning up.
+ *
+ * When we're told to shut down, take the connection manager
+ * with us.
+ */
+public class ConnectionMonitorThread extends Thread {
+ private static final long REAP_INTERVAL = 5000; // 5 seconds.
+ private static final String LOG_TAG = "ConnectionMonitorThread";
+
+ private volatile boolean stopping;
+
+ @Override
+ public void run() {
+ try {
+ while (!stopping) {
+ synchronized (this) {
+ wait(REAP_INTERVAL);
+ BaseResource.closeExpiredConnections();
+ }
+ }
+ } catch (InterruptedException e) {
+ Logger.trace(LOG_TAG, "Interrupted.");
+ }
+ BaseResource.shutdownConnectionManager();
+ }
+
+ public void shutdown() {
+ Logger.debug(LOG_TAG, "ConnectionMonitorThread told to shut down.");
+ stopping = true;
+ synchronized (this) {
+ notifyAll();
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/GzipNonChunkedCompressingEntity.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/GzipNonChunkedCompressingEntity.java
new file mode 100644
index 0000000000..1e238c022a
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/GzipNonChunkedCompressingEntity.java
@@ -0,0 +1,92 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.client.entity.GzipCompressingEntity;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Wrapping entity that compresses content when {@link #writeTo writing}.
+ *
+ * This differs from {@link GzipCompressingEntity} in that it does not chunk
+ * the sent data, therefore replacing the "Transfer-Encoding" HTTP header with
+ * the "Content-Length" header required by some servers.
+ *
+ * However, to measure the content length, the gzipped content will be temporarily
+ * stored in memory so be careful what content you send!
+ */
+public class GzipNonChunkedCompressingEntity extends GzipCompressingEntity {
+ final int MAX_BUFFER_SIZE_BYTES = 10 * 1000 * 1000; // 10 MB.
+
+ private byte[] gzippedContent;
+
+ public GzipNonChunkedCompressingEntity(final HttpEntity entity) {
+ super(entity);
+ }
+
+ /**
+ * @return content length for gzipped content or -1 if there is an error
+ */
+ @Override
+ public long getContentLength() {
+ try {
+ initBuffer();
+ } catch (final IOException e) {
+ // GzipCompressingEntity always returns -1 in which case a 'Content-Length' header is omitted.
+ // Presumably, without it the request will fail (either client-side or server-side).
+ return -1;
+ }
+ return gzippedContent.length;
+ }
+
+ @Override
+ public boolean isChunked() {
+ // "Content-Length" & chunked encoding are mutually exclusive:
+ // https://en.wikipedia.org/wiki/Chunked_transfer_encoding
+ return false;
+ }
+
+ @Override
+ public InputStream getContent() throws IOException {
+ initBuffer();
+ return new ByteArrayInputStream(gzippedContent);
+ }
+
+ @Override
+ public void writeTo(final OutputStream outstream) throws IOException {
+ initBuffer();
+ outstream.write(gzippedContent);
+ }
+
+ private void initBuffer() throws IOException {
+ if (gzippedContent != null) {
+ return;
+ }
+
+ final long unzippedContentLength = wrappedEntity.getContentLength();
+ if (unzippedContentLength > MAX_BUFFER_SIZE_BYTES) {
+ throw new IOException(
+ "Wrapped entity content length, " + unzippedContentLength + " bytes, exceeds max: " + MAX_BUFFER_SIZE_BYTES);
+ }
+
+ // The buffer size needed by the gzipped content should be smaller than this,
+ // but it's more efficient just to allocate one larger buffer than allocate
+ // twice if the gzipped content is too large for the default buffer.
+ final ByteArrayOutputStream s = new ByteArrayOutputStream((int) unzippedContentLength);
+ try {
+ super.writeTo(s);
+ } finally {
+ s.close();
+ }
+
+ gzippedContent = s.toByteArray();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HMACAuthHeaderProvider.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HMACAuthHeaderProvider.java
new file mode 100644
index 0000000000..5314d345b6
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HMACAuthHeaderProvider.java
@@ -0,0 +1,257 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.Utils;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
+import ch.boye.httpclientandroidlib.message.BasicHeader;
+import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
+
+/**
+ * An <code>AuthHeaderProvider</code> that returns an Authorization header for
+ * HMAC-SHA1-signed requests in the format expected by Mozilla Services
+ * identity-attached services and specified by the MAC Authentication spec, available at
+ * <a href="https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac">https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac</a>.
+ * <p>
+ * See <a href="https://wiki.mozilla.org/Services/Sagrada/ServiceClientFlow#Access">https://wiki.mozilla.org/Services/Sagrada/ServiceClientFlow#Access</a>.
+ */
+public class HMACAuthHeaderProvider implements AuthHeaderProvider {
+ public static final String LOG_TAG = "HMACAuthHeaderProvider";
+
+ public static final int NONCE_LENGTH_IN_BYTES = 8;
+
+ public static final String HMAC_SHA1_ALGORITHM = "hmacSHA1";
+
+ public final String identifier;
+ public final String key;
+
+ public HMACAuthHeaderProvider(String identifier, String key) {
+ // Validate identifier string. From the MAC Authentication spec:
+ // id = "id" "=" string-value
+ // string-value = ( <"> plain-string <"> ) / plain-string
+ // plain-string = 1*( %x20-21 / %x23-5B / %x5D-7E )
+ // We add quotes around the id string, so input identifier must be a plain-string.
+ if (identifier == null) {
+ throw new IllegalArgumentException("identifier must not be null.");
+ }
+ if (!isPlainString(identifier)) {
+ throw new IllegalArgumentException("identifier must be a plain-string.");
+ }
+
+ if (key == null) {
+ throw new IllegalArgumentException("key must not be null.");
+ }
+
+ this.identifier = identifier;
+ this.key = key;
+ }
+
+ @Override
+ public Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client) throws GeneralSecurityException {
+ long timestamp = System.currentTimeMillis() / 1000;
+ String nonce = Base64.encodeBase64String(Utils.generateRandomBytes(NONCE_LENGTH_IN_BYTES));
+ String extra = "";
+
+ try {
+ return getAuthHeader(request, context, client, timestamp, nonce, extra);
+ } catch (InvalidKeyException | NoSuchAlgorithmException | UnsupportedEncodingException e) {
+ // We lie a little and make every exception a GeneralSecurityException.
+ throw new GeneralSecurityException(e);
+ }
+ }
+
+ /**
+ * Test if input is a <code>plain-string</code>.
+ * <p>
+ * A plain-string is defined by the MAC Authentication spec as
+ * <code>plain-string = 1*( %x20-21 / %x23-5B / %x5D-7E )</code>.
+ *
+ * @param input
+ * as a String of "US-ASCII" bytes.
+ * @return true if input is a <code>plain-string</code>; false otherwise.
+ * @throws UnsupportedEncodingException
+ */
+ protected static boolean isPlainString(String input) {
+ if (input == null || input.length() == 0) {
+ return false;
+ }
+
+ byte[] bytes;
+ try {
+ bytes = input.getBytes("US-ASCII");
+ } catch (UnsupportedEncodingException e) {
+ // Should never happen.
+ Logger.warn(LOG_TAG, "Got exception in isPlainString; returning false.", e);
+ return false;
+ }
+
+ for (byte b : bytes) {
+ if ((0x20 <= b && b <= 0x21) || (0x23 <= b && b <= 0x5B) || (0x5D <= b && b <= 0x7E)) {
+ continue;
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Helper function that generates an HTTP Authorization header given
+ * additional MAC Authentication specific data.
+ *
+ * @throws UnsupportedEncodingException
+ * @throws NoSuchAlgorithmException
+ * @throws InvalidKeyException
+ */
+ protected Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client,
+ long timestamp, String nonce, String extra)
+ throws UnsupportedEncodingException, InvalidKeyException, NoSuchAlgorithmException {
+ // Validate timestamp. From the MAC Authentication spec:
+ // timestamp = 1*DIGIT
+ // This is equivalent to timestamp >= 0.
+ if (timestamp < 0) {
+ throw new IllegalArgumentException("timestamp must contain only [0-9].");
+ }
+
+ // Validate nonce string. From the MAC Authentication spec:
+ // nonce = "nonce" "=" string-value
+ // string-value = ( <"> plain-string <"> ) / plain-string
+ // plain-string = 1*( %x20-21 / %x23-5B / %x5D-7E )
+ // We add quotes around the nonce string, so input nonce must be a plain-string.
+ if (nonce == null) {
+ throw new IllegalArgumentException("nonce must not be null.");
+ }
+ if (nonce.length() == 0) {
+ throw new IllegalArgumentException("nonce must not be empty.");
+ }
+ if (!isPlainString(nonce)) {
+ throw new IllegalArgumentException("nonce must be a plain-string.");
+ }
+
+ // Validate extra string. From the MAC Authentication spec:
+ // ext = "ext" "=" string-value
+ // string-value = ( <"> plain-string <"> ) / plain-string
+ // plain-string = 1*( %x20-21 / %x23-5B / %x5D-7E )
+ // We add quotes around the extra string, so input extra must be a plain-string.
+ // We break the spec by allowing ext to be an empty string, i.e. to match 0*(...).
+ if (extra == null) {
+ throw new IllegalArgumentException("extra must not be null.");
+ }
+ if (extra.length() > 0 && !isPlainString(extra)) {
+ throw new IllegalArgumentException("extra must be a plain-string.");
+ }
+
+ String requestString = getRequestString(request, timestamp, nonce, extra);
+ String macString = getSignature(requestString, this.key);
+
+ String h = "MAC id=\"" + this.identifier + "\", " +
+ "ts=\"" + timestamp + "\", " +
+ "nonce=\"" + nonce + "\", " +
+ "mac=\"" + macString + "\"";
+
+ if (extra != null) {
+ h += ", ext=\"" + extra + "\"";
+ }
+
+ Header header = new BasicHeader("Authorization", h);
+
+ return header;
+ }
+
+ protected static byte[] sha1(byte[] message, byte[] key)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+
+ SecretKeySpec keySpec = new SecretKeySpec(key, HMAC_SHA1_ALGORITHM);
+
+ Mac hasher = Mac.getInstance(HMAC_SHA1_ALGORITHM);
+ hasher.init(keySpec);
+ hasher.update(message);
+
+ byte[] hmac = hasher.doFinal();
+
+ return hmac;
+ }
+
+ /**
+ * Sign an HMAC request string.
+ *
+ * @param requestString to sign.
+ * @param key as <code>String</code>.
+ * @return signature as base-64 encoded string.
+ * @throws InvalidKeyException
+ * @throws NoSuchAlgorithmException
+ * @throws UnsupportedEncodingException
+ */
+ protected static String getSignature(String requestString, String key)
+ throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
+ String macString = Base64.encodeBase64String(sha1(requestString.getBytes("UTF-8"), key.getBytes("UTF-8")));
+
+ return macString;
+ }
+
+ /**
+ * Generate an HMAC request string.
+ * <p>
+ * This method trusts its inputs to be valid as per the MAC Authentication spec.
+ *
+ * @param request HTTP request.
+ * @param timestamp to use.
+ * @param nonce to use.
+ * @param extra to use.
+ * @return request string.
+ */
+ protected static String getRequestString(HttpUriRequest request, long timestamp, String nonce, String extra) {
+ String method = request.getMethod().toUpperCase();
+
+ URI uri = request.getURI();
+ String host = uri.getHost();
+
+ String path = uri.getRawPath();
+ if (uri.getRawQuery() != null) {
+ path += "?";
+ path += uri.getRawQuery();
+ }
+ if (uri.getRawFragment() != null) {
+ path += "#";
+ path += uri.getRawFragment();
+ }
+
+ int port = uri.getPort();
+ String scheme = uri.getScheme();
+ if (port != -1) {
+ } else if ("http".equalsIgnoreCase(scheme)) {
+ port = 80;
+ } else if ("https".equalsIgnoreCase(scheme)) {
+ port = 443;
+ } else {
+ throw new IllegalArgumentException("Unsupported URI scheme: " + scheme + ".");
+ }
+
+ String requestString = timestamp + "\n" +
+ nonce + "\n" +
+ method + "\n" +
+ path + "\n" +
+ host + "\n" +
+ port + "\n" +
+ extra + "\n";
+
+ return requestString;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HandleProgressException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HandleProgressException.java
new file mode 100644
index 0000000000..27ec74b66c
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HandleProgressException.java
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import org.mozilla.gecko.sync.SyncException;
+
+public class HandleProgressException extends SyncException {
+ private static final long serialVersionUID = -4444933937013161059L;
+
+ public HandleProgressException(Exception ex) {
+ super(ex);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HawkAuthHeaderProvider.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HawkAuthHeaderProvider.java
new file mode 100644
index 0000000000..2bdd5604a8
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HawkAuthHeaderProvider.java
@@ -0,0 +1,403 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Locale;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.Utils;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
+import ch.boye.httpclientandroidlib.message.BasicHeader;
+import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
+
+/**
+ * An <code>AuthHeaderProvider</code> that returns an Authorization header for
+ * Hawk: <a href="https://github.com/hueniverse/hawk">https://github.com/hueniverse/hawk</a>.
+ *
+ * Hawk is an HTTP authentication scheme using a message authentication code
+ * (MAC) algorithm to provide partial HTTP request cryptographic verification.
+ * Hawk is the successor to the HMAC authentication scheme.
+ */
+public class HawkAuthHeaderProvider implements AuthHeaderProvider {
+ public static final String LOG_TAG = HawkAuthHeaderProvider.class.getSimpleName();
+
+ public static final int HAWK_HEADER_VERSION = 1;
+
+ protected static final int NONCE_LENGTH_IN_BYTES = 8;
+ protected static final String HMAC_SHA256_ALGORITHM = "hmacSHA256";
+
+ protected final String id;
+ protected final byte[] key;
+ protected final boolean includePayloadHash;
+ protected final long skewSeconds;
+
+ /**
+ * Create a Hawk Authorization header provider.
+ * <p>
+ * Hawk specifies no mechanism by which a client receives an
+ * identifier-and-key pair from the server.
+ * <p>
+ * Hawk requests can include a payload verification hash with requests that
+ * enclose an entity (PATCH, POST, and PUT requests). <b>You should default
+ * to including the payload verification hash<b> unless you have a good reason
+ * not to -- the server can always ignore payload verification hashes provided
+ * by the client.
+ *
+ * @param id
+ * to name requests with.
+ * @param key
+ * to sign request with.
+ *
+ * @param includePayloadHash
+ * true if payload verification hash should be included in signed
+ * request header. See <a href="https://github.com/hueniverse/hawk#payload-validation">https://github.com/hueniverse/hawk#payload-validation</a>.
+ *
+ * @param skewSeconds
+ * a number of seconds by which to skew the current time when
+ * computing a header.
+ */
+ public HawkAuthHeaderProvider(String id, byte[] key, boolean includePayloadHash, long skewSeconds) {
+ if (id == null) {
+ throw new IllegalArgumentException("id must not be null");
+ }
+ if (key == null) {
+ throw new IllegalArgumentException("key must not be null");
+ }
+ this.id = id;
+ this.key = key;
+ this.includePayloadHash = includePayloadHash;
+ this.skewSeconds = skewSeconds;
+ }
+
+ /**
+ * @return the current time in milliseconds.
+ */
+ @SuppressWarnings("static-method")
+ protected long now() {
+ return System.currentTimeMillis();
+ }
+
+ /**
+ * @return the current time in seconds, adjusted for skew. This should
+ * approximate the server's timestamp.
+ */
+ protected long getTimestampSeconds() {
+ return (now() / 1000) + skewSeconds;
+ }
+
+ @Override
+ public Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client) throws GeneralSecurityException {
+ long timestamp = getTimestampSeconds();
+ String nonce = Base64.encodeBase64String(Utils.generateRandomBytes(NONCE_LENGTH_IN_BYTES));
+ String extra = "";
+
+ try {
+ return getAuthHeader(request, context, client, timestamp, nonce, extra, this.includePayloadHash);
+ } catch (Exception e) {
+ // We lie a little and make every exception a GeneralSecurityException.
+ throw new GeneralSecurityException(e);
+ }
+ }
+
+ /**
+ * Helper function that generates an HTTP Authorization: Hawk header given
+ * additional Hawk specific data.
+ *
+ * @throws NoSuchAlgorithmException
+ * @throws InvalidKeyException
+ * @throws IOException
+ */
+ protected Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client,
+ long timestamp, String nonce, String extra, boolean includePayloadHash)
+ throws InvalidKeyException, NoSuchAlgorithmException, IOException {
+ if (timestamp < 0) {
+ throw new IllegalArgumentException("timestamp must contain only [0-9].");
+ }
+
+ if (nonce == null) {
+ throw new IllegalArgumentException("nonce must not be null.");
+ }
+ if (nonce.length() == 0) {
+ throw new IllegalArgumentException("nonce must not be empty.");
+ }
+
+ String payloadHash = null;
+ if (includePayloadHash) {
+ payloadHash = getPayloadHashString(request);
+ } else {
+ Logger.debug(LOG_TAG, "Configured to not include payload hash for this request.");
+ }
+
+ String app = null;
+ String dlg = null;
+ String requestString = getRequestString(request, "header", timestamp, nonce, payloadHash, extra, app, dlg);
+ String macString = getSignature(requestString.getBytes("UTF-8"), this.key);
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("Hawk id=\"");
+ sb.append(this.id);
+ sb.append("\", ");
+ sb.append("ts=\"");
+ sb.append(timestamp);
+ sb.append("\", ");
+ sb.append("nonce=\"");
+ sb.append(nonce);
+ sb.append("\", ");
+ if (payloadHash != null) {
+ sb.append("hash=\"");
+ sb.append(payloadHash);
+ sb.append("\", ");
+ }
+ if (extra != null && extra.length() > 0) {
+ sb.append("ext=\"");
+ sb.append(escapeExtraHeaderAttribute(extra));
+ sb.append("\", ");
+ }
+ sb.append("mac=\"");
+ sb.append(macString);
+ sb.append("\"");
+
+ return new BasicHeader("Authorization", sb.toString());
+ }
+
+ /**
+ * Get the payload verification hash for the given request, if possible.
+ * <p>
+ * Returns null if the request does not enclose an entity (is not an HTTP
+ * PATCH, POST, or PUT). Throws if the payload verification hash cannot be
+ * computed.
+ *
+ * @param request
+ * to compute hash for.
+ * @return verification hash, or null if the request does not enclose an entity.
+ * @throws IllegalArgumentException if the request does not enclose a valid non-null entity.
+ * @throws UnsupportedEncodingException
+ * @throws NoSuchAlgorithmException
+ * @throws IOException
+ */
+ protected static String getPayloadHashString(HttpRequestBase request)
+ throws UnsupportedEncodingException, NoSuchAlgorithmException, IOException, IllegalArgumentException {
+ final boolean shouldComputePayloadHash = request instanceof HttpEntityEnclosingRequest;
+ if (!shouldComputePayloadHash) {
+ Logger.debug(LOG_TAG, "Not computing payload verification hash for non-enclosing request.");
+ return null;
+ }
+ if (!(request instanceof HttpEntityEnclosingRequest)) {
+ throw new IllegalArgumentException("Cannot compute payload verification hash for enclosing request without an entity");
+ }
+ final HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
+ if (entity == null) {
+ throw new IllegalArgumentException("Cannot compute payload verification hash for enclosing request with a null entity");
+ }
+ return Base64.encodeBase64String(getPayloadHash(entity));
+ }
+
+ /**
+ * Escape the user-provided extra string for the ext="" header attribute.
+ * <p>
+ * Hawk escapes the header ext="" attribute differently than it does the extra
+ * line in the normalized request string.
+ * <p>
+ * See <a href="https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/browser.js#L385">https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/browser.js#L385</a>.
+ *
+ * @param extra to escape.
+ * @return extra escaped for the ext="" header attribute.
+ */
+ protected static String escapeExtraHeaderAttribute(String extra) {
+ return extra.replaceAll("\\\\", "\\\\").replaceAll("\"", "\\\"");
+ }
+
+ /**
+ * Escape the user-provided extra string for inserting into the normalized
+ * request string.
+ * <p>
+ * Hawk escapes the header ext="" attribute differently than it does the extra
+ * line in the normalized request string.
+ * <p>
+ * See <a href="https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L67">https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L67</a>.
+ *
+ * @param extra to escape.
+ * @return extra escaped for the normalized request string.
+ */
+ protected static String escapeExtraString(String extra) {
+ return extra.replaceAll("\\\\", "\\\\").replaceAll("\n", "\\n");
+ }
+
+ /**
+ * Return the content type with no parameters (pieces following ;).
+ *
+ * @param contentTypeHeader to interrogate.
+ * @return base content type.
+ */
+ protected static String getBaseContentType(Header contentTypeHeader) {
+ if (contentTypeHeader == null) {
+ throw new IllegalArgumentException("contentTypeHeader must not be null.");
+ }
+ String contentType = contentTypeHeader.getValue();
+ if (contentType == null) {
+ throw new IllegalArgumentException("contentTypeHeader value must not be null.");
+ }
+ int index = contentType.indexOf(";");
+ if (index < 0) {
+ return contentType.trim();
+ }
+ return contentType.substring(0, index).trim();
+ }
+
+ /**
+ * Generate the SHA-256 hash of a normalized Hawk payload generated from an
+ * HTTP entity.
+ * <p>
+ * <b>Warning:</b> the entity <b>must</b> be repeatable. If it is not, this
+ * code throws an <code>IllegalArgumentException</code>.
+ * <p>
+ * This is under-specified; the code here was reverse engineered from the code
+ * at
+ * <a href="https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L81">https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L81</a>.
+ * @param entity to normalize and hash.
+ * @return hash.
+ * @throws IllegalArgumentException if entity is not repeatable.
+ */
+ protected static byte[] getPayloadHash(HttpEntity entity) throws UnsupportedEncodingException, IOException, NoSuchAlgorithmException {
+ if (!entity.isRepeatable()) {
+ throw new IllegalArgumentException("entity must be repeatable");
+ }
+ final MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ digest.update(("hawk." + HAWK_HEADER_VERSION + ".payload\n").getBytes("UTF-8"));
+ digest.update(getBaseContentType(entity.getContentType()).getBytes("UTF-8"));
+ digest.update("\n".getBytes("UTF-8"));
+ InputStream stream = entity.getContent();
+ try {
+ int numRead;
+ byte[] buffer = new byte[4096];
+ while (-1 != (numRead = stream.read(buffer))) {
+ if (numRead > 0) {
+ digest.update(buffer, 0, numRead);
+ }
+ }
+ digest.update("\n".getBytes("UTF-8")); // Trailing newline is specified by Hawk.
+ return digest.digest();
+ } finally {
+ stream.close();
+ }
+ }
+
+ /**
+ * Generate a normalized Hawk request string. This is under-specified; the
+ * code here was reverse engineered from the code at
+ * <a href="https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L55">https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L55</a>.
+ * <p>
+ * This method trusts its inputs to be valid.
+ */
+ protected static String getRequestString(HttpUriRequest request, String type, long timestamp, String nonce, String hash, String extra, String app, String dlg) {
+ String method = request.getMethod().toUpperCase(Locale.US);
+
+ URI uri = request.getURI();
+ String host = uri.getHost();
+
+ String path = uri.getRawPath();
+ if (uri.getRawQuery() != null) {
+ path += "?";
+ path += uri.getRawQuery();
+ }
+ if (uri.getRawFragment() != null) {
+ path += "#";
+ path += uri.getRawFragment();
+ }
+
+ int port = uri.getPort();
+ String scheme = uri.getScheme();
+ if (port != -1) {
+ } else if ("http".equalsIgnoreCase(scheme)) {
+ port = 80;
+ } else if ("https".equalsIgnoreCase(scheme)) {
+ port = 443;
+ } else {
+ throw new IllegalArgumentException("Unsupported URI scheme: " + scheme + ".");
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("hawk.");
+ sb.append(HAWK_HEADER_VERSION);
+ sb.append('.');
+ sb.append(type);
+ sb.append('\n');
+ sb.append(timestamp);
+ sb.append('\n');
+ sb.append(nonce);
+ sb.append('\n');
+ sb.append(method);
+ sb.append('\n');
+ sb.append(path);
+ sb.append('\n');
+ sb.append(host);
+ sb.append('\n');
+ sb.append(port);
+ sb.append('\n');
+ if (hash != null) {
+ sb.append(hash);
+ }
+ sb.append("\n");
+ if (extra != null && extra.length() > 0) {
+ sb.append(escapeExtraString(extra));
+ }
+ sb.append("\n");
+ if (app != null) {
+ sb.append(app);
+ sb.append("\n");
+ if (dlg != null) {
+ sb.append(dlg);
+ }
+ sb.append("\n");
+ }
+
+ return sb.toString();
+ }
+
+ protected static byte[] hmacSha256(byte[] message, byte[] key)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+
+ SecretKeySpec keySpec = new SecretKeySpec(key, HMAC_SHA256_ALGORITHM);
+
+ Mac hasher = Mac.getInstance(HMAC_SHA256_ALGORITHM);
+ hasher.init(keySpec);
+ hasher.update(message);
+
+ return hasher.doFinal();
+ }
+
+ /**
+ * Sign a Hawk request string.
+ *
+ * @param requestString to sign.
+ * @param key as <code>String</code>.
+ * @return signature as base-64 encoded string.
+ * @throws InvalidKeyException
+ * @throws NoSuchAlgorithmException
+ * @throws UnsupportedEncodingException
+ */
+ protected static String getSignature(byte[] requestString, byte[] key)
+ throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
+ return Base64.encodeBase64String(hmacSha256(requestString, key));
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HttpResponseObserver.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HttpResponseObserver.java
new file mode 100644
index 0000000000..24b37a0e61
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HttpResponseObserver.java
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+
+public interface HttpResponseObserver {
+ /**
+ * Observe an HTTP response.
+ * @param request
+ * The <code>HttpUriRequest<code> that elicited the response.
+ *
+ * @param response
+ * The <code>HttpResponse</code> to observe.
+ */
+ public void observeHttpResponse(HttpUriRequest request, HttpResponse response);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/MozResponse.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/MozResponse.java
new file mode 100644
index 0000000000..3f76f929f0
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/MozResponse.java
@@ -0,0 +1,225 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.Scanner;
+
+import org.json.simple.JSONArray;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonArrayJSONException;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.impl.cookie.DateParseException;
+import ch.boye.httpclientandroidlib.impl.cookie.DateUtils;
+
+public class MozResponse {
+ private static final String LOG_TAG = "MozResponse";
+
+ private static final String HEADER_RETRY_AFTER = "retry-after";
+
+ protected HttpResponse response;
+ private String body = null;
+
+ public HttpResponse httpResponse() {
+ return this.response;
+ }
+
+ public int getStatusCode() {
+ return this.response.getStatusLine().getStatusCode();
+ }
+
+ public boolean wasSuccessful() {
+ return this.getStatusCode() == 200;
+ }
+
+ public boolean isInvalidAuthentication() {
+ return this.getStatusCode() == HttpStatus.SC_UNAUTHORIZED;
+ }
+
+ /**
+ * Fetch the content type of the HTTP response body.
+ *
+ * @return a <code>Header</code> instance, or <code>null</code> if there was
+ * no body or no valid Content-Type.
+ */
+ public Header getContentType() {
+ HttpEntity entity = this.response.getEntity();
+ if (entity == null) {
+ return null;
+ }
+ return entity.getContentType();
+ }
+
+ private static boolean missingHeader(String value) {
+ return value == null ||
+ value.trim().length() == 0;
+ }
+
+ public String body() throws IllegalStateException, IOException {
+ if (body != null) {
+ return body;
+ }
+ final HttpEntity entity = this.response.getEntity();
+ if (entity == null) {
+ body = null;
+ return null;
+ }
+
+ InputStreamReader is = new InputStreamReader(entity.getContent());
+ // Oh, Java, you are so evil.
+ body = new Scanner(is).useDelimiter("\\A").next();
+ return body;
+ }
+
+ /**
+ * Return the body as a <b>non-null</b> <code>ExtendedJSONObject</code>.
+ *
+ * @return A non-null <code>ExtendedJSONObject</code>.
+ *
+ * @throws IllegalStateException
+ * @throws IOException
+ * @throws NonObjectJSONException
+ */
+ public ExtendedJSONObject jsonObjectBody() throws IllegalStateException, IOException, NonObjectJSONException {
+ if (body != null) {
+ // Do it from the cached String.
+ return new ExtendedJSONObject(body);
+ }
+
+ HttpEntity entity = this.response.getEntity();
+ if (entity == null) {
+ throw new IOException("no entity");
+ }
+
+ InputStream content = entity.getContent();
+ try {
+ Reader in = new BufferedReader(new InputStreamReader(content, "UTF-8"));
+ return new ExtendedJSONObject(in);
+ } finally {
+ content.close();
+ }
+ }
+
+ public JSONArray jsonArrayBody() throws NonArrayJSONException, IOException {
+ final JSONParser parser = new JSONParser();
+ try {
+ if (body != null) {
+ // Do it from the cached String.
+ return (JSONArray) parser.parse(body);
+ }
+
+ final HttpEntity entity = this.response.getEntity();
+ if (entity == null) {
+ throw new IOException("no entity");
+ }
+
+ final InputStream content = entity.getContent();
+ final Reader in = new BufferedReader(new InputStreamReader(content, "UTF-8"));
+ try {
+ return (JSONArray) parser.parse(in);
+ } finally {
+ in.close();
+ }
+ } catch (ClassCastException | ParseException e) {
+ NonArrayJSONException exception = new NonArrayJSONException("value must be a json array");
+ exception.initCause(e);
+ throw exception;
+ }
+ }
+
+ protected boolean hasHeader(String h) {
+ return this.response.containsHeader(h);
+ }
+
+ public MozResponse(HttpResponse res) {
+ response = res;
+ }
+
+ protected String getNonMissingHeader(String h) {
+ if (!this.hasHeader(h)) {
+ return null;
+ }
+
+ final Header header = this.response.getFirstHeader(h);
+ final String value = header.getValue();
+ if (missingHeader(value)) {
+ Logger.warn(LOG_TAG, h + " header present but empty.");
+ return null;
+ }
+ return value;
+ }
+
+ protected long getLongHeader(String h) throws NumberFormatException {
+ final String value = getNonMissingHeader(h);
+ if (value == null) {
+ return -1L;
+ }
+ return Long.parseLong(value, 10);
+ }
+
+ protected int getIntegerHeader(String h) throws NumberFormatException {
+ final String value = getNonMissingHeader(h);
+ if (value == null) {
+ return -1;
+ }
+ return Integer.parseInt(value, 10);
+ }
+
+ /**
+ * @return A number of seconds, or -1 if the 'Retry-After' header was not present.
+ */
+ public int retryAfterInSeconds() throws NumberFormatException {
+ final String retryAfter = getNonMissingHeader(HEADER_RETRY_AFTER);
+ if (retryAfter == null) {
+ return -1;
+ }
+
+ try {
+ return Integer.parseInt(retryAfter, 10);
+ } catch (NumberFormatException e) {
+ // Fall through to try date format.
+ }
+
+ try {
+ final long then = DateUtils.parseDate(retryAfter).getTime();
+ final long now = System.currentTimeMillis();
+ return (int)((then - now) / 1000); // Convert milliseconds to seconds.
+ } catch (DateParseException e) {
+ Logger.warn(LOG_TAG, "Retry-After header neither integer nor date: " + retryAfter);
+ return -1;
+ }
+ }
+
+ /**
+ * @return A number of seconds, or -1 if the 'Backoff' header was not
+ * present.
+ */
+ public int backoffInSeconds() throws NumberFormatException {
+ return this.getIntegerHeader("backoff");
+ }
+
+ public void logResponseBody(final String logTag) {
+ if (!Logger.LOG_PERSONAL_INFORMATION) {
+ return;
+ }
+ try {
+ Logger.pii(logTag, "Response body: " + body());
+ } catch (Throwable e) {
+ Logger.debug(logTag, "No response body.");
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/Resource.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/Resource.java
new file mode 100644
index 0000000000..ab7b98aff8
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/Resource.java
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import java.net.URI;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+
+public interface Resource {
+ public abstract URI getURI();
+ public abstract String getURIString();
+ public abstract String getHostname();
+ public abstract void get();
+ public abstract void delete();
+ public abstract void post(HttpEntity body);
+ public abstract void patch(HttpEntity body);
+ public abstract void put(HttpEntity body);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/ResourceDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/ResourceDelegate.java
new file mode 100644
index 0000000000..0dea9432b3
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/ResourceDelegate.java
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+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;
+
+/**
+ * ResourceDelegate implementers must ensure that HTTP responses
+ * are fully consumed to ensure that connections are returned to
+ * the pool:
+ *
+ * EntityUtils.consume(entity);
+ * @author rnewman
+ *
+ */
+public interface ResourceDelegate {
+ // Request augmentation.
+ AuthHeaderProvider getAuthHeaderProvider();
+ void addHeaders(HttpRequestBase request, DefaultHttpClient client);
+
+ /**
+ * The value of the User-Agent header to include with the request.
+ *
+ * @return User-Agent header value; null means do not set User-Agent header.
+ */
+ public String getUserAgent();
+
+ // Response handling.
+
+ /**
+ * Override this to handle an HttpResponse.
+ *
+ * ResourceDelegate implementers <b>must</b> ensure that HTTP responses are
+ * fully consumed to ensure that connections are returned to the pool, for
+ * example by calling <code>EntityUtils.consume(response.getEntity())</code>.
+ */
+ void handleHttpResponse(HttpResponse response);
+ void handleHttpProtocolException(ClientProtocolException e);
+ void handleHttpIOException(IOException e);
+
+ // During preparation.
+ void handleTransportException(GeneralSecurityException e);
+
+ // Connection parameters.
+ int connectionTimeout();
+ int socketTimeout();
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SRPConstants.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SRPConstants.java
new file mode 100644
index 0000000000..5dfe660eff
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SRPConstants.java
@@ -0,0 +1,174 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import java.math.BigInteger;
+
+/**
+ * SRP Group Parameters from
+ * <a href="http://tools.ietf.org/html/rfc5054#appendix-A">Appendix A of RFC 5054</a>.
+ *
+ * The 1024-, 1536-, and 2048-bit groups are taken from software
+ * developed by Tom Wu and Eugene Jhong for the Stanford SRP
+ * distribution, and subsequently proven to be prime. The larger primes
+ * are taken from [MODP], but generators have been calculated that are
+ * primitive roots of N, unlike the generators in [MODP].
+ *
+ * The 1024-bit and 1536-bit groups <b>MUST</b> be supported.
+ */
+public class SRPConstants {
+ public static class Parameters {
+ public final BigInteger N;
+ public final BigInteger g;
+ public final int bitLength;
+ public final int byteLength;
+ public final int hexLength;
+
+ protected Parameters(String N, long g) {
+ if (N == null) {
+ throw new IllegalArgumentException("N must not be null");
+ }
+ this.N = new BigInteger(N.replaceAll(" ", ""), 16); // Hex.
+ this.g = BigInteger.valueOf(g);
+ this.hexLength = this.N.toString(16).length();
+ this.byteLength = hexLength / 2;
+ this.bitLength = this.byteLength * 8;
+ }
+ }
+
+ public static final Parameters _1024 = new Parameters("" +
+ "EEAF0AB9 ADB38DD6 9C33F80A FA8FC5E8 60726187 75FF3C0B 9EA2314C" +
+ "9C256576 D674DF74 96EA81D3 383B4813 D692C6E0 E0D5D8E2 50B98BE4" +
+ "8E495C1D 6089DAD1 5DC7D7B4 6154D6B6 CE8EF4AD 69B15D49 82559B29" +
+ "7BCF1885 C529F566 660E57EC 68EDBC3C 05726CC0 2FD4CBF4 976EAA9A" +
+ "FD5138FE 8376435B 9FC61D2F C0EB06E3", 2L);
+
+ public static final Parameters _1536 = new Parameters("" +
+ "9DEF3CAF B939277A B1F12A86 17A47BBB DBA51DF4 99AC4C80 BEEEA961" +
+ "4B19CC4D 5F4F5F55 6E27CBDE 51C6A94B E4607A29 1558903B A0D0F843" +
+ "80B655BB 9A22E8DC DF028A7C EC67F0D0 8134B1C8 B9798914 9B609E0B" +
+ "E3BAB63D 47548381 DBC5B1FC 764E3F4B 53DD9DA1 158BFD3E 2B9C8CF5" +
+ "6EDF0195 39349627 DB2FD53D 24B7C486 65772E43 7D6C7F8C E442734A" +
+ "F7CCB7AE 837C264A E3A9BEB8 7F8A2FE9 B8B5292E 5A021FFF 5E91479E" +
+ "8CE7A28C 2442C6F3 15180F93 499A234D CF76E3FE D135F9BB", 2L);
+
+ public static final Parameters _2048 = new Parameters("" +
+ "AC6BDB41 324A9A9B F166DE5E 1389582F AF72B665 1987EE07 FC319294" +
+ "3DB56050 A37329CB B4A099ED 8193E075 7767A13D D52312AB 4B03310D" +
+ "CD7F48A9 DA04FD50 E8083969 EDB767B0 CF609517 9A163AB3 661A05FB" +
+ "D5FAAAE8 2918A996 2F0B93B8 55F97993 EC975EEA A80D740A DBF4FF74" +
+ "7359D041 D5C33EA7 1D281E44 6B14773B CA97B43A 23FB8016 76BD207A" +
+ "436C6481 F1D2B907 8717461A 5B9D32E6 88F87748 544523B5 24B0D57D" +
+ "5EA77A27 75D2ECFA 032CFBDB F52FB378 61602790 04E57AE6 AF874E73" +
+ "03CE5329 9CCC041C 7BC308D8 2A5698F3 A8D0C382 71AE35F8 E9DBFBB6" +
+ "94B5C803 D89F7AE4 35DE236D 525F5475 9B65E372 FCD68EF2 0FA7111F" +
+ "9E4AFF73", 2L);
+
+ public static final Parameters _3072 = new Parameters("" +
+ "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08" +
+ "8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B" +
+ "302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9" +
+ "A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6" +
+ "49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8" +
+ "FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" +
+ "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C" +
+ "180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718" +
+ "3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D" +
+ "04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D" +
+ "B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226" +
+ "1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" +
+ "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC" +
+ "E0FD108E 4B82D120 A93AD2CA FFFFFFFF FFFFFFFF", 5L);
+
+ public static final Parameters _4096 = new Parameters("" +
+ "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08" +
+ "8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B" +
+ "302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9" +
+ "A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6" +
+ "49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8" +
+ "FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" +
+ "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C" +
+ "180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718" +
+ "3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D" +
+ "04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D" +
+ "B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226" +
+ "1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" +
+ "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC" +
+ "E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26" +
+ "99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB" +
+ "04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2" +
+ "233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127" +
+ "D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34063199" +
+ "FFFFFFFF FFFFFFFF", 5L);
+
+ public static final Parameters _6144 = new Parameters("" +
+ "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08" +
+ "8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B" +
+ "302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9" +
+ "A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6" +
+ "49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8" +
+ "FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" +
+ "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C" +
+ "180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718" +
+ "3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D" +
+ "04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D" +
+ "B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226" +
+ "1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" +
+ "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC" +
+ "E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26" +
+ "99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB" +
+ "04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2" +
+ "233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127" +
+ "D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492" +
+ "36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD F8FF9406" +
+ "AD9E530E E5DB382F 413001AE B06A53ED 9027D831 179727B0 865A8918" +
+ "DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B DB7F1447 E6CC254B 33205151" +
+ "2BD7AF42 6FB8F401 378CD2BF 5983CA01 C64B92EC F032EA15 D1721D03" +
+ "F482D7CE 6E74FEF6 D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F" +
+ "BEC7E8F3 23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA" +
+ "CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328 06A1D58B" +
+ "B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C DA56C9EC 2EF29632" +
+ "387FE8D7 6E3C0468 043E8F66 3F4860EE 12BF2D5B 0B7474D6 E694F91E" +
+ "6DCC4024 FFFFFFFF FFFFFFFF", 5L);
+
+ public static final Parameters _8192 = new Parameters("" +
+ "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08" +
+ "8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B" +
+ "302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9" +
+ "A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6" +
+ "49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8" +
+ "FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" +
+ "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C" +
+ "180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718" +
+ "3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D" +
+ "04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D" +
+ "B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226" +
+ "1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" +
+ "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC" +
+ "E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26" +
+ "99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB" +
+ "04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2" +
+ "233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127" +
+ "D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492" +
+ "36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD F8FF9406" +
+ "AD9E530E E5DB382F 413001AE B06A53ED 9027D831 179727B0 865A8918" +
+ "DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B DB7F1447 E6CC254B 33205151" +
+ "2BD7AF42 6FB8F401 378CD2BF 5983CA01 C64B92EC F032EA15 D1721D03" +
+ "F482D7CE 6E74FEF6 D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F" +
+ "BEC7E8F3 23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA" +
+ "CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328 06A1D58B" +
+ "B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C DA56C9EC 2EF29632" +
+ "387FE8D7 6E3C0468 043E8F66 3F4860EE 12BF2D5B 0B7474D6 E694F91E" +
+ "6DBE1159 74A3926F 12FEE5E4 38777CB6 A932DF8C D8BEC4D0 73B931BA" +
+ "3BC832B6 8D9DD300 741FA7BF 8AFC47ED 2576F693 6BA42466 3AAB639C" +
+ "5AE4F568 3423B474 2BF1C978 238F16CB E39D652D E3FDB8BE FC848AD9" +
+ "22222E04 A4037C07 13EB57A8 1A23F0C7 3473FC64 6CEA306B 4BCBC886" +
+ "2F8385DD FA9D4B7F A2C087E8 79683303 ED5BDD3A 062B3CF5 B3A278A6" +
+ "6D2A13F8 3F44F82D DF310EE0 74AB6A36 4597E899 A0255DC1 64F31CC5" +
+ "0846851D F9AB4819 5DED7EA1 B1D510BD 7EE74D73 FAF36BC3 1ECFA268" +
+ "359046F4 EB879F92 4009438B 481C6CD7 889A002E D5EE382B C9190DA6" +
+ "FC026E47 9558E447 5677E9AA 9E3050E2 765694DF C81F56E8 80B96E71" +
+ "60C980DD 98EDD3DF FFFFFFFF FFFFFFFF", 19L);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncResponse.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncResponse.java
new file mode 100644
index 0000000000..177d7aabad
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncResponse.java
@@ -0,0 +1,157 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import android.support.annotation.Nullable;
+
+import org.mozilla.gecko.sync.Utils;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+
+public class SyncResponse extends MozResponse {
+ public static final String X_WEAVE_BACKOFF = "x-weave-backoff";
+ public static final String X_BACKOFF = "x-backoff";
+ public static final String X_LAST_MODIFIED = "x-last-modified";
+ public static final String X_WEAVE_TIMESTAMP = "x-weave-timestamp";
+ public static final String X_WEAVE_RECORDS = "x-weave-records";
+ public static final String X_WEAVE_QUOTA_REMAINING = "x-weave-quota-remaining";
+ public static final String X_WEAVE_ALERT = "x-weave-alert";
+ public static final String X_WEAVE_NEXT_OFFSET = "x-weave-next-offset";
+
+ public SyncResponse(HttpResponse res) {
+ super(res);
+ }
+
+ /**
+ * @return A number of seconds, or -1 if the 'X-Weave-Backoff' header was not
+ * present.
+ */
+ public int weaveBackoffInSeconds() throws NumberFormatException {
+ return this.getIntegerHeader(X_WEAVE_BACKOFF);
+ }
+
+ /**
+ * @return A number of seconds, or -1 if the 'X-Backoff' header was not
+ * present.
+ */
+ public int xBackoffInSeconds() throws NumberFormatException {
+ return this.getIntegerHeader(X_BACKOFF);
+ }
+
+ /**
+ * Extract a number of seconds, or -1 if none of the specified headers were present.
+ *
+ * @param includeRetryAfter
+ * if <code>true</code>, the Retry-After header is excluded. This is
+ * useful for processing non-error responses where a Retry-After
+ * header would be unexpected.
+ * @return the maximum of the three possible backoff headers, in seconds.
+ */
+ public int totalBackoffInSeconds(boolean includeRetryAfter) {
+ int retryAfterInSeconds = -1;
+ if (includeRetryAfter) {
+ try {
+ retryAfterInSeconds = retryAfterInSeconds();
+ } catch (NumberFormatException e) {
+ }
+ }
+
+ int weaveBackoffInSeconds = -1;
+ try {
+ weaveBackoffInSeconds = weaveBackoffInSeconds();
+ } catch (NumberFormatException e) {
+ }
+
+ int backoffInSeconds = -1;
+ try {
+ backoffInSeconds = xBackoffInSeconds();
+ } catch (NumberFormatException e) {
+ }
+
+ int totalBackoff = Math.max(retryAfterInSeconds, Math.max(backoffInSeconds, weaveBackoffInSeconds));
+ if (totalBackoff < 0) {
+ return -1;
+ } else {
+ return totalBackoff;
+ }
+ }
+
+ /**
+ * @return A number of milliseconds, or -1 if neither the 'Retry-After',
+ * 'X-Backoff', or 'X-Weave-Backoff' header were present.
+ */
+ public long totalBackoffInMilliseconds() {
+ long totalBackoff = totalBackoffInSeconds(true);
+ if (totalBackoff < 0) {
+ return -1;
+ } else {
+ return 1000 * totalBackoff;
+ }
+ }
+
+ public long normalizedWeaveTimestamp() {
+ return normalizedTimestampForHeader(X_WEAVE_TIMESTAMP);
+ }
+
+ /**
+ * Timestamps returned from a Sync server are decimal numbers of seconds,
+ * e.g., 1323393518.04.
+ *
+ * We want milliseconds since epoch.
+ *
+ * @return milliseconds since the epoch, as a long, or -1 if the header
+ * was missing or invalid.
+ */
+ public long normalizedTimestampForHeader(String header) {
+ if (!this.hasHeader(header)) {
+ return -1;
+ }
+
+ return Utils.decimalSecondsToMilliseconds(
+ this.response.getFirstHeader(header).getValue()
+ );
+ }
+
+ public int weaveRecords() throws NumberFormatException {
+ return this.getIntegerHeader(X_WEAVE_RECORDS);
+ }
+
+ public int weaveQuotaRemaining() throws NumberFormatException {
+ return this.getIntegerHeader(X_WEAVE_QUOTA_REMAINING);
+ }
+
+ public String weaveAlert() {
+ return this.getNonMissingHeader(X_WEAVE_ALERT);
+ }
+
+ /**
+ * This header may be sent back with multi-record responses where the request included a limit parameter.
+ * Its presence indicates that the number of available records exceeded the given limit.
+ * The value from this header can be passed back in the offset parameter to retrieve additional records.
+ * The value of this header will always be a string of characters from the urlsafe-base64 alphabet.
+ * The specific contents of the string are an implementation detail of the server,
+ * so clients should treat it as an opaque token.
+ *
+ * @return the offset header
+ */
+ public String weaveOffset() {
+ return this.getNonMissingHeader(X_WEAVE_NEXT_OFFSET);
+ }
+
+ /**
+ * This header gives the last-modified time of the target resource as seen during processing of the request,
+ * and will be included in all success responses (200, 201, 204).
+ * When given in response to a write request, this will be equal to the server’s current time and
+ * to the new last-modified time of any BSOs created or changed by the request.
+ * It is similar to the standard HTTP Last-Modified header,
+ * but the value is a decimal timestamp rather than a HTTP-format date.
+ *
+ * @return the last modified header
+ */
+ @Nullable
+ public String lastModified() {
+ return this.getNonMissingHeader(X_LAST_MODIFIED);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageCollectionRequest.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageCollectionRequest.java
new file mode 100644
index 0000000000..3ae672f21e
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageCollectionRequest.java
@@ -0,0 +1,145 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URI;
+
+import org.mozilla.gecko.background.common.log.Logger;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
+import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
+
+/**
+ * A request class that handles line-by-line responses. Eventually this will
+ * handle real stream processing; for now, just parse the returned body
+ * line-by-line.
+ *
+ * @author rnewman
+ *
+ */
+public class SyncStorageCollectionRequest extends SyncStorageRequest {
+ private static final String LOG_TAG = "CollectionRequest";
+
+ public SyncStorageCollectionRequest(URI uri) {
+ super(uri);
+ }
+
+ protected volatile boolean aborting = false;
+
+ /**
+ * Instruct the request that it should process no more records,
+ * and decline to notify any more delegate callbacks.
+ */
+ public void abort() {
+ aborting = true;
+ try {
+ this.resource.request.abort();
+ } catch (Exception e) {
+ // Just in case.
+ Logger.warn(LOG_TAG, "Got exception in abort: " + e);
+ }
+ }
+
+ @Override
+ protected BaseResourceDelegate makeResourceDelegate(SyncStorageRequest request) {
+ return new SyncCollectionResourceDelegate((SyncStorageCollectionRequest) request);
+ }
+
+ // TODO: this is awful.
+ public class SyncCollectionResourceDelegate extends
+ SyncStorageResourceDelegate {
+
+ private static final String CONTENT_TYPE_INCREMENTAL = "application/newlines";
+ private static final int FETCH_BUFFER_SIZE = 16 * 1024; // 16K chars.
+
+ SyncCollectionResourceDelegate(SyncStorageCollectionRequest request) {
+ super(request);
+ }
+
+ @Override
+ public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
+ super.addHeaders(request, client);
+ request.setHeader("Accept", CONTENT_TYPE_INCREMENTAL);
+ // Caller is responsible for setting full=1.
+ }
+
+ @Override
+ public void handleHttpResponse(HttpResponse response) {
+ if (aborting) {
+ return;
+ }
+
+ if (response.getStatusLine().getStatusCode() != 200) {
+ super.handleHttpResponse(response);
+ return;
+ }
+
+ HttpEntity entity = response.getEntity();
+ Header contentType = entity.getContentType();
+ if (!contentType.getValue().startsWith(CONTENT_TYPE_INCREMENTAL)) {
+ // Not incremental!
+ super.handleHttpResponse(response);
+ return;
+ }
+
+ // TODO: at this point we can access X-Weave-Timestamp, compare
+ // that to our local timestamp, and compute an estimate of clock
+ // skew. We can provide this to the incremental delegate, which
+ // will allow it to seamlessly correct timestamps on the records
+ // it processes. Bug 721887.
+
+ // Line-by-line processing, then invoke success.
+ SyncStorageCollectionRequestDelegate delegate = (SyncStorageCollectionRequestDelegate) this.request.delegate;
+ InputStream content = null;
+ BufferedReader br = null;
+ try {
+ content = entity.getContent();
+ br = new BufferedReader(new InputStreamReader(content), FETCH_BUFFER_SIZE);
+ String line;
+
+ // This relies on connection timeouts at the HTTP layer.
+ while (!aborting &&
+ null != (line = br.readLine())) {
+ try {
+ delegate.handleRequestProgress(line);
+ } catch (Exception ex) {
+ delegate.handleRequestError(new HandleProgressException(ex));
+ BaseResource.consumeEntity(entity);
+ return;
+ }
+ }
+ if (aborting) {
+ // So we don't hit the success case below.
+ return;
+ }
+ } catch (IOException ex) {
+ if (!aborting) {
+ delegate.handleRequestError(ex);
+ }
+ BaseResource.consumeEntity(entity);
+ return;
+ } finally {
+ // Attempt to close the stream and reader.
+ if (br != null) {
+ try {
+ br.close();
+ } catch (IOException e) {
+ // We don't care if this fails.
+ }
+ }
+ }
+ // We're done processing the entity. Don't let fetching the body succeed!
+ BaseResource.consumeEntity(entity);
+ delegate.handleRequestSuccess(new SyncStorageResponse(response));
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageCollectionRequestDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageCollectionRequestDelegate.java
new file mode 100644
index 0000000000..ddf52007be
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageCollectionRequestDelegate.java
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+public abstract class SyncStorageCollectionRequestDelegate implements
+ SyncStorageRequestIncrementalDelegate, SyncStorageRequestDelegate {
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRecordRequest.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRecordRequest.java
new file mode 100644
index 0000000000..c18c4fe157
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRecordRequest.java
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.mozilla.gecko.sync.CryptoRecord;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+/**
+ * Resource class that implements expected headers and processing for Sync.
+ * Accepts a simplified delegate.
+ *
+ * Includes:
+ * * Basic Auth headers (via Resource)
+ * * Error responses:
+ * * 401
+ * * 503
+ * * Headers:
+ * * Retry-After
+ * * X-Weave-Backoff
+ * * X-Backoff
+ * * X-Weave-Records?
+ * * ...
+ * * Timeouts
+ * * Network errors
+ * * application/newlines
+ * * JSON parsing
+ * * Content-Type and Content-Length validation.
+ */
+public class SyncStorageRecordRequest extends SyncStorageRequest {
+
+ public class SyncStorageRecordResourceDelegate extends SyncStorageResourceDelegate {
+ SyncStorageRecordResourceDelegate(SyncStorageRequest request) {
+ super(request);
+ }
+ }
+
+ public SyncStorageRecordRequest(URI uri) {
+ super(uri);
+ }
+
+ public SyncStorageRecordRequest(String url) throws URISyntaxException {
+ this(new URI(url));
+ }
+
+ @Override
+ protected BaseResourceDelegate makeResourceDelegate(SyncStorageRequest request) {
+ return new SyncStorageRecordResourceDelegate(request);
+ }
+
+ @SuppressWarnings("unchecked")
+ public void post(JSONObject body) {
+ // Let's do this the trivial way for now.
+ // Note that POSTs should be an array, so we wrap here.
+ final JSONArray toPOST = new JSONArray();
+ toPOST.add(body);
+ try {
+ this.resource.post(toPOST);
+ } catch (UnsupportedEncodingException e) {
+ this.delegate.handleRequestError(e);
+ }
+ }
+
+ public void post(JSONArray body) {
+ // Let's do this the trivial way for now.
+ try {
+ this.resource.post(body);
+ } catch (UnsupportedEncodingException e) {
+ this.delegate.handleRequestError(e);
+ }
+ }
+
+ public void put(JSONObject body) {
+ // Let's do this the trivial way for now.
+ try {
+ this.resource.put(body);
+ } catch (UnsupportedEncodingException e) {
+ this.delegate.handleRequestError(e);
+ }
+ }
+
+ public void post(CryptoRecord record) {
+ this.post(record.toJSONObject());
+ }
+
+ public void put(CryptoRecord record) {
+ this.put(record.toJSONObject());
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRequest.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRequest.java
new file mode 100644
index 0000000000..3ede9cdeda
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRequest.java
@@ -0,0 +1,204 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+import java.util.HashMap;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.SyncConstants;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+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;
+
+public class SyncStorageRequest implements Resource {
+ public static HashMap<String, String> SERVER_ERROR_MESSAGES;
+ static {
+ HashMap<String, String> errors = new HashMap<String, String>();
+
+ // Sync protocol errors.
+ errors.put("1", "Illegal method/protocol");
+ errors.put("2", "Incorrect/missing CAPTCHA");
+ errors.put("3", "Invalid/missing username");
+ errors.put("4", "Attempt to overwrite data that can't be overwritten (such as creating a user ID that already exists)");
+ errors.put("5", "User ID does not match account in path");
+ errors.put("6", "JSON parse failure");
+ errors.put("7", "Missing password field");
+ errors.put("8", "Invalid Weave Basic Object");
+ errors.put("9", "Requested password not strong enough");
+ errors.put("10", "Invalid/missing password reset code");
+ errors.put("11", "Unsupported function");
+ errors.put("12", "No email address on file");
+ errors.put("13", "Invalid collection");
+ errors.put("14", "User over quota");
+ errors.put("15", "The email does not match the username");
+ errors.put("16", "Client upgrade required");
+ errors.put("255", "An unexpected server error occurred: pool is empty.");
+
+ // Infrastructure-generated errors.
+ errors.put("\"server issue: getVS failed\"", "server issue: getVS failed");
+ errors.put("\"server issue: prefix not set\"", "server issue: prefix not set");
+ errors.put("\"server issue: host header not received from client\"", "server issue: host header not received from client");
+ errors.put("\"server issue: database lookup failed\"", "server issue: database lookup failed");
+ errors.put("\"server issue: database is not healthy\"", "server issue: database is not healthy");
+ errors.put("\"server issue: database not in pool\"", "server issue: database not in pool");
+ errors.put("\"server issue: database marked as down\"", "server issue: database marked as down");
+ SERVER_ERROR_MESSAGES = errors;
+ }
+ public static String getServerErrorMessage(String body) {
+ if (SERVER_ERROR_MESSAGES.containsKey(body)) {
+ return SERVER_ERROR_MESSAGES.get(body);
+ }
+ return body;
+ }
+
+ /**
+ * @param uri
+ * @throws URISyntaxException
+ */
+ public SyncStorageRequest(String uri) throws URISyntaxException {
+ this(new URI(uri));
+ }
+
+ /**
+ * @param uri
+ */
+ public SyncStorageRequest(URI uri) {
+ this.resource = new BaseResource(uri);
+ this.resourceDelegate = this.makeResourceDelegate(this);
+ this.resource.delegate = this.resourceDelegate;
+ }
+
+ @Override
+ public URI getURI() {
+ return this.resource.getURI();
+ }
+
+ @Override
+ public String getURIString() {
+ return this.resource.getURIString();
+ }
+
+ @Override
+ public String getHostname() {
+ return this.resource.getHostname();
+ }
+
+ /**
+ * A ResourceDelegate that mediates between Resource-level notifications and the SyncStorageRequest.
+ */
+ public class SyncStorageResourceDelegate extends BaseResourceDelegate {
+ private static final String LOG_TAG = "SSResourceDelegate";
+ protected SyncStorageRequest request;
+
+ SyncStorageResourceDelegate(SyncStorageRequest request) {
+ super(request);
+ this.request = request;
+ }
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return request.delegate.getAuthHeaderProvider();
+ }
+
+ @Override
+ public String getUserAgent() {
+ return SyncConstants.USER_AGENT;
+ }
+
+ @Override
+ public void handleHttpResponse(HttpResponse response) {
+ Logger.debug(LOG_TAG, "SyncStorageResourceDelegate handling response: " + response.getStatusLine() + ".");
+ SyncStorageRequestDelegate d = this.request.delegate;
+ SyncStorageResponse res = new SyncStorageResponse(response);
+ // It is the responsibility of the delegate handlers to completely consume the response.
+ // In context of a Sync storage response, success is either a 200 OK or 202 Accepted.
+ // 202 is returned during uploads of data in a batching mode, indicating that more is expected.
+ if (res.getStatusCode() == 200 || res.getStatusCode() == 202) {
+ d.handleRequestSuccess(res);
+ } else {
+ Logger.warn(LOG_TAG, "HTTP request failed.");
+ try {
+ Logger.warn(LOG_TAG, "HTTP response body: " + res.getErrorMessage());
+ } catch (Exception e) {
+ Logger.error(LOG_TAG, "Can't fetch HTTP response body.", e);
+ }
+ d.handleRequestFailure(res);
+ }
+ }
+
+ @Override
+ public void handleHttpProtocolException(ClientProtocolException e) {
+ this.request.delegate.handleRequestError(e);
+ }
+
+ @Override
+ public void handleHttpIOException(IOException e) {
+ this.request.delegate.handleRequestError(e);
+ }
+
+ @Override
+ public void handleTransportException(GeneralSecurityException e) {
+ this.request.delegate.handleRequestError(e);
+ }
+
+ @Override
+ public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
+ // Clients can use their delegate interface to specify X-If-Unmodified-Since.
+ String ifUnmodifiedSince = this.request.delegate.ifUnmodifiedSince();
+ if (ifUnmodifiedSince != null) {
+ Logger.debug(LOG_TAG, "Making request with X-If-Unmodified-Since = " + ifUnmodifiedSince);
+ request.setHeader("x-if-unmodified-since", ifUnmodifiedSince);
+ }
+ if (request.getMethod().equalsIgnoreCase("DELETE")) {
+ request.addHeader("x-confirm-delete", "1");
+ }
+ }
+ }
+
+ protected BaseResourceDelegate resourceDelegate;
+ public SyncStorageRequestDelegate delegate;
+ protected BaseResource resource;
+
+ public SyncStorageRequest() {
+ super();
+ }
+
+ // Default implementation. Override this.
+ protected BaseResourceDelegate makeResourceDelegate(SyncStorageRequest request) {
+ return new SyncStorageResourceDelegate(request);
+ }
+
+ @Override
+ public void get() {
+ this.resource.get();
+ }
+
+ @Override
+ public void delete() {
+ this.resource.delete();
+ }
+
+ @Override
+ public void post(HttpEntity body) {
+ this.resource.post(body);
+ }
+
+ @Override
+ public void patch(HttpEntity body) {
+ this.resource.patch(body);
+ }
+
+ @Override
+ public void put(HttpEntity body) {
+ this.resource.put(body);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRequestDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRequestDelegate.java
new file mode 100644
index 0000000000..29f42cc289
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRequestDelegate.java
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+public interface SyncStorageRequestDelegate {
+ public AuthHeaderProvider getAuthHeaderProvider();
+
+ String ifUnmodifiedSince();
+
+ // TODO: at this point we can access X-Weave-Timestamp, compare
+ // that to our local timestamp, and compute an estimate of clock
+ // skew. Bug 721887.
+
+ /**
+ * Override this to handle a successful SyncStorageRequest.
+ *
+ * SyncStorageResourceDelegate implementers <b>must</b> ensure that the HTTP
+ * responses underlying SyncStorageResponses are fully consumed to ensure that
+ * connections are returned to the pool, for example by calling
+ * <code>BaseResource.consumeEntity(response)</code>.
+ */
+ void handleRequestSuccess(SyncStorageResponse response);
+
+ /**
+ * Override this to handle a failed SyncStorageRequest.
+ *
+ *
+ * SyncStorageResourceDelegate implementers <b>must</b> ensure that the HTTP
+ * responses underlying SyncStorageResponses are fully consumed to ensure that
+ * connections are returned to the pool, for example by calling
+ * <code>BaseResource.consumeEntity(response)</code>.
+ */
+ void handleRequestFailure(SyncStorageResponse response);
+
+ void handleRequestError(Exception ex);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRequestIncrementalDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRequestIncrementalDelegate.java
new file mode 100644
index 0000000000..aa5d735bf2
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRequestIncrementalDelegate.java
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+public interface SyncStorageRequestIncrementalDelegate {
+ void handleRequestProgress(String progress); // For line-by-line.
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageResponse.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageResponse.java
new file mode 100644
index 0000000000..644df314c3
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageResponse.java
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import org.mozilla.gecko.background.common.log.Logger;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+
+public class SyncStorageResponse extends SyncResponse {
+ private static final String LOG_TAG = "SyncStorageResponse";
+
+ // Responses that are actionable get constant status codes.
+ public static final String RESPONSE_CLIENT_UPGRADE_REQUIRED = "16";
+
+ public static HashMap<String, String> SERVER_ERROR_MESSAGES;
+ static {
+ HashMap<String, String> errors = new HashMap<String, String>();
+
+ // Sync protocol errors.
+ errors.put("1", "Illegal method/protocol");
+ errors.put("2", "Incorrect/missing CAPTCHA");
+ errors.put("3", "Invalid/missing username");
+ errors.put("4", "Attempt to overwrite data that can't be overwritten (such as creating a user ID that already exists)");
+ errors.put("5", "User ID does not match account in path");
+ errors.put("6", "JSON parse failure");
+ errors.put("7", "Missing password field");
+ errors.put("8", "Invalid Weave Basic Object");
+ errors.put("9", "Requested password not strong enough");
+ errors.put("10", "Invalid/missing password reset code");
+ errors.put("11", "Unsupported function");
+ errors.put("12", "No email address on file");
+ errors.put("13", "Invalid collection");
+ errors.put("14", "User over quota");
+ errors.put("15", "The email does not match the username");
+ errors.put(RESPONSE_CLIENT_UPGRADE_REQUIRED, "Client upgrade required");
+ errors.put("255", "An unexpected server error occurred: pool is empty.");
+
+ // Infrastructure-generated errors.
+ errors.put("\"server issue: getVS failed\"", "server issue: getVS failed");
+ errors.put("\"server issue: prefix not set\"", "server issue: prefix not set");
+ errors.put("\"server issue: host header not received from client\"", "server issue: host header not received from client");
+ errors.put("\"server issue: database lookup failed\"", "server issue: database lookup failed");
+ errors.put("\"server issue: database is not healthy\"", "server issue: database is not healthy");
+ errors.put("\"server issue: database not in pool\"", "server issue: database not in pool");
+ errors.put("\"server issue: database marked as down\"", "server issue: database marked as down");
+ SERVER_ERROR_MESSAGES = errors;
+ }
+ public static String getServerErrorMessage(String body) {
+ Logger.debug(LOG_TAG, "Looking up message for body \"" + body + "\"");
+ if (SERVER_ERROR_MESSAGES.containsKey(body)) {
+ return SERVER_ERROR_MESSAGES.get(body);
+ }
+ return body;
+ }
+
+
+ public SyncStorageResponse(HttpResponse res) {
+ super(res);
+ }
+
+ public String getErrorMessage() throws IllegalStateException, IOException {
+ return SyncStorageResponse.getServerErrorMessage(this.body().trim());
+ }
+
+ /**
+ * This header gives the last-modified time of the target resource as seen during processing of
+ * the request, and will be included in all success responses (200, 201, 204).
+ * When given in response to a write request, this will be equal to the server’s current time and
+ * to the new last-modified time of any BSOs created or changed by the request.
+ */
+ public String getLastModified() {
+ if (!response.containsHeader(X_LAST_MODIFIED)) {
+ return null;
+ }
+ return response.getFirstHeader(X_LAST_MODIFIED).getValue();
+ }
+
+ // TODO: Content-Type and Content-Length validation.
+
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/TLSSocketFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/TLSSocketFactory.java
new file mode 100644
index 0000000000..dd68c0515d
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/TLSSocketFactory.java
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import java.io.IOException;
+import java.net.Socket;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+
+import org.mozilla.gecko.background.common.GlobalConstants;
+import org.mozilla.gecko.background.common.log.Logger;
+
+import ch.boye.httpclientandroidlib.conn.ssl.SSLSocketFactory;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+public class TLSSocketFactory extends SSLSocketFactory {
+ private static final String LOG_TAG = "TLSSocketFactory";
+
+ // Guarded by `this`.
+ private static String[] cipherSuites = GlobalConstants.DEFAULT_CIPHER_SUITES;
+
+ public TLSSocketFactory(SSLContext sslContext) {
+ super(sslContext);
+ }
+
+ /**
+ * Attempt to specify the cipher suites to use for a connection. If
+ * setting fails (as it will on Android 2.2, because the wrong names
+ * are in use to specify ciphers), attempt to set the defaults.
+ *
+ * We store the list of cipher suites in `cipherSuites`, which
+ * avoids this fallback handling having to be executed more than once.
+ *
+ * This method is synchronized to ensure correct use of that member.
+ *
+ * See Bug 717691 for more details.
+ *
+ * @param socket
+ * The SSLSocket on which to operate.
+ */
+ public static synchronized void setEnabledCipherSuites(SSLSocket socket) {
+ try {
+ socket.setEnabledCipherSuites(cipherSuites);
+ } catch (IllegalArgumentException e) {
+ cipherSuites = socket.getSupportedCipherSuites();
+ Logger.warn(LOG_TAG, "Setting enabled cipher suites failed: " + e.getMessage());
+ Logger.warn(LOG_TAG, "Using " + cipherSuites.length + " supported suites.");
+ socket.setEnabledCipherSuites(cipherSuites);
+ }
+ }
+
+ @Override
+ public Socket createSocket(HttpParams params) throws IOException {
+ SSLSocket socket = (SSLSocket) super.createSocket(params);
+ socket.setEnabledProtocols(GlobalConstants.DEFAULT_PROTOCOLS);
+ setEnabledCipherSuites(socket);
+ return socket;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/WBOCollectionRequestDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/WBOCollectionRequestDelegate.java
new file mode 100644
index 0000000000..2e26f041b1
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/WBOCollectionRequestDelegate.java
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.KeyBundleProvider;
+
+/**
+ * Subclass this to handle collection fetches.
+ * @author rnewman
+ *
+ */
+public abstract class WBOCollectionRequestDelegate
+extends SyncStorageCollectionRequestDelegate
+implements KeyBundleProvider {
+
+ @Override
+ public abstract KeyBundle keyBundle();
+ public abstract void handleWBO(CryptoRecord record);
+
+ @Override
+ public void handleRequestProgress(String progress) {
+ try {
+ CryptoRecord record = CryptoRecord.fromJSONRecord(progress);
+ record.keyBundle = this.keyBundle();
+ this.handleWBO(record);
+ } catch (Exception e) {
+ this.handleRequestError(e);
+ // TODO: abort?! Allow exception to propagate to fail?
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/WBORequestDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/WBORequestDelegate.java
new file mode 100644
index 0000000000..8a09e0c7f0
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/WBORequestDelegate.java
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import org.mozilla.gecko.sync.KeyBundleProvider;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+
+public abstract class WBORequestDelegate
+implements SyncStorageRequestDelegate, KeyBundleProvider {
+ @Override
+ public abstract KeyBundle keyBundle();
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/BookmarkNeedsReparentingException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/BookmarkNeedsReparentingException.java
new file mode 100644
index 0000000000..5fe3dc9fae
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/BookmarkNeedsReparentingException.java
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import org.mozilla.gecko.sync.SyncException;
+
+public class BookmarkNeedsReparentingException extends SyncException {
+
+ private static final long serialVersionUID = -7018336108709392800L;
+
+ public BookmarkNeedsReparentingException(Exception ex) {
+ super(ex);
+ }
+
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/BookmarksRepository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/BookmarksRepository.java
new file mode 100644
index 0000000000..289fc48ecf
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/BookmarksRepository.java
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+/**
+ * Shared interface for repositories that consume and produce
+ * bookmark records.
+ *
+ * @author rnewman
+ *
+ */
+public interface BookmarksRepository {
+
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ConstrainedServer11Repository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ConstrainedServer11Repository.java
new file mode 100644
index 0000000000..a6dc3f6b85
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ConstrainedServer11Repository.java
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import java.net.URISyntaxException;
+
+import org.mozilla.gecko.sync.InfoCollections;
+import org.mozilla.gecko.sync.InfoConfiguration;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+
+/**
+ * A kind of Server11Repository that supports explicit setting of total fetch limit, per-batch fetch limit, and a sort order.
+ *
+ * @author rnewman
+ *
+ */
+public class ConstrainedServer11Repository extends Server11Repository {
+
+ private final String sort;
+ private final long batchLimit;
+ private final long totalLimit;
+
+ public ConstrainedServer11Repository(String collection, String storageURL,
+ AuthHeaderProvider authHeaderProvider,
+ InfoCollections infoCollections,
+ InfoConfiguration infoConfiguration,
+ long batchLimit, long totalLimit, String sort)
+ throws URISyntaxException {
+ super(collection, storageURL, authHeaderProvider, infoCollections, infoConfiguration);
+ this.batchLimit = batchLimit;
+ this.totalLimit = totalLimit;
+ this.sort = sort;
+ }
+
+ @Override
+ public String getDefaultSort() {
+ return sort;
+ }
+
+ @Override
+ public long getDefaultBatchLimit() {
+ return batchLimit;
+ }
+
+ @Override
+ public long getDefaultTotalLimit() {
+ return totalLimit;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/FetchFailedException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/FetchFailedException.java
new file mode 100644
index 0000000000..8b29a37bae
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/FetchFailedException.java
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import org.mozilla.gecko.sync.SyncException;
+
+public class FetchFailedException extends SyncException {
+ private static final long serialVersionUID = -7533105300182522946L;
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/HashSetStoreTracker.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/HashSetStoreTracker.java
new file mode 100644
index 0000000000..3b6facc316
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/HashSetStoreTracker.java
@@ -0,0 +1,61 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import java.util.HashSet;
+import java.util.Iterator;
+
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+public class HashSetStoreTracker implements StoreTracker {
+
+ // Guarded by `this`.
+ // Used to store GUIDs that were not locally modified but
+ // have been modified by a call to `store`, and thus
+ // should not be returned by a subsequent fetch.
+ private final HashSet<String> guids;
+
+ public HashSetStoreTracker() {
+ guids = new HashSet<String>();
+ }
+
+ @Override
+ public String toString() {
+ return "#<Tracker: " + guids.size() + " guids tracked.>";
+ }
+
+ @Override
+ public synchronized boolean trackRecordForExclusion(String guid) {
+ return (guid != null) && guids.add(guid);
+ }
+
+ @Override
+ public synchronized boolean isTrackedForExclusion(String guid) {
+ return (guid != null) && guids.contains(guid);
+ }
+
+ @Override
+ public synchronized boolean untrackStoredForExclusion(String guid) {
+ return (guid != null) && guids.remove(guid);
+ }
+
+ @Override
+ public RecordFilter getFilter() {
+ if (guids.size() == 0) {
+ return null;
+ }
+ return new RecordFilter() {
+ @Override
+ public boolean excludeRecord(Record r) {
+ return isTrackedForExclusion(r.guid);
+ }
+ };
+ }
+
+ @Override
+ public Iterator<String> recordsTrackedForExclusion() {
+ return this.guids.iterator();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/HistoryRepository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/HistoryRepository.java
new file mode 100644
index 0000000000..eddc321029
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/HistoryRepository.java
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+/**
+ * Shared interface for repositories that consume and produce
+ * history records.
+ *
+ * @author rnewman
+ *
+ */
+public interface HistoryRepository {
+
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/IdentityRecordFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/IdentityRecordFactory.java
new file mode 100644
index 0000000000..acedc66e2c
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/IdentityRecordFactory.java
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+public class IdentityRecordFactory extends RecordFactory {
+
+ @Override
+ public Record createRecord(Record record) {
+ return record;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InactiveSessionException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InactiveSessionException.java
new file mode 100644
index 0000000000..185f0d724f
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InactiveSessionException.java
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import org.mozilla.gecko.sync.SyncException;
+
+public class InactiveSessionException extends SyncException {
+
+ private static final long serialVersionUID = 537241160815940991L;
+
+ public InactiveSessionException(Exception ex) {
+ super(ex);
+ }
+
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidBookmarkTypeException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidBookmarkTypeException.java
new file mode 100644
index 0000000000..3597276a45
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidBookmarkTypeException.java
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import org.mozilla.gecko.sync.SyncException;
+
+public class InvalidBookmarkTypeException extends SyncException {
+
+ private static final long serialVersionUID = -6098516814844387449L;
+
+ public InvalidBookmarkTypeException(Exception e) {
+ super(e);
+ }
+
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidRequestException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidRequestException.java
new file mode 100644
index 0000000000..3f761e5408
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidRequestException.java
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import org.mozilla.gecko.sync.SyncException;
+
+public class InvalidRequestException extends SyncException {
+
+ private static final long serialVersionUID = 4502951350743608243L;
+
+ public InvalidRequestException(Exception ex) {
+ super(ex);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidSessionTransitionException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidSessionTransitionException.java
new file mode 100644
index 0000000000..0963892c93
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidSessionTransitionException.java
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import org.mozilla.gecko.sync.SyncException;
+
+public class InvalidSessionTransitionException extends SyncException {
+
+ private static final long serialVersionUID = 4157729859314427281L;
+
+ public InvalidSessionTransitionException(Exception ex) {
+ super(ex);
+ }
+
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/MultipleRecordsForGuidException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/MultipleRecordsForGuidException.java
new file mode 100644
index 0000000000..58cca4a49c
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/MultipleRecordsForGuidException.java
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import org.mozilla.gecko.sync.SyncException;
+
+public class MultipleRecordsForGuidException extends SyncException {
+
+ private static final long serialVersionUID = 7426987323485324741L;
+
+ public MultipleRecordsForGuidException(Exception ex) {
+ super(ex);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoContentProviderException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoContentProviderException.java
new file mode 100644
index 0000000000..85d119a5d3
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoContentProviderException.java
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import org.mozilla.gecko.sync.SyncException;
+
+import android.net.Uri;
+
+/**
+ * Raised when a Content Provider cannot be retrieved.
+ *
+ * @author rnewman
+ *
+ */
+public class NoContentProviderException extends SyncException {
+ private static final long serialVersionUID = 1L;
+
+ public final Uri requestedProvider;
+ public NoContentProviderException(Uri requested) {
+ super();
+ this.requestedProvider = requested;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoGuidForIdException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoGuidForIdException.java
new file mode 100644
index 0000000000..3681deffd6
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoGuidForIdException.java
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import org.mozilla.gecko.sync.SyncException;
+
+public class NoGuidForIdException extends SyncException {
+
+ private static final long serialVersionUID = -675614284405829041L;
+
+ public NoGuidForIdException(Exception ex) {
+ super(ex);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoStoreDelegateException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoStoreDelegateException.java
new file mode 100644
index 0000000000..5747039aaa
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoStoreDelegateException.java
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import org.mozilla.gecko.sync.SyncException;
+
+public class NoStoreDelegateException extends SyncException {
+ private static final long serialVersionUID = 6631689468978422074L;
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NullCursorException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NullCursorException.java
new file mode 100644
index 0000000000..4d90579928
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NullCursorException.java
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import org.mozilla.gecko.sync.SyncException;
+
+public class NullCursorException extends SyncException {
+
+ private static final long serialVersionUID = 3146506225701104661L;
+
+ public NullCursorException(Exception e) {
+ super(e);
+ }
+
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ParentNotFoundException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ParentNotFoundException.java
new file mode 100644
index 0000000000..991fd74268
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ParentNotFoundException.java
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import org.mozilla.gecko.sync.SyncException;
+
+public class ParentNotFoundException extends SyncException {
+
+ private static final long serialVersionUID = -2687003621705922982L;
+
+ public ParentNotFoundException(Exception ex) {
+ super(ex);
+ }
+
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ProfileDatabaseException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ProfileDatabaseException.java
new file mode 100644
index 0000000000..0f8075133f
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ProfileDatabaseException.java
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import org.mozilla.gecko.sync.SyncException;
+
+public class ProfileDatabaseException extends SyncException {
+
+ private static final long serialVersionUID = -4916908502042261602L;
+
+ public ProfileDatabaseException(Exception ex) {
+ super(ex);
+ }
+
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RecordFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RecordFactory.java
new file mode 100644
index 0000000000..6a8d81a77f
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RecordFactory.java
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+// Take a record retrieved from some middleware, producing
+// some concrete record type for application to some local repository.
+public abstract class RecordFactory {
+ public abstract Record createRecord(Record record);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RecordFilter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RecordFilter.java
new file mode 100644
index 0000000000..733448ded7
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RecordFilter.java
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+public interface RecordFilter {
+ public boolean excludeRecord(Record r);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Repository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Repository.java
new file mode 100644
index 0000000000..3dd3fd2c40
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Repository.java
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCleanDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+
+import android.content.Context;
+
+public abstract class Repository {
+ public abstract void createSession(RepositorySessionCreationDelegate delegate, Context context);
+
+ public void clean(boolean success, RepositorySessionCleanDelegate delegate, Context context) {
+ delegate.onCleaned(this);
+ }
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySession.java
new file mode 100644
index 0000000000..84fca13799
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySession.java
@@ -0,0 +1,384 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+/**
+ * A <code>RepositorySession</code> is created and used thusly:
+ *
+ *<ul>
+ * <li>Construct, with a reference to its parent {@link Repository}, by calling
+ * {@link Repository#createSession(RepositorySessionCreationDelegate, android.content.Context)}.</li>
+ * <li>Populate with saved information by calling {@link #unbundle(RepositorySessionBundle)}.</li>
+ * <li>Begin a sync by calling {@link #begin(RepositorySessionBeginDelegate)}. <code>begin()</code>
+ * is an appropriate place to initialize expensive resources.</li>
+ * <li>Perform operations such as {@link #fetchSince(long, RepositorySessionFetchRecordsDelegate)} and
+ * {@link #store(Record)}.</li>
+ * <li>Finish by calling {@link #finish(RepositorySessionFinishDelegate)}, retrieving and storing
+ * the current bundle.</li>
+ *</ul>
+ *
+ * If <code>finish()</code> is not called, {@link #abort()} must be called. These calls must
+ * <em>always</em> be paired with <code>begin()</code>.
+ *
+ */
+public abstract class RepositorySession {
+
+ public enum SessionStatus {
+ UNSTARTED,
+ ACTIVE,
+ ABORTED,
+ DONE
+ }
+
+ private static final String LOG_TAG = "RepositorySession";
+
+ protected static void trace(String message) {
+ Logger.trace(LOG_TAG, message);
+ }
+
+ private SessionStatus status = SessionStatus.UNSTARTED;
+ protected Repository repository;
+ protected RepositorySessionStoreDelegate delegate;
+
+ /**
+ * A queue of Runnables which call out into delegates.
+ */
+ protected ExecutorService delegateQueue = Executors.newSingleThreadExecutor();
+
+ /**
+ * A queue of Runnables which effect storing.
+ * This includes actual store work, and also the consequences of storeDone.
+ * This provides strict ordering.
+ */
+ protected ExecutorService storeWorkQueue = Executors.newSingleThreadExecutor();
+
+ // The time that the last sync on this collection completed, in milliseconds since epoch.
+ private long lastSyncTimestamp = 0;
+
+ public long getLastSyncTimestamp() {
+ return lastSyncTimestamp;
+ }
+
+ public static long now() {
+ return System.currentTimeMillis();
+ }
+
+ public RepositorySession(Repository repository) {
+ this.repository = repository;
+ }
+
+ public abstract void guidsSince(long timestamp, RepositorySessionGuidsSinceDelegate delegate);
+ public abstract void fetchSince(long timestamp, RepositorySessionFetchRecordsDelegate delegate);
+ public abstract void fetch(String[] guids, RepositorySessionFetchRecordsDelegate delegate) throws InactiveSessionException;
+ public abstract void fetchAll(RepositorySessionFetchRecordsDelegate delegate);
+
+ /**
+ * Override this if you wish to short-circuit a sync when you know --
+ * e.g., by inspecting the database or info/collections -- that no new
+ * data are available.
+ *
+ * @return true if a sync should proceed.
+ */
+ public boolean dataAvailable() {
+ return true;
+ }
+
+ /**
+ * @return true if we cannot safely sync from this <code>RepositorySession</code>.
+ */
+ public boolean shouldSkip() {
+ return false;
+ }
+
+ /*
+ * Store operations proceed thusly:
+ *
+ * * Set a delegate
+ * * Store an arbitrary number of records. At any time the delegate can be
+ * notified of an error.
+ * * Call storeDone to notify the session that no more items are forthcoming.
+ * * The store delegate will be notified of error or completion.
+ *
+ * This arrangement of calls allows for batching at the session level.
+ *
+ * Store success calls are not guaranteed.
+ */
+ public void setStoreDelegate(RepositorySessionStoreDelegate delegate) {
+ Logger.debug(LOG_TAG, "Setting store delegate to " + delegate);
+ this.delegate = delegate;
+ }
+ public abstract void store(Record record) throws NoStoreDelegateException;
+
+ public void storeDone() {
+ // Our default behavior will be to assume that the Runnable is
+ // executed as soon as all the stores synchronously finish, so
+ // our end timestamp can just be… now.
+ storeDone(now());
+ }
+
+ public void storeDone(final long end) {
+ Logger.debug(LOG_TAG, "Scheduling onStoreCompleted for after storing is done: " + end);
+ Runnable command = new Runnable() {
+ @Override
+ public void run() {
+ delegate.onStoreCompleted(end);
+ }
+ };
+ storeWorkQueue.execute(command);
+ }
+
+ public abstract void wipe(RepositorySessionWipeDelegate delegate);
+
+ /**
+ * Synchronously perform the shared work of beginning. Throws on failure.
+ * @throws InvalidSessionTransitionException
+ *
+ */
+ protected void sharedBegin() throws InvalidSessionTransitionException {
+ Logger.debug(LOG_TAG, "Shared begin.");
+ if (delegateQueue.isShutdown()) {
+ throw new InvalidSessionTransitionException(null);
+ }
+ if (storeWorkQueue.isShutdown()) {
+ throw new InvalidSessionTransitionException(null);
+ }
+ this.transitionFrom(SessionStatus.UNSTARTED, SessionStatus.ACTIVE);
+ }
+
+ /**
+ * Start the session. This is an appropriate place to initialize
+ * data access components such as database handles.
+ *
+ * @param delegate
+ * @throws InvalidSessionTransitionException
+ */
+ public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException {
+ sharedBegin();
+ delegate.deferredBeginDelegate(delegateQueue).onBeginSucceeded(this);
+ }
+
+ public void unbundle(RepositorySessionBundle bundle) {
+ this.lastSyncTimestamp = bundle == null ? 0 : bundle.getTimestamp();
+ }
+
+ /**
+ * Override this in your subclasses to return values to save between sessions.
+ * Note that RepositorySession automatically bumps the timestamp to the time
+ * the last sync began. If unbundled but not begun, this will be the same as the
+ * value in the input bundle.
+ *
+ * The Synchronizer most likely wants to bump the bundle timestamp to be a value
+ * return from a fetch call.
+ */
+ protected RepositorySessionBundle getBundle() {
+ // Why don't we just persist the old bundle?
+ long timestamp = getLastSyncTimestamp();
+ RepositorySessionBundle bundle = new RepositorySessionBundle(timestamp);
+ Logger.debug(LOG_TAG, "Setting bundle timestamp to " + timestamp + ".");
+
+ return bundle;
+ }
+
+ /**
+ * Just like finish(), but doesn't do any work that should only be performed
+ * at the end of a successful sync, and can be called any time.
+ */
+ public void abort(RepositorySessionFinishDelegate delegate) {
+ this.abort();
+ delegate.deferredFinishDelegate(delegateQueue).onFinishSucceeded(this, this.getBundle());
+ }
+
+ /**
+ * Abnormally terminate the repository session, freeing or closing
+ * any resources that were opened during the lifetime of the session.
+ */
+ public void abort() {
+ // TODO: do something here.
+ this.setStatus(SessionStatus.ABORTED);
+ try {
+ storeWorkQueue.shutdownNow();
+ } catch (Exception e) {
+ Logger.error(LOG_TAG, "Caught exception shutting down store work queue.", e);
+ }
+ try {
+ delegateQueue.shutdown();
+ } catch (Exception e) {
+ Logger.error(LOG_TAG, "Caught exception shutting down delegate queue.", e);
+ }
+ }
+
+ /**
+ * End the repository session, freeing or closing any resources
+ * that were opened during the lifetime of the session.
+ *
+ * @param delegate notified of success or failure.
+ * @throws InactiveSessionException
+ */
+ public void finish(final RepositorySessionFinishDelegate delegate) throws InactiveSessionException {
+ try {
+ this.transitionFrom(SessionStatus.ACTIVE, SessionStatus.DONE);
+ delegate.deferredFinishDelegate(delegateQueue).onFinishSucceeded(this, this.getBundle());
+ } catch (InvalidSessionTransitionException e) {
+ Logger.error(LOG_TAG, "Tried to finish() an unstarted or already finished session");
+ throw new InactiveSessionException(e);
+ }
+
+ Logger.trace(LOG_TAG, "Shutting down work queues.");
+ storeWorkQueue.shutdown();
+ delegateQueue.shutdown();
+ }
+
+ /**
+ * Run the provided command if we're active and our delegate queue
+ * is not shut down.
+ */
+ protected synchronized void executeDelegateCommand(Runnable command)
+ throws InactiveSessionException {
+ if (!isActive() || delegateQueue.isShutdown()) {
+ throw new InactiveSessionException(null);
+ }
+ delegateQueue.execute(command);
+ }
+
+ public synchronized void ensureActive() throws InactiveSessionException {
+ if (!isActive()) {
+ throw new InactiveSessionException(null);
+ }
+ }
+
+ public synchronized boolean isActive() {
+ return status == SessionStatus.ACTIVE;
+ }
+
+ public synchronized SessionStatus getStatus() {
+ return status;
+ }
+
+ public synchronized void setStatus(SessionStatus status) {
+ this.status = status;
+ }
+
+ public synchronized void transitionFrom(SessionStatus from, SessionStatus to) throws InvalidSessionTransitionException {
+ if (from == null || this.status == from) {
+ Logger.trace(LOG_TAG, "Successfully transitioning from " + this.status + " to " + to);
+
+ this.status = to;
+ return;
+ }
+ Logger.warn(LOG_TAG, "Wanted to transition from " + from + " but in state " + this.status);
+ throw new InvalidSessionTransitionException(null);
+ }
+
+ /**
+ * Produce a record that is some combination of the remote and local records
+ * provided.
+ *
+ * The returned record must be produced without mutating either remoteRecord
+ * or localRecord. It is acceptable to return either remoteRecord or localRecord
+ * if no modifications are to be propagated.
+ *
+ * The returned record *should* have the local androidID and the remote GUID,
+ * and some optional merge of data from the two records.
+ *
+ * This method can be called with records that are identical, or differ in
+ * any regard.
+ *
+ * This method will not be called if:
+ *
+ * * either record is marked as deleted, or
+ * * there is no local mapping for a new remote record.
+ *
+ * Otherwise, it will be called precisely once.
+ *
+ * Side-effects (e.g., for transactional storage) can be hooked in here.
+ *
+ * @param remoteRecord
+ * The record retrieved from upstream, already adjusted for clock skew.
+ * @param localRecord
+ * The record retrieved from local storage.
+ * @param lastRemoteRetrieval
+ * The timestamp of the last retrieved set of remote records, adjusted for
+ * clock skew.
+ * @param lastLocalRetrieval
+ * The timestamp of the last retrieved set of local records.
+ * @return
+ * A Record instance to apply, or null to apply nothing.
+ */
+ protected Record reconcileRecords(final Record remoteRecord,
+ final Record localRecord,
+ final long lastRemoteRetrieval,
+ final long lastLocalRetrieval) {
+ Logger.debug(LOG_TAG, "Reconciling remote " + remoteRecord.guid + " against local " + localRecord.guid);
+
+ if (localRecord.equalPayloads(remoteRecord)) {
+ if (remoteRecord.lastModified > localRecord.lastModified) {
+ Logger.debug(LOG_TAG, "Records are equal. No record application needed.");
+ return null;
+ }
+
+ // Local wins.
+ return null;
+ }
+
+ // TODO: Decide what to do based on:
+ // * Which of the two records is modified;
+ // * Whether they are equal or congruent;
+ // * The modified times of each record (interpreted through the lens of clock skew);
+ // * ...
+ boolean localIsMoreRecent = localRecord.lastModified > remoteRecord.lastModified;
+ Logger.debug(LOG_TAG, "Local record is more recent? " + localIsMoreRecent);
+ Record donor = localIsMoreRecent ? localRecord : remoteRecord;
+
+ // Modify the local record to match the remote record's GUID and values.
+ // Preserve the local Android ID, and merge data where possible.
+ // It sure would be nice if copyWithIDs didn't give a shit about androidID, mm?
+ Record out = donor.copyWithIDs(remoteRecord.guid, localRecord.androidID);
+
+ // We don't want to upload the record if the remote record was
+ // applied without changes.
+ // This logic will become more complicated as reconciling becomes smarter.
+ if (!localIsMoreRecent) {
+ trackGUID(out.guid);
+ }
+ return out;
+ }
+
+ /**
+ * Depending on the RepositorySession implementation, track
+ * that a record — most likely a brand-new record that has been
+ * applied unmodified — should be tracked so as to not be uploaded
+ * redundantly.
+ *
+ * The default implementations do nothing.
+ */
+ protected void trackGUID(String guid) {
+ }
+
+ protected synchronized void untrackGUIDs(Collection<String> guids) {
+ }
+
+ protected void untrackGUID(String guid) {
+ }
+
+ // Ah, Java. You wretched creature.
+ public Iterator<String> getTrackedRecordIDs() {
+ return new ArrayList<String>().iterator();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySessionBundle.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySessionBundle.java
new file mode 100644
index 0000000000..7908ec7971
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySessionBundle.java
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+
+import java.io.IOException;
+
+public class RepositorySessionBundle {
+ public static final String LOG_TAG = RepositorySessionBundle.class.getSimpleName();
+
+ protected static final String JSON_KEY_TIMESTAMP = "timestamp";
+
+ protected final ExtendedJSONObject object;
+
+ public RepositorySessionBundle(String jsonString) throws IOException, NonObjectJSONException {
+
+ object = new ExtendedJSONObject(jsonString);
+ }
+
+ public RepositorySessionBundle(long lastSyncTimestamp) {
+ object = new ExtendedJSONObject();
+ this.setTimestamp(lastSyncTimestamp);
+ }
+
+ public long getTimestamp() {
+ if (object.containsKey(JSON_KEY_TIMESTAMP)) {
+ return object.getLong(JSON_KEY_TIMESTAMP);
+ }
+
+ return -1;
+ }
+
+ public void setTimestamp(long timestamp) {
+ Logger.debug(LOG_TAG, "Setting timestamp to " + timestamp + ".");
+ object.put(JSON_KEY_TIMESTAMP, timestamp);
+ }
+
+ public void bumpTimestamp(long timestamp) {
+ long existing = this.getTimestamp();
+ if (timestamp > existing) {
+ this.setTimestamp(timestamp);
+ } else {
+ Logger.debug(LOG_TAG, "Timestamp " + timestamp + " not greater than " + existing + "; not bumping.");
+ }
+ }
+
+ public String toJSONString() {
+ return object.toJSONString();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11Repository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11Repository.java
new file mode 100644
index 0000000000..4404fda256
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11Repository.java
@@ -0,0 +1,144 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+
+import org.mozilla.gecko.sync.InfoCollections;
+import org.mozilla.gecko.sync.InfoConfiguration;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+/**
+ * A Server11Repository implements fetching and storing against the Sync 1.1 API.
+ * It doesn't do crypto: that's the job of the middleware.
+ *
+ * @author rnewman
+ */
+public class Server11Repository extends Repository {
+ protected String collection;
+ protected URI collectionURI;
+ protected final AuthHeaderProvider authHeaderProvider;
+ protected final InfoCollections infoCollections;
+
+ private final InfoConfiguration infoConfiguration;
+
+ /**
+ * Construct a new repository that fetches and stores against the Sync 1.1. API.
+ *
+ * @param collection name.
+ * @param storageURL full URL to storage endpoint.
+ * @param authHeaderProvider to use in requests; may be null.
+ * @param infoCollections instance; must not be null.
+ * @throws URISyntaxException
+ */
+ public Server11Repository(@NonNull String collection, @NonNull String storageURL, AuthHeaderProvider authHeaderProvider, @NonNull InfoCollections infoCollections, @NonNull InfoConfiguration infoConfiguration) throws URISyntaxException {
+ if (collection == null) {
+ throw new IllegalArgumentException("collection must not be null");
+ }
+ if (storageURL == null) {
+ throw new IllegalArgumentException("storageURL must not be null");
+ }
+ if (infoCollections == null) {
+ throw new IllegalArgumentException("infoCollections must not be null");
+ }
+ this.collection = collection;
+ this.collectionURI = new URI(storageURL + (storageURL.endsWith("/") ? collection : "/" + collection));
+ this.authHeaderProvider = authHeaderProvider;
+ this.infoCollections = infoCollections;
+ this.infoConfiguration = infoConfiguration;
+ }
+
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ delegate.onSessionCreated(new Server11RepositorySession(this));
+ }
+
+ public URI collectionURI() {
+ return this.collectionURI;
+ }
+
+ public URI collectionURI(boolean full, long newer, long limit, String sort, String ids, String offset) throws URISyntaxException {
+ ArrayList<String> params = new ArrayList<String>();
+ if (full) {
+ params.add("full=1");
+ }
+ if (newer >= 0) {
+ // Translate local millisecond timestamps into server decimal seconds.
+ String newerString = Utils.millisecondsToDecimalSecondsString(newer);
+ params.add("newer=" + newerString);
+ }
+ if (limit > 0) {
+ params.add("limit=" + limit);
+ }
+ if (sort != null) {
+ params.add("sort=" + sort); // We trust these values.
+ }
+ if (ids != null) {
+ params.add("ids=" + ids); // We trust these values.
+ }
+ if (offset != null) {
+ // Offset comes straight out of HTTP headers and it is the responsibility of the caller to URI-escape it.
+ params.add("offset=" + offset);
+ }
+ if (params.size() == 0) {
+ return this.collectionURI;
+ }
+
+ StringBuilder out = new StringBuilder();
+ char indicator = '?';
+ for (String param : params) {
+ out.append(indicator);
+ indicator = '&';
+ out.append(param);
+ }
+ String uri = this.collectionURI + out.toString();
+ return new URI(uri);
+ }
+
+ public URI wboURI(String id) throws URISyntaxException {
+ return new URI(this.collectionURI + "/" + id);
+ }
+
+ // Override these.
+ @SuppressWarnings("static-method")
+ public long getDefaultBatchLimit() {
+ return -1;
+ }
+
+ @SuppressWarnings("static-method")
+ public String getDefaultSort() {
+ return null;
+ }
+
+ public long getDefaultTotalLimit() {
+ return -1;
+ }
+
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return authHeaderProvider;
+ }
+
+ public boolean updateNeeded(long lastSyncTimestamp) {
+ return infoCollections.updateNeeded(collection, lastSyncTimestamp);
+ }
+
+ @Nullable
+ public Long getCollectionLastModified() {
+ return infoCollections.getTimestamp(collection);
+ }
+
+ public InfoConfiguration getInfoConfiguration() {
+ return infoConfiguration;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11RepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11RepositorySession.java
new file mode 100644
index 0000000000..20c735a6b0
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11RepositorySession.java
@@ -0,0 +1,104 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+import org.mozilla.gecko.sync.repositories.downloaders.BatchingDownloader;
+import org.mozilla.gecko.sync.repositories.uploaders.BatchingUploader;
+
+public class Server11RepositorySession extends RepositorySession {
+ public static final String LOG_TAG = "Server11Session";
+
+ Server11Repository serverRepository;
+ private BatchingUploader uploader;
+ private final BatchingDownloader downloader;
+
+ public Server11RepositorySession(Repository repository) {
+ super(repository);
+ serverRepository = (Server11Repository) repository;
+ this.downloader = new BatchingDownloader(serverRepository, this);
+ }
+
+ public Server11Repository getServerRepository() {
+ return serverRepository;
+ }
+
+ @Override
+ public void setStoreDelegate(RepositorySessionStoreDelegate delegate) {
+ this.delegate = delegate;
+
+ // Now that we have the delegate, we can initialize our uploader.
+ this.uploader = new BatchingUploader(this, storeWorkQueue, delegate);
+ }
+
+ @Override
+ public void guidsSince(long timestamp,
+ RepositorySessionGuidsSinceDelegate delegate) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void fetchSince(long timestamp,
+ RepositorySessionFetchRecordsDelegate delegate) {
+ this.downloader.fetchSince(timestamp, delegate);
+ }
+
+ @Override
+ public void fetchAll(RepositorySessionFetchRecordsDelegate delegate) {
+ this.fetchSince(-1, delegate);
+ }
+
+ @Override
+ public void fetch(String[] guids,
+ RepositorySessionFetchRecordsDelegate delegate) {
+ this.downloader.fetch(guids, delegate);
+ }
+
+ @Override
+ public void wipe(RepositorySessionWipeDelegate delegate) {
+ if (!isActive()) {
+ delegate.onWipeFailed(new InactiveSessionException(null));
+ return;
+ }
+ // TODO: implement wipe.
+ }
+
+ @Override
+ public void store(Record record) throws NoStoreDelegateException {
+ if (delegate == null) {
+ throw new NoStoreDelegateException();
+ }
+
+ // If delegate was set, this shouldn't happen.
+ if (uploader == null) {
+ throw new IllegalStateException("Uploader haven't been initialized");
+ }
+
+ uploader.process(record);
+ }
+
+ @Override
+ public void storeDone() {
+ Logger.debug(LOG_TAG, "storeDone().");
+
+ // If delegate was set, this shouldn't happen.
+ if (uploader == null) {
+ throw new IllegalStateException("Uploader haven't been initialized");
+ }
+
+ uploader.noMoreRecordsToUpload();
+ }
+
+ @Override
+ public boolean dataAvailable() {
+ return serverRepository.updateNeeded(getLastSyncTimestamp());
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreFailedException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreFailedException.java
new file mode 100644
index 0000000000..fcb09e32e1
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreFailedException.java
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import org.mozilla.gecko.sync.SyncException;
+
+public class StoreFailedException extends SyncException {
+ private static final long serialVersionUID = 6080340122855859752L;
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreTracker.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreTracker.java
new file mode 100644
index 0000000000..b6a3071a9d
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreTracker.java
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import java.util.Iterator;
+
+/**
+ * Our hacky version of transactional semantics. The goal is to prevent
+ * the following situation:
+ *
+ * * AAA is not modified locally.
+ * * A modified AAA is downloaded during the storing phase. Its local
+ * timestamp is advanced.
+ * * The direction of syncing changes, and AAA is now uploaded to the server.
+ *
+ * The following situation should still be supported:
+ *
+ * * AAA is not modified locally.
+ * * A modified AAA is downloaded and merged with the local AAA.
+ * * The merged AAA is uploaded to the server.
+ *
+ * As should:
+ *
+ * * AAA is modified locally.
+ * * A modified AAA is downloaded, and discarded or merged.
+ * * The current version of AAA is uploaded to the server.
+ *
+ * We achieve this by tracking GUIDs during the storing phase. If we
+ * apply a record such that the local copy is substantially the same
+ * as the record we just downloaded, we add it to a list of records
+ * to avoid uploading. The definition of "substantially the same"
+ * depends on the particular repository. The only consideration is "do we
+ * want to upload this record in this sync?".
+ *
+ * Note that items are removed from this list when a fetch that
+ * considers them for upload completes successfully. The entire list
+ * is discarded when the session is completed.
+ *
+ * This interface exposes methods to:
+ *
+ * * During a store, recording that a record has been stored, and should
+ * thus not be returned in subsequent fetches;
+ * * During a fetch, checking whether a record should be returned.
+ *
+ * In the future this might also grow self-persistence.
+ *
+ * See also RepositorySession.trackRecord.
+ *
+ * @author rnewman
+ *
+ */
+public interface StoreTracker {
+
+ /**
+ * @param guid
+ * The GUID of the item to track.
+ * @return
+ * Whether the GUID was a newly tracked value.
+ */
+ public boolean trackRecordForExclusion(String guid);
+
+ /**
+ * @param guid
+ * The GUID of the item to check.
+ * @return
+ * true if the item is already tracked.
+ */
+ public boolean isTrackedForExclusion(String guid);
+
+ /**
+ *
+ * @param guid
+ * @return true if the specified GUID was removed from the tracked set.
+ */
+ public boolean untrackStoredForExclusion(String guid);
+
+ public RecordFilter getFilter();
+
+ public Iterator<String> recordsTrackedForExclusion();
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreTrackingRepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreTrackingRepositorySession.java
new file mode 100644
index 0000000000..1a5c1e96a6
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreTrackingRepositorySession.java
@@ -0,0 +1,102 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+public abstract class StoreTrackingRepositorySession extends RepositorySession {
+ private static final String LOG_TAG = "StoreTrackSession";
+ protected StoreTracker storeTracker;
+
+ protected static StoreTracker createStoreTracker() {
+ return new HashSetStoreTracker();
+ }
+
+ public StoreTrackingRepositorySession(Repository repository) {
+ super(repository);
+ }
+
+ @Override
+ public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException {
+ RepositorySessionBeginDelegate deferredDelegate = delegate.deferredBeginDelegate(delegateQueue);
+ try {
+ super.sharedBegin();
+ } catch (InvalidSessionTransitionException e) {
+ deferredDelegate.onBeginFailed(e);
+ return;
+ }
+ // Or do this in your own subclass.
+ storeTracker = createStoreTracker();
+ deferredDelegate.onBeginSucceeded(this);
+ }
+
+ @Override
+ protected synchronized void trackGUID(String guid) {
+ if (this.storeTracker == null) {
+ throw new IllegalStateException("Store tracker not yet initialized!");
+ }
+ this.storeTracker.trackRecordForExclusion(guid);
+ }
+
+ @Override
+ protected synchronized void untrackGUID(String guid) {
+ if (this.storeTracker == null) {
+ throw new IllegalStateException("Store tracker not yet initialized!");
+ }
+ this.storeTracker.untrackStoredForExclusion(guid);
+ }
+
+ @Override
+ protected synchronized void untrackGUIDs(Collection<String> guids) {
+ if (this.storeTracker == null) {
+ throw new IllegalStateException("Store tracker not yet initialized!");
+ }
+ if (guids == null) {
+ return;
+ }
+ for (String guid : guids) {
+ this.storeTracker.untrackStoredForExclusion(guid);
+ }
+ }
+
+ protected void trackRecord(Record record) {
+
+ Logger.debug(LOG_TAG, "Tracking record " + record.guid +
+ " (" + record.lastModified + ") to avoid re-upload.");
+ // Future: we care about the timestamp…
+ trackGUID(record.guid);
+ }
+
+ protected void untrackRecord(Record record) {
+ Logger.debug(LOG_TAG, "Un-tracking record " + record.guid + ".");
+ untrackGUID(record.guid);
+ }
+
+ @Override
+ public Iterator<String> getTrackedRecordIDs() {
+ if (this.storeTracker == null) {
+ throw new IllegalStateException("Store tracker not yet initialized!");
+ }
+ return this.storeTracker.recordsTrackedForExclusion();
+ }
+
+ @Override
+ public void abort(RepositorySessionFinishDelegate delegate) {
+ this.storeTracker = null;
+ super.abort(delegate);
+ }
+
+ @Override
+ public void finish(RepositorySessionFinishDelegate delegate) throws InactiveSessionException {
+ super.finish(delegate);
+ this.storeTracker = null;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksDataAccessor.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksDataAccessor.java
new file mode 100644
index 0000000000..fd3c35da01
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksDataAccessor.java
@@ -0,0 +1,326 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.android;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.json.simple.JSONArray;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class AndroidBrowserBookmarksDataAccessor extends AndroidBrowserRepositoryDataAccessor {
+
+ private static final String LOG_TAG = "BookmarksDataAccessor";
+
+ /*
+ * Fragments of SQL to make our lives easier.
+ */
+ private static final String BOOKMARK_IS_FOLDER = BrowserContract.Bookmarks.TYPE + " = " +
+ BrowserContract.Bookmarks.TYPE_FOLDER;
+
+ // SQL fragment to retrieve GUIDs whose ID mappings should be tracked by this session.
+ // Exclude folders we don't want to sync.
+ private static final String GUID_SHOULD_TRACK = BrowserContract.SyncColumns.GUID + " NOT IN ('" +
+ BrowserContract.Bookmarks.TAGS_FOLDER_GUID + "', '" +
+ BrowserContract.Bookmarks.PLACES_FOLDER_GUID + "', '" +
+ BrowserContract.Bookmarks.PINNED_FOLDER_GUID + "')";
+
+ private static final String EXCLUDE_SPECIAL_GUIDS_WHERE_CLAUSE;
+ static {
+ if (AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS.length > 0) {
+ StringBuilder b = new StringBuilder(BrowserContract.SyncColumns.GUID + " NOT IN (");
+
+ int remaining = AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS.length - 1;
+ for (String specialGuid : AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS) {
+ b.append('"');
+ b.append(specialGuid);
+ b.append('"');
+ if (remaining-- > 0) {
+ b.append(", ");
+ }
+ }
+ b.append(')');
+ EXCLUDE_SPECIAL_GUIDS_WHERE_CLAUSE = b.toString();
+ } else {
+ EXCLUDE_SPECIAL_GUIDS_WHERE_CLAUSE = null; // null is a valid WHERE clause.
+ }
+ }
+
+ public static final String TYPE_FOLDER = "folder";
+ public static final String TYPE_BOOKMARK = "bookmark";
+
+ private final RepoUtils.QueryHelper queryHelper;
+
+ public AndroidBrowserBookmarksDataAccessor(Context context) {
+ super(context);
+ this.queryHelper = new RepoUtils.QueryHelper(context, getUri(), LOG_TAG);
+ }
+
+ @Override
+ protected Uri getUri() {
+ return BrowserContractHelpers.BOOKMARKS_CONTENT_URI;
+ }
+
+ protected static Uri getPositionsUri() {
+ return BrowserContractHelpers.BOOKMARKS_POSITIONS_CONTENT_URI;
+ }
+
+ @Override
+ public void wipe() {
+ Uri uri = getUri();
+ Logger.info(LOG_TAG, "wiping (except for special guids): " + uri);
+ context.getContentResolver().delete(uri, EXCLUDE_SPECIAL_GUIDS_WHERE_CLAUSE, null);
+ }
+
+ private final String[] GUID_AND_ID = new String[] { BrowserContract.Bookmarks.GUID,
+ BrowserContract.Bookmarks._ID };
+
+ protected Cursor getGuidsIDsForFolders() throws NullCursorException {
+ // Exclude items that we don't want to sync (pinned items, reading list,
+ // tags, the places root), in case they've ended up in the DB.
+ String where = BOOKMARK_IS_FOLDER + " AND " + GUID_SHOULD_TRACK;
+ return queryHelper.safeQuery(".getGuidsIDsForFolders", GUID_AND_ID, where, null, null);
+ }
+
+ /**
+ * Issue a request to the Content Provider to update the positions of the
+ * records named by the provided GUIDs to the index of their GUID in the
+ * provided array.
+ *
+ * @param childArray
+ * A sequence of GUID strings.
+ */
+ public int updatePositions(ArrayList<String> childArray) {
+ final int size = childArray.size();
+ if (size == 0) {
+ return 0;
+ }
+
+ Logger.debug(LOG_TAG, "Updating positions for " + size + " items.");
+ String[] args = childArray.toArray(new String[size]);
+ return context.getContentResolver().update(getPositionsUri(), new ContentValues(), null, args);
+ }
+
+ public int bumpModifiedByGUID(Collection<String> ids, long modified) {
+ final int size = ids.size();
+ if (size == 0) {
+ return 0;
+ }
+
+ Logger.debug(LOG_TAG, "Bumping modified for " + size + " items to " + modified);
+ String where = RepoUtils.computeSQLInClause(size, BrowserContract.Bookmarks.GUID);
+ String[] selectionArgs = ids.toArray(new String[size]);
+ ContentValues values = new ContentValues();
+ values.put(BrowserContract.Bookmarks.DATE_MODIFIED, modified);
+
+ return context.getContentResolver().update(getUri(), values, where, selectionArgs);
+ }
+
+ /**
+ * Bump the modified time of a record by ID.
+ */
+ public int bumpModified(long id, long modified) {
+ Logger.debug(LOG_TAG, "Bumping modified for " + id + " to " + modified);
+ String where = BrowserContract.Bookmarks._ID + " = ?";
+ String[] selectionArgs = new String[] { String.valueOf(id) };
+ ContentValues values = new ContentValues();
+ values.put(BrowserContract.Bookmarks.DATE_MODIFIED, modified);
+
+ return context.getContentResolver().update(getUri(), values, where, selectionArgs);
+ }
+
+ protected void updateParentAndPosition(String guid, long newParentId, long position) {
+ ContentValues cv = new ContentValues();
+ cv.put(BrowserContract.Bookmarks.PARENT, newParentId);
+ if (position >= 0) {
+ cv.put(BrowserContract.Bookmarks.POSITION, position);
+ }
+ updateByGuid(guid, cv);
+ }
+
+ protected Map<String, Long> idsForGUIDs(String[] guids) throws NullCursorException {
+ final String where = RepoUtils.computeSQLInClause(guids.length, BrowserContract.Bookmarks.GUID);
+ Cursor c = queryHelper.safeQuery(".idsForGUIDs", GUID_AND_ID, where, guids, null);
+ try {
+ HashMap<String, Long> out = new HashMap<String, Long>();
+ if (!c.moveToFirst()) {
+ return out;
+ }
+ final int guidIndex = c.getColumnIndexOrThrow(BrowserContract.Bookmarks.GUID);
+ final int idIndex = c.getColumnIndexOrThrow(BrowserContract.Bookmarks._ID);
+ while (!c.isAfterLast()) {
+ out.put(c.getString(guidIndex), c.getLong(idIndex));
+ c.moveToNext();
+ }
+ return out;
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Move the children of each source folder to the destination folder.
+ * Bump the modified time of each child.
+ * The caller should bump the modified time of the destination if desired.
+ *
+ * @param fromIDs the Android IDs of the source folders.
+ * @param to the Android ID of the destination folder.
+ * @return the number of updated rows.
+ */
+ protected int moveChildren(String[] fromIDs, long to) {
+ long now = System.currentTimeMillis();
+ long pos = -1;
+
+ ContentValues cv = new ContentValues();
+ cv.put(BrowserContract.Bookmarks.PARENT, to);
+ cv.put(BrowserContract.Bookmarks.DATE_MODIFIED, now);
+ cv.put(BrowserContract.Bookmarks.POSITION, pos);
+
+ final String where = RepoUtils.computeSQLInClause(fromIDs.length, BrowserContract.Bookmarks.PARENT);
+ return context.getContentResolver().update(getUri(), cv, where, fromIDs);
+ }
+
+ /*
+ * Verify that all special GUIDs are present and that they aren't marked as deleted.
+ * Insert them if they aren't there.
+ */
+ public void checkAndBuildSpecialGuids() throws NullCursorException {
+ final String[] specialGUIDs = AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS;
+ Cursor cur = fetch(specialGUIDs);
+ long placesRoot = 0;
+
+ // Map from GUID to whether deleted. Non-presence implies just that.
+ HashMap<String, Boolean> statuses = new HashMap<String, Boolean>(specialGUIDs.length);
+ try {
+ if (cur.moveToFirst()) {
+ while (!cur.isAfterLast()) {
+ String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
+ if ("places".equals(guid)) {
+ placesRoot = RepoUtils.getLongFromCursor(cur, BrowserContract.CommonColumns._ID);
+ }
+ // Make sure none of these folders are marked as deleted.
+ boolean deleted = RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1;
+ statuses.put(guid, deleted);
+ cur.moveToNext();
+ }
+ }
+ } finally {
+ cur.close();
+ }
+
+ // Insert or undelete them if missing.
+ for (String guid : specialGUIDs) {
+ if (statuses.containsKey(guid)) {
+ if (statuses.get(guid)) {
+ // Undelete.
+ Logger.info(LOG_TAG, "Undeleting special GUID " + guid);
+ ContentValues cv = new ContentValues();
+ cv.put(BrowserContract.SyncColumns.IS_DELETED, 0);
+ updateByGuid(guid, cv);
+ }
+ } else {
+ // Insert.
+ if (guid.equals("places")) {
+ // This is awkward.
+ Logger.info(LOG_TAG, "No places root. Inserting one.");
+ placesRoot = insertSpecialFolder("places", 0);
+ } else if (guid.equals("mobile")) {
+ Logger.info(LOG_TAG, "No mobile folder. Inserting one under the places root.");
+ insertSpecialFolder("mobile", placesRoot);
+ } else {
+ // unfiled, menu, toolbar.
+ Logger.info(LOG_TAG, "No " + guid + " root. Inserting one under places (" + placesRoot + ").");
+ insertSpecialFolder(guid, placesRoot);
+ }
+ }
+ }
+ }
+
+ private long insertSpecialFolder(String guid, long parentId) {
+ BookmarkRecord record = new BookmarkRecord(guid);
+ record.title = AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS_MAP.get(guid);
+ record.type = "folder";
+ record.androidParentID = parentId;
+ return ContentUris.parseId(insert(record));
+ }
+
+ @Override
+ protected ContentValues getContentValues(Record record) {
+ BookmarkRecord rec = (BookmarkRecord) record;
+
+ if (rec.deleted) {
+ ContentValues cv = new ContentValues();
+ cv.put(BrowserContract.SyncColumns.GUID, rec.guid);
+ cv.put(BrowserContract.Bookmarks.IS_DELETED, 1);
+ return cv;
+ }
+
+ final int recordType = BrowserContractHelpers.typeCodeForString(rec.type);
+ if (recordType == -1) {
+ throw new IllegalStateException("Unexpected record type " + rec.type);
+ }
+
+ ContentValues cv = new ContentValues();
+ cv.put(BrowserContract.SyncColumns.GUID, rec.guid);
+ cv.put(BrowserContract.Bookmarks.TYPE, recordType);
+ cv.put(BrowserContract.Bookmarks.TITLE, rec.title);
+ cv.put(BrowserContract.Bookmarks.URL, rec.bookmarkURI);
+ cv.put(BrowserContract.Bookmarks.DESCRIPTION, rec.description);
+ if (rec.tags == null) {
+ rec.tags = new JSONArray();
+ }
+ cv.put(BrowserContract.Bookmarks.TAGS, rec.tags.toJSONString());
+ cv.put(BrowserContract.Bookmarks.KEYWORD, rec.keyword);
+ cv.put(BrowserContract.Bookmarks.PARENT, rec.androidParentID);
+ cv.put(BrowserContract.Bookmarks.POSITION, rec.androidPosition);
+
+ // Note that we don't set the modified timestamp: we allow the
+ // content provider to do that for us.
+ return cv;
+ }
+
+ /**
+ * Returns a cursor over non-deleted records that list the given androidID as a parent.
+ */
+ public Cursor getChildren(long androidID) throws NullCursorException {
+ return getChildren(androidID, false);
+ }
+
+ /**
+ * Returns a cursor with any records that list the given androidID as a parent.
+ * Excludes 'places', and optionally any deleted records.
+ */
+ public Cursor getChildren(long androidID, boolean includeDeleted) throws NullCursorException {
+ final String where = BrowserContract.Bookmarks.PARENT + " = ? AND " +
+ BrowserContract.SyncColumns.GUID + " <> ? " +
+ (!includeDeleted ? ("AND " + BrowserContract.SyncColumns.IS_DELETED + " = 0") : "");
+
+ final String[] args = new String[] { String.valueOf(androidID), "places" };
+
+ // Order by position, falling back on creation date and ID.
+ final String order = BrowserContract.Bookmarks.POSITION + ", " +
+ BrowserContract.SyncColumns.DATE_CREATED + ", " +
+ BrowserContract.Bookmarks._ID;
+ return queryHelper.safeQuery(".getChildren", getAllColumns(), where, args, order);
+ }
+
+
+ @Override
+ protected String[] getAllColumns() {
+ return BrowserContractHelpers.BookmarkColumns;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksRepository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksRepository.java
new file mode 100644
index 0000000000..38520fd7aa
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksRepository.java
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.android;
+
+import org.mozilla.gecko.sync.repositories.BookmarksRepository;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+
+import android.content.Context;
+
+public class AndroidBrowserBookmarksRepository extends AndroidBrowserRepository implements BookmarksRepository {
+
+ @Override
+ protected void sessionCreator(RepositorySessionCreationDelegate delegate, Context context) {
+ AndroidBrowserBookmarksRepositorySession session = new AndroidBrowserBookmarksRepositorySession(AndroidBrowserBookmarksRepository.this, context);
+ final RepositorySessionCreationDelegate deferredCreationDelegate = delegate.deferredCreationDelegate();
+ deferredCreationDelegate.onSessionCreated(session);
+ }
+
+ @Override
+ protected AndroidBrowserRepositoryDataAccessor getDataAccessor(Context context) {
+ return new AndroidBrowserBookmarksDataAccessor(context);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksRepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksRepositorySession.java
new file mode 100644
index 0000000000..fb79901a14
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksRepositorySession.java
@@ -0,0 +1,1107 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.android;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+import org.json.simple.JSONArray;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.repositories.InactiveSessionException;
+import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
+import org.mozilla.gecko.sync.repositories.NoGuidForIdException;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.ParentNotFoundException;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepositorySession
+ implements BookmarksInsertionManager.BookmarkInserter {
+
+ public static final int DEFAULT_DELETION_FLUSH_THRESHOLD = 50;
+ public static final int DEFAULT_INSERTION_FLUSH_THRESHOLD = 50;
+
+ // TODO: synchronization for these.
+ private final HashMap<String, Long> parentGuidToIDMap = new HashMap<String, Long>();
+ private final HashMap<Long, String> parentIDToGuidMap = new HashMap<Long, String>();
+
+ /**
+ * Some notes on reparenting/reordering.
+ *
+ * Fennec stores new items with a high-negative position, because it doesn't care.
+ * On the other hand, it also doesn't give us any help managing positions.
+ *
+ * We can process records and folders in any order, though we'll usually see folders
+ * first because their sortindex is larger.
+ *
+ * We can also see folders that refer to children we haven't seen, and children we
+ * won't see (perhaps due to a TTL, perhaps due to a limit on our fetch).
+ *
+ * And of course folders can refer to local children (including ones that might
+ * be reconciled into oblivion!), or local children in other folders. And the local
+ * version of a folder -- which might be a reconciling target, or might not -- can
+ * have local additions or removals. (That causes complications with on-the-fly
+ * reordering: we don't know in advance which records will even exist by the end
+ * of the sync.)
+ *
+ * We opt to leave records in a reasonable state as we go, applying reordering/
+ * reparenting operations whenever possible. A final sequence is applied after all
+ * incoming records have been handled.
+ *
+ * As such, we need to track a bunch of stuff as we go:
+ *
+ * • For each downloaded folder, the array of children. These will be server GUIDs,
+ * but not necessarily identical to the remote list: if we download a record and
+ * it's been locally moved, it must be removed from this child array.
+ *
+ * This mapping can be discarded when final reordering has occurred, either on
+ * store completion or when every child has been seen within this session.
+ *
+ * • A list of orphans: records whose parent folder does not yet exist. This can be
+ * trimmed as orphans are reparented.
+ *
+ * • Mappings from folder GUIDs to folder IDs, so that we can parent items without
+ * having to look in the DB. Of course, this must be kept up-to-date as we
+ * reconcile.
+ *
+ * Reordering also needs to occur during fetch. That is, a folder might have been
+ * created locally, or modified locally without any remote changes. An order must
+ * be generated for the folder's children array, and it must be persisted into the
+ * database to act as a starting point for future changes. But of course we don't
+ * want to incur a database write if the children already have a satisfactory order.
+ *
+ * Do we also need a list of "adopters", parents that are still waiting for children?
+ * As items get picked out of the orphans list, we can do on-the-fly ordering, until
+ * we're left with lonely records at the end.
+ *
+ * As we modify local folders, perhaps by moving children out of their purview, we
+ * must bump their modification time so as to cause them to be uploaded on the next
+ * stage of syncing. The same applies to simple reordering.
+ */
+
+ // TODO: can we guarantee serial access to these?
+ private final HashMap<String, ArrayList<String>> missingParentToChildren = new HashMap<String, ArrayList<String>>();
+ private final HashMap<String, JSONArray> parentToChildArray = new HashMap<String, JSONArray>();
+ private int needsReparenting = 0;
+
+ private final AndroidBrowserBookmarksDataAccessor dataAccessor;
+
+ protected BookmarksDeletionManager deletionManager;
+ protected BookmarksInsertionManager insertionManager;
+
+ /**
+ * An array of known-special GUIDs.
+ */
+ public static final String[] SPECIAL_GUIDS = new String[] {
+ // Mobile and desktop places roots have to come first.
+ "places",
+ "mobile",
+ "toolbar",
+ "menu",
+ "unfiled"
+ };
+
+ /**
+ * = A note about folder mapping =
+ *
+ * Note that _none_ of Places's folders actually have a special GUID. They're all
+ * randomly generated. Special folders are indicated by membership in the
+ * moz_bookmarks_roots table, and by having the parent `1`.
+ *
+ * Additionally, the mobile root is annotated. In Firefox Sync, PlacesUtils is
+ * used to find the IDs of these special folders.
+ *
+ * We need to consume records with these various GUIDs, producing a local
+ * representation which we are able to stably map upstream.
+ *
+ * Android Sync skips over the contents of some special GUIDs -- `places`, `tags`,
+ * etc. -- when finding IDs.
+ * Some of these special GUIDs are part of desktop structure (places, tags). Some
+ * are part of Fennec's custom data (readinglist, pinned).
+ *
+ * We don't want to upload or apply these records.
+ *
+ * That is:
+ *
+ * * We should not upload a `places`,`tags`, `readinglist`, or `pinned` record.
+ * * We can stably _store_ menu/toolbar/unfiled/mobile as special GUIDs, and set
+ * their parent ID as appropriate on upload.
+ *
+ * Fortunately, Fennec stores our representation of the data, not Places: that is,
+ * there's a "places" root, containing "mobile", "menu", "toolbar", etc.
+ *
+ * These are guaranteed to exist when the database is created.
+ *
+ * = Places folders =
+ *
+ * guid root_name folder_id parent
+ * ---------- ---------- ---------- ----------
+ * ? places 1 0
+ * ? menu 2 1
+ * ? toolbar 3 1
+ * ? tags 4 1
+ * ? unfiled 5 1
+ *
+ * ? mobile* 474 1
+ *
+ *
+ * = Fennec folders =
+ *
+ * guid folder_id parent
+ * ---------- ---------- ----------
+ * places 0 0
+ * mobile 1 0
+ * menu 2 0
+ * etc.
+ *
+ */
+ public static final Map<String, String> SPECIAL_GUID_PARENTS;
+ static {
+ HashMap<String, String> m = new HashMap<String, String>();
+ m.put("places", null);
+ m.put("menu", "places");
+ m.put("toolbar", "places");
+ m.put("tags", "places");
+ m.put("unfiled", "places");
+ m.put("mobile", "places");
+ SPECIAL_GUID_PARENTS = Collections.unmodifiableMap(m);
+ }
+
+
+ /**
+ * A map of guids to their localized name strings.
+ */
+ // Oh, if only we could make this final and initialize it in the static initializer.
+ public static Map<String, String> SPECIAL_GUIDS_MAP;
+
+ /**
+ * Return true if the provided record GUID should be skipped
+ * in child lists or fetch results.
+ *
+ * @param recordGUID the GUID of the record to check.
+ * @return true if the record should be skipped.
+ */
+ public static boolean forbiddenGUID(final String recordGUID) {
+ return recordGUID == null ||
+ BrowserContract.Bookmarks.PINNED_FOLDER_GUID.equals(recordGUID) ||
+ BrowserContract.Bookmarks.PLACES_FOLDER_GUID.equals(recordGUID) ||
+ BrowserContract.Bookmarks.TAGS_FOLDER_GUID.equals(recordGUID);
+ }
+
+ /**
+ * Return true if the provided parent GUID's children should
+ * be skipped in child lists or fetch results.
+ * This differs from {@link #forbiddenGUID(String)} in that we're skipping
+ * part of the hierarchy.
+ *
+ * @param parentGUID the GUID of parent of the record to check.
+ * @return true if the record should be skipped.
+ */
+ public static boolean forbiddenParent(final String parentGUID) {
+ return parentGUID == null ||
+ BrowserContract.Bookmarks.PINNED_FOLDER_GUID.equals(parentGUID);
+ }
+
+ public AndroidBrowserBookmarksRepositorySession(Repository repository, Context context) {
+ super(repository);
+
+ if (SPECIAL_GUIDS_MAP == null) {
+ HashMap<String, String> m = new HashMap<String, String>();
+
+ // Note that we always use the literal name "mobile" for the Mobile Bookmarks
+ // folder, regardless of its actual name in the database or the Fennec UI.
+ // This is to match desktop (working around Bug 747699) and to avoid a similar
+ // issue locally. See Bug 748898.
+ m.put("mobile", "mobile");
+
+ // Other folders use their contextualized names, and we simply rely on
+ // these not changing, matching desktop, and such to avoid issues.
+ m.put("menu", context.getString(R.string.bookmarks_folder_menu));
+ m.put("places", context.getString(R.string.bookmarks_folder_places));
+ m.put("toolbar", context.getString(R.string.bookmarks_folder_toolbar));
+ m.put("unfiled", context.getString(R.string.bookmarks_folder_unfiled));
+
+ SPECIAL_GUIDS_MAP = Collections.unmodifiableMap(m);
+ }
+
+ dbHelper = new AndroidBrowserBookmarksDataAccessor(context);
+ dataAccessor = (AndroidBrowserBookmarksDataAccessor) dbHelper;
+ }
+
+ private static int getTypeFromCursor(Cursor cur) {
+ return RepoUtils.getIntFromCursor(cur, BrowserContract.Bookmarks.TYPE);
+ }
+
+ private static boolean rowIsFolder(Cursor cur) {
+ return getTypeFromCursor(cur) == BrowserContract.Bookmarks.TYPE_FOLDER;
+ }
+
+ private String getGUIDForID(long androidID) {
+ String guid = parentIDToGuidMap.get(androidID);
+ trace(" " + androidID + " => " + guid);
+ return guid;
+ }
+
+ private long getIDForGUID(String guid) {
+ Long id = parentGuidToIDMap.get(guid);
+ if (id == null) {
+ Logger.warn(LOG_TAG, "Couldn't find local ID for GUID " + guid);
+ return -1;
+ }
+ return id;
+ }
+
+ private String getGUID(Cursor cur) {
+ return RepoUtils.getStringFromCursor(cur, "guid");
+ }
+
+ private long getParentID(Cursor cur) {
+ return RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks.PARENT);
+ }
+
+ // More efficient for bulk operations.
+ private long getPosition(Cursor cur, int positionIndex) {
+ return cur.getLong(positionIndex);
+ }
+ private long getPosition(Cursor cur) {
+ return RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks.POSITION);
+ }
+
+ private String getParentName(String parentGUID) throws ParentNotFoundException, NullCursorException {
+ if (parentGUID == null) {
+ return "";
+ }
+ if (SPECIAL_GUIDS_MAP.containsKey(parentGUID)) {
+ return SPECIAL_GUIDS_MAP.get(parentGUID);
+ }
+
+ // Get parent name from database.
+ String parentName = "";
+ Cursor name = dataAccessor.fetch(new String[] { parentGUID });
+ try {
+ name.moveToFirst();
+ if (!name.isAfterLast()) {
+ parentName = RepoUtils.getStringFromCursor(name, BrowserContract.Bookmarks.TITLE);
+ }
+ else {
+ Logger.error(LOG_TAG, "Couldn't find record with guid '" + parentGUID + "' when looking for parent name.");
+ throw new ParentNotFoundException(null);
+ }
+ } finally {
+ name.close();
+ }
+ return parentName;
+ }
+
+ /**
+ * Retrieve the child array for a record, repositioning and updating the database as necessary.
+ *
+ * @param folderID
+ * The database ID of the folder.
+ * @param persist
+ * True if generated positions should be written to the database. The modified
+ * time of the parent folder is only bumped if this is true.
+ * @param childArray
+ * A new, empty JSONArray which will be populated with an array of GUIDs.
+ * @return
+ * True if the resulting array is "clean" (i.e., reflects the content of the database).
+ * @throws NullCursorException
+ */
+ @SuppressWarnings("unchecked")
+ private boolean getChildrenArray(long folderID, boolean persist, JSONArray childArray) throws NullCursorException {
+ trace("Calling getChildren for androidID " + folderID);
+ Cursor children = dataAccessor.getChildren(folderID);
+ try {
+ if (!children.moveToFirst()) {
+ trace("No children: empty cursor.");
+ return true;
+ }
+ final int positionIndex = children.getColumnIndex(BrowserContract.Bookmarks.POSITION);
+ final int count = children.getCount();
+ Logger.debug(LOG_TAG, "Expecting " + count + " children.");
+
+ // Sorted by requested position.
+ TreeMap<Long, ArrayList<String>> guids = new TreeMap<Long, ArrayList<String>>();
+
+ while (!children.isAfterLast()) {
+ final String childGuid = getGUID(children);
+ final long childPosition = getPosition(children, positionIndex);
+ trace(" Child GUID: " + childGuid);
+ trace(" Child position: " + childPosition);
+ Utils.addToIndexBucketMap(guids, Math.abs(childPosition), childGuid);
+ children.moveToNext();
+ }
+
+ // This will suffice for taking a jumble of records and indices and
+ // producing a sorted sequence that preserves some kind of order --
+ // from the abs of the position, falling back on cursor order (that
+ // is, creation time and ID).
+ // Note that this code is not intended to merge values from two sources!
+ boolean changed = false;
+ int i = 0;
+ for (Entry<Long, ArrayList<String>> entry : guids.entrySet()) {
+ long pos = entry.getKey();
+ int atPos = entry.getValue().size();
+
+ // If every element has a different index, and the indices are
+ // in strict natural order, then changed will be false.
+ if (atPos > 1 || pos != i) {
+ changed = true;
+ }
+
+ ++i;
+
+ for (String guid : entry.getValue()) {
+ if (!forbiddenGUID(guid)) {
+ childArray.add(guid);
+ }
+ }
+ }
+
+ if (Logger.shouldLogVerbose(LOG_TAG)) {
+ // Don't JSON-encode unless we're logging.
+ Logger.trace(LOG_TAG, "Output child array: " + childArray.toJSONString());
+ }
+
+ if (!changed) {
+ Logger.debug(LOG_TAG, "Nothing moved! Database reflects child array.");
+ return true;
+ }
+
+ if (!persist) {
+ Logger.debug(LOG_TAG, "Returned array does not match database, and not persisting.");
+ return false;
+ }
+
+ Logger.debug(LOG_TAG, "Generating child array required moving records. Updating DB.");
+ final long time = now();
+ if (0 < dataAccessor.updatePositions(childArray)) {
+ Logger.debug(LOG_TAG, "Bumping parent time to " + time + ".");
+ dataAccessor.bumpModified(folderID, time);
+ }
+ return true;
+ } finally {
+ children.close();
+ }
+ }
+
+ protected static boolean isDeleted(Cursor cur) {
+ return RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) != 0;
+ }
+
+ @Override
+ protected Record retrieveDuringStore(Cursor cur) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
+ // During storing of a retrieved record, we never care about the children
+ // array that's already present in the database -- we don't use it for
+ // reconciling. Skip all that effort for now.
+ return retrieveRecord(cur, false);
+ }
+
+ @Override
+ protected Record retrieveDuringFetch(Cursor cur) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
+ return retrieveRecord(cur, true);
+ }
+
+ /**
+ * Build a record from a cursor, with a flag to dictate whether the
+ * children array should be computed and written back into the database.
+ */
+ protected BookmarkRecord retrieveRecord(Cursor cur, boolean computeAndPersistChildren) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
+ String recordGUID = getGUID(cur);
+ Logger.trace(LOG_TAG, "Record from mirror cursor: " + recordGUID);
+
+ if (forbiddenGUID(recordGUID)) {
+ Logger.debug(LOG_TAG, "Ignoring " + recordGUID + " record in recordFromMirrorCursor.");
+ return null;
+ }
+
+ // Short-cut for deleted items.
+ if (isDeleted(cur)) {
+ return AndroidBrowserBookmarksRepositorySession.bookmarkFromMirrorCursor(cur, null, null, null);
+ }
+
+ long androidParentID = getParentID(cur);
+
+ // Ensure special folders stay in the right place.
+ String androidParentGUID = SPECIAL_GUID_PARENTS.get(recordGUID);
+ if (androidParentGUID == null) {
+ androidParentGUID = getGUIDForID(androidParentID);
+ }
+
+ boolean needsReparenting = false;
+
+ if (androidParentGUID == null) {
+ Logger.debug(LOG_TAG, "No parent GUID for record " + recordGUID + " with parent " + androidParentID);
+ // If the parent has been stored and somehow has a null GUID, throw an error.
+ if (parentIDToGuidMap.containsKey(androidParentID)) {
+ Logger.error(LOG_TAG, "Have the parent android ID for the record but the parent's GUID wasn't found.");
+ throw new NoGuidForIdException(null);
+ }
+
+ // We have a parent ID but it's wrong. If the record is deleted,
+ // we'll just say that it was in the Unsorted Bookmarks folder.
+ // If not, we'll move it into Mobile Bookmarks.
+ needsReparenting = true;
+ }
+
+ // If record is a folder, and we want to see children at this time, then build out the children array.
+ final JSONArray childArray;
+ if (computeAndPersistChildren) {
+ childArray = getChildrenArrayForRecordCursor(cur, recordGUID, true);
+ } else {
+ childArray = null;
+ }
+ String parentName = getParentName(androidParentGUID);
+ BookmarkRecord bookmark = AndroidBrowserBookmarksRepositorySession.bookmarkFromMirrorCursor(cur, androidParentGUID, parentName, childArray);
+
+ if (bookmark == null) {
+ Logger.warn(LOG_TAG, "Unable to extract bookmark from cursor. Record GUID " + recordGUID +
+ ", parent " + androidParentGUID + "/" + androidParentID);
+ return null;
+ }
+
+ if (needsReparenting) {
+ Logger.warn(LOG_TAG, "Bookmark record " + recordGUID + " has a bad parent pointer. Reparenting now.");
+
+ String destination = bookmark.deleted ? "unfiled" : "mobile";
+ bookmark.androidParentID = getIDForGUID(destination);
+ bookmark.androidPosition = getPosition(cur);
+ bookmark.parentID = destination;
+ bookmark.parentName = getParentName(destination);
+ if (!bookmark.deleted) {
+ // Actually move it.
+ // TODO: compute position. Persist.
+ relocateBookmark(bookmark);
+ }
+ }
+
+ return bookmark;
+ }
+
+ /**
+ * Ensure that the local database row for the provided bookmark
+ * reflects this record's parent information.
+ *
+ * @param bookmark
+ */
+ private void relocateBookmark(BookmarkRecord bookmark) {
+ dataAccessor.updateParentAndPosition(bookmark.guid, bookmark.androidParentID, bookmark.androidPosition);
+ }
+
+ protected JSONArray getChildrenArrayForRecordCursor(Cursor cur, String recordGUID, boolean persist) throws NullCursorException {
+ boolean isFolder = rowIsFolder(cur);
+ if (!isFolder) {
+ return null;
+ }
+
+ long androidID = parentGuidToIDMap.get(recordGUID);
+ JSONArray childArray = new JSONArray();
+ getChildrenArray(androidID, persist, childArray);
+
+ Logger.debug(LOG_TAG, "Fetched " + childArray.size() + " children for " + recordGUID);
+ return childArray;
+ }
+
+ @Override
+ public boolean shouldIgnore(Record record) {
+ if (!(record instanceof BookmarkRecord)) {
+ return true;
+ }
+ if (record.deleted) {
+ return false;
+ }
+
+ BookmarkRecord bmk = (BookmarkRecord) record;
+
+ if (forbiddenGUID(bmk.guid)) {
+ Logger.debug(LOG_TAG, "Ignoring forbidden record with guid: " + bmk.guid);
+ return true;
+ }
+
+ if (forbiddenParent(bmk.parentID)) {
+ Logger.debug(LOG_TAG, "Ignoring child " + bmk.guid + " of forbidden parent folder " + bmk.parentID);
+ return true;
+ }
+
+ if (BrowserContractHelpers.isSupportedType(bmk.type)) {
+ return false;
+ }
+
+ Logger.debug(LOG_TAG, "Ignoring record with guid: " + bmk.guid + " and type: " + bmk.type);
+ return true;
+ }
+
+ @Override
+ public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException {
+ // Check for the existence of special folders
+ // and insert them if they don't exist.
+ Cursor cur;
+ try {
+ Logger.debug(LOG_TAG, "Check and build special GUIDs.");
+ dataAccessor.checkAndBuildSpecialGuids();
+ cur = dataAccessor.getGuidsIDsForFolders();
+ Logger.debug(LOG_TAG, "Got GUIDs for folders.");
+ } catch (android.database.sqlite.SQLiteConstraintException e) {
+ Logger.error(LOG_TAG, "Got sqlite constraint exception working with Fennec bookmark DB.", e);
+ delegate.onBeginFailed(e);
+ return;
+ } catch (Exception e) {
+ delegate.onBeginFailed(e);
+ return;
+ }
+
+ // To deal with parent mapping of bookmarks we have to do some
+ // hairy stuff. Here's the setup for it.
+
+ Logger.debug(LOG_TAG, "Preparing folder ID mappings.");
+
+ // Fake our root.
+ Logger.debug(LOG_TAG, "Tracking places root as ID 0.");
+ parentIDToGuidMap.put(0L, "places");
+ parentGuidToIDMap.put("places", 0L);
+ try {
+ cur.moveToFirst();
+ while (!cur.isAfterLast()) {
+ String guid = getGUID(cur);
+ long id = RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks._ID);
+ parentGuidToIDMap.put(guid, id);
+ parentIDToGuidMap.put(id, guid);
+ Logger.debug(LOG_TAG, "GUID " + guid + " maps to " + id);
+ cur.moveToNext();
+ }
+ } finally {
+ cur.close();
+ }
+ deletionManager = new BookmarksDeletionManager(dataAccessor, DEFAULT_DELETION_FLUSH_THRESHOLD);
+
+ // We just crawled the database enumerating all folders; we'll start the
+ // insertion manager with exactly these folders as the known parents (the
+ // collection is copied) in the manager constructor.
+ insertionManager = new BookmarksInsertionManager(DEFAULT_INSERTION_FLUSH_THRESHOLD, parentGuidToIDMap.keySet(), this);
+
+ Logger.debug(LOG_TAG, "Done with initial setup of bookmarks session.");
+ super.begin(delegate);
+ }
+
+ /**
+ * Implement method of BookmarksInsertionManager.BookmarkInserter.
+ */
+ @Override
+ public boolean insertFolder(BookmarkRecord record) {
+ // A folder that is *not* deleted needs its androidID updated, so that
+ // updateBookkeeping can re-parent, etc.
+ Record toStore = prepareRecord(record);
+ try {
+ Uri recordURI = dbHelper.insert(toStore);
+ if (recordURI == null) {
+ delegate.onRecordStoreFailed(new RuntimeException("Got null URI inserting folder with guid " + toStore.guid + "."), record.guid);
+ return false;
+ }
+ toStore.androidID = ContentUris.parseId(recordURI);
+ Logger.debug(LOG_TAG, "Inserted folder with guid " + toStore.guid + " as androidID " + toStore.androidID);
+
+ updateBookkeeping(toStore);
+ } catch (Exception e) {
+ delegate.onRecordStoreFailed(e, record.guid);
+ return false;
+ }
+ trackRecord(toStore);
+ delegate.onRecordStoreSucceeded(record.guid);
+ return true;
+ }
+
+ /**
+ * Implement method of BookmarksInsertionManager.BookmarkInserter.
+ */
+ @Override
+ public void bulkInsertNonFolders(Collection<BookmarkRecord> records) {
+ // All of these records are *not* deleted and *not* folders, so we don't
+ // need to update androidID at all!
+ // TODO: persist records that fail to insert for later retry.
+ ArrayList<Record> toStores = new ArrayList<Record>(records.size());
+ for (Record record : records) {
+ toStores.add(prepareRecord(record));
+ }
+
+ try {
+ int stored = dataAccessor.bulkInsert(toStores);
+ if (stored != toStores.size()) {
+ // Something failed; most pessimistic action is to declare that all insertions failed.
+ // TODO: perform the bulkInsert in a transaction and rollback unless all insertions succeed?
+ for (Record failed : toStores) {
+ delegate.onRecordStoreFailed(new RuntimeException("Possibly failed to bulkInsert non-folder with guid " + failed.guid + "."), failed.guid);
+ }
+ return;
+ }
+ } catch (NullCursorException e) {
+ for (Record failed : toStores) {
+ delegate.onRecordStoreFailed(e, failed.guid);
+ }
+ return;
+ }
+
+ // Success For All!
+ for (Record succeeded : toStores) {
+ try {
+ updateBookkeeping(succeeded);
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Got exception updating bookkeeping of non-folder with guid " + succeeded.guid + ".", e);
+ }
+ trackRecord(succeeded);
+ delegate.onRecordStoreSucceeded(succeeded.guid);
+ }
+ }
+
+ @Override
+ public void finish(RepositorySessionFinishDelegate delegate) throws InactiveSessionException {
+ // Allow these to be GCed.
+ deletionManager = null;
+ insertionManager = null;
+
+ // Override finish to do this check; make sure all records
+ // needing re-parenting have been re-parented.
+ if (needsReparenting != 0) {
+ Logger.error(LOG_TAG, "Finish called but " + needsReparenting +
+ " bookmark(s) have been placed in unsorted bookmarks and not been reparented.");
+
+ // TODO: handling of failed reparenting.
+ // E.g., delegate.onFinishFailed(new BookmarkNeedsReparentingException(null));
+ }
+ super.finish(delegate);
+ };
+
+ @Override
+ public void setStoreDelegate(RepositorySessionStoreDelegate delegate) {
+ super.setStoreDelegate(delegate);
+
+ if (deletionManager != null) {
+ deletionManager.setDelegate(delegate);
+ }
+ }
+
+ @Override
+ protected Record reconcileRecords(Record remoteRecord, Record localRecord,
+ long lastRemoteRetrieval,
+ long lastLocalRetrieval) {
+
+ BookmarkRecord reconciled = (BookmarkRecord) super.reconcileRecords(remoteRecord, localRecord,
+ lastRemoteRetrieval,
+ lastLocalRetrieval);
+
+ // For now we *always* use the remote record's children array as a starting point.
+ // We won't write it into the database yet; we'll record it and process as we go.
+ reconciled.children = ((BookmarkRecord) remoteRecord).children;
+
+ // *Always* track folders, though: if we decide we need to reposition items, we'll
+ // untrack later.
+ if (reconciled.isFolder()) {
+ trackRecord(reconciled);
+ }
+ return reconciled;
+ }
+
+ /**
+ * Rename mobile folders to "mobile", both in and out. The other half of
+ * this logic lives in {@link #computeParentFields(BookmarkRecord, String, String)}, where
+ * the parent name of a record is set from {@link #SPECIAL_GUIDS_MAP} rather than
+ * from source data.
+ *
+ * Apply this approach generally for symmetry.
+ */
+ @Override
+ protected void fixupRecord(Record record) {
+ final BookmarkRecord r = (BookmarkRecord) record;
+ final String parentName = SPECIAL_GUIDS_MAP.get(r.parentID);
+ if (parentName == null) {
+ return;
+ }
+ if (Logger.shouldLogVerbose(LOG_TAG)) {
+ Logger.trace(LOG_TAG, "Replacing parent name \"" + r.parentName + "\" with \"" + parentName + "\".");
+ }
+ r.parentName = parentName;
+ }
+
+ @Override
+ protected Record prepareRecord(Record record) {
+ if (record.deleted) {
+ Logger.debug(LOG_TAG, "No need to prepare deleted record " + record.guid);
+ return record;
+ }
+
+ BookmarkRecord bmk = (BookmarkRecord) record;
+
+ if (!isSpecialRecord(record)) {
+ // We never want to reparent special records.
+ handleParenting(bmk);
+ }
+
+ if (Logger.LOG_PERSONAL_INFORMATION) {
+ if (bmk.isFolder()) {
+ Logger.pii(LOG_TAG, "Inserting folder " + bmk.guid + ", " + bmk.title +
+ " with parent " + bmk.androidParentID +
+ " (" + bmk.parentID + ", " + bmk.parentName +
+ ", " + bmk.androidPosition + ")");
+ } else {
+ Logger.pii(LOG_TAG, "Inserting bookmark " + bmk.guid + ", " + bmk.title + ", " +
+ bmk.bookmarkURI + " with parent " + bmk.androidParentID +
+ " (" + bmk.parentID + ", " + bmk.parentName +
+ ", " + bmk.androidPosition + ")");
+ }
+ } else {
+ if (bmk.isFolder()) {
+ Logger.debug(LOG_TAG, "Inserting folder " + bmk.guid + ", parent " +
+ bmk.androidParentID +
+ " (" + bmk.parentID + ", " + bmk.androidPosition + ")");
+ } else {
+ Logger.debug(LOG_TAG, "Inserting bookmark " + bmk.guid + " with parent " +
+ bmk.androidParentID +
+ " (" + bmk.parentID + ", " + ", " + bmk.androidPosition + ")");
+ }
+ }
+ return bmk;
+ }
+
+ /**
+ * If the provided record doesn't have correct parent information,
+ * update appropriate bookkeeping to improve the situation.
+ *
+ * @param bmk
+ */
+ private void handleParenting(BookmarkRecord bmk) {
+ if (parentGuidToIDMap.containsKey(bmk.parentID)) {
+ bmk.androidParentID = parentGuidToIDMap.get(bmk.parentID);
+
+ // Might as well set a basic position from the downloaded children array.
+ JSONArray children = parentToChildArray.get(bmk.parentID);
+ if (children != null) {
+ int index = children.indexOf(bmk.guid);
+ if (index >= 0) {
+ bmk.androidPosition = index;
+ }
+ }
+ }
+ else {
+ bmk.androidParentID = parentGuidToIDMap.get("unfiled");
+ ArrayList<String> children;
+ if (missingParentToChildren.containsKey(bmk.parentID)) {
+ children = missingParentToChildren.get(bmk.parentID);
+ } else {
+ children = new ArrayList<String>();
+ }
+ children.add(bmk.guid);
+ needsReparenting++;
+ missingParentToChildren.put(bmk.parentID, children);
+ }
+ }
+
+ private boolean isSpecialRecord(Record record) {
+ return SPECIAL_GUID_PARENTS.containsKey(record.guid);
+ }
+
+ @Override
+ protected void updateBookkeeping(Record record) throws NoGuidForIdException,
+ NullCursorException,
+ ParentNotFoundException {
+ super.updateBookkeeping(record);
+ BookmarkRecord bmk = (BookmarkRecord) record;
+
+ // If record is folder, update maps and re-parent children if necessary.
+ if (!bmk.isFolder()) {
+ Logger.debug(LOG_TAG, "Not a folder. No bookkeeping.");
+ return;
+ }
+
+ Logger.debug(LOG_TAG, "Updating bookkeeping for folder " + record.guid);
+
+ // Mappings between ID and GUID.
+ // TODO: update our persisted children arrays!
+ // TODO: if our Android ID just changed, replace parents for all of our children.
+ parentGuidToIDMap.put(bmk.guid, bmk.androidID);
+ parentIDToGuidMap.put(bmk.androidID, bmk.guid);
+
+ JSONArray childArray = bmk.children;
+
+ if (Logger.shouldLogVerbose(LOG_TAG)) {
+ Logger.trace(LOG_TAG, bmk.guid + " has children " + childArray.toJSONString());
+ }
+ parentToChildArray.put(bmk.guid, childArray);
+
+ // Re-parent.
+ if (missingParentToChildren.containsKey(bmk.guid)) {
+ for (String child : missingParentToChildren.get(bmk.guid)) {
+ // This might return -1; that's OK, the bookmark will
+ // be properly repositioned later.
+ long position = childArray.indexOf(child);
+ dataAccessor.updateParentAndPosition(child, bmk.androidID, position);
+ needsReparenting--;
+ }
+ missingParentToChildren.remove(bmk.guid);
+ }
+ }
+
+ @Override
+ protected void insert(Record record) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
+ try {
+ insertionManager.enqueueRecord((BookmarkRecord) record);
+ } catch (Exception e) {
+ throw new NullCursorException(e);
+ }
+ }
+
+ @Override
+ protected void storeRecordDeletion(final Record record, final Record existingRecord) {
+ if (SPECIAL_GUIDS_MAP.containsKey(record.guid)) {
+ Logger.debug(LOG_TAG, "Told to delete record " + record.guid + ". Ignoring.");
+ return;
+ }
+ final BookmarkRecord bookmarkRecord = (BookmarkRecord) record;
+ final BookmarkRecord existingBookmark = (BookmarkRecord) existingRecord;
+ final boolean isFolder = existingBookmark.isFolder();
+ final String parentGUID = existingBookmark.parentID;
+ deletionManager.deleteRecord(bookmarkRecord.guid, isFolder, parentGUID);
+ }
+
+ protected void flushQueues() {
+ long now = now();
+ Logger.debug(LOG_TAG, "Applying remaining insertions.");
+ try {
+ insertionManager.finishUp();
+ Logger.debug(LOG_TAG, "Done applying remaining insertions.");
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Unable to apply remaining insertions.", e);
+ }
+
+ Logger.debug(LOG_TAG, "Applying deletions.");
+ try {
+ untrackGUIDs(deletionManager.flushAll(getIDForGUID("unfiled"), now));
+ Logger.debug(LOG_TAG, "Done applying deletions.");
+ } catch (Exception e) {
+ Logger.error(LOG_TAG, "Unable to apply deletions.", e);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void finishUp() {
+ try {
+ flushQueues();
+ Logger.debug(LOG_TAG, "Have " + parentToChildArray.size() + " folders whose children might need repositioning.");
+ for (Entry<String, JSONArray> entry : parentToChildArray.entrySet()) {
+ String guid = entry.getKey();
+ JSONArray onServer = entry.getValue();
+ try {
+ final long folderID = getIDForGUID(guid);
+ final JSONArray inDB = new JSONArray();
+ final boolean clean = getChildrenArray(folderID, false, inDB);
+ final boolean sameArrays = Utils.sameArrays(onServer, inDB);
+
+ // If the local children and the remote children are already
+ // the same, then we don't need to bump the modified time of the
+ // parent: we wouldn't upload a different record, so avoid the cycle.
+ if (!sameArrays) {
+ int added = 0;
+ for (Object o : inDB) {
+ if (!onServer.contains(o)) {
+ onServer.add(o);
+ added++;
+ }
+ }
+ Logger.debug(LOG_TAG, "Added " + added + " items locally.");
+ Logger.debug(LOG_TAG, "Untracking and bumping " + guid + "(" + folderID + ")");
+ dataAccessor.bumpModified(folderID, now());
+ untrackGUID(guid);
+ }
+
+ // If the arrays are different, or they're the same but not flushed to disk,
+ // write them out now.
+ if (!sameArrays || !clean) {
+ dataAccessor.updatePositions(new ArrayList<String>(onServer));
+ }
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Error repositioning children for " + guid, e);
+ }
+ }
+ } finally {
+ super.storeDone();
+ }
+ }
+
+ /**
+ * Hook into the deletion manager on wipe.
+ */
+ class BookmarkWipeRunnable extends WipeRunnable {
+ public BookmarkWipeRunnable(RepositorySessionWipeDelegate delegate) {
+ super(delegate);
+ }
+
+ @Override
+ public void run() {
+ try {
+ // Clear our queued deletions.
+ deletionManager.clear();
+ insertionManager.clear();
+ super.run();
+ } catch (Exception ex) {
+ delegate.onWipeFailed(ex);
+ return;
+ }
+ }
+ }
+
+ @Override
+ protected WipeRunnable getWipeRunnable(RepositorySessionWipeDelegate delegate) {
+ return new BookmarkWipeRunnable(delegate);
+ }
+
+ @Override
+ public void storeDone() {
+ Runnable command = new Runnable() {
+ @Override
+ public void run() {
+ finishUp();
+ }
+ };
+ storeWorkQueue.execute(command);
+ }
+
+ @Override
+ protected String buildRecordString(Record record) {
+ BookmarkRecord bmk = (BookmarkRecord) record;
+ String parent = bmk.parentName + "/";
+ if (bmk.isBookmark()) {
+ return "b" + parent + bmk.bookmarkURI + ":" + bmk.title;
+ }
+ if (bmk.isFolder()) {
+ return "f" + parent + bmk.title;
+ }
+ if (bmk.isSeparator()) {
+ return "s" + parent + bmk.androidPosition;
+ }
+ if (bmk.isQuery()) {
+ return "q" + parent + bmk.bookmarkURI;
+ }
+ return null;
+ }
+
+ public static BookmarkRecord computeParentFields(BookmarkRecord rec, String suggestedParentGUID, String suggestedParentName) {
+ final String guid = rec.guid;
+ if (guid == null) {
+ // Oh dear.
+ Logger.error(LOG_TAG, "No guid in computeParentFields!");
+ return null;
+ }
+
+ String realParent = SPECIAL_GUID_PARENTS.get(guid);
+ if (realParent == null) {
+ // No magic parent. Use whatever the caller suggests.
+ realParent = suggestedParentGUID;
+ } else {
+ Logger.debug(LOG_TAG, "Ignoring suggested parent ID " + suggestedParentGUID +
+ " for " + guid + "; using " + realParent);
+ }
+
+ if (realParent == null) {
+ // Oh dear.
+ Logger.error(LOG_TAG, "No parent for record " + guid);
+ return null;
+ }
+
+ // Always set the parent name for special folders back to default.
+ String parentName = SPECIAL_GUIDS_MAP.get(realParent);
+ if (parentName == null) {
+ parentName = suggestedParentName;
+ }
+
+ rec.parentID = realParent;
+ rec.parentName = parentName;
+ return rec;
+ }
+
+ private static BookmarkRecord logBookmark(BookmarkRecord rec) {
+ try {
+ Logger.debug(LOG_TAG, "Returning " + (rec.deleted ? "deleted " : "") +
+ "bookmark record " + rec.guid + " (" + rec.androidID +
+ ", parent " + rec.parentID + ")");
+ if (!rec.deleted && Logger.LOG_PERSONAL_INFORMATION) {
+ Logger.pii(LOG_TAG, "> Parent name: " + rec.parentName);
+ Logger.pii(LOG_TAG, "> Title: " + rec.title);
+ Logger.pii(LOG_TAG, "> Type: " + rec.type);
+ Logger.pii(LOG_TAG, "> URI: " + rec.bookmarkURI);
+ Logger.pii(LOG_TAG, "> Position: " + rec.androidPosition);
+ if (rec.isFolder()) {
+ Logger.pii(LOG_TAG, "FOLDER: Children are " +
+ (rec.children == null ?
+ "null" :
+ rec.children.toJSONString()));
+ }
+ }
+ } catch (Exception e) {
+ Logger.debug(LOG_TAG, "Exception logging bookmark record " + rec, e);
+ }
+ return rec;
+ }
+
+ // Create a BookmarkRecord object from a cursor on a row containing a Fennec bookmark.
+ public static BookmarkRecord bookmarkFromMirrorCursor(Cursor cur, String parentGUID, String parentName, JSONArray children) {
+ final String collection = "bookmarks";
+ final String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
+ final long lastModified = RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.DATE_MODIFIED);
+ final boolean deleted = isDeleted(cur);
+ BookmarkRecord rec = new BookmarkRecord(guid, collection, lastModified, deleted);
+
+ // No point in populating it.
+ if (deleted) {
+ return logBookmark(rec);
+ }
+
+ int rowType = getTypeFromCursor(cur);
+ String typeString = BrowserContractHelpers.typeStringForCode(rowType);
+
+ if (typeString == null) {
+ Logger.warn(LOG_TAG, "Unsupported type code " + rowType);
+ return null;
+ }
+
+ Logger.trace(LOG_TAG, "Record " + guid + " has type " + typeString);
+
+ rec.type = typeString;
+ rec.title = RepoUtils.getStringFromCursor(cur, BrowserContract.Bookmarks.TITLE);
+ rec.bookmarkURI = RepoUtils.getStringFromCursor(cur, BrowserContract.Bookmarks.URL);
+ rec.description = RepoUtils.getStringFromCursor(cur, BrowserContract.Bookmarks.DESCRIPTION);
+ rec.tags = RepoUtils.getJSONArrayFromCursor(cur, BrowserContract.Bookmarks.TAGS);
+ rec.keyword = RepoUtils.getStringFromCursor(cur, BrowserContract.Bookmarks.KEYWORD);
+
+ rec.androidID = RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks._ID);
+ rec.androidPosition = RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks.POSITION);
+ rec.children = children;
+
+ // Need to restore the parentId since it isn't stored in content provider.
+ // We also take this opportunity to fix up parents for special folders,
+ // allowing us to map between the hierarchies used by Fennec and Places.
+ BookmarkRecord withParentFields = computeParentFields(rec, parentGUID, parentName);
+ if (withParentFields == null) {
+ // Oh dear. Something went wrong.
+ return null;
+ }
+ return logBookmark(withParentFields);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryDataAccessor.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryDataAccessor.java
new file mode 100644
index 0000000000..c09d64708a
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryDataAccessor.java
@@ -0,0 +1,188 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.android;
+
+import java.util.ArrayList;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.domain.HistoryRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+
+public class AndroidBrowserHistoryDataAccessor extends
+ AndroidBrowserRepositoryDataAccessor {
+
+ public AndroidBrowserHistoryDataAccessor(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected Uri getUri() {
+ return BrowserContractHelpers.HISTORY_CONTENT_URI;
+ }
+
+ @Override
+ protected ContentValues getContentValues(Record record) {
+ ContentValues cv = new ContentValues();
+ HistoryRecord rec = (HistoryRecord) record;
+ cv.put(BrowserContract.History.GUID, rec.guid);
+ cv.put(BrowserContract.History.TITLE, rec.title);
+ cv.put(BrowserContract.History.URL, rec.histURI);
+ if (rec.visits != null) {
+ JSONArray visits = rec.visits;
+ long mostRecent = getLastVisited(visits);
+
+ // Fennec stores history timestamps in milliseconds, and visit timestamps in microseconds.
+ // The rest of Sync works in microseconds. This is the conversion point for records coming form Sync.
+ cv.put(BrowserContract.History.DATE_LAST_VISITED, mostRecent / 1000);
+ cv.put(BrowserContract.History.REMOTE_DATE_LAST_VISITED, mostRecent / 1000);
+ cv.put(BrowserContract.History.VISITS, Long.toString(visits.size()));
+ }
+ return cv;
+ }
+
+ @Override
+ protected String[] getAllColumns() {
+ return BrowserContractHelpers.HistoryColumns;
+ }
+
+ @Override
+ public Uri insert(Record record) {
+ HistoryRecord rec = (HistoryRecord) record;
+
+ Logger.debug(LOG_TAG, "Storing record " + record.guid);
+ Uri newRecordUri = super.insert(record);
+
+ Logger.debug(LOG_TAG, "Storing visits for " + record.guid);
+ context.getContentResolver().bulkInsert(
+ BrowserContract.Visits.CONTENT_URI,
+ VisitsHelper.getVisitsContentValues(rec.guid, rec.visits)
+ );
+
+ return newRecordUri;
+ }
+
+ /**
+ * Given oldGUID, first updates corresponding history record with new values (super operation),
+ * and then inserts visits from the new record.
+ * Existing visits from the old record are updated on database level to point to new GUID if necessary.
+ *
+ * @param oldGUID GUID of old <code>HistoryRecord</code>
+ * @param newRecord new <code>HistoryRecord</code> to replace old one with, and insert visits from
+ */
+ @Override
+ public void update(String oldGUID, Record newRecord) {
+ // First, update existing history records with new values. This might involve changing history GUID,
+ // and thanks to ON UPDATE CASCADE clause on Visits.HISTORY_GUID foreign key, visits will be "ported over"
+ // to the new GUID.
+ super.update(oldGUID, newRecord);
+
+ // Now we need to insert any visits from the new record
+ HistoryRecord rec = (HistoryRecord) newRecord;
+ String newGUID = newRecord.guid;
+ Logger.debug(LOG_TAG, "Storing visits for " + newGUID + ", replacing " + oldGUID);
+
+ context.getContentResolver().bulkInsert(
+ BrowserContract.Visits.CONTENT_URI,
+ VisitsHelper.getVisitsContentValues(newGUID, rec.visits)
+ );
+ }
+
+ /**
+ * Insert records.
+ * <p>
+ * This inserts all the records (using <code>ContentProvider.bulkInsert</code>),
+ * then inserts all the visit information (also using <code>ContentProvider.bulkInsert</code>).
+ *
+ * @param records
+ * the records to insert.
+ * @return
+ * the number of records actually inserted.
+ * @throws NullCursorException
+ */
+ public int bulkInsert(ArrayList<HistoryRecord> records) throws NullCursorException {
+ if (records.isEmpty()) {
+ Logger.debug(LOG_TAG, "No records to insert, returning.");
+ }
+
+ int size = records.size();
+ ContentValues[] cvs = new ContentValues[size];
+ int index = 0;
+ for (Record record : records) {
+ if (record.guid == null) {
+ throw new IllegalArgumentException("Record with null GUID passed in to bulkInsert.");
+ }
+ cvs[index] = getContentValues(record);
+ index += 1;
+ }
+
+ // First update the history records.
+ int inserted = context.getContentResolver().bulkInsert(getUri(), cvs);
+ if (inserted == size) {
+ Logger.debug(LOG_TAG, "Inserted " + inserted + " records, as expected.");
+ } else {
+ Logger.debug(LOG_TAG, "Inserted " +
+ inserted + " records but expected " +
+ size + " records; continuing to update visits.");
+ }
+
+ final ContentValues remoteVisitAggregateValues = new ContentValues();
+ final Uri historyIncrementRemoteAggregateUri = getUri().buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_REMOTE_AGGREGATES, "true")
+ .build();
+ for (Record record : records) {
+ HistoryRecord rec = (HistoryRecord) record;
+ if (rec.visits != null && rec.visits.size() != 0) {
+ int remoteVisitsInserted = context.getContentResolver().bulkInsert(
+ BrowserContract.Visits.CONTENT_URI,
+ VisitsHelper.getVisitsContentValues(rec.guid, rec.visits)
+ );
+
+ // If we just inserted any visits, update remote visit aggregate values.
+ // While inserting visits, we might not insert all of rec.visits - if we already have a local
+ // visit record with matching (guid,date), we will skip that visit.
+ // Remote visits aggregate value will be incremented by number of visits inserted.
+ // Note that we don't need to set REMOTE_DATE_LAST_VISITED, because it already gets set above.
+ if (remoteVisitsInserted > 0) {
+ // Note that REMOTE_VISITS must be set before calling cr.update(...) with a URI
+ // that has PARAM_INCREMENT_REMOTE_AGGREGATES=true.
+ remoteVisitAggregateValues.put(BrowserContract.History.REMOTE_VISITS, remoteVisitsInserted);
+ context.getContentResolver().update(
+ historyIncrementRemoteAggregateUri,
+ remoteVisitAggregateValues,
+ BrowserContract.History.GUID + " = ?", new String[] {rec.guid}
+ );
+ }
+ }
+ }
+
+ return inserted;
+ }
+
+ /**
+ * Helper method used to find largest <code>VisitsHelper.SYNC_DATE_KEY</code> value in a provided JSONArray.
+ *
+ * @param visits Array of objects which will be searched.
+ * @return largest value of <code>VisitsHelper.SYNC_DATE_KEY</code>.
+ */
+ private long getLastVisited(JSONArray visits) {
+ long mostRecent = 0;
+ for (int i = 0; i < visits.size(); i++) {
+ final JSONObject visit = (JSONObject) visits.get(i);
+ long visitDate = (Long) visit.get(VisitsHelper.SYNC_DATE_KEY);
+ if (visitDate > mostRecent) {
+ mostRecent = visitDate;
+ }
+ }
+ return mostRecent;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepository.java
new file mode 100644
index 0000000000..bd2b5d31fa
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepository.java
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.android;
+
+import org.mozilla.gecko.sync.repositories.HistoryRepository;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+
+import android.content.Context;
+
+public class AndroidBrowserHistoryRepository extends AndroidBrowserRepository implements HistoryRepository {
+
+ @Override
+ protected void sessionCreator(RepositorySessionCreationDelegate delegate, Context context) {
+ AndroidBrowserHistoryRepositorySession session = new AndroidBrowserHistoryRepositorySession(AndroidBrowserHistoryRepository.this, context);
+ delegate.onSessionCreated(session);
+ }
+
+ @Override
+ protected AndroidBrowserRepositoryDataAccessor getDataAccessor(Context context) {
+ return new AndroidBrowserHistoryDataAccessor(context);
+ }
+
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepositorySession.java
new file mode 100644
index 0000000000..7c462abc36
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepositorySession.java
@@ -0,0 +1,208 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.android;
+
+import java.util.ArrayList;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
+import org.mozilla.gecko.sync.repositories.NoGuidForIdException;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.ParentNotFoundException;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
+import org.mozilla.gecko.sync.repositories.domain.HistoryRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import android.content.ContentProviderClient;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.RemoteException;
+
+public class AndroidBrowserHistoryRepositorySession extends AndroidBrowserRepositorySession {
+ public static final String LOG_TAG = "ABHistoryRepoSess";
+
+ /**
+ * The number of records to queue for insertion before writing to databases.
+ */
+ public static final int INSERT_RECORD_THRESHOLD = 50;
+ public static final int RECENT_VISITS_LIMIT = 20;
+
+ public AndroidBrowserHistoryRepositorySession(Repository repository, Context context) {
+ super(repository);
+ dbHelper = new AndroidBrowserHistoryDataAccessor(context);
+ }
+
+ @Override
+ public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException {
+ // HACK: Fennec creates history records without a GUID. Mercilessly drop
+ // them on the floor. See Bug 739514.
+ try {
+ dbHelper.delete(BrowserContract.History.GUID + " IS NULL", null);
+ } catch (Exception e) {
+ // Ignore.
+ }
+ super.begin(delegate);
+ }
+
+ @Override
+ protected Record retrieveDuringStore(Cursor cur) {
+ return RepoUtils.historyFromMirrorCursor(cur);
+ }
+
+ @Override
+ protected Record retrieveDuringFetch(Cursor cur) {
+ return RepoUtils.historyFromMirrorCursor(cur);
+ }
+
+ @Override
+ protected String buildRecordString(Record record) {
+ HistoryRecord hist = (HistoryRecord) record;
+ return hist.histURI;
+ }
+
+ @Override
+ public boolean shouldIgnore(Record record) {
+ if (super.shouldIgnore(record)) {
+ return true;
+ }
+ if (!(record instanceof HistoryRecord)) {
+ return true;
+ }
+ HistoryRecord r = (HistoryRecord) record;
+ return !RepoUtils.isValidHistoryURI(r.histURI);
+ }
+
+ @Override
+ protected Record transformRecord(Record record) throws NullCursorException {
+ return addVisitsToRecord(record);
+ }
+
+ private Record addVisitsToRecord(Record record) throws NullCursorException {
+ Logger.debug(LOG_TAG, "Adding visits for GUID " + record.guid);
+
+ // Sync is an object store, so what we attach here will replace what's already present on the Sync servers.
+ // We upload just a recent subset of visits for each history record for space and bandwidth reasons.
+ // We chose 20 to be conservative. See Bug 1164660 for details.
+ ContentProviderClient visitsClient = dbHelper.context.getContentResolver().acquireContentProviderClient(BrowserContractHelpers.VISITS_CONTENT_URI);
+ if (visitsClient == null) {
+ throw new IllegalStateException("Could not obtain a ContentProviderClient for Visits URI");
+ }
+
+ try {
+ ((HistoryRecord) record).visits = VisitsHelper.getRecentHistoryVisitsForGUID(
+ visitsClient, record.guid, RECENT_VISITS_LIMIT);
+ } catch (RemoteException e) {
+ throw new IllegalStateException("Error while obtaining visits for a record", e);
+ } finally {
+ visitsClient.release();
+ }
+
+ return record;
+ }
+
+ @Override
+ protected Record prepareRecord(Record record) {
+ return record;
+ }
+
+ protected final Object recordsBufferMonitor = new Object();
+ protected ArrayList<HistoryRecord> recordsBuffer = new ArrayList<HistoryRecord>();
+
+ /**
+ * Queue record for insertion, possibly flushing the queue.
+ * <p>
+ * Must be called on <code>storeWorkQueue</code> thread! But this is only
+ * called from <code>store</code>, which is called on the queue thread.
+ *
+ * @param record
+ * A <code>Record</code> with a GUID that is not present locally.
+ */
+ @Override
+ protected void insert(Record record) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
+ enqueueNewRecord((HistoryRecord) prepareRecord(record));
+ }
+
+ /**
+ * Batch incoming records until some reasonable threshold is hit or storeDone
+ * is received.
+ * <p>
+ * Must be called on <code>storeWorkQueue</code> thread!
+ *
+ * @param record A <code>Record</code> with a GUID that is not present locally.
+ * @throws NullCursorException
+ */
+ protected void enqueueNewRecord(HistoryRecord record) throws NullCursorException {
+ synchronized (recordsBufferMonitor) {
+ if (recordsBuffer.size() >= INSERT_RECORD_THRESHOLD) {
+ flushNewRecords();
+ }
+ Logger.debug(LOG_TAG, "Enqueuing new record with GUID " + record.guid);
+ recordsBuffer.add(record);
+ }
+ }
+
+ /**
+ * Flush queue of incoming records to database.
+ * <p>
+ * Must be called on <code>storeWorkQueue</code> thread!
+ * <p>
+ * Must be locked by recordsBufferMonitor!
+ * @throws NullCursorException
+ */
+ protected void flushNewRecords() throws NullCursorException {
+ if (recordsBuffer.size() < 1) {
+ Logger.debug(LOG_TAG, "No records to flush, returning.");
+ return;
+ }
+
+ final ArrayList<HistoryRecord> outgoing = recordsBuffer;
+ recordsBuffer = new ArrayList<HistoryRecord>();
+ Logger.debug(LOG_TAG, "Flushing " + outgoing.size() + " records to database.");
+ // TODO: move bulkInsert to AndroidBrowserDataAccessor?
+ int inserted = ((AndroidBrowserHistoryDataAccessor) dbHelper).bulkInsert(outgoing);
+ if (inserted != outgoing.size()) {
+ // Something failed; most pessimistic action is to declare that all insertions failed.
+ // TODO: perform the bulkInsert in a transaction and rollback unless all insertions succeed?
+ for (HistoryRecord failed : outgoing) {
+ delegate.onRecordStoreFailed(new RuntimeException("Failed to insert history item with guid " + failed.guid + "."), failed.guid);
+ }
+ return;
+ }
+
+ // All good, everybody succeeded.
+ for (HistoryRecord succeeded : outgoing) {
+ try {
+ // Does not use androidID -- just GUID -> String map.
+ updateBookkeeping(succeeded);
+ } catch (NoGuidForIdException | ParentNotFoundException e) {
+ // Should not happen.
+ throw new NullCursorException(e);
+ } catch (NullCursorException e) {
+ throw e;
+ }
+ trackRecord(succeeded);
+ delegate.onRecordStoreSucceeded(succeeded.guid); // At this point, we are really inserted.
+ }
+ }
+
+ @Override
+ public void storeDone() {
+ storeWorkQueue.execute(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (recordsBufferMonitor) {
+ try {
+ flushNewRecords();
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Error flushing records to database.", e);
+ }
+ }
+ storeDone(System.currentTimeMillis());
+ }
+ });
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepository.java
new file mode 100644
index 0000000000..6c5c661eea
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepository.java
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.android;
+
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCleanDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+
+import android.content.Context;
+
+public abstract class AndroidBrowserRepository extends Repository {
+
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate, Context context) {
+ new CreateSessionThread(delegate, context).start();
+ }
+
+ @Override
+ public void clean(boolean success, RepositorySessionCleanDelegate delegate, Context context) {
+ // Only clean deleted records if success
+ if (success) {
+ new CleanThread(delegate, context).start();
+ }
+ }
+
+ class CleanThread extends Thread {
+ private final RepositorySessionCleanDelegate delegate;
+ private final Context context;
+
+ public CleanThread(RepositorySessionCleanDelegate delegate, Context context) {
+ if (context == null) {
+ throw new IllegalArgumentException("context is null");
+ }
+ this.delegate = delegate;
+ this.context = context;
+ }
+
+ @Override
+ public void run() {
+ try {
+ getDataAccessor(context).purgeDeleted();
+ } catch (Exception e) {
+ delegate.onCleanFailed(AndroidBrowserRepository.this, e);
+ return;
+ }
+ delegate.onCleaned(AndroidBrowserRepository.this);
+ }
+ }
+
+ protected abstract AndroidBrowserRepositoryDataAccessor getDataAccessor(Context context);
+ protected abstract void sessionCreator(RepositorySessionCreationDelegate delegate, Context context);
+
+ class CreateSessionThread extends Thread {
+ private final RepositorySessionCreationDelegate delegate;
+ private final Context context;
+
+ public CreateSessionThread(RepositorySessionCreationDelegate delegate, Context context) {
+ if (context == null) {
+ throw new IllegalArgumentException("context is null.");
+ }
+ this.delegate = delegate;
+ this.context = context;
+ }
+
+ @Override
+ public void run() {
+ sessionCreator(delegate, context);
+ }
+ }
+
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepositoryDataAccessor.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepositoryDataAccessor.java
new file mode 100644
index 0000000000..138d63d4cc
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepositoryDataAccessor.java
@@ -0,0 +1,232 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.android;
+
+import java.util.List;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.db.CursorDumper;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+
+public abstract class AndroidBrowserRepositoryDataAccessor {
+
+ private static final String[] GUID_COLUMNS = new String[] { BrowserContract.SyncColumns.GUID };
+ protected Context context;
+ protected static String LOG_TAG = "BrowserDataAccessor";
+ protected final RepoUtils.QueryHelper queryHelper;
+
+ public AndroidBrowserRepositoryDataAccessor(Context context) {
+ this.context = context;
+ this.queryHelper = new RepoUtils.QueryHelper(context, getUri(), LOG_TAG);
+ }
+
+ protected abstract String[] getAllColumns();
+
+ /**
+ * Produce a <code>ContentValues</code> instance that represents the provided <code>Record</code>.
+ *
+ * @param record The <code>Record</code> to be converted.
+ * @return The <code>ContentValues</code> corresponding to <code>record</code>.
+ */
+ protected abstract ContentValues getContentValues(Record record);
+
+ protected abstract Uri getUri();
+
+ /**
+ * Dump all the records in raw format.
+ */
+ public void dumpDB() {
+ Cursor cur = null;
+ try {
+ cur = queryHelper.safeQuery(".dumpDB", null, null, null, null);
+ CursorDumper.dumpCursor(cur);
+ } catch (NullCursorException e) {
+ } finally {
+ if (cur != null) {
+ cur.close();
+ }
+ }
+ }
+
+ public String dateModifiedWhere(long timestamp) {
+ return BrowserContract.SyncColumns.DATE_MODIFIED + " >= " + Long.toString(timestamp);
+ }
+
+ public void delete(String where, String[] args) {
+ Uri uri = getUri();
+ context.getContentResolver().delete(uri, where, args);
+ }
+
+ public void wipe() {
+ Logger.debug(LOG_TAG, "Wiping.");
+ delete(null, null);
+ }
+
+ public void purgeDeleted() throws NullCursorException {
+ String where = BrowserContract.SyncColumns.IS_DELETED + "= 1";
+ Uri uri = getUri();
+ Logger.info(LOG_TAG, "Purging deleted from: " + uri);
+ context.getContentResolver().delete(uri, where, null);
+ }
+
+ /**
+ * Remove matching records from the database entirely, i.e., do not set a
+ * deleted flag, delete entirely.
+ *
+ * @param guid
+ * The GUID of the record to be deleted.
+ * @return The number of records deleted.
+ */
+ public int purgeGuid(String guid) {
+ String where = BrowserContract.SyncColumns.GUID + " = ?";
+ String[] args = new String[] { guid };
+
+ int deleted = context.getContentResolver().delete(getUri(), where, args);
+ if (deleted != 1) {
+ Logger.warn(LOG_TAG, "Unexpectedly deleted " + deleted + " records for guid " + guid);
+ }
+ return deleted;
+ }
+
+ public void update(String guid, Record newRecord) {
+ String where = BrowserContract.SyncColumns.GUID + " = ?";
+ String[] args = new String[] { guid };
+ ContentValues cv = getContentValues(newRecord);
+ int updated = context.getContentResolver().update(getUri(), cv, where, args);
+ if (updated != 1) {
+ Logger.warn(LOG_TAG, "Unexpectedly updated " + updated + " rows for guid " + guid);
+ }
+ }
+
+ public Uri insert(Record record) {
+ ContentValues cv = getContentValues(record);
+ return context.getContentResolver().insert(getUri(), cv);
+ }
+
+ /**
+ * Fetch all records.
+ * <p>
+ * The caller is responsible for closing the cursor.
+ *
+ * @return A cursor. You </b>must</b> close this when you're done with it.
+ * @throws NullCursorException
+ */
+ public Cursor fetchAll() throws NullCursorException {
+ return queryHelper.safeQuery(".fetchAll", getAllColumns(), null, null, null);
+ }
+
+ /**
+ * Fetch GUIDs for records modified since the provided timestamp.
+ * <p>
+ * The caller is responsible for closing the cursor.
+ *
+ * @param timestamp A timestamp in milliseconds.
+ * @return A cursor. You <b>must</b> close this when you're done with it.
+ * @throws NullCursorException
+ */
+ public Cursor getGUIDsSince(long timestamp) throws NullCursorException {
+ return queryHelper.safeQuery(".getGUIDsSince",
+ GUID_COLUMNS,
+ dateModifiedWhere(timestamp),
+ null, null);
+ }
+
+ /**
+ * Fetch records modified since the provided timestamp.
+ * <p>
+ * The caller is responsible for closing the cursor.
+ *
+ * @param timestamp A timestamp in milliseconds.
+ * @return A cursor. You <b>must</b> close this when you're done with it.
+ * @throws NullCursorException
+ */
+ public Cursor fetchSince(long timestamp) throws NullCursorException {
+ return queryHelper.safeQuery(".fetchSince",
+ getAllColumns(),
+ dateModifiedWhere(timestamp),
+ null, null);
+ }
+
+ /**
+ * Fetch records for the provided GUIDs.
+ * <p>
+ * The caller is responsible for closing the cursor.
+ *
+ * @param guids The GUIDs of the records to fetch.
+ * @return A cursor. You <b>must</b> close this when you're done with it.
+ * @throws NullCursorException
+ */
+ public Cursor fetch(String guids[]) throws NullCursorException {
+ String where = RepoUtils.computeSQLInClause(guids.length, "guid");
+ return queryHelper.safeQuery(".fetch", getAllColumns(), where, guids, null);
+ }
+
+ public void updateByGuid(String guid, ContentValues cv) {
+ String where = BrowserContract.SyncColumns.GUID + " = ?";
+ String[] args = new String[] { guid };
+
+ int updated = context.getContentResolver().update(getUri(), cv, where, args);
+ if (updated == 1) {
+ return;
+ }
+ Logger.warn(LOG_TAG, "Unexpectedly updated " + updated + " rows for guid " + guid);
+ }
+
+ /**
+ * Insert records.
+ * <p>
+ * This inserts all the records (using <code>ContentProvider.bulkInsert</code>),
+ * but does <b>not</b> update the <code>androidID</code> of each record.
+ *
+ * @param records
+ * the records to insert.
+ * @return
+ * the number of records actually inserted.
+ * @throws NullCursorException
+ */
+ public int bulkInsert(List<Record> records) throws NullCursorException {
+ if (records.isEmpty()) {
+ Logger.debug(LOG_TAG, "No records to insert, returning.");
+ }
+
+ int size = records.size();
+ ContentValues[] cvs = new ContentValues[size];
+ int index = 0;
+ for (Record record : records) {
+ try {
+ cvs[index] = getContentValues(record);
+ index += 1;
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Got exception in getContentValues for record with guid " + record.guid, e);
+ }
+ }
+
+ if (index != size) {
+ // bulkInsert treats null ContentValues as blank rows, which we don't want
+ // to insert into the database.
+ // We expect exceptions in getContentValues to be exceedingly rare, so we
+ // re-allocate in the (rare) error case and maintain a fast path for the
+ // success case.
+ size = index;
+ }
+
+ int inserted = context.getContentResolver().bulkInsert(getUri(), cvs);
+ if (inserted == size) {
+ Logger.debug(LOG_TAG, "Inserted " + inserted + " records, as expected.");
+ } else {
+ Logger.debug(LOG_TAG, "Inserted " +
+ inserted + " records but expected " +
+ size + " records.");
+ }
+ return inserted;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepositorySession.java
new file mode 100644
index 0000000000..4f0da0bcc1
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepositorySession.java
@@ -0,0 +1,792 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.android;
+
+import java.util.ArrayList;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.repositories.InactiveSessionException;
+import org.mozilla.gecko.sync.repositories.InvalidRequestException;
+import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
+import org.mozilla.gecko.sync.repositories.MultipleRecordsForGuidException;
+import org.mozilla.gecko.sync.repositories.NoGuidForIdException;
+import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.ParentNotFoundException;
+import org.mozilla.gecko.sync.repositories.ProfileDatabaseException;
+import org.mozilla.gecko.sync.repositories.RecordFilter;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.StoreTrackingRepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import android.content.ContentUris;
+import android.database.Cursor;
+import android.net.Uri;
+import android.util.SparseArray;
+
+/**
+ * You'll notice that all delegate calls *either*:
+ *
+ * - request a deferred delegate with the appropriate work queue, then
+ * make the appropriate call, or
+ * - create a Runnable which makes the appropriate call, and pushes it
+ * directly into the appropriate work queue.
+ *
+ * This is to ensure that all delegate callbacks happen off the current
+ * thread. This provides lock safety (we don't enter another method that
+ * might try to take a lock already taken in our caller), and ensures
+ * that operations take place off the main thread.
+ *
+ * Don't do both -- the two approaches are equivalent -- and certainly
+ * don't do neither unless you know what you're doing!
+ *
+ * Similarly, all store calls go through the appropriate store queue. This
+ * ensures that store() and storeDone() consequences occur before-after.
+ *
+ * @author rnewman
+ *
+ */
+public abstract class AndroidBrowserRepositorySession extends StoreTrackingRepositorySession {
+ public static final String LOG_TAG = "BrowserRepoSession";
+
+ protected AndroidBrowserRepositoryDataAccessor dbHelper;
+
+ /**
+ * In order to reconcile the "same record" with two *different* GUIDs (for
+ * example, the same bookmark created by two different clients), we maintain a
+ * mapping for each local record from a "record string" to
+ * "local record GUID".
+ * <p>
+ * The "record string" above is a "record identifying unique key" produced by
+ * <code>buildRecordString</code>.
+ * <p>
+ * Since we hash each "record string", this map may produce a false positive.
+ * In this case, we search the database for a matching record explicitly using
+ * <code>findByRecordString</code>.
+ */
+ protected SparseArray<String> recordToGuid;
+
+ public AndroidBrowserRepositorySession(Repository repository) {
+ super(repository);
+ }
+
+ /**
+ * Retrieve a record from a cursor. Act as if we don't know the final contents of
+ * the record: for example, a folder's child array might change.
+ *
+ * Return null if this record should not be processed.
+ *
+ * @throws NoGuidForIdException
+ * @throws NullCursorException
+ * @throws ParentNotFoundException
+ */
+ protected abstract Record retrieveDuringStore(Cursor cur) throws NoGuidForIdException, NullCursorException, ParentNotFoundException;
+
+ /**
+ * Retrieve a record from a cursor. Ensure that the contents of the database are
+ * updated to match the record that we're constructing: for example, the children
+ * of a folder might be repositioned as we generate the folder's record.
+ *
+ * @throws NoGuidForIdException
+ * @throws NullCursorException
+ * @throws ParentNotFoundException
+ */
+ protected abstract Record retrieveDuringFetch(Cursor cur) throws NoGuidForIdException, NullCursorException, ParentNotFoundException;
+
+ /**
+ * Override this to allow records to be skipped during insertion.
+ *
+ * For example, a session subclass might skip records of an unsupported type.
+ */
+ @SuppressWarnings("static-method")
+ public boolean shouldIgnore(Record record) {
+ return false;
+ }
+
+ /**
+ * Perform any necessary transformation of a record prior to searching by
+ * any field other than GUID.
+ *
+ * Example: translating remote folder names into local names.
+ */
+ @SuppressWarnings("static-method")
+ protected void fixupRecord(Record record) {
+ return;
+ }
+
+ /**
+ * Override in subclass to implement record extension.
+ *
+ * Populate any fields of the record that are expensive to calculate,
+ * prior to reconciling.
+ *
+ * Example: computing children arrays.
+ *
+ * Return null if this record should not be processed.
+ *
+ * @param record
+ * The record to transform. Can be null.
+ * @return The transformed record. Can be null.
+ * @throws NullCursorException
+ */
+ @SuppressWarnings("static-method")
+ protected Record transformRecord(Record record) throws NullCursorException {
+ return record;
+ }
+
+ @Override
+ public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException {
+ RepositorySessionBeginDelegate deferredDelegate = delegate.deferredBeginDelegate(delegateQueue);
+ super.sharedBegin();
+
+ try {
+ // We do this check here even though it results in one extra call to the DB
+ // because if we didn't, we have to do a check on every other call since there
+ // is no way of knowing which call would be hit first.
+ checkDatabase();
+ } catch (ProfileDatabaseException e) {
+ Logger.error(LOG_TAG, "ProfileDatabaseException from begin. Fennec must be launched once until this error is fixed");
+ deferredDelegate.onBeginFailed(e);
+ return;
+ } catch (Exception e) {
+ deferredDelegate.onBeginFailed(e);
+ return;
+ }
+ storeTracker = createStoreTracker();
+ deferredDelegate.onBeginSucceeded(this);
+ }
+
+ @Override
+ public void finish(RepositorySessionFinishDelegate delegate) throws InactiveSessionException {
+ dbHelper = null;
+ recordToGuid = null;
+ super.finish(delegate);
+ }
+
+ /**
+ * Produce a "record string" (record identifying unique key).
+ *
+ * @param record
+ * the <code>Record</code> to identify.
+ * @return a <code>String</code> instance.
+ */
+ protected abstract String buildRecordString(Record record);
+
+ protected void checkDatabase() throws ProfileDatabaseException, NullCursorException {
+ Logger.debug(LOG_TAG, "BEGIN: checking database.");
+ try {
+ dbHelper.fetch(new String[] { "none" }).close();
+ Logger.debug(LOG_TAG, "END: checking database.");
+ } catch (NullPointerException e) {
+ throw new ProfileDatabaseException(e);
+ }
+ }
+
+ @Override
+ public void guidsSince(long timestamp, RepositorySessionGuidsSinceDelegate delegate) {
+ GuidsSinceRunnable command = new GuidsSinceRunnable(timestamp, delegate);
+ delegateQueue.execute(command);
+ }
+
+ class GuidsSinceRunnable implements Runnable {
+
+ private final RepositorySessionGuidsSinceDelegate delegate;
+ private final long timestamp;
+
+ public GuidsSinceRunnable(long timestamp,
+ RepositorySessionGuidsSinceDelegate delegate) {
+ this.timestamp = timestamp;
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void run() {
+ if (!isActive()) {
+ delegate.onGuidsSinceFailed(new InactiveSessionException(null));
+ return;
+ }
+
+ Cursor cur;
+ try {
+ cur = dbHelper.getGUIDsSince(timestamp);
+ } catch (Exception e) {
+ delegate.onGuidsSinceFailed(e);
+ return;
+ }
+
+ ArrayList<String> guids;
+ try {
+ if (!cur.moveToFirst()) {
+ delegate.onGuidsSinceSucceeded(new String[] {});
+ return;
+ }
+ guids = new ArrayList<String>();
+ while (!cur.isAfterLast()) {
+ guids.add(RepoUtils.getStringFromCursor(cur, "guid"));
+ cur.moveToNext();
+ }
+ } finally {
+ Logger.debug(LOG_TAG, "Closing cursor after guidsSince.");
+ cur.close();
+ }
+
+ String guidsArray[] = new String[guids.size()];
+ guids.toArray(guidsArray);
+ delegate.onGuidsSinceSucceeded(guidsArray);
+ }
+ }
+
+ @Override
+ public void fetch(String[] guids,
+ RepositorySessionFetchRecordsDelegate delegate) throws InactiveSessionException {
+ FetchRunnable command = new FetchRunnable(guids, now(), null, delegate);
+ executeDelegateCommand(command);
+ }
+
+ abstract class FetchingRunnable implements Runnable {
+ protected final RepositorySessionFetchRecordsDelegate delegate;
+
+ public FetchingRunnable(RepositorySessionFetchRecordsDelegate delegate) {
+ this.delegate = delegate;
+ }
+
+ protected void fetchFromCursor(Cursor cursor, RecordFilter filter, long end) {
+ Logger.debug(LOG_TAG, "Fetch from cursor:");
+ try {
+ try {
+ if (!cursor.moveToFirst()) {
+ delegate.onFetchCompleted(end);
+ return;
+ }
+ while (!cursor.isAfterLast()) {
+ Record r = retrieveDuringFetch(cursor);
+ if (r != null) {
+ if (filter == null || !filter.excludeRecord(r)) {
+ Logger.trace(LOG_TAG, "Processing record " + r.guid);
+ delegate.onFetchedRecord(transformRecord(r));
+ } else {
+ Logger.debug(LOG_TAG, "Skipping filtered record " + r.guid);
+ }
+ }
+ cursor.moveToNext();
+ }
+ delegate.onFetchCompleted(end);
+ } catch (NoGuidForIdException e) {
+ Logger.warn(LOG_TAG, "No GUID for ID.", e);
+ delegate.onFetchFailed(e, null);
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Exception in fetchFromCursor.", e);
+ delegate.onFetchFailed(e, null);
+ return;
+ }
+ } finally {
+ Logger.trace(LOG_TAG, "Closing cursor after fetch.");
+ cursor.close();
+ }
+ }
+ }
+
+ public class FetchRunnable extends FetchingRunnable {
+ private final String[] guids;
+ private final long end;
+ private final RecordFilter filter;
+
+ public FetchRunnable(String[] guids,
+ long end,
+ RecordFilter filter,
+ RepositorySessionFetchRecordsDelegate delegate) {
+ super(delegate);
+ this.guids = guids;
+ this.end = end;
+ this.filter = filter;
+ }
+
+ @Override
+ public void run() {
+ if (!isActive()) {
+ delegate.onFetchFailed(new InactiveSessionException(null), null);
+ return;
+ }
+
+ if (guids == null || guids.length < 1) {
+ Logger.error(LOG_TAG, "No guids sent to fetch");
+ delegate.onFetchFailed(new InvalidRequestException(null), null);
+ return;
+ }
+
+ try {
+ Cursor cursor = dbHelper.fetch(guids);
+ this.fetchFromCursor(cursor, filter, end);
+ } catch (NullCursorException e) {
+ delegate.onFetchFailed(e, null);
+ }
+ }
+ }
+
+ @Override
+ public void fetchSince(long timestamp,
+ RepositorySessionFetchRecordsDelegate delegate) {
+ if (this.storeTracker == null) {
+ throw new IllegalStateException("Store tracker not yet initialized!");
+ }
+
+ Logger.debug(LOG_TAG, "Running fetchSince(" + timestamp + ").");
+ FetchSinceRunnable command = new FetchSinceRunnable(timestamp, now(), this.storeTracker.getFilter(), delegate);
+ delegateQueue.execute(command);
+ }
+
+ class FetchSinceRunnable extends FetchingRunnable {
+ private final long since;
+ private final long end;
+ private final RecordFilter filter;
+
+ public FetchSinceRunnable(long since,
+ long end,
+ RecordFilter filter,
+ RepositorySessionFetchRecordsDelegate delegate) {
+ super(delegate);
+ this.since = since;
+ this.end = end;
+ this.filter = filter;
+ }
+
+ @Override
+ public void run() {
+ if (!isActive()) {
+ delegate.onFetchFailed(new InactiveSessionException(null), null);
+ return;
+ }
+
+ try {
+ Cursor cursor = dbHelper.fetchSince(since);
+ this.fetchFromCursor(cursor, filter, end);
+ } catch (NullCursorException e) {
+ delegate.onFetchFailed(e, null);
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void fetchAll(RepositorySessionFetchRecordsDelegate delegate) {
+ this.fetchSince(0, delegate);
+ }
+
+ protected int storeCount = 0;
+
+ @Override
+ public void store(final Record record) throws NoStoreDelegateException {
+ if (delegate == null) {
+ throw new NoStoreDelegateException();
+ }
+ if (record == null) {
+ Logger.error(LOG_TAG, "Record sent to store was null");
+ throw new IllegalArgumentException("Null record passed to AndroidBrowserRepositorySession.store().");
+ }
+
+ storeCount += 1;
+ Logger.debug(LOG_TAG, "Storing record with GUID " + record.guid + " (stored " + storeCount + " records this session).");
+
+ // Store Runnables *must* complete synchronously. It's OK, they
+ // run on a background thread.
+ Runnable command = new Runnable() {
+
+ @Override
+ public void run() {
+ if (!isActive()) {
+ Logger.warn(LOG_TAG, "AndroidBrowserRepositorySession is inactive. Store failing.");
+ delegate.onRecordStoreFailed(new InactiveSessionException(null), record.guid);
+ return;
+ }
+
+ // Check that the record is a valid type.
+ // Fennec only supports bookmarks and folders. All other types of records,
+ // including livemarks and queries, are simply ignored.
+ // See Bug 708149. This might be resolved by Fennec changing its database
+ // schema, or by Sync storing non-applied records in its own private database.
+ if (shouldIgnore(record)) {
+ Logger.debug(LOG_TAG, "Ignoring record " + record.guid);
+
+ // Don't throw: we don't want to abort the entire sync when we get a livemark!
+ // delegate.onRecordStoreFailed(new InvalidBookmarkTypeException(null));
+ return;
+ }
+
+
+ // TODO: lift these into the session.
+ // Temporary: this matches prior syncing semantics, in which only
+ // the relationship between the local and remote record is considered.
+ // In the future we'll track these two timestamps and use them to
+ // determine which records have changed, and thus process incoming
+ // records more efficiently.
+ long lastLocalRetrieval = 0; // lastSyncTimestamp?
+ long lastRemoteRetrieval = 0; // TODO: adjust for clock skew.
+ boolean remotelyModified = record.lastModified > lastRemoteRetrieval;
+
+ Record existingRecord;
+ try {
+ // GUID matching only: deleted records don't have a payload with which to search.
+ existingRecord = retrieveByGUIDDuringStore(record.guid);
+ if (record.deleted) {
+ if (existingRecord == null) {
+ // We're done. Don't bother with a callback. That can change later
+ // if we want it to.
+ trace("Incoming record " + record.guid + " is deleted, and no local version. Bye!");
+ return;
+ }
+
+ if (existingRecord.deleted) {
+ trace("Local record already deleted. Bye!");
+ return;
+ }
+
+ // Which one wins?
+ if (!remotelyModified) {
+ trace("Ignoring deleted record from the past.");
+ return;
+ }
+
+ boolean locallyModified = existingRecord.lastModified > lastLocalRetrieval;
+ if (!locallyModified) {
+ trace("Remote modified, local not. Deleting.");
+ storeRecordDeletion(record, existingRecord);
+ return;
+ }
+
+ trace("Both local and remote records have been modified.");
+ if (record.lastModified > existingRecord.lastModified) {
+ trace("Remote is newer, and deleted. Deleting local.");
+ storeRecordDeletion(record, existingRecord);
+ return;
+ }
+
+ trace("Remote is older, local is not deleted. Ignoring.");
+ return;
+ }
+ // End deletion logic.
+
+ // Now we're processing a non-deleted incoming record.
+ // Apply any changes we need in order to correctly find existing records.
+ fixupRecord(record);
+
+ if (existingRecord == null) {
+ trace("Looking up match for record " + record.guid);
+ existingRecord = findExistingRecord(record);
+ }
+
+ if (existingRecord == null) {
+ // The record is new.
+ trace("No match. Inserting.");
+ insert(record);
+ return;
+ }
+
+ // We found a local dupe.
+ trace("Incoming record " + record.guid + " dupes to local record " + existingRecord.guid);
+
+ // Populate more expensive fields prior to reconciling.
+ existingRecord = transformRecord(existingRecord);
+ Record toStore = reconcileRecords(record, existingRecord, lastRemoteRetrieval, lastLocalRetrieval);
+
+ if (toStore == null) {
+ Logger.debug(LOG_TAG, "Reconciling returned null. Not inserting a record.");
+ return;
+ }
+
+ // TODO: pass in timestamps?
+
+ // This section of code will only run if the incoming record is not
+ // marked as deleted, so we never want to just drop ours from the database:
+ // we need to upload it later.
+ // Allowing deleted items to propagate through `replace` allows normal
+ // logging and side-effects to occur, and is no more expensive than simply
+ // bumping the modified time.
+ Logger.debug(LOG_TAG, "Replacing existing " + existingRecord.guid +
+ (toStore.deleted ? " with deleted record " : " with record ") +
+ toStore.guid);
+ Record replaced = replace(toStore, existingRecord);
+
+ // Note that we don't track records here; deciding that is the job
+ // of reconcileRecords.
+ Logger.debug(LOG_TAG, "Calling delegate callback with guid " + replaced.guid +
+ "(" + replaced.androidID + ")");
+ delegate.onRecordStoreSucceeded(replaced.guid);
+ return;
+
+ } catch (MultipleRecordsForGuidException e) {
+ Logger.error(LOG_TAG, "Multiple records returned for given guid: " + record.guid);
+ delegate.onRecordStoreFailed(e, record.guid);
+ return;
+ } catch (NoGuidForIdException e) {
+ Logger.error(LOG_TAG, "Store failed for " + record.guid, e);
+ delegate.onRecordStoreFailed(e, record.guid);
+ return;
+ } catch (Exception e) {
+ Logger.error(LOG_TAG, "Store failed for " + record.guid, e);
+ delegate.onRecordStoreFailed(e, record.guid);
+ return;
+ }
+ }
+ };
+ storeWorkQueue.execute(command);
+ }
+
+ /**
+ * Process a request for deletion of a record.
+ * Neither argument will ever be null.
+ *
+ * @param record the incoming record. This will be mostly blank, given that it's a deletion.
+ * @param existingRecord the existing record. Use this to decide how to process the deletion.
+ */
+ protected void storeRecordDeletion(final Record record, final Record existingRecord) {
+ // TODO: we ought to mark the record as deleted rather than purging it,
+ // in order to support syncing to multiple destinations. Bug 722607.
+ dbHelper.purgeGuid(record.guid);
+ delegate.onRecordStoreSucceeded(record.guid);
+ }
+
+ protected void insert(Record record) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
+ Record toStore = prepareRecord(record);
+ Uri recordURI = dbHelper.insert(toStore);
+ if (recordURI == null) {
+ throw new NullCursorException(new RuntimeException("Got null URI inserting record with guid " + record.guid));
+ }
+ toStore.androidID = ContentUris.parseId(recordURI);
+
+ updateBookkeeping(toStore);
+ trackRecord(toStore);
+ delegate.onRecordStoreSucceeded(toStore.guid);
+
+ Logger.debug(LOG_TAG, "Inserted record with guid " + toStore.guid + " as androidID " + toStore.androidID);
+ }
+
+ protected Record replace(Record newRecord, Record existingRecord) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
+ Record toStore = prepareRecord(newRecord);
+
+ // newRecord should already have suitable androidID and guid.
+ dbHelper.update(existingRecord.guid, toStore);
+ updateBookkeeping(toStore);
+ Logger.debug(LOG_TAG, "replace() returning record " + toStore.guid);
+ return toStore;
+ }
+
+ /**
+ * Retrieve a record from the store by GUID, without writing unnecessarily to the
+ * database.
+ *
+ * @throws NoGuidForIdException
+ * @throws NullCursorException
+ * @throws ParentNotFoundException
+ * @throws MultipleRecordsForGuidException
+ */
+ protected Record retrieveByGUIDDuringStore(String guid) throws
+ NoGuidForIdException,
+ NullCursorException,
+ ParentNotFoundException,
+ MultipleRecordsForGuidException {
+ Cursor cursor = dbHelper.fetch(new String[] { guid });
+ try {
+ if (!cursor.moveToFirst()) {
+ return null;
+ }
+
+ Record r = retrieveDuringStore(cursor);
+
+ cursor.moveToNext();
+ if (cursor.isAfterLast()) {
+ // Got one record!
+ return r; // Not transformed.
+ }
+
+ // More than one. Oh dear.
+ throw (new MultipleRecordsForGuidException(null));
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Attempt to find an equivalent record through some means other than GUID.
+ *
+ * @param record
+ * The record for which to search.
+ * @return
+ * An equivalent Record object, or null if none is found.
+ *
+ * @throws MultipleRecordsForGuidException
+ * @throws NoGuidForIdException
+ * @throws NullCursorException
+ * @throws ParentNotFoundException
+ */
+ protected Record findExistingRecord(Record record) throws MultipleRecordsForGuidException,
+ NoGuidForIdException, NullCursorException, ParentNotFoundException {
+
+ Logger.debug(LOG_TAG, "Finding existing record for incoming record with GUID " + record.guid);
+ String recordString = buildRecordString(record);
+ if (recordString == null) {
+ Logger.debug(LOG_TAG, "No record string for incoming record " + record.guid);
+ return null;
+ }
+
+ if (Logger.LOG_PERSONAL_INFORMATION) {
+ Logger.pii(LOG_TAG, "Searching with record string " + recordString);
+ } else {
+ Logger.debug(LOG_TAG, "Searching with record string.");
+ }
+ String guid = getGuidForString(recordString);
+ if (guid == null) {
+ Logger.debug(LOG_TAG, "Failed to find existing record for " + record.guid);
+ return null;
+ }
+
+ // Our map contained a match, but it could be a false positive. Since
+ // computed record string is supposed to be a unique key, we can easily
+ // verify our positive.
+ Logger.debug(LOG_TAG, "Found one. Checking stored record.");
+ Record stored = retrieveByGUIDDuringStore(guid);
+ String storedRecordString = buildRecordString(record);
+ if (recordString.equals(storedRecordString)) {
+ Logger.debug(LOG_TAG, "Existing record matches incoming record. Returning existing record.");
+ return stored;
+ }
+
+ // Oh no, we got a false positive! (This should be *very* rare --
+ // essentially, we got a hash collision.) Search the DB for this record
+ // explicitly by hand.
+ Logger.debug(LOG_TAG, "Existing record does not match incoming record. Trying to find record by record string.");
+ return findByRecordString(recordString);
+ }
+
+ protected String getGuidForString(String recordString) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
+ if (recordToGuid == null) {
+ createRecordToGuidMap();
+ }
+ return recordToGuid.get(recordString.hashCode());
+ }
+
+ protected void createRecordToGuidMap() throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
+ Logger.info(LOG_TAG, "BEGIN: creating record -> GUID map.");
+ recordToGuid = new SparseArray<String>();
+
+ // TODO: we should be able to do this entire thing with string concatenations within SQL.
+ // Also consider whether it's better to fetch and process every record in the DB into
+ // memory, or run a query per record to do the same thing.
+ Cursor cur = dbHelper.fetchAll();
+ try {
+ if (!cur.moveToFirst()) {
+ return;
+ }
+ while (!cur.isAfterLast()) {
+ Record record = retrieveDuringStore(cur);
+ if (record != null) {
+ final String recordString = buildRecordString(record);
+ if (recordString != null) {
+ recordToGuid.put(recordString.hashCode(), record.guid);
+ }
+ }
+ cur.moveToNext();
+ }
+ } finally {
+ cur.close();
+ }
+ Logger.info(LOG_TAG, "END: creating record -> GUID map.");
+ }
+
+ /**
+ * Search the local database for a record with the same "record string".
+ * <p>
+ * We expect to do this only in the unlikely event of a hash
+ * collision, so we iterate the database completely. Since we want
+ * to include information about the parents of bookmarks, it is
+ * difficult to do better purely using the
+ * <code>ContentProvider</code> interface.
+ *
+ * @param recordString
+ * the "record string" to search for; must be n
+ * @return a <code>Record</code> with the same "record string", or
+ * <code>null</code> if none is present.
+ * @throws ParentNotFoundException
+ * @throws NullCursorException
+ * @throws NoGuidForIdException
+ */
+ protected Record findByRecordString(String recordString) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
+ Cursor cur = dbHelper.fetchAll();
+ try {
+ if (!cur.moveToFirst()) {
+ return null;
+ }
+ while (!cur.isAfterLast()) {
+ Record record = retrieveDuringStore(cur);
+ if (record != null) {
+ final String storedRecordString = buildRecordString(record);
+ if (recordString.equals(storedRecordString)) {
+ return record;
+ }
+ }
+ cur.moveToNext();
+ }
+ return null;
+ } finally {
+ cur.close();
+ }
+ }
+
+ public void putRecordToGuidMap(String recordString, String guid) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
+ if (recordString == null) {
+ return;
+ }
+
+ if (recordToGuid == null) {
+ createRecordToGuidMap();
+ }
+ recordToGuid.put(recordString.hashCode(), guid);
+ }
+
+ protected abstract Record prepareRecord(Record record);
+
+ protected void updateBookkeeping(Record record) throws NoGuidForIdException,
+ NullCursorException,
+ ParentNotFoundException {
+ putRecordToGuidMap(buildRecordString(record), record.guid);
+ }
+
+ protected WipeRunnable getWipeRunnable(RepositorySessionWipeDelegate delegate) {
+ return new WipeRunnable(delegate);
+ }
+
+ @Override
+ public void wipe(RepositorySessionWipeDelegate delegate) {
+ Runnable command = getWipeRunnable(delegate);
+ storeWorkQueue.execute(command);
+ }
+
+ class WipeRunnable implements Runnable {
+ protected RepositorySessionWipeDelegate delegate;
+
+ public WipeRunnable(RepositorySessionWipeDelegate delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void run() {
+ if (!isActive()) {
+ delegate.onWipeFailed(new InactiveSessionException(null));
+ return;
+ }
+ dbHelper.wipe();
+ delegate.onWipeSucceeded();
+ }
+ }
+
+ // For testing purposes.
+ public AndroidBrowserRepositoryDataAccessor getDBHelper() {
+ return dbHelper;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksDeletionManager.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksDeletionManager.java
new file mode 100644
index 0000000000..d8d8756f77
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksDeletionManager.java
@@ -0,0 +1,239 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.android;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
+
+/**
+ * Queue up deletions. Process them at the end.
+ *
+ * Algorithm:
+ *
+ * * Collect GUIDs as we go. For convenience we partition these into
+ * folders and non-folders.
+ *
+ * * Non-folders can be deleted in batches as we go.
+ *
+ * * At the end of the sync:
+ * * Delete all that aren't folders.
+ * * Move the remaining children of any that are folders to an "Orphans" folder.
+ * - We do this even for children that are _marked_ as deleted -- we still want
+ * to upload them, and their parent is irrelevant.
+ * * Delete all the folders.
+ *
+ * * Any outstanding records -- the ones we moved to "Orphans" -- are true orphans.
+ * These should be reuploaded (because their parent has changed), as should their
+ * new parent (because its children array has changed).
+ * We achieve the former by moving them without tracking (but we don't make any
+ * special effort here -- warning! Lurking bug!).
+ * We achieve the latter by bumping its mtime. The caller should take care of untracking it.
+ *
+ * Note that we make no particular effort to handle repositioning or reparenting:
+ * batching deletes at the end should be handled seamlessly by existing code,
+ * because the deleted records could have arrived in a batch at the end regardless.
+ *
+ * Note that this class is not thread safe. This should be fine: call it only
+ * from within a store runnable.
+ *
+ */
+public class BookmarksDeletionManager {
+ private static final String LOG_TAG = "BookmarkDelete";
+
+ private final AndroidBrowserBookmarksDataAccessor dataAccessor;
+ private RepositorySessionStoreDelegate delegate;
+
+ private final int flushThreshold;
+
+ private final HashSet<String> folders = new HashSet<String>();
+ private final HashSet<String> nonFolders = new HashSet<String>();
+ private int nonFolderCount = 0;
+
+ // Records that we need to touch once we've deleted the non-folders.
+ private HashSet<String> nonFolderParents = new HashSet<String>();
+ private HashSet<String> folderParents = new HashSet<String>();
+
+ /**
+ * Create an instance to be used for tracking deletions in a bookmarks
+ * repository session.
+ *
+ * @param dataAccessor
+ * Used to effect database changes.
+ *
+ * @param flushThreshold
+ * When this many non-folder records have been stored for deletion,
+ * an incremental flush occurs.
+ */
+ public BookmarksDeletionManager(AndroidBrowserBookmarksDataAccessor dataAccessor, int flushThreshold) {
+ this.dataAccessor = dataAccessor;
+ this.flushThreshold = flushThreshold;
+ }
+
+ /**
+ * Set the delegate to use for callbacks.
+ * If not invoked, no callbacks will be submitted.
+ *
+ * @param delegate a delegate, which should already be a delayed delegate.
+ */
+ public void setDelegate(RepositorySessionStoreDelegate delegate) {
+ this.delegate = delegate;
+ }
+
+ public void deleteRecord(String guid, boolean isFolder, String parentGUID) {
+ if (guid == null) {
+ Logger.warn(LOG_TAG, "Cannot queue deletion of record with no GUID.");
+ return;
+ }
+ Logger.debug(LOG_TAG, "Queuing deletion of " + guid);
+
+ if (isFolder) {
+ folders.add(guid);
+ if (!folders.contains(parentGUID)) {
+ // We're not going to delete its parent; will need to bump it.
+ folderParents.add(parentGUID);
+ }
+
+ nonFolderParents.remove(guid);
+ folderParents.remove(guid);
+ return;
+ }
+
+ if (!folders.contains(parentGUID)) {
+ // We're not going to delete its parent; will need to bump it.
+ nonFolderParents.add(parentGUID);
+ }
+
+ if (nonFolders.add(guid)) {
+ if (++nonFolderCount >= flushThreshold) {
+ deleteNonFolders();
+ }
+ }
+ }
+
+ /**
+ * Flush deletions that can be easily taken care of right now.
+ */
+ public void incrementalFlush() {
+ // Yes, this means we only bump when we finish, not during an incremental flush.
+ deleteNonFolders();
+ }
+
+ /**
+ * Apply all pending deletions and reset state for the next batch of stores.
+ *
+ * @param orphanDestination the ID of the folder to which orphaned children
+ * should be moved.
+ *
+ * @throws NullCursorException
+ * @return a set of IDs to untrack. Will not be null.
+ */
+ public Set<String> flushAll(long orphanDestination, long now) throws NullCursorException {
+ Logger.debug(LOG_TAG, "Doing complete flush of deleted items. Moving orphans to " + orphanDestination);
+ deleteNonFolders();
+
+ // Find out which parents *won't* be deleted, and thus need to have their
+ // modified times bumped.
+ nonFolderParents.removeAll(folders);
+
+ Logger.debug(LOG_TAG, "Bumping modified times for " + nonFolderParents.size() +
+ " parents of deleted non-folders.");
+ dataAccessor.bumpModifiedByGUID(nonFolderParents, now);
+
+ if (folders.size() > 0) {
+ final String[] folderGUIDs = folders.toArray(new String[folders.size()]);
+ final String[] folderIDs = getIDs(folderGUIDs); // Throws if any don't exist.
+ int moved = dataAccessor.moveChildren(folderIDs, orphanDestination);
+ if (moved > 0) {
+ dataAccessor.bumpModified(orphanDestination, now);
+ }
+
+ // We've deleted or moved anything that might be under these folders.
+ // Just delete them.
+ final String folderWhere = RepoUtils.computeSQLInClause(folders.size(), BrowserContract.Bookmarks.GUID);
+ dataAccessor.delete(folderWhere, folderGUIDs);
+ invokeCallbacks(delegate, folderGUIDs);
+
+ folderParents.removeAll(folders);
+ Logger.debug(LOG_TAG, "Bumping modified times for " + folderParents.size() +
+ " parents of deleted folders.");
+ dataAccessor.bumpModifiedByGUID(folderParents, now);
+
+ // Clean up.
+ folders.clear();
+ }
+
+ HashSet<String> ret = nonFolderParents;
+ ret.addAll(folderParents);
+
+ nonFolderParents = new HashSet<String>();
+ folderParents = new HashSet<String>();
+ return ret;
+ }
+
+ private String[] getIDs(String[] guids) throws NullCursorException {
+ // Convert GUIDs to numeric IDs.
+ String[] ids = new String[guids.length];
+ Map<String, Long> guidsToIDs = dataAccessor.idsForGUIDs(guids);
+ for (int i = 0; i < guids.length; ++i) {
+ String guid = guids[i];
+ Long id = guidsToIDs.get(guid);
+ if (id == null) {
+ throw new IllegalArgumentException("Can't get ID for unknown record " + guid);
+ }
+ ids[i] = id.toString();
+ }
+ return ids;
+ }
+
+ /**
+ * Flush non-folder deletions. This can be called at any time.
+ */
+ private void deleteNonFolders() {
+ if (nonFolderCount == 0) {
+ Logger.debug(LOG_TAG, "No non-folders to delete.");
+ return;
+ }
+
+ Logger.debug(LOG_TAG, "Applying deletion of " + nonFolderCount + " non-folders.");
+ final String[] nonFolderGUIDs = nonFolders.toArray(new String[nonFolderCount]);
+ final String nonFolderWhere = RepoUtils.computeSQLInClause(nonFolderCount, BrowserContract.Bookmarks.GUID);
+ dataAccessor.delete(nonFolderWhere, nonFolderGUIDs);
+
+ invokeCallbacks(delegate, nonFolderGUIDs);
+
+ // Discard these.
+ // Note that we maintain folderParents and nonFolderParents; we need them later.
+ nonFolders.clear();
+ nonFolderCount = 0;
+ }
+
+ private void invokeCallbacks(RepositorySessionStoreDelegate delegate,
+ String[] nonFolderGUIDs) {
+ if (delegate == null) {
+ return;
+ }
+ Logger.trace(LOG_TAG, "Invoking store callback for " + nonFolderGUIDs.length + " GUIDs.");
+ for (String guid : nonFolderGUIDs) {
+ delegate.onRecordStoreSucceeded(guid);
+ }
+ }
+
+ /**
+ * Clear state in case of redundancy (e.g., wipe).
+ */
+ public void clear() {
+ nonFolders.clear();
+ nonFolderCount = 0;
+ folders.clear();
+ nonFolderParents.clear();
+ folderParents.clear();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksInsertionManager.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksInsertionManager.java
new file mode 100644
index 0000000000..98670d39be
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksInsertionManager.java
@@ -0,0 +1,298 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.android;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
+
+/**
+ * Queue up insertions:
+ * <ul>
+ * <li>Folder inserts where the parent is known. Do these immediately, because
+ * they allow other records to be inserted. Requires bookkeeping updates. On
+ * insert, flush the next set.</li>
+ * <li>Regular inserts where the parent is known. These can happen whenever.
+ * Batch for speed.</li>
+ * <li>Records where the parent is not known. These can be flushed out when the
+ * parent is known, or entered as orphans. This can be a queue earlier in the
+ * process, so they don't get assigned to Unsorted. Feed into the main batch
+ * when the parent arrives.</li>
+ * </ul>
+ * <p>
+ * Deletions are always done at the end so that orphaning is minimized, and
+ * that's why we are batching folders and non-folders separately.
+ * <p>
+ * Updates are always applied as they arrive.
+ * <p>
+ * Note that this class is not thread safe. This should be fine: call it only
+ * from within a store runnable.
+ */
+public class BookmarksInsertionManager {
+ public static final String LOG_TAG = "BookmarkInsert";
+ public static boolean DEBUG = false;
+
+ protected final int flushThreshold;
+ protected final BookmarkInserter inserter;
+
+ /**
+ * Folders that have been successfully inserted.
+ */
+ private final Set<String> insertedFolders = new HashSet<String>();
+
+ /**
+ * Non-folders waiting for bulk insertion.
+ * <p>
+ * We write in insertion order to keep things easy to debug.
+ */
+ private final Set<BookmarkRecord> nonFoldersToWrite = new LinkedHashSet<BookmarkRecord>();
+
+ /**
+ * Map from parent folder GUID to child records (folders and non-folders)
+ * waiting to be enqueued after parent folder is inserted.
+ */
+ private final Map<String, Set<BookmarkRecord>> recordsWaitingForParent = new HashMap<String, Set<BookmarkRecord>>();
+
+ /**
+ * Create an instance to be used for tracking insertions in a bookmarks
+ * repository session.
+ *
+ * @param flushThreshold
+ * When this many non-folder records have been stored for insertion,
+ * an incremental flush occurs.
+ * @param insertedFolders
+ * The GUIDs of all the folders already inserted into the database.
+ * @param inserter
+ * The <code>BookmarkInsert</code> to use.
+ */
+ public BookmarksInsertionManager(int flushThreshold, Collection<String> insertedFolders, BookmarkInserter inserter) {
+ this.flushThreshold = flushThreshold;
+ this.insertedFolders.addAll(insertedFolders);
+ this.inserter = inserter;
+ }
+
+ protected void addRecordWithUnwrittenParent(BookmarkRecord record) {
+ Set<BookmarkRecord> destination = recordsWaitingForParent.get(record.parentID);
+ if (destination == null) {
+ destination = new LinkedHashSet<BookmarkRecord>();
+ recordsWaitingForParent.put(record.parentID, destination);
+ }
+ destination.add(record);
+ }
+
+ /**
+ * If <code>record</code> is a folder, insert it immediately; if it is a
+ * non-folder, enqueue it. Then do the same for any records waiting for this record.
+ *
+ * @param record
+ * the <code>BookmarkRecord</code> to enqueue.
+ */
+ protected void recursivelyEnqueueRecordAndChildren(BookmarkRecord record) {
+ if (record.isFolder()) {
+ if (!inserter.insertFolder(record)) {
+ Logger.warn(LOG_TAG, "Folder with known parent with guid " + record.parentID + " failed to insert!");
+ return;
+ }
+ Logger.debug(LOG_TAG, "Folder with known parent with guid " + record.parentID + " inserted; adding to inserted folders.");
+ insertedFolders.add(record.guid);
+ } else {
+ Logger.debug(LOG_TAG, "Non-folder has known parent with guid " + record.parentID + "; adding to insertion queue.");
+ nonFoldersToWrite.add(record);
+ }
+
+ // Now process record's children.
+ Set<BookmarkRecord> waiting = recordsWaitingForParent.remove(record.guid);
+ if (waiting == null) {
+ return;
+ }
+ for (BookmarkRecord waiter : waiting) {
+ recursivelyEnqueueRecordAndChildren(waiter);
+ }
+ }
+
+ /**
+ * Enqueue a folder.
+ *
+ * @param record
+ * the folder to enqueue.
+ */
+ protected void enqueueFolder(BookmarkRecord record) {
+ Logger.debug(LOG_TAG, "Inserting folder with guid " + record.guid);
+
+ if (!insertedFolders.contains(record.parentID)) {
+ Logger.debug(LOG_TAG, "Folder has unknown parent with guid " + record.parentID + "; keeping until we see the parent.");
+ addRecordWithUnwrittenParent(record);
+ return;
+ }
+
+ // Parent is known; add as much of the tree as this roots.
+ recursivelyEnqueueRecordAndChildren(record);
+ flushNonFoldersIfNecessary();
+ }
+
+ /**
+ * Enqueue a non-folder.
+ *
+ * @param record
+ * the non-folder to enqueue.
+ */
+ protected void enqueueNonFolder(BookmarkRecord record) {
+ Logger.debug(LOG_TAG, "Inserting non-folder with guid " + record.guid);
+
+ if (!insertedFolders.contains(record.parentID)) {
+ Logger.debug(LOG_TAG, "Non-folder has unknown parent with guid " + record.parentID + "; keeping until we see the parent.");
+ addRecordWithUnwrittenParent(record);
+ return;
+ }
+
+ // Parent is known; add to insertion queue and maybe write.
+ Logger.debug(LOG_TAG, "Non-folder has known parent with guid " + record.parentID + "; adding to insertion queue.");
+ nonFoldersToWrite.add(record);
+ flushNonFoldersIfNecessary();
+ }
+
+ /**
+ * Enqueue a bookmark record for eventual insertion.
+ *
+ * @param record
+ * the <code>BookmarkRecord</code> to enqueue.
+ */
+ public void enqueueRecord(BookmarkRecord record) {
+ if (record.isFolder()) {
+ enqueueFolder(record);
+ } else {
+ enqueueNonFolder(record);
+ }
+ if (DEBUG) {
+ dumpState();
+ }
+ }
+
+ /**
+ * Flush non-folders; empties the insertion queue entirely.
+ */
+ protected void flushNonFolders() {
+ inserter.bulkInsertNonFolders(nonFoldersToWrite); // All errors are handled in bulkInsertNonFolders.
+ nonFoldersToWrite.clear();
+ }
+
+ /**
+ * Flush non-folder insertions if there are many of them; empties the
+ * insertion queue entirely.
+ */
+ protected void flushNonFoldersIfNecessary() {
+ int num = nonFoldersToWrite.size();
+ if (num < flushThreshold) {
+ Logger.debug(LOG_TAG, "Incremental flush called with " + num + " < " + flushThreshold + " non-folders; not flushing.");
+ return;
+ }
+ Logger.debug(LOG_TAG, "Incremental flush called with " + num + " non-folders; flushing.");
+ flushNonFolders();
+ }
+
+ /**
+ * Insert all remaining folders followed by all remaining non-folders,
+ * regardless of whether parent records have been successfully inserted.
+ */
+ public void finishUp() {
+ // Iterate through all waiting records, writing the folders and collecting
+ // the non-folders for bulk insertion.
+ int numFolders = 0;
+ int numNonFolders = 0;
+ for (Set<BookmarkRecord> records : recordsWaitingForParent.values()) {
+ for (BookmarkRecord record : records) {
+ if (!record.isFolder()) {
+ numNonFolders += 1;
+ nonFoldersToWrite.add(record);
+ continue;
+ }
+
+ numFolders += 1;
+ if (!inserter.insertFolder(record)) {
+ Logger.warn(LOG_TAG, "Folder with known parent with guid " + record.parentID + " failed to insert!");
+ continue;
+ }
+
+ Logger.debug(LOG_TAG, "Folder with known parent with guid " + record.parentID + " inserted; adding to inserted folders.");
+ insertedFolders.add(record.guid);
+ }
+ }
+ recordsWaitingForParent.clear();
+ flushNonFolders();
+
+ Logger.debug(LOG_TAG, "finishUp inserted " +
+ numFolders + " folders without known parents and " +
+ numNonFolders + " non-folders without known parents.");
+ if (DEBUG) {
+ dumpState();
+ }
+ }
+
+ public void clear() {
+ this.insertedFolders.clear();
+ this.nonFoldersToWrite.clear();
+ this.recordsWaitingForParent.clear();
+ }
+
+ // For debugging.
+ public boolean isClear() {
+ return nonFoldersToWrite.isEmpty() && recordsWaitingForParent.isEmpty();
+ }
+
+ // For debugging.
+ public void dumpState() {
+ ArrayList<String> readies = new ArrayList<String>();
+ for (BookmarkRecord record : nonFoldersToWrite) {
+ readies.add(record.guid);
+ }
+ String ready = Utils.toCommaSeparatedString(new ArrayList<String>(readies));
+
+ ArrayList<String> waits = new ArrayList<String>();
+ for (Set<BookmarkRecord> recs : recordsWaitingForParent.values()) {
+ for (BookmarkRecord rec : recs) {
+ waits.add(rec.guid);
+ }
+ }
+ String waiting = Utils.toCommaSeparatedString(waits);
+ String known = Utils.toCommaSeparatedString(insertedFolders);
+
+ Logger.debug(LOG_TAG, "Q=(" + ready + "), W = (" + waiting + "), P=(" + known + ")");
+ }
+
+ public interface BookmarkInserter {
+ /**
+ * Insert a single folder.
+ * <p>
+ * All exceptions should be caught and all delegate callbacks invoked here.
+ *
+ * @param record
+ * the record to insert.
+ * @return
+ * <code>true</code> if the folder was inserted; <code>false</code> otherwise.
+ */
+ public boolean insertFolder(BookmarkRecord record);
+
+ /**
+ * Insert many non-folders. Each non-folder's parent was already present in
+ * the database before this <code>BookmarkInsertionsManager</code> was
+ * created, or had <code>insertFolder</code> called with it as argument (and
+ * possibly was not inserted).
+ * <p>
+ * All exceptions should be caught and all delegate callbacks invoked here.
+ *
+ * @param records
+ * the records to insert.
+ */
+ public void bulkInsertNonFolders(Collection<BookmarkRecord> records);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BrowserContractHelpers.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BrowserContractHelpers.java
new file mode 100644
index 0000000000..e83aea087e
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BrowserContractHelpers.java
@@ -0,0 +1,154 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.android;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.sync.setup.Constants;
+
+import android.net.Uri;
+
+public class BrowserContractHelpers extends BrowserContract {
+
+ protected static Uri withSyncAndDeletedAndProfile(Uri u) {
+ return u.buildUpon()
+ .appendQueryParameter(PARAM_PROFILE, Constants.DEFAULT_PROFILE)
+ .appendQueryParameter(PARAM_IS_SYNC, "true")
+ .appendQueryParameter(PARAM_SHOW_DELETED, "true")
+ .build();
+ }
+ protected static Uri withSyncAndProfile(Uri u) {
+ return u.buildUpon()
+ .appendQueryParameter(PARAM_PROFILE, Constants.DEFAULT_PROFILE)
+ .appendQueryParameter(PARAM_IS_SYNC, "true")
+ .build();
+ }
+
+ public static final Uri BOOKMARKS_CONTENT_URI = withSyncAndDeletedAndProfile(Bookmarks.CONTENT_URI);
+ public static final Uri BOOKMARKS_PARENTS_CONTENT_URI = withSyncAndDeletedAndProfile(Bookmarks.PARENTS_CONTENT_URI);
+ public static final Uri BOOKMARKS_POSITIONS_CONTENT_URI = withSyncAndDeletedAndProfile(Bookmarks.POSITIONS_CONTENT_URI);
+ public static final Uri HISTORY_CONTENT_URI = withSyncAndDeletedAndProfile(History.CONTENT_URI);
+ public static final Uri VISITS_CONTENT_URI = withSyncAndDeletedAndProfile(Visits.CONTENT_URI);
+ public static final Uri SCHEMA_CONTENT_URI = withSyncAndDeletedAndProfile(Schema.CONTENT_URI);
+ public static final Uri PASSWORDS_CONTENT_URI = withSyncAndDeletedAndProfile(Passwords.CONTENT_URI);
+ public static final Uri DELETED_PASSWORDS_CONTENT_URI = withSyncAndDeletedAndProfile(DeletedPasswords.CONTENT_URI);
+ public static final Uri FORM_HISTORY_CONTENT_URI = withSyncAndProfile(FormHistory.CONTENT_URI);
+ public static final Uri DELETED_FORM_HISTORY_CONTENT_URI = withSyncAndProfile(DeletedFormHistory.CONTENT_URI);
+ public static final Uri TABS_CONTENT_URI = withSyncAndProfile(Tabs.CONTENT_URI);
+ public static final Uri CLIENTS_CONTENT_URI = withSyncAndProfile(Clients.CONTENT_URI);
+ public static final Uri LOGINS_CONTENT_URI = withSyncAndProfile(Logins.CONTENT_URI);
+
+ public static final String[] PasswordColumns = new String[] {
+ Passwords.ID,
+ Passwords.HOSTNAME,
+ Passwords.HTTP_REALM,
+ Passwords.FORM_SUBMIT_URL,
+ Passwords.USERNAME_FIELD,
+ Passwords.PASSWORD_FIELD,
+ Passwords.ENCRYPTED_USERNAME,
+ Passwords.ENCRYPTED_PASSWORD,
+ Passwords.ENC_TYPE,
+ Passwords.TIME_CREATED,
+ Passwords.TIME_LAST_USED,
+ Passwords.TIME_PASSWORD_CHANGED,
+ Passwords.TIMES_USED,
+ Passwords.GUID
+ };
+
+ public static final String[] HistoryColumns = new String[] {
+ CommonColumns._ID,
+ SyncColumns.GUID,
+ SyncColumns.DATE_CREATED,
+ SyncColumns.DATE_MODIFIED,
+ SyncColumns.IS_DELETED,
+ History.TITLE,
+ History.URL,
+ History.DATE_LAST_VISITED,
+ History.VISITS
+ };
+
+ public static final String[] BookmarkColumns = new String[] {
+ CommonColumns._ID,
+ SyncColumns.GUID,
+ SyncColumns.DATE_CREATED,
+ SyncColumns.DATE_MODIFIED,
+ SyncColumns.IS_DELETED,
+ Bookmarks.TITLE,
+ Bookmarks.URL,
+ Bookmarks.TYPE,
+ Bookmarks.PARENT,
+ Bookmarks.POSITION,
+ Bookmarks.TAGS,
+ Bookmarks.DESCRIPTION,
+ Bookmarks.KEYWORD
+ };
+
+ public static final String[] FormHistoryColumns = new String[] {
+ FormHistory.ID,
+ FormHistory.GUID,
+ FormHistory.FIELD_NAME,
+ FormHistory.VALUE,
+ FormHistory.TIMES_USED,
+ FormHistory.FIRST_USED,
+ FormHistory.LAST_USED
+ };
+
+ public static final String[] DeletedColumns = new String[] {
+ BrowserContract.DeletedColumns.ID,
+ BrowserContract.DeletedColumns.GUID,
+ BrowserContract.DeletedColumns.TIME_DELETED
+ };
+
+ // Mapping from Sync types to Fennec types.
+ public static final String[] BOOKMARK_TYPE_CODE_TO_STRING = {
+ // Observe omissions: "microsummary", "item".
+ "folder", "bookmark", "separator", "livemark", "query"
+ };
+ private static final int MAX_BOOKMARK_TYPE_CODE = BOOKMARK_TYPE_CODE_TO_STRING.length - 1;
+ public static final Map<String, Integer> BOOKMARK_TYPE_STRING_TO_CODE;
+ static {
+ HashMap<String, Integer> t = new HashMap<String, Integer>();
+ t.put("folder", Bookmarks.TYPE_FOLDER);
+ t.put("bookmark", Bookmarks.TYPE_BOOKMARK);
+ t.put("separator", Bookmarks.TYPE_SEPARATOR);
+ t.put("livemark", Bookmarks.TYPE_LIVEMARK);
+ t.put("query", Bookmarks.TYPE_QUERY);
+ BOOKMARK_TYPE_STRING_TO_CODE = Collections.unmodifiableMap(t);
+ }
+
+ /**
+ * Convert a database bookmark type code into the Sync string equivalent.
+ *
+ * @param code one of the <code>Bookmarks.TYPE_*</code> enumerations.
+ * @return the string equivalent, or null if not found.
+ */
+ public static String typeStringForCode(int code) {
+ if (0 <= code && code <= MAX_BOOKMARK_TYPE_CODE) {
+ return BOOKMARK_TYPE_CODE_TO_STRING[code];
+ }
+ return null;
+ }
+
+ /**
+ * Convert a Sync type string into a Fennec type code.
+ *
+ * @param type a type string, such as "livemark".
+ * @return the type code, or -1 if not found.
+ */
+ public static int typeCodeForString(String type) {
+ Integer found = BOOKMARK_TYPE_STRING_TO_CODE.get(type);
+ if (found == null) {
+ return -1;
+ }
+ return found;
+ }
+
+ public static boolean isSupportedType(String type) {
+ return BOOKMARK_TYPE_STRING_TO_CODE.containsKey(type);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/CachedSQLiteOpenHelper.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/CachedSQLiteOpenHelper.java
new file mode 100644
index 0000000000..5c17f9b85d
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/CachedSQLiteOpenHelper.java
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.android;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+import android.database.sqlite.SQLiteOpenHelper;
+
+public abstract class CachedSQLiteOpenHelper extends SQLiteOpenHelper {
+
+ public CachedSQLiteOpenHelper(Context context, String name, CursorFactory factory,
+ int version) {
+ super(context, name, factory, version);
+ }
+
+ // Cache these so we don't have to track them across cursors. Call `close`
+ // when you're done.
+ private SQLiteDatabase readableDatabase;
+ private SQLiteDatabase writableDatabase;
+
+ synchronized protected SQLiteDatabase getCachedReadableDatabase() {
+ if (readableDatabase == null) {
+ if (writableDatabase == null) {
+ readableDatabase = this.getReadableDatabase();
+ return readableDatabase;
+ } else {
+ return writableDatabase;
+ }
+ } else {
+ return readableDatabase;
+ }
+ }
+
+ synchronized protected SQLiteDatabase getCachedWritableDatabase() {
+ if (writableDatabase == null) {
+ writableDatabase = this.getWritableDatabase();
+ }
+ return writableDatabase;
+ }
+
+ @Override
+ synchronized public void close() {
+ if (readableDatabase != null) {
+ readableDatabase.close();
+ readableDatabase = null;
+ }
+ if (writableDatabase != null) {
+ writableDatabase.close();
+ writableDatabase = null;
+ }
+ super.close();
+ }
+
+ // Used for testing.
+ public boolean isClosed() {
+ return readableDatabase == null &&
+ writableDatabase == null;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/ClientsDatabase.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/ClientsDatabase.java
new file mode 100644
index 0000000000..4962a20c68
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/ClientsDatabase.java
@@ -0,0 +1,252 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.android;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+
+public class ClientsDatabase extends CachedSQLiteOpenHelper {
+
+ public static final String LOG_TAG = "ClientsDatabase";
+
+ // Database Specifications.
+ protected static final String DB_NAME = "clients_database";
+ protected static final int SCHEMA_VERSION = 3;
+
+ // Clients Table.
+ public static final String TBL_CLIENTS = "clients";
+ public static final String COL_ACCOUNT_GUID = "guid";
+ public static final String COL_PROFILE = "profile";
+ public static final String COL_NAME = "name";
+ public static final String COL_TYPE = "device_type";
+
+ // Optional fields.
+ public static final String COL_FORMFACTOR = "formfactor";
+ public static final String COL_OS = "os";
+ public static final String COL_APPLICATION = "application";
+ public static final String COL_APP_PACKAGE = "appPackage";
+ public static final String COL_DEVICE = "device";
+
+ public static final String[] TBL_CLIENTS_COLUMNS = new String[] { COL_ACCOUNT_GUID, COL_PROFILE, COL_NAME, COL_TYPE,
+ COL_FORMFACTOR, COL_OS, COL_APPLICATION, COL_APP_PACKAGE, COL_DEVICE };
+ public static final String TBL_CLIENTS_KEY = COL_ACCOUNT_GUID + " = ? AND " +
+ COL_PROFILE + " = ?";
+
+ // Commands Table.
+ public static final String TBL_COMMANDS = "commands";
+ public static final String COL_COMMAND = "command";
+ public static final String COL_ARGS = "args";
+
+ public static final String[] TBL_COMMANDS_COLUMNS = new String[] { COL_ACCOUNT_GUID, COL_COMMAND, COL_ARGS };
+ public static final String TBL_COMMANDS_KEY = COL_ACCOUNT_GUID + " = ? AND " +
+ COL_COMMAND + " = ? AND " +
+ COL_ARGS + " = ?";
+ public static final String TBL_COMMANDS_GUID_QUERY = COL_ACCOUNT_GUID + " = ? ";
+
+ private final RepoUtils.QueryHelper queryHelper;
+
+ public ClientsDatabase(Context context) {
+ super(context, DB_NAME, null, SCHEMA_VERSION);
+ this.queryHelper = new RepoUtils.QueryHelper(context, null, LOG_TAG);
+ Logger.debug(LOG_TAG, "ClientsDatabase instantiated.");
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ Logger.debug(LOG_TAG, "ClientsDatabase.onCreate().");
+ createClientsTable(db);
+ createCommandsTable(db);
+ }
+
+ public static void createClientsTable(SQLiteDatabase db) {
+ Logger.debug(LOG_TAG, "ClientsDatabase.createClientsTable().");
+ String createClientsTableSql = "CREATE TABLE " + TBL_CLIENTS + " ("
+ + COL_ACCOUNT_GUID + " TEXT, "
+ + COL_PROFILE + " TEXT, "
+ + COL_NAME + " TEXT, "
+ + COL_TYPE + " TEXT, "
+ + COL_FORMFACTOR + " TEXT, "
+ + COL_OS + " TEXT, "
+ + COL_APPLICATION + " TEXT, "
+ + COL_APP_PACKAGE + " TEXT, "
+ + COL_DEVICE + " TEXT, "
+ + "PRIMARY KEY (" + COL_ACCOUNT_GUID + ", " + COL_PROFILE + "))";
+ db.execSQL(createClientsTableSql);
+ }
+
+ public static void createCommandsTable(SQLiteDatabase db) {
+ Logger.debug(LOG_TAG, "ClientsDatabase.createCommandsTable().");
+ String createCommandsTableSql = "CREATE TABLE " + TBL_COMMANDS + " ("
+ + COL_ACCOUNT_GUID + " TEXT, "
+ + COL_COMMAND + " TEXT, "
+ + COL_ARGS + " TEXT, "
+ + "PRIMARY KEY (" + COL_ACCOUNT_GUID + ", " + COL_COMMAND + ", " + COL_ARGS + "), "
+ + "FOREIGN KEY (" + COL_ACCOUNT_GUID + ") REFERENCES " + TBL_CLIENTS + " (" + COL_ACCOUNT_GUID + "))";
+ db.execSQL(createCommandsTableSql);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ Logger.debug(LOG_TAG, "ClientsDatabase.onUpgrade(" + oldVersion + ", " + newVersion + ").");
+ if (oldVersion < 2) {
+ // For now we'll just drop and recreate the tables.
+ db.execSQL("DROP TABLE IF EXISTS " + TBL_CLIENTS);
+ db.execSQL("DROP TABLE IF EXISTS " + TBL_COMMANDS);
+ onCreate(db);
+ return;
+ }
+
+ if (newVersion >= 3) {
+ // Add the optional columns to clients.
+ db.execSQL("ALTER TABLE " + TBL_CLIENTS + " ADD COLUMN " + COL_FORMFACTOR + " TEXT");
+ db.execSQL("ALTER TABLE " + TBL_CLIENTS + " ADD COLUMN " + COL_OS + " TEXT");
+ db.execSQL("ALTER TABLE " + TBL_CLIENTS + " ADD COLUMN " + COL_APPLICATION + " TEXT");
+ db.execSQL("ALTER TABLE " + TBL_CLIENTS + " ADD COLUMN " + COL_APP_PACKAGE + " TEXT");
+ db.execSQL("ALTER TABLE " + TBL_CLIENTS + " ADD COLUMN " + COL_DEVICE + " TEXT");
+ }
+ }
+
+ public void wipeDB() {
+ SQLiteDatabase db = this.getCachedWritableDatabase();
+ onUpgrade(db, 0, SCHEMA_VERSION);
+ }
+
+ public void wipeClientsTable() {
+ SQLiteDatabase db = this.getCachedWritableDatabase();
+ db.execSQL("DELETE FROM " + TBL_CLIENTS);
+ }
+
+ public void wipeCommandsTable() {
+ SQLiteDatabase db = this.getCachedWritableDatabase();
+ db.execSQL("DELETE FROM " + TBL_COMMANDS);
+ }
+
+ // If a record with given GUID exists, we'll update it,
+ // otherwise we'll insert it.
+ public void store(String profileId, ClientRecord record) {
+ SQLiteDatabase db = this.getCachedWritableDatabase();
+
+ ContentValues cv = new ContentValues();
+ cv.put(COL_ACCOUNT_GUID, record.guid);
+ cv.put(COL_PROFILE, profileId);
+ cv.put(COL_NAME, record.name);
+ cv.put(COL_TYPE, record.type);
+
+ if (record.formfactor != null) {
+ cv.put(COL_FORMFACTOR, record.formfactor);
+ }
+
+ if (record.os != null) {
+ cv.put(COL_OS, record.os);
+ }
+
+ if (record.application != null) {
+ cv.put(COL_APPLICATION, record.application);
+ }
+
+ if (record.appPackage != null) {
+ cv.put(COL_APP_PACKAGE, record.appPackage);
+ }
+
+ if (record.device != null) {
+ cv.put(COL_DEVICE, record.device);
+ }
+
+ String[] args = new String[] { record.guid, profileId };
+ int rowsUpdated = db.update(TBL_CLIENTS, cv, TBL_CLIENTS_KEY, args);
+
+ if (rowsUpdated >= 1) {
+ Logger.debug(LOG_TAG, "Replaced client record for row with accountGUID " + record.guid);
+ } else {
+ long rowId = db.insert(TBL_CLIENTS, null, cv);
+ Logger.debug(LOG_TAG, "Inserted client record into row: " + rowId);
+ }
+ }
+
+ /**
+ * Store a command in the commands database if it doesn't already exist.
+ *
+ * @param accountGUID
+ * @param command - The command type
+ * @param args - A JSON string of args
+ * @throws NullCursorException
+ */
+ public void store(String accountGUID, String command, String args) throws NullCursorException {
+ if (Logger.LOG_PERSONAL_INFORMATION) {
+ Logger.pii(LOG_TAG, "Storing command " + command + " with args " + args);
+ } else {
+ Logger.trace(LOG_TAG, "Storing command " + command + ".");
+ }
+ SQLiteDatabase db = this.getCachedWritableDatabase();
+
+ ContentValues cv = new ContentValues();
+ cv.put(COL_ACCOUNT_GUID, accountGUID);
+ cv.put(COL_COMMAND, command);
+ if (args == null) {
+ cv.put(COL_ARGS, "[]");
+ } else {
+ cv.put(COL_ARGS, args);
+ }
+
+ Cursor cur = this.fetchSpecificCommand(accountGUID, command, args);
+ try {
+ if (cur.moveToFirst()) {
+ Logger.debug(LOG_TAG, "Command already exists in database.");
+ return;
+ }
+ } finally {
+ cur.close();
+ }
+
+ long rowId = db.insert(TBL_COMMANDS, null, cv);
+ Logger.debug(LOG_TAG, "Inserted command into row: " + rowId);
+ }
+
+ public Cursor fetchClientsCursor(String accountGUID, String profileId) throws NullCursorException {
+ String[] args = new String[] { accountGUID, profileId };
+ SQLiteDatabase db = this.getCachedReadableDatabase();
+
+ return queryHelper.safeQuery(db, ".fetchClientsCursor", TBL_CLIENTS, TBL_CLIENTS_COLUMNS, TBL_CLIENTS_KEY, args);
+ }
+
+ public Cursor fetchSpecificCommand(String accountGUID, String command, String commandArgs) throws NullCursorException {
+ String[] args = new String[] { accountGUID, command, commandArgs };
+ SQLiteDatabase db = this.getCachedReadableDatabase();
+
+ return queryHelper.safeQuery(db, ".fetchSpecificCommand", TBL_COMMANDS, TBL_COMMANDS_COLUMNS, TBL_COMMANDS_KEY, args);
+ }
+
+ public Cursor fetchCommandsForClient(String accountGUID) throws NullCursorException {
+ String[] args = new String[] { accountGUID };
+ SQLiteDatabase db = this.getCachedReadableDatabase();
+
+ return queryHelper.safeQuery(db, ".fetchCommandsForClient", TBL_COMMANDS, TBL_COMMANDS_COLUMNS, TBL_COMMANDS_GUID_QUERY, args);
+ }
+
+ public Cursor fetchAllClients() throws NullCursorException {
+ SQLiteDatabase db = this.getCachedReadableDatabase();
+
+ return queryHelper.safeQuery(db, ".fetchAllClients", TBL_CLIENTS, TBL_CLIENTS_COLUMNS, null, null);
+ }
+
+ public Cursor fetchAllCommands() throws NullCursorException {
+ SQLiteDatabase db = this.getCachedReadableDatabase();
+
+ return queryHelper.safeQuery(db, ".fetchAllCommands", TBL_COMMANDS, TBL_COMMANDS_COLUMNS, null, null);
+ }
+
+ public void deleteClient(String accountGUID, String profileId) {
+ String[] args = new String[] { accountGUID, profileId };
+
+ SQLiteDatabase db = this.getCachedWritableDatabase();
+ db.delete(TBL_CLIENTS, TBL_CLIENTS_KEY, args);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/ClientsDatabaseAccessor.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/ClientsDatabaseAccessor.java
new file mode 100644
index 0000000000..4af84ceaf0
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/ClientsDatabaseAccessor.java
@@ -0,0 +1,178 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.android;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.json.simple.JSONArray;
+
+import org.mozilla.gecko.sync.CommandProcessor.Command;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
+import org.mozilla.gecko.sync.setup.Constants;
+
+import android.content.Context;
+import android.database.Cursor;
+
+public class ClientsDatabaseAccessor {
+
+ public static final String LOG_TAG = "ClientsDatabaseAccessor";
+
+ private ClientsDatabase db;
+
+ // Need this so we can properly stub out the class for testing.
+ public ClientsDatabaseAccessor() {}
+
+ public ClientsDatabaseAccessor(Context context) {
+ db = new ClientsDatabase(context);
+ }
+
+ public void store(ClientRecord record) {
+ db.store(getProfileId(), record);
+ }
+
+ public void store(Collection<ClientRecord> records) {
+ for (ClientRecord record : records) {
+ this.store(record);
+ }
+ }
+
+ public void store(String accountGUID, Command command) throws NullCursorException {
+ db.store(accountGUID, command.commandType, command.args.toJSONString());
+ }
+
+ public ClientRecord fetchClient(String accountGUID) throws NullCursorException {
+ final Cursor cur = db.fetchClientsCursor(accountGUID, getProfileId());
+ try {
+ if (!cur.moveToFirst()) {
+ return null;
+ }
+ return recordFromCursor(cur);
+ } finally {
+ cur.close();
+ }
+ }
+
+ public Map<String, ClientRecord> fetchAllClients() throws NullCursorException {
+ final HashMap<String, ClientRecord> map = new HashMap<String, ClientRecord>();
+ final Cursor cur = db.fetchAllClients();
+ try {
+ if (!cur.moveToFirst()) {
+ return Collections.unmodifiableMap(map);
+ }
+
+ while (!cur.isAfterLast()) {
+ ClientRecord clientRecord = recordFromCursor(cur);
+ map.put(clientRecord.guid, clientRecord);
+ cur.moveToNext();
+ }
+ return Collections.unmodifiableMap(map);
+ } finally {
+ cur.close();
+ }
+ }
+
+ public List<Command> fetchAllCommands() throws NullCursorException {
+ final List<Command> commands = new ArrayList<Command>();
+ final Cursor cur = db.fetchAllCommands();
+ try {
+ if (!cur.moveToFirst()) {
+ return Collections.unmodifiableList(commands);
+ }
+
+ while (!cur.isAfterLast()) {
+ Command command = commandFromCursor(cur);
+ commands.add(command);
+ cur.moveToNext();
+ }
+ return Collections.unmodifiableList(commands);
+ } finally {
+ cur.close();
+ }
+ }
+
+ public List<Command> fetchCommandsForClient(String accountGUID) throws NullCursorException {
+ final List<Command> commands = new ArrayList<Command>();
+ final Cursor cur = db.fetchCommandsForClient(accountGUID);
+ try {
+ if (!cur.moveToFirst()) {
+ return Collections.unmodifiableList(commands);
+ }
+
+ while(!cur.isAfterLast()) {
+ Command command = commandFromCursor(cur);
+ commands.add(command);
+ cur.moveToNext();
+ }
+ return Collections.unmodifiableList(commands);
+ } finally {
+ cur.close();
+ }
+ }
+
+ protected static ClientRecord recordFromCursor(Cursor cur) {
+ final String accountGUID = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_ACCOUNT_GUID);
+ final String clientName = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_NAME);
+ final String clientType = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_TYPE);
+
+ final ClientRecord record = new ClientRecord(accountGUID);
+ record.name = clientName;
+ record.type = clientType;
+
+ // Optional fields. These will either be null or strings.
+ record.formfactor = RepoUtils.optStringFromCursor(cur, ClientsDatabase.COL_FORMFACTOR);
+ record.os = RepoUtils.optStringFromCursor(cur, ClientsDatabase.COL_OS);
+ record.device = RepoUtils.optStringFromCursor(cur, ClientsDatabase.COL_DEVICE);
+ record.appPackage = RepoUtils.optStringFromCursor(cur, ClientsDatabase.COL_APP_PACKAGE);
+ record.application = RepoUtils.optStringFromCursor(cur, ClientsDatabase.COL_APPLICATION);
+
+ return record;
+ }
+
+ protected static Command commandFromCursor(Cursor cur) {
+ String commandType = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_COMMAND);
+ JSONArray commandArgs = RepoUtils.getJSONArrayFromCursor(cur, ClientsDatabase.COL_ARGS);
+ return new Command(commandType, commandArgs);
+ }
+
+ public int clientsCount() {
+ try {
+ final Cursor cur = db.fetchAllClients();
+ try {
+ return cur.getCount();
+ } finally {
+ cur.close();
+ }
+ } catch (NullCursorException e) {
+ return 0;
+ }
+
+ }
+
+ private String getProfileId() {
+ return Constants.DEFAULT_PROFILE;
+ }
+
+ public void wipeDB() {
+ db.wipeDB();
+ }
+
+ public void wipeClientsTable() {
+ db.wipeClientsTable();
+ }
+
+ public void wipeCommandsTable() {
+ db.wipeCommandsTable();
+ }
+
+ public void close() {
+ db.close();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FennecTabsRepository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FennecTabsRepository.java
new file mode 100644
index 0000000000..720d856eb2
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FennecTabsRepository.java
@@ -0,0 +1,383 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.android;
+
+import java.util.ArrayList;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.db.Tab;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserContract.Clients;
+import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
+import org.mozilla.gecko.sync.repositories.InactiveSessionException;
+import org.mozilla.gecko.sync.repositories.NoContentProviderException;
+import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
+import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+import org.mozilla.gecko.sync.repositories.domain.TabsRecord;
+
+import android.content.ContentProviderClient;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+
+public class FennecTabsRepository extends Repository {
+ private static final String LOG_TAG = "FennecTabsRepository";
+
+ protected final ClientsDataDelegate clientsDataDelegate;
+
+ public FennecTabsRepository(ClientsDataDelegate clientsDataDelegate) {
+ this.clientsDataDelegate = clientsDataDelegate;
+ }
+
+ /**
+ * Note that -- unlike most repositories -- this will only fetch Fennec's tabs,
+ * and only store tabs from other clients.
+ *
+ * It will never retrieve tabs from other clients, or store tabs for Fennec,
+ * unless you use {@link #fetch(String[], RepositorySessionFetchRecordsDelegate)}
+ * and specify an explicit GUID.
+ */
+ public class FennecTabsRepositorySession extends RepositorySession {
+ protected static final String LOG_TAG = "FennecTabsSession";
+
+ private final ContentProviderClient tabsProvider;
+ private final ContentProviderClient clientsProvider;
+
+ protected final RepoUtils.QueryHelper tabsHelper;
+
+ protected final ClientsDatabaseAccessor clientsDatabase;
+
+ protected ContentProviderClient getContentProvider(final Context context, final Uri uri) throws NoContentProviderException {
+ ContentProviderClient client = context.getContentResolver().acquireContentProviderClient(uri);
+ if (client == null) {
+ throw new NoContentProviderException(uri);
+ }
+ return client;
+ }
+
+ protected void releaseProviders() {
+ try {
+ clientsProvider.release();
+ } catch (Exception e) {}
+ try {
+ tabsProvider.release();
+ } catch (Exception e) {}
+ clientsDatabase.close();
+ }
+
+ public FennecTabsRepositorySession(Repository repository, Context context) throws NoContentProviderException {
+ super(repository);
+ clientsProvider = getContentProvider(context, BrowserContractHelpers.CLIENTS_CONTENT_URI);
+ try {
+ tabsProvider = getContentProvider(context, BrowserContractHelpers.TABS_CONTENT_URI);
+ } catch (NoContentProviderException e) {
+ clientsProvider.release();
+ throw e;
+ } catch (Exception e) {
+ clientsProvider.release();
+ // Oh, Java.
+ throw new RuntimeException(e);
+ }
+
+ tabsHelper = new RepoUtils.QueryHelper(context, BrowserContractHelpers.TABS_CONTENT_URI, LOG_TAG);
+ clientsDatabase = new ClientsDatabaseAccessor(context);
+ }
+
+ @Override
+ public void abort() {
+ releaseProviders();
+ super.abort();
+ }
+
+ @Override
+ public void finish(final RepositorySessionFinishDelegate delegate) throws InactiveSessionException {
+ releaseProviders();
+ super.finish(delegate);
+ }
+
+ // Default parameters for local data: local client has null GUID. Override
+ // these to test against non-live data.
+ protected String localClientSelection() {
+ return BrowserContract.Tabs.CLIENT_GUID + " IS NULL";
+ }
+
+ protected String[] localClientSelectionArgs() {
+ return null;
+ }
+
+ @Override
+ public void guidsSince(final long timestamp,
+ final RepositorySessionGuidsSinceDelegate delegate) {
+ // Bug 783692: Now that Bug 730039 has landed, we could implement this,
+ // but it's not a priority since it's not used (yet).
+ Logger.warn(LOG_TAG, "Not returning anything from guidsSince.");
+ delegateQueue.execute(new Runnable() {
+ @Override
+ public void run() {
+ delegate.onGuidsSinceSucceeded(new String[] {});
+ }
+ });
+ }
+
+ @Override
+ public void fetchSince(final long timestamp,
+ final RepositorySessionFetchRecordsDelegate delegate) {
+ if (tabsProvider == null) {
+ throw new IllegalArgumentException("tabsProvider was null.");
+ }
+ if (tabsHelper == null) {
+ throw new IllegalArgumentException("tabsHelper was null.");
+ }
+
+ final String positionAscending = BrowserContract.Tabs.POSITION + " ASC";
+
+ final String localClientSelection = localClientSelection();
+ final String[] localClientSelectionArgs = localClientSelectionArgs();
+
+ final Runnable command = new Runnable() {
+ @Override
+ public void run() {
+ // We fetch all local tabs (since the record must contain them all)
+ // but only process the record if the timestamp is sufficiently
+ // recent, or if the client data has been modified.
+ try {
+ final Cursor cursor = tabsHelper.safeQuery(tabsProvider, ".fetchSince()", null,
+ localClientSelection, localClientSelectionArgs, positionAscending);
+ try {
+ final String localClientGuid = clientsDataDelegate.getAccountGUID();
+ final String localClientName = clientsDataDelegate.getClientName();
+ final TabsRecord tabsRecord = FennecTabsRepository.tabsRecordFromCursor(cursor, localClientGuid, localClientName);
+
+ if (tabsRecord.lastModified >= timestamp ||
+ clientsDataDelegate.getLastModifiedTimestamp() >= timestamp) {
+ delegate.onFetchedRecord(tabsRecord);
+ }
+ } finally {
+ cursor.close();
+ }
+ } catch (Exception e) {
+ delegate.onFetchFailed(e, null);
+ return;
+ }
+ delegate.onFetchCompleted(now());
+ }
+ };
+
+ delegateQueue.execute(command);
+ }
+
+ @Override
+ public void fetch(final String[] guids,
+ final RepositorySessionFetchRecordsDelegate delegate) {
+ // Bug 783692: Now that Bug 730039 has landed, we could implement this,
+ // but it's not a priority since it's not used (yet).
+ Logger.warn(LOG_TAG, "Not returning anything from fetch");
+ delegateQueue.execute(new Runnable() {
+ @Override
+ public void run() {
+ delegate.onFetchCompleted(now());
+ }
+ });
+ }
+
+ @Override
+ public void fetchAll(final RepositorySessionFetchRecordsDelegate delegate) {
+ fetchSince(0, delegate);
+ }
+
+ private static final String TABS_CLIENT_GUID_IS = BrowserContract.Tabs.CLIENT_GUID + " = ?";
+ private static final String CLIENT_GUID_IS = BrowserContract.Clients.GUID + " = ?";
+
+ @Override
+ public void store(final Record record) throws NoStoreDelegateException {
+ if (delegate == null) {
+ Logger.warn(LOG_TAG, "No store delegate.");
+ throw new NoStoreDelegateException();
+ }
+ if (record == null) {
+ Logger.error(LOG_TAG, "Record sent to store was null");
+ throw new IllegalArgumentException("Null record passed to FennecTabsRepositorySession.store().");
+ }
+ if (!(record instanceof TabsRecord)) {
+ Logger.error(LOG_TAG, "Can't store anything but a TabsRecord");
+ throw new IllegalArgumentException("Non-TabsRecord passed to FennecTabsRepositorySession.store().");
+ }
+ final TabsRecord tabsRecord = (TabsRecord) record;
+
+ Runnable command = new Runnable() {
+ @Override
+ public void run() {
+ Logger.debug(LOG_TAG, "Storing tabs for client " + tabsRecord.guid);
+ if (!isActive()) {
+ delegate.onRecordStoreFailed(new InactiveSessionException(null), record.guid);
+ return;
+ }
+ if (tabsRecord.guid == null) {
+ delegate.onRecordStoreFailed(new RuntimeException("Can't store record with null GUID."), record.guid);
+ return;
+ }
+
+ try {
+ // This is nice and easy: we *always* store.
+ final String[] selectionArgs = new String[] { tabsRecord.guid };
+ if (tabsRecord.deleted) {
+ try {
+ Logger.debug(LOG_TAG, "Clearing entry for client " + tabsRecord.guid);
+ clientsProvider.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI,
+ CLIENT_GUID_IS,
+ selectionArgs);
+ delegate.onRecordStoreSucceeded(record.guid);
+ } catch (Exception e) {
+ delegate.onRecordStoreFailed(e, record.guid);
+ }
+ return;
+ }
+
+ // If it exists, update the client record; otherwise insert.
+ final ContentValues clientsCV = tabsRecord.getClientsContentValues();
+
+ final ClientRecord clientRecord = clientsDatabase.fetchClient(tabsRecord.guid);
+ if (null != clientRecord) {
+ // Null is an acceptable device type.
+ clientsCV.put(Clients.DEVICE_TYPE, clientRecord.type);
+ }
+
+ Logger.debug(LOG_TAG, "Updating clients provider.");
+ final int updated = clientsProvider.update(BrowserContractHelpers.CLIENTS_CONTENT_URI,
+ clientsCV,
+ CLIENT_GUID_IS,
+ selectionArgs);
+ if (0 == updated) {
+ clientsProvider.insert(BrowserContractHelpers.CLIENTS_CONTENT_URI, clientsCV);
+ }
+
+ // Now insert tabs.
+ final ContentValues[] tabsArray = tabsRecord.getTabsContentValues();
+ Logger.debug(LOG_TAG, "Inserting " + tabsArray.length + " tabs for client " + tabsRecord.guid);
+
+ tabsProvider.delete(BrowserContractHelpers.TABS_CONTENT_URI, TABS_CLIENT_GUID_IS, selectionArgs);
+ final int inserted = tabsProvider.bulkInsert(BrowserContractHelpers.TABS_CONTENT_URI, tabsArray);
+ Logger.trace(LOG_TAG, "Inserted: " + inserted);
+
+ delegate.onRecordStoreSucceeded(record.guid);
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Error storing tabs.", e);
+ delegate.onRecordStoreFailed(e, record.guid);
+ }
+ }
+ };
+
+ storeWorkQueue.execute(command);
+ }
+
+ @Override
+ public void wipe(RepositorySessionWipeDelegate delegate) {
+ try {
+ tabsProvider.delete(BrowserContractHelpers.TABS_CONTENT_URI, null, null);
+ clientsProvider.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, null, null);
+ } catch (RemoteException e) {
+ Logger.warn(LOG_TAG, "Got RemoteException in wipe.", e);
+ delegate.onWipeFailed(e);
+ return;
+ }
+ delegate.onWipeSucceeded();
+ }
+ }
+
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ try {
+ final FennecTabsRepositorySession session = new FennecTabsRepositorySession(this, context);
+ delegate.onSessionCreated(session);
+ } catch (Exception e) {
+ delegate.onSessionCreateFailed(e);
+ }
+ }
+
+ /**
+ * Extract a <code>TabsRecord</code> from a cursor.
+ * <p>
+ * Caller is responsible for creating and closing cursor. Each row of the
+ * cursor should be an individual tab record.
+ * <p>
+ * The extracted tabs record has the given client GUID and client name.
+ *
+ * @param cursor
+ * to inspect.
+ * @param clientGuid
+ * returned tabs record will have this client GUID.
+ * @param clientName
+ * returned tabs record will have this client name.
+ * @return <code>TabsRecord</code> instance.
+ */
+ public static TabsRecord tabsRecordFromCursor(final Cursor cursor, final String clientGuid, final String clientName) {
+ final String collection = "tabs";
+ final TabsRecord record = new TabsRecord(clientGuid, collection, 0, false);
+ record.tabs = new ArrayList<Tab>();
+ record.clientName = clientName;
+
+ record.androidID = -1;
+ record.deleted = false;
+
+ record.lastModified = 0;
+
+ int position = cursor.getPosition();
+ try {
+ cursor.moveToFirst();
+ while (!cursor.isAfterLast()) {
+ final Tab tab = Tab.fromCursor(cursor);
+ record.tabs.add(tab);
+
+ if (tab.lastUsed > record.lastModified) {
+ record.lastModified = tab.lastUsed;
+ }
+
+ cursor.moveToNext();
+ }
+ } finally {
+ cursor.moveToPosition(position);
+ }
+
+ return record;
+ }
+
+ /**
+ * Deletes all non-local clients and their associated remote tabs.
+ */
+ public static void deleteNonLocalClientsAndTabs(Context context) {
+ final String nonLocalClientSelection = BrowserContract.Clients.GUID + " IS NOT NULL";
+
+ ContentProviderClient clientsProvider = context.getContentResolver()
+ .acquireContentProviderClient(BrowserContractHelpers.CLIENTS_CONTENT_URI);
+ if (clientsProvider == null) {
+ Logger.warn(LOG_TAG, "Unable to create clientsProvider!");
+ return;
+ }
+
+ try {
+ Logger.info(LOG_TAG, "Clearing all non-local clients and their associated remote tabs for default profile.");
+ clientsProvider.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, nonLocalClientSelection, null);
+ } catch (RemoteException e) {
+ Logger.warn(LOG_TAG, "Error while deleting", e);
+ } finally {
+ try {
+ clientsProvider.release();
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Got exception releasing clientsProvider!", e);
+ }
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FormHistoryRepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FormHistoryRepositorySession.java
new file mode 100644
index 0000000000..9beafa712e
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FormHistoryRepositorySession.java
@@ -0,0 +1,723 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.android;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserContract.DeletedFormHistory;
+import org.mozilla.gecko.db.BrowserContract.FormHistory;
+import org.mozilla.gecko.sync.repositories.InactiveSessionException;
+import org.mozilla.gecko.sync.repositories.NoContentProviderException;
+import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.RecordFilter;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.StoreTrackingRepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
+import org.mozilla.gecko.sync.repositories.domain.FormHistoryRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import android.content.ContentProviderClient;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+
+public class FormHistoryRepositorySession extends
+ StoreTrackingRepositorySession {
+ public static final String LOG_TAG = "FormHistoryRepoSess";
+
+ /**
+ * Number of records to insert in one batch.
+ */
+ public static final int INSERT_ITEM_THRESHOLD = 200;
+
+ private static final Uri FORM_HISTORY_CONTENT_URI = BrowserContractHelpers.FORM_HISTORY_CONTENT_URI;
+ private static final Uri DELETED_FORM_HISTORY_CONTENT_URI = BrowserContractHelpers.DELETED_FORM_HISTORY_CONTENT_URI;
+
+ public static class FormHistoryRepository extends Repository {
+
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ try {
+ final FormHistoryRepositorySession session = new FormHistoryRepositorySession(this, context);
+ delegate.onSessionCreated(session);
+ } catch (Exception e) {
+ delegate.onSessionCreateFailed(e);
+ }
+ }
+ }
+
+ protected final ContentProviderClient formsProvider;
+ protected final RepoUtils.QueryHelper regularHelper;
+ protected final RepoUtils.QueryHelper deletedHelper;
+
+ /**
+ * Acquire the content provider client.
+ * <p>
+ * The caller is responsible for releasing the client.
+ *
+ * @param context The application context.
+ * @return The <code>ContentProviderClient</code>.
+ * @throws NoContentProviderException
+ */
+ public static ContentProviderClient acquireContentProvider(final Context context)
+ throws NoContentProviderException {
+ Uri uri = BrowserContract.FORM_HISTORY_AUTHORITY_URI;
+ ContentProviderClient client = context.getContentResolver().acquireContentProviderClient(uri);
+ if (client == null) {
+ throw new NoContentProviderException(uri);
+ }
+ return client;
+ }
+
+ protected void releaseProviders() {
+ try {
+ if (formsProvider != null) {
+ formsProvider.release();
+ }
+ } catch (Exception e) {
+ }
+ }
+
+ // Only used for testing.
+ public ContentProviderClient getFormsProvider() {
+ return formsProvider;
+ }
+
+ public FormHistoryRepositorySession(Repository repository, Context context)
+ throws NoContentProviderException {
+ super(repository);
+ formsProvider = acquireContentProvider(context);
+ regularHelper = new RepoUtils.QueryHelper(context, BrowserContractHelpers.FORM_HISTORY_CONTENT_URI, LOG_TAG);
+ deletedHelper = new RepoUtils.QueryHelper(context, BrowserContractHelpers.DELETED_FORM_HISTORY_CONTENT_URI, LOG_TAG);
+ }
+
+ @Override
+ public void abort() {
+ releaseProviders();
+ super.abort();
+ }
+
+ @Override
+ public void finish(final RepositorySessionFinishDelegate delegate)
+ throws InactiveSessionException {
+ releaseProviders();
+ super.finish(delegate);
+ }
+
+ protected static final String[] GUID_COLUMNS = new String[] { FormHistory.GUID };
+
+ @Override
+ public void guidsSince(final long timestamp, final RepositorySessionGuidsSinceDelegate delegate) {
+ Runnable command = new Runnable() {
+ @Override
+ public void run() {
+ if (!isActive()) {
+ delegate.onGuidsSinceFailed(new InactiveSessionException(null));
+ return;
+ }
+
+ ArrayList<String> guids = new ArrayList<String>();
+
+ final long sharedEnd = now();
+ Cursor cur = null;
+ try {
+ cur = regularHelper.safeQuery(formsProvider, "", GUID_COLUMNS, regularBetween(timestamp, sharedEnd), null, null);
+ cur.moveToFirst();
+ while (!cur.isAfterLast()) {
+ guids.add(cur.getString(0));
+ cur.moveToNext();
+ }
+ } catch (RemoteException | NullCursorException e) {
+ delegate.onGuidsSinceFailed(e);
+ return;
+ } finally {
+ if (cur != null) {
+ cur.close();
+ }
+ }
+
+ try {
+ cur = deletedHelper.safeQuery(formsProvider, "", GUID_COLUMNS, deletedBetween(timestamp, sharedEnd), null, null);
+ cur.moveToFirst();
+ while (!cur.isAfterLast()) {
+ guids.add(cur.getString(0));
+ cur.moveToNext();
+ }
+ } catch (RemoteException | NullCursorException e) {
+ delegate.onGuidsSinceFailed(e);
+ return;
+ } finally {
+ if (cur != null) {
+ cur.close();
+ }
+ }
+
+ String guidsArray[] = guids.toArray(new String[guids.size()]);
+ delegate.onGuidsSinceSucceeded(guidsArray);
+ }
+ };
+ delegateQueue.execute(command);
+ }
+
+ protected static FormHistoryRecord retrieveDuringFetch(final Cursor cursor) {
+ // A simple and efficient way to distinguish two tables.
+ if (cursor.getColumnCount() == BrowserContractHelpers.FormHistoryColumns.length) {
+ return formHistoryRecordFromCursor(cursor);
+ } else {
+ return deletedFormHistoryRecordFromCursor(cursor);
+ }
+ }
+
+ protected static FormHistoryRecord formHistoryRecordFromCursor(final Cursor cursor) {
+ String guid = RepoUtils.getStringFromCursor(cursor, FormHistory.GUID);
+ String collection = "forms";
+ FormHistoryRecord record = new FormHistoryRecord(guid, collection, 0, false);
+
+ record.fieldName = RepoUtils.getStringFromCursor(cursor, FormHistory.FIELD_NAME);
+ record.fieldValue = RepoUtils.getStringFromCursor(cursor, FormHistory.VALUE);
+ record.androidID = RepoUtils.getLongFromCursor(cursor, FormHistory.ID);
+ record.lastModified = RepoUtils.getLongFromCursor(cursor, FormHistory.FIRST_USED) / 1000; // Convert microseconds to milliseconds.
+ record.deleted = false;
+
+ record.log(LOG_TAG);
+ return record;
+ }
+
+ protected static FormHistoryRecord deletedFormHistoryRecordFromCursor(final Cursor cursor) {
+ String guid = RepoUtils.getStringFromCursor(cursor, DeletedFormHistory.GUID);
+ String collection = "forms";
+ FormHistoryRecord record = new FormHistoryRecord(guid, collection, 0, false);
+
+ record.guid = RepoUtils.getStringFromCursor(cursor, DeletedFormHistory.GUID);
+ record.androidID = RepoUtils.getLongFromCursor(cursor, DeletedFormHistory.ID);
+ record.lastModified = RepoUtils.getLongFromCursor(cursor, DeletedFormHistory.TIME_DELETED);
+ record.deleted = true;
+
+ record.log(LOG_TAG);
+ return record;
+ }
+
+ protected static void fetchFromCursor(final Cursor cursor, final RecordFilter filter, final RepositorySessionFetchRecordsDelegate delegate)
+ throws NullCursorException {
+ Logger.debug(LOG_TAG, "Fetch from cursor");
+ if (cursor == null) {
+ throw new NullCursorException(null);
+ }
+ try {
+ if (!cursor.moveToFirst()) {
+ return;
+ }
+ while (!cursor.isAfterLast()) {
+ Record r = retrieveDuringFetch(cursor);
+ if (r != null) {
+ if (filter == null || !filter.excludeRecord(r)) {
+ Logger.trace(LOG_TAG, "Processing record " + r.guid);
+ delegate.onFetchedRecord(r);
+ } else {
+ Logger.debug(LOG_TAG, "Skipping filtered record " + r.guid);
+ }
+ }
+ cursor.moveToNext();
+ }
+ } finally {
+ Logger.trace(LOG_TAG, "Closing cursor after fetch.");
+ cursor.close();
+ }
+ }
+
+ protected void fetchHelper(final RepositorySessionFetchRecordsDelegate delegate, final long end, final List<Callable<Cursor>> cursorCallables) {
+ if (this.storeTracker == null) {
+ throw new IllegalStateException("Store tracker not yet initialized!");
+ }
+
+ final RecordFilter filter = this.storeTracker.getFilter();
+
+ Runnable command = new Runnable() {
+ @Override
+ public void run() {
+ if (!isActive()) {
+ delegate.onFetchFailed(new InactiveSessionException(null), null);
+ return;
+ }
+
+ for (Callable<Cursor> cursorCallable : cursorCallables) {
+ Cursor cursor = null;
+ try {
+ cursor = cursorCallable.call();
+ fetchFromCursor(cursor, filter, delegate); // Closes cursor.
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Exception during fetchHelper", e);
+ delegate.onFetchFailed(e, null);
+ return;
+ }
+ }
+
+ delegate.onFetchCompleted(end);
+ }
+ };
+
+ delegateQueue.execute(command);
+ }
+
+ protected static String regularBetween(long start, long end) {
+ return FormHistory.FIRST_USED + " >= " + Long.toString(1000 * start) + " AND " +
+ FormHistory.FIRST_USED + " <= " + Long.toString(1000 * end); // Microseconds.
+ }
+
+ protected static String deletedBetween(long start, long end) {
+ return DeletedFormHistory.TIME_DELETED + " >= " + Long.toString(start) + " AND " +
+ DeletedFormHistory.TIME_DELETED + " <= " + Long.toString(end); // Milliseconds.
+ }
+
+ @Override
+ public void fetchSince(final long timestamp, final RepositorySessionFetchRecordsDelegate delegate) {
+ Logger.trace(LOG_TAG, "Running fetchSince(" + timestamp + ").");
+
+ /*
+ * We need to be careful about the timestamp we complete the fetch with. If
+ * the first cursor Callable takes a year, then the second could return
+ * records long after the first was kicked off. To protect against this, we
+ * set an end point and bound our search.
+ */
+ final long sharedEnd = now();
+
+ Callable<Cursor> regularCallable = new Callable<Cursor>() {
+ @Override
+ public Cursor call() throws Exception {
+ return regularHelper.safeQuery(formsProvider, ".fetchSince(regular)", null, regularBetween(timestamp, sharedEnd), null, null);
+ }
+ };
+
+ Callable<Cursor> deletedCallable = new Callable<Cursor>() {
+ @Override
+ public Cursor call() throws Exception {
+ return deletedHelper.safeQuery(formsProvider, ".fetchSince(deleted)", null, deletedBetween(timestamp, sharedEnd), null, null);
+ }
+ };
+
+ @SuppressWarnings("unchecked")
+ List<Callable<Cursor>> callableCursors = Arrays.asList(regularCallable, deletedCallable);
+
+ fetchHelper(delegate, sharedEnd, callableCursors);
+ }
+
+ @Override
+ public void fetchAll(RepositorySessionFetchRecordsDelegate delegate) {
+ Logger.trace(LOG_TAG, "Running fetchAll.");
+ fetchSince(0, delegate);
+ }
+
+ @Override
+ public void fetch(final String[] guids, final RepositorySessionFetchRecordsDelegate delegate) {
+ Logger.trace(LOG_TAG, "Running fetch.");
+
+ final long sharedEnd = now();
+ final String where = RepoUtils.computeSQLInClause(guids.length, FormHistory.GUID);
+
+ Callable<Cursor> regularCallable = new Callable<Cursor>() {
+ @Override
+ public Cursor call() throws Exception {
+ String regularWhere = where + " AND " + FormHistory.FIRST_USED + " <= " + Long.toString(1000 * sharedEnd); // Microseconds.
+ return regularHelper.safeQuery(formsProvider, ".fetch(regular)", null, regularWhere, guids, null);
+ }
+ };
+
+ Callable<Cursor> deletedCallable = new Callable<Cursor>() {
+ @Override
+ public Cursor call() throws Exception {
+ String deletedWhere = where + " AND " + DeletedFormHistory.TIME_DELETED + " <= " + Long.toString(sharedEnd); // Milliseconds.
+ return deletedHelper.safeQuery(formsProvider, ".fetch(deleted)", null, deletedWhere, guids, null);
+ }
+ };
+
+ @SuppressWarnings("unchecked")
+ List<Callable<Cursor>> callableCursors = Arrays.asList(regularCallable, deletedCallable);
+
+ fetchHelper(delegate, sharedEnd, callableCursors);
+ }
+
+ protected static final String GUID_IS = FormHistory.GUID + " = ?";
+
+ protected Record findExistingRecordByGuid(String guid)
+ throws RemoteException, NullCursorException {
+ Cursor cursor = null;
+ try {
+ cursor = regularHelper.safeQuery(formsProvider, ".findExistingRecordByGuid(regular)",
+ null, GUID_IS, new String[] { guid }, null);
+ if (cursor.moveToFirst()) {
+ return formHistoryRecordFromCursor(cursor);
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ try {
+ cursor = deletedHelper.safeQuery(formsProvider, ".findExistingRecordByGuid(deleted)",
+ null, GUID_IS, new String[] { guid }, null);
+ if (cursor.moveToFirst()) {
+ return deletedFormHistoryRecordFromCursor(cursor);
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ return null;
+ }
+
+ protected Record findExistingRecordByPayload(Record rawRecord)
+ throws RemoteException, NullCursorException {
+ if (!rawRecord.deleted) {
+ FormHistoryRecord record = (FormHistoryRecord) rawRecord;
+ Cursor cursor = null;
+ try {
+ String where = FormHistory.FIELD_NAME + " = ? AND " + FormHistory.VALUE + " = ?";
+ cursor = regularHelper.safeQuery(formsProvider, ".findExistingRecordByPayload",
+ null, where, new String[] { record.fieldName, record.fieldValue }, null);
+ if (cursor.moveToFirst()) {
+ return formHistoryRecordFromCursor(cursor);
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Called when a record with locally known GUID has been reported deleted by
+ * the server.
+ * <p>
+ * We purge the record's GUID from the regular and deleted tables.
+ *
+ * @param existingRecord
+ * The local <code>Record</code> to replace.
+ * @throws RemoteException
+ */
+ protected void deleteExistingRecord(Record existingRecord) throws RemoteException {
+ if (existingRecord.deleted) {
+ formsProvider.delete(DELETED_FORM_HISTORY_CONTENT_URI, GUID_IS, new String[] { existingRecord.guid });
+ return;
+ }
+ formsProvider.delete(FORM_HISTORY_CONTENT_URI, GUID_IS, new String[] { existingRecord.guid });
+ }
+
+ protected static ContentValues contentValuesForRegularRecord(Record rawRecord) {
+ if (rawRecord.deleted) {
+ throw new IllegalArgumentException("Deleted record passed to insertNewRegularRecord.");
+ }
+
+ FormHistoryRecord record = (FormHistoryRecord) rawRecord;
+ ContentValues cv = new ContentValues();
+ cv.put(FormHistory.GUID, record.guid);
+ cv.put(FormHistory.FIELD_NAME, record.fieldName);
+ cv.put(FormHistory.VALUE, record.fieldValue);
+ cv.put(FormHistory.FIRST_USED, 1000 * record.lastModified); // Microseconds.
+ return cv;
+ }
+
+ protected final Object recordsBufferMonitor = new Object();
+ protected ArrayList<ContentValues> recordsBuffer = new ArrayList<ContentValues>();
+
+ protected void enqueueRegularRecord(Record record) {
+ synchronized (recordsBufferMonitor) {
+ if (recordsBuffer.size() >= INSERT_ITEM_THRESHOLD) {
+ // Insert the existing contents, then enqueue.
+ try {
+ flushInsertQueue();
+ } catch (Exception e) {
+ delegate.onRecordStoreFailed(e, record.guid);
+ return;
+ }
+ }
+ // Store the ContentValues, rather than the record.
+ recordsBuffer.add(contentValuesForRegularRecord(record));
+ }
+ }
+
+ // Should always be called from storeWorkQueue.
+ protected void flushInsertQueue() throws RemoteException {
+ synchronized (recordsBufferMonitor) {
+ if (recordsBuffer.size() > 0) {
+ final ContentValues[] outgoing = recordsBuffer.toArray(new ContentValues[recordsBuffer.size()]);
+ recordsBuffer = new ArrayList<ContentValues>();
+
+ if (outgoing == null || outgoing.length == 0) {
+ Logger.debug(LOG_TAG, "No form history items to insert; returning immediately.");
+ return;
+ }
+
+ long before = System.currentTimeMillis();
+ formsProvider.bulkInsert(FORM_HISTORY_CONTENT_URI, outgoing);
+ long after = System.currentTimeMillis();
+ Logger.debug(LOG_TAG, "Inserted " + outgoing.length + " form history items in (" + (after - before) + " milliseconds).");
+ }
+ }
+ }
+
+ @Override
+ public void storeDone() {
+ Runnable command = new Runnable() {
+ @Override
+ public void run() {
+ Logger.debug(LOG_TAG, "Checking for residual form history items to insert.");
+ try {
+ synchronized (recordsBufferMonitor) {
+ flushInsertQueue();
+ }
+ storeDone(now());
+ } catch (Exception e) {
+ // XXX TODO
+ delegate.onRecordStoreFailed(e, null);
+ }
+ }
+ };
+ storeWorkQueue.execute(command);
+ }
+
+ /**
+ * Called when a regular record with locally unknown GUID has been fetched
+ * from the server.
+ * <p>
+ * Since the record is regular, we insert it into the regular table.
+ *
+ * @param record The regular <code>Record</code> from the server.
+ * @throws RemoteException
+ */
+ protected void insertNewRegularRecord(Record record)
+ throws RemoteException {
+ enqueueRegularRecord(record);
+ }
+
+ /**
+ * Called when a regular record with has been fetched from the server and
+ * should replace an existing record.
+ * <p>
+ * We delete the existing record entirely, and then insert the new record into
+ * the regular table.
+ *
+ * @param toStore
+ * The regular <code>Record</code> from the server.
+ * @param existingRecord
+ * The local <code>Record</code> to replace.
+ * @throws RemoteException
+ */
+ protected void replaceExistingRecordWithRegularRecord(Record toStore, Record existingRecord)
+ throws RemoteException {
+ if (existingRecord.deleted) {
+ // Need two database operations -- purge from deleted table, insert into regular table.
+ deleteExistingRecord(existingRecord);
+ insertNewRegularRecord(toStore);
+ return;
+ }
+
+ final ContentValues cv = contentValuesForRegularRecord(toStore);
+ int updated = formsProvider.update(FORM_HISTORY_CONTENT_URI, cv, GUID_IS, new String[] { existingRecord.guid });
+ if (updated != 1) {
+ Logger.warn(LOG_TAG, "Expected to update 1 record with guid " + existingRecord.guid + " but updated " + updated + " records.");
+ }
+ }
+
+ @Override
+ public void store(Record rawRecord) throws NoStoreDelegateException {
+ if (delegate == null) {
+ Logger.warn(LOG_TAG, "No store delegate.");
+ throw new NoStoreDelegateException();
+ }
+ if (rawRecord == null) {
+ Logger.error(LOG_TAG, "Record sent to store was null");
+ throw new IllegalArgumentException("Null record passed to FormHistoryRepositorySession.store().");
+ }
+ if (!(rawRecord instanceof FormHistoryRecord)) {
+ Logger.error(LOG_TAG, "Can't store anything but a FormHistoryRecord");
+ throw new IllegalArgumentException("Non-FormHistoryRecord passed to FormHistoryRepositorySession.store().");
+ }
+ final FormHistoryRecord record = (FormHistoryRecord) rawRecord;
+
+ Runnable command = new Runnable() {
+ @Override
+ public void run() {
+ if (!isActive()) {
+ Logger.warn(LOG_TAG, "FormHistoryRepositorySession is inactive. Store failing.");
+ delegate.onRecordStoreFailed(new InactiveSessionException(null), record.guid);
+ return;
+ }
+
+ // TODO: lift these into the session.
+ // Temporary: this matches prior syncing semantics, in which only
+ // the relationship between the local and remote record is considered.
+ // In the future we'll track these two timestamps and use them to
+ // determine which records have changed, and thus process incoming
+ // records more efficiently.
+ long lastLocalRetrieval = 0; // lastSyncTimestamp?
+ long lastRemoteRetrieval = 0; // TODO: adjust for clock skew.
+ boolean remotelyModified = record.lastModified > lastRemoteRetrieval;
+
+ Record existingRecord;
+ try {
+ // GUID matching only: deleted records don't have a payload with which to search.
+ existingRecord = findExistingRecordByGuid(record.guid);
+ if (record.deleted) {
+ if (existingRecord == null) {
+ // We're done. Don't bother with a callback. That can change later
+ // if we want it to.
+ Logger.trace(LOG_TAG, "Incoming record " + record.guid + " is deleted, and no local version. Bye!");
+ return;
+ }
+
+ if (existingRecord.deleted) {
+ Logger.trace(LOG_TAG, "Local record already deleted. Purging local.");
+ deleteExistingRecord(existingRecord);
+ return;
+ }
+
+ // Which one wins?
+ if (!remotelyModified) {
+ Logger.trace(LOG_TAG, "Ignoring deleted record from the past.");
+ return;
+ }
+
+ boolean locallyModified = existingRecord.lastModified > lastLocalRetrieval;
+ if (!locallyModified) {
+ Logger.trace(LOG_TAG, "Remote modified, local not. Deleting.");
+ deleteExistingRecord(existingRecord);
+ trackRecord(record);
+ delegate.onRecordStoreSucceeded(record.guid);
+ return;
+ }
+
+ Logger.trace(LOG_TAG, "Both local and remote records have been modified.");
+ if (record.lastModified > existingRecord.lastModified) {
+ Logger.trace(LOG_TAG, "Remote is newer, and deleted. Purging local.");
+ deleteExistingRecord(existingRecord);
+ trackRecord(record);
+ delegate.onRecordStoreSucceeded(record.guid);
+ return;
+ }
+
+ Logger.trace(LOG_TAG, "Remote is older, local is not deleted. Ignoring.");
+ if (!locallyModified) {
+ Logger.warn(LOG_TAG, "Inconsistency: old remote record is deleted, but local record not modified!");
+ // Ensure that this is tracked for upload.
+ }
+ return;
+ }
+ // End deletion logic.
+
+ // Now we're processing a non-deleted incoming record.
+ if (existingRecord == null) {
+ Logger.trace(LOG_TAG, "Looking up match for record " + record.guid);
+ existingRecord = findExistingRecordByPayload(record);
+ }
+
+ if (existingRecord == null) {
+ // The record is new.
+ Logger.trace(LOG_TAG, "No match. Inserting.");
+ insertNewRegularRecord(record);
+ trackRecord(record);
+ delegate.onRecordStoreSucceeded(record.guid);
+ return;
+ }
+
+ // We found a local duplicate.
+ Logger.trace(LOG_TAG, "Incoming record " + record.guid + " dupes to local record " + existingRecord.guid);
+
+ if (!RepoUtils.stringsEqual(record.guid, existingRecord.guid)) {
+ // We found a local record that does NOT have the same GUID -- keep the server's version.
+ Logger.trace(LOG_TAG, "Remote guid different from local guid. Storing to keep remote guid.");
+ replaceExistingRecordWithRegularRecord(record, existingRecord);
+ trackRecord(record);
+ delegate.onRecordStoreSucceeded(record.guid);
+ return;
+ }
+
+ // We found a local record that does have the same GUID -- check modification times.
+ boolean locallyModified = existingRecord.lastModified > lastLocalRetrieval;
+ if (!locallyModified) {
+ Logger.trace(LOG_TAG, "Remote modified, local not. Storing.");
+ replaceExistingRecordWithRegularRecord(record, existingRecord);
+ trackRecord(record);
+ delegate.onRecordStoreSucceeded(record.guid);
+ return;
+ }
+
+ Logger.trace(LOG_TAG, "Both local and remote records have been modified.");
+ if (record.lastModified > existingRecord.lastModified) {
+ Logger.trace(LOG_TAG, "Remote is newer, and not deleted. Storing.");
+ replaceExistingRecordWithRegularRecord(record, existingRecord);
+ trackRecord(record);
+ delegate.onRecordStoreSucceeded(record.guid);
+ return;
+ }
+
+ Logger.trace(LOG_TAG, "Remote is older, local is not deleted. Ignoring.");
+ if (!locallyModified) {
+ Logger.warn(LOG_TAG, "Inconsistency: old remote record is not deleted, but local record not modified!");
+ }
+ return;
+ } catch (Exception e) {
+ Logger.error(LOG_TAG, "Store failed for " + record.guid, e);
+ delegate.onRecordStoreFailed(e, record.guid);
+ return;
+ }
+ }
+ };
+
+ storeWorkQueue.execute(command);
+ }
+
+ /**
+ * Purge all data from the underlying databases.
+ */
+ public static void purgeDatabases(ContentProviderClient formsProvider)
+ throws RemoteException {
+ formsProvider.delete(FORM_HISTORY_CONTENT_URI, null, null);
+ formsProvider.delete(DELETED_FORM_HISTORY_CONTENT_URI, null, null);
+ }
+
+ @Override
+ public void wipe(final RepositorySessionWipeDelegate delegate) {
+ Runnable command = new Runnable() {
+ @Override
+ public void run() {
+ if (!isActive()) {
+ delegate.onWipeFailed(new InactiveSessionException(null));
+ return;
+ }
+
+ try {
+ Logger.debug(LOG_TAG, "Wiping form history and deleted form history...");
+ purgeDatabases(formsProvider);
+ Logger.debug(LOG_TAG, "Wiping form history and deleted form history... DONE");
+ } catch (Exception e) {
+ delegate.onWipeFailed(e);
+ return;
+ }
+
+ delegate.onWipeSucceeded();
+ }
+ };
+ storeWorkQueue.execute(command);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/PasswordsRepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/PasswordsRepositorySession.java
new file mode 100644
index 0000000000..f7b7416df9
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/PasswordsRepositorySession.java
@@ -0,0 +1,725 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.android;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserContract.DeletedColumns;
+import org.mozilla.gecko.db.BrowserContract.DeletedPasswords;
+import org.mozilla.gecko.db.BrowserContract.Passwords;
+import org.mozilla.gecko.sync.repositories.InactiveSessionException;
+import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.RecordFilter;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.StoreTrackingRepositorySession;
+import org.mozilla.gecko.sync.repositories.android.RepoUtils.QueryHelper;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
+import org.mozilla.gecko.sync.repositories.domain.PasswordRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import android.content.ContentProviderClient;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+
+public class PasswordsRepositorySession extends
+ StoreTrackingRepositorySession {
+
+ public static class PasswordsRepository extends Repository {
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ PasswordsRepositorySession session = new PasswordsRepositorySession(PasswordsRepository.this, context);
+ final RepositorySessionCreationDelegate deferredCreationDelegate = delegate.deferredCreationDelegate();
+ deferredCreationDelegate.onSessionCreated(session);
+ }
+ }
+
+ private static final String LOG_TAG = "PasswordsRepoSession";
+ private static final String COLLECTION = "passwords";
+
+ private final RepoUtils.QueryHelper passwordsHelper;
+ private final RepoUtils.QueryHelper deletedPasswordsHelper;
+ private final ContentProviderClient passwordsProvider;
+
+ private final Context context;
+
+ public PasswordsRepositorySession(Repository repository, Context context) {
+ super(repository);
+ this.context = context;
+ this.passwordsHelper = new QueryHelper(context, BrowserContractHelpers.PASSWORDS_CONTENT_URI, LOG_TAG);
+ this.deletedPasswordsHelper = new QueryHelper(context, BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, LOG_TAG);
+ this.passwordsProvider = context.getContentResolver().acquireContentProviderClient(BrowserContract.PASSWORDS_AUTHORITY_URI);
+ }
+
+ private static final String[] GUID_COLS = new String[] { Passwords.GUID };
+ private static final String[] DELETED_GUID_COLS = new String[] { DeletedColumns.GUID };
+
+ private static final String WHERE_GUID_IS = Passwords.GUID + " = ?";
+ private static final String WHERE_DELETED_GUID_IS = DeletedPasswords.GUID + " = ?";
+
+ @Override
+ public void guidsSince(final long timestamp, final RepositorySessionGuidsSinceDelegate delegate) {
+ final Runnable guidsSinceRunnable = new Runnable() {
+ @Override
+ public void run() {
+
+ if (!isActive()) {
+ delegate.onGuidsSinceFailed(new InactiveSessionException(null));
+ return;
+ }
+
+ // Checks succeeded, now get GUIDs.
+ final List<String> guids = new ArrayList<String>();
+ try {
+ Logger.debug(LOG_TAG, "Fetching guidsSince from data table.");
+ final Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".getGUIDsSince", GUID_COLS, dateModifiedWhere(timestamp), null, null);
+ try {
+ if (data.moveToFirst()) {
+ while (!data.isAfterLast()) {
+ guids.add(RepoUtils.getStringFromCursor(data, Passwords.GUID));
+ data.moveToNext();
+ }
+ }
+ } finally {
+ data.close();
+ }
+
+ // Fetch guids from deleted table.
+ Logger.debug(LOG_TAG, "Fetching guidsSince from deleted table.");
+ final Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".getGUIDsSince", DELETED_GUID_COLS, dateModifiedWhereDeleted(timestamp), null, null);
+ try {
+ if (deleted.moveToFirst()) {
+ while (!deleted.isAfterLast()) {
+ guids.add(RepoUtils.getStringFromCursor(deleted, DeletedColumns.GUID));
+ deleted.moveToNext();
+ }
+ }
+ } finally {
+ deleted.close();
+ }
+ } catch (Exception e) {
+ Logger.error(LOG_TAG, "Exception in fetch.");
+ delegate.onGuidsSinceFailed(e);
+ return;
+ }
+ String[] guidStrings = new String[guids.size()];
+ delegate.onGuidsSinceSucceeded(guids.toArray(guidStrings));
+ }
+ };
+
+ delegateQueue.execute(guidsSinceRunnable);
+ }
+
+ @Override
+ public void fetchSince(final long timestamp, final RepositorySessionFetchRecordsDelegate delegate) {
+ final RecordFilter filter = this.storeTracker.getFilter();
+ final Runnable fetchSinceRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (!isActive()) {
+ delegate.onFetchFailed(new InactiveSessionException(null), null);
+ return;
+ }
+
+ final long end = now();
+ try {
+ // Fetch from data table.
+ Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".fetchSince",
+ getAllColumns(),
+ dateModifiedWhere(timestamp),
+ null, null);
+ if (!fetchAndCloseCursorDeleted(data, false, filter, delegate)) {
+ return;
+ }
+
+ // Fetch from deleted table.
+ Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".fetchSince",
+ getAllDeletedColumns(),
+ dateModifiedWhereDeleted(timestamp),
+ null, null);
+ if (!fetchAndCloseCursorDeleted(deleted, true, filter, delegate)) {
+ return;
+ }
+
+ // Success!
+ try {
+ delegate.onFetchCompleted(end);
+ } catch (Exception e) {
+ Logger.error(LOG_TAG, "Delegate fetch completed callback failed.", e);
+ // Don't call failure callback.
+ return;
+ }
+ } catch (Exception e) {
+ Logger.error(LOG_TAG, "Exception in fetch.");
+ delegate.onFetchFailed(e, null);
+ }
+ }
+ };
+
+ delegateQueue.execute(fetchSinceRunnable);
+ }
+
+ @Override
+ public void fetch(final String[] guids, final RepositorySessionFetchRecordsDelegate delegate) {
+ if (guids == null || guids.length < 1) {
+ Logger.error(LOG_TAG, "No guids to be fetched.");
+ final long end = now();
+ delegateQueue.execute(new Runnable() {
+ @Override
+ public void run() {
+ delegate.onFetchCompleted(end);
+ }
+ });
+ return;
+ }
+
+ // Checks succeeded, now fetch.
+ final RecordFilter filter = this.storeTracker.getFilter();
+ final Runnable fetchRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (!isActive()) {
+ delegate.onFetchFailed(new InactiveSessionException(null), null);
+ return;
+ }
+
+ final long end = now();
+ final String where = RepoUtils.computeSQLInClause(guids.length, "guid");
+ Logger.trace(LOG_TAG, "Fetch guids where: " + where);
+
+ try {
+ // Fetch records from data table.
+ Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".fetch",
+ getAllColumns(),
+ where, guids, null);
+ if (!fetchAndCloseCursorDeleted(data, false, filter, delegate)) {
+ return;
+ }
+
+ // Fetch records from deleted table.
+ Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".fetch",
+ getAllDeletedColumns(),
+ where, guids, null);
+ if (!fetchAndCloseCursorDeleted(deleted, true, filter, delegate)) {
+ return;
+ }
+
+ delegate.onFetchCompleted(end);
+
+ } catch (Exception e) {
+ Logger.error(LOG_TAG, "Exception in fetch.");
+ delegate.onFetchFailed(e, null);
+ }
+ }
+ };
+
+ delegateQueue.execute(fetchRunnable);
+ }
+
+ @Override
+ public void fetchAll(RepositorySessionFetchRecordsDelegate delegate) {
+ fetchSince(0, delegate);
+ }
+
+ @Override
+ public void store(final Record record) throws NoStoreDelegateException {
+ if (delegate == null) {
+ Logger.error(LOG_TAG, "No store delegate.");
+ throw new NoStoreDelegateException();
+ }
+ if (record == null) {
+ Logger.error(LOG_TAG, "Record sent to store was null.");
+ throw new IllegalArgumentException("Null record passed to PasswordsRepositorySession.store().");
+ }
+ if (!(record instanceof PasswordRecord)) {
+ Logger.error(LOG_TAG, "Can't store anything but a PasswordRecord.");
+ throw new IllegalArgumentException("Non-PasswordRecord passed to PasswordsRepositorySession.store().");
+ }
+
+ final PasswordRecord remoteRecord = (PasswordRecord) record;
+
+ final Runnable storeRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (!isActive()) {
+ Logger.warn(LOG_TAG, "RepositorySession is inactive. Store failing.");
+ delegate.onRecordStoreFailed(new InactiveSessionException(null), record.guid);
+ return;
+ }
+
+ final String guid = remoteRecord.guid;
+ if (guid == null) {
+ delegate.onRecordStoreFailed(new RuntimeException("Can't store record with null GUID."), record.guid);
+ return;
+ }
+
+ PasswordRecord existingRecord;
+ try {
+ existingRecord = retrieveByGUID(guid);
+ } catch (NullCursorException | RemoteException e) {
+ // Indicates a serious problem.
+ delegate.onRecordStoreFailed(e, record.guid);
+ return;
+ }
+
+ long lastLocalRetrieval = 0; // lastSyncTimestamp?
+ long lastRemoteRetrieval = 0; // TODO: adjust for clock skew.
+ boolean remotelyModified = remoteRecord.lastModified > lastRemoteRetrieval;
+
+ // Check deleted state first.
+ if (remoteRecord.deleted) {
+ if (existingRecord == null) {
+ // Do nothing, record does not exist anyways.
+ Logger.info(LOG_TAG, "Incoming record " + remoteRecord.guid + " is deleted, and no local version.");
+ return;
+ }
+
+ if (existingRecord.deleted) {
+ // Record is already tracked as deleted. Delete from local.
+ storeRecordDeletion(existingRecord); // different from ABRepoSess.
+ Logger.info(LOG_TAG, "Incoming record " + remoteRecord.guid + " and local are both deleted.");
+ return;
+ }
+
+ // Which one wins?
+ if (!remotelyModified) {
+ trace("Ignoring deleted record from the past.");
+ return;
+ }
+
+ boolean locallyModified = existingRecord.lastModified > lastLocalRetrieval;
+ if (!locallyModified) {
+ trace("Remote modified, local not. Deleting.");
+ storeRecordDeletion(remoteRecord);
+ return;
+ }
+
+ trace("Both local and remote records have been modified.");
+ if (remoteRecord.lastModified > existingRecord.lastModified) {
+ trace("Remote is newer, and deleted. Deleting local.");
+ storeRecordDeletion(remoteRecord);
+ return;
+ }
+
+ trace("Remote is older, local is not deleted. Ignoring.");
+ if (!locallyModified) {
+ Logger.warn(LOG_TAG, "Inconsistency: old remote record is deleted, but local record not modified!");
+ // Ensure that this is tracked for upload.
+ }
+ return;
+ }
+ // End deletion logic.
+
+ // Validate the incoming record.
+ if (!remoteRecord.isValid()) {
+ Logger.warn(LOG_TAG, "Incoming record is invalid. Reporting store failed.");
+ delegate.onRecordStoreFailed(new RuntimeException("Can't store invalid password record."), record.guid);
+ return;
+ }
+
+ // Now we're processing a non-deleted incoming record.
+ if (existingRecord == null) {
+ trace("Looking up match for record " + remoteRecord.guid);
+ try {
+ existingRecord = findExistingRecord(remoteRecord);
+ } catch (RemoteException e) {
+ Logger.error(LOG_TAG, "Remote exception in findExistingRecord.");
+ delegate.onRecordStoreFailed(e, record.guid);
+ } catch (NullCursorException e) {
+ Logger.error(LOG_TAG, "Null cursor in findExistingRecord.");
+ delegate.onRecordStoreFailed(e, record.guid);
+ }
+ }
+
+ if (existingRecord == null) {
+ // The record is new.
+ trace("No match. Inserting.");
+ Logger.debug(LOG_TAG, "Didn't find matching record. Inserting.");
+ Record inserted = null;
+ try {
+ inserted = insert(remoteRecord);
+ } catch (RemoteException e) {
+ Logger.debug(LOG_TAG, "Record insert caused a RemoteException.");
+ delegate.onRecordStoreFailed(e, record.guid);
+ return;
+ }
+ trackRecord(inserted);
+ delegate.onRecordStoreSucceeded(inserted.guid);
+ return;
+ }
+
+ // We found a local dupe.
+ trace("Incoming record " + remoteRecord.guid + " dupes to local record " + existingRecord.guid);
+ Logger.debug(LOG_TAG, "remote " + remoteRecord.guid + " dupes to " + existingRecord.guid);
+
+ if (existingRecord.deleted && existingRecord.lastModified > remoteRecord.lastModified) {
+ Logger.debug(LOG_TAG, "Local deletion is newer, not storing remote record.");
+ return;
+ }
+
+ Record toStore = reconcileRecords(remoteRecord, existingRecord, lastRemoteRetrieval, lastLocalRetrieval);
+ if (toStore == null) {
+ Logger.debug(LOG_TAG, "Reconciling returned null. Not inserting a record.");
+ return;
+ }
+
+ // TODO: pass in timestamps?
+ Logger.debug(LOG_TAG, "Replacing " + existingRecord.guid + " with record " + toStore.guid);
+ Record replaced = null;
+ try {
+ replaced = replace(existingRecord, toStore);
+ } catch (RemoteException e) {
+ Logger.debug(LOG_TAG, "Record replace caused a RemoteException.");
+ delegate.onRecordStoreFailed(e, record.guid);
+ return;
+ }
+
+ // Note that we don't track records here; deciding that is the job
+ // of reconcileRecords.
+ Logger.debug(LOG_TAG, "Calling delegate callback with guid " + replaced.guid +
+ "(" + replaced.androidID + ")");
+ delegate.onRecordStoreSucceeded(record.guid);
+ return;
+ }
+ };
+ storeWorkQueue.execute(storeRunnable);
+ }
+
+ @Override
+ public void wipe(final RepositorySessionWipeDelegate delegate) {
+ Logger.info(LOG_TAG, "Wiping " + BrowserContractHelpers.PASSWORDS_CONTENT_URI + ", " + BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI);
+
+ Runnable wipeRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (!isActive()) {
+ delegate.onWipeFailed(new InactiveSessionException(null));
+ return;
+ }
+
+ // Wipe both data and deleted.
+ try {
+ context.getContentResolver().delete(BrowserContractHelpers.PASSWORDS_CONTENT_URI, null, null);
+ context.getContentResolver().delete(BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, null, null);
+ } catch (Exception e) {
+ delegate.onWipeFailed(e);
+ return;
+ }
+ delegate.onWipeSucceeded();
+ }
+ };
+ storeWorkQueue.execute(wipeRunnable);
+ }
+
+ @Override
+ public void abort() {
+ passwordsProvider.release();
+ super.abort();
+ }
+
+ @Override
+ public void finish(final RepositorySessionFinishDelegate delegate) throws InactiveSessionException {
+ passwordsProvider.release();
+ super.finish(delegate);
+ }
+
+ public void deleteGUID(String guid) throws RemoteException {
+ final String[] args = new String[] { guid };
+
+ int deleted = passwordsProvider.delete(BrowserContractHelpers.PASSWORDS_CONTENT_URI, WHERE_GUID_IS, args) +
+ passwordsProvider.delete(BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, WHERE_DELETED_GUID_IS, args);
+ if (deleted == 1) {
+ return;
+ }
+ Logger.warn(LOG_TAG, "Unexpectedly deleted " + deleted + " rows for guid " + guid);
+ }
+
+ /**
+ * Insert record and return the record with its updated androidId set.
+ *
+ * @param record the record to insert.
+ * @return updated record.
+ * @throws RemoteException
+ */
+ public PasswordRecord insert(PasswordRecord record) throws RemoteException {
+ record.timePasswordChanged = now();
+ // TODO: are these necessary for Fennec autocomplete?
+ // record.timesUsed = 1;
+ // record.timeLastUsed = now();
+ ContentValues cv = getContentValues(record);
+ Uri insertedUri = passwordsProvider.insert(BrowserContractHelpers.PASSWORDS_CONTENT_URI, cv);
+ if (insertedUri == null) {
+ throw new RemoteException(); // Not much to be done here, save throw.
+ }
+ record.androidID = ContentUris.parseId(insertedUri);
+ return record;
+ }
+
+ public Record replace(Record origRecord, Record newRecord) throws RemoteException {
+ PasswordRecord newPasswordRecord = (PasswordRecord) newRecord;
+ PasswordRecord origPasswordRecord = (PasswordRecord) origRecord;
+ propagateTimes(newPasswordRecord, origPasswordRecord);
+ ContentValues cv = getContentValues(newPasswordRecord);
+
+ final String[] args = new String[] { origRecord.guid };
+
+ if (origRecord.deleted) {
+ // Purge from deleted table.
+ deleteGUID(origRecord.guid);
+ insert(newPasswordRecord);
+ } else {
+ int updated = context.getContentResolver().update(BrowserContractHelpers.PASSWORDS_CONTENT_URI, cv, WHERE_GUID_IS, args);
+ if (updated != 1) {
+ Logger.warn(LOG_TAG, "Unexpectedly updated " + updated + " rows for guid " + origPasswordRecord.guid);
+ }
+ }
+
+ return newRecord;
+ }
+
+ // When replacing a record, propagate the times.
+ private static void propagateTimes(PasswordRecord toRecord, PasswordRecord fromRecord) {
+ toRecord.timePasswordChanged = now();
+ toRecord.timeCreated = fromRecord.timeCreated;
+ toRecord.timeLastUsed = fromRecord.timeLastUsed;
+ toRecord.timesUsed = fromRecord.timesUsed;
+ }
+
+ private static String[] getAllColumns() {
+ return BrowserContractHelpers.PasswordColumns;
+ }
+
+ private static String[] getAllDeletedColumns() {
+ return BrowserContractHelpers.DeletedColumns;
+ }
+
+ /**
+ * Constructs the DB query string for entry age for deleted records.
+ *
+ * @param timestamp
+ * @return String DB query string for dates to fetch.
+ */
+ private static String dateModifiedWhereDeleted(long timestamp) {
+ return DeletedColumns.TIME_DELETED + " >= " + Long.toString(timestamp);
+ }
+
+ /**
+ * Constructs the DB query string for entry age for (undeleted) records.
+ *
+ * @param timestamp
+ * @return String DB query string for dates to fetch.
+ */
+ private static String dateModifiedWhere(long timestamp) {
+ return Passwords.TIME_PASSWORD_CHANGED + " >= " + Long.toString(timestamp);
+ }
+
+
+ /**
+ * Fetch from the cursor with the given parameters, invoking
+ * delegate callbacks and closing the cursor.
+ * Returns true on success, false if failure was signaled.
+ *
+ * @param cursor
+ fetch* cursor.
+ * @param deleted
+ * true if using deleted table, false when using data table.
+ * @param delegate
+ * FetchRecordsDelegate to process records.
+ */
+ private static boolean fetchAndCloseCursorDeleted(final Cursor cursor,
+ final boolean deleted,
+ final RecordFilter filter,
+ final RepositorySessionFetchRecordsDelegate delegate) {
+ if (cursor == null) {
+ return true;
+ }
+
+ try {
+ while (cursor.moveToNext()) {
+ Record r = deleted ? deletedPasswordRecordFromCursor(cursor) : passwordRecordFromCursor(cursor);
+ if (r != null) {
+ if (filter == null || !filter.excludeRecord(r)) {
+ Logger.debug(LOG_TAG, "Processing record " + r.guid);
+ delegate.onFetchedRecord(r);
+ } else {
+ Logger.debug(LOG_TAG, "Skipping filtered record " + r.guid);
+ }
+ }
+ }
+ } catch (Exception e) {
+ Logger.error(LOG_TAG, "Exception in fetch.");
+ delegate.onFetchFailed(e, null);
+ return false;
+ } finally {
+ cursor.close();
+ }
+
+ return true;
+ }
+
+ private PasswordRecord retrieveByGUID(String guid) throws NullCursorException, RemoteException {
+ final String[] guidArg = new String[] { guid };
+
+ // Check data table.
+ final Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".store", BrowserContractHelpers.PasswordColumns, WHERE_GUID_IS, guidArg, null);
+ try {
+ if (data.moveToFirst()) {
+ return passwordRecordFromCursor(data);
+ }
+ } finally {
+ data.close();
+ }
+
+ // Check deleted table.
+ final Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".retrieveByGuid", BrowserContractHelpers.DeletedColumns, WHERE_DELETED_GUID_IS, guidArg, null);
+ try {
+ if (deleted.moveToFirst()) {
+ return deletedPasswordRecordFromCursor(deleted);
+ }
+ } finally {
+ deleted.close();
+ }
+
+ return null;
+ }
+
+ private static final String WHERE_RECORD_DATA =
+ Passwords.HOSTNAME + " = ? AND " +
+ Passwords.HTTP_REALM + " = ? AND " +
+ Passwords.FORM_SUBMIT_URL + " = ? AND " +
+ Passwords.USERNAME_FIELD + " = ? AND " +
+ Passwords.PASSWORD_FIELD + " = ?";
+
+ private PasswordRecord findExistingRecord(PasswordRecord record) throws NullCursorException, RemoteException {
+ PasswordRecord foundRecord = null;
+ Cursor cursor = null;
+ // Only check the data table.
+ // We can't encrypt username directly for query, so run a more general query and then filter.
+ final String[] whereArgs = new String[] {
+ record.hostname,
+ record.httpRealm,
+ record.formSubmitURL,
+ record.usernameField,
+ record.passwordField
+ };
+
+ try {
+ cursor = passwordsHelper.safeQuery(passwordsProvider, ".findRecord", getAllColumns(), WHERE_RECORD_DATA, whereArgs, null);
+ while (cursor.moveToNext()) {
+ foundRecord = passwordRecordFromCursor(cursor);
+
+ // We don't directly query for username because the
+ // username/password values are encrypted in the db.
+ // We don't have the keys for encrypting our query,
+ // so we run a more general query and then filter
+ // the returned records for a matching username.
+ Logger.pii(LOG_TAG, "Checking incoming [" + record.encryptedUsername + "] to [" + foundRecord.encryptedUsername + "]");
+ if (record.encryptedUsername.equals(foundRecord.encryptedUsername)) {
+ Logger.trace(LOG_TAG, "Found matching record: " + foundRecord.guid);
+ return foundRecord;
+ }
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ Logger.debug(LOG_TAG, "No matching records, returning null.");
+ return null;
+ }
+
+ private void storeRecordDeletion(Record record) {
+ try {
+ deleteGUID(record.guid);
+ } catch (RemoteException e) {
+ Logger.error(LOG_TAG, "RemoteException in password delete.");
+ delegate.onRecordStoreFailed(e, record.guid);
+ return;
+ }
+ delegate.onRecordStoreSucceeded(record.guid);
+ }
+
+ /**
+ * Make a PasswordRecord from a Cursor.
+ * @param cur
+ * Cursor from query.
+ * @param deleted
+ * true if creating a deleted Record, false if otherwise.
+ * @return
+ * PasswordRecord populated from Cursor.
+ */
+ private static PasswordRecord passwordRecordFromCursor(Cursor cur) {
+ if (cur.isAfterLast()) {
+ return null;
+ }
+ String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.GUID);
+ long lastModified = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_PASSWORD_CHANGED);
+
+ PasswordRecord rec = new PasswordRecord(guid, COLLECTION, lastModified, false);
+ rec.id = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ID);
+ rec.hostname = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.HOSTNAME);
+ rec.httpRealm = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.HTTP_REALM);
+ rec.formSubmitURL = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.FORM_SUBMIT_URL);
+ rec.usernameField = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.USERNAME_FIELD);
+ rec.passwordField = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.PASSWORD_FIELD);
+ rec.encType = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ENC_TYPE);
+
+ // TODO decryption of username/password here (Bug 711636)
+ rec.encryptedUsername = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ENCRYPTED_USERNAME);
+ rec.encryptedPassword = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ENCRYPTED_PASSWORD);
+
+ rec.timeCreated = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_CREATED);
+ rec.timeLastUsed = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_LAST_USED);
+ rec.timePasswordChanged = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_PASSWORD_CHANGED);
+ rec.timesUsed = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIMES_USED);
+ return rec;
+ }
+
+ private static PasswordRecord deletedPasswordRecordFromCursor(Cursor cur) {
+ if (cur.isAfterLast()) {
+ return null;
+ }
+ String guid = RepoUtils.getStringFromCursor(cur, DeletedColumns.GUID);
+ long lastModified = RepoUtils.getLongFromCursor(cur, DeletedColumns.TIME_DELETED);
+ PasswordRecord rec = new PasswordRecord(guid, COLLECTION, lastModified, true);
+ rec.androidID = RepoUtils.getLongFromCursor(cur, DeletedColumns.ID);
+ return rec;
+ }
+
+ private static ContentValues getContentValues(Record record) {
+ PasswordRecord rec = (PasswordRecord) record;
+
+ ContentValues cv = new ContentValues();
+ cv.put(BrowserContract.Passwords.GUID, rec.guid);
+ cv.put(BrowserContract.Passwords.HOSTNAME, rec.hostname);
+ cv.put(BrowserContract.Passwords.HTTP_REALM, rec.httpRealm);
+ cv.put(BrowserContract.Passwords.FORM_SUBMIT_URL, rec.formSubmitURL);
+ cv.put(BrowserContract.Passwords.USERNAME_FIELD, rec.usernameField);
+ cv.put(BrowserContract.Passwords.PASSWORD_FIELD, rec.passwordField);
+
+ // TODO Do encryption of username/password here. Bug 711636
+ cv.put(BrowserContract.Passwords.ENC_TYPE, rec.encType);
+ cv.put(BrowserContract.Passwords.ENCRYPTED_USERNAME, rec.encryptedUsername);
+ cv.put(BrowserContract.Passwords.ENCRYPTED_PASSWORD, rec.encryptedPassword);
+
+ cv.put(BrowserContract.Passwords.TIME_CREATED, rec.timeCreated);
+ cv.put(BrowserContract.Passwords.TIME_LAST_USED, rec.timeLastUsed);
+ cv.put(BrowserContract.Passwords.TIME_PASSWORD_CHANGED, rec.timePasswordChanged);
+ cv.put(BrowserContract.Passwords.TIMES_USED, rec.timesUsed);
+ return cv;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/RepoUtils.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/RepoUtils.java
new file mode 100644
index 0000000000..9c29953f8d
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/RepoUtils.java
@@ -0,0 +1,290 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.android;
+
+import android.content.ContentProviderClient;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.os.RemoteException;
+
+import org.json.simple.JSONArray;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonArrayJSONException;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
+import org.mozilla.gecko.sync.repositories.domain.HistoryRecord;
+
+import java.io.IOException;
+
+public class RepoUtils {
+
+ private static final String LOG_TAG = "RepoUtils";
+
+ /**
+ * A helper class for monotonous SQL querying. Does timing and logging,
+ * offers a utility to throw on a null cursor.
+ *
+ * @author rnewman
+ *
+ */
+ public static class QueryHelper {
+ private final Context context;
+ private final Uri uri;
+ private final String tag;
+
+ public QueryHelper(Context context, Uri uri, String tag) {
+ this.context = context;
+ this.uri = uri;
+ this.tag = tag;
+ }
+
+ // For ContentProvider queries.
+ public Cursor safeQuery(String label, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder) throws NullCursorException {
+ long queryStart = android.os.SystemClock.uptimeMillis();
+ Cursor c = context.getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder);
+ return checkAndLogCursor(label, queryStart, c);
+ }
+
+ public Cursor safeQuery(String[] projection, String selection, String[] selectionArgs, String sortOrder) throws NullCursorException {
+ return this.safeQuery(null, projection, selection, selectionArgs, sortOrder);
+ }
+
+ // For ContentProviderClient queries.
+ public Cursor safeQuery(ContentProviderClient client, String label, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder) throws NullCursorException, RemoteException {
+ long queryStart = android.os.SystemClock.uptimeMillis();
+ Cursor c = client.query(uri, projection, selection, selectionArgs, sortOrder);
+ return checkAndLogCursor(label, queryStart, c);
+ }
+
+ // For SQLiteOpenHelper queries.
+ public Cursor safeQuery(SQLiteDatabase db, String label, String table, String[] columns,
+ String selection, String[] selectionArgs,
+ String groupBy, String having, String orderBy, String limit) throws NullCursorException {
+ long queryStart = android.os.SystemClock.uptimeMillis();
+ Cursor c = db.query(table, columns, selection, selectionArgs, groupBy, having, orderBy, limit);
+ return checkAndLogCursor(label, queryStart, c);
+ }
+
+ public Cursor safeQuery(SQLiteDatabase db, String label, String table, String[] columns,
+ String selection, String[] selectionArgs) throws NullCursorException {
+ return safeQuery(db, label, table, columns, selection, selectionArgs, null, null, null, null);
+ }
+
+ private Cursor checkAndLogCursor(String label, long queryStart, Cursor c) throws NullCursorException {
+ long queryEnd = android.os.SystemClock.uptimeMillis();
+ String logLabel = (label == null) ? tag : (tag + label);
+ RepoUtils.queryTimeLogger(logLabel, queryStart, queryEnd);
+ return checkNullCursor(logLabel, c);
+ }
+
+ public Cursor checkNullCursor(String logLabel, Cursor cursor) throws NullCursorException {
+ if (cursor == null) {
+ Logger.error(tag, "Got null cursor exception in " + logLabel);
+ throw new NullCursorException(null);
+ }
+ return cursor;
+ }
+ }
+
+ /**
+ * This method exists because the behavior of <code>cur.getString()</code> is undefined
+ * when the value in the database is <code>NULL</code>.
+ * This method will return <code>null</code> in that case.
+ */
+ public static String optStringFromCursor(final Cursor cur, final String colId) {
+ final int col = cur.getColumnIndex(colId);
+ if (cur.isNull(col)) {
+ return null;
+ }
+ return cur.getString(col);
+ }
+
+ /**
+ * The behavior of this method when the value in the database is <code>NULL</code> is
+ * determined by the implementation of the {@link Cursor}.
+ */
+ public static String getStringFromCursor(final Cursor cur, final String colId) {
+ // TODO: getColumnIndexOrThrow?
+ // TODO: don't look up columns by name!
+ return cur.getString(cur.getColumnIndex(colId));
+ }
+
+ public static long getLongFromCursor(Cursor cur, String colId) {
+ return cur.getLong(cur.getColumnIndex(colId));
+ }
+
+ public static int getIntFromCursor(Cursor cur, String colId) {
+ return cur.getInt(cur.getColumnIndex(colId));
+ }
+
+ public static JSONArray getJSONArrayFromCursor(Cursor cur, String colId) {
+ String jsonArrayAsString = getStringFromCursor(cur, colId);
+ if (jsonArrayAsString == null) {
+ return new JSONArray();
+ }
+ try {
+ return ExtendedJSONObject.parseJSONArray(getStringFromCursor(cur, colId));
+ } catch (NonArrayJSONException e) {
+ Logger.error(LOG_TAG, "JSON parsing error for " + colId, e);
+ return null;
+ } catch (IOException e) {
+ Logger.error(LOG_TAG, "JSON parsing error for " + colId, e);
+ return null;
+ }
+ }
+
+ /**
+ * Return true if the provided URI is non-empty and acceptable to Fennec
+ * (i.e., not an undesirable scheme).
+ *
+ * This code is pilfered from Fennec, which pilfered from Places.
+ */
+ public static boolean isValidHistoryURI(String uri) {
+ if (uri == null || uri.length() == 0) {
+ return false;
+ }
+
+ // First, check the most common cases (HTTP, HTTPS) to avoid most of the work.
+ if (uri.startsWith("http:") || uri.startsWith("https:")) {
+ return true;
+ }
+
+ String scheme = Uri.parse(uri).getScheme();
+ if (scheme == null) {
+ return false;
+ }
+
+ // Now check for all bad things.
+ if (scheme.equals("about") ||
+ scheme.equals("imap") ||
+ scheme.equals("news") ||
+ scheme.equals("mailbox") ||
+ scheme.equals("moz-anno") ||
+ scheme.equals("view-source") ||
+ scheme.equals("chrome") ||
+ scheme.equals("resource") ||
+ scheme.equals("data") ||
+ scheme.equals("wyciwyg") ||
+ scheme.equals("javascript")) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Create a HistoryRecord object from a cursor row.
+ *
+ * @return a HistoryRecord, or null if this row would produce
+ * an invalid record (e.g., with a null URI or no visits).
+ */
+ public static HistoryRecord historyFromMirrorCursor(Cursor cur) {
+ final String guid = getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
+ if (guid == null) {
+ Logger.debug(LOG_TAG, "Skipping history record with null GUID.");
+ return null;
+ }
+
+ final String historyURI = getStringFromCursor(cur, BrowserContract.History.URL);
+ if (!isValidHistoryURI(historyURI)) {
+ Logger.debug(LOG_TAG, "Skipping history record " + guid + " with unwanted/invalid URI " + historyURI);
+ return null;
+ }
+
+ final long visitCount = getLongFromCursor(cur, BrowserContract.History.VISITS);
+ if (visitCount <= 0) {
+ Logger.debug(LOG_TAG, "Skipping history record " + guid + " with <= 0 visit count.");
+ return null;
+ }
+
+ final String collection = "history";
+ final long lastModified = getLongFromCursor(cur, BrowserContract.SyncColumns.DATE_MODIFIED);
+ final boolean deleted = getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1;
+
+ final HistoryRecord rec = new HistoryRecord(guid, collection, lastModified, deleted);
+
+ rec.androidID = getLongFromCursor(cur, BrowserContract.History._ID);
+ rec.fennecDateVisited = getLongFromCursor(cur, BrowserContract.History.DATE_LAST_VISITED);
+ rec.fennecVisitCount = visitCount;
+ rec.histURI = historyURI;
+ rec.title = getStringFromCursor(cur, BrowserContract.History.TITLE);
+
+ return logHistory(rec);
+ }
+
+ private static HistoryRecord logHistory(HistoryRecord rec) {
+ try {
+ Logger.debug(LOG_TAG, "Returning history record " + rec.guid + " (" + rec.androidID + ")");
+ Logger.debug(LOG_TAG, "> Visited: " + rec.fennecDateVisited);
+ Logger.debug(LOG_TAG, "> Visits: " + rec.fennecVisitCount);
+ if (Logger.LOG_PERSONAL_INFORMATION) {
+ Logger.pii(LOG_TAG, "> Title: " + rec.title);
+ Logger.pii(LOG_TAG, "> URI: " + rec.histURI);
+ }
+ } catch (Exception e) {
+ Logger.debug(LOG_TAG, "Exception logging history record " + rec, e);
+ }
+ return rec;
+ }
+
+ public static void logClient(ClientRecord rec) {
+ if (Logger.shouldLogVerbose(LOG_TAG)) {
+ Logger.trace(LOG_TAG, "Returning client record " + rec.guid + " (" + rec.androidID + ")");
+ Logger.trace(LOG_TAG, "Client Name: " + rec.name);
+ Logger.trace(LOG_TAG, "Client Type: " + rec.type);
+ Logger.trace(LOG_TAG, "Last Modified: " + rec.lastModified);
+ Logger.trace(LOG_TAG, "Deleted: " + rec.deleted);
+ }
+ }
+
+ public static void queryTimeLogger(String methodCallingQuery, long queryStart, long queryEnd) {
+ long elapsedTime = queryEnd - queryStart;
+ Logger.debug(LOG_TAG, "Query timer: " + methodCallingQuery + " took " + elapsedTime + "ms.");
+ }
+
+ public static boolean stringsEqual(String a, String b) {
+ // Check for nulls
+ if (a == b) return true;
+ if (a == null && b != null) return false;
+ if (a != null && b == null) return false;
+
+ return a.equals(b);
+ }
+
+ public static String computeSQLLongInClause(long[] items, String field) {
+ final StringBuilder builder = new StringBuilder(field);
+ builder.append(" IN (");
+ int i = 0;
+ for (; i < items.length - 1; ++i) {
+ builder.append(items[i]);
+ builder.append(", ");
+ }
+ if (i < items.length) {
+ builder.append(items[i]);
+ }
+ builder.append(")");
+ return builder.toString();
+ }
+
+ 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();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/VisitsHelper.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/VisitsHelper.java
new file mode 100644
index 0000000000..9ba784759b
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/VisitsHelper.java
@@ -0,0 +1,130 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.android;
+
+import android.content.ContentProviderClient;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.support.annotation.NonNull;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.mozilla.gecko.db.BrowserContract.Visits;
+
+/**
+ * This class is used by History Sync code (see <code>AndroidBrowserHistoryDataAccessor</code> and <code>AndroidBrowserHistoryRepositorySession</code>,
+ * and provides utility functions for working with history visits. Primarily we're either inserting visits
+ * into local database based on data received from Sync, or we're preparing local visits for upload into Sync.
+ */
+public class VisitsHelper {
+ public static final boolean DEFAULT_IS_LOCAL_VALUE = false;
+ public static final String SYNC_TYPE_KEY = "type";
+ public static final String SYNC_DATE_KEY = "date";
+
+ /**
+ * Returns a list of ContentValues of visits ready for insertion for a provided History GUID.
+ * Visits must have data and type. See <code>getVisitContentValues</code>.
+ *
+ * @param guid History GUID to use when inserting visit records
+ * @param visits <code>JSONArray</code> list of (date, type) tuples for visits
+ * @return visits ready for insertion
+ */
+ public static ContentValues[] getVisitsContentValues(@NonNull String guid, @NonNull JSONArray visits) {
+ final ContentValues[] visitsToStore = new ContentValues[visits.size()];
+ final int visitCount = visits.size();
+
+ if (visitCount == 0) {
+ return visitsToStore;
+ }
+
+ for (int i = 0; i < visitCount; i++) {
+ visitsToStore[i] = getVisitContentValues(
+ guid, (JSONObject) visits.get(i), DEFAULT_IS_LOCAL_VALUE);
+ }
+ return visitsToStore;
+ }
+
+ /**
+ * Maps up to <code>limit</code> visits for a given history GUID to an array of JSONObjects with "date" and "type" keys
+ *
+ * @param contentClient <code>ContentProviderClient</code> to use for querying Visits table
+ * @param guid History GUID for which to return visits
+ * @param limit Will return at most this number of visits
+ * @return <code>JSONArray</code> of all visits found for given History GUID
+ */
+ public static JSONArray getRecentHistoryVisitsForGUID(@NonNull ContentProviderClient contentClient,
+ @NonNull String guid, int limit) throws RemoteException {
+ final JSONArray visits = new JSONArray();
+
+ final Cursor cursor = contentClient.query(
+ visitsUriWithLimit(limit),
+ new String[] {Visits.VISIT_TYPE, Visits.DATE_VISITED},
+ Visits.HISTORY_GUID + " = ?",
+ new String[] {guid}, null);
+ if (cursor == null) {
+ return visits;
+ }
+ try {
+ if (!cursor.moveToFirst()) {
+ return visits;
+ }
+
+ final int dateVisitedCol = cursor.getColumnIndexOrThrow(Visits.DATE_VISITED);
+ final int visitTypeCol = cursor.getColumnIndexOrThrow(Visits.VISIT_TYPE);
+
+ while (!cursor.isAfterLast()) {
+ insertTupleIntoVisitsUnchecked(visits,
+ cursor.getLong(visitTypeCol),
+ cursor.getLong(dateVisitedCol)
+ );
+ cursor.moveToNext();
+ }
+ } finally {
+ cursor.close();
+ }
+
+ return visits;
+ }
+
+ /**
+ * Constructs <code>ContentValues</code> object for a visit based on passed in parameters.
+ *
+ * @param visit <code>JSONObject</code> containing visit type and visit date keys for the visit
+ * @param guid History GUID with with to associate this visit
+ * @param isLocal Whether or not to mark this visit as local
+ * @return <code>ContentValues</code> with all visit values necessary for database insertion
+ * @throws IllegalArgumentException if visit object is missing date or type keys
+ */
+ public static ContentValues getVisitContentValues(@NonNull String guid, @NonNull JSONObject visit, boolean isLocal) {
+ if (!visit.containsKey(SYNC_DATE_KEY) || !visit.containsKey(SYNC_TYPE_KEY)) {
+ throw new IllegalArgumentException("Visit missing required keys");
+ }
+
+ final ContentValues cv = new ContentValues();
+ cv.put(Visits.HISTORY_GUID, guid);
+ cv.put(Visits.IS_LOCAL, isLocal ? Visits.VISIT_IS_LOCAL : Visits.VISIT_IS_REMOTE);
+ cv.put(Visits.VISIT_TYPE, (Long) visit.get(SYNC_TYPE_KEY));
+ cv.put(Visits.DATE_VISITED, (Long) visit.get(SYNC_DATE_KEY));
+
+ return cv;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static void insertTupleIntoVisitsUnchecked(@NonNull final JSONArray visits, @NonNull Long type, @NonNull Long date) {
+ final JSONObject visit = new JSONObject();
+ visit.put(SYNC_TYPE_KEY, type);
+ visit.put(SYNC_DATE_KEY, date);
+ visits.add(visit);
+ }
+
+ private static Uri visitsUriWithLimit(int limit) {
+ return BrowserContractHelpers.VISITS_CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter("limit", Integer.toString(limit))
+ .build();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferrableRepositorySessionCreationDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferrableRepositorySessionCreationDelegate.java
new file mode 100644
index 0000000000..f292600e47
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferrableRepositorySessionCreationDelegate.java
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.delegates;
+
+import org.mozilla.gecko.sync.ThreadPool;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+
+public abstract class DeferrableRepositorySessionCreationDelegate implements RepositorySessionCreationDelegate {
+ @Override
+ public RepositorySessionCreationDelegate deferredCreationDelegate() {
+ final RepositorySessionCreationDelegate self = this;
+ return new RepositorySessionCreationDelegate() {
+
+ // TODO: rewrite to use ExecutorService.
+ @Override
+ public void onSessionCreated(final RepositorySession session) {
+ ThreadPool.run(new Runnable() {
+ @Override
+ public void run() {
+ self.onSessionCreated(session);
+ }});
+ }
+
+ @Override
+ public void onSessionCreateFailed(final Exception ex) {
+ ThreadPool.run(new Runnable() {
+ @Override
+ public void run() {
+ self.onSessionCreateFailed(ex);
+ }});
+ }
+
+ @Override
+ public RepositorySessionCreationDelegate deferredCreationDelegate() {
+ return this;
+ }
+ };
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionBeginDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionBeginDelegate.java
new file mode 100644
index 0000000000..1ccdcce19a
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionBeginDelegate.java
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.delegates;
+
+import java.util.concurrent.ExecutorService;
+
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+
+public class DeferredRepositorySessionBeginDelegate implements RepositorySessionBeginDelegate {
+ private final RepositorySessionBeginDelegate inner;
+ private final ExecutorService executor;
+ public DeferredRepositorySessionBeginDelegate(final RepositorySessionBeginDelegate inner, final ExecutorService executor) {
+ this.inner = inner;
+ this.executor = executor;
+ }
+
+ @Override
+ public void onBeginSucceeded(final RepositorySession session) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ inner.onBeginSucceeded(session);
+ }
+ });
+ }
+
+ @Override
+ public void onBeginFailed(final Exception ex) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ inner.onBeginFailed(ex);
+ }
+ });
+ }
+
+ @Override
+ public RepositorySessionBeginDelegate deferredBeginDelegate(ExecutorService newExecutor) {
+ if (newExecutor == executor) {
+ return this;
+ }
+ throw new IllegalArgumentException("Can't re-defer this delegate.");
+ }
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionFetchRecordsDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionFetchRecordsDelegate.java
new file mode 100644
index 0000000000..1178d9b5bf
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionFetchRecordsDelegate.java
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.delegates;
+
+import java.util.concurrent.ExecutorService;
+
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+public class DeferredRepositorySessionFetchRecordsDelegate implements RepositorySessionFetchRecordsDelegate {
+ private final RepositorySessionFetchRecordsDelegate inner;
+ private final ExecutorService executor;
+ public DeferredRepositorySessionFetchRecordsDelegate(final RepositorySessionFetchRecordsDelegate inner, final ExecutorService executor) {
+ this.inner = inner;
+ this.executor = executor;
+ }
+
+ @Override
+ public void onFetchedRecord(final Record record) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ inner.onFetchedRecord(record);
+ }
+ });
+ }
+
+ @Override
+ public void onFetchFailed(final Exception ex, final Record record) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ inner.onFetchFailed(ex, record);
+ }
+ });
+ }
+
+ @Override
+ public void onFetchCompleted(final long fetchEnd) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ inner.onFetchCompleted(fetchEnd);
+ }
+ });
+ }
+
+ @Override
+ public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService newExecutor) {
+ if (newExecutor == executor) {
+ return this;
+ }
+ throw new IllegalArgumentException("Can't re-defer this delegate.");
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionFinishDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionFinishDelegate.java
new file mode 100644
index 0000000000..dbe7e43275
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionFinishDelegate.java
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.delegates;
+
+import java.util.concurrent.ExecutorService;
+
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
+
+public class DeferredRepositorySessionFinishDelegate implements
+ RepositorySessionFinishDelegate {
+ protected final ExecutorService executor;
+ protected final RepositorySessionFinishDelegate inner;
+
+ public DeferredRepositorySessionFinishDelegate(RepositorySessionFinishDelegate inner,
+ ExecutorService executor) {
+ this.executor = executor;
+ this.inner = inner;
+ }
+
+ @Override
+ public void onFinishSucceeded(final RepositorySession session,
+ final RepositorySessionBundle bundle) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ inner.onFinishSucceeded(session, bundle);
+ }
+ });
+ }
+
+ @Override
+ public void onFinishFailed(final Exception ex) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ inner.onFinishFailed(ex);
+ }
+ });
+ }
+
+ @Override
+ public RepositorySessionFinishDelegate deferredFinishDelegate(ExecutorService newExecutor) {
+ if (newExecutor == executor) {
+ return this;
+ }
+ throw new IllegalArgumentException("Can't re-defer this delegate.");
+ }
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionStoreDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionStoreDelegate.java
new file mode 100644
index 0000000000..2f659c733f
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionStoreDelegate.java
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.delegates;
+
+import java.util.concurrent.ExecutorService;
+
+public class DeferredRepositorySessionStoreDelegate implements
+ RepositorySessionStoreDelegate {
+ protected final RepositorySessionStoreDelegate inner;
+ protected final ExecutorService executor;
+
+ public DeferredRepositorySessionStoreDelegate(
+ RepositorySessionStoreDelegate inner, ExecutorService executor) {
+ this.inner = inner;
+ this.executor = executor;
+ }
+
+ @Override
+ public void onRecordStoreSucceeded(final String guid) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ inner.onRecordStoreSucceeded(guid);
+ }
+ });
+ }
+
+ @Override
+ public void onRecordStoreFailed(final Exception ex, final String guid) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ inner.onRecordStoreFailed(ex, guid);
+ }
+ });
+ }
+
+ @Override
+ public RepositorySessionStoreDelegate deferredStoreDelegate(ExecutorService newExecutor) {
+ if (newExecutor == executor) {
+ return this;
+ }
+ throw new IllegalArgumentException("Can't re-defer this delegate.");
+ }
+
+ @Override
+ public void onStoreCompleted(final long storeEnd) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ inner.onStoreCompleted(storeEnd);
+ }
+ });
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionBeginDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionBeginDelegate.java
new file mode 100644
index 0000000000..f5853647f7
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionBeginDelegate.java
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.delegates;
+
+import java.util.concurrent.ExecutorService;
+
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+
+/**
+ * One of these two methods is guaranteed to be called after session.begin() is
+ * invoked (possibly during the invocation). The callback will be invoked prior
+ * to any other RepositorySession callbacks.
+ *
+ * @author rnewman
+ *
+ */
+public interface RepositorySessionBeginDelegate {
+ public void onBeginFailed(Exception ex);
+ public void onBeginSucceeded(RepositorySession session);
+ public RepositorySessionBeginDelegate deferredBeginDelegate(ExecutorService executor);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionCleanDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionCleanDelegate.java
new file mode 100644
index 0000000000..139c561a0b
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionCleanDelegate.java
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.delegates;
+
+import org.mozilla.gecko.sync.repositories.Repository;
+
+public interface RepositorySessionCleanDelegate {
+ public void onCleaned(Repository repo);
+ public void onCleanFailed(Repository repo, Exception ex);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionCreationDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionCreationDelegate.java
new file mode 100644
index 0000000000..6ad4991c3b
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionCreationDelegate.java
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.delegates;
+
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+
+// Used to provide the sessionCallback and storeCallback
+// mechanism to repository instances.
+public interface RepositorySessionCreationDelegate {
+ public void onSessionCreateFailed(Exception ex);
+ public void onSessionCreated(RepositorySession session);
+ public RepositorySessionCreationDelegate deferredCreationDelegate();
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionFetchRecordsDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionFetchRecordsDelegate.java
new file mode 100644
index 0000000000..589a093dc2
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionFetchRecordsDelegate.java
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.delegates;
+
+import java.util.concurrent.ExecutorService;
+
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+public interface RepositorySessionFetchRecordsDelegate {
+ public void onFetchFailed(Exception ex, Record record);
+ public void onFetchedRecord(Record record);
+
+ /**
+ * Called when all records in this fetch have been returned.
+ *
+ * @param fetchEnd
+ * A millisecond-resolution timestamp indicating the *remote* timestamp
+ * at the end of the range of records. Usually this is the timestamp at
+ * which the request was received.
+ * E.g., the (normalized) value of the X-Weave-Timestamp header.
+ */
+ public void onFetchCompleted(final long fetchEnd);
+
+ public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService executor);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionFinishDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionFinishDelegate.java
new file mode 100644
index 0000000000..40296dd4fe
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionFinishDelegate.java
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.delegates;
+
+import java.util.concurrent.ExecutorService;
+
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
+
+public interface RepositorySessionFinishDelegate {
+ public void onFinishFailed(Exception ex);
+ public void onFinishSucceeded(RepositorySession session, RepositorySessionBundle bundle);
+ public RepositorySessionFinishDelegate deferredFinishDelegate(ExecutorService executor);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionGuidsSinceDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionGuidsSinceDelegate.java
new file mode 100644
index 0000000000..4f82768f11
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionGuidsSinceDelegate.java
@@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.delegates;
+
+public interface RepositorySessionGuidsSinceDelegate {
+ public void onGuidsSinceFailed(Exception ex);
+ public void onGuidsSinceSucceeded(String[] guids);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionStoreDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionStoreDelegate.java
new file mode 100644
index 0000000000..01e44c3aea
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionStoreDelegate.java
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.delegates;
+
+import java.util.concurrent.ExecutorService;
+
+/**
+ * These methods *must* be invoked asynchronously. Use deferredStoreDelegate if you
+ * need help doing this.
+ *
+ * @author rnewman
+ *
+ */
+public interface RepositorySessionStoreDelegate {
+ public void onRecordStoreFailed(Exception ex, String recordGuid);
+
+ // Called with a GUID when store has succeeded.
+ public void onRecordStoreSucceeded(String guid);
+ public void onStoreCompleted(long storeEnd);
+ public RepositorySessionStoreDelegate deferredStoreDelegate(ExecutorService executor);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionWipeDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionWipeDelegate.java
new file mode 100644
index 0000000000..cc88307298
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionWipeDelegate.java
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.delegates;
+
+import java.util.concurrent.ExecutorService;
+
+public interface RepositorySessionWipeDelegate {
+ public void onWipeFailed(Exception ex);
+ public void onWipeSucceeded();
+ public RepositorySessionWipeDelegate deferredWipeDelegate(ExecutorService executor);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/BookmarkRecord.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/BookmarkRecord.java
new file mode 100644
index 0000000000..27b8e7151a
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/BookmarkRecord.java
@@ -0,0 +1,488 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.domain;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.Map;
+
+import org.json.simple.JSONArray;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonArrayJSONException;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.repositories.android.RepoUtils;
+
+/**
+ * Covers the fields used by all bookmark objects.
+ * @author rnewman
+ *
+ */
+public class BookmarkRecord extends Record {
+ public static final String PLACES_URI_PREFIX = "places:";
+
+ private static final String LOG_TAG = "BookmarkRecord";
+
+ public static final String COLLECTION_NAME = "bookmarks";
+ public static final long BOOKMARKS_TTL = -1; // Never ttl bookmarks.
+
+ public BookmarkRecord(String guid, String collection, long lastModified, boolean deleted) {
+ super(guid, collection, lastModified, deleted);
+ this.ttl = BOOKMARKS_TTL;
+ }
+ public BookmarkRecord(String guid, String collection, long lastModified) {
+ this(guid, collection, lastModified, false);
+ }
+ public BookmarkRecord(String guid, String collection) {
+ this(guid, collection, 0, false);
+ }
+ public BookmarkRecord(String guid) {
+ this(guid, COLLECTION_NAME, 0, false);
+ }
+ public BookmarkRecord() {
+ this(Utils.generateGuid(), COLLECTION_NAME, 0, false);
+ }
+
+ // Note: redundant accessors are evil. We're all grownups; let's just use
+ // public fields.
+ public String title;
+ public String bookmarkURI;
+ public String description;
+ public String keyword;
+ public String parentID;
+ public String parentName;
+ public long androidParentID;
+ public String type;
+ public long androidPosition;
+
+ public JSONArray children;
+ public JSONArray tags;
+
+ @Override
+ public String toString() {
+ return "#<Bookmark " + guid + " (" + androidID + "), parent " +
+ parentID + "/" + androidParentID + "/" + parentName + ">";
+ }
+
+ // Oh God, this is terribly thread-unsafe. These record objects should be immutable.
+ @SuppressWarnings("unchecked")
+ protected JSONArray copyChildren() {
+ if (this.children == null) {
+ return null;
+ }
+ JSONArray children = new JSONArray();
+ children.addAll(this.children);
+ return children;
+ }
+
+ @SuppressWarnings("unchecked")
+ protected JSONArray copyTags() {
+ if (this.tags == null) {
+ return null;
+ }
+ JSONArray tags = new JSONArray();
+ tags.addAll(this.tags);
+ return tags;
+ }
+
+ @Override
+ public Record copyWithIDs(String guid, long androidID) {
+ BookmarkRecord out = new BookmarkRecord(guid, this.collection, this.lastModified, this.deleted);
+ out.androidID = androidID;
+ out.sortIndex = this.sortIndex;
+ out.ttl = this.ttl;
+
+ // Copy BookmarkRecord fields.
+ out.title = this.title;
+ out.bookmarkURI = this.bookmarkURI;
+ out.description = this.description;
+ out.keyword = this.keyword;
+ out.parentID = this.parentID;
+ out.parentName = this.parentName;
+ out.androidParentID = this.androidParentID;
+ out.type = this.type;
+ out.androidPosition = this.androidPosition;
+
+ out.children = this.copyChildren();
+ out.tags = this.copyTags();
+
+ return out;
+ }
+
+ public boolean isBookmark() {
+ if (type == null) {
+ return false;
+ }
+ return type.equals("bookmark");
+ }
+
+ public boolean isFolder() {
+ if (type == null) {
+ return false;
+ }
+ return type.equals("folder");
+ }
+
+ public boolean isLivemark() {
+ if (type == null) {
+ return false;
+ }
+ return type.equals("livemark");
+ }
+
+ public boolean isSeparator() {
+ if (type == null) {
+ return false;
+ }
+ return type.equals("separator");
+ }
+
+ public boolean isMicrosummary() {
+ if (type == null) {
+ return false;
+ }
+ return type.equals("microsummary");
+ }
+
+ public boolean isQuery() {
+ if (type == null) {
+ return false;
+ }
+ return type.equals("query");
+ }
+
+ /**
+ * Return true if this record should have the Sync fields
+ * of a bookmark, microsummary, or query.
+ */
+ private boolean isBookmarkIsh() {
+ if (type == null) {
+ return false;
+ }
+ return type.equals("bookmark") ||
+ type.equals("microsummary") ||
+ type.equals("query");
+ }
+
+ @Override
+ protected void initFromPayload(ExtendedJSONObject payload) {
+ this.type = payload.getString("type");
+ this.title = payload.getString("title");
+ this.description = payload.getString("description");
+ this.parentID = payload.getString("parentid");
+ this.parentName = payload.getString("parentName");
+
+ if (isFolder()) {
+ try {
+ this.children = payload.getArray("children");
+ } catch (NonArrayJSONException e) {
+ Logger.error(LOG_TAG, "Got non-array children in bookmark record " + this.guid, e);
+ // Let's see if we can recover later by using the parentid pointers.
+ this.children = new JSONArray();
+ }
+ return;
+ }
+
+ final String bmkUri = payload.getString("bmkUri");
+
+ // bookmark, microsummary, query.
+ if (isBookmarkIsh()) {
+ this.keyword = payload.getString("keyword");
+ try {
+ this.tags = payload.getArray("tags");
+ } catch (NonArrayJSONException e) {
+ Logger.warn(LOG_TAG, "Got non-array tags in bookmark record " + this.guid, e);
+ this.tags = new JSONArray();
+ }
+ }
+
+ if (isBookmark()) {
+ this.bookmarkURI = bmkUri;
+ return;
+ }
+
+ if (isLivemark()) {
+ String siteUri = payload.getString("siteUri");
+ String feedUri = payload.getString("feedUri");
+ this.bookmarkURI = encodeUnsupportedTypeURI(bmkUri,
+ "siteUri", siteUri,
+ "feedUri", feedUri);
+ return;
+ }
+ if (isQuery()) {
+ String queryId = payload.getString("queryId");
+ String folderName = payload.getString("folderName");
+ this.bookmarkURI = encodeUnsupportedTypeURI(bmkUri,
+ "queryId", queryId,
+ "folderName", folderName);
+ return;
+ }
+ if (isMicrosummary()) {
+ String generatorUri = payload.getString("generatorUri");
+ String staticTitle = payload.getString("staticTitle");
+ this.bookmarkURI = encodeUnsupportedTypeURI(bmkUri,
+ "generatorUri", generatorUri,
+ "staticTitle", staticTitle);
+ return;
+ }
+ if (isSeparator()) {
+ Object p = payload.get("pos");
+ if (p instanceof Long) {
+ this.androidPosition = (Long) p;
+ } else if (p instanceof String) {
+ try {
+ this.androidPosition = Long.parseLong((String) p, 10);
+ } catch (NumberFormatException e) {
+ return;
+ }
+ } else {
+ Logger.warn(LOG_TAG, "Unsupported position value " + p);
+ return;
+ }
+ String pos = String.valueOf(this.androidPosition);
+ this.bookmarkURI = encodeUnsupportedTypeURI(null, "pos", pos, null, null);
+ return;
+ }
+ }
+
+ @Override
+ protected void populatePayload(ExtendedJSONObject payload) {
+ putPayload(payload, "type", this.type);
+ putPayload(payload, "title", this.title);
+ putPayload(payload, "description", this.description);
+ putPayload(payload, "parentid", this.parentID);
+ putPayload(payload, "parentName", this.parentName);
+ putPayload(payload, "keyword", this.keyword);
+
+ if (isFolder()) {
+ payload.put("children", this.children);
+ return;
+ }
+
+ // bookmark, microsummary, query.
+ if (isBookmarkIsh()) {
+ if (isBookmark()) {
+ payload.put("bmkUri", bookmarkURI);
+ }
+
+ if (isQuery()) {
+ Map<String, String> parts = Utils.extractURIComponents(PLACES_URI_PREFIX, this.bookmarkURI);
+ putPayload(payload, "queryId", parts.get("queryId"), true);
+ putPayload(payload, "folderName", parts.get("folderName"), true);
+ putPayload(payload, "bmkUri", parts.get("uri"));
+ return;
+ }
+
+ if (this.tags != null) {
+ payload.put("tags", this.tags);
+ }
+
+ putPayload(payload, "keyword", this.keyword);
+ return;
+ }
+
+ if (isLivemark()) {
+ Map<String, String> parts = Utils.extractURIComponents(PLACES_URI_PREFIX, this.bookmarkURI);
+ putPayload(payload, "siteUri", parts.get("siteUri"));
+ putPayload(payload, "feedUri", parts.get("feedUri"));
+ return;
+ }
+ if (isMicrosummary()) {
+ Map<String, String> parts = Utils.extractURIComponents(PLACES_URI_PREFIX, this.bookmarkURI);
+ putPayload(payload, "generatorUri", parts.get("generatorUri"));
+ putPayload(payload, "staticTitle", parts.get("staticTitle"));
+ return;
+ }
+ if (isSeparator()) {
+ Map<String, String> parts = Utils.extractURIComponents(PLACES_URI_PREFIX, this.bookmarkURI);
+ String pos = parts.get("pos");
+ if (pos == null) {
+ return;
+ }
+ try {
+ payload.put("pos", Long.parseLong(pos, 10));
+ } catch (NumberFormatException e) {
+ return;
+ }
+ return;
+ }
+ }
+
+ private void trace(String s) {
+ Logger.trace(LOG_TAG, s);
+ }
+
+ @Override
+ public boolean equalPayloads(Object o) {
+ trace("Calling BookmarkRecord.equalPayloads.");
+ if (!(o instanceof BookmarkRecord)) {
+ return false;
+ }
+
+ BookmarkRecord other = (BookmarkRecord) o;
+ if (!super.equalPayloads(other)) {
+ return false;
+ }
+
+ if (!RepoUtils.stringsEqual(this.type, other.type)) {
+ return false;
+ }
+
+ // Check children.
+ if (isFolder() && (this.children != other.children)) {
+ trace("BookmarkRecord.equals: this folder: " + this.title + ", " + this.guid);
+ trace("BookmarkRecord.equals: other: " + other.title + ", " + other.guid);
+ if (this.children == null &&
+ other.children != null) {
+ trace("Records differ: one children array is null.");
+ return false;
+ }
+ if (this.children != null &&
+ other.children == null) {
+ trace("Records differ: one children array is null.");
+ return false;
+ }
+ if (this.children.size() != other.children.size()) {
+ trace("Records differ: children arrays differ in size (" +
+ this.children.size() + " vs. " + other.children.size() + ").");
+ return false;
+ }
+
+ for (int i = 0; i < this.children.size(); i++) {
+ String child = (String) this.children.get(i);
+ if (!other.children.contains(child)) {
+ trace("Records differ: child " + child + " not found.");
+ return false;
+ }
+ }
+ }
+
+ trace("Checking strings.");
+ return RepoUtils.stringsEqual(this.title, other.title)
+ && RepoUtils.stringsEqual(this.bookmarkURI, other.bookmarkURI)
+ && RepoUtils.stringsEqual(this.parentID, other.parentID)
+ && RepoUtils.stringsEqual(this.parentName, other.parentName)
+ && RepoUtils.stringsEqual(this.description, other.description)
+ && RepoUtils.stringsEqual(this.keyword, other.keyword)
+ && jsonArrayStringsEqual(this.tags, other.tags);
+ }
+
+ // TODO: two records can be congruent if their child lists are different.
+ @Override
+ public boolean congruentWith(Object o) {
+ return this.equalPayloads(o) &&
+ super.congruentWith(o);
+ }
+
+ // Converts two JSONArrays to strings and checks if they are the same.
+ // This is only useful for stuff like tags where we aren't actually
+ // touching the data there (and therefore ordering won't change)
+ private boolean jsonArrayStringsEqual(JSONArray a, JSONArray b) {
+ // Check for nulls
+ if (a == b) return true;
+ if (a == null && b != null) return false;
+ if (a != null && b == null) return false;
+ return RepoUtils.stringsEqual(a.toJSONString(), b.toJSONString());
+ }
+
+ /**
+ * URL-encode the provided string. If the input is null,
+ * the empty string is returned.
+ *
+ * @param in the string to encode.
+ * @return a URL-encoded version of the input.
+ */
+ protected static String encode(String in) {
+ if (in == null) {
+ return "";
+ }
+ try {
+ return URLEncoder.encode(in, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ // Will never occur.
+ return null;
+ }
+ }
+
+ /**
+ * Take the provided URI and two parameters, constructing a URI like
+ *
+ * places:uri=$uri&p1=$p1&p2=$p2
+ *
+ * null values in either parameter or value result in the parameter being omitted.
+ */
+ protected static String encodeUnsupportedTypeURI(String originalURI, String p1, String v1, String p2, String v2) {
+ StringBuilder b = new StringBuilder(PLACES_URI_PREFIX);
+ boolean previous = false;
+ if (originalURI != null) {
+ b.append("uri=");
+ b.append(encode(originalURI));
+ previous = true;
+ }
+ if (p1 != null && v1 != null) {
+ if (previous) {
+ b.append("&");
+ }
+ b.append(p1);
+ b.append("=");
+ b.append(encode(v1));
+ previous = true;
+ }
+ if (p2 != null && v2 != null) {
+ if (previous) {
+ b.append("&");
+ }
+ b.append(p2);
+ b.append("=");
+ b.append(encode(v2));
+ previous = true;
+ }
+ return b.toString();
+ }
+}
+
+
+/*
+// Bookmark:
+{cleartext:
+ {id: "l7p2xqOTMMXw",
+ type: "bookmark",
+ title: "Your Flight Status",
+ parentName: "mobile",
+ bmkUri: "http: //www.flightstats.com/go/Mobile/flightStatusByFlightProcess.do;jsessionid=13A6C8DCC9592AF141A43349040262CE.web3: 8009?utm_medium=cpc&utm_campaign=co-op&utm_source=airlineInformationAndStatus&id=212492593",
+ tags: [],
+ keyword: null,
+ description: null,
+ loadInSidebar: false,
+ parentid: "mobile"},
+ data: {payload: {ciphertext: null},
+ id: "l7p2xqOTMMXw",
+ sortindex: 107},
+ collection: "bookmarks"}
+
+// Folder:
+{cleartext:
+ {id: "mobile",
+ type: "folder",
+ parentName: "",
+ title: "mobile",
+ description: null,
+ children: ["1ROdlTuIoddD", "3Z_bMIHPSZQ8", "4mSDUuOo2iVB", "8aEdE9IIrJVr",
+ "9DzPTmkkZRDb", "Qwwb99HtVKsD", "s8tM36aGPKbq", "JMTi61hOO3JV",
+ "JQUDk0wSvYip", "LmVH-J1r3HLz", "NhgQlC5ykYGW", "OVanevUUaqO2",
+ "OtQVX0PMiWQj", "_GP5cF595iie", "fkRssjXSZDL3", "k7K_NwIA1Ya0",
+ "raox_QGzvqh1", "vXYL-xHjK06k", "QKHKUN6Dm-xv", "pmN2dYWT2MJ_",
+ "EVeO_J1SQiwL", "7N-qkepS7bec", "NIGa3ha-HVOE", "2Phv1I25wbuH",
+ "TTSIAH1fV0VE", "WOmZ8PfH39Da", "gDTXNg4m1AJZ", "ayI30OZslHbO",
+ "zSEs4O3n6CzQ", "oWTDR0gO2aWf", "wWHUoFaInXi9", "F7QTuVJDpsTM",
+ "FIboggegplk-", "G4HWrT5nfRYS", "MHA7y9bupDdv", "T_Ldzmj0Ttte",
+ "U9eYu3SxsE_U", "bk463Kl9IO_m", "brUfrqJjFNSR", "ccpawfWsD-bY",
+ "l7p2xqOTMMXw", "o-nSDKtXYln7"],
+ parentid: "places"},
+ data: {payload: {ciphertext: null},
+ id: "mobile",
+ sortindex: 1000000},
+ collection: "bookmarks"}
+*/
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/BookmarkRecordFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/BookmarkRecordFactory.java
new file mode 100644
index 0000000000..edf7b288ca
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/BookmarkRecordFactory.java
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.domain;
+
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.repositories.RecordFactory;
+
+/**
+ * Turns CryptoRecords into BookmarkRecords.
+ *
+ * @author rnewman
+ *
+ */
+public class BookmarkRecordFactory extends RecordFactory {
+
+ @Override
+ public Record createRecord(Record record) {
+ BookmarkRecord r = new BookmarkRecord();
+ r.initFromEnvelope((CryptoRecord) record);
+ return r;
+ }
+
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/ClientRecord.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/ClientRecord.java
new file mode 100644
index 0000000000..0c513a4a0a
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/ClientRecord.java
@@ -0,0 +1,231 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.domain;
+
+import org.json.simple.JSONArray;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonArrayJSONException;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.repositories.android.RepoUtils;
+
+public class ClientRecord extends Record {
+ private static final String LOG_TAG = "ClientRecord";
+
+ public static final String CLIENT_TYPE = "mobile";
+ public static final String COLLECTION_NAME = "clients";
+ public static final long CLIENTS_TTL = 21 * 24 * 60 * 60; // 21 days in seconds.
+ public static final String DEFAULT_CLIENT_NAME = "Default Name";
+
+ public static final String PROTOCOL_LEGACY_SYNC = "1.1";
+ public static final String PROTOCOL_FXA_SYNC = "1.5";
+
+ /**
+ * Each of these fields is 'owned' by the client it represents. For example,
+ * the "version" field is the Firefox version of that client; some time after
+ * that client upgrades, it'll upload a new record with its new version.
+ *
+ * The only exception is for commands. When a command is sent to a client, the
+ * sender will download its current record, append the command to the
+ * "commands" array, and reupload the record. After processing, the recipient
+ * will reupload its record with an empty commands array.
+ *
+ * Note that the version, then, will remain the version of the recipient, as
+ * with the other descriptive fields.
+ */
+ public String name = ClientRecord.DEFAULT_CLIENT_NAME;
+ public String type = ClientRecord.CLIENT_TYPE;
+ public String version = null; // Free-form string, optional.
+ public JSONArray commands;
+ public JSONArray protocols;
+
+ // Optional fields.
+ // See <https://github.com/mozilla-services/docs/blob/master/source/sync/objectformats.rst#user-content-clients>
+ // for full formats.
+ // If a value isn't known, the field is omitted.
+ public String formfactor; // "phone", "largetablet", "smalltablet", "desktop", "laptop", "tv".
+ public String os; // One of "Android", "Darwin", "WINNT", "Linux", "iOS", "Firefox OS".
+ public String application; // Display name, E.g., "Firefox Beta"
+ public String appPackage; // E.g., "org.mozilla.firefox_beta"
+ public String device; // E.g., "HTC One"
+ public String fxaDeviceId; // E.g., "525b624eaaf1e40d21ec8997c3116ad8"
+
+ public ClientRecord(String guid, String collection, long lastModified, boolean deleted) {
+ super(guid, collection, lastModified, deleted);
+ this.ttl = CLIENTS_TTL;
+ }
+
+ public ClientRecord(String guid, String collection, long lastModified) {
+ this(guid, collection, lastModified, false);
+ }
+
+ public ClientRecord(String guid, String collection) {
+ this(guid, collection, 0, false);
+ }
+
+ public ClientRecord(String guid) {
+ this(guid, COLLECTION_NAME, 0, false);
+ }
+
+ public ClientRecord() {
+ this(Utils.generateGuid(), COLLECTION_NAME, 0, false);
+ }
+
+ @Override
+ protected void initFromPayload(ExtendedJSONObject payload) {
+ this.name = (String) payload.get("name");
+ this.type = (String) payload.get("type");
+ try {
+ this.version = (String) payload.get("version");
+ } catch (Exception e) {
+ // Oh well.
+ }
+
+ try {
+ commands = payload.getArray("commands");
+ } catch (NonArrayJSONException e) {
+ Logger.debug(LOG_TAG, "Got non-array commands in client record " + guid, e);
+ commands = null;
+ }
+
+ try {
+ protocols = payload.getArray("protocols");
+ } catch (NonArrayJSONException e) {
+ Logger.debug(LOG_TAG, "Got non-array protocols in client record " + guid, e);
+ protocols = null;
+ }
+
+ if (payload.containsKey("formfactor")) {
+ this.formfactor = payload.getString("formfactor");
+ }
+
+ if (payload.containsKey("os")) {
+ this.os = payload.getString("os");
+ }
+
+ if (payload.containsKey("application")) {
+ this.application = payload.getString("application");
+ }
+
+ if (payload.containsKey("appPackage")) {
+ this.appPackage = payload.getString("appPackage");
+ }
+
+ if (payload.containsKey("device")) {
+ this.device = payload.getString("device");
+ }
+
+ if (payload.containsKey("fxaDeviceId")) {
+ this.fxaDeviceId = payload.getString("fxaDeviceId");
+ }
+ }
+
+ @Override
+ protected void populatePayload(ExtendedJSONObject payload) {
+ putPayload(payload, "id", this.guid);
+ putPayload(payload, "name", this.name);
+ putPayload(payload, "type", this.type);
+ putPayload(payload, "version", this.version);
+
+ if (this.commands != null) {
+ payload.put("commands", this.commands);
+ }
+
+ if (this.protocols != null) {
+ payload.put("protocols", this.protocols);
+ }
+
+ if (this.formfactor != null) {
+ payload.put("formfactor", this.formfactor);
+ }
+
+ if (this.os != null) {
+ payload.put("os", this.os);
+ }
+
+ if (this.application != null) {
+ payload.put("application", this.application);
+ }
+
+ if (this.appPackage != null) {
+ payload.put("appPackage", this.appPackage);
+ }
+
+ if (this.device != null) {
+ payload.put("device", this.device);
+ }
+
+ if (this.fxaDeviceId != null) {
+ payload.put("fxaDeviceId", this.fxaDeviceId);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof ClientRecord) || !super.equals(o)) {
+ return false;
+ }
+
+ return this.equalPayloads(o);
+ }
+
+ @Override
+ public int hashCode() {
+ return super.hashCode();
+ }
+
+ @Override
+ public boolean equalPayloads(Object o) {
+ if (!(o instanceof ClientRecord) || !super.equalPayloads(o)) {
+ return false;
+ }
+
+ // Don't compare versions, protocols, or other optional fields, no matter how much we might want to.
+ // They're not required by the spec.
+ ClientRecord other = (ClientRecord) o;
+ if (!RepoUtils.stringsEqual(other.name, this.name) ||
+ !RepoUtils.stringsEqual(other.type, this.type)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public Record copyWithIDs(String guid, long androidID) {
+ ClientRecord out = new ClientRecord(guid, this.collection, this.lastModified, this.deleted);
+ out.androidID = androidID;
+ out.sortIndex = this.sortIndex;
+ out.ttl = this.ttl;
+
+ out.name = this.name;
+ out.type = this.type;
+ out.version = this.version;
+ out.protocols = this.protocols;
+
+ out.formfactor = this.formfactor;
+ out.os = this.os;
+ out.application = this.application;
+ out.appPackage = this.appPackage;
+ out.device = this.device;
+ out.fxaDeviceId = this.fxaDeviceId;
+
+ return out;
+ }
+
+/*
+Example record:
+
+{id:"relf31w7B4F1",
+ name:"marina_mac",
+ type:"mobile"
+ commands:[{"args":["bookmarks"],"command":"wipeEngine"},
+ {"args":["forms"],"command":"wipeEngine"},
+ {"args":["history"],"command":"wipeEngine"},
+ {"args":["passwords"],"command":"wipeEngine"},
+ {"args":["prefs"],"command":"wipeEngine"},
+ {"args":["tabs"],"command":"wipeEngine"},
+ {"args":["addons"],"command":"wipeEngine"}]}
+*/
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/ClientRecordFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/ClientRecordFactory.java
new file mode 100644
index 0000000000..897d2859c3
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/ClientRecordFactory.java
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.domain;
+
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.repositories.RecordFactory;
+
+public class ClientRecordFactory extends RecordFactory {
+ @Override
+ public Record createRecord(Record record) {
+ ClientRecord r = new ClientRecord();
+ r.initFromEnvelope((CryptoRecord) record);
+ return r;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/FormHistoryRecord.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/FormHistoryRecord.java
new file mode 100644
index 0000000000..e7ca70cb41
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/FormHistoryRecord.java
@@ -0,0 +1,139 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.domain;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.repositories.android.RepoUtils;
+
+/**
+ * A FormHistoryRecord represents a saved form element.
+ *
+ * I map a <code>fieldName</code> string to a <code>value</code> string.
+ *
+ * @see "<a href='http://dxr.mozilla.org/services-central/source/services-central/services/sync/modules/engines/forms.js'>http://dxr.mozilla.org/services-central/source/services-central/services/sync/modules/engines/forms.js</a>."
+ */
+public class FormHistoryRecord extends Record {
+ private static final String LOG_TAG = "FormHistoryRecord";
+
+ public static final String COLLECTION_NAME = "forms";
+ private static final String PAYLOAD_NAME = "name";
+ private static final String PAYLOAD_VALUE = "value";
+ public static final long FORMS_TTL = 3 * 365 * 24 * 60 * 60; // Three years in seconds.
+
+ /**
+ * The name of the saved form field.
+ */
+ public String fieldName;
+
+ /**
+ * The value of the saved form field.
+ */
+ public String fieldValue;
+
+ public FormHistoryRecord(String guid, String collection, long lastModified, boolean deleted) {
+ super(guid, collection, lastModified, deleted);
+ this.ttl = FORMS_TTL;
+ }
+
+ public FormHistoryRecord(String guid, String collection, long lastModified) {
+ this(guid, collection, lastModified, false);
+ }
+
+ public FormHistoryRecord(String guid, String collection) {
+ this(guid, collection, 0, false);
+ }
+
+ public FormHistoryRecord(String guid) {
+ this(guid, COLLECTION_NAME, 0, false);
+ }
+
+ public FormHistoryRecord() {
+ this(Utils.generateGuid(), COLLECTION_NAME, 0, false);
+ }
+
+ @Override
+ public Record copyWithIDs(String guid, long androidID) {
+ FormHistoryRecord out = new FormHistoryRecord(guid, this.collection, this.lastModified, this.deleted);
+ out.androidID = androidID;
+ out.sortIndex = this.sortIndex;
+
+ // Copy FormHistoryRecord fields.
+ out.fieldName = this.fieldName;
+ out.fieldValue = this.fieldValue;
+
+ return out;
+ }
+
+ @Override
+ public void populatePayload(ExtendedJSONObject payload) {
+ putPayload(payload, PAYLOAD_NAME, this.fieldName);
+ putPayload(payload, PAYLOAD_VALUE, this.fieldValue);
+ }
+
+ @Override
+ public void initFromPayload(ExtendedJSONObject payload) {
+ this.fieldName = payload.getString(PAYLOAD_NAME);
+ this.fieldValue = payload.getString(PAYLOAD_VALUE);
+ }
+
+ /**
+ * We consider two form history records to be congruent if they represent the
+ * same form element regardless of times used.
+ */
+ @Override
+ public boolean congruentWith(Object o) {
+ if (!(o instanceof FormHistoryRecord)) {
+ return false;
+ }
+ FormHistoryRecord other = (FormHistoryRecord) o;
+ if (!super.congruentWith(other)) {
+ return false;
+ }
+ return RepoUtils.stringsEqual(this.fieldName, other.fieldName) &&
+ RepoUtils.stringsEqual(this.fieldValue, other.fieldValue);
+ }
+
+ @Override
+ public boolean equalPayloads(Object o) {
+ if (!(o instanceof FormHistoryRecord)) {
+ Logger.debug(LOG_TAG, "Not a FormHistoryRecord: " + o.getClass());
+ return false;
+ }
+ FormHistoryRecord other = (FormHistoryRecord) o;
+ if (!super.equalPayloads(other)) {
+ Logger.debug(LOG_TAG, "super.equalPayloads returned false.");
+ return false;
+ }
+
+ if (this.deleted) {
+ // FormHistoryRecords are equal if they are both deleted (which
+ // they are, since super.equalPayloads is true) and have the
+ // same GUID.
+ if (other.deleted) {
+ return RepoUtils.stringsEqual(this.guid, other.guid);
+ }
+ return false;
+ }
+
+ return RepoUtils.stringsEqual(this.fieldName, other.fieldName) &&
+ RepoUtils.stringsEqual(this.fieldValue, other.fieldValue);
+ }
+
+ public FormHistoryRecord log(String logTag) {
+ try {
+ Logger.debug(logTag, "Returning form history record " + guid + " (" + androidID + ")");
+ Logger.debug(logTag, "> Last modified: " + lastModified);
+ if (Logger.LOG_PERSONAL_INFORMATION) {
+ Logger.pii(logTag, "> Field name: " + fieldName);
+ Logger.pii(logTag, "> Field value: " + fieldValue);
+ }
+ } catch (Exception e) {
+ Logger.debug(logTag, "Exception logging form history record " + this, e);
+ }
+ return this;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/HistoryRecord.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/HistoryRecord.java
new file mode 100644
index 0000000000..94eae13a7b
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/HistoryRecord.java
@@ -0,0 +1,217 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.domain;
+
+import java.util.HashMap;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonArrayJSONException;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.repositories.android.RepoUtils;
+
+/**
+ * Visits are in microsecond precision.
+ *
+ * @author rnewman
+ *
+ */
+public class HistoryRecord extends Record {
+ private static final String LOG_TAG = "HistoryRecord";
+
+ public static final String COLLECTION_NAME = "history";
+ public static final long HISTORY_TTL = 60 * 24 * 60 * 60; // 60 days in seconds.
+
+ public HistoryRecord(String guid, String collection, long lastModified, boolean deleted) {
+ super(guid, collection, lastModified, deleted);
+ this.ttl = HISTORY_TTL;
+ }
+ public HistoryRecord(String guid, String collection, long lastModified) {
+ this(guid, collection, lastModified, false);
+ }
+ public HistoryRecord(String guid, String collection) {
+ this(guid, collection, 0, false);
+ }
+ public HistoryRecord(String guid) {
+ this(guid, COLLECTION_NAME, 0, false);
+ }
+ public HistoryRecord() {
+ this(Utils.generateGuid(), COLLECTION_NAME, 0, false);
+ }
+
+ public String title;
+ public String histURI;
+ public JSONArray visits;
+ public long fennecDateVisited;
+ public long fennecVisitCount;
+
+ @SuppressWarnings("unchecked")
+ private JSONArray copyVisits() {
+ if (this.visits == null) {
+ return null;
+ }
+ JSONArray out = new JSONArray();
+ out.addAll(this.visits);
+ return out;
+ }
+
+ @Override
+ public Record copyWithIDs(String guid, long androidID) {
+ HistoryRecord out = new HistoryRecord(guid, this.collection, this.lastModified, this.deleted);
+ out.androidID = androidID;
+ out.sortIndex = this.sortIndex;
+ out.ttl = this.ttl;
+
+ // Copy HistoryRecord fields.
+ out.title = this.title;
+ out.histURI = this.histURI;
+ out.fennecDateVisited = this.fennecDateVisited;
+ out.fennecVisitCount = this.fennecVisitCount;
+ out.visits = this.copyVisits();
+
+ return out;
+ }
+
+ @Override
+ protected void populatePayload(ExtendedJSONObject payload) {
+ putPayload(payload, "id", this.guid);
+ putPayload(payload, "title", this.title);
+ putPayload(payload, "histUri", this.histURI); // TODO: encoding?
+ payload.put("visits", this.visits);
+ }
+
+ @Override
+ protected void initFromPayload(ExtendedJSONObject payload) {
+ this.histURI = (String) payload.get("histUri");
+ this.title = (String) payload.get("title");
+ try {
+ this.visits = payload.getArray("visits");
+ } catch (NonArrayJSONException e) {
+ Logger.error(LOG_TAG, "Got non-array visits in history record " + this.guid, e);
+ this.visits = new JSONArray();
+ }
+ }
+
+ /**
+ * We consider two history records to be congruent if they represent the
+ * same history record regardless of visits. Titles are allowed to differ,
+ * but the URI must be the same.
+ */
+ @Override
+ public boolean congruentWith(Object o) {
+ if (!(o instanceof HistoryRecord)) {
+ return false;
+ }
+ HistoryRecord other = (HistoryRecord) o;
+ if (!super.congruentWith(other)) {
+ return false;
+ }
+ return RepoUtils.stringsEqual(this.histURI, other.histURI);
+ }
+
+ @Override
+ public boolean equalPayloads(Object o) {
+ if (!(o instanceof HistoryRecord)) {
+ Logger.debug(LOG_TAG, "Not a HistoryRecord: " + o.getClass());
+ return false;
+ }
+ HistoryRecord other = (HistoryRecord) o;
+ if (!super.equalPayloads(other)) {
+ Logger.debug(LOG_TAG, "super.equalPayloads returned false.");
+ return false;
+ }
+ return RepoUtils.stringsEqual(this.title, other.title) &&
+ RepoUtils.stringsEqual(this.histURI, other.histURI) &&
+ checkVisitsEquals(other);
+ }
+
+ @Override
+ public boolean equalAndroidIDs(Record other) {
+ return super.equalAndroidIDs(other) &&
+ this.equalFennecVisits(other);
+ }
+
+ private boolean equalFennecVisits(Record other) {
+ if (!(other instanceof HistoryRecord)) {
+ return false;
+ }
+ HistoryRecord h = (HistoryRecord) other;
+ return this.fennecDateVisited == h.fennecDateVisited &&
+ this.fennecVisitCount == h.fennecVisitCount;
+ }
+
+ private boolean checkVisitsEquals(HistoryRecord other) {
+ Logger.debug(LOG_TAG, "Checking visits.");
+ if (Logger.LOG_PERSONAL_INFORMATION) {
+ // Don't JSON-encode unless we're logging.
+ Logger.pii(LOG_TAG, ">> Mine: " + ((this.visits == null) ? "null" : this.visits.toJSONString()));
+ Logger.pii(LOG_TAG, ">> Theirs: " + ((other.visits == null) ? "null" : other.visits.toJSONString()));
+ }
+
+ // Handle nulls.
+ if (this.visits == other.visits) {
+ return true;
+ }
+
+ // Now they can't both be null.
+ int aSize = this.visits == null ? 0 : this.visits.size();
+ int bSize = other.visits == null ? 0 : other.visits.size();
+
+ if (aSize != bSize) {
+ return false;
+ }
+
+ // Now neither of them can be null.
+
+ // TODO: do this by maintaining visits as a sorted array.
+ HashMap<Long, Long> otherVisits = new HashMap<Long, Long>();
+ for (int i = 0; i < bSize; i++) {
+ JSONObject visit = (JSONObject) other.visits.get(i);
+ otherVisits.put((Long) visit.get("date"), (Long) visit.get("type"));
+ }
+
+ for (int i = 0; i < aSize; i++) {
+ JSONObject visit = (JSONObject) this.visits.get(i);
+ if (!otherVisits.containsKey(visit.get("date"))) {
+ return false;
+ }
+ Long otherDate = (Long) visit.get("date");
+ Long otherType = otherVisits.get(otherDate);
+ if (otherType == null) {
+ return false;
+ }
+ if (!otherType.equals((Long) visit.get("type"))) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+//
+// Example record (note microsecond resolution):
+//
+// {id:"--DUvUomABNq",
+// histUri:"https://bugzilla.mozilla.org/show_bug.cgi?id=697634",
+// title:"697634 \u2013 xpcshell test failures on 10.7",
+// visits:[{date:1320087601465600, type:2},
+// {date:1320084970724990, type:1},
+// {date:1320084847035717, type:1},
+// {date:1319764134412287, type:1},
+// {date:1319757917982518, type:1},
+// {date:1319751664627351, type:1},
+// {date:1319681421072326, type:1},
+// {date:1319681306455594, type:1},
+// {date:1319678117125234, type:1},
+// {date:1319677508862901, type:1}]
+// }
+//
+//"type" is a transition type:
+//
+//https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsINavHistoryService#Transition_type_constants
+
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/HistoryRecordFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/HistoryRecordFactory.java
new file mode 100644
index 0000000000..ac2c6a1dcb
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/HistoryRecordFactory.java
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.domain;
+
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.repositories.RecordFactory;
+
+/**
+ * Turns CryptoRecords into HistoryRecords.
+ *
+ * @author rnewman
+ *
+ */
+public class HistoryRecordFactory extends RecordFactory {
+
+ @Override
+ public Record createRecord(Record record) {
+ HistoryRecord r = new HistoryRecord();
+ r.initFromEnvelope((CryptoRecord) record);
+ return r;
+ }
+
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/PasswordRecord.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/PasswordRecord.java
new file mode 100644
index 0000000000..b2de60f3c1
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/PasswordRecord.java
@@ -0,0 +1,205 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.domain;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.repositories.android.RepoUtils;
+
+public class PasswordRecord extends Record {
+ private static final String LOG_TAG = "PasswordRecord";
+
+ public static final String COLLECTION_NAME = "passwords";
+ public static long PASSWORDS_TTL = -1; // Never expire passwords.
+
+ // Payload strings.
+ public static final String PAYLOAD_HOSTNAME = "hostname";
+ public static final String PAYLOAD_FORM_SUBMIT_URL = "formSubmitURL";
+ public static final String PAYLOAD_HTTP_REALM = "httpRealm";
+ public static final String PAYLOAD_USERNAME = "username";
+ public static final String PAYLOAD_PASSWORD = "password";
+ public static final String PAYLOAD_USERNAME_FIELD = "usernameField";
+ public static final String PAYLOAD_PASSWORD_FIELD = "passwordField";
+
+ public PasswordRecord(String guid, String collection, long lastModified, boolean deleted) {
+ super(guid, collection, lastModified, deleted);
+ this.ttl = PASSWORDS_TTL;
+ }
+ public PasswordRecord(String guid, String collection, long lastModified) {
+ this(guid, collection, lastModified, false);
+ }
+ public PasswordRecord(String guid, String collection) {
+ this(guid, collection, 0, false);
+ }
+ public PasswordRecord(String guid) {
+ this(guid, COLLECTION_NAME, 0, false);
+ }
+ public PasswordRecord() {
+ this(Utils.generateGuid(), COLLECTION_NAME, 0, false);
+ }
+
+ public String id;
+ public String hostname;
+ public String formSubmitURL;
+ public String httpRealm;
+ // TODO these are encrypted in the passwords content provider,
+ // need to figure out what we need to do here.
+ public String usernameField;
+ public String passwordField;
+ public String encryptedUsername;
+ public String encryptedPassword;
+ public String encType;
+
+ public long timeCreated;
+ public long timeLastUsed;
+ public long timePasswordChanged;
+ public long timesUsed;
+
+
+ @Override
+ public Record copyWithIDs(String guid, long androidID) {
+ PasswordRecord out = new PasswordRecord(guid, this.collection, this.lastModified, this.deleted);
+ out.androidID = androidID;
+ out.sortIndex = this.sortIndex;
+ out.ttl = this.ttl;
+
+ // Copy PasswordRecord fields.
+ out.id = this.id;
+ out.hostname = this.hostname;
+ out.formSubmitURL = this.formSubmitURL;
+ out.httpRealm = this.httpRealm;
+
+ out.usernameField = this.usernameField;
+ out.passwordField = this.passwordField;
+ out.encryptedUsername = this.encryptedUsername;
+ out.encryptedPassword = this.encryptedPassword;
+ out.encType = this.encType;
+
+ out.timeCreated = this.timeCreated;
+ out.timeLastUsed = this.timeLastUsed;
+ out.timePasswordChanged = this.timePasswordChanged;
+ out.timesUsed = this.timesUsed;
+
+ return out;
+ }
+
+ @Override
+ public void initFromPayload(ExtendedJSONObject payload) {
+ this.hostname = payload.getString(PAYLOAD_HOSTNAME);
+ this.formSubmitURL = payload.getString(PAYLOAD_FORM_SUBMIT_URL);
+ this.httpRealm = payload.getString(PAYLOAD_HTTP_REALM);
+ this.encryptedUsername = payload.getString(PAYLOAD_USERNAME);
+ this.encryptedPassword = payload.getString(PAYLOAD_PASSWORD);
+ this.usernameField = payload.getString(PAYLOAD_USERNAME_FIELD);
+ this.passwordField = payload.getString(PAYLOAD_PASSWORD_FIELD);
+ }
+
+ @Override
+ public void populatePayload(ExtendedJSONObject payload) {
+ putPayload(payload, PAYLOAD_HOSTNAME, this.hostname);
+ putPayload(payload, PAYLOAD_FORM_SUBMIT_URL, this.formSubmitURL);
+ putPayload(payload, PAYLOAD_HTTP_REALM, this.httpRealm);
+ putPayload(payload, PAYLOAD_USERNAME, this.encryptedUsername);
+ putPayload(payload, PAYLOAD_PASSWORD, this.encryptedPassword);
+ putPayload(payload, PAYLOAD_USERNAME_FIELD, this.usernameField);
+ putPayload(payload, PAYLOAD_PASSWORD_FIELD, this.passwordField);
+ }
+
+ @Override
+ public boolean congruentWith(Object o) {
+ if (!(o instanceof PasswordRecord)) {
+ return false;
+ }
+ PasswordRecord other = (PasswordRecord) o;
+ if (!super.congruentWith(other)) {
+ return false;
+ }
+ return RepoUtils.stringsEqual(this.hostname, other.hostname)
+ && RepoUtils.stringsEqual(this.formSubmitURL, other.formSubmitURL)
+ // Bug 738347 - SQLiteBridge does not check for nulls in ContentValues.
+ // && RepoUtils.stringsEqual(this.httpRealm, other.httpRealm)
+ // && RepoUtils.stringsEqual(this.encType, other.encType)
+ && RepoUtils.stringsEqual(this.usernameField, other.usernameField)
+ && RepoUtils.stringsEqual(this.passwordField, other.passwordField)
+ && RepoUtils.stringsEqual(this.encryptedUsername, other.encryptedUsername)
+ && RepoUtils.stringsEqual(this.encryptedPassword, other.encryptedPassword);
+ }
+
+ @Override
+ public boolean equalPayloads(Object o) {
+ if (!(o instanceof PasswordRecord)) {
+ return false;
+ }
+
+ PasswordRecord other = (PasswordRecord) o;
+ Logger.debug("PasswordRecord", "thisRecord:" + this.toString());
+ Logger.debug("PasswordRecord", "otherRecord:" + o.toString());
+
+ if (this.deleted) {
+ if (other.deleted) {
+ // Deleted records are equal if their guids match.
+ return RepoUtils.stringsEqual(this.guid, other.guid);
+ }
+ // One record is deleted, the other is not. Not equal.
+ return false;
+ }
+
+ if (!super.equalPayloads(other)) {
+ Logger.debug(LOG_TAG, "super.equalPayloads returned false.");
+ return false;
+ }
+
+ return RepoUtils.stringsEqual(this.hostname, other.hostname)
+ && RepoUtils.stringsEqual(this.formSubmitURL, other.formSubmitURL)
+ // Bug 738347 - SQLiteBridge does not check for nulls in ContentValues.
+ // && RepoUtils.stringsEqual(this.httpRealm, other.httpRealm)
+ // && RepoUtils.stringsEqual(this.encType, other.encType)
+ && RepoUtils.stringsEqual(this.usernameField, other.usernameField)
+ && RepoUtils.stringsEqual(this.passwordField, other.passwordField)
+ && RepoUtils.stringsEqual(this.encryptedUsername, other.encryptedUsername)
+ && RepoUtils.stringsEqual(this.encryptedPassword, other.encryptedPassword);
+ // Desktop sync never sets timeCreated so this isn't relevant for sync records.
+ }
+
+ @Override
+ public String toString() {
+ return "PasswordRecord {"
+ + "lastModified: " + this.lastModified + ", "
+ + "hostname null?: " + (this.hostname == null) + ", "
+ + "formSubmitURL null?: " + (this.formSubmitURL == null) + ", "
+ + "httpRealm null?: " + (this.httpRealm == null) + ", "
+ + "usernameField null?: " + (this.usernameField == null) + ", "
+ + "passwordField null?: " + (this.passwordField == null) + ", "
+ + "encryptedUsername null?: " + (this.encryptedUsername == null) + ", "
+ + "encryptedPassword null?: " + (this.encryptedPassword == null) + ", "
+ + "encType: " + this.encType + ", "
+ + "timeCreated: " + this.timeCreated + ", "
+ + "timeLastUsed: " + this.timeLastUsed + ", "
+ + "timePasswordChanged: " + this.timePasswordChanged + ", "
+ + "timesUsed: " + this.timesUsed;
+ }
+
+ /**
+ * A PasswordRecord is considered valid if it abides by the database
+ * constraints of the PasswordsProvider (moz_logins).
+ *
+ * See toolkit/components/passwordmgr/storage-mozStorage.js for the
+ * definitions:
+ *
+ * http://hg.mozilla.org/mozilla-central/file/00955d61cc94/toolkit/components/passwordmgr/storage-mozStorage.js#l98
+ */
+ public boolean isValid() {
+ if (this.deleted) {
+ return true;
+ }
+
+ return this.hostname != null &&
+ this.encryptedUsername != null &&
+ this.encryptedPassword != null &&
+ this.usernameField != null &&
+ this.passwordField != null;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/PasswordRecordFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/PasswordRecordFactory.java
new file mode 100644
index 0000000000..fc7ef916d5
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/PasswordRecordFactory.java
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.domain;
+
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.repositories.RecordFactory;
+import org.mozilla.gecko.sync.repositories.domain.PasswordRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+public class PasswordRecordFactory extends RecordFactory {
+ @Override
+ public Record createRecord(Record record) {
+ PasswordRecord r = new PasswordRecord();
+ r.initFromEnvelope((CryptoRecord) record);
+ return r;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/Record.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/Record.java
new file mode 100644
index 0000000000..145704c1c8
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/Record.java
@@ -0,0 +1,308 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.domain;
+
+import java.io.UnsupportedEncodingException;
+
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+/**
+ * Record is the abstract base class for all entries that Sync processes:
+ * bookmarks, passwords, history, and such.
+ *
+ * A Record can be initialized from or serialized to a CryptoRecord for
+ * submission to an encrypted store.
+ *
+ * Records should be considered to be conventionally immutable: modifications
+ * should be completed before the new record object escapes its constructing
+ * scope. Note that this is a critically important part of equality. As Rich
+ * Hickey notes:
+ *
+ * … the only things you can really compare for equality are immutable things,
+ * because if you compare two things for equality that are mutable, and ever
+ * say true, and they're ever not the same thing, you are wrong. Or you will
+ * become wrong at some point in the future.
+ *
+ * Records have a layered definition of equality. Two records can be said to be
+ * "equal" if:
+ *
+ * * They have the same GUID and collection. Two crypto/keys records are in some
+ * way "the same".
+ * This is `equalIdentifiers`.
+ *
+ * * Their most significant fields are the same. That is to say, they share a
+ * GUID, a collection, deletion, and domain-specific fields. Two copies of
+ * crypto/keys, neither deleted, with the same encrypted data but different
+ * modified times and sortIndex are in a stronger way "the same".
+ * This is `equalPayloads`.
+ *
+ * * Their most significant fields are the same, and their local fields (e.g.,
+ * the androidID to which we have decided that this record maps) are congruent.
+ * A record with the same androidID, or one whose androidID has not been set,
+ * can be considered "the same".
+ * This concept can be extended by Record subclasses. The key point is that
+ * reconciling should be applied to the contents of these records. For example,
+ * two history records with the same URI and GUID, but different visit arrays,
+ * can be said to be congruent.
+ * This is `congruentWith`.
+ *
+ * * They are strictly identical. Every field that is persisted, including
+ * lastModified and androidID, is equal.
+ * This is `equals`.
+ *
+ * Different parts of the codebase have use for different layers of this
+ * comparison hierarchy. For instance, lastModified times change every time a
+ * record is stored; a store followed by a retrieval will return a Record that
+ * shares its most significant fields with the input, but has a later
+ * lastModified time and might not yet have values set for others. Reconciling
+ * will thus ignore the modification time of a record.
+ *
+ * @author rnewman
+ *
+ */
+public abstract class Record {
+
+ public String guid;
+ public String collection;
+ public long lastModified;
+ public boolean deleted;
+ public long androidID;
+ /**
+ * An integer indicating the relative importance of this item in the collection.
+ * <p>
+ * Default is 0.
+ */
+ public long sortIndex;
+ /**
+ * The number of seconds to keep this record. After that time this item will
+ * no longer be returned in response to any request, and it may be pruned from
+ * the database.
+ * <p>
+ * Negative values mean never forget this record.
+ * <p>
+ * Default is 1 year.
+ */
+ public long ttl;
+
+ public Record(String guid, String collection, long lastModified, boolean deleted) {
+ this.guid = guid;
+ this.collection = collection;
+ this.lastModified = lastModified;
+ this.deleted = deleted;
+ this.sortIndex = 0;
+ this.ttl = 365 * 24 * 60 * 60; // Seconds.
+ this.androidID = -1;
+ }
+
+ /**
+ * Return true iff the input is a Record and has the same
+ * collection and guid as this object.
+ */
+ public boolean equalIdentifiers(Object o) {
+ if (!(o instanceof Record)) {
+ return false;
+ }
+
+ Record other = (Record) o;
+ if (this.guid == null) {
+ if (other.guid != null) {
+ return false;
+ }
+ } else {
+ if (!this.guid.equals(other.guid)) {
+ return false;
+ }
+ }
+ if (this.collection == null) {
+ if (other.collection != null) {
+ return false;
+ }
+ } else {
+ if (!this.collection.equals(other.collection)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * @param o
+ * The object to which this object should be compared.
+ * @return
+ * true iff the input is a Record which is substantially the
+ * same as this object.
+ */
+ public boolean equalPayloads(Object o) {
+ if (!this.equalIdentifiers(o)) {
+ return false;
+ }
+ Record other = (Record) o;
+ return this.deleted == other.deleted;
+ }
+
+ /**
+ *
+ *
+ * @param o
+ * The object to which this object should be compared.
+ * @return
+ * true iff the input is a Record which is substantially the
+ * same as this object, considering the ability and desire to
+ * reconcile the two objects if possible.
+ */
+ public boolean congruentWith(Object o) {
+ if (!this.equalIdentifiers(o)) {
+ return false;
+ }
+ Record other = (Record) o;
+ return congruentAndroidIDs(other) &&
+ (this.deleted == other.deleted);
+ }
+
+ public boolean congruentAndroidIDs(Record other) {
+ // We treat -1 as "unset", and treat this as
+ // congruent with any other value.
+ if (this.androidID != -1 &&
+ other.androidID != -1 &&
+ this.androidID != other.androidID) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Return true iff the input is both equal in terms of payload,
+ * and also shares transient values such as timestamps.
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Record)) {
+ return false;
+ }
+
+ Record other = (Record) o;
+ return equalTimestamps(other) &&
+ equalSortIndices(other) &&
+ equalAndroidIDs(other) &&
+ equalPayloads(o);
+ }
+
+ public boolean equalAndroidIDs(Record other) {
+ return this.androidID == other.androidID;
+ }
+
+ public boolean equalSortIndices(Record other) {
+ return this.sortIndex == other.sortIndex;
+ }
+
+ public boolean equalTimestamps(Object o) {
+ if (!(o instanceof Record)) {
+ return false;
+ }
+ return ((Record) o).lastModified == this.lastModified;
+ }
+
+ protected abstract void populatePayload(ExtendedJSONObject payload);
+ protected abstract void initFromPayload(ExtendedJSONObject payload);
+
+ public void initFromEnvelope(CryptoRecord envelope) {
+ ExtendedJSONObject p = envelope.payload;
+ this.guid = envelope.guid;
+ checkGUIDs(p);
+
+ this.collection = envelope.collection;
+ this.lastModified = envelope.lastModified;
+
+ final Object del = p.get("deleted");
+ if (del instanceof Boolean) {
+ this.deleted = (Boolean) del;
+ } else {
+ this.initFromPayload(p);
+ }
+
+ }
+
+ public CryptoRecord getEnvelope() {
+ CryptoRecord rec = new CryptoRecord(this);
+ ExtendedJSONObject payload = new ExtendedJSONObject();
+ payload.put("id", this.guid);
+
+ if (this.deleted) {
+ payload.put("deleted", true);
+ } else {
+ populatePayload(payload);
+ }
+ rec.payload = payload;
+ return rec;
+ }
+
+ @SuppressWarnings("static-method")
+ public String toJSONString() {
+ throw new RuntimeException("Cannot JSONify non-CryptoRecord Records.");
+ }
+
+ public byte[] toJSONBytes() {
+ try {
+ return this.toJSONString().getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ // Can't happen.
+ return null;
+ }
+ }
+
+ /**
+ * Utility for safely populating an output CryptoRecord.
+ *
+ * @param rec
+ * @param key
+ * @param value
+ */
+ @SuppressWarnings("static-method")
+ protected void putPayload(CryptoRecord rec, String key, String value) {
+ if (value == null) {
+ return;
+ }
+ rec.payload.put(key, value);
+ }
+
+ protected void putPayload(ExtendedJSONObject payload, String key, String value) {
+ this.putPayload(payload, key, value, false);
+ }
+
+ @SuppressWarnings("static-method")
+ protected void putPayload(ExtendedJSONObject payload, String key, String value, boolean excludeEmpty) {
+ if (value == null) {
+ return;
+ }
+ if (excludeEmpty && value.equals("")) {
+ return;
+ }
+ payload.put(key, value);
+ }
+
+ protected void checkGUIDs(ExtendedJSONObject payload) {
+ String payloadGUID = (String) payload.get("id");
+ if (this.guid == null ||
+ payloadGUID == null) {
+ String detailMessage = "Inconsistency: either envelope or payload GUID missing.";
+ throw new IllegalStateException(detailMessage);
+ }
+ if (!this.guid.equals(payloadGUID)) {
+ String detailMessage = "Inconsistency: record has envelope ID " + this.guid + ", payload ID " + payloadGUID;
+ throw new IllegalStateException(detailMessage);
+ }
+ }
+
+ /**
+ * Oh for persistent data structures.
+ *
+ * @param guid
+ * @param androidID
+ * @return
+ * An identical copy of this record with the provided two values.
+ */
+ public abstract Record copyWithIDs(String guid, long androidID);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/RecordParseException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/RecordParseException.java
new file mode 100644
index 0000000000..0d8fe90b2e
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/RecordParseException.java
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.domain;
+
+
+public class RecordParseException extends Exception {
+ private static final long serialVersionUID = -5145494854722254491L;
+
+ public RecordParseException(String detailMessage) {
+ super(detailMessage);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/TabsRecord.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/TabsRecord.java
new file mode 100644
index 0000000000..eb3a4f6d04
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/TabsRecord.java
@@ -0,0 +1,153 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.domain;
+
+import java.util.ArrayList;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.db.Tab;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonArrayJSONException;
+import org.mozilla.gecko.sync.Utils;
+
+import android.content.ContentValues;
+
+/**
+ * Represents a client's collection of tabs.
+ *
+ * @author rnewman
+ *
+ */
+public class TabsRecord extends Record {
+ public static final String LOG_TAG = "TabsRecord";
+
+ public static final String COLLECTION_NAME = "tabs";
+ public static final long TABS_TTL = 7 * 24 * 60 * 60; // 7 days in seconds.
+
+ public TabsRecord(String guid, String collection, long lastModified, boolean deleted) {
+ super(guid, collection, lastModified, deleted);
+ this.ttl = TABS_TTL;
+ }
+ public TabsRecord(String guid, String collection, long lastModified) {
+ this(guid, collection, lastModified, false);
+ }
+ public TabsRecord(String guid, String collection) {
+ this(guid, collection, 0, false);
+ }
+ public TabsRecord(String guid) {
+ this(guid, COLLECTION_NAME, 0, false);
+ }
+ public TabsRecord() {
+ this(Utils.generateGuid(), COLLECTION_NAME, 0, false);
+ }
+
+ public String clientName;
+ public ArrayList<Tab> tabs;
+
+ @Override
+ public void initFromPayload(ExtendedJSONObject payload) {
+ clientName = (String) payload.get("clientName");
+ try {
+ tabs = tabsFrom(payload.getArray("tabs"));
+ } catch (NonArrayJSONException e) {
+ // Oh well.
+ tabs = new ArrayList<Tab>();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ protected static JSONArray tabsToJSON(ArrayList<Tab> tabs) {
+ JSONArray out = new JSONArray();
+ for (Tab tab : tabs) {
+ out.add(tabToJSONObject(tab));
+ }
+ return out;
+ }
+
+ protected static ArrayList<Tab> tabsFrom(JSONArray in) {
+ ArrayList<Tab> tabs = new ArrayList<Tab>(in.size());
+ for (Object o : in) {
+ if (o instanceof JSONObject) {
+ try {
+ tabs.add(TabsRecord.tabFromJSONObject((JSONObject) o));
+ } catch (NonArrayJSONException e) {
+ Logger.warn(LOG_TAG, "urlHistory is not an array for this tab.", e);
+ }
+ }
+ }
+ return tabs;
+ }
+
+ @Override
+ public void populatePayload(ExtendedJSONObject payload) {
+ putPayload(payload, "id", this.guid);
+ putPayload(payload, "clientName", this.clientName);
+ payload.put("tabs", tabsToJSON(this.tabs));
+ }
+
+ @Override
+ public Record copyWithIDs(String guid, long androidID) {
+ TabsRecord out = new TabsRecord(guid, this.collection, this.lastModified, this.deleted);
+ out.androidID = androidID;
+ out.sortIndex = this.sortIndex;
+ out.ttl = this.ttl;
+
+ out.clientName = this.clientName;
+ out.tabs = new ArrayList<Tab>(this.tabs);
+
+ return out;
+ }
+
+ public ContentValues getClientsContentValues() {
+ ContentValues cv = new ContentValues();
+ cv.put(BrowserContract.Clients.GUID, this.guid);
+ cv.put(BrowserContract.Clients.NAME, this.clientName);
+ cv.put(BrowserContract.Clients.LAST_MODIFIED, this.lastModified);
+ return cv;
+ }
+
+ public ContentValues[] getTabsContentValues() {
+ int c = tabs.size();
+ ContentValues[] out = new ContentValues[c];
+ for (int i = 0; i < c; i++) {
+ out[i] = tabs.get(i).toContentValues(this.guid, i);
+ }
+ return out;
+ }
+
+ public static Tab tabFromJSONObject(JSONObject o) throws NonArrayJSONException {
+ ExtendedJSONObject obj = new ExtendedJSONObject(o);
+ String title = obj.getString("title");
+ String icon = obj.getString("icon");
+ JSONArray history = obj.getArray("urlHistory");
+
+ // Last used is inexplicably a string in seconds. Most of the time.
+ long lastUsed = 0;
+ Object lU = obj.get("lastUsed");
+ if (lU instanceof Number) {
+ lastUsed = ((Long) lU) * 1000L;
+ } else if (lU instanceof String) {
+ try {
+ lastUsed = Long.parseLong((String) lU, 10) * 1000L;
+ } catch (NumberFormatException e) {
+ Logger.debug(TabsRecord.LOG_TAG, "Invalid number format in lastUsed: " + lU);
+ }
+ }
+ return new Tab(title, icon, history, lastUsed);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static JSONObject tabToJSONObject(Tab tab) {
+ JSONObject o = new JSONObject();
+ o.put("title", tab.title);
+ o.put("icon", tab.icon);
+ o.put("urlHistory", tab.history);
+ o.put("lastUsed", tab.lastUsed / 1000);
+ return o;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/TabsRecordFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/TabsRecordFactory.java
new file mode 100644
index 0000000000..9504434d8c
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/TabsRecordFactory.java
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.domain;
+
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.repositories.RecordFactory;
+
+public class TabsRecordFactory extends RecordFactory {
+ @Override
+ public Record createRecord(Record record) {
+ TabsRecord r = new TabsRecord();
+ r.initFromEnvelope((CryptoRecord) record);
+ return r;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/VersionConstants.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/VersionConstants.java
new file mode 100644
index 0000000000..2d3d4fd32e
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/VersionConstants.java
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.domain;
+
+public class VersionConstants {
+ public static final int BOOKMARKS_ENGINE_VERSION = 2;
+ public static final int CLIENTS_ENGINE_VERSION = 1;
+ public static final int FORMS_ENGINE_VERSION = 1;
+ public static final int HISTORY_ENGINE_VERSION = 1;
+ public static final int PASSWORDS_ENGINE_VERSION = 1;
+ public static final int TABS_ENGINE_VERSION = 1;
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloader.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloader.java
new file mode 100644
index 0000000000..5c3037e4d7
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloader.java
@@ -0,0 +1,310 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.downloaders;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.DelayedWorkTracker;
+import org.mozilla.gecko.sync.net.SyncResponse;
+import org.mozilla.gecko.sync.net.SyncStorageCollectionRequest;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.sync.repositories.Server11Repository;
+import org.mozilla.gecko.sync.repositories.Server11RepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Batching Downloader, which implements batching protocol as supported by Sync 1.5.
+ *
+ * Downloader's batching behaviour is configured via two parameters, obtained from the repository:
+ * - Per-batch limit, which specified how many records may be fetched in an individual GET request.
+ * - Total limit, which controls number of batch GET requests we will make.
+ *
+ *
+ * Batching is implemented via specifying a 'limit' GET parameter, and looking for an 'offset' token
+ * in the response. If offset token is present, this indicates that there are more records than what
+ * we've received so far, and we perform an additional fetch. Batching stops when either we hit a total
+ * limit, or offset token is no longer present (indicating that we're done).
+ *
+ * For unlimited repositories (such as passwords), both of these value will be -1. Downloader will not
+ * specify a limit parameter in this case, and the response will contain every record available and no
+ * offset token, thus fully completing in one go.
+ *
+ * In between batches, we maintain a Last-Modified timestamp, based off the value return in the header
+ * of the first response. Every response will have a Last-Modified header, indicating when the collection
+ * was modified last. We pass along this header in our subsequent requests in a X-If-Unmodified-Since
+ * header. Server will ensure that our collection did not change while we are batching, if it did it will
+ * fail our fetch with a 412 (Consequent Modification) error. Additionally, we perform the same checks
+ * locally.
+ */
+public class BatchingDownloader {
+ public static final String LOG_TAG = "BatchingDownloader";
+
+ protected final Server11Repository repository;
+ private final Server11RepositorySession repositorySession;
+ private final DelayedWorkTracker workTracker = new DelayedWorkTracker();
+ // Used to track outstanding requests, so that we can abort them as needed.
+ @VisibleForTesting
+ protected final Set<SyncStorageCollectionRequest> pending = Collections.synchronizedSet(new HashSet<SyncStorageCollectionRequest>());
+ /* @GuardedBy("this") */ private String lastModified;
+ /* @GuardedBy("this") */ private long numRecords = 0;
+
+ public BatchingDownloader(final Server11Repository repository, final Server11RepositorySession repositorySession) {
+ this.repository = repository;
+ this.repositorySession = repositorySession;
+ }
+
+ @VisibleForTesting
+ protected static String flattenIDs(String[] guids) {
+ // Consider using Utils.toDelimitedString if and when the signature changes
+ // to Collection<String> guids.
+ if (guids.length == 0) {
+ return "";
+ }
+ if (guids.length == 1) {
+ return guids[0];
+ }
+ // Assuming 12-char GUIDs. There should be a -1 in there, but we accumulate one comma too many.
+ StringBuilder b = new StringBuilder(guids.length * 12 + guids.length);
+ for (String guid : guids) {
+ b.append(guid);
+ b.append(",");
+ }
+ return b.substring(0, b.length() - 1);
+ }
+
+ @VisibleForTesting
+ protected void fetchWithParameters(long newer,
+ long batchLimit,
+ boolean full,
+ String sort,
+ String ids,
+ SyncStorageCollectionRequest request,
+ RepositorySessionFetchRecordsDelegate fetchRecordsDelegate)
+ throws URISyntaxException, UnsupportedEncodingException {
+ if (batchLimit > repository.getDefaultTotalLimit()) {
+ throw new IllegalArgumentException("Batch limit should not be greater than total limit");
+ }
+
+ request.delegate = new BatchingDownloaderDelegate(this, fetchRecordsDelegate, request,
+ newer, batchLimit, full, sort, ids);
+ this.pending.add(request);
+ request.get();
+ }
+
+ @VisibleForTesting
+ @Nullable
+ protected String encodeParam(String param) throws UnsupportedEncodingException {
+ if (param != null) {
+ return URLEncoder.encode(param, "UTF-8");
+ }
+ return null;
+ }
+
+ @VisibleForTesting
+ protected SyncStorageCollectionRequest makeSyncStorageCollectionRequest(long newer,
+ long batchLimit,
+ boolean full,
+ String sort,
+ String ids,
+ String offset)
+ throws URISyntaxException, UnsupportedEncodingException {
+ URI collectionURI = repository.collectionURI(full, newer, batchLimit, sort, ids, encodeParam(offset));
+ Logger.debug(LOG_TAG, collectionURI.toString());
+
+ return new SyncStorageCollectionRequest(collectionURI);
+ }
+
+ public void fetchSince(long timestamp, RepositorySessionFetchRecordsDelegate fetchRecordsDelegate) {
+ this.fetchSince(timestamp, null, fetchRecordsDelegate);
+ }
+
+ private void fetchSince(long timestamp, String offset,
+ RepositorySessionFetchRecordsDelegate fetchRecordsDelegate) {
+ long batchLimit = repository.getDefaultBatchLimit();
+ String sort = repository.getDefaultSort();
+
+ try {
+ SyncStorageCollectionRequest request = makeSyncStorageCollectionRequest(timestamp,
+ batchLimit, true, sort, null, offset);
+ this.fetchWithParameters(timestamp, batchLimit, true, sort, null, request, fetchRecordsDelegate);
+ } catch (URISyntaxException | UnsupportedEncodingException e) {
+ fetchRecordsDelegate.onFetchFailed(e, null);
+ }
+ }
+
+ public void fetch(String[] guids, RepositorySessionFetchRecordsDelegate fetchRecordsDelegate) {
+ String ids = flattenIDs(guids);
+ String index = "index";
+
+ try {
+ SyncStorageCollectionRequest request = makeSyncStorageCollectionRequest(
+ -1, -1, true, index, ids, null);
+ this.fetchWithParameters(-1, -1, true, index, ids, request, fetchRecordsDelegate);
+ } catch (URISyntaxException | UnsupportedEncodingException e) {
+ fetchRecordsDelegate.onFetchFailed(e, null);
+ }
+ }
+
+ public Server11Repository getServerRepository() {
+ return this.repository;
+ }
+
+ public void onFetchCompleted(SyncStorageResponse response,
+ final RepositorySessionFetchRecordsDelegate fetchRecordsDelegate,
+ final SyncStorageCollectionRequest request, long newer,
+ long limit, boolean full, String sort, String ids) {
+ removeRequestFromPending(request);
+
+ // When we process our first request, we get back a X-Last-Modified header indicating when collection was modified last.
+ // We pass it to the server with every subsequent request (if we need to make more) as the X-If-Unmodified-Since header,
+ // and server is supposed to ensure that this pre-condition is met, and fail our request with a 412 error code otherwise.
+ // So, if all of this happens, these checks should never fail.
+ // However, we also track this header in client side, and can defensively validate against it here as well.
+ final String currentLastModifiedTimestamp = response.lastModified();
+ Logger.debug(LOG_TAG, "Last modified timestamp " + currentLastModifiedTimestamp);
+
+ // Sanity check. We also did a null check in delegate before passing it into here.
+ if (currentLastModifiedTimestamp == null) {
+ this.abort(fetchRecordsDelegate, "Last modified timestamp is missing");
+ return;
+ }
+
+ final boolean lastModifiedChanged;
+ synchronized (this) {
+ if (this.lastModified == null) {
+ // First time seeing last modified timestamp.
+ this.lastModified = currentLastModifiedTimestamp;
+ }
+ lastModifiedChanged = !this.lastModified.equals(currentLastModifiedTimestamp);
+ }
+
+ if (lastModifiedChanged) {
+ this.abort(fetchRecordsDelegate, "Last modified timestamp has changed unexpectedly");
+ return;
+ }
+
+ final boolean hasNotReachedLimit;
+ synchronized (this) {
+ this.numRecords += response.weaveRecords();
+ hasNotReachedLimit = this.numRecords < repository.getDefaultTotalLimit();
+ }
+
+ final String offset = response.weaveOffset();
+ final SyncStorageCollectionRequest newRequest;
+ try {
+ newRequest = makeSyncStorageCollectionRequest(newer,
+ limit, full, sort, ids, offset);
+ } catch (final URISyntaxException | UnsupportedEncodingException e) {
+ this.workTracker.delayWorkItem(new Runnable() {
+ @Override
+ public void run() {
+ Logger.debug(LOG_TAG, "Delayed onFetchCompleted running.");
+ fetchRecordsDelegate.onFetchFailed(e, null);
+ }
+ });
+ return;
+ }
+
+ if (offset != null && hasNotReachedLimit) {
+ try {
+ this.fetchWithParameters(newer, limit, full, sort, ids, newRequest, fetchRecordsDelegate);
+ } catch (final URISyntaxException | UnsupportedEncodingException e) {
+ this.workTracker.delayWorkItem(new Runnable() {
+ @Override
+ public void run() {
+ Logger.debug(LOG_TAG, "Delayed onFetchCompleted running.");
+ fetchRecordsDelegate.onFetchFailed(e, null);
+ }
+ });
+ }
+ return;
+ }
+
+ final long normalizedTimestamp = response.normalizedTimestampForHeader(SyncResponse.X_LAST_MODIFIED);
+ Logger.debug(LOG_TAG, "Fetch completed. Timestamp is " + normalizedTimestamp);
+
+ this.workTracker.delayWorkItem(new Runnable() {
+ @Override
+ public void run() {
+ Logger.debug(LOG_TAG, "Delayed onFetchCompleted running.");
+ fetchRecordsDelegate.onFetchCompleted(normalizedTimestamp);
+ }
+ });
+ }
+
+ public void onFetchFailed(final Exception ex,
+ final RepositorySessionFetchRecordsDelegate fetchRecordsDelegate,
+ final SyncStorageCollectionRequest request) {
+ removeRequestFromPending(request);
+ this.workTracker.delayWorkItem(new Runnable() {
+ @Override
+ public void run() {
+ Logger.debug(LOG_TAG, "Running onFetchFailed.");
+ fetchRecordsDelegate.onFetchFailed(ex, null);
+ }
+ });
+ }
+
+ public void onFetchedRecord(CryptoRecord record,
+ RepositorySessionFetchRecordsDelegate fetchRecordsDelegate) {
+ this.workTracker.incrementOutstanding();
+ try {
+ fetchRecordsDelegate.onFetchedRecord(record);
+ } catch (Exception ex) {
+ Logger.warn(LOG_TAG, "Got exception calling onFetchedRecord with WBO.", ex);
+ throw new RuntimeException(ex);
+ } finally {
+ this.workTracker.decrementOutstanding();
+ }
+ }
+
+ private void removeRequestFromPending(SyncStorageCollectionRequest request) {
+ if (request == null) {
+ return;
+ }
+ this.pending.remove(request);
+ }
+
+ @VisibleForTesting
+ protected void abortRequests() {
+ this.repositorySession.abort();
+ synchronized (this.pending) {
+ for (SyncStorageCollectionRequest request : this.pending) {
+ request.abort();
+ }
+ this.pending.clear();
+ }
+ }
+
+ @Nullable
+ protected synchronized String getLastModified() {
+ return this.lastModified;
+ }
+
+ private void abort(final RepositorySessionFetchRecordsDelegate delegate, final String msg) {
+ Logger.error(LOG_TAG, msg);
+ this.abortRequests();
+ this.workTracker.delayWorkItem(new Runnable() {
+ @Override
+ public void run() {
+ Logger.debug(LOG_TAG, "Delayed onFetchCompleted running.");
+ delegate.onFetchFailed(
+ new IllegalStateException(msg),
+ null);
+ }
+ });
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderDelegate.java
new file mode 100644
index 0000000000..eb9f76d6b4
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderDelegate.java
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.downloaders;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.HTTPFailureException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.SyncStorageCollectionRequest;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.sync.net.WBOCollectionRequestDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
+
+/**
+ * Delegate that gets passed into fetch methods to handle server response from fetch.
+ */
+public class BatchingDownloaderDelegate extends WBOCollectionRequestDelegate {
+ public static final String LOG_TAG = "BatchingDownloaderDelegate";
+
+ private BatchingDownloader downloader;
+ private RepositorySessionFetchRecordsDelegate fetchRecordsDelegate;
+ public SyncStorageCollectionRequest request;
+ // Used to pass back to BatchDownloader to start another fetch with these parameters if needed.
+ private long newer;
+ private long batchLimit;
+ private boolean full;
+ private String sort;
+ private String ids;
+
+ public BatchingDownloaderDelegate(final BatchingDownloader downloader,
+ final RepositorySessionFetchRecordsDelegate fetchRecordsDelegate,
+ final SyncStorageCollectionRequest request, long newer,
+ long batchLimit, boolean full, String sort, String ids) {
+ this.downloader = downloader;
+ this.fetchRecordsDelegate = fetchRecordsDelegate;
+ this.request = request;
+ this.newer = newer;
+ this.batchLimit = batchLimit;
+ this.full = full;
+ this.sort = sort;
+ this.ids = ids;
+ }
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return this.downloader.getServerRepository().getAuthHeaderProvider();
+ }
+
+ @Override
+ public String ifUnmodifiedSince() {
+ return this.downloader.getLastModified();
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse response) {
+ Logger.debug(LOG_TAG, "Fetch done.");
+ if (response.lastModified() != null) {
+ this.downloader.onFetchCompleted(response, this.fetchRecordsDelegate, this.request,
+ this.newer, this.batchLimit, this.full, this.sort, this.ids);
+ return;
+ }
+ this.downloader.onFetchFailed(
+ new IllegalStateException("Missing last modified header from response"),
+ this.fetchRecordsDelegate,
+ this.request);
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ this.handleRequestError(new HTTPFailureException(response));
+ }
+
+ @Override
+ public void handleRequestError(final Exception ex) {
+ Logger.warn(LOG_TAG, "Got request error.", ex);
+ this.downloader.onFetchFailed(ex, this.fetchRecordsDelegate, this.request);
+ }
+
+ @Override
+ public void handleWBO(CryptoRecord record) {
+ this.downloader.onFetchedRecord(record, this.fetchRecordsDelegate);
+ }
+
+ @Override
+ public KeyBundle keyBundle() {
+ return null;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchMeta.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchMeta.java
new file mode 100644
index 0000000000..9515885862
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchMeta.java
@@ -0,0 +1,165 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.uploaders;
+
+import android.support.annotation.CheckResult;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import org.mozilla.gecko.background.common.log.Logger;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.mozilla.gecko.sync.repositories.uploaders.BatchingUploader.TokenModifiedException;
+import org.mozilla.gecko.sync.repositories.uploaders.BatchingUploader.LastModifiedChangedUnexpectedly;
+import org.mozilla.gecko.sync.repositories.uploaders.BatchingUploader.LastModifiedDidNotChange;
+
+/**
+ * Keeps track of token, Last-Modified value and GUIDs of succeeded records.
+ */
+/* @ThreadSafe */
+public class BatchMeta extends BufferSizeTracker {
+ private static final String LOG_TAG = "BatchMeta";
+
+ // Will be set once first payload upload succeeds. We don't expect this to change until we
+ // commit the batch, and which point it must change.
+ /* @GuardedBy("this") */ private Long lastModified;
+
+ // Will be set once first payload upload succeeds. We don't expect this to ever change until
+ // a commit succeeds, at which point this gets set to null.
+ /* @GuardedBy("this") */ private String token;
+
+ /* @GuardedBy("accessLock") */ private boolean isUnlimited = false;
+
+ // Accessed by synchronously running threads.
+ /* @GuardedBy("accessLock") */ private final List<String> successRecordGuids = new ArrayList<>();
+
+ /* @GuardedBy("accessLock") */ private boolean needsCommit = false;
+
+ protected final Long collectionLastModified;
+
+ public BatchMeta(@NonNull Object payloadLock, long maxBytes, long maxRecords, @Nullable Long collectionLastModified) {
+ super(payloadLock, maxBytes, maxRecords);
+ this.collectionLastModified = collectionLastModified;
+ }
+
+ protected void setIsUnlimited(boolean isUnlimited) {
+ synchronized (accessLock) {
+ this.isUnlimited = isUnlimited;
+ }
+ }
+
+ @Override
+ protected boolean canFit(long recordDeltaByteCount) {
+ synchronized (accessLock) {
+ return isUnlimited || super.canFit(recordDeltaByteCount);
+ }
+ }
+
+ @Override
+ @CheckResult
+ protected boolean addAndEstimateIfFull(long recordDeltaByteCount) {
+ synchronized (accessLock) {
+ needsCommit = true;
+ boolean isFull = super.addAndEstimateIfFull(recordDeltaByteCount);
+ return !isUnlimited && isFull;
+ }
+ }
+
+ protected boolean needToCommit() {
+ synchronized (accessLock) {
+ return needsCommit;
+ }
+ }
+
+ protected synchronized String getToken() {
+ return token;
+ }
+
+ protected synchronized void setToken(final String newToken, boolean isCommit) throws TokenModifiedException {
+ // Set token once in a batching mode.
+ // In a non-batching mode, this.token and newToken will be null, and this is a no-op.
+ if (token == null) {
+ token = newToken;
+ return;
+ }
+
+ // Sanity checks.
+ if (isCommit) {
+ // We expect token to be null when commit payload succeeds.
+ if (newToken != null) {
+ throw new TokenModifiedException();
+ } else {
+ token = null;
+ }
+ return;
+ }
+
+ // We expect new token to always equal current token for non-commit payloads.
+ if (!token.equals(newToken)) {
+ throw new TokenModifiedException();
+ }
+ }
+
+ protected synchronized Long getLastModified() {
+ if (lastModified == null) {
+ return collectionLastModified;
+ }
+ return lastModified;
+ }
+
+ protected synchronized void setLastModified(final Long newLastModified, final boolean expectedToChange) throws LastModifiedChangedUnexpectedly, LastModifiedDidNotChange {
+ if (lastModified == null) {
+ lastModified = newLastModified;
+ return;
+ }
+
+ if (!expectedToChange && !lastModified.equals(newLastModified)) {
+ Logger.debug(LOG_TAG, "Last-Modified timestamp changed when we didn't expect it");
+ throw new LastModifiedChangedUnexpectedly();
+
+ } else if (expectedToChange && lastModified.equals(newLastModified)) {
+ Logger.debug(LOG_TAG, "Last-Modified timestamp did not change when we expected it to");
+ throw new LastModifiedDidNotChange();
+
+ } else {
+ lastModified = newLastModified;
+ }
+ }
+
+ protected ArrayList<String> getSuccessRecordGuids() {
+ synchronized (accessLock) {
+ return new ArrayList<>(this.successRecordGuids);
+ }
+ }
+
+ protected void recordSucceeded(final String recordGuid) {
+ // Sanity check.
+ if (recordGuid == null) {
+ throw new IllegalStateException();
+ }
+
+ synchronized (accessLock) {
+ successRecordGuids.add(recordGuid);
+ }
+ }
+
+ @Override
+ protected boolean canFitRecordByteDelta(long byteDelta, long recordCount, long byteCount) {
+ return isUnlimited || super.canFitRecordByteDelta(byteDelta, recordCount, byteCount);
+ }
+
+ @Override
+ protected void reset() {
+ synchronized (accessLock) {
+ super.reset();
+ token = null;
+ lastModified = null;
+ successRecordGuids.clear();
+ needsCommit = false;
+ }
+ }
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploader.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploader.java
new file mode 100644
index 0000000000..26efbd1368
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploader.java
@@ -0,0 +1,344 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.uploaders;
+
+import android.net.Uri;
+import android.support.annotation.VisibleForTesting;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.InfoConfiguration;
+import org.mozilla.gecko.sync.Server11RecordPostFailedException;
+import org.mozilla.gecko.sync.net.SyncResponse;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.sync.repositories.Server11RepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import java.util.ArrayList;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Uploader which implements batching introduced in Sync 1.5.
+ *
+ * Batch vs payload terminology:
+ * - batch is comprised of a series of payloads, which are all committed at the same time.
+ * -- identified via a "batch token", which is returned after first payload for the batch has been uploaded.
+ * - payload is a collection of records which are uploaded together. Associated with a batch.
+ * -- last payload, identified via commit=true, commits the batch.
+ *
+ * Limits for how many records can fit into a payload and into a batch are defined in the passed-in
+ * InfoConfiguration object.
+ *
+ * If we can't fit everything we'd like to upload into one batch (according to max-total-* limits),
+ * then we commit that batch, and start a new one. There are no explicit limits on total number of
+ * batches we might use, although at some point we'll start to run into storage limit errors from the API.
+ *
+ * Once we go past using one batch this uploader is no longer "atomic". Partial state is exposed
+ * to other clients after our first batch is committed and before our last batch is committed.
+ * However, our per-batch limits are high, X-I-U-S mechanics help protect downloading clients
+ * (as long as they implement X-I-U-S) with 412 error codes in case of interleaving upload and download,
+ * and most mobile clients will not be uploading large-enough amounts of data (especially structured
+ * data, such as bookmarks).
+ *
+ * Last-Modified header returned with the first batch payload POST success is maintained for a batch,
+ * to guard against concurrent-modification errors (different uploader commits before we're done).
+ *
+ * Non-batching mode notes:
+ * We also support Sync servers which don't enable batching for uploads. In this case, we respect
+ * payload limits for individual uploads, and every upload is considered a commit. Batching limits
+ * do not apply, and batch token is irrelevant.
+ * We do keep track of Last-Modified and send along X-I-U-S with our uploads, to protect against
+ * concurrent modifications by other clients.
+ */
+public class BatchingUploader {
+ private static final String LOG_TAG = "BatchingUploader";
+
+ private final Uri collectionUri;
+
+ private volatile boolean recordUploadFailed = false;
+
+ private final BatchMeta batchMeta;
+ private final Payload payload;
+
+ // Accessed by synchronously running threads, OK to not synchronize and just make it volatile.
+ private volatile Boolean inBatchingMode;
+
+ // Used to ensure we have thread-safe access to the following:
+ // - byte and record counts in both Payload and BatchMeta objects
+ // - buffers in the Payload object
+ private final Object payloadLock = new Object();
+
+ protected Executor workQueue;
+ protected final RepositorySessionStoreDelegate sessionStoreDelegate;
+ protected final Server11RepositorySession repositorySession;
+
+ protected AtomicLong uploadTimestamp = new AtomicLong(0);
+
+ protected static final int PER_RECORD_OVERHEAD_BYTE_COUNT = RecordUploadRunnable.RECORD_SEPARATOR.length;
+ protected static final int PER_PAYLOAD_OVERHEAD_BYTE_COUNT = RecordUploadRunnable.RECORDS_END.length;
+
+ // Sanity check. RECORD_SEPARATOR and RECORD_START are assumed to be of the same length.
+ static {
+ if (RecordUploadRunnable.RECORD_SEPARATOR.length != RecordUploadRunnable.RECORDS_START.length) {
+ throw new IllegalStateException("Separator and start tokens must be of the same length");
+ }
+ }
+
+ public BatchingUploader(final Server11RepositorySession repositorySession, final Executor workQueue, final RepositorySessionStoreDelegate sessionStoreDelegate) {
+ this.repositorySession = repositorySession;
+ this.workQueue = workQueue;
+ this.sessionStoreDelegate = sessionStoreDelegate;
+ this.collectionUri = Uri.parse(repositorySession.getServerRepository().collectionURI().toString());
+
+ InfoConfiguration config = repositorySession.getServerRepository().getInfoConfiguration();
+ this.batchMeta = new BatchMeta(
+ payloadLock, config.maxTotalBytes, config.maxTotalRecords,
+ repositorySession.getServerRepository().getCollectionLastModified()
+ );
+ this.payload = new Payload(payloadLock, config.maxPostBytes, config.maxPostRecords);
+ }
+
+ public void process(final Record record) {
+ final String guid = record.guid;
+ final byte[] recordBytes = record.toJSONBytes();
+ final long recordDeltaByteCount = recordBytes.length + PER_RECORD_OVERHEAD_BYTE_COUNT;
+
+ Logger.debug(LOG_TAG, "Processing a record with guid: " + guid);
+
+ // We can't upload individual records which exceed our payload byte limit.
+ if ((recordDeltaByteCount + PER_PAYLOAD_OVERHEAD_BYTE_COUNT) > payload.maxBytes) {
+ sessionStoreDelegate.onRecordStoreFailed(new RecordTooLargeToUpload(), guid);
+ return;
+ }
+
+ synchronized (payloadLock) {
+ final boolean canFitRecordIntoBatch = batchMeta.canFit(recordDeltaByteCount);
+ final boolean canFitRecordIntoPayload = payload.canFit(recordDeltaByteCount);
+
+ // Record fits!
+ if (canFitRecordIntoBatch && canFitRecordIntoPayload) {
+ Logger.debug(LOG_TAG, "Record fits into the current batch and payload");
+ addAndFlushIfNecessary(recordDeltaByteCount, recordBytes, guid);
+
+ // Payload won't fit the record.
+ } else if (canFitRecordIntoBatch) {
+ Logger.debug(LOG_TAG, "Current payload won't fit incoming record, uploading payload.");
+ flush(false, false);
+
+ Logger.debug(LOG_TAG, "Recording the incoming record into a new payload");
+
+ // Keep track of the overflow record.
+ addAndFlushIfNecessary(recordDeltaByteCount, recordBytes, guid);
+
+ // Batch won't fit the record.
+ } else {
+ Logger.debug(LOG_TAG, "Current batch won't fit incoming record, committing batch.");
+ flush(true, false);
+
+ Logger.debug(LOG_TAG, "Recording the incoming record into a new batch");
+ batchMeta.reset();
+
+ // Keep track of the overflow record.
+ addAndFlushIfNecessary(recordDeltaByteCount, recordBytes, guid);
+ }
+ }
+ }
+
+ // Convenience function used from the process method; caller must hold a payloadLock.
+ private void addAndFlushIfNecessary(long byteCount, byte[] recordBytes, String guid) {
+ boolean isPayloadFull = payload.addAndEstimateIfFull(byteCount, recordBytes, guid);
+ boolean isBatchFull = batchMeta.addAndEstimateIfFull(byteCount);
+
+ // Preemptive commit batch or upload a payload if they're estimated to be full.
+ if (isBatchFull) {
+ flush(true, false);
+ batchMeta.reset();
+ } else if (isPayloadFull) {
+ flush(false, false);
+ }
+ }
+
+ public void noMoreRecordsToUpload() {
+ Logger.debug(LOG_TAG, "Received 'no more records to upload' signal.");
+
+ // Run this after the last payload succeeds, so that we know for sure if we're in a batching
+ // mode and need to commit with a potentially empty payload.
+ workQueue.execute(new Runnable() {
+ @Override
+ public void run() {
+ commitIfNecessaryAfterLastPayload();
+ }
+ });
+ }
+
+ @VisibleForTesting
+ protected void commitIfNecessaryAfterLastPayload() {
+ // Must be called after last payload upload finishes.
+ synchronized (payload) {
+ // If we have any pending records in the Payload, flush them!
+ if (!payload.isEmpty()) {
+ flush(true, true);
+
+ // If we have an empty payload but need to commit the batch in the batching mode, flush!
+ } else if (batchMeta.needToCommit() && Boolean.TRUE.equals(inBatchingMode)) {
+ flush(true, true);
+
+ // Otherwise, we're done.
+ } else {
+ finished(uploadTimestamp);
+ }
+ }
+ }
+
+ /**
+ * We've been told by our upload delegate that a payload succeeded.
+ * Depending on the type of payload and batch mode status, inform our delegate of progress.
+ *
+ * @param response success response to our commit post
+ * @param isCommit was this a commit upload?
+ * @param isLastPayload was this a very last payload we'll upload?
+ */
+ public void payloadSucceeded(final SyncStorageResponse response, final boolean isCommit, final boolean isLastPayload) {
+ // Sanity check.
+ if (inBatchingMode == null) {
+ throw new IllegalStateException("Can't process payload success until we know if we're in a batching mode");
+ }
+
+ // We consider records to have been committed if we're not in a batching mode or this was a commit.
+ // If records have been committed, notify our store delegate.
+ if (!inBatchingMode || isCommit) {
+ for (String guid : batchMeta.getSuccessRecordGuids()) {
+ sessionStoreDelegate.onRecordStoreSucceeded(guid);
+ }
+ }
+
+ // If this was our very last commit, we're done storing records.
+ // Get Last-Modified timestamp from the response, and pass it upstream.
+ if (isLastPayload) {
+ finished(response.normalizedTimestampForHeader(SyncResponse.X_LAST_MODIFIED));
+ }
+ }
+
+ public void lastPayloadFailed() {
+ finished(uploadTimestamp);
+ }
+
+ private void finished(long lastModifiedTimestamp) {
+ bumpTimestampTo(uploadTimestamp, lastModifiedTimestamp);
+ finished(uploadTimestamp);
+ }
+
+ private void finished(AtomicLong lastModifiedTimestamp) {
+ repositorySession.storeDone(lastModifiedTimestamp.get());
+ }
+
+ public BatchMeta getCurrentBatch() {
+ return batchMeta;
+ }
+
+ public void setInBatchingMode(boolean inBatchingMode) {
+ this.inBatchingMode = inBatchingMode;
+
+ // If we know for sure that we're not in a batching mode,
+ // consider our batch to be of unlimited size.
+ this.batchMeta.setIsUnlimited(!inBatchingMode);
+ }
+
+ public Boolean getInBatchingMode() {
+ return inBatchingMode;
+ }
+
+ public void setLastModified(final Long lastModified, final boolean isCommit) throws BatchingUploaderException {
+ // Sanity check.
+ if (inBatchingMode == null) {
+ throw new IllegalStateException("Can't process Last-Modified before we know we're in a batching mode.");
+ }
+
+ // In non-batching mode, every time we receive a Last-Modified timestamp, we expect it to change
+ // since records are "committed" (become visible to other clients) on every payload.
+ // In batching mode, we only expect Last-Modified to change when we commit a batch.
+ batchMeta.setLastModified(lastModified, isCommit || !inBatchingMode);
+ }
+
+ public void recordSucceeded(final String recordGuid) {
+ Logger.debug(LOG_TAG, "Record store succeeded: " + recordGuid);
+ batchMeta.recordSucceeded(recordGuid);
+ }
+
+ public void recordFailed(final String recordGuid) {
+ recordFailed(new Server11RecordPostFailedException(), recordGuid);
+ }
+
+ public void recordFailed(final Exception e, final String recordGuid) {
+ Logger.debug(LOG_TAG, "Record store failed for guid " + recordGuid + " with exception: " + e.toString());
+ recordUploadFailed = true;
+ sessionStoreDelegate.onRecordStoreFailed(e, recordGuid);
+ }
+
+ public Server11RepositorySession getRepositorySession() {
+ return repositorySession;
+ }
+
+ private static void bumpTimestampTo(final AtomicLong current, long newValue) {
+ while (true) {
+ long existing = current.get();
+ if (existing > newValue) {
+ return;
+ }
+ if (current.compareAndSet(existing, newValue)) {
+ return;
+ }
+ }
+ }
+
+ private void flush(final boolean isCommit, final boolean isLastPayload) {
+ final ArrayList<byte[]> outgoing;
+ final ArrayList<String> outgoingGuids;
+ final long byteCount;
+
+ // Even though payload object itself is thread-safe, we want to ensure we get these altogether
+ // as a "unit". Another approach would be to create a wrapper object for these values, but this works.
+ synchronized (payloadLock) {
+ outgoing = payload.getRecordsBuffer();
+ outgoingGuids = payload.getRecordGuidsBuffer();
+ byteCount = payload.getByteCount();
+ }
+
+ workQueue.execute(new RecordUploadRunnable(
+ new BatchingAtomicUploaderMayUploadProvider(),
+ collectionUri,
+ batchMeta,
+ new PayloadUploadDelegate(this, outgoingGuids, isCommit, isLastPayload),
+ outgoing,
+ byteCount,
+ isCommit
+ ));
+
+ payload.reset();
+ }
+
+ private class BatchingAtomicUploaderMayUploadProvider implements MayUploadProvider {
+ public boolean mayUpload() {
+ return !recordUploadFailed;
+ }
+ }
+
+ public static class BatchingUploaderException extends Exception {
+ private static final long serialVersionUID = 1L;
+ }
+ public static class RecordTooLargeToUpload extends BatchingUploaderException {
+ private static final long serialVersionUID = 1L;
+ }
+ public static class LastModifiedDidNotChange extends BatchingUploaderException {
+ private static final long serialVersionUID = 1L;
+ }
+ public static class LastModifiedChangedUnexpectedly extends BatchingUploaderException {
+ private static final long serialVersionUID = 1L;
+ }
+ public static class TokenModifiedException extends BatchingUploaderException {
+ private static final long serialVersionUID = 1L;
+ };
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BufferSizeTracker.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BufferSizeTracker.java
new file mode 100644
index 0000000000..7f4c305f3a
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BufferSizeTracker.java
@@ -0,0 +1,103 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.uploaders;
+
+import android.support.annotation.CallSuper;
+import android.support.annotation.CheckResult;
+
+/**
+ * Implements functionality shared by BatchMeta and Payload objects, namely:
+ * - keeping track of byte and record counts
+ * - incrementing those counts when records are added
+ * - checking if a record can fit
+ */
+/* @ThreadSafe */
+public abstract class BufferSizeTracker {
+ protected final Object accessLock;
+
+ /* @GuardedBy("accessLock") */ private long byteCount = BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT;
+ /* @GuardedBy("accessLock") */ private long recordCount = 0;
+ /* @GuardedBy("accessLock") */ protected Long smallestRecordByteCount;
+
+ protected final long maxBytes;
+ protected final long maxRecords;
+
+ public BufferSizeTracker(Object accessLock, long maxBytes, long maxRecords) {
+ this.accessLock = accessLock;
+ this.maxBytes = maxBytes;
+ this.maxRecords = maxRecords;
+ }
+
+ @CallSuper
+ protected boolean canFit(long recordDeltaByteCount) {
+ synchronized (accessLock) {
+ return canFitRecordByteDelta(recordDeltaByteCount, recordCount, byteCount);
+ }
+ }
+
+ protected boolean isEmpty() {
+ synchronized (accessLock) {
+ return recordCount == 0;
+ }
+ }
+
+ /**
+ * Adds a record and returns a boolean indicating whether batch is estimated to be full afterwards.
+ */
+ @CheckResult
+ protected boolean addAndEstimateIfFull(long recordDeltaByteCount) {
+ synchronized (accessLock) {
+ // Sanity check. Calling this method when buffer won't fit the record is an error.
+ if (!canFitRecordByteDelta(recordDeltaByteCount, recordCount, byteCount)) {
+ throw new IllegalStateException("Buffer size exceeded");
+ }
+
+ byteCount += recordDeltaByteCount;
+ recordCount += 1;
+
+ if (smallestRecordByteCount == null || smallestRecordByteCount > recordDeltaByteCount) {
+ smallestRecordByteCount = recordDeltaByteCount;
+ }
+
+ // See if we're full or nearly full after adding a record.
+ // We're halving smallestRecordByteCount because we're erring
+ // on the side of "can hopefully fit". We're trying to upload as soon as we know we
+ // should, but we also need to be mindful of minimizing total number of uploads we make.
+ return !canFitRecordByteDelta(smallestRecordByteCount / 2, recordCount, byteCount);
+ }
+ }
+
+ protected long getByteCount() {
+ synchronized (accessLock) {
+ // Ensure we account for payload overhead twice when the batch is empty.
+ // Payload overhead is either RECORDS_START ("[") or RECORDS_END ("]"),
+ // and for an empty payload we need account for both ("[]").
+ if (recordCount == 0) {
+ return byteCount + BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT;
+ }
+ return byteCount;
+ }
+ }
+
+ protected long getRecordCount() {
+ synchronized (accessLock) {
+ return recordCount;
+ }
+ }
+
+ @CallSuper
+ protected void reset() {
+ synchronized (accessLock) {
+ byteCount = BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT;
+ recordCount = 0;
+ }
+ }
+
+ @CallSuper
+ protected boolean canFitRecordByteDelta(long byteDelta, long recordCount, long byteCount) {
+ return recordCount < maxRecords
+ && (byteCount + byteDelta) <= maxBytes;
+ }
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/MayUploadProvider.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/MayUploadProvider.java
new file mode 100644
index 0000000000..a1994cf62c
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/MayUploadProvider.java
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.uploaders;
+
+public interface MayUploadProvider {
+ boolean mayUpload();
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/Payload.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/Payload.java
new file mode 100644
index 0000000000..1ed9b57985
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/Payload.java
@@ -0,0 +1,66 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.uploaders;
+
+import android.support.annotation.CheckResult;
+
+import java.util.ArrayList;
+
+/**
+ * Owns per-payload record byte and recordGuid buffers.
+ */
+/* @ThreadSafe */
+public class Payload extends BufferSizeTracker {
+ // Data of outbound records.
+ /* @GuardedBy("accessLock") */ private final ArrayList<byte[]> recordsBuffer = new ArrayList<>();
+
+ // GUIDs of outbound records. Used to fail entire payloads.
+ /* @GuardedBy("accessLock") */ private final ArrayList<String> recordGuidsBuffer = new ArrayList<>();
+
+ public Payload(Object payloadLock, long maxBytes, long maxRecords) {
+ super(payloadLock, maxBytes, maxRecords);
+ }
+
+ @Override
+ protected boolean addAndEstimateIfFull(long recordDelta) {
+ throw new UnsupportedOperationException();
+ }
+
+ @CheckResult
+ protected boolean addAndEstimateIfFull(long recordDelta, byte[] recordBytes, String guid) {
+ synchronized (accessLock) {
+ recordsBuffer.add(recordBytes);
+ recordGuidsBuffer.add(guid);
+ return super.addAndEstimateIfFull(recordDelta);
+ }
+ }
+
+ @Override
+ protected void reset() {
+ synchronized (accessLock) {
+ super.reset();
+ recordsBuffer.clear();
+ recordGuidsBuffer.clear();
+ }
+ }
+
+ protected ArrayList<byte[]> getRecordsBuffer() {
+ synchronized (accessLock) {
+ return new ArrayList<>(recordsBuffer);
+ }
+ }
+
+ protected ArrayList<String> getRecordGuidsBuffer() {
+ synchronized (accessLock) {
+ return new ArrayList<>(recordGuidsBuffer);
+ }
+ }
+
+ protected boolean isEmpty() {
+ synchronized (accessLock) {
+ return recordsBuffer.isEmpty();
+ }
+ }
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegate.java
new file mode 100644
index 0000000000..e8bbb7df65
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegate.java
@@ -0,0 +1,185 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.uploaders;
+
+import org.json.simple.JSONArray;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.HTTPFailureException;
+import org.mozilla.gecko.sync.NonArrayJSONException;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.SyncResponse;
+import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+import java.util.ArrayList;
+
+public class PayloadUploadDelegate implements SyncStorageRequestDelegate {
+ private static final String LOG_TAG = "PayloadUploadDelegate";
+
+ private static final String KEY_BATCH = "batch";
+
+ private final BatchingUploader uploader;
+ private ArrayList<String> postedRecordGuids;
+ private final boolean isCommit;
+ private final boolean isLastPayload;
+
+ public PayloadUploadDelegate(BatchingUploader uploader, ArrayList<String> postedRecordGuids, boolean isCommit, boolean isLastPayload) {
+ this.uploader = uploader;
+ this.postedRecordGuids = postedRecordGuids;
+ this.isCommit = isCommit;
+ this.isLastPayload = isLastPayload;
+ }
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return uploader.getRepositorySession().getServerRepository().getAuthHeaderProvider();
+ }
+
+ @Override
+ public String ifUnmodifiedSince() {
+ final Long lastModified = uploader.getCurrentBatch().getLastModified();
+ if (lastModified == null) {
+ return null;
+ }
+ return Utils.millisecondsToDecimalSecondsString(lastModified);
+ }
+
+ @Override
+ public void handleRequestSuccess(final SyncStorageResponse response) {
+ // First, do some sanity checking.
+ if (response.getStatusCode() != 200 && response.getStatusCode() != 202) {
+ handleRequestError(
+ new IllegalStateException("handleRequestSuccess received a non-200/202 response: " + response.getStatusCode())
+ );
+ return;
+ }
+
+ // We always expect to see a Last-Modified header. It's returned with every success response.
+ if (!response.httpResponse().containsHeader(SyncResponse.X_LAST_MODIFIED)) {
+ handleRequestError(
+ new IllegalStateException("Response did not have a Last-Modified header")
+ );
+ return;
+ }
+
+ // We expect to be able to parse the response as a JSON object.
+ final ExtendedJSONObject body;
+ try {
+ body = response.jsonObjectBody(); // jsonObjectBody() throws or returns non-null.
+ } catch (Exception e) {
+ Logger.error(LOG_TAG, "Got exception parsing POST success body.", e);
+ this.handleRequestError(e);
+ return;
+ }
+
+ // If we got a 200, it could be either a non-batching result, or a batch commit.
+ // - if we're in a batching mode, we expect this to be a commit.
+ // If we got a 202, we expect there to be a token present in the response
+ if (response.getStatusCode() == 200 && uploader.getCurrentBatch().getToken() != null) {
+ if (uploader.getInBatchingMode() && !isCommit) {
+ handleRequestError(
+ new IllegalStateException("Got 200 OK in batching mode, but this was not a commit payload")
+ );
+ return;
+ }
+ } else if (response.getStatusCode() == 202) {
+ if (!body.containsKey(KEY_BATCH)) {
+ handleRequestError(
+ new IllegalStateException("Batch response did not have a batch ID")
+ );
+ return;
+ }
+ }
+
+ // With sanity checks out of the way, can now safely say if we're in a batching mode or not.
+ // We only do this once per session.
+ if (uploader.getInBatchingMode() == null) {
+ uploader.setInBatchingMode(body.containsKey(KEY_BATCH));
+ }
+
+ // Tell current batch about the token we've received.
+ // Throws if token changed after being set once, or if we got a non-null token after a commit.
+ try {
+ uploader.getCurrentBatch().setToken(body.getString(KEY_BATCH), isCommit);
+ } catch (BatchingUploader.BatchingUploaderException e) {
+ handleRequestError(e);
+ return;
+ }
+
+ // Will throw if Last-Modified changed when it shouldn't have.
+ try {
+ uploader.setLastModified(
+ response.normalizedTimestampForHeader(SyncResponse.X_LAST_MODIFIED),
+ isCommit);
+ } catch (BatchingUploader.BatchingUploaderException e) {
+ handleRequestError(e);
+ return;
+ }
+
+ // All looks good up to this point, let's process success and failed arrays.
+ JSONArray success;
+ try {
+ success = body.getArray("success");
+ } catch (NonArrayJSONException e) {
+ handleRequestError(e);
+ return;
+ }
+
+ if (success != null && !success.isEmpty()) {
+ Logger.trace(LOG_TAG, "Successful records: " + success.toString());
+ for (Object o : success) {
+ try {
+ uploader.recordSucceeded((String) o);
+ } catch (ClassCastException e) {
+ Logger.error(LOG_TAG, "Got exception parsing POST success guid.", e);
+ // Not much to be done.
+ }
+ }
+ }
+ // GC
+ success = null;
+
+ ExtendedJSONObject failed;
+ try {
+ failed = body.getObject("failed");
+ } catch (NonObjectJSONException e) {
+ handleRequestError(e);
+ return;
+ }
+
+ if (failed != null && !failed.object.isEmpty()) {
+ Logger.debug(LOG_TAG, "Failed records: " + failed.object.toString());
+ for (String guid : failed.keySet()) {
+ uploader.recordFailed(guid);
+ }
+ }
+ // GC
+ failed = null;
+
+ // And we're done! Let uploader finish up.
+ uploader.payloadSucceeded(response, isCommit, isLastPayload);
+ }
+
+ @Override
+ public void handleRequestFailure(final SyncStorageResponse response) {
+ this.handleRequestError(new HTTPFailureException(response));
+ }
+
+ @Override
+ public void handleRequestError(Exception e) {
+ for (String guid : postedRecordGuids) {
+ uploader.recordFailed(e, guid);
+ }
+ // GC
+ postedRecordGuids = null;
+
+ if (isLastPayload) {
+ uploader.lastPayloadFailed();
+ }
+ }
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/RecordUploadRunnable.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/RecordUploadRunnable.java
new file mode 100644
index 0000000000..ce2955102a
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/RecordUploadRunnable.java
@@ -0,0 +1,176 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.uploaders;
+
+import android.net.Uri;
+import android.support.annotation.VisibleForTesting;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.Server11PreviousPostFailedException;
+import org.mozilla.gecko.sync.net.SyncStorageRequest;
+import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+
+import ch.boye.httpclientandroidlib.entity.ContentProducer;
+import ch.boye.httpclientandroidlib.entity.EntityTemplate;
+
+/**
+ * Responsible for creating and posting a <code>SyncStorageRequest</code> request object.
+ */
+public class RecordUploadRunnable implements Runnable {
+ public final String LOG_TAG = "RecordUploadRunnable";
+
+ public final static byte[] RECORDS_START = { 91 }; // [ in UTF-8
+ public final static byte[] RECORD_SEPARATOR = { 44 }; // , in UTF-8
+ public final static byte[] RECORDS_END = { 93 }; // ] in UTF-8
+
+ private static final String QUERY_PARAM_BATCH = "batch";
+ private static final String QUERY_PARAM_TRUE = "true";
+ private static final String QUERY_PARAM_BATCH_COMMIT = "commit";
+
+ private final MayUploadProvider mayUploadProvider;
+ private final SyncStorageRequestDelegate uploadDelegate;
+
+ private final ArrayList<byte[]> outgoing;
+ private final long byteCount;
+
+ // Used to construct POST URI during run().
+ @VisibleForTesting
+ public final boolean isCommit;
+ private final Uri collectionUri;
+ private final BatchMeta batchMeta;
+
+ public RecordUploadRunnable(MayUploadProvider mayUploadProvider,
+ Uri collectionUri,
+ BatchMeta batchMeta,
+ SyncStorageRequestDelegate uploadDelegate,
+ ArrayList<byte[]> outgoing,
+ long byteCount,
+ boolean isCommit) {
+ this.mayUploadProvider = mayUploadProvider;
+ this.uploadDelegate = uploadDelegate;
+ this.outgoing = outgoing;
+ this.byteCount = byteCount;
+ this.batchMeta = batchMeta;
+ this.collectionUri = collectionUri;
+ this.isCommit = isCommit;
+ }
+
+ public static class ByteArraysContentProducer implements ContentProducer {
+ ArrayList<byte[]> outgoing;
+ public ByteArraysContentProducer(ArrayList<byte[]> arrays) {
+ outgoing = arrays;
+ }
+
+ @Override
+ public void writeTo(OutputStream outstream) throws IOException {
+ int count = outgoing.size();
+ outstream.write(RECORDS_START);
+ if (count > 0) {
+ outstream.write(outgoing.get(0));
+ for (int i = 1; i < count; ++i) {
+ outstream.write(RECORD_SEPARATOR);
+ outstream.write(outgoing.get(i));
+ }
+ }
+ outstream.write(RECORDS_END);
+ }
+
+ public static long outgoingBytesCount(ArrayList<byte[]> outgoing) {
+ final long numberOfRecords = outgoing.size();
+
+ // Account for start and end tokens.
+ long count = RECORDS_START.length + RECORDS_END.length;
+
+ // Account for all the records.
+ for (int i = 0; i < numberOfRecords; i++) {
+ count += outgoing.get(i).length;
+ }
+
+ // Account for a separator between the records.
+ // There's one less separator than there are records.
+ if (numberOfRecords > 1) {
+ count += RECORD_SEPARATOR.length * (numberOfRecords - 1);
+ }
+
+ return count;
+ }
+ }
+
+ public static class ByteArraysEntity extends EntityTemplate {
+ private final long count;
+ public ByteArraysEntity(ArrayList<byte[]> arrays, long totalBytes) {
+ super(new ByteArraysContentProducer(arrays));
+ this.count = totalBytes;
+ this.setContentType("application/json");
+ // charset is set in BaseResource.
+
+ // Sanity check our byte counts.
+ long realByteCount = ByteArraysContentProducer.outgoingBytesCount(arrays);
+ if (realByteCount != totalBytes) {
+ throw new IllegalStateException("Mismatched byte counts. Received " + totalBytes + " while real byte count is " + realByteCount);
+ }
+ }
+
+ @Override
+ public long getContentLength() {
+ return count;
+ }
+
+ @Override
+ public boolean isRepeatable() {
+ return true;
+ }
+ }
+
+ @Override
+ public void run() {
+ if (!mayUploadProvider.mayUpload()) {
+ Logger.info(LOG_TAG, "Told not to proceed by the uploader. Cancelling upload, failing records.");
+ uploadDelegate.handleRequestError(new Server11PreviousPostFailedException());
+ return;
+ }
+
+ Logger.trace(LOG_TAG, "Running upload task. Outgoing records: " + outgoing.size());
+
+ // We don't want the task queue to proceed until this request completes.
+ // Fortunately, BaseResource is currently synchronous.
+ // If that ever changes, you'll need to block here.
+
+ final URI postURI = buildPostURI(isCommit, batchMeta, collectionUri);
+ final SyncStorageRequest request = new SyncStorageRequest(postURI);
+ request.delegate = uploadDelegate;
+
+ ByteArraysEntity body = new ByteArraysEntity(outgoing, byteCount);
+ request.post(body);
+ }
+
+ @VisibleForTesting
+ public static URI buildPostURI(boolean isCommit, BatchMeta batchMeta, Uri collectionUri) {
+ final Uri.Builder uriBuilder = collectionUri.buildUpon();
+ final String batchToken = batchMeta.getToken();
+
+ if (batchToken != null) {
+ uriBuilder.appendQueryParameter(QUERY_PARAM_BATCH, batchToken);
+ } else {
+ uriBuilder.appendQueryParameter(QUERY_PARAM_BATCH, QUERY_PARAM_TRUE);
+ }
+
+ if (isCommit) {
+ uriBuilder.appendQueryParameter(QUERY_PARAM_BATCH_COMMIT, QUERY_PARAM_TRUE);
+ }
+
+ try {
+ return new URI(uriBuilder.build().toString());
+ } catch (URISyntaxException e) {
+ throw new IllegalStateException("Failed to construct a collection URI", e);
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/Constants.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/Constants.java
new file mode 100644
index 0000000000..66e6768b48
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/Constants.java
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.setup;
+
+public class Constants {
+ public static final String DEFAULT_PROFILE = "default";
+
+ /**
+ * Key in sync extras bundle specifying stages to sync this sync session.
+ * <p>
+ * Corresponding value should be a String JSON-encoding an object, the keys of
+ * which are the stage names to sync. For example:
+ * <code>"{ \"stageToSync\": 0 }"</code>.
+ */
+ public static final String EXTRAS_KEY_STAGES_TO_SYNC = "sync";
+
+ /**
+ * Key in sync extras bundle specifying stages to skip this sync session.
+ * <p>
+ * Corresponding value should be a String JSON-encoding an object, the keys of
+ * which are the stage names to skip. For example:
+ * <code>"{ \"stageToSkip\": 0 }"</code>.
+ */
+ public static final String EXTRAS_KEY_STAGES_TO_SKIP = "skip";
+
+ public static final String JSON_KEY_ACCOUNT = "account";
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/InvalidSyncKeyException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/InvalidSyncKeyException.java
new file mode 100644
index 0000000000..ac0fd58d04
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/InvalidSyncKeyException.java
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.setup;
+
+public class InvalidSyncKeyException extends Exception {
+ private static final long serialVersionUID = -6504925951580479894L;
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/activities/ActivityUtils.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/activities/ActivityUtils.java
new file mode 100644
index 0000000000..6542e1b00a
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/activities/ActivityUtils.java
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.setup.activities;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+
+import org.mozilla.gecko.background.common.GlobalConstants;
+import org.mozilla.gecko.db.BrowserContract;
+
+public class ActivityUtils {
+ /**
+ * Open a URL in Fennec, if one is provided; or just open Fennec.
+ *
+ * @param context Android context.
+ * @param url to visit, or null to just open Fennec.
+ */
+ public static void openURLInFennec(final Context context, final String url) {
+ Intent intent;
+ if (url != null) {
+ intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(url));
+ } else {
+ intent = new Intent(Intent.ACTION_MAIN);
+ }
+ intent.setClassName(GlobalConstants.BROWSER_INTENT_PACKAGE, GlobalConstants.BROWSER_INTENT_CLASS);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra(BrowserContract.SKIP_TAB_QUEUE_FLAG, true);
+ context.startActivity(intent);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/activities/WebURLFinder.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/activities/WebURLFinder.java
new file mode 100644
index 0000000000..8411d2a626
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/activities/WebURLFinder.java
@@ -0,0 +1,161 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.setup.activities;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class WebURLFinder {
+ /**
+ * These regular expressions are taken from Android's Patterns.java.
+ * We brought them in to standardize URL matching across Android versions, instead of relying
+ * on Android version-dependent built-ins that can vary across Android versions.
+ * The original code can be found here:
+ * http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/java/android/util/Patterns.java
+ *
+ */
+ public static final String GOOD_IRI_CHAR = "a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF";
+ public static final String GOOD_GTLD_CHAR = "a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF";
+ public static final String IRI = "[" + GOOD_IRI_CHAR + "]([" + GOOD_IRI_CHAR + "\\-]{0,61}[" + GOOD_IRI_CHAR + "]){0,1}";
+ public static final String GTLD = "[" + GOOD_GTLD_CHAR + "]{2,63}";
+ public static final String HOST_NAME = "(" + IRI + "\\.)+" + GTLD;
+ public static final Pattern IP_ADDRESS = Pattern.compile("((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]"
+ + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]"
+ + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}"
+ + "|[1-9][0-9]|[0-9]))");
+ public static final Pattern DOMAIN_NAME = Pattern.compile("(" + HOST_NAME + "|" + IP_ADDRESS + ")");
+ public static final Pattern WEB_URL = Pattern.compile("((?:(http|https|Http|Https|rtsp|Rtsp):\\/\\/(?:(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)"
+ + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_"
+ + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@)?)?"
+ + "(?:" + DOMAIN_NAME + ")"
+ + "(?:\\:\\d{1,5})?)"
+ + "(\\/(?:(?:[" + GOOD_IRI_CHAR + "\\;\\/\\?\\:\\@\\&\\=\\#\\~"
+ + "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*)?"
+ + "(?:\\b|$)");
+
+ public final List<String> candidates;
+
+ public WebURLFinder(String string) {
+ if (string == null) {
+ throw new IllegalArgumentException("string must not be null");
+ }
+
+ this.candidates = candidateWebURLs(string);
+ }
+
+ public WebURLFinder(List<String> strings) {
+ if (strings == null) {
+ throw new IllegalArgumentException("strings must not be null");
+ }
+
+ this.candidates = candidateWebURLs(strings);
+ }
+
+ /**
+ * Check if string is a Web URL.
+ * <p>
+ * A Web URL is a URI that is not a <code>file:</code> or
+ * <code>javascript:</code> scheme.
+ *
+ * @param string
+ * to check.
+ * @return <code>true</code> if <code>string</code> is a Web URL.
+ */
+ public static boolean isWebURL(String string) {
+ try {
+ new URI(string);
+ } catch (Exception e) {
+ return false;
+ }
+
+ if (android.webkit.URLUtil.isFileUrl(string) ||
+ android.webkit.URLUtil.isJavaScriptUrl(string)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Return best Web URL.
+ * <p>
+ * "Best" means a Web URL with a scheme, and failing that, a Web URL without a
+ * scheme.
+ *
+ * @return a Web URL or <code>null</code>.
+ */
+ public String bestWebURL() {
+ String firstWebURLWithScheme = firstWebURLWithScheme();
+ if (firstWebURLWithScheme != null) {
+ return firstWebURLWithScheme;
+ }
+
+ return firstWebURLWithoutScheme();
+ }
+
+ protected static List<String> candidateWebURLs(Collection<String> strings) {
+ List<String> candidates = new ArrayList<String>();
+
+ for (String string : strings) {
+ if (string == null) {
+ continue;
+ }
+
+ candidates.addAll(candidateWebURLs(string));
+ }
+
+ return candidates;
+ }
+
+ protected static List<String> candidateWebURLs(String string) {
+ Matcher matcher = WEB_URL.matcher(string);
+ List<String> matches = new LinkedList<String>();
+
+ while (matcher.find()) {
+ // Remove URLs with bad schemes.
+ if (!isWebURL(matcher.group())) {
+ continue;
+ }
+
+ // Remove parts of email addresses.
+ if (matcher.start() > 0 && (string.charAt(matcher.start() - 1) == '@')) {
+ continue;
+ }
+
+ matches.add(matcher.group());
+ }
+
+ return matches;
+ }
+
+ protected String firstWebURLWithScheme() {
+ for (String match : candidates) {
+ try {
+ if (new URI(match).getScheme() != null) {
+ return match;
+ }
+ } catch (URISyntaxException e) {
+ // Ignore: on to the next.
+ continue;
+ }
+ }
+
+ return null;
+ }
+
+ protected String firstWebURLWithoutScheme() {
+ if (!candidates.isEmpty()) {
+ return candidates.get(0);
+ }
+
+ return null;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AbstractNonRepositorySyncStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AbstractNonRepositorySyncStage.java
new file mode 100644
index 0000000000..c910216eb0
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AbstractNonRepositorySyncStage.java
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.stage;
+
+
+/**
+ * This is simply a stage that is not responsible for synchronizing repositories.
+ */
+public abstract class AbstractNonRepositorySyncStage extends AbstractSessionManagingSyncStage {
+ @Override
+ protected void resetLocal() {
+ // Do nothing.
+ }
+
+ @Override
+ protected void wipeLocal() {
+ // Do nothing.
+ }
+
+ @Override
+ public Integer getStorageVersion() {
+ return null; // Never include these engines in any meta/global records.
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AbstractSessionManagingSyncStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AbstractSessionManagingSyncStage.java
new file mode 100644
index 0000000000..6592c3baad
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AbstractSessionManagingSyncStage.java
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.stage;
+
+import org.mozilla.gecko.sync.GlobalSession;
+
+/**
+ * A global sync stage that manages a <code>GlobalSession</code> instance. This
+ * class is intended to be temporary: it should disappear as work to make
+ * data-driven syncs progresses.
+ * <p>
+ * This class is inherently <b>thread-unsafe</b>: if <code>session</code> is
+ * mutated after being set, all sorts of bad things could occur. At the time of
+ * writing, every <code>GlobalSyncStage</code> created is executed (wiped,
+ * reset) with the same <code>GlobalSession</code> argument.
+ */
+public abstract class AbstractSessionManagingSyncStage implements GlobalSyncStage {
+ protected GlobalSession session;
+
+ protected abstract void execute() throws NoSuchStageException;
+ protected abstract void resetLocal();
+ protected abstract void wipeLocal() throws Exception;
+
+ @Override
+ public void resetLocal(GlobalSession session) {
+ this.session = session;
+ resetLocal();
+ }
+
+ @Override
+ public void wipeLocal(GlobalSession session) throws Exception {
+ this.session = session;
+ wipeLocal();
+ }
+
+ @Override
+ public void execute(GlobalSession session) throws NoSuchStageException {
+ this.session = session;
+ execute();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AndroidBrowserBookmarksServerSyncStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AndroidBrowserBookmarksServerSyncStage.java
new file mode 100644
index 0000000000..10e209230c
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AndroidBrowserBookmarksServerSyncStage.java
@@ -0,0 +1,80 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.stage;
+
+import java.net.URISyntaxException;
+
+import org.mozilla.gecko.sync.JSONRecordFetcher;
+import org.mozilla.gecko.sync.MetaGlobalException;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.repositories.RecordFactory;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksRepository;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecordFactory;
+import org.mozilla.gecko.sync.repositories.domain.VersionConstants;
+
+public class AndroidBrowserBookmarksServerSyncStage extends ServerSyncStage {
+ protected static final String LOG_TAG = "BookmarksStage";
+
+ // Eventually this kind of sync stage will be data-driven,
+ // and all this hard-coding can go away.
+ private static final String BOOKMARKS_SORT = "index";
+ // Sanity limit. Batch and total limit are the same for now, and will be adjusted
+ // once buffer and high water mark are in place. See Bug 730142.
+ private static final long BOOKMARKS_BATCH_LIMIT = 5000;
+ private static final long BOOKMARKS_TOTAL_LIMIT = 5000;
+
+ @Override
+ protected String getCollection() {
+ return "bookmarks";
+ }
+
+ @Override
+ protected String getEngineName() {
+ return "bookmarks";
+ }
+
+ @Override
+ public Integer getStorageVersion() {
+ return VersionConstants.BOOKMARKS_ENGINE_VERSION;
+ }
+
+ @Override
+ protected Repository getRemoteRepository() throws URISyntaxException {
+ // If this is a first sync, we need to check server counts to make sure that we aren't
+ // going to screw up. SafeConstrainedServer11Repository does this. See Bug 814331.
+ AuthHeaderProvider authHeaderProvider = session.getAuthHeaderProvider();
+ final JSONRecordFetcher countsFetcher = new JSONRecordFetcher(session.config.infoCollectionCountsURL(), authHeaderProvider);
+ String collection = getCollection();
+ return new SafeConstrainedServer11Repository(
+ collection,
+ session.config.storageURL(),
+ session.getAuthHeaderProvider(),
+ session.config.infoCollections,
+ session.config.infoConfiguration,
+ BOOKMARKS_BATCH_LIMIT,
+ BOOKMARKS_TOTAL_LIMIT,
+ BOOKMARKS_SORT,
+ countsFetcher);
+ }
+
+ @Override
+ protected Repository getLocalRepository() {
+ return new AndroidBrowserBookmarksRepository();
+ }
+
+ @Override
+ protected RecordFactory getRecordFactory() {
+ return new BookmarkRecordFactory();
+ }
+
+ @Override
+ protected boolean isEnabled() throws MetaGlobalException {
+ if (session == null || session.getContext() == null) {
+ return false;
+ }
+ return super.isEnabled();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AndroidBrowserHistoryServerSyncStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AndroidBrowserHistoryServerSyncStage.java
new file mode 100644
index 0000000000..947a108986
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AndroidBrowserHistoryServerSyncStage.java
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.stage;
+
+import java.net.URISyntaxException;
+
+import org.mozilla.gecko.sync.MetaGlobalException;
+import org.mozilla.gecko.sync.repositories.ConstrainedServer11Repository;
+import org.mozilla.gecko.sync.repositories.RecordFactory;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.android.AndroidBrowserHistoryRepository;
+import org.mozilla.gecko.sync.repositories.domain.HistoryRecordFactory;
+import org.mozilla.gecko.sync.repositories.domain.VersionConstants;
+
+public class AndroidBrowserHistoryServerSyncStage extends ServerSyncStage {
+ protected static final String LOG_TAG = "HistoryStage";
+
+ // Eventually this kind of sync stage will be data-driven,
+ // and all this hard-coding can go away.
+ private static final String HISTORY_SORT = "index";
+ // Sanity limit. Batch and total limit are the same for now, and will be adjusted
+ // once buffer and high water mark are in place. See Bug 730142.
+ private static final long HISTORY_BATCH_LIMIT = 250;
+ private static final long HISTORY_TOTAL_LIMIT = 250;
+
+ @Override
+ protected String getCollection() {
+ return "history";
+ }
+
+ @Override
+ protected String getEngineName() {
+ return "history";
+ }
+
+ @Override
+ public Integer getStorageVersion() {
+ return VersionConstants.HISTORY_ENGINE_VERSION;
+ }
+
+ @Override
+ protected Repository getLocalRepository() {
+ return new AndroidBrowserHistoryRepository();
+ }
+
+ @Override
+ protected Repository getRemoteRepository() throws URISyntaxException {
+ String collection = getCollection();
+ return new ConstrainedServer11Repository(
+ collection,
+ session.config.storageURL(),
+ session.getAuthHeaderProvider(),
+ session.config.infoCollections,
+ session.config.infoConfiguration,
+ HISTORY_BATCH_LIMIT,
+ HISTORY_TOTAL_LIMIT,
+ HISTORY_SORT);
+ }
+
+ @Override
+ protected RecordFactory getRecordFactory() {
+ return new HistoryRecordFactory();
+ }
+
+ @Override
+ protected boolean isEnabled() throws MetaGlobalException {
+ if (session == null || session.getContext() == null) {
+ return false;
+ }
+ return super.isEnabled();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/CheckPreconditionsStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/CheckPreconditionsStage.java
new file mode 100644
index 0000000000..b33f83ad1d
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/CheckPreconditionsStage.java
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.stage;
+
+
+public class CheckPreconditionsStage extends AbstractNonRepositorySyncStage {
+ @Override
+ public void execute() throws NoSuchStageException {
+ session.advance();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/CompletedStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/CompletedStage.java
new file mode 100644
index 0000000000..7ec7763247
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/CompletedStage.java
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.stage;
+
+
+
+public class CompletedStage extends AbstractNonRepositorySyncStage {
+ @Override
+ public void execute() throws NoSuchStageException {
+ // TODO: Update tracking timestamps, close connections, etc.
+ // TODO: call clean() on each Repository in the sync constellation.
+ session.completeSync();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/EnsureCrypto5KeysStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/EnsureCrypto5KeysStage.java
new file mode 100644
index 0000000000..5031cf770f
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/EnsureCrypto5KeysStage.java
@@ -0,0 +1,192 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.stage;
+
+import java.net.URISyntaxException;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.CollectionKeys;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.InfoCollections;
+import org.mozilla.gecko.sync.NoCollectionKeysSetException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.crypto.PersistedCrypto5Keys;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.SyncStorageRecordRequest;
+import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+public class EnsureCrypto5KeysStage
+extends AbstractNonRepositorySyncStage
+implements SyncStorageRequestDelegate {
+
+ private static final String LOG_TAG = "EnsureC5KeysStage";
+ private static final String CRYPTO_COLLECTION = "crypto";
+ protected boolean retrying = false;
+
+ @Override
+ public void execute() throws NoSuchStageException {
+ InfoCollections infoCollections = session.config.infoCollections;
+ if (infoCollections == null) {
+ session.abort(null, "No info/collections set in EnsureCrypto5KeysStage.");
+ return;
+ }
+
+ PersistedCrypto5Keys pck = session.config.persistedCryptoKeys();
+ long lastModified = pck.lastModified();
+ if (retrying || !infoCollections.updateNeeded(CRYPTO_COLLECTION, lastModified)) {
+ // Try to use our local collection keys for this session.
+ Logger.debug(LOG_TAG, "Trying to use persisted collection keys for this session.");
+ CollectionKeys keys = pck.keys();
+ if (keys != null) {
+ Logger.trace(LOG_TAG, "Using persisted collection keys for this session.");
+ session.config.setCollectionKeys(keys);
+ session.advance();
+ return;
+ }
+ Logger.trace(LOG_TAG, "Failed to use persisted collection keys for this session.");
+ }
+
+ // We need an update: fetch fresh keys.
+ Logger.debug(LOG_TAG, "Fetching fresh collection keys for this session.");
+ try {
+ SyncStorageRecordRequest request = new SyncStorageRecordRequest(session.wboURI(CRYPTO_COLLECTION, "keys"));
+ request.delegate = this;
+ request.get();
+ } catch (URISyntaxException e) {
+ session.abort(e, "Invalid URI.");
+ }
+ }
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return session.getAuthHeaderProvider();
+ }
+
+ @Override
+ public String ifUnmodifiedSince() {
+ // TODO: last key time!
+ return null;
+ }
+
+ protected void setAndPersist(PersistedCrypto5Keys pck, CollectionKeys keys, long timestamp) {
+ session.config.setCollectionKeys(keys);
+ pck.persistKeys(keys);
+ pck.persistLastModified(timestamp);
+ }
+
+ /**
+ * Return collections where either the individual key has changed, or if the
+ * new default key is not the same as the old default key, where the
+ * collection is using the default key.
+ */
+ protected Set<String> collectionsToUpdate(CollectionKeys oldKeys, CollectionKeys newKeys) {
+ // These keys have explicitly changed; they definitely need updating.
+ Set<String> changedKeys = new HashSet<String>(CollectionKeys.differences(oldKeys, newKeys));
+
+ boolean defaultKeyChanged = true; // Most pessimistic is to assume default key has changed.
+ KeyBundle newDefaultKeyBundle = null;
+ try {
+ KeyBundle oldDefaultKeyBundle = oldKeys.defaultKeyBundle();
+ newDefaultKeyBundle = newKeys.defaultKeyBundle();
+ defaultKeyChanged = !oldDefaultKeyBundle.equals(newDefaultKeyBundle);
+ } catch (NoCollectionKeysSetException e) {
+ Logger.warn(LOG_TAG, "NoCollectionKeysSetException in EnsureCrypto5KeysStage.", e);
+ }
+
+ if (newDefaultKeyBundle == null) {
+ Logger.trace(LOG_TAG, "New default key not provided; returning changed individual keys.");
+ return changedKeys;
+ }
+
+ if (!defaultKeyChanged) {
+ Logger.trace(LOG_TAG, "New default key is the same as old default key; returning changed individual keys.");
+ return changedKeys;
+ }
+
+ // New keys have a different default/sync key; check known collections against the default key.
+ Logger.debug(LOG_TAG, "New default key is not the same as old default key.");
+ for (Stage stage : Stage.getNamedStages()) {
+ String name = stage.getRepositoryName();
+ if (!newKeys.keyBundleForCollectionIsNotDefault(name)) {
+ // Default key has changed, so this collection has changed.
+ changedKeys.add(name);
+ }
+ }
+
+ return changedKeys;
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse response) {
+ // Take the timestamp from the response since it is later than the timestamp from info/collections.
+ long responseTimestamp = response.normalizedWeaveTimestamp();
+ CollectionKeys keys = new CollectionKeys();
+ try {
+ ExtendedJSONObject body = response.jsonObjectBody();
+ if (Logger.LOG_PERSONAL_INFORMATION) {
+ Logger.pii(LOG_TAG, "Fetched keys: " + body.toJSONString());
+ }
+ keys.setKeyPairsFromWBO(CryptoRecord.fromJSONRecord(body), session.config.syncKeyBundle);
+ } catch (Exception e) {
+ session.abort(e, "Invalid keys WBO.");
+ return;
+ }
+
+ PersistedCrypto5Keys pck = session.config.persistedCryptoKeys();
+ if (!pck.persistedKeysExist()) {
+ // New keys, and no old keys! Persist keys and server timestamp.
+ Logger.trace(LOG_TAG, "Setting fetched keys for this session; persisting fetched keys and last modified.");
+ setAndPersist(pck, keys, responseTimestamp);
+ session.advance();
+ return;
+ }
+
+ // New keys, but we had old keys. Check for differences.
+ CollectionKeys oldKeys = pck.keys();
+ Set<String> changedCollections = collectionsToUpdate(oldKeys, keys);
+ if (!changedCollections.isEmpty()) {
+ // New keys, different from old keys.
+ Logger.trace(LOG_TAG, "Fetched keys are not the same as persisted keys; " +
+ "setting fetched keys for this session before resetting changed engines.");
+ setAndPersist(pck, keys, responseTimestamp);
+ session.resetStagesByName(changedCollections);
+ session.abort(null, "crypto/keys changed on server.");
+ return;
+ }
+
+ // New keys don't differ from old keys; persist timestamp and move on.
+ Logger.trace(LOG_TAG, "Fetched keys are the same as persisted keys; persisting only last modified.");
+ session.config.setCollectionKeys(oldKeys);
+ pck.persistLastModified(response.normalizedWeaveTimestamp());
+ session.advance();
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ if (retrying) {
+ // Should happen very rarely -- this means we uploaded our crypto/keys
+ // successfully, but failed to re-download.
+ session.handleHTTPError(response, "Failure while re-downloading already uploaded keys.");
+ return;
+ }
+
+ int statusCode = response.getStatusCode();
+ if (statusCode == 404) {
+ Logger.info(LOG_TAG, "Got 404 fetching keys. Fresh starting since keys are missing on server.");
+ session.freshStart();
+ return;
+ }
+ session.handleHTTPError(response, "Failure fetching keys: got response status code " + statusCode);
+ }
+
+ @Override
+ public void handleRequestError(Exception ex) {
+ session.abort(ex, "Failure fetching keys.");
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FennecTabsServerSyncStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FennecTabsServerSyncStage.java
new file mode 100644
index 0000000000..40a474ef47
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FennecTabsServerSyncStage.java
@@ -0,0 +1,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/. */
+
+package org.mozilla.gecko.sync.stage;
+
+import org.mozilla.gecko.sync.repositories.RecordFactory;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository;
+import org.mozilla.gecko.sync.repositories.domain.TabsRecordFactory;
+import org.mozilla.gecko.sync.repositories.domain.VersionConstants;
+
+public class FennecTabsServerSyncStage extends ServerSyncStage {
+ private static final String COLLECTION = "tabs";
+
+ @Override
+ protected String getCollection() {
+ return COLLECTION;
+ }
+
+ @Override
+ protected String getEngineName() {
+ return COLLECTION;
+ }
+
+ @Override
+ public Integer getStorageVersion() {
+ return VersionConstants.TABS_ENGINE_VERSION;
+ }
+
+ @Override
+ protected Repository getLocalRepository() {
+ return new FennecTabsRepository(session.getClientsDelegate());
+ }
+
+ @Override
+ protected RecordFactory getRecordFactory() {
+ return new TabsRecordFactory();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchInfoCollectionsStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchInfoCollectionsStage.java
new file mode 100644
index 0000000000..088321d5b4
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchInfoCollectionsStage.java
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.stage;
+
+import java.net.URISyntaxException;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.InfoCollections;
+import org.mozilla.gecko.sync.delegates.JSONRecordFetchDelegate;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+public class FetchInfoCollectionsStage extends AbstractNonRepositorySyncStage {
+ public class StageInfoCollectionsDelegate implements JSONRecordFetchDelegate {
+
+ @Override
+ public void handleSuccess(ExtendedJSONObject global) {
+ session.config.infoCollections = new InfoCollections(global);
+ session.advance();
+ }
+
+ @Override
+ public void handleFailure(SyncStorageResponse response) {
+ session.handleHTTPError(response, "Failure fetching info/collections.");
+ }
+
+ @Override
+ public void handleError(Exception e) {
+ session.abort(e, "Failure fetching info/collections.");
+ }
+
+ }
+
+ @Override
+ public void execute() throws NoSuchStageException {
+ try {
+ session.fetchInfoCollections(new StageInfoCollectionsDelegate());
+ } catch (URISyntaxException e) {
+ session.abort(e, "Invalid URI.");
+ }
+ }
+
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchInfoConfigurationStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchInfoConfigurationStage.java
new file mode 100644
index 0000000000..7f53c27391
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchInfoConfigurationStage.java
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.stage;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.InfoConfiguration;
+import org.mozilla.gecko.sync.JSONRecordFetcher;
+import org.mozilla.gecko.sync.delegates.JSONRecordFetchDelegate;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+/**
+ * Fetches configuration data from info/configurations endpoint.
+ */
+public class FetchInfoConfigurationStage extends AbstractNonRepositorySyncStage {
+ private final String configurationURL;
+ private final AuthHeaderProvider authHeaderProvider;
+
+ public FetchInfoConfigurationStage(final String configurationURL, final AuthHeaderProvider authHeaderProvider) {
+ super();
+ this.configurationURL = configurationURL;
+ this.authHeaderProvider = authHeaderProvider;
+ }
+
+ public class StageInfoConfigurationDelegate implements JSONRecordFetchDelegate {
+ @Override
+ public void handleSuccess(final ExtendedJSONObject result) {
+ session.config.infoConfiguration = new InfoConfiguration(result);
+ session.advance();
+ }
+
+ @Override
+ public void handleFailure(final SyncStorageResponse response) {
+ // Handle all non-404 failures upstream.
+ if (response.getStatusCode() != 404) {
+ session.handleHTTPError(response, "Failure fetching info/configuration");
+ return;
+ }
+
+ // End-point might not be available (404) if server is running an older version.
+ // We will use default config values in this case.
+ session.config.infoConfiguration = new InfoConfiguration();
+ session.advance();
+ }
+
+ @Override
+ public void handleError(final Exception e) {
+ session.abort(e, "Failure fetching info/configuration");
+ }
+ }
+ @Override
+ public void execute() {
+ final StageInfoConfigurationDelegate delegate = new StageInfoConfigurationDelegate();
+ final JSONRecordFetcher fetcher = new JSONRecordFetcher(configurationURL, authHeaderProvider);
+ fetcher.fetch(delegate);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchMetaGlobalStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchMetaGlobalStage.java
new file mode 100644
index 0000000000..b4407b26bb
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchMetaGlobalStage.java
@@ -0,0 +1,79 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.stage;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.InfoCollections;
+import org.mozilla.gecko.sync.MetaGlobal;
+import org.mozilla.gecko.sync.PersistedMetaGlobal;
+import org.mozilla.gecko.sync.delegates.MetaGlobalDelegate;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+public class FetchMetaGlobalStage extends AbstractNonRepositorySyncStage {
+ private static final String LOG_TAG = "FetchMetaGlobalStage";
+ private static final String META_COLLECTION = "meta";
+
+ public class StageMetaGlobalDelegate implements MetaGlobalDelegate {
+
+ private final GlobalSession session;
+ public StageMetaGlobalDelegate(GlobalSession session) {
+ this.session = session;
+ }
+
+ @Override
+ public void handleSuccess(MetaGlobal global, SyncStorageResponse response) {
+ Logger.trace(LOG_TAG, "Persisting fetched meta/global and last modified.");
+ PersistedMetaGlobal pmg = session.config.persistedMetaGlobal();
+ pmg.persistMetaGlobal(global);
+ // Take the timestamp from the response since it is later than the timestamp from info/collections.
+ pmg.persistLastModified(response.normalizedWeaveTimestamp());
+
+ session.processMetaGlobal(global);
+ }
+
+ @Override
+ public void handleFailure(SyncStorageResponse response) {
+ session.handleHTTPError(response, "Failure fetching meta/global.");
+ }
+
+ @Override
+ public void handleError(Exception e) {
+ session.abort(e, "Failure fetching meta/global.");
+ }
+
+ @Override
+ public void handleMissing(MetaGlobal global, SyncStorageResponse response) {
+ session.processMissingMetaGlobal(global);
+ }
+ }
+
+ @Override
+ public void execute() throws NoSuchStageException {
+ InfoCollections infoCollections = session.config.infoCollections;
+ if (infoCollections == null) {
+ session.abort(null, "No info/collections set in FetchMetaGlobalStage.");
+ return;
+ }
+
+ long lastModified = session.config.persistedMetaGlobal().lastModified();
+ if (!infoCollections.updateNeeded(META_COLLECTION, lastModified)) {
+ // Try to use our local collection keys for this session.
+ Logger.info(LOG_TAG, "Trying to use persisted meta/global for this session.");
+ MetaGlobal global = session.config.persistedMetaGlobal().metaGlobal(session.config.metaURL(), session.getAuthHeaderProvider());
+ if (global != null) {
+ Logger.info(LOG_TAG, "Using persisted meta/global for this session.");
+ session.processMetaGlobal(global); // Calls session.advance().
+ return;
+ }
+ Logger.info(LOG_TAG, "Failed to use persisted meta/global for this session.");
+ }
+
+ // We need an update: fetch or upload meta/global as necessary.
+ Logger.info(LOG_TAG, "Fetching fresh meta/global for this session.");
+ MetaGlobal global = new MetaGlobal(session.config.metaURL(), session.getAuthHeaderProvider());
+ global.fetch(new StageMetaGlobalDelegate(session));
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FormHistoryServerSyncStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FormHistoryServerSyncStage.java
new file mode 100644
index 0000000000..0a5d974b8f
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FormHistoryServerSyncStage.java
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.stage;
+
+import java.net.URISyntaxException;
+
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.repositories.ConstrainedServer11Repository;
+import org.mozilla.gecko.sync.repositories.RecordFactory;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.android.FormHistoryRepositorySession;
+import org.mozilla.gecko.sync.repositories.domain.FormHistoryRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+import org.mozilla.gecko.sync.repositories.domain.VersionConstants;
+
+public class FormHistoryServerSyncStage extends ServerSyncStage {
+
+ // Eventually this kind of sync stage will be data-driven,
+ // and all this hard-coding can go away.
+ private static final String FORM_HISTORY_SORT = "index";
+ // Sanity limit. Batch and total limit are the same for now, and will be adjusted
+ // once buffer and high water mark are in place. See Bug 730142.
+ private static final long FORM_HISTORY_BATCH_LIMIT = 5000;
+ private static final long FORM_HISTORY_TOTAL_LIMIT = 5000;
+
+ @Override
+ protected String getCollection() {
+ return "forms";
+ }
+
+ @Override
+ protected String getEngineName() {
+ return "forms";
+ }
+
+ @Override
+ public Integer getStorageVersion() {
+ return VersionConstants.FORMS_ENGINE_VERSION;
+ }
+
+ @Override
+ protected Repository getRemoteRepository() throws URISyntaxException {
+ String collection = getCollection();
+ return new ConstrainedServer11Repository(
+ collection,
+ session.config.storageURL(),
+ session.getAuthHeaderProvider(),
+ session.config.infoCollections,
+ session.config.infoConfiguration,
+ FORM_HISTORY_BATCH_LIMIT,
+ FORM_HISTORY_TOTAL_LIMIT,
+ FORM_HISTORY_SORT);
+ }
+
+ @Override
+ protected Repository getLocalRepository() {
+ return new FormHistoryRepositorySession.FormHistoryRepository();
+ }
+
+ public class FormHistoryRecordFactory extends RecordFactory {
+
+ @Override
+ public Record createRecord(Record record) {
+ FormHistoryRecord r = new FormHistoryRecord();
+ r.initFromEnvelope((CryptoRecord) record);
+ return r;
+ }
+ }
+
+ @Override
+ protected RecordFactory getRecordFactory() {
+ return new FormHistoryRecordFactory();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/GlobalSyncStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/GlobalSyncStage.java
new file mode 100644
index 0000000000..6dee71f90b
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/GlobalSyncStage.java
@@ -0,0 +1,93 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.stage;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.mozilla.gecko.sync.GlobalSession;
+
+
+public interface GlobalSyncStage {
+ public static enum Stage {
+ idle, // Start state.
+ checkPreconditions, // Preparation of the basics. TODO: clear status
+ fetchInfoCollections, // Take a look at timestamps.
+ fetchInfoConfiguration, // Fetch server upload limits
+ fetchMetaGlobal,
+ ensureKeysStage,
+ /*
+ ensureSpecialRecords,
+ updateEngineTimestamps,
+ */
+ syncClientsEngine(SyncClientsEngineStage.STAGE_NAME),
+ /*
+ processFirstSyncPref,
+ processClientCommands,
+ updateEnabledEngines,
+ */
+ syncTabs("tabs"),
+ syncPasswords("passwords"),
+ syncBookmarks("bookmarks"),
+ syncHistory("history"),
+ syncFormHistory("forms"),
+
+ uploadMetaGlobal,
+ completed;
+
+ // Maintain a mapping from names ("bookmarks") to Stage enumerations (syncBookmarks).
+ private static final Map<String, Stage> named = new HashMap<String, Stage>();
+ static {
+ for (Stage s : EnumSet.allOf(Stage.class)) {
+ if (s.getRepositoryName() != null) {
+ named.put(s.getRepositoryName(), s);
+ }
+ }
+ }
+
+ public static Stage byName(final String name) {
+ if (name == null) {
+ return null;
+ }
+ return named.get(name);
+ }
+
+ /**
+ * @return an immutable collection of Stages.
+ */
+ public static Collection<Stage> getNamedStages() {
+ return Collections.unmodifiableCollection(named.values());
+ }
+
+ // Each Stage tracks its repositoryName.
+ private final String repositoryName;
+ public String getRepositoryName() {
+ return repositoryName;
+ }
+
+ private Stage() {
+ this.repositoryName = null;
+ }
+
+ private Stage(final String name) {
+ this.repositoryName = name;
+ }
+ }
+
+ public void execute(GlobalSession session) throws NoSuchStageException;
+ public void resetLocal(GlobalSession session);
+ public void wipeLocal(GlobalSession session) throws Exception;
+
+ /**
+ * What storage version number this engine supports.
+ * <p>
+ * Used to generate a fresh meta/global record for upload.
+ * @return a version number or <code>null</code> to never include this engine in a fresh meta/global record.
+ */
+ public Integer getStorageVersion();
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/NoSuchStageException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/NoSuchStageException.java
new file mode 100644
index 0000000000..14c9bb43ea
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/NoSuchStageException.java
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.stage;
+
+public class NoSuchStageException extends Exception {
+ private static final long serialVersionUID = 8338484472880746971L;
+ GlobalSyncStage.Stage stage;
+ public NoSuchStageException(GlobalSyncStage.Stage stage) {
+ this.stage = stage;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/PasswordsServerSyncStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/PasswordsServerSyncStage.java
new file mode 100644
index 0000000000..c781ce2cc5
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/PasswordsServerSyncStage.java
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.stage;
+
+import org.mozilla.gecko.sync.repositories.RecordFactory;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.android.PasswordsRepositorySession;
+import org.mozilla.gecko.sync.repositories.domain.PasswordRecordFactory;
+import org.mozilla.gecko.sync.repositories.domain.VersionConstants;
+
+public class PasswordsServerSyncStage extends ServerSyncStage {
+ @Override
+ protected String getCollection() {
+ return "passwords";
+ }
+
+ @Override
+ protected String getEngineName() {
+ return "passwords";
+ }
+
+ @Override
+ public Integer getStorageVersion() {
+ return VersionConstants.PASSWORDS_ENGINE_VERSION;
+ }
+
+ @Override
+ protected Repository getLocalRepository() {
+ return new PasswordsRepositorySession.PasswordsRepository();
+ }
+
+ @Override
+ protected RecordFactory getRecordFactory() {
+ return new PasswordRecordFactory();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SafeConstrainedServer11Repository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SafeConstrainedServer11Repository.java
new file mode 100644
index 0000000000..733c887f0a
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SafeConstrainedServer11Repository.java
@@ -0,0 +1,110 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.stage;
+
+import java.net.URISyntaxException;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.InfoCollections;
+import org.mozilla.gecko.sync.InfoConfiguration;
+import org.mozilla.gecko.sync.InfoCounts;
+import org.mozilla.gecko.sync.JSONRecordFetcher;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.repositories.ConstrainedServer11Repository;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.Server11RepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+
+import android.content.Context;
+
+/**
+ * This is a constrained repository -- one which fetches a limited number
+ * of records -- that additionally refuses to sync if the limit will
+ * be exceeded on a first sync by the number of records on the server.
+ *
+ * You must pass an {@link InfoCounts} instance, which will be interrogated
+ * in the event of a first sync.
+ *
+ * "First sync" means that our sync timestamp is not greater than zero.
+ */
+public class SafeConstrainedServer11Repository extends ConstrainedServer11Repository {
+
+ // This can be lazily evaluated if we need it.
+ private final JSONRecordFetcher countFetcher;
+
+ public SafeConstrainedServer11Repository(String collection,
+ String storageURL,
+ AuthHeaderProvider authHeaderProvider,
+ InfoCollections infoCollections,
+ InfoConfiguration infoConfiguration,
+ long batchLimit,
+ long totalLimit,
+ String sort,
+ JSONRecordFetcher countFetcher)
+ throws URISyntaxException {
+ super(collection, storageURL, authHeaderProvider, infoCollections, infoConfiguration,
+ batchLimit, totalLimit, sort);
+ if (countFetcher == null) {
+ throw new IllegalArgumentException("countFetcher must not be null");
+ }
+ this.countFetcher = countFetcher;
+ }
+
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ delegate.onSessionCreated(new CountCheckingServer11RepositorySession(this, this.getDefaultBatchLimit()));
+ }
+
+ public class CountCheckingServer11RepositorySession extends Server11RepositorySession {
+ private static final String LOG_TAG = "CountCheckingServer11RepositorySession";
+
+ /**
+ * The session will report no data available if this is a first sync
+ * and the server has more data available than this limit.
+ */
+ private final long fetchLimit;
+
+ public CountCheckingServer11RepositorySession(Repository repository, long fetchLimit) {
+ super(repository);
+ this.fetchLimit = fetchLimit;
+ }
+
+ @Override
+ public boolean shouldSkip() {
+ // If this is a first sync, verify that we aren't going to blow through our limit.
+ final long lastSyncTimestamp = getLastSyncTimestamp();
+ if (lastSyncTimestamp > 0) {
+ Logger.info(LOG_TAG, "Collection " + collection + " has already had a first sync: " +
+ "timestamp is " + lastSyncTimestamp + "; " +
+ "ignoring any updated counts and syncing as usual.");
+ } else {
+ Logger.info(LOG_TAG, "Collection " + collection + " is starting a first sync; checking counts.");
+
+ final InfoCounts counts;
+ try {
+ // This'll probably be the same object, but best to obey the API.
+ counts = new InfoCounts(countFetcher.fetchBlocking());
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Skipping " + collection + " until we can fetch counts.", e);
+ return true;
+ }
+
+ Integer c = counts.getCount(collection);
+ if (c == null) {
+ Logger.info(LOG_TAG, "Fetched counts does not include collection " + collection + "; syncing as usual.");
+ return false;
+ }
+
+ Logger.info(LOG_TAG, "First sync for " + collection + ": " + c + " items.");
+ if (c > fetchLimit) {
+ Logger.warn(LOG_TAG, "Too many items to sync safely. Skipping.");
+ return true;
+ }
+ }
+ return super.shouldSkip();
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/ServerSyncStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/ServerSyncStage.java
new file mode 100644
index 0000000000..733e69da5f
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/ServerSyncStage.java
@@ -0,0 +1,627 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.stage;
+
+import android.content.Context;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.EngineSettings;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.HTTPFailureException;
+import org.mozilla.gecko.sync.MetaGlobalException;
+import org.mozilla.gecko.sync.NoCollectionKeysSetException;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.SynchronizerConfiguration;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.delegates.WipeServerDelegate;
+import org.mozilla.gecko.sync.middleware.Crypto5MiddlewareRepository;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.SyncStorageRequest;
+import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.sync.repositories.InactiveSessionException;
+import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
+import org.mozilla.gecko.sync.repositories.RecordFactory;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
+import org.mozilla.gecko.sync.repositories.Server11Repository;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
+import org.mozilla.gecko.sync.synchronizer.ServerLocalSynchronizer;
+import org.mozilla.gecko.sync.synchronizer.Synchronizer;
+import org.mozilla.gecko.sync.synchronizer.SynchronizerDelegate;
+import org.mozilla.gecko.sync.synchronizer.SynchronizerSession;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Fetch from a server collection into a local repository, encrypting
+ * and decrypting along the way.
+ *
+ * @author rnewman
+ *
+ */
+public abstract class ServerSyncStage extends AbstractSessionManagingSyncStage implements SynchronizerDelegate {
+
+ protected static final String LOG_TAG = "ServerSyncStage";
+
+ protected long stageStartTimestamp = -1;
+ protected long stageCompleteTimestamp = -1;
+
+ /**
+ * Override these in your subclasses.
+ *
+ * @return true if this stage should be executed.
+ * @throws MetaGlobalException
+ */
+ protected boolean isEnabled() throws MetaGlobalException {
+ EngineSettings engineSettings = null;
+ try {
+ engineSettings = getEngineSettings();
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Unable to get engine settings for " + this + ": fetching config failed.", e);
+ // Fall through; null engineSettings will pass below.
+ }
+
+ // We can be disabled by the server's meta/global record, or malformed in the server's meta/global record,
+ // or by the user manually in Sync Settings.
+ // We catch the subclasses of MetaGlobalException to trigger various resets and wipes in execute().
+ boolean enabledInMetaGlobal = session.isEngineRemotelyEnabled(this.getEngineName(), engineSettings);
+
+ // Check for manual changes to engines by the user.
+ checkAndUpdateUserSelectedEngines(enabledInMetaGlobal);
+
+ // Check for changes on the server.
+ if (!enabledInMetaGlobal) {
+ Logger.debug(LOG_TAG, "Stage " + this.getEngineName() + " disabled by server meta/global.");
+ return false;
+ }
+
+ // We can also be disabled just for this sync.
+ boolean enabledThisSync = session.isEngineLocallyEnabled(this.getEngineName()); // For ServerSyncStage, stage name == engine name.
+ if (!enabledThisSync) {
+ Logger.debug(LOG_TAG, "Stage " + this.getEngineName() + " disabled just for this sync.");
+ }
+ return enabledThisSync;
+ }
+
+ /**
+ * Compares meta/global engine state to user selected engines from Sync
+ * Settings and throws an exception if they don't match and meta/global needs
+ * to be updated.
+ *
+ * @param enabledInMetaGlobal
+ * boolean of engine sync state in meta/global
+ * @throws MetaGlobalException
+ * if engine sync state has been changed in Sync Settings, with new
+ * engine sync state.
+ */
+ protected void checkAndUpdateUserSelectedEngines(boolean enabledInMetaGlobal) throws MetaGlobalException {
+ Map<String, Boolean> selectedEngines = session.config.userSelectedEngines;
+ String thisEngine = this.getEngineName();
+
+ if (selectedEngines != null && selectedEngines.containsKey(thisEngine)) {
+ boolean enabledInSelection = selectedEngines.get(thisEngine);
+ if (enabledInMetaGlobal != enabledInSelection) {
+ // Engine enable state has been changed by the user.
+ Logger.debug(LOG_TAG, "Engine state has been changed by user. Throwing exception.");
+ throw new MetaGlobalException.MetaGlobalEngineStateChangedException(enabledInSelection);
+ }
+ }
+ }
+
+ protected EngineSettings getEngineSettings() throws NonObjectJSONException, IOException {
+ Integer version = getStorageVersion();
+ if (version == null) {
+ Logger.warn(LOG_TAG, "null storage version for " + this + "; using version 0.");
+ version = 0;
+ }
+
+ SynchronizerConfiguration config = this.getConfig();
+ if (config == null) {
+ return new EngineSettings(null, version);
+ }
+ return new EngineSettings(config.syncID, version);
+ }
+
+ protected abstract String getCollection();
+ protected abstract String getEngineName();
+ protected abstract Repository getLocalRepository();
+ protected abstract RecordFactory getRecordFactory();
+
+ // Override this in subclasses.
+ protected Repository getRemoteRepository() throws URISyntaxException {
+ String collection = getCollection();
+ return new Server11Repository(collection,
+ session.config.storageURL(),
+ session.getAuthHeaderProvider(),
+ session.config.infoCollections,
+ session.config.infoConfiguration);
+ }
+
+ /**
+ * Return a Crypto5Middleware-wrapped Server11Repository.
+ *
+ * @throws NoCollectionKeysSetException
+ * @throws URISyntaxException
+ */
+ protected Repository wrappedServerRepo() throws NoCollectionKeysSetException, URISyntaxException {
+ String collection = this.getCollection();
+ KeyBundle collectionKey = session.keyBundleForCollection(collection);
+ Crypto5MiddlewareRepository cryptoRepo = new Crypto5MiddlewareRepository(getRemoteRepository(), collectionKey);
+ cryptoRepo.recordFactory = getRecordFactory();
+ return cryptoRepo;
+ }
+
+ protected String bundlePrefix() {
+ return this.getCollection() + ".";
+ }
+
+ protected SynchronizerConfiguration getConfig() throws NonObjectJSONException, IOException {
+ return new SynchronizerConfiguration(session.config.getBranch(bundlePrefix()));
+ }
+
+ protected void persistConfig(SynchronizerConfiguration synchronizerConfiguration) {
+ synchronizerConfiguration.persist(session.config.getBranch(bundlePrefix()));
+ }
+
+ public Synchronizer getConfiguredSynchronizer(GlobalSession session) throws NoCollectionKeysSetException, URISyntaxException, NonObjectJSONException, IOException {
+ Repository remote = wrappedServerRepo();
+
+ Synchronizer synchronizer = new ServerLocalSynchronizer();
+ synchronizer.repositoryA = remote;
+ synchronizer.repositoryB = this.getLocalRepository();
+ synchronizer.load(getConfig());
+
+ return synchronizer;
+ }
+
+ /**
+ * Reset timestamps.
+ */
+ @Override
+ protected void resetLocal() {
+ resetLocalWithSyncID(null);
+ }
+
+ /**
+ * Reset timestamps and possibly set syncID.
+ * @param syncID if non-null, new syncID to persist.
+ */
+ protected void resetLocalWithSyncID(String syncID) {
+ // Clear both timestamps.
+ SynchronizerConfiguration config;
+ try {
+ config = this.getConfig();
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Unable to reset " + this + ": fetching config failed.", e);
+ return;
+ }
+
+ if (syncID != null) {
+ config.syncID = syncID;
+ Logger.info(LOG_TAG, "Setting syncID for " + this + " to '" + syncID + "'.");
+ }
+ config.localBundle.setTimestamp(0L);
+ config.remoteBundle.setTimestamp(0L);
+ persistConfig(config);
+ Logger.info(LOG_TAG, "Reset timestamps for " + this);
+ }
+
+ // Not thread-safe. Use with caution.
+ private class WipeWaiter {
+ public boolean sessionSucceeded = true;
+ public boolean wipeSucceeded = true;
+ public Exception error;
+
+ public void notify(Exception e, boolean sessionSucceeded) {
+ this.sessionSucceeded = sessionSucceeded;
+ this.wipeSucceeded = false;
+ this.error = e;
+ this.notify();
+ }
+ }
+
+ /**
+ * Synchronously wipe this stage by instantiating a local repository session
+ * and wiping that.
+ * <p>
+ * Logs and re-throws an exception on failure.
+ */
+ @Override
+ protected void wipeLocal() throws Exception {
+ // Reset, then clear data.
+ this.resetLocal();
+
+ final WipeWaiter monitor = new WipeWaiter();
+ final Context context = session.getContext();
+ final Repository r = this.getLocalRepository();
+
+ final Runnable doWipe = new Runnable() {
+ @Override
+ public void run() {
+ r.createSession(new RepositorySessionCreationDelegate() {
+
+ @Override
+ public void onSessionCreated(final RepositorySession session) {
+ try {
+ session.begin(new RepositorySessionBeginDelegate() {
+
+ @Override
+ public void onBeginSucceeded(final RepositorySession session) {
+ session.wipe(new RepositorySessionWipeDelegate() {
+ @Override
+ public void onWipeSucceeded() {
+ try {
+ session.finish(new RepositorySessionFinishDelegate() {
+
+ @Override
+ public void onFinishSucceeded(RepositorySession session,
+ RepositorySessionBundle bundle) {
+ // Hurrah.
+ synchronized (monitor) {
+ monitor.notify();
+ }
+ }
+
+ @Override
+ public void onFinishFailed(Exception ex) {
+ // Assume that no finish => no wipe.
+ synchronized (monitor) {
+ monitor.notify(ex, true);
+ }
+ }
+
+ @Override
+ public RepositorySessionFinishDelegate deferredFinishDelegate(ExecutorService executor) {
+ return this;
+ }
+ });
+ } catch (InactiveSessionException e) {
+ // Cannot happen. Call for safety.
+ synchronized (monitor) {
+ monitor.notify(e, true);
+ }
+ }
+ }
+
+ @Override
+ public void onWipeFailed(Exception ex) {
+ session.abort();
+ synchronized (monitor) {
+ monitor.notify(ex, true);
+ }
+ }
+
+ @Override
+ public RepositorySessionWipeDelegate deferredWipeDelegate(ExecutorService executor) {
+ return this;
+ }
+ });
+ }
+
+ @Override
+ public void onBeginFailed(Exception ex) {
+ session.abort();
+ synchronized (monitor) {
+ monitor.notify(ex, true);
+ }
+ }
+
+ @Override
+ public RepositorySessionBeginDelegate deferredBeginDelegate(ExecutorService executor) {
+ return this;
+ }
+ });
+ } catch (InvalidSessionTransitionException e) {
+ session.abort();
+ synchronized (monitor) {
+ monitor.notify(e, true);
+ }
+ }
+ }
+
+ @Override
+ public void onSessionCreateFailed(Exception ex) {
+ synchronized (monitor) {
+ monitor.notify(ex, false);
+ }
+ }
+
+ @Override
+ public RepositorySessionCreationDelegate deferredCreationDelegate() {
+ return this;
+ }
+ }, context);
+ }
+ };
+
+ final Thread wiping = new Thread(doWipe);
+ synchronized (monitor) {
+ wiping.start();
+ try {
+ monitor.wait();
+ } catch (InterruptedException e) {
+ Logger.error(LOG_TAG, "Wipe interrupted.");
+ }
+ }
+
+ if (!monitor.sessionSucceeded) {
+ Logger.error(LOG_TAG, "Failed to create session for wipe.");
+ throw monitor.error;
+ }
+
+ if (!monitor.wipeSucceeded) {
+ Logger.error(LOG_TAG, "Failed to wipe session.");
+ throw monitor.error;
+ }
+
+ Logger.info(LOG_TAG, "Wiping stage complete.");
+ }
+
+ /**
+ * Asynchronously wipe collection on server.
+ */
+ protected void wipeServer(final AuthHeaderProvider authHeaderProvider, final WipeServerDelegate wipeDelegate) {
+ SyncStorageRequest request;
+
+ try {
+ request = new SyncStorageRequest(session.config.collectionURI(getCollection()));
+ } catch (URISyntaxException ex) {
+ Logger.warn(LOG_TAG, "Invalid URI in wipeServer.");
+ wipeDelegate.onWipeFailed(ex);
+ return;
+ }
+
+ request.delegate = new SyncStorageRequestDelegate() {
+
+ @Override
+ public String ifUnmodifiedSince() {
+ return null;
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse response) {
+ BaseResource.consumeEntity(response);
+ resetLocal();
+ wipeDelegate.onWiped(response.normalizedWeaveTimestamp());
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ Logger.warn(LOG_TAG, "Got request failure " + response.getStatusCode() + " in wipeServer.");
+ // Process HTTP failures here to pick up backoffs, etc.
+ session.interpretHTTPFailure(response.httpResponse());
+ BaseResource.consumeEntity(response); // The exception thrown should not need the body of the response.
+ wipeDelegate.onWipeFailed(new HTTPFailureException(response));
+ }
+
+ @Override
+ public void handleRequestError(Exception ex) {
+ Logger.warn(LOG_TAG, "Got exception in wipeServer.", ex);
+ wipeDelegate.onWipeFailed(ex);
+ }
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return authHeaderProvider;
+ }
+ };
+
+ request.delete();
+ }
+
+ /**
+ * Synchronously wipe the server.
+ * <p>
+ * Logs and re-throws an exception on failure.
+ */
+ public void wipeServer(final GlobalSession session) throws Exception {
+ this.session = session;
+
+ final WipeWaiter monitor = new WipeWaiter();
+
+ final Runnable doWipe = new Runnable() {
+ @Override
+ public void run() {
+ wipeServer(session.getAuthHeaderProvider(), new WipeServerDelegate() {
+ @Override
+ public void onWiped(long timestamp) {
+ synchronized (monitor) {
+ monitor.notify();
+ }
+ }
+
+ @Override
+ public void onWipeFailed(Exception e) {
+ synchronized (monitor) {
+ monitor.notify(e, false);
+ }
+ }
+ });
+ }
+ };
+
+ final Thread wiping = new Thread(doWipe);
+ synchronized (monitor) {
+ wiping.start();
+ try {
+ monitor.wait();
+ } catch (InterruptedException e) {
+ Logger.error(LOG_TAG, "Server wipe interrupted.");
+ }
+ }
+
+ if (!monitor.wipeSucceeded) {
+ Logger.error(LOG_TAG, "Failed to wipe server.");
+ throw monitor.error;
+ }
+
+ Logger.info(LOG_TAG, "Wiping server complete.");
+ }
+
+ @Override
+ public void execute() throws NoSuchStageException {
+ final String name = getEngineName();
+ Logger.debug(LOG_TAG, "Starting execute for " + name);
+
+ stageStartTimestamp = System.currentTimeMillis();
+
+ try {
+ if (!this.isEnabled()) {
+ Logger.info(LOG_TAG, "Skipping stage " + name + ".");
+ session.advance();
+ return;
+ }
+ } catch (MetaGlobalException.MetaGlobalMalformedSyncIDException e) {
+ // Bad engine syncID. This should never happen. Wipe the server.
+ try {
+ session.recordForMetaGlobalUpdate(name, new EngineSettings(Utils.generateGuid(), this.getStorageVersion()));
+ Logger.info(LOG_TAG, "Wiping server because malformed engine sync ID was found in meta/global.");
+ wipeServer(session);
+ Logger.info(LOG_TAG, "Wiped server after malformed engine sync ID found in meta/global.");
+ } catch (Exception ex) {
+ session.abort(ex, "Failed to wipe server after malformed engine sync ID found in meta/global.");
+ }
+ } catch (MetaGlobalException.MetaGlobalMalformedVersionException e) {
+ // Bad engine version. This should never happen. Wipe the server.
+ try {
+ session.recordForMetaGlobalUpdate(name, new EngineSettings(Utils.generateGuid(), this.getStorageVersion()));
+ Logger.info(LOG_TAG, "Wiping server because malformed engine version was found in meta/global.");
+ wipeServer(session);
+ Logger.info(LOG_TAG, "Wiped server after malformed engine version found in meta/global.");
+ } catch (Exception ex) {
+ session.abort(ex, "Failed to wipe server after malformed engine version found in meta/global.");
+ }
+ } catch (MetaGlobalException.MetaGlobalStaleClientSyncIDException e) {
+ // Our syncID is wrong. Reset client and take the server syncID.
+ Logger.warn(LOG_TAG, "Remote engine syncID different from local engine syncID:" +
+ " resetting local engine and assuming remote engine syncID.");
+ this.resetLocalWithSyncID(e.serverSyncID);
+ } catch (MetaGlobalException.MetaGlobalEngineStateChangedException e) {
+ boolean isEnabled = e.isEnabled;
+ if (!isEnabled) {
+ // Engine has been disabled; update meta/global with engine removal for upload.
+ session.removeEngineFromMetaGlobal(name);
+ session.config.declinedEngineNames.add(name);
+ } else {
+ session.config.declinedEngineNames.remove(name);
+ // Add engine with new syncID to meta/global for upload.
+ String newSyncID = Utils.generateGuid();
+ session.recordForMetaGlobalUpdate(name, new EngineSettings(newSyncID, this.getStorageVersion()));
+ // Update SynchronizerConfiguration w/ new engine syncID.
+ this.resetLocalWithSyncID(newSyncID);
+ }
+ try {
+ // Engine sync status has changed. Wipe server.
+ Logger.warn(LOG_TAG, "Wiping server because engine sync state changed.");
+ wipeServer(session);
+ Logger.warn(LOG_TAG, "Wiped server because engine sync state changed.");
+ } catch (Exception ex) {
+ session.abort(ex, "Failed to wipe server after engine sync state changed");
+ }
+ if (!isEnabled) {
+ Logger.warn(LOG_TAG, "Stage has been disabled. Advancing to next stage.");
+ session.advance();
+ return;
+ }
+ } catch (MetaGlobalException e) {
+ session.abort(e, "Inappropriate meta/global; refusing to execute " + name + " stage.");
+ return;
+ }
+
+ Synchronizer synchronizer;
+ try {
+ synchronizer = this.getConfiguredSynchronizer(session);
+ } catch (NoCollectionKeysSetException e) {
+ session.abort(e, "No CollectionKeys.");
+ return;
+ } catch (URISyntaxException e) {
+ session.abort(e, "Invalid URI syntax for server repository.");
+ return;
+ } catch (NonObjectJSONException | IOException e) {
+ session.abort(e, "Invalid persisted JSON for config.");
+ return;
+ }
+
+ Logger.debug(LOG_TAG, "Invoking synchronizer.");
+ synchronizer.synchronize(session.getContext(), this);
+ Logger.debug(LOG_TAG, "Reached end of execute.");
+ }
+
+ /**
+ * Express the duration taken by this stage as a String, like "0.56 seconds".
+ *
+ * @return formatted string.
+ */
+ protected String getStageDurationString() {
+ return Utils.formatDuration(stageStartTimestamp, stageCompleteTimestamp);
+ }
+
+ /**
+ * We synced this engine! Persist timestamps and advance the session.
+ *
+ * @param synchronizer the <code>Synchronizer</code> that succeeded.
+ */
+ @Override
+ public void onSynchronized(Synchronizer synchronizer) {
+ stageCompleteTimestamp = System.currentTimeMillis();
+ Logger.debug(LOG_TAG, "onSynchronized.");
+
+ SynchronizerConfiguration newConfig = synchronizer.save();
+ if (newConfig != null) {
+ persistConfig(newConfig);
+ } else {
+ Logger.warn(LOG_TAG, "Didn't get configuration from synchronizer after success.");
+ }
+
+ final SynchronizerSession synchronizerSession = synchronizer.getSynchronizerSession();
+ int inboundCount = synchronizerSession.getInboundCount();
+ int outboundCount = synchronizerSession.getOutboundCount();
+ Logger.info(LOG_TAG, "Stage " + getEngineName() +
+ " received " + inboundCount + " and sent " + outboundCount +
+ " records in " + getStageDurationString() + ".");
+ Logger.info(LOG_TAG, "Advancing session.");
+ session.advance();
+ }
+
+ /**
+ * We failed to sync this engine! Do not persist timestamps (which means that
+ * the next sync will include this sync's data), but do advance the session
+ * (if we didn't get a Retry-After header).
+ *
+ * @param synchronizer the <code>Synchronizer</code> that failed.
+ */
+ @Override
+ public void onSynchronizeFailed(Synchronizer synchronizer,
+ Exception lastException, String reason) {
+ stageCompleteTimestamp = System.currentTimeMillis();
+ Logger.warn(LOG_TAG, "Synchronize failed: " + reason, lastException);
+
+ // This failure could be due to a 503 or a 401 and it could have headers.
+ // Interrogate the headers but only abort the global session if Retry-After header is set.
+ if (lastException instanceof HTTPFailureException) {
+ SyncStorageResponse response = ((HTTPFailureException)lastException).response;
+ if (response.retryAfterInSeconds() > 0) {
+ session.handleHTTPError(response, reason); // Calls session.abort().
+ return;
+ } else {
+ session.interpretHTTPFailure(response.httpResponse()); // Does not call session.abort().
+ }
+ }
+
+ Logger.info(LOG_TAG, "Advancing session even though stage failed (took " + getStageDurationString() +
+ "). Timestamps not persisted.");
+ session.advance();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SyncClientsEngineStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SyncClientsEngineStage.java
new file mode 100644
index 0000000000..04d3e7ce29
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SyncClientsEngineStage.java
@@ -0,0 +1,691 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.stage;
+
+import android.accounts.Account;
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.FxAccountClient;
+import org.mozilla.gecko.background.fxa.FxAccountClient20;
+import org.mozilla.gecko.background.fxa.FxAccountClientException;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.sync.CommandProcessor;
+import org.mozilla.gecko.sync.CommandProcessor.Command;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.HTTPFailureException;
+import org.mozilla.gecko.sync.NoCollectionKeysSetException;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.SyncStorageCollectionRequest;
+import org.mozilla.gecko.sync.net.SyncStorageRecordRequest;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.sync.net.WBOCollectionRequestDelegate;
+import org.mozilla.gecko.sync.net.WBORequestDelegate;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
+import org.mozilla.gecko.sync.repositories.android.RepoUtils;
+import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
+import org.mozilla.gecko.sync.repositories.domain.ClientRecordFactory;
+import org.mozilla.gecko.sync.repositories.domain.VersionConstants;
+
+import ch.boye.httpclientandroidlib.HttpStatus;
+
+public class SyncClientsEngineStage extends AbstractSessionManagingSyncStage {
+ private static final String LOG_TAG = "SyncClientsEngineStage";
+
+ public static final String COLLECTION_NAME = "clients";
+ public static final String STAGE_NAME = COLLECTION_NAME;
+ public static final int CLIENTS_TTL_REFRESH = 604800000; // 7 days in milliseconds.
+ public static final int MAX_UPLOAD_FAILURE_COUNT = 5;
+ public static final long NOTIFY_TAB_SENT_TTL_SECS = TimeUnit.SECONDS.convert(1L, TimeUnit.HOURS); // 1 hour
+
+ protected final ClientRecordFactory factory = new ClientRecordFactory();
+ protected ClientUploadDelegate clientUploadDelegate;
+ protected ClientDownloadDelegate clientDownloadDelegate;
+
+ // Be sure to use this safely via getClientsDatabaseAccessor/closeDataAccessor.
+ protected ClientsDatabaseAccessor db;
+
+ protected volatile boolean shouldWipe;
+ protected volatile boolean shouldUploadLocalRecord; // Set if, e.g., we received commands or need to refresh our version.
+ protected final AtomicInteger uploadAttemptsCount = new AtomicInteger();
+ protected final List<ClientRecord> modifiedClientsToUpload = new ArrayList<ClientRecord>();
+
+ protected int getClientsCount() {
+ return getClientsDatabaseAccessor().clientsCount();
+ }
+
+ protected synchronized ClientsDatabaseAccessor getClientsDatabaseAccessor() {
+ if (db == null) {
+ db = new ClientsDatabaseAccessor(session.getContext());
+ }
+ return db;
+ }
+
+ protected synchronized void closeDataAccessor() {
+ if (db == null) {
+ return;
+ }
+ db.close();
+ db = null;
+ }
+
+ /**
+ * The following two delegates, ClientDownloadDelegate and ClientUploadDelegate
+ * are both triggered in a chain, starting when execute() calls
+ * downloadClientRecords().
+ *
+ * Client records are downloaded using a get() request. Upon success of the
+ * get() request, the local client record is uploaded.
+ *
+ * @author Marina Samuel
+ *
+ */
+ public class ClientDownloadDelegate extends WBOCollectionRequestDelegate {
+
+ // We use this on each WBO, so lift it out.
+ final ClientsDataDelegate clientsDelegate = session.getClientsDelegate();
+ boolean localAccountGUIDDownloaded = false;
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return session.getAuthHeaderProvider();
+ }
+
+ @Override
+ public String ifUnmodifiedSince() {
+ // TODO last client download time?
+ return null;
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse response) {
+
+ // Hang onto the server's last modified timestamp to use
+ // in X-If-Unmodified-Since for upload.
+ session.config.persistServerClientsTimestamp(response.normalizedWeaveTimestamp());
+ BaseResource.consumeEntity(response);
+
+ // Wipe the clients table if it still hasn't been wiped but needs to be.
+ wipeAndStore(null);
+
+ // If we successfully downloaded all records but ours was not one of them
+ // then reset the timestamp.
+ if (!localAccountGUIDDownloaded) {
+ Logger.info(LOG_TAG, "Local client GUID does not exist on the server. Upload timestamp will be reset.");
+ session.config.persistServerClientRecordTimestamp(0);
+ }
+ localAccountGUIDDownloaded = false;
+
+ final int clientsCount;
+ try {
+ clientsCount = getClientsCount();
+ } finally {
+ // Close the database to clear cached readableDatabase/writableDatabase
+ // after we've completed our last transaction (db.store()).
+ closeDataAccessor();
+ }
+
+ Logger.debug(LOG_TAG, "Database contains " + clientsCount + " clients.");
+ Logger.debug(LOG_TAG, "Server response asserts " + response.weaveRecords() + " records.");
+
+ // TODO: persist the response timestamp to know whether to download next time (Bug 726055).
+ clientUploadDelegate = new ClientUploadDelegate();
+ clientsDelegate.setClientsCount(clientsCount);
+
+ // If we upload remote records, checkAndUpload() will be called upon
+ // upload success in the delegate. Otherwise call checkAndUpload() now.
+ if (modifiedClientsToUpload.size() > 0) {
+ // modifiedClientsToUpload is cleared in uploadRemoteRecords, save what we need here
+ final List<String> devicesToNotify = new ArrayList<>();
+ for (ClientRecord record : modifiedClientsToUpload) {
+ if (!TextUtils.isEmpty(record.fxaDeviceId)) {
+ devicesToNotify.add(record.fxaDeviceId);
+ }
+ }
+
+ // This method is synchronous, there's no risk of notifying the clients
+ // before we actually uploaded the records
+ uploadRemoteRecords();
+
+ // Notify the clients who got their record written
+ notifyClients(devicesToNotify);
+
+ return;
+ }
+ checkAndUpload();
+ }
+
+ private void notifyClients(final List<String> devicesToNotify) {
+ final ExecutorService executor = Executors.newSingleThreadExecutor();
+ final Context context = session.getContext();
+ final Account account = FirefoxAccounts.getFirefoxAccount(context);
+ if (account == null) {
+ Log.e(LOG_TAG, "Can't notify other clients: no account");
+ return;
+ }
+ final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
+ final ExtendedJSONObject payload = createNotifyDevicesPayload();
+
+ final byte[] sessionToken;
+ try {
+ sessionToken = fxAccount.getSessionToken();
+ } catch (AndroidFxAccount.InvalidFxAState invalidFxAState) {
+ Log.e(LOG_TAG, "Could not get session token", invalidFxAState);
+ return;
+ }
+
+ // API doc : https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#post-v1accountdevicesnotify
+ final FxAccountClient fxAccountClient = new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
+ fxAccountClient.notifyDevices(sessionToken, devicesToNotify, payload, NOTIFY_TAB_SENT_TTL_SECS, new FxAccountClient20.RequestDelegate<ExtendedJSONObject>() {
+ @Override
+ public void handleError(Exception e) {
+ Log.e(LOG_TAG, "Error while notifying devices", e);
+ }
+
+ @Override
+ public void handleFailure(FxAccountClientException.FxAccountClientRemoteException e) {
+ Log.e(LOG_TAG, "Error while notifying devices", e);
+ }
+
+ @Override
+ public void handleSuccess(ExtendedJSONObject result) {
+ Log.i(LOG_TAG, devicesToNotify.size() + " devices notified");
+ }
+ });
+ }
+
+ @NonNull
+ @SuppressWarnings("unchecked")
+ private ExtendedJSONObject createNotifyDevicesPayload() {
+ final ExtendedJSONObject payload = new ExtendedJSONObject();
+ payload.put("version", 1);
+ payload.put("command", "sync:collection_changed");
+ final ExtendedJSONObject data = new ExtendedJSONObject();
+ final JSONArray collections = new JSONArray();
+ collections.add("clients");
+ data.put("collections", collections);
+ payload.put("data", data);
+ return payload;
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ BaseResource.consumeEntity(response); // We don't need the response at all, and any exception handling shouldn't need the response body.
+ localAccountGUIDDownloaded = false;
+
+ try {
+ Logger.info(LOG_TAG, "Client upload failed. Aborting sync.");
+ session.abort(new HTTPFailureException(response), "Client download failed.");
+ } finally {
+ // Close the database upon failure.
+ closeDataAccessor();
+ }
+ }
+
+ @Override
+ public void handleRequestError(Exception ex) {
+ localAccountGUIDDownloaded = false;
+ try {
+ Logger.info(LOG_TAG, "Client upload error. Aborting sync.");
+ session.abort(ex, "Failure fetching client record.");
+ } finally {
+ // Close the database upon error.
+ closeDataAccessor();
+ }
+ }
+
+ @Override
+ public void handleWBO(CryptoRecord record) {
+ ClientRecord r;
+ try {
+ r = (ClientRecord) factory.createRecord(record.decrypt());
+ if (clientsDelegate.isLocalGUID(r.guid)) {
+ Logger.info(LOG_TAG, "Local client GUID exists on server and was downloaded.");
+ localAccountGUIDDownloaded = true;
+ handleDownloadedLocalRecord(r);
+ } else {
+ // Only need to store record if it isn't our local one.
+ wipeAndStore(r);
+ addCommands(r);
+ }
+ RepoUtils.logClient(r);
+ } catch (Exception e) {
+ session.abort(e, "Exception handling client WBO.");
+ return;
+ }
+ }
+
+ @Override
+ public KeyBundle keyBundle() {
+ try {
+ return session.keyBundleForCollection(COLLECTION_NAME);
+ } catch (NoCollectionKeysSetException e) {
+ return null;
+ }
+ }
+ }
+
+ public class ClientUploadDelegate extends WBORequestDelegate {
+ protected static final String LOG_TAG = "ClientUploadDelegate";
+ public Long currentlyUploadingRecordTimestamp;
+ public boolean currentlyUploadingLocalRecord;
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return session.getAuthHeaderProvider();
+ }
+
+ private void setUploadDetails(boolean isLocalRecord) {
+ // Use the timestamp for the whole collection per Sync storage 1.1 spec.
+ currentlyUploadingRecordTimestamp = session.config.getPersistedServerClientsTimestamp();
+ currentlyUploadingLocalRecord = isLocalRecord;
+ }
+
+ @Override
+ public String ifUnmodifiedSince() {
+ Long timestampInMilliseconds = currentlyUploadingRecordTimestamp;
+
+ // It's the first upload so we don't care about X-If-Unmodified-Since.
+ if (timestampInMilliseconds <= 0) {
+ return null;
+ }
+
+ return Utils.millisecondsToDecimalSecondsString(timestampInMilliseconds);
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse response) {
+ Logger.debug(LOG_TAG, "Upload succeeded.");
+ uploadAttemptsCount.set(0);
+
+ // X-Weave-Timestamp is the modified time of uploaded records.
+ // Always persist this.
+ final long responseTimestamp = response.normalizedWeaveTimestamp();
+ Logger.trace(LOG_TAG, "Timestamp from header is: " + responseTimestamp);
+
+ if (responseTimestamp == -1) {
+ final String message = "Response did not contain a valid timestamp.";
+ session.abort(new RuntimeException(message), message);
+ return;
+ }
+
+ BaseResource.consumeEntity(response);
+ session.config.persistServerClientsTimestamp(responseTimestamp);
+
+ // If we're not uploading our record, we're done here; just
+ // clean up and finish.
+ if (!currentlyUploadingLocalRecord) {
+ // TODO: check failed uploads in body.
+ clearRecordsToUpload();
+ checkAndUpload();
+ return;
+ }
+
+ // If we're processing our record, we have a little more cleanup
+ // to do.
+ shouldUploadLocalRecord = false;
+ session.config.persistServerClientRecordTimestamp(responseTimestamp);
+ session.advance();
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ int statusCode = response.getStatusCode();
+
+ // If upload failed because of `ifUnmodifiedSince` then there are new
+ // commands uploaded to our record. We must download and process them first.
+ if (!shouldUploadLocalRecord ||
+ statusCode == HttpStatus.SC_PRECONDITION_FAILED ||
+ uploadAttemptsCount.incrementAndGet() > MAX_UPLOAD_FAILURE_COUNT) {
+
+ Logger.debug(LOG_TAG, "Client upload failed. Aborting sync.");
+ if (!currentlyUploadingLocalRecord) {
+ modifiedClientsToUpload.clear(); // These will be redownloaded.
+ }
+ BaseResource.consumeEntity(response); // The exception thrown should need the response body.
+ session.abort(new HTTPFailureException(response), "Client upload failed.");
+ return;
+ }
+ Logger.trace(LOG_TAG, "Retrying upload…");
+ // Preconditions:
+ // shouldUploadLocalRecord == true &&
+ // statusCode != 412 &&
+ // uploadAttemptCount < MAX_UPLOAD_FAILURE_COUNT
+ checkAndUpload();
+ }
+
+ @Override
+ public void handleRequestError(Exception ex) {
+ Logger.info(LOG_TAG, "Client upload error. Aborting sync.");
+ session.abort(ex, "Client upload failed.");
+ }
+
+ @Override
+ public KeyBundle keyBundle() {
+ try {
+ return session.keyBundleForCollection(COLLECTION_NAME);
+ } catch (NoCollectionKeysSetException e) {
+ return null;
+ }
+ }
+ }
+
+ @Override
+ public void execute() throws NoSuchStageException {
+ // We can be disabled just for this sync.
+ boolean enabledThisSync = session.isEngineLocallyEnabled(STAGE_NAME);
+ if (!enabledThisSync) {
+ // These log messages look best when they match the messages in ServerSyncStage.
+ Logger.debug(LOG_TAG, "Stage " + STAGE_NAME + " disabled just for this sync.");
+ Logger.info(LOG_TAG, "Skipping stage " + STAGE_NAME + ".");
+ session.advance();
+ return;
+ }
+
+ if (shouldDownload()) {
+ downloadClientRecords(); // Will kick off upload, too…
+ } else {
+ // Upload if necessary.
+ }
+ }
+
+ @Override
+ protected void resetLocal() {
+ // Clear timestamps and local data.
+ session.config.persistServerClientRecordTimestamp(0L); // TODO: roll these into one.
+ session.config.persistServerClientsTimestamp(0L);
+
+ session.getClientsDelegate().setClientsCount(0);
+ try {
+ getClientsDatabaseAccessor().wipeDB();
+ } finally {
+ closeDataAccessor();
+ }
+ }
+
+ @Override
+ protected void wipeLocal() throws Exception {
+ // Nothing more to do.
+ this.resetLocal();
+ }
+
+ @Override
+ public Integer getStorageVersion() {
+ return VersionConstants.CLIENTS_ENGINE_VERSION;
+ }
+
+ protected String getLocalClientVersion() {
+ return AppConstants.MOZ_APP_VERSION;
+ }
+
+ @SuppressWarnings("unchecked")
+ protected JSONArray getLocalClientProtocols() {
+ final JSONArray protocols = new JSONArray();
+ protocols.add(ClientRecord.PROTOCOL_LEGACY_SYNC);
+ protocols.add(ClientRecord.PROTOCOL_FXA_SYNC);
+ return protocols;
+ }
+
+ protected ClientRecord newLocalClientRecord(ClientsDataDelegate delegate) {
+ final String ourGUID = delegate.getAccountGUID();
+ final String ourName = delegate.getClientName();
+
+ ClientRecord r = new ClientRecord(ourGUID);
+ r.name = ourName;
+ r.version = getLocalClientVersion();
+ r.protocols = getLocalClientProtocols();
+
+ r.os = "Android";
+ r.application = AppConstants.MOZ_APP_DISPLAYNAME;
+ r.appPackage = AppConstants.ANDROID_PACKAGE_NAME;
+ r.device = android.os.Build.MODEL;
+ r.formfactor = delegate.getFormFactor();
+
+ Context context = session.getContext();
+ final Account account = FirefoxAccounts.getFirefoxAccount(context);
+ if (account != null) {
+ final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
+ final String deviceId = fxAccount.getDeviceId();
+ if (!TextUtils.isEmpty(deviceId)) {
+ r.fxaDeviceId = deviceId;
+ }
+ }
+
+ return r;
+ }
+
+ // TODO: Bug 726055 - More considered handling of when to sync.
+ protected boolean shouldDownload() {
+ // Ask info/collections whether a download is needed.
+ return true;
+ }
+
+ protected boolean shouldUpload() {
+ if (shouldUploadLocalRecord) {
+ return true;
+ }
+
+ long lastUpload = session.config.getPersistedServerClientRecordTimestamp(); // Defaults to 0.
+ if (lastUpload == 0) {
+ return true;
+ }
+
+ if (session.getClientsDelegate().getLastModifiedTimestamp() > lastUpload) {
+ // Something's changed locally since we last uploaded.
+ return true;
+ }
+
+ // Note the opportunity for clock drift problems here.
+ // TODO: if we track download times, we can use the timestamp of most
+ // recent download response instead of the current time.
+ long now = System.currentTimeMillis();
+ long age = now - lastUpload;
+ return age >= CLIENTS_TTL_REFRESH;
+ }
+
+ protected void handleDownloadedLocalRecord(ClientRecord r) {
+ session.config.persistServerClientRecordTimestamp(r.lastModified);
+
+ if (!getLocalClientVersion().equals(r.version) ||
+ !getLocalClientProtocols().equals(r.protocols)) {
+ shouldUploadLocalRecord = true;
+ }
+ processCommands(r.commands);
+ }
+
+ protected void processCommands(JSONArray commands) {
+ if (commands == null ||
+ commands.size() == 0) {
+ return;
+ }
+
+ shouldUploadLocalRecord = true;
+ CommandProcessor processor = CommandProcessor.getProcessor();
+
+ for (Object o : commands) {
+ processor.processCommand(session, new ExtendedJSONObject((JSONObject) o));
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ protected void addCommands(ClientRecord record) throws NullCursorException {
+ Logger.trace(LOG_TAG, "Adding commands to " + record.guid);
+ List<Command> commands = db.fetchCommandsForClient(record.guid);
+
+ if (commands == null || commands.size() == 0) {
+ Logger.trace(LOG_TAG, "No commands to add.");
+ return;
+ }
+
+ for (Command command : commands) {
+ JSONObject jsonCommand = command.asJSONObject();
+ if (record.commands == null) {
+ record.commands = new JSONArray();
+ }
+ record.commands.add(jsonCommand);
+ }
+ modifiedClientsToUpload.add(record);
+ }
+
+ @SuppressWarnings("unchecked")
+ protected void uploadRemoteRecords() {
+ Logger.trace(LOG_TAG, "In uploadRemoteRecords. Uploading " + modifiedClientsToUpload.size() + " records" );
+
+ for (ClientRecord r : modifiedClientsToUpload) {
+ Logger.trace(LOG_TAG, ">> Uploading record " + r.guid + ": " + r.name);
+ }
+
+ if (modifiedClientsToUpload.size() == 1) {
+ ClientRecord record = modifiedClientsToUpload.get(0);
+ Logger.debug(LOG_TAG, "Only 1 remote record to upload.");
+ Logger.debug(LOG_TAG, "Record last modified: " + record.lastModified);
+ CryptoRecord cryptoRecord = encryptClientRecord(record);
+ if (cryptoRecord != null) {
+ clientUploadDelegate.setUploadDetails(false);
+ this.uploadClientRecord(cryptoRecord);
+ }
+ return;
+ }
+
+ JSONArray cryptoRecords = new JSONArray();
+ for (ClientRecord record : modifiedClientsToUpload) {
+ Logger.trace(LOG_TAG, "Record " + record.guid + " is being uploaded" );
+
+ CryptoRecord cryptoRecord = encryptClientRecord(record);
+ cryptoRecords.add(cryptoRecord.toJSONObject());
+ }
+ Logger.debug(LOG_TAG, "Uploading records: " + cryptoRecords.size());
+ clientUploadDelegate.setUploadDetails(false);
+ this.uploadClientRecords(cryptoRecords);
+ }
+
+ protected void checkAndUpload() {
+ if (!shouldUpload()) {
+ Logger.debug(LOG_TAG, "Not uploading client record.");
+ session.advance();
+ return;
+ }
+
+ final ClientRecord localClient = newLocalClientRecord(session.getClientsDelegate());
+ clientUploadDelegate.setUploadDetails(true);
+ CryptoRecord cryptoRecord = encryptClientRecord(localClient);
+ if (cryptoRecord != null) {
+ this.uploadClientRecord(cryptoRecord);
+ }
+ }
+
+ protected CryptoRecord encryptClientRecord(ClientRecord recordToUpload) {
+ // Generate CryptoRecord from ClientRecord to upload.
+ final String encryptionFailure = "Couldn't encrypt new client record.";
+
+ try {
+ CryptoRecord cryptoRecord = recordToUpload.getEnvelope();
+ cryptoRecord.keyBundle = clientUploadDelegate.keyBundle();
+ if (cryptoRecord.keyBundle == null) {
+ session.abort(new NoCollectionKeysSetException(), "No collection keys set.");
+ return null;
+ }
+ return cryptoRecord.encrypt();
+ } catch (UnsupportedEncodingException e) {
+ session.abort(e, encryptionFailure + " Unsupported encoding.");
+ } catch (CryptoException e) {
+ session.abort(e, encryptionFailure);
+ }
+ return null;
+ }
+
+ public void clearRecordsToUpload() {
+ try {
+ getClientsDatabaseAccessor().wipeCommandsTable();
+ modifiedClientsToUpload.clear();
+ } finally {
+ closeDataAccessor();
+ }
+ }
+
+ protected void downloadClientRecords() {
+ shouldWipe = true;
+ clientDownloadDelegate = makeClientDownloadDelegate();
+
+ try {
+ final URI getURI = session.config.collectionURI(COLLECTION_NAME, true);
+ final SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(getURI);
+ request.delegate = clientDownloadDelegate;
+
+ Logger.trace(LOG_TAG, "Downloading client records.");
+ request.get();
+ } catch (URISyntaxException e) {
+ session.abort(e, "Invalid URI.");
+ }
+ }
+
+ protected void uploadClientRecords(JSONArray records) {
+ Logger.trace(LOG_TAG, "Uploading " + records.size() + " client records.");
+ try {
+ final URI postURI = session.config.collectionURI(COLLECTION_NAME, false);
+ final SyncStorageRecordRequest request = new SyncStorageRecordRequest(postURI);
+ request.delegate = clientUploadDelegate;
+ request.post(records);
+ } catch (URISyntaxException e) {
+ session.abort(e, "Invalid URI.");
+ } catch (Exception e) {
+ session.abort(e, "Unable to parse body.");
+ }
+ }
+
+ /**
+ * Upload a client record via HTTP POST to the parent collection.
+ */
+ protected void uploadClientRecord(CryptoRecord record) {
+ Logger.debug(LOG_TAG, "Uploading client record " + record.guid);
+ try {
+ final URI postURI = session.config.collectionURI(COLLECTION_NAME);
+ final SyncStorageRecordRequest request = new SyncStorageRecordRequest(postURI);
+ request.delegate = clientUploadDelegate;
+ request.post(record);
+ } catch (URISyntaxException e) {
+ session.abort(e, "Invalid URI.");
+ }
+ }
+
+ protected ClientDownloadDelegate makeClientDownloadDelegate() {
+ return new ClientDownloadDelegate();
+ }
+
+ protected void wipeAndStore(ClientRecord record) {
+ final ClientsDatabaseAccessor db = getClientsDatabaseAccessor();
+ if (shouldWipe) {
+ db.wipeClientsTable();
+ shouldWipe = false;
+ }
+ if (record != null) {
+ db.store(record);
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/UploadMetaGlobalStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/UploadMetaGlobalStage.java
new file mode 100644
index 0000000000..77846c212d
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/UploadMetaGlobalStage.java
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.stage;
+
+
+public class UploadMetaGlobalStage extends AbstractNonRepositorySyncStage {
+ public static final String LOG_TAG = "UploadMGStage";
+
+ @Override
+ public void execute() throws NoSuchStageException {
+ if (session.hasUpdatedMetaGlobal()) {
+ session.uploadUpdatedMetaGlobal();
+ }
+ session.advance();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ConcurrentRecordConsumer.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ConcurrentRecordConsumer.java
new file mode 100644
index 0000000000..9b1ef3e85f
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ConcurrentRecordConsumer.java
@@ -0,0 +1,122 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.synchronizer;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+/**
+ * Consume records from a queue inside a RecordsChannel, as fast as we can.
+ * TODO: rewrite this in terms of an ExecutorService and a CompletionService.
+ * See Bug 713483.
+ *
+ * @author rnewman
+ *
+ */
+class ConcurrentRecordConsumer extends RecordConsumer {
+ private static final String LOG_TAG = "CRecordConsumer";
+
+ /**
+ * When this is true and all records have been processed, the consumer
+ * will notify its delegate.
+ */
+ protected boolean allRecordsQueued = false;
+ private long counter = 0;
+
+ public ConcurrentRecordConsumer(RecordsConsumerDelegate delegate) {
+ this.delegate = delegate;
+ }
+
+ private final Object monitor = new Object();
+ @Override
+ public void doNotify() {
+ synchronized (monitor) {
+ monitor.notify();
+ }
+ }
+
+ @Override
+ public void queueFilled() {
+ Logger.debug(LOG_TAG, "Queue filled.");
+ synchronized (monitor) {
+ this.allRecordsQueued = true;
+ monitor.notify();
+ }
+ }
+
+ @Override
+ public void halt() {
+ synchronized (monitor) {
+ this.stopImmediately = true;
+ monitor.notify();
+ }
+ }
+
+ private final Object countMonitor = new Object();
+ @Override
+ public void stored() {
+ Logger.trace(LOG_TAG, "Record stored. Notifying.");
+ synchronized (countMonitor) {
+ counter++;
+ }
+ }
+
+ private void consumerIsDone() {
+ Logger.debug(LOG_TAG, "Consumer is done. Processed " + counter + ((counter == 1) ? " record." : " records."));
+ delegate.consumerIsDone(!allRecordsQueued);
+ }
+
+ @Override
+ public void run() {
+ Record record;
+
+ while (true) {
+ // The queue is concurrent-safe.
+ while ((record = delegate.getQueue().poll()) != null) {
+ synchronized (monitor) {
+ Logger.trace(LOG_TAG, "run() took monitor.");
+ if (stopImmediately) {
+ Logger.debug(LOG_TAG, "Stopping immediately. Clearing queue.");
+ delegate.getQueue().clear();
+ Logger.debug(LOG_TAG, "Notifying consumer.");
+ consumerIsDone();
+ return;
+ }
+ Logger.debug(LOG_TAG, "run() dropped monitor.");
+ }
+
+ Logger.trace(LOG_TAG, "Storing record with guid " + record.guid + ".");
+ try {
+ delegate.store(record);
+ } catch (Exception e) {
+ // TODO: Bug 709371: track records that failed to apply.
+ Logger.error(LOG_TAG, "Caught error in store.", e);
+ }
+ Logger.trace(LOG_TAG, "Done with record.");
+ }
+ synchronized (monitor) {
+ Logger.trace(LOG_TAG, "run() took monitor.");
+
+ if (allRecordsQueued) {
+ Logger.debug(LOG_TAG, "Done with records and no more to come. Notifying consumerIsDone.");
+ consumerIsDone();
+ return;
+ }
+ if (stopImmediately) {
+ Logger.debug(LOG_TAG, "Done with records and told to stop immediately. Notifying consumerIsDone.");
+ consumerIsDone();
+ return;
+ }
+ try {
+ Logger.debug(LOG_TAG, "Not told to stop but no records. Waiting.");
+ monitor.wait(10000);
+ } catch (InterruptedException e) {
+ // TODO
+ }
+ Logger.trace(LOG_TAG, "run() dropped monitor.");
+ }
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordConsumer.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordConsumer.java
new file mode 100644
index 0000000000..35e57d9c2a
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordConsumer.java
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.synchronizer;
+
+public abstract class RecordConsumer implements Runnable {
+
+ public abstract void stored();
+
+ /**
+ * There are no more store items to arrive at the delegate.
+ * When you're done, take care of finishing up.
+ */
+ public abstract void queueFilled();
+ public abstract void halt();
+
+ public abstract void doNotify();
+
+ protected boolean stopImmediately = false;
+ protected RecordsConsumerDelegate delegate;
+
+ public RecordConsumer() {
+ super();
+ }
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsChannel.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsChannel.java
new file mode 100644
index 0000000000..f929cdc757
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsChannel.java
@@ -0,0 +1,292 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.synchronizer;
+
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.ThreadPool;
+import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
+import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.DeferredRepositorySessionBeginDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.DeferredRepositorySessionStoreDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+/**
+ * Pulls records from `source`, applying them to `sink`.
+ * Notifies its delegate of errors and completion.
+ *
+ * All stores (initiated by a fetch) must have been completed before storeDone
+ * is invoked on the sink. This is to avoid the existing stored items being
+ * considered as the total set, with onStoreCompleted being called when they're
+ * done:
+ *
+ * store(A) store(B)
+ * store(C) storeDone()
+ * store(A) finishes. Store job begins.
+ * store(C) finishes. Store job begins.
+ * storeDone() finishes.
+ * Storing of A complete.
+ * Storing of C complete.
+ * We're done! Call onStoreCompleted.
+ * store(B) finishes... uh oh.
+ *
+ * In other words, storeDone must be gated on the synchronous invocation of every store.
+ *
+ * Similarly, we require that every store callback have returned before onStoreCompleted is invoked.
+ *
+ * This whole set of guarantees should be achievable thusly:
+ *
+ * * The fetch process must run in a single thread, and invoke store()
+ * synchronously. After processing every incoming record, storeDone is called,
+ * setting a flag.
+ * If the fetch cannot be implicitly queued, it must be explicitly queued.
+ * In this implementation, we assume that fetch callbacks are strictly ordered in this way.
+ *
+ * * The store process must be (implicitly or explicitly) queued. When the
+ * queue empties, the consumer checks the storeDone flag. If it's set, and the
+ * queue is exhausted, invoke onStoreCompleted.
+ *
+ * RecordsChannel exists to enforce this ordering of operations.
+ *
+ * @author rnewman
+ *
+ */
+public class RecordsChannel implements
+ RepositorySessionFetchRecordsDelegate,
+ RepositorySessionStoreDelegate,
+ RecordsConsumerDelegate,
+ RepositorySessionBeginDelegate {
+
+ private static final String LOG_TAG = "RecordsChannel";
+ public RepositorySession source;
+ public RepositorySession sink;
+ private final RecordsChannelDelegate delegate;
+ private long fetchEnd = -1;
+
+ protected final AtomicInteger numFetched = new AtomicInteger();
+ protected final AtomicInteger numFetchFailed = new AtomicInteger();
+ protected final AtomicInteger numStored = new AtomicInteger();
+ protected final AtomicInteger numStoreFailed = new AtomicInteger();
+
+ public RecordsChannel(RepositorySession source, RepositorySession sink, RecordsChannelDelegate delegate) {
+ this.source = source;
+ this.sink = sink;
+ this.delegate = delegate;
+ }
+
+ /*
+ * We push fetched records into a queue.
+ * A separate thread is waiting for us to notify it of work to do.
+ * When we tell it to stop, it'll stop. We do that when the fetch
+ * is completed.
+ * When it stops, we tell the sink that there are no more records,
+ * and wait for the sink to tell us that storing is done.
+ * Then we notify our delegate of completion.
+ */
+ private RecordConsumer consumer;
+ private boolean waitingForQueueDone = false;
+ private final ConcurrentLinkedQueue<Record> toProcess = new ConcurrentLinkedQueue<Record>();
+
+ @Override
+ public ConcurrentLinkedQueue<Record> getQueue() {
+ return toProcess;
+ }
+
+ protected boolean isReady() {
+ return source.isActive() && sink.isActive();
+ }
+
+ /**
+ * Get the number of records fetched so far.
+ *
+ * @return number of fetches.
+ */
+ public int getFetchCount() {
+ return numFetched.get();
+ }
+
+ /**
+ * Get the number of fetch failures recorded so far.
+ *
+ * @return number of fetch failures.
+ */
+ public int getFetchFailureCount() {
+ return numFetchFailed.get();
+ }
+
+ /**
+ * Get the number of store attempts (successful or not) so far.
+ *
+ * @return number of stores attempted.
+ */
+ public int getStoreCount() {
+ return numStored.get();
+ }
+
+ /**
+ * Get the number of store failures recorded so far.
+ *
+ * @return number of store failures.
+ */
+ public int getStoreFailureCount() {
+ return numStoreFailed.get();
+ }
+
+ /**
+ * Start records flowing through the channel.
+ */
+ public void flow() {
+ if (!isReady()) {
+ RepositorySession failed = source;
+ if (source.isActive()) {
+ failed = sink;
+ }
+ this.delegate.onFlowBeginFailed(this, new SessionNotBegunException(failed));
+ return;
+ }
+
+ if (!source.dataAvailable()) {
+ Logger.info(LOG_TAG, "No data available: short-circuiting flow from source " + source);
+ long now = System.currentTimeMillis();
+ this.delegate.onFlowCompleted(this, now, now);
+ return;
+ }
+
+ sink.setStoreDelegate(this);
+ numFetched.set(0);
+ numFetchFailed.set(0);
+ numStored.set(0);
+ numStoreFailed.set(0);
+ // Start a consumer thread.
+ this.consumer = new ConcurrentRecordConsumer(this);
+ ThreadPool.run(this.consumer);
+ waitingForQueueDone = true;
+ source.fetchSince(source.getLastSyncTimestamp(), this);
+ }
+
+ /**
+ * Begin both sessions, invoking flow() when done.
+ * @throws InvalidSessionTransitionException
+ */
+ public void beginAndFlow() throws InvalidSessionTransitionException {
+ Logger.trace(LOG_TAG, "Beginning source.");
+ source.begin(this);
+ }
+
+ @Override
+ public void store(Record record) {
+ numStored.incrementAndGet();
+ try {
+ sink.store(record);
+ } catch (NoStoreDelegateException e) {
+ Logger.error(LOG_TAG, "Got NoStoreDelegateException in RecordsChannel.store(). This should not occur. Aborting.", e);
+ delegate.onFlowStoreFailed(this, e, record.guid);
+ }
+ }
+
+ @Override
+ public void onFetchFailed(Exception ex, Record record) {
+ Logger.warn(LOG_TAG, "onFetchFailed. Calling for immediate stop.", ex);
+ numFetchFailed.incrementAndGet();
+ this.consumer.halt();
+ delegate.onFlowFetchFailed(this, ex);
+ }
+
+ @Override
+ public void onFetchedRecord(Record record) {
+ numFetched.incrementAndGet();
+ this.toProcess.add(record);
+ this.consumer.doNotify();
+ }
+
+ @Override
+ public void onFetchCompleted(final long fetchEnd) {
+ Logger.trace(LOG_TAG, "onFetchCompleted. Stopping consumer once stores are done.");
+ Logger.trace(LOG_TAG, "Fetch timestamp is " + fetchEnd);
+ this.fetchEnd = fetchEnd;
+ this.consumer.queueFilled();
+ }
+
+ @Override
+ public void onRecordStoreFailed(Exception ex, String recordGuid) {
+ Logger.trace(LOG_TAG, "Failed to store record with guid " + recordGuid);
+ numStoreFailed.incrementAndGet();
+ this.consumer.stored();
+ delegate.onFlowStoreFailed(this, ex, recordGuid);
+ // TODO: abort?
+ }
+
+ @Override
+ public void onRecordStoreSucceeded(String guid) {
+ Logger.trace(LOG_TAG, "Stored record with guid " + guid);
+ this.consumer.stored();
+ }
+
+
+ @Override
+ public void consumerIsDone(boolean allRecordsQueued) {
+ Logger.trace(LOG_TAG, "Consumer is done. Are we waiting for it? " + waitingForQueueDone);
+ if (waitingForQueueDone) {
+ waitingForQueueDone = false;
+ this.sink.storeDone(); // Now we'll be waiting for onStoreCompleted.
+ }
+ }
+
+ @Override
+ public void onStoreCompleted(long storeEnd) {
+ Logger.trace(LOG_TAG, "onStoreCompleted. Notifying delegate of onFlowCompleted. " +
+ "Fetch end is " + fetchEnd + ", store end is " + storeEnd);
+ // TODO: synchronize on consumer callback?
+ delegate.onFlowCompleted(this, fetchEnd, storeEnd);
+ }
+
+ @Override
+ public void onBeginFailed(Exception ex) {
+ delegate.onFlowBeginFailed(this, ex);
+ }
+
+ @Override
+ public void onBeginSucceeded(RepositorySession session) {
+ if (session == source) {
+ Logger.trace(LOG_TAG, "Source session began. Beginning sink session.");
+ try {
+ sink.begin(this);
+ } catch (InvalidSessionTransitionException e) {
+ onBeginFailed(e);
+ return;
+ }
+ }
+ if (session == sink) {
+ Logger.trace(LOG_TAG, "Sink session began. Beginning flow.");
+ this.flow();
+ return;
+ }
+
+ // TODO: error!
+ }
+
+ @Override
+ public RepositorySessionStoreDelegate deferredStoreDelegate(final ExecutorService executor) {
+ return new DeferredRepositorySessionStoreDelegate(this, executor);
+ }
+
+ @Override
+ public RepositorySessionBeginDelegate deferredBeginDelegate(final ExecutorService executor) {
+ return new DeferredRepositorySessionBeginDelegate(this, executor);
+ }
+
+ @Override
+ public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService executor) {
+ // Lie outright. We know that all of our fetch methods are safe.
+ return this;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsChannelDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsChannelDelegate.java
new file mode 100644
index 0000000000..8daeb7ad53
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsChannelDelegate.java
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.synchronizer;
+
+public interface RecordsChannelDelegate {
+ public void onFlowCompleted(RecordsChannel recordsChannel, long fetchEnd, long storeEnd);
+ public void onFlowBeginFailed(RecordsChannel recordsChannel, Exception ex);
+ public void onFlowFetchFailed(RecordsChannel recordsChannel, Exception ex);
+ public void onFlowStoreFailed(RecordsChannel recordsChannel, Exception ex, String recordGuid);
+ public void onFlowFinishFailed(RecordsChannel recordsChannel, Exception ex);
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsConsumerDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsConsumerDelegate.java
new file mode 100644
index 0000000000..a00abf8483
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsConsumerDelegate.java
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.synchronizer;
+
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+interface RecordsConsumerDelegate {
+ public abstract ConcurrentLinkedQueue<Record> getQueue();
+
+ /**
+ * Called when no more items will be processed.
+ * If forced is true, the consumer is terminating because it was told to halt;
+ * not all items will necessarily have been processed.
+ * If forced is false, the consumer has invoked store and received an onStoreCompleted callback.
+ * @param forced
+ */
+ public abstract void consumerIsDone(boolean forced);
+ public abstract void store(Record record);
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SerialRecordConsumer.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SerialRecordConsumer.java
new file mode 100644
index 0000000000..6ee44ea2ba
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SerialRecordConsumer.java
@@ -0,0 +1,131 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.synchronizer;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+/**
+ * Consume records from a queue inside a RecordsChannel, storing them serially.
+ * @author rnewman
+ *
+ */
+class SerialRecordConsumer extends RecordConsumer {
+ private static final String LOG_TAG = "SerialRecordConsumer";
+ protected boolean stopEventually = false;
+ private volatile long counter = 0;
+
+ public SerialRecordConsumer(RecordsConsumerDelegate delegate) {
+ this.delegate = delegate;
+ }
+
+ private final Object monitor = new Object();
+ @Override
+ public void doNotify() {
+ synchronized (monitor) {
+ monitor.notify();
+ }
+ }
+
+ @Override
+ public void queueFilled() {
+ Logger.debug(LOG_TAG, "Queue filled.");
+ synchronized (monitor) {
+ this.stopEventually = true;
+ monitor.notify();
+ }
+ }
+
+ @Override
+ public void halt() {
+ Logger.debug(LOG_TAG, "Halting.");
+ synchronized (monitor) {
+ this.stopEventually = true;
+ this.stopImmediately = true;
+ monitor.notify();
+ }
+ }
+
+ private final Object storeSerializer = new Object();
+ @Override
+ public void stored() {
+ Logger.debug(LOG_TAG, "Record stored. Notifying.");
+ synchronized (storeSerializer) {
+ Logger.debug(LOG_TAG, "stored() took storeSerializer.");
+ counter++;
+ storeSerializer.notify();
+ Logger.debug(LOG_TAG, "stored() dropped storeSerializer.");
+ }
+ }
+ private void storeSerially(Record record) {
+ Logger.debug(LOG_TAG, "New record to store.");
+ synchronized (storeSerializer) {
+ Logger.debug(LOG_TAG, "storeSerially() took storeSerializer.");
+ Logger.debug(LOG_TAG, "Storing...");
+ try {
+ this.delegate.store(record);
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Got exception in store. Not waiting.", e);
+ return; // So we don't block for a stored() that never comes.
+ }
+ try {
+ Logger.debug(LOG_TAG, "Waiting...");
+ storeSerializer.wait();
+ } catch (InterruptedException e) {
+ // TODO
+ }
+ Logger.debug(LOG_TAG, "storeSerially() dropped storeSerializer.");
+ }
+ }
+
+ private void consumerIsDone() {
+ long counterNow = this.counter;
+ Logger.info(LOG_TAG, "Consumer is done. Processed " + counterNow + ((counterNow == 1) ? " record." : " records."));
+ delegate.consumerIsDone(stopImmediately);
+ }
+
+ @Override
+ public void run() {
+ while (true) {
+ synchronized (monitor) {
+ Logger.debug(LOG_TAG, "run() took monitor.");
+ if (stopImmediately) {
+ Logger.debug(LOG_TAG, "Stopping immediately. Clearing queue.");
+ delegate.getQueue().clear();
+ Logger.debug(LOG_TAG, "Notifying consumer.");
+ consumerIsDone();
+ return;
+ }
+ Logger.debug(LOG_TAG, "run() dropped monitor.");
+ }
+ // The queue is concurrent-safe.
+ while (!delegate.getQueue().isEmpty()) {
+ Logger.debug(LOG_TAG, "Grabbing record...");
+ Record record = delegate.getQueue().remove();
+ // Block here, allowing us to process records
+ // serially.
+ Logger.debug(LOG_TAG, "Invoking storeSerially...");
+ this.storeSerially(record);
+ Logger.debug(LOG_TAG, "Done with record.");
+ }
+ synchronized (monitor) {
+ Logger.debug(LOG_TAG, "run() took monitor.");
+
+ if (stopEventually) {
+ Logger.debug(LOG_TAG, "Done with records and told to stop. Notifying consumer.");
+ consumerIsDone();
+ return;
+ }
+ try {
+ Logger.debug(LOG_TAG, "Not told to stop but no records. Waiting.");
+ monitor.wait(10000);
+ } catch (InterruptedException e) {
+ // TODO
+ }
+ Logger.debug(LOG_TAG, "run() dropped monitor.");
+ }
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ServerLocalSynchronizer.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ServerLocalSynchronizer.java
new file mode 100644
index 0000000000..ac4f487895
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ServerLocalSynchronizer.java
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.synchronizer;
+
+/**
+ * A <code>SynchronizerSession</code> designed to be used between a remote
+ * server and a local repository.
+ * <p>
+ * See <code>ServerLocalSynchronizerSession</code> for error handling details.
+ */
+public class ServerLocalSynchronizer extends Synchronizer {
+ @Override
+ public SynchronizerSession newSynchronizerSession() {
+ return new ServerLocalSynchronizerSession(this, this);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ServerLocalSynchronizerSession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ServerLocalSynchronizerSession.java
new file mode 100644
index 0000000000..dc9eb01a0c
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ServerLocalSynchronizerSession.java
@@ -0,0 +1,78 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.synchronizer;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.repositories.FetchFailedException;
+import org.mozilla.gecko.sync.repositories.StoreFailedException;
+
+/**
+ * A <code>SynchronizerSession</code> designed to be used between a remote
+ * server and a local repository.
+ * <p>
+ * Handles failure cases as follows (in the order they will occur during a sync):
+ * <ul>
+ * <li>Remote fetch failures abort.</li>
+ * <li>Local store failures are ignored.</li>
+ * <li>Local fetch failures abort.</li>
+ * <li>Remote store failures abort.</li>
+ * </ul>
+ */
+public class ServerLocalSynchronizerSession extends SynchronizerSession {
+ protected static final String LOG_TAG = "ServLocSynchronizerSess";
+
+ public ServerLocalSynchronizerSession(Synchronizer synchronizer, SynchronizerSessionDelegate delegate) {
+ super(synchronizer, delegate);
+ }
+
+ @Override
+ public void onFirstFlowCompleted(RecordsChannel recordsChannel, long fetchEnd, long storeEnd) {
+ // Fetch failures always abort.
+ int numRemoteFetchFailed = recordsChannel.getFetchFailureCount();
+ if (numRemoteFetchFailed > 0) {
+ final String message = "Got " + numRemoteFetchFailed + " failures fetching remote records!";
+ Logger.warn(LOG_TAG, message + " Aborting session.");
+ delegate.onSynchronizeFailed(this, new FetchFailedException(), message);
+ return;
+ }
+ Logger.trace(LOG_TAG, "No failures fetching remote records.");
+
+ // Local store failures are ignored.
+ int numLocalStoreFailed = recordsChannel.getStoreFailureCount();
+ if (numLocalStoreFailed > 0) {
+ final String message = "Got " + numLocalStoreFailed + " failures storing local records!";
+ Logger.warn(LOG_TAG, message + " Ignoring local store failures and continuing synchronizer session.");
+ } else {
+ Logger.trace(LOG_TAG, "No failures storing local records.");
+ }
+
+ super.onFirstFlowCompleted(recordsChannel, fetchEnd, storeEnd);
+ }
+
+ @Override
+ public void onSecondFlowCompleted(RecordsChannel recordsChannel, long fetchEnd, long storeEnd) {
+ // Fetch failures always abort.
+ int numLocalFetchFailed = recordsChannel.getFetchFailureCount();
+ if (numLocalFetchFailed > 0) {
+ final String message = "Got " + numLocalFetchFailed + " failures fetching local records!";
+ Logger.warn(LOG_TAG, message + " Aborting session.");
+ delegate.onSynchronizeFailed(this, new FetchFailedException(), message);
+ return;
+ }
+ Logger.trace(LOG_TAG, "No failures fetching local records.");
+
+ // Remote store failures abort!
+ int numRemoteStoreFailed = recordsChannel.getStoreFailureCount();
+ if (numRemoteStoreFailed > 0) {
+ final String message = "Got " + numRemoteStoreFailed + " failures storing remote records!";
+ Logger.warn(LOG_TAG, message + " Aborting session.");
+ delegate.onSynchronizeFailed(this, new StoreFailedException(), message);
+ return;
+ }
+ Logger.trace(LOG_TAG, "No failures storing remote records.");
+
+ super.onSecondFlowCompleted(recordsChannel, fetchEnd, storeEnd);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SessionNotBegunException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SessionNotBegunException.java
new file mode 100644
index 0000000000..20c7fcd564
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SessionNotBegunException.java
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.synchronizer;
+
+import org.mozilla.gecko.sync.SyncException;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+
+public class SessionNotBegunException extends SyncException {
+
+ public RepositorySession failed;
+
+ public SessionNotBegunException(RepositorySession failed) {
+ this.failed = failed;
+ }
+
+ private static final long serialVersionUID = -4565241449897072841L;
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/Synchronizer.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/Synchronizer.java
new file mode 100644
index 0000000000..cc15b35a9b
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/Synchronizer.java
@@ -0,0 +1,105 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.synchronizer;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.SynchronizerConfiguration;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
+
+import android.content.Context;
+
+/**
+ * I perform a sync.
+ *
+ * Initialize me by calling `load` with a SynchronizerConfiguration.
+ *
+ * Start synchronizing by calling `synchronize` with a SynchronizerDelegate. I
+ * provide coarse-grained feedback by calling my delegate's callback methods.
+ *
+ * I always call exactly one of my delegate's `onSynchronized` or
+ * `onSynchronizeFailed` callback methods. In addition, I call
+ * `onSynchronizeAborted` before `onSynchronizeFailed` when I encounter a fetch,
+ * store, or session error while synchronizing.
+ *
+ * After synchronizing, call `save` to get back a SynchronizerConfiguration with
+ * updated bundle information.
+ */
+public class Synchronizer implements SynchronizerSessionDelegate {
+ public static final String LOG_TAG = "SyncDelSDelegate";
+
+ protected String configSyncID; // Used to pass syncID from load() back into save().
+
+ protected SynchronizerDelegate synchronizerDelegate;
+
+ protected SynchronizerSession session = null;
+
+ public SynchronizerSession getSynchronizerSession() {
+ return session;
+ }
+
+ @Override
+ public void onInitialized(SynchronizerSession session) {
+ session.synchronize();
+ }
+
+ @Override
+ public void onSynchronized(SynchronizerSession synchronizerSession) {
+ Logger.debug(LOG_TAG, "Got onSynchronized.");
+ Logger.debug(LOG_TAG, "Notifying SynchronizerDelegate.");
+ this.synchronizerDelegate.onSynchronized(synchronizerSession.getSynchronizer());
+ }
+
+ @Override
+ public void onSynchronizeSkipped(SynchronizerSession synchronizerSession) {
+ Logger.debug(LOG_TAG, "Got onSynchronizeSkipped.");
+ Logger.debug(LOG_TAG, "Notifying SynchronizerDelegate as if on success.");
+ this.synchronizerDelegate.onSynchronized(synchronizerSession.getSynchronizer());
+ }
+
+ @Override
+ public void onSynchronizeFailed(SynchronizerSession session,
+ Exception lastException, String reason) {
+ this.synchronizerDelegate.onSynchronizeFailed(session.getSynchronizer(), lastException, reason);
+ }
+
+ public Repository repositoryA;
+ public Repository repositoryB;
+ public RepositorySessionBundle bundleA;
+ public RepositorySessionBundle bundleB;
+
+ /**
+ * Fetch a synchronizer session appropriate for this <code>Synchronizer</code>
+ */
+ protected SynchronizerSession newSynchronizerSession() {
+ return new SynchronizerSession(this, this);
+ }
+
+ /**
+ * Start synchronizing, calling delegate's callback methods.
+ */
+ public void synchronize(Context context, SynchronizerDelegate delegate) {
+ this.synchronizerDelegate = delegate;
+ this.session = newSynchronizerSession();
+ this.session.init(context, bundleA, bundleB);
+ }
+
+ public SynchronizerConfiguration save() {
+ return new SynchronizerConfiguration(configSyncID, bundleA, bundleB);
+ }
+
+ /**
+ * Set my repository session bundles from a SynchronizerConfiguration.
+ *
+ * This method is not thread-safe.
+ *
+ * @param config
+ */
+ public void load(SynchronizerConfiguration config) {
+ bundleA = config.remoteBundle;
+ bundleB = config.localBundle;
+ configSyncID = config.syncID;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerDelegate.java
new file mode 100644
index 0000000000..a290188ab2
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerDelegate.java
@@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.synchronizer;
+
+public interface SynchronizerDelegate {
+ public void onSynchronized(Synchronizer synchronizer);
+ public void onSynchronizeFailed(Synchronizer synchronizer, Exception lastException, String reason);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerSession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerSession.java
new file mode 100644
index 0000000000..c4d244b4cc
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerSession.java
@@ -0,0 +1,425 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.synchronizer;
+
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.repositories.InactiveSessionException;
+import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
+import org.mozilla.gecko.sync.repositories.delegates.DeferrableRepositorySessionCreationDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.DeferredRepositorySessionFinishDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
+
+import android.content.Context;
+
+/**
+ * I coordinate the moving parts of a sync started by
+ * {@link Synchronizer#synchronize}.
+ *
+ * I flow records twice: first from A to B, and then from B to A. I provide
+ * fine-grained feedback by calling my delegate's callback methods.
+ *
+ * Initialize me by creating me with a Synchronizer and a
+ * SynchronizerSessionDelegate. Kick things off by calling `init` with two
+ * RepositorySessionBundles, and then call `synchronize` in your `onInitialized`
+ * callback.
+ *
+ * I always call exactly one of my delegate's `onInitialized` or
+ * `onSessionError` callback methods from `init`.
+ *
+ * I call my delegate's `onSynchronizeSkipped` callback method if there is no
+ * data to be synchronized in `synchronize`.
+ *
+ * In addition, I call `onFetchError`, `onStoreError`, and `onSessionError` when
+ * I encounter a fetch, store, or session error while synchronizing.
+ *
+ * Typically my delegate will call `abort` in its error callbacks, which will
+ * call my delegate's `onSynchronizeAborted` method and halt the sync.
+ *
+ * I always call exactly one of my delegate's `onSynchronized` or
+ * `onSynchronizeFailed` callback methods if I have not seen an error.
+ */
+public class SynchronizerSession
+extends DeferrableRepositorySessionCreationDelegate
+implements RecordsChannelDelegate,
+ RepositorySessionFinishDelegate {
+
+ protected static final String LOG_TAG = "SynchronizerSession";
+ protected Synchronizer synchronizer;
+ protected SynchronizerSessionDelegate delegate;
+ protected Context context;
+
+ /*
+ * Computed during init.
+ */
+ private RepositorySession sessionA;
+ private RepositorySession sessionB;
+ private RepositorySessionBundle bundleA;
+ private RepositorySessionBundle bundleB;
+
+ // Bug 726054: just like desktop, we track our last interaction with the server,
+ // not the last record timestamp that we fetched. This ensures that we don't re-
+ // download the records we just uploaded, at the cost of skipping any records
+ // that a concurrently syncing client has uploaded.
+ private long pendingATimestamp = -1;
+ private long pendingBTimestamp = -1;
+ private long storeEndATimestamp = -1;
+ private long storeEndBTimestamp = -1;
+ private boolean flowAToBCompleted = false;
+ private boolean flowBToACompleted = false;
+
+ protected final AtomicInteger numInboundRecords = new AtomicInteger(-1);
+ protected final AtomicInteger numOutboundRecords = new AtomicInteger(-1);
+
+ /*
+ * Public API: constructor, init, synchronize.
+ */
+ public SynchronizerSession(Synchronizer synchronizer, SynchronizerSessionDelegate delegate) {
+ this.setSynchronizer(synchronizer);
+ this.delegate = delegate;
+ }
+
+ public Synchronizer getSynchronizer() {
+ return synchronizer;
+ }
+
+ public void setSynchronizer(Synchronizer synchronizer) {
+ this.synchronizer = synchronizer;
+ }
+
+ public void init(Context context, RepositorySessionBundle bundleA, RepositorySessionBundle bundleB) {
+ this.context = context;
+ this.bundleA = bundleA;
+ this.bundleB = bundleB;
+ // Begin sessionA and sessionB, call onInitialized in callbacks.
+ this.getSynchronizer().repositoryA.createSession(this, context);
+ }
+
+ /**
+ * Get the number of records fetched from the first repository (usually the
+ * server, hence inbound).
+ * <p>
+ * Valid only after first flow has completed.
+ *
+ * @return number of records, or -1 if not valid.
+ */
+ public int getInboundCount() {
+ return numInboundRecords.get();
+ }
+
+ /**
+ * Get the number of records fetched from the second repository (usually the
+ * local store, hence outbound).
+ * <p>
+ * Valid only after second flow has completed.
+ *
+ * @return number of records, or -1 if not valid.
+ */
+ public int getOutboundCount() {
+ return numOutboundRecords.get();
+ }
+
+ // These are accessed by `abort` and `synchronize`, both of which are synchronized.
+ // Guarded by `this`.
+ protected RecordsChannel channelAToB;
+ protected RecordsChannel channelBToA;
+
+ /**
+ * Please don't call this until you've been notified with onInitialized.
+ */
+ public synchronized void synchronize() {
+ numInboundRecords.set(-1);
+ numOutboundRecords.set(-1);
+
+ // First thing: decide whether we should.
+ if (sessionA.shouldSkip() ||
+ sessionB.shouldSkip()) {
+ Logger.info(LOG_TAG, "Session requested skip. Short-circuiting sync.");
+ sessionA.abort();
+ sessionB.abort();
+ this.delegate.onSynchronizeSkipped(this);
+ return;
+ }
+
+ final SynchronizerSession session = this;
+
+ // TODO: failed record handling.
+
+ // This is the *second* record channel to flow.
+ // I, SynchronizerSession, am the delegate for the *second* flow.
+ channelBToA = new RecordsChannel(this.sessionB, this.sessionA, this);
+
+ // This is the delegate for the *first* flow.
+ RecordsChannelDelegate channelAToBDelegate = new RecordsChannelDelegate() {
+ @Override
+ public void onFlowCompleted(RecordsChannel recordsChannel, long fetchEnd, long storeEnd) {
+ session.onFirstFlowCompleted(recordsChannel, fetchEnd, storeEnd);
+ }
+
+ @Override
+ public void onFlowBeginFailed(RecordsChannel recordsChannel, Exception ex) {
+ Logger.warn(LOG_TAG, "First RecordsChannel onFlowBeginFailed. Logging session error.", ex);
+ session.delegate.onSynchronizeFailed(session, ex, "Failed to begin first flow.");
+ }
+
+ @Override
+ public void onFlowFetchFailed(RecordsChannel recordsChannel, Exception ex) {
+ Logger.warn(LOG_TAG, "First RecordsChannel onFlowFetchFailed. Logging remote fetch error.", ex);
+ }
+
+ @Override
+ public void onFlowStoreFailed(RecordsChannel recordsChannel, Exception ex, String recordGuid) {
+ Logger.warn(LOG_TAG, "First RecordsChannel onFlowStoreFailed. Logging local store error.", ex);
+ }
+
+ @Override
+ public void onFlowFinishFailed(RecordsChannel recordsChannel, Exception ex) {
+ Logger.warn(LOG_TAG, "First RecordsChannel onFlowFinishedFailed. Logging session error.", ex);
+ session.delegate.onSynchronizeFailed(session, ex, "Failed to finish first flow.");
+ }
+ };
+
+ // This is the *first* channel to flow.
+ channelAToB = new RecordsChannel(this.sessionA, this.sessionB, channelAToBDelegate);
+
+ Logger.trace(LOG_TAG, "Starting A to B flow. Channel is " + channelAToB);
+ try {
+ channelAToB.beginAndFlow();
+ } catch (InvalidSessionTransitionException e) {
+ onFlowBeginFailed(channelAToB, e);
+ }
+ }
+
+ /**
+ * Called after the first flow completes.
+ * <p>
+ * By default, any fetch and store failures are ignored.
+ * @param recordsChannel the <code>RecordsChannel</code> (for error testing).
+ * @param fetchEnd timestamp when fetches completed.
+ * @param storeEnd timestamp when stores completed.
+ */
+ public void onFirstFlowCompleted(RecordsChannel recordsChannel, long fetchEnd, long storeEnd) {
+ Logger.trace(LOG_TAG, "First RecordsChannel onFlowCompleted.");
+ Logger.debug(LOG_TAG, "Fetch end is " + fetchEnd + ". Store end is " + storeEnd + ". Starting next.");
+ pendingATimestamp = fetchEnd;
+ storeEndBTimestamp = storeEnd;
+ numInboundRecords.set(recordsChannel.getFetchCount());
+ flowAToBCompleted = true;
+ channelBToA.flow();
+ }
+
+ /**
+ * Called after the second flow completes.
+ * <p>
+ * By default, any fetch and store failures are ignored.
+ * @param recordsChannel the <code>RecordsChannel</code> (for error testing).
+ * @param fetchEnd timestamp when fetches completed.
+ * @param storeEnd timestamp when stores completed.
+ */
+ public void onSecondFlowCompleted(RecordsChannel recordsChannel, long fetchEnd, long storeEnd) {
+ Logger.trace(LOG_TAG, "Second RecordsChannel onFlowCompleted.");
+ Logger.debug(LOG_TAG, "Fetch end is " + fetchEnd + ". Store end is " + storeEnd + ". Finishing.");
+
+ pendingBTimestamp = fetchEnd;
+ storeEndATimestamp = storeEnd;
+ numOutboundRecords.set(recordsChannel.getFetchCount());
+ flowBToACompleted = true;
+
+ // Finish the two sessions.
+ try {
+ this.sessionA.finish(this);
+ } catch (InactiveSessionException e) {
+ this.onFinishFailed(e);
+ return;
+ }
+ }
+
+ @Override
+ public void onFlowCompleted(RecordsChannel recordsChannel, long fetchEnd, long storeEnd) {
+ onSecondFlowCompleted(recordsChannel, fetchEnd, storeEnd);
+ }
+
+ @Override
+ public void onFlowBeginFailed(RecordsChannel recordsChannel, Exception ex) {
+ Logger.warn(LOG_TAG, "Second RecordsChannel onFlowBeginFailed. Logging session error.", ex);
+ this.delegate.onSynchronizeFailed(this, ex, "Failed to begin second flow.");
+ }
+
+ @Override
+ public void onFlowFetchFailed(RecordsChannel recordsChannel, Exception ex) {
+ Logger.warn(LOG_TAG, "Second RecordsChannel onFlowFetchFailed. Logging local fetch error.", ex);
+ }
+
+ @Override
+ public void onFlowStoreFailed(RecordsChannel recordsChannel, Exception ex, String recordGuid) {
+ Logger.warn(LOG_TAG, "Second RecordsChannel onFlowStoreFailed. Logging remote store error.", ex);
+ }
+
+ @Override
+ public void onFlowFinishFailed(RecordsChannel recordsChannel, Exception ex) {
+ Logger.warn(LOG_TAG, "Second RecordsChannel onFlowFinishedFailed. Logging session error.", ex);
+ this.delegate.onSynchronizeFailed(this, ex, "Failed to finish second flow.");
+ }
+
+ /*
+ * RepositorySessionCreationDelegate methods.
+ */
+
+ /**
+ * I could be called twice: once for sessionA and once for sessionB.
+ *
+ * I try to clean up sessionA if it is not null, since the creation of
+ * sessionB must have failed.
+ */
+ @Override
+ public void onSessionCreateFailed(Exception ex) {
+ // Attempt to finish the first session, if the second is the one that failed.
+ if (this.sessionA != null) {
+ try {
+ // We no longer need a reference to our context.
+ this.context = null;
+ this.sessionA.finish(this);
+ } catch (Exception e) {
+ // Never mind; best-effort finish.
+ }
+ }
+ // We no longer need a reference to our context.
+ this.context = null;
+ this.delegate.onSynchronizeFailed(this, ex, "Failed to create session");
+ }
+
+ /**
+ * I should be called twice: first for sessionA and second for sessionB.
+ *
+ * If I am called for sessionB, I call my delegate's `onInitialized` callback
+ * method because my repository sessions are correctly initialized.
+ */
+ // TODO: some of this "finish and clean up" code can be refactored out.
+ @Override
+ public void onSessionCreated(RepositorySession session) {
+ if (session == null ||
+ this.sessionA == session) {
+ // TODO: clean up sessionA.
+ this.delegate.onSynchronizeFailed(this, new UnexpectedSessionException(session), "Failed to create session.");
+ return;
+ }
+ if (this.sessionA == null) {
+ this.sessionA = session;
+
+ // Unbundle.
+ try {
+ this.sessionA.unbundle(this.bundleA);
+ } catch (Exception e) {
+ this.delegate.onSynchronizeFailed(this, new UnbundleError(e, sessionA), "Failed to unbundle first session.");
+ // TODO: abort
+ return;
+ }
+ this.getSynchronizer().repositoryB.createSession(this, this.context);
+ return;
+ }
+ if (this.sessionB == null) {
+ this.sessionB = session;
+ // We no longer need a reference to our context.
+ this.context = null;
+
+ // Unbundle. We unbundled sessionA when that session was created.
+ try {
+ this.sessionB.unbundle(this.bundleB);
+ } catch (Exception e) {
+ this.delegate.onSynchronizeFailed(this, new UnbundleError(e, sessionA), "Failed to unbundle second session.");
+ return;
+ }
+
+ this.delegate.onInitialized(this);
+ return;
+ }
+ // TODO: need a way to make sure we don't call any more delegate methods.
+ this.delegate.onSynchronizeFailed(this, new UnexpectedSessionException(session), "Failed to create session.");
+ }
+
+ /*
+ * RepositorySessionFinishDelegate methods.
+ */
+
+ /**
+ * I could be called twice: once for sessionA and once for sessionB.
+ *
+ * If sessionB couldn't be created, I don't fail again.
+ */
+ @Override
+ public void onFinishFailed(Exception ex) {
+ if (this.sessionB == null) {
+ // Ah, it was a problem cleaning up. Never mind.
+ Logger.warn(LOG_TAG, "Got exception cleaning up first after second session creation failed.", ex);
+ return;
+ }
+ String session = (this.sessionA == null) ? "B" : "A";
+ this.delegate.onSynchronizeFailed(this, ex, "Finish of session " + session + " failed.");
+ }
+
+ /**
+ * I should be called twice: first for sessionA and second for sessionB.
+ *
+ * If I am called for sessionA, I try to finish sessionB.
+ *
+ * If I am called for sessionB, I call my delegate's `onSynchronized` callback
+ * method because my flows should have completed.
+ */
+ @Override
+ public void onFinishSucceeded(RepositorySession session,
+ RepositorySessionBundle bundle) {
+ Logger.debug(LOG_TAG, "onFinishSucceeded. Flows? " + flowAToBCompleted + ", " + flowBToACompleted);
+
+ if (session == sessionA) {
+ if (flowAToBCompleted) {
+ Logger.debug(LOG_TAG, "onFinishSucceeded: bumping session A's timestamp to " + pendingATimestamp + " or " + storeEndATimestamp);
+ bundle.bumpTimestamp(Math.max(pendingATimestamp, storeEndATimestamp));
+ this.synchronizer.bundleA = bundle;
+ } else {
+ // Should not happen!
+ this.delegate.onSynchronizeFailed(this, new UnexpectedSessionException(sessionA), "Failed to finish first session.");
+ return;
+ }
+ if (this.sessionB != null) {
+ Logger.trace(LOG_TAG, "Finishing session B.");
+ // On to the next.
+ try {
+ this.sessionB.finish(this);
+ } catch (InactiveSessionException e) {
+ this.onFinishFailed(e);
+ return;
+ }
+ }
+ } else if (session == sessionB) {
+ if (flowBToACompleted) {
+ Logger.debug(LOG_TAG, "onFinishSucceeded: bumping session B's timestamp to " + pendingBTimestamp + " or " + storeEndBTimestamp);
+ bundle.bumpTimestamp(Math.max(pendingBTimestamp, storeEndBTimestamp));
+ this.synchronizer.bundleB = bundle;
+ Logger.trace(LOG_TAG, "Notifying delegate.onSynchronized.");
+ this.delegate.onSynchronized(this);
+ } else {
+ // Should not happen!
+ this.delegate.onSynchronizeFailed(this, new UnexpectedSessionException(sessionB), "Failed to finish second session.");
+ return;
+ }
+ } else {
+ // TODO: hurrrrrr...
+ }
+
+ if (this.sessionB == null) {
+ this.sessionA = null; // We're done.
+ }
+ }
+
+ @Override
+ public RepositorySessionFinishDelegate deferredFinishDelegate(final ExecutorService executor) {
+ return new DeferredRepositorySessionFinishDelegate(this, executor);
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerSessionDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerSessionDelegate.java
new file mode 100644
index 0000000000..1d55274e82
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerSessionDelegate.java
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.synchronizer;
+
+public interface SynchronizerSessionDelegate {
+ public void onInitialized(SynchronizerSession session);
+
+ public void onSynchronized(SynchronizerSession session);
+ public void onSynchronizeFailed(SynchronizerSession session, Exception lastException, String reason);
+ public void onSynchronizeSkipped(SynchronizerSession synchronizerSession);
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/UnbundleError.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/UnbundleError.java
new file mode 100644
index 0000000000..fea779636e
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/UnbundleError.java
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.synchronizer;
+
+import org.mozilla.gecko.sync.SyncException;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+
+public class UnbundleError extends SyncException {
+ private static final long serialVersionUID = -8709503281041697522L;
+
+ public RepositorySession failedSession;
+
+ public UnbundleError(Exception e, RepositorySession session) {
+ super(e);
+ this.failedSession = session;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/UnexpectedSessionException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/UnexpectedSessionException.java
new file mode 100644
index 0000000000..0237b884b5
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/UnexpectedSessionException.java
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.synchronizer;
+
+import org.mozilla.gecko.sync.SyncException;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+
+/**
+ * An exception class that indicates that a session was passed
+ * to a begin callback and wasn't expected.
+ *
+ * This shouldn't occur.
+ *
+ * @author rnewman
+ *
+ */
+public class UnexpectedSessionException extends SyncException {
+ private static final long serialVersionUID = 949010933527484721L;
+ public RepositorySession session;
+
+ public UnexpectedSessionException(RepositorySession session) {
+ this.session = session;
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/telemetry/TelemetryContract.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/telemetry/TelemetryContract.java
new file mode 100644
index 0000000000..e3e134fe55
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/telemetry/TelemetryContract.java
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.telemetry;
+
+public class TelemetryContract {
+ /**
+ * We are a Sync 1.1 (legacy) client, and we downloaded a migration sentinel.
+ */
+ public static final String SYNC11_MIGRATION_SENTINELS_SEEN = "FENNEC_SYNC11_MIGRATION_SENTINELS_SEEN";
+
+ /**
+ * We are a Sync 1.1 (legacy) client and we have downloaded a migration
+ * sentinel, but there was an error creating a Firefox Account from that
+ * sentinel.
+ * <p>
+ * We have logged the error and are ignoring that sentinel.
+ */
+ public static final String SYNC11_MIGRATIONS_FAILED = "FENNEC_SYNC11_MIGRATIONS_FAILED";
+
+ /**
+ * We are a Sync 1.1 (legacy) client and we have downloaded a migration
+ * sentinel, and there was no reported error creating a Firefox Account from
+ * that sentinel.
+ * <p>
+ * We have created a Firefox Account corresponding to the sentinel and have
+ * queued the existing Old Sync account for removal.
+ */
+ public static final String SYNC11_MIGRATIONS_SUCCEEDED = "FENNEC_SYNC11_MIGRATIONS_SUCCEEDED";
+
+ /**
+ * We are (now) a Sync 1.5 (Firefox Accounts-based) client that migrated from
+ * Sync 1.1. We have presented the user the "complete upgrade" notification.
+ * <p>
+ * We will offer every time a sync is triggered, including when a notification
+ * is already pending.
+ */
+ public static final String SYNC11_MIGRATION_NOTIFICATIONS_OFFERED = "FENNEC_SYNC11_MIGRATION_NOTIFICATIONS_OFFERED";
+
+ /**
+ * We are (now) a Sync 1.5 (Firefox Accounts-based) client that migrated from
+ * Sync 1.1. We have presented the user the "complete upgrade" notification
+ * and they have successfully completed the upgrade process by entering their
+ * Firefox Account credentials.
+ */
+ public static final String SYNC11_MIGRATIONS_COMPLETED = "FENNEC_SYNC11_MIGRATIONS_COMPLETED";
+
+ public static final String SYNC_STARTED = "FENNEC_SYNC_NUMBER_OF_SYNCS_STARTED";
+
+ public static final String SYNC_COMPLETED = "FENNEC_SYNC_NUMBER_OF_SYNCS_COMPLETED";
+
+ public static final String SYNC_FAILED = "FENNEC_SYNC_NUMBER_OF_SYNCS_FAILED";
+
+ public static final String SYNC_FAILED_BACKOFF = "FENNEC_SYNC_NUMBER_OF_SYNCS_FAILED_BACKOFF";
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerClient.java b/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerClient.java
new file mode 100644
index 0000000000..9ee014dcbc
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerClient.java
@@ -0,0 +1,330 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tokenserver;
+
+import java.io.IOException;
+import java.net.URI;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+import org.json.simple.JSONObject;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.SkewHandler;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonArrayJSONException;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.UnexpectedJSONException.BadRequiredFieldJSONException;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BaseResourceDelegate;
+import org.mozilla.gecko.sync.net.BrowserIDAuthHeaderProvider;
+import org.mozilla.gecko.sync.net.SyncResponse;
+import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerConditionsRequiredException;
+import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerInvalidCredentialsException;
+import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedRequestException;
+import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedResponseException;
+import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerUnknownServiceException;
+
+import ch.boye.httpclientandroidlib.Header;
+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 ch.boye.httpclientandroidlib.message.BasicHeader;
+
+/**
+ * HTTP client for interacting with the Mozilla Services Token Server API v1.0,
+ * as documented at
+ * <a href="http://docs.services.mozilla.com/token/apis.html">http://docs.services.mozilla.com/token/apis.html</a>.
+ * <p>
+ * A token server accepts some authorization credential and returns a different
+ * authorization credential. Usually, it used to exchange a public-key
+ * authorization token that is expensive to validate for a symmetric-key
+ * authorization that is cheap to validate. For example, we might exchange a
+ * BrowserID assertion for a HAWK id and key pair.
+ */
+public class TokenServerClient {
+ protected static final String LOG_TAG = "TokenServerClient";
+
+ public static final String JSON_KEY_API_ENDPOINT = "api_endpoint";
+ public static final String JSON_KEY_CONDITION_URLS = "condition_urls";
+ public static final String JSON_KEY_DURATION = "duration";
+ public static final String JSON_KEY_ERRORS = "errors";
+ public static final String JSON_KEY_ID = "id";
+ public static final String JSON_KEY_KEY = "key";
+ public static final String JSON_KEY_UID = "uid";
+
+ public static final String HEADER_CONDITIONS_ACCEPTED = "X-Conditions-Accepted";
+ public static final String HEADER_CLIENT_STATE = "X-Client-State";
+
+ protected final Executor executor;
+ protected final URI uri;
+
+ public TokenServerClient(URI uri, Executor executor) {
+ if (uri == null) {
+ throw new IllegalArgumentException("uri must not be null");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("executor must not be null");
+ }
+ this.uri = uri;
+ this.executor = executor;
+ }
+
+ protected void invokeHandleSuccess(final TokenServerClientDelegate delegate, final TokenServerToken token) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ delegate.handleSuccess(token);
+ }
+ });
+ }
+
+ protected void invokeHandleFailure(final TokenServerClientDelegate delegate, final TokenServerException e) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ delegate.handleFailure(e);
+ }
+ });
+ }
+
+ /**
+ * Notify the delegate that some kind of backoff header (X-Backoff,
+ * X-Weave-Backoff, Retry-After) was received and should be acted upon.
+ *
+ * This method is non-terminal, and will be followed by a separate
+ * <code>invoke*</code> call.
+ *
+ * @param delegate
+ * the delegate to inform.
+ * @param backoffSeconds
+ * the number of seconds for which the system should wait before
+ * making another token server request to this server.
+ */
+ protected void notifyBackoff(final TokenServerClientDelegate delegate, final int backoffSeconds) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ delegate.handleBackoff(backoffSeconds);
+ }
+ });
+ }
+
+ protected void invokeHandleError(final TokenServerClientDelegate delegate, final Exception e) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ delegate.handleError(e);
+ }
+ });
+ }
+
+ public TokenServerToken processResponse(SyncResponse res) throws TokenServerException {
+ int statusCode = res.getStatusCode();
+
+ Logger.debug(LOG_TAG, "Got token response with status code " + statusCode + ".");
+
+ // Responses should *always* be JSON, even in the case of 4xx and 5xx
+ // errors. If we don't see JSON, the server is likely very unhappy.
+ final Header contentType = res.getContentType();
+ if (contentType == null) {
+ throw new TokenServerMalformedResponseException(null, "Non-JSON response Content-Type.");
+ }
+
+ final String type = contentType.getValue();
+ if (!type.equals("application/json") &&
+ !type.startsWith("application/json;")) {
+ Logger.warn(LOG_TAG, "Got non-JSON response with Content-Type " +
+ contentType + ". Misconfigured server?");
+ throw new TokenServerMalformedResponseException(null, "Non-JSON response Content-Type.");
+ }
+
+ // Responses should *always* be a valid JSON object.
+ // It turns out that right now they're not always, but that's a server bug...
+ ExtendedJSONObject result;
+ try {
+ result = res.jsonObjectBody();
+ } catch (Exception e) {
+ Logger.debug(LOG_TAG, "Malformed token response.", e);
+ throw new TokenServerMalformedResponseException(null, e);
+ }
+
+ // The service shouldn't have any 3xx, so we don't need to handle those.
+ if (res.getStatusCode() != 200) {
+ // We should have a (Cornice) error report in the JSON. We log that to
+ // help with debugging.
+ List<ExtendedJSONObject> errorList = new ArrayList<ExtendedJSONObject>();
+
+ if (result.containsKey(JSON_KEY_ERRORS)) {
+ try {
+ for (Object error : result.getArray(JSON_KEY_ERRORS)) {
+ Logger.warn(LOG_TAG, "" + error);
+
+ if (error instanceof JSONObject) {
+ errorList.add(new ExtendedJSONObject((JSONObject) error));
+ }
+ }
+ } catch (NonArrayJSONException e) {
+ Logger.warn(LOG_TAG, "Got non-JSON array '" + JSON_KEY_ERRORS + "'.", e);
+ }
+ }
+
+ if (statusCode == 400) {
+ throw new TokenServerMalformedRequestException(errorList, result.toJSONString());
+ }
+
+ if (statusCode == 401) {
+ throw new TokenServerInvalidCredentialsException(errorList, result.toJSONString());
+ }
+
+ // 403 should represent a "condition acceptance needed" response.
+ //
+ // The extra validation of "urls" is important. We don't want to signal
+ // conditions required unless we are absolutely sure that is what the
+ // server is asking for.
+ if (statusCode == 403) {
+ // Bug 792674 and Bug 783598: make this testing simpler. For now, we
+ // check that errors is an array, and take any condition_urls from the
+ // first element.
+
+ try {
+ if (errorList == null || errorList.isEmpty()) {
+ throw new TokenServerMalformedResponseException(errorList, "403 response without proper fields.");
+ }
+
+ ExtendedJSONObject error = errorList.get(0);
+
+ ExtendedJSONObject condition_urls = error.getObject(JSON_KEY_CONDITION_URLS);
+ if (condition_urls != null) {
+ throw new TokenServerConditionsRequiredException(condition_urls);
+ }
+ } catch (NonObjectJSONException e) {
+ Logger.warn(LOG_TAG, "Got non-JSON error object.");
+ }
+
+ throw new TokenServerMalformedResponseException(errorList, "403 response without proper fields.");
+ }
+
+ if (statusCode == 404) {
+ throw new TokenServerUnknownServiceException(errorList);
+ }
+
+ // We shouldn't ever get here...
+ throw new TokenServerException(errorList);
+ }
+
+ try {
+ result.throwIfFieldsMissingOrMisTyped(new String[] { JSON_KEY_ID, JSON_KEY_KEY, JSON_KEY_API_ENDPOINT }, String.class);
+ result.throwIfFieldsMissingOrMisTyped(new String[] { JSON_KEY_UID }, Long.class);
+ } catch (BadRequiredFieldJSONException e ) {
+ throw new TokenServerMalformedResponseException(null, e);
+ }
+
+ Logger.debug(LOG_TAG, "Successful token response: " + result.getString(JSON_KEY_ID));
+
+ return new TokenServerToken(result.getString(JSON_KEY_ID),
+ result.getString(JSON_KEY_KEY),
+ result.get(JSON_KEY_UID).toString(),
+ result.getString(JSON_KEY_API_ENDPOINT));
+ }
+
+ public static class TokenFetchResourceDelegate extends BaseResourceDelegate {
+ private final TokenServerClient client;
+ private final TokenServerClientDelegate delegate;
+ private final String assertion;
+ private final String clientState;
+ private final BaseResource resource;
+ private final boolean conditionsAccepted;
+
+ public TokenFetchResourceDelegate(TokenServerClient client,
+ BaseResource resource,
+ TokenServerClientDelegate delegate,
+ String assertion, String clientState,
+ boolean conditionsAccepted) {
+ super(resource);
+ this.client = client;
+ this.delegate = delegate;
+ this.assertion = assertion;
+ this.clientState = clientState;
+ this.resource = resource;
+ this.conditionsAccepted = conditionsAccepted;
+ }
+
+ @Override
+ public String getUserAgent() {
+ return delegate.getUserAgent();
+ }
+
+ @Override
+ public void handleHttpResponse(HttpResponse response) {
+ // Skew.
+ SkewHandler skewHandler = SkewHandler.getSkewHandlerForResource(resource);
+ skewHandler.updateSkew(response, System.currentTimeMillis());
+
+ // Extract backoff regardless of whether this was an error response, and
+ // Retry-After for 503 responses. The error will be handled elsewhere.)
+ SyncResponse res = new SyncResponse(response);
+ final boolean includeRetryAfter = res.getStatusCode() == 503;
+ int backoffInSeconds = res.totalBackoffInSeconds(includeRetryAfter);
+ if (backoffInSeconds > -1) {
+ client.notifyBackoff(delegate, backoffInSeconds);
+ }
+
+ try {
+ TokenServerToken token = client.processResponse(res);
+ client.invokeHandleSuccess(delegate, token);
+ } catch (TokenServerException e) {
+ client.invokeHandleFailure(delegate, e);
+ }
+ }
+
+ @Override
+ public void handleTransportException(GeneralSecurityException e) {
+ client.invokeHandleError(delegate, e);
+ }
+
+ @Override
+ public void handleHttpProtocolException(ClientProtocolException e) {
+ client.invokeHandleError(delegate, e);
+ }
+
+ @Override
+ public void handleHttpIOException(IOException e) {
+ client.invokeHandleError(delegate, e);
+ }
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return new BrowserIDAuthHeaderProvider(assertion);
+ }
+
+ @Override
+ public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
+ String host = request.getURI().getHost();
+ request.setHeader(new BasicHeader(HttpHeaders.HOST, host));
+ if (clientState != null) {
+ request.setHeader(new BasicHeader(HEADER_CLIENT_STATE, clientState));
+ }
+ if (conditionsAccepted) {
+ request.addHeader(HEADER_CONDITIONS_ACCEPTED, "1");
+ }
+ }
+ }
+
+ public void getTokenFromBrowserIDAssertion(final String assertion,
+ final boolean conditionsAccepted,
+ final String clientState,
+ final TokenServerClientDelegate delegate) {
+ final BaseResource resource = new BaseResource(this.uri);
+ resource.delegate = new TokenFetchResourceDelegate(this, resource, delegate,
+ assertion, clientState,
+ conditionsAccepted);
+ resource.get();
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerClientDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerClientDelegate.java
new file mode 100644
index 0000000000..e1dfe24229
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerClientDelegate.java
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tokenserver;
+
+
+public interface TokenServerClientDelegate {
+ void handleSuccess(TokenServerToken token);
+ void handleFailure(TokenServerException e);
+ void handleError(Exception e);
+
+ /**
+ * Might be called multiple times, in addition to the other terminating handler methods.
+ */
+ void handleBackoff(int backoffSeconds);
+
+ public String getUserAgent();
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerException.java
new file mode 100644
index 0000000000..099e51867b
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerException.java
@@ -0,0 +1,89 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tokenserver;
+
+import java.util.List;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+public class TokenServerException extends Exception {
+ private static final long serialVersionUID = 7185692034925819696L;
+
+ public final List<ExtendedJSONObject> errors;
+
+ public TokenServerException(List<ExtendedJSONObject> errors) {
+ super();
+ this.errors = errors;
+ }
+
+ public TokenServerException(List<ExtendedJSONObject> errors, String string) {
+ super(string);
+ this.errors = errors;
+ }
+
+ public TokenServerException(List<ExtendedJSONObject> errors, Throwable e) {
+ super(e);
+ this.errors = errors;
+ }
+
+ public static class TokenServerConditionsRequiredException extends TokenServerException {
+ private static final long serialVersionUID = 7578072663150608399L;
+
+ public final ExtendedJSONObject conditionUrls;
+
+ public TokenServerConditionsRequiredException(ExtendedJSONObject urls) {
+ super(null);
+ this.conditionUrls = urls;
+ }
+ }
+
+ public static class TokenServerInvalidCredentialsException extends TokenServerException {
+ private static final long serialVersionUID = 7578072663150608398L;
+
+ public TokenServerInvalidCredentialsException(List<ExtendedJSONObject> errors) {
+ super(errors);
+ }
+
+ public TokenServerInvalidCredentialsException(List<ExtendedJSONObject> errors, String message) {
+ super(errors, message);
+ }
+ }
+
+ public static class TokenServerUnknownServiceException extends TokenServerException {
+ private static final long serialVersionUID = 7578072663150608397L;
+
+ public TokenServerUnknownServiceException(List<ExtendedJSONObject> errors) {
+ super(errors);
+ }
+
+ public TokenServerUnknownServiceException(List<ExtendedJSONObject> errors, String message) {
+ super(errors, message);
+ }
+ }
+
+ public static class TokenServerMalformedRequestException extends TokenServerException {
+ private static final long serialVersionUID = 7578072663150608396L;
+
+ public TokenServerMalformedRequestException(List<ExtendedJSONObject> errors) {
+ super(errors);
+ }
+
+ public TokenServerMalformedRequestException(List<ExtendedJSONObject> errors, String message) {
+ super(errors, message);
+ }
+ }
+
+ public static class TokenServerMalformedResponseException extends TokenServerException {
+ private static final long serialVersionUID = 7578072663150608395L;
+
+ public TokenServerMalformedResponseException(List<ExtendedJSONObject> errors, String message) {
+ super(errors, message);
+ }
+
+ public TokenServerMalformedResponseException(List<ExtendedJSONObject> errors, Throwable e) {
+ super(errors, e);
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerToken.java b/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerToken.java
new file mode 100644
index 0000000000..916586cdcb
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerToken.java
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tokenserver;
+
+public class TokenServerToken {
+ public final String id;
+ public final String key;
+ public final String uid;
+ public final String endpoint;
+
+ public TokenServerToken(String id, String key, String uid, String endpoint) {
+ this.id = id;
+ this.key = key;
+ this.uid = uid;
+ this.endpoint = endpoint;
+ }
+} \ No newline at end of file
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/util/PRNGFixes.java b/mobile/android/services/src/main/java/org/mozilla/gecko/util/PRNGFixes.java
new file mode 100644
index 0000000000..ebb50f7659
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/util/PRNGFixes.java
@@ -0,0 +1,339 @@
+/*
+ * This software is provided 'as-is', without any express or implied
+ * warranty. In no event will Google be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, as long as the origin is not misrepresented.
+ */
+
+package org.mozilla.gecko.util;
+
+import android.os.Build;
+import android.os.Process;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
+import java.security.SecureRandom;
+import java.security.SecureRandomSpi;
+import java.security.Security;
+
+/**
+ * Fixes for the output of the default PRNG having low entropy.
+ *
+ * The fixes need to be applied via {@link #apply()} before any use of Java
+ * Cryptography Architecture primitives. A good place to invoke them is in the
+ * application's {@code onCreate}.
+ */
+public final class PRNGFixes {
+ private static final long serialVersionUID = -687331492884005033L;
+
+ private static final int VERSION_CODE_JELLY_BEAN = 16;
+ private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18;
+ private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL =
+ getBuildFingerprintAndDeviceSerial();
+
+ /** Hidden constructor to prevent instantiation. */
+ private PRNGFixes() {}
+
+ /**
+ * Applies all fixes.
+ *
+ * @throws SecurityException if a fix is needed but could not be applied.
+ */
+ public static void apply() {
+ applyOpenSSLFix();
+ installLinuxPRNGSecureRandom();
+ }
+
+ /**
+ * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the
+ * fix is not needed.
+ *
+ * @throws SecurityException if the fix is needed but could not be applied.
+ */
+ private static void applyOpenSSLFix() throws SecurityException {
+ if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN)
+ || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) {
+ // No need to apply the fix
+ return;
+ }
+
+ try {
+ // Mix in the device- and invocation-specific seed.
+ Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
+ .getMethod("RAND_seed", byte[].class)
+ .invoke(null, generateSeed());
+
+ // Mix output of Linux PRNG into OpenSSL's PRNG
+ int bytesRead = (Integer) Class.forName(
+ "org.apache.harmony.xnet.provider.jsse.NativeCrypto")
+ .getMethod("RAND_load_file", String.class, long.class)
+ .invoke(null, "/dev/urandom", 1024);
+ if (bytesRead != 1024) {
+ throw new IOException(
+ "Unexpected number of bytes read from Linux PRNG: "
+ + bytesRead);
+ }
+ } catch (Exception e) {
+ throw new SecurityException("Failed to seed OpenSSL PRNG", e);
+ }
+ }
+
+ /**
+ * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the
+ * default. Does nothing if the implementation is already the default or if
+ * there is not need to install the implementation.
+ *
+ * @throws SecurityException if the fix is needed but could not be applied.
+ */
+ private static void installLinuxPRNGSecureRandom()
+ throws SecurityException {
+ if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) {
+ // No need to apply the fix
+ return;
+ }
+
+ // Install a Linux PRNG-based SecureRandom implementation as the
+ // default, if not yet installed.
+ Provider[] secureRandomProviders =
+ Security.getProviders("SecureRandom.SHA1PRNG");
+ if ((secureRandomProviders == null)
+ || (secureRandomProviders.length < 1)
+ || (!LinuxPRNGSecureRandomProvider.class.equals(
+ secureRandomProviders[0].getClass()))) {
+ Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
+ }
+
+ // Assert that new SecureRandom() and
+ // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
+ // by the Linux PRNG-based SecureRandom implementation.
+ SecureRandom rng1 = new SecureRandom();
+ if (!LinuxPRNGSecureRandomProvider.class.equals(
+ rng1.getProvider().getClass())) {
+ throw new SecurityException(
+ "new SecureRandom() backed by wrong Provider: "
+ + rng1.getProvider().getClass());
+ }
+
+ SecureRandom rng2;
+ try {
+ rng2 = SecureRandom.getInstance("SHA1PRNG");
+ } catch (NoSuchAlgorithmException e) {
+ throw new SecurityException("SHA1PRNG not available", e);
+ }
+ if (!LinuxPRNGSecureRandomProvider.class.equals(
+ rng2.getProvider().getClass())) {
+ throw new SecurityException(
+ "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
+ + " Provider: " + rng2.getProvider().getClass());
+ }
+ }
+
+ /**
+ * {@code Provider} of {@code SecureRandom} engines which pass through
+ * all requests to the Linux PRNG.
+ */
+ private static class LinuxPRNGSecureRandomProvider extends Provider {
+ private static final long serialVersionUID = -686731492884005033L;
+
+ public LinuxPRNGSecureRandomProvider() {
+ super("LinuxPRNG",
+ 1.0,
+ "A Linux-specific random number provider that uses"
+ + " /dev/urandom");
+ // Although /dev/urandom is not a SHA-1 PRNG, some apps
+ // explicitly request a SHA1PRNG SecureRandom and we thus need to
+ // prevent them from getting the default implementation whose output
+ // may have low entropy.
+ put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName());
+ put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
+ }
+ }
+
+ /**
+ * {@link SecureRandomSpi} which passes all requests to the Linux PRNG
+ * ({@code /dev/urandom}).
+ */
+ public static class LinuxPRNGSecureRandom extends SecureRandomSpi {
+ private static final long serialVersionUID = -696231492884005033L;
+
+ /*
+ * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed
+ * are passed through to the Linux PRNG (/dev/urandom). Instances of
+ * this class seed themselves by mixing in the current time, PID, UID,
+ * build fingerprint, and hardware serial number (where available) into
+ * Linux PRNG.
+ *
+ * Concurrency: Read requests to the underlying Linux PRNG are
+ * serialized (on sLock) to ensure that multiple threads do not get
+ * duplicated PRNG output.
+ */
+
+ private static final File URANDOM_FILE = new File("/dev/urandom");
+
+ private static final Object sLock = new Object();
+
+ /**
+ * Input stream for reading from Linux PRNG or {@code null} if not yet
+ * opened.
+ *
+ * @GuardedBy("sLock")
+ */
+ private static DataInputStream sUrandomIn;
+
+ /**
+ * Output stream for writing to Linux PRNG or {@code null} if not yet
+ * opened.
+ *
+ * @GuardedBy("sLock")
+ */
+ private static OutputStream sUrandomOut;
+
+ /**
+ * Whether this engine instance has been seeded. This is needed because
+ * each instance needs to seed itself if the client does not explicitly
+ * seed it.
+ */
+ private boolean mSeeded;
+
+ @Override
+ protected void engineSetSeed(byte[] bytes) {
+ try {
+ OutputStream out;
+ synchronized (sLock) {
+ out = getUrandomOutputStream();
+ }
+ out.write(bytes);
+ out.flush();
+ } catch (IOException e) {
+ // On a small fraction of devices /dev/urandom is not writable.
+ // Log and ignore.
+ Log.w(PRNGFixes.class.getSimpleName(),
+ "Failed to mix seed into " + URANDOM_FILE);
+ } finally {
+ mSeeded = true;
+ }
+ }
+
+ @Override
+ protected void engineNextBytes(byte[] bytes) {
+ if (!mSeeded) {
+ // Mix in the device- and invocation-specific seed.
+ engineSetSeed(generateSeed());
+ }
+
+ try {
+ DataInputStream in;
+ synchronized (sLock) {
+ in = getUrandomInputStream();
+ }
+ synchronized (in) {
+ in.readFully(bytes);
+ }
+ } catch (IOException e) {
+ throw new SecurityException(
+ "Failed to read from " + URANDOM_FILE, e);
+ }
+ }
+
+ @Override
+ protected byte[] engineGenerateSeed(int size) {
+ byte[] seed = new byte[size];
+ engineNextBytes(seed);
+ return seed;
+ }
+
+ private DataInputStream getUrandomInputStream() {
+ synchronized (sLock) {
+ if (sUrandomIn == null) {
+ // NOTE: Consider inserting a BufferedInputStream between
+ // DataInputStream and FileInputStream if you need higher
+ // PRNG output performance and can live with future PRNG
+ // output being pulled into this process prematurely.
+ try {
+ sUrandomIn = new DataInputStream(
+ new FileInputStream(URANDOM_FILE));
+ } catch (IOException e) {
+ throw new SecurityException("Failed to open "
+ + URANDOM_FILE + " for reading", e);
+ }
+ }
+ return sUrandomIn;
+ }
+ }
+
+ private OutputStream getUrandomOutputStream() throws IOException {
+ synchronized (sLock) {
+ if (sUrandomOut == null) {
+ sUrandomOut = new FileOutputStream(URANDOM_FILE);
+ }
+ return sUrandomOut;
+ }
+ }
+ }
+
+ /**
+ * Generates a device- and invocation-specific seed to be mixed into the
+ * Linux PRNG.
+ */
+ private static byte[] generateSeed() {
+ try {
+ ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
+ DataOutputStream seedBufferOut =
+ new DataOutputStream(seedBuffer);
+ seedBufferOut.writeLong(System.currentTimeMillis());
+ seedBufferOut.writeLong(System.nanoTime());
+ seedBufferOut.writeInt(Process.myPid());
+ seedBufferOut.writeInt(Process.myUid());
+ seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL);
+ seedBufferOut.close();
+ return seedBuffer.toByteArray();
+ } catch (IOException e) {
+ throw new SecurityException("Failed to generate seed", e);
+ }
+ }
+
+ /**
+ * Gets the hardware serial number of this device.
+ *
+ * @return serial number or {@code null} if not available.
+ */
+ private static String getDeviceSerialNumber() {
+ // We're using the Reflection API because Build.SERIAL is only available
+ // since API Level 9 (Gingerbread, Android 2.3).
+ try {
+ return (String) Build.class.getField("SERIAL").get(null);
+ } catch (Exception ignored) {
+ return null;
+ }
+ }
+
+ private static byte[] getBuildFingerprintAndDeviceSerial() {
+ StringBuilder result = new StringBuilder();
+ String fingerprint = Build.FINGERPRINT;
+ if (fingerprint != null) {
+ result.append(fingerprint);
+ }
+ String serial = getDeviceSerialNumber();
+ if (serial != null) {
+ result.append(serial);
+ }
+ try {
+ return result.toString().getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("UTF-8 encoding not supported");
+ }
+ }
+}
diff --git a/mobile/android/services/src/main/res/drawable-hdpi/fxaccount_sync_error.png b/mobile/android/services/src/main/res/drawable-hdpi/fxaccount_sync_error.png
new file mode 100644
index 0000000000..3a2cbc4bff
--- /dev/null
+++ b/mobile/android/services/src/main/res/drawable-hdpi/fxaccount_sync_error.png
Binary files differ
diff --git a/mobile/android/services/src/main/res/drawable-hdpi/sync_avatar_default.png b/mobile/android/services/src/main/res/drawable-hdpi/sync_avatar_default.png
new file mode 100644
index 0000000000..caa6ed2466
--- /dev/null
+++ b/mobile/android/services/src/main/res/drawable-hdpi/sync_avatar_default.png
Binary files differ
diff --git a/mobile/android/services/src/main/res/drawable-hdpi/sync_desktop.png b/mobile/android/services/src/main/res/drawable-hdpi/sync_desktop.png
new file mode 100644
index 0000000000..abf87f16cb
--- /dev/null
+++ b/mobile/android/services/src/main/res/drawable-hdpi/sync_desktop.png
Binary files differ
diff --git a/mobile/android/services/src/main/res/drawable-hdpi/sync_desktop_inactive.png b/mobile/android/services/src/main/res/drawable-hdpi/sync_desktop_inactive.png
new file mode 100644
index 0000000000..869dbf4029
--- /dev/null
+++ b/mobile/android/services/src/main/res/drawable-hdpi/sync_desktop_inactive.png
Binary files differ
diff --git a/mobile/android/services/src/main/res/drawable-hdpi/sync_mobile.png b/mobile/android/services/src/main/res/drawable-hdpi/sync_mobile.png
new file mode 100644
index 0000000000..4b25152b2f
--- /dev/null
+++ b/mobile/android/services/src/main/res/drawable-hdpi/sync_mobile.png
Binary files differ
diff --git a/mobile/android/services/src/main/res/drawable-hdpi/sync_mobile_inactive.png b/mobile/android/services/src/main/res/drawable-hdpi/sync_mobile_inactive.png
new file mode 100644
index 0000000000..e9401797db
--- /dev/null
+++ b/mobile/android/services/src/main/res/drawable-hdpi/sync_mobile_inactive.png
Binary files differ
diff --git a/mobile/android/services/src/main/res/drawable-hdpi/sync_promo.png b/mobile/android/services/src/main/res/drawable-hdpi/sync_promo.png
new file mode 100644
index 0000000000..ea2150508b
--- /dev/null
+++ b/mobile/android/services/src/main/res/drawable-hdpi/sync_promo.png
Binary files differ
diff --git a/mobile/android/services/src/main/res/drawable-xhdpi/fxaccount_sync_error.png b/mobile/android/services/src/main/res/drawable-xhdpi/fxaccount_sync_error.png
new file mode 100644
index 0000000000..f9bf849fa2
--- /dev/null
+++ b/mobile/android/services/src/main/res/drawable-xhdpi/fxaccount_sync_error.png
Binary files differ
diff --git a/mobile/android/services/src/main/res/drawable-xhdpi/sync_desktop.png b/mobile/android/services/src/main/res/drawable-xhdpi/sync_desktop.png
new file mode 100644
index 0000000000..30d5b5c09d
--- /dev/null
+++ b/mobile/android/services/src/main/res/drawable-xhdpi/sync_desktop.png
Binary files differ
diff --git a/mobile/android/services/src/main/res/drawable-xhdpi/sync_desktop_inactive.png b/mobile/android/services/src/main/res/drawable-xhdpi/sync_desktop_inactive.png
new file mode 100644
index 0000000000..1b5b00a75d
--- /dev/null
+++ b/mobile/android/services/src/main/res/drawable-xhdpi/sync_desktop_inactive.png
Binary files differ
diff --git a/mobile/android/services/src/main/res/drawable-xhdpi/sync_mobile.png b/mobile/android/services/src/main/res/drawable-xhdpi/sync_mobile.png
new file mode 100644
index 0000000000..2c3f45d4a3
--- /dev/null
+++ b/mobile/android/services/src/main/res/drawable-xhdpi/sync_mobile.png
Binary files differ
diff --git a/mobile/android/services/src/main/res/drawable-xhdpi/sync_mobile_inactive.png b/mobile/android/services/src/main/res/drawable-xhdpi/sync_mobile_inactive.png
new file mode 100644
index 0000000000..60fd77c8a9
--- /dev/null
+++ b/mobile/android/services/src/main/res/drawable-xhdpi/sync_mobile_inactive.png
Binary files differ
diff --git a/mobile/android/services/src/main/res/drawable-xhdpi/sync_promo.png b/mobile/android/services/src/main/res/drawable-xhdpi/sync_promo.png
new file mode 100644
index 0000000000..63f1a55ad7
--- /dev/null
+++ b/mobile/android/services/src/main/res/drawable-xhdpi/sync_promo.png
Binary files differ
diff --git a/mobile/android/services/src/main/res/drawable-xxhdpi/fxaccount_sync_error.png b/mobile/android/services/src/main/res/drawable-xxhdpi/fxaccount_sync_error.png
new file mode 100644
index 0000000000..7555bc9d67
--- /dev/null
+++ b/mobile/android/services/src/main/res/drawable-xxhdpi/fxaccount_sync_error.png
Binary files differ
diff --git a/mobile/android/services/src/main/res/drawable-xxhdpi/sync_avatar_default.png b/mobile/android/services/src/main/res/drawable-xxhdpi/sync_avatar_default.png
new file mode 100644
index 0000000000..16d127882f
--- /dev/null
+++ b/mobile/android/services/src/main/res/drawable-xxhdpi/sync_avatar_default.png
Binary files differ
diff --git a/mobile/android/services/src/main/res/drawable-xxhdpi/sync_desktop.png b/mobile/android/services/src/main/res/drawable-xxhdpi/sync_desktop.png
new file mode 100644
index 0000000000..9bb9a55c2c
--- /dev/null
+++ b/mobile/android/services/src/main/res/drawable-xxhdpi/sync_desktop.png
Binary files differ
diff --git a/mobile/android/services/src/main/res/drawable-xxhdpi/sync_desktop_inactive.png b/mobile/android/services/src/main/res/drawable-xxhdpi/sync_desktop_inactive.png
new file mode 100644
index 0000000000..c3fe0ec1d5
--- /dev/null
+++ b/mobile/android/services/src/main/res/drawable-xxhdpi/sync_desktop_inactive.png
Binary files differ
diff --git a/mobile/android/services/src/main/res/drawable-xxhdpi/sync_mobile.png b/mobile/android/services/src/main/res/drawable-xxhdpi/sync_mobile.png
new file mode 100644
index 0000000000..400ddf65bd
--- /dev/null
+++ b/mobile/android/services/src/main/res/drawable-xxhdpi/sync_mobile.png
Binary files differ
diff --git a/mobile/android/services/src/main/res/drawable-xxhdpi/sync_mobile_inactive.png b/mobile/android/services/src/main/res/drawable-xxhdpi/sync_mobile_inactive.png
new file mode 100644
index 0000000000..a688b0d7b6
--- /dev/null
+++ b/mobile/android/services/src/main/res/drawable-xxhdpi/sync_mobile_inactive.png
Binary files differ
diff --git a/mobile/android/services/src/main/res/layout/fxaccount_preference_list_fragment.xml b/mobile/android/services/src/main/res/layout/fxaccount_preference_list_fragment.xml
new file mode 100644
index 0000000000..acaafc7c2f
--- /dev/null
+++ b/mobile/android/services/src/main/res/layout/fxaccount_preference_list_fragment.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 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.
+*/
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_height="fill_parent"
+ android:layout_width="fill_parent"
+ android:background="@android:color/transparent">
+
+ <ListView android:id="@android:id/list"
+ android:layout_width="fill_parent"
+ android:layout_height="0px"
+ android:layout_weight="1"
+ android:paddingTop="0dip"
+ android:paddingBottom="@dimen/preference_fragment_padding_bottom"
+ android:paddingLeft="@dimen/preference_fragment_padding_side"
+ android:paddingRight="@dimen/preference_fragment_padding_side"
+ android:scrollbarStyle="@integer/preference_fragment_scrollbarStyle"
+ android:clipToPadding="false"
+ android:drawSelectorOnTop="false"
+ android:cacheColorHint="@android:color/transparent"
+ android:scrollbarAlwaysDrawVerticalTrack="true" />
+
+</LinearLayout>
diff --git a/mobile/android/services/src/main/res/layout/fxaccount_status_error_preference.xml b/mobile/android/services/src/main/res/layout/fxaccount_status_error_preference.xml
new file mode 100644
index 0000000000..4a507cddd0
--- /dev/null
+++ b/mobile/android/services/src/main/res/layout/fxaccount_status_error_preference.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/fxaccount_error_preference_backgroundcolor"
+ android:gravity="center_vertical"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:paddingRight="?android:attr/scrollbarSize" >
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:minWidth="0dp"
+ android:orientation="horizontal" >
+
+ <ImageView
+ android:id="@+android:id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:minWidth="48dip"
+ android:padding="10dip" />
+ </LinearLayout>
+
+ <RelativeLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="6dip"
+ android:layout_marginLeft="15dip"
+ android:layout_marginRight="6dip"
+ android:layout_marginTop="6dip"
+ android:layout_weight="1" >
+
+ <TextView
+ android:id="@+android:id/title"
+ style="@style/FxAccountTextItem"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center_vertical" >
+ </TextView>
+ </RelativeLayout>
+
+ <!-- We ignore summary and widget_frame, but they still need to be present. We set them to be gone. -->
+
+ <TextView
+ android:id="@+android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:maxLines="4"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:attr/textColorSecondary"
+ android:visibility="gone" />
+
+ <!-- Preference should place its actual preference widget here. -->
+
+ <LinearLayout
+ android:id="@+android:id/widget_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:visibility="gone" />
+
+</LinearLayout>
diff --git a/mobile/android/services/src/main/res/layout/homescreen_prompt.xml b/mobile/android/services/src/main/res/layout/homescreen_prompt.xml
new file mode 100644
index 0000000000..26d04ad176
--- /dev/null
+++ b/mobile/android/services/src/main/res/layout/homescreen_prompt.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipChildren="false"
+ android:clipToPadding="false">
+
+ <RelativeLayout
+ android:id="@+id/container"
+ android:layout_width="@dimen/overlay_prompt_container_width"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|center"
+ android:background="@android:color/white"
+ android:clickable="true"
+ android:orientation="vertical">
+
+ <ImageView
+ android:id="@+id/close"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_alignParentRight="true"
+ android:layout_marginLeft="10dp"
+ android:layout_marginRight="30dp"
+ android:layout_marginTop="30dp"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:padding="6dp"
+ android:src="@drawable/tab_close_active" />
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="6dp"
+ android:layout_marginLeft="30dp"
+ android:layout_marginTop="30dp"
+ android:layout_toLeftOf="@id/close"
+ android:fontFamily="sans-serif-light"
+ android:textColor="@color/text_and_tabs_tray_grey"
+ android:textSize="20sp"
+ tools:text="The Pokedex" />
+
+ <TextView
+ android:id="@+id/host"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/title"
+ android:layout_marginBottom="20dp"
+ android:layout_marginLeft="30dp"
+ android:layout_marginRight="30dp"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textColor="@color/placeholder_grey"
+ android:textSize="16sp"
+ tools:text="pokedex.org" />
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="50dp"
+ android:layout_height="50dp"
+ android:layout_below="@id/host"
+ android:layout_marginBottom="20dp"
+ android:layout_marginLeft="30dp"
+ android:src="@drawable/icon" />
+
+ <Button
+ android:id="@+id/add"
+ style="@style/Widget.BaseButton"
+ android:layout_width="wrap_content"
+ android:layout_height="50dp"
+ android:layout_alignParentRight="true"
+ android:layout_below="@id/host"
+ android:layout_marginBottom="20dp"
+ android:layout_marginLeft="100dp"
+ android:layout_marginRight="30dp"
+ android:background="@drawable/button_background_action_orange_round"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:text="@string/promotion_add_to_homescreen"
+ android:maxLines="2"
+ android:ellipsize="end"
+ android:textColor="@android:color/white"
+ android:textSize="16sp" />
+
+ </RelativeLayout>
+</merge>
diff --git a/mobile/android/services/src/main/res/layout/simple_helper_ui.xml b/mobile/android/services/src/main/res/layout/simple_helper_ui.xml
new file mode 100644
index 0000000000..f549d5c31f
--- /dev/null
+++ b/mobile/android/services/src/main/res/layout/simple_helper_ui.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipChildren="false"
+ android:clipToPadding="false">
+
+ <LinearLayout
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@android:color/white"
+ android:layout_gravity="bottom|center"
+ android:clickable="true"
+ android:orientation="vertical">
+
+ <ImageView
+ android:id="@+id/image"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="40dp"
+ android:layout_marginBottom="40dp"
+ android:scaleType="fitCenter"
+ android:layout_gravity="center"
+ android:adjustViewBounds="true"/>
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="@dimen/firstrun_content_width"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:textAppearance="@style/TextAppearance.FirstrunLight.Main"/>
+
+
+ <TextView
+ android:id="@+id/message"
+ android:layout_width="@dimen/firstrun_content_width"
+ android:layout_height="wrap_content"
+ android:paddingTop="20dp"
+ android:paddingBottom="30dp"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:textAppearance="@style/TextAppearance.FirstrunRegular.Body"
+ android:singleLine="false"/>
+
+ <Button
+ android:id="@+id/button"
+ style="@style/Widget.Firstrun.Button"
+ android:background="@drawable/button_background_action_orange_round"
+ android:layout_gravity="center"
+ android:layout_marginBottom="30dp"/>
+
+ </LinearLayout>
+</merge>
diff --git a/mobile/android/services/src/main/res/menu/fxaccount_status_menu.xml b/mobile/android/services/src/main/res/menu/fxaccount_status_menu.xml
new file mode 100644
index 0000000000..16f72a7ca1
--- /dev/null
+++ b/mobile/android/services/src/main/res/menu/fxaccount_status_menu.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item
+ android:id="@+id/enable_debug_mode"
+ android:checkable="true"
+ android:checked="false"
+ android:title="@string/fxaccount_enable_debug_mode" />
+</menu>
diff --git a/mobile/android/services/src/main/res/values-v11/fxaccount_styles.xml b/mobile/android/services/src/main/res/values-v11/fxaccount_styles.xml
new file mode 100644
index 0000000000..5c0a23db5d
--- /dev/null
+++ b/mobile/android/services/src/main/res/values-v11/fxaccount_styles.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!-- FxAccountStatusActivity ActionBar -->
+ <style name="ActionBar.FxAccountStatusActivity">
+ <item name="android:displayOptions">showHome|homeAsUp|showTitle</item>
+ </style>
+
+ <style name="FxAccountTheme" parent="Gecko.Preferences" />
+
+ <style name="FxAccountTheme.FxAccountStatusActivity" parent="Gecko.Preferences">
+ <item name="android:actionBarStyle">@style/ActionBar.FxAccountStatusActivity</item>
+ </style>
+
+</resources>
diff --git a/mobile/android/services/src/main/res/values/fxaccount_colors.xml b/mobile/android/services/src/main/res/values/fxaccount_colors.xml
new file mode 100644
index 0000000000..f7140faff4
--- /dev/null
+++ b/mobile/android/services/src/main/res/values/fxaccount_colors.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <color name="fxaccount_textColor">#424f59</color>
+ <color name="fxaccount_error_preference_backgroundcolor">#fad4d2</color>
+</resources>
diff --git a/mobile/android/services/src/main/res/values/fxaccount_dimens.xml b/mobile/android/services/src/main/res/values/fxaccount_dimens.xml
new file mode 100644
index 0000000000..d1d44585d7
--- /dev/null
+++ b/mobile/android/services/src/main/res/values/fxaccount_dimens.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+ <!-- Preference fragment padding, bottom -->
+ <dimen name="preference_fragment_padding_bottom">0dp</dimen>
+ <!-- Preference fragment padding, sides -->
+ <dimen name="preference_fragment_padding_side">16dp</dimen>
+
+ <integer name="preference_fragment_scrollbarStyle">0x02000000</integer> <!-- outsideOverlay -->
+
+ <!-- Profile avatar image height. -->
+ <dimen name="fxaccount_profile_image_height">48dp</dimen>
+ <!-- Profile avatar image width. -->
+ <dimen name="fxaccount_profile_image_width">48dp</dimen>
+</resources>
diff --git a/mobile/android/services/src/main/res/values/fxaccount_styles.xml b/mobile/android/services/src/main/res/values/fxaccount_styles.xml
new file mode 100644
index 0000000000..d74efac91a
--- /dev/null
+++ b/mobile/android/services/src/main/res/values/fxaccount_styles.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <style name="FxAccountTheme" parent="Gecko.Preferences" />
+
+ <style name="FxAccountTheme.FxAccountStatusActivity" parent="@style/FxAccountTheme">
+ <item name="android:windowNoTitle">false</item>
+ </style>
+
+ <style name="FxAccountTextItem" parent="@android:style/TextAppearance.Medium">
+ <item name="android:textColor">@color/fxaccount_textColor</item>
+ <item name="android:layout_width">fill_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:gravity">center_horizontal</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:layout_marginBottom">10dp</item>
+ <item name="android:layout_marginLeft">10dp</item>
+ <item name="android:layout_marginRight">10dp</item>
+ </style>
+
+</resources>
diff --git a/mobile/android/services/src/main/res/xml/fxaccount_authenticator.xml b/mobile/android/services/src/main/res/xml/fxaccount_authenticator.xml
new file mode 100644
index 0000000000..7b004e2091
--- /dev/null
+++ b/mobile/android/services/src/main/res/xml/fxaccount_authenticator.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:accountType="@string/moz_android_shared_fxaccount_type"
+ android:icon="@drawable/icon"
+ android:smallIcon="@drawable/icon"
+ android:label="@string/fxaccount_label"
+ android:accountPreferences="@xml/fxaccount_options" />
diff --git a/mobile/android/services/src/main/res/xml/fxaccount_options.xml b/mobile/android/services/src/main/res/xml/fxaccount_options.xml
new file mode 100644
index 0000000000..449fc05455
--- /dev/null
+++ b/mobile/android/services/src/main/res/xml/fxaccount_options.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+ <PreferenceCategory
+ android:title="@string/fxaccount_options_title" />
+ <PreferenceScreen
+ android:key="options"
+ android:title="@string/fxaccount_options_configure_title">
+ <intent
+ android:action="android.intent.action.MAIN"
+ android:targetPackage="@string/android_package_name_for_ui"
+ android:targetClass="org.mozilla.gecko.fxa.activities.FxAccountStatusActivity">
+ </intent>
+ </PreferenceScreen>
+</PreferenceScreen>
diff --git a/mobile/android/services/src/main/res/xml/fxaccount_status_prefscreen.xml b/mobile/android/services/src/main/res/xml/fxaccount_status_prefscreen.xml
new file mode 100644
index 0000000000..570e362cc2
--- /dev/null
+++ b/mobile/android/services/src/main/res/xml/fxaccount_status_prefscreen.xml
@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:gecko="http://schemas.android.com/apk/res-auto"
+ android:key="status_screen">
+
+ <PreferenceCategory
+ android:key="signed_in_as_category"
+ android:title="@string/fxaccount_status_signed_in_as" >
+ <Preference
+ android:editable="false"
+ android:key="profile"
+ android:icon="@drawable/sync_avatar_default"
+ android:persistent="false"
+ android:title="" />
+ <Preference
+ android:editable="false"
+ android:key="manage_account"
+ android:persistent="false"
+ android:title="@string/fxaccount_status_manage_account" />
+ <Preference
+ android:editable="false"
+ android:key="auth_server"
+ android:persistent="false"
+ android:title="@string/fxaccount_status_auth_server" />
+ </PreferenceCategory>
+ <PreferenceCategory
+ android:key="sync_category"
+ android:title="@string/fxaccount_status_sync" >
+ <Preference
+ android:editable="false"
+ android:icon="@drawable/fxaccount_sync_error"
+ android:key="needs_credentials"
+ android:layout="@layout/fxaccount_status_error_preference"
+ android:persistent="false"
+ android:title="@string/fxaccount_status_needs_credentials" />
+ <Preference
+ android:editable="false"
+ android:icon="@drawable/fxaccount_sync_error"
+ android:key="needs_upgrade"
+ android:layout="@layout/fxaccount_status_error_preference"
+ android:persistent="false"
+ android:title="@string/fxaccount_status_needs_upgrade" />
+ <Preference
+ android:editable="false"
+ android:icon="@drawable/fxaccount_sync_error"
+ android:key="needs_verification"
+ android:layout="@layout/fxaccount_status_error_preference"
+ android:persistent="false"
+ android:title="@string/fxaccount_status_needs_verification" />
+ <Preference
+ android:editable="false"
+ android:icon="@drawable/fxaccount_sync_error"
+ android:key="needs_master_sync_automatically_enabled"
+ android:layout="@layout/fxaccount_status_error_preference"
+ android:persistent="false"
+ android:title="@string/fxaccount_status_needs_master_sync_automatically_enabled" />
+ <Preference
+ android:editable="false"
+ android:icon="@drawable/fxaccount_sync_error"
+ android:key="needs_finish_migrating"
+ android:layout="@layout/fxaccount_status_error_preference"
+ android:persistent="false"
+ android:title="@string/fxaccount_status_needs_finish_migrating" />
+
+ <Preference
+ android:editable="false"
+ android:key="sync_now"
+ android:defaultValue=""
+ android:persistent="false"
+ android:title="@string/fxaccount_status_sync_now"
+ android:summary="" />
+
+ <CheckBoxPreference
+ android:key="bookmarks"
+ android:persistent="false"
+ android:title="@string/fxaccount_status_bookmarks" />
+ <CheckBoxPreference
+ android:key="history"
+ android:persistent="false"
+ android:title="@string/fxaccount_status_history" />
+ <CheckBoxPreference
+ android:key="tabs"
+ android:persistent="false"
+ android:title="@string/fxaccount_status_tabs" />
+ <CheckBoxPreference
+ android:key="passwords"
+ android:persistent="false"
+ android:title="@string/fxaccount_status_passwords" />
+
+ <EditTextPreference
+ android:singleLine="true"
+ android:key="device_name"
+ android:persistent="false"
+ android:title="@string/fxaccount_status_device_name" />
+
+ <Preference
+ android:editable="false"
+ android:key="sync_server"
+ android:persistent="false"
+ android:title="@string/fxaccount_status_sync_server" />
+ <org.mozilla.gecko.fxa.activities.CustomColorPreference
+ android:editable="false"
+ android:key="remove_account"
+ android:persistent="false"
+ gecko:titleColor="@color/rejection_red"
+ android:title="@string/fxaccount_remove_account" />
+ <Preference
+ android:editable="false"
+ android:key="more"
+ android:persistent="false"
+ android:title="@string/fxaccount_status_more" />
+
+ </PreferenceCategory>
+ <PreferenceCategory
+ android:key="legal_category"
+ android:title="@string/fxaccount_status_legal" >
+ <Preference
+ android:editable="false"
+ android:key="linktos"
+ android:persistent="false"
+ android:title="@string/fxaccount_status_linktos" />
+ <Preference
+ android:editable="false"
+ android:key="linkprivacy"
+ android:persistent="false"
+ android:title="@string/fxaccount_status_linkprivacy" />
+ </PreferenceCategory>
+ <PreferenceCategory
+ android:key="debug_category" >
+ <Preference android:key="debug_refresh" />
+ <Preference android:key="debug_dump" />
+ <Preference android:key="debug_force_sync" />
+ <Preference android:key="debug_invalidate_certificate" />
+ <Preference android:key="debug_forget_certificate" />
+ <Preference android:key="debug_require_password" />
+ <Preference android:key="debug_require_upgrade" />
+ <Preference android:key="debug_migrated_from_sync11" />
+ <Preference android:key="debug_make_account_stage" />
+ <Preference android:key="debug_make_account_default" />
+ </PreferenceCategory>
+
+</PreferenceScreen>
diff --git a/mobile/android/services/src/main/res/xml/fxaccount_syncadapter.xml b/mobile/android/services/src/main/res/xml/fxaccount_syncadapter.xml
new file mode 100644
index 0000000000..761920667a
--- /dev/null
+++ b/mobile/android/services/src/main/res/xml/fxaccount_syncadapter.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+ android:accountType="@string/moz_android_shared_fxaccount_type"
+ android:contentAuthority="@string/content_authority_db_browser"
+ android:isAlwaysSyncable="true"
+ android:supportsUploading="true"
+ android:userVisible="true"
+/>
diff --git a/mobile/android/services/strings.xml.in b/mobile/android/services/strings.xml.in
new file mode 100644
index 0000000000..143a3db424
--- /dev/null
+++ b/mobile/android/services/strings.xml.in
@@ -0,0 +1,86 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- Configure Engines -->
+<string name="sync_configure_engines_title_passwords">&sync.configure.engines.title.passwords2;</string>
+<string name="sync_configure_engines_title_history">&sync.configure.engines.title.history;</string>
+<string name="sync_configure_engines_title_tabs">&sync.configure.engines.title.tabs;</string>
+
+<!-- Bookmark folder strings -->
+<string name="bookmarks_folder_menu">&bookmarks.folder.menu.label;</string>
+<string name="bookmarks_folder_places">&bookmarks.folder.places.label;</string>
+<string name="bookmarks_folder_tags">&bookmarks.folder.tags.label;</string>
+<string name="bookmarks_folder_toolbar">&bookmarks.folder.toolbar.label;</string>
+<string name="bookmarks_folder_unfiled">&bookmarks.folder.other.label;</string>
+<string name="bookmarks_folder_desktop">&bookmarks.folder.desktop.label;</string>
+<string name="bookmarks_folder_mobile">&bookmarks.folder.mobile.label;</string>
+<string name="bookmarks_folder_pinned">&bookmarks.folder.pinned.label;</string>
+
+<!-- Send tab to device. -->
+<string name="sync_default_client_name">&sync.default.client.name;</string>
+
+<!-- Firefox Account links. -->
+<string name="fxaccount_link_tos">https://accounts.firefox.com/legal/terms</string>
+<string name="fxaccount_link_pn">https://accounts.firefox.com/legal/privacy</string>
+
+<string name="fxaccount_getting_started_welcome_to_sync">&fxaccount_getting_started_welcome_to_sync;</string>
+<string name="fxaccount_getting_started_description">&fxaccount_getting_started_description2;</string>
+<string name="fxaccount_getting_started_get_started">&fxaccount_getting_started_get_started;</string>
+
+<string name="fxaccount_status_activity_label">&syncBrand.shortName.label;</string>
+<string name="fxaccount_status_signed_in_as">&fxaccount_status_signed_in_as;</string>
+<string name="fxaccount_status_manage_account">&fxaccount_status_manage_account;</string>
+<string name="fxaccount_status_auth_server">&fxaccount_status_auth_server;</string>
+<string name="fxaccount_status_sync_now">&fxaccount_status_sync_now;</string>
+<string name="fxaccount_status_syncing">&fxaccount_status_syncing2;</string>
+<string name="fxaccount_status_last_synced">&remote_tabs_last_synced;</string>
+<string name="fxaccount_status_never_synced">&remote_tabs_never_synced;</string>
+<string name="fxaccount_status_device_name">&fxaccount_status_device_name;</string>
+<string name="fxaccount_status_sync_server">&fxaccount_status_sync_server;</string>
+<string name="fxaccount_status_sync">&fxaccount_status_sync;</string>
+<string name="fxaccount_status_sync_enabled">&fxaccount_status_sync_enabled;</string>
+<string name="fxaccount_status_needs_verification">&fxaccount_status_needs_verification2;</string>
+<string name="fxaccount_status_needs_credentials">&fxaccount_status_needs_credentials;</string>
+<string name="fxaccount_status_needs_upgrade">&fxaccount_status_needs_upgrade;</string>
+<string name="fxaccount_status_needs_master_sync_automatically_enabled">&fxaccount_status_needs_master_sync_automatically_enabled;</string>
+<string name="fxaccount_status_needs_master_sync_automatically_enabled_v21">&fxaccount_status_needs_master_sync_automatically_enabled_v21;</string>
+<string name="fxaccount_status_needs_finish_migrating">&fxaccount_status_needs_finish_migrating;</string>
+<string name="fxaccount_status_bookmarks">&fxaccount_status_bookmarks;</string>
+<string name="fxaccount_status_history">&fxaccount_status_history;</string>
+<string name="fxaccount_status_passwords">&fxaccount_status_passwords2;</string>
+<string name="fxaccount_status_tabs">&fxaccount_status_tabs;</string>
+<string name="fxaccount_status_legal">&fxaccount_status_legal;</string>
+<string name="fxaccount_status_linktos">&fxaccount_status_linktos2;</string>
+<string name="fxaccount_status_linkprivacy">&fxaccount_status_linkprivacy2;</string>
+<string name="fxaccount_status_more">&fxaccount_status_more;</string>
+<string name="fxaccount_remove_account">&fxaccount_remove_account;</string>
+
+<string name="fxaccount_label">&fxaccount_account_type_label;</string>
+
+<string name="fxaccount_options_title">&fxaccount_options_title;</string>
+<string name="fxaccount_options_configure_title">&fxaccount_options_configure_title;</string>
+
+<string name="fxaccount_remote_error_UPGRADE_REQUIRED">&fxaccount_remote_error_UPGRADE_REQUIRED;</string>
+<string name="fxaccount_remote_error_ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS">&fxaccount_remote_error_ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS_2;</string>
+<string name="fxaccount_remote_error_ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST">&fxaccount_remote_error_ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST;</string>
+<string name="fxaccount_remote_error_INCORRECT_PASSWORD">&fxaccount_remote_error_INCORRECT_PASSWORD;</string>
+<string name="fxaccount_remote_error_ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT">&fxaccount_remote_error_ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT;</string>
+<string name="fxaccount_remote_error_CLIENT_HAS_SENT_TOO_MANY_REQUESTS">&fxaccount_remote_error_CLIENT_HAS_SENT_TOO_MANY_REQUESTS;</string>
+<string name="fxaccount_remote_error_SERVICE_TEMPORARILY_UNAVAILABLE_TO_DUE_HIGH_LOAD">&fxaccount_remote_error_SERVICE_TEMPORARILY_UNAVAILABLE_TO_DUE_HIGH_LOAD;</string>
+<string name="fxaccount_remote_error_UNKNOWN_ERROR">&fxaccount_remote_error_UNKNOWN_ERROR;</string>
+<string name="fxaccount_remote_error_ACCOUNT_LOCKED">&fxaccount_remote_error_ACCOUNT_LOCKED;</string>
+
+<string name="fxaccount_sync_sign_in_error_notification_title">&fxaccount_sync_sign_in_error_notification_title2;</string>
+<string name="fxaccount_sync_sign_in_error_notification_text">&fxaccount_sync_sign_in_error_notification_text2;</string>
+
+<!-- Remove Account -->
+<string name="fxaccount_remove_account_dialog_title">&fxaccount_remove_account_dialog_title;</string>
+<string name="fxaccount_remove_account_dialog_message">&fxaccount_remove_account_dialog_message;</string>
+<string name="fxaccount_remove_account_toast">&fxaccount_remove_account_toast;</string>
+
+<string name="fxaccount_sync_finish_migrating_notification_title">&fxaccount_sync_finish_migrating_notification_title;</string>
+<string name="fxaccount_sync_finish_migrating_notification_text">&fxaccount_sync_finish_migrating_notification_text;</string>
+
+<!-- Log Personal information -->
+<string name="fxaccount_enable_debug_mode">&fxaccount_enable_debug_mode;</string>
diff --git a/mobile/android/stumbler/Makefile.in b/mobile/android/stumbler/Makefile.in
new file mode 100644
index 0000000000..958d503621
--- /dev/null
+++ b/mobile/android/stumbler/Makefile.in
@@ -0,0 +1,9 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+include $(topsrcdir)/config/rules.mk
+
+include $(topsrcdir)/config/android-common.mk
+
+libs:: stumbler.jar
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/AppGlobals.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/AppGlobals.java
new file mode 100644
index 0000000000..11a3bf4e01
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/AppGlobals.java
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service;
+
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+public class AppGlobals {
+ public static final String LOG_PREFIX = "Stumbler_";
+
+ /* All intent actions start with this string. Only locally broadcasted. */
+ public static final String ACTION_NAMESPACE = "org.mozilla.mozstumbler.intent.action";
+
+ /* Handle this for logging reporter info. */
+ public static final String ACTION_GUI_LOG_MESSAGE = AppGlobals.ACTION_NAMESPACE + ".LOG_MESSAGE";
+ public static final String ACTION_GUI_LOG_MESSAGE_EXTRA = ACTION_GUI_LOG_MESSAGE + ".MESSAGE";
+
+ /* Defined here so that the Reporter class can access the time of an Intent in a generic fashion.
+ * Classes should have their own constant that is assigned to this, for example,
+ * WifiScanner has ACTION_WIFIS_SCANNED_ARG_TIME = ACTION_ARG_TIME.
+ * This member definition in the broadcaster makes it clear what the extra Intent args are for that class. */
+ public static final String ACTION_ARG_TIME = "time";
+
+ /* Location constructor requires a named origin, these are created in the app. */
+ public static final String LOCATION_ORIGIN_INTERNAL = "internal";
+
+ public enum ActiveOrPassiveStumbling { ACTIVE_STUMBLING, PASSIVE_STUMBLING }
+
+ /* In passive mode, only scan this many times for each gps. */
+ public static final int PASSIVE_MODE_MAX_SCANS_PER_GPS = 3;
+
+ /* These are set on startup. The appVersionName and code are not used in the service-only case. */
+ public static String appVersionName = "0.0.0";
+ public static int appVersionCode = 0;
+ public static String appName = "StumblerService";
+ public static boolean isDebug;
+
+ /* The log activity will clear this periodically, and display the messages.
+ * Always null when the stumbler service is used stand-alone. */
+ public static volatile ConcurrentLinkedQueue<String> guiLogMessageBuffer;
+
+ public static void guiLogError(String msg) {
+ guiLogInfo(msg, "red", true);
+ }
+
+ public static void guiLogInfo(String msg) {
+ guiLogInfo(msg, "white", false);
+ }
+
+ public static void guiLogInfo(String msg, String color, boolean isBold) {
+ if (guiLogMessageBuffer != null) {
+ if (isBold) {
+ msg = "<b>" + msg + "</b>";
+ }
+ guiLogMessageBuffer.add("<font color='" + color +"'>" + msg + "</font>");
+ }
+ }
+
+ public static String makeLogTag(String name) {
+ final int maxLen = 23 - LOG_PREFIX.length();
+ if (name.length() > maxLen) {
+ name = name.substring(name.length() - maxLen, name.length());
+ }
+ return LOG_PREFIX + name;
+ }
+
+ public static final String ACTION_TEST_SETTING_ENABLED = "stumbler-test-setting-enabled";
+ public static final String ACTION_TEST_SETTING_DISABLED = "stumbler-test-setting-disabled";
+
+ // Histogram values
+ public static final String TELEMETRY_TIME_BETWEEN_UPLOADS_SEC = "STUMBLER_TIME_BETWEEN_UPLOADS_SEC";
+ public static final String TELEMETRY_BYTES_UPLOADED_PER_SEC = "STUMBLER_VOLUME_BYTES_UPLOADED_PER_SEC";
+ public static final String TELEMETRY_TIME_BETWEEN_STARTS_SEC = "STUMBLER_TIME_BETWEEN_START_SEC";
+ public static final String TELEMETRY_BYTES_PER_UPLOAD = "STUMBLER_UPLOAD_BYTES";
+ public static final String TELEMETRY_OBSERVATIONS_PER_UPLOAD = "STUMBLER_UPLOAD_OBSERVATION_COUNT";
+ public static final String TELEMETRY_CELLS_PER_UPLOAD = "STUMBLER_UPLOAD_CELL_COUNT";
+ public static final String TELEMETRY_WIFIS_PER_UPLOAD = "STUMBLER_UPLOAD_WIFI_AP_COUNT";
+ public static final String TELEMETRY_OBSERVATIONS_PER_DAY = "STUMBLER_OBSERVATIONS_PER_DAY";
+ public static final String TELEMETRY_TIME_BETWEEN_RECEIVED_LOCATIONS_SEC = "STUMBLER_TIME_BETWEEN_RECEIVED_LOCATIONS_SEC";
+}
+
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/Prefs.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/Prefs.java
new file mode 100644
index 0000000000..fa00f29e9b
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/Prefs.java
@@ -0,0 +1,205 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.location.Location;
+import android.os.Build.VERSION;
+import android.text.TextUtils;
+import android.util.Log;
+
+public final class Prefs {
+ private static final String LOG_TAG = AppGlobals.makeLogTag(Prefs.class.getSimpleName());
+ private static final String NICKNAME_PREF = "nickname";
+ private static final String USER_AGENT_PREF = "user-agent";
+ private static final String VALUES_VERSION_PREF = "values_version";
+ private static final String WIFI_ONLY = "wifi_only";
+ private static final String LAT_PREF = "lat_pref";
+ private static final String LON_PREF = "lon_pref";
+ private static final String GEOFENCE_HERE = "geofence_here";
+ private static final String GEOFENCE_SWITCH = "geofence_switch";
+ private static final String FIREFOX_SCAN_ENABLED = "firefox_scan_on";
+ private static final String MOZ_API_KEY = "moz_api_key";
+ private static final String WIFI_SCAN_ALWAYS = "wifi_scan_always";
+ private static final String LAST_ATTEMPTED_UPLOAD_TIME = "last_attempted_upload_time";
+ // Public for MozStumbler to use for manual upgrade of old prefs.
+ public static final String PREFS_FILE = Prefs.class.getSimpleName();
+
+ private final SharedPreferences mSharedPrefs;
+ static private Prefs sInstance;
+
+ private Prefs(Context context) {
+ mSharedPrefs = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
+ if (getPrefs().getInt(VALUES_VERSION_PREF, -1) != AppGlobals.appVersionCode) {
+ Log.i(LOG_TAG, "Version of the application has changed. Updating default values.");
+ // Remove old keys
+ getPrefs().edit()
+ .remove("reports")
+ .remove("power_saving_mode")
+ .commit();
+
+ getPrefs().edit().putInt(VALUES_VERSION_PREF, AppGlobals.appVersionCode).commit();
+ getPrefs().edit().commit();
+ }
+ }
+
+ public static Prefs getInstance(Context c) {
+ if (sInstance == null) {
+ sInstance = new Prefs(c);
+ }
+ return sInstance;
+ }
+
+ // Allows code without a context handle to grab the prefs. The caller must null check the return value.
+ public static Prefs getInstanceWithoutContext() {
+ return sInstance;
+ }
+
+ ///
+ /// Setters
+ ///
+ public synchronized void setUserAgent(String userAgent) {
+ setStringPref(USER_AGENT_PREF, userAgent);
+ }
+
+ public synchronized void setUseWifiOnly(boolean state) {
+ setBoolPref(WIFI_ONLY, state);
+ }
+
+ public synchronized void setGeofenceEnabled(boolean state) {
+ setBoolPref(GEOFENCE_SWITCH, state);
+ }
+
+ public synchronized void setGeofenceHere(boolean flag) {
+ setBoolPref(GEOFENCE_HERE, flag);
+ }
+
+ public synchronized void setGeofenceLocation(Location location) {
+ SharedPreferences.Editor editor = getPrefs().edit();
+ editor.putFloat(LAT_PREF, (float) location.getLatitude());
+ editor.putFloat(LON_PREF, (float) location.getLongitude());
+ apply(editor);
+ }
+
+ public synchronized void setMozApiKey(String s) {
+ setStringPref(MOZ_API_KEY, s);
+ }
+
+ ///
+ /// Getters
+ ///
+ public synchronized String getUserAgent() {
+ String s = getStringPref(USER_AGENT_PREF);
+ return (s == null)? AppGlobals.appName + "/" + AppGlobals.appVersionName : s;
+ }
+
+ public synchronized boolean getFirefoxScanEnabled() {
+ return getBoolPrefWithDefault(FIREFOX_SCAN_ENABLED, false);
+ }
+
+ public synchronized String getMozApiKey() {
+ String s = getStringPref(MOZ_API_KEY);
+ return (s == null)? "no-mozilla-api-key" : s;
+ }
+
+ public synchronized boolean getGeofenceEnabled() {
+ return getBoolPrefWithDefault(GEOFENCE_SWITCH, false);
+ }
+
+ public synchronized boolean getGeofenceHere() {
+ return getBoolPrefWithDefault(GEOFENCE_HERE, false);
+ }
+
+ public synchronized Location getGeofenceLocation() {
+ Location loc = new Location(AppGlobals.LOCATION_ORIGIN_INTERNAL);
+ loc.setLatitude(getPrefs().getFloat(LAT_PREF, 0));
+ loc.setLongitude(getPrefs().getFloat(LON_PREF,0));
+ return loc;
+ }
+
+ // This is the time an upload was last attempted, not necessarily successful.
+ // Used to ensure upload attempts aren't happening too frequently.
+ public synchronized long getLastAttemptedUploadTime() {
+ return getPrefs().getLong(LAST_ATTEMPTED_UPLOAD_TIME, 0);
+ }
+
+ public synchronized String getNickname() {
+ String nickname = getStringPref(NICKNAME_PREF);
+ if (nickname != null) {
+ nickname = nickname.trim();
+ }
+ return TextUtils.isEmpty(nickname) ? null : nickname;
+ }
+
+ public synchronized void setFirefoxScanEnabled(boolean on) {
+ setBoolPref(FIREFOX_SCAN_ENABLED, on);
+ }
+
+ public synchronized void setLastAttemptedUploadTime(long time) {
+ SharedPreferences.Editor editor = getPrefs().edit();
+ editor.putLong(LAST_ATTEMPTED_UPLOAD_TIME, time);
+ apply(editor);
+ }
+
+ public synchronized void setNickname(String nick) {
+ if (nick != null) {
+ nick = nick.trim();
+ if (nick.length() > 0) {
+ setStringPref(NICKNAME_PREF, nick);
+ }
+ }
+ }
+
+ public synchronized boolean getUseWifiOnly() {
+ return getBoolPrefWithDefault(WIFI_ONLY, true);
+ }
+
+ public synchronized boolean getWifiScanAlways() {
+ return getBoolPrefWithDefault(WIFI_SCAN_ALWAYS, false);
+ }
+
+ public synchronized void setWifiScanAlways(boolean b) {
+ setBoolPref(WIFI_SCAN_ALWAYS, b);
+ }
+
+ ///
+ /// Privates
+ ///
+
+ private String getStringPref(String key) {
+ return getPrefs().getString(key, null);
+ }
+
+ private boolean getBoolPrefWithDefault(String key, boolean def) {
+ return getPrefs().getBoolean(key, def);
+ }
+
+ private void setBoolPref(String key, Boolean state) {
+ SharedPreferences.Editor editor = getPrefs().edit();
+ editor.putBoolean(key,state);
+ apply(editor);
+ }
+
+ private void setStringPref(String key, String value) {
+ SharedPreferences.Editor editor = getPrefs().edit();
+ editor.putString(key, value);
+ apply(editor);
+ }
+
+ @TargetApi(9)
+ private static void apply(SharedPreferences.Editor editor) {
+ if (VERSION.SDK_INT >= 9) {
+ editor.apply();
+ } else if (!editor.commit()) {
+ Log.e(LOG_TAG, "", new IllegalStateException("commit() failed?!"));
+ }
+ }
+
+ private SharedPreferences getPrefs() {
+ return mSharedPrefs;
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/LocalPreferenceReceiver.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/LocalPreferenceReceiver.java
new file mode 100644
index 0000000000..388abba15a
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/LocalPreferenceReceiver.java
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.mainthread;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import org.mozilla.mozstumbler.service.AppGlobals;
+import org.mozilla.mozstumbler.service.stumblerthread.StumblerService;
+
+/**
+ * Starts the StumblerService, an Intent service, which by definition runs on its own thread.
+ * Registered as a local broadcast receiver in SafeReceiver.
+ * Starts the StumblerService in passive listening mode.
+ *
+ * The received intent contains enabled state, upload API key and user agent,
+ * and is used to initialize the StumblerService.
+ */
+public class LocalPreferenceReceiver extends BroadcastReceiver {
+ // This allows global debugging logs to be enabled by doing
+ // |adb shell setprop log.tag.PassiveStumbler DEBUG|
+ static final String LOG_TAG = "PassiveStumbler";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent == null) {
+ return;
+ }
+
+ // This value is cached, so if |setprop| is performed (as described on the LOG_TAG above),
+ // then the start/stop intent must be resent by toggling the setting or stopping/starting Fennec.
+ // This does not guard against dumping PII (PII in stumbler is location, wifi BSSID, cell tower details).
+ AppGlobals.isDebug = Log.isLoggable(LOG_TAG, Log.DEBUG);
+
+ StumblerService.sFirefoxStumblingEnabled.set(intent.getBooleanExtra("enabled", false));
+
+ if (!StumblerService.sFirefoxStumblingEnabled.get()) {
+ Log.d(LOG_TAG, "Stopping StumblerService | isDebug:" + AppGlobals.isDebug);
+ // This calls the service's onDestroy(), and the service's onHandleIntent(...) is not called
+ context.stopService(new Intent(context, StumblerService.class));
+ // For testing service messages were received
+ context.sendBroadcast(new Intent(AppGlobals.ACTION_TEST_SETTING_DISABLED));
+ return;
+ }
+
+ // For testing service messages were received
+ context.sendBroadcast(new Intent(AppGlobals.ACTION_TEST_SETTING_ENABLED));
+
+ Log.d(LOG_TAG, "Sending passive start message | isDebug:" + AppGlobals.isDebug);
+
+ final Intent startServiceIntent = new Intent(context, StumblerService.class);
+
+ startServiceIntent.putExtra(StumblerService.ACTION_START_PASSIVE, true);
+ startServiceIntent.putExtra(
+ StumblerService.ACTION_EXTRA_MOZ_API_KEY,
+ intent.getStringExtra("moz_mozilla_api_key")
+ );
+ startServiceIntent.putExtra(
+ StumblerService.ACTION_EXTRA_USER_AGENT,
+ intent.getStringExtra("user_agent")
+ );
+
+ context.startService(startServiceIntent);
+ }
+}
+
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/SafeReceiver.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/SafeReceiver.java
new file mode 100644
index 0000000000..e145dbb0f6
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/SafeReceiver.java
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.mainthread;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.support.v4.content.LocalBroadcastManager;
+import android.util.Log;
+
+/**
+ * Responsible for registering LocalPreferenceReceiver as a receiver with LocalBroadcastManager.
+ * This receiver is registered in the AndroidManifest.xml
+ */
+public class SafeReceiver extends BroadcastReceiver {
+ static final String LOG_TAG = "StumblerSafeReceiver";
+ static final String PREFERENCE_INTENT_FILTER = "STUMBLER_PREF";
+
+ private boolean registeredLocalReceiver = false;
+
+ @Override
+ public synchronized void onReceive(Context context, Intent intent) {
+ if (intent == null) {
+ return;
+ }
+
+ if (registeredLocalReceiver) {
+ return;
+ }
+
+ LocalBroadcastManager.getInstance(context).registerReceiver(
+ new LocalPreferenceReceiver(),
+ new IntentFilter(PREFERENCE_INTENT_FILTER)
+ );
+
+ Log.d(LOG_TAG, "Registered local preference listener");
+
+ registeredLocalReceiver = true;
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/SystemReceiver.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/SystemReceiver.java
new file mode 100644
index 0000000000..eaaab3423c
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/mainthread/SystemReceiver.java
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.mainthread;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.mozilla.mozstumbler.service.stumblerthread.StumblerService;
+
+/**
+ * Responsible for starting StumblerService in response to
+ * BOOT_COMPLETE and EXTERNAL_APPLICATIONS_AVAILABLE system intents.
+ */
+public class SystemReceiver extends BroadcastReceiver {
+ static final String LOG_TAG = "StumblerSystemReceiver";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent == null) {
+ return;
+ }
+
+ final String action = intent.getAction();
+
+ if (!TextUtils.equals(action, Intent.ACTION_BOOT_COMPLETED) && !TextUtils.equals(action, Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE)) {
+ // This is not the broadcast you are looking for.
+ return;
+ }
+
+ final Intent startServiceIntent = new Intent(context, StumblerService.class);
+ startServiceIntent.putExtra(StumblerService.ACTION_NOT_FROM_HOST_APP, true);
+ context.startService(startServiceIntent);
+
+ Log.d(LOG_TAG, "Responded to a system intent");
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/Reporter.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/Reporter.java
new file mode 100644
index 0000000000..8f7f19c8d2
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/Reporter.java
@@ -0,0 +1,219 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.stumblerthread;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.location.Location;
+import android.net.wifi.ScanResult;
+import android.support.v4.content.LocalBroadcastManager;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.mozstumbler.service.AppGlobals;
+import org.mozilla.mozstumbler.service.stumblerthread.datahandling.DataStorageContract;
+import org.mozilla.mozstumbler.service.stumblerthread.datahandling.DataStorageManager;
+import org.mozilla.mozstumbler.service.stumblerthread.datahandling.StumblerBundle;
+import org.mozilla.mozstumbler.service.stumblerthread.scanners.cellscanner.CellInfo;
+import org.mozilla.mozstumbler.service.stumblerthread.scanners.cellscanner.CellScanner;
+import org.mozilla.mozstumbler.service.stumblerthread.scanners.GPSScanner;
+import org.mozilla.mozstumbler.service.stumblerthread.scanners.WifiScanner;
+
+public final class Reporter extends BroadcastReceiver {
+ private static final String LOG_TAG = AppGlobals.makeLogTag(Reporter.class.getSimpleName());
+ public static final String ACTION_FLUSH_TO_BUNDLE = AppGlobals.ACTION_NAMESPACE + ".FLUSH";
+ public static final String ACTION_NEW_BUNDLE = AppGlobals.ACTION_NAMESPACE + ".NEW_BUNDLE";
+ private boolean mIsStarted;
+
+ /* The maximum number of Wi-Fi access points in a single observation. */
+ private static final int MAX_WIFIS_PER_LOCATION = 200;
+
+ /* The maximum number of cells in a single observation */
+ private static final int MAX_CELLS_PER_LOCATION = 50;
+
+ private Context mContext;
+ private int mPhoneType;
+
+ private StumblerBundle mBundle;
+
+ Reporter() {}
+
+ private void resetData() {
+ mBundle = null;
+ }
+
+ public void flush() {
+ reportCollectedLocation();
+ }
+
+ void startup(Context context) {
+ if (mIsStarted) {
+ return;
+ }
+
+ mContext = context.getApplicationContext();
+ TelephonyManager tm = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+ if (tm != null) {
+ mPhoneType = tm.getPhoneType();
+ } else {
+ Log.d(LOG_TAG, "No telephony manager.");
+ mPhoneType = TelephonyManager.PHONE_TYPE_NONE;
+ }
+
+ mIsStarted = true;
+
+ resetData();
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(WifiScanner.ACTION_WIFIS_SCANNED);
+ intentFilter.addAction(CellScanner.ACTION_CELLS_SCANNED);
+ intentFilter.addAction(GPSScanner.ACTION_GPS_UPDATED);
+ intentFilter.addAction(ACTION_FLUSH_TO_BUNDLE);
+ LocalBroadcastManager.getInstance(mContext).registerReceiver(this,
+ intentFilter);
+ }
+
+ void shutdown() {
+ if (mContext == null) {
+ return;
+ }
+
+ mIsStarted = false;
+
+ Log.d(LOG_TAG, "shutdown");
+ flush();
+ LocalBroadcastManager.getInstance(mContext).unregisterReceiver(this);
+ }
+
+ private void receivedWifiMessage(Intent intent) {
+ List<ScanResult> results = intent.getParcelableArrayListExtra(WifiScanner.ACTION_WIFIS_SCANNED_ARG_RESULTS);
+ putWifiResults(results);
+ }
+
+ private void receivedCellMessage(Intent intent) {
+ List<CellInfo> results = intent.getParcelableArrayListExtra(CellScanner.ACTION_CELLS_SCANNED_ARG_CELLS);
+ putCellResults(results);
+ }
+
+ private void receivedGpsMessage(Intent intent) {
+ String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
+ if (GPSScanner.SUBJECT_NEW_LOCATION.equals(subject)) {
+ reportCollectedLocation();
+ Location newPosition = intent.getParcelableExtra(GPSScanner.NEW_LOCATION_ARG_LOCATION);
+ mBundle = (newPosition != null) ? new StumblerBundle(newPosition, mPhoneType) : mBundle;
+ }
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+
+ switch (action) {
+ case ACTION_FLUSH_TO_BUNDLE:
+ flush();
+ return;
+ case WifiScanner.ACTION_WIFIS_SCANNED:
+ receivedWifiMessage(intent);
+ break;
+ case CellScanner.ACTION_CELLS_SCANNED:
+ receivedCellMessage(intent);
+ break;
+ case GPSScanner.ACTION_GPS_UPDATED:
+ // Calls reportCollectedLocation, this is the ideal case
+ receivedGpsMessage(intent);
+ break;
+ }
+
+ if (mBundle != null &&
+ (mBundle.getWifiData().size() > MAX_WIFIS_PER_LOCATION ||
+ mBundle.getCellData().size() > MAX_CELLS_PER_LOCATION)) {
+ // no gps for a while, have too much data, just bundle it
+ reportCollectedLocation();
+ }
+ }
+
+ private void putWifiResults(List<ScanResult> results) {
+ if (mBundle == null) {
+ return;
+ }
+
+ Map<String, ScanResult> currentWifiData = mBundle.getWifiData();
+ for (ScanResult result : results) {
+ if (currentWifiData.size() > MAX_WIFIS_PER_LOCATION) {
+ return;
+ }
+
+ String key = result.BSSID;
+ if (!currentWifiData.containsKey(key)) {
+ currentWifiData.put(key, result);
+ }
+ }
+ }
+
+ private void putCellResults(List<CellInfo> cells) {
+ if (mBundle == null) {
+ return;
+ }
+
+ Map<String, CellInfo> currentCellData = mBundle.getCellData();
+ for (CellInfo result : cells) {
+ if (currentCellData.size() > MAX_CELLS_PER_LOCATION) {
+ return;
+ }
+ String key = result.getCellIdentity();
+ if (!currentCellData.containsKey(key)) {
+ currentCellData.put(key, result);
+ }
+ }
+ }
+
+ private void reportCollectedLocation() {
+ if (mBundle == null) {
+ return;
+ }
+
+ storeBundleAsJSON(mBundle);
+
+ mBundle.wasSent();
+ }
+
+ private void storeBundleAsJSON(StumblerBundle bundle) {
+ JSONObject mlsObj;
+ int wifiCount = 0;
+ int cellCount = 0;
+ try {
+ mlsObj = bundle.toMLSJSON();
+ wifiCount = mlsObj.getInt(DataStorageContract.ReportsColumns.WIFI_COUNT);
+ cellCount = mlsObj.getInt(DataStorageContract.ReportsColumns.CELL_COUNT);
+
+ } catch (JSONException e) {
+ Log.w(LOG_TAG, "Failed to convert bundle to JSON: " + e);
+ return;
+ }
+
+ if (AppGlobals.isDebug) {
+ // PII: do not log the bundle without obfuscating it
+ Log.d(LOG_TAG, "Received bundle");
+ }
+
+ if (wifiCount + cellCount < 1) {
+ return;
+ }
+
+ try {
+ DataStorageManager.getInstance().insert(mlsObj.toString(), wifiCount, cellCount);
+ } catch (IOException e) {
+ Log.w(LOG_TAG, e.toString());
+ }
+ }
+}
+
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/StumblerService.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/StumblerService.java
new file mode 100644
index 0000000000..5d1a278b9e
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/StumblerService.java
@@ -0,0 +1,254 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.stumblerthread;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.location.Location;
+import android.os.AsyncTask;
+import android.support.v4.content.ContextCompat;
+import android.util.Log;
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.mozilla.mozstumbler.service.AppGlobals;
+import org.mozilla.mozstumbler.service.Prefs;
+import org.mozilla.mozstumbler.service.stumblerthread.blocklist.WifiBlockListInterface;
+import org.mozilla.mozstumbler.service.stumblerthread.datahandling.DataStorageManager;
+import org.mozilla.mozstumbler.service.stumblerthread.scanners.ScanManager;
+import org.mozilla.mozstumbler.service.uploadthread.UploadAlarmReceiver;
+import org.mozilla.mozstumbler.service.utils.PersistentIntentService;
+
+// In stand-alone service mode (a.k.a passive scanning mode), this is created from PassiveServiceReceiver (by calling startService).
+// The StumblerService is a sticky unbound service in this usage.
+//
+public class StumblerService extends PersistentIntentService
+ implements DataStorageManager.StorageIsEmptyTracker {
+ private static final String LOG_TAG = AppGlobals.makeLogTag(StumblerService.class.getSimpleName());
+ public static final String ACTION_BASE = AppGlobals.ACTION_NAMESPACE;
+ public static final String ACTION_START_PASSIVE = ACTION_BASE + ".START_PASSIVE";
+ public static final String ACTION_EXTRA_MOZ_API_KEY = ACTION_BASE + ".MOZKEY";
+ public static final String ACTION_EXTRA_USER_AGENT = ACTION_BASE + ".USER_AGENT";
+ public static final String ACTION_NOT_FROM_HOST_APP = ACTION_BASE + ".NOT_FROM_HOST";
+ public static final AtomicBoolean sFirefoxStumblingEnabled = new AtomicBoolean();
+ protected final ScanManager mScanManager = new ScanManager();
+ protected final Reporter mReporter = new Reporter();
+
+ // This is a delay before the single-shot upload is attempted. The number is arbitrary
+ // and used to avoid startup tasks bunching up.
+ private static final int DELAY_IN_SEC_BEFORE_STARTING_UPLOAD_IN_PASSIVE_MODE = 2;
+
+ // This is the frequency of the repeating upload alarm in active scanning mode.
+ private static final int FREQUENCY_IN_SEC_OF_UPLOAD_IN_ACTIVE_MODE = 5 * 60;
+
+ // Used to guard against attempting to upload too frequently in passive mode.
+ private static final long PASSIVE_UPLOAD_FREQ_GUARD_MSEC = 5 * 60 * 1000;
+
+ public StumblerService() {
+ this("StumblerService");
+ }
+
+ public StumblerService(String name) {
+ super(name);
+ }
+
+ public boolean isScanning() {
+ return mScanManager.isScanning();
+ }
+
+ public void startScanning() {
+ mScanManager.startScanning(this);
+ }
+
+ // This is optional, not used in Fennec, and is for clients to specify a (potentially long) list
+ // of blocklisted SSIDs/BSSIDs
+ public void setWifiBlockList(WifiBlockListInterface list) {
+ mScanManager.setWifiBlockList(list);
+ }
+
+ public Prefs getPrefs(Context c) {
+ return Prefs.getInstance(c);
+ }
+
+ public void checkPrefs() {
+ mScanManager.checkPrefs();
+ }
+
+ public int getLocationCount() {
+ return mScanManager.getLocationCount();
+ }
+
+ public double getLatitude() {
+ return mScanManager.getLatitude();
+ }
+
+ public double getLongitude() {
+ return mScanManager.getLongitude();
+ }
+
+ public Location getLocation() {
+ return mScanManager.getLocation();
+ }
+
+ public int getWifiStatus() {
+ return mScanManager.getWifiStatus();
+ }
+
+ public int getAPCount() {
+ return mScanManager.getAPCount();
+ }
+
+ public int getVisibleAPCount() {
+ return mScanManager.getVisibleAPCount();
+ }
+
+ public int getCellInfoCount() {
+ return mScanManager.getCellInfoCount();
+ }
+
+ public boolean isGeofenced () {
+ return mScanManager.isGeofenced();
+ }
+
+ // Previously this was done in onCreate(). Moved out of that so that in the passive standalone service
+ // use (i.e. Fennec), init() can be called from this class's dedicated thread.
+ // Safe to call more than once, ensure added code complies with that intent.
+ protected void init() {
+ // Ensure Prefs is created, so internal utility code can use getInstanceWithoutContext
+ Prefs.getInstance(this);
+ DataStorageManager.createGlobalInstance(this, this);
+
+ mReporter.startup(this);
+ }
+
+ // Called from the main thread.
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ setIntentRedelivery(true);
+ }
+
+ // Called from the main thread
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ if (!mScanManager.isScanning()) {
+ return;
+ }
+
+ // Used to move these disk I/O ops off the calling thread. The current operations here are synchronized,
+ // however instead of creating another thread (if onDestroy grew to have concurrency complications)
+ // we could be messaging the stumbler thread to perform a shutdown function.
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ if (AppGlobals.isDebug) {
+ Log.d(LOG_TAG, "onDestroy");
+ }
+
+ if (!sFirefoxStumblingEnabled.get()) {
+ Prefs.getInstance(StumblerService.this).setFirefoxScanEnabled(false);
+ }
+
+ if (DataStorageManager.getInstance() != null) {
+ try {
+ DataStorageManager.getInstance().saveCurrentReportsToDisk();
+ } catch (IOException ex) {
+ AppGlobals.guiLogInfo(ex.toString());
+ Log.e(LOG_TAG, "Exception in onDestroy saving reports" + ex.toString());
+ }
+ }
+ return null;
+ }
+ }.execute();
+
+ mReporter.shutdown();
+ mScanManager.stopScanning();
+ }
+
+ // This is the entry point for the stumbler thread.
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ // Do init() in all cases, there is no cost, whereas it is easy to add code that depends on this.
+ init();
+
+ // Post-init(), set the mode to passive.
+ mScanManager.setPassiveMode(true);
+
+ if (!hasLocationPermission()) {
+ Log.d(LOG_TAG, "Location permission not granted. Aborting.");
+ return;
+ }
+
+ if (intent == null) {
+ return;
+ }
+
+ final boolean isScanEnabledInPrefs = Prefs.getInstance(this).getFirefoxScanEnabled();
+
+ if (!isScanEnabledInPrefs && intent.getBooleanExtra(ACTION_NOT_FROM_HOST_APP, false)) {
+ stopSelf();
+ return;
+ }
+
+ boolean hasFilesWaiting = !DataStorageManager.getInstance().isDirEmpty();
+ if (AppGlobals.isDebug) {
+ Log.d(LOG_TAG, "Files waiting:" + hasFilesWaiting);
+ }
+ if (hasFilesWaiting) {
+ // non-empty on startup, schedule an upload
+ // This is the only upload trigger in Firefox mode
+ // Firefox triggers this ~4 seconds after startup (after Gecko is loaded), add a small delay to avoid
+ // clustering with other operations that are triggered at this time.
+ final long lastAttemptedTime = Prefs.getInstance(this).getLastAttemptedUploadTime();
+ final long timeNow = System.currentTimeMillis();
+
+ if (timeNow - lastAttemptedTime < PASSIVE_UPLOAD_FREQ_GUARD_MSEC) {
+ // TODO Consider telemetry to track this.
+ if (AppGlobals.isDebug) {
+ Log.d(LOG_TAG, "Upload attempt too frequent.");
+ }
+ } else {
+ Prefs.getInstance(this).setLastAttemptedUploadTime(timeNow);
+ UploadAlarmReceiver.scheduleAlarm(this, DELAY_IN_SEC_BEFORE_STARTING_UPLOAD_IN_PASSIVE_MODE, false /* no repeat*/);
+ }
+ }
+
+ if (!isScanEnabledInPrefs) {
+ Prefs.getInstance(this).setFirefoxScanEnabled(true);
+ }
+
+ String apiKey = intent.getStringExtra(ACTION_EXTRA_MOZ_API_KEY);
+ if (apiKey != null && !apiKey.equals(Prefs.getInstance(this).getMozApiKey())) {
+ Prefs.getInstance(this).setMozApiKey(apiKey);
+ }
+
+ String userAgent = intent.getStringExtra(ACTION_EXTRA_USER_AGENT);
+ if (userAgent != null && !userAgent.equals(Prefs.getInstance(this).getUserAgent())) {
+ Prefs.getInstance(this).setUserAgent(userAgent);
+ }
+
+ if (!mScanManager.isScanning()) {
+ startScanning();
+ }
+ }
+
+ // Note that in passive mode, having data isn't an upload trigger, it is triggered by the start intent
+ @Override
+ public void notifyStorageStateEmpty(boolean isEmpty) {
+ if (isEmpty) {
+ UploadAlarmReceiver.cancelAlarm(this, !mScanManager.isPassiveMode());
+ } else if (!mScanManager.isPassiveMode()) {
+ UploadAlarmReceiver.scheduleAlarm(this, FREQUENCY_IN_SEC_OF_UPLOAD_IN_ACTIVE_MODE, true /* repeating */);
+ }
+ }
+
+ private boolean hasLocationPermission() {
+ return ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/BSSIDBlockList.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/BSSIDBlockList.java
new file mode 100644
index 0000000000..6354cb0cc1
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/BSSIDBlockList.java
@@ -0,0 +1,65 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.stumblerthread.blocklist;
+
+import android.net.wifi.ScanResult;
+import android.util.Log;
+import org.mozilla.mozstumbler.service.AppGlobals;
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+public final class BSSIDBlockList {
+ private static final String LOG_TAG = AppGlobals.makeLogTag(BSSIDBlockList.class.getSimpleName());
+ private static final String NULL_BSSID = "000000000000";
+ private static final String WILDCARD_BSSID = "ffffffffffff";
+ private static final Pattern BSSID_PATTERN = Pattern.compile("([0-9a-f]{12})");
+ private static String[] sOuiList = new String[]{};
+
+ private BSSIDBlockList() {
+ }
+
+ public static void setFilterList(String[] list) {
+ sOuiList = list;
+ }
+
+ public static boolean contains(ScanResult scanResult) {
+ String BSSID = scanResult.BSSID;
+ if (BSSID == null || NULL_BSSID.equals(BSSID) || WILDCARD_BSSID.equals(BSSID)) {
+ return true; // blocked!
+ }
+
+ if (!isCanonicalBSSID(BSSID)) {
+ Log.w(LOG_TAG, "", new IllegalArgumentException("Unexpected BSSID format: " + BSSID));
+ return true; // blocked!
+ }
+
+ for (String oui : sOuiList) {
+ if (BSSID.startsWith(oui)) {
+ return true; // blocked!
+ }
+ }
+
+ return false; // OK
+ }
+
+ public static String canonicalizeBSSID(String BSSID) {
+ if (BSSID == null) {
+ return "";
+ }
+
+ if (isCanonicalBSSID(BSSID)) {
+ return BSSID;
+ }
+
+ // Some devices may return BSSIDs with ':', '-' or '.' delimiters.
+ BSSID = BSSID.toLowerCase(Locale.US).replaceAll("[\\-\\.:]", "");
+
+ return isCanonicalBSSID(BSSID) ? BSSID : "";
+ }
+
+ private static boolean isCanonicalBSSID(String BSSID) {
+ return BSSID_PATTERN.matcher(BSSID).matches();
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/SSIDBlockList.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/SSIDBlockList.java
new file mode 100644
index 0000000000..f5086ab342
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/SSIDBlockList.java
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.stumblerthread.blocklist;
+
+import android.net.wifi.ScanResult;
+
+public final class SSIDBlockList {
+ private static String[] sPrefixList = new String[]{};
+ private static String[] sSuffixList = new String[]{"_nomap"};
+
+ private SSIDBlockList() {
+ }
+
+ public static void setFilterLists(String[] prefix, String[] suffix) {
+ sPrefixList = prefix;
+ sSuffixList = suffix;
+ }
+
+ public static boolean contains(ScanResult scanResult) {
+ String SSID = scanResult.SSID;
+ if (SSID == null) {
+ return true; // no SSID?
+ }
+
+ for (String prefix : sPrefixList) {
+ if (SSID.startsWith(prefix)) {
+ return true; // blocked!
+ }
+ }
+
+ for (String suffix : sSuffixList) {
+ if (SSID.endsWith(suffix)) {
+ return true; // blocked!
+ }
+ }
+
+ return false; // OK
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/WifiBlockListInterface.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/WifiBlockListInterface.java
new file mode 100644
index 0000000000..0e940cdc99
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/WifiBlockListInterface.java
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.stumblerthread.blocklist;
+
+public interface WifiBlockListInterface {
+ String[] getSsidPrefixList();
+ String[] getSsidSuffixList();
+ String[] getBssidOuiList();
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/DataStorageContract.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/DataStorageContract.java
new file mode 100644
index 0000000000..2aaeb05ff9
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/DataStorageContract.java
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.stumblerthread.datahandling;
+
+public final class DataStorageContract {
+
+ public static class ReportsColumns {
+ public static final String LAT = "lat";
+ public static final String LON = "lon";
+ public static final String TIME = "timestamp";
+ public static final String ACCURACY = "accuracy";
+ public static final String ALTITUDE = "altitude";
+ public static final String RADIO = "radio";
+ public static final String CELL = "cell";
+ public static final String WIFI = "wifi";
+ public static final String CELL_COUNT = "cell_count";
+ public static final String WIFI_COUNT = "wifi_count";
+ public static final String HEADING = "heading";
+ public static final String SPEED = "speed";
+ public static final String PRESSURE = "pressure";
+ }
+
+ public static class Stats {
+ public static final String KEY_VERSION = "version_code";
+ public static final int VERSION_CODE = 2;
+ public static final String KEY_BYTES_SENT = "bytes_sent";
+ public static final String KEY_LAST_UPLOAD_TIME = "last_upload_time";
+ public static final String KEY_OBSERVATIONS_SENT = "observations_sent";
+ public static final String KEY_WIFIS_SENT = "wifis_sent";
+ public static final String KEY_CELLS_SENT = "cells_sent";
+ public static final String KEY_OBSERVATIONS_PER_DAY = "obs_per_day";
+ }
+
+ private DataStorageContract() {
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/DataStorageManager.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/DataStorageManager.java
new file mode 100644
index 0000000000..adaaea4dc2
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/DataStorageManager.java
@@ -0,0 +1,473 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.stumblerthread.datahandling;
+
+import android.content.Context;
+import android.util.Log;
+import org.mozilla.mozstumbler.service.AppGlobals;
+import org.mozilla.mozstumbler.service.utils.Zipper;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.ArrayList;
+import java.util.Properties;
+import java.util.Timer;
+import java.util.TimerTask;
+
+/* Stores reports in memory (mCurrentReports) until MAX_REPORTS_IN_MEMORY,
+ * then writes them to disk as a .gz file. The name of the file has
+ * the time written, the # of reports, and the # of cells and wifis.
+ *
+ * Each .gz file is typically 1-5KB. File name example: reports-t1406863343313-r4-w25-c7.gz
+ *
+ * The sync stats are written as a key-value pair file (not zipped).
+ *
+ * The tricky bit is the mCurrentReportsSendBuffer. When the uploader code begins accessing the
+ * report batches, mCurrentReports gets pushed to mCurrentReportsSendBuffer.
+ * The mCurrentReports is then cleared, and can continue receiving new reports.
+ * From the uploader perspective, mCurrentReportsSendBuffer looks and acts exactly like a batch file on disk.
+ *
+ * If the network is reasonably active, and reporting is slow enough, there is no disk I/O, it all happens
+ * in-memory.
+ *
+ * Also of note: the in-memory buffers (both mCurrentReports and mCurrentReportsSendBuffer) are saved
+ * when the service is destroyed.
+ */
+public class DataStorageManager {
+ private static final String LOG_TAG = AppGlobals.makeLogTag(DataStorageManager.class.getSimpleName());
+
+ // The max number of reports stored in the mCurrentReports. Each report is a GPS location plus wifi and cell scan.
+ // After this size is reached, data is persisted to disk, mCurrentReports is cleared.
+ private static final int MAX_REPORTS_IN_MEMORY = 50;
+
+ // Used to cap the amount of data stored. When this limit is hit, no more data is saved to disk
+ // until the data is uploaded, or and data exceeds DEFAULT_MAX_WEEKS_DATA_ON_DISK.
+ private static final long DEFAULT_MAX_BYTES_STORED_ON_DISK = 1024 * 250; // 250 KiB max by default
+
+ // Used as a safeguard to ensure stumbling data is not persisted. The intended use case of the stumbler lib is not
+ // for long-term storage, and so if ANY data on disk is this old, ALL data is wiped as a privacy mechanism.
+ private static final int DEFAULT_MAX_WEEKS_DATA_ON_DISK = 2;
+
+ // Set to the default value specified above.
+ private final long mMaxBytesDiskStorage;
+
+ // Set to the default value specified above.
+ private final int mMaxWeeksStored;
+
+ private final ReportBatchBuilder mCurrentReports = new ReportBatchBuilder();
+ private final File mReportsDir;
+ private final File mStatsFile;
+ private final StorageIsEmptyTracker mTracker;
+
+ private static DataStorageManager sInstance;
+
+ private ReportBatch mCurrentReportsSendBuffer;
+ private ReportBatchIterator mReportBatchIterator;
+ private final ReportFileList mFileList;
+ private Timer mFlushMemoryBuffersToDiskTimer;
+ private final PersistedStats mPersistedOnDiskUploadStats;
+
+ static final String SEP_REPORT_COUNT = "-r";
+ static final String SEP_WIFI_COUNT = "-w";
+ static final String SEP_CELL_COUNT = "-c";
+ static final String SEP_TIME_MS = "-t";
+ static final String FILENAME_PREFIX = "reports";
+ static final String MEMORY_BUFFER_NAME = "in memory send buffer";
+
+ public static class QueuedCounts {
+ public final int mReportCount;
+ public final int mWifiCount;
+ public final int mCellCount;
+ public final long mBytes;
+
+ QueuedCounts(int reportCount, int wifiCount, int cellCount, long bytes) {
+ this.mReportCount = reportCount;
+ this.mWifiCount = wifiCount;
+ this.mCellCount = cellCount;
+ this.mBytes = bytes;
+ }
+ }
+
+ /* Some data is calculated on-demand, don't abuse this function */
+ public QueuedCounts getQueuedCounts() {
+ int reportCount = mFileList.mReportCount + mCurrentReports.reports.size();
+ int wifiCount = mFileList.mWifiCount + mCurrentReports.wifiCount;
+ int cellCount = mFileList.mCellCount + mCurrentReports.cellCount;
+ long bytes = 0;
+
+ if (mCurrentReports.reports.size() > 0) {
+ try {
+ bytes = Zipper.zipData(finalizeReports(mCurrentReports.reports).getBytes()).length;
+ } catch (IOException ex) {
+ Log.e(LOG_TAG, "Zip error in getQueuedCounts()", ex);
+ }
+
+ if (mFileList.mReportCount > 0) {
+ bytes += mFileList.mFilesOnDiskBytes;
+ }
+ }
+
+ if (mCurrentReportsSendBuffer != null) {
+ reportCount += mCurrentReportsSendBuffer.reportCount;
+ wifiCount += mCurrentReportsSendBuffer.wifiCount;
+ cellCount += mCurrentReportsSendBuffer.cellCount;
+ bytes += mCurrentReportsSendBuffer.data.length;
+ }
+ return new QueuedCounts(reportCount, wifiCount, cellCount, bytes);
+ }
+
+ private static class ReportFileList {
+ File[] mFiles;
+ int mReportCount;
+ int mWifiCount;
+ int mCellCount;
+ long mFilesOnDiskBytes;
+
+ public ReportFileList() {}
+ public ReportFileList(ReportFileList other) {
+ if (other == null) {
+ return;
+ }
+
+ if (other.mFiles != null) {
+ mFiles = other.mFiles.clone();
+ }
+
+ mReportCount = other.mReportCount;
+ mWifiCount = other.mWifiCount;
+ mCellCount = other.mCellCount;
+ mFilesOnDiskBytes = other.mFilesOnDiskBytes;
+ }
+
+ void update(File directory) {
+ mFiles = directory.listFiles();
+ if (mFiles == null) {
+ return;
+ }
+
+ if (AppGlobals.isDebug) {
+ for (File f : mFiles) {
+ Log.d("StumblerFiles", f.getName());
+ }
+ }
+
+ mFilesOnDiskBytes = mReportCount = mWifiCount = mCellCount = 0;
+ for (File f : mFiles) {
+ mReportCount += (int) getLongFromFilename(f.getName(), SEP_REPORT_COUNT);
+ mWifiCount += (int) getLongFromFilename(f.getName(), SEP_WIFI_COUNT);
+ mCellCount += (int) getLongFromFilename(f.getName(), SEP_CELL_COUNT);
+ mFilesOnDiskBytes += f.length();
+ }
+ }
+ }
+
+ public static class ReportBatch {
+ public final String filename;
+ public final byte[] data;
+ public final int reportCount;
+ public final int wifiCount;
+ public final int cellCount;
+
+ public ReportBatch(String filename, byte[] data, int reportCount, int wifiCount, int cellCount) {
+ this.filename = filename;
+ this.data = data;
+ this.reportCount = reportCount;
+ this.wifiCount = wifiCount;
+ this.cellCount = cellCount;
+ }
+ }
+
+ private static class ReportBatchBuilder {
+ public final ArrayList<String> reports = new ArrayList<String>();
+ public int wifiCount;
+ public int cellCount;
+ }
+
+ private static class ReportBatchIterator {
+ public ReportBatchIterator(ReportFileList list) {
+ fileList = new ReportFileList(list);
+ }
+
+ static final int BATCH_INDEX_FOR_MEM_BUFFER = -1;
+ public int currentIndex = BATCH_INDEX_FOR_MEM_BUFFER;
+ public final ReportFileList fileList;
+ }
+
+ public interface StorageIsEmptyTracker {
+ public void notifyStorageStateEmpty(boolean isEmpty);
+ }
+
+ private String getStorageDir(Context c) {
+ File dir = c.getFilesDir();
+ if (!dir.exists()) {
+ boolean ok = dir.mkdirs();
+ if (!ok) {
+ Log.d(LOG_TAG, "getStorageDir: error in mkdirs()");
+ }
+ }
+
+ return dir.getPath();
+ }
+
+ public static synchronized void createGlobalInstance(Context context, StorageIsEmptyTracker tracker) {
+ DataStorageManager.createGlobalInstance(context, tracker,
+ DEFAULT_MAX_BYTES_STORED_ON_DISK, DEFAULT_MAX_WEEKS_DATA_ON_DISK);
+ }
+
+ public static synchronized void createGlobalInstance(Context context, StorageIsEmptyTracker tracker,
+ long maxBytesStoredOnDisk, int maxWeeksDataStored) {
+ if (sInstance != null) {
+ return;
+ }
+ sInstance = new DataStorageManager(context, tracker, maxBytesStoredOnDisk, maxWeeksDataStored);
+ }
+
+ public static synchronized DataStorageManager getInstance() {
+ return sInstance;
+ }
+
+ private DataStorageManager(Context c, StorageIsEmptyTracker tracker,
+ long maxBytesStoredOnDisk, int maxWeeksDataStored) {
+ mMaxBytesDiskStorage = maxBytesStoredOnDisk;
+ mMaxWeeksStored = maxWeeksDataStored;
+ mTracker = tracker;
+ final String baseDir = getStorageDir(c);
+ mStatsFile = new File(baseDir, "upload_stats.ini");
+ mReportsDir = new File(baseDir + "/reports");
+ if (!mReportsDir.exists()) {
+ mReportsDir.mkdirs();
+ }
+ mFileList = new ReportFileList();
+ mFileList.update(mReportsDir);
+ mPersistedOnDiskUploadStats = new PersistedStats(baseDir);
+ }
+
+ public synchronized int getMaxWeeksStored() {
+ return mMaxWeeksStored;
+ }
+
+ private static byte[] readFile(File file) throws IOException {
+ final RandomAccessFile f = new RandomAccessFile(file, "r");
+ try {
+ final byte[] data = new byte[(int) f.length()];
+ f.readFully(data);
+ return data;
+ } finally {
+ f.close();
+ }
+ }
+
+ public synchronized boolean isDirEmpty() {
+ return (mFileList.mFiles == null || mFileList.mFiles.length < 1);
+ }
+
+ /* Pass filename returned from dataToSend() */
+ public synchronized boolean delete(String filename) {
+ if (filename.equals(MEMORY_BUFFER_NAME)) {
+ mCurrentReportsSendBuffer = null;
+ return true;
+ }
+
+ final File file = new File(mReportsDir, filename);
+ final boolean ok = file.delete();
+ mFileList.update(mReportsDir);
+ return ok;
+ }
+
+ private static long getLongFromFilename(String name, String separator) {
+ final int s = name.indexOf(separator) + separator.length();
+ int e = name.indexOf('-', s);
+ if (e < 0) {
+ e = name.indexOf('.', s);
+ }
+ return Long.parseLong(name.substring(s, e));
+ }
+
+ /* return name of file used, or memory buffer sentinel value.
+ * The return value is used to delete the file/buffer later. */
+ public synchronized ReportBatch getFirstBatch() throws IOException {
+ final boolean dirEmpty = isDirEmpty();
+ final int currentReportsCount = mCurrentReports.reports.size();
+
+ if (dirEmpty && currentReportsCount < 1) {
+ return null;
+ }
+
+ mReportBatchIterator = new ReportBatchIterator(mFileList);
+
+ if (currentReportsCount > 0) {
+ final String filename = MEMORY_BUFFER_NAME;
+ final byte[] data = Zipper.zipData(finalizeReports(mCurrentReports.reports).getBytes());
+ final int wifiCount = mCurrentReports.wifiCount;
+ final int cellCount = mCurrentReports.cellCount;
+ clearCurrentReports();
+ final ReportBatch result = new ReportBatch(filename, data, currentReportsCount, wifiCount, cellCount);
+ mCurrentReportsSendBuffer = result;
+ return result;
+ } else {
+ return getNextBatch();
+ }
+ }
+
+ private void clearCurrentReports() {
+ mCurrentReports.reports.clear();
+ mCurrentReports.wifiCount = mCurrentReports.cellCount = 0;
+ }
+
+ public synchronized ReportBatch getNextBatch() throws IOException {
+ if (mReportBatchIterator == null) {
+ return null;
+ }
+
+ mReportBatchIterator.currentIndex++;
+ if (mReportBatchIterator.currentIndex < 0 ||
+ mReportBatchIterator.currentIndex > mReportBatchIterator.fileList.mFiles.length - 1) {
+ return null;
+ }
+
+ final File f = mReportBatchIterator.fileList.mFiles[mReportBatchIterator.currentIndex];
+ final String filename = f.getName();
+ final int reportCount = (int) getLongFromFilename(f.getName(), SEP_REPORT_COUNT);
+ final int wifiCount = (int) getLongFromFilename(f.getName(), SEP_WIFI_COUNT);
+ final int cellCount = (int) getLongFromFilename(f.getName(), SEP_CELL_COUNT);
+ final byte[] data = readFile(f);
+ return new ReportBatch(filename, data, reportCount, wifiCount, cellCount);
+ }
+
+ private File createFile(int reportCount, int wifiCount, int cellCount) {
+ final long time = System.currentTimeMillis();
+ final String name = FILENAME_PREFIX +
+ SEP_TIME_MS + time +
+ SEP_REPORT_COUNT + reportCount +
+ SEP_WIFI_COUNT + wifiCount +
+ SEP_CELL_COUNT + cellCount + ".gz";
+ return new File(mReportsDir, name);
+ }
+
+ public synchronized long getOldestBatchTimeMs() {
+ if (isDirEmpty()) {
+ return 0;
+ }
+
+ long oldest = Long.MAX_VALUE;
+ for (File f : mFileList.mFiles) {
+ final long t = getLongFromFilename(f.getName(), SEP_TIME_MS);
+ if (t < oldest) {
+ oldest = t;
+ }
+ }
+ return oldest;
+ }
+
+ public synchronized void saveCurrentReportsSendBufferToDisk() throws IOException {
+ if (mCurrentReportsSendBuffer == null || mCurrentReportsSendBuffer.reportCount < 1) {
+ return;
+ }
+
+ saveToDisk(mCurrentReportsSendBuffer.data,
+ mCurrentReportsSendBuffer.reportCount,
+ mCurrentReportsSendBuffer.wifiCount,
+ mCurrentReportsSendBuffer.cellCount);
+ mCurrentReportsSendBuffer = null;
+ }
+
+ private void saveToDisk(byte[] bytes, int reportCount, int wifiCount, int cellCount)
+ throws IOException {
+ if (mFileList.mFilesOnDiskBytes > mMaxBytesDiskStorage) {
+ return;
+ }
+
+ final FileOutputStream fos = new FileOutputStream(createFile(reportCount, wifiCount, cellCount));
+ try {
+ fos.write(bytes);
+ } finally {
+ fos.close();
+ }
+ mFileList.update(mReportsDir);
+ }
+
+ private String finalizeReports(ArrayList<String> reports) {
+ final String kPrefix = "{\"items\":[";
+ final String kSuffix = "]}";
+ final StringBuilder sb = new StringBuilder(kPrefix);
+ String sep = "";
+ final String separator = ",";
+ if (reports != null) {
+ for(String s: reports) {
+ sb.append(sep).append(s);
+ sep = separator;
+ }
+ }
+
+ final String result = sb.append(kSuffix).toString();
+ return result;
+ }
+
+ public synchronized void saveCurrentReportsToDisk() throws IOException {
+ saveCurrentReportsSendBufferToDisk();
+ if (mCurrentReports.reports.size() < 1) {
+ return;
+ }
+ final byte[] bytes = Zipper.zipData(finalizeReports(mCurrentReports.reports).getBytes());
+ saveToDisk(bytes, mCurrentReports.reports.size(), mCurrentReports.wifiCount, mCurrentReports.cellCount);
+ clearCurrentReports();
+ }
+
+ public synchronized void insert(String report, int wifiCount, int cellCount) throws IOException {
+ notifyStorageIsEmpty(false);
+
+ if (mFlushMemoryBuffersToDiskTimer != null) {
+ mFlushMemoryBuffersToDiskTimer.cancel();
+ mFlushMemoryBuffersToDiskTimer = null;
+ }
+
+ mCurrentReports.reports.add(report);
+ mCurrentReports.wifiCount += wifiCount;
+ mCurrentReports.cellCount += cellCount;
+
+ if (mCurrentReports.reports.size() >= MAX_REPORTS_IN_MEMORY) {
+ // save to disk
+ saveCurrentReportsToDisk();
+ } else {
+ // Schedule a timer to flush to disk after a few mins.
+ // If collection stops and wifi not available for uploading, the memory buffer is flushed to disk.
+ final int kMillis = 1000 * 60 * 3;
+ mFlushMemoryBuffersToDiskTimer = new Timer();
+ mFlushMemoryBuffersToDiskTimer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ try {
+ saveCurrentReportsToDisk();
+ } catch (IOException ex) {
+ Log.e(LOG_TAG, "mFlushMemoryBuffersToDiskTimer exception" + ex);
+ }
+ }
+ }, kMillis);
+ }
+ }
+
+ public synchronized void deleteAll() {
+ if (mFileList.mFiles == null) {
+ return;
+ }
+
+ for (File f : mFileList.mFiles) {
+ f.delete();
+ }
+ mFileList.update(mReportsDir);
+ }
+
+ private void notifyStorageIsEmpty(boolean isEmpty) {
+ if (mTracker != null) {
+ mTracker.notifyStorageStateEmpty(isEmpty);
+ }
+ }
+
+ public synchronized void incrementSyncStats(long bytesSent, long reports, long cells, long wifis) throws IOException {
+ mPersistedOnDiskUploadStats.incrementSyncStats(bytesSent, reports, cells, wifis);
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/PersistedStats.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/PersistedStats.java
new file mode 100644
index 0000000000..79c8e59d13
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/PersistedStats.java
@@ -0,0 +1,99 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.stumblerthread.datahandling;
+
+import org.mozilla.mozstumbler.service.AppGlobals;
+import org.mozilla.mozstumbler.service.utils.TelemetryWrapper;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
+
+class PersistedStats {
+ private final File mStatsFile;
+
+ public PersistedStats(String baseDir) {
+ mStatsFile = new File(baseDir, "upload_stats.ini");
+ }
+
+ public synchronized Properties readSyncStats() throws IOException {
+ if (!mStatsFile.exists()) {
+ return new Properties();
+ }
+
+ final FileInputStream input = new FileInputStream(mStatsFile);
+ try {
+ final Properties props = new Properties();
+ props.load(input);
+ return props;
+ } finally {
+ input.close();
+ }
+ }
+
+ public synchronized void incrementSyncStats(long bytesSent, long reports, long cells, long wifis) throws IOException {
+ if (reports + cells + wifis < 1) {
+ return;
+ }
+
+ final Properties properties = readSyncStats();
+ final long time = System.currentTimeMillis();
+ final long lastUploadTime = Long.parseLong(properties.getProperty(DataStorageContract.Stats.KEY_LAST_UPLOAD_TIME, "0"));
+ final long storedObsPerDay = Long.parseLong(properties.getProperty(DataStorageContract.Stats.KEY_OBSERVATIONS_PER_DAY, "0"));
+ long observationsToday = reports;
+ if (lastUploadTime > 0) {
+ long dayLastUploaded = TimeUnit.MILLISECONDS.toDays(lastUploadTime);
+ long dayDiff = TimeUnit.MILLISECONDS.toDays(time) - dayLastUploaded;
+ if (dayDiff > 0) {
+ // send value of store obs per day
+ TelemetryWrapper.addToHistogram(AppGlobals.TELEMETRY_OBSERVATIONS_PER_DAY,
+ Long.valueOf(storedObsPerDay / dayDiff).intValue());
+ } else {
+ observationsToday += storedObsPerDay;
+ }
+ }
+
+ writeSyncStats(time,
+ Long.parseLong(properties.getProperty(DataStorageContract.Stats.KEY_BYTES_SENT, "0")) + bytesSent,
+ Long.parseLong(properties.getProperty(DataStorageContract.Stats.KEY_OBSERVATIONS_SENT, "0")) + reports,
+ Long.parseLong(properties.getProperty(DataStorageContract.Stats.KEY_CELLS_SENT, "0")) + cells,
+ Long.parseLong(properties.getProperty(DataStorageContract.Stats.KEY_WIFIS_SENT, "0")) + wifis,
+ observationsToday);
+
+
+ final long lastUploadMs = Long.parseLong(properties.getProperty(DataStorageContract.Stats.KEY_LAST_UPLOAD_TIME, "0"));
+ final int timeDiffSec = Long.valueOf((time - lastUploadMs) / 1000).intValue();
+ if (lastUploadMs > 0 && timeDiffSec > 0) {
+ TelemetryWrapper.addToHistogram(AppGlobals.TELEMETRY_TIME_BETWEEN_UPLOADS_SEC, timeDiffSec);
+ TelemetryWrapper.addToHistogram(AppGlobals.TELEMETRY_BYTES_UPLOADED_PER_SEC, Long.valueOf(bytesSent).intValue() / timeDiffSec);
+ }
+ TelemetryWrapper.addToHistogram(AppGlobals.TELEMETRY_BYTES_PER_UPLOAD, Long.valueOf(bytesSent).intValue());
+ TelemetryWrapper.addToHistogram(AppGlobals.TELEMETRY_OBSERVATIONS_PER_UPLOAD, Long.valueOf(reports).intValue());
+ TelemetryWrapper.addToHistogram(AppGlobals.TELEMETRY_WIFIS_PER_UPLOAD, Long.valueOf(wifis).intValue());
+ TelemetryWrapper.addToHistogram(AppGlobals.TELEMETRY_CELLS_PER_UPLOAD, Long.valueOf(cells).intValue());
+ }
+
+ public synchronized void writeSyncStats(long time, long bytesSent, long totalObs,
+ long totalCells, long totalWifis, long obsPerDay) throws IOException {
+ final FileOutputStream out = new FileOutputStream(mStatsFile);
+ try {
+ final Properties props = new Properties();
+ props.setProperty(DataStorageContract.Stats.KEY_LAST_UPLOAD_TIME, String.valueOf(time));
+ props.setProperty(DataStorageContract.Stats.KEY_BYTES_SENT, String.valueOf(bytesSent));
+ props.setProperty(DataStorageContract.Stats.KEY_OBSERVATIONS_SENT, String.valueOf(totalObs));
+ props.setProperty(DataStorageContract.Stats.KEY_CELLS_SENT, String.valueOf(totalCells));
+ props.setProperty(DataStorageContract.Stats.KEY_WIFIS_SENT, String.valueOf(totalWifis));
+ props.setProperty(DataStorageContract.Stats.KEY_VERSION, String.valueOf(DataStorageContract.Stats.VERSION_CODE));
+ props.setProperty(DataStorageContract.Stats.KEY_OBSERVATIONS_PER_DAY, String.valueOf(obsPerDay));
+ props.store(out, null);
+ } finally {
+ out.close();
+ }
+ }
+
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/StumblerBundle.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/StumblerBundle.java
new file mode 100644
index 0000000000..4f47e33024
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/StumblerBundle.java
@@ -0,0 +1,187 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.stumblerthread.datahandling;
+
+import android.location.Location;
+import android.net.wifi.ScanResult;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.telephony.TelephonyManager;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.mozstumbler.service.stumblerthread.scanners.cellscanner.CellInfo;
+
+public final class StumblerBundle implements Parcelable {
+ private final int mPhoneType;
+ private final Location mGpsPosition;
+ private final Map<String, ScanResult> mWifiData;
+ private final Map<String, CellInfo> mCellData;
+ private float mPressureHPA;
+
+
+ public void wasSent() {
+ mGpsPosition.setTime(System.currentTimeMillis());
+ mWifiData.clear();
+ mCellData.clear();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ Bundle wifiBundle = new Bundle(ScanResult.class.getClassLoader());
+ Collection<String> scans = mWifiData.keySet();
+ for (String s : scans) {
+ wifiBundle.putParcelable(s, mWifiData.get(s));
+ }
+
+ Bundle cellBundle = new Bundle(CellInfo.class.getClassLoader());
+ Collection<String> cells = mCellData.keySet();
+ for (String c : cells) {
+ cellBundle.putParcelable(c, mCellData.get(c));
+ }
+
+ out.writeBundle(wifiBundle);
+ out.writeBundle(cellBundle);
+ out.writeParcelable(mGpsPosition, 0);
+ out.writeInt(mPhoneType);
+ }
+
+ public static final Parcelable.Creator<StumblerBundle> CREATOR
+ = new Parcelable.Creator<StumblerBundle>() {
+ @Override
+ public StumblerBundle createFromParcel(Parcel in) {
+ return new StumblerBundle(in);
+ }
+
+ @Override
+ public StumblerBundle[] newArray(int size) {
+ return new StumblerBundle[size];
+ }
+ };
+
+ private StumblerBundle(Parcel in) {
+ mWifiData = new HashMap<String, ScanResult>();
+ mCellData = new HashMap<String, CellInfo>();
+
+ Bundle wifiBundle = in.readBundle(ScanResult.class.getClassLoader());
+ Bundle cellBundle = in.readBundle(CellInfo.class.getClassLoader());
+
+ Collection<String> scans = wifiBundle.keySet();
+ for (String s : scans) {
+ mWifiData.put(s, (ScanResult) wifiBundle.get(s));
+ }
+
+ Collection<String> cells = cellBundle.keySet();
+ for (String c : cells) {
+ mCellData.put(c, (CellInfo) cellBundle.get(c));
+ }
+
+ mGpsPosition = in.readParcelable(Location.class.getClassLoader());
+ mPhoneType = in.readInt();
+ }
+
+ public StumblerBundle(Location position, int phoneType) {
+ mGpsPosition = position;
+ mPhoneType = phoneType;
+ mWifiData = new HashMap<String, ScanResult>();
+ mCellData = new HashMap<String, CellInfo>();
+ }
+
+ public Location getGpsPosition() {
+ return mGpsPosition;
+ }
+
+ public Map<String, ScanResult> getWifiData() {
+ return mWifiData;
+ }
+
+ public Map<String, CellInfo> getCellData() {
+ return mCellData;
+ }
+
+ public JSONObject toMLSJSON() throws JSONException {
+ JSONObject item = new JSONObject();
+
+ item.put(DataStorageContract.ReportsColumns.TIME, mGpsPosition.getTime());
+ item.put(DataStorageContract.ReportsColumns.LAT, Math.floor(mGpsPosition.getLatitude() * 1.0E6) / 1.0E6);
+ item.put(DataStorageContract.ReportsColumns.LON, Math.floor(mGpsPosition.getLongitude() * 1.0E6) / 1.0E6);
+
+ item.put(DataStorageContract.ReportsColumns.HEADING, mGpsPosition.getBearing());
+ item.put(DataStorageContract.ReportsColumns.SPEED, mGpsPosition.getSpeed());
+ if (mPressureHPA != 0.0) {
+ item.put(DataStorageContract.ReportsColumns.PRESSURE, mPressureHPA);
+ }
+
+
+ if (mGpsPosition.hasAccuracy()) {
+ item.put(DataStorageContract.ReportsColumns.ACCURACY, (int) Math.ceil(mGpsPosition.getAccuracy()));
+ }
+
+ if (mGpsPosition.hasAltitude()) {
+ item.put(DataStorageContract.ReportsColumns.ALTITUDE, Math.round(mGpsPosition.getAltitude()));
+ }
+
+ if (mPhoneType == TelephonyManager.PHONE_TYPE_GSM) {
+ item.put(DataStorageContract.ReportsColumns.RADIO, "gsm");
+ } else if (mPhoneType == TelephonyManager.PHONE_TYPE_CDMA) {
+ item.put(DataStorageContract.ReportsColumns.RADIO, "cdma");
+ } else {
+ // issue #598. investigate this case further in future
+ item.put(DataStorageContract.ReportsColumns.RADIO, "");
+ }
+
+ JSONArray cellJSON = new JSONArray();
+ for (CellInfo c : mCellData.values()) {
+ JSONObject obj = c.toJSONObject();
+ cellJSON.put(obj);
+ }
+
+ item.put(DataStorageContract.ReportsColumns.CELL, cellJSON);
+ item.put(DataStorageContract.ReportsColumns.CELL_COUNT, cellJSON.length());
+
+ JSONArray wifis = new JSONArray();
+
+ long gpsTimeSinceBootInMS = 0;
+
+ if (Build.VERSION.SDK_INT >= 17) {
+ gpsTimeSinceBootInMS = mGpsPosition.getElapsedRealtimeNanos() / 1000000;
+ }
+
+ for (ScanResult s : mWifiData.values()) {
+ JSONObject wifiEntry = new JSONObject();
+ wifiEntry.put("key", s.BSSID);
+ wifiEntry.put("frequency", s.frequency);
+ wifiEntry.put("signal", s.level);
+
+ if (Build.VERSION.SDK_INT >= 17) {
+ long wifiTimeSinceBootInMS = (s.timestamp / 1000);
+ long ageMS = wifiTimeSinceBootInMS - gpsTimeSinceBootInMS;
+ wifiEntry.put("age", ageMS);
+ }
+
+ wifis.put(wifiEntry);
+ }
+ item.put(DataStorageContract.ReportsColumns.WIFI, wifis);
+ item.put(DataStorageContract.ReportsColumns.WIFI_COUNT, wifis.length());
+
+ return item;
+ }
+
+
+ public void addPressure(float hPa) {
+ mPressureHPA = hPa;
+ }
+
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/GPSScanner.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/GPSScanner.java
new file mode 100644
index 0000000000..218b97af4e
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/GPSScanner.java
@@ -0,0 +1,293 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.stumblerthread.scanners;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.Intent;
+import android.location.GpsSatellite;
+import android.location.GpsStatus;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.location.LocationProvider;
+import android.os.Bundle;
+import android.support.v4.content.LocalBroadcastManager;
+import android.util.Log;
+import org.mozilla.mozstumbler.service.AppGlobals;
+import org.mozilla.mozstumbler.service.AppGlobals.ActiveOrPassiveStumbling;
+import org.mozilla.mozstumbler.service.Prefs;
+import org.mozilla.mozstumbler.service.utils.TelemetryWrapper;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+public class GPSScanner implements LocationListener {
+ public static final String ACTION_BASE = AppGlobals.ACTION_NAMESPACE + ".GPSScanner.";
+ public static final String ACTION_GPS_UPDATED = ACTION_BASE + "GPS_UPDATED";
+ public static final String ACTION_ARG_TIME = AppGlobals.ACTION_ARG_TIME;
+ public static final String SUBJECT_NEW_STATUS = "new_status";
+ public static final String SUBJECT_LOCATION_LOST = "location_lost";
+ public static final String SUBJECT_NEW_LOCATION = "new_location";
+ public static final String NEW_STATUS_ARG_FIXES = "fixes";
+ public static final String NEW_STATUS_ARG_SATS = "sats";
+ public static final String NEW_LOCATION_ARG_LOCATION = "location";
+
+ private static final String LOG_TAG = AppGlobals.makeLogTag(GPSScanner.class.getSimpleName());
+ private static final int MIN_SAT_USED_IN_FIX = 3;
+ private static final long ACTIVE_MODE_GPS_MIN_UPDATE_TIME_MS = 1000;
+ private static final float ACTIVE_MODE_GPS_MIN_UPDATE_DISTANCE_M = 10;
+ private static final long PASSIVE_GPS_MIN_UPDATE_FREQ_MS = 3000;
+ private static final float PASSIVE_GPS_MOVEMENT_MIN_DELTA_M = 30;
+
+ private final LocationBlockList mBlockList = new LocationBlockList();
+ private final Context mContext;
+ private GpsStatus.Listener mGPSListener;
+ private int mLocationCount;
+ private Location mLocation = new Location("internal");
+ private boolean mAutoGeofencing;
+ private boolean mIsPassiveMode;
+ private long mTelemetry_lastStartedMs;
+ private final ScanManager mScanManager;
+
+ public GPSScanner(Context context, ScanManager scanManager) {
+ mContext = context;
+ mScanManager = scanManager;
+ }
+
+ public void start(final ActiveOrPassiveStumbling stumblingMode) {
+ mIsPassiveMode = (stumblingMode == ActiveOrPassiveStumbling.PASSIVE_STUMBLING);
+ if (mIsPassiveMode ) {
+ startPassiveMode();
+ } else {
+ startActiveMode();
+ }
+ }
+
+ private boolean isGpsAvailable(LocationManager locationManager) {
+ if (locationManager == null ||
+ locationManager.getProvider(LocationManager.GPS_PROVIDER) == null) {
+ String msg = "No GPS available, scanning not started.";
+ Log.d(LOG_TAG, msg);
+ AppGlobals.guiLogError(msg);
+ return false;
+ }
+ return true;
+ }
+
+ @SuppressLint("MissingPermission") // Permissions are explicitly checked for in StumblerService.onHandleIntent()
+ private void startPassiveMode() {
+ LocationManager locationManager = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
+ if (!isGpsAvailable(locationManager)) {
+ return;
+ }
+
+ locationManager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, 0, 0, this);
+
+ final int timeDiffSec = Long.valueOf((System.currentTimeMillis() - mTelemetry_lastStartedMs) / 1000).intValue();
+ if (mTelemetry_lastStartedMs > 0 && timeDiffSec > 0) {
+ TelemetryWrapper.addToHistogram(AppGlobals.TELEMETRY_TIME_BETWEEN_STARTS_SEC, timeDiffSec);
+ }
+ mTelemetry_lastStartedMs = System.currentTimeMillis();
+ }
+
+ @SuppressLint("MissingPermission") // Permissions are explicitly checked for in StumblerService.onHandleIntent()
+ private void startActiveMode() {
+ LocationManager lm = getLocationManager();
+ if (!isGpsAvailable(lm)) {
+ return;
+ }
+
+ lm.requestLocationUpdates(LocationManager.GPS_PROVIDER,
+ ACTIVE_MODE_GPS_MIN_UPDATE_TIME_MS,
+ ACTIVE_MODE_GPS_MIN_UPDATE_DISTANCE_M,
+ this);
+
+ reportLocationLost();
+ mGPSListener = new GpsStatus.Listener() {
+ @Override
+ public void onGpsStatusChanged(int event) {
+ if (event == GpsStatus.GPS_EVENT_SATELLITE_STATUS) {
+ GpsStatus status = getLocationManager().getGpsStatus(null);
+ Iterable<GpsSatellite> sats = status.getSatellites();
+
+ int satellites = 0;
+ int fixes = 0;
+
+ for (GpsSatellite sat : sats) {
+ satellites++;
+ if (sat.usedInFix()) {
+ fixes++;
+ }
+ }
+ reportNewGpsStatus(fixes, satellites);
+ if (fixes < MIN_SAT_USED_IN_FIX) {
+ reportLocationLost();
+ }
+
+ if (AppGlobals.isDebug) {
+ Log.v(LOG_TAG, "onGpsStatusChange - satellites: " + satellites + " fixes: " + fixes);
+ }
+ } else if (event == GpsStatus.GPS_EVENT_STOPPED) {
+ reportLocationLost();
+ }
+ }
+ };
+
+ lm.addGpsStatusListener(mGPSListener);
+ }
+
+ @SuppressLint("MissingPermission") // Permissions are explicitly checked for in StumblerService.onHandleIntent()
+ public void stop() {
+ LocationManager lm = getLocationManager();
+ lm.removeUpdates(this);
+ reportLocationLost();
+
+ if (mGPSListener != null) {
+ lm.removeGpsStatusListener(mGPSListener);
+ mGPSListener = null;
+ }
+ }
+
+ public int getLocationCount() {
+ return mLocationCount;
+ }
+
+ public double getLatitude() {
+ return mLocation.getLatitude();
+ }
+
+ public double getLongitude() {
+ return mLocation.getLongitude();
+ }
+
+ public Location getLocation() {
+ return mLocation;
+ }
+
+ public void checkPrefs() {
+ if (mBlockList != null) {
+ mBlockList.updateBlocks();
+ }
+
+ Prefs prefs = Prefs.getInstanceWithoutContext();
+ if (prefs == null) {
+ return;
+ }
+ mAutoGeofencing = prefs.getGeofenceHere();
+ }
+
+ public boolean isGeofenced() {
+ return (mBlockList != null) && mBlockList.isGeofenced();
+ }
+
+ private void sendToLogActivity(String msg) {
+ AppGlobals.guiLogInfo(msg, "#33ccff", false);
+ }
+
+ @Override
+ public void onLocationChanged(Location location) {
+ if (location == null) { // TODO: is this even possible??
+ reportLocationLost();
+ return;
+ }
+
+ String logMsg = (mIsPassiveMode)? "[Passive] " : "[Active] ";
+
+ String provider = location.getProvider();
+ if (!provider.toLowerCase().contains("gps")) {
+ Log.d(LOG_TAG, "Discard fused/network location.");
+ // only interested in GPS locations
+ return;
+ }
+
+ final long timeDeltaMs = location.getTime() - mLocation.getTime();
+
+ // Seem to get greater likelihood of non-fused location with higher update freq.
+ // Check dist and time threshold here, not set on the listener.
+ if (mIsPassiveMode) {
+ final boolean hasMoved = location.distanceTo(mLocation) > PASSIVE_GPS_MOVEMENT_MIN_DELTA_M;
+
+ if (timeDeltaMs < PASSIVE_GPS_MIN_UPDATE_FREQ_MS || !hasMoved) {
+ return;
+ }
+ }
+
+ Date date = new Date(location.getTime());
+ SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
+ String time = formatter.format(date);
+ logMsg += String.format("%s Coord: %.4f,%.4f, Acc: %.0f, Speed: %.0f, Alt: %.0f, Bearing: %.1f", time, location.getLatitude(),
+ location.getLongitude(), location.getAccuracy(), location.getSpeed(), location.getAltitude(), location.getBearing());
+ sendToLogActivity(logMsg);
+
+ if (mBlockList.contains(location)) {
+ reportLocationLost();
+ return;
+ }
+
+ mLocation = location;
+
+ if (!mAutoGeofencing) {
+ reportNewLocationReceived(location);
+ }
+ mLocationCount++;
+
+ if (mIsPassiveMode) {
+ mScanManager.newPassiveGpsLocation();
+ }
+
+ if (timeDeltaMs > 0) {
+ TelemetryWrapper.addToHistogram(AppGlobals.TELEMETRY_TIME_BETWEEN_RECEIVED_LOCATIONS_SEC,
+ Long.valueOf(timeDeltaMs).intValue() / 1000);
+ }
+ }
+
+ @Override
+ public void onProviderDisabled(String provider) {
+ if (LocationManager.GPS_PROVIDER.equals(provider)) {
+ reportLocationLost();
+ }
+ }
+
+ @Override
+ public void onProviderEnabled(String provider) {
+ }
+
+ @Override
+ public void onStatusChanged(String provider, int status, Bundle extras) {
+ if ((status != LocationProvider.AVAILABLE) &&
+ (LocationManager.GPS_PROVIDER.equals(provider))) {
+ reportLocationLost();
+ }
+ }
+
+ private LocationManager getLocationManager() {
+ return (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
+ }
+
+ private void reportNewLocationReceived(Location location) {
+ Intent i = new Intent(ACTION_GPS_UPDATED);
+ i.putExtra(Intent.EXTRA_SUBJECT, SUBJECT_NEW_LOCATION);
+ i.putExtra(NEW_LOCATION_ARG_LOCATION, location);
+ i.putExtra(ACTION_ARG_TIME, System.currentTimeMillis());
+ LocalBroadcastManager.getInstance(mContext).sendBroadcastSync(i);
+ }
+
+ private void reportLocationLost() {
+ Intent i = new Intent(ACTION_GPS_UPDATED);
+ i.putExtra(Intent.EXTRA_SUBJECT, SUBJECT_LOCATION_LOST);
+ i.putExtra(ACTION_ARG_TIME, System.currentTimeMillis());
+ LocalBroadcastManager.getInstance(mContext).sendBroadcastSync(i);
+ }
+
+ private void reportNewGpsStatus(int fixes, int sats) {
+ Intent i = new Intent(ACTION_GPS_UPDATED);
+ i.putExtra(Intent.EXTRA_SUBJECT, SUBJECT_NEW_STATUS);
+ i.putExtra(NEW_STATUS_ARG_FIXES, fixes);
+ i.putExtra(NEW_STATUS_ARG_SATS, sats);
+ i.putExtra(ACTION_ARG_TIME, System.currentTimeMillis());
+ LocalBroadcastManager.getInstance(mContext).sendBroadcastSync(i);
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/LocationBlockList.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/LocationBlockList.java
new file mode 100644
index 0000000000..c3cba7b453
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/LocationBlockList.java
@@ -0,0 +1,105 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.stumblerthread.scanners;
+
+import android.location.Location;
+import android.util.Log;
+import org.mozilla.mozstumbler.service.AppGlobals;
+import org.mozilla.mozstumbler.service.Prefs;
+
+public final class LocationBlockList {
+ private static final String LOG_TAG = AppGlobals.makeLogTag(LocationBlockList.class.getSimpleName());
+ private static final double MAX_ALTITUDE = 8848; // Mount Everest's altitude in meters
+ private static final double MIN_ALTITUDE = -418; // Dead Sea's altitude in meters
+ private static final float MAX_SPEED = 340.29f; // Mach 1 in meters/second
+ private static final float MIN_ACCURACY = 500; // meter radius
+ private static final long MIN_TIMESTAMP = 946684801; // 2000-01-01 00:00:01
+ private static final double GEOFENCE_RADIUS = 0.01; // .01 degrees is approximately 1km
+ private static final long MILLISECONDS_PER_DAY = 86400000;
+
+ private Location mBlockedLocation;
+ private boolean mGeofencingEnabled;
+ private boolean mIsGeofenced = false;
+
+ public LocationBlockList() {
+ updateBlocks();
+ }
+
+ public void updateBlocks() {
+ Prefs prefs = Prefs.getInstanceWithoutContext();
+ if (prefs == null) {
+ return;
+ }
+ mBlockedLocation = prefs.getGeofenceLocation();
+ mGeofencingEnabled = prefs.getGeofenceEnabled();
+ }
+
+ public boolean contains(Location location) {
+ final float inaccuracy = location.getAccuracy();
+ final double altitude = location.getAltitude();
+ final float bearing = location.getBearing();
+ final double latitude = location.getLatitude();
+ final double longitude = location.getLongitude();
+ final float speed = location.getSpeed();
+ final long timestamp = location.getTime();
+ final long tomorrow = System.currentTimeMillis() + MILLISECONDS_PER_DAY;
+
+ boolean block = false;
+ mIsGeofenced = false;
+
+ if (latitude == 0 && longitude == 0) {
+ block = true;
+ Log.w(LOG_TAG, "Bogus latitude,longitude: 0,0");
+ } else {
+ if (latitude < -90 || latitude > 90) {
+ block = true;
+ Log.w(LOG_TAG, "Bogus latitude: " + latitude);
+ }
+
+ if (longitude < -180 || longitude > 180) {
+ block = true;
+ Log.w(LOG_TAG, "Bogus longitude: " + longitude);
+ }
+ }
+
+ if (location.hasAccuracy() && (inaccuracy < 0 || inaccuracy > MIN_ACCURACY)) {
+ block = true;
+ Log.w(LOG_TAG, "Insufficient accuracy: " + inaccuracy + " meters");
+ }
+
+ if (location.hasAltitude() && (altitude < MIN_ALTITUDE || altitude > MAX_ALTITUDE)) {
+ block = true;
+ Log.w(LOG_TAG, "Bogus altitude: " + altitude + " meters");
+ }
+
+ if (location.hasBearing() && (bearing < 0 || bearing > 360)) {
+ block = true;
+ Log.w(LOG_TAG, "Bogus bearing: " + bearing + " degrees");
+ }
+
+ if (location.hasSpeed() && (speed < 0 || speed > MAX_SPEED)) {
+ block = true;
+ Log.w(LOG_TAG, "Bogus speed: " + speed + " meters/second");
+ }
+
+ if (timestamp < MIN_TIMESTAMP || timestamp > tomorrow) {
+ block = true;
+ Log.w(LOG_TAG, "Bogus timestamp: " + timestamp);
+ }
+
+ if (mGeofencingEnabled &&
+ Math.abs(location.getLatitude() - mBlockedLocation.getLatitude()) < GEOFENCE_RADIUS &&
+ Math.abs(location.getLongitude() - mBlockedLocation.getLongitude()) < GEOFENCE_RADIUS) {
+ block = true;
+ mIsGeofenced = true;
+ }
+
+ return block;
+ }
+
+ public boolean isGeofenced() {
+ return mIsGeofenced;
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/ScanManager.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/ScanManager.java
new file mode 100644
index 0000000000..60d7c8f1c3
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/ScanManager.java
@@ -0,0 +1,191 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.stumblerthread.scanners;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.location.Location;
+import android.os.BatteryManager;
+import android.support.v4.content.LocalBroadcastManager;
+import android.util.Log;
+
+import org.mozilla.mozstumbler.service.AppGlobals;
+import org.mozilla.mozstumbler.service.stumblerthread.Reporter;
+import org.mozilla.mozstumbler.service.stumblerthread.blocklist.WifiBlockListInterface;
+import org.mozilla.mozstumbler.service.stumblerthread.scanners.cellscanner.CellScanner;
+import org.mozilla.mozstumbler.service.AppGlobals.ActiveOrPassiveStumbling;
+
+import java.util.Date;
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class ScanManager {
+ private static final String LOG_TAG = AppGlobals.makeLogTag(ScanManager.class.getSimpleName());
+ private Timer mPassiveModeFlushTimer;
+ private Context mContext;
+ private boolean mIsScanning;
+ private GPSScanner mGPSScanner;
+ private WifiScanner mWifiScanner;
+ private CellScanner mCellScanner;
+ private ActiveOrPassiveStumbling mStumblingMode = ActiveOrPassiveStumbling.ACTIVE_STUMBLING;
+
+ public ScanManager() {
+ }
+
+ private boolean isBatteryLow() {
+ Intent intent = mContext.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
+ if (intent == null) {
+ return false;
+ }
+
+ int rawLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
+ int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
+ int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
+ boolean isCharging = (status == BatteryManager.BATTERY_STATUS_CHARGING);
+ int level = Math.round(rawLevel * scale/100.0f);
+
+ final int kMinBatteryPct = 15;
+ return !isCharging && level < kMinBatteryPct;
+ }
+
+ public void newPassiveGpsLocation() {
+ if (isBatteryLow()) {
+ return;
+ }
+
+ if (AppGlobals.isDebug) {
+ Log.d(LOG_TAG, "New passive location");
+ }
+
+ mWifiScanner.start(ActiveOrPassiveStumbling.PASSIVE_STUMBLING);
+ mCellScanner.start(ActiveOrPassiveStumbling.PASSIVE_STUMBLING);
+
+ // how often to flush a leftover bundle to the reports table
+ // If there is a bundle, and nothing happens for 10sec, then flush it
+ final int flushRate_ms = 10000;
+
+ if (mPassiveModeFlushTimer != null) {
+ mPassiveModeFlushTimer.cancel();
+ }
+
+ Date when = new Date();
+ when.setTime(when.getTime() + flushRate_ms);
+ mPassiveModeFlushTimer = new Timer();
+ mPassiveModeFlushTimer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ Intent flush = new Intent(Reporter.ACTION_FLUSH_TO_BUNDLE);
+ LocalBroadcastManager.getInstance(mContext).sendBroadcastSync(flush);
+ }
+ }, when);
+ }
+
+ public void setPassiveMode(boolean on) {
+ mStumblingMode = (on)? ActiveOrPassiveStumbling.PASSIVE_STUMBLING :
+ ActiveOrPassiveStumbling.ACTIVE_STUMBLING;
+ }
+
+ public boolean isPassiveMode() {
+ return ActiveOrPassiveStumbling.PASSIVE_STUMBLING == mStumblingMode;
+ }
+
+ public void startScanning(Context context) {
+ if (mIsScanning) {
+ return;
+ }
+
+ mContext = context.getApplicationContext();
+ if (mContext == null) {
+ Log.w(LOG_TAG, "No app context available.");
+ return;
+ }
+
+ if (mGPSScanner == null) {
+ mGPSScanner = new GPSScanner(context, this);
+ mWifiScanner = new WifiScanner(context);
+ mCellScanner = new CellScanner(context);
+ }
+
+ if (AppGlobals.isDebug) {
+ Log.d(LOG_TAG, "Scanning started...");
+ }
+
+ mGPSScanner.start(mStumblingMode);
+ if (mStumblingMode == ActiveOrPassiveStumbling.ACTIVE_STUMBLING) {
+ mWifiScanner.start(mStumblingMode);
+ mCellScanner.start(mStumblingMode);
+ // in passive mode, these scans are started by passive gps notifications
+ }
+ mIsScanning = true;
+ }
+
+ public boolean stopScanning() {
+ if (!mIsScanning) {
+ return false;
+ }
+
+ if (AppGlobals.isDebug) {
+ Log.d(LOG_TAG, "Scanning stopped");
+ }
+
+ mGPSScanner.stop();
+ mWifiScanner.stop();
+ mCellScanner.stop();
+
+ mIsScanning = false;
+ return true;
+ }
+
+ public void setWifiBlockList(WifiBlockListInterface list) {
+ WifiScanner.setWifiBlockList(list);
+ }
+
+ public boolean isScanning() {
+ return mIsScanning;
+ }
+
+ public int getAPCount() {
+ return (mWifiScanner == null)? 0 : mWifiScanner.getAPCount();
+ }
+
+ public int getVisibleAPCount() {
+ return (mWifiScanner == null)? 0 :mWifiScanner.getVisibleAPCount();
+ }
+
+ public int getWifiStatus() {
+ return (mWifiScanner == null)? 0 : mWifiScanner.getStatus();
+ }
+
+ public int getCellInfoCount() {
+ return (mCellScanner == null)? 0 :mCellScanner.getCellInfoCount();
+ }
+
+ public int getLocationCount() {
+ return (mGPSScanner == null)? 0 : mGPSScanner.getLocationCount();
+ }
+
+ public double getLatitude() {
+ return (mGPSScanner == null)? 0.0 : mGPSScanner.getLatitude();
+ }
+
+ public double getLongitude() {
+ return (mGPSScanner == null)? 0.0 : mGPSScanner.getLongitude();
+ }
+
+ public Location getLocation() {
+ return (mGPSScanner == null)? new Location("null") : mGPSScanner.getLocation();
+ }
+
+ public void checkPrefs() {
+ if (mGPSScanner != null) {
+ mGPSScanner.checkPrefs();
+ }
+ }
+
+ public boolean isGeofenced() {
+ return (mGPSScanner != null) && mGPSScanner.isGeofenced();
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/WifiScanner.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/WifiScanner.java
new file mode 100644
index 0000000000..eed61d8bbf
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/WifiScanner.java
@@ -0,0 +1,228 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.stumblerthread.scanners;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.WifiLock;
+import android.support.v4.content.LocalBroadcastManager;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.mozilla.mozstumbler.service.AppGlobals;
+import org.mozilla.mozstumbler.service.stumblerthread.blocklist.BSSIDBlockList;
+import org.mozilla.mozstumbler.service.stumblerthread.blocklist.SSIDBlockList;
+import org.mozilla.mozstumbler.service.AppGlobals.ActiveOrPassiveStumbling;
+import org.mozilla.mozstumbler.service.Prefs;
+import org.mozilla.mozstumbler.service.stumblerthread.blocklist.WifiBlockListInterface;
+
+public class WifiScanner extends BroadcastReceiver {
+ public static final String ACTION_BASE = AppGlobals.ACTION_NAMESPACE + ".WifiScanner.";
+ public static final String ACTION_WIFIS_SCANNED = ACTION_BASE + "WIFIS_SCANNED";
+ public static final String ACTION_WIFIS_SCANNED_ARG_RESULTS = "scan_results";
+ public static final String ACTION_WIFIS_SCANNED_ARG_TIME = AppGlobals.ACTION_ARG_TIME;
+
+ public static final int STATUS_IDLE = 0;
+ public static final int STATUS_ACTIVE = 1;
+ public static final int STATUS_WIFI_DISABLED = -1;
+
+ private static final String LOG_TAG = AppGlobals.makeLogTag(WifiScanner.class.getSimpleName());
+ private static final long WIFI_MIN_UPDATE_TIME = 5000; // milliseconds
+
+ private boolean mStarted;
+ private final Context mContext;
+ private WifiLock mWifiLock;
+ private Timer mWifiScanTimer;
+ private final Set<String> mAPs = Collections.synchronizedSet(new HashSet<String>());
+ private final AtomicInteger mVisibleAPs = new AtomicInteger();
+
+ /* Testing */
+ public static boolean sIsTestMode;
+ public List<ScanResult> mTestModeFakeScanResults = new ArrayList<ScanResult>();
+ public Set<String> getAccessPoints(android.test.AndroidTestCase restrictedAccessor) { return mAPs; }
+ /* ------- */
+
+ public WifiScanner(Context c) {
+ mContext = c;
+ }
+
+ private boolean isWifiEnabled() {
+ return (sIsTestMode) || getWifiManager().isWifiEnabled();
+ }
+
+ private List<ScanResult> getScanResults() {
+ WifiManager manager = getWifiManager();
+ if (manager == null) {
+ return null;
+ }
+ return getWifiManager().getScanResults();
+ }
+
+
+ public synchronized void start(final ActiveOrPassiveStumbling stumblingMode) {
+ Prefs prefs = Prefs.getInstanceWithoutContext();
+ if (mStarted || prefs == null) {
+ return;
+ }
+ mStarted = true;
+
+ boolean scanAlways = prefs.getWifiScanAlways();
+
+ if (scanAlways || isWifiEnabled()) {
+ activatePeriodicScan(stumblingMode);
+ }
+
+ IntentFilter i = new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
+ if (!scanAlways) i.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
+ mContext.registerReceiver(this, i);
+ }
+
+ public synchronized void stop() {
+ if (mStarted) {
+ mContext.unregisterReceiver(this);
+ }
+ deactivatePeriodicScan();
+ mStarted = false;
+ }
+
+ @Override
+ public void onReceive(Context c, Intent intent) {
+ String action = intent.getAction();
+
+ if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) {
+ if (isWifiEnabled()) {
+ activatePeriodicScan(ActiveOrPassiveStumbling.ACTIVE_STUMBLING);
+ } else {
+ deactivatePeriodicScan();
+ }
+ } else if (WifiManager.SCAN_RESULTS_AVAILABLE_ACTION.equals(action)) {
+ final List<ScanResult> scanResultList = getScanResults();
+ if (scanResultList == null) {
+ return;
+ }
+ final ArrayList<ScanResult> scanResults = new ArrayList<ScanResult>();
+ for (ScanResult scanResult : scanResultList) {
+ scanResult.BSSID = BSSIDBlockList.canonicalizeBSSID(scanResult.BSSID);
+ if (shouldLog(scanResult)) {
+ scanResults.add(scanResult);
+ mAPs.add(scanResult.BSSID);
+ }
+ }
+ mVisibleAPs.set(scanResults.size());
+ reportScanResults(scanResults);
+ }
+ }
+
+ public static void setWifiBlockList(WifiBlockListInterface blockList) {
+ BSSIDBlockList.setFilterList(blockList.getBssidOuiList());
+ SSIDBlockList.setFilterLists(blockList.getSsidPrefixList(), blockList.getSsidSuffixList());
+ }
+
+ public int getAPCount() {
+ return mAPs.size();
+ }
+
+ public int getVisibleAPCount() {
+ return mVisibleAPs.get();
+ }
+
+ public synchronized int getStatus() {
+ if (!mStarted) {
+ return STATUS_IDLE;
+ }
+ if (mWifiScanTimer == null) {
+ return STATUS_WIFI_DISABLED;
+ }
+ return STATUS_ACTIVE;
+ }
+
+ private synchronized void activatePeriodicScan(final ActiveOrPassiveStumbling stumblingMode) {
+ if (mWifiScanTimer != null) {
+ return;
+ }
+
+ if (AppGlobals.isDebug) {
+ Log.v(LOG_TAG, "Activate Periodic Scan");
+ }
+
+ mWifiLock = getWifiManager().createWifiLock(WifiManager.WIFI_MODE_SCAN_ONLY, "MozStumbler");
+ mWifiLock.acquire();
+
+ // Ensure that we are constantly scanning for new access points.
+ mWifiScanTimer = new Timer();
+ mWifiScanTimer.schedule(new TimerTask() {
+ int mPassiveScanCount;
+ @Override
+ public void run() {
+ if (stumblingMode == ActiveOrPassiveStumbling.PASSIVE_STUMBLING &&
+ mPassiveScanCount++ > AppGlobals.PASSIVE_MODE_MAX_SCANS_PER_GPS)
+ {
+ mPassiveScanCount = 0;
+ stop(); // set mWifiScanTimer to null
+ return;
+ }
+ if (AppGlobals.isDebug) {
+ Log.v(LOG_TAG, "WiFi Scanning Timer fired");
+ }
+ getWifiManager().startScan();
+ }
+ }, 0, WIFI_MIN_UPDATE_TIME);
+ }
+
+ private synchronized void deactivatePeriodicScan() {
+ if (mWifiScanTimer == null) {
+ return;
+ }
+
+ if (AppGlobals.isDebug) {
+ Log.v(LOG_TAG, "Deactivate periodic scan");
+ }
+
+ mWifiLock.release();
+ mWifiLock = null;
+
+ mWifiScanTimer.cancel();
+ mWifiScanTimer = null;
+
+ mVisibleAPs.set(0);
+ }
+
+ public static boolean shouldLog(ScanResult scanResult) {
+ if (BSSIDBlockList.contains(scanResult)) {
+ return false;
+ }
+ if (SSIDBlockList.contains(scanResult)) {
+ return false;
+ }
+ return true;
+ }
+
+ private WifiManager getWifiManager() {
+ return (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+ }
+
+ private void reportScanResults(ArrayList<ScanResult> scanResults) {
+ if (scanResults.isEmpty()) {
+ return;
+ }
+
+ Intent i = new Intent(ACTION_WIFIS_SCANNED);
+ i.putParcelableArrayListExtra(ACTION_WIFIS_SCANNED_ARG_RESULTS, scanResults);
+ i.putExtra(ACTION_WIFIS_SCANNED_ARG_TIME, System.currentTimeMillis());
+ LocalBroadcastManager.getInstance(mContext).sendBroadcastSync(i);
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellInfo.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellInfo.java
new file mode 100644
index 0000000000..f435dcf114
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellInfo.java
@@ -0,0 +1,391 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.stumblerthread.scanners.cellscanner;
+
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.telephony.CellLocation;
+import android.telephony.NeighboringCellInfo;
+import android.telephony.TelephonyManager;
+import android.telephony.cdma.CdmaCellLocation;
+import android.telephony.gsm.GsmCellLocation;
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.mozstumbler.service.AppGlobals;
+
+public class CellInfo implements Parcelable {
+ private static final String LOG_TAG = AppGlobals.makeLogTag(CellInfo.class.getSimpleName());
+
+ public static final String RADIO_GSM = "gsm";
+ public static final String RADIO_CDMA = "cdma";
+ public static final String RADIO_WCDMA = "wcdma";
+
+ public static final String CELL_RADIO_GSM = "gsm";
+ public static final String CELL_RADIO_UMTS = "umts";
+ public static final String CELL_RADIO_CDMA = "cdma";
+ public static final String CELL_RADIO_LTE = "lte";
+
+ public static final int UNKNOWN_CID = -1;
+ public static final int UNKNOWN_SIGNAL = -1000;
+
+ public static final Parcelable.Creator<CellInfo> CREATOR
+ = new Parcelable.Creator<CellInfo>() {
+ @Override
+ public CellInfo createFromParcel(Parcel in) {
+ return new CellInfo(in);
+ }
+
+ @Override
+ public CellInfo[] newArray(int size) {
+ return new CellInfo[size];
+ }
+ };
+
+ private String mRadio;
+ private String mCellRadio;
+
+ private int mMcc;
+ private int mMnc;
+ private int mCid;
+ private int mLac;
+ private int mSignal;
+ private int mAsu;
+ private int mTa;
+ private int mPsc;
+
+ public CellInfo(int phoneType) {
+ reset();
+ setRadio(phoneType);
+ }
+
+ private CellInfo(Parcel in) {
+ mRadio = in.readString();
+ mCellRadio = in.readString();
+ mMcc = in.readInt();
+ mMnc = in.readInt();
+ mCid = in.readInt();
+ mLac = in.readInt();
+ mSignal = in.readInt();
+ mAsu = in.readInt();
+ mTa = in.readInt();
+ mPsc = in.readInt();
+ }
+
+ public boolean isCellRadioValid() {
+ return mCellRadio != null && (mCellRadio.length() > 0) && !mCellRadio.equals("0");
+ }
+
+ public String getRadio() {
+ return mRadio;
+ }
+
+ public String getCellRadio() {
+ return mCellRadio;
+ }
+
+ public int getMcc() {
+ return mMcc;
+ }
+
+ public int getMnc() {
+ return mMnc;
+ }
+
+ public int getCid() {
+ return mCid;
+ }
+
+ public int getLac() {
+ return mLac;
+ }
+
+ public int getPsc() {
+ return mPsc;
+ }
+
+ public JSONObject toJSONObject() {
+ final JSONObject obj = new JSONObject();
+
+ try {
+ obj.put("radio", getCellRadio());
+ obj.put("mcc", mMcc);
+ obj.put("mnc", mMnc);
+ if (mLac != UNKNOWN_CID) obj.put("lac", mLac);
+ if (mCid != UNKNOWN_CID) obj.put("cid", mCid);
+ if (mSignal != UNKNOWN_SIGNAL) obj.put("signal", mSignal);
+ if (mAsu != UNKNOWN_SIGNAL) obj.put("asu", mAsu);
+ if (mTa != UNKNOWN_CID) obj.put("ta", mTa);
+ if (mPsc != UNKNOWN_CID) obj.put("psc", mPsc);
+ } catch (JSONException jsonE) {
+ throw new IllegalStateException(jsonE);
+ }
+
+ return obj;
+ }
+
+ public String getCellIdentity() {
+ return getRadio()
+ + " " + getCellRadio()
+ + " " + getMcc()
+ + " " + getMnc()
+ + " " + getLac()
+ + " " + getCid()
+ + " " + getPsc();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mRadio);
+ dest.writeString(mCellRadio);
+ dest.writeInt(mMcc);
+ dest.writeInt(mMnc);
+ dest.writeInt(mCid);
+ dest.writeInt(mLac);
+ dest.writeInt(mSignal);
+ dest.writeInt(mAsu);
+ dest.writeInt(mTa);
+ dest.writeInt(mPsc);
+ }
+
+ void reset() {
+ mRadio = RADIO_GSM;
+ mCellRadio = CELL_RADIO_GSM;
+ mMcc = UNKNOWN_CID;
+ mMnc = UNKNOWN_CID;
+ mLac = UNKNOWN_CID;
+ mCid = UNKNOWN_CID;
+ mSignal = UNKNOWN_SIGNAL;
+ mAsu = UNKNOWN_SIGNAL;
+ mTa = UNKNOWN_CID;
+ mPsc = UNKNOWN_CID;
+ }
+
+ void setRadio(int phoneType) {
+ mRadio = getRadioTypeName(phoneType);
+ }
+
+ void setCellLocation(CellLocation cl,
+ int networkType,
+ String networkOperator,
+ Integer gsmSignalStrength,
+ Integer cdmaRssi) {
+ if (cl instanceof GsmCellLocation) {
+ final int lac, cid;
+ final GsmCellLocation gcl = (GsmCellLocation) cl;
+
+ reset();
+ mCellRadio = getCellRadioTypeName(networkType);
+ setNetworkOperator(networkOperator);
+
+ lac = gcl.getLac();
+ cid = gcl.getCid();
+ if (lac >= 0) mLac = lac;
+ if (cid >= 0) mCid = cid;
+
+ if (Build.VERSION.SDK_INT >= 9) {
+ final int psc = gcl.getPsc();
+ if (psc >= 0) mPsc = psc;
+ }
+
+ if (gsmSignalStrength != null) {
+ mAsu = gsmSignalStrength;
+ }
+ } else if (cl instanceof CdmaCellLocation) {
+ final CdmaCellLocation cdl = (CdmaCellLocation) cl;
+
+ reset();
+ mCellRadio = getCellRadioTypeName(networkType);
+
+ setNetworkOperator(networkOperator);
+
+ mMnc = cdl.getSystemId();
+
+ mLac = cdl.getNetworkId();
+ mCid = cdl.getBaseStationId();
+
+ if (cdmaRssi != null) {
+ mSignal = cdmaRssi;
+ }
+ } else {
+ throw new IllegalArgumentException("Unexpected CellLocation type: " + cl.getClass().getName());
+ }
+ }
+
+ void setNeighboringCellInfo(NeighboringCellInfo nci, String networkOperator) {
+ final int lac, cid, psc, rssi;
+
+ reset();
+ mCellRadio = getCellRadioTypeName(nci.getNetworkType());
+ setNetworkOperator(networkOperator);
+
+ lac = nci.getLac();
+ cid = nci.getCid();
+ psc = nci.getPsc();
+ rssi = nci.getRssi();
+
+ if (lac >= 0) mLac = lac;
+ if (cid >= 0) mCid = cid;
+ if (psc >= 0) mPsc = psc;
+ if (rssi != NeighboringCellInfo.UNKNOWN_RSSI) mAsu = rssi;
+ }
+
+ void setGsmCellInfo(int mcc, int mnc, int lac, int cid, int asu) {
+ mCellRadio = CELL_RADIO_GSM;
+ mMcc = mcc != Integer.MAX_VALUE ? mcc : UNKNOWN_CID;
+ mMnc = mnc != Integer.MAX_VALUE ? mnc : UNKNOWN_CID;
+ mLac = lac != Integer.MAX_VALUE ? lac : UNKNOWN_CID;
+ mCid = cid != Integer.MAX_VALUE ? cid : UNKNOWN_CID;
+ mAsu = asu;
+ }
+
+ public void setWcmdaCellInfo(int mcc, int mnc, int lac, int cid, int psc, int asu) {
+ mCellRadio = CELL_RADIO_UMTS;
+ mMcc = mcc != Integer.MAX_VALUE ? mcc : UNKNOWN_CID;
+ mMnc = mnc != Integer.MAX_VALUE ? mnc : UNKNOWN_CID;
+ mLac = lac != Integer.MAX_VALUE ? lac : UNKNOWN_CID;
+ mCid = cid != Integer.MAX_VALUE ? cid : UNKNOWN_CID;
+ mPsc = psc != Integer.MAX_VALUE ? psc : UNKNOWN_CID;
+ mAsu = asu;
+ }
+
+ /**
+ * @param mcc Mobile Country Code, Integer.MAX_VALUE if unknown
+ * @param mnc Mobile Network Code, Integer.MAX_VALUE if unknown
+ * @param ci Cell Identity, Integer.MAX_VALUE if unknown
+ * @param pci Physical Cell Id, Integer.MAX_VALUE if unknown
+ * @param tac Tracking Area Code, Integer.MAX_VALUE if unknown
+ * @param asu Arbitrary strength unit
+ * @param ta Timing advance
+ */
+ void setLteCellInfo(int mcc, int mnc, int ci, int pci, int tac, int asu, int ta) {
+ mCellRadio = CELL_RADIO_LTE;
+ mMcc = mcc != Integer.MAX_VALUE ? mcc : UNKNOWN_CID;
+ mMnc = mnc != Integer.MAX_VALUE ? mnc : UNKNOWN_CID;
+ mLac = tac != Integer.MAX_VALUE ? tac : UNKNOWN_CID;
+ mCid = ci != Integer.MAX_VALUE ? ci : UNKNOWN_CID;
+ mPsc = pci != Integer.MAX_VALUE ? pci : UNKNOWN_CID;
+ mAsu = asu;
+ mTa = ta;
+ }
+
+ void setCdmaCellInfo(int baseStationId, int networkId, int systemId, int dbm) {
+ mCellRadio = CELL_RADIO_CDMA;
+ mMnc = systemId != Integer.MAX_VALUE ? systemId : UNKNOWN_CID;
+ mLac = networkId != Integer.MAX_VALUE ? networkId : UNKNOWN_CID;
+ mCid = baseStationId != Integer.MAX_VALUE ? baseStationId : UNKNOWN_CID;
+ mSignal = dbm;
+ }
+
+ void setNetworkOperator(String mccMnc) {
+ if (mccMnc == null || mccMnc.length() < 5 || mccMnc.length() > 8) {
+ throw new IllegalArgumentException("Bad mccMnc: " + mccMnc);
+ }
+ mMcc = Integer.parseInt(mccMnc.substring(0, 3));
+ mMnc = Integer.parseInt(mccMnc.substring(3));
+ }
+
+ static String getCellRadioTypeName(int networkType) {
+ switch (networkType) {
+ // If the network is either GSM or any high-data-rate variant of it, the radio
+ // field should be specified as `gsm`. This includes `GSM`, `EDGE` and `GPRS`.
+ case TelephonyManager.NETWORK_TYPE_GPRS:
+ case TelephonyManager.NETWORK_TYPE_EDGE:
+ return CELL_RADIO_GSM;
+
+ // If the network is either UMTS or any high-data-rate variant of it, the radio
+ // field should be specified as `umts`. This includes `UMTS`, `HSPA`, `HSDPA`,
+ // `HSPA+` and `HSUPA`.
+ case TelephonyManager.NETWORK_TYPE_UMTS:
+ case TelephonyManager.NETWORK_TYPE_HSDPA:
+ case TelephonyManager.NETWORK_TYPE_HSUPA:
+ case TelephonyManager.NETWORK_TYPE_HSPA:
+ case TelephonyManager.NETWORK_TYPE_HSPAP:
+ return CELL_RADIO_UMTS;
+
+ case TelephonyManager.NETWORK_TYPE_LTE:
+ return CELL_RADIO_LTE;
+
+ // If the network is either CDMA or one of the EVDO variants, the radio
+ // field should be specified as `cdma`. This includes `1xRTT`, `CDMA`, `eHRPD`,
+ // `EVDO_0`, `EVDO_A`, `EVDO_B`, `IS95A` and `IS95B`.
+ case TelephonyManager.NETWORK_TYPE_EVDO_0:
+ case TelephonyManager.NETWORK_TYPE_EVDO_A:
+ case TelephonyManager.NETWORK_TYPE_EVDO_B:
+ case TelephonyManager.NETWORK_TYPE_1xRTT:
+ case TelephonyManager.NETWORK_TYPE_EHRPD:
+ case TelephonyManager.NETWORK_TYPE_IDEN:
+ return CELL_RADIO_CDMA;
+
+ default:
+ Log.e(LOG_TAG, "", new IllegalArgumentException("Unexpected network type: " + networkType));
+ return String.valueOf(networkType);
+ }
+ }
+
+ @SuppressWarnings("fallthrough")
+ private static String getRadioTypeName(int phoneType) {
+ switch (phoneType) {
+ case TelephonyManager.PHONE_TYPE_CDMA:
+ return RADIO_CDMA;
+
+ case TelephonyManager.PHONE_TYPE_GSM:
+ return RADIO_GSM;
+
+ default:
+ Log.e(LOG_TAG, "", new IllegalArgumentException("Unexpected phone type: " + phoneType));
+ // fallthrough
+
+ case TelephonyManager.PHONE_TYPE_NONE:
+ case TelephonyManager.PHONE_TYPE_SIP:
+ // These devices have no radio.
+ return "";
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof CellInfo)) {
+ return false;
+ }
+ CellInfo ci = (CellInfo) o;
+ return mRadio.equals(ci.mRadio)
+ && mCellRadio.equals(ci.mCellRadio)
+ && mMcc == ci.mMcc
+ && mMnc == ci.mMnc
+ && mCid == ci.mCid
+ && mLac == ci.mLac
+ && mSignal == ci.mSignal
+ && mAsu == ci.mAsu
+ && mTa == ci.mTa
+ && mPsc == ci.mPsc;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + mRadio.hashCode();
+ result = 31 * result + mCellRadio.hashCode();
+ result = 31 * result + mMcc;
+ result = 31 * result + mMnc;
+ result = 31 * result + mCid;
+ result = 31 * result + mLac;
+ result = 31 * result + mSignal;
+ result = 31 * result + mAsu;
+ result = 31 * result + mTa;
+ result = 31 * result + mPsc;
+ return result;
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScanner.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScanner.java
new file mode 100644
index 0000000000..193de99234
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScanner.java
@@ -0,0 +1,178 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.stumblerthread.scanners.cellscanner;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Message;
+import android.support.v4.content.LocalBroadcastManager;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import org.mozilla.mozstumbler.service.AppGlobals;
+import org.mozilla.mozstumbler.service.AppGlobals.ActiveOrPassiveStumbling;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.mozilla.mozstumbler.service.AppGlobals.ActiveOrPassiveStumbling;
+import org.mozilla.mozstumbler.service.stumblerthread.Reporter;
+
+public class CellScanner {
+ public static final String ACTION_BASE = AppGlobals.ACTION_NAMESPACE + ".CellScanner.";
+ public static final String ACTION_CELLS_SCANNED = ACTION_BASE + "CELLS_SCANNED";
+ public static final String ACTION_CELLS_SCANNED_ARG_CELLS = "cells";
+ public static final String ACTION_CELLS_SCANNED_ARG_TIME = AppGlobals.ACTION_ARG_TIME;
+
+ private static final String LOG_TAG = AppGlobals.makeLogTag(CellScanner.class.getSimpleName());
+ private static final long CELL_MIN_UPDATE_TIME = 1000; // milliseconds
+
+ private final Context mContext;
+ private Timer mCellScanTimer;
+ private final Set<String> mCells = new HashSet<String>();
+ private final ReportFlushedReceiver mReportFlushedReceiver = new ReportFlushedReceiver();
+ private final AtomicBoolean mReportWasFlushed = new AtomicBoolean();
+ private Handler mBroadcastScannedHandler;
+ private final CellScannerImpl mCellScannerImplementation;
+
+ public ArrayList<CellInfo> sTestingModeCellInfoArray;
+
+ public interface CellScannerImpl {
+ void start();
+ boolean isStarted();
+ boolean isSupportedOnThisDevice();
+ void stop();
+ List<CellInfo> getCellInfo();
+ }
+
+ public CellScanner(Context context) {
+ mContext = context;
+ mCellScannerImplementation = new CellScannerImplementation(context);
+ }
+
+ public void start(final ActiveOrPassiveStumbling stumblingMode) {
+ if (!mCellScannerImplementation.isSupportedOnThisDevice()) {
+ return;
+ }
+
+ if (mCellScanTimer != null) {
+ return;
+ }
+
+ LocalBroadcastManager.getInstance(mContext).registerReceiver(mReportFlushedReceiver,
+ new IntentFilter(Reporter.ACTION_NEW_BUNDLE));
+
+ // This is to ensure the broadcast happens from the same thread the CellScanner start() is on
+ mBroadcastScannedHandler = new BroadcastScannedHandler(this);
+
+ mCellScannerImplementation.start();
+
+ mCellScanTimer = new Timer();
+
+ mCellScanTimer.schedule(new TimerTask() {
+ int mPassiveScanCount;
+ @Override
+ public void run() {
+ if (!mCellScannerImplementation.isStarted()) {
+ return;
+ }
+
+ if (stumblingMode == ActiveOrPassiveStumbling.PASSIVE_STUMBLING &&
+ mPassiveScanCount++ > AppGlobals.PASSIVE_MODE_MAX_SCANS_PER_GPS)
+ {
+ mPassiveScanCount = 0;
+ stop();
+ return;
+ }
+
+ final long curTime = System.currentTimeMillis();
+
+ ArrayList<CellInfo> cells = (sTestingModeCellInfoArray != null)? sTestingModeCellInfoArray :
+ new ArrayList<CellInfo>(mCellScannerImplementation.getCellInfo());
+
+ if (mReportWasFlushed.getAndSet(false)) {
+ clearCells();
+ }
+
+ if (cells.isEmpty()) {
+ return;
+ }
+
+ for (CellInfo cell : cells) {
+ addToCells(cell.getCellIdentity());
+ }
+
+ Intent intent = new Intent(ACTION_CELLS_SCANNED);
+ intent.putParcelableArrayListExtra(ACTION_CELLS_SCANNED_ARG_CELLS, cells);
+ intent.putExtra(ACTION_CELLS_SCANNED_ARG_TIME, curTime);
+ // send to handler, so broadcast is not from timer thread
+ Message message = new Message();
+ message.obj = intent;
+ mBroadcastScannedHandler.sendMessage(message);
+
+ }
+ }, 0, CELL_MIN_UPDATE_TIME);
+ }
+
+ private synchronized void clearCells() {
+ mCells.clear();
+ }
+
+ private synchronized void addToCells(String cell) {
+ mCells.add(cell);
+ }
+
+ public synchronized void stop() {
+ mReportWasFlushed.set(false);
+ clearCells();
+ LocalBroadcastManager.getInstance(mContext).unregisterReceiver(mReportFlushedReceiver);
+
+ if (mCellScanTimer != null) {
+ mCellScanTimer.cancel();
+ mCellScanTimer = null;
+ }
+ mCellScannerImplementation.stop();
+ }
+
+ public synchronized int getCellInfoCount() {
+ return mCells.size();
+ }
+
+ private class ReportFlushedReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context c, Intent i) {
+ mReportWasFlushed.set(true);
+ }
+ }
+
+ // Note: this reimplements org.mozilla.gecko.util.WeakReferenceHandler because it's not available here.
+ private static class BroadcastScannedHandler extends Handler {
+ private WeakReference<CellScanner> mTarget;
+
+ public BroadcastScannedHandler(final CellScanner that) {
+ super();
+ mTarget = new WeakReference<>(that);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ final CellScanner that = mTarget.get();
+ if (that == null) {
+ return;
+ }
+
+ final Intent intent = (Intent) msg.obj;
+ LocalBroadcastManager.getInstance(that.mContext).sendBroadcastSync(intent);
+ }
+ };
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScannerImplementation.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScannerImplementation.java
new file mode 100644
index 0000000000..c6674a3c44
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScannerImplementation.java
@@ -0,0 +1,299 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.stumblerthread.scanners.cellscanner;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.telephony.CellIdentityCdma;
+import android.telephony.CellIdentityGsm;
+import android.telephony.CellIdentityLte;
+import android.telephony.CellIdentityWcdma;
+import android.telephony.CellInfoCdma;
+import android.telephony.CellInfoGsm;
+import android.telephony.CellInfoLte;
+import android.telephony.CellInfoWcdma;
+import android.telephony.CellLocation;
+import android.telephony.CellSignalStrengthCdma;
+import android.telephony.CellSignalStrengthGsm;
+import android.telephony.CellSignalStrengthLte;
+import android.telephony.CellSignalStrengthWcdma;
+import android.telephony.NeighboringCellInfo;
+import android.telephony.PhoneStateListener;
+import android.telephony.SignalStrength;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import org.mozilla.mozstumbler.service.AppGlobals;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+public class CellScannerImplementation implements CellScanner.CellScannerImpl {
+
+ protected static String LOG_TAG = AppGlobals.makeLogTag(CellScannerImplementation.class.getSimpleName());
+ protected GetAllCellInfoScannerImpl mGetAllInfoCellScanner;
+ protected TelephonyManager mTelephonyManager;
+ protected boolean mIsStarted;
+ protected int mPhoneType;
+ protected final Context mContext;
+
+ protected volatile int mSignalStrength = CellInfo.UNKNOWN_SIGNAL;
+ protected volatile int mCdmaDbm = CellInfo.UNKNOWN_SIGNAL;
+
+ private PhoneStateListener mPhoneStateListener;
+
+ private static class GetAllCellInfoScannerDummy implements GetAllCellInfoScannerImpl {
+ @Override
+ public List<CellInfo> getAllCellInfo(TelephonyManager tm) {
+ return Collections.emptyList();
+ }
+ }
+
+ interface GetAllCellInfoScannerImpl {
+ List<CellInfo> getAllCellInfo(TelephonyManager tm);
+ }
+
+ public CellScannerImplementation(Context context) {
+ mContext = context;
+ }
+
+ public boolean isSupportedOnThisDevice() {
+ TelephonyManager telephonyManager = mTelephonyManager;
+ if (telephonyManager == null) {
+ telephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+ }
+ return telephonyManager != null &&
+ (telephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA ||
+ telephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM);
+ }
+
+ @Override
+ public synchronized boolean isStarted() {
+ return mIsStarted;
+ }
+
+ @Override
+ public synchronized void start() {
+ if (mIsStarted || !isSupportedOnThisDevice()) {
+ return;
+ }
+ mIsStarted = true;
+
+ if (mTelephonyManager == null) {
+ if (Build.VERSION.SDK_INT >= 18 /*Build.VERSION_CODES.JELLY_BEAN_MR2 */) {
+ mGetAllInfoCellScanner = new GetAllCellInfoScannerMr2();
+ } else {
+ mGetAllInfoCellScanner = new GetAllCellInfoScannerDummy();
+ }
+
+ mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+ }
+
+ mPhoneStateListener = new PhoneStateListener() {
+ @Override
+ public void onSignalStrengthsChanged(SignalStrength ss) {
+ if (ss.isGsm()) {
+ mSignalStrength = ss.getGsmSignalStrength();
+ } else {
+ mCdmaDbm = ss.getCdmaDbm();
+ }
+ }
+ };
+ mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
+ }
+
+ @Override
+ public synchronized void stop() {
+ mIsStarted = false;
+ if (mTelephonyManager != null && mPhoneStateListener != null) {
+ mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
+ }
+ mSignalStrength = CellInfo.UNKNOWN_SIGNAL;
+ mCdmaDbm = CellInfo.UNKNOWN_SIGNAL;
+ }
+
+ @Override
+ public synchronized List<CellInfo> getCellInfo() {
+ List<CellInfo> records = new ArrayList<CellInfo>();
+
+ List<CellInfo> allCells = mGetAllInfoCellScanner.getAllCellInfo(mTelephonyManager);
+ if (allCells.isEmpty()) {
+ CellInfo currentCell = getCurrentCellInfo();
+ if (currentCell == null) {
+ return records;
+ }
+ records.add(currentCell);
+ }else {
+ records.addAll(allCells);
+ }
+
+ // getNeighboringCells() sometimes contains more information than that is already
+ // in getAllCellInfo(). Use the results of both of them.
+ records.addAll(getNeighboringCells());
+ return records;
+ }
+
+ private String getNetworkOperator() {
+ String networkOperator = mTelephonyManager.getNetworkOperator();
+ // getNetworkOperator() may be unreliable on CDMA networks
+ if (networkOperator == null || networkOperator.length() <= 3) {
+ networkOperator = mTelephonyManager.getSimOperator();
+ }
+ return networkOperator;
+ }
+
+ protected CellInfo getCurrentCellInfo() {
+ final CellLocation currentCell = mTelephonyManager.getCellLocation();
+ if (currentCell == null) {
+ return null;
+ }
+
+ try {
+ final CellInfo info = new CellInfo(mPhoneType);
+ final int signalStrength = mSignalStrength;
+ final int cdmaDbm = mCdmaDbm;
+ info.setCellLocation(currentCell,
+ mTelephonyManager.getNetworkType(),
+ getNetworkOperator(),
+ signalStrength == CellInfo.UNKNOWN_SIGNAL ? null : signalStrength,
+ cdmaDbm == CellInfo.UNKNOWN_SIGNAL ? null : cdmaDbm);
+ return info;
+ } catch (IllegalArgumentException iae) {
+ Log.e(LOG_TAG, "Skip invalid or incomplete CellLocation: " + currentCell, iae);
+ }
+ return null;
+ }
+
+ private List<CellInfo> getNeighboringCells() {
+ // For max fennec compatibility, avoid VERSION_CODES
+ if (Build.VERSION.SDK_INT >= 22 /* Build.VERSION_CODES.LOLLIPOP_MR1 */) {
+ return Collections.emptyList();
+ }
+
+ @SuppressWarnings("deprecation")
+ Collection<NeighboringCellInfo> cells = mTelephonyManager.getNeighboringCellInfo();
+ if (cells == null || cells.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ String networkOperator = getNetworkOperator();
+ List<CellInfo> records = new ArrayList<CellInfo>(cells.size());
+ for (NeighboringCellInfo nci : cells) {
+ try {
+ final CellInfo record = new CellInfo(mPhoneType);
+ record.setNeighboringCellInfo(nci, networkOperator);
+ if (record.isCellRadioValid()) {
+ records.add(record);
+ }
+ } catch (IllegalArgumentException iae) {
+ Log.e(LOG_TAG, "Skip invalid or incomplete NeighboringCellInfo: " + nci, iae);
+ }
+ }
+ return records;
+ }
+
+
+ @TargetApi(18)
+ protected boolean addWCDMACellToList(List<CellInfo> cells,
+ android.telephony.CellInfo observedCell,
+ TelephonyManager tm) {
+ boolean added = false;
+ if (Build.VERSION.SDK_INT >= 18 &&
+ observedCell instanceof CellInfoWcdma) {
+ CellIdentityWcdma ident = ((CellInfoWcdma) observedCell).getCellIdentity();
+ if (ident.getMnc() != Integer.MAX_VALUE && ident.getMcc() != Integer.MAX_VALUE) {
+ CellInfo cell = new CellInfo(tm.getPhoneType());
+ CellSignalStrengthWcdma strength = ((CellInfoWcdma) observedCell).getCellSignalStrength();
+ cell.setWcmdaCellInfo(ident.getMcc(),
+ ident.getMnc(),
+ ident.getLac(),
+ ident.getCid(),
+ ident.getPsc(),
+ strength.getAsuLevel());
+ cells.add(cell);
+ added = true;
+ }
+ }
+ return added;
+ }
+
+ @TargetApi(18)
+ protected boolean addCellToList(List<CellInfo> cells,
+ android.telephony.CellInfo observedCell,
+ TelephonyManager tm) {
+ if (tm.getPhoneType() == 0) {
+ return false;
+ }
+
+ boolean added = false;
+ if (observedCell instanceof CellInfoGsm) {
+ CellIdentityGsm ident = ((CellInfoGsm) observedCell).getCellIdentity();
+ if (ident.getMcc() != Integer.MAX_VALUE && ident.getMnc() != Integer.MAX_VALUE) {
+ CellSignalStrengthGsm strength = ((CellInfoGsm) observedCell).getCellSignalStrength();
+ CellInfo cell = new CellInfo(tm.getPhoneType());
+ cell.setGsmCellInfo(ident.getMcc(),
+ ident.getMnc(),
+ ident.getLac(),
+ ident.getCid(),
+ strength.getAsuLevel());
+ cells.add(cell);
+ added = true;
+ }
+ } else if (observedCell instanceof CellInfoCdma) {
+ CellInfo cell = new CellInfo(tm.getPhoneType());
+ CellIdentityCdma ident = ((CellInfoCdma) observedCell).getCellIdentity();
+ CellSignalStrengthCdma strength = ((CellInfoCdma) observedCell).getCellSignalStrength();
+ cell.setCdmaCellInfo(ident.getBasestationId(),
+ ident.getNetworkId(),
+ ident.getSystemId(),
+ strength.getDbm());
+ cells.add(cell);
+ added = true;
+ } else if (observedCell instanceof CellInfoLte) {
+ CellIdentityLte ident = ((CellInfoLte) observedCell).getCellIdentity();
+ if (ident.getMnc() != Integer.MAX_VALUE && ident.getMcc() != Integer.MAX_VALUE) {
+ CellInfo cell = new CellInfo(tm.getPhoneType());
+ CellSignalStrengthLte strength = ((CellInfoLte) observedCell).getCellSignalStrength();
+ cell.setLteCellInfo(ident.getMcc(),
+ ident.getMnc(),
+ ident.getCi(),
+ ident.getPci(),
+ ident.getTac(),
+ strength.getAsuLevel(),
+ strength.getTimingAdvance());
+ cells.add(cell);
+ added = true;
+ }
+ }
+
+ if (!added && Build.VERSION.SDK_INT >= 18) {
+ added = addWCDMACellToList(cells, observedCell, tm);
+ }
+
+ return added;
+ }
+
+ @TargetApi(18)
+ private class GetAllCellInfoScannerMr2 implements GetAllCellInfoScannerImpl {
+ @Override
+ public List<CellInfo> getAllCellInfo(TelephonyManager tm) {
+ final List<android.telephony.CellInfo> observed = tm.getAllCellInfo();
+ if (observed == null || observed.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ List<CellInfo> cells = new ArrayList<CellInfo>(observed.size());
+ for (android.telephony.CellInfo observedCell : observed) {
+ if (!addCellToList(cells, observedCell, tm)) {
+ //Log.i(LOG_TAG, "Skipped CellInfo of unknown class: " + observedCell.toString());
+ }
+ }
+ return cells;
+ }
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/uploadthread/AsyncUploader.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/uploadthread/AsyncUploader.java
new file mode 100644
index 0000000000..308e356788
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/uploadthread/AsyncUploader.java
@@ -0,0 +1,214 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.uploadthread;
+
+import android.os.AsyncTask;
+import android.util.Log;
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.mozilla.mozstumbler.service.Prefs;
+import org.mozilla.mozstumbler.service.utils.AbstractCommunicator;
+import org.mozilla.mozstumbler.service.utils.AbstractCommunicator.SyncSummary;
+import org.mozilla.mozstumbler.service.AppGlobals;
+import org.mozilla.mozstumbler.service.stumblerthread.datahandling.DataStorageManager;
+import org.mozilla.mozstumbler.service.utils.NetworkUtils;
+
+/* Only one at a time may be uploading. If executed while another upload is in progress
+* it will return immediately, and SyncResult is null.
+*
+* Threading:
+* Uploads on a separate thread. ONLY DataStorageManager is thread-safe, do not call
+* preferences, do not call any code that isn't thread-safe. You will cause suffering.
+* An exception is made for AppGlobals.isDebug, a false reading is of no consequence. */
+public class AsyncUploader extends AsyncTask<Void, Void, SyncSummary> {
+ private static final String LOG_TAG = AppGlobals.makeLogTag(AsyncUploader.class.getSimpleName());
+ private final AsyncUploadArgs mUploadArgs;
+ private final Object mListenerLock = new Object();
+ private AsyncUploaderListener mListener;
+ private static final AtomicBoolean sIsUploading = new AtomicBoolean();
+ private String mNickname;
+
+ public interface AsyncUploaderListener {
+ public void onUploadComplete(SyncSummary result);
+ public void onUploadProgress();
+ }
+
+ public static class AsyncUploadArgs {
+ public final NetworkUtils mNetworkUtils;
+ public final boolean mShouldIgnoreWifiStatus;
+ public final boolean mUseWifiOnly;
+ public AsyncUploadArgs(NetworkUtils networkUtils,
+ boolean shouldIgnoreWifiStatus,
+ boolean useWifiOnly) {
+ mNetworkUtils = networkUtils;
+ mShouldIgnoreWifiStatus = shouldIgnoreWifiStatus;
+ mUseWifiOnly = useWifiOnly;
+ }
+ }
+
+ public AsyncUploader(AsyncUploadArgs args, AsyncUploaderListener listener) {
+ mListener = listener;
+ mUploadArgs = args;
+ }
+
+ public void setNickname(String name) {
+ mNickname = name;
+ }
+
+ public void clearListener() {
+ synchronized (mListenerLock) {
+ mListener = null;
+ }
+ }
+
+ public static boolean isUploading() {
+ return sIsUploading.get();
+ }
+
+ @Override
+ protected SyncSummary doInBackground(Void... voids) {
+ if (sIsUploading.get()) {
+ // This if-block is not synchronized, don't care, this is an erroneous usage.
+ Log.d(LOG_TAG, "Usage error: check isUploading first, only one at a time task usage is permitted.");
+ return null;
+ }
+
+ sIsUploading.set(true);
+ SyncSummary result = new SyncSummary();
+ Runnable progressListener = null;
+
+ // no need to lock here, lock is checked again later
+ if (mListener != null) {
+ progressListener = new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mListenerLock) {
+ if (mListener != null) {
+ mListener.onUploadProgress();
+ }
+ }
+ }
+ };
+ }
+
+ uploadReports(result, progressListener);
+
+ return result;
+ }
+ @Override
+ protected void onPostExecute(SyncSummary result) {
+ sIsUploading.set(false);
+
+ synchronized (mListenerLock) {
+ if (mListener != null) {
+ mListener.onUploadComplete(result);
+ }
+ }
+ }
+ @Override
+ protected void onCancelled(SyncSummary result) {
+ sIsUploading.set(false);
+ }
+
+ private class Submitter extends AbstractCommunicator {
+ private static final String SUBMIT_URL = "https://location.services.mozilla.com/v1/submit";
+
+ @Override
+ public String getUrlString() {
+ return SUBMIT_URL;
+ }
+
+ @Override
+ public String getNickname(){
+ return mNickname;
+ }
+
+ @Override
+ public NetworkSendResult cleanSend(byte[] data) {
+ final NetworkSendResult result = new NetworkSendResult();
+ try {
+ result.bytesSent = this.send(data, ZippedState.eAlreadyZipped);
+ result.errorCode = 0;
+ } catch (IOException ex) {
+ String msg = "Error submitting: " + ex;
+ if (ex instanceof HttpErrorException) {
+ result.errorCode = ((HttpErrorException) ex).responseCode;
+ msg += " Code:" + result.errorCode;
+ }
+ Log.e(LOG_TAG, msg);
+ AppGlobals.guiLogError(msg);
+ }
+ return result;
+ }
+ }
+
+ private void uploadReports(AbstractCommunicator.SyncSummary syncResult, Runnable progressListener) {
+ long uploadedObservations = 0;
+ long uploadedCells = 0;
+ long uploadedWifis = 0;
+
+ if (!mUploadArgs.mShouldIgnoreWifiStatus && mUploadArgs.mUseWifiOnly &&
+ !mUploadArgs.mNetworkUtils.isWifiAvailable()) {
+ if (AppGlobals.isDebug) {
+ Log.d(LOG_TAG, "not on WiFi, not sending");
+ }
+ syncResult.numIoExceptions += 1;
+ return;
+ }
+
+ Submitter submitter = new Submitter();
+ DataStorageManager dm = DataStorageManager.getInstance();
+
+ String error = null;
+
+ try {
+ DataStorageManager.ReportBatch batch = dm.getFirstBatch();
+ while (batch != null) {
+ AbstractCommunicator.NetworkSendResult result = submitter.cleanSend(batch.data);
+
+ if (result.errorCode == 0) {
+ syncResult.totalBytesSent += result.bytesSent;
+
+ dm.delete(batch.filename);
+
+ uploadedObservations += batch.reportCount;
+ uploadedWifis += batch.wifiCount;
+ uploadedCells += batch.cellCount;
+ } else {
+ if (result.errorCode / 100 == 4) {
+ // delete on 4xx, no point in resending
+ dm.delete(batch.filename);
+ } else {
+ DataStorageManager.getInstance().saveCurrentReportsSendBufferToDisk();
+ }
+ syncResult.numIoExceptions += 1;
+ }
+
+ if (progressListener != null) {
+ progressListener.run();
+ }
+
+ batch = dm.getNextBatch();
+ }
+ }
+ catch (IOException ex) {
+ error = ex.toString();
+ }
+
+ try {
+ dm.incrementSyncStats(syncResult.totalBytesSent, uploadedObservations, uploadedCells, uploadedWifis);
+ } catch (IOException ex) {
+ error = ex.toString();
+ } finally {
+ if (error != null) {
+ syncResult.numIoExceptions += 1;
+ Log.d(LOG_TAG, error);
+ AppGlobals.guiLogError(error + " (uploadReports)");
+ }
+ submitter.close();
+ }
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/uploadthread/UploadAlarmReceiver.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/uploadthread/UploadAlarmReceiver.java
new file mode 100644
index 0000000000..d6680a1613
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/uploadthread/UploadAlarmReceiver.java
@@ -0,0 +1,138 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.uploadthread;
+
+import android.app.AlarmManager;
+import android.app.IntentService;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import org.mozilla.mozstumbler.service.AppGlobals;
+import org.mozilla.mozstumbler.service.Prefs;
+import org.mozilla.mozstumbler.service.stumblerthread.datahandling.DataStorageManager;
+import org.mozilla.mozstumbler.service.utils.NetworkUtils;
+
+// Only if data is queued and device awake: check network availability and upload.
+// MozStumbler use: this alarm is periodic and repeating.
+// Fennec use: The alarm is single-shot and it is set to run -if there is data in the queue-
+// under these conditions:
+// 1) Fennec start/pause (actually gecko start which is ~4 sec after Fennec start).
+// 2) Changing the pref in Fennec to stumble or not.
+// 3) Boot intent (and SD card app available intent).
+//
+// Threading:
+// - scheduled from the stumbler thread
+// - triggered from the main thread
+// - actual work is done the upload thread (AsyncUploader)
+public class UploadAlarmReceiver extends BroadcastReceiver {
+ private static final String LOG_TAG = AppGlobals.makeLogTag(UploadAlarmReceiver.class.getSimpleName());
+ private static final String EXTRA_IS_REPEATING = "is_repeating";
+ private static boolean sIsAlreadyScheduled;
+
+ public UploadAlarmReceiver() {}
+
+ public static class UploadAlarmService extends IntentService {
+
+ public UploadAlarmService(String name) {
+ super(name);
+ // makes the service START_NOT_STICKY, that is, the service is not auto-restarted
+ setIntentRedelivery(false);
+ }
+
+ public UploadAlarmService() {
+ this(LOG_TAG);
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ if (intent == null) {
+ return;
+ }
+ boolean isRepeating = intent.getBooleanExtra(EXTRA_IS_REPEATING, true);
+ if (DataStorageManager.getInstance() == null) {
+ DataStorageManager.createGlobalInstance(this, null);
+ }
+ upload(isRepeating);
+ }
+
+ void upload(boolean isRepeating) {
+ if (!isRepeating) {
+ sIsAlreadyScheduled = false;
+ }
+
+ // Defensive approach: if it is too old, delete all data
+ long oldestMs = DataStorageManager.getInstance().getOldestBatchTimeMs();
+ int maxWeeks = DataStorageManager.getInstance().getMaxWeeksStored();
+ if (oldestMs > 0) {
+ long currentTime = System.currentTimeMillis();
+ long msPerWeek = 604800 * 1000;
+ if (currentTime - oldestMs > maxWeeks * msPerWeek) {
+ DataStorageManager.getInstance().deleteAll();
+ UploadAlarmReceiver.cancelAlarm(this, isRepeating);
+ return;
+ }
+ }
+
+ NetworkUtils networkUtils = new NetworkUtils(this);
+ if (networkUtils.isWifiAvailable() &&
+ !AsyncUploader.isUploading()) {
+ Log.d(LOG_TAG, "Alarm upload(), call AsyncUploader");
+ AsyncUploader.AsyncUploadArgs settings =
+ new AsyncUploader.AsyncUploadArgs(networkUtils,
+ Prefs.getInstance(this).getWifiScanAlways(),
+ Prefs.getInstance(this).getUseWifiOnly());
+ AsyncUploader uploader = new AsyncUploader(settings, null);
+ uploader.setNickname(Prefs.getInstance(this).getNickname());
+ uploader.execute();
+ // we could listen for completion and cancel, instead, cancel on next alarm when db empty
+ }
+ }
+ }
+
+ static PendingIntent createIntent(Context c, boolean isRepeating) {
+ Intent intent = new Intent(c, UploadAlarmReceiver.class);
+ intent.putExtra(EXTRA_IS_REPEATING, isRepeating);
+ PendingIntent pi = PendingIntent.getBroadcast(c, 0, intent, 0);
+ return pi;
+ }
+
+ public static void cancelAlarm(Context c, boolean isRepeating) {
+ Log.d(LOG_TAG, "cancelAlarm");
+ // this is to stop scheduleAlarm from constantly rescheduling, not to guard cancellation.
+ sIsAlreadyScheduled = false;
+ AlarmManager alarmManager = (AlarmManager) c.getSystemService(Context.ALARM_SERVICE);
+ PendingIntent pi = createIntent(c, isRepeating);
+ alarmManager.cancel(pi);
+ }
+
+ public static void scheduleAlarm(Context c, long secondsToWait, boolean isRepeating) {
+ if (sIsAlreadyScheduled) {
+ return;
+ }
+
+ long intervalMsec = secondsToWait * 1000;
+ Log.d(LOG_TAG, "schedule alarm (ms):" + intervalMsec);
+
+ sIsAlreadyScheduled = true;
+ AlarmManager alarmManager = (AlarmManager) c.getSystemService(Context.ALARM_SERVICE);
+ PendingIntent pi = createIntent(c, isRepeating);
+
+ long triggerAtMs = System.currentTimeMillis() + intervalMsec;
+ if (isRepeating) {
+ alarmManager.setInexactRepeating(AlarmManager.RTC, triggerAtMs, intervalMsec, pi);
+ } else {
+ alarmManager.set(AlarmManager.RTC, triggerAtMs, pi);
+ }
+ }
+
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ Intent startServiceIntent = new Intent(context, UploadAlarmService.class);
+ context.startService(startServiceIntent);
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/AbstractCommunicator.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/AbstractCommunicator.java
new file mode 100644
index 0000000000..70816371a3
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/AbstractCommunicator.java
@@ -0,0 +1,158 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.utils;
+
+import android.os.Build;
+import android.util.Log;
+
+import org.mozilla.mozstumbler.service.AppGlobals;
+import org.mozilla.mozstumbler.service.Prefs;
+
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public abstract class AbstractCommunicator {
+
+ private static final String LOG_TAG = AppGlobals.makeLogTag(AbstractCommunicator.class.getSimpleName());
+ private static final String NICKNAME_HEADER = "X-Nickname";
+ private static final String USER_AGENT_HEADER = "User-Agent";
+ private HttpURLConnection mHttpURLConnection;
+ private final String mUserAgent;
+ private static int sBytesSentTotal = 0;
+ private static String sMozApiKey;
+
+ public abstract String getUrlString();
+
+ public static class HttpErrorException extends IOException {
+ private static final long serialVersionUID = -5404095858043243126L;
+ public final int responseCode;
+
+ public HttpErrorException(int responseCode) {
+ super();
+ this.responseCode = responseCode;
+ }
+
+ public boolean isTemporary() {
+ return responseCode >= 500 && responseCode <= 599;
+ }
+ }
+
+ public static class SyncSummary {
+ public int numIoExceptions;
+ public int totalBytesSent;
+ }
+
+ public static class NetworkSendResult {
+ public int bytesSent;
+ // Zero is no error, for HTTP error cases, set this code to the error
+ public int errorCode = -1;
+ }
+
+ public abstract NetworkSendResult cleanSend(byte[] data);
+
+ public String getNickname() {
+ return null;
+ }
+
+ public AbstractCommunicator() {
+ Prefs prefs = Prefs.getInstanceWithoutContext();
+ mUserAgent = (prefs != null)? prefs.getUserAgent() : "fennec-stumbler-unset-user-agent";
+ }
+
+ private void openConnectionAndSetHeaders() {
+ try {
+ Prefs prefs = Prefs.getInstanceWithoutContext();
+ if (sMozApiKey == null || prefs != null) {
+ sMozApiKey = prefs.getMozApiKey();
+ }
+ URL url = new URL(getUrlString() + "?key=" + sMozApiKey);
+ mHttpURLConnection = (HttpURLConnection) url.openConnection();
+ mHttpURLConnection.setRequestMethod("POST");
+ } catch (MalformedURLException e) {
+ throw new IllegalArgumentException(e);
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "Couldn't open a connection: " + e);
+ }
+ mHttpURLConnection.setDoOutput(true);
+ mHttpURLConnection.setRequestProperty(USER_AGENT_HEADER, mUserAgent);
+ mHttpURLConnection.setRequestProperty("Content-Type", "application/json");
+
+ // Workaround for a bug in Android mHttpURLConnection. When the library
+ // reuses a stale connection, the connection may fail with an EOFException
+ if (Build.VERSION.SDK_INT > 13 && Build.VERSION.SDK_INT < 19) {
+ mHttpURLConnection.setRequestProperty("Connection", "Close");
+ }
+ String nickname = getNickname();
+ if (nickname != null) {
+ mHttpURLConnection.setRequestProperty(NICKNAME_HEADER, nickname);
+ }
+ }
+
+ private byte[] zipData(byte[] data) throws IOException {
+ byte[] output = Zipper.zipData(data);
+ return output;
+ }
+
+ private void sendData(byte[] data) throws IOException{
+ mHttpURLConnection.setFixedLengthStreamingMode(data.length);
+ OutputStream out = new BufferedOutputStream(mHttpURLConnection.getOutputStream());
+ out.write(data);
+ out.flush();
+ int code = mHttpURLConnection.getResponseCode();
+ final boolean isSuccessCode2XX = (code/100 == 2);
+ if (!isSuccessCode2XX) {
+ throw new HttpErrorException(code);
+ }
+ }
+
+ public enum ZippedState { eNotZipped, eAlreadyZipped };
+ /* Return the number of bytes sent. */
+ public int send(byte[] data, ZippedState isAlreadyZipped) throws IOException {
+ openConnectionAndSetHeaders();
+ String logMsg;
+ try {
+ if (isAlreadyZipped != ZippedState.eAlreadyZipped) {
+ data = zipData(data);
+ }
+ mHttpURLConnection.setRequestProperty("Content-Encoding","gzip");
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "Couldn't compress and send data, falling back to plain-text: ", e);
+ close();
+ }
+
+ try {
+ sendData(data);
+ } finally {
+ close();
+ }
+ sBytesSentTotal += data.length;
+ logMsg = "Send data: " + String.format("%.2f", data.length / 1024.0) + " kB";
+ logMsg += " Session Total:" + String.format("%.2f", sBytesSentTotal / 1024.0) + " kB";
+ AppGlobals.guiLogInfo(logMsg, "#FFFFCC", true);
+ Log.d(LOG_TAG, logMsg);
+ return data.length;
+ }
+
+ public InputStream getInputStream() {
+ try {
+ return mHttpURLConnection.getInputStream();
+ } catch (IOException e) {
+ return mHttpURLConnection.getErrorStream();
+ }
+ }
+
+ public void close() {
+ if (mHttpURLConnection == null) {
+ return;
+ }
+ mHttpURLConnection.disconnect();
+ mHttpURLConnection = null;
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/NetworkUtils.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/NetworkUtils.java
new file mode 100644
index 0000000000..b3b33b02a5
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/NetworkUtils.java
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.utils;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.util.Log;
+import org.mozilla.mozstumbler.service.AppGlobals;
+
+public final class NetworkUtils {
+ private static final String LOG_TAG = AppGlobals.makeLogTag(NetworkUtils.class.getSimpleName());
+
+ ConnectivityManager mConnectivityManager;
+
+ public NetworkUtils(Context context) {
+ mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ }
+
+ public synchronized boolean isWifiAvailable() {
+ if (mConnectivityManager == null) {
+ Log.e(LOG_TAG, "ConnectivityManager is null!");
+ return false;
+ }
+
+ NetworkInfo aNet = mConnectivityManager.getActiveNetworkInfo();
+ return (aNet != null && aNet.getType() == ConnectivityManager.TYPE_WIFI);
+ }
+
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/PersistentIntentService.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/PersistentIntentService.java
new file mode 100644
index 0000000000..8387c7edd2
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/PersistentIntentService.java
@@ -0,0 +1,85 @@
+package org.mozilla.mozstumbler.service.utils;
+
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+
+/* This code is copied from android IntentService, with stopSelf commented out. */
+public abstract class PersistentIntentService extends Service {
+ private volatile Looper mServiceLooper;
+ private volatile ServiceHandler mServiceHandler;
+ private final String mName;
+ private boolean mRedelivery;
+
+ private final class ServiceHandler extends Handler {
+ public ServiceHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ onHandleIntent((Intent) msg.obj);
+ // stopSelf(msg.arg1); <-- modified from original file
+ }
+ }
+
+ public PersistentIntentService(String name) {
+ super();
+ mName = name;
+ }
+
+ public void setIntentRedelivery(boolean enabled) {
+ mRedelivery = enabled;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
+ thread.start();
+ mServiceLooper = thread.getLooper();
+ mServiceHandler = new ServiceHandler(mServiceLooper);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ Message msg = mServiceHandler.obtainMessage();
+ msg.arg1 = startId;
+ msg.obj = intent;
+ mServiceHandler.sendMessage(msg);
+ return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ mServiceLooper.quit();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ protected abstract void onHandleIntent(Intent intent);
+}
+
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/TelemetryWrapper.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/TelemetryWrapper.java
new file mode 100644
index 0000000000..91cde26f24
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/TelemetryWrapper.java
@@ -0,0 +1,35 @@
+package org.mozilla.mozstumbler.service.utils;
+
+import android.util.Log;
+import org.mozilla.mozstumbler.service.AppGlobals;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class TelemetryWrapper {
+ private static final String LOG_TAG = AppGlobals.makeLogTag(TelemetryWrapper.class.getSimpleName());
+ private static Method mAddToHistogram;
+
+ public static void addToHistogram(String key, int value) {
+ if (mAddToHistogram == null) {
+ try {
+ Class<?> telemetry = Class.forName("org.mozilla.gecko.Telemetry");
+ mAddToHistogram = telemetry.getMethod("addToHistogram", String.class, int.class);
+ } catch (ClassNotFoundException e) {
+ Log.d(LOG_TAG, "Class not found!");
+ return;
+ } catch (NoSuchMethodException e) {
+ Log.d(LOG_TAG, "Method not found!");
+ return;
+ }
+ }
+
+ if (mAddToHistogram != null) {
+ try {
+ mAddToHistogram.invoke(null, key, value);
+ }
+ catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException e) {
+ Log.d(LOG_TAG, "Got exception invoking.");
+ }
+ }
+ }
+}
diff --git a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/Zipper.java b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/Zipper.java
new file mode 100644
index 0000000000..90e0ee7f5f
--- /dev/null
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/utils/Zipper.java
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.mozstumbler.service.utils;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+public class Zipper {
+ public static byte[] zipData(byte[] data) throws IOException {
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ GZIPOutputStream gstream = new GZIPOutputStream(os);
+ byte[] output;
+ try {
+ gstream.write(data);
+ gstream.finish();
+ output = os.toByteArray();
+ } finally {
+ gstream.close();
+ os.close();
+ }
+ return output;
+ }
+
+ public static String unzipData(byte[] data) throws IOException {
+ StringBuilder result = new StringBuilder();
+ final ByteArrayInputStream bs = new ByteArrayInputStream(data);
+ GZIPInputStream gstream = new GZIPInputStream(bs);
+ try {
+ InputStreamReader reader = new InputStreamReader(gstream);
+ BufferedReader in = new BufferedReader(reader);
+ String read;
+ while ((read = in.readLine()) != null) {
+ result.append(read);
+ }
+ } finally {
+ gstream.close();
+ bs.close();
+ }
+ return result.toString();
+ }
+}
diff --git a/mobile/android/stumbler/manifests/StumblerManifest_services.xml.in b/mobile/android/stumbler/manifests/StumblerManifest_services.xml.in
new file mode 100644
index 0000000000..400863187c
--- /dev/null
+++ b/mobile/android/stumbler/manifests/StumblerManifest_services.xml.in
@@ -0,0 +1,32 @@
+<service
+ android:name="org.mozilla.mozstumbler.service.stumblerthread.StumblerService"
+ android:label="stumbler">
+</service>
+
+<receiver android:name="org.mozilla.mozstumbler.service.uploadthread.UploadAlarmReceiver" />
+<service android:name="org.mozilla.mozstumbler.service.uploadthread.UploadAlarmReceiver$UploadAlarmService" />
+
+<!-- How Fennec and Stumbler interact:
+- On start, Fennec broadcasts an empty STUMBLER_REGISTER_LOCAL_LISTENER intent, indicating that Stumbler should
+ start listening for a locally-broadcast Stumbler preferences.
+- In response, Stumbler's SafeReceiver registers LocalPreferenceReceiver to listen for broadcasts
+ sent over LocalBroadcastManager which contain sensitive information.
+- This registration happens only once, and SafeReceiver can't unregister the listener.
+- LocalPreferenceReceiver responds to internal broadcasts with sensitive information,
+ and is able to start/stop StumblerService.
+- Fennec startup (if stumbling is enabled) or Fennec stumbling preference adjustment will trigger
+ a local preference intent, and Stumbler's internal state will be adjusted via LocalPreferenceReceiver.
+-->
+<receiver android:exported="false" android:name="org.mozilla.mozstumbler.service.mainthread.SafeReceiver">
+ <intent-filter>
+ <action android:name="org.mozilla.gecko.STUMBLER_REGISTER_LOCAL_LISTENER" />
+ </intent-filter>
+</receiver>
+
+<receiver android:exported="true" android:name="org.mozilla.mozstumbler.service.mainthread.SystemReceiver">
+ <intent-filter>
+ <action android:name="android.intent.action.BOOT_COMPLETED" />
+ <action android:name="android.intent.action.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE" />
+ </intent-filter>
+</receiver>
+
diff --git a/mobile/android/stumbler/moz.build b/mobile/android/stumbler/moz.build
new file mode 100644
index 0000000000..651cd18dd8
--- /dev/null
+++ b/mobile/android/stumbler/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+include('stumbler_sources.mozbuild')
+
+stumbler_jar = add_java_jar('stumbler')
+stumbler_jar.sources += stumbler_sources
+stumbler_jar.extra_jars += [CONFIG['ANDROID_SUPPORT_V4_AAR_LIB']]
+stumbler_jar.javac_flags += ['-Xlint:all']
diff --git a/mobile/android/stumbler/stumbler_sources.mozbuild b/mobile/android/stumbler/stumbler_sources.mozbuild
new file mode 100644
index 0000000000..63bc559f61
--- /dev/null
+++ b/mobile/android/stumbler/stumbler_sources.mozbuild
@@ -0,0 +1,36 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+stumbler_sources = [
+ 'java/org/mozilla/mozstumbler/service/AppGlobals.java',
+ 'java/org/mozilla/mozstumbler/service/mainthread/LocalPreferenceReceiver.java',
+ 'java/org/mozilla/mozstumbler/service/mainthread/SafeReceiver.java',
+ 'java/org/mozilla/mozstumbler/service/mainthread/SystemReceiver.java',
+ 'java/org/mozilla/mozstumbler/service/Prefs.java',
+ 'java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/BSSIDBlockList.java',
+ 'java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/SSIDBlockList.java',
+ 'java/org/mozilla/mozstumbler/service/stumblerthread/blocklist/WifiBlockListInterface.java',
+ 'java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/DataStorageContract.java',
+ 'java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/DataStorageManager.java',
+ 'java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/PersistedStats.java',
+ 'java/org/mozilla/mozstumbler/service/stumblerthread/datahandling/StumblerBundle.java',
+ 'java/org/mozilla/mozstumbler/service/stumblerthread/Reporter.java',
+ 'java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellInfo.java',
+ 'java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScanner.java',
+ 'java/org/mozilla/mozstumbler/service/stumblerthread/scanners/cellscanner/CellScannerImplementation.java',
+ 'java/org/mozilla/mozstumbler/service/stumblerthread/scanners/GPSScanner.java',
+ 'java/org/mozilla/mozstumbler/service/stumblerthread/scanners/LocationBlockList.java',
+ 'java/org/mozilla/mozstumbler/service/stumblerthread/scanners/ScanManager.java',
+ 'java/org/mozilla/mozstumbler/service/stumblerthread/scanners/WifiScanner.java',
+ 'java/org/mozilla/mozstumbler/service/stumblerthread/StumblerService.java',
+ 'java/org/mozilla/mozstumbler/service/uploadthread/AsyncUploader.java',
+ 'java/org/mozilla/mozstumbler/service/uploadthread/UploadAlarmReceiver.java',
+ 'java/org/mozilla/mozstumbler/service/utils/AbstractCommunicator.java',
+ 'java/org/mozilla/mozstumbler/service/utils/NetworkUtils.java',
+ 'java/org/mozilla/mozstumbler/service/utils/PersistentIntentService.java',
+ 'java/org/mozilla/mozstumbler/service/utils/TelemetryWrapper.java',
+ 'java/org/mozilla/mozstumbler/service/utils/Zipper.java',
+]
diff --git a/mobile/android/tests/.eslintrc b/mobile/android/tests/.eslintrc
new file mode 100644
index 0000000000..6784be2d6e
--- /dev/null
+++ b/mobile/android/tests/.eslintrc
@@ -0,0 +1,18 @@
+globals:
+ # TODO: Verify that these are correct.
+ Point: false
+ SpecialPowers: false
+ XPCNativeWrapper: false
+ add_task: false
+ add_test: false
+ do_check_eq: false
+ do_check_false: false
+ do_check_neq: false
+ do_check_true: false
+ do_print: false
+ do_register_cleanup: false
+ do_report_result: false
+ do_test_finished: false
+ do_test_pending: false
+ do_throw: false
+ run_next_test: false
diff --git a/mobile/android/tests/background/junit3/AndroidManifest.xml.in b/mobile/android/tests/background/junit3/AndroidManifest.xml.in
new file mode 100644
index 0000000000..09f1a25a15
--- /dev/null
+++ b/mobile/android/tests/background/junit3/AndroidManifest.xml.in
@@ -0,0 +1,23 @@
+#filter substitution
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.mozilla.gecko.background.tests"
+ sharedUserId="@MOZ_ANDROID_SHARED_ID@"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-sdk android:minSdkVersion="8"
+ android:targetSdkVersion="@ANDROID_TARGET_SDK@" />
+
+ <application
+ android:debuggable="true"
+ android:icon="@drawable/icon"
+ android:label="@ANDROID_BACKGROUND_APP_DISPLAYNAME@">
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:label="@string/app_name"
+ android:name="android.test.InstrumentationTestRunner"
+ android:targetPackage="@ANDROID_BACKGROUND_TARGET_PACKAGE_NAME@" />
+</manifest>
diff --git a/mobile/android/tests/background/junit3/Makefile.in b/mobile/android/tests/background/junit3/Makefile.in
new file mode 100644
index 0000000000..f7f40ca788
--- /dev/null
+++ b/mobile/android/tests/background/junit3/Makefile.in
@@ -0,0 +1,13 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+ANDROID_EXTRA_JARS := \
+ background-junit3.jar \
+ $(NULL)
+
+ANDROID_MANIFEST_FILE := $(CURDIR)/AndroidManifest.xml
+
+include $(topsrcdir)/config/rules.mk
+
+tools:: $(ANDROID_APK_NAME).apk
diff --git a/mobile/android/tests/background/junit3/background_junit3_sources.mozbuild b/mobile/android/tests/background/junit3/background_junit3_sources.mozbuild
new file mode 100644
index 0000000000..021af2eb80
--- /dev/null
+++ b/mobile/android/tests/background/junit3/background_junit3_sources.mozbuild
@@ -0,0 +1,78 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+background_junit3_sources = [
+ 'src/org/mozilla/gecko/background/common/TestAndroidLogWriters.java',
+ 'src/org/mozilla/gecko/background/common/TestUtils.java',
+ 'src/org/mozilla/gecko/background/common/TestWaitHelper.java',
+ 'src/org/mozilla/gecko/background/db/AndroidBrowserRepositoryTestCase.java',
+ 'src/org/mozilla/gecko/background/db/TestAndroidBrowserBookmarksRepository.java',
+ 'src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryRepository.java',
+ 'src/org/mozilla/gecko/background/db/TestBookmarks.java',
+ 'src/org/mozilla/gecko/background/db/TestClientsDatabase.java',
+ 'src/org/mozilla/gecko/background/db/TestClientsDatabaseAccessor.java',
+ 'src/org/mozilla/gecko/background/db/TestFennecTabsRepositorySession.java',
+ 'src/org/mozilla/gecko/background/db/TestFormHistoryRepositorySession.java',
+ 'src/org/mozilla/gecko/background/db/TestPasswordsRepository.java',
+ 'src/org/mozilla/gecko/background/db/TestTopSites.java',
+ 'src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java',
+ 'src/org/mozilla/gecko/background/fxa/TestAccountLoader.java',
+ 'src/org/mozilla/gecko/background/fxa/TestBrowserIDKeyPairGeneration.java',
+ 'src/org/mozilla/gecko/background/helpers/AndroidSyncTestCase.java',
+ 'src/org/mozilla/gecko/background/helpers/DBHelpers.java',
+ 'src/org/mozilla/gecko/background/helpers/DBProviderTestCase.java',
+ 'src/org/mozilla/gecko/background/nativecode/test/TestNativeCrypto.java',
+ 'src/org/mozilla/gecko/background/sync/AndroidSyncTestCaseWithAccounts.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/BookmarkHelpers.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/DefaultBeginDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/DefaultCleanDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/DefaultDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/DefaultFetchDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/DefaultFinishDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/DefaultGuidsSinceDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/DefaultSessionCreationDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/DefaultStoreDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/ExpectBeginDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/ExpectBeginFailDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/ExpectFetchDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/ExpectFetchSinceDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/ExpectFinishDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/ExpectFinishFailDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/ExpectGuidsSinceDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidRequestFetchDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidTypeStoreDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/ExpectManyStoredDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/ExpectNoGUIDsSinceDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/ExpectNoStoreDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/ExpectStoreCompletedDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/ExpectStoredDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/HistoryHelpers.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/PasswordHelpers.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/SessionTestHelper.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessBeginDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessCreationDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFetchDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFinishDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessStoreDelegate.java',
+ 'src/org/mozilla/gecko/background/sync/TestClientsStage.java',
+ 'src/org/mozilla/gecko/background/sync/TestResetting.java',
+ 'src/org/mozilla/gecko/background/sync/TestStoreTracking.java',
+ 'src/org/mozilla/gecko/background/sync/TestSyncConfiguration.java',
+ 'src/org/mozilla/gecko/background/sync/TestWebURLFinder.java',
+ 'src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java',
+ 'src/org/mozilla/gecko/background/testhelpers/CommandHelpers.java',
+ 'src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java',
+ 'src/org/mozilla/gecko/background/testhelpers/MockAbstractNonRepositorySyncStage.java',
+ 'src/org/mozilla/gecko/background/testhelpers/MockClientsDatabaseAccessor.java',
+ 'src/org/mozilla/gecko/background/testhelpers/MockClientsDataDelegate.java',
+ 'src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java',
+ 'src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java',
+ 'src/org/mozilla/gecko/background/testhelpers/MockRecord.java',
+ 'src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java',
+ 'src/org/mozilla/gecko/background/testhelpers/MockSharedPreferences.java',
+ 'src/org/mozilla/gecko/background/testhelpers/WaitHelper.java',
+ 'src/org/mozilla/gecko/background/testhelpers/WBORepository.java',
+]
diff --git a/mobile/android/tests/background/junit3/instrumentation.ini b/mobile/android/tests/background/junit3/instrumentation.ini
new file mode 100644
index 0000000000..e2c7b2ea12
--- /dev/null
+++ b/mobile/android/tests/background/junit3/instrumentation.ini
@@ -0,0 +1,22 @@
+[DEFAULT]
+subsuite = background
+
+[src/org/mozilla/gecko/background/common/TestAndroidLogWriters.java]
+[src/org/mozilla/gecko/background/common/TestUtils.java]
+[src/org/mozilla/gecko/background/common/TestWaitHelper.java]
+[src/org/mozilla/gecko/background/db/TestAndroidBrowserBookmarksRepository.java]
+[src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryRepository.java]
+[src/org/mozilla/gecko/background/db/TestBookmarks.java]
+[src/org/mozilla/gecko/background/db/TestClientsDatabase.java]
+[src/org/mozilla/gecko/background/db/TestClientsDatabaseAccessor.java]
+[src/org/mozilla/gecko/background/db/TestFennecTabsRepositorySession.java]
+[src/org/mozilla/gecko/background/db/TestFormHistoryRepositorySession.java]
+[src/org/mozilla/gecko/background/db/TestPasswordsRepository.java]
+[src/org/mozilla/gecko/background/fxa/TestBrowserIDKeyPairGeneration.java]
+[src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java]
+[src/org/mozilla/gecko/background/nativecode/test/TestNativeCrypto.java]
+[src/org/mozilla/gecko/background/sync/TestClientsStage.java]
+[src/org/mozilla/gecko/background/sync/TestResetting.java]
+[src/org/mozilla/gecko/background/sync/TestStoreTracking.java]
+[src/org/mozilla/gecko/background/sync/TestSyncConfiguration.java]
+[src/org/mozilla/gecko/background/sync/TestWebURLFinder.java]
diff --git a/mobile/android/tests/background/junit3/moz.build b/mobile/android/tests/background/junit3/moz.build
new file mode 100644
index 0000000000..3e81e1e326
--- /dev/null
+++ b/mobile/android/tests/background/junit3/moz.build
@@ -0,0 +1,42 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEFINES['ANDROID_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME']
+
+ANDROID_APK_NAME = 'background-junit3-debug'
+ANDROID_APK_PACKAGE = 'org.mozilla.gecko.background.tests'
+
+include('background_junit3_sources.mozbuild')
+
+jar = add_java_jar('background-junit3')
+jar.sources += background_junit3_sources
+jar.extra_jars += [
+ CONFIG['ANDROID_SUPPORT_V4_AAR_LIB'],
+ CONFIG['ANDROID_RECYCLERVIEW_V7_AAR_LIB'],
+ TOPOBJDIR + '/mobile/android/base/constants.jar',
+ TOPOBJDIR + '/mobile/android/base/gecko-R.jar',
+ TOPOBJDIR + '/mobile/android/base/gecko-browser.jar',
+ TOPOBJDIR + '/mobile/android/base/gecko-mozglue.jar',
+ TOPOBJDIR + '/mobile/android/base/gecko-thirdparty.jar',
+ TOPOBJDIR + '/mobile/android/base/gecko-util.jar',
+ TOPOBJDIR + '/mobile/android/base/gecko-view.jar',
+ TOPOBJDIR + '/mobile/android/base/services.jar',
+ TOPOBJDIR + '/mobile/android/base/sync-thirdparty.jar',
+]
+
+if CONFIG['MOZ_ANDROID_MLS_STUMBLER']:
+ jar.extra_jars += [
+ TOPOBJDIR + '/mobile/android/stumbler/stumbler.jar',
+ ]
+
+ANDROID_INSTRUMENTATION_MANIFESTS += ['instrumentation.ini']
+
+DEFINES['ANDROID_BACKGROUND_TARGET_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME']
+DEFINES['ANDROID_BACKGROUND_APP_DISPLAYNAME'] = '%s Background Tests' % CONFIG['MOZ_APP_DISPLAYNAME']
+DEFINES['MOZ_ANDROID_SHARED_ID'] = CONFIG['MOZ_ANDROID_SHARED_ID']
+OBJDIR_PP_FILES.mobile.android.tests.background.junit3 += [
+ 'AndroidManifest.xml.in',
+]
diff --git a/mobile/android/tests/background/junit3/res/drawable-hdpi/icon.png b/mobile/android/tests/background/junit3/res/drawable-hdpi/icon.png
new file mode 100644
index 0000000000..e83438eee4
--- /dev/null
+++ b/mobile/android/tests/background/junit3/res/drawable-hdpi/icon.png
Binary files differ
diff --git a/mobile/android/tests/background/junit3/res/drawable-ldpi/icon.png b/mobile/android/tests/background/junit3/res/drawable-ldpi/icon.png
new file mode 100644
index 0000000000..0483c95e99
--- /dev/null
+++ b/mobile/android/tests/background/junit3/res/drawable-ldpi/icon.png
Binary files differ
diff --git a/mobile/android/tests/background/junit3/res/drawable-mdpi/icon.png b/mobile/android/tests/background/junit3/res/drawable-mdpi/icon.png
new file mode 100644
index 0000000000..86b4dee546
--- /dev/null
+++ b/mobile/android/tests/background/junit3/res/drawable-mdpi/icon.png
Binary files differ
diff --git a/mobile/android/tests/background/junit3/res/layout/main.xml b/mobile/android/tests/background/junit3/res/layout/main.xml
new file mode 100644
index 0000000000..14dbff7e05
--- /dev/null
+++ b/mobile/android/tests/background/junit3/res/layout/main.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical" >
+
+ <TextView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/app_name" />
+
+</LinearLayout>
diff --git a/mobile/android/tests/background/junit3/res/values/strings.xml b/mobile/android/tests/background/junit3/res/values/strings.xml
new file mode 100644
index 0000000000..d6534d7fae
--- /dev/null
+++ b/mobile/android/tests/background/junit3/res/values/strings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">Gecko Background Tests</string>
+
+</resources>
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestAndroidLogWriters.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestAndroidLogWriters.java
new file mode 100644
index 0000000000..7f4b9bb9cb
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestAndroidLogWriters.java
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.common;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.common.log.writers.AndroidLevelCachingLogWriter;
+import org.mozilla.gecko.background.common.log.writers.AndroidLogWriter;
+import org.mozilla.gecko.background.common.log.writers.LogWriter;
+import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
+
+public class TestAndroidLogWriters extends AndroidSyncTestCase {
+ public static final String TEST_LOG_TAG = "TestAndroidLogWriters";
+
+ public static final String TEST_MESSAGE_1 = "LOG TEST MESSAGE one";
+ public static final String TEST_MESSAGE_2 = "LOG TEST MESSAGE two";
+ public static final String TEST_MESSAGE_3 = "LOG TEST MESSAGE three";
+
+ public void setUp() {
+ Logger.stopLoggingToAll();
+ }
+
+ public void tearDown() {
+ Logger.resetLogging();
+ }
+
+ /**
+ * Verify these *all* appear in the Android log by using
+ * <code>adb logcat | grep TestAndroidLogWriters</code> after executing
+ * <code>adb shell setprop log.tag.TestAndroidLogWriters ERROR</code>.
+ * <p>
+ * This writer does not use the Android log levels!
+ */
+ public void testAndroidLogWriter() {
+ LogWriter lw = new AndroidLogWriter();
+
+ Logger.error(TEST_LOG_TAG, TEST_MESSAGE_1, new RuntimeException());
+ Logger.startLoggingTo(lw);
+ Logger.error(TEST_LOG_TAG, TEST_MESSAGE_2);
+ Logger.warn(TEST_LOG_TAG, TEST_MESSAGE_2);
+ Logger.info(TEST_LOG_TAG, TEST_MESSAGE_2);
+ Logger.debug(TEST_LOG_TAG, TEST_MESSAGE_2);
+ Logger.trace(TEST_LOG_TAG, TEST_MESSAGE_2);
+ Logger.stopLoggingTo(lw);
+ Logger.error(TEST_LOG_TAG, TEST_MESSAGE_3, new RuntimeException());
+ }
+
+ /**
+ * Verify only *some* of these appear in the Android log by using
+ * <code>adb logcat | grep TestAndroidLogWriters</code> after executing
+ * <code>adb shell setprop log.tag.TestAndroidLogWriters INFO</code>.
+ * <p>
+ * This writer should use the Android log levels!
+ */
+ public void testAndroidLevelCachingLogWriter() throws Exception {
+ LogWriter lw = new AndroidLevelCachingLogWriter(new AndroidLogWriter());
+
+ Logger.error(TEST_LOG_TAG, TEST_MESSAGE_1, new RuntimeException());
+ Logger.startLoggingTo(lw);
+ Logger.error(TEST_LOG_TAG, TEST_MESSAGE_2);
+ Logger.warn(TEST_LOG_TAG, TEST_MESSAGE_2);
+ Logger.info(TEST_LOG_TAG, TEST_MESSAGE_2);
+ Logger.debug(TEST_LOG_TAG, TEST_MESSAGE_2);
+ Logger.trace(TEST_LOG_TAG, TEST_MESSAGE_2);
+ Logger.stopLoggingTo(lw);
+ Logger.error(TEST_LOG_TAG, TEST_MESSAGE_3, new RuntimeException());
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestUtils.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestUtils.java
new file mode 100644
index 0000000000..270eae6f62
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestUtils.java
@@ -0,0 +1,159 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.common;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
+import org.mozilla.gecko.sync.Utils;
+
+import android.os.Bundle;
+
+public class TestUtils extends AndroidSyncTestCase {
+ protected static void assertStages(String[] all, String[] sync, String[] skip, String[] expected) {
+ final Set<String> sAll = new HashSet<String>();
+ for (String s : all) {
+ sAll.add(s);
+ }
+ List<String> sSync = null;
+ if (sync != null) {
+ sSync = new ArrayList<String>();
+ for (String s : sync) {
+ sSync.add(s);
+ }
+ }
+ List<String> sSkip = null;
+ if (skip != null) {
+ sSkip = new ArrayList<String>();
+ for (String s : skip) {
+ sSkip.add(s);
+ }
+ }
+ List<String> stages = new ArrayList<String>(Utils.getStagesToSync(sAll, sSync, sSkip));
+ Collections.sort(stages);
+ List<String> exp = new ArrayList<String>();
+ for (String e : expected) {
+ exp.add(e);
+ }
+ assertEquals(exp, stages);
+ }
+
+ public void testGetStagesToSync() {
+ final String[] all = new String[] { "other1", "other2", "skip1", "skip2", "sync1", "sync2" };
+ assertStages(all, null, null, all);
+ assertStages(all, new String[] { "sync1" }, null, new String[] { "sync1" });
+ assertStages(all, null, new String[] { "skip1", "skip2" }, new String[] { "other1", "other2", "sync1", "sync2" });
+ assertStages(all, new String[] { "sync1", "sync2" }, new String[] { "skip1", "skip2" }, new String[] { "sync1", "sync2" });
+ }
+
+ protected static void assertStagesFromBundle(String[] all, String[] sync, String[] skip, String[] expected) {
+ final Set<String> sAll = new HashSet<String>();
+ for (String s : all) {
+ sAll.add(s);
+ }
+ final Bundle bundle = new Bundle();
+ Utils.putStageNamesToSync(bundle, sync, skip);
+
+ Collection<String> ss = Utils.getStagesToSyncFromBundle(sAll, bundle);
+ List<String> stages = new ArrayList<String>(ss);
+ Collections.sort(stages);
+ List<String> exp = new ArrayList<String>();
+ for (String e : expected) {
+ exp.add(e);
+ }
+ assertEquals(exp, stages);
+ }
+
+ public void testGetStagesToSyncFromBundle() {
+ final String[] all = new String[] { "other1", "other2", "skip1", "skip2", "sync1", "sync2" };
+ assertStagesFromBundle(all, null, null, all);
+ assertStagesFromBundle(all, new String[] { "sync1" }, null, new String[] { "sync1" });
+ assertStagesFromBundle(all, null, new String[] { "skip1", "skip2" }, new String[] { "other1", "other2", "sync1", "sync2" });
+ assertStagesFromBundle(all, new String[] { "sync1", "sync2" }, new String[] { "skip1", "skip2" }, new String[] { "sync1", "sync2" });
+ }
+
+ public static void deleteDirectoryRecursively(final File dir) throws IOException {
+ if (!dir.isDirectory()) {
+ throw new IllegalStateException("Given directory, " + dir + ", is not a directory!");
+ }
+
+ for (File f : dir.listFiles()) {
+ if (f.isDirectory()) {
+ deleteDirectoryRecursively(f);
+ } else if (!f.delete()) {
+ // Since this method is for testing, we assume we should be able to do this.
+ throw new IOException("Could not delete file, " + f.getAbsolutePath() + ". Permissions?");
+ }
+ }
+
+ if (!dir.delete()) {
+ throw new IOException("Could not delete dir, " + dir.getAbsolutePath() + ".");
+ }
+ }
+
+ public void testDeleteDirectoryRecursively() throws Exception {
+ final String TEST_DIR = getApplicationContext().getCacheDir().getAbsolutePath() +
+ "-testDeleteDirectory-" + System.currentTimeMillis();
+
+ // Non-existent directory.
+ final File nonexistent = new File("nonexistentDirectory"); // Hopefully. ;)
+ assertFalse(nonexistent.exists());
+ try {
+ deleteDirectoryRecursively(nonexistent);
+ fail("deleteDirectoryRecursively on a nonexistent directory should throw Exception");
+ } catch (IllegalStateException e) { }
+
+ // Empty dir.
+ File dir = mkdir(TEST_DIR);
+ deleteDirectoryRecursively(dir);
+ assertFalse(dir.exists());
+
+ // Filled dir.
+ dir = mkdir(TEST_DIR);
+ populateDir(dir);
+ deleteDirectoryRecursively(dir);
+ assertFalse(dir.exists());
+
+ // Filled dir with empty dir.
+ dir = mkdir(TEST_DIR);
+ populateDir(dir);
+ File subDir = new File(TEST_DIR + File.separator + "subDir");
+ assertTrue(subDir.mkdir());
+ deleteDirectoryRecursively(dir);
+ assertFalse(subDir.exists()); // For short-circuiting errors.
+ assertFalse(dir.exists());
+
+ // Filled dir with filled dir.
+ dir = mkdir(TEST_DIR);
+ populateDir(dir);
+ subDir = new File(TEST_DIR + File.separator + "subDir");
+ assertTrue(subDir.mkdir());
+ populateDir(subDir);
+ deleteDirectoryRecursively(dir);
+ assertFalse(subDir.exists()); // For short-circuiting errors.
+ assertFalse(dir.exists());
+ }
+
+ private File mkdir(final String name) {
+ final File dir = new File(name);
+ assertTrue(dir.mkdir());
+ return dir;
+ }
+
+ private void populateDir(final File dir) throws IOException {
+ assertTrue(dir.isDirectory());
+ final String dirPath = dir.getAbsolutePath();
+ for (int i = 0; i < 3; i++) {
+ final File f = new File(dirPath + File.separator + i);
+ assertTrue(f.createNewFile()); // Throws IOException if file could not be created.
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestWaitHelper.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestWaitHelper.java
new file mode 100644
index 0000000000..1f818e0cf9
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestWaitHelper.java
@@ -0,0 +1,356 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.common;
+
+import junit.framework.AssertionFailedError;
+
+import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.background.testhelpers.WaitHelper.InnerError;
+import org.mozilla.gecko.background.testhelpers.WaitHelper.TimeoutError;
+import org.mozilla.gecko.sync.ThreadPool;
+
+public class TestWaitHelper extends AndroidSyncTestCase {
+ private static final String ERROR_UNIQUE_IDENTIFIER = "error unique identifier";
+
+ public static int NO_WAIT = 1; // Milliseconds.
+ public static int SHORT_WAIT = 100; // Milliseconds.
+ public static int LONG_WAIT = 3 * SHORT_WAIT;
+
+ private Object notifyMonitor = new Object();
+ // Guarded by notifyMonitor.
+ private boolean performNotifyCalled = false;
+ private boolean performNotifyErrorCalled = false;
+ private void setPerformNotifyCalled() {
+ synchronized (notifyMonitor) {
+ performNotifyCalled = true;
+ }
+ }
+ private void setPerformNotifyErrorCalled() {
+ synchronized (notifyMonitor) {
+ performNotifyErrorCalled = true;
+ }
+ }
+ private void resetNotifyCalled() {
+ synchronized (notifyMonitor) {
+ performNotifyCalled = false;
+ performNotifyErrorCalled = false;
+ }
+ }
+ private void assertBothCalled() {
+ synchronized (notifyMonitor) {
+ assertTrue(performNotifyCalled);
+ assertTrue(performNotifyErrorCalled);
+ }
+ }
+ private void assertErrorCalled() {
+ synchronized (notifyMonitor) {
+ assertFalse(performNotifyCalled);
+ assertTrue(performNotifyErrorCalled);
+ }
+ }
+ private void assertCalled() {
+ synchronized (notifyMonitor) {
+ assertTrue(performNotifyCalled);
+ assertFalse(performNotifyErrorCalled);
+ }
+ }
+
+ public WaitHelper waitHelper;
+
+ public TestWaitHelper() {
+ super();
+ }
+
+ public void setUp() {
+ WaitHelper.resetTestWaiter();
+ waitHelper = WaitHelper.getTestWaiter();
+ resetNotifyCalled();
+ }
+
+ public void tearDown() {
+ assertTrue(waitHelper.isIdle());
+ }
+
+ public Runnable performNothingRunnable() {
+ return new Runnable() {
+ public void run() {
+ }
+ };
+ }
+
+ public Runnable performNotifyRunnable() {
+ return new Runnable() {
+ public void run() {
+ setPerformNotifyCalled();
+ waitHelper.performNotify();
+ }
+ };
+ }
+
+ public Runnable performNotifyAfterDelayRunnable(final int delayInMillis) {
+ return new Runnable() {
+ public void run() {
+ try {
+ Thread.sleep(delayInMillis);
+ } catch (InterruptedException e) {
+ fail("Interrupted.");
+ }
+
+ setPerformNotifyCalled();
+ waitHelper.performNotify();
+ }
+ };
+ }
+
+ public Runnable performNotifyErrorRunnable() {
+ return new Runnable() {
+ public void run() {
+ setPerformNotifyCalled();
+ waitHelper.performNotify(new AssertionFailedError(ERROR_UNIQUE_IDENTIFIER));
+ }
+ };
+ }
+
+ public Runnable inThreadPool(final Runnable runnable) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ ThreadPool.run(runnable);
+ }
+ };
+ }
+
+ public Runnable inThread(final Runnable runnable) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ new Thread(runnable).start();
+ }
+ };
+ }
+
+ protected void expectAssertionFailedError(Runnable runnable) {
+ try {
+ waitHelper.performWait(runnable);
+ } catch (InnerError e) {
+ AssertionFailedError inner = (AssertionFailedError)e.innerError;
+ setPerformNotifyErrorCalled();
+ String message = inner.getMessage();
+ assertTrue("Expected '" + message + "' to contain '" + ERROR_UNIQUE_IDENTIFIER + "'",
+ message.contains(ERROR_UNIQUE_IDENTIFIER));
+ }
+ }
+
+ protected void expectAssertionFailedErrorAfterDelay(int wait, Runnable runnable) {
+ try {
+ waitHelper.performWait(wait, runnable);
+ } catch (InnerError e) {
+ AssertionFailedError inner = (AssertionFailedError)e.innerError;
+ setPerformNotifyErrorCalled();
+ String message = inner.getMessage();
+ assertTrue("Expected '" + message + "' to contain '" + ERROR_UNIQUE_IDENTIFIER + "'",
+ message.contains(ERROR_UNIQUE_IDENTIFIER));
+ }
+ }
+
+ public void testPerformWait() {
+ waitHelper.performWait(performNotifyRunnable());
+ assertCalled();
+ }
+
+ public void testPerformWaitInThread() {
+ waitHelper.performWait(inThread(performNotifyRunnable()));
+ assertCalled();
+ }
+
+ public void testPerformWaitInThreadPool() {
+ waitHelper.performWait(inThreadPool(performNotifyRunnable()));
+ assertCalled();
+ }
+
+ public void testPerformTimeoutWait() {
+ waitHelper.performWait(SHORT_WAIT, performNotifyRunnable());
+ assertCalled();
+ }
+
+ public void testPerformTimeoutWaitInThread() {
+ waitHelper.performWait(SHORT_WAIT, inThread(performNotifyRunnable()));
+ assertCalled();
+ }
+
+ public void testPerformTimeoutWaitInThreadPool() {
+ waitHelper.performWait(SHORT_WAIT, inThreadPool(performNotifyRunnable()));
+ assertCalled();
+ }
+
+ public void testPerformErrorWaitInThread() {
+ expectAssertionFailedError(inThread(performNotifyErrorRunnable()));
+ assertBothCalled();
+ }
+
+ public void testPerformErrorWaitInThreadPool() {
+ expectAssertionFailedError(inThreadPool(performNotifyErrorRunnable()));
+ assertBothCalled();
+ }
+
+ public void testPerformErrorTimeoutWaitInThread() {
+ expectAssertionFailedErrorAfterDelay(SHORT_WAIT, inThread(performNotifyErrorRunnable()));
+ assertBothCalled();
+ }
+
+ public void testPerformErrorTimeoutWaitInThreadPool() {
+ expectAssertionFailedErrorAfterDelay(SHORT_WAIT, inThreadPool(performNotifyErrorRunnable()));
+ assertBothCalled();
+ }
+
+ public void testTimeout() {
+ try {
+ waitHelper.performWait(SHORT_WAIT, performNothingRunnable());
+ } catch (TimeoutError e) {
+ setPerformNotifyErrorCalled();
+ assertEquals(SHORT_WAIT, e.waitTimeInMillis);
+ }
+ assertErrorCalled();
+ }
+
+ /**
+ * This will pass. The sequence in the main thread is:
+ * - A short delay.
+ * - performNotify is called.
+ * - performWait is called and immediately finds that performNotify was called before.
+ */
+ public void testDelay() {
+ try {
+ waitHelper.performWait(1, performNotifyAfterDelayRunnable(SHORT_WAIT));
+ } catch (AssertionFailedError e) {
+ setPerformNotifyErrorCalled();
+ assertTrue(e.getMessage(), e.getMessage().contains("TIMEOUT"));
+ }
+ assertCalled();
+ }
+
+ public Runnable performNotifyMultipleTimesRunnable() {
+ return new Runnable() {
+ public void run() {
+ waitHelper.performNotify();
+ setPerformNotifyCalled();
+ waitHelper.performNotify();
+ }
+ };
+ }
+
+ public void testPerformNotifyMultipleTimesFails() {
+ try {
+ waitHelper.performWait(NO_WAIT, performNotifyMultipleTimesRunnable()); // Not run on thread, so runnable executes before performWait looks for notifications.
+ } catch (WaitHelper.MultipleNotificationsError e) {
+ setPerformNotifyErrorCalled();
+ }
+ assertBothCalled();
+ assertFalse(waitHelper.isIdle()); // First perform notify should be hanging around.
+ waitHelper.performWait(NO_WAIT, performNothingRunnable());
+ }
+
+ public void testNestedWaitsAndNotifies() {
+ waitHelper.performWait(new Runnable() {
+ @Override
+ public void run() {
+ waitHelper.performWait(new Runnable() {
+ public void run() {
+ setPerformNotifyCalled();
+ waitHelper.performNotify();
+ }
+ });
+ setPerformNotifyErrorCalled();
+ waitHelper.performNotify();
+ }
+ });
+ assertBothCalled();
+ }
+
+ public void testAssertIsReported() {
+ try {
+ waitHelper.performWait(1, new Runnable() {
+ @Override
+ public void run() {
+ assertTrue("unique identifier", false);
+ }
+ });
+ } catch (AssertionFailedError e) {
+ setPerformNotifyErrorCalled();
+ assertTrue(e.getMessage(), e.getMessage().contains("unique identifier"));
+ }
+ assertErrorCalled();
+ }
+
+ /**
+ * The inner wait will timeout, but the outer wait will succeed. The sequence in the helper thread is:
+ * - A short delay.
+ * - performNotify is called.
+ *
+ * The sequence in the main thread is:
+ * - performWait is called and times out because the helper thread does not call
+ * performNotify quickly enough.
+ */
+ public void testDelayInThread() throws InterruptedException {
+ waitHelper.performWait(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ waitHelper.performWait(NO_WAIT, inThread(new Runnable() {
+ public void run() {
+ try {
+ Thread.sleep(SHORT_WAIT);
+ } catch (InterruptedException e) {
+ fail("Interrupted.");
+ }
+
+ setPerformNotifyCalled();
+ waitHelper.performNotify();
+ }
+ }));
+ } catch (WaitHelper.TimeoutError e) {
+ setPerformNotifyErrorCalled();
+ assertEquals(NO_WAIT, e.waitTimeInMillis);
+ }
+ }
+ });
+ assertBothCalled();
+ }
+
+ /**
+ * The inner wait will timeout, but the outer wait will succeed. The sequence in the helper thread is:
+ * - A short delay.
+ * - performNotify is called.
+ *
+ * The sequence in the main thread is:
+ * - performWait is called and times out because the helper thread does not call
+ * performNotify quickly enough.
+ */
+ public void testDelayInThreadPool() throws InterruptedException {
+ waitHelper.performWait(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ waitHelper.performWait(NO_WAIT, inThreadPool(new Runnable() {
+ public void run() {
+ try {
+ Thread.sleep(SHORT_WAIT);
+ } catch (InterruptedException e) {
+ fail("Interrupted.");
+ }
+
+ setPerformNotifyCalled();
+ waitHelper.performNotify();
+ }
+ }));
+ } catch (WaitHelper.TimeoutError e) {
+ setPerformNotifyErrorCalled();
+ assertEquals(NO_WAIT, e.waitTimeInMillis);
+ }
+ }
+ });
+ assertBothCalled();
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/AndroidBrowserRepositoryTestCase.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/AndroidBrowserRepositoryTestCase.java
new file mode 100644
index 0000000000..da980735b3
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/AndroidBrowserRepositoryTestCase.java
@@ -0,0 +1,818 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.db;
+
+import java.util.concurrent.ExecutorService;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
+import org.mozilla.gecko.background.sync.helpers.DefaultBeginDelegate;
+import org.mozilla.gecko.background.sync.helpers.DefaultCleanDelegate;
+import org.mozilla.gecko.background.sync.helpers.DefaultFetchDelegate;
+import org.mozilla.gecko.background.sync.helpers.DefaultFinishDelegate;
+import org.mozilla.gecko.background.sync.helpers.DefaultSessionCreationDelegate;
+import org.mozilla.gecko.background.sync.helpers.DefaultStoreDelegate;
+import org.mozilla.gecko.background.sync.helpers.ExpectBeginDelegate;
+import org.mozilla.gecko.background.sync.helpers.ExpectBeginFailDelegate;
+import org.mozilla.gecko.background.sync.helpers.ExpectFetchDelegate;
+import org.mozilla.gecko.background.sync.helpers.ExpectFetchSinceDelegate;
+import org.mozilla.gecko.background.sync.helpers.ExpectFinishDelegate;
+import org.mozilla.gecko.background.sync.helpers.ExpectFinishFailDelegate;
+import org.mozilla.gecko.background.sync.helpers.ExpectGuidsSinceDelegate;
+import org.mozilla.gecko.background.sync.helpers.ExpectInvalidRequestFetchDelegate;
+import org.mozilla.gecko.background.sync.helpers.ExpectManyStoredDelegate;
+import org.mozilla.gecko.background.sync.helpers.ExpectStoreCompletedDelegate;
+import org.mozilla.gecko.background.sync.helpers.ExpectStoredDelegate;
+import org.mozilla.gecko.background.sync.helpers.SessionTestHelper;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.repositories.InactiveSessionException;
+import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
+import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.android.AndroidBrowserRepositoryDataAccessor;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import android.content.ContentValues;
+import android.content.Context;
+
+public abstract class AndroidBrowserRepositoryTestCase extends AndroidSyncTestCase {
+ protected static String LOG_TAG = "BrowserRepositoryTest";
+
+ protected static void wipe(AndroidBrowserRepositoryDataAccessor helper) {
+ Logger.debug(LOG_TAG, "Wiping.");
+ try {
+ helper.wipe();
+ } catch (NullPointerException e) {
+ // This will be handled in begin, here we can just ignore
+ // the error if it actually occurs since this is just test
+ // code. We will throw a ProfileDatabaseException. This
+ // error shouldn't occur in the future, but results from
+ // trying to access content providers before Fennec has
+ // been run at least once.
+ Logger.error(LOG_TAG, "ProfileDatabaseException seen in wipe. Begin should fail");
+ fail("NullPointerException in wipe.");
+ }
+ }
+
+ @Override
+ public void setUp() {
+ AndroidBrowserRepositoryDataAccessor helper = getDataAccessor();
+ wipe(helper);
+ assertTrue(WaitHelper.getTestWaiter().isIdle());
+ closeDataAccessor(helper);
+ }
+
+ public void tearDown() {
+ assertTrue(WaitHelper.getTestWaiter().isIdle());
+ }
+
+ protected RepositorySession createSession() {
+ return SessionTestHelper.createSession(
+ getApplicationContext(),
+ getRepository());
+ }
+
+ protected RepositorySession createAndBeginSession() {
+ return SessionTestHelper.createAndBeginSession(
+ getApplicationContext(),
+ getRepository());
+ }
+
+ protected static void dispose(RepositorySession session) {
+ if (session != null) {
+ session.abort();
+ }
+ }
+
+ /**
+ * Hook to return an ExpectFetchDelegate, possibly with special GUIDs ignored.
+ */
+ public ExpectFetchDelegate preparedExpectFetchDelegate(Record[] expected) {
+ return new ExpectFetchDelegate(expected);
+ }
+
+ /**
+ * Hook to return an ExpectGuidsSinceDelegate, possibly with special GUIDs ignored.
+ */
+ public ExpectGuidsSinceDelegate preparedExpectGuidsSinceDelegate(String[] expected) {
+ return new ExpectGuidsSinceDelegate(expected);
+ }
+
+ /**
+ * Hook to return an ExpectGuidsSinceDelegate expecting only special GUIDs (if there are any).
+ */
+ public ExpectGuidsSinceDelegate preparedExpectOnlySpecialGuidsSinceDelegate() {
+ return new ExpectGuidsSinceDelegate(new String[] {});
+ }
+
+ /**
+ * Hook to return an ExpectFetchSinceDelegate, possibly with special GUIDs ignored.
+ */
+ public ExpectFetchSinceDelegate preparedExpectFetchSinceDelegate(long timestamp, String[] expected) {
+ return new ExpectFetchSinceDelegate(timestamp, expected);
+ }
+
+ public static Runnable storeRunnable(final RepositorySession session, final Record record, final DefaultStoreDelegate delegate) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ session.setStoreDelegate(delegate);
+ try {
+ session.store(record);
+ session.storeDone();
+ } catch (NoStoreDelegateException e) {
+ fail("NoStoreDelegateException should not occur.");
+ }
+ }
+ };
+ }
+
+ public static Runnable storeRunnable(final RepositorySession session, final Record record) {
+ return storeRunnable(session, record, new ExpectStoredDelegate(record.guid));
+ }
+
+ public static Runnable storeManyRunnable(final RepositorySession session, final Record[] records, final DefaultStoreDelegate delegate) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ session.setStoreDelegate(delegate);
+ try {
+ for (Record record : records) {
+ session.store(record);
+ }
+ session.storeDone();
+ } catch (NoStoreDelegateException e) {
+ fail("NoStoreDelegateException should not occur.");
+ }
+ }
+ };
+ }
+
+ public static Runnable storeManyRunnable(final RepositorySession session, final Record[] records) {
+ return storeManyRunnable(session, records, new ExpectManyStoredDelegate(records));
+ }
+
+ /**
+ * Store a record and don't expect a store callback until we're done.
+ *
+ * @param session
+ * @param record
+ * @return Runnable.
+ */
+ public static Runnable quietStoreRunnable(final RepositorySession session, final Record record) {
+ return storeRunnable(session, record, new ExpectStoreCompletedDelegate());
+ }
+
+ public static Runnable beginRunnable(final RepositorySession session, final DefaultBeginDelegate delegate) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ try {
+ session.begin(delegate);
+ } catch (InvalidSessionTransitionException e) {
+ performNotify(e);
+ }
+ }
+ };
+ }
+
+ public static Runnable finishRunnable(final RepositorySession session, final DefaultFinishDelegate delegate) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ try {
+ session.finish(delegate);
+ } catch (InactiveSessionException e) {
+ performNotify(e);
+ }
+ }
+ };
+ }
+
+ public static Runnable fetchAllRunnable(final RepositorySession session, final ExpectFetchDelegate delegate) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ session.fetchAll(delegate);
+ }
+ };
+ }
+
+ public Runnable fetchAllRunnable(final RepositorySession session, final Record[] expectedRecords) {
+ return fetchAllRunnable(session, preparedExpectFetchDelegate(expectedRecords));
+ }
+
+ public Runnable guidsSinceRunnable(final RepositorySession session, final long timestamp, final String[] expected) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ session.guidsSince(timestamp, preparedExpectGuidsSinceDelegate(expected));
+ }
+ };
+ }
+
+ public Runnable fetchSinceRunnable(final RepositorySession session, final long timestamp, final String[] expected) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ session.fetchSince(timestamp, preparedExpectFetchSinceDelegate(timestamp, expected));
+ }
+ };
+ }
+
+ public static Runnable fetchRunnable(final RepositorySession session, final String[] guids, final DefaultFetchDelegate delegate) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ try {
+ session.fetch(guids, delegate);
+ } catch (InactiveSessionException e) {
+ performNotify(e);
+ }
+ }
+ };
+ }
+ public Runnable fetchRunnable(final RepositorySession session, final String[] guids, final Record[] expected) {
+ return fetchRunnable(session, guids, preparedExpectFetchDelegate(expected));
+ }
+
+ public static Runnable cleanRunnable(final Repository repository, final boolean success, final Context context, final DefaultCleanDelegate delegate) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ repository.clean(success, delegate, context);
+ }
+ };
+ }
+
+ protected abstract Repository getRepository();
+ protected abstract AndroidBrowserRepositoryDataAccessor getDataAccessor();
+
+ protected static void doStore(RepositorySession session, Record[] records) {
+ performWait(storeManyRunnable(session, records));
+ }
+
+ // Tests to implement
+ public abstract void testFetchAll();
+ public abstract void testGuidsSinceReturnMultipleRecords();
+ public abstract void testGuidsSinceReturnNoRecords();
+ public abstract void testFetchSinceOneRecord();
+ public abstract void testFetchSinceReturnNoRecords();
+ public abstract void testFetchOneRecordByGuid();
+ public abstract void testFetchMultipleRecordsByGuids();
+ public abstract void testFetchNoRecordByGuid();
+ public abstract void testWipe();
+ public abstract void testStore();
+ public abstract void testRemoteNewerTimeStamp();
+ public abstract void testLocalNewerTimeStamp();
+ public abstract void testDeleteRemoteNewer();
+ public abstract void testDeleteLocalNewer();
+ public abstract void testDeleteRemoteLocalNonexistent();
+ public abstract void testStoreIdenticalExceptGuid();
+ public abstract void testCleanMultipleRecords();
+
+
+ /*
+ * Test abstractions
+ */
+ protected void basicStoreTest(Record record) {
+ final RepositorySession session = createAndBeginSession();
+ performWait(storeRunnable(session, record));
+ }
+
+ protected void basicFetchAllTest(Record[] expected) {
+ Logger.debug("rnewman", "Starting testFetchAll.");
+ RepositorySession session = createAndBeginSession();
+ Logger.debug("rnewman", "Prepared.");
+
+ AndroidBrowserRepositoryDataAccessor helper = getDataAccessor();
+ helper.dumpDB();
+ performWait(storeManyRunnable(session, expected));
+
+ helper.dumpDB();
+ performWait(fetchAllRunnable(session, expected));
+
+ closeDataAccessor(helper);
+ dispose(session);
+ }
+
+ /*
+ * Tests for clean
+ */
+ // Input: 4 records; 2 which are to be cleaned, 2 which should remain after the clean
+ protected void cleanMultipleRecords(Record delete0, Record delete1, Record keep0, Record keep1, Record keep2) {
+ RepositorySession session = createAndBeginSession();
+ doStore(session, new Record[] {
+ delete0, delete1, keep0, keep1, keep2
+ });
+
+ // Force two records to appear deleted.
+ AndroidBrowserRepositoryDataAccessor db = getDataAccessor();
+ ContentValues cv = new ContentValues();
+ cv.put(BrowserContract.SyncColumns.IS_DELETED, 1);
+ db.updateByGuid(delete0.guid, cv);
+ db.updateByGuid(delete1.guid, cv);
+
+ final DefaultCleanDelegate delegate = new DefaultCleanDelegate() {
+ public void onCleaned(Repository repo) {
+ performNotify();
+ }
+ };
+
+ final Runnable cleanRunnable = cleanRunnable(
+ getRepository(),
+ true,
+ getApplicationContext(),
+ delegate);
+
+ performWait(cleanRunnable);
+ performWait(fetchAllRunnable(session, preparedExpectFetchDelegate(new Record[] { keep0, keep1, keep2})));
+ closeDataAccessor(db);
+ dispose(session);
+ }
+
+ /*
+ * Tests for guidsSince
+ */
+ protected void guidsSinceReturnMultipleRecords(Record record0, Record record1) {
+ RepositorySession session = createAndBeginSession();
+ long timestamp = System.currentTimeMillis();
+
+ String[] expected = new String[2];
+ expected[0] = record0.guid;
+ expected[1] = record1.guid;
+
+ Logger.debug(getName(), "Storing two records...");
+ performWait(storeManyRunnable(session, new Record[] { record0, record1 }));
+ Logger.debug(getName(), "Getting guids since " + timestamp + "; expecting " + expected.length);
+ performWait(guidsSinceRunnable(session, timestamp, expected));
+ dispose(session);
+ }
+
+ protected void guidsSinceReturnNoRecords(Record record0) {
+ RepositorySession session = createAndBeginSession();
+
+ // Store 1 record in the past.
+ performWait(storeRunnable(session, record0));
+
+ String[] expected = {};
+ performWait(guidsSinceRunnable(session, System.currentTimeMillis() + 1000, expected));
+ dispose(session);
+ }
+
+ /*
+ * Tests for fetchSince
+ */
+ protected void fetchSinceOneRecord(Record record0, Record record1) {
+ RepositorySession session = createAndBeginSession();
+
+ performWait(storeRunnable(session, record0));
+ long timestamp = System.currentTimeMillis();
+ Logger.debug("fetchSinceOneRecord", "Entering synchronized section. Timestamp " + timestamp);
+ synchronized(this) {
+ try {
+ wait(1000);
+ } catch (InterruptedException e) {
+ Logger.warn("fetchSinceOneRecord", "Interrupted.", e);
+ }
+ }
+ Logger.debug("fetchSinceOneRecord", "Storing.");
+ performWait(storeRunnable(session, record1));
+
+ Logger.debug("fetchSinceOneRecord", "Fetching record 1.");
+ String[] expectedOne = new String[] { record1.guid };
+ performWait(fetchSinceRunnable(session, timestamp + 10, expectedOne));
+
+ Logger.debug("fetchSinceOneRecord", "Fetching both, relying on inclusiveness.");
+ String[] expectedBoth = new String[] { record0.guid, record1.guid };
+ performWait(fetchSinceRunnable(session, timestamp - 3000, expectedBoth));
+
+ Logger.debug("fetchSinceOneRecord", "Done.");
+ dispose(session);
+ }
+
+ protected void fetchSinceReturnNoRecords(Record record) {
+ RepositorySession session = createAndBeginSession();
+
+ performWait(storeRunnable(session, record));
+
+ long timestamp = System.currentTimeMillis();
+
+ performWait(fetchSinceRunnable(session, timestamp + 2000, new String[] {}));
+ dispose(session);
+ }
+
+ protected void fetchOneRecordByGuid(Record record0, Record record1) {
+ RepositorySession session = createAndBeginSession();
+
+ Record[] store = new Record[] { record0, record1 };
+ performWait(storeManyRunnable(session, store));
+
+ String[] guids = new String[] { record0.guid };
+ Record[] expected = new Record[] { record0 };
+ performWait(fetchRunnable(session, guids, expected));
+ dispose(session);
+ }
+
+ protected void fetchMultipleRecordsByGuids(Record record0,
+ Record record1, Record record2) {
+ RepositorySession session = createAndBeginSession();
+
+ Record[] store = new Record[] { record0, record1, record2 };
+ performWait(storeManyRunnable(session, store));
+
+ String[] guids = new String[] { record0.guid, record2.guid };
+ Record[] expected = new Record[] { record0, record2 };
+ performWait(fetchRunnable(session, guids, expected));
+ dispose(session);
+ }
+
+ protected void fetchNoRecordByGuid(Record record) {
+ RepositorySession session = createAndBeginSession();
+
+ performWait(storeRunnable(session, record));
+ performWait(fetchRunnable(session,
+ new String[] { Utils.generateGuid() },
+ new Record[] {}));
+ dispose(session);
+ }
+
+ /*
+ * Test wipe
+ */
+ protected void doWipe(final Record record0, final Record record1) {
+ final RepositorySession session = createAndBeginSession();
+ final Runnable run = new Runnable() {
+ @Override
+ public void run() {
+ session.wipe(new RepositorySessionWipeDelegate() {
+ public void onWipeSucceeded() {
+ performNotify();
+ }
+ public void onWipeFailed(Exception ex) {
+ fail("wipe should have succeeded");
+ performNotify();
+ }
+ @Override
+ public RepositorySessionWipeDelegate deferredWipeDelegate(final ExecutorService executor) {
+ final RepositorySessionWipeDelegate self = this;
+ return new RepositorySessionWipeDelegate() {
+
+ @Override
+ public void onWipeSucceeded() {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ self.onWipeSucceeded();
+ }}).start();
+ }
+
+ @Override
+ public void onWipeFailed(final Exception ex) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ self.onWipeFailed(ex);
+ }}).start();
+ }
+
+ @Override
+ public RepositorySessionWipeDelegate deferredWipeDelegate(ExecutorService newExecutor) {
+ if (newExecutor == executor) {
+ return this;
+ }
+ throw new IllegalArgumentException("Can't re-defer this delegate.");
+ }
+ };
+ }
+ });
+ }
+ };
+
+ // Store 2 records.
+ Record[] records = new Record[] { record0, record1 };
+ performWait(storeManyRunnable(session, records));
+ performWait(fetchAllRunnable(session, records));
+
+ // Wipe.
+ performWait(run);
+ dispose(session);
+ }
+
+ /*
+ * TODO adding or subtracting from lastModified timestamps does NOTHING
+ * since it gets overwritten when we store stuff. See other tests
+ * for ways to do this properly.
+ */
+
+ /*
+ * Record being stored has newer timestamp than existing local record, local
+ * record has not been modified since last sync.
+ */
+ protected void remoteNewerTimeStamp(Record local, Record remote) {
+ final RepositorySession session = createAndBeginSession();
+
+ // Record existing and hasn't changed since before lastSync.
+ // Automatically will be assigned lastModified = current time.
+ performWait(storeRunnable(session, local));
+
+ remote.guid = local.guid;
+
+ // Get the timestamp and make remote newer than it
+ ExpectFetchDelegate timestampDelegate = preparedExpectFetchDelegate(new Record[] { local });
+ performWait(fetchRunnable(session, new String[] { remote.guid }, timestampDelegate));
+ remote.lastModified = timestampDelegate.records.get(0).lastModified + 1000;
+ performWait(storeRunnable(session, remote));
+
+ Record[] expected = new Record[] { remote };
+ ExpectFetchDelegate delegate = preparedExpectFetchDelegate(expected);
+ performWait(fetchAllRunnable(session, delegate));
+ dispose(session);
+ }
+
+ /*
+ * Local record has a newer timestamp than the record being stored. For now,
+ * we just take newer (local) record)
+ */
+ protected void localNewerTimeStamp(Record local, Record remote) {
+ final RepositorySession session = createAndBeginSession();
+
+ performWait(storeRunnable(session, local));
+
+ remote.guid = local.guid;
+
+ // Get the timestamp and make remote older than it
+ ExpectFetchDelegate timestampDelegate = preparedExpectFetchDelegate(new Record[] { local });
+ performWait(fetchRunnable(session, new String[] { remote.guid }, timestampDelegate));
+ remote.lastModified = timestampDelegate.records.get(0).lastModified - 1000;
+ performWait(storeRunnable(session, remote));
+
+ // Do a fetch and make sure that we get back the local record.
+ Record[] expected = new Record[] { local };
+ performWait(fetchAllRunnable(session, preparedExpectFetchDelegate(expected)));
+ dispose(session);
+ }
+
+ /*
+ * Insert a record that is marked as deleted, remote has newer timestamp
+ */
+ protected void deleteRemoteNewer(Record local, Record remote) {
+ final RepositorySession session = createAndBeginSession();
+
+ // Record existing and hasn't changed since before lastSync.
+ // Automatically will be assigned lastModified = current time.
+ performWait(storeRunnable(session, local));
+
+ // Pass the same record to store, but mark it deleted and modified
+ // more recently
+ ExpectFetchDelegate timestampDelegate = preparedExpectFetchDelegate(new Record[] { local });
+ performWait(fetchRunnable(session, new String[] { local.guid }, timestampDelegate));
+ remote.lastModified = timestampDelegate.records.get(0).lastModified + 1000;
+ remote.deleted = true;
+ remote.guid = local.guid;
+ performWait(storeRunnable(session, remote));
+
+ performWait(fetchAllRunnable(session, preparedExpectFetchDelegate(new Record[]{})));
+ dispose(session);
+ }
+
+ // Store two records that are identical (this has different meanings based on the
+ // type of record) other than their guids. The record existing locally already
+ // should have its guid replaced (the assumption is that the record existed locally
+ // and then sync was enabled and this record existed on another sync'd device).
+ public void storeIdenticalExceptGuid(Record record0) {
+ Logger.debug("storeIdenticalExceptGuid", "Started.");
+ final RepositorySession session = createAndBeginSession();
+ Logger.debug("storeIdenticalExceptGuid", "Session is " + session);
+ performWait(storeRunnable(session, record0));
+ Logger.debug("storeIdenticalExceptGuid", "Stored record0.");
+ DefaultFetchDelegate timestampDelegate = getTimestampDelegate(record0.guid);
+
+ performWait(fetchRunnable(session, new String[] { record0.guid }, timestampDelegate));
+ Logger.debug("storeIdenticalExceptGuid", "fetchRunnable done.");
+ record0.lastModified = timestampDelegate.records.get(0).lastModified + 3000;
+ record0.guid = Utils.generateGuid();
+ Logger.debug("storeIdenticalExceptGuid", "Storing modified...");
+ performWait(storeRunnable(session, record0));
+ Logger.debug("storeIdenticalExceptGuid", "Stored modified.");
+
+ Record[] expected = new Record[] { record0 };
+ performWait(fetchAllRunnable(session, preparedExpectFetchDelegate(expected)));
+ Logger.debug("storeIdenticalExceptGuid", "Fetched all. Returning.");
+ dispose(session);
+ }
+
+ // Special delegate so that we don't verify parenting is correct since
+ // at some points it won't be since parent folder hasn't been stored.
+ private DefaultFetchDelegate getTimestampDelegate(final String guid) {
+ return new DefaultFetchDelegate() {
+ @Override
+ public void onFetchCompleted(final long fetchEnd) {
+ assertEquals(guid, this.records.get(0).guid);
+ performNotify();
+ }
+ };
+ }
+
+ /*
+ * Insert a record that is marked as deleted, local has newer timestamp
+ * and was not marked deleted (so keep it)
+ */
+ protected void deleteLocalNewer(Record local, Record remote) {
+ Logger.debug("deleteLocalNewer", "Begin.");
+ final RepositorySession session = createAndBeginSession();
+
+ Logger.debug("deleteLocalNewer", "Storing local...");
+ performWait(storeRunnable(session, local));
+
+ // Create an older version of a record with the same GUID.
+ remote.guid = local.guid;
+
+ Logger.debug("deleteLocalNewer", "Fetching...");
+
+ // Get the timestamp and make remote older than it
+ Record[] expected = new Record[] { local };
+ ExpectFetchDelegate timestampDelegate = preparedExpectFetchDelegate(expected);
+ performWait(fetchRunnable(session, new String[] { remote.guid }, timestampDelegate));
+
+ Logger.debug("deleteLocalNewer", "Fetched.");
+ remote.lastModified = timestampDelegate.records.get(0).lastModified - 1000;
+
+ Logger.debug("deleteLocalNewer", "Last modified is " + remote.lastModified);
+ remote.deleted = true;
+ Logger.debug("deleteLocalNewer", "Storing deleted...");
+ performWait(quietStoreRunnable(session, remote)); // This appears to do a lot of work...?!
+
+ // Do a fetch and make sure that we get back the first (local) record.
+ performWait(fetchAllRunnable(session, preparedExpectFetchDelegate(expected)));
+ Logger.debug("deleteLocalNewer", "Fetched and done!");
+ dispose(session);
+ }
+
+ /*
+ * Insert a record that is marked as deleted, record never existed locally
+ */
+ protected void deleteRemoteLocalNonexistent(Record remote) {
+ final RepositorySession session = createAndBeginSession();
+
+ long timestamp = 1000000000;
+
+ // Pass a record marked deleted to store, doesn't exist locally
+ remote.lastModified = timestamp;
+ remote.deleted = true;
+ performWait(quietStoreRunnable(session, remote));
+
+ ExpectFetchDelegate delegate = preparedExpectFetchDelegate(new Record[]{});
+ performWait(fetchAllRunnable(session, delegate));
+ dispose(session);
+ }
+
+ /*
+ * Tests that don't require specific records based on type of repository.
+ * These tests don't need to be overriden in subclasses, they will just work.
+ */
+ public void testCreateSessionNullContext() {
+ Logger.debug(LOG_TAG, "In testCreateSessionNullContext.");
+ Repository repo = getRepository();
+ try {
+ repo.createSession(new DefaultSessionCreationDelegate(), null);
+ fail("Should throw.");
+ } catch (Exception ex) {
+ assertNotNull(ex);
+ }
+ }
+
+ public void testStoreNullRecord() {
+ final RepositorySession session = createAndBeginSession();
+ try {
+ session.setStoreDelegate(new DefaultStoreDelegate());
+ session.store(null);
+ fail("Should throw.");
+ } catch (Exception ex) {
+ assertNotNull(ex);
+ }
+ dispose(session);
+ }
+
+ public void testFetchNoGuids() {
+ final RepositorySession session = createAndBeginSession();
+ performWait(fetchRunnable(session, new String[] {}, new ExpectInvalidRequestFetchDelegate()));
+ dispose(session);
+ }
+
+ public void testFetchNullGuids() {
+ final RepositorySession session = createAndBeginSession();
+ performWait(fetchRunnable(session, null, new ExpectInvalidRequestFetchDelegate()));
+ dispose(session);
+ }
+
+ public void testBeginOnNewSession() {
+ final RepositorySession session = createSession();
+ performWait(beginRunnable(session, new ExpectBeginDelegate()));
+ dispose(session);
+ }
+
+ public void testBeginOnRunningSession() {
+ final RepositorySession session = createAndBeginSession();
+ try {
+ session.begin(new ExpectBeginFailDelegate());
+ } catch (InvalidSessionTransitionException e) {
+ dispose(session);
+ return;
+ }
+ fail("Should have caught InvalidSessionTransitionException.");
+ }
+
+ public void testBeginOnFinishedSession() throws InactiveSessionException {
+ final RepositorySession session = createAndBeginSession();
+ performWait(finishRunnable(session, new ExpectFinishDelegate()));
+ try {
+ session.begin(new ExpectBeginFailDelegate());
+ } catch (InvalidSessionTransitionException e) {
+ Logger.debug(getName(), "Yay! Got an exception.", e);
+ dispose(session);
+ return;
+ } catch (Exception e) {
+ Logger.debug(getName(), "Yay! Got an exception.", e);
+ dispose(session);
+ return;
+ }
+ fail("Should have caught InvalidSessionTransitionException.");
+ }
+
+ public void testFinishOnFinishedSession() throws InactiveSessionException {
+ final RepositorySession session = createAndBeginSession();
+ performWait(finishRunnable(session, new ExpectFinishDelegate()));
+ try {
+ session.finish(new ExpectFinishFailDelegate());
+ } catch (InactiveSessionException e) {
+ dispose(session);
+ return;
+ }
+ fail("Should have caught InactiveSessionException.");
+ }
+
+ public void testFetchOnInactiveSession() throws InactiveSessionException {
+ final RepositorySession session = createSession();
+ try {
+ session.fetch(new String[] { Utils.generateGuid() }, new DefaultFetchDelegate());
+ } catch (InactiveSessionException e) {
+ // Yay.
+ dispose(session);
+ return;
+ };
+ fail("Should have caught InactiveSessionException.");
+ }
+
+ public void testFetchOnFinishedSession() {
+ final RepositorySession session = createAndBeginSession();
+ Logger.debug(getName(), "Finishing...");
+ performWait(finishRunnable(session, new ExpectFinishDelegate()));
+ try {
+ session.fetch(new String[] { Utils.generateGuid() }, new DefaultFetchDelegate());
+ } catch (InactiveSessionException e) {
+ // Yay.
+ dispose(session);
+ return;
+ };
+ fail("Should have caught InactiveSessionException.");
+ }
+
+ public void testGuidsSinceOnUnstartedSession() {
+ final RepositorySession session = createSession();
+ Runnable run = new Runnable() {
+ @Override
+ public void run() {
+ session.guidsSince(System.currentTimeMillis(),
+ new RepositorySessionGuidsSinceDelegate() {
+ public void onGuidsSinceSucceeded(String[] guids) {
+ fail("Session inactive, should fail");
+ performNotify();
+ }
+
+ public void onGuidsSinceFailed(Exception ex) {
+ verifyInactiveException(ex);
+ performNotify();
+ }
+ });
+ }
+ };
+ performWait(run);
+ dispose(session);
+ }
+
+ private static void verifyInactiveException(Exception ex) {
+ if (!(ex instanceof InactiveSessionException)) {
+ fail("Wrong exception type");
+ }
+ }
+
+ protected void closeDataAccessor(AndroidBrowserRepositoryDataAccessor dataAccessor) {
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserBookmarksRepository.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserBookmarksRepository.java
new file mode 100644
index 0000000000..71563a46cd
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserBookmarksRepository.java
@@ -0,0 +1,636 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.db;
+
+import java.util.ArrayList;
+
+import org.json.simple.JSONArray;
+import org.mozilla.gecko.background.sync.helpers.BookmarkHelpers;
+import org.mozilla.gecko.background.sync.helpers.ExpectFetchDelegate;
+import org.mozilla.gecko.background.sync.helpers.ExpectFetchSinceDelegate;
+import org.mozilla.gecko.background.sync.helpers.ExpectFinishDelegate;
+import org.mozilla.gecko.background.sync.helpers.ExpectGuidsSinceDelegate;
+import org.mozilla.gecko.background.sync.helpers.ExpectInvalidTypeStoreDelegate;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.repositories.InactiveSessionException;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksDataAccessor;
+import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksRepository;
+import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksRepositorySession;
+import org.mozilla.gecko.sync.repositories.android.AndroidBrowserRepository;
+import org.mozilla.gecko.sync.repositories.android.AndroidBrowserRepositoryDataAccessor;
+import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
+import org.mozilla.gecko.sync.repositories.android.RepoUtils;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+
+public class TestAndroidBrowserBookmarksRepository extends AndroidBrowserRepositoryTestCase {
+
+ @Override
+ protected AndroidBrowserRepository getRepository() {
+
+ /**
+ * Override this chain in order to avoid our test code having to create two
+ * sessions all the time.
+ */
+ return new AndroidBrowserBookmarksRepository() {
+ @Override
+ protected void sessionCreator(RepositorySessionCreationDelegate delegate, Context context) {
+ AndroidBrowserBookmarksRepositorySession session;
+ session = new AndroidBrowserBookmarksRepositorySession(this, context) {
+ @Override
+ protected synchronized void trackGUID(String guid) {
+ System.out.println("Ignoring trackGUID call: this is a test!");
+ }
+ };
+ delegate.deferredCreationDelegate().onSessionCreated(session);
+ }
+ };
+ }
+
+ @Override
+ protected AndroidBrowserRepositoryDataAccessor getDataAccessor() {
+ return new AndroidBrowserBookmarksDataAccessor(getApplicationContext());
+ }
+
+ /**
+ * Hook to return an ExpectFetchDelegate, possibly with special GUIDs ignored.
+ */
+ @Override
+ public ExpectFetchDelegate preparedExpectFetchDelegate(Record[] expected) {
+ ExpectFetchDelegate delegate = new ExpectFetchDelegate(expected);
+ delegate.ignore.addAll(AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS_MAP.keySet());
+ return delegate;
+ }
+
+ /**
+ * Hook to return an ExpectGuidsSinceDelegate expecting only special GUIDs (if there are any).
+ */
+ public ExpectGuidsSinceDelegate preparedExpectOnlySpecialGuidsSinceDelegate() {
+ ExpectGuidsSinceDelegate delegate = new ExpectGuidsSinceDelegate(AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS_MAP.keySet().toArray(new String[] {}));
+ return delegate;
+ }
+
+ /**
+ * Hook to return an ExpectGuidsSinceDelegate, possibly with special GUIDs ignored.
+ */
+ @Override
+ public ExpectGuidsSinceDelegate preparedExpectGuidsSinceDelegate(String[] expected) {
+ ExpectGuidsSinceDelegate delegate = new ExpectGuidsSinceDelegate(expected);
+ delegate.ignore.addAll(AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS_MAP.keySet());
+ return delegate;
+ }
+
+ /**
+ * Hook to return an ExpectFetchSinceDelegate, possibly with special GUIDs ignored.
+ */
+ public ExpectFetchSinceDelegate preparedExpectFetchSinceDelegate(long timestamp, String[] expected) {
+ ExpectFetchSinceDelegate delegate = new ExpectFetchSinceDelegate(timestamp, expected);
+ delegate.ignore.addAll(AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS_MAP.keySet());
+ return delegate;
+ }
+
+ // NOTE NOTE NOTE
+ // Must store folder before records if we we are checking that the
+ // records returned are the same as those sent in. If the folder isn't stored
+ // first, the returned records won't be identical to those stored because we
+ // aren't able to find the parent name/guid when we do a fetch. If you don't want
+ // to store a folder first, store your record in "mobile" or one of the folders
+ // that always exists.
+
+ public void testFetchOneWithChildren() {
+ BookmarkRecord folder = BookmarkHelpers.createFolder1();
+ BookmarkRecord bookmark1 = BookmarkHelpers.createBookmark1();
+ BookmarkRecord bookmark2 = BookmarkHelpers.createBookmark2();
+
+ RepositorySession session = createAndBeginSession();
+
+ Record[] records = new Record[] { folder, bookmark1, bookmark2 };
+ performWait(storeManyRunnable(session, records));
+
+ AndroidBrowserRepositoryDataAccessor helper = getDataAccessor();
+ helper.dumpDB();
+ closeDataAccessor(helper);
+
+ String[] guids = new String[] { folder.guid };
+ Record[] expected = new Record[] { folder };
+ performWait(fetchRunnable(session, guids, expected));
+ dispose(session);
+ }
+
+ @Override
+ public void testFetchAll() {
+ Record[] expected = new Record[3];
+ expected[0] = BookmarkHelpers.createFolder1();
+ expected[1] = BookmarkHelpers.createBookmark1();
+ expected[2] = BookmarkHelpers.createBookmark2();
+ basicFetchAllTest(expected);
+ }
+
+ @Override
+ public void testGuidsSinceReturnMultipleRecords() {
+ BookmarkRecord record0 = BookmarkHelpers.createBookmark1();
+ BookmarkRecord record1 = BookmarkHelpers.createBookmark2();
+ guidsSinceReturnMultipleRecords(record0, record1);
+ }
+
+ @Override
+ public void testGuidsSinceReturnNoRecords() {
+ guidsSinceReturnNoRecords(BookmarkHelpers.createBookmarkInMobileFolder1());
+ }
+
+ @Override
+ public void testFetchSinceOneRecord() {
+ fetchSinceOneRecord(BookmarkHelpers.createBookmarkInMobileFolder1(),
+ BookmarkHelpers.createBookmarkInMobileFolder2());
+ }
+
+ @Override
+ public void testFetchSinceReturnNoRecords() {
+ fetchSinceReturnNoRecords(BookmarkHelpers.createBookmark1());
+ }
+
+ @Override
+ public void testFetchOneRecordByGuid() {
+ fetchOneRecordByGuid(BookmarkHelpers.createBookmarkInMobileFolder1(),
+ BookmarkHelpers.createBookmarkInMobileFolder2());
+ }
+
+ @Override
+ public void testFetchMultipleRecordsByGuids() {
+ BookmarkRecord record0 = BookmarkHelpers.createFolder1();
+ BookmarkRecord record1 = BookmarkHelpers.createBookmark1();
+ BookmarkRecord record2 = BookmarkHelpers.createBookmark2();
+ fetchMultipleRecordsByGuids(record0, record1, record2);
+ }
+
+ @Override
+ public void testFetchNoRecordByGuid() {
+ fetchNoRecordByGuid(BookmarkHelpers.createBookmark1());
+ }
+
+
+ @Override
+ public void testWipe() {
+ doWipe(BookmarkHelpers.createBookmarkInMobileFolder1(),
+ BookmarkHelpers.createBookmarkInMobileFolder2());
+ }
+
+ @Override
+ public void testStore() {
+ basicStoreTest(BookmarkHelpers.createBookmark1());
+ }
+
+
+ public void testStoreFolder() {
+ basicStoreTest(BookmarkHelpers.createFolder1());
+ }
+
+ /**
+ * TODO: 2011-12-24, tests disabled because we no longer fail
+ * a store call if we get an unknown record type.
+ */
+ /*
+ * Test storing each different type of Bookmark record.
+ * We expect any records with type other than "bookmark"
+ * or "folder" to fail. For now we throw these away.
+ */
+ /*
+ public void testStoreMicrosummary() {
+ basicStoreFailTest(BookmarkHelpers.createMicrosummary());
+ }
+
+ public void testStoreQuery() {
+ basicStoreFailTest(BookmarkHelpers.createQuery());
+ }
+
+ public void testStoreLivemark() {
+ basicStoreFailTest(BookmarkHelpers.createLivemark());
+ }
+
+ public void testStoreSeparator() {
+ basicStoreFailTest(BookmarkHelpers.createSeparator());
+ }
+ */
+
+ protected void basicStoreFailTest(Record record) {
+ final RepositorySession session = createAndBeginSession();
+ performWait(storeRunnable(session, record, new ExpectInvalidTypeStoreDelegate()));
+ dispose(session);
+ }
+
+ /*
+ * Re-parenting tests
+ */
+ // Insert two records missing parent, then insert their parent.
+ // Make sure they end up with the correct parent on fetch.
+ public void testBasicReparenting() throws InactiveSessionException {
+ Record[] expected = new Record[] {
+ BookmarkHelpers.createBookmark1(),
+ BookmarkHelpers.createBookmark2(),
+ BookmarkHelpers.createFolder1()
+ };
+ doMultipleFolderReparentingTest(expected);
+ }
+
+ // Insert 3 folders and 4 bookmarks in different orders
+ // and make sure they come out parented correctly
+ public void testMultipleFolderReparenting1() throws InactiveSessionException {
+ Record[] expected = new Record[] {
+ BookmarkHelpers.createBookmark1(),
+ BookmarkHelpers.createBookmark2(),
+ BookmarkHelpers.createBookmark3(),
+ BookmarkHelpers.createFolder1(),
+ BookmarkHelpers.createBookmark4(),
+ BookmarkHelpers.createFolder3(),
+ BookmarkHelpers.createFolder2(),
+ };
+ doMultipleFolderReparentingTest(expected);
+ }
+
+ public void testMultipleFolderReparenting2() throws InactiveSessionException {
+ Record[] expected = new Record[] {
+ BookmarkHelpers.createBookmark1(),
+ BookmarkHelpers.createBookmark2(),
+ BookmarkHelpers.createBookmark3(),
+ BookmarkHelpers.createFolder1(),
+ BookmarkHelpers.createBookmark4(),
+ BookmarkHelpers.createFolder3(),
+ BookmarkHelpers.createFolder2(),
+ };
+ doMultipleFolderReparentingTest(expected);
+ }
+
+ public void testMultipleFolderReparenting3() throws InactiveSessionException {
+ Record[] expected = new Record[] {
+ BookmarkHelpers.createBookmark1(),
+ BookmarkHelpers.createBookmark2(),
+ BookmarkHelpers.createBookmark3(),
+ BookmarkHelpers.createFolder1(),
+ BookmarkHelpers.createBookmark4(),
+ BookmarkHelpers.createFolder3(),
+ BookmarkHelpers.createFolder2(),
+ };
+ doMultipleFolderReparentingTest(expected);
+ }
+
+ private void doMultipleFolderReparentingTest(Record[] expected) throws InactiveSessionException {
+ final RepositorySession session = createAndBeginSession();
+ doStore(session, expected);
+ ExpectFetchDelegate delegate = preparedExpectFetchDelegate(expected);
+ performWait(fetchAllRunnable(session, delegate));
+ performWait(finishRunnable(session, new ExpectFinishDelegate()));
+ }
+
+ /*
+ * Test storing identical records with different guids.
+ * For bookmarks identical is defined by the following fields
+ * being the same: title, uri, type, parentName
+ */
+ @Override
+ public void testStoreIdenticalExceptGuid() {
+ storeIdenticalExceptGuid(BookmarkHelpers.createBookmarkInMobileFolder1());
+ }
+
+ /*
+ * More complicated situation in which we insert a folder
+ * followed by a couple of its children. We then insert
+ * the folder again but with a different guid. Children
+ * must still get correct parent when they are fetched.
+ * Store a record after with the new guid as the parent
+ * and make sure it works as well.
+ */
+ public void testStoreIdenticalFoldersWithChildren() {
+ final RepositorySession session = createAndBeginSession();
+ Record record0 = BookmarkHelpers.createFolder1();
+
+ // Get timestamp so that the conflicting folder that we store below is newer.
+ // Children won't come back on this fetch since they haven't been stored, so remove them
+ // before our delegate throws a failure.
+ BookmarkRecord rec0 = (BookmarkRecord) record0;
+ rec0.children = new JSONArray();
+ performWait(storeRunnable(session, record0));
+
+ ExpectFetchDelegate timestampDelegate = preparedExpectFetchDelegate(new Record[] { rec0 });
+ performWait(fetchRunnable(session, new String[] { record0.guid }, timestampDelegate));
+
+ AndroidBrowserRepositoryDataAccessor helper = getDataAccessor();
+ helper.dumpDB();
+ closeDataAccessor(helper);
+
+ Record record1 = BookmarkHelpers.createBookmark1();
+ Record record2 = BookmarkHelpers.createBookmark2();
+ Record record3 = BookmarkHelpers.createFolder1();
+ BookmarkRecord bmk3 = (BookmarkRecord) record3;
+ record3.guid = Utils.generateGuid();
+ record3.lastModified = timestampDelegate.records.get(0).lastModified + 3000;
+ assertFalse(record0.guid.equals(record3.guid));
+
+ // Store an additional record after inserting the duplicate folder
+ // with new GUID. Make sure it comes back as well.
+ Record record4 = BookmarkHelpers.createBookmark3();
+ BookmarkRecord bmk4 = (BookmarkRecord) record4;
+ bmk4.parentID = bmk3.guid;
+ bmk4.parentName = bmk3.parentName;
+
+ doStore(session, new Record[] {
+ record1, record2, record3, bmk4
+ });
+ BookmarkRecord bmk1 = (BookmarkRecord) record1;
+ bmk1.parentID = record3.guid;
+ BookmarkRecord bmk2 = (BookmarkRecord) record2;
+ bmk2.parentID = record3.guid;
+ Record[] expect = new Record[] {
+ bmk1, bmk2, record3
+ };
+ fetchAllRunnable(session, preparedExpectFetchDelegate(expect));
+ dispose(session);
+ }
+
+ @Override
+ public void testRemoteNewerTimeStamp() {
+ BookmarkRecord local = BookmarkHelpers.createBookmarkInMobileFolder1();
+ BookmarkRecord remote = BookmarkHelpers.createBookmarkInMobileFolder2();
+ remoteNewerTimeStamp(local, remote);
+ }
+
+ @Override
+ public void testLocalNewerTimeStamp() {
+ BookmarkRecord local = BookmarkHelpers.createBookmarkInMobileFolder1();
+ BookmarkRecord remote = BookmarkHelpers.createBookmarkInMobileFolder2();
+ localNewerTimeStamp(local, remote);
+ }
+
+ @Override
+ public void testDeleteRemoteNewer() {
+ BookmarkRecord local = BookmarkHelpers.createBookmarkInMobileFolder1();
+ BookmarkRecord remote = BookmarkHelpers.createBookmarkInMobileFolder2();
+ deleteRemoteNewer(local, remote);
+ }
+
+ @Override
+ public void testDeleteLocalNewer() {
+ BookmarkRecord local = BookmarkHelpers.createBookmarkInMobileFolder1();
+ BookmarkRecord remote = BookmarkHelpers.createBookmarkInMobileFolder2();
+ deleteLocalNewer(local, remote);
+ }
+
+ @Override
+ public void testDeleteRemoteLocalNonexistent() {
+ BookmarkRecord remote = BookmarkHelpers.createBookmark2();
+ deleteRemoteLocalNonexistent(remote);
+ }
+
+ @Override
+ public void testCleanMultipleRecords() {
+ cleanMultipleRecords(
+ BookmarkHelpers.createBookmarkInMobileFolder1(),
+ BookmarkHelpers.createBookmarkInMobileFolder2(),
+ BookmarkHelpers.createBookmark1(),
+ BookmarkHelpers.createBookmark2(),
+ BookmarkHelpers.createFolder1());
+ }
+
+ public void testBasicPositioning() {
+ final RepositorySession session = createAndBeginSession();
+ Record[] expected = new Record[] {
+ BookmarkHelpers.createBookmark1(),
+ BookmarkHelpers.createFolder1(),
+ BookmarkHelpers.createBookmark2()
+ };
+ System.out.println("TEST: Inserting " + expected[0].guid + ", "
+ + expected[1].guid + ", "
+ + expected[2].guid);
+ doStore(session, expected);
+
+ ExpectFetchDelegate delegate = preparedExpectFetchDelegate(expected);
+ performWait(fetchAllRunnable(session, delegate));
+
+ int found = 0;
+ boolean foundFolder = false;
+ for (int i = 0; i < delegate.records.size(); i++) {
+ BookmarkRecord rec = (BookmarkRecord) delegate.records.get(i);
+ if (rec.guid.equals(expected[0].guid)) {
+ assertEquals(0, ((BookmarkRecord) delegate.records.get(i)).androidPosition);
+ found++;
+ } else if (rec.guid.equals(expected[2].guid)) {
+ assertEquals(1, ((BookmarkRecord) delegate.records.get(i)).androidPosition);
+ found++;
+ } else if (rec.guid.equals(expected[1].guid)) {
+ foundFolder = true;
+ } else {
+ System.out.println("TEST: found " + rec.guid);
+ }
+ }
+ assertTrue(foundFolder);
+ assertEquals(2, found);
+ dispose(session);
+ }
+
+ public void testSqlInjectPurgeDeleteAndUpdateByGuid() {
+ // Some setup.
+ RepositorySession session = createAndBeginSession();
+ AndroidBrowserRepositoryDataAccessor db = getDataAccessor();
+
+ ContentValues cv = new ContentValues();
+ cv.put(BrowserContract.SyncColumns.IS_DELETED, 1);
+
+ // Create and insert 2 bookmarks, 2nd one is evil (attempts injection).
+ BookmarkRecord bmk1 = BookmarkHelpers.createBookmark1();
+ BookmarkRecord bmk2 = BookmarkHelpers.createBookmark2();
+ bmk2.guid = "' or '1'='1";
+
+ db.insert(bmk1);
+ db.insert(bmk2);
+
+ // Test 1 - updateByGuid() handles evil bookmarks correctly.
+ db.updateByGuid(bmk2.guid, cv);
+
+ // Query bookmarks table.
+ Cursor cur = getAllBookmarks();
+ int numBookmarks = cur.getCount();
+
+ // Ensure only the evil bookmark is marked for deletion.
+ try {
+ cur.moveToFirst();
+ while (!cur.isAfterLast()) {
+ String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
+ boolean deleted = RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1;
+
+ if (guid.equals(bmk2.guid)) {
+ assertTrue(deleted);
+ } else {
+ assertFalse(deleted);
+ }
+ cur.moveToNext();
+ }
+ } finally {
+ cur.close();
+ }
+
+ // Test 2 - Ensure purgeDelete()'s call to delete() deletes only 1 record.
+ try {
+ db.purgeDeleted();
+ } catch (NullCursorException e) {
+ e.printStackTrace();
+ }
+
+ cur = getAllBookmarks();
+ int numBookmarksAfterDeletion = cur.getCount();
+
+ // Ensure we have only 1 deleted row.
+ assertEquals(numBookmarksAfterDeletion, numBookmarks - 1);
+
+ // Ensure only the evil bookmark is deleted.
+ try {
+ cur.moveToFirst();
+ while (!cur.isAfterLast()) {
+ String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
+ boolean deleted = RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1;
+
+ if (guid.equals(bmk2.guid)) {
+ fail("Evil guid was not deleted!");
+ } else {
+ assertFalse(deleted);
+ }
+ cur.moveToNext();
+ }
+ } finally {
+ cur.close();
+ }
+ dispose(session);
+ }
+
+ protected Cursor getAllBookmarks() {
+ Context context = getApplicationContext();
+ Cursor cur = context.getContentResolver().query(BrowserContractHelpers.BOOKMARKS_CONTENT_URI,
+ BrowserContractHelpers.BookmarkColumns, null, null, null);
+ return cur;
+ }
+
+ public void testSqlInjectFetch() {
+ // Some setup.
+ RepositorySession session = createAndBeginSession();
+ AndroidBrowserRepositoryDataAccessor db = getDataAccessor();
+
+ // Create and insert 4 bookmarks, last one is evil (attempts injection).
+ BookmarkRecord bmk1 = BookmarkHelpers.createBookmark1();
+ BookmarkRecord bmk2 = BookmarkHelpers.createBookmark2();
+ BookmarkRecord bmk3 = BookmarkHelpers.createBookmark3();
+ BookmarkRecord bmk4 = BookmarkHelpers.createBookmark4();
+ bmk4.guid = "' or '1'='1";
+
+ db.insert(bmk1);
+ db.insert(bmk2);
+ db.insert(bmk3);
+ db.insert(bmk4);
+
+ // Perform a fetch.
+ Cursor cur = null;
+ try {
+ cur = db.fetch(new String[] { bmk3.guid, bmk4.guid });
+ } catch (NullCursorException e1) {
+ e1.printStackTrace();
+ }
+
+ // Ensure the correct number (2) of records were fetched and with the correct guids.
+ if (cur == null) {
+ fail("No records were fetched.");
+ }
+
+ try {
+ if (cur.getCount() != 2) {
+ fail("Wrong number of guids fetched!");
+ }
+ cur.moveToFirst();
+ while (!cur.isAfterLast()) {
+ String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
+ if (!guid.equals(bmk3.guid) && !guid.equals(bmk4.guid)) {
+ fail("Wrong guids were fetched!");
+ }
+ cur.moveToNext();
+ }
+ } finally {
+ cur.close();
+ }
+ dispose(session);
+ }
+
+ public void testSqlInjectDelete() {
+ // Some setup.
+ RepositorySession session = createAndBeginSession();
+ AndroidBrowserRepositoryDataAccessor db = getDataAccessor();
+
+ // Create and insert 2 bookmarks, 2nd one is evil (attempts injection).
+ BookmarkRecord bmk1 = BookmarkHelpers.createBookmark1();
+ BookmarkRecord bmk2 = BookmarkHelpers.createBookmark2();
+ bmk2.guid = "' or '1'='1";
+
+ db.insert(bmk1);
+ db.insert(bmk2);
+
+ // Note size of table before delete.
+ Cursor cur = getAllBookmarks();
+ int numBookmarks = cur.getCount();
+
+ db.purgeGuid(bmk2.guid);
+
+ // Note size of table after delete.
+ cur = getAllBookmarks();
+ int numBookmarksAfterDelete = cur.getCount();
+
+ // Ensure size of table after delete is *only* 1 less.
+ assertEquals(numBookmarksAfterDelete, numBookmarks - 1);
+
+ try {
+ cur.moveToFirst();
+ while (!cur.isAfterLast()) {
+ String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
+ if (guid.equals(bmk2.guid)) {
+ fail("Guid was not deleted!");
+ }
+ cur.moveToNext();
+ }
+ } finally {
+ cur.close();
+ }
+ dispose(session);
+ }
+
+ /**
+ * Verify that data accessor's bulkInsert actually inserts.
+ * @throws NullCursorException
+ */
+ public void testBulkInsert() throws NullCursorException {
+ RepositorySession session = createAndBeginSession();
+ AndroidBrowserRepositoryDataAccessor db = getDataAccessor();
+
+ // Have to set androidID of parent manually.
+ Cursor cur = db.fetch(new String[] { "mobile" } );
+ assertEquals(1, cur.getCount());
+ cur.moveToFirst();
+ int mobileAndroidID = RepoUtils.getIntFromCursor(cur, BrowserContract.Bookmarks._ID);
+
+ BookmarkRecord bookmark1 = BookmarkHelpers.createBookmarkInMobileFolder1();
+ BookmarkRecord bookmark2 = BookmarkHelpers.createBookmarkInMobileFolder2();
+ bookmark1.androidParentID = mobileAndroidID;
+ bookmark2.androidParentID = mobileAndroidID;
+ ArrayList<Record> recordList = new ArrayList<Record>();
+ recordList.add(bookmark1);
+ recordList.add(bookmark2);
+ db.bulkInsert(recordList);
+
+ String[] guids = new String[] { bookmark1.guid, bookmark2.guid };
+ Record[] expected = new Record[] { bookmark1, bookmark2 };
+ performWait(fetchRunnable(session, guids, expected));
+ dispose(session);
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryRepository.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryRepository.java
new file mode 100644
index 0000000000..ffde595757
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryRepository.java
@@ -0,0 +1,450 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.db;
+
+import java.util.ArrayList;
+
+import org.json.simple.JSONObject;
+import org.mozilla.gecko.background.sync.helpers.ExpectFetchDelegate;
+import org.mozilla.gecko.background.sync.helpers.HistoryHelpers;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.android.AndroidBrowserHistoryDataAccessor;
+import org.mozilla.gecko.sync.repositories.android.AndroidBrowserHistoryRepository;
+import org.mozilla.gecko.sync.repositories.android.AndroidBrowserHistoryRepositorySession;
+import org.mozilla.gecko.sync.repositories.android.AndroidBrowserRepository;
+import org.mozilla.gecko.sync.repositories.android.AndroidBrowserRepositoryDataAccessor;
+import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
+import org.mozilla.gecko.sync.repositories.android.RepoUtils;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+import org.mozilla.gecko.sync.repositories.domain.HistoryRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class TestAndroidBrowserHistoryRepository extends AndroidBrowserRepositoryTestCase {
+
+ @Override
+ protected AndroidBrowserRepository getRepository() {
+
+ /**
+ * Override this chain in order to avoid our test code having to create two
+ * sessions all the time.
+ */
+ return new AndroidBrowserHistoryRepository() {
+ @Override
+ protected void sessionCreator(RepositorySessionCreationDelegate delegate, Context context) {
+ AndroidBrowserHistoryRepositorySession session;
+ session = new AndroidBrowserHistoryRepositorySession(this, context) {
+ @Override
+ protected synchronized void trackGUID(String guid) {
+ System.out.println("Ignoring trackGUID call: this is a test!");
+ }
+ };
+ delegate.onSessionCreated(session);
+ }
+ };
+ }
+
+ @Override
+ protected AndroidBrowserRepositoryDataAccessor getDataAccessor() {
+ return new AndroidBrowserHistoryDataAccessor(getApplicationContext());
+ }
+
+ @Override
+ public void testFetchAll() {
+ Record[] expected = new Record[2];
+ expected[0] = HistoryHelpers.createHistory3();
+ expected[1] = HistoryHelpers.createHistory2();
+ basicFetchAllTest(expected);
+ }
+
+ /*
+ * Test storing identical records with different guids.
+ * For bookmarks identical is defined by the following fields
+ * being the same: title, uri, type, parentName
+ */
+ @Override
+ public void testStoreIdenticalExceptGuid() {
+ storeIdenticalExceptGuid(HistoryHelpers.createHistory1());
+ }
+
+ @Override
+ public void testCleanMultipleRecords() {
+ cleanMultipleRecords(
+ HistoryHelpers.createHistory1(),
+ HistoryHelpers.createHistory2(),
+ HistoryHelpers.createHistory3(),
+ HistoryHelpers.createHistory4(),
+ HistoryHelpers.createHistory5()
+ );
+ }
+
+ @Override
+ public void testGuidsSinceReturnMultipleRecords() {
+ HistoryRecord record0 = HistoryHelpers.createHistory1();
+ HistoryRecord record1 = HistoryHelpers.createHistory2();
+ guidsSinceReturnMultipleRecords(record0, record1);
+ }
+
+ @Override
+ public void testGuidsSinceReturnNoRecords() {
+ guidsSinceReturnNoRecords(HistoryHelpers.createHistory3());
+ }
+
+ @Override
+ public void testFetchSinceOneRecord() {
+ fetchSinceOneRecord(HistoryHelpers.createHistory1(),
+ HistoryHelpers.createHistory2());
+ }
+
+ @Override
+ public void testFetchSinceReturnNoRecords() {
+ fetchSinceReturnNoRecords(HistoryHelpers.createHistory3());
+ }
+
+ @Override
+ public void testFetchOneRecordByGuid() {
+ fetchOneRecordByGuid(HistoryHelpers.createHistory1(),
+ HistoryHelpers.createHistory2());
+ }
+
+ @Override
+ public void testFetchMultipleRecordsByGuids() {
+ HistoryRecord record0 = HistoryHelpers.createHistory1();
+ HistoryRecord record1 = HistoryHelpers.createHistory2();
+ HistoryRecord record2 = HistoryHelpers.createHistory3();
+ fetchMultipleRecordsByGuids(record0, record1, record2);
+ }
+
+ @Override
+ public void testFetchNoRecordByGuid() {
+ fetchNoRecordByGuid(HistoryHelpers.createHistory1());
+ }
+
+ @Override
+ public void testWipe() {
+ doWipe(HistoryHelpers.createHistory2(), HistoryHelpers.createHistory3());
+ }
+
+ @Override
+ public void testStore() {
+ basicStoreTest(HistoryHelpers.createHistory1());
+ }
+
+ @Override
+ public void testRemoteNewerTimeStamp() {
+ HistoryRecord local = HistoryHelpers.createHistory1();
+ HistoryRecord remote = HistoryHelpers.createHistory2();
+ remoteNewerTimeStamp(local, remote);
+ }
+
+ @Override
+ public void testLocalNewerTimeStamp() {
+ HistoryRecord local = HistoryHelpers.createHistory1();
+ HistoryRecord remote = HistoryHelpers.createHistory2();
+ localNewerTimeStamp(local, remote);
+ }
+
+ @Override
+ public void testDeleteRemoteNewer() {
+ HistoryRecord local = HistoryHelpers.createHistory1();
+ HistoryRecord remote = HistoryHelpers.createHistory2();
+ deleteRemoteNewer(local, remote);
+ }
+
+ @Override
+ public void testDeleteLocalNewer() {
+ HistoryRecord local = HistoryHelpers.createHistory1();
+ HistoryRecord remote = HistoryHelpers.createHistory2();
+ deleteLocalNewer(local, remote);
+ }
+
+ @Override
+ public void testDeleteRemoteLocalNonexistent() {
+ deleteRemoteLocalNonexistent(HistoryHelpers.createHistory2());
+ }
+
+ /**
+ * Exists to provide access to record string logic.
+ */
+ protected class HelperHistorySession extends AndroidBrowserHistoryRepositorySession {
+ public HelperHistorySession(Repository repository, Context context) {
+ super(repository, context);
+ }
+
+ public boolean sameRecordString(HistoryRecord r1, HistoryRecord r2) {
+ return buildRecordString(r1).equals(buildRecordString(r2));
+ }
+ }
+
+ /**
+ * Verifies that two history records with the same URI but different
+ * titles will be reconciled locally.
+ */
+ public void testRecordStringCollisionAndEquality() {
+ final AndroidBrowserHistoryRepository repo = new AndroidBrowserHistoryRepository();
+ final HelperHistorySession testSession = new HelperHistorySession(repo, getApplicationContext());
+
+ final long now = RepositorySession.now();
+
+ final HistoryRecord record0 = new HistoryRecord(null, "history", now + 1, false);
+ final HistoryRecord record1 = new HistoryRecord(null, "history", now + 2, false);
+ final HistoryRecord record2 = new HistoryRecord(null, "history", now + 3, false);
+
+ record0.histURI = "http://example.com/foo";
+ record1.histURI = "http://example.com/foo";
+ record2.histURI = "http://example.com/bar";
+ record0.title = "Foo 0";
+ record1.title = "Foo 1";
+ record2.title = "Foo 2";
+
+ // Ensure that two records with the same URI produce the same record string,
+ // and two records with different URIs do not.
+ assertTrue(testSession.sameRecordString(record0, record1));
+ assertFalse(testSession.sameRecordString(record0, record2));
+
+ // Two records are congruent if they have the same URI and their
+ // identifiers match (which is why these all have null GUIDs).
+ assertTrue(record0.congruentWith(record0));
+ assertTrue(record0.congruentWith(record1));
+ assertTrue(record1.congruentWith(record0));
+ assertFalse(record0.congruentWith(record2));
+ assertFalse(record1.congruentWith(record2));
+ assertFalse(record2.congruentWith(record1));
+ assertFalse(record2.congruentWith(record0));
+
+ // None of these records are equal, because they have different titles.
+ // (Except for being equal to themselves, of course.)
+ assertTrue(record0.equalPayloads(record0));
+ assertTrue(record1.equalPayloads(record1));
+ assertTrue(record2.equalPayloads(record2));
+ assertFalse(record0.equalPayloads(record1));
+ assertFalse(record1.equalPayloads(record0));
+ assertFalse(record1.equalPayloads(record2));
+ }
+
+ /*
+ * Tests for adding some visits to a history record
+ * and doing a fetch.
+ */
+ @SuppressWarnings("unchecked")
+ public void testAddOneVisit() {
+ final RepositorySession session = createAndBeginSession();
+
+ HistoryRecord record0 = HistoryHelpers.createHistory3();
+ performWait(storeRunnable(session, record0));
+
+ // Add one visit to the count and put in a new
+ // last visited date.
+ ContentValues cv = new ContentValues();
+ int visits = record0.visits.size() + 1;
+ long newVisitTime = System.currentTimeMillis();
+ cv.put(BrowserContract.History.VISITS, visits);
+ cv.put(BrowserContract.History.DATE_LAST_VISITED, newVisitTime);
+ final AndroidBrowserRepositoryDataAccessor dataAccessor = getDataAccessor();
+ dataAccessor.updateByGuid(record0.guid, cv);
+
+ // Add expected visit to record for verification.
+ JSONObject expectedVisit = new JSONObject();
+ expectedVisit.put("date", newVisitTime * 1000); // Microseconds.
+ expectedVisit.put("type", 1L);
+ record0.visits.add(expectedVisit);
+
+ performWait(fetchRunnable(session, new String[] { record0.guid }, new ExpectFetchDelegate(new Record[] { record0 })));
+ closeDataAccessor(dataAccessor);
+ }
+
+ @SuppressWarnings("unchecked")
+ public void testAddMultipleVisits() {
+ final RepositorySession session = createAndBeginSession();
+
+ HistoryRecord record0 = HistoryHelpers.createHistory4();
+ performWait(storeRunnable(session, record0));
+
+ // Add three visits to the count and put in a new
+ // last visited date.
+ ContentValues cv = new ContentValues();
+ int visits = record0.visits.size() + 3;
+ long newVisitTime = System.currentTimeMillis();
+ cv.put(BrowserContract.History.VISITS, visits);
+ cv.put(BrowserContract.History.DATE_LAST_VISITED, newVisitTime);
+ final AndroidBrowserRepositoryDataAccessor dataAccessor = getDataAccessor();
+ dataAccessor.updateByGuid(record0.guid, cv);
+
+ // Now shift to microsecond timing for visits.
+ long newMicroVisitTime = newVisitTime * 1000;
+
+ // Add expected visits to record for verification
+ JSONObject expectedVisit = new JSONObject();
+ expectedVisit.put("date", newMicroVisitTime);
+ expectedVisit.put("type", 1L);
+ record0.visits.add(expectedVisit);
+ expectedVisit = new JSONObject();
+ expectedVisit.put("date", newMicroVisitTime - 1000);
+ expectedVisit.put("type", 1L);
+ record0.visits.add(expectedVisit);
+ expectedVisit = new JSONObject();
+ expectedVisit.put("date", newMicroVisitTime - 2000);
+ expectedVisit.put("type", 1L);
+ record0.visits.add(expectedVisit);
+
+ ExpectFetchDelegate delegate = new ExpectFetchDelegate(new Record[] { record0 });
+ performWait(fetchRunnable(session, new String[] { record0.guid }, delegate));
+
+ Record fetched = delegate.records.get(0);
+ assertTrue(record0.equalPayloads(fetched));
+ closeDataAccessor(dataAccessor);
+ }
+
+ public void testInvalidHistoryItemIsSkipped() throws NullCursorException {
+ final AndroidBrowserHistoryRepositorySession session = (AndroidBrowserHistoryRepositorySession) createAndBeginSession();
+ final AndroidBrowserRepositoryDataAccessor dbHelper = session.getDBHelper();
+
+ final long now = System.currentTimeMillis();
+ final HistoryRecord emptyURL = new HistoryRecord(Utils.generateGuid(), "history", now, false);
+ final HistoryRecord noVisits = new HistoryRecord(Utils.generateGuid(), "history", now, false);
+ final HistoryRecord aboutURL = new HistoryRecord(Utils.generateGuid(), "history", now, false);
+
+ emptyURL.fennecDateVisited = now;
+ emptyURL.fennecVisitCount = 1;
+ emptyURL.histURI = "";
+ emptyURL.title = "Something";
+
+ noVisits.fennecDateVisited = now;
+ noVisits.fennecVisitCount = 0;
+ noVisits.histURI = "http://example.org/novisits";
+ noVisits.title = "Something Else";
+
+ aboutURL.fennecDateVisited = now;
+ aboutURL.fennecVisitCount = 1;
+ aboutURL.histURI = "about:home";
+ aboutURL.title = "Fennec Home";
+
+ Uri one = dbHelper.insert(emptyURL);
+ Uri two = dbHelper.insert(noVisits);
+ Uri tre = dbHelper.insert(aboutURL);
+ assertNotNull(one);
+ assertNotNull(two);
+ assertNotNull(tre);
+
+ // The records are in the DB.
+ final Cursor all = dbHelper.fetchAll();
+ assertEquals(3, all.getCount());
+ all.close();
+
+ // But aren't returned by fetching.
+ performWait(fetchAllRunnable(session, new Record[] {}));
+
+ // And we'd ignore about:home if we downloaded it.
+ assertTrue(session.shouldIgnore(aboutURL));
+
+ session.abort();
+ }
+
+ public void testSqlInjectPurgeDelete() {
+ // Some setup.
+ RepositorySession session = createAndBeginSession();
+ final AndroidBrowserRepositoryDataAccessor db = getDataAccessor();
+
+ try {
+ ContentValues cv = new ContentValues();
+ cv.put(BrowserContract.SyncColumns.IS_DELETED, 1);
+
+ // Create and insert 2 history entries, 2nd one is evil (attempts injection).
+ HistoryRecord h1 = HistoryHelpers.createHistory1();
+ HistoryRecord h2 = HistoryHelpers.createHistory2();
+ h2.guid = "' or '1'='1";
+
+ db.insert(h1);
+ db.insert(h2);
+
+ // Test 1 - updateByGuid() handles evil history entries correctly.
+ db.updateByGuid(h2.guid, cv);
+
+ // Query history table.
+ Cursor cur = getAllHistory();
+ int numHistory = cur.getCount();
+
+ // Ensure only the evil history entry is marked for deletion.
+ try {
+ cur.moveToFirst();
+ while (!cur.isAfterLast()) {
+ String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
+ boolean deleted = RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1;
+
+ if (guid.equals(h2.guid)) {
+ assertTrue(deleted);
+ } else {
+ assertFalse(deleted);
+ }
+ cur.moveToNext();
+ }
+ } finally {
+ cur.close();
+ }
+
+ // Test 2 - Ensure purgeDelete()'s call to delete() deletes only 1 record.
+ try {
+ db.purgeDeleted();
+ } catch (NullCursorException e) {
+ e.printStackTrace();
+ }
+
+ cur = getAllHistory();
+ int numHistoryAfterDeletion = cur.getCount();
+
+ // Ensure we have only 1 deleted row.
+ assertEquals(numHistoryAfterDeletion, numHistory - 1);
+
+ // Ensure only the evil history is deleted.
+ try {
+ cur.moveToFirst();
+ while (!cur.isAfterLast()) {
+ String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
+ boolean deleted = RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1;
+
+ if (guid.equals(h2.guid)) {
+ fail("Evil guid was not deleted!");
+ } else {
+ assertFalse(deleted);
+ }
+ cur.moveToNext();
+ }
+ } finally {
+ cur.close();
+ }
+ } finally {
+ closeDataAccessor(db);
+ session.abort();
+ }
+ }
+
+ protected Cursor getAllHistory() {
+ Context context = getApplicationContext();
+ Cursor cur = context.getContentResolver().query(BrowserContractHelpers.HISTORY_CONTENT_URI,
+ BrowserContractHelpers.HistoryColumns, null, null, null);
+ return cur;
+ }
+
+ public void testDataAccessorBulkInsert() throws NullCursorException {
+ final AndroidBrowserHistoryRepositorySession session = (AndroidBrowserHistoryRepositorySession) createAndBeginSession();
+ AndroidBrowserHistoryDataAccessor db = (AndroidBrowserHistoryDataAccessor) session.getDBHelper();
+
+ ArrayList<HistoryRecord> records = new ArrayList<HistoryRecord>();
+ records.add(HistoryHelpers.createHistory1());
+ records.add(HistoryHelpers.createHistory2());
+ records.add(HistoryHelpers.createHistory3());
+ db.bulkInsert(records);
+
+ performWait(fetchAllRunnable(session, preparedExpectFetchDelegate(records.toArray(new Record[records.size()]))));
+ session.abort();
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestBookmarks.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestBookmarks.java
new file mode 100644
index 0000000000..783aea1ff8
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestBookmarks.java
@@ -0,0 +1,1063 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.db;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+
+import org.json.simple.JSONArray;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
+import org.mozilla.gecko.background.sync.helpers.BookmarkHelpers;
+import org.mozilla.gecko.background.sync.helpers.SimpleSuccessBeginDelegate;
+import org.mozilla.gecko.background.sync.helpers.SimpleSuccessCreationDelegate;
+import org.mozilla.gecko.background.sync.helpers.SimpleSuccessFetchDelegate;
+import org.mozilla.gecko.background.sync.helpers.SimpleSuccessFinishDelegate;
+import org.mozilla.gecko.background.sync.helpers.SimpleSuccessStoreDelegate;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserContract.Bookmarks;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.repositories.InactiveSessionException;
+import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
+import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
+import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksDataAccessor;
+import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksRepository;
+import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksRepositorySession;
+import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class TestBookmarks extends AndroidSyncTestCase {
+
+ protected static final String LOG_TAG = "BookmarksTest";
+
+ /**
+ * Trivial test that forbidden records such as pinned items
+ * will be ignored if processed.
+ */
+ public void testForbiddenItemsAreIgnored() {
+ final AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository();
+ final long now = System.currentTimeMillis();
+ final String bookmarksCollection = "bookmarks";
+
+ final BookmarkRecord pinned = new BookmarkRecord("pinpinpinpin", "bookmarks", now - 1, false);
+ final BookmarkRecord normal = new BookmarkRecord("baaaaaaaaaaa", "bookmarks", now - 2, false);
+
+ final BookmarkRecord pinnedItems = new BookmarkRecord(Bookmarks.PINNED_FOLDER_GUID,
+ bookmarksCollection, now - 4, false);
+
+ normal.type = "bookmark";
+ pinned.type = "bookmark";
+ pinnedItems.type = "folder";
+
+ pinned.parentID = Bookmarks.PINNED_FOLDER_GUID;
+ normal.parentID = Bookmarks.TOOLBAR_FOLDER_GUID;
+
+ pinnedItems.parentID = Bookmarks.PLACES_FOLDER_GUID;
+
+ inBegunSession(repo, new SimpleSuccessBeginDelegate() {
+ @Override
+ public void onBeginSucceeded(RepositorySession session) {
+ assertTrue(((AndroidBrowserBookmarksRepositorySession) session).shouldIgnore(pinned));
+ assertTrue(((AndroidBrowserBookmarksRepositorySession) session).shouldIgnore(pinnedItems));
+ assertFalse(((AndroidBrowserBookmarksRepositorySession) session).shouldIgnore(normal));
+ finishAndNotify(session);
+ }
+ });
+ }
+
+ /**
+ * Trivial test that pinned items will be skipped if present in the DB.
+ */
+ public void testPinnedItemsAreNotRetrieved() {
+ final AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository();
+
+ // Ensure that they exist.
+ setUpFennecPinnedItemsRecord();
+
+ // They're there in the DB…
+ final ArrayList<String> roots = fetchChildrenDirect(Bookmarks.FIXED_ROOT_ID);
+ Logger.info(LOG_TAG, "Roots: " + roots);
+ assertTrue(roots.contains(Bookmarks.PINNED_FOLDER_GUID));
+
+ final ArrayList<String> pinned = fetchChildrenDirect(Bookmarks.FIXED_PINNED_LIST_ID);
+ Logger.info(LOG_TAG, "Pinned: " + pinned);
+ assertTrue(pinned.contains("dapinneditem"));
+
+ // … but not when we fetch.
+ final ArrayList<String> guids = fetchGUIDs(repo);
+ assertFalse(guids.contains(Bookmarks.PINNED_FOLDER_GUID));
+ assertFalse(guids.contains("dapinneditem"));
+ }
+
+ public void testRetrieveFolderHasAccurateChildren() {
+ AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository();
+
+ final long now = System.currentTimeMillis();
+
+ final String folderGUID = "eaaaaaaaafff";
+ BookmarkRecord folder = new BookmarkRecord(folderGUID, "bookmarks", now - 5, false);
+ BookmarkRecord bookmarkA = new BookmarkRecord("daaaaaaaaaaa", "bookmarks", now - 1, false);
+ BookmarkRecord bookmarkB = new BookmarkRecord("baaaaaaaabbb", "bookmarks", now - 3, false);
+ BookmarkRecord bookmarkC = new BookmarkRecord("aaaaaaaaaccc", "bookmarks", now - 2, false);
+
+ folder.children = childrenFromRecords(bookmarkA, bookmarkB, bookmarkC);
+ folder.sortIndex = 150;
+ folder.title = "Test items";
+ folder.parentID = "toolbar";
+ folder.parentName = "Bookmarks Toolbar";
+ folder.type = "folder";
+
+ bookmarkA.parentID = folderGUID;
+ bookmarkA.bookmarkURI = "http://example.com/A";
+ bookmarkA.title = "Title A";
+ bookmarkA.type = "bookmark";
+
+ bookmarkB.parentID = folderGUID;
+ bookmarkB.bookmarkURI = "http://example.com/B";
+ bookmarkB.title = "Title B";
+ bookmarkB.type = "bookmark";
+
+ bookmarkC.parentID = folderGUID;
+ bookmarkC.bookmarkURI = "http://example.com/C";
+ bookmarkC.title = "Title C";
+ bookmarkC.type = "bookmark";
+
+ BookmarkRecord[] folderOnly = new BookmarkRecord[1];
+ BookmarkRecord[] children = new BookmarkRecord[3];
+
+ folderOnly[0] = folder;
+
+ children[0] = bookmarkA;
+ children[1] = bookmarkB;
+ children[2] = bookmarkC;
+
+ wipe();
+ Logger.debug(getName(), "Storing just folder...");
+ storeRecordsInSession(repo, folderOnly, null);
+
+ // We don't have any children, despite our insistence upon storing.
+ assertChildrenAreOrdered(repo, folderGUID, new Record[] {});
+
+ // Now store the children.
+ Logger.debug(getName(), "Storing children...");
+ storeRecordsInSession(repo, children, null);
+
+ // Now we have children, but their order is not determined, because
+ // they were stored out-of-session with the original folder.
+ assertChildrenAreUnordered(repo, folderGUID, children);
+
+ // Now if we store the folder record again, they'll be put in the
+ // right place.
+ folder.lastModified++;
+ Logger.debug(getName(), "Storing just folder again...");
+ storeRecordsInSession(repo, folderOnly, null);
+ Logger.debug(getName(), "Fetching children yet again...");
+ assertChildrenAreOrdered(repo, folderGUID, children);
+
+ // Now let's see what happens when we see records in the same session.
+ BookmarkRecord[] parentMixed = new BookmarkRecord[4];
+ BookmarkRecord[] parentFirst = new BookmarkRecord[4];
+ BookmarkRecord[] parentLast = new BookmarkRecord[4];
+
+ // None of our records have a position set.
+ assertTrue(bookmarkA.androidPosition <= 0);
+ assertTrue(bookmarkB.androidPosition <= 0);
+ assertTrue(bookmarkC.androidPosition <= 0);
+
+ parentMixed[1] = folder;
+ parentMixed[0] = bookmarkA;
+ parentMixed[2] = bookmarkC;
+ parentMixed[3] = bookmarkB;
+
+ parentFirst[0] = folder;
+ parentFirst[1] = bookmarkC;
+ parentFirst[2] = bookmarkA;
+ parentFirst[3] = bookmarkB;
+
+ parentLast[3] = folder;
+ parentLast[0] = bookmarkB;
+ parentLast[1] = bookmarkA;
+ parentLast[2] = bookmarkC;
+
+ wipe();
+ storeRecordsInSession(repo, parentMixed, null);
+ assertChildrenAreOrdered(repo, folderGUID, children);
+
+ wipe();
+ storeRecordsInSession(repo, parentFirst, null);
+ assertChildrenAreOrdered(repo, folderGUID, children);
+
+ wipe();
+ storeRecordsInSession(repo, parentLast, null);
+ assertChildrenAreOrdered(repo, folderGUID, children);
+
+ // Ensure that records are ordered even if we re-process the folder.
+ wipe();
+ storeRecordsInSession(repo, parentLast, null);
+ folder.lastModified++;
+ storeRecordsInSession(repo, folderOnly, null);
+ assertChildrenAreOrdered(repo, folderGUID, children);
+ }
+
+ public void testMergeFoldersPreservesSaneOrder() {
+ AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository();
+
+ final long now = System.currentTimeMillis();
+ final String folderGUID = "mobile";
+
+ wipe();
+ final long mobile = setUpFennecMobileRecord();
+
+ // No children.
+ assertChildrenAreUnordered(repo, folderGUID, new Record[] {});
+
+ // Add some, as Fennec would.
+ fennecAddBookmark("Bookmark One", "http://example.com/fennec/One");
+ fennecAddBookmark("Bookmark Two", "http://example.com/fennec/Two");
+
+ Logger.debug(getName(), "Fetching children...");
+ JSONArray folderChildren = fetchChildrenForGUID(repo, folderGUID);
+
+ assertTrue(folderChildren != null);
+ Logger.debug(getName(), "Children are " + folderChildren.toJSONString());
+ assertEquals(2, folderChildren.size());
+ String guidOne = (String) folderChildren.get(0);
+ String guidTwo = (String) folderChildren.get(1);
+
+ // Make sure positions were saved.
+ assertChildrenAreDirect(mobile, new String[] {
+ guidOne,
+ guidTwo
+ });
+
+ // Add some through Sync.
+ BookmarkRecord folder = new BookmarkRecord(folderGUID, "bookmarks", now, false);
+ BookmarkRecord bookmarkA = new BookmarkRecord("daaaaaaaaaaa", "bookmarks", now, false);
+ BookmarkRecord bookmarkB = new BookmarkRecord("baaaaaaaabbb", "bookmarks", now, false);
+
+ folder.children = childrenFromRecords(bookmarkA, bookmarkB);
+ folder.sortIndex = 150;
+ folder.title = "Mobile Bookmarks";
+ folder.parentID = "places";
+ folder.parentName = "";
+ folder.type = "folder";
+
+ bookmarkA.parentID = folderGUID;
+ bookmarkA.parentName = "Mobile Bookmarks"; // Using this title exercises Bug 748898.
+ bookmarkA.bookmarkURI = "http://example.com/A";
+ bookmarkA.title = "Title A";
+ bookmarkA.type = "bookmark";
+
+ bookmarkB.parentID = folderGUID;
+ bookmarkB.parentName = "mobile";
+ bookmarkB.bookmarkURI = "http://example.com/B";
+ bookmarkB.title = "Title B";
+ bookmarkB.type = "bookmark";
+
+ BookmarkRecord[] parentMixed = new BookmarkRecord[3];
+ parentMixed[0] = bookmarkA;
+ parentMixed[1] = folder;
+ parentMixed[2] = bookmarkB;
+
+ storeRecordsInSession(repo, parentMixed, null);
+
+ BookmarkRecord expectedOne = new BookmarkRecord(guidOne, "bookmarks", now - 10, false);
+ BookmarkRecord expectedTwo = new BookmarkRecord(guidTwo, "bookmarks", now - 10, false);
+
+ // We want the server to win in this case, and otherwise to preserve order.
+ // TODO
+ assertChildrenAreOrdered(repo, folderGUID, new Record[] {
+ bookmarkA,
+ bookmarkB,
+ expectedOne,
+ expectedTwo
+ });
+
+ // Furthermore, the children of that folder should be correct in the DB.
+ ContentResolver cr = getApplicationContext().getContentResolver();
+ final long folderId = fennecGetFolderId(cr, folderGUID);
+ Logger.debug(getName(), "Folder " + folderGUID + " => " + folderId);
+
+ assertChildrenAreDirect(folderId, new String[] {
+ bookmarkA.guid,
+ bookmarkB.guid,
+ expectedOne.guid,
+ expectedTwo.guid
+ });
+ }
+
+ /**
+ * Apply a folder record whose children array is already accurately
+ * stored in the database. Verify that the parent folder is not flagged
+ * for reupload (i.e., that its modified time is *ahem* unmodified).
+ */
+ public void testNoReorderingMeansNoReupload() {
+ AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository();
+
+ final long now = System.currentTimeMillis();
+
+ final String folderGUID = "eaaaaaaaafff";
+ BookmarkRecord folder = new BookmarkRecord(folderGUID, "bookmarks", now -5, false);
+ BookmarkRecord bookmarkA = new BookmarkRecord("daaaaaaaaaaa", "bookmarks", now -1, false);
+ BookmarkRecord bookmarkB = new BookmarkRecord("baaaaaaaabbb", "bookmarks", now -3, false);
+
+ folder.children = childrenFromRecords(bookmarkA, bookmarkB);
+ folder.sortIndex = 150;
+ folder.title = "Test items";
+ folder.parentID = "toolbar";
+ folder.parentName = "Bookmarks Toolbar";
+ folder.type = "folder";
+
+ bookmarkA.parentID = folderGUID;
+ bookmarkA.bookmarkURI = "http://example.com/A";
+ bookmarkA.title = "Title A";
+ bookmarkA.type = "bookmark";
+
+ bookmarkB.parentID = folderGUID;
+ bookmarkB.bookmarkURI = "http://example.com/B";
+ bookmarkB.title = "Title B";
+ bookmarkB.type = "bookmark";
+
+ BookmarkRecord[] abf = new BookmarkRecord[3];
+ BookmarkRecord[] justFolder = new BookmarkRecord[1];
+
+ abf[0] = bookmarkA;
+ abf[1] = bookmarkB;
+ abf[2] = folder;
+
+ justFolder[0] = folder;
+
+ final String[] abGUIDs = new String[] { bookmarkA.guid, bookmarkB.guid };
+ final Record[] abRecords = new Record[] { bookmarkA, bookmarkB };
+ final String[] baGUIDs = new String[] { bookmarkB.guid, bookmarkA.guid };
+ final Record[] baRecords = new Record[] { bookmarkB, bookmarkA };
+
+ wipe();
+ Logger.debug(getName(), "Storing A, B, folder...");
+ storeRecordsInSession(repo, abf, null);
+
+ ContentResolver cr = getApplicationContext().getContentResolver();
+ final long folderID = fennecGetFolderId(cr, folderGUID);
+ assertChildrenAreOrdered(repo, folderGUID, abRecords);
+ assertChildrenAreDirect(folderID, abGUIDs);
+
+ // To ensure an interval.
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ }
+
+ // Store the same folder record again, and check the tracking.
+ // Because the folder array didn't change,
+ // the item is still tracked to not be uploaded.
+ folder.lastModified = System.currentTimeMillis() + 1;
+ HashSet<String> tracked = new HashSet<String>();
+ storeRecordsInSession(repo, justFolder, tracked);
+ assertChildrenAreOrdered(repo, folderGUID, abRecords);
+ assertChildrenAreDirect(folderID, abGUIDs);
+
+ assertTrue(tracked.contains(folderGUID));
+
+ // Store again, but with a different order.
+ tracked = new HashSet<String>();
+ folder.children = childrenFromRecords(bookmarkB, bookmarkA);
+ folder.lastModified = System.currentTimeMillis() + 1;
+ storeRecordsInSession(repo, justFolder, tracked);
+ assertChildrenAreOrdered(repo, folderGUID, baRecords);
+ assertChildrenAreDirect(folderID, baGUIDs);
+
+ // Now it's going to be reuploaded.
+ assertFalse(tracked.contains(folderGUID));
+ }
+
+ /**
+ * Exercise the deletion of folders when their children have not been
+ * marked as deleted. In a database with constraints, this would fail
+ * if we simply deleted the records, so we move them first.
+ */
+ public void testFolderDeletionOrphansChildren() {
+ AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository();
+
+ long now = System.currentTimeMillis();
+
+ // Add a folder and four children.
+ final String folderGUID = "eaaaaaaaafff";
+ BookmarkRecord folder = new BookmarkRecord(folderGUID, "bookmarks", now -5, false);
+ BookmarkRecord bookmarkA = new BookmarkRecord("daaaaaaaaaaa", "bookmarks", now -1, false);
+ BookmarkRecord bookmarkB = new BookmarkRecord("baaaaaaaabbb", "bookmarks", now -3, false);
+ BookmarkRecord bookmarkC = new BookmarkRecord("daaaaaaaaccc", "bookmarks", now -7, false);
+ BookmarkRecord bookmarkD = new BookmarkRecord("baaaaaaaaddd", "bookmarks", now -4, false);
+
+ folder.children = childrenFromRecords(bookmarkA, bookmarkB, bookmarkC, bookmarkD);
+ folder.sortIndex = 150;
+ folder.title = "Test items";
+ folder.parentID = "toolbar";
+ folder.parentName = "Bookmarks Toolbar";
+ folder.type = "folder";
+
+ bookmarkA.parentID = folderGUID;
+ bookmarkA.bookmarkURI = "http://example.com/A";
+ bookmarkA.title = "Title A";
+ bookmarkA.type = "bookmark";
+
+ bookmarkB.parentID = folderGUID;
+ bookmarkB.bookmarkURI = "http://example.com/B";
+ bookmarkB.title = "Title B";
+ bookmarkB.type = "bookmark";
+
+ bookmarkC.parentID = folderGUID;
+ bookmarkC.bookmarkURI = "http://example.com/C";
+ bookmarkC.title = "Title C";
+ bookmarkC.type = "bookmark";
+
+ bookmarkD.parentID = folderGUID;
+ bookmarkD.bookmarkURI = "http://example.com/D";
+ bookmarkD.title = "Title D";
+ bookmarkD.type = "bookmark";
+
+ BookmarkRecord[] abfcd = new BookmarkRecord[5];
+ BookmarkRecord[] justFolder = new BookmarkRecord[1];
+ abfcd[0] = bookmarkA;
+ abfcd[1] = bookmarkB;
+ abfcd[2] = folder;
+ abfcd[3] = bookmarkC;
+ abfcd[4] = bookmarkD;
+
+ justFolder[0] = folder;
+
+ final String[] abcdGUIDs = new String[] { bookmarkA.guid, bookmarkB.guid, bookmarkC.guid, bookmarkD.guid };
+ final Record[] abcdRecords = new Record[] { bookmarkA, bookmarkB, bookmarkC, bookmarkD };
+
+ wipe();
+ Logger.debug(getName(), "Storing A, B, folder, C, D...");
+ storeRecordsInSession(repo, abfcd, null);
+
+ // Verify that it worked.
+ ContentResolver cr = getApplicationContext().getContentResolver();
+ final long folderID = fennecGetFolderId(cr, folderGUID);
+ assertChildrenAreOrdered(repo, folderGUID, abcdRecords);
+ assertChildrenAreDirect(folderID, abcdGUIDs);
+
+ now = System.currentTimeMillis();
+
+ // Add one child to unsorted bookmarks.
+ BookmarkRecord unsortedA = new BookmarkRecord("yiamunsorted", "bookmarks", now, false);
+ unsortedA.parentID = "unfiled";
+ unsortedA.title = "Unsorted A";
+ unsortedA.type = "bookmark";
+ unsortedA.androidPosition = 0;
+
+ BookmarkRecord[] ua = new BookmarkRecord[1];
+ ua[0] = unsortedA;
+
+ storeRecordsInSession(repo, ua, null);
+
+ // Ensure that the database is in this state.
+ assertChildrenAreOrdered(repo, "unfiled", ua);
+
+ // Delete the second child, the folder, and then the third child.
+ bookmarkB.bookmarkURI = bookmarkC.bookmarkURI = folder.bookmarkURI = null;
+ bookmarkB.deleted = bookmarkC.deleted = folder.deleted = true;
+ bookmarkB.title = bookmarkC.title = folder.title = null;
+
+ // Nulling the type of folder is very important: it verifies
+ // that the session can behave correctly according to local type.
+ bookmarkB.type = bookmarkC.type = folder.type = null;
+
+ bookmarkB.lastModified = bookmarkC.lastModified = folder.lastModified = now = System.currentTimeMillis();
+
+ BookmarkRecord[] deletions = new BookmarkRecord[] { bookmarkB, folder, bookmarkC };
+ storeRecordsInSession(repo, deletions, null);
+
+ // Verify that the unsorted bookmarks folder contains its child and the
+ // first and fourth children of the now-deleted folder.
+ // Also verify that the folder is gone.
+ long unsortedID = fennecGetFolderId(cr, "unfiled");
+ long toolbarID = fennecGetFolderId(cr, "toolbar");
+ String[] expected = new String[] { unsortedA.guid, bookmarkA.guid, bookmarkD.guid };
+
+ // This will trigger positioning.
+ assertChildrenAreUnordered(repo, "unfiled", new Record[] { unsortedA, bookmarkA, bookmarkD });
+ assertChildrenAreDirect(unsortedID, expected);
+ assertChildrenAreDirect(toolbarID, new String[] {});
+ }
+
+ /**
+ * A test where we expect to replace a local folder with a new folder (with a
+ * new GUID), whilst adding children to it. Verifies that replace and insert
+ * co-operate.
+ */
+ public void testInsertAndReplaceGuid() {
+ AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository();
+ wipe();
+
+ BookmarkRecord folder1 = BookmarkHelpers.createFolder1();
+ BookmarkRecord folder2 = BookmarkHelpers.createFolder2(); // child of folder1
+ BookmarkRecord folder3 = BookmarkHelpers.createFolder3(); // child of folder2
+ BookmarkRecord bmk1 = BookmarkHelpers.createBookmark1(); // child of folder1
+ BookmarkRecord bmk2 = BookmarkHelpers.createBookmark2(); // child of folder1
+ BookmarkRecord bmk3 = BookmarkHelpers.createBookmark3(); // child of folder2
+ BookmarkRecord bmk4 = BookmarkHelpers.createBookmark4(); // child of folder3
+
+ BookmarkRecord[] records = new BookmarkRecord[] {
+ folder1, folder2, folder3,
+ bmk1, bmk4
+ };
+ storeRecordsInSession(repo, records, null);
+
+ assertChildrenAreUnordered(repo, folder1.guid, new Record[] { bmk1, folder2 });
+ assertChildrenAreUnordered(repo, folder2.guid, new Record[] { folder3 });
+ assertChildrenAreUnordered(repo, folder3.guid, new Record[] { bmk4 });
+
+ // Replace folder3 with a record with a new GUID, and add bmk4 as folder3's child.
+ final long now = System.currentTimeMillis();
+ folder3.guid = Utils.generateGuid();
+ folder3.lastModified = now;
+ bmk4.title = bmk4.title + "/NEW";
+ bmk4.parentID = folder3.guid; // Incoming child knows its parent.
+ bmk4.parentName = folder3.title;
+ bmk4.lastModified = now;
+
+ // Order of store should not matter.
+ ArrayList<BookmarkRecord> changedRecords = new ArrayList<BookmarkRecord>();
+ changedRecords.add(bmk2); changedRecords.add(bmk3); changedRecords.add(bmk4); changedRecords.add(folder3);
+ Collections.shuffle(changedRecords);
+ storeRecordsInSession(repo, changedRecords.toArray(new BookmarkRecord[changedRecords.size()]), null);
+
+ assertChildrenAreUnordered(repo, folder1.guid, new Record[] { bmk1, bmk2, folder2 });
+ assertChildrenAreUnordered(repo, folder2.guid, new Record[] { bmk3, folder3 });
+ assertChildrenAreUnordered(repo, folder3.guid, new Record[] { bmk4 });
+
+ assertNotNull(fetchGUID(repo, folder3.guid));
+ assertEquals(bmk4.title, fetchGUID(repo, bmk4.guid).title);
+ }
+
+ /**
+ * A test where we expect to replace a local folder with a new folder (with a
+ * new title but the same GUID), whilst adding children to it. Verifies that
+ * replace and insert co-operate.
+ */
+ public void testInsertAndReplaceTitle() {
+ AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository();
+ wipe();
+
+ BookmarkRecord folder1 = BookmarkHelpers.createFolder1();
+ BookmarkRecord folder2 = BookmarkHelpers.createFolder2(); // child of folder1
+ BookmarkRecord folder3 = BookmarkHelpers.createFolder3(); // child of folder2
+ BookmarkRecord bmk1 = BookmarkHelpers.createBookmark1(); // child of folder1
+ BookmarkRecord bmk2 = BookmarkHelpers.createBookmark2(); // child of folder1
+ BookmarkRecord bmk3 = BookmarkHelpers.createBookmark3(); // child of folder2
+ BookmarkRecord bmk4 = BookmarkHelpers.createBookmark4(); // child of folder3
+
+ BookmarkRecord[] records = new BookmarkRecord[] {
+ folder1, folder2, folder3,
+ bmk1, bmk4
+ };
+ storeRecordsInSession(repo, records, null);
+
+ assertChildrenAreUnordered(repo, folder1.guid, new Record[] { bmk1, folder2 });
+ assertChildrenAreUnordered(repo, folder2.guid, new Record[] { folder3 });
+ assertChildrenAreUnordered(repo, folder3.guid, new Record[] { bmk4 });
+
+ // Rename folder1, and add bmk2 as folder1's child.
+ final long now = System.currentTimeMillis();
+ folder1.title = folder1.title + "/NEW";
+ folder1.lastModified = now;
+ bmk2.title = bmk2.title + "/NEW";
+ bmk2.parentID = folder1.guid; // Incoming child knows its parent.
+ bmk2.parentName = folder1.title;
+ bmk2.lastModified = now;
+
+ // Order of store should not matter.
+ ArrayList<BookmarkRecord> changedRecords = new ArrayList<BookmarkRecord>();
+ changedRecords.add(bmk2); changedRecords.add(bmk3); changedRecords.add(folder1);
+ Collections.shuffle(changedRecords);
+ storeRecordsInSession(repo, changedRecords.toArray(new BookmarkRecord[changedRecords.size()]), null);
+
+ assertChildrenAreUnordered(repo, folder1.guid, new Record[] { bmk1, bmk2, folder2 });
+ assertChildrenAreUnordered(repo, folder2.guid, new Record[] { bmk3, folder3 });
+ assertChildrenAreUnordered(repo, folder3.guid, new Record[] { bmk4 });
+
+ assertEquals(folder1.title, fetchGUID(repo, folder1.guid).title);
+ assertEquals(bmk2.title, fetchGUID(repo, bmk2.guid).title);
+ }
+
+ /**
+ * Create and begin a new session, handing control to the delegate when started.
+ * Returns when the delegate has notified.
+ */
+ public void inBegunSession(final AndroidBrowserBookmarksRepository repo,
+ final RepositorySessionBeginDelegate beginDelegate) {
+ Runnable go = new Runnable() {
+ @Override
+ public void run() {
+ RepositorySessionCreationDelegate delegate = new SimpleSuccessCreationDelegate() {
+ @Override
+ public void onSessionCreated(final RepositorySession session) {
+ try {
+ session.begin(beginDelegate);
+ } catch (InvalidSessionTransitionException e) {
+ performNotify(e);
+ }
+ }
+ };
+ repo.createSession(delegate, getApplicationContext());
+ }
+ };
+ performWait(go);
+ }
+
+ /**
+ * Finish the provided session, notifying on success.
+ *
+ * @param session
+ */
+ public void finishAndNotify(final RepositorySession session) {
+ try {
+ session.finish(new SimpleSuccessFinishDelegate() {
+ @Override
+ public void onFinishSucceeded(RepositorySession session,
+ RepositorySessionBundle bundle) {
+ performNotify();
+ }
+ });
+ } catch (InactiveSessionException e) {
+ performNotify(e);
+ }
+ }
+
+ /**
+ * Simple helper class for fetching all records.
+ * The fetched records' GUIDs are stored in `fetchedGUIDs`.
+ */
+ public class SimpleFetchAllBeginDelegate extends SimpleSuccessBeginDelegate {
+ public final ArrayList<String> fetchedGUIDs = new ArrayList<String>();
+
+ @Override
+ public void onBeginSucceeded(final RepositorySession session) {
+ RepositorySessionFetchRecordsDelegate fetchDelegate = new SimpleSuccessFetchDelegate() {
+
+ @Override
+ public void onFetchedRecord(Record record) {
+ fetchedGUIDs.add(record.guid);
+ }
+
+ @Override
+ public void onFetchCompleted(long end) {
+ finishAndNotify(session);
+ }
+ };
+ session.fetchSince(0, fetchDelegate);
+ }
+ }
+
+ /**
+ * Simple helper class for fetching a single record by GUID.
+ * The fetched record is stored in `fetchedRecord`.
+ */
+ public class SimpleFetchOneBeginDelegate extends SimpleSuccessBeginDelegate {
+ public final String guid;
+ public Record fetchedRecord = null;
+
+ public SimpleFetchOneBeginDelegate(String guid) {
+ this.guid = guid;
+ }
+
+ @Override
+ public void onBeginSucceeded(final RepositorySession session) {
+ RepositorySessionFetchRecordsDelegate fetchDelegate = new SimpleSuccessFetchDelegate() {
+
+ @Override
+ public void onFetchedRecord(Record record) {
+ fetchedRecord = record;
+ }
+
+ @Override
+ public void onFetchCompleted(long end) {
+ finishAndNotify(session);
+ }
+ };
+ try {
+ session.fetch(new String[] { guid }, fetchDelegate);
+ } catch (InactiveSessionException e) {
+ performNotify("Session is inactive.", e);
+ }
+ }
+ }
+
+ /**
+ * Create a new session for the given repository, storing each record
+ * from the provided array. Notifies on failure or success.
+ *
+ * Optionally populates a provided Collection with tracked items.
+ * @param repo
+ * @param records
+ * @param tracked
+ */
+ public void storeRecordsInSession(AndroidBrowserBookmarksRepository repo,
+ final BookmarkRecord[] records,
+ final Collection<String> tracked) {
+ SimpleSuccessBeginDelegate beginDelegate = new SimpleSuccessBeginDelegate() {
+ @Override
+ public void onBeginSucceeded(final RepositorySession session) {
+ RepositorySessionStoreDelegate storeDelegate = new SimpleSuccessStoreDelegate() {
+
+ @Override
+ public void onStoreCompleted(final long storeEnd) {
+ // Pass back whatever we tracked.
+ if (tracked != null) {
+ Iterator<String> iter = session.getTrackedRecordIDs();
+ while (iter.hasNext()) {
+ tracked.add(iter.next());
+ }
+ }
+ finishAndNotify(session);
+ }
+
+ @Override
+ public void onRecordStoreSucceeded(String guid) {
+ }
+ };
+ session.setStoreDelegate(storeDelegate);
+ for (BookmarkRecord record : records) {
+ try {
+ session.store(record);
+ } catch (NoStoreDelegateException e) {
+ // Never happens.
+ }
+ }
+ session.storeDone();
+ }
+ };
+ inBegunSession(repo, beginDelegate);
+ }
+
+ public ArrayList<String> fetchGUIDs(AndroidBrowserBookmarksRepository repo) {
+ SimpleFetchAllBeginDelegate beginDelegate = new SimpleFetchAllBeginDelegate();
+ inBegunSession(repo, beginDelegate);
+ return beginDelegate.fetchedGUIDs;
+ }
+
+ public BookmarkRecord fetchGUID(AndroidBrowserBookmarksRepository repo,
+ final String guid) {
+ Logger.info(LOG_TAG, "Fetching for " + guid);
+ SimpleFetchOneBeginDelegate beginDelegate = new SimpleFetchOneBeginDelegate(guid);
+ inBegunSession(repo, beginDelegate);
+ Logger.info(LOG_TAG, "Fetched " + beginDelegate.fetchedRecord);
+ assertTrue(beginDelegate.fetchedRecord != null);
+ return (BookmarkRecord) beginDelegate.fetchedRecord;
+ }
+
+ public JSONArray fetchChildrenForGUID(AndroidBrowserBookmarksRepository repo,
+ final String guid) {
+ return fetchGUID(repo, guid).children;
+ }
+
+ @SuppressWarnings("unchecked")
+ protected static JSONArray childrenFromRecords(BookmarkRecord... records) {
+ JSONArray children = new JSONArray();
+ for (BookmarkRecord record : records) {
+ children.add(record.guid);
+ }
+ return children;
+ }
+
+
+ protected void updateRow(ContentValues values) {
+ Uri uri = BrowserContractHelpers.BOOKMARKS_CONTENT_URI;
+ final String where = BrowserContract.Bookmarks.GUID + " = ?";
+ final String[] args = new String[] { values.getAsString(BrowserContract.Bookmarks.GUID) };
+ getApplicationContext().getContentResolver().update(uri, values, where, args);
+ }
+
+ protected Uri insertRow(ContentValues values) {
+ Uri uri = BrowserContractHelpers.BOOKMARKS_CONTENT_URI;
+ return getApplicationContext().getContentResolver().insert(uri, values);
+ }
+
+ protected static ContentValues specialFolder() {
+ ContentValues values = new ContentValues();
+
+ final long now = System.currentTimeMillis();
+ values.put(Bookmarks.DATE_CREATED, now);
+ values.put(Bookmarks.DATE_MODIFIED, now);
+ values.put(Bookmarks.TYPE, BrowserContract.Bookmarks.TYPE_FOLDER);
+
+ return values;
+ }
+
+ protected static ContentValues fennecMobileRecordWithoutTitle() {
+ ContentValues values = specialFolder();
+ values.put(BrowserContract.SyncColumns.GUID, "mobile");
+ values.putNull(BrowserContract.Bookmarks.TITLE);
+
+ return values;
+ }
+
+ protected ContentValues fennecPinnedItemsRecord() {
+ final ContentValues values = specialFolder();
+ final String title = getApplicationContext().getResources().getString(R.string.bookmarks_folder_pinned);
+
+ values.put(BrowserContract.SyncColumns.GUID, Bookmarks.PINNED_FOLDER_GUID);
+ values.put(Bookmarks._ID, Bookmarks.FIXED_PINNED_LIST_ID);
+ values.put(Bookmarks.PARENT, Bookmarks.FIXED_ROOT_ID);
+ values.put(Bookmarks.TITLE, title);
+ return values;
+ }
+
+ protected static ContentValues fennecPinnedChildItemRecord() {
+ ContentValues values = new ContentValues();
+
+ final long now = System.currentTimeMillis();
+
+ values.put(BrowserContract.SyncColumns.GUID, "dapinneditem");
+ values.put(Bookmarks.DATE_CREATED, now);
+ values.put(Bookmarks.DATE_MODIFIED, now);
+ values.put(Bookmarks.TYPE, BrowserContract.Bookmarks.TYPE_BOOKMARK);
+ values.put(Bookmarks.URL, "user-entered:foobar");
+ values.put(Bookmarks.PARENT, Bookmarks.FIXED_PINNED_LIST_ID);
+ values.put(Bookmarks.TITLE, "Foobar");
+ return values;
+ }
+
+ protected long setUpFennecMobileRecordWithoutTitle() {
+ ContentResolver cr = getApplicationContext().getContentResolver();
+ ContentValues values = fennecMobileRecordWithoutTitle();
+ updateRow(values);
+ return fennecGetMobileBookmarksFolderId(cr);
+ }
+
+ protected long setUpFennecMobileRecord() {
+ ContentResolver cr = getApplicationContext().getContentResolver();
+ ContentValues values = fennecMobileRecordWithoutTitle();
+ values.put(BrowserContract.Bookmarks.PARENT, BrowserContract.Bookmarks.FIXED_ROOT_ID);
+ String title = getApplicationContext().getResources().getString(R.string.bookmarks_folder_mobile);
+ values.put(BrowserContract.Bookmarks.TITLE, title);
+ updateRow(values);
+ return fennecGetMobileBookmarksFolderId(cr);
+ }
+
+ protected void setUpFennecPinnedItemsRecord() {
+ insertRow(fennecPinnedItemsRecord());
+ insertRow(fennecPinnedChildItemRecord());
+ }
+
+ //
+ // Fennec fake layer.
+ //
+ private Uri appendProfile(Uri uri) {
+ final String defaultProfile = "default"; // Fennec constant removed in Bug 715307.
+ return uri.buildUpon().appendQueryParameter(BrowserContract.PARAM_PROFILE, defaultProfile).build();
+ }
+
+ private long fennecGetFolderId(ContentResolver cr, String guid) {
+ Cursor c = null;
+ try {
+ c = cr.query(appendProfile(BrowserContractHelpers.BOOKMARKS_CONTENT_URI),
+ new String[] { BrowserContract.Bookmarks._ID },
+ BrowserContract.Bookmarks.GUID + " = ?",
+ new String[] { guid },
+ null);
+
+ if (c.moveToFirst()) {
+ return c.getLong(c.getColumnIndexOrThrow(BrowserContract.Bookmarks._ID));
+ }
+ return -1;
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+
+ private long fennecGetMobileBookmarksFolderId(ContentResolver cr) {
+ return fennecGetFolderId(cr, BrowserContract.Bookmarks.MOBILE_FOLDER_GUID);
+ }
+
+ public void fennecAddBookmark(String title, String uri) {
+ ContentResolver cr = getApplicationContext().getContentResolver();
+
+ long folderId = fennecGetMobileBookmarksFolderId(cr);
+ if (folderId < 0) {
+ return;
+ }
+
+ ContentValues values = new ContentValues();
+ values.put(BrowserContract.Bookmarks.TITLE, title);
+ values.put(BrowserContract.Bookmarks.URL, uri);
+ values.put(BrowserContract.Bookmarks.PARENT, folderId);
+
+ // Restore deleted record if possible
+ values.put(BrowserContract.Bookmarks.IS_DELETED, 0);
+
+ Logger.debug(getName(), "Adding bookmark " + title + ", " + uri + " in " + folderId);
+ int updated = cr.update(appendProfile(BrowserContractHelpers.BOOKMARKS_CONTENT_URI),
+ values,
+ BrowserContract.Bookmarks.URL + " = ?",
+ new String[] { uri });
+
+ if (updated == 0) {
+ Uri insert = cr.insert(appendProfile(BrowserContractHelpers.BOOKMARKS_CONTENT_URI), values);
+ long idFromUri = ContentUris.parseId(insert);
+ Logger.debug(getName(), "Inserted " + uri + " as " + idFromUri);
+ Logger.debug(getName(), "Position is " + getPosition(idFromUri));
+ }
+ }
+
+ private long getPosition(long idFromUri) {
+ ContentResolver cr = getApplicationContext().getContentResolver();
+ Cursor c = cr.query(appendProfile(BrowserContractHelpers.BOOKMARKS_CONTENT_URI),
+ new String[] { BrowserContract.Bookmarks.POSITION },
+ BrowserContract.Bookmarks._ID + " = ?",
+ new String[] { String.valueOf(idFromUri) },
+ null);
+ if (!c.moveToFirst()) {
+ return -2;
+ }
+ return c.getLong(0);
+ }
+
+ protected AndroidBrowserBookmarksDataAccessor dataAccessor = null;
+ protected AndroidBrowserBookmarksDataAccessor getDataAccessor() {
+ if (dataAccessor == null) {
+ dataAccessor = new AndroidBrowserBookmarksDataAccessor(getApplicationContext());
+ }
+ return dataAccessor;
+ }
+
+ protected void wipe() {
+ Logger.debug(getName(), "Wiping.");
+ getDataAccessor().wipe();
+ }
+
+ protected void assertChildrenAreOrdered(AndroidBrowserBookmarksRepository repo, String guid, Record[] expected) {
+ Logger.debug(getName(), "Fetching children...");
+ JSONArray folderChildren = fetchChildrenForGUID(repo, guid);
+
+ assertTrue(folderChildren != null);
+ Logger.debug(getName(), "Children are " + folderChildren.toJSONString());
+ assertEquals(expected.length, folderChildren.size());
+ for (int i = 0; i < expected.length; ++i) {
+ assertEquals(expected[i].guid, ((String) folderChildren.get(i)));
+ }
+ }
+
+ protected void assertChildrenAreUnordered(AndroidBrowserBookmarksRepository repo, String guid, Record[] expected) {
+ Logger.debug(getName(), "Fetching children...");
+ JSONArray folderChildren = fetchChildrenForGUID(repo, guid);
+
+ assertTrue(folderChildren != null);
+ Logger.debug(getName(), "Children are " + folderChildren.toJSONString());
+ assertEquals(expected.length, folderChildren.size());
+ for (Record record : expected) {
+ folderChildren.contains(record.guid);
+ }
+ }
+
+ /**
+ * Return a sequence of children GUIDs for the provided folder ID.
+ */
+ protected ArrayList<String> fetchChildrenDirect(long id) {
+ Logger.debug(getName(), "Fetching children directly from DB...");
+ final ArrayList<String> out = new ArrayList<String>();
+ final AndroidBrowserBookmarksDataAccessor accessor = new AndroidBrowserBookmarksDataAccessor(getApplicationContext());
+ Cursor cur = null;
+ try {
+ cur = accessor.getChildren(id);
+ } catch (NullCursorException e) {
+ fail("Got null cursor.");
+ }
+ try {
+ if (!cur.moveToFirst()) {
+ return out;
+ }
+ final int guidCol = cur.getColumnIndex(BrowserContract.SyncColumns.GUID);
+ while (!cur.isAfterLast()) {
+ out.add(cur.getString(guidCol));
+ cur.moveToNext();
+ }
+ } finally {
+ cur.close();
+ }
+ return out;
+ }
+
+ /**
+ * Assert that the children of the provided ID are correct and positioned in the database.
+ * @param id
+ * @param guids
+ */
+ protected void assertChildrenAreDirect(long id, String[] guids) {
+ Logger.debug(getName(), "Fetching children directly from DB...");
+ AndroidBrowserBookmarksDataAccessor accessor = new AndroidBrowserBookmarksDataAccessor(getApplicationContext());
+ Cursor cur = null;
+ try {
+ cur = accessor.getChildren(id);
+ } catch (NullCursorException e) {
+ fail("Got null cursor.");
+ }
+ try {
+ if (guids == null || guids.length == 0) {
+ assertFalse(cur.moveToFirst());
+ return;
+ }
+
+ assertTrue(cur.moveToFirst());
+ int i = 0;
+ final int guidCol = cur.getColumnIndex(BrowserContract.SyncColumns.GUID);
+ final int posCol = cur.getColumnIndex(BrowserContract.Bookmarks.POSITION);
+ while (!cur.isAfterLast()) {
+ assertTrue(i < guids.length);
+ final String guid = cur.getString(guidCol);
+ final int pos = cur.getInt(posCol);
+ Logger.debug(getName(), "Fetched child: " + guid + " has position " + pos);
+ assertEquals(guids[i], guid);
+ assertEquals(i, pos);
+
+ ++i;
+ cur.moveToNext();
+ }
+ assertEquals(guids.length, i);
+ } finally {
+ cur.close();
+ }
+ }
+}
+
+/**
+TODO
+
+Test for storing a record that will reconcile to mobile; postcondition is
+that there's still a directory called mobile that includes all the items that
+it used to.
+
+mobile folder created without title.
+Unsorted put in mobile???
+Tests for children retrieval
+Tests for children merge
+Tests for modify retrieve parent when child added, removed, reordered (oh, reorder is hard! Any change, then.)
+Safety mode?
+Test storing folder first, contents first.
+Store folder in next session. Verify order recovery.
+
+
+*/
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestClientsDatabase.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestClientsDatabase.java
new file mode 100644
index 0000000000..198073fcf3
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestClientsDatabase.java
@@ -0,0 +1,200 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.db;
+
+import java.util.ArrayList;
+
+import org.json.simple.JSONArray;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.android.ClientsDatabase;
+import org.mozilla.gecko.sync.repositories.android.RepoUtils;
+import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
+import org.mozilla.gecko.sync.setup.Constants;
+
+import android.database.Cursor;
+import android.test.AndroidTestCase;
+
+public class TestClientsDatabase extends AndroidTestCase {
+
+ protected ClientsDatabase db;
+
+ public void setUp() {
+ db = new ClientsDatabase(mContext);
+ db.wipeDB();
+ }
+
+ public void testStoreAndFetch() {
+ ClientRecord record = new ClientRecord();
+ String profileConst = Constants.DEFAULT_PROFILE;
+ db.store(profileConst, record);
+
+ Cursor cur = null;
+ try {
+ // Test stored item gets fetched correctly.
+ cur = db.fetchClientsCursor(record.guid, profileConst);
+ assertTrue(cur.moveToFirst());
+ assertEquals(1, cur.getCount());
+
+ String guid = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_ACCOUNT_GUID);
+ String profileId = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_PROFILE);
+ String clientName = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_NAME);
+ String clientType = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_TYPE);
+
+ assertEquals(record.guid, guid);
+ assertEquals(profileConst, profileId);
+ assertEquals(record.name, clientName);
+ assertEquals(record.type, clientType);
+ } catch (NullCursorException e) {
+ fail("Should not have NullCursorException");
+ } finally {
+ if (cur != null) {
+ cur.close();
+ }
+ }
+ }
+
+ public void testStoreAndFetchSpecificCommands() {
+ String accountGUID = Utils.generateGuid();
+ ArrayList<String> args = new ArrayList<String>();
+ args.add("URI of Page");
+ args.add("Sender GUID");
+ args.add("Title of Page");
+ String jsonArgs = JSONArray.toJSONString(args);
+
+ Cursor cur = null;
+ try {
+ db.store(accountGUID, "displayURI", jsonArgs);
+
+ // This row should not show up in the fetch.
+ args.add("Another arg.");
+ db.store(accountGUID, "displayURI", JSONArray.toJSONString(args));
+
+ // Test stored item gets fetched correctly.
+ cur = db.fetchSpecificCommand(accountGUID, "displayURI", jsonArgs);
+ assertTrue(cur.moveToFirst());
+ assertEquals(1, cur.getCount());
+
+ String guid = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_ACCOUNT_GUID);
+ String commandType = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_COMMAND);
+ String fetchedArgs = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_ARGS);
+
+ assertEquals(accountGUID, guid);
+ assertEquals("displayURI", commandType);
+ assertEquals(jsonArgs, fetchedArgs);
+ } catch (NullCursorException e) {
+ fail("Should not have NullCursorException");
+ } finally {
+ if (cur != null) {
+ cur.close();
+ }
+ }
+ }
+
+ public void testFetchCommandsForClient() {
+ String accountGUID = Utils.generateGuid();
+ ArrayList<String> args = new ArrayList<String>();
+ args.add("URI of Page");
+ args.add("Sender GUID");
+ args.add("Title of Page");
+ String jsonArgs = JSONArray.toJSONString(args);
+
+ Cursor cur = null;
+ try {
+ db.store(accountGUID, "displayURI", jsonArgs);
+
+ // This row should ALSO show up in the fetch.
+ args.add("Another arg.");
+ db.store(accountGUID, "displayURI", JSONArray.toJSONString(args));
+
+ // Test both stored items with the same GUID but different command are fetched.
+ cur = db.fetchCommandsForClient(accountGUID);
+ assertTrue(cur.moveToFirst());
+ assertEquals(2, cur.getCount());
+ } catch (NullCursorException e) {
+ fail("Should not have NullCursorException");
+ } finally {
+ if (cur != null) {
+ cur.close();
+ }
+ }
+ }
+
+ @SuppressWarnings("resource")
+ public void testDelete() {
+ ClientRecord record1 = new ClientRecord();
+ ClientRecord record2 = new ClientRecord();
+ String profileConst = Constants.DEFAULT_PROFILE;
+
+ db.store(profileConst, record1);
+ db.store(profileConst, record2);
+
+ Cursor cur = null;
+ try {
+ // Test record doesn't exist after delete.
+ db.deleteClient(record1.guid, profileConst);
+ cur = db.fetchClientsCursor(record1.guid, profileConst);
+ assertFalse(cur.moveToFirst());
+ assertEquals(0, cur.getCount());
+
+ // Test record2 still there after deleting record1.
+ cur = db.fetchClientsCursor(record2.guid, profileConst);
+ assertTrue(cur.moveToFirst());
+ assertEquals(1, cur.getCount());
+
+ String guid = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_ACCOUNT_GUID);
+ String profileId = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_PROFILE);
+ String clientName = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_NAME);
+ String clientType = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_TYPE);
+
+ assertEquals(record2.guid, guid);
+ assertEquals(profileConst, profileId);
+ assertEquals(record2.name, clientName);
+ assertEquals(record2.type, clientType);
+ } catch (NullCursorException e) {
+ fail("Should not have NullCursorException");
+ } finally {
+ if (cur != null) {
+ cur.close();
+ }
+ }
+ }
+
+ @SuppressWarnings("resource")
+ public void testWipe() {
+ ClientRecord record1 = new ClientRecord();
+ ClientRecord record2 = new ClientRecord();
+ String profileConst = Constants.DEFAULT_PROFILE;
+
+ db.store(profileConst, record1);
+ db.store(profileConst, record2);
+
+
+ Cursor cur = null;
+ try {
+ // Test before wipe the records are there.
+ cur = db.fetchClientsCursor(record2.guid, profileConst);
+ assertTrue(cur.moveToFirst());
+ assertEquals(1, cur.getCount());
+ cur = db.fetchClientsCursor(record2.guid, profileConst);
+ assertTrue(cur.moveToFirst());
+ assertEquals(1, cur.getCount());
+
+ // Test after wipe neither record exists.
+ db.wipeClientsTable();
+ cur = db.fetchClientsCursor(record2.guid, profileConst);
+ assertFalse(cur.moveToFirst());
+ assertEquals(0, cur.getCount());
+ cur = db.fetchClientsCursor(record1.guid, profileConst);
+ assertFalse(cur.moveToFirst());
+ assertEquals(0, cur.getCount());
+ } catch (NullCursorException e) {
+ fail("Should not have NullCursorException");
+ } finally {
+ if (cur != null) {
+ cur.close();
+ }
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestClientsDatabaseAccessor.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestClientsDatabaseAccessor.java
new file mode 100644
index 0000000000..65b14e860f
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestClientsDatabaseAccessor.java
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.db;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.mozilla.gecko.background.testhelpers.CommandHelpers;
+import org.mozilla.gecko.sync.CommandProcessor.Command;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
+import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.test.AndroidTestCase;
+
+public class TestClientsDatabaseAccessor extends AndroidTestCase {
+
+ public class StubbedClientsDatabaseAccessor extends ClientsDatabaseAccessor {
+ public StubbedClientsDatabaseAccessor(Context mContext) {
+ super(mContext);
+ }
+ }
+
+ StubbedClientsDatabaseAccessor db;
+
+ public void setUp() {
+ db = new StubbedClientsDatabaseAccessor(mContext);
+ db.wipeDB();
+ }
+
+ public void tearDown() {
+ db.close();
+ }
+
+ public void testStoreArrayListAndFetch() throws NullCursorException {
+ ArrayList<ClientRecord> list = new ArrayList<ClientRecord>();
+ ClientRecord record1 = new ClientRecord(Utils.generateGuid());
+ ClientRecord record2 = new ClientRecord(Utils.generateGuid());
+ ClientRecord record3 = new ClientRecord(Utils.generateGuid());
+
+ list.add(record1);
+ list.add(record2);
+ db.store(list);
+
+ ClientRecord r1 = db.fetchClient(record1.guid);
+ ClientRecord r2 = db.fetchClient(record2.guid);
+ ClientRecord r3 = db.fetchClient(record3.guid);
+
+ assertNotNull(r1);
+ assertNotNull(r2);
+ assertNull(r3);
+ assertTrue(record1.equals(r1));
+ assertTrue(record2.equals(r2));
+ assertFalse(record3.equals(r3));
+ }
+
+ public void testStoreAndFetchCommandsForClient() {
+ String accountGUID1 = Utils.generateGuid();
+ String accountGUID2 = Utils.generateGuid();
+
+ Command command1 = CommandHelpers.getCommand1();
+ Command command2 = CommandHelpers.getCommand2();
+ Command command3 = CommandHelpers.getCommand3();
+
+ Cursor cur = null;
+ try {
+ db.store(accountGUID1, command1);
+ db.store(accountGUID1, command2);
+ db.store(accountGUID2, command3);
+
+ List<Command> commands = db.fetchCommandsForClient(accountGUID1);
+ assertEquals(2, commands.size());
+ assertEquals(1, commands.get(0).args.size());
+ assertEquals(1, commands.get(1).args.size());
+ } catch (NullCursorException e) {
+ fail("Should not have NullCursorException");
+ } finally {
+ if (cur != null) {
+ cur.close();
+ }
+ }
+ }
+
+ public void testNumClients() {
+ final int COUNT = 5;
+ ArrayList<ClientRecord> list = new ArrayList<ClientRecord>();
+ for (int i = 0; i < 5; i++) {
+ list.add(new ClientRecord());
+ }
+ db.store(list);
+ assertEquals(COUNT, db.clientsCount());
+ }
+
+ public void testFetchAll() throws NullCursorException {
+ ArrayList<ClientRecord> list = new ArrayList<ClientRecord>();
+ ClientRecord record1 = new ClientRecord(Utils.generateGuid());
+ ClientRecord record2 = new ClientRecord(Utils.generateGuid());
+
+ list.add(record1);
+ list.add(record2);
+
+ boolean thrown = false;
+ try {
+ Map<String, ClientRecord> records = db.fetchAllClients();
+
+ assertNotNull(records);
+ assertEquals(0, records.size());
+
+ db.store(list);
+ records = db.fetchAllClients();
+ assertNotNull(records);
+ assertEquals(2, records.size());
+ assertTrue(record1.equals(records.get(record1.guid)));
+ assertTrue(record2.equals(records.get(record2.guid)));
+
+ // put() should throw an exception since records is immutable.
+ records.put(null, null);
+ } catch (UnsupportedOperationException e) {
+ thrown = true;
+ }
+ assertTrue(thrown);
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestFennecTabsRepositorySession.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestFennecTabsRepositorySession.java
new file mode 100644
index 0000000000..02d393ce86
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestFennecTabsRepositorySession.java
@@ -0,0 +1,297 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.db;
+
+import org.json.simple.JSONArray;
+import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
+import org.mozilla.gecko.background.sync.helpers.ExpectFetchDelegate;
+import org.mozilla.gecko.background.sync.helpers.SessionTestHelper;
+import org.mozilla.gecko.background.testhelpers.MockClientsDataDelegate;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserContract.Clients;
+import org.mozilla.gecko.sync.repositories.NoContentProviderException;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
+import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
+import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository;
+import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository.FennecTabsRepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+import org.mozilla.gecko.sync.repositories.domain.TabsRecord;
+
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.RemoteException;
+
+public class TestFennecTabsRepositorySession extends AndroidSyncTestCase {
+ public static final MockClientsDataDelegate clientsDataDelegate = new MockClientsDataDelegate();
+ public static final String TEST_CLIENT_GUID = clientsDataDelegate.getAccountGUID();
+ public static final String TEST_CLIENT_NAME = clientsDataDelegate.getClientName();
+ public static final String TEST_CLIENT_DEVICE_TYPE = "phablet";
+
+ // Override these to test against data that is not live.
+ public static final String TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS ?";
+ public static final String[] TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION_ARGS = new String[] { TEST_CLIENT_GUID };
+
+ public static final String TEST_CLIENTS_GUID_IS_LOCAL_SELECTION = BrowserContract.Clients.GUID + " IS ?";
+ public static final String[] TEST_CLIENTS_GUID_IS_LOCAL_SELECTION_ARGS = new String[] { TEST_CLIENT_GUID };
+
+ protected ContentProviderClient tabsClient = null;
+ protected ContentProviderClient clientsClient = null;
+
+ protected ContentProviderClient getTabsClient() {
+ final ContentResolver cr = getApplicationContext().getContentResolver();
+ return cr.acquireContentProviderClient(BrowserContractHelpers.TABS_CONTENT_URI);
+ }
+
+ protected ContentProviderClient getClientsClient() {
+ final ContentResolver cr = getApplicationContext().getContentResolver();
+ return cr.acquireContentProviderClient(BrowserContractHelpers.CLIENTS_CONTENT_URI);
+ }
+
+ public TestFennecTabsRepositorySession() throws NoContentProviderException {
+ super();
+ }
+
+ @Override
+ public void setUp() {
+ if (tabsClient == null) {
+ tabsClient = getTabsClient();
+ }
+ if (clientsClient == null) {
+ clientsClient = getClientsClient();
+ }
+ }
+
+ protected int deleteTestClient(final ContentProviderClient clientsClient) throws RemoteException {
+ if (clientsClient == null) {
+ return -1;
+ }
+ return clientsClient.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, TEST_CLIENTS_GUID_IS_LOCAL_SELECTION, TEST_CLIENTS_GUID_IS_LOCAL_SELECTION_ARGS);
+ }
+
+ protected int deleteAllTestTabs(final ContentProviderClient tabsClient) throws RemoteException {
+ if (tabsClient == null) {
+ return -1;
+ }
+ return tabsClient.delete(BrowserContractHelpers.TABS_CONTENT_URI,
+ TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION, TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION_ARGS);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ if (tabsClient != null) {
+ deleteAllTestTabs(tabsClient);
+
+ tabsClient.release();
+ tabsClient = null;
+ }
+
+ if (clientsClient != null) {
+ deleteTestClient(clientsClient);
+
+ clientsClient.release();
+ clientsClient = null;
+ }
+ }
+
+ protected FennecTabsRepository getRepository() {
+ /**
+ * Override this chain in order to avoid our test code having to create two
+ * sessions all the time.
+ */
+ return new FennecTabsRepository(clientsDataDelegate) {
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ try {
+ final FennecTabsRepositorySession session = new FennecTabsRepositorySession(this, context) {
+ @Override
+ protected synchronized void trackGUID(String guid) {
+ }
+
+ @Override
+ protected String localClientSelection() {
+ return TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION;
+ }
+
+ @Override
+ protected String[] localClientSelectionArgs() {
+ return TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION_ARGS;
+ }
+ };
+ delegate.onSessionCreated(session);
+ } catch (Exception e) {
+ delegate.onSessionCreateFailed(e);
+ }
+ }
+ };
+ }
+
+ protected FennecTabsRepositorySession createSession() {
+ return (FennecTabsRepositorySession) SessionTestHelper.createSession(
+ getApplicationContext(),
+ getRepository());
+ }
+
+ protected FennecTabsRepositorySession createAndBeginSession() {
+ return (FennecTabsRepositorySession) SessionTestHelper.createAndBeginSession(
+ getApplicationContext(),
+ getRepository());
+ }
+
+ protected Runnable fetchSinceRunnable(final RepositorySession session, final long timestamp, final Record[] expectedRecords) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ session.fetchSince(timestamp, new ExpectFetchDelegate(expectedRecords));
+ }
+ };
+ }
+
+ protected Runnable fetchAllRunnable(final RepositorySession session, final Record[] expectedRecords) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ session.fetchAll(new ExpectFetchDelegate(expectedRecords));
+ }
+ };
+ }
+
+ protected Tab testTab1;
+ protected Tab testTab2;
+ protected Tab testTab3;
+
+ @SuppressWarnings("unchecked")
+ private void insertSomeTestTabs(ContentProviderClient tabsClient) throws RemoteException {
+ final JSONArray history1 = new JSONArray();
+ history1.add("http://test.com/test1.html");
+ testTab1 = new Tab("test title 1", "http://test.com/test1.png", history1, 1000);
+
+ final JSONArray history2 = new JSONArray();
+ history2.add("http://test.com/test2.html#1");
+ history2.add("http://test.com/test2.html#2");
+ history2.add("http://test.com/test2.html#3");
+ testTab2 = new Tab("test title 2", "http://test.com/test2.png", history2, 2000);
+
+ final JSONArray history3 = new JSONArray();
+ history3.add("http://test.com/test3.html#1");
+ history3.add("http://test.com/test3.html#2");
+ testTab3 = new Tab("test title 3", "http://test.com/test3.png", history3, 3000);
+
+ tabsClient.insert(BrowserContractHelpers.TABS_CONTENT_URI, testTab1.toContentValues(TEST_CLIENT_GUID, 0));
+ tabsClient.insert(BrowserContractHelpers.TABS_CONTENT_URI, testTab2.toContentValues(TEST_CLIENT_GUID, 1));
+ tabsClient.insert(BrowserContractHelpers.TABS_CONTENT_URI, testTab3.toContentValues(TEST_CLIENT_GUID, 2));
+ }
+
+ protected TabsRecord insertTestTabsAndExtractTabsRecord() throws RemoteException {
+ insertSomeTestTabs(tabsClient);
+
+ final String positionAscending = BrowserContract.Tabs.POSITION + " ASC";
+ Cursor cursor = null;
+ try {
+ cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null,
+ TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION, TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION_ARGS, positionAscending);
+ CursorDumper.dumpCursor(cursor);
+
+ final TabsRecord tabsRecord = FennecTabsRepository.tabsRecordFromCursor(cursor, TEST_CLIENT_GUID, TEST_CLIENT_NAME);
+
+ assertEquals(TEST_CLIENT_GUID, tabsRecord.guid);
+ assertEquals(TEST_CLIENT_NAME, tabsRecord.clientName);
+
+ assertNotNull(tabsRecord.tabs);
+ assertEquals(cursor.getCount(), tabsRecord.tabs.size());
+
+ return tabsRecord;
+ } finally {
+ cursor.close();
+ }
+ }
+
+ public void testFetchAll() throws NoContentProviderException, RemoteException {
+ final TabsRecord tabsRecord = insertTestTabsAndExtractTabsRecord();
+
+ final FennecTabsRepositorySession session = createAndBeginSession();
+ performWait(fetchAllRunnable(session, new Record[] { tabsRecord }));
+
+ session.abort();
+ }
+
+ public void testFetchSince() throws NoContentProviderException, RemoteException {
+ final TabsRecord tabsRecord = insertTestTabsAndExtractTabsRecord();
+
+ final FennecTabsRepositorySession session = createAndBeginSession();
+
+ // Not all tabs are modified after this, but the record should contain them all.
+ performWait(fetchSinceRunnable(session, 1000, new Record[] { tabsRecord }));
+
+ // No tabs are modified after this, but our client name has changed in the interim.
+ performWait(fetchSinceRunnable(session, 4000, new Record[] { tabsRecord }));
+
+ // No tabs are modified after this, and our client name hasn't changed, so
+ // we shouldn't get a record at all. Note: this runs after our static
+ // initializer that sets the client data timestamp.
+ final long now = System.currentTimeMillis();
+ performWait(fetchSinceRunnable(session, now, new Record[] { }));
+
+ // No tabs are modified after this, but our client name has changed, so
+ // again we get a record.
+ clientsDataDelegate.setClientName("new client name", System.currentTimeMillis());
+ performWait(fetchSinceRunnable(session, now, new Record[] { tabsRecord }));
+
+ session.abort();
+ }
+
+ // Verify that storing a tabs record writes a clients record with the correct
+ // device type to the Fennec clients provider.
+ public void testStore() throws NoContentProviderException, RemoteException {
+ // Get a valid tabsRecord to write.
+ final TabsRecord tabsRecord = insertTestTabsAndExtractTabsRecord();
+ deleteAllTestTabs(tabsClient);
+ deleteTestClient(clientsClient);
+
+ final ContentResolver cr = getApplicationContext().getContentResolver();
+ final ContentProviderClient clientsClient = cr.acquireContentProviderClient(BrowserContractHelpers.CLIENTS_CONTENT_URI);
+
+ try {
+ // This clients DB is not the Fennec DB; it's Sync's own clients DB.
+ final ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(getApplicationContext());
+ try {
+ ClientRecord clientRecord = new ClientRecord(TEST_CLIENT_GUID);
+ clientRecord.name = TEST_CLIENT_NAME;
+ clientRecord.type = TEST_CLIENT_DEVICE_TYPE;
+ db.store(clientRecord);
+ } finally {
+ db.close();
+ }
+
+ final FennecTabsRepositorySession session = createAndBeginSession();
+ performWait(AndroidBrowserRepositoryTestCase.storeRunnable(session, tabsRecord));
+
+ session.abort();
+
+ // This store should write Sync's idea of the client's device_type to Fennec's clients CP.
+ final Cursor cursor = clientsClient.query(BrowserContractHelpers.CLIENTS_CONTENT_URI, null,
+ TEST_CLIENTS_GUID_IS_LOCAL_SELECTION, TEST_CLIENTS_GUID_IS_LOCAL_SELECTION_ARGS, null);
+ assertNotNull(cursor);
+
+ try {
+ assertTrue(cursor.moveToFirst());
+ assertEquals(TEST_CLIENT_GUID, cursor.getString(cursor.getColumnIndex(Clients.GUID)));
+ assertEquals(TEST_CLIENT_NAME, cursor.getString(cursor.getColumnIndex(Clients.NAME)));
+ assertEquals(TEST_CLIENT_DEVICE_TYPE, cursor.getString(cursor.getColumnIndex(Clients.DEVICE_TYPE)));
+ assertTrue(cursor.isLast());
+ } finally {
+ cursor.close();
+ }
+ } finally {
+ // We can't delete only our test client due to a Fennec CP issue with guid vs. client_guid.
+ clientsClient.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, null, null);
+ clientsClient.release();
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestFormHistoryRepositorySession.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestFormHistoryRepositorySession.java
new file mode 100644
index 0000000000..5d5014b753
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestFormHistoryRepositorySession.java
@@ -0,0 +1,441 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.db;
+
+import java.util.concurrent.ExecutorService;
+
+import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
+import org.mozilla.gecko.background.sync.helpers.ExpectFetchDelegate;
+import org.mozilla.gecko.background.sync.helpers.ExpectFetchSinceDelegate;
+import org.mozilla.gecko.background.sync.helpers.ExpectGuidsSinceDelegate;
+import org.mozilla.gecko.background.sync.helpers.ExpectNoStoreDelegate;
+import org.mozilla.gecko.background.sync.helpers.ExpectStoredDelegate;
+import org.mozilla.gecko.background.sync.helpers.SessionTestHelper;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.sync.repositories.InactiveSessionException;
+import org.mozilla.gecko.sync.repositories.NoContentProviderException;
+import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
+import org.mozilla.gecko.sync.repositories.android.FormHistoryRepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
+import org.mozilla.gecko.sync.repositories.domain.FormHistoryRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import android.content.ContentProviderClient;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+
+public class TestFormHistoryRepositorySession extends AndroidSyncTestCase {
+ protected ContentProviderClient formsProvider = null;
+
+ public TestFormHistoryRepositorySession() throws NoContentProviderException {
+ super();
+ }
+
+ public void setUp() {
+ if (formsProvider == null) {
+ try {
+ formsProvider = FormHistoryRepositorySession.acquireContentProvider(getApplicationContext());
+ } catch (NoContentProviderException e) {
+ fail("Failed to acquireContentProvider: " + e);
+ }
+ }
+
+ try {
+ FormHistoryRepositorySession.purgeDatabases(formsProvider);
+ } catch (RemoteException e) {
+ fail("Failed to purgeDatabases: " + e);
+ }
+ }
+
+ public void tearDown() {
+ if (formsProvider != null) {
+ formsProvider.release();
+ formsProvider = null;
+ }
+ }
+
+ protected FormHistoryRepositorySession.FormHistoryRepository getRepository() {
+ /**
+ * Override this chain in order to avoid our test code having to create two
+ * sessions all the time.
+ */
+ return new FormHistoryRepositorySession.FormHistoryRepository() {
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ try {
+ final FormHistoryRepositorySession session = new FormHistoryRepositorySession(this, context) {
+ @Override
+ protected synchronized void trackGUID(String guid) {
+ }
+ };
+ delegate.onSessionCreated(session);
+ } catch (Exception e) {
+ delegate.onSessionCreateFailed(e);
+ }
+ }
+ };
+ }
+
+
+ protected FormHistoryRepositorySession createSession() {
+ return (FormHistoryRepositorySession) SessionTestHelper.createSession(
+ getApplicationContext(),
+ getRepository());
+ }
+
+ protected FormHistoryRepositorySession createAndBeginSession() {
+ return (FormHistoryRepositorySession) SessionTestHelper.createAndBeginSession(
+ getApplicationContext(),
+ getRepository());
+ }
+
+ public void testAcquire() throws NoContentProviderException {
+ final FormHistoryRepositorySession session = createAndBeginSession();
+ assertNotNull(session.getFormsProvider());
+ session.abort();
+ }
+
+ protected int numRecords(FormHistoryRepositorySession session, Uri uri) throws RemoteException {
+ Cursor cur = null;
+ try {
+ cur = session.getFormsProvider().query(uri, null, null, null, null);
+ return cur.getCount();
+ } finally {
+ if (cur != null) {
+ cur.close();
+ }
+ }
+ }
+
+ protected long after0;
+ protected long after1;
+ protected long after2;
+ protected long after3;
+ protected long after4;
+ protected FormHistoryRecord regular1;
+ protected FormHistoryRecord regular2;
+ protected FormHistoryRecord deleted1;
+ protected FormHistoryRecord deleted2;
+
+ public void insertTwoRecords(FormHistoryRepositorySession session) throws RemoteException {
+ Uri regularUri = BrowserContractHelpers.FORM_HISTORY_CONTENT_URI;
+ Uri deletedUri = BrowserContractHelpers.DELETED_FORM_HISTORY_CONTENT_URI;
+ after0 = System.currentTimeMillis();
+
+ regular1 = new FormHistoryRecord("guid1", "forms", System.currentTimeMillis(), false);
+ regular1.fieldName = "fieldName1";
+ regular1.fieldValue = "value1";
+ final ContentValues cv1 = new ContentValues();
+ cv1.put(BrowserContract.FormHistory.GUID, regular1.guid);
+ cv1.put(BrowserContract.FormHistory.FIELD_NAME, regular1.fieldName);
+ cv1.put(BrowserContract.FormHistory.VALUE, regular1.fieldValue);
+ cv1.put(BrowserContract.FormHistory.FIRST_USED, 1000 * regular1.lastModified); // Microseconds.
+
+ int regularInserted = session.getFormsProvider().bulkInsert(regularUri, new ContentValues[] { cv1 });
+ assertEquals(1, regularInserted);
+ after1 = System.currentTimeMillis();
+
+ deleted1 = new FormHistoryRecord("guid3", "forms", -1, true);
+ final ContentValues cv3 = new ContentValues();
+ cv3.put(BrowserContract.FormHistory.GUID, deleted1.guid);
+ // cv3.put(BrowserContract.DeletedFormHistory.TIME_DELETED, record3.lastModified); // Set by CP.
+
+ int deletedInserted = session.getFormsProvider().bulkInsert(deletedUri, new ContentValues[] { cv3 });
+ assertEquals(1, deletedInserted);
+ after2 = System.currentTimeMillis();
+
+ regular2 = null;
+ deleted2 = null;
+ }
+
+ public void insertFourRecords(FormHistoryRepositorySession session) throws RemoteException {
+ Uri regularUri = BrowserContractHelpers.FORM_HISTORY_CONTENT_URI;
+ Uri deletedUri = BrowserContractHelpers.DELETED_FORM_HISTORY_CONTENT_URI;
+
+ insertTwoRecords(session);
+
+ regular2 = new FormHistoryRecord("guid2", "forms", System.currentTimeMillis(), false);
+ regular2.fieldName = "fieldName2";
+ regular2.fieldValue = "value2";
+ final ContentValues cv2 = new ContentValues();
+ cv2.put(BrowserContract.FormHistory.GUID, regular2.guid);
+ cv2.put(BrowserContract.FormHistory.FIELD_NAME, regular2.fieldName);
+ cv2.put(BrowserContract.FormHistory.VALUE, regular2.fieldValue);
+ cv2.put(BrowserContract.FormHistory.FIRST_USED, 1000 * regular2.lastModified); // Microseconds.
+
+ int regularInserted = session.getFormsProvider().bulkInsert(regularUri, new ContentValues[] { cv2 });
+ assertEquals(1, regularInserted);
+ after3 = System.currentTimeMillis();
+
+ deleted2 = new FormHistoryRecord("guid4", "forms", -1, true);
+ final ContentValues cv4 = new ContentValues();
+ cv4.put(BrowserContract.FormHistory.GUID, deleted2.guid);
+ // cv4.put(BrowserContract.DeletedFormHistory.TIME_DELETED, record4.lastModified); // Set by CP.
+
+ int deletedInserted = session.getFormsProvider().bulkInsert(deletedUri, new ContentValues[] { cv4 });
+ assertEquals(1, deletedInserted);
+ after4 = System.currentTimeMillis();
+ }
+
+ public void testWipe() throws NoContentProviderException, RemoteException {
+ final FormHistoryRepositorySession session = createAndBeginSession();
+
+ insertTwoRecords(session);
+ assertTrue(numRecords(session, BrowserContractHelpers.FORM_HISTORY_CONTENT_URI) > 0);
+ assertTrue(numRecords(session, BrowserContractHelpers.DELETED_FORM_HISTORY_CONTENT_URI) > 0);
+
+ performWait(WaitHelper.onThreadRunnable(new Runnable() {
+ @Override
+ public void run() {
+ session.wipe(new RepositorySessionWipeDelegate() {
+ public void onWipeSucceeded() {
+ performNotify();
+ }
+ public void onWipeFailed(Exception ex) {
+ performNotify("Wipe should have succeeded", ex);
+ }
+ @Override
+ public RepositorySessionWipeDelegate deferredWipeDelegate(final ExecutorService executor) {
+ return this;
+ }
+ });
+ }
+ }));
+
+ assertEquals(0, numRecords(session, BrowserContractHelpers.FORM_HISTORY_CONTENT_URI));
+ assertEquals(0, numRecords(session, BrowserContractHelpers.DELETED_FORM_HISTORY_CONTENT_URI));
+
+ session.abort();
+ }
+
+ protected Runnable fetchSinceRunnable(final RepositorySession session, final long timestamp, final String[] expectedGuids) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ session.fetchSince(timestamp, new ExpectFetchSinceDelegate(timestamp, expectedGuids));
+ }
+ };
+ }
+
+ protected Runnable fetchAllRunnable(final RepositorySession session, final Record[] expectedRecords) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ session.fetchAll(new ExpectFetchDelegate(expectedRecords));
+ }
+ };
+ }
+
+ protected Runnable fetchRunnable(final RepositorySession session, final String[] guids, final Record[] expectedRecords) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ try {
+ session.fetch(guids, new ExpectFetchDelegate(expectedRecords));
+ } catch (InactiveSessionException e) {
+ performNotify(e);
+ }
+ }
+ };
+ }
+
+ public void testFetchAll() throws NoContentProviderException, RemoteException {
+ final FormHistoryRepositorySession session = createAndBeginSession();
+
+ insertTwoRecords(session);
+
+ performWait(fetchAllRunnable(session, new Record[] { regular1, deleted1 }));
+
+ session.abort();
+ }
+
+ public void testFetchByGuid() throws NoContentProviderException, RemoteException {
+ final FormHistoryRepositorySession session = createAndBeginSession();
+
+ insertTwoRecords(session);
+
+ performWait(fetchRunnable(session,
+ new String[] { regular1.guid, deleted1.guid },
+ new Record[] { regular1, deleted1 }));
+ performWait(fetchRunnable(session,
+ new String[] { regular1.guid },
+ new Record[] { regular1 }));
+ performWait(fetchRunnable(session,
+ new String[] { deleted1.guid, "NON_EXISTENT_GUID?" },
+ new Record[] { deleted1 }));
+ performWait(fetchRunnable(session,
+ new String[] { "FIRST_NON_EXISTENT_GUID", "SECOND_NON_EXISTENT_GUID?" },
+ new Record[] { }));
+
+ session.abort();
+ }
+
+ public void testFetchSince() throws NoContentProviderException, RemoteException {
+ final FormHistoryRepositorySession session = createAndBeginSession();
+
+ insertFourRecords(session);
+
+ performWait(fetchSinceRunnable(session,
+ after0, new String[] { regular1.guid, deleted1.guid, regular2.guid, deleted2.guid }));
+ performWait(fetchSinceRunnable(session,
+ after1, new String[] { deleted1.guid, regular2.guid, deleted2.guid }));
+ performWait(fetchSinceRunnable(session,
+ after2, new String[] { regular2.guid, deleted2.guid }));
+ performWait(fetchSinceRunnable(session,
+ after3, new String[] { deleted2.guid }));
+ performWait(fetchSinceRunnable(session,
+ after4, new String[] { }));
+
+ session.abort();
+ }
+
+ protected Runnable guidsSinceRunnable(final RepositorySession session, final long timestamp, final String[] expectedGuids) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ session.guidsSince(timestamp, new ExpectGuidsSinceDelegate(expectedGuids));
+ }
+ };
+ }
+
+ public void testGuidsSince() throws NoContentProviderException, RemoteException {
+ final FormHistoryRepositorySession session = createAndBeginSession();
+
+ insertTwoRecords(session);
+
+ performWait(guidsSinceRunnable(session,
+ after0, new String[] { regular1.guid, deleted1.guid }));
+ performWait(guidsSinceRunnable(session,
+ after1, new String[] { deleted1.guid}));
+ performWait(guidsSinceRunnable(session,
+ after2, new String[] { }));
+
+ session.abort();
+ }
+
+ protected Runnable storeRunnable(final RepositorySession session, final Record record, final RepositorySessionStoreDelegate delegate) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ session.setStoreDelegate(delegate);
+ try {
+ session.store(record);
+ session.storeDone();
+ } catch (NoStoreDelegateException e) {
+ performNotify("NoStoreDelegateException should not occur.", e);
+ }
+ }
+ };
+ }
+
+ public void testStoreRemoteNew() throws NoContentProviderException, RemoteException {
+ final FormHistoryRepositorySession session = createAndBeginSession();
+
+ insertTwoRecords(session);
+
+ FormHistoryRecord rec;
+
+ // remote regular, local missing => should store.
+ rec = new FormHistoryRecord("new1", "forms", System.currentTimeMillis(), false);
+ rec.fieldName = "fieldName1";
+ rec.fieldValue = "fieldValue1";
+ performWait(storeRunnable(session, rec, new ExpectStoredDelegate(rec.guid)));
+ performWait(fetchRunnable(session, new String[] { rec.guid }, new Record[] { rec }));
+
+ // remote deleted, local missing => should delete, but at the moment we ignore.
+ rec = new FormHistoryRecord("new2", "forms", System.currentTimeMillis(), true);
+ performWait(storeRunnable(session, rec, new ExpectNoStoreDelegate()));
+ performWait(fetchRunnable(session, new String[] { rec.guid }, new Record[] { }));
+
+ session.abort();
+ }
+
+ public void testStoreRemoteNewer() throws NoContentProviderException, RemoteException {
+ final FormHistoryRepositorySession session = createAndBeginSession();
+
+ insertFourRecords(session);
+ long newTimestamp = System.currentTimeMillis();
+
+ FormHistoryRecord rec;
+
+ // remote regular, local regular, remote newer => should update.
+ rec = new FormHistoryRecord(regular1.guid, regular1.collection, newTimestamp, false);
+ rec.fieldName = regular1.fieldName;
+ rec.fieldValue = regular1.fieldValue + "NEW";
+ performWait(storeRunnable(session, rec, new ExpectStoredDelegate(rec.guid)));
+ performWait(fetchRunnable(session, new String[] { regular1.guid }, new Record[] { rec }));
+
+ // remote deleted, local regular, remote newer => should delete everything.
+ rec = new FormHistoryRecord(regular2.guid, regular2.collection, newTimestamp, true);
+ performWait(storeRunnable(session, rec, new ExpectStoredDelegate(rec.guid)));
+ performWait(fetchRunnable(session, new String[] { regular2.guid }, new Record[] { }));
+
+ // remote regular, local deleted, remote newer => should update.
+ rec = new FormHistoryRecord(deleted1.guid, deleted1.collection, newTimestamp, false);
+ rec.fieldName = regular1.fieldName;
+ rec.fieldValue = regular1.fieldValue + "NEW";
+ performWait(storeRunnable(session, rec, new ExpectStoredDelegate(rec.guid)));
+ performWait(fetchRunnable(session, new String[] { deleted1.guid }, new Record[] { rec }));
+
+ // remote deleted, local deleted, remote newer => should delete everything.
+ rec = new FormHistoryRecord(deleted2.guid, deleted2.collection, newTimestamp, true);
+ performWait(storeRunnable(session, rec, new ExpectNoStoreDelegate()));
+ performWait(fetchRunnable(session, new String[] { deleted2.guid }, new Record[] { }));
+
+ session.abort();
+ }
+
+ public void testStoreRemoteOlder() throws NoContentProviderException, RemoteException {
+ final FormHistoryRepositorySession session = createAndBeginSession();
+
+ long oldTimestamp = System.currentTimeMillis() - 100;
+ insertFourRecords(session);
+
+ FormHistoryRecord rec;
+
+ // remote regular, local regular, remote older => should ignore.
+ rec = new FormHistoryRecord(regular1.guid, regular1.collection, oldTimestamp, false);
+ rec.fieldName = regular1.fieldName;
+ rec.fieldValue = regular1.fieldValue + "NEW";
+ performWait(storeRunnable(session, rec, new ExpectNoStoreDelegate()));
+
+ // remote deleted, local regular, remote older => should ignore.
+ rec = new FormHistoryRecord(regular2.guid, regular2.collection, oldTimestamp, true);
+ performWait(storeRunnable(session, rec, new ExpectNoStoreDelegate()));
+
+ // remote regular, local deleted, remote older => should ignore.
+ rec = new FormHistoryRecord(deleted1.guid, deleted1.collection, oldTimestamp, false);
+ rec.fieldName = regular1.fieldName;
+ rec.fieldValue = regular1.fieldValue + "NEW";
+ performWait(storeRunnable(session, rec, new ExpectNoStoreDelegate()));
+
+ // remote deleted, local deleted, remote older => should ignore.
+ rec = new FormHistoryRecord(deleted2.guid, deleted2.collection, oldTimestamp, true);
+ performWait(storeRunnable(session, rec, new ExpectNoStoreDelegate()));
+
+ session.abort();
+ }
+
+ public void testStoreDifferentGuid() throws NoContentProviderException, RemoteException {
+ final FormHistoryRepositorySession session = createAndBeginSession();
+
+ insertTwoRecords(session);
+
+ FormHistoryRecord rec = (FormHistoryRecord) regular1.copyWithIDs("distinct", 999);
+ performWait(storeRunnable(session, rec, new ExpectStoredDelegate(rec.guid)));
+ // Existing record should take remote record's GUID.
+ performWait(fetchAllRunnable(session, new Record[] { rec, deleted1 }));
+
+ session.abort();
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestPasswordsRepository.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestPasswordsRepository.java
new file mode 100644
index 0000000000..210c8ca8cf
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestPasswordsRepository.java
@@ -0,0 +1,482 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.db;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
+import org.mozilla.gecko.background.sync.helpers.ExpectFetchDelegate;
+import org.mozilla.gecko.background.sync.helpers.ExpectFetchSinceDelegate;
+import org.mozilla.gecko.background.sync.helpers.ExpectGuidsSinceDelegate;
+import org.mozilla.gecko.background.sync.helpers.ExpectNoStoreDelegate;
+import org.mozilla.gecko.background.sync.helpers.ExpectStoredDelegate;
+import org.mozilla.gecko.background.sync.helpers.PasswordHelpers;
+import org.mozilla.gecko.background.sync.helpers.SessionTestHelper;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.repositories.InactiveSessionException;
+import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
+import org.mozilla.gecko.sync.repositories.android.PasswordsRepositorySession;
+import org.mozilla.gecko.sync.repositories.android.RepoUtils;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
+import org.mozilla.gecko.sync.repositories.domain.PasswordRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import android.content.ContentProviderClient;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.RemoteException;
+
+public class TestPasswordsRepository extends AndroidSyncTestCase {
+ private final String NEW_PASSWORD1 = "password";
+ private final String NEW_PASSWORD2 = "drowssap";
+
+ @Override
+ public void setUp() {
+ wipe();
+ assertTrue(WaitHelper.getTestWaiter().isIdle());
+ }
+
+ public void testFetchAll() {
+ RepositorySession session = createAndBeginSession();
+ Record[] expected = new Record[] { PasswordHelpers.createPassword1(),
+ PasswordHelpers.createPassword2() };
+
+ performWait(storeRunnable(session, expected[0]));
+ performWait(storeRunnable(session, expected[1]));
+
+ performWait(fetchAllRunnable(session, expected));
+ dispose(session);
+ }
+
+ public void testGuidsSinceReturnMultipleRecords() {
+ RepositorySession session = createAndBeginSession();
+
+ PasswordRecord record1 = PasswordHelpers.createPassword1();
+ PasswordRecord record2 = PasswordHelpers.createPassword2();
+
+ updatePassword(NEW_PASSWORD1, record1);
+ long timestamp = updatePassword(NEW_PASSWORD2, record2);
+
+ String[] expected = new String[] { record1.guid, record2.guid };
+
+ performWait(storeRunnable(session, record1));
+ performWait(storeRunnable(session, record2));
+
+ performWait(guidsSinceRunnable(session, timestamp, expected));
+ dispose(session);
+ }
+
+ public void testGuidsSinceReturnNoRecords() {
+ RepositorySession session = createAndBeginSession();
+
+ // Store 1 record in the past.
+ performWait(storeRunnable(session, PasswordHelpers.createPassword1()));
+
+ String[] expected = {};
+ performWait(guidsSinceRunnable(session, System.currentTimeMillis() + 1000, expected));
+ dispose(session);
+ }
+
+ public void testFetchSinceOneRecord() {
+ RepositorySession session = createAndBeginSession();
+
+ // Passwords fetchSince checks timePasswordChanged, not insertion time.
+ PasswordRecord record1 = PasswordHelpers.createPassword1();
+ long timeModified1 = updatePassword(NEW_PASSWORD1, record1);
+ performWait(storeRunnable(session, record1));
+
+ PasswordRecord record2 = PasswordHelpers.createPassword2();
+ long timeModified2 = updatePassword(NEW_PASSWORD2, record2);
+ performWait(storeRunnable(session, record2));
+
+ String[] expectedOne = new String[] { record2.guid };
+ performWait(fetchSinceRunnable(session, timeModified2 - 10, expectedOne));
+
+ String[] expectedBoth = new String[] { record1.guid, record2.guid };
+ performWait(fetchSinceRunnable(session, timeModified1 - 10, expectedBoth));
+
+ dispose(session);
+ }
+
+ public void testFetchSinceReturnNoRecords() {
+ RepositorySession session = createAndBeginSession();
+
+ performWait(storeRunnable(session, PasswordHelpers.createPassword2()));
+
+ long timestamp = System.currentTimeMillis();
+
+ performWait(fetchSinceRunnable(session, timestamp + 2000, new String[] {}));
+ dispose(session);
+ }
+
+ public void testFetchOneRecordByGuid() {
+ RepositorySession session = createAndBeginSession();
+ Record record = PasswordHelpers.createPassword1();
+ performWait(storeRunnable(session, record));
+ performWait(storeRunnable(session, PasswordHelpers.createPassword2()));
+
+ String[] guids = new String[] { record.guid };
+ Record[] expected = new Record[] { record };
+ performWait(fetchRunnable(session, guids, expected));
+ dispose(session);
+ }
+
+ public void testFetchMultipleRecordsByGuids() {
+ RepositorySession session = createAndBeginSession();
+ PasswordRecord record1 = PasswordHelpers.createPassword1();
+ PasswordRecord record2 = PasswordHelpers.createPassword2();
+ PasswordRecord record3 = PasswordHelpers.createPassword3();
+
+ performWait(storeRunnable(session, record1));
+ performWait(storeRunnable(session, record2));
+ performWait(storeRunnable(session, record3));
+
+ String[] guids = new String[] { record1.guid, record2.guid };
+ Record[] expected = new Record[] { record1, record2 };
+ performWait(fetchRunnable(session, guids, expected));
+ dispose(session);
+ }
+
+ public void testFetchNoRecordByGuid() {
+ RepositorySession session = createAndBeginSession();
+ Record record = PasswordHelpers.createPassword1();
+
+ performWait(storeRunnable(session, record));
+ performWait(fetchRunnable(session,
+ new String[] { Utils.generateGuid() },
+ new Record[] {}));
+ dispose(session);
+ }
+
+ public void testStore() {
+ final RepositorySession session = createAndBeginSession();
+ performWait(storeRunnable(session, PasswordHelpers.createPassword1()));
+ dispose(session);
+ }
+
+ public void testRemoteNewerTimeStamp() {
+ final RepositorySession session = createAndBeginSession();
+
+ // Store updated local record.
+ PasswordRecord local = PasswordHelpers.createPassword1();
+ updatePassword(NEW_PASSWORD1, local, System.currentTimeMillis() - 1000);
+ performWait(storeRunnable(session, local));
+
+ // Sync a remote record version that is newer.
+ PasswordRecord remote = PasswordHelpers.createPassword2();
+ remote.guid = local.guid;
+ updatePassword(NEW_PASSWORD2, remote);
+ performWait(storeRunnable(session, remote));
+
+ // Make a fetch, expecting only the newer (remote) record.
+ performWait(fetchAllRunnable(session, new Record[] { remote }));
+
+ // Store an older local record.
+ PasswordRecord local2 = PasswordHelpers.createPassword3();
+ updatePassword(NEW_PASSWORD2, local2, System.currentTimeMillis() - 1000);
+ performWait(storeRunnable(session, local2));
+
+ // Sync a remote record version that is newer and is deleted.
+ PasswordRecord remote2 = PasswordHelpers.createPassword3();
+ remote2.guid = local2.guid;
+ remote2.deleted = true;
+ updatePassword(NEW_PASSWORD2, remote2);
+ performWait(storeRunnable(session, remote2));
+
+ // Make a fetch, expecting the local record to be deleted.
+ performWait(fetchRunnable(session, new String[] { remote2.guid }, new Record[] {}));
+
+ // Store an older deleted local record.
+ PasswordRecord local3 = PasswordHelpers.createPassword4();
+ updatePassword(NEW_PASSWORD2, local3, System.currentTimeMillis() - 1000);
+ local3.deleted = true;
+ storeLocalDeletedRecord(local3, System.currentTimeMillis() - 1000);
+
+ // Sync a remote record version that is newer and is deleted.
+ PasswordRecord remote3 = PasswordHelpers.createPassword5();
+ remote3.guid = local3.guid;
+ remote3.deleted = true;
+ updatePassword(NEW_PASSWORD2, remote3);
+ performWait(storeRunnable(session, remote3));
+
+ // Make a fetch, expecting the local record to be deleted.
+ performWait(fetchRunnable(session, new String[] { remote3.guid }, new Record[] {}));
+ dispose(session);
+ }
+
+ public void testLocalNewerTimeStamp() {
+ final RepositorySession session = createAndBeginSession();
+ // Remote record updated before local record.
+ PasswordRecord remote = PasswordHelpers.createPassword1();
+ updatePassword(NEW_PASSWORD1, remote, System.currentTimeMillis() - 1000);
+
+ // Store updated local record.
+ PasswordRecord local = PasswordHelpers.createPassword2();
+ updatePassword(NEW_PASSWORD2, local);
+ performWait(storeRunnable(session, local));
+
+ // Sync a remote record version that is older.
+ remote.guid = local.guid;
+ performWait(storeRunnable(session, remote));
+
+ // Make a fetch, expecting only the newer (local) record.
+ performWait(fetchAllRunnable(session, new Record[] { local }));
+
+ // Remote record updated before local record.
+ PasswordRecord remote2 = PasswordHelpers.createPassword3();
+ updatePassword(NEW_PASSWORD1, remote2, System.currentTimeMillis() - 1000);
+
+ // Store updated local record that is deleted.
+ PasswordRecord local2 = PasswordHelpers.createPassword3();
+ updatePassword(NEW_PASSWORD2, local2);
+ local2.deleted = true;
+ storeLocalDeletedRecord(local2, System.currentTimeMillis());
+
+ // Sync a remote record version that is older.
+ remote2.guid = local2.guid;
+ performWait(storeRunnable(session, remote2, new ExpectNoStoreDelegate()));
+
+ // Make a fetch, expecting only the deleted newer (local) record.
+ performWait(fetchRunnable(session, new String[] { local2.guid }, new Record[] { local2 }));
+
+ // Remote record updated before local record.
+ PasswordRecord remote3 = PasswordHelpers.createPassword4();
+ updatePassword(NEW_PASSWORD1, remote3, System.currentTimeMillis() - 1000);
+
+ // Store updated local record that is deleted.
+ PasswordRecord local3 = PasswordHelpers.createPassword4();
+ updatePassword(NEW_PASSWORD2, local3);
+ local3.deleted = true;
+ storeLocalDeletedRecord(local3, System.currentTimeMillis());
+
+ // Sync a remote record version that is older and is deleted.
+ remote3.guid = local3.guid;
+ remote3.deleted = true;
+ performWait(storeRunnable(session, remote3));
+
+ // Make a fetch, expecting the local record to be deleted.
+ performWait(fetchRunnable(session, new String[] { local3.guid }, new Record[] {}));
+ dispose(session);
+ }
+
+ /*
+ * Store two records that are identical except for guid. Expect to find the
+ * remote one after reconciling.
+ */
+ public void testStoreIdenticalExceptGuid() {
+ RepositorySession session = createAndBeginSession();
+ PasswordRecord record = PasswordHelpers.createPassword1();
+ record.guid = "before1";
+ // Store record.
+ performWait(storeRunnable(session, record));
+
+ // Store same record, but with different guid.
+ record.guid = Utils.generateGuid();
+ performWait(storeRunnable(session, record));
+
+ performWait(fetchAllRunnable(session, new Record[] { record }));
+ dispose(session);
+
+ session = createAndBeginSession();
+
+ PasswordRecord record2 = PasswordHelpers.createPassword2();
+ record2.guid = "before2";
+ // Store record.
+ performWait(storeRunnable(session, record2));
+
+ // Store same record, but with different guid.
+ record2.guid = Utils.generateGuid();
+ performWait(storeRunnable(session, record2));
+
+ performWait(fetchAllRunnable(session, new Record[] { record, record2 }));
+ dispose(session);
+ }
+
+ /*
+ * Store two records that are identical except for guid when they both point
+ * to the same site and there are multiple records for that site. Expect to
+ * find the remote one after reconciling.
+ */
+ public void testStoreIdenticalExceptGuidOnSameSite() {
+ RepositorySession session = createAndBeginSession();
+ PasswordRecord record1 = PasswordHelpers.createPassword1();
+ record1.encryptedUsername = "original";
+ record1.guid = "before1";
+ PasswordRecord record2 = PasswordHelpers.createPassword1();
+ record2.encryptedUsername = "different";
+ record1.guid = "before2";
+ // Store records.
+ performWait(storeRunnable(session, record1));
+ performWait(storeRunnable(session, record2));
+ performWait(fetchAllRunnable(session, new Record[] { record1, record2 }));
+
+ dispose(session);
+ session = createAndBeginSession();
+
+ // Store same records, but with different guids.
+ record1.guid = Utils.generateGuid();
+ performWait(storeRunnable(session, record1));
+ performWait(fetchAllRunnable(session, new Record[] { record1, record2 }));
+
+ record2.guid = Utils.generateGuid();
+ performWait(storeRunnable(session, record2));
+ performWait(fetchAllRunnable(session, new Record[] { record1, record2 }));
+
+ dispose(session);
+ }
+
+ public void testRawFetch() throws RemoteException {
+ RepositorySession session = createAndBeginSession();
+ Record[] expected = new Record[] { PasswordHelpers.createPassword1(),
+ PasswordHelpers.createPassword2() };
+
+ performWait(storeRunnable(session, expected[0]));
+ performWait(storeRunnable(session, expected[1]));
+
+ ContentProviderClient client = getApplicationContext().getContentResolver().acquireContentProviderClient(BrowserContract.PASSWORDS_AUTHORITY_URI);
+ Cursor cursor = client.query(BrowserContractHelpers.PASSWORDS_CONTENT_URI, null, null, null, null);
+ assertEquals(2, cursor.getCount());
+ cursor.moveToFirst();
+ Set<String> guids = new HashSet<String>();
+ while (!cursor.isAfterLast()) {
+ String guid = RepoUtils.getStringFromCursor(cursor, BrowserContract.Passwords.GUID);
+ guids.add(guid);
+ cursor.moveToNext();
+ }
+ cursor.close();
+ assertEquals(2, guids.size());
+ assertTrue(guids.contains(expected[0].guid));
+ assertTrue(guids.contains(expected[1].guid));
+ dispose(session);
+ }
+
+ // Helper methods.
+ private RepositorySession createAndBeginSession() {
+ return SessionTestHelper.createAndBeginSession(
+ getApplicationContext(),
+ getRepository());
+ }
+
+ private Repository getRepository() {
+ /**
+ * Override this chain in order to avoid our test code having to create two
+ * sessions all the time. Don't track records, so they filtering doesn't happen.
+ */
+ return new PasswordsRepositorySession.PasswordsRepository() {
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ PasswordsRepositorySession session;
+ session = new PasswordsRepositorySession(this, context) {
+ @Override
+ protected synchronized void trackGUID(String guid) {
+ }
+ };
+ delegate.onSessionCreated(session);
+ }
+ };
+ }
+
+ private void wipe() {
+ Context context = getApplicationContext();
+ context.getContentResolver().delete(BrowserContractHelpers.PASSWORDS_CONTENT_URI, null, null);
+ context.getContentResolver().delete(BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, null, null);
+ }
+
+ private void storeLocalDeletedRecord(Record record, long time) {
+ // Wipe data-store
+ wipe();
+ // Store record in deleted table.
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(BrowserContract.DeletedColumns.GUID, record.guid);
+ contentValues.put(BrowserContract.DeletedColumns.TIME_DELETED, time);
+ contentValues.put(BrowserContract.DeletedColumns.ID, record.androidID);
+ getApplicationContext().getContentResolver().insert(BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, contentValues);
+ }
+
+ private static void dispose(RepositorySession session) {
+ if (session != null) {
+ session.abort();
+ }
+ }
+
+ private static long updatePassword(String password, PasswordRecord record, long timestamp) {
+ record.encryptedPassword = password;
+ long modifiedTime = System.currentTimeMillis();
+ record.timePasswordChanged = record.lastModified = modifiedTime;
+ return modifiedTime;
+ }
+
+ private static long updatePassword(String password, PasswordRecord record) {
+ return updatePassword(password, record, System.currentTimeMillis());
+ }
+
+ // Runnable Helpers.
+ private static Runnable storeRunnable(final RepositorySession session, final Record record) {
+ return storeRunnable(session, record, new ExpectStoredDelegate(record.guid));
+ }
+
+ private static Runnable storeRunnable(final RepositorySession session, final Record record, final RepositorySessionStoreDelegate delegate) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ session.setStoreDelegate(delegate);
+ try {
+ session.store(record);
+ session.storeDone();
+ } catch (NoStoreDelegateException e) {
+ fail("NoStoreDelegateException should not occur.");
+ }
+ }
+ };
+ }
+
+ private static Runnable fetchAllRunnable(final RepositorySession session, final Record[] records) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ session.fetchAll(new ExpectFetchDelegate(records));
+ }
+ };
+ }
+
+ private static Runnable guidsSinceRunnable(final RepositorySession session, final long timestamp, final String[] expected) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ session.guidsSince(timestamp, new ExpectGuidsSinceDelegate(expected));
+ }
+ };
+ }
+
+ private static Runnable fetchSinceRunnable(final RepositorySession session, final long timestamp, final String[] expected) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ session.fetchSince(timestamp, new ExpectFetchSinceDelegate(timestamp, expected));
+ }
+ };
+ }
+
+ private static Runnable fetchRunnable(final RepositorySession session, final String[] guids, final Record[] expected) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ try {
+ session.fetch(guids, new ExpectFetchDelegate(expected));
+ } catch (InactiveSessionException e) {
+ performNotify(e);
+ }
+ }
+ };
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestTopSites.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestTopSites.java
new file mode 100644
index 0000000000..003fc71725
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestTopSites.java
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.db;
+
+
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.BrowserContract.Combined;
+import org.mozilla.gecko.db.SuggestedSites;
+import org.mozilla.gecko.sync.setup.Constants;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.test.ActivityInstrumentationTestCase2;
+
+/**
+ * Exercise BrowserDB's getTopSites
+ *
+ * @author ahunt
+ *
+ */
+public class TestTopSites extends ActivityInstrumentationTestCase2<Activity> {
+ Context mContext;
+ SuggestedSites mSuggestedSites;
+
+ public TestTopSites() {
+ super(Activity.class);
+ }
+
+ @Override
+ public void setUp() {
+ mContext = getInstrumentation().getTargetContext();
+ mSuggestedSites = new SuggestedSites(mContext);
+
+ // By default we're using StubBrowserDB which has no suggested sites available.
+ BrowserDB.from(GeckoProfile.get(mContext, Constants.DEFAULT_PROFILE)).setSuggestedSites(mSuggestedSites);
+ }
+
+ @Override
+ public void tearDown() {
+ BrowserDB.from(GeckoProfile.get(mContext, Constants.DEFAULT_PROFILE)).setSuggestedSites(null);
+ }
+
+ public void testGetTopSites() {
+ final int SUGGESTED_LIMIT = 6;
+ final int TOTAL_LIMIT = 50;
+
+ ContentResolver cr = mContext.getContentResolver();
+
+ final Uri uri = BrowserContract.TopSites.CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_PROFILE,
+ Constants.DEFAULT_PROFILE)
+ .appendQueryParameter(BrowserContract.PARAM_LIMIT,
+ String.valueOf(SUGGESTED_LIMIT))
+ .appendQueryParameter(BrowserContract.PARAM_SUGGESTEDSITES_LIMIT,
+ String.valueOf(TOTAL_LIMIT))
+ .build();
+
+ final Cursor c = cr.query(uri,
+ new String[] { Combined._ID,
+ Combined.URL,
+ Combined.TITLE,
+ Combined.BOOKMARK_ID,
+ Combined.HISTORY_ID },
+ null,
+ null,
+ null);
+
+ int suggestedCount = 0;
+ try {
+ while (c.moveToNext()) {
+ int type = c.getInt(c.getColumnIndexOrThrow(BrowserContract.Bookmarks.TYPE));
+ assertEquals(BrowserContract.TopSites.TYPE_SUGGESTED, type);
+ suggestedCount++;
+ }
+ } finally {
+ c.close();
+ }
+
+ Cursor suggestedSitesCursor = mSuggestedSites.get(SUGGESTED_LIMIT);
+
+ assertEquals(suggestedSitesCursor.getCount(), suggestedCount);
+
+ suggestedSitesCursor.close();
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/TestAccountLoader.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/TestAccountLoader.java
new file mode 100644
index 0000000000..3009cac3e7
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/TestAccountLoader.java
@@ -0,0 +1,163 @@
+/*
+ * 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.background.fxa;
+
+import android.accounts.Account;
+import android.content.Context;
+import android.content.Loader;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+import org.mozilla.gecko.background.sync.AndroidSyncTestCaseWithAccounts;
+import org.mozilla.gecko.fxa.AccountLoader;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.Separated;
+import org.mozilla.gecko.fxa.login.State;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A version of https://android.googlesource.com/platform/frameworks/base/+/c91893511dc1b9e634648406c9ae61b15476e65d/test-runner/src/android/test/LoaderTestCase.java,
+ * hacked to work with the v4 support library, and patched to work around
+ * https://code.google.com/p/android/issues/detail?id=40987.
+ */
+public class TestAccountLoader extends AndroidSyncTestCaseWithAccounts {
+ // Test account names must start with TEST_USERNAME in order to be recognized
+ // as test accounts and deleted in tearDown.
+ private static final String TEST_USERNAME = "testAccount@mozilla.com";
+ private static final String TEST_ACCOUNTTYPE = FxAccountConstants.ACCOUNT_TYPE;
+
+ private static final String TEST_SYNCKEY = "testSyncKey";
+ private static final String TEST_SYNCPASSWORD = "testSyncPassword";
+
+ private static final String TEST_TOKEN_SERVER_URI = "testTokenServerURI";
+ private static final String TEST_PROFILE_SERVER_URI = "testProfileServerURI";
+ private static final String TEST_AUTH_SERVER_URI = "testAuthServerURI";
+ private static final String TEST_PROFILE = "testProfile";
+
+ public TestAccountLoader() {
+ super(TEST_ACCOUNTTYPE, TEST_USERNAME);
+ }
+
+ static {
+ // Force class loading of AsyncTask on the main thread so that it's handlers are tied to
+ // the main thread and responses from the worker thread get delivered on the main thread.
+ // The tests are run on another thread, allowing them to block waiting on a response from
+ // the code running on the main thread. The main thread can't block since the AsyncTask
+ // results come in via the event loop.
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... args) {
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ }
+ };
+ }
+
+ /**
+ * Runs a Loader synchronously and returns the result of the load. The loader will
+ * be started, stopped, and destroyed by this method so it cannot be reused.
+ *
+ * @param loader The loader to run synchronously
+ * @return The result from the loader
+ */
+ public <T> T getLoaderResultSynchronously(final Loader<T> loader) {
+ // The test thread blocks on this queue until the loader puts it's result in
+ final ArrayBlockingQueue<AtomicReference<T>> queue = new ArrayBlockingQueue<AtomicReference<T>>(1);
+
+ // This callback runs on the "main" thread and unblocks the test thread
+ // when it puts the result into the blocking queue
+ final Loader.OnLoadCompleteListener<T> listener = new Loader.OnLoadCompleteListener<T>() {
+ @Override
+ public void onLoadComplete(Loader<T> completedLoader, T data) {
+ // Shut the loader down
+ completedLoader.unregisterListener(this);
+ completedLoader.stopLoading();
+ completedLoader.reset();
+ // Store the result, unblocking the test thread
+ queue.add(new AtomicReference<T>(data));
+ }
+ };
+
+ // This handler runs on the "main" thread of the process since AsyncTask
+ // is documented as needing to run on the main thread and many Loaders use
+ // AsyncTask
+ final Handler mainThreadHandler = new Handler(Looper.getMainLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ loader.registerListener(0, listener);
+ loader.startLoading();
+ }
+ };
+
+ // Ask the main thread to start the loading process
+ mainThreadHandler.sendEmptyMessage(0);
+
+ // Block on the queue waiting for the result of the load to be inserted
+ T result;
+ while (true) {
+ try {
+ result = queue.take().get();
+ break;
+ } catch (InterruptedException e) {
+ throw new RuntimeException("waiting thread interrupted", e);
+ }
+ }
+ return result;
+ }
+
+ public void testInitialLoad() throws UnsupportedEncodingException, GeneralSecurityException, URISyntaxException {
+ // This is tricky. We can't mock the AccountManager easily -- see
+ // https://groups.google.com/d/msg/android-mock/VXyzvKTMUGs/Y26wVPrl50sJ --
+ // and we don't want to delete any existing accounts on device. So our test
+ // needs to be adaptive (and therefore a little race-prone).
+
+ final Context context = getApplicationContext();
+ final AccountLoader loader = new AccountLoader(context);
+
+ final boolean firefoxAccountsExist = FirefoxAccounts.firefoxAccountsExist(context);
+
+ if (firefoxAccountsExist) {
+ assertFirefoxAccount(getLoaderResultSynchronously((Loader<Account>) loader));
+ return;
+ }
+
+ // This account will get cleaned up in tearDown.
+ final State state = new Separated(TEST_USERNAME, "uid", false); // State choice is arbitrary.
+ final AndroidFxAccount account = AndroidFxAccount.addAndroidAccount(context,
+ TEST_USERNAME, TEST_PROFILE, TEST_AUTH_SERVER_URI, TEST_TOKEN_SERVER_URI, TEST_PROFILE_SERVER_URI,
+ state, AndroidSyncTestCaseWithAccounts.TEST_SYNC_AUTOMATICALLY_MAP_WITH_ALL_AUTHORITIES_DISABLED);
+ assertNotNull(account);
+ assertFirefoxAccount(getLoaderResultSynchronously((Loader<Account>) loader));
+ }
+
+ protected void assertFirefoxAccount(Account account) {
+ assertNotNull(account);
+ assertEquals(FxAccountConstants.ACCOUNT_TYPE, account.type);
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/TestBrowserIDKeyPairGeneration.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/TestBrowserIDKeyPairGeneration.java
new file mode 100644
index 0000000000..18fb58a974
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/TestBrowserIDKeyPairGeneration.java
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.fxa;
+
+import java.security.GeneralSecurityException;
+
+import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
+import org.mozilla.gecko.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.browserid.DSACryptoImplementation;
+import org.mozilla.gecko.browserid.JSONWebTokenUtils;
+import org.mozilla.gecko.browserid.RSACryptoImplementation;
+import org.mozilla.gecko.browserid.SigningPrivateKey;
+import org.mozilla.gecko.browserid.VerifyingPublicKey;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.Utils;
+
+public class TestBrowserIDKeyPairGeneration extends AndroidSyncTestCase {
+ public void doTestEncodeDecode(BrowserIDKeyPair keyPair) throws Exception {
+ SigningPrivateKey privateKey = keyPair.getPrivate();
+ VerifyingPublicKey publicKey = keyPair.getPublic();
+
+ ExtendedJSONObject o = new ExtendedJSONObject();
+ o.put("key", Utils.generateGuid());
+
+ String token = JSONWebTokenUtils.encode(o.toJSONString(), privateKey);
+ assertNotNull(token);
+
+ String payload = JSONWebTokenUtils.decode(token, publicKey);
+ assertEquals(o.toJSONString(), payload);
+
+ try {
+ JSONWebTokenUtils.decode(token + "x", publicKey);
+ fail("Expected exception.");
+ } catch (GeneralSecurityException e) {
+ // Do nothing.
+ }
+ }
+
+ public void testEncodeDecodeSuccessRSA() throws Exception {
+ doTestEncodeDecode(RSACryptoImplementation.generateKeyPair(1024));
+ doTestEncodeDecode(RSACryptoImplementation.generateKeyPair(2048));
+ }
+
+ public void testEncodeDecodeSuccessDSA() throws Exception {
+ doTestEncodeDecode(DSACryptoImplementation.generateKeyPair(512));
+ doTestEncodeDecode(DSACryptoImplementation.generateKeyPair(1024));
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java
new file mode 100644
index 0000000000..d50bd47e00
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.fxa.authenticator;
+
+import org.mozilla.gecko.background.sync.AndroidSyncTestCaseWithAccounts;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.authenticator.AccountPickler;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.Separated;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.Utils;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.test.RenamingDelegatingContext;
+
+public class TestAccountPickler extends AndroidSyncTestCaseWithAccounts {
+ private static final String TEST_TOKEN_SERVER_URI = "tokenServerURI";
+ private static final String TEST_PROFILE_SERVER_URI = "profileServerURI";
+ private static final String TEST_AUTH_SERVER_URI = "serverURI";
+ private static final String TEST_PROFILE = "profile";
+ private final static String FILENAME_PREFIX = "TestAccountPickler-";
+ private final static String PICKLE_FILENAME = "pickle";
+
+ private final static String TEST_ACCOUNTTYPE = FxAccountConstants.ACCOUNT_TYPE;
+
+ // Test account names must start with TEST_USERNAME in order to be recognized
+ // as test accounts and deleted in tearDown.
+ public static final String TEST_USERNAME = "testFirefoxAccount@mozilla.com";
+
+ public Account account;
+ public RenamingDelegatingContext context;
+
+ public TestAccountPickler() {
+ super(TEST_ACCOUNTTYPE, TEST_USERNAME);
+ }
+
+ @Override
+ public void setUp() {
+ super.setUp();
+ this.account = null;
+ // Randomize the filename prefix in case we don't clean up correctly.
+ this.context = new RenamingDelegatingContext(getApplicationContext(), FILENAME_PREFIX +
+ Math.random() * 1000001 + "-");
+ this.accountManager = AccountManager.get(context);
+ }
+
+ @Override
+ public void tearDown() {
+ super.tearDown();
+ this.context.deleteFile(PICKLE_FILENAME);
+ }
+
+ public AndroidFxAccount addTestAccount() throws Exception {
+ final State state = new Separated(TEST_USERNAME, "uid", false); // State choice is arbitrary.
+ final AndroidFxAccount account = AndroidFxAccount.addAndroidAccount(context, TEST_USERNAME,
+ TEST_PROFILE, TEST_AUTH_SERVER_URI, TEST_TOKEN_SERVER_URI, TEST_PROFILE_SERVER_URI, state,
+ AndroidSyncTestCaseWithAccounts.TEST_SYNC_AUTOMATICALLY_MAP_WITH_ALL_AUTHORITIES_DISABLED);
+ assertNotNull(account);
+ assertNotNull(account.getProfile());
+ assertTrue(testAccountsExist()); // Sanity check.
+ this.account = account.getAndroidAccount(); // To remove in tearDown() if we throw.
+ return account;
+ }
+
+ public void testPickle() throws Exception {
+ final AndroidFxAccount account = addTestAccount();
+
+ final long now = System.currentTimeMillis();
+ final ExtendedJSONObject o = AccountPickler.toJSON(account, now);
+ assertNotNull(o.toJSONString());
+
+ assertEquals(3, o.getLong(AccountPickler.KEY_PICKLE_VERSION).longValue());
+ assertTrue(o.getLong(AccountPickler.KEY_PICKLE_TIMESTAMP).longValue() < System.currentTimeMillis());
+
+ assertEquals(AndroidFxAccount.CURRENT_ACCOUNT_VERSION, o.getIntegerSafely(AccountPickler.KEY_ACCOUNT_VERSION).intValue());
+ assertEquals(FxAccountConstants.ACCOUNT_TYPE, o.getString(AccountPickler.KEY_ACCOUNT_TYPE));
+
+ assertEquals(TEST_USERNAME, o.getString(AccountPickler.KEY_EMAIL));
+ assertEquals(TEST_PROFILE, o.getString(AccountPickler.KEY_PROFILE));
+ assertEquals(TEST_AUTH_SERVER_URI, o.getString(AccountPickler.KEY_IDP_SERVER_URI));
+ assertEquals(TEST_TOKEN_SERVER_URI, o.getString(AccountPickler.KEY_TOKEN_SERVER_URI));
+ assertEquals(TEST_PROFILE_SERVER_URI, o.getString(AccountPickler.KEY_PROFILE_SERVER_URI));
+
+ assertNotNull(o.getObject(AccountPickler.KEY_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP));
+ assertNotNull(o.get(AccountPickler.KEY_BUNDLE));
+ }
+
+ public void testPickleAndUnpickle() throws Exception {
+ final AndroidFxAccount inputAccount = addTestAccount();
+
+ AccountPickler.pickle(inputAccount, PICKLE_FILENAME);
+ final ExtendedJSONObject inputJSON = AccountPickler.toJSON(inputAccount, 0);
+ final State inputState = inputAccount.getState();
+ assertNotNull(inputJSON);
+ assertNotNull(inputState);
+
+ // unpickle adds an account to the AccountManager so delete it first.
+ deleteTestAccounts();
+ assertFalse(testAccountsExist());
+
+ final AndroidFxAccount unpickledAccount = AccountPickler.unpickle(context, PICKLE_FILENAME);
+ assertNotNull(unpickledAccount);
+ final ExtendedJSONObject unpickledJSON = AccountPickler.toJSON(unpickledAccount, 0);
+ final State unpickledState = unpickledAccount.getState();
+ assertNotNull(unpickledJSON);
+ assertNotNull(unpickledState);
+
+ assertEquals(inputJSON, unpickledJSON);
+ assertStateEquals(inputState, unpickledState);
+ }
+
+ public void testDeletePickle() throws Exception {
+ final AndroidFxAccount account = addTestAccount();
+ AccountPickler.pickle(account, PICKLE_FILENAME);
+
+ final String s = Utils.readFile(context, PICKLE_FILENAME);
+ assertNotNull(s);
+ assertTrue(s.length() > 0);
+
+ AccountPickler.deletePickle(context, PICKLE_FILENAME);
+ assertFileNotPresent(context, PICKLE_FILENAME);
+ }
+
+ private void assertStateEquals(final State expected, final State actual) throws Exception {
+ // TODO: Write and use State.equals. Thus, this is only thorough for the State base class.
+ assertEquals(expected.getStateLabel(), actual.getStateLabel());
+ assertEquals(expected.email, actual.email);
+ assertEquals(expected.uid, actual.uid);
+ assertEquals(expected.verified, actual.verified);
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/AndroidSyncTestCase.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/AndroidSyncTestCase.java
new file mode 100644
index 0000000000..5cdbe44f4e
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/AndroidSyncTestCase.java
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.helpers;
+
+import junit.framework.AssertionFailedError;
+
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+
+import android.app.Activity;
+import android.content.Context;
+import android.test.ActivityInstrumentationTestCase2;
+
+/**
+ * AndroidSyncTestCase provides helper methods for testing.
+ */
+public class AndroidSyncTestCase extends ActivityInstrumentationTestCase2<Activity> {
+ protected static String LOG_TAG = "AndroidSyncTestCase";
+
+ public AndroidSyncTestCase() {
+ super(Activity.class);
+ WaitHelper.resetTestWaiter();
+ }
+
+ public Context getApplicationContext() {
+ return this.getInstrumentation().getTargetContext();
+ }
+
+ public static void performWait(Runnable runnable) {
+ try {
+ WaitHelper.getTestWaiter().performWait(runnable);
+ } catch (WaitHelper.InnerError e) {
+ AssertionFailedError inner = new AssertionFailedError("Caught error in performWait");
+ inner.initCause(e.innerError);
+ throw inner;
+ }
+ }
+
+ public static void performNotify() {
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ public static void performNotify(Throwable e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+
+ public static void performNotify(String reason, Throwable e) {
+ AssertionFailedError er = new AssertionFailedError(reason + ": " + e.getMessage());
+ er.initCause(e);
+ WaitHelper.getTestWaiter().performNotify(er);
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/DBHelpers.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/DBHelpers.java
new file mode 100644
index 0000000000..c37f1e8bdc
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/DBHelpers.java
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.helpers;
+
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import junit.framework.Assert;
+
+public class DBHelpers {
+
+ /*
+ * Works for strings and int-ish values.
+ */
+ public static void assertCursorContains(Object[][] expected, Cursor actual) {
+ Assert.assertEquals(expected.length, actual.getCount());
+ int i = 0, j = 0;
+ Object[] row;
+
+ do {
+ row = expected[i];
+ for (j = 0; j < row.length; ++j) {
+ Object atIndex = row[j];
+ if (atIndex == null) {
+ continue;
+ }
+ if (atIndex instanceof String) {
+ Assert.assertEquals(atIndex, actual.getString(j));
+ } else {
+ Assert.assertEquals(atIndex, actual.getInt(j));
+ }
+ }
+ ++i;
+ } while (actual.moveToPosition(i));
+ }
+
+ public static int getRowCount(SQLiteDatabase db, String table) {
+ return getRowCount(db, table, null, null);
+ }
+
+ public static int getRowCount(SQLiteDatabase db, String table, String selection, String[] selectionArgs) {
+ final Cursor c = db.query(table, null, selection, selectionArgs, null, null, null);
+ try {
+ return c.getCount();
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Returns an ID that is non-existent in the given sqlite table. Assumes that a column named
+ * "id" exists.
+ */
+ public static int getNonExistentID(SQLiteDatabase db, String table) {
+ // XXX: We should use selectionArgs to concatenate table, but sqlite throws a syntax error on
+ // "?" because it wants to ensure id is a valid column in table.
+ final Cursor c = db.rawQuery("SELECT MAX(id) + 1 FROM " + table, null);
+ try {
+ if (!c.moveToNext()) {
+ return 0;
+ }
+ return c.getInt(0);
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Returns an ID that exists in the given sqlite table. Assumes that a column named * "id"
+ * exists.
+ */
+ public static long getExistentID(SQLiteDatabase db, String table) {
+ final Cursor c = db.query(table, new String[] {"id"}, null, null, null, null, null, "1");
+ try {
+ if (!c.moveToNext()) {
+ throw new IllegalStateException("Given table does not contain any entries.");
+ }
+ return c.getInt(0);
+ } finally {
+ c.close();
+ }
+ }
+
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/DBProviderTestCase.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/DBProviderTestCase.java
new file mode 100644
index 0000000000..ccfeb8d639
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/DBProviderTestCase.java
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.helpers;
+
+import java.io.File;
+
+import android.content.ContentProvider;
+import android.content.Context;
+import android.test.AndroidTestCase;
+import android.test.mock.MockContentResolver;
+
+/**
+ * Because ProviderTestCase2 is unable to handle custom DB paths.
+ */
+public abstract class DBProviderTestCase<T extends ContentProvider> extends
+ AndroidTestCase {
+
+ Class<T> providerClass;
+ String providerAuthority;
+
+ protected File fakeProfileDirectory;
+ private MockContentResolver resolver;
+ private T provider;
+
+ public DBProviderTestCase(Class<T> providerClass, String providerAuthority) {
+ this.providerClass = providerClass;
+ this.providerAuthority = providerAuthority;
+ }
+
+ public T getProvider() {
+ return provider;
+ }
+
+ public MockContentResolver getMockContentResolver() {
+ return resolver;
+ }
+
+ protected String getCacheSuffix() {
+ return this.getClass().getName() + "-" + System.currentTimeMillis();
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ File cache = getContext().getCacheDir();
+ fakeProfileDirectory = new File(cache.getAbsolutePath() + getCacheSuffix());
+ System.out.println("Test: Creating profile directory " + fakeProfileDirectory.getAbsolutePath());
+ if (!fakeProfileDirectory.mkdir()) {
+ throw new IllegalStateException("Could not create temporary directory.");
+ }
+
+ final Context context = getContext();
+ assertNotNull(context);
+ resolver = new MockContentResolver();
+ provider = providerClass.newInstance();
+ provider.attachInfo(context, null);
+ assertNotNull(provider);
+ resolver.addProvider(providerAuthority, getProvider());
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ // We don't check return values.
+ System.out.println("Test: Cleaning up " + fakeProfileDirectory.getAbsolutePath());
+ for (File child : fakeProfileDirectory.listFiles()) {
+ child.delete();
+ }
+ fakeProfileDirectory.delete();
+ super.tearDown();
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/nativecode/test/TestNativeCrypto.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/nativecode/test/TestNativeCrypto.java
new file mode 100644
index 0000000000..d24f284914
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/nativecode/test/TestNativeCrypto.java
@@ -0,0 +1,175 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.nativecode.test;
+
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import junit.framework.TestCase;
+
+import org.mozilla.gecko.background.nativecode.NativeCrypto;
+import org.mozilla.gecko.sync.Utils;
+
+/*
+ * Tests the Java wrapper over native implementations of crypto code. Test vectors from:
+ * * PBKDF2SHA256:
+ * - <https://github.com/ircmaxell/PHP-PasswordLib/blob/master/test/Data/Vectors/pbkdf2-draft-josefsson-sha256.test-vectors>
+ * - <https://gitorious.org/scrypt/nettle-scrypt/blobs/37c0d5288e991604fe33dba2f1724986a8dddf56/testsuite/pbkdf2-test.c>
+ * * SHA-1:
+ * - <http://oauth.googlecode.com/svn/code/c/liboauth/src/sha1.c>
+ */
+public class TestNativeCrypto extends TestCase {
+
+ public final void testPBKDF2SHA256A() throws UnsupportedEncodingException, GeneralSecurityException {
+ String p = "password";
+ String s = "salt";
+ int dkLen = 32;
+
+ checkPBKDF2SHA256(p, s, 1, dkLen, "120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b");
+ checkPBKDF2SHA256(p, s, 4096, dkLen, "c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a");
+ }
+
+ public final void testPBKDF2SHA256B() throws UnsupportedEncodingException, GeneralSecurityException {
+ String p = "passwordPASSWORDpassword";
+ String s = "saltSALTsaltSALTsaltSALTsaltSALTsalt";
+ int dkLen = 40;
+
+ checkPBKDF2SHA256(p, s, 4096, dkLen, "348c89dbcbd32b2f32d814b8116e84cf2b17347ebc1800181c4e2a1fb8dd53e1c635518c7dac47e9");
+ }
+
+ public final void testPBKDF2SHA256scryptA() throws UnsupportedEncodingException, GeneralSecurityException {
+ String p = "passwd";
+ String s = "salt";
+ int dkLen = 64;
+
+ checkPBKDF2SHA256(p, s, 1, dkLen, "55ac046e56e3089fec1691c22544b605f94185216dde0465e68b9d57c20dacbc49ca9cccf179b645991664b39d77ef317c71b845b1e30bd509112041d3a19783");
+ }
+
+ public final void testPBKDF2SHA256scryptB() throws UnsupportedEncodingException, GeneralSecurityException {
+ String p = "Password";
+ String s = "NaCl";
+ int dkLen = 64;
+
+ checkPBKDF2SHA256(p, s, 80000, dkLen, "4ddcd8f60b98be21830cee5ef22701f9641a4418d04c0414aeff08876b34ab56a1d425a1225833549adb841b51c9b3176a272bdebba1d078478f62b397f33c8d");
+ }
+
+ public final void testPBKDF2SHA256C() throws UnsupportedEncodingException, GeneralSecurityException {
+ String p = "pass\0word";
+ String s = "sa\0lt";
+ int dkLen = 16;
+
+ checkPBKDF2SHA256(p, s, 4096, dkLen, "89b69d0516f829893c696226650a8687");
+ }
+
+ /*
+ // This test takes two or three minutes to run, so we don't.
+ public final void testPBKDF2SHA256D() throws UnsupportedEncodingException, GeneralSecurityException {
+ String p = "password";
+ String s = "salt";
+ int dkLen = 32;
+
+ checkPBKDF2SHA256(p, s, 16777216, dkLen, "cf81c66fe8cfc04d1f31ecb65dab4089f7f179e89b3b0bcb17ad10e3ac6eba46");
+ }
+ */
+
+ public final void testTimePBKDF2SHA256() throws UnsupportedEncodingException, GeneralSecurityException {
+ checkPBKDF2SHA256("password", "salt", 80000, 32, null);
+ }
+
+ public final void testPBKDF2SHA256InvalidLenArg() throws UnsupportedEncodingException, GeneralSecurityException {
+ final String p = "password";
+ final String s = "salt";
+ final int c = 1;
+ final int dkLen = -1; // Should always be positive.
+
+ try {
+ NativeCrypto.pbkdf2SHA256(p.getBytes("US-ASCII"), s.getBytes("US-ASCII"), c, dkLen);
+ fail("Expected sha256 to throw with negative dkLen argument.");
+ } catch (IllegalArgumentException e) { } // Expected.
+ }
+
+ public final void testSHA1() throws UnsupportedEncodingException {
+ final String[] inputs = new String[] {
+ "abc",
+ "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
+ "" // To be filled in below.
+ };
+ final String baseStr = "01234567";
+ final int repetitions = 80;
+ final StringBuilder builder = new StringBuilder(baseStr.length() * repetitions);
+ for (int i = 0; i < 80; ++i) {
+ builder.append(baseStr);
+ }
+ inputs[2] = builder.toString();
+
+ final String[] expecteds = new String[] {
+ "a9993e364706816aba3e25717850c26c9cd0d89d",
+ "84983e441c3bd26ebaae4aa1f95129e5e54670f1",
+ "dea356a2cddd90c7a7ecedc5ebb563934f460452"
+ };
+
+ for (int i = 0; i < inputs.length; ++i) {
+ final byte[] input = inputs[i].getBytes("US-ASCII");
+ final String expected = expecteds[i];
+
+ final byte[] actual = NativeCrypto.sha1(input);
+ assertNotNull("Hashed value is non-null", actual);
+ assertExpectedBytes(expected, actual);
+ }
+ }
+
+ /**
+ * Test to ensure the output of our SHA1 algo is the same as MessageDigest's. This is important
+ * because we intend to replace MessageDigest in FHR with this SHA-1 algo (bug 959652).
+ */
+ public final void testSHA1AgainstMessageDigest() throws UnsupportedEncodingException,
+ NoSuchAlgorithmException {
+ final String[] inputs = {
+ "password",
+ "saranghae",
+ "aoeusnthaoeusnthaoeusnth \0 12345098765432109876_!"
+ };
+
+ final MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ for (final String input : inputs) {
+ final byte[] inputBytes = input.getBytes("US-ASCII");
+
+ final byte[] mdBytes = digest.digest(inputBytes);
+ final byte[] ourBytes = NativeCrypto.sha1(inputBytes);
+ assertTrue("MessageDigest hash is the same as NativeCrypto SHA-1 hash",
+ Arrays.equals(ourBytes, mdBytes));
+ }
+ }
+
+ private void checkPBKDF2SHA256(String p, String s, int c, int dkLen,
+ final String expectedStr)
+ throws GeneralSecurityException, UnsupportedEncodingException {
+ long start = System.currentTimeMillis();
+ byte[] key = NativeCrypto.pbkdf2SHA256(p.getBytes("US-ASCII"), s.getBytes("US-ASCII"), c, dkLen);
+ assertNotNull(key);
+
+ long end = System.currentTimeMillis();
+
+ System.err.println("SHA-256 " + c + " took " + (end - start) + "ms");
+ if (expectedStr == null) {
+ return;
+ }
+
+ assertEquals(dkLen, Utils.hex2Byte(expectedStr).length);
+ assertExpectedBytes(expectedStr, key);
+ }
+
+ private void assertExpectedBytes(final String expectedStr, byte[] key) {
+ assertEquals(expectedStr, Utils.byte2Hex(key));
+ byte[] expected = Utils.hex2Byte(expectedStr);
+
+ assertEquals(expected.length, key.length);
+ for (int i = 0; i < key.length; i++) {
+ assertEquals(expected[i], key[i]);
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/AndroidSyncTestCaseWithAccounts.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/AndroidSyncTestCaseWithAccounts.java
new file mode 100644
index 0000000000..e0e4e8bb17
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/AndroidSyncTestCaseWithAccounts.java
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerCallback;
+import android.accounts.AccountManagerFuture;
+import android.content.Context;
+import android.test.InstrumentationTestCase;
+
+import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+public class AndroidSyncTestCaseWithAccounts extends AndroidSyncTestCase {
+ public final String testAccountType;
+ public final String testAccountPrefix;
+
+ protected Context context;
+ protected AccountManager accountManager;
+ protected int numAccounts;
+
+ public static final Map<String, Boolean> TEST_SYNC_AUTOMATICALLY_MAP_WITH_ALL_AUTHORITIES_DISABLED;
+ static {
+ final Map<String, Boolean> m = new HashMap<String, Boolean>();
+ for (String authority : AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP.keySet()) {
+ m.put(authority, false);
+ }
+ TEST_SYNC_AUTOMATICALLY_MAP_WITH_ALL_AUTHORITIES_DISABLED = m;
+ }
+
+ public AndroidSyncTestCaseWithAccounts(String accountType, String accountPrefix) {
+ super();
+ this.testAccountType = accountType;
+ this.testAccountPrefix = accountPrefix;
+ }
+
+ @Override
+ public void setUp() {
+ context = getApplicationContext();
+ accountManager = AccountManager.get(context);
+ deleteTestAccounts(); // Always start with no test accounts.
+ numAccounts = accountManager.getAccountsByType(testAccountType).length;
+ }
+
+ public List<Account> getTestAccounts() {
+ final List<Account> testAccounts = new ArrayList<Account>();
+
+ final Account[] accounts = accountManager.getAccountsByType(testAccountType);
+ for (Account account : accounts) {
+ if (account.name.startsWith(testAccountPrefix)) {
+ testAccounts.add(account);
+ }
+ }
+
+ return testAccounts;
+ }
+
+ public static void deleteAccount(final InstrumentationTestCase test, final AccountManager accountManager, final Account account) {
+ performWait(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ test.runTestOnUiThread(new Runnable() {
+ final AccountManagerCallback<Boolean> callback = new AccountManagerCallback<Boolean>() {
+ @Override
+ public void run(AccountManagerFuture<Boolean> future) {
+ try {
+ future.getResult(5L, TimeUnit.SECONDS);
+ } catch (Exception e) {
+ }
+ performNotify();
+ }
+ };
+
+ @Override
+ public void run() {
+ accountManager.removeAccount(account, callback, null);
+ }
+ });
+ } catch (Throwable e) {
+ performNotify(e);
+ }
+ }
+ });
+ }
+
+ public void deleteTestAccounts() {
+ for (Account account : getTestAccounts()) {
+ deleteAccount(this, accountManager, account);
+ }
+ }
+
+ public boolean testAccountsExist() {
+ // Note that we don't use FirefoxAccounts.firefoxAccountsExist because it unpickles.
+ return !getTestAccounts().isEmpty();
+ }
+
+ @Override
+ public void tearDown() {
+ deleteTestAccounts();
+ assertEquals(numAccounts, accountManager.getAccountsByType(testAccountType).length);
+ }
+
+ public static void assertFileNotPresent(final Context context, final String filename) throws Exception {
+ // Verify file is not present.
+ FileInputStream fis = null;
+ try {
+ fis = context.openFileInput(filename);
+ fail("Should get FileNotFoundException.");
+ } catch (FileNotFoundException e) {
+ // Do nothing; file should not exist.
+ } finally {
+ if (fis != null) {
+ fis.close();
+ }
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestClientsStage.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestClientsStage.java
new file mode 100644
index 0000000000..39e24b4d4b
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestClientsStage.java
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync;
+
+import org.json.simple.JSONArray;
+import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
+import org.mozilla.gecko.background.testhelpers.DefaultGlobalSessionCallback;
+import org.mozilla.gecko.background.testhelpers.MockClientsDataDelegate;
+import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.SyncConfiguration;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
+import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
+import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
+import org.mozilla.gecko.sync.stage.SyncClientsEngineStage;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+public class TestClientsStage extends AndroidSyncTestCase {
+ private static final String TEST_USERNAME = "johndoe";
+ private static final String TEST_PASSWORD = "password";
+ private static final String TEST_SYNC_KEY = "abcdeabcdeabcdeabcdeabcdea";
+
+ @Override
+ public void setUp() {
+ ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(getApplicationContext());
+ db.wipeDB();
+ db.close();
+ }
+
+ public void testWipeClearsClients() throws Exception {
+
+ // Wiping clients is equivalent to a reset and dropping all local stored client records.
+ // Resetting is defined as being the same as for other engines -- discard local
+ // and remote timestamps, tracked failed records, and tracked records to fetch.
+
+ final Context context = getApplicationContext();
+ final ClientsDatabaseAccessor dataAccessor = new ClientsDatabaseAccessor(context);
+ final GlobalSessionCallback callback = new DefaultGlobalSessionCallback();
+ final ClientsDataDelegate delegate = new MockClientsDataDelegate();
+
+ final KeyBundle keyBundle = new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY);
+ final AuthHeaderProvider authHeaderProvider = new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD);
+ final SharedPreferences prefs = new MockSharedPreferences();
+ final SyncConfiguration config = new SyncConfiguration(TEST_USERNAME, authHeaderProvider, prefs);
+ config.syncKeyBundle = keyBundle;
+ GlobalSession session = new GlobalSession(config, callback, context, delegate);
+
+ SyncClientsEngineStage stage = new SyncClientsEngineStage() {
+
+ @Override
+ public synchronized ClientsDatabaseAccessor getClientsDatabaseAccessor() {
+ if (db == null) {
+ db = dataAccessor;
+ }
+ return db;
+ }
+ };
+
+ final String guid = "clientabcdef";
+ long lastModified = System.currentTimeMillis();
+ ClientRecord record = new ClientRecord(guid, "clients", lastModified , false);
+ record.name = "John's Phone";
+ record.type = "mobile";
+ record.device = "Some Device";
+ record.os = "iOS";
+ record.commands = new JSONArray();
+
+ dataAccessor.store(record);
+ assertEquals(1, dataAccessor.clientsCount());
+
+ final ClientRecord stored = dataAccessor.fetchAllClients().get(guid);
+ assertNotNull(stored);
+ assertEquals("John's Phone", stored.name);
+ assertEquals("mobile", stored.type);
+ assertEquals("Some Device", stored.device);
+ assertEquals("iOS", stored.os);
+
+ stage.wipeLocal(session);
+
+ try {
+ assertEquals(0, dataAccessor.clientsCount());
+ assertEquals(0L, session.config.getPersistedServerClientRecordTimestamp());
+ assertEquals(0, session.getClientsDelegate().getClientsCount());
+ } finally {
+ dataAccessor.close();
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestResetting.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestResetting.java
new file mode 100644
index 0000000000..52af2ad016
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestResetting.java
@@ -0,0 +1,198 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync;
+
+import android.content.SharedPreferences;
+
+import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
+import org.mozilla.gecko.background.testhelpers.BaseMockServerSyncStage;
+import org.mozilla.gecko.background.testhelpers.DefaultGlobalSessionCallback;
+import org.mozilla.gecko.background.testhelpers.MockRecord;
+import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.background.testhelpers.WBORepository;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.EngineSettings;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.MetaGlobalException;
+import org.mozilla.gecko.sync.SyncConfiguration;
+import org.mozilla.gecko.sync.SynchronizerConfiguration;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+import org.mozilla.gecko.sync.stage.NoSuchStageException;
+import org.mozilla.gecko.sync.synchronizer.Synchronizer;
+
+/**
+ * Test the on-device side effects of reset operations on a stage.
+ *
+ * See also "TestResetCommands" in the unit test suite.
+ */
+public class TestResetting extends AndroidSyncTestCase {
+ private static final String TEST_USERNAME = "johndoe";
+ private static final String TEST_PASSWORD = "password";
+ private static final String TEST_SYNC_KEY = "abcdeabcdeabcdeabcdeabcdea";
+
+ @Override
+ public void setUp() {
+ assertTrue(WaitHelper.getTestWaiter().isIdle());
+ }
+
+ /**
+ * Set up a mock stage that synchronizes two mock repositories. Apply various
+ * reset/sync/wipe permutations and check state.
+ */
+ public void testResetAndWipeStage() throws Exception {
+
+ final long startTime = System.currentTimeMillis();
+ final GlobalSessionCallback callback = createGlobalSessionCallback();
+ final GlobalSession session = createDefaultGlobalSession(callback);
+
+ final ExecutableMockServerSyncStage stage = new ExecutableMockServerSyncStage() {
+ @Override
+ public void onSynchronized(Synchronizer synchronizer) {
+ try {
+ assertTrue(startTime <= synchronizer.bundleA.getTimestamp());
+ assertTrue(startTime <= synchronizer.bundleB.getTimestamp());
+
+ // Call up to allow the usual persistence etc. to happen.
+ super.onSynchronized(synchronizer);
+ } catch (Throwable e) {
+ performNotify(e);
+ return;
+ }
+ performNotify();
+ }
+ };
+
+ final boolean bumpTimestamps = true;
+ WBORepository local = new WBORepository(bumpTimestamps);
+ WBORepository remote = new WBORepository(bumpTimestamps);
+
+ stage.name = "mock";
+ stage.collection = "mock";
+ stage.local = local;
+ stage.remote = remote;
+
+ stage.executeSynchronously(session);
+
+ // Verify the persisted values.
+ assertConfigTimestampsGreaterThan(stage.leakConfig(), startTime, startTime);
+
+ // Reset.
+ stage.resetLocal(session);
+
+ // Verify that they're gone.
+ assertConfigTimestampsEqual(stage.leakConfig(), 0, 0);
+
+ // Now sync data, ensure that timestamps come back.
+ final long afterReset = System.currentTimeMillis();
+ final String recordGUID = "abcdefghijkl";
+ local.wbos.put(recordGUID, new MockRecord(recordGUID, "mock", startTime, false));
+
+ // Sync again with data and verify timestamps and data.
+ stage.executeSynchronously(session);
+
+ assertConfigTimestampsGreaterThan(stage.leakConfig(), afterReset, afterReset);
+ assertEquals(1, remote.wbos.size());
+ assertEquals(1, local.wbos.size());
+
+ Record remoteRecord = remote.wbos.get(recordGUID);
+ assertNotNull(remoteRecord);
+ assertNotNull(local.wbos.get(recordGUID));
+ assertEquals(recordGUID, remoteRecord.guid);
+ assertTrue(afterReset <= remoteRecord.lastModified);
+
+ // Reset doesn't clear data.
+ stage.resetLocal(session);
+ assertConfigTimestampsEqual(stage.leakConfig(), 0, 0);
+ assertEquals(1, remote.wbos.size());
+ assertEquals(1, local.wbos.size());
+ remoteRecord = remote.wbos.get(recordGUID);
+ assertNotNull(remoteRecord);
+ assertNotNull(local.wbos.get(recordGUID));
+
+ // Wipe does. Recover from reset...
+ final long beforeWipe = System.currentTimeMillis();
+ stage.executeSynchronously(session);
+ assertEquals(1, remote.wbos.size());
+ assertEquals(1, local.wbos.size());
+ assertConfigTimestampsGreaterThan(stage.leakConfig(), beforeWipe, beforeWipe);
+
+ // ... then wipe.
+ stage.wipeLocal(session);
+ assertConfigTimestampsEqual(stage.leakConfig(), 0, 0);
+ assertEquals(1, remote.wbos.size()); // We don't wipe the server.
+ assertEquals(0, local.wbos.size()); // We do wipe local.
+ }
+
+ /**
+ * A stage that joins two Repositories with no wrapping.
+ */
+ public class ExecutableMockServerSyncStage extends BaseMockServerSyncStage {
+ /**
+ * Run this stage synchronously.
+ */
+ public void executeSynchronously(final GlobalSession session) {
+ final BaseMockServerSyncStage self = this;
+ performWait(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ self.execute(session);
+ } catch (NoSuchStageException e) {
+ performNotify(e);
+ }
+ }
+ });
+ }
+ }
+
+ private GlobalSession createDefaultGlobalSession(final GlobalSessionCallback callback) throws Exception {
+ final KeyBundle keyBundle = new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY);
+ final AuthHeaderProvider authHeaderProvider = new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD);
+ final SharedPreferences prefs = new MockSharedPreferences();
+ final SyncConfiguration config = new SyncConfiguration(TEST_USERNAME, authHeaderProvider, prefs);
+ config.syncKeyBundle = keyBundle;
+ return new GlobalSession(config, callback, getApplicationContext(), null) {
+ @Override
+ public boolean isEngineRemotelyEnabled(String engineName,
+ EngineSettings engineSettings)
+ throws MetaGlobalException {
+ return true;
+ }
+
+ @Override
+ public void advance() {
+ // So we don't proceed and run other stages.
+ }
+ };
+ }
+
+ private static GlobalSessionCallback createGlobalSessionCallback() {
+ return new DefaultGlobalSessionCallback() {
+
+ @Override
+ public void handleAborted(GlobalSession globalSession, String reason) {
+ performNotify(new Exception("Aborted"));
+ }
+
+ @Override
+ public void handleError(GlobalSession globalSession, Exception ex) {
+ performNotify(ex);
+ }
+ };
+ }
+
+ private static void assertConfigTimestampsGreaterThan(SynchronizerConfiguration config, long local, long remote) {
+ assertTrue(local <= config.localBundle.getTimestamp());
+ assertTrue(remote <= config.remoteBundle.getTimestamp());
+ }
+
+ private static void assertConfigTimestampsEqual(SynchronizerConfiguration config, long local, long remote) {
+ assertEquals(local, config.localBundle.getTimestamp());
+ assertEquals(remote, config.remoteBundle.getTimestamp());
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestStoreTracking.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestStoreTracking.java
new file mode 100644
index 0000000000..bac6c7f49f
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestStoreTracking.java
@@ -0,0 +1,377 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+import junit.framework.AssertionFailedError;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
+import org.mozilla.gecko.background.sync.helpers.SimpleSuccessBeginDelegate;
+import org.mozilla.gecko.background.sync.helpers.SimpleSuccessCreationDelegate;
+import org.mozilla.gecko.background.sync.helpers.SimpleSuccessFetchDelegate;
+import org.mozilla.gecko.background.sync.helpers.SimpleSuccessFinishDelegate;
+import org.mozilla.gecko.background.sync.helpers.SimpleSuccessStoreDelegate;
+import org.mozilla.gecko.background.testhelpers.WBORepository;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.repositories.InactiveSessionException;
+import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
+import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+import org.mozilla.gecko.sync.synchronizer.Synchronizer;
+import org.mozilla.gecko.sync.synchronizer.SynchronizerDelegate;
+
+import android.content.Context;
+
+public class TestStoreTracking extends AndroidSyncTestCase {
+ public void assertEq(Object expected, Object actual) {
+ try {
+ assertEquals(expected, actual);
+ } catch (AssertionFailedError e) {
+ performNotify(e);
+ }
+ }
+
+ public class TrackingWBORepository extends WBORepository {
+ @Override
+ public synchronized boolean shouldTrack() {
+ return true;
+ }
+ }
+
+ public void doTestStoreRetrieveByGUID(final WBORepository repository,
+ final RepositorySession session,
+ final String expectedGUID,
+ final Record record) {
+
+ final SimpleSuccessStoreDelegate storeDelegate = new SimpleSuccessStoreDelegate() {
+
+ @Override
+ public void onRecordStoreSucceeded(String guid) {
+ Logger.debug(getName(), "Stored " + guid);
+ assertEq(expectedGUID, guid);
+ }
+
+ @Override
+ public void onStoreCompleted(long storeEnd) {
+ Logger.debug(getName(), "Store completed at " + storeEnd + ".");
+ try {
+ session.fetch(new String[] { expectedGUID }, new SimpleSuccessFetchDelegate() {
+ @Override
+ public void onFetchedRecord(Record record) {
+ Logger.debug(getName(), "Hurrah! Fetched record " + record.guid);
+ assertEq(expectedGUID, record.guid);
+ }
+
+ @Override
+ public void onFetchCompleted(final long fetchEnd) {
+ Logger.debug(getName(), "Fetch completed at " + fetchEnd + ".");
+
+ // But fetching by time returns nothing.
+ session.fetchSince(0, new SimpleSuccessFetchDelegate() {
+ private AtomicBoolean fetched = new AtomicBoolean(false);
+
+ @Override
+ public void onFetchedRecord(Record record) {
+ Logger.debug(getName(), "Fetched record " + record.guid);
+ fetched.set(true);
+ performNotify(new AssertionFailedError("Should have fetched no record!"));
+ }
+
+ @Override
+ public void onFetchCompleted(final long fetchEnd) {
+ if (fetched.get()) {
+ Logger.debug(getName(), "Not finishing session: record retrieved.");
+ return;
+ }
+ try {
+ session.finish(new SimpleSuccessFinishDelegate() {
+ @Override
+ public void onFinishSucceeded(RepositorySession session,
+ RepositorySessionBundle bundle) {
+ performNotify();
+ }
+ });
+ } catch (InactiveSessionException e) {
+ performNotify(e);
+ }
+ }
+ });
+ }
+ });
+ } catch (InactiveSessionException e) {
+ performNotify(e);
+ }
+ }
+ };
+
+ session.setStoreDelegate(storeDelegate);
+ try {
+ Logger.debug(getName(), "Storing...");
+ session.store(record);
+ session.storeDone();
+ } catch (NoStoreDelegateException e) {
+ // Should not happen.
+ }
+ }
+
+ private void doTestNewSessionRetrieveByTime(final WBORepository repository,
+ final String expectedGUID) {
+ final SimpleSuccessCreationDelegate createDelegate = new SimpleSuccessCreationDelegate() {
+ @Override
+ public void onSessionCreated(final RepositorySession session) {
+ Logger.debug(getName(), "Session created.");
+ try {
+ session.begin(new SimpleSuccessBeginDelegate() {
+ @Override
+ public void onBeginSucceeded(final RepositorySession session) {
+ // Now we get a result.
+ session.fetchSince(0, new SimpleSuccessFetchDelegate() {
+
+ @Override
+ public void onFetchedRecord(Record record) {
+ assertEq(expectedGUID, record.guid);
+ }
+
+ @Override
+ public void onFetchCompleted(long end) {
+ try {
+ session.finish(new SimpleSuccessFinishDelegate() {
+ @Override
+ public void onFinishSucceeded(RepositorySession session,
+ RepositorySessionBundle bundle) {
+ // Hooray!
+ performNotify();
+ }
+ });
+ } catch (InactiveSessionException e) {
+ performNotify(e);
+ }
+ }
+ });
+ }
+ });
+ } catch (InvalidSessionTransitionException e) {
+ performNotify(e);
+ }
+ }
+ };
+ Runnable create = new Runnable() {
+ @Override
+ public void run() {
+ repository.createSession(createDelegate, getApplicationContext());
+ }
+ };
+
+ performWait(create);
+ }
+
+ /**
+ * Store a record in one session. Verify that fetching by GUID returns
+ * the record. Verify that fetching by timestamp fails to return records.
+ * Start a new session. Verify that fetching by timestamp returns the
+ * stored record.
+ *
+ * Invokes doTestStoreRetrieveByGUID, doTestNewSessionRetrieveByTime.
+ */
+ public void testStoreRetrieveByGUID() {
+ Logger.debug(getName(), "Started.");
+ final WBORepository r = new TrackingWBORepository();
+ final long now = System.currentTimeMillis();
+ final String expectedGUID = "abcdefghijkl";
+ final Record record = new BookmarkRecord(expectedGUID, "bookmarks", now , false);
+
+ final RepositorySessionCreationDelegate createDelegate = new SimpleSuccessCreationDelegate() {
+ @Override
+ public void onSessionCreated(RepositorySession session) {
+ Logger.debug(getName(), "Session created: " + session);
+ try {
+ session.begin(new SimpleSuccessBeginDelegate() {
+ @Override
+ public void onBeginSucceeded(final RepositorySession session) {
+ doTestStoreRetrieveByGUID(r, session, expectedGUID, record);
+ }
+ });
+ } catch (InvalidSessionTransitionException e) {
+ performNotify(e);
+ }
+ }
+ };
+
+ final Context applicationContext = getApplicationContext();
+
+ // This has to happen on a new thread so that we
+ // can wait for it!
+ Runnable create = onThreadRunnable(new Runnable() {
+ @Override
+ public void run() {
+ r.createSession(createDelegate, applicationContext);
+ }
+ });
+
+ Runnable retrieve = onThreadRunnable(new Runnable() {
+ @Override
+ public void run() {
+ doTestNewSessionRetrieveByTime(r, expectedGUID);
+ performNotify();
+ }
+ });
+
+ performWait(create);
+ performWait(retrieve);
+ }
+
+ private Runnable onThreadRunnable(final Runnable r) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ new Thread(r).start();
+ }
+ };
+ }
+
+
+ public class CountingWBORepository extends TrackingWBORepository {
+ public AtomicLong counter = new AtomicLong(0L);
+ public class CountingWBORepositorySession extends WBORepositorySession {
+ private static final String LOG_TAG = "CountingRepoSession";
+
+ public CountingWBORepositorySession(WBORepository repository) {
+ super(repository);
+ }
+
+ @Override
+ public void store(final Record record) throws NoStoreDelegateException {
+ Logger.debug(LOG_TAG, "Counter now " + counter.incrementAndGet());
+ super.store(record);
+ }
+ }
+
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ delegate.deferredCreationDelegate().onSessionCreated(new CountingWBORepositorySession(this));
+ }
+ }
+
+ public class TestRecord extends Record {
+ public TestRecord(String guid, String collection, long lastModified,
+ boolean deleted) {
+ super(guid, collection, lastModified, deleted);
+ }
+
+ @Override
+ public void initFromEnvelope(CryptoRecord payload) {
+ return;
+ }
+
+ @Override
+ public CryptoRecord getEnvelope() {
+ return null;
+ }
+
+ @Override
+ protected void populatePayload(ExtendedJSONObject payload) {
+ }
+
+ @Override
+ protected void initFromPayload(ExtendedJSONObject payload) {
+ }
+
+ @Override
+ public Record copyWithIDs(String guid, long androidID) {
+ return new TestRecord(guid, this.collection, this.lastModified, this.deleted);
+ }
+ }
+
+ /**
+ * Create two repositories, syncing from one to the other. Ensure
+ * that records stored from one aren't re-uploaded.
+ */
+ public void testStoreBetweenRepositories() {
+ final CountingWBORepository repoA = new CountingWBORepository(); // "Remote". First source.
+ final CountingWBORepository repoB = new CountingWBORepository(); // "Local". First sink.
+ long now = System.currentTimeMillis();
+
+ TestRecord recordA1 = new TestRecord("aacdefghiaaa", "coll", now - 30, false);
+ TestRecord recordA2 = new TestRecord("aacdefghibbb", "coll", now - 20, false);
+ TestRecord recordB1 = new TestRecord("aacdefghiaaa", "coll", now - 10, false);
+ TestRecord recordB2 = new TestRecord("aacdefghibbb", "coll", now - 40, false);
+
+ TestRecord recordA3 = new TestRecord("nncdefghibbb", "coll", now, false);
+ TestRecord recordB3 = new TestRecord("nncdefghiaaa", "coll", now, false);
+
+ // A1 and B1 are the same, but B's version is newer. We expect A1 to be downloaded
+ // and B1 to be uploaded.
+ // A2 and B2 are the same, but A's version is newer. We expect A2 to be downloaded
+ // and B2 to not be uploaded.
+ // Both A3 and B3 are new. We expect them to go in each direction.
+ // Expected counts, then:
+ // Repo A: B1 + B3
+ // Repo B: A1 + A2 + A3
+ repoB.wbos.put(recordB1.guid, recordB1);
+ repoB.wbos.put(recordB2.guid, recordB2);
+ repoB.wbos.put(recordB3.guid, recordB3);
+ repoA.wbos.put(recordA1.guid, recordA1);
+ repoA.wbos.put(recordA2.guid, recordA2);
+ repoA.wbos.put(recordA3.guid, recordA3);
+
+ final Synchronizer s = new Synchronizer();
+ s.repositoryA = repoA;
+ s.repositoryB = repoB;
+
+ Runnable r = new Runnable() {
+ @Override
+ public void run() {
+ s.synchronize(getApplicationContext(), new SynchronizerDelegate() {
+
+ @Override
+ public void onSynchronized(Synchronizer synchronizer) {
+ long countA = repoA.counter.get();
+ long countB = repoB.counter.get();
+ Logger.debug(getName(), "Counts: " + countA + ", " + countB);
+ assertEq(2L, countA);
+ assertEq(3L, countB);
+
+ // Testing for store timestamp 'hack'.
+ // We fetched from A first, and so its bundle timestamp will be the last
+ // stored time. We fetched from B second, so its bundle timestamp will be
+ // the last fetched time.
+ final long timestampA = synchronizer.bundleA.getTimestamp();
+ final long timestampB = synchronizer.bundleB.getTimestamp();
+ Logger.debug(getName(), "Repo A timestamp: " + timestampA);
+ Logger.debug(getName(), "Repo B timestamp: " + timestampB);
+ Logger.debug(getName(), "Repo A fetch done: " + repoA.stats.fetchCompleted);
+ Logger.debug(getName(), "Repo A store done: " + repoA.stats.storeCompleted);
+ Logger.debug(getName(), "Repo B fetch done: " + repoB.stats.fetchCompleted);
+ Logger.debug(getName(), "Repo B store done: " + repoB.stats.storeCompleted);
+
+ assertTrue(timestampB <= timestampA);
+ assertTrue(repoA.stats.fetchCompleted <= timestampA);
+ assertTrue(repoA.stats.storeCompleted >= repoA.stats.fetchCompleted);
+ assertEquals(repoA.stats.storeCompleted, timestampA);
+ assertEquals(repoB.stats.fetchCompleted, timestampB);
+ performNotify();
+ }
+
+ @Override
+ public void onSynchronizeFailed(Synchronizer synchronizer,
+ Exception lastException, String reason) {
+ Logger.debug(getName(), "Failed.");
+ performNotify(new AssertionFailedError("Should not fail."));
+ }
+ });
+ }
+ };
+
+ performWait(onThreadRunnable(r));
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestSyncConfiguration.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestSyncConfiguration.java
new file mode 100644
index 0000000000..389aaf8915
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestSyncConfiguration.java
@@ -0,0 +1,146 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
+import org.mozilla.gecko.sync.SyncConfiguration;
+
+import android.content.SharedPreferences;
+
+public class TestSyncConfiguration extends AndroidSyncTestCase {
+ public static final String TEST_PREFS_NAME = "test";
+
+ public SharedPreferences getPrefs(String name, int mode) {
+ return this.getApplicationContext().getSharedPreferences(name, mode);
+ }
+
+ /**
+ * Ensure that declined engines persist through prefs.
+ */
+ public void testDeclinedEngineNames() {
+ SyncConfiguration config = null;
+ SharedPreferences prefs = getPrefs(TEST_PREFS_NAME, 0);
+
+ config = newSyncConfiguration();
+ config.declinedEngineNames = new HashSet<String>();
+ config.declinedEngineNames.add("test1");
+ config.declinedEngineNames.add("test2");
+ config.persistToPrefs();
+ assertTrue(prefs.contains(SyncConfiguration.PREF_DECLINED_ENGINE_NAMES));
+ config = newSyncConfiguration();
+ Set<String> expected = new HashSet<String>();
+ for (String name : new String[] { "test1", "test2" }) {
+ expected.add(name);
+ }
+ assertEquals(expected, config.declinedEngineNames);
+
+ config.declinedEngineNames = null;
+ config.persistToPrefs();
+ assertFalse(prefs.contains(SyncConfiguration.PREF_DECLINED_ENGINE_NAMES));
+ config = newSyncConfiguration();
+ assertNotNull(config.declinedEngineNames);
+ assertTrue(config.declinedEngineNames.isEmpty());
+ }
+
+ public void testEnabledEngineNames() {
+ SyncConfiguration config = null;
+ SharedPreferences prefs = getPrefs(TEST_PREFS_NAME, 0);
+
+ config = newSyncConfiguration();
+ config.enabledEngineNames = new HashSet<String>();
+ config.enabledEngineNames.add("test1");
+ config.enabledEngineNames.add("test2");
+ config.persistToPrefs();
+ assertTrue(prefs.contains(SyncConfiguration.PREF_ENABLED_ENGINE_NAMES));
+ config = newSyncConfiguration();
+ Set<String> expected = new HashSet<String>();
+ for (String name : new String[] { "test1", "test2" }) {
+ expected.add(name);
+ }
+ assertEquals(expected, config.enabledEngineNames);
+
+ config.enabledEngineNames = null;
+ config.persistToPrefs();
+ assertFalse(prefs.contains(SyncConfiguration.PREF_ENABLED_ENGINE_NAMES));
+ config = newSyncConfiguration();
+ assertNull(config.enabledEngineNames);
+ }
+
+ public void testSyncID() {
+ SyncConfiguration config = null;
+ SharedPreferences prefs = getPrefs(TEST_PREFS_NAME, 0);
+
+ config = newSyncConfiguration();
+ config.syncID = "test1";
+ config.persistToPrefs();
+ assertTrue(prefs.contains(SyncConfiguration.PREF_SYNC_ID));
+ config = newSyncConfiguration();
+ assertEquals("test1", config.syncID);
+ }
+
+ public void testStoreSelectedEnginesToPrefs() {
+ SharedPreferences prefs = getPrefs(TEST_PREFS_NAME, 0);
+ // Store engines, excluding history/forms special case.
+ Map<String, Boolean> expectedEngines = new HashMap<String, Boolean>();
+ expectedEngines.put("test1", true);
+ expectedEngines.put("test2", false);
+ expectedEngines.put("test3", true);
+
+ SyncConfiguration.storeSelectedEnginesToPrefs(prefs, expectedEngines);
+
+ // Read values from selectedEngines.
+ assertTrue(prefs.contains(SyncConfiguration.PREF_USER_SELECTED_ENGINES_TO_SYNC));
+ SyncConfiguration config = null;
+ config = newSyncConfiguration();
+ config.loadFromPrefs(prefs);
+ assertEquals(expectedEngines, config.userSelectedEngines);
+ }
+
+ /**
+ * Tests dependency of forms engine on history engine.
+ */
+ public void testSelectedEnginesHistoryAndForms() {
+ SharedPreferences prefs = getPrefs(TEST_PREFS_NAME, 0);
+ // Store engines, excluding history/forms special case.
+ Map<String, Boolean> storedEngines = new HashMap<String, Boolean>();
+ storedEngines.put("history", true);
+
+ SyncConfiguration.storeSelectedEnginesToPrefs(prefs, storedEngines);
+
+ // Expected engines.
+ storedEngines.put("forms", true);
+ // Read values from selectedEngines.
+ assertTrue(prefs.contains(SyncConfiguration.PREF_USER_SELECTED_ENGINES_TO_SYNC));
+ SyncConfiguration config = null;
+ config = newSyncConfiguration();
+ config.loadFromPrefs(prefs);
+ assertEquals(storedEngines, config.userSelectedEngines);
+ }
+
+ public void testsSelectedEnginesNoHistoryNorForms() {
+ SharedPreferences prefs = getPrefs(TEST_PREFS_NAME, 0);
+ // Store engines, excluding history/forms special case.
+ Map<String, Boolean> storedEngines = new HashMap<String, Boolean>();
+ storedEngines.put("forms", true);
+
+ SyncConfiguration.storeSelectedEnginesToPrefs(prefs, storedEngines);
+
+ // Read values from selectedEngines.
+ assertTrue(prefs.contains(SyncConfiguration.PREF_USER_SELECTED_ENGINES_TO_SYNC));
+ SyncConfiguration config = null;
+ config = newSyncConfiguration();
+ config.loadFromPrefs(prefs);
+ // Forms should not be selected if history is not present.
+ assertTrue(config.userSelectedEngines.isEmpty());
+ }
+
+ protected SyncConfiguration newSyncConfiguration() {
+ return new SyncConfiguration(null, null, getPrefs(TEST_PREFS_NAME, 0));
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestWebURLFinder.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestWebURLFinder.java
new file mode 100644
index 0000000000..a7ebb71d5c
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestWebURLFinder.java
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync;
+
+import java.util.Arrays;
+
+import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
+import org.mozilla.gecko.sync.setup.activities.WebURLFinder;
+
+/**
+ * These tests are on device because the WebKit APIs are stubs on desktop.
+ */
+public class TestWebURLFinder extends AndroidSyncTestCase {
+ public String find(String string) {
+ return new WebURLFinder(string).bestWebURL();
+ }
+
+ public String find(String[] strings) {
+ return new WebURLFinder(Arrays.asList(strings)).bestWebURL();
+ }
+
+ public void testNoEmail() {
+ assertNull(find("test@test.com"));
+ }
+
+ public void testSchemeFirst() {
+ assertEquals("http://scheme.com", find("test.com http://scheme.com"));
+ }
+
+ public void testFullURL() {
+ assertEquals("http://scheme.com:8080/inner#anchor&arg=1", find("test.com http://scheme.com:8080/inner#anchor&arg=1"));
+ }
+
+ public void testNoScheme() {
+ assertEquals("noscheme.com", find("noscheme.com"));
+ }
+
+ public void testNoBadScheme() {
+ assertNull(find("file:///test javascript:///test.js"));
+ }
+
+ public void testStrings() {
+ assertEquals("http://test.com", find(new String[] { "http://test.com", "noscheme.com" }));
+ assertEquals("http://test.com", find(new String[] { "noschemefirst.com", "http://test.com" }));
+ assertEquals("http://test.com/inner#test", find(new String[] { "noschemefirst.com", "http://test.com/inner#test", "http://second.org/fark" }));
+ assertEquals("http://test.com", find(new String[] { "javascript:///test.js", "http://test.com" }));
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/BookmarkHelpers.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/BookmarkHelpers.java
new file mode 100644
index 0000000000..0fcd762f40
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/BookmarkHelpers.java
@@ -0,0 +1,216 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import org.json.simple.JSONArray;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
+
+public class BookmarkHelpers {
+
+ private static String mobileFolderGuid = "mobile";
+ private static String mobileFolderName = "mobile";
+ private static String topFolderGuid = Utils.generateGuid();
+ private static String topFolderName = "My Top Folder";
+ private static String middleFolderGuid = Utils.generateGuid();
+ private static String middleFolderName = "My Middle Folder";
+ private static String bottomFolderGuid = Utils.generateGuid();
+ private static String bottomFolderName = "My Bottom Folder";
+ private static String bmk1Guid = Utils.generateGuid();
+ private static String bmk2Guid = Utils.generateGuid();
+ private static String bmk3Guid = Utils.generateGuid();
+ private static String bmk4Guid = Utils.generateGuid();
+
+ /*
+ * Helpers for creating bookmark records of different types
+ */
+ public static BookmarkRecord createBookmarkInMobileFolder1() {
+ BookmarkRecord rec = createBookmark1();
+ rec.guid = Utils.generateGuid();
+ rec.parentID = mobileFolderGuid;
+ rec.parentName = mobileFolderName;
+ return rec;
+ }
+
+ public static BookmarkRecord createBookmarkInMobileFolder2() {
+ BookmarkRecord rec = createBookmark2();
+ rec.guid = Utils.generateGuid();
+ rec.parentID = mobileFolderGuid;
+ rec.parentName = mobileFolderName;
+ return rec;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static BookmarkRecord createBookmark1() {
+ BookmarkRecord record = new BookmarkRecord();
+ JSONArray tags = new JSONArray();
+ tags.add("tag1");
+ tags.add("tag2");
+ tags.add("tag3");
+ record.guid = bmk1Guid;
+ record.title = "Foo!!!";
+ record.bookmarkURI = "http://foo.bar.com";
+ record.description = "This is a description for foo.bar.com";
+ record.tags = tags;
+ record.keyword = "fooooozzzzz";
+ record.parentID = topFolderGuid;
+ record.parentName = topFolderName;
+ record.type = "bookmark";
+ return record;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static BookmarkRecord createBookmark2() {
+ BookmarkRecord record = new BookmarkRecord();
+ JSONArray tags = new JSONArray();
+ tags.add("tag1");
+ tags.add("tag2");
+ record.guid = bmk2Guid;
+ record.title = "Bar???";
+ record.bookmarkURI = "http://bar.foo.com";
+ record.description = "This is a description for Bar???";
+ record.tags = tags;
+ record.keyword = "keywordzzz";
+ record.parentID = topFolderGuid;
+ record.parentName = topFolderName;
+ record.type = "bookmark";
+ return record;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static BookmarkRecord createBookmark3() {
+ BookmarkRecord record = new BookmarkRecord();
+ JSONArray tags = new JSONArray();
+ tags.add("tag1");
+ tags.add("tag2");
+ record.guid = bmk3Guid;
+ record.title = "Bmk3";
+ record.bookmarkURI = "http://bmk3.com";
+ record.description = "This is a description for bmk3";
+ record.tags = tags;
+ record.keyword = "snooozzz";
+ record.parentID = middleFolderGuid;
+ record.parentName = middleFolderName;
+ record.type = "bookmark";
+ return record;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static BookmarkRecord createBookmark4() {
+ BookmarkRecord record = new BookmarkRecord();
+ JSONArray tags = new JSONArray();
+ tags.add("tag1");
+ tags.add("tag2");
+ record.guid = bmk4Guid;
+ record.title = "Bmk4";
+ record.bookmarkURI = "http://bmk4.com";
+ record.description = "This is a description for bmk4?";
+ record.tags = tags;
+ record.keyword = "booooozzz";
+ record.parentID = bottomFolderGuid;
+ record.parentName = bottomFolderName;
+ record.type = "bookmark";
+ return record;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static BookmarkRecord createMicrosummary() {
+ BookmarkRecord record = new BookmarkRecord();
+ JSONArray tags = new JSONArray();
+ tags.add("tag1");
+ tags.add("tag2");
+ record.guid = Utils.generateGuid();
+ record.title = "Microsummary 1";
+ record.bookmarkURI = "www.bmkuri.com";
+ record.description = "microsummary description";
+ record.tags = tags;
+ record.keyword = "keywordzzz";
+ record.parentID = topFolderGuid;
+ record.parentName = topFolderName;
+ record.type = "microsummary";
+ return record;
+ }
+
+ public static BookmarkRecord createQuery() {
+ BookmarkRecord record = new BookmarkRecord();
+ record.guid = Utils.generateGuid();
+ record.title = "Query 1";
+ record.bookmarkURI = "http://www.query.com";
+ record.description = "Query 1 description";
+ record.tags = new JSONArray();
+ record.keyword = "queryKeyword";
+ record.parentID = topFolderGuid;
+ record.parentName = topFolderName;
+ record.type = "query";
+ return record;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static BookmarkRecord createFolder1() {
+ BookmarkRecord record = new BookmarkRecord();
+ record.guid = topFolderGuid;
+ record.title = topFolderName;
+ record.parentID = "mobile";
+ record.parentName = "mobile";
+ JSONArray children = new JSONArray();
+ children.add(bmk1Guid);
+ children.add(bmk2Guid);
+ record.children = children;
+ record.type = "folder";
+ return record;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static BookmarkRecord createFolder2() {
+ BookmarkRecord record = new BookmarkRecord();
+ record.guid = middleFolderGuid;
+ record.title = middleFolderName;
+ record.parentID = topFolderGuid;
+ record.parentName = topFolderName;
+ JSONArray children = new JSONArray();
+ children.add(bmk3Guid);
+ record.children = children;
+ record.type = "folder";
+ return record;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static BookmarkRecord createFolder3() {
+ BookmarkRecord record = new BookmarkRecord();
+ record.guid = bottomFolderGuid;
+ record.title = bottomFolderName;
+ record.parentID = middleFolderGuid;
+ record.parentName = middleFolderName;
+ JSONArray children = new JSONArray();
+ children.add(bmk4Guid);
+ record.children = children;
+ record.type = "folder";
+ return record;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static BookmarkRecord createLivemark() {
+ BookmarkRecord record = new BookmarkRecord();
+ record.guid = Utils.generateGuid();
+ record.title = "Livemark title";
+ record.parentID = topFolderGuid;
+ record.parentName = topFolderName;
+ JSONArray children = new JSONArray();
+ children.add(Utils.generateGuid());
+ children.add(Utils.generateGuid());
+ record.children = children;
+ record.type = "livemark";
+ return record;
+ }
+
+ public static BookmarkRecord createSeparator() {
+ BookmarkRecord record = new BookmarkRecord();
+ record.guid = Utils.generateGuid();
+ record.androidPosition = 3;
+ record.parentID = topFolderGuid;
+ record.parentName = topFolderName;
+ record.type = "separator";
+ return record;
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultBeginDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultBeginDelegate.java
new file mode 100644
index 0000000000..67aa81fff3
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultBeginDelegate.java
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import java.util.concurrent.ExecutorService;
+
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
+
+public class DefaultBeginDelegate extends DefaultDelegate implements RepositorySessionBeginDelegate {
+ @Override
+ public void onBeginFailed(Exception ex) {
+ performNotify("Begin failed", ex);
+ }
+
+ @Override
+ public void onBeginSucceeded(RepositorySession session) {
+ performNotify("Default begin delegate hit.", null);
+ }
+
+ @Override
+ public RepositorySessionBeginDelegate deferredBeginDelegate(ExecutorService executor) {
+ DefaultBeginDelegate copy;
+ try {
+ copy = (DefaultBeginDelegate) this.clone();
+ copy.executor = executor;
+ return copy;
+ } catch (CloneNotSupportedException e) {
+ return this;
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultCleanDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultCleanDelegate.java
new file mode 100644
index 0000000000..a1f5b7a97d
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultCleanDelegate.java
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCleanDelegate;
+
+public class DefaultCleanDelegate extends DefaultDelegate implements RepositorySessionCleanDelegate {
+
+ @Override
+ public void onCleaned(Repository repo) {
+ performNotify("Default begin delegate hit.", null);
+ }
+
+ @Override
+ public void onCleanFailed(Repository repo, Exception ex) {
+ performNotify("Clean failed.", ex);
+ }
+
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultDelegate.java
new file mode 100644
index 0000000000..7e9341f023
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultDelegate.java
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import java.util.concurrent.ExecutorService;
+
+import junit.framework.AssertionFailedError;
+
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+
+public abstract class DefaultDelegate {
+ protected ExecutorService executor;
+
+ protected final WaitHelper waitHelper;
+
+ public DefaultDelegate() {
+ waitHelper = WaitHelper.getTestWaiter();
+ }
+
+ public DefaultDelegate(WaitHelper waitHelper) {
+ this.waitHelper = waitHelper;
+ }
+
+ protected WaitHelper getTestWaiter() {
+ return waitHelper;
+ }
+
+ public void performWait(Runnable runnable) throws AssertionFailedError {
+ getTestWaiter().performWait(runnable);
+ }
+
+ public void performNotify() {
+ getTestWaiter().performNotify();
+ }
+
+ public void performNotify(Throwable e) {
+ getTestWaiter().performNotify(e);
+ }
+
+ public void performNotify(String reason, Throwable e) {
+ String message = reason;
+ if (e != null) {
+ message += ": " + e.getMessage();
+ }
+ AssertionFailedError ex = new AssertionFailedError(message);
+ if (e != null) {
+ ex.initCause(e);
+ }
+ getTestWaiter().performNotify(ex);
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultFetchDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultFetchDelegate.java
new file mode 100644
index 0000000000..3d7d23bab2
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultFetchDelegate.java
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.fail;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+
+import junit.framework.AssertionFailedError;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.repositories.delegates.DeferredRepositorySessionFetchRecordsDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+public class DefaultFetchDelegate extends DefaultDelegate implements RepositorySessionFetchRecordsDelegate {
+
+ private static final String LOG_TAG = "DefaultFetchDelegate";
+ public ArrayList<Record> records = new ArrayList<Record>();
+ public Set<String> ignore = new HashSet<String>();
+
+ @Override
+ public void onFetchFailed(Exception ex, Record record) {
+ performNotify("Fetch failed.", ex);
+ }
+
+ protected void onDone(ArrayList<Record> records, HashMap<String, Record> expected, long end) {
+ Logger.debug(LOG_TAG, "onDone.");
+ Logger.debug(LOG_TAG, "End timestamp is " + end);
+ Logger.debug(LOG_TAG, "Expected is " + expected);
+ Logger.debug(LOG_TAG, "Records is " + records);
+ Set<String> foundGuids = new HashSet<String>();
+ try {
+ int expectedCount = 0;
+ int expectedFound = 0;
+ Logger.debug(LOG_TAG, "Counting expected keys.");
+ for (String key : expected.keySet()) {
+ if (!ignore.contains(key)) {
+ expectedCount++;
+ }
+ }
+ Logger.debug(LOG_TAG, "Expected keys: " + expectedCount);
+ for (Record record : records) {
+ Logger.debug(LOG_TAG, "Record.");
+ Logger.debug(LOG_TAG, record.guid);
+
+ // Ignore special GUIDs (e.g., for bookmarks).
+ if (!ignore.contains(record.guid)) {
+ if (foundGuids.contains(record.guid)) {
+ fail("Found duplicate guid " + record.guid);
+ }
+ Record expect = expected.get(record.guid);
+ if (expect == null) {
+ fail("Do not expect to get back a record with guid: " + record.guid); // Caught below
+ }
+ Logger.debug(LOG_TAG, "Checking equality.");
+ try {
+ assertTrue(expect.equalPayloads(record)); // Caught below
+ } catch (Exception e) {
+ Logger.error(LOG_TAG, "ONOZ!", e);
+ }
+ Logger.debug(LOG_TAG, "Checked equality.");
+ expectedFound += 1;
+ // Track record once we've found it.
+ foundGuids.add(record.guid);
+ }
+ }
+ assertEquals(expectedCount, expectedFound); // Caught below
+ Logger.debug(LOG_TAG, "Notifying success.");
+ performNotify();
+ } catch (AssertionFailedError e) {
+ Logger.error(LOG_TAG, "Notifying assertion failure.");
+ performNotify(e);
+ } catch (Exception e) {
+ Logger.error(LOG_TAG, "No!");
+ performNotify();
+ }
+ }
+
+ public int recordCount() {
+ return (this.records == null) ? 0 : this.records.size();
+ }
+
+ @Override
+ public void onFetchedRecord(Record record) {
+ Logger.debug(LOG_TAG, "onFetchedRecord(" + record.guid + ")");
+ records.add(record);
+ }
+
+ @Override
+ public void onFetchCompleted(final long fetchEnd) {
+ Logger.debug(LOG_TAG, "onFetchCompleted. Doing nothing.");
+ }
+
+ @Override
+ public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(final ExecutorService executor) {
+ return new DeferredRepositorySessionFetchRecordsDelegate(this, executor);
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultFinishDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultFinishDelegate.java
new file mode 100644
index 0000000000..11e451e821
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultFinishDelegate.java
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import java.util.concurrent.ExecutorService;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
+
+public class DefaultFinishDelegate extends DefaultDelegate implements RepositorySessionFinishDelegate {
+
+ @Override
+ public void onFinishFailed(Exception ex) {
+ performNotify("Finish failed", ex);
+ }
+
+ @Override
+ public void onFinishSucceeded(RepositorySession session, RepositorySessionBundle bundle) {
+ performNotify("Hit default finish delegate", null);
+ }
+
+ @Override
+ public RepositorySessionFinishDelegate deferredFinishDelegate(final ExecutorService executor) {
+ final RepositorySessionFinishDelegate self = this;
+
+ Logger.info("DefaultFinishDelegate", "Deferring…");
+ return new RepositorySessionFinishDelegate() {
+ @Override
+ public void onFinishSucceeded(final RepositorySession session,
+ final RepositorySessionBundle bundle) {
+ Logger.info("DefaultFinishDelegate", "Executing onFinishSucceeded Runnable…");
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ self.onFinishSucceeded(session, bundle);
+ }});
+ }
+
+ @Override
+ public void onFinishFailed(final Exception ex) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ self.onFinishFailed(ex);
+ }});
+ }
+
+ @Override
+ public RepositorySessionFinishDelegate deferredFinishDelegate(ExecutorService newExecutor) {
+ if (newExecutor == executor) {
+ return this;
+ }
+ throw new IllegalArgumentException("Can't re-defer this delegate.");
+ }
+ };
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultGuidsSinceDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultGuidsSinceDelegate.java
new file mode 100644
index 0000000000..78e3cc84f0
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultGuidsSinceDelegate.java
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate;
+
+public class DefaultGuidsSinceDelegate extends DefaultDelegate implements RepositorySessionGuidsSinceDelegate {
+
+ @Override
+ public void onGuidsSinceFailed(Exception ex) {
+ performNotify("shouldn't fail", ex);
+ }
+
+ @Override
+ public void onGuidsSinceSucceeded(String[] guids) {
+ performNotify("default guids since delegate called", null);
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultSessionCreationDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultSessionCreationDelegate.java
new file mode 100644
index 0000000000..5d52df84d5
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultSessionCreationDelegate.java
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+
+public class DefaultSessionCreationDelegate extends DefaultDelegate implements
+ RepositorySessionCreationDelegate {
+
+ @Override
+ public void onSessionCreateFailed(Exception ex) {
+ performNotify("Session creation failed", ex);
+ }
+
+ @Override
+ public void onSessionCreated(RepositorySession session) {
+ performNotify("Should not have been created.", null);
+ }
+
+ @Override
+ public RepositorySessionCreationDelegate deferredCreationDelegate() {
+ final RepositorySessionCreationDelegate self = this;
+ return new RepositorySessionCreationDelegate() {
+
+ @Override
+ public void onSessionCreated(final RepositorySession session) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ self.onSessionCreated(session);
+ }
+ }).start();
+ }
+
+ @Override
+ public void onSessionCreateFailed(final Exception ex) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ self.onSessionCreateFailed(ex);
+ }
+ }).start();
+ }
+
+ @Override
+ public RepositorySessionCreationDelegate deferredCreationDelegate() {
+ return this;
+ }
+ };
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultStoreDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultStoreDelegate.java
new file mode 100644
index 0000000000..7ba2e6df60
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultStoreDelegate.java
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import java.util.concurrent.ExecutorService;
+
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
+
+public class DefaultStoreDelegate extends DefaultDelegate implements RepositorySessionStoreDelegate {
+
+ @Override
+ public void onRecordStoreFailed(Exception ex, String guid) {
+ performNotify("Store failed", ex);
+ }
+
+ @Override
+ public void onRecordStoreSucceeded(String guid) {
+ performNotify("DefaultStoreDelegate used", null);
+ }
+
+ @Override
+ public void onStoreCompleted(long storeEnd) {
+ performNotify("DefaultStoreDelegate used", null);
+ }
+
+ @Override
+ public RepositorySessionStoreDelegate deferredStoreDelegate(final ExecutorService executor) {
+ final RepositorySessionStoreDelegate self = this;
+ return new RepositorySessionStoreDelegate() {
+
+ @Override
+ public void onRecordStoreSucceeded(final String guid) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ self.onRecordStoreSucceeded(guid);
+ }
+ });
+ }
+
+ @Override
+ public void onRecordStoreFailed(final Exception ex, final String guid) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ self.onRecordStoreFailed(ex, guid);
+ }
+ });
+ }
+
+ @Override
+ public void onStoreCompleted(final long storeEnd) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ self.onStoreCompleted(storeEnd);
+ }
+ });
+ }
+
+ @Override
+ public RepositorySessionStoreDelegate deferredStoreDelegate(ExecutorService newExecutor) {
+ if (newExecutor == executor) {
+ return this;
+ }
+ throw new IllegalArgumentException("Can't re-defer this delegate.");
+ }
+ };
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectBeginDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectBeginDelegate.java
new file mode 100644
index 0000000000..d320cfd7a3
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectBeginDelegate.java
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import static junit.framework.Assert.assertNotNull;
+import junit.framework.AssertionFailedError;
+
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+
+public class ExpectBeginDelegate extends DefaultBeginDelegate {
+ @Override
+ public void onBeginSucceeded(RepositorySession session) {
+ try {
+ assertNotNull(session);
+ } catch (AssertionFailedError e) {
+ performNotify("Expected non-null session", e);
+ return;
+ }
+ performNotify();
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectBeginFailDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectBeginFailDelegate.java
new file mode 100644
index 0000000000..ff1807d510
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectBeginFailDelegate.java
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
+
+public class ExpectBeginFailDelegate extends DefaultBeginDelegate {
+
+ @Override
+ public void onBeginFailed(Exception ex) {
+ if (!(ex instanceof InvalidSessionTransitionException)) {
+ performNotify("Expected InvalidSessionTransititionException but got ", ex);
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFetchDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFetchDelegate.java
new file mode 100644
index 0000000000..5cfe5327a0
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFetchDelegate.java
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import java.util.HashMap;
+
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+public class ExpectFetchDelegate extends DefaultFetchDelegate {
+ private HashMap<String, Record> expect = new HashMap<String, Record>();
+
+ public ExpectFetchDelegate(Record[] records) {
+ for(int i = 0; i < records.length; i++) {
+ expect.put(records[i].guid, records[i]);
+ }
+ }
+
+ @Override
+ public void onFetchedRecord(Record record) {
+ this.records.add(record);
+ }
+
+ @Override
+ public void onFetchCompleted(final long fetchEnd) {
+ super.onDone(this.records, this.expect, fetchEnd);
+ }
+
+ public Record recordAt(int i) {
+ return this.records.get(i);
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFetchSinceDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFetchSinceDelegate.java
new file mode 100644
index 0000000000..7dcada0d48
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFetchSinceDelegate.java
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import java.util.Arrays;
+
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import junit.framework.AssertionFailedError;
+
+public class ExpectFetchSinceDelegate extends DefaultFetchDelegate {
+ private String[] expected;
+ private long earliest;
+
+ public ExpectFetchSinceDelegate(long timestamp, String[] guids) {
+ expected = guids;
+ earliest = timestamp;
+ Arrays.sort(expected);
+ }
+
+ @Override
+ public void onFetchCompleted(final long fetchEnd) {
+ AssertionFailedError err = null;
+ try {
+ int countSpecials = 0;
+ for (Record record : records) {
+ // Check if record should be ignored.
+ if (!ignore.contains(record.guid)) {
+ assertFalse(-1 == Arrays.binarySearch(this.expected, record.guid));
+ } else {
+ countSpecials++;
+ }
+ // Check that record is later than timestamp-earliest.
+ assertTrue(record.lastModified >= this.earliest);
+ }
+ assertEquals(this.expected.length, records.size() - countSpecials);
+ } catch (AssertionFailedError e) {
+ err = e;
+ }
+ performNotify(err);
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFinishDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFinishDelegate.java
new file mode 100644
index 0000000000..0b6f1de889
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFinishDelegate.java
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
+
+public class ExpectFinishDelegate extends DefaultFinishDelegate {
+
+ @Override
+ public void onFinishSucceeded(RepositorySession session, RepositorySessionBundle bundle) {
+ Logger.info("ExpectFinishDelegate", "Finish succeeded.");
+ performNotify();
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFinishFailDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFinishFailDelegate.java
new file mode 100644
index 0000000000..df83432bcd
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFinishFailDelegate.java
@@ -0,0 +1,15 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
+
+public class ExpectFinishFailDelegate extends DefaultFinishDelegate {
+ @Override
+ public void onFinishFailed(Exception ex) {
+ if (!(ex instanceof InvalidSessionTransitionException)) {
+ performNotify("Expected InvalidSessionTransititionException but got ", ex);
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectGuidsSinceDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectGuidsSinceDelegate.java
new file mode 100644
index 0000000000..435ba75022
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectGuidsSinceDelegate.java
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import junit.framework.AssertionFailedError;
+
+public class ExpectGuidsSinceDelegate extends DefaultGuidsSinceDelegate {
+ private String[] expected;
+ public Set<String> ignore = new HashSet<String>();
+
+ public ExpectGuidsSinceDelegate(String[] guids) {
+ expected = guids;
+ Arrays.sort(expected);
+ }
+
+ @Override
+ public void onGuidsSinceSucceeded(String[] guids) {
+ AssertionFailedError err = null;
+ try {
+ int notIgnored = 0;
+ for (String guid : guids) {
+ if (!ignore.contains(guid)) {
+ notIgnored++;
+ assertFalse(-1 == Arrays.binarySearch(this.expected, guid));
+ }
+ }
+ assertEquals(this.expected.length, notIgnored);
+ } catch (AssertionFailedError e) {
+ err = e;
+ }
+ performNotify(err);
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidRequestFetchDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidRequestFetchDelegate.java
new file mode 100644
index 0000000000..73035869c7
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidRequestFetchDelegate.java
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import org.mozilla.gecko.sync.repositories.InvalidRequestException;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+public class ExpectInvalidRequestFetchDelegate extends DefaultFetchDelegate {
+ public static final String LOG_TAG = "ExpInvRequestFetchDel";
+
+ @Override
+ public void onFetchFailed(Exception ex, Record rec) {
+ if (ex instanceof InvalidRequestException) {
+ onDone();
+ } else {
+ performNotify("Expected InvalidRequestException but got ", ex);
+ }
+ }
+
+ private void onDone() {
+ performNotify();
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidTypeStoreDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidTypeStoreDelegate.java
new file mode 100644
index 0000000000..5ce56ee5f6
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidTypeStoreDelegate.java
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import static junit.framework.Assert.assertEquals;
+
+import org.mozilla.gecko.sync.repositories.InvalidBookmarkTypeException;
+
+public class ExpectInvalidTypeStoreDelegate extends DefaultStoreDelegate {
+
+ @Override
+ public void onRecordStoreFailed(Exception ex, String guid) {
+ assertEquals(InvalidBookmarkTypeException.class, ex.getClass());
+ performNotify();
+ }
+
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectManyStoredDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectManyStoredDelegate.java
new file mode 100644
index 0000000000..2a8df0228b
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectManyStoredDelegate.java
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+
+import java.util.HashSet;
+import java.util.concurrent.atomic.AtomicLong;
+
+import junit.framework.AssertionFailedError;
+
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+public class ExpectManyStoredDelegate extends DefaultStoreDelegate {
+ HashSet<String> expectedGUIDs;
+ AtomicLong stored;
+
+ public ExpectManyStoredDelegate(Record[] records) {
+ HashSet<String> s = new HashSet<String>();
+ for (Record record : records) {
+ s.add(record.guid);
+ }
+ expectedGUIDs = s;
+ stored = new AtomicLong(0);
+ }
+
+ @Override
+ public void onStoreCompleted(long storeEnd) {
+ try {
+ assertEquals(expectedGUIDs.size(), stored.get());
+ performNotify();
+ } catch (AssertionFailedError e) {
+ performNotify(e);
+ }
+ }
+
+ @Override
+ public void onRecordStoreSucceeded(String guid) {
+ try {
+ assertTrue(expectedGUIDs.contains(guid));
+ } catch (AssertionFailedError e) {
+ performNotify(e);
+ }
+ stored.incrementAndGet();
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectNoGUIDsSinceDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectNoGUIDsSinceDelegate.java
new file mode 100644
index 0000000000..a9f11d7b08
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectNoGUIDsSinceDelegate.java
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import static junit.framework.Assert.assertEquals;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import junit.framework.AssertionFailedError;
+
+public class ExpectNoGUIDsSinceDelegate extends DefaultGuidsSinceDelegate {
+
+ public Set<String> ignore = new HashSet<String>();
+
+ @Override
+ public void onGuidsSinceSucceeded(String[] guids) {
+ AssertionFailedError err = null;
+ try {
+ int nonIgnored = 0;
+ for (int i = 0; i < guids.length; i++) {
+ if (!ignore.contains(guids[i])) {
+ nonIgnored++;
+ }
+ }
+ assertEquals(0, nonIgnored);
+ } catch (AssertionFailedError e) {
+ err = e;
+ }
+ performNotify(err);
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectNoStoreDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectNoStoreDelegate.java
new file mode 100644
index 0000000000..93626898ee
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectNoStoreDelegate.java
@@ -0,0 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+public class ExpectNoStoreDelegate extends ExpectStoreCompletedDelegate {
+ @Override
+ public void onRecordStoreSucceeded(String guid) {
+ performNotify("Should not have stored record " + guid, null);
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectStoreCompletedDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectStoreCompletedDelegate.java
new file mode 100644
index 0000000000..b3cc909a12
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectStoreCompletedDelegate.java
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+public class ExpectStoreCompletedDelegate extends DefaultStoreDelegate {
+
+ @Override
+ public void onRecordStoreSucceeded(String guid) {
+ // That's fine.
+ }
+
+ @Override
+ public void onStoreCompleted(long storeEnd) {
+ performNotify();
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectStoredDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectStoredDelegate.java
new file mode 100644
index 0000000000..dc2e8a2d1b
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectStoredDelegate.java
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+import junit.framework.AssertionFailedError;
+
+public class ExpectStoredDelegate extends DefaultStoreDelegate {
+ String expectedGUID;
+ String storedGuid;
+
+ public ExpectStoredDelegate(String guid) {
+ this.expectedGUID = guid;
+ }
+
+ @Override
+ public synchronized void onStoreCompleted(long storeEnd) {
+ try {
+ assertNotNull(storedGuid);
+ performNotify();
+ } catch (AssertionFailedError e) {
+ performNotify("GUID " + this.expectedGUID + " was not stored", e);
+ }
+ }
+
+ @Override
+ public synchronized void onRecordStoreSucceeded(String guid) {
+ this.storedGuid = guid;
+ try {
+ if (this.expectedGUID != null) {
+ assertEquals(this.expectedGUID, guid);
+ }
+ } catch (AssertionFailedError e) {
+ performNotify(e);
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/HistoryHelpers.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/HistoryHelpers.java
new file mode 100644
index 0000000000..68f80043e9
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/HistoryHelpers.java
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.mozilla.gecko.sync.repositories.domain.HistoryRecord;
+
+public class HistoryHelpers {
+
+ @SuppressWarnings("unchecked")
+ private static JSONArray getVisits1() {
+ JSONArray json = new JSONArray();
+ JSONObject obj = new JSONObject();
+ obj.put("date", 1320087601465600000L);
+ obj.put("type", 2L);
+ json.add(obj);
+ obj = new JSONObject();
+ obj.put("date", 1320084970724990000L);
+ obj.put("type", 1L);
+ json.add(obj);
+ obj = new JSONObject();
+ obj.put("date", 1319764134412287000L);
+ obj.put("type", 1L);
+ json.add(obj);
+ obj = new JSONObject();
+ obj.put("date", 1319681306455594000L);
+ obj.put("type", 2L);
+ json.add(obj);
+ return json;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static JSONArray getVisits2() {
+ JSONArray json = new JSONArray();
+ JSONObject obj = new JSONObject();
+ obj = new JSONObject();
+ obj.put("date", 1319764134412345000L);
+ obj.put("type", 4L);
+ json.add(obj);
+ obj = new JSONObject();
+ obj.put("date", 1319681306454321000L);
+ obj.put("type", 3L);
+ json.add(obj);
+ return json;
+ }
+
+ public static HistoryRecord createHistory1() {
+ HistoryRecord record = new HistoryRecord();
+ record.title = "History 1";
+ record.histURI = "http://history.page1.com";
+ record.visits = getVisits1();
+ return record;
+ }
+
+
+ public static HistoryRecord createHistory2() {
+ HistoryRecord record = new HistoryRecord();
+ record.title = "History 2";
+ record.histURI = "http://history.page2.com";
+ record.visits = getVisits2();
+ return record;
+ }
+
+ public static HistoryRecord createHistory3() {
+ HistoryRecord record = new HistoryRecord();
+ record.title = "History 3";
+ record.histURI = "http://history.page3.com";
+ record.visits = getVisits2();
+ return record;
+ }
+
+ public static HistoryRecord createHistory4() {
+ HistoryRecord record = new HistoryRecord();
+ record.title = "History 4";
+ record.histURI = "http://history.page4.com";
+ record.visits = getVisits1();
+ return record;
+ }
+
+ public static HistoryRecord createHistory5() {
+ HistoryRecord record = new HistoryRecord();
+ record.title = "History 5";
+ record.histURI = "http://history.page5.com";
+ record.visits = getVisits2();
+ return record;
+ }
+
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/PasswordHelpers.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/PasswordHelpers.java
new file mode 100644
index 0000000000..87400a2b0c
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/PasswordHelpers.java
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import org.mozilla.gecko.sync.repositories.domain.PasswordRecord;
+
+public class PasswordHelpers {
+
+ public static PasswordRecord createPassword1() {
+ PasswordRecord rec = new PasswordRecord();
+ rec.encType = "some type";
+ rec.formSubmitURL = "http://submit.html";
+ rec.hostname = "http://hostname";
+ rec.httpRealm = "httpRealm";
+ rec.encryptedPassword ="12345";
+ rec.passwordField = "box.pass.field";
+ rec.timeCreated = 111111111L;
+ rec.timeLastUsed = 123412352435L;
+ rec.timePasswordChanged = 121111111L;
+ rec.timesUsed = 5L;
+ rec.encryptedUsername = "jvoll";
+ rec.usernameField = "box.user.field";
+ return rec;
+ }
+
+ public static PasswordRecord createPassword2() {
+ PasswordRecord rec = new PasswordRecord();
+ rec.encType = "some type";
+ rec.formSubmitURL = "http://submit2.html";
+ rec.hostname = "http://hostname2";
+ rec.httpRealm = "httpRealm2";
+ rec.encryptedPassword ="54321";
+ rec.passwordField = "box.pass.field2";
+ rec.timeCreated = 12111111111L;
+ rec.timeLastUsed = 123412352213L;
+ rec.timePasswordChanged = 123111111111L;
+ rec.timesUsed = 2L;
+ rec.encryptedUsername = "rnewman";
+ rec.usernameField = "box.user.field2";
+ return rec;
+ }
+
+ public static PasswordRecord createPassword3() {
+ PasswordRecord rec = new PasswordRecord();
+ rec.encType = "some type3";
+ rec.formSubmitURL = "http://submit3.html";
+ rec.hostname = "http://hostname3";
+ rec.httpRealm = "httpRealm3";
+ rec.encryptedPassword ="54321";
+ rec.passwordField = "box.pass.field3";
+ rec.timeCreated = 100000000000L;
+ rec.timeLastUsed = 123412352213L;
+ rec.timePasswordChanged = 110000000000L;
+ rec.timesUsed = 2L;
+ rec.encryptedUsername = "rnewman";
+ rec.usernameField = "box.user.field3";
+ return rec;
+ }
+
+ public static PasswordRecord createPassword4() {
+ PasswordRecord rec = new PasswordRecord();
+ rec.encType = "some type";
+ rec.formSubmitURL = "http://submit4.html";
+ rec.hostname = "http://hostname4";
+ rec.httpRealm = "httpRealm4";
+ rec.encryptedPassword ="54324";
+ rec.passwordField = "box.pass.field4";
+ rec.timeCreated = 101000000000L;
+ rec.timeLastUsed = 123412354444L;
+ rec.timePasswordChanged = 110000000000L;
+ rec.timesUsed = 4L;
+ rec.encryptedUsername = "rnewman4";
+ rec.usernameField = "box.user.field4";
+ return rec;
+ }
+
+ public static PasswordRecord createPassword5() {
+ PasswordRecord rec = new PasswordRecord();
+ rec.encType = "some type5";
+ rec.formSubmitURL = "http://submit5.html";
+ rec.hostname = "http://hostname5";
+ rec.httpRealm = "httpRealm5";
+ rec.encryptedPassword ="54325";
+ rec.passwordField = "box.pass.field5";
+ rec.timeCreated = 101000000000L;
+ rec.timeLastUsed = 123412352555L;
+ rec.timePasswordChanged = 111111111111L;
+ rec.timesUsed = 5L;
+ rec.encryptedUsername = "jvoll5";
+ rec.usernameField = "box.user.field5";
+ return rec;
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SessionTestHelper.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SessionTestHelper.java
new file mode 100644
index 0000000000..9c9e6719bf
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SessionTestHelper.java
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import static junit.framework.Assert.assertNotNull;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+
+import android.content.Context;
+
+public class SessionTestHelper {
+
+ protected static RepositorySession prepareRepositorySession(
+ final Context context,
+ final boolean begin,
+ final Repository repository) {
+
+ final WaitHelper testWaiter = WaitHelper.getTestWaiter();
+
+ final String logTag = "prepareRepositorySession";
+ class CreationDelegate extends DefaultSessionCreationDelegate {
+ private RepositorySession session;
+ synchronized void setSession(RepositorySession session) {
+ this.session = session;
+ }
+ synchronized RepositorySession getSession() {
+ return this.session;
+ }
+
+ @Override
+ public void onSessionCreated(final RepositorySession session) {
+ assertNotNull(session);
+ Logger.info(logTag, "Setting session to " + session);
+ setSession(session);
+ if (begin) {
+ Logger.info(logTag, "Calling session.begin on new session.");
+ // The begin callbacks will notify.
+ try {
+ session.begin(new ExpectBeginDelegate());
+ } catch (InvalidSessionTransitionException e) {
+ testWaiter.performNotify(e);
+ }
+ } else {
+ Logger.info(logTag, "Notifying after setting new session.");
+ testWaiter.performNotify();
+ }
+ }
+ }
+
+ final CreationDelegate delegate = new CreationDelegate();
+ try {
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ repository.createSession(delegate, context);
+ }
+ };
+ testWaiter.performWait(runnable);
+ } catch (IllegalArgumentException ex) {
+ Logger.warn(logTag, "Caught IllegalArgumentException.");
+ }
+
+ Logger.info(logTag, "Retrieving new session.");
+ final RepositorySession session = delegate.getSession();
+ assertNotNull(session);
+
+ return session;
+ }
+
+ public static RepositorySession createSession(final Context context, final Repository repository) {
+ return prepareRepositorySession(context, false, repository);
+ }
+
+ public static RepositorySession createAndBeginSession(Context context, Repository repository) {
+ return prepareRepositorySession(context, true, repository);
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessBeginDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessBeginDelegate.java
new file mode 100644
index 0000000000..0eb477be7b
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessBeginDelegate.java
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import java.util.concurrent.ExecutorService;
+
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
+
+public abstract class SimpleSuccessBeginDelegate extends DefaultDelegate implements RepositorySessionBeginDelegate {
+ @Override
+ public void onBeginFailed(Exception ex) {
+ performNotify("Begin failed", ex);
+ }
+
+ @Override
+ public RepositorySessionBeginDelegate deferredBeginDelegate(ExecutorService executor) {
+ return this;
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessCreationDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessCreationDelegate.java
new file mode 100644
index 0000000000..3b3b3d5fa8
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessCreationDelegate.java
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+
+public abstract class SimpleSuccessCreationDelegate extends DefaultDelegate implements RepositorySessionCreationDelegate {
+ @Override
+ public void onSessionCreateFailed(Exception ex) {
+ performNotify("Session creation failed", ex);
+ }
+
+ @Override
+ public RepositorySessionCreationDelegate deferredCreationDelegate() {
+ return this;
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFetchDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFetchDelegate.java
new file mode 100644
index 0000000000..f0e9428ba1
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFetchDelegate.java
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import java.util.concurrent.ExecutorService;
+
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+public abstract class SimpleSuccessFetchDelegate extends DefaultDelegate implements
+ RepositorySessionFetchRecordsDelegate {
+ @Override
+ public void onFetchFailed(Exception ex, Record record) {
+ performNotify("Fetch failed", ex);
+ }
+
+ @Override
+ public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService executor) {
+ return this;
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFinishDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFinishDelegate.java
new file mode 100644
index 0000000000..5ac1bcde78
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFinishDelegate.java
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import java.util.concurrent.ExecutorService;
+
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
+
+public abstract class SimpleSuccessFinishDelegate extends DefaultDelegate implements RepositorySessionFinishDelegate {
+ @Override
+ public void onFinishFailed(Exception ex) {
+ performNotify("Finish failed", ex);
+ }
+
+ @Override
+ public RepositorySessionFinishDelegate deferredFinishDelegate(ExecutorService executor) {
+ return this;
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessStoreDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessStoreDelegate.java
new file mode 100644
index 0000000000..c725c4a469
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessStoreDelegate.java
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.sync.helpers;
+
+import java.util.concurrent.ExecutorService;
+
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
+
+public abstract class SimpleSuccessStoreDelegate extends DefaultDelegate implements RepositorySessionStoreDelegate {
+ @Override
+ public void onRecordStoreFailed(Exception ex, String guid) {
+ performNotify("Store failed", ex);
+ }
+
+ @Override
+ public RepositorySessionStoreDelegate deferredStoreDelegate(ExecutorService executor) {
+ return this;
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java
new file mode 100644
index 0000000000..d2a8b84764
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import org.mozilla.gecko.sync.NoCollectionKeysSetException;
+import org.mozilla.gecko.sync.SynchronizerConfiguration;
+import org.mozilla.gecko.sync.repositories.RecordFactory;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.stage.ServerSyncStage;
+
+import java.net.URISyntaxException;
+
+/**
+ * A stage that joins two Repositories with no wrapping.
+ */
+public abstract class BaseMockServerSyncStage extends ServerSyncStage {
+
+ public Repository local;
+ public Repository remote;
+ public String name;
+ public String collection;
+ public int version = 1;
+
+ @Override
+ public boolean isEnabled() {
+ return true;
+ }
+
+ @Override
+ protected String getCollection() {
+ return collection;
+ }
+
+ @Override
+ protected Repository getLocalRepository() {
+ return local;
+ }
+
+ @Override
+ protected Repository getRemoteRepository() throws URISyntaxException {
+ return remote;
+ }
+
+ @Override
+ protected String getEngineName() {
+ return name;
+ }
+
+ @Override
+ public Integer getStorageVersion() {
+ return version;
+ }
+
+ @Override
+ protected RecordFactory getRecordFactory() {
+ return null;
+ }
+
+ @Override
+ protected Repository wrappedServerRepo()
+ throws NoCollectionKeysSetException, URISyntaxException {
+ return getRemoteRepository();
+ }
+
+ public SynchronizerConfiguration leakConfig() throws Exception {
+ return this.getConfig();
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/CommandHelpers.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/CommandHelpers.java
new file mode 100644
index 0000000000..48217f1b06
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/CommandHelpers.java
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import org.json.simple.JSONArray;
+import org.mozilla.gecko.sync.CommandProcessor.Command;
+
+public class CommandHelpers {
+
+ @SuppressWarnings("unchecked")
+ public static Command getCommand1() {
+ JSONArray args = new JSONArray();
+ args.add("argsA");
+ return new Command("displayURI", args);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Command getCommand2() {
+ JSONArray args = new JSONArray();
+ args.add("argsB");
+ return new Command("displayURI", args);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Command getCommand3() {
+ JSONArray args = new JSONArray();
+ args.add("argsC");
+ return new Command("displayURI", args);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Command getCommand4() {
+ JSONArray args = new JSONArray();
+ args.add("URI of Page");
+ args.add("Sender ID");
+ args.add("Title of Page");
+ return new Command("displayURI", args);
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java
new file mode 100644
index 0000000000..c8be7e3305
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import java.net.URI;
+
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
+
+public class DefaultGlobalSessionCallback implements GlobalSessionCallback {
+
+ @Override
+ public void requestBackoff(long backoff) {
+ }
+
+ @Override
+ public void informUnauthorizedResponse(GlobalSession globalSession,
+ URI oldClusterURL) {
+ }
+
+ @Override
+ public void informUpgradeRequiredResponse(GlobalSession session) {
+ }
+
+ @Override
+ public void informMigrated(GlobalSession globalSession) {
+ }
+
+ @Override
+ public void handleAborted(GlobalSession globalSession, String reason) {
+ }
+
+ @Override
+ public void handleError(GlobalSession globalSession, Exception ex) {
+ }
+
+ @Override
+ public void handleSuccess(GlobalSession globalSession) {
+ }
+
+ @Override
+ public void handleStageCompleted(Stage currentState,
+ GlobalSession globalSession) {
+ }
+
+ @Override
+ public boolean shouldBackOffStorage() {
+ return false;
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockAbstractNonRepositorySyncStage.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockAbstractNonRepositorySyncStage.java
new file mode 100644
index 0000000000..d8380df972
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockAbstractNonRepositorySyncStage.java
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import org.mozilla.gecko.sync.stage.AbstractNonRepositorySyncStage;
+
+public class MockAbstractNonRepositorySyncStage extends AbstractNonRepositorySyncStage {
+ @Override
+ public void execute() {
+ session.advance();
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockClientsDataDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockClientsDataDelegate.java
new file mode 100644
index 0000000000..f4af51f648
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockClientsDataDelegate.java
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
+
+public class MockClientsDataDelegate implements ClientsDataDelegate {
+ private String accountGUID;
+ private String clientName;
+ private int clientsCount;
+ private long clientDataTimestamp = 0;
+
+ @Override
+ public synchronized String getAccountGUID() {
+ if (accountGUID == null) {
+ accountGUID = Utils.generateGuid();
+ }
+ return accountGUID;
+ }
+
+ @Override
+ public synchronized String getDefaultClientName() {
+ return "Default client";
+ }
+
+ @Override
+ public synchronized void setClientName(String clientName, long now) {
+ this.clientName = clientName;
+ this.clientDataTimestamp = now;
+ }
+
+ @Override
+ public synchronized String getClientName() {
+ if (clientName == null) {
+ setClientName(getDefaultClientName(), System.currentTimeMillis());
+ }
+ return clientName;
+ }
+
+ @Override
+ public synchronized void setClientsCount(int clientsCount) {
+ this.clientsCount = clientsCount;
+ }
+
+ @Override
+ public synchronized int getClientsCount() {
+ return clientsCount;
+ }
+
+ @Override
+ public synchronized boolean isLocalGUID(String guid) {
+ return getAccountGUID().equals(guid);
+ }
+
+ @Override
+ public synchronized long getLastModifiedTimestamp() {
+ return clientDataTimestamp;
+ }
+
+ @Override
+ public String getFormFactor() {
+ return "phone";
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockClientsDatabaseAccessor.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockClientsDatabaseAccessor.java
new file mode 100644
index 0000000000..5e574e33d3
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockClientsDatabaseAccessor.java
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.mozilla.gecko.sync.CommandProcessor.Command;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
+import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
+
+public class MockClientsDatabaseAccessor extends ClientsDatabaseAccessor {
+ public boolean storedRecord = false;
+ public boolean dbWiped = false;
+ public boolean clientsTableWiped = false;
+ public boolean closed = false;
+ public boolean storedArrayList = false;
+ public boolean storedCommand;
+
+ @Override
+ public void store(ClientRecord record) {
+ storedRecord = true;
+ }
+
+ @Override
+ public void store(Collection<ClientRecord> records) {
+ storedArrayList = false;
+ }
+
+ @Override
+ public void store(String accountGUID, Command command) throws NullCursorException {
+ storedCommand = true;
+ }
+
+ @Override
+ public ClientRecord fetchClient(String profileID) throws NullCursorException {
+ return null;
+ }
+
+ @Override
+ public Map<String, ClientRecord> fetchAllClients() throws NullCursorException {
+ return null;
+ }
+
+ @Override
+ public List<Command> fetchCommandsForClient(String accountGUID) throws NullCursorException {
+ return null;
+ }
+
+ @Override
+ public int clientsCount() {
+ return 0;
+ }
+
+ @Override
+ public void wipeDB() {
+ dbWiped = true;
+ }
+
+ @Override
+ public void wipeClientsTable() {
+ clientsTableWiped = true;
+ }
+
+ @Override
+ public void close() {
+ closed = true;
+ }
+
+ public void resetVars() {
+ storedRecord = dbWiped = clientsTableWiped = closed = storedArrayList = false;
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java
new file mode 100644
index 0000000000..63afdd1ac0
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import org.mozilla.gecko.sync.EngineSettings;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.SyncConfiguration;
+import org.mozilla.gecko.sync.SyncConfigurationException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+import org.mozilla.gecko.sync.stage.CompletedStage;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+
+public class MockGlobalSession extends MockPrefsGlobalSession {
+
+ public MockGlobalSession(String username, String password, KeyBundle keyBundle, GlobalSessionCallback callback) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException {
+ this(new SyncConfiguration(username, new BasicAuthHeaderProvider(username, password), new MockSharedPreferences(), keyBundle), callback);
+ }
+
+ public MockGlobalSession(SyncConfiguration config, GlobalSessionCallback callback)
+ throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException {
+ super(config, callback, null, null);
+ }
+
+ @Override
+ public boolean isEngineRemotelyEnabled(String engine, EngineSettings engineSettings) {
+ return false;
+ }
+
+ @Override
+ protected void prepareStages() {
+ super.prepareStages();
+ HashMap<Stage, GlobalSyncStage> newStages = new HashMap<Stage, GlobalSyncStage>(this.stages);
+
+ for (Stage stage : this.stages.keySet()) {
+ newStages.put(stage, new MockServerSyncStage());
+ }
+
+ // This signals that the global session is complete.
+ newStages.put(Stage.completed, new CompletedStage());
+
+ this.stages = newStages;
+ }
+
+ public MockGlobalSession withStage(Stage stage, GlobalSyncStage syncStage) {
+ stages.put(stage, syncStage);
+
+ return this;
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java
new file mode 100644
index 0000000000..2ff29453f7
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.SyncConfiguration;
+import org.mozilla.gecko.sync.SyncConfigurationException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
+import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+
+import java.io.IOException;
+
+/**
+ * GlobalSession touches the Android prefs system. Stub that out.
+ */
+public class MockPrefsGlobalSession extends GlobalSession {
+
+ public MockSharedPreferences prefs;
+
+ public MockPrefsGlobalSession(
+ SyncConfiguration config, GlobalSessionCallback callback, Context context,
+ ClientsDataDelegate clientsDelegate)
+ throws SyncConfigurationException, IllegalArgumentException, IOException,
+ NonObjectJSONException {
+ super(config, callback, context, clientsDelegate);
+ }
+
+ public static MockPrefsGlobalSession getSession(
+ String username, String password,
+ KeyBundle syncKeyBundle, GlobalSessionCallback callback, Context context,
+ ClientsDataDelegate clientsDelegate)
+ throws SyncConfigurationException, IllegalArgumentException, IOException,
+ NonObjectJSONException {
+ return getSession(username, new BasicAuthHeaderProvider(username, password), null,
+ syncKeyBundle, callback, context, clientsDelegate);
+ }
+
+ public static MockPrefsGlobalSession getSession(
+ String username, AuthHeaderProvider authHeaderProvider, String prefsPath,
+ KeyBundle syncKeyBundle, GlobalSessionCallback callback, Context context,
+ ClientsDataDelegate clientsDelegate)
+ throws SyncConfigurationException, IllegalArgumentException, IOException,
+ NonObjectJSONException {
+
+ final SharedPreferences prefs = new MockSharedPreferences();
+ final SyncConfiguration config = new SyncConfiguration(username, authHeaderProvider, prefs);
+ config.syncKeyBundle = syncKeyBundle;
+ return new MockPrefsGlobalSession(config, callback, context, clientsDelegate);
+ }
+
+ @Override
+ public Context getContext() {
+ return null;
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockRecord.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockRecord.java
new file mode 100644
index 0000000000..d3a05bcecf
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockRecord.java
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+public class MockRecord extends Record {
+
+ public MockRecord(String guid, String collection, long lastModified, boolean deleted) {
+ super(guid, collection, lastModified, deleted);
+ }
+
+ @Override
+ protected void populatePayload(ExtendedJSONObject payload) {
+ }
+
+ @Override
+ protected void initFromPayload(ExtendedJSONObject payload) {
+ }
+
+ @Override
+ public Record copyWithIDs(String guid, long androidID) {
+ MockRecord r = new MockRecord(guid, this.collection, this.lastModified, this.deleted);
+ r.androidID = androidID;
+ return r;
+ }
+
+ @Override
+ public String toJSONString() {
+ return "{\"id\":\"" + guid + "\", \"payload\": \"foo\"}";
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java
new file mode 100644
index 0000000000..02b72b6765
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java
@@ -0,0 +1,12 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+
+public class MockServerSyncStage extends BaseMockServerSyncStage {
+ @Override
+ public void execute() {
+ session.advance();
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockSharedPreferences.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockSharedPreferences.java
new file mode 100644
index 0000000000..bc49fa7fba
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockSharedPreferences.java
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import android.content.SharedPreferences;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A programmable mock content provider.
+ */
+public class MockSharedPreferences implements SharedPreferences, SharedPreferences.Editor {
+ private HashMap<String, Object> mValues;
+ private HashMap<String, Object> mTempValues;
+
+ public MockSharedPreferences() {
+ mValues = new HashMap<String, Object>();
+ mTempValues = new HashMap<String, Object>();
+ }
+
+ public Editor edit() {
+ return this;
+ }
+
+ public boolean contains(String key) {
+ return mValues.containsKey(key);
+ }
+
+ public Map<String, ?> getAll() {
+ return new HashMap<String, Object>(mValues);
+ }
+
+ public boolean getBoolean(String key, boolean defValue) {
+ if (mValues.containsKey(key)) {
+ return ((Boolean)mValues.get(key)).booleanValue();
+ }
+ return defValue;
+ }
+
+ public float getFloat(String key, float defValue) {
+ if (mValues.containsKey(key)) {
+ return ((Float)mValues.get(key)).floatValue();
+ }
+ return defValue;
+ }
+
+ public int getInt(String key, int defValue) {
+ if (mValues.containsKey(key)) {
+ return ((Integer)mValues.get(key)).intValue();
+ }
+ return defValue;
+ }
+
+ public long getLong(String key, long defValue) {
+ if (mValues.containsKey(key)) {
+ return ((Long)mValues.get(key)).longValue();
+ }
+ return defValue;
+ }
+
+ public String getString(String key, String defValue) {
+ if (mValues.containsKey(key))
+ return (String)mValues.get(key);
+ return defValue;
+ }
+
+ @SuppressWarnings("unchecked")
+ public Set<String> getStringSet(String key, Set<String> defValues) {
+ if (mValues.containsKey(key)) {
+ return (Set<String>) mValues.get(key);
+ }
+ return defValues;
+ }
+
+ public void registerOnSharedPreferenceChangeListener(
+ OnSharedPreferenceChangeListener listener) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void unregisterOnSharedPreferenceChangeListener(
+ OnSharedPreferenceChangeListener listener) {
+ throw new UnsupportedOperationException();
+ }
+
+ public Editor putBoolean(String key, boolean value) {
+ mTempValues.put(key, Boolean.valueOf(value));
+ return this;
+ }
+
+ public Editor putFloat(String key, float value) {
+ mTempValues.put(key, value);
+ return this;
+ }
+
+ public Editor putInt(String key, int value) {
+ mTempValues.put(key, value);
+ return this;
+ }
+
+ public Editor putLong(String key, long value) {
+ mTempValues.put(key, value);
+ return this;
+ }
+
+ public Editor putString(String key, String value) {
+ mTempValues.put(key, value);
+ return this;
+ }
+
+ public Editor putStringSet(String key, Set<String> values) {
+ mTempValues.put(key, values);
+ return this;
+ }
+
+ public Editor remove(String key) {
+ mTempValues.remove(key);
+ return this;
+ }
+
+ public Editor clear() {
+ mTempValues.clear();
+ return this;
+ }
+
+ @SuppressWarnings("unchecked")
+ public boolean commit() {
+ mValues = (HashMap<String, Object>)mTempValues.clone();
+ return true;
+ }
+
+ public void apply() {
+ commit();
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/WBORepository.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/WBORepository.java
new file mode 100644
index 0000000000..bd2e7f791c
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/WBORepository.java
@@ -0,0 +1,231 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.repositories.InactiveSessionException;
+import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
+import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
+import org.mozilla.gecko.sync.repositories.RecordFilter;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.StoreTrackingRepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import android.content.Context;
+
+public class WBORepository extends Repository {
+
+ public class WBORepositoryStats {
+ public long created = -1;
+ public long begun = -1;
+ public long fetchBegan = -1;
+ public long fetchCompleted = -1;
+ public long storeBegan = -1;
+ public long storeCompleted = -1;
+ public long finished = -1;
+ }
+
+ public static final String LOG_TAG = "WBORepository";
+
+ // Access to stats is not guarded.
+ public WBORepositoryStats stats;
+
+ // Whether or not to increment the timestamp of stored records.
+ public final boolean bumpTimestamps;
+
+ public class WBORepositorySession extends StoreTrackingRepositorySession {
+
+ protected WBORepository wboRepository;
+ protected ExecutorService delegateExecutor = Executors.newSingleThreadExecutor();
+ public ConcurrentHashMap<String, Record> wbos;
+
+ public WBORepositorySession(WBORepository repository) {
+ super(repository);
+
+ wboRepository = repository;
+ wbos = new ConcurrentHashMap<String, Record>();
+ stats = new WBORepositoryStats();
+ stats.created = now();
+ }
+
+ @Override
+ protected synchronized void trackGUID(String guid) {
+ if (wboRepository.shouldTrack()) {
+ super.trackGUID(guid);
+ }
+ }
+
+ @Override
+ public void guidsSince(long timestamp,
+ RepositorySessionGuidsSinceDelegate delegate) {
+ throw new RuntimeException("guidsSince not implemented.");
+ }
+
+ @Override
+ public void fetchSince(long timestamp,
+ RepositorySessionFetchRecordsDelegate delegate) {
+ long fetchBegan = now();
+ stats.fetchBegan = fetchBegan;
+ RecordFilter filter = storeTracker.getFilter();
+
+ for (Entry<String, Record> entry : wbos.entrySet()) {
+ Record record = entry.getValue();
+ if (record.lastModified >= timestamp) {
+ if (filter != null &&
+ filter.excludeRecord(record)) {
+ Logger.debug(LOG_TAG, "Excluding record " + record.guid);
+ continue;
+ }
+ delegate.deferredFetchDelegate(delegateExecutor).onFetchedRecord(record);
+ }
+ }
+ long fetchCompleted = now();
+ stats.fetchCompleted = fetchCompleted;
+ delegate.deferredFetchDelegate(delegateExecutor).onFetchCompleted(fetchCompleted);
+ }
+
+ @Override
+ public void fetch(final String[] guids,
+ final RepositorySessionFetchRecordsDelegate delegate) {
+ long fetchBegan = now();
+ stats.fetchBegan = fetchBegan;
+ for (String guid : guids) {
+ if (wbos.containsKey(guid)) {
+ delegate.deferredFetchDelegate(delegateExecutor).onFetchedRecord(wbos.get(guid));
+ }
+ }
+ long fetchCompleted = now();
+ stats.fetchCompleted = fetchCompleted;
+ delegate.deferredFetchDelegate(delegateExecutor).onFetchCompleted(fetchCompleted);
+ }
+
+ @Override
+ public void fetchAll(final RepositorySessionFetchRecordsDelegate delegate) {
+ long fetchBegan = now();
+ stats.fetchBegan = fetchBegan;
+ for (Entry<String, Record> entry : wbos.entrySet()) {
+ Record record = entry.getValue();
+ delegate.deferredFetchDelegate(delegateExecutor).onFetchedRecord(record);
+ }
+ long fetchCompleted = now();
+ stats.fetchCompleted = fetchCompleted;
+ delegate.deferredFetchDelegate(delegateExecutor).onFetchCompleted(fetchCompleted);
+ }
+
+ @Override
+ public void store(final Record record) throws NoStoreDelegateException {
+ if (delegate == null) {
+ throw new NoStoreDelegateException();
+ }
+ final long now = now();
+ if (stats.storeBegan < 0) {
+ stats.storeBegan = now;
+ }
+ Record existing = wbos.get(record.guid);
+ Logger.debug(LOG_TAG, "Existing record is " + (existing == null ? "<null>" : (existing.guid + ", " + existing)));
+ if (existing != null &&
+ existing.lastModified > record.lastModified) {
+ Logger.debug(LOG_TAG, "Local record is newer. Not storing.");
+ delegate.deferredStoreDelegate(delegateExecutor).onRecordStoreSucceeded(record.guid);
+ return;
+ }
+ if (existing != null) {
+ Logger.debug(LOG_TAG, "Replacing local record.");
+ }
+
+ // Store a copy of the record with an updated modified time.
+ Record toStore = record.copyWithIDs(record.guid, record.androidID);
+ if (bumpTimestamps) {
+ toStore.lastModified = now;
+ }
+ wbos.put(record.guid, toStore);
+
+ trackRecord(toStore);
+ delegate.deferredStoreDelegate(delegateExecutor).onRecordStoreSucceeded(record.guid);
+ }
+
+ @Override
+ public void wipe(final RepositorySessionWipeDelegate delegate) {
+ if (!isActive()) {
+ delegate.onWipeFailed(new InactiveSessionException(null));
+ return;
+ }
+
+ Logger.info(LOG_TAG, "Wiping WBORepositorySession.");
+ this.wbos = new ConcurrentHashMap<String, Record>();
+
+ // Wipe immediately for the convenience of test code.
+ wboRepository.wbos = new ConcurrentHashMap<String, Record>();
+ delegate.deferredWipeDelegate(delegateExecutor).onWipeSucceeded();
+ }
+
+ @Override
+ public void finish(RepositorySessionFinishDelegate delegate) throws InactiveSessionException {
+ Logger.info(LOG_TAG, "Finishing WBORepositorySession: handing back " + this.wbos.size() + " WBOs.");
+ wboRepository.wbos = this.wbos;
+ stats.finished = now();
+ super.finish(delegate);
+ }
+
+ @Override
+ public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException {
+ this.wbos = wboRepository.cloneWBOs();
+ stats.begun = now();
+ super.begin(delegate);
+ }
+
+ @Override
+ public void storeDone(long end) {
+ // TODO: this is not guaranteed to be called after all of the record
+ // store callbacks have completed!
+ if (stats.storeBegan < 0) {
+ stats.storeBegan = end;
+ }
+ stats.storeCompleted = end;
+ delegate.deferredStoreDelegate(delegateExecutor).onStoreCompleted(end);
+ }
+ }
+
+ public ConcurrentHashMap<String, Record> wbos;
+
+ public WBORepository(boolean bumpTimestamps) {
+ super();
+ this.bumpTimestamps = bumpTimestamps;
+ this.wbos = new ConcurrentHashMap<String, Record>();
+ }
+
+ public WBORepository() {
+ this(false);
+ }
+
+ public synchronized boolean shouldTrack() {
+ return false;
+ }
+
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ delegate.deferredCreationDelegate().onSessionCreated(new WBORepositorySession(this));
+ }
+
+ public ConcurrentHashMap<String, Record> cloneWBOs() {
+ ConcurrentHashMap<String, Record> out = new ConcurrentHashMap<String, Record>();
+ for (Entry<String, Record> entry : wbos.entrySet()) {
+ out.put(entry.getKey(), entry.getValue()); // Assume that records are
+ // immutable.
+ }
+ return out;
+ }
+}
diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java
new file mode 100644
index 0000000000..1a84977cfe
--- /dev/null
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java
@@ -0,0 +1,171 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import org.mozilla.gecko.background.common.log.Logger;
+
+/**
+ * Implements waiting for asynchronous test events.
+ *
+ * Call WaitHelper.getTestWaiter() to get the unique instance.
+ *
+ * Call performWait(runnable) to execute runnable synchronously.
+ * runnable *must* call performNotify() on all exit paths to signal to
+ * the TestWaiter that the runnable has completed.
+ *
+ * @author rnewman
+ * @author nalexander
+ */
+public class WaitHelper {
+
+ public static final String LOG_TAG = "WaitHelper";
+
+ public static class Result {
+ public Throwable error;
+ public Result() {
+ error = null;
+ }
+
+ public Result(Throwable error) {
+ this.error = error;
+ }
+ }
+
+ public static abstract class WaitHelperError extends Error {
+ private static final long serialVersionUID = 7074690961681883619L;
+ }
+
+ /**
+ * Immutable.
+ *
+ * @author rnewman
+ */
+ public static class TimeoutError extends WaitHelperError {
+ private static final long serialVersionUID = 8591672555848651736L;
+ public final int waitTimeInMillis;
+
+ public TimeoutError(int waitTimeInMillis) {
+ this.waitTimeInMillis = waitTimeInMillis;
+ }
+ }
+
+ public static class MultipleNotificationsError extends WaitHelperError {
+ private static final long serialVersionUID = -9072736521571635495L;
+ }
+
+ public static class InterruptedError extends WaitHelperError {
+ private static final long serialVersionUID = 8383948170038639308L;
+ }
+
+ public static class InnerError extends WaitHelperError {
+ private static final long serialVersionUID = 3008502618576773778L;
+ public Throwable innerError;
+
+ public InnerError(Throwable e) {
+ innerError = e;
+ if (e != null) {
+ // Eclipse prints the stack trace of the cause.
+ this.initCause(e);
+ }
+ }
+ }
+
+ public BlockingQueue<Result> queue = new ArrayBlockingQueue<Result>(1);
+
+ /**
+ * How long performWait should wait for, in milliseconds, with the
+ * convention that a negative value means "wait forever".
+ */
+ public static int defaultWaitTimeoutInMillis = -1;
+
+ public void performWait(Runnable action) throws WaitHelperError {
+ this.performWait(defaultWaitTimeoutInMillis, action);
+ }
+
+ public void performWait(int waitTimeoutInMillis, Runnable action) throws WaitHelperError {
+ Logger.debug(LOG_TAG, "performWait called.");
+
+ Result result = null;
+
+ try {
+ if (action != null) {
+ try {
+ action.run();
+ Logger.debug(LOG_TAG, "Action done.");
+ } catch (Exception ex) {
+ Logger.debug(LOG_TAG, "Performing action threw: " + ex.getMessage());
+ throw new InnerError(ex);
+ }
+ }
+
+ if (waitTimeoutInMillis < 0) {
+ result = queue.take();
+ } else {
+ result = queue.poll(waitTimeoutInMillis, TimeUnit.MILLISECONDS);
+ }
+ Logger.debug(LOG_TAG, "Got result from queue: " + result);
+ } catch (InterruptedException e) {
+ // We were interrupted.
+ Logger.debug(LOG_TAG, "performNotify interrupted with InterruptedException " + e);
+ final InterruptedError interruptedError = new InterruptedError();
+ interruptedError.initCause(e);
+ throw interruptedError;
+ }
+
+ if (result == null) {
+ // We timed out.
+ throw new TimeoutError(waitTimeoutInMillis);
+ } else if (result.error != null) {
+ Logger.debug(LOG_TAG, "Notified with error: " + result.error.getMessage());
+
+ // Rethrow any assertion with which we were notified.
+ InnerError innerError = new InnerError(result.error);
+ throw innerError;
+ }
+ // Success!
+ }
+
+ public void performNotify(final Throwable e) {
+ if (e != null) {
+ Logger.debug(LOG_TAG, "performNotify called with Throwable: " + e.getMessage());
+ } else {
+ Logger.debug(LOG_TAG, "performNotify called.");
+ }
+
+ if (!queue.offer(new Result(e))) {
+ // This could happen if performNotify is called multiple times (which is an error).
+ throw new MultipleNotificationsError();
+ }
+ }
+
+ public void performNotify() {
+ this.performNotify(null);
+ }
+
+ public static Runnable onThreadRunnable(final Runnable r) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ new Thread(r).start();
+ }
+ };
+ }
+
+ private static WaitHelper singleWaiter = new WaitHelper();
+ public static WaitHelper getTestWaiter() {
+ return singleWaiter;
+ }
+
+ public static void resetTestWaiter() {
+ singleWaiter = new WaitHelper();
+ }
+
+ public boolean isIdle() {
+ return queue.isEmpty();
+ }
+}
diff --git a/mobile/android/tests/background/junit4/resources/dlc_sync_deleted_item.json b/mobile/android/tests/background/junit4/resources/dlc_sync_deleted_item.json
new file mode 100644
index 0000000000..50b04f0e22
--- /dev/null
+++ b/mobile/android/tests/background/junit4/resources/dlc_sync_deleted_item.json
@@ -0,0 +1,8 @@
+{
+ "data":[
+ {
+ "id":"c906275c-3747-fe27-426f-6187526a6f06",
+ "deleted": true
+ }
+ ]
+}
diff --git a/mobile/android/tests/background/junit4/resources/dlc_sync_old_format.json b/mobile/android/tests/background/junit4/resources/dlc_sync_old_format.json
new file mode 100644
index 0000000000..378bc64c68
--- /dev/null
+++ b/mobile/android/tests/background/junit4/resources/dlc_sync_old_format.json
@@ -0,0 +1,23 @@
+{
+ "data":[
+ {
+ "kind":"font",
+ "original": {
+ "mimetype":"application/x-font-ttf",
+ "filename":"CharisSILCompact-R.ttf",
+ "hash":"4ed509317f1bb441b185ea13bf1c9d19d1a0b396962efa3b5dc3190ad88f2067",
+ "size":1727656
+ },
+ "last_modified":1455710632607,
+ "attachment": {
+ "mimetype":"application/x-gzip",
+ "size":548720,
+ "hash":"960be4fc5a92c1dc488582b215d5d75429fd4ffbee463105d29992cd792a912e",
+ "location":"/attachments/0d28a72d-a51f-46f8-9e5a-f95c61de904e.gz",
+ "filename":"CharisSILCompact-R.ttf.gz"
+ },
+ "type":"asset-archive",
+ "id":"c906275c-3747-fe27-426f-6187526a6f06"
+ }
+ ]
+}
diff --git a/mobile/android/tests/background/junit4/resources/dlc_sync_single_font.json b/mobile/android/tests/background/junit4/resources/dlc_sync_single_font.json
new file mode 100644
index 0000000000..0de84b85dd
--- /dev/null
+++ b/mobile/android/tests/background/junit4/resources/dlc_sync_single_font.json
@@ -0,0 +1,23 @@
+{
+ "data":[
+ {
+ "kind":"font",
+ "last_modified":1455710632607,
+ "attachment": {
+ "mimetype":"application/x-gzip",
+ "size":548720,
+ "hash":"960be4fc5a92c1dc488582b215d5d75429fd4ffbee463105d29992cd792a912e",
+ "location":"/attachments/0d28a72d-a51f-46f8-9e5a-f95c61de904e.gz",
+ "filename":"CharisSILCompact-R.ttf.gz",
+ "original": {
+ "mimetype":"application/x-font-ttf",
+ "filename":"CharisSILCompact-R.ttf",
+ "hash":"4ed509317f1bb441b185ea13bf1c9d19d1a0b396962efa3b5dc3190ad88f2067",
+ "size":1727656
+ }
+ },
+ "type":"asset-archive",
+ "id":"c906275c-3747-fe27-426f-6187526a6f06"
+ }
+ ]
+}
diff --git a/mobile/android/tests/background/junit4/resources/experiments.json b/mobile/android/tests/background/junit4/resources/experiments.json
new file mode 100644
index 0000000000..870a577781
--- /dev/null
+++ b/mobile/android/tests/background/junit4/resources/experiments.json
@@ -0,0 +1,99 @@
+
+{
+ "data": [
+ {
+ "name": "active-experiment",
+ "match": {
+ },
+ "buckets": {
+ "min": "0",
+ "max": "100"
+ },
+ "values": {
+ "foo": true
+ }
+ },
+
+ {
+ "name": "inactive-experiment",
+ "match": {
+ "appId": "^NOPE$"
+ },
+ "buckets": {
+ "min": "0",
+ "max": "0"
+ }
+ },
+ {
+ "name": "bookmark-history-menu",
+ "match": {
+ },
+ "buckets": {
+ "min": "0",
+ "max": "100"
+ }
+ },
+ {
+ "name": "is-matching",
+ "match": {
+ "appId": "^org.mozilla.gecko$"
+ },
+ "buckets": {
+ "min": "0",
+ "max": "100"
+ }
+ },
+ {
+ "name": "is-not-matching",
+ "match": {
+ "appId": "^org.mozilla.fennec|^org.mozilla.firefox_beta$"
+ },
+ "buckets": {
+ "min": "0",
+ "max": "100"
+ }
+ },
+ {
+ "name": "promote-add-to-homescreen",
+ "buckets": {
+ "max": "100",
+ "min": "50"
+ },
+ "last_modified": 1467705654772,
+ "values": {
+ "lastVisitMaximumAgeMs": 600000,
+ "minimumTotalVisits": 5,
+ "lastVisitMinimumAgeMs": 30000
+ },
+ "id": "20d278d7-0d35-4811-8f01-bf24e31ba51b",
+ "match": {
+ "appId": "^org.mozilla.fennec|^org.mozilla.firefox_beta$"
+ },
+ "schema": 1467705310595
+ },
+ {
+ "name": "offline-cache",
+ "buckets": {
+ "max": "100",
+ "min": "0"
+ },
+ "last_modified": 1467705429859,
+ "id": "9f1ea043-c1d8-48ba-802d-aeabaf667afe",
+ "match": {
+ "appId": "^org.mozilla.fennec|^org.mozilla.firefox_beta$"
+ },
+ "schema": 1467705310595
+ },
+ {
+ "name": "bookmark-history-menu",
+ "buckets": {
+ "max": "100",
+ "min": "0"
+ },
+ "last_modified": 1467705381971,
+ "id": "29988035-1a59-4671-b679-2c717a68bd12",
+ "match": {},
+ "schema": 1467705310595
+ }
+ ]
+}
diff --git a/mobile/android/tests/background/junit4/resources/feed_atom_blogger.xml b/mobile/android/tests/background/junit4/resources/feed_atom_blogger.xml
new file mode 100644
index 0000000000..994876d762
--- /dev/null
+++ b/mobile/android/tests/background/junit4/resources/feed_atom_blogger.xml
@@ -0,0 +1,13 @@
+<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:blogger='http://schemas.google.com/blogger/2008' xmlns:georss='http://www.georss.org/georss' xmlns:gd="http://schemas.google.com/g/2005" xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-18929277</id><updated>2016-02-18T09:07:17.583-08:00</updated><category term="jetpack"/><title type='text'>mykzilla</title><subtitle type='html'></subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><link rel='next' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default?start-index=26&amp;max-results=25'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/01837818348188071923</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-_YZppM5V97U/VedxzrpG9BI/AAAAAAAAAGA/wHrKKAgCoH0/s220/headshot-2014.jpg'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>114</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-18929277.post-3538029308224239292</id><published>2016-01-11T08:57:00.001-08:00</published><updated>2016-01-11T08:57:31.366-08:00</updated><title type='text'>URL Has Been Changed</title><content type='html'>&lt;dl&gt;&lt;dd&gt;The URL you have reached, &lt;a href=&quot;http://mykzilla.blogspot.com/&quot;&gt;http://mykzilla.blogspot.com/&lt;/a&gt;, has been changed. The new URL is &lt;a href=&quot;https://mykzilla.org/&quot;&gt;https://mykzilla.org/&lt;/a&gt;. Please make a note of it.&lt;/dd&gt;&lt;/dl&gt;</content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/3538029308224239292/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=3538029308224239292' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/3538029308224239292'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/3538029308224239292'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2016/01/url-has-been-changed.html' title='URL Has Been Changed'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/08518329693863067865</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-7658939959003799797</id><published>2015-06-23T16:05:00.000-07:00</published><updated>2015-06-23T16:07:06.667-07:00</updated><title type='text'>Introducing PluotSorbet</title><content type='html'>&lt;a href=&quot;https://github.com/mozilla/pluotsorbet&quot;&gt;PluotSorbet&lt;/a&gt; is a &lt;a href=&quot;https://en.wikipedia.org/wiki/Java_Platform,_Micro_Edition&quot;&gt;J2ME&lt;/a&gt;-compatible virtual machine written in JavaScript. Its goal is to enable users you run J2ME apps (i.e. &lt;a href=&quot;https://en.wikipedia.org/wiki/MIDlet&quot;&gt;MIDlets&lt;/a&gt;) in web apps without a native plugin. It does this by interpreting Java bytecode and compiling it to JavaScript code. It also provides a virtual filesystem (via &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API&quot;&gt;IndexedDB&lt;/a&gt;), network sockets (through the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/TCPSocket&quot;&gt;TCPSocket API&lt;/a&gt;), and other common J2ME APIs, like &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Contacts_API&quot;&gt;Contacts&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;The project reuses as much existing code as possible, to minimize its surface area and maximize its compatibility with other J2ME implementations. It incorporates the &lt;a href=&quot;https://java.net/projects/phoneme&quot;&gt;PhoneME&lt;/a&gt; reference implementation, numerous tests from &lt;a href=&quot;https://www.sourceware.org/mauve/&quot;&gt;Mauve&lt;/a&gt;, and a variety of JavaScript libraries (including &lt;a href=&quot;http://www-cs-students.stanford.edu/%7Etjw/jsbn/&quot;&gt;jsbn&lt;/a&gt;, &lt;a href=&quot;https://github.com/digitalbazaar/forge&quot;&gt;Forge&lt;/a&gt;, and &lt;a href=&quot;https://github.com/eligrey/FileSaver.js&quot;&gt;FileSaver.js&lt;/a&gt;). The virtual machine is originally based on &lt;a href=&quot;https://github.com/YaroslavGaponov/node-jvm&quot;&gt;node-jvm&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;PluotSorbet makes it possible to bring J2ME apps to Firefox OS. J2ME may be a moribund platform, but it still has &lt;a href=&quot;http://netmarketshare.com/operating-system-market-share.aspx?qprid=9&amp;amp;qpcustom=Java+ME&amp;amp;qpcustomb=1&quot;&gt;non-negligible market share&lt;/a&gt;, not to mention a number of useful apps. So it retains residual value, which PluotSorbet can extend to Firefox OS devices.&lt;br /&gt;&lt;br /&gt;PluotSorbet is also still under development, with a variety of issues to address. To learn more about PluotSorbet, check out its &lt;a href=&quot;https://github.com/mozilla/pluotsorbet/blob/master/README.md&quot;&gt;README&lt;/a&gt;, clone its &lt;a href=&quot;https://github.com/mozilla/pluotsorbet&quot;&gt;Git repository&lt;/a&gt;, peruse its &lt;a href=&quot;https://github.com/mozilla/pluotsorbet/issues&quot;&gt;issue tracker&lt;/a&gt;, and say hello to its developers in &lt;a href=&quot;irc://irc.mozilla.org/pluotsorbet&quot;&gt;irc.mozilla.org#pluotsorbet&lt;/a&gt;!&lt;br /&gt;&lt;br /&gt;</content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/7658939959003799797/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=7658939959003799797' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/7658939959003799797'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/7658939959003799797'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2015/06/introducing-pluotsorbet.html' title='Introducing PluotSorbet'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/08518329693863067865</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-2146007198307190404</id><published>2014-03-28T16:58:00.002-07:00</published><updated>2014-03-28T16:58:42.947-07:00</updated><title type='text'>simplify asynchronous method declarations with Task.async()</title><content type='html'>In Mozilla code, &lt;span style=&quot;font-family: &amp;quot;Courier New&amp;quot;,Courier,monospace;&quot;&gt;Task.spawn()&lt;/span&gt; is becoming a common way to implement asynchronous operations, especially methods like the &lt;span style=&quot;font-family: &amp;quot;Courier New&amp;quot;,Courier,monospace;&quot;&gt;greet&lt;/span&gt; method in this &lt;span style=&quot;font-family: &amp;quot;Courier New&amp;quot;,Courier,monospace;&quot;&gt;greeter&lt;/span&gt; object:&lt;br /&gt;&lt;br /&gt;&lt;div style=&quot;background: #202020; border-width: .1em .1em .1em .8em; border: solid gray; overflow: auto; padding: .2em .6em; width: auto;&quot;&gt;&lt;pre style=&quot;line-height: 125%; margin: 0;&quot;&gt;&lt;span style=&quot;color: #6ab825; font-weight: bold;&quot;&gt;let&lt;/span&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;greeter&lt;/span&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;{&lt;/span&gt;&lt;br /&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;message:&lt;/span&gt; &lt;span style=&quot;color: #ed9d13;&quot;&gt;&quot;Hello, NAME!&quot;&lt;/span&gt;&lt;span style=&quot;color: #d0d0d0;&quot;&gt;,&lt;/span&gt;&lt;br /&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;greet:&lt;/span&gt; &lt;span style=&quot;color: #6ab825; font-weight: bold;&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color: #d0d0d0;&quot;&gt;(name)&lt;/span&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;{&lt;/span&gt;&lt;br /&gt; &lt;span style=&quot;color: #6ab825; font-weight: bold;&quot;&gt;return&lt;/span&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;Task.spawn((&lt;/span&gt;&lt;span style=&quot;color: #6ab825; font-weight: bold;&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color: #d0d0d0;&quot;&gt;*()&lt;/span&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;{&lt;/span&gt;&lt;br /&gt; &lt;span style=&quot;color: #6ab825; font-weight: bold;&quot;&gt;return&lt;/span&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;yield&lt;/span&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;sendGreeting(&lt;/span&gt;&lt;span style=&quot;color: #6ab825; font-weight: bold;&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color: #d0d0d0;&quot;&gt;.message.replace(&lt;/span&gt;&lt;span style=&quot;color: #ed9d13;&quot;&gt;/NAME/&lt;/span&gt;&lt;span style=&quot;color: #d0d0d0;&quot;&gt;,&lt;/span&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;name));&lt;/span&gt;&lt;br /&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;}).bind(&lt;/span&gt;&lt;span style=&quot;color: #6ab825; font-weight: bold;&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color: #d0d0d0;&quot;&gt;);&lt;/span&gt;&lt;br /&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;})&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #d0d0d0;&quot;&gt;};&lt;/span&gt;&lt;br /&gt;&lt;/pre&gt;&lt;/div&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &amp;quot;Courier New&amp;quot;,Courier,monospace;&quot;&gt;Task.spawn()&lt;/span&gt; makes the operation logic simple, but the wrapper function and &lt;span style=&quot;font-family: &amp;quot;Courier New&amp;quot;,Courier,monospace;&quot;&gt;bind()&lt;/span&gt; call required to start the task on method invocation and bind its &lt;span style=&quot;font-family: &amp;quot;Courier New&amp;quot;,Courier,monospace;&quot;&gt;this&lt;/span&gt; reference make the overall implementation complex.&lt;br /&gt;&lt;br /&gt;Enter &lt;span style=&quot;font-family: &amp;quot;Courier New&amp;quot;,Courier,monospace;&quot;&gt;Task.async()&lt;/span&gt;.&lt;br /&gt;&lt;br /&gt;Like &lt;span style=&quot;font-family: &amp;quot;Courier New&amp;quot;,Courier,monospace;&quot;&gt;Task.spawn()&lt;/span&gt;, it creates a task, but it doesn&#39;t immediately start it. Instead, it returns an &quot;async function&quot; whose invocation starts the task, and the async function binds the task to its own &lt;span style=&quot;font-family: &amp;quot;Courier New&amp;quot;,Courier,monospace;&quot;&gt;this&lt;/span&gt; reference at invocation time. That makes it simpler to declare the method:&lt;br /&gt;&lt;br /&gt;&lt;!-- HTML generated using hilite.me --&gt; &lt;div style=&quot;background: #202020; border-width: .1em .1em .1em .8em; border: solid gray; overflow: auto; padding: .2em .6em; width: auto;&quot;&gt;&lt;pre style=&quot;line-height: 125%; margin: 0;&quot;&gt;&lt;span style=&quot;color: #6ab825; font-weight: bold;&quot;&gt;let&lt;/span&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;greeter&lt;/span&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;{&lt;/span&gt;&lt;br /&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;message:&lt;/span&gt; &lt;span style=&quot;color: #ed9d13;&quot;&gt;&quot;Hello, NAME!&quot;&lt;/span&gt;&lt;span style=&quot;color: #d0d0d0;&quot;&gt;,&lt;/span&gt;&lt;br /&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;greet:&lt;/span&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;Task.async(&lt;/span&gt;&lt;span style=&quot;color: #6ab825; font-weight: bold;&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color: #d0d0d0;&quot;&gt;*(name)&lt;/span&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;{&lt;/span&gt;&lt;br /&gt; &lt;span style=&quot;color: #6ab825; font-weight: bold;&quot;&gt;return&lt;/span&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;yield&lt;/span&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;sendGreeting(&lt;/span&gt;&lt;span style=&quot;color: #6ab825; font-weight: bold;&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color: #d0d0d0;&quot;&gt;.message.replace(&lt;/span&gt;&lt;span style=&quot;color: #ed9d13;&quot;&gt;/NAME/&lt;/span&gt;&lt;span style=&quot;color: #d0d0d0;&quot;&gt;,&lt;/span&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;name));&lt;/span&gt;&lt;br /&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;})&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #d0d0d0;&quot;&gt;};&lt;/span&gt;&lt;br /&gt;&lt;/pre&gt;&lt;/div&gt;&lt;br /&gt;With identical semantics:&lt;br /&gt;&lt;br /&gt;&lt;div style=&quot;background: #202020; border-width: .1em .1em .1em .8em; border: solid gray; overflow: auto; padding: .2em .6em; width: auto;&quot;&gt;&lt;pre style=&quot;line-height: 125%; margin: 0;&quot;&gt;&lt;span style=&quot;color: #d0d0d0;&quot;&gt;greeter.greet(&lt;/span&gt;&lt;span style=&quot;color: #ed9d13;&quot;&gt;&quot;Mitchell&quot;&lt;/span&gt;&lt;span style=&quot;color: #d0d0d0;&quot;&gt;).then((reply)&lt;/span&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;{&lt;/span&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;...&lt;/span&gt; &lt;span style=&quot;color: #d0d0d0;&quot;&gt;});&lt;/span&gt; &lt;span style=&quot;color: #999999; font-style: italic;&quot;&gt;// behaves the same&lt;/span&gt;&lt;br /&gt;&lt;/pre&gt;&lt;/div&gt;&lt;br /&gt;(And it avoids a couple anti-patterns in the process.)&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &amp;quot;Courier New&amp;quot;,Courier,monospace;&quot;&gt;Task.async()&lt;/span&gt; is inspired by ECMAScript&#39;s &lt;a href=&quot;http://wiki.ecmascript.org/doku.php?id=strawman:async_functions&quot;&gt;Async Functions strawman proposal&lt;/a&gt; and &lt;a href=&quot;http://msdn.microsoft.com/en-us/library/hh191443.aspx&quot;&gt;C#&#39;s Async modifier&lt;/a&gt; and was implemented in &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=966182&quot;&gt;bug 966182&lt;/a&gt;. It isn&#39;t limited to use in method declarations, although it&#39;s particularly helpful for them.&lt;br /&gt;&lt;br /&gt;Use it to implement your next asynchronous operation!&lt;br /&gt;&lt;br /&gt;</content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/2146007198307190404/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=2146007198307190404' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/2146007198307190404'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/2146007198307190404'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2014/03/simplify-asynchronous-method.html' title='simplify asynchronous method declarations with Task.async()'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/08518329693863067865</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-7603765594353319606</id><published>2014-03-27T13:14:00.000-07:00</published><updated>2014-03-27T13:14:20.326-07:00</updated><title type='text'>qualifications for leadership</title><content type='html'>I&#39;ve been surprised by the negative reaction to Brendan&#39;s promotion by some of my fellow supporters of marriage equality. Perhaps I take it too much for granted that Mozillians recognize the diversity of their community in every possible respect, including politically and religiously, and that the &lt;span style=&quot;font-style: italic;&quot;&gt;only&lt;/span&gt; thing we share in common is our commitment to Mozilla&#39;s mission and the principles for participation.&lt;br /&gt;&lt;br /&gt;Those principles are reflected in our &lt;a href=&quot;http://www.mozilla.org/en-US/about/governance/policies/participation/&quot;&gt;Community Participation Agreement&lt;/a&gt;, to which Brendan has always shown fealty (since long before it was formalized, in my 15-year experience with him), and which could not possibly be clearer about the welcoming nature of Mozilla to all constructive contributors.&lt;br /&gt; &lt;br /&gt;I know that marriage equality has been a long, difficult, and painful battle, the kind that rubs nerves raw and makes it challenging to show any charity to its opponents. But they aren&#39;t all bigots, and I take Brendan at his &lt;a href=&quot;https://brendaneich.com/2014/03/inclusiveness-at-mozilla/&quot;&gt;word and deed&lt;/a&gt; that he&#39;s as committed as I am to the community&#39;s inclusive ideals (and the organization&#39;s employment policies).&lt;br /&gt; &lt;br /&gt;As Andrew Sullivan eloquently states in his recent blog post on &lt;a href=&quot;http://dish.andrewsullivan.com/2014/03/24/religious-belief-and-bigotry/&quot;&gt;Religious Belief and Bigotry&lt;/a&gt;:&lt;br /&gt;&lt;br /&gt;&lt;div style=&quot;margin-left: 40px;&quot;&gt;&quot;Twenty years ago, I was confidently told by my leftist gay friends that Americans were all anti-gay bigots and would never, ever back marriage rights so I should stop trying to reason them out of their opposition. My friends were wrong. Americans are not all bigots. Not even close. They can be persuaded rather than attacked. And if we behave magnanimously and give maximal space for those who sincerely oppose us, then eventual persuasion will be more likely. And our victory more moral and more enduring.&quot; &lt;/div&gt;&lt;br /&gt;I&#39;m chastened to admit that I substantially shared his friends&#39; opinion twenty years ago. But I&#39;m happy to realize I was wrong. And perhaps Brendan will one day do the same. Either way, he qualifies to be a leader at any level in the Mozilla community (and organization), as do the many other Mozilla leaders whose beliefs undoubtedly differ sharply from my own.&lt;br /&gt;&lt;br /&gt;</content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/7603765594353319606/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=7603765594353319606' title='21 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/7603765594353319606'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/7603765594353319606'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2014/03/qualifications-for-leadership.html' title='qualifications for leadership'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/08518329693863067865</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>21</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-4907835732389141704</id><published>2013-10-15T17:05:00.003-07:00</published><updated>2013-10-15T17:05:53.226-07:00</updated><title type='text'>from Webapp SDK to r2d2b2g, Firefox OS Simulator, and the App Manager</title><content type='html'>A little over a year ago, on August 31, 2012, I brainstormed the outline of a &quot;Webapp SDK&quot;:&lt;br /&gt;&lt;br /&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;http://2.bp.blogspot.com/-qFEN48-NFBI/Ul2-QToFUkI/AAAAAAAAAEY/Pah7FtanWfo/s1600/2012-08-31+10.57.00.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;300&quot; src=&quot;http://2.bp.blogspot.com/-qFEN48-NFBI/Ul2-QToFUkI/AAAAAAAAAEY/Pah7FtanWfo/s400/2012-08-31+10.57.00.jpg&quot; width=&quot;400&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;That outline was the genesis for the &lt;a href=&quot;https://hacks.mozilla.org/2012/10/r2d2b2g-an-experimental-prototype-firefox-os-test-environment/&quot;&gt;r2d2b2g experiment&lt;/a&gt;, which built the &lt;a href=&quot;http://www.blueskyonmars.com/2012/11/08/r2d2b2g-is-becoming-the-firefox-os-simulator/&quot;&gt;Firefox OS Simulator&lt;/a&gt;, whose initial version hit the web on September 14, 2012 and which has gone through numerous iterations since then as we evaluated various features to enhance app development.&lt;br /&gt;&lt;br /&gt;And that experiment spurred Mozilla&#39;s &lt;a href=&quot;https://wiki.mozilla.org/DevTools&quot;&gt;Developer Tools group&lt;/a&gt;, particularly its nascent &lt;a href=&quot;https://wiki.mozilla.org/DevTools/AppTools&quot;&gt;App Tools team&lt;/a&gt;, to build Firefox&#39;s new App Manager, which landed last month and was &lt;a href=&quot;https://hacks.mozilla.org/2013/10/introducing-the-firefox-os-app-manager/&quot;&gt;introduced today on Hacks&lt;/a&gt;!&lt;br /&gt;&lt;br /&gt;Despite the twisty passage from experiment to product, that initial outline bears a surprising resemblance to the App Manager feature set. The Manager checks off three of the four features on the outline&#39;s primary list—&quot;start Gaia in B2G,&quot; &quot;package app,&quot; and &quot;test app in Gaia/B2G&quot;—plus a few on its secondary list, like &quot;debug from Firefox&quot; and &quot;test on mobile device,&quot; with &quot;edit manifest in GUI&quot; well underway over in &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=912912&quot;&gt;bug 912912&lt;/a&gt;. And the Simulator continues to provide B2G/Gaia via an easy-to-install addon that integrates with the Manager.&lt;br /&gt;&lt;br /&gt;Like any good product of a successful experiment, however, the Manager&#39;s reach has exceeded its progenitor&#39;s grasp! So it also gives you access to pre-installed apps, lets you take screenshots of device/Simulator screens, and will doubtless continue to sprout handy features to make app development great.&lt;br /&gt;&lt;br /&gt;So kudos to the folks who built it, and long live the App Manager!&lt;br /&gt;&lt;br /&gt;</content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/4907835732389141704/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=4907835732389141704' title='3 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/4907835732389141704'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/4907835732389141704'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2013/10/from-webapp-sdk-to-r2d2b2g-firefox-os.html' title='from Webapp SDK to r2d2b2g, Firefox OS Simulator, and the App Manager'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/01837818348188071923</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-_YZppM5V97U/VedxzrpG9BI/AAAAAAAAAGA/wHrKKAgCoH0/s220/headshot-2014.jpg'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://2.bp.blogspot.com/-qFEN48-NFBI/Ul2-QToFUkI/AAAAAAAAAEY/Pah7FtanWfo/s72-c/2012-08-31+10.57.00.jpg" height="72" width="72"/><thr:total>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-4775473125409851813</id><published>2013-08-23T10:55:00.001-07:00</published><updated>2013-08-23T10:55:06.641-07:00</updated><title type='text'>fixing this morning&#39;s mach OS X psutil bustage</title><content type='html'>If mach is broken in your mozilla-central clone on Mac OS X this morning:&lt;br&gt; &lt;blockquote&gt;&lt;tt&gt;08-23 10:20 &amp;gt; ./mach build&lt;/tt&gt;&lt;br&gt; &lt;tt&gt;Error running mach:&lt;/tt&gt;&lt;br&gt; &lt;br&gt; &lt;tt&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; [&#39;build&#39;]&lt;/tt&gt;&lt;br&gt; &lt;br&gt; &lt;tt&gt;The error occurred in code that was called by the mach command. This is either&lt;/tt&gt;&lt;br&gt; &lt;tt&gt;a bug in the called code itself or in the way that mach is calling it.&lt;/tt&gt;&lt;br&gt; &lt;br&gt; &lt;tt&gt;You should consider filing a bug for this issue.&lt;/tt&gt;&lt;br&gt; &lt;br&gt; &lt;tt&gt;If filing a bug, please include the full output of mach, including this error&lt;/tt&gt;&lt;br&gt; &lt;tt&gt;message.&lt;/tt&gt;&lt;br&gt; &lt;br&gt; &lt;tt&gt;The details of the failure are as follows:&lt;/tt&gt;&lt;br&gt; &lt;br&gt; &lt;tt&gt;AttributeError: &#39;module&#39; object has no attribute &#39;TCPS_ESTABLISHED&#39;&lt;/tt&gt;&lt;br&gt; &lt;br&gt; &lt;tt&gt;&amp;nbsp; File &quot;/Users/myk/Mozilla/central/python/mozbuild/mozbuild/mach_commands.py&quot;, line 293, in build&lt;/tt&gt;&lt;br&gt; &lt;tt&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; from mozbuild.controller.building import BuildMonitor&lt;/tt&gt;&lt;br&gt; &lt;tt&gt;&amp;nbsp; File &quot;/Users/myk/Mozilla/central/python/mozbuild/mozbuild/controller/building.py&quot;, line 22, in &amp;lt;module&amp;gt;&lt;/tt&gt;&lt;br&gt; &lt;tt&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; import psutil&lt;/tt&gt;&lt;br&gt; &lt;tt&gt;&amp;nbsp; File &quot;/Users/myk/Mozilla/central/python/psutil/psutil/__init__.py&quot;, line 95, in &amp;lt;module&amp;gt;&lt;/tt&gt;&lt;br&gt; &lt;tt&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; import psutil._psosx as _psplatform&lt;/tt&gt;&lt;br&gt; &lt;tt&gt;&amp;nbsp; File &quot;/Users/myk/Mozilla/central/python/psutil/psutil/_psosx.py&quot;, line 48, in &amp;lt;module&amp;gt;&lt;/tt&gt;&lt;br&gt; &lt;tt&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; _TCP_STATES_TABLE = {_psutil_osx.TCPS_ESTABLISHED : CONN_ESTABLISHED,&lt;/tt&gt;&lt;br&gt; &lt;/blockquote&gt; &lt;br&gt; Then you&#39;ve been bit by the fix for &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=908296&quot;&gt;bug 908296&lt;/a&gt;. To resolve the bustage, run this command in your Hg clone:&lt;br&gt; &lt;meta http-equiv=&quot;content-type&quot; content=&quot;text/html; charset=ISO-8859-1&quot;&gt; &lt;blockquote&gt;hg status -in python/psutil | xargs rm&lt;br&gt; &lt;/blockquote&gt; &lt;br&gt; Or, if you&#39;ve cloned the &lt;a href=&quot;https://github.com/mozilla/mozilla-central&quot;&gt;Git mirror&lt;/a&gt;, run this instead:&lt;br&gt; &lt;meta http-equiv=&quot;content-type&quot; content=&quot;text/html; charset=ISO-8859-1&quot;&gt; &lt;blockquote&gt;git clean -xf python/psutil&lt;br&gt; &lt;/blockquote&gt; &lt;br&gt; </content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/4775473125409851813/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=4775473125409851813' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/4775473125409851813'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/4775473125409851813'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2013/08/fixing-this-mornings-mach-os-x-psutil.html' title='fixing this morning&#39;s mach OS X psutil bustage'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/01837818348188071923</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-_YZppM5V97U/VedxzrpG9BI/AAAAAAAAAGA/wHrKKAgCoH0/s220/headshot-2014.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-7658176042227879683</id><published>2013-06-07T16:04:00.000-07:00</published><updated>2013-06-07T16:04:04.952-07:00</updated><title type='text'>64-bit Linux ADB for Simulator</title><content type='html'>Thanks to the efforts of new Mozilla intern &lt;a href=&quot;https://github.com/bkase&quot;&gt;Brandon Kase&lt;/a&gt;, the &lt;a href=&quot;https://ftp.mozilla.org/pub/mozilla.org/labs/r2d2b2g/r2d2b2g-linux.xpi&quot;&gt;latest preview build of Firefox OS Simulator for Linux&lt;/a&gt; includes a 64-bit version of ADB, so you can push an app to an FxOS device from 64-bit Linux installations without any extra packages!&lt;br /&gt; &lt;br /&gt; Building it was tricky, because the Android SDK build scripts don&#39;t support that target. Brandon first tried simply specifying the target, which worked on an older version of ADB (1.0.24). But it failed on the latest version (1.0.31), which links with a bundled copy of libcrypto that includes 32-bit assembly.&lt;br /&gt; &lt;br /&gt; Ubuntu 13.04 (Raring) ships a 64-bit &lt;a href=&quot;http://packages.ubuntu.com/raring/android-tools-adb&quot;&gt;android-tools-adb package&lt;/a&gt;, though, so we knew it could be done. And its &lt;a href=&quot;http://packages.ubuntu.com/source/raring/android-tools&quot;&gt;source package&lt;/a&gt;&#39;s build system is much simpler than the SDK&#39;s. We just needed a binary that works on distributions with older versions of glibc than Raring&#39;s 2.17. And one that doesn&#39;t depend on a specific version of libcrypto, which varies around the Linux world; whereas Raring&#39;s ADB executable appears to need the specific version that comes with that distribution.&lt;br /&gt; &lt;br /&gt; So Brandon modified the source package&#39;s Makefile to link libcrypto statically (note the absolute path to libcrypto.a, which may vary):&lt;br /&gt;&lt;blockquote class=&quot;tr_bq&quot;&gt; &lt;tt&gt;--- debian/makefiles/adb.mk&amp;nbsp;&amp;nbsp;&amp;nbsp; 2013-03-26 14:15:41.000000000 -0700&lt;/tt&gt;&lt;br /&gt;&lt;tt&gt; &lt;/tt&gt;&lt;tt&gt;+++ adb-static-crypto.mk&amp;nbsp;&amp;nbsp;&amp;nbsp; 2013-06-06 16:51:52.794521267 -0700&lt;/tt&gt;&lt;br /&gt;&lt;tt&gt; &lt;/tt&gt;&lt;tt&gt;@@ -40,15 +40,16 @@&lt;/tt&gt;&lt;br /&gt;&lt;tt&gt; &lt;/tt&gt;&lt;tt&gt;&amp;nbsp;CPPFLAGS+= -I.&lt;/tt&gt;&lt;br /&gt;&lt;tt&gt; &lt;/tt&gt;&lt;tt&gt;&amp;nbsp;CPPFLAGS+= -I../include&lt;/tt&gt;&lt;br /&gt;&lt;tt&gt; &lt;/tt&gt;&lt;tt&gt;&amp;nbsp;CPPFLAGS+= -I../../../external/zlib&lt;/tt&gt;&lt;br /&gt;&lt;tt&gt; &lt;/tt&gt;&lt;tt&gt;+CPPFLAGS+= -I/usr/include/openssl&lt;/tt&gt;&lt;br /&gt;&lt;tt&gt; &lt;/tt&gt;&lt;tt&gt;&amp;nbsp;&lt;/tt&gt;&lt;br /&gt;&lt;tt&gt; &lt;/tt&gt;&lt;tt&gt;-LIBS+= -lc -lpthread -lz -lcrypto&lt;/tt&gt;&lt;br /&gt;&lt;tt&gt; &lt;/tt&gt;&lt;tt&gt;+LIBS+= -lc -lpthread -lz -ldl&lt;/tt&gt;&lt;br /&gt;&lt;tt&gt; &lt;/tt&gt;&lt;tt&gt;&amp;nbsp;&lt;/tt&gt;&lt;br /&gt;&lt;tt&gt; &lt;/tt&gt;&lt;tt&gt;&amp;nbsp;OBJS= $(SRCS:.c=.o)&lt;/tt&gt;&lt;br /&gt;&lt;tt&gt; &lt;/tt&gt;&lt;tt&gt;&amp;nbsp;&lt;/tt&gt;&lt;br /&gt;&lt;tt&gt; &lt;/tt&gt;&lt;tt&gt;&amp;nbsp;all: adb&lt;/tt&gt;&lt;br /&gt;&lt;tt&gt; &lt;/tt&gt;&lt;tt&gt;&amp;nbsp;&lt;/tt&gt;&lt;br /&gt;&lt;tt&gt; &lt;/tt&gt;&lt;tt&gt;&amp;nbsp;adb: $(OBJS)&lt;/tt&gt;&lt;br /&gt;&lt;tt&gt; &lt;/tt&gt;&lt;tt&gt;-&amp;nbsp;&amp;nbsp;&amp;nbsp; $(CC) -o $@ $(LDFLAGS) $(OBJS) $(LIBS)&lt;/tt&gt;&lt;br /&gt;&lt;tt&gt; &lt;/tt&gt;&lt;tt&gt;+&amp;nbsp;&amp;nbsp;&amp;nbsp; $(CC) -o $@ $(LDFLAGS) $(OBJS) /usr/lib/x86_64-linux-gnu/libcrypto.a $(LIBS)&lt;/tt&gt;&lt;br /&gt;&lt;tt&gt; &lt;/tt&gt;&lt;tt&gt;&amp;nbsp;&lt;/tt&gt;&lt;br /&gt;&lt;tt&gt; &lt;/tt&gt;&lt;tt&gt;&amp;nbsp;clean:&lt;/tt&gt;&lt;br /&gt;&lt;tt&gt; &lt;/tt&gt;&lt;tt&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; rm -rf $(OBJS) adb&lt;/tt&gt;&lt;/blockquote&gt;&lt;br /&gt; Then I copied the source package to my CentOS 16 build machine (which has glibc 2.12) and built it there. After which the resultant executable worked on all the distributions we tested: Ubuntu 13.04, Ubuntu 10.04, CentOS 16, and Arch Linux (kernel 3.9.3-1-ARCH).&lt;br /&gt; &lt;br /&gt; Presumably it will work on others too. But if it still doesn&#39;t work for you, &lt;a href=&quot;https://github.com/mozilla/r2d2b2g/issues&quot;&gt;let us know&lt;/a&gt;!&lt;br /&gt; &lt;br /&gt; And if you just want the ADB executable, sans Simulator, &lt;a href=&quot;https://ftp.mozilla.org/pub/mozilla.org/labs/r2d2b2g/adb-1.0.31-linux64.zip&quot;&gt;here it is&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;</content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/7658176042227879683/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=7658176042227879683' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/7658176042227879683'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/7658176042227879683'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2013/06/64-bit-linux-adb-for-simulator.html' title='64-bit Linux ADB for Simulator'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/01837818348188071923</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-_YZppM5V97U/VedxzrpG9BI/AAAAAAAAAGA/wHrKKAgCoH0/s220/headshot-2014.jpg'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-2950140535922521480</id><published>2012-10-02T12:08:00.001-07:00</published><updated>2012-10-02T12:13:24.657-07:00</updated><title type='text'>r2d2b2g implementation details</title><content type='html'>Over at Mozilla Hacks, I just &lt;a href=&quot;https://hacks.mozilla.org/2012/10/r2d2b2g-an-experimental-prototype-firefox-os-test-environment/&quot;&gt;blogged about r2d2b2g&lt;/a&gt; (ratta-datta-batta-ga), an experimental prototype test environment for Firefox OS that makes it drop-dead simple to test your app in &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Mozilla/Boot_to_Gecko/Using_the_B2G_desktop_client&quot;&gt;B2G Desktop&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;r2d2b2g is an addon, but it bundles &lt;a href=&quot;https://ftp.mozilla.org/pub/mozilla.org/b2g/nightly/latest-mozilla-central/&quot;&gt;B2G Desktop nightly builds&lt;/a&gt;, which are native executables, and thus the addon is platform-specific, with packages available for &lt;a href=&quot;https://ftp.mozilla.org/pub/mozilla.org/labs/r2d2b2g/r2d2b2g-mac.xpi&quot;&gt;Mac&lt;/a&gt;, &lt;a href=&quot;https://ftp.mozilla.org/pub/mozilla.org/labs/r2d2b2g/r2d2b2g-linux.xpi&quot;&gt;Linux 32-bit&lt;/a&gt;, and &lt;a href=&quot;https://ftp.mozilla.org/pub/mozilla.org/labs/r2d2b2g/r2d2b2g-windows.xpi&quot;&gt;Windows&lt;/a&gt; (caveat: B2G Desktop for Windows currently crashes on startup due to bug &lt;strike&gt;&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=794662&quot;&gt;794662&lt;/a&gt;&lt;/strike&gt; &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=795484&quot;&gt;795484&lt;/a&gt;).&lt;br /&gt;&lt;br /&gt;The packages are large, 50-60MB each, partly because of the executables, but mostly because they also bundle &lt;a href=&quot;https://wiki.mozilla.org/Gaia&quot;&gt;Gaia&lt;/a&gt; profiles, including all default apps. (It&#39;s probably worth bundling a few of these, for demonstration purposes, but we could make the packages much smaller by removing the rest.)&lt;br /&gt;&lt;br /&gt;r2d2b2g uses the &lt;a href=&quot;https://addons.mozilla.org/en-US/developers/builder&quot;&gt;Add-on SDK&lt;/a&gt; as its addon framework and relies on several third-party addon modules (&lt;a href=&quot;https://github.com/ochameau/jetpack-subprocess&quot;&gt;subprocess&lt;/a&gt;, &lt;a href=&quot;https://github.com/voldsoftware/menuitems-jplib&quot;&gt;menuitems&lt;/a&gt;) along with some Python utilities (&lt;a href=&quot;https://github.com/mozilla/mozdownload&quot;&gt;mozdownload&lt;/a&gt;, &lt;a href=&quot;https://github.com/mozilla/mozbase&quot;&gt;mozbase&lt;/a&gt;) to download and unpack B2G Desktop builds. Plus Gaia, although recent work to bundle Gaia profiles with B2G Desktop builds may break that dependency.&lt;br /&gt;&lt;br /&gt;I&#39;ve demoed the project to a variety of folks over the last couple weeks, and I&#39;ve received a bunch of positive feedback about it. B2G Desktop combines approachability with phoneliness and is the best existing test environment for Firefox OS. But its configuration is a challenge, and it provides no obvious affordances for installing and testing your own app. r2d2b2g shows that these problems are tractable (even if it doesn&#39;t yet solve them all) and demonstrates a promising product path.&lt;br /&gt;&lt;br /&gt;After seeing r2d2b2g, Kevin Dangoor drafted a &lt;a href=&quot;https://docs.google.com/document/d/1OptOCWO4b_b1aa4Gtwr82_q-mW5ybteoWNpPAJUIFNk/edit&quot;&gt;PRD for a Firefox OS Simulator&lt;/a&gt; that I&#39;ll use to guide further development. Interested in participating? Clone the code from its &lt;a href=&quot;https://github.com/mozilla/r2d2b2g&quot;&gt;GitHub repository&lt;/a&gt; and contribute your improvements!&lt;br /&gt;&lt;br /&gt;</content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/2950140535922521480/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=2950140535922521480' title='5 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/2950140535922521480'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/2950140535922521480'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2012/10/r2d2b2g-implementation-details.html' title='r2d2b2g implementation details'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/01837818348188071923</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-_YZppM5V97U/VedxzrpG9BI/AAAAAAAAAGA/wHrKKAgCoH0/s220/headshot-2014.jpg'/></author><thr:total>5</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-704490864096897779</id><published>2012-03-07T18:04:00.001-08:00</published><updated>2012-03-07T18:04:16.546-08:00</updated><title type='text'>Next/Previous Tab on Mac Consistent At Last</title><content type='html'>After blogging about the &lt;a href=&quot;http://mykzilla.blogspot.com/2011/09/nextprevious-tab-keyboard-shortcuts-on.html&quot;&gt;inconsistency of keyboard shortcuts for Next/Previous Tab on Mac&lt;/a&gt; last year, I found out that Firefox, Thunderbird, and Komodo also support Command + Option + LeftArrow|RightArrow, and Adium has a General &amp;gt; &quot;Switch tabs with&quot; pref that I can set to the same chord.&lt;br&gt; &lt;br&gt; (Later, I switched IM clients from Adium to InstantBird, which also supports that combination.)&lt;br&gt; &lt;br&gt; That left Terminal, which I couldn&#39;t figure out how to configure to support the same shortcut. Until now.&lt;br&gt; &lt;br&gt; I&#39;m not sure if it&#39;s because I have since upgraded to Mac OS X 10.7 (Lion). I could&#39;ve sworn I tried something like this back when I wrote that previous blog post, and it didn&#39;t work.&lt;br&gt; &lt;ol&gt; &lt;li&gt;Go to System Preferences &amp;gt; Keyboard &amp;gt; Keyboard Shortcuts &amp;gt; Application Shortcuts.&lt;/li&gt; &lt;li&gt;Press the + (plus) button.&lt;/li&gt; &lt;li&gt;Select &quot;Other...&quot; from the Application menu and select Utilities &amp;gt; Terminal from the file picker dialog.&lt;/li&gt; &lt;li&gt;Enter &quot;Select Next Tab&quot; (without the quotes) into the Menu Title field.&lt;/li&gt; &lt;li&gt;Focus the Keyboard Shortcut field and press Command + Option + RightArrow to set the keyboard shortcut, which will appear as &lt;meta http-equiv=&quot;content-type&quot; content=&quot;text/html; charset=ISO-8859-1&quot;&gt; &amp;#8997;&amp;#8984;&amp;#8594;.&lt;/li&gt; &lt;li&gt;Press the Add button.&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;Repeat steps 4-6 with &quot;Select Previous Tab&quot; and Command + Option + LeftArrow, which will appear as &lt;meta http-equiv=&quot;content-type&quot; content=&quot;text/html; charset=ISO-8859-1&quot;&gt; &amp;#8997;&amp;#8984;&amp;#8592;.&lt;br&gt; &lt;/p&gt; &lt;p&gt;Those shortcuts should now work in Terminal.&lt;br&gt; &lt;/p&gt; &lt;p&gt;With this change, all five of my current primary productivity applications on Mac (Firefox, Thunderbird, Instantbird, Komodo, and Terminal) support a consistent pair of keyboard shortcuts for Next/Previous Tab, which are two of the most common commands I issue in all of those apps.&lt;br&gt; &lt;/p&gt; &lt;p&gt;Woot!&lt;br&gt; &lt;br&gt; &lt;/p&gt; </content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/704490864096897779/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=704490864096897779' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/704490864096897779'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/704490864096897779'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2012/03/nextprevious-tab-on-mac-consistent-at.html' title='Next/Previous Tab on Mac Consistent At Last'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/01837818348188071923</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-_YZppM5V97U/VedxzrpG9BI/AAAAAAAAAGA/wHrKKAgCoH0/s220/headshot-2014.jpg'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-4717272844287397408</id><published>2012-03-07T13:46:00.001-08:00</published><updated>2012-03-07T13:46:45.454-08:00</updated><title type='text'>generating a fingerprint for an SSH key</title><content type='html'>After recently &lt;a href=&quot;https://github.com/blog/1068-public-key-security-vulnerability-and-mitigation&quot;&gt;discovering a security vulnerability&lt;/a&gt; that allows an attacker to add an SSH key to a GitHub user account, GitHub is requiring all users to audit their SSH keys. Its &lt;a href=&quot;https://github.com/settings/ssh/audit&quot;&gt;audit page&lt;/a&gt; lists one&#39;s keys by type and fingerprint, but it doesn&#39;t say how it generated the fingerprint or how to generate one for your local copy of a key to compare it with. Nor does it let you see the whole key.&lt;br&gt; &lt;br&gt; And since I don&#39;t generate such fingerprints very often, I didn&#39;t know how to do it. So I tried &lt;tt&gt;cksum&lt;/tt&gt;, &lt;tt&gt;md5&lt;/tt&gt;, and &lt;tt&gt;shasum&lt;/tt&gt; on my Mac, but none of their checksums matched. Turns out the tool to use is &lt;tt&gt;ssh-keygen&lt;/tt&gt;:&lt;br&gt; &lt;br&gt; &lt;tt&gt; &amp;nbsp;&amp;nbsp;&amp;nbsp; ssh-keygen -l -f path/to/keyfile&lt;/tt&gt;&lt;br&gt; &lt;br&gt; </content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/4717272844287397408/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=4717272844287397408' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/4717272844287397408'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/4717272844287397408'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2012/03/generating-fingerprint-for-ssh-key.html' title='generating a fingerprint for an SSH key'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/01837818348188071923</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-_YZppM5V97U/VedxzrpG9BI/AAAAAAAAAGA/wHrKKAgCoH0/s220/headshot-2014.jpg'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-3538011216013400890</id><published>2011-10-17T10:38:00.001-07:00</published><updated>2011-10-17T10:38:52.345-07:00</updated><title type='text'>Mozilla Status Board Text is Markdown</title><content type='html'>It isn&#39;t documented anywhere that I can find, but Benjamin Smedberg&#39;s handy &lt;a href=&quot;http://benjamin.smedbergs.us/weekly-updates.fcgi/&quot;&gt;Mozilla Status Board&lt;/a&gt; tool parses status text as &lt;a href=&quot;http://daringfireball.net/projects/markdown/&quot;&gt;Markdown&lt;/a&gt;, which is how I added a &lt;b&gt;Didn&#39;t&lt;/b&gt; header to the &lt;b&gt;Done&lt;/b&gt; section of my &lt;a href=&quot;http://benjamin.smedbergs.us/weekly-updates.fcgi/user/mykmelez&quot;&gt;status update&lt;/a&gt; with all the things I planned to do last week but didn&#39;t make happen. (The &lt;b&gt;Done&lt;/b&gt;, &lt;b&gt;Next&lt;/b&gt;, and &lt;b&gt;Coordination&lt;/b&gt; headers are all &lt;b&gt;H4&lt;/b&gt;s, so I prepended four hash marks to &lt;tt&gt;#### &lt;b&gt;Didn&#39;t&lt;/b&gt;&lt;/tt&gt; to make it the same size).&lt;br&gt; &lt;br&gt; (Note that &lt;tt&gt;[&lt;a href=&quot;http://daringfireball.net/projects/markdown/basics&quot;&gt;Markdown-style links&lt;/a&gt;](&lt;a class=&quot;moz-txt-link-freetext&quot; href=&quot;http://daringfireball.net/projects/markdown/basics&quot;&gt;http://daringfireball.net/projects/markdown/basics&lt;/a&gt;)&lt;/tt&gt; don&#39;t work and cause the entire section in which they appear to remain unparsed. However angle-bracketed URLs, as recommended by &lt;tt&gt;&lt;a href=&quot;http://labs.apache.org/webarch/uri/rfc/rfc3986.html#delimiting&quot;&gt;RFC 3986&lt;/a&gt; &lt;a class=&quot;moz-txt-link-rfc2396E&quot; href=&quot;http://labs.apache.org/webarch/uri/rfc/rfc3986.html#delimiting&quot;&gt;&amp;lt;http://labs.apache.org/webarch/uri/rfc/rfc3986.html#delimiting&amp;gt;&lt;/a&gt;&lt;/tt&gt;, work when added to the ends of lines. And &quot;&lt;tt&gt;bug ###&lt;/tt&gt;&quot; references are auto-linkified.)&lt;br&gt; &lt;br&gt; </content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/3538011216013400890/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=3538011216013400890' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/3538011216013400890'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/3538011216013400890'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2011/10/mozilla-status-board-text-is-markdown.html' title='Mozilla Status Board Text is Markdown'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/01837818348188071923</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-_YZppM5V97U/VedxzrpG9BI/AAAAAAAAAGA/wHrKKAgCoH0/s220/headshot-2014.jpg'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-3002229182794927051</id><published>2011-09-16T08:32:00.001-07:00</published><updated>2011-09-16T08:32:13.528-07:00</updated><title type='text'>to all the bugs I&#39;ve filed before</title><content type='html'>The &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=20142&quot;&gt;first bug I filed&lt;/a&gt; was marked as &lt;i&gt;duplicate&lt;/i&gt;; the &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=20187&quot;&gt;second&lt;/a&gt; was &lt;i&gt;worksforme&lt;/i&gt; (although Chris Petersen could reproduce it before he couldn&#39;t anymore); and the &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=24840&quot;&gt;third&lt;/a&gt; was &lt;i&gt;invalid&lt;/i&gt; (it was the spec, not the code, that was errant). The &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=25082&quot;&gt;fourth&lt;/a&gt; is the first that was &lt;i&gt;fixed&lt;/i&gt;.&lt;br&gt; &lt;br&gt; </content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/3002229182794927051/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=3002229182794927051' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/3002229182794927051'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/3002229182794927051'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2011/09/to-all-bugs-ive-filed-before.html' title='to all the bugs I&#39;ve filed before'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/01837818348188071923</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-_YZppM5V97U/VedxzrpG9BI/AAAAAAAAAGA/wHrKKAgCoH0/s220/headshot-2014.jpg'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-2481752447493541518</id><published>2011-09-09T17:01:00.000-07:00</published><updated>2011-09-09T17:00:36.580-07:00</updated><title type='text'>&quot;Next/Previous Tab&quot; Keyboard Shortcuts on Windows</title><content type='html'>On my Windows laptop, I use the following four programs with tabbed interfaces on a regular basis:&lt;br&gt; &lt;ul&gt; &lt;li&gt;Firefox&lt;/li&gt; &lt;li&gt;Thunderbird&lt;/li&gt; &lt;li&gt;Instantbird&lt;/li&gt; &lt;li&gt;Komodo IDE&lt;/li&gt; &lt;/ul&gt; (I&#39;d love to have tabs in my Windows terminal app of choice, &lt;a href=&quot;http://code.google.com/p/mintty/&quot;&gt;Mintty&lt;/a&gt;, but its developer &lt;a href=&quot;http://code.google.com/p/mintty/issues/detail?id=8&quot;&gt;thinks tabs should be implemented at the window manager level&lt;/a&gt;.)&lt;br&gt; &lt;br&gt; Unlike &lt;a href=&quot;http://mykzilla.blogspot.com/2011/09/nextprevious-tab-keyboard-shortcuts-on.html&quot;&gt;on my Mac&lt;/a&gt;, all those programs implement the same keyboard shortcut for switching to the previous/next tab, and it&#39;s a simple one with just a two-key chord: Control + PageUp / PageDown.&lt;br&gt; &lt;br&gt; Ha!&lt;br&gt; &lt;br&gt; </content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/2481752447493541518/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=2481752447493541518' title='8 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/2481752447493541518'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/2481752447493541518'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2011/09/nextprevious-tab-keyboard-shortcuts-on_09.html' title='&quot;Next/Previous Tab&quot; Keyboard Shortcuts on Windows'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/01837818348188071923</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-_YZppM5V97U/VedxzrpG9BI/AAAAAAAAAGA/wHrKKAgCoH0/s220/headshot-2014.jpg'/></author><thr:total>8</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-1639696569383368856</id><published>2011-09-08T17:03:00.001-07:00</published><updated>2011-09-08T17:03:31.723-07:00</updated><title type='text'>&quot;Next/Previous Tab&quot; Keyboard Shortcuts on Mac</title><content type='html'>On my Mac, I use the following five programs with tabbed interfaces on a regular basis:&lt;br&gt; &lt;ul&gt; &lt;li&gt;Firefox&lt;/li&gt; &lt;li&gt;Thunderbird&lt;/li&gt; &lt;li&gt;Adium&lt;/li&gt; &lt;li&gt;Terminal&lt;/li&gt; &lt;li&gt;Komodo IDE&lt;/li&gt; &lt;/ul&gt; &lt;br&gt; And those programs implement the following five different keyboard shortcuts for switching to the previous/next tab:&lt;br&gt; &lt;ul&gt; &lt;li&gt;Control + PageUp / PageDown (Firefox, Thunderbird)&lt;br&gt; &lt;/li&gt; &lt;li&gt;Command + LeftArrow / RightArrow (Adium)&lt;/li&gt; &lt;li&gt;Command + PageUp / PageDown (Komodo IDE)&lt;/li&gt; &lt;li&gt;Command + Shift + [ / ] (Terminal)&lt;/li&gt; &lt;li&gt;Command + Shift + LeftArrow / RightArrow (Terminal)&lt;/li&gt; &lt;/ul&gt; Hrm.&lt;br&gt; &lt;br&gt; </content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/1639696569383368856/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=1639696569383368856' title='6 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/1639696569383368856'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/1639696569383368856'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2011/09/nextprevious-tab-keyboard-shortcuts-on.html' title='&quot;Next/Previous Tab&quot; Keyboard Shortcuts on Mac'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/01837818348188071923</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-_YZppM5V97U/VedxzrpG9BI/AAAAAAAAAGA/wHrKKAgCoH0/s220/headshot-2014.jpg'/></author><thr:total>6</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-4435398397928021957</id><published>2011-09-07T14:25:00.000-07:00</published><updated>2011-09-20T11:37:47.397-07:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="jetpack"/><title type='text'>gitflow vs. the SDK</title><content type='html'>&lt;a href=&quot;http://nvie.com/posts/a-successful-git-branching-model/&quot;&gt;gitflow&lt;/a&gt; is a model for developing and shipping software using &lt;a href=&quot;http://git-scm.com/&quot;&gt;Git&lt;/a&gt;. &lt;a href=&quot;https://addons.mozilla.org/en-US/developers/builder&quot;&gt;Add-on SDK&lt;/a&gt; uses Git, and &lt;a href=&quot;https://wiki.mozilla.org/Jetpack/Development_Process&quot;&gt;it too has a model&lt;/a&gt;, which is similar to gitflow in some ways and different in others. Here&#39;s a comparison of the two and some thoughts on why they vary.&lt;br /&gt;&lt;br /&gt;First, some similarities: both models use multiple branches, including an ongoing branch for general development and another ongoing branch that is always ready for release (their names vary, but that&#39;s a trivial difference). Both also permit development on temporary feature (topic) branches and utilize a branch for stabilization of the codebase leading up to a release. And both accommodate the occasional hotfix release in similar ways.&lt;br /&gt;&lt;br /&gt;(Aside: gitflow appears to encourage feature branches, but I tend to agree with &lt;a href=&quot;http://martinfowler.com/bliki/FeatureBranch.html&quot;&gt;Martin Fowler&lt;/a&gt; through &lt;a href=&quot;http://pauljulius.com/blog/2009/09/03/feature-branches-are-poor-mans-modular-architecture/&quot;&gt;Paul Julius&lt;/a&gt; that continuously integrating with a central development branch is preferable.)&lt;br /&gt;&lt;br /&gt;Second, some differences: the SDK uses a single ongoing stabilization branch, while gitflow uses multiple short-lived stabilization branches, one per release. And in the SDK, stabilization fixes land on the development branch and then get cherry-picked to the stabilization branch; whereas in gitflow, stabilization fixes land on the stabilization branch and then get merged to the development branch.&lt;br /&gt;&lt;br /&gt;(Also, the SDK releases on a regular time/quality-driven &quot;train&quot; schedule similar to &lt;a href=&quot;http://mozilla.github.com/process-releases/draft/development_overview/&quot;&gt;Firefox&#39;s&lt;/a&gt;, while gitflow may anticipate an irregular feature/quality-driven release schedule, although it can be applied to projects with train schedules, like &lt;a href=&quot;http://lloyd.io/applying-gitflow&quot;&gt;BrowserID&lt;/a&gt;.)&lt;br /&gt;&lt;br /&gt;A benefit of gitflow&#39;s approach to stabilization is that its change graph includes only distinct changes, whereas cherry-picking adds duplicate, semi-associated changes to the SDK&#39;s graph. However, a downside of gitflow&#39;s approach is that developers must attend to where they land changes, whereas SDK developers always land changes on its development branch, and its release manager takes on the chore of getting those changes onto the stabilization branch.&lt;br /&gt;&lt;br /&gt;(It isn&#39;t clear what happens in gitflow if a change lands on the development branch while a release is being stabilized and afterward is identified as being wanted for the release. Perhaps it gets cherry-picked?)&lt;br /&gt;&lt;br /&gt;Overall, these models seem fairly similar, and it wouldn&#39;t be too hard to make the SDK&#39;s be essentially gitflow. We would just need to stipulate that developers land stabilization fixes on the stabilization branch, and the release manager&#39;s job would then be to merge that branch back to the development branch periodically instead of cherry-picking in the other direction.&lt;br /&gt;&lt;br /&gt;However, it isn&#39;t clear to me that such a change would be preferable. What do you think?</content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/4435398397928021957/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=4435398397928021957' title='5 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/4435398397928021957'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/4435398397928021957'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2011/09/gitflow-vs-sdk.html' title='gitflow vs. the SDK'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/01837818348188071923</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-_YZppM5V97U/VedxzrpG9BI/AAAAAAAAAGA/wHrKKAgCoH0/s220/headshot-2014.jpg'/></author><thr:total>5</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-8020854027593557159</id><published>2011-08-21T22:51:00.000-07:00</published><updated>2011-09-20T11:38:43.594-07:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="jetpack"/><title type='text'>Administer Git? Get a job!</title><content type='html'>As I &lt;a href=&quot;http://mykzilla.blogspot.com/2011/08/why-add-on-sdk-doesnt-land-in-mozilla.html&quot;&gt;mentioned recently&lt;/a&gt;, &lt;a href=&quot;http://git-scm.com/&quot;&gt;Git&lt;/a&gt; (on &lt;a href=&quot;https://github.com/&quot;&gt;GitHub&lt;/a&gt;) has become a popular VCS for Mozilla-related projects.&lt;br&gt; &lt;br&gt; GitHub is a fantastic tool for collaboration, and the site does a great job running a Git server, but given the importance of the VCS, and because Mozilla&#39;s automated test machines don&#39;t have access to servers outside the Mozilla firewall, Mozilla should &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=528360&quot;&gt;run its own Git server&lt;/a&gt; (that syncs with GitHub, so developers can continue to use that site for collaboration).&lt;br&gt; &lt;br&gt; Unfortunately, the organization doesn&#39;t have a great deal of in-house Git server administration experience, but we&#39;re &lt;a href=&quot;http://hire.jobvite.com/CompanyJobs/Careers.aspx?c=qpX9Vfwa&amp;amp;cs=9Kt9Vfw1&amp;amp;page=Job%20Description&amp;amp;j=oIfPVfwr&quot;&gt;hiring systems administrators&lt;/a&gt;, so if you grok Git hosting and meet the other requirements, &lt;a href=&quot;http://hire.jobvite.com/CompanyJobs/Careers.aspx?c=qpX9Vfwa&amp;amp;page=Apply&amp;amp;j=oIfPVfwr&quot;&gt;send in your resume&lt;/a&gt;!&lt;br&gt; &lt;br&gt; </content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/8020854027593557159/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=8020854027593557159' title='5 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/8020854027593557159'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/8020854027593557159'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2011/08/administer-git-get-job.html' title='Administer Git? Get a job!'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/01837818348188071923</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-_YZppM5V97U/VedxzrpG9BI/AAAAAAAAAGA/wHrKKAgCoH0/s220/headshot-2014.jpg'/></author><thr:total>5</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-1307851753993957811</id><published>2011-08-11T13:33:00.000-07:00</published><updated>2011-09-20T11:38:43.545-07:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="jetpack"/><title type='text'>Why the Add-on SDK Doesn&#39;t &quot;Land in mozilla-central&quot;</title><content type='html'>Various Mozillians sometimes suggest that the Add-on SDK should &quot;land in mozilla-central&quot; and wonder why it doesn&#39;t. Here&#39;s why.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;The Add-on SDK depends on features of Firefox (and Gecko), and the SDK&#39;s development process synchronizes its release schedule with Firefox&#39;s. Nevertheless, the SDK isn&#39;t a component of Firefox, it&#39;s a distinct product with its own codebase, development process, and release schedule.&lt;br /&gt;&lt;br /&gt;Mozilla makes multiple products that interact with Firefox (addons.mozilla.org, a.k.a. AMO, is another), and distinct product development efforts should generally utilize separate code repositories, to avoid contention between the projects regarding tree management, the stages of the software development lifecycle (i.e. when which branch is in alpha, beta, etc.), and the schedules for merging between branches.&lt;br /&gt;&lt;br /&gt;There can be exceptions to that principle, for products that share a bunch of code, use the same development process, and have the same release schedule (cf. the Firefoxes for desktop and mobile). But the SDK is not one of those exceptions.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;It shares no code with Firefox. Its process utilizes one fewer branch and six fewer weeks of development than the Firefox development process, to minimize the burden of branch management and stabilization build testing on its much smaller development team and testing community. And it merges its branches and ships its releases two weeks before Firefox, to give AMO and addon developers time to update addons for each new version of the browser.&lt;br /&gt;&lt;br /&gt;Living in its own repository makes it possible for the SDK to have these differences in its process, and it also makes it possible for us to change the process in the future, for example to move up the branch/release dates one week, if we discover that AMO and addon developers would benefit from three weeks of lead time; or to ship twice as frequently, if we determine that doing so would get APIs for new Firefox features into developers&#39; hands faster.&lt;br /&gt;&lt;br /&gt;Finally, the Jetpack project has a vibrant community of contributors (including both organization staff and volunteers) who strongly prefer contributing via Git and &lt;a href=&quot;https://github.com/&quot;&gt;GitHub&lt;/a&gt;, because they find it easier, more efficient, and more enjoyable, and for whom working in mozilla-central would mean taking too great a hit on their productivity, passion, and participation.&lt;br /&gt;&lt;br /&gt;Mozilla Labs innovates not only on features and user experience but also on development process and tools, and while Jetpack didn&#39;t lead the way to GitHub, we were a fast follower once early experiments validated its benefits. And our experience since then has only confirmed our decision, as GitHub has proven to be a fantastic tool for branch management, code review/integration, and other software development tasks.&lt;br /&gt;&lt;br /&gt;Other Mozillians agree: there are now almost two hundred members and over one hundred repositories (not counting forks) in the Mozilla organization on GitHub, with major initiatives like &lt;a href=&quot;https://github.com/mozilla/openwebapps&quot;&gt;Open Web Apps&lt;/a&gt; and &lt;a href=&quot;https://github.com/mozilla/browserid&quot;&gt;BrowserID&lt;/a&gt; being hosted there, not to mention all the Mozilla projects in user repositories, including &lt;a href=&quot;https://github.com/graydon/rust&quot;&gt;Rust&lt;/a&gt; and &lt;a href=&quot;https://github.com/jbalogh/zamboni&quot;&gt;Zamboni&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;Even if we don&#39;t make mozilla-central the canonical repository for SDK development, however, we could still periodically drop a copy of the SDK source against which Firefox changes should be tested into mozilla-central. And doing so would theoretically make it easier for Firefox developers to run SDK tests when they discover that a Firefox change breaks the SDK, because they wouldn&#39;t have to get the SDK first.&lt;br /&gt;&lt;br /&gt;But the benefit to Firefox developers is minimal. Currently, we periodically drop a reference to the SDK revision against which Firefox changes should be tested, and developers have to do the following to initiate testing:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;&amp;nbsp; wget -i testing/jetpack/jetpack-location.txt -O addon-sdk.tar.bz2
+ &lt;br /&gt;&amp;nbsp; tar xjf addon-sdk.tar.bz2
+ &lt;br /&gt;&amp;nbsp; cd addon-sdk-[revision]
+ &lt;br /&gt;&amp;nbsp; source bin/activate
+ &lt;br /&gt;&amp;nbsp; cfx testall --binary path/to/Firefox/build
+ &lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;We can simplify this to:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;&amp;nbsp; testing/jetpack/clone
+ &lt;br /&gt;&amp;nbsp; cd addon-sdk
+ &lt;br /&gt;&amp;nbsp; source bin/activate
+ &lt;br /&gt;&amp;nbsp; cfx testall --binary path/to/Firefox/build
+ &lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;Whereas if we dropped the source instead of just a reference to it, it would instead be the only slightly simpler: &lt;br /&gt;&lt;br /&gt;&lt;pre&gt;&amp;nbsp; cd testing/jetpack/addon-sdk
+ &lt;br /&gt;&amp;nbsp; source bin/activate
+ &lt;br /&gt;&amp;nbsp; cfx testall --binary path/to/Firefox/build
+ &lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;Either of which can be abstracted to a single make target.&lt;br /&gt;&lt;br /&gt;But if we were to drop source instead of a reference thereto, the drops would be larger and riskier changes. And test automation would still need to be updated to support Git (or at least continue to use brittle Git -&amp;gt; Mercurial mirroring), in order to run tests on SDK changes, which periodic source drops do not address.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;Now, this doesn&#39;t mean that no SDK code will ever land in mozilla-central.&lt;br /&gt;&lt;br /&gt;Various folks have discussed integrating parts of the SDK into core Firefox&lt;span class=&quot;st&quot;&gt;—&lt;/span&gt;including stable API implementations, the module loader, and possibly the bootstrapper&lt;span class=&quot;st&quot;&gt;—&lt;/span&gt;to reduce the size of addon packages, improve addon startup times, and decrease addon memory consumption. I have written a very preliminary draft of a &lt;a href=&quot;https://wiki.mozilla.org/Features/Jetpack/Land_Parts_of_Add-on_SDK_In_Core&quot;&gt;feature page describing this work&lt;/a&gt;, although I do not think it is a high priority at the moment, relative to the other priorities identified in the &lt;a href=&quot;https://wiki.mozilla.org/Jetpack/Roadmap&quot;&gt;Jetpack roadmap&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;And Dietrich Ayala recently suggested &lt;a href=&quot;http://groups.google.com/group/mozilla.dev.planning/browse_frm/thread/2b57ebe15aad4130&quot;&gt;integrating the SDK into core Firefox for use by core features&lt;/a&gt;, by which he presumably also means the API implementations/module loader/bootstrapper rather than the command-line tool for testing and packaging addons.&lt;br /&gt;&lt;br /&gt;Nevertheless, I am (and, I suspect, the whole Jetpack team is) even open to discussing integration of the command-line tool (or its replacement by a graphical equivalent), merging together the two products, and erasing the distinction between them, just as Firefox ships with core features for web development.&amp;nbsp; We&#39;ve even drafted a &lt;a href=&quot;https://wiki.mozilla.org/Features/Jetpack/Add-on_SDK_as_an_Addon&quot;&gt;feature page for converting the SDK into an addon&lt;/a&gt;, which is a big step in that direction.&lt;br /&gt;&lt;br /&gt;But until that happens, farther on up the road, the SDK is its own product that we develop with its own process and ship on its own schedule. And it has good reason to live in its own repository, and a Git one at that, as do the many (and growing number of) other Mozilla projects using similar processes and tools, which our community-wide development, collaboration, and testing infrastructure must evolve to accommodate.</content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/1307851753993957811/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=1307851753993957811' title='6 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/1307851753993957811'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/1307851753993957811'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2011/08/why-add-on-sdk-doesnt-land-in-mozilla.html' title='Why the Add-on SDK Doesn&#39;t &quot;Land in mozilla-central&quot;'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/01837818348188071923</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-_YZppM5V97U/VedxzrpG9BI/AAAAAAAAAGA/wHrKKAgCoH0/s220/headshot-2014.jpg'/></author><thr:total>6</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-6715608859512848344</id><published>2010-12-02T11:07:00.001-08:00</published><updated>2011-09-20T11:38:43.568-07:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="jetpack"/><title type='text'>SDK Training and More at Add-on-Con</title><content type='html'>&lt;div class=&quot;moz-text-html&quot; lang=&quot;x-western&quot;&gt;Next Wednesday, December 8, I&#39;ll be at &lt;a href=&quot;http://addoncon.com/&quot;&gt;Add-on-Con&lt;/a&gt;.&lt;br&gt; &lt;br&gt; In the morning, I&#39;ll conduct a training session introducing Mozilla&#39;s new Add-on SDK, which makes it faster and easier to build Firefox add-ons. Afterwards, I&#39;ll be around and about to discuss add-ons and answer questions about the SDK and add-on development generally.&lt;br&gt; &lt;br&gt; Lots of other Mozilla folks will also be on hand over the course of the two-day conference, including &lt;a href=&quot;http://www.oxymoronical.com/&quot;&gt;Dave Townsend&lt;/a&gt;, Jorge Villalobos, &lt;a href=&quot;http://jboriss.wordpress.com/&quot;&gt;Jeniffer Boriss&lt;/a&gt;, &lt;a href=&quot;http://starkravingfinkle.org/blog/&quot;&gt;Mark Finkle&lt;/a&gt;, and &lt;a href=&quot;http://blog.fligtar.com/&quot;&gt;Justin Scott&lt;/a&gt;. A rockin&#39; time should be had by all. Join us!&lt;br&gt; &lt;br&gt; &lt;/div&gt; </content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/6715608859512848344/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=6715608859512848344' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/6715608859512848344'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/6715608859512848344'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2010/12/sdk-training-and-more-at-add-on-con.html' title='SDK Training and More at Add-on-Con'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/01837818348188071923</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-_YZppM5V97U/VedxzrpG9BI/AAAAAAAAAGA/wHrKKAgCoH0/s220/headshot-2014.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-1323551422046235302</id><published>2010-11-27T20:47:00.001-08:00</published><updated>2011-09-20T11:38:43.550-07:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="jetpack"/><title type='text'>Further Adventures In Git(/Hub)ery</title><content type='html'>&lt;div class=&quot;moz-text-html&quot; lang=&quot;x-western&quot;&gt; This evening I decided to check if there were any outstanding pull requests for the SDK repository (to which I haven&#39;t been paying attention).&lt;br&gt; &lt;br&gt; There were! The oldest was &lt;a href=&quot;https://github.com/mozilla/addon-sdk/pull/29&quot;&gt;pull request 29&lt;/a&gt; from Thomas Bassetto, which contains two small fixes (&lt;a href=&quot;https://github.com/tbassetto/addon-sdk/commit/8268334070d03a896d5c006d1b4db94d4cb44b17&quot;&gt;first&lt;/a&gt;, &lt;a href=&quot;https://github.com/tbassetto/addon-sdk/commit/666ad7a99e05e338348dfc579d5b1f75e8d3bb1b&quot;&gt;second&lt;/a&gt;) to the docs.&lt;br&gt; &lt;br&gt; So I fetched the branch of his fork in which the changes reside:&lt;br&gt; &lt;br&gt; &lt;blockquote&gt;&lt;tt&gt;$ git fetch &lt;a class=&quot;moz-txt-link-freetext&quot; href=&quot;https://github.com/tbassetto/addon-sdk.git&quot;&gt;https://github.com/tbassetto/addon-sdk.git&lt;/a&gt; master&lt;/tt&gt;&lt;br&gt; &lt;/blockquote&gt; &lt;br&gt; But that branch (and the fork in general) is a few weeks out-of-date, so &quot;&lt;tt&gt;git diff HEAD FETCH_HEAD&lt;/tt&gt;&quot; showed a bunch of changes, and it was unclear how painful the merge would be.&lt;br&gt; &lt;br&gt; Thus I decided to try cherry-picking the changes, my first time using &quot;&lt;tt&gt;git cherry-pick&lt;/tt&gt;&quot;.&lt;br&gt; &lt;br&gt; The first one went great:&lt;br&gt; &lt;br&gt; &lt;blockquote&gt;&lt;tt&gt;$ git cherry-pick 8268334070d03a896d5c006d1b4db94d4cb44b17&lt;/tt&gt;&lt;br&gt; &lt;tt&gt;Finished one cherry-pick.&lt;/tt&gt;&lt;br&gt; &lt;tt&gt;[master ceadb1f] Fixed an internal link in the widget doc&lt;/tt&gt;&lt;br&gt; &lt;tt&gt;&amp;nbsp;1 files changed, 1 insertions(+), 1 deletions(-)&lt;/tt&gt;&lt;br&gt; &lt;/blockquote&gt; &lt;br&gt; Except that I realized afterward I hadn&#39;t added &quot;r,a=myk&quot; to the commit message. So I tried &quot;&lt;tt&gt;git commit --amend&lt;/tt&gt;&quot; for the first time, which worked just fine:&lt;br&gt; &lt;br&gt; &lt;blockquote&gt;&lt;tt&gt;$ git commit --amend&lt;/tt&gt;&lt;br&gt; &lt;tt&gt;[master 2d674a6] Fixed an internal link in the widget doc; r,a=myk&lt;/tt&gt;&lt;br&gt; &lt;tt&gt;&amp;nbsp;1 files changed, 1 insertions(+), 1 deletions(-)&lt;/tt&gt;&lt;br&gt; &lt;/blockquote&gt; &lt;br&gt; Next time I&#39;ll remember to use the &quot;&lt;tt&gt;--edit&lt;/tt&gt;&quot; flag to &quot;&lt;tt&gt;git cherry-pick&lt;/tt&gt;&quot;, which lets one &quot;edit the commit message prior to committing.&quot;&lt;br&gt; &lt;br&gt; The second cherry-pick was more complicated, because I only wanted one of the two changes in the commit (in &lt;a href=&quot;https://github.com/tbassetto/addon-sdk/commit/666ad7a99e05e338348dfc579d5b1f75e8d3bb1b#commitcomment-204023&quot;&gt;my review&lt;/a&gt;, I had identified the second change as unnecessary); and, as it turned out, also because there was a merge conflict with other commits.&lt;br&gt; &lt;br&gt; I started by cherry-picking the commit with the &quot;&lt;tt&gt;--no-commit&lt;/tt&gt;&quot; option (so I could remove the second change):&lt;br&gt; &lt;br&gt; &lt;blockquote&gt;&lt;tt&gt;$ git cherry-pick --no-commit 666ad7a99e05e338348dfc579d5b1f75e8d3bb1b&lt;/tt&gt;&lt;br&gt; &lt;tt&gt;Automatic cherry-pick failed.&amp;nbsp; After resolving the conflicts,&lt;/tt&gt;&lt;br&gt; &lt;tt&gt;mark the corrected paths with &#39;git add &amp;lt;paths&amp;gt;&#39; or &#39;git rm &amp;lt;paths&amp;gt;&#39; and commit the result.&lt;/tt&gt;&lt;br&gt; &lt;tt&gt;When commiting, use the option &#39;-c 666ad7a&#39; to retain authorship and message.&lt;/tt&gt;&lt;br&gt; &lt;/blockquote&gt; &lt;br&gt; The conflict was trivial, and I knew where it was, so I resolved it manually (instead of trying &quot;&lt;tt&gt;git mergetool&lt;/tt&gt;&quot; for the first time), removed the second change, added the merged file, and committed the result, using the &quot;&lt;tt&gt;-c&lt;/tt&gt;&quot; option to preserve the original author and commit message while allowing me to edit the message to add &quot;r,a=myk&quot;:&lt;br&gt; &lt;br&gt; &lt;blockquote&gt;&lt;tt&gt;$ git add packages/addon-kit/docs/request.md&lt;/tt&gt;&lt;br&gt; &lt;tt&gt;$ git commit -c 666ad7a&lt;/tt&gt;&lt;br&gt; &lt;tt&gt;[master 774d1cb] Completed the example in the Request module documentation; r,a=myk&lt;/tt&gt;&lt;br&gt; &lt;tt&gt;&amp;nbsp;1 files changed, 1 insertions(+), 0 deletions(-)&lt;/tt&gt;&lt;br&gt; &lt;/blockquote&gt; &lt;br&gt; Then I used &quot;&lt;tt&gt;gitg&lt;/tt&gt;&quot; and &quot;&lt;tt&gt;git log master ^upstream/master&lt;/tt&gt;&quot; to verify that the commits looked good to go, after which I pushed them:&lt;br&gt; &lt;br&gt; &lt;blockquote&gt;&lt;tt&gt;$ git push upstream master&lt;/tt&gt;&lt;br&gt; &lt;tt&gt;[git&#39;s standard obscure and disconcerting gobbledygook]&lt;/tt&gt;&lt;br&gt; &lt;/blockquote&gt; &lt;br&gt; Finally, I closed the pull request with &lt;a href=&quot;https://github.com/mozilla/addon-sdk/pull/29#issuecomment-570630&quot;&gt;this comment&lt;/a&gt; that summarized what I did and provided links to the cherry-picked commits.&lt;br&gt; &lt;br&gt; It would have been nice if the cherry-picked commit that didn&#39;t have merge conflicts (and which I didn&#39;t change in the process of merging) had kept its original commit ID, but I sense that that is somehow a fundamental violation of the model.&lt;br&gt; &lt;br&gt; It would also have been nice if the cherry-picked commit messages had been automatically annotated with references to the original commits.&lt;br&gt; &lt;br&gt; But overall the process seemed pretty reasonable, it was fairly easy to do what I wanted and recover from mistakes, and the author, committer, reviewer, and approver are clearly indicated in the cherry-picked commits (&lt;a href=&quot;https://github.com/mozilla/addon-sdk/commit/2d674a6ea84d3be88b5365b2d24b994297a60d7a&quot;&gt;first&lt;/a&gt;, &lt;a href=&quot;https://github.com/mozilla/addon-sdk/commit/774d1cbf49e152a030a0bf6cbde7b4139c8c3f49&quot;&gt;second&lt;/a&gt;).&lt;br&gt; &lt;br&gt; [Also &lt;a href=&quot;http://groups.google.com/group/mozilla-labs-jetpack/browse_thread/thread/430750c65fe80231&quot;&gt;posted to the discussion group&lt;/a&gt;.]&lt;br&gt; &lt;br&gt; &lt;/div&gt; </content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/1323551422046235302/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=1323551422046235302' title='3 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/1323551422046235302'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/1323551422046235302'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2010/11/further-adventures-in-githubery.html' title='Further Adventures In Git(/Hub)ery'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/01837818348188071923</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-_YZppM5V97U/VedxzrpG9BI/AAAAAAAAAGA/wHrKKAgCoH0/s220/headshot-2014.jpg'/></author><thr:total>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-7960433840999647174</id><published>2010-11-27T20:39:00.001-08:00</published><updated>2011-09-20T11:38:43.583-07:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="jetpack"/><title type='text'>More Git/Hub Workflow Experiences</title><content type='html'>After posting about my &lt;a href=&quot;http://mykzilla.blogspot.com/2010/11/github-workflow-experiences.html&quot;&gt;first Git/Hub workflow experiences&lt;/a&gt;, I got lots of helpful input from various folks, particularly Erik Vold, Irakli Gozalishvili, and Brian Warner, which led me to refine my process for handling pull requests:&lt;br /&gt;&lt;br /&gt;&lt;ol&gt;&lt;li&gt;From the &quot;how to merge this pull request&quot; section of the pull request page (f.e. &lt;a href=&quot;https://github.com/mozilla/addon-sdk/pull/43&quot;&gt;pull request 34&lt;/a&gt;), copy the command from step two, but change the word &quot;pull&quot; to &quot;fetch&quot; to fetch the remote branch containing the changes without also merging it:&lt;br /&gt;&lt;br /&gt;&lt;code&gt;git fetch &lt;a class=&quot;moz-txt-link-freetext&quot; href=&quot;https://github.com/toolness/jetpack-sdk.git&quot;&gt;https://github.com/toolness/jetpack-sdk.git&lt;/a&gt; bug-610507&lt;br /&gt;&lt;br /&gt;&lt;/code&gt;&lt;/li&gt;&lt;li&gt;Use the magic FETCH_HEAD reference to the last fetched branch to verify that the set of changes is what you expect:&lt;br /&gt;&lt;br /&gt;&lt;tt&gt;git diff &lt;/tt&gt;&lt;tt&gt;HEAD &lt;/tt&gt;&lt;tt&gt;FETCH_HEAD&lt;/tt&gt;&lt;br /&gt;&lt;br /&gt;(The exact syntax here may need some work; HEAD..FETCH_HEAD? three dots?)&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;&lt;li&gt;Merge the remote branch into your local branch with a custom commit message:&lt;br /&gt;&lt;br /&gt;&lt;tt&gt;git merge FETCH_HEAD --no-ff -m&quot;bug 610507: get rid of the nsjetpack package; r=myk&quot;&lt;/tt&gt;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;&lt;li&gt;Push the changes upstream:&lt;br /&gt;&lt;br /&gt;&lt;tt&gt;git push upstream master&lt;/tt&gt;&lt;br /&gt;&lt;/li&gt;&lt;/ol&gt;&lt;br /&gt;I like this set of commands because it doesn&#39;t require me to add a remote, I can copy/paste the fetch command from GitHub (being careful not to issue the pull before I change it to a fetch), and I always type the same FETCH_HEAD reference to the remote branch in step three.&lt;br /&gt;&lt;br /&gt;However, I wish the &lt;a href=&quot;https://github.com/mozilla/addon-sdk/commit/0e23d1c1555d5de228ed7ad62c8715e2775d2390&quot;&gt;merge commit page&lt;/a&gt; explicitly referenced the &lt;a href=&quot;https://github.com/mozilla/addon-sdk/commit/68b6e306dfeccef103b071e0812dc3a375830ac0&quot;&gt;specific&lt;/a&gt; &lt;a href=&quot;https://github.com/mozilla/addon-sdk/commit/715cb47c720bcdd11846cae6c6cab325bb1a982b&quot;&gt;commits&lt;/a&gt; that were merged. It does mention that it&#39;s a branch merge, it isn&#39;t obvious how to get from that page to the pages for the commits I merged from the branch.&lt;br /&gt;&lt;br /&gt;&quot;&lt;tt&gt;git log --oneline --graph&lt;/tt&gt;&quot;, &lt;tt&gt;gitg&lt;/tt&gt;, and &lt;tt&gt;gitk&lt;/tt&gt; do give me that information, though, so I&#39;m ok on the command line, anyway.&lt;br /&gt;&lt;br /&gt;[More discussion can be found in the &lt;a href=&quot;http://groups.google.com/group/mozilla-labs-jetpack/browse_thread/thread/2c6cb3e7f3bec468&quot;&gt;discussion group thread&lt;/a&gt;.]</content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/7960433840999647174/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=7960433840999647174' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/7960433840999647174'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/7960433840999647174'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2010/11/more-github-workflow-experiences.html' title='More Git/Hub Workflow Experiences'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/01837818348188071923</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-_YZppM5V97U/VedxzrpG9BI/AAAAAAAAAGA/wHrKKAgCoH0/s220/headshot-2014.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-7037189428773681360</id><published>2010-11-12T18:55:00.000-08:00</published><updated>2011-09-20T11:38:43.577-07:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="jetpack"/><title type='text'>Git/Hub Workflow Experiences</title><content type='html'>&lt;div class=&quot;moz-text-html&quot; lang=&quot;x-western&quot;&gt;The Jetpack project recently migrated its SDK repository to Git (hosted on GitHub), and we&#39;ve been working out changes to the bug/review/commit workflow that GitHub&#39;s tools enable (specifically, pull requests).&lt;/div&gt;&lt;div class=&quot;moz-text-html&quot; lang=&quot;x-western&quot;&gt;&amp;nbsp;&lt;/div&gt;&lt;div class=&quot;moz-text-html&quot; lang=&quot;x-western&quot;&gt;Here are some of my initial experiences and my thoughts on them (which I&#39;ve also &lt;a href=&quot;http://groups.google.com/group/mozilla-labs-jetpack/browse_thread/thread/2c6cb3e7f3bec468&quot;&gt;posted to the Jetpack discussion group&lt;/a&gt;).&lt;/div&gt;&lt;div class=&quot;moz-text-html&quot; lang=&quot;x-western&quot;&gt;&amp;nbsp;&lt;/div&gt;&lt;div class=&quot;moz-text-html&quot; lang=&quot;x-western&quot;&gt; &lt;/div&gt;&lt;div class=&quot;moz-text-html&quot; lang=&quot;x-western&quot;&gt; Warning: Git wonkery ahead, with excruciating details. I would not want to read this post. I recommend you skip it. ;-)&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-size: large;&quot;&gt;&lt;b&gt; Part 1: Wherein I Handle My First Pull Request&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;To fix some test failures, Atul submitted &lt;a href=&quot;https://github.com/mozilla/addon-sdk/pull/33&quot;&gt;GitHub pull request 33&lt;/a&gt;, I reviewed the changes (comprising &lt;a href=&quot;https://github.com/toolness/jetpack-sdk/commit/97619b0b25554712756827de883883c9b810319d&quot;&gt;two&lt;/a&gt; &lt;a href=&quot;https://github.com/toolness/jetpack-sdk/commit/405390a586f6c09bad2b26183fe2925d09bcd52b&quot;&gt;commits&lt;/a&gt;) on GitHub, and then I pushed them to the canonical repository via the following set of commands:&lt;br /&gt;&lt;ol&gt;&lt;li&gt;git checkout -b toolness-&lt;span class=&quot;commit-ref from&quot;&gt;4.0b7-bustage-fixes&lt;/span&gt; master&lt;/li&gt;&lt;li&gt;git pull &lt;a class=&quot;moz-txt-link-freetext&quot; href=&quot;https://github.com/toolness/jetpack-sdk.git&quot;&gt;https://github.com/toolness/jetpack-sdk.git&lt;/a&gt; &lt;span class=&quot;commit-ref from&quot;&gt;4.0b7-bustage-fixes&lt;/span&gt;&lt;/li&gt;&lt;li&gt;git checkout master&lt;/li&gt;&lt;li&gt;git merge toolness-&lt;span class=&quot;commit-ref from&quot;&gt;4.0b7-bustage-fixes&lt;/span&gt;&lt;/li&gt;&lt;li&gt;git push upstream master&lt;/li&gt;&lt;/ol&gt;&lt;br /&gt;That landed the &lt;a href=&quot;https://github.com/mozilla/addon-sdk/commit/97619b0b25554712756827de883883c9b810319d&quot;&gt;two&lt;/a&gt; &lt;a href=&quot;https://github.com/mozilla/addon-sdk/commit/405390a586f6c09bad2b26183fe2925d09bcd52b&quot;&gt;commits&lt;/a&gt; in the canonical repository, but it isn&#39;t obvious that they were related (i.e. part of the same pull request), that I was the one who reviewed them, or that I was the one who pushed them.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-size: large;&quot;&gt;&lt;b&gt;Part 2: Wherein I Handle My Second Pull Request&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;Thus, for the fix for &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=611042&quot;&gt;bug 611042&lt;/a&gt;, for which Atul submitted &lt;a href=&quot;https://github.com/mozilla/addon-sdk/pull/34&quot;&gt;GitHub pull request 34&lt;/a&gt;, I again reviewed the changes (also comprising &lt;a href=&quot;https://github.com/toolness/jetpack-sdk/commit/5e6ca0e1834e65623f6ac87d3828965da420847c&quot;&gt;two&lt;/a&gt; &lt;a href=&quot;https://github.com/toolness/jetpack-sdk/commit/1ab9c78c94fb08610460ad19fd763a7402fc233c&quot;&gt;commits&lt;/a&gt;) on GitHub, but then I pushed them to the &lt;a href=&quot;https://github.com/mozilla/addon-sdk&quot;&gt;canonical repository&lt;/a&gt; via this different set of commands (after discussion with Atul and Patrick Walton of the Rust team):&lt;br /&gt;&lt;ol&gt;&lt;li&gt;git checkout -b toolness-bug-611042 master&lt;/li&gt;&lt;li&gt;git pull &lt;a class=&quot;moz-txt-link-freetext&quot; href=&quot;https://github.com/toolness/jetpack-sdk.git&quot;&gt;https://github.com/toolness/jetpack-sdk.git&lt;/a&gt; bug-611042&lt;/li&gt;&lt;li&gt;(There might have been something else here, since the pull request resulted in a merge; I don&#39;t quite remember.)&lt;br /&gt;&lt;/li&gt;&lt;li&gt;git checkout master&lt;/li&gt;&lt;li&gt;git merge --no-ff --no-commit toolness-bug-611042&lt;/li&gt;&lt;li&gt;git commit --signoff -m &quot;bug 611042: remove request.response.xml for e10s compatibility; r=myk&quot; --author &quot;atul&quot;&lt;/li&gt;&lt;li&gt;git push upstream master&lt;/li&gt;&lt;/ol&gt;&lt;br /&gt;Because Atul&#39;s pull request was no longer against the tip (since I had just merged those previous changes), when I pulled the remote bug-611042 branch into my local toolness-bug-611042 branch (step 2), I had to merge his changes, which resulted in a &lt;a href=&quot;https://github.com/mozilla/addon-sdk/commit/6a3c9e2a614f29b61e580a7a7619f91dd1306eea&quot;&gt;merge commit&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;Merging the changes to my local master with &quot;--no-ff&quot; and &quot;--no-commit&quot; (step 5) then allowed me to commit the merge to my master branch manually (step 6), resulting in another &lt;a href=&quot;https://github.com/mozilla/addon-sdk/commit/9f202a3003cddace040bc695ab7137d4a31051ec&quot;&gt;merge commit&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;For the second merge commit, I specified &quot;--signoff&quot;, which added &quot;Signed-off-by: Myk Melez &lt;a class=&quot;moz-txt-link-rfc2396E&quot; href=&quot;mailto:myk@mozilla.org&quot;&gt;&lt;myk@mozilla.org&gt;&lt;/myk@mozilla.org&gt;&lt;/a&gt;&quot; to the commit message; crafted a custom commit message that included &quot;r=myk&quot;; and specified &#39;--author &quot;atul&quot;&#39;, which made Atul the author of the merge.&lt;br /&gt;&lt;br /&gt;I dislike having the former merge commit in history, since it&#39;s extraneous, unuseful details about how I did the merging locally before I pushed to the canonical repository. I&#39;m not sure how to avoid it, though.&lt;br /&gt;&lt;br /&gt;On the other hand, I like having the latter merge commit in history, since it provides context for Atul&#39;s &lt;a href=&quot;https://github.com/mozilla/addon-sdk/commit/5e6ca0e1834e65623f6ac87d3828965da420847c&quot;&gt;two&lt;/a&gt; &lt;a href=&quot;https://github.com/mozilla/addon-sdk/commit/1ab9c78c94fb08610460ad19fd763a7402fc233c&quot;&gt;commits&lt;/a&gt;: the bug number, the fact that the changes were reviewed, and a commit message that describes the changes as a whole.&lt;br /&gt;&lt;br /&gt;I&#39;m ambivalent about --signoff vs. adding &quot;r=myk&quot; to the commit message, as they seem equivalentish, with --signoff being more explicit (so in theory it might form part of an enlightened workflow in the future), while &quot;r=myk&quot; is simpler.&lt;br /&gt;&lt;br /&gt;And I dislike having made Atul the author of the merge, since it&#39;s incorrect: he wasn&#39;t the author of the merge, he was only the author of the changes (for which he is correctly credited). And if the merge itself caused problems (f.e. I accidentally backed out other recent changes in the process), I would be the one responsible for fixing those problems, not Atul.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-size: large;&quot;&gt;&lt;b&gt;Part 3: Pushing Patches&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;In addition to pull requests, one can also contribute via patches. I&#39;ve pushed a few of these via something like the following set of commands:&lt;br /&gt;&lt;ol&gt;&lt;li&gt;git apply patch.diff&lt;/li&gt;&lt;li&gt;git commit -a -m &quot;bug &lt;number&gt;: &lt;description changes=&quot;&quot; of=&quot;&quot;&gt;; r=myk&quot; --author &quot;&lt;author name=&quot;&quot;&gt;&quot;&lt;br /&gt;&lt;/author&gt;&lt;/description&gt;&lt;/number&gt;&lt;/li&gt;&lt;li&gt;git push upstream master&lt;/li&gt;&lt;/ol&gt;That results in a commit like &lt;a href=&quot;https://github.com/mozilla/addon-sdk/commit/026b4e8e78336c2dbbf30edb14e5db78ca4afb21&quot;&gt;this one&lt;/a&gt;, which shows me as the committer and the patch author as the author. And that seems like a fine record of what happened.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-size: large;&quot;&gt;&lt;b&gt;Part 4: To Bug or Not To Bug?&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;One of the questions GitHub raises is whether or not every change deserves a bug report. And if not, how do we differentiate those that do from the rest?&lt;br /&gt;&lt;br /&gt;I don&#39;t have the definitive answers to these questions, but my sense, from my experience so far, is that we shouldn&#39;t require all changes to be accompanied by bug reports, but larger, riskier, time-consuming, and/or controversial changes should have reports to capture history, provide a forum for discussion, and permit project planning; while bug reports should be optional for smaller, safer, quickly-resolved, and/or non-controversial changes.&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/7037189428773681360/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=7037189428773681360' title='3 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/7037189428773681360'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/7037189428773681360'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2010/11/github-workflow-experiences.html' title='Git/Hub Workflow Experiences'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/01837818348188071923</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-_YZppM5V97U/VedxzrpG9BI/AAAAAAAAAGA/wHrKKAgCoH0/s220/headshot-2014.jpg'/></author><thr:total>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-236254320002589931</id><published>2010-07-15T14:22:00.000-07:00</published><updated>2011-09-20T11:38:43.557-07:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="jetpack"/><title type='text'>My Recent Jetpack Presentations</title><content type='html'>The last few weeks have been presentation-heavy.&lt;br /&gt;&lt;br /&gt;First, I gave a presentation about the Jetpack project (past accomplishments, present status, future plans) at the &lt;a href=&quot;https://wiki.mozilla.org/MAOW:2010:London&quot;&gt;2010 London Mozilla Add-ons Workshop&lt;/a&gt; (MAOW), including a demo of using &lt;a href=&quot;https://builder.mozillalabs.com/&quot;&gt;Add-on Builder&lt;/a&gt; to build an add-on in five minutes.&lt;br /&gt;&lt;br /&gt;Then I reprised the Add-on Builder demo as part of the opening day keynote at the &lt;a href=&quot;https://wiki.mozilla.org/Summit2010&quot;&gt;Mozilla Summit&lt;/a&gt;, where it got a great reception. You can watch it in &lt;a href=&quot;http://www.youtube.com/watch?v=lKN4_fOKEWQ&quot;&gt;this Youtube video&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;Finally, I gave an updated version of the MAOW presentation on the third day of the summit. The slides are available in &lt;a href=&quot;https://people.mozilla.com/%7Emyk/presentations/Prepare%20for%20Liftoff%20-%20Summit%202010.odp&quot;&gt;OpenDocument&lt;/a&gt; and &lt;a href=&quot;https://people.mozilla.com/%7Emyk/presentations/Prepare%20for%20Liftoff%20-%20Summit%202010.pdf&quot;&gt;PDF&lt;/a&gt; formats, and Jetpack presentation materials generally are all available from the &lt;a href=&quot;https://wiki.mozilla.org/Labs/Jetpack/Presentations&quot;&gt;Jetpack Presentations wiki page&lt;/a&gt;.</content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/236254320002589931/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=236254320002589931' title='6 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/236254320002589931'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/236254320002589931'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2010/07/my-recent-jetpack-presentations.html' title='My Recent Jetpack Presentations'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/01837818348188071923</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-_YZppM5V97U/VedxzrpG9BI/AAAAAAAAAGA/wHrKKAgCoH0/s220/headshot-2014.jpg'/></author><thr:total>6</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-6583468542975089893</id><published>2010-03-05T23:03:00.001-08:00</published><updated>2010-04-18T23:58:52.841-07:00</updated><title type='text'>This blog has moved</title><content type='html'>&lt;br /&gt; This blog is now located at http://mykzilla.blogspot.com/.&lt;br /&gt; You will be automatically redirected in 30 seconds, or you may click &lt;a href=&#39;http://mykzilla.blogspot.com/&#39;&gt;here&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt; For feed subscribers, please update your feed subscriptions to&lt;br /&gt; http://mykzilla.blogspot.com/feeds/posts/default.&lt;br /&gt; </content><link rel="related" href="http://mykzilla.blogspot.com/" title="This blog has moved"/><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/6583468542975089893/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=6583468542975089893' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/6583468542975089893'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/6583468542975089893'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2010/03/this-blog-has-moved.html' title='This blog has moved'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/01837818348188071923</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-_YZppM5V97U/VedxzrpG9BI/AAAAAAAAAGA/wHrKKAgCoH0/s220/headshot-2014.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-7015776887411907934</id><published>2009-11-17T17:26:00.000-08:00</published><updated>2009-11-17T17:26:50.807-08:00</updated><title type='text'>The Skinny on Raindrop&#39;s Mailing List Extensions</title><content type='html'>Raindrop is an exploration of messaging innovation that strives to intelligently assist people in managing their flood of incoming messages. And mailing lists are a common source of messages you need to manage. So, with assistance from the Raindrop hackers, I wrote extensions that make it easier to deal with messages from mailing lists.&lt;br /&gt;&lt;br /&gt;Their goal is to soothe two particular pain points when dealing with mailing lists: grouping their messages together by list and unsubscribing from them once you&#39;re no longer interested in their subject matter.&lt;br /&gt;&lt;br /&gt;This post explains how the extensions do this; touches on some aspects of Raindrop&#39;s message processing and data storage models; and speculates about possible future directions for the extensions.&lt;br /&gt;&lt;h3&gt;Raindrop Extensibility&lt;/h3&gt;Raindrop is being built with the explicit goal of being broadly and deeply extensible, and it includes a number of APIs for adding and modifying functionality. The mailing list enhancements comprise two related extensions, one in the backend and one in the user interface.&lt;br /&gt;&lt;br /&gt;The backend extension plugs into Raindrop&#39;s incoming message processor, intercepting incoming email messages and extracting info about the mailing lists to which they belong. It also handles much of the work of unsubscribing from a list.&lt;br /&gt;&lt;br /&gt;The frontend extension plugs into Raindrop&#39;s Inflow application, modifying its interface to show you the most recent mailing list messages at a glance, group mailing list conversations together by list, and provide a button you can press to easily unsubscribe from a mailing list.&lt;br /&gt;&lt;h3&gt;Message Processing and Data Storage&lt;br /&gt;&lt;/h3&gt;Before getting into how the extensions work, it&#39;s useful to know a bit about how Raindrop processes and stores messages.&lt;br /&gt;&lt;br /&gt;Raindrop stores information using &lt;a href=&quot;http://couchdb.apache.org/&quot;&gt;CouchDB&lt;/a&gt;, a document-centric database whose principal unit of information storage and retrieval is the document (the equivalent of a record in SQL databases). Documents are just JSON blobs that can contain arbitrary name -&gt; value pairs (unlike SQL records, which can only contain values for predeclared columns).&lt;br /&gt;&lt;br /&gt;To distinguish between different kinds of documents, Raindrop assigns each a schema (similar to a table in SQL parlance) that describes (and may one day constrain) its properties. The &lt;tt&gt;rd.msg.email&lt;/tt&gt; schema is the primary schema representing an email message, while the &lt;tt&gt;rd.mailing-list&lt;/tt&gt; is the schema representing a mailing list, and the &lt;tt&gt;rd.msg.email.mailing-list&lt;/tt&gt; is a simple schema that associates messages with their lists.&lt;br /&gt;&lt;br /&gt;(In an SQL database, &lt;tt&gt;rd.msg.email&lt;/tt&gt; and &lt;tt&gt;rd.mailing-list&lt;/tt&gt; would be tables whose rows represent email messages and mailing lists, while &lt;tt&gt;rd.msg.email.mailing-list&lt;/tt&gt; would be a table whose rows map one to the other.)&lt;br /&gt;&lt;br /&gt;Note that there&#39;s a many-to-one relationship between messages and lists, since messages belong to a single list, although lists contain many messages, so &lt;tt&gt;rd.msg.email.mailing-list&lt;/tt&gt; isn&#39;t strictly necessary. Its &lt;tt&gt;list-id&lt;/tt&gt; property (which identifies the list to which the message belongs) could simply be a property of &lt;tt&gt;rd.msg.email&lt;/tt&gt; docs (or, in SQL terms, a foreign key in the &lt;tt&gt;rd.msg.email&lt;/tt&gt; table).&lt;br /&gt;&lt;br /&gt;But putting it into its own document has several advantages. First, it improves robustness, as it reduces the possibility of conflicts between extensions and core code writing to the same documents.&lt;br /&gt;&lt;br /&gt;It also improves write performance, as it&#39;s faster to add a document than to modify an existing one (although index generation and read performance can be an issue).&lt;br /&gt;&lt;br /&gt;Finally, it improves extensibility, because it makes it possible to write an extension that extends the backend mailing list extension.&lt;br /&gt;&lt;br /&gt;That&#39;s because Raindrop&#39;s incoming message processing model allows extensions to observe the creation of any kind of document, including those created by other extensions.&lt;br /&gt;&lt;br /&gt;So just as the mailing list extension observes the creation of &lt;tt&gt;rd.msg.email&lt;/tt&gt; documents, another extension can observe the creation of &lt;tt&gt;rd.msg.email.mailing-list&lt;/tt&gt; documents and process them further in some useful way. If the mailing list extension simply modified the original document instead of creating its own, that would require some additional and more complicated API.&lt;br /&gt;&lt;h3&gt;The Backend Extension&lt;/h3&gt;The primary function of the backend extension is to examine every incoming message and dress the ones from mailing lists with some additional structured information that the frontend can use to organize them.&lt;br /&gt;&lt;br /&gt;Backend extensions are accompanied by a JSON manifest that tells Raindrop what kinds of incoming documents it wants to intercept. The mailing list extension&#39;s manifest registers it as an observer of incoming &lt;tt&gt;rd.msg.email&lt;/tt&gt; documents, which get created when Raindrop retrieves an email message:&lt;br /&gt;&lt;pre style=&quot;background-color: rgb(238, 238, 238); border: 1px solid rgb(187, 187, 187); color: black; padding: 10px;&quot;&gt;&quot;schemas&quot; : {&lt;br /&gt; &quot;rd.ext.workqueue&quot; : {&lt;br /&gt; &quot;source_schemas&quot; : [&quot;rd.msg.email&quot;],&lt;br /&gt;...&lt;/pre&gt;&lt;br /&gt;The extension itself is a Python script with a &lt;tt&gt;handler&lt;/tt&gt; function that gets passed the &lt;tt&gt;rd.msg.email&lt;/tt&gt; document and looks to see if it contains a &lt;tt&gt;List-ID&lt;/tt&gt; header (or, in certain cases, another identifier) identifying the mailing list from which the message comes:&lt;br /&gt;&lt;pre style=&quot;background-color: rgb(238, 238, 238); border: 1px solid rgb(187, 187, 187); color: black; padding: 10px;&quot;&gt;def handler(message):&lt;br /&gt; ...&lt;br /&gt; if &#39;list-id&#39; in message[&#39;headers&#39;]:&lt;br /&gt; # Extract the ID and name of the mailing list from the list-id header.&lt;br /&gt; # Some mailing lists give only the ID, but others (Google Groups,&lt;br /&gt; # Mailman) provide both using the format &#39;NAME &amp;lt;id&amp;gt;&#39;, so we extract them&lt;br /&gt; # separately if we detect that format.&lt;br /&gt; list_id = message[&#39;headers&#39;][&#39;list-id&#39;][0]&lt;br /&gt; ...&lt;/pre&gt;&lt;br /&gt;If it doesn&#39;t find a list identifier, it simply returns, and Raindrop continues processing the message:&lt;br /&gt;&lt;pre style=&quot;background-color: rgb(238, 238, 238); border: 1px solid rgb(187, 187, 187); color: black; padding: 10px;&quot;&gt;if not list_id:&lt;br /&gt; logger.debug(&quot;NO LIST ID; ignoring message %s&quot;, message_id)&lt;br /&gt; return&lt;/pre&gt;&lt;br /&gt;Otherwise, it calls Raindrop&#39;s &lt;tt&gt;emit_schema&lt;/tt&gt; function to create an &lt;tt&gt;rd.msg.email.mailing-list&lt;/tt&gt; document linking the message document to an &lt;tt&gt;rd.mailing-list&lt;/tt&gt; document representing the mailing list:&lt;br /&gt;&lt;pre style=&quot;background-color: rgb(238, 238, 238); border: 1px solid rgb(187, 187, 187); color: black; padding: 10px;&quot;&gt;emit_schema(&#39;rd.msg.email.mailing-list&#39;, { &#39;list_id&#39;: list_id })&lt;/pre&gt;&lt;br /&gt;In this function call, &lt;tt&gt;rd.msg.email.mailing-list&lt;/tt&gt; is the type of document to create, while &lt;tt&gt;{ &#39;list_id&#39;: list_id }&lt;/tt&gt; is the document itself, written as Python that will get serialized to JSON.&lt;br /&gt;&lt;br /&gt;A document created inside a backend extension like this automatically gets a reference to the document the extension is processing (i.e. the &lt;tt&gt;rd.msg.email&lt;/tt&gt; document), so the only thing it has to explicitly include is a reference to the list document, in the form of a &lt;tt&gt;list_id&lt;/tt&gt; property whose value is the list identifier.&lt;br /&gt;&lt;br /&gt;The extension also checks if there&#39;s an &lt;tt&gt;rd.mailing-list&lt;/tt&gt; document in the database for the mailing list itself, and if not, it creates one, populating it with information from the message&#39;s &lt;tt&gt;List-*&lt;/tt&gt; headers, like how to unsubscribe from the list. Otherwise, it updates the existing mailing list document if the message&#39;s &lt;tt&gt;List-*&lt;/tt&gt; headers contain updates.&lt;br /&gt;&lt;h3&gt;The Frontend Extension&lt;/h3&gt;The frontend extension uses the information extracted by the backend to help users manage mailing lists in the Inflow application.&lt;br /&gt;&lt;br /&gt;It adds a widget to the Home view that shows you the last few messages from your lists at the bottom of the page, so you can keep an eye on those messages without having to give them your full attention:&lt;br /&gt;&lt;br /&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;http://www.melez.com/mykzilla/uploaded_images/latest-list-messages-714113.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img src=&quot;http://www.melez.com/mykzilla/uploaded_images/latest-list-messages-714111.png&quot; height=&quot;176&quot; width=&quot;320&quot; border=&quot;0&quot; /&gt;&lt;/a&gt;&lt;br /&gt;&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;It adds a list of your mailing lists to the Organizer widget:&lt;br /&gt;&lt;br /&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;http://www.melez.com/mykzilla/uploaded_images/mailing-list-list-722772.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img src=&quot;http://www.melez.com/mykzilla/uploaded_images/mailing-list-list-722768.png&quot; height=&quot;320&quot; width=&quot;190&quot; border=&quot;0&quot; /&gt;&lt;/a&gt;&lt;br /&gt;&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;And when you click on the name of a list, it shows you its conversations in the conversation pane:&lt;br /&gt;&lt;br /&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;http://www.melez.com/mykzilla/uploaded_images/list-conversations-763392.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img src=&quot;http://www.melez.com/mykzilla/uploaded_images/list-conversations-763369.png&quot; height=&quot;201&quot; width=&quot;320&quot; border=&quot;0&quot; /&gt;&lt;/a&gt;&lt;br /&gt;&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;In traditional mail clients, users who want to break out their list messages into separate buckets like this typically have to create a folder for each list to contain its messages and then a filter for each list to move incoming list messages into the appropriate folders. The extension does this for you automatically!&lt;br /&gt;&lt;br /&gt;Finally, while viewing list conversations, if the extension knows how to unsubscribe you from the list, it displays an Unsubscribe button:&lt;br /&gt;&lt;br /&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;http://www.melez.com/mykzilla/uploaded_images/unsubscribe-button-794151.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img src=&quot;http://www.melez.com/mykzilla/uploaded_images/unsubscribe-button-794149.png&quot; height=&quot;201&quot; width=&quot;320&quot; border=&quot;0&quot; /&gt;&lt;/a&gt;&lt;br /&gt;&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;Pressing the button (and then confirming your decision) unsubscribes you from the list. You don&#39;t have to do anything else, like remembering your username/password for some web page, sending an email, or confirming your request with the list admin. The extensions handle all those details for you so you don&#39;t have to know about them!&lt;br /&gt;&lt;h3&gt;List Unsubscription&lt;/h3&gt;In case you do want to know the details, however, it goes like this...&lt;br /&gt;&lt;br /&gt;First, the frontend extension sends a message to the list&#39;s admin address requesting unsubscription, with a certain command (like &quot;unsubscribe&quot;) in the subject or body of the message (lists often specify exactly what command to send in the &lt;tt&gt;mailto:&lt;/tt&gt; link they include in the &lt;tt&gt;List-Unsubscribe&lt;/tt&gt; header):&lt;br /&gt;&lt;pre style=&quot;background-color: rgb(238, 238, 238); border: 1px solid rgb(187, 187, 187); color: black; padding: 10px;&quot;&gt;From: Jan Reilly &lt;jan@example.com&gt;&lt;br /&gt;To: wasbigtalk-admin@example.com&lt;br /&gt;Subject: unsubscribe&lt;/jan@example.com&gt;&lt;/pre&gt;&lt;br /&gt;Then the server responds with a message requesting confirmation of the request, often putting a unique token into the Subject or Reply-To header to track the request:&lt;br /&gt;&lt;pre style=&quot;background-color: rgb(238, 238, 238); border: 1px solid rgb(187, 187, 187); color: black; padding: 10px;&quot;&gt;From: wasbigtalk-admin@example.com&lt;br /&gt;To: jan@example.com&lt;br /&gt;Subject: please confirm unsubscribe from wasbigtalk (4bc3b7e439fd)&lt;br /&gt;&lt;br /&gt;Hello jan@example.com,&lt;br /&gt;&lt;br /&gt;We have received a request to unsubscribe you from wasbigtalk.&lt;br /&gt;Please confirm this request to unsubscribe by replying to this email.&lt;br /&gt;...&lt;/pre&gt;&lt;br /&gt;Then the backend extension responds with a message confirming the request that includes the unique token:&lt;br /&gt;&lt;pre style=&quot;background-color: rgb(238, 238, 238); border: 1px solid rgb(187, 187, 187); color: black; padding: 10px;&quot;&gt;From: jan@example.com&lt;br /&gt;To: wasbigtalk-admin@example.com&lt;br /&gt;Subject: Re: please confirm unsubscribe from wasbigtalk (4bc3b7e439fd)&lt;/pre&gt;&lt;br /&gt;Finally, the server responds with a message confirming that the subscriber has, indeed, been unsubscribed:&lt;br /&gt;&lt;pre style=&quot;background-color: rgb(238, 238, 238); border: 1px solid rgb(187, 187, 187); color: black; padding: 10px;&quot;&gt;From: wasbigtalk-admin@example.com&lt;br /&gt;To: jan@example.com&lt;br /&gt;Subject: you have been unsubscribed from wasbigtalk&lt;br /&gt;&lt;br /&gt;Hello jan@example.com,&lt;br /&gt;&lt;br /&gt;Your unsubscription from wasbigtalk was successful.&lt;br /&gt;...&lt;/pre&gt;&lt;br /&gt;At this point, the backend extension marks the list unsubscribed in the database, and the frontend extension marks it unsubscribed in the user interface.&lt;br /&gt;&lt;br /&gt;This process matches the way much mailing list server software works, although there are daemons in the details, so the extensions have to be programmed to support each server individually.&lt;br /&gt;&lt;br /&gt;Currently, they know how to handle &lt;a href=&quot;http://groups.google.com/&quot;&gt;Google Groups&lt;/a&gt; and &lt;a href=&quot;http://www.gnu.org/software/mailman/&quot;&gt;Mailman&lt;/a&gt; lists. &lt;a href=&quot;http://www.mj2.org/&quot;&gt;Majordomo2&lt;/a&gt; (used by the &lt;a href=&quot;http://www.bugzilla.org/&quot;&gt;Bugzilla&lt;/a&gt; and &lt;a href=&quot;http://www.openbsd.org/&quot;&gt;OpenBSD&lt;/a&gt; projects, among others) is not supported, because it doesn&#39;t send &lt;tt&gt;List-*&lt;/tt&gt; headers (alhough supposedly it can be configured to do so). The &lt;a href=&quot;http://www.w3.org/&quot;&gt;W3C&lt;/a&gt;&#39;s list server is not yet supported, although it does send &lt;tt&gt;List-*&lt;/tt&gt; headers, and support should be fairly easy to add.&lt;br /&gt;&lt;br /&gt;Note that some of the processing the extension does is (locale-dependent) &quot;screen&quot;-scraping, as Google Groups and Mailman don&#39;t consistently identify the list ID and message type in some of their correspondence. In the long run, hopefully server software will improve in that regard. Perhaps someone can spearhead an effort to make it so?&lt;br /&gt;&lt;h3&gt;The Future&lt;/h3&gt;The extensions&#39; current features fit in well with Raindrop&#39;s goal of helping people better handle their flood of incoming messages. But there is surely much more they could do to help in this regard.&lt;br /&gt;&lt;br /&gt;Besides general improvements to reliability and robustness--like support for additional list servers and handling of localized admin messages--they could let you resubscribe to a mailing list from which you&#39;ve unsubscribed. And perhaps they could automatically fetch the messages you missed while you were away. Or even retrieve the entire archive of a list to which you&#39;re subscribed, so you can browse the archive in Raindrop!&lt;br /&gt;&lt;br /&gt;What bugs you about mailing lists? And how might Raindrop&#39;s mailing list extensions make them easier (and even funner) to use?</content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/7015776887411907934/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=7015776887411907934' title='7 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/7015776887411907934'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/7015776887411907934'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2009/11/skinny-on-raindrops-mailing-list.html' title='The Skinny on Raindrop&#39;s Mailing List Extensions'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/01837818348188071923</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-_YZppM5V97U/VedxzrpG9BI/AAAAAAAAAGA/wHrKKAgCoH0/s220/headshot-2014.jpg'/></author><thr:total>7</thr:total></entry><entry><id>tag:blogger.com,1999:blog-18929277.post-7740719470656815276</id><published>2009-11-04T15:58:00.001-08:00</published><updated>2009-11-04T15:58:03.687-08:00</updated><title type='text'>Building/Releasing Personas</title><content type='html'>Want to know how a popular extension like Personas gets built and released? Neither do I! Yet I know anyway. And I&#39;ve written it down for your edification! So &lt;a href=&quot;https://wiki.mozilla.org/Labs/Personas/Build&quot;&gt;check it out&lt;/a&gt;.&lt;br&gt; &lt;br&gt; </content><link rel='replies' type='application/atom+xml' href='http://mykzilla.blogspot.com/feeds/7740719470656815276/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=18929277&amp;postID=7740719470656815276' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/7740719470656815276'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/18929277/posts/default/7740719470656815276'/><link rel='alternate' type='text/html' href='http://mykzilla.blogspot.com/2009/11/buildingreleasing-personas.html' title='Building/Releasing Personas'/><author><name>Myk Melez</name><uri>http://www.blogger.com/profile/01837818348188071923</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://2.bp.blogspot.com/-_YZppM5V97U/VedxzrpG9BI/AAAAAAAAAGA/wHrKKAgCoH0/s220/headshot-2014.jpg'/></author><thr:total>0</thr:total></entry></feed> \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/resources/feed_atom_feedburner.xml b/mobile/android/tests/background/junit4/resources/feed_atom_feedburner.xml
new file mode 100644
index 0000000000..85c4aaddff
--- /dev/null
+++ b/mobile/android/tests/background/junit4/resources/feed_atom_feedburner.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet type="text/xsl" media="screen" href="/~d/styles/atom10full.xsl"?><?xml-stylesheet type="text/css" media="screen" href="http://feeds.feedburner.com/~d/styles/itemcontent.css"?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:openSearch="http://a9.com/-/spec/opensearchrss/1.0/" xmlns:blogger="http://schemas.google.com/blogger/2008" xmlns:georss="http://www.georss.org/georss" xmlns:gd="http://schemas.google.com/g/2005" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0"><id>tag:blogger.com,1999:blog-1531829516427013333</id><updated>2016-01-21T16:20:29.143+01:00</updated><category term="Instant Mustache" /><category term="Intent" /><category term="ViewPager" /><category term="camera" /><category term="BlinkLayout" /><category term="CameraFragment" /><category term="Layout" /><category term="ViewGroup" /><category term="glass" /><category term="google glass" /><category term="picture" /><category term="API" /><category term="Animation" /><category term="Boolean" /><category term="Clean Code" /><category term="Discovery" /><category term="External storage" /><category term="Fragment" /><category term="FragmentPagerAdapter" /><category term="FragmentStatePagerAdapter" /><category term="Framework" /><category term="Handler" /><category term="Integer" /><category term="IntentService" /><category term="Library" /><category term="Looper" /><category term="MediaScanner" /><category term="Minimal marketable app" /><category term="Network" /><category term="Offscreen pages" /><category term="OnClickListener" /><category term="OnPageChangeListener" /><category term="OrientationEventListener" /><category term="PagerAdapter" /><category term="Resources" /><category term="SeekBar" /><category term="Service" /><category term="Sharing" /><category term="String" /><category term="SurfaceView" /><category term="TextUtils" /><category term="UI" /><category term="Uri" /><category term="bezel swipe" /><category term="fake dragging" /><category term="firefox" /><category term="gdk" /><category term="immersion" /><category term="margin" /><category term="mirror api" /><category term="mockup" /><category term="mozilla" /><category term="orientation" /><category term="restricted profiles" /><category term="roadmap" /><category term="rotation" /><category term="scrolling" /><category term="sketch" /><category term="support library" /><category term="voice input" /><title type="text">Android Zeitgeist</title><subtitle type="html" /><link rel="alternate" type="text/html" href="http://www.androidzeitgeist.com/" /><author><name>Sebastian Kaspari</name><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><generator version="7.00" uri="http://www.blogger.com">Blogger</generator><openSearch:totalResults>23</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="self" type="application/atom+xml" href="http://feeds.feedburner.com/AndroidZeitgeist" /><feedburner:info uri="androidzeitgeist" /><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="hub" href="http://pubsubhubbub.appspot.com/" /><entry><id>tag:blogger.com,1999:blog-1531829516427013333.post-4435702616301783869</id><published>2015-09-17T19:46:00.000+02:00</published><updated>2015-09-17T19:46:08.239+02:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="firefox" /><category scheme="http://www.blogger.com/atom/ns#" term="mozilla" /><category scheme="http://www.blogger.com/atom/ns#" term="restricted profiles" /><title type="text">Support for restricted profiles in Firefox 42</title><content type="html">One of our goals for &lt;a href="https://www.mozilla.org/en-US/firefox/android/"&gt;Firefox for Android&lt;/a&gt; 42.0 was to create a kid and parent-friendly web experience (Project &lt;i&gt;KinderFox / KidFox&lt;/i&gt;): The browser should be easy to use for a kid and at the same time parents want to be in control and decide what the kid can do with it.&lt;br /&gt;&lt;br /&gt;There are a lot of things you can do to create a kid-friendly browsing experience. In this first version we focused on making the browser simpler by hiding complex or kid-unfriendly features and utilizing the parental controls of the Android system: &lt;i&gt;Restricted profiles&lt;/i&gt;.&lt;br /&gt;&lt;br /&gt;&lt;h3&gt;What are restricted profiles?&lt;/h3&gt;&lt;br /&gt;Restricted Profiles have been introduced in Android 4.3. The device administrator can create these profiles and restrict access to apps and features on the device. In addition to that restrictions inside an app can be configured if supported by the app.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://4.bp.blogspot.com/-2nsGZS3PMBQ/VeSXR42xZcI/AAAAAAAAluA/eXr4RUP6QQs/s1600/restricted-profiles-settings.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" height="400" src="http://4.bp.blogspot.com/-2nsGZS3PMBQ/VeSXR42xZcI/AAAAAAAAluA/eXr4RUP6QQs/s640/restricted-profiles-settings.png" width="640" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;Configuring which apps the restricted profile can acccess.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;A unique feature of restricted profiles is that they share the Google account of the device owner. It does not allow full-access to everything connected to the account but it allows the restricted user to watch content (e.g. movies and music) bought with that account or use paid applications. Of course only if the device owner explicitly allowed this. &lt;br /&gt;&lt;br /&gt;Unfortunately Restricted Profiles are only supported on tablets so far. It is a pity because in the meantime Google allowed to create full-featured and guest profiles on phones too.&lt;br /&gt;&lt;br /&gt;The following DevBytes episodes gives a good overview about the Restricted Profiles APIs:&lt;br /&gt;&lt;br /&gt;&lt;iframe allowfullscreen="" frameborder="0" height="315" src="https://www.youtube.com/embed/pdUcANNm72o" width="560"&gt;&lt;/iframe&gt; &lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;h3&gt;Being in control&lt;/h3&gt;&lt;br /&gt;Our final list of restrictable features for Firefox 42 contains 10 items:&lt;br /&gt;&lt;br /&gt;&lt;ul&gt;&lt;/ul&gt;&lt;ul&gt;&lt;li&gt;&lt;i&gt;Disable add-on installation &lt;/i&gt;&lt;/li&gt;&lt;li&gt;&lt;i&gt;Disable 'Import from Android' (Bookmark import) &lt;/i&gt;&lt;/li&gt;&lt;li&gt;&lt;i&gt;Disable developer tools &lt;/i&gt;&lt;/li&gt;&lt;li&gt;&lt;i&gt;Disable Home customization (&lt;a href="https://hacks.mozilla.org/2014/07/building-firefox-hub-add-ons-for-firefox-for-android/"&gt;Home panels&lt;/a&gt;) &lt;/i&gt;&lt;/li&gt;&lt;li&gt;&lt;i&gt;Disable Private Browsing &lt;/i&gt;&lt;/li&gt;&lt;li&gt;&lt;i&gt;Disable Location Services (Contributing to &lt;a href="https://location.services.mozilla.com/"&gt;Mozilla's Location Service&lt;/a&gt;) &lt;/i&gt;&lt;/li&gt;&lt;li&gt;&lt;i&gt;Disable Display settings &lt;/i&gt;&lt;/li&gt;&lt;li&gt;&lt;i&gt;Disable 'Clear browsing history' &lt;/i&gt;&lt;/li&gt;&lt;li&gt;&lt;i&gt;Disable master password &lt;/i&gt;&lt;/li&gt;&lt;li&gt;&lt;i&gt;Disable Guest Browsing &lt;/i&gt;&lt;/li&gt;&lt;/ul&gt;&lt;ul&gt;&lt;/ul&gt;&lt;br /&gt;Limiting access to features is a very personal decision. Not every parent wants to control every aspect of the browsing experience. That's why we decided to make these restrictions configurable by the parent. By implementing a broadcast receiver that listens to &lt;a href="https://developer.android.com/reference/android/content/Intent.html#ACTION_GET_RESTRICTION_ENTRIES"&gt;ACTION_GET_RESTRICTION_ENTRIES&lt;/a&gt; actions it's possible to send a list of restrictions to the system and so they will show up in the admin interface:&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/-9loC85f7nwM/VfrYo9ck7lI/AAAAAAAAmXo/ffox2Np3nZk/s1600/app-restrictions.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" height="400" src="http://1.bp.blogspot.com/-9loC85f7nwM/VfrYo9ck7lI/AAAAAAAAmXo/ffox2Np3nZk/s640/app-restrictions.png" width="640" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;Configuring restrictions of an application.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;Later the application can query the &lt;a href="http://developer.android.com/reference/android/os/UserManager.html"&gt;UserManager&lt;/a&gt; to ask which restrictions have been enabled or disabled.&lt;br /&gt;&lt;br /&gt;&lt;h3&gt;Technical details&lt;/h3&gt;&lt;br /&gt;&lt;b&gt;User restrictions vs. application restrictions&lt;/b&gt;&lt;br /&gt;There are two kinds of restrictions. &lt;a href="http://developer.android.com/reference/android/os/UserManager.html#getUserRestrictions%28%29"&gt;User restrictions&lt;/a&gt; are imposed on the user by the system and &lt;a href="http://developer.android.com/reference/android/os/UserManager.html#getApplicationRestrictions%28java.lang.String%29"&gt;application restrictions&lt;/a&gt; are added by an application via the broadcast mechanism mentioned above. An application can query the &lt;a href="http://developer.android.com/reference/android/os/UserManager.html"&gt;UserManager&lt;/a&gt; only for its own and global user restrictions.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Detecting restricted profiles&lt;/b&gt;&lt;br /&gt;One of the first things you might want to do in your app is to detect if the current user is using a restricted or a normal profile. There's no API method to do that and the video linked above suggests to query the user restrictions from the &lt;a href="http://developer.android.com/reference/android/os/UserManager.html"&gt;UserManager&lt;/a&gt; and if the returned bundle is not empty then you are in a restricted profile.&lt;br /&gt;&lt;br /&gt;This worked fine until we deployed the application to a phone running an Android M preview build. On this phone - that doesn't even support restricted profiles - the &lt;a href="http://developer.android.com/reference/android/os/UserManager.html"&gt;UserManager&lt;/a&gt; always returned a restriction. Whoops, suddenly everyone with Android M on their phones had a very limited &lt;a href="https://nightly.mozilla.org/"&gt;Firefox Nightly&lt;/a&gt;. We then switched to iterating over the returned Bundles (for application and user restrictions) and only assuming we are in a restricted profile if at least one restriction in those bundles is enabled (&lt;i&gt;getBoolean()&lt;/i&gt; returns true).&lt;br /&gt;&lt;br /&gt;In general it is a better approach to never detect whether you are in a restricted profile or not but instead always check whether a specific (application) restriction is enabled or not.&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;A resource qualifier would be nice&lt;/b&gt;&lt;br /&gt;Hiding features and UI in restricted profiles will add a lot of &lt;i&gt;if&lt;/i&gt; statements to the code base. It would have been nice to have a &lt;a href="http://developer.android.com/guide/topics/resources/providing-resources.html#AlternativeResources"&gt;resource qualifier&lt;/a&gt; for restricted profiles to load different layouts, drawables and other configurations. Besides that this would also solve the "detect restricted profile" problem quite elegant.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Strict mode: Disk read violation&lt;/b&gt;&lt;br /&gt;At runtime we noticed that we have been triggering a lot of disk read violations. Looking at &lt;a href="http://androidxref.com/5.1.1_r6/xref/frameworks/base/services/core/java/com/android/server/pm/UserManagerService.java#readApplicationRestrictionsLocked"&gt;Android's source code&lt;/a&gt; it turns out that &lt;a href="http://developer.android.com/reference/android/os/UserManager.html#getApplicationRestrictions%28java.lang.String%29"&gt;UserManager.getApplicationRestrictions()&lt;/a&gt; reads and parses an XML file on every call. Without caching anything like &lt;a href="http://developer.android.com/reference/android/content/SharedPreferences.html"&gt;SharedPreferences&lt;/a&gt; do. We worked around that by implementing our own memory cache and refreshing the list of restrictions whenever the application is resumed. To update restrictions the user will always have to switch to the admin profile and therefore leave (and later resume) the application.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;No Fun&lt;/b&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: left;"&gt;Android Marshmallow (6.0) introduced a new &lt;i&gt;Easter egg&lt;/i&gt; system restriction: &lt;a href="http://developer.android.com/reference/android/os/UserManager.html#DISALLOW_FUN"&gt;UserManager.DISALLOW_FUN&lt;/a&gt; - &lt;i&gt;Specifies if the user is not allowed to have fun. In some cases, the device owner may wish to prevent the user from experiencing amusement or joy while using the device. The default value is false&lt;/i&gt;.&amp;nbsp;&lt;/div&gt;&lt;div class="separator" style="clear: both; text-align: left;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;h3&gt;What's next?&lt;/h3&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/-8hdHsE2sf6Q/Vfr6lwJChxI/AAAAAAAAmZA/lSpXgbzrKsQ/s1600/kidbrowser.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" height="400" src="http://3.bp.blogspot.com/-8hdHsE2sf6Q/Vfr6lwJChxI/AAAAAAAAmZA/lSpXgbzrKsQ/s640/kidbrowser.png" width="640" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;Simplified browser UI with custom theme using a restricted profile (Firefox 42).&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;With the current set of restrictions parents can create a kid-friendly and simplified browsing experience. Of course there are a lot of more possible features around parental controls that come to mind like block lists and restricting Web APIs (Microphone, Webcam). Some of these ideas have already been filed (&lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1125710"&gt;Bug 1125710&lt;/a&gt;) and in addition to that we just started planning features for the next version (&lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1205615"&gt;Bug 1205615&lt;/a&gt;). More ideas are definitely welcome!&lt;br /&gt;&lt;br /&gt;&lt;h3&gt;Testing and feedback&lt;/h3&gt;&lt;br /&gt;At the time of this writing support for restricted profiles is available in &lt;a href="https://ftp.mozilla.org/pub/mozilla.org/mobile/nightly/latest-mozilla-aurora-android-api-11/"&gt;Aurora&lt;/a&gt; (42.0) and &lt;a href="https://nightly.mozilla.org/"&gt;Nightly&lt;/a&gt; (43.0) builds of Firefox. Restricted Profiles serve a very specific use case and therefore do not get as much usage coverage like other browser features. If you do have a tablet and are interested in restricted profiles then help us testing it! :)&lt;br /&gt;&lt;br /&gt;&lt;h3&gt;Contributing &lt;/h3&gt;&lt;br /&gt;Firefox for Android is open-source software and contributors are very welcome!&amp;nbsp;You can find me on IRC (irc.mozilla.org) in #mobile (my nickname is "sebastian"), on &lt;a href="https://twitter.com/Anti_Hype"&gt;Twitter&lt;/a&gt; and &lt;a href="http://plus.google.com/+SebastianKaspari"&gt;Google+&lt;/a&gt;. &lt;a href="https://wiki.mozilla.org/Mobile/Get_Involved"&gt;Get involved with Firefox for Android&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;&lt;h3&gt;More resources about Project &lt;i&gt;KidFox&lt;/i&gt;&lt;/h3&gt;&lt;ul&gt;&lt;/ul&gt;&lt;br /&gt;&lt;ul&gt;&lt;li&gt;&lt;a href="https://wiki.mozilla.org/Mobile/Projects/Kinderfox"&gt;Mozilla Wiki: Project KinderFox&lt;/a&gt; &lt;/li&gt;&lt;li&gt;&lt;a href="https://wiki.mozilla.org/Mobile/Projects/Kid_browsing"&gt;Mozilla Wiki: KidFox proposal&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="https://wiki.mozilla.org/Mobile/Briefs/Kidfox"&gt;Mozilla wiki: KidFox brief&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1125710"&gt;Generic meta bug &lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1125984"&gt;Bugzilla meta bug for v1&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1205615"&gt;Bugzilla meta bug for v2&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;ul&gt;&lt;/ul&gt;&lt;img src="http://feeds.feedburner.com/~r/AndroidZeitgeist/~4/xaSicfGuwOU" height="1" width="1" alt=""/&gt;</content><link rel="replies" type="application/atom+xml" href="http://www.androidzeitgeist.com/feeds/4435702616301783869/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="http://www.androidzeitgeist.com/2015/09/support-restricted-profiles-firefox.html#comment-form" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/4435702616301783869" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/4435702616301783869" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/AndroidZeitgeist/~3/xaSicfGuwOU/support-restricted-profiles-firefox.html" title="Support for restricted profiles in Firefox 42" /><author><name>Sebastian Kaspari</name><uri>https://plus.google.com/112283223674539938062</uri><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://4.bp.blogspot.com/-2nsGZS3PMBQ/VeSXR42xZcI/AAAAAAAAluA/eXr4RUP6QQs/s72-c/restricted-profiles-settings.png" height="72" width="72" /><thr:total>0</thr:total><gd:extendedProperty name="commentSource" value="1" /><gd:extendedProperty name="commentModerationMode" value="FILTERED_POSTMOD" /><feedburner:origLink>http://www.androidzeitgeist.com/2015/09/support-restricted-profiles-firefox.html</feedburner:origLink></entry><entry><id>tag:blogger.com,1999:blog-1531829516427013333.post-1833936883751020212</id><published>2014-11-20T08:30:00.001+01:00</published><updated>2014-11-20T08:32:09.334+01:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Discovery" /><category scheme="http://www.blogger.com/atom/ns#" term="Intent" /><category scheme="http://www.blogger.com/atom/ns#" term="Library" /><category scheme="http://www.blogger.com/atom/ns#" term="Network" /><title type="text">Introducing Android Network Intents</title><content type="html">&lt;a href="https://github.com/pocmo/Android-Network-Intents"&gt;Android Network Intents&lt;/a&gt; is a library that I wrote for &lt;a href="http://landsofruin.com/"&gt;Lands of Ruin&lt;/a&gt; - a game that two friends and I are developing. To avoid a complicated network setup to play the game against a friend, we needed a way to discover games running on the local network. Android offers a &lt;a href="http://developer.android.com/training/connect-devices-wirelessly/nsd.html"&gt;Network Service Discovery (NSD)&lt;/a&gt; since API level 16 (Android 4.1) but we kept running into problems using it. This lead to writing this library.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;What does the library do?&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;The library allows you to send &lt;a href="http://developer.android.com/reference/android/content/Intent.html"&gt;Intents&lt;/a&gt; to listening clients on the local network (WiFi) without knowing who these clients are. Sender and receiver do not need to connect to each other. Therefore the library can be used to write custom discovery protocols.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;Sending Intents (Transmitter)&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;An &lt;i&gt;Intent&lt;/i&gt; is sent by using the&amp;nbsp;&lt;i&gt;Transmitter&lt;/i&gt;&amp;nbsp;class. A&amp;nbsp;&lt;i&gt;TransmitterException&lt;/i&gt;&amp;nbsp;is thrown in case of error.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-r6N5E7HFdJc/VGyPPqvem4I/AAAAAAAAbn8/aJ_XtPS7lkU/s1600/Screen%2BShot%2B2014-11-19%2Bat%2B13.33.29.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/-r6N5E7HFdJc/VGyPPqvem4I/AAAAAAAAbn8/aJ_XtPS7lkU/s1600/Screen%2BShot%2B2014-11-19%2Bat%2B13.33.29.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Android-Network-Intents/wiki/Sending-Intents"&gt;Sending an Intent&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;Receiving Intents (Receiver)&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;Intents are received using the&amp;nbsp;&lt;i&gt;Discovery&lt;/i&gt;&amp;nbsp;class. Once started by calling&amp;nbsp;&lt;i&gt;enable()&lt;/i&gt;&amp;nbsp;the&amp;nbsp;&lt;i&gt;Discovery&lt;/i&gt;&amp;nbsp;class will spawn a background thread that will wait for incoming&amp;nbsp;&lt;i&gt;Intent&lt;/i&gt;&amp;nbsp;objects. A&amp;nbsp;&lt;i&gt;DiscoveryListener&lt;/i&gt;&amp;nbsp;instance will be notified about every incoming&amp;nbsp;&lt;i&gt;Intent&lt;/i&gt;.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-fNRqCrKBhbg/VGyPnT00VdI/AAAAAAAAboE/XnejMaKLuyw/s1600/Screen%2BShot%2B2014-11-19%2Bat%2B13.37.46.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/-fNRqCrKBhbg/VGyPnT00VdI/AAAAAAAAboE/XnejMaKLuyw/s1600/Screen%2BShot%2B2014-11-19%2Bat%2B13.37.46.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Android-Network-Intents/wiki/Receiving-Intents"&gt;Writing a DiscoveryListener to receive events.&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/-_GOcgSh-4qY/VGyPpC2sA-I/AAAAAAAAboM/TVMtxnj-zKk/s1600/Screen%2BShot%2B2014-11-19%2Bat%2B13.37.55.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://3.bp.blogspot.com/-_GOcgSh-4qY/VGyPpC2sA-I/AAAAAAAAboM/TVMtxnj-zKk/s1600/Screen%2BShot%2B2014-11-19%2Bat%2B13.37.55.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Android-Network-Intents/wiki/Receiving-Intents"&gt;Starting and stoping the discovery.&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;Things you should know&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;The Intents are sent as &lt;a href="http://en.wikipedia.org/wiki/Multicast"&gt;UDP multicast&lt;/a&gt; packets. Unlike TCP the UDP protocol does not guarantee that a sent packet will be received and there is no confirmation or retry mechanism. Even though losing a packet only happens rarely in a stable WiFi, the library is not intended for using as a stable &lt;i&gt;communication&lt;/i&gt; protocol. Instead you can use it to find other clients (by sending an Intent in a periodic interval) and then establish a stable TCP connection for communication.&lt;br /&gt;&lt;br /&gt;On GitHub you can find a &lt;a href="https://github.com/pocmo/Android-Network-Intents/tree/master/samples"&gt;chat sample application&lt;/a&gt; using the library. While this is a convenient example, it is not a good use of the library for the reasons state above. You obviously do not want to lose chat messages.&lt;br /&gt;&lt;br /&gt;We are using the library for almost two years in Lands of Ruin and didn't observe any problems. However the game only runs on tablets so far. In theory the library should run on all Android versions back to API level 3 (Android 1.5) but this has obviously never been tested.&lt;br /&gt;&lt;br /&gt;You can find &lt;a href="https://github.com/pocmo/Android-Network-Intents"&gt;Android Network Intents on GitHub&lt;/a&gt;.&lt;img src="http://feeds.feedburner.com/~r/AndroidZeitgeist/~4/frg0Ba2z4H0" height="1" width="1" alt=""/&gt;</content><link rel="replies" type="application/atom+xml" href="http://www.androidzeitgeist.com/feeds/1833936883751020212/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="http://www.androidzeitgeist.com/2014/11/introducing-android-network-intents17.html#comment-form" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/1833936883751020212" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/1833936883751020212" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/AndroidZeitgeist/~3/frg0Ba2z4H0/introducing-android-network-intents17.html" title="Introducing Android Network Intents" /><author><name>Sebastian Kaspari</name><uri>https://plus.google.com/112283223674539938062</uri><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://2.bp.blogspot.com/-r6N5E7HFdJc/VGyPPqvem4I/AAAAAAAAbn8/aJ_XtPS7lkU/s72-c/Screen%2BShot%2B2014-11-19%2Bat%2B13.33.29.png" height="72" width="72" /><thr:total>0</thr:total><gd:extendedProperty name="commentSource" value="1" /><gd:extendedProperty name="commentModerationMode" value="FILTERED_POSTMOD" /><feedburner:origLink>http://www.androidzeitgeist.com/2014/11/introducing-android-network-intents17.html</feedburner:origLink></entry><entry><id>tag:blogger.com,1999:blog-1531829516427013333.post-8654031246499256112</id><published>2013-12-24T15:24:00.001+01:00</published><updated>2013-12-24T15:24:38.598+01:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="glass" /><category scheme="http://www.blogger.com/atom/ns#" term="google glass" /><category scheme="http://www.blogger.com/atom/ns#" term="immersion" /><category scheme="http://www.blogger.com/atom/ns#" term="voice input" /><title type="text">Hello World Immersion - Developing for Google Glass #2</title><content type="html">This article describes how to create a simple hello world application for Google Glass using the&amp;nbsp;Glass Development Kit (GDK). As described in the &lt;a href="http://www.androidzeitgeist.com/2013/12/mirror-api-gdk-developing-google-glass.html"&gt;previous article&lt;/a&gt; you have two options how your Glassware should show up on the device: As a &lt;i&gt;live card&lt;/i&gt; that is part of the timeline or as an &lt;i&gt;immersion&lt;/i&gt;&amp;nbsp;that is displayed outside of the context of the timeline. This article focuses on how to write an immersion.&lt;br /&gt;&lt;b&gt;&lt;span style="font-size: large;"&gt;&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style="font-size: large;"&gt;What is an immersion?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;An immersion is basically an Android activity. The name immersion implies that it is not part of the normal Glass timeline. Instead it takes full control of the device - except for the back gesture (Swipe down). To go back to the timeline you need to leave the immersion.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/-75TfF3lNoWs/UrhgTb9OPZI/AAAAAAAASH4/wESuu-yTUy4/s1600/glass_timeline_immersion.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://1.bp.blogspot.com/-75TfF3lNoWs/UrhgTb9OPZI/AAAAAAAASH4/wESuu-yTUy4/s1600/glass_timeline_immersion.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;Once started an immersion takes full control of the screen.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;b&gt;&lt;span style="font-size: large;"&gt;Project setup&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;Create a normal Android project with the following settings:&lt;br /&gt;&lt;br /&gt;&lt;ul&gt;&lt;li&gt;Set &lt;i&gt;minSdkVersion&lt;/i&gt; and &lt;i&gt;targetSdkVersion&lt;/i&gt; to 15 (Android 4.0.3)&lt;/li&gt;&lt;li&gt;Set &lt;i&gt;compileSdkVersion&lt;/i&gt; to&amp;nbsp;&lt;i&gt;"Google Inc.:Glass Development Kit Sneak Peek:15"&lt;/i&gt;&lt;/li&gt;&lt;li&gt;Do not assign a theme to your application or derive your own theme from&amp;nbsp;&lt;i&gt;Theme.DeviceDefault&lt;/i&gt;&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;&lt;i&gt;&lt;br /&gt;&lt;/i&gt;&lt;/div&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;Creating the immersion&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;Let's create a simple activity. The &lt;a href="https://developers.google.com/glass/develop/gdk/reference/com/google/android/glass/app/Card"&gt;Card&lt;/a&gt; class helps us to create a layout that looks like a timeline card.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/-Fj2t5BJi7yk/Urlo4Z4_FYI/AAAAAAAASI0/4qv2fF4ElX8/s1600/helloworldactivity.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://1.bp.blogspot.com/-Fj2t5BJi7yk/Urlo4Z4_FYI/AAAAAAAASI0/4qv2fF4ElX8/s1600/helloworldactivity.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Android-Zeitgeist-Samples/blob/master/Glass/HelloWorldImmersion/HelloWorld/src/main/java/de/androidzeitgeist/glass/helloworld/immersion/HelloWorldActivity.java"&gt;HelloWorldActivity.java&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;b&gt;&lt;span style="font-size: large;"&gt;Launching the Glassware - Voice commands&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;After creating the activity we need a way to start our Glassware. A common way to launch Glassware is to use a voice trigger. Let's add a simple voice trigger to start our &lt;i&gt;hello world&lt;/i&gt; activity.&lt;br /&gt;&lt;br /&gt;First we need to declare a string resource for our voice command.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/-MnJseZKHYxY/Url83ImZEZI/AAAAAAAASJc/Lrxu_J2gZ8w/s1600/resource_hello_world.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://3.bp.blogspot.com/-MnJseZKHYxY/Url83ImZEZI/AAAAAAAASJc/Lrxu_J2gZ8w/s1600/resource_hello_world.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Android-Zeitgeist-Samples/blob/master/Glass/HelloWorldImmersion/HelloWorld/src/main/res/values/strings.xml"&gt;strings.xml&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;The next step is to create an XML resource file for the voice trigger using the previously created string value.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://4.bp.blogspot.com/-8y21kaJD3fI/Url839921yI/AAAAAAAASJw/HUZ4Po3-lr4/s1600/trigger_hello_world.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://4.bp.blogspot.com/-8y21kaJD3fI/Url839921yI/AAAAAAAASJw/HUZ4Po3-lr4/s1600/trigger_hello_world.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Android-Zeitgeist-Samples/blob/master/Glass/HelloWorldImmersion/HelloWorld/src/main/res/xml/voice_trigger.xml"&gt;voice_trigger.xml&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;Now we can add an intent filter for the VOICE_TRIGGER action to our activity. A meta-data tag links it to the XML file we wrote above.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/-GfHAXMZTDas/Url83illC8I/AAAAAAAASJs/oBmo7h4_Tuk/s1600/hello_world_activity.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://1.bp.blogspot.com/-GfHAXMZTDas/Url83illC8I/AAAAAAAASJs/oBmo7h4_Tuk/s1600/hello_world_activity.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Android-Zeitgeist-Samples/blob/master/Glass/HelloWorldImmersion/HelloWorld/src/main/AndroidManifest.xml"&gt;AndroidManifest.xml&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;The developer guide requires you to add an icon for the touch menu to the activity (white in color on transparent background, 50x50 pixels). The&amp;nbsp;&lt;a href="http://glass-asset-utils.appspot.com/icons-submission.html"&gt;Glass Asset Studio&lt;/a&gt;&amp;nbsp;is a helpful tool to generate these icons.&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/-MWCSEYXWpUQ/UrmYIdPJp6I/AAAAAAAASKE/h2c7egiWuNs/s1600/50x50_icon.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" src="http://3.bp.blogspot.com/-MWCSEYXWpUQ/UrmYIdPJp6I/AAAAAAAASKE/h2c7egiWuNs/s1600/50x50_icon.png" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;The final Glassware&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;Now we can start our Glassware by saying "&lt;i&gt;ok glass, show hello world&lt;/i&gt;":&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://4.bp.blogspot.com/-R3N3nwLXIIg/UrlsMbHPgkI/AAAAAAAASJA/9P6foH55OUc/s1600/hello_world_glassware.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" height="340" src="http://4.bp.blogspot.com/-R3N3nwLXIIg/UrlsMbHPgkI/AAAAAAAASJA/9P6foH55OUc/s400/hello_world_glassware.png" width="400" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;Another option to start our Glassware is to use the touch menu and scroll to the "&lt;i&gt;show hello world&lt;/i&gt;" command:&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-5KjJBjb5qn4/UrlupI3MTuI/AAAAAAAASJM/j6-nNCKgWJM/s1600/hello_world_glassware_menu.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" height="340" src="http://2.bp.blogspot.com/-5KjJBjb5qn4/UrlupI3MTuI/AAAAAAAASJM/j6-nNCKgWJM/s400/hello_world_glassware_menu.png" width="400" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;The source code for this &lt;a href="https://github.com/pocmo/Android-Zeitgeist-Samples/tree/master/Glass/HelloWorldImmersion"&gt;Hello World Glassware is available on GitHub&lt;/a&gt;.&lt;img src="http://feeds.feedburner.com/~r/AndroidZeitgeist/~4/fr-WO1v1SIU" height="1" width="1" alt=""/&gt;</content><link rel="replies" type="application/atom+xml" href="http://www.androidzeitgeist.com/feeds/8654031246499256112/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="http://www.androidzeitgeist.com/2013/12/google-glass-immersion-hello-world.html#comment-form" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/8654031246499256112" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/8654031246499256112" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/AndroidZeitgeist/~3/fr-WO1v1SIU/google-glass-immersion-hello-world.html" title="Hello World Immersion - Developing for Google Glass #2" /><author><name>Sebastian Kaspari</name><uri>https://plus.google.com/112283223674539938062</uri><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://1.bp.blogspot.com/-75TfF3lNoWs/UrhgTb9OPZI/AAAAAAAASH4/wESuu-yTUy4/s72-c/glass_timeline_immersion.png" height="72" width="72" /><thr:total>0</thr:total><gd:extendedProperty name="commentSource" value="1" /><gd:extendedProperty name="commentModerationMode" value="FILTERED_POSTMOD" /><feedburner:origLink>http://www.androidzeitgeist.com/2013/12/google-glass-immersion-hello-world.html</feedburner:origLink></entry><entry><id>tag:blogger.com,1999:blog-1531829516427013333.post-5605536583111663735</id><published>2013-12-22T13:03:00.001+01:00</published><updated>2013-12-22T13:03:25.308+01:00</updated><title type="text">Android 2013</title><content type="html">&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://4.bp.blogspot.com/-swoL-7rAksk/UrbIgrRXS0I/AAAAAAAASEk/FuB-KSnuxuU/s1600/android_2013.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" src="http://4.bp.blogspot.com/-swoL-7rAksk/UrbIgrRXS0I/AAAAAAAASEk/FuB-KSnuxuU/s1600/android_2013.png" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;It's the end of the year - &lt;a href="http://www.youtube.com/watch?v=H7jtC8vjXw8"&gt;YouTube&lt;/a&gt; and &lt;a href="http://www.youtube.com/watch?v=Lv-sY_z8MNs"&gt;Google Zeitgeist&lt;/a&gt; have posted their reviews. Let's have a look on what happened in the Android world in 2013.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;a href="http://4.bp.blogspot.com/-S2ecXdl66_I/UrbQgaKZPxI/AAAAAAAASFE/qChEPdoly-w/s1600/nexus4.png" imageanchor="1" style="clear: right; float: right; margin-bottom: 1em; margin-left: 1em;"&gt;&lt;img border="0" height="200" src="http://4.bp.blogspot.com/-S2ecXdl66_I/UrbQgaKZPxI/AAAAAAAASFE/qChEPdoly-w/s200/nexus4.png" width="200" /&gt;&lt;/a&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;January&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;2012 is over and the Nexus 4 is the current flagship phone made by Google and LG.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;February&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;a href="http://android-developers.blogspot.de/2013/02/google-sign-in-now-part-of-google-play.html"&gt;Google+ Sign-In is integrated&lt;/a&gt; into the Google Play Services and Google starts accepting &lt;a href="http://www.nytimes.com/2013/02/21/technology/google-looks-to-make-its-computer-glasses-stylish.html?_r=0"&gt;applications for the Google Glass Explorer program&lt;/a&gt;.&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;March&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;The new &lt;a href="http://android-developers.blogspot.de/2013/03/now-is-time-to-switch-to-new-google.html"&gt;Android developer console is out of preview&lt;/a&gt;. While &lt;a href="http://officialandroid.blogspot.de/2013/03/celebrating-google-plays-first-birthday.html"&gt;Google Play celebrates it first birthday&lt;/a&gt;, the &lt;a href="http://techcrunch.com/2013/07/01/android-led-by-samsung-continues-to-storm-the-smartphone-market-pushing-a-global-70-market-share/?ncid=tcdaily"&gt;market share of Android hits 64%&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;April&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;The &lt;a href="http://android-developers.blogspot.de/2013/04/update-on-tablet-app-guidelines-and.html"&gt;tablet guidelines are updated&lt;/a&gt; and the Android developer console starts to &lt;a href="http://android-developers.blogspot.de/2013_04_01_archive.html"&gt;show tablet optimization tips&lt;/a&gt;. Google pushes a &lt;a href="http://android-developers.blogspot.de/2013/04/new-look-new-purchase-flow-in-google.html"&gt;Google Play app update&lt;/a&gt; that features a redesigned UI. Samsung releases it new flaship phone - &lt;a href="http://en.wikipedia.org/wiki/Samsung_Galaxy_S4"&gt;the Samsung Galaxy S4&lt;/a&gt;.&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;May&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;a href="http://3.bp.blogspot.com/-wZeaMyx7VUs/UrbPAzv69mI/AAAAAAAASE4/-VEbnrV9MF4/s1600/io2013.png" imageanchor="1" style="clear: right; float: right; margin-bottom: 1em; margin-left: 1em;"&gt;&lt;img border="0" src="http://3.bp.blogspot.com/-wZeaMyx7VUs/UrbPAzv69mI/AAAAAAAASE4/-VEbnrV9MF4/s1600/io2013.png" /&gt;&lt;/a&gt;The &lt;a href="https://developers.google.com/events/io/"&gt;Google I/O&lt;/a&gt;&amp;nbsp;takes place for three days from May 15th to 17th. This time there won't be a new Android release. Instead Google releases &lt;a href="http://android-developers.blogspot.de/2013/05/social-gaming-location-and-more-in.html"&gt;new game services and a new location API&lt;/a&gt;. At the Google I/O a new IDE for Android development is introduced: &lt;a href="http://android-developers.blogspot.de/2013/05/android-studio-ide-built-for-android.html"&gt;Android Studio&lt;/a&gt;. Since then every couple of weeks a new Android Studio update is pushed to the developer community.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt; &lt;span style="font-size: large;"&gt;&lt;b&gt;July&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;A new flavor of Android Jelly Bean is released: &lt;a href="http://android-developers.blogspot.de/2013/07/android-43-and-updated-developer-tools.html"&gt;Android 4.3&lt;/a&gt;. Open GL ES 3.0 and support for&amp;nbsp;low-power Bluetooth Smart devices are some of the new features. Furthermore a new version of the Nexus 7 is released. Together with the new tablet Google &lt;a href="http://officialandroid.blogspot.de/2013/07/from-tvs-to-tablets-everything-you-love.html"&gt;releases the Chromecast dongle&lt;/a&gt; and the &lt;a href="http://googledevelopers.blogspot.de/2013/07/cast-content-from-your-apps-to-tv-with.html"&gt;Google Cast SDK preview&lt;/a&gt;.&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;August&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;Google releases &lt;a href="http://android-developers.blogspot.de/2013/08/google-play-services-32.html"&gt;version 3.2 of the Google Play Services&lt;/a&gt;. The update&amp;nbsp;includes several enhancements to the Location Based Services. With the r18 release of the support library Google&amp;nbsp;released a new backward-compatible &lt;a href="http://android-developers.blogspot.de/2013/08/actionbarcompat-and-io-2013-app-source.html"&gt;Action Bar implementation called ActionBarCompat&lt;/a&gt;. Motorola is releasing the &lt;a href="http://en.wikipedia.org/wiki/Moto_X"&gt;Moto X&lt;/a&gt; - its first phone since the company has been acquired by Google. The same month &lt;a href="https://plus.google.com/u/0/+HugoBarra/posts/BzZMqRht1xQ"&gt;Hugo Barra announces to leave Google&lt;/a&gt; after 5½ years to join the Xiaomi team in China.&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;September&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;a href="http://android-developers.blogspot.de/2013/09/renderscript-in-android-support-library.html"&gt;RenderScript is now part of the support library&lt;/a&gt; and can be used&amp;nbsp;on plaform versions all the way back to Android 2.2 (Froyo). Jean-Baptiste Queru, who worked on the Android Open Source Project at Google, &lt;a href="http://www.androidpolice.com/2013/09/17/jean-baptiste-queru-now-at-yahoo-as-senior-principal-engineer-working-on-mobile-apps/"&gt;starts a new job at Yahoo&lt;/a&gt;. Google launches the &lt;a href="http://officialandroid.blogspot.de/2013/08/find-your-lost-phone-with-android.html"&gt;Android device manager website&lt;/a&gt; to&amp;nbsp;locate, lock and ring misplaced devices.&lt;br /&gt;&lt;br /&gt; &lt;a href="http://1.bp.blogspot.com/-qksojJjcafk/UrbQ_JNFI_I/AAAAAAAASFM/mOaNvKPz2d0/s1600/nexus5.png" imageanchor="1" style="clear: right; float: right; margin-bottom: 1em; margin-left: 1em;"&gt;&lt;img border="0" height="200" src="http://1.bp.blogspot.com/-qksojJjcafk/UrbQ_JNFI_I/AAAAAAAASFM/mOaNvKPz2d0/s200/nexus5.png" width="200" /&gt;&lt;/a&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;October&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;After a lot of leaks and rumors &lt;a href="https://www.google.com/nexus/5/"&gt;a new Nexus phone is released&lt;/a&gt; on Halloween. Together with the Nexus 5 a new Android version - &lt;a href="http://www.android.com/kitkat/"&gt;Android 4.4 KitKat&lt;/a&gt; - is published.&amp;nbsp;Full-screen immersive mode, a new transitions framework, a printing framework and a storage access framework are some of the many new features. In addition to that he &lt;a href="http://android-developers.blogspot.de/2013/10/google-play-services-40.html"&gt;Google Play Services are updated to version 4.0&lt;/a&gt;. With &lt;a href="https://plus.google.com/+RomainGuy/posts/faCzPs6GKtg"&gt;Romain Guy another popular Android team member is leaving&lt;/a&gt; - but remaining at Google.&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;November&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;The &lt;a href="http://android-developers.blogspot.de/2013/11/app-translation-service-now-available.html"&gt;App Translation Service&lt;/a&gt;, announced at Google I/O, is now available for every developer. Motorola releases a second phone - the &lt;a href="http://en.wikipedia.org/wiki/Moto_G"&gt;Moto G&lt;/a&gt;. Android hits a new record with &lt;a href="http://www.highlightpress.com/android-tops-80-global-smartphone-market-share-windows-phone-up-156-year-on-year/6708/tharper"&gt;80% market share&lt;/a&gt;. The Google Glass team releases a first sneak peek version of the &lt;a href="https://developers.google.com/glass/develop/gdk/"&gt;Glass development kit (GDK)&lt;/a&gt;.&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;December&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;Two small updates for Android KitKat are released: &lt;a href="http://en.wikipedia.org/wiki/Android_version_history#Android_4.4_KitKat_.28API_level_19.29"&gt;Android 4.4.1 and 4.4.2&lt;/a&gt;. The Android device manager &lt;a href="http://techcrunch.com/2013/12/11/google-android-device-manager-play-store/"&gt;is now available as an app&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;The &lt;i&gt;Android Design in Action&lt;/i&gt; team releases its 2013 Recap:&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;iframe allowfullscreen="" frameborder="0" height="315" src="//www.youtube.com/embed/ajO2zFEtEYs" width="560"&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;span style="font-size: large;"&gt;2014?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;What has been your Android highlight in 2013 and what are your wishes for 2014?&lt;img src="http://feeds.feedburner.com/~r/AndroidZeitgeist/~4/f-NE_F-73GY" height="1" width="1" alt=""/&gt;</content><link rel="replies" type="application/atom+xml" href="http://www.androidzeitgeist.com/feeds/5605536583111663735/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="http://www.androidzeitgeist.com/2013/12/android-2013.html#comment-form" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/5605536583111663735" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/5605536583111663735" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/AndroidZeitgeist/~3/f-NE_F-73GY/android-2013.html" title="Android 2013" /><author><name>Sebastian Kaspari</name><uri>https://plus.google.com/112283223674539938062</uri><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://4.bp.blogspot.com/-swoL-7rAksk/UrbIgrRXS0I/AAAAAAAASEk/FuB-KSnuxuU/s72-c/android_2013.png" height="72" width="72" /><thr:total>0</thr:total><gd:extendedProperty name="commentSource" value="1" /><gd:extendedProperty name="commentModerationMode" value="FILTERED_POSTMOD" /><feedburner:origLink>http://www.androidzeitgeist.com/2013/12/android-2013.html</feedburner:origLink></entry><entry><id>tag:blogger.com,1999:blog-1531829516427013333.post-3139284173480483099</id><published>2013-12-16T21:21:00.004+01:00</published><updated>2013-12-16T21:21:56.392+01:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="gdk" /><category scheme="http://www.blogger.com/atom/ns#" term="glass" /><category scheme="http://www.blogger.com/atom/ns#" term="google glass" /><category scheme="http://www.blogger.com/atom/ns#" term="mirror api" /><title type="text">Mirror API and GDK - Developing for Google Glass #1</title><content type="html">I recently got my hands on&lt;a href="http://www.google.com/glass/start/"&gt; Google Glass&lt;/a&gt; and decided to write some articles about developing applications for Glass. After all it's Android that is running on Glass.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;What is Glass?&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;It's very complicated to explain Google Glass just using text. Only wearing and using it will give you this aha moment. However the following video, made by Google, gives you a good impression about how it feels like.&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;iframe allowfullscreen="" frameborder="0" height="315" src="//www.youtube.com/embed/v1uyQZNg2vE" width="560"&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;What is Glass from a developer's point of view?&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;Google Glass is an Android device running Android 4.0.3. What you see through Glass is basically a customized &lt;i&gt;Launcher&lt;/i&gt; / &lt;i&gt;Home screen&lt;/i&gt; application (a timeline of cards about current and past events) and a slightly different theme. This makes it really interesting for Android developers to develop for Glass: You can use almost all the familiar Android framework APIs. However wearing Glass feels totally different than using a mobile phone. So there's a big difference in designing applications. But not only the UI is different: You can't just port an existing application to Glass. Use cases have to be designed especially for Glass. Some features of your app might not make sense on Glass. Some other interesting features might only be possible on Glass. It's almost impossible to get a feeling for that without using Glass for some days.&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Back to writing code.. Currently we can decide between two ways to develop for Glass: The Mirror API or an early preview of the Glass Development Kit (GDK). Let's have a look at both and see what they are capable of.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style="font-size: large;"&gt;The Mirror API&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;The Mirror API has been the first API that has been introduced by the Glass team. It's a server-side API meaning the applications don't run on Glass itself but on your server and it's your server that interacts with Glass.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;The Mirror API is great for pushing cards to the timeline of Glass and sharing content from Glass with your server application.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Some examples of applications that could use the Mirror API:&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;&lt;b&gt;Twitter client&lt;/b&gt;: The server pushes interesting tweets to the timeline of the Glass owner. The user can share photos and messages with the application and they will be posted to the Twitter timeline.&lt;/li&gt;&lt;li&gt;&lt;b&gt;Context-aware notifications&lt;/b&gt;: Your server subscribes to the user's location. Every now and then your server will receive the latest user location. You use this location to post interesting and related cards to the timeline of the user.&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;b&gt;More about the Mirror API&lt;/b&gt;:&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;&lt;a href="https://developers.google.com/glass/develop/mirror/quickstart/"&gt;Google Developers: Mirror API Quick Start&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="http://www.youtube.com/watch?v=CxB1DuwGRqk"&gt;YouTube: Google I/O 2013 - Building Glass Services with the Google Mirror API&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;The Glass Development Kit (GDK)&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;With the GDK you can build Android applications that run directly on Glass. Think of the GDK as Android 4.0.3 SDK with some extra APIs for Google Glass. It's worth mentioning that the GDK is currently in an early preview state. The API is not complete and some important parts are missing.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;When developing Glass you have two options how your application should show up on Glass:&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;Live Cards&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/-wpH3qxEjT_Y/Uq9XLpCVIJI/AAAAAAAAR4A/my3NMMsA1SQ/s1600/glass_timeline_livecard.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://1.bp.blogspot.com/-wpH3qxEjT_Y/Uq9XLpCVIJI/AAAAAAAAR4A/my3NMMsA1SQ/s1600/glass_timeline_livecard.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;How a live card shows up in the Glass timeline.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;Your application shows up as a card in the timeline (left of the Glass clock). You have again two options how to render these cards:&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;&lt;b&gt;Low-Frequency Rendering&lt;/b&gt;: Your card is rendered using &lt;a href="https://developer.android.com/reference/android/widget/RemoteViews.html"&gt;Remote Views&lt;/a&gt;. Think of it as a Home screen widget on Android phones. A background service is responsible for updating these views. You only update the views every now and then.&lt;/li&gt;&lt;li&gt;&lt;b&gt;High Frequency Rendering&lt;/b&gt;: Your background service renders directly on the live card's surface. You can draw anything and are not limited to Android views. Furthermore you can update the card many times a second.&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;&lt;b&gt;Immersion&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/-9Wv_J7QV5AM/Uq9XdQLAvuI/AAAAAAAAR4I/AwZ0p8RzYPM/s1600/glass_timeline_immersion.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://3.bp.blogspot.com/-9Wv_J7QV5AM/Uq9XdQLAvuI/AAAAAAAAR4I/AwZ0p8RzYPM/s1600/glass_timeline_immersion.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;An Immersion is not part of the timeline but "replaces" it.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;An immersion is at the bottom a regular Android activity. For your activity to look like a timeline card:&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;Don't assign a theme to your activity or use the DeviceDefault theme as base for your customization.&lt;/li&gt;&lt;li&gt;Even though you can use the touch pad of Glass almost like a d-pad: Try to avoid most input-related Android widgets. They don't make much sense on Glass because you are not using a touch screen. Instead try to use gestures with the &lt;a href="https://developers.google.com/glass/develop/gdk/reference/com/google/android/glass/touchpad/GestureDetector"&gt;GestureDetector&lt;/a&gt;&amp;nbsp;class or &lt;a href="https://developers.google.com/glass/develop/gdk/input/voice"&gt;voice input&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Use the &lt;a href="https://developers.google.com/glass/develop/gdk/reference/com/google/android/glass/app/Card"&gt;Card class&lt;/a&gt; and its &lt;a href="https://developers.google.com/glass/develop/gdk/reference/com/google/android/glass/app/Card#toView()"&gt;toView()&lt;/a&gt; method to create a view that looks like regular Glass card.&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;b&gt;More about the GDK&lt;/b&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;&lt;a href="https://developers.google.com/glass/develop/gdk/quick-start"&gt;Google Developers: GDK Quick Start&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=oZSLKtpgQkc"&gt;YouTube: Glass Development Kit Sneak Peek&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/AndroidZeitgeist/~4/JXGHhmdRusw" height="1" width="1" alt=""/&gt;</content><link rel="replies" type="application/atom+xml" href="http://www.androidzeitgeist.com/feeds/3139284173480483099/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="http://www.androidzeitgeist.com/2013/12/mirror-api-gdk-developing-google-glass.html#comment-form" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/3139284173480483099" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/3139284173480483099" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/AndroidZeitgeist/~3/JXGHhmdRusw/mirror-api-gdk-developing-google-glass.html" title="Mirror API and GDK - Developing for Google Glass #1" /><author><name>Sebastian Kaspari</name><uri>https://plus.google.com/112283223674539938062</uri><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://1.bp.blogspot.com/-wpH3qxEjT_Y/Uq9XLpCVIJI/AAAAAAAAR4A/my3NMMsA1SQ/s72-c/glass_timeline_livecard.png" height="72" width="72" /><thr:total>0</thr:total><gd:extendedProperty name="commentSource" value="1" /><gd:extendedProperty name="commentModerationMode" value="FILTERED_POSTMOD" /><feedburner:origLink>http://www.androidzeitgeist.com/2013/12/mirror-api-gdk-developing-google-glass.html</feedburner:origLink></entry><entry><id>tag:blogger.com,1999:blog-1531829516427013333.post-3096175864289654512</id><published>2013-08-22T12:43:00.002+02:00</published><updated>2013-08-22T12:43:53.597+02:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Framework" /><category scheme="http://www.blogger.com/atom/ns#" term="Handler" /><category scheme="http://www.blogger.com/atom/ns#" term="Intent" /><category scheme="http://www.blogger.com/atom/ns#" term="IntentService" /><category scheme="http://www.blogger.com/atom/ns#" term="Looper" /><category scheme="http://www.blogger.com/atom/ns#" term="Service" /><title type="text">Read the code: IntentService</title><content type="html">&lt;span style="font-size: x-small;"&gt;In the new category &lt;b&gt;Read the code&lt;/b&gt; I’m going to show the internals of the Android framework. Reading the code of the framework can give you a good impression about what’s going on under the hood. In addition to that knowing how the framework developers solved common problems can help you to find the best solutions when facing problems in your own app code.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;What is the IntentService class good for?&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;This article is about the &lt;a href="https://developer.android.com/reference/android/app/IntentService.html"&gt;IntentService&lt;/a&gt; class of Android. Extending the IntentService class is the best solution for implementing a background service that is going to process something in a queue-like fashion. You can pass data via &lt;a href="https://developer.android.com/reference/android/content/Intent.html"&gt;Intents&lt;/a&gt; to the IntentService and it will take care of queuing and processing the Intents on a worker thread one at a time. When writing your IntentService implementation you are required to override the &lt;a href="https://developer.android.com/reference/android/app/IntentService.html#onHandleIntent(android.content.Intent)"&gt;onHandleIntent()&lt;/a&gt; method to process the data of the supplied Intents.&lt;br /&gt;&lt;br /&gt;Let’s take a look at a simple example: This DownloadService class receives Uris to download data from. It will download only one thing at a time with the other requests waiting in a queue.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-VJ13VmPcpx4/UhXgpEhsitI/AAAAAAAANSk/ZLzJYR-7ypE/s1600/DownloadService.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/-VJ13VmPcpx4/UhXgpEhsitI/AAAAAAAANSk/ZLzJYR-7ypE/s1600/DownloadService.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Android-Zeitgeist-Samples/blob/master/IntentService/DownloadService.java"&gt;DownloadService&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;span style="font-size: large;"&gt;The components&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;Before we dip into the source code of the IntentService class, let's first take a look at the different components that we need to know in order to understand the source code.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Handler&lt;/b&gt;&amp;nbsp;(&lt;a href="https://developer.android.com/reference/android/os/Handler.html"&gt;documentation&lt;/a&gt;) (&lt;a href="http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.3_r2.1/android/os/Handler.java"&gt;source code&lt;/a&gt;)&lt;br /&gt;You may already have used Handler objects. When a Handler is created on the UI thread, messages can be posted to it and these messages will be processed on the UI thread.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;ServiceHandler&lt;/b&gt;&amp;nbsp;(&lt;a href="http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.3_r2.1/android/app/IntentService.java#58"&gt;source code&lt;/a&gt;)&lt;br /&gt;The ServiceHandler inner-class is a helper class extending the Handler class to delegate the Intent wrapped inside a Message object to the IntentService for processing.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/--OVWzXz_dlQ/UhXZNL7nZII/AAAAAAAANR0/3pWwyp_Iw3g/s1600/ServiceHandler.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/--OVWzXz_dlQ/UhXZNL7nZII/AAAAAAAANR0/3pWwyp_Iw3g/s1600/ServiceHandler.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.3_r2.1/android/app/IntentService.java#58"&gt;ServiceHandler inner class of&amp;nbsp;android.app.IntentService&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;b&gt;Looper&lt;/b&gt;&amp;nbsp;(&lt;a href="https://developer.android.com/reference/android/os/Looper.html"&gt;documentation&lt;/a&gt;) (&lt;a href="http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.3_r2.1/android/os/Looper.java#Looper"&gt;source code&lt;/a&gt;)&lt;br /&gt;The Looper class has a MessageQueue object attached to it and blocks the current thread until a Message is received. This message will be passed to the assigned Handler. After that the Looper processes the next message in the queue or blocks again until a message is received.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;HandlerThread&lt;/b&gt;&amp;nbsp;(&lt;a href="https://developer.android.com/reference/android/os/HandlerThread.html"&gt;documentation&lt;/a&gt;) (&lt;a href="http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.3_r2.1/android/os/HandlerThread.java#HandlerThread"&gt;source code&lt;/a&gt;)&lt;br /&gt;A HandlerThread is a Thread implementation that does all the Looper setup for you. By creating and starting a HandlerThread instance you will have a running thread with a Looper attached to it waiting for messages to process.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;Read the code!&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;Now we know enough about all the components to understand the &lt;a href="http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.3_r2.1/android/app/IntentService.java"&gt;IntentService code&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;onCreate()&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://4.bp.blogspot.com/-YHAG_7OKKhI/UhXazQgkmkI/AAAAAAAANSA/57KqOScdP9k/s1600/IntentService_oncreate.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://4.bp.blogspot.com/-YHAG_7OKKhI/UhXazQgkmkI/AAAAAAAANSA/57KqOScdP9k/s1600/IntentService_oncreate.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.3_r2.1/android/app/IntentService.java#101"&gt;IntentService.onCreate()&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;At first a HandlerThread is created and started. We now have a background thread running that already has a Looper assigned. This Looper is waiting on the background thread for messages to process.&lt;br /&gt;&lt;br /&gt;Next a ServiceHandler is created for this Looper. The Handler’s &lt;a href="https://developer.android.com/reference/android/os/Handler.html#handleMessage(android.os.Message)"&gt;handleMessage&lt;/a&gt;() method will be called for every message received by the Looper. The ServiceHandler obtains the Intent object from the Message and passes it to the onHandleIntent() method of the IntentService.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;onStart()&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-C91WSSI62cw/UhXbfPQsAJI/AAAAAAAANSI/k5Hyb0QB9qI/s1600/IntentService_onstart.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/-C91WSSI62cw/UhXbfPQsAJI/AAAAAAAANSI/k5Hyb0QB9qI/s1600/IntentService_onstart.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.3_r2.1/android/app/IntentService.java#115"&gt;IntentService.onStart()&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;The onStart() method is called every time &lt;a href="https://developer.android.com/reference/android/content/Context.html#startService(android.content.Intent)"&gt;startService()&lt;/a&gt; is called. We wrap the Intent in a Message object and post it to the Handler. The Handler will enqueue it in the message queue of the Looper. The onStart() method is deprecated since API level 5 (Android 2.0). Instead onStartCommand() should be implemented.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;onStartCommand()&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/-HB0fPJbsY8A/UhXcBOrAYAI/AAAAAAAANSQ/pfU3UUXUmE8/s1600/IntentService_onstartcommand.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://3.bp.blogspot.com/-HB0fPJbsY8A/UhXcBOrAYAI/AAAAAAAANSQ/pfU3UUXUmE8/s1600/IntentService_onstartcommand.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.3_r2.1/android/app/IntentService.java#129"&gt;IntentService.onStartCommand()&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;In onStartCommand() we call onStart() to enqueue the Intent. We return &lt;a href="https://developer.android.com/reference/android/app/Service.html#START_REDELIVER_INTENT"&gt;START_REDELIVER_INTENT&lt;/a&gt; or &lt;a href="https://developer.android.com/reference/android/app/Service.html#START_NOT_STICKY"&gt;START_NOT_STICK&lt;/a&gt; depending on what the child class has set via&amp;nbsp;&lt;a href="https://developer.android.com/reference/android/app/IntentService.html#setIntentRedelivery(boolean)"&gt;setIntentRedelivery()&lt;/a&gt;. Depending on this setting an Intent will be redelivered to the service if the process dies before onHandleIntent() returns or the Intent will die as well.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;onDestroy()&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/-GkK4xWmOWu0/UhXcmqOLkQI/AAAAAAAANSY/M8oZn2GF5FY/s1600/IntentService_ondestroy.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://1.bp.blogspot.com/-GkK4xWmOWu0/UhXcmqOLkQI/AAAAAAAANSY/M8oZn2GF5FY/s1600/IntentService_ondestroy.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.3_r2.1/android/app/IntentService.java#134"&gt;IntentService.onDestroy()&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;In onDestroy() we just need to stop the Looper.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;Conclusion&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;The IntentService code is quite short and simple, yet a powerful pattern. With the &lt;a href="https://developer.android.com/reference/android/os/Handler.html"&gt;Handler&lt;/a&gt;, &lt;a href="https://developer.android.com/reference/android/os/Looper.html"&gt;Looper&lt;/a&gt; and &lt;a href="https://developer.android.com/reference/java/lang/Thread.html"&gt;Thread&lt;/a&gt; class you can easily build your own simple processing queues.&lt;br /&gt;&lt;br /&gt;Oh, and if you are looking for an exercise. The code of the onCreate() method contains a TODO comment that I omitted above:&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://4.bp.blogspot.com/-QlS7sJHfj1I/UhXp96c9r_I/AAAAAAAANS0/iRXyCb9E7NI/s1600/oncreate_todo.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://4.bp.blogspot.com/-QlS7sJHfj1I/UhXp96c9r_I/AAAAAAAANS0/iRXyCb9E7NI/s1600/oncreate_todo.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.3_r2.1/android/app/IntentService.java#101"&gt;TODO in onCreate()&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;img src="http://feeds.feedburner.com/~r/AndroidZeitgeist/~4/ohV1Ybma6A4" height="1" width="1" alt=""/&gt;</content><link rel="replies" type="application/atom+xml" href="http://www.androidzeitgeist.com/feeds/3096175864289654512/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="http://www.androidzeitgeist.com/2013/08/read-code-intentservice.html#comment-form" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/3096175864289654512" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/3096175864289654512" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/AndroidZeitgeist/~3/ohV1Ybma6A4/read-code-intentservice.html" title="Read the code: IntentService" /><author><name>Sebastian Kaspari</name><uri>https://plus.google.com/112283223674539938062</uri><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://2.bp.blogspot.com/-VJ13VmPcpx4/UhXgpEhsitI/AAAAAAAANSk/ZLzJYR-7ypE/s72-c/DownloadService.png" height="72" width="72" /><thr:total>0</thr:total><gd:extendedProperty name="commentSource" value="1" /><gd:extendedProperty name="commentModerationMode" value="FILTERED_POSTMOD" /><feedburner:origLink>http://www.androidzeitgeist.com/2013/08/read-code-intentservice.html</feedburner:origLink></entry><entry><id>tag:blogger.com,1999:blog-1531829516427013333.post-5050374432842781763</id><published>2013-05-27T19:31:00.001+02:00</published><updated>2013-05-27T19:31:31.801+02:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Instant Mustache" /><category scheme="http://www.blogger.com/atom/ns#" term="Intent" /><category scheme="http://www.blogger.com/atom/ns#" term="picture" /><category scheme="http://www.blogger.com/atom/ns#" term="Sharing" /><title type="text">Sharing the taken picture - Instant Mustache #9</title><content type="html">&lt;div&gt;&lt;span style="font-size: x-small;"&gt;This article is part of a series of articles about the development process of Instant Mustache, a fun camera app that adds mustaches to all faces using face detection.&amp;nbsp;&lt;a href="http://www.androidzeitgeist.com/p/instant-mustache.html"&gt;Click here&lt;/a&gt;&amp;nbsp;to get a chronological list of all published&amp;nbsp;&lt;a href="http://www.androidzeitgeist.com/p/instant-mustache.html"&gt;articles about Instant Mustache&lt;/a&gt;.&lt;/span&gt;&lt;/div&gt;&lt;br /&gt;&lt;a href="http://www.androidzeitgeist.com/p/instant-mustache.html"&gt;Up to now&lt;/a&gt; our app can take and view pictures. The next step is to share the taken picture with other Android apps. This is done via &lt;a href="https://developer.android.com/guide/components/intents-filters.html"&gt;Intents&lt;/a&gt;. The Intent system is one of the most powerful features of Android. It allows us to interact with any app that accepts images with almost no extra afford.&lt;br /&gt;&lt;br /&gt;We could create an &lt;a href="https://developer.android.com/guide/topics/ui/actionbar.html"&gt;ActionBar&lt;/a&gt; item and when clicked launch an &lt;a href="https://developer.android.com/reference/android/content/Intent.html"&gt;Intent&lt;/a&gt;&amp;nbsp;to share the image but instead we are going to use a &lt;a href="http://developer.android.com/training/sharing/shareaction.html"&gt;ShareActionProvider&lt;/a&gt;. The ShareActionProvider adds a share icon to the ActionBar as well as the icon of the app that the user has shared pictures the most with. By clicking this icon the user can share directly with this app. In addition to that the ShareActionProvider shows a sub menu with more apps that the given picture can be shared with.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/-pDiZ5iw7DMw/UaOT6GqMHuI/AAAAAAAALjk/oLbP5J6_nok/s1600/shareactionprovider.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://3.bp.blogspot.com/-pDiZ5iw7DMw/UaOT6GqMHuI/AAAAAAAALjk/oLbP5J6_nok/s1600/shareactionprovider.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;A ShareActionProvider with Google+ as default share action.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://4.bp.blogspot.com/-L_-glPQZGF8/UaOXqTIYImI/AAAAAAAALkE/ofFypChT2Ew/s1600/shareactionprovider_menu.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" height="285" src="http://4.bp.blogspot.com/-L_-glPQZGF8/UaOXqTIYImI/AAAAAAAALkE/ofFypChT2Ew/s320/shareactionprovider_menu.png" width="320" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;Sub menu of a ShareActionProvider.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;b&gt;If sharing is a key feature of your activity, you should consider using the ShareActionProvider.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;We start by creating an XML menu file for adding the share action. For legacy reasons the ActionBar uses the same approach for creating action items as the menu in Android 2.x.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/-KWMveO6nNuo/UaOTQWFlM1I/AAAAAAAALjc/OXqcUuxPmhg/s1600/activity_photo_menu.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://1.bp.blogspot.com/-KWMveO6nNuo/UaOTQWFlM1I/AAAAAAAALjc/OXqcUuxPmhg/s1600/activity_photo_menu.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-09/res/menu/activity_photo.xml"&gt;activity_photo.xml&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;Once we inflated the menu in &lt;a href="https://developer.android.com/reference/android/app/Activity.html#onCreateOptionsMenu(android.view.Menu)"&gt;onCreateOptionsMenu()&lt;/a&gt; we need to set the Intent used to share the photo.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-0eTGVnhBo_0/UaOVjb4aX-I/AAAAAAAALj0/mXc2KKjxi0k/s1600/initialize_share_action.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/-0eTGVnhBo_0/UaOVjb4aX-I/AAAAAAAALj0/mXc2KKjxi0k/s1600/initialize_share_action.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-09/src/com/androidzeitgeist/mustache/activity/PhotoActivity.java#L45"&gt;PhotoActivity.initializeShareAction()&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;Let's take a look at the different components of the Intent:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;&lt;b&gt;ACTION_SEND&lt;/b&gt;: The default action used for “sending†data to an other unspecified activity.&lt;/li&gt;&lt;li&gt;&lt;b&gt;MIME&lt;/b&gt; type: The MIME type of the data being sent. Other apps can define multiple MIME types they accept. We are sending a JPEG image and therefore we are using the MIME type “image/jpegâ€. To learn more about MIME types start with the &lt;a href="https://en.wikipedia.org/wiki/Internet_media_type"&gt;"Internet media type" Wikipedia article&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;b&gt;EXTRA_STREAM&lt;/b&gt;: &lt;a href="https://developer.android.com/reference/android/net/Uri.html"&gt;Uri&lt;/a&gt; that points to the data that should be sent. In our case the Uri is pointing to the image file on the external storage.&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;&lt;br /&gt;That's it already. For all changes done to the code base, see the &lt;a href="https://github.com/pocmo/Instant-Mustache/commit/4bded2002dfd56f63245d59e07a44e71deb04172"&gt;repository on GitHub&lt;/a&gt;. In the next article we'll polish some aspects of the app before we start implementing the Face detection feature.&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/AndroidZeitgeist/~4/APLuQCN5xxA" height="1" width="1" alt=""/&gt;</content><link rel="replies" type="application/atom+xml" href="http://www.androidzeitgeist.com/feeds/5050374432842781763/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="http://www.androidzeitgeist.com/2013/05/sharing-taken-picture-instant-mustache-9.html#comment-form" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/5050374432842781763" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/5050374432842781763" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/AndroidZeitgeist/~3/APLuQCN5xxA/sharing-taken-picture-instant-mustache-9.html" title="Sharing the taken picture - Instant Mustache #9" /><author><name>Sebastian Kaspari</name><uri>https://plus.google.com/112283223674539938062</uri><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://3.bp.blogspot.com/-pDiZ5iw7DMw/UaOT6GqMHuI/AAAAAAAALjk/oLbP5J6_nok/s72-c/shareactionprovider.png" height="72" width="72" /><thr:total>0</thr:total><gd:extendedProperty name="commentSource" value="1" /><gd:extendedProperty name="commentModerationMode" value="FILTERED_POSTMOD" /><feedburner:origLink>http://www.androidzeitgeist.com/2013/05/sharing-taken-picture-instant-mustache-9.html</feedburner:origLink></entry><entry><id>tag:blogger.com,1999:blog-1531829516427013333.post-8353658165969956441</id><published>2013-01-14T20:47:00.000+01:00</published><updated>2013-01-14T20:47:04.700+01:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="camera" /><category scheme="http://www.blogger.com/atom/ns#" term="Instant Mustache" /><category scheme="http://www.blogger.com/atom/ns#" term="orientation" /><category scheme="http://www.blogger.com/atom/ns#" term="OrientationEventListener" /><category scheme="http://www.blogger.com/atom/ns#" term="rotation" /><title type="text">Fixing the rotation - Instant Mustache #8</title><content type="html">&lt;div&gt;&lt;span style="font-size: x-small;"&gt;This article is part of a series of articles about the development process of Instant Mustache, a fun camera app that adds mustaches to all faces using face detection.&amp;nbsp;&lt;a href="http://www.androidzeitgeist.com/p/instant-mustache.html"&gt;Click here&lt;/a&gt;&amp;nbsp;to get a chronological list of all published&amp;nbsp;&lt;a href="http://www.androidzeitgeist.com/p/instant-mustache.html"&gt;articles about Instant Mustache&lt;/a&gt;.&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;br /&gt;&lt;b&gt;&lt;span style="font-size: large;"&gt;Wrong orientation&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;If you run the current version of Instant Mustache and take some pictures you'll notice something odd: The orientation of the taken pictures is sometimes wrong. This may depend on the device you are using. When using a Galaxy Nexus the picture will be rotated 90° to the left when taking a picture in portrait mode but will be rotated correctly when taking a picture in landscape mode.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/-RDkIH8Iw8VY/UPRZTnyZA2I/AAAAAAAAH0Q/BU7sRAlQQOw/s1600/rotation_error.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" height="239" src="http://1.bp.blogspot.com/-RDkIH8Iw8VY/UPRZTnyZA2I/AAAAAAAAH0Q/BU7sRAlQQOw/s320/rotation_error.png" width="320" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;Wrong orientation of photo that has been taken in portrait mode&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;How does this happen? You may remember that we've used &lt;a href="https://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int)"&gt;Camera.setDisplayOrientation()&lt;/a&gt; in one of the &lt;a href="http://www.androidzeitgeist.com/2012/10/displaying-camera-preview-instant.html"&gt;previous articles&lt;/a&gt; to explicitly set the display rotation. First, this setting only affects the preview picture. The picture passed to the &lt;a href="https://developer.android.com/reference/android/hardware/Camera.ShutterCallback.html"&gt;Camera.ShutterCallback&lt;/a&gt; isn't affected by this setting. And second, we still have to account into how the device is rotated in the moment of taking the picture.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;span style="font-size: large;"&gt;Detecting and remembering the orientation&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;What we need to do in our code is to register an &lt;a href="https://developer.android.com/reference/android/view/OrientationEventListener.html"&gt;OrientationEventListener&lt;/a&gt; to get notified whenever the orientation changes. We'll remember this orientation and use this to rotate the taken image once the callback returns.&lt;br /&gt;&lt;br /&gt;Whenever the orientation changes &lt;a href="https://developer.android.com/reference/android/view/OrientationEventListener.html#onOrientationChanged(int)"&gt;onOrientationChanged(int)&lt;/a&gt; of the listener will be called. The orientation will be passed to the method in degrees, ranging from 0 to 359. We need to normalize this value as we are only interested in 90° steps for rotating the picture.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-JQ2Rm-uZ0gQ/UPRahx8kf2I/AAAAAAAAH0c/v7NIwzqReBM/s1600/cameraorientationlistener_normalize.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/-JQ2Rm-uZ0gQ/UPRahx8kf2I/AAAAAAAAH0c/v7NIwzqReBM/s1600/cameraorientationlistener_normalize.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-08/src/com/androidzeitgeist/mustache/listener/CameraOrientationListener.java#L31"&gt;CameraOrientationListener.normalize()&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;Another method called &lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-08/src/com/androidzeitgeist/mustache/listener/CameraOrientationListener.java#L51"&gt;rememberOrientation()&lt;/a&gt; will be used to save the orientation of the device in the moment of the user pressing the shutter button.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/-rqdv0tzl52Y/UPRbbrb1vBI/AAAAAAAAH00/2Ldj2OrPXBM/s1600/camerafragment_takepicture.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://1.bp.blogspot.com/-rqdv0tzl52Y/UPRbbrb1vBI/AAAAAAAAH00/2Ldj2OrPXBM/s1600/camerafragment_takepicture.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-08/src/com/androidzeitgeist/mustache/fragment/CameraFragment.java#L231"&gt;CameraFragment.takePicture()&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;Rotating the picture&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;Now we just need to rotate the Bitmap. We do this by creating a new Bitmap object and applying a rotated &lt;a href="http://developer.android.com/reference/android/graphics/Matrix.html"&gt;Matrix&lt;/a&gt; to the pixels. The rotation angle is calculated by summing the remembered orientation, the display orientation and the natural rotation of the device.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-Kp9GQ7CSHik/UPRcKSjRiBI/AAAAAAAAH1I/cTRdA_NYgjI/s1600/camerafragment_onpicturetaken.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/-Kp9GQ7CSHik/UPRcKSjRiBI/AAAAAAAAH1I/cTRdA_NYgjI/s1600/camerafragment_onpicturetaken.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-08/src/com/androidzeitgeist/mustache/fragment/CameraFragment.java#L241"&gt;CameraFragment.onPictureTaken()&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;Result&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/-3lBP-pDo5cQ/UPRd8jZp-LI/AAAAAAAAH14/kJaLh3-Mdak/s1600/result_rotation.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" height="242" src="http://3.bp.blogspot.com/-3lBP-pDo5cQ/UPRd8jZp-LI/AAAAAAAAH14/kJaLh3-Mdak/s320/result_rotation.png" width="320" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;Photos rotated correctly in portrait and landscape mode&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;img src="http://feeds.feedburner.com/~r/AndroidZeitgeist/~4/17oriJk6I8w" height="1" width="1" alt=""/&gt;</content><link rel="replies" type="application/atom+xml" href="http://www.androidzeitgeist.com/feeds/8353658165969956441/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="http://www.androidzeitgeist.com/2013/01/fixing-rotation-camera-picture.html#comment-form" title="1 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/8353658165969956441" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/8353658165969956441" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/AndroidZeitgeist/~3/17oriJk6I8w/fixing-rotation-camera-picture.html" title="Fixing the rotation - Instant Mustache #8" /><author><name>Sebastian Kaspari</name><uri>https://plus.google.com/112283223674539938062</uri><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://1.bp.blogspot.com/-RDkIH8Iw8VY/UPRZTnyZA2I/AAAAAAAAH0Q/BU7sRAlQQOw/s72-c/rotation_error.png" height="72" width="72" /><thr:total>1</thr:total><gd:extendedProperty name="commentSource" value="1" /><gd:extendedProperty name="commentModerationMode" value="FILTERED_POSTMOD" /><feedburner:origLink>http://www.androidzeitgeist.com/2013/01/fixing-rotation-camera-picture.html</feedburner:origLink></entry><entry><id>tag:blogger.com,1999:blog-1531829516427013333.post-1598031232613876637</id><published>2012-12-22T14:21:00.000+01:00</published><updated>2012-12-22T14:27:19.476+01:00</updated><title type="text">Android 2012</title><content type="html">&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-dKfHvBySVQk/UNWsnfVSK6I/AAAAAAAAHR8/FYWEUfw0yeU/s1600/android2012.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/-dKfHvBySVQk/UNWsnfVSK6I/AAAAAAAAHR8/FYWEUfw0yeU/s1600/android2012.png" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;After &lt;a href="http://www.youtube.com/watch?v=xY_MUB8adEQ"&gt;Google Zeitgeist&lt;/a&gt; and&amp;nbsp;&lt;a href="http://www.youtube.com/watch?v=iCkYw3cRwLo"&gt;YouTube&lt;/a&gt;&amp;nbsp;looked back on 2012 it’s time to do the same for Android.&lt;span id="goog_2128203782"&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;January&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;a href="http://4.bp.blogspot.com/-4cV7er0VgPI/UNWw0GXYL5I/AAAAAAAAHSQ/nDFaspNqWns/s1600/androiddesign.png" imageanchor="1" style="clear: right; float: right; margin-bottom: 1em; margin-left: 1em;"&gt;&lt;img border="0" height="157" src="http://4.bp.blogspot.com/-4cV7er0VgPI/UNWw0GXYL5I/AAAAAAAAHSQ/nDFaspNqWns/s200/androiddesign.png" width="200" /&gt;&lt;/a&gt;The year 2011 has just ended and the &lt;b&gt;Galaxy Nexus&lt;/b&gt; is the current flagship phone. 12 days later on January 12th the &lt;b&gt;&lt;a href="http://developer.android.com/design/index.html"&gt;Android Design&lt;/a&gt;&lt;/b&gt; website launched. Followed by the &lt;b&gt;&lt;a href="https://plus.google.com/u/0/+AndroidDevelopers/posts"&gt;Android developers Google+ page&lt;/a&gt;&lt;/b&gt; on January 30th.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;February&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;The first numbers of the year are published. &lt;b&gt;850,000&lt;/b&gt; Android phones are activated every day. &lt;b&gt;300 million &lt;/b&gt;devices have been activated so far.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;March&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;On March 5th &lt;b&gt;&lt;a href="http://android-developers.blogspot.de/2012/03/android-apps-break-50mb-barrier.html"&gt;expansion files&lt;/a&gt;&lt;/b&gt; are introduced and &lt;b&gt;Android apps break the 50MB barrier&lt;/b&gt; expanding the size limit to &lt;b&gt;4GB&lt;/b&gt;. The Android market retires and is reborn as &lt;b&gt;Google play&lt;/b&gt; on March 6th. The same month on March 21st the &lt;b&gt;SDK tools and ADT revision 17&lt;/b&gt; are released, adding an emulator that supports running x86 system images on Windows and Mac OS X. An update to the Android Developer Console on March 29th allows &lt;b&gt;multiple users&lt;/b&gt; to manage published Android apps.&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/-qPqhAtg_PVg/UNWxPkos_2I/AAAAAAAAHSY/ivgQD4D4Nmo/s1600/appclinic.jpg" imageanchor="1" style="clear: right; float: right; margin-bottom: 1em; margin-left: 1em;"&gt;&lt;img border="0" height="150" src="http://1.bp.blogspot.com/-qPqhAtg_PVg/UNWxPkos_2I/AAAAAAAAHSY/ivgQD4D4Nmo/s200/appclinic.jpg" width="200" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;April&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;The emulator gets even more faster on April 9th by adding &lt;b&gt;GPU support&lt;/b&gt;. On April 20th the first episode of &lt;b&gt;&lt;a href="http://www.youtube.com/playlist?list=PLB7B9B23D864A55C3"&gt;Friday App Review&lt;/a&gt;&lt;/b&gt; airs and is later called &lt;b&gt;&lt;a href="http://www.youtube.com/playlist?list=PLB7B9B23D864A55C3"&gt;The app clinic&lt;/a&gt;&lt;/b&gt;.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;span style="font-size: large;"&gt;May&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;On May 4th Wolfram Rittmeyer publishes the first posting on his blog &lt;b&gt;&lt;a href="http://www.grokkingandroid.com/"&gt;Grokking Android&lt;/a&gt;&lt;/b&gt;. Followed by the first article published on &lt;a href="http://www.androidzeitgeist.com/"&gt;&lt;b&gt;Android Zeitgeist&lt;/b&gt;&lt;/a&gt; on May 27th. 3 days before on May 24th &lt;b&gt;&lt;a href="http://android-developers.blogspot.de/2012/05/in-app-subscriptions-in-google-play.html"&gt;In-app Subscriptions&lt;/a&gt;&lt;/b&gt; are launched on Google Play.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;June&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;a href="http://1.bp.blogspot.com/-HvAwcdnUaq4/UNWxlZCN7XI/AAAAAAAAHSg/jM3slhIq--k/s1600/google-io-logo.png" imageanchor="1" style="clear: right; float: right; margin-bottom: 1em; margin-left: 1em;"&gt;&lt;img border="0" height="40" src="http://1.bp.blogspot.com/-HvAwcdnUaq4/UNWxlZCN7XI/AAAAAAAAHSg/jM3slhIq--k/s200/google-io-logo.png" width="200" /&gt;&lt;/a&gt;&lt;br /&gt;The &lt;b&gt;&lt;a href="https://developers.google.com/events/io/"&gt;Google I/O&lt;/a&gt;&lt;/b&gt; takes place for three days from June 27th to 29th. There are now &lt;b&gt;900,000&lt;/b&gt; Android devices activated every day and &lt;b&gt;400 million&lt;/b&gt; devices have been activated up to now. &lt;b&gt;Android 4.1 (Jelly Bean)&lt;/b&gt; is publicly shown for the first time on June 27th. The same day the &lt;b&gt;Android 4.1 SDK&lt;/b&gt; is released. In addition to that the first tablet by Google is unveiled: The &lt;b&gt;Nexus 7&lt;/b&gt;. On the second day of the Google I/O the &lt;b&gt;Android SDK tools&lt;/b&gt; are updated to &lt;b&gt;revision 20&lt;/b&gt;. At the end of the Google I/O there have been 3.5 million live streams seen from 170 countries.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;July&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/-iu-C9xVT-_U/UNWyPKppPJI/AAAAAAAAHS4/JfRWJvOp7dg/s1600/ouya.png" imageanchor="1" style="clear: right; float: right; margin-bottom: 1em; margin-left: 1em;"&gt;&lt;img border="0" height="200" src="http://3.bp.blogspot.com/-iu-C9xVT-_U/UNWyPKppPJI/AAAAAAAAHS4/JfRWJvOp7dg/s200/ouya.png" width="200" /&gt;&lt;/a&gt;&lt;/div&gt;On July 3rd the &lt;b&gt;&lt;a href="http://www.ouya.tv/"&gt;Ouya&lt;/a&gt;&lt;/b&gt;, an Android based console, is unveiled and a Kickstarter campaign is started on July 10th. On July 9th the &lt;b&gt;Android 4.1 source code&lt;/b&gt; is published as part of the Android Open Source Project (AOSP).&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;August&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;The funding phase for the &lt;b&gt;Ouya&lt;/b&gt; is completed. The campaign collected $8,596,475. That’s 904% more than the initial campaign goal.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;September&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;New numbers are released. There are now &lt;b&gt;1.3 million devices&lt;/b&gt; activated every a day. About &lt;b&gt;70,000&lt;/b&gt; of these devices are tablets. &lt;b&gt;480 million&lt;/b&gt; devices have been activated up to now. On September 9th the first episode of &lt;b&gt;&lt;a href="http://www.youtube.com/playlist?list=PLWz5rJ2EKKc9Wam5jE-9oY8l6RpeAx-XM"&gt;This week in Android development&lt;/a&gt;&lt;/b&gt; airs. A day later the first episode of &lt;b&gt;&lt;a href="http://www.youtube.com/playlist?list=PLWz5rJ2EKKc8j2B95zGMb8muZvrIy-wcF"&gt;Android Design in Action&lt;/a&gt;&lt;/b&gt; is uploaded to YouTube.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;October&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;a href="http://3.bp.blogspot.com/-iMCBRXK31yE/UNWx_vXgIjI/AAAAAAAAHSw/9PXrh3tQBs4/s1600/nexus4.png" imageanchor="1" style="clear: right; float: right; margin-bottom: 1em; margin-left: 1em;"&gt;&lt;img border="0" height="167" src="http://3.bp.blogspot.com/-iMCBRXK31yE/UNWx_vXgIjI/AAAAAAAAHSw/9PXrh3tQBs4/s200/nexus4.png" width="200" /&gt;&lt;/a&gt;Till mid October &lt;b&gt;3 million Nexus 7&lt;/b&gt; units have been sold. Starting from October 15th the &lt;b&gt;new Google Play Developer Console&lt;/b&gt; is available to everyone. Google planned a &lt;b&gt;launch event&lt;/b&gt; on October 29th in New York but it has been cancelled due to Hurricane Sandy. Nevertheless the &lt;b&gt;Nexus 4&lt;/b&gt; and &lt;b&gt;Nexus 10&lt;/b&gt; are introduced online this day. These are the first devices to run &lt;b&gt;Android 4.2&lt;/b&gt;.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;November&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;The first episode of &lt;a href="http://www.youtube.com/playlist?list=PLWz5rJ2EKKc9loen4OjS03gdjI0JhF4cW"&gt;(╯°□°)╯︵ â”»â”â”»&lt;/a&gt; airs on November 8th. On November 13th the &lt;b&gt;Nexus 4&lt;/b&gt; and &lt;b&gt;Nexus 10&lt;/b&gt; went on sale and are sold out in minutes. Later that day the &lt;b&gt;Android 4.2 SDK platform&lt;/b&gt; is released. Another day later the &lt;b&gt;Android SDK tools revision 21&lt;/b&gt; are released.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;December&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;Google releases a new &lt;b&gt;&lt;a href="http://android-developers.blogspot.de/2012/12/new-google-maps-android-api-now-part-of.html"&gt;Google Maps API&lt;/a&gt;&lt;/b&gt; for Android on December 3rd. On December 10th a new version of the&lt;b&gt; &lt;a href="http://android-developers.blogspot.de/2012/12/in-app-billing-version-3.html"&gt;In-App billing API&lt;/a&gt;&lt;/b&gt; is released.&lt;br /&gt;&lt;br /&gt;The Android team releases their &lt;b&gt;Happy Holidays&lt;/b&gt; video:&lt;br /&gt;&lt;br /&gt;&lt;iframe allowfullscreen="allowfullscreen" frameborder="0" height="315" src="http://www.youtube.com/embed/967nio2LF7s" width="560"&gt;&lt;/iframe&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;2013?&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;What has been your Android highlight in 2012 and what are your wishes for 2013?&lt;/b&gt;&lt;img src="http://feeds.feedburner.com/~r/AndroidZeitgeist/~4/6_wXwDjJRdc" height="1" width="1" alt=""/&gt;</content><link rel="replies" type="application/atom+xml" href="http://www.androidzeitgeist.com/feeds/1598031232613876637/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="http://www.androidzeitgeist.com/2012/12/android-2012.html#comment-form" title="1 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/1598031232613876637" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/1598031232613876637" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/AndroidZeitgeist/~3/6_wXwDjJRdc/android-2012.html" title="Android 2012" /><author><name>Sebastian Kaspari</name><uri>https://plus.google.com/112283223674539938062</uri><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://2.bp.blogspot.com/-dKfHvBySVQk/UNWsnfVSK6I/AAAAAAAAHR8/FYWEUfw0yeU/s72-c/android2012.png" height="72" width="72" /><thr:total>1</thr:total><gd:extendedProperty name="commentSource" value="1" /><gd:extendedProperty name="commentModerationMode" value="FILTERED_POSTMOD" /><feedburner:origLink>http://www.androidzeitgeist.com/2012/12/android-2012.html</feedburner:origLink></entry><entry><id>tag:blogger.com,1999:blog-1531829516427013333.post-8734004828090755204</id><published>2012-12-11T20:41:00.000+01:00</published><updated>2012-12-11T20:41:51.969+01:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="API" /><category scheme="http://www.blogger.com/atom/ns#" term="Clean Code" /><category scheme="http://www.blogger.com/atom/ns#" term="String" /><category scheme="http://www.blogger.com/atom/ns#" term="TextUtils" /><title type="text">Mind the gap: String.isEmpty()</title><content type="html">&lt;span style="font-size: x-small;"&gt;Articles labeled "Mind the gap" are short articles mostly about simple problems that arise from using different API levels of Android. They are more short trivia postings than big teachings about Android development.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;I try to write code as readable as possible. That's the reason why I don't want to compare a String to an other empty String object or check its length when I want to know if a String is empty.&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/-FWLirpvHLAM/UMbfKHp3tUI/AAAAAAAAHCI/NPnjM4Qx5Pk/s1600/isemptyvariants.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" src="http://3.bp.blogspot.com/-FWLirpvHLAM/UMbfKHp3tUI/AAAAAAAAHCI/NPnjM4Qx5Pk/s1600/isemptyvariants.png" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;/div&gt;In the book "&lt;a href="http://books.google.de/books?id=dwSfGQAACAAJ&amp;amp;dq=isbn:0132350882y"&gt;Clean code&lt;/a&gt;" by &lt;a href="http://en.wikipedia.org/wiki/Robert_Cecil_Martin"&gt;Robert C. Martin&lt;/a&gt; you can read&amp;nbsp;&lt;a href="http://en.wikipedia.org/wiki/Grady_Booch"&gt;Grady Booch&lt;/a&gt; saying: "Clean code reads like well-written prose". So I try to use &lt;a href="http://developer.android.com/reference/java/lang/String.html#isEmpty()"&gt;String.isEmpty()&lt;/a&gt; for that reason. Internally it may do a length check as well (I stopped my investigation at the &lt;i&gt;native&lt;/i&gt; keyword)&amp;nbsp;but when reading the following snippet it is absolutely obvious what I intend to do.&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/-fPosAzNzRYc/UMbecA_7d3I/AAAAAAAAHB4/K6lPAMAkrIs/s1600/isEmptyMethod.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" src="http://1.bp.blogspot.com/-fPosAzNzRYc/UMbecA_7d3I/AAAAAAAAHB4/K6lPAMAkrIs/s1600/isEmptyMethod.png" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;Even though &lt;a href="http://developer.android.com/reference/java/lang/String.html#isEmpty()"&gt;isEmpty()&lt;/a&gt; has been introduced in Java 1.6 it hasn't been available in Android until API level 9 (Android 2.3) so I accidentally caused some crashes on earlier versions of Android. Nowadays &lt;a href="http://tools.android.com/tips/lint"&gt;lint&lt;/a&gt;&amp;nbsp;thankfully&amp;nbsp;saves me from doing this error.&lt;br /&gt;&lt;br /&gt;So what to do now? I don't know if someone at Google felt the same but there is a class in the Android framework that solves that problem: &lt;a href="http://developer.android.com/reference/android/text/TextUtils.html"&gt;TextUtils&lt;/a&gt;. This class also has a lot of other helpful methods like &lt;a href="http://developer.android.com/reference/android/text/TextUtils.html#join(java.lang.CharSequence, java.lang.Object[])"&gt;join()&lt;/a&gt; to join an array of elements to a String using a&amp;nbsp;delimiter&amp;nbsp;or &lt;a href="http://developer.android.com/reference/android/text/TextUtils.html#getReverse(java.lang.CharSequence, int, int)"&gt;getReverse() &lt;/a&gt;to reverse a String (Take that interview question!).&lt;br /&gt;&lt;br /&gt;In addition to that the TextUtils class has a method &lt;a href="http://developer.android.com/reference/android/text/TextUtils.html#isEmpty(java.lang.CharSequence)"&gt;isEmpty()&lt;/a&gt; that is available since API level 1.&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/-nzyWV2TAHaM/UMbelfdSHCI/AAAAAAAAHCA/Z2lLIH_YddU/s1600/TextUtils.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" src="http://3.bp.blogspot.com/-nzyWV2TAHaM/UMbelfdSHCI/AAAAAAAAHCA/Z2lLIH_YddU/s1600/TextUtils.png" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;Phew! By the way: &lt;a href="http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.1.1_r1/android/text/TextUtils.java#TextUtils.isEmpty%28java.lang.CharSequence%29"&gt;Internally&lt;/a&gt; isEmpty() checks if the length of the String is 0 (and does a null check).&lt;img src="http://feeds.feedburner.com/~r/AndroidZeitgeist/~4/jbePXNFlMnA" height="1" width="1" alt=""/&gt;</content><link rel="replies" type="application/atom+xml" href="http://www.androidzeitgeist.com/feeds/8734004828090755204/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="http://www.androidzeitgeist.com/2012/12/string-is-empty.html#comment-form" title="5 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/8734004828090755204" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/8734004828090755204" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/AndroidZeitgeist/~3/jbePXNFlMnA/string-is-empty.html" title="Mind the gap: String.isEmpty()" /><author><name>Sebastian Kaspari</name><uri>https://plus.google.com/112283223674539938062</uri><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://3.bp.blogspot.com/-FWLirpvHLAM/UMbfKHp3tUI/AAAAAAAAHCI/NPnjM4Qx5Pk/s72-c/isemptyvariants.png" height="72" width="72" /><thr:total>5</thr:total><gd:extendedProperty name="commentSource" value="1" /><gd:extendedProperty name="commentModerationMode" value="FILTERED_POSTMOD" /><feedburner:origLink>http://www.androidzeitgeist.com/2012/12/string-is-empty.html</feedburner:origLink></entry><entry><id>tag:blogger.com,1999:blog-1531829516427013333.post-4510260145421419236</id><published>2012-11-05T20:40:00.000+01:00</published><updated>2012-11-05T20:40:10.181+01:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="bezel swipe" /><category scheme="http://www.blogger.com/atom/ns#" term="fake dragging" /><category scheme="http://www.blogger.com/atom/ns#" term="scrolling" /><category scheme="http://www.blogger.com/atom/ns#" term="SeekBar" /><category scheme="http://www.blogger.com/atom/ns#" term="support library" /><category scheme="http://www.blogger.com/atom/ns#" term="ViewPager" /><title type="text">Examining the ViewPager #3</title><content type="html">&lt;span style="font-size: x-small;"&gt;This article is part of a series of articles about the ViewPager component.&amp;nbsp;&lt;/span&gt;&lt;a href="http://www.androidzeitgeist.com/p/viewpager.html" style="font-size: small;"&gt;Click here&lt;/a&gt;&lt;span style="font-size: x-small;"&gt;&amp;nbsp;to see a list of&amp;nbsp;&lt;/span&gt;&lt;a href="http://www.androidzeitgeist.com/p/viewpager.html" style="font-size: small;"&gt;all articles of this series&lt;/a&gt;&lt;span style="font-size: x-small;"&gt;.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;span style="font-size: large;"&gt;Horizontal scrolling pages&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;Have you ever tried putting horizontal scrolling components inside a &lt;a href="http://developer.android.com/reference/android/support/v4/view/ViewPager.html"&gt;ViewPager&lt;/a&gt;? Well, since revision 9 of the &lt;a href="http://developer.android.com/tools/extras/support-library.html"&gt;support library&lt;/a&gt; this is supported by the ViewPager. As long as the inner component can scroll horizontally this component will be scrolled. Whenever the component can't be further scrolled the ViewPager will handle the touch events and you start to switch to the next page. This works out-of-the-box for scrolling view components of Android like the &lt;a href="http://developer.android.com/reference/android/webkit/WebView.html"&gt;WebView&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;Internally the ViewPager uses &lt;a href="http://developer.android.com/reference/android/support/v4/view/ViewCompat.html#canScrollHorizontally(android.view.View, int)"&gt;ViewCompat.canScrollHorizontally(View v, int direction)&lt;/a&gt;&amp;nbsp;to determine if a child view can be scrolled horizontally and should receive the according touch events. Unfortunately this method is only implemented for Android 4.0 (API level 14) and above. For all earlier versions this method will always return false and therefore never scroll the components inside the ViewPager.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;span style="font-size: large;"&gt;Bezel swipe&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;Allowing horizontal scrolling components introduces a new problem: What if you want to switch pages but not scroll every component to its horizontal end? When you start the swipe at the phone's bezel (or actually from the edge of the ViewPager) you'll switch pages instead of scrolling the page's content. This gesture is called &lt;i&gt;bezel swipe&lt;/i&gt;.&lt;br /&gt;&lt;br /&gt;For reading more about the bezel swipe gesture from a UI point of view read "&lt;a href="http://www.androiduipatterns.com/2012/02/bezel-swipe-solution-to-pan-and-swipe.html"&gt;Bezel swipe, a Solution to Pan and Swipe Confusion?&lt;/a&gt;" on &lt;a href="http://www.androiduipatterns.com/"&gt;Android UI Patterns&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;The good news is again: The ViewPager supports bezel swipe out of the box. But as you can't horizontally scroll inside pages on devices running an Android version lower than 4.0 (API level 14) bezel swipe isn't of any use on these as well.&lt;br /&gt;&lt;br /&gt;The area to start a bezel swipe has a width of either 16dp or a 10th of the total width of the ViewPager depending on which one is smaller.&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/-dNYV1OuZvdQ/UJgLXTptXHI/AAAAAAAAGS4/Zt0OXlBwbaM/s1600/viewpager_bezel_swipe.png" imageanchor="1"&gt;&lt;img border="0" src="http://1.bp.blogspot.com/-dNYV1OuZvdQ/UJgLXTptXHI/AAAAAAAAGS4/Zt0OXlBwbaM/s1600/viewpager_bezel_swipe.png" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;Fake dragging&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;The ViewPager supports fake dragging. Fake dragging can be used to simulate a dragging event/animation, e.g. for detecting drag events on a different component and delegating these to the ViewPager.&lt;br /&gt;&lt;br /&gt;You have to signal the ViewPager when to start or end a fake drag by calling &lt;a href="http://developer.android.com/reference/android/support/v4/view/ViewPager.html#beginFakeDrag()"&gt;beginFakeDrag()&lt;/a&gt; and &lt;a href="http://developer.android.com/reference/android/support/v4/view/ViewPager.html#endFakeDrag()"&gt;endFakeDrag()&lt;/a&gt; on it. After starting a fake drag you can use &lt;a href="http://developer.android.com/reference/android/support/v4/view/ViewPager.html#fakeDragBy(float)"&gt;fakeDragBy(float)&lt;/a&gt; to drag the ViewPager by the given amount of pixels along the x axis (negative values to the left and positive values to the right).&lt;br /&gt;&lt;br /&gt;The following example uses a &lt;a href="http://developer.android.com/reference/android/widget/SeekBar.html"&gt;SeekBar&lt;/a&gt;&amp;nbsp;whose current progress state is used to fake drag a ViewPager by the given percentage.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://4.bp.blogspot.com/-SFr1EyEfN4Q/UJgNTawCuSI/AAAAAAAAGTA/Pq_75XpuIes/s1600/SeekBarPagerListener.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://4.bp.blogspot.com/-SFr1EyEfN4Q/UJgNTawCuSI/AAAAAAAAGTA/Pq_75XpuIes/s1600/SeekBarPagerListener.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Android-Zeitgeist-Samples/blob/master/ViewPager/03/SeekBarPagerListener.java"&gt;SeekBarPagerListener.java&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;This video shows the fake drag in action:&lt;br /&gt;&lt;br /&gt;&lt;iframe allowfullscreen="allowfullscreen" frameborder="0" height="360" src="http://www.youtube.com/embed/us8w2g9YXC4?rel=0" width="480"&gt;&lt;/iframe&gt;&lt;img src="http://feeds.feedburner.com/~r/AndroidZeitgeist/~4/GsNXoeK7N6Y" height="1" width="1" alt=""/&gt;</content><link rel="replies" type="application/atom+xml" href="http://www.androidzeitgeist.com/feeds/4510260145421419236/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="http://www.androidzeitgeist.com/2012/11/examining-viewpager-3.html#comment-form" title="1 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/4510260145421419236" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/4510260145421419236" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/AndroidZeitgeist/~3/GsNXoeK7N6Y/examining-viewpager-3.html" title="Examining the ViewPager #3" /><author><name>Sebastian Kaspari</name><uri>https://plus.google.com/112283223674539938062</uri><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://1.bp.blogspot.com/-dNYV1OuZvdQ/UJgLXTptXHI/AAAAAAAAGS4/Zt0OXlBwbaM/s72-c/viewpager_bezel_swipe.png" height="72" width="72" /><thr:total>1</thr:total><gd:extendedProperty name="commentSource" value="1" /><gd:extendedProperty name="commentModerationMode" value="FILTERED_POSTMOD" /><feedburner:origLink>http://www.androidzeitgeist.com/2012/11/examining-viewpager-3.html</feedburner:origLink></entry><entry><id>tag:blogger.com,1999:blog-1531829516427013333.post-5361828375857120954</id><published>2012-10-30T23:35:00.000+01:00</published><updated>2012-10-30T23:35:59.679+01:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Instant Mustache" /><category scheme="http://www.blogger.com/atom/ns#" term="Intent" /><category scheme="http://www.blogger.com/atom/ns#" term="Uri" /><title type="text">Displaying the taken picture – Instant Mustache #7</title><content type="html">&lt;div&gt;&lt;span style="font-size: x-small;"&gt;This article is part of a series of articles about the development process of Instant Mustache, a fun camera app that adds mustaches to all faces using face detection. &lt;a href="http://www.androidzeitgeist.com/p/instant-mustache.html"&gt;Click here&lt;/a&gt; to get a chronological list of all published &lt;a href="http://www.androidzeitgeist.com/p/instant-mustache.html"&gt;articles about Instant Mustache&lt;/a&gt;.&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style="font-size: x-small;"&gt;&lt;br /&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;Writing the PhotoActivity&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;In the &lt;a href="http://www.androidzeitgeist.com/2012/10/taking-picture-instant-mustache-6.html"&gt;last article&lt;/a&gt; we wrote the code to take a camera picture and save it on the external storage. After saving the file the activity will be finished and a Toast will show up. This is not really user-friendly so now we'll write our next activity which will display the taken picture and later offer the option to share this picture.&lt;br /&gt;&lt;br /&gt;We'll start by creating an empty activity called &lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-07/src/com/androidzeitgeist/mustache/activity/PhotoActivity.java"&gt;PhotoActivity&lt;/a&gt; and add it to the &lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-07/AndroidManifest.xml#L30"&gt;manifest&lt;/a&gt; of our application. For now the layout will only contain an &lt;a href="http://developer.android.com/reference/android/widget/ImageView.html"&gt;ImageView&lt;/a&gt; to display the picture:&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/-KnA5sDjgZds/UJBOSqCkiZI/AAAAAAAAF2k/q3_2TKxMJ8I/s1600/layout_activity_photo.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://1.bp.blogspot.com/-KnA5sDjgZds/UJBOSqCkiZI/AAAAAAAAF2k/q3_2TKxMJ8I/s1600/layout_activity_photo.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-07/res/layout/activity_photo.xml"&gt;activity_photo.xml&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;Instead of showing a toast in our &lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-07/src/com/androidzeitgeist/mustache/activity/CameraActivity.java"&gt;CameraActivity&lt;/a&gt; we create an Intent to start the PhotoActivity and use &lt;a href="http://developer.android.com/reference/android/content/Intent.html#setData(android.net.Uri)"&gt;setData(Uri)&lt;/a&gt; on the Intent object to pass a &lt;a href="http://developer.android.com/reference/android/net/Uri.html"&gt;Uri&lt;/a&gt; pointing to the picture file:&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/-VSkxvSyGYTU/UJBOjJYq-aI/AAAAAAAAF2s/q1DBQt-Ib9A/s1600/intent_start_photo_activity.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://1.bp.blogspot.com/-VSkxvSyGYTU/UJBOjJYq-aI/AAAAAAAAF2s/q1DBQt-Ib9A/s1600/intent_start_photo_activity.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-07/src/com/androidzeitgeist/mustache/activity/CameraActivity.java#L116"&gt;onPictureTaken() - CameraActivity.java&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;In &lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-07/src/com/androidzeitgeist/mustache/activity/PhotoActivity.java#L19"&gt;onCreate(Bundle)&lt;/a&gt; of the PhotoActivity we'll retrieve the Uri from the Intent and pass it to the ImageView. The ImageView will take care of loading the picture from the external storage and displaying it.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/-tE-1b0nu5Ic/UJBO0nSip_I/AAAAAAAAF20/Thmkq7JwsmM/s1600/photo_activity_on_create.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://3.bp.blogspot.com/-tE-1b0nu5Ic/UJBO0nSip_I/AAAAAAAAF20/Thmkq7JwsmM/s1600/photo_activity_on_create.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-07/src/com/androidzeitgeist/mustache/activity/PhotoActivity.java#L19"&gt;onCreate() - PhotoActivity.java&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;And that's already all the code we need for the first version of the &lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-07/src/com/androidzeitgeist/mustache/activity/PhotoActivity.java"&gt;PhotoActivity&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-sX-QuJWGZtQ/UJBVljLcABI/AAAAAAAAF3I/XEL1UJHnjiI/s1600/camera_and_photo_screenshot.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/-sX-QuJWGZtQ/UJBVljLcABI/AAAAAAAAF3I/XEL1UJHnjiI/s1600/camera_and_photo_screenshot.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;CameraActivity (left) and PhotoActivity (right)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;img src="http://feeds.feedburner.com/~r/AndroidZeitgeist/~4/bA7tWUcBxoc" height="1" width="1" alt=""/&gt;</content><link rel="replies" type="application/atom+xml" href="http://www.androidzeitgeist.com/feeds/5361828375857120954/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="http://www.androidzeitgeist.com/2012/10/displaying-taken-picture-instant.html#comment-form" title="3 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/5361828375857120954" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/5361828375857120954" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/AndroidZeitgeist/~3/bA7tWUcBxoc/displaying-taken-picture-instant.html" title="Displaying the taken picture – Instant Mustache #7" /><author><name>Sebastian Kaspari</name><uri>https://plus.google.com/112283223674539938062</uri><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://1.bp.blogspot.com/-KnA5sDjgZds/UJBOSqCkiZI/AAAAAAAAF2k/q3_2TKxMJ8I/s72-c/layout_activity_photo.png" height="72" width="72" /><thr:total>3</thr:total><gd:extendedProperty name="commentSource" value="1" /><gd:extendedProperty name="commentModerationMode" value="FILTERED_POSTMOD" /><feedburner:origLink>http://www.androidzeitgeist.com/2012/10/displaying-taken-picture-instant.html</feedburner:origLink></entry><entry><id>tag:blogger.com,1999:blog-1531829516427013333.post-73829038323965617</id><published>2012-10-26T20:59:00.000+02:00</published><updated>2012-10-30T23:17:25.534+01:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="camera" /><category scheme="http://www.blogger.com/atom/ns#" term="CameraFragment" /><category scheme="http://www.blogger.com/atom/ns#" term="External storage" /><category scheme="http://www.blogger.com/atom/ns#" term="Instant Mustache" /><category scheme="http://www.blogger.com/atom/ns#" term="MediaScanner" /><category scheme="http://www.blogger.com/atom/ns#" term="OnClickListener" /><category scheme="http://www.blogger.com/atom/ns#" term="picture" /><title type="text">Taking a picture – Instant Mustache #6</title><content type="html">&lt;span style="font-size: x-small;"&gt;This article is part of a series of articles about the development process of Instant Mustache, a fun camera app that adds mustaches to all faces using face detection.&amp;nbsp;&lt;/span&gt;&lt;a href="http://www.androidzeitgeist.com/p/instant-mustache.html" style="font-size: small;"&gt;Click here&lt;/a&gt;&lt;span style="font-size: x-small;"&gt;&amp;nbsp;to get a chronological list of all published&amp;nbsp;&lt;/span&gt;&lt;a href="http://www.androidzeitgeist.com/p/instant-mustache.html" style="font-size: small;"&gt;articles about Instant Mustache&lt;/a&gt;&lt;span style="font-size: x-small;"&gt;.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;After writing the necessary code to display a camera preview in &lt;a href="http://www.androidzeitgeist.com/2012/10/displaying-camera-preview-instant.html"&gt;the last article&lt;/a&gt; it's now time to actually take a picture and save it on the external storage of the device.&lt;br /&gt;&lt;br /&gt;We start by extending the layout of the &lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-06/src/com/androidzeitgeist/mustache/activity/CameraActivity.java"&gt;CameraActivity&lt;/a&gt; to include a button for taking a picture. We assign a method to it to be called when the user clicks on the button via the attribute &lt;a href="http://developer.android.com/reference/android/view/View.html#attr_android:onClick"&gt;android:onClick&lt;/a&gt;. Another option would be to assign an &lt;a href="http://developer.android.com/reference/android/view/View.OnClickListener.html"&gt;OnClickListener&lt;/a&gt; to the view in code. Defining the method in the XML results in less code but has the disadvantage of not being checked by the compiler. Since one of the last releases of the Android SDK the &lt;a href="http://tools.android.com/tips/lint"&gt;lint&lt;/a&gt; tool is able to check the onClick attributes for correctness. So we will use the XML attribute here.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/-hmHriQ9ppUo/UIqjpzAIkZI/AAAAAAAAFpQ/PJZvXOEn5XE/s1600/activity_camera.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://1.bp.blogspot.com/-hmHriQ9ppUo/UIqjpzAIkZI/AAAAAAAAFpQ/PJZvXOEn5XE/s1600/activity_camera.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-06/res/layout/activity_camera.xml"&gt;activity_camera.xml&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;As we encapsulated the code handling the &lt;a href="http://developer.android.com/reference/android/hardware/Camera.html"&gt;Camera&lt;/a&gt; object inside the &lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-06/src/com/androidzeitgeist/mustache/fragment/CameraFragment.java"&gt;CameraFragment&lt;/a&gt; class the activity just calls takePicture() on the fragment when the user presses the button.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/-FE7ro562XUI/UIqkAHnPvzI/AAAAAAAAFpY/-IZ0ncr7CbU/s1600/CameraActivity_takePicture.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://3.bp.blogspot.com/-FE7ro562XUI/UIqkAHnPvzI/AAAAAAAAFpY/-IZ0ncr7CbU/s1600/CameraActivity_takePicture.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-06/src/com/androidzeitgeist/mustache/activity/CameraActivity.java#L64"&gt;takePicutre() - CameraActivity.java&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;The fragment calls &lt;a href="http://developer.android.com/reference/android/hardware/Camera.html#takePicture(android.hardware.Camera.ShutterCallback, android.hardware.Camera.PictureCallback, android.hardware.Camera.PictureCallback, android.hardware.Camera.PictureCallback)"&gt;takePicture()&lt;/a&gt; on the Camera object. It's possible to pass up to four callbacks to this method: A shutter callback, a raw callback, a postview callback and a jpeg callback. According to the documentation their usage is:&lt;br /&gt;&lt;br /&gt;&lt;ul&gt;&lt;li&gt;The shutter callback occurs after the image is captured. This can be used to trigger a sound to let the user know that image has been captured.&lt;/li&gt;&lt;li&gt;The raw callback occurs when the raw image data is available.&lt;/li&gt;&lt;li&gt;The postview callback occurs when a scaled, fully processed postview image is available.&lt;/li&gt;&lt;li&gt;The jpeg callback occurs when the compressed image is available.&lt;/li&gt;&lt;/ul&gt;&lt;br /&gt;We are only interested in the JPEG image. The fragment itself is also the callback so we'll implement the Camera.PictureCallback interface.&lt;br /&gt;&lt;br /&gt;Once the picture is taken &lt;a href="http://developer.android.com/reference/android/hardware/Camera.PictureCallback.html#onPictureTaken(byte[], android.hardware.Camera)"&gt;onPictureTaken()&lt;/a&gt; will be called with a byte array. We decode the given byte array and create a &lt;a href="http://developer.android.com/reference/android/graphics/Bitmap.html"&gt;Bitmap&lt;/a&gt; object and pass it via the &lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-06/src/com/androidzeitgeist/mustache/listener/CameraFragmentListener.java"&gt;CameraFragmentListener&lt;/a&gt; interface to our CameraActivity. Later we will use this bitmap to draw the mustaches on it.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-HmhBl2pWTlo/UIqkdSoODnI/AAAAAAAAFpg/cJ69yQZOIeg/s1600/CameraFragment_onPictureTaken.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/-HmhBl2pWTlo/UIqkdSoODnI/AAAAAAAAFpg/cJ69yQZOIeg/s1600/CameraFragment_onPictureTaken.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-06/src/com/androidzeitgeist/mustache/fragment/CameraFragment.java#L224"&gt;onPictureTaken() - CameraFragment.java&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;b&gt;&lt;span style="font-size: large;"&gt;Saving the picture to the external storage&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;Once the activity receives the bitmap object it needs to do a bunch of things:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;Determine the directory to save the picture to and create it if necessary&lt;/li&gt;&lt;li&gt;Create a unique file for the picture inside the directory and save the image data to it&lt;/li&gt;&lt;li&gt;Notify the MediaScanner that we created a new file&lt;/li&gt;&lt;li&gt;Show a toast that the picture has been saved successfully (For now until we've written the activity to display the taken picture).&lt;/li&gt;&lt;/ul&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;Determining the directory&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;We want to save the picture into a directory on the external storage that is visible for all other applications. By calling &lt;a href="http://developer.android.com/reference/android/os/Environment.html#getExternalStoragePublicDirectory(java.lang.String)"&gt;Environment.getExternalStoragePublicDirectory()&lt;/a&gt; and passing &lt;a href="http://developer.android.com/reference/android/os/Environment.html#DIRECTORY_PICTURES"&gt;Environment.DIRECTORY_PICTURES&lt;/a&gt; we get the public directory for pictures (Available since API Level 8). Inside this directory we'll create a directory with the name of our application (if it doesn't exist already).&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;/div&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://4.bp.blogspot.com/-etMHinejAMU/UIqlvdRKOlI/AAAAAAAAFpo/QlH_Lu4dUOY/s1600/determining_directory.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://4.bp.blogspot.com/-etMHinejAMU/UIqlvdRKOlI/AAAAAAAAFpo/QlH_Lu4dUOY/s1600/determining_directory.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-06/src/com/androidzeitgeist/mustache/activity/CameraActivity.java#L77"&gt;onPictureTaken() - CameraActivity.java&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&amp;nbsp;&lt;span style="font-size: large;"&gt;Saving the file&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;We'll create a file with a unique file name containing a timestamp, e.g.: MUSTACHE_20121031_235959.jpg. The &lt;a href="http://developer.android.com/reference/android/graphics/Bitmap.html#compress(android.graphics.Bitmap.CompressFormat, int, java.io.OutputStream)"&gt;compress()&lt;/a&gt; method of the bitmap object is used to save the picture into the file.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/-yNhLaUTQOs8/UIqlxhiP9tI/AAAAAAAAFp0/HlPPxj7_L9w/s1600/saving_picture.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://1.bp.blogspot.com/-yNhLaUTQOs8/UIqlxhiP9tI/AAAAAAAAFp0/HlPPxj7_L9w/s1600/saving_picture.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-06/src/com/androidzeitgeist/mustache/activity/CameraActivity.java#L92"&gt;onPictureTaken() - CameraActivity.java&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;Notifying the MediaScanner&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;Scanning the SD card for changes is costly. Therefore most Android versions only scan the whole card if the card is re-inserted or was mounted by another device. This seems to be different in different vendor versions of Android but nevertheless you can't assume a file to be seen by other applications (for example the gallery) until it has been scanned by the &lt;a href="http://developer.android.com/reference/android/media/MediaScannerConnection.html"&gt;MediaScanner&lt;/a&gt;. For that reason we'll notify the MediaScanner about the file we've created.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://4.bp.blogspot.com/-BKiT4GJmj8s/UIqlw9LsiFI/AAAAAAAAFpw/K1_jEkvhX98/s1600/notify_mediascanner.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://4.bp.blogspot.com/-BKiT4GJmj8s/UIqlw9LsiFI/AAAAAAAAFpw/K1_jEkvhX98/s1600/notify_mediascanner.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-06/src/com/androidzeitgeist/mustache/activity/CameraActivity.java#L107"&gt;onPictureTaken() - CameraActivity.java&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;By now we've already created a simple camera application. We can take pictures and they show up in the gallery of the device. Pretty cool so far, huh?&lt;img src="http://feeds.feedburner.com/~r/AndroidZeitgeist/~4/c-K683UuF5g" height="1" width="1" alt=""/&gt;</content><link rel="replies" type="application/atom+xml" href="http://www.androidzeitgeist.com/feeds/73829038323965617/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="http://www.androidzeitgeist.com/2012/10/taking-picture-instant-mustache-6.html#comment-form" title="1 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/73829038323965617" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/73829038323965617" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/AndroidZeitgeist/~3/c-K683UuF5g/taking-picture-instant-mustache-6.html" title="Taking a picture – Instant Mustache #6" /><author><name>Sebastian Kaspari</name><uri>https://plus.google.com/112283223674539938062</uri><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://1.bp.blogspot.com/-hmHriQ9ppUo/UIqjpzAIkZI/AAAAAAAAFpQ/PJZvXOEn5XE/s72-c/activity_camera.png" height="72" width="72" /><thr:total>1</thr:total><gd:extendedProperty name="commentSource" value="1" /><gd:extendedProperty name="commentModerationMode" value="FILTERED_POSTMOD" /><feedburner:origLink>http://www.androidzeitgeist.com/2012/10/taking-picture-instant-mustache-6.html</feedburner:origLink></entry><entry><id>tag:blogger.com,1999:blog-1531829516427013333.post-8727545494996059717</id><published>2012-10-25T21:24:00.000+02:00</published><updated>2012-11-05T19:31:06.680+01:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="margin" /><category scheme="http://www.blogger.com/atom/ns#" term="Offscreen pages" /><category scheme="http://www.blogger.com/atom/ns#" term="OnPageChangeListener" /><category scheme="http://www.blogger.com/atom/ns#" term="ViewPager" /><title type="text">Examining the ViewPager #2</title><content type="html">&lt;span style="font-size: x-small;"&gt;This article is part of a series of articles about the ViewPager component.&amp;nbsp;&lt;/span&gt;&lt;a href="http://www.androidzeitgeist.com/p/viewpager.html" style="font-size: small;"&gt;Click here&lt;/a&gt;&lt;span style="font-size: x-small;"&gt;&amp;nbsp;to see a list of&amp;nbsp;&lt;/span&gt;&lt;a href="http://www.androidzeitgeist.com/p/viewpager.html" style="font-size: small;"&gt;all articles of this series&lt;/a&gt;&lt;span style="font-size: x-small;"&gt;.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;span style="font-size: large;"&gt;Offscreen pages&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;The &lt;a href="http://developer.android.com/reference/android/support/v4/view/ViewPager.html"&gt;ViewPager&lt;/a&gt; doesn't create all its pages at once. When using a lot of pages this would be horribly slow and even unnecessary if the user would never swipe through all these pages. By default the ViewPager only creates the current page as well as the offscreen pages to the left and right of the current page.&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/-HzlJvCZIyQc/UIkzJpr1neI/AAAAAAAAFls/jfMZCwnu30I/s1600/offscreen_pages.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" src="http://1.bp.blogspot.com/-HzlJvCZIyQc/UIkzJpr1neI/AAAAAAAAFls/jfMZCwnu30I/s1600/offscreen_pages.png" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;If you only use a small amount of pages you may get a better performance by creating them all at once. You can use &lt;a href="http://developer.android.com/reference/android/support/v4/view/ViewPager.html#setOffscreenPageLimit(int)"&gt;setOffscreenPageLimit(int limit)&lt;/a&gt; to set the number of pages that will be created and retained. Note that the limit applies to both sides of the current page. So if you set the offscreen page limit to 2 the ViewPager will retain 5 pages: The current page plus 2 pages to the left and right.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;span style="font-size: large;"&gt;Responding to changing states&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;You can get notified whenever the displayed page changes or is incrementally scrolled. To listen to these state changes you can implement the &lt;a href="http://developer.android.com/reference/android/support/v4/view/ViewPager.OnPageChangeListener.html"&gt;OnPageChangeListener&lt;/a&gt; interface or extend the &lt;a href="http://developer.android.com/reference/android/support/v4/view/ViewPager.SimpleOnPageChangeListener.html"&gt;SimpleOnPageChangeListener&lt;/a&gt; class if you do not intent to override every method of the interfacce.&lt;br /&gt;&lt;br /&gt;The following example listener updates the title of the activity according to the title of the currently selected page:&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-xgUfOiVWc_g/UIkzQ_OCF8I/AAAAAAAAFl0/Fp_eXNYlt4g/s1600/UpdateTitleListener.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/-xgUfOiVWc_g/UIkzQ_OCF8I/AAAAAAAAFl0/Fp_eXNYlt4g/s1600/UpdateTitleListener.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Android-Zeitgeist-Samples/blob/master/ViewPager/02/UpdateTitleListener.java"&gt;UpdateTitleListener.java&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;b&gt;&lt;span style="font-size: large;"&gt;Margin between pages&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;When scrolling through pages they all look like glued together. You can use &lt;a href="http://developer.android.com/reference/android/support/v4/view/ViewPager.html#setPageMargin(int)"&gt;setPageMargin(int pixels)&lt;/a&gt; to define a margin between pages. The gap is filled with the background color of the ViewPager.&lt;br /&gt;&lt;br /&gt;The following screenshots are showing the same ViewPager during switching pages. The left screenshot shows the ViewPager with no page margin set. In the right screenshot the margin has been set to 20 pixels. Notice that the method expects the margin to be defined in pixels. To use the same physical margin on all kind of screens independent from their pixel density define the margin in density independent pixels (dp) and &lt;a href="http://stackoverflow.com/a/6327095/234908"&gt;convert them to the actual number of pixels&lt;/a&gt; for the current screen.&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-fS9q-7IO-Uc/UIkzWvgeSoI/AAAAAAAAFl8/GESmB7vau7c/s1600/page_margin.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/-fS9q-7IO-Uc/UIkzWvgeSoI/AAAAAAAAFl8/GESmB7vau7c/s1600/page_margin.png" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;It's also possible to define a drawable that will be used to fill the margin between two pages using &lt;a href="http://developer.android.com/reference/android/support/v4/view/ViewPager.html#setPageMarginDrawable(int)"&gt;setPageMarginDrawable(int resId)&lt;/a&gt; or &lt;a href="http://developer.android.com/reference/android/support/v4/view/ViewPager.html#setPageMarginDrawable(android.graphics.drawable.Drawable)"&gt;setPageMarginDrawable(Drawable d)&lt;/a&gt;. The best approach is to use a &lt;a href="http://developer.android.com/guide/topics/graphics/2d-graphics.html#nine-patch"&gt;Nine-patch&lt;/a&gt; that be scaled dynamically by the system to fill the space between the pages.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;span style="font-size: large;"&gt;Switching pages programmatically&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;You can also switch between pages programmatically. Using &lt;a href="http://developer.android.com/reference/android/support/v4/view/ViewPager.html#setCurrentItem(int, boolean)"&gt;setCurrentItem(int position, boolean smoothScroll)&lt;/a&gt; you can switch to the given position. If the second parameter is &lt;span style="color: #660000;"&gt;true&lt;/span&gt; a smooth animated transition is being performed. Using &lt;span style="color: #660000;"&gt;false&lt;/span&gt; the ViewPager will switch to the given page without any animation. If you always want to switch pages with an animation you can also leave the second parameter and use &lt;a href="http://developer.android.com/reference/android/support/v4/view/ViewPager.html#setCurrentItem(int)"&gt;setCurrentItem(int position)&lt;/a&gt;.&lt;img src="http://feeds.feedburner.com/~r/AndroidZeitgeist/~4/EUMG4ssJLOA" height="1" width="1" alt=""/&gt;</content><link rel="replies" type="application/atom+xml" href="http://www.androidzeitgeist.com/feeds/8727545494996059717/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="http://www.androidzeitgeist.com/2012/10/examining-viewpager-2.html#comment-form" title="1 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/8727545494996059717" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/8727545494996059717" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/AndroidZeitgeist/~3/EUMG4ssJLOA/examining-viewpager-2.html" title="Examining the ViewPager #2" /><author><name>Sebastian Kaspari</name><uri>https://plus.google.com/112283223674539938062</uri><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://1.bp.blogspot.com/-HzlJvCZIyQc/UIkzJpr1neI/AAAAAAAAFls/jfMZCwnu30I/s72-c/offscreen_pages.png" height="72" width="72" /><thr:total>1</thr:total><gd:extendedProperty name="commentSource" value="1" /><gd:extendedProperty name="commentModerationMode" value="FILTERED_POSTMOD" /><feedburner:origLink>http://www.androidzeitgeist.com/2012/10/examining-viewpager-2.html</feedburner:origLink></entry><entry><id>tag:blogger.com,1999:blog-1531829516427013333.post-1618586447717593615</id><published>2012-10-22T18:30:00.000+02:00</published><updated>2012-10-30T23:18:43.358+01:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="FragmentPagerAdapter" /><category scheme="http://www.blogger.com/atom/ns#" term="FragmentStatePagerAdapter" /><category scheme="http://www.blogger.com/atom/ns#" term="PagerAdapter" /><category scheme="http://www.blogger.com/atom/ns#" term="ViewPager" /><title type="text">Examining the ViewPager #1</title><content type="html">&lt;span style="font-size: x-small;"&gt;This article is part of a series of articles about the ViewPager component. &lt;a href="http://www.androidzeitgeist.com/p/viewpager.html"&gt;Click here&lt;/a&gt; to see a list of &lt;a href="http://www.androidzeitgeist.com/p/viewpager.html"&gt;all articles of this series&lt;/a&gt;.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;The &lt;a href="http://developer.android.com/tools/extras/support-library.html"&gt;Android support library&lt;/a&gt; offers a great UI component for horizontal scrolling pages: &lt;a href="http://developer.android.com/reference/android/support/v4/view/ViewPager.html"&gt;The ViewPager&lt;/a&gt;. Over the last iterations of the support library more and more functionality has been added to the ViewPager silently. For that reason I decided to study the various features of the ViewPager more closely. This will be a series of articles covering several of these features.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;Disclaimer upfront&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;According to the &lt;a href="http://developer.android.com/reference/android/support/v4/view/ViewPager.html"&gt;documentation of the ViewPager&lt;/a&gt; the implementation and API of the class may change in future releases. Therefore also this blog posting may not be up-to-date if you are reading this a long time after the published date. Check the &lt;a href="http://developer.android.com/reference/android/support/v4/view/ViewPager.html"&gt;documentation&lt;/a&gt; if some of the examples may not work anymore as I described them here.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;The basics&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;The ViewPager is a &lt;a href="http://developer.android.com/reference/android/view/ViewGroup.html"&gt;ViewGroup&lt;/a&gt; that displays by default one page at a time. The user can switch between these pages by swiping horizontally. A &lt;a href="http://developer.android.com/reference/android/support/v4/view/PagerAdapter.html"&gt;PagerAdapter&lt;/a&gt; dynamically provides these pages which can be just views or fragments. However the ViewPager is not an AdapterView like the ListView or the GridView. Therefore you need to implement a specific adapter class in order to use the ViewPager class.&lt;br /&gt;&lt;br /&gt;The following video shows a ViewPager with different colored pages:&lt;br /&gt;&lt;br /&gt;&lt;iframe allowfullscreen="allowfullscreen" frameborder="0" height="360" src="http://www.youtube.com/embed/tcnGyRc9t0M?rel=0" width="480"&gt;&lt;/iframe&gt; &lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;Adapter using fragments&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;The easiest way to write an adapter for a ViewPager is to use fragments and let your adapter implementation extend the &lt;a href="http://developer.android.com/reference/android/support/v4/app/FragmentPagerAdapter.html"&gt;FragmentPagerAdapter&lt;/a&gt; class. You only need to implement &lt;a href="http://developer.android.com/reference/android/support/v4/app/FragmentPagerAdapter.html#getItem(int)"&gt;getItem(int position)&lt;/a&gt; to return a fragment for the page at the given position and &lt;a href="http://developer.android.com/reference/android/support/v4/view/PagerAdapter.html#getCount()"&gt;getCount()&lt;/a&gt; to return the number of pages to display.&lt;br /&gt;&lt;br /&gt;The following implementation shows the FragmentPagerAdapter used for the video above:&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/-CkknYiuF5vQ/UIQMY-pOFOI/AAAAAAAAFjY/es0HrjuPA0U/s1600/SampleFragmentPagerAdapter.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://1.bp.blogspot.com/-CkknYiuF5vQ/UIQMY-pOFOI/AAAAAAAAFjY/es0HrjuPA0U/s1600/SampleFragmentPagerAdapter.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Android-Zeitgeist-Samples/blob/master/ViewPager/01/SampleFragmentPagerAdapter.java"&gt;SampleFragmentPagerAdapter.java&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;Saving fragment states&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;When using a &lt;a href="http://developer.android.com/reference/android/support/v13/app/FragmentPagerAdapter.html"&gt;FragmentPagerAdapter&lt;/a&gt; and swiping through the pages the ViewPager may eventually have created fragments for all the pages. Depending on the number of pages this may need a large amount of memory just for holding all the offscreen pages. To solve this problem the support library offers the &lt;a href="http://developer.android.com/reference/android/support/v4/app/FragmentStatePagerAdapter.html"&gt;FragmentStatePagerAdapter&lt;/a&gt; class. This adapter will destroy fragments not visible to the user when needed. Whenever this happens the adapter saves the fragment's current state using &lt;a href="http://developer.android.com/reference/android/app/Fragment.html#onSaveInstanceState(android.os.Bundle)"&gt;onSaveInstanceState(Bundle outState) &lt;/a&gt;of the fragment class to restore it when the fragment for this page gets recreated.&lt;br /&gt;&lt;br /&gt;Implementing an adapter extending the FragmentStatePagerAdapter is exactly the same as when using the FragmentPagerAdapter class. Just your fragment needs to take care of saving its state when getting destroyed. Take a look at the &lt;a href="http://developer.android.com/reference/android/app/Fragment.html#onSaveInstanceState(android.os.Bundle)"&gt;documentation&lt;/a&gt; for an example on how to retain the state of a fragment.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;Adapter using views&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;You can also use the ViewPager with only View objects as pages if using fragments isn't an option for you. It's a bit more tricky to implement the adapter as you'll have to directly extend the &lt;a href="http://developer.android.com/reference/android/support/v4/view/PagerAdapter.html"&gt;PagerAdapter&lt;/a&gt; class.&lt;br /&gt;&lt;br /&gt;The following example creates an adapter that shows a number of different colored pages like the FragmentPagerAdapter implementation above.&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;/div&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-mxaO0n1cYhQ/UIQNo43ZszI/AAAAAAAAFjw/FQ5bbhdhrMU/s1600/SamplePagerAdapter.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/-mxaO0n1cYhQ/UIQNo43ZszI/AAAAAAAAFjw/FQ5bbhdhrMU/s1600/SamplePagerAdapter.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Android-Zeitgeist-Samples/blob/master/ViewPager/01/SamplePagerAdapter.java"&gt;SamplePagerAdaper.java&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;As you can see you need to write some boilerplate code to add and remove the views to the pager. I published a simple&amp;nbsp;&lt;a href="https://gist.github.com/3927202"&gt;ViewPagerAdapter&lt;/a&gt; class on GitHub that does all this for you so that you don't need to write much more code than when using fragments:&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-RG1Pybe0Qko/UIQNEZ0emlI/AAAAAAAAFjo/wiW86E8k3lY/s1600/SamplePagerAdapter2.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/-RG1Pybe0Qko/UIQNEZ0emlI/AAAAAAAAFjo/wiW86E8k3lY/s1600/SamplePagerAdapter2.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Android-Zeitgeist-Samples/blob/master/ViewPager/01/SamplePagerAdapter2.java"&gt;SamplePagerAdapter2.java&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;img src="http://feeds.feedburner.com/~r/AndroidZeitgeist/~4/Xd7oNSltXJE" height="1" width="1" alt=""/&gt;</content><link rel="replies" type="application/atom+xml" href="http://www.androidzeitgeist.com/feeds/1618586447717593615/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="http://www.androidzeitgeist.com/2012/10/examining-viewpager-14.html#comment-form" title="4 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/1618586447717593615" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/1618586447717593615" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/AndroidZeitgeist/~3/Xd7oNSltXJE/examining-viewpager-14.html" title="Examining the ViewPager #1" /><author><name>Sebastian Kaspari</name><uri>https://plus.google.com/112283223674539938062</uri><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://img.youtube.com/vi/tcnGyRc9t0M/default.jpg" height="72" width="72" /><thr:total>4</thr:total><gd:extendedProperty name="commentSource" value="1" /><gd:extendedProperty name="commentModerationMode" value="FILTERED_POSTMOD" /><feedburner:origLink>http://www.androidzeitgeist.com/2012/10/examining-viewpager-14.html</feedburner:origLink></entry><entry><id>tag:blogger.com,1999:blog-1531829516427013333.post-2935160722418880762</id><published>2012-10-18T19:28:00.003+02:00</published><updated>2012-10-30T23:19:15.647+01:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="camera" /><category scheme="http://www.blogger.com/atom/ns#" term="CameraFragment" /><category scheme="http://www.blogger.com/atom/ns#" term="Instant Mustache" /><category scheme="http://www.blogger.com/atom/ns#" term="SurfaceView" /><title type="text">Displaying the camera preview - Instant Mustache #5</title><content type="html">&lt;span style="font-size: x-small;"&gt;This article is part of a series of articles about the development process of Instant Mustache, a fun camera app that adds mustaches to all faces using face detection.&amp;nbsp;&lt;/span&gt;&lt;a href="http://www.androidzeitgeist.com/p/instant-mustache.html" style="font-size: small;"&gt;Click here&lt;/a&gt;&lt;span style="font-size: x-small;"&gt;&amp;nbsp;to get a chronological list of all published&amp;nbsp;&lt;/span&gt;&lt;a href="http://www.androidzeitgeist.com/p/instant-mustache.html" style="font-size: small;"&gt;articles about Instant Mustache&lt;/a&gt;&lt;span style="font-size: x-small;"&gt;.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;From &lt;a href="http://www.androidzeitgeist.com/2012/10/using-fragment-for-camera-preview.html"&gt;the last article&lt;/a&gt; we already have three components: A &lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-04/src/com/androidzeitgeist/mustache/activity/CameraActivity.java"&gt;CameraActivity&lt;/a&gt; with not much code, an empty &lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-04/src/com/androidzeitgeist/mustache/fragment/CameraFragment.java"&gt;CameraFragment&lt;/a&gt; and a &lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-04/src/com/androidzeitgeist/mustache/listener/CameraFragmentListener.java"&gt;CameraFragmentListener&lt;/a&gt; interface for the communication between fragment and activity. Now we need to write the actual CameraFragment code to display the camera preview.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;The CameraPreview component&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;For displaying the preview we need an instance of &lt;a href="http://developer.android.com/reference/android/view/SurfaceView.html"&gt;SurfaceView&lt;/a&gt; to draw the actual camera picture on. We'll extend the SurfaceView to create our own view component called &lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-05/src/com/androidzeitgeist/mustache/view/CameraPreview.java"&gt;CameraPreview&lt;/a&gt;.&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;a href="http://www.androidzeitgeist.com/2012/08/lets-start-coding-not-instant-mustache-3.html"&gt;In the first articles&lt;/a&gt; we decided to use a square ratio for the preview. After some testing around it seems the emulator is the only "device" that supports a square sized camera preview and picture size out of the box. To work around this issue we would need to use a widely supported ratio (4:3) and crop the preview picture as well as the taken photo ourselves. To keep the code and the first version of the app small (and to follow the &lt;a href="http://www.androidzeitgeist.com/2012/07/minimal-marketable-app-instant-mustache.html"&gt;Minimal Marketable App&lt;/a&gt; principle) I decided to change this requirement. We will use the commonly supported ratio of 4:3.&lt;br /&gt;&lt;br /&gt;We implement onMeasure() to set the dimension of the view to a 4:3 ratio:&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/-NJ3UtjTFli4/UIA4oX9FTuI/AAAAAAAAFhg/AMivOOGn94Y/s1600/onmeasure.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://3.bp.blogspot.com/-NJ3UtjTFli4/UIA4oX9FTuI/AAAAAAAAFhg/AMivOOGn94Y/s1600/onmeasure.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-05/src/com/androidzeitgeist/mustache/view/CameraPreview.java#L32"&gt;onMeasure() - CameraPreview.java&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;b&gt;&lt;span style="font-size: large;"&gt;Setting up the camera&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;Even though we know our desired ratio we can't just set a size via &lt;a href="http://developer.android.com/reference/android/hardware/Camera.html#setParameters(android.hardware.Camera.Parameters)"&gt;camera.setParameters(Camera.Parameters)&lt;/a&gt;. Instead we have to query the &lt;a href="http://developer.android.com/reference/android/hardware/Camera.Parameters.html"&gt;Camera.Parameters&lt;/a&gt; object we get from getParameters() to retrieve a list of supported preview and picture sizes. Then we have to scan this list for a size that has our desired ratio. This is what determineBestSize() does:&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/-9EpZpQQQk_g/UH2o5PQn4kI/AAAAAAAAFgk/Ip5ecGrvDSU/s1600/determine_size.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://3.bp.blogspot.com/-9EpZpQQQk_g/UH2o5PQn4kI/AAAAAAAAFgk/Ip5ecGrvDSU/s1600/determine_size.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-05/src/com/androidzeitgeist/mustache/fragment/CameraFragment.java#L190"&gt;determineBestSize() - CameraFragment.java&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;We are using a threshold for the preview and picture size to save some heap space for the bitmap transformations we'll do later. For now these limits are 640x480 for the preview size and 1280x960 for the &amp;nbsp;picture size.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;Finally the determined values are used to setup the camera object:&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-aeWvWYDnT2s/UH2uJVZkAFI/AAAAAAAAFg4/puouCbK7VOQ/s1600/setup_camera.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/-aeWvWYDnT2s/UH2uJVZkAFI/AAAAAAAAFg4/puouCbK7VOQ/s1600/setup_camera.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-05/src/com/androidzeitgeist/mustache/fragment/CameraFragment.java#L166"&gt;setupCamera() - CameraFragment.java&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;The CameraPreview component will be used as view of our CameraFragment by returning it in &lt;a href="http://developer.android.com/reference/android/app/Fragment.html#onCreateView(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle)"&gt;onCreateView()&lt;/a&gt;. In addition to that our CameraFragment needs to implement &lt;a href="http://developer.android.com/reference/android/view/SurfaceHolder.Callback.html"&gt;SurfaceHolder.Callback&lt;/a&gt; in order to get notified when the surface is created or destroyed.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style="font-size: large;"&gt;Accessing the camera&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;To access the camera of the device we need to call &lt;a href="http://developer.android.com/reference/android/hardware/Camera.html#open(int)"&gt;Camera.open(int cameraId)&lt;/a&gt; to obtain a &lt;a href="http://developer.android.com/reference/android/hardware/Camera.html"&gt;Camera&lt;/a&gt; object. The camera ids are numbered starting with 0. As we currently don't want to support multiple cameras we will just call Camera.open(0).&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Unfortunately using the camera can cause undefined exceptions to be thrown. Therefore we need to wrap some code accessing the camera object into try-catch blocks and catch the generic Exception object. This is usually &lt;a href="http://source.android.com/source/code-style.html#dont-catch-generic-exception"&gt;considered bad practice&lt;/a&gt; but in this case &lt;a href="http://developer.android.com/guide/topics/media/camera.html#access-camera"&gt;encouraged to do by the documentation&lt;/a&gt;.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;Sharing the camera resource&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;The camera can only be used by one application at a time so we need to release the camera every time the activity gets paused. Otherwise the user would not be able to use other camera applications while our activity is in the background. In some cases not releasing the camera can lead to the CameraService crashing. If this happens no camera application can be used until the user reboots his phone. We never want this to happen.&lt;br /&gt;&lt;br /&gt;To be safe we will open the camera by calling Camera.open() in the &lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-05/src/com/androidzeitgeist/mustache/fragment/CameraFragment.java#L70"&gt;onResume()&lt;/a&gt; method of the CameraFragment and releasing it again in &lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-05/src/com/androidzeitgeist/mustache/fragment/CameraFragment.java#L87"&gt;onPause()&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;Once we have a Camera object and our surface is created we can assign the surface to the camera object and start the preview. We wrap the call to startPreview() in our own method called &lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-05/src/com/androidzeitgeist/mustache/fragment/CameraFragment.java#L97"&gt;startCameraPreview()&lt;/a&gt; inside our fragment. This way we can setup the camera object before actually starting the preview.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://4.bp.blogspot.com/-KFpfQOjQJY4/UIA567dmV6I/AAAAAAAAFho/fv7wu7wsUYg/s1600/startpreview.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://4.bp.blogspot.com/-KFpfQOjQJY4/UIA567dmV6I/AAAAAAAAFho/fv7wu7wsUYg/s1600/startpreview.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-05/src/com/androidzeitgeist/mustache/fragment/CameraFragment.java#L97"&gt;startCameraPreview() - CameraFragment.java&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size: large;"&gt;&lt;b&gt;Determining the display orientation&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;The screen can be rotated in four different angles (0°, 90°, 180°, 270°). In addition to that the camera can also be built into the device in four different angles by the manufacturer. Finally the camera can be on the front or on the back of the device. We will need to get all these angles and tell the camera object the display orientation so that the preview will be drawn on the surface using the right rotation.&lt;br /&gt;&lt;br /&gt;This sounds tricky but fortunately there's a &lt;a href="http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int)"&gt;code snippet for that in the Android documentation&lt;/a&gt;. We'll use &lt;a href="http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int)"&gt;this code snippet&lt;/a&gt; to implement &lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-05/src/com/androidzeitgeist/mustache/fragment/CameraFragment.java#L126"&gt;determineDisplayOrientation()&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;span style="font-size: large;"&gt;Screenshot&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;And that's how our app looks like so far:&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-YuVQvTJ3OU0/UIA00sECLRI/AAAAAAAAFhM/R8mHYZhRnIE/s1600/mustache_screen.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/-YuVQvTJ3OU0/UIA00sECLRI/AAAAAAAAFhM/R8mHYZhRnIE/s1600/mustache_screen.png" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class="separator" style="clear: both; text-align: left;"&gt;As always the&amp;nbsp;&lt;a href="https://github.com/pocmo/Instant-Mustache/tree/article-05"&gt;source code&amp;nbsp;of the current version&lt;/a&gt; of the app is available at Github.&lt;/div&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/AndroidZeitgeist/~4/C01oUsX3kSI" height="1" width="1" alt=""/&gt;</content><link rel="replies" type="application/atom+xml" href="http://www.androidzeitgeist.com/feeds/2935160722418880762/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="http://www.androidzeitgeist.com/2012/10/displaying-camera-preview-instant.html#comment-form" title="1 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/2935160722418880762" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/2935160722418880762" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/AndroidZeitgeist/~3/C01oUsX3kSI/displaying-camera-preview-instant.html" title="Displaying the camera preview - Instant Mustache #5" /><author><name>Sebastian Kaspari</name><uri>https://plus.google.com/112283223674539938062</uri><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://3.bp.blogspot.com/-NJ3UtjTFli4/UIA4oX9FTuI/AAAAAAAAFhg/AMivOOGn94Y/s72-c/onmeasure.png" height="72" width="72" /><thr:total>1</thr:total><gd:extendedProperty name="commentSource" value="1" /><gd:extendedProperty name="commentModerationMode" value="FILTERED_POSTMOD" /><feedburner:origLink>http://www.androidzeitgeist.com/2012/10/displaying-camera-preview-instant.html</feedburner:origLink></entry><entry><id>tag:blogger.com,1999:blog-1531829516427013333.post-7757019473292818494</id><published>2012-10-15T19:28:00.002+02:00</published><updated>2012-10-30T23:19:35.663+01:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Fragment" /><category scheme="http://www.blogger.com/atom/ns#" term="Instant Mustache" /><title type="text">Using a fragment for the camera preview - Instant Mustache #4</title><content type="html">&lt;span style="font-size: x-small;"&gt;This article is part of a series of articles about the development process of Instant Mustache, a fun camera app that adds mustaches to all faces using face detection.&amp;nbsp;&lt;/span&gt;&lt;a href="http://www.androidzeitgeist.com/p/instant-mustache.html" style="font-size: small;"&gt;Click here&lt;/a&gt;&lt;span style="font-size: x-small;"&gt;&amp;nbsp;to get a chronological list of all published&amp;nbsp;&lt;/span&gt;&lt;a href="http://www.androidzeitgeist.com/p/instant-mustache.html" style="font-size: small;"&gt;articles about Instant Mustache&lt;/a&gt;&lt;span style="font-size: x-small;"&gt;.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;This article will be the first one about writing the CameraActivity for &lt;a href="http://www.androidzeitgeist.com/p/instant-mustache.html"&gt;Instant Mustache&lt;/a&gt;. From &lt;a href="http://www.androidzeitgeist.com/2012/08/lets-start-coding-not-instant-mustache-3.html"&gt;the last article&lt;/a&gt; we already know how the layout of the activity should look like. Now it's time to start the coding part.&lt;br /&gt;&lt;br /&gt;I won't cover all the lines of code in this and the following blog articles but you can always get the complete &lt;a href="https://github.com/pocmo/Instant-Mustache"&gt;source code of Instant Mustache at GitHub&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Using a fragment&lt;/b&gt;&lt;br /&gt;We'll move the actual code (and layout) that handles the camera preview to a &lt;a href="http://developer.android.com/guide/components/fragments.html"&gt;fragment&lt;/a&gt;. Using fragments has several advantages:&lt;br /&gt;&lt;br /&gt;&lt;ul&gt;&lt;li&gt;Fragments can be reused in different activities and layouts. Therefore they are perfect building blocks for composing UIs for different screen sizes.&lt;/li&gt;&lt;li&gt;They encapsulate the code for a specific component including the layout of the component. This makes the activity code and layout much more simpler and cleaner. By using a bunch of fragments the activity basically only has to handle events and delegate them to the appropriate fragments. This leads to the activity becoming some kind of light meta controller instead of an unreadable dumping ground for handling everything on the current screen.&lt;/li&gt;&lt;li&gt;Fragments can be dynamically added, removed, replaced, added to the back stack, animated and other cool things that can be painful to do yourself.&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;&lt;b&gt;&lt;br /&gt;Activity layout&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;div style="margin-bottom: 0in;"&gt;As said before we are moving the camera preview code and layout to a fragment. Therefore our activity's layout is really quite simple:&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;div style="margin-bottom: 0in;"&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;span style="margin-left: auto; margin-right: auto;"&gt;&lt;a href="http://www.blogger.com/goog_154278281"&gt;&lt;img border="0" src="http://1.bp.blogspot.com/-9A5F03FauP0/UHxDx3aMDcI/AAAAAAAAFfE/x245XxDRKJk/s1600/camera_activity_layout_2.png" /&gt;&lt;/a&gt;&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-04/res/layout/activity_camera.xml"&gt;activity_camera.xml&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;/div&gt;&lt;div style="margin-bottom: 0in;"&gt;&lt;b&gt;Communication between activity and fragment&lt;/b&gt;&lt;/div&gt;&lt;div style="margin-bottom: 0in;"&gt;&lt;/div&gt;&lt;div style="margin-bottom: 0in;"&gt;There are situations where the fragment needs to notify the activity about events. In our case the fragment needs to tell the activity when it's unable to instantiate a camera preview so that the activity can handle this error case. Of course we could just access the activity inside the fragment using &lt;a href="http://developer.android.com/reference/android/app/Fragment.html#getActivity()"&gt;getActivity()&lt;/a&gt; and cast it to a CameraActivity object to get access to a method like onCameraError() but then our fragment would only be usable with this particular activity:&lt;/div&gt;&lt;div style="margin-bottom: 0in;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;div style="margin-bottom: 0in;"&gt;&lt;/div&gt;&lt;div style="margin-bottom: 0in;"&gt;&lt;span style="color: #444444; font-family: Courier New, Courier, monospace;"&gt;CameraActivity activity = (CameraActivity) getActivity();&lt;/span&gt;&lt;/div&gt;&lt;div style="margin-bottom: 0in;"&gt;&lt;span style="color: #444444; font-family: Courier New, Courier, monospace;"&gt;activity.onCameraError();&lt;/span&gt;&lt;/div&gt;&lt;div style="margin-bottom: 0in;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;div style="margin-bottom: 0in;"&gt;&lt;/div&gt;&lt;div style="margin-bottom: 0in;"&gt;We work around this issue by defining an interface that has to be implemented by the CameraActivity and every other component that wants to use the CameraFragment:&lt;/div&gt;&lt;div style="margin-bottom: 0in;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/--DMu6Y45ciU/UHxBNXetSII/AAAAAAAAFe0/YrGtXiPkUXE/s1600/fragment_listener.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;span style="color: black;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/--DMu6Y45ciU/UHxBNXetSII/AAAAAAAAFe0/YrGtXiPkUXE/s1600/fragment_listener.png" /&gt;&lt;/span&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-04/src/com/androidzeitgeist/mustache/listener/CameraFragmentListener.java"&gt;&lt;span style="color: black; font-size: small;"&gt;CameraFragmentListener.java&lt;/span&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;Every time our fragment gets attached to an activity &lt;a href="http://developer.android.com/reference/android/app/Fragment.html#onAttach(android.app.Activity)"&gt;onAttach()&lt;/a&gt; will be called with the activity as parameter. We use this method to enforce that the activity implements our defined interface:&lt;br /&gt;&lt;div style="margin-bottom: 0in;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;span style="margin-left: auto; margin-right: auto;"&gt;&lt;a href="http://www.blogger.com/goog_154278245"&gt;&lt;img border="0" src="http://3.bp.blogspot.com/-hGixG23tyHc/UHxBneeyqOI/AAAAAAAAFe8/VF7FmuzO8Jk/s1600/fragment_on_attach.png" /&gt;&lt;/a&gt;&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-04/src/com/androidzeitgeist/mustache/fragment/CameraFragment.java"&gt;CameraFragment.java&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;div style="margin-bottom: 0in;"&gt;&lt;/div&gt;&lt;div style="margin-bottom: 0in;"&gt;Finally take a look at the &lt;a href="https://github.com/pocmo/Instant-Mustache/blob/article-04/src/com/androidzeitgeist/mustache/activity/CameraActivity.java"&gt;CameraActivity source code on GitHub&lt;/a&gt;. It just sets the layout in onCreate() and shows a &lt;a href="http://developer.android.com/guide/topics/ui/notifiers/toasts.html"&gt;toast&lt;/a&gt; in case of the fragment calling onCameraError(). As this method will be called in case of non-recoverable errors we'll also finish the activity.&lt;/div&gt;&lt;/div&gt;&lt;br /&gt;&lt;img src="http://feeds.feedburner.com/~r/AndroidZeitgeist/~4/BCc8E5OcNig" height="1" width="1" alt=""/&gt;</content><link rel="replies" type="application/atom+xml" href="http://www.androidzeitgeist.com/feeds/7757019473292818494/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="http://www.androidzeitgeist.com/2012/10/using-fragment-for-camera-preview.html#comment-form" title="2 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/7757019473292818494" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/7757019473292818494" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/AndroidZeitgeist/~3/BCc8E5OcNig/using-fragment-for-camera-preview.html" title="Using a fragment for the camera preview - Instant Mustache #4" /><author><name>Sebastian Kaspari</name><uri>https://plus.google.com/112283223674539938062</uri><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://1.bp.blogspot.com/-9A5F03FauP0/UHxDx3aMDcI/AAAAAAAAFfE/x245XxDRKJk/s72-c/camera_activity_layout_2.png" height="72" width="72" /><thr:total>2</thr:total><gd:extendedProperty name="commentSource" value="1" /><gd:extendedProperty name="commentModerationMode" value="FILTERED_POSTMOD" /><feedburner:origLink>http://www.androidzeitgeist.com/2012/10/using-fragment-for-camera-preview.html</feedburner:origLink></entry><entry><id>tag:blogger.com,1999:blog-1531829516427013333.post-8978580997544983984</id><published>2012-08-20T19:02:00.001+02:00</published><updated>2012-10-30T23:19:51.789+01:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Boolean" /><category scheme="http://www.blogger.com/atom/ns#" term="Integer" /><category scheme="http://www.blogger.com/atom/ns#" term="Resources" /><title type="text">Know your resources: Integers and Booleans</title><content type="html">An Android application isn't only code but comes with a bunch of &lt;a href="http://developer.android.com/guide/topics/resources/index.html"&gt;resources&lt;/a&gt;. Probably the most important resources (besides layouts and drawables) are the string resources. In conjunction with the &lt;a href="http://developer.android.com/guide/topics/resources/providing-resources.html#AlternativeResources"&gt;resource qualifiers&lt;/a&gt; they are not only perfect to externalize strings for localization but also for defining different internal configurations of your app.&lt;br /&gt;&lt;br /&gt;However sometimes string resources are overused. Especially when they are used to define boolean or integer values. Let's look at the following example. Code lines like the next ones can be found in several Android projects. For example you can find &lt;a href="https://github.com/pocmo/Yaaic/blob/master/application/src/org/yaaic/model/Settings.java#L74"&gt;some of these in Yaaic&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;/div&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/-XLbaLF9yRos/UDJrT8HzfaI/AAAAAAAAFMk/6_ctbD-1rQ8/s1600/resources_example01.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://3.bp.blogspot.com/-XLbaLF9yRos/UDJrT8HzfaI/AAAAAAAAFMk/6_ctbD-1rQ8/s1600/resources_example01.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;Loading string resources and parsing as boolean or integer&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;Or even worse in a condition without casting, doing a string comparison:&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://4.bp.blogspot.com/-IfOUvr3e9og/UDJrri2Gu3I/AAAAAAAAFMs/qB5wMlRTBzI/s1600/resources_example02.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://4.bp.blogspot.com/-IfOUvr3e9og/UDJrri2Gu3I/AAAAAAAAFMs/qB5wMlRTBzI/s1600/resources_example02.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;Loading boolean as string resource and doing string comparison&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;The code above is not wrong but instead of using string resources and parsing or comparing them it would be easier to use resources of the appropriate type. Fortunately Android allows to&amp;nbsp;define &lt;a href="http://developer.android.com/guide/topics/resources/more-resources.html#Bool"&gt;boolean&lt;/a&gt; and &lt;a href="http://developer.android.com/guide/topics/resources/more-resources.html#Integer"&gt;integer&lt;/a&gt; resources too (since API level 1). It's basically the same process as defining a string resource in XML just use the bool and integer tags:&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://4.bp.blogspot.com/-BIkUDBXNXFU/UDJr4uQMhTI/AAAAAAAAFM0/39d0Vj3jIKo/s1600/xml_resources.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://4.bp.blogspot.com/-BIkUDBXNXFU/UDJr4uQMhTI/AAAAAAAAFM0/39d0Vj3jIKo/s1600/xml_resources.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;Defining boolean and integer resources in XML&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;After that you can access these resources in code. However there are no &lt;a href="http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.1.1_r1/android/content/Context.java#304"&gt;alias methods&lt;/a&gt; on the &lt;a href="http://developer.android.com/reference/android/content/Context.html"&gt;Context&lt;/a&gt; object like for string resources. Therefore you need to get a &lt;a href="http://developer.android.com/reference/android/content/res/Resources.html"&gt;Resources&lt;/a&gt; instance first and then ask it for the defined resources.&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-bLP5ceuA0xY/UDOhAxoQunI/AAAAAAAAFNM/ONwuHscnEDw/s1600/accessing_resources.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/-bLP5ceuA0xY/UDOhAxoQunI/AAAAAAAAFNM/ONwuHscnEDw/s1600/accessing_resources.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;Accessing integer and boolean resources from code&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;img src="http://feeds.feedburner.com/~r/AndroidZeitgeist/~4/SH5Bn82Meao" height="1" width="1" alt=""/&gt;</content><link rel="replies" type="application/atom+xml" href="http://www.androidzeitgeist.com/feeds/8978580997544983984/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="http://www.androidzeitgeist.com/2012/08/know-your-resources-integers-and.html#comment-form" title="1 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/8978580997544983984" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/8978580997544983984" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/AndroidZeitgeist/~3/SH5Bn82Meao/know-your-resources-integers-and.html" title="Know your resources: Integers and Booleans" /><author><name>Sebastian Kaspari</name><uri>https://plus.google.com/112283223674539938062</uri><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://3.bp.blogspot.com/-XLbaLF9yRos/UDJrT8HzfaI/AAAAAAAAFMk/6_ctbD-1rQ8/s72-c/resources_example01.png" height="72" width="72" /><thr:total>1</thr:total><gd:extendedProperty name="commentSource" value="1" /><gd:extendedProperty name="commentModerationMode" value="FILTERED_POSTMOD" /><feedburner:origLink>http://www.androidzeitgeist.com/2012/08/know-your-resources-integers-and.html</feedburner:origLink></entry><entry><id>tag:blogger.com,1999:blog-1531829516427013333.post-7567694303001801787</id><published>2012-08-01T21:01:00.002+02:00</published><updated>2012-10-30T23:20:51.695+01:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Instant Mustache" /><category scheme="http://www.blogger.com/atom/ns#" term="mockup" /><category scheme="http://www.blogger.com/atom/ns#" term="sketch" /><category scheme="http://www.blogger.com/atom/ns#" term="UI" /><title type="text">Let's start coding ... NOT! - Instant Mustache #3</title><content type="html">&lt;span style="font-size: x-small;"&gt;This article is part of a series of articles about the development process of Instant Mustache, a fun camera app that adds mustaches to all faces using face detection.&amp;nbsp;&lt;/span&gt;&lt;a href="http://www.androidzeitgeist.com/p/instant-mustache.html" style="font-size: small;"&gt;Click here&lt;/a&gt;&lt;span style="font-size: x-small;"&gt;&amp;nbsp;to get a chronological list of all published&amp;nbsp;&lt;/span&gt;&lt;a href="http://www.androidzeitgeist.com/p/instant-mustache.html" style="font-size: small;"&gt;articles about Instant Mustache&lt;/a&gt;&lt;span style="font-size: x-small;"&gt;.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;In the &lt;a href="http://www.androidzeitgeist.com/p/instant-mustache.html"&gt;last two articles&lt;/a&gt; about Instant Mustache we've talked a lot and it's about time to start coding! Yet I’ve to disappoint you for now. There still will be no code in this article. Before we’ll start coding we’ve to define what we actually need or want. From the last article we already know there will be two screens. But what we don’t know yet is how they should look like. So how will we figure out how the (very simple) UI will look like? Of course we can fire up our code editor or visual editor but there’s a much faster way to get results: Good old paper.&lt;br /&gt;&lt;br /&gt;I won’t talk about paper prototypes or UI development and user testing here. But there’s a better resource for that. The &lt;a href="http://www.androiduipatterns.com/"&gt;Android UI Patterns blog&lt;/a&gt; has a lot of articles published covering a variety of Android UI topics. Here we are going to just sketch some screens, play around and hopefully come up with an idea of how the app will feel like when it’s finally developed. As said before I'll use paper for that. Other people may like to use software for creating mockups (&lt;a href="http://www.fluidui.com/"&gt;Fluid UI&lt;/a&gt; seems to be a nice online tool for that). It's up to you what works the best for you. For me paper is the easiest way to try different ideas without the limitations of a software tool. But the important thing is: We won't write a lot of code that we need to throw away again after we realize that our initial idea may not be as good as we thought.&lt;br /&gt;&lt;br /&gt;The following photo shows a bunch of discarded drawings I came up with during brainstorming the UI:&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;/div&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-vLyg3OA870M/UBlqUI8HXhI/AAAAAAAAEeA/E3oDqTCMi9o/s1600/screens.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" height="176" src="http://2.bp.blogspot.com/-vLyg3OA870M/UBlqUI8HXhI/AAAAAAAAEeA/E3oDqTCMi9o/s400/screens.png" width="400" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;/div&gt;&lt;b&gt;The camera screen (CameraActivity)&lt;/b&gt;&lt;br /&gt;The first screen the user will see after launching the app will be the camera screen. There are two main components we need: A camera preview and a button to take a photo. In addition to that we may want to reserve some space for future features like switching between cameras and mustaches.&lt;br /&gt;&lt;br /&gt;Let's take a look at the the final version of the camera screen as I've drawn it:&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/-ACfaRYTWXiQ/UBlrUkZlmPI/AAAAAAAAEe4/c5nw7R2I3EM/s1600/final_camera_activity.jpg" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://1.bp.blogspot.com/-ACfaRYTWXiQ/UBlrUkZlmPI/AAAAAAAAEe4/c5nw7R2I3EM/s1600/final_camera_activity.jpg" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;CameraActivity&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;The following decisions I've made during drawing and experimenting:&lt;br /&gt;&lt;br /&gt;&lt;ul&gt;&lt;li&gt;I want the app to have an &lt;a href="http://developer.android.com/design/patterns/actionbar.html"&gt;ActionBar&lt;/a&gt;. To be consistent I also want the ActionBar in the camera screen. I don't know yet if it's useful to show the app title "Instant Mustache" in the ActionBar as I've drawn it. It's quite long and may take up too much space. Furthermore the overflow menu drawn here may not be visible as we have no menu entries defined yet.&lt;/li&gt;&lt;li&gt;The camera picture will be a square. We'll have to define a picture ratio and a square may fit quite nice into the screen. The darker areas on the screen will grow or shrink depending on the screen size of the device and always center the camera preview.&lt;/li&gt;&lt;li&gt;There will be a bar at the bottom of the screen which will house the button for taking a picture. Here we will have enough space for adding more functionality in later releases.&lt;/li&gt;&lt;li&gt;Almost all activities showing a camera preview are fixed in their orientation. While moving the phone around to take a photo you don't want the phone to destroy the current activity, do the rotation and re-create the activity. Especially because the camera picture will be oriented correctly anyways because you are rotating the camera inside the phone as well. To satisfy the previous points we will fixate the orientation to portrait mode.&lt;/li&gt;&lt;/ul&gt;&lt;br /&gt;&lt;b&gt;The photo screen (PhotoActivity)&lt;/b&gt;&lt;br /&gt;After taking a photo you will see another screen showing the photo you've taken and you'll have the option to share the photo with other apps on your phone.&lt;br /&gt;&lt;br /&gt;That's how my final version of the photo screen looks like:&lt;br /&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/-ZkXDFTYoBTs/UBlv6nT4mRI/AAAAAAAAEfI/RGFW1HHcngE/s1600/final_share_activity.jpg" imageanchor="1" style="margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://3.bp.blogspot.com/-ZkXDFTYoBTs/UBlv6nT4mRI/AAAAAAAAEfI/RGFW1HHcngE/s1600/final_share_activity.jpg" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;PhotoActivtiy&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class="separator" style="clear: both; text-align: left;"&gt;The first version of this screen will be really simple:&lt;/div&gt;&lt;div class="separator" style="clear: both; text-align: left;"&gt;&lt;/div&gt;&lt;ul&gt;&lt;li&gt;At the top we'll have the ActionBar again. To share the photo we'll add a &lt;a href="http://developer.android.com/guide/topics/ui/actionbar.html#ActionProvider"&gt;ShareActionProvider&lt;/a&gt; that shows a list of available share targets as well as the most used app as an icon right beside it.&lt;/li&gt;&lt;li&gt;Below the ActionBar we'll show the taken photo and that's everything for now.&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;&lt;b&gt;What's next?&lt;/b&gt;&lt;/div&gt;&lt;div&gt;Now we've defined how our two screens will look like. In the next article we are going to launch our IDE and start writing the CameraActivity. I am curious to know what you think about the UI as described here and if you have other ideas. Let me know in the comments or on Google+.&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/AndroidZeitgeist/~4/xRHDRu4wROY" height="1" width="1" alt=""/&gt;</content><link rel="replies" type="application/atom+xml" href="http://www.androidzeitgeist.com/feeds/7567694303001801787/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="http://www.androidzeitgeist.com/2012/08/lets-start-coding-not-instant-mustache-3.html#comment-form" title="2 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/7567694303001801787" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/7567694303001801787" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/AndroidZeitgeist/~3/xRHDRu4wROY/lets-start-coding-not-instant-mustache-3.html" title="Let's start coding ... NOT! - Instant Mustache #3" /><author><name>Sebastian Kaspari</name><uri>https://plus.google.com/112283223674539938062</uri><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://2.bp.blogspot.com/-vLyg3OA870M/UBlqUI8HXhI/AAAAAAAAEeA/E3oDqTCMi9o/s72-c/screens.png" height="72" width="72" /><thr:total>2</thr:total><gd:extendedProperty name="commentSource" value="1" /><gd:extendedProperty name="commentModerationMode" value="FILTERED_POSTMOD" /><feedburner:origLink>http://www.androidzeitgeist.com/2012/08/lets-start-coding-not-instant-mustache-3.html</feedburner:origLink></entry><entry><id>tag:blogger.com,1999:blog-1531829516427013333.post-2631040260680088231</id><published>2012-07-03T20:08:00.003+02:00</published><updated>2012-10-30T23:21:12.300+01:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Instant Mustache" /><category scheme="http://www.blogger.com/atom/ns#" term="Minimal marketable app" /><category scheme="http://www.blogger.com/atom/ns#" term="roadmap" /><title type="text">Minimal marketable app - Instant Mustache #2</title><content type="html">&lt;span style="font-size: x-small;"&gt;This article is part of a series of articles about the development process of Instant Mustache, a fun camera app that adds mustaches to all faces using face detection.&amp;nbsp;&lt;/span&gt;&lt;a href="http://www.androidzeitgeist.com/p/instant-mustache.html" style="font-size: small;"&gt;Click here&lt;/a&gt;&lt;span style="font-size: x-small;"&gt;&amp;nbsp;to get a chronological list of all published&amp;nbsp;&lt;/span&gt;&lt;a href="http://www.androidzeitgeist.com/p/instant-mustache.html" style="font-size: small;"&gt;articles about Instant Mustache&lt;/a&gt;&lt;span style="font-size: x-small;"&gt;.&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;b id="internal-source-marker_0.039026354206725955"&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;In &lt;a href="http://www.androidzeitgeist.com/2012/06/instant-mustache-1-idea.html"&gt;the last article&lt;/a&gt; I described roughly the idea behind Instant Mustache. Now it's time to get more concrete. While doing so we try to follow the principle of the minimal marketable feature (MMF).&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;span style="font-family: Arial; font-size: 15px; vertical-align: baseline; white-space: pre-wrap;"&gt;Minimal marketable feature&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span id="internal-source-marker_0.039026354206725955"&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;The idea behind the minimal marketable feature is to strip all aspects of a feature that are not necessarily needed to end up with a useful (or marketable) feature. After that you'll end up with smaller features that are easier and faster to develop. This is only a very rough explanation of MMF. You can find a way better &lt;a href="http://www.upstarthq.com/2010/04/introduction-to-minimum-marketable-features-mmf/"&gt;Introduction to Minimum marketable feature&lt;/a&gt; at the upstart blog.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;b style="background-color: white; font-family: Arial; font-size: 15px; white-space: pre-wrap;"&gt;Minimal marketable app&lt;/b&gt;&lt;br /&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;In this case we are slightly modifying this idea to end up with something like a &lt;/span&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: bold; vertical-align: baseline; white-space: pre-wrap;"&gt;minimal marketable app&lt;/span&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;. This means we are going one level higher and strip all features that are not essentially required for a first releasable app. This does not mean that we won't develop these features at all but our first version will be without them enabling us to ship the app as early as possible. &lt;/span&gt;&lt;br /&gt;&lt;span style="background-color: white; font-family: Arial; font-size: 15px; white-space: pre-wrap;"&gt;Let's continue with an example. When you think about the app (&lt;a href="http://www.androidzeitgeist.com/2012/06/instant-mustache-1-idea.html"&gt;see first article&lt;/a&gt;) you will very fast come up with an idea like: The user should be able to select between a bunch of mustaches - more mustaches = more fun. But do we really need this feature when we launch the app? No, we don't. Our fun app will still be fun with one single mustache. Of course this is one of the first features we should have in the next release after the initial one.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;Another feature that's easy to come up with is switching between the cameras of a device. In most cases this means switching between back and front camera. Again for our basic app it's totally acceptable to strip this feature in the first release. The users of our app won't be able to take pictures of themselves easily and therefore they won't have much fun when there's no mustache-less friend to photograph around. Therefore we should rank this feature high as well to implement it right after the first release.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;In our case the minimal marketable feature set is an app that has two screens: A camera screen - with a camera preview and a button to take a picture - and a second screen that shows the picture you took and has an option to share this picture. The camera preview will show a live preview. Every detected face on the live preview will automatically have a mustache overlayed. If you take a picture the second screen will show the composed picture including the added mustaches. The user will be able to share the picture via the &lt;a href="http://android-developers.blogspot.co.uk/2012/02/share-with-intents.html"&gt;Android intent system&lt;/a&gt;.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;span style="font-family: Arial; font-size: 15px; vertical-align: baseline; white-space: pre-wrap;"&gt;Roadmap and Milestones&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b id="internal-source-marker_0.039026354206725955"&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;Nothing is more motivating than playing around with what you have created. In contrast nothing is more killing motivation than programming for ages without coming up with something that runs at all.&lt;/span&gt;&lt;span style="vertical-align: baseline;"&gt; &lt;/span&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;This leads us to the goal to always have a running prototype that step by step gets features added until it's ready to ship. So let's break the functionality into parts that can be implemented in order and always leave us with a working prototype that we can deploy on our phone and use at the next party.&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;ul&gt;&lt;li&gt;&lt;span style="font-family: Arial;"&gt;&lt;span style="font-size: 15px; white-space: pre-wrap;"&gt;&lt;b&gt;Milestone #1&lt;/b&gt;: &lt;/span&gt;&lt;/span&gt;&lt;b id="internal-source-marker_0.039026354206725955"&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;A camera screen that shows the camera preview and has a button to take a photo. The photos will be saved on the SD card. After that we will end up with a basic camera app that we can use to take photos on the go. Goodbye native camera app!&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;&lt;li&gt;&lt;b&gt;&lt;span style="font-family: Arial; font-size: 15px; vertical-align: baseline; white-space: pre-wrap;"&gt;Milestone #2&lt;/span&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;: &lt;/span&gt;&lt;/b&gt;&lt;b id="internal-source-marker_0.039026354206725955"&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;We add the second screen. After taking a photo we’ll switch to this second screen which shows the taken photo and has an option to share the photo.&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;&lt;li&gt;&lt;b&gt;&lt;span style="font-family: Arial; font-size: 15px; vertical-align: baseline; white-space: pre-wrap;"&gt;Milestone #3&lt;/span&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;: &lt;/span&gt;&lt;/b&gt;&lt;b id="internal-source-marker_0.039026354206725955"&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;It’s time to add the face tracking. We’ll use the face tracking and display mustaches for every face on top of the camera preview.&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;&lt;li&gt;&lt;b&gt;&lt;span style="font-family: Arial; font-size: 15px; vertical-align: baseline; white-space: pre-wrap;"&gt;Milestone #4&lt;/span&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;: &lt;/span&gt;&lt;/b&gt;&lt;b id="internal-source-marker_0.039026354206725955"&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;When taking a photo we’ll compose the final photo using the information of the face tracking. The composed photo will contain mustaches like the preview screen before. The composed photo will be saved on the SD card and shown in the second screen. We are ready to release!&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;&lt;/ul&gt;&lt;img src="http://feeds.feedburner.com/~r/AndroidZeitgeist/~4/D7VVl5yQcUE" height="1" width="1" alt=""/&gt;</content><link rel="replies" type="application/atom+xml" href="http://www.androidzeitgeist.com/feeds/2631040260680088231/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="http://www.androidzeitgeist.com/2012/07/minimal-marketable-app-instant-mustache.html#comment-form" title="2 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/2631040260680088231" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/2631040260680088231" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/AndroidZeitgeist/~3/D7VVl5yQcUE/minimal-marketable-app-instant-mustache.html" title="Minimal marketable app - Instant Mustache #2" /><author><name>Sebastian Kaspari</name><uri>https://plus.google.com/112283223674539938062</uri><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><thr:total>2</thr:total><gd:extendedProperty name="commentSource" value="1" /><gd:extendedProperty name="commentModerationMode" value="FILTERED_POSTMOD" /><feedburner:origLink>http://www.androidzeitgeist.com/2012/07/minimal-marketable-app-instant-mustache.html</feedburner:origLink></entry><entry><id>tag:blogger.com,1999:blog-1531829516427013333.post-2571392770508842451</id><published>2012-06-08T12:14:00.002+02:00</published><updated>2012-10-30T23:21:30.173+01:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Animation" /><category scheme="http://www.blogger.com/atom/ns#" term="BlinkLayout" /><category scheme="http://www.blogger.com/atom/ns#" term="Layout" /><category scheme="http://www.blogger.com/atom/ns#" term="ViewGroup" /><title type="text">Creating a MarqueeLayout with the Android Animation System</title><content type="html">When I posted the article about &lt;a href="http://www.androidzeitgeist.com/2012/05/curious-blinklayout.html"&gt;the hidden BlinkLayout&lt;/a&gt; inside Android’s LayoutInflater &lt;a href="https://plus.google.com/105344175486242358933/posts"&gt;Thierry-Dimitri Roy&lt;/a&gt; wrote on Google+:&lt;br /&gt;&lt;br /&gt;&lt;a href="http://2.bp.blogspot.com/-kpn1WJrtNRw/T9CZQxI269I/AAAAAAAADnM/Pxbd7NWhWkM/s1600/gpluscomment.png" imageanchor="1" style="clear: left; display: inline !important; margin-bottom: 1em; margin-right: 1em; text-align: center;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/-kpn1WJrtNRw/T9CZQxI269I/AAAAAAAADnM/Pxbd7NWhWkM/s1600/gpluscomment.png" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;That’s a challenge I accept! Back in the days when I worked at Jimdo we developed “&lt;a href="http://www.youtube.com/watch?v=RDNhra-6dQo"&gt;a lifeboat for GeoCities&lt;/a&gt;†which allowed users to migrate their GeoCities website to Jimdo before GeoCities finally shut down in 2009. So I’ve some kind of heart for&amp;nbsp;GeoCities&amp;nbsp;users.&lt;br /&gt;&lt;br /&gt;The &lt;a href="http://developer.android.com/guide/topics/graphics/view-animation.html"&gt;view animation system&lt;/a&gt; of Android makes it quite easy to develop something like a MarqueeLayout. All we need to do is to extend an existing ViewGroup and attach a marquee-like animation to every instance.&lt;br /&gt;&lt;br /&gt;The &lt;a href="http://developer.android.com/reference/android/view/animation/TranslateAnimation.html"&gt;TranslateAnimation&lt;/a&gt; does already exactly what we need: It moves a view from a starting position to an ending position. The two positions can either be declared absolute (position on the screen) or relative to the view or its parent. After that we only need to define the duration of the animation and Android will do the rest for us.&lt;br /&gt;&lt;br /&gt;We'll define the start and end positions of the animation by using coordinates relative to the view. When using relative coordinates you can think of our view having a size of 1x1 and it's top left corner placed at (0,0).&lt;br /&gt;&lt;br /&gt;&lt;table cellpadding="0" cellspacing="0" class="tr-caption-container" style="float: left; margin-right: 1em; text-align: left;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-002il9hIkbs/T9CiYixSw3I/AAAAAAAADnY/-04SitxZqVg/s1600/view.png" imageanchor="1" style="clear: left; margin-bottom: 1em; margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/-002il9hIkbs/T9CiYixSw3I/AAAAAAAADnY/-04SitxZqVg/s1600/view.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;View of size 1x1 at position (0,0)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;div class="separator" style="clear: both; text-align: left;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class="separator" style="clear: both; text-align: left;"&gt;If our view's width stretches to the whole width of the screen we can consider the screen's size also 1 (relative to the view's width).&lt;/div&gt;&lt;div class="separator" style="clear: both; text-align: left;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;table cellpadding="0" cellspacing="0" class="tr-caption-container" style="float: left; margin-right: 1em; text-align: left;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://4.bp.blogspot.com/-a8TAfL-nbQs/T9ClZz9tuxI/AAAAAAAADnk/qwDhuGD7DRU/s1600/view_on_screen.png" imageanchor="1" style="clear: left; margin-bottom: 1em; margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" src="http://4.bp.blogspot.com/-a8TAfL-nbQs/T9ClZz9tuxI/AAAAAAAADnk/qwDhuGD7DRU/s1600/view_on_screen.png" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;View (green) filling full width of screen (grey)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;div class="separator" style="clear: both; text-align: left;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;div style="text-align: left;"&gt;Assuming a screen size of 1 and a starting position on the right side outside of the screen gives us a relative starting position of (1,0). This will place our view exactly outside the screen (or it's parent).&lt;/div&gt;&lt;div style="text-align: left;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;table cellpadding="0" cellspacing="0" class="tr-caption-container" style="float: left; margin-right: 1em; text-align: left;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-wuSTX1TEt88/T9Cl3Z4JaxI/AAAAAAAADns/orVD4QJqXMg/s1600/start_at_1_0.png" imageanchor="1" style="clear: left; margin-bottom: 1em; margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" height="231" src="http://2.bp.blogspot.com/-wuSTX1TEt88/T9Cl3Z4JaxI/AAAAAAAADns/orVD4QJqXMg/s320/start_at_1_0.png" width="320" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;View animation starting at (1,0)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;div class="separator" style="clear: both; text-align: left;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;div style="text-align: left;"&gt;The view should move from the starting position to the left until it's outside of the screen at (-1, 0).&lt;/div&gt;&lt;div style="text-align: left;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;table cellpadding="0" cellspacing="0" class="tr-caption-container" style="float: left; margin-right: 1em; text-align: left;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://4.bp.blogspot.com/-NOoImxh16nw/T9CmIXQJRqI/AAAAAAAADn0/OF_7nGDw9Pw/s1600/end_-1_0_1.png" imageanchor="1" style="clear: left; margin-bottom: 1em; margin-left: auto; margin-right: auto;"&gt;&lt;img border="0" height="231" src="http://4.bp.blogspot.com/-NOoImxh16nw/T9CmIXQJRqI/AAAAAAAADn0/OF_7nGDw9Pw/s320/end_-1_0_1.png" width="320" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;View animation stopping at (-1,0)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;div class="separator" style="clear: both; text-align: left;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;div style="text-align: left;"&gt;The animation system will generate all transient positions needed for the animation. Given all the information above we can write a simple MarqueeLayout in a few lines:&lt;/div&gt;&lt;div style="text-align: left;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;table cellpadding="0" cellspacing="0" class="tr-caption-container" style="float: left; margin-right: 1em; text-align: left;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;span style="clear: left; margin-bottom: 1em; margin-left: auto; margin-right: auto;"&gt;&lt;a href="http://www.blogger.com/goog_1690609543"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/-OaTBb9uyHZ4/T9Cox5IvbMI/AAAAAAAADoA/bD9eXxmXu-Y/s1600/marquee_layout_code.png" /&gt;&lt;/a&gt;&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="http://source.androidzeitgeist.com/raw/marquee_layout_code.txt"&gt;MarqueeLayout.java&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;div class="separator" style="clear: both; text-align: left;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;div style="text-align: left;"&gt;That's what our MarqueeLayout looks like when adding a TextView to it:&lt;/div&gt;&lt;div style="text-align: left;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;div style="text-align: left;"&gt;&lt;div class="separator" style="clear: both; text-align: left;"&gt;&lt;iframe allowfullscreen="" frameborder="0" height="360" src="http://www.youtube.com/embed/r1LT2EpIB0s" width="480"&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div style="text-align: left;"&gt;The following code was used to set up an activity using our MarqueeLayout:&lt;/div&gt;&lt;div style="text-align: left;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;table cellpadding="0" cellspacing="0" class="tr-caption-container" style="float: left; margin-right: 1em; text-align: left;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;span style="clear: left; margin-bottom: 1em; margin-left: auto; margin-right: auto;"&gt;&lt;a href="http://www.blogger.com/goog_1690609550"&gt;&lt;img border="0" src="http://3.bp.blogspot.com/-VzGJqlPup70/T9CpDx20HUI/AAAAAAAADoI/iw61M_02ZuQ/s1600/marquee_activity.png" /&gt;&lt;/a&gt;&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="http://source.androidzeitgeist.com/raw/marquee_layout_activity.txt"&gt;MarqueeLayoutActivity.java&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;div style="text-align: left;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/AndroidZeitgeist/~4/5ogW6KCmbFc" height="1" width="1" alt=""/&gt;</content><link rel="replies" type="application/atom+xml" href="http://www.androidzeitgeist.com/feeds/2571392770508842451/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="http://www.androidzeitgeist.com/2012/06/creating-marqueelayout-with-android.html#comment-form" title="1 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/2571392770508842451" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/2571392770508842451" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/AndroidZeitgeist/~3/5ogW6KCmbFc/creating-marqueelayout-with-android.html" title="Creating a MarqueeLayout with the Android Animation System" /><author><name>Sebastian Kaspari</name><uri>https://plus.google.com/112283223674539938062</uri><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://2.bp.blogspot.com/-kpn1WJrtNRw/T9CZQxI269I/AAAAAAAADnM/Pxbd7NWhWkM/s72-c/gpluscomment.png" height="72" width="72" /><thr:total>1</thr:total><gd:extendedProperty name="commentSource" value="1" /><gd:extendedProperty name="commentModerationMode" value="FILTERED_POSTMOD" /><feedburner:origLink>http://www.androidzeitgeist.com/2012/06/creating-marqueelayout-with-android.html</feedburner:origLink></entry><entry><id>tag:blogger.com,1999:blog-1531829516427013333.post-8985540882459629006</id><published>2012-06-05T20:33:00.000+02:00</published><updated>2012-10-30T23:21:46.957+01:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Instant Mustache" /><title type="text">Instant Mustache #1 - The idea</title><content type="html">&lt;span style="font-family: Arial; font-size: 15px; vertical-align: baseline; white-space: pre-wrap;"&gt;&lt;span style="font-family: 'Times New Roman'; font-size: x-small; white-space: normal;"&gt;This article is part of a series of articles about the development process of Instant Mustache, a fun camera app that adds mustaches to all faces using face detection.&amp;nbsp;&lt;/span&gt;&lt;a href="http://www.androidzeitgeist.com/p/instant-mustache.html" style="font-family: 'Times New Roman'; font-size: small; white-space: normal;"&gt;Click here&lt;/a&gt;&lt;span style="font-family: 'Times New Roman'; font-size: x-small; white-space: normal;"&gt;&amp;nbsp;to get a chronological list of all published&amp;nbsp;&lt;/span&gt;&lt;a href="http://www.androidzeitgeist.com/p/instant-mustache.html" style="font-family: 'Times New Roman'; font-size: small; white-space: normal;"&gt;articles about Instant Mustache&lt;/a&gt;&lt;span style="font-family: 'Times New Roman'; font-size: x-small; white-space: normal;"&gt;.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style="font-family: Arial; font-size: 15px; vertical-align: baseline; white-space: pre-wrap;"&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style="font-family: Arial; font-size: 15px; vertical-align: baseline; white-space: pre-wrap;"&gt;&lt;b&gt;The idea&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span id="internal-source-marker_0.8338896373752505"&gt;&lt;span style="font-family: Arial; vertical-align: baseline;"&gt;&lt;span style="font-size: 15px; white-space: pre-wrap;"&gt;Instant Mustache is the name of an app that I'm going to develop. The idea is to show the development process of an app starting from a rough idea to a full featured app in the Android market. I'll demonstrate the process of developing the app as well as the thoughts and decisions involved. The source code will be publicly available on &lt;/span&gt;&lt;a href="http://github.com/pocmo/Instant-Mustache" style="font-size: 15px; font-weight: normal; white-space: pre-wrap;"&gt;GitHub&lt;/a&gt;&lt;span style="font-size: 15px; white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span id="internal-source-marker_0.8338896373752505"&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;Later versions of this series of articles are expected to be about topics like &lt;i&gt;refactoring&lt;/i&gt;, adding &lt;i&gt;features&lt;/i&gt;, &lt;i&gt;testing&lt;/i&gt; and maybe &lt;i&gt;monetization&lt;/i&gt;. I'll try to make everything as transparent as possible including user statistics of the Android market.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;span style="font-family: Arial; font-size: 15px; vertical-align: baseline; white-space: pre-wrap;"&gt;The app&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b id="internal-source-marker_0.8338896373752505"&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;Instant Mustache is a fun app that utilizes the face tracking feature of Android 4.x to add mustaches to all detected faces currently visible to the camera. Users will be able to take funny pictures and share them with their friends on social networks or other apps on their mobile phone. The user shouldn't need to place the mustaches herself. Instead the placing should be done automatically for every detected face.&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;div&gt;&lt;span id="internal-source-marker_0.8338896373752505"&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;This is a rough idea every developer can come up with. In the following postings we will refine the idea, plan the app with some basic wireframes and develop an elementary version that can be extended to match our planned full-featured app later.&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;&lt;br /&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style="font-family: Arial;"&gt;&lt;span style="font-size: 15px; white-space: pre-wrap;"&gt;&lt;b&gt;Do not forget the napkin&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;b id="internal-source-marker_0.8338896373752505"&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;Of course as a good entrepreneur we had this world-changing idea on the go and had only time to sketch it on a napkin to not forget it later.&lt;/span&gt;&lt;br /&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;So here is the napkin we came up with:&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div class="separator" style="clear: both; text-align: left;"&gt;&lt;/div&gt;&lt;div&gt;&lt;div class="separator" style="clear: both; text-align: left;"&gt;&lt;a href="http://1.bp.blogspot.com/-fF1hf6-tlzA/T8ZmAQiTOzI/AAAAAAAADb4/ADS5E1ODlLU/s1600/instant_mustache_tissue.jpg" imageanchor="1" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"&gt;&lt;img border="0" height="200" src="http://1.bp.blogspot.com/-fF1hf6-tlzA/T8ZmAQiTOzI/AAAAAAAADb4/ADS5E1ODlLU/s200/instant_mustache_tissue.jpg" width="200" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;b&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/AndroidZeitgeist/~4/QNOIAC4E51A" height="1" width="1" alt=""/&gt;</content><link rel="replies" type="application/atom+xml" href="http://www.androidzeitgeist.com/feeds/8985540882459629006/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="http://www.androidzeitgeist.com/2012/06/instant-mustache-1-idea.html#comment-form" title="2 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/8985540882459629006" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/8985540882459629006" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/AndroidZeitgeist/~3/QNOIAC4E51A/instant-mustache-1-idea.html" title="Instant Mustache #1 - The idea" /><author><name>Sebastian Kaspari</name><uri>https://plus.google.com/112283223674539938062</uri><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://1.bp.blogspot.com/-fF1hf6-tlzA/T8ZmAQiTOzI/AAAAAAAADb4/ADS5E1ODlLU/s72-c/instant_mustache_tissue.jpg" height="72" width="72" /><thr:total>2</thr:total><gd:extendedProperty name="commentSource" value="1" /><gd:extendedProperty name="commentModerationMode" value="FILTERED_POSTMOD" /><feedburner:origLink>http://www.androidzeitgeist.com/2012/06/instant-mustache-1-idea.html</feedburner:origLink></entry><entry><id>tag:blogger.com,1999:blog-1531829516427013333.post-9065086572488467738</id><published>2012-05-27T19:01:00.002+02:00</published><updated>2012-10-30T23:22:02.346+01:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="BlinkLayout" /><category scheme="http://www.blogger.com/atom/ns#" term="Layout" /><category scheme="http://www.blogger.com/atom/ns#" term="ViewGroup" /><title type="text">The curious BlinkLayout</title><content type="html">&lt;span id="internal-source-marker_0.19517051335424185"&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;While reading the source code of Android's &lt;a href="http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.0.3_r1/android/view/LayoutInflater.java#LayoutInflater"&gt;LayoutInflater class&lt;/a&gt; I found a hidden gem that seems to be quite unnoticed yet. Ladies and gentlemen I present you the mighty &lt;a href="http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.0.3_r1/android/view/LayoutInflater.java#LayoutInflater.BlinkLayout"&gt;BlinkLayout&lt;/a&gt;. Views that are placed inside this ViewGroup blink at a rate of 500ms.&lt;/span&gt;&lt;br /&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;The BlinkLayout is an inner class of the LayoutInflater and can therefore only be used in a XML layout that will be parsed by a LayoutInflater instance. Due to the implementation it's only possible to use it as root node using the &amp;lt;blink&amp;gt; tag inside a XML layout.&lt;/span&gt;&lt;br /&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;It seems like the BlinkLayout is available since Android 4.0 and isn't used in the Android source code I observed. Maybe it was added for debugging reasons and was forgotten later.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style="font-family: Arial;"&gt;&lt;span style="font-size: 15px; white-space: pre-wrap;"&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;object class="BLOGGER-youtube-video" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0" data-thumbnail-src="http://i.ytimg.com/vi/o_EX1mH5ZvM/0.jpg" height="266" width="320"&gt;&lt;param name="movie" value="http://www.youtube.com/v/o_EX1mH5ZvM?version=3&amp;f=user_uploads&amp;c=google-webdrive-0&amp;app=youtube_gdata" /&gt; &lt;param name="bgcolor" value="#FFFFFF" /&gt; &lt;embed width="320" height="266" src="http://www.youtube.com/v/o_EX1mH5ZvM?version=3&amp;f=user_uploads&amp;c=google-webdrive-0&amp;app=youtube_gdata" type="application/x-shockwave-flash"&gt;&lt;/embed&gt;&lt;/object&gt;&lt;/div&gt;&lt;span style="font-family: Arial;"&gt;&lt;span style="font-size: 15px; white-space: pre-wrap;"&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;T&lt;/span&gt;&lt;span style="font-family: Arial; font-size: 15px; vertical-align: baseline; white-space: pre-wrap;"&gt;he video above shows the BlinkLayout in action using the following layout XML:&lt;/span&gt;&lt;br /&gt;&lt;table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="float: left; margin-right: 1em; text-align: left;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/-2PBd4Bdwjr8/T8I-BukTXnI/AAAAAAAADZs/_z-Ajl7hOlo/s1600/blinklayout_xml.png" imageanchor="1" style="clear: left; margin-bottom: 1em; margin-left: auto; margin-right: auto;"&gt;&lt;img alt="" border="0" src="http://1.bp.blogspot.com/-2PBd4Bdwjr8/T8I-BukTXnI/AAAAAAAADZs/_z-Ajl7hOlo/s1600/blinklayout_xml.png" title="" /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="http://source.androidzeitgeist.com/raw/blinklayout_view_xml.txt"&gt;main_layout.xml&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;span style="font-family: Arial; font-size: 15px; vertical-align: baseline; white-space: pre-wrap;"&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style="font-family: Arial; font-size: 15px; vertical-align: baseline; white-space: pre-wrap;"&gt;&lt;/span&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;span style="font-family: Arial; font-size: 15px; vertical-align: baseline; white-space: pre-wrap;"&gt;T&lt;/span&gt;&lt;span style="font-family: Arial; font-size: 15px; vertical-align: baseline; white-space: pre-wrap;"&gt;he following snippet was used to inflate the layout and pass it to the activity:&lt;/span&gt;&lt;br /&gt;&lt;table cellpadding="0" cellspacing="0" class="tr-caption-container" style="float: left; margin-right: 1em; text-align: left;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style="text-align: center;"&gt;&lt;span style="clear: left; margin-bottom: 1em; margin-left: auto; margin-right: auto;"&gt;&lt;a href="http://www.blogger.com/goog_899898043"&gt;&lt;img border="0" src="http://1.bp.blogspot.com/-zgP0dePQwzw/T8JIp8nqZ0I/AAAAAAAADaY/ZP7LER-JmM0/s1600/blinklayout_code.png" /&gt;&lt;/a&gt;&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="tr-caption" style="text-align: center;"&gt;&lt;a href="http://source.androidzeitgeist.com/raw/blinklayout_activity_code.txt"&gt;BlinkLayoutActivity.java&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;div style="text-align: left;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;span style="font-family: Arial;"&gt;&lt;span style="font-size: 15px; white-space: pre-wrap;"&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style="font-family: Arial;"&gt;&lt;span style="font-size: 15px; white-space: pre-wrap;"&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style="font-family: Arial;"&gt;&lt;span style="font-size: 15px; white-space: pre-wrap;"&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="font-family: Arial; font-size: 15px; font-weight: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;&lt;br /&gt;&lt;/span&gt;&lt;img src="http://feeds.feedburner.com/~r/AndroidZeitgeist/~4/1MleyCfQAow" height="1" width="1" alt=""/&gt;</content><link rel="replies" type="application/atom+xml" href="http://www.androidzeitgeist.com/feeds/9065086572488467738/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="http://www.androidzeitgeist.com/2012/05/curious-blinklayout.html#comment-form" title="2 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/9065086572488467738" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/1531829516427013333/posts/default/9065086572488467738" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/AndroidZeitgeist/~3/1MleyCfQAow/curious-blinklayout.html" title="The curious BlinkLayout" /><author><name>Sebastian Kaspari</name><uri>https://plus.google.com/112283223674539938062</uri><email>noreply@blogger.com</email><gd:image rel="http://schemas.google.com/g/2005#thumbnail" width="32" height="32" src="//lh5.googleusercontent.com/-20YfT7gfh08/AAAAAAAAAAI/AAAAAAAArYw/l6jF4JnmC2E/s512-c/photo.jpg" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://1.bp.blogspot.com/-2PBd4Bdwjr8/T8I-BukTXnI/AAAAAAAADZs/_z-Ajl7hOlo/s72-c/blinklayout_xml.png" height="72" width="72" /><thr:total>2</thr:total><gd:extendedProperty name="commentSource" value="1" /><gd:extendedProperty name="commentModerationMode" value="FILTERED_POSTMOD" /><feedburner:origLink>http://www.androidzeitgeist.com/2012/05/curious-blinklayout.html</feedburner:origLink></entry></feed>
diff --git a/mobile/android/tests/background/junit4/resources/feed_atom_planetmozilla.xml b/mobile/android/tests/background/junit4/resources/feed_atom_planetmozilla.xml
new file mode 100644
index 0000000000..1638ed9b1e
--- /dev/null
+++ b/mobile/android/tests/background/junit4/resources/feed_atom_planetmozilla.xml
@@ -0,0 +1,4996 @@
+<?xml version="1.0"?>
+<!--
+ Snapshot from http://planet.mozilla.org/atom.xml
+-->
+<feed xmlns="http://www.w3.org/2005/Atom" xmlns:planet="http://planet.intertwingly.net/" xmlns:indexing="urn:atom-extension:indexing" indexing:index="no"><access:restriction xmlns:access="http://www.bloglines.com/about/specs/fac-1.0" relationship="deny"/>
+ <title>Planet Mozilla</title>
+ <updated>2016-01-26T19:01:54Z</updated>
+ <generator uri="http://intertwingly.net/code/venus/">Venus</generator>
+ <author>
+ <name>Planet Mozilla Module Team</name>
+ <email>planet@mozilla.org</email>
+ </author>
+ <id>http://planet.mozilla.org/atom.xml</id>
+ <link href="http://planet.mozilla.org/atom.xml" rel="self" type="application/atom+xml"/>
+ <link href="http://planet.mozilla.org/" rel="alternate"/>
+
+ <entry xml:lang="en-US">
+ <id>https://quality.mozilla.org/?p=49454</id>
+ <link href="https://quality.mozilla.org/2016/01/firefox-45-0-beta-3-testday-february-5th/" rel="alternate" type="text/html"/>
+ <title>Firefox 45.0 Beta 3 Testday, February 5th</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">Hello Mozillians, We are happy to announce that Friday, February 5th, we are organizing Firefox 45.0 Beta 3 Testday. We will be focusing our testing on the following features: Search Refactoring, Synced Tabs Menu, Text to Speech and Grouped Tabs Migration. Check out the … <a class="go" href="https://quality.mozilla.org/2016/01/firefox-45-0-beta-3-testday-february-5th/">Continue reading</a></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>Hello Mozillians,</p>
+ <p>We are happy to announce that <strong>Friday, February 5th</strong>, we are organizing <strong>Firefox 45.0 Beta 3 Testday</strong>. We will be focusing our testing on the following features: <em>Search Refactoring, Synced Tabs Menu, Text to Speech and Grouped Tabs Migration</em>. Check out the detailed instructions via <a href="https://public.etherpad-mozilla.org/p/testday-20160205" target="_blank">this etherpad</a>.</p>
+ <p>No previous testing experience is required, so feel free to join us on <strong><a href="http://widget01.mibbit.com/?server=irc.mozilla.org&amp;channel=%23qa">#qa IRC channel</a></strong> where our moderators will offer you guidance and answer your questions.</p>
+ <p>Join us and help us make Firefox better! See you on <strong>Friday</strong>!</p></div>
+ </content>
+ <updated>2016-01-26T14:40:55Z</updated>
+ <category term="Community"/>
+ <category term="Firefox Team"/>
+ <category term="QMO News"/>
+ <author>
+ <name>vasilica.mihasca</name>
+ </author>
+ <source>
+ <id>https://quality.mozilla.org</id>
+ <link href="https://quality.mozilla.org/feed/" rel="self" type="application/rss+xml"/>
+ <link href="https://quality.mozilla.org" rel="alternate" type="text/html"/>
+ <subtitle>Driving quality across Mozilla with data, metrics and a strong community focus</subtitle>
+ <title>Mozilla Quality Assurance</title>
+ <updated>2016-01-26T14:46:40Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en">
+ <id>http://dlawrence.wordpress.com/?p=29</id>
+ <link href="https://dlawrence.wordpress.com/2016/01/26/happy-bmo-push-day-4/" rel="alternate" type="text/html"/>
+ <title>Happy BMO Push Day!</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">the following changes have been pushed to bugzilla.mozilla.org: [1240575] Update form.reps.budget [1226028] API for batching MozReview requests discuss these changes on mozilla.tools.bmo.<img alt="" border="0" height="1" src="https://pixel.wp.com/b.gif?host=dlawrence.wordpress.com&amp;blog=58816&amp;post=29&amp;subd=dlawrence&amp;ref=&amp;feed=1" width="1"/></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>the following changes have been pushed to bugzilla.mozilla.org:</p>
+ <ul>
+ <li>[<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1240575" target="_blank">1240575</a>] Update form.reps.budget</li>
+ <li>[<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1226028" target="_blank">1226028</a>] API for batching MozReview requests</li>
+ </ul>
+ <p>discuss these changes on <a href="https://lists.mozilla.org/listinfo/tools-bmo" target="_blank">mozilla.tools.bmo</a>.</p><br/> <a href="http://feeds.wordpress.com/1.0/gocomments/dlawrence.wordpress.com/29/" rel="nofollow"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/comments/dlawrence.wordpress.com/29/"/></a> <img alt="" border="0" height="1" src="https://pixel.wp.com/b.gif?host=dlawrence.wordpress.com&amp;blog=58816&amp;post=29&amp;subd=dlawrence&amp;ref=&amp;feed=1" width="1"/></div>
+ </content>
+ <updated>2016-01-26T14:27:50Z</updated>
+ <category term="Uncategorized"/>
+ <author>
+ <name>dlawrence</name>
+ </author>
+ <source>
+ <id>https://dlawrence.wordpress.com</id>
+ <logo>https://s2.wp.com/i/buttonw-com.png</logo>
+ <link href="https://dlawrence.wordpress.com/feed/" rel="self" type="application/rss+xml"/>
+ <link href="https://dlawrence.wordpress.com" rel="alternate" type="text/html"/>
+ <link href="https://dlawrence.wordpress.com/osd.xml" rel="search" title="Dave's Ramblings" type="application/opensearchdescription+xml"/>
+ <link href="https://dlawrence.wordpress.com/?pushpress=hub" rel="hub" type="text/html"/>
+ <subtitle>Thoughts somehow related to web, linux, mobile and other things I am interested in</subtitle>
+ <title>Dave's Ramblings</title>
+ <updated>2016-01-26T14:31:40Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>http://blog.mozilla.org/tanvi/?p=198</id>
+ <link href="https://blog.mozilla.org/tanvi/2016/01/26/updated-firefox-security-indicators/" rel="alternate" type="text/html"/>
+ <title>Updated Firefox Security Indicators</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">This article has been coauthored by Aislinn Grigas, Senior Interaction Designer, Firefox Desktop Cross posting with Mozilla’s Security Blog November 3, 2015 Over the past few months, Mozilla has been improving the user experience of our privacy and security features in Firefox. One specific initiative has focused on the feedback shown in our address bar… <a class="more-link" href="https://blog.mozilla.org/tanvi/2016/01/26/updated-firefox-security-indicators/" title="Read the rest of &#x201C;Updated Firefox Security Indicators&#x201D;">Read more</a></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p><em>This article has been coauthored by Aislinn Grigas, Senior Interaction Designer, Firefox Desktop</em><br/>
+ <em>Cross posting with <a href="https://blog.mozilla.org/security/2015/11/03/updated-firefox-security-indicators-2/">Mozilla’s Security Blog</a></em></p>
+ <p>November 3, 2015</p>
+ <p>Over the past few months, Mozilla has been improving the user experience of our privacy and security features in Firefox. One specific initiative has focused on the feedback shown in our address bar around a site’s security. The major changes are highlighted below along with the rationale behind each change.</p>
+ <p><a href="https://blog.mozilla.org/security/files/2015/10/combo-graph21.png"><img alt="" class="alignnone wp-image-2045 size-full" height="914" src="https://blog.mozilla.org/security/files/2015/10/combo-graph21.png" width="1518"/></a></p>
+ <h3>Change to DV Certificate treatment in the address bar</h3>
+ <p>Color and iconography is commonly used today to communicate to users when a site is secure. The most widely used patterns are coloring a lock icon and parts of the address bar green. This treatment has a straightforward rationale given green = good in most cultures. Firefox has historically used two different color treatments for the lock icon – a gray lock for <a href="https://en.wikipedia.org/wiki/Domain-validated_certificate">Domain-validated (DV) certificates</a> and a green lock for <a href="https://en.wikipedia.org/wiki/Extended_Validation_Certificate">Extended Validation (EV) certificates</a>. The average user is likely not going to understand this color distinction between EV and DV certificates. The overarching message we want users to take from both certificate states is that their connection to the site is secure. We’re therefore updating the color of the lock when a DV certificate is used to match that of an EV certificate.</p>
+ <p>Although the same green icon will be used, the UI for a site using EV certificates will continue to differ from a site using a DV certificate. Specifically, EV certificates are used when <a href="https://en.wikipedia.org/wiki/Certificate_authority">Certificate Authorities (CA)</a> verify the owner of a domain. Hence, we will continue to include the organization name verified by the CA in the address bar.</p>
+ <h3>Changes to Mixed Content Blocker UI on HTTPS sites</h3>
+ <p>A second change we’re introducing addresses what happens when a page served over a secure connection contains <a href="https://developer.mozilla.org/en-US/docs/Security/MixedContent">Mixed Content</a>. Firefox’s Mixed Content Blocker proactively blocks <a href="https://developer.mozilla.org/en-US/docs/Security/MixedContent#Mixed_active_content">Mixed Active Content</a> by default. Users historically saw a <a href="https://people.mozilla.org/~tvyas/FigureA.jpg">shield icon</a> when Mixed Active Content was blocked and were given the option to disable the protection.</p>
+ <p>Since the Mixed Content state is closely tied to site security, the information should be communicated in one place instead of having two separate icons. Moreover, we have seen that the <a href="https://telemetry.mozilla.org/new-pipeline/dist.html#!cumulative=0&amp;end_date=2015-09-17&amp;keys=__none__!__none__!__none__&amp;max_channel_version=beta%252F41&amp;measure=MIXED_CONTENT_UNBLOCK_COUNTER&amp;min_channel_version=null&amp;product=Firefox&amp;sanitize=1&amp;sort_keys=submissions&amp;start_date=2015-08-11&amp;table=0&amp;trim=1&amp;use_submission_date=0">number of times users override mixed content protection</a> is slim, and hence the need for dedicated mixed content iconography is diminishing. Firefox is also using the shield icon for another feature in <a href="https://support.mozilla.org/en-US/kb/private-browsing-use-firefox-without-history">Private Browsing Mode</a> and we want to avoid making the iconography ambiguous.</p>
+ <p>The updated design that ships with Firefox 42 combines the lock icon with a warning sign which represents Mixed Content. When Firefox blocks Mixed Active Content, we retain the green lock since the HTTP content is blocked and hence the site remains secure.</p>
+ <p>For users who want to learn more about a site’s security state, we have added an informational panel to further explain differences in page security. This panel appears anytime a user clicks on the lock icon in the address bar.</p>
+ <p>Previously users could <a href="https://people.mozilla.org/~tvyas/FigureB.jpg">click on the shield icon</a> in the rare case they needed to override mixed content protection. With this new UI, users can still do this by clicking the arrow icon to expose more information about the site security, along with a disable protection button.</p>
+ <div class="wp-caption alignnone" id="attachment_2034" style="width: 557px;"><a href="https://blog.mozilla.org/security/files/2015/10/mixed-active-content-click-and-subpanel.png"><img alt="mixed active content click and subpanel" class="wp-image-2034 " height="176" src="https://blog.mozilla.org/security/files/2015/10/mixed-active-content-click-and-subpanel.png" width="547"/></a><p class="wp-caption-text">Users can click the lock with warning icon and proceed to disable Mixed Content Protection.</p></div>
+ <h3/>
+ <h3>Loading Mixed Passive Content on HTTPS sites</h3>
+ <p>There is a second category of Mixed Content called <a href="https://developer.mozilla.org/en-US/docs/Security/MixedContent#Mixed_passivedisplay_content">Mixed Passive Content</a>. Firefox does not block Mixed Passive Content by default. However, when it is loaded on an HTTPS page, we let the user know with iconography and text. In previous versions of Firefox, we used a gray warning sign to reflect this case.</p>
+ <p>We have updated this iconography in Firefox 42 to a gray lock with a yellow warning sign. We degrade the lock from green to gray to emphasize that the site is no longer completely secure. In addition, we use a vibrant color for the warning icon to amplify that there is something wrong with the security state of the page.</p>
+ <p><a href="https://blog.mozilla.org/security/files/2015/10/mixed-passive-click1.png"><img alt="" class="alignnone wp-image-2042 " height="100" src="https://blog.mozilla.org/security/files/2015/10/mixed-passive-click1-600x221.png" width="268"/></a></p>
+ <p>We also use this iconography when the certificate or TLS connection used by the website relies on deprecated cryptographic algorithms.</p>
+ <p>The above changes will be rolled out in Firefox 42. Overall, the design improvements make it simpler for our users to understand whether or not their interactions with a site are secure.</p>
+ <h3>Firefox Mobile</h3>
+ <p>We have made similar changes to the site security indicators in Firefox for Android, which you can learn more about <a href="https://support.mozilla.org/en-US/kb/mixed-content-blocker-firefox-android#w_how-do-i-know-if-a-page-has-mixed-content">here</a>.</p></div>
+ </content>
+ <updated>2016-01-26T05:58:29Z</updated>
+ <category term="Browser Security"/>
+ <author>
+ <name>Tanvi Vyas</name>
+ </author>
+ <source>
+ <id>https://blog.mozilla.org/tanvi</id>
+ <link href="https://blog.mozilla.org/tanvi/feed/" rel="self" type="application/rss+xml"/>
+ <link href="https://blog.mozilla.org/tanvi" rel="alternate" type="text/html"/>
+ <subtitle>Security Engineering - @TanviHacks</subtitle>
+ <title>Tanvi's Blog</title>
+ <updated>2016-01-26T06:16:19Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>https://blog.mozilla.org/?p=9166</id>
+ <link href="https://blog.mozilla.org/blog/2016/01/25/firefox-can-now-get-push-notifications-from-your-favorite-sites/" rel="alternate" type="text/html"/>
+ <title>Firefox Can Now Get Push Notifications From Your Favorite Sites</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">Firefox for Windows, Mac and Linux now lets you choose to receive push notifications from websites if you give them permission. This is similar to Web notifications, except now you can receive notifications for websites even when they’re not loaded … <a class="go" href="https://blog.mozilla.org/blog/2016/01/25/firefox-can-now-get-push-notifications-from-your-favorite-sites/">Continue reading</a></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>Firefox for Windows, Mac and Linux now lets you choose to receive push notifications from websites if you give them permission. This is similar to Web notifications, except now you can receive notifications for websites even when they’re not loaded in a tab. This is super useful for websites like email, weather, social networks and shopping, which you might check frequently for updates.</p>
+ <p>You can manage your notifications in the Control Center by clicking the <img alt="'I' Icon" class="alignnone wp-image-9170" height="10" src="https://blog.mozilla.org/wp-content/uploads/2016/01/IIcon.png" width="10"/> icon on the left side of the address bar.</p>
+ <p><b>Push Notifications for Web Developers</b><br/>
+ To make this functionality possible, Mozilla helped establish the Web Push W3C standard that’s gaining momentum across the Web. We also continue to explore the new design pattern known as<a href="https://blog.mozilla.org/futurereleases/2015/11/17/extending-the-webs-capabilities-in-firefox-and-beyond/"> Progressive Web Apps</a>. If you’re a developer who wants to implement push notifications on your site, you can learn more in this<a href="https://hacks.mozilla.org/2016/01/web-push-arrives-in-firefox-44/"> Hacks blog post</a>.</p>
+ <p><b>More information:</b></p>
+ <ul>
+ <li>Download<a href="https://www.mozilla.org/firefox/new/"> Firefox for Windows, Mac, Linux</a></li>
+ <li>Release Notes for<a href="https://www.mozilla.org/firefox/44.0/releasenotes/"> Firefox for Windows, Mac, Linux</a></li>
+ <li>Download<a href="https://play.google.com/store/apps/details?id=org.mozilla.firefox&amp;referrer=utm_source%3Dmozilla%26utm_medium"> Firefox for Android</a></li>
+ <li>Release Notes for<a href="https://www.mozilla.org/firefox/android/44.0/releasenotes/"> Firefox for Android</a></li>
+ </ul></div>
+ </content>
+ <updated>2016-01-26T01:56:50Z</updated>
+ <category term="Firefox"/>
+ <author>
+ <name>Mozilla</name>
+ </author>
+ <source>
+ <id>https://blog.mozilla.org</id>
+ <link href="https://blog.mozilla.org/feed/" rel="self" type="application/rss+xml"/>
+ <link href="https://blog.mozilla.org" rel="alternate" type="text/html"/>
+ <subtitle>News, notes and ramblings from the Mozilla project</subtitle>
+ <title>The Mozilla Blog</title>
+ <updated>2016-01-26T16:01:47Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en">
+ <id>http://benoitgirard.wordpress.com/?p=651</id>
+ <link href="https://benoitgirard.wordpress.com/2016/01/25/using-recordreplay-to-investigate-intermittent-oranges/" rel="alternate" type="text/html"/>
+ <title>Using RecordReplay to investigate intermittent oranges</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">This is a quick write up to summarize my, and Jeff’s, experience, using RR to debug a fairly rare intermittent reftest failure. There’s still a lot of be learned about how to use RR effectively so I’m hoping sharing this will help others. Finding the root of the bad pixel First given a offending pixel […]<img alt="" border="0" height="1" src="https://pixel.wp.com/b.gif?host=benoitgirard.wordpress.com&amp;blog=12112851&amp;post=651&amp;subd=benoitgirard&amp;ref=&amp;feed=1" width="1"/></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>This is a quick write up to summarize my, and Jeff’s, experience, using RR to debug a <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1226748">fairly rare intermittent reftest failure</a>. There’s still a lot of be learned about how to use RR effectively so I’m hoping sharing this will help others.</p>
+ <h3>Finding the root of the bad pixel</h3>
+ <p>First given a offending pixel I was able to set a breakpoint on it using <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Hacking_Tips#rr_with_reftest">these instructions</a>. Next using <a href="https://github.com/jrmuizel/rr-dataflow">rr-dataflow</a> I was able to step from the offending bad pixel to the display item responsible for this pixel. Let me emphasize this for a second since it’s incredibly impressive. rr + rr-dataflow allows you to go from a buffer, through an intermediate surface, to the compositor on another thread, through another intermediate surface, back to the main thread and eventually back to the relevant display item. All of this was automated except for when the two pixels are blended together which is logically ambiguous. The speed at which rr was able to reverse continue through this execution was very impressive!</p>
+ <p>Here’s the trace of this part: <a href="https://gist.github.com/bgirard/e707e9b97556b500d9ae">rr-trace-reftest-pixel-origin</a></p>
+ <h3>Understanding the decoding step</h3>
+ <p>From here I started comparing a replay of a failing test and a non failing step and it was clear that the DisplayList was different. In one we have a nsDisplayBackgroundColor in the other we don’t. From here I was able to step through the decoder and compare the sequence. This was very useful in ruling out possible theories. It was easy to step forward and backwards in the good and bad replay debugging sessions to test out various theories about race conditions and understanding at which part of the decode process the image was rejected. It turned out that we sent two decodes, one for the metadata that is used to sized the frame tree and the other one for the image data itself.</p>
+ <h3>Comparing the frame tree</h3>
+ <p>In hindsight, it would have been more effective to start debugging this test by looking at the frame tree (and I imagine for other tests looking at the display list and layer tree) first would have been a quicker start. It works even better if you have a good and a bad trace to compare the difference in the frame tree. From here, I found that the difference in the layer tree came from a change hint that wasn’t guaranteed to come in before the draw.</p>
+ <p>The problem is now well understood: When we do a sync decode on reftest draw, if there’s an image error we wont flush the style hints since we’re already too deep in the painting pipeline.</p>
+ <h3>Take away</h3>
+ <ul>
+ <li>Finding the root cause of a bad pixel is very easy, and fast, to do using rr-dataflow.</li>
+ <li>However it might be better to look for obvious frame tree/display list/layer tree difference(s) first.</li>
+ <li>Debugging a replay is a lot simpler then debugging against non-determinist re-runs and a lot less frustrating too.</li>
+ <li>rr is really useful for race conditions, especially rare ones.</li>
+ </ul><br/> <a href="http://feeds.wordpress.com/1.0/gocomments/benoitgirard.wordpress.com/651/" rel="nofollow"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/comments/benoitgirard.wordpress.com/651/"/></a> <img alt="" border="0" height="1" src="https://pixel.wp.com/b.gif?host=benoitgirard.wordpress.com&amp;blog=12112851&amp;post=651&amp;subd=benoitgirard&amp;ref=&amp;feed=1" width="1"/></div>
+ </content>
+ <updated>2016-01-25T22:16:01Z</updated>
+ <category term="Graphics"/>
+ <category term="Mozilla"/>
+ <author>
+ <name>benoitgirard</name>
+ </author>
+ <source>
+ <id>https://benoitgirard.wordpress.com</id>
+ <logo>https://s2.wp.com/i/buttonw-com.png</logo>
+ <link href="https://benoitgirard.wordpress.com/feed/" rel="self" type="application/rss+xml"/>
+ <link href="https://benoitgirard.wordpress.com" rel="alternate" type="text/html"/>
+ <link href="https://benoitgirard.wordpress.com/osd.xml" rel="search" title="Benoit Girard's Blog" type="application/opensearchdescription+xml"/>
+ <link href="https://benoitgirard.wordpress.com/?pushpress=hub" rel="hub" type="text/html"/>
+ <subtitle>My Programming Experiences</subtitle>
+ <title>Benoit Girard's Blog</title>
+ <updated>2016-01-25T22:30:59Z</updated>
+ </source>
+ </entry>
+
+ <entry>
+ <id>http://blog.servo.org/2016/01/25/twis-48/</id>
+ <link href="http://blog.servo.org/2016/01/25/twis-48/" rel="alternate" type="text/html"/>
+ <title>These Weeks In Servo 48</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>In the <a href="https://github.com/pulls?page=1&amp;q=is%3Apr+is%3Amerged+closed%3A2016-01-11..2016-01-25+user%3Aservo">last two weeks</a>, we landed 130 PRs in the Servo organization’s repositories.</p>
+
+ <p>After months of work by vlad and many others, Windows support <a href="https://github.com/servo/servo/pull/9385">landed</a>! Thanks to everyone who contributed fixes, tests, reviews, and even encouragement (or impatience!) to help us make this happen.</p>
+
+ <h3 id="notable-additions">Notable Additions</h3>
+
+ <ul>
+ <li>nikki <a href="https://github.com/servo/servo/pull/9391">added</a> tests and support for checking the Fetch redirect count</li>
+ <li>glennw <a href="https://github.com/servo/servo/pull/9359">implemented</a> horizontal scrolling with arrow keys</li>
+ <li>simon <a href="https://github.com/servo/servo/pull/9333">created</a> a script that parses all of the CSS properties parsed by Servo</li>
+ <li>ms2ger <a href="https://github.com/servo/servo/pull/9293">removed</a> the legacy reftest framework</li>
+ <li>fernando <a href="https://github.com/servo/crowbot/pull/33">made</a> crowbot able to rejoin IRC after it accidentally floods the channel</li>
+ <li>jack <a href="https://github.com/servo/saltfs/pull/193">added</a> testing the <code>geckolib</code> target to our CI</li>
+ <li>antrik <a href="https://github.com/servo/ipc-channel/pull/25">fixed</a> transfer corruption in ipc-channel on 32-bit</li>
+ <li>valentin <a href="https://github.com/servo/rust-url/pull/119">added</a> and simon <a href="https://github.com/servo/rust-url/pull/152">extended</a> IDNA support in rust-url, which is required for both web and Gecko compatibility</li>
+ </ul>
+
+ <h3 id="new-contributors">New Contributors</h3>
+
+ <ul>
+ <li><a href="https://github.com/Chandler">Chandler Abraham</a></li>
+ <li><a href="https://github.com/DarinM223">Darin Minamoto</a></li>
+ <li><a href="https://github.com/coder543">Josh Leverette</a></li>
+ <li><a href="https://github.com/shssoichiro">Joshua Holmer</a></li>
+ <li><a href="https://github.com/therealkbhat">Kishor Bhat</a></li>
+ <li><a href="https://github.com/MonsieurLanza">Lanza</a></li>
+ <li><a href="https://github.com/mattkuo">Matthew Kuo</a></li>
+ <li><a href="https://github.com/waterlink">Oleksii Fedorov</a></li>
+ <li><a href="https://github.com/stspyder">St.Spyder</a></li>
+ <li><a href="https://github.com/vvuk">Vladimir Vukicevic</a></li>
+ <li><a href="https://github.com/apopiak">apopiak</a></li>
+ <li><a href="https://github.com/askalski">askalski</a></li>
+ </ul>
+
+ <h3 id="screenshot">Screenshot</h3>
+
+ <p>Screencast of this post being upvoted on reddit… from Windows!</p>
+
+ <p><img alt="(screencast)" src="http://blog.servo.org/images/upvote-windows.gif" title="Screencast of upvoting on Reddit on Windows."/></p>
+
+ <h3 id="meetings">Meetings</h3>
+
+ <p>We had a <a href="https://github.com/servo/servo/wiki/Meeting-2016-01-11">meeting</a> on some CI-related woes, documenting tags and mentoring, and dependencies for the style subsystem.</p></div>
+ </summary>
+ <updated>2016-01-25T20:30:00Z</updated>
+ <source>
+ <id>http://blog.servo.org/</id>
+ <author>
+ <name>The Servo Blog</name>
+ </author>
+ <link href="http://blog.servo.org/" rel="alternate" type="text/html"/>
+ <link href="http://blog.servo.org/feed.xml" rel="self" type="application/rss+xml"/>
+ <title>Servo Blog</title>
+ <updated>2016-01-26T02:01:45Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>https://air.mozilla.org/mozilla-weekly-project-meeting-20160125/</id>
+ <link href="https://air.mozilla.org/mozilla-weekly-project-meeting-20160125/" rel="alternate" type="text/html"/>
+ <title>Mozilla Weekly Project Meeting, 25 Jan 2016</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>
+ <img alt="Mozilla Weekly Project Meeting" class="wp-post-image" height="90" src="https://air.cdn.mozilla.net/media/cache/e9/4f/e94fbd7f8df916c75a60e63a85b9168c.png" width="160"/>
+ The Monday Project Meeting
+ </p></div>
+ </summary>
+ <updated>2016-01-25T19:00:00Z</updated>
+ <author>
+ <name>Air Mozilla</name>
+ </author>
+ <source>
+ <id>https://air.mozilla.org/</id>
+ <link href="https://air.mozilla.org/" rel="alternate" type="text/html"/>
+ <link href="https://air.mozilla.org/" rel="self" type="application/rss+xml"/>
+ <rights>Except where otherwise noted, content on this site is licensed under the Creative Commons Attribution Share-Alike License v3.0 or any later version.</rights>
+ <subtitle>Air Mozilla is the Internet multimedia presence of Mozilla, with live and pre-recorded shows, interviews, news snippets, tutorial videos, and features about the Mozilla community.</subtitle>
+ <title>Air Mozilla</title>
+ <updated>2016-01-25T20:31:50Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>http://blog.mozilla.org/community/?p=2292</id>
+ <link href="http://blog.mozilla.org/community/2016/01/25/firefox-44-new-contributors/" rel="alternate" type="text/html"/>
+ <title>Firefox 44 new contributors</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">With the release of Firefox 44, we are pleased to welcome the 28 developers who contributed their first code change to Firefox in this release, 23 of whom were brand new volunteers! Please join us in thanking each of these … <a class="go" href="http://blog.mozilla.org/community/2016/01/25/firefox-44-new-contributors/">Continue reading</a></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>With the release of Firefox 44, we are pleased to welcome the <strong>28 developers</strong> who contributed their first code change to Firefox in this release, <strong>23</strong> of whom were brand new volunteers! Please join us in thanking each of these diligent and enthusiastic individuals, and take a look at their contributions:</p>
+ <ul>
+ <li>mkm: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1208124">1208124</a></li>
+ <li>Aditya Motwani: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1209087">1209087</a></li>
+ <li>Aniket Vyas: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1197309">1197309</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1197315">1197315</a></li>
+ <li>Chirath R: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1216941">1216941</a></li>
+ <li>Christiane Ruetten: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1209091">1209091</a></li>
+ <li>Fernando Campo: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1199815">1199815</a></li>
+ <li>Grisha Pushkov: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=994555">994555</a></li>
+ <li>Guang-De Lin: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1150305">1150305</a></li>
+ <li>Hassen ben tanfous: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1074804">1074804</a></li>
+ <li>Helen V. Holmes: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1205046">1205046</a></li>
+ <li>Henrik Tjäder: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1161698">1161698</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1209912">1209912</a></li>
+ <li>Johann Hofmann: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1192432">1192432</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1198405">1198405</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1204072">1204072</a></li>
+ <li>Kapeel Sable: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1212171">1212171</a></li>
+ <li>Manav Batra: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1202618">1202618</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1212280">1212280</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1214626">1214626</a></li>
+ <li>Manuel Casas Barrado: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1172662">1172662</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1193674">1193674</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1200693">1200693</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1203298">1203298</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1205684">1205684</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1212331">1212331</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1212338">1212338</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1214582">1214582</a></li>
+ <li>Matt Howell: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1208626">1208626</a></li>
+ <li>Matthew Turnbull: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1213620">1213620</a></li>
+ <li>Olivier Yiptong: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1210936">1210936</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1210940">1210940</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1213078">1213078</a></li>
+ <li>Piotr Tworek: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1209446">1209446</a></li>
+ <li>Rocik: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1070719">1070719</a></li>
+ <li>Roland Sako: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1207733">1207733</a></li>
+ <li>Ronald Claveau: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1207266">1207266</a></li>
+ <li>Sanchit Nevgi: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1205181">1205181</a></li>
+ <li>Shaif Chowdhury: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1185606">1185606</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1208121">1208121</a></li>
+ <li>Shubham Jain: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1208470">1208470</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1208705">1208705</a></li>
+ <li>Stanislas Daniel Claude Dolcini: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1147197">1147197</a></li>
+ <li>Stephanie Ouillon: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1178533">1178533</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1201626">1201626</a></li>
+ <li>Tim Huang: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1181489">1181489</a></li>
+ <li>simplyblue24: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1218204">1218204</a></li>
+ </ul></div>
+ </content>
+ <updated>2016-01-25T16:21:33Z</updated>
+ <category term="Spotlight"/>
+ <author>
+ <name>Josh Matthews</name>
+ </author>
+ <source>
+ <id>http://blog.mozilla.org/community</id>
+ <link href="http://blog.mozilla.org/community/feed/" rel="self" type="application/rss+xml"/>
+ <link href="http://blog.mozilla.org/community" rel="alternate" type="text/html"/>
+ <subtitle>News and notes from and for the Mozilla community.</subtitle>
+ <title>about:community</title>
+ <updated>2016-01-25T16:31:42Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>tag:literaci.es,2014:Post/digital-skills-curriculum</id>
+ <link href="http://literaci.es/digital-skills-curriculum" rel="alternate" type="text/html"/>
+ <title xml:lang="en-US">3 things to consider when designing a digital skills framework</title>
+ <content type="xhtml" xml:lang="en-US"><div xmlns="http://www.w3.org/1999/xhtml"><p><img alt="Learning to credential" src="http://bryanmmathers.com/wp-content/uploads/2016/01/learning-to-credential.png"/></p>
+
+ <p>The image above was created by <a href="http://bryanmmathers.com/learning-to-credential" rel="nofollow">Bryan Mathers</a> for our <a href="https://goo.gl/QqwUKP" rel="nofollow">presentation</a> at <a href="http://bettshow.com" rel="nofollow">BETT</a> last week. It shows the way that, in broad brushstrokes, learning design <em>should</em> happen. Before microcredentials such as <a href="http://openbadges.org" rel="nofollow">Open Badges</a> this was a difficult thing to do as both the credential and the assessment are usually given to educators. The flow tends to go <em>backwards</em> from credentials instead of forwards from what we want people to learn.</p>
+
+ <p>But what if you really <em>were</em> starting from scratch? How could you design a digital skills framework that contains knowledge, skills, and behaviours worth learning? Having written my <a href="http://neverendingthesis.com" rel="nofollow">thesis</a> on digital literacies and led Mozilla’s <a href="https://teach.mozilla.org/activities/web-literacy/" rel="nofollow">Web Literacy Map</a> for a couple of years, I’ve got some suggestions. </p>
+ <h3>
+ <a class="head_anchor" href="http://literaci.es/feed#1-define-your-audience" name="1-define-your-audience" rel="nofollow"> </a>1. Define your audience</h3>
+ <p>One of the most important things to define is who your audience is for your digital skills framework. Is it for learners to read? Who are they? How old are they? Are you excluding anyone on purpose? Why / why not?</p>
+
+ <p>You might want to do some research and work around <a href="https://en.wikipedia.org/wiki/Persona_(user_experience)" rel="nofollow">user personas</a> as part of a user-centred design approach. This ensures you’re designing for real people instead of figments of your imagination (or, worse still, in line with your prejudices).</p>
+
+ <p>It’s also good practice to make the language used in the skills framework as precise as possible. Jargon is technical language used for the sake of it. There may be times when it’s impossible not to use a word (e.g. ’<a href="https://en.wikipedia.org/wiki/Meme" rel="nofollow">meme</a>’). If you do this then link to a definition or include a glossary. It’s also useful to check the ‘reading level’ of your framework and, if you really want a challenge, try using <a href="http://splasho.com/upgoer5/" rel="nofollow">Up-Goer Five</a> language.</p>
+ <h3>
+ <a class="head_anchor" href="http://literaci.es/feed#2-focus-on-verbs" name="2-focus-on-verbs" rel="nofollow"> </a>2. Focus on verbs</h3>
+ <p>It’s extremely easy, when creating a framework for learning, to fall into the 'knowledge trap’. Our aim when creating the raw materials from which someone can build a curriculum is to focus on <em>action</em>. Knowledge should make a difference in practice.</p>
+
+ <p>One straightforward way to ensure that you’re focusing on action rather than head knowledge is to use <strong>verbs</strong> when constructing your digital skills framework. If you’re familiar with <a href="https://en.wikipedia.org/wiki/Bloom%27s_taxonomy" rel="nofollow">Bloom’s Taxonomy</a>, then you may find <a href="http://byrdseed.com/differentiator/" rel="nofollow">The Differentiator</a> useful. This pairs verbs with the various levels of Bloom’s.</p>
+ <h3>
+ <a class="head_anchor" href="http://literaci.es/feed#3-add-version-numbers" name="3-add-version-numbers" rel="nofollow"> </a>3. Add version numbers</h3>
+ <p>A framework needs to be a living, breathing thing. It should be subject to revision and updated often. For this reason, you should add version numbers to your documentation. Ideally, the latest version should be at a canonical URL and you should archive previous versions to static URLs. </p>
+
+ <p>I would also advise releasing the first version of your framework not as 'version 1.0’ but as 'v0.1’. This shows that you’re willing for others to provide input, that there will be further versions, and that you know you haven’t got it right first time (and forevermore). </p>
+
+ <hr/>
+
+ <p><strong>Questions? Comments?</strong> Ask me on Twitter (<a href="http://twitter.com/dajbelshaw" rel="nofollow">@dajbelshaw</a>). I also consult around this kind of thing, so hit me up on <a href="http://literaci.es/hello@dynamicskillset.com" rel="nofollow">hello@dynamicskillset.com</a></p></div>
+ </content>
+ <updated>2016-01-25T14:46:34Z</updated>
+ <published>2016-01-25T14:46:34Z</published>
+ <source>
+ <id>tag:literaci.es,2014:/feed</id>
+ <author>
+ <name>Doug Belshaw</name>
+ <email>mail@dougbelshaw.com</email>
+ <uri>http://literaci.es</uri>
+ </author>
+ <link href="http://literaci.es" rel="alternate" type="text/html"/>
+ <link href="http://literaci.es/feed" rel="self" type="application/atom+xml"/>
+ <title xml:lang="en-US">Literacies</title>
+ <updated>2016-01-25T14:46:34Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>https://fundraising.mozilla.org/?p=800</id>
+ <link href="https://fundraising.mozilla.org/why-did-you-decide-to-donate-today/" rel="alternate" type="text/html"/>
+ <title>Why did you decide to donate today?</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">This year, we asked some of our donors why they decided to donate to our end of year fundraising campaign. The Survey The Audience The survey was shown to a random sample of donors whose browser language was set to … <a class="go" href="https://fundraising.mozilla.org/why-did-you-decide-to-donate-today/">Continue reading</a></div>
+ </summary>
+ <updated>2016-01-25T13:31:34Z</updated>
+ <category term="metrics"/>
+ <category term="mozilla"/>
+ <author>
+ <name>Adam Lofting</name>
+ </author>
+ <source>
+ <id>https://fundraising.mozilla.org</id>
+ <link href="https://fundraising.mozilla.org/category/mozilla/feed/" rel="self" type="application/rss+xml"/>
+ <link href="https://fundraising.mozilla.org" rel="alternate" type="text/html"/>
+ <subtitle>We work in the open, we fundraise in the open. This site shows you how we work, shares what we know, and challenges you to help us do it better.</subtitle>
+ <title>Mozilla: View Source Fundraising » mozilla</title>
+ <updated>2016-01-25T13:31:34Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-us">
+ <id>http://www.agmweb.ca/robbie-burns</id>
+ <link href="http://www.agmweb.ca/2016-01-25-robbie-burns/" rel="alternate" type="text/html"/>
+ <title xml:lang="en-us">Robbie Burns</title>
+ <content type="xhtml" xml:lang="en-us"><div xmlns="http://www.w3.org/1999/xhtml"><p>Tonight is Robbie Burns night, in honour of that great Scottish poet. But tonight had me thinking about another night in my past.</p>
+
+ <p>It was about 5 years ago, maybe less, I struggle to remember now. I was in the UK visiting family and my Dad was sick. Cancer and it's treatment is tough, you have good weeks, you have bad weeks and you have really fucking bad weeks. This was a good week and for some reason I was in the UK.</p>
+
+ <p>Myself, my brother and my sister-in-law went down to see him that night. It was Robbie Burns night and that meant an excuse for haggis, really, truly terrible scotch, Scottish dancing and all that. There are many times when I look back at time with my Dad in those last few years. This was definitely one of those times. He was my Dad at his best, cracking jokes and having fun. Living life to the absolute fullest, while you still have that chance.</p>
+
+ <p>We had a great night. That ended way too soon.</p>
+
+ <p>Not long after that the cancer came back and that was that.</p>
+
+ <p>But suddenly tonight, in a bar in Portland I had these memories of my Dad in a waistcoat cracking jokes and having fun on Robbie Burns night. No-one else in the bar seemed to know what night it was. You'd think Robbie Burns night might get a little bit more appreciation, but hey.</p>
+
+ <p>In the many years I've been running this blog I've never written about my Dad passing away. Here's the first time. I miss him.</p>
+
+ <p>Hey Robbie Burns? Thanks for making me remember that night.</p></div>
+ </content>
+ <updated>2016-01-25T08:00:00Z</updated>
+ <source>
+ <id>http://www.agmweb.ca/blog/andy</id>
+ <author>
+ <name>Andy McKay</name>
+ <email>andy@clearwind.ca</email>
+ </author>
+ <link href="http://www.agmweb.ca/blog/andy" rel="alternate" type="text/html"/>
+ <link href="http://www.agmweb.ca/blog/rss/latest/andy/" rel="self" type="application/atom+xml"/>
+ <title xml:lang="en-us">Andy McKay</title>
+ <updated>2016-01-26T06:33:30Z</updated>
+ </source>
+ </entry>
+
+ <entry>
+ <id>tag:this-week-in-rust.org,2016-01-25:blog/2016/01/25/this-week-in-rust-115/</id>
+ <link href="http://this-week-in-rust.org/blog/2016/01/25/this-week-in-rust-115/" rel="alternate" type="text/html"/>
+ <title>This Week in Rust 115</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>Hello and welcome to another issue of <em>This Week in Rust</em>!
+ <a href="http://rust-lang.org">Rust</a> is a systems language pursuing the trifecta:
+ safety, concurrency, and speed. This is a weekly summary of its progress and
+ community. Want something mentioned? Tweet us at <a href="https://twitter.com/ThisWeekInRust">@ThisWeekInRust</a> or <a href="mailto:corey@octayn.net?subject=This%20Week%20in%20Rust%20Suggestion">send us an
+ email</a>!
+ Want to get involved? <a href="https://github.com/rust-lang/rust/blob/master/CONTRIBUTING.md">We love
+ contributions</a>.</p>
+ <p><em>This Week in Rust</em> is openly developed <a href="https://github.com/cmr/this-week-in-rust">on GitHub</a>.
+ If you find any errors in this week's issue, <a href="https://github.com/cmr/this-week-in-rust/pulls">please submit a PR</a>.</p>
+ <p>This week's edition was edited by: <a href="https://github.com/nasa42">nasa42</a>, <a href="https://github.com/brson">brson</a>, and <a href="https://github.com/llogiq">llogiq</a>.</p>
+ <h3>Updates from Rust Community</h3>
+ <h4>News &amp; Blog Posts</h4>
+ <ul>
+ <li><img alt="balloon" class="emoji" src="https://cdn.discourse.org/business/images/emoji/emoji_one/balloon.png?v=0" title=":balloon:"/><img alt="tada" class="emoji" src="https://cdn.discourse.org/business/images/emoji/emoji_one/tada.png?v=0" title=":tada:"/> <a href="http://blog.rust-lang.org/2016/01/21/Rust-1.6.html">Announcing Rust 1.6</a>. <img alt="tada" class="emoji" src="https://cdn.discourse.org/business/images/emoji/emoji_one/tada.png?v=0" title=":tada:"/><img alt="balloon" class="emoji" src="https://cdn.discourse.org/business/images/emoji/emoji_one/balloon.png?v=0" title=":balloon:"/></li>
+ <li><a href="http://www.poumeyrol.fr/2016/01/15/Awkward-zone/">Rust, BigData and my laptop</a>.</li>
+ <li>[pdf]<a href="https://cdn.rawgit.com/Gankro/thesis/master/thesis.pdf">You can't spell trust without Rust</a>. Analysis of the semantics and expressiveness of Rust’s type system.</li>
+ <li><a href="http://www.ncameron.org/blog/libmacro/">Libmacro - an API for procedural macros to interact with the compiler</a>.</li>
+ <li><a href="http://www.jonathanturner.org/2016/01/rust-and-blub-paradox.html">Rust and the Blub Paradox</a>. And the <a href="http://www.jonathanturner.org/2016/01/rethinking-the-blub-paradox.html">follow-up</a>.</li>
+ <li>[video] <a href="https://www.youtube.com/channel/UC4mpLlHn0FOekNg05yCnkzQ/videos">Ferris Makes Emulators</a>. Live stream of Ferris developing a N64 emulator in Rust (also on <a href="http://www.twitch.tv/ferrisstreamsstuff/profile">Twitch</a>).</li>
+ </ul>
+ <h4>Notable New Crates &amp; Project Updates</h4>
+ <ul>
+ <li><a href="http://areweconcurrentyet.com/">Are we concurrent yet</a>?</li>
+ <li><a href="https://github.com/gfx-rs/gfx">GFX</a> epic rewrite for the Pipeline State Objects paradigm has <a href="https://github.com/gfx-rs/gfx/pull/828">landed</a>, described <a href="http://gfx-rs.github.io/2016/01/22/pso.html">on the blog</a>.</li>
+ <li><a href="https://github.com/mcarton/rust-herbie-lint">Herbie</a>. A rustc plugin to check for numerical instability.</li>
+ <li><a href="http://blog.piston.rs/2016/01/23/dynamo/">Dynamo</a>. A rusty dynamically typed scripting language.</li>
+ <li><a href="https://github.com/whitequark/rust-vnc">rust-vnc</a>. An implementation of VNC protocol, client state machine and a client.</li>
+ </ul>
+ <h3>Updates from Rust Core</h3>
+ <p>129 pull requests were <a href="https://github.com/issues?q=is%3Apr+org%3Arust-lang+is%3Amerged+merged%3A2016-01-18..2016-01-25">merged in the last week</a>.</p>
+ <p>See the <a href="https://internals.rust-lang.org/t/triage-digest-mon-jan-25-2016/3111">triage digest</a> and <a href="https://internals.rust-lang.org/t/subteam-reports-2016-01-22/3106">subteam reports</a> for more details.</p>
+ <h4>Notable changes</h4>
+ <ul>
+ <li><a href="https://github.com/rust-lang/rust/pull/30872">Implement RFC 1252 expanding the OpenOptions structure</a>.</li>
+ <li><a href="https://github.com/rust-lang/book/pull/58">Book: First draft of 'ownership'</a>.</li>
+ <li><a href="https://github.com/rust-lang/cargo/pull/2205">Cargo: Add convenience syntax to install current crate</a>.</li>
+ <li><a href="https://github.com/rust-lang/cargo/pull/2196">Cargo: Introduce cargo metadata subcommand</a>.</li>
+ <li><a href="https://github.com/rust-lang/cargo/pull/2081">Cargo: Implement <code>cargo init</code></a>.</li>
+ <li><a href="https://github.com/rust-lang/cargo/pull/2270">Cargo: Emit a warning when manifest specifies empty dependency constraints</a>.</li>
+ <li><a href="https://github.com/rust-lang/rust/pull/29520">Change name when outputting staticlibs on Windows</a>.</li>
+ <li><a href="https://github.com/rust-lang/rust/pull/30998">Make <code>btree_set::{IntoIter, Iter, Range}</code> covariant</a>.</li>
+ <li><a href="https://github.com/rust-lang/rust/pull/30917">Avoid bounds checking at <code>slice::binary_search</code></a>.</li>
+ <li><a href="https://github.com/rust-lang/rust/pull/30894"><code>std::sync::mpsc</code>: Add <code>fmt::Debug</code> stubs</a>.</li>
+ <li><a href="https://github.com/rust-lang/rust/pull/30882">resolve: Fix variant namespacing</a>.</li>
+ </ul>
+ <h4>New Contributors</h4>
+ <ul>
+ <li>Adrian Heine</li>
+ <li>Andrea Bedini</li>
+ <li>Guillaume Bonnet</li>
+ <li>Kamal Marhubi</li>
+ <li>Keith Yeung</li>
+ <li>Marc Bowes</li>
+ <li>Martin</li>
+ <li>mopp</li>
+ <li>Olaf Buddenhagen</li>
+ <li>Paul Dicker</li>
+ <li>Peter Kolloch</li>
+ <li>Stephen (Ziyun) Li</li>
+ </ul>
+ <h4>Approved RFCs</h4>
+ <p>Changes to Rust follow the Rust <a href="https://github.com/rust-lang/rfcs#rust-rfcs">RFC (request for comments)
+ process</a>. These
+ are the RFCs that were approved for implementation this week:</p>
+ <ul>
+ <li><a href="https://github.com/rust-lang/rfcs/pull/1462">Amendment to RFC 550: Add <code>[</code> to the FOLLOW(ty) in macro future-proofing rules</a>.</li>
+ <li><a href="https://github.com/rust-lang/rfcs/pull/1320">Amendment to RFC 1192: Amend <code>RangeInclusive</code> to use an enum</a>.</li>
+ </ul>
+ <h4>Final Comment Period</h4>
+ <p>Every week <a href="https://rust-lang.org/team.html">the team</a> announces the
+ 'final comment period' for RFCs and key PRs which are reaching a
+ decision. Express your opinions now. <a href="https://github.com/rust-lang/rfcs/labels/final-comment-period">This week's FCPs</a> are:</p>
+ <ul>
+ <li><a href="https://github.com/rust-lang/rfcs/pull/243">Trait-based exception handling</a>.</li>
+ <li><a href="https://github.com/rust-lang/rfcs/pull/1361">Improve Cargo target-specific dependencies</a>.</li>
+ <li><a href="https://github.com/rust-lang/rfcs/pull/1129">Add a <code>IndexAssign</code> trait that allows overloading "indexed assignment" expressions like <code>a[b] = c</code></a>.</li>
+ <li><a href="https://github.com/rust-lang/rfcs/pull/1196">Allow eliding more type parameters</a>.</li>
+ <li><a href="https://github.com/rust-lang/rfcs/pull/1296">Add an <code>alias</code> attribute to <code>#[link]</code> and <code>-l</code></a>.</li>
+ </ul>
+ <h4>New RFCs</h4>
+ <ul>
+ <li><a href="https://github.com/rust-lang/rfcs/pull/1477">Add compiler support for generic atomic operations</a>.</li>
+ <li><a href="https://github.com/rust-lang/rfcs/pull/1478">Translate undefined generic intrinsics to an LLVM <code>unreachable</code> and a lint</a>.</li>
+ </ul>
+ <h3>Upcoming Events</h3>
+ <ul>
+ <li><a href="http://www.meetup.com/opentechschool-berlin/">1/27. OpenTechSchool Berlin: Rust Hack and Learn</a>.</li>
+ <li><a href="http://www.meetup.com/Tokyo-Rust-Meetup/events/227871840/">1/28. Tokyo Rust Meetup #2</a>.</li>
+ <li><a href="http://www.meetup.com/Rust-Berlin/events/227321071/">2/3. Rust Berlin: Leaf and Collenchyma</a>.</li>
+ <li><a href="http://www.meetup.com/de/Rust-Cologne-Bonn/events/227534456/">2/3. Rust Meetup in Cologne / Germany</a>.</li>
+ <li><a href="https://www.eventbrite.com/e/mozilla-rust-seattle-meetup-tickets-12222326307?aff=erelexporg">2/8. Seattle Rust Meetup</a>.</li>
+ </ul>
+ <p>If you are running a Rust event please add it to the <a href="https://www.google.com/calendar/embed?src=apd9vmbc22egenmtu5l6c5jbfc%40group.calendar.google.com">calendar</a> to get
+ it mentioned here. Email <a href="mailto:erick.tryzelaar@gmail.com">Erick Tryzelaar</a> or <a href="mailto:banderson@mozilla.com">Brian
+ Anderson</a> for access.</p>
+ <h3>fn work(on: RustProject) -&gt; Money</h3>
+ <ul>
+ <li><a href="http://maidsafe.net/rust_engineer.html">Rust Engineer</a> at MaidSafe.</li>
+ <li><a href="https://careers.mozilla.org/en-US/position/ozy21fwU">Research Engineer - Servo</a> at Mozilla.</li>
+ <li><a href="https://careers.mozilla.org/en-US/position/o0H41fww">Senior Research Engineer - Rust</a> at Mozilla.</li>
+ <li><a href="http://plv.mpi-sws.org/rustbelt/">PhD and postdoc positions</a> at MPI-SWS.</li>
+ </ul>
+ <p><em>Tweet us at <a href="https://twitter.com/ThisWeekInRust">@ThisWeekInRust</a> to get your job offers listed here!</em></p>
+ <h3>Crate of the Week</h3>
+ <p>This week's Crate of the Week is <a href="https://github.com/phildawes/racer">racer</a> which powers code completion in all Rust development environments.</p>
+ <p>Thanks to <a href="https://users.rust-lang.org/users/stebalien">Steven Allen</a> for the suggestion.</p>
+ <p><a href="https://users.rust-lang.org/t/crate-of-the-week/2704">Submit your suggestions for next week</a>!</p>
+ <h3>Quote of the Week</h3>
+ <blockquote>
+ <p>Memory errors are fundamentally state errors, and Rust's move semantics, borrowing, and aliasing XOR mutating help enormously for me to reason about how my program changes state as it executes, to avoid accidental shared state and side effects at a distance. Rust more than any other language I know enables me to do compiler driven design. And internalizing its rules has helped me design better systems, even in other languages.</p>
+ </blockquote>
+ <p>— <a href="https://www.reddit.com/r/rust/comments/4275gz/rust_and_the_blub_paradox/cz8akv9">desiringmachines on /r/rust</a>.</p>
+ <p>Thanks to <a href="https://users.rust-lang.org/users/dikaiosune">dikaiosune</a> for the suggestion.</p>
+ <p><a href="http://users.rust-lang.org/t/twir-quote-of-the-week/328">Submit your quotes for next week</a>!</p></div>
+ </summary>
+ <updated>2016-01-25T05:00:00Z</updated>
+ <author>
+ <name>Corey Richardson</name>
+ </author>
+ <source>
+ <id>http://this-week-in-rust.org/</id>
+ <link href="http://this-week-in-rust.org/" rel="alternate" type="text/html"/>
+ <link href="http://this-week-in-rust.org/atom.xml" rel="self" type="application/atom+xml"/>
+ <title>This Week in Rust</title>
+ <updated>2016-01-25T05:00:00Z</updated>
+ </source>
+ </entry>
+
+ <entry>
+ <id>tag:blogger.com,1999:blog-1015214236289077798.post-7056349209464984020</id>
+ <link href="http://tenfourfox.blogspot.com/2016/01/3860-available.html" rel="alternate" type="text/html"/>
+ <title>38.6.0 available</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">TenFourFox 38.6.0 is available for testing (<a href="https://sourceforge.net/projects/tenfourfox/files/38.6.0/">downloads</a>, <a href="https://github.com/classilla/tenfourfox/wiki/Hashes">hashes</a>, <a href="https://github.com/classilla/tenfourfox/wiki/ZZReleaseNotes3860">release notes</a>). I'm sorry it's been so quiet around here; I'm in the middle of a backbreaking Master's course, my last one before I'm finally done with the lousy thing, and I haven't had any time to start on 45 so far. 38.6 does have some other fixes in it, though: I think I found the last place where bookmark backups were being mistakenly saved in LZ4 based on Chris Trusch's report, and the problematic fonts on the iCloud login page are now blacklisted, so you should be able to login again. I can't do much more testing than that, however, since I don't use iCloud personally, so other lapses in font functionality will require the font URL and I'll add them to the blacklist in 38.7. The browser will go live Monday Pacific time as usual. (The temporary workaround is to set <tt>gfx.downloadable_fonts.enabled</tt> to <tt>false</tt>, and switch the setting back when you don't need it anymore.) <p>Speaking of, downloadable fonts were exactly the same problem on the Sun Ultra-3 laptop I've been refurbishing; Oracle still provides a free Solaris 10 build of 38ESR, but it crashes on web fonts for reasons I have yet to diagnose, so I just have them turned off. Yes, it really is a SPARC laptop, a rebranded Tadpole Viper, and I think the fastest one ever made in this form factor (a 1.2GHz UltraSPARC IIIi). It's pretty much what I expected the PowerBook G5 would have been -- hot, overthrottled and power-hungry -- but Tadpole actually built the thing and it's not a disaster, relatively speaking. There's no JIT in this Firefox build, the brand new battery gets only 70 minutes of runtime even with the CPU clock-skewed to hell, it stands a very good chance of rendering me sterile and/or medium rare if I actually use it in my lap and it had at least one sudden overtemp shutdown and pooped all over the filesystem, but between Firefox, Star Office and <tt>pkgsrc</tt> I can actually use it. More on that for laughs in a future post. </p><p>It has been pointed out to me that Leopard Webkit has not made an update in over three months, so hopefully Tobias is still doing okay with his port.</p></div>
+ </summary>
+ <updated>2016-01-23T06:02:00Z</updated>
+ <author>
+ <name>ClassicHasClass</name>
+ <email>noreply@blogger.com</email>
+ </author>
+ <source>
+ <id>tag:blogger.com,1999:blog-1015214236289077798</id>
+ <category term="security"/>
+ <category term="mozilla"/>
+ <category term="anfscd"/>
+ <category term="qte"/>
+ <category term="transition"/>
+ <category term="PowerPC"/>
+ <category term="shame"/>
+ <category term="mte"/>
+ <category term="ppc970"/>
+ <category term="applesnark"/>
+ <category term="judgment day"/>
+ <category term="shoutout"/>
+ <category term="parity"/>
+ <category term="tenfourfoxbox"/>
+ <category term="68k"/>
+ <category term="classilla"/>
+ <category term="intel"/>
+ <category term="kubrick"/>
+ <category term="sluggo"/>
+ <category term="statistics"/>
+ <category term="thereisnoxulonlywebextensions"/>
+ <author>
+ <name>ClassicHasClass</name>
+ <email>noreply@blogger.com</email>
+ </author>
+ <link href="http://tenfourfox.blogspot.com/" rel="alternate" type="text/html"/>
+ <link href="http://tenfourfox.blogspot.com/feeds/posts/default?alt=rss" rel="self" type="application/rss+xml"/>
+ <subtitle>What's new in TenFourFox, the Mozilla browser for Power Macs.</subtitle>
+ <title>TenFourFox Development</title>
+ <updated>2016-01-26T18:31:40Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>https://blog.mozilla.org/netpolicy/?p=907</id>
+ <link href="https://blog.mozilla.org/netpolicy/2016/01/22/addressing-the-chilling-effect-of-patent-damages/" rel="alternate" type="text/html"/>
+ <title>Addressing the Chilling Effect of Patent Damages</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">Last year, we unveiled the Mozilla Open Software Patent License as part of our Initiative to help limit the negative impacts that patents have on open source software. While those were an important first step for us, we continue to … <a class="go" href="https://blog.mozilla.org/netpolicy/2016/01/22/addressing-the-chilling-effect-of-patent-damages/">Continue reading</a></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>Last year, we unveiled the <a href="https://www.mozilla.org/about/patents/license/">Mozilla Open Software Patent License</a> as part of our <a href="https://www.mozilla.org/about/patents/">Initiative</a> to help limit the negative impacts that patents have on open source software. While those were an important first step for us, we continue to do more. This past Wednesday, Mozilla joined several other tech and software companies in filing an <a href="https://blog.mozilla.org/netpolicy/files/2016/01/Halo-Stryker-Internet-Companies-brief.pdf">amicus brief</a> with the Supreme Court of the United States in the <i>Halo</i> and <i>Stryker</i> cases.</p>
+ <p>In the brief, we urge the Court to limit the availability of treble damages. Treble damages are significant because they greatly increase the amount of money owed if a defendant is found to “willfully infringe†a patent. As a result, many open source projects and technology companies will refuse to look into or engage in discussions about patents, in order to avoid even a remote possibility of willful infringement. This makes it very hard to address the chilling effects that patents can have on open source software development, open innovation, and collaborative efforts.</p>
+ <p>We hope that our brief will help the Court see how this legal standard has affected technology companies and persuade the Court to limit treble damages.</p></div>
+ </content>
+ <updated>2016-01-23T00:17:34Z</updated>
+ <category term="Open Source"/>
+ <category term="patent"/>
+ <category term="United States"/>
+ <author>
+ <name>Elvin Lee</name>
+ </author>
+ <source>
+ <id>https://blog.mozilla.org/netpolicy</id>
+ <link href="http://blog.mozilla.org/netpolicy/feed/" rel="self" type="application/rss+xml"/>
+ <link href="https://blog.mozilla.org/netpolicy" rel="alternate" type="text/html"/>
+ <subtitle>Mozilla's official blog on open Internet policy initiatives and developments</subtitle>
+ <title>Open Policy &amp; Advocacy</title>
+ <updated>2016-01-25T20:46:35Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>http://blog.mozilla.org/addons/?p=7640</id>
+ <link href="https://blog.mozilla.org/addons/2016/01/22/add-on-signing-update/" rel="alternate" type="text/html"/>
+ <title>Add-on Signing Update</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">In Firefox 43, we made it a default requirement for add-ons to be signed. This requirement can be disabled by toggling a preference that was originally scheduled to be removed in Firefox 44 for release and beta versions (this preference … <a class="go" href="https://blog.mozilla.org/addons/2016/01/22/add-on-signing-update/">Continue reading</a></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>In Firefox 43, we made it a default requirement for add-ons to be signed. This requirement can be disabled by <a href="https://wiki.mozilla.org/Addons/Extension_Signing#FAQ">toggling a preference</a> that was originally scheduled to be removed in Firefox 44 for release and beta versions (this preference will continue to be available in the Nightly, Developer, and ESR Editions of Firefox for the foreseeable future). </p>
+ <p>We are delaying the removal of this preference to Firefox 46 for a couple of reasons: We’re adding a feature in Firefox 45 that allows <a href="https://blog.mozilla.org/addons/2015/12/23/loading-temporary-add-ons/">temporarily loading unsigned restartless add-ons</a> in release, which will allow developers of those add-ons to use Firefox for testing, and we’d like this option to be available when we remove the preference. We also want to ensure that developers have adequate time to finish the transition to signed add-ons. </p>
+ <p>The <a href="https://wiki.mozilla.org/Addons/Extension_Signing#Timeline">updated timeline</a> is available on the signing wiki, and you can look up <a href="https://wiki.mozilla.org/RapidRelease/Calendar">release dates for Firefox versions</a> on the releases wiki. Signing will be mandatory in the beta and release versions of Firefox from 46 onwards, at which point unbranded builds based on beta and release will be provided for testing.</p></div>
+ </content>
+ <updated>2016-01-22T22:40:59Z</updated>
+ <category term="developers"/>
+ <category term="general"/>
+ <category term="releases"/>
+ <author>
+ <name>Kev Needham</name>
+ </author>
+ <source>
+ <id>https://blog.mozilla.org/addons</id>
+ <link href="https://blog.mozilla.org/addons/feed/" rel="self" type="application/rss+xml"/>
+ <link href="https://blog.mozilla.org/addons" rel="alternate" type="text/html"/>
+ <title>Mozilla Add-ons Blog</title>
+ <updated>2016-01-25T20:46:40Z</updated>
+ </source>
+ </entry>
+
+ <entry>
+ <id>http://coopcoopbware.tumblr.com/post/137832199980</id>
+ <link href="http://coopcoopbware.tumblr.com/post/137832199980" rel="alternate" type="text/html"/>
+ <title>RelEng &amp; RelOps Weekly Highlights - January 22, 2016</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p/><figure class="alignright"><a href="https://www.flickr.com/photos/proud2bcan8dn/1150097247/in/faves-19934681@N00/" target="_blank" title="wine-and-pies"><img alt="wine-and-pies" src="https://farm2.staticflickr.com/1216/1150097247_2f11cb4c2d_z.jpg?zz=1" width="200px"/></a>Releng: drinkin’ wine and makin’ pies.</figure>It’s encouraging to see more progress this week on both the build/release promotion and TaskCluster migration fronts, our two major efforts for this quarter.<p/>
+
+ <p><b>Modernize infrastructure:</b></p>
+ <p>In a continuing effort to enable faster, more reliable, and more easily-run tests for TaskCluster components, Dustin landed support for an in-memory, credential-free mock of Azure Table Storage in the <a href="https://www.npmjs.com/package/azure-entities" target="_blank">azure-entities</a> package. Together with the fake mock support he added to <a href="https://github.com/djmitche/taskcluster-lib-testing" target="_blank">taskcluster-lib-testing</a>, this allows tests for components like taskcluster-hooks to run without network access and without the need for any credentials, substantially decreasing the barrier to external contributions.</p>
+
+ <p>All release promotion tasks are now signed by default. Thanks to Rail for his work here to help improve verifiability and chain-of-custody in our upcoming release process. (<a href="https://bugzil.la/1239682" target="_blank">https://bugzil.la/1239682</a>)
+ Beetmover has been spotted in the wild! Jordan has been working on this new tool as part of our release promotion project. Beetmover helps move build artifacts from one place to another (generally between S3 buckets these days), but can also be extended to perform validation actions inline, e.g. checksums and anti-virus. (<a href="https://bugzil.la/1225899" target="_blank">https://bugzil.la/1225899</a>)</p>
+
+ <p>Dustin configured the “desktop-test†and “desktop-build†docker images to build automatically on push. That means that you can modify the Dockerfile under `testing/docker`, push to try, and have the try job run in the resulting image, all without pushing any images. This should enable much quicker iteration on tweaks to the docker images. Note, however, that updates to the base OS images (ubuntu1204-build and centos6-build) still require manual pushes.</p>
+
+ <p>Mark landed Puppet code for base windows 10 support including secrets and ssh keys management.</p>
+
+ <p><b>Improve CI pipeline:</b></p>
+
+ <p>Vlad and Amy repurposed 10 Windows XP machines as Windows 7 to improve the wait times in that test pool (<a href="https://bugzil.la/1239785" target="_blank">https://bugzil.la/1239785</a>)
+ Armen and Joel have been working on porting the Gecko tests to run under TaskCluster, and have narrowed the failures down to the single digits. This puts us on-track to enable Linux debug builds and tests in TaskCluster as the canonical build/test process.</p>
+
+ <p><b>Release:</b></p>
+
+ <p>Ben finished up work on enhanced Release Blob validation in Balrog (<a href="https://bugzil.la/703040" target="_blank">https://bugzil.la/703040</a>), which makes it much more difficult to enter bad data into our update server.</p>
+
+ <p>You may recall Mihai, our former intern who <a href="http://coopcoopbware.tumblr.com/post/133490693210/welcome-back-mihai" target="_blank">we just hired back in November</a>. Shortly after joining the team, he jumped into the <a href="https://wiki.mozilla.org/ReleaseEngineering/Releaseduty" target="_blank">releaseduty</a> rotation to provide much-needed extra bandwidth. The learning curve here is steep, but over the course of the Firefox 44 release cycle, he’s taken on more and more responsibility. He’s even volunteered to do releaseduty for the Firefox 45 release cycle as well. Perhaps the most impressive thing is that he’s also taken the time to update (or write) the releaseduty docs so that the next person who joins the rotation will be that much further ahead of the game. Thanks for your hard work here, Mihai!</p>
+
+ <p><b>Operational:</b></p>
+
+ <p>Hal did some cleanup work to remove unused mozharness configs and directories from the build mercurial repos. These resources have long-since moved into the main mozilla-central tree. Hopefully this will make it easier for contributors to find the canonical copy! (<a href="https://bugzil.la/1239003" target="_blank">https://bugzil.la/1239003</a>)</p>
+
+ <p><b>Hiring:</b></p>
+
+ <p>We’re still hiring for a full-time <a href="https://careers.mozilla.org/position/oi8b2fwn" target="_blank">Build &amp; Release Engineer</a>, and we are still accepting applications for <a href="https://careers.mozilla.org/position/ofA51fwF" target="_blank">interns for 2016</a>. Come join us!</p>
+
+ <p>Well, I don’t know about you, but all that hard work makes me hungry for pie. See you next week!</p></div>
+ </summary>
+ <updated>2016-01-22T20:49:38Z</updated>
+ <category term="Mozilla"/>
+ <category term="releng"/>
+ <category term="highlights"/>
+ <source>
+ <id>http://coopcoopbware.tumblr.com/</id>
+ <author>
+ <name>Chris Cooper</name>
+ </author>
+ <link href="http://coopcoopbware.tumblr.com/" rel="alternate" type="text/html"/>
+ <link href="http://coopcoopbware.tumblr.com/tagged/Mozilla/rss" rel="self" type="application/rss+xml"/>
+ <title>Five different types of fried cheese</title>
+ <updated>2016-01-22T21:00:12Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>https://air.mozilla.org/foundation-demos-january-22-2016/</id>
+ <link href="https://air.mozilla.org/foundation-demos-january-22-2016/" rel="alternate" type="text/html"/>
+ <title>Foundation Demos January 22 2016</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>
+ <img alt="Foundation Demos January 22 2016" class="wp-post-image" height="90" src="https://air.cdn.mozilla.net/media/cache/1c/a0/1ca0b9b2609cdd4e6e3577a8c3df8cfc.jpg" width="160"/>
+ Mozilla Foundation Demos January 22 2016
+ </p></div>
+ </summary>
+ <updated>2016-01-22T18:00:00Z</updated>
+ <author>
+ <name>Air Mozilla</name>
+ </author>
+ <source>
+ <id>https://air.mozilla.org/</id>
+ <link href="https://air.mozilla.org/" rel="alternate" type="text/html"/>
+ <link href="https://air.mozilla.org/" rel="self" type="application/rss+xml"/>
+ <rights>Except where otherwise noted, content on this site is licensed under the Creative Commons Attribution Share-Alike License v3.0 or any later version.</rights>
+ <subtitle>Air Mozilla is the Internet multimedia presence of Mozilla, with live and pre-recorded shows, interviews, news snippets, tutorial videos, and features about the Mozilla community.</subtitle>
+ <title>Air Mozilla</title>
+ <updated>2016-01-25T20:31:50Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>http://blog.mozilla.org/sumo/?p=3667</id>
+ <link href="https://blog.mozilla.org/sumo/2016/01/22/whats-up-with-sumo-22nd-january/" rel="alternate" type="text/html"/>
+ <title>What’s up with SUMO – 22nd January</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">Hello, SUMO Nation! The third week of the new year is already behind us. Time flies when you’re not paying attention… What are you going to do this weekend? Let us know in the comments, if you feel like sharing … <a class="go" href="https://blog.mozilla.org/sumo/2016/01/22/whats-up-with-sumo-22nd-january/">Continue reading</a></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p><strong>Hello, SUMO Nation!</strong></p>
+ <p><a href="http://blog.mozilla.org/sumo/files/2016/01/sumo_logo.png"><img alt="sumo_logo" class="aligncenter size-full wp-image-3670" height="387" src="http://blog.mozilla.org/sumo/files/2016/01/sumo_logo.png" width="383"/></a>The third week of the new year is already behind us. Time flies when you’re not paying attention… What are you going to do this weekend? Let us know in the comments, if you feel like sharing :-) I hope to be in the mountains, getting some fresh (bracing) air, and enjoying nature.</p>
+ <h3><strong class="username">Welcome, new contributors!<br/>
+ </strong></h3>
+ <ul>
+ <li class="author">
+ <div class="author"><a class="username" href="https://support.mozilla.org/user/johnmwc2" target="_blank">johnmwc2</a></div>
+ </li>
+ <li class="author"><a class="author-name" href="https://support.mozilla.org/user/myanesp" target="_blank">myanesp</a></li>
+ <li class="author"><a class="author-name" href="https://support.mozilla.org/user/Harish.A" target="_blank">Harish.A</a></li>
+ <li class="author"><a class="author-name" href="https://support.mozilla.org/user/hoolibob" target="_blank">hoolibob</a></li>
+ <li class="author"><a class="author-name" href="https://support.mozilla.org/user/Meteoro890" target="_blank">Meteoro890</a></li>
+ </ul>
+ <div class="author">If you just joined us, don’t hesitate – come over and <a href="https://support.mozilla.org/forums/buddies" target="_blank">say “hi†in the forums!</a></div>
+ <div class="author"/>
+ <div class="author">
+ <h3><strong>Contributors of the week<br/>
+ </strong></h3>
+ <ul>
+ <li><span class="author-a-z74z1rz89z69z76zbz72zz69zz67z9z82zniz71z"><a href="https://support.mozilla.org/user/safwan.rahman" target="_blank">Safwan</a> for his work on the <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=619284" target="_blank">draft feature for l10n / KB editing</a> – rock on!</span></li>
+ <li><a href="https://support.mozilla.org/user/artist" target="_blank">Artist</a> and <a href="https://support.mozilla.org/user/pollti" target="_blank">Pollti</a> for their the work on updating important articles for Focus with limited time – woot!</li>
+ </ul>
+ <div class="" id="magicdomid64">
+ <p><strong><span style="text-decoration: underline;">We salute you!</span></strong></p>
+ </div>
+ <div class="author">Don’t forget that if you are new to SUMO and someone helped you get started in a nice way you can <a href="https://support.mozilla.org/forums/buddies/711364?last=65670" target="_blank">nominate them for the Buddy of the Month!</a></div>
+ <div class="author"/>
+ </div>
+ <h3><strong>Most recent SUMO Community meeting</strong></h3>
+ <ul>
+ <li><a href="https://public.etherpad-mozilla.org/p/sumo-2016-01-18" target="_blank">You can read the notes here</a> (most of the staff members were AFK due to MLK Day in the US) and see the video on our <a href="https://www.youtube.com/channel/UCaiposaIhA7HfMqH2NIciyA/videos" target="_blank">YouTube channel</a> and <a href="https://air.mozilla.org/search/?q=sumo" target="_blank">at AirMozilla</a>.<del> </del><del><br/>
+ </del></li>
+ <li><strong>IMPORTANT: We are considering changing the way the meetings work. Help us figure out what’s best for you – join the discussion on the forums in this thread: <a href="https://support.mozilla.org/en-US/forums/contributors/711752?last=67873">(Monday) Community Meetings in 2016</a>.</strong></li>
+ </ul>
+ <h3><strong>The next SUMO Community meeting… </strong></h3>
+ <ul>
+ <li style="text-align: left;">is happening on <a href="https://public.etherpad-mozilla.org/p/sumo-2016-01-25" target="_blank">Monday the 25th – join us</a>!</li>
+ <li style="text-align: left;"><strong>Reminder: if you want to add a discussion topic to the upcoming meeting agenda:</strong>
+ <ul>
+ <li style="text-align: left;">Start a thread in the <a href="https://support.mozilla.org/forums/contributors" target="_blank">Community Forums</a>, so that everyone in the community can see what will be discussed and voice their opinion here before Monday (this will make it easier to have an efficient meeting).</li>
+ <li style="text-align: left;">Please do so as soon as you can before the meeting, so that people have time to read, think, and reply (and also add it to the agenda).</li>
+ <li style="text-align: left;">If you can, please attend the meeting in person (or via IRC), so we can follow up on your discussion topic during the meeting with your feedback.</li>
+ </ul>
+ </li>
+ </ul>
+ <h3><strong class="author-g-ivsra51ph44x461i">Developers</strong></h3>
+ <ul>
+ <li><a href="http://edwin.mozilla.io/t/sumo" target="_blank">You can see the current state of the backlog our developers are working on here</a>.</li>
+ <li><a href="https://public.etherpad-mozilla.org/p/sumo-p-2016-01-21" target="_blank">The latest SUMO Platform meeting notes can be found here</a>.</li>
+ <li>Interested in learning how Kitsune (the engine behind SUMO) works? <a href="http://kitsune.readthedocs.org/" target="_blank">Read more about it here</a> and <a href="https://github.com/mozilla/kitsune/" target="_blank">fork it on GitHub</a>!</li>
+ <li>We have a new link for promoting contributions to Kitsune’s code. Please use <strong>http://mzl.la/SUMOdev</strong> whenever you want to show interested people to see what Kitsune is all about – thanks!</li>
+ </ul>
+ <p><a href="http://blog.mozilla.org/sumo/files/2016/01/mission_developers.png"><img alt="mission_developers" class="aligncenter size-full wp-image-3668" height="406" src="http://blog.mozilla.org/sumo/files/2016/01/mission_developers.png" width="437"/></a></p>
+ <h3><strong>Social</strong></h3>
+ <ul>
+ <li>Next week, there will be a kick-off meeting for the rethinking of Mozilla’s general support strategy through social networks. <a href="https://support.mozilla.org/user/Madasan" target="_blank">Are you interested in taking part? Let Madalina know!</a></li>
+ </ul>
+ <h3><strong>Community</strong></h3>
+ <ul>
+ <li>The NDA process and list is currently being reworked under the leadership of the Participation Team. Expect to see messaging on this subject in the coming days.</li>
+ <li>
+ <div class="title"><strong><a href="https://support.mozilla.org/forums/contributors/711729?last=67763">IMPORTANT: take a look at our Work Week Summary for Mozlando. We need your feedback for a few things there.</a></strong></div>
+ </li>
+ <li>Are you going to FOSDEM next week? Would you like to have a small SUMO-meetup? <a href="https://support.mozilla.org/user/vesper" target="_blank">Let me know</a>!</li>
+ <li>
+ <div class="title">Ongoing reminder: if you think you can benefit from getting <a href="https://wiki.mozilla.org/Community_Hardware" target="_blank">a second-hand device</a> to help you with contributing to SUMO, you know where to find us.</div>
+ </li>
+ </ul>
+ <p><a href="http://blog.mozilla.org/sumo/files/2016/01/hero_support.png"><img alt="hero_support" class="aligncenter size-full wp-image-3669" height="383" src="http://blog.mozilla.org/sumo/files/2016/01/hero_support.png" width="367"/></a></p>
+ <div class="">
+ <div class="" id="magicdomid83">
+ <h3><strong class="author-g-ivsra51ph44x461i">Localization</strong></h3>
+ </div>
+ </div>
+ <div class="" id="magicdomid95">
+ <ul>
+ <li>You can <a href="https://support.mozilla.org/forums/l10n-forum/711781" target="_blank">read more about the recent “infrequent contributor survey†in this thread</a>. In short: the good news is that we’re doing a good job at making it easy enough for everyone to contribute. The bad news – we’re not doing enough to make sure they know what to do after their first contribution. Expect some changes in the messaging for first-time contributors to the KB :-)</li>
+ <li><a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1012384" target="_blank">Our magical l10n dashboards keep being magical</a> ;-) Thank you for your patience. If you see any discrepancies between the number of localized articles and the percentage shown in the bar, file a bug!</li>
+ </ul>
+ </div>
+ <div class="" id="magicdomid75">
+ <h3><strong>Firefox<br/>
+ </strong></h3>
+ <ul>
+ <li><strong>for Android</strong>
+ <ul>
+ <li><a href="https://support.mozilla.org/forums/contributors/711712?last=67653">Learn more about Firefox 43 for Android from the official thread with release notes / issues / discussions</a>.</li>
+ <li>
+ <div class="title"><a href="https://support.mozilla.org/forums/contributors/711718?last=67822">Reminder: Roland is sharing Firefox 44 for Android release notes / issues / discussions</a> with everyone in the forum.</div>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ <ul>
+ <li><strong>for Desktop</strong>
+ <ul>
+ <li>Heads up – next week should be release week! Keep your eyes peeled ;-)</li>
+ </ul>
+ </li>
+ </ul>
+ <ul>
+ <li><strong>for iOS</strong>
+ <div class="" id="magicdomid85">
+ <ul class="list-bullet1">
+ <li><span class="author-a-107uz69zz81zhz78z0z78zz84zz66zz76zz82zz77zj">No news from the world of Firefox for iOS this week.</span></li>
+ </ul>
+ </div>
+ </li>
+ </ul>
+ </div>
+ <p>Thank you for reading all the way down here… More to come next week! You know where to find us, so see you around – keep rocking the open &amp; helpful web!</p></div>
+ </content>
+ <updated>2016-01-22T17:43:56Z</updated>
+ <category term="General"/>
+ <author>
+ <name>Michał</name>
+ </author>
+ <source>
+ <id>https://blog.mozilla.org/sumo</id>
+ <link href="https://blog.mozilla.org/sumo/feed/" rel="self" type="application/rss+xml"/>
+ <link href="https://blog.mozilla.org/sumo" rel="alternate" type="text/html"/>
+ <subtitle>SUpport MOzilla's official blog - rocking the helpful web since 2008!</subtitle>
+ <title>SUMO Blog</title>
+ <updated>2016-01-25T09:31:47Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>https://air.mozilla.org/bay-area-rust-meetup-january-2016/</id>
+ <link href="https://air.mozilla.org/bay-area-rust-meetup-january-2016/" rel="alternate" type="text/html"/>
+ <title>Bay Area Rust Meetup January 2016</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>
+ <img alt="Bay Area Rust Meetup January 2016" class="wp-post-image" height="90" src="https://air.cdn.mozilla.net/media/cache/87/4f/874f4abef76f55213d50e43d6417ed99.png" width="160"/>
+ Bay Area Rust meetup for January 2016. Topics TBD.
+ </p></div>
+ </summary>
+ <updated>2016-01-22T03:00:00Z</updated>
+ <author>
+ <name>Air Mozilla</name>
+ </author>
+ <source>
+ <id>https://air.mozilla.org/</id>
+ <link href="https://air.mozilla.org/" rel="alternate" type="text/html"/>
+ <link href="https://air.mozilla.org/" rel="self" type="application/rss+xml"/>
+ <rights>Except where otherwise noted, content on this site is licensed under the Creative Commons Attribution Share-Alike License v3.0 or any later version.</rights>
+ <subtitle>Air Mozilla is the Internet multimedia presence of Mozilla, with live and pre-recorded shows, interviews, news snippets, tutorial videos, and features about the Mozilla community.</subtitle>
+ <title>Air Mozilla</title>
+ <updated>2016-01-25T20:31:49Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>https://blog.lizardwrangler.com/?p=3953</id>
+ <link href="http://blog.lizardwrangler.com/2016/01/22/honored-to-participate-in-new-un-panel-on-womens-economic-empowerment/" rel="alternate" type="text/html"/>
+ <title>Honored to Participate in New UN Panel on Women’s Economic Empowerment</title>
+ <summary>Women’s economic empowerment is necessary for many reasons. It is necessary to bring health, safety and opportunity to half of humanity. It is necessary to bring investment and health to families and communities. It is necessary to unlock economic growth and build more stable societies. Today the UN Secretary General Ban Ki-moon launched the first […]</summary>
+ <updated>2016-01-22T02:45:58Z</updated>
+ <category term="Mozilla"/>
+ <author>
+ <name>Mitchell Baker</name>
+ </author>
+ <source>
+ <id>http://blog.lizardwrangler.com</id>
+ <link href="http://blog.lizardwrangler.com/feed/" rel="self" type="application/rss+xml"/>
+ <link href="http://blog.lizardwrangler.com" rel="alternate" type="text/html"/>
+ <title>Mitchell's Blog</title>
+ <updated>2016-01-22T03:00:15Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>https://blog.mozilla.org/webdev/?p=4082</id>
+ <link href="https://blog.mozilla.org/webdev/2016/01/21/beer-and-tell-january-2016/" rel="alternate" type="text/html"/>
+ <title>Beer and Tell – January 2016</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">Once a month, web developers from across the Mozilla Project get together to talk about our side projects and drink, an occurrence we like to call “Beer and Tellâ€. There’s a wiki page available with a list of the presenters, … <a class="go" href="https://blog.mozilla.org/webdev/2016/01/21/beer-and-tell-january-2016/">Continue reading</a></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>Once a month, web developers from across the Mozilla Project get together to talk about our side projects and drink, an occurrence we like to call “Beer and Tellâ€.</p>
+ <p>There’s a <a href="https://wiki.mozilla.org/Webdev/Beer_And_Tell/January_2016">wiki page available</a> with a list of the presenters, as well as links to their presentation materials. There’s also a <a href="https://air.mozilla.org/webdev-beer-and-tell-january-2016/">recording available</a> courtesy of Air Mozilla.</p>
+ <h3>shobson: CSS-Only Disco Ball</h3>
+ <p>First up was <a href="https://mozillians.org/en-US/u/stephaniehobson/">shobson</a> with a cool demo of an <a href="http://codepen.io/stephaniehobson/pen/ZGZBVW?editors=110">animated disco ball made entirely with CSS</a>. The demo uses a repeated radial gradient for the background, and linear gradients plus a border radius for the disco ball itself. The demo was made for use in shobson’s <a href="https://www.youtube.com/watch?v=7poVasAQjos">WordCamp talk</a> about debugging CSS. A <a href="http://stephaniehobson.ca/wordpress/2015/08/15/how-to-debug-css/">blog post</a> with notes from the talk is available as well.</p>
+ <h3>craigcook: Proton – A CSS Framework for Prototyping</h3>
+ <p>Next was <a href="https://mozillians.org/en-US/u/craigcook/">craigcook</a>, who presented <a href="http://craigcook.github.io/proton/">Proton</a>. It’s a CSS framework that is intentionally ugly to encourage use for prototypes only. Unlike other CSS frameworks, the temptation to reuse the classes from the framework in your final page doesn’t occur, which helps avoid the presentational classes that plague sites built using a framework normally.</p>
+ <p>Proton’s website includes an overview of the layout and components provided, as well as examples of prototypes made using the framework.</p>
+ <hr/>
+ <p>If you’re interested in attending the next Beer and Tell, sign up for the <a href="https://lists.mozilla.org/listinfo/dev-webdev">dev-webdev@lists.mozilla.org mailing list</a>. An email is sent out a week beforehand with connection details. You could even add yourself to the wiki and show off your side-project!</p>
+ <p>See you next month!</p></div>
+ </content>
+ <updated>2016-01-21T18:56:46Z</updated>
+ <category term="Beer and Tell"/>
+ <author>
+ <name>Michael Kelly</name>
+ </author>
+ <source>
+ <id>https://blog.mozilla.org/webdev</id>
+ <link href="https://blog.mozilla.org/webdev/feed/" rel="self" type="application/rss+xml"/>
+ <link href="https://blog.mozilla.org/webdev" rel="alternate" type="text/html"/>
+ <subtitle>For make benefit of glorious tubes</subtitle>
+ <title>Mozilla Web Development</title>
+ <updated>2016-01-21T19:01:37Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>http://blog.mozilla.org/community/?p=2287</id>
+ <link href="http://blog.mozilla.org/community/2016/01/21/this-month-at-mozilla/" rel="alternate" type="text/html"/>
+ <title>This Month at Mozilla</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">A lot of exciting things are happening with Participation at Mozilla this month. Here’s a quick round-up of some of the things that are going on! Mozillians Profiles Got a Facelift: Since the start of this year, the Participation Infrastructure … <a class="go" href="http://blog.mozilla.org/community/2016/01/21/this-month-at-mozilla/">Continue reading</a></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p style="text-align: center;"><em>A lot of exciting things are happening with Participation at Mozilla this month. Here’s a quick round-up of some of the things that are going on!</em></p>
+ <h3><b>Mozillians Profiles Got a Facelift: </b></h3>
+ <p>Since the start of this year, the Participation Infrastructure team has had a renewed focus on making mozillians.org a modern community directory to meet Mozilla’s growing needs.</p>
+ <p>Their first target for 2016 was to improve the UX on the profile edit interface.</p>
+ <p><a href="https://blog.mozilla.org/community/files/2016/01/new-profile-768x548.png"><img alt="new-profile-768x548" class="aligncenter wp-image-2288 size-large" height="428" src="https://blog.mozilla.org/community/files/2016/01/new-profile-768x548-600x428.png" width="600"/></a><br/>
+ â€We chose it due to relatively self-contained nature of it, and cause many people were not happy with the current UX. After research of existing tools and applying latest best practices, we designed, coded and deployed a new profile edit interface (which by the way is renamed to Settings now) that we are happy to deliver to all Mozillians.â€</p>
+ <p>Read the full blog <a href="http://pierros.papadeas.gr/?p=447">here</a>!</p>
+ <h3><b>There are New Ways to Bring Your Design Skills to Mozilla: </b></h3>
+ <p>Are you a passionate designer looking to contribute to Mozilla? You’ll be happy to hear there is a new way to contribute to the many design projects around Mozilla! Submit issues, find collaborators, and work on open source projects by getting involved!</p>
+ <ul>
+ <li>You can check out the projects looking for help, or submit your own on the <a href="https://github.com/mozilla/Community-Design/issues">GitHub Repo</a>.</li>
+ <li><a href="https://docs.google.com/a/mozilla.com/forms/d/1Tw3Mw_CMiqcIQrJF7TB1yIETGYec__NiVhaSz0CAaE8/viewform">Sign-up to the mailing list</a> to be added as a contributor to the Repo, added to the regular meeting list, and to get emails about GitHub trainings and more!</li>
+ <li>And read<a href="http://elioqoshi.me/en/2016/01/mozilla-community-design-kickoff/"> a blogpost</a> about the project and its first meeting.</li>
+ </ul>
+ <p>Learn more <a href="https://discourse.mozilla-community.org/c/community-design">here</a>.</p>
+ <h3><b>136 Volunteers Are Going to Singapore: </b></h3>
+ <p>This weekend 136 participation leaders from all over the world are<a href="https://twitter.com/thephoenixbird/status/690181985222926336"> heading to Singapore</a> to undergo two days of<a href="https://wiki.mozilla.org/Participation/Global_Gatherings_2015"> leadership training</a> to develop the skills, knowledge and attitude to lead Participation in 2016.</p>
+ <div class="wp-caption aligncenter" id="attachment_2289" style="width: 609px;"><a href="https://blog.mozilla.org/community/files/2016/01/CZQE241WIAA6R2J.jpg"><img alt="Photo credit @thephoenixbird on Twitter" class="wp-image-2289 size-full" height="337" src="https://blog.mozilla.org/community/files/2016/01/CZQE241WIAA6R2J.jpg" width="599"/></a><p class="wp-caption-text">Photo credit @<a href="https://twitter.com/thephoenixbird/status/690181985222926336" target="_blank">thephoenixbird</a> on Twitter</p></div>
+ <p>If you know someone attending don’t forget to share your questions and goals with them, and follow along over the weekend by watching the hashtag<a href="https://twitter.com/search?q=%23mozsummit"> #MozSummit</a>.</p>
+ <p>Stay tuned after the event for a debrief of the weekend!</p>
+ <h3><b>Friday’s Plenary from Mozlando is now public on Air Mozilla: </b></h3>
+ <p>If you’re interested in learning more about all the exciting new features, projects, and plans that were presented at Mozlando look no further! You can now watch the final plenary sessions on Air Mozilla (it’s a lot of fun so I highly recommend it!) <a href="https://air.mozilla.org/channels/mozlando/">here</a>.</p>
+ <p>Share your questions and comments on discourse <a href="https://discourse.mozilla-community.org/t/friday-plenary-from-mozlando-now-public-on-air-mozilla/6659">here</a>.</p>
+ <p><em>Look forward to more updates like these in the coming months!</em></p></div>
+ </content>
+ <updated>2016-01-21T17:58:33Z</updated>
+ <category term="Participation"/>
+ <category term="Air Mozilla"/>
+ <category term="contributor"/>
+ <category term="MonthlyUpdate"/>
+ <category term="MozParticipation"/>
+ <author>
+ <name>Lucy Harris</name>
+ </author>
+ <source>
+ <id>http://blog.mozilla.org/community</id>
+ <link href="http://blog.mozilla.org/community/feed/" rel="self" type="application/rss+xml"/>
+ <link href="http://blog.mozilla.org/community" rel="alternate" type="text/html"/>
+ <subtitle>News and notes from and for the Mozilla community.</subtitle>
+ <title>about:community</title>
+ <updated>2016-01-25T16:31:42Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>https://blog.mozilla.org/netpolicy/?p=912</id>
+ <link href="https://blog.mozilla.org/netpolicy/2016/01/21/prioritizing-privacy-good-for-business/" rel="alternate" type="text/html"/>
+ <title>Prioritizing privacy: Good for business</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">This was originally posted at StaySafeOnline.org in advance of Data Privacy Day. Data Privacy Day – which arrives in just a week – is a day designed to raise awareness and promote best practices for privacy and data protection. It … <a class="go" href="https://blog.mozilla.org/netpolicy/2016/01/21/prioritizing-privacy-good-for-business/">Continue reading</a></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p><em>This was originally posted at <a href="http://staysafeonline.org/blog/prioritizing-privacy-good-for-business/">StaySafeOnline.org</a> in advance of <a href="http://www.staysafeonline.org/data-privacy-day/events/">Data Privacy Day</a>.</em></p>
+ <p>Data Privacy Day – which arrives in just a week – is a day designed to raise awareness and promote best practices for privacy and data protection. It is a day that looks to the future and recognizes that we can and should do better as an industry. It reminds us that we need to focus on the importance of having the trust of our users.</p>
+ <p>We seek to build trust so we can collectively create the Web our users want – the Web we all want.</p>
+ <p>That Web is based on relationships, the same way that the offline world is. When I log in to a social media account, schedule a grocery delivery online or browse the news, I’m relying on those services to respect my data. While companies are innovating their products and services, they need to be innovating on user trust as well, which means designing to address privacy concerns – and making smart choices (early!) about how to manage data.</p>
+ <p>A <a href="http://www.pewinternet.org/2016/01/14/privacy-and-information-sharing/">recent survey by Pew</a> highlights the thought that each user puts into their choices – and the contextual considerations in various scenarios. They concluded that many participants were annoyed and uncertain by how their information was used, and they are choosing not to interact with those services that they don’t trust. This is a clear call to businesses to foster more trust with their users, which starts by making sure that there are people empowered within your company to ask the right questions: what do your users expect? What data do you need to collect? How can you communicate about that data collection? How should you protect their data? Is holding on to data a risk, or should you delete it?</p>
+ <p>It’s crucial that users are a part of this process – consumers’ data is needed to offer cool, new experiences and a user needs to trust you in order to choose to give you their data. Pro-user innovation can’t happen in a vacuum – the system as it stands today isn’t doing a good job of aligning user interests with business incentives. Good user decisions can be good business decisions, but only if we create thoughtful user-centric products in a way that closes the feedback loop so that positive user experiences are rewarded with better business outcomes.</p>
+ <p>Not prioritizing privacy in product decisions will impact the bottom line. From the many data breaches over the last few years to increasing evidence of eroding trust in online services, data practices are proving to be the dark horse in the online economy. When a company loses user trust, whether on privacy or <a href="https://medium.com/@davidamerland/the-cost-of-losing-trust-97d764a1e696">anything else</a>, it loses customers and the potential for growth.</p>
+ <p>Privacy means different things to different people but what’s clear is that people make decisions about the products and services that they use based on how those companies choose to treat their users. Over this time, the Internet ecosystem has evolved, as has its relationship with users – and some aspects of this evolution threaten the trust that lies at the heart of that relationship. Treating a user as a target – whether for an ad, purchase, or service – undermines the trust and relationship that a business may have with a consumer.</p>
+ <p>The solution is not to abandon the massive value that robust data can bring to users, but rather, to collect and use data leanly, productively and transparently. At Mozilla, we have created a strong set of internal data practices to ensure that data decisions align with our <a href="https://www.mozilla.org/en-US/privacy/principles/">privacy principles</a>. As an industry, we need to keep users at the center of the product vision rather than viewing them as targets of the product – it’s the only way to stay true to consumers and deliver the best, most trusted experiences possible.</p>
+ <p>Want to hear more about how businesses can build relationships with their users by focusing on trust and privacy? We’re holding events in Washington, D.C., and <a href="https://www.eventbrite.com/e/january-privacy-lab-privacy-for-startups-tickets-19849219550?aff=es2">San Francisco</a> with some of our partners to talk about it. Please join us!</p></div>
+ </content>
+ <updated>2016-01-21T17:42:00Z</updated>
+ <category term="Data Governance"/>
+ <category term="privacy"/>
+ <category term="Trust"/>
+ <author>
+ <name>Heather West</name>
+ </author>
+ <source>
+ <id>https://blog.mozilla.org/netpolicy</id>
+ <link href="http://blog.mozilla.org/netpolicy/feed/" rel="self" type="application/rss+xml"/>
+ <link href="https://blog.mozilla.org/netpolicy" rel="alternate" type="text/html"/>
+ <subtitle>Mozilla's official blog on open Internet policy initiatives and developments</subtitle>
+ <title>Open Policy &amp; Advocacy</title>
+ <updated>2016-01-25T20:46:35Z</updated>
+ </source>
+ </entry>
+
+ <entry>
+ <id>https://tacticalsecret.com/tag/mozilla/rss/9c39ad13-14ae-4456-a84e-13612637d832</id>
+ <link href="https://tacticalsecret.com/issuance-rate-for-lets-encrypt/" rel="alternate" type="text/html"/>
+ <title>Issuance Rate for Let's Encrypt</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>Gathering data from <a href="https://github.com/jcjones/letsencrypt_statistics">Certificate Transparency logs</a>, here's a snapshot in time of Let's Encrypt's certificate issuance rate per minute from 7-21 January 2016. On 20 January, DreamHost launched formal support for Let's Encrypt, which coincides with a rate increase.</p>
+
+ <p>Note: This is mostly an experimental post with embedding charts; I've</p></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>Gathering data from <a href="https://github.com/jcjones/letsencrypt_statistics">Certificate Transparency logs</a>, here's a snapshot in time of Let's Encrypt's certificate issuance rate per minute from 7-21 January 2016. On 20 January, DreamHost launched formal support for Let's Encrypt, which coincides with a rate increase.</p>
+
+ <p>Note: This is mostly an experimental post with embedding charts; I've more data in the queue.</p>
+
+ <h3>Let's Encrypt Issuance Rate per Minute</h3>
+
+ <div id="rate_hours"/></div>
+ </content>
+ <updated>2016-01-21T17:07:25Z</updated>
+ <category term="letsencrypt"/>
+ <category term="mozilla"/>
+ <category term="charts"/>
+ <author>
+ <name>James 'J.C.' Jones</name>
+ </author>
+ <source>
+ <id>https://tacticalsecret.com/</id>
+ <link href="https://tacticalsecret.com/" rel="alternate" type="text/html"/>
+ <link href="https://tacticalsecret.com/tag/mozilla/rss/" rel="self" type="application/rss+xml"/>
+ <subtitle>On a mission to solve information security issues for the whole Internet. That, and whatever else comes up.</subtitle>
+ <title>mozilla - The Internet of Secure Things</title>
+ <updated>2016-01-21T17:16:43Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>https://air.mozilla.org/web-qa-weekly-meeting-20160121/</id>
+ <link href="https://air.mozilla.org/web-qa-weekly-meeting-20160121/" rel="alternate" type="text/html"/>
+ <title>Web QA Weekly Meeting, 21 Jan 2016</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>
+ <img alt="Web QA Weekly Meeting" class="wp-post-image" height="90" src="https://air.cdn.mozilla.net/media/cache/f5/13/f5137857516694df0458e837c2d3a4be.png" width="160"/>
+ This is our weekly gathering of Mozilla'a Web QA team filled with discussion on our current and future projects, ideas, demos, and fun facts.
+ </p></div>
+ </summary>
+ <updated>2016-01-21T17:00:00Z</updated>
+ <author>
+ <name>Air Mozilla</name>
+ </author>
+ <source>
+ <id>https://air.mozilla.org/</id>
+ <link href="https://air.mozilla.org/" rel="alternate" type="text/html"/>
+ <link href="https://air.mozilla.org/" rel="self" type="application/rss+xml"/>
+ <rights>Except where otherwise noted, content on this site is licensed under the Creative Commons Attribution Share-Alike License v3.0 or any later version.</rights>
+ <subtitle>Air Mozilla is the Internet multimedia presence of Mozilla, with live and pre-recorded shows, interviews, news snippets, tutorial videos, and features about the Mozilla community.</subtitle>
+ <title>Air Mozilla</title>
+ <updated>2016-01-25T20:31:49Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>http://soledadpenades.com/?p=6379</id>
+ <link href="http://soledadpenades.com/2016/01/21/no-more-tap-tap-tap-sounds-yay/" rel="alternate" type="text/html"/>
+ <link href="https://flattr.com/submit/auto?user_id=8399&amp;popout=1&amp;url=http%3A%2F%2Fsoledadpenades.com%2F2016%2F01%2F21%2Fno-more-tap-tap-tap-sounds-yay%2F&amp;language=en_GB&amp;category=text&amp;title=No+more+tap+tap+tap+sounds%3A+yay%21&amp;description=A+few+days+ago+the+fantastic+Fritz+from+the+Netherlands+told+me+that+my+Hands+On+Web+Audio+slides+had+stopping+working+and+there+was+no+sound+coming+out+from...&amp;tags=bugs%2Cfirefox%2Cjavascript%2Cmozilla%2Cweb+audio%2Cblog" rel="payment" title="Flattr this!" type="text/html"/>
+ <title>No more tap tap tap sounds: yay!</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">A few days ago the fantastic Fritz from the Netherlands told me that my Hands On Web Audio slides had stopping working and there was no sound coming out from them in Firefox. @supersole oh noes! I reopened your slides: https://t.co/SO35UfljMI and it doesn't work in @firefox anymore 😱 (works in chrome though.. 😢) — … <a class="more-link" href="http://soledadpenades.com/2016/01/21/no-more-tap-tap-tap-sounds-yay/">Continue reading <span class="screen-reader-text">No more tap tap tap sounds: yay!</span> <span class="meta-nav">→</span></a></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>A few days ago the fantastic Fritz from the Netherlands told me that my <a href="http://soledadpenades.com/files/t/2015_howa/">Hands On Web Audio slides</a> had stopping working and there was no sound coming out from them in Firefox.</p>
+ <blockquote class="twitter-tweet" width="550"><p dir="ltr" lang="en"><a href="https://twitter.com/supersole">@supersole</a> oh noes! I reopened your slides: <a href="https://t.co/SO35UfljMI">https://t.co/SO35UfljMI</a> and it doesn't work in <a href="https://twitter.com/firefox">@firefox</a> anymore <img alt="&#x1F631;" class="wp-smiley" src="http://s.w.org/images/core/emoji/72x72/1f631.png" style="height: 1em;"/> (works in chrome though.. <img alt="&#x1F622;" class="wp-smiley" src="http://s.w.org/images/core/emoji/72x72/1f622.png" style="height: 1em;"/>)</p>
+ <p>— Boring Stranger (@fritzvd) <a href="https://twitter.com/fritzvd/status/686481500611735552">January 11, 2016</a></p></blockquote>
+ <p/>
+ <p>Which is pretty disappointing for a slide deck that is built to teach you about Web Audio!</p>
+ <p>I noticed that the issue was only on the introductory slide which uses a modified version of Stuart Memo’s <a href="https://blog.stuartmemo.com/thx-deep-note-in-javascript/">fantastic THX sound recreation</a>-the rest of slides did play sound.</p>
+ <p>I built <a href="http://sole.github.io/test_cases/web_audio/thx_cutting_out/">an isolated test case</a> <small><a href="https://github.com/sole/test_cases/tree/gh-pages/web_audio/thx_cutting_out">(source)</a></small> that used a parameter-capable version of the THX sound code, just in case the issue depended on the number of oscillators, and submitted this funnily titled bug to the Web Audio component: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1240054">Entirely Web Audio generated sound cuts out after a little while, or emits random tap tap tap sounds then silence</a>.</p>
+ <p>I can happily confirm that the bug has been fixed in Nightly and the fix will hopefully be “uplifted†to DevEdition very soon, as it was due to a regression.</p>
+ <p><a href="https://paul.cx/">Paul Adenot</a> (who works in Web Audio and is a Web Audio spec editor, amongst a couple tons of other cool things) was really excited about the bug, saying it was very edge-casey! Yay! And he also explained what did actually happen in lay terms: “you’d have to have a frequency that goes down very very slowly so that the FFT code could not keep upâ€, which is what the THX sound is doing with the filter frequency automation.</p>
+ <p>I want to thank both Fritz for spotting this out and letting me know and also Stuart for sharing his THX code. It’s amazing what happens when you put stuff on the net and lots of different people use it in different ways and configurations. Together we make everything more robust <img alt=":-)" class="wp-smiley" src="http://soledadpenades.com/wp-includes/images/smilies/simple-smile.png" style="height: 1em;"/></p>
+ <p>Of course also sending thanks to Paul and Ben for identifying and fixing the issue so fast! It’s not been even a week! Woohoo!</p>
+ <p>Well done everyone! <img alt="&#x1F44F;" class="wp-smiley" src="http://s.w.org/images/core/emoji/72x72/1f44f.png" style="height: 1em;"/><img alt="&#x1F3FC;" class="wp-smiley" src="http://s.w.org/images/core/emoji/72x72/1f3fc.png" style="height: 1em;"/></p>
+ <p><a href="http://soledadpenades.com/?flattrss_redirect&amp;id=6379&amp;md5=57babe624711830f95e4b8fbd6e52c91" target="_blank" title="Flattr"><img alt="flattr this!" src="http://soledadpenades.com/wp-content/plugins/flattr/img/flattr-badge-large.png"/></a></p></div>
+ </content>
+ <updated>2016-01-21T15:49:05Z</updated>
+ <category term="Software"/>
+ <category term="bugs"/>
+ <category term="firefox"/>
+ <category term="javascript"/>
+ <category term="mozilla"/>
+ <category term="web audio"/>
+ <author>
+ <name>sole</name>
+ </author>
+ <source>
+ <id>http://soledadpenades.com</id>
+ <link href="http://soledadpenades.com/tag/mozilla/feed/" rel="self" type="application/rss+xml"/>
+ <link href="http://soledadpenades.com" rel="alternate" type="text/html"/>
+ <subtitle>repeat 4[fd 100 rt 90]</subtitle>
+ <title>mozilla – soledad penadés</title>
+ <updated>2016-01-26T02:46:28Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>http://pierros.papadeas.gr/?p=447</id>
+ <link href="http://pierros.papadeas.gr/?p=447" rel="alternate" type="text/html"/>
+ <title>Mozillians.org Profile Edit refresh</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">Since the start of this year, Participation Infrastructure team has a renewed focus on making mozillians.org a modern community directory to meet Mozilla’s growing needs. This will not be an one-time effort. We need to invest technically and programmatically in … <a href="http://pierros.papadeas.gr/?p=447">Continue reading <span class="meta-nav">→</span></a></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>Since the start of this year, Participation Infrastructure team has a renewed focus on making mozillians.org a modern community directory to meet Mozilla’s growing needs. This will not be an one-time effort. We need to invest technically and programmatically in order to deliver a first-class product that will be the foundation for identity management across the Mozilla ecosystem.</p>
+ <p>Mozillians.org is full of functionality as it is today, but is paying the debt of being developed by 5 different teams over the past 5 years. We started simple this time. Updated all core technology pieces, did privacy and security reviews, and started the process of consolidating and modernizing many of the things we do in the site.</p>
+ <p>Our first target was Profile Edit. We chose it due to relatively self-contained nature of it, and cause many people were not happy with the current UX. After research of existing tools and applying latest best practices, we designed, coded and deployed a new profile edit interface (which by the way is renamed to Settings now) that we are happy to deliver to all Mozillians.</p>
+ <p><a href="http://pierros.papadeas.gr/wp-content/uploads/2016/01/new-profile.png" rel="attachment wp-att-448"><img alt="new-profile" class="aligncenter size-large wp-image-448" height="417" src="http://pierros.papadeas.gr/wp-content/uploads/2016/01/new-profile-1024x731.png" width="584"/></a>Have a<a href="https://mozillians.org/en-US/user/edit/"> look for yourself </a>and don’t miss the chance to update your profile while you do it!</p>
+ <p><a href="https://mozillians.org/en-US/u/comzeradd/">Nikos</a> (on the front-end), <a href="https://mozillians.org/en-US/u/akatsoulas/">Tasos</a> and <a href="https://mozillians.org/en-US/u/jgiannelos/">Nemo</a> (on the back-end) worked hard to deliver this in a speedy manner (as they are used to), and the end result is a testament to what is coming next on Mozillians.org.</p>
+ <p>Our next target? Groups. Currently it is obscure and unclear what all those settings in groups are, what is the functionality and how teams within Mozilla will be using it. We will be tackling this soon. After that, search and stats will be our attention, in an ongoing effort to fortify mozillians.org functionality. Stay tuned, and as always feel free to <a href="https://bugzilla.mozilla.org/enter_bug.cgi?product=Participation%20Infrastructure&amp;component=Phonebook">file bugs</a> and <a href="https://github.com/mozilla/mozillians">contribute </a>in the process.</p></div>
+ </content>
+ <updated>2016-01-21T11:41:39Z</updated>
+ <category term="Foss Life"/>
+ <category term="Software"/>
+ <category term="Weblog"/>
+ <category term="computer"/>
+ <category term="foss"/>
+ <category term="mozilla"/>
+ <category term="mozillians"/>
+ <category term="partinfra"/>
+ <author>
+ <name>Pierros Papadeas</name>
+ </author>
+ <source>
+ <id>http://pierros.papadeas.gr</id>
+ <link href="http://pierros.papadeas.gr/?feed=rss2&amp;tag=mozilla" rel="self" type="application/rss+xml"/>
+ <link href="http://pierros.papadeas.gr" rel="alternate" type="text/html"/>
+ <subtitle>whereabouts of a life</subtitle>
+ <title>mozilla – Pierro's Spot</title>
+ <updated>2016-01-21T11:45:53Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>http://adamlofting.com/?p=1396</id>
+ <link href="http://feedproxy.google.com/~r/adamlofting/blog/~3/DoEWpBapwiw/" rel="alternate" type="text/html"/>
+ <title>Blog posts I haven’t written lately</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">Last year I joked… Thinking about writing a blog post listing the blog posts I’ve been meaning to write… Maybe that will save some time — Adam Lofting (@adamlofting) November 20, 2015 Now, it has come to this. 9 blog posts I’ve not been writing Working on working on the impact of impact Designing Games … <a class="more-link" href="http://adamlofting.com/1396/blog-posts-i-havent-written-lately/">Continue reading <span class="screen-reader-text">Blog posts I haven’t written lately</span></a></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>Last year I joked…</p>
+ <blockquote class="twitter-tweet" lang="en">
+ <p dir="ltr" lang="en">Thinking about writing a blog post listing the blog posts I’ve been meaning to write… Maybe that will save some time</p>
+ <p>— Adam Lofting (@adamlofting) <a href="https://twitter.com/adamlofting/status/667657889817956352">November 20, 2015</a></p></blockquote>
+ <p/>
+ <p>Now, it has come to this.</p>
+ <h4>9 blog posts I’ve not been writing</h4>
+ <ul>
+ <li>Working on working on the impact of impact</li>
+ <li>Designing Games in <a href="https://en.wikipedia.org/wiki/Amateur" target="_blank">my free time</a></li>
+ <li>Moving Out (the board game)</li>
+ <li>Mozilla Foundation 2016 KPIs</li>
+ <li>Studying Network Science</li>
+ <li>Learning Analytics plans for 2016</li>
+ <li>Daily practice / you are what you do every day</li>
+ <li>Several more A/B tests to write up from <a href="http://fundraising.mozilla.org/">the fundraising campaign</a></li>
+ <li>CRM Progress in 2015</li>
+ </ul>
+ <p>But my most requested blog by far, is an update on the status of my shed / office that I was tagging on to the end my blog posts at this time last year. Many people at Mozfest wanted to know about the shed… so here it is.</p>
+ <p>This time last year:</p>
+ <blockquote class="twitter-tweet" lang="en"><p>
+ Starting in the new office today. It will take time to make it *nice* but it works for now. <a href="http://t.co/sWoC4kFNLc">pic.twitter.com/sWoC4kFNLc</a></p>
+ <p>— Adam Lofting (@adamlofting) <a href="https://twitter.com/adamlofting/status/560361913339899904">January 28, 2015</a>
+ </p></blockquote>
+ <p/>
+ <p>Some pictures from this morning:</p>
+ <p><img alt="office1" class="alignright size-large wp-image-1398" height="282" src="http://adamlofting.com/wp-content/uploads/2016/01/office1-750x320.jpg" width="660"/></p>
+ <p><img alt="office2" class="aligncenter size-large wp-image-1399" height="237" src="http://adamlofting.com/wp-content/uploads/2016/01/office2-750x269.jpg" width="660"/></p>
+ <p>It’s a pretty nice place to work now and it doubles as useful workshop on the weekends. It needs a few finishing touches, but the law of diminishing returns means those finishing touches are lower priority than work that needs to be done elsewhere in the house and garden. So it’ll stay like this a while longer.</p>
+ <img alt="" height="1" src="http://feeds.feedburner.com/~r/adamlofting/blog/~4/DoEWpBapwiw" width="1"/></div>
+ </content>
+ <updated>2016-01-21T09:44:24Z</updated>
+ <category term="Personal"/>
+ <category term="Taking note"/>
+ <category term="Work"/>
+ <category term="life"/>
+ <category term="Mozilla"/>
+ <category term="Planning"/>
+ <category term="shed"/><feedburner:origLink xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0">http://adamlofting.com/1396/blog-posts-i-havent-written-lately/</feedburner:origLink>
+ <author>
+ <name>Adam</name>
+ </author>
+ <source>
+ <id>http://adamlofting.com</id>
+ <link href="http://adamlofting.com" rel="alternate" type="text/html"/>
+ <link href="http://feeds.feedburner.com/adamlofting/blog" rel="self" type="application/rss+xml"/>
+ <link href="http://pubsubhubbub.appspot.com/" rel="hub" type="text/html"/>
+ <subtitle>Thinking out loud about metrics, systems, human experience and the web.</subtitle>
+ <title>Adam Lofting</title>
+ <updated>2016-01-21T09:46:30Z</updated>
+ </source>
+ </entry>
+
+ <entry>
+ <id>http://blog.ziade.org/2016/01/21/a-pelican-web-editor/</id>
+ <link href="http://blog.ziade.org/2016/01/21/a-pelican-web-editor/" rel="alternate" type="text/html"/>
+ <title>A Pelican web editor</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>The benefit of being a father again (Freya my 3rd child, was born last week) is
+ that while on paternity leave &amp; between two baby bottles, I can hack on fun stuff.</p>
+ <p>A few months ago, I've built for my running club a Pelican-based website, check it out
+ at : <a class="reference external" href="http://acr-dijon.org">http://acr-dijon.org</a>. Nothing's special about it, except that I am not
+ the one feeding it. The content is added by people from the club that have zero
+ knowledge about softwares, let alone stuff like vim or command line tools.</p>
+ <p>I set up a github-based flow for them, where they add content through the
+ github UI and its minimal reStructuredText preview feature - and then a few
+ of my crons update the website on the server I host.
+ For images and other media, they are uploading them via FTP using FireSSH in Firefox.</p>
+ <p>For the comments, I've switched from Disqus to <a class="reference external" href="https://posativ.org/isso/">ISSO</a>
+ after I got annoyed by the fact that it was impossible to display a simple Disqus
+ UI for people to comment without having to log in.</p>
+ <p>I had to make my club friends go through a minimal
+ reStructuredText syntax training, and things are more of less working now.</p>
+ <p>The system has a few caveats though:</p>
+ <ul class="simple">
+ <li>it's dependent on Github. I'd rather have everything hosted on my server.</li>
+ <li>the github restTRucturedText preview will not display syntax errors and warnings
+ and very often, articles get broken</li>
+ <li>the resulting reST is ugly, and it's a bit hard to force my editors to be stricter
+ about details like empty lines, not using tabs etc.</li>
+ <li>adding folders or organizing articles from Github is a pain</li>
+ <li>editing the metadata tags is prone to many mistakes</li>
+ </ul>
+ <p>So I've decided to build my own web editing tool with the following features:</p>
+ <ul class="simple">
+ <li>resTructuredText cleanup</li>
+ <li>content browsing</li>
+ <li>resTructuredText web editor with live preview that shows warnings &amp; errors</li>
+ <li>a little bit of wsgi glue and a few forms to create articles without
+ having to worry about metadata syntax.</li>
+ </ul>
+ <div class="section" id="restructuredtext-cleanup">
+ <h3>resTructuredText cleanup</h3>
+ <p>The first step was to build a reStructuredText parser that would read some
+ reStructuredText and render it back into a cleaner version.</p>
+ <p>We've imported almost 2000 articles in Pelican from the old blog, so I had
+ a <strong>lot</strong> of samples to make my parser work well.</p>
+ <p>I first tried <a class="reference external" href="https://github.com/benoitbryon/rst2rst">rst2rst</a> but that
+ parser was built for a very specific use case (text wrapping) and was
+ incomplete. It was not parsing all of the reStructuredText syntax.</p>
+ <p>Inspired by it, I wrote my own little parser using <strong>docutils</strong>.</p>
+ <p>Understanding docutils is not a small task. This project is very powerfull
+ but quite complex. One thing that cruelly misses in docutils parser tools
+ is the ability to get the source text from any node, including its children,
+ so you can render back the same source.</p>
+ <p>That's roughly what I had to add in my code. It's ugly but it does the job:
+ it will parse rst files and render the same content, minus all the extraneous
+ empty lines, spaces, tabs etc.</p>
+ </div>
+ <div class="section" id="content-browsing">
+ <h3>Content browsing</h3>
+ <p>Content browsing is pretty straightforward: my admin tool let you browse
+ the Pelican <em>content</em> directory and lists all articles, organized by categories.</p>
+ <p>In our case, each category has a top directory in <em>content</em>. The browser
+ parses the articles using my parser and displays paginated lists.</p>
+ <p>I had to add a cache system for the parser, because one of the directory
+ contains over 1000 articles -- and browsing was kind of slow :)</p>
+ <img alt="http://ziade.org/henet-browsing.png" src="http://ziade.org/henet-browsing.png"/>
+ </div>
+ <div class="section" id="restructuredtext-web-editor">
+ <h3>resTructuredText web editor</h3>
+ <p>The last big bit was the live editor. I've stumbled on a neat little tool
+ called <strong>rsted</strong>, that provides a live preview of the reStructuredText
+ as you are typing it. And it includes warnings !</p>
+ <p>Check it out: <a class="reference external" href="http://rst.ninjs.org/">http://rst.ninjs.org/</a></p>
+ <p>I've stripped it and kept what I needed, and included it in my app.</p>
+ <img alt="http://ziade.org/henet.png" src="http://ziade.org/henet.png"/>
+ <p>I am quite happy with the result so far. I need to add real tests and
+ a bit of documentation, and I will start to train my club friends on it.</p>
+ <p>The next features I'd like to add are:</p>
+ <ul class="simple">
+ <li>comments management, to replace Isso (working on it now)</li>
+ <li>smart Pelican builds. e.g. if a comment is added I don't want to rebuild the whole
+ blog (~1500 articles)</li>
+ <li>media management</li>
+ <li>spell checker</li>
+ </ul>
+ <p>The project lives here: <a class="reference external" href="https://github.com/AcrDijon/henet">https://github.com/AcrDijon/henet</a></p>
+ <p>I am not going to release it, but if someone finds it useful, I could.</p>
+ <p>It's built with Bottle &amp; Bootstrap as well.</p>
+ </div></div>
+ </summary>
+ <updated>2016-01-21T09:40:00Z</updated>
+ <category term="python"/>
+ <category term="mozilla"/>
+ <author>
+ <name>Tarek Ziade</name>
+ </author>
+ <source>
+ <id>http://blog.ziade.org</id>
+ <link href="http://blog.ziade.org" rel="alternate" type="text/html"/>
+ <link href="http://blog.ziade.org/tag/mozilla/feed" rel="self" type="application/rss+xml"/>
+ <subtitle>Tarek Ziadé</subtitle>
+ <title>Fetchez le Python</title>
+ <updated>2016-01-24T20:45:46Z</updated>
+ </source>
+ </entry>
+
+ <entry>
+ <id>http://www.ncameron.org/blog/rss/631106eb-e7b1-47d5-82f9-cb6ad210ea89</id>
+ <link href="http://www.ncameron.org/blog/closures-and-first-class-functions/" rel="alternate" type="text/html"/>
+ <title>Closures and first-class functions</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>I wrote a long and probably dull chapter on closures and first-class and higher-order functions in Rust. It goes into some detail on the implementation and some of the subtleties like higher-ranked lifetime bounds.</p>
+
+ <p>I was going to post it here too, but it is really too long. Instead, pop</p></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>I wrote a long and probably dull chapter on closures and first-class and higher-order functions in Rust. It goes into some detail on the implementation and some of the subtleties like higher-ranked lifetime bounds.</p>
+
+ <p>I was going to post it here too, but it is really too long. Instead, pop over to the 'Rust for C++ programmers' repo and read it <a href="https://github.com/nrc/r4cppp/blob/master/closures.md">there</a>.</p></div>
+ </content>
+ <updated>2016-01-21T08:36:21Z</updated>
+ <category term="Mozilla"/>
+ <category term="Rust"/>
+ <category term="rust-for-c"/>
+ <author>
+ <name>Nick Cameron</name>
+ </author>
+ <source>
+ <id>http://www.ncameron.org/blog/</id>
+ <link href="http://www.ncameron.org/blog/" rel="alternate" type="text/html"/>
+ <link href="http://www.ncameron.org/blog/rss/" rel="self" type="application/rss+xml"/>
+ <subtitle>I'm a research engineer at Mozilla working on Rust: the language, compiler, and tools. @nick_r_cameron</subtitle>
+ <title>featherweight musings</title>
+ <updated>2016-01-21T08:46:17Z</updated>
+ </source>
+ </entry>
+
+ <entry>
+ <id>http://nickdesaulniers.github.io/blog/2016/01/20/debugging-x86-64-assembly-with-lldb-and-dtrace</id>
+ <link href="http://nickdesaulniers.github.io/blog/2016/01/20/debugging-x86-64-assembly-with-lldb-and-dtrace/" rel="alternate" type="text/html"/>
+ <title>Intro to Debugging x86-64 Assembly</title>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>I’m hacking on an assembly project, and wanted to document some of the tricks I
+ was using for figuring out what was going on. This post might seem a little
+ basic for folks who spend all day heads down in gdb or who do this stuff
+ professionally, but I just wanted to share a quick intro to some tools that
+ others may find useful.
+ (<a href="https://pchiusano.github.io/2014-10-11/defensive-writing.html">oh god, I’m doing it</a>)</p>
+
+ <p>If your coming from gdb to lldb, there’s a few differences in commands. LLDB
+ has
+ <a href="http://lldb.llvm.org/lldb-gdb.html">great documentation</a>
+ on some of the differences. Everything in this post about LLDB is pretty much
+ there.</p>
+
+ <p>The bread and butter commands when working with gdb or lldb are:</p>
+
+ <ul>
+ <li>r (run the program)</li>
+ <li>s (step in)</li>
+ <li>n (step over)</li>
+ <li>finish (step out)</li>
+ <li>c (continue)</li>
+ <li>q (quit the program)</li>
+ </ul>
+
+
+ <p>You can hit enter if you want to run the last command again, which is really
+ useful if you want to keep stepping over statements repeatedly.</p>
+
+ <p>I’ve been using LLDB on OSX. Let’s say I want to debug a program I can build,
+ but is crashing or something:</p>
+
+ <figure class="code"><span/><div class="highlight"><table><tbody><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
+ </pre></td><td class="code"><pre><code class="sh"><span class="line"><span class="nv">$ </span>sudo lldb ./asmttpd web_root
+ </span></code></pre></td></tr></tbody></table></div></figure>
+
+
+ <p>Setting a breakpoint on jump to label:</p>
+
+ <figure class="code"><span/><div class="highlight"><table><tbody><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
+ <span class="line-number">2</span>
+ </pre></td><td class="code"><pre><code class="sh"><span class="line"><span class="o">(</span>lldb<span class="o">)</span> b sys_write
+ </span><span class="line">Breakpoint 3: <span class="nv">where</span> <span class="o">=</span> asmttpd<span class="sb">`</span>sys_write, <span class="nv">address</span> <span class="o">=</span> 0x00000000000029ae
+ </span></code></pre></td></tr></tbody></table></div></figure>
+
+
+ <p>Running the program until breakpoint hit:</p>
+
+ <figure class="code"><span/><div class="highlight"><table><tbody><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
+ <span class="line-number">2</span>
+ <span class="line-number">3</span>
+ <span class="line-number">4</span>
+ <span class="line-number">5</span>
+ <span class="line-number">6</span>
+ <span class="line-number">7</span>
+ <span class="line-number">8</span>
+ <span class="line-number">9</span>
+ <span class="line-number">10</span>
+ </pre></td><td class="code"><pre><code class="sh"><span class="line"><span class="o">(</span>lldb<span class="o">)</span> r
+ </span><span class="line">Process 32236 launched: <span class="s1">'./asmttpd'</span> <span class="o">(</span>x86_64<span class="o">)</span>
+ </span><span class="line">Process 32236 stopped
+ </span><span class="line">* thread <span class="c">#1: tid = 0xe69b9, 0x00000000000029ae asmttpd`sys_write, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1</span>
+ </span><span class="line"> frame <span class="c">#0: 0x00000000000029ae asmttpd`sys_write</span>
+ </span><span class="line">asmttpd<span class="sb">`</span>sys_write:
+ </span><span class="line">-&gt; 0x29ae &lt;+0&gt;: pushq %rdi
+ </span><span class="line"> 0x29af &lt;+1&gt;: pushq %rsi
+ </span><span class="line"> 0x29b0 &lt;+2&gt;: pushq %rdx
+ </span><span class="line"> 0x29b1 &lt;+3&gt;: pushq %r10
+ </span></code></pre></td></tr></tbody></table></div></figure>
+
+
+ <p>Seeing more of the current stack frame:</p>
+
+ <figure class="code"><span/><div class="highlight"><table><tbody><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
+ <span class="line-number">2</span>
+ <span class="line-number">3</span>
+ <span class="line-number">4</span>
+ <span class="line-number">5</span>
+ <span class="line-number">6</span>
+ <span class="line-number">7</span>
+ <span class="line-number">8</span>
+ <span class="line-number">9</span>
+ <span class="line-number">10</span>
+ <span class="line-number">11</span>
+ <span class="line-number">12</span>
+ <span class="line-number">13</span>
+ <span class="line-number">14</span>
+ <span class="line-number">15</span>
+ <span class="line-number">16</span>
+ <span class="line-number">17</span>
+ <span class="line-number">18</span>
+ <span class="line-number">19</span>
+ <span class="line-number">20</span>
+ <span class="line-number">21</span>
+ <span class="line-number">22</span>
+ <span class="line-number">23</span>
+ <span class="line-number">24</span>
+ </pre></td><td class="code"><pre><code class="sh"><span class="line"><span class="o">(</span>lldb<span class="o">)</span> d
+ </span><span class="line">asmttpd<span class="sb">`</span>sys_write:
+ </span><span class="line">-&gt; 0x29ae &lt;+0&gt;: pushq %rdi
+ </span><span class="line"> 0x29af &lt;+1&gt;: pushq %rsi
+ </span><span class="line"> 0x29b0 &lt;+2&gt;: pushq %rdx
+ </span><span class="line"> 0x29b1 &lt;+3&gt;: pushq %r10
+ </span><span class="line"> 0x29b3 &lt;+5&gt;: pushq %r8
+ </span><span class="line"> 0x29b5 &lt;+7&gt;: pushq %r9
+ </span><span class="line"> 0x29b7 &lt;+9&gt;: pushq %rbx
+ </span><span class="line"> 0x29b8 &lt;+10&gt;: pushq %rcx
+ </span><span class="line"> 0x29b9 &lt;+11&gt;: movq %rsi, %rdx
+ </span><span class="line"> 0x29bc &lt;+14&gt;: movq %rdi, %rsi
+ </span><span class="line"> 0x29bf &lt;+17&gt;: movq <span class="nv">$0x1</span>, %rdi
+ </span><span class="line"> 0x29c6 &lt;+24&gt;: movq <span class="nv">$0x2000004</span>, %rax
+ </span><span class="line"> 0x29cd &lt;+31&gt;: syscall
+ </span><span class="line"> 0x29cf &lt;+33&gt;: popq %rcx
+ </span><span class="line"> 0x29d0 &lt;+34&gt;: popq %rbx
+ </span><span class="line"> 0x29d1 &lt;+35&gt;: popq %r9
+ </span><span class="line"> 0x29d3 &lt;+37&gt;: popq %r8
+ </span><span class="line"> 0x29 &lt;+39&gt;: popq %r10
+ </span><span class="line"> 0x29d7 &lt;+41&gt;: popq %rdx
+ </span><span class="line"> 0x29d8 &lt;+42&gt;: popq %rsi
+ </span><span class="line"> 0x29d9 &lt;+43&gt;: popq %rdi
+ </span><span class="line"> 0x29da &lt;+44&gt;: retq
+ </span></code></pre></td></tr></tbody></table></div></figure>
+
+
+ <p>Getting a back trace (call stack):</p>
+
+ <figure class="code"><span/><div class="highlight"><table><tbody><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
+ <span class="line-number">2</span>
+ <span class="line-number">3</span>
+ <span class="line-number">4</span>
+ <span class="line-number">5</span>
+ <span class="line-number">6</span>
+ <span class="line-number">7</span>
+ </pre></td><td class="code"><pre><code class="sh"><span class="line"><span class="o">(</span>lldb<span class="o">)</span> bt
+ </span><span class="line">* thread <span class="c">#1: tid = 0xe69b9, 0x00000000000029ae asmttpd`sys_write, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1</span>
+ </span><span class="line"> * frame <span class="c">#0: 0x00000000000029ae asmttpd`sys_write</span>
+ </span><span class="line"> frame <span class="c">#1: 0x00000000000021b6 asmttpd`print_line + 16</span>
+ </span><span class="line"> frame <span class="c">#2: 0x0000000000002ab3 asmttpd`start + 35</span>
+ </span><span class="line"> frame <span class="c">#3: 0x00007fff9900c5ad libdyld.dylib`start + 1</span>
+ </span><span class="line"> frame <span class="c">#4: 0x00007fff9900c5ad libdyld.dylib`start + 1</span>
+ </span></code></pre></td></tr></tbody></table></div></figure>
+
+
+ <p>peeking at the upper stack frame:</p>
+
+ <figure class="code"><span/><div class="highlight"><table><tbody><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
+ <span class="line-number">2</span>
+ <span class="line-number">3</span>
+ <span class="line-number">4</span>
+ <span class="line-number">5</span>
+ <span class="line-number">6</span>
+ <span class="line-number">7</span>
+ </pre></td><td class="code"><pre><code class="sh"><span class="line"><span class="o">(</span>lldb<span class="o">)</span> up
+ </span><span class="line">frame <span class="c">#1: 0x00000000000021b6 asmttpd`print_line + 16</span>
+ </span><span class="line">asmttpd<span class="sb">`</span>print_line:
+ </span><span class="line"> 0x21b6 &lt;+16&gt;: movabsq <span class="nv">$0x30cb</span>, %rdi
+ </span><span class="line"> 0x21c0 &lt;+26&gt;: movq <span class="nv">$0x1</span>, %rsi
+ </span><span class="line"> 0x21c7 &lt;+33&gt;: callq 0x29ae ; sys_write
+ </span><span class="line"> 0x21cc &lt;+38&gt;: popq %rcx
+ </span></code></pre></td></tr></tbody></table></div></figure>
+
+
+ <p>back down to the breakpoint-halted stack frame:</p>
+
+ <figure class="code"><span/><div class="highlight"><table><tbody><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
+ <span class="line-number">2</span>
+ <span class="line-number">3</span>
+ <span class="line-number">4</span>
+ <span class="line-number">5</span>
+ <span class="line-number">6</span>
+ <span class="line-number">7</span>
+ </pre></td><td class="code"><pre><code class="sh"><span class="line"><span class="o">(</span>lldb<span class="o">)</span> down
+ </span><span class="line">frame <span class="c">#0: 0x00000000000029ae asmttpd`sys_write</span>
+ </span><span class="line">asmttpd<span class="sb">`</span>sys_write:
+ </span><span class="line">-&gt; 0x29ae &lt;+0&gt;: pushq %rdi
+ </span><span class="line"> 0x29af &lt;+1&gt;: pushq %rsi
+ </span><span class="line"> 0x29b0 &lt;+2&gt;: pushq %rdx
+ </span><span class="line"> 0x29b1 &lt;+3&gt;: pushq %r10
+ </span></code></pre></td></tr></tbody></table></div></figure>
+
+
+ <p>dumping the values of registers:</p>
+
+ <figure class="code"><span/><div class="highlight"><table><tbody><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
+ <span class="line-number">2</span>
+ <span class="line-number">3</span>
+ <span class="line-number">4</span>
+ <span class="line-number">5</span>
+ <span class="line-number">6</span>
+ <span class="line-number">7</span>
+ <span class="line-number">8</span>
+ <span class="line-number">9</span>
+ <span class="line-number">10</span>
+ <span class="line-number">11</span>
+ <span class="line-number">12</span>
+ <span class="line-number">13</span>
+ <span class="line-number">14</span>
+ <span class="line-number">15</span>
+ <span class="line-number">16</span>
+ <span class="line-number">17</span>
+ <span class="line-number">18</span>
+ <span class="line-number">19</span>
+ <span class="line-number">20</span>
+ <span class="line-number">21</span>
+ <span class="line-number">22</span>
+ <span class="line-number">23</span>
+ </pre></td><td class="code"><pre><code class="sh"><span class="line"><span class="o">(</span>lldb<span class="o">)</span> register <span class="nb">read</span>
+ </span><span class="line">General Purpose Registers:
+ </span><span class="line"> <span class="nv">rax</span> <span class="o">=</span> 0x0000000000002a90 asmttpd<span class="sb">`</span>start
+ </span><span class="line"> <span class="nv">rbx</span> <span class="o">=</span> 0x0000000000000000
+ </span><span class="line"> <span class="nv">rcx</span> <span class="o">=</span> 0x00007fff5fbffaf8
+ </span><span class="line"> <span class="nv">rdx</span> <span class="o">=</span> 0x00007fff5fbffa40
+ </span><span class="line"> <span class="nv">rdi</span> <span class="o">=</span> 0x00000000000030cc start_text
+ </span><span class="line"> <span class="nv">rsi</span> <span class="o">=</span> 0x000000000000000f
+ </span><span class="line"> <span class="nv">rbp</span> <span class="o">=</span> 0x00007fff5fbffa18
+ </span><span class="line"> <span class="nv">rsp</span> <span class="o">=</span> 0x00007fff5fbff9b8
+ </span><span class="line"> <span class="nv">r8</span> <span class="o">=</span> 0x0000000000000000
+ </span><span class="line"> <span class="nv">r9</span> <span class="o">=</span> 0x00007fff7b1670c8 atexit_mutex + 24
+ </span><span class="line"> <span class="nv">r10</span> <span class="o">=</span> 0x00000000ffffffff
+ </span><span class="line"> <span class="nv">r11</span> <span class="o">=</span> 0xffffffff00000000
+ </span><span class="line"> <span class="nv">r12</span> <span class="o">=</span> 0x0000000000000000
+ </span><span class="line"> <span class="nv">r13</span> <span class="o">=</span> 0x0000000000000000
+ </span><span class="line"> <span class="nv">r14</span> <span class="o">=</span> 0x0000000000000000
+ </span><span class="line"> <span class="nv">r15</span> <span class="o">=</span> 0x0000000000000000
+ </span><span class="line"> <span class="nv">rip</span> <span class="o">=</span> 0x00000000000029ae asmttpd<span class="sb">`</span>sys_write
+ </span><span class="line"> <span class="nv">rflags</span> <span class="o">=</span> 0x0000000000000246
+ </span><span class="line"> <span class="nv">cs</span> <span class="o">=</span> 0x000000000000002b
+ </span><span class="line"> <span class="nv">fs</span> <span class="o">=</span> 0x0000000000000000
+ </span><span class="line"> <span class="nv">gs</span> <span class="o">=</span> 0x0000000000000000
+ </span></code></pre></td></tr></tbody></table></div></figure>
+
+
+ <p>read just one register:</p>
+
+ <figure class="code"><span/><div class="highlight"><table><tbody><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
+ <span class="line-number">2</span>
+ </pre></td><td class="code"><pre><code class="sh"><span class="line"><span class="o">(</span>lldb<span class="o">)</span> register <span class="nb">read </span>rdi
+ </span><span class="line"> <span class="nv">rdi</span> <span class="o">=</span> 0x00000000000030cc start_text
+ </span></code></pre></td></tr></tbody></table></div></figure>
+
+
+ <p>When you’re trying to figure out what system calls are made by some C code,
+ using dtruss is very helpful. dtruss is available on OSX and seems to be some
+ kind of wrapper around DTrace.</p>
+
+ <figure class="code"><span/><div class="highlight"><table><tbody><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
+ <span class="line-number">2</span>
+ <span class="line-number">3</span>
+ <span class="line-number">4</span>
+ <span class="line-number">5</span>
+ <span class="line-number">6</span>
+ <span class="line-number">7</span>
+ <span class="line-number">8</span>
+ <span class="line-number">9</span>
+ <span class="line-number">10</span>
+ <span class="line-number">11</span>
+ <span class="line-number">12</span>
+ <span class="line-number">13</span>
+ <span class="line-number">14</span>
+ <span class="line-number">15</span>
+ <span class="line-number">16</span>
+ </pre></td><td class="code"><pre><code class="sh"><span class="line"><span class="nv">$ </span>cat sleep.c
+ </span><span class="line"><span class="c">#include &lt;time.h&gt;</span>
+ </span><span class="line">int main <span class="o">()</span> <span class="o">{</span>
+ </span><span class="line"> struct timespec <span class="nv">rqtp</span> <span class="o">=</span> <span class="o">{</span>
+ </span><span class="line"> 2,
+ </span><span class="line"> 0
+ </span><span class="line"> <span class="o">}</span>;
+ </span><span class="line">
+ </span><span class="line"> nanosleep<span class="o">(</span>&amp;rqtp, NULL<span class="o">)</span>;
+ </span><span class="line"><span class="o">}</span>
+ </span><span class="line">
+ </span><span class="line"><span class="nv">$ </span>clang sleep.c
+ </span><span class="line">
+ </span><span class="line"><span class="nv">$ </span>sudo dtruss ./a.out
+ </span><span class="line">...all kinds of fun stuff
+ </span><span class="line">__semwait_signal<span class="o">(</span>0xB03, 0x0, 0x1<span class="o">)</span> <span class="o">=</span> -1 Err#60
+ </span></code></pre></td></tr></tbody></table></div></figure>
+
+
+ <p>If you compile with <code>-g</code> to emit debug symbols, you can use lldb’s disassemble
+ command to get the equivalent assembly:</p>
+
+ <figure class="code"><span/><div class="highlight"><table><tbody><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
+ <span class="line-number">2</span>
+ <span class="line-number">3</span>
+ <span class="line-number">4</span>
+ <span class="line-number">5</span>
+ <span class="line-number">6</span>
+ <span class="line-number">7</span>
+ <span class="line-number">8</span>
+ <span class="line-number">9</span>
+ <span class="line-number">10</span>
+ <span class="line-number">11</span>
+ <span class="line-number">12</span>
+ <span class="line-number">13</span>
+ <span class="line-number">14</span>
+ <span class="line-number">15</span>
+ <span class="line-number">16</span>
+ <span class="line-number">17</span>
+ <span class="line-number">18</span>
+ <span class="line-number">19</span>
+ <span class="line-number">20</span>
+ <span class="line-number">21</span>
+ <span class="line-number">22</span>
+ <span class="line-number">23</span>
+ <span class="line-number">24</span>
+ <span class="line-number">25</span>
+ <span class="line-number">26</span>
+ <span class="line-number">27</span>
+ <span class="line-number">28</span>
+ <span class="line-number">29</span>
+ <span class="line-number">30</span>
+ <span class="line-number">31</span>
+ <span class="line-number">32</span>
+ <span class="line-number">33</span>
+ <span class="line-number">34</span>
+ <span class="line-number">35</span>
+ <span class="line-number">36</span>
+ <span class="line-number">37</span>
+ </pre></td><td class="code"><pre><code class="sh"><span class="line"><span class="nv">$ </span>clang sleep.c -g
+ </span><span class="line"><span class="nv">$ </span>lldb a.out
+ </span><span class="line"><span class="o">(</span>lldb<span class="o">)</span> target create <span class="s2">"a.out"</span>
+ </span><span class="line">Current executable <span class="nb">set </span>to <span class="s1">'a.out'</span> <span class="o">(</span>x86_64<span class="o">)</span>.
+ </span><span class="line"><span class="o">(</span>lldb<span class="o">)</span> b main
+ </span><span class="line">Breakpoint 1: <span class="nv">where</span> <span class="o">=</span> a.out<span class="sb">`</span>main + 16 at sleep.c:3, <span class="nv">address</span> <span class="o">=</span> 0x0000000100000f40
+ </span><span class="line"><span class="o">(</span>lldb<span class="o">)</span> r
+ </span><span class="line">Process 33213 launched: <span class="s1">'/Users/Nicholas/code/assembly/asmttpd/a.out'</span> <span class="o">(</span>x86_64<span class="o">)</span>
+ </span><span class="line">Process 33213 stopped
+ </span><span class="line">* thread <span class="c">#1: tid = 0xeca04, 0x0000000100000f40 a.out`main + 16 at sleep.c:3, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1</span>
+ </span><span class="line"> frame <span class="c">#0: 0x0000000100000f40 a.out`main + 16 at sleep.c:3</span>
+ </span><span class="line"> 1 <span class="c">#include &lt;time.h&gt;</span>
+ </span><span class="line"> 2 int main <span class="o">()</span> <span class="o">{</span>
+ </span><span class="line">-&gt; 3 struct timespec <span class="nv">rqtp</span> <span class="o">=</span> <span class="o">{</span>
+ </span><span class="line"> 4 2,
+ </span><span class="line"> 5 0
+ </span><span class="line"> 6 <span class="o">}</span>;
+ </span><span class="line"> 7
+ </span><span class="line"><span class="o">(</span>lldb<span class="o">)</span> disassemble
+ </span><span class="line">a.out<span class="sb">`</span>main:
+ </span><span class="line"> 0x100000f30 &lt;+0&gt;: pushq %rbp
+ </span><span class="line"> 0x100000f31 &lt;+1&gt;: movq %rsp, %rbp
+ </span><span class="line"> 0x100000f34 &lt;+4&gt;: subq <span class="nv">$0x20</span>, %rsp
+ </span><span class="line"> 0x100000f38 &lt;+8&gt;: leaq -0x10<span class="o">(</span>%rbp<span class="o">)</span>, %rdi
+ </span><span class="line"> 0x100000f3c &lt;+12&gt;: xorl %eax, %eax
+ </span><span class="line"> 0x100000f3e &lt;+14&gt;: movl %eax, %esi
+ </span><span class="line">-&gt; 0x100000f40 &lt;+16&gt;: movq 0x49<span class="o">(</span>%rip<span class="o">)</span>, %rcx
+ </span><span class="line"> 0x100000f47 &lt;+23&gt;: movq %rcx, -0x10<span class="o">(</span>%rbp<span class="o">)</span>
+ </span><span class="line"> 0x100000f4b &lt;+27&gt;: movq 0x46<span class="o">(</span>%rip<span class="o">)</span>, %rcx
+ </span><span class="line"> 0x100000f52 &lt;+34&gt;: movq %rcx, -0x8<span class="o">(</span>%rbp<span class="o">)</span>
+ </span><span class="line"> 0x100000f56 &lt;+38&gt;: callq 0x100000f68 ; symbol stub <span class="k">for</span>: nanosleep
+ </span><span class="line"> 0x100000f5b &lt;+43&gt;: xorl %edx, %edx
+ </span><span class="line"> 0x100000f5d &lt;+45&gt;: movl %eax, -0x14<span class="o">(</span>%rbp<span class="o">)</span>
+ </span><span class="line"> 0x100000f60 &lt;+48&gt;: movl %edx, %eax
+ </span><span class="line"> 0x100000f62 &lt;+50&gt;: addq <span class="nv">$0x20</span>, %rsp
+ </span><span class="line"> 0x100000f66 &lt;+54&gt;: popq %rbp
+ </span><span class="line"> 0x100000f67 &lt;+55&gt;: retq
+ </span></code></pre></td></tr></tbody></table></div></figure>
+
+
+ <p>Anyways, I’ve been learning some interesting things about OSX that I’ll be
+ sharing soon. If you’d like to learn more about x86-64 assembly programming,
+ you should read my other posts about
+ <a href="http://nickdesaulniers.github.io/blog/2014/04/18/lets-write-some-x86-64/">writing x86-64</a>
+ and a toy
+ <a href="http://nickdesaulniers.github.io/blog/2015/05/25/interpreter-compiler-jit/">JIT for Brainfuck</a>
+ (<a href="https://www.reddit.com/r/programming/comments/377ov9/interpreter_compiler_jit/crkkrz4">the creator of Brainfuck liked it</a>).</p>
+
+ <p>I should also do a post on
+ <a href="http://rr-project.org/">Mozilla’s rr</a>,
+ because it can do amazing things like step backwards. Another day…</p></div>
+ </content>
+ <updated>2016-01-21T04:04:00Z</updated>
+ <source>
+ <id>http://nickdesaulniers.github.io/</id>
+ <author>
+ <name>Nick Desaulniers</name>
+ </author>
+ <link href="http://nickdesaulniers.github.io/atom.xml" rel="self" type="application/atom+xml"/>
+ <link href="http://nickdesaulniers.github.io/" rel="alternate" type="text/html"/>
+ <title>Nick Desaulniers</title>
+ <updated>2016-01-21T05:07:32Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en">
+ <id>https://rail.merail.ca/posts/rebooting-productivity.html</id>
+ <link href="https://rail.merail.ca/posts/rebooting-productivity.html" rel="alternate" type="text/html"/>
+ <title>Rebooting productivity</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><div><p>Every new year gives you an opportunity to sit back, relax,
+ <span class="strike">have some scotch</span> and re-think the passed year. Holidays give
+ you enough free time. Even if you decide to not take a vacation around
+ the holidays, it's usually calm and peaceful.</p>
+ <p>This time, I found myself thinking mostly about productivity, being
+ effective, feeling busy, overwhelmed with work and other related topics.</p>
+ <p>When I started at Mozilla (almost 6 years ago!), I tried to apply all my
+ GTD and time management knowledge and techniques. Working remotely and
+ in a different time zone was an advantage - I had close to zero
+ interruptions. It worked perfect.</p>
+ <p>Last year I realized that my productivity skills had faded away somehow.
+ 40h+ workweeks, working on weekends, delivering goals in the last week
+ of quarter don't sound like good signs. Instead of being productive I
+ felt busy.</p>
+ <p>"Every crisis is an opportunity". Time to make a step back and reboot
+ myself. Burning out at work is not a good idea. :)</p>
+ <p>Here are some ideas/tips that I wrote down for myself you may found
+ useful.</p>
+ <div class="section" id="health-related">
+ <h3>Health related</h3>
+ <ul class="simple">
+ <li>Morning exercises. A 20-minute walk will wake your brain up and
+ generate enough endorphins for the first half of the day.</li>
+ <li>Meditation. 2x20min a day is ideal; 2x10min would work too. Something
+ like <a class="reference external" href="http://www.calm.com/">calm.com</a> makes this a peace of cake.</li>
+ </ul>
+ </div>
+ <div class="section" id="concentration">
+ <h3>Concentration</h3>
+ <ul class="simple">
+ <li>Task #1: make a daily plan. No plan - no work.</li>
+ <li>Don't start your day by reading emails. Get one (little) thing done
+ first - THEN check your email.</li>
+ <li>Try to define outcomes, not tasks. "Ship XYZ" instead of "Work on XYZ".</li>
+ <li>Meetings are time consuming, so "Set a goal for each meeting".
+ Consider skipping a meeting if you don't have any goal set, unless it's a
+ beer-and-tell meeting! :)</li>
+ <li>Constantly ask yourself if what you're working on is important.</li>
+ <li>3-4 times a day ask yourself whether you are doing something towards
+ your goal or just finding something else to keep you busy. If you want
+ to look busy, take your phone and walk around the office with some
+ papers in your hand. Everybody will think that you are a busy person!
+ This way you can take a break and look busy at the same time!</li>
+ <li>Take breaks! <a class="reference external" href="https://en.wikipedia.org/wiki/Pomodoro_Technique">Pomodoro technique</a> has this option
+ built-in. Taking breaks helps not only to avoid <a class="reference external" href="https://en.wikipedia.org/wiki/Repetitive_strain_injury">RSI</a>, but also
+ keeps your brain sane and gives you time to ask yourself the questions
+ mentioned above. I use <a class="reference external" href="http://www.workrave.org/">Workrave</a> on my
+ laptop, but you can use a real kitchen timer instead.</li>
+ <li>Wear headphones, especially at office. Noise cancelling ones are even
+ better. White noise, nature sounds, or instrumental music are your
+ friends.</li>
+ </ul>
+ </div>
+ <div class="section" id="home-office">
+ <h3>(Home) Office</h3>
+ <ul class="simple">
+ <li>Make sure you enjoy your work environment. Why on the earth would you
+ spend your valuable time working without joy?!</li>
+ <li>De-clutter and organize your desk. Less things around - less
+ distractions.</li>
+ <li>Desk, chair, monitor, keyboard, mouse, etc - don't cheap out on them.
+ Your health is more important and expensive. Thanks to <a class="reference external" href="https://twitter.com/mhoye">mhoye</a> for this advice!</li>
+ </ul>
+ </div>
+ <div class="section" id="other">
+ <h3>Other</h3>
+ <ul class="simple">
+ <li>Don't check email every 30 seconds. If there is an emergency, they
+ will call you! :)</li>
+ <li>Reward yourself at a certain time. "I'm going to have a chocolate at
+ 11am", or "MFBT at 4pm sharp!" are good examples. Don't forget, you
+ are <a class="reference external" href="https://en.wikipedia.org/wiki/Classical_conditioning">Pavlov's dog</a> too!</li>
+ <li>Don't try to read everything NOW. Save it for later and read in a
+ batch.</li>
+ <li>Capture all creative ideas. You can delete them later. ;)</li>
+ <li>Prepare for next task before break. Make sure you know what's next, so
+ you can think about it during the break.</li>
+ </ul>
+ <p>This is my list of things that I try to use everyday. Looking forward to
+ see improvements!</p>
+ <p>I would appreciate your thoughts this topic. Feel free to comment or
+ send a private email.</p>
+ <p>Happy Productive New Year!</p>
+ </div></div></div>
+ </summary>
+ <updated>2016-01-21T02:06:37Z</updated>
+ <category term="mozilla"/>
+ <category term="productivity"/>
+ <author>
+ <name>Rail Aliiev</name>
+ </author>
+ <source>
+ <id>https://rail.merail.ca/</id>
+ <link href="https://rail.merail.ca/" rel="alternate" type="text/html"/>
+ <link href="https://rail.merail.ca/categories/mozilla.xml" rel="self" type="application/rss+xml"/>
+ <title>Rail's Blog (mozilla)</title>
+ <updated>2016-01-21T02:31:38Z</updated>
+ </source>
+ </entry>
+
+ <entry>
+ <id>http://blog.rust-lang.org/2016/01/21/Rust-1.6.html</id>
+ <link href="http://blog.rust-lang.org/2016/01/21/Rust-1.6.html" rel="alternate" type="text/html"/>
+ <title>Announcing Rust 1.6</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>Hello 2016! We’re happy to announce the first Rust release of the year, 1.6.
+ Rust is a systems programming language focused on safety, speed, and
+ concurrency.</p>
+
+ <p>As always, you can <a href="http://www.rust-lang.org/install.html">install Rust 1.6</a> from the appropriate page on our
+ website, and check out the <a href="https://github.com/rust-lang/rust/blob/stable/RELEASES.md#version-160-2016-01-21">detailed release notes for 1.6</a> on GitHub.
+ About 1100 patches were landed in this release.</p>
+
+ <h3 id="what-39-s-in-1-6-stable">What’s in 1.6 stable</h3>
+
+ <p>This release contains a number of small refinements, one major feature, and
+ a change to <a href="https://crates.io">Crates.io</a>.</p>
+
+ <h4 id="libcore-stabilization">libcore stabilization</h4>
+
+ <p>The largest new feature in 1.6 is that <a href="http://doc.rust-lang.org/nightly/core/"><code>libcore</code></a> is now stable! Rust’s
+ standard library is two-tiered: there’s a small core library, <code>libcore</code>, and
+ the full standard library, <code>libstd</code>, that builds on top of it. <code>libcore</code> is
+ completely platform agnostic, and requires only a handful of external symbols
+ to be defined. Rust’s <code>libstd</code> builds on top of <code>libcore</code>, adding support for
+ memory allocation, I/O, and concurrency. Applications using Rust in the
+ embedded space, as well as those writing operating systems, often eschew
+ <code>libstd</code>, using only <code>libcore</code>.</p>
+
+ <p><code>libcore</code> being stabilized is a major step towards being able to write the
+ lowest levels of software using stable Rust. There’s still future work to be
+ done, however. This will allow for a library ecosystem to develop around
+ <code>libcore</code>, but <em>applications</em> are not fully supported yet. Expect to hear more
+ about this in future release notes.</p>
+
+ <h4 id="library-stabilizations">Library stabilizations</h4>
+
+ <p>About 30 library functions and methods are now stable in 1.6. Notable
+ improvements include:</p>
+
+ <p>The <code>drain()</code> family of functions on collections. These methods let you move
+ elements out of a collection while allowing them to retain their backing
+ memory, reducing allocation in certain situations.</p>
+
+ <p>A number of implementations of <code>From</code> for converting between standard library
+ types, mainly between various integral and floating-point types.</p>
+
+ <p>Finally, <code>Vec::extend_from_slice()</code>, which was previously known as
+ <code>push_all()</code>. This method has a significantly faster implementation than the
+ more general <code>extend()</code>.</p>
+
+ <p>See the <a href="https://github.com/rust-lang/rust/blob/stable/RELEASES.md#version-160-2016-01-21">detailed release notes</a> for more.</p>
+
+ <h4 id="crates-io-disallows-wildcards">Crates.io disallows wildcards</h4>
+
+ <p>If you maintain a crate on <a href="https://crates.io">Crates.io</a>, you might have seen
+ a warning: newly uploaded crates are no longer allowed to use a wildcard when
+ describing their dependencies. In other words, this is not allowed:</p>
+ <div class="highlight"><pre><code class="language-toml"><span class="p">[</span><span class="n">dependencies</span><span class="p">]</span>
+ <span class="n">regex</span> <span class="o">=</span> <span class="s">"*"</span>
+ </code></pre></div>
+ <p>Instead, you must actually specify <a href="http://doc.crates.io/crates-io.html#using-cratesio-based-crates">a specific version or range of
+ versions</a>, using one of the <code>semver</code> crate’s various options: <code>^</code>,
+ <code>~</code>, or <code>=</code>.</p>
+
+ <p>A wildcard dependency means that you work with any possible version of your
+ dependency. This is highly unlikely to be true, and causes unnecessary breakage
+ in the ecosystem. We’ve been advertising this change as a warning for some time;
+ now it’s time to turn it into an error.</p>
+
+ <h3 id="contributors-to-1-6">Contributors to 1.6</h3>
+
+ <p>We had 132 individuals contribute to 1.6. Thank you so much!</p>
+
+ <ul>
+ <li>Aaron Turon</li>
+ <li>Adam Badawy</li>
+ <li>Aleksey Kladov</li>
+ <li>Alexander Bulaev</li>
+ <li>Alex Burka</li>
+ <li>Alex Crichton</li>
+ <li>Alex Gaynor</li>
+ <li>Alexis Beingessner</li>
+ <li>Amanieu d'Antras</li>
+ <li>Amit Saha</li>
+ <li>Andrea Canciani</li>
+ <li>Andrew Paseltiner</li>
+ <li>androm3da</li>
+ <li>angelsl</li>
+ <li>Angus Lees</li>
+ <li>Antti Keränen</li>
+ <li>arcnmx</li>
+ <li>Ariel Ben-Yehuda</li>
+ <li>Ashkan Kiani</li>
+ <li>Barosl Lee</li>
+ <li>Benjamin Herr</li>
+ <li>Ben Striegel</li>
+ <li>Bhargav Patel</li>
+ <li>Björn Steinbrink</li>
+ <li>Boris Egorov</li>
+ <li>bors</li>
+ <li>Brian Anderson</li>
+ <li>Bruno Tavares</li>
+ <li>Bryce Van Dyk</li>
+ <li>Cameron Sun</li>
+ <li>Christopher Sumnicht</li>
+ <li>Cole Reynolds</li>
+ <li>corentih</li>
+ <li>Daniel Campbell</li>
+ <li>Daniel Keep</li>
+ <li>Daniel Rollins</li>
+ <li>Daniel Trebbien</li>
+ <li>Danilo Bargen</li>
+ <li>Devon Hollowood</li>
+ <li>Doug Goldstein</li>
+ <li>Dylan McKay</li>
+ <li>ebadf</li>
+ <li>Eli Friedman</li>
+ <li>Eric Findlay</li>
+ <li>Erik Davidson</li>
+ <li>Felix S. Klock II</li>
+ <li>Florian Hahn</li>
+ <li>Florian Hartwig</li>
+ <li>Gleb Kozyrev</li>
+ <li>Guillaume Gomez</li>
+ <li>Huon Wilson</li>
+ <li>Igor Shuvalov</li>
+ <li>Ivan Ivaschenko</li>
+ <li>Ivan Kozik</li>
+ <li>Ivan Stankovic</li>
+ <li>Jack Fransham</li>
+ <li>Jake Goulding</li>
+ <li>Jake Worth</li>
+ <li>James Miller</li>
+ <li>Jan Likar</li>
+ <li>Jean Maillard</li>
+ <li>Jeffrey Seyfried</li>
+ <li>Jethro Beekman</li>
+ <li>John KÃ¥re Alsaker</li>
+ <li>John Talling</li>
+ <li>Jonas Schievink</li>
+ <li>Jonathan S</li>
+ <li>Jose Narvaez</li>
+ <li>Josh Austin</li>
+ <li>Josh Stone</li>
+ <li>Joshua Holmer</li>
+ <li>JP Sugarbroad</li>
+ <li>jrburke</li>
+ <li>Kevin Butler</li>
+ <li>Kevin Yeh</li>
+ <li>Kohei Hasegawa</li>
+ <li>Kyle Mayes</li>
+ <li>Lee Jeffery</li>
+ <li>Manish Goregaokar</li>
+ <li>Marcell Pardavi</li>
+ <li>Markus Unterwaditzer</li>
+ <li>Martin Pool</li>
+ <li>Marvin Löbel</li>
+ <li>Matt Brubeck</li>
+ <li>Matthias Bussonnier</li>
+ <li>Matthias Kauer</li>
+ <li>mdinger</li>
+ <li>Michael Layzell</li>
+ <li>Michael Neumann</li>
+ <li>Michael Sproul</li>
+ <li>Michael Woerister</li>
+ <li>Mihaly Barasz</li>
+ <li>Mika Attila</li>
+ <li>mitaa</li>
+ <li>Ms2ger</li>
+ <li>Nicholas Mazzuca</li>
+ <li>Nick Cameron</li>
+ <li>Niko Matsakis</li>
+ <li>Ole Krüger</li>
+ <li>Oliver Middleton</li>
+ <li>Oliver Schneider</li>
+ <li>Ori Avtalion</li>
+ <li>Paul A. Jungwirth</li>
+ <li>Peter Atashian</li>
+ <li>Philipp Matthias Schäfer</li>
+ <li>pierzchalski</li>
+ <li>Ravi Shankar</li>
+ <li>Ricardo Martins</li>
+ <li>Ricardo Signes</li>
+ <li>Richard Diamond</li>
+ <li>Rizky Luthfianto</li>
+ <li>Ryan Scheel</li>
+ <li>Scott Olson</li>
+ <li>Sean Griffin</li>
+ <li>Sebastian Hahn</li>
+ <li>Sébastien Marie</li>
+ <li>Seo Sanghyeon</li>
+ <li>Simonas Kazlauskas</li>
+ <li>Simon Sapin</li>
+ <li>Stepan Koltsov</li>
+ <li>Steve Klabnik</li>
+ <li>Steven Fackler</li>
+ <li>Tamir Duberstein</li>
+ <li>Tobias Bucher</li>
+ <li>Toby Scrace</li>
+ <li>Tshepang Lekhonkhobe</li>
+ <li>Ulrik Sverdrup</li>
+ <li>Vadim Chugunov</li>
+ <li>Vadim Petrochenkov</li>
+ <li>William Throwe</li>
+ <li>xd1le</li>
+ <li>Xmasreturns</li>
+ </ul></div>
+ </summary>
+ <updated>2016-01-21T00:00:00Z</updated>
+ <source>
+ <id>http://blog.rust-lang.org/</id>
+ <author>
+ <name>The Rust Programming Language Blog</name>
+ </author>
+ <link href="http://blog.rust-lang.org/" rel="alternate" type="text/html"/>
+ <link href="http://blog.rust-lang.org/feed.xml" rel="self" type="application/rss+xml"/>
+ <subtitle>Words from the Rust team</subtitle>
+ <title>The Rust Programming Language Blog</title>
+ <updated>2016-01-21T21:36:56Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>http://blog.mozilla.org/addons/?p=7644</id>
+ <link href="https://blog.mozilla.org/addons/2016/01/20/archiving-amo-stats/" rel="alternate" type="text/html"/>
+ <title>Archiving AMO Stats</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">One of the advantages of listing an add-on or theme on addons.mozilla.org (AMO) is that you’ll get statistics on your add-on’s usage. These stats, which are covered by the Mozilla privacy policy, provide add-on developers with information such as the … <a class="go" href="https://blog.mozilla.org/addons/2016/01/20/archiving-amo-stats/">Continue reading</a></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>One of the advantages of listing an add-on or theme on <a href="https://addons.mozilla.org" target="_blank">addons.mozilla.org</a> (AMO) is that you’ll get statistics on your add-on’s usage. These stats, which are covered by the <a href="https://www.mozilla.org/privacy/" target="_blank">Mozilla privacy policy</a>, provide add-on developers with information such as the number of downloads and daily users, among other insights.</p>
+ <p>Currently, the data that generates these statistics can go back as far as 2007, as we haven’t had an archiving policy. As a result, statistics take up the vast majority of disk space in our database and require a significant amount of processing and operations time. Statistics over a year old are very rarely accessed, and the value of their generation is very low, while the costs are increasing.</p>
+ <p>To reduce our operating and development costs, and increase the site’s reliability for developers, we are introducing an archiving policy.</p>
+ <p>In the coming weeks, statistics data <strong>over one year old</strong> will no longer be stored in the AMO database, and reports generated from them will no longer be accessible through AMO’s add-on statistics pages. Instead, the data will be archived and maintained as plain text files, which developers can download. We will write a follow-up post when these archives become available.</p>
+ <p>If you’ve chosen to keep your add-on’s statistics private, they will remain private when stats are archived. You can check your privacy settings by going to your add-on in the <a href="https://addons.mozilla.org/developers/addons" target="_blank">Developer Hub</a>, clicking on <strong>Edit Listing</strong>, and then <strong>Technical Details</strong>.</p>
+ <p><a href="https://blog.mozilla.org/addons/files/2016/01/Screenshot-2016-01-20-14.52.33.png"><img alt="editlisting" class="alignnone size-large wp-image-7645" height="389" src="https://blog.mozilla.org/addons/files/2016/01/Screenshot-2016-01-20-14.52.33-600x389.png" width="600"/></a></p>
+ <p>The total number of users and other cumulative counts on add-ons and themes will not be affected and these will continue to function.</p>
+ <p>If you have feedback or concerns, please head to our <a href="https://discourse.mozilla-community.org/t/archiving-of-add-on-statistics/6573" target="_blank">forum post</a> on this topic.</p></div>
+ </content>
+ <updated>2016-01-20T23:54:09Z</updated>
+ <category term="developers"/>
+ <category term="policy"/>
+ <author>
+ <name>Andy McKay</name>
+ </author>
+ <source>
+ <id>https://blog.mozilla.org/addons</id>
+ <link href="https://blog.mozilla.org/addons/feed/" rel="self" type="application/rss+xml"/>
+ <link href="https://blog.mozilla.org/addons" rel="alternate" type="text/html"/>
+ <title>Mozilla Add-ons Blog</title>
+ <updated>2016-01-25T20:46:40Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>https://air.mozilla.org/the-joy-of-coding-episode-41/</id>
+ <link href="https://air.mozilla.org/the-joy-of-coding-episode-41/" rel="alternate" type="text/html"/>
+ <title>The Joy of Coding - Episode 41</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>
+ <img alt="The Joy of Coding - Episode 41" class="wp-post-image" height="90" src="https://air.cdn.mozilla.net/media/cache/cb/68/cb68b6ac48452be7e7f25ddc7b63c959.png" width="160"/>
+ mconley livehacks on real Firefox bugs while thinking aloud.
+ </p></div>
+ </summary>
+ <updated>2016-01-20T18:00:00Z</updated>
+ <author>
+ <name>Air Mozilla</name>
+ </author>
+ <source>
+ <id>https://air.mozilla.org/</id>
+ <link href="https://air.mozilla.org/" rel="alternate" type="text/html"/>
+ <link href="https://air.mozilla.org/" rel="self" type="application/rss+xml"/>
+ <rights>Except where otherwise noted, content on this site is licensed under the Creative Commons Attribution Share-Alike License v3.0 or any later version.</rights>
+ <subtitle>Air Mozilla is the Internet multimedia presence of Mozilla, with live and pre-recorded shows, interviews, news snippets, tutorial videos, and features about the Mozilla community.</subtitle>
+ <title>Air Mozilla</title>
+ <updated>2016-01-25T20:31:50Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>http://blog.mozilla.org/nfroyd/?p=452</id>
+ <link href="https://blog.mozilla.org/nfroyd/2016/01/20/gecko-and-c-onboarding-presentation/" rel="alternate" type="text/html"/>
+ <title>gecko and c++ onboarding presentation</title>
+ <summary>One of the things the Firefox team has been doing recently is having onboarding sessions for new hires. This onboarding currently covers: 1st day setup Bugzilla Building Firefox Desktop Firefox Architecture / Product Communication and Community Javascript and the DOM C++ and Gecko Shipping Software Telemetry Org structure and career development My first day consisted […]</summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>One of the things the Firefox team has been doing recently is having onboarding sessions for new hires. This onboarding currently covers:</p>
+ <ul>
+ <li>1st day setup</li>
+ <li>Bugzilla</li>
+ <li>Building Firefox</li>
+ <li>Desktop Firefox Architecture / Product</li>
+ <li>Communication and Community</li>
+ <li>Javascript and the DOM</li>
+ <li>C++ and Gecko</li>
+ <li>Shipping Software</li>
+ <li>Telemetry</li>
+ <li>Org structure and career development</li>
+ </ul>
+ <p>My first day consisted of some useful HR presentations and then I was given my laptop and a pointer to a wiki page on building Firefox. Needless to say, it took me a while to get started! It would have been super convenient to have an introduction to all the stuff above.</p>
+ <p>I’ve been asked to do the C++ and Gecko session three times. All of the sessions are open to whoever wants to come, not just the new hires, and I think yesterday’s session was easily the most well-attended yet: somewhere between 10 and 20 people showed up. Yesterday’s session was the first session where I made the slides available to attendees (should have been doing that from the start…) and it seemed equally useful to make the slides available to a broader audience as well. The <a href="https://docs.google.com/presentation/d/1ZHUkNzZK2TrF5_4MWd_lqEq7Ph5B6CDbNsizIkBxbnQ/edit?usp=sharing">Gecko and C++ Onboarding slides</a> are up now!</p>
+ <p>This presentation is a “living†presentation; it will get updated for future sessions with feedback and as I think of things that should have been in the presentation or better ways to set things up (some diagrams would be nice…). If you have feedback (good, bad, or ugly) on particular things in the slides or you have suggestions on what other things should be covered, please contact me! Next time I do this I’ll try to record the presentation so folks can watch that if they prefer.</p></div>
+ </content>
+ <updated>2016-01-20T16:48:29Z</updated>
+ <category term="Uncategorized"/>
+ <category term="c++"/>
+ <category term="mozilla"/>
+ <category term="onboarding"/>
+ <category term="presentations"/>
+ <author>
+ <name>Nathan Froyd</name>
+ </author>
+ <source>
+ <id>https://blog.mozilla.org/nfroyd</id>
+ <link href="https://blog.mozilla.org/nfroyd/feed/" rel="self" type="application/rss+xml"/>
+ <link href="https://blog.mozilla.org/nfroyd" rel="alternate" type="text/html"/>
+ <subtitle>writing code to help other people write code</subtitle>
+ <title>Nathan's Blog</title>
+ <updated>2016-01-20T17:01:01Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en">
+ <id>http://andreasgal.com/?p=573</id>
+ <link href="http://andreasgal.com/2016/01/20/brendan-is-back-to-save-the-web/" rel="alternate" type="text/html"/>
+ <title>Brendan is back to save the Web</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">Brendan is back, and he has a plan to save the Web. Its a big and bold plan, and it may just work. I am pretty excited about this. If you have 5 minutes to read along I’ll explain why I think you should be as well. The Web is broken Lets face it, the Web […]<img alt="" border="0" height="1" src="http://pixel.wp.com/b.gif?host=andreasgal.com&amp;blog=891661&amp;post=573&amp;subd=andreasgal&amp;ref=&amp;feed=1" width="1"/></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p class="p1">Brendan is <a href="https://github.com/brave">back</a>, and he has a <a href="http://brave.com/">plan</a> to save the Web. Its a big and bold plan, and it may just work. I am pretty excited about this. If you have 5 minutes to read along I’ll explain why I think you should be as well.</p>
+ <p class="p1"><strong>The Web is broken</strong></p>
+ <p class="p1">Lets face it, the Web today is a mess. Everywhere we go online we are constantly inundated with annoying ads. Often pages are more ads than content, and the more ads the industry throws at us, the more we ignore them, the more obnoxious ads get, trying to catch our attention. As Brendan explains in his blog post, the browser used to be on the user’s side—we call browsers the user agent for a reason. Part of the early success of Firefox was that it blocked popup ads. But somewhere over the last 10 years of modern Web browsers, browsers lost their way and stopped being the user’s agent alone. Why?</p>
+ <p class="p1"><strong>Browsers aren’t free</strong></p>
+ <p class="p1">Making a modern Web browser is not free. It takes hundreds of engineers to make a competitive modern browser engine. Someone has to pay for that, and that someone needs to have a reason to pay for it. Google doesn’t make Chrome for the good of mankind. Google makes Chrome so you can consume more Web and along with it, more Google ads. Each time you click on one, Google makes more money. Chrome is a billion dollar business for Google. And the same is true for pretty much every other browser. Every major browser out there is funded through advertisement. No browser maker can escape this dilemma. Maybe now you understand why no major browser ships with a builtin enabled by default ad-blocker, even though ad-blockers are by far the most popular add-ons.</p>
+ <p class="p1"><strong>Our privacy is at stake</strong></p>
+ <p class="p1">It’s not just the unregulated flood of advertisement that needs a solution. Every ad you see is often selected based on sensitive private information advertisement networks have extracted from your browsing behavior through tracking. Remember how the FBI used to track what books Americans read at the library, and it was a big scandal? Today the Googles and Facebooks of the world know almost every site you visit, everything you buy online, and they use this data to target you with advertisement. I am often puzzled why people are so afraid of the NSA spying on us but show so little concern about all the deeply personal data Google and Facebook are amassing about everyone.</p>
+ <p class="p1"><strong>Blocking alone doesn’t scale</strong></p>
+ <p class="p1">I wish the solution was as easy as just blocking all ads. There is a lot of great Web content out there: news, entertainment, educational content. It’s not free to make all this content, but we have gotten used to consuming it “for freeâ€. Banning all ads without an alternative mechanism would break the economic backbone of the Web. This dilemma has existed for many years, and the big browser vendors seem to have given up on it. It’s hard to blame them. How do you disrupt the status quo without sawing off the (ad revenue) branch you are sitting on?</p>
+ <p class="p1"><strong>It takes an newcomer to fix this mess</strong></p>
+ <p class="p1">I think its unlikely that the incumbent browser vendors will make any bold moves to solve this mess. There is too much money at stake. I am excited to see a startup take a swipe at this problem, because they have little to lose (seed money aside). Brave is getting the user agent back into the game. Browsers have intentionally remained silent onlookers to the ad industry invading users’ privacy. With Brave, Brendan makes the user agent step up and fight for the user as it was always intended to do.</p>
+ <p class="p1">Brave basically consists of two parts: part one blocks third party ad content and tracking signals. Instead of these Brave inserts alternative ad content. Sites can sign up to get a fair share of any ads that Brave displays for them. The big change in comparison to the status quo is that the Brave user agent is in control and can regulate what you see. It’s like a speed limit for advertisement on the Web, with the goal to restore balance and give sites a fair way to monetize while giving the user control through the user agent.</p>
+ <p class="p1"><strong>Making money with a better Web</strong></p>
+ <p class="p1">The ironic part of Brave is that its for-profit. Brave can make money by reducing obnoxious ads and protecting your privacy at the same time. If Brave succeeds, it’s going to drain money away from the crappy privacy-invasive obnoxious advertisement world we have today, and publishers and sites will start transacting in the new Brave world that is regulated by the user agent. Brave will take a cut of these transactions. And I think this is key. It aligns the incentives right. The current funding structure of major browsers encourages them to keep things as they are. Brave’s incentive is to bring down the whole diseased temple and usher in a better Web. Exciting.</p>
+ <p class="p1"><strong>Quick update:</strong> I had a chance to look over the Brave GitHub repo. It looks like the Brave Desktop browser is based on Chromium, not Gecko. Yes, you read that right. <span style="text-decoration: underline;">Brave is using Google’s rendering engine, not Mozilla’s.</span> Much to write about this one, but it will definitely help Brave “hide†better in the large volume of Chrome users, making it harder for sites to identify and block Brave users. Brave for iOS seems to be a <span style="text-decoration: underline;">fork of Firefox for iOS, but it manages to block ads</span> (Mozilla says they can’t).</p><br/>Filed under: <a href="http://andreasgal.com/category/mozilla/">Mozilla</a> <a href="http://feeds.wordpress.com/1.0/gocomments/andreasgal.wordpress.com/573/" rel="nofollow"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/comments/andreasgal.wordpress.com/573/"/></a> <a href="http://feeds.wordpress.com/1.0/godelicious/andreasgal.wordpress.com/573/" rel="nofollow"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/delicious/andreasgal.wordpress.com/573/"/></a> <a href="http://feeds.wordpress.com/1.0/gofacebook/andreasgal.wordpress.com/573/" rel="nofollow"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/facebook/andreasgal.wordpress.com/573/"/></a> <a href="http://feeds.wordpress.com/1.0/gotwitter/andreasgal.wordpress.com/573/" rel="nofollow"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/twitter/andreasgal.wordpress.com/573/"/></a> <a href="http://feeds.wordpress.com/1.0/gostumble/andreasgal.wordpress.com/573/" rel="nofollow"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/stumble/andreasgal.wordpress.com/573/"/></a> <a href="http://feeds.wordpress.com/1.0/godigg/andreasgal.wordpress.com/573/" rel="nofollow"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/digg/andreasgal.wordpress.com/573/"/></a> <a href="http://feeds.wordpress.com/1.0/goreddit/andreasgal.wordpress.com/573/" rel="nofollow"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/reddit/andreasgal.wordpress.com/573/"/></a> <img alt="" border="0" height="1" src="http://pixel.wp.com/b.gif?host=andreasgal.com&amp;blog=891661&amp;post=573&amp;subd=andreasgal&amp;ref=&amp;feed=1" width="1"/></div>
+ </content>
+ <updated>2016-01-20T16:00:00Z</updated>
+ <category term="Mozilla"/>
+ <author>
+ <name>Andreas</name>
+ </author>
+ <source>
+ <id>http://andreasgal.com</id>
+ <logo>http://s2.wp.com/i/buttonw-com.png</logo>
+ <link href="http://andreasgal.com/feed/" rel="self" type="application/rss+xml"/>
+ <link href="http://andreasgal.com" rel="alternate" type="text/html"/>
+ <link href="http://andreasgal.com/osd.xml" rel="search" title="Andreas Gal " type="application/opensearchdescription+xml"/>
+ <link href="http://andreasgal.com/?pushpress=hub" rel="hub" type="text/html"/>
+ <subtitle>Entrepreneur. Technologist. Former CTO Mozilla</subtitle>
+ <title>Andreas Gal</title>
+ <updated>2016-01-22T11:45:34Z</updated>
+ </source>
+ </entry>
+
+ <entry>
+ <id>https://miketaylr.com/posts/2016/01/at-media-webkit-transform-three-dee.html</id>
+ <link href="https://miketaylr.com/posts/2016/01/at-media-webkit-transform-three-dee.html" rel="alternate" type="text/html"/>
+ <title>🙅 @media (-webkit-transform-3d)</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p><code>@media (-webkit-transform-3d)</code> is a funny thing that exists on the web.</p>
+
+ <p>It's like, a <a href="https://drafts.csswg.org/mediaqueries-4/#mq-features">media query feature</a> in the form of a prefixed CSS property, which should tell you if your (once upon a time probably Safari-only) browser supports 3D transforms, invented back in the day before we had <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@supports"><code>@supports</code></a>.</p>
+
+ <p>(According to <a href="https://developer.apple.com/library/safari/documentation/AppleApplications/Reference/SafariCSSRef/Articles/OtherStandardCSS3Features.html#//apple_ref/doc/uid/TP40007601-SW3">Apple docs</a> it first appeared in Safari 4, along side the other <code>-webkit-transition</code> and <code>-webkit-transform-2d</code> hybrid-media-query-feature-prefixed-css-properties-things that you should immediately forget exist.)</p>
+
+ <p>Older versions of Modernizr <a href="https://github.com/Modernizr/Modernizr/blob/66c694d136241d356e0d24fcbaa5c068b0b0cdae/feature-detects/css/transforms3d.js#L26-L27">used this (and only this)</a> to detect support for 3D transforms, and that seemed pretty OK. (They also did the polite thing and tested <code>@media (transform-3d)</code>, but no browser has ever actually supported that, as it turns out). And because they're so consistently polite, they've since <a href="https://github.com/patrickkettner/Modernizr/commit/a54308e47e269a058472854b1ef417bd54f4e616">updated the test</a> to prefer <code>@supports</code> too (via a pull request from Edge developer Jacob Rossi).</p>
+
+ <p>As it turns out other browsers have been <a href="http://caniuse.com/#feat=transforms3d">updated to support 3D CSS transforms</a>, but sites didn't go back and update their version of Modernizr. So unless you support <code>@media (-webkit-transform-3d)</code> these sites break. Niche websites like <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1239136">yahoo.com</a> and <a href="https://github.com/webcompat/web-bugs/issues/2151">about.com</a>.</p>
+
+ <p>So, anyways. I added <a href="https://compat.spec.whatwg.org/#css-media-queries-webkit-transform-3d"><code>@media (-webkit-transform-3d)</code> to the Compat Standard</a> and we <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1239799">added support for it Firefox</a> so websites stop breaking.</p>
+
+ <p>But you shouldn't ever use it—use <code>@supports</code>. In fact, don't even share this blog post. Maybe delete it from your browser history just in case.</p></div>
+ </summary>
+ <updated>2016-01-20T08:00:00Z</updated>
+ <author>
+ <name>Mike Taylor</name>
+ </author>
+ <source>
+ <id>https://miketaylr.com/posts</id>
+ <link href="https://miketaylr.com/posts" rel="alternate" type="text/html"/>
+ <link href="https://miketaylr.com/posts/rss.xml" rel="self" type="application/rss+xml"/>
+ <rights>3000</rights>
+ <subtitle>Erotic web browser fan-fiction.</subtitle>
+ <title>Mike Taylr Dot Com Web Log</title>
+ <updated>2016-01-20T19:46:39Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en">
+ <id>http://globau.wordpress.com/?p=881</id>
+ <link href="https://globau.wordpress.com/2016/01/20/happy-bmo-push-day-166/" rel="alternate" type="text/html"/>
+ <title>happy bmo push day!</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">the following changes have been pushed to bugzilla.mozilla.org: [1236161] when converting a BMP attachment to PNG fails a zero byte attachment is created [1231918] error handler doesn’t close multi-part responses discuss these changes on mozilla.tools.bmo.Filed under: bmo, mozilla<img alt="" border="0" height="1" src="https://pixel.wp.com/b.gif?host=globau.wordpress.com&amp;blog=25718030&amp;post=881&amp;subd=globau&amp;ref=&amp;feed=1" width="1"/></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>the following changes have been pushed to bugzilla.mozilla.org:</p>
+ <ul>
+ <li>[<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1236161" target="_blank">1236161</a>] when converting a BMP attachment to PNG fails a zero byte attachment is created</li>
+ <li>[<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1231918" target="_blank">1231918</a>] error handler doesn’t close multi-part responses</li>
+ </ul>
+ <p>discuss these changes on <a href="https://lists.mozilla.org/listinfo/tools-bmo" target="_blank">mozilla.tools.bmo</a>.</p><br/>Filed under: <a href="https://globau.wordpress.com/category/mozilla/bmo/">bmo</a>, <a href="https://globau.wordpress.com/category/mozilla/">mozilla</a> <img alt="" border="0" height="1" src="https://pixel.wp.com/b.gif?host=globau.wordpress.com&amp;blog=25718030&amp;post=881&amp;subd=globau&amp;ref=&amp;feed=1" width="1"/></div>
+ </content>
+ <updated>2016-01-20T07:33:46Z</updated>
+ <category term="bmo"/>
+ <category term="mozilla"/>
+ <author>
+ <name>glob</name>
+ </author>
+ <source>
+ <id>https://globau.wordpress.com</id>
+ <logo>https://s2.wp.com/i/buttonw-com.png</logo>
+ <link href="https://globau.wordpress.com/category/mozilla/feed/" rel="self" type="application/rss+xml"/>
+ <link href="https://globau.wordpress.com" rel="alternate" type="text/html"/>
+ <link href="https://globau.wordpress.com/osd.xml" rel="search" title="glob blog" type="application/opensearchdescription+xml"/>
+ <link href="https://globau.wordpress.com/?pushpress=hub" rel="hub" type="text/html"/>
+ <title>mozilla – glob blog</title>
+ <updated>2016-01-26T19:01:06Z</updated>
+ </source>
+ </entry>
+
+ <entry>
+ <id>https://www.alex-johnson.net/tag/mozilla/rss/85d84c54-ed0c-4ee5-beb3-8823edb3c074</id>
+ <link href="https://www.alex-johnson.net/removing-honeycomb-code/" rel="alternate" type="text/html"/>
+ <title>Removing Honeycomb Code</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>As an effort to reduce the APK size of Firefox for Android and to remove unnecessary code, I will be helping remove the Honeycomb code throughout the Fennec project. Honeycomb will not be supported since Firefox 46, so this code is not necessary. <br/>
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1217675">Bug 1217675</a> will keep track of the</p></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>As an effort to reduce the APK size of Firefox for Android and to remove unnecessary code, I will be helping remove the Honeycomb code throughout the Fennec project. Honeycomb will not be supported since Firefox 46, so this code is not necessary. <br/>
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1217675">Bug 1217675</a> will keep track of the progress. <br/>
+ Hopefully this will help reduce the APK size some and clean up the road for <a href="https://www.youtube.com/watch?v=NJ6kzW5t02Y">killing Gingerbread</a> hopefully sometime in the near future.</p></div>
+ </content>
+ <updated>2016-01-20T04:59:34Z</updated>
+ <category term="Mozilla"/>
+ <category term="Android"/>
+ <category term="Mobile"/>
+ <author>
+ <name>Alex Johnson</name>
+ </author>
+ <source>
+ <id>https://www.alex-johnson.net/</id>
+ <link href="https://www.alex-johnson.net/" rel="alternate" type="text/html"/>
+ <link href="https://www.alex-johnson.net/tag/mozilla/rss/" rel="self" type="application/rss+xml"/>
+ <subtitle>Open source evangelist; lover of technology and video games.</subtitle>
+ <title>Mozilla - Alex Johnson</title>
+ <updated>2016-01-26T19:01:53Z</updated>
+ </source>
+ </entry>
+
+ <entry>
+ <id>http://www.brianbondy.com/blog/id/172</id>
+ <link href="http://www.brianbondy.com/blog/172/brave-software" rel="alternate" type="text/html"/>
+ <title>Brave Software</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p/><p>Since June of last year, I’ve been co-founding a new startup called <a href="https://brave.com/">Brave Software</a> with <a href="https://en.wikipedia.org/wiki/Brendan_Eich">Brendan Eich</a>.
+ With our amazing team, we're developing something pretty epic.</p><p/>
+ <p/><p>We're building the next-generation of browsers for smartphones and laptops as part of our new ad-tech platform.
+ Our terms of use give our users control over their personal data by blocking ad trackers and third party cookies.
+ We re-integrate fewer and better ads directly into programmatic ad positions, paying revenue shares to users and publishers to support both of these essential parties in the web ecosystem.</p><p/>
+ <p/><p>Coming built in, we have new faster engines for tracking protection, ad block, HTTPS Everywhere, safe ads with rev-share, and more.
+ We're seeing massive web page load time speedups.</p><p/>
+
+
+ <p/><p>We're starting to bring people in for early developer build access on all platforms.</p><p/>
+ <p/><p>I’m happy to share that the browsers we’re developing were made fully open sourced.
+ We welcome contributors, and would love your help.</p><p/>
+ <p/><p>Some of the repositories include:</p><p/>
+ <ul>
+ <li><a href="https://github.com/brave/browser-laptop">Brave OSX and Windows x64 browsers</a>: Prototyped as a Gecko based browser, but now replaced with a powerful new browser built on top of the electron framework. The electron framework is the same one in use by Slack and the Atom editor. It uses the latest libchromiumcontent and Node.</li>
+ <li><a href="https://github.com/brave/link-bubble">Brave for Android</a>: Formerly Link Bubble, working as a background service so you can use other apps as your pages load.</li>
+ <li><a href="https://github.com/brave/browser-ios">Brave for iOS</a>: Originally forked from Firefox for iOS but with all of the built-in greatness described above.</li>
+ <li>And many others: Website, updater code, vault, electron fork, and others.</li>
+ </ul></div>
+ </summary>
+ <updated>2016-01-20T00:00:00Z</updated>
+ <category term="brave"/>
+ <category term="electron"/>
+ <category term="node"/>
+ <category term="firefox"/>
+ <category term="firefox-ios"/>
+ <category term="mozilla"/>
+ <category term="gecko"/>
+ <author>
+ <name>Brian R. Bondy</name>
+ </author>
+ <source>
+ <id>http://www.brianbondy.com/blog/tagged/mozilla</id>
+ <logo>http://www.brianbondy.com/img/logo.png</logo>
+ <link href="http://www.brianbondy.com/blog/tagged/mozilla" rel="alternate" type="text/html"/>
+ <link href="http://www.brianbondy.com/feeds/rss/mozilla" rel="self" type="application/rss+xml"/>
+ <subtitle>Blog posts tagged mozilla by Brian R. Bondy</subtitle>
+ <title>Brian R. Bondy's feed for tag mozilla</title>
+ <updated>2016-01-26T19:01:09Z</updated>
+ </source>
+ </entry>
+
+ <entry>
+ <id>http://coffeeonthekeyboard.com/rss/0388d8a6-fc86-477e-a161-1b356e01fe77</id>
+ <link href="http://coffeeonthekeyboard.com/piefection-slides-up/" rel="alternate" type="text/html"/>
+ <title>PIEfection Slides Up</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>I put <a href="https://github.com/jsocol/talks/tree/master/2016-01-13-manhattanjs-pie">the slides for my ManhattanJS talk, "PIEfection"</a> up on GitHub the other day (sans images, but there are links in the source for all of those).</p>
+
+ <p>I completely neglected to talk about the <a href="https://en.wikipedia.org/wiki/Maillard_reaction">Maillard reaction</a>, which is responsible for food tasting good, and specifically for browning pie crusts.</p></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>I put <a href="https://github.com/jsocol/talks/tree/master/2016-01-13-manhattanjs-pie">the slides for my ManhattanJS talk, "PIEfection"</a> up on GitHub the other day (sans images, but there are links in the source for all of those).</p>
+
+ <p>I completely neglected to talk about the <a href="https://en.wikipedia.org/wiki/Maillard_reaction">Maillard reaction</a>, which is responsible for food tasting good, and specifically for browning pie crusts. tl;dr: Amino acid (protein) + sugar + ~300°F (~150°C) = delicious. There are innumerable and poorly understood combinations of amino acids and sugars, but this class of reaction is responsible for everything from searing stakes to browning crusts to toasting marshmallows.</p>
+
+ <p>Above ~330°F, you get caramelization, which is also a delicious part of the pie and crust, but you don't want to overdo it. Starting around ~400°F, you get pyrolysis (burning, charring, carbonization) and below 285°F the reaction won't occur (at least not quickly) so you won't get the delicious compounds.</p>
+
+ <p>(All of these are, of course, temperatures measured in the material, not in the air of the oven.)</p>
+
+ <p>So, instead of an egg wash on your top crust, try whole milk, which has more sugar to react with the gluten in the crust.</p>
+
+ <p>I also didn't get a chance to mention a rolling technique I use, that I learned from a <a href="https://www.facebook.com/ellenspirerstaffing">cousin of mine</a>, in whose baking shadow I happily live.</p>
+
+ <p>When rolling out a crust after it's been in the fridge, first roll it out in a long stretch, then fold it in thirds; do it again; then start rolling it out into a round. Not only do you add more layer structure (mmm, flaky, delicious layers) but it'll fill in the cracks that often form if you try to roll it out directly, resulting in a stronger crust.</p>
+
+ <p>Those <a href="http://www.amazon.com/Cheese-Shaker-Pepper-Perforated-Stainless/dp/B007T40P28/ref=sr_1_1?ie=UTF8&amp;qid=1453236391&amp;sr=8-1&amp;keywords=pizza+shaker">pepper flake shakers</a>, filled with flour, are a great way to keep adding flour to the workspace without worrying about your buttery hands.</p>
+
+ <p>For transferring the crust to the pie plate, try rolling it up onto your rolling pin and unrolling it on the plate. <a href="http://www.amazon.com/Ateco-20-Inch-Length-French-Rolling/dp/B000KESQ1G">Tapered (or "French") rolling pins</a> (or wine bottle) are particularly good at this since they don't have moving parts.</p>
+
+ <p>Finally, thanks again to <a href="https://twitter.com/renrutnnej">Jenn</a> for helping me get pies from one island to another. It would not have been possible without her!</p></div>
+ </content>
+ <updated>2016-01-19T20:45:34Z</updated>
+ <author>
+ <name>James Socol</name>
+ </author>
+ <source>
+ <id>http://coffeeonthekeyboard.com/</id>
+ <link href="http://coffeeonthekeyboard.com/" rel="alternate" type="text/html"/>
+ <link href="http://coffeeonthekeyboard.com/rss/" rel="self" type="application/rss+xml"/>
+ <subtitle>Coffee on the Keyboard</subtitle>
+ <title>Coffee on the Keyboard</title>
+ <updated>2016-01-25T18:00:43Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>https://air.mozilla.org/reprendre-le-controle-de-sa-vie-privee-sur-internet/</id>
+ <link href="https://air.mozilla.org/reprendre-le-controle-de-sa-vie-privee-sur-internet/" rel="alternate" type="text/html"/>
+ <title>Reprendre le contrôle de sa vie privée sur Internet</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>
+ <img alt="Reprendre le contr&#xF4;le de sa vie priv&#xE9;e sur Internet" class="wp-post-image" height="90" src="https://air.cdn.mozilla.net/media/cache/be/f6/bef62897fb87e08dc8392fe61d10bcfa.png" width="160"/>
+ L'omniprésence des réseaux sociaux, des moteurs de recherches et de la publicité est-elle compatible avec notre droit à la vie privée ?
+ </p></div>
+ </summary>
+ <updated>2016-01-19T18:00:00Z</updated>
+ <author>
+ <name>Air Mozilla</name>
+ </author>
+ <source>
+ <id>https://air.mozilla.org/</id>
+ <link href="https://air.mozilla.org/" rel="alternate" type="text/html"/>
+ <link href="https://air.mozilla.org/" rel="self" type="application/rss+xml"/>
+ <rights>Except where otherwise noted, content on this site is licensed under the Creative Commons Attribution Share-Alike License v3.0 or any later version.</rights>
+ <subtitle>Air Mozilla is the Internet multimedia presence of Mozilla, with live and pre-recorded shows, interviews, news snippets, tutorial videos, and features about the Mozilla community.</subtitle>
+ <title>Air Mozilla</title>
+ <updated>2016-01-25T20:31:50Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>https://mykzilla.org/?p=245</id>
+ <link href="https://mykzilla.org/2016/01/19/new-year-new-blogware/" rel="alternate" type="text/html"/>
+ <title>New Year, New Blogware</title>
+ <summary>Four score and many moons ago, I decided to move this blog from Blogger to WordPress. The transition took longer than expected, but it’s finally done. If you’ve been following along at the old address, https://mykzilla.blogspot.com/, now’s the time to update your address book! If you’ve been going to https://mykzilla.org/, however, or you read the […]</summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>Four score and many moons ago, I decided to move this blog from Blogger to WordPress. The transition took longer than expected, but it’s finally done.</p>
+ <p>If you’ve been following along at the old address, <a href="https://mykzilla.blogspot.com/">https://mykzilla.blogspot.com/</a>, now’s the time to update your address book! If you’ve been going to <a href="https://mykzilla.org/">https://mykzilla.org/</a>, however, or you read the blog on <a href="http://planet.mozilla.org/">Planet Mozilla</a>, then there’s nothing to do, as that’s the new address, and Planet Mozilla has been updated to syndicate posts from it.</p></div>
+ </content>
+ <updated>2016-01-19T16:56:05Z</updated>
+ <category term="Mozilla"/>
+ <author>
+ <name>Myk Melez</name>
+ </author>
+ <source>
+ <id>https://mykzilla.org</id>
+ <logo>https://mykzilla.org/wp-content/uploads/2016/01/cropped-headshot-2014-32x32.jpg</logo>
+ <link href="https://mykzilla.org/category/mozilla/feed/" rel="self" type="application/rss+xml"/>
+ <link href="https://mykzilla.org" rel="alternate" type="text/html"/>
+ <title>Mozilla – Mykzilla</title>
+ <updated>2016-01-19T17:30:17Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>http://michaelkohler.info/?p=348</id>
+ <link href="https://michaelkohler.info/2016/mozillas-strategische-leitlinien-fur-2016-und-danach" rel="alternate" type="text/html"/>
+ <title>Mozillas strategische Leitlinien für 2016 und danach</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">Dieser Beitrag wurde zuerst im Blog auf https://blog.mozilla.org/community veröffentlicht. Herzlichen Dank an Aryx und Coce für die Übersetzung! Auf der ganzen Welt arbeiten leidenschaftliche Mozillianer am Fortschritt für Mozillas Mission. Aber fragt man fünf verschiedene Mozillianer, was die Mission ist, erhält man womöglich sieben verschiedene Antworten. Am Ende des letzten Jahres legte Mozillas CEO Chris Beard klare Vorstellungen über Mozillas Mission, Vision und Rolle dar und zeigte auf, wie unsere Produkte uns diesem Ziel in den nächsten fünf Jahren näher bringen. Das Ziel dieser strategischen Leitlinien besteht darin, für Mozilla...<a class="read-more" href="https://michaelkohler.info/2016/mozillas-strategische-leitlinien-fur-2016-und-danach">read more</a><img alt="" height="0" src="http://piwik.michaelkohler.info/piwik.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fmichaelkohler.info%2F2016%2Fmozillas-strategische-leitlinien-fur-2016-und-danach&amp;action_name=Mozillas+strategische+Leitlinien+f%C3%BCr+2016+und+danach&amp;urlref=https%3A%2F%2Fmichaelkohler.info%2Ffeed" style="border: 0; width: 0; height: 0;" width="0"/></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>Dieser Beitrag wurde zuerst im Blog auf<a href="https://blog.mozilla.org/community"> https://blog.mozilla.org/community</a> veröffentlicht. Herzlichen Dank an Aryx und Coce für die Übersetzung!</p>
+ <p>Auf der ganzen Welt arbeiten leidenschaftliche Mozillianer am Fortschritt für Mozillas Mission. Aber fragt man fünf verschiedene Mozillianer, was die Mission ist, erhält man womöglich sieben verschiedene Antworten.</p>
+ <p>Am Ende des letzten Jahres legte Mozillas CEO Chris Beard klare Vorstellungen über Mozillas Mission, Vision und Rolle dar und zeigte auf, wie unsere Produkte uns diesem Ziel in den nächsten fünf Jahren näher bringen. Das Ziel dieser strategischen Leitlinien besteht darin, für Mozilla insgesamt ein prägnantes, gemeinsames Verständnis unserer Ziele zu entwickeln, die uns als Individuen das Treffen von Entscheidungen und Erkennen von Möglichkeiten erleichtert, mit denen wir Mozilla voranbringen.</p>
+ <p>Mozillas Mission können wir nicht alleine erreichen. Die Tausenden von Mozillianern auf der ganzen Welt müssen dahinter stehen, damit wir zügig und mit lauterer Stimme als je zuvor Unglaubliches erreichen können.</p>
+ <p>Deswegen ist eine der sechs<a href="https://docs.google.com/presentation/d/1A3Ma9gNawAYYGbYC2bUW0wUwcpHuvyMiZvHNiMLriw0/edit#slide=id.gdaa7a0bd0_1_0"> strategischen Initiativen</a> des Participation Teams für die erste Jahreshälfte, möglichst viele Mozillianer über diese Leitlinien aufzuklären, damit wir 2016 den bisher wesentlichsten Einfluss erzielen können. Wir werden einen weiteren Beitrag veröffentlichen, der sich näher mit der Strategie des Participation Teams für das Jahr 2016 befassen wird.</p>
+ <p><img alt="" class="alignnone" height="335" src="https://ffp4g1ylyit3jdyti1hqcvtb-wpengine.netdna-ssl.com/community/files/2016/01/Screen-Shot-2015-12-18-at-2.02.07-PM-600x335.png" width="600"/></p>
+ <p>Das Verstehen dieser Strategie wird unabdingbar sein für jeden, der bei Mozilla in diesem Jahr etwas bewirken möchte, denn sie wird bestimmen, wofür wir eintreten, wo wir unsere Ressourcen einsetzen und auf welche Projekte wir uns 2016 konzentrieren werden.</p>
+ <p>Zu Jahresbeginn werden wir näher auf diese Strategie eingehen und weitere Details dazu bekanntgeben, wie die diversen Teams und Projekte bei Mozilla auf diese Ziele hinarbeiten.</p>
+ <p>Der aktuelle Aufruf zum Handeln besteht darin, im Kontext Ihrer Arbeit über diese Ziele nachzudenken und darüber, wie Sie im kommenden Jahr bei Mozilla mitwirken möchten. Dies hilft, Ihre Innovationen, Ambitionen und Ihren Einfluss im Jahr 2016 zu gestalten.</p>
+ <p>Wir hoffen, dass Sie mitdiskutieren und Ihre Fragen, Kommentare und Pläne für das Vorantreiben der strategischen Leitlinien im Jahr 2016<a href="https://discourse.mozilla-community.org/t/mozillas-strategic-narrative-2016/6397"> hier</a> auf Discourse teilen und Ihre Gedanken auf Twitter mit dem Hashtag <a href="https://twitter.com/search?q=%23mozilla2016strategy&amp;src=typd">#Mozilla2016Strategy</a> mitteilen.</p>
+ <p> </p>
+ <h3>Mission, Vision &amp; Strategie</h3>
+ <p><b>Unsere Mission</b></p>
+ <p>Dafür zu sorgen, dass das Internet eine weltweite öffentliche Ressource ist, die allen zugänglich ist.</p>
+ <p><b>Unsere Vision</b></p>
+ <p>Ein Internet, für das Menschen tatsächlich an erster Stelle stehen. Ein Internet, in dem Menschen ihr eigenes Erlebnis gestalten können. Ein Internet, in dem die Menschen selbst entscheiden können sowie sicher und unabhängig sind.</p>
+ <p><b>Unsere Rolle</b></p>
+ <p>Mozilla setzt sich im wahrsten Sinne des Wortes in Ihrem Online-Leben für Sie ein. Wir setzen uns für Sie ein, sowohl in Ihrem Online-Erlebnis als auch für Ihre Interessen beim Zustand des Internets.</p>
+ <p><b>Unsere Arbeit</b></p>
+ <p>Unsere Säulen</p>
+ <ol>
+ <li><b>Produkte:</b> Wir entwickeln Produkte mit Menschen im Mittelpunkt sowie Bildungsprogramme, mit deren Hilfe Menschen online ihr gesamtes Potential ausschöpfen können.</li>
+ <li><b>Technologie:</b> Wir entwickeln robuste technische Lösungen, die das Internet über verschiedene Plattformen hinweg zum Leben erwecken.</li>
+ <li><b>Menschen:</b> Wir entwickeln Führungspersonen und Mitwirkende in der Gemeinschaft, die das Internet erfinden, gestalten und verteidigen.</li>
+ </ol>
+ <p>Wir wir positive Veränderungen in Zukunft anpacken wollen</p>
+ <p>Die Arbeitsweise ist ebensowichtig wie das Ziel. Unsere Gesundheit und bleibender Einfluss hängen davon ab, wie sehr unsere Produkte und Aktivitäten:</p>
+ <ol>
+ <li>Interoperabilität, Open Source und offene Standards fördern,</li>
+ <li>Gemeinschaften aufbauen und fördern,</li>
+ <li>Für politische Veränderungen und rechtlichen Schutz eintreten sowie</li>
+ <li>Netzbürger bilden und einbeziehen.</li>
+ </ol>
+ <p> </p>
+ <img alt="" height="0" src="http://piwik.michaelkohler.info/piwik.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fmichaelkohler.info%2F2016%2Fmozillas-strategische-leitlinien-fur-2016-und-danach&amp;action_name=Mozillas+strategische+Leitlinien+f%C3%BCr+2016+und+danach&amp;urlref=https%3A%2F%2Fmichaelkohler.info%2Ffeed" style="border: 0; width: 0; height: 0;" width="0"/></div>
+ </content>
+ <updated>2016-01-19T15:27:24Z</updated>
+ <category term="Mozilla"/>
+ <category term="MozillaDeutsch"/>
+ <author>
+ <name>Michael Kohler</name>
+ </author>
+ <source>
+ <id>https://michaelkohler.info</id>
+ <link href="https://michaelkohler.info/category/mozilla/feed" rel="self" type="application/rss+xml"/>
+ <link href="https://michaelkohler.info" rel="alternate" type="text/html"/>
+ <title>Mozilla – Michael Kohler</title>
+ <updated>2016-01-19T15:31:45Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en">
+ <id>http://dlawrence.wordpress.com/?p=27</id>
+ <link href="https://dlawrence.wordpress.com/2016/01/19/happy-bmo-push-day-3/" rel="alternate" type="text/html"/>
+ <title>happy bmo push day!</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">the following changes have been pushed to bugzilla.mozilla.org: [1238573] Change label of “New Bug†menu to “New/Clone Bug†[1239065] Project Kickoff Form: Adjustments needed to Mozilla Infosec review portion [1240157] Fix a typo in bug.rst [1236461] Mass update mozilla-reps group discuss these changes on mozilla.tools.bmo.<img alt="" border="0" height="1" src="https://pixel.wp.com/b.gif?host=dlawrence.wordpress.com&amp;blog=58816&amp;post=27&amp;subd=dlawrence&amp;ref=&amp;feed=1" width="1"/></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>the following changes have been pushed to bugzilla.mozilla.org:</p>
+ <ul>
+ <li>[<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1238573" target="_blank">1238573</a>] Change label of “New Bug†menu to “New/Clone Bugâ€</li>
+ <li>[<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1239065" target="_blank">1239065</a>] Project Kickoff Form: Adjustments needed to Mozilla Infosec review portion</li>
+ <li>[<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1240157" target="_blank">1240157</a>] Fix a typo in bug.rst</li>
+ <li>[<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1236461" target="_blank">1236461</a>] Mass update mozilla-reps group</li>
+ </ul>
+ <p>discuss these changes on <a href="https://lists.mozilla.org/listinfo/tools-bmo" target="_blank">mozilla.tools.bmo</a>.</p><br/> <a href="http://feeds.wordpress.com/1.0/gocomments/dlawrence.wordpress.com/27/" rel="nofollow"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/comments/dlawrence.wordpress.com/27/"/></a> <img alt="" border="0" height="1" src="https://pixel.wp.com/b.gif?host=dlawrence.wordpress.com&amp;blog=58816&amp;post=27&amp;subd=dlawrence&amp;ref=&amp;feed=1" width="1"/></div>
+ </content>
+ <updated>2016-01-19T14:49:59Z</updated>
+ <category term="Uncategorized"/>
+ <author>
+ <name>dlawrence</name>
+ </author>
+ <source>
+ <id>https://dlawrence.wordpress.com</id>
+ <logo>https://s2.wp.com/i/buttonw-com.png</logo>
+ <link href="https://dlawrence.wordpress.com/feed/" rel="self" type="application/rss+xml"/>
+ <link href="https://dlawrence.wordpress.com" rel="alternate" type="text/html"/>
+ <link href="https://dlawrence.wordpress.com/osd.xml" rel="search" title="Dave's Ramblings" type="application/opensearchdescription+xml"/>
+ <link href="https://dlawrence.wordpress.com/?pushpress=hub" rel="hub" type="text/html"/>
+ <subtitle>Thoughts somehow related to web, linux, mobile and other things I am interested in</subtitle>
+ <title>Dave's Ramblings</title>
+ <updated>2016-01-26T14:31:39Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>http://soledadpenades.com/?p=6335</id>
+ <link href="http://soledadpenades.com/2016/01/19/hardware-hack-day-mozldn-1/" rel="alternate" type="text/html"/>
+ <link href="https://flattr.com/submit/auto?user_id=8399&amp;popout=1&amp;url=http%3A%2F%2Fsoledadpenades.com%2F2016%2F01%2F19%2Fhardware-hack-day-mozldn-1%2F&amp;language=en_GB&amp;category=text&amp;title=Hardware+Hack+Day+%40+MozLDN%2C+1&amp;description=Last+week+we+ran+an+internal+%26%238220%3Bhack+day%26%238221%3B+here+at+the+Mozilla+space+in+London.+It+was+just+a+bunch+of+software+engineers+looking+at+various+hardware+boards+and+things...&amp;tags=arduino%2Clinux%2Cmdns%2Cmozilla%2Craspberry+pi%2Cwi-fi%2Cblog" rel="payment" title="Flattr this!" type="text/html"/>
+ <title>Hardware Hack Day @ MozLDN, 1</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">Last week we ran an internal “hack day†here at the Mozilla space in London. It was just a bunch of software engineers looking at various hardware boards and things and learning about them Here’s what we did! Sole I essentially kind of bricked my Arduino Duemilanove trying to get it working with Johnny Five, … <a class="more-link" href="http://soledadpenades.com/2016/01/19/hardware-hack-day-mozldn-1/">Continue reading <span class="screen-reader-text">Hardware Hack Day @ MozLDN, 1</span> <span class="meta-nav">→</span></a></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>Last week we ran an internal “hack day†here at the Mozilla space in London. It was just a bunch of <em>software</em> engineers looking at various <em>hardware</em> boards and things and learning about them <img alt=":-)" class="wp-smiley" src="http://soledadpenades.com/wp-includes/images/smilies/simple-smile.png" style="height: 1em;"/></p>
+ <p>Here’s what we did!</p>
+ <h3><a href="http://soledadpenades.com/">Sole</a></h3>
+ <p>I essentially <a href="http://soledadpenades.com/2016/01/19/kind-of-bricking-an-arduino-duemilanove-by-exhausting-its-memory/">kind of bricked my Arduino Duemilanove</a> trying to get it working with Johnny Five, but it was fine–apparently there’s a way to recover it using another Arduino, and someone offered to help with that in the next <a href="http://www.meetup.com/NodeBots-of-London/events/227890374/">NodeBots</a> London, which I’m going to attend.</p>
+ <h3><a href="http://ardeenelinfierno.com/">Francisco</a></h3>
+ <p>Thinks he’s having issues with cables. It seems like the boards are not reset automatically by the Arduino IDE nowadays? He found the button in the board actually resets the board when pressed i.e. it’s the RESET button.</p>
+ <p>On the Raspberry Pi side of things, he was very happy to put all his old-school Linux skills in action configuring network interfaces without GUIs!</p>
+ <h3><a href="http://gu.illau.me/">Guillaume</a></h3>
+ <p>Played with mDNS advertising and listening to services on Raspberry Pi.</p>
+ <p>(He was very quiet)</p>
+ <p>(He also built a very nice LEGO case for the Raspberry Pi, but I do not have a picture, so just imagine it).</p>
+ <h3><a href="http://wilsonpage.co.uk/">Wilson</a></h3>
+ <blockquote><p>
+ Wilson: “I got my Raspberry Pi on the Wi-Fiâ€</p>
+ <p>Francisco: “Sorry?â€</p>
+ <p>Wilson: “I mean, you got my Raspberry Pi on the network. And now I’m trying to build a web app on the Pi…â€</p></blockquote>
+ <h3><a href="http://chrislord.net/">Chris</a></h3>
+ <p>Exploring the Pebble with Linux. There’s a libpebble, and he managed to connect…</p>
+ <p><del datetime="2016-01-20T11:22:33+00:00"><em><small>(sorry, I had to leave early so I do not know what else did Chris do!)</small></em></del></p>
+ <p>Updated, 20 January: Chris told me he just managed to successfully connect to the Pebble watch using the bluetooth WebAPI. It requires two Gecko patches (one regression patch and one obvious logic error that he hasn’t filed yet). PROGRESS!</p>
+ <p>~~~</p>
+ <p>So as you can see we didn’t really get super far in just a day, and I even ended up with unusable hardware. BUT! we all learned something, and next time we know what NOT to do (or at least I DO KNOW what NOT to do!).</p>
+ <p><a href="http://soledadpenades.com/?flattrss_redirect&amp;id=6335&amp;md5=40427d69faa3b9c2d1530732fd78e66d" target="_blank" title="Flattr"><img alt="flattr this!" src="http://soledadpenades.com/wp-content/plugins/flattr/img/flattr-badge-large.png"/></a></p></div>
+ </content>
+ <updated>2016-01-19T13:31:55Z</updated>
+ <category term="Events"/>
+ <category term="Hardware"/>
+ <category term="arduino"/>
+ <category term="linux"/>
+ <category term="mdns"/>
+ <category term="mozilla"/>
+ <category term="raspberry pi"/>
+ <category term="wi-fi"/>
+ <author>
+ <name>sole</name>
+ </author>
+ <source>
+ <id>http://soledadpenades.com</id>
+ <link href="http://soledadpenades.com/tag/mozilla/feed/" rel="self" type="application/rss+xml"/>
+ <link href="http://soledadpenades.com" rel="alternate" type="text/html"/>
+ <subtitle>repeat 4[fd 100 rt 90]</subtitle>
+ <title>mozilla – soledad penadés</title>
+ <updated>2016-01-26T02:46:29Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>http://daniel.haxx.se/blog/?p=8544</id>
+ <link href="http://daniel.haxx.se/blog/2016/01/19/subject-urgent-warning/" rel="alternate" type="text/html"/>
+ <title>“Subject: Urgent Warningâ€</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">Back in December I got a desperate email from this person. A woman who said her Instagram had been hacked and since she found my contact info in the app she mailed me and asked for help. I of course replied and said that I have nothing to do with her being hacked but I … <a class="more-link" href="http://daniel.haxx.se/blog/2016/01/19/subject-urgent-warning/">Continue reading <span class="screen-reader-text">“Subject: Urgent Warningâ€</span> <span class="meta-nav">→</span></a></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>Back in December I got a desperate email from this person. A woman who said her Instagram had been hacked and since she found my contact info in the app she mailed me and asked for help. I of course replied and said that I have nothing to do with her being hacked but I also have nothing to do with Instagram other than that they use software I’ve written.</p>
+ <p>Today she writes back. Clearly not convinced I told the truth before, and now she strikes back with more “evidence†of my wrongdoings.</p>
+ <p><em>Dear Daniel,</em></p>
+ <p><em>I had emailed you a couple months ago about my “screen dumps†aka screenshots and asked for your help with restoring my Instagram account since it had been hacked, my photos changed, and your name was included in the coding. You claimed to have no involvement whatsoever in developing a third party app for Instagram and could not help me salvage my original Instagram photos, pre-hacked, despite Instagram serving as my Photography portfolio and my career is a Photographer.</em></p>
+ <p><em>Since you weren’t aware that your name was attached to Instagram related hacking code, I thought you might want to know, in case you weren’t already aware, that your name is also included in Spotify terms and conditions. I came across this information using my Spotify which has also been hacked into and would love your help hacking out of Spotify. Also, I have yet to figure out how to unhack the hackers from my Instagram so if you change your mind and want to restore my Instagram to its original form as well as help me secure my account from future privacy breaches, I’d be extremely grateful. As you know, changing my passwords did nothing to resolve the problem. Please keep in mind that Facebook owns Instagram and these are big companies that you likely don’t want to have a trail of evidence that you are a part of an Instagram and Spotify hacking ring. Also, Spotify is a major partner of Spotify so you are likely familiar with the coding for all of these illegally developed third party apps. I’d be grateful for your help fixing this error immediately.</em></p>
+ <p><em>Thank you,</em></p>
+ <p>[name redacted]</p>
+ <p><em>P.S. Please see attached screen dump for a screen shot of your contact info included in Spotify (or what more likely seems to be a hacked Spotify developed illegally by a third party).</em></p>
+ <p><a href="http://daniel.haxx.se/blog/wp-content/uploads/2016/01/IMG_7393.png" rel="attachment wp-att-8545"><img alt="Spotify credits screenshot" class="aligncenter size-medium wp-image-8545" height="450" src="http://daniel.haxx.se/blog/wp-content/uploads/2016/01/IMG_7393-253x450.png" width="253"/></a></p>
+ <p>Here’s the Instagram screenshot she sent me in a previous email:</p>
+ <p><a href="http://daniel.haxx.se/blog/wp-content/uploads/2016/01/IMG_2156.jpg" rel="attachment wp-att-8546"><img alt="Instagram credits screenshot" class="aligncenter size-medium wp-image-8546" height="450" src="http://daniel.haxx.se/blog/wp-content/uploads/2016/01/IMG_2156-253x450.jpg" width="253"/></a></p>
+ <p>I’ve tried to respond with calm and clear reasonable logic and technical details on why she’s seeing my name there. That clearly failed. What do I try next?</p></div>
+ </content>
+ <updated>2016-01-19T08:37:32Z</updated>
+ <category term="cURL and libcurl"/>
+ <category term="Open Source"/>
+ <category term="Technology"/>
+ <category term="hacking"/>
+ <category term="Mail"/>
+ <author>
+ <name>Daniel Stenberg</name>
+ </author>
+ <source>
+ <id>http://daniel.haxx.se/blog</id>
+ <logo>http://daniel.haxx.se/blog/wp-content/uploads/2015/08/cropped-Daniel-head-greenshirt-32x32.jpg</logo>
+ <link href="http://daniel.haxx.se/blog/feed/" rel="self" type="application/rss+xml"/>
+ <link href="http://daniel.haxx.se/blog" rel="alternate" type="text/html"/>
+ <subtitle>tech, open source and networking</subtitle>
+ <title>daniel.haxx.se</title>
+ <updated>2016-01-26T07:16:26Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-us">
+ <id>http://edunham.net/2016/01/19/how_much_knowledge_do_you_need_to_give_a_conference_talk.html</id>
+ <link href="http://edunham.net/2016/01/19/how_much_knowledge_do_you_need_to_give_a_conference_talk.html" rel="alternate" type="text/html"/>
+ <title>How much knowledge do you need to give a conference talk?</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><h3>How much knowledge do you need to give a conference talk?</h3>
+ <p>I was recently asked an excellent question when I promoted the <a class="reference external" href="http://www.linuxfestnorthwest.org/2016/present">LFNW CFP</a> on
+ IRC:</p>
+ <blockquote>
+ <div>As someone who has never done a talk, but wants to, what kind of knowledge
+ do you need about a subject to give a talk on it?</div></blockquote>
+ <p>If you answer “yes†to any of the following questions, you know enough to
+ propose a talk:</p>
+ <ul class="simple">
+ <li>Do you have a <strong>hobby</strong> that most tech people aren’t experts on? Talk
+ about applying a lesson or skill from that hobby to tech! For instance, I
+ turned a habit of reading about psychology into my <a class="reference external" href="http://talks.edunham.net/scale13x/#1">Human Hacking</a> talk.</li>
+ <li>Have you ever spent a bunch of hours forcing two tools to work with each
+ other, because the documentation wasn’t very helpful and Googling didn’t get
+ you very far, and built something useful? “How to build ___ with ___†makes
+ a catchy talk title, if the <strong>thing you built</strong> solves a common problem.</li>
+ <li>Have you ever had a mentor sit down with you and explain a tool or
+ technique, and the new understanding improved the quality of your work or
+ code? Passing along useful <strong>lessons from your mentors</strong> is a valuable talk,
+ because it allows others to benefit from the knowledge without taking as
+ much of your mentor’s time.</li>
+ <li>Have you seen a dozen newbies ask the same question over the course of a few
+ months? When your <strong>answer to a common question</strong> starts to feel like a
+ broken record, it’s time to compose it into a talk then link the newbies to
+ your slides or recording!</li>
+ <li>Have you taken a really <strong>interesting class</strong> lately? Can you distill part of it
+ into a 1-hour lesson that would appeal to nerds who don’t have the time or
+ resources to take the class themselves? (thanks <a class="reference external" href="http://lucywyman.me/">lucyw</a> for adding this to
+ the list!)</li>
+ <li>Have you built a cool thing that over a dozen other people use? A <strong>tutorial
+ talk</strong> can not only expand your community, but its recording can augment your
+ documentation and make the project more accessible for those who prefer to
+ learn directly from humans!</li>
+ <li>Did you benefit from a really great introductory talk when you were learning
+ a tool? Consider doing your own tutorial! Any conference with beginners in
+ their target audience needs at least one Git lesson, an IRC talk, and some
+ discussions of how to use basic Unix utilities. These <strong>introductory talks</strong>
+ are actually better when given by someone who learned the technology
+ relatively recently, because newer users remember what it’s like not to know
+ how to use it. Just remember to have a more expert user look over your slides
+ before you present, in case you made an incorrect assumption about the tool’s
+ more advanced functionality.</li>
+ </ul>
+ <p>I personally try to propose talks I want to hear, because the dealine of a
+ CFP or conference is great motivation to prioritize a cool project over
+ ordinary chores.</p></div>
+ </summary>
+ <updated>2016-01-19T08:00:00Z</updated>
+ <source>
+ <id>http://edunham.net/</id>
+ <author>
+ <name>Emily Dunham</name>
+ </author>
+ <link href="http://edunham.net/" rel="alternate" type="text/html"/>
+ <link href="http://edunham.net/rss.html?tag=planetmozilla" rel="self" type="application/rss+xml"/>
+ <subtitle>is a "DevOps" Engineer at Mozilla Research</subtitle>
+ <title>edunham</title>
+ <updated>2016-01-19T08:00:00Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>https://quality.mozilla.org/?p=49441</id>
+ <link href="https://quality.mozilla.org/2016/01/aurora-45-0-testday-results/" rel="alternate" type="text/html"/>
+ <title>Aurora 45.0 Testday Results</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">Howdy mozillians! Last week – on Friday, January 15th – we held Aurora 45.0 Testday; and, of course, it was another outstanding event! Thank you all – Mahmoudi Dris, Iryna Thompson, Chandrakant Dhutadmal, Preethi Dhinesh, Moin Shaikh, Ilse Macías, Hossain Al Ikram, Rezaul Huque Nayeem, Tahsan Chowdhury Akash, Kazi Nuzhat Tasnem, Fahmida … <a class="go" href="https://quality.mozilla.org/2016/01/aurora-45-0-testday-results/">Continue reading</a></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>Howdy mozillians!</p>
+ <p>Last week – on <em>Friday, January 15th</em> – we held <a href="https://quality.mozilla.org/2016/01/firefox-45-0-aurora-testday-january-15th/">Aurora 45.0 Testday</a>; and, of course, it was another outstanding event!</p>
+ <p><strong>Thank you</strong> all – <span class="author-a-oz90z4z89zz89za7qfz70zda5z87zxz85z i"><i>Mahmoudi Dris, Iryna Thompson, Chandrakant Dhutadmal, Preethi Dhinesh, Moin Shaikh, Ilse Macías, Hossain Al Ikram, Rezaul Huque Nayeem, Tahsan Chowdhury Akash, Kazi Nuzhat Tasnem, Fahmida Noor, Tazin Ahmed, Md. Ehsanul Hassan, Mohammad Maruf Islam, Kazi Sakib Ahmad, Khalid Syfullah Zaman, Asiful Kabir, Tabassum Mehnaz, Hasibul Hasan, Saddam Hossain, Mohammad Kamran Hossain, Amlan Biswas, Fazle Rabbi, Mohammed Jawad Ibne Ishaque, Asif Mahmud Shuvo, Nazir Ahmed Sabbir, Md. Raihan Ali, Md. Almas Hossain, Sadik Khan, Md. Faysal Alam Riyad, Faisal Mahmud, Md. Oliullah Sizan, Asif Mahmud Rony, Forhad Hossain </i>and<i> Tanvir Rahman </i></span>– for the participation!</p>
+ <p>A big <strong>thank you</strong> to all our active moderators too!</p>
+ <p><span style="color: #333333;"><span style="font-family: 'Open Sans', sans-serif;"><span style="font-size: medium;"><u>Results:</u></span></span></span></p>
+ <ul>
+ <li><span style="color: #333333;"><span style="font-family: 'Open Sans', sans-serif;"><span style="font-size: medium;"><strong>15</strong> issues were verified: </span></span></span><span style="color: #333333;"><span style="font-family: 'Open Sans', sans-serif;"><span style="font-size: medium;"> <span style="font-weight: 400;"><a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1235821">1235821</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1228518">1228518</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1165637">1165637</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1232647">1232647</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1235379">1235379</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=842356">842356</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1222971">1222971</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=915962">915962</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1180761">1180761</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1218455">1218455</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1222747">1222747</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1210752">1210752</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1198450">1198450</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1222820">1222820</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1225514">1225514</a></span></span></span></span></li>
+ <li><strong>1</strong> bug was triaged: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1230789"><span style="font-weight: 400;">1230789</span></a></li>
+ <li>some failures were mentioned for <i>Search Refactoring </i>feature in the etherpads (<a href="https://public.etherpad-mozilla.org/p/testday-20160115">link 1</a> and <a href="https://public.etherpad-mozilla.org/p/bangladesh.testday-15012016">link 2</a>); please feel free to add the requested details in the etherpads or, even better, join us on <a href="http://widget01.mibbit.com/?server=irc.mozilla.org&amp;channel=%23qa" target="_blank">#qa IRC channel</a> and let’s figure them out</li>
+ </ul>
+ <p>I <strong>strongly</strong> advise everyone of you to reach out to us, the moderators, via <a href="http://widget01.mibbit.com/?server=irc.mozilla.org&amp;channel=%23qa">#qa</a> during the events when you encountered any kind of failures. Keep up the great work! \o/</p>
+ <p>And keep an eye on QMO for upcoming events! <img alt="&#x1F609;" class="wp-smiley" src="https://s.w.org/images/core/emoji/72x72/1f609.png" style="height: 1em;"/></p></div>
+ </content>
+ <updated>2016-01-19T07:51:57Z</updated>
+ <category term="Community"/>
+ <category term="Events"/>
+ <category term="QMO News"/>
+ <author>
+ <name>Alexandra Lucinet</name>
+ </author>
+ <source>
+ <id>https://quality.mozilla.org</id>
+ <link href="https://quality.mozilla.org/feed/" rel="self" type="application/rss+xml"/>
+ <link href="https://quality.mozilla.org" rel="alternate" type="text/html"/>
+ <subtitle>Driving quality across Mozilla with data, metrics and a strong community focus</subtitle>
+ <title>Mozilla Quality Assurance</title>
+ <updated>2016-01-26T14:46:39Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en">
+ <id>http://blog.monotonous.org/?p=678</id>
+ <link href="http://blog.monotonous.org/2016/01/18/its-mlk-day-and-its-not-too-late-to-do-something-about-it/" rel="alternate" type="text/html"/>
+ <link href="http://blog.monotonous.org/2016/01/18/its-mlk-day-and-its-not-too-late-to-do-something-about-it/#comments" rel="replies" type="text/html"/>
+ <link href="http://blog.monotonous.org/2016/01/18/its-mlk-day-and-its-not-too-late-to-do-something-about-it/feed/atom/" rel="replies" type="application/atom+xml"/>
+ <title xml:lang="en">It’s MLK Day and It’s Not Too Late to Do Something About It</title>
+ <summary type="xhtml" xml:lang="en"><div xmlns="http://www.w3.org/1999/xhtml">For the last three years I have had the opportunity to send out a reminder to Mozilla staff that Martin Luther King Jr. Day is coming up, and that U.S. employees get the day off. It has turned into my MLK Day eve ritual. I read his letters, listen to speeches, and then I compose […]<img alt="" border="0" height="1" src="http://pixel.wp.com/b.gif?host=blog.monotonous.org&amp;blog=34885741&amp;post=678&amp;subd=blogdotmonotonousdotorg&amp;ref=&amp;feed=1" width="1"/></div>
+ </summary>
+ <content type="xhtml" xml:lang="en"><div xmlns="http://www.w3.org/1999/xhtml"><p>For the last three years I have had the opportunity to send out a reminder to Mozilla staff that Martin Luther King Jr. Day is coming up, and that U.S. employees get the day off. It has turned into my MLK Day eve ritual. I read his letters, listen to speeches, and then I compose a belabored paragraph about Dr. King with some choice quotes.</p>
+ <p>If you didn’t get a chance to celebrate Dr. King’s legacy and the movements he was a part of, you still have a chance:</p>
+ <ul>
+ <li>Watch <a href="http://www.imdb.com/title/tt1020072/" target="_blank">Selma.</a></li>
+ <li>Watch <a href="http://www.imdb.com/title/tt1592527/" target="_blank">The Black Power Mixtape</a> (it’s on Netflix).</li>
+ <li>Read <a href="http://www.africa.upenn.edu/Articles_Gen/Letter_Birmingham.html" target="_blank">A Letter from a Birmingham Jail</a> (it’s really really good).</li>
+ <li>Listen to his speech <a href="https://www.youtube.com/watch?v=3Qf6x9_MLD0" target="_blank">Beyond Vietnam</a>.</li>
+ <li>Listen to his last speech <a href="https://www.youtube.com/watch?v=IDl84vusXos" target="_blank">I Have Been To The Mountaintop</a>.</li>
+ </ul><br/> <a href="http://feeds.wordpress.com/1.0/gocomments/blogdotmonotonousdotorg.wordpress.com/678/" rel="nofollow"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/comments/blogdotmonotonousdotorg.wordpress.com/678/"/></a> <img alt="" border="0" height="1" src="http://pixel.wp.com/b.gif?host=blog.monotonous.org&amp;blog=34885741&amp;post=678&amp;subd=blogdotmonotonousdotorg&amp;ref=&amp;feed=1" width="1"/></div>
+ </content>
+ <updated>2016-01-18T23:35:19Z</updated>
+ <published>2016-01-18T23:34:26Z</published>
+ <category scheme="http://blog.monotonous.org" term="Personal"/>
+ <category scheme="http://blog.monotonous.org" term="Software"/>
+ <category scheme="http://blog.monotonous.org" term="World Affairs"/>
+ <author>
+ <name>Eitan</name>
+ <uri>http://mememe82.wordpress.com/</uri>
+ </author>
+ <source>
+ <id>http://blog.monotonous.org/feed/atom/</id>
+ <link href="http://blog.monotonous.org" rel="alternate" type="text/html"/>
+ <link href="http://blog.monotonous.org/feed/atom/" rel="self" type="application/atom+xml"/>
+ <link href="http://blog.monotonous.org/osd.xml" rel="search" title="Monotonous.org" type="application/opensearchdescription+xml"/>
+ <link href="https://s1.wp.com/opensearch.xml" rel="search" title="WordPress.com" type="application/opensearchdescription+xml"/>
+ <link href="http://blog.monotonous.org/?pushpress=hub" rel="hub" type="text/html"/>
+ <subtitle xml:lang="en">Eitan's Pitch</subtitle>
+ <title xml:lang="en">Monotonous.org</title>
+ <updated>2016-01-18T23:45:54Z</updated>
+ </source>
+ </entry>
+
+ <entry>
+ <id>http://www.ncameron.org/blog/rss/0e4d587c-380c-40ce-954a-7206f69bc1dd</id>
+ <link href="http://www.ncameron.org/blog/libmacro/" rel="alternate" type="text/html"/>
+ <title>Libmacro</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>As I outlined in an <a href="http://ncameron.org/blog/procedural-macros-framework/">earlier post</a>, libmacro is a new crate designed to be used by procedural macro authors. It provides the basic API for procedural macros to interact with the compiler. I expect higher level functionality to be provided by library crates. In this post I'll go into</p></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>As I outlined in an <a href="http://ncameron.org/blog/procedural-macros-framework/">earlier post</a>, libmacro is a new crate designed to be used by procedural macro authors. It provides the basic API for procedural macros to interact with the compiler. I expect higher level functionality to be provided by library crates. In this post I'll go into a bit more detail about the API I think should be exposed here.</p>
+
+ <p>This is a lot of stuff. I've probably missed something. If you use syntax extensions today and do something with libsyntax that would not be possible with libmacro, please let me know!</p>
+
+ <p>I previously introduced <code>MacroContext</code> as one of the gateways to libmacro. All procedural macros will have access to a <code>&amp;mut MacroContext</code>.</p>
+
+ <h3>Tokens</h3>
+
+ <p>I described the <code>tokens</code> module in the last post, I won't repeat that here.</p>
+
+ <p>There are a few more things I thought of. I mentioned a <code>TokenStream</code> which is a sequence of tokens. We should also have <code>TokenSlice</code> which is a borrowed slice of tokens (the slice to <code>TokenStream</code>'s <code>Vec</code>). These should implement the standard methods for sequences, in particular they support iteration, so can be <code>map</code>ed, etc.</p>
+
+ <p>In the earlier blog post, I talked about a token kind called <code>Delimited</code> which contains a delimited sequence of tokens. I would like to rename that to <code>Sequence</code> and add a <code>None</code> variant to the <code>Delimiter</code> enum. The <code>None</code> option is so that we can have blocks of tokens without using delimiters. It will be used for noting unsafety and other properties of tokens. Furthermore, it is useful for macro expansion (replacing the interpolated AST tokens currently present). Although <code>None</code> blocks do not affect scoping, they do affect precedence and parsing.</p>
+
+ <p>We should provide API for creating tokens. By default these have no hygiene information and come with a span which has no place in the source code, but shows the source of the token to be the procedural macro itself (see below for how this interacts with expansion of the current macro). I expect a <code>make_</code> function for each kind of token. We should also have API for creating macros in a given scope (which do the same thing but with provided hygiene information). This could be considered an over-rich API, since the hygiene information could be set after construction. However, since hygiene is fiddly and annoying to get right, we should make it as easy as possible to work with.</p>
+
+ <p>There should also be a function for creating a token which is just a fresh name. This is useful for creating new identifiers. Although this can be done by interning a string and then creating a token around it, it is used frequently enough to deserve a helper function.</p>
+
+ <h3>Emitting errors and warnings</h3>
+
+ <p>Procedural macros should report errors, warnings, etc. via the <code>MacroContext</code>. They should avoid panicking as much as possible since this will crash the compiler (once <code>catch_panic</code> is available, we should use it to catch such panics and exit gracefully, however, they will certainly still meaning aborting compilation).</p>
+
+ <p>Libmacro will 're-export' <code>DiagnosticBuilder</code> from <a href="https://dxr.mozilla.org/rust/source/src/libsyntax/errors/mod.rs">syntax::errors</a>. I don't actually expect this to be a literal re-export. We will use libmacro's version of <code>Span</code>, for example.</p>
+
+ <pre><code>impl MacroContext {
+ pub fn struct_error(&amp;self, &amp;str) -&gt; DiagnosticBuilder;
+ pub fn error(&amp;self, Option&lt;Span&gt;, &amp;str);
+ }
+
+ pub mod errors {
+ pub struct DiagnosticBuilder { ... }
+ impl DiagnosticBuilder { ... }
+ pub enum ErrorLevel { ... }
+ }
+ </code></pre>
+
+ <p>There should be a macro <code>try_emit!</code>, which reduces a <code>Result&lt;T, ErrStruct&gt;</code> to a T or calls <code>emit()</code> and then calls <code>unreachable!()</code> (if the error is not fatal, then it should be upgraded to a fatal error).</p>
+
+ <h3>Tokenising and quasi-quoting</h3>
+
+ <p>The simplest function here is <code>tokenize</code> which takes a string (<code>&amp;str</code>) and returns a <code>Result&lt;TokenStream, ErrStruct&gt;</code>. The string is treated like source text. The success option is the tokenised version of the string. I expect this function must take a <code>MacroContext</code> argument.</p>
+
+ <p>We will offer a quasi-quoting macro. This will return a <code>TokenStream</code> (in contrast to today's quasi-quoting which returns AST nodes), to be precise a <code>Result&lt;TokenStream, ErrStruct&gt;</code>. The string which is quoted may include metavariables (<code>$x</code>), and these are filled in with variables from the environment. The type of the variables should be either a <code>TokenStream</code>, a <code>TokenTree</code>, or a <code>Result&lt;TokenStream, ErrStruct&gt;</code> (in this last case, if the variable is an error, then it is just returned by the macro). For example,</p>
+
+ <pre><code>fn foo(cx: &amp;mut MacroContext, tokens: TokenStream) -&gt; TokenStream {
+ quote!(cx, fn foo() { $tokens }).unwrap()
+ }
+ </code></pre>
+
+ <p>The <code>quote!</code> macro can also handle multiple tokens when the variable corresponding with the metavariable has type <code>[TokenStream]</code> (or is dereferencable to it). In this case, the same syntax as used in macros-by-example can be used. For example, if <code>x: Vec&lt;TokenStream&gt;</code> then <code>quote!(cx, ($x),*)</code> will produce a <code>TokenStream</code> of a comma-separated list of tokens from the elements of <code>x</code>.</p>
+
+ <p>Since the <code>tokenize</code> function is a degenerate case of quasi-quoting, an alternative would be to always use <code>quote!</code> and remove <code>tokenize</code>. I believe there is utility in the simple function, and it must be used internally in any case.</p>
+
+ <p>These functions and macros should create tokens with spans and hygiene information set as described above for making new tokens. We might also offer versions which takes a scope and uses that as the context for tokenising.</p>
+
+ <h3>Parsing helper functions</h3>
+
+ <p>There are some common patterns for tokens to follow in macros. In particular those used as arguments for attribute-like macros. We will offer some functions which attempt to parse tokens into these patterns. I expect there will be more of these in time; to start with:</p>
+
+ <pre><code>pub mod parsing {
+ // Expects `(foo = "bar"),*`
+ pub fn parse_keyed_values(&amp;TokenSlice, &amp;mut MacroContext) -&gt; Result&lt;Vec&lt;(InternedString, String)&gt;, ErrStruct&gt;;
+ // Expects `"bar"`
+ pub fn parse_string(&amp;TokenSlice, &amp;mut MacroContext) -&gt; Result&lt;String, ErrStruct&gt;;
+ }
+ </code></pre>
+
+ <p>To be honest, given the token design in the last post, I think <code>parse_string</code> is unnecessary, but I wanted to give more than one example of this kind of function. If <code>parse_keyed_values</code> is the only one we end up with, then that is fine.</p>
+
+ <h3>Pattern matching</h3>
+
+ <p>The goal with the pattern matching API is to allow procedural macros to operate on tokens in the same way as macros-by-example. The pattern language is thus the same as that for macros-by-example.</p>
+
+ <p>There is a single macro, which I propose calling <code>matches</code>. Its first argument is the name of a <code>MacroContext</code>. Its second argument is the input, which must be a <code>TokenSlice</code> (or dereferencable to one). The third argument is a pattern definition. The macro produces a <code>Result&lt;T, ErrStruct&gt;</code> where <code>T</code> is the type produced by the pattern arms. If the pattern has multiple arms, then each arm must have the same type. An error is produced if none of the arms in the pattern are matched.</p>
+
+ <p>The pattern language follows the language for defining macros-by-example (but is slightly stricter). There are two forms, a single pattern form and a multiple pattern form. If the first character is a <code>{</code> then the pattern is treated as a multiple pattern form, if it starts with <code>(</code> then as a single pattern form, otherwise an error (causes a panic with a <code>Bug</code> error, as opposed to returning an <code>Err</code>).</p>
+
+ <p>The single pattern form is <code>(pattern) =&gt; { code }</code>. The multiple pattern form is <code>{(pattern) =&gt; { code } (pattern) =&gt; { code } ... (pattern) =&gt; { code }}</code>. <code>code</code> is any old Rust code which is executed when the corresponding pattern is matched. The pattern follows from macros-by-example - it is a series of characters treated as literals, meta-variables indicated with <code>$</code>, and the syntax for matching multiple variables. Any meta-variables are available as variables in the righthand side, e.g., <code>$x</code> becomes available as <code>x</code>. These variables have type <code>TokenStream</code> if they appear singly or <code>Vec&lt;TokenStream&gt;</code> if they appear multiply (or <code>Vec&lt;Vec&lt;TokenStream&gt;&gt;</code> and so forth).</p>
+
+ <p>Examples:</p>
+
+ <pre><code>matches!(cx, input, (foo($x:expr) bar) =&gt; {quote(cx, foo_bar($x).unwrap()}).unwrap()
+
+ matches!(cx, input, {
+ () =&gt; {
+ cx.err("No input?");
+ }
+ (foo($($x:ident),+ bar) =&gt; {
+ println!("found {} idents", x.len());
+ quote!(($x);*).unwrap()
+ }
+ }
+ })
+ </code></pre>
+
+ <p>Note that since we match AST items here, our backwards compatibility story is a bit complicated (though hopefully not much more so than with current macros).</p>
+
+ <h3>Hygiene</h3>
+
+ <p>The intention of the design is that the actual hygiene algorithm applied is irrelevant. Procedural macros should be able to use the same API if the hygiene algorithm changes (of course the result of applying the API might change). To this end, all hygiene objects are opaque and cannot be directly manipulated by macros.</p>
+
+ <p>I propose one module (<code>hygiene</code>) and two types: <code>Context</code> and <code>Scope</code>.</p>
+
+ <p>A <code>Context</code> is attached to each token and contains all hygiene information about that token. If two tokens have the same <code>Context</code>, then they may be compared syntactically. The reverse is not true - two tokens can have different <code>Context</code>s and still be equal. <code>Context</code>s can only be created by applying the hygiene algorithm and cannot be manipulated, only moved and stored.</p>
+
+ <p><code>MacroContext</code> has a method <code>fresh_hygiene_context</code> for creating a new, fresh <code>Context</code> (i.e., a <code>Context</code> not shared with any other tokens).</p>
+
+ <p><code>MacroContext</code> has a method <code>expansion_hygiene_context</code> for getting the <code>Context</code> where the macro is defined. This is equivalent to <code>.expansion_scope().direct_context()</code>, but might be more efficient (and I expect it to be used a lot).</p>
+
+ <p>A <code>Scope</code> provides information about a position within an AST at a certain point during macro expansion. For example,</p>
+
+ <pre><code>fn foo() {
+ a
+ {
+ b
+ c
+ }
+ }
+ </code></pre>
+
+ <p><code>a</code> and <code>b</code> will have different <code>Scope</code>s. <code>b</code> and <code>c</code> will have the same <code>Scope</code>s, even if <code>b</code> was written in this position and <code>c</code> is due to macro expansion. However, a <code>Scope</code> may contain more information than just the syntactic scopes, for example, it may contain information about pending scopes yet to be applied by the hygiene algorithm (i.e., information about <code>let</code> expressions which are in scope).</p>
+
+ <p>Note that a <code>Scope</code> means a scope in the macro hygiene sense, not the commonly used sense of a scope declared with <code>{}</code>. In particular, each <code>let</code> statement starts a new scope and the items and statements in a function body are in different scopes.</p>
+
+ <p>The functions <code>lookup_item_scope</code> and <code>lookup_statement_scope</code> take a <code>MacroContext</code> and a path, represented as a <code>TokenSlice</code>, and return the <code>Scope</code> which that item defines or an error if the path does not refer to an item, or the item does not define a scope of the right kind.</p>
+
+ <p>The function <code>lookup_scope_for</code> is similar, but returns the <code>Scope</code> in which an item is declared.</p>
+
+ <p><code>MacroContext</code> has a method <code>expansion_scope</code> for getting the scope in which the current macro is being expanded.</p>
+
+ <p><code>Scope</code> has a method <code>direct_context</code> which returns a <code>Context</code> for items declared directly (c.f., via macro expansion) in that <code>Scope</code>.</p>
+
+ <p><code>Scope</code> has a method <code>nested</code> which creates a fresh <code>Scope</code> nested within the receiver scope.</p>
+
+ <p><code>Scope</code> has a static method <code>empty</code> for creating an empty scope, that is one with no scope information at all (note that this is different from a top-level scope).</p>
+
+ <p>I expect the exact API around <code>Scope</code>s and <code>Context</code>s will need some work. <code>Scope</code> seems halfway between an intuitive, algorithm-neutral abstraction, and the scopes from the sets of scopes hygiene algorithm. I would prefer a <code>Scope</code> should be more abstract, on the other hand, macro authors may want fine-grained control over hygiene application.</p>
+
+ <h4>Manipulating hygiene information on tokens,</h4>
+
+ <pre><code>pub mod hygiene {
+ pub fn add(cx: &amp;mut MacroContext, t: &amp;Token, scope: &amp;Scope) -&gt; Token;
+ // Maybe unnecessary if we have direct access to Tokens.
+ pub fn set(t: &amp;Token, cx: &amp;Context) -&gt; Token;
+ // Maybe unnecessary - can use set with cx.expansion_hygiene_context().
+ // Also, bad name.
+ pub fn current(cx: &amp;MacroContext, t: &amp;Token) -&gt; Token;
+ }
+ </code></pre>
+
+ <p><code>add</code> adds <code>scope</code> to any context already on <code>t</code> (<code>Context</code> should have a similar method). Note that the implementation is a bit complex - the nature of the <code>Scope</code> might mean we replace the old context completely, or add to it.</p>
+
+ <h4>Applying hygiene when expanding the current macro</h4>
+
+ <p>By default, the current macro will be expanded in the standard way, having hygiene applied as expected. Mechanically, hygiene information is added to tokens when the macro is expanded. Assuming the sets of scopes algorithm, scopes (for example, for the macro's definition, and for the introduction) are added to any scopes already present on the token. A token with no hygiene information will thus behave like a token in a macro-by-example macro. Hygiene due to nested scopes created by the macro do not need to be taken into account by the macro author, this is handled at expansion time.</p>
+
+ <p>Procedural macro authors may want to customise hygiene application (it is common in Racket), for example, to introduce items that can be referred to by code in the call-site scope.</p>
+
+ <p>We must provide an option to expand the current macro without applying hygiene; the macro author must then handle hygiene. For this to work, the macro must be able to access information about the scope in which it is applied (see <code>MacroContext::expansion_scope</code>, above) and to supply a <code>Scope</code> indicating scopes that should be added to tokens following the macro expansion.</p>
+
+ <pre><code>pub mod hygiene {
+ pub enum ExpansionMode {
+ Automatic,
+ Manual(Scope),
+ }
+ }
+
+ impl MacroContext {
+ pub fn set_hygienic_expansion(hygiene::ExpansionMode);
+ }
+ </code></pre>
+
+ <p>We may wish to offer other modes for expansion which allow for tweaking hygiene application without requiring full manual application. One possible mode is where the author provides a <code>Scope</code> for the macro definition (rather than using the scope where the macro is actually defined), but hygiene is otherwise applied automatically. We might wish to give the author the option of applying scopes due to the macro definition, but not the introduction scopes.</p>
+
+ <p>On a related note, might we want to affect how spans are applied when the current macro is expanded? I can't think of a use case right now, but it seems like something that might be wanted.</p>
+
+ <p>Blocks of tokens (that is a <code>Sequence</code> token) may be marked (not sure how, exactly, perhaps using a distinguished context) such that it is expanded without any hygiene being applied or spans changed. There should be a function for creating such a <code>Sequence</code> from a <code>TokenSlice</code> in the <code>tokens</code> module. The primary motivation for this is to handle the tokens representing the body on which an annotation-like macro is present. For a 'decorator' macro, these tokens will be untouched (passed through by the macro), and since they are not touched by the macro, they should appear untouched by it (in terms of hygiene and spans).</p>
+
+ <h3>Applying macros</h3>
+
+ <p>We provide functionality to expand a provided macro or to lookup and expand a macro.</p>
+
+ <pre><code>pub mod apply {
+ pub fn expand_macro(cx: &amp;mut MacroContext,
+ expansion_scope: Scope,
+ macro: &amp;TokenSlice,
+ macro_scope: Scope,
+ input: &amp;TokenSlice)
+ -&gt; Result&lt;(TokenStream, Scope), ErrStruct&gt;;
+ pub fn lookup_and_expand_macro(cx: &amp;mut MacroContext,
+ expansion_scope: Scope,
+ macro: &amp;TokenSlice,
+ input: &amp;TokenSlice)
+ -&gt; Result&lt;(TokenStream, Scope), ErrStruct&gt;;
+ }
+ </code></pre>
+
+ <p>These functions apply macro hygiene in the usual way, with <code>expansion_scope</code> dictating the scope into which the macro is expanded. Other spans and hygiene information is taken from the tokens. <code>expand_macro</code> takes pending scopes from <code>macro_scope</code>, <code>lookup_and_expand_macro</code> uses the proper pending scopes. In order to apply the hygiene algorithm, the result of the macro must be parsable. The returned scope will contain pending scopes that can be applied by the macro to subsequent tokens.</p>
+
+ <p>We could provide versions that don't take an <code>expansion_scope</code> and use <code>cx.expansion_scope()</code>. Probably unnecessary.</p>
+
+ <pre><code>pub mod apply {
+ pub fn expand_macro_unhygienic(cx: &amp;mut MacroContext,
+ macro: &amp;TokenSlice,
+ input: &amp;TokenSlice)
+ -&gt; Result&lt;TokenStream, ErrStruct&gt;;
+ pub fn lookup_and_expand_macro_unhygienic(cx: &amp;mut MacroContext,
+ macro: &amp;TokenSlice,
+ input: &amp;TokenSlice)
+ -&gt; Result&lt;TokenStream, ErrStruct&gt;;
+ }
+ </code></pre>
+
+ <p>The <code>_unhygienic</code> variants expand a macro as in the first functions, but do not apply the hygiene algorithm or change any hygiene information. Any hygiene information on tokens is preserved. I'm not sure if <code>_unhygienic</code> are the right names - using these is not necessarily unhygienic, just that we are automatically applying the hygiene algorithm.</p>
+
+ <p>Note that all these functions are doing an eager expansion of macros, or in Scheme terms they are <code>local-expand</code> functions. </p>
+
+ <h3>Looking up items</h3>
+
+ <p>The function <code>lookup_item</code> takes a <code>MacroContext</code> and a path represented as a <code>TokenSlice</code> and returns a <code>TokenStream</code> for the item referred to by the path, or an error if name resolution failed. I'm not sure where this function should live.</p>
+
+ <h3>Interned strings</h3>
+
+ <pre><code>pub mod strings {
+ pub struct InternedString;
+
+ impl InternedString {
+ pub fn get(&amp;self) -&gt; String;
+ }
+
+ pub fn intern(cx: &amp;mut MacroContext, s: &amp;str) -&gt; Result&lt;InternedString, ErrStruct&gt;;
+ pub fn find(cx: &amp;mut MacroContext, s: &amp;str) -&gt; Result&lt;InternedString, ErrStruct&gt;;
+ pub fn find_or_intern(cx: &amp;mut MacroContext, s: &amp;str) -&gt; Result&lt;InternedString, ErrStruct&gt;;
+ }
+ </code></pre>
+
+ <p><code>intern</code> interns a string and returns a fresh <code>InternedString</code>. <code>find</code> tries to find <em>an</em> existing <code>InternedString</code>.</p>
+
+ <h3>Spans</h3>
+
+ <p>A span gives information about where in the source code a token is defined. It also gives information about where the token came from (how it was generated, if it was generated code).</p>
+
+ <p>There should be a <code>spans</code> module in libmacro, which will include a <code>Span</code> type which can be easily inter-converted with the <code>Span</code> defined in libsyntax. Libsyntax spans currently include information about stability, this will not be present in libmacro spans.</p>
+
+ <p>If the programmer does nothing special with spans, then they will be 'correct' by default. There are two important cases: tokens passed to the macro and tokens made fresh by the macro. The former will have the source span indicating where they were written and will include their history. The latter will have no source span and indicate they were created by the current macro. All tokens will have the history relating to expansion of the current macro added when the macro is expanded. At macro expansion, tokens with no source span will be given the macro use-site as their source.</p>
+
+ <p><code>Span</code>s can be freely copied between tokens.</p>
+
+ <p>It will probably useful to make it easy to manipulate spans. For example, rather than point at the macro's defining function, point at a helper function where the token is made. Or to set the origin to the current macro when the token was produced by another which should an implementation detail. I'm not sure what such an interface should look like (and is probably not necessary in an initial library).</p>
+
+ <h3>Feature gates</h3>
+
+ <pre><code>pub mod features {
+ pub enum FeatureStatus {
+ // The feature gate is allowed.
+ Allowed,
+ // The feature gate has not been enabled.
+ Disallowed,
+ // Use of the feature is forbidden by the compiler.
+ Forbidden,
+ }
+
+ pub fn query_feature(cx: &amp;MacroContext, feature: Token) -&gt; Result&lt;FeatureStatus, ErrStruct&gt;;
+ pub fn query_feature_by_str(cx: &amp;MacroContext, feature: &amp;str) -&gt; Result&lt;FeatureStatus, ErrStruct&gt;;
+ pub fn query_feature_unused(cx: &amp;MacroContext, feature: Token) -&gt; Result&lt;FeatureStatus, ErrStruct&gt;;
+ pub fn query_feature_by_str_unused(cx: &amp;MacroContext, feature: &amp;str) -&gt; Result&lt;FeatureStatus, ErrStruct&gt;;
+
+ pub fn used_feature_gate(cx: &amp;MacroContext, feature: Token) -&gt; Result&lt;(), ErrStruct&gt;;
+ pub fn used_feature_by_str(cx: &amp;MacroContext, feature: &amp;str) -&gt; Result&lt;(), ErrStruct&gt;;
+
+ pub fn allow_feature_gate(cx: &amp;MacroContext, feature: Token) -&gt; Result&lt;(), ErrStruct&gt;;
+ pub fn allow_feature_by_str(cx: &amp;MacroContext, feature: &amp;str) -&gt; Result&lt;(), ErrStruct&gt;;
+ pub fn disallow_feature_gate(cx: &amp;MacroContext, feature: Token) -&gt; Result&lt;(), ErrStruct&gt;;
+ pub fn disallow_feature_by_str(cx: &amp;MacroContext, feature: &amp;str) -&gt; Result&lt;(), ErrStruct&gt;;
+ }
+ </code></pre>
+
+ <p>The <code>query_*</code> functions query if a feature gate has been set. They return an error if the feature gate does not exist. The <code>_unused</code> variants do not mark the feature gate as used. The <code>used_</code> functions mark a feature gate as used, or return an error if it does not exist.</p>
+
+ <p>The <code>allow_</code> and <code>disallow_</code> functions set a feature gate as allowed or disallowed for the current crate. These functions will only affect feature gates which take affect after parsing and expansion are complete. They do not affect feature gates which are checked during parsing or expansion.</p>
+
+ <p>Question: do we need the <code>used_</code> functions? Could just call <code>query_</code> and ignore the result.</p>
+
+ <h3>Attributes</h3>
+
+ <p>We need some mechanism for setting attributes as used. I don't actually know how the unused attribute checking in the compiler works, so I can't spec this area. But, I expect <code>MacroContext</code> to make available some interface for reading attributes on a macro use and marking them as used.</p></div>
+ </content>
+ <updated>2016-01-18T21:40:42Z</updated>
+ <category term="Mozilla"/>
+ <category term="Rust"/>
+ <author>
+ <name>Nick Cameron</name>
+ </author>
+ <source>
+ <id>http://www.ncameron.org/blog/</id>
+ <link href="http://www.ncameron.org/blog/" rel="alternate" type="text/html"/>
+ <link href="http://www.ncameron.org/blog/rss/" rel="self" type="application/rss+xml"/>
+ <subtitle>I'm a research engineer at Mozilla working on Rust: the language, compiler, and tools. @nick_r_cameron</subtitle>
+ <title>featherweight musings</title>
+ <updated>2016-01-21T08:46:18Z</updated>
+ </source>
+ </entry>
+
+ <entry>
+ <id>http://geekyogre.com/rss/63eb682d-66b4-447d-8fb6-f4ed448019df</id>
+ <link href="http://geekyogre.com/skizze-progress-and-repl/" rel="alternate" type="text/html"/>
+ <title>Skizze progress and REPL</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p><img align="center" height="190" src="http://i.imgur.com/9z47NdA.png" width="600"/> <br/>
+ <br/> <br/>
+ Over the last 3 weeks, based on feedback we proceeded fledging out the concepts and the code behind <a href="https://github.com/skizzehq/skizze">Skizze</a>. <br/>
+ <a href="https://medium.com/@njpatel/">Neil Patel</a> suggested the following:</p>
+
+ <hr/>
+
+ <p><em>So I've been thinking about the server API. I think we want to choose one thing and do it as well as possible, instead of having</em></p></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p><img align="center" height="190" src="http://i.imgur.com/9z47NdA.png" width="600"/> <br/>
+ <br/> <br/>
+ Over the last 3 weeks, based on feedback we proceeded fledging out the concepts and the code behind <a href="https://github.com/skizzehq/skizze">Skizze</a>. <br/>
+ <a href="https://medium.com/@njpatel/">Neil Patel</a> suggested the following:</p>
+
+ <hr/>
+
+ <p><em>So I've been thinking about the server API. I think we want to choose one thing and do it as well as possible, instead of having six ways to talk to the server. I think that helps to keep things sane and simple overall.</em></p>
+
+ <p><em>Thinking about usage, I can only really imagine Skizze in an environment like <a href="https://xamarin.com/insights">ours</a>, which is high-throughput. I think that is it's 'home' and we should be optimising for that all day long.</em></p>
+
+ <p><em>Taking that into account, I believe we have two options:</em></p>
+
+ <ol>
+ <li><p><em>We go the gRPC route, provide .proto files and let people use the existing gRPC tooling to build support for their favourite language. That means we can happily give Ruby/Node/C#/etc devs a real way to get started up with Skizze almost immediately, piggy-backing on the gRPC docs etc.</em></p></li>
+ <li><p><em>We absorb the Redis Protocol. It does everything we need, is very lean, and we can (mostly) easily adapt it for what we need to do. The downside is that to get support from other libs, there will have to be actual libraries for every language. This could slow adoption, or it might be easy enough if people can reuse existing REDIS code. It's hard to tell how that would end up.</em></p></li>
+ </ol>
+
+ <p><em>gRPC is interesting because it's built already for distributed systems, across bad networks, and obviously is bi-directional etc. Without us having to spend time on the protocol, gRPC let's us easily add features that require streaming. Like, imagine a client being able to listen for changes in count/size and be notified instantly. That's something that gRPC is built for right now.</em></p>
+
+ <p><em>I think gRPC is a bit verbose, but I think it'll pay off for ease of third-party lib support and as things grow.</em></p>
+
+ <p><em>The CLI could easily be built to work with gRPC, including adding support for streaming stuff etc. Which could be pretty exciting.</em></p>
+
+ <hr/>
+
+ <p>That being said, we gave Skizze <a href="https://github.com/skizzehq/">a new home</a>, where based on feedback we developed .proto files and started rewriting big chunks of the code.</p>
+
+ <p>We added a new wrapper called "domain" which represents a stream. It wraps around Count-Min-Log, Bloom Filter, Top-K and HyperLogLog++, so when feeding it values it feeds all the sketches. Later we intend to allow attaching and detaching sketches from "domains" (We need a better name).</p>
+
+ <p>We also implemented a gRPC API which should allow easy wrapper creation in other languages.</p>
+
+ <p>Special thanks go to <a href="https://twitter.com/martinpintob">Martin Pinto</a> for helping out with unit tests and <a href="http://dopeness.org">Soren Macbeth</a> for thorough feedback and ideas about the "domain" concept. <br/>
+ Take a look at our initial REPL work there:</p>
+
+ <p><a href="http://geekyogre.com/content/images/2016/01/MBCY64aaKL.gif"><img alt="Link to this page" border="0" src="http://geekyogre.com/content/images/2016/01/skizze-1.png"/></a> <br/>
+ <a href="http://geekyogre.com/content/images/2016/01/MBCY64aaKL.gif">click for GIF</a></p></div>
+ </content>
+ <updated>2016-01-18T17:41:43Z</updated>
+ <category term="Big Data"/>
+ <category term="Data Science"/>
+ <author>
+ <name>Seif Lotfy</name>
+ </author>
+ <source>
+ <id>http://geekyogre.com/</id>
+ <link href="http://geekyogre.com/" rel="alternate" type="text/html"/>
+ <link href="http://geekyogre.com/rss/" rel="self" type="application/rss+xml"/>
+ <subtitle>The geekiest ogre alive</subtitle>
+ <title>Geeky Ogre</title>
+ <updated>2016-01-18T18:16:25Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en">
+ <id>http://dougbelshaw.com/blog/?p=39986</id>
+ <link href="http://dougbelshaw.com/blog/2016/01/18/open-badges-persona/" rel="alternate" type="text/html"/>
+ <title>What a post-Persona landscape means for Open Badges</title>
+ <summary>Why you shouldn't worry about Mozilla's recent announcement.</summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p><em><strong>Note:</strong> I don’t work for Mozilla any more, so (like <a href="https://www.youtube.com/watch?v=YQHsXMglC9A">Adele</a>) these are my thoughts ‘from the outside’…</em></p>
+ <hr/>
+ <h3>Introduction</h3>
+ <p><a href="http://openbadges.org">Open Badges</a> is no longer a <a href="http://mozilla.org">Mozilla</a> project. In fact, it hasn’t been for a while — the <a href="http://badgealliance.org">Badge Alliance</a> was set up a couple of years ago to promote the specification on a both a technical and community basis. As I stated in a recent post, this is a <strong>good</strong> thing and means that <a href="http://dougbelshaw.com/blog/2015/11/08/bright-future-badges/">the future is bright for Open Badges</a>.</p>
+ <p>However, Mozilla <em>is</em> still involved with the Open Badges project: Mark Surman, Executive Director of the Mozilla Foundation, sits on the board of the Badge Alliance. Mozilla also pays for contractors to work on the <a href="http://backpack.openbadges.org">Open Badges backpack</a> and there were badges earned at the <a href="http://mozillafestival.org">Mozilla Festival</a> a few months ago.</p>
+ <p>Although it may seem strange for those used to corporates interested purely in profit, Mozilla creates what the open web needs at any given time. Like any organisation, sometimes it gets these wrong, either because the concept was flawed, or because the execution was poor. Other times, I’d argue, Mozilla doesn’t give ideas and concepts enough time to gain traction.</p>
+ <h3>The end of Persona at Mozilla</h3>
+ <p>Open Badges, at its very essence, is a technical specification. It allows credentials with metadata hard-coded into them to be issued, exchanged, and displayed. This is done in a secure, standardised manner.</p>
+ <p><img alt="OBI diagram" class="alignnone wp-image-39987 size-full" src="http://i1.wp.com/dougbelshaw.com/blog/wp-content/uploads/2016/01/obi-diagram.png?w=100%25"/></p>
+ <p>For users to be able to access their ‘backpack’ (i.e. the place they store badges) they needed a secure login system.Back in 2011 at the start of the Open Badges project it made sense to make use of Mozilla’s nascent <a href="https://www.mozilla.org/en-US/persona/">Persona</a> project. This aimed to provide a way for users to easily sign into sites around the web without using their Facebook/Google logins. These ‘social’ sign-in methods mean that users are tracked around the web — something that Mozilla was obviously against.</p>
+ <p>By 2014, Persona wasn’t seen to be having the kind of ‘growth trajectory’ that Mozilla wanted. The project was transferred to <a href="http://identity.mozilla.com/post/78873831485/transitioning-persona-to-community-ownership">community ownership</a> and most of the team left Mozilla in 2015. It was <a href="https://groups.google.com/forum/#!msg/mozilla.dev.identity/mibOQrD6K0c/kt0NdMWbEQAJ">announced</a> that Persona would be shutting down as a Mozilla service in November 2016. While Persona will exist as an open source project, it won’t be hosted by Mozilla.</p>
+ <h3>What this means for Open Badges</h3>
+ <p>Although I’m not aware of an official announcement from the Badge Alliance, I think it’s worth making three points here.</p>
+ <h5>1. You can still use Persona</h5>
+ <p>If you’re a developer, you can still use Persona. It’s open source. It works.</p>
+ <h5>2. Persona is not central to the Open Badges Infrastructure</h5>
+ <p>The Open Badges backpack is <em>one</em> place where users can store their badges. There are others, including the <a href="https://openbadgepassport.com/">Open Badge Passport</a> and <a href="https://www.openbadgeacademy.com/">Open Badge Academy</a>. MacArthur, who seed-funded the Open Badges ecosystem, have a new platform launching through <a href="https://www.lrng.org/">LRNG</a>.</p>
+ <p>It is up to the organisations behind these various solutions as to how they allow users to authenticate. They may choose to allow social logins. They may force users to create logins based on their email address. They may decide to use an open source version of Persona. It’s entirely up to them.</p>
+ <h5>3. A post-Persona badges system has its advantages</h5>
+ <p>The Persona authentication system runs off email addresses. This means that transitioning <em>from</em> Persona to another system is relatively straightforward. It has, however, meant that for the past few years we’ve had a recurrent problem: what do you do with people being issued badges to multiple email addresses?</p>
+ <p>Tying badges to emails seemed like the easiest and fastest way to get to a critical mass in terms of Open Badge adoption. Now that’s worked, we need to think in a more nuanced way about allowing users to tie multiple identities to a single badge.</p>
+ <h4>Conclusion</h4>
+ <p>Persona was always a slightly awkward fit for Open Badges. Although, for a time, it made sense to use Persona for authentication to the Open Badges backpack, we’re now in a post-Persona landscape. This brings with it certain advantages.</p>
+ <p>As Nate Otto wrote in his post <a href="https://medium.com/badge-alliance/open-badges-in-2016-a-look-ahead-3cfe5c3c9878#.l5mhiztwx">Open Badges in 2016: A Look Ahead</a>, the project is growing up. It’s time to move beyond what was expedient at the dawn of Open Badges and look to the future. I’m sad to see the decline of Persona, but I’m excited what the future holds!</p>
+ <p style="text-align: right;"><em>Header image CC BY-NC-SA <a href="https://www.flickr.com/photos/blmiers2/6904758951/">Barbara</a></em></p></div>
+ </content>
+ <updated>2016-01-18T11:34:19Z</updated>
+ <category term="Open Badges"/>
+ <category term="Badge Alliance"/>
+ <category term="future"/>
+ <category term="Mozilla"/>
+ <category term="Persona"/>
+ <author>
+ <name>Doug Belshaw</name>
+ </author>
+ <source>
+ <id>http://dougbelshaw.com/blog</id>
+ <logo>http://dougbelshaw.com/blog/wp-content/plugins/podpress/images/powered_by_podpress_large.jpg</logo>
+ <category scheme="http://www.itunes.com/" term="education,"/>
+ <category scheme="http://www.itunes.com/" term="technology,"/>
+ <category scheme="http://www.itunes.com/" term="productivity,"/>
+ <category scheme="http://www.itunes.com/" term="leadership,"/>
+ <category scheme="http://www.itunes.com/" term="Mozilla"/>
+ <category scheme="http://www.itunes.com/" term="Education"/>
+ <category scheme="http://www.itunes.com/" term="Education Technology"/>
+ <category scheme="http://www.itunes.com/" term="Technology"/>
+ <category scheme="http://www.itunes.com/" term="Society &amp; Culture"/>
+ <category scheme="http://www.itunes.com/" term="Philosophy"/>
+ <author>
+ <name>Doug Belshaw</name>
+ <email>dajbelshaw@gmail.com</email>
+ </author>
+ <link href="http://dougbelshaw.com/blog/tag/mozilla/feed/" rel="self" type="application/rss+xml"/>
+ <link href="http://dougbelshaw.com/blog" rel="alternate" type="text/html"/>
+ <rights>Copyright © Open Educational Thinkering 2013</rights>
+ <subtitle>Doug Belshaw's blog</subtitle>
+ <title>Mozilla – Doug Belshaw’s blog</title>
+ <updated>2016-01-23T07:46:17Z</updated>
+ </source>
+ </entry>
+
+ <entry>
+ <id>tag:this-week-in-rust.org,2016-01-18:blog/2016/01/18/this-week-in-rust-114/</id>
+ <link href="http://this-week-in-rust.org/blog/2016/01/18/this-week-in-rust-114/" rel="alternate" type="text/html"/>
+ <title>This Week in Rust 114</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>Hello and welcome to another issue of <em>This Week in Rust</em>!
+ <a href="http://rust-lang.org">Rust</a> is a systems language pursuing the trifecta:
+ safety, concurrency, and speed. This is a weekly summary of its progress and
+ community. Want something mentioned? Tweet us at <a href="https://twitter.com/ThisWeekInRust">@ThisWeekInRust</a> or <a href="mailto:corey@octayn.net?subject=This%20Week%20in%20Rust%20Suggestion">send us an
+ email</a>!
+ Want to get involved? <a href="https://github.com/rust-lang/rust/blob/master/CONTRIBUTING.md">We love
+ contributions</a>.</p>
+ <p><em>This Week in Rust</em> is openly developed <a href="https://github.com/cmr/this-week-in-rust">on GitHub</a>.
+ If you find any errors in this week's issue, <a href="https://github.com/cmr/this-week-in-rust/pulls">please submit a PR</a>.</p>
+ <p>This week's edition was edited by: <a href="https://github.com/nasa42">nasa42</a>, <a href="https://github.com/brson">brson</a>, and <a href="https://github.com/llogiq">llogiq</a>.</p>
+ <h3>Updates from Rust Community</h3>
+ <h4>News &amp; Blog Posts</h4>
+ <ul>
+ <li><a href="http://gregchapple.com/contributing-to-the-rust-compiler/">Guide: Contributing to the Rust compiler</a>.</li>
+ <li><a href="http://www.ncameron.org/blog/a-type-safe-and-zero-allocation-library-for-reading-and-navigating-elf-files/">A type-safe and zero-allocation library for reading and navigating ELF files</a>.</li>
+ <li>[podcast] <a href="http://www.newrustacean.com/show_notes/e009/">New Rustacean podcast episode 09</a>. Getting into the nitty-gritty with Rust's traits.</li>
+ <li><a href="https://jadpole.github.io/arcaders/arcaders-1-12/">ArcadeRS 1.12: Brawl, at last</a>! Part of the series <a href="https://jadpole.github.io/arcaders/arcaders-1-0/">ArcadeRS 1.0: The project</a> - a series whose objective is to explore the Rust programming language and ecosystem through the development of a simple, old-school shooter.</li>
+ <li><a href="https://blog.thiago.me/raspberry-pi-bare-metal-programming-with-rust/">Raspberry Pi bare metal programming with Rust</a>.</li>
+ <li><a href="http://blog.servo.org/2016/01/11/twis-47/">This week in Servo 47</a>.</li>
+ <li><a href="http://www.redox-os.org/news/this-week-in-redox-10/">This week in Redox OS 10</a>.</li>
+ </ul>
+ <h4>Notable New Crates &amp; Project Updates</h4>
+ <ul>
+ <li><a href="https://github.com/ebkalderon/amethyst">Amethyst</a>. Data-oriented game engine written in Rust.</li>
+ <li><a href="https://www.rust-lang.org/">Rust website</a> has received some <a href="https://www.reddit.com/r/rust/comments/40zxey/major_website_updates/">major updates</a>.</li>
+ <li><a href="https://packages.debian.org/stretch/rustc">Rust</a> and <a href="https://packages.debian.org/stretch/cargo">Cargo</a> are now available in Debian stretch.</li>
+ <li><a href="https://community.particle.io/t/rust-on-particle-call-for-contributors/19090">Rust on Particle: Call for contributors</a>.</li>
+ <li><a href="https://dwrensha.github.io/capnproto-rust/2016/01/11/async-rpc.html">capnp-rpc-rust rewritten to use async I/O</a>.</li>
+ <li><a href="https://github.com/Ogeon/palette">Palette</a>. A Rust library for linear color calculations and conversion.</li>
+ </ul>
+ <h3>Updates from Rust Core</h3>
+ <p>164 pull requests were <a href="https://github.com/issues?q=is%3Apr+org%3Arust-lang+is%3Amerged+merged%3A2016-01-11..2016-01-18">merged in the last week</a>.</p>
+ <p>See the <a href="https://internals.rust-lang.org/t/triage-digest-tue-jan-05-2016/3052">triage digest</a> and <a href="https://internals.rust-lang.org/t/subteam-reports-2016-01-08/3067">subteam reports</a> for more details.</p>
+ <h4>Notable changes</h4>
+ <ul>
+ <li><a href="https://github.com/rust-lang/rust/pull/30943">std: Stabilize APIs for the 1.7 release</a>.</li>
+ <li><a href="https://github.com/rust-lang/rust/pull/27807">Refactor and improve: Arena, TypedArena</a>.</li>
+ <li><a href="https://github.com/rust-lang/rust/pull/29498">Let <code>str::replace</code> take a pattern</a>.</li>
+ <li><a href="https://github.com/rust-lang/rust/pull/30295">rustc_resolve: Fix bug in duplicate checking for extern crates</a>.</li>
+ <li><a href="https://github.com/rust-lang/rust/pull/30426">Rewrite BTreeMap to use parent pointers</a>.</li>
+ <li><a href="https://github.com/rust-lang/rust/pull/30446">Support generic associated consts</a>.</li>
+ <li><a href="https://github.com/rust-lang/rust/pull/30509">Add an <code>impl</code> for <code>Box&lt;Error&gt;</code> from String</a>.</li>
+ <li><a href="https://github.com/rust-lang/rust/pull/30533">Introduce "obligation forest" data structure into fulfillment to track backtraces</a>.</li>
+ <li><a href="https://github.com/rust-lang/rust/pull/30538">Remove negate_unsigned feature gate</a>.</li>
+ <li><a href="https://github.com/rust-lang/rust/pull/30567">llvm: Add support for vectorcall (X86_VectorCall) convention</a>.</li>
+ <li><a href="https://github.com/rust-lang/rust/pull/30676">Make coherence more tolerant of error types</a>.</li>
+ <li><a href="https://github.com/rust-lang/rust/pull/30740">Add fast path for ASCII in UTF-8 validation</a>.</li>
+ <li><a href="https://github.com/rust-lang/rust/pull/30753">Downgrade unit struct match via S(..) warnings to errors</a>.</li>
+ <li><a href="https://github.com/rust-lang/rust/pull/30930">Move const block checks before lowering step</a>.</li>
+ </ul>
+ <h4>New Contributors</h4>
+ <ul>
+ <li>Anton Blanchard</li>
+ <li>Jonas Tepe</li>
+ <li>Jörg Krause</li>
+ <li>Joshua Olson</li>
+ <li>kalita.alexey</li>
+ <li>Pierre Krieger</li>
+ <li>Sergey Veselkov</li>
+ <li>Simon Martin</li>
+ <li>Steffen</li>
+ <li>tomaka</li>
+ </ul>
+ <h4>Approved RFCs</h4>
+ <p>Changes to Rust follow the Rust <a href="https://github.com/rust-lang/rfcs#rust-rfcs">RFC (request for comments)
+ process</a>. These
+ are the RFCs that were approved for implementation this week:</p>
+ <ul>
+ <li><a href="https://github.com/rust-lang/rfcs/pull/1331">RFC 1331: <code>src/grammar</code> for the canonical grammar of the Rust language</a>.</li>
+ </ul>
+ <h4>Final Comment Period</h4>
+ <p>Every week <a href="https://rust-lang.org/team.html">the team</a> announces the
+ 'final comment period' for RFCs and key PRs which are reaching a
+ decision. Express your opinions now. <a href="https://github.com/rust-lang/rfcs/labels/final-comment-period">This week's FCPs</a> are:</p>
+ <ul>
+ <li><a href="https://github.com/rust-lang/rfcs/pull/1462">Add <code>[</code> to the FOLLOW(ty) in macro future-proofing rules</a>.</li>
+ <li><a href="https://github.com/rust-lang/rfcs/pull/1457">Rewrite <code>for</code> loop desugaring to use language items</a>.</li>
+ <li><a href="https://github.com/rust-lang/rfcs/pull/1320">Amend 1192 (RangeInclusive) to use an enum</a>.</li>
+ <li><a href="https://github.com/rust-lang/rfcs/pull/243">Trait-based exception handling</a>.</li>
+ <li><a href="https://github.com/rust-lang/rfcs/pull/1361">Improve Cargo target-specific dependencies</a>.</li>
+ <li><a href="https://github.com/rust-lang/rfcs/pull/1129">Add a <code>IndexAssign</code> trait that allows overloading "indexed assignment" expressions like <code>a[b] = c</code></a>.</li>
+ <li><a href="https://github.com/rust-lang/rfcs/pull/1196">Allow eliding more type parameters</a>.</li>
+ <li><a href="https://github.com/rust-lang/rfcs/pull/1296">Add an <code>alias</code> attribute to <code>#[link]</code> and <code>-l</code></a>.</li>
+ </ul>
+ <h4>New RFCs</h4>
+ <ul>
+ <li><a href="https://github.com/rust-lang/rfcs/pull/1459">Add a used attribute to prevent symbols from being discarded</a>.</li>
+ <li><a href="https://github.com/rust-lang/rfcs/pull/1461">Move some net2 functionality into libstd</a>.</li>
+ <li><a href="https://github.com/rust-lang/rfcs/pull/1465">Add <code>some!</code> macro for unwrapping Option more safely</a>.</li>
+ <li><a href="https://github.com/rust-lang/rfcs/pull/1467">Stabilize the <code>volatile_load</code> and <code>volatile_store</code> intrinsics as <code>ptr::volatile_read</code> and <code>ptr::volatile_write</code></a>.</li>
+ </ul>
+ <h3>Upcoming Events</h3>
+ <ul>
+ <li><a href="http://www.meetup.com/Rust-Meetup-Hamburg/events/227838367/">1/19. Rust Hack and Learn Hamburg @ Ponton</a>.</li>
+ <li><a href="http://www.meetup.com/Rust-Bay-Area/events/227841778/">1/21. SF Bay Area: Rust Concurrency and Parallelism</a>.</li>
+ <li><a href="http://www.meetup.com/opentechschool-berlin/">1/27. OpenTechSchool Berlin: Rust Hack and Learn</a>.</li>
+ </ul>
+ <p>If you are running a Rust event please add it to the <a href="https://www.google.com/calendar/embed?src=apd9vmbc22egenmtu5l6c5jbfc%40group.calendar.google.com">calendar</a> to get
+ it mentioned here. Email <a href="mailto:erick.tryzelaar@gmail.com">Erick Tryzelaar</a> or <a href="mailto:banderson@mozilla.com">Brian
+ Anderson</a> for access.</p>
+ <h3>fn work(on: RustProject) -&gt; Money</h3>
+ <ul>
+ <li><a href="http://maidsafe.net/rust_engineer.html">Rust Engineer</a> at MaidSafe.</li>
+ <li><a href="https://careers.mozilla.org/en-US/position/ozy21fwU">Research Engineer - Servo</a> at Mozilla.</li>
+ <li><a href="https://careers.mozilla.org/en-US/position/o0H41fww">Senior Research Engineer - Rust</a> at Mozilla.</li>
+ <li><a href="http://plv.mpi-sws.org/rustbelt/">PhD and postdoc positions</a> at MPI-SWS.</li>
+ </ul>
+ <p><em>Tweet us at <a href="https://twitter.com/ThisWeekInRust">@ThisWeekInRust</a> to get your job offers listed here!</em></p>
+ <h3>Crate of the Week</h3>
+ <p>This week's Crate of the Week is <a href="https://github.com/alexcrichton/toml-rs">toml</a>, a crate for all our configuration needs, simple yet effective.</p>
+ <p>Thanks to <a href="https://users.rust-lang.org/users/stebalien">Steven Allen</a> for the suggestion.</p>
+ <p><a href="https://users.rust-lang.org/t/crate-of-the-week/2704">Submit your suggestions for next week</a>!</p>
+ <h3>Quote of the Week</h3>
+ <blockquote>
+ <p>Borrow/lifetime errors are usually Rust compiler bugs.
+ Typically, I will spend 20 minutes detailing the precise conditions of
+ the bug, using language that understates my immense knowledge, while
+ demonstrating sympathetic understanding of the pressures placed on a
+ Rust compiler developer, who is also probably studying for several exams
+ at the moment. The developer reading my bug report may not understand
+ this stuff as well as I do, so I will carefully trace the lifetimes of
+ each variable, where memory is allocated on the stack vs the heap, which
+ struct or function owns a value at any point in time, where borrows
+ begin and where they... oh yeah, actually that variable really doesn't
+ live long enough.</p>
+ </blockquote>
+ <p>— <a href="https://www.reddit.com/r/rust/comments/4084yx/my_trick_when_i_get_stuck_as_a_beginner/cysqz3s">peterjoel on /r/rust</a>.</p>
+ <p>Thanks to <a href="https://users.rust-lang.org/users/WaDelma">Wa Delma</a> for the suggestion.</p>
+ <p><a href="http://users.rust-lang.org/t/twir-quote-of-the-week/328">Submit your quotes for next week</a>!</p></div>
+ </summary>
+ <updated>2016-01-18T05:00:00Z</updated>
+ <author>
+ <name>Corey Richardson</name>
+ </author>
+ <source>
+ <id>http://this-week-in-rust.org/</id>
+ <link href="http://this-week-in-rust.org/" rel="alternate" type="text/html"/>
+ <link href="http://this-week-in-rust.org/atom.xml" rel="self" type="application/atom+xml"/>
+ <title>This Week in Rust</title>
+ <updated>2016-01-25T05:00:00Z</updated>
+ </source>
+ </entry>
+
+ <entry>
+ <id>http://nikkisquared.github.io/2016/01/17/what-does-your-work-mean-part-2.html</id>
+ <link href="http://nikkisquared.github.io/2016/01/17/what-does-your-work-mean-part-2.html" rel="alternate" type="text/html"/>
+ <title>Okay, But What Does Your Work Actually Mean, Nikki? Part 2: The Fetch Standard and Servo</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>In my previous post, I started discussing in more detail what my internship entails, by talking about my first contribution to Servo. As a refresher, my first contribution was as part of my application to Outreachy, which I later revisited during my internship after a change I introduced to the HTML Standard it relied on. I’m going to expand on that last point today- specifically, how easy it is to introduce changes in <a href="https://wiki.whatwg.org/wiki/FAQ#What_is_the_WHATWG.3F">WHATWG</a>’s various standards. I’m also going to talk about how this accessibility to changing web standards affects how I can understand it, how I can help improve it, and my work on Servo.</p>
+
+ <h3>Two Ways To Change</h3>
+
+ <p>There are many ways to <a href="https://wiki.whatwg.org/wiki/What_you_can_do">get involved with WHATWG</a>, but there are two that I’ve become the most familiar with: firstly, by opening a discussion about a perceived issue and asking how it should be resolved; secondly, by taking on an issue approved as needing change and making the desired change. I’ve almost entirely only done the former, and the latter only for some minor typos. Any changes that relate directly to my work, however minor, are significant for me though! Like I discussed in my previous post, I brought attention to <a href="https://github.com/whatwg/html/issues/296">an inconsistency</a> that was resolved, giving me a new task of updating my first contribution to Servo to reflect the change in the HTML Standard. I’ve done that several times since, for the Fetch Standard.</p>
+
+ <h3>Understanding Fetch</h3>
+
+ <p>My first two weeks of my internship were spent on reading through the majority of the <a href="https://fetch.spec.whatwg.org/">Fetch Standard</a>, primarily the various Fetch functions. I took many notes describing the steps to myself, annotated with questions I had and the answers I got from either other people on the Servo team who had worked with Fetch (including my internship mentor, of course!) or people from WHATWG who were involved in the Fetch Standard. Getting so familiar with Fetch meant a few things: I would notice minor errors (such as an out of date link) that I could submit a <a href="https://github.com/whatwg/fetch/pull/173">simple fix for</a>, or a bigger issue that I couldn’t resolve myself.</p>
+
+ <h3>Discussions &amp; Resolutions</h3>
+
+ <p>I’m going to go into more detail about some of those bigger issues. From my perspective, when I start a discussion about a piece of documentation (such as the Fetch Standard, or reading about a programming library Servo uses), I go into it thinking “Either this documentation is incorrect, or my understanding is incorrectâ€. Whichever the answer is, it doesn’t mean that the documentation is bad, or that I’m bad at reading comprehension. I understand best by building up a model of something in my head, putting that to practice, and asking a lot of questions along the way. I learn by getting things wrong and figuring out why I was wrong, and sometimes in the process I uncover a point that could be made more clear, or an inconsistency! I have good examples of both of the different outcomes I listed, which I’ll cover over the next two sections.</p>
+
+ <h5>Looking For The Big Picture</h5>
+
+ <p>Early on in my initial review of the Fetch Standard’s several protocols, I found a major step that seemed to have no use. I understood that since I was learning Fetch on a step-by-step basis, I did not have a view of the bigger picture, so I asked around what I was missing that would help me understand this. One of the people I work with on implementing Fetch agreed with me that the step seemed to have no purpose, and so we decided to <a href="https://github.com/whatwg/fetch/issues/174">open an issue</a> asking about removing it from the standard. It turned out that I had actually missed the meaning of it, as we learned. However, instead of leaving it there, I shifted the issue into asking for some explanatory notes on why this step is needed, which was fulfilled. This meant that I would have a reference to go back to should I forget the significance of the step, and that people reading the Fetch Standard in the future would be much less likely to come to the same incorrect conclusion I had.</p>
+
+ <h5>A Confusing Order</h5>
+
+ <p>Shortly after I had first discovered that apparent issue, I found myself struggling to comprehend a sequence of actions in another Fetch protocol. The specification seemed to say that part of an early step was meant to only be done after the final step. I unfortunately don’t remember details of the discussion I had about this- if there was a reason for why it was organized like this, I forget what it was. Regardless, it was agreed that <a href="https://github.com/whatwg/fetch/issues/176">moving those sub-steps</a> to be actually listed after the step they’re supposed to run after would be a good change. This meant that I would need to re-organize my notes to reflect the re-arranged sequence of actions, as well as have an easier time being able to follow this part of the Fetch Standard.</p>
+
+ <h3>A Living Standard</h3>
+
+ <p>Like I said at the start of this post, I’m going to talk about how changes in the Fetch Standard affects my work on Servo itself. What I’ve covered so far has mostly been how changes affect my understanding of the standard itself. A key aspect in understanding the Fetch protocols is reviewing them for updates that impact me. WHATWG labels every standard they author as a “<a href="https://wiki.whatwg.org/wiki/FAQ#What_does_.22Living_Standard.22_mean.3F">Living Standard</a>†for good reason. It was one thing for me to learn how easy it is to introduce changes, while knowing exactly what’s going on, but it’s another for me to understand that anybody else can, and often does, make changes to the Fetch Standard!</p>
+
+ <h5>Changes Over Time</h5>
+
+ <p>When an update is made to the Fetch Standard, it’s not so difficult to deal with as one might imagine. The Fetch Standard always notes the last day it was updated at the top of the document, I follow a Twitter account that <a href="https://twitter.com/fetchstandard">posts about updates</a>, and all the history can be <a href="https://github.com/whatwg/fetch/commits">seen on GitHub</a> which will show me exactly what has been changed as well as some discussion on what the change does. All of these together alert me to the fact that the Fetch Standard has been modified, and I can quickly see what was revised. If it’s relevant to what I’m going to be implementing, I update my notes to match it. Occasionally, I need to change existing code to reflect the new Standard, which is also easily done by comparing my new notes to the Fetch implementation in Servo!</p>
+
+ <h5>Snapshots</h5>
+
+ <p>From all of this, it might sound like the Fetch Standard is unfinished, or unreliable/inconsistent. I don’t mean to misrepresent it- the many small improvements help make the Fetch Standard, like all of WHATWG’s standards, better and more reliable. You can think of the status of the Fetch Standard at any point in time as a single, working snapshot. If somebody implemented all of Fetch as it is now, they’d have something that works by itself correctly. A different snapshot of Fetch is just that- different. It will have an improvement or two, but that doesn’t obsolete anybody who implemented it previously. It just means if they revisit the implementation, they’ll have things to update.</p>
+
+ <p>Third post over.</p></div>
+ </summary>
+ <updated>2016-01-17T20:20:27Z</updated>
+ <category term="outreachy,"/>
+ <category term="planet"/>
+ <source>
+ <id>http://nikkisquared.github.io/</id>
+ <author>
+ <name>Nikki Bee</name>
+ </author>
+ <link href="http://nikkisquared.github.io/" rel="alternate" type="text/html"/>
+ <link href="http://nikkisquared.github.io/feed.xml" rel="self" type="application/rss+xml"/>
+ <subtitle>Hi! I'm currently doing an internship for Outreachy. Wow!</subtitle>
+ <title>Nikki Bee Blog</title>
+ <updated>2016-01-18T05:28:11Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-us">
+ <id>http://ngokevin.com/blog/aframe-component/</id>
+ <link href="http://ngokevin.com/blog/aframe-component/" rel="alternate" type="text/html"/>
+ <title>How to Write an A-Frame VR Component</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><img align="left" hspace="5" src="http://thevrjump.com/assets/img/articles/aframe-system/aframe-example.jpg" width="320"/>Abstract representation of components by @rubenmueller of thevrjump.com.
+
+ <p><a href="http://ngokevin.com/blog/aframe">A-Frame</a> is a WebVR framework that introduces the
+ <a href="http://ngokevin.com/blog/aframe-vs-3dml">entity-component system</a> (<a href="http://ngokevin.com/rss/docs">docs</a>) to the DOM. The
+ entity-component system treats every <strong>entity</strong> in the scene as a placeholder
+ object which we apply and mix <strong>components</strong> to in order to add appearance,
+ behavior, and functionality. A-Frame comes with some standard components out of
+ the box like camera, geometry, material, light, or sound. However, people can
+ write, publish, and register their own components to do <strong>whatever</strong> they want
+ like have entities <a href="https://github.com/dmarcos/a-invaders/tree/master/js/components">collide/explode/spawn</a>, be controlled by
+ <a href="https://github.com/ngokevin/aframe-physics-components">physics</a>, or <a href="https://jsbin.com/dasefeh/edit?html,output">follow a path</a>. Today, we'll be going through
+ how we can write our own A-Frame components.</p>
+ <blockquote>
+ <p>Note that this tutorial will be covering the upcoming release of <a href="https://github.com/aframevr/aframe/blob/dev/CHANGELOG.md#dev">A-Frame
+ 0.2.0</a> which vastly improves the component API.</p>
+ </blockquote>
+ <h3>Table of Contents</h3>
+ <ul>
+ <li><a href="http://ngokevin.com/rss/index.xml#what-a-component-looks-like">What a Component Looks Like</a><ul>
+ <li><a href="http://ngokevin.com/rss/index.xml#from-the-dom">From the DOM</a></li>
+ <li><a href="http://ngokevin.com/rss/index.xml#under-the-hood">Under the Hood</a></li>
+ </ul>
+ </li>
+ <li><a href="http://ngokevin.com/rss/index.xml#defining-the-schema">Defining the Schema</a><ul>
+ <li><a href="http://ngokevin.com/rss/index.xml#property-types">Property Types</a></li>
+ <li><a href="http://ngokevin.com/rss/index.xml#single-property-schemas">Single-Property Schemas</a></li>
+ <li><a href="http://ngokevin.com/rss/index.xml#multiple-property-schemas">Multiple-Property Schemas</a></li>
+ </ul>
+ </li>
+ <li><a href="http://ngokevin.com/rss/index.xml#defining-the-lifecycle-methods">Defining the Lifecycle Methods</a><ul>
+ <li><a href="http://ngokevin.com/rss/index.xml#component-init-set-up">Component.init() - Set Up</a></li>
+ <li><a href="http://ngokevin.com/rss/index.xml#component-update-olddata-do-the-magic">Component.update(oldData) - Do the Magic</a></li>
+ <li><a href="http://ngokevin.com/rss/index.xml#component-remove-tear-down">Component.remove() - Tear Down</a></li>
+ <li><a href="http://ngokevin.com/rss/index.xml#component-tick-time-background-behavior">Component.tick() - Background Behavior</a></li>
+ <li><a href="http://ngokevin.com/rss/index.xml#component-pause-and-component-play-stop-and-go">Component.pause() and Component.play() - Stop and Go</a></li>
+ </ul>
+ </li>
+ <li><a href="http://ngokevin.com/rss/index.xml#boilerplate">Boilerplate</a></li>
+ <li><a href="http://ngokevin.com/rss/index.xml#examples">Examples</a><ul>
+ <li><a href="http://ngokevin.com/rss/index.xml#text-component">Text Component</a></li>
+ <li><a href="http://ngokevin.com/rss/index.xml#physics-components">Physics Components</a></li>
+ <li><a href="http://ngokevin.com/rss/index.xml#layout-component">Layout Component</a></li>
+ </ul>
+ </li>
+ </ul>
+ <h3>What a Component Looks Like</h3>
+ <p>A component contains a bucket of data in the form of component properties. This
+ data is used to modify the entity. For example, we might have an <em>engine</em>
+ component. Possible properties might be <em>horsepower</em> or <em>cylinders</em>.</p>
+ <p><img alt="" src="http://thevrjump.com/assets/img/articles/aframe-system/aframe-system.jpg"/>
+ </p><div class="page-caption"><span>
+ Abstract representation of a component by @rubenmueller of thevrjump.com.
+ </span></div><p/>
+ <h4>From the DOM</h4>
+ <p>Let's first see what a component looks like from the DOM.</p>
+ <p>For example, the <a href="https://aframe.io/docs/components/light.html">light component</a> has properties such as type, color,
+ and intensity. In A-Frame, we register and configure a component to an entity
+ using an HTML attribute and a style-like syntax:</p>
+ <div class="highlight"><pre><span class="p">&lt;</span><span class="nt">a-entity</span> <span class="na">light</span><span class="o">=</span><span class="s">"type: point; color: crimson; intensity: 2.5"</span><span class="p">&gt;&lt;/</span><span class="nt">a-entity</span><span class="p">&gt;</span>
+ </pre></div>
+
+
+ <p>This would give us a light in the scene. To demonstrate composability, we could
+ give the light a spherical representation by mixing in the <a href="https://aframe.io/docs/components/geometry.html">geometry
+ component</a>.</p>
+ <div class="highlight"><pre><span class="p">&lt;</span><span class="nt">a-entity</span> <span class="na">geometry</span><span class="o">=</span><span class="s">"primitive: sphere; radius: 5"</span>
+ <span class="na">light</span><span class="o">=</span><span class="s">"type: point; color: crimson; intensity: 2.5"</span><span class="p">&gt;&lt;/</span><span class="nt">a-entity</span><span class="p">&gt;</span>
+ </pre></div>
+
+
+ <p>Or we can configure the position component to move the light sphere a bit to the right.</p>
+ <div class="highlight"><pre><span class="p">&lt;</span><span class="nt">a-entity</span> <span class="na">geometry</span><span class="o">=</span><span class="s">"primitive: sphere; radius: 5"</span>
+ <span class="na">light</span><span class="o">=</span><span class="s">"type: point; color: crimson; intensity: 2.5"</span>
+ <span class="na">position</span><span class="o">=</span><span class="s">"5 0 0"</span><span class="p">&gt;&lt;/</span><span class="nt">a-entity</span><span class="p">&gt;</span>
+ </pre></div>
+
+
+ <p>Given the style-like syntax and that it modifies the appearance and behavior of
+ DOM nodes, component properties can be thought of as a rough analog to CSS. In
+ the near future, I can imagine component property stylesheets.</p>
+ <h4>Under the Hood</h4>
+ <p>Now let's see what a component looks like <strong>under the hood</strong>. A-Frame's most
+ basic component is the <a href="https://aframe.io/docs/components/position.html">position component</a>:</p>
+ <div class="highlight"><pre><span class="nx">AFRAME</span><span class="p">.</span><span class="nx">registerComponent</span><span class="p">(</span><span class="s1">'position'</span><span class="p">,</span> <span class="p">{</span>
+ <span class="nx">schema</span><span class="o">:</span> <span class="p">{</span> <span class="nx">type</span><span class="o">:</span> <span class="s1">'vec3'</span> <span class="p">},</span>
+
+ <span class="nx">update</span><span class="o">:</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
+ <span class="kd">var</span> <span class="nx">object3D</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">el</span><span class="p">.</span><span class="nx">object3D</span><span class="p">;</span>
+ <span class="kd">var</span> <span class="nx">data</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">data</span><span class="p">;</span>
+ <span class="nx">object3D</span><span class="p">.</span><span class="nx">position</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">x</span><span class="p">,</span> <span class="nx">data</span><span class="p">.</span><span class="nx">y</span><span class="p">,</span> <span class="nx">data</span><span class="p">.</span><span class="nx">z</span><span class="p">);</span>
+ <span class="p">}</span>
+ <span class="p">});</span>
+ </pre></div>
+
+
+ <p>The position component uses only a tiny subset of the component API, but what
+ this does is register the component with the name "position", define a <code>schema</code>
+ where the component's value with be parsed to an <code>{x, y, z}</code> object, and when
+ the component initializes or the component's data updates, set the position of
+ the entity with the <code>update</code> callback. <code>this.el</code> is a reference from the
+ component to the DOM element, or entity, and <code>object3D</code> is the entity's
+ <a href="http://threejs.org/">three.js</a>. Note that A-Frame is built on top of three.js so many
+ components will be using the three.js API.</p>
+ <p>So we see that components consist of a name and a definition, and then they can
+ be registered to A-Frame. We saw the the position component definition defined
+ a <code>schema</code> and an <code>update</code> handler. Components simply consist of the <code>schema</code>,
+ which defines the shape of the data, and several handlers for the component to
+ modify the entity in reaction to different types of events.</p>
+ <p>Here is the current list of properties and methods of a component definition:</p>
+ <table class="pure-table-striped">
+ <tbody><tr>
+ <th>Property</th>
+ <th>Description</th>
+ </tr>
+ <tr>
+ <td>data</td>
+ <td>Data of the component derived from the schema default values, mixins, and the entity's attributes.</td>
+ </tr>
+ <tr>
+ <td>el</td>
+ <td>Reference to the <a href="https://aframe.io/docs/core/entity.html">entity</a> element.</td>
+ </tr>
+ <tr>
+ <td>schema</td>
+ <td>Names, types, and default values of the component property value(s)</td>
+ </tr>
+ </tbody></table>
+
+ <table class="pure-table-striped">
+ <tbody><tr><th>Method</th><th>Description</th></tr>
+ <tr>
+ <td>init</td>
+ <td>Called once when the component is initialized.</td>
+ </tr>
+ <tr>
+ <td>update</td>
+ <td>Called both when the component is initialized and whenever the component's data changes (e.g, via <i>setAttribute</i>).</td>
+ </tr>
+ <tr>
+ <td>remove</td>
+ <td>Called when the component detaches from the element (e.g., via <i>removeAttribute</i>).</td>
+ </tr>
+ <tr>
+ <td>tick</td>
+ <td>Called on each render loop or tick of the scene.</td>
+ </tr>
+ <tr>
+ <td>play</td>
+ <td>Called whenever the scene or entity plays to add any background or dynamic behavior.</td>
+ </tr>
+ <tr>
+ <td>pause</td>
+ <td>Called whenever the scene or entity pauses to remove any background or dynamic behavior.</td>
+ </tr>
+ </tbody></table>
+
+ <h3>Defining the Schema</h3>
+ <p>The component's schema defines what type of data it takes. A component can
+ either be single-property or consist of multiple properties. And properties
+ have <em>property types</em>. Note that single-property schemas and property types are
+ being released in A-Frame <code>v0.2.0</code>.</p>
+ <p>A property might look like:</p>
+ <div class="highlight"><pre><span class="p">{</span> <span class="nx">type</span><span class="o">:</span> <span class="s1">'int'</span><span class="p">,</span> <span class="k">default</span><span class="o">:</span> <span class="mi">5</span> <span class="p">}</span>
+ </pre></div>
+
+
+ <p>And a schema consisting of multiple properties might look like:</p>
+ <div class="highlight"><pre><span class="p">{</span>
+ <span class="nx">color</span><span class="o">:</span> <span class="p">{</span> <span class="k">default</span><span class="o">:</span> <span class="s1">'#FFF'</span> <span class="p">},</span>
+ <span class="nx">target</span><span class="o">:</span> <span class="p">{</span> <span class="nx">type</span><span class="o">:</span> <span class="s1">'selector'</span> <span class="p">},</span>
+ <span class="nx">uv</span><span class="o">:</span> <span class="p">{</span>
+ <span class="k">default</span><span class="o">:</span> <span class="s1">'1 1'</span><span class="p">,</span>
+ <span class="nx">parse</span><span class="o">:</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">value</span><span class="p">)</span> <span class="p">{</span>
+ <span class="k">return</span> <span class="nx">value</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="s1">' '</span><span class="p">).</span><span class="nx">map</span><span class="p">(</span><span class="nb">parseFloat</span><span class="p">);</span>
+ <span class="p">}</span>
+ <span class="p">},</span>
+ <span class="p">}</span>
+ </pre></div>
+
+
+ <p>Since components in the entity-component system are just buckets of data that
+ are used to affect the appearance or behavior of the entity, the schema plays a
+ crucial role in the definition of the component.</p>
+ <h4>Property Types</h4>
+ <p>A-Frame comes with several built-in property types such as <code>boolean</code>, <code>int</code>,
+ <code>number</code>, <code>selector</code>, <code>string</code>, or <code>vec3</code>. Every single property is assigned a
+ type, whether explicitly through the <code>type</code> key or implictly via inferring the
+ value. And each type is used to assign <code>parse</code> and <code>stringify</code> functions. The
+ parser deserializes the incoming string value from the DOM to be put into the
+ component's data object. The stringifier is used when using <code>setAttribute</code> to
+ serialize back to the DOM.</p>
+ <p>We can actually define and register our own property types:</p>
+ <div class="highlight"><pre><span class="nx">AFRAME</span><span class="p">.</span><span class="nx">registerPropertyType</span><span class="p">(</span><span class="s1">'radians'</span><span class="p">,</span> <span class="p">{</span>
+ <span class="nx">parse</span><span class="o">:</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
+
+ <span class="p">}</span>
+
+ <span class="c1">// Default stringify is .toString().</span>
+ <span class="p">});</span>
+ </pre></div>
+
+
+ <h4>Single-Property Schemas</h4>
+ <p>If a component has only one property, then it must either have a <code>type</code> or a
+ <code>default</code> value. If the type is defined, then the type is used to parse and
+ coerce the string retrieved from the DOM (e.g., <code>getAttribute</code>). Or if the
+ default value is defined, the default value is used to infer the type.</p>
+ <p>Take for instance the <a href="https://aframe.io/docs/components/visible.html">visible component</a>. The schema property
+ definition implicitly defines it as a boolean:</p>
+ <div class="highlight"><pre><span class="nx">AFRAME</span><span class="p">.</span><span class="nx">registerComponent</span><span class="p">(</span><span class="s1">'visible'</span><span class="p">,</span> <span class="p">{</span>
+ <span class="nx">schema</span><span class="o">:</span> <span class="p">{</span>
+ <span class="c1">// Type will be inferred to be boolean.</span>
+ <span class="k">default</span><span class="o">:</span> <span class="kc">true</span>
+ <span class="p">},</span>
+
+ <span class="c1">// ...</span>
+ <span class="p">});</span>
+ </pre></div>
+
+
+ <p>Or the <a href="https://aframe.io/docs/components/rotation.html">rotation component</a> which explicitly defines the value as a <code>vec3</code>:</p>
+ <div class="highlight"><pre><span class="nx">AFRAME</span><span class="p">.</span><span class="nx">registerComponent</span><span class="p">(</span><span class="s1">'rotation'</span><span class="p">,</span> <span class="p">{</span>
+ <span class="nx">schema</span><span class="o">:</span> <span class="p">{</span>
+ <span class="c1">// Default value will be 0, 0, 0 as defined by the vec3 property type.</span>
+ <span class="nx">type</span><span class="o">:</span> <span class="s1">'vec3'</span>
+ <span class="p">}</span>
+
+ <span class="c1">// ...</span>
+ <span class="p">});</span>
+ </pre></div>
+
+
+ <p>Using these defined property types, schemas are processed by
+ <code>registerComponent</code> to inject default values, parsers, and stringifiers for
+ each property. So if a default value is not defined, the default value will be
+ whatever the property type defines as the "default default value".</p>
+ <h4>Multiple-Property Schemas</h4>
+ <p>If a component has multiple properties (or one named property), then it consists of
+ one or more property definitions, in the form described above, in an object keyed by
+ property name. For instance, a physics body component might define a schema:</p>
+ <div class="highlight"><pre><span class="nx">AFRAME</span><span class="p">.</span><span class="nx">registerComponent</span><span class="p">(</span><span class="s1">'physics-body'</span><span class="p">,</span> <span class="p">{</span>
+ <span class="nx">schema</span><span class="o">:</span> <span class="p">{</span>
+ <span class="nx">boundingBox</span><span class="o">:</span> <span class="p">{</span>
+ <span class="nx">type</span><span class="o">:</span> <span class="s1">'vec3'</span><span class="p">,</span>
+ <span class="k">default</span><span class="o">:</span> <span class="p">{</span> <span class="nx">x</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">y</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">z</span><span class="o">:</span> <span class="mi">1</span> <span class="p">}</span>
+ <span class="p">},</span>
+ <span class="nx">mass</span><span class="o">:</span> <span class="p">{</span>
+ <span class="k">default</span><span class="o">:</span> <span class="mi">0</span>
+ <span class="p">},</span>
+ <span class="nx">velocity</span><span class="o">:</span> <span class="p">{</span>
+ <span class="nx">type</span><span class="o">:</span> <span class="s1">'vec3'</span>
+ <span class="p">}</span>
+ <span class="p">}</span>
+ <span class="p">}</span>
+ </pre></div>
+
+
+ <p>Having multiple properties is what makes the component take the syntax in the
+ form of <code>physics="mass: 2; velocity: 1 1 1"</code>.</p>
+ <p>With the schema defined, all data coming into the component will be passed
+ through the schema for parsing. Then in the lifecycle methods, the component
+ has access to <code>this.data</code> which in a single-property schema is a value and in a
+ multiple-propery schema is an object.</p>
+ <h3>Defining the Lifecycle Methods</h3>
+ <h4>Component.init() - Set Up</h4>
+ <p><code>init</code> is called once in the component's lifecycle when it is mounted to the
+ entity. <code>init</code> is generally used to set up variables or members that may used
+ throughout the component or to set up state. Though not every component will
+ need to define an <code>init</code> handler. Sort of like the component-equivalent method
+ to <code>createdCallback</code> or <code>React.ComponentDidMount</code>.</p>
+ <p>For example, the <code>look-at</code> component's <code>init</code> handler sets up some variables:</p>
+ <div class="highlight"><pre><span class="nx">init</span><span class="o">:</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
+ <span class="k">this</span><span class="p">.</span><span class="nx">target3D</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
+ <span class="k">this</span><span class="p">.</span><span class="nx">vector</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">THREE</span><span class="p">.</span><span class="nx">Vector3</span><span class="p">();</span>
+ <span class="p">},</span>
+
+ <span class="c1">// ...</span>
+ </pre></div>
+
+
+ <h4>Component.update(oldData) - Do the Magic</h4>
+ <p>The <code>update</code> handler is called both at the beginning of the component's
+ lifecycle with the initial <code>this.data</code> <em>and</em> every time the component's data
+ changes (generally during the entity's <code>attributeChangedCallback</code> like with a
+ <code>setAttribute</code>). The update handler gets access to the previous state of the
+ component data passed in through <code>oldData</code>. The previous state of the component
+ can be used to tell exactly which properties changed to do more granular
+ updates.</p>
+ <p>The update handler uses <code>this.data</code> to modify the entity, usually interacting
+ with three.js APIs. One of the simplest update handlers is the
+ <a href="https://aframe.io/docs/components/visible.html">visible</a> component's:</p>
+ <div class="highlight"><pre><span class="nx">update</span><span class="o">:</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
+ <span class="k">this</span><span class="p">.</span><span class="nx">el</span><span class="p">.</span><span class="nx">object3D</span><span class="p">.</span><span class="nx">visible</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">data</span><span class="p">;</span>
+ <span class="p">}</span>
+ </pre></div>
+
+
+ <p>A slightly more complex update handler might be the <a href="https://aframe.io/docs/components/light.html">light</a> component's,
+ which we'll show via abbreviated code:</p>
+ <div class="highlight"><pre><span class="nx">update</span><span class="o">:</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">oldData</span><span class="p">)</span> <span class="p">{</span>
+ <span class="kd">var</span> <span class="nx">diffData</span> <span class="o">=</span> <span class="nx">diff</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span> <span class="nx">oldData</span> <span class="o">||</span> <span class="p">{});</span>
+
+ <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">light</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="p">(</span><span class="s1">'type'</span> <span class="k">in</span> <span class="nx">diffData</span><span class="p">))</span> <span class="p">{</span>
+ <span class="c1">// If there is an existing light and the type hasn't changed, update light.</span>
+ <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">diffData</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">property</span><span class="p">)</span> <span class="p">{</span>
+ <span class="nx">light</span><span class="p">[</span><span class="nx">property</span><span class="p">]</span> <span class="o">=</span> <span class="nx">diffData</span><span class="p">[</span><span class="nx">property</span><span class="p">];</span>
+ <span class="p">});</span>
+ <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
+ <span class="c1">// No light exists yet or the type of light has changed, create a new light.</span>
+ <span class="k">this</span><span class="p">.</span><span class="nx">light</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">getLight</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">data</span><span class="p">));</span>
+
+ <span class="c1">// Register the object3D of type `light` to the entity.</span>
+ <span class="k">this</span><span class="p">.</span><span class="nx">el</span><span class="p">.</span><span class="nx">setObject3D</span><span class="p">(</span><span class="s1">'light'</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">light</span><span class="p">);</span>
+ <span class="p">}</span>
+ <span class="p">}</span>
+ </pre></div>
+
+
+ <p>The entity's <code>object3D</code> is a plain THREE.Object3D. Other three.js object types
+ such as meshes, lights, and cameras can be set with <code>setObject3D</code> where they
+ will be appeneded to the entity's <code>object3D</code>.</p>
+ <h4>Component.remove() - Tear Down</h4>
+ <p>The <code>remove</code> handler is called when the component detaches from the entity such
+ as with <code>removeAttribute</code>. This is generally used to remove all modifications,
+ listeners, and behaviors to the entity that the component added.</p>
+ <p>For example, when the <a href="https://aframe.io/docs/components/light.html">light component</a> detaches, it removes the light
+ it previously attached from the entity and thus the scene:</p>
+ <div class="highlight"><pre><span class="nx">remove</span><span class="o">:</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
+ <span class="k">this</span><span class="p">.</span><span class="nx">el</span><span class="p">.</span><span class="nx">removeObject3D</span><span class="p">(</span><span class="s1">'light'</span><span class="p">);</span>
+ <span class="p">}</span>
+ </pre></div>
+
+
+ <h4>Component.tick(time) - Background Behavior</h4>
+ <p>The <code>tick</code> handler is called on every single tick or render loop of the scene.
+ So expect it to run on the order of 60-120 times for second. The global uptime of
+ the scene in seconds is passed into the tick handler.</p>
+ <p>For example, the <a href="https://aframe.io/docs/components/look-at.html">look-at</a> component, which instructs an entity to
+ look at another target entity, uses the tick handler to update the rotation in
+ case the target entity changes its position:</p>
+ <div class="highlight"><pre><span class="nx">tick</span><span class="o">:</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">t</span><span class="p">)</span> <span class="p">{</span>
+ <span class="c1">// target3D and vector are set from the update handler.</span>
+ <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">target3D</span><span class="p">)</span> <span class="p">{</span>
+ <span class="k">this</span><span class="p">.</span><span class="nx">el</span><span class="p">.</span><span class="nx">object3D</span><span class="p">.</span><span class="nx">lookAt</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">vector</span><span class="p">.</span><span class="nx">setFromMatrixPosition</span><span class="p">(</span><span class="nx">target3D</span><span class="p">.</span><span class="nx">matrixWorld</span><span class="p">));</span>
+ <span class="p">}</span>
+ <span class="p">}</span>
+ </pre></div>
+
+
+ <h4>Component.pause() and Component.play() - Stop and Go</h4>
+ <p>To support pause and play, just as with a video game or to toggle entities for
+ performance, components can implement <code>play</code> and <code>pause</code> handlers. These are
+ invoked when the component's entity runs its <code>play</code> or <code>pause</code> method. When an
+ entity plays or pauses, all of its child entities are also played or paused.</p>
+ <p>Components should implement play or pause handlers if they register any
+ dynamic, asynchronous, or background behavior such as animations, event
+ listeners, or tick handlers.</p>
+ <p>For example, the <code>look-controls</code> component simply removes its event listeners
+ such that the camera does not move when the scene is paused, and it adds its
+ event listeners when the scene starts playing or is resumed:</p>
+ <div class="highlight"><pre><span class="nx">pause</span><span class="o">:</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
+ <span class="k">this</span><span class="p">.</span><span class="nx">removeEventListeners</span><span class="p">()</span>
+ <span class="p">},</span>
+
+ <span class="nx">play</span><span class="o">:</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
+ <span class="k">this</span><span class="p">.</span><span class="nx">addEventListeners</span><span class="p">()</span>
+ <span class="p">}</span>
+ </pre></div>
+
+
+ <h3>Boilerplate</h3>
+ <p>I suggest that people start off with my <a href="https://github.com/ngokevin/aframe-component-boilerplate">component boilerplate</a>,
+ even hardcore tool junkies. This will get you straight into building a
+ component and comes with everything you will need to publish your component
+ into the wild. The boilerplate handles creating a stubbed component, build
+ steps for both NPM and browser distribution files, and publishing to Github
+ Pages.</p>
+ <p>Generally with boilerplates, it is better to start from scratch and build your
+ own boilerplate, but the A-Frame component boilerplate contains a lot of tribal
+ inside knowledge about A-Frame and is updated frequently to reflect new things
+ landing on A-Frame. The only possibly opinionated pieces about the boilerplate
+ is the development tools it internally uses that are hidden away by NPM
+ scripts.</p>
+ <h3>Examples</h3>
+ <p>Under construction. Stay tuned!</p>
+ <h4>Text Component</h4>
+ <p><a href="https://github.com/ngokevin/aframe-text-component">Text component</a></p>
+ <h4>Physics Components</h4>
+ <p><a href="https://github.com/ngokevin/aframe-physics-components">Physics components</a></p>
+ <h4>Layout Component</h4>
+ <p><a href="https://github.com/ngokevin/aframe-layout-component">Layout component</a></p></div>
+ </summary>
+ <updated>2016-01-17T00:00:00Z</updated>
+ <source>
+ <id>http://ngokevin.com/rss</id>
+ <author>
+ <name>Kevin Ngo</name>
+ </author>
+ <link href="http://ngokevin.com/rss" rel="self" type="application/rss+xml"/>
+ <link href="http://ngokevin.com/rss" rel="alternate" type="text/html"/>
+ <updated>2016-01-26T18:09:34Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>http://blog.gerv.net/?p=3527</id>
+ <link href="http://feedproxy.google.com/~r/HackingForChrist/~3/DN054t04_dE/" rel="alternate" type="text/html"/>
+ <link href="http://blog.gerv.net/2016/01/convenient-and-creepy/#comments" rel="replies" type="text/html"/>
+ <link href="http://blog.gerv.net/2016/01/convenient-and-creepy/feed/atom/" rel="replies" type="application/atom+xml"/>
+ <title xml:lang="en-US">Convenient… and Creepy</title>
+ <summary type="xhtml" xml:lang="en-US"><div xmlns="http://www.w3.org/1999/xhtml">The last Mozilla All-Hands was at one of the hotels in the Walt Disney World Resort in Florida. Every attendee was issued with one of these (although their use was optional): It’s called a “Magic Bandâ€. You register it online … <a href="http://blog.gerv.net/2016/01/convenient-and-creepy/">Continue reading <span class="meta-nav">→</span></a></div>
+ </summary>
+ <content type="xhtml" xml:lang="en-US"><div xmlns="http://www.w3.org/1999/xhtml"><p>The last Mozilla All-Hands was at one of the hotels in the Walt Disney World Resort in Florida. Every attendee was issued with one of these (although their use was optional):<br/>
+ <a href="http://blog.gerv.net/files/2016/01/Disneys_MagicBand.jpg"><img class="alignnone size-large wp-image-3530" src="http://blog.gerv.net/files/2016/01/Disneys_MagicBand-1024x832.jpg" width="292"/></a></p>
+ <p>It’s called a “Magic Bandâ€. You register it online and connect it to your Disney account, and then it can be used for park entry, entry to pre-booked rides so you don’t have to queue (called “FastPass+â€), payment, picking up photos, as your room key, and all sorts of other convenient features. Note that it has no UI whatsoever – no lights, no buttons. Not even a battery compartment. (It does contain a battery, but it’s not replaceable.) These are specific design decisions – the aim is for ultra-simple convenience.</p>
+ <p>One of the talks we had at the All Hands was from one of the Magic Band team. The audience reactions to some of the things he said was really interesting. He gave the example of Cinderella wishing you a Happy Birthday as you walk round the park. “Cinderella just knowsâ€, he said. Of course, in fact, her costume’s tech prompts her when it silently reads your Magic Band from a distance. This got some initial impressed applause, but it was noticeable that after a few moments, it wavered – people were thinking “Cool… er, but creepy?â€</p>
+ <p>The Magic Band also has range sufficient that Disney can track you around the park. This enables some features which are good for both customers and Disney – for example, they can use it for load balancing. If one area of the park seems to be getting overcrowded, have some characters pop up in a neighbouring area to try and draw people away. But it means that they always know where you are and where you’ve been.</p>
+ <p>My take-away from learning about the Magic Band is that it’s really hard to have a technical solution to this kind of requirement which allows all the Convenient features but not the Creepy features. Disney does offer an RFID-card-based solution for the privacy-conscious which does some of these things, but not all of them. And it’s easier to lose. It seems to me that the only way to distinguish the two types of feature, and get one and not the other, is policy – either the policy of the organization, or external restrictions on them (e.g. from a watchdog body’s code of conduct they sign up to, or from law). And it’s often not in the organization’s interest to limit themselves in this way.</p>
+ <img alt="" height="1" src="http://feeds.feedburner.com/~r/HackingForChrist/~4/DN054t04_dE" width="1"/></div>
+ </content>
+ <updated>2016-01-16T12:18:38Z</updated>
+ <published>2016-01-16T12:18:38Z</published>
+ <category scheme="http://blog.gerv.net" term="Mozilla"/>
+ <category scheme="http://blog.gerv.net" term="Syndicate"/><feedburner:origLink xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0">http://blog.gerv.net/2016/01/convenient-and-creepy/</feedburner:origLink>
+ <author>
+ <name>gerv</name>
+ </author>
+ <source>
+ <id>http://blog.gerv.net/feed/atom/</id>
+ <link href="http://blog.gerv.net" rel="alternate" type="text/html"/>
+ <link href="http://feeds.feedburner.com/HackingForChrist" rel="self" type="application/atom+xml"/>
+ <link href="http://pubsubhubbub.appspot.com/" rel="hub" type="text/html"/>
+ <subtitle xml:lang="en-US">Gervase Markham</subtitle>
+ <title xml:lang="en-US">Syndicate – Hacking for Christ</title>
+ <updated>2016-01-16T12:18:38Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>https://www.christianheilmann.com/?p=4957</id>
+ <link href="https://www.christianheilmann.com/2016/01/16/dont-tell-me-what-my-browser-cant-do/" rel="alternate" type="text/html"/>
+ <title>Don’t tell me what my browser can’t do!</title>
+ <summary>Chances are, your guess is wrong! Arrogance towards possible customers never pays out – as shown in “Pretty Woman†There is nothing more frustrating than being capable of something and not getting a chance to do it. The same goes for being blocked out from something although you are capable of consuming it. Or you’re […]</summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p><em class="markup--em markup--p-em">Chances are, your guess is wrong!</em></p>
+
+ <p/><figure><img alt="you are obviously in the wrong place" src="https://d262ilb51hltx0.cloudfront.net/max/800/1*l9jPbOyAl00kjPhyNYA-IQ.jpeg" width="100%"/>Arrogance towards possible customers never pays out – as shown in “Pretty Womanâ€</figure><p/>
+
+ <p>There is nothing more frustrating than being capable of something and not getting a chance to do it. The same goes for being blocked out from something although you are capable of consuming it. Or you’re even willing to put some extra effort or even money in and you still don’t get to consume it.</p>
+
+ <p>For example, I’d happily pay $50 a month to get access to Netflix’s world-wide library from any country I’m in. But the companies Netflix get their content from won’t go for that. Movies and TV show are budgeted by predicted revenue in different geographical markets with month-long breaks in between the releases. A world-wide network capable of delivering content in real time? Preposterous — let’s shut that down.</p>
+
+ <p>On a less “let’s break a 100 year old monopoly†scale of annoyance, <a href="https://twitter.com/codepo8/status/687616620529844224">I tweeted yesterday something glib and apparently cruel</a>:</p>
+
+ <p/><blockquote>“Sorry, but your browser does not support WebGL!†– sorry, you are a shit coder.</blockquote><p/>
+
+ <p><strong>And I stand by this</strong>. I went to a web site that promised me some cute, pointless animation and technological demo. I was using Firefox Nightly — a WebGL capable browser. I also went there with Microsoft Edge — another WebGL capable browser. Finally, using Chrome, I was able to delight in seeing an animation.</p>
+
+ <p><strong>I’m not saying the creators of that thing lack in development capabilities</strong>. The demo was slick, beautiful and well coded. They still do lack in two things developers of <em>web products </em>(and I count apps into that) should have: empathy for the end user and an understanding that they are not in control.</p>
+
+ <p>Now, I am a pretty capable technical person. When you tell me that I might be lacking WebGL, I know what you mean. I don’t lack WebGL. I was blocked out because the web site did browser sniffing instead of capability testing. But I know what could be the problem.</p>
+
+ <p>A normal user of the web has no idea what WebGL is and — if you’re lucky — will try to find it on an app store. If you’re not lucky all you did is confuse a person. A person who went through the effort to click a link, open a browser and wait for your thing to load. A person that feels stupid for using your product as they have no clue what WebGL is and won’t ask. Humans hate feeling stupid and we do anything not to appear it or show it.</p>
+
+ <p>This is what I mean by empathy for the end user. Our problems should never become theirs.</p>
+
+ <p/><blockquote>A cryptic error message telling the user that they lack some technology helps nobody and is sloppy development at best, sheer arrogance at worst.</blockquote><p/>
+
+ <p>The web is, sadly enough, littered with unhelpful error messages and assumptions that it is the user’s fault when they can’t consume the thing we built.</p>
+
+ <p>Here’s a reality check — this is what our users should have to do to consume the things we build:</p>
+
+ <p><img alt="" height="600" src="https://d262ilb51hltx0.cloudfront.net/max/800/1*DXtRIWTu-UzRb0YB-h8SmA.png" width="10"/></p>
+
+ <p><strong>That’s right. Nothing</strong>. This is the web. Everybody is invited to consume, contribute and create. This is what made it the success it is. This is what will make it outlive whatever other platform threatens it with shiny impressive interactions. Interactions at that time impossible to achieve with web technologies.</p>
+
+ <p>Whenever I mention this, the knee-jerk reaction is the same:</p>
+
+ <p/><blockquote class="graf--blockquote graf-after--p" id="79d6" name="79d6">How can you expect us to build delightful experiences close to magic (and whatever other soundbites were in the last Apple keynote) if we keep having to support old browsers and users with terrible setups?</blockquote><p/>
+
+ <p>You don’t have to support old browsers and terrible setups. But you are not allowed to block them out. It is a simple matter of giving a usable interface to end users. A button that does nothing when you click it is not a good experience. Test if the functionality is available, then create or show the button. <strong class="markup--strong markup--p-strong">This is as simple as it is.</strong></p>
+
+ <p>If you really have to rely on some technology then show people what they are missing out on and tell them how to upgrade. A screenshot or a video of a WebGL animation is still lovely to see. A message telling me I have no WebGL less so.</p>
+
+ <p>Even more on the black and white scale, what the discussion boils down to is in essence:</p>
+
+ <p/><blockquote class="graf--blockquote graf-after--p" id="a775" name="a775">But it is 2016 — surely we can expect people to have JavaScript enabled — it is after all “the assembly language of the webâ€</blockquote><p/>
+
+ <p>Despite the cringe-worthy <a href="http://www.hanselman.com/blog/JavaScriptIsAssemblyLanguageForTheWebSematicMarkupIsDeadCleanVsMachinecodedHTML.aspx">misquote of the assembly language</a> thing, here is a harsh truth:</p>
+
+ <p/><blockquote>You can absolutely expect JavaScript to be available on your end users computers in 2016. At the same time it is painfully <strong>naive</strong> to expect it to work under all circumstances.</blockquote><p/>
+
+ <p><strong>JavaScript is brittle</strong>. <span class="caps">HTML</span> and <span class="caps">CSS</span> both are <em>fault tolerant</em>. If something goes wrong in <span class="caps">HTML</span>, browsers either display the content of the element or try to fix minor issues like unclosed elements for you. <span class="caps">CSS</span> skips lines of code it can’t understand and merrily goes on its way to show the rest of it. JavaScript breaks on errors and tells you that something went wrong. It will not execute the rest of the script, but throws in the towel and tells you to get your house in order first.</p>
+
+ <p>There <a href="http://kryogenix.org/code/browser/everyonehasjs.html">are many outside influences</a> that will interfere with the execution of your JavaScript. That’s why a non-naive and non-arrogant — a dedicated and seasoned web developer — will never rely on it. Instead, you treat it as an enhancement and in an almost paranoid fashion test for the availability of everything before you access it.</p>
+
+ <p><strong>Sorry (not sorry) — this will never go away</strong>. This is the nature of JavaScript. And it is a good thing. It means we can access new features of the language as they come along instead of getting stuck in a certain state. It means we have to think about using it every time instead of relying on libraries to do the work for us. It means that we need to keep evolving with the web — a living and constantly changing medium, and not a software platform. That’s just part of it.</p>
+
+ <p>This is why the whole discussion about JavaScript enabled or disabled is a massive waste of time. It is not the availability of JavaScript we need to worry about. It is our products breaking in perfectly capable environments because we rely on perfect execution instead of writing defensive code. A tumblr like <a class="markup--anchor markup--p-anchor" href="http://sighjavascript.tumblr.com/" rel="nofollow">Sigh, JavaScript</a> is fun, but is pithy finger-pointing.</p>
+
+ <p/><blockquote>There is nothing wrong with using JavaScript to build things. Just be aware that the error handling is your responsibility.</blockquote><p/>
+
+ <p>Any message telling the user that they have to turn on JavaScript to use a certain product is a proof that you care more about your developer convenience than your users.</p>
+
+ <p/><blockquote>It is damn hard these days to turn off JavaScript – you are complaining about a almost non-existent issue and tell the confused user to do something they don’t know how to.</blockquote><p/>
+
+ <p>The chance that something in the JavaScript execution of any of your dozens of dependencies went wrong is much higher – and this is your job to fix. This is why advice like <a href="http://webdesign.tutsplus.com/tutorials/quick-tip-dont-forget-the-noscript-element--cms-25498">using noscript to provide alternative content</a> is terrible. It means you double your workload instead of enhancing what works. Who knows? If you start with something not JavaScript dependent (or running it server side) you might find that you don’t need the complex solution you started with in the first place. Faster, smaller, easier. Sounds good, right?</p>
+
+ <p>So, please, stop sniffing my browser, you will fail and tell me lies. Stop pretending that working with a brittle technology is the user’s fault when something goes wrong.</p>
+
+ <p/><blockquote>As web developers we work in the service industry. We deliver products to people. And keeping these people happy and non-worried is our job. Nothing more, nothing less.</blockquote><p/>
+
+ <p>Without users, your product is nothing. Sure, we are better paid and well educated and we are not flipping burgers. But we have no right whatsoever to be arrogant and not understanding that our mistakes are not the fault of our end users.</p>
+
+ <p>Our demeanor when complaining about how stupid our end users and their terrible setups are reminds me of <a href="https://www.youtube.com/watch?v=CSj5stmFkQ0">this Mitchell and Webb sketch</a>.</p>
+
+ <p/>
+
+ <p><strong class="markup--strong markup--p-strong">Don’t be that person. </strong>Our job is to enable people to consume, participate and create the web. This is magic. This is beautiful. This is incredibly rewarding. The next markets we should care about are ready to be as excited about the web as we were when we first encountered it. Browsers are good these days. Use what they offer after testing for it and enjoy what you can achieve. Don’t tell the user when things go wrong – they can not fix what you messed up.</p>
+
+
+ <img alt="" height="1" src="http://feeds.feedburner.com/~r/chrisheilmann/~4/vqtqgcNQXy8" width="1"/></div>
+ </content>
+ <updated>2016-01-16T11:28:10Z</updated>
+ <category term="General"/>
+ <author>
+ <name>Chris Heilmann</name>
+ </author>
+ <source>
+ <id>https://www.christianheilmann.com</id>
+ <link href="https://www.christianheilmann.com" rel="alternate" type="text/html"/>
+ <link href="http://feeds.feedburner.com/chrisheilmann" rel="self" type="application/rss+xml"/>
+ <link href="http://pubsubhubbub.appspot.com/" rel="hub" type="text/html"/>
+ <subtitle>For a better web with more professional jobs - can talk, will travel</subtitle>
+ <title>Christian Heilmann</title>
+ <updated>2016-01-16T11:46:15Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>http://glandium.org/blog/?p=3510</id>
+ <link href="http://glandium.org/blog/?p=3510" rel="alternate" type="text/html"/>
+ <title>Announcing git-cinnabar 0.3.1</title>
+ <summary>This is a brown paper bag release. It turns out I managed to break the upgrade path only 10 commits before the release. What’s new since 0.3.0? git cinnabar fsck doesn’t fail to upgrade metadata. The remote.$remote.cinnabar-draft config works again. Don’t fail to clone an empty repository. Allow to specify mercurial configuration items in a […]</summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>This is a brown paper bag release. It turns out I managed to break the upgrade<br/>
+ path only 10 commits before the release.</p>
+ <h3>What’s new since 0.3.0?</h3>
+ <ul>
+ <li><code>git cinnabar fsck</code> doesn’t fail to upgrade metadata.</li>
+ <li>The <code>remote.$remote.cinnabar-draft</code> config works again.</li>
+ <li>Don’t fail to clone an empty repository.</li>
+ <li>Allow to specify mercurial configuration items in a .git/hgrc file.</li>
+ </ul></div>
+ </content>
+ <updated>2016-01-16T11:26:45Z</updated>
+ <category term="cinnabar"/>
+ <category term="p.m.o"/>
+ <category term="en"/>
+ <author>
+ <name>glandium</name>
+ </author>
+ <source>
+ <id>http://glandium.org/blog</id>
+ <link href="http://glandium.org/blog/?feed=rss2&amp;cat=25&amp;tag=en" rel="self" type="application/rss+xml"/>
+ <link href="http://glandium.org/blog" rel="alternate" type="text/html"/>
+ <subtitle>glandium.org</subtitle>
+ <title>p.m.o – glandium.org</title>
+ <updated>2016-01-16T11:30:43Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-us">
+ <id>http://edunham.net/2016/01/16/buildbot_and_eoferror.html</id>
+ <link href="http://edunham.net/2016/01/16/buildbot_and_eoferror.html" rel="alternate" type="text/html"/>
+ <title>Buildbot and EOFError</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><h3>Buildbot and EOFError</h3>
+ <p>More SEO-bait, after tracking down an poorly documented problem:</p>
+ <div class="highlight-python"><div class="highlight"><pre># buildbot start master
+ Following twistd.log until startup finished..
+ 2016-01-17 04:35:49+0000 [-] Log opened.
+ 2016-01-17 04:35:49+0000 [-] twistd 14.0.2 (/usr/bin/python 2.7.6) starting up.
+ 2016-01-17 04:35:49+0000 [-] reactor class: twisted.internet.epollreactor.EPollReactor.
+ 2016-01-17 04:35:49+0000 [-] Starting BuildMaster -- buildbot.version: 0.8.12
+ 2016-01-17 04:35:49+0000 [-] Loading configuration from '/home/user/buildbot/master/master.cfg'
+ 2016-01-17 04:35:53+0000 [-] error while parsing config file:
+ Traceback (most recent call last):
+ File "/usr/local/lib/python2.7/dist-packages/twisted/internet/defer.py", line 577, in _runCallbacks
+ current.result = callback(current.result, *args, **kw)
+ File "/usr/local/lib/python2.7/dist-packages/twisted/internet/defer.py", line 1155, in gotResult
+ _inlineCallbacks(r, g, deferred)
+ File "/usr/local/lib/python2.7/dist-packages/twisted/internet/defer.py", line 1099, in _inlineCallbacks
+ result = g.send(result)
+ File "/usr/local/lib/python2.7/dist-packages/buildbot/master.py", line 189, in startService
+ self.configFileName)
+ --- &lt;exception caught here&gt; ---
+ File "/usr/local/lib/python2.7/dist-packages/buildbot/config.py", line 156, in loadConfig
+ exec f in localDict
+ File "/home/user/buildbot/master/master.cfg", line 415, in &lt;module&gt;
+ extra_post_params={'secret': HOMU_BUILDBOT_SECRET},
+ File "/usr/local/lib/python2.7/dist-packages/buildbot/status/status_push.py", line 404, in __init__
+ secondaryQueue=DiskQueue(path, maxItems=maxDiskItems))
+ File "/usr/local/lib/python2.7/dist-packages/buildbot/status/persistent_queue.py", line 286, in __init__
+ self.secondaryQueue.popChunk(self.primaryQueue.maxItems()))
+ File "/usr/local/lib/python2.7/dist-packages/buildbot/status/persistent_queue.py", line 208, in popChunk
+ ret.append(self.unpickleFn(ReadFile(path)))
+ exceptions.EOFError:
+
+ 2016-01-17 04:35:53+0000 [-] Configuration Errors:
+ 2016-01-17 04:35:53+0000 [-] error while parsing config file: (traceback in logfile)
+ 2016-01-17 04:35:53+0000 [-] Halting master.
+ 2016-01-17 04:35:53+0000 [-] Main loop terminated.
+ 2016-01-17 04:35:53+0000 [-] Server Shut Down.
+ </pre></div>
+ </div>
+ <p>This happened after the buildmaster’s disk filled up and a bunch of stuff was
+ manually deleted. There were no changes to master.cfg since it worked
+ perfectly.</p>
+ <p>The fix was to examine <span class="docutils literal"><span class="pre">master.cfg</span></span> to see <a class="reference external" href="https://github.com/servo/saltfs/blob/master/buildbot/master/master.cfg#L413">where the HttpStatusPush was
+ created</a>,
+ of the form:</p>
+ <div class="highlight-python"><div class="highlight"><pre><span class="n">c</span><span class="p">[</span><span class="s">'status'</span><span class="p">]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">HttpStatusPush</span><span class="p">(</span>
+ <span class="n">serverUrl</span><span class="o">=</span><span class="s">'http://build.servo.org:54856/buildbot'</span><span class="p">,</span>
+ <span class="n">extra_post_params</span><span class="o">=</span><span class="p">{</span><span class="s">'secret'</span><span class="p">:</span> <span class="n">HOMU_BUILDBOT_SECRET</span><span class="p">},</span>
+ <span class="p">))</span>
+ </pre></div>
+ </div>
+ <p>Digging in the Buildbot source reveals that <span class="docutils literal"><span class="pre">persistent_queue.py</span></span> wants to
+ unpickle a cache file from <span class="docutils literal"><span class="pre">/events_build.servo.org/-1</span></span> if there was nothing
+ in <span class="docutils literal"><span class="pre">/events_build.servo.org/</span></span>. To fix this the right way, create that file
+ and make sure Buildbot has <span class="docutils literal"><span class="pre">+rwx</span></span> on it.</p>
+ <p>Alternately, you can give up on writing your status push cache to disk
+ entirely by adding the line <span class="docutils literal"><span class="pre">maxDiskItems=0</span></span> to the creation of the
+ HttpStatusPush, giving you:</p>
+ <div class="highlight-python"><div class="highlight"><pre><span class="n">c</span><span class="p">[</span><span class="s">'status'</span><span class="p">]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">HttpStatusPush</span><span class="p">(</span>
+ <span class="n">serverUrl</span><span class="o">=</span><span class="s">'http://build.servo.org:54856/buildbot'</span><span class="p">,</span>
+ <span class="n">maxDiskItems</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span>
+ <span class="n">extra_post_params</span><span class="o">=</span><span class="p">{</span><span class="s">'secret'</span><span class="p">:</span> <span class="n">HOMU_BUILDBOT_SECRET</span><span class="p">},</span>
+ <span class="p">))</span>
+ </pre></div>
+ </div>
+ <p>The real moral of the story is “remember to use <a class="reference external" href="http://www.linuxcommand.org/man_pages/logrotate8.html">logrotate</a>.</p></div>
+ </summary>
+ <updated>2016-01-16T08:00:00Z</updated>
+ <source>
+ <id>http://edunham.net/</id>
+ <author>
+ <name>Emily Dunham</name>
+ </author>
+ <link href="http://edunham.net/" rel="alternate" type="text/html"/>
+ <link href="http://edunham.net/rss.html?tag=planetmozilla" rel="self" type="application/rss+xml"/>
+ <subtitle>is a "DevOps" Engineer at Mozilla Research</subtitle>
+ <title>edunham</title>
+ <updated>2016-01-19T08:00:00Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en">
+ <id>urn:md5:41d039bb28fb15c761578cba0b1454fa</id>
+ <link href="http://www.glazman.org/weblog/dotclear/index.php?post/2016/01/16/Ebook-pagination-and-CSS" rel="alternate" type="text/html"/>
+ <title>Ebook pagination and CSS</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>Let's suppose you have a rather long document, for instance a book chapter, and you want to render it in your browser <em>à la</em> iBooks/Kindle. That's rather easy with just a dash of CSS:</p>
+ <pre>body {
+ height: calc(100vh - 24px);
+ column-width: 45vw;
+ overflow: hidden;
+ margin-left: calc(-50vw * attr(currentpage integer));
+ }</pre>
+ <p>Yes, yes, I know that no browser implements that <code>attr()</code>extended syntax. So put an inline style on your body for <code>margin-left: calc(-50vw * <em>&lt;n&gt;</em>)</code> where <em><code>&lt;n&gt;</code></em> is the page number you want minus 1.</p>
+ <p>Then add the fixed positioned controls you need to let user change page, plus gesture detection. Add a transition on margin-left to make it nicer. Done. Works perfectly in Firefox, Safari, Chrome and Opera. I don't have a Windows box handy so I can't test on Edge.</p></div>
+ </summary>
+ <updated>2016-01-16T03:43:00Z</updated>
+ <category term="CSS and style"/>
+ <author>
+ <name>glazou</name>
+ </author>
+ <source>
+ <id>http://www.glazman.org/weblog/dotclear/index.php</id>
+ <link href="http://www.glazman.org/weblog/dotclear/index.php" rel="alternate" type="text/html"/>
+ <link href="http://glazman.org/weblog/dotclear/?feed/planetmoz" rel="self" type="application/rss+xml"/>
+ <subtitle>Un Glazman, un blog, un Glazblog</subtitle>
+ <title>&lt;Glazblog/&gt;</title>
+ <updated>2016-01-25T16:34:47Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="fr-FR">
+ <id>https://repeer.org/?p=48</id>
+ <link href="https://repeer.org/2016/01/16/mozilla-cultural-revolution-from-radical-participation-to-radical-user-centric/" rel="alternate" type="text/html"/>
+ <title>Mozilla cultural revolution: from ‘radical participation’ to ‘radical user-centric’</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">This post has been written about the Mozilla Foundation (MoFo) 2020 strategy. The ideas developed in this post are in different levels: some are global, some focus on particular points of the proposed draft. But in my point of view, they all carry a transversal meaning: articulation (as piece connected to a structure allowing movement) <a class="read-more" href="https://repeer.org/2016/01/16/mozilla-cultural-revolution-from-radical-participation-to-radical-user-centric/">[…]</a></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>This post has been written about the <a href="http://marksurman.commons.ca/2015/12/21/mofo2020/">Mozilla Foundation (MoFo) 2020 strategy</a>.</p>
+ <p>The ideas developed in this post are in different levels: some are global, some focus on particular points of the proposed draft. But in my point of view, they all carry a transversal meaning: articulation (as piece connected to a structure allowing movement) with others and consistency with our mission.</p>
+ <h3>Summary</h3>
+ <p>On the way to <a href="http://marksurman.commons.ca/2015/01/09/what-is-radical-participation/">radical participation</a>, Mozilla should be radical <sup class="footnote"><a href="https://repeer.org/tag/mozilla/feed/#fn-48-1" id="fnref-48-1">1</a></sup> user-centric. Mozilla should not go against the social understanding of the (tech and whole society) situation because it’s what is massively shared and what polarizes the prism of understanding of the society. <strong>We should built solutions for it and transform (develop and change) it on the way. Our responsibility is to build <em>inclusivity</em> (inclusion strengths) everywhere, to gather for multiplying our impact.</strong> We must build (progressive) victories instead of battles (of static positions and postures).<br/>
+ If we don’t do it, we go against users self-perceived need: use. We value our differences more than our commonalities and <strong>consider ethic more as an absolute objective than a concrete process</strong>: we divide, separate, compete. Our solutions get irrelevant, we get rejected and marginalized, we reject compromises that improve the current situation for the ideal, we loose influence and therefore impact on the definition of the present and future. We already done it for the good and the bad in the past (H.264+Daala, pocket integration, Hello login, no Firefox for iOS, Google fishing vs Disconnect, FxOS Notes app which sync is evernote only, …).<br/>
+ To get a consistent and impactful ability to integrate and transform the social understanding, there are four domains where we can take and articulate (connected structure allowing movement) action:</p>
+ <ul>
+ <li><strong>People</strong>: identity is the key to grow consciousness, understanding, skills, voice, representation and to articulate global/local, personal/common. <strong>[Activate]</strong></li>
+ <li><strong>Technology</strong>: universality is key for a platform (for resilience) with interfaces (for modularity) where services, features and front-ends can plug-in and communicate to provide (inter)active support ; Decouple conditions of fulfillment with execution (content/appearance/policy ; material/immaterial) to support remix (policy continuity, consistency thought providers, …). <strong>[Unlock]</strong></li>
+ <li><strong>Product</strong>: persona and (current and emerging) use via user-agents are the keys. Be on all major platforms depending on use, ethical alignment and opportunities, emerging newness to provide continuity (task, device) to users and leading on new practices. Features should be about products parity and opening new possibilities carrying our values to the action at a massive scale. <strong>[Build]</strong></li>
+ <li><strong>Organizations/institutions</strong>: sociological innovation for participation is the key. Research on historical (evolution) and sociological (human organizations, social institutions and social behaviors) analysis based on social networks (link as social interactions), in the perspective of producing commons. <strong>[Drive]</strong></li>
+ </ul>
+ <p>Our front has two sides: <strong>propose and protect</strong>. But each of them are connected and can have different strategic expressions, if our actions generate improving (progressive) curves:</p>
+ <ul>
+ <li>For the <strong>action taking</strong>: consciousness, understanding, symbolic actions, behavior change, behavior advocacy (evangelism)</li>
+ <li>For the <strong>action mode</strong>: promotion (spreading the idea), incitement (giving a competitive advantage to people involved), collaboration (open interactions to make a win-win exchange; process-centric), contractualization (formalize domains where a win-win exchange is made; object-centric), coercion (giving a competitive disadvantage to people not involved).</li>
+ </ul>
+ <p>Social history is a history of social values.<strong> The way we understand and tell the problem determine the solution we can create</strong>: we need, all the way long, a shared understanding. Tools and technologies are not tied, bound forever to their social value, which depends on people’s social representations that evolve over time.</p>
+ <ul>
+ <li><strong>The social behavior</strong> is a first key. It is the narrative, and therefore its <strong>inclusion in the social history that we make, which converges the product with the values that it stands for</strong>. Here is the articulation of product with people and technology, of product with leadership network and advocacy engine (it could be less persistent and inclusive: marketing).</li>
+ <li><strong>The social organization</strong> is a second key. It is about how the process, the tools, the architecture, the governance and the opportunities/constraints have changed for Mozilla (org) and Mozillians (people). <strong>Here comes the question of being open</strong>. It is not enough because it is about availability (passive) and not inclusivity (active). The high level of automation coming is a challenge. We should level-up the meaning to differentiate from others: <strong>Mozilla should activate and unlock societal progress to build fair technical progress</strong>. Mozilla need to <strong>identify its resilient backbone</strong> (not only a technology, the web, but something that articulate people, technology and products) and make it more universal (through people and products). But our goals can’t be absolutely achieved because they have to be considered in a dynamic context. However, the brand engagement is persistent, if it’s included in the product, visible, and centered on easing the user’s action.<br/>
+ Linked to the ‘being open’ question, the advocacy engine could be a thing to unlock societal progress. People are satisfied of narrow hills of choice until they understand it’s not socially neutral. It’s the case with technology: they accept things about technology to be build top-down. <strong>A successful advocacy, even one about technology, is always built bottom-up</strong>, as its function is to give back the voice to the people, to get them involved, not to make them fulfill our predefined aims. The top-down method is too organization centric and administrative content centric: it can’t massively drive people that are not already committed to the org. It’s usually named advertisement or propaganda. <strong>If we want to have an impact, we should listen to people needs, not tell them to listen to ours</strong>. People want (first) to be empowered, not to empower an org. We need to have content and user centric (not org and it’s process) tools/platform for advocates and leaders: let’s build the technology advocacy plan together. Yes it’s slower, but much more massive, inclusive and persistent. The impact will be higher because it will carry a meaning for people and it wont be too org centric. So it will be qualitatively better: not just an amount, <strong>accumulation is not our goal, but impact, that comes from articulation</strong>. Likewise we should be careful to not use best practice as absolute solutions, but as solutions in a context, if we want to transpose them massively: when we unify we should avoid to homogenize. On the narrative side, our preoccupation should be about building short, medium and long term narrative to get action.</li>
+ <li><strong>The social institutions</strong> are the third key. Here is the articulation of the leadership network with the advocacy engine. <strong>Leaders build new solutions (products) and Advocates new voices (rallying), they are both about personal development and empower commons.</strong> Leadership=learn+create and advocacy=teach+spread commons. Leaders are projects/orgs leaders, the ones that traduce DNA (values) in products (concrete ability and availability). Advocates are values advocates, the ones that traduce DNA (values) in actions (behavior). As they are both targeting commons, they both produce the same social organization (collaboration instead of competition). They are both involved in creating (different) representations (institutions) and organizations (foundation/firms) but <strong>with a different DNA (values) processing</strong>: from public good to personal benefit or from personal interest to public benefit. If Mozilla cares about public good resilience, <strong>the articulation of their domains of values is critical</strong>. So, on the social organization side, their articulation’s expression and the revision process must be said and clear: from hierarchy or contract or different autonomy levels (internal incubation and external advocacy), or … to criteria to start a revision. About the narrative, and hence about the social behavior side, leaders carry a lot of legitimacy and avoid the stay-experimental or non-massive (unique) thoughts. And we need legitimacy to get impact. But this legitimacy is already present if we<strong> make clear that our actions are about commons</strong>. We should name them creators (compositors or managers) to make it clear that the creative process is a collaboration, made by a team and that the public good do not have the same role in the process and outcome. Full circle.</li>
+ <li><strong>The social networks</strong> are the keystone. Let’s shortly take an example based on social networks (link as social interactions) with the perspective of producing people, technological and product commons. <strong>We need better tools for collaboration and participation</strong>: tools that merge discussion channels, capitalize on the discussion and preview the results to build a plan. From evolving the wiki discussion page to feature document production into peer-to-peer discussion.</li>
+ </ul>
+ <p>An analysis of the creation process is another way to the articulation of product with people and technology.<br/>
+ Platforms move closer to strict ‘walled garden’ ecosystems. We need bridges from lab to home that carry different mix of customization and reliability to support the emancipation curve. We need to build pathways thought audiences and thought IT layers (content, software, hardware, distant service). <strong>We should find a convergence between customization</strong> (dev code patch to users add-ons) <strong>and reliability</strong> (self made to mass product), <strong>between first time experience, support and add-ons thought all our users’ persona by building bridges, pathways</strong>. Mozilla should find ways to <strong>integrate learning</strong> in its products, in-content, as we have code comment on code: on-boarding levels, progression from simple to high level techniques, reproducible/universal next task/skill building.</p>
+ <h3>Detailed discussion content</h3>
+ <p>Here are the developed ideas, with more reference to our allies and detractors’ products.</p>
+ <h4>People, the sociological side</h4>
+ <h5>From focused to systemic action</h5>
+ <p>First of all, I think <strong>the strategy move Mozilla is doing is the right one</strong> as it embraces more our real life. People are not defined by one characteristic, we are complex: ex. we can be pedestrian, car driver, biker, Public Transport user… we think and do simultaneously. So why Mozilla should restrict its strategy by targeting people on skills, through education, thought better material only (the Mozilla Academy program). Education, even popular education, can’t do everything for the people to build change. <strong>We need a plan that balance intellectual and practical (abstraction/action, think/do) integrating progressive paths to massively scale so we get an impact: build change.</strong></p>
+ <h5>Real life: Social history, individuals and institutions as an articulation founding the action.</h5>
+ <p>Let’s start by some definitions based on my understanding of some <a href="https://fr.wikipedia.org/wiki/Sociologie">Wikipedia articles</a>. Sociology is the study of the evolution of societies: human organizations and social institutions. It is about <strong>the impact of the social dimension on humans representations (ways of thinking) and behaviors (ways of acting)</strong>. It allows to study the conceptions of social relations according to fundamental criteria (structuralism, functionalism, conventionalism, etc.) and the hooks to reality (interactionism, institutionalism, regulationisme, actionism, etc.), to think and shape the modernity. Currently (and this is key for Mozilla’s positioning), the combination of models replace the models’ unity, which aims to assume the multidimensionality. There are three major sociological paradigms, including one emerging:</p>
+ <ul>
+ <li><strong>The holistic paradigm</strong>: Society is a whole that is greater than the sum of its parts, it exists before the individual and individuals are governed by it. In this context, the Society includes the individual and the individual consciousness is seen only as a fragment of the collective consciousness. The emphasis is on the social fact, whose cause must be sought in earlier social facts. The social fact is part of a system of interlocking institutions that govern individuals. It is external to the individual and constraint it. Sociology is then the science of institutional invariants in which are the observable phenomenas.</li>
+ <li><strong>The atomistic paradigm</strong>: each individual is a social atom. The atoms act according to self motives, interests, emotions and are linked to other atoms. A system of constant interaction between atoms produces and reproduces Society. The emphasis is on the cause of social actions and the meaning given by individuals to their actions. A horizon of meanings serve as reference instead of the arrangements of institutions. The institution is there but it serves the motives and interests of agents. Sociology is then the study of the social action.</li>
+ <li>The recent emergence of a sociological analysis based on <strong>social networks</strong> (which are a collection of individuals or organizations connected by regular social interactions) suggest lines of research <strong>beyond the opposition between the holistic and the atomistic approaches</strong>. The theory of social networks conceives social relationships in terms of nodes and links. The nodes are usually social actors in the network but can also represent institutions, and links are the relationships between these nodes. There may be several kinds of links between nodes and their analysis determines social capital of the social actors.</li>
+ </ul>
+ <p>Consequently, Mozilla should build its strategy on <strong>historical</strong> (evolution) and <strong>sociological</strong> (human organizations, social institutions and social behaviors) analysis based on <strong>social networks</strong> (links as social interactions), in the perspective of producing <strong>commons</strong>. That is to say as an <strong>engine of transition from a model of value</strong> on its last leg (rarity capitalism) to the emerging one (new articulation of the individual and the collective: commons).<br/>
+ It is important and strategic to propose a sociological articulation supporting our mission and its purpose (commons) since <strong>the sociological concept (the paradigm) reveals an ideological characteristic</strong>: because it participates in societal movements made in the Society, it serves an ideal. The societal domain, what’s making society, a political object, should be a stake for Mozilla.</p>
+ <h5>Build on a basement: current tech challenge articulated with current social meaning/perception</h5>
+ <p><strong>We should articulate ‘our real life’ with the nowadays tech challenge</strong>: how to get back control over our data at the time of IoT, cloud, big data, convergence (multi-devices/form factor)? From a user point of view, we have devices and want them convenient, easy and nice. The big moves in the tech industry (IoT, cloud, big data, convergence) free us for somethings and lock us for others. The lock key is that our devices don’t compute anymore our data that are in silos. From a developer point of view, the innovation is going very fast and it’s hard to have a complete open source toolbox that we can share, mostly because we don’t lead: Open has turn to be more open-releasing.<br/>
+ We should articulate our new strategy with the tech industry moves: for example, <strong>as a user, how can I get (email) encryption on all my devices?</strong> Should I follow (fragmented) different kind of howtos/tools/apps to achieve that? How do I know these are consistent together? How can I be sure it won’t brake my continuous workflow? (app silo? social silo? level of trust and reliability?)<br/>
+ Mozilla have the skills to answer this as we already faced and solved some of these issues on particular points: like how to ease the installation of Firefox for Android for Firefox desktop users, open and discoverable choice of search engines, synchronization across devices, …<br/>
+ <strong>Mozilla’s challenge is to not be marginalized by the change of practices. Having an impact is embracing the new practice and give it an alternative.</strong> Mozilla already made that move by saying « <em>Firefox will go where users are</em>« , by trying to balance the advertisement practice between adds companies and users, by integrating H.264 and developing Daala. But <strong>Mozilla never stated that clearly as a strategy</strong>.</p>
+ <h5>A backbone to make our mission resilient in it expressions</h5>
+ <p>If we think about the <strong>Facebook’s strategy, they first built a network of people whiling to share</strong> (no matter what they share) and then use this <strong>transversal backbone to power vertical business segments</strong> (search, donation, local market selling, …). Google with its search engine and its open source policy have a similar (in a way) strategy. The difference here is that the backbone is people’s data and control over digital formats. In both cases, the level of use (of the social network, search engine, mobile OS, …) is the key (with fast innovation) to have an impact. And that’s a major obstacle to build successful alternatives.<br/>
+ The proposed Mozilla’s strategy is built in the opposite way, and that’s questioning. <strong>We try to build people network depending on some shared matters</strong>. Then, is our strategy able to scale enough to compete against GAFAM, or are we trying to build a third way ?<br/>
+ For the products, the Mozilla’s strategy is still (and has always been) inclusive: everybody can use the product and then benefit of its open web values. A good product that answer people needs, plus giving people back/new power (allow new use) build a big community. For the network, should we build our global force of people based on concentric circles (of shared matters) or based on a (Mozilla own) transversal backbone (matter agnostic)? It seems to me the actual presentation of the strategy do not answer clearly enough this big question: <strong>which <em>inclusivity</em> (inclusion strengths) mechanism in the strategy?</strong><br/>
+ And that <strong>call back to our product strategy</strong>: build a community that shares values, that is used to spread outcomes (product) OR build a community that shares a product, that is used to spread values. This is not a question on what matters more (product VS values) but on the strategy to get to a point, an objective (many web citizens). Shouldn’t we use our product to built a people network backbone ? Back to GAFAM: what can we learn from the Google try with Google+?<br/>
+ If our core is not enough transversal (the backbone), more new web/tech market there will be, more we will be marginalized, because focused on our circles center not taking in account that the war front (the context) have changed. <strong>Mozilla have to be resilient: mutability of the means, stability in the objectives.</strong><br/>
+ The document is the MoFo strategy, and so it doesn’t say anything about ‘build Firefox’ (aka the product strategy) and so don’t articulate our main product (Firefox) with our main people network building effort and values sharing engine. We should do it: at a strategic scale and a particular scale (articulating the agenda-setting with main product features).</p>
+ <h5>Brand engagement, a psychological backbone on the user side ?</h5>
+ <p>It seems that our GAFAM challengers get big and have impact by not educating (that much) people, and that’s what makes them not involved in the web citizenship. Or only when they are pushed by their customers. At the opposite, making people aware about web citizenship at first, makes it hard to have that much people involved and so to have impact. However, there is <strong>an other prism that drive people: the brand perceived values</strong>. Google is seen as a tech pioneer innovator and doing the good because of its open policy, free model, fast innovation… Facebook is seen as really cool firm trying to help people by connecting them…<br/>
+ Is the increase of marketing of Mozilla doing good enough to gains back users ? Is this resilient compared to the next-tech-thing coming ?<br/>
+ Most of the time when I meet Goggle Chrome users and ask then why they use it and don’t switch to Firefox, they answer about use allowed (sync thought devices, apps everywhere that run only on GC, …). Sometimes, they argue that they make effort on other areas, and that they want to keep they digital life simple. They <strong>experience is not centered in a product/brand, but more on the person</strong>: on that Google Chrome with its Person (with one click ‘auto-login’ to all Google services) is far superior than Firefox.</p>
+ <h5>User-agent or products ?</h5>
+ <p>A user-agent is an intermediary acting on behalf of a supplier. As a representative, it is the contact point with customers; It’s role is to manage, to administer the affairs; it is entrusted with a mission by one or more persons; it both acts and produce an effect.<br/>
+ So, the user-agent can be describe with three criteria. It is: an intermediate (user/technology) ; a tool (used to manage and administrate depending on the user’s skills) ; a representative (mission bearer, values vector, for a group of people). It exceeds partly the contradiction between being active and passive.<br/>
+ A <strong>user-agent articulate personal-identity with technology-identity</strong> and give informations about available skills over these domains. It’s much more universal than a product that is about featuring a user-agent. <strong>If we target resilience, user-agent should be the target</strong>.</p>
+ <h4>Social history, marketing: how we understand things to make choices</h4>
+ <h5>History of the social value</h5>
+ <p>The way we look at the past and current facts shape our understanding and determine if we open new ways to solve the issues identified. That’s the way to understand the challenges that come on the way and to agree on an adaptation of the strategy instead of splitting things. The way we understand and tell the problem determine the solution we can create: we need, all the way long, <strong>a shared understanding.</strong><br/>
+ <strong>Tools and technologies are not necessarily tied to their social value, which depends on social representations. The social value can be built upstream and evolve downstream.</strong> It also depends on the perspective in which we look at it, on the understanding of the action and therefore on past or current history. Example: the social value of a weapon can be a potential danger or defense, creative (liberating) or destructive. The nuclear bomb is a weapon of mass destruction (negative), whose social value was (ingeniously built as) freedom (positive).</p>
+ <h5>Impact in our strategy: a missing root</h5>
+ <p>To engage the public, before to « <em>Focus on creative campaigns that use media + software to engage the public.</em> » we need to step back, in our speeding world, for understanding together the big picture and the big movement.<br/>
+ Mozilla want to fuel a movement and propose a strong and consistent strategy. However, I think <strong>this plan miss a key point, a root point: build a common (hi)story.</strong> This should be an objective, not just an action.<br/>
+ Also, that’s maybe a missing root for the State of the web report: how do we understand what we want to evaluate? But it’s not only a missing root for an (annual?) report (a ‘Reporters without borders’ Press-Freedom like?), it’s a missing root for a new grow of our products’ market share.<br/>
+ For example, I do think that most users don’t know and understand that Mozilla is a foundation, Firefox build by a community as a product to keep the web healthy: <strong>they don’t imagine any meaning about technology</strong>, because they see it as a neutral tool at its root, so as a tool that should just fit they producing needs.<br/>
+ Firefox, its technologies and its features are not bound for ever. It is the narrative, and therefore their inclusion in the social history that we make, which converges Firefox with the values that it stand for. <strong>Stoping or changing the deep narrative means cutting the source of common understanding and making stronger other consistencies captured by other objects, turning as centrifugal forces for Firefox.</strong><br/>
+ Marketing is a way to change what we socially say about things: that’s why Google Chrome marketing campaign (and consistent features maturity) has been the decreasing starting point of Firefox. <strong>Our message has been scrambled.</strong></p>
+ <h4>From participation to emancipation: values, people and org relationships</h4>
+ <p>How to emancipate people in the digital world ?</p>
+ <h5>Keeping the open open</h5>
+ <p>Being open is not a thing we can achieve, it’s a constant process. « <em>Mozilla needs to engage on both fronts, tackling the big problems but also fuelling the next wave of open.</em> » Yes, but <strong>Mozilla should say too how the next wave of open can stay under people’s control and rally new people</strong>. Not only open code, but open participation, open governance, open organization. Being open is not a releasing policy about objects, it’s a mutation to participation process: a metamorphosis. It’s not reached by expanding, but by shifting. It’s not only about an amount, but about values: it’s qualitative.<br/>
+ Maybe <strong>open is not enough</strong>, because it doesn’t say enough about who control and how, about the governance, and says too much about <strong>availability (passive)</strong> and not enough <strong>about <em>inclusivity</em> (active ; inclusion strengths)</strong>. It doesn’t say how the power is organized and articulated to the people (ex. think about how closed is the open Android). We may need to change the wording: indie web, the web that fuel autonomy, is a try, but it doesn’t say enough about <em>inclusivity</em> compared to openness &amp; opportunity. Emancipation is the concept. It’s strategic because it says what is aligned to what, especially how to articulate values and uses. It’s important because it tells what are the sufficient conditions of realization to ‘open/indie’. That’s key to get ‘open/indie at small and large scales, from Internet people to Internet institutions, thought all ‘open/indie’ detractors in the always-current situation: a resilient ecosystem.<br/>
+ My intuition is that <strong>the leadership network and advocacy engine promoting open will be efficient if we clarify ‘open’ while keeping it universal</strong>. We can do it by looking back at the raw material that we have worked for years, our DNA in action. Because after all, we are experts about it and wish others to become experts too. It does not mean to essentialize it (opposing its nature and its culture), <strong>but to define its conditions of continuous achievement in our social context</strong>.</p>
+ <h5>Starting point: exemplary projects that tell a lot about the evolution of our DNA in action</h5>
+ <p>Clarifying the idea of ‘open’ is strategic to our action because it outlines the constitution of ‘open’, its high ‘rules’, like with laws in political regimes. It clarifies for all, if you are part of it or not, and it tells you what to change to get in. It can reinforce the brand by differentiating from the big players that are the GAFAM: <strong>it’s a way to drive, not to be driven by others lowering the meaning to catch the social impact. We should say that ‘open’ at Mozilla means more than ‘open’ at GAFAM</strong>. I wish Mozilla to speak about its openness, not as an ‘equal in opportunity’ but as an ‘equal in participation’, because it fits openness not only for a moment (on boarding) or for a person, but during the whole process of people’s interaction.<br/>
+ <a href="https://www.rust-lang.org/">Rust</a> and <a href="https://servo.org/">Servo</a> or <a href="https://firefoxos.mozilla.community/">Firefox OS</a> (since the Mozilla’s shift to radical participation) seem to be very good examples of projects with participation &amp; impact centric rules, tools, process (RFC, new team and owners, …). Think about how Rust and <a href="http://arc.applause.com/2015/03/27/google-dart-virtual-machine-chrome/">Dart emerged and are evolving</a>. Think about how stronger has been the locked-open Android with partnership than the open-locked FxOS. We should tell those stories, not as recipes that can be reproduced, but as process based on a Constitution (inclusive rules) that make a political regime (open) and define a mode of government (participation). That’s key to social understanding and therefore to transpose and advocate for it.<br/>
+ As projects<strong> compared to ‘original Mozilla’, Rust, Servo and FxOS could say a lot</strong> about how different they implemented learning/interaction/participation at the roots of the project. How the process, the tools, the architecture, the governance and the opportunities/constraints have changed for Mozilla and participants. This could definitely help to setup our curriculum resources, database and workshop at a personal (e.g., “How to teach / facilitate / organize / lead in the open like Mozilla.â€) and orgs levels, with personal and orgs policies.</p>
+ <h5>Spreading the high meanings in our strategy to consolidate it consistency</h5>
+ <p>Clarifying the constitution of ‘open’ calls to clarify other related wordings.<br/>
+ I’m satisfied to read back (social) ‘movement’ instead of ‘community’, because it means that our goal can’t be achieve forever (is static), but we should protect it by acting. And it seems more inclusive, less ‘folds on itself’ and less ‘build the alternative beside’ than ‘community’: the alternative can be everywhere the actual system is. It can make a system. It can get global, convergent, continuous, … all at the same time. Because it’s roots are decentralized _and_ consistent, collaborating, …</p>
+ <p>About participation, we should think too (again) about engagement VS contribute VS participate: how much am I engaged ? Free about defining and receiving cost/gains? What is the impact of my actions ? … <strong>These different words carry different ideas about how we connect the ‘open’</strong>: spread is not enough because it diffuses, _be_ everywhere is more permanent. Applied to Mozilla’s own actions, <strong>funding open projects and leaders, is maybe not enough and there should be others areas where we can connect</strong> inside products, technology, people and organizations that build emancipation. So that say something about getting control (who, how, …).</p>
+ <h5>IA: a challenge for ‘open’</h5>
+ <p>IA is first developed to help us by improving our interactions. However, this seems to start to shift into taking decisions instead of us. This is problematic because these are indirect and direct ways for us to loose control, to be locked. And that can be as far as computers smarter than humans. The problem is that technical progress is made without any consideration of the societal progress it should made.<br/>
+ That’s an other point, why open is not enough: automation should be build-in with superior humanization. <strong>Mozilla should activate and unlock societal progress to build fair technical progress.</strong></p>
+ <h5>Digital integration &amp; democracy</h5>
+ <p>The digital (&amp; virtual) world is gaining control over the physical world in many domains of our society (economy to finance, mail to email, automatic car, voting machine, …). It’s getting more and more integrated to our lives without getting back our (imperfect) democracy integrated into them. Public benefit and public good are turning ‘self benefit’ and ‘own sake’ because citizens don’t have control over private companies. <strong>We should build a digital democracy if we don’t want to loose at all the democratic governing of society.</strong> We must overcome the poses and postures battles about private and public. We need to build.</p>
+ <h4>‘Leader’ &amp; ‘Leadership’ need a clarification</h4>
+ <h5>Why a clarification?</h5>
+ <p>At some level, I’m not the only one to ask this question:</p>
+ <blockquote><p>How do CRM requirements for Leadership and Advocacy overlap / differ? What’s our email management / communications platform for Leadership?</p></blockquote>
+ <p>Connect leaders to lead what ? How ? To whose benefit ? Do we want to connect leaders or initiatives (people or orgs) ? Will the leaders be emerging ones (building new networks) or established ones (use they influence to rally more people)? Are Leaders leaders of something part of Mozilla (like can be Reps) or outside of Mozilla (leaders of project, companies, newspaper: tech leaders, news leaders, …) ? This is especially important depending on what is the desire for the leaders to become in the future. <strong>The MoFo’s document should be more precise</strong> about this and go forward than « <em>Mozilla must attract, develop, and support a global network of diverse leaders who use their expertise to collaboratively advance points-of-view, policies and practices that maintain the overall health of the Internet.</em> »<br/>
+ We should do it because <strong>the confusion about the leadership impact the advocacy engine</strong>: « <em>The shared themes also provide explicit opportunities for our Leadership and Advocacy efforts to work together.</em> » Regarding Mozilla, is the leaders role to be advocacy leaders ? It seems as they share themes and key initiatives (even if not worded the same sometimes). Or in other words, who Drives the Advocacy engine?</p>
+ <h5>Iterations with the actual definition: creators</h5>
+ <p>Here are my iterations on the definition of ‘Leaders’:</p>
+ <ul>
+ <li>The Leaders could be the people platform (the community) and the advocacy engine the tool/themes/actions platform (the product).</li>
+ <li>Leaders could build at the end new solutions (products) and Advocates new voices (rallying), that could be translated in a learning area divided like Leadership=learn+create and advocacy=teach+spread.</li>
+ <li>Leadership: personal development to produce (turn into) new commons or add new facets to commons. Advocacy: personal development to protect established/identified commons.</li>
+ </ul>
+ <p>With these definitions, then Leaders are maybe more a Lab, R&amp;D place, incubation tool (if we think about start-up incubators, then it shows a tool-set that we will need to inspire for the future). But if we want to keep the emphasis on people, <strong>we could name them ‘creators’</strong> (compositors or managers ; not commoners, because leaders and advocates are commoners ; yes, traditionally creators are craftspersons and intellectual designers). This make sens with the examples given in the MoFo 2020 strategy 0.8 document, where all persona are involved in a building-something-new process.</p>
+ <p>However, it’s interesting to understand why we choose at first ‘Leaders’. <strong>Leaders build new solutions (products) and Advocates new voices (rallying), they are both about personal development and empower commons.</strong> Leadership=learn+create and advocacy=teach+spread commons. Leaders are projects/orgs leaders, the ones that traduce DNA (values) in products (concrete ability and availability). Advocates are values advocates, the ones that traduce DNA (values) in actions (behavior). As they are both targeting commons, they both produce the same social organization (collaboration instead of competition). They are both involved to create (different) representation (institutions) and organization (foundation/firms) but <strong>with a different DNA (values) processing</strong>: from public good to personal interest or the opposite. If Mozilla cares about public good resilience, <strong>the articulation of they domains of values is critical. So their articulation’s expression and the revision process must be said and clear</strong>: from hierarchy vs contract vs different autonomy levels (internal incubation and external advocacy), vs … to criteria to start a revision.</p>
+ <h5>The network effect</h5>
+ <p>Another argument for the switch from Leader to Creator is that the Leader word it too much tight to a single-person-made innovation. <strong>Creator make more clear that the innovation is possible not because of one genius, but because of a team</strong>, a group, a collective: personS (where there could also be genius). The value is made by the collaboration of people (especially in an open project, especially in a network).<br/>
+ That’s important because that could impact how well we do the convening part: not self-promoting, not-advertising, but sharing skills and knowledge for people and catalysing projects.<br/>
+ <strong>The same for the wording ‘talent’</strong>: alone, a talent can do nothing that has an impact. At least, we need two talents, a team (plus some assistants at some point).</p>
+ <h5>The cultural prism</h5>
+ <p>Again, this seems to be an open question:</p>
+ <blockquote><p>Define and articulate “leadership.†Hone our story, ethos and definition for what we mean by “leadership development†(including cultural / localization aspects).</p></blockquote>
+ <p>In my culture, Leader carry positive (take action) and negative (dominate) meanings. That’s another reason why I prefer another naming.<br/>
+ I understand too that it carries a lot of legitimacy (ex. market leader) in our societies and it avoids the stay-experimental or non-massive (unique) thoughts. And we need legitimacy to get impact.<br/>
+ But the way Mozilla has an impact thought all cultures, its <strong>legitimacy, is by creating or expanding a common</strong>. To do this, depending on the maturity, Mozilla could follow the market proposing an alternative with superior usability OR opening a new market by adding a vertical segment.</p>
+ <h5>Existing tool-set opportunities</h5>
+ <p>If Leadership is « <em>a year-round MozFest + Lab</em>« , so it’s a social network + an incubation place. Then, we already have a social network for people involved with Mozilla: Which kind of link should have the leadership network with <strong>mozillians.org</strong> ? What can we learn from this project and other specialized social network projects (linkedin, viadeo, …) to build the leadership network ?</p>
+ <h4>Advocacy engine: make it clear</h4>
+ <h5>What it is &amp; how it works</h5>
+ <p>Mozilla is doing a great effort to build its advocacy engine on collaboration (« <em>Develop new partnerships and build on current partnerships</em>« , « <em>begin collaboration</em>« , « <em>build alliances with similar orgs</em>« ) but at the same time affirms that Mozilla should be « <em>Part of a broader movement, be the boldest, loudest and most effective advocates</em> » that could be seen as too centralized, too exclusive.<br/>
+ While this can be consistent (or contradictory), <strong>the consistency has to be explained</strong> looking at orgs and people, global and local, abstract and real, with a complementarity/competitive grid.<br/>
+ First, <strong>the articulation with other orgs has to be explained</strong>. What about others orgs doing things global (<a href="https://eff.org/">EFF</a>, <a href="https://fsf.org/">FSF</a>, …) and local (<a href="http://www.laquadrature.net/">Quadrature du net</a>, CCC, …) ? What about the value they give and that Mozilla doesn’t have (juridic expertise for example) ? What about other advocate engines (<a href="https://change.org/">change.org</a>, <a href="https://secure.avaaz.org/">Avaaz</a>…) ? That should not be at an administrative level only like « <em>Develop an affiliate policy. Defining what MoFo does / does not offer to effectively govern relationships w. affiliated partners and networks (e.g., for issues like branding, fundraising, incentives, participation guidelines, in-kind resources.)</em> »<br/>
+ Second, this is key for users to understand and <strong>articulate the global level of the brand engagement and their local preoccupations and engagement</strong>. How the engine will be used for local (non-US) battles ? In the past Mozilla totally involved against PIPA, SOPA by taking action, and hesitate a lot to take position and just published a blog post (and too late to gain traction and get impact) against French spying law for example.<br/>
+ Third, <strong>the articulation ‘action(own agenda)/reaction’ should be clarified</strong> in the objectives and functioning of the advocacy engine. Especially because other orgs, allies or detractors, try to to setup the social agenda. It’s important because it can change the social perception of our narrative (alternative promotion/issue fighting) and therefore people’s contributions.<br/>
+ People think the technology is socially neutral. People are satisfied of narrow hills of choice (not the meaning, the aim, but only the ability to show your favorite avatar). <strong>People don’t want to feel guilty or oppressed</strong>, they don’t want new constraints, they are looking for solution only: they want to use, not to do more, they want they things to be done. Part of the problem is about understanding (literacy, education), part of it is about the personal/common duality, part of it is about being hopeless about having an impact, part of it is about expressing change as a positive goal and a new possible way (alternative), not a fight against an issue. About the advocacy engine, I think <strong>our preoccupation should be people-centric and the aim to give them a short, medium and long term narrative to get action without being individuals-centric</strong>.</p>
+ <h5>How we build it ?</h5>
+ <p>How to build a social movement ? How it has been built in the past ? Is it the same today ? Can it be transposed to the digital domain from others social domains ? How strong are the cultural differences between nations? These are the main questions we should answer, and our pivot era gives us many examples in diverse domains (climate change advocates, Syriza &amp; Podemos, NSA &amp; surveillance services in Europe, empowered syndicates in Venezuela, <a href="http://blogs.valvesoftware.com/economics/why-valve-or-what-do-we-need-corporations-for-and-how-does-valves-management-structure-fit-into-todays-corporate-world/#more-252">Valve corp. internal organization</a>…) to set a search terrain. However, I will go strait to my intuitive understanding below.<br/>
+ I’m kind of worried that it’s imagined to build the advocacy engine themes by a top-down method. <strong>I think a successful advocacy is always built bottom-up</strong>, as its function is to give back the voice to the people, to get them involved, not to make them fulfill our predefined aims. The top-down method is too organization centric: it can’t massively drive people that are not already committed to the org. It’s usually named advertisement or propaganda. If we want to have impact, <strong>we should listen to people needs, not tell them to listen to ours. People want (first) to be empowered, not to empower an org</strong>. So let’s organize the infrastructure, set the agenda and draw the horizon (strategic understanding) participative: make people fill them with content of their experience. It seems to me it is the only way, the only successful method, if we want to build a movement, and not just a shifting moment (that could be built by the top, with a good press campaign locally relayed for example ; that’s what happen in old style politics: the aim is short term, to cleave).<br/>
+ <strong>Isn’t the advocacy engine a new Drumbeat ?</strong> We shifted from Drumbeat to Webmaker+web literacy to Mozilla Academy and now to Leadership plus advocacy: it could be good to tell that story now that we are shifting again and learn from it.<br/>
+ <strong>Mozilla should support, behave as a platform</strong>, not define, not focus. Letting the people set the agenda makes them more involved and is a good way to build a network of shared aims with other orgs, that is not invasive or alienating, but a support relationship in a win-win move. The strength comes from the all agendas sewed. So at an org level, let’s on-board allies organizations as soon as plan building-time (now), to build it together. Yes it’s slower, but much more massive, inclusive and persistent.</p>
+ <h5>How we evaluate it: cultural bias &amp; qualitative analysis</h5>
+ <p>First, about the agenda-setting KPI for 2016, should these KPI be an evaluation of the inclusion and rank in others strategic agendas, governance systems and productions (outcome/products) ? Others org could be from different domains: political, social, economy orgs.<br/>
+ Then, as a wide size audience KPI, Mozilla wants « <em>celebration of our campaigns with ‘headline KPIs’ including number of actions, and number of advocates.</em>« . While doing this could be the right thing to do for some cultures, it could be the worst for others. I think that these KPI don’t carry a meaning for people and are too org centric. In a way, they are to generic: it’s just an amount. <strong>Accumulation is not our goal: we want impact that is the grow of articulated actions</strong> made by diverse people toward the same aim. <strong>We need our massive KPI to be more qualitative</strong>, or at least find a way to present them in a more qualitative way: interactive map ? a global to local prism that engages people for the next step ?</p>
+ <h5>Best practices &amp; massive impact</h5>
+ <p>Selecting best practices are an appealing method when we want to have a fast and strong impact in a wide area. However, <strong>when we unify we should avoid to homogenize</strong>. The gain in area by scaling-up is always at the cost of loosing local impact because it is not corresponding to local specificities, hence to local expectations. Federating instead of scaling-up is a way to solve this challenge. So we should be careful to not <strong>use best practice as absolute solutions, but as solutions in a context</strong> if we want to transpose them massively.</p>
+ <h5>Tools &amp; platform balanced between user-centric and org-centric outcomes</h5>
+ <p>It’s good to hear that we will build a advocacy platform. As we ‘had’ bugzilla+svn then mercurial (hg)+… and are going to the <strong>integrated</strong>, <strong>pluggable</strong> and <strong>content-centric</strong> (but non-free; admin tools are closed source) github (targeting more coder than users, but with a lower entry price for users still), we need to be able to have the same kind of tool for advocates and leaders. Something inspired maybe at some levels by the remixing tools we built in Webmakers for web users.</p>
+ <h4>From experiment to production: support (self made to mass product) + modularity (dev code patch to users add-ons).</h4>
+ <p><strong>We need pathways from lab to home that carry different mix of customization and reliability to support the emancipation curve.</strong><br/>
+ Users want things to work, because they want to use it. Geeks want to be able to modify a lot and accept to put their hands in the engine to build growing reliability. Advanced users want to customize their experience and keep control and understanding on working status. They want to be able to fix the reliability at a medium/low technical cost. They are OK to gain more control at these prices. Users want to use things to do what they need and want to trust a reliability maintained for them. They are OK to gain control at a no technical cost. Depending on the matter we all have different skill levels, so we are all geeks, advanced users and users depending on our position or on the moment. And depending on our aspirations, we all want to be able to move from one category to an other. That’s what we need to build: we don’t just need to « <em>better articulate the value to our audiences</em>« , <strong>we need to build pathways thought audiences and thought IT layers</strong> (content, software, hardware, distant service). <strong>We should find a convergence between customization and reliability, between first time experience, support and add-ons thought all our users’ persona by building bridges, pathways</strong>. So, « <em>better articulate the value to our audiences</em> » should not be restrained in our minds to the Mozilla Leadership Network.<br/>
+ <strong>Part of this is being done in other projects outside of Mozilla in the commons movement.</strong> There are many, but let’s take just one example, the <a href="https://www.fairphone.com/">Fairphone</a> project: modularity, howtos, … all this help to break the product-to-use walls and drive appropriation/emancipation. <strong>Products are less product and brand centric and more people/user centric</strong>.<br/>
+ Part of this has been done inside Mozilla, like integrating learning in our products, in-content, as we have code comment on code. I think <strong>the <a href="https://wiki.mozilla.org/Firefox_OS/Spark">Spark</a> project on Firefox OS is on a promising path</strong>, even if maybe immature: it maybe has not been released mainstream because it misses bridges/pathways (on-boarding levels, progression from simple to high level techniques, and no or not enough reproducible/universal next task/skill building).<br/>
+ So some solutions start to emerge, the direction is here, but has never been conceived and implemented that globally, as there isn’t integrated pathways with choice and opportunity and a strategy embracing all products and technologies (platform, tools, …).</p>
+ <h4>Better tools for collaboration and participation: task-centric to process-centric (use) infrastructure</h4>
+ <p><strong>The open community should definitely improve the collaboration tools and infrastructure to ease participation.</strong><br/>
+ <strong><a href="http://www.discourse.org">Discourse</a> ‘merged’ discussion channels</strong>: email+forum(+instant, messaging, … and others peer-to-peer discussion?). <strong><a href="http://stackexchange.com">Stack exchange</a> merged the questioning/solving process</strong> and added a vote mechanism to rank answers: it eased the collaboration on editing the statement and the results while staying synchronous with the discussion and keeping the discussion history. We need such kind of possibilities with discourse: <strong>capitalize on the discussion and preview the results to build a plan.</strong><br/>
+ This exist in document oriented software (that added collaboration editing tools), but not that much in collaboration software (that don’t produce documents). For example, while discussing the future plan for Fx/FxOS be supported to keep track on a doc about the proposals plans + criteria &amp; dependencies. In action, it is from <a href="https://mail.mozilla.org/pipermail/firefox-dev/2015-July/003063.html">this</a> plus all the discussion taking place to <a href="https://mail.mozilla.org/pipermail/firefox-dev/2015-July/003119.html">that</a>.<br/>
+ This is maybe something like integrating Discourse+Wiki, maybe with the need to have competing and ranked (both for content and underlaying meaning of content=strategy?) plan/page proposals. <strong>From evolving the wiki discussion page to featuring document production into peer-to-peer discussion.</strong></p>
+ <h4>A recovering strategy: from fail to win</h4>
+ <p>There is maybe one thing that is in the shadow in this plan: <strong>what do we do when/if we (partially) fail ?</strong><br/>
+ I think at least we should say that <strong>we document</strong> (keep research going on) to be able to outline and spread the outcomes of what we tried to fight against. So we still try to built consciousness to be ready for the next round.</p>
+ <p> </p>
+ <p><em>If you see some contradiction in my thoughts, let’s say it’s my state of thinking right now: please voice them so we can go forward.</em><br/>
+ <em> The same for thoughts that are voiced definitive (like users are): take it as a first attempt with my bias: let’s state these bias to go forward.</em></p>
+ <div class="footnotes" id="footnotes-48">
+ <div class="footnotedivider"/>
+ <ol>
+ <li id="fn-48-1"> ‘<em>Radical</em>‘ can be in some cultures an euphemism to ‘<em>violent</em>‘. Let’s be clear that the change by increasing violence is done to make a popular uprising of some part against others. While it does not help the majority to magically understand that the minority is right, it stigmatize the radical-violent-changers and in the way it discredits the alternative proposed. <span class="footnotereverse"><a href="https://repeer.org/tag/mozilla/feed/#fnref-48-1">↩</a></span></li>
+ </ol>
+ </div></div>
+ </content>
+ <updated>2016-01-16T00:27:13Z</updated>
+ <category term="Mozilla"/>
+ <category term="Technologie"/>
+ <category term="[EN]"/>
+ <category term="Firefox"/>
+ <category term="Firefox OS"/>
+ <category term="Rust"/>
+ <category term="Servo"/>
+ <category term="Social networks"/>
+ <author>
+ <name>Nicolas</name>
+ </author>
+ <source>
+ <id>https://repeer.org</id>
+ <link href="https://repeer.org/tag/mozilla/feed/" rel="self" type="application/rss+xml"/>
+ <link href="https://repeer.org" rel="alternate" type="text/html"/>
+ <subtitle>Reprenez le pouvoir !</subtitle>
+ <title>Repeer » Mozilla</title>
+ <updated>2016-01-16T00:46:46Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en">
+ <id>http://bluesock.org/%7Ewillkg/blog/pyvideo/status_20160115.html</id>
+ <link href="http://bluesock.org/%7Ewillkg/blog/pyvideo/status_20160115.html" rel="alternate" type="text/html"/>
+ <title>pyvideo status: January 15th, 2016</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><div class="section" id="what-is-pyvideo-org">
+ <h3>What is pyvideo.org</h3>
+ <p><a class="reference external" href="http://pyvideo.org/">pyvideo.org</a> is an index of Python-related conference and user-group videos on
+ the Internet. Saw a session you liked and want to share it? It's likely you can
+ find it, watch it, and share it with pyvideo.org.</p>
+ <p>This is the latest status report for all things happening on the site.</p>
+ <p>It's also an announcement about the end.</p>
+ <p><a href="http://bluesock.org/~willkg/blog/pyvideo/status_20160115.html">Read more…</a> (5 mins to read)</p></div></div>
+ </summary>
+ <updated>2016-01-15T23:30:00Z</updated>
+ <category term="dev"/>
+ <category term="python"/>
+ <category term="pyvideo"/>
+ <category term="richard"/>
+ <author>
+ <name>Will Kahn-Greene</name>
+ </author>
+ <source>
+ <id>http://bluesock.org/%7Ewillkg/blog/</id>
+ <link href="http://bluesock.org/%7Ewillkg/blog/" rel="alternate" type="text/html"/>
+ <link href="http://bluesock.org/~willkg/blog/rss.xml" rel="self" type="application/rss+xml"/>
+ <subtitle>Will Kahn-Greene's blog of Python, Mozilla, GNU/Linux, random content, dennis, Input, SUMO, and other projects mixed in there ad hoc, half-baked and with a twist of lemon</subtitle>
+ <title>Will's blog</title>
+ <updated>2016-01-16T16:31:16Z</updated>
+ </source>
+ </entry>
+
+ <entry>
+ <id>http://coopcoopbware.tumblr.com/post/137371863755</id>
+ <link href="http://coopcoopbware.tumblr.com/post/137371863755" rel="alternate" type="text/html"/>
+ <title>RelEng &amp; RelOps Weekly Highlights - January 15, 2016</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>One of releng’s big goals for Q1 is to deliver a beta via <a href="https://bugzil.la/release-promotion" target="_blank">build promotion</a>. It was great to have some tangible progress there this week with bouncer submission.</p>
+
+ <p>Lots of other stuff in-flight, more details below!
+ </p><p><b>Modernize infrastructure</b>:</p>
+
+ <p>Dustin worked with Armen and Joel Maher to run Firefox tests in TaskCluster on an older EC2 instance type where the tests seem to fail less often, perhaps because they are single-CPU or slower.</p>
+
+ <p><b>Improve CI pipeline</b>:</p>
+
+ <p>We turned off automation for b2g 2.2 builds this week, which allowed us to remove some code, reduce some complexity, and regain some small amount of capacity. Thanks to Vlad and Alin on buildduty for helping to land those patches. (<a href="https://bugzil.la/1236835" target="_blank">https://bugzil.la/1236835</a> and <a href="https://bugzil.la/1237985" target="_blank">https://bugzil.la/1237985</a>)</p>
+
+ <p>In a similar vein, Callek landed code to disable all b2g desktop builds and tests on all trees. Another win for increased capacity and reduced complexity! (<a href="https://bugzil.la/1236835" target="_blank">https://bugzil.la/1236835</a>)</p>
+
+ <p><b>Release</b>:</p>
+
+ <p>Kim finished integrating bouncer submission with our release promotion project. That’s one more blocker out of the way! (<a href="https://bugzil.la/1215204" target="_blank">https://bugzil.la/1215204</a>)</p>
+
+ <p>Ben landed several enhancements to our update server: adding aliases to update rules (<a href="https://bugzil.la/1067402" target="_blank">https://bugzil.la/1067402</a>), and allowing fallbacks for rules with whitelists (<a href="https://bugzil.la/1235073" target="_blank">https://bugzil.la/1235073</a>).</p>
+
+ <p><b>Operational</b>:</p>
+ <p>There was some excitement last Sunday when all the trees were closed due to timeouts connectivity issues between our SCL3 datacentre and AWS. (<a href="https://bugzil.la/238369" target="_blank">https://bugzil.la/238369</a>)</p>
+
+ <p><b>Build config</b>:</p>
+
+ <p>Mike released v0.7.4 of <a href="http://gittup.org/tup/" target="_blank">tup</a>, and is working on generating the tup backend from moz.build. We hope to offer tup as an alternative build backend sometime soon.</p>
+
+ <p>See you all next week!</p></div>
+ </summary>
+ <updated>2016-01-15T22:44:13Z</updated>
+ <category term="Mozilla"/>
+ <category term="releng"/>
+ <category term="highlights"/>
+ <source>
+ <id>http://coopcoopbware.tumblr.com/</id>
+ <author>
+ <name>Chris Cooper</name>
+ </author>
+ <link href="http://coopcoopbware.tumblr.com/" rel="alternate" type="text/html"/>
+ <link href="http://coopcoopbware.tumblr.com/tagged/Mozilla/rss" rel="self" type="application/rss+xml"/>
+ <title>Five different types of fried cheese</title>
+ <updated>2016-01-22T21:00:12Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>https://air.mozilla.org/webdev-beer-and-tell-january-2016/</id>
+ <link href="https://air.mozilla.org/webdev-beer-and-tell-january-2016/" rel="alternate" type="text/html"/>
+ <title>Webdev Beer and Tell: January 2016</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>
+ <img alt="Webdev Beer and Tell: January 2016" class="wp-post-image" height="90" src="https://air.cdn.mozilla.net/media/cache/35/0f/350f246037ead3bab95fdbd4c2b77484.png" width="160"/>
+ Once a month web developers across the Mozilla community get together (in person and virtually) to share what cool stuff we've been working on in...
+ </p></div>
+ </summary>
+ <updated>2016-01-15T22:00:00Z</updated>
+ <author>
+ <name>Air Mozilla</name>
+ </author>
+ <source>
+ <id>https://air.mozilla.org/</id>
+ <link href="https://air.mozilla.org/" rel="alternate" type="text/html"/>
+ <link href="https://air.mozilla.org/" rel="self" type="application/rss+xml"/>
+ <rights>Except where otherwise noted, content on this site is licensed under the Creative Commons Attribution Share-Alike License v3.0 or any later version.</rights>
+ <subtitle>Air Mozilla is the Internet multimedia presence of Mozilla, with live and pre-recorded shows, interviews, news snippets, tutorial videos, and features about the Mozilla community.</subtitle>
+ <title>Air Mozilla</title>
+ <updated>2016-01-25T20:31:50Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>http://blog.mozilla.org/sumo/?p=3665</id>
+ <link href="https://blog.mozilla.org/sumo/2016/01/15/whats-up-with-sumo-15th-january/" rel="alternate" type="text/html"/>
+ <title>What’s up with SUMO – 15th January</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">Hello, SUMO Nation! The second post of the year is here. Have you had a good time in 2016 so far? Let us know in the comments! Now, let’s get going with the updates and activity summaries. It will be … <a class="go" href="https://blog.mozilla.org/sumo/2016/01/15/whats-up-with-sumo-15th-january/">Continue reading</a></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p><strong>Hello, SUMO Nation!</strong></p>
+ <p>The second post of the year is here. Have you had a good time in 2016 so far? Let us know in the comments!</p>
+ <p>Now, let’s get going with the updates and activity summaries. It will be brief today, I promise.</p>
+ <h3><strong class="author-name">Welcome, new contributors!<br/>
+ </strong></h3>
+ <ul>
+ <li class="author">
+ <div class="author"><a class="username" href="https://support.mozilla.org/en-US/user/Andy.Yang">Andy.Yang</a></div>
+ </li>
+ </ul>
+ <div class="author">After the massive influx over the last few weeks, we only had Andy introducing himself recently – the warmer the welcome for him!</div>
+ <div class="author"/>
+ <div class="author">If you just joined us, don’t hesitate – come over and <a href="https://support.mozilla.org/forums/buddies" target="_blank">say “hi†in the forums!</a></div>
+ <div class="author"/>
+ <div class="author">
+ <h3><strong>Contributors of the week<br/>
+ </strong></h3>
+ <ul>
+ <li><a href="https://blog.mozilla.org/sumo/2016/01/08/whats-up-with-sumo-8th-january/" target="_blank">All the people who joined us in the winter season so far!</a></li>
+ </ul>
+ <div class="" id="magicdomid64">
+ <p><strong><span style="text-decoration: underline;">We salute you!</span></strong></p>
+ </div>
+ <div class="author">Don’t forget that if you are new to SUMO and someone helped you get started in a nice way you can <a href="https://support.mozilla.org/forums/buddies/711364?last=65670" target="_blank">nominate them for the Buddy of the Month!</a></div>
+ <div class="author"/>
+ </div>
+ <h3><strong>Most recent SUMO Community meeting</strong></h3>
+ <ul>
+ <li><a href="https://public.etherpad-mozilla.org/p/sumo-2016-01-11" target="_blank">You can read the notes here</a> and see the video on our <a href="https://www.youtube.com/channel/UCaiposaIhA7HfMqH2NIciyA/videos" target="_blank">YouTube channel</a> and <a href="https://air.mozilla.org/search/?q=sumo" target="_blank">at AirMozilla</a>.<del> </del><del><br/>
+ </del></li>
+ <li><strong>IMPORTANT: We are considering changing the way the meetings work. Help us figure out what’s best for you – join the discussion on the forums in this thread: <a href="https://support.mozilla.org/en-US/forums/contributors/711752?last=67873">(Monday) Community Meetings in 2016</a>.</strong></li>
+ </ul>
+ <h3><strong>The next SUMO Community meeting… </strong></h3>
+ <ul>
+ <li style="text-align: left;">is happening on <a href="https://public.etherpad-mozilla.org/p/sumo-2016-01-18" target="_blank">Monday the 18th – join us</a>!</li>
+ <li style="text-align: left;"><strong>Reminder: if you want to add a discussion topic to the upcoming meeting agenda:</strong>
+ <ul>
+ <li style="text-align: left;">Start a thread in the <a href="https://support.mozilla.org/forums/contributors" target="_blank">Community Forums</a>, so that everyone in the community can see what will be discussed and voice their opinion here before Monday (this will make it easier to have an efficient meeting).</li>
+ <li style="text-align: left;">Please do so as soon as you can before the meeting, so that people have time to read, think, and reply (and also add it to the agenda).</li>
+ <li style="text-align: left;">If you can, please attend the meeting in person (or via IRC), so we can follow up on your discussion topic during the meeting with your feedback.</li>
+ </ul>
+ </li>
+ </ul>
+ <h3><strong class="author-g-ivsra51ph44x461i">Developers</strong></h3>
+ <ul>
+ <li>The new version of the Ask A Question page is here!</li>
+ <li>The 2.0 version of the KPI dashboard is in the works.</li>
+ <li><a href="http://edwin.mozilla.io/t/sumo" target="_blank">You can see the current state of the backlog our developers are working on here</a>.</li>
+ <li><a href="https://public.etherpad-mozilla.org/p/sumo-p-2016-01-14" target="_blank">The latest SUMO Platform meeting notes can be found here</a>.</li>
+ <li>Interested in learning how Kitsune (the engine behind SUMO) works? <a href="http://kitsune.readthedocs.org/" target="_blank">Read more about it here</a> and <a href="https://github.com/mozilla/kitsune/" target="_blank">fork it on GitHub</a>!</li>
+ </ul>
+ <h3><strong>Community</strong></h3>
+ <ul>
+ <li>Our awesome Bangladesh SUMO Warriors are on the road again! Follow their adventures on Twitter under this tag: <a href="https://twitter.com/search?q=%23sumotourctg" target="_blank">#sumotourctg</a></li>
+ <li>
+ <div class="title"><a href="https://support.mozilla.org/forums/contributors/711729?last=67763">Reminder: take a look at our Work Week Summary for Mozlando. We need your feedback for a few things there.</a></div>
+ </li>
+ <li>
+ <div class="title">Ongoing reminder: if you think you can benefit from getting <a href="https://wiki.mozilla.org/Community_Hardware" target="_blank">a second-hand device</a> to help you with contributing to SUMO, you know where to find us.</div>
+ </li>
+ </ul>
+ <h3><strong class="user-chip" title="adriel0415">Support Forum</strong></h3>
+ <ul>
+ <li>Say hello to the new people on the forums!
+ <ul>
+ <li><span class="author-a-z87zkz70z39yz83zw7ykz89z3gz82zt"><a href="https://support.mozilla.org/user/Tomi55" target="_blank">Tomi55</a> (Hungarian)</span></li>
+ <li><span class="author-a-z87zkz70z39yz83zw7ykz89z3gz82zt"><a href="https://support.mozilla.org/user/jdc20181" target="_blank">jdc20181</a> (English)</span></li>
+ <li><span class="author-a-z87zkz70z39yz83zw7ykz89z3gz82zt"><a href="https://support.mozilla.org/user/andexi" target="_blank">andexi</a> (Spanish)</span></li>
+ <li><span class="author-a-z87zkz70z39yz83zw7ykz89z3gz82zt"><a href="https://support.mozilla.org/user/Qantas94Heavy" target="_blank">Qantas94Heavy</a> (English)</span></li>
+ <li><span class="author-a-z87zkz70z39yz83zw7ykz89z3gz82zt"><a href="https://support.mozilla.org/user/samuelms79" target="_blank">samuelms79</a> (Brazilian-PT)</span></li>
+ <li><span class="author-a-z87zkz70z39yz83zw7ykz89z3gz82zt"><a href="https://support.mozilla.org/user/jorgecomun" target="_blank">jorgecomun</a> (Spanish)</span></li>
+ </ul>
+ </li>
+ </ul>
+ <div class="">
+ <h3><strong class="author-g-ivsra51ph44x461i">Knowledge Base</strong></h3>
+ <div class="" id="magicdomid90">
+ <div class="" id="magicdomid82">
+ <ul class="list-bullet1">
+ <li><span class="author-a-z87zjz80zxwjz85z4z65zytdpz68zoz69z"><a href="https://support.mozilla.org/forums/knowledge-base-articles/711304#post-65289" target="_blank">Thanks to everyone who took part in the most recent KB Day!</a></span></li>
+ <li>Version 44 updates should be live now.</li>
+ <li><span class="author-a-w2dz70zaz70z7z89zqz78ziz69zz78zz85zz90zj"><a href="https://docs.google.com/spreadsheets/d/1lkpRPJp9P1P5MRU-c9dwbDC0w5bMmrMdu-BNMp1xe8w/edit#gid=6" target="_blank">Ongoing reminder: learn more about upcoming English article updates by clicking here</a></span>.</li>
+ <li><span style="text-decoration: underline;">Ongoing reminder #2:<a href="https://support.mozilla.org/forums/knowledge-base-articles/" target="_blank"> do you have ideas about improving the KB guidelines and training materials? Let us know in the forums</a>!</span></li>
+ </ul>
+ </div>
+ <div class="" id="magicdomid83">
+ <h3><strong class="author-g-ivsra51ph44x461i">Localization</strong></h3>
+ </div>
+ </div>
+ </div>
+ <div class="" id="magicdomid95">
+ <ul>
+ <li>Thanks to everyone writing in with problems, ideas, reports of bugs – all your feedback matters!</li>
+ </ul>
+ </div>
+ <div class="" id="magicdomid75">
+ <h3><strong>Firefox<br/>
+ </strong></h3>
+ <ul>
+ <li><strong>for Android</strong>
+ <ul>
+ <li><a href="https://support.mozilla.org/forums/contributors/711712?last=67653">Learn more about Firefox 43 for Android from the official thread with release notes / issues / discussions</a>.</li>
+ <li>
+ <div class="title"><a href="https://support.mozilla.org/forums/contributors/711718?last=67822">Reminder: Roland is sharing Firefox 44 for Android release notes / issues / discussions</a> with everyone in the forum.</div>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ <ul>
+ <li><strong>for Desktop</strong>
+ <ul>
+ <li>The <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1238620" target="_blank">uploading issues reported by many users are being tracked here.</a></li>
+ <li><a href="https://support.mozilla.org/questions/firefox?tagged=bug1208145&amp;show=all" target="_blank">The “show passwords†button has been removed from the password manager for the Beta of Version 44</a>. The developers are looking into <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1208145" target="_blank">last minute fixes for that in this bug</a>.</li>
+ <li>Also in Version 44, the <span class="author-a-kz88zz80zhz89z6hlz81znytez70zz66zz68z"><a href="https://bugzilla.mozilla.org/show_bug.cgi?id=606655" target="_blank">“ask me everytime†option for cookies will be removed from the privacy panel.</a></span></li>
+ </ul>
+ </li>
+ </ul>
+ <ul>
+ <li><strong>for iOS</strong>
+ <div class="" id="magicdomid85">
+ <ul class="list-bullet1">
+ <li><span class="author-a-107uz69zz81zhz78z0z78zz84zz66zz76zz82zz77zj"><a href="https://www.mozilla.org/en-US/firefox/ios/1.4/releasenotes/" target="_blank">Firefox for iOS 1.4 primarily with features for China is here</a>.<br/>
+ </span></li>
+ </ul>
+ </div>
+ <div class="" id="magicdomid86">
+ <ul class="list-bullet1">
+ <li><span class="author-a-107uz69zz81zhz78z0z78zz84zz66zz76zz82zz77zj">Firefox for iOS 2.0 is after 1.4 and hopefully sometime this quarter!</span></li>
+ </ul>
+ </div>
+ </li>
+ </ul>
+ </div>
+ <p>Not that many updates this week, since we’re coming out of our winter slumber (even though winter will be here for a while, still) and plotting an awesome 2016 with you and for you. Take it easy, have a great weekend and see you around SUMO.</p></div>
+ </content>
+ <updated>2016-01-15T19:38:51Z</updated>
+ <category term="General"/>
+ <author>
+ <name>Michał</name>
+ </author>
+ <source>
+ <id>https://blog.mozilla.org/sumo</id>
+ <link href="https://blog.mozilla.org/sumo/feed/" rel="self" type="application/rss+xml"/>
+ <link href="https://blog.mozilla.org/sumo" rel="alternate" type="text/html"/>
+ <subtitle>SUpport MOzilla's official blog - rocking the helpful web since 2008!</subtitle>
+ <title>SUMO Blog</title>
+ <updated>2016-01-25T09:31:47Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>https://air.mozilla.org/paris-firefox-os-hackathon-presentations/</id>
+ <link href="https://air.mozilla.org/paris-firefox-os-hackathon-presentations/" rel="alternate" type="text/html"/>
+ <title>Paris Firefox OS Hackathon Presentations</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>
+ <img alt="Paris Firefox OS Hackathon Presentations" class="wp-post-image" height="90" src="https://air.cdn.mozilla.net/media/cache/35/83/358305bfa246fff07d707061082134aa.png" width="160"/>
+ As an introduction to this weekend's Firefox OS Hackathon in Paris we'll have two presentations: - Guillaume Marty will talk about the current state of...
+ </p></div>
+ </summary>
+ <updated>2016-01-15T18:00:00Z</updated>
+ <author>
+ <name>Air Mozilla</name>
+ </author>
+ <source>
+ <id>https://air.mozilla.org/</id>
+ <link href="https://air.mozilla.org/" rel="alternate" type="text/html"/>
+ <link href="https://air.mozilla.org/" rel="self" type="application/rss+xml"/>
+ <rights>Except where otherwise noted, content on this site is licensed under the Creative Commons Attribution Share-Alike License v3.0 or any later version.</rights>
+ <subtitle>Air Mozilla is the Internet multimedia presence of Mozilla, with live and pre-recorded shows, interviews, news snippets, tutorial videos, and features about the Mozilla community.</subtitle>
+ <title>Air Mozilla</title>
+ <updated>2016-01-25T20:31:49Z</updated>
+ </source>
+ </entry>
+
+ <entry>
+ <id>https://tacticalsecret.com/tag/mozilla/rss/db7fec0c-34d3-4633-9904-79b98aab34e7</id>
+ <link href="https://tacticalsecret.com/renewing-lets-encrypt-certs-nginx/" rel="alternate" type="text/html"/>
+ <title>Renewing Let's Encrypt Certs (Nginx)</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>All the first <a href="https://crt.sh/?id=10172479">Let's Encrypt certs for my websites</a> from the LE private beta began expiring last week, so it was time to work through the renewal tooling. I wanted a script that:</p>
+
+ <ol>
+ <li>Would be okay to run daily, so there'd be plenty of retries if something went wrong, </li>
+ <li>Wouldn't</li></ol></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>All the first <a href="https://crt.sh/?id=10172479">Let's Encrypt certs for my websites</a> from the LE private beta began expiring last week, so it was time to work through the renewal tooling. I wanted a script that:</p>
+
+ <ol>
+ <li>Would be okay to run daily, so there'd be plenty of retries if something went wrong, </li>
+ <li>Wouldn't require extra config for me to forget about if I add a new site, </li>
+ <li>Would only renew certificates expiring in the next few weeks.</li>
+ </ol>
+
+ <p>The official Let's Encrypt client team is hard at work producing a great renew tool to handle all this, but it's not released yet. Of course I could use <a href="https://caddyserver.com/">Caddy Server</a> that <a href="https://www.youtube.com/watch?v=nk4EWHvvZtI">just handles all this</a>, but I have a lot invested in Nginx here.</p>
+
+ <p>So I wrote a short script and <a href="https://gist.github.com/jcjones/432eeaa6a2bf25e2c746">put it up in a Gist</a>. </p>
+
+ <p>The script is designed to run daily, with a random start between 00:00 and 02:00 to protect against load spikes at Let's Encrypt's infrastructure. It doesn't do any real reporting, though, except to maintain <code>/var/log/letsencrypt/renew.log</code> as the most-recent failure if one fails.</p>
+
+ <p>It's written to handle Nginx with Upstart's <code>service</code> command. It's pretty modular though; you could make this operate any webserver, or use the webroot method quite easily. Feel free to use the OpenSSL SubjectAlternativeName processing code for whatever purposes you have.</p>
+
+ <p>Happy renewing!</p></div>
+ </content>
+ <updated>2016-01-15T16:01:19Z</updated>
+ <category term="letsencrypt"/>
+ <category term="mozilla"/>
+ <author>
+ <name>James 'J.C.' Jones</name>
+ </author>
+ <source>
+ <id>https://tacticalsecret.com/</id>
+ <link href="https://tacticalsecret.com/" rel="alternate" type="text/html"/>
+ <link href="https://tacticalsecret.com/tag/mozilla/rss/" rel="self" type="application/rss+xml"/>
+ <subtitle>On a mission to solve information security issues for the whole Internet. That, and whatever else comes up.</subtitle>
+ <title>mozilla - The Internet of Secure Things</title>
+ <updated>2016-01-21T17:31:50Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="es-ES">
+ <id>http://firefoxmania.uci.cu/?p=15521</id>
+ <link href="http://firefoxmania.uci.cu/conoce-los-complementos-destacados-para-enero-2016/" rel="alternate" type="text/html"/>
+ <title>Conoce los complementos destacados para enero</title>
+ <summary>Comenzó un nuevo año y con él, te traemos nuevos e interesantes complementos para tu navegador preferido que mejoran con creces tu experiencia de navegación. Durante los próximos 6 meses estará trabajando nuevos miembros en el Add-ons Board Team, en la próxima selección desde Firefoxmanía te avisaremos.</summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p style="text-align: left;">Comenzó un nuevo año y con él, te traemos nuevos e interesantes complementos para tu navegador preferido que mejoran con creces tu experiencia de navegación. Durante los próximos 6 meses estará trabajando nuevos miembros en el Add-ons Board Team, en la próxima selección desde Firefoxmanía te avisaremos.</p>
+ <h3 style="text-align: left;">Elección del mes: uMatrix</h3>
+ <p>uMatrix es muy parecido a un <em>firewall</em> y desde una ventana fácilmente podrás controlar todos los lugares a donde tu navegador tiene permitido conectarse, qué tipo de datos pueden descargarse y cual puede ejecutar.</p>
+ <blockquote><p>Esta puede ser la extensión perfecta para el control avanzado de los usuarios.</p></blockquote>
+ <p><span id="more-15521"/></p>
+
+ <a href="http://firefoxmania.uci.cu/conoce-los-complementos-destacados-para-enero-2016/umatrix/"><img alt="Interfaz principal de uMatrix" class="attachment-thumbnail size-thumbnail" height="160" src="http://firefoxmania.uci.cu/wp-content/uploads/2016/01/uMatrix-160x160.png" width="160"/></a>
+ <a href="http://firefoxmania.uci.cu/conoce-los-complementos-destacados-para-enero-2016/umatrix2/"><img alt="Opciones de configuraci&#xF3;n de uMatrix" class="attachment-thumbnail size-thumbnail" height="160" src="http://firefoxmania.uci.cu/wp-content/uploads/2016/01/uMatrix2-160x160.png" width="160"/></a>
+
+ <p><em><a href="http://addons.firefoxmania.uci.cu/umatrix/" target="_blank">Instalar uMatrix »</a></em></p>
+ <h3>También te recomendamos</h3>
+ <p style="text-align: left;"><a href="http://addons.firefoxmania.uci.cu/https-everywhere/" target="_blank">⇒ HTTPS Everywhere</a> por <a href="https://addons.mozilla.org/en-US/firefox/user/eff-technologists/" title="EFF Technologists">EFF Technologists</a></p>
+ <p style="text-align: left;">Protege tus comunicaciones habilitando la encriptación HTTPS automáticamente en los sitios conocidos que la soportan, incluso cuando navegas mediante sitios que no incluyen el prefijo “https†en la URL.</p>
+ <p style="text-align: left;"><a href="http://addons.firefoxmania.uci.cu/add-to-search-bar/" target="_blank">⇒ Add to Search Bar</a> por <a href="https://addons.mozilla.org/firefox/user/dr-evil/" target="_blank" title="AdblockLite">Dr. Evil</a></p>
+ <p style="text-align: left;">Hace posible que cualquier página con un formulario de búsqueda disponible pueda ser añadido fácilmente a la barra de búsqueda de Firefox.</p>
+ <div class="wp-caption aligncenter" id="attachment_15528" style="width: 262px;"><a href="http://firefoxmania.uci.cu/wp-content/uploads/2016/01/add_to_search_bar.png" rel="attachment wp-att-15528"><img alt="add_to_search_bar" class="wp-image-15528 size-medium" height="226" src="http://firefoxmania.uci.cu/wp-content/uploads/2016/01/add_to_search_bar-252x226.png" width="252"/></a><p class="wp-caption-text">Añadiendo la búsqueda de un sitio web a la barra de búsqueda</p></div>
+ <p style="text-align: left;"><a href="http://addons.firefoxmania.uci.cu/duplicate-tabs-closer/" target="_blank">⇒ Duplicate Tabs Closer</a> por <a href="https://addons.mozilla.org/firefox/user/peuj/" target="_blank" title="The 1-Click YouTube Video Download Team">Peuj</a></p>
+ <p style="text-align: left;">Detecta las pestañas duplicadas en tu navegador y automáticamente las cierra.</p>
+ <h3 style="text-align: left;">Nomina tus complementos favoritos</h3>
+ <p style="text-align: left;">A nosotros nos encantaría que <strong>fueras parte del proceso</strong> de seleccionar los mejores complementos para Firefox y nos gustaría escucharte. <em>¿No sabes cómo?</em> Sólo tienes que <em>enviar un correo electrónico</em> a la dirección <strong>amo-featured@mozilla.org</strong> con el nombre del complemento o el archivo de instalación y los miembros evaluarán tu recomendación.</p>
+ <p style="text-align: left;"><strong>Fuente:</strong> <a href="https://blog.mozilla.org/addons/2016/01/01/january-2016-featured-add-ons/" target="_blank">Mozilla Add-ons Blog</a></p></div>
+ </content>
+ <updated>2016-01-15T15:10:26Z</updated>
+ <category term="Addons"/>
+ <category term="firefox"/>
+ <category term="complementos"/>
+ <author>
+ <name>Yunier J</name>
+ </author>
+ <source>
+ <id>http://firefoxmania.uci.cu</id>
+ <link href="http://firefoxmania.uci.cu/author/sosa/feed/" rel="self" type="application/rss+xml"/>
+ <link href="http://firefoxmania.uci.cu" rel="alternate" type="text/html"/>
+ <subtitle>Comunidad Mozilla Firefox de Cuba</subtitle>
+ <title>Yunier J – Firefoxmanía</title>
+ <updated>2016-01-23T12:16:22Z</updated>
+ </source>
+ </entry>
+
+ <entry>
+ <id>https://timtaubert.de/blog/2016/01/build-your-own-signal-desktop</id>
+ <link href="https://timtaubert.de/blog/2016/01/build-your-own-signal-desktop/" rel="alternate" type="text/html"/>
+ <title>Build Your Own Signal Desktop</title>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>The Signal Private Messenger is great. <strong>Use it.</strong> It’s probably the best secure
+ messenger on the market. When recently a desktop app was announced people were
+ eager to join the beta and even happier when an invite finally showed up in
+ their inbox. So was I, it’s a great app and works surprisingly well for an early
+ version.</p>
+
+ <p>The only problem is that it’s a Chrome App. Apart from excluding folks with
+ other browsers it’s also a shitty user experience. If you too want your
+ messaging app not tied to a browser then let’s just build our own standalone
+ variant of Signal Desktop.</p>
+
+ <h3>NW.js beta with Chrome App support</h3>
+
+ <p>Signal Desktop is a Chrome App, so the easiest way to turn it into a standalone
+ app is to use <a href="http://nwjs.io/">NW.js</a>. Conveniently, their next release v0.13
+ will ship with Chrome App support and is available for download as a beta
+ version.</p>
+
+ <p>First, make sure you have <code>git</code> and <code>npm</code> installed. Then open a terminal and
+ prepare a temporary build directory to which we can download a few things and
+ where we can build the app:</p>
+
+ <figure class="code"><div class="highlight"><pre>$ mkdir signal-build
+ $ cd signal-build
+ </pre></div></figure>
+
+
+ <h3>[OS X] Packaging Signal and NW.js</h3>
+
+ <p>Download the latest beta of NW.js and <code>unzip</code> it. We’ll extract the application
+ and use it as a template for our Signal clone. The NW.js project does
+ unfortunately not seem to provide a secure source (or at least hashes)
+ for their downloads.</p>
+
+ <figure class="code"><div class="highlight"><pre>$ wget http://dl.nwjs.io/v0.13.0-beta3/nwjs-sdk-v0.13.0-beta3-osx-x64.zip
+ $ unzip nwjs-sdk-v0.13.0-beta3-osx-x64.zip
+ $ cp -r nwjs-sdk-v0.13.0-beta3-osx-x64/nwjs.app SignalPrivateMessenger.app
+ </pre></div></figure>
+
+
+ <p>Next, clone the Signal repository and use NPM to install the necessary modules.
+ Run the <code>grunt</code> automation tool to build the application.</p>
+
+ <figure class="code"><div class="highlight"><pre>$ git clone https://github.com/WhisperSystems/Signal-Desktop.git
+ $ cd Signal-Desktop/
+ $ npm install
+ $ node_modules/grunt-cli/bin/grunt
+ </pre></div></figure>
+
+
+ <p>Finally, simply to copy the <code>dist</code> folder containing all the juicy Signal files
+ into the application template we created a few moments ago.</p>
+
+ <figure class="code"><div class="highlight"><pre>$ cp -r dist ../SignalPrivateMessenger.app/Contents/Resources/app.nw
+ $ open ..
+ </pre></div></figure>
+
+
+ <p>The last command opens a Finder window. Move <code>SignalPrivateMessenger.app</code> to
+ your Applications folder and launch it as usual. You should now see a welcome
+ page!</p>
+
+ <h3>[Linux] Packaging Signal and NW.js</h3>
+
+ <p>The build instructions for Linux aren’t too different but I’ll write them down,
+ if just for convenience. Start by cloning the Signal Desktop repository and
+ build.</p>
+
+ <figure class="code"><div class="highlight"><pre>$ git clone https://github.com/WhisperSystems/Signal-Desktop.git
+ $ cd Signal-Desktop/
+ $ npm install
+ $ node_modules/grunt-cli/bin/grunt
+ </pre></div></figure>
+
+
+ <p>The <code>dist</code> folder contains the app, ready to be launched. <code>zip</code> it and place
+ the resulting package somewhere handy.</p>
+
+ <figure class="code"><div class="highlight"><pre>$ cd dist
+ $ zip -r ../../package.nw *
+ </pre></div></figure>
+
+
+ <p>Back to the top. Download the NW.js binary, extract it, and change into the
+ newly created directory. Move the <code>package.nw</code> file we created earlier next to
+ the <code>nw</code> binary and we’re done. The <code>nwjs-sdk-v0.13.0-beta3-linux-x64</code> folder
+ does now contain the standalone Signal app.</p>
+
+ <figure class="code"><div class="highlight"><pre>$ cd ../..
+ $ wget http://dl.nwjs.io/v0.13.0-beta3/nwjs-sdk-v0.13.0-beta3-linux-x64.tar.gz
+ $ tar xfz nwjs-sdk-v0.13.0-beta3-linux-x64.tar.gz
+ $ cd nwjs-sdk-v0.13.0-beta3-linux-x64
+ $ mv ../package.nw .
+ </pre></div></figure>
+
+
+ <p>Finally, launch NW.js. You should see a welcome page!</p>
+
+ <figure class="code"><div class="highlight"><pre>$ ./nw
+ </pre></div></figure>
+
+
+ <h3>If you see something, file something</h3>
+
+ <p>Our standalone Signal clone mostly works, but it’s far from perfect. We’re
+ pulling from master and that might bring breaking changes that weren’t
+ sufficiently tested.</p>
+
+ <p>We don’t have the right icons. The app crashes when you click a media message.
+ It opens a blank popup when you click a link. It’s quite big because also NW.js
+ has bugs and so we have to use the SDK build for now. In the future it would be
+ great to have automatic updates, and maybe even signed builds.</p>
+
+ <p>Remember, Signal Desktop is beta, and completely untested with NW.js. If you
+ want to help file bugs, but only after checking that those affect the Chrome
+ App too. If you want to fix a bug only occurring in the standalone version
+ it’s probably best to file a pull request and cross fingers.</p>
+
+ <h3>Is this secure?</h3>
+
+ <p>Great question! I don’t know. I would love to get some more insights from people
+ that know more about the NW.js security model and whether it comes with all the
+ protections Chromium can offer. Another interesting question is whether bundling
+ Signal Desktop with NW.js is in any way worse (from a security perspective) than
+ installing it as a Chrome extension. If you happen to have an opinion about
+ that, I would love to hear it.</p>
+
+ <p>Another important thing to keep in mind is that when building Signal on your
+ own you will possibly miss automatic and signed security updates from the
+ Chrome Web Store. Keep an eye on the repository and rebuild your app from
+ time to time to not fall behind too much.</p></div>
+ </content>
+ <updated>2016-01-15T14:00:00Z</updated>
+ <source>
+ <id>https://timtaubert.de/</id>
+ <author>
+ <name>Tim Taubert</name>
+ </author>
+ <link href="https://timtaubert.de/atom.xml" rel="self" type="application/atom+xml"/>
+ <link href="https://timtaubert.de/" rel="alternate" type="text/html"/>
+ <title>Tim Taubert</title>
+ <updated>2016-01-15T15:04:09Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>http://glandium.org/blog/?p=3579</id>
+ <link href="http://glandium.org/blog/?p=3579" rel="alternate" type="text/html"/>
+ <title>Announcing git-cinnabar 0.3.0</title>
+ <summary>Git-cinnabar is a git remote helper to interact with mercurial repositories. It allows to clone, pull and push from/to mercurial remote repositories, using git. Get it on github. These release notes are also available on the git-cinnabar wiki. Development had been stalled for a few months, with many improvements in the next branch without any […]</summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>Git-cinnabar is a git remote helper to interact with mercurial repositories. It allows to clone, pull and push from/to mercurial remote repositories, using git.</p>
+ <p><a href="https://github.com/glandium/git-cinnabar">Get it on github</a>.</p>
+ <p>These release notes are also <a href="https://github.com/glandium/git-cinnabar/wiki/Release-Notes:-0.3.0">available on the git-cinnabar wiki</a>.</p>
+ <p>Development had been stalled for a few months, with many improvements in the<br/>
+ <code>next</code> branch without any new release. I used some time during the new year<br/>
+ break and after in order to straighten things up in order to create a new<br/>
+ release, delaying many of the originally planned changes to a future 0.4.0<br/>
+ release.</p>
+ <h3>What’s new since 0.2.2?</h3>
+ <ul>
+ <li>Speed and memory usage were improved when doing <code>git push</code>.</li>
+ <li>Now works on Windows, at least to some extent. See <a href="http://glandium.org/blog/Windows-Support">details</a>.</li>
+ <li>Support for pre-0.1.0 git-cinnabar repositories was removed. You must first<br/>
+ use a git-cinnabar version between 0.1.0 and 0.2.2 to upgrade its metadata.</li>
+ <li>It is now possible to attach/graft git-cinnabar metadata to existing commits<br/>
+ matching mercurial changesets. This allows to migrate from some other<br/>
+ hg-to-git tool to git-cinnabar while preserving the existing git commits.<br/>
+ See <a href="http://glandium.org/blog/Mozilla%3A-Using-a-git-clone-of-gecko%E2%80%90dev-to-push-to-mercurial">an example of how this works with the git clone of the Gecko mercurial<br/>
+ repository</a>
+ </li>
+ <li>Avoid mercurial printing its progress bar, messing up with git-cinnabar’s<br/>
+ output.</li>
+ <li>It is now possible to fetch from an incremental mercurial bundle (without<br/>
+ a root changeset).</li>
+ <li>It is now possible to push to a new mercurial repository without <code>-f</code>.</li>
+ <li>By default, reject pushing a new root to a mercurial repository.</li>
+ <li>Make the connection to a mercurial repository through ssh respect the<br/>
+ <code>GIT_SSH</code> and <code>GIT_SSH_COMMAND</code> environment variables.</li>
+ <li>
+ <code>git cinnabar</code> now has a proper argument parser for all its subcommands.</li>
+ <li>
+ </li>
+ <li>A new <code>git cinnabar python</code> command allows to run python scripts or open a<br/>
+ python shell with the right sys.path to import the cinnabar module.</li>
+ <li>All git-cinnabar metadata is now kept under a single ref (although for<br/>
+ convenience, other refs are created, but they can be derived if necessary).</li>
+ <li>Consequently, a new <code>git cinnabar rollback</code> command allows to roll back to<br/>
+ previous metadata states.</li>
+ <li>git-cinnabar metadata now tracks the manifests DAG.</li>
+ <li>A new <code>git cinnabar bundle</code> command allows to create mercurial bundles,<br/>
+ mostly for debugging purposes, without requiring to hit a mercurial server.</li>
+ <li>Updated git to 2.7.0 for the native helper.</li>
+ </ul>
+ <h3>Development process changes</h3>
+ <p>Up to before this release closing in, the <code>master</code> branch was dedicated to<br/>
+ releases, and development was happening on the <code>next</code> branch, until a new<br/>
+ release happens.</p>
+ <p>From now on, the <code>release</code> branch will take dot-release fixes and new<br/>
+ releases, while the <code>master</code> branch will receive all changes that are<br/>
+ validated through testing (currently semi-automatically tested with<br/>
+ out-of-tree tests based on four real-life mercurial repositories, with<br/>
+ some automated CI based on in-tree tests used in the future).</p>
+ <p>The <code>next</code> branch will receive changes to be tested in CI when things<br/>
+ will be hooked up, and may have rewritten history as a consequence of<br/>
+ wanting passing tests on every commit on <code>master</code>.</p></div>
+ </content>
+ <updated>2016-01-15T08:56:40Z</updated>
+ <category term="cinnabar"/>
+ <category term="p.m.o"/>
+ <category term="en"/>
+ <author>
+ <name>glandium</name>
+ </author>
+ <source>
+ <id>http://glandium.org/blog</id>
+ <link href="http://glandium.org/blog/?feed=rss2&amp;cat=25&amp;tag=en" rel="self" type="application/rss+xml"/>
+ <link href="http://glandium.org/blog" rel="alternate" type="text/html"/>
+ <subtitle>glandium.org</subtitle>
+ <title>p.m.o – glandium.org</title>
+ <updated>2016-01-16T11:30:44Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>https://air.mozilla.org/web-qa-weekly-meeting-20160114/</id>
+ <link href="https://air.mozilla.org/web-qa-weekly-meeting-20160114/" rel="alternate" type="text/html"/>
+ <title>Web QA Weekly Meeting, 14 Jan 2016</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>
+ <img alt="Web QA Weekly Meeting" class="wp-post-image" height="90" src="https://air.cdn.mozilla.net/media/cache/f5/13/f5137857516694df0458e837c2d3a4be.png" width="160"/>
+ This is our weekly gathering of Mozilla'a Web QA team filled with discussion on our current and future projects, ideas, demos, and fun facts.
+ </p></div>
+ </summary>
+ <updated>2016-01-14T17:00:00Z</updated>
+ <author>
+ <name>Air Mozilla</name>
+ </author>
+ <source>
+ <id>https://air.mozilla.org/</id>
+ <link href="https://air.mozilla.org/" rel="alternate" type="text/html"/>
+ <link href="https://air.mozilla.org/" rel="self" type="application/rss+xml"/>
+ <rights>Except where otherwise noted, content on this site is licensed under the Creative Commons Attribution Share-Alike License v3.0 or any later version.</rights>
+ <subtitle>Air Mozilla is the Internet multimedia presence of Mozilla, with live and pre-recorded shows, interviews, news snippets, tutorial videos, and features about the Mozilla community.</subtitle>
+ <title>Air Mozilla</title>
+ <updated>2016-01-25T20:31:49Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>https://air.mozilla.org/reps-weekly-20160114/</id>
+ <link href="https://air.mozilla.org/reps-weekly-20160114/" rel="alternate" type="text/html"/>
+ <title>Reps weekly, 14 Jan 2016</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>
+ <img alt="Reps weekly" class="wp-post-image" height="90" src="https://air.cdn.mozilla.net/media/cache/ea/95/ea959e7cca319261380787a7d8f66a94.png" width="160"/>
+ This is a weekly call with some of the Reps to discuss all matters about/affecting Reps and invite Reps to share their work with everyone.
+ </p></div>
+ </summary>
+ <updated>2016-01-14T16:00:00Z</updated>
+ <author>
+ <name>Air Mozilla</name>
+ </author>
+ <source>
+ <id>https://air.mozilla.org/</id>
+ <link href="https://air.mozilla.org/" rel="alternate" type="text/html"/>
+ <link href="https://air.mozilla.org/" rel="self" type="application/rss+xml"/>
+ <rights>Except where otherwise noted, content on this site is licensed under the Creative Commons Attribution Share-Alike License v3.0 or any later version.</rights>
+ <subtitle>Air Mozilla is the Internet multimedia presence of Mozilla, with live and pre-recorded shows, interviews, news snippets, tutorial videos, and features about the Mozilla community.</subtitle>
+ <title>Air Mozilla</title>
+ <updated>2016-01-25T20:31:50Z</updated>
+ </source>
+ </entry>
+
+ <entry xml:lang="en-US">
+ <id>http://blog.mozilla.org/community/?p=2281</id>
+ <link href="http://blog.mozilla.org/community/2016/01/13/32c3-report-chaos-time-zone/" rel="alternate" type="text/html"/>
+ <title>32C3 Report – Chaos Time Zone</title>
+ <summary type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">Written by Valentin Schmitt. Entering the CCH (Congress Center Hamburg) between Christmas and new year brings you somewhere else than Hamburg on Central European Time. Most people you’ll meet will say they are from Internet (or the Internets, if they … <a class="go" href="http://blog.mozilla.org/community/2016/01/13/32c3-report-chaos-time-zone/">Continue reading</a></div>
+ </summary>
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>Written by <a href="https://mozillians.org/en-US/u/Valentin/">Valentin Schmitt</a>.</p>
+ <p>Entering the CCH (Congress Center Hamburg) between Christmas and new year brings you somewhere else than Hamburg on Central European Time.</p>
+ <p>Most people you’ll meet will say they are from Internet (or the Internets, if they are funny), and for a few days you’ll live in -what a friend of mine called- Chaos Time Zone: a blurry mix of everyone’s time difference. Days are pretty shorts anyway and you’ll probably spend a lot of time under artificial light, so it won’t help your internal clock keeping on track. The organizers will gently remind you the 6,2,1 rule: 6 hours of sleep, 2 meals and 1 shower per day, that should keep you safe. You’ll probably meet a lot of great people, and will often have a hard time to decide which talk or workshop to go to.<br/>
+ This is the <a href="https://events.ccc.de/congress/2015/wiki/Static:Main_Page">32nd Chaos Communication Congress</a>. Welcome, and have fun!</p>
+ <div class="wp-caption aligncenter" id="attachment_2282" style="width: 610px;"><a href="https://www.flickr.com/photos/mariobehling/23398995803/"><img alt="32C3 Chaos Communication Congress" class="size-large wp-image-2282" height="400" src="https://blog.mozilla.org/community/files/2016/01/CCCPost1-600x400.jpg" width="600"/></a><p class="wp-caption-text">32C3 Chaos Communication Congress – Photo: Mario Behling</p></div>
+ <h3>FxOS is not dead.</h3>
+ <p>I looked for a screen printer, or anything to do myself a t-shirt with the message “Firefox OS is not dead!†on it, but very surprisingly regarding the variety of machines there, I couldn’t find any on site. I really wanted to do that, because most of the people I talked to about<br/>
+ Firefox OS answered me “But isn’t Firefox OS dead?â€. I bet it won’t come as a surprise for you, as there was a lot of feedback from the community regarding what some might call “a PR disasterâ€. It just made it very clear to me that we (still) have to communicate a lot on this topic, and very loudly, because the tech news websites will be less likely to spread the word this time.</p>
+ <p>Once this detail (<b>*cough*</b>) was clarified, almost everybody I had the chance to talk to showed a lot of interested for the project, the only ones who didn’t were the hardcore Free Software enthusiasts, whom have been disappointed by Mozilla recent policy choices (like the tiles with<br/>
+ advertisement, or the DRM support in Firefox desktop), or the people who care less about software freedom and prefer an iPhone to a free (as in freedom) mobile OS.</p>
+ <div class="wp-caption aligncenter" id="attachment_2283" style="width: 610px;"><a href="https://www.flickr.com/photos/mariobehling/23943014701"><img alt="Mozilla and Firefox at 32C3 with friends" class="size-large wp-image-2283" height="400" src="https://blog.mozilla.org/community/files/2016/01/CCCPost2-600x400.jpg" width="600"/></a><p class="wp-caption-text">Mozilla and Firefox at 32C3 with friends – Photo: Mario Behling</p></div>
+ <h3>“Well, it’s Mozilla.â€</h3>
+ <p>Mozilla has a pretty good image in the Free Software community, and the main reason why people never tried a Firefox OS device is only because they never had the chance to do so (not many devices marketed in Europe or the US, not many ports on mainstream phones). Fortunately enough, I had some foxfooding devices to hand out. The <a href="https://firefoxos.mozilla.community/foxfooding-faq/">foxfooding program</a> had a very positive reception, most people were happy to have the chance to try the OS, participate in sending data to Mozilla, file bugs, some were eager to develop apps, and try port the OS on their favorite phone or device (the RasPi got a bunch of them very excited).</p>
+ <p>More importantly, they really asked me how to flash a device, where to find the documentation to get started, how to file a bug. The people I handed a device to planned to show it to their colleagues, friends and fellow hacktivists, and were very excited to have phone with a hardware good enough to provide a responsive experience.</p>
+ <h3>Questions?</h3>
+ <p>“Is there a Signal app or any secure messaging app?â€<br/>
+ “Can I use Tor?â€<br/>
+ “Can I keep OSM maps in cache?â€<br/>
+ “Is there an app for WhatsApp?â€<br/>
+ These were the questions I was asked the most. It’s pretty expected that the hackers community is looking for reliable privacy tools, but I was a bit surprised by the last question that still came up several times. <img alt=":)" class="wp-smiley" src="http://blog.mozilla.org/community/wp-includes/images/smilies/simple-smile.png" style="height: 1em;"/></p>
+ <h3>Mozillians, assemble!</h3>
+ <p>An <a href="https://events.ccc.de/congress/2015/wiki/Static:Assemblies">assembly</a> is the name the Chaos Communication Congress gives to the physical place (typically a bunch of tables with a power outlet) within the CCH where people can gather to hack, share ideas and have self organized-sessions on a particular topic, or around a particular project, there were 277 registered this year.</p>
+ <div class="wp-caption aligncenter" id="attachment_2284" style="width: 610px;"><a href="https://www.flickr.com/photos/fossasia/23742587859"><img alt="Assembling Under The Lights" class="size-large wp-image-2284" height="450" src="https://blog.mozilla.org/community/files/2016/01/CCCPost3-600x450.jpg" width="600"/></a><p class="wp-caption-text">Assembling Under The Lights – Photo: Hong Phuc FOSSASIA</p></div>
+ <p>With the Mozilla Assembly, we had several sessions (directly at the Assembly or in dedicated rooms) over these 4 days:</p>
+ <ul>
+ <li>Several Nightly Firefox OS workshops, combining more than 50 participants;</li>
+ <li>The Mozilla community meetup that gathered 20 participants;</li>
+ <li>a Thunderbird session with 42 participants;</li>
+ <li>an IoT and Firefox OS workshop, in a dedicated room that was <a href="https://www.flickr.com/photos/fossasia/23485427703/">packed with 90 participants</a>;</li>
+ </ul>
+ <p>On average, there were around 15 Mozillians at the Assembly and a continuous flow of people from different community.</p>
+ <p>Other projects where Mozilla is involved were represented, <a href="https://media.ccc.de/v/32c3-7528-let_s_encrypt_--_what_launching_a_free_ca_looks_like">like Let’s Encrypt</a>, with a talk so successful that the conference room was full, and New Palmyra, for which Mozillians organized a session for 25 participants.</p>
+ <p>The hackers and makers communities have a real ethical and practical interest in a mobile or embedded OS that’s trustworthy and hackable, we bear similar values and Firefox OS is a great opportunity to strengthen the ties between us.</p></div>
+ </content>
+ <updated>2016-01-13T22:55:23Z</updated>
+ <category term="Events"/>
+ <category term="Participation"/>
+ <category term="Privacy"/>
+ <category term="32c3"/>
+ <category term="CCC"/>
+ <category term="contributor"/>
+ <category term="firefox os"/>
+ <category term="Moz32c3"/>
+ <category term="MozParticipation"/>
+ <category term="Participation Team"/>
+ <category term="privacy"/>
+ <author>
+ <name>Brian King</name>
+ </author>
+ <source>
+ <id>http://blog.mozilla.org/community</id>
+ <link href="http://blog.mozilla.org/community/feed/" rel="self" type="application/rss+xml"/>
+ <link href="http://blog.mozilla.org/community" rel="alternate" type="text/html"/>
+ <subtitle>News and notes from and for the Mozilla community.</subtitle>
+ <title>about:community</title>
+ <updated>2016-01-25T16:31:43Z</updated>
+ </source>
+ </entry>
+</feed>
diff --git a/mobile/android/tests/background/junit4/resources/feed_atom_wikipedia.xml b/mobile/android/tests/background/junit4/resources/feed_atom_wikipedia.xml
new file mode 100644
index 0000000000..8b0344c59f
--- /dev/null
+++ b/mobile/android/tests/background/junit4/resources/feed_atom_wikipedia.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ https://en.wikipedia.org/wiki/Atom_%28standard%29#Example_of_an_Atom_1.0_feed
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+
+ <title>Example Feed</title>
+ <subtitle>A subtitle.</subtitle>
+ <link href="http://example.org/feed/" rel="self" />
+ <link href="http://example.org/" />
+ <id>urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6</id>
+ <updated>2003-12-13T18:30:02Z</updated>
+
+
+ <entry>
+ <title>Atom-Powered Robots Run Amok</title>
+ <link href="http://example.org/2003/12/13/atom03" />
+ <link rel="alternate" type="text/html" href="http://example.org/2003/12/13/atom03.html"/>
+ <link rel="edit" href="http://example.org/2003/12/13/atom03/edit"/>
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <summary>Some text.</summary>
+ <content type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml">
+ <p>This is the entry content.</p>
+ </div>
+ </content>
+ <author>
+ <name>John Doe</name>
+ <email>johndoe@example.com</email>
+ </author>
+ </entry>
+
+</feed> \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/resources/feed_rss10_planetmozilla.xml b/mobile/android/tests/background/junit4/resources/feed_rss10_planetmozilla.xml
new file mode 100644
index 0000000000..2cfd93d3cd
--- /dev/null
+++ b/mobile/android/tests/background/junit4/resources/feed_rss10_planetmozilla.xml
@@ -0,0 +1,3860 @@
+<?xml version="1.0"?>
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:foaf="http://xmlns.com/foaf/0.1/"
+ xmlns:content="http://purl.org/rss/1.0/modules/content/"
+ xmlns:atom="http://www.w3.org/2005/Atom"
+ xmlns="http://purl.org/rss/1.0/"
+ >
+ <channel rdf:about="http://planet.mozilla.org/">
+ <title>Planet Mozilla</title>
+ <link>http://planet.mozilla.org/</link>
+ <description>Planet Mozilla - http://planet.mozilla.org/</description>
+ <atom:link rel="self" href="http://planet.mozilla.org/rss10.xml" type="application/rss+xml"/>
+
+ <items>
+ <rdf:Seq>
+ <rdf:li rdf:resource="http://dblohm7.ca/blog/2016/01/26/announcing-mozdbgext" />
+ <rdf:li rdf:resource="http://firefoxmania.uci.cu/?p=15548" />
+ <rdf:li rdf:resource="https://quality.mozilla.org/?p=49454" />
+ <rdf:li rdf:resource="http://dlawrence.wordpress.com/?p=29" />
+ <rdf:li rdf:resource="http://blog.mozilla.org/tanvi/?p=198" />
+ <rdf:li rdf:resource="https://blog.mozilla.org/?p=9166" />
+ <rdf:li rdf:resource="http://benoitgirard.wordpress.com/?p=651" />
+ <rdf:li rdf:resource="http://blog.servo.org/2016/01/25/twis-48/" />
+ <rdf:li rdf:resource="https://air.mozilla.org/mozilla-weekly-project-meeting-20160125/" />
+ <rdf:li rdf:resource="http://blog.mozilla.org/community/?p=2292" />
+ <rdf:li rdf:resource="tag:literaci.es,2014:Post/digital-skills-curriculum" />
+ <rdf:li rdf:resource="https://fundraising.mozilla.org/?p=800" />
+ <rdf:li rdf:resource="http://www.agmweb.ca/robbie-burns" />
+ <rdf:li rdf:resource="tag:this-week-in-rust.org,2016-01-25:blog/2016/01/25/this-week-in-rust-115/" />
+ <rdf:li rdf:resource="tag:blogger.com,1999:blog-1015214236289077798.post-7056349209464984020" />
+ <rdf:li rdf:resource="https://blog.mozilla.org/netpolicy/?p=907" />
+ <rdf:li rdf:resource="http://blog.mozilla.org/addons/?p=7640" />
+ <rdf:li rdf:resource="http://coopcoopbware.tumblr.com/post/137832199980" />
+ <rdf:li rdf:resource="https://air.mozilla.org/foundation-demos-january-22-2016/" />
+ <rdf:li rdf:resource="http://blog.mozilla.org/sumo/?p=3667" />
+ <rdf:li rdf:resource="https://air.mozilla.org/bay-area-rust-meetup-january-2016/" />
+ <rdf:li rdf:resource="https://blog.lizardwrangler.com/?p=3953" />
+ <rdf:li rdf:resource="https://blog.mozilla.org/webdev/?p=4082" />
+ <rdf:li rdf:resource="http://blog.mozilla.org/community/?p=2287" />
+ <rdf:li rdf:resource="https://blog.mozilla.org/netpolicy/?p=912" />
+ <rdf:li rdf:resource="https://tacticalsecret.com/tag/mozilla/rss/9c39ad13-14ae-4456-a84e-13612637d832" />
+ <rdf:li rdf:resource="https://air.mozilla.org/web-qa-weekly-meeting-20160121/" />
+ <rdf:li rdf:resource="http://soledadpenades.com/?p=6379" />
+ <rdf:li rdf:resource="http://pierros.papadeas.gr/?p=447" />
+ <rdf:li rdf:resource="http://adamlofting.com/?p=1396" />
+ <rdf:li rdf:resource="http://blog.ziade.org/2016/01/21/a-pelican-web-editor/" />
+ <rdf:li rdf:resource="http://www.ncameron.org/blog/rss/631106eb-e7b1-47d5-82f9-cb6ad210ea89" />
+ <rdf:li rdf:resource="http://nickdesaulniers.github.io/blog/2016/01/20/debugging-x86-64-assembly-with-lldb-and-dtrace" />
+ <rdf:li rdf:resource="https://rail.merail.ca/posts/rebooting-productivity.html" />
+ <rdf:li rdf:resource="http://blog.rust-lang.org/2016/01/21/Rust-1.6.html" />
+ <rdf:li rdf:resource="http://blog.mozilla.org/addons/?p=7644" />
+ <rdf:li rdf:resource="https://air.mozilla.org/the-joy-of-coding-episode-41/" />
+ <rdf:li rdf:resource="http://blog.mozilla.org/nfroyd/?p=452" />
+ <rdf:li rdf:resource="http://andreasgal.com/?p=573" />
+ <rdf:li rdf:resource="https://miketaylr.com/posts/2016/01/at-media-webkit-transform-three-dee.html" />
+ <rdf:li rdf:resource="http://globau.wordpress.com/?p=881" />
+ <rdf:li rdf:resource="https://www.alex-johnson.net/tag/mozilla/rss/85d84c54-ed0c-4ee5-beb3-8823edb3c074" />
+ <rdf:li rdf:resource="http://www.brianbondy.com/blog/id/172" />
+ <rdf:li rdf:resource="http://coffeeonthekeyboard.com/rss/0388d8a6-fc86-477e-a161-1b356e01fe77" />
+ <rdf:li rdf:resource="https://air.mozilla.org/reprendre-le-controle-de-sa-vie-privee-sur-internet/" />
+ <rdf:li rdf:resource="https://mykzilla.org/?p=245" />
+ <rdf:li rdf:resource="http://michaelkohler.info/?p=348" />
+ <rdf:li rdf:resource="http://dlawrence.wordpress.com/?p=27" />
+ <rdf:li rdf:resource="http://soledadpenades.com/?p=6335" />
+ <rdf:li rdf:resource="http://daniel.haxx.se/blog/?p=8544" />
+ <rdf:li rdf:resource="http://edunham.net/2016/01/19/how_much_knowledge_do_you_need_to_give_a_conference_talk.html" />
+ <rdf:li rdf:resource="https://quality.mozilla.org/?p=49441" />
+ <rdf:li rdf:resource="http://blog.monotonous.org/?p=678" />
+ <rdf:li rdf:resource="http://www.ncameron.org/blog/rss/0e4d587c-380c-40ce-954a-7206f69bc1dd" />
+ <rdf:li rdf:resource="http://geekyogre.com/rss/63eb682d-66b4-447d-8fb6-f4ed448019df" />
+ <rdf:li rdf:resource="http://dougbelshaw.com/blog/?p=39986" />
+ <rdf:li rdf:resource="tag:this-week-in-rust.org,2016-01-18:blog/2016/01/18/this-week-in-rust-114/" />
+ <rdf:li rdf:resource="http://nikkisquared.github.io/2016/01/17/what-does-your-work-mean-part-2.html" />
+ <rdf:li rdf:resource="http://ngokevin.com/blog/aframe-component/" />
+ <rdf:li rdf:resource="http://blog.gerv.net/?p=3527" />
+ <rdf:li rdf:resource="https://www.christianheilmann.com/?p=4957" />
+ <rdf:li rdf:resource="http://glandium.org/blog/?p=3510" />
+ <rdf:li rdf:resource="http://edunham.net/2016/01/16/buildbot_and_eoferror.html" />
+ <rdf:li rdf:resource="urn:md5:41d039bb28fb15c761578cba0b1454fa" />
+ <rdf:li rdf:resource="https://repeer.org/?p=48" />
+ <rdf:li rdf:resource="http://bluesock.org/%7Ewillkg/blog/pyvideo/status_20160115.html" />
+ <rdf:li rdf:resource="http://coopcoopbware.tumblr.com/post/137371863755" />
+ <rdf:li rdf:resource="https://air.mozilla.org/webdev-beer-and-tell-january-2016/" />
+ <rdf:li rdf:resource="http://blog.mozilla.org/sumo/?p=3665" />
+ <rdf:li rdf:resource="https://air.mozilla.org/paris-firefox-os-hackathon-presentations/" />
+ <rdf:li rdf:resource="https://tacticalsecret.com/tag/mozilla/rss/db7fec0c-34d3-4633-9904-79b98aab34e7" />
+ <rdf:li rdf:resource="http://firefoxmania.uci.cu/?p=15521" />
+ <rdf:li rdf:resource="https://timtaubert.de/blog/2016/01/build-your-own-signal-desktop" />
+ <rdf:li rdf:resource="http://glandium.org/blog/?p=3579" />
+ <rdf:li rdf:resource="https://air.mozilla.org/web-qa-weekly-meeting-20160114/" />
+ </rdf:Seq>
+ </items>
+ </channel>
+
+ <item rdf:about="http://dblohm7.ca/blog/2016/01/26/announcing-mozdbgext">
+ <title>Aaron Klotz: Announcing Mozdbgext</title>
+ <link>http://dblohm7.ca/blog/2016/01/26/announcing-mozdbgext/</link>
+ <content:encoded>&lt;p&gt;A well-known problem at Mozilla is that, while most of our desktop users run
+ Windows, most of Mozilla’s developers do not. There are a lot of problems that
+ result from that, but one of the most frustrating to me is that sometimes
+ those of us that actually use Windows for development find ourselves at a
+ disadvantage when it comes to tooling or other productivity enhancers.&lt;/p&gt;
+
+ &lt;p&gt;In many ways this problem is also a Catch-22: People don’t want to use Windows
+ for many reasons, but tooling is big part of the problem. OTOH, nobody is
+ motivated to improve the tooling situation if nobody is actually going to
+ use them.&lt;/p&gt;
+
+ &lt;p&gt;A couple of weeks ago my frustrations with the situation boiled over when I
+ learned that our &lt;code&gt;Cpp&lt;/code&gt; unit test suite could not log symbolicated call stacks,
+ resulting in my filing of &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1238305&quot; title=&quot;cppunittests do not look up breakpad symbols for logged stack traces&quot;&gt;bug 1238305&lt;/a&gt; and &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1240605&quot; title=&quot;Set _NT_SYMBOL_PATH on Windows test machines&quot;&gt;bug 1240605&lt;/a&gt;. Not only could we
+ not log those stacks, in many situations we could not view them in a debugger
+ either.&lt;/p&gt;
+
+ &lt;p&gt;Due to the fact that PDB files consume a large amount of disk space, we don’t
+ keep those when building from integration or try repositories. Unfortunately
+ they are be quite useful to have when there is a build failure. Most of our
+ integration builds, however, do include breakpad symbols. Developers may also
+ explicitly &lt;a href=&quot;https://wiki.mozilla.org/ReleaseEngineering/TryServer#Getting_debug_symbols&quot;&gt;request symbols&lt;/a&gt;
+ for their try builds.&lt;/p&gt;
+
+ &lt;p&gt;A couple of years ago I had begun working on a WinDbg debugger extension that
+ was tailored to Mozilla development. It had mostly bitrotted over time, but I
+ decided to resurrect it for a new purpose: to help WinDbg&lt;sup&gt;&lt;a href=&quot;http://dblohm7.ca/atom.xml#fn1&quot; id=&quot;r1&quot;&gt;*&lt;/a&gt;&lt;/sup&gt;
+ grok breakpad.&lt;/p&gt;
+
+ &lt;h3&gt;Enter mozdbgext&lt;/h3&gt;
+
+ &lt;p&gt;&lt;a href=&quot;https://github.com/dblohm7/mozdbgext&quot;&gt;&lt;code&gt;mozdbgext&lt;/code&gt;&lt;/a&gt; is the result. This extension
+ adds a few commands that makes Win32 debugging with breakpad a little bit easier.&lt;/p&gt;
+
+ &lt;p&gt;The original plan was that I wanted &lt;code&gt;mozdbgext&lt;/code&gt; to load breakpad symbols and then
+ insert them into the debugger’s symbol table via the &lt;a href=&quot;https://msdn.microsoft.com/en-us/library/windows/hardware/ff537943%28v=vs.85%29.aspx&quot;&gt;&lt;code&gt;IDebugSymbols3::AddSyntheticSymbol&lt;/code&gt;&lt;/a&gt;
+ API. Unfortunately the design of this API is not well equipped for bulk loading
+ of synthetic symbols: each individual symbol insertion causes the debugger to
+ re-sort its entire symbol table. Since &lt;code&gt;xul.dll&lt;/code&gt;’s quantity of symbols is in the
+ six-figure range, using this API to load that quantity of symbols is
+ prohibitively expensive. I tweeted a Microsoft PM who works on Debugging Tools
+ for Windows, asking if there would be any improvements there, but it sounds like
+ this is not going to be happening any time soon.&lt;/p&gt;
+
+ &lt;p&gt;My original plan would have been ideal from a UX perspective: the breakpad
+ symbols would look just like any other symbols in the debugger and could be
+ accessed and manipulated using the same set of commands. Since synthetic symbols
+ would not work for me in this case, I went for “Plan B:†Extension commands that
+ are separate from, but analagous to, regular WinDbg commands.&lt;/p&gt;
+
+ &lt;p&gt;I plan to continuously improve the commands that are available. Until I have a
+ proper README checked in, I’ll introduce the commands here.&lt;/p&gt;
+
+ &lt;h4&gt;Loading the Extension&lt;/h4&gt;
+
+ &lt;ol&gt;
+ &lt;li&gt;Use the &lt;code&gt;.load&lt;/code&gt; command: &lt;code&gt;.load &amp;lt;path_to_mozdbgext_dll&amp;gt;&lt;/code&gt;&lt;/li&gt;
+ &lt;/ol&gt;
+
+
+ &lt;h4&gt;Loading the Breakpad Symbols&lt;/h4&gt;
+
+ &lt;ol&gt;
+ &lt;li&gt;Extract the breakpad symbols into a directory.&lt;/li&gt;
+ &lt;li&gt;In the debugger, enter &lt;code&gt;!bploadsyms &amp;lt;path_to_breakpad_symbol_directory&amp;gt;&lt;/code&gt;&lt;/li&gt;
+ &lt;li&gt;Note that this command will take some time to load all the relevant symbols.&lt;/li&gt;
+ &lt;/ol&gt;
+
+
+ &lt;h4&gt;Working with Breakpad Symbols&lt;/h4&gt;
+
+ &lt;p&gt;&lt;strong&gt;Note: You must have successfully run the &lt;code&gt;!bploadsyms&lt;/code&gt; command first!&lt;/strong&gt;&lt;/p&gt;
+
+ &lt;p&gt;As a general guide, I am attempting to name each breakpad command similarly to
+ the native WinDbg command, except that the command name is prefixed by &lt;code&gt;!bp&lt;/code&gt;.&lt;/p&gt;
+
+ &lt;ul&gt;
+ &lt;li&gt;Stack trace: &lt;code&gt;!bpk&lt;/code&gt;&lt;/li&gt;
+ &lt;li&gt;Find nearest symbol to address: &lt;code&gt;!bpln &amp;lt;address&amp;gt;&lt;/code&gt; where &lt;em&gt;address&lt;/em&gt; is specified
+ as a hexadecimal value.&lt;/li&gt;
+ &lt;/ul&gt;
+
+
+ &lt;h4&gt;Downloading windbgext&lt;/h4&gt;
+
+ &lt;p&gt;I have pre-built a &lt;a href=&quot;https://github.com/dblohm7/mozdbgext/blob/master/bin/mozdbgext.dll?raw=true&quot;&gt;32-bit binary&lt;/a&gt;
+ (which obviously requires 32-bit WinDbg). I have not built a 64-bit binary yet,
+ but the code should be source compatible.&lt;/p&gt;
+
+ &lt;p&gt;Note that there are several other commands that are “roughed-in†at this point
+ and do not work correctly yet. Please stick to the documented commands at this
+ time.&lt;/p&gt;
+
+ &lt;hr /&gt;
+
+ &lt;p&gt;&lt;sup&gt;&lt;a href=&quot;http://dblohm7.ca/atom.xml#r1&quot; id=&quot;fn1&quot;&gt;*&lt;/a&gt;&lt;/sup&gt; When I write “WinDbgâ€, I am really
+ referring to any debugger in the &lt;em&gt;Debugging Tools for Windows&lt;/em&gt; package,
+ including &lt;code&gt;cdb&lt;/code&gt;.&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-26T19:45:00+00:00</dc:date>
+ </item>
+ <item rdf:about="http://firefoxmania.uci.cu/?p=15548">
+ <title>Yunier José Sosa Vázquez: Soporte para WebM/VP9, más seguridad y nuevas herramientas para desarrolladores en el nuevo Firefox</title>
+ <link>http://firefoxmania.uci.cu/soporte-para-webmvp9-mas-seguridad-y-nuevas-herramientas-para-desarrolladores-en-el-nuevo-firefox/</link>
+ <content:encoded>&lt;p style=&quot;text-align: left;&quot;&gt;¡Como pasa el tiempo amigos! Casi sin darnos cuenta han transcurrido 6 semanas y hasta hemos comenzado un año nuevo, un año en el que Mozilla prepara nuevas funcionalidades que harán de Firefox un mejor como por ejemplo: la &lt;a href=&quot;http://firefoxmania.uci.cu/como-se-hace-activar-electrolysis-en-firefox/&quot; target=&quot;_blank&quot;&gt;separación de procesos&lt;/a&gt;, el uso de vías alternas para &lt;a href=&quot;http://firefoxmania.uci.cu/el-futuro-de-los-plugins-npapi-en-firefox/&quot; target=&quot;_blank&quot;&gt;ejecutar plugins&lt;/a&gt; y la nueva API para desarrollar &lt;a href=&quot;http://firefoxmania.uci.cu/el-futuro-de-los-complementos-en-firefox/&quot; target=&quot;_blank&quot;&gt;complementos “multi navegadorâ€&lt;/a&gt;.&lt;/p&gt;
+ &lt;p style=&quot;text-align: left;&quot;&gt;Desde el anuncio en 2010 del formato de video WebM, &lt;a href=&quot;https://blog.mozilla.org/blog/2010/05/19/open-web-open-video-and-webm/&quot; target=&quot;_blank&quot;&gt;Mozilla ha mostrado un especial interés&lt;/a&gt; al ser una alternativa potente frente a los formatos propietarios del mercado que existían en aquel momento y de esta forma mejorar la experiencia de los usuarios al reproducir videos en la web. Con esta liberación se ha habilitado el &lt;strong&gt;soporte para WebM/VP9 en aquellos sistemas que no soportan MP4/H.264&lt;/strong&gt;.&lt;/p&gt;
+ &lt;p style=&quot;text-align: left;&quot;&gt;Desde algunas versiones atrás, Firefox incluye el plugin &lt;a href=&quot;http://andreasgal.com/2014/10/14/openh264-now-in-firefox/&quot; target=&quot;_blank&quot;&gt;OpenH264 proveído por Cisco&lt;/a&gt; para cumplir las especificaciones de WebRTC y habilitar las llamadas con dispositivos que lo requieran. Ahora, si el &lt;strong&gt;decodificador de H.264 está disponible&lt;/strong&gt; en el sistema, entonces se habilita este codec de video.&lt;span id=&quot;more-15548&quot;&gt;&lt;/span&gt;&lt;/p&gt;
+ &lt;h3 style=&quot;text-align: left;&quot;&gt;&lt;em&gt;Novedades para desarrolladores&lt;/em&gt;&lt;/h3&gt;
+ &lt;p style=&quot;text-align: left;&quot;&gt;En esta oportunidad, los desarrolladores podrán contar con herramientas de animación y filtros CSS, informes sobre consumo de memoria, depuración de WebSocket y más. Todo esto puedes leerlo en &lt;a href=&quot;https://www.mozilla-hispano.org/edicion-para-desarrolladores-44-editor-visual-manejo-de-memoria/&quot; target=&quot;_blank&quot;&gt;el blog de Labs&lt;/a&gt; de Mozilla Hispano.&lt;/p&gt;
+ &lt;h3 style=&quot;text-align: left;&quot;&gt;&lt;em&gt;Novedades en Android&lt;/em&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Los usuarios pueden elegir la página de inicio a mostrar, en vez de los sitios más visitados.&lt;/li&gt;
+ &lt;li&gt;El servicio de impresión de Android permite activar la impresión en la nube.&lt;/li&gt;
+ &lt;li&gt;Al &lt;a href=&quot;https://developer.chrome.com/multidevice/android/intents&quot; target=&quot;_blank&quot;&gt;intentar abrir una URIs&lt;/a&gt;, se le pregunta al usuario si desea abrirla en una pestaña privada.&lt;/li&gt;
+ &lt;li&gt;Adicionado el soporte para ejecutar URIs con el protocolo mms.&lt;/li&gt;
+ &lt;li&gt;Fácil acceso a la configuración de la búsqueda mientras buscamos en Internet.&lt;/li&gt;
+ &lt;li&gt;Ahora se muestran las sugerencias del historial de búsqueda.&lt;/li&gt;
+ &lt;li&gt;La página Cuentas Firefox ahora está basada en la web.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;&lt;em&gt;Otras novedades&lt;/em&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;El soporte para el algoritmo criptográfico RC4 ha sido removido.&lt;/li&gt;
+ &lt;li&gt;Soporte para el formato de compresión brotli cuando se usa HTTPS.&lt;/li&gt;
+ &lt;li&gt;Uso de un certificado de firmado SHA256 para las versiones de Windows en aras de adaptarse a los nuevos requerimientos.&lt;/li&gt;
+ &lt;li&gt;Para soportar el descriptor unicode-range de las fuentes web, el algoritmo de concordancia en Linux usa el mismo código como en las demás plataformas.&lt;/li&gt;
+ &lt;li&gt;Firefox no confiará más en la autoridad de certificación Equifax Secure Certificate Authority 1024-bit root o UTN – DATACorp SGC para validar &lt;a href=&quot;https://support.mozilla.org/ta/kb/secure-website-certificate&quot; target=&quot;_blank&quot;&gt;certificados web seguros&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;El soporte para el teclado en pantalla ha sido temporalmente desactivado en Windows 8 y 8.1.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;Si deseas conocer más, puedes leer las &lt;a href=&quot;http://www.mozilla.org/en-US/firefox/44.0/releasenotes/&quot; target=&quot;_blank&quot;&gt;notas de lanzamiento&lt;/a&gt; (en inglés) para conocer más novedades.&lt;/p&gt;
+ &lt;p&gt;&lt;strong&gt;Aclaración para la versión móvil.&lt;/strong&gt;&lt;/p&gt;
+ &lt;p&gt;En las descargas se pueden encontrar 3 versiones para Android. El archivo que contiene &lt;strong&gt;i386&lt;/strong&gt; es para los dispositivos que tengan la &lt;strong&gt;arquitectura de Intel&lt;/strong&gt;. Mientras que en los nombrados &lt;strong&gt;arm&lt;/strong&gt;, el que dice &lt;strong&gt;api11 funciona con Honeycomb (3.0) o superior&lt;/strong&gt; y el de &lt;strong&gt;api9 es para Gingerbread (2.3)&lt;/strong&gt;.&lt;/p&gt;
+ &lt;p&gt;Puedes obtener esta versión desde nuestra &lt;a href=&quot;http://firefoxmania.uci.cu/download/&quot; target=&quot;_blank&quot;&gt;zona de Descargas&lt;/a&gt; en español e inglés para Linux, Mac, Windows y Android. Recuerda que para navegar a través de servidores proxy debes modificar la preferencia &lt;strong&gt;network.auth.force-generic-ntlm&lt;/strong&gt; a &lt;code&gt;true&lt;/code&gt; desde &lt;a target=&quot;_blank&quot;&gt;about:config&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;Si te ha gustado, por favor comparte con tus amigos esta noticia en las redes sociales. No dudes en dejarnos un comentario.&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-26T18:56:54+00:00</dc:date>
+ <dc:creator>Yunier J</dc:creator>
+ </item>
+ <item rdf:about="https://quality.mozilla.org/?p=49454">
+ <title>QMO: Firefox 45.0 Beta 3 Testday, February 5th</title>
+ <link>https://quality.mozilla.org/2016/01/firefox-45-0-beta-3-testday-february-5th/</link>
+ <content:encoded>&lt;p&gt;Hello Mozillians,&lt;/p&gt;
+ &lt;p&gt;We are happy to announce that &lt;strong&gt;Friday, February 5th&lt;/strong&gt;, we are organizing &lt;strong&gt;Firefox 45.0 Beta 3 Testday&lt;/strong&gt;. We will be focusing our testing on the following features: &lt;em&gt;Search Refactoring, Synced Tabs Menu, Text to Speech and Grouped Tabs Migration&lt;/em&gt;. Check out the detailed instructions via &lt;a href=&quot;https://public.etherpad-mozilla.org/p/testday-20160205&quot; target=&quot;_blank&quot;&gt;this etherpad&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;No previous testing experience is required, so feel free to join us on &lt;strong&gt;&lt;a href=&quot;http://widget01.mibbit.com/?server=irc.mozilla.org&amp;amp;channel=%23qa&quot;&gt;#qa IRC channel&lt;/a&gt;&lt;/strong&gt; where our moderators will offer you guidance and answer your questions.&lt;/p&gt;
+ &lt;p&gt;Join us and help us make Firefox better! See you on &lt;strong&gt;Friday&lt;/strong&gt;!&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-26T14:40:55+00:00</dc:date>
+ <dc:creator>vasilica.mihasca</dc:creator>
+ </item>
+ <item rdf:about="http://dlawrence.wordpress.com/?p=29">
+ <title>David Lawrence: Happy BMO Push Day!</title>
+ <link>https://dlawrence.wordpress.com/2016/01/26/happy-bmo-push-day-4/</link>
+ <content:encoded>&lt;p&gt;the following changes have been pushed to bugzilla.mozilla.org:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;[&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1240575&quot; target=&quot;_blank&quot;&gt;1240575&lt;/a&gt;] Update form.reps.budget&lt;/li&gt;
+ &lt;li&gt;[&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1226028&quot; target=&quot;_blank&quot;&gt;1226028&lt;/a&gt;] API for batching MozReview requests&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;discuss these changes on &lt;a href=&quot;https://lists.mozilla.org/listinfo/tools-bmo&quot; target=&quot;_blank&quot;&gt;mozilla.tools.bmo&lt;/a&gt;.&lt;/p&gt;&lt;br /&gt; &lt;a href=&quot;http://feeds.wordpress.com/1.0/gocomments/dlawrence.wordpress.com/29/&quot; rel=&quot;nofollow&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; src=&quot;http://feeds.wordpress.com/1.0/comments/dlawrence.wordpress.com/29/&quot; /&gt;&lt;/a&gt; &lt;img alt=&quot;&quot; border=&quot;0&quot; height=&quot;1&quot; src=&quot;https://pixel.wp.com/b.gif?host=dlawrence.wordpress.com&amp;amp;blog=58816&amp;amp;post=29&amp;amp;subd=dlawrence&amp;amp;ref=&amp;amp;feed=1&quot; width=&quot;1&quot; /&gt;</content:encoded>
+ <dc:date>2016-01-26T14:27:50+00:00</dc:date>
+ <dc:creator>dlawrence</dc:creator>
+ </item>
+ <item rdf:about="http://blog.mozilla.org/tanvi/?p=198">
+ <title>Tanvi Vyas: Updated Firefox Security Indicators</title>
+ <link>https://blog.mozilla.org/tanvi/2016/01/26/updated-firefox-security-indicators/</link>
+ <content:encoded>&lt;p&gt;&lt;em&gt;This article has been coauthored by Aislinn Grigas, Senior Interaction Designer, Firefox Desktop&lt;/em&gt;&lt;br /&gt;
+ &lt;em&gt;Cross posting with &lt;a href=&quot;https://blog.mozilla.org/security/2015/11/03/updated-firefox-security-indicators-2/&quot;&gt;Mozilla’s Security Blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
+ &lt;p&gt;November 3, 2015&lt;/p&gt;
+ &lt;p&gt;Over the past few months, Mozilla has been improving the user experience of our privacy and security features in Firefox. One specific initiative has focused on the feedback shown in our address bar around a site’s security. The major changes are highlighted below along with the rationale behind each change.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;https://blog.mozilla.org/security/files/2015/10/combo-graph21.png&quot;&gt;&lt;img alt=&quot;&quot; class=&quot;alignnone wp-image-2045 size-full&quot; height=&quot;914&quot; src=&quot;https://blog.mozilla.org/security/files/2015/10/combo-graph21.png&quot; width=&quot;1518&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
+ &lt;h3&gt;Change to DV Certificate treatment in the address bar&lt;/h3&gt;
+ &lt;p&gt;Color and iconography is commonly used today to communicate to users when a site is secure. The most widely used patterns are coloring a lock icon and parts of the address bar green. This treatment has a straightforward rationale given green = good in most cultures. Firefox has historically used two different color treatments for the lock icon – a gray lock for &lt;a href=&quot;https://en.wikipedia.org/wiki/Domain-validated_certificate&quot;&gt;Domain-validated (DV) certificates&lt;/a&gt; and a green lock for &lt;a href=&quot;https://en.wikipedia.org/wiki/Extended_Validation_Certificate&quot;&gt;Extended Validation (EV) certificates&lt;/a&gt;. The average user is likely not going to understand this color distinction between EV and DV certificates. The overarching message we want users to take from both certificate states is that their connection to the site is secure. We’re therefore updating the color of the lock when a DV certificate is used to match that of an EV certificate.&lt;/p&gt;
+ &lt;p&gt;Although the same green icon will be used, the UI for a site using EV certificates will continue to differ from a site using a DV certificate. Specifically, EV certificates are used when &lt;a href=&quot;https://en.wikipedia.org/wiki/Certificate_authority&quot;&gt;Certificate Authorities (CA)&lt;/a&gt; verify the owner of a domain. Hence, we will continue to include the organization name verified by the CA in the address bar.&lt;/p&gt;
+ &lt;h3&gt;Changes to Mixed Content Blocker UI on HTTPS sites&lt;/h3&gt;
+ &lt;p&gt;A second change we’re introducing addresses what happens when a page served over a secure connection contains &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Security/MixedContent&quot;&gt;Mixed Content&lt;/a&gt;. Firefox’s Mixed Content Blocker proactively blocks &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Security/MixedContent#Mixed_active_content&quot;&gt;Mixed Active Content&lt;/a&gt; by default. Users historically saw a &lt;a href=&quot;https://people.mozilla.org/~tvyas/FigureA.jpg&quot;&gt;shield icon&lt;/a&gt; when Mixed Active Content was blocked and were given the option to disable the protection.&lt;/p&gt;
+ &lt;p&gt;Since the Mixed Content state is closely tied to site security, the information should be communicated in one place instead of having two separate icons. Moreover, we have seen that the &lt;a href=&quot;https://telemetry.mozilla.org/new-pipeline/dist.html#!cumulative=0&amp;amp;end_date=2015-09-17&amp;amp;keys=__none__!__none__!__none__&amp;amp;max_channel_version=beta%252F41&amp;amp;measure=MIXED_CONTENT_UNBLOCK_COUNTER&amp;amp;min_channel_version=null&amp;amp;product=Firefox&amp;amp;sanitize=1&amp;amp;sort_keys=submissions&amp;amp;start_date=2015-08-11&amp;amp;table=0&amp;amp;trim=1&amp;amp;use_submission_date=0&quot;&gt;number of times users override mixed content protection&lt;/a&gt; is slim, and hence the need for dedicated mixed content iconography is diminishing. Firefox is also using the shield icon for another feature in &lt;a href=&quot;https://support.mozilla.org/en-US/kb/private-browsing-use-firefox-without-history&quot;&gt;Private Browsing Mode&lt;/a&gt; and we want to avoid making the iconography ambiguous.&lt;/p&gt;
+ &lt;p&gt;The updated design that ships with Firefox 42 combines the lock icon with a warning sign which represents Mixed Content. When Firefox blocks Mixed Active Content, we retain the green lock since the HTTP content is blocked and hence the site remains secure.&lt;/p&gt;
+ &lt;p&gt;For users who want to learn more about a site’s security state, we have added an informational panel to further explain differences in page security. This panel appears anytime a user clicks on the lock icon in the address bar.&lt;/p&gt;
+ &lt;p&gt;Previously users could &lt;a href=&quot;https://people.mozilla.org/~tvyas/FigureB.jpg&quot;&gt;click on the shield icon&lt;/a&gt; in the rare case they needed to override mixed content protection. With this new UI, users can still do this by clicking the arrow icon to expose more information about the site security, along with a disable protection button.&lt;/p&gt;
+ &lt;div class=&quot;wp-caption alignnone&quot; id=&quot;attachment_2034&quot; style=&quot;width: 557px;&quot;&gt;&lt;a href=&quot;https://blog.mozilla.org/security/files/2015/10/mixed-active-content-click-and-subpanel.png&quot;&gt;&lt;img alt=&quot;mixed active content click and subpanel&quot; class=&quot;wp-image-2034 &quot; height=&quot;176&quot; src=&quot;https://blog.mozilla.org/security/files/2015/10/mixed-active-content-click-and-subpanel.png&quot; width=&quot;547&quot; /&gt;&lt;/a&gt;&lt;p class=&quot;wp-caption-text&quot;&gt;Users can click the lock with warning icon and proceed to disable Mixed Content Protection.&lt;/p&gt;&lt;/div&gt;
+ &lt;h3&gt;&lt;/h3&gt;
+ &lt;h3&gt;Loading Mixed Passive Content on HTTPS sites&lt;/h3&gt;
+ &lt;p&gt;There is a second category of Mixed Content called &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Security/MixedContent#Mixed_passivedisplay_content&quot;&gt;Mixed Passive Content&lt;/a&gt;. Firefox does not block Mixed Passive Content by default. However, when it is loaded on an HTTPS page, we let the user know with iconography and text. In previous versions of Firefox, we used a gray warning sign to reflect this case.&lt;/p&gt;
+ &lt;p&gt;We have updated this iconography in Firefox 42 to a gray lock with a yellow warning sign. We degrade the lock from green to gray to emphasize that the site is no longer completely secure. In addition, we use a vibrant color for the warning icon to amplify that there is something wrong with the security state of the page.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;https://blog.mozilla.org/security/files/2015/10/mixed-passive-click1.png&quot;&gt;&lt;img alt=&quot;&quot; class=&quot;alignnone wp-image-2042 &quot; height=&quot;100&quot; src=&quot;https://blog.mozilla.org/security/files/2015/10/mixed-passive-click1-600x221.png&quot; width=&quot;268&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
+ &lt;p&gt;We also use this iconography when the certificate or TLS connection used by the website relies on deprecated cryptographic algorithms.&lt;/p&gt;
+ &lt;p&gt;The above changes will be rolled out in Firefox 42. Overall, the design improvements make it simpler for our users to understand whether or not their interactions with a site are secure.&lt;/p&gt;
+ &lt;h3&gt;Firefox Mobile&lt;/h3&gt;
+ &lt;p&gt;We have made similar changes to the site security indicators in Firefox for Android, which you can learn more about &lt;a href=&quot;https://support.mozilla.org/en-US/kb/mixed-content-blocker-firefox-android#w_how-do-i-know-if-a-page-has-mixed-content&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-26T05:58:29+00:00</dc:date>
+ <dc:creator>Tanvi Vyas</dc:creator>
+ </item>
+ <item rdf:about="https://blog.mozilla.org/?p=9166">
+ <title>The Mozilla Blog: Firefox Can Now Get Push Notifications From Your Favorite Sites</title>
+ <link>https://blog.mozilla.org/blog/2016/01/25/firefox-can-now-get-push-notifications-from-your-favorite-sites/</link>
+ <content:encoded>&lt;p&gt;UPDATED TO CLARIFY HOW TO MANAGE PUSH NOTIFICATIONS&lt;/p&gt;
+ &lt;p&gt;Firefox for Windows, Mac and Linux now lets you choose to receive push notifications from websites if you give them permission. This is similar to Web notifications, except now you can receive notifications for websites even when they’re not loaded in a tab. This is super useful for websites like email, weather, social networks and shopping, which you might check frequently for updates.&lt;/p&gt;
+ &lt;p&gt;You can manage your notifications in the Control Center by clicking the green lock icon on the left side of the address bar. You can learn more about how to manage push notifications&lt;a href=&quot;https://support.mozilla.org/en-US/kb/push-notifications-firefox?as=u&amp;amp;utm_source=inproduct#w_upgraded-notifications&quot;&gt; here&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;&lt;b&gt;Push Notifications for Web Developers&lt;/b&gt;&lt;br /&gt;
+ To make this functionality possible, Mozilla helped establish the Web Push W3C standard that’s gaining momentum across the Web. We also continue to explore the new design pattern known as&lt;a href=&quot;https://blog.mozilla.org/futurereleases/2015/11/17/extending-the-webs-capabilities-in-firefox-and-beyond/&quot;&gt; Progressive Web Apps&lt;/a&gt;. If you’re a developer who wants to implement push notifications on your site, you can learn more in this&lt;a href=&quot;https://hacks.mozilla.org/2016/01/web-push-arrives-in-firefox-44/&quot;&gt; Hacks blog post&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;&lt;b&gt;More information:&lt;/b&gt;&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Download&lt;a href=&quot;https://www.mozilla.org/firefox/new/&quot;&gt; Firefox for Windows, Mac, Linux&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Release Notes for&lt;a href=&quot;https://www.mozilla.org/firefox/44.0/releasenotes/&quot;&gt; Firefox for Windows, Mac, Linux&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Download&lt;a href=&quot;https://play.google.com/store/apps/details?id=org.mozilla.firefox&amp;amp;referrer=utm_source%3Dmozilla%26utm_medium&quot;&gt; Firefox for Android&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Release Notes for&lt;a href=&quot;https://www.mozilla.org/firefox/android/44.0/releasenotes/&quot;&gt; Firefox for Android&lt;/a&gt;&lt;/li&gt;
+ &lt;/ul&gt;</content:encoded>
+ <dc:date>2016-01-26T01:56:50+00:00</dc:date>
+ <dc:creator>Mozilla</dc:creator>
+ </item>
+ <item rdf:about="http://benoitgirard.wordpress.com/?p=651">
+ <title>Benoit Girard: Using RecordReplay to investigate intermittent oranges</title>
+ <link>https://benoitgirard.wordpress.com/2016/01/25/using-recordreplay-to-investigate-intermittent-oranges/</link>
+ <content:encoded>&lt;p&gt;This is a quick write up to summarize my, and Jeff’s, experience, using RR to debug a &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1226748&quot;&gt;fairly rare intermittent reftest failure&lt;/a&gt;. There’s still a lot of be learned about how to use RR effectively so I’m hoping sharing this will help others.&lt;/p&gt;
+ &lt;h3&gt;Finding the root of the bad pixel&lt;/h3&gt;
+ &lt;p&gt;First given a offending pixel I was able to set a breakpoint on it using &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Hacking_Tips#rr_with_reftest&quot;&gt;these instructions&lt;/a&gt;. Next using &lt;a href=&quot;https://github.com/jrmuizel/rr-dataflow&quot;&gt;rr-dataflow&lt;/a&gt; I was able to step from the offending bad pixel to the display item responsible for this pixel. Let me emphasize this for a second since it’s incredibly impressive. rr + rr-dataflow allows you to go from a buffer, through an intermediate surface, to the compositor on another thread, through another intermediate surface, back to the main thread and eventually back to the relevant display item. All of this was automated except for when the two pixels are blended together which is logically ambiguous. The speed at which rr was able to reverse continue through this execution was very impressive!&lt;/p&gt;
+ &lt;p&gt;Here’s the trace of this part: &lt;a href=&quot;https://gist.github.com/bgirard/e707e9b97556b500d9ae&quot;&gt;rr-trace-reftest-pixel-origin&lt;/a&gt;&lt;/p&gt;
+ &lt;h3&gt;Understanding the decoding step&lt;/h3&gt;
+ &lt;p&gt;From here I started comparing a replay of a failing test and a non failing step and it was clear that the DisplayList was different. In one we have a nsDisplayBackgroundColor in the other we don’t. From here I was able to step through the decoder and compare the sequence. This was very useful in ruling out possible theories. It was easy to step forward and backwards in the good and bad replay debugging sessions to test out various theories about race conditions and understanding at which part of the decode process the image was rejected. It turned out that we sent two decodes, one for the metadata that is used to sized the frame tree and the other one for the image data itself.&lt;/p&gt;
+ &lt;h3&gt;Comparing the frame tree&lt;/h3&gt;
+ &lt;p&gt;In hindsight, it would have been more effective to start debugging this test by looking at the frame tree (and I imagine for other tests looking at the display list and layer tree) first would have been a quicker start. It works even better if you have a good and a bad trace to compare the difference in the frame tree. From here, I found that the difference in the layer tree came from a change hint that wasn’t guaranteed to come in before the draw.&lt;/p&gt;
+ &lt;p&gt;The problem is now well understood: When we do a sync decode on reftest draw, if there’s an image error we wont flush the style hints since we’re already too deep in the painting pipeline.&lt;/p&gt;
+ &lt;h3&gt;Take away&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Finding the root cause of a bad pixel is very easy, and fast, to do using rr-dataflow.&lt;/li&gt;
+ &lt;li&gt;However it might be better to look for obvious frame tree/display list/layer tree difference(s) first.&lt;/li&gt;
+ &lt;li&gt;Debugging a replay is a lot simpler then debugging against non-determinist re-runs and a lot less frustrating too.&lt;/li&gt;
+ &lt;li&gt;rr is really useful for race conditions, especially rare ones.&lt;/li&gt;
+ &lt;/ul&gt;&lt;br /&gt; &lt;a href=&quot;http://feeds.wordpress.com/1.0/gocomments/benoitgirard.wordpress.com/651/&quot; rel=&quot;nofollow&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; src=&quot;http://feeds.wordpress.com/1.0/comments/benoitgirard.wordpress.com/651/&quot; /&gt;&lt;/a&gt; &lt;img alt=&quot;&quot; border=&quot;0&quot; height=&quot;1&quot; src=&quot;https://pixel.wp.com/b.gif?host=benoitgirard.wordpress.com&amp;amp;blog=12112851&amp;amp;post=651&amp;amp;subd=benoitgirard&amp;amp;ref=&amp;amp;feed=1&quot; width=&quot;1&quot; /&gt;</content:encoded>
+ <dc:date>2016-01-25T22:16:01+00:00</dc:date>
+ <dc:creator>benoitgirard</dc:creator>
+ </item>
+ <item rdf:about="http://blog.servo.org/2016/01/25/twis-48/">
+ <title>The Servo Blog: These Weeks In Servo 48</title>
+ <link>http://blog.servo.org/2016/01/25/twis-48/</link>
+ <content:encoded>&lt;p&gt;In the &lt;a href=&quot;https://github.com/pulls?page=1&amp;amp;q=is%3Apr+is%3Amerged+closed%3A2016-01-11..2016-01-25+user%3Aservo&quot;&gt;last two weeks&lt;/a&gt;, we landed 130 PRs in the Servo organization’s repositories.&lt;/p&gt;
+
+ &lt;p&gt;After months of work by vlad and many others, Windows support &lt;a href=&quot;https://github.com/servo/servo/pull/9385&quot;&gt;landed&lt;/a&gt;! Thanks to everyone who contributed fixes, tests, reviews, and even encouragement (or impatience!) to help us make this happen.&lt;/p&gt;
+
+ &lt;h3 id=&quot;notable-additions&quot;&gt;Notable Additions&lt;/h3&gt;
+
+ &lt;ul&gt;
+ &lt;li&gt;nikki &lt;a href=&quot;https://github.com/servo/servo/pull/9391&quot;&gt;added&lt;/a&gt; tests and support for checking the Fetch redirect count&lt;/li&gt;
+ &lt;li&gt;glennw &lt;a href=&quot;https://github.com/servo/servo/pull/9359&quot;&gt;implemented&lt;/a&gt; horizontal scrolling with arrow keys&lt;/li&gt;
+ &lt;li&gt;simon &lt;a href=&quot;https://github.com/servo/servo/pull/9333&quot;&gt;created&lt;/a&gt; a script that parses all of the CSS properties parsed by Servo&lt;/li&gt;
+ &lt;li&gt;ms2ger &lt;a href=&quot;https://github.com/servo/servo/pull/9293&quot;&gt;removed&lt;/a&gt; the legacy reftest framework&lt;/li&gt;
+ &lt;li&gt;fernando &lt;a href=&quot;https://github.com/servo/crowbot/pull/33&quot;&gt;made&lt;/a&gt; crowbot able to rejoin IRC after it accidentally floods the channel&lt;/li&gt;
+ &lt;li&gt;jack &lt;a href=&quot;https://github.com/servo/saltfs/pull/193&quot;&gt;added&lt;/a&gt; testing the &lt;code&gt;geckolib&lt;/code&gt; target to our CI&lt;/li&gt;
+ &lt;li&gt;antrik &lt;a href=&quot;https://github.com/servo/ipc-channel/pull/25&quot;&gt;fixed&lt;/a&gt; transfer corruption in ipc-channel on 32-bit&lt;/li&gt;
+ &lt;li&gt;valentin &lt;a href=&quot;https://github.com/servo/rust-url/pull/119&quot;&gt;added&lt;/a&gt; and simon &lt;a href=&quot;https://github.com/servo/rust-url/pull/152&quot;&gt;extended&lt;/a&gt; IDNA support in rust-url, which is required for both web and Gecko compatibility&lt;/li&gt;
+ &lt;/ul&gt;
+
+ &lt;h3 id=&quot;new-contributors&quot;&gt;New Contributors&lt;/h3&gt;
+
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/Chandler&quot;&gt;Chandler Abraham&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/DarinM223&quot;&gt;Darin Minamoto&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/coder543&quot;&gt;Josh Leverette&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/shssoichiro&quot;&gt;Joshua Holmer&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/therealkbhat&quot;&gt;Kishor Bhat&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/MonsieurLanza&quot;&gt;Lanza&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/mattkuo&quot;&gt;Matthew Kuo&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/waterlink&quot;&gt;Oleksii Fedorov&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/stspyder&quot;&gt;St.Spyder&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/vvuk&quot;&gt;Vladimir Vukicevic&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/apopiak&quot;&gt;apopiak&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/askalski&quot;&gt;askalski&lt;/a&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+
+ &lt;h3 id=&quot;screenshot&quot;&gt;Screenshot&lt;/h3&gt;
+
+ &lt;p&gt;Screencast of this post being upvoted on reddit… from Windows!&lt;/p&gt;
+
+ &lt;p&gt;&lt;img alt=&quot;(screencast)&quot; src=&quot;http://blog.servo.org/images/upvote-windows.gif&quot; title=&quot;Screencast of upvoting on Reddit on Windows.&quot; /&gt;&lt;/p&gt;
+
+ &lt;h3 id=&quot;meetings&quot;&gt;Meetings&lt;/h3&gt;
+
+ &lt;p&gt;We had a &lt;a href=&quot;https://github.com/servo/servo/wiki/Meeting-2016-01-11&quot;&gt;meeting&lt;/a&gt; on some CI-related woes, documenting tags and mentoring, and dependencies for the style subsystem.&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-25T20:30:00+00:00</dc:date>
+ </item>
+ <item rdf:about="https://air.mozilla.org/mozilla-weekly-project-meeting-20160125/">
+ <title>Air Mozilla: Mozilla Weekly Project Meeting, 25 Jan 2016</title>
+ <link>https://air.mozilla.org/mozilla-weekly-project-meeting-20160125/</link>
+ <content:encoded>&lt;p&gt;
+ &lt;img alt=&quot;Mozilla Weekly Project Meeting&quot; class=&quot;wp-post-image&quot; height=&quot;90&quot; src=&quot;https://air.cdn.mozilla.net/media/cache/e9/4f/e94fbd7f8df916c75a60e63a85b9168c.png&quot; width=&quot;160&quot; /&gt;
+ The Monday Project Meeting
+ &lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-25T19:00:00+00:00</dc:date>
+ <dc:creator>Air Mozilla</dc:creator>
+ </item>
+ <item rdf:about="http://blog.mozilla.org/community/?p=2292">
+ <title>About:Community: Firefox 44 new contributors</title>
+ <link>http://blog.mozilla.org/community/2016/01/25/firefox-44-new-contributors/</link>
+ <content:encoded>&lt;p&gt;With the release of Firefox 44, we are pleased to welcome the &lt;strong&gt;28 developers&lt;/strong&gt; who contributed their first code change to Firefox in this release, &lt;strong&gt;23&lt;/strong&gt; of whom were brand new volunteers! Please join us in thanking each of these diligent and enthusiastic individuals, and take a look at their contributions:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;mkm: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1208124&quot;&gt;1208124&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Aditya Motwani: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1209087&quot;&gt;1209087&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Aniket Vyas: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1197309&quot;&gt;1197309&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1197315&quot;&gt;1197315&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Chirath R: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1216941&quot;&gt;1216941&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Christiane Ruetten: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1209091&quot;&gt;1209091&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Fernando Campo: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1199815&quot;&gt;1199815&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Grisha Pushkov: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=994555&quot;&gt;994555&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Guang-De Lin: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1150305&quot;&gt;1150305&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Hassen ben tanfous: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1074804&quot;&gt;1074804&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Helen V. Holmes: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1205046&quot;&gt;1205046&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Henrik Tjäder: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1161698&quot;&gt;1161698&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1209912&quot;&gt;1209912&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Johann Hofmann: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1192432&quot;&gt;1192432&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1198405&quot;&gt;1198405&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1204072&quot;&gt;1204072&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Kapeel Sable: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1212171&quot;&gt;1212171&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Manav Batra: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1202618&quot;&gt;1202618&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1212280&quot;&gt;1212280&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1214626&quot;&gt;1214626&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Manuel Casas Barrado: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1172662&quot;&gt;1172662&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1193674&quot;&gt;1193674&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1200693&quot;&gt;1200693&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1203298&quot;&gt;1203298&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1205684&quot;&gt;1205684&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1212331&quot;&gt;1212331&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1212338&quot;&gt;1212338&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1214582&quot;&gt;1214582&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Matt Howell: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1208626&quot;&gt;1208626&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Matthew Turnbull: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1213620&quot;&gt;1213620&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Olivier Yiptong: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1210936&quot;&gt;1210936&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1210940&quot;&gt;1210940&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1213078&quot;&gt;1213078&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Piotr Tworek: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1209446&quot;&gt;1209446&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Rocik: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1070719&quot;&gt;1070719&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Roland Sako: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1207733&quot;&gt;1207733&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Ronald Claveau: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1207266&quot;&gt;1207266&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Sanchit Nevgi: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1205181&quot;&gt;1205181&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Shaif Chowdhury: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1185606&quot;&gt;1185606&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1208121&quot;&gt;1208121&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Shubham Jain: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1208470&quot;&gt;1208470&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1208705&quot;&gt;1208705&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Stanislas Daniel Claude Dolcini: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1147197&quot;&gt;1147197&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Stephanie Ouillon: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1178533&quot;&gt;1178533&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1201626&quot;&gt;1201626&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Tim Huang: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1181489&quot;&gt;1181489&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;simplyblue24: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1218204&quot;&gt;1218204&lt;/a&gt;&lt;/li&gt;
+ &lt;/ul&gt;</content:encoded>
+ <dc:date>2016-01-25T16:21:33+00:00</dc:date>
+ <dc:creator>Josh Matthews</dc:creator>
+ </item>
+ <item rdf:about="tag:literaci.es,2014:Post/digital-skills-curriculum">
+ <title>Doug Belshaw: 3 things to consider when designing a digital skills framework</title>
+ <link>http://literaci.es/digital-skills-curriculum</link>
+ <content:encoded>&lt;p&gt;&lt;img alt=&quot;Learning to credential&quot; src=&quot;http://bryanmmathers.com/wp-content/uploads/2016/01/learning-to-credential.png&quot; /&gt;&lt;/p&gt;
+
+ &lt;p&gt;The image above was created by &lt;a href=&quot;http://bryanmmathers.com/learning-to-credential&quot; rel=&quot;nofollow&quot;&gt;Bryan Mathers&lt;/a&gt; for our &lt;a href=&quot;https://goo.gl/QqwUKP&quot; rel=&quot;nofollow&quot;&gt;presentation&lt;/a&gt; at &lt;a href=&quot;http://bettshow.com&quot; rel=&quot;nofollow&quot;&gt;BETT&lt;/a&gt; last week. It shows the way that, in broad brushstrokes, learning design &lt;em&gt;should&lt;/em&gt; happen. Before microcredentials such as &lt;a href=&quot;http://openbadges.org&quot; rel=&quot;nofollow&quot;&gt;Open Badges&lt;/a&gt; this was a difficult thing to do as both the credential and the assessment are usually given to educators. The flow tends to go &lt;em&gt;backwards&lt;/em&gt; from credentials instead of forwards from what we want people to learn.&lt;/p&gt;
+
+ &lt;p&gt;But what if you really &lt;em&gt;were&lt;/em&gt; starting from scratch? How could you design a digital skills framework that contains knowledge, skills, and behaviours worth learning? Having written my &lt;a href=&quot;http://neverendingthesis.com&quot; rel=&quot;nofollow&quot;&gt;thesis&lt;/a&gt; on digital literacies and led Mozilla’s &lt;a href=&quot;https://teach.mozilla.org/activities/web-literacy/&quot; rel=&quot;nofollow&quot;&gt;Web Literacy Map&lt;/a&gt; for a couple of years, I’ve got some suggestions. &lt;/p&gt;
+ &lt;h3&gt;
+ &lt;a class=&quot;head_anchor&quot; href=&quot;http://literaci.es/feed#1-define-your-audience&quot; name=&quot;1-define-your-audience&quot; rel=&quot;nofollow&quot;&gt; &lt;/a&gt;1. Define your audience&lt;/h3&gt;
+ &lt;p&gt;One of the most important things to define is who your audience is for your digital skills framework. Is it for learners to read? Who are they? How old are they? Are you excluding anyone on purpose? Why / why not?&lt;/p&gt;
+
+ &lt;p&gt;You might want to do some research and work around &lt;a href=&quot;https://en.wikipedia.org/wiki/Persona_(user_experience)&quot; rel=&quot;nofollow&quot;&gt;user personas&lt;/a&gt; as part of a user-centred design approach. This ensures you’re designing for real people instead of figments of your imagination (or, worse still, in line with your prejudices).&lt;/p&gt;
+
+ &lt;p&gt;It’s also good practice to make the language used in the skills framework as precise as possible. Jargon is technical language used for the sake of it. There may be times when it’s impossible not to use a word (e.g. ’&lt;a href=&quot;https://en.wikipedia.org/wiki/Meme&quot; rel=&quot;nofollow&quot;&gt;meme&lt;/a&gt;’). If you do this then link to a definition or include a glossary. It’s also useful to check the ‘reading level’ of your framework and, if you really want a challenge, try using &lt;a href=&quot;http://splasho.com/upgoer5/&quot; rel=&quot;nofollow&quot;&gt;Up-Goer Five&lt;/a&gt; language.&lt;/p&gt;
+ &lt;h3&gt;
+ &lt;a class=&quot;head_anchor&quot; href=&quot;http://literaci.es/feed#2-focus-on-verbs&quot; name=&quot;2-focus-on-verbs&quot; rel=&quot;nofollow&quot;&gt; &lt;/a&gt;2. Focus on verbs&lt;/h3&gt;
+ &lt;p&gt;It’s extremely easy, when creating a framework for learning, to fall into the 'knowledge trap’. Our aim when creating the raw materials from which someone can build a curriculum is to focus on &lt;em&gt;action&lt;/em&gt;. Knowledge should make a difference in practice.&lt;/p&gt;
+
+ &lt;p&gt;One straightforward way to ensure that you’re focusing on action rather than head knowledge is to use &lt;strong&gt;verbs&lt;/strong&gt; when constructing your digital skills framework. If you’re familiar with &lt;a href=&quot;https://en.wikipedia.org/wiki/Bloom%27s_taxonomy&quot; rel=&quot;nofollow&quot;&gt;Bloom’s Taxonomy&lt;/a&gt;, then you may find &lt;a href=&quot;http://byrdseed.com/differentiator/&quot; rel=&quot;nofollow&quot;&gt;The Differentiator&lt;/a&gt; useful. This pairs verbs with the various levels of Bloom’s.&lt;/p&gt;
+ &lt;h3&gt;
+ &lt;a class=&quot;head_anchor&quot; href=&quot;http://literaci.es/feed#3-add-version-numbers&quot; name=&quot;3-add-version-numbers&quot; rel=&quot;nofollow&quot;&gt; &lt;/a&gt;3. Add version numbers&lt;/h3&gt;
+ &lt;p&gt;A framework needs to be a living, breathing thing. It should be subject to revision and updated often. For this reason, you should add version numbers to your documentation. Ideally, the latest version should be at a canonical URL and you should archive previous versions to static URLs. &lt;/p&gt;
+
+ &lt;p&gt;I would also advise releasing the first version of your framework not as 'version 1.0’ but as 'v0.1’. This shows that you’re willing for others to provide input, that there will be further versions, and that you know you haven’t got it right first time (and forevermore). &lt;/p&gt;
+
+ &lt;hr /&gt;
+
+ &lt;p&gt;&lt;strong&gt;Questions? Comments?&lt;/strong&gt; Ask me on Twitter (&lt;a href=&quot;http://twitter.com/dajbelshaw&quot; rel=&quot;nofollow&quot;&gt;@dajbelshaw&lt;/a&gt;). I also consult around this kind of thing, so hit me up on &lt;a href=&quot;http://literaci.es/hello@dynamicskillset.com&quot; rel=&quot;nofollow&quot;&gt;hello@dynamicskillset.com&lt;/a&gt;&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-25T14:46:34+00:00</dc:date>
+ </item>
+ <item rdf:about="https://fundraising.mozilla.org/?p=800">
+ <title>Mozilla Fundraising: Why did you decide to donate today?</title>
+ <link>https://fundraising.mozilla.org/why-did-you-decide-to-donate-today/</link>
+ <content:encoded>This year, we asked some of our donors why they decided to donate to our end of year fundraising campaign. The Survey The Audience The survey was shown to a random sample of donors whose browser language was set to … &lt;a class=&quot;go&quot; href=&quot;https://fundraising.mozilla.org/why-did-you-decide-to-donate-today/&quot;&gt;Continue reading&lt;/a&gt;</content:encoded>
+ <dc:date>2016-01-25T13:31:34+00:00</dc:date>
+ <dc:creator>Adam Lofting</dc:creator>
+ </item>
+ <item rdf:about="http://www.agmweb.ca/robbie-burns">
+ <title>Andy McKay: Robbie Burns</title>
+ <link>http://www.agmweb.ca/2016-01-25-robbie-burns/</link>
+ <content:encoded>&lt;p&gt;Tonight is Robbie Burns night, in honour of that great Scottish poet. But tonight had me thinking about another night in my past.&lt;/p&gt;
+
+ &lt;p&gt;It was about 5 years ago, maybe less, I struggle to remember now. I was in the UK visiting family and my Dad was sick. Cancer and it's treatment is tough, you have good weeks, you have bad weeks and you have really fucking bad weeks. This was a good week and for some reason I was in the UK.&lt;/p&gt;
+
+ &lt;p&gt;Myself, my brother and my sister-in-law went down to see him that night. It was Robbie Burns night and that meant an excuse for haggis, really, truly terrible scotch, Scottish dancing and all that. There are many times when I look back at time with my Dad in those last few years. This was definitely one of those times. He was my Dad at his best, cracking jokes and having fun. Living life to the absolute fullest, while you still have that chance.&lt;/p&gt;
+
+ &lt;p&gt;We had a great night. That ended way too soon.&lt;/p&gt;
+
+ &lt;p&gt;Not long after that the cancer came back and that was that.&lt;/p&gt;
+
+ &lt;p&gt;But suddenly tonight, in a bar in Portland I had these memories of my Dad in a waistcoat cracking jokes and having fun on Robbie Burns night. No-one else in the bar seemed to know what night it was. You'd think Robbie Burns night might get a little bit more appreciation, but hey.&lt;/p&gt;
+
+ &lt;p&gt;In the many years I've been running this blog I've never written about my Dad passing away. Here's the first time. I miss him.&lt;/p&gt;
+
+ &lt;p&gt;Hey Robbie Burns? Thanks for making me remember that night.&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-25T08:00:00+00:00</dc:date>
+ </item>
+ <item rdf:about="tag:this-week-in-rust.org,2016-01-25:blog/2016/01/25/this-week-in-rust-115/">
+ <title>This Week In Rust: This Week in Rust 115</title>
+ <link>http://this-week-in-rust.org/blog/2016/01/25/this-week-in-rust-115/</link>
+ <content:encoded>&lt;p&gt;Hello and welcome to another issue of &lt;em&gt;This Week in Rust&lt;/em&gt;!
+ &lt;a href=&quot;http://rust-lang.org&quot;&gt;Rust&lt;/a&gt; is a systems language pursuing the trifecta:
+ safety, concurrency, and speed. This is a weekly summary of its progress and
+ community. Want something mentioned? Tweet us at &lt;a href=&quot;https://twitter.com/ThisWeekInRust&quot;&gt;@ThisWeekInRust&lt;/a&gt; or &lt;a href=&quot;mailto:corey@octayn.net?subject=This%20Week%20in%20Rust%20Suggestion&quot;&gt;send us an
+ email&lt;/a&gt;!
+ Want to get involved? &lt;a href=&quot;https://github.com/rust-lang/rust/blob/master/CONTRIBUTING.md&quot;&gt;We love
+ contributions&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;&lt;em&gt;This Week in Rust&lt;/em&gt; is openly developed &lt;a href=&quot;https://github.com/cmr/this-week-in-rust&quot;&gt;on GitHub&lt;/a&gt;.
+ If you find any errors in this week's issue, &lt;a href=&quot;https://github.com/cmr/this-week-in-rust/pulls&quot;&gt;please submit a PR&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;This week's edition was edited by: &lt;a href=&quot;https://github.com/nasa42&quot;&gt;nasa42&lt;/a&gt;, &lt;a href=&quot;https://github.com/brson&quot;&gt;brson&lt;/a&gt;, and &lt;a href=&quot;https://github.com/llogiq&quot;&gt;llogiq&lt;/a&gt;.&lt;/p&gt;
+ &lt;h3&gt;Updates from Rust Community&lt;/h3&gt;
+ &lt;h4&gt;News &amp;amp; Blog Posts&lt;/h4&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;img alt=&quot;balloon&quot; class=&quot;emoji&quot; src=&quot;https://cdn.discourse.org/business/images/emoji/emoji_one/balloon.png?v=0&quot; title=&quot;:balloon:&quot; /&gt;&lt;img alt=&quot;tada&quot; class=&quot;emoji&quot; src=&quot;https://cdn.discourse.org/business/images/emoji/emoji_one/tada.png?v=0&quot; title=&quot;:tada:&quot; /&gt; &lt;a href=&quot;http://blog.rust-lang.org/2016/01/21/Rust-1.6.html&quot;&gt;Announcing Rust 1.6&lt;/a&gt;. &lt;img alt=&quot;tada&quot; class=&quot;emoji&quot; src=&quot;https://cdn.discourse.org/business/images/emoji/emoji_one/tada.png?v=0&quot; title=&quot;:tada:&quot; /&gt;&lt;img alt=&quot;balloon&quot; class=&quot;emoji&quot; src=&quot;https://cdn.discourse.org/business/images/emoji/emoji_one/balloon.png?v=0&quot; title=&quot;:balloon:&quot; /&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.poumeyrol.fr/2016/01/15/Awkward-zone/&quot;&gt;Rust, BigData and my laptop&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;[pdf]&lt;a href=&quot;https://cdn.rawgit.com/Gankro/thesis/master/thesis.pdf&quot;&gt;You can't spell trust without Rust&lt;/a&gt;. Analysis of the semantics and expressiveness of Rust’s type system.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.ncameron.org/blog/libmacro/&quot;&gt;Libmacro - an API for procedural macros to interact with the compiler&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.jonathanturner.org/2016/01/rust-and-blub-paradox.html&quot;&gt;Rust and the Blub Paradox&lt;/a&gt;. And the &lt;a href=&quot;http://www.jonathanturner.org/2016/01/rethinking-the-blub-paradox.html&quot;&gt;follow-up&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;[video] &lt;a href=&quot;https://www.youtube.com/channel/UC4mpLlHn0FOekNg05yCnkzQ/videos&quot;&gt;Ferris Makes Emulators&lt;/a&gt;. Live stream of Ferris developing a N64 emulator in Rust (also on &lt;a href=&quot;http://www.twitch.tv/ferrisstreamsstuff/profile&quot;&gt;Twitch&lt;/a&gt;).&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h4&gt;Notable New Crates &amp;amp; Project Updates&lt;/h4&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://areweconcurrentyet.com/&quot;&gt;Are we concurrent yet&lt;/a&gt;?&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/gfx-rs/gfx&quot;&gt;GFX&lt;/a&gt; epic rewrite for the Pipeline State Objects paradigm has &lt;a href=&quot;https://github.com/gfx-rs/gfx/pull/828&quot;&gt;landed&lt;/a&gt;, described &lt;a href=&quot;http://gfx-rs.github.io/2016/01/22/pso.html&quot;&gt;on the blog&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/mcarton/rust-herbie-lint&quot;&gt;Herbie&lt;/a&gt;. A rustc plugin to check for numerical instability.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://blog.piston.rs/2016/01/23/dynamo/&quot;&gt;Dynamo&lt;/a&gt;. A rusty dynamically typed scripting language.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/whitequark/rust-vnc&quot;&gt;rust-vnc&lt;/a&gt;. An implementation of VNC protocol, client state machine and a client.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;Updates from Rust Core&lt;/h3&gt;
+ &lt;p&gt;129 pull requests were &lt;a href=&quot;https://github.com/issues?q=is%3Apr+org%3Arust-lang+is%3Amerged+merged%3A2016-01-18..2016-01-25&quot;&gt;merged in the last week&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;See the &lt;a href=&quot;https://internals.rust-lang.org/t/triage-digest-mon-jan-25-2016/3111&quot;&gt;triage digest&lt;/a&gt; and &lt;a href=&quot;https://internals.rust-lang.org/t/subteam-reports-2016-01-22/3106&quot;&gt;subteam reports&lt;/a&gt; for more details.&lt;/p&gt;
+ &lt;h4&gt;Notable changes&lt;/h4&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30872&quot;&gt;Implement RFC 1252 expanding the OpenOptions structure&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/book/pull/58&quot;&gt;Book: First draft of 'ownership'&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/cargo/pull/2205&quot;&gt;Cargo: Add convenience syntax to install current crate&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/cargo/pull/2196&quot;&gt;Cargo: Introduce cargo metadata subcommand&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/cargo/pull/2081&quot;&gt;Cargo: Implement &lt;code&gt;cargo init&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/cargo/pull/2270&quot;&gt;Cargo: Emit a warning when manifest specifies empty dependency constraints&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/29520&quot;&gt;Change name when outputting staticlibs on Windows&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30998&quot;&gt;Make &lt;code&gt;btree_set::{IntoIter, Iter, Range}&lt;/code&gt; covariant&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30917&quot;&gt;Avoid bounds checking at &lt;code&gt;slice::binary_search&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30894&quot;&gt;&lt;code&gt;std::sync::mpsc&lt;/code&gt;: Add &lt;code&gt;fmt::Debug&lt;/code&gt; stubs&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30882&quot;&gt;resolve: Fix variant namespacing&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h4&gt;New Contributors&lt;/h4&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Adrian Heine&lt;/li&gt;
+ &lt;li&gt;Andrea Bedini&lt;/li&gt;
+ &lt;li&gt;Guillaume Bonnet&lt;/li&gt;
+ &lt;li&gt;Kamal Marhubi&lt;/li&gt;
+ &lt;li&gt;Keith Yeung&lt;/li&gt;
+ &lt;li&gt;Marc Bowes&lt;/li&gt;
+ &lt;li&gt;Martin&lt;/li&gt;
+ &lt;li&gt;mopp&lt;/li&gt;
+ &lt;li&gt;Olaf Buddenhagen&lt;/li&gt;
+ &lt;li&gt;Paul Dicker&lt;/li&gt;
+ &lt;li&gt;Peter Kolloch&lt;/li&gt;
+ &lt;li&gt;Stephen (Ziyun) Li&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h4&gt;Approved RFCs&lt;/h4&gt;
+ &lt;p&gt;Changes to Rust follow the Rust &lt;a href=&quot;https://github.com/rust-lang/rfcs#rust-rfcs&quot;&gt;RFC (request for comments)
+ process&lt;/a&gt;. These
+ are the RFCs that were approved for implementation this week:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1462&quot;&gt;Amendment to RFC 550: Add &lt;code&gt;[&lt;/code&gt; to the FOLLOW(ty) in macro future-proofing rules&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1320&quot;&gt;Amendment to RFC 1192: Amend &lt;code&gt;RangeInclusive&lt;/code&gt; to use an enum&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h4&gt;Final Comment Period&lt;/h4&gt;
+ &lt;p&gt;Every week &lt;a href=&quot;https://rust-lang.org/team.html&quot;&gt;the team&lt;/a&gt; announces the
+ 'final comment period' for RFCs and key PRs which are reaching a
+ decision. Express your opinions now. &lt;a href=&quot;https://github.com/rust-lang/rfcs/labels/final-comment-period&quot;&gt;This week's FCPs&lt;/a&gt; are:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/243&quot;&gt;Trait-based exception handling&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1361&quot;&gt;Improve Cargo target-specific dependencies&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1129&quot;&gt;Add a &lt;code&gt;IndexAssign&lt;/code&gt; trait that allows overloading &quot;indexed assignment&quot; expressions like &lt;code&gt;a[b] = c&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1196&quot;&gt;Allow eliding more type parameters&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1296&quot;&gt;Add an &lt;code&gt;alias&lt;/code&gt; attribute to &lt;code&gt;#[link]&lt;/code&gt; and &lt;code&gt;-l&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h4&gt;New RFCs&lt;/h4&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1477&quot;&gt;Add compiler support for generic atomic operations&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1478&quot;&gt;Translate undefined generic intrinsics to an LLVM &lt;code&gt;unreachable&lt;/code&gt; and a lint&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;Upcoming Events&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.meetup.com/opentechschool-berlin/&quot;&gt;1/27. OpenTechSchool Berlin: Rust Hack and Learn&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.meetup.com/Tokyo-Rust-Meetup/events/227871840/&quot;&gt;1/28. Tokyo Rust Meetup #2&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.meetup.com/Rust-Berlin/events/227321071/&quot;&gt;2/3. Rust Berlin: Leaf and Collenchyma&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.meetup.com/de/Rust-Cologne-Bonn/events/227534456/&quot;&gt;2/3. Rust Meetup in Cologne / Germany&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://www.eventbrite.com/e/mozilla-rust-seattle-meetup-tickets-12222326307?aff=erelexporg&quot;&gt;2/8. Seattle Rust Meetup&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.meetup.com/de-DE/Rust-Rhein-Main/events/228170051/&quot;&gt;2/12. Embedded Rust Workshop Frankfurt&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;If you are running a Rust event please add it to the &lt;a href=&quot;https://www.google.com/calendar/embed?src=apd9vmbc22egenmtu5l6c5jbfc%40group.calendar.google.com&quot;&gt;calendar&lt;/a&gt; to get
+ it mentioned here. Email &lt;a href=&quot;mailto:erick.tryzelaar@gmail.com&quot;&gt;Erick Tryzelaar&lt;/a&gt; or &lt;a href=&quot;mailto:banderson@mozilla.com&quot;&gt;Brian
+ Anderson&lt;/a&gt; for access.&lt;/p&gt;
+ &lt;h3&gt;fn work(on: RustProject) -&amp;gt; Money&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://maidsafe.net/rust_engineer.html&quot;&gt;Rust Engineer&lt;/a&gt; at MaidSafe.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://careers.mozilla.org/en-US/position/ozy21fwU&quot;&gt;Research Engineer - Servo&lt;/a&gt; at Mozilla.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://careers.mozilla.org/en-US/position/o0H41fww&quot;&gt;Senior Research Engineer - Rust&lt;/a&gt; at Mozilla.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://plv.mpi-sws.org/rustbelt/&quot;&gt;PhD and postdoc positions&lt;/a&gt; at MPI-SWS.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;&lt;em&gt;Tweet us at &lt;a href=&quot;https://twitter.com/ThisWeekInRust&quot;&gt;@ThisWeekInRust&lt;/a&gt; to get your job offers listed here!&lt;/em&gt;&lt;/p&gt;
+ &lt;h3&gt;Crate of the Week&lt;/h3&gt;
+ &lt;p&gt;This week's Crate of the Week is &lt;a href=&quot;https://github.com/phildawes/racer&quot;&gt;racer&lt;/a&gt; which powers code completion in all Rust development environments.&lt;/p&gt;
+ &lt;p&gt;Thanks to &lt;a href=&quot;https://users.rust-lang.org/users/stebalien&quot;&gt;Steven Allen&lt;/a&gt; for the suggestion.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;https://users.rust-lang.org/t/crate-of-the-week/2704&quot;&gt;Submit your suggestions for next week&lt;/a&gt;!&lt;/p&gt;
+ &lt;h3&gt;Quote of the Week&lt;/h3&gt;
+ &lt;blockquote&gt;
+ &lt;p&gt;Memory errors are fundamentally state errors, and Rust's move semantics, borrowing, and aliasing XOR mutating help enormously for me to reason about how my program changes state as it executes, to avoid accidental shared state and side effects at a distance. Rust more than any other language I know enables me to do compiler driven design. And internalizing its rules has helped me design better systems, even in other languages.&lt;/p&gt;
+ &lt;/blockquote&gt;
+ &lt;p&gt;— &lt;a href=&quot;https://www.reddit.com/r/rust/comments/4275gz/rust_and_the_blub_paradox/cz8akv9&quot;&gt;desiringmachines on /r/rust&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;Thanks to &lt;a href=&quot;https://users.rust-lang.org/users/dikaiosune&quot;&gt;dikaiosune&lt;/a&gt; for the suggestion.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;http://users.rust-lang.org/t/twir-quote-of-the-week/328&quot;&gt;Submit your quotes for next week&lt;/a&gt;!&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-25T05:00:00+00:00</dc:date>
+ <dc:creator>Corey Richardson</dc:creator>
+ </item>
+ <item rdf:about="tag:blogger.com,1999:blog-1015214236289077798.post-7056349209464984020">
+ <title>Cameron Kaiser: 38.6.0 available</title>
+ <link>http://tenfourfox.blogspot.com/2016/01/3860-available.html</link>
+ <content:encoded>TenFourFox 38.6.0 is available for testing (&lt;a href=&quot;https://sourceforge.net/projects/tenfourfox/files/38.6.0/&quot;&gt;downloads&lt;/a&gt;, &lt;a href=&quot;https://github.com/classilla/tenfourfox/wiki/Hashes&quot;&gt;hashes&lt;/a&gt;, &lt;a href=&quot;https://github.com/classilla/tenfourfox/wiki/ZZReleaseNotes3860&quot;&gt;release notes&lt;/a&gt;). I'm sorry it's been so quiet around here; I'm in the middle of a backbreaking Master's course, my last one before I'm finally done with the lousy thing, and I haven't had any time to start on 45 so far. 38.6 does have some other fixes in it, though: I think I found the last place where bookmark backups were being mistakenly saved in LZ4 based on Chris Trusch's report, and the problematic fonts on the iCloud login page are now blacklisted, so you should be able to login again. I can't do much more testing than that, however, since I don't use iCloud personally, so other lapses in font functionality will require the font URL and I'll add them to the blacklist in 38.7. The browser will go live Monday Pacific time as usual. (The temporary workaround is to set &lt;tt&gt;gfx.downloadable_fonts.enabled&lt;/tt&gt; to &lt;tt&gt;false&lt;/tt&gt;, and switch the setting back when you don't need it anymore.) &lt;p&gt;Speaking of, downloadable fonts were exactly the same problem on the Sun Ultra-3 laptop I've been refurbishing; Oracle still provides a free Solaris 10 build of 38ESR, but it crashes on web fonts for reasons I have yet to diagnose, so I just have them turned off. Yes, it really is a SPARC laptop, a rebranded Tadpole Viper, and I think the fastest one ever made in this form factor (a 1.2GHz UltraSPARC IIIi). It's pretty much what I expected the PowerBook G5 would have been -- hot, overthrottled and power-hungry -- but Tadpole actually built the thing and it's not a disaster, relatively speaking. There's no JIT in this Firefox build, the brand new battery gets only 70 minutes of runtime even with the CPU clock-skewed to hell, it stands a very good chance of rendering me sterile and/or medium rare if I actually use it in my lap and it had at least one sudden overtemp shutdown and pooped all over the filesystem, but between Firefox, Star Office and &lt;tt&gt;pkgsrc&lt;/tt&gt; I can actually use it. More on that for laughs in a future post. &lt;/p&gt;&lt;p&gt;It has been pointed out to me that Leopard Webkit has not made an update in over three months, so hopefully Tobias is still doing okay with his port.&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-23T06:02:00+00:00</dc:date>
+ <dc:creator>ClassicHasClass</dc:creator>
+ </item>
+ <item rdf:about="https://blog.mozilla.org/netpolicy/?p=907">
+ <title>Mozilla Privacy Blog: Addressing the Chilling Effect of Patent Damages</title>
+ <link>https://blog.mozilla.org/netpolicy/2016/01/22/addressing-the-chilling-effect-of-patent-damages/</link>
+ <content:encoded>&lt;p&gt;Last year, we unveiled the &lt;a href=&quot;https://www.mozilla.org/about/patents/license/&quot;&gt;Mozilla Open Software Patent License&lt;/a&gt; as part of our &lt;a href=&quot;https://www.mozilla.org/about/patents/&quot;&gt;Initiative&lt;/a&gt; to help limit the negative impacts that patents have on open source software. While those were an important first step for us, we continue to do more. This past Wednesday, Mozilla joined several other tech and software companies in filing an &lt;a href=&quot;https://blog.mozilla.org/netpolicy/files/2016/01/Halo-Stryker-Internet-Companies-brief.pdf&quot;&gt;amicus brief&lt;/a&gt; with the Supreme Court of the United States in the &lt;i&gt;Halo&lt;/i&gt; and &lt;i&gt;Stryker&lt;/i&gt; cases.&lt;/p&gt;
+ &lt;p&gt;In the brief, we urge the Court to limit the availability of treble damages. Treble damages are significant because they greatly increase the amount of money owed if a defendant is found to “willfully infringe†a patent. As a result, many open source projects and technology companies will refuse to look into or engage in discussions about patents, in order to avoid even a remote possibility of willful infringement. This makes it very hard to address the chilling effects that patents can have on open source software development, open innovation, and collaborative efforts.&lt;/p&gt;
+ &lt;p&gt;We hope that our brief will help the Court see how this legal standard has affected technology companies and persuade the Court to limit treble damages.&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-23T00:17:34+00:00</dc:date>
+ <dc:creator>Elvin Lee</dc:creator>
+ </item>
+ <item rdf:about="http://blog.mozilla.org/addons/?p=7640">
+ <title>Mozilla Addons Blog: Add-on Signing Update</title>
+ <link>https://blog.mozilla.org/addons/2016/01/22/add-on-signing-update/</link>
+ <content:encoded>&lt;p&gt;In Firefox 43, we made it a default requirement for add-ons to be signed. This requirement can be disabled by &lt;a href=&quot;https://wiki.mozilla.org/Addons/Extension_Signing#FAQ&quot;&gt;toggling a preference&lt;/a&gt; that was originally scheduled to be removed in Firefox 44 for release and beta versions (this preference will continue to be available in the Nightly, Developer, and ESR Editions of Firefox for the foreseeable future). &lt;/p&gt;
+ &lt;p&gt;We are delaying the removal of this preference to Firefox 46 for a couple of reasons: We’re adding a feature in Firefox 45 that allows &lt;a href=&quot;https://blog.mozilla.org/addons/2015/12/23/loading-temporary-add-ons/&quot;&gt;temporarily loading unsigned restartless add-ons&lt;/a&gt; in release, which will allow developers of those add-ons to use Firefox for testing, and we’d like this option to be available when we remove the preference. We also want to ensure that developers have adequate time to finish the transition to signed add-ons. &lt;/p&gt;
+ &lt;p&gt;The &lt;a href=&quot;https://wiki.mozilla.org/Addons/Extension_Signing#Timeline&quot;&gt;updated timeline&lt;/a&gt; is available on the signing wiki, and you can look up &lt;a href=&quot;https://wiki.mozilla.org/RapidRelease/Calendar&quot;&gt;release dates for Firefox versions&lt;/a&gt; on the releases wiki. Signing will be mandatory in the beta and release versions of Firefox from 46 onwards, at which point unbranded builds based on beta and release will be provided for testing.&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-22T22:40:59+00:00</dc:date>
+ <dc:creator>Kev Needham</dc:creator>
+ </item>
+ <item rdf:about="http://coopcoopbware.tumblr.com/post/137832199980">
+ <title>Chris Cooper: RelEng &amp; RelOps Weekly Highlights - January 22, 2016</title>
+ <link>http://coopcoopbware.tumblr.com/post/137832199980</link>
+ <content:encoded>&lt;p&gt;&lt;/p&gt;&lt;figure class=&quot;alignright&quot;&gt;&lt;a href=&quot;https://www.flickr.com/photos/proud2bcan8dn/1150097247/in/faves-19934681@N00/&quot; target=&quot;_blank&quot; title=&quot;wine-and-pies&quot;&gt;&lt;img alt=&quot;wine-and-pies&quot; src=&quot;https://farm2.staticflickr.com/1216/1150097247_2f11cb4c2d_z.jpg?zz=1&quot; width=&quot;200px&quot; /&gt;&lt;/a&gt;Releng: drinkin’ wine and makin’ pies.&lt;/figure&gt;It’s encouraging to see more progress this week on both the build/release promotion and TaskCluster migration fronts, our two major efforts for this quarter.&lt;p&gt;&lt;/p&gt;
+
+ &lt;p&gt;&lt;b&gt;Modernize infrastructure:&lt;/b&gt;&lt;/p&gt;
+ &lt;p&gt;In a continuing effort to enable faster, more reliable, and more easily-run tests for TaskCluster components, Dustin landed support for an in-memory, credential-free mock of Azure Table Storage in the &lt;a href=&quot;https://www.npmjs.com/package/azure-entities&quot; target=&quot;_blank&quot;&gt;azure-entities&lt;/a&gt; package. Together with the fake mock support he added to &lt;a href=&quot;https://github.com/djmitche/taskcluster-lib-testing&quot; target=&quot;_blank&quot;&gt;taskcluster-lib-testing&lt;/a&gt;, this allows tests for components like taskcluster-hooks to run without network access and without the need for any credentials, substantially decreasing the barrier to external contributions.&lt;/p&gt;
+
+ &lt;p&gt;All release promotion tasks are now signed by default. Thanks to Rail for his work here to help improve verifiability and chain-of-custody in our upcoming release process. (&lt;a href=&quot;https://bugzil.la/1239682&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/1239682&lt;/a&gt;)
+ Beetmover has been spotted in the wild! Jordan has been working on this new tool as part of our release promotion project. Beetmover helps move build artifacts from one place to another (generally between S3 buckets these days), but can also be extended to perform validation actions inline, e.g. checksums and anti-virus. (&lt;a href=&quot;https://bugzil.la/1225899&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/1225899&lt;/a&gt;)&lt;/p&gt;
+
+ &lt;p&gt;Dustin configured the “desktop-test†and “desktop-build†docker images to build automatically on push. That means that you can modify the Dockerfile under `testing/docker`, push to try, and have the try job run in the resulting image, all without pushing any images. This should enable much quicker iteration on tweaks to the docker images. Note, however, that updates to the base OS images (ubuntu1204-build and centos6-build) still require manual pushes.&lt;/p&gt;
+
+ &lt;p&gt;Mark landed Puppet code for base windows 10 support including secrets and ssh keys management.&lt;/p&gt;
+
+ &lt;p&gt;&lt;b&gt;Improve CI pipeline:&lt;/b&gt;&lt;/p&gt;
+
+ &lt;p&gt;Vlad and Amy repurposed 10 Windows XP machines as Windows 7 to improve the wait times in that test pool (&lt;a href=&quot;https://bugzil.la/1239785&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/1239785&lt;/a&gt;)
+ Armen and Joel have been working on porting the Gecko tests to run under TaskCluster, and have narrowed the failures down to the single digits. This puts us on-track to enable Linux debug builds and tests in TaskCluster as the canonical build/test process.&lt;/p&gt;
+
+ &lt;p&gt;&lt;b&gt;Release:&lt;/b&gt;&lt;/p&gt;
+
+ &lt;p&gt;Ben finished up work on enhanced Release Blob validation in Balrog (&lt;a href=&quot;https://bugzil.la/703040&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/703040&lt;/a&gt;), which makes it much more difficult to enter bad data into our update server.&lt;/p&gt;
+
+ &lt;p&gt;You may recall Mihai, our former intern who &lt;a href=&quot;http://coopcoopbware.tumblr.com/post/133490693210/welcome-back-mihai&quot; target=&quot;_blank&quot;&gt;we just hired back in November&lt;/a&gt;. Shortly after joining the team, he jumped into the &lt;a href=&quot;https://wiki.mozilla.org/ReleaseEngineering/Releaseduty&quot; target=&quot;_blank&quot;&gt;releaseduty&lt;/a&gt; rotation to provide much-needed extra bandwidth. The learning curve here is steep, but over the course of the Firefox 44 release cycle, he’s taken on more and more responsibility. He’s even volunteered to do releaseduty for the Firefox 45 release cycle as well. Perhaps the most impressive thing is that he’s also taken the time to update (or write) the releaseduty docs so that the next person who joins the rotation will be that much further ahead of the game. Thanks for your hard work here, Mihai!&lt;/p&gt;
+
+ &lt;p&gt;&lt;b&gt;Operational:&lt;/b&gt;&lt;/p&gt;
+
+ &lt;p&gt;Hal did some cleanup work to remove unused mozharness configs and directories from the build mercurial repos. These resources have long-since moved into the main mozilla-central tree. Hopefully this will make it easier for contributors to find the canonical copy! (&lt;a href=&quot;https://bugzil.la/1239003&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/1239003&lt;/a&gt;)&lt;/p&gt;
+
+ &lt;p&gt;&lt;b&gt;Hiring:&lt;/b&gt;&lt;/p&gt;
+
+ &lt;p&gt;We’re still hiring for a full-time &lt;a href=&quot;https://careers.mozilla.org/position/oi8b2fwn&quot; target=&quot;_blank&quot;&gt;Build &amp;amp; Release Engineer&lt;/a&gt;, and we are still accepting applications for &lt;a href=&quot;https://careers.mozilla.org/position/ofA51fwF&quot; target=&quot;_blank&quot;&gt;interns for 2016&lt;/a&gt;. Come join us!&lt;/p&gt;
+
+ &lt;p&gt;Well, I don’t know about you, but all that hard work makes me hungry for pie. See you next week!&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-22T20:49:38+00:00</dc:date>
+ </item>
+ <item rdf:about="https://air.mozilla.org/foundation-demos-january-22-2016/">
+ <title>Air Mozilla: Foundation Demos January 22 2016</title>
+ <link>https://air.mozilla.org/foundation-demos-january-22-2016/</link>
+ <content:encoded>&lt;p&gt;
+ &lt;img alt=&quot;Foundation Demos January 22 2016&quot; class=&quot;wp-post-image&quot; height=&quot;90&quot; src=&quot;https://air.cdn.mozilla.net/media/cache/1c/a0/1ca0b9b2609cdd4e6e3577a8c3df8cfc.jpg&quot; width=&quot;160&quot; /&gt;
+ Mozilla Foundation Demos January 22 2016
+ &lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-22T18:00:00+00:00</dc:date>
+ <dc:creator>Air Mozilla</dc:creator>
+ </item>
+ <item rdf:about="http://blog.mozilla.org/sumo/?p=3667">
+ <title>Support.Mozilla.Org: What’s up with SUMO – 22nd January</title>
+ <link>https://blog.mozilla.org/sumo/2016/01/22/whats-up-with-sumo-22nd-january/</link>
+ <content:encoded>&lt;p&gt;&lt;strong&gt;Hello, SUMO Nation!&lt;/strong&gt;&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;http://blog.mozilla.org/sumo/files/2016/01/sumo_logo.png&quot;&gt;&lt;img alt=&quot;sumo_logo&quot; class=&quot;aligncenter size-full wp-image-3670&quot; height=&quot;387&quot; src=&quot;http://blog.mozilla.org/sumo/files/2016/01/sumo_logo.png&quot; width=&quot;383&quot; /&gt;&lt;/a&gt;The third week of the new year is already behind us. Time flies when you’re not paying attention… What are you going to do this weekend? Let us know in the comments, if you feel like sharing :-) I hope to be in the mountains, getting some fresh (bracing) air, and enjoying nature.&lt;/p&gt;
+ &lt;h3&gt;&lt;strong class=&quot;username&quot;&gt;Welcome, new contributors!&lt;br /&gt;
+ &lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li class=&quot;author&quot;&gt;
+ &lt;div class=&quot;author&quot;&gt;&lt;a class=&quot;username&quot; href=&quot;https://support.mozilla.org/user/johnmwc2&quot; target=&quot;_blank&quot;&gt;johnmwc2&lt;/a&gt;&lt;/div&gt;
+ &lt;/li&gt;
+ &lt;li class=&quot;author&quot;&gt;&lt;a class=&quot;author-name&quot; href=&quot;https://support.mozilla.org/user/myanesp&quot; target=&quot;_blank&quot;&gt;myanesp&lt;/a&gt;&lt;/li&gt;
+ &lt;li class=&quot;author&quot;&gt;&lt;a class=&quot;author-name&quot; href=&quot;https://support.mozilla.org/user/Harish.A&quot; target=&quot;_blank&quot;&gt;Harish.A&lt;/a&gt;&lt;/li&gt;
+ &lt;li class=&quot;author&quot;&gt;&lt;a class=&quot;author-name&quot; href=&quot;https://support.mozilla.org/user/hoolibob&quot; target=&quot;_blank&quot;&gt;hoolibob&lt;/a&gt;&lt;/li&gt;
+ &lt;li class=&quot;author&quot;&gt;&lt;a class=&quot;author-name&quot; href=&quot;https://support.mozilla.org/user/Meteoro890&quot; target=&quot;_blank&quot;&gt;Meteoro890&lt;/a&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;div class=&quot;author&quot;&gt;If you just joined us, don’t hesitate – come over and &lt;a href=&quot;https://support.mozilla.org/forums/buddies&quot; target=&quot;_blank&quot;&gt;say “hi†in the forums!&lt;/a&gt;&lt;/div&gt;
+ &lt;div class=&quot;author&quot;&gt;&lt;/div&gt;
+ &lt;div class=&quot;author&quot;&gt;
+ &lt;h3&gt;&lt;strong&gt;Contributors of the week&lt;br /&gt;
+ &lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-z74z1rz89z69z76zbz72zz69zz67z9z82zniz71z&quot;&gt;&lt;a href=&quot;https://support.mozilla.org/user/safwan.rahman&quot; target=&quot;_blank&quot;&gt;Safwan&lt;/a&gt; for his work on the &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=619284&quot; target=&quot;_blank&quot;&gt;draft feature for l10n / KB editing&lt;/a&gt; – rock on!&lt;/span&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://support.mozilla.org/user/artist&quot; target=&quot;_blank&quot;&gt;Artist&lt;/a&gt; and &lt;a href=&quot;https://support.mozilla.org/user/pollti&quot; target=&quot;_blank&quot;&gt;Pollti&lt;/a&gt; for their the work on updating important articles for Focus with limited time – woot!&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid64&quot;&gt;
+ &lt;p&gt;&lt;strong&gt;&lt;span style=&quot;text-decoration: underline;&quot;&gt;We salute you!&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;author&quot;&gt;Don’t forget that if you are new to SUMO and someone helped you get started in a nice way you can &lt;a href=&quot;https://support.mozilla.org/forums/buddies/711364?last=65670&quot; target=&quot;_blank&quot;&gt;nominate them for the Buddy of the Month!&lt;/a&gt;&lt;/div&gt;
+ &lt;div class=&quot;author&quot;&gt;&lt;/div&gt;
+ &lt;/div&gt;
+ &lt;h3&gt;&lt;strong&gt;Most recent SUMO Community meeting&lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://public.etherpad-mozilla.org/p/sumo-2016-01-18&quot; target=&quot;_blank&quot;&gt;You can read the notes here&lt;/a&gt; (most of the staff members were AFK due to MLK Day in the US) and see the video on our &lt;a href=&quot;https://www.youtube.com/channel/UCaiposaIhA7HfMqH2NIciyA/videos&quot; target=&quot;_blank&quot;&gt;YouTube channel&lt;/a&gt; and &lt;a href=&quot;https://air.mozilla.org/search/?q=sumo&quot; target=&quot;_blank&quot;&gt;at AirMozilla&lt;/a&gt;.&lt;del&gt; &lt;/del&gt;&lt;del&gt;&lt;br /&gt;
+ &lt;/del&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;strong&gt;IMPORTANT: We are considering changing the way the meetings work. Help us figure out what’s best for you – join the discussion on the forums in this thread: &lt;a href=&quot;https://support.mozilla.org/en-US/forums/contributors/711752?last=67873&quot;&gt;(Monday) Community Meetings in 2016&lt;/a&gt;.&lt;/strong&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;&lt;strong&gt;The next SUMO Community meeting… &lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li style=&quot;text-align: left;&quot;&gt;is happening on &lt;a href=&quot;https://public.etherpad-mozilla.org/p/sumo-2016-01-25&quot; target=&quot;_blank&quot;&gt;Monday the 25th – join us&lt;/a&gt;!&lt;/li&gt;
+ &lt;li style=&quot;text-align: left;&quot;&gt;&lt;strong&gt;Reminder: if you want to add a discussion topic to the upcoming meeting agenda:&lt;/strong&gt;
+ &lt;ul&gt;
+ &lt;li style=&quot;text-align: left;&quot;&gt;Start a thread in the &lt;a href=&quot;https://support.mozilla.org/forums/contributors&quot; target=&quot;_blank&quot;&gt;Community Forums&lt;/a&gt;, so that everyone in the community can see what will be discussed and voice their opinion here before Monday (this will make it easier to have an efficient meeting).&lt;/li&gt;
+ &lt;li style=&quot;text-align: left;&quot;&gt;Please do so as soon as you can before the meeting, so that people have time to read, think, and reply (and also add it to the agenda).&lt;/li&gt;
+ &lt;li style=&quot;text-align: left;&quot;&gt;If you can, please attend the meeting in person (or via IRC), so we can follow up on your discussion topic during the meeting with your feedback.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;&lt;strong class=&quot;author-g-ivsra51ph44x461i&quot;&gt;Developers&lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://edwin.mozilla.io/t/sumo&quot; target=&quot;_blank&quot;&gt;You can see the current state of the backlog our developers are working on here&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://public.etherpad-mozilla.org/p/sumo-p-2016-01-21&quot; target=&quot;_blank&quot;&gt;The latest SUMO Platform meeting notes can be found here&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;Interested in learning how Kitsune (the engine behind SUMO) works? &lt;a href=&quot;http://kitsune.readthedocs.org/&quot; target=&quot;_blank&quot;&gt;Read more about it here&lt;/a&gt; and &lt;a href=&quot;https://github.com/mozilla/kitsune/&quot; target=&quot;_blank&quot;&gt;fork it on GitHub&lt;/a&gt;!&lt;/li&gt;
+ &lt;li&gt;We have a new link for promoting contributions to Kitsune’s code. Please use &lt;strong&gt;http://mzl.la/SUMOdev&lt;/strong&gt; whenever you want to show interested people to see what Kitsune is all about – thanks!&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;&lt;a href=&quot;http://blog.mozilla.org/sumo/files/2016/01/mission_developers.png&quot;&gt;&lt;img alt=&quot;mission_developers&quot; class=&quot;aligncenter size-full wp-image-3668&quot; height=&quot;406&quot; src=&quot;http://blog.mozilla.org/sumo/files/2016/01/mission_developers.png&quot; width=&quot;437&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
+ &lt;h3&gt;&lt;strong&gt;Social&lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Next week, there will be a kick-off meeting for the rethinking of Mozilla’s general support strategy through social networks. &lt;a href=&quot;https://support.mozilla.org/user/Madasan&quot; target=&quot;_blank&quot;&gt;Are you interested in taking part? Let Madalina know!&lt;/a&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;&lt;strong&gt;Community&lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;The NDA process and list is currently being reworked under the leadership of the Participation Team. Expect to see messaging on this subject in the coming days.&lt;/li&gt;
+ &lt;li&gt;
+ &lt;div class=&quot;title&quot;&gt;&lt;strong&gt;&lt;a href=&quot;https://support.mozilla.org/forums/contributors/711729?last=67763&quot;&gt;IMPORTANT: take a look at our Work Week Summary for Mozlando. We need your feedback for a few things there.&lt;/a&gt;&lt;/strong&gt;&lt;/div&gt;
+ &lt;/li&gt;
+ &lt;li&gt;Are you going to FOSDEM next week? Would you like to have a small SUMO-meetup? &lt;a href=&quot;https://support.mozilla.org/user/vesper&quot; target=&quot;_blank&quot;&gt;Let me know&lt;/a&gt;!&lt;/li&gt;
+ &lt;li&gt;
+ &lt;div class=&quot;title&quot;&gt;Ongoing reminder: if you think you can benefit from getting &lt;a href=&quot;https://wiki.mozilla.org/Community_Hardware&quot; target=&quot;_blank&quot;&gt;a second-hand device&lt;/a&gt; to help you with contributing to SUMO, you know where to find us.&lt;/div&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;&lt;a href=&quot;http://blog.mozilla.org/sumo/files/2016/01/hero_support.png&quot;&gt;&lt;img alt=&quot;hero_support&quot; class=&quot;aligncenter size-full wp-image-3669&quot; height=&quot;383&quot; src=&quot;http://blog.mozilla.org/sumo/files/2016/01/hero_support.png&quot; width=&quot;367&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
+ &lt;div class=&quot;&quot;&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid83&quot;&gt;
+ &lt;h3&gt;&lt;strong class=&quot;author-g-ivsra51ph44x461i&quot;&gt;Localization&lt;/strong&gt;&lt;/h3&gt;
+ &lt;/div&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid95&quot;&gt;
+ &lt;ul&gt;
+ &lt;li&gt;You can &lt;a href=&quot;https://support.mozilla.org/forums/l10n-forum/711781&quot; target=&quot;_blank&quot;&gt;read more about the recent “infrequent contributor survey†in this thread&lt;/a&gt;. In short: the good news is that we’re doing a good job at making it easy enough for everyone to contribute. The bad news – we’re not doing enough to make sure they know what to do after their first contribution. Expect some changes in the messaging for first-time contributors to the KB :-)&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1012384&quot; target=&quot;_blank&quot;&gt;Our magical l10n dashboards keep being magical&lt;/a&gt; ;-) Thank you for your patience. If you see any discrepancies between the number of localized articles and the percentage shown in the bar, file a bug!&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid75&quot;&gt;
+ &lt;h3&gt;&lt;strong&gt;Firefox&lt;br /&gt;
+ &lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;strong&gt;for Android&lt;/strong&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://support.mozilla.org/forums/contributors/711712?last=67653&quot;&gt;Learn more about Firefox 43 for Android from the official thread with release notes / issues / discussions&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;
+ &lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://support.mozilla.org/forums/contributors/711718?last=67822&quot;&gt;Reminder: Roland is sharing Firefox 44 for Android release notes / issues / discussions&lt;/a&gt; with everyone in the forum.&lt;/div&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;strong&gt;for Desktop&lt;/strong&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Heads up – next week should be release week! Keep your eyes peeled ;-)&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;strong&gt;for iOS&lt;/strong&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid85&quot;&gt;
+ &lt;ul class=&quot;list-bullet1&quot;&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-107uz69zz81zhz78z0z78zz84zz66zz76zz82zz77zj&quot;&gt;No news from the world of Firefox for iOS this week.&lt;/span&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+ &lt;p&gt;Thank you for reading all the way down here… More to come next week! You know where to find us, so see you around – keep rocking the open &amp;amp; helpful web!&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-22T17:43:56+00:00</dc:date>
+ <dc:creator>Michał</dc:creator>
+ </item>
+ <item rdf:about="https://air.mozilla.org/bay-area-rust-meetup-january-2016/">
+ <title>Air Mozilla: Bay Area Rust Meetup January 2016</title>
+ <link>https://air.mozilla.org/bay-area-rust-meetup-january-2016/</link>
+ <content:encoded>&lt;p&gt;
+ &lt;img alt=&quot;Bay Area Rust Meetup January 2016&quot; class=&quot;wp-post-image&quot; height=&quot;90&quot; src=&quot;https://air.cdn.mozilla.net/media/cache/87/4f/874f4abef76f55213d50e43d6417ed99.png&quot; width=&quot;160&quot; /&gt;
+ Bay Area Rust meetup for January 2016. Topics TBD.
+ &lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-22T03:00:00+00:00</dc:date>
+ <dc:creator>Air Mozilla</dc:creator>
+ </item>
+ <item rdf:about="https://blog.lizardwrangler.com/?p=3953">
+ <title>Mitchell Baker: Honored to Participate in New UN Panel on Women’s Economic Empowerment</title>
+ <link>http://blog.lizardwrangler.com/2016/01/22/honored-to-participate-in-new-un-panel-on-womens-economic-empowerment/</link>
+ <content:encoded>Women’s economic empowerment is necessary for many reasons. It is necessary to bring health, safety and opportunity to half of humanity. It is necessary to bring investment and health to families and communities. It is necessary to unlock economic growth and build more stable societies. Today the UN Secretary General Ban Ki-moon launched the first […]</content:encoded>
+ <dc:date>2016-01-22T02:45:58+00:00</dc:date>
+ <dc:creator>Mitchell Baker</dc:creator>
+ </item>
+ <item rdf:about="https://blog.mozilla.org/webdev/?p=4082">
+ <title>Mozilla WebDev Community: Beer and Tell – January 2016</title>
+ <link>https://blog.mozilla.org/webdev/2016/01/21/beer-and-tell-january-2016/</link>
+ <content:encoded>&lt;p&gt;Once a month, web developers from across the Mozilla Project get together to talk about our side projects and drink, an occurrence we like to call “Beer and Tellâ€.&lt;/p&gt;
+ &lt;p&gt;There’s a &lt;a href=&quot;https://wiki.mozilla.org/Webdev/Beer_And_Tell/January_2016&quot;&gt;wiki page available&lt;/a&gt; with a list of the presenters, as well as links to their presentation materials. There’s also a &lt;a href=&quot;https://air.mozilla.org/webdev-beer-and-tell-january-2016/&quot;&gt;recording available&lt;/a&gt; courtesy of Air Mozilla.&lt;/p&gt;
+ &lt;h3&gt;shobson: CSS-Only Disco Ball&lt;/h3&gt;
+ &lt;p&gt;First up was &lt;a href=&quot;https://mozillians.org/en-US/u/stephaniehobson/&quot;&gt;shobson&lt;/a&gt; with a cool demo of an &lt;a href=&quot;http://codepen.io/stephaniehobson/pen/ZGZBVW?editors=110&quot;&gt;animated disco ball made entirely with CSS&lt;/a&gt;. The demo uses a repeated radial gradient for the background, and linear gradients plus a border radius for the disco ball itself. The demo was made for use in shobson’s &lt;a href=&quot;https://www.youtube.com/watch?v=7poVasAQjos&quot;&gt;WordCamp talk&lt;/a&gt; about debugging CSS. A &lt;a href=&quot;http://stephaniehobson.ca/wordpress/2015/08/15/how-to-debug-css/&quot;&gt;blog post&lt;/a&gt; with notes from the talk is available as well.&lt;/p&gt;
+ &lt;h3&gt;craigcook: Proton – A CSS Framework for Prototyping&lt;/h3&gt;
+ &lt;p&gt;Next was &lt;a href=&quot;https://mozillians.org/en-US/u/craigcook/&quot;&gt;craigcook&lt;/a&gt;, who presented &lt;a href=&quot;http://craigcook.github.io/proton/&quot;&gt;Proton&lt;/a&gt;. It’s a CSS framework that is intentionally ugly to encourage use for prototypes only. Unlike other CSS frameworks, the temptation to reuse the classes from the framework in your final page doesn’t occur, which helps avoid the presentational classes that plague sites built using a framework normally.&lt;/p&gt;
+ &lt;p&gt;Proton’s website includes an overview of the layout and components provided, as well as examples of prototypes made using the framework.&lt;/p&gt;
+ &lt;hr /&gt;
+ &lt;p&gt;If you’re interested in attending the next Beer and Tell, sign up for the &lt;a href=&quot;https://lists.mozilla.org/listinfo/dev-webdev&quot;&gt;dev-webdev@lists.mozilla.org mailing list&lt;/a&gt;. An email is sent out a week beforehand with connection details. You could even add yourself to the wiki and show off your side-project!&lt;/p&gt;
+ &lt;p&gt;See you next month!&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-21T18:56:46+00:00</dc:date>
+ <dc:creator>Michael Kelly</dc:creator>
+ </item>
+ <item rdf:about="http://blog.mozilla.org/community/?p=2287">
+ <title>About:Community: This Month at Mozilla</title>
+ <link>http://blog.mozilla.org/community/2016/01/21/this-month-at-mozilla/</link>
+ <content:encoded>&lt;p style=&quot;text-align: center;&quot;&gt;&lt;em&gt;A lot of exciting things are happening with Participation at Mozilla this month. Here’s a quick round-up of some of the things that are going on!&lt;/em&gt;&lt;/p&gt;
+ &lt;h3&gt;&lt;b&gt;Mozillians Profiles Got a Facelift: &lt;/b&gt;&lt;/h3&gt;
+ &lt;p&gt;Since the start of this year, the Participation Infrastructure team has had a renewed focus on making mozillians.org a modern community directory to meet Mozilla’s growing needs.&lt;/p&gt;
+ &lt;p&gt;Their first target for 2016 was to improve the UX on the profile edit interface.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;https://blog.mozilla.org/community/files/2016/01/new-profile-768x548.png&quot;&gt;&lt;img alt=&quot;new-profile-768x548&quot; class=&quot;aligncenter wp-image-2288 size-large&quot; height=&quot;428&quot; src=&quot;https://blog.mozilla.org/community/files/2016/01/new-profile-768x548-600x428.png&quot; width=&quot;600&quot; /&gt;&lt;/a&gt;&lt;br /&gt;
+ â€We chose it due to relatively self-contained nature of it, and cause many people were not happy with the current UX. After research of existing tools and applying latest best practices, we designed, coded and deployed a new profile edit interface (which by the way is renamed to Settings now) that we are happy to deliver to all Mozillians.â€&lt;/p&gt;
+ &lt;p&gt;Read the full blog &lt;a href=&quot;http://pierros.papadeas.gr/?p=447&quot;&gt;here&lt;/a&gt;!&lt;/p&gt;
+ &lt;h3&gt;&lt;b&gt;There are New Ways to Bring Your Design Skills to Mozilla: &lt;/b&gt;&lt;/h3&gt;
+ &lt;p&gt;Are you a passionate designer looking to contribute to Mozilla? You’ll be happy to hear there is a new way to contribute to the many design projects around Mozilla! Submit issues, find collaborators, and work on open source projects by getting involved!&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;You can check out the projects looking for help, or submit your own on the &lt;a href=&quot;https://github.com/mozilla/Community-Design/issues&quot;&gt;GitHub Repo&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://docs.google.com/a/mozilla.com/forms/d/1Tw3Mw_CMiqcIQrJF7TB1yIETGYec__NiVhaSz0CAaE8/viewform&quot;&gt;Sign-up to the mailing list&lt;/a&gt; to be added as a contributor to the Repo, added to the regular meeting list, and to get emails about GitHub trainings and more!&lt;/li&gt;
+ &lt;li&gt;And read&lt;a href=&quot;http://elioqoshi.me/en/2016/01/mozilla-community-design-kickoff/&quot;&gt; a blogpost&lt;/a&gt; about the project and its first meeting.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;Learn more &lt;a href=&quot;https://discourse.mozilla-community.org/c/community-design&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
+ &lt;h3&gt;&lt;b&gt;136 Volunteers Are Going to Singapore: &lt;/b&gt;&lt;/h3&gt;
+ &lt;p&gt;This weekend 136 participation leaders from all over the world are&lt;a href=&quot;https://twitter.com/thephoenixbird/status/690181985222926336&quot;&gt; heading to Singapore&lt;/a&gt; to undergo two days of&lt;a href=&quot;https://wiki.mozilla.org/Participation/Global_Gatherings_2015&quot;&gt; leadership training&lt;/a&gt; to develop the skills, knowledge and attitude to lead Participation in 2016.&lt;/p&gt;
+ &lt;div class=&quot;wp-caption aligncenter&quot; id=&quot;attachment_2289&quot; style=&quot;width: 609px;&quot;&gt;&lt;a href=&quot;https://blog.mozilla.org/community/files/2016/01/CZQE241WIAA6R2J.jpg&quot;&gt;&lt;img alt=&quot;Photo credit @thephoenixbird on Twitter&quot; class=&quot;wp-image-2289 size-full&quot; height=&quot;337&quot; src=&quot;https://blog.mozilla.org/community/files/2016/01/CZQE241WIAA6R2J.jpg&quot; width=&quot;599&quot; /&gt;&lt;/a&gt;&lt;p class=&quot;wp-caption-text&quot;&gt;Photo credit @&lt;a href=&quot;https://twitter.com/thephoenixbird/status/690181985222926336&quot; target=&quot;_blank&quot;&gt;thephoenixbird&lt;/a&gt; on Twitter&lt;/p&gt;&lt;/div&gt;
+ &lt;p&gt;If you know someone attending don’t forget to share your questions and goals with them, and follow along over the weekend by watching the hashtag&lt;a href=&quot;https://twitter.com/search?q=%23mozsummit&quot;&gt; #MozSummit&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;Stay tuned after the event for a debrief of the weekend!&lt;/p&gt;
+ &lt;h3&gt;&lt;b&gt;Friday’s Plenary from Mozlando is now public on Air Mozilla: &lt;/b&gt;&lt;/h3&gt;
+ &lt;p&gt;If you’re interested in learning more about all the exciting new features, projects, and plans that were presented at Mozlando look no further! You can now watch the final plenary sessions on Air Mozilla (it’s a lot of fun so I highly recommend it!) &lt;a href=&quot;https://air.mozilla.org/channels/mozlando/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;Share your questions and comments on discourse &lt;a href=&quot;https://discourse.mozilla-community.org/t/friday-plenary-from-mozlando-now-public-on-air-mozilla/6659&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;&lt;em&gt;Look forward to more updates like these in the coming months!&lt;/em&gt;&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-21T17:58:33+00:00</dc:date>
+ <dc:creator>Lucy Harris</dc:creator>
+ </item>
+ <item rdf:about="https://blog.mozilla.org/netpolicy/?p=912">
+ <title>Mozilla Privacy Blog: Prioritizing privacy: Good for business</title>
+ <link>https://blog.mozilla.org/netpolicy/2016/01/21/prioritizing-privacy-good-for-business/</link>
+ <content:encoded>&lt;p&gt;&lt;em&gt;This was originally posted at &lt;a href=&quot;http://staysafeonline.org/blog/prioritizing-privacy-good-for-business/&quot;&gt;StaySafeOnline.org&lt;/a&gt; in advance of &lt;a href=&quot;http://www.staysafeonline.org/data-privacy-day/events/&quot;&gt;Data Privacy Day&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
+ &lt;p&gt;Data Privacy Day – which arrives in just a week – is a day designed to raise awareness and promote best practices for privacy and data protection. It is a day that looks to the future and recognizes that we can and should do better as an industry. It reminds us that we need to focus on the importance of having the trust of our users.&lt;/p&gt;
+ &lt;p&gt;We seek to build trust so we can collectively create the Web our users want – the Web we all want.&lt;/p&gt;
+ &lt;p&gt;That Web is based on relationships, the same way that the offline world is. When I log in to a social media account, schedule a grocery delivery online or browse the news, I’m relying on those services to respect my data. While companies are innovating their products and services, they need to be innovating on user trust as well, which means designing to address privacy concerns – and making smart choices (early!) about how to manage data.&lt;/p&gt;
+ &lt;p&gt;A &lt;a href=&quot;http://www.pewinternet.org/2016/01/14/privacy-and-information-sharing/&quot;&gt;recent survey by Pew&lt;/a&gt; highlights the thought that each user puts into their choices – and the contextual considerations in various scenarios. They concluded that many participants were annoyed and uncertain by how their information was used, and they are choosing not to interact with those services that they don’t trust. This is a clear call to businesses to foster more trust with their users, which starts by making sure that there are people empowered within your company to ask the right questions: what do your users expect? What data do you need to collect? How can you communicate about that data collection? How should you protect their data? Is holding on to data a risk, or should you delete it?&lt;/p&gt;
+ &lt;p&gt;It’s crucial that users are a part of this process – consumers’ data is needed to offer cool, new experiences and a user needs to trust you in order to choose to give you their data. Pro-user innovation can’t happen in a vacuum – the system as it stands today isn’t doing a good job of aligning user interests with business incentives. Good user decisions can be good business decisions, but only if we create thoughtful user-centric products in a way that closes the feedback loop so that positive user experiences are rewarded with better business outcomes.&lt;/p&gt;
+ &lt;p&gt;Not prioritizing privacy in product decisions will impact the bottom line. From the many data breaches over the last few years to increasing evidence of eroding trust in online services, data practices are proving to be the dark horse in the online economy. When a company loses user trust, whether on privacy or &lt;a href=&quot;https://medium.com/@davidamerland/the-cost-of-losing-trust-97d764a1e696&quot;&gt;anything else&lt;/a&gt;, it loses customers and the potential for growth.&lt;/p&gt;
+ &lt;p&gt;Privacy means different things to different people but what’s clear is that people make decisions about the products and services that they use based on how those companies choose to treat their users. Over this time, the Internet ecosystem has evolved, as has its relationship with users – and some aspects of this evolution threaten the trust that lies at the heart of that relationship. Treating a user as a target – whether for an ad, purchase, or service – undermines the trust and relationship that a business may have with a consumer.&lt;/p&gt;
+ &lt;p&gt;The solution is not to abandon the massive value that robust data can bring to users, but rather, to collect and use data leanly, productively and transparently. At Mozilla, we have created a strong set of internal data practices to ensure that data decisions align with our &lt;a href=&quot;https://www.mozilla.org/en-US/privacy/principles/&quot;&gt;privacy principles&lt;/a&gt;. As an industry, we need to keep users at the center of the product vision rather than viewing them as targets of the product – it’s the only way to stay true to consumers and deliver the best, most trusted experiences possible.&lt;/p&gt;
+ &lt;p&gt;Want to hear more about how businesses can build relationships with their users by focusing on trust and privacy? We’re holding events in Washington, D.C., and &lt;a href=&quot;https://www.eventbrite.com/e/january-privacy-lab-privacy-for-startups-tickets-19849219550?aff=es2&quot;&gt;San Francisco&lt;/a&gt; with some of our partners to talk about it. Please join us!&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-21T17:42:00+00:00</dc:date>
+ <dc:creator>Heather West</dc:creator>
+ </item>
+ <item rdf:about="https://tacticalsecret.com/tag/mozilla/rss/9c39ad13-14ae-4456-a84e-13612637d832">
+ <title>J.C. Jones: Issuance Rate for Let's Encrypt</title>
+ <link>https://tacticalsecret.com/issuance-rate-for-lets-encrypt/</link>
+ <content:encoded>&lt;p&gt;Gathering data from &lt;a href=&quot;https://github.com/jcjones/letsencrypt_statistics&quot;&gt;Certificate Transparency logs&lt;/a&gt;, here's a snapshot in time of Let's Encrypt's certificate issuance rate per minute from 7-21 January 2016. On 20 January, DreamHost launched formal support for Let's Encrypt, which coincides with a rate increase.&lt;/p&gt;
+
+ &lt;p&gt;Note: This is mostly an experimental post with embedding charts; I've more data in the queue.&lt;/p&gt;
+
+ &lt;h3&gt;Let's Encrypt Issuance Rate per Minute&lt;/h3&gt;
+
+ &lt;div id=&quot;rate_hours&quot;&gt;&lt;/div&gt;</content:encoded>
+ <dc:date>2016-01-21T17:07:25+00:00</dc:date>
+ <dc:creator>James 'J.C.' Jones</dc:creator>
+ </item>
+ <item rdf:about="https://air.mozilla.org/web-qa-weekly-meeting-20160121/">
+ <title>Air Mozilla: Web QA Weekly Meeting, 21 Jan 2016</title>
+ <link>https://air.mozilla.org/web-qa-weekly-meeting-20160121/</link>
+ <content:encoded>&lt;p&gt;
+ &lt;img alt=&quot;Web QA Weekly Meeting&quot; class=&quot;wp-post-image&quot; height=&quot;90&quot; src=&quot;https://air.cdn.mozilla.net/media/cache/f5/13/f5137857516694df0458e837c2d3a4be.png&quot; width=&quot;160&quot; /&gt;
+ This is our weekly gathering of Mozilla'a Web QA team filled with discussion on our current and future projects, ideas, demos, and fun facts.
+ &lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-21T17:00:00+00:00</dc:date>
+ <dc:creator>Air Mozilla</dc:creator>
+ </item>
+ <item rdf:about="http://soledadpenades.com/?p=6379">
+ <title>Soledad Penades: No more tap tap tap sounds: yay!</title>
+ <link>http://soledadpenades.com/2016/01/21/no-more-tap-tap-tap-sounds-yay/</link>
+ <content:encoded>&lt;p&gt;A few days ago the fantastic Fritz from the Netherlands told me that my &lt;a href=&quot;http://soledadpenades.com/files/t/2015_howa/&quot;&gt;Hands On Web Audio slides&lt;/a&gt; had stopping working and there was no sound coming out from them in Firefox.&lt;/p&gt;
+ &lt;blockquote class=&quot;twitter-tweet&quot; width=&quot;550&quot;&gt;&lt;p dir=&quot;ltr&quot; lang=&quot;en&quot;&gt;&lt;a href=&quot;https://twitter.com/supersole&quot;&gt;@supersole&lt;/a&gt; oh noes! I reopened your slides: &lt;a href=&quot;https://t.co/SO35UfljMI&quot;&gt;https://t.co/SO35UfljMI&lt;/a&gt; and it doesn't work in &lt;a href=&quot;https://twitter.com/firefox&quot;&gt;@firefox&lt;/a&gt; anymore &lt;img alt=&quot;😱&quot; class=&quot;wp-smiley&quot; src=&quot;http://s.w.org/images/core/emoji/72x72/1f631.png&quot; style=&quot;height: 1em;&quot; /&gt; (works in chrome though.. &lt;img alt=&quot;😢&quot; class=&quot;wp-smiley&quot; src=&quot;http://s.w.org/images/core/emoji/72x72/1f622.png&quot; style=&quot;height: 1em;&quot; /&gt;)&lt;/p&gt;
+ &lt;p&gt;— Boring Stranger (@fritzvd) &lt;a href=&quot;https://twitter.com/fritzvd/status/686481500611735552&quot;&gt;January 11, 2016&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;
+ &lt;p&gt;&lt;/p&gt;
+ &lt;p&gt;Which is pretty disappointing for a slide deck that is built to teach you about Web Audio!&lt;/p&gt;
+ &lt;p&gt;I noticed that the issue was only on the introductory slide which uses a modified version of Stuart Memo’s &lt;a href=&quot;https://blog.stuartmemo.com/thx-deep-note-in-javascript/&quot;&gt;fantastic THX sound recreation&lt;/a&gt;-the rest of slides did play sound.&lt;/p&gt;
+ &lt;p&gt;I built &lt;a href=&quot;http://sole.github.io/test_cases/web_audio/thx_cutting_out/&quot;&gt;an isolated test case&lt;/a&gt; &lt;small&gt;&lt;a href=&quot;https://github.com/sole/test_cases/tree/gh-pages/web_audio/thx_cutting_out&quot;&gt;(source)&lt;/a&gt;&lt;/small&gt; that used a parameter-capable version of the THX sound code, just in case the issue depended on the number of oscillators, and submitted this funnily titled bug to the Web Audio component: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1240054&quot;&gt;Entirely Web Audio generated sound cuts out after a little while, or emits random tap tap tap sounds then silence&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;I can happily confirm that the bug has been fixed in Nightly and the fix will hopefully be “uplifted†to DevEdition very soon, as it was due to a regression.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;https://paul.cx/&quot;&gt;Paul Adenot&lt;/a&gt; (who works in Web Audio and is a Web Audio spec editor, amongst a couple tons of other cool things) was really excited about the bug, saying it was very edge-casey! Yay! And he also explained what did actually happen in lay terms: “you’d have to have a frequency that goes down very very slowly so that the FFT code could not keep upâ€, which is what the THX sound is doing with the filter frequency automation.&lt;/p&gt;
+ &lt;p&gt;I want to thank both Fritz for spotting this out and letting me know and also Stuart for sharing his THX code. It’s amazing what happens when you put stuff on the net and lots of different people use it in different ways and configurations. Together we make everything more robust &lt;img alt=&quot;:-)&quot; class=&quot;wp-smiley&quot; src=&quot;http://soledadpenades.com/wp-includes/images/smilies/simple-smile.png&quot; style=&quot;height: 1em;&quot; /&gt;&lt;/p&gt;
+ &lt;p&gt;Of course also sending thanks to Paul and Ben for identifying and fixing the issue so fast! It’s not been even a week! Woohoo!&lt;/p&gt;
+ &lt;p&gt;Well done everyone! &lt;img alt=&quot;ðŸ‘&quot; class=&quot;wp-smiley&quot; src=&quot;http://s.w.org/images/core/emoji/72x72/1f44f.png&quot; style=&quot;height: 1em;&quot; /&gt;&lt;img alt=&quot;ðŸ¼&quot; class=&quot;wp-smiley&quot; src=&quot;http://s.w.org/images/core/emoji/72x72/1f3fc.png&quot; style=&quot;height: 1em;&quot; /&gt;&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;http://soledadpenades.com/?flattrss_redirect&amp;amp;id=6379&amp;amp;md5=57babe624711830f95e4b8fbd6e52c91&quot; target=&quot;_blank&quot; title=&quot;Flattr&quot;&gt;&lt;img alt=&quot;flattr this!&quot; src=&quot;http://soledadpenades.com/wp-content/plugins/flattr/img/flattr-badge-large.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-21T15:49:05+00:00</dc:date>
+ <dc:creator>sole</dc:creator>
+ </item>
+ <item rdf:about="http://pierros.papadeas.gr/?p=447">
+ <title>Pierros Papadeas: Mozillians.org Profile Edit refresh</title>
+ <link>http://pierros.papadeas.gr/?p=447</link>
+ <content:encoded>&lt;p&gt;Since the start of this year, Participation Infrastructure team has a renewed focus on making mozillians.org a modern community directory to meet Mozilla’s growing needs. This will not be an one-time effort. We need to invest technically and programmatically in order to deliver a first-class product that will be the foundation for identity management across the Mozilla ecosystem.&lt;/p&gt;
+ &lt;p&gt;Mozillians.org is full of functionality as it is today, but is paying the debt of being developed by 5 different teams over the past 5 years. We started simple this time. Updated all core technology pieces, did privacy and security reviews, and started the process of consolidating and modernizing many of the things we do in the site.&lt;/p&gt;
+ &lt;p&gt;Our first target was Profile Edit. We chose it due to relatively self-contained nature of it, and cause many people were not happy with the current UX. After research of existing tools and applying latest best practices, we designed, coded and deployed a new profile edit interface (which by the way is renamed to Settings now) that we are happy to deliver to all Mozillians.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;http://pierros.papadeas.gr/wp-content/uploads/2016/01/new-profile.png&quot; rel=&quot;attachment wp-att-448&quot;&gt;&lt;img alt=&quot;new-profile&quot; class=&quot;aligncenter size-large wp-image-448&quot; height=&quot;417&quot; src=&quot;http://pierros.papadeas.gr/wp-content/uploads/2016/01/new-profile-1024x731.png&quot; width=&quot;584&quot; /&gt;&lt;/a&gt;Have a&lt;a href=&quot;https://mozillians.org/en-US/user/edit/&quot;&gt; look for yourself &lt;/a&gt;and don’t miss the chance to update your profile while you do it!&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;https://mozillians.org/en-US/u/comzeradd/&quot;&gt;Nikos&lt;/a&gt; (on the front-end), &lt;a href=&quot;https://mozillians.org/en-US/u/akatsoulas/&quot;&gt;Tasos&lt;/a&gt; and &lt;a href=&quot;https://mozillians.org/en-US/u/jgiannelos/&quot;&gt;Nemo&lt;/a&gt; (on the back-end) worked hard to deliver this in a speedy manner (as they are used to), and the end result is a testament to what is coming next on Mozillians.org.&lt;/p&gt;
+ &lt;p&gt;Our next target? Groups. Currently it is obscure and unclear what all those settings in groups are, what is the functionality and how teams within Mozilla will be using it. We will be tackling this soon. After that, search and stats will be our attention, in an ongoing effort to fortify mozillians.org functionality. Stay tuned, and as always feel free to &lt;a href=&quot;https://bugzilla.mozilla.org/enter_bug.cgi?product=Participation%20Infrastructure&amp;amp;component=Phonebook&quot;&gt;file bugs&lt;/a&gt; and &lt;a href=&quot;https://github.com/mozilla/mozillians&quot;&gt;contribute &lt;/a&gt;in the process.&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-21T11:41:39+00:00</dc:date>
+ <dc:creator>Pierros Papadeas</dc:creator>
+ </item>
+ <item rdf:about="http://adamlofting.com/?p=1396">
+ <title>Adam Lofting: Blog posts I haven’t written lately</title>
+ <link>http://feedproxy.google.com/~r/adamlofting/blog/~3/DoEWpBapwiw/</link>
+ <content:encoded>&lt;p&gt;Last year I joked…&lt;/p&gt;
+ &lt;blockquote class=&quot;twitter-tweet&quot; lang=&quot;en&quot;&gt;
+ &lt;p dir=&quot;ltr&quot; lang=&quot;en&quot;&gt;Thinking about writing a blog post listing the blog posts I’ve been meaning to write… Maybe that will save some time&lt;/p&gt;
+ &lt;p&gt;— Adam Lofting (@adamlofting) &lt;a href=&quot;https://twitter.com/adamlofting/status/667657889817956352&quot;&gt;November 20, 2015&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;
+ &lt;p&gt;&lt;/p&gt;
+ &lt;p&gt;Now, it has come to this.&lt;/p&gt;
+ &lt;h4&gt;9 blog posts I’ve not been writing&lt;/h4&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Working on working on the impact of impact&lt;/li&gt;
+ &lt;li&gt;Designing Games in &lt;a href=&quot;https://en.wikipedia.org/wiki/Amateur&quot; target=&quot;_blank&quot;&gt;my free time&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Moving Out (the board game)&lt;/li&gt;
+ &lt;li&gt;Mozilla Foundation 2016 KPIs&lt;/li&gt;
+ &lt;li&gt;Studying Network Science&lt;/li&gt;
+ &lt;li&gt;Learning Analytics plans for 2016&lt;/li&gt;
+ &lt;li&gt;Daily practice / you are what you do every day&lt;/li&gt;
+ &lt;li&gt;Several more A/B tests to write up from &lt;a href=&quot;http://fundraising.mozilla.org/&quot;&gt;the fundraising campaign&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;CRM Progress in 2015&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;But my most requested blog by far, is an update on the status of my shed / office that I was tagging on to the end my blog posts at this time last year. Many people at Mozfest wanted to know about the shed… so here it is.&lt;/p&gt;
+ &lt;p&gt;This time last year:&lt;/p&gt;
+ &lt;blockquote class=&quot;twitter-tweet&quot; lang=&quot;en&quot;&gt;&lt;p&gt;
+ Starting in the new office today. It will take time to make it *nice* but it works for now. &lt;a href=&quot;http://t.co/sWoC4kFNLc&quot;&gt;pic.twitter.com/sWoC4kFNLc&lt;/a&gt;&lt;/p&gt;
+ &lt;p&gt;— Adam Lofting (@adamlofting) &lt;a href=&quot;https://twitter.com/adamlofting/status/560361913339899904&quot;&gt;January 28, 2015&lt;/a&gt;
+ &lt;/p&gt;&lt;/blockquote&gt;
+ &lt;p&gt;&lt;/p&gt;
+ &lt;p&gt;Some pictures from this morning:&lt;/p&gt;
+ &lt;p&gt;&lt;img alt=&quot;office1&quot; class=&quot;alignright size-large wp-image-1398&quot; height=&quot;282&quot; src=&quot;http://adamlofting.com/wp-content/uploads/2016/01/office1-750x320.jpg&quot; width=&quot;660&quot; /&gt;&lt;/p&gt;
+ &lt;p&gt;&lt;img alt=&quot;office2&quot; class=&quot;aligncenter size-large wp-image-1399&quot; height=&quot;237&quot; src=&quot;http://adamlofting.com/wp-content/uploads/2016/01/office2-750x269.jpg&quot; width=&quot;660&quot; /&gt;&lt;/p&gt;
+ &lt;p&gt;It’s a pretty nice place to work now and it doubles as useful workshop on the weekends. It needs a few finishing touches, but the law of diminishing returns means those finishing touches are lower priority than work that needs to be done elsewhere in the house and garden. So it’ll stay like this a while longer.&lt;/p&gt;
+ &lt;div class=&quot;feedflare&quot;&gt;
+ &lt;a href=&quot;http://feeds.feedburner.com/~ff/adamlofting/blog?a=DoEWpBapwiw:VxTJGXwqhlI:yIl2AUoC8zA&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;http://feeds.feedburner.com/~ff/adamlofting/blog?d=yIl2AUoC8zA&quot; /&gt;&lt;/a&gt; &lt;a href=&quot;http://feeds.feedburner.com/~ff/adamlofting/blog?a=DoEWpBapwiw:VxTJGXwqhlI:qj6IDK7rITs&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;http://feeds.feedburner.com/~ff/adamlofting/blog?d=qj6IDK7rITs&quot; /&gt;&lt;/a&gt;
+ &lt;/div&gt;&lt;img alt=&quot;&quot; height=&quot;1&quot; src=&quot;http://feeds.feedburner.com/~r/adamlofting/blog/~4/DoEWpBapwiw&quot; width=&quot;1&quot; /&gt;</content:encoded>
+ <dc:date>2016-01-21T09:44:24+00:00</dc:date>
+ <dc:creator>Adam</dc:creator>
+ </item>
+ <item rdf:about="http://blog.ziade.org/2016/01/21/a-pelican-web-editor/">
+ <title>Tarek Ziadé: A Pelican web editor</title>
+ <link>http://blog.ziade.org/2016/01/21/a-pelican-web-editor/</link>
+ <content:encoded>&lt;p&gt;The benefit of being a father again (Freya my 3rd child, was born last week) is
+ that while on paternity leave &amp;amp; between two baby bottles, I can hack on fun stuff.&lt;/p&gt;
+ &lt;p&gt;A few months ago, I've built for my running club a Pelican-based website, check it out
+ at : &lt;a class=&quot;reference external&quot; href=&quot;http://acr-dijon.org&quot;&gt;http://acr-dijon.org&lt;/a&gt;. Nothing's special about it, except that I am not
+ the one feeding it. The content is added by people from the club that have zero
+ knowledge about softwares, let alone stuff like vim or command line tools.&lt;/p&gt;
+ &lt;p&gt;I set up a github-based flow for them, where they add content through the
+ github UI and its minimal reStructuredText preview feature - and then a few
+ of my crons update the website on the server I host.
+ For images and other media, they are uploading them via FTP using FireSSH in Firefox.&lt;/p&gt;
+ &lt;p&gt;For the comments, I've switched from Disqus to &lt;a class=&quot;reference external&quot; href=&quot;https://posativ.org/isso/&quot;&gt;ISSO&lt;/a&gt;
+ after I got annoyed by the fact that it was impossible to display a simple Disqus
+ UI for people to comment without having to log in.&lt;/p&gt;
+ &lt;p&gt;I had to make my club friends go through a minimal
+ reStructuredText syntax training, and things are more of less working now.&lt;/p&gt;
+ &lt;p&gt;The system has a few caveats though:&lt;/p&gt;
+ &lt;ul class=&quot;simple&quot;&gt;
+ &lt;li&gt;it's dependent on Github. I'd rather have everything hosted on my server.&lt;/li&gt;
+ &lt;li&gt;the github restTRucturedText preview will not display syntax errors and warnings
+ and very often, articles get broken&lt;/li&gt;
+ &lt;li&gt;the resulting reST is ugly, and it's a bit hard to force my editors to be stricter
+ about details like empty lines, not using tabs etc.&lt;/li&gt;
+ &lt;li&gt;adding folders or organizing articles from Github is a pain&lt;/li&gt;
+ &lt;li&gt;editing the metadata tags is prone to many mistakes&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;So I've decided to build my own web editing tool with the following features:&lt;/p&gt;
+ &lt;ul class=&quot;simple&quot;&gt;
+ &lt;li&gt;resTructuredText cleanup&lt;/li&gt;
+ &lt;li&gt;content browsing&lt;/li&gt;
+ &lt;li&gt;resTructuredText web editor with live preview that shows warnings &amp;amp; errors&lt;/li&gt;
+ &lt;li&gt;a little bit of wsgi glue and a few forms to create articles without
+ having to worry about metadata syntax.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;div class=&quot;section&quot; id=&quot;restructuredtext-cleanup&quot;&gt;
+ &lt;h3&gt;resTructuredText cleanup&lt;/h3&gt;
+ &lt;p&gt;The first step was to build a reStructuredText parser that would read some
+ reStructuredText and render it back into a cleaner version.&lt;/p&gt;
+ &lt;p&gt;We've imported almost 2000 articles in Pelican from the old blog, so I had
+ a &lt;strong&gt;lot&lt;/strong&gt; of samples to make my parser work well.&lt;/p&gt;
+ &lt;p&gt;I first tried &lt;a class=&quot;reference external&quot; href=&quot;https://github.com/benoitbryon/rst2rst&quot;&gt;rst2rst&lt;/a&gt; but that
+ parser was built for a very specific use case (text wrapping) and was
+ incomplete. It was not parsing all of the reStructuredText syntax.&lt;/p&gt;
+ &lt;p&gt;Inspired by it, I wrote my own little parser using &lt;strong&gt;docutils&lt;/strong&gt;.&lt;/p&gt;
+ &lt;p&gt;Understanding docutils is not a small task. This project is very powerfull
+ but quite complex. One thing that cruelly misses in docutils parser tools
+ is the ability to get the source text from any node, including its children,
+ so you can render back the same source.&lt;/p&gt;
+ &lt;p&gt;That's roughly what I had to add in my code. It's ugly but it does the job:
+ it will parse rst files and render the same content, minus all the extraneous
+ empty lines, spaces, tabs etc.&lt;/p&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;section&quot; id=&quot;content-browsing&quot;&gt;
+ &lt;h3&gt;Content browsing&lt;/h3&gt;
+ &lt;p&gt;Content browsing is pretty straightforward: my admin tool let you browse
+ the Pelican &lt;em&gt;content&lt;/em&gt; directory and lists all articles, organized by categories.&lt;/p&gt;
+ &lt;p&gt;In our case, each category has a top directory in &lt;em&gt;content&lt;/em&gt;. The browser
+ parses the articles using my parser and displays paginated lists.&lt;/p&gt;
+ &lt;p&gt;I had to add a cache system for the parser, because one of the directory
+ contains over 1000 articles -- and browsing was kind of slow :)&lt;/p&gt;
+ &lt;img alt=&quot;http://ziade.org/henet-browsing.png&quot; src=&quot;http://ziade.org/henet-browsing.png&quot; /&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;section&quot; id=&quot;restructuredtext-web-editor&quot;&gt;
+ &lt;h3&gt;resTructuredText web editor&lt;/h3&gt;
+ &lt;p&gt;The last big bit was the live editor. I've stumbled on a neat little tool
+ called &lt;strong&gt;rsted&lt;/strong&gt;, that provides a live preview of the reStructuredText
+ as you are typing it. And it includes warnings !&lt;/p&gt;
+ &lt;p&gt;Check it out: &lt;a class=&quot;reference external&quot; href=&quot;http://rst.ninjs.org/&quot;&gt;http://rst.ninjs.org/&lt;/a&gt;&lt;/p&gt;
+ &lt;p&gt;I've stripped it and kept what I needed, and included it in my app.&lt;/p&gt;
+ &lt;img alt=&quot;http://ziade.org/henet.png&quot; src=&quot;http://ziade.org/henet.png&quot; /&gt;
+ &lt;p&gt;I am quite happy with the result so far. I need to add real tests and
+ a bit of documentation, and I will start to train my club friends on it.&lt;/p&gt;
+ &lt;p&gt;The next features I'd like to add are:&lt;/p&gt;
+ &lt;ul class=&quot;simple&quot;&gt;
+ &lt;li&gt;comments management, to replace Isso (working on it now)&lt;/li&gt;
+ &lt;li&gt;smart Pelican builds. e.g. if a comment is added I don't want to rebuild the whole
+ blog (~1500 articles)&lt;/li&gt;
+ &lt;li&gt;media management&lt;/li&gt;
+ &lt;li&gt;spell checker&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;The project lives here: &lt;a class=&quot;reference external&quot; href=&quot;https://github.com/AcrDijon/henet&quot;&gt;https://github.com/AcrDijon/henet&lt;/a&gt;&lt;/p&gt;
+ &lt;p&gt;I am not going to release it, but if someone finds it useful, I could.&lt;/p&gt;
+ &lt;p&gt;It's built with Bottle &amp;amp; Bootstrap as well.&lt;/p&gt;
+ &lt;/div&gt;</content:encoded>
+ <dc:date>2016-01-21T09:40:00+00:00</dc:date>
+ <dc:creator>Tarek Ziade</dc:creator>
+ </item>
+ <item rdf:about="http://www.ncameron.org/blog/rss/631106eb-e7b1-47d5-82f9-cb6ad210ea89">
+ <title>Nick Cameron: Closures and first-class functions</title>
+ <link>http://www.ncameron.org/blog/closures-and-first-class-functions/</link>
+ <content:encoded>&lt;p&gt;I wrote a long and probably dull chapter on closures and first-class and higher-order functions in Rust. It goes into some detail on the implementation and some of the subtleties like higher-ranked lifetime bounds.&lt;/p&gt;
+
+ &lt;p&gt;I was going to post it here too, but it is really too long. Instead, pop over to the 'Rust for C++ programmers' repo and read it &lt;a href=&quot;https://github.com/nrc/r4cppp/blob/master/closures.md&quot;&gt;there&lt;/a&gt;.&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-21T08:36:21+00:00</dc:date>
+ <dc:creator>Nick Cameron</dc:creator>
+ </item>
+ <item rdf:about="http://nickdesaulniers.github.io/blog/2016/01/20/debugging-x86-64-assembly-with-lldb-and-dtrace">
+ <title>Nick Desaulniers: Intro to Debugging x86-64 Assembly</title>
+ <link>http://nickdesaulniers.github.io/blog/2016/01/20/debugging-x86-64-assembly-with-lldb-and-dtrace/</link>
+ <content:encoded>&lt;p&gt;I’m hacking on an assembly project, and wanted to document some of the tricks I
+ was using for figuring out what was going on. This post might seem a little
+ basic for folks who spend all day heads down in gdb or who do this stuff
+ professionally, but I just wanted to share a quick intro to some tools that
+ others may find useful.
+ (&lt;a href=&quot;https://pchiusano.github.io/2014-10-11/defensive-writing.html&quot;&gt;oh god, I’m doing it&lt;/a&gt;)&lt;/p&gt;
+
+ &lt;p&gt;If your coming from gdb to lldb, there’s a few differences in commands. LLDB
+ has
+ &lt;a href=&quot;http://lldb.llvm.org/lldb-gdb.html&quot;&gt;great documentation&lt;/a&gt;
+ on some of the differences. Everything in this post about LLDB is pretty much
+ there.&lt;/p&gt;
+
+ &lt;p&gt;The bread and butter commands when working with gdb or lldb are:&lt;/p&gt;
+
+ &lt;ul&gt;
+ &lt;li&gt;r (run the program)&lt;/li&gt;
+ &lt;li&gt;s (step in)&lt;/li&gt;
+ &lt;li&gt;n (step over)&lt;/li&gt;
+ &lt;li&gt;finish (step out)&lt;/li&gt;
+ &lt;li&gt;c (continue)&lt;/li&gt;
+ &lt;li&gt;q (quit the program)&lt;/li&gt;
+ &lt;/ul&gt;
+
+
+ &lt;p&gt;You can hit enter if you want to run the last command again, which is really
+ useful if you want to keep stepping over statements repeatedly.&lt;/p&gt;
+
+ &lt;p&gt;I’ve been using LLDB on OSX. Let’s say I want to debug a program I can build,
+ but is crashing or something:&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre class=&quot;line-numbers&quot;&gt;&lt;span class=&quot;line-number&quot;&gt;1&lt;/span&gt;
+ &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;sudo lldb ./asmttpd web_root
+ &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;Setting a breakpoint on jump to label:&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre class=&quot;line-numbers&quot;&gt;&lt;span class=&quot;line-number&quot;&gt;1&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;2&lt;/span&gt;
+ &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; b sys_write
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;Breakpoint 3: &lt;span class=&quot;nv&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; asmttpd&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;sys_write, &lt;span class=&quot;nv&quot;&gt;address&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x00000000000029ae
+ &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;Running the program until breakpoint hit:&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre class=&quot;line-numbers&quot;&gt;&lt;span class=&quot;line-number&quot;&gt;1&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;2&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;3&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;4&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;5&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;6&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;7&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;8&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;9&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;10&lt;/span&gt;
+ &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; r
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;Process 32236 launched: &lt;span class=&quot;s1&quot;&gt;'./asmttpd'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;x86_64&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;Process 32236 stopped
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;* thread &lt;span class=&quot;c&quot;&gt;#1: tid = 0xe69b9, 0x00000000000029ae asmttpd`sys_write, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; frame &lt;span class=&quot;c&quot;&gt;#0: 0x00000000000029ae asmttpd`sys_write&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;asmttpd&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;sys_write:
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;-&amp;gt; 0x29ae &amp;lt;+0&amp;gt;: pushq %rdi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29af &amp;lt;+1&amp;gt;: pushq %rsi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29b0 &amp;lt;+2&amp;gt;: pushq %rdx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29b1 &amp;lt;+3&amp;gt;: pushq %r10
+ &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;Seeing more of the current stack frame:&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre class=&quot;line-numbers&quot;&gt;&lt;span class=&quot;line-number&quot;&gt;1&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;2&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;3&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;4&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;5&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;6&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;7&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;8&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;9&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;10&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;11&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;12&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;13&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;14&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;15&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;16&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;17&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;18&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;19&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;20&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;21&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;22&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;23&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;24&lt;/span&gt;
+ &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; d
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;asmttpd&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;sys_write:
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;-&amp;gt; 0x29ae &amp;lt;+0&amp;gt;: pushq %rdi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29af &amp;lt;+1&amp;gt;: pushq %rsi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29b0 &amp;lt;+2&amp;gt;: pushq %rdx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29b1 &amp;lt;+3&amp;gt;: pushq %r10
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29b3 &amp;lt;+5&amp;gt;: pushq %r8
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29b5 &amp;lt;+7&amp;gt;: pushq %r9
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29b7 &amp;lt;+9&amp;gt;: pushq %rbx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29b8 &amp;lt;+10&amp;gt;: pushq %rcx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29b9 &amp;lt;+11&amp;gt;: movq %rsi, %rdx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29bc &amp;lt;+14&amp;gt;: movq %rdi, %rsi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29bf &amp;lt;+17&amp;gt;: movq &lt;span class=&quot;nv&quot;&gt;$0x1&lt;/span&gt;, %rdi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29c6 &amp;lt;+24&amp;gt;: movq &lt;span class=&quot;nv&quot;&gt;$0x2000004&lt;/span&gt;, %rax
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29cd &amp;lt;+31&amp;gt;: syscall
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29cf &amp;lt;+33&amp;gt;: popq %rcx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29d0 &amp;lt;+34&amp;gt;: popq %rbx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29d1 &amp;lt;+35&amp;gt;: popq %r9
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29d3 &amp;lt;+37&amp;gt;: popq %r8
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29 &amp;lt;+39&amp;gt;: popq %r10
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29d7 &amp;lt;+41&amp;gt;: popq %rdx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29d8 &amp;lt;+42&amp;gt;: popq %rsi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29d9 &amp;lt;+43&amp;gt;: popq %rdi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29da &amp;lt;+44&amp;gt;: retq
+ &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;Getting a back trace (call stack):&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre class=&quot;line-numbers&quot;&gt;&lt;span class=&quot;line-number&quot;&gt;1&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;2&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;3&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;4&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;5&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;6&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;7&lt;/span&gt;
+ &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; bt
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;* thread &lt;span class=&quot;c&quot;&gt;#1: tid = 0xe69b9, 0x00000000000029ae asmttpd`sys_write, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; * frame &lt;span class=&quot;c&quot;&gt;#0: 0x00000000000029ae asmttpd`sys_write&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; frame &lt;span class=&quot;c&quot;&gt;#1: 0x00000000000021b6 asmttpd`print_line + 16&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; frame &lt;span class=&quot;c&quot;&gt;#2: 0x0000000000002ab3 asmttpd`start + 35&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; frame &lt;span class=&quot;c&quot;&gt;#3: 0x00007fff9900c5ad libdyld.dylib`start + 1&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; frame &lt;span class=&quot;c&quot;&gt;#4: 0x00007fff9900c5ad libdyld.dylib`start + 1&lt;/span&gt;
+ &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;peeking at the upper stack frame:&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre class=&quot;line-numbers&quot;&gt;&lt;span class=&quot;line-number&quot;&gt;1&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;2&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;3&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;4&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;5&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;6&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;7&lt;/span&gt;
+ &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; up
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;frame &lt;span class=&quot;c&quot;&gt;#1: 0x00000000000021b6 asmttpd`print_line + 16&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;asmttpd&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;print_line:
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x21b6 &amp;lt;+16&amp;gt;: movabsq &lt;span class=&quot;nv&quot;&gt;$0x30cb&lt;/span&gt;, %rdi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x21c0 &amp;lt;+26&amp;gt;: movq &lt;span class=&quot;nv&quot;&gt;$0x1&lt;/span&gt;, %rsi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x21c7 &amp;lt;+33&amp;gt;: callq 0x29ae ; sys_write
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x21cc &amp;lt;+38&amp;gt;: popq %rcx
+ &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;back down to the breakpoint-halted stack frame:&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre class=&quot;line-numbers&quot;&gt;&lt;span class=&quot;line-number&quot;&gt;1&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;2&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;3&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;4&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;5&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;6&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;7&lt;/span&gt;
+ &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; down
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;frame &lt;span class=&quot;c&quot;&gt;#0: 0x00000000000029ae asmttpd`sys_write&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;asmttpd&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;sys_write:
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;-&amp;gt; 0x29ae &amp;lt;+0&amp;gt;: pushq %rdi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29af &amp;lt;+1&amp;gt;: pushq %rsi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29b0 &amp;lt;+2&amp;gt;: pushq %rdx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29b1 &amp;lt;+3&amp;gt;: pushq %r10
+ &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;dumping the values of registers:&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre class=&quot;line-numbers&quot;&gt;&lt;span class=&quot;line-number&quot;&gt;1&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;2&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;3&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;4&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;5&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;6&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;7&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;8&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;9&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;10&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;11&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;12&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;13&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;14&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;15&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;16&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;17&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;18&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;19&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;20&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;21&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;22&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;23&lt;/span&gt;
+ &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; register &lt;span class=&quot;nb&quot;&gt;read&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;General Purpose Registers:
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;rax&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x0000000000002a90 asmttpd&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;start
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;rbx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x0000000000000000
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;rcx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x00007fff5fbffaf8
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;rdx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x00007fff5fbffa40
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;rdi&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x00000000000030cc start_text
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;rsi&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x000000000000000f
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;rbp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x00007fff5fbffa18
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;rsp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x00007fff5fbff9b8
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;r8&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x0000000000000000
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;r9&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x00007fff7b1670c8 atexit_mutex + 24
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;r10&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x00000000ffffffff
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;r11&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0xffffffff00000000
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;r12&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x0000000000000000
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;r13&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x0000000000000000
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;r14&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x0000000000000000
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;r15&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x0000000000000000
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;rip&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x00000000000029ae asmttpd&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;sys_write
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;rflags&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x0000000000000246
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;cs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x000000000000002b
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;fs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x0000000000000000
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;gs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x0000000000000000
+ &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;read just one register:&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre class=&quot;line-numbers&quot;&gt;&lt;span class=&quot;line-number&quot;&gt;1&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;2&lt;/span&gt;
+ &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; register &lt;span class=&quot;nb&quot;&gt;read &lt;/span&gt;rdi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;rdi&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x00000000000030cc start_text
+ &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;When you’re trying to figure out what system calls are made by some C code,
+ using dtruss is very helpful. dtruss is available on OSX and seems to be some
+ kind of wrapper around DTrace.&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre class=&quot;line-numbers&quot;&gt;&lt;span class=&quot;line-number&quot;&gt;1&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;2&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;3&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;4&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;5&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;6&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;7&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;8&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;9&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;10&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;11&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;12&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;13&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;14&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;15&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;16&lt;/span&gt;
+ &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;cat sleep.c
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;c&quot;&gt;#include &amp;lt;time.h&amp;gt;&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;int main &lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; struct timespec &lt;span class=&quot;nv&quot;&gt;rqtp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 2,
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; nanosleep&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&amp;amp;rqtp, NULL&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;clang sleep.c
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;sudo dtruss ./a.out
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;...all kinds of fun stuff
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;__semwait_signal&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0xB03, 0x0, 0x1&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; -1 Err#60
+ &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;If you compile with &lt;code&gt;-g&lt;/code&gt; to emit debug symbols, you can use lldb’s disassemble
+ command to get the equivalent assembly:&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre class=&quot;line-numbers&quot;&gt;&lt;span class=&quot;line-number&quot;&gt;1&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;2&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;3&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;4&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;5&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;6&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;7&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;8&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;9&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;10&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;11&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;12&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;13&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;14&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;15&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;16&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;17&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;18&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;19&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;20&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;21&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;22&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;23&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;24&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;25&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;26&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;27&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;28&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;29&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;30&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;31&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;32&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;33&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;34&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;35&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;36&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;37&lt;/span&gt;
+ &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;clang sleep.c -g
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;lldb a.out
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; target create &lt;span class=&quot;s2&quot;&gt;&quot;a.out&quot;&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;Current executable &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;to &lt;span class=&quot;s1&quot;&gt;'a.out'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;x86_64&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;.
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; b main
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;Breakpoint 1: &lt;span class=&quot;nv&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; a.out&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;main + 16 at sleep.c:3, &lt;span class=&quot;nv&quot;&gt;address&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x0000000100000f40
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; r
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;Process 33213 launched: &lt;span class=&quot;s1&quot;&gt;'/Users/Nicholas/code/assembly/asmttpd/a.out'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;x86_64&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;Process 33213 stopped
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;* thread &lt;span class=&quot;c&quot;&gt;#1: tid = 0xeca04, 0x0000000100000f40 a.out`main + 16 at sleep.c:3, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; frame &lt;span class=&quot;c&quot;&gt;#0: 0x0000000100000f40 a.out`main + 16 at sleep.c:3&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 1 &lt;span class=&quot;c&quot;&gt;#include &amp;lt;time.h&amp;gt;&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 2 int main &lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;-&amp;gt; 3 struct timespec &lt;span class=&quot;nv&quot;&gt;rqtp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 4 2,
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 5 0
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 6 &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 7
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; disassemble
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;a.out&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;main:
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f30 &amp;lt;+0&amp;gt;: pushq %rbp
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f31 &amp;lt;+1&amp;gt;: movq %rsp, %rbp
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f34 &amp;lt;+4&amp;gt;: subq &lt;span class=&quot;nv&quot;&gt;$0x20&lt;/span&gt;, %rsp
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f38 &amp;lt;+8&amp;gt;: leaq -0x10&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;%rbp&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;, %rdi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f3c &amp;lt;+12&amp;gt;: xorl %eax, %eax
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f3e &amp;lt;+14&amp;gt;: movl %eax, %esi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;-&amp;gt; 0x100000f40 &amp;lt;+16&amp;gt;: movq 0x49&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;%rip&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;, %rcx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f47 &amp;lt;+23&amp;gt;: movq %rcx, -0x10&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;%rbp&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f4b &amp;lt;+27&amp;gt;: movq 0x46&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;%rip&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;, %rcx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f52 &amp;lt;+34&amp;gt;: movq %rcx, -0x8&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;%rbp&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f56 &amp;lt;+38&amp;gt;: callq 0x100000f68 ; symbol stub &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt;: nanosleep
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f5b &amp;lt;+43&amp;gt;: xorl %edx, %edx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f5d &amp;lt;+45&amp;gt;: movl %eax, -0x14&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;%rbp&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f60 &amp;lt;+48&amp;gt;: movl %edx, %eax
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f62 &amp;lt;+50&amp;gt;: addq &lt;span class=&quot;nv&quot;&gt;$0x20&lt;/span&gt;, %rsp
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f66 &amp;lt;+54&amp;gt;: popq %rbp
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f67 &amp;lt;+55&amp;gt;: retq
+ &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;Anyways, I’ve been learning some interesting things about OSX that I’ll be
+ sharing soon. If you’d like to learn more about x86-64 assembly programming,
+ you should read my other posts about
+ &lt;a href=&quot;http://nickdesaulniers.github.io/blog/2014/04/18/lets-write-some-x86-64/&quot;&gt;writing x86-64&lt;/a&gt;
+ and a toy
+ &lt;a href=&quot;http://nickdesaulniers.github.io/blog/2015/05/25/interpreter-compiler-jit/&quot;&gt;JIT for Brainfuck&lt;/a&gt;
+ (&lt;a href=&quot;https://www.reddit.com/r/programming/comments/377ov9/interpreter_compiler_jit/crkkrz4&quot;&gt;the creator of Brainfuck liked it&lt;/a&gt;).&lt;/p&gt;
+
+ &lt;p&gt;I should also do a post on
+ &lt;a href=&quot;http://rr-project.org/&quot;&gt;Mozilla’s rr&lt;/a&gt;,
+ because it can do amazing things like step backwards. Another day…&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-21T04:04:00+00:00</dc:date>
+ </item>
+ <item rdf:about="https://rail.merail.ca/posts/rebooting-productivity.html">
+ <title>Rail Aliiev: Rebooting productivity</title>
+ <link>https://rail.merail.ca/posts/rebooting-productivity.html</link>
+ <content:encoded>&lt;div&gt;&lt;p&gt;Every new year gives you an opportunity to sit back, relax,
+ &lt;span class=&quot;strike&quot;&gt;have some scotch&lt;/span&gt; and re-think the passed year. Holidays give
+ you enough free time. Even if you decide to not take a vacation around
+ the holidays, it's usually calm and peaceful.&lt;/p&gt;
+ &lt;p&gt;This time, I found myself thinking mostly about productivity, being
+ effective, feeling busy, overwhelmed with work and other related topics.&lt;/p&gt;
+ &lt;p&gt;When I started at Mozilla (almost 6 years ago!), I tried to apply all my
+ GTD and time management knowledge and techniques. Working remotely and
+ in a different time zone was an advantage - I had close to zero
+ interruptions. It worked perfect.&lt;/p&gt;
+ &lt;p&gt;Last year I realized that my productivity skills had faded away somehow.
+ 40h+ workweeks, working on weekends, delivering goals in the last week
+ of quarter don't sound like good signs. Instead of being productive I
+ felt busy.&lt;/p&gt;
+ &lt;p&gt;&quot;Every crisis is an opportunity&quot;. Time to make a step back and reboot
+ myself. Burning out at work is not a good idea. :)&lt;/p&gt;
+ &lt;p&gt;Here are some ideas/tips that I wrote down for myself you may found
+ useful.&lt;/p&gt;
+ &lt;div class=&quot;section&quot; id=&quot;health-related&quot;&gt;
+ &lt;h3&gt;Health related&lt;/h3&gt;
+ &lt;ul class=&quot;simple&quot;&gt;
+ &lt;li&gt;Morning exercises. A 20-minute walk will wake your brain up and
+ generate enough endorphins for the first half of the day.&lt;/li&gt;
+ &lt;li&gt;Meditation. 2x20min a day is ideal; 2x10min would work too. Something
+ like &lt;a class=&quot;reference external&quot; href=&quot;http://www.calm.com/&quot;&gt;calm.com&lt;/a&gt; makes this a peace of cake.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;section&quot; id=&quot;concentration&quot;&gt;
+ &lt;h3&gt;Concentration&lt;/h3&gt;
+ &lt;ul class=&quot;simple&quot;&gt;
+ &lt;li&gt;Task #1: make a daily plan. No plan - no work.&lt;/li&gt;
+ &lt;li&gt;Don't start your day by reading emails. Get one (little) thing done
+ first - THEN check your email.&lt;/li&gt;
+ &lt;li&gt;Try to define outcomes, not tasks. &quot;Ship XYZ&quot; instead of &quot;Work on XYZ&quot;.&lt;/li&gt;
+ &lt;li&gt;Meetings are time consuming, so &quot;Set a goal for each meeting&quot;.
+ Consider skipping a meeting if you don't have any goal set, unless it's a
+ beer-and-tell meeting! :)&lt;/li&gt;
+ &lt;li&gt;Constantly ask yourself if what you're working on is important.&lt;/li&gt;
+ &lt;li&gt;3-4 times a day ask yourself whether you are doing something towards
+ your goal or just finding something else to keep you busy. If you want
+ to look busy, take your phone and walk around the office with some
+ papers in your hand. Everybody will think that you are a busy person!
+ This way you can take a break and look busy at the same time!&lt;/li&gt;
+ &lt;li&gt;Take breaks! &lt;a class=&quot;reference external&quot; href=&quot;https://en.wikipedia.org/wiki/Pomodoro_Technique&quot;&gt;Pomodoro technique&lt;/a&gt; has this option
+ built-in. Taking breaks helps not only to avoid &lt;a class=&quot;reference external&quot; href=&quot;https://en.wikipedia.org/wiki/Repetitive_strain_injury&quot;&gt;RSI&lt;/a&gt;, but also
+ keeps your brain sane and gives you time to ask yourself the questions
+ mentioned above. I use &lt;a class=&quot;reference external&quot; href=&quot;http://www.workrave.org/&quot;&gt;Workrave&lt;/a&gt; on my
+ laptop, but you can use a real kitchen timer instead.&lt;/li&gt;
+ &lt;li&gt;Wear headphones, especially at office. Noise cancelling ones are even
+ better. White noise, nature sounds, or instrumental music are your
+ friends.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;section&quot; id=&quot;home-office&quot;&gt;
+ &lt;h3&gt;(Home) Office&lt;/h3&gt;
+ &lt;ul class=&quot;simple&quot;&gt;
+ &lt;li&gt;Make sure you enjoy your work environment. Why on the earth would you
+ spend your valuable time working without joy?!&lt;/li&gt;
+ &lt;li&gt;De-clutter and organize your desk. Less things around - less
+ distractions.&lt;/li&gt;
+ &lt;li&gt;Desk, chair, monitor, keyboard, mouse, etc - don't cheap out on them.
+ Your health is more important and expensive. Thanks to &lt;a class=&quot;reference external&quot; href=&quot;https://twitter.com/mhoye&quot;&gt;mhoye&lt;/a&gt; for this advice!&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;section&quot; id=&quot;other&quot;&gt;
+ &lt;h3&gt;Other&lt;/h3&gt;
+ &lt;ul class=&quot;simple&quot;&gt;
+ &lt;li&gt;Don't check email every 30 seconds. If there is an emergency, they
+ will call you! :)&lt;/li&gt;
+ &lt;li&gt;Reward yourself at a certain time. &quot;I'm going to have a chocolate at
+ 11am&quot;, or &quot;MFBT at 4pm sharp!&quot; are good examples. Don't forget, you
+ are &lt;a class=&quot;reference external&quot; href=&quot;https://en.wikipedia.org/wiki/Classical_conditioning&quot;&gt;Pavlov's dog&lt;/a&gt; too!&lt;/li&gt;
+ &lt;li&gt;Don't try to read everything NOW. Save it for later and read in a
+ batch.&lt;/li&gt;
+ &lt;li&gt;Capture all creative ideas. You can delete them later. ;)&lt;/li&gt;
+ &lt;li&gt;Prepare for next task before break. Make sure you know what's next, so
+ you can think about it during the break.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;This is my list of things that I try to use everyday. Looking forward to
+ see improvements!&lt;/p&gt;
+ &lt;p&gt;I would appreciate your thoughts this topic. Feel free to comment or
+ send a private email.&lt;/p&gt;
+ &lt;p&gt;Happy Productive New Year!&lt;/p&gt;
+ &lt;/div&gt;&lt;/div&gt;</content:encoded>
+ <dc:date>2016-01-21T02:06:37+00:00</dc:date>
+ <dc:creator>Rail Aliiev</dc:creator>
+ </item>
+ <item rdf:about="http://blog.rust-lang.org/2016/01/21/Rust-1.6.html">
+ <title>The Rust Programming Language Blog: Announcing Rust 1.6</title>
+ <link>http://blog.rust-lang.org/2016/01/21/Rust-1.6.html</link>
+ <content:encoded>&lt;p&gt;Hello 2016! We’re happy to announce the first Rust release of the year, 1.6.
+ Rust is a systems programming language focused on safety, speed, and
+ concurrency.&lt;/p&gt;
+
+ &lt;p&gt;As always, you can &lt;a href=&quot;http://www.rust-lang.org/install.html&quot;&gt;install Rust 1.6&lt;/a&gt; from the appropriate page on our
+ website, and check out the &lt;a href=&quot;https://github.com/rust-lang/rust/blob/stable/RELEASES.md#version-160-2016-01-21&quot;&gt;detailed release notes for 1.6&lt;/a&gt; on GitHub.
+ About 1100 patches were landed in this release.&lt;/p&gt;
+
+ &lt;h3 id=&quot;what-39-s-in-1-6-stable&quot;&gt;What’s in 1.6 stable&lt;/h3&gt;
+
+ &lt;p&gt;This release contains a number of small refinements, one major feature, and
+ a change to &lt;a href=&quot;https://crates.io&quot;&gt;Crates.io&lt;/a&gt;.&lt;/p&gt;
+
+ &lt;h4 id=&quot;libcore-stabilization&quot;&gt;libcore stabilization&lt;/h4&gt;
+
+ &lt;p&gt;The largest new feature in 1.6 is that &lt;a href=&quot;http://doc.rust-lang.org/nightly/core/&quot;&gt;&lt;code&gt;libcore&lt;/code&gt;&lt;/a&gt; is now stable! Rust’s
+ standard library is two-tiered: there’s a small core library, &lt;code&gt;libcore&lt;/code&gt;, and
+ the full standard library, &lt;code&gt;libstd&lt;/code&gt;, that builds on top of it. &lt;code&gt;libcore&lt;/code&gt; is
+ completely platform agnostic, and requires only a handful of external symbols
+ to be defined. Rust’s &lt;code&gt;libstd&lt;/code&gt; builds on top of &lt;code&gt;libcore&lt;/code&gt;, adding support for
+ memory allocation, I/O, and concurrency. Applications using Rust in the
+ embedded space, as well as those writing operating systems, often eschew
+ &lt;code&gt;libstd&lt;/code&gt;, using only &lt;code&gt;libcore&lt;/code&gt;.&lt;/p&gt;
+
+ &lt;p&gt;&lt;code&gt;libcore&lt;/code&gt; being stabilized is a major step towards being able to write the
+ lowest levels of software using stable Rust. There’s still future work to be
+ done, however. This will allow for a library ecosystem to develop around
+ &lt;code&gt;libcore&lt;/code&gt;, but &lt;em&gt;applications&lt;/em&gt; are not fully supported yet. Expect to hear more
+ about this in future release notes.&lt;/p&gt;
+
+ &lt;h4 id=&quot;library-stabilizations&quot;&gt;Library stabilizations&lt;/h4&gt;
+
+ &lt;p&gt;About 30 library functions and methods are now stable in 1.6. Notable
+ improvements include:&lt;/p&gt;
+
+ &lt;p&gt;The &lt;code&gt;drain()&lt;/code&gt; family of functions on collections. These methods let you move
+ elements out of a collection while allowing them to retain their backing
+ memory, reducing allocation in certain situations.&lt;/p&gt;
+
+ &lt;p&gt;A number of implementations of &lt;code&gt;From&lt;/code&gt; for converting between standard library
+ types, mainly between various integral and floating-point types.&lt;/p&gt;
+
+ &lt;p&gt;Finally, &lt;code&gt;Vec::extend_from_slice()&lt;/code&gt;, which was previously known as
+ &lt;code&gt;push_all()&lt;/code&gt;. This method has a significantly faster implementation than the
+ more general &lt;code&gt;extend()&lt;/code&gt;.&lt;/p&gt;
+
+ &lt;p&gt;See the &lt;a href=&quot;https://github.com/rust-lang/rust/blob/stable/RELEASES.md#version-160-2016-01-21&quot;&gt;detailed release notes&lt;/a&gt; for more.&lt;/p&gt;
+
+ &lt;h4 id=&quot;crates-io-disallows-wildcards&quot;&gt;Crates.io disallows wildcards&lt;/h4&gt;
+
+ &lt;p&gt;If you maintain a crate on &lt;a href=&quot;https://crates.io&quot;&gt;Crates.io&lt;/a&gt;, you might have seen
+ a warning: newly uploaded crates are no longer allowed to use a wildcard when
+ describing their dependencies. In other words, this is not allowed:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dependencies&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
+ &lt;span class=&quot;n&quot;&gt;regex&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;*&quot;&lt;/span&gt;
+ &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
+ &lt;p&gt;Instead, you must actually specify &lt;a href=&quot;http://doc.crates.io/crates-io.html#using-cratesio-based-crates&quot;&gt;a specific version or range of
+ versions&lt;/a&gt;, using one of the &lt;code&gt;semver&lt;/code&gt; crate’s various options: &lt;code&gt;^&lt;/code&gt;,
+ &lt;code&gt;~&lt;/code&gt;, or &lt;code&gt;=&lt;/code&gt;.&lt;/p&gt;
+
+ &lt;p&gt;A wildcard dependency means that you work with any possible version of your
+ dependency. This is highly unlikely to be true, and causes unnecessary breakage
+ in the ecosystem. We’ve been advertising this change as a warning for some time;
+ now it’s time to turn it into an error.&lt;/p&gt;
+
+ &lt;h3 id=&quot;contributors-to-1-6&quot;&gt;Contributors to 1.6&lt;/h3&gt;
+
+ &lt;p&gt;We had 132 individuals contribute to 1.6. Thank you so much!&lt;/p&gt;
+
+ &lt;ul&gt;
+ &lt;li&gt;Aaron Turon&lt;/li&gt;
+ &lt;li&gt;Adam Badawy&lt;/li&gt;
+ &lt;li&gt;Aleksey Kladov&lt;/li&gt;
+ &lt;li&gt;Alexander Bulaev&lt;/li&gt;
+ &lt;li&gt;Alex Burka&lt;/li&gt;
+ &lt;li&gt;Alex Crichton&lt;/li&gt;
+ &lt;li&gt;Alex Gaynor&lt;/li&gt;
+ &lt;li&gt;Alexis Beingessner&lt;/li&gt;
+ &lt;li&gt;Amanieu d'Antras&lt;/li&gt;
+ &lt;li&gt;Amit Saha&lt;/li&gt;
+ &lt;li&gt;Andrea Canciani&lt;/li&gt;
+ &lt;li&gt;Andrew Paseltiner&lt;/li&gt;
+ &lt;li&gt;androm3da&lt;/li&gt;
+ &lt;li&gt;angelsl&lt;/li&gt;
+ &lt;li&gt;Angus Lees&lt;/li&gt;
+ &lt;li&gt;Antti Keränen&lt;/li&gt;
+ &lt;li&gt;arcnmx&lt;/li&gt;
+ &lt;li&gt;Ariel Ben-Yehuda&lt;/li&gt;
+ &lt;li&gt;Ashkan Kiani&lt;/li&gt;
+ &lt;li&gt;Barosl Lee&lt;/li&gt;
+ &lt;li&gt;Benjamin Herr&lt;/li&gt;
+ &lt;li&gt;Ben Striegel&lt;/li&gt;
+ &lt;li&gt;Bhargav Patel&lt;/li&gt;
+ &lt;li&gt;Björn Steinbrink&lt;/li&gt;
+ &lt;li&gt;Boris Egorov&lt;/li&gt;
+ &lt;li&gt;bors&lt;/li&gt;
+ &lt;li&gt;Brian Anderson&lt;/li&gt;
+ &lt;li&gt;Bruno Tavares&lt;/li&gt;
+ &lt;li&gt;Bryce Van Dyk&lt;/li&gt;
+ &lt;li&gt;Cameron Sun&lt;/li&gt;
+ &lt;li&gt;Christopher Sumnicht&lt;/li&gt;
+ &lt;li&gt;Cole Reynolds&lt;/li&gt;
+ &lt;li&gt;corentih&lt;/li&gt;
+ &lt;li&gt;Daniel Campbell&lt;/li&gt;
+ &lt;li&gt;Daniel Keep&lt;/li&gt;
+ &lt;li&gt;Daniel Rollins&lt;/li&gt;
+ &lt;li&gt;Daniel Trebbien&lt;/li&gt;
+ &lt;li&gt;Danilo Bargen&lt;/li&gt;
+ &lt;li&gt;Devon Hollowood&lt;/li&gt;
+ &lt;li&gt;Doug Goldstein&lt;/li&gt;
+ &lt;li&gt;Dylan McKay&lt;/li&gt;
+ &lt;li&gt;ebadf&lt;/li&gt;
+ &lt;li&gt;Eli Friedman&lt;/li&gt;
+ &lt;li&gt;Eric Findlay&lt;/li&gt;
+ &lt;li&gt;Erik Davidson&lt;/li&gt;
+ &lt;li&gt;Felix S. Klock II&lt;/li&gt;
+ &lt;li&gt;Florian Hahn&lt;/li&gt;
+ &lt;li&gt;Florian Hartwig&lt;/li&gt;
+ &lt;li&gt;Gleb Kozyrev&lt;/li&gt;
+ &lt;li&gt;Guillaume Gomez&lt;/li&gt;
+ &lt;li&gt;Huon Wilson&lt;/li&gt;
+ &lt;li&gt;Igor Shuvalov&lt;/li&gt;
+ &lt;li&gt;Ivan Ivaschenko&lt;/li&gt;
+ &lt;li&gt;Ivan Kozik&lt;/li&gt;
+ &lt;li&gt;Ivan Stankovic&lt;/li&gt;
+ &lt;li&gt;Jack Fransham&lt;/li&gt;
+ &lt;li&gt;Jake Goulding&lt;/li&gt;
+ &lt;li&gt;Jake Worth&lt;/li&gt;
+ &lt;li&gt;James Miller&lt;/li&gt;
+ &lt;li&gt;Jan Likar&lt;/li&gt;
+ &lt;li&gt;Jean Maillard&lt;/li&gt;
+ &lt;li&gt;Jeffrey Seyfried&lt;/li&gt;
+ &lt;li&gt;Jethro Beekman&lt;/li&gt;
+ &lt;li&gt;John KÃ¥re Alsaker&lt;/li&gt;
+ &lt;li&gt;John Talling&lt;/li&gt;
+ &lt;li&gt;Jonas Schievink&lt;/li&gt;
+ &lt;li&gt;Jonathan S&lt;/li&gt;
+ &lt;li&gt;Jose Narvaez&lt;/li&gt;
+ &lt;li&gt;Josh Austin&lt;/li&gt;
+ &lt;li&gt;Josh Stone&lt;/li&gt;
+ &lt;li&gt;Joshua Holmer&lt;/li&gt;
+ &lt;li&gt;JP Sugarbroad&lt;/li&gt;
+ &lt;li&gt;jrburke&lt;/li&gt;
+ &lt;li&gt;Kevin Butler&lt;/li&gt;
+ &lt;li&gt;Kevin Yeh&lt;/li&gt;
+ &lt;li&gt;Kohei Hasegawa&lt;/li&gt;
+ &lt;li&gt;Kyle Mayes&lt;/li&gt;
+ &lt;li&gt;Lee Jeffery&lt;/li&gt;
+ &lt;li&gt;Manish Goregaokar&lt;/li&gt;
+ &lt;li&gt;Marcell Pardavi&lt;/li&gt;
+ &lt;li&gt;Markus Unterwaditzer&lt;/li&gt;
+ &lt;li&gt;Martin Pool&lt;/li&gt;
+ &lt;li&gt;Marvin Löbel&lt;/li&gt;
+ &lt;li&gt;Matt Brubeck&lt;/li&gt;
+ &lt;li&gt;Matthias Bussonnier&lt;/li&gt;
+ &lt;li&gt;Matthias Kauer&lt;/li&gt;
+ &lt;li&gt;mdinger&lt;/li&gt;
+ &lt;li&gt;Michael Layzell&lt;/li&gt;
+ &lt;li&gt;Michael Neumann&lt;/li&gt;
+ &lt;li&gt;Michael Sproul&lt;/li&gt;
+ &lt;li&gt;Michael Woerister&lt;/li&gt;
+ &lt;li&gt;Mihaly Barasz&lt;/li&gt;
+ &lt;li&gt;Mika Attila&lt;/li&gt;
+ &lt;li&gt;mitaa&lt;/li&gt;
+ &lt;li&gt;Ms2ger&lt;/li&gt;
+ &lt;li&gt;Nicholas Mazzuca&lt;/li&gt;
+ &lt;li&gt;Nick Cameron&lt;/li&gt;
+ &lt;li&gt;Niko Matsakis&lt;/li&gt;
+ &lt;li&gt;Ole Krüger&lt;/li&gt;
+ &lt;li&gt;Oliver Middleton&lt;/li&gt;
+ &lt;li&gt;Oliver Schneider&lt;/li&gt;
+ &lt;li&gt;Ori Avtalion&lt;/li&gt;
+ &lt;li&gt;Paul A. Jungwirth&lt;/li&gt;
+ &lt;li&gt;Peter Atashian&lt;/li&gt;
+ &lt;li&gt;Philipp Matthias Schäfer&lt;/li&gt;
+ &lt;li&gt;pierzchalski&lt;/li&gt;
+ &lt;li&gt;Ravi Shankar&lt;/li&gt;
+ &lt;li&gt;Ricardo Martins&lt;/li&gt;
+ &lt;li&gt;Ricardo Signes&lt;/li&gt;
+ &lt;li&gt;Richard Diamond&lt;/li&gt;
+ &lt;li&gt;Rizky Luthfianto&lt;/li&gt;
+ &lt;li&gt;Ryan Scheel&lt;/li&gt;
+ &lt;li&gt;Scott Olson&lt;/li&gt;
+ &lt;li&gt;Sean Griffin&lt;/li&gt;
+ &lt;li&gt;Sebastian Hahn&lt;/li&gt;
+ &lt;li&gt;Sébastien Marie&lt;/li&gt;
+ &lt;li&gt;Seo Sanghyeon&lt;/li&gt;
+ &lt;li&gt;Simonas Kazlauskas&lt;/li&gt;
+ &lt;li&gt;Simon Sapin&lt;/li&gt;
+ &lt;li&gt;Stepan Koltsov&lt;/li&gt;
+ &lt;li&gt;Steve Klabnik&lt;/li&gt;
+ &lt;li&gt;Steven Fackler&lt;/li&gt;
+ &lt;li&gt;Tamir Duberstein&lt;/li&gt;
+ &lt;li&gt;Tobias Bucher&lt;/li&gt;
+ &lt;li&gt;Toby Scrace&lt;/li&gt;
+ &lt;li&gt;Tshepang Lekhonkhobe&lt;/li&gt;
+ &lt;li&gt;Ulrik Sverdrup&lt;/li&gt;
+ &lt;li&gt;Vadim Chugunov&lt;/li&gt;
+ &lt;li&gt;Vadim Petrochenkov&lt;/li&gt;
+ &lt;li&gt;William Throwe&lt;/li&gt;
+ &lt;li&gt;xd1le&lt;/li&gt;
+ &lt;li&gt;Xmasreturns&lt;/li&gt;
+ &lt;/ul&gt;</content:encoded>
+ <dc:date>2016-01-21T00:00:00+00:00</dc:date>
+ </item>
+ <item rdf:about="http://blog.mozilla.org/addons/?p=7644">
+ <title>Mozilla Addons Blog: Archiving AMO Stats</title>
+ <link>https://blog.mozilla.org/addons/2016/01/20/archiving-amo-stats/</link>
+ <content:encoded>&lt;p&gt;One of the advantages of listing an add-on or theme on &lt;a href=&quot;https://addons.mozilla.org&quot; target=&quot;_blank&quot;&gt;addons.mozilla.org&lt;/a&gt; (AMO) is that you’ll get statistics on your add-on’s usage. These stats, which are covered by the &lt;a href=&quot;https://www.mozilla.org/privacy/&quot; target=&quot;_blank&quot;&gt;Mozilla privacy policy&lt;/a&gt;, provide add-on developers with information such as the number of downloads and daily users, among other insights.&lt;/p&gt;
+ &lt;p&gt;Currently, the data that generates these statistics can go back as far as 2007, as we haven’t had an archiving policy. As a result, statistics take up the vast majority of disk space in our database and require a significant amount of processing and operations time. Statistics over a year old are very rarely accessed, and the value of their generation is very low, while the costs are increasing.&lt;/p&gt;
+ &lt;p&gt;To reduce our operating and development costs, and increase the site’s reliability for developers, we are introducing an archiving policy.&lt;/p&gt;
+ &lt;p&gt;In the coming weeks, statistics data &lt;strong&gt;over one year old&lt;/strong&gt; will no longer be stored in the AMO database, and reports generated from them will no longer be accessible through AMO’s add-on statistics pages. Instead, the data will be archived and maintained as plain text files, which developers can download. We will write a follow-up post when these archives become available.&lt;/p&gt;
+ &lt;p&gt;If you’ve chosen to keep your add-on’s statistics private, they will remain private when stats are archived. You can check your privacy settings by going to your add-on in the &lt;a href=&quot;https://addons.mozilla.org/developers/addons&quot; target=&quot;_blank&quot;&gt;Developer Hub&lt;/a&gt;, clicking on &lt;strong&gt;Edit Listing&lt;/strong&gt;, and then &lt;strong&gt;Technical Details&lt;/strong&gt;.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;https://blog.mozilla.org/addons/files/2016/01/Screenshot-2016-01-20-14.52.33.png&quot;&gt;&lt;img alt=&quot;editlisting&quot; class=&quot;alignnone size-large wp-image-7645&quot; height=&quot;389&quot; src=&quot;https://blog.mozilla.org/addons/files/2016/01/Screenshot-2016-01-20-14.52.33-600x389.png&quot; width=&quot;600&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
+ &lt;p&gt;The total number of users and other cumulative counts on add-ons and themes will not be affected and these will continue to function.&lt;/p&gt;
+ &lt;p&gt;If you have feedback or concerns, please head to our &lt;a href=&quot;https://discourse.mozilla-community.org/t/archiving-of-add-on-statistics/6573&quot; target=&quot;_blank&quot;&gt;forum post&lt;/a&gt; on this topic.&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-20T23:54:09+00:00</dc:date>
+ <dc:creator>Andy McKay</dc:creator>
+ </item>
+ <item rdf:about="https://air.mozilla.org/the-joy-of-coding-episode-41/">
+ <title>Air Mozilla: The Joy of Coding - Episode 41</title>
+ <link>https://air.mozilla.org/the-joy-of-coding-episode-41/</link>
+ <content:encoded>&lt;p&gt;
+ &lt;img alt=&quot;The Joy of Coding - Episode 41&quot; class=&quot;wp-post-image&quot; height=&quot;90&quot; src=&quot;https://air.cdn.mozilla.net/media/cache/cb/68/cb68b6ac48452be7e7f25ddc7b63c959.png&quot; width=&quot;160&quot; /&gt;
+ mconley livehacks on real Firefox bugs while thinking aloud.
+ &lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-20T18:00:00+00:00</dc:date>
+ <dc:creator>Air Mozilla</dc:creator>
+ </item>
+ <item rdf:about="http://blog.mozilla.org/nfroyd/?p=452">
+ <title>Nathan Froyd: gecko and c++ onboarding presentation</title>
+ <link>https://blog.mozilla.org/nfroyd/2016/01/20/gecko-and-c-onboarding-presentation/</link>
+ <content:encoded>&lt;p&gt;One of the things the Firefox team has been doing recently is having onboarding sessions for new hires. This onboarding currently covers:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;1st day setup&lt;/li&gt;
+ &lt;li&gt;Bugzilla&lt;/li&gt;
+ &lt;li&gt;Building Firefox&lt;/li&gt;
+ &lt;li&gt;Desktop Firefox Architecture / Product&lt;/li&gt;
+ &lt;li&gt;Communication and Community&lt;/li&gt;
+ &lt;li&gt;Javascript and the DOM&lt;/li&gt;
+ &lt;li&gt;C++ and Gecko&lt;/li&gt;
+ &lt;li&gt;Shipping Software&lt;/li&gt;
+ &lt;li&gt;Telemetry&lt;/li&gt;
+ &lt;li&gt;Org structure and career development&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;My first day consisted of some useful HR presentations and then I was given my laptop and a pointer to a wiki page on building Firefox. Needless to say, it took me a while to get started! It would have been super convenient to have an introduction to all the stuff above.&lt;/p&gt;
+ &lt;p&gt;I’ve been asked to do the C++ and Gecko session three times. All of the sessions are open to whoever wants to come, not just the new hires, and I think yesterday’s session was easily the most well-attended yet: somewhere between 10 and 20 people showed up. Yesterday’s session was the first session where I made the slides available to attendees (should have been doing that from the start…) and it seemed equally useful to make the slides available to a broader audience as well. The &lt;a href=&quot;https://docs.google.com/presentation/d/1ZHUkNzZK2TrF5_4MWd_lqEq7Ph5B6CDbNsizIkBxbnQ/edit?usp=sharing&quot;&gt;Gecko and C++ Onboarding slides&lt;/a&gt; are up now!&lt;/p&gt;
+ &lt;p&gt;This presentation is a “living†presentation; it will get updated for future sessions with feedback and as I think of things that should have been in the presentation or better ways to set things up (some diagrams would be nice…). If you have feedback (good, bad, or ugly) on particular things in the slides or you have suggestions on what other things should be covered, please contact me! Next time I do this I’ll try to record the presentation so folks can watch that if they prefer.&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-20T16:48:29+00:00</dc:date>
+ <dc:creator>Nathan Froyd</dc:creator>
+ </item>
+ <item rdf:about="http://andreasgal.com/?p=573">
+ <title>Andreas Gal: Brendan is back to save the Web</title>
+ <link>http://andreasgal.com/2016/01/20/brendan-is-back-to-save-the-web/</link>
+ <content:encoded>&lt;p class=&quot;p1&quot;&gt;Brendan is &lt;a href=&quot;https://github.com/brave&quot;&gt;back&lt;/a&gt;, and he has a &lt;a href=&quot;http://brave.com/&quot;&gt;plan&lt;/a&gt; to save the Web. Its a big and bold plan, and it may just work. I am pretty excited about this. If you have 5 minutes to read along I’ll explain why I think you should be as well.&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;&lt;strong&gt;The Web is broken&lt;/strong&gt;&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;Lets face it, the Web today is a mess. Everywhere we go online we are constantly inundated with annoying ads. Often pages are more ads than content, and the more ads the industry throws at us, the more we ignore them, the more obnoxious ads get, trying to catch our attention. As Brendan explains in his blog post, the browser used to be on the user’s side—we call browsers the user agent for a reason. Part of the early success of Firefox was that it blocked popup ads. But somewhere over the last 10 years of modern Web browsers, browsers lost their way and stopped being the user’s agent alone. Why?&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;&lt;strong&gt;Browsers aren’t free&lt;/strong&gt;&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;Making a modern Web browser is not free. It takes hundreds of engineers to make a competitive modern browser engine. Someone has to pay for that, and that someone needs to have a reason to pay for it. Google doesn’t make Chrome for the good of mankind. Google makes Chrome so you can consume more Web and along with it, more Google ads. Each time you click on one, Google makes more money. Chrome is a billion dollar business for Google. And the same is true for pretty much every other browser. Every major browser out there is funded through advertisement. No browser maker can escape this dilemma. Maybe now you understand why no major browser ships with a builtin enabled by default ad-blocker, even though ad-blockers are by far the most popular add-ons.&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;&lt;strong&gt;Our privacy is at stake&lt;/strong&gt;&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;It’s not just the unregulated flood of advertisement that needs a solution. Every ad you see is often selected based on sensitive private information advertisement networks have extracted from your browsing behavior through tracking. Remember how the FBI used to track what books Americans read at the library, and it was a big scandal? Today the Googles and Facebooks of the world know almost every site you visit, everything you buy online, and they use this data to target you with advertisement. I am often puzzled why people are so afraid of the NSA spying on us but show so little concern about all the deeply personal data Google and Facebook are amassing about everyone.&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;&lt;strong&gt;Blocking alone doesn’t scale&lt;/strong&gt;&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;I wish the solution was as easy as just blocking all ads. There is a lot of great Web content out there: news, entertainment, educational content. It’s not free to make all this content, but we have gotten used to consuming it “for freeâ€. Banning all ads without an alternative mechanism would break the economic backbone of the Web. This dilemma has existed for many years, and the big browser vendors seem to have given up on it. It’s hard to blame them. How do you disrupt the status quo without sawing off the (ad revenue) branch you are sitting on?&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;&lt;strong&gt;It takes an newcomer to fix this mess&lt;/strong&gt;&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;I think its unlikely that the incumbent browser vendors will make any bold moves to solve this mess. There is too much money at stake. I am excited to see a startup take a swipe at this problem, because they have little to lose (seed money aside). Brave is getting the user agent back into the game. Browsers have intentionally remained silent onlookers to the ad industry invading users’ privacy. With Brave, Brendan makes the user agent step up and fight for the user as it was always intended to do.&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;Brave basically consists of two parts: part one blocks third party ad content and tracking signals. Instead of these Brave inserts alternative ad content. Sites can sign up to get a fair share of any ads that Brave displays for them. The big change in comparison to the status quo is that the Brave user agent is in control and can regulate what you see. It’s like a speed limit for advertisement on the Web, with the goal to restore balance and give sites a fair way to monetize while giving the user control through the user agent.&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;&lt;strong&gt;Making money with a better Web&lt;/strong&gt;&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;The ironic part of Brave is that its for-profit. Brave can make money by reducing obnoxious ads and protecting your privacy at the same time. If Brave succeeds, it’s going to drain money away from the crappy privacy-invasive obnoxious advertisement world we have today, and publishers and sites will start transacting in the new Brave world that is regulated by the user agent. Brave will take a cut of these transactions. And I think this is key. It aligns the incentives right. The current funding structure of major browsers encourages them to keep things as they are. Brave’s incentive is to bring down the whole diseased temple and usher in a better Web. Exciting.&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;&lt;strong&gt;Quick update:&lt;/strong&gt; I had a chance to look over the Brave GitHub repo. It looks like the Brave Desktop browser is based on Chromium, not Gecko. Yes, you read that right. &lt;span style=&quot;text-decoration: underline;&quot;&gt;Brave is using Google’s rendering engine, not Mozilla’s.&lt;/span&gt; Much to write about this one, but it will definitely help Brave “hide†better in the large volume of Chrome users, making it harder for sites to identify and block Brave users. Brave for iOS seems to be a &lt;span style=&quot;text-decoration: underline;&quot;&gt;fork of Firefox for iOS, but it manages to block ads&lt;/span&gt; (Mozilla says they can’t).&lt;/p&gt;&lt;br /&gt;Filed under: &lt;a href=&quot;http://andreasgal.com/category/mozilla/&quot;&gt;Mozilla&lt;/a&gt; &lt;a href=&quot;http://feeds.wordpress.com/1.0/gocomments/andreasgal.wordpress.com/573/&quot; rel=&quot;nofollow&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; src=&quot;http://feeds.wordpress.com/1.0/comments/andreasgal.wordpress.com/573/&quot; /&gt;&lt;/a&gt; &lt;a href=&quot;http://feeds.wordpress.com/1.0/godelicious/andreasgal.wordpress.com/573/&quot; rel=&quot;nofollow&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; src=&quot;http://feeds.wordpress.com/1.0/delicious/andreasgal.wordpress.com/573/&quot; /&gt;&lt;/a&gt; &lt;a href=&quot;http://feeds.wordpress.com/1.0/gofacebook/andreasgal.wordpress.com/573/&quot; rel=&quot;nofollow&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; src=&quot;http://feeds.wordpress.com/1.0/facebook/andreasgal.wordpress.com/573/&quot; /&gt;&lt;/a&gt; &lt;a href=&quot;http://feeds.wordpress.com/1.0/gotwitter/andreasgal.wordpress.com/573/&quot; rel=&quot;nofollow&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; src=&quot;http://feeds.wordpress.com/1.0/twitter/andreasgal.wordpress.com/573/&quot; /&gt;&lt;/a&gt; &lt;a href=&quot;http://feeds.wordpress.com/1.0/gostumble/andreasgal.wordpress.com/573/&quot; rel=&quot;nofollow&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; src=&quot;http://feeds.wordpress.com/1.0/stumble/andreasgal.wordpress.com/573/&quot; /&gt;&lt;/a&gt; &lt;a href=&quot;http://feeds.wordpress.com/1.0/godigg/andreasgal.wordpress.com/573/&quot; rel=&quot;nofollow&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; src=&quot;http://feeds.wordpress.com/1.0/digg/andreasgal.wordpress.com/573/&quot; /&gt;&lt;/a&gt; &lt;a href=&quot;http://feeds.wordpress.com/1.0/goreddit/andreasgal.wordpress.com/573/&quot; rel=&quot;nofollow&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; src=&quot;http://feeds.wordpress.com/1.0/reddit/andreasgal.wordpress.com/573/&quot; /&gt;&lt;/a&gt; &lt;img alt=&quot;&quot; border=&quot;0&quot; height=&quot;1&quot; src=&quot;http://pixel.wp.com/b.gif?host=andreasgal.com&amp;amp;blog=891661&amp;amp;post=573&amp;amp;subd=andreasgal&amp;amp;ref=&amp;amp;feed=1&quot; width=&quot;1&quot; /&gt;</content:encoded>
+ <dc:date>2016-01-20T16:00:00+00:00</dc:date>
+ <dc:creator>Andreas</dc:creator>
+ </item>
+ <item rdf:about="https://miketaylr.com/posts/2016/01/at-media-webkit-transform-three-dee.html">
+ <title>Mike Taylor: 🙅 @media (-webkit-transform-3d)</title>
+ <link>https://miketaylr.com/posts/2016/01/at-media-webkit-transform-three-dee.html</link>
+ <content:encoded>&lt;p&gt;&lt;code&gt;@media (-webkit-transform-3d)&lt;/code&gt; is a funny thing that exists on the web.&lt;/p&gt;
+
+ &lt;p&gt;It's like, a &lt;a href=&quot;https://drafts.csswg.org/mediaqueries-4/#mq-features&quot;&gt;media query feature&lt;/a&gt; in the form of a prefixed CSS property, which should tell you if your (once upon a time probably Safari-only) browser supports 3D transforms, invented back in the day before we had &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@supports&quot;&gt;&lt;code&gt;@supports&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
+
+ &lt;p&gt;(According to &lt;a href=&quot;https://developer.apple.com/library/safari/documentation/AppleApplications/Reference/SafariCSSRef/Articles/OtherStandardCSS3Features.html#//apple_ref/doc/uid/TP40007601-SW3&quot;&gt;Apple docs&lt;/a&gt; it first appeared in Safari 4, along side the other &lt;code&gt;-webkit-transition&lt;/code&gt; and &lt;code&gt;-webkit-transform-2d&lt;/code&gt; hybrid-media-query-feature-prefixed-css-properties-things that you should immediately forget exist.)&lt;/p&gt;
+
+ &lt;p&gt;Older versions of Modernizr &lt;a href=&quot;https://github.com/Modernizr/Modernizr/blob/66c694d136241d356e0d24fcbaa5c068b0b0cdae/feature-detects/css/transforms3d.js#L26-L27&quot;&gt;used this (and only this)&lt;/a&gt; to detect support for 3D transforms, and that seemed pretty OK. (They also did the polite thing and tested &lt;code&gt;@media (transform-3d)&lt;/code&gt;, but no browser has ever actually supported that, as it turns out). And because they're so consistently polite, they've since &lt;a href=&quot;https://github.com/patrickkettner/Modernizr/commit/a54308e47e269a058472854b1ef417bd54f4e616&quot;&gt;updated the test&lt;/a&gt; to prefer &lt;code&gt;@supports&lt;/code&gt; too (via a pull request from Edge developer Jacob Rossi).&lt;/p&gt;
+
+ &lt;p&gt;As it turns out other browsers have been &lt;a href=&quot;http://caniuse.com/#feat=transforms3d&quot;&gt;updated to support 3D CSS transforms&lt;/a&gt;, but sites didn't go back and update their version of Modernizr. So unless you support &lt;code&gt;@media (-webkit-transform-3d)&lt;/code&gt; these sites break. Niche websites like &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1239136&quot;&gt;yahoo.com&lt;/a&gt; and &lt;a href=&quot;https://github.com/webcompat/web-bugs/issues/2151&quot;&gt;about.com&lt;/a&gt;.&lt;/p&gt;
+
+ &lt;p&gt;So, anyways. I added &lt;a href=&quot;https://compat.spec.whatwg.org/#css-media-queries-webkit-transform-3d&quot;&gt;&lt;code&gt;@media (-webkit-transform-3d)&lt;/code&gt; to the Compat Standard&lt;/a&gt; and we &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1239799&quot;&gt;added support for it Firefox&lt;/a&gt; so websites stop breaking.&lt;/p&gt;
+
+ &lt;p&gt;But you shouldn't ever use it—use &lt;code&gt;@supports&lt;/code&gt;. In fact, don't even share this blog post. Maybe delete it from your browser history just in case.&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-20T08:00:00+00:00</dc:date>
+ <dc:creator>Mike Taylor</dc:creator>
+ </item>
+ <item rdf:about="http://globau.wordpress.com/?p=881">
+ <title>Byron Jones: happy bmo push day!</title>
+ <link>https://globau.wordpress.com/2016/01/20/happy-bmo-push-day-166/</link>
+ <content:encoded>&lt;p&gt;the following changes have been pushed to bugzilla.mozilla.org:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;[&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1236161&quot; target=&quot;_blank&quot;&gt;1236161&lt;/a&gt;] when converting a BMP attachment to PNG fails a zero byte attachment is created&lt;/li&gt;
+ &lt;li&gt;[&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1231918&quot; target=&quot;_blank&quot;&gt;1231918&lt;/a&gt;] error handler doesn’t close multi-part responses&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;discuss these changes on &lt;a href=&quot;https://lists.mozilla.org/listinfo/tools-bmo&quot; target=&quot;_blank&quot;&gt;mozilla.tools.bmo&lt;/a&gt;.&lt;/p&gt;&lt;br /&gt;Filed under: &lt;a href=&quot;https://globau.wordpress.com/category/mozilla/bmo/&quot;&gt;bmo&lt;/a&gt;, &lt;a href=&quot;https://globau.wordpress.com/category/mozilla/&quot;&gt;mozilla&lt;/a&gt; &lt;img alt=&quot;&quot; border=&quot;0&quot; height=&quot;1&quot; src=&quot;https://pixel.wp.com/b.gif?host=globau.wordpress.com&amp;amp;blog=25718030&amp;amp;post=881&amp;amp;subd=globau&amp;amp;ref=&amp;amp;feed=1&quot; width=&quot;1&quot; /&gt;</content:encoded>
+ <dc:date>2016-01-20T07:33:46+00:00</dc:date>
+ <dc:creator>glob</dc:creator>
+ </item>
+ <item rdf:about="https://www.alex-johnson.net/tag/mozilla/rss/85d84c54-ed0c-4ee5-beb3-8823edb3c074">
+ <title>Alex Johnson: Removing Honeycomb Code</title>
+ <link>https://www.alex-johnson.net/removing-honeycomb-code/</link>
+ <content:encoded>&lt;p&gt;As an effort to reduce the APK size of Firefox for Android and to remove unnecessary code, I will be helping remove the Honeycomb code throughout the Fennec project. Honeycomb will not be supported since Firefox 46, so this code is not necessary. &lt;br /&gt;
+ &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1217675&quot;&gt;Bug 1217675&lt;/a&gt; will keep track of the progress. &lt;br /&gt;
+ Hopefully this will help reduce the APK size some and clean up the road for &lt;a href=&quot;https://www.youtube.com/watch?v=NJ6kzW5t02Y&quot;&gt;killing Gingerbread&lt;/a&gt; hopefully sometime in the near future.&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-20T04:59:34+00:00</dc:date>
+ <dc:creator>Alex Johnson</dc:creator>
+ </item>
+ <item rdf:about="http://www.brianbondy.com/blog/id/172">
+ <title>Brian R. Bondy: Brave Software</title>
+ <link>http://www.brianbondy.com/blog/172/brave-software</link>
+ <content:encoded>&lt;p&gt;&lt;/p&gt;&lt;p&gt;Since June of last year, I’ve been co-founding a new startup called &lt;a href=&quot;https://brave.com/&quot;&gt;Brave Software&lt;/a&gt; with &lt;a href=&quot;https://en.wikipedia.org/wiki/Brendan_Eich&quot;&gt;Brendan Eich&lt;/a&gt;.
+ With our amazing team, we're developing something pretty epic.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
+ &lt;p&gt;&lt;/p&gt;&lt;p&gt;We're building the next-generation of browsers for smartphones and laptops as part of our new ad-tech platform.
+ Our terms of use give our users control over their personal data by blocking ad trackers and third party cookies.
+ We re-integrate fewer and better ads directly into programmatic ad positions, paying revenue shares to users and publishers to support both of these essential parties in the web ecosystem.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
+ &lt;p&gt;&lt;/p&gt;&lt;p&gt;Coming built in, we have new faster engines for tracking protection, ad block, HTTPS Everywhere, safe ads with rev-share, and more.
+ We're seeing massive web page load time speedups.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
+
+
+ &lt;p&gt;&lt;/p&gt;&lt;p&gt;We're starting to bring people in for early developer build access on all platforms.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
+ &lt;p&gt;&lt;/p&gt;&lt;p&gt;I’m happy to share that the browsers we’re developing were made fully open sourced.
+ We welcome contributors, and would love your help.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
+ &lt;p&gt;&lt;/p&gt;&lt;p&gt;Some of the repositories include:&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/brave/browser-laptop&quot;&gt;Brave OSX and Windows x64 browsers&lt;/a&gt;: Prototyped as a Gecko based browser, but now replaced with a powerful new browser built on top of the electron framework. The electron framework is the same one in use by Slack and the Atom editor. It uses the latest libchromiumcontent and Node.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/brave/link-bubble&quot;&gt;Brave for Android&lt;/a&gt;: Formerly Link Bubble, working as a background service so you can use other apps as your pages load.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/brave/browser-ios&quot;&gt;Brave for iOS&lt;/a&gt;: Originally forked from Firefox for iOS but with all of the built-in greatness described above.&lt;/li&gt;
+ &lt;li&gt;And many others: Website, updater code, vault, electron fork, and others.&lt;/li&gt;
+ &lt;/ul&gt;</content:encoded>
+ <dc:date>2016-01-20T00:00:00+00:00</dc:date>
+ <dc:creator>Brian R. Bondy</dc:creator>
+ </item>
+ <item rdf:about="http://coffeeonthekeyboard.com/rss/0388d8a6-fc86-477e-a161-1b356e01fe77">
+ <title>James Socol: PIEfection Slides Up</title>
+ <link>http://coffeeonthekeyboard.com/piefection-slides-up/</link>
+ <content:encoded>&lt;p&gt;I put &lt;a href=&quot;https://github.com/jsocol/talks/tree/master/2016-01-13-manhattanjs-pie&quot;&gt;the slides for my ManhattanJS talk, &quot;PIEfection&quot;&lt;/a&gt; up on GitHub the other day (sans images, but there are links in the source for all of those).&lt;/p&gt;
+
+ &lt;p&gt;I completely neglected to talk about the &lt;a href=&quot;https://en.wikipedia.org/wiki/Maillard_reaction&quot;&gt;Maillard reaction&lt;/a&gt;, which is responsible for food tasting good, and specifically for browning pie crusts. tl;dr: Amino acid (protein) + sugar + ~300°F (~150°C) = delicious. There are innumerable and poorly understood combinations of amino acids and sugars, but this class of reaction is responsible for everything from searing stakes to browning crusts to toasting marshmallows.&lt;/p&gt;
+
+ &lt;p&gt;Above ~330°F, you get caramelization, which is also a delicious part of the pie and crust, but you don't want to overdo it. Starting around ~400°F, you get pyrolysis (burning, charring, carbonization) and below 285°F the reaction won't occur (at least not quickly) so you won't get the delicious compounds.&lt;/p&gt;
+
+ &lt;p&gt;(All of these are, of course, temperatures measured in the material, not in the air of the oven.)&lt;/p&gt;
+
+ &lt;p&gt;So, instead of an egg wash on your top crust, try whole milk, which has more sugar to react with the gluten in the crust.&lt;/p&gt;
+
+ &lt;p&gt;I also didn't get a chance to mention a rolling technique I use, that I learned from a &lt;a href=&quot;https://www.facebook.com/ellenspirerstaffing&quot;&gt;cousin of mine&lt;/a&gt;, in whose baking shadow I happily live.&lt;/p&gt;
+
+ &lt;p&gt;When rolling out a crust after it's been in the fridge, first roll it out in a long stretch, then fold it in thirds; do it again; then start rolling it out into a round. Not only do you add more layer structure (mmm, flaky, delicious layers) but it'll fill in the cracks that often form if you try to roll it out directly, resulting in a stronger crust.&lt;/p&gt;
+
+ &lt;p&gt;Those &lt;a href=&quot;http://www.amazon.com/Cheese-Shaker-Pepper-Perforated-Stainless/dp/B007T40P28/ref=sr_1_1?ie=UTF8&amp;amp;qid=1453236391&amp;amp;sr=8-1&amp;amp;keywords=pizza+shaker&quot;&gt;pepper flake shakers&lt;/a&gt;, filled with flour, are a great way to keep adding flour to the workspace without worrying about your buttery hands.&lt;/p&gt;
+
+ &lt;p&gt;For transferring the crust to the pie plate, try rolling it up onto your rolling pin and unrolling it on the plate. &lt;a href=&quot;http://www.amazon.com/Ateco-20-Inch-Length-French-Rolling/dp/B000KESQ1G&quot;&gt;Tapered (or &quot;French&quot;) rolling pins&lt;/a&gt; (or wine bottle) are particularly good at this since they don't have moving parts.&lt;/p&gt;
+
+ &lt;p&gt;Finally, thanks again to &lt;a href=&quot;https://twitter.com/renrutnnej&quot;&gt;Jenn&lt;/a&gt; for helping me get pies from one island to another. It would not have been possible without her!&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-19T20:45:34+00:00</dc:date>
+ <dc:creator>James Socol</dc:creator>
+ </item>
+ <item rdf:about="https://air.mozilla.org/reprendre-le-controle-de-sa-vie-privee-sur-internet/">
+ <title>Air Mozilla: Reprendre le contrôle de sa vie privée sur Internet</title>
+ <link>https://air.mozilla.org/reprendre-le-controle-de-sa-vie-privee-sur-internet/</link>
+ <content:encoded>&lt;p&gt;
+ &lt;img alt=&quot;Reprendre le contrôle de sa vie privée sur Internet&quot; class=&quot;wp-post-image&quot; height=&quot;90&quot; src=&quot;https://air.cdn.mozilla.net/media/cache/be/f6/bef62897fb87e08dc8392fe61d10bcfa.png&quot; width=&quot;160&quot; /&gt;
+ L'omniprésence des réseaux sociaux, des moteurs de recherches et de la publicité est-elle compatible avec notre droit à la vie privée ?
+ &lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-19T18:00:00+00:00</dc:date>
+ <dc:creator>Air Mozilla</dc:creator>
+ </item>
+ <item rdf:about="https://mykzilla.org/?p=245">
+ <title>Myk Melez: New Year, New Blogware</title>
+ <link>https://mykzilla.org/2016/01/19/new-year-new-blogware/</link>
+ <content:encoded>&lt;p&gt;Four score and many moons ago, I decided to move this blog from Blogger to WordPress. The transition took longer than expected, but it’s finally done.&lt;/p&gt;
+ &lt;p&gt;If you’ve been following along at the old address, &lt;a href=&quot;https://mykzilla.blogspot.com/&quot;&gt;https://mykzilla.blogspot.com/&lt;/a&gt;, now’s the time to update your address book! If you’ve been going to &lt;a href=&quot;https://mykzilla.org/&quot;&gt;https://mykzilla.org/&lt;/a&gt;, however, or you read the blog on &lt;a href=&quot;http://planet.mozilla.org/&quot;&gt;Planet Mozilla&lt;/a&gt;, then there’s nothing to do, as that’s the new address, and Planet Mozilla has been updated to syndicate posts from it.&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-19T16:56:05+00:00</dc:date>
+ <dc:creator>Myk Melez</dc:creator>
+ </item>
+ <item rdf:about="http://michaelkohler.info/?p=348">
+ <title>Michael Kohler: Mozillas strategische Leitlinien für 2016 und danach</title>
+ <link>https://michaelkohler.info/2016/mozillas-strategische-leitlinien-fur-2016-und-danach</link>
+ <content:encoded>&lt;p&gt;Dieser Beitrag wurde zuerst im Blog auf&lt;a href=&quot;https://blog.mozilla.org/community&quot;&gt; https://blog.mozilla.org/community&lt;/a&gt; veröffentlicht. Herzlichen Dank an Aryx und Coce für die Übersetzung!&lt;/p&gt;
+ &lt;p&gt;Auf der ganzen Welt arbeiten leidenschaftliche Mozillianer am Fortschritt für Mozillas Mission. Aber fragt man fünf verschiedene Mozillianer, was die Mission ist, erhält man womöglich sieben verschiedene Antworten.&lt;/p&gt;
+ &lt;p&gt;Am Ende des letzten Jahres legte Mozillas CEO Chris Beard klare Vorstellungen über Mozillas Mission, Vision und Rolle dar und zeigte auf, wie unsere Produkte uns diesem Ziel in den nächsten fünf Jahren näher bringen. Das Ziel dieser strategischen Leitlinien besteht darin, für Mozilla insgesamt ein prägnantes, gemeinsames Verständnis unserer Ziele zu entwickeln, die uns als Individuen das Treffen von Entscheidungen und Erkennen von Möglichkeiten erleichtert, mit denen wir Mozilla voranbringen.&lt;/p&gt;
+ &lt;p&gt;Mozillas Mission können wir nicht alleine erreichen. Die Tausenden von Mozillianern auf der ganzen Welt müssen dahinter stehen, damit wir zügig und mit lauterer Stimme als je zuvor Unglaubliches erreichen können.&lt;/p&gt;
+ &lt;p&gt;Deswegen ist eine der sechs&lt;a href=&quot;https://docs.google.com/presentation/d/1A3Ma9gNawAYYGbYC2bUW0wUwcpHuvyMiZvHNiMLriw0/edit#slide=id.gdaa7a0bd0_1_0&quot;&gt; strategischen Initiativen&lt;/a&gt; des Participation Teams für die erste Jahreshälfte, möglichst viele Mozillianer über diese Leitlinien aufzuklären, damit wir 2016 den bisher wesentlichsten Einfluss erzielen können. Wir werden einen weiteren Beitrag veröffentlichen, der sich näher mit der Strategie des Participation Teams für das Jahr 2016 befassen wird.&lt;/p&gt;
+ &lt;p&gt;&lt;img alt=&quot;&quot; class=&quot;alignnone&quot; height=&quot;335&quot; src=&quot;https://ffp4g1ylyit3jdyti1hqcvtb-wpengine.netdna-ssl.com/community/files/2016/01/Screen-Shot-2015-12-18-at-2.02.07-PM-600x335.png&quot; width=&quot;600&quot; /&gt;&lt;/p&gt;
+ &lt;p&gt;Das Verstehen dieser Strategie wird unabdingbar sein für jeden, der bei Mozilla in diesem Jahr etwas bewirken möchte, denn sie wird bestimmen, wofür wir eintreten, wo wir unsere Ressourcen einsetzen und auf welche Projekte wir uns 2016 konzentrieren werden.&lt;/p&gt;
+ &lt;p&gt;Zu Jahresbeginn werden wir näher auf diese Strategie eingehen und weitere Details dazu bekanntgeben, wie die diversen Teams und Projekte bei Mozilla auf diese Ziele hinarbeiten.&lt;/p&gt;
+ &lt;p&gt;Der aktuelle Aufruf zum Handeln besteht darin, im Kontext Ihrer Arbeit über diese Ziele nachzudenken und darüber, wie Sie im kommenden Jahr bei Mozilla mitwirken möchten. Dies hilft, Ihre Innovationen, Ambitionen und Ihren Einfluss im Jahr 2016 zu gestalten.&lt;/p&gt;
+ &lt;p&gt;Wir hoffen, dass Sie mitdiskutieren und Ihre Fragen, Kommentare und Pläne für das Vorantreiben der strategischen Leitlinien im Jahr 2016&lt;a href=&quot;https://discourse.mozilla-community.org/t/mozillas-strategic-narrative-2016/6397&quot;&gt; hier&lt;/a&gt; auf Discourse teilen und Ihre Gedanken auf Twitter mit dem Hashtag &lt;a href=&quot;https://twitter.com/search?q=%23mozilla2016strategy&amp;amp;src=typd&quot;&gt;#Mozilla2016Strategy&lt;/a&gt; mitteilen.&lt;/p&gt;
+ &lt;p&gt; &lt;/p&gt;
+ &lt;h3&gt;Mission, Vision &amp;amp; Strategie&lt;/h3&gt;
+ &lt;p&gt;&lt;b&gt;Unsere Mission&lt;/b&gt;&lt;/p&gt;
+ &lt;p&gt;Dafür zu sorgen, dass das Internet eine weltweite öffentliche Ressource ist, die allen zugänglich ist.&lt;/p&gt;
+ &lt;p&gt;&lt;b&gt;Unsere Vision&lt;/b&gt;&lt;/p&gt;
+ &lt;p&gt;Ein Internet, für das Menschen tatsächlich an erster Stelle stehen. Ein Internet, in dem Menschen ihr eigenes Erlebnis gestalten können. Ein Internet, in dem die Menschen selbst entscheiden können sowie sicher und unabhängig sind.&lt;/p&gt;
+ &lt;p&gt;&lt;b&gt;Unsere Rolle&lt;/b&gt;&lt;/p&gt;
+ &lt;p&gt;Mozilla setzt sich im wahrsten Sinne des Wortes in Ihrem Online-Leben für Sie ein. Wir setzen uns für Sie ein, sowohl in Ihrem Online-Erlebnis als auch für Ihre Interessen beim Zustand des Internets.&lt;/p&gt;
+ &lt;p&gt;&lt;b&gt;Unsere Arbeit&lt;/b&gt;&lt;/p&gt;
+ &lt;p&gt;Unsere Säulen&lt;/p&gt;
+ &lt;ol&gt;
+ &lt;li&gt;&lt;b&gt;Produkte:&lt;/b&gt; Wir entwickeln Produkte mit Menschen im Mittelpunkt sowie Bildungsprogramme, mit deren Hilfe Menschen online ihr gesamtes Potential ausschöpfen können.&lt;/li&gt;
+ &lt;li&gt;&lt;b&gt;Technologie:&lt;/b&gt; Wir entwickeln robuste technische Lösungen, die das Internet über verschiedene Plattformen hinweg zum Leben erwecken.&lt;/li&gt;
+ &lt;li&gt;&lt;b&gt;Menschen:&lt;/b&gt; Wir entwickeln Führungspersonen und Mitwirkende in der Gemeinschaft, die das Internet erfinden, gestalten und verteidigen.&lt;/li&gt;
+ &lt;/ol&gt;
+ &lt;p&gt;Wir wir positive Veränderungen in Zukunft anpacken wollen&lt;/p&gt;
+ &lt;p&gt;Die Arbeitsweise ist ebensowichtig wie das Ziel. Unsere Gesundheit und bleibender Einfluss hängen davon ab, wie sehr unsere Produkte und Aktivitäten:&lt;/p&gt;
+ &lt;ol&gt;
+ &lt;li&gt;Interoperabilität, Open Source und offene Standards fördern,&lt;/li&gt;
+ &lt;li&gt;Gemeinschaften aufbauen und fördern,&lt;/li&gt;
+ &lt;li&gt;Für politische Veränderungen und rechtlichen Schutz eintreten sowie&lt;/li&gt;
+ &lt;li&gt;Netzbürger bilden und einbeziehen.&lt;/li&gt;
+ &lt;/ol&gt;
+ &lt;p&gt; &lt;/p&gt;
+ &lt;img alt=&quot;&quot; height=&quot;0&quot; src=&quot;http://piwik.michaelkohler.info/piwik.php?idsite=1&amp;amp;rec=1&amp;amp;url=https%3A%2F%2Fmichaelkohler.info%2F2016%2Fmozillas-strategische-leitlinien-fur-2016-und-danach&amp;amp;action_name=Mozillas+strategische+Leitlinien+f%C3%BCr+2016+und+danach&amp;amp;urlref=https%3A%2F%2Fmichaelkohler.info%2Ffeed&quot; style=&quot;border: 0; width: 0; height: 0;&quot; width=&quot;0&quot; /&gt;</content:encoded>
+ <dc:date>2016-01-19T15:27:24+00:00</dc:date>
+ <dc:creator>Michael Kohler</dc:creator>
+ </item>
+ <item rdf:about="http://dlawrence.wordpress.com/?p=27">
+ <title>David Lawrence: happy bmo push day!</title>
+ <link>https://dlawrence.wordpress.com/2016/01/19/happy-bmo-push-day-3/</link>
+ <content:encoded>&lt;p&gt;the following changes have been pushed to bugzilla.mozilla.org:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;[&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1238573&quot; target=&quot;_blank&quot;&gt;1238573&lt;/a&gt;] Change label of “New Bug†menu to “New/Clone Bugâ€&lt;/li&gt;
+ &lt;li&gt;[&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1239065&quot; target=&quot;_blank&quot;&gt;1239065&lt;/a&gt;] Project Kickoff Form: Adjustments needed to Mozilla Infosec review portion&lt;/li&gt;
+ &lt;li&gt;[&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1240157&quot; target=&quot;_blank&quot;&gt;1240157&lt;/a&gt;] Fix a typo in bug.rst&lt;/li&gt;
+ &lt;li&gt;[&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1236461&quot; target=&quot;_blank&quot;&gt;1236461&lt;/a&gt;] Mass update mozilla-reps group&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;discuss these changes on &lt;a href=&quot;https://lists.mozilla.org/listinfo/tools-bmo&quot; target=&quot;_blank&quot;&gt;mozilla.tools.bmo&lt;/a&gt;.&lt;/p&gt;&lt;br /&gt; &lt;a href=&quot;http://feeds.wordpress.com/1.0/gocomments/dlawrence.wordpress.com/27/&quot; rel=&quot;nofollow&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; src=&quot;http://feeds.wordpress.com/1.0/comments/dlawrence.wordpress.com/27/&quot; /&gt;&lt;/a&gt; &lt;img alt=&quot;&quot; border=&quot;0&quot; height=&quot;1&quot; src=&quot;https://pixel.wp.com/b.gif?host=dlawrence.wordpress.com&amp;amp;blog=58816&amp;amp;post=27&amp;amp;subd=dlawrence&amp;amp;ref=&amp;amp;feed=1&quot; width=&quot;1&quot; /&gt;</content:encoded>
+ <dc:date>2016-01-19T14:49:59+00:00</dc:date>
+ <dc:creator>dlawrence</dc:creator>
+ </item>
+ <item rdf:about="http://soledadpenades.com/?p=6335">
+ <title>Soledad Penades: Hardware Hack Day @ MozLDN, 1</title>
+ <link>http://soledadpenades.com/2016/01/19/hardware-hack-day-mozldn-1/</link>
+ <content:encoded>&lt;p&gt;Last week we ran an internal “hack day†here at the Mozilla space in London. It was just a bunch of &lt;em&gt;software&lt;/em&gt; engineers looking at various &lt;em&gt;hardware&lt;/em&gt; boards and things and learning about them &lt;img alt=&quot;:-)&quot; class=&quot;wp-smiley&quot; src=&quot;http://soledadpenades.com/wp-includes/images/smilies/simple-smile.png&quot; style=&quot;height: 1em;&quot; /&gt;&lt;/p&gt;
+ &lt;p&gt;Here’s what we did!&lt;/p&gt;
+ &lt;h3&gt;&lt;a href=&quot;http://soledadpenades.com/&quot;&gt;Sole&lt;/a&gt;&lt;/h3&gt;
+ &lt;p&gt;I essentially &lt;a href=&quot;http://soledadpenades.com/2016/01/19/kind-of-bricking-an-arduino-duemilanove-by-exhausting-its-memory/&quot;&gt;kind of bricked my Arduino Duemilanove&lt;/a&gt; trying to get it working with Johnny Five, but it was fine–apparently there’s a way to recover it using another Arduino, and someone offered to help with that in the next &lt;a href=&quot;http://www.meetup.com/NodeBots-of-London/events/227890374/&quot;&gt;NodeBots&lt;/a&gt; London, which I’m going to attend.&lt;/p&gt;
+ &lt;h3&gt;&lt;a href=&quot;http://ardeenelinfierno.com/&quot;&gt;Francisco&lt;/a&gt;&lt;/h3&gt;
+ &lt;p&gt;Thinks he’s having issues with cables. It seems like the boards are not reset automatically by the Arduino IDE nowadays? He found the button in the board actually resets the board when pressed i.e. it’s the RESET button.&lt;/p&gt;
+ &lt;p&gt;On the Raspberry Pi side of things, he was very happy to put all his old-school Linux skills in action configuring network interfaces without GUIs!&lt;/p&gt;
+ &lt;h3&gt;&lt;a href=&quot;http://gu.illau.me/&quot;&gt;Guillaume&lt;/a&gt;&lt;/h3&gt;
+ &lt;p&gt;Played with mDNS advertising and listening to services on Raspberry Pi.&lt;/p&gt;
+ &lt;p&gt;(He was very quiet)&lt;/p&gt;
+ &lt;p&gt;(He also built a very nice LEGO case for the Raspberry Pi, but I do not have a picture, so just imagine it).&lt;/p&gt;
+ &lt;h3&gt;&lt;a href=&quot;http://wilsonpage.co.uk/&quot;&gt;Wilson&lt;/a&gt;&lt;/h3&gt;
+ &lt;blockquote&gt;&lt;p&gt;
+ Wilson: “I got my Raspberry Pi on the Wi-Fiâ€&lt;/p&gt;
+ &lt;p&gt;Francisco: “Sorry?â€&lt;/p&gt;
+ &lt;p&gt;Wilson: “I mean, you got my Raspberry Pi on the network. And now I’m trying to build a web app on the Pi…â€&lt;/p&gt;&lt;/blockquote&gt;
+ &lt;h3&gt;&lt;a href=&quot;http://chrislord.net/&quot;&gt;Chris&lt;/a&gt;&lt;/h3&gt;
+ &lt;p&gt;Exploring the Pebble with Linux. There’s a libpebble, and he managed to connect…&lt;/p&gt;
+ &lt;p&gt;&lt;del datetime=&quot;2016-01-20T11:22:33+00:00&quot;&gt;&lt;em&gt;&lt;small&gt;(sorry, I had to leave early so I do not know what else did Chris do!)&lt;/small&gt;&lt;/em&gt;&lt;/del&gt;&lt;/p&gt;
+ &lt;p&gt;Updated, 20 January: Chris told me he just managed to successfully connect to the Pebble watch using the bluetooth WebAPI. It requires two Gecko patches (one regression patch and one obvious logic error that he hasn’t filed yet). PROGRESS!&lt;/p&gt;
+ &lt;p&gt;~~~&lt;/p&gt;
+ &lt;p&gt;So as you can see we didn’t really get super far in just a day, and I even ended up with unusable hardware. BUT! we all learned something, and next time we know what NOT to do (or at least I DO KNOW what NOT to do!).&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;http://soledadpenades.com/?flattrss_redirect&amp;amp;id=6335&amp;amp;md5=40427d69faa3b9c2d1530732fd78e66d&quot; target=&quot;_blank&quot; title=&quot;Flattr&quot;&gt;&lt;img alt=&quot;flattr this!&quot; src=&quot;http://soledadpenades.com/wp-content/plugins/flattr/img/flattr-badge-large.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-19T13:31:55+00:00</dc:date>
+ <dc:creator>sole</dc:creator>
+ </item>
+ <item rdf:about="http://daniel.haxx.se/blog/?p=8544">
+ <title>Daniel Stenberg: “Subject: Urgent Warningâ€</title>
+ <link>http://daniel.haxx.se/blog/2016/01/19/subject-urgent-warning/</link>
+ <content:encoded>&lt;p&gt;Back in December I got a desperate email from this person. A woman who said her Instagram had been hacked and since she found my contact info in the app she mailed me and asked for help. I of course replied and said that I have nothing to do with her being hacked but I also have nothing to do with Instagram other than that they use software I’ve written.&lt;/p&gt;
+ &lt;p&gt;Today she writes back. Clearly not convinced I told the truth before, and now she strikes back with more “evidence†of my wrongdoings.&lt;/p&gt;
+ &lt;p&gt;&lt;em&gt;Dear Daniel,&lt;/em&gt;&lt;/p&gt;
+ &lt;p&gt;&lt;em&gt;I had emailed you a couple months ago about my “screen dumps†aka screenshots and asked for your help with restoring my Instagram account since it had been hacked, my photos changed, and your name was included in the coding. You claimed to have no involvement whatsoever in developing a third party app for Instagram and could not help me salvage my original Instagram photos, pre-hacked, despite Instagram serving as my Photography portfolio and my career is a Photographer.&lt;/em&gt;&lt;/p&gt;
+ &lt;p&gt;&lt;em&gt;Since you weren’t aware that your name was attached to Instagram related hacking code, I thought you might want to know, in case you weren’t already aware, that your name is also included in Spotify terms and conditions. I came across this information using my Spotify which has also been hacked into and would love your help hacking out of Spotify. Also, I have yet to figure out how to unhack the hackers from my Instagram so if you change your mind and want to restore my Instagram to its original form as well as help me secure my account from future privacy breaches, I’d be extremely grateful. As you know, changing my passwords did nothing to resolve the problem. Please keep in mind that Facebook owns Instagram and these are big companies that you likely don’t want to have a trail of evidence that you are a part of an Instagram and Spotify hacking ring. Also, Spotify is a major partner of Spotify so you are likely familiar with the coding for all of these illegally developed third party apps. I’d be grateful for your help fixing this error immediately.&lt;/em&gt;&lt;/p&gt;
+ &lt;p&gt;&lt;em&gt;Thank you,&lt;/em&gt;&lt;/p&gt;
+ &lt;p&gt;[name redacted]&lt;/p&gt;
+ &lt;p&gt;&lt;em&gt;P.S. Please see attached screen dump for a screen shot of your contact info included in Spotify (or what more likely seems to be a hacked Spotify developed illegally by a third party).&lt;/em&gt;&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;http://daniel.haxx.se/blog/wp-content/uploads/2016/01/IMG_7393.png&quot; rel=&quot;attachment wp-att-8545&quot;&gt;&lt;img alt=&quot;Spotify credits screenshot&quot; class=&quot;aligncenter size-medium wp-image-8545&quot; height=&quot;450&quot; src=&quot;http://daniel.haxx.se/blog/wp-content/uploads/2016/01/IMG_7393-253x450.png&quot; width=&quot;253&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
+ &lt;p&gt;Here’s the Instagram screenshot she sent me in a previous email:&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;http://daniel.haxx.se/blog/wp-content/uploads/2016/01/IMG_2156.jpg&quot; rel=&quot;attachment wp-att-8546&quot;&gt;&lt;img alt=&quot;Instagram credits screenshot&quot; class=&quot;aligncenter size-medium wp-image-8546&quot; height=&quot;450&quot; src=&quot;http://daniel.haxx.se/blog/wp-content/uploads/2016/01/IMG_2156-253x450.jpg&quot; width=&quot;253&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
+ &lt;p&gt;I’ve tried to respond with calm and clear reasonable logic and technical details on why she’s seeing my name there. That clearly failed. What do I try next?&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-19T08:37:32+00:00</dc:date>
+ <dc:creator>Daniel Stenberg</dc:creator>
+ </item>
+ <item rdf:about="http://edunham.net/2016/01/19/how_much_knowledge_do_you_need_to_give_a_conference_talk.html">
+ <title>Emily Dunham: How much knowledge do you need to give a conference talk?</title>
+ <link>http://edunham.net/2016/01/19/how_much_knowledge_do_you_need_to_give_a_conference_talk.html</link>
+ <content:encoded>&lt;h3&gt;How much knowledge do you need to give a conference talk?&lt;/h3&gt;
+ &lt;p&gt;I was recently asked an excellent question when I promoted the &lt;a class=&quot;reference external&quot; href=&quot;http://www.linuxfestnorthwest.org/2016/present&quot;&gt;LFNW CFP&lt;/a&gt; on
+ IRC:&lt;/p&gt;
+ &lt;blockquote&gt;
+ &lt;div&gt;As someone who has never done a talk, but wants to, what kind of knowledge
+ do you need about a subject to give a talk on it?&lt;/div&gt;&lt;/blockquote&gt;
+ &lt;p&gt;If you answer “yes†to any of the following questions, you know enough to
+ propose a talk:&lt;/p&gt;
+ &lt;ul class=&quot;simple&quot;&gt;
+ &lt;li&gt;Do you have a &lt;strong&gt;hobby&lt;/strong&gt; that most tech people aren’t experts on? Talk
+ about applying a lesson or skill from that hobby to tech! For instance, I
+ turned a habit of reading about psychology into my &lt;a class=&quot;reference external&quot; href=&quot;http://talks.edunham.net/scale13x/#1&quot;&gt;Human Hacking&lt;/a&gt; talk.&lt;/li&gt;
+ &lt;li&gt;Have you ever spent a bunch of hours forcing two tools to work with each
+ other, because the documentation wasn’t very helpful and Googling didn’t get
+ you very far, and built something useful? “How to build ___ with ___†makes
+ a catchy talk title, if the &lt;strong&gt;thing you built&lt;/strong&gt; solves a common problem.&lt;/li&gt;
+ &lt;li&gt;Have you ever had a mentor sit down with you and explain a tool or
+ technique, and the new understanding improved the quality of your work or
+ code? Passing along useful &lt;strong&gt;lessons from your mentors&lt;/strong&gt; is a valuable talk,
+ because it allows others to benefit from the knowledge without taking as
+ much of your mentor’s time.&lt;/li&gt;
+ &lt;li&gt;Have you seen a dozen newbies ask the same question over the course of a few
+ months? When your &lt;strong&gt;answer to a common question&lt;/strong&gt; starts to feel like a
+ broken record, it’s time to compose it into a talk then link the newbies to
+ your slides or recording!&lt;/li&gt;
+ &lt;li&gt;Have you taken a really &lt;strong&gt;interesting class&lt;/strong&gt; lately? Can you distill part of it
+ into a 1-hour lesson that would appeal to nerds who don’t have the time or
+ resources to take the class themselves? (thanks &lt;a class=&quot;reference external&quot; href=&quot;http://lucywyman.me/&quot;&gt;lucyw&lt;/a&gt; for adding this to
+ the list!)&lt;/li&gt;
+ &lt;li&gt;Have you built a cool thing that over a dozen other people use? A &lt;strong&gt;tutorial
+ talk&lt;/strong&gt; can not only expand your community, but its recording can augment your
+ documentation and make the project more accessible for those who prefer to
+ learn directly from humans!&lt;/li&gt;
+ &lt;li&gt;Did you benefit from a really great introductory talk when you were learning
+ a tool? Consider doing your own tutorial! Any conference with beginners in
+ their target audience needs at least one Git lesson, an IRC talk, and some
+ discussions of how to use basic Unix utilities. These &lt;strong&gt;introductory talks&lt;/strong&gt;
+ are actually better when given by someone who learned the technology
+ relatively recently, because newer users remember what it’s like not to know
+ how to use it. Just remember to have a more expert user look over your slides
+ before you present, in case you made an incorrect assumption about the tool’s
+ more advanced functionality.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;I personally try to propose talks I want to hear, because the dealine of a
+ CFP or conference is great motivation to prioritize a cool project over
+ ordinary chores.&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-19T08:00:00+00:00</dc:date>
+ </item>
+ <item rdf:about="https://quality.mozilla.org/?p=49441">
+ <title>QMO: Aurora 45.0 Testday Results</title>
+ <link>https://quality.mozilla.org/2016/01/aurora-45-0-testday-results/</link>
+ <content:encoded>&lt;p&gt;Howdy mozillians!&lt;/p&gt;
+ &lt;p&gt;Last week – on &lt;em&gt;Friday, January 15th&lt;/em&gt; – we held &lt;a href=&quot;https://quality.mozilla.org/2016/01/firefox-45-0-aurora-testday-january-15th/&quot;&gt;Aurora 45.0 Testday&lt;/a&gt;; and, of course, it was another outstanding event!&lt;/p&gt;
+ &lt;p&gt;&lt;strong&gt;Thank you&lt;/strong&gt; all – &lt;span class=&quot;author-a-oz90z4z89zz89za7qfz70zda5z87zxz85z i&quot;&gt;&lt;i&gt;Mahmoudi Dris, Iryna Thompson, Chandrakant Dhutadmal, Preethi Dhinesh, Moin Shaikh, Ilse Macías, Hossain Al Ikram, Rezaul Huque Nayeem, Tahsan Chowdhury Akash, Kazi Nuzhat Tasnem, Fahmida Noor, Tazin Ahmed, Md. Ehsanul Hassan, Mohammad Maruf Islam, Kazi Sakib Ahmad, Khalid Syfullah Zaman, Asiful Kabir, Tabassum Mehnaz, Hasibul Hasan, Saddam Hossain, Mohammad Kamran Hossain, Amlan Biswas, Fazle Rabbi, Mohammed Jawad Ibne Ishaque, Asif Mahmud Shuvo, Nazir Ahmed Sabbir, Md. Raihan Ali, Md. Almas Hossain, Sadik Khan, Md. Faysal Alam Riyad, Faisal Mahmud, Md. Oliullah Sizan, Asif Mahmud Rony, Forhad Hossain &lt;/i&gt;and&lt;i&gt; Tanvir Rahman &lt;/i&gt;&lt;/span&gt;– for the participation!&lt;/p&gt;
+ &lt;p&gt;A big &lt;strong&gt;thank you&lt;/strong&gt; to all our active moderators too!&lt;/p&gt;
+ &lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;font-family: 'Open Sans', sans-serif;&quot;&gt;&lt;span style=&quot;font-size: medium;&quot;&gt;&lt;u&gt;Results:&lt;/u&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;font-family: 'Open Sans', sans-serif;&quot;&gt;&lt;span style=&quot;font-size: medium;&quot;&gt;&lt;strong&gt;15&lt;/strong&gt; issues were verified: &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;font-family: 'Open Sans', sans-serif;&quot;&gt;&lt;span style=&quot;font-size: medium;&quot;&gt; &lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1235821&quot;&gt;1235821&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1228518&quot;&gt;1228518&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1165637&quot;&gt;1165637&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1232647&quot;&gt;1232647&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1235379&quot;&gt;1235379&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=842356&quot;&gt;842356&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1222971&quot;&gt;1222971&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=915962&quot;&gt;915962&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1180761&quot;&gt;1180761&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1218455&quot;&gt;1218455&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1222747&quot;&gt;1222747&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1210752&quot;&gt;1210752&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1198450&quot;&gt;1198450&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1222820&quot;&gt;1222820&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1225514&quot;&gt;1225514&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;strong&gt;1&lt;/strong&gt; bug was triaged: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1230789&quot;&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;1230789&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;some failures were mentioned for &lt;i&gt;Search Refactoring &lt;/i&gt;feature in the etherpads (&lt;a href=&quot;https://public.etherpad-mozilla.org/p/testday-20160115&quot;&gt;link 1&lt;/a&gt; and &lt;a href=&quot;https://public.etherpad-mozilla.org/p/bangladesh.testday-15012016&quot;&gt;link 2&lt;/a&gt;); please feel free to add the requested details in the etherpads or, even better, join us on &lt;a href=&quot;http://widget01.mibbit.com/?server=irc.mozilla.org&amp;amp;channel=%23qa&quot; target=&quot;_blank&quot;&gt;#qa IRC channel&lt;/a&gt; and let’s figure them out&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;I &lt;strong&gt;strongly&lt;/strong&gt; advise everyone of you to reach out to us, the moderators, via &lt;a href=&quot;http://widget01.mibbit.com/?server=irc.mozilla.org&amp;amp;channel=%23qa&quot;&gt;#qa&lt;/a&gt; during the events when you encountered any kind of failures. Keep up the great work! \o/&lt;/p&gt;
+ &lt;p&gt;And keep an eye on QMO for upcoming events! &lt;img alt=&quot;😉&quot; class=&quot;wp-smiley&quot; src=&quot;https://s.w.org/images/core/emoji/72x72/1f609.png&quot; style=&quot;height: 1em;&quot; /&gt;&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-19T07:51:57+00:00</dc:date>
+ <dc:creator>Alexandra Lucinet</dc:creator>
+ </item>
+ <item rdf:about="http://blog.monotonous.org/?p=678">
+ <title>Eitan Isaacson: It’s MLK Day and It’s Not Too Late to Do Something About It</title>
+ <link>http://blog.monotonous.org/2016/01/18/its-mlk-day-and-its-not-too-late-to-do-something-about-it/</link>
+ <content:encoded>&lt;p&gt;For the last three years I have had the opportunity to send out a reminder to Mozilla staff that Martin Luther King Jr. Day is coming up, and that U.S. employees get the day off. It has turned into my MLK Day eve ritual. I read his letters, listen to speeches, and then I compose a belabored paragraph about Dr. King with some choice quotes.&lt;/p&gt;
+ &lt;p&gt;If you didn’t get a chance to celebrate Dr. King’s legacy and the movements he was a part of, you still have a chance:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Watch &lt;a href=&quot;http://www.imdb.com/title/tt1020072/&quot; target=&quot;_blank&quot;&gt;Selma.&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Watch &lt;a href=&quot;http://www.imdb.com/title/tt1592527/&quot; target=&quot;_blank&quot;&gt;The Black Power Mixtape&lt;/a&gt; (it’s on Netflix).&lt;/li&gt;
+ &lt;li&gt;Read &lt;a href=&quot;http://www.africa.upenn.edu/Articles_Gen/Letter_Birmingham.html&quot; target=&quot;_blank&quot;&gt;A Letter from a Birmingham Jail&lt;/a&gt; (it’s really really good).&lt;/li&gt;
+ &lt;li&gt;Listen to his speech &lt;a href=&quot;https://www.youtube.com/watch?v=3Qf6x9_MLD0&quot; target=&quot;_blank&quot;&gt;Beyond Vietnam&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;Listen to his last speech &lt;a href=&quot;https://www.youtube.com/watch?v=IDl84vusXos&quot; target=&quot;_blank&quot;&gt;I Have Been To The Mountaintop&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;&lt;br /&gt; &lt;a href=&quot;http://feeds.wordpress.com/1.0/gocomments/blogdotmonotonousdotorg.wordpress.com/678/&quot; rel=&quot;nofollow&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; src=&quot;http://feeds.wordpress.com/1.0/comments/blogdotmonotonousdotorg.wordpress.com/678/&quot; /&gt;&lt;/a&gt; &lt;img alt=&quot;&quot; border=&quot;0&quot; height=&quot;1&quot; src=&quot;http://pixel.wp.com/b.gif?host=blog.monotonous.org&amp;amp;blog=34885741&amp;amp;post=678&amp;amp;subd=blogdotmonotonousdotorg&amp;amp;ref=&amp;amp;feed=1&quot; width=&quot;1&quot; /&gt;</content:encoded>
+ <dc:date>2016-01-18T23:35:19+00:00</dc:date>
+ <dc:creator>Eitan</dc:creator>
+ </item>
+ <item rdf:about="http://www.ncameron.org/blog/rss/0e4d587c-380c-40ce-954a-7206f69bc1dd">
+ <title>Nick Cameron: Libmacro</title>
+ <link>http://www.ncameron.org/blog/libmacro/</link>
+ <content:encoded>&lt;p&gt;As I outlined in an &lt;a href=&quot;http://ncameron.org/blog/procedural-macros-framework/&quot;&gt;earlier post&lt;/a&gt;, libmacro is a new crate designed to be used by procedural macro authors. It provides the basic API for procedural macros to interact with the compiler. I expect higher level functionality to be provided by library crates. In this post I'll go into a bit more detail about the API I think should be exposed here.&lt;/p&gt;
+
+ &lt;p&gt;This is a lot of stuff. I've probably missed something. If you use syntax extensions today and do something with libsyntax that would not be possible with libmacro, please let me know!&lt;/p&gt;
+
+ &lt;p&gt;I previously introduced &lt;code&gt;MacroContext&lt;/code&gt; as one of the gateways to libmacro. All procedural macros will have access to a &lt;code&gt;&amp;amp;mut MacroContext&lt;/code&gt;.&lt;/p&gt;
+
+ &lt;h3&gt;Tokens&lt;/h3&gt;
+
+ &lt;p&gt;I described the &lt;code&gt;tokens&lt;/code&gt; module in the last post, I won't repeat that here.&lt;/p&gt;
+
+ &lt;p&gt;There are a few more things I thought of. I mentioned a &lt;code&gt;TokenStream&lt;/code&gt; which is a sequence of tokens. We should also have &lt;code&gt;TokenSlice&lt;/code&gt; which is a borrowed slice of tokens (the slice to &lt;code&gt;TokenStream&lt;/code&gt;'s &lt;code&gt;Vec&lt;/code&gt;). These should implement the standard methods for sequences, in particular they support iteration, so can be &lt;code&gt;map&lt;/code&gt;ed, etc.&lt;/p&gt;
+
+ &lt;p&gt;In the earlier blog post, I talked about a token kind called &lt;code&gt;Delimited&lt;/code&gt; which contains a delimited sequence of tokens. I would like to rename that to &lt;code&gt;Sequence&lt;/code&gt; and add a &lt;code&gt;None&lt;/code&gt; variant to the &lt;code&gt;Delimiter&lt;/code&gt; enum. The &lt;code&gt;None&lt;/code&gt; option is so that we can have blocks of tokens without using delimiters. It will be used for noting unsafety and other properties of tokens. Furthermore, it is useful for macro expansion (replacing the interpolated AST tokens currently present). Although &lt;code&gt;None&lt;/code&gt; blocks do not affect scoping, they do affect precedence and parsing.&lt;/p&gt;
+
+ &lt;p&gt;We should provide API for creating tokens. By default these have no hygiene information and come with a span which has no place in the source code, but shows the source of the token to be the procedural macro itself (see below for how this interacts with expansion of the current macro). I expect a &lt;code&gt;make_&lt;/code&gt; function for each kind of token. We should also have API for creating macros in a given scope (which do the same thing but with provided hygiene information). This could be considered an over-rich API, since the hygiene information could be set after construction. However, since hygiene is fiddly and annoying to get right, we should make it as easy as possible to work with.&lt;/p&gt;
+
+ &lt;p&gt;There should also be a function for creating a token which is just a fresh name. This is useful for creating new identifiers. Although this can be done by interning a string and then creating a token around it, it is used frequently enough to deserve a helper function.&lt;/p&gt;
+
+ &lt;h3&gt;Emitting errors and warnings&lt;/h3&gt;
+
+ &lt;p&gt;Procedural macros should report errors, warnings, etc. via the &lt;code&gt;MacroContext&lt;/code&gt;. They should avoid panicking as much as possible since this will crash the compiler (once &lt;code&gt;catch_panic&lt;/code&gt; is available, we should use it to catch such panics and exit gracefully, however, they will certainly still meaning aborting compilation).&lt;/p&gt;
+
+ &lt;p&gt;Libmacro will 're-export' &lt;code&gt;DiagnosticBuilder&lt;/code&gt; from &lt;a href=&quot;https://dxr.mozilla.org/rust/source/src/libsyntax/errors/mod.rs&quot;&gt;syntax::errors&lt;/a&gt;. I don't actually expect this to be a literal re-export. We will use libmacro's version of &lt;code&gt;Span&lt;/code&gt;, for example.&lt;/p&gt;
+
+ &lt;pre&gt;&lt;code&gt;impl MacroContext {
+ pub fn struct_error(&amp;amp;self, &amp;amp;str) -&amp;gt; DiagnosticBuilder;
+ pub fn error(&amp;amp;self, Option&amp;lt;Span&amp;gt;, &amp;amp;str);
+ }
+
+ pub mod errors {
+ pub struct DiagnosticBuilder { ... }
+ impl DiagnosticBuilder { ... }
+ pub enum ErrorLevel { ... }
+ }
+ &lt;/code&gt;&lt;/pre&gt;
+
+ &lt;p&gt;There should be a macro &lt;code&gt;try_emit!&lt;/code&gt;, which reduces a &lt;code&gt;Result&amp;lt;T, ErrStruct&amp;gt;&lt;/code&gt; to a T or calls &lt;code&gt;emit()&lt;/code&gt; and then calls &lt;code&gt;unreachable!()&lt;/code&gt; (if the error is not fatal, then it should be upgraded to a fatal error).&lt;/p&gt;
+
+ &lt;h3&gt;Tokenising and quasi-quoting&lt;/h3&gt;
+
+ &lt;p&gt;The simplest function here is &lt;code&gt;tokenize&lt;/code&gt; which takes a string (&lt;code&gt;&amp;amp;str&lt;/code&gt;) and returns a &lt;code&gt;Result&amp;lt;TokenStream, ErrStruct&amp;gt;&lt;/code&gt;. The string is treated like source text. The success option is the tokenised version of the string. I expect this function must take a &lt;code&gt;MacroContext&lt;/code&gt; argument.&lt;/p&gt;
+
+ &lt;p&gt;We will offer a quasi-quoting macro. This will return a &lt;code&gt;TokenStream&lt;/code&gt; (in contrast to today's quasi-quoting which returns AST nodes), to be precise a &lt;code&gt;Result&amp;lt;TokenStream, ErrStruct&amp;gt;&lt;/code&gt;. The string which is quoted may include metavariables (&lt;code&gt;$x&lt;/code&gt;), and these are filled in with variables from the environment. The type of the variables should be either a &lt;code&gt;TokenStream&lt;/code&gt;, a &lt;code&gt;TokenTree&lt;/code&gt;, or a &lt;code&gt;Result&amp;lt;TokenStream, ErrStruct&amp;gt;&lt;/code&gt; (in this last case, if the variable is an error, then it is just returned by the macro). For example,&lt;/p&gt;
+
+ &lt;pre&gt;&lt;code&gt;fn foo(cx: &amp;amp;mut MacroContext, tokens: TokenStream) -&amp;gt; TokenStream {
+ quote!(cx, fn foo() { $tokens }).unwrap()
+ }
+ &lt;/code&gt;&lt;/pre&gt;
+
+ &lt;p&gt;The &lt;code&gt;quote!&lt;/code&gt; macro can also handle multiple tokens when the variable corresponding with the metavariable has type &lt;code&gt;[TokenStream]&lt;/code&gt; (or is dereferencable to it). In this case, the same syntax as used in macros-by-example can be used. For example, if &lt;code&gt;x: Vec&amp;lt;TokenStream&amp;gt;&lt;/code&gt; then &lt;code&gt;quote!(cx, ($x),*)&lt;/code&gt; will produce a &lt;code&gt;TokenStream&lt;/code&gt; of a comma-separated list of tokens from the elements of &lt;code&gt;x&lt;/code&gt;.&lt;/p&gt;
+
+ &lt;p&gt;Since the &lt;code&gt;tokenize&lt;/code&gt; function is a degenerate case of quasi-quoting, an alternative would be to always use &lt;code&gt;quote!&lt;/code&gt; and remove &lt;code&gt;tokenize&lt;/code&gt;. I believe there is utility in the simple function, and it must be used internally in any case.&lt;/p&gt;
+
+ &lt;p&gt;These functions and macros should create tokens with spans and hygiene information set as described above for making new tokens. We might also offer versions which takes a scope and uses that as the context for tokenising.&lt;/p&gt;
+
+ &lt;h3&gt;Parsing helper functions&lt;/h3&gt;
+
+ &lt;p&gt;There are some common patterns for tokens to follow in macros. In particular those used as arguments for attribute-like macros. We will offer some functions which attempt to parse tokens into these patterns. I expect there will be more of these in time; to start with:&lt;/p&gt;
+
+ &lt;pre&gt;&lt;code&gt;pub mod parsing {
+ // Expects `(foo = &quot;bar&quot;),*`
+ pub fn parse_keyed_values(&amp;amp;TokenSlice, &amp;amp;mut MacroContext) -&amp;gt; Result&amp;lt;Vec&amp;lt;(InternedString, String)&amp;gt;, ErrStruct&amp;gt;;
+ // Expects `&quot;bar&quot;`
+ pub fn parse_string(&amp;amp;TokenSlice, &amp;amp;mut MacroContext) -&amp;gt; Result&amp;lt;String, ErrStruct&amp;gt;;
+ }
+ &lt;/code&gt;&lt;/pre&gt;
+
+ &lt;p&gt;To be honest, given the token design in the last post, I think &lt;code&gt;parse_string&lt;/code&gt; is unnecessary, but I wanted to give more than one example of this kind of function. If &lt;code&gt;parse_keyed_values&lt;/code&gt; is the only one we end up with, then that is fine.&lt;/p&gt;
+
+ &lt;h3&gt;Pattern matching&lt;/h3&gt;
+
+ &lt;p&gt;The goal with the pattern matching API is to allow procedural macros to operate on tokens in the same way as macros-by-example. The pattern language is thus the same as that for macros-by-example.&lt;/p&gt;
+
+ &lt;p&gt;There is a single macro, which I propose calling &lt;code&gt;matches&lt;/code&gt;. Its first argument is the name of a &lt;code&gt;MacroContext&lt;/code&gt;. Its second argument is the input, which must be a &lt;code&gt;TokenSlice&lt;/code&gt; (or dereferencable to one). The third argument is a pattern definition. The macro produces a &lt;code&gt;Result&amp;lt;T, ErrStruct&amp;gt;&lt;/code&gt; where &lt;code&gt;T&lt;/code&gt; is the type produced by the pattern arms. If the pattern has multiple arms, then each arm must have the same type. An error is produced if none of the arms in the pattern are matched.&lt;/p&gt;
+
+ &lt;p&gt;The pattern language follows the language for defining macros-by-example (but is slightly stricter). There are two forms, a single pattern form and a multiple pattern form. If the first character is a &lt;code&gt;{&lt;/code&gt; then the pattern is treated as a multiple pattern form, if it starts with &lt;code&gt;(&lt;/code&gt; then as a single pattern form, otherwise an error (causes a panic with a &lt;code&gt;Bug&lt;/code&gt; error, as opposed to returning an &lt;code&gt;Err&lt;/code&gt;).&lt;/p&gt;
+
+ &lt;p&gt;The single pattern form is &lt;code&gt;(pattern) =&amp;gt; { code }&lt;/code&gt;. The multiple pattern form is &lt;code&gt;{(pattern) =&amp;gt; { code } (pattern) =&amp;gt; { code } ... (pattern) =&amp;gt; { code }}&lt;/code&gt;. &lt;code&gt;code&lt;/code&gt; is any old Rust code which is executed when the corresponding pattern is matched. The pattern follows from macros-by-example - it is a series of characters treated as literals, meta-variables indicated with &lt;code&gt;$&lt;/code&gt;, and the syntax for matching multiple variables. Any meta-variables are available as variables in the righthand side, e.g., &lt;code&gt;$x&lt;/code&gt; becomes available as &lt;code&gt;x&lt;/code&gt;. These variables have type &lt;code&gt;TokenStream&lt;/code&gt; if they appear singly or &lt;code&gt;Vec&amp;lt;TokenStream&amp;gt;&lt;/code&gt; if they appear multiply (or &lt;code&gt;Vec&amp;lt;Vec&amp;lt;TokenStream&amp;gt;&amp;gt;&lt;/code&gt; and so forth).&lt;/p&gt;
+
+ &lt;p&gt;Examples:&lt;/p&gt;
+
+ &lt;pre&gt;&lt;code&gt;matches!(cx, input, (foo($x:expr) bar) =&amp;gt; {quote(cx, foo_bar($x).unwrap()}).unwrap()
+
+ matches!(cx, input, {
+ () =&amp;gt; {
+ cx.err(&quot;No input?&quot;);
+ }
+ (foo($($x:ident),+ bar) =&amp;gt; {
+ println!(&quot;found {} idents&quot;, x.len());
+ quote!(($x);*).unwrap()
+ }
+ }
+ })
+ &lt;/code&gt;&lt;/pre&gt;
+
+ &lt;p&gt;Note that since we match AST items here, our backwards compatibility story is a bit complicated (though hopefully not much more so than with current macros).&lt;/p&gt;
+
+ &lt;h3&gt;Hygiene&lt;/h3&gt;
+
+ &lt;p&gt;The intention of the design is that the actual hygiene algorithm applied is irrelevant. Procedural macros should be able to use the same API if the hygiene algorithm changes (of course the result of applying the API might change). To this end, all hygiene objects are opaque and cannot be directly manipulated by macros.&lt;/p&gt;
+
+ &lt;p&gt;I propose one module (&lt;code&gt;hygiene&lt;/code&gt;) and two types: &lt;code&gt;Context&lt;/code&gt; and &lt;code&gt;Scope&lt;/code&gt;.&lt;/p&gt;
+
+ &lt;p&gt;A &lt;code&gt;Context&lt;/code&gt; is attached to each token and contains all hygiene information about that token. If two tokens have the same &lt;code&gt;Context&lt;/code&gt;, then they may be compared syntactically. The reverse is not true - two tokens can have different &lt;code&gt;Context&lt;/code&gt;s and still be equal. &lt;code&gt;Context&lt;/code&gt;s can only be created by applying the hygiene algorithm and cannot be manipulated, only moved and stored.&lt;/p&gt;
+
+ &lt;p&gt;&lt;code&gt;MacroContext&lt;/code&gt; has a method &lt;code&gt;fresh_hygiene_context&lt;/code&gt; for creating a new, fresh &lt;code&gt;Context&lt;/code&gt; (i.e., a &lt;code&gt;Context&lt;/code&gt; not shared with any other tokens).&lt;/p&gt;
+
+ &lt;p&gt;&lt;code&gt;MacroContext&lt;/code&gt; has a method &lt;code&gt;expansion_hygiene_context&lt;/code&gt; for getting the &lt;code&gt;Context&lt;/code&gt; where the macro is defined. This is equivalent to &lt;code&gt;.expansion_scope().direct_context()&lt;/code&gt;, but might be more efficient (and I expect it to be used a lot).&lt;/p&gt;
+
+ &lt;p&gt;A &lt;code&gt;Scope&lt;/code&gt; provides information about a position within an AST at a certain point during macro expansion. For example,&lt;/p&gt;
+
+ &lt;pre&gt;&lt;code&gt;fn foo() {
+ a
+ {
+ b
+ c
+ }
+ }
+ &lt;/code&gt;&lt;/pre&gt;
+
+ &lt;p&gt;&lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; will have different &lt;code&gt;Scope&lt;/code&gt;s. &lt;code&gt;b&lt;/code&gt; and &lt;code&gt;c&lt;/code&gt; will have the same &lt;code&gt;Scope&lt;/code&gt;s, even if &lt;code&gt;b&lt;/code&gt; was written in this position and &lt;code&gt;c&lt;/code&gt; is due to macro expansion. However, a &lt;code&gt;Scope&lt;/code&gt; may contain more information than just the syntactic scopes, for example, it may contain information about pending scopes yet to be applied by the hygiene algorithm (i.e., information about &lt;code&gt;let&lt;/code&gt; expressions which are in scope).&lt;/p&gt;
+
+ &lt;p&gt;Note that a &lt;code&gt;Scope&lt;/code&gt; means a scope in the macro hygiene sense, not the commonly used sense of a scope declared with &lt;code&gt;{}&lt;/code&gt;. In particular, each &lt;code&gt;let&lt;/code&gt; statement starts a new scope and the items and statements in a function body are in different scopes.&lt;/p&gt;
+
+ &lt;p&gt;The functions &lt;code&gt;lookup_item_scope&lt;/code&gt; and &lt;code&gt;lookup_statement_scope&lt;/code&gt; take a &lt;code&gt;MacroContext&lt;/code&gt; and a path, represented as a &lt;code&gt;TokenSlice&lt;/code&gt;, and return the &lt;code&gt;Scope&lt;/code&gt; which that item defines or an error if the path does not refer to an item, or the item does not define a scope of the right kind.&lt;/p&gt;
+
+ &lt;p&gt;The function &lt;code&gt;lookup_scope_for&lt;/code&gt; is similar, but returns the &lt;code&gt;Scope&lt;/code&gt; in which an item is declared.&lt;/p&gt;
+
+ &lt;p&gt;&lt;code&gt;MacroContext&lt;/code&gt; has a method &lt;code&gt;expansion_scope&lt;/code&gt; for getting the scope in which the current macro is being expanded.&lt;/p&gt;
+
+ &lt;p&gt;&lt;code&gt;Scope&lt;/code&gt; has a method &lt;code&gt;direct_context&lt;/code&gt; which returns a &lt;code&gt;Context&lt;/code&gt; for items declared directly (c.f., via macro expansion) in that &lt;code&gt;Scope&lt;/code&gt;.&lt;/p&gt;
+
+ &lt;p&gt;&lt;code&gt;Scope&lt;/code&gt; has a method &lt;code&gt;nested&lt;/code&gt; which creates a fresh &lt;code&gt;Scope&lt;/code&gt; nested within the receiver scope.&lt;/p&gt;
+
+ &lt;p&gt;&lt;code&gt;Scope&lt;/code&gt; has a static method &lt;code&gt;empty&lt;/code&gt; for creating an empty scope, that is one with no scope information at all (note that this is different from a top-level scope).&lt;/p&gt;
+
+ &lt;p&gt;I expect the exact API around &lt;code&gt;Scope&lt;/code&gt;s and &lt;code&gt;Context&lt;/code&gt;s will need some work. &lt;code&gt;Scope&lt;/code&gt; seems halfway between an intuitive, algorithm-neutral abstraction, and the scopes from the sets of scopes hygiene algorithm. I would prefer a &lt;code&gt;Scope&lt;/code&gt; should be more abstract, on the other hand, macro authors may want fine-grained control over hygiene application.&lt;/p&gt;
+
+ &lt;h4&gt;Manipulating hygiene information on tokens,&lt;/h4&gt;
+
+ &lt;pre&gt;&lt;code&gt;pub mod hygiene {
+ pub fn add(cx: &amp;amp;mut MacroContext, t: &amp;amp;Token, scope: &amp;amp;Scope) -&amp;gt; Token;
+ // Maybe unnecessary if we have direct access to Tokens.
+ pub fn set(t: &amp;amp;Token, cx: &amp;amp;Context) -&amp;gt; Token;
+ // Maybe unnecessary - can use set with cx.expansion_hygiene_context().
+ // Also, bad name.
+ pub fn current(cx: &amp;amp;MacroContext, t: &amp;amp;Token) -&amp;gt; Token;
+ }
+ &lt;/code&gt;&lt;/pre&gt;
+
+ &lt;p&gt;&lt;code&gt;add&lt;/code&gt; adds &lt;code&gt;scope&lt;/code&gt; to any context already on &lt;code&gt;t&lt;/code&gt; (&lt;code&gt;Context&lt;/code&gt; should have a similar method). Note that the implementation is a bit complex - the nature of the &lt;code&gt;Scope&lt;/code&gt; might mean we replace the old context completely, or add to it.&lt;/p&gt;
+
+ &lt;h4&gt;Applying hygiene when expanding the current macro&lt;/h4&gt;
+
+ &lt;p&gt;By default, the current macro will be expanded in the standard way, having hygiene applied as expected. Mechanically, hygiene information is added to tokens when the macro is expanded. Assuming the sets of scopes algorithm, scopes (for example, for the macro's definition, and for the introduction) are added to any scopes already present on the token. A token with no hygiene information will thus behave like a token in a macro-by-example macro. Hygiene due to nested scopes created by the macro do not need to be taken into account by the macro author, this is handled at expansion time.&lt;/p&gt;
+
+ &lt;p&gt;Procedural macro authors may want to customise hygiene application (it is common in Racket), for example, to introduce items that can be referred to by code in the call-site scope.&lt;/p&gt;
+
+ &lt;p&gt;We must provide an option to expand the current macro without applying hygiene; the macro author must then handle hygiene. For this to work, the macro must be able to access information about the scope in which it is applied (see &lt;code&gt;MacroContext::expansion_scope&lt;/code&gt;, above) and to supply a &lt;code&gt;Scope&lt;/code&gt; indicating scopes that should be added to tokens following the macro expansion.&lt;/p&gt;
+
+ &lt;pre&gt;&lt;code&gt;pub mod hygiene {
+ pub enum ExpansionMode {
+ Automatic,
+ Manual(Scope),
+ }
+ }
+
+ impl MacroContext {
+ pub fn set_hygienic_expansion(hygiene::ExpansionMode);
+ }
+ &lt;/code&gt;&lt;/pre&gt;
+
+ &lt;p&gt;We may wish to offer other modes for expansion which allow for tweaking hygiene application without requiring full manual application. One possible mode is where the author provides a &lt;code&gt;Scope&lt;/code&gt; for the macro definition (rather than using the scope where the macro is actually defined), but hygiene is otherwise applied automatically. We might wish to give the author the option of applying scopes due to the macro definition, but not the introduction scopes.&lt;/p&gt;
+
+ &lt;p&gt;On a related note, might we want to affect how spans are applied when the current macro is expanded? I can't think of a use case right now, but it seems like something that might be wanted.&lt;/p&gt;
+
+ &lt;p&gt;Blocks of tokens (that is a &lt;code&gt;Sequence&lt;/code&gt; token) may be marked (not sure how, exactly, perhaps using a distinguished context) such that it is expanded without any hygiene being applied or spans changed. There should be a function for creating such a &lt;code&gt;Sequence&lt;/code&gt; from a &lt;code&gt;TokenSlice&lt;/code&gt; in the &lt;code&gt;tokens&lt;/code&gt; module. The primary motivation for this is to handle the tokens representing the body on which an annotation-like macro is present. For a 'decorator' macro, these tokens will be untouched (passed through by the macro), and since they are not touched by the macro, they should appear untouched by it (in terms of hygiene and spans).&lt;/p&gt;
+
+ &lt;h3&gt;Applying macros&lt;/h3&gt;
+
+ &lt;p&gt;We provide functionality to expand a provided macro or to lookup and expand a macro.&lt;/p&gt;
+
+ &lt;pre&gt;&lt;code&gt;pub mod apply {
+ pub fn expand_macro(cx: &amp;amp;mut MacroContext,
+ expansion_scope: Scope,
+ macro: &amp;amp;TokenSlice,
+ macro_scope: Scope,
+ input: &amp;amp;TokenSlice)
+ -&amp;gt; Result&amp;lt;(TokenStream, Scope), ErrStruct&amp;gt;;
+ pub fn lookup_and_expand_macro(cx: &amp;amp;mut MacroContext,
+ expansion_scope: Scope,
+ macro: &amp;amp;TokenSlice,
+ input: &amp;amp;TokenSlice)
+ -&amp;gt; Result&amp;lt;(TokenStream, Scope), ErrStruct&amp;gt;;
+ }
+ &lt;/code&gt;&lt;/pre&gt;
+
+ &lt;p&gt;These functions apply macro hygiene in the usual way, with &lt;code&gt;expansion_scope&lt;/code&gt; dictating the scope into which the macro is expanded. Other spans and hygiene information is taken from the tokens. &lt;code&gt;expand_macro&lt;/code&gt; takes pending scopes from &lt;code&gt;macro_scope&lt;/code&gt;, &lt;code&gt;lookup_and_expand_macro&lt;/code&gt; uses the proper pending scopes. In order to apply the hygiene algorithm, the result of the macro must be parsable. The returned scope will contain pending scopes that can be applied by the macro to subsequent tokens.&lt;/p&gt;
+
+ &lt;p&gt;We could provide versions that don't take an &lt;code&gt;expansion_scope&lt;/code&gt; and use &lt;code&gt;cx.expansion_scope()&lt;/code&gt;. Probably unnecessary.&lt;/p&gt;
+
+ &lt;pre&gt;&lt;code&gt;pub mod apply {
+ pub fn expand_macro_unhygienic(cx: &amp;amp;mut MacroContext,
+ macro: &amp;amp;TokenSlice,
+ input: &amp;amp;TokenSlice)
+ -&amp;gt; Result&amp;lt;TokenStream, ErrStruct&amp;gt;;
+ pub fn lookup_and_expand_macro_unhygienic(cx: &amp;amp;mut MacroContext,
+ macro: &amp;amp;TokenSlice,
+ input: &amp;amp;TokenSlice)
+ -&amp;gt; Result&amp;lt;TokenStream, ErrStruct&amp;gt;;
+ }
+ &lt;/code&gt;&lt;/pre&gt;
+
+ &lt;p&gt;The &lt;code&gt;_unhygienic&lt;/code&gt; variants expand a macro as in the first functions, but do not apply the hygiene algorithm or change any hygiene information. Any hygiene information on tokens is preserved. I'm not sure if &lt;code&gt;_unhygienic&lt;/code&gt; are the right names - using these is not necessarily unhygienic, just that we are automatically applying the hygiene algorithm.&lt;/p&gt;
+
+ &lt;p&gt;Note that all these functions are doing an eager expansion of macros, or in Scheme terms they are &lt;code&gt;local-expand&lt;/code&gt; functions. &lt;/p&gt;
+
+ &lt;h3&gt;Looking up items&lt;/h3&gt;
+
+ &lt;p&gt;The function &lt;code&gt;lookup_item&lt;/code&gt; takes a &lt;code&gt;MacroContext&lt;/code&gt; and a path represented as a &lt;code&gt;TokenSlice&lt;/code&gt; and returns a &lt;code&gt;TokenStream&lt;/code&gt; for the item referred to by the path, or an error if name resolution failed. I'm not sure where this function should live.&lt;/p&gt;
+
+ &lt;h3&gt;Interned strings&lt;/h3&gt;
+
+ &lt;pre&gt;&lt;code&gt;pub mod strings {
+ pub struct InternedString;
+
+ impl InternedString {
+ pub fn get(&amp;amp;self) -&amp;gt; String;
+ }
+
+ pub fn intern(cx: &amp;amp;mut MacroContext, s: &amp;amp;str) -&amp;gt; Result&amp;lt;InternedString, ErrStruct&amp;gt;;
+ pub fn find(cx: &amp;amp;mut MacroContext, s: &amp;amp;str) -&amp;gt; Result&amp;lt;InternedString, ErrStruct&amp;gt;;
+ pub fn find_or_intern(cx: &amp;amp;mut MacroContext, s: &amp;amp;str) -&amp;gt; Result&amp;lt;InternedString, ErrStruct&amp;gt;;
+ }
+ &lt;/code&gt;&lt;/pre&gt;
+
+ &lt;p&gt;&lt;code&gt;intern&lt;/code&gt; interns a string and returns a fresh &lt;code&gt;InternedString&lt;/code&gt;. &lt;code&gt;find&lt;/code&gt; tries to find &lt;em&gt;an&lt;/em&gt; existing &lt;code&gt;InternedString&lt;/code&gt;.&lt;/p&gt;
+
+ &lt;h3&gt;Spans&lt;/h3&gt;
+
+ &lt;p&gt;A span gives information about where in the source code a token is defined. It also gives information about where the token came from (how it was generated, if it was generated code).&lt;/p&gt;
+
+ &lt;p&gt;There should be a &lt;code&gt;spans&lt;/code&gt; module in libmacro, which will include a &lt;code&gt;Span&lt;/code&gt; type which can be easily inter-converted with the &lt;code&gt;Span&lt;/code&gt; defined in libsyntax. Libsyntax spans currently include information about stability, this will not be present in libmacro spans.&lt;/p&gt;
+
+ &lt;p&gt;If the programmer does nothing special with spans, then they will be 'correct' by default. There are two important cases: tokens passed to the macro and tokens made fresh by the macro. The former will have the source span indicating where they were written and will include their history. The latter will have no source span and indicate they were created by the current macro. All tokens will have the history relating to expansion of the current macro added when the macro is expanded. At macro expansion, tokens with no source span will be given the macro use-site as their source.&lt;/p&gt;
+
+ &lt;p&gt;&lt;code&gt;Span&lt;/code&gt;s can be freely copied between tokens.&lt;/p&gt;
+
+ &lt;p&gt;It will probably useful to make it easy to manipulate spans. For example, rather than point at the macro's defining function, point at a helper function where the token is made. Or to set the origin to the current macro when the token was produced by another which should an implementation detail. I'm not sure what such an interface should look like (and is probably not necessary in an initial library).&lt;/p&gt;
+
+ &lt;h3&gt;Feature gates&lt;/h3&gt;
+
+ &lt;pre&gt;&lt;code&gt;pub mod features {
+ pub enum FeatureStatus {
+ // The feature gate is allowed.
+ Allowed,
+ // The feature gate has not been enabled.
+ Disallowed,
+ // Use of the feature is forbidden by the compiler.
+ Forbidden,
+ }
+
+ pub fn query_feature(cx: &amp;amp;MacroContext, feature: Token) -&amp;gt; Result&amp;lt;FeatureStatus, ErrStruct&amp;gt;;
+ pub fn query_feature_by_str(cx: &amp;amp;MacroContext, feature: &amp;amp;str) -&amp;gt; Result&amp;lt;FeatureStatus, ErrStruct&amp;gt;;
+ pub fn query_feature_unused(cx: &amp;amp;MacroContext, feature: Token) -&amp;gt; Result&amp;lt;FeatureStatus, ErrStruct&amp;gt;;
+ pub fn query_feature_by_str_unused(cx: &amp;amp;MacroContext, feature: &amp;amp;str) -&amp;gt; Result&amp;lt;FeatureStatus, ErrStruct&amp;gt;;
+
+ pub fn used_feature_gate(cx: &amp;amp;MacroContext, feature: Token) -&amp;gt; Result&amp;lt;(), ErrStruct&amp;gt;;
+ pub fn used_feature_by_str(cx: &amp;amp;MacroContext, feature: &amp;amp;str) -&amp;gt; Result&amp;lt;(), ErrStruct&amp;gt;;
+
+ pub fn allow_feature_gate(cx: &amp;amp;MacroContext, feature: Token) -&amp;gt; Result&amp;lt;(), ErrStruct&amp;gt;;
+ pub fn allow_feature_by_str(cx: &amp;amp;MacroContext, feature: &amp;amp;str) -&amp;gt; Result&amp;lt;(), ErrStruct&amp;gt;;
+ pub fn disallow_feature_gate(cx: &amp;amp;MacroContext, feature: Token) -&amp;gt; Result&amp;lt;(), ErrStruct&amp;gt;;
+ pub fn disallow_feature_by_str(cx: &amp;amp;MacroContext, feature: &amp;amp;str) -&amp;gt; Result&amp;lt;(), ErrStruct&amp;gt;;
+ }
+ &lt;/code&gt;&lt;/pre&gt;
+
+ &lt;p&gt;The &lt;code&gt;query_*&lt;/code&gt; functions query if a feature gate has been set. They return an error if the feature gate does not exist. The &lt;code&gt;_unused&lt;/code&gt; variants do not mark the feature gate as used. The &lt;code&gt;used_&lt;/code&gt; functions mark a feature gate as used, or return an error if it does not exist.&lt;/p&gt;
+
+ &lt;p&gt;The &lt;code&gt;allow_&lt;/code&gt; and &lt;code&gt;disallow_&lt;/code&gt; functions set a feature gate as allowed or disallowed for the current crate. These functions will only affect feature gates which take affect after parsing and expansion are complete. They do not affect feature gates which are checked during parsing or expansion.&lt;/p&gt;
+
+ &lt;p&gt;Question: do we need the &lt;code&gt;used_&lt;/code&gt; functions? Could just call &lt;code&gt;query_&lt;/code&gt; and ignore the result.&lt;/p&gt;
+
+ &lt;h3&gt;Attributes&lt;/h3&gt;
+
+ &lt;p&gt;We need some mechanism for setting attributes as used. I don't actually know how the unused attribute checking in the compiler works, so I can't spec this area. But, I expect &lt;code&gt;MacroContext&lt;/code&gt; to make available some interface for reading attributes on a macro use and marking them as used.&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-18T21:40:42+00:00</dc:date>
+ <dc:creator>Nick Cameron</dc:creator>
+ </item>
+ <item rdf:about="http://geekyogre.com/rss/63eb682d-66b4-447d-8fb6-f4ed448019df">
+ <title>Seif Lotfy: Skizze progress and REPL</title>
+ <link>http://geekyogre.com/skizze-progress-and-repl/</link>
+ <content:encoded>&lt;p&gt;&lt;img align=&quot;center&quot; height=&quot;190&quot; src=&quot;http://i.imgur.com/9z47NdA.png&quot; width=&quot;600&quot; /&gt; &lt;br /&gt;
+ &lt;br /&gt; &lt;br /&gt;
+ Over the last 3 weeks, based on feedback we proceeded fledging out the concepts and the code behind &lt;a href=&quot;https://github.com/skizzehq/skizze&quot;&gt;Skizze&lt;/a&gt;. &lt;br /&gt;
+ &lt;a href=&quot;https://medium.com/@njpatel/&quot;&gt;Neil Patel&lt;/a&gt; suggested the following:&lt;/p&gt;
+
+ &lt;hr /&gt;
+
+ &lt;p&gt;&lt;em&gt;So I've been thinking about the server API. I think we want to choose one thing and do it as well as possible, instead of having six ways to talk to the server. I think that helps to keep things sane and simple overall.&lt;/em&gt;&lt;/p&gt;
+
+ &lt;p&gt;&lt;em&gt;Thinking about usage, I can only really imagine Skizze in an environment like &lt;a href=&quot;https://xamarin.com/insights&quot;&gt;ours&lt;/a&gt;, which is high-throughput. I think that is it's 'home' and we should be optimising for that all day long.&lt;/em&gt;&lt;/p&gt;
+
+ &lt;p&gt;&lt;em&gt;Taking that into account, I believe we have two options:&lt;/em&gt;&lt;/p&gt;
+
+ &lt;ol&gt;
+ &lt;li&gt;&lt;p&gt;&lt;em&gt;We go the gRPC route, provide .proto files and let people use the existing gRPC tooling to build support for their favourite language. That means we can happily give Ruby/Node/C#/etc devs a real way to get started up with Skizze almost immediately, piggy-backing on the gRPC docs etc.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;p&gt;&lt;em&gt;We absorb the Redis Protocol. It does everything we need, is very lean, and we can (mostly) easily adapt it for what we need to do. The downside is that to get support from other libs, there will have to be actual libraries for every language. This could slow adoption, or it might be easy enough if people can reuse existing REDIS code. It's hard to tell how that would end up.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
+ &lt;/ol&gt;
+
+ &lt;p&gt;&lt;em&gt;gRPC is interesting because it's built already for distributed systems, across bad networks, and obviously is bi-directional etc. Without us having to spend time on the protocol, gRPC let's us easily add features that require streaming. Like, imagine a client being able to listen for changes in count/size and be notified instantly. That's something that gRPC is built for right now.&lt;/em&gt;&lt;/p&gt;
+
+ &lt;p&gt;&lt;em&gt;I think gRPC is a bit verbose, but I think it'll pay off for ease of third-party lib support and as things grow.&lt;/em&gt;&lt;/p&gt;
+
+ &lt;p&gt;&lt;em&gt;The CLI could easily be built to work with gRPC, including adding support for streaming stuff etc. Which could be pretty exciting.&lt;/em&gt;&lt;/p&gt;
+
+ &lt;hr /&gt;
+
+ &lt;p&gt;That being said, we gave Skizze &lt;a href=&quot;https://github.com/skizzehq/&quot;&gt;a new home&lt;/a&gt;, where based on feedback we developed .proto files and started rewriting big chunks of the code.&lt;/p&gt;
+
+ &lt;p&gt;We added a new wrapper called &quot;domain&quot; which represents a stream. It wraps around Count-Min-Log, Bloom Filter, Top-K and HyperLogLog++, so when feeding it values it feeds all the sketches. Later we intend to allow attaching and detaching sketches from &quot;domains&quot; (We need a better name).&lt;/p&gt;
+
+ &lt;p&gt;We also implemented a gRPC API which should allow easy wrapper creation in other languages.&lt;/p&gt;
+
+ &lt;p&gt;Special thanks go to &lt;a href=&quot;https://twitter.com/martinpintob&quot;&gt;Martin Pinto&lt;/a&gt; for helping out with unit tests and &lt;a href=&quot;http://dopeness.org&quot;&gt;Soren Macbeth&lt;/a&gt; for thorough feedback and ideas about the &quot;domain&quot; concept. &lt;br /&gt;
+ Take a look at our initial REPL work there:&lt;/p&gt;
+
+ &lt;p&gt;&lt;a href=&quot;http://geekyogre.com/content/images/2016/01/MBCY64aaKL.gif&quot;&gt;&lt;img alt=&quot;Link to this page&quot; border=&quot;0&quot; src=&quot;http://geekyogre.com/content/images/2016/01/skizze-1.png&quot; /&gt;&lt;/a&gt; &lt;br /&gt;
+ &lt;a href=&quot;http://geekyogre.com/content/images/2016/01/MBCY64aaKL.gif&quot;&gt;click for GIF&lt;/a&gt;&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-18T17:41:43+00:00</dc:date>
+ <dc:creator>Seif Lotfy</dc:creator>
+ </item>
+ <item rdf:about="http://dougbelshaw.com/blog/?p=39986">
+ <title>Doug Belshaw: What a post-Persona landscape means for Open Badges</title>
+ <link>http://dougbelshaw.com/blog/2016/01/18/open-badges-persona/</link>
+ <content:encoded>&lt;p&gt;&lt;em&gt;&lt;strong&gt;Note:&lt;/strong&gt; I don’t work for Mozilla any more, so (like &lt;a href=&quot;https://www.youtube.com/watch?v=YQHsXMglC9A&quot;&gt;Adele&lt;/a&gt;) these are my thoughts ‘from the outside’…&lt;/em&gt;&lt;/p&gt;
+ &lt;hr /&gt;
+ &lt;h3&gt;Introduction&lt;/h3&gt;
+ &lt;p&gt;&lt;a href=&quot;http://openbadges.org&quot;&gt;Open Badges&lt;/a&gt; is no longer a &lt;a href=&quot;http://mozilla.org&quot;&gt;Mozilla&lt;/a&gt; project. In fact, it hasn’t been for a while — the &lt;a href=&quot;http://badgealliance.org&quot;&gt;Badge Alliance&lt;/a&gt; was set up a couple of years ago to promote the specification on a both a technical and community basis. As I stated in a recent post, this is a &lt;strong&gt;good&lt;/strong&gt; thing and means that &lt;a href=&quot;http://dougbelshaw.com/blog/2015/11/08/bright-future-badges/&quot;&gt;the future is bright for Open Badges&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;However, Mozilla &lt;em&gt;is&lt;/em&gt; still involved with the Open Badges project: Mark Surman, Executive Director of the Mozilla Foundation, sits on the board of the Badge Alliance. Mozilla also pays for contractors to work on the &lt;a href=&quot;http://backpack.openbadges.org&quot;&gt;Open Badges backpack&lt;/a&gt; and there were badges earned at the &lt;a href=&quot;http://mozillafestival.org&quot;&gt;Mozilla Festival&lt;/a&gt; a few months ago.&lt;/p&gt;
+ &lt;p&gt;Although it may seem strange for those used to corporates interested purely in profit, Mozilla creates what the open web needs at any given time. Like any organisation, sometimes it gets these wrong, either because the concept was flawed, or because the execution was poor. Other times, I’d argue, Mozilla doesn’t give ideas and concepts enough time to gain traction.&lt;/p&gt;
+ &lt;h3&gt;The end of Persona at Mozilla&lt;/h3&gt;
+ &lt;p&gt;Open Badges, at its very essence, is a technical specification. It allows credentials with metadata hard-coded into them to be issued, exchanged, and displayed. This is done in a secure, standardised manner.&lt;/p&gt;
+ &lt;p&gt;&lt;img alt=&quot;OBI diagram&quot; class=&quot;alignnone wp-image-39987 size-full&quot; src=&quot;http://i1.wp.com/dougbelshaw.com/blog/wp-content/uploads/2016/01/obi-diagram.png?w=100%25&quot; /&gt;&lt;/p&gt;
+ &lt;p&gt;For users to be able to access their ‘backpack’ (i.e. the place they store badges) they needed a secure login system.Back in 2011 at the start of the Open Badges project it made sense to make use of Mozilla’s nascent &lt;a href=&quot;https://www.mozilla.org/en-US/persona/&quot;&gt;Persona&lt;/a&gt; project. This aimed to provide a way for users to easily sign into sites around the web without using their Facebook/Google logins. These ‘social’ sign-in methods mean that users are tracked around the web — something that Mozilla was obviously against.&lt;/p&gt;
+ &lt;p&gt;By 2014, Persona wasn’t seen to be having the kind of ‘growth trajectory’ that Mozilla wanted. The project was transferred to &lt;a href=&quot;http://identity.mozilla.com/post/78873831485/transitioning-persona-to-community-ownership&quot;&gt;community ownership&lt;/a&gt; and most of the team left Mozilla in 2015. It was &lt;a href=&quot;https://groups.google.com/forum/#!msg/mozilla.dev.identity/mibOQrD6K0c/kt0NdMWbEQAJ&quot;&gt;announced&lt;/a&gt; that Persona would be shutting down as a Mozilla service in November 2016. While Persona will exist as an open source project, it won’t be hosted by Mozilla.&lt;/p&gt;
+ &lt;h3&gt;What this means for Open Badges&lt;/h3&gt;
+ &lt;p&gt;Although I’m not aware of an official announcement from the Badge Alliance, I think it’s worth making three points here.&lt;/p&gt;
+ &lt;h5&gt;1. You can still use Persona&lt;/h5&gt;
+ &lt;p&gt;If you’re a developer, you can still use Persona. It’s open source. It works.&lt;/p&gt;
+ &lt;h5&gt;2. Persona is not central to the Open Badges Infrastructure&lt;/h5&gt;
+ &lt;p&gt;The Open Badges backpack is &lt;em&gt;one&lt;/em&gt; place where users can store their badges. There are others, including the &lt;a href=&quot;https://openbadgepassport.com/&quot;&gt;Open Badge Passport&lt;/a&gt; and &lt;a href=&quot;https://www.openbadgeacademy.com/&quot;&gt;Open Badge Academy&lt;/a&gt;. MacArthur, who seed-funded the Open Badges ecosystem, have a new platform launching through &lt;a href=&quot;https://www.lrng.org/&quot;&gt;LRNG&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;It is up to the organisations behind these various solutions as to how they allow users to authenticate. They may choose to allow social logins. They may force users to create logins based on their email address. They may decide to use an open source version of Persona. It’s entirely up to them.&lt;/p&gt;
+ &lt;h5&gt;3. A post-Persona badges system has its advantages&lt;/h5&gt;
+ &lt;p&gt;The Persona authentication system runs off email addresses. This means that transitioning &lt;em&gt;from&lt;/em&gt; Persona to another system is relatively straightforward. It has, however, meant that for the past few years we’ve had a recurrent problem: what do you do with people being issued badges to multiple email addresses?&lt;/p&gt;
+ &lt;p&gt;Tying badges to emails seemed like the easiest and fastest way to get to a critical mass in terms of Open Badge adoption. Now that’s worked, we need to think in a more nuanced way about allowing users to tie multiple identities to a single badge.&lt;/p&gt;
+ &lt;h4&gt;Conclusion&lt;/h4&gt;
+ &lt;p&gt;Persona was always a slightly awkward fit for Open Badges. Although, for a time, it made sense to use Persona for authentication to the Open Badges backpack, we’re now in a post-Persona landscape. This brings with it certain advantages.&lt;/p&gt;
+ &lt;p&gt;As Nate Otto wrote in his post &lt;a href=&quot;https://medium.com/badge-alliance/open-badges-in-2016-a-look-ahead-3cfe5c3c9878#.l5mhiztwx&quot;&gt;Open Badges in 2016: A Look Ahead&lt;/a&gt;, the project is growing up. It’s time to move beyond what was expedient at the dawn of Open Badges and look to the future. I’m sad to see the decline of Persona, but I’m excited what the future holds!&lt;/p&gt;
+ &lt;p style=&quot;text-align: right;&quot;&gt;&lt;em&gt;Header image CC BY-NC-SA &lt;a href=&quot;https://www.flickr.com/photos/blmiers2/6904758951/&quot;&gt;Barbara&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-18T11:34:19+00:00</dc:date>
+ <dc:creator>Doug Belshaw</dc:creator>
+ </item>
+ <item rdf:about="tag:this-week-in-rust.org,2016-01-18:blog/2016/01/18/this-week-in-rust-114/">
+ <title>This Week In Rust: This Week in Rust 114</title>
+ <link>http://this-week-in-rust.org/blog/2016/01/18/this-week-in-rust-114/</link>
+ <content:encoded>&lt;p&gt;Hello and welcome to another issue of &lt;em&gt;This Week in Rust&lt;/em&gt;!
+ &lt;a href=&quot;http://rust-lang.org&quot;&gt;Rust&lt;/a&gt; is a systems language pursuing the trifecta:
+ safety, concurrency, and speed. This is a weekly summary of its progress and
+ community. Want something mentioned? Tweet us at &lt;a href=&quot;https://twitter.com/ThisWeekInRust&quot;&gt;@ThisWeekInRust&lt;/a&gt; or &lt;a href=&quot;mailto:corey@octayn.net?subject=This%20Week%20in%20Rust%20Suggestion&quot;&gt;send us an
+ email&lt;/a&gt;!
+ Want to get involved? &lt;a href=&quot;https://github.com/rust-lang/rust/blob/master/CONTRIBUTING.md&quot;&gt;We love
+ contributions&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;&lt;em&gt;This Week in Rust&lt;/em&gt; is openly developed &lt;a href=&quot;https://github.com/cmr/this-week-in-rust&quot;&gt;on GitHub&lt;/a&gt;.
+ If you find any errors in this week's issue, &lt;a href=&quot;https://github.com/cmr/this-week-in-rust/pulls&quot;&gt;please submit a PR&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;This week's edition was edited by: &lt;a href=&quot;https://github.com/nasa42&quot;&gt;nasa42&lt;/a&gt;, &lt;a href=&quot;https://github.com/brson&quot;&gt;brson&lt;/a&gt;, and &lt;a href=&quot;https://github.com/llogiq&quot;&gt;llogiq&lt;/a&gt;.&lt;/p&gt;
+ &lt;h3&gt;Updates from Rust Community&lt;/h3&gt;
+ &lt;h4&gt;News &amp;amp; Blog Posts&lt;/h4&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://gregchapple.com/contributing-to-the-rust-compiler/&quot;&gt;Guide: Contributing to the Rust compiler&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.ncameron.org/blog/a-type-safe-and-zero-allocation-library-for-reading-and-navigating-elf-files/&quot;&gt;A type-safe and zero-allocation library for reading and navigating ELF files&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;[podcast] &lt;a href=&quot;http://www.newrustacean.com/show_notes/e009/&quot;&gt;New Rustacean podcast episode 09&lt;/a&gt;. Getting into the nitty-gritty with Rust's traits.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://jadpole.github.io/arcaders/arcaders-1-12/&quot;&gt;ArcadeRS 1.12: Brawl, at last&lt;/a&gt;! Part of the series &lt;a href=&quot;https://jadpole.github.io/arcaders/arcaders-1-0/&quot;&gt;ArcadeRS 1.0: The project&lt;/a&gt; - a series whose objective is to explore the Rust programming language and ecosystem through the development of a simple, old-school shooter.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://blog.thiago.me/raspberry-pi-bare-metal-programming-with-rust/&quot;&gt;Raspberry Pi bare metal programming with Rust&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://blog.servo.org/2016/01/11/twis-47/&quot;&gt;This week in Servo 47&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.redox-os.org/news/this-week-in-redox-10/&quot;&gt;This week in Redox OS 10&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h4&gt;Notable New Crates &amp;amp; Project Updates&lt;/h4&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/ebkalderon/amethyst&quot;&gt;Amethyst&lt;/a&gt;. Data-oriented game engine written in Rust.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://www.rust-lang.org/&quot;&gt;Rust website&lt;/a&gt; has received some &lt;a href=&quot;https://www.reddit.com/r/rust/comments/40zxey/major_website_updates/&quot;&gt;major updates&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://packages.debian.org/stretch/rustc&quot;&gt;Rust&lt;/a&gt; and &lt;a href=&quot;https://packages.debian.org/stretch/cargo&quot;&gt;Cargo&lt;/a&gt; are now available in Debian stretch.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://community.particle.io/t/rust-on-particle-call-for-contributors/19090&quot;&gt;Rust on Particle: Call for contributors&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://dwrensha.github.io/capnproto-rust/2016/01/11/async-rpc.html&quot;&gt;capnp-rpc-rust rewritten to use async I/O&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/Ogeon/palette&quot;&gt;Palette&lt;/a&gt;. A Rust library for linear color calculations and conversion.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;Updates from Rust Core&lt;/h3&gt;
+ &lt;p&gt;164 pull requests were &lt;a href=&quot;https://github.com/issues?q=is%3Apr+org%3Arust-lang+is%3Amerged+merged%3A2016-01-11..2016-01-18&quot;&gt;merged in the last week&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;See the &lt;a href=&quot;https://internals.rust-lang.org/t/triage-digest-tue-jan-05-2016/3052&quot;&gt;triage digest&lt;/a&gt; and &lt;a href=&quot;https://internals.rust-lang.org/t/subteam-reports-2016-01-08/3067&quot;&gt;subteam reports&lt;/a&gt; for more details.&lt;/p&gt;
+ &lt;h4&gt;Notable changes&lt;/h4&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30943&quot;&gt;std: Stabilize APIs for the 1.7 release&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/27807&quot;&gt;Refactor and improve: Arena, TypedArena&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/29498&quot;&gt;Let &lt;code&gt;str::replace&lt;/code&gt; take a pattern&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30295&quot;&gt;rustc_resolve: Fix bug in duplicate checking for extern crates&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30426&quot;&gt;Rewrite BTreeMap to use parent pointers&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30446&quot;&gt;Support generic associated consts&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30509&quot;&gt;Add an &lt;code&gt;impl&lt;/code&gt; for &lt;code&gt;Box&amp;lt;Error&amp;gt;&lt;/code&gt; from String&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30533&quot;&gt;Introduce &quot;obligation forest&quot; data structure into fulfillment to track backtraces&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30538&quot;&gt;Remove negate_unsigned feature gate&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30567&quot;&gt;llvm: Add support for vectorcall (X86_VectorCall) convention&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30676&quot;&gt;Make coherence more tolerant of error types&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30740&quot;&gt;Add fast path for ASCII in UTF-8 validation&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30753&quot;&gt;Downgrade unit struct match via S(..) warnings to errors&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30930&quot;&gt;Move const block checks before lowering step&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h4&gt;New Contributors&lt;/h4&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Anton Blanchard&lt;/li&gt;
+ &lt;li&gt;Jonas Tepe&lt;/li&gt;
+ &lt;li&gt;Jörg Krause&lt;/li&gt;
+ &lt;li&gt;Joshua Olson&lt;/li&gt;
+ &lt;li&gt;kalita.alexey&lt;/li&gt;
+ &lt;li&gt;Pierre Krieger&lt;/li&gt;
+ &lt;li&gt;Sergey Veselkov&lt;/li&gt;
+ &lt;li&gt;Simon Martin&lt;/li&gt;
+ &lt;li&gt;Steffen&lt;/li&gt;
+ &lt;li&gt;tomaka&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h4&gt;Approved RFCs&lt;/h4&gt;
+ &lt;p&gt;Changes to Rust follow the Rust &lt;a href=&quot;https://github.com/rust-lang/rfcs#rust-rfcs&quot;&gt;RFC (request for comments)
+ process&lt;/a&gt;. These
+ are the RFCs that were approved for implementation this week:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1331&quot;&gt;RFC 1331: &lt;code&gt;src/grammar&lt;/code&gt; for the canonical grammar of the Rust language&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h4&gt;Final Comment Period&lt;/h4&gt;
+ &lt;p&gt;Every week &lt;a href=&quot;https://rust-lang.org/team.html&quot;&gt;the team&lt;/a&gt; announces the
+ 'final comment period' for RFCs and key PRs which are reaching a
+ decision. Express your opinions now. &lt;a href=&quot;https://github.com/rust-lang/rfcs/labels/final-comment-period&quot;&gt;This week's FCPs&lt;/a&gt; are:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1462&quot;&gt;Add &lt;code&gt;[&lt;/code&gt; to the FOLLOW(ty) in macro future-proofing rules&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1457&quot;&gt;Rewrite &lt;code&gt;for&lt;/code&gt; loop desugaring to use language items&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1320&quot;&gt;Amend 1192 (RangeInclusive) to use an enum&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/243&quot;&gt;Trait-based exception handling&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1361&quot;&gt;Improve Cargo target-specific dependencies&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1129&quot;&gt;Add a &lt;code&gt;IndexAssign&lt;/code&gt; trait that allows overloading &quot;indexed assignment&quot; expressions like &lt;code&gt;a[b] = c&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1196&quot;&gt;Allow eliding more type parameters&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1296&quot;&gt;Add an &lt;code&gt;alias&lt;/code&gt; attribute to &lt;code&gt;#[link]&lt;/code&gt; and &lt;code&gt;-l&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h4&gt;New RFCs&lt;/h4&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1459&quot;&gt;Add a used attribute to prevent symbols from being discarded&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1461&quot;&gt;Move some net2 functionality into libstd&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1465&quot;&gt;Add &lt;code&gt;some!&lt;/code&gt; macro for unwrapping Option more safely&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1467&quot;&gt;Stabilize the &lt;code&gt;volatile_load&lt;/code&gt; and &lt;code&gt;volatile_store&lt;/code&gt; intrinsics as &lt;code&gt;ptr::volatile_read&lt;/code&gt; and &lt;code&gt;ptr::volatile_write&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;Upcoming Events&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.meetup.com/Rust-Meetup-Hamburg/events/227838367/&quot;&gt;1/19. Rust Hack and Learn Hamburg @ Ponton&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.meetup.com/Rust-Bay-Area/events/227841778/&quot;&gt;1/21. SF Bay Area: Rust Concurrency and Parallelism&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.meetup.com/opentechschool-berlin/&quot;&gt;1/27. OpenTechSchool Berlin: Rust Hack and Learn&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;If you are running a Rust event please add it to the &lt;a href=&quot;https://www.google.com/calendar/embed?src=apd9vmbc22egenmtu5l6c5jbfc%40group.calendar.google.com&quot;&gt;calendar&lt;/a&gt; to get
+ it mentioned here. Email &lt;a href=&quot;mailto:erick.tryzelaar@gmail.com&quot;&gt;Erick Tryzelaar&lt;/a&gt; or &lt;a href=&quot;mailto:banderson@mozilla.com&quot;&gt;Brian
+ Anderson&lt;/a&gt; for access.&lt;/p&gt;
+ &lt;h3&gt;fn work(on: RustProject) -&amp;gt; Money&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://maidsafe.net/rust_engineer.html&quot;&gt;Rust Engineer&lt;/a&gt; at MaidSafe.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://careers.mozilla.org/en-US/position/ozy21fwU&quot;&gt;Research Engineer - Servo&lt;/a&gt; at Mozilla.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://careers.mozilla.org/en-US/position/o0H41fww&quot;&gt;Senior Research Engineer - Rust&lt;/a&gt; at Mozilla.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://plv.mpi-sws.org/rustbelt/&quot;&gt;PhD and postdoc positions&lt;/a&gt; at MPI-SWS.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;&lt;em&gt;Tweet us at &lt;a href=&quot;https://twitter.com/ThisWeekInRust&quot;&gt;@ThisWeekInRust&lt;/a&gt; to get your job offers listed here!&lt;/em&gt;&lt;/p&gt;
+ &lt;h3&gt;Crate of the Week&lt;/h3&gt;
+ &lt;p&gt;This week's Crate of the Week is &lt;a href=&quot;https://github.com/alexcrichton/toml-rs&quot;&gt;toml&lt;/a&gt;, a crate for all our configuration needs, simple yet effective.&lt;/p&gt;
+ &lt;p&gt;Thanks to &lt;a href=&quot;https://users.rust-lang.org/users/stebalien&quot;&gt;Steven Allen&lt;/a&gt; for the suggestion.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;https://users.rust-lang.org/t/crate-of-the-week/2704&quot;&gt;Submit your suggestions for next week&lt;/a&gt;!&lt;/p&gt;
+ &lt;h3&gt;Quote of the Week&lt;/h3&gt;
+ &lt;blockquote&gt;
+ &lt;p&gt;Borrow/lifetime errors are usually Rust compiler bugs.
+ Typically, I will spend 20 minutes detailing the precise conditions of
+ the bug, using language that understates my immense knowledge, while
+ demonstrating sympathetic understanding of the pressures placed on a
+ Rust compiler developer, who is also probably studying for several exams
+ at the moment. The developer reading my bug report may not understand
+ this stuff as well as I do, so I will carefully trace the lifetimes of
+ each variable, where memory is allocated on the stack vs the heap, which
+ struct or function owns a value at any point in time, where borrows
+ begin and where they... oh yeah, actually that variable really doesn't
+ live long enough.&lt;/p&gt;
+ &lt;/blockquote&gt;
+ &lt;p&gt;— &lt;a href=&quot;https://www.reddit.com/r/rust/comments/4084yx/my_trick_when_i_get_stuck_as_a_beginner/cysqz3s&quot;&gt;peterjoel on /r/rust&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;Thanks to &lt;a href=&quot;https://users.rust-lang.org/users/WaDelma&quot;&gt;Wa Delma&lt;/a&gt; for the suggestion.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;http://users.rust-lang.org/t/twir-quote-of-the-week/328&quot;&gt;Submit your quotes for next week&lt;/a&gt;!&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-18T05:00:00+00:00</dc:date>
+ <dc:creator>Corey Richardson</dc:creator>
+ </item>
+ <item rdf:about="http://nikkisquared.github.io/2016/01/17/what-does-your-work-mean-part-2.html">
+ <title>Nikki Bee: Okay, But What Does Your Work Actually Mean, Nikki? Part 2: The Fetch Standard and Servo</title>
+ <link>http://nikkisquared.github.io/2016/01/17/what-does-your-work-mean-part-2.html</link>
+ <content:encoded>&lt;p&gt;In my previous post, I started discussing in more detail what my internship entails, by talking about my first contribution to Servo. As a refresher, my first contribution was as part of my application to Outreachy, which I later revisited during my internship after a change I introduced to the HTML Standard it relied on. I’m going to expand on that last point today- specifically, how easy it is to introduce changes in &lt;a href=&quot;https://wiki.whatwg.org/wiki/FAQ#What_is_the_WHATWG.3F&quot;&gt;WHATWG&lt;/a&gt;’s various standards. I’m also going to talk about how this accessibility to changing web standards affects how I can understand it, how I can help improve it, and my work on Servo.&lt;/p&gt;
+
+ &lt;h3&gt;Two Ways To Change&lt;/h3&gt;
+
+ &lt;p&gt;There are many ways to &lt;a href=&quot;https://wiki.whatwg.org/wiki/What_you_can_do&quot;&gt;get involved with WHATWG&lt;/a&gt;, but there are two that I’ve become the most familiar with: firstly, by opening a discussion about a perceived issue and asking how it should be resolved; secondly, by taking on an issue approved as needing change and making the desired change. I’ve almost entirely only done the former, and the latter only for some minor typos. Any changes that relate directly to my work, however minor, are significant for me though! Like I discussed in my previous post, I brought attention to &lt;a href=&quot;https://github.com/whatwg/html/issues/296&quot;&gt;an inconsistency&lt;/a&gt; that was resolved, giving me a new task of updating my first contribution to Servo to reflect the change in the HTML Standard. I’ve done that several times since, for the Fetch Standard.&lt;/p&gt;
+
+ &lt;h3&gt;Understanding Fetch&lt;/h3&gt;
+
+ &lt;p&gt;My first two weeks of my internship were spent on reading through the majority of the &lt;a href=&quot;https://fetch.spec.whatwg.org/&quot;&gt;Fetch Standard&lt;/a&gt;, primarily the various Fetch functions. I took many notes describing the steps to myself, annotated with questions I had and the answers I got from either other people on the Servo team who had worked with Fetch (including my internship mentor, of course!) or people from WHATWG who were involved in the Fetch Standard. Getting so familiar with Fetch meant a few things: I would notice minor errors (such as an out of date link) that I could submit a &lt;a href=&quot;https://github.com/whatwg/fetch/pull/173&quot;&gt;simple fix for&lt;/a&gt;, or a bigger issue that I couldn’t resolve myself.&lt;/p&gt;
+
+ &lt;h3&gt;Discussions &amp;amp; Resolutions&lt;/h3&gt;
+
+ &lt;p&gt;I’m going to go into more detail about some of those bigger issues. From my perspective, when I start a discussion about a piece of documentation (such as the Fetch Standard, or reading about a programming library Servo uses), I go into it thinking “Either this documentation is incorrect, or my understanding is incorrectâ€. Whichever the answer is, it doesn’t mean that the documentation is bad, or that I’m bad at reading comprehension. I understand best by building up a model of something in my head, putting that to practice, and asking a lot of questions along the way. I learn by getting things wrong and figuring out why I was wrong, and sometimes in the process I uncover a point that could be made more clear, or an inconsistency! I have good examples of both of the different outcomes I listed, which I’ll cover over the next two sections.&lt;/p&gt;
+
+ &lt;h5&gt;Looking For The Big Picture&lt;/h5&gt;
+
+ &lt;p&gt;Early on in my initial review of the Fetch Standard’s several protocols, I found a major step that seemed to have no use. I understood that since I was learning Fetch on a step-by-step basis, I did not have a view of the bigger picture, so I asked around what I was missing that would help me understand this. One of the people I work with on implementing Fetch agreed with me that the step seemed to have no purpose, and so we decided to &lt;a href=&quot;https://github.com/whatwg/fetch/issues/174&quot;&gt;open an issue&lt;/a&gt; asking about removing it from the standard. It turned out that I had actually missed the meaning of it, as we learned. However, instead of leaving it there, I shifted the issue into asking for some explanatory notes on why this step is needed, which was fulfilled. This meant that I would have a reference to go back to should I forget the significance of the step, and that people reading the Fetch Standard in the future would be much less likely to come to the same incorrect conclusion I had.&lt;/p&gt;
+
+ &lt;h5&gt;A Confusing Order&lt;/h5&gt;
+
+ &lt;p&gt;Shortly after I had first discovered that apparent issue, I found myself struggling to comprehend a sequence of actions in another Fetch protocol. The specification seemed to say that part of an early step was meant to only be done after the final step. I unfortunately don’t remember details of the discussion I had about this- if there was a reason for why it was organized like this, I forget what it was. Regardless, it was agreed that &lt;a href=&quot;https://github.com/whatwg/fetch/issues/176&quot;&gt;moving those sub-steps&lt;/a&gt; to be actually listed after the step they’re supposed to run after would be a good change. This meant that I would need to re-organize my notes to reflect the re-arranged sequence of actions, as well as have an easier time being able to follow this part of the Fetch Standard.&lt;/p&gt;
+
+ &lt;h3&gt;A Living Standard&lt;/h3&gt;
+
+ &lt;p&gt;Like I said at the start of this post, I’m going to talk about how changes in the Fetch Standard affects my work on Servo itself. What I’ve covered so far has mostly been how changes affect my understanding of the standard itself. A key aspect in understanding the Fetch protocols is reviewing them for updates that impact me. WHATWG labels every standard they author as a “&lt;a href=&quot;https://wiki.whatwg.org/wiki/FAQ#What_does_.22Living_Standard.22_mean.3F&quot;&gt;Living Standard&lt;/a&gt;†for good reason. It was one thing for me to learn how easy it is to introduce changes, while knowing exactly what’s going on, but it’s another for me to understand that anybody else can, and often does, make changes to the Fetch Standard!&lt;/p&gt;
+
+ &lt;h5&gt;Changes Over Time&lt;/h5&gt;
+
+ &lt;p&gt;When an update is made to the Fetch Standard, it’s not so difficult to deal with as one might imagine. The Fetch Standard always notes the last day it was updated at the top of the document, I follow a Twitter account that &lt;a href=&quot;https://twitter.com/fetchstandard&quot;&gt;posts about updates&lt;/a&gt;, and all the history can be &lt;a href=&quot;https://github.com/whatwg/fetch/commits&quot;&gt;seen on GitHub&lt;/a&gt; which will show me exactly what has been changed as well as some discussion on what the change does. All of these together alert me to the fact that the Fetch Standard has been modified, and I can quickly see what was revised. If it’s relevant to what I’m going to be implementing, I update my notes to match it. Occasionally, I need to change existing code to reflect the new Standard, which is also easily done by comparing my new notes to the Fetch implementation in Servo!&lt;/p&gt;
+
+ &lt;h5&gt;Snapshots&lt;/h5&gt;
+
+ &lt;p&gt;From all of this, it might sound like the Fetch Standard is unfinished, or unreliable/inconsistent. I don’t mean to misrepresent it- the many small improvements help make the Fetch Standard, like all of WHATWG’s standards, better and more reliable. You can think of the status of the Fetch Standard at any point in time as a single, working snapshot. If somebody implemented all of Fetch as it is now, they’d have something that works by itself correctly. A different snapshot of Fetch is just that- different. It will have an improvement or two, but that doesn’t obsolete anybody who implemented it previously. It just means if they revisit the implementation, they’ll have things to update.&lt;/p&gt;
+
+ &lt;p&gt;Third post over.&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-17T20:20:27+00:00</dc:date>
+ </item>
+ <item rdf:about="http://ngokevin.com/blog/aframe-component/">
+ <title>Kevin Ngo: How to Write an A-Frame VR Component</title>
+ <link>http://ngokevin.com/blog/aframe-component/</link>
+ <content:encoded>&lt;img align=&quot;left&quot; hspace=&quot;5&quot; src=&quot;http://thevrjump.com/assets/img/articles/aframe-system/aframe-example.jpg&quot; width=&quot;320&quot; /&gt;Abstract representation of components by @rubenmueller of thevrjump.com.
+
+ &lt;p&gt;&lt;a href=&quot;http://ngokevin.com/blog/aframe&quot;&gt;A-Frame&lt;/a&gt; is a WebVR framework that introduces the
+ &lt;a href=&quot;http://ngokevin.com/blog/aframe-vs-3dml&quot;&gt;entity-component system&lt;/a&gt; (&lt;a href=&quot;http://ngokevin.com/rss/docs&quot;&gt;docs&lt;/a&gt;) to the DOM. The
+ entity-component system treats every &lt;strong&gt;entity&lt;/strong&gt; in the scene as a placeholder
+ object which we apply and mix &lt;strong&gt;components&lt;/strong&gt; to in order to add appearance,
+ behavior, and functionality. A-Frame comes with some standard components out of
+ the box like camera, geometry, material, light, or sound. However, people can
+ write, publish, and register their own components to do &lt;strong&gt;whatever&lt;/strong&gt; they want
+ like have entities &lt;a href=&quot;https://github.com/dmarcos/a-invaders/tree/master/js/components&quot;&gt;collide/explode/spawn&lt;/a&gt;, be controlled by
+ &lt;a href=&quot;https://github.com/ngokevin/aframe-physics-components&quot;&gt;physics&lt;/a&gt;, or &lt;a href=&quot;https://jsbin.com/dasefeh/edit?html,output&quot;&gt;follow a path&lt;/a&gt;. Today, we'll be going through
+ how we can write our own A-Frame components.&lt;/p&gt;
+ &lt;blockquote&gt;
+ &lt;p&gt;Note that this tutorial will be covering the upcoming release of &lt;a href=&quot;https://github.com/aframevr/aframe/blob/dev/CHANGELOG.md#dev&quot;&gt;A-Frame
+ 0.2.0&lt;/a&gt; which vastly improves the component API.&lt;/p&gt;
+ &lt;/blockquote&gt;
+ &lt;h3&gt;Table of Contents&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#what-a-component-looks-like&quot;&gt;What a Component Looks Like&lt;/a&gt;&lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#from-the-dom&quot;&gt;From the DOM&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#under-the-hood&quot;&gt;Under the Hood&lt;/a&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#defining-the-schema&quot;&gt;Defining the Schema&lt;/a&gt;&lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#property-types&quot;&gt;Property Types&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#single-property-schemas&quot;&gt;Single-Property Schemas&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#multiple-property-schemas&quot;&gt;Multiple-Property Schemas&lt;/a&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#defining-the-lifecycle-methods&quot;&gt;Defining the Lifecycle Methods&lt;/a&gt;&lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#component-init-set-up&quot;&gt;Component.init() - Set Up&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#component-update-olddata-do-the-magic&quot;&gt;Component.update(oldData) - Do the Magic&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#component-remove-tear-down&quot;&gt;Component.remove() - Tear Down&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#component-tick-time-background-behavior&quot;&gt;Component.tick() - Background Behavior&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#component-pause-and-component-play-stop-and-go&quot;&gt;Component.pause() and Component.play() - Stop and Go&lt;/a&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#boilerplate&quot;&gt;Boilerplate&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#examples&quot;&gt;Examples&lt;/a&gt;&lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#text-component&quot;&gt;Text Component&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#physics-components&quot;&gt;Physics Components&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#layout-component&quot;&gt;Layout Component&lt;/a&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;What a Component Looks Like&lt;/h3&gt;
+ &lt;p&gt;A component contains a bucket of data in the form of component properties. This
+ data is used to modify the entity. For example, we might have an &lt;em&gt;engine&lt;/em&gt;
+ component. Possible properties might be &lt;em&gt;horsepower&lt;/em&gt; or &lt;em&gt;cylinders&lt;/em&gt;.&lt;/p&gt;
+ &lt;p&gt;&lt;img alt=&quot;&quot; src=&quot;http://thevrjump.com/assets/img/articles/aframe-system/aframe-system.jpg&quot; /&gt;
+ &lt;/p&gt;&lt;div class=&quot;page-caption&quot;&gt;&lt;span&gt;
+ Abstract representation of a component by @rubenmueller of thevrjump.com.
+ &lt;/span&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;
+ &lt;h4&gt;From the DOM&lt;/h4&gt;
+ &lt;p&gt;Let's first see what a component looks like from the DOM.&lt;/p&gt;
+ &lt;p&gt;For example, the &lt;a href=&quot;https://aframe.io/docs/components/light.html&quot;&gt;light component&lt;/a&gt; has properties such as type, color,
+ and intensity. In A-Frame, we register and configure a component to an entity
+ using an HTML attribute and a style-like syntax:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;a-entity&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;light&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;type: point; color: crimson; intensity: 2.5&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;a-entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;p&gt;This would give us a light in the scene. To demonstrate composability, we could
+ give the light a spherical representation by mixing in the &lt;a href=&quot;https://aframe.io/docs/components/geometry.html&quot;&gt;geometry
+ component&lt;/a&gt;.&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;a-entity&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;geometry&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;primitive: sphere; radius: 5&quot;&lt;/span&gt;
+ &lt;span class=&quot;na&quot;&gt;light&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;type: point; color: crimson; intensity: 2.5&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;a-entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;p&gt;Or we can configure the position component to move the light sphere a bit to the right.&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;a-entity&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;geometry&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;primitive: sphere; radius: 5&quot;&lt;/span&gt;
+ &lt;span class=&quot;na&quot;&gt;light&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;type: point; color: crimson; intensity: 2.5&quot;&lt;/span&gt;
+ &lt;span class=&quot;na&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;5 0 0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;a-entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;p&gt;Given the style-like syntax and that it modifies the appearance and behavior of
+ DOM nodes, component properties can be thought of as a rough analog to CSS. In
+ the near future, I can imagine component property stylesheets.&lt;/p&gt;
+ &lt;h4&gt;Under the Hood&lt;/h4&gt;
+ &lt;p&gt;Now let's see what a component looks like &lt;strong&gt;under the hood&lt;/strong&gt;. A-Frame's most
+ basic component is the &lt;a href=&quot;https://aframe.io/docs/components/position.html&quot;&gt;position component&lt;/a&gt;:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nx&quot;&gt;AFRAME&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;registerComponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'position'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;schema&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'vec3'&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
+
+ &lt;span class=&quot;nx&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;object3D&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;el&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;object3D&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
+ &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;object3D&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;z&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;p&gt;The position component uses only a tiny subset of the component API, but what
+ this does is register the component with the name &quot;position&quot;, define a &lt;code&gt;schema&lt;/code&gt;
+ where the component's value with be parsed to an &lt;code&gt;{x, y, z}&lt;/code&gt; object, and when
+ the component initializes or the component's data updates, set the position of
+ the entity with the &lt;code&gt;update&lt;/code&gt; callback. &lt;code&gt;this.el&lt;/code&gt; is a reference from the
+ component to the DOM element, or entity, and &lt;code&gt;object3D&lt;/code&gt; is the entity's
+ &lt;a href=&quot;http://threejs.org/&quot;&gt;three.js&lt;/a&gt;. Note that A-Frame is built on top of three.js so many
+ components will be using the three.js API.&lt;/p&gt;
+ &lt;p&gt;So we see that components consist of a name and a definition, and then they can
+ be registered to A-Frame. We saw the the position component definition defined
+ a &lt;code&gt;schema&lt;/code&gt; and an &lt;code&gt;update&lt;/code&gt; handler. Components simply consist of the &lt;code&gt;schema&lt;/code&gt;,
+ which defines the shape of the data, and several handlers for the component to
+ modify the entity in reaction to different types of events.&lt;/p&gt;
+ &lt;p&gt;Here is the current list of properties and methods of a component definition:&lt;/p&gt;
+ &lt;table class=&quot;pure-table-striped&quot;&gt;
+ &lt;tbody&gt;&lt;tr&gt;
+ &lt;th&gt;Property&lt;/th&gt;
+ &lt;th&gt;Description&lt;/th&gt;
+ &lt;/tr&gt;
+ &lt;tr&gt;
+ &lt;td&gt;data&lt;/td&gt;
+ &lt;td&gt;Data of the component derived from the schema default values, mixins, and the entity's attributes.&lt;/td&gt;
+ &lt;/tr&gt;
+ &lt;tr&gt;
+ &lt;td&gt;el&lt;/td&gt;
+ &lt;td&gt;Reference to the &lt;a href=&quot;https://aframe.io/docs/core/entity.html&quot;&gt;entity&lt;/a&gt; element.&lt;/td&gt;
+ &lt;/tr&gt;
+ &lt;tr&gt;
+ &lt;td&gt;schema&lt;/td&gt;
+ &lt;td&gt;Names, types, and default values of the component property value(s)&lt;/td&gt;
+ &lt;/tr&gt;
+ &lt;/tbody&gt;&lt;/table&gt;
+
+ &lt;table class=&quot;pure-table-striped&quot;&gt;
+ &lt;tbody&gt;&lt;tr&gt;&lt;th&gt;Method&lt;/th&gt;&lt;th&gt;Description&lt;/th&gt;&lt;/tr&gt;
+ &lt;tr&gt;
+ &lt;td&gt;init&lt;/td&gt;
+ &lt;td&gt;Called once when the component is initialized.&lt;/td&gt;
+ &lt;/tr&gt;
+ &lt;tr&gt;
+ &lt;td&gt;update&lt;/td&gt;
+ &lt;td&gt;Called both when the component is initialized and whenever the component's data changes (e.g, via &lt;i&gt;setAttribute&lt;/i&gt;).&lt;/td&gt;
+ &lt;/tr&gt;
+ &lt;tr&gt;
+ &lt;td&gt;remove&lt;/td&gt;
+ &lt;td&gt;Called when the component detaches from the element (e.g., via &lt;i&gt;removeAttribute&lt;/i&gt;).&lt;/td&gt;
+ &lt;/tr&gt;
+ &lt;tr&gt;
+ &lt;td&gt;tick&lt;/td&gt;
+ &lt;td&gt;Called on each render loop or tick of the scene.&lt;/td&gt;
+ &lt;/tr&gt;
+ &lt;tr&gt;
+ &lt;td&gt;play&lt;/td&gt;
+ &lt;td&gt;Called whenever the scene or entity plays to add any background or dynamic behavior.&lt;/td&gt;
+ &lt;/tr&gt;
+ &lt;tr&gt;
+ &lt;td&gt;pause&lt;/td&gt;
+ &lt;td&gt;Called whenever the scene or entity pauses to remove any background or dynamic behavior.&lt;/td&gt;
+ &lt;/tr&gt;
+ &lt;/tbody&gt;&lt;/table&gt;
+
+ &lt;h3&gt;Defining the Schema&lt;/h3&gt;
+ &lt;p&gt;The component's schema defines what type of data it takes. A component can
+ either be single-property or consist of multiple properties. And properties
+ have &lt;em&gt;property types&lt;/em&gt;. Note that single-property schemas and property types are
+ being released in A-Frame &lt;code&gt;v0.2.0&lt;/code&gt;.&lt;/p&gt;
+ &lt;p&gt;A property might look like:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'int'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;p&gt;And a schema consisting of multiple properties might look like:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'#FFF'&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'selector'&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;uv&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'1 1'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;' '&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;parseFloat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;p&gt;Since components in the entity-component system are just buckets of data that
+ are used to affect the appearance or behavior of the entity, the schema plays a
+ crucial role in the definition of the component.&lt;/p&gt;
+ &lt;h4&gt;Property Types&lt;/h4&gt;
+ &lt;p&gt;A-Frame comes with several built-in property types such as &lt;code&gt;boolean&lt;/code&gt;, &lt;code&gt;int&lt;/code&gt;,
+ &lt;code&gt;number&lt;/code&gt;, &lt;code&gt;selector&lt;/code&gt;, &lt;code&gt;string&lt;/code&gt;, or &lt;code&gt;vec3&lt;/code&gt;. Every single property is assigned a
+ type, whether explicitly through the &lt;code&gt;type&lt;/code&gt; key or implictly via inferring the
+ value. And each type is used to assign &lt;code&gt;parse&lt;/code&gt; and &lt;code&gt;stringify&lt;/code&gt; functions. The
+ parser deserializes the incoming string value from the DOM to be put into the
+ component's data object. The stringifier is used when using &lt;code&gt;setAttribute&lt;/code&gt; to
+ serialize back to the DOM.&lt;/p&gt;
+ &lt;p&gt;We can actually define and register our own property types:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nx&quot;&gt;AFRAME&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;registerPropertyType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'radians'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+
+ &lt;span class=&quot;c1&quot;&gt;// Default stringify is .toString().&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;h4&gt;Single-Property Schemas&lt;/h4&gt;
+ &lt;p&gt;If a component has only one property, then it must either have a &lt;code&gt;type&lt;/code&gt; or a
+ &lt;code&gt;default&lt;/code&gt; value. If the type is defined, then the type is used to parse and
+ coerce the string retrieved from the DOM (e.g., &lt;code&gt;getAttribute&lt;/code&gt;). Or if the
+ default value is defined, the default value is used to infer the type.&lt;/p&gt;
+ &lt;p&gt;Take for instance the &lt;a href=&quot;https://aframe.io/docs/components/visible.html&quot;&gt;visible component&lt;/a&gt;. The schema property
+ definition implicitly defines it as a boolean:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nx&quot;&gt;AFRAME&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;registerComponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'visible'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;schema&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;c1&quot;&gt;// Type will be inferred to be boolean.&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
+
+ &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;p&gt;Or the &lt;a href=&quot;https://aframe.io/docs/components/rotation.html&quot;&gt;rotation component&lt;/a&gt; which explicitly defines the value as a &lt;code&gt;vec3&lt;/code&gt;:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nx&quot;&gt;AFRAME&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;registerComponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'rotation'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;schema&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;c1&quot;&gt;// Default value will be 0, 0, 0 as defined by the vec3 property type.&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'vec3'&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+
+ &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;p&gt;Using these defined property types, schemas are processed by
+ &lt;code&gt;registerComponent&lt;/code&gt; to inject default values, parsers, and stringifiers for
+ each property. So if a default value is not defined, the default value will be
+ whatever the property type defines as the &quot;default default value&quot;.&lt;/p&gt;
+ &lt;h4&gt;Multiple-Property Schemas&lt;/h4&gt;
+ &lt;p&gt;If a component has multiple properties (or one named property), then it consists of
+ one or more property definitions, in the form described above, in an object keyed by
+ property name. For instance, a physics body component might define a schema:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nx&quot;&gt;AFRAME&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;registerComponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'physics-body'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;schema&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;boundingBox&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'vec3'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;z&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;mass&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;velocity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'vec3'&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;p&gt;Having multiple properties is what makes the component take the syntax in the
+ form of &lt;code&gt;physics=&quot;mass: 2; velocity: 1 1 1&quot;&lt;/code&gt;.&lt;/p&gt;
+ &lt;p&gt;With the schema defined, all data coming into the component will be passed
+ through the schema for parsing. Then in the lifecycle methods, the component
+ has access to &lt;code&gt;this.data&lt;/code&gt; which in a single-property schema is a value and in a
+ multiple-propery schema is an object.&lt;/p&gt;
+ &lt;h3&gt;Defining the Lifecycle Methods&lt;/h3&gt;
+ &lt;h4&gt;Component.init() - Set Up&lt;/h4&gt;
+ &lt;p&gt;&lt;code&gt;init&lt;/code&gt; is called once in the component's lifecycle when it is mounted to the
+ entity. &lt;code&gt;init&lt;/code&gt; is generally used to set up variables or members that may used
+ throughout the component or to set up state. Though not every component will
+ need to define an &lt;code&gt;init&lt;/code&gt; handler. Sort of like the component-equivalent method
+ to &lt;code&gt;createdCallback&lt;/code&gt; or &lt;code&gt;React.ComponentDidMount&lt;/code&gt;.&lt;/p&gt;
+ &lt;p&gt;For example, the &lt;code&gt;look-at&lt;/code&gt; component's &lt;code&gt;init&lt;/code&gt; handler sets up some variables:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nx&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;target3D&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;vector&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;THREE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Vector3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
+
+ &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;h4&gt;Component.update(oldData) - Do the Magic&lt;/h4&gt;
+ &lt;p&gt;The &lt;code&gt;update&lt;/code&gt; handler is called both at the beginning of the component's
+ lifecycle with the initial &lt;code&gt;this.data&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; every time the component's data
+ changes (generally during the entity's &lt;code&gt;attributeChangedCallback&lt;/code&gt; like with a
+ &lt;code&gt;setAttribute&lt;/code&gt;). The update handler gets access to the previous state of the
+ component data passed in through &lt;code&gt;oldData&lt;/code&gt;. The previous state of the component
+ can be used to tell exactly which properties changed to do more granular
+ updates.&lt;/p&gt;
+ &lt;p&gt;The update handler uses &lt;code&gt;this.data&lt;/code&gt; to modify the entity, usually interacting
+ with three.js APIs. One of the simplest update handlers is the
+ &lt;a href=&quot;https://aframe.io/docs/components/visible.html&quot;&gt;visible&lt;/a&gt; component's:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nx&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;el&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;object3D&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;visible&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;p&gt;A slightly more complex update handler might be the &lt;a href=&quot;https://aframe.io/docs/components/light.html&quot;&gt;light&lt;/a&gt; component's,
+ which we'll show via abbreviated code:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nx&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;oldData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;diffData&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;diff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;oldData&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{});&lt;/span&gt;
+
+ &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;light&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'type'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;diffData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;c1&quot;&gt;// If there is an existing light and the type hasn't changed, update light.&lt;/span&gt;
+ &lt;span class=&quot;nb&quot;&gt;Object&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;keys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;diffData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;light&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;diffData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;c1&quot;&gt;// No light exists yet or the type of light has changed, create a new light.&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;light&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getLight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
+
+ &lt;span class=&quot;c1&quot;&gt;// Register the object3D of type `light` to the entity.&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;el&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setObject3D&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'light'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;light&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;p&gt;The entity's &lt;code&gt;object3D&lt;/code&gt; is a plain THREE.Object3D. Other three.js object types
+ such as meshes, lights, and cameras can be set with &lt;code&gt;setObject3D&lt;/code&gt; where they
+ will be appeneded to the entity's &lt;code&gt;object3D&lt;/code&gt;.&lt;/p&gt;
+ &lt;h4&gt;Component.remove() - Tear Down&lt;/h4&gt;
+ &lt;p&gt;The &lt;code&gt;remove&lt;/code&gt; handler is called when the component detaches from the entity such
+ as with &lt;code&gt;removeAttribute&lt;/code&gt;. This is generally used to remove all modifications,
+ listeners, and behaviors to the entity that the component added.&lt;/p&gt;
+ &lt;p&gt;For example, when the &lt;a href=&quot;https://aframe.io/docs/components/light.html&quot;&gt;light component&lt;/a&gt; detaches, it removes the light
+ it previously attached from the entity and thus the scene:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nx&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;el&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;removeObject3D&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'light'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;h4&gt;Component.tick(time) - Background Behavior&lt;/h4&gt;
+ &lt;p&gt;The &lt;code&gt;tick&lt;/code&gt; handler is called on every single tick or render loop of the scene.
+ So expect it to run on the order of 60-120 times for second. The global uptime of
+ the scene in seconds is passed into the tick handler.&lt;/p&gt;
+ &lt;p&gt;For example, the &lt;a href=&quot;https://aframe.io/docs/components/look-at.html&quot;&gt;look-at&lt;/a&gt; component, which instructs an entity to
+ look at another target entity, uses the tick handler to update the rotation in
+ case the target entity changes its position:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nx&quot;&gt;tick&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;c1&quot;&gt;// target3D and vector are set from the update handler.&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;target3D&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;el&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;object3D&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lookAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;vector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setFromMatrixPosition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;target3D&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;matrixWorld&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;h4&gt;Component.pause() and Component.play() - Stop and Go&lt;/h4&gt;
+ &lt;p&gt;To support pause and play, just as with a video game or to toggle entities for
+ performance, components can implement &lt;code&gt;play&lt;/code&gt; and &lt;code&gt;pause&lt;/code&gt; handlers. These are
+ invoked when the component's entity runs its &lt;code&gt;play&lt;/code&gt; or &lt;code&gt;pause&lt;/code&gt; method. When an
+ entity plays or pauses, all of its child entities are also played or paused.&lt;/p&gt;
+ &lt;p&gt;Components should implement play or pause handlers if they register any
+ dynamic, asynchronous, or background behavior such as animations, event
+ listeners, or tick handlers.&lt;/p&gt;
+ &lt;p&gt;For example, the &lt;code&gt;look-controls&lt;/code&gt; component simply removes its event listeners
+ such that the camera does not move when the scene is paused, and it adds its
+ event listeners when the scene starts playing or is resumed:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nx&quot;&gt;pause&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;removeEventListeners&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
+
+ &lt;span class=&quot;nx&quot;&gt;play&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addEventListeners&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;h3&gt;Boilerplate&lt;/h3&gt;
+ &lt;p&gt;I suggest that people start off with my &lt;a href=&quot;https://github.com/ngokevin/aframe-component-boilerplate&quot;&gt;component boilerplate&lt;/a&gt;,
+ even hardcore tool junkies. This will get you straight into building a
+ component and comes with everything you will need to publish your component
+ into the wild. The boilerplate handles creating a stubbed component, build
+ steps for both NPM and browser distribution files, and publishing to Github
+ Pages.&lt;/p&gt;
+ &lt;p&gt;Generally with boilerplates, it is better to start from scratch and build your
+ own boilerplate, but the A-Frame component boilerplate contains a lot of tribal
+ inside knowledge about A-Frame and is updated frequently to reflect new things
+ landing on A-Frame. The only possibly opinionated pieces about the boilerplate
+ is the development tools it internally uses that are hidden away by NPM
+ scripts.&lt;/p&gt;
+ &lt;h3&gt;Examples&lt;/h3&gt;
+ &lt;p&gt;Under construction. Stay tuned!&lt;/p&gt;
+ &lt;h4&gt;Text Component&lt;/h4&gt;
+ &lt;p&gt;&lt;a href=&quot;https://github.com/ngokevin/aframe-text-component&quot;&gt;Text component&lt;/a&gt;&lt;/p&gt;
+ &lt;h4&gt;Physics Components&lt;/h4&gt;
+ &lt;p&gt;&lt;a href=&quot;https://github.com/ngokevin/aframe-physics-components&quot;&gt;Physics components&lt;/a&gt;&lt;/p&gt;
+ &lt;h4&gt;Layout Component&lt;/h4&gt;
+ &lt;p&gt;&lt;a href=&quot;https://github.com/ngokevin/aframe-layout-component&quot;&gt;Layout component&lt;/a&gt;&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-17T00:00:00+00:00</dc:date>
+ </item>
+ <item rdf:about="http://blog.gerv.net/?p=3527">
+ <title>Gervase Markham: Convenient… and Creepy</title>
+ <link>http://feedproxy.google.com/~r/HackingForChrist/~3/DN054t04_dE/</link>
+ <content:encoded>&lt;p&gt;The last Mozilla All-Hands was at one of the hotels in the Walt Disney World Resort in Florida. Every attendee was issued with one of these (although their use was optional):&lt;br /&gt;
+ &lt;a href=&quot;http://blog.gerv.net/files/2016/01/Disneys_MagicBand.jpg&quot;&gt;&lt;img class=&quot;alignnone size-large wp-image-3530&quot; src=&quot;http://blog.gerv.net/files/2016/01/Disneys_MagicBand-1024x832.jpg&quot; width=&quot;292&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
+ &lt;p&gt;It’s called a “Magic Bandâ€. You register it online and connect it to your Disney account, and then it can be used for park entry, entry to pre-booked rides so you don’t have to queue (called “FastPass+â€), payment, picking up photos, as your room key, and all sorts of other convenient features. Note that it has no UI whatsoever – no lights, no buttons. Not even a battery compartment. (It does contain a battery, but it’s not replaceable.) These are specific design decisions – the aim is for ultra-simple convenience.&lt;/p&gt;
+ &lt;p&gt;One of the talks we had at the All Hands was from one of the Magic Band team. The audience reactions to some of the things he said was really interesting. He gave the example of Cinderella wishing you a Happy Birthday as you walk round the park. “Cinderella just knowsâ€, he said. Of course, in fact, her costume’s tech prompts her when it silently reads your Magic Band from a distance. This got some initial impressed applause, but it was noticeable that after a few moments, it wavered – people were thinking “Cool… er, but creepy?â€&lt;/p&gt;
+ &lt;p&gt;The Magic Band also has range sufficient that Disney can track you around the park. This enables some features which are good for both customers and Disney – for example, they can use it for load balancing. If one area of the park seems to be getting overcrowded, have some characters pop up in a neighbouring area to try and draw people away. But it means that they always know where you are and where you’ve been.&lt;/p&gt;
+ &lt;p&gt;My take-away from learning about the Magic Band is that it’s really hard to have a technical solution to this kind of requirement which allows all the Convenient features but not the Creepy features. Disney does offer an RFID-card-based solution for the privacy-conscious which does some of these things, but not all of them. And it’s easier to lose. It seems to me that the only way to distinguish the two types of feature, and get one and not the other, is policy – either the policy of the organization, or external restrictions on them (e.g. from a watchdog body’s code of conduct they sign up to, or from law). And it’s often not in the organization’s interest to limit themselves in this way.&lt;/p&gt;
+ &lt;img alt=&quot;&quot; height=&quot;1&quot; src=&quot;http://feeds.feedburner.com/~r/HackingForChrist/~4/DN054t04_dE&quot; width=&quot;1&quot; /&gt;</content:encoded>
+ <dc:date>2016-01-16T12:18:38+00:00</dc:date>
+ <dc:creator>gerv</dc:creator>
+ </item>
+ <item rdf:about="https://www.christianheilmann.com/?p=4957">
+ <title>Christian Heilmann: Don’t tell me what my browser can’t do!</title>
+ <link>https://www.christianheilmann.com/2016/01/16/dont-tell-me-what-my-browser-cant-do/</link>
+ <content:encoded>&lt;p&gt;&lt;em class=&quot;markup--em markup--p-em&quot;&gt;Chances are, your guess is wrong!&lt;/em&gt;&lt;/p&gt;
+
+ &lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt=&quot;you are obviously in the wrong place&quot; src=&quot;https://d262ilb51hltx0.cloudfront.net/max/800/1*l9jPbOyAl00kjPhyNYA-IQ.jpeg&quot; width=&quot;100%&quot; /&gt;Arrogance towards possible customers never pays out – as shown in “Pretty Womanâ€&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;
+
+ &lt;p&gt;There is nothing more frustrating than being capable of something and not getting a chance to do it. The same goes for being blocked out from something although you are capable of consuming it. Or you’re even willing to put some extra effort or even money in and you still don’t get to consume it.&lt;/p&gt;
+
+ &lt;p&gt;For example, I’d happily pay $50 a month to get access to Netflix’s world-wide library from any country I’m in. But the companies Netflix get their content from won’t go for that. Movies and TV show are budgeted by predicted revenue in different geographical markets with month-long breaks in between the releases. A world-wide network capable of delivering content in real time? Preposterous — let’s shut that down.&lt;/p&gt;
+
+ &lt;p&gt;On a less “let’s break a 100 year old monopoly†scale of annoyance, &lt;a href=&quot;https://twitter.com/codepo8/status/687616620529844224&quot;&gt;I tweeted yesterday something glib and apparently cruel&lt;/a&gt;:&lt;/p&gt;
+
+ &lt;p&gt;&lt;/p&gt;&lt;blockquote&gt;“Sorry, but your browser does not support WebGL!†– sorry, you are a shit coder.&lt;/blockquote&gt;&lt;p&gt;&lt;/p&gt;
+
+ &lt;p&gt;&lt;strong&gt;And I stand by this&lt;/strong&gt;. I went to a web site that promised me some cute, pointless animation and technological demo. I was using Firefox Nightly — a WebGL capable browser. I also went there with Microsoft Edge — another WebGL capable browser. Finally, using Chrome, I was able to delight in seeing an animation.&lt;/p&gt;
+
+ &lt;p&gt;&lt;strong&gt;I’m not saying the creators of that thing lack in development capabilities&lt;/strong&gt;. The demo was slick, beautiful and well coded. They still do lack in two things developers of &lt;em&gt;web products &lt;/em&gt;(and I count apps into that) should have: empathy for the end user and an understanding that they are not in control.&lt;/p&gt;
+
+ &lt;p&gt;Now, I am a pretty capable technical person. When you tell me that I might be lacking WebGL, I know what you mean. I don’t lack WebGL. I was blocked out because the web site did browser sniffing instead of capability testing. But I know what could be the problem.&lt;/p&gt;
+
+ &lt;p&gt;A normal user of the web has no idea what WebGL is and — if you’re lucky — will try to find it on an app store. If you’re not lucky all you did is confuse a person. A person who went through the effort to click a link, open a browser and wait for your thing to load. A person that feels stupid for using your product as they have no clue what WebGL is and won’t ask. Humans hate feeling stupid and we do anything not to appear it or show it.&lt;/p&gt;
+
+ &lt;p&gt;This is what I mean by empathy for the end user. Our problems should never become theirs.&lt;/p&gt;
+
+ &lt;p&gt;&lt;/p&gt;&lt;blockquote&gt;A cryptic error message telling the user that they lack some technology helps nobody and is sloppy development at best, sheer arrogance at worst.&lt;/blockquote&gt;&lt;p&gt;&lt;/p&gt;
+
+ &lt;p&gt;The web is, sadly enough, littered with unhelpful error messages and assumptions that it is the user’s fault when they can’t consume the thing we built.&lt;/p&gt;
+
+ &lt;p&gt;Here’s a reality check — this is what our users should have to do to consume the things we build:&lt;/p&gt;
+
+ &lt;p&gt;&lt;img alt=&quot;&quot; height=&quot;600&quot; src=&quot;https://d262ilb51hltx0.cloudfront.net/max/800/1*DXtRIWTu-UzRb0YB-h8SmA.png&quot; width=&quot;10&quot; /&gt;&lt;/p&gt;
+
+ &lt;p&gt;&lt;strong&gt;That’s right. Nothing&lt;/strong&gt;. This is the web. Everybody is invited to consume, contribute and create. This is what made it the success it is. This is what will make it outlive whatever other platform threatens it with shiny impressive interactions. Interactions at that time impossible to achieve with web technologies.&lt;/p&gt;
+
+ &lt;p&gt;Whenever I mention this, the knee-jerk reaction is the same:&lt;/p&gt;
+
+ &lt;p&gt;&lt;/p&gt;&lt;blockquote class=&quot;graf--blockquote graf-after--p&quot; id=&quot;79d6&quot; name=&quot;79d6&quot;&gt;How can you expect us to build delightful experiences close to magic (and whatever other soundbites were in the last Apple keynote) if we keep having to support old browsers and users with terrible setups?&lt;/blockquote&gt;&lt;p&gt;&lt;/p&gt;
+
+ &lt;p&gt;You don’t have to support old browsers and terrible setups. But you are not allowed to block them out. It is a simple matter of giving a usable interface to end users. A button that does nothing when you click it is not a good experience. Test if the functionality is available, then create or show the button. &lt;strong class=&quot;markup--strong markup--p-strong&quot;&gt;This is as simple as it is.&lt;/strong&gt;&lt;/p&gt;
+
+ &lt;p&gt;If you really have to rely on some technology then show people what they are missing out on and tell them how to upgrade. A screenshot or a video of a WebGL animation is still lovely to see. A message telling me I have no WebGL less so.&lt;/p&gt;
+
+ &lt;p&gt;Even more on the black and white scale, what the discussion boils down to is in essence:&lt;/p&gt;
+
+ &lt;p&gt;&lt;/p&gt;&lt;blockquote class=&quot;graf--blockquote graf-after--p&quot; id=&quot;a775&quot; name=&quot;a775&quot;&gt;But it is 2016 — surely we can expect people to have JavaScript enabled — it is after all “the assembly language of the webâ€&lt;/blockquote&gt;&lt;p&gt;&lt;/p&gt;
+
+ &lt;p&gt;Despite the cringe-worthy &lt;a href=&quot;http://www.hanselman.com/blog/JavaScriptIsAssemblyLanguageForTheWebSematicMarkupIsDeadCleanVsMachinecodedHTML.aspx&quot;&gt;misquote of the assembly language&lt;/a&gt; thing, here is a harsh truth:&lt;/p&gt;
+
+ &lt;p&gt;&lt;/p&gt;&lt;blockquote&gt;You can absolutely expect JavaScript to be available on your end users computers in 2016. At the same time it is painfully &lt;strong&gt;naive&lt;/strong&gt; to expect it to work under all circumstances.&lt;/blockquote&gt;&lt;p&gt;&lt;/p&gt;
+
+ &lt;p&gt;&lt;strong&gt;JavaScript is brittle&lt;/strong&gt;. &lt;span class=&quot;caps&quot;&gt;HTML&lt;/span&gt; and &lt;span class=&quot;caps&quot;&gt;CSS&lt;/span&gt; both are &lt;em&gt;fault tolerant&lt;/em&gt;. If something goes wrong in &lt;span class=&quot;caps&quot;&gt;HTML&lt;/span&gt;, browsers either display the content of the element or try to fix minor issues like unclosed elements for you. &lt;span class=&quot;caps&quot;&gt;CSS&lt;/span&gt; skips lines of code it can’t understand and merrily goes on its way to show the rest of it. JavaScript breaks on errors and tells you that something went wrong. It will not execute the rest of the script, but throws in the towel and tells you to get your house in order first.&lt;/p&gt;
+
+ &lt;p&gt;There &lt;a href=&quot;http://kryogenix.org/code/browser/everyonehasjs.html&quot;&gt;are many outside influences&lt;/a&gt; that will interfere with the execution of your JavaScript. That’s why a non-naive and non-arrogant — a dedicated and seasoned web developer — will never rely on it. Instead, you treat it as an enhancement and in an almost paranoid fashion test for the availability of everything before you access it.&lt;/p&gt;
+
+ &lt;p&gt;&lt;strong&gt;Sorry (not sorry) — this will never go away&lt;/strong&gt;. This is the nature of JavaScript. And it is a good thing. It means we can access new features of the language as they come along instead of getting stuck in a certain state. It means we have to think about using it every time instead of relying on libraries to do the work for us. It means that we need to keep evolving with the web — a living and constantly changing medium, and not a software platform. That’s just part of it.&lt;/p&gt;
+
+ &lt;p&gt;This is why the whole discussion about JavaScript enabled or disabled is a massive waste of time. It is not the availability of JavaScript we need to worry about. It is our products breaking in perfectly capable environments because we rely on perfect execution instead of writing defensive code. A tumblr like &lt;a class=&quot;markup--anchor markup--p-anchor&quot; href=&quot;http://sighjavascript.tumblr.com/&quot; rel=&quot;nofollow&quot;&gt;Sigh, JavaScript&lt;/a&gt; is fun, but is pithy finger-pointing.&lt;/p&gt;
+
+ &lt;p&gt;&lt;/p&gt;&lt;blockquote&gt;There is nothing wrong with using JavaScript to build things. Just be aware that the error handling is your responsibility.&lt;/blockquote&gt;&lt;p&gt;&lt;/p&gt;
+
+ &lt;p&gt;Any message telling the user that they have to turn on JavaScript to use a certain product is a proof that you care more about your developer convenience than your users.&lt;/p&gt;
+
+ &lt;p&gt;&lt;/p&gt;&lt;blockquote&gt;It is damn hard these days to turn off JavaScript – you are complaining about a almost non-existent issue and tell the confused user to do something they don’t know how to.&lt;/blockquote&gt;&lt;p&gt;&lt;/p&gt;
+
+ &lt;p&gt;The chance that something in the JavaScript execution of any of your dozens of dependencies went wrong is much higher – and this is your job to fix. This is why advice like &lt;a href=&quot;http://webdesign.tutsplus.com/tutorials/quick-tip-dont-forget-the-noscript-element--cms-25498&quot;&gt;using noscript to provide alternative content&lt;/a&gt; is terrible. It means you double your workload instead of enhancing what works. Who knows? If you start with something not JavaScript dependent (or running it server side) you might find that you don’t need the complex solution you started with in the first place. Faster, smaller, easier. Sounds good, right?&lt;/p&gt;
+
+ &lt;p&gt;So, please, stop sniffing my browser, you will fail and tell me lies. Stop pretending that working with a brittle technology is the user’s fault when something goes wrong.&lt;/p&gt;
+
+ &lt;p&gt;&lt;/p&gt;&lt;blockquote&gt;As web developers we work in the service industry. We deliver products to people. And keeping these people happy and non-worried is our job. Nothing more, nothing less.&lt;/blockquote&gt;&lt;p&gt;&lt;/p&gt;
+
+ &lt;p&gt;Without users, your product is nothing. Sure, we are better paid and well educated and we are not flipping burgers. But we have no right whatsoever to be arrogant and not understanding that our mistakes are not the fault of our end users.&lt;/p&gt;
+
+ &lt;p&gt;Our demeanor when complaining about how stupid our end users and their terrible setups are reminds me of &lt;a href=&quot;https://www.youtube.com/watch?v=CSj5stmFkQ0&quot;&gt;this Mitchell and Webb sketch&lt;/a&gt;.&lt;/p&gt;
+
+ &lt;p&gt;&lt;/p&gt;
+
+ &lt;p&gt;&lt;strong class=&quot;markup--strong markup--p-strong&quot;&gt;Don’t be that person. &lt;/strong&gt;Our job is to enable people to consume, participate and create the web. This is magic. This is beautiful. This is incredibly rewarding. The next markets we should care about are ready to be as excited about the web as we were when we first encountered it. Browsers are good these days. Use what they offer after testing for it and enjoy what you can achieve. Don’t tell the user when things go wrong – they can not fix what you messed up.&lt;/p&gt;
+
+
+ &lt;img alt=&quot;&quot; height=&quot;1&quot; src=&quot;http://feeds.feedburner.com/~r/chrisheilmann/~4/vqtqgcNQXy8&quot; width=&quot;1&quot; /&gt;</content:encoded>
+ <dc:date>2016-01-16T11:28:10+00:00</dc:date>
+ <dc:creator>Chris Heilmann</dc:creator>
+ </item>
+ <item rdf:about="http://glandium.org/blog/?p=3510">
+ <title>Mike Hommey: Announcing git-cinnabar 0.3.1</title>
+ <link>http://glandium.org/blog/?p=3510</link>
+ <content:encoded>&lt;p&gt;This is a brown paper bag release. It turns out I managed to break the upgrade&lt;br /&gt;
+ path only 10 commits before the release.&lt;/p&gt;
+ &lt;h3&gt;What’s new since 0.3.0?&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;code&gt;git cinnabar fsck&lt;/code&gt; doesn’t fail to upgrade metadata.&lt;/li&gt;
+ &lt;li&gt;The &lt;code&gt;remote.$remote.cinnabar-draft&lt;/code&gt; config works again.&lt;/li&gt;
+ &lt;li&gt;Don’t fail to clone an empty repository.&lt;/li&gt;
+ &lt;li&gt;Allow to specify mercurial configuration items in a .git/hgrc file.&lt;/li&gt;
+ &lt;/ul&gt;</content:encoded>
+ <dc:date>2016-01-16T11:26:45+00:00</dc:date>
+ <dc:creator>glandium</dc:creator>
+ </item>
+ <item rdf:about="http://edunham.net/2016/01/16/buildbot_and_eoferror.html">
+ <title>Emily Dunham: Buildbot and EOFError</title>
+ <link>http://edunham.net/2016/01/16/buildbot_and_eoferror.html</link>
+ <content:encoded>&lt;h3&gt;Buildbot and EOFError&lt;/h3&gt;
+ &lt;p&gt;More SEO-bait, after tracking down an poorly documented problem:&lt;/p&gt;
+ &lt;div class=&quot;highlight-python&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;# buildbot start master
+ Following twistd.log until startup finished..
+ 2016-01-17 04:35:49+0000 [-] Log opened.
+ 2016-01-17 04:35:49+0000 [-] twistd 14.0.2 (/usr/bin/python 2.7.6) starting up.
+ 2016-01-17 04:35:49+0000 [-] reactor class: twisted.internet.epollreactor.EPollReactor.
+ 2016-01-17 04:35:49+0000 [-] Starting BuildMaster -- buildbot.version: 0.8.12
+ 2016-01-17 04:35:49+0000 [-] Loading configuration from '/home/user/buildbot/master/master.cfg'
+ 2016-01-17 04:35:53+0000 [-] error while parsing config file:
+ Traceback (most recent call last):
+ File &quot;/usr/local/lib/python2.7/dist-packages/twisted/internet/defer.py&quot;, line 577, in _runCallbacks
+ current.result = callback(current.result, *args, **kw)
+ File &quot;/usr/local/lib/python2.7/dist-packages/twisted/internet/defer.py&quot;, line 1155, in gotResult
+ _inlineCallbacks(r, g, deferred)
+ File &quot;/usr/local/lib/python2.7/dist-packages/twisted/internet/defer.py&quot;, line 1099, in _inlineCallbacks
+ result = g.send(result)
+ File &quot;/usr/local/lib/python2.7/dist-packages/buildbot/master.py&quot;, line 189, in startService
+ self.configFileName)
+ --- &amp;lt;exception caught here&amp;gt; ---
+ File &quot;/usr/local/lib/python2.7/dist-packages/buildbot/config.py&quot;, line 156, in loadConfig
+ exec f in localDict
+ File &quot;/home/user/buildbot/master/master.cfg&quot;, line 415, in &amp;lt;module&amp;gt;
+ extra_post_params={'secret': HOMU_BUILDBOT_SECRET},
+ File &quot;/usr/local/lib/python2.7/dist-packages/buildbot/status/status_push.py&quot;, line 404, in __init__
+ secondaryQueue=DiskQueue(path, maxItems=maxDiskItems))
+ File &quot;/usr/local/lib/python2.7/dist-packages/buildbot/status/persistent_queue.py&quot;, line 286, in __init__
+ self.secondaryQueue.popChunk(self.primaryQueue.maxItems()))
+ File &quot;/usr/local/lib/python2.7/dist-packages/buildbot/status/persistent_queue.py&quot;, line 208, in popChunk
+ ret.append(self.unpickleFn(ReadFile(path)))
+ exceptions.EOFError:
+
+ 2016-01-17 04:35:53+0000 [-] Configuration Errors:
+ 2016-01-17 04:35:53+0000 [-] error while parsing config file: (traceback in logfile)
+ 2016-01-17 04:35:53+0000 [-] Halting master.
+ 2016-01-17 04:35:53+0000 [-] Main loop terminated.
+ 2016-01-17 04:35:53+0000 [-] Server Shut Down.
+ &lt;/pre&gt;&lt;/div&gt;
+ &lt;/div&gt;
+ &lt;p&gt;This happened after the buildmaster’s disk filled up and a bunch of stuff was
+ manually deleted. There were no changes to master.cfg since it worked
+ perfectly.&lt;/p&gt;
+ &lt;p&gt;The fix was to examine &lt;span class=&quot;docutils literal&quot;&gt;&lt;span class=&quot;pre&quot;&gt;master.cfg&lt;/span&gt;&lt;/span&gt; to see &lt;a class=&quot;reference external&quot; href=&quot;https://github.com/servo/saltfs/blob/master/buildbot/master/master.cfg#L413&quot;&gt;where the HttpStatusPush was
+ created&lt;/a&gt;,
+ of the form:&lt;/p&gt;
+ &lt;div class=&quot;highlight-python&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'status'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;HttpStatusPush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
+ &lt;span class=&quot;n&quot;&gt;serverUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'http://build.servo.org:54856/buildbot'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
+ &lt;span class=&quot;n&quot;&gt;extra_post_params&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'secret'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HOMU_BUILDBOT_SECRET&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+ &lt;/div&gt;
+ &lt;p&gt;Digging in the Buildbot source reveals that &lt;span class=&quot;docutils literal&quot;&gt;&lt;span class=&quot;pre&quot;&gt;persistent_queue.py&lt;/span&gt;&lt;/span&gt; wants to
+ unpickle a cache file from &lt;span class=&quot;docutils literal&quot;&gt;&lt;span class=&quot;pre&quot;&gt;/events_build.servo.org/-1&lt;/span&gt;&lt;/span&gt; if there was nothing
+ in &lt;span class=&quot;docutils literal&quot;&gt;&lt;span class=&quot;pre&quot;&gt;/events_build.servo.org/&lt;/span&gt;&lt;/span&gt;. To fix this the right way, create that file
+ and make sure Buildbot has &lt;span class=&quot;docutils literal&quot;&gt;&lt;span class=&quot;pre&quot;&gt;+rwx&lt;/span&gt;&lt;/span&gt; on it.&lt;/p&gt;
+ &lt;p&gt;Alternately, you can give up on writing your status push cache to disk
+ entirely by adding the line &lt;span class=&quot;docutils literal&quot;&gt;&lt;span class=&quot;pre&quot;&gt;maxDiskItems=0&lt;/span&gt;&lt;/span&gt; to the creation of the
+ HttpStatusPush, giving you:&lt;/p&gt;
+ &lt;div class=&quot;highlight-python&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'status'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;HttpStatusPush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
+ &lt;span class=&quot;n&quot;&gt;serverUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'http://build.servo.org:54856/buildbot'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
+ &lt;span class=&quot;n&quot;&gt;maxDiskItems&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
+ &lt;span class=&quot;n&quot;&gt;extra_post_params&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'secret'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HOMU_BUILDBOT_SECRET&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+ &lt;/div&gt;
+ &lt;p&gt;The real moral of the story is “remember to use &lt;a class=&quot;reference external&quot; href=&quot;http://www.linuxcommand.org/man_pages/logrotate8.html&quot;&gt;logrotate&lt;/a&gt;.&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-16T08:00:00+00:00</dc:date>
+ </item>
+ <item rdf:about="urn:md5:41d039bb28fb15c761578cba0b1454fa">
+ <title>Daniel Glazman: Ebook pagination and CSS</title>
+ <link>http://www.glazman.org/weblog/dotclear/index.php?post/2016/01/16/Ebook-pagination-and-CSS</link>
+ <content:encoded>&lt;p&gt;Let's suppose you have a rather long document, for instance a book chapter, and you want to render it in your browser &lt;em&gt;à la&lt;/em&gt; iBooks/Kindle. That's rather easy with just a dash of CSS:&lt;/p&gt;
+ &lt;pre&gt;body {
+ height: calc(100vh - 24px);
+ column-width: 45vw;
+ overflow: hidden;
+ margin-left: calc(-50vw * attr(currentpage integer));
+ }&lt;/pre&gt;
+ &lt;p&gt;Yes, yes, I know that no browser implements that &lt;code&gt;attr()&lt;/code&gt;extended syntax. So put an inline style on your body for &lt;code&gt;margin-left: calc(-50vw * &lt;em&gt;&amp;lt;n&amp;gt;&lt;/em&gt;)&lt;/code&gt; where &lt;em&gt;&lt;code&gt;&amp;lt;n&amp;gt;&lt;/code&gt;&lt;/em&gt; is the page number you want minus 1.&lt;/p&gt;
+ &lt;p&gt;Then add the fixed positioned controls you need to let user change page, plus gesture detection. Add a transition on margin-left to make it nicer. Done. Works perfectly in Firefox, Safari, Chrome and Opera. I don't have a Windows box handy so I can't test on Edge.&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-16T03:43:00+00:00</dc:date>
+ <dc:creator>glazou</dc:creator>
+ </item>
+ <item rdf:about="https://repeer.org/?p=48">
+ <title>Nicolas Mandil: Mozilla cultural revolution: from ‘radical participation’ to ‘radical user-centric’</title>
+ <link>https://repeer.org/2016/01/16/mozilla-cultural-revolution-from-radical-participation-to-radical-user-centric/</link>
+ <content:encoded>&lt;p&gt;This post has been written about the &lt;a href=&quot;http://marksurman.commons.ca/2015/12/21/mofo2020/&quot;&gt;Mozilla Foundation (MoFo) 2020 strategy&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;The ideas developed in this post are in different levels: some are global, some focus on particular points of the proposed draft. But in my point of view, they all carry a transversal meaning: articulation (as piece connected to a structure allowing movement) with others and consistency with our mission.&lt;/p&gt;
+ &lt;h3&gt;Summary&lt;/h3&gt;
+ &lt;p&gt;On the way to &lt;a href=&quot;http://marksurman.commons.ca/2015/01/09/what-is-radical-participation/&quot;&gt;radical participation&lt;/a&gt;, Mozilla should be radical &lt;sup class=&quot;footnote&quot;&gt;&lt;a href=&quot;https://repeer.org/tag/mozilla/feed/#fn-48-1&quot; id=&quot;fnref-48-1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; user-centric. Mozilla should not go against the social understanding of the (tech and whole society) situation because it’s what is massively shared and what polarizes the prism of understanding of the society. &lt;strong&gt;We should built solutions for it and transform (develop and change) it on the way. Our responsibility is to build &lt;em&gt;inclusivity&lt;/em&gt; (inclusion strengths) everywhere, to gather for multiplying our impact.&lt;/strong&gt; We must build (progressive) victories instead of battles (of static positions and postures).&lt;br /&gt;
+ If we don’t do it, we go against users self-perceived need: use. We value our differences more than our commonalities and &lt;strong&gt;consider ethic more as an absolute objective than a concrete process&lt;/strong&gt;: we divide, separate, compete. Our solutions get irrelevant, we get rejected and marginalized, we reject compromises that improve the current situation for the ideal, we loose influence and therefore impact on the definition of the present and future. We already done it for the good and the bad in the past (H.264+Daala, pocket integration, Hello login, no Firefox for iOS, Google fishing vs Disconnect, FxOS Notes app which sync is evernote only, …).&lt;br /&gt;
+ To get a consistent and impactful ability to integrate and transform the social understanding, there are four domains where we can take and articulate (connected structure allowing movement) action:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;strong&gt;People&lt;/strong&gt;: identity is the key to grow consciousness, understanding, skills, voice, representation and to articulate global/local, personal/common. &lt;strong&gt;[Activate]&lt;/strong&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;strong&gt;Technology&lt;/strong&gt;: universality is key for a platform (for resilience) with interfaces (for modularity) where services, features and front-ends can plug-in and communicate to provide (inter)active support ; Decouple conditions of fulfillment with execution (content/appearance/policy ; material/immaterial) to support remix (policy continuity, consistency thought providers, …). &lt;strong&gt;[Unlock]&lt;/strong&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;strong&gt;Product&lt;/strong&gt;: persona and (current and emerging) use via user-agents are the keys. Be on all major platforms depending on use, ethical alignment and opportunities, emerging newness to provide continuity (task, device) to users and leading on new practices. Features should be about products parity and opening new possibilities carrying our values to the action at a massive scale. &lt;strong&gt;[Build]&lt;/strong&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;strong&gt;Organizations/institutions&lt;/strong&gt;: sociological innovation for participation is the key. Research on historical (evolution) and sociological (human organizations, social institutions and social behaviors) analysis based on social networks (link as social interactions), in the perspective of producing commons. &lt;strong&gt;[Drive]&lt;/strong&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;Our front has two sides: &lt;strong&gt;propose and protect&lt;/strong&gt;. But each of them are connected and can have different strategic expressions, if our actions generate improving (progressive) curves:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;For the &lt;strong&gt;action taking&lt;/strong&gt;: consciousness, understanding, symbolic actions, behavior change, behavior advocacy (evangelism)&lt;/li&gt;
+ &lt;li&gt;For the &lt;strong&gt;action mode&lt;/strong&gt;: promotion (spreading the idea), incitement (giving a competitive advantage to people involved), collaboration (open interactions to make a win-win exchange; process-centric), contractualization (formalize domains where a win-win exchange is made; object-centric), coercion (giving a competitive disadvantage to people not involved).&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;Social history is a history of social values.&lt;strong&gt; The way we understand and tell the problem determine the solution we can create&lt;/strong&gt;: we need, all the way long, a shared understanding. Tools and technologies are not tied, bound forever to their social value, which depends on people’s social representations that evolve over time.&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;strong&gt;The social behavior&lt;/strong&gt; is a first key. It is the narrative, and therefore its &lt;strong&gt;inclusion in the social history that we make, which converges the product with the values that it stands for&lt;/strong&gt;. Here is the articulation of product with people and technology, of product with leadership network and advocacy engine (it could be less persistent and inclusive: marketing).&lt;/li&gt;
+ &lt;li&gt;&lt;strong&gt;The social organization&lt;/strong&gt; is a second key. It is about how the process, the tools, the architecture, the governance and the opportunities/constraints have changed for Mozilla (org) and Mozillians (people). &lt;strong&gt;Here comes the question of being open&lt;/strong&gt;. It is not enough because it is about availability (passive) and not inclusivity (active). The high level of automation coming is a challenge. We should level-up the meaning to differentiate from others: &lt;strong&gt;Mozilla should activate and unlock societal progress to build fair technical progress&lt;/strong&gt;. Mozilla need to &lt;strong&gt;identify its resilient backbone&lt;/strong&gt; (not only a technology, the web, but something that articulate people, technology and products) and make it more universal (through people and products). But our goals can’t be absolutely achieved because they have to be considered in a dynamic context. However, the brand engagement is persistent, if it’s included in the product, visible, and centered on easing the user’s action.&lt;br /&gt;
+ Linked to the ‘being open’ question, the advocacy engine could be a thing to unlock societal progress. People are satisfied of narrow hills of choice until they understand it’s not socially neutral. It’s the case with technology: they accept things about technology to be build top-down. &lt;strong&gt;A successful advocacy, even one about technology, is always built bottom-up&lt;/strong&gt;, as its function is to give back the voice to the people, to get them involved, not to make them fulfill our predefined aims. The top-down method is too organization centric and administrative content centric: it can’t massively drive people that are not already committed to the org. It’s usually named advertisement or propaganda. &lt;strong&gt;If we want to have an impact, we should listen to people needs, not tell them to listen to ours&lt;/strong&gt;. People want (first) to be empowered, not to empower an org. We need to have content and user centric (not org and it’s process) tools/platform for advocates and leaders: let’s build the technology advocacy plan together. Yes it’s slower, but much more massive, inclusive and persistent. The impact will be higher because it will carry a meaning for people and it wont be too org centric. So it will be qualitatively better: not just an amount, &lt;strong&gt;accumulation is not our goal, but impact, that comes from articulation&lt;/strong&gt;. Likewise we should be careful to not use best practice as absolute solutions, but as solutions in a context, if we want to transpose them massively: when we unify we should avoid to homogenize. On the narrative side, our preoccupation should be about building short, medium and long term narrative to get action.&lt;/li&gt;
+ &lt;li&gt;&lt;strong&gt;The social institutions&lt;/strong&gt; are the third key. Here is the articulation of the leadership network with the advocacy engine. &lt;strong&gt;Leaders build new solutions (products) and Advocates new voices (rallying), they are both about personal development and empower commons.&lt;/strong&gt; Leadership=learn+create and advocacy=teach+spread commons. Leaders are projects/orgs leaders, the ones that traduce DNA (values) in products (concrete ability and availability). Advocates are values advocates, the ones that traduce DNA (values) in actions (behavior). As they are both targeting commons, they both produce the same social organization (collaboration instead of competition). They are both involved in creating (different) representations (institutions) and organizations (foundation/firms) but &lt;strong&gt;with a different DNA (values) processing&lt;/strong&gt;: from public good to personal benefit or from personal interest to public benefit. If Mozilla cares about public good resilience, &lt;strong&gt;the articulation of their domains of values is critical&lt;/strong&gt;. So, on the social organization side, their articulation’s expression and the revision process must be said and clear: from hierarchy or contract or different autonomy levels (internal incubation and external advocacy), or … to criteria to start a revision. About the narrative, and hence about the social behavior side, leaders carry a lot of legitimacy and avoid the stay-experimental or non-massive (unique) thoughts. And we need legitimacy to get impact. But this legitimacy is already present if we&lt;strong&gt; make clear that our actions are about commons&lt;/strong&gt;. We should name them creators (compositors or managers) to make it clear that the creative process is a collaboration, made by a team and that the public good do not have the same role in the process and outcome. Full circle.&lt;/li&gt;
+ &lt;li&gt;&lt;strong&gt;The social networks&lt;/strong&gt; are the keystone. Let’s shortly take an example based on social networks (link as social interactions) with the perspective of producing people, technological and product commons. &lt;strong&gt;We need better tools for collaboration and participation&lt;/strong&gt;: tools that merge discussion channels, capitalize on the discussion and preview the results to build a plan. From evolving the wiki discussion page to feature document production into peer-to-peer discussion.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;An analysis of the creation process is another way to the articulation of product with people and technology.&lt;br /&gt;
+ Platforms move closer to strict ‘walled garden’ ecosystems. We need bridges from lab to home that carry different mix of customization and reliability to support the emancipation curve. We need to build pathways thought audiences and thought IT layers (content, software, hardware, distant service). &lt;strong&gt;We should find a convergence between customization&lt;/strong&gt; (dev code patch to users add-ons) &lt;strong&gt;and reliability&lt;/strong&gt; (self made to mass product), &lt;strong&gt;between first time experience, support and add-ons thought all our users’ persona by building bridges, pathways&lt;/strong&gt;. Mozilla should find ways to &lt;strong&gt;integrate learning&lt;/strong&gt; in its products, in-content, as we have code comment on code: on-boarding levels, progression from simple to high level techniques, reproducible/universal next task/skill building.&lt;/p&gt;
+ &lt;h3&gt;Detailed discussion content&lt;/h3&gt;
+ &lt;p&gt;Here are the developed ideas, with more reference to our allies and detractors’ products.&lt;/p&gt;
+ &lt;h4&gt;People, the sociological side&lt;/h4&gt;
+ &lt;h5&gt;From focused to systemic action&lt;/h5&gt;
+ &lt;p&gt;First of all, I think &lt;strong&gt;the strategy move Mozilla is doing is the right one&lt;/strong&gt; as it embraces more our real life. People are not defined by one characteristic, we are complex: ex. we can be pedestrian, car driver, biker, Public Transport user… we think and do simultaneously. So why Mozilla should restrict its strategy by targeting people on skills, through education, thought better material only (the Mozilla Academy program). Education, even popular education, can’t do everything for the people to build change. &lt;strong&gt;We need a plan that balance intellectual and practical (abstraction/action, think/do) integrating progressive paths to massively scale so we get an impact: build change.&lt;/strong&gt;&lt;/p&gt;
+ &lt;h5&gt;Real life: Social history, individuals and institutions as an articulation founding the action.&lt;/h5&gt;
+ &lt;p&gt;Let’s start by some definitions based on my understanding of some &lt;a href=&quot;https://fr.wikipedia.org/wiki/Sociologie&quot;&gt;Wikipedia articles&lt;/a&gt;. Sociology is the study of the evolution of societies: human organizations and social institutions. It is about &lt;strong&gt;the impact of the social dimension on humans representations (ways of thinking) and behaviors (ways of acting)&lt;/strong&gt;. It allows to study the conceptions of social relations according to fundamental criteria (structuralism, functionalism, conventionalism, etc.) and the hooks to reality (interactionism, institutionalism, regulationisme, actionism, etc.), to think and shape the modernity. Currently (and this is key for Mozilla’s positioning), the combination of models replace the models’ unity, which aims to assume the multidimensionality. There are three major sociological paradigms, including one emerging:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;strong&gt;The holistic paradigm&lt;/strong&gt;: Society is a whole that is greater than the sum of its parts, it exists before the individual and individuals are governed by it. In this context, the Society includes the individual and the individual consciousness is seen only as a fragment of the collective consciousness. The emphasis is on the social fact, whose cause must be sought in earlier social facts. The social fact is part of a system of interlocking institutions that govern individuals. It is external to the individual and constraint it. Sociology is then the science of institutional invariants in which are the observable phenomenas.&lt;/li&gt;
+ &lt;li&gt;&lt;strong&gt;The atomistic paradigm&lt;/strong&gt;: each individual is a social atom. The atoms act according to self motives, interests, emotions and are linked to other atoms. A system of constant interaction between atoms produces and reproduces Society. The emphasis is on the cause of social actions and the meaning given by individuals to their actions. A horizon of meanings serve as reference instead of the arrangements of institutions. The institution is there but it serves the motives and interests of agents. Sociology is then the study of the social action.&lt;/li&gt;
+ &lt;li&gt;The recent emergence of a sociological analysis based on &lt;strong&gt;social networks&lt;/strong&gt; (which are a collection of individuals or organizations connected by regular social interactions) suggest lines of research &lt;strong&gt;beyond the opposition between the holistic and the atomistic approaches&lt;/strong&gt;. The theory of social networks conceives social relationships in terms of nodes and links. The nodes are usually social actors in the network but can also represent institutions, and links are the relationships between these nodes. There may be several kinds of links between nodes and their analysis determines social capital of the social actors.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;Consequently, Mozilla should build its strategy on &lt;strong&gt;historical&lt;/strong&gt; (evolution) and &lt;strong&gt;sociological&lt;/strong&gt; (human organizations, social institutions and social behaviors) analysis based on &lt;strong&gt;social networks&lt;/strong&gt; (links as social interactions), in the perspective of producing &lt;strong&gt;commons&lt;/strong&gt;. That is to say as an &lt;strong&gt;engine of transition from a model of value&lt;/strong&gt; on its last leg (rarity capitalism) to the emerging one (new articulation of the individual and the collective: commons).&lt;br /&gt;
+ It is important and strategic to propose a sociological articulation supporting our mission and its purpose (commons) since &lt;strong&gt;the sociological concept (the paradigm) reveals an ideological characteristic&lt;/strong&gt;: because it participates in societal movements made in the Society, it serves an ideal. The societal domain, what’s making society, a political object, should be a stake for Mozilla.&lt;/p&gt;
+ &lt;h5&gt;Build on a basement: current tech challenge articulated with current social meaning/perception&lt;/h5&gt;
+ &lt;p&gt;&lt;strong&gt;We should articulate ‘our real life’ with the nowadays tech challenge&lt;/strong&gt;: how to get back control over our data at the time of IoT, cloud, big data, convergence (multi-devices/form factor)? From a user point of view, we have devices and want them convenient, easy and nice. The big moves in the tech industry (IoT, cloud, big data, convergence) free us for somethings and lock us for others. The lock key is that our devices don’t compute anymore our data that are in silos. From a developer point of view, the innovation is going very fast and it’s hard to have a complete open source toolbox that we can share, mostly because we don’t lead: Open has turn to be more open-releasing.&lt;br /&gt;
+ We should articulate our new strategy with the tech industry moves: for example, &lt;strong&gt;as a user, how can I get (email) encryption on all my devices?&lt;/strong&gt; Should I follow (fragmented) different kind of howtos/tools/apps to achieve that? How do I know these are consistent together? How can I be sure it won’t brake my continuous workflow? (app silo? social silo? level of trust and reliability?)&lt;br /&gt;
+ Mozilla have the skills to answer this as we already faced and solved some of these issues on particular points: like how to ease the installation of Firefox for Android for Firefox desktop users, open and discoverable choice of search engines, synchronization across devices, …&lt;br /&gt;
+ &lt;strong&gt;Mozilla’s challenge is to not be marginalized by the change of practices. Having an impact is embracing the new practice and give it an alternative.&lt;/strong&gt; Mozilla already made that move by saying « &lt;em&gt;Firefox will go where users are&lt;/em&gt;« , by trying to balance the advertisement practice between adds companies and users, by integrating H.264 and developing Daala. But &lt;strong&gt;Mozilla never stated that clearly as a strategy&lt;/strong&gt;.&lt;/p&gt;
+ &lt;h5&gt;A backbone to make our mission resilient in it expressions&lt;/h5&gt;
+ &lt;p&gt;If we think about the &lt;strong&gt;Facebook’s strategy, they first built a network of people whiling to share&lt;/strong&gt; (no matter what they share) and then use this &lt;strong&gt;transversal backbone to power vertical business segments&lt;/strong&gt; (search, donation, local market selling, …). Google with its search engine and its open source policy have a similar (in a way) strategy. The difference here is that the backbone is people’s data and control over digital formats. In both cases, the level of use (of the social network, search engine, mobile OS, …) is the key (with fast innovation) to have an impact. And that’s a major obstacle to build successful alternatives.&lt;br /&gt;
+ The proposed Mozilla’s strategy is built in the opposite way, and that’s questioning. &lt;strong&gt;We try to build people network depending on some shared matters&lt;/strong&gt;. Then, is our strategy able to scale enough to compete against GAFAM, or are we trying to build a third way ?&lt;br /&gt;
+ For the products, the Mozilla’s strategy is still (and has always been) inclusive: everybody can use the product and then benefit of its open web values. A good product that answer people needs, plus giving people back/new power (allow new use) build a big community. For the network, should we build our global force of people based on concentric circles (of shared matters) or based on a (Mozilla own) transversal backbone (matter agnostic)? It seems to me the actual presentation of the strategy do not answer clearly enough this big question: &lt;strong&gt;which &lt;em&gt;inclusivity&lt;/em&gt; (inclusion strengths) mechanism in the strategy?&lt;/strong&gt;&lt;br /&gt;
+ And that &lt;strong&gt;call back to our product strategy&lt;/strong&gt;: build a community that shares values, that is used to spread outcomes (product) OR build a community that shares a product, that is used to spread values. This is not a question on what matters more (product VS values) but on the strategy to get to a point, an objective (many web citizens). Shouldn’t we use our product to built a people network backbone ? Back to GAFAM: what can we learn from the Google try with Google+?&lt;br /&gt;
+ If our core is not enough transversal (the backbone), more new web/tech market there will be, more we will be marginalized, because focused on our circles center not taking in account that the war front (the context) have changed. &lt;strong&gt;Mozilla have to be resilient: mutability of the means, stability in the objectives.&lt;/strong&gt;&lt;br /&gt;
+ The document is the MoFo strategy, and so it doesn’t say anything about ‘build Firefox’ (aka the product strategy) and so don’t articulate our main product (Firefox) with our main people network building effort and values sharing engine. We should do it: at a strategic scale and a particular scale (articulating the agenda-setting with main product features).&lt;/p&gt;
+ &lt;h5&gt;Brand engagement, a psychological backbone on the user side ?&lt;/h5&gt;
+ &lt;p&gt;It seems that our GAFAM challengers get big and have impact by not educating (that much) people, and that’s what makes them not involved in the web citizenship. Or only when they are pushed by their customers. At the opposite, making people aware about web citizenship at first, makes it hard to have that much people involved and so to have impact. However, there is &lt;strong&gt;an other prism that drive people: the brand perceived values&lt;/strong&gt;. Google is seen as a tech pioneer innovator and doing the good because of its open policy, free model, fast innovation… Facebook is seen as really cool firm trying to help people by connecting them…&lt;br /&gt;
+ Is the increase of marketing of Mozilla doing good enough to gains back users ? Is this resilient compared to the next-tech-thing coming ?&lt;br /&gt;
+ Most of the time when I meet Goggle Chrome users and ask then why they use it and don’t switch to Firefox, they answer about use allowed (sync thought devices, apps everywhere that run only on GC, …). Sometimes, they argue that they make effort on other areas, and that they want to keep they digital life simple. They &lt;strong&gt;experience is not centered in a product/brand, but more on the person&lt;/strong&gt;: on that Google Chrome with its Person (with one click ‘auto-login’ to all Google services) is far superior than Firefox.&lt;/p&gt;
+ &lt;h5&gt;User-agent or products ?&lt;/h5&gt;
+ &lt;p&gt;A user-agent is an intermediary acting on behalf of a supplier. As a representative, it is the contact point with customers; It’s role is to manage, to administer the affairs; it is entrusted with a mission by one or more persons; it both acts and produce an effect.&lt;br /&gt;
+ So, the user-agent can be describe with three criteria. It is: an intermediate (user/technology) ; a tool (used to manage and administrate depending on the user’s skills) ; a representative (mission bearer, values vector, for a group of people). It exceeds partly the contradiction between being active and passive.&lt;br /&gt;
+ A &lt;strong&gt;user-agent articulate personal-identity with technology-identity&lt;/strong&gt; and give informations about available skills over these domains. It’s much more universal than a product that is about featuring a user-agent. &lt;strong&gt;If we target resilience, user-agent should be the target&lt;/strong&gt;.&lt;/p&gt;
+ &lt;h4&gt;Social history, marketing: how we understand things to make choices&lt;/h4&gt;
+ &lt;h5&gt;History of the social value&lt;/h5&gt;
+ &lt;p&gt;The way we look at the past and current facts shape our understanding and determine if we open new ways to solve the issues identified. That’s the way to understand the challenges that come on the way and to agree on an adaptation of the strategy instead of splitting things. The way we understand and tell the problem determine the solution we can create: we need, all the way long, &lt;strong&gt;a shared understanding.&lt;/strong&gt;&lt;br /&gt;
+ &lt;strong&gt;Tools and technologies are not necessarily tied to their social value, which depends on social representations. The social value can be built upstream and evolve downstream.&lt;/strong&gt; It also depends on the perspective in which we look at it, on the understanding of the action and therefore on past or current history. Example: the social value of a weapon can be a potential danger or defense, creative (liberating) or destructive. The nuclear bomb is a weapon of mass destruction (negative), whose social value was (ingeniously built as) freedom (positive).&lt;/p&gt;
+ &lt;h5&gt;Impact in our strategy: a missing root&lt;/h5&gt;
+ &lt;p&gt;To engage the public, before to « &lt;em&gt;Focus on creative campaigns that use media + software to engage the public.&lt;/em&gt; » we need to step back, in our speeding world, for understanding together the big picture and the big movement.&lt;br /&gt;
+ Mozilla want to fuel a movement and propose a strong and consistent strategy. However, I think &lt;strong&gt;this plan miss a key point, a root point: build a common (hi)story.&lt;/strong&gt; This should be an objective, not just an action.&lt;br /&gt;
+ Also, that’s maybe a missing root for the State of the web report: how do we understand what we want to evaluate? But it’s not only a missing root for an (annual?) report (a ‘Reporters without borders’ Press-Freedom like?), it’s a missing root for a new grow of our products’ market share.&lt;br /&gt;
+ For example, I do think that most users don’t know and understand that Mozilla is a foundation, Firefox build by a community as a product to keep the web healthy: &lt;strong&gt;they don’t imagine any meaning about technology&lt;/strong&gt;, because they see it as a neutral tool at its root, so as a tool that should just fit they producing needs.&lt;br /&gt;
+ Firefox, its technologies and its features are not bound for ever. It is the narrative, and therefore their inclusion in the social history that we make, which converges Firefox with the values that it stand for. &lt;strong&gt;Stoping or changing the deep narrative means cutting the source of common understanding and making stronger other consistencies captured by other objects, turning as centrifugal forces for Firefox.&lt;/strong&gt;&lt;br /&gt;
+ Marketing is a way to change what we socially say about things: that’s why Google Chrome marketing campaign (and consistent features maturity) has been the decreasing starting point of Firefox. &lt;strong&gt;Our message has been scrambled.&lt;/strong&gt;&lt;/p&gt;
+ &lt;h4&gt;From participation to emancipation: values, people and org relationships&lt;/h4&gt;
+ &lt;p&gt;How to emancipate people in the digital world ?&lt;/p&gt;
+ &lt;h5&gt;Keeping the open open&lt;/h5&gt;
+ &lt;p&gt;Being open is not a thing we can achieve, it’s a constant process. « &lt;em&gt;Mozilla needs to engage on both fronts, tackling the big problems but also fuelling the next wave of open.&lt;/em&gt; » Yes, but &lt;strong&gt;Mozilla should say too how the next wave of open can stay under people’s control and rally new people&lt;/strong&gt;. Not only open code, but open participation, open governance, open organization. Being open is not a releasing policy about objects, it’s a mutation to participation process: a metamorphosis. It’s not reached by expanding, but by shifting. It’s not only about an amount, but about values: it’s qualitative.&lt;br /&gt;
+ Maybe &lt;strong&gt;open is not enough&lt;/strong&gt;, because it doesn’t say enough about who control and how, about the governance, and says too much about &lt;strong&gt;availability (passive)&lt;/strong&gt; and not enough &lt;strong&gt;about &lt;em&gt;inclusivity&lt;/em&gt; (active ; inclusion strengths)&lt;/strong&gt;. It doesn’t say how the power is organized and articulated to the people (ex. think about how closed is the open Android). We may need to change the wording: indie web, the web that fuel autonomy, is a try, but it doesn’t say enough about &lt;em&gt;inclusivity&lt;/em&gt; compared to openness &amp;amp; opportunity. Emancipation is the concept. It’s strategic because it says what is aligned to what, especially how to articulate values and uses. It’s important because it tells what are the sufficient conditions of realization to ‘open/indie’. That’s key to get ‘open/indie at small and large scales, from Internet people to Internet institutions, thought all ‘open/indie’ detractors in the always-current situation: a resilient ecosystem.&lt;br /&gt;
+ My intuition is that &lt;strong&gt;the leadership network and advocacy engine promoting open will be efficient if we clarify ‘open’ while keeping it universal&lt;/strong&gt;. We can do it by looking back at the raw material that we have worked for years, our DNA in action. Because after all, we are experts about it and wish others to become experts too. It does not mean to essentialize it (opposing its nature and its culture), &lt;strong&gt;but to define its conditions of continuous achievement in our social context&lt;/strong&gt;.&lt;/p&gt;
+ &lt;h5&gt;Starting point: exemplary projects that tell a lot about the evolution of our DNA in action&lt;/h5&gt;
+ &lt;p&gt;Clarifying the idea of ‘open’ is strategic to our action because it outlines the constitution of ‘open’, its high ‘rules’, like with laws in political regimes. It clarifies for all, if you are part of it or not, and it tells you what to change to get in. It can reinforce the brand by differentiating from the big players that are the GAFAM: &lt;strong&gt;it’s a way to drive, not to be driven by others lowering the meaning to catch the social impact. We should say that ‘open’ at Mozilla means more than ‘open’ at GAFAM&lt;/strong&gt;. I wish Mozilla to speak about its openness, not as an ‘equal in opportunity’ but as an ‘equal in participation’, because it fits openness not only for a moment (on boarding) or for a person, but during the whole process of people’s interaction.&lt;br /&gt;
+ &lt;a href=&quot;https://www.rust-lang.org/&quot;&gt;Rust&lt;/a&gt; and &lt;a href=&quot;https://servo.org/&quot;&gt;Servo&lt;/a&gt; or &lt;a href=&quot;https://firefoxos.mozilla.community/&quot;&gt;Firefox OS&lt;/a&gt; (since the Mozilla’s shift to radical participation) seem to be very good examples of projects with participation &amp;amp; impact centric rules, tools, process (RFC, new team and owners, …). Think about how Rust and &lt;a href=&quot;http://arc.applause.com/2015/03/27/google-dart-virtual-machine-chrome/&quot;&gt;Dart emerged and are evolving&lt;/a&gt;. Think about how stronger has been the locked-open Android with partnership than the open-locked FxOS. We should tell those stories, not as recipes that can be reproduced, but as process based on a Constitution (inclusive rules) that make a political regime (open) and define a mode of government (participation). That’s key to social understanding and therefore to transpose and advocate for it.&lt;br /&gt;
+ As projects&lt;strong&gt; compared to ‘original Mozilla’, Rust, Servo and FxOS could say a lot&lt;/strong&gt; about how different they implemented learning/interaction/participation at the roots of the project. How the process, the tools, the architecture, the governance and the opportunities/constraints have changed for Mozilla and participants. This could definitely help to setup our curriculum resources, database and workshop at a personal (e.g., “How to teach / facilitate / organize / lead in the open like Mozilla.â€) and orgs levels, with personal and orgs policies.&lt;/p&gt;
+ &lt;h5&gt;Spreading the high meanings in our strategy to consolidate it consistency&lt;/h5&gt;
+ &lt;p&gt;Clarifying the constitution of ‘open’ calls to clarify other related wordings.&lt;br /&gt;
+ I’m satisfied to read back (social) ‘movement’ instead of ‘community’, because it means that our goal can’t be achieve forever (is static), but we should protect it by acting. And it seems more inclusive, less ‘folds on itself’ and less ‘build the alternative beside’ than ‘community’: the alternative can be everywhere the actual system is. It can make a system. It can get global, convergent, continuous, … all at the same time. Because it’s roots are decentralized _and_ consistent, collaborating, …&lt;/p&gt;
+ &lt;p&gt;About participation, we should think too (again) about engagement VS contribute VS participate: how much am I engaged ? Free about defining and receiving cost/gains? What is the impact of my actions ? … &lt;strong&gt;These different words carry different ideas about how we connect the ‘open’&lt;/strong&gt;: spread is not enough because it diffuses, _be_ everywhere is more permanent. Applied to Mozilla’s own actions, &lt;strong&gt;funding open projects and leaders, is maybe not enough and there should be others areas where we can connect&lt;/strong&gt; inside products, technology, people and organizations that build emancipation. So that say something about getting control (who, how, …).&lt;/p&gt;
+ &lt;h5&gt;IA: a challenge for ‘open’&lt;/h5&gt;
+ &lt;p&gt;IA is first developed to help us by improving our interactions. However, this seems to start to shift into taking decisions instead of us. This is problematic because these are indirect and direct ways for us to loose control, to be locked. And that can be as far as computers smarter than humans. The problem is that technical progress is made without any consideration of the societal progress it should made.&lt;br /&gt;
+ That’s an other point, why open is not enough: automation should be build-in with superior humanization. &lt;strong&gt;Mozilla should activate and unlock societal progress to build fair technical progress.&lt;/strong&gt;&lt;/p&gt;
+ &lt;h5&gt;Digital integration &amp;amp; democracy&lt;/h5&gt;
+ &lt;p&gt;The digital (&amp;amp; virtual) world is gaining control over the physical world in many domains of our society (economy to finance, mail to email, automatic car, voting machine, …). It’s getting more and more integrated to our lives without getting back our (imperfect) democracy integrated into them. Public benefit and public good are turning ‘self benefit’ and ‘own sake’ because citizens don’t have control over private companies. &lt;strong&gt;We should build a digital democracy if we don’t want to loose at all the democratic governing of society.&lt;/strong&gt; We must overcome the poses and postures battles about private and public. We need to build.&lt;/p&gt;
+ &lt;h4&gt;‘Leader’ &amp;amp; ‘Leadership’ need a clarification&lt;/h4&gt;
+ &lt;h5&gt;Why a clarification?&lt;/h5&gt;
+ &lt;p&gt;At some level, I’m not the only one to ask this question:&lt;/p&gt;
+ &lt;blockquote&gt;&lt;p&gt;How do CRM requirements for Leadership and Advocacy overlap / differ? What’s our email management / communications platform for Leadership?&lt;/p&gt;&lt;/blockquote&gt;
+ &lt;p&gt;Connect leaders to lead what ? How ? To whose benefit ? Do we want to connect leaders or initiatives (people or orgs) ? Will the leaders be emerging ones (building new networks) or established ones (use they influence to rally more people)? Are Leaders leaders of something part of Mozilla (like can be Reps) or outside of Mozilla (leaders of project, companies, newspaper: tech leaders, news leaders, …) ? This is especially important depending on what is the desire for the leaders to become in the future. &lt;strong&gt;The MoFo’s document should be more precise&lt;/strong&gt; about this and go forward than « &lt;em&gt;Mozilla must attract, develop, and support a global network of diverse leaders who use their expertise to collaboratively advance points-of-view, policies and practices that maintain the overall health of the Internet.&lt;/em&gt; »&lt;br /&gt;
+ We should do it because &lt;strong&gt;the confusion about the leadership impact the advocacy engine&lt;/strong&gt;: « &lt;em&gt;The shared themes also provide explicit opportunities for our Leadership and Advocacy efforts to work together.&lt;/em&gt; » Regarding Mozilla, is the leaders role to be advocacy leaders ? It seems as they share themes and key initiatives (even if not worded the same sometimes). Or in other words, who Drives the Advocacy engine?&lt;/p&gt;
+ &lt;h5&gt;Iterations with the actual definition: creators&lt;/h5&gt;
+ &lt;p&gt;Here are my iterations on the definition of ‘Leaders’:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;The Leaders could be the people platform (the community) and the advocacy engine the tool/themes/actions platform (the product).&lt;/li&gt;
+ &lt;li&gt;Leaders could build at the end new solutions (products) and Advocates new voices (rallying), that could be translated in a learning area divided like Leadership=learn+create and advocacy=teach+spread.&lt;/li&gt;
+ &lt;li&gt;Leadership: personal development to produce (turn into) new commons or add new facets to commons. Advocacy: personal development to protect established/identified commons.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;With these definitions, then Leaders are maybe more a Lab, R&amp;amp;D place, incubation tool (if we think about start-up incubators, then it shows a tool-set that we will need to inspire for the future). But if we want to keep the emphasis on people, &lt;strong&gt;we could name them ‘creators’&lt;/strong&gt; (compositors or managers ; not commoners, because leaders and advocates are commoners ; yes, traditionally creators are craftspersons and intellectual designers). This make sens with the examples given in the MoFo 2020 strategy 0.8 document, where all persona are involved in a building-something-new process.&lt;/p&gt;
+ &lt;p&gt;However, it’s interesting to understand why we choose at first ‘Leaders’. &lt;strong&gt;Leaders build new solutions (products) and Advocates new voices (rallying), they are both about personal development and empower commons.&lt;/strong&gt; Leadership=learn+create and advocacy=teach+spread commons. Leaders are projects/orgs leaders, the ones that traduce DNA (values) in products (concrete ability and availability). Advocates are values advocates, the ones that traduce DNA (values) in actions (behavior). As they are both targeting commons, they both produce the same social organization (collaboration instead of competition). They are both involved to create (different) representation (institutions) and organization (foundation/firms) but &lt;strong&gt;with a different DNA (values) processing&lt;/strong&gt;: from public good to personal interest or the opposite. If Mozilla cares about public good resilience, &lt;strong&gt;the articulation of they domains of values is critical. So their articulation’s expression and the revision process must be said and clear&lt;/strong&gt;: from hierarchy vs contract vs different autonomy levels (internal incubation and external advocacy), vs … to criteria to start a revision.&lt;/p&gt;
+ &lt;h5&gt;The network effect&lt;/h5&gt;
+ &lt;p&gt;Another argument for the switch from Leader to Creator is that the Leader word it too much tight to a single-person-made innovation. &lt;strong&gt;Creator make more clear that the innovation is possible not because of one genius, but because of a team&lt;/strong&gt;, a group, a collective: personS (where there could also be genius). The value is made by the collaboration of people (especially in an open project, especially in a network).&lt;br /&gt;
+ That’s important because that could impact how well we do the convening part: not self-promoting, not-advertising, but sharing skills and knowledge for people and catalysing projects.&lt;br /&gt;
+ &lt;strong&gt;The same for the wording ‘talent’&lt;/strong&gt;: alone, a talent can do nothing that has an impact. At least, we need two talents, a team (plus some assistants at some point).&lt;/p&gt;
+ &lt;h5&gt;The cultural prism&lt;/h5&gt;
+ &lt;p&gt;Again, this seems to be an open question:&lt;/p&gt;
+ &lt;blockquote&gt;&lt;p&gt;Define and articulate “leadership.†Hone our story, ethos and definition for what we mean by “leadership development†(including cultural / localization aspects).&lt;/p&gt;&lt;/blockquote&gt;
+ &lt;p&gt;In my culture, Leader carry positive (take action) and negative (dominate) meanings. That’s another reason why I prefer another naming.&lt;br /&gt;
+ I understand too that it carries a lot of legitimacy (ex. market leader) in our societies and it avoids the stay-experimental or non-massive (unique) thoughts. And we need legitimacy to get impact.&lt;br /&gt;
+ But the way Mozilla has an impact thought all cultures, its &lt;strong&gt;legitimacy, is by creating or expanding a common&lt;/strong&gt;. To do this, depending on the maturity, Mozilla could follow the market proposing an alternative with superior usability OR opening a new market by adding a vertical segment.&lt;/p&gt;
+ &lt;h5&gt;Existing tool-set opportunities&lt;/h5&gt;
+ &lt;p&gt;If Leadership is « &lt;em&gt;a year-round MozFest + Lab&lt;/em&gt;« , so it’s a social network + an incubation place. Then, we already have a social network for people involved with Mozilla: Which kind of link should have the leadership network with &lt;strong&gt;mozillians.org&lt;/strong&gt; ? What can we learn from this project and other specialized social network projects (linkedin, viadeo, …) to build the leadership network ?&lt;/p&gt;
+ &lt;h4&gt;Advocacy engine: make it clear&lt;/h4&gt;
+ &lt;h5&gt;What it is &amp;amp; how it works&lt;/h5&gt;
+ &lt;p&gt;Mozilla is doing a great effort to build its advocacy engine on collaboration (« &lt;em&gt;Develop new partnerships and build on current partnerships&lt;/em&gt;« , « &lt;em&gt;begin collaboration&lt;/em&gt;« , « &lt;em&gt;build alliances with similar orgs&lt;/em&gt;« ) but at the same time affirms that Mozilla should be « &lt;em&gt;Part of a broader movement, be the boldest, loudest and most effective advocates&lt;/em&gt; » that could be seen as too centralized, too exclusive.&lt;br /&gt;
+ While this can be consistent (or contradictory), &lt;strong&gt;the consistency has to be explained&lt;/strong&gt; looking at orgs and people, global and local, abstract and real, with a complementarity/competitive grid.&lt;br /&gt;
+ First, &lt;strong&gt;the articulation with other orgs has to be explained&lt;/strong&gt;. What about others orgs doing things global (&lt;a href=&quot;https://eff.org/&quot;&gt;EFF&lt;/a&gt;, &lt;a href=&quot;https://fsf.org/&quot;&gt;FSF&lt;/a&gt;, …) and local (&lt;a href=&quot;http://www.laquadrature.net/&quot;&gt;Quadrature du net&lt;/a&gt;, CCC, …) ? What about the value they give and that Mozilla doesn’t have (juridic expertise for example) ? What about other advocate engines (&lt;a href=&quot;https://change.org/&quot;&gt;change.org&lt;/a&gt;, &lt;a href=&quot;https://secure.avaaz.org/&quot;&gt;Avaaz&lt;/a&gt;…) ? That should not be at an administrative level only like « &lt;em&gt;Develop an affiliate policy. Defining what MoFo does / does not offer to effectively govern relationships w. affiliated partners and networks (e.g., for issues like branding, fundraising, incentives, participation guidelines, in-kind resources.)&lt;/em&gt; »&lt;br /&gt;
+ Second, this is key for users to understand and &lt;strong&gt;articulate the global level of the brand engagement and their local preoccupations and engagement&lt;/strong&gt;. How the engine will be used for local (non-US) battles ? In the past Mozilla totally involved against PIPA, SOPA by taking action, and hesitate a lot to take position and just published a blog post (and too late to gain traction and get impact) against French spying law for example.&lt;br /&gt;
+ Third, &lt;strong&gt;the articulation ‘action(own agenda)/reaction’ should be clarified&lt;/strong&gt; in the objectives and functioning of the advocacy engine. Especially because other orgs, allies or detractors, try to to setup the social agenda. It’s important because it can change the social perception of our narrative (alternative promotion/issue fighting) and therefore people’s contributions.&lt;br /&gt;
+ People think the technology is socially neutral. People are satisfied of narrow hills of choice (not the meaning, the aim, but only the ability to show your favorite avatar). &lt;strong&gt;People don’t want to feel guilty or oppressed&lt;/strong&gt;, they don’t want new constraints, they are looking for solution only: they want to use, not to do more, they want they things to be done. Part of the problem is about understanding (literacy, education), part of it is about the personal/common duality, part of it is about being hopeless about having an impact, part of it is about expressing change as a positive goal and a new possible way (alternative), not a fight against an issue. About the advocacy engine, I think &lt;strong&gt;our preoccupation should be people-centric and the aim to give them a short, medium and long term narrative to get action without being individuals-centric&lt;/strong&gt;.&lt;/p&gt;
+ &lt;h5&gt;How we build it ?&lt;/h5&gt;
+ &lt;p&gt;How to build a social movement ? How it has been built in the past ? Is it the same today ? Can it be transposed to the digital domain from others social domains ? How strong are the cultural differences between nations? These are the main questions we should answer, and our pivot era gives us many examples in diverse domains (climate change advocates, Syriza &amp;amp; Podemos, NSA &amp;amp; surveillance services in Europe, empowered syndicates in Venezuela, &lt;a href=&quot;http://blogs.valvesoftware.com/economics/why-valve-or-what-do-we-need-corporations-for-and-how-does-valves-management-structure-fit-into-todays-corporate-world/#more-252&quot;&gt;Valve corp. internal organization&lt;/a&gt;…) to set a search terrain. However, I will go strait to my intuitive understanding below.&lt;br /&gt;
+ I’m kind of worried that it’s imagined to build the advocacy engine themes by a top-down method. &lt;strong&gt;I think a successful advocacy is always built bottom-up&lt;/strong&gt;, as its function is to give back the voice to the people, to get them involved, not to make them fulfill our predefined aims. The top-down method is too organization centric: it can’t massively drive people that are not already committed to the org. It’s usually named advertisement or propaganda. If we want to have impact, &lt;strong&gt;we should listen to people needs, not tell them to listen to ours. People want (first) to be empowered, not to empower an org&lt;/strong&gt;. So let’s organize the infrastructure, set the agenda and draw the horizon (strategic understanding) participative: make people fill them with content of their experience. It seems to me it is the only way, the only successful method, if we want to build a movement, and not just a shifting moment (that could be built by the top, with a good press campaign locally relayed for example ; that’s what happen in old style politics: the aim is short term, to cleave).&lt;br /&gt;
+ &lt;strong&gt;Isn’t the advocacy engine a new Drumbeat ?&lt;/strong&gt; We shifted from Drumbeat to Webmaker+web literacy to Mozilla Academy and now to Leadership plus advocacy: it could be good to tell that story now that we are shifting again and learn from it.&lt;br /&gt;
+ &lt;strong&gt;Mozilla should support, behave as a platform&lt;/strong&gt;, not define, not focus. Letting the people set the agenda makes them more involved and is a good way to build a network of shared aims with other orgs, that is not invasive or alienating, but a support relationship in a win-win move. The strength comes from the all agendas sewed. So at an org level, let’s on-board allies organizations as soon as plan building-time (now), to build it together. Yes it’s slower, but much more massive, inclusive and persistent.&lt;/p&gt;
+ &lt;h5&gt;How we evaluate it: cultural bias &amp;amp; qualitative analysis&lt;/h5&gt;
+ &lt;p&gt;First, about the agenda-setting KPI for 2016, should these KPI be an evaluation of the inclusion and rank in others strategic agendas, governance systems and productions (outcome/products) ? Others org could be from different domains: political, social, economy orgs.&lt;br /&gt;
+ Then, as a wide size audience KPI, Mozilla wants « &lt;em&gt;celebration of our campaigns with ‘headline KPIs’ including number of actions, and number of advocates.&lt;/em&gt;« . While doing this could be the right thing to do for some cultures, it could be the worst for others. I think that these KPI don’t carry a meaning for people and are too org centric. In a way, they are to generic: it’s just an amount. &lt;strong&gt;Accumulation is not our goal: we want impact that is the grow of articulated actions&lt;/strong&gt; made by diverse people toward the same aim. &lt;strong&gt;We need our massive KPI to be more qualitative&lt;/strong&gt;, or at least find a way to present them in a more qualitative way: interactive map ? a global to local prism that engages people for the next step ?&lt;/p&gt;
+ &lt;h5&gt;Best practices &amp;amp; massive impact&lt;/h5&gt;
+ &lt;p&gt;Selecting best practices are an appealing method when we want to have a fast and strong impact in a wide area. However, &lt;strong&gt;when we unify we should avoid to homogenize&lt;/strong&gt;. The gain in area by scaling-up is always at the cost of loosing local impact because it is not corresponding to local specificities, hence to local expectations. Federating instead of scaling-up is a way to solve this challenge. So we should be careful to not &lt;strong&gt;use best practice as absolute solutions, but as solutions in a context&lt;/strong&gt; if we want to transpose them massively.&lt;/p&gt;
+ &lt;h5&gt;Tools &amp;amp; platform balanced between user-centric and org-centric outcomes&lt;/h5&gt;
+ &lt;p&gt;It’s good to hear that we will build a advocacy platform. As we ‘had’ bugzilla+svn then mercurial (hg)+… and are going to the &lt;strong&gt;integrated&lt;/strong&gt;, &lt;strong&gt;pluggable&lt;/strong&gt; and &lt;strong&gt;content-centric&lt;/strong&gt; (but non-free; admin tools are closed source) github (targeting more coder than users, but with a lower entry price for users still), we need to be able to have the same kind of tool for advocates and leaders. Something inspired maybe at some levels by the remixing tools we built in Webmakers for web users.&lt;/p&gt;
+ &lt;h4&gt;From experiment to production: support (self made to mass product) + modularity (dev code patch to users add-ons).&lt;/h4&gt;
+ &lt;p&gt;&lt;strong&gt;We need pathways from lab to home that carry different mix of customization and reliability to support the emancipation curve.&lt;/strong&gt;&lt;br /&gt;
+ Users want things to work, because they want to use it. Geeks want to be able to modify a lot and accept to put their hands in the engine to build growing reliability. Advanced users want to customize their experience and keep control and understanding on working status. They want to be able to fix the reliability at a medium/low technical cost. They are OK to gain more control at these prices. Users want to use things to do what they need and want to trust a reliability maintained for them. They are OK to gain control at a no technical cost. Depending on the matter we all have different skill levels, so we are all geeks, advanced users and users depending on our position or on the moment. And depending on our aspirations, we all want to be able to move from one category to an other. That’s what we need to build: we don’t just need to « &lt;em&gt;better articulate the value to our audiences&lt;/em&gt;« , &lt;strong&gt;we need to build pathways thought audiences and thought IT layers&lt;/strong&gt; (content, software, hardware, distant service). &lt;strong&gt;We should find a convergence between customization and reliability, between first time experience, support and add-ons thought all our users’ persona by building bridges, pathways&lt;/strong&gt;. So, « &lt;em&gt;better articulate the value to our audiences&lt;/em&gt; » should not be restrained in our minds to the Mozilla Leadership Network.&lt;br /&gt;
+ &lt;strong&gt;Part of this is being done in other projects outside of Mozilla in the commons movement.&lt;/strong&gt; There are many, but let’s take just one example, the &lt;a href=&quot;https://www.fairphone.com/&quot;&gt;Fairphone&lt;/a&gt; project: modularity, howtos, … all this help to break the product-to-use walls and drive appropriation/emancipation. &lt;strong&gt;Products are less product and brand centric and more people/user centric&lt;/strong&gt;.&lt;br /&gt;
+ Part of this has been done inside Mozilla, like integrating learning in our products, in-content, as we have code comment on code. I think &lt;strong&gt;the &lt;a href=&quot;https://wiki.mozilla.org/Firefox_OS/Spark&quot;&gt;Spark&lt;/a&gt; project on Firefox OS is on a promising path&lt;/strong&gt;, even if maybe immature: it maybe has not been released mainstream because it misses bridges/pathways (on-boarding levels, progression from simple to high level techniques, and no or not enough reproducible/universal next task/skill building).&lt;br /&gt;
+ So some solutions start to emerge, the direction is here, but has never been conceived and implemented that globally, as there isn’t integrated pathways with choice and opportunity and a strategy embracing all products and technologies (platform, tools, …).&lt;/p&gt;
+ &lt;h4&gt;Better tools for collaboration and participation: task-centric to process-centric (use) infrastructure&lt;/h4&gt;
+ &lt;p&gt;&lt;strong&gt;The open community should definitely improve the collaboration tools and infrastructure to ease participation.&lt;/strong&gt;&lt;br /&gt;
+ &lt;strong&gt;&lt;a href=&quot;http://www.discourse.org&quot;&gt;Discourse&lt;/a&gt; ‘merged’ discussion channels&lt;/strong&gt;: email+forum(+instant, messaging, … and others peer-to-peer discussion?). &lt;strong&gt;&lt;a href=&quot;http://stackexchange.com&quot;&gt;Stack exchange&lt;/a&gt; merged the questioning/solving process&lt;/strong&gt; and added a vote mechanism to rank answers: it eased the collaboration on editing the statement and the results while staying synchronous with the discussion and keeping the discussion history. We need such kind of possibilities with discourse: &lt;strong&gt;capitalize on the discussion and preview the results to build a plan.&lt;/strong&gt;&lt;br /&gt;
+ This exist in document oriented software (that added collaboration editing tools), but not that much in collaboration software (that don’t produce documents). For example, while discussing the future plan for Fx/FxOS be supported to keep track on a doc about the proposals plans + criteria &amp;amp; dependencies. In action, it is from &lt;a href=&quot;https://mail.mozilla.org/pipermail/firefox-dev/2015-July/003063.html&quot;&gt;this&lt;/a&gt; plus all the discussion taking place to &lt;a href=&quot;https://mail.mozilla.org/pipermail/firefox-dev/2015-July/003119.html&quot;&gt;that&lt;/a&gt;.&lt;br /&gt;
+ This is maybe something like integrating Discourse+Wiki, maybe with the need to have competing and ranked (both for content and underlaying meaning of content=strategy?) plan/page proposals. &lt;strong&gt;From evolving the wiki discussion page to featuring document production into peer-to-peer discussion.&lt;/strong&gt;&lt;/p&gt;
+ &lt;h4&gt;A recovering strategy: from fail to win&lt;/h4&gt;
+ &lt;p&gt;There is maybe one thing that is in the shadow in this plan: &lt;strong&gt;what do we do when/if we (partially) fail ?&lt;/strong&gt;&lt;br /&gt;
+ I think at least we should say that &lt;strong&gt;we document&lt;/strong&gt; (keep research going on) to be able to outline and spread the outcomes of what we tried to fight against. So we still try to built consciousness to be ready for the next round.&lt;/p&gt;
+ &lt;p&gt; &lt;/p&gt;
+ &lt;p&gt;&lt;em&gt;If you see some contradiction in my thoughts, let’s say it’s my state of thinking right now: please voice them so we can go forward.&lt;/em&gt;&lt;br /&gt;
+ &lt;em&gt; The same for thoughts that are voiced definitive (like users are): take it as a first attempt with my bias: let’s state these bias to go forward.&lt;/em&gt;&lt;/p&gt;
+ &lt;div class=&quot;footnotes&quot; id=&quot;footnotes-48&quot;&gt;
+ &lt;div class=&quot;footnotedivider&quot;&gt;&lt;/div&gt;
+ &lt;ol&gt;
+ &lt;li id=&quot;fn-48-1&quot;&gt; ‘&lt;em&gt;Radical&lt;/em&gt;‘ can be in some cultures an euphemism to ‘&lt;em&gt;violent&lt;/em&gt;‘. Let’s be clear that the change by increasing violence is done to make a popular uprising of some part against others. While it does not help the majority to magically understand that the minority is right, it stigmatize the radical-violent-changers and in the way it discredits the alternative proposed. &lt;span class=&quot;footnotereverse&quot;&gt;&lt;a href=&quot;https://repeer.org/tag/mozilla/feed/#fnref-48-1&quot;&gt;↩&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
+ &lt;/ol&gt;
+ &lt;/div&gt;</content:encoded>
+ <dc:date>2016-01-16T00:27:13+00:00</dc:date>
+ <dc:creator>Nicolas</dc:creator>
+ </item>
+ <item rdf:about="http://bluesock.org/%7Ewillkg/blog/pyvideo/status_20160115.html">
+ <title>Will Kahn-Greene: pyvideo status: January 15th, 2016</title>
+ <link>http://bluesock.org/%7Ewillkg/blog/pyvideo/status_20160115.html</link>
+ <content:encoded>&lt;div class=&quot;section&quot; id=&quot;what-is-pyvideo-org&quot;&gt;
+ &lt;h3&gt;What is pyvideo.org&lt;/h3&gt;
+ &lt;p&gt;&lt;a class=&quot;reference external&quot; href=&quot;http://pyvideo.org/&quot;&gt;pyvideo.org&lt;/a&gt; is an index of Python-related conference and user-group videos on
+ the Internet. Saw a session you liked and want to share it? It's likely you can
+ find it, watch it, and share it with pyvideo.org.&lt;/p&gt;
+ &lt;p&gt;This is the latest status report for all things happening on the site.&lt;/p&gt;
+ &lt;p&gt;It's also an announcement about the end.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;http://bluesock.org/~willkg/blog/pyvideo/status_20160115.html&quot;&gt;Read more…&lt;/a&gt; (5 mins to read)&lt;/p&gt;&lt;/div&gt;</content:encoded>
+ <dc:date>2016-01-15T23:30:00+00:00</dc:date>
+ <dc:creator>Will Kahn-Greene</dc:creator>
+ </item>
+ <item rdf:about="http://coopcoopbware.tumblr.com/post/137371863755">
+ <title>Chris Cooper: RelEng &amp; RelOps Weekly Highlights - January 15, 2016</title>
+ <link>http://coopcoopbware.tumblr.com/post/137371863755</link>
+ <content:encoded>&lt;p&gt;One of releng’s big goals for Q1 is to deliver a beta via &lt;a href=&quot;https://bugzil.la/release-promotion&quot; target=&quot;_blank&quot;&gt;build promotion&lt;/a&gt;. It was great to have some tangible progress there this week with bouncer submission.&lt;/p&gt;
+
+ &lt;p&gt;Lots of other stuff in-flight, more details below!
+ &lt;/p&gt;&lt;p&gt;&lt;b&gt;Modernize infrastructure&lt;/b&gt;:&lt;/p&gt;
+
+ &lt;p&gt;Dustin worked with Armen and Joel Maher to run Firefox tests in TaskCluster on an older EC2 instance type where the tests seem to fail less often, perhaps because they are single-CPU or slower.&lt;/p&gt;
+
+ &lt;p&gt;&lt;b&gt;Improve CI pipeline&lt;/b&gt;:&lt;/p&gt;
+
+ &lt;p&gt;We turned off automation for b2g 2.2 builds this week, which allowed us to remove some code, reduce some complexity, and regain some small amount of capacity. Thanks to Vlad and Alin on buildduty for helping to land those patches. (&lt;a href=&quot;https://bugzil.la/1236835&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/1236835&lt;/a&gt; and &lt;a href=&quot;https://bugzil.la/1237985&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/1237985&lt;/a&gt;)&lt;/p&gt;
+
+ &lt;p&gt;In a similar vein, Callek landed code to disable all b2g desktop builds and tests on all trees. Another win for increased capacity and reduced complexity! (&lt;a href=&quot;https://bugzil.la/1236835&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/1236835&lt;/a&gt;)&lt;/p&gt;
+
+ &lt;p&gt;&lt;b&gt;Release&lt;/b&gt;:&lt;/p&gt;
+
+ &lt;p&gt;Kim finished integrating bouncer submission with our release promotion project. That’s one more blocker out of the way! (&lt;a href=&quot;https://bugzil.la/1215204&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/1215204&lt;/a&gt;)&lt;/p&gt;
+
+ &lt;p&gt;Ben landed several enhancements to our update server: adding aliases to update rules (&lt;a href=&quot;https://bugzil.la/1067402&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/1067402&lt;/a&gt;), and allowing fallbacks for rules with whitelists (&lt;a href=&quot;https://bugzil.la/1235073&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/1235073&lt;/a&gt;).&lt;/p&gt;
+
+ &lt;p&gt;&lt;b&gt;Operational&lt;/b&gt;:&lt;/p&gt;
+ &lt;p&gt;There was some excitement last Sunday when all the trees were closed due to timeouts connectivity issues between our SCL3 datacentre and AWS. (&lt;a href=&quot;https://bugzil.la/238369&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/238369&lt;/a&gt;)&lt;/p&gt;
+
+ &lt;p&gt;&lt;b&gt;Build config&lt;/b&gt;:&lt;/p&gt;
+
+ &lt;p&gt;Mike released v0.7.4 of &lt;a href=&quot;http://gittup.org/tup/&quot; target=&quot;_blank&quot;&gt;tup&lt;/a&gt;, and is working on generating the tup backend from moz.build. We hope to offer tup as an alternative build backend sometime soon.&lt;/p&gt;
+
+ &lt;p&gt;See you all next week!&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-15T22:44:13+00:00</dc:date>
+ </item>
+ <item rdf:about="https://air.mozilla.org/webdev-beer-and-tell-january-2016/">
+ <title>Air Mozilla: Webdev Beer and Tell: January 2016</title>
+ <link>https://air.mozilla.org/webdev-beer-and-tell-january-2016/</link>
+ <content:encoded>&lt;p&gt;
+ &lt;img alt=&quot;Webdev Beer and Tell: January 2016&quot; class=&quot;wp-post-image&quot; height=&quot;90&quot; src=&quot;https://air.cdn.mozilla.net/media/cache/35/0f/350f246037ead3bab95fdbd4c2b77484.png&quot; width=&quot;160&quot; /&gt;
+ Once a month web developers across the Mozilla community get together (in person and virtually) to share what cool stuff we've been working on in...
+ &lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-15T22:00:00+00:00</dc:date>
+ <dc:creator>Air Mozilla</dc:creator>
+ </item>
+ <item rdf:about="http://blog.mozilla.org/sumo/?p=3665">
+ <title>Support.Mozilla.Org: What’s up with SUMO – 15th January</title>
+ <link>https://blog.mozilla.org/sumo/2016/01/15/whats-up-with-sumo-15th-january/</link>
+ <content:encoded>&lt;p&gt;&lt;strong&gt;Hello, SUMO Nation!&lt;/strong&gt;&lt;/p&gt;
+ &lt;p&gt;The second post of the year is here. Have you had a good time in 2016 so far? Let us know in the comments!&lt;/p&gt;
+ &lt;p&gt;Now, let’s get going with the updates and activity summaries. It will be brief today, I promise.&lt;/p&gt;
+ &lt;h3&gt;&lt;strong class=&quot;author-name&quot;&gt;Welcome, new contributors!&lt;br /&gt;
+ &lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li class=&quot;author&quot;&gt;
+ &lt;div class=&quot;author&quot;&gt;&lt;a class=&quot;username&quot; href=&quot;https://support.mozilla.org/en-US/user/Andy.Yang&quot;&gt;Andy.Yang&lt;/a&gt;&lt;/div&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;div class=&quot;author&quot;&gt;After the massive influx over the last few weeks, we only had Andy introducing himself recently – the warmer the welcome for him!&lt;/div&gt;
+ &lt;div class=&quot;author&quot;&gt;&lt;/div&gt;
+ &lt;div class=&quot;author&quot;&gt;If you just joined us, don’t hesitate – come over and &lt;a href=&quot;https://support.mozilla.org/forums/buddies&quot; target=&quot;_blank&quot;&gt;say “hi†in the forums!&lt;/a&gt;&lt;/div&gt;
+ &lt;div class=&quot;author&quot;&gt;&lt;/div&gt;
+ &lt;div class=&quot;author&quot;&gt;
+ &lt;h3&gt;&lt;strong&gt;Contributors of the week&lt;br /&gt;
+ &lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://blog.mozilla.org/sumo/2016/01/08/whats-up-with-sumo-8th-january/&quot; target=&quot;_blank&quot;&gt;All the people who joined us in the winter season so far!&lt;/a&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid64&quot;&gt;
+ &lt;p&gt;&lt;strong&gt;&lt;span style=&quot;text-decoration: underline;&quot;&gt;We salute you!&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;author&quot;&gt;Don’t forget that if you are new to SUMO and someone helped you get started in a nice way you can &lt;a href=&quot;https://support.mozilla.org/forums/buddies/711364?last=65670&quot; target=&quot;_blank&quot;&gt;nominate them for the Buddy of the Month!&lt;/a&gt;&lt;/div&gt;
+ &lt;div class=&quot;author&quot;&gt;&lt;/div&gt;
+ &lt;/div&gt;
+ &lt;h3&gt;&lt;strong&gt;Most recent SUMO Community meeting&lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://public.etherpad-mozilla.org/p/sumo-2016-01-11&quot; target=&quot;_blank&quot;&gt;You can read the notes here&lt;/a&gt; and see the video on our &lt;a href=&quot;https://www.youtube.com/channel/UCaiposaIhA7HfMqH2NIciyA/videos&quot; target=&quot;_blank&quot;&gt;YouTube channel&lt;/a&gt; and &lt;a href=&quot;https://air.mozilla.org/search/?q=sumo&quot; target=&quot;_blank&quot;&gt;at AirMozilla&lt;/a&gt;.&lt;del&gt; &lt;/del&gt;&lt;del&gt;&lt;br /&gt;
+ &lt;/del&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;strong&gt;IMPORTANT: We are considering changing the way the meetings work. Help us figure out what’s best for you – join the discussion on the forums in this thread: &lt;a href=&quot;https://support.mozilla.org/en-US/forums/contributors/711752?last=67873&quot;&gt;(Monday) Community Meetings in 2016&lt;/a&gt;.&lt;/strong&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;&lt;strong&gt;The next SUMO Community meeting… &lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li style=&quot;text-align: left;&quot;&gt;is happening on &lt;a href=&quot;https://public.etherpad-mozilla.org/p/sumo-2016-01-18&quot; target=&quot;_blank&quot;&gt;Monday the 18th – join us&lt;/a&gt;!&lt;/li&gt;
+ &lt;li style=&quot;text-align: left;&quot;&gt;&lt;strong&gt;Reminder: if you want to add a discussion topic to the upcoming meeting agenda:&lt;/strong&gt;
+ &lt;ul&gt;
+ &lt;li style=&quot;text-align: left;&quot;&gt;Start a thread in the &lt;a href=&quot;https://support.mozilla.org/forums/contributors&quot; target=&quot;_blank&quot;&gt;Community Forums&lt;/a&gt;, so that everyone in the community can see what will be discussed and voice their opinion here before Monday (this will make it easier to have an efficient meeting).&lt;/li&gt;
+ &lt;li style=&quot;text-align: left;&quot;&gt;Please do so as soon as you can before the meeting, so that people have time to read, think, and reply (and also add it to the agenda).&lt;/li&gt;
+ &lt;li style=&quot;text-align: left;&quot;&gt;If you can, please attend the meeting in person (or via IRC), so we can follow up on your discussion topic during the meeting with your feedback.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;&lt;strong class=&quot;author-g-ivsra51ph44x461i&quot;&gt;Developers&lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;The new version of the Ask A Question page is here!&lt;/li&gt;
+ &lt;li&gt;The 2.0 version of the KPI dashboard is in the works.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://edwin.mozilla.io/t/sumo&quot; target=&quot;_blank&quot;&gt;You can see the current state of the backlog our developers are working on here&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://public.etherpad-mozilla.org/p/sumo-p-2016-01-14&quot; target=&quot;_blank&quot;&gt;The latest SUMO Platform meeting notes can be found here&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;Interested in learning how Kitsune (the engine behind SUMO) works? &lt;a href=&quot;http://kitsune.readthedocs.org/&quot; target=&quot;_blank&quot;&gt;Read more about it here&lt;/a&gt; and &lt;a href=&quot;https://github.com/mozilla/kitsune/&quot; target=&quot;_blank&quot;&gt;fork it on GitHub&lt;/a&gt;!&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;&lt;strong&gt;Community&lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Our awesome Bangladesh SUMO Warriors are on the road again! Follow their adventures on Twitter under this tag: &lt;a href=&quot;https://twitter.com/search?q=%23sumotourctg&quot; target=&quot;_blank&quot;&gt;#sumotourctg&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;
+ &lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://support.mozilla.org/forums/contributors/711729?last=67763&quot;&gt;Reminder: take a look at our Work Week Summary for Mozlando. We need your feedback for a few things there.&lt;/a&gt;&lt;/div&gt;
+ &lt;/li&gt;
+ &lt;li&gt;
+ &lt;div class=&quot;title&quot;&gt;Ongoing reminder: if you think you can benefit from getting &lt;a href=&quot;https://wiki.mozilla.org/Community_Hardware&quot; target=&quot;_blank&quot;&gt;a second-hand device&lt;/a&gt; to help you with contributing to SUMO, you know where to find us.&lt;/div&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;&lt;strong class=&quot;user-chip&quot; title=&quot;adriel0415&quot;&gt;Support Forum&lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Say hello to the new people on the forums!
+ &lt;ul&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-z87zkz70z39yz83zw7ykz89z3gz82zt&quot;&gt;&lt;a href=&quot;https://support.mozilla.org/user/Tomi55&quot; target=&quot;_blank&quot;&gt;Tomi55&lt;/a&gt; (Hungarian)&lt;/span&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-z87zkz70z39yz83zw7ykz89z3gz82zt&quot;&gt;&lt;a href=&quot;https://support.mozilla.org/user/jdc20181&quot; target=&quot;_blank&quot;&gt;jdc20181&lt;/a&gt; (English)&lt;/span&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-z87zkz70z39yz83zw7ykz89z3gz82zt&quot;&gt;&lt;a href=&quot;https://support.mozilla.org/user/andexi&quot; target=&quot;_blank&quot;&gt;andexi&lt;/a&gt; (Spanish)&lt;/span&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-z87zkz70z39yz83zw7ykz89z3gz82zt&quot;&gt;&lt;a href=&quot;https://support.mozilla.org/user/Qantas94Heavy&quot; target=&quot;_blank&quot;&gt;Qantas94Heavy&lt;/a&gt; (English)&lt;/span&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-z87zkz70z39yz83zw7ykz89z3gz82zt&quot;&gt;&lt;a href=&quot;https://support.mozilla.org/user/samuelms79&quot; target=&quot;_blank&quot;&gt;samuelms79&lt;/a&gt; (Brazilian-PT)&lt;/span&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-z87zkz70z39yz83zw7ykz89z3gz82zt&quot;&gt;&lt;a href=&quot;https://support.mozilla.org/user/jorgecomun&quot; target=&quot;_blank&quot;&gt;jorgecomun&lt;/a&gt; (Spanish)&lt;/span&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;div class=&quot;&quot;&gt;
+ &lt;h3&gt;&lt;strong class=&quot;author-g-ivsra51ph44x461i&quot;&gt;Knowledge Base&lt;/strong&gt;&lt;/h3&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid90&quot;&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid82&quot;&gt;
+ &lt;ul class=&quot;list-bullet1&quot;&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-z87zjz80zxwjz85z4z65zytdpz68zoz69z&quot;&gt;&lt;a href=&quot;https://support.mozilla.org/forums/knowledge-base-articles/711304#post-65289&quot; target=&quot;_blank&quot;&gt;Thanks to everyone who took part in the most recent KB Day!&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
+ &lt;li&gt;Version 44 updates should be live now.&lt;/li&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-w2dz70zaz70z7z89zqz78ziz69zz78zz85zz90zj&quot;&gt;&lt;a href=&quot;https://docs.google.com/spreadsheets/d/1lkpRPJp9P1P5MRU-c9dwbDC0w5bMmrMdu-BNMp1xe8w/edit#gid=6&quot; target=&quot;_blank&quot;&gt;Ongoing reminder: learn more about upcoming English article updates by clicking here&lt;/a&gt;&lt;/span&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;span style=&quot;text-decoration: underline;&quot;&gt;Ongoing reminder #2:&lt;a href=&quot;https://support.mozilla.org/forums/knowledge-base-articles/&quot; target=&quot;_blank&quot;&gt; do you have ideas about improving the KB guidelines and training materials? Let us know in the forums&lt;/a&gt;!&lt;/span&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid83&quot;&gt;
+ &lt;h3&gt;&lt;strong class=&quot;author-g-ivsra51ph44x461i&quot;&gt;Localization&lt;/strong&gt;&lt;/h3&gt;
+ &lt;/div&gt;
+ &lt;/div&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid95&quot;&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Thanks to everyone writing in with problems, ideas, reports of bugs – all your feedback matters!&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid75&quot;&gt;
+ &lt;h3&gt;&lt;strong&gt;Firefox&lt;br /&gt;
+ &lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;strong&gt;for Android&lt;/strong&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://support.mozilla.org/forums/contributors/711712?last=67653&quot;&gt;Learn more about Firefox 43 for Android from the official thread with release notes / issues / discussions&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;
+ &lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://support.mozilla.org/forums/contributors/711718?last=67822&quot;&gt;Reminder: Roland is sharing Firefox 44 for Android release notes / issues / discussions&lt;/a&gt; with everyone in the forum.&lt;/div&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;strong&gt;for Desktop&lt;/strong&gt;
+ &lt;ul&gt;
+ &lt;li&gt;The &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1238620&quot; target=&quot;_blank&quot;&gt;uploading issues reported by many users are being tracked here.&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://support.mozilla.org/questions/firefox?tagged=bug1208145&amp;amp;show=all&quot; target=&quot;_blank&quot;&gt;The “show passwords†button has been removed from the password manager for the Beta of Version 44&lt;/a&gt;. The developers are looking into &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1208145&quot; target=&quot;_blank&quot;&gt;last minute fixes for that in this bug&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;Also in Version 44, the &lt;span class=&quot;author-a-kz88zz80zhz89z6hlz81znytez70zz66zz68z&quot;&gt;&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=606655&quot; target=&quot;_blank&quot;&gt;“ask me everytime†option for cookies will be removed from the privacy panel.&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;strong&gt;for iOS&lt;/strong&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid85&quot;&gt;
+ &lt;ul class=&quot;list-bullet1&quot;&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-107uz69zz81zhz78z0z78zz84zz66zz76zz82zz77zj&quot;&gt;&lt;a href=&quot;https://www.mozilla.org/en-US/firefox/ios/1.4/releasenotes/&quot; target=&quot;_blank&quot;&gt;Firefox for iOS 1.4 primarily with features for China is here&lt;/a&gt;.&lt;br /&gt;
+ &lt;/span&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid86&quot;&gt;
+ &lt;ul class=&quot;list-bullet1&quot;&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-107uz69zz81zhz78z0z78zz84zz66zz76zz82zz77zj&quot;&gt;Firefox for iOS 2.0 is after 1.4 and hopefully sometime this quarter!&lt;/span&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+ &lt;p&gt;Not that many updates this week, since we’re coming out of our winter slumber (even though winter will be here for a while, still) and plotting an awesome 2016 with you and for you. Take it easy, have a great weekend and see you around SUMO.&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-15T19:38:51+00:00</dc:date>
+ <dc:creator>Michał</dc:creator>
+ </item>
+ <item rdf:about="https://air.mozilla.org/paris-firefox-os-hackathon-presentations/">
+ <title>Air Mozilla: Paris Firefox OS Hackathon Presentations</title>
+ <link>https://air.mozilla.org/paris-firefox-os-hackathon-presentations/</link>
+ <content:encoded>&lt;p&gt;
+ &lt;img alt=&quot;Paris Firefox OS Hackathon Presentations&quot; class=&quot;wp-post-image&quot; height=&quot;90&quot; src=&quot;https://air.cdn.mozilla.net/media/cache/35/83/358305bfa246fff07d707061082134aa.png&quot; width=&quot;160&quot; /&gt;
+ As an introduction to this weekend's Firefox OS Hackathon in Paris we'll have two presentations: - Guillaume Marty will talk about the current state of...
+ &lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-15T18:00:00+00:00</dc:date>
+ <dc:creator>Air Mozilla</dc:creator>
+ </item>
+ <item rdf:about="https://tacticalsecret.com/tag/mozilla/rss/db7fec0c-34d3-4633-9904-79b98aab34e7">
+ <title>J.C. Jones: Renewing Let's Encrypt Certs (Nginx)</title>
+ <link>https://tacticalsecret.com/renewing-lets-encrypt-certs-nginx/</link>
+ <content:encoded>&lt;p&gt;All the first &lt;a href=&quot;https://crt.sh/?id=10172479&quot;&gt;Let's Encrypt certs for my websites&lt;/a&gt; from the LE private beta began expiring last week, so it was time to work through the renewal tooling. I wanted a script that:&lt;/p&gt;
+
+ &lt;ol&gt;
+ &lt;li&gt;Would be okay to run daily, so there'd be plenty of retries if something went wrong, &lt;/li&gt;
+ &lt;li&gt;Wouldn't require extra config for me to forget about if I add a new site, &lt;/li&gt;
+ &lt;li&gt;Would only renew certificates expiring in the next few weeks.&lt;/li&gt;
+ &lt;/ol&gt;
+
+ &lt;p&gt;The official Let's Encrypt client team is hard at work producing a great renew tool to handle all this, but it's not released yet. Of course I could use &lt;a href=&quot;https://caddyserver.com/&quot;&gt;Caddy Server&lt;/a&gt; that &lt;a href=&quot;https://www.youtube.com/watch?v=nk4EWHvvZtI&quot;&gt;just handles all this&lt;/a&gt;, but I have a lot invested in Nginx here.&lt;/p&gt;
+
+ &lt;p&gt;So I wrote a short script and &lt;a href=&quot;https://gist.github.com/jcjones/432eeaa6a2bf25e2c746&quot;&gt;put it up in a Gist&lt;/a&gt;. &lt;/p&gt;
+
+ &lt;p&gt;The script is designed to run daily, with a random start between 00:00 and 02:00 to protect against load spikes at Let's Encrypt's infrastructure. It doesn't do any real reporting, though, except to maintain &lt;code&gt;/var/log/letsencrypt/renew.log&lt;/code&gt; as the most-recent failure if one fails.&lt;/p&gt;
+
+ &lt;p&gt;It's written to handle Nginx with Upstart's &lt;code&gt;service&lt;/code&gt; command. It's pretty modular though; you could make this operate any webserver, or use the webroot method quite easily. Feel free to use the OpenSSL SubjectAlternativeName processing code for whatever purposes you have.&lt;/p&gt;
+
+ &lt;p&gt;Happy renewing!&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-15T16:01:19+00:00</dc:date>
+ <dc:creator>James 'J.C.' Jones</dc:creator>
+ </item>
+ <item rdf:about="http://firefoxmania.uci.cu/?p=15521">
+ <title>Yunier José Sosa Vázquez: Conoce los complementos destacados para enero</title>
+ <link>http://firefoxmania.uci.cu/conoce-los-complementos-destacados-para-enero-2016/</link>
+ <content:encoded>&lt;p style=&quot;text-align: left;&quot;&gt;Comenzó un nuevo año y con él, te traemos nuevos e interesantes complementos para tu navegador preferido que mejoran con creces tu experiencia de navegación. Durante los próximos 6 meses estará trabajando nuevos miembros en el Add-ons Board Team, en la próxima selección desde Firefoxmanía te avisaremos.&lt;/p&gt;
+ &lt;h3 style=&quot;text-align: left;&quot;&gt;Elección del mes: uMatrix&lt;/h3&gt;
+ &lt;p&gt;uMatrix es muy parecido a un &lt;em&gt;firewall&lt;/em&gt; y desde una ventana fácilmente podrás controlar todos los lugares a donde tu navegador tiene permitido conectarse, qué tipo de datos pueden descargarse y cual puede ejecutar.&lt;/p&gt;
+ &lt;blockquote&gt;&lt;p&gt;Esta puede ser la extensión perfecta para el control avanzado de los usuarios.&lt;/p&gt;&lt;/blockquote&gt;
+ &lt;p&gt;&lt;span id=&quot;more-15521&quot;&gt;&lt;/span&gt;&lt;/p&gt;
+
+ &lt;a href=&quot;http://firefoxmania.uci.cu/conoce-los-complementos-destacados-para-enero-2016/umatrix/&quot;&gt;&lt;img alt=&quot;Interfaz principal de uMatrix&quot; class=&quot;attachment-thumbnail size-thumbnail&quot; height=&quot;160&quot; src=&quot;http://firefoxmania.uci.cu/wp-content/uploads/2016/01/uMatrix-160x160.png&quot; width=&quot;160&quot; /&gt;&lt;/a&gt;
+ &lt;a href=&quot;http://firefoxmania.uci.cu/conoce-los-complementos-destacados-para-enero-2016/umatrix2/&quot;&gt;&lt;img alt=&quot;Opciones de configuración de uMatrix&quot; class=&quot;attachment-thumbnail size-thumbnail&quot; height=&quot;160&quot; src=&quot;http://firefoxmania.uci.cu/wp-content/uploads/2016/01/uMatrix2-160x160.png&quot; width=&quot;160&quot; /&gt;&lt;/a&gt;
+
+ &lt;p&gt;&lt;em&gt;&lt;a href=&quot;http://addons.firefoxmania.uci.cu/umatrix/&quot; target=&quot;_blank&quot;&gt;Instalar uMatrix »&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
+ &lt;h3&gt;También te recomendamos&lt;/h3&gt;
+ &lt;p style=&quot;text-align: left;&quot;&gt;&lt;a href=&quot;http://addons.firefoxmania.uci.cu/https-everywhere/&quot; target=&quot;_blank&quot;&gt;⇒ HTTPS Everywhere&lt;/a&gt; por &lt;a href=&quot;https://addons.mozilla.org/en-US/firefox/user/eff-technologists/&quot; title=&quot;EFF Technologists&quot;&gt;EFF Technologists&lt;/a&gt;&lt;/p&gt;
+ &lt;p style=&quot;text-align: left;&quot;&gt;Protege tus comunicaciones habilitando la encriptación HTTPS automáticamente en los sitios conocidos que la soportan, incluso cuando navegas mediante sitios que no incluyen el prefijo “https†en la URL.&lt;/p&gt;
+ &lt;p style=&quot;text-align: left;&quot;&gt;&lt;a href=&quot;http://addons.firefoxmania.uci.cu/add-to-search-bar/&quot; target=&quot;_blank&quot;&gt;⇒ Add to Search Bar&lt;/a&gt; por &lt;a href=&quot;https://addons.mozilla.org/firefox/user/dr-evil/&quot; target=&quot;_blank&quot; title=&quot;AdblockLite&quot;&gt;Dr. Evil&lt;/a&gt;&lt;/p&gt;
+ &lt;p style=&quot;text-align: left;&quot;&gt;Hace posible que cualquier página con un formulario de búsqueda disponible pueda ser añadido fácilmente a la barra de búsqueda de Firefox.&lt;/p&gt;
+ &lt;div class=&quot;wp-caption aligncenter&quot; id=&quot;attachment_15528&quot; style=&quot;width: 262px;&quot;&gt;&lt;a href=&quot;http://firefoxmania.uci.cu/wp-content/uploads/2016/01/add_to_search_bar.png&quot; rel=&quot;attachment wp-att-15528&quot;&gt;&lt;img alt=&quot;add_to_search_bar&quot; class=&quot;wp-image-15528 size-medium&quot; height=&quot;226&quot; src=&quot;http://firefoxmania.uci.cu/wp-content/uploads/2016/01/add_to_search_bar-252x226.png&quot; width=&quot;252&quot; /&gt;&lt;/a&gt;&lt;p class=&quot;wp-caption-text&quot;&gt;Añadiendo la búsqueda de un sitio web a la barra de búsqueda&lt;/p&gt;&lt;/div&gt;
+ &lt;p style=&quot;text-align: left;&quot;&gt;&lt;a href=&quot;http://addons.firefoxmania.uci.cu/duplicate-tabs-closer/&quot; target=&quot;_blank&quot;&gt;⇒ Duplicate Tabs Closer&lt;/a&gt; por &lt;a href=&quot;https://addons.mozilla.org/firefox/user/peuj/&quot; target=&quot;_blank&quot; title=&quot;The 1-Click YouTube Video Download Team&quot;&gt;Peuj&lt;/a&gt;&lt;/p&gt;
+ &lt;p style=&quot;text-align: left;&quot;&gt;Detecta las pestañas duplicadas en tu navegador y automáticamente las cierra.&lt;/p&gt;
+ &lt;h3 style=&quot;text-align: left;&quot;&gt;Nomina tus complementos favoritos&lt;/h3&gt;
+ &lt;p style=&quot;text-align: left;&quot;&gt;A nosotros nos encantaría que &lt;strong&gt;fueras parte del proceso&lt;/strong&gt; de seleccionar los mejores complementos para Firefox y nos gustaría escucharte. &lt;em&gt;¿No sabes cómo?&lt;/em&gt; Sólo tienes que &lt;em&gt;enviar un correo electrónico&lt;/em&gt; a la dirección &lt;strong&gt;amo-featured@mozilla.org&lt;/strong&gt; con el nombre del complemento o el archivo de instalación y los miembros evaluarán tu recomendación.&lt;/p&gt;
+ &lt;p style=&quot;text-align: left;&quot;&gt;&lt;strong&gt;Fuente:&lt;/strong&gt; &lt;a href=&quot;https://blog.mozilla.org/addons/2016/01/01/january-2016-featured-add-ons/&quot; target=&quot;_blank&quot;&gt;Mozilla Add-ons Blog&lt;/a&gt;&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-15T15:10:26+00:00</dc:date>
+ <dc:creator>Yunier J</dc:creator>
+ </item>
+ <item rdf:about="https://timtaubert.de/blog/2016/01/build-your-own-signal-desktop">
+ <title>Tim Taubert: Build Your Own Signal Desktop</title>
+ <link>https://timtaubert.de/blog/2016/01/build-your-own-signal-desktop/</link>
+ <content:encoded>&lt;p&gt;The Signal Private Messenger is great. &lt;strong&gt;Use it.&lt;/strong&gt; It’s probably the best secure
+ messenger on the market. When recently a desktop app was announced people were
+ eager to join the beta and even happier when an invite finally showed up in
+ their inbox. So was I, it’s a great app and works surprisingly well for an early
+ version.&lt;/p&gt;
+
+ &lt;p&gt;The only problem is that it’s a Chrome App. Apart from excluding folks with
+ other browsers it’s also a shitty user experience. If you too want your
+ messaging app not tied to a browser then let’s just build our own standalone
+ variant of Signal Desktop.&lt;/p&gt;
+
+ &lt;h3&gt;NW.js beta with Chrome App support&lt;/h3&gt;
+
+ &lt;p&gt;Signal Desktop is a Chrome App, so the easiest way to turn it into a standalone
+ app is to use &lt;a href=&quot;http://nwjs.io/&quot;&gt;NW.js&lt;/a&gt;. Conveniently, their next release v0.13
+ will ship with Chrome App support and is available for download as a beta
+ version.&lt;/p&gt;
+
+ &lt;p&gt;First, make sure you have &lt;code&gt;git&lt;/code&gt; and &lt;code&gt;npm&lt;/code&gt; installed. Then open a terminal and
+ prepare a temporary build directory to which we can download a few things and
+ where we can build the app:&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;$ mkdir signal-build
+ $ cd signal-build
+ &lt;/pre&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;h3&gt;[OS X] Packaging Signal and NW.js&lt;/h3&gt;
+
+ &lt;p&gt;Download the latest beta of NW.js and &lt;code&gt;unzip&lt;/code&gt; it. We’ll extract the application
+ and use it as a template for our Signal clone. The NW.js project does
+ unfortunately not seem to provide a secure source (or at least hashes)
+ for their downloads.&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;$ wget http://dl.nwjs.io/v0.13.0-beta3/nwjs-sdk-v0.13.0-beta3-osx-x64.zip
+ $ unzip nwjs-sdk-v0.13.0-beta3-osx-x64.zip
+ $ cp -r nwjs-sdk-v0.13.0-beta3-osx-x64/nwjs.app SignalPrivateMessenger.app
+ &lt;/pre&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;Next, clone the Signal repository and use NPM to install the necessary modules.
+ Run the &lt;code&gt;grunt&lt;/code&gt; automation tool to build the application.&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;$ git clone https://github.com/WhisperSystems/Signal-Desktop.git
+ $ cd Signal-Desktop/
+ $ npm install
+ $ node_modules/grunt-cli/bin/grunt
+ &lt;/pre&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;Finally, simply to copy the &lt;code&gt;dist&lt;/code&gt; folder containing all the juicy Signal files
+ into the application template we created a few moments ago.&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;$ cp -r dist ../SignalPrivateMessenger.app/Contents/Resources/app.nw
+ $ open ..
+ &lt;/pre&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;The last command opens a Finder window. Move &lt;code&gt;SignalPrivateMessenger.app&lt;/code&gt; to
+ your Applications folder and launch it as usual. You should now see a welcome
+ page!&lt;/p&gt;
+
+ &lt;h3&gt;[Linux] Packaging Signal and NW.js&lt;/h3&gt;
+
+ &lt;p&gt;The build instructions for Linux aren’t too different but I’ll write them down,
+ if just for convenience. Start by cloning the Signal Desktop repository and
+ build.&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;$ git clone https://github.com/WhisperSystems/Signal-Desktop.git
+ $ cd Signal-Desktop/
+ $ npm install
+ $ node_modules/grunt-cli/bin/grunt
+ &lt;/pre&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;The &lt;code&gt;dist&lt;/code&gt; folder contains the app, ready to be launched. &lt;code&gt;zip&lt;/code&gt; it and place
+ the resulting package somewhere handy.&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;$ cd dist
+ $ zip -r ../../package.nw *
+ &lt;/pre&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;Back to the top. Download the NW.js binary, extract it, and change into the
+ newly created directory. Move the &lt;code&gt;package.nw&lt;/code&gt; file we created earlier next to
+ the &lt;code&gt;nw&lt;/code&gt; binary and we’re done. The &lt;code&gt;nwjs-sdk-v0.13.0-beta3-linux-x64&lt;/code&gt; folder
+ does now contain the standalone Signal app.&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;$ cd ../..
+ $ wget http://dl.nwjs.io/v0.13.0-beta3/nwjs-sdk-v0.13.0-beta3-linux-x64.tar.gz
+ $ tar xfz nwjs-sdk-v0.13.0-beta3-linux-x64.tar.gz
+ $ cd nwjs-sdk-v0.13.0-beta3-linux-x64
+ $ mv ../package.nw .
+ &lt;/pre&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;Finally, launch NW.js. You should see a welcome page!&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;$ ./nw
+ &lt;/pre&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;h3&gt;If you see something, file something&lt;/h3&gt;
+
+ &lt;p&gt;Our standalone Signal clone mostly works, but it’s far from perfect. We’re
+ pulling from master and that might bring breaking changes that weren’t
+ sufficiently tested.&lt;/p&gt;
+
+ &lt;p&gt;We don’t have the right icons. The app crashes when you click a media message.
+ It opens a blank popup when you click a link. It’s quite big because also NW.js
+ has bugs and so we have to use the SDK build for now. In the future it would be
+ great to have automatic updates, and maybe even signed builds.&lt;/p&gt;
+
+ &lt;p&gt;Remember, Signal Desktop is beta, and completely untested with NW.js. If you
+ want to help file bugs, but only after checking that those affect the Chrome
+ App too. If you want to fix a bug only occurring in the standalone version
+ it’s probably best to file a pull request and cross fingers.&lt;/p&gt;
+
+ &lt;h3&gt;Is this secure?&lt;/h3&gt;
+
+ &lt;p&gt;Great question! I don’t know. I would love to get some more insights from people
+ that know more about the NW.js security model and whether it comes with all the
+ protections Chromium can offer. Another interesting question is whether bundling
+ Signal Desktop with NW.js is in any way worse (from a security perspective) than
+ installing it as a Chrome extension. If you happen to have an opinion about
+ that, I would love to hear it.&lt;/p&gt;
+
+ &lt;p&gt;Another important thing to keep in mind is that when building Signal on your
+ own you will possibly miss automatic and signed security updates from the
+ Chrome Web Store. Keep an eye on the repository and rebuild your app from
+ time to time to not fall behind too much.&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-15T14:00:00+00:00</dc:date>
+ </item>
+ <item rdf:about="http://glandium.org/blog/?p=3579">
+ <title>Mike Hommey: Announcing git-cinnabar 0.3.0</title>
+ <link>http://glandium.org/blog/?p=3579</link>
+ <content:encoded>&lt;p&gt;Git-cinnabar is a git remote helper to interact with mercurial repositories. It allows to clone, pull and push from/to mercurial remote repositories, using git.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;https://github.com/glandium/git-cinnabar&quot;&gt;Get it on github&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;These release notes are also &lt;a href=&quot;https://github.com/glandium/git-cinnabar/wiki/Release-Notes:-0.3.0&quot;&gt;available on the git-cinnabar wiki&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;Development had been stalled for a few months, with many improvements in the&lt;br /&gt;
+ &lt;code&gt;next&lt;/code&gt; branch without any new release. I used some time during the new year&lt;br /&gt;
+ break and after in order to straighten things up in order to create a new&lt;br /&gt;
+ release, delaying many of the originally planned changes to a future 0.4.0&lt;br /&gt;
+ release.&lt;/p&gt;
+ &lt;h3&gt;What’s new since 0.2.2?&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Speed and memory usage were improved when doing &lt;code&gt;git push&lt;/code&gt;.&lt;/li&gt;
+ &lt;li&gt;Now works on Windows, at least to some extent. See &lt;a href=&quot;http://glandium.org/blog/Windows-Support&quot;&gt;details&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;Support for pre-0.1.0 git-cinnabar repositories was removed. You must first&lt;br /&gt;
+ use a git-cinnabar version between 0.1.0 and 0.2.2 to upgrade its metadata.&lt;/li&gt;
+ &lt;li&gt;It is now possible to attach/graft git-cinnabar metadata to existing commits&lt;br /&gt;
+ matching mercurial changesets. This allows to migrate from some other&lt;br /&gt;
+ hg-to-git tool to git-cinnabar while preserving the existing git commits.&lt;br /&gt;
+ See &lt;a href=&quot;http://glandium.org/blog/Mozilla%3A-Using-a-git-clone-of-gecko%E2%80%90dev-to-push-to-mercurial&quot;&gt;an example of how this works with the git clone of the Gecko mercurial&lt;br /&gt;
+ repository&lt;/a&gt;
+ &lt;/li&gt;
+ &lt;li&gt;Avoid mercurial printing its progress bar, messing up with git-cinnabar’s&lt;br /&gt;
+ output.&lt;/li&gt;
+ &lt;li&gt;It is now possible to fetch from an incremental mercurial bundle (without&lt;br /&gt;
+ a root changeset).&lt;/li&gt;
+ &lt;li&gt;It is now possible to push to a new mercurial repository without &lt;code&gt;-f&lt;/code&gt;.&lt;/li&gt;
+ &lt;li&gt;By default, reject pushing a new root to a mercurial repository.&lt;/li&gt;
+ &lt;li&gt;Make the connection to a mercurial repository through ssh respect the&lt;br /&gt;
+ &lt;code&gt;GIT_SSH&lt;/code&gt; and &lt;code&gt;GIT_SSH_COMMAND&lt;/code&gt; environment variables.&lt;/li&gt;
+ &lt;li&gt;
+ &lt;code&gt;git cinnabar&lt;/code&gt; now has a proper argument parser for all its subcommands.&lt;/li&gt;
+ &lt;li&gt;
+ &lt;/li&gt;
+ &lt;li&gt;A new &lt;code&gt;git cinnabar python&lt;/code&gt; command allows to run python scripts or open a&lt;br /&gt;
+ python shell with the right sys.path to import the cinnabar module.&lt;/li&gt;
+ &lt;li&gt;All git-cinnabar metadata is now kept under a single ref (although for&lt;br /&gt;
+ convenience, other refs are created, but they can be derived if necessary).&lt;/li&gt;
+ &lt;li&gt;Consequently, a new &lt;code&gt;git cinnabar rollback&lt;/code&gt; command allows to roll back to&lt;br /&gt;
+ previous metadata states.&lt;/li&gt;
+ &lt;li&gt;git-cinnabar metadata now tracks the manifests DAG.&lt;/li&gt;
+ &lt;li&gt;A new &lt;code&gt;git cinnabar bundle&lt;/code&gt; command allows to create mercurial bundles,&lt;br /&gt;
+ mostly for debugging purposes, without requiring to hit a mercurial server.&lt;/li&gt;
+ &lt;li&gt;Updated git to 2.7.0 for the native helper.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;Development process changes&lt;/h3&gt;
+ &lt;p&gt;Up to before this release closing in, the &lt;code&gt;master&lt;/code&gt; branch was dedicated to&lt;br /&gt;
+ releases, and development was happening on the &lt;code&gt;next&lt;/code&gt; branch, until a new&lt;br /&gt;
+ release happens.&lt;/p&gt;
+ &lt;p&gt;From now on, the &lt;code&gt;release&lt;/code&gt; branch will take dot-release fixes and new&lt;br /&gt;
+ releases, while the &lt;code&gt;master&lt;/code&gt; branch will receive all changes that are&lt;br /&gt;
+ validated through testing (currently semi-automatically tested with&lt;br /&gt;
+ out-of-tree tests based on four real-life mercurial repositories, with&lt;br /&gt;
+ some automated CI based on in-tree tests used in the future).&lt;/p&gt;
+ &lt;p&gt;The &lt;code&gt;next&lt;/code&gt; branch will receive changes to be tested in CI when things&lt;br /&gt;
+ will be hooked up, and may have rewritten history as a consequence of&lt;br /&gt;
+ wanting passing tests on every commit on &lt;code&gt;master&lt;/code&gt;.&lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-15T08:56:40+00:00</dc:date>
+ <dc:creator>glandium</dc:creator>
+ </item>
+ <item rdf:about="https://air.mozilla.org/web-qa-weekly-meeting-20160114/">
+ <title>Air Mozilla: Web QA Weekly Meeting, 14 Jan 2016</title>
+ <link>https://air.mozilla.org/web-qa-weekly-meeting-20160114/</link>
+ <content:encoded>&lt;p&gt;
+ &lt;img alt=&quot;Web QA Weekly Meeting&quot; class=&quot;wp-post-image&quot; height=&quot;90&quot; src=&quot;https://air.cdn.mozilla.net/media/cache/f5/13/f5137857516694df0458e837c2d3a4be.png&quot; width=&quot;160&quot; /&gt;
+ This is our weekly gathering of Mozilla'a Web QA team filled with discussion on our current and future projects, ideas, demos, and fun facts.
+ &lt;/p&gt;</content:encoded>
+ <dc:date>2016-01-14T17:00:00+00:00</dc:date>
+ <dc:creator>Air Mozilla</dc:creator>
+ </item>
+
+</rdf:RDF>
diff --git a/mobile/android/tests/background/junit4/resources/feed_rss20_planetmozilla.xml b/mobile/android/tests/background/junit4/resources/feed_rss20_planetmozilla.xml
new file mode 100644
index 0000000000..a3447ab8ac
--- /dev/null
+++ b/mobile/android/tests/background/junit4/resources/feed_rss20_planetmozilla.xml
@@ -0,0 +1,3853 @@
+<?xml version="1.0"?>
+<rss version="2.0"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:atom="http://www.w3.org/2005/Atom"
+ >
+
+ <channel>
+ <title>Planet Mozilla</title>
+ <link>http://planet.mozilla.org/</link>
+ <language>en</language>
+ <description>Planet Mozilla - http://planet.mozilla.org/</description>
+ <atom:link rel="self" href="http://planet.mozilla.org/rss20.xml" type="application/rss+xml"/>
+
+ <item>
+ <title>Aaron Klotz: Announcing Mozdbgext</title>
+ <guid isPermaLink="false">http://dblohm7.ca/blog/2016/01/26/announcing-mozdbgext</guid>
+ <link>http://dblohm7.ca/blog/2016/01/26/announcing-mozdbgext/</link>
+ <description>&lt;p&gt;A well-known problem at Mozilla is that, while most of our desktop users run
+ Windows, most of Mozilla’s developers do not. There are a lot of problems that
+ result from that, but one of the most frustrating to me is that sometimes
+ those of us that actually use Windows for development find ourselves at a
+ disadvantage when it comes to tooling or other productivity enhancers.&lt;/p&gt;
+
+ &lt;p&gt;In many ways this problem is also a Catch-22: People don’t want to use Windows
+ for many reasons, but tooling is big part of the problem. OTOH, nobody is
+ motivated to improve the tooling situation if nobody is actually going to
+ use them.&lt;/p&gt;
+
+ &lt;p&gt;A couple of weeks ago my frustrations with the situation boiled over when I
+ learned that our &lt;code&gt;Cpp&lt;/code&gt; unit test suite could not log symbolicated call stacks,
+ resulting in my filing of &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1238305&quot; title=&quot;cppunittests do not look up breakpad symbols for logged stack traces&quot;&gt;bug 1238305&lt;/a&gt; and &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1240605&quot; title=&quot;Set _NT_SYMBOL_PATH on Windows test machines&quot;&gt;bug 1240605&lt;/a&gt;. Not only could we
+ not log those stacks, in many situations we could not view them in a debugger
+ either.&lt;/p&gt;
+
+ &lt;p&gt;Due to the fact that PDB files consume a large amount of disk space, we don’t
+ keep those when building from integration or try repositories. Unfortunately
+ they are be quite useful to have when there is a build failure. Most of our
+ integration builds, however, do include breakpad symbols. Developers may also
+ explicitly &lt;a href=&quot;https://wiki.mozilla.org/ReleaseEngineering/TryServer#Getting_debug_symbols&quot;&gt;request symbols&lt;/a&gt;
+ for their try builds.&lt;/p&gt;
+
+ &lt;p&gt;A couple of years ago I had begun working on a WinDbg debugger extension that
+ was tailored to Mozilla development. It had mostly bitrotted over time, but I
+ decided to resurrect it for a new purpose: to help WinDbg&lt;sup&gt;&lt;a href=&quot;http://dblohm7.ca/atom.xml#fn1&quot; id=&quot;r1&quot;&gt;*&lt;/a&gt;&lt;/sup&gt;
+ grok breakpad.&lt;/p&gt;
+
+ &lt;h3&gt;Enter mozdbgext&lt;/h3&gt;
+
+ &lt;p&gt;&lt;a href=&quot;https://github.com/dblohm7/mozdbgext&quot;&gt;&lt;code&gt;mozdbgext&lt;/code&gt;&lt;/a&gt; is the result. This extension
+ adds a few commands that makes Win32 debugging with breakpad a little bit easier.&lt;/p&gt;
+
+ &lt;p&gt;The original plan was that I wanted &lt;code&gt;mozdbgext&lt;/code&gt; to load breakpad symbols and then
+ insert them into the debugger’s symbol table via the &lt;a href=&quot;https://msdn.microsoft.com/en-us/library/windows/hardware/ff537943%28v=vs.85%29.aspx&quot;&gt;&lt;code&gt;IDebugSymbols3::AddSyntheticSymbol&lt;/code&gt;&lt;/a&gt;
+ API. Unfortunately the design of this API is not well equipped for bulk loading
+ of synthetic symbols: each individual symbol insertion causes the debugger to
+ re-sort its entire symbol table. Since &lt;code&gt;xul.dll&lt;/code&gt;’s quantity of symbols is in the
+ six-figure range, using this API to load that quantity of symbols is
+ prohibitively expensive. I tweeted a Microsoft PM who works on Debugging Tools
+ for Windows, asking if there would be any improvements there, but it sounds like
+ this is not going to be happening any time soon.&lt;/p&gt;
+
+ &lt;p&gt;My original plan would have been ideal from a UX perspective: the breakpad
+ symbols would look just like any other symbols in the debugger and could be
+ accessed and manipulated using the same set of commands. Since synthetic symbols
+ would not work for me in this case, I went for “Plan B:†Extension commands that
+ are separate from, but analagous to, regular WinDbg commands.&lt;/p&gt;
+
+ &lt;p&gt;I plan to continuously improve the commands that are available. Until I have a
+ proper README checked in, I’ll introduce the commands here.&lt;/p&gt;
+
+ &lt;h4&gt;Loading the Extension&lt;/h4&gt;
+
+ &lt;ol&gt;
+ &lt;li&gt;Use the &lt;code&gt;.load&lt;/code&gt; command: &lt;code&gt;.load &amp;lt;path_to_mozdbgext_dll&amp;gt;&lt;/code&gt;&lt;/li&gt;
+ &lt;/ol&gt;
+
+
+ &lt;h4&gt;Loading the Breakpad Symbols&lt;/h4&gt;
+
+ &lt;ol&gt;
+ &lt;li&gt;Extract the breakpad symbols into a directory.&lt;/li&gt;
+ &lt;li&gt;In the debugger, enter &lt;code&gt;!bploadsyms &amp;lt;path_to_breakpad_symbol_directory&amp;gt;&lt;/code&gt;&lt;/li&gt;
+ &lt;li&gt;Note that this command will take some time to load all the relevant symbols.&lt;/li&gt;
+ &lt;/ol&gt;
+
+
+ &lt;h4&gt;Working with Breakpad Symbols&lt;/h4&gt;
+
+ &lt;p&gt;&lt;strong&gt;Note: You must have successfully run the &lt;code&gt;!bploadsyms&lt;/code&gt; command first!&lt;/strong&gt;&lt;/p&gt;
+
+ &lt;p&gt;As a general guide, I am attempting to name each breakpad command similarly to
+ the native WinDbg command, except that the command name is prefixed by &lt;code&gt;!bp&lt;/code&gt;.&lt;/p&gt;
+
+ &lt;ul&gt;
+ &lt;li&gt;Stack trace: &lt;code&gt;!bpk&lt;/code&gt;&lt;/li&gt;
+ &lt;li&gt;Find nearest symbol to address: &lt;code&gt;!bpln &amp;lt;address&amp;gt;&lt;/code&gt; where &lt;em&gt;address&lt;/em&gt; is specified
+ as a hexadecimal value.&lt;/li&gt;
+ &lt;/ul&gt;
+
+
+ &lt;h4&gt;Downloading windbgext&lt;/h4&gt;
+
+ &lt;p&gt;I have pre-built a &lt;a href=&quot;https://github.com/dblohm7/mozdbgext/blob/master/bin/mozdbgext.dll?raw=true&quot;&gt;32-bit binary&lt;/a&gt;
+ (which obviously requires 32-bit WinDbg). I have not built a 64-bit binary yet,
+ but the code should be source compatible.&lt;/p&gt;
+
+ &lt;p&gt;Note that there are several other commands that are “roughed-in†at this point
+ and do not work correctly yet. Please stick to the documented commands at this
+ time.&lt;/p&gt;
+
+ &lt;hr /&gt;
+
+ &lt;p&gt;&lt;sup&gt;&lt;a href=&quot;http://dblohm7.ca/atom.xml#r1&quot; id=&quot;fn1&quot;&gt;*&lt;/a&gt;&lt;/sup&gt; When I write “WinDbgâ€, I am really
+ referring to any debugger in the &lt;em&gt;Debugging Tools for Windows&lt;/em&gt; package,
+ including &lt;code&gt;cdb&lt;/code&gt;.&lt;/p&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 19:45:00 +0000</pubDate>
+ </item>
+ <item>
+ <title>Yunier José Sosa Vázquez: Soporte para WebM/VP9, más seguridad y nuevas herramientas para desarrolladores en el nuevo Firefox</title>
+ <guid isPermaLink="false">http://firefoxmania.uci.cu/?p=15548</guid>
+ <link>http://firefoxmania.uci.cu/soporte-para-webmvp9-mas-seguridad-y-nuevas-herramientas-para-desarrolladores-en-el-nuevo-firefox/</link>
+ <description>&lt;p style=&quot;text-align: left;&quot;&gt;¡Como pasa el tiempo amigos! Casi sin darnos cuenta han transcurrido 6 semanas y hasta hemos comenzado un año nuevo, un año en el que Mozilla prepara nuevas funcionalidades que harán de Firefox un mejor como por ejemplo: la &lt;a href=&quot;http://firefoxmania.uci.cu/como-se-hace-activar-electrolysis-en-firefox/&quot; target=&quot;_blank&quot;&gt;separación de procesos&lt;/a&gt;, el uso de vías alternas para &lt;a href=&quot;http://firefoxmania.uci.cu/el-futuro-de-los-plugins-npapi-en-firefox/&quot; target=&quot;_blank&quot;&gt;ejecutar plugins&lt;/a&gt; y la nueva API para desarrollar &lt;a href=&quot;http://firefoxmania.uci.cu/el-futuro-de-los-complementos-en-firefox/&quot; target=&quot;_blank&quot;&gt;complementos “multi navegadorâ€&lt;/a&gt;.&lt;/p&gt;
+ &lt;p style=&quot;text-align: left;&quot;&gt;Desde el anuncio en 2010 del formato de video WebM, &lt;a href=&quot;https://blog.mozilla.org/blog/2010/05/19/open-web-open-video-and-webm/&quot; target=&quot;_blank&quot;&gt;Mozilla ha mostrado un especial interés&lt;/a&gt; al ser una alternativa potente frente a los formatos propietarios del mercado que existían en aquel momento y de esta forma mejorar la experiencia de los usuarios al reproducir videos en la web. Con esta liberación se ha habilitado el &lt;strong&gt;soporte para WebM/VP9 en aquellos sistemas que no soportan MP4/H.264&lt;/strong&gt;.&lt;/p&gt;
+ &lt;p style=&quot;text-align: left;&quot;&gt;Desde algunas versiones atrás, Firefox incluye el plugin &lt;a href=&quot;http://andreasgal.com/2014/10/14/openh264-now-in-firefox/&quot; target=&quot;_blank&quot;&gt;OpenH264 proveído por Cisco&lt;/a&gt; para cumplir las especificaciones de WebRTC y habilitar las llamadas con dispositivos que lo requieran. Ahora, si el &lt;strong&gt;decodificador de H.264 está disponible&lt;/strong&gt; en el sistema, entonces se habilita este codec de video.&lt;span id=&quot;more-15548&quot;&gt;&lt;/span&gt;&lt;/p&gt;
+ &lt;h3 style=&quot;text-align: left;&quot;&gt;&lt;em&gt;Novedades para desarrolladores&lt;/em&gt;&lt;/h3&gt;
+ &lt;p style=&quot;text-align: left;&quot;&gt;En esta oportunidad, los desarrolladores podrán contar con herramientas de animación y filtros CSS, informes sobre consumo de memoria, depuración de WebSocket y más. Todo esto puedes leerlo en &lt;a href=&quot;https://www.mozilla-hispano.org/edicion-para-desarrolladores-44-editor-visual-manejo-de-memoria/&quot; target=&quot;_blank&quot;&gt;el blog de Labs&lt;/a&gt; de Mozilla Hispano.&lt;/p&gt;
+ &lt;h3 style=&quot;text-align: left;&quot;&gt;&lt;em&gt;Novedades en Android&lt;/em&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Los usuarios pueden elegir la página de inicio a mostrar, en vez de los sitios más visitados.&lt;/li&gt;
+ &lt;li&gt;El servicio de impresión de Android permite activar la impresión en la nube.&lt;/li&gt;
+ &lt;li&gt;Al &lt;a href=&quot;https://developer.chrome.com/multidevice/android/intents&quot; target=&quot;_blank&quot;&gt;intentar abrir una URIs&lt;/a&gt;, se le pregunta al usuario si desea abrirla en una pestaña privada.&lt;/li&gt;
+ &lt;li&gt;Adicionado el soporte para ejecutar URIs con el protocolo mms.&lt;/li&gt;
+ &lt;li&gt;Fácil acceso a la configuración de la búsqueda mientras buscamos en Internet.&lt;/li&gt;
+ &lt;li&gt;Ahora se muestran las sugerencias del historial de búsqueda.&lt;/li&gt;
+ &lt;li&gt;La página Cuentas Firefox ahora está basada en la web.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;&lt;em&gt;Otras novedades&lt;/em&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;El soporte para el algoritmo criptográfico RC4 ha sido removido.&lt;/li&gt;
+ &lt;li&gt;Soporte para el formato de compresión brotli cuando se usa HTTPS.&lt;/li&gt;
+ &lt;li&gt;Uso de un certificado de firmado SHA256 para las versiones de Windows en aras de adaptarse a los nuevos requerimientos.&lt;/li&gt;
+ &lt;li&gt;Para soportar el descriptor unicode-range de las fuentes web, el algoritmo de concordancia en Linux usa el mismo código como en las demás plataformas.&lt;/li&gt;
+ &lt;li&gt;Firefox no confiará más en la autoridad de certificación Equifax Secure Certificate Authority 1024-bit root o UTN – DATACorp SGC para validar &lt;a href=&quot;https://support.mozilla.org/ta/kb/secure-website-certificate&quot; target=&quot;_blank&quot;&gt;certificados web seguros&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;El soporte para el teclado en pantalla ha sido temporalmente desactivado en Windows 8 y 8.1.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;Si deseas conocer más, puedes leer las &lt;a href=&quot;http://www.mozilla.org/en-US/firefox/44.0/releasenotes/&quot; target=&quot;_blank&quot;&gt;notas de lanzamiento&lt;/a&gt; (en inglés) para conocer más novedades.&lt;/p&gt;
+ &lt;p&gt;&lt;strong&gt;Aclaración para la versión móvil.&lt;/strong&gt;&lt;/p&gt;
+ &lt;p&gt;En las descargas se pueden encontrar 3 versiones para Android. El archivo que contiene &lt;strong&gt;i386&lt;/strong&gt; es para los dispositivos que tengan la &lt;strong&gt;arquitectura de Intel&lt;/strong&gt;. Mientras que en los nombrados &lt;strong&gt;arm&lt;/strong&gt;, el que dice &lt;strong&gt;api11 funciona con Honeycomb (3.0) o superior&lt;/strong&gt; y el de &lt;strong&gt;api9 es para Gingerbread (2.3)&lt;/strong&gt;.&lt;/p&gt;
+ &lt;p&gt;Puedes obtener esta versión desde nuestra &lt;a href=&quot;http://firefoxmania.uci.cu/download/&quot; target=&quot;_blank&quot;&gt;zona de Descargas&lt;/a&gt; en español e inglés para Linux, Mac, Windows y Android. Recuerda que para navegar a través de servidores proxy debes modificar la preferencia &lt;strong&gt;network.auth.force-generic-ntlm&lt;/strong&gt; a &lt;code&gt;true&lt;/code&gt; desde &lt;a target=&quot;_blank&quot;&gt;about:config&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;Si te ha gustado, por favor comparte con tus amigos esta noticia en las redes sociales. No dudes en dejarnos un comentario.&lt;/p&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 18:56:54 +0000</pubDate>
+ <dc:creator>Yunier J</dc:creator>
+ </item>
+ <item>
+ <title>QMO: Firefox 45.0 Beta 3 Testday, February 5th</title>
+ <guid isPermaLink="false">https://quality.mozilla.org/?p=49454</guid>
+ <link>https://quality.mozilla.org/2016/01/firefox-45-0-beta-3-testday-february-5th/</link>
+ <description>&lt;p&gt;Hello Mozillians,&lt;/p&gt;
+ &lt;p&gt;We are happy to announce that &lt;strong&gt;Friday, February 5th&lt;/strong&gt;, we are organizing &lt;strong&gt;Firefox 45.0 Beta 3 Testday&lt;/strong&gt;. We will be focusing our testing on the following features: &lt;em&gt;Search Refactoring, Synced Tabs Menu, Text to Speech and Grouped Tabs Migration&lt;/em&gt;. Check out the detailed instructions via &lt;a href=&quot;https://public.etherpad-mozilla.org/p/testday-20160205&quot; target=&quot;_blank&quot;&gt;this etherpad&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;No previous testing experience is required, so feel free to join us on &lt;strong&gt;&lt;a href=&quot;http://widget01.mibbit.com/?server=irc.mozilla.org&amp;amp;channel=%23qa&quot;&gt;#qa IRC channel&lt;/a&gt;&lt;/strong&gt; where our moderators will offer you guidance and answer your questions.&lt;/p&gt;
+ &lt;p&gt;Join us and help us make Firefox better! See you on &lt;strong&gt;Friday&lt;/strong&gt;!&lt;/p&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 14:40:55 +0000</pubDate>
+ <dc:creator>vasilica.mihasca</dc:creator>
+ </item>
+ <item>
+ <title>David Lawrence: Happy BMO Push Day!</title>
+ <guid isPermaLink="false">http://dlawrence.wordpress.com/?p=29</guid>
+ <link>https://dlawrence.wordpress.com/2016/01/26/happy-bmo-push-day-4/</link>
+ <description>&lt;p&gt;the following changes have been pushed to bugzilla.mozilla.org:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;[&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1240575&quot; target=&quot;_blank&quot;&gt;1240575&lt;/a&gt;] Update form.reps.budget&lt;/li&gt;
+ &lt;li&gt;[&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1226028&quot; target=&quot;_blank&quot;&gt;1226028&lt;/a&gt;] API for batching MozReview requests&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;discuss these changes on &lt;a href=&quot;https://lists.mozilla.org/listinfo/tools-bmo&quot; target=&quot;_blank&quot;&gt;mozilla.tools.bmo&lt;/a&gt;.&lt;/p&gt;&lt;br /&gt; &lt;a href=&quot;http://feeds.wordpress.com/1.0/gocomments/dlawrence.wordpress.com/29/&quot; rel=&quot;nofollow&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; src=&quot;http://feeds.wordpress.com/1.0/comments/dlawrence.wordpress.com/29/&quot; /&gt;&lt;/a&gt; &lt;img alt=&quot;&quot; border=&quot;0&quot; height=&quot;1&quot; src=&quot;https://pixel.wp.com/b.gif?host=dlawrence.wordpress.com&amp;amp;blog=58816&amp;amp;post=29&amp;amp;subd=dlawrence&amp;amp;ref=&amp;amp;feed=1&quot; width=&quot;1&quot; /&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 14:27:50 +0000</pubDate>
+ <dc:creator>dlawrence</dc:creator>
+ </item>
+ <item>
+ <title>Tanvi Vyas: Updated Firefox Security Indicators</title>
+ <guid isPermaLink="false">http://blog.mozilla.org/tanvi/?p=198</guid>
+ <link>https://blog.mozilla.org/tanvi/2016/01/26/updated-firefox-security-indicators/</link>
+ <description>&lt;p&gt;&lt;em&gt;This article has been coauthored by Aislinn Grigas, Senior Interaction Designer, Firefox Desktop&lt;/em&gt;&lt;br /&gt;
+ &lt;em&gt;Cross posting with &lt;a href=&quot;https://blog.mozilla.org/security/2015/11/03/updated-firefox-security-indicators-2/&quot;&gt;Mozilla’s Security Blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
+ &lt;p&gt;November 3, 2015&lt;/p&gt;
+ &lt;p&gt;Over the past few months, Mozilla has been improving the user experience of our privacy and security features in Firefox. One specific initiative has focused on the feedback shown in our address bar around a site’s security. The major changes are highlighted below along with the rationale behind each change.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;https://blog.mozilla.org/security/files/2015/10/combo-graph21.png&quot;&gt;&lt;img alt=&quot;&quot; class=&quot;alignnone wp-image-2045 size-full&quot; height=&quot;914&quot; src=&quot;https://blog.mozilla.org/security/files/2015/10/combo-graph21.png&quot; width=&quot;1518&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
+ &lt;h3&gt;Change to DV Certificate treatment in the address bar&lt;/h3&gt;
+ &lt;p&gt;Color and iconography is commonly used today to communicate to users when a site is secure. The most widely used patterns are coloring a lock icon and parts of the address bar green. This treatment has a straightforward rationale given green = good in most cultures. Firefox has historically used two different color treatments for the lock icon – a gray lock for &lt;a href=&quot;https://en.wikipedia.org/wiki/Domain-validated_certificate&quot;&gt;Domain-validated (DV) certificates&lt;/a&gt; and a green lock for &lt;a href=&quot;https://en.wikipedia.org/wiki/Extended_Validation_Certificate&quot;&gt;Extended Validation (EV) certificates&lt;/a&gt;. The average user is likely not going to understand this color distinction between EV and DV certificates. The overarching message we want users to take from both certificate states is that their connection to the site is secure. We’re therefore updating the color of the lock when a DV certificate is used to match that of an EV certificate.&lt;/p&gt;
+ &lt;p&gt;Although the same green icon will be used, the UI for a site using EV certificates will continue to differ from a site using a DV certificate. Specifically, EV certificates are used when &lt;a href=&quot;https://en.wikipedia.org/wiki/Certificate_authority&quot;&gt;Certificate Authorities (CA)&lt;/a&gt; verify the owner of a domain. Hence, we will continue to include the organization name verified by the CA in the address bar.&lt;/p&gt;
+ &lt;h3&gt;Changes to Mixed Content Blocker UI on HTTPS sites&lt;/h3&gt;
+ &lt;p&gt;A second change we’re introducing addresses what happens when a page served over a secure connection contains &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Security/MixedContent&quot;&gt;Mixed Content&lt;/a&gt;. Firefox’s Mixed Content Blocker proactively blocks &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Security/MixedContent#Mixed_active_content&quot;&gt;Mixed Active Content&lt;/a&gt; by default. Users historically saw a &lt;a href=&quot;https://people.mozilla.org/~tvyas/FigureA.jpg&quot;&gt;shield icon&lt;/a&gt; when Mixed Active Content was blocked and were given the option to disable the protection.&lt;/p&gt;
+ &lt;p&gt;Since the Mixed Content state is closely tied to site security, the information should be communicated in one place instead of having two separate icons. Moreover, we have seen that the &lt;a href=&quot;https://telemetry.mozilla.org/new-pipeline/dist.html#!cumulative=0&amp;amp;end_date=2015-09-17&amp;amp;keys=__none__!__none__!__none__&amp;amp;max_channel_version=beta%252F41&amp;amp;measure=MIXED_CONTENT_UNBLOCK_COUNTER&amp;amp;min_channel_version=null&amp;amp;product=Firefox&amp;amp;sanitize=1&amp;amp;sort_keys=submissions&amp;amp;start_date=2015-08-11&amp;amp;table=0&amp;amp;trim=1&amp;amp;use_submission_date=0&quot;&gt;number of times users override mixed content protection&lt;/a&gt; is slim, and hence the need for dedicated mixed content iconography is diminishing. Firefox is also using the shield icon for another feature in &lt;a href=&quot;https://support.mozilla.org/en-US/kb/private-browsing-use-firefox-without-history&quot;&gt;Private Browsing Mode&lt;/a&gt; and we want to avoid making the iconography ambiguous.&lt;/p&gt;
+ &lt;p&gt;The updated design that ships with Firefox 42 combines the lock icon with a warning sign which represents Mixed Content. When Firefox blocks Mixed Active Content, we retain the green lock since the HTTP content is blocked and hence the site remains secure.&lt;/p&gt;
+ &lt;p&gt;For users who want to learn more about a site’s security state, we have added an informational panel to further explain differences in page security. This panel appears anytime a user clicks on the lock icon in the address bar.&lt;/p&gt;
+ &lt;p&gt;Previously users could &lt;a href=&quot;https://people.mozilla.org/~tvyas/FigureB.jpg&quot;&gt;click on the shield icon&lt;/a&gt; in the rare case they needed to override mixed content protection. With this new UI, users can still do this by clicking the arrow icon to expose more information about the site security, along with a disable protection button.&lt;/p&gt;
+ &lt;div class=&quot;wp-caption alignnone&quot; id=&quot;attachment_2034&quot; style=&quot;width: 557px;&quot;&gt;&lt;a href=&quot;https://blog.mozilla.org/security/files/2015/10/mixed-active-content-click-and-subpanel.png&quot;&gt;&lt;img alt=&quot;mixed active content click and subpanel&quot; class=&quot;wp-image-2034 &quot; height=&quot;176&quot; src=&quot;https://blog.mozilla.org/security/files/2015/10/mixed-active-content-click-and-subpanel.png&quot; width=&quot;547&quot; /&gt;&lt;/a&gt;&lt;p class=&quot;wp-caption-text&quot;&gt;Users can click the lock with warning icon and proceed to disable Mixed Content Protection.&lt;/p&gt;&lt;/div&gt;
+ &lt;h3&gt;&lt;/h3&gt;
+ &lt;h3&gt;Loading Mixed Passive Content on HTTPS sites&lt;/h3&gt;
+ &lt;p&gt;There is a second category of Mixed Content called &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Security/MixedContent#Mixed_passivedisplay_content&quot;&gt;Mixed Passive Content&lt;/a&gt;. Firefox does not block Mixed Passive Content by default. However, when it is loaded on an HTTPS page, we let the user know with iconography and text. In previous versions of Firefox, we used a gray warning sign to reflect this case.&lt;/p&gt;
+ &lt;p&gt;We have updated this iconography in Firefox 42 to a gray lock with a yellow warning sign. We degrade the lock from green to gray to emphasize that the site is no longer completely secure. In addition, we use a vibrant color for the warning icon to amplify that there is something wrong with the security state of the page.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;https://blog.mozilla.org/security/files/2015/10/mixed-passive-click1.png&quot;&gt;&lt;img alt=&quot;&quot; class=&quot;alignnone wp-image-2042 &quot; height=&quot;100&quot; src=&quot;https://blog.mozilla.org/security/files/2015/10/mixed-passive-click1-600x221.png&quot; width=&quot;268&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
+ &lt;p&gt;We also use this iconography when the certificate or TLS connection used by the website relies on deprecated cryptographic algorithms.&lt;/p&gt;
+ &lt;p&gt;The above changes will be rolled out in Firefox 42. Overall, the design improvements make it simpler for our users to understand whether or not their interactions with a site are secure.&lt;/p&gt;
+ &lt;h3&gt;Firefox Mobile&lt;/h3&gt;
+ &lt;p&gt;We have made similar changes to the site security indicators in Firefox for Android, which you can learn more about &lt;a href=&quot;https://support.mozilla.org/en-US/kb/mixed-content-blocker-firefox-android#w_how-do-i-know-if-a-page-has-mixed-content&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 05:58:29 +0000</pubDate>
+ <dc:creator>Tanvi Vyas</dc:creator>
+ </item>
+ <item>
+ <title>The Mozilla Blog: Firefox Can Now Get Push Notifications From Your Favorite Sites</title>
+ <guid isPermaLink="false">https://blog.mozilla.org/?p=9166</guid>
+ <link>https://blog.mozilla.org/blog/2016/01/25/firefox-can-now-get-push-notifications-from-your-favorite-sites/</link>
+ <description>&lt;p&gt;UPDATED TO CLARIFY HOW TO MANAGE PUSH NOTIFICATIONS&lt;/p&gt;
+ &lt;p&gt;Firefox for Windows, Mac and Linux now lets you choose to receive push notifications from websites if you give them permission. This is similar to Web notifications, except now you can receive notifications for websites even when they’re not loaded in a tab. This is super useful for websites like email, weather, social networks and shopping, which you might check frequently for updates.&lt;/p&gt;
+ &lt;p&gt;You can manage your notifications in the Control Center by clicking the green lock icon on the left side of the address bar. You can learn more about how to manage push notifications&lt;a href=&quot;https://support.mozilla.org/en-US/kb/push-notifications-firefox?as=u&amp;amp;utm_source=inproduct#w_upgraded-notifications&quot;&gt; here&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;&lt;b&gt;Push Notifications for Web Developers&lt;/b&gt;&lt;br /&gt;
+ To make this functionality possible, Mozilla helped establish the Web Push W3C standard that’s gaining momentum across the Web. We also continue to explore the new design pattern known as&lt;a href=&quot;https://blog.mozilla.org/futurereleases/2015/11/17/extending-the-webs-capabilities-in-firefox-and-beyond/&quot;&gt; Progressive Web Apps&lt;/a&gt;. If you’re a developer who wants to implement push notifications on your site, you can learn more in this&lt;a href=&quot;https://hacks.mozilla.org/2016/01/web-push-arrives-in-firefox-44/&quot;&gt; Hacks blog post&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;&lt;b&gt;More information:&lt;/b&gt;&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Download&lt;a href=&quot;https://www.mozilla.org/firefox/new/&quot;&gt; Firefox for Windows, Mac, Linux&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Release Notes for&lt;a href=&quot;https://www.mozilla.org/firefox/44.0/releasenotes/&quot;&gt; Firefox for Windows, Mac, Linux&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Download&lt;a href=&quot;https://play.google.com/store/apps/details?id=org.mozilla.firefox&amp;amp;referrer=utm_source%3Dmozilla%26utm_medium&quot;&gt; Firefox for Android&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Release Notes for&lt;a href=&quot;https://www.mozilla.org/firefox/android/44.0/releasenotes/&quot;&gt; Firefox for Android&lt;/a&gt;&lt;/li&gt;
+ &lt;/ul&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 01:56:50 +0000</pubDate>
+ <dc:creator>Mozilla</dc:creator>
+ </item>
+ <item>
+ <title>Benoit Girard: Using RecordReplay to investigate intermittent oranges</title>
+ <guid isPermaLink="false">http://benoitgirard.wordpress.com/?p=651</guid>
+ <link>https://benoitgirard.wordpress.com/2016/01/25/using-recordreplay-to-investigate-intermittent-oranges/</link>
+ <description>&lt;p&gt;This is a quick write up to summarize my, and Jeff’s, experience, using RR to debug a &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1226748&quot;&gt;fairly rare intermittent reftest failure&lt;/a&gt;. There’s still a lot of be learned about how to use RR effectively so I’m hoping sharing this will help others.&lt;/p&gt;
+ &lt;h3&gt;Finding the root of the bad pixel&lt;/h3&gt;
+ &lt;p&gt;First given a offending pixel I was able to set a breakpoint on it using &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Hacking_Tips#rr_with_reftest&quot;&gt;these instructions&lt;/a&gt;. Next using &lt;a href=&quot;https://github.com/jrmuizel/rr-dataflow&quot;&gt;rr-dataflow&lt;/a&gt; I was able to step from the offending bad pixel to the display item responsible for this pixel. Let me emphasize this for a second since it’s incredibly impressive. rr + rr-dataflow allows you to go from a buffer, through an intermediate surface, to the compositor on another thread, through another intermediate surface, back to the main thread and eventually back to the relevant display item. All of this was automated except for when the two pixels are blended together which is logically ambiguous. The speed at which rr was able to reverse continue through this execution was very impressive!&lt;/p&gt;
+ &lt;p&gt;Here’s the trace of this part: &lt;a href=&quot;https://gist.github.com/bgirard/e707e9b97556b500d9ae&quot;&gt;rr-trace-reftest-pixel-origin&lt;/a&gt;&lt;/p&gt;
+ &lt;h3&gt;Understanding the decoding step&lt;/h3&gt;
+ &lt;p&gt;From here I started comparing a replay of a failing test and a non failing step and it was clear that the DisplayList was different. In one we have a nsDisplayBackgroundColor in the other we don’t. From here I was able to step through the decoder and compare the sequence. This was very useful in ruling out possible theories. It was easy to step forward and backwards in the good and bad replay debugging sessions to test out various theories about race conditions and understanding at which part of the decode process the image was rejected. It turned out that we sent two decodes, one for the metadata that is used to sized the frame tree and the other one for the image data itself.&lt;/p&gt;
+ &lt;h3&gt;Comparing the frame tree&lt;/h3&gt;
+ &lt;p&gt;In hindsight, it would have been more effective to start debugging this test by looking at the frame tree (and I imagine for other tests looking at the display list and layer tree) first would have been a quicker start. It works even better if you have a good and a bad trace to compare the difference in the frame tree. From here, I found that the difference in the layer tree came from a change hint that wasn’t guaranteed to come in before the draw.&lt;/p&gt;
+ &lt;p&gt;The problem is now well understood: When we do a sync decode on reftest draw, if there’s an image error we wont flush the style hints since we’re already too deep in the painting pipeline.&lt;/p&gt;
+ &lt;h3&gt;Take away&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Finding the root cause of a bad pixel is very easy, and fast, to do using rr-dataflow.&lt;/li&gt;
+ &lt;li&gt;However it might be better to look for obvious frame tree/display list/layer tree difference(s) first.&lt;/li&gt;
+ &lt;li&gt;Debugging a replay is a lot simpler then debugging against non-determinist re-runs and a lot less frustrating too.&lt;/li&gt;
+ &lt;li&gt;rr is really useful for race conditions, especially rare ones.&lt;/li&gt;
+ &lt;/ul&gt;&lt;br /&gt; &lt;a href=&quot;http://feeds.wordpress.com/1.0/gocomments/benoitgirard.wordpress.com/651/&quot; rel=&quot;nofollow&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; src=&quot;http://feeds.wordpress.com/1.0/comments/benoitgirard.wordpress.com/651/&quot; /&gt;&lt;/a&gt; &lt;img alt=&quot;&quot; border=&quot;0&quot; height=&quot;1&quot; src=&quot;https://pixel.wp.com/b.gif?host=benoitgirard.wordpress.com&amp;amp;blog=12112851&amp;amp;post=651&amp;amp;subd=benoitgirard&amp;amp;ref=&amp;amp;feed=1&quot; width=&quot;1&quot; /&gt;</description>
+ <pubDate>Mon, 25 Jan 2016 22:16:01 +0000</pubDate>
+ <dc:creator>benoitgirard</dc:creator>
+ </item>
+ <item>
+ <title>The Servo Blog: These Weeks In Servo 48</title>
+ <guid isPermaLink="true">http://blog.servo.org/2016/01/25/twis-48/</guid>
+ <link>http://blog.servo.org/2016/01/25/twis-48/</link>
+ <description>&lt;p&gt;In the &lt;a href=&quot;https://github.com/pulls?page=1&amp;amp;q=is%3Apr+is%3Amerged+closed%3A2016-01-11..2016-01-25+user%3Aservo&quot;&gt;last two weeks&lt;/a&gt;, we landed 130 PRs in the Servo organization’s repositories.&lt;/p&gt;
+
+ &lt;p&gt;After months of work by vlad and many others, Windows support &lt;a href=&quot;https://github.com/servo/servo/pull/9385&quot;&gt;landed&lt;/a&gt;! Thanks to everyone who contributed fixes, tests, reviews, and even encouragement (or impatience!) to help us make this happen.&lt;/p&gt;
+
+ &lt;h3 id=&quot;notable-additions&quot;&gt;Notable Additions&lt;/h3&gt;
+
+ &lt;ul&gt;
+ &lt;li&gt;nikki &lt;a href=&quot;https://github.com/servo/servo/pull/9391&quot;&gt;added&lt;/a&gt; tests and support for checking the Fetch redirect count&lt;/li&gt;
+ &lt;li&gt;glennw &lt;a href=&quot;https://github.com/servo/servo/pull/9359&quot;&gt;implemented&lt;/a&gt; horizontal scrolling with arrow keys&lt;/li&gt;
+ &lt;li&gt;simon &lt;a href=&quot;https://github.com/servo/servo/pull/9333&quot;&gt;created&lt;/a&gt; a script that parses all of the CSS properties parsed by Servo&lt;/li&gt;
+ &lt;li&gt;ms2ger &lt;a href=&quot;https://github.com/servo/servo/pull/9293&quot;&gt;removed&lt;/a&gt; the legacy reftest framework&lt;/li&gt;
+ &lt;li&gt;fernando &lt;a href=&quot;https://github.com/servo/crowbot/pull/33&quot;&gt;made&lt;/a&gt; crowbot able to rejoin IRC after it accidentally floods the channel&lt;/li&gt;
+ &lt;li&gt;jack &lt;a href=&quot;https://github.com/servo/saltfs/pull/193&quot;&gt;added&lt;/a&gt; testing the &lt;code&gt;geckolib&lt;/code&gt; target to our CI&lt;/li&gt;
+ &lt;li&gt;antrik &lt;a href=&quot;https://github.com/servo/ipc-channel/pull/25&quot;&gt;fixed&lt;/a&gt; transfer corruption in ipc-channel on 32-bit&lt;/li&gt;
+ &lt;li&gt;valentin &lt;a href=&quot;https://github.com/servo/rust-url/pull/119&quot;&gt;added&lt;/a&gt; and simon &lt;a href=&quot;https://github.com/servo/rust-url/pull/152&quot;&gt;extended&lt;/a&gt; IDNA support in rust-url, which is required for both web and Gecko compatibility&lt;/li&gt;
+ &lt;/ul&gt;
+
+ &lt;h3 id=&quot;new-contributors&quot;&gt;New Contributors&lt;/h3&gt;
+
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/Chandler&quot;&gt;Chandler Abraham&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/DarinM223&quot;&gt;Darin Minamoto&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/coder543&quot;&gt;Josh Leverette&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/shssoichiro&quot;&gt;Joshua Holmer&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/therealkbhat&quot;&gt;Kishor Bhat&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/MonsieurLanza&quot;&gt;Lanza&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/mattkuo&quot;&gt;Matthew Kuo&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/waterlink&quot;&gt;Oleksii Fedorov&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/stspyder&quot;&gt;St.Spyder&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/vvuk&quot;&gt;Vladimir Vukicevic&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/apopiak&quot;&gt;apopiak&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/askalski&quot;&gt;askalski&lt;/a&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+
+ &lt;h3 id=&quot;screenshot&quot;&gt;Screenshot&lt;/h3&gt;
+
+ &lt;p&gt;Screencast of this post being upvoted on reddit… from Windows!&lt;/p&gt;
+
+ &lt;p&gt;&lt;img alt=&quot;(screencast)&quot; src=&quot;http://blog.servo.org/images/upvote-windows.gif&quot; title=&quot;Screencast of upvoting on Reddit on Windows.&quot; /&gt;&lt;/p&gt;
+
+ &lt;h3 id=&quot;meetings&quot;&gt;Meetings&lt;/h3&gt;
+
+ &lt;p&gt;We had a &lt;a href=&quot;https://github.com/servo/servo/wiki/Meeting-2016-01-11&quot;&gt;meeting&lt;/a&gt; on some CI-related woes, documenting tags and mentoring, and dependencies for the style subsystem.&lt;/p&gt;</description>
+ <pubDate>Mon, 25 Jan 2016 20:30:00 +0000</pubDate>
+ </item>
+ <item>
+ <title>Air Mozilla: Mozilla Weekly Project Meeting, 25 Jan 2016</title>
+ <guid isPermaLink="true">https://air.mozilla.org/mozilla-weekly-project-meeting-20160125/</guid>
+ <link>https://air.mozilla.org/mozilla-weekly-project-meeting-20160125/</link>
+ <description>&lt;p&gt;
+ &lt;img alt=&quot;Mozilla Weekly Project Meeting&quot; class=&quot;wp-post-image&quot; height=&quot;90&quot; src=&quot;https://air.cdn.mozilla.net/media/cache/e9/4f/e94fbd7f8df916c75a60e63a85b9168c.png&quot; width=&quot;160&quot; /&gt;
+ The Monday Project Meeting
+ &lt;/p&gt;</description>
+ <pubDate>Mon, 25 Jan 2016 19:00:00 +0000</pubDate>
+ <dc:creator>Air Mozilla</dc:creator>
+ </item>
+ <item>
+ <title>About:Community: Firefox 44 new contributors</title>
+ <guid isPermaLink="false">http://blog.mozilla.org/community/?p=2292</guid>
+ <link>http://blog.mozilla.org/community/2016/01/25/firefox-44-new-contributors/</link>
+ <description>&lt;p&gt;With the release of Firefox 44, we are pleased to welcome the &lt;strong&gt;28 developers&lt;/strong&gt; who contributed their first code change to Firefox in this release, &lt;strong&gt;23&lt;/strong&gt; of whom were brand new volunteers! Please join us in thanking each of these diligent and enthusiastic individuals, and take a look at their contributions:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;mkm: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1208124&quot;&gt;1208124&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Aditya Motwani: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1209087&quot;&gt;1209087&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Aniket Vyas: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1197309&quot;&gt;1197309&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1197315&quot;&gt;1197315&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Chirath R: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1216941&quot;&gt;1216941&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Christiane Ruetten: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1209091&quot;&gt;1209091&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Fernando Campo: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1199815&quot;&gt;1199815&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Grisha Pushkov: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=994555&quot;&gt;994555&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Guang-De Lin: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1150305&quot;&gt;1150305&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Hassen ben tanfous: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1074804&quot;&gt;1074804&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Helen V. Holmes: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1205046&quot;&gt;1205046&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Henrik Tjäder: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1161698&quot;&gt;1161698&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1209912&quot;&gt;1209912&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Johann Hofmann: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1192432&quot;&gt;1192432&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1198405&quot;&gt;1198405&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1204072&quot;&gt;1204072&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Kapeel Sable: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1212171&quot;&gt;1212171&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Manav Batra: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1202618&quot;&gt;1202618&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1212280&quot;&gt;1212280&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1214626&quot;&gt;1214626&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Manuel Casas Barrado: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1172662&quot;&gt;1172662&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1193674&quot;&gt;1193674&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1200693&quot;&gt;1200693&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1203298&quot;&gt;1203298&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1205684&quot;&gt;1205684&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1212331&quot;&gt;1212331&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1212338&quot;&gt;1212338&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1214582&quot;&gt;1214582&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Matt Howell: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1208626&quot;&gt;1208626&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Matthew Turnbull: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1213620&quot;&gt;1213620&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Olivier Yiptong: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1210936&quot;&gt;1210936&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1210940&quot;&gt;1210940&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1213078&quot;&gt;1213078&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Piotr Tworek: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1209446&quot;&gt;1209446&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Rocik: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1070719&quot;&gt;1070719&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Roland Sako: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1207733&quot;&gt;1207733&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Ronald Claveau: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1207266&quot;&gt;1207266&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Sanchit Nevgi: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1205181&quot;&gt;1205181&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Shaif Chowdhury: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1185606&quot;&gt;1185606&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1208121&quot;&gt;1208121&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Shubham Jain: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1208470&quot;&gt;1208470&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1208705&quot;&gt;1208705&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Stanislas Daniel Claude Dolcini: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1147197&quot;&gt;1147197&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Stephanie Ouillon: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1178533&quot;&gt;1178533&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1201626&quot;&gt;1201626&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Tim Huang: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1181489&quot;&gt;1181489&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;simplyblue24: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1218204&quot;&gt;1218204&lt;/a&gt;&lt;/li&gt;
+ &lt;/ul&gt;</description>
+ <pubDate>Mon, 25 Jan 2016 16:21:33 +0000</pubDate>
+ <dc:creator>Josh Matthews</dc:creator>
+ </item>
+ <item>
+ <title>Doug Belshaw: 3 things to consider when designing a digital skills framework</title>
+ <guid isPermaLink="false">tag:literaci.es,2014:Post/digital-skills-curriculum</guid>
+ <link>http://literaci.es/digital-skills-curriculum</link>
+ <description>&lt;p&gt;&lt;img alt=&quot;Learning to credential&quot; src=&quot;http://bryanmmathers.com/wp-content/uploads/2016/01/learning-to-credential.png&quot; /&gt;&lt;/p&gt;
+
+ &lt;p&gt;The image above was created by &lt;a href=&quot;http://bryanmmathers.com/learning-to-credential&quot; rel=&quot;nofollow&quot;&gt;Bryan Mathers&lt;/a&gt; for our &lt;a href=&quot;https://goo.gl/QqwUKP&quot; rel=&quot;nofollow&quot;&gt;presentation&lt;/a&gt; at &lt;a href=&quot;http://bettshow.com&quot; rel=&quot;nofollow&quot;&gt;BETT&lt;/a&gt; last week. It shows the way that, in broad brushstrokes, learning design &lt;em&gt;should&lt;/em&gt; happen. Before microcredentials such as &lt;a href=&quot;http://openbadges.org&quot; rel=&quot;nofollow&quot;&gt;Open Badges&lt;/a&gt; this was a difficult thing to do as both the credential and the assessment are usually given to educators. The flow tends to go &lt;em&gt;backwards&lt;/em&gt; from credentials instead of forwards from what we want people to learn.&lt;/p&gt;
+
+ &lt;p&gt;But what if you really &lt;em&gt;were&lt;/em&gt; starting from scratch? How could you design a digital skills framework that contains knowledge, skills, and behaviours worth learning? Having written my &lt;a href=&quot;http://neverendingthesis.com&quot; rel=&quot;nofollow&quot;&gt;thesis&lt;/a&gt; on digital literacies and led Mozilla’s &lt;a href=&quot;https://teach.mozilla.org/activities/web-literacy/&quot; rel=&quot;nofollow&quot;&gt;Web Literacy Map&lt;/a&gt; for a couple of years, I’ve got some suggestions. &lt;/p&gt;
+ &lt;h3&gt;
+ &lt;a class=&quot;head_anchor&quot; href=&quot;http://literaci.es/feed#1-define-your-audience&quot; name=&quot;1-define-your-audience&quot; rel=&quot;nofollow&quot;&gt; &lt;/a&gt;1. Define your audience&lt;/h3&gt;
+ &lt;p&gt;One of the most important things to define is who your audience is for your digital skills framework. Is it for learners to read? Who are they? How old are they? Are you excluding anyone on purpose? Why / why not?&lt;/p&gt;
+
+ &lt;p&gt;You might want to do some research and work around &lt;a href=&quot;https://en.wikipedia.org/wiki/Persona_(user_experience)&quot; rel=&quot;nofollow&quot;&gt;user personas&lt;/a&gt; as part of a user-centred design approach. This ensures you’re designing for real people instead of figments of your imagination (or, worse still, in line with your prejudices).&lt;/p&gt;
+
+ &lt;p&gt;It’s also good practice to make the language used in the skills framework as precise as possible. Jargon is technical language used for the sake of it. There may be times when it’s impossible not to use a word (e.g. ’&lt;a href=&quot;https://en.wikipedia.org/wiki/Meme&quot; rel=&quot;nofollow&quot;&gt;meme&lt;/a&gt;’). If you do this then link to a definition or include a glossary. It’s also useful to check the ‘reading level’ of your framework and, if you really want a challenge, try using &lt;a href=&quot;http://splasho.com/upgoer5/&quot; rel=&quot;nofollow&quot;&gt;Up-Goer Five&lt;/a&gt; language.&lt;/p&gt;
+ &lt;h3&gt;
+ &lt;a class=&quot;head_anchor&quot; href=&quot;http://literaci.es/feed#2-focus-on-verbs&quot; name=&quot;2-focus-on-verbs&quot; rel=&quot;nofollow&quot;&gt; &lt;/a&gt;2. Focus on verbs&lt;/h3&gt;
+ &lt;p&gt;It’s extremely easy, when creating a framework for learning, to fall into the 'knowledge trap’. Our aim when creating the raw materials from which someone can build a curriculum is to focus on &lt;em&gt;action&lt;/em&gt;. Knowledge should make a difference in practice.&lt;/p&gt;
+
+ &lt;p&gt;One straightforward way to ensure that you’re focusing on action rather than head knowledge is to use &lt;strong&gt;verbs&lt;/strong&gt; when constructing your digital skills framework. If you’re familiar with &lt;a href=&quot;https://en.wikipedia.org/wiki/Bloom%27s_taxonomy&quot; rel=&quot;nofollow&quot;&gt;Bloom’s Taxonomy&lt;/a&gt;, then you may find &lt;a href=&quot;http://byrdseed.com/differentiator/&quot; rel=&quot;nofollow&quot;&gt;The Differentiator&lt;/a&gt; useful. This pairs verbs with the various levels of Bloom’s.&lt;/p&gt;
+ &lt;h3&gt;
+ &lt;a class=&quot;head_anchor&quot; href=&quot;http://literaci.es/feed#3-add-version-numbers&quot; name=&quot;3-add-version-numbers&quot; rel=&quot;nofollow&quot;&gt; &lt;/a&gt;3. Add version numbers&lt;/h3&gt;
+ &lt;p&gt;A framework needs to be a living, breathing thing. It should be subject to revision and updated often. For this reason, you should add version numbers to your documentation. Ideally, the latest version should be at a canonical URL and you should archive previous versions to static URLs. &lt;/p&gt;
+
+ &lt;p&gt;I would also advise releasing the first version of your framework not as 'version 1.0’ but as 'v0.1’. This shows that you’re willing for others to provide input, that there will be further versions, and that you know you haven’t got it right first time (and forevermore). &lt;/p&gt;
+
+ &lt;hr /&gt;
+
+ &lt;p&gt;&lt;strong&gt;Questions? Comments?&lt;/strong&gt; Ask me on Twitter (&lt;a href=&quot;http://twitter.com/dajbelshaw&quot; rel=&quot;nofollow&quot;&gt;@dajbelshaw&lt;/a&gt;). I also consult around this kind of thing, so hit me up on &lt;a href=&quot;http://literaci.es/hello@dynamicskillset.com&quot; rel=&quot;nofollow&quot;&gt;hello@dynamicskillset.com&lt;/a&gt;&lt;/p&gt;</description>
+ <pubDate>Mon, 25 Jan 2016 14:46:34 +0000</pubDate>
+ </item>
+ <item>
+ <title>Mozilla Fundraising: Why did you decide to donate today?</title>
+ <guid isPermaLink="false">https://fundraising.mozilla.org/?p=800</guid>
+ <link>https://fundraising.mozilla.org/why-did-you-decide-to-donate-today/</link>
+ <description>This year, we asked some of our donors why they decided to donate to our end of year fundraising campaign. The Survey The Audience The survey was shown to a random sample of donors whose browser language was set to … &lt;a class=&quot;go&quot; href=&quot;https://fundraising.mozilla.org/why-did-you-decide-to-donate-today/&quot;&gt;Continue reading&lt;/a&gt;</description>
+ <pubDate>Mon, 25 Jan 2016 13:31:34 +0000</pubDate>
+ <dc:creator>Adam Lofting</dc:creator>
+ </item>
+ <item>
+ <title>Andy McKay: Robbie Burns</title>
+ <guid isPermaLink="false">http://www.agmweb.ca/robbie-burns</guid>
+ <link>http://www.agmweb.ca/2016-01-25-robbie-burns/</link>
+ <description>&lt;p&gt;Tonight is Robbie Burns night, in honour of that great Scottish poet. But tonight had me thinking about another night in my past.&lt;/p&gt;
+
+ &lt;p&gt;It was about 5 years ago, maybe less, I struggle to remember now. I was in the UK visiting family and my Dad was sick. Cancer and it's treatment is tough, you have good weeks, you have bad weeks and you have really fucking bad weeks. This was a good week and for some reason I was in the UK.&lt;/p&gt;
+
+ &lt;p&gt;Myself, my brother and my sister-in-law went down to see him that night. It was Robbie Burns night and that meant an excuse for haggis, really, truly terrible scotch, Scottish dancing and all that. There are many times when I look back at time with my Dad in those last few years. This was definitely one of those times. He was my Dad at his best, cracking jokes and having fun. Living life to the absolute fullest, while you still have that chance.&lt;/p&gt;
+
+ &lt;p&gt;We had a great night. That ended way too soon.&lt;/p&gt;
+
+ &lt;p&gt;Not long after that the cancer came back and that was that.&lt;/p&gt;
+
+ &lt;p&gt;But suddenly tonight, in a bar in Portland I had these memories of my Dad in a waistcoat cracking jokes and having fun on Robbie Burns night. No-one else in the bar seemed to know what night it was. You'd think Robbie Burns night might get a little bit more appreciation, but hey.&lt;/p&gt;
+
+ &lt;p&gt;In the many years I've been running this blog I've never written about my Dad passing away. Here's the first time. I miss him.&lt;/p&gt;
+
+ &lt;p&gt;Hey Robbie Burns? Thanks for making me remember that night.&lt;/p&gt;</description>
+ <pubDate>Mon, 25 Jan 2016 08:00:00 +0000</pubDate>
+ </item>
+ <item>
+ <title>This Week In Rust: This Week in Rust 115</title>
+ <guid isPermaLink="false">tag:this-week-in-rust.org,2016-01-25:blog/2016/01/25/this-week-in-rust-115/</guid>
+ <link>http://this-week-in-rust.org/blog/2016/01/25/this-week-in-rust-115/</link>
+ <description>&lt;p&gt;Hello and welcome to another issue of &lt;em&gt;This Week in Rust&lt;/em&gt;!
+ &lt;a href=&quot;http://rust-lang.org&quot;&gt;Rust&lt;/a&gt; is a systems language pursuing the trifecta:
+ safety, concurrency, and speed. This is a weekly summary of its progress and
+ community. Want something mentioned? Tweet us at &lt;a href=&quot;https://twitter.com/ThisWeekInRust&quot;&gt;@ThisWeekInRust&lt;/a&gt; or &lt;a href=&quot;mailto:corey@octayn.net?subject=This%20Week%20in%20Rust%20Suggestion&quot;&gt;send us an
+ email&lt;/a&gt;!
+ Want to get involved? &lt;a href=&quot;https://github.com/rust-lang/rust/blob/master/CONTRIBUTING.md&quot;&gt;We love
+ contributions&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;&lt;em&gt;This Week in Rust&lt;/em&gt; is openly developed &lt;a href=&quot;https://github.com/cmr/this-week-in-rust&quot;&gt;on GitHub&lt;/a&gt;.
+ If you find any errors in this week's issue, &lt;a href=&quot;https://github.com/cmr/this-week-in-rust/pulls&quot;&gt;please submit a PR&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;This week's edition was edited by: &lt;a href=&quot;https://github.com/nasa42&quot;&gt;nasa42&lt;/a&gt;, &lt;a href=&quot;https://github.com/brson&quot;&gt;brson&lt;/a&gt;, and &lt;a href=&quot;https://github.com/llogiq&quot;&gt;llogiq&lt;/a&gt;.&lt;/p&gt;
+ &lt;h3&gt;Updates from Rust Community&lt;/h3&gt;
+ &lt;h4&gt;News &amp;amp; Blog Posts&lt;/h4&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;img alt=&quot;balloon&quot; class=&quot;emoji&quot; src=&quot;https://cdn.discourse.org/business/images/emoji/emoji_one/balloon.png?v=0&quot; title=&quot;:balloon:&quot; /&gt;&lt;img alt=&quot;tada&quot; class=&quot;emoji&quot; src=&quot;https://cdn.discourse.org/business/images/emoji/emoji_one/tada.png?v=0&quot; title=&quot;:tada:&quot; /&gt; &lt;a href=&quot;http://blog.rust-lang.org/2016/01/21/Rust-1.6.html&quot;&gt;Announcing Rust 1.6&lt;/a&gt;. &lt;img alt=&quot;tada&quot; class=&quot;emoji&quot; src=&quot;https://cdn.discourse.org/business/images/emoji/emoji_one/tada.png?v=0&quot; title=&quot;:tada:&quot; /&gt;&lt;img alt=&quot;balloon&quot; class=&quot;emoji&quot; src=&quot;https://cdn.discourse.org/business/images/emoji/emoji_one/balloon.png?v=0&quot; title=&quot;:balloon:&quot; /&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.poumeyrol.fr/2016/01/15/Awkward-zone/&quot;&gt;Rust, BigData and my laptop&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;[pdf]&lt;a href=&quot;https://cdn.rawgit.com/Gankro/thesis/master/thesis.pdf&quot;&gt;You can't spell trust without Rust&lt;/a&gt;. Analysis of the semantics and expressiveness of Rust’s type system.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.ncameron.org/blog/libmacro/&quot;&gt;Libmacro - an API for procedural macros to interact with the compiler&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.jonathanturner.org/2016/01/rust-and-blub-paradox.html&quot;&gt;Rust and the Blub Paradox&lt;/a&gt;. And the &lt;a href=&quot;http://www.jonathanturner.org/2016/01/rethinking-the-blub-paradox.html&quot;&gt;follow-up&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;[video] &lt;a href=&quot;https://www.youtube.com/channel/UC4mpLlHn0FOekNg05yCnkzQ/videos&quot;&gt;Ferris Makes Emulators&lt;/a&gt;. Live stream of Ferris developing a N64 emulator in Rust (also on &lt;a href=&quot;http://www.twitch.tv/ferrisstreamsstuff/profile&quot;&gt;Twitch&lt;/a&gt;).&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h4&gt;Notable New Crates &amp;amp; Project Updates&lt;/h4&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://areweconcurrentyet.com/&quot;&gt;Are we concurrent yet&lt;/a&gt;?&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/gfx-rs/gfx&quot;&gt;GFX&lt;/a&gt; epic rewrite for the Pipeline State Objects paradigm has &lt;a href=&quot;https://github.com/gfx-rs/gfx/pull/828&quot;&gt;landed&lt;/a&gt;, described &lt;a href=&quot;http://gfx-rs.github.io/2016/01/22/pso.html&quot;&gt;on the blog&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/mcarton/rust-herbie-lint&quot;&gt;Herbie&lt;/a&gt;. A rustc plugin to check for numerical instability.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://blog.piston.rs/2016/01/23/dynamo/&quot;&gt;Dynamo&lt;/a&gt;. A rusty dynamically typed scripting language.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/whitequark/rust-vnc&quot;&gt;rust-vnc&lt;/a&gt;. An implementation of VNC protocol, client state machine and a client.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;Updates from Rust Core&lt;/h3&gt;
+ &lt;p&gt;129 pull requests were &lt;a href=&quot;https://github.com/issues?q=is%3Apr+org%3Arust-lang+is%3Amerged+merged%3A2016-01-18..2016-01-25&quot;&gt;merged in the last week&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;See the &lt;a href=&quot;https://internals.rust-lang.org/t/triage-digest-mon-jan-25-2016/3111&quot;&gt;triage digest&lt;/a&gt; and &lt;a href=&quot;https://internals.rust-lang.org/t/subteam-reports-2016-01-22/3106&quot;&gt;subteam reports&lt;/a&gt; for more details.&lt;/p&gt;
+ &lt;h4&gt;Notable changes&lt;/h4&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30872&quot;&gt;Implement RFC 1252 expanding the OpenOptions structure&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/book/pull/58&quot;&gt;Book: First draft of 'ownership'&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/cargo/pull/2205&quot;&gt;Cargo: Add convenience syntax to install current crate&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/cargo/pull/2196&quot;&gt;Cargo: Introduce cargo metadata subcommand&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/cargo/pull/2081&quot;&gt;Cargo: Implement &lt;code&gt;cargo init&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/cargo/pull/2270&quot;&gt;Cargo: Emit a warning when manifest specifies empty dependency constraints&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/29520&quot;&gt;Change name when outputting staticlibs on Windows&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30998&quot;&gt;Make &lt;code&gt;btree_set::{IntoIter, Iter, Range}&lt;/code&gt; covariant&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30917&quot;&gt;Avoid bounds checking at &lt;code&gt;slice::binary_search&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30894&quot;&gt;&lt;code&gt;std::sync::mpsc&lt;/code&gt;: Add &lt;code&gt;fmt::Debug&lt;/code&gt; stubs&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30882&quot;&gt;resolve: Fix variant namespacing&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h4&gt;New Contributors&lt;/h4&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Adrian Heine&lt;/li&gt;
+ &lt;li&gt;Andrea Bedini&lt;/li&gt;
+ &lt;li&gt;Guillaume Bonnet&lt;/li&gt;
+ &lt;li&gt;Kamal Marhubi&lt;/li&gt;
+ &lt;li&gt;Keith Yeung&lt;/li&gt;
+ &lt;li&gt;Marc Bowes&lt;/li&gt;
+ &lt;li&gt;Martin&lt;/li&gt;
+ &lt;li&gt;mopp&lt;/li&gt;
+ &lt;li&gt;Olaf Buddenhagen&lt;/li&gt;
+ &lt;li&gt;Paul Dicker&lt;/li&gt;
+ &lt;li&gt;Peter Kolloch&lt;/li&gt;
+ &lt;li&gt;Stephen (Ziyun) Li&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h4&gt;Approved RFCs&lt;/h4&gt;
+ &lt;p&gt;Changes to Rust follow the Rust &lt;a href=&quot;https://github.com/rust-lang/rfcs#rust-rfcs&quot;&gt;RFC (request for comments)
+ process&lt;/a&gt;. These
+ are the RFCs that were approved for implementation this week:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1462&quot;&gt;Amendment to RFC 550: Add &lt;code&gt;[&lt;/code&gt; to the FOLLOW(ty) in macro future-proofing rules&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1320&quot;&gt;Amendment to RFC 1192: Amend &lt;code&gt;RangeInclusive&lt;/code&gt; to use an enum&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h4&gt;Final Comment Period&lt;/h4&gt;
+ &lt;p&gt;Every week &lt;a href=&quot;https://rust-lang.org/team.html&quot;&gt;the team&lt;/a&gt; announces the
+ 'final comment period' for RFCs and key PRs which are reaching a
+ decision. Express your opinions now. &lt;a href=&quot;https://github.com/rust-lang/rfcs/labels/final-comment-period&quot;&gt;This week's FCPs&lt;/a&gt; are:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/243&quot;&gt;Trait-based exception handling&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1361&quot;&gt;Improve Cargo target-specific dependencies&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1129&quot;&gt;Add a &lt;code&gt;IndexAssign&lt;/code&gt; trait that allows overloading &quot;indexed assignment&quot; expressions like &lt;code&gt;a[b] = c&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1196&quot;&gt;Allow eliding more type parameters&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1296&quot;&gt;Add an &lt;code&gt;alias&lt;/code&gt; attribute to &lt;code&gt;#[link]&lt;/code&gt; and &lt;code&gt;-l&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h4&gt;New RFCs&lt;/h4&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1477&quot;&gt;Add compiler support for generic atomic operations&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1478&quot;&gt;Translate undefined generic intrinsics to an LLVM &lt;code&gt;unreachable&lt;/code&gt; and a lint&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;Upcoming Events&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.meetup.com/opentechschool-berlin/&quot;&gt;1/27. OpenTechSchool Berlin: Rust Hack and Learn&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.meetup.com/Tokyo-Rust-Meetup/events/227871840/&quot;&gt;1/28. Tokyo Rust Meetup #2&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.meetup.com/Rust-Berlin/events/227321071/&quot;&gt;2/3. Rust Berlin: Leaf and Collenchyma&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.meetup.com/de/Rust-Cologne-Bonn/events/227534456/&quot;&gt;2/3. Rust Meetup in Cologne / Germany&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://www.eventbrite.com/e/mozilla-rust-seattle-meetup-tickets-12222326307?aff=erelexporg&quot;&gt;2/8. Seattle Rust Meetup&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.meetup.com/de-DE/Rust-Rhein-Main/events/228170051/&quot;&gt;2/12. Embedded Rust Workshop Frankfurt&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;If you are running a Rust event please add it to the &lt;a href=&quot;https://www.google.com/calendar/embed?src=apd9vmbc22egenmtu5l6c5jbfc%40group.calendar.google.com&quot;&gt;calendar&lt;/a&gt; to get
+ it mentioned here. Email &lt;a href=&quot;mailto:erick.tryzelaar@gmail.com&quot;&gt;Erick Tryzelaar&lt;/a&gt; or &lt;a href=&quot;mailto:banderson@mozilla.com&quot;&gt;Brian
+ Anderson&lt;/a&gt; for access.&lt;/p&gt;
+ &lt;h3&gt;fn work(on: RustProject) -&amp;gt; Money&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://maidsafe.net/rust_engineer.html&quot;&gt;Rust Engineer&lt;/a&gt; at MaidSafe.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://careers.mozilla.org/en-US/position/ozy21fwU&quot;&gt;Research Engineer - Servo&lt;/a&gt; at Mozilla.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://careers.mozilla.org/en-US/position/o0H41fww&quot;&gt;Senior Research Engineer - Rust&lt;/a&gt; at Mozilla.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://plv.mpi-sws.org/rustbelt/&quot;&gt;PhD and postdoc positions&lt;/a&gt; at MPI-SWS.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;&lt;em&gt;Tweet us at &lt;a href=&quot;https://twitter.com/ThisWeekInRust&quot;&gt;@ThisWeekInRust&lt;/a&gt; to get your job offers listed here!&lt;/em&gt;&lt;/p&gt;
+ &lt;h3&gt;Crate of the Week&lt;/h3&gt;
+ &lt;p&gt;This week's Crate of the Week is &lt;a href=&quot;https://github.com/phildawes/racer&quot;&gt;racer&lt;/a&gt; which powers code completion in all Rust development environments.&lt;/p&gt;
+ &lt;p&gt;Thanks to &lt;a href=&quot;https://users.rust-lang.org/users/stebalien&quot;&gt;Steven Allen&lt;/a&gt; for the suggestion.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;https://users.rust-lang.org/t/crate-of-the-week/2704&quot;&gt;Submit your suggestions for next week&lt;/a&gt;!&lt;/p&gt;
+ &lt;h3&gt;Quote of the Week&lt;/h3&gt;
+ &lt;blockquote&gt;
+ &lt;p&gt;Memory errors are fundamentally state errors, and Rust's move semantics, borrowing, and aliasing XOR mutating help enormously for me to reason about how my program changes state as it executes, to avoid accidental shared state and side effects at a distance. Rust more than any other language I know enables me to do compiler driven design. And internalizing its rules has helped me design better systems, even in other languages.&lt;/p&gt;
+ &lt;/blockquote&gt;
+ &lt;p&gt;— &lt;a href=&quot;https://www.reddit.com/r/rust/comments/4275gz/rust_and_the_blub_paradox/cz8akv9&quot;&gt;desiringmachines on /r/rust&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;Thanks to &lt;a href=&quot;https://users.rust-lang.org/users/dikaiosune&quot;&gt;dikaiosune&lt;/a&gt; for the suggestion.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;http://users.rust-lang.org/t/twir-quote-of-the-week/328&quot;&gt;Submit your quotes for next week&lt;/a&gt;!&lt;/p&gt;</description>
+ <pubDate>Mon, 25 Jan 2016 05:00:00 +0000</pubDate>
+ <dc:creator>Corey Richardson</dc:creator>
+ </item>
+ <item>
+ <title>Cameron Kaiser: 38.6.0 available</title>
+ <guid isPermaLink="false">tag:blogger.com,1999:blog-1015214236289077798.post-7056349209464984020</guid>
+ <link>http://tenfourfox.blogspot.com/2016/01/3860-available.html</link>
+ <description>TenFourFox 38.6.0 is available for testing (&lt;a href=&quot;https://sourceforge.net/projects/tenfourfox/files/38.6.0/&quot;&gt;downloads&lt;/a&gt;, &lt;a href=&quot;https://github.com/classilla/tenfourfox/wiki/Hashes&quot;&gt;hashes&lt;/a&gt;, &lt;a href=&quot;https://github.com/classilla/tenfourfox/wiki/ZZReleaseNotes3860&quot;&gt;release notes&lt;/a&gt;). I'm sorry it's been so quiet around here; I'm in the middle of a backbreaking Master's course, my last one before I'm finally done with the lousy thing, and I haven't had any time to start on 45 so far. 38.6 does have some other fixes in it, though: I think I found the last place where bookmark backups were being mistakenly saved in LZ4 based on Chris Trusch's report, and the problematic fonts on the iCloud login page are now blacklisted, so you should be able to login again. I can't do much more testing than that, however, since I don't use iCloud personally, so other lapses in font functionality will require the font URL and I'll add them to the blacklist in 38.7. The browser will go live Monday Pacific time as usual. (The temporary workaround is to set &lt;tt&gt;gfx.downloadable_fonts.enabled&lt;/tt&gt; to &lt;tt&gt;false&lt;/tt&gt;, and switch the setting back when you don't need it anymore.) &lt;p&gt;Speaking of, downloadable fonts were exactly the same problem on the Sun Ultra-3 laptop I've been refurbishing; Oracle still provides a free Solaris 10 build of 38ESR, but it crashes on web fonts for reasons I have yet to diagnose, so I just have them turned off. Yes, it really is a SPARC laptop, a rebranded Tadpole Viper, and I think the fastest one ever made in this form factor (a 1.2GHz UltraSPARC IIIi). It's pretty much what I expected the PowerBook G5 would have been -- hot, overthrottled and power-hungry -- but Tadpole actually built the thing and it's not a disaster, relatively speaking. There's no JIT in this Firefox build, the brand new battery gets only 70 minutes of runtime even with the CPU clock-skewed to hell, it stands a very good chance of rendering me sterile and/or medium rare if I actually use it in my lap and it had at least one sudden overtemp shutdown and pooped all over the filesystem, but between Firefox, Star Office and &lt;tt&gt;pkgsrc&lt;/tt&gt; I can actually use it. More on that for laughs in a future post. &lt;/p&gt;&lt;p&gt;It has been pointed out to me that Leopard Webkit has not made an update in over three months, so hopefully Tobias is still doing okay with his port.&lt;/p&gt;</description>
+ <pubDate>Sat, 23 Jan 2016 06:02:00 +0000</pubDate>
+ <author>noreply@blogger.com (ClassicHasClass)</author>
+ </item>
+ <item>
+ <title>Mozilla Privacy Blog: Addressing the Chilling Effect of Patent Damages</title>
+ <guid isPermaLink="false">https://blog.mozilla.org/netpolicy/?p=907</guid>
+ <link>https://blog.mozilla.org/netpolicy/2016/01/22/addressing-the-chilling-effect-of-patent-damages/</link>
+ <description>&lt;p&gt;Last year, we unveiled the &lt;a href=&quot;https://www.mozilla.org/about/patents/license/&quot;&gt;Mozilla Open Software Patent License&lt;/a&gt; as part of our &lt;a href=&quot;https://www.mozilla.org/about/patents/&quot;&gt;Initiative&lt;/a&gt; to help limit the negative impacts that patents have on open source software. While those were an important first step for us, we continue to do more. This past Wednesday, Mozilla joined several other tech and software companies in filing an &lt;a href=&quot;https://blog.mozilla.org/netpolicy/files/2016/01/Halo-Stryker-Internet-Companies-brief.pdf&quot;&gt;amicus brief&lt;/a&gt; with the Supreme Court of the United States in the &lt;i&gt;Halo&lt;/i&gt; and &lt;i&gt;Stryker&lt;/i&gt; cases.&lt;/p&gt;
+ &lt;p&gt;In the brief, we urge the Court to limit the availability of treble damages. Treble damages are significant because they greatly increase the amount of money owed if a defendant is found to “willfully infringe†a patent. As a result, many open source projects and technology companies will refuse to look into or engage in discussions about patents, in order to avoid even a remote possibility of willful infringement. This makes it very hard to address the chilling effects that patents can have on open source software development, open innovation, and collaborative efforts.&lt;/p&gt;
+ &lt;p&gt;We hope that our brief will help the Court see how this legal standard has affected technology companies and persuade the Court to limit treble damages.&lt;/p&gt;</description>
+ <pubDate>Sat, 23 Jan 2016 00:17:34 +0000</pubDate>
+ <dc:creator>Elvin Lee</dc:creator>
+ </item>
+ <item>
+ <title>Mozilla Addons Blog: Add-on Signing Update</title>
+ <guid isPermaLink="false">http://blog.mozilla.org/addons/?p=7640</guid>
+ <link>https://blog.mozilla.org/addons/2016/01/22/add-on-signing-update/</link>
+ <description>&lt;p&gt;In Firefox 43, we made it a default requirement for add-ons to be signed. This requirement can be disabled by &lt;a href=&quot;https://wiki.mozilla.org/Addons/Extension_Signing#FAQ&quot;&gt;toggling a preference&lt;/a&gt; that was originally scheduled to be removed in Firefox 44 for release and beta versions (this preference will continue to be available in the Nightly, Developer, and ESR Editions of Firefox for the foreseeable future). &lt;/p&gt;
+ &lt;p&gt;We are delaying the removal of this preference to Firefox 46 for a couple of reasons: We’re adding a feature in Firefox 45 that allows &lt;a href=&quot;https://blog.mozilla.org/addons/2015/12/23/loading-temporary-add-ons/&quot;&gt;temporarily loading unsigned restartless add-ons&lt;/a&gt; in release, which will allow developers of those add-ons to use Firefox for testing, and we’d like this option to be available when we remove the preference. We also want to ensure that developers have adequate time to finish the transition to signed add-ons. &lt;/p&gt;
+ &lt;p&gt;The &lt;a href=&quot;https://wiki.mozilla.org/Addons/Extension_Signing#Timeline&quot;&gt;updated timeline&lt;/a&gt; is available on the signing wiki, and you can look up &lt;a href=&quot;https://wiki.mozilla.org/RapidRelease/Calendar&quot;&gt;release dates for Firefox versions&lt;/a&gt; on the releases wiki. Signing will be mandatory in the beta and release versions of Firefox from 46 onwards, at which point unbranded builds based on beta and release will be provided for testing.&lt;/p&gt;</description>
+ <pubDate>Fri, 22 Jan 2016 22:40:59 +0000</pubDate>
+ <dc:creator>Kev Needham</dc:creator>
+ </item>
+ <item>
+ <title>Chris Cooper: RelEng &amp; RelOps Weekly Highlights - January 22, 2016</title>
+ <guid isPermaLink="true">http://coopcoopbware.tumblr.com/post/137832199980</guid>
+ <link>http://coopcoopbware.tumblr.com/post/137832199980</link>
+ <description>&lt;p&gt;&lt;/p&gt;&lt;figure class=&quot;alignright&quot;&gt;&lt;a href=&quot;https://www.flickr.com/photos/proud2bcan8dn/1150097247/in/faves-19934681@N00/&quot; target=&quot;_blank&quot; title=&quot;wine-and-pies&quot;&gt;&lt;img alt=&quot;wine-and-pies&quot; src=&quot;https://farm2.staticflickr.com/1216/1150097247_2f11cb4c2d_z.jpg?zz=1&quot; width=&quot;200px&quot; /&gt;&lt;/a&gt;Releng: drinkin’ wine and makin’ pies.&lt;/figure&gt;It’s encouraging to see more progress this week on both the build/release promotion and TaskCluster migration fronts, our two major efforts for this quarter.&lt;p&gt;&lt;/p&gt;
+
+ &lt;p&gt;&lt;b&gt;Modernize infrastructure:&lt;/b&gt;&lt;/p&gt;
+ &lt;p&gt;In a continuing effort to enable faster, more reliable, and more easily-run tests for TaskCluster components, Dustin landed support for an in-memory, credential-free mock of Azure Table Storage in the &lt;a href=&quot;https://www.npmjs.com/package/azure-entities&quot; target=&quot;_blank&quot;&gt;azure-entities&lt;/a&gt; package. Together with the fake mock support he added to &lt;a href=&quot;https://github.com/djmitche/taskcluster-lib-testing&quot; target=&quot;_blank&quot;&gt;taskcluster-lib-testing&lt;/a&gt;, this allows tests for components like taskcluster-hooks to run without network access and without the need for any credentials, substantially decreasing the barrier to external contributions.&lt;/p&gt;
+
+ &lt;p&gt;All release promotion tasks are now signed by default. Thanks to Rail for his work here to help improve verifiability and chain-of-custody in our upcoming release process. (&lt;a href=&quot;https://bugzil.la/1239682&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/1239682&lt;/a&gt;)
+ Beetmover has been spotted in the wild! Jordan has been working on this new tool as part of our release promotion project. Beetmover helps move build artifacts from one place to another (generally between S3 buckets these days), but can also be extended to perform validation actions inline, e.g. checksums and anti-virus. (&lt;a href=&quot;https://bugzil.la/1225899&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/1225899&lt;/a&gt;)&lt;/p&gt;
+
+ &lt;p&gt;Dustin configured the “desktop-test†and “desktop-build†docker images to build automatically on push. That means that you can modify the Dockerfile under `testing/docker`, push to try, and have the try job run in the resulting image, all without pushing any images. This should enable much quicker iteration on tweaks to the docker images. Note, however, that updates to the base OS images (ubuntu1204-build and centos6-build) still require manual pushes.&lt;/p&gt;
+
+ &lt;p&gt;Mark landed Puppet code for base windows 10 support including secrets and ssh keys management.&lt;/p&gt;
+
+ &lt;p&gt;&lt;b&gt;Improve CI pipeline:&lt;/b&gt;&lt;/p&gt;
+
+ &lt;p&gt;Vlad and Amy repurposed 10 Windows XP machines as Windows 7 to improve the wait times in that test pool (&lt;a href=&quot;https://bugzil.la/1239785&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/1239785&lt;/a&gt;)
+ Armen and Joel have been working on porting the Gecko tests to run under TaskCluster, and have narrowed the failures down to the single digits. This puts us on-track to enable Linux debug builds and tests in TaskCluster as the canonical build/test process.&lt;/p&gt;
+
+ &lt;p&gt;&lt;b&gt;Release:&lt;/b&gt;&lt;/p&gt;
+
+ &lt;p&gt;Ben finished up work on enhanced Release Blob validation in Balrog (&lt;a href=&quot;https://bugzil.la/703040&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/703040&lt;/a&gt;), which makes it much more difficult to enter bad data into our update server.&lt;/p&gt;
+
+ &lt;p&gt;You may recall Mihai, our former intern who &lt;a href=&quot;http://coopcoopbware.tumblr.com/post/133490693210/welcome-back-mihai&quot; target=&quot;_blank&quot;&gt;we just hired back in November&lt;/a&gt;. Shortly after joining the team, he jumped into the &lt;a href=&quot;https://wiki.mozilla.org/ReleaseEngineering/Releaseduty&quot; target=&quot;_blank&quot;&gt;releaseduty&lt;/a&gt; rotation to provide much-needed extra bandwidth. The learning curve here is steep, but over the course of the Firefox 44 release cycle, he’s taken on more and more responsibility. He’s even volunteered to do releaseduty for the Firefox 45 release cycle as well. Perhaps the most impressive thing is that he’s also taken the time to update (or write) the releaseduty docs so that the next person who joins the rotation will be that much further ahead of the game. Thanks for your hard work here, Mihai!&lt;/p&gt;
+
+ &lt;p&gt;&lt;b&gt;Operational:&lt;/b&gt;&lt;/p&gt;
+
+ &lt;p&gt;Hal did some cleanup work to remove unused mozharness configs and directories from the build mercurial repos. These resources have long-since moved into the main mozilla-central tree. Hopefully this will make it easier for contributors to find the canonical copy! (&lt;a href=&quot;https://bugzil.la/1239003&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/1239003&lt;/a&gt;)&lt;/p&gt;
+
+ &lt;p&gt;&lt;b&gt;Hiring:&lt;/b&gt;&lt;/p&gt;
+
+ &lt;p&gt;We’re still hiring for a full-time &lt;a href=&quot;https://careers.mozilla.org/position/oi8b2fwn&quot; target=&quot;_blank&quot;&gt;Build &amp;amp; Release Engineer&lt;/a&gt;, and we are still accepting applications for &lt;a href=&quot;https://careers.mozilla.org/position/ofA51fwF&quot; target=&quot;_blank&quot;&gt;interns for 2016&lt;/a&gt;. Come join us!&lt;/p&gt;
+
+ &lt;p&gt;Well, I don’t know about you, but all that hard work makes me hungry for pie. See you next week!&lt;/p&gt;</description>
+ <pubDate>Fri, 22 Jan 2016 20:49:38 +0000</pubDate>
+ </item>
+ <item>
+ <title>Air Mozilla: Foundation Demos January 22 2016</title>
+ <guid isPermaLink="true">https://air.mozilla.org/foundation-demos-january-22-2016/</guid>
+ <link>https://air.mozilla.org/foundation-demos-january-22-2016/</link>
+ <description>&lt;p&gt;
+ &lt;img alt=&quot;Foundation Demos January 22 2016&quot; class=&quot;wp-post-image&quot; height=&quot;90&quot; src=&quot;https://air.cdn.mozilla.net/media/cache/1c/a0/1ca0b9b2609cdd4e6e3577a8c3df8cfc.jpg&quot; width=&quot;160&quot; /&gt;
+ Mozilla Foundation Demos January 22 2016
+ &lt;/p&gt;</description>
+ <pubDate>Fri, 22 Jan 2016 18:00:00 +0000</pubDate>
+ <dc:creator>Air Mozilla</dc:creator>
+ </item>
+ <item>
+ <title>Support.Mozilla.Org: What’s up with SUMO – 22nd January</title>
+ <guid isPermaLink="false">http://blog.mozilla.org/sumo/?p=3667</guid>
+ <link>https://blog.mozilla.org/sumo/2016/01/22/whats-up-with-sumo-22nd-january/</link>
+ <description>&lt;p&gt;&lt;strong&gt;Hello, SUMO Nation!&lt;/strong&gt;&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;http://blog.mozilla.org/sumo/files/2016/01/sumo_logo.png&quot;&gt;&lt;img alt=&quot;sumo_logo&quot; class=&quot;aligncenter size-full wp-image-3670&quot; height=&quot;387&quot; src=&quot;http://blog.mozilla.org/sumo/files/2016/01/sumo_logo.png&quot; width=&quot;383&quot; /&gt;&lt;/a&gt;The third week of the new year is already behind us. Time flies when you’re not paying attention… What are you going to do this weekend? Let us know in the comments, if you feel like sharing :-) I hope to be in the mountains, getting some fresh (bracing) air, and enjoying nature.&lt;/p&gt;
+ &lt;h3&gt;&lt;strong class=&quot;username&quot;&gt;Welcome, new contributors!&lt;br /&gt;
+ &lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li class=&quot;author&quot;&gt;
+ &lt;div class=&quot;author&quot;&gt;&lt;a class=&quot;username&quot; href=&quot;https://support.mozilla.org/user/johnmwc2&quot; target=&quot;_blank&quot;&gt;johnmwc2&lt;/a&gt;&lt;/div&gt;
+ &lt;/li&gt;
+ &lt;li class=&quot;author&quot;&gt;&lt;a class=&quot;author-name&quot; href=&quot;https://support.mozilla.org/user/myanesp&quot; target=&quot;_blank&quot;&gt;myanesp&lt;/a&gt;&lt;/li&gt;
+ &lt;li class=&quot;author&quot;&gt;&lt;a class=&quot;author-name&quot; href=&quot;https://support.mozilla.org/user/Harish.A&quot; target=&quot;_blank&quot;&gt;Harish.A&lt;/a&gt;&lt;/li&gt;
+ &lt;li class=&quot;author&quot;&gt;&lt;a class=&quot;author-name&quot; href=&quot;https://support.mozilla.org/user/hoolibob&quot; target=&quot;_blank&quot;&gt;hoolibob&lt;/a&gt;&lt;/li&gt;
+ &lt;li class=&quot;author&quot;&gt;&lt;a class=&quot;author-name&quot; href=&quot;https://support.mozilla.org/user/Meteoro890&quot; target=&quot;_blank&quot;&gt;Meteoro890&lt;/a&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;div class=&quot;author&quot;&gt;If you just joined us, don’t hesitate – come over and &lt;a href=&quot;https://support.mozilla.org/forums/buddies&quot; target=&quot;_blank&quot;&gt;say “hi†in the forums!&lt;/a&gt;&lt;/div&gt;
+ &lt;div class=&quot;author&quot;&gt;&lt;/div&gt;
+ &lt;div class=&quot;author&quot;&gt;
+ &lt;h3&gt;&lt;strong&gt;Contributors of the week&lt;br /&gt;
+ &lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-z74z1rz89z69z76zbz72zz69zz67z9z82zniz71z&quot;&gt;&lt;a href=&quot;https://support.mozilla.org/user/safwan.rahman&quot; target=&quot;_blank&quot;&gt;Safwan&lt;/a&gt; for his work on the &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=619284&quot; target=&quot;_blank&quot;&gt;draft feature for l10n / KB editing&lt;/a&gt; – rock on!&lt;/span&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://support.mozilla.org/user/artist&quot; target=&quot;_blank&quot;&gt;Artist&lt;/a&gt; and &lt;a href=&quot;https://support.mozilla.org/user/pollti&quot; target=&quot;_blank&quot;&gt;Pollti&lt;/a&gt; for their the work on updating important articles for Focus with limited time – woot!&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid64&quot;&gt;
+ &lt;p&gt;&lt;strong&gt;&lt;span style=&quot;text-decoration: underline;&quot;&gt;We salute you!&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;author&quot;&gt;Don’t forget that if you are new to SUMO and someone helped you get started in a nice way you can &lt;a href=&quot;https://support.mozilla.org/forums/buddies/711364?last=65670&quot; target=&quot;_blank&quot;&gt;nominate them for the Buddy of the Month!&lt;/a&gt;&lt;/div&gt;
+ &lt;div class=&quot;author&quot;&gt;&lt;/div&gt;
+ &lt;/div&gt;
+ &lt;h3&gt;&lt;strong&gt;Most recent SUMO Community meeting&lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://public.etherpad-mozilla.org/p/sumo-2016-01-18&quot; target=&quot;_blank&quot;&gt;You can read the notes here&lt;/a&gt; (most of the staff members were AFK due to MLK Day in the US) and see the video on our &lt;a href=&quot;https://www.youtube.com/channel/UCaiposaIhA7HfMqH2NIciyA/videos&quot; target=&quot;_blank&quot;&gt;YouTube channel&lt;/a&gt; and &lt;a href=&quot;https://air.mozilla.org/search/?q=sumo&quot; target=&quot;_blank&quot;&gt;at AirMozilla&lt;/a&gt;.&lt;del&gt; &lt;/del&gt;&lt;del&gt;&lt;br /&gt;
+ &lt;/del&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;strong&gt;IMPORTANT: We are considering changing the way the meetings work. Help us figure out what’s best for you – join the discussion on the forums in this thread: &lt;a href=&quot;https://support.mozilla.org/en-US/forums/contributors/711752?last=67873&quot;&gt;(Monday) Community Meetings in 2016&lt;/a&gt;.&lt;/strong&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;&lt;strong&gt;The next SUMO Community meeting… &lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li style=&quot;text-align: left;&quot;&gt;is happening on &lt;a href=&quot;https://public.etherpad-mozilla.org/p/sumo-2016-01-25&quot; target=&quot;_blank&quot;&gt;Monday the 25th – join us&lt;/a&gt;!&lt;/li&gt;
+ &lt;li style=&quot;text-align: left;&quot;&gt;&lt;strong&gt;Reminder: if you want to add a discussion topic to the upcoming meeting agenda:&lt;/strong&gt;
+ &lt;ul&gt;
+ &lt;li style=&quot;text-align: left;&quot;&gt;Start a thread in the &lt;a href=&quot;https://support.mozilla.org/forums/contributors&quot; target=&quot;_blank&quot;&gt;Community Forums&lt;/a&gt;, so that everyone in the community can see what will be discussed and voice their opinion here before Monday (this will make it easier to have an efficient meeting).&lt;/li&gt;
+ &lt;li style=&quot;text-align: left;&quot;&gt;Please do so as soon as you can before the meeting, so that people have time to read, think, and reply (and also add it to the agenda).&lt;/li&gt;
+ &lt;li style=&quot;text-align: left;&quot;&gt;If you can, please attend the meeting in person (or via IRC), so we can follow up on your discussion topic during the meeting with your feedback.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;&lt;strong class=&quot;author-g-ivsra51ph44x461i&quot;&gt;Developers&lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://edwin.mozilla.io/t/sumo&quot; target=&quot;_blank&quot;&gt;You can see the current state of the backlog our developers are working on here&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://public.etherpad-mozilla.org/p/sumo-p-2016-01-21&quot; target=&quot;_blank&quot;&gt;The latest SUMO Platform meeting notes can be found here&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;Interested in learning how Kitsune (the engine behind SUMO) works? &lt;a href=&quot;http://kitsune.readthedocs.org/&quot; target=&quot;_blank&quot;&gt;Read more about it here&lt;/a&gt; and &lt;a href=&quot;https://github.com/mozilla/kitsune/&quot; target=&quot;_blank&quot;&gt;fork it on GitHub&lt;/a&gt;!&lt;/li&gt;
+ &lt;li&gt;We have a new link for promoting contributions to Kitsune’s code. Please use &lt;strong&gt;http://mzl.la/SUMOdev&lt;/strong&gt; whenever you want to show interested people to see what Kitsune is all about – thanks!&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;&lt;a href=&quot;http://blog.mozilla.org/sumo/files/2016/01/mission_developers.png&quot;&gt;&lt;img alt=&quot;mission_developers&quot; class=&quot;aligncenter size-full wp-image-3668&quot; height=&quot;406&quot; src=&quot;http://blog.mozilla.org/sumo/files/2016/01/mission_developers.png&quot; width=&quot;437&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
+ &lt;h3&gt;&lt;strong&gt;Social&lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Next week, there will be a kick-off meeting for the rethinking of Mozilla’s general support strategy through social networks. &lt;a href=&quot;https://support.mozilla.org/user/Madasan&quot; target=&quot;_blank&quot;&gt;Are you interested in taking part? Let Madalina know!&lt;/a&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;&lt;strong&gt;Community&lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;The NDA process and list is currently being reworked under the leadership of the Participation Team. Expect to see messaging on this subject in the coming days.&lt;/li&gt;
+ &lt;li&gt;
+ &lt;div class=&quot;title&quot;&gt;&lt;strong&gt;&lt;a href=&quot;https://support.mozilla.org/forums/contributors/711729?last=67763&quot;&gt;IMPORTANT: take a look at our Work Week Summary for Mozlando. We need your feedback for a few things there.&lt;/a&gt;&lt;/strong&gt;&lt;/div&gt;
+ &lt;/li&gt;
+ &lt;li&gt;Are you going to FOSDEM next week? Would you like to have a small SUMO-meetup? &lt;a href=&quot;https://support.mozilla.org/user/vesper&quot; target=&quot;_blank&quot;&gt;Let me know&lt;/a&gt;!&lt;/li&gt;
+ &lt;li&gt;
+ &lt;div class=&quot;title&quot;&gt;Ongoing reminder: if you think you can benefit from getting &lt;a href=&quot;https://wiki.mozilla.org/Community_Hardware&quot; target=&quot;_blank&quot;&gt;a second-hand device&lt;/a&gt; to help you with contributing to SUMO, you know where to find us.&lt;/div&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;&lt;a href=&quot;http://blog.mozilla.org/sumo/files/2016/01/hero_support.png&quot;&gt;&lt;img alt=&quot;hero_support&quot; class=&quot;aligncenter size-full wp-image-3669&quot; height=&quot;383&quot; src=&quot;http://blog.mozilla.org/sumo/files/2016/01/hero_support.png&quot; width=&quot;367&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
+ &lt;div class=&quot;&quot;&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid83&quot;&gt;
+ &lt;h3&gt;&lt;strong class=&quot;author-g-ivsra51ph44x461i&quot;&gt;Localization&lt;/strong&gt;&lt;/h3&gt;
+ &lt;/div&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid95&quot;&gt;
+ &lt;ul&gt;
+ &lt;li&gt;You can &lt;a href=&quot;https://support.mozilla.org/forums/l10n-forum/711781&quot; target=&quot;_blank&quot;&gt;read more about the recent “infrequent contributor survey†in this thread&lt;/a&gt;. In short: the good news is that we’re doing a good job at making it easy enough for everyone to contribute. The bad news – we’re not doing enough to make sure they know what to do after their first contribution. Expect some changes in the messaging for first-time contributors to the KB :-)&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1012384&quot; target=&quot;_blank&quot;&gt;Our magical l10n dashboards keep being magical&lt;/a&gt; ;-) Thank you for your patience. If you see any discrepancies between the number of localized articles and the percentage shown in the bar, file a bug!&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid75&quot;&gt;
+ &lt;h3&gt;&lt;strong&gt;Firefox&lt;br /&gt;
+ &lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;strong&gt;for Android&lt;/strong&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://support.mozilla.org/forums/contributors/711712?last=67653&quot;&gt;Learn more about Firefox 43 for Android from the official thread with release notes / issues / discussions&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;
+ &lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://support.mozilla.org/forums/contributors/711718?last=67822&quot;&gt;Reminder: Roland is sharing Firefox 44 for Android release notes / issues / discussions&lt;/a&gt; with everyone in the forum.&lt;/div&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;strong&gt;for Desktop&lt;/strong&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Heads up – next week should be release week! Keep your eyes peeled ;-)&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;strong&gt;for iOS&lt;/strong&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid85&quot;&gt;
+ &lt;ul class=&quot;list-bullet1&quot;&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-107uz69zz81zhz78z0z78zz84zz66zz76zz82zz77zj&quot;&gt;No news from the world of Firefox for iOS this week.&lt;/span&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+ &lt;p&gt;Thank you for reading all the way down here… More to come next week! You know where to find us, so see you around – keep rocking the open &amp;amp; helpful web!&lt;/p&gt;</description>
+ <pubDate>Fri, 22 Jan 2016 17:43:56 +0000</pubDate>
+ <dc:creator>Michał</dc:creator>
+ </item>
+ <item>
+ <title>Air Mozilla: Bay Area Rust Meetup January 2016</title>
+ <guid isPermaLink="true">https://air.mozilla.org/bay-area-rust-meetup-january-2016/</guid>
+ <link>https://air.mozilla.org/bay-area-rust-meetup-january-2016/</link>
+ <description>&lt;p&gt;
+ &lt;img alt=&quot;Bay Area Rust Meetup January 2016&quot; class=&quot;wp-post-image&quot; height=&quot;90&quot; src=&quot;https://air.cdn.mozilla.net/media/cache/87/4f/874f4abef76f55213d50e43d6417ed99.png&quot; width=&quot;160&quot; /&gt;
+ Bay Area Rust meetup for January 2016. Topics TBD.
+ &lt;/p&gt;</description>
+ <pubDate>Fri, 22 Jan 2016 03:00:00 +0000</pubDate>
+ <dc:creator>Air Mozilla</dc:creator>
+ </item>
+ <item>
+ <title>Mitchell Baker: Honored to Participate in New UN Panel on Women’s Economic Empowerment</title>
+ <guid isPermaLink="false">https://blog.lizardwrangler.com/?p=3953</guid>
+ <link>http://blog.lizardwrangler.com/2016/01/22/honored-to-participate-in-new-un-panel-on-womens-economic-empowerment/</link>
+ <description>Women’s economic empowerment is necessary for many reasons. It is necessary to bring health, safety and opportunity to half of humanity. It is necessary to bring investment and health to families and communities. It is necessary to unlock economic growth and build more stable societies. Today the UN Secretary General Ban Ki-moon launched the first […]</description>
+ <pubDate>Fri, 22 Jan 2016 02:45:58 +0000</pubDate>
+ <dc:creator>Mitchell Baker</dc:creator>
+ </item>
+ <item>
+ <title>Mozilla WebDev Community: Beer and Tell – January 2016</title>
+ <guid isPermaLink="false">https://blog.mozilla.org/webdev/?p=4082</guid>
+ <link>https://blog.mozilla.org/webdev/2016/01/21/beer-and-tell-january-2016/</link>
+ <description>&lt;p&gt;Once a month, web developers from across the Mozilla Project get together to talk about our side projects and drink, an occurrence we like to call “Beer and Tellâ€.&lt;/p&gt;
+ &lt;p&gt;There’s a &lt;a href=&quot;https://wiki.mozilla.org/Webdev/Beer_And_Tell/January_2016&quot;&gt;wiki page available&lt;/a&gt; with a list of the presenters, as well as links to their presentation materials. There’s also a &lt;a href=&quot;https://air.mozilla.org/webdev-beer-and-tell-january-2016/&quot;&gt;recording available&lt;/a&gt; courtesy of Air Mozilla.&lt;/p&gt;
+ &lt;h3&gt;shobson: CSS-Only Disco Ball&lt;/h3&gt;
+ &lt;p&gt;First up was &lt;a href=&quot;https://mozillians.org/en-US/u/stephaniehobson/&quot;&gt;shobson&lt;/a&gt; with a cool demo of an &lt;a href=&quot;http://codepen.io/stephaniehobson/pen/ZGZBVW?editors=110&quot;&gt;animated disco ball made entirely with CSS&lt;/a&gt;. The demo uses a repeated radial gradient for the background, and linear gradients plus a border radius for the disco ball itself. The demo was made for use in shobson’s &lt;a href=&quot;https://www.youtube.com/watch?v=7poVasAQjos&quot;&gt;WordCamp talk&lt;/a&gt; about debugging CSS. A &lt;a href=&quot;http://stephaniehobson.ca/wordpress/2015/08/15/how-to-debug-css/&quot;&gt;blog post&lt;/a&gt; with notes from the talk is available as well.&lt;/p&gt;
+ &lt;h3&gt;craigcook: Proton – A CSS Framework for Prototyping&lt;/h3&gt;
+ &lt;p&gt;Next was &lt;a href=&quot;https://mozillians.org/en-US/u/craigcook/&quot;&gt;craigcook&lt;/a&gt;, who presented &lt;a href=&quot;http://craigcook.github.io/proton/&quot;&gt;Proton&lt;/a&gt;. It’s a CSS framework that is intentionally ugly to encourage use for prototypes only. Unlike other CSS frameworks, the temptation to reuse the classes from the framework in your final page doesn’t occur, which helps avoid the presentational classes that plague sites built using a framework normally.&lt;/p&gt;
+ &lt;p&gt;Proton’s website includes an overview of the layout and components provided, as well as examples of prototypes made using the framework.&lt;/p&gt;
+ &lt;hr /&gt;
+ &lt;p&gt;If you’re interested in attending the next Beer and Tell, sign up for the &lt;a href=&quot;https://lists.mozilla.org/listinfo/dev-webdev&quot;&gt;dev-webdev@lists.mozilla.org mailing list&lt;/a&gt;. An email is sent out a week beforehand with connection details. You could even add yourself to the wiki and show off your side-project!&lt;/p&gt;
+ &lt;p&gt;See you next month!&lt;/p&gt;</description>
+ <pubDate>Thu, 21 Jan 2016 18:56:46 +0000</pubDate>
+ <dc:creator>Michael Kelly</dc:creator>
+ </item>
+ <item>
+ <title>About:Community: This Month at Mozilla</title>
+ <guid isPermaLink="false">http://blog.mozilla.org/community/?p=2287</guid>
+ <link>http://blog.mozilla.org/community/2016/01/21/this-month-at-mozilla/</link>
+ <description>&lt;p style=&quot;text-align: center;&quot;&gt;&lt;em&gt;A lot of exciting things are happening with Participation at Mozilla this month. Here’s a quick round-up of some of the things that are going on!&lt;/em&gt;&lt;/p&gt;
+ &lt;h3&gt;&lt;b&gt;Mozillians Profiles Got a Facelift: &lt;/b&gt;&lt;/h3&gt;
+ &lt;p&gt;Since the start of this year, the Participation Infrastructure team has had a renewed focus on making mozillians.org a modern community directory to meet Mozilla’s growing needs.&lt;/p&gt;
+ &lt;p&gt;Their first target for 2016 was to improve the UX on the profile edit interface.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;https://blog.mozilla.org/community/files/2016/01/new-profile-768x548.png&quot;&gt;&lt;img alt=&quot;new-profile-768x548&quot; class=&quot;aligncenter wp-image-2288 size-large&quot; height=&quot;428&quot; src=&quot;https://blog.mozilla.org/community/files/2016/01/new-profile-768x548-600x428.png&quot; width=&quot;600&quot; /&gt;&lt;/a&gt;&lt;br /&gt;
+ â€We chose it due to relatively self-contained nature of it, and cause many people were not happy with the current UX. After research of existing tools and applying latest best practices, we designed, coded and deployed a new profile edit interface (which by the way is renamed to Settings now) that we are happy to deliver to all Mozillians.â€&lt;/p&gt;
+ &lt;p&gt;Read the full blog &lt;a href=&quot;http://pierros.papadeas.gr/?p=447&quot;&gt;here&lt;/a&gt;!&lt;/p&gt;
+ &lt;h3&gt;&lt;b&gt;There are New Ways to Bring Your Design Skills to Mozilla: &lt;/b&gt;&lt;/h3&gt;
+ &lt;p&gt;Are you a passionate designer looking to contribute to Mozilla? You’ll be happy to hear there is a new way to contribute to the many design projects around Mozilla! Submit issues, find collaborators, and work on open source projects by getting involved!&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;You can check out the projects looking for help, or submit your own on the &lt;a href=&quot;https://github.com/mozilla/Community-Design/issues&quot;&gt;GitHub Repo&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://docs.google.com/a/mozilla.com/forms/d/1Tw3Mw_CMiqcIQrJF7TB1yIETGYec__NiVhaSz0CAaE8/viewform&quot;&gt;Sign-up to the mailing list&lt;/a&gt; to be added as a contributor to the Repo, added to the regular meeting list, and to get emails about GitHub trainings and more!&lt;/li&gt;
+ &lt;li&gt;And read&lt;a href=&quot;http://elioqoshi.me/en/2016/01/mozilla-community-design-kickoff/&quot;&gt; a blogpost&lt;/a&gt; about the project and its first meeting.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;Learn more &lt;a href=&quot;https://discourse.mozilla-community.org/c/community-design&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
+ &lt;h3&gt;&lt;b&gt;136 Volunteers Are Going to Singapore: &lt;/b&gt;&lt;/h3&gt;
+ &lt;p&gt;This weekend 136 participation leaders from all over the world are&lt;a href=&quot;https://twitter.com/thephoenixbird/status/690181985222926336&quot;&gt; heading to Singapore&lt;/a&gt; to undergo two days of&lt;a href=&quot;https://wiki.mozilla.org/Participation/Global_Gatherings_2015&quot;&gt; leadership training&lt;/a&gt; to develop the skills, knowledge and attitude to lead Participation in 2016.&lt;/p&gt;
+ &lt;div class=&quot;wp-caption aligncenter&quot; id=&quot;attachment_2289&quot; style=&quot;width: 609px;&quot;&gt;&lt;a href=&quot;https://blog.mozilla.org/community/files/2016/01/CZQE241WIAA6R2J.jpg&quot;&gt;&lt;img alt=&quot;Photo credit @thephoenixbird on Twitter&quot; class=&quot;wp-image-2289 size-full&quot; height=&quot;337&quot; src=&quot;https://blog.mozilla.org/community/files/2016/01/CZQE241WIAA6R2J.jpg&quot; width=&quot;599&quot; /&gt;&lt;/a&gt;&lt;p class=&quot;wp-caption-text&quot;&gt;Photo credit @&lt;a href=&quot;https://twitter.com/thephoenixbird/status/690181985222926336&quot; target=&quot;_blank&quot;&gt;thephoenixbird&lt;/a&gt; on Twitter&lt;/p&gt;&lt;/div&gt;
+ &lt;p&gt;If you know someone attending don’t forget to share your questions and goals with them, and follow along over the weekend by watching the hashtag&lt;a href=&quot;https://twitter.com/search?q=%23mozsummit&quot;&gt; #MozSummit&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;Stay tuned after the event for a debrief of the weekend!&lt;/p&gt;
+ &lt;h3&gt;&lt;b&gt;Friday’s Plenary from Mozlando is now public on Air Mozilla: &lt;/b&gt;&lt;/h3&gt;
+ &lt;p&gt;If you’re interested in learning more about all the exciting new features, projects, and plans that were presented at Mozlando look no further! You can now watch the final plenary sessions on Air Mozilla (it’s a lot of fun so I highly recommend it!) &lt;a href=&quot;https://air.mozilla.org/channels/mozlando/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;Share your questions and comments on discourse &lt;a href=&quot;https://discourse.mozilla-community.org/t/friday-plenary-from-mozlando-now-public-on-air-mozilla/6659&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;&lt;em&gt;Look forward to more updates like these in the coming months!&lt;/em&gt;&lt;/p&gt;</description>
+ <pubDate>Thu, 21 Jan 2016 17:58:33 +0000</pubDate>
+ <dc:creator>Lucy Harris</dc:creator>
+ </item>
+ <item>
+ <title>Mozilla Privacy Blog: Prioritizing privacy: Good for business</title>
+ <guid isPermaLink="false">https://blog.mozilla.org/netpolicy/?p=912</guid>
+ <link>https://blog.mozilla.org/netpolicy/2016/01/21/prioritizing-privacy-good-for-business/</link>
+ <description>&lt;p&gt;&lt;em&gt;This was originally posted at &lt;a href=&quot;http://staysafeonline.org/blog/prioritizing-privacy-good-for-business/&quot;&gt;StaySafeOnline.org&lt;/a&gt; in advance of &lt;a href=&quot;http://www.staysafeonline.org/data-privacy-day/events/&quot;&gt;Data Privacy Day&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
+ &lt;p&gt;Data Privacy Day – which arrives in just a week – is a day designed to raise awareness and promote best practices for privacy and data protection. It is a day that looks to the future and recognizes that we can and should do better as an industry. It reminds us that we need to focus on the importance of having the trust of our users.&lt;/p&gt;
+ &lt;p&gt;We seek to build trust so we can collectively create the Web our users want – the Web we all want.&lt;/p&gt;
+ &lt;p&gt;That Web is based on relationships, the same way that the offline world is. When I log in to a social media account, schedule a grocery delivery online or browse the news, I’m relying on those services to respect my data. While companies are innovating their products and services, they need to be innovating on user trust as well, which means designing to address privacy concerns – and making smart choices (early!) about how to manage data.&lt;/p&gt;
+ &lt;p&gt;A &lt;a href=&quot;http://www.pewinternet.org/2016/01/14/privacy-and-information-sharing/&quot;&gt;recent survey by Pew&lt;/a&gt; highlights the thought that each user puts into their choices – and the contextual considerations in various scenarios. They concluded that many participants were annoyed and uncertain by how their information was used, and they are choosing not to interact with those services that they don’t trust. This is a clear call to businesses to foster more trust with their users, which starts by making sure that there are people empowered within your company to ask the right questions: what do your users expect? What data do you need to collect? How can you communicate about that data collection? How should you protect their data? Is holding on to data a risk, or should you delete it?&lt;/p&gt;
+ &lt;p&gt;It’s crucial that users are a part of this process – consumers’ data is needed to offer cool, new experiences and a user needs to trust you in order to choose to give you their data. Pro-user innovation can’t happen in a vacuum – the system as it stands today isn’t doing a good job of aligning user interests with business incentives. Good user decisions can be good business decisions, but only if we create thoughtful user-centric products in a way that closes the feedback loop so that positive user experiences are rewarded with better business outcomes.&lt;/p&gt;
+ &lt;p&gt;Not prioritizing privacy in product decisions will impact the bottom line. From the many data breaches over the last few years to increasing evidence of eroding trust in online services, data practices are proving to be the dark horse in the online economy. When a company loses user trust, whether on privacy or &lt;a href=&quot;https://medium.com/@davidamerland/the-cost-of-losing-trust-97d764a1e696&quot;&gt;anything else&lt;/a&gt;, it loses customers and the potential for growth.&lt;/p&gt;
+ &lt;p&gt;Privacy means different things to different people but what’s clear is that people make decisions about the products and services that they use based on how those companies choose to treat their users. Over this time, the Internet ecosystem has evolved, as has its relationship with users – and some aspects of this evolution threaten the trust that lies at the heart of that relationship. Treating a user as a target – whether for an ad, purchase, or service – undermines the trust and relationship that a business may have with a consumer.&lt;/p&gt;
+ &lt;p&gt;The solution is not to abandon the massive value that robust data can bring to users, but rather, to collect and use data leanly, productively and transparently. At Mozilla, we have created a strong set of internal data practices to ensure that data decisions align with our &lt;a href=&quot;https://www.mozilla.org/en-US/privacy/principles/&quot;&gt;privacy principles&lt;/a&gt;. As an industry, we need to keep users at the center of the product vision rather than viewing them as targets of the product – it’s the only way to stay true to consumers and deliver the best, most trusted experiences possible.&lt;/p&gt;
+ &lt;p&gt;Want to hear more about how businesses can build relationships with their users by focusing on trust and privacy? We’re holding events in Washington, D.C., and &lt;a href=&quot;https://www.eventbrite.com/e/january-privacy-lab-privacy-for-startups-tickets-19849219550?aff=es2&quot;&gt;San Francisco&lt;/a&gt; with some of our partners to talk about it. Please join us!&lt;/p&gt;</description>
+ <pubDate>Thu, 21 Jan 2016 17:42:00 +0000</pubDate>
+ <dc:creator>Heather West</dc:creator>
+ </item>
+ <item>
+ <title>J.C. Jones: Issuance Rate for Let's Encrypt</title>
+ <guid isPermaLink="false">https://tacticalsecret.com/tag/mozilla/rss/9c39ad13-14ae-4456-a84e-13612637d832</guid>
+ <link>https://tacticalsecret.com/issuance-rate-for-lets-encrypt/</link>
+ <description>&lt;p&gt;Gathering data from &lt;a href=&quot;https://github.com/jcjones/letsencrypt_statistics&quot;&gt;Certificate Transparency logs&lt;/a&gt;, here's a snapshot in time of Let's Encrypt's certificate issuance rate per minute from 7-21 January 2016. On 20 January, DreamHost launched formal support for Let's Encrypt, which coincides with a rate increase.&lt;/p&gt;
+
+ &lt;p&gt;Note: This is mostly an experimental post with embedding charts; I've more data in the queue.&lt;/p&gt;
+
+ &lt;h3&gt;Let's Encrypt Issuance Rate per Minute&lt;/h3&gt;
+
+ &lt;div id=&quot;rate_hours&quot;&gt;&lt;/div&gt;</description>
+ <pubDate>Thu, 21 Jan 2016 17:07:25 +0000</pubDate>
+ <dc:creator>James 'J.C.' Jones</dc:creator>
+ </item>
+ <item>
+ <title>Air Mozilla: Web QA Weekly Meeting, 21 Jan 2016</title>
+ <guid isPermaLink="true">https://air.mozilla.org/web-qa-weekly-meeting-20160121/</guid>
+ <link>https://air.mozilla.org/web-qa-weekly-meeting-20160121/</link>
+ <description>&lt;p&gt;
+ &lt;img alt=&quot;Web QA Weekly Meeting&quot; class=&quot;wp-post-image&quot; height=&quot;90&quot; src=&quot;https://air.cdn.mozilla.net/media/cache/f5/13/f5137857516694df0458e837c2d3a4be.png&quot; width=&quot;160&quot; /&gt;
+ This is our weekly gathering of Mozilla'a Web QA team filled with discussion on our current and future projects, ideas, demos, and fun facts.
+ &lt;/p&gt;</description>
+ <pubDate>Thu, 21 Jan 2016 17:00:00 +0000</pubDate>
+ <dc:creator>Air Mozilla</dc:creator>
+ </item>
+ <item>
+ <title>Soledad Penades: No more tap tap tap sounds: yay!</title>
+ <guid isPermaLink="false">http://soledadpenades.com/?p=6379</guid>
+ <link>http://soledadpenades.com/2016/01/21/no-more-tap-tap-tap-sounds-yay/</link>
+ <description>&lt;p&gt;A few days ago the fantastic Fritz from the Netherlands told me that my &lt;a href=&quot;http://soledadpenades.com/files/t/2015_howa/&quot;&gt;Hands On Web Audio slides&lt;/a&gt; had stopping working and there was no sound coming out from them in Firefox.&lt;/p&gt;
+ &lt;blockquote class=&quot;twitter-tweet&quot; width=&quot;550&quot;&gt;&lt;p dir=&quot;ltr&quot; lang=&quot;en&quot;&gt;&lt;a href=&quot;https://twitter.com/supersole&quot;&gt;@supersole&lt;/a&gt; oh noes! I reopened your slides: &lt;a href=&quot;https://t.co/SO35UfljMI&quot;&gt;https://t.co/SO35UfljMI&lt;/a&gt; and it doesn't work in &lt;a href=&quot;https://twitter.com/firefox&quot;&gt;@firefox&lt;/a&gt; anymore &lt;img alt=&quot;😱&quot; class=&quot;wp-smiley&quot; src=&quot;http://s.w.org/images/core/emoji/72x72/1f631.png&quot; style=&quot;height: 1em;&quot; /&gt; (works in chrome though.. &lt;img alt=&quot;😢&quot; class=&quot;wp-smiley&quot; src=&quot;http://s.w.org/images/core/emoji/72x72/1f622.png&quot; style=&quot;height: 1em;&quot; /&gt;)&lt;/p&gt;
+ &lt;p&gt;— Boring Stranger (@fritzvd) &lt;a href=&quot;https://twitter.com/fritzvd/status/686481500611735552&quot;&gt;January 11, 2016&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;
+ &lt;p&gt;&lt;/p&gt;
+ &lt;p&gt;Which is pretty disappointing for a slide deck that is built to teach you about Web Audio!&lt;/p&gt;
+ &lt;p&gt;I noticed that the issue was only on the introductory slide which uses a modified version of Stuart Memo’s &lt;a href=&quot;https://blog.stuartmemo.com/thx-deep-note-in-javascript/&quot;&gt;fantastic THX sound recreation&lt;/a&gt;-the rest of slides did play sound.&lt;/p&gt;
+ &lt;p&gt;I built &lt;a href=&quot;http://sole.github.io/test_cases/web_audio/thx_cutting_out/&quot;&gt;an isolated test case&lt;/a&gt; &lt;small&gt;&lt;a href=&quot;https://github.com/sole/test_cases/tree/gh-pages/web_audio/thx_cutting_out&quot;&gt;(source)&lt;/a&gt;&lt;/small&gt; that used a parameter-capable version of the THX sound code, just in case the issue depended on the number of oscillators, and submitted this funnily titled bug to the Web Audio component: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1240054&quot;&gt;Entirely Web Audio generated sound cuts out after a little while, or emits random tap tap tap sounds then silence&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;I can happily confirm that the bug has been fixed in Nightly and the fix will hopefully be “uplifted†to DevEdition very soon, as it was due to a regression.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;https://paul.cx/&quot;&gt;Paul Adenot&lt;/a&gt; (who works in Web Audio and is a Web Audio spec editor, amongst a couple tons of other cool things) was really excited about the bug, saying it was very edge-casey! Yay! And he also explained what did actually happen in lay terms: “you’d have to have a frequency that goes down very very slowly so that the FFT code could not keep upâ€, which is what the THX sound is doing with the filter frequency automation.&lt;/p&gt;
+ &lt;p&gt;I want to thank both Fritz for spotting this out and letting me know and also Stuart for sharing his THX code. It’s amazing what happens when you put stuff on the net and lots of different people use it in different ways and configurations. Together we make everything more robust &lt;img alt=&quot;:-)&quot; class=&quot;wp-smiley&quot; src=&quot;http://soledadpenades.com/wp-includes/images/smilies/simple-smile.png&quot; style=&quot;height: 1em;&quot; /&gt;&lt;/p&gt;
+ &lt;p&gt;Of course also sending thanks to Paul and Ben for identifying and fixing the issue so fast! It’s not been even a week! Woohoo!&lt;/p&gt;
+ &lt;p&gt;Well done everyone! &lt;img alt=&quot;ðŸ‘&quot; class=&quot;wp-smiley&quot; src=&quot;http://s.w.org/images/core/emoji/72x72/1f44f.png&quot; style=&quot;height: 1em;&quot; /&gt;&lt;img alt=&quot;ðŸ¼&quot; class=&quot;wp-smiley&quot; src=&quot;http://s.w.org/images/core/emoji/72x72/1f3fc.png&quot; style=&quot;height: 1em;&quot; /&gt;&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;http://soledadpenades.com/?flattrss_redirect&amp;amp;id=6379&amp;amp;md5=57babe624711830f95e4b8fbd6e52c91&quot; target=&quot;_blank&quot; title=&quot;Flattr&quot;&gt;&lt;img alt=&quot;flattr this!&quot; src=&quot;http://soledadpenades.com/wp-content/plugins/flattr/img/flattr-badge-large.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;</description>
+ <pubDate>Thu, 21 Jan 2016 15:49:05 +0000</pubDate>
+ <dc:creator>sole</dc:creator>
+ </item>
+ <item>
+ <title>Pierros Papadeas: Mozillians.org Profile Edit refresh</title>
+ <guid isPermaLink="true">http://pierros.papadeas.gr/?p=447</guid>
+ <link>http://pierros.papadeas.gr/?p=447</link>
+ <description>&lt;p&gt;Since the start of this year, Participation Infrastructure team has a renewed focus on making mozillians.org a modern community directory to meet Mozilla’s growing needs. This will not be an one-time effort. We need to invest technically and programmatically in order to deliver a first-class product that will be the foundation for identity management across the Mozilla ecosystem.&lt;/p&gt;
+ &lt;p&gt;Mozillians.org is full of functionality as it is today, but is paying the debt of being developed by 5 different teams over the past 5 years. We started simple this time. Updated all core technology pieces, did privacy and security reviews, and started the process of consolidating and modernizing many of the things we do in the site.&lt;/p&gt;
+ &lt;p&gt;Our first target was Profile Edit. We chose it due to relatively self-contained nature of it, and cause many people were not happy with the current UX. After research of existing tools and applying latest best practices, we designed, coded and deployed a new profile edit interface (which by the way is renamed to Settings now) that we are happy to deliver to all Mozillians.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;http://pierros.papadeas.gr/wp-content/uploads/2016/01/new-profile.png&quot; rel=&quot;attachment wp-att-448&quot;&gt;&lt;img alt=&quot;new-profile&quot; class=&quot;aligncenter size-large wp-image-448&quot; height=&quot;417&quot; src=&quot;http://pierros.papadeas.gr/wp-content/uploads/2016/01/new-profile-1024x731.png&quot; width=&quot;584&quot; /&gt;&lt;/a&gt;Have a&lt;a href=&quot;https://mozillians.org/en-US/user/edit/&quot;&gt; look for yourself &lt;/a&gt;and don’t miss the chance to update your profile while you do it!&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;https://mozillians.org/en-US/u/comzeradd/&quot;&gt;Nikos&lt;/a&gt; (on the front-end), &lt;a href=&quot;https://mozillians.org/en-US/u/akatsoulas/&quot;&gt;Tasos&lt;/a&gt; and &lt;a href=&quot;https://mozillians.org/en-US/u/jgiannelos/&quot;&gt;Nemo&lt;/a&gt; (on the back-end) worked hard to deliver this in a speedy manner (as they are used to), and the end result is a testament to what is coming next on Mozillians.org.&lt;/p&gt;
+ &lt;p&gt;Our next target? Groups. Currently it is obscure and unclear what all those settings in groups are, what is the functionality and how teams within Mozilla will be using it. We will be tackling this soon. After that, search and stats will be our attention, in an ongoing effort to fortify mozillians.org functionality. Stay tuned, and as always feel free to &lt;a href=&quot;https://bugzilla.mozilla.org/enter_bug.cgi?product=Participation%20Infrastructure&amp;amp;component=Phonebook&quot;&gt;file bugs&lt;/a&gt; and &lt;a href=&quot;https://github.com/mozilla/mozillians&quot;&gt;contribute &lt;/a&gt;in the process.&lt;/p&gt;</description>
+ <pubDate>Thu, 21 Jan 2016 11:41:39 +0000</pubDate>
+ <dc:creator>Pierros Papadeas</dc:creator>
+ </item>
+ <item>
+ <title>Adam Lofting: Blog posts I haven’t written lately</title>
+ <guid isPermaLink="false">http://adamlofting.com/?p=1396</guid>
+ <link>http://feedproxy.google.com/~r/adamlofting/blog/~3/DoEWpBapwiw/</link>
+ <description>&lt;p&gt;Last year I joked…&lt;/p&gt;
+ &lt;blockquote class=&quot;twitter-tweet&quot; lang=&quot;en&quot;&gt;
+ &lt;p dir=&quot;ltr&quot; lang=&quot;en&quot;&gt;Thinking about writing a blog post listing the blog posts I’ve been meaning to write… Maybe that will save some time&lt;/p&gt;
+ &lt;p&gt;— Adam Lofting (@adamlofting) &lt;a href=&quot;https://twitter.com/adamlofting/status/667657889817956352&quot;&gt;November 20, 2015&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;
+ &lt;p&gt;&lt;/p&gt;
+ &lt;p&gt;Now, it has come to this.&lt;/p&gt;
+ &lt;h4&gt;9 blog posts I’ve not been writing&lt;/h4&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Working on working on the impact of impact&lt;/li&gt;
+ &lt;li&gt;Designing Games in &lt;a href=&quot;https://en.wikipedia.org/wiki/Amateur&quot; target=&quot;_blank&quot;&gt;my free time&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Moving Out (the board game)&lt;/li&gt;
+ &lt;li&gt;Mozilla Foundation 2016 KPIs&lt;/li&gt;
+ &lt;li&gt;Studying Network Science&lt;/li&gt;
+ &lt;li&gt;Learning Analytics plans for 2016&lt;/li&gt;
+ &lt;li&gt;Daily practice / you are what you do every day&lt;/li&gt;
+ &lt;li&gt;Several more A/B tests to write up from &lt;a href=&quot;http://fundraising.mozilla.org/&quot;&gt;the fundraising campaign&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;CRM Progress in 2015&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;But my most requested blog by far, is an update on the status of my shed / office that I was tagging on to the end my blog posts at this time last year. Many people at Mozfest wanted to know about the shed… so here it is.&lt;/p&gt;
+ &lt;p&gt;This time last year:&lt;/p&gt;
+ &lt;blockquote class=&quot;twitter-tweet&quot; lang=&quot;en&quot;&gt;&lt;p&gt;
+ Starting in the new office today. It will take time to make it *nice* but it works for now. &lt;a href=&quot;http://t.co/sWoC4kFNLc&quot;&gt;pic.twitter.com/sWoC4kFNLc&lt;/a&gt;&lt;/p&gt;
+ &lt;p&gt;— Adam Lofting (@adamlofting) &lt;a href=&quot;https://twitter.com/adamlofting/status/560361913339899904&quot;&gt;January 28, 2015&lt;/a&gt;
+ &lt;/p&gt;&lt;/blockquote&gt;
+ &lt;p&gt;&lt;/p&gt;
+ &lt;p&gt;Some pictures from this morning:&lt;/p&gt;
+ &lt;p&gt;&lt;img alt=&quot;office1&quot; class=&quot;alignright size-large wp-image-1398&quot; height=&quot;282&quot; src=&quot;http://adamlofting.com/wp-content/uploads/2016/01/office1-750x320.jpg&quot; width=&quot;660&quot; /&gt;&lt;/p&gt;
+ &lt;p&gt;&lt;img alt=&quot;office2&quot; class=&quot;aligncenter size-large wp-image-1399&quot; height=&quot;237&quot; src=&quot;http://adamlofting.com/wp-content/uploads/2016/01/office2-750x269.jpg&quot; width=&quot;660&quot; /&gt;&lt;/p&gt;
+ &lt;p&gt;It’s a pretty nice place to work now and it doubles as useful workshop on the weekends. It needs a few finishing touches, but the law of diminishing returns means those finishing touches are lower priority than work that needs to be done elsewhere in the house and garden. So it’ll stay like this a while longer.&lt;/p&gt;
+ &lt;div class=&quot;feedflare&quot;&gt;
+ &lt;a href=&quot;http://feeds.feedburner.com/~ff/adamlofting/blog?a=DoEWpBapwiw:VxTJGXwqhlI:yIl2AUoC8zA&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;http://feeds.feedburner.com/~ff/adamlofting/blog?d=yIl2AUoC8zA&quot; /&gt;&lt;/a&gt; &lt;a href=&quot;http://feeds.feedburner.com/~ff/adamlofting/blog?a=DoEWpBapwiw:VxTJGXwqhlI:qj6IDK7rITs&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;http://feeds.feedburner.com/~ff/adamlofting/blog?d=qj6IDK7rITs&quot; /&gt;&lt;/a&gt;
+ &lt;/div&gt;&lt;img alt=&quot;&quot; height=&quot;1&quot; src=&quot;http://feeds.feedburner.com/~r/adamlofting/blog/~4/DoEWpBapwiw&quot; width=&quot;1&quot; /&gt;</description>
+ <pubDate>Thu, 21 Jan 2016 09:44:24 +0000</pubDate>
+ <dc:creator>Adam</dc:creator>
+ </item>
+ <item>
+ <title>Tarek Ziadé: A Pelican web editor</title>
+ <guid isPermaLink="true">http://blog.ziade.org/2016/01/21/a-pelican-web-editor/</guid>
+ <link>http://blog.ziade.org/2016/01/21/a-pelican-web-editor/</link>
+ <description>&lt;p&gt;The benefit of being a father again (Freya my 3rd child, was born last week) is
+ that while on paternity leave &amp;amp; between two baby bottles, I can hack on fun stuff.&lt;/p&gt;
+ &lt;p&gt;A few months ago, I've built for my running club a Pelican-based website, check it out
+ at : &lt;a class=&quot;reference external&quot; href=&quot;http://acr-dijon.org&quot;&gt;http://acr-dijon.org&lt;/a&gt;. Nothing's special about it, except that I am not
+ the one feeding it. The content is added by people from the club that have zero
+ knowledge about softwares, let alone stuff like vim or command line tools.&lt;/p&gt;
+ &lt;p&gt;I set up a github-based flow for them, where they add content through the
+ github UI and its minimal reStructuredText preview feature - and then a few
+ of my crons update the website on the server I host.
+ For images and other media, they are uploading them via FTP using FireSSH in Firefox.&lt;/p&gt;
+ &lt;p&gt;For the comments, I've switched from Disqus to &lt;a class=&quot;reference external&quot; href=&quot;https://posativ.org/isso/&quot;&gt;ISSO&lt;/a&gt;
+ after I got annoyed by the fact that it was impossible to display a simple Disqus
+ UI for people to comment without having to log in.&lt;/p&gt;
+ &lt;p&gt;I had to make my club friends go through a minimal
+ reStructuredText syntax training, and things are more of less working now.&lt;/p&gt;
+ &lt;p&gt;The system has a few caveats though:&lt;/p&gt;
+ &lt;ul class=&quot;simple&quot;&gt;
+ &lt;li&gt;it's dependent on Github. I'd rather have everything hosted on my server.&lt;/li&gt;
+ &lt;li&gt;the github restTRucturedText preview will not display syntax errors and warnings
+ and very often, articles get broken&lt;/li&gt;
+ &lt;li&gt;the resulting reST is ugly, and it's a bit hard to force my editors to be stricter
+ about details like empty lines, not using tabs etc.&lt;/li&gt;
+ &lt;li&gt;adding folders or organizing articles from Github is a pain&lt;/li&gt;
+ &lt;li&gt;editing the metadata tags is prone to many mistakes&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;So I've decided to build my own web editing tool with the following features:&lt;/p&gt;
+ &lt;ul class=&quot;simple&quot;&gt;
+ &lt;li&gt;resTructuredText cleanup&lt;/li&gt;
+ &lt;li&gt;content browsing&lt;/li&gt;
+ &lt;li&gt;resTructuredText web editor with live preview that shows warnings &amp;amp; errors&lt;/li&gt;
+ &lt;li&gt;a little bit of wsgi glue and a few forms to create articles without
+ having to worry about metadata syntax.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;div class=&quot;section&quot; id=&quot;restructuredtext-cleanup&quot;&gt;
+ &lt;h3&gt;resTructuredText cleanup&lt;/h3&gt;
+ &lt;p&gt;The first step was to build a reStructuredText parser that would read some
+ reStructuredText and render it back into a cleaner version.&lt;/p&gt;
+ &lt;p&gt;We've imported almost 2000 articles in Pelican from the old blog, so I had
+ a &lt;strong&gt;lot&lt;/strong&gt; of samples to make my parser work well.&lt;/p&gt;
+ &lt;p&gt;I first tried &lt;a class=&quot;reference external&quot; href=&quot;https://github.com/benoitbryon/rst2rst&quot;&gt;rst2rst&lt;/a&gt; but that
+ parser was built for a very specific use case (text wrapping) and was
+ incomplete. It was not parsing all of the reStructuredText syntax.&lt;/p&gt;
+ &lt;p&gt;Inspired by it, I wrote my own little parser using &lt;strong&gt;docutils&lt;/strong&gt;.&lt;/p&gt;
+ &lt;p&gt;Understanding docutils is not a small task. This project is very powerfull
+ but quite complex. One thing that cruelly misses in docutils parser tools
+ is the ability to get the source text from any node, including its children,
+ so you can render back the same source.&lt;/p&gt;
+ &lt;p&gt;That's roughly what I had to add in my code. It's ugly but it does the job:
+ it will parse rst files and render the same content, minus all the extraneous
+ empty lines, spaces, tabs etc.&lt;/p&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;section&quot; id=&quot;content-browsing&quot;&gt;
+ &lt;h3&gt;Content browsing&lt;/h3&gt;
+ &lt;p&gt;Content browsing is pretty straightforward: my admin tool let you browse
+ the Pelican &lt;em&gt;content&lt;/em&gt; directory and lists all articles, organized by categories.&lt;/p&gt;
+ &lt;p&gt;In our case, each category has a top directory in &lt;em&gt;content&lt;/em&gt;. The browser
+ parses the articles using my parser and displays paginated lists.&lt;/p&gt;
+ &lt;p&gt;I had to add a cache system for the parser, because one of the directory
+ contains over 1000 articles -- and browsing was kind of slow :)&lt;/p&gt;
+ &lt;img alt=&quot;http://ziade.org/henet-browsing.png&quot; src=&quot;http://ziade.org/henet-browsing.png&quot; /&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;section&quot; id=&quot;restructuredtext-web-editor&quot;&gt;
+ &lt;h3&gt;resTructuredText web editor&lt;/h3&gt;
+ &lt;p&gt;The last big bit was the live editor. I've stumbled on a neat little tool
+ called &lt;strong&gt;rsted&lt;/strong&gt;, that provides a live preview of the reStructuredText
+ as you are typing it. And it includes warnings !&lt;/p&gt;
+ &lt;p&gt;Check it out: &lt;a class=&quot;reference external&quot; href=&quot;http://rst.ninjs.org/&quot;&gt;http://rst.ninjs.org/&lt;/a&gt;&lt;/p&gt;
+ &lt;p&gt;I've stripped it and kept what I needed, and included it in my app.&lt;/p&gt;
+ &lt;img alt=&quot;http://ziade.org/henet.png&quot; src=&quot;http://ziade.org/henet.png&quot; /&gt;
+ &lt;p&gt;I am quite happy with the result so far. I need to add real tests and
+ a bit of documentation, and I will start to train my club friends on it.&lt;/p&gt;
+ &lt;p&gt;The next features I'd like to add are:&lt;/p&gt;
+ &lt;ul class=&quot;simple&quot;&gt;
+ &lt;li&gt;comments management, to replace Isso (working on it now)&lt;/li&gt;
+ &lt;li&gt;smart Pelican builds. e.g. if a comment is added I don't want to rebuild the whole
+ blog (~1500 articles)&lt;/li&gt;
+ &lt;li&gt;media management&lt;/li&gt;
+ &lt;li&gt;spell checker&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;The project lives here: &lt;a class=&quot;reference external&quot; href=&quot;https://github.com/AcrDijon/henet&quot;&gt;https://github.com/AcrDijon/henet&lt;/a&gt;&lt;/p&gt;
+ &lt;p&gt;I am not going to release it, but if someone finds it useful, I could.&lt;/p&gt;
+ &lt;p&gt;It's built with Bottle &amp;amp; Bootstrap as well.&lt;/p&gt;
+ &lt;/div&gt;</description>
+ <pubDate>Thu, 21 Jan 2016 09:40:00 +0000</pubDate>
+ <dc:creator>Tarek Ziade</dc:creator>
+ </item>
+ <item>
+ <title>Nick Cameron: Closures and first-class functions</title>
+ <guid isPermaLink="false">http://www.ncameron.org/blog/rss/631106eb-e7b1-47d5-82f9-cb6ad210ea89</guid>
+ <link>http://www.ncameron.org/blog/closures-and-first-class-functions/</link>
+ <description>&lt;p&gt;I wrote a long and probably dull chapter on closures and first-class and higher-order functions in Rust. It goes into some detail on the implementation and some of the subtleties like higher-ranked lifetime bounds.&lt;/p&gt;
+
+ &lt;p&gt;I was going to post it here too, but it is really too long. Instead, pop over to the 'Rust for C++ programmers' repo and read it &lt;a href=&quot;https://github.com/nrc/r4cppp/blob/master/closures.md&quot;&gt;there&lt;/a&gt;.&lt;/p&gt;</description>
+ <pubDate>Thu, 21 Jan 2016 08:36:21 +0000</pubDate>
+ <dc:creator>Nick Cameron</dc:creator>
+ </item>
+ <item>
+ <title>Nick Desaulniers: Intro to Debugging x86-64 Assembly</title>
+ <guid isPermaLink="false">http://nickdesaulniers.github.io/blog/2016/01/20/debugging-x86-64-assembly-with-lldb-and-dtrace</guid>
+ <link>http://nickdesaulniers.github.io/blog/2016/01/20/debugging-x86-64-assembly-with-lldb-and-dtrace/</link>
+ <description>&lt;p&gt;I’m hacking on an assembly project, and wanted to document some of the tricks I
+ was using for figuring out what was going on. This post might seem a little
+ basic for folks who spend all day heads down in gdb or who do this stuff
+ professionally, but I just wanted to share a quick intro to some tools that
+ others may find useful.
+ (&lt;a href=&quot;https://pchiusano.github.io/2014-10-11/defensive-writing.html&quot;&gt;oh god, I’m doing it&lt;/a&gt;)&lt;/p&gt;
+
+ &lt;p&gt;If your coming from gdb to lldb, there’s a few differences in commands. LLDB
+ has
+ &lt;a href=&quot;http://lldb.llvm.org/lldb-gdb.html&quot;&gt;great documentation&lt;/a&gt;
+ on some of the differences. Everything in this post about LLDB is pretty much
+ there.&lt;/p&gt;
+
+ &lt;p&gt;The bread and butter commands when working with gdb or lldb are:&lt;/p&gt;
+
+ &lt;ul&gt;
+ &lt;li&gt;r (run the program)&lt;/li&gt;
+ &lt;li&gt;s (step in)&lt;/li&gt;
+ &lt;li&gt;n (step over)&lt;/li&gt;
+ &lt;li&gt;finish (step out)&lt;/li&gt;
+ &lt;li&gt;c (continue)&lt;/li&gt;
+ &lt;li&gt;q (quit the program)&lt;/li&gt;
+ &lt;/ul&gt;
+
+
+ &lt;p&gt;You can hit enter if you want to run the last command again, which is really
+ useful if you want to keep stepping over statements repeatedly.&lt;/p&gt;
+
+ &lt;p&gt;I’ve been using LLDB on OSX. Let’s say I want to debug a program I can build,
+ but is crashing or something:&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre class=&quot;line-numbers&quot;&gt;&lt;span class=&quot;line-number&quot;&gt;1&lt;/span&gt;
+ &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;sudo lldb ./asmttpd web_root
+ &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;Setting a breakpoint on jump to label:&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre class=&quot;line-numbers&quot;&gt;&lt;span class=&quot;line-number&quot;&gt;1&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;2&lt;/span&gt;
+ &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; b sys_write
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;Breakpoint 3: &lt;span class=&quot;nv&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; asmttpd&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;sys_write, &lt;span class=&quot;nv&quot;&gt;address&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x00000000000029ae
+ &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;Running the program until breakpoint hit:&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre class=&quot;line-numbers&quot;&gt;&lt;span class=&quot;line-number&quot;&gt;1&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;2&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;3&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;4&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;5&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;6&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;7&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;8&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;9&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;10&lt;/span&gt;
+ &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; r
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;Process 32236 launched: &lt;span class=&quot;s1&quot;&gt;'./asmttpd'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;x86_64&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;Process 32236 stopped
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;* thread &lt;span class=&quot;c&quot;&gt;#1: tid = 0xe69b9, 0x00000000000029ae asmttpd`sys_write, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; frame &lt;span class=&quot;c&quot;&gt;#0: 0x00000000000029ae asmttpd`sys_write&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;asmttpd&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;sys_write:
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;-&amp;gt; 0x29ae &amp;lt;+0&amp;gt;: pushq %rdi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29af &amp;lt;+1&amp;gt;: pushq %rsi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29b0 &amp;lt;+2&amp;gt;: pushq %rdx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29b1 &amp;lt;+3&amp;gt;: pushq %r10
+ &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;Seeing more of the current stack frame:&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre class=&quot;line-numbers&quot;&gt;&lt;span class=&quot;line-number&quot;&gt;1&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;2&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;3&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;4&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;5&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;6&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;7&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;8&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;9&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;10&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;11&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;12&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;13&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;14&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;15&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;16&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;17&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;18&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;19&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;20&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;21&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;22&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;23&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;24&lt;/span&gt;
+ &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; d
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;asmttpd&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;sys_write:
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;-&amp;gt; 0x29ae &amp;lt;+0&amp;gt;: pushq %rdi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29af &amp;lt;+1&amp;gt;: pushq %rsi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29b0 &amp;lt;+2&amp;gt;: pushq %rdx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29b1 &amp;lt;+3&amp;gt;: pushq %r10
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29b3 &amp;lt;+5&amp;gt;: pushq %r8
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29b5 &amp;lt;+7&amp;gt;: pushq %r9
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29b7 &amp;lt;+9&amp;gt;: pushq %rbx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29b8 &amp;lt;+10&amp;gt;: pushq %rcx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29b9 &amp;lt;+11&amp;gt;: movq %rsi, %rdx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29bc &amp;lt;+14&amp;gt;: movq %rdi, %rsi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29bf &amp;lt;+17&amp;gt;: movq &lt;span class=&quot;nv&quot;&gt;$0x1&lt;/span&gt;, %rdi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29c6 &amp;lt;+24&amp;gt;: movq &lt;span class=&quot;nv&quot;&gt;$0x2000004&lt;/span&gt;, %rax
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29cd &amp;lt;+31&amp;gt;: syscall
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29cf &amp;lt;+33&amp;gt;: popq %rcx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29d0 &amp;lt;+34&amp;gt;: popq %rbx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29d1 &amp;lt;+35&amp;gt;: popq %r9
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29d3 &amp;lt;+37&amp;gt;: popq %r8
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29 &amp;lt;+39&amp;gt;: popq %r10
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29d7 &amp;lt;+41&amp;gt;: popq %rdx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29d8 &amp;lt;+42&amp;gt;: popq %rsi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29d9 &amp;lt;+43&amp;gt;: popq %rdi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29da &amp;lt;+44&amp;gt;: retq
+ &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;Getting a back trace (call stack):&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre class=&quot;line-numbers&quot;&gt;&lt;span class=&quot;line-number&quot;&gt;1&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;2&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;3&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;4&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;5&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;6&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;7&lt;/span&gt;
+ &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; bt
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;* thread &lt;span class=&quot;c&quot;&gt;#1: tid = 0xe69b9, 0x00000000000029ae asmttpd`sys_write, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; * frame &lt;span class=&quot;c&quot;&gt;#0: 0x00000000000029ae asmttpd`sys_write&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; frame &lt;span class=&quot;c&quot;&gt;#1: 0x00000000000021b6 asmttpd`print_line + 16&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; frame &lt;span class=&quot;c&quot;&gt;#2: 0x0000000000002ab3 asmttpd`start + 35&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; frame &lt;span class=&quot;c&quot;&gt;#3: 0x00007fff9900c5ad libdyld.dylib`start + 1&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; frame &lt;span class=&quot;c&quot;&gt;#4: 0x00007fff9900c5ad libdyld.dylib`start + 1&lt;/span&gt;
+ &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;peeking at the upper stack frame:&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre class=&quot;line-numbers&quot;&gt;&lt;span class=&quot;line-number&quot;&gt;1&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;2&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;3&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;4&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;5&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;6&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;7&lt;/span&gt;
+ &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; up
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;frame &lt;span class=&quot;c&quot;&gt;#1: 0x00000000000021b6 asmttpd`print_line + 16&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;asmttpd&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;print_line:
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x21b6 &amp;lt;+16&amp;gt;: movabsq &lt;span class=&quot;nv&quot;&gt;$0x30cb&lt;/span&gt;, %rdi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x21c0 &amp;lt;+26&amp;gt;: movq &lt;span class=&quot;nv&quot;&gt;$0x1&lt;/span&gt;, %rsi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x21c7 &amp;lt;+33&amp;gt;: callq 0x29ae ; sys_write
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x21cc &amp;lt;+38&amp;gt;: popq %rcx
+ &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;back down to the breakpoint-halted stack frame:&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre class=&quot;line-numbers&quot;&gt;&lt;span class=&quot;line-number&quot;&gt;1&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;2&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;3&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;4&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;5&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;6&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;7&lt;/span&gt;
+ &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; down
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;frame &lt;span class=&quot;c&quot;&gt;#0: 0x00000000000029ae asmttpd`sys_write&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;asmttpd&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;sys_write:
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;-&amp;gt; 0x29ae &amp;lt;+0&amp;gt;: pushq %rdi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29af &amp;lt;+1&amp;gt;: pushq %rsi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29b0 &amp;lt;+2&amp;gt;: pushq %rdx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x29b1 &amp;lt;+3&amp;gt;: pushq %r10
+ &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;dumping the values of registers:&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre class=&quot;line-numbers&quot;&gt;&lt;span class=&quot;line-number&quot;&gt;1&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;2&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;3&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;4&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;5&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;6&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;7&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;8&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;9&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;10&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;11&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;12&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;13&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;14&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;15&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;16&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;17&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;18&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;19&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;20&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;21&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;22&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;23&lt;/span&gt;
+ &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; register &lt;span class=&quot;nb&quot;&gt;read&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;General Purpose Registers:
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;rax&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x0000000000002a90 asmttpd&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;start
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;rbx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x0000000000000000
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;rcx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x00007fff5fbffaf8
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;rdx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x00007fff5fbffa40
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;rdi&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x00000000000030cc start_text
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;rsi&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x000000000000000f
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;rbp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x00007fff5fbffa18
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;rsp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x00007fff5fbff9b8
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;r8&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x0000000000000000
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;r9&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x00007fff7b1670c8 atexit_mutex + 24
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;r10&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x00000000ffffffff
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;r11&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0xffffffff00000000
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;r12&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x0000000000000000
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;r13&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x0000000000000000
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;r14&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x0000000000000000
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;r15&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x0000000000000000
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;rip&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x00000000000029ae asmttpd&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;sys_write
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;rflags&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x0000000000000246
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;cs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x000000000000002b
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;fs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x0000000000000000
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;gs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x0000000000000000
+ &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;read just one register:&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre class=&quot;line-numbers&quot;&gt;&lt;span class=&quot;line-number&quot;&gt;1&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;2&lt;/span&gt;
+ &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; register &lt;span class=&quot;nb&quot;&gt;read &lt;/span&gt;rdi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;nv&quot;&gt;rdi&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x00000000000030cc start_text
+ &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;When you’re trying to figure out what system calls are made by some C code,
+ using dtruss is very helpful. dtruss is available on OSX and seems to be some
+ kind of wrapper around DTrace.&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre class=&quot;line-numbers&quot;&gt;&lt;span class=&quot;line-number&quot;&gt;1&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;2&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;3&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;4&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;5&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;6&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;7&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;8&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;9&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;10&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;11&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;12&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;13&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;14&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;15&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;16&lt;/span&gt;
+ &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;cat sleep.c
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;c&quot;&gt;#include &amp;lt;time.h&amp;gt;&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;int main &lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; struct timespec &lt;span class=&quot;nv&quot;&gt;rqtp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 2,
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; nanosleep&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&amp;amp;rqtp, NULL&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;clang sleep.c
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;sudo dtruss ./a.out
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;...all kinds of fun stuff
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;__semwait_signal&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0xB03, 0x0, 0x1&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; -1 Err#60
+ &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;If you compile with &lt;code&gt;-g&lt;/code&gt; to emit debug symbols, you can use lldb’s disassemble
+ command to get the equivalent assembly:&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre class=&quot;line-numbers&quot;&gt;&lt;span class=&quot;line-number&quot;&gt;1&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;2&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;3&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;4&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;5&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;6&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;7&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;8&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;9&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;10&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;11&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;12&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;13&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;14&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;15&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;16&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;17&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;18&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;19&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;20&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;21&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;22&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;23&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;24&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;25&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;26&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;27&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;28&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;29&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;30&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;31&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;32&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;33&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;34&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;35&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;36&lt;/span&gt;
+ &lt;span class=&quot;line-number&quot;&gt;37&lt;/span&gt;
+ &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;sh&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;clang sleep.c -g
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;lldb a.out
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; target create &lt;span class=&quot;s2&quot;&gt;&quot;a.out&quot;&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;Current executable &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;to &lt;span class=&quot;s1&quot;&gt;'a.out'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;x86_64&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;.
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; b main
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;Breakpoint 1: &lt;span class=&quot;nv&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; a.out&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;main + 16 at sleep.c:3, &lt;span class=&quot;nv&quot;&gt;address&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0x0000000100000f40
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; r
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;Process 33213 launched: &lt;span class=&quot;s1&quot;&gt;'/Users/Nicholas/code/assembly/asmttpd/a.out'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;x86_64&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;Process 33213 stopped
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;* thread &lt;span class=&quot;c&quot;&gt;#1: tid = 0xeca04, 0x0000000100000f40 a.out`main + 16 at sleep.c:3, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; frame &lt;span class=&quot;c&quot;&gt;#0: 0x0000000100000f40 a.out`main + 16 at sleep.c:3&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 1 &lt;span class=&quot;c&quot;&gt;#include &amp;lt;time.h&amp;gt;&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 2 int main &lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;-&amp;gt; 3 struct timespec &lt;span class=&quot;nv&quot;&gt;rqtp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 4 2,
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 5 0
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 6 &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 7
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;lldb&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; disassemble
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;a.out&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;main:
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f30 &amp;lt;+0&amp;gt;: pushq %rbp
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f31 &amp;lt;+1&amp;gt;: movq %rsp, %rbp
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f34 &amp;lt;+4&amp;gt;: subq &lt;span class=&quot;nv&quot;&gt;$0x20&lt;/span&gt;, %rsp
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f38 &amp;lt;+8&amp;gt;: leaq -0x10&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;%rbp&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;, %rdi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f3c &amp;lt;+12&amp;gt;: xorl %eax, %eax
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f3e &amp;lt;+14&amp;gt;: movl %eax, %esi
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt;-&amp;gt; 0x100000f40 &amp;lt;+16&amp;gt;: movq 0x49&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;%rip&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;, %rcx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f47 &amp;lt;+23&amp;gt;: movq %rcx, -0x10&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;%rbp&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f4b &amp;lt;+27&amp;gt;: movq 0x46&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;%rip&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;, %rcx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f52 &amp;lt;+34&amp;gt;: movq %rcx, -0x8&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;%rbp&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f56 &amp;lt;+38&amp;gt;: callq 0x100000f68 ; symbol stub &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt;: nanosleep
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f5b &amp;lt;+43&amp;gt;: xorl %edx, %edx
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f5d &amp;lt;+45&amp;gt;: movl %eax, -0x14&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;%rbp&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f60 &amp;lt;+48&amp;gt;: movl %edx, %eax
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f62 &amp;lt;+50&amp;gt;: addq &lt;span class=&quot;nv&quot;&gt;$0x20&lt;/span&gt;, %rsp
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f66 &amp;lt;+54&amp;gt;: popq %rbp
+ &lt;/span&gt;&lt;span class=&quot;line&quot;&gt; 0x100000f67 &amp;lt;+55&amp;gt;: retq
+ &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;Anyways, I’ve been learning some interesting things about OSX that I’ll be
+ sharing soon. If you’d like to learn more about x86-64 assembly programming,
+ you should read my other posts about
+ &lt;a href=&quot;http://nickdesaulniers.github.io/blog/2014/04/18/lets-write-some-x86-64/&quot;&gt;writing x86-64&lt;/a&gt;
+ and a toy
+ &lt;a href=&quot;http://nickdesaulniers.github.io/blog/2015/05/25/interpreter-compiler-jit/&quot;&gt;JIT for Brainfuck&lt;/a&gt;
+ (&lt;a href=&quot;https://www.reddit.com/r/programming/comments/377ov9/interpreter_compiler_jit/crkkrz4&quot;&gt;the creator of Brainfuck liked it&lt;/a&gt;).&lt;/p&gt;
+
+ &lt;p&gt;I should also do a post on
+ &lt;a href=&quot;http://rr-project.org/&quot;&gt;Mozilla’s rr&lt;/a&gt;,
+ because it can do amazing things like step backwards. Another day…&lt;/p&gt;</description>
+ <pubDate>Thu, 21 Jan 2016 04:04:00 +0000</pubDate>
+ </item>
+ <item>
+ <title>Rail Aliiev: Rebooting productivity</title>
+ <guid isPermaLink="true">https://rail.merail.ca/posts/rebooting-productivity.html</guid>
+ <link>https://rail.merail.ca/posts/rebooting-productivity.html</link>
+ <description>&lt;div&gt;&lt;p&gt;Every new year gives you an opportunity to sit back, relax,
+ &lt;span class=&quot;strike&quot;&gt;have some scotch&lt;/span&gt; and re-think the passed year. Holidays give
+ you enough free time. Even if you decide to not take a vacation around
+ the holidays, it's usually calm and peaceful.&lt;/p&gt;
+ &lt;p&gt;This time, I found myself thinking mostly about productivity, being
+ effective, feeling busy, overwhelmed with work and other related topics.&lt;/p&gt;
+ &lt;p&gt;When I started at Mozilla (almost 6 years ago!), I tried to apply all my
+ GTD and time management knowledge and techniques. Working remotely and
+ in a different time zone was an advantage - I had close to zero
+ interruptions. It worked perfect.&lt;/p&gt;
+ &lt;p&gt;Last year I realized that my productivity skills had faded away somehow.
+ 40h+ workweeks, working on weekends, delivering goals in the last week
+ of quarter don't sound like good signs. Instead of being productive I
+ felt busy.&lt;/p&gt;
+ &lt;p&gt;&quot;Every crisis is an opportunity&quot;. Time to make a step back and reboot
+ myself. Burning out at work is not a good idea. :)&lt;/p&gt;
+ &lt;p&gt;Here are some ideas/tips that I wrote down for myself you may found
+ useful.&lt;/p&gt;
+ &lt;div class=&quot;section&quot; id=&quot;health-related&quot;&gt;
+ &lt;h3&gt;Health related&lt;/h3&gt;
+ &lt;ul class=&quot;simple&quot;&gt;
+ &lt;li&gt;Morning exercises. A 20-minute walk will wake your brain up and
+ generate enough endorphins for the first half of the day.&lt;/li&gt;
+ &lt;li&gt;Meditation. 2x20min a day is ideal; 2x10min would work too. Something
+ like &lt;a class=&quot;reference external&quot; href=&quot;http://www.calm.com/&quot;&gt;calm.com&lt;/a&gt; makes this a peace of cake.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;section&quot; id=&quot;concentration&quot;&gt;
+ &lt;h3&gt;Concentration&lt;/h3&gt;
+ &lt;ul class=&quot;simple&quot;&gt;
+ &lt;li&gt;Task #1: make a daily plan. No plan - no work.&lt;/li&gt;
+ &lt;li&gt;Don't start your day by reading emails. Get one (little) thing done
+ first - THEN check your email.&lt;/li&gt;
+ &lt;li&gt;Try to define outcomes, not tasks. &quot;Ship XYZ&quot; instead of &quot;Work on XYZ&quot;.&lt;/li&gt;
+ &lt;li&gt;Meetings are time consuming, so &quot;Set a goal for each meeting&quot;.
+ Consider skipping a meeting if you don't have any goal set, unless it's a
+ beer-and-tell meeting! :)&lt;/li&gt;
+ &lt;li&gt;Constantly ask yourself if what you're working on is important.&lt;/li&gt;
+ &lt;li&gt;3-4 times a day ask yourself whether you are doing something towards
+ your goal or just finding something else to keep you busy. If you want
+ to look busy, take your phone and walk around the office with some
+ papers in your hand. Everybody will think that you are a busy person!
+ This way you can take a break and look busy at the same time!&lt;/li&gt;
+ &lt;li&gt;Take breaks! &lt;a class=&quot;reference external&quot; href=&quot;https://en.wikipedia.org/wiki/Pomodoro_Technique&quot;&gt;Pomodoro technique&lt;/a&gt; has this option
+ built-in. Taking breaks helps not only to avoid &lt;a class=&quot;reference external&quot; href=&quot;https://en.wikipedia.org/wiki/Repetitive_strain_injury&quot;&gt;RSI&lt;/a&gt;, but also
+ keeps your brain sane and gives you time to ask yourself the questions
+ mentioned above. I use &lt;a class=&quot;reference external&quot; href=&quot;http://www.workrave.org/&quot;&gt;Workrave&lt;/a&gt; on my
+ laptop, but you can use a real kitchen timer instead.&lt;/li&gt;
+ &lt;li&gt;Wear headphones, especially at office. Noise cancelling ones are even
+ better. White noise, nature sounds, or instrumental music are your
+ friends.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;section&quot; id=&quot;home-office&quot;&gt;
+ &lt;h3&gt;(Home) Office&lt;/h3&gt;
+ &lt;ul class=&quot;simple&quot;&gt;
+ &lt;li&gt;Make sure you enjoy your work environment. Why on the earth would you
+ spend your valuable time working without joy?!&lt;/li&gt;
+ &lt;li&gt;De-clutter and organize your desk. Less things around - less
+ distractions.&lt;/li&gt;
+ &lt;li&gt;Desk, chair, monitor, keyboard, mouse, etc - don't cheap out on them.
+ Your health is more important and expensive. Thanks to &lt;a class=&quot;reference external&quot; href=&quot;https://twitter.com/mhoye&quot;&gt;mhoye&lt;/a&gt; for this advice!&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;section&quot; id=&quot;other&quot;&gt;
+ &lt;h3&gt;Other&lt;/h3&gt;
+ &lt;ul class=&quot;simple&quot;&gt;
+ &lt;li&gt;Don't check email every 30 seconds. If there is an emergency, they
+ will call you! :)&lt;/li&gt;
+ &lt;li&gt;Reward yourself at a certain time. &quot;I'm going to have a chocolate at
+ 11am&quot;, or &quot;MFBT at 4pm sharp!&quot; are good examples. Don't forget, you
+ are &lt;a class=&quot;reference external&quot; href=&quot;https://en.wikipedia.org/wiki/Classical_conditioning&quot;&gt;Pavlov's dog&lt;/a&gt; too!&lt;/li&gt;
+ &lt;li&gt;Don't try to read everything NOW. Save it for later and read in a
+ batch.&lt;/li&gt;
+ &lt;li&gt;Capture all creative ideas. You can delete them later. ;)&lt;/li&gt;
+ &lt;li&gt;Prepare for next task before break. Make sure you know what's next, so
+ you can think about it during the break.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;This is my list of things that I try to use everyday. Looking forward to
+ see improvements!&lt;/p&gt;
+ &lt;p&gt;I would appreciate your thoughts this topic. Feel free to comment or
+ send a private email.&lt;/p&gt;
+ &lt;p&gt;Happy Productive New Year!&lt;/p&gt;
+ &lt;/div&gt;&lt;/div&gt;</description>
+ <pubDate>Thu, 21 Jan 2016 02:06:37 +0000</pubDate>
+ <dc:creator>Rail Aliiev</dc:creator>
+ </item>
+ <item>
+ <title>The Rust Programming Language Blog: Announcing Rust 1.6</title>
+ <guid isPermaLink="true">http://blog.rust-lang.org/2016/01/21/Rust-1.6.html</guid>
+ <link>http://blog.rust-lang.org/2016/01/21/Rust-1.6.html</link>
+ <description>&lt;p&gt;Hello 2016! We’re happy to announce the first Rust release of the year, 1.6.
+ Rust is a systems programming language focused on safety, speed, and
+ concurrency.&lt;/p&gt;
+
+ &lt;p&gt;As always, you can &lt;a href=&quot;http://www.rust-lang.org/install.html&quot;&gt;install Rust 1.6&lt;/a&gt; from the appropriate page on our
+ website, and check out the &lt;a href=&quot;https://github.com/rust-lang/rust/blob/stable/RELEASES.md#version-160-2016-01-21&quot;&gt;detailed release notes for 1.6&lt;/a&gt; on GitHub.
+ About 1100 patches were landed in this release.&lt;/p&gt;
+
+ &lt;h3 id=&quot;what-39-s-in-1-6-stable&quot;&gt;What’s in 1.6 stable&lt;/h3&gt;
+
+ &lt;p&gt;This release contains a number of small refinements, one major feature, and
+ a change to &lt;a href=&quot;https://crates.io&quot;&gt;Crates.io&lt;/a&gt;.&lt;/p&gt;
+
+ &lt;h4 id=&quot;libcore-stabilization&quot;&gt;libcore stabilization&lt;/h4&gt;
+
+ &lt;p&gt;The largest new feature in 1.6 is that &lt;a href=&quot;http://doc.rust-lang.org/nightly/core/&quot;&gt;&lt;code&gt;libcore&lt;/code&gt;&lt;/a&gt; is now stable! Rust’s
+ standard library is two-tiered: there’s a small core library, &lt;code&gt;libcore&lt;/code&gt;, and
+ the full standard library, &lt;code&gt;libstd&lt;/code&gt;, that builds on top of it. &lt;code&gt;libcore&lt;/code&gt; is
+ completely platform agnostic, and requires only a handful of external symbols
+ to be defined. Rust’s &lt;code&gt;libstd&lt;/code&gt; builds on top of &lt;code&gt;libcore&lt;/code&gt;, adding support for
+ memory allocation, I/O, and concurrency. Applications using Rust in the
+ embedded space, as well as those writing operating systems, often eschew
+ &lt;code&gt;libstd&lt;/code&gt;, using only &lt;code&gt;libcore&lt;/code&gt;.&lt;/p&gt;
+
+ &lt;p&gt;&lt;code&gt;libcore&lt;/code&gt; being stabilized is a major step towards being able to write the
+ lowest levels of software using stable Rust. There’s still future work to be
+ done, however. This will allow for a library ecosystem to develop around
+ &lt;code&gt;libcore&lt;/code&gt;, but &lt;em&gt;applications&lt;/em&gt; are not fully supported yet. Expect to hear more
+ about this in future release notes.&lt;/p&gt;
+
+ &lt;h4 id=&quot;library-stabilizations&quot;&gt;Library stabilizations&lt;/h4&gt;
+
+ &lt;p&gt;About 30 library functions and methods are now stable in 1.6. Notable
+ improvements include:&lt;/p&gt;
+
+ &lt;p&gt;The &lt;code&gt;drain()&lt;/code&gt; family of functions on collections. These methods let you move
+ elements out of a collection while allowing them to retain their backing
+ memory, reducing allocation in certain situations.&lt;/p&gt;
+
+ &lt;p&gt;A number of implementations of &lt;code&gt;From&lt;/code&gt; for converting between standard library
+ types, mainly between various integral and floating-point types.&lt;/p&gt;
+
+ &lt;p&gt;Finally, &lt;code&gt;Vec::extend_from_slice()&lt;/code&gt;, which was previously known as
+ &lt;code&gt;push_all()&lt;/code&gt;. This method has a significantly faster implementation than the
+ more general &lt;code&gt;extend()&lt;/code&gt;.&lt;/p&gt;
+
+ &lt;p&gt;See the &lt;a href=&quot;https://github.com/rust-lang/rust/blob/stable/RELEASES.md#version-160-2016-01-21&quot;&gt;detailed release notes&lt;/a&gt; for more.&lt;/p&gt;
+
+ &lt;h4 id=&quot;crates-io-disallows-wildcards&quot;&gt;Crates.io disallows wildcards&lt;/h4&gt;
+
+ &lt;p&gt;If you maintain a crate on &lt;a href=&quot;https://crates.io&quot;&gt;Crates.io&lt;/a&gt;, you might have seen
+ a warning: newly uploaded crates are no longer allowed to use a wildcard when
+ describing their dependencies. In other words, this is not allowed:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dependencies&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
+ &lt;span class=&quot;n&quot;&gt;regex&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;*&quot;&lt;/span&gt;
+ &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
+ &lt;p&gt;Instead, you must actually specify &lt;a href=&quot;http://doc.crates.io/crates-io.html#using-cratesio-based-crates&quot;&gt;a specific version or range of
+ versions&lt;/a&gt;, using one of the &lt;code&gt;semver&lt;/code&gt; crate’s various options: &lt;code&gt;^&lt;/code&gt;,
+ &lt;code&gt;~&lt;/code&gt;, or &lt;code&gt;=&lt;/code&gt;.&lt;/p&gt;
+
+ &lt;p&gt;A wildcard dependency means that you work with any possible version of your
+ dependency. This is highly unlikely to be true, and causes unnecessary breakage
+ in the ecosystem. We’ve been advertising this change as a warning for some time;
+ now it’s time to turn it into an error.&lt;/p&gt;
+
+ &lt;h3 id=&quot;contributors-to-1-6&quot;&gt;Contributors to 1.6&lt;/h3&gt;
+
+ &lt;p&gt;We had 132 individuals contribute to 1.6. Thank you so much!&lt;/p&gt;
+
+ &lt;ul&gt;
+ &lt;li&gt;Aaron Turon&lt;/li&gt;
+ &lt;li&gt;Adam Badawy&lt;/li&gt;
+ &lt;li&gt;Aleksey Kladov&lt;/li&gt;
+ &lt;li&gt;Alexander Bulaev&lt;/li&gt;
+ &lt;li&gt;Alex Burka&lt;/li&gt;
+ &lt;li&gt;Alex Crichton&lt;/li&gt;
+ &lt;li&gt;Alex Gaynor&lt;/li&gt;
+ &lt;li&gt;Alexis Beingessner&lt;/li&gt;
+ &lt;li&gt;Amanieu d'Antras&lt;/li&gt;
+ &lt;li&gt;Amit Saha&lt;/li&gt;
+ &lt;li&gt;Andrea Canciani&lt;/li&gt;
+ &lt;li&gt;Andrew Paseltiner&lt;/li&gt;
+ &lt;li&gt;androm3da&lt;/li&gt;
+ &lt;li&gt;angelsl&lt;/li&gt;
+ &lt;li&gt;Angus Lees&lt;/li&gt;
+ &lt;li&gt;Antti Keränen&lt;/li&gt;
+ &lt;li&gt;arcnmx&lt;/li&gt;
+ &lt;li&gt;Ariel Ben-Yehuda&lt;/li&gt;
+ &lt;li&gt;Ashkan Kiani&lt;/li&gt;
+ &lt;li&gt;Barosl Lee&lt;/li&gt;
+ &lt;li&gt;Benjamin Herr&lt;/li&gt;
+ &lt;li&gt;Ben Striegel&lt;/li&gt;
+ &lt;li&gt;Bhargav Patel&lt;/li&gt;
+ &lt;li&gt;Björn Steinbrink&lt;/li&gt;
+ &lt;li&gt;Boris Egorov&lt;/li&gt;
+ &lt;li&gt;bors&lt;/li&gt;
+ &lt;li&gt;Brian Anderson&lt;/li&gt;
+ &lt;li&gt;Bruno Tavares&lt;/li&gt;
+ &lt;li&gt;Bryce Van Dyk&lt;/li&gt;
+ &lt;li&gt;Cameron Sun&lt;/li&gt;
+ &lt;li&gt;Christopher Sumnicht&lt;/li&gt;
+ &lt;li&gt;Cole Reynolds&lt;/li&gt;
+ &lt;li&gt;corentih&lt;/li&gt;
+ &lt;li&gt;Daniel Campbell&lt;/li&gt;
+ &lt;li&gt;Daniel Keep&lt;/li&gt;
+ &lt;li&gt;Daniel Rollins&lt;/li&gt;
+ &lt;li&gt;Daniel Trebbien&lt;/li&gt;
+ &lt;li&gt;Danilo Bargen&lt;/li&gt;
+ &lt;li&gt;Devon Hollowood&lt;/li&gt;
+ &lt;li&gt;Doug Goldstein&lt;/li&gt;
+ &lt;li&gt;Dylan McKay&lt;/li&gt;
+ &lt;li&gt;ebadf&lt;/li&gt;
+ &lt;li&gt;Eli Friedman&lt;/li&gt;
+ &lt;li&gt;Eric Findlay&lt;/li&gt;
+ &lt;li&gt;Erik Davidson&lt;/li&gt;
+ &lt;li&gt;Felix S. Klock II&lt;/li&gt;
+ &lt;li&gt;Florian Hahn&lt;/li&gt;
+ &lt;li&gt;Florian Hartwig&lt;/li&gt;
+ &lt;li&gt;Gleb Kozyrev&lt;/li&gt;
+ &lt;li&gt;Guillaume Gomez&lt;/li&gt;
+ &lt;li&gt;Huon Wilson&lt;/li&gt;
+ &lt;li&gt;Igor Shuvalov&lt;/li&gt;
+ &lt;li&gt;Ivan Ivaschenko&lt;/li&gt;
+ &lt;li&gt;Ivan Kozik&lt;/li&gt;
+ &lt;li&gt;Ivan Stankovic&lt;/li&gt;
+ &lt;li&gt;Jack Fransham&lt;/li&gt;
+ &lt;li&gt;Jake Goulding&lt;/li&gt;
+ &lt;li&gt;Jake Worth&lt;/li&gt;
+ &lt;li&gt;James Miller&lt;/li&gt;
+ &lt;li&gt;Jan Likar&lt;/li&gt;
+ &lt;li&gt;Jean Maillard&lt;/li&gt;
+ &lt;li&gt;Jeffrey Seyfried&lt;/li&gt;
+ &lt;li&gt;Jethro Beekman&lt;/li&gt;
+ &lt;li&gt;John KÃ¥re Alsaker&lt;/li&gt;
+ &lt;li&gt;John Talling&lt;/li&gt;
+ &lt;li&gt;Jonas Schievink&lt;/li&gt;
+ &lt;li&gt;Jonathan S&lt;/li&gt;
+ &lt;li&gt;Jose Narvaez&lt;/li&gt;
+ &lt;li&gt;Josh Austin&lt;/li&gt;
+ &lt;li&gt;Josh Stone&lt;/li&gt;
+ &lt;li&gt;Joshua Holmer&lt;/li&gt;
+ &lt;li&gt;JP Sugarbroad&lt;/li&gt;
+ &lt;li&gt;jrburke&lt;/li&gt;
+ &lt;li&gt;Kevin Butler&lt;/li&gt;
+ &lt;li&gt;Kevin Yeh&lt;/li&gt;
+ &lt;li&gt;Kohei Hasegawa&lt;/li&gt;
+ &lt;li&gt;Kyle Mayes&lt;/li&gt;
+ &lt;li&gt;Lee Jeffery&lt;/li&gt;
+ &lt;li&gt;Manish Goregaokar&lt;/li&gt;
+ &lt;li&gt;Marcell Pardavi&lt;/li&gt;
+ &lt;li&gt;Markus Unterwaditzer&lt;/li&gt;
+ &lt;li&gt;Martin Pool&lt;/li&gt;
+ &lt;li&gt;Marvin Löbel&lt;/li&gt;
+ &lt;li&gt;Matt Brubeck&lt;/li&gt;
+ &lt;li&gt;Matthias Bussonnier&lt;/li&gt;
+ &lt;li&gt;Matthias Kauer&lt;/li&gt;
+ &lt;li&gt;mdinger&lt;/li&gt;
+ &lt;li&gt;Michael Layzell&lt;/li&gt;
+ &lt;li&gt;Michael Neumann&lt;/li&gt;
+ &lt;li&gt;Michael Sproul&lt;/li&gt;
+ &lt;li&gt;Michael Woerister&lt;/li&gt;
+ &lt;li&gt;Mihaly Barasz&lt;/li&gt;
+ &lt;li&gt;Mika Attila&lt;/li&gt;
+ &lt;li&gt;mitaa&lt;/li&gt;
+ &lt;li&gt;Ms2ger&lt;/li&gt;
+ &lt;li&gt;Nicholas Mazzuca&lt;/li&gt;
+ &lt;li&gt;Nick Cameron&lt;/li&gt;
+ &lt;li&gt;Niko Matsakis&lt;/li&gt;
+ &lt;li&gt;Ole Krüger&lt;/li&gt;
+ &lt;li&gt;Oliver Middleton&lt;/li&gt;
+ &lt;li&gt;Oliver Schneider&lt;/li&gt;
+ &lt;li&gt;Ori Avtalion&lt;/li&gt;
+ &lt;li&gt;Paul A. Jungwirth&lt;/li&gt;
+ &lt;li&gt;Peter Atashian&lt;/li&gt;
+ &lt;li&gt;Philipp Matthias Schäfer&lt;/li&gt;
+ &lt;li&gt;pierzchalski&lt;/li&gt;
+ &lt;li&gt;Ravi Shankar&lt;/li&gt;
+ &lt;li&gt;Ricardo Martins&lt;/li&gt;
+ &lt;li&gt;Ricardo Signes&lt;/li&gt;
+ &lt;li&gt;Richard Diamond&lt;/li&gt;
+ &lt;li&gt;Rizky Luthfianto&lt;/li&gt;
+ &lt;li&gt;Ryan Scheel&lt;/li&gt;
+ &lt;li&gt;Scott Olson&lt;/li&gt;
+ &lt;li&gt;Sean Griffin&lt;/li&gt;
+ &lt;li&gt;Sebastian Hahn&lt;/li&gt;
+ &lt;li&gt;Sébastien Marie&lt;/li&gt;
+ &lt;li&gt;Seo Sanghyeon&lt;/li&gt;
+ &lt;li&gt;Simonas Kazlauskas&lt;/li&gt;
+ &lt;li&gt;Simon Sapin&lt;/li&gt;
+ &lt;li&gt;Stepan Koltsov&lt;/li&gt;
+ &lt;li&gt;Steve Klabnik&lt;/li&gt;
+ &lt;li&gt;Steven Fackler&lt;/li&gt;
+ &lt;li&gt;Tamir Duberstein&lt;/li&gt;
+ &lt;li&gt;Tobias Bucher&lt;/li&gt;
+ &lt;li&gt;Toby Scrace&lt;/li&gt;
+ &lt;li&gt;Tshepang Lekhonkhobe&lt;/li&gt;
+ &lt;li&gt;Ulrik Sverdrup&lt;/li&gt;
+ &lt;li&gt;Vadim Chugunov&lt;/li&gt;
+ &lt;li&gt;Vadim Petrochenkov&lt;/li&gt;
+ &lt;li&gt;William Throwe&lt;/li&gt;
+ &lt;li&gt;xd1le&lt;/li&gt;
+ &lt;li&gt;Xmasreturns&lt;/li&gt;
+ &lt;/ul&gt;</description>
+ <pubDate>Thu, 21 Jan 2016 00:00:00 +0000</pubDate>
+ </item>
+ <item>
+ <title>Mozilla Addons Blog: Archiving AMO Stats</title>
+ <guid isPermaLink="false">http://blog.mozilla.org/addons/?p=7644</guid>
+ <link>https://blog.mozilla.org/addons/2016/01/20/archiving-amo-stats/</link>
+ <description>&lt;p&gt;One of the advantages of listing an add-on or theme on &lt;a href=&quot;https://addons.mozilla.org&quot; target=&quot;_blank&quot;&gt;addons.mozilla.org&lt;/a&gt; (AMO) is that you’ll get statistics on your add-on’s usage. These stats, which are covered by the &lt;a href=&quot;https://www.mozilla.org/privacy/&quot; target=&quot;_blank&quot;&gt;Mozilla privacy policy&lt;/a&gt;, provide add-on developers with information such as the number of downloads and daily users, among other insights.&lt;/p&gt;
+ &lt;p&gt;Currently, the data that generates these statistics can go back as far as 2007, as we haven’t had an archiving policy. As a result, statistics take up the vast majority of disk space in our database and require a significant amount of processing and operations time. Statistics over a year old are very rarely accessed, and the value of their generation is very low, while the costs are increasing.&lt;/p&gt;
+ &lt;p&gt;To reduce our operating and development costs, and increase the site’s reliability for developers, we are introducing an archiving policy.&lt;/p&gt;
+ &lt;p&gt;In the coming weeks, statistics data &lt;strong&gt;over one year old&lt;/strong&gt; will no longer be stored in the AMO database, and reports generated from them will no longer be accessible through AMO’s add-on statistics pages. Instead, the data will be archived and maintained as plain text files, which developers can download. We will write a follow-up post when these archives become available.&lt;/p&gt;
+ &lt;p&gt;If you’ve chosen to keep your add-on’s statistics private, they will remain private when stats are archived. You can check your privacy settings by going to your add-on in the &lt;a href=&quot;https://addons.mozilla.org/developers/addons&quot; target=&quot;_blank&quot;&gt;Developer Hub&lt;/a&gt;, clicking on &lt;strong&gt;Edit Listing&lt;/strong&gt;, and then &lt;strong&gt;Technical Details&lt;/strong&gt;.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;https://blog.mozilla.org/addons/files/2016/01/Screenshot-2016-01-20-14.52.33.png&quot;&gt;&lt;img alt=&quot;editlisting&quot; class=&quot;alignnone size-large wp-image-7645&quot; height=&quot;389&quot; src=&quot;https://blog.mozilla.org/addons/files/2016/01/Screenshot-2016-01-20-14.52.33-600x389.png&quot; width=&quot;600&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
+ &lt;p&gt;The total number of users and other cumulative counts on add-ons and themes will not be affected and these will continue to function.&lt;/p&gt;
+ &lt;p&gt;If you have feedback or concerns, please head to our &lt;a href=&quot;https://discourse.mozilla-community.org/t/archiving-of-add-on-statistics/6573&quot; target=&quot;_blank&quot;&gt;forum post&lt;/a&gt; on this topic.&lt;/p&gt;</description>
+ <pubDate>Wed, 20 Jan 2016 23:54:09 +0000</pubDate>
+ <dc:creator>Andy McKay</dc:creator>
+ </item>
+ <item>
+ <title>Air Mozilla: The Joy of Coding - Episode 41</title>
+ <guid isPermaLink="true">https://air.mozilla.org/the-joy-of-coding-episode-41/</guid>
+ <link>https://air.mozilla.org/the-joy-of-coding-episode-41/</link>
+ <description>&lt;p&gt;
+ &lt;img alt=&quot;The Joy of Coding - Episode 41&quot; class=&quot;wp-post-image&quot; height=&quot;90&quot; src=&quot;https://air.cdn.mozilla.net/media/cache/cb/68/cb68b6ac48452be7e7f25ddc7b63c959.png&quot; width=&quot;160&quot; /&gt;
+ mconley livehacks on real Firefox bugs while thinking aloud.
+ &lt;/p&gt;</description>
+ <pubDate>Wed, 20 Jan 2016 18:00:00 +0000</pubDate>
+ <dc:creator>Air Mozilla</dc:creator>
+ </item>
+ <item>
+ <title>Nathan Froyd: gecko and c++ onboarding presentation</title>
+ <guid isPermaLink="false">http://blog.mozilla.org/nfroyd/?p=452</guid>
+ <link>https://blog.mozilla.org/nfroyd/2016/01/20/gecko-and-c-onboarding-presentation/</link>
+ <description>&lt;p&gt;One of the things the Firefox team has been doing recently is having onboarding sessions for new hires. This onboarding currently covers:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;1st day setup&lt;/li&gt;
+ &lt;li&gt;Bugzilla&lt;/li&gt;
+ &lt;li&gt;Building Firefox&lt;/li&gt;
+ &lt;li&gt;Desktop Firefox Architecture / Product&lt;/li&gt;
+ &lt;li&gt;Communication and Community&lt;/li&gt;
+ &lt;li&gt;Javascript and the DOM&lt;/li&gt;
+ &lt;li&gt;C++ and Gecko&lt;/li&gt;
+ &lt;li&gt;Shipping Software&lt;/li&gt;
+ &lt;li&gt;Telemetry&lt;/li&gt;
+ &lt;li&gt;Org structure and career development&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;My first day consisted of some useful HR presentations and then I was given my laptop and a pointer to a wiki page on building Firefox. Needless to say, it took me a while to get started! It would have been super convenient to have an introduction to all the stuff above.&lt;/p&gt;
+ &lt;p&gt;I’ve been asked to do the C++ and Gecko session three times. All of the sessions are open to whoever wants to come, not just the new hires, and I think yesterday’s session was easily the most well-attended yet: somewhere between 10 and 20 people showed up. Yesterday’s session was the first session where I made the slides available to attendees (should have been doing that from the start…) and it seemed equally useful to make the slides available to a broader audience as well. The &lt;a href=&quot;https://docs.google.com/presentation/d/1ZHUkNzZK2TrF5_4MWd_lqEq7Ph5B6CDbNsizIkBxbnQ/edit?usp=sharing&quot;&gt;Gecko and C++ Onboarding slides&lt;/a&gt; are up now!&lt;/p&gt;
+ &lt;p&gt;This presentation is a “living†presentation; it will get updated for future sessions with feedback and as I think of things that should have been in the presentation or better ways to set things up (some diagrams would be nice…). If you have feedback (good, bad, or ugly) on particular things in the slides or you have suggestions on what other things should be covered, please contact me! Next time I do this I’ll try to record the presentation so folks can watch that if they prefer.&lt;/p&gt;</description>
+ <pubDate>Wed, 20 Jan 2016 16:48:29 +0000</pubDate>
+ <dc:creator>Nathan Froyd</dc:creator>
+ </item>
+ <item>
+ <title>Andreas Gal: Brendan is back to save the Web</title>
+ <guid isPermaLink="false">http://andreasgal.com/?p=573</guid>
+ <link>http://andreasgal.com/2016/01/20/brendan-is-back-to-save-the-web/</link>
+ <description>&lt;p class=&quot;p1&quot;&gt;Brendan is &lt;a href=&quot;https://github.com/brave&quot;&gt;back&lt;/a&gt;, and he has a &lt;a href=&quot;http://brave.com/&quot;&gt;plan&lt;/a&gt; to save the Web. Its a big and bold plan, and it may just work. I am pretty excited about this. If you have 5 minutes to read along I’ll explain why I think you should be as well.&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;&lt;strong&gt;The Web is broken&lt;/strong&gt;&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;Lets face it, the Web today is a mess. Everywhere we go online we are constantly inundated with annoying ads. Often pages are more ads than content, and the more ads the industry throws at us, the more we ignore them, the more obnoxious ads get, trying to catch our attention. As Brendan explains in his blog post, the browser used to be on the user’s side—we call browsers the user agent for a reason. Part of the early success of Firefox was that it blocked popup ads. But somewhere over the last 10 years of modern Web browsers, browsers lost their way and stopped being the user’s agent alone. Why?&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;&lt;strong&gt;Browsers aren’t free&lt;/strong&gt;&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;Making a modern Web browser is not free. It takes hundreds of engineers to make a competitive modern browser engine. Someone has to pay for that, and that someone needs to have a reason to pay for it. Google doesn’t make Chrome for the good of mankind. Google makes Chrome so you can consume more Web and along with it, more Google ads. Each time you click on one, Google makes more money. Chrome is a billion dollar business for Google. And the same is true for pretty much every other browser. Every major browser out there is funded through advertisement. No browser maker can escape this dilemma. Maybe now you understand why no major browser ships with a builtin enabled by default ad-blocker, even though ad-blockers are by far the most popular add-ons.&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;&lt;strong&gt;Our privacy is at stake&lt;/strong&gt;&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;It’s not just the unregulated flood of advertisement that needs a solution. Every ad you see is often selected based on sensitive private information advertisement networks have extracted from your browsing behavior through tracking. Remember how the FBI used to track what books Americans read at the library, and it was a big scandal? Today the Googles and Facebooks of the world know almost every site you visit, everything you buy online, and they use this data to target you with advertisement. I am often puzzled why people are so afraid of the NSA spying on us but show so little concern about all the deeply personal data Google and Facebook are amassing about everyone.&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;&lt;strong&gt;Blocking alone doesn’t scale&lt;/strong&gt;&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;I wish the solution was as easy as just blocking all ads. There is a lot of great Web content out there: news, entertainment, educational content. It’s not free to make all this content, but we have gotten used to consuming it “for freeâ€. Banning all ads without an alternative mechanism would break the economic backbone of the Web. This dilemma has existed for many years, and the big browser vendors seem to have given up on it. It’s hard to blame them. How do you disrupt the status quo without sawing off the (ad revenue) branch you are sitting on?&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;&lt;strong&gt;It takes an newcomer to fix this mess&lt;/strong&gt;&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;I think its unlikely that the incumbent browser vendors will make any bold moves to solve this mess. There is too much money at stake. I am excited to see a startup take a swipe at this problem, because they have little to lose (seed money aside). Brave is getting the user agent back into the game. Browsers have intentionally remained silent onlookers to the ad industry invading users’ privacy. With Brave, Brendan makes the user agent step up and fight for the user as it was always intended to do.&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;Brave basically consists of two parts: part one blocks third party ad content and tracking signals. Instead of these Brave inserts alternative ad content. Sites can sign up to get a fair share of any ads that Brave displays for them. The big change in comparison to the status quo is that the Brave user agent is in control and can regulate what you see. It’s like a speed limit for advertisement on the Web, with the goal to restore balance and give sites a fair way to monetize while giving the user control through the user agent.&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;&lt;strong&gt;Making money with a better Web&lt;/strong&gt;&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;The ironic part of Brave is that its for-profit. Brave can make money by reducing obnoxious ads and protecting your privacy at the same time. If Brave succeeds, it’s going to drain money away from the crappy privacy-invasive obnoxious advertisement world we have today, and publishers and sites will start transacting in the new Brave world that is regulated by the user agent. Brave will take a cut of these transactions. And I think this is key. It aligns the incentives right. The current funding structure of major browsers encourages them to keep things as they are. Brave’s incentive is to bring down the whole diseased temple and usher in a better Web. Exciting.&lt;/p&gt;
+ &lt;p class=&quot;p1&quot;&gt;&lt;strong&gt;Quick update:&lt;/strong&gt; I had a chance to look over the Brave GitHub repo. It looks like the Brave Desktop browser is based on Chromium, not Gecko. Yes, you read that right. &lt;span style=&quot;text-decoration: underline;&quot;&gt;Brave is using Google’s rendering engine, not Mozilla’s.&lt;/span&gt; Much to write about this one, but it will definitely help Brave “hide†better in the large volume of Chrome users, making it harder for sites to identify and block Brave users. Brave for iOS seems to be a &lt;span style=&quot;text-decoration: underline;&quot;&gt;fork of Firefox for iOS, but it manages to block ads&lt;/span&gt; (Mozilla says they can’t).&lt;/p&gt;&lt;br /&gt;Filed under: &lt;a href=&quot;http://andreasgal.com/category/mozilla/&quot;&gt;Mozilla&lt;/a&gt; &lt;a href=&quot;http://feeds.wordpress.com/1.0/gocomments/andreasgal.wordpress.com/573/&quot; rel=&quot;nofollow&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; src=&quot;http://feeds.wordpress.com/1.0/comments/andreasgal.wordpress.com/573/&quot; /&gt;&lt;/a&gt; &lt;a href=&quot;http://feeds.wordpress.com/1.0/godelicious/andreasgal.wordpress.com/573/&quot; rel=&quot;nofollow&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; src=&quot;http://feeds.wordpress.com/1.0/delicious/andreasgal.wordpress.com/573/&quot; /&gt;&lt;/a&gt; &lt;a href=&quot;http://feeds.wordpress.com/1.0/gofacebook/andreasgal.wordpress.com/573/&quot; rel=&quot;nofollow&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; src=&quot;http://feeds.wordpress.com/1.0/facebook/andreasgal.wordpress.com/573/&quot; /&gt;&lt;/a&gt; &lt;a href=&quot;http://feeds.wordpress.com/1.0/gotwitter/andreasgal.wordpress.com/573/&quot; rel=&quot;nofollow&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; src=&quot;http://feeds.wordpress.com/1.0/twitter/andreasgal.wordpress.com/573/&quot; /&gt;&lt;/a&gt; &lt;a href=&quot;http://feeds.wordpress.com/1.0/gostumble/andreasgal.wordpress.com/573/&quot; rel=&quot;nofollow&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; src=&quot;http://feeds.wordpress.com/1.0/stumble/andreasgal.wordpress.com/573/&quot; /&gt;&lt;/a&gt; &lt;a href=&quot;http://feeds.wordpress.com/1.0/godigg/andreasgal.wordpress.com/573/&quot; rel=&quot;nofollow&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; src=&quot;http://feeds.wordpress.com/1.0/digg/andreasgal.wordpress.com/573/&quot; /&gt;&lt;/a&gt; &lt;a href=&quot;http://feeds.wordpress.com/1.0/goreddit/andreasgal.wordpress.com/573/&quot; rel=&quot;nofollow&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; src=&quot;http://feeds.wordpress.com/1.0/reddit/andreasgal.wordpress.com/573/&quot; /&gt;&lt;/a&gt; &lt;img alt=&quot;&quot; border=&quot;0&quot; height=&quot;1&quot; src=&quot;http://pixel.wp.com/b.gif?host=andreasgal.com&amp;amp;blog=891661&amp;amp;post=573&amp;amp;subd=andreasgal&amp;amp;ref=&amp;amp;feed=1&quot; width=&quot;1&quot; /&gt;</description>
+ <pubDate>Wed, 20 Jan 2016 16:00:00 +0000</pubDate>
+ <dc:creator>Andreas</dc:creator>
+ </item>
+ <item>
+ <title>Mike Taylor: 🙅 @media (-webkit-transform-3d)</title>
+ <guid isPermaLink="true">https://miketaylr.com/posts/2016/01/at-media-webkit-transform-three-dee.html</guid>
+ <link>https://miketaylr.com/posts/2016/01/at-media-webkit-transform-three-dee.html</link>
+ <description>&lt;p&gt;&lt;code&gt;@media (-webkit-transform-3d)&lt;/code&gt; is a funny thing that exists on the web.&lt;/p&gt;
+
+ &lt;p&gt;It's like, a &lt;a href=&quot;https://drafts.csswg.org/mediaqueries-4/#mq-features&quot;&gt;media query feature&lt;/a&gt; in the form of a prefixed CSS property, which should tell you if your (once upon a time probably Safari-only) browser supports 3D transforms, invented back in the day before we had &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@supports&quot;&gt;&lt;code&gt;@supports&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
+
+ &lt;p&gt;(According to &lt;a href=&quot;https://developer.apple.com/library/safari/documentation/AppleApplications/Reference/SafariCSSRef/Articles/OtherStandardCSS3Features.html#//apple_ref/doc/uid/TP40007601-SW3&quot;&gt;Apple docs&lt;/a&gt; it first appeared in Safari 4, along side the other &lt;code&gt;-webkit-transition&lt;/code&gt; and &lt;code&gt;-webkit-transform-2d&lt;/code&gt; hybrid-media-query-feature-prefixed-css-properties-things that you should immediately forget exist.)&lt;/p&gt;
+
+ &lt;p&gt;Older versions of Modernizr &lt;a href=&quot;https://github.com/Modernizr/Modernizr/blob/66c694d136241d356e0d24fcbaa5c068b0b0cdae/feature-detects/css/transforms3d.js#L26-L27&quot;&gt;used this (and only this)&lt;/a&gt; to detect support for 3D transforms, and that seemed pretty OK. (They also did the polite thing and tested &lt;code&gt;@media (transform-3d)&lt;/code&gt;, but no browser has ever actually supported that, as it turns out). And because they're so consistently polite, they've since &lt;a href=&quot;https://github.com/patrickkettner/Modernizr/commit/a54308e47e269a058472854b1ef417bd54f4e616&quot;&gt;updated the test&lt;/a&gt; to prefer &lt;code&gt;@supports&lt;/code&gt; too (via a pull request from Edge developer Jacob Rossi).&lt;/p&gt;
+
+ &lt;p&gt;As it turns out other browsers have been &lt;a href=&quot;http://caniuse.com/#feat=transforms3d&quot;&gt;updated to support 3D CSS transforms&lt;/a&gt;, but sites didn't go back and update their version of Modernizr. So unless you support &lt;code&gt;@media (-webkit-transform-3d)&lt;/code&gt; these sites break. Niche websites like &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1239136&quot;&gt;yahoo.com&lt;/a&gt; and &lt;a href=&quot;https://github.com/webcompat/web-bugs/issues/2151&quot;&gt;about.com&lt;/a&gt;.&lt;/p&gt;
+
+ &lt;p&gt;So, anyways. I added &lt;a href=&quot;https://compat.spec.whatwg.org/#css-media-queries-webkit-transform-3d&quot;&gt;&lt;code&gt;@media (-webkit-transform-3d)&lt;/code&gt; to the Compat Standard&lt;/a&gt; and we &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1239799&quot;&gt;added support for it Firefox&lt;/a&gt; so websites stop breaking.&lt;/p&gt;
+
+ &lt;p&gt;But you shouldn't ever use it—use &lt;code&gt;@supports&lt;/code&gt;. In fact, don't even share this blog post. Maybe delete it from your browser history just in case.&lt;/p&gt;</description>
+ <pubDate>Wed, 20 Jan 2016 08:00:00 +0000</pubDate>
+ <dc:creator>Mike Taylor</dc:creator>
+ </item>
+ <item>
+ <title>Byron Jones: happy bmo push day!</title>
+ <guid isPermaLink="false">http://globau.wordpress.com/?p=881</guid>
+ <link>https://globau.wordpress.com/2016/01/20/happy-bmo-push-day-166/</link>
+ <description>&lt;p&gt;the following changes have been pushed to bugzilla.mozilla.org:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;[&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1236161&quot; target=&quot;_blank&quot;&gt;1236161&lt;/a&gt;] when converting a BMP attachment to PNG fails a zero byte attachment is created&lt;/li&gt;
+ &lt;li&gt;[&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1231918&quot; target=&quot;_blank&quot;&gt;1231918&lt;/a&gt;] error handler doesn’t close multi-part responses&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;discuss these changes on &lt;a href=&quot;https://lists.mozilla.org/listinfo/tools-bmo&quot; target=&quot;_blank&quot;&gt;mozilla.tools.bmo&lt;/a&gt;.&lt;/p&gt;&lt;br /&gt;Filed under: &lt;a href=&quot;https://globau.wordpress.com/category/mozilla/bmo/&quot;&gt;bmo&lt;/a&gt;, &lt;a href=&quot;https://globau.wordpress.com/category/mozilla/&quot;&gt;mozilla&lt;/a&gt; &lt;img alt=&quot;&quot; border=&quot;0&quot; height=&quot;1&quot; src=&quot;https://pixel.wp.com/b.gif?host=globau.wordpress.com&amp;amp;blog=25718030&amp;amp;post=881&amp;amp;subd=globau&amp;amp;ref=&amp;amp;feed=1&quot; width=&quot;1&quot; /&gt;</description>
+ <pubDate>Wed, 20 Jan 2016 07:33:46 +0000</pubDate>
+ <dc:creator>glob</dc:creator>
+ </item>
+ <item>
+ <title>Alex Johnson: Removing Honeycomb Code</title>
+ <guid isPermaLink="false">https://www.alex-johnson.net/tag/mozilla/rss/85d84c54-ed0c-4ee5-beb3-8823edb3c074</guid>
+ <link>https://www.alex-johnson.net/removing-honeycomb-code/</link>
+ <description>&lt;p&gt;As an effort to reduce the APK size of Firefox for Android and to remove unnecessary code, I will be helping remove the Honeycomb code throughout the Fennec project. Honeycomb will not be supported since Firefox 46, so this code is not necessary. &lt;br /&gt;
+ &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1217675&quot;&gt;Bug 1217675&lt;/a&gt; will keep track of the progress. &lt;br /&gt;
+ Hopefully this will help reduce the APK size some and clean up the road for &lt;a href=&quot;https://www.youtube.com/watch?v=NJ6kzW5t02Y&quot;&gt;killing Gingerbread&lt;/a&gt; hopefully sometime in the near future.&lt;/p&gt;</description>
+ <pubDate>Wed, 20 Jan 2016 04:59:34 +0000</pubDate>
+ <dc:creator>Alex Johnson</dc:creator>
+ </item>
+ <item>
+ <title>Brian R. Bondy: Brave Software</title>
+ <guid isPermaLink="false">http://www.brianbondy.com/blog/id/172</guid>
+ <link>http://www.brianbondy.com/blog/172/brave-software</link>
+ <description>&lt;p&gt;&lt;/p&gt;&lt;p&gt;Since June of last year, I’ve been co-founding a new startup called &lt;a href=&quot;https://brave.com/&quot;&gt;Brave Software&lt;/a&gt; with &lt;a href=&quot;https://en.wikipedia.org/wiki/Brendan_Eich&quot;&gt;Brendan Eich&lt;/a&gt;.
+ With our amazing team, we're developing something pretty epic.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
+ &lt;p&gt;&lt;/p&gt;&lt;p&gt;We're building the next-generation of browsers for smartphones and laptops as part of our new ad-tech platform.
+ Our terms of use give our users control over their personal data by blocking ad trackers and third party cookies.
+ We re-integrate fewer and better ads directly into programmatic ad positions, paying revenue shares to users and publishers to support both of these essential parties in the web ecosystem.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
+ &lt;p&gt;&lt;/p&gt;&lt;p&gt;Coming built in, we have new faster engines for tracking protection, ad block, HTTPS Everywhere, safe ads with rev-share, and more.
+ We're seeing massive web page load time speedups.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
+
+
+ &lt;p&gt;&lt;/p&gt;&lt;p&gt;We're starting to bring people in for early developer build access on all platforms.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
+ &lt;p&gt;&lt;/p&gt;&lt;p&gt;I’m happy to share that the browsers we’re developing were made fully open sourced.
+ We welcome contributors, and would love your help.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
+ &lt;p&gt;&lt;/p&gt;&lt;p&gt;Some of the repositories include:&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/brave/browser-laptop&quot;&gt;Brave OSX and Windows x64 browsers&lt;/a&gt;: Prototyped as a Gecko based browser, but now replaced with a powerful new browser built on top of the electron framework. The electron framework is the same one in use by Slack and the Atom editor. It uses the latest libchromiumcontent and Node.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/brave/link-bubble&quot;&gt;Brave for Android&lt;/a&gt;: Formerly Link Bubble, working as a background service so you can use other apps as your pages load.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/brave/browser-ios&quot;&gt;Brave for iOS&lt;/a&gt;: Originally forked from Firefox for iOS but with all of the built-in greatness described above.&lt;/li&gt;
+ &lt;li&gt;And many others: Website, updater code, vault, electron fork, and others.&lt;/li&gt;
+ &lt;/ul&gt;</description>
+ <pubDate>Wed, 20 Jan 2016 00:00:00 +0000</pubDate>
+ <dc:creator>Brian R. Bondy</dc:creator>
+ </item>
+ <item>
+ <title>James Socol: PIEfection Slides Up</title>
+ <guid isPermaLink="false">http://coffeeonthekeyboard.com/rss/0388d8a6-fc86-477e-a161-1b356e01fe77</guid>
+ <link>http://coffeeonthekeyboard.com/piefection-slides-up/</link>
+ <description>&lt;p&gt;I put &lt;a href=&quot;https://github.com/jsocol/talks/tree/master/2016-01-13-manhattanjs-pie&quot;&gt;the slides for my ManhattanJS talk, &quot;PIEfection&quot;&lt;/a&gt; up on GitHub the other day (sans images, but there are links in the source for all of those).&lt;/p&gt;
+
+ &lt;p&gt;I completely neglected to talk about the &lt;a href=&quot;https://en.wikipedia.org/wiki/Maillard_reaction&quot;&gt;Maillard reaction&lt;/a&gt;, which is responsible for food tasting good, and specifically for browning pie crusts. tl;dr: Amino acid (protein) + sugar + ~300°F (~150°C) = delicious. There are innumerable and poorly understood combinations of amino acids and sugars, but this class of reaction is responsible for everything from searing stakes to browning crusts to toasting marshmallows.&lt;/p&gt;
+
+ &lt;p&gt;Above ~330°F, you get caramelization, which is also a delicious part of the pie and crust, but you don't want to overdo it. Starting around ~400°F, you get pyrolysis (burning, charring, carbonization) and below 285°F the reaction won't occur (at least not quickly) so you won't get the delicious compounds.&lt;/p&gt;
+
+ &lt;p&gt;(All of these are, of course, temperatures measured in the material, not in the air of the oven.)&lt;/p&gt;
+
+ &lt;p&gt;So, instead of an egg wash on your top crust, try whole milk, which has more sugar to react with the gluten in the crust.&lt;/p&gt;
+
+ &lt;p&gt;I also didn't get a chance to mention a rolling technique I use, that I learned from a &lt;a href=&quot;https://www.facebook.com/ellenspirerstaffing&quot;&gt;cousin of mine&lt;/a&gt;, in whose baking shadow I happily live.&lt;/p&gt;
+
+ &lt;p&gt;When rolling out a crust after it's been in the fridge, first roll it out in a long stretch, then fold it in thirds; do it again; then start rolling it out into a round. Not only do you add more layer structure (mmm, flaky, delicious layers) but it'll fill in the cracks that often form if you try to roll it out directly, resulting in a stronger crust.&lt;/p&gt;
+
+ &lt;p&gt;Those &lt;a href=&quot;http://www.amazon.com/Cheese-Shaker-Pepper-Perforated-Stainless/dp/B007T40P28/ref=sr_1_1?ie=UTF8&amp;amp;qid=1453236391&amp;amp;sr=8-1&amp;amp;keywords=pizza+shaker&quot;&gt;pepper flake shakers&lt;/a&gt;, filled with flour, are a great way to keep adding flour to the workspace without worrying about your buttery hands.&lt;/p&gt;
+
+ &lt;p&gt;For transferring the crust to the pie plate, try rolling it up onto your rolling pin and unrolling it on the plate. &lt;a href=&quot;http://www.amazon.com/Ateco-20-Inch-Length-French-Rolling/dp/B000KESQ1G&quot;&gt;Tapered (or &quot;French&quot;) rolling pins&lt;/a&gt; (or wine bottle) are particularly good at this since they don't have moving parts.&lt;/p&gt;
+
+ &lt;p&gt;Finally, thanks again to &lt;a href=&quot;https://twitter.com/renrutnnej&quot;&gt;Jenn&lt;/a&gt; for helping me get pies from one island to another. It would not have been possible without her!&lt;/p&gt;</description>
+ <pubDate>Tue, 19 Jan 2016 20:45:34 +0000</pubDate>
+ <dc:creator>James Socol</dc:creator>
+ </item>
+ <item>
+ <title>Air Mozilla: Reprendre le contrôle de sa vie privée sur Internet</title>
+ <guid isPermaLink="true">https://air.mozilla.org/reprendre-le-controle-de-sa-vie-privee-sur-internet/</guid>
+ <link>https://air.mozilla.org/reprendre-le-controle-de-sa-vie-privee-sur-internet/</link>
+ <description>&lt;p&gt;
+ &lt;img alt=&quot;Reprendre le contrôle de sa vie privée sur Internet&quot; class=&quot;wp-post-image&quot; height=&quot;90&quot; src=&quot;https://air.cdn.mozilla.net/media/cache/be/f6/bef62897fb87e08dc8392fe61d10bcfa.png&quot; width=&quot;160&quot; /&gt;
+ L'omniprésence des réseaux sociaux, des moteurs de recherches et de la publicité est-elle compatible avec notre droit à la vie privée ?
+ &lt;/p&gt;</description>
+ <pubDate>Tue, 19 Jan 2016 18:00:00 +0000</pubDate>
+ <dc:creator>Air Mozilla</dc:creator>
+ </item>
+ <item>
+ <title>Myk Melez: New Year, New Blogware</title>
+ <guid isPermaLink="false">https://mykzilla.org/?p=245</guid>
+ <link>https://mykzilla.org/2016/01/19/new-year-new-blogware/</link>
+ <description>&lt;p&gt;Four score and many moons ago, I decided to move this blog from Blogger to WordPress. The transition took longer than expected, but it’s finally done.&lt;/p&gt;
+ &lt;p&gt;If you’ve been following along at the old address, &lt;a href=&quot;https://mykzilla.blogspot.com/&quot;&gt;https://mykzilla.blogspot.com/&lt;/a&gt;, now’s the time to update your address book! If you’ve been going to &lt;a href=&quot;https://mykzilla.org/&quot;&gt;https://mykzilla.org/&lt;/a&gt;, however, or you read the blog on &lt;a href=&quot;http://planet.mozilla.org/&quot;&gt;Planet Mozilla&lt;/a&gt;, then there’s nothing to do, as that’s the new address, and Planet Mozilla has been updated to syndicate posts from it.&lt;/p&gt;</description>
+ <pubDate>Tue, 19 Jan 2016 16:56:05 +0000</pubDate>
+ <dc:creator>Myk Melez</dc:creator>
+ </item>
+ <item>
+ <title>Michael Kohler: Mozillas strategische Leitlinien für 2016 und danach</title>
+ <guid isPermaLink="false">http://michaelkohler.info/?p=348</guid>
+ <link>https://michaelkohler.info/2016/mozillas-strategische-leitlinien-fur-2016-und-danach</link>
+ <description>&lt;p&gt;Dieser Beitrag wurde zuerst im Blog auf&lt;a href=&quot;https://blog.mozilla.org/community&quot;&gt; https://blog.mozilla.org/community&lt;/a&gt; veröffentlicht. Herzlichen Dank an Aryx und Coce für die Übersetzung!&lt;/p&gt;
+ &lt;p&gt;Auf der ganzen Welt arbeiten leidenschaftliche Mozillianer am Fortschritt für Mozillas Mission. Aber fragt man fünf verschiedene Mozillianer, was die Mission ist, erhält man womöglich sieben verschiedene Antworten.&lt;/p&gt;
+ &lt;p&gt;Am Ende des letzten Jahres legte Mozillas CEO Chris Beard klare Vorstellungen über Mozillas Mission, Vision und Rolle dar und zeigte auf, wie unsere Produkte uns diesem Ziel in den nächsten fünf Jahren näher bringen. Das Ziel dieser strategischen Leitlinien besteht darin, für Mozilla insgesamt ein prägnantes, gemeinsames Verständnis unserer Ziele zu entwickeln, die uns als Individuen das Treffen von Entscheidungen und Erkennen von Möglichkeiten erleichtert, mit denen wir Mozilla voranbringen.&lt;/p&gt;
+ &lt;p&gt;Mozillas Mission können wir nicht alleine erreichen. Die Tausenden von Mozillianern auf der ganzen Welt müssen dahinter stehen, damit wir zügig und mit lauterer Stimme als je zuvor Unglaubliches erreichen können.&lt;/p&gt;
+ &lt;p&gt;Deswegen ist eine der sechs&lt;a href=&quot;https://docs.google.com/presentation/d/1A3Ma9gNawAYYGbYC2bUW0wUwcpHuvyMiZvHNiMLriw0/edit#slide=id.gdaa7a0bd0_1_0&quot;&gt; strategischen Initiativen&lt;/a&gt; des Participation Teams für die erste Jahreshälfte, möglichst viele Mozillianer über diese Leitlinien aufzuklären, damit wir 2016 den bisher wesentlichsten Einfluss erzielen können. Wir werden einen weiteren Beitrag veröffentlichen, der sich näher mit der Strategie des Participation Teams für das Jahr 2016 befassen wird.&lt;/p&gt;
+ &lt;p&gt;&lt;img alt=&quot;&quot; class=&quot;alignnone&quot; height=&quot;335&quot; src=&quot;https://ffp4g1ylyit3jdyti1hqcvtb-wpengine.netdna-ssl.com/community/files/2016/01/Screen-Shot-2015-12-18-at-2.02.07-PM-600x335.png&quot; width=&quot;600&quot; /&gt;&lt;/p&gt;
+ &lt;p&gt;Das Verstehen dieser Strategie wird unabdingbar sein für jeden, der bei Mozilla in diesem Jahr etwas bewirken möchte, denn sie wird bestimmen, wofür wir eintreten, wo wir unsere Ressourcen einsetzen und auf welche Projekte wir uns 2016 konzentrieren werden.&lt;/p&gt;
+ &lt;p&gt;Zu Jahresbeginn werden wir näher auf diese Strategie eingehen und weitere Details dazu bekanntgeben, wie die diversen Teams und Projekte bei Mozilla auf diese Ziele hinarbeiten.&lt;/p&gt;
+ &lt;p&gt;Der aktuelle Aufruf zum Handeln besteht darin, im Kontext Ihrer Arbeit über diese Ziele nachzudenken und darüber, wie Sie im kommenden Jahr bei Mozilla mitwirken möchten. Dies hilft, Ihre Innovationen, Ambitionen und Ihren Einfluss im Jahr 2016 zu gestalten.&lt;/p&gt;
+ &lt;p&gt;Wir hoffen, dass Sie mitdiskutieren und Ihre Fragen, Kommentare und Pläne für das Vorantreiben der strategischen Leitlinien im Jahr 2016&lt;a href=&quot;https://discourse.mozilla-community.org/t/mozillas-strategic-narrative-2016/6397&quot;&gt; hier&lt;/a&gt; auf Discourse teilen und Ihre Gedanken auf Twitter mit dem Hashtag &lt;a href=&quot;https://twitter.com/search?q=%23mozilla2016strategy&amp;amp;src=typd&quot;&gt;#Mozilla2016Strategy&lt;/a&gt; mitteilen.&lt;/p&gt;
+ &lt;p&gt; &lt;/p&gt;
+ &lt;h3&gt;Mission, Vision &amp;amp; Strategie&lt;/h3&gt;
+ &lt;p&gt;&lt;b&gt;Unsere Mission&lt;/b&gt;&lt;/p&gt;
+ &lt;p&gt;Dafür zu sorgen, dass das Internet eine weltweite öffentliche Ressource ist, die allen zugänglich ist.&lt;/p&gt;
+ &lt;p&gt;&lt;b&gt;Unsere Vision&lt;/b&gt;&lt;/p&gt;
+ &lt;p&gt;Ein Internet, für das Menschen tatsächlich an erster Stelle stehen. Ein Internet, in dem Menschen ihr eigenes Erlebnis gestalten können. Ein Internet, in dem die Menschen selbst entscheiden können sowie sicher und unabhängig sind.&lt;/p&gt;
+ &lt;p&gt;&lt;b&gt;Unsere Rolle&lt;/b&gt;&lt;/p&gt;
+ &lt;p&gt;Mozilla setzt sich im wahrsten Sinne des Wortes in Ihrem Online-Leben für Sie ein. Wir setzen uns für Sie ein, sowohl in Ihrem Online-Erlebnis als auch für Ihre Interessen beim Zustand des Internets.&lt;/p&gt;
+ &lt;p&gt;&lt;b&gt;Unsere Arbeit&lt;/b&gt;&lt;/p&gt;
+ &lt;p&gt;Unsere Säulen&lt;/p&gt;
+ &lt;ol&gt;
+ &lt;li&gt;&lt;b&gt;Produkte:&lt;/b&gt; Wir entwickeln Produkte mit Menschen im Mittelpunkt sowie Bildungsprogramme, mit deren Hilfe Menschen online ihr gesamtes Potential ausschöpfen können.&lt;/li&gt;
+ &lt;li&gt;&lt;b&gt;Technologie:&lt;/b&gt; Wir entwickeln robuste technische Lösungen, die das Internet über verschiedene Plattformen hinweg zum Leben erwecken.&lt;/li&gt;
+ &lt;li&gt;&lt;b&gt;Menschen:&lt;/b&gt; Wir entwickeln Führungspersonen und Mitwirkende in der Gemeinschaft, die das Internet erfinden, gestalten und verteidigen.&lt;/li&gt;
+ &lt;/ol&gt;
+ &lt;p&gt;Wir wir positive Veränderungen in Zukunft anpacken wollen&lt;/p&gt;
+ &lt;p&gt;Die Arbeitsweise ist ebensowichtig wie das Ziel. Unsere Gesundheit und bleibender Einfluss hängen davon ab, wie sehr unsere Produkte und Aktivitäten:&lt;/p&gt;
+ &lt;ol&gt;
+ &lt;li&gt;Interoperabilität, Open Source und offene Standards fördern,&lt;/li&gt;
+ &lt;li&gt;Gemeinschaften aufbauen und fördern,&lt;/li&gt;
+ &lt;li&gt;Für politische Veränderungen und rechtlichen Schutz eintreten sowie&lt;/li&gt;
+ &lt;li&gt;Netzbürger bilden und einbeziehen.&lt;/li&gt;
+ &lt;/ol&gt;
+ &lt;p&gt; &lt;/p&gt;
+ &lt;img alt=&quot;&quot; height=&quot;0&quot; src=&quot;http://piwik.michaelkohler.info/piwik.php?idsite=1&amp;amp;rec=1&amp;amp;url=https%3A%2F%2Fmichaelkohler.info%2F2016%2Fmozillas-strategische-leitlinien-fur-2016-und-danach&amp;amp;action_name=Mozillas+strategische+Leitlinien+f%C3%BCr+2016+und+danach&amp;amp;urlref=https%3A%2F%2Fmichaelkohler.info%2Ffeed&quot; style=&quot;border: 0; width: 0; height: 0;&quot; width=&quot;0&quot; /&gt;</description>
+ <pubDate>Tue, 19 Jan 2016 15:27:24 +0000</pubDate>
+ <dc:creator>Michael Kohler</dc:creator>
+ </item>
+ <item>
+ <title>David Lawrence: happy bmo push day!</title>
+ <guid isPermaLink="false">http://dlawrence.wordpress.com/?p=27</guid>
+ <link>https://dlawrence.wordpress.com/2016/01/19/happy-bmo-push-day-3/</link>
+ <description>&lt;p&gt;the following changes have been pushed to bugzilla.mozilla.org:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;[&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1238573&quot; target=&quot;_blank&quot;&gt;1238573&lt;/a&gt;] Change label of “New Bug†menu to “New/Clone Bugâ€&lt;/li&gt;
+ &lt;li&gt;[&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1239065&quot; target=&quot;_blank&quot;&gt;1239065&lt;/a&gt;] Project Kickoff Form: Adjustments needed to Mozilla Infosec review portion&lt;/li&gt;
+ &lt;li&gt;[&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1240157&quot; target=&quot;_blank&quot;&gt;1240157&lt;/a&gt;] Fix a typo in bug.rst&lt;/li&gt;
+ &lt;li&gt;[&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1236461&quot; target=&quot;_blank&quot;&gt;1236461&lt;/a&gt;] Mass update mozilla-reps group&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;discuss these changes on &lt;a href=&quot;https://lists.mozilla.org/listinfo/tools-bmo&quot; target=&quot;_blank&quot;&gt;mozilla.tools.bmo&lt;/a&gt;.&lt;/p&gt;&lt;br /&gt; &lt;a href=&quot;http://feeds.wordpress.com/1.0/gocomments/dlawrence.wordpress.com/27/&quot; rel=&quot;nofollow&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; src=&quot;http://feeds.wordpress.com/1.0/comments/dlawrence.wordpress.com/27/&quot; /&gt;&lt;/a&gt; &lt;img alt=&quot;&quot; border=&quot;0&quot; height=&quot;1&quot; src=&quot;https://pixel.wp.com/b.gif?host=dlawrence.wordpress.com&amp;amp;blog=58816&amp;amp;post=27&amp;amp;subd=dlawrence&amp;amp;ref=&amp;amp;feed=1&quot; width=&quot;1&quot; /&gt;</description>
+ <pubDate>Tue, 19 Jan 2016 14:49:59 +0000</pubDate>
+ <dc:creator>dlawrence</dc:creator>
+ </item>
+ <item>
+ <title>Soledad Penades: Hardware Hack Day @ MozLDN, 1</title>
+ <guid isPermaLink="false">http://soledadpenades.com/?p=6335</guid>
+ <link>http://soledadpenades.com/2016/01/19/hardware-hack-day-mozldn-1/</link>
+ <description>&lt;p&gt;Last week we ran an internal “hack day†here at the Mozilla space in London. It was just a bunch of &lt;em&gt;software&lt;/em&gt; engineers looking at various &lt;em&gt;hardware&lt;/em&gt; boards and things and learning about them &lt;img alt=&quot;:-)&quot; class=&quot;wp-smiley&quot; src=&quot;http://soledadpenades.com/wp-includes/images/smilies/simple-smile.png&quot; style=&quot;height: 1em;&quot; /&gt;&lt;/p&gt;
+ &lt;p&gt;Here’s what we did!&lt;/p&gt;
+ &lt;h3&gt;&lt;a href=&quot;http://soledadpenades.com/&quot;&gt;Sole&lt;/a&gt;&lt;/h3&gt;
+ &lt;p&gt;I essentially &lt;a href=&quot;http://soledadpenades.com/2016/01/19/kind-of-bricking-an-arduino-duemilanove-by-exhausting-its-memory/&quot;&gt;kind of bricked my Arduino Duemilanove&lt;/a&gt; trying to get it working with Johnny Five, but it was fine–apparently there’s a way to recover it using another Arduino, and someone offered to help with that in the next &lt;a href=&quot;http://www.meetup.com/NodeBots-of-London/events/227890374/&quot;&gt;NodeBots&lt;/a&gt; London, which I’m going to attend.&lt;/p&gt;
+ &lt;h3&gt;&lt;a href=&quot;http://ardeenelinfierno.com/&quot;&gt;Francisco&lt;/a&gt;&lt;/h3&gt;
+ &lt;p&gt;Thinks he’s having issues with cables. It seems like the boards are not reset automatically by the Arduino IDE nowadays? He found the button in the board actually resets the board when pressed i.e. it’s the RESET button.&lt;/p&gt;
+ &lt;p&gt;On the Raspberry Pi side of things, he was very happy to put all his old-school Linux skills in action configuring network interfaces without GUIs!&lt;/p&gt;
+ &lt;h3&gt;&lt;a href=&quot;http://gu.illau.me/&quot;&gt;Guillaume&lt;/a&gt;&lt;/h3&gt;
+ &lt;p&gt;Played with mDNS advertising and listening to services on Raspberry Pi.&lt;/p&gt;
+ &lt;p&gt;(He was very quiet)&lt;/p&gt;
+ &lt;p&gt;(He also built a very nice LEGO case for the Raspberry Pi, but I do not have a picture, so just imagine it).&lt;/p&gt;
+ &lt;h3&gt;&lt;a href=&quot;http://wilsonpage.co.uk/&quot;&gt;Wilson&lt;/a&gt;&lt;/h3&gt;
+ &lt;blockquote&gt;&lt;p&gt;
+ Wilson: “I got my Raspberry Pi on the Wi-Fiâ€&lt;/p&gt;
+ &lt;p&gt;Francisco: “Sorry?â€&lt;/p&gt;
+ &lt;p&gt;Wilson: “I mean, you got my Raspberry Pi on the network. And now I’m trying to build a web app on the Pi…â€&lt;/p&gt;&lt;/blockquote&gt;
+ &lt;h3&gt;&lt;a href=&quot;http://chrislord.net/&quot;&gt;Chris&lt;/a&gt;&lt;/h3&gt;
+ &lt;p&gt;Exploring the Pebble with Linux. There’s a libpebble, and he managed to connect…&lt;/p&gt;
+ &lt;p&gt;&lt;del datetime=&quot;2016-01-20T11:22:33+00:00&quot;&gt;&lt;em&gt;&lt;small&gt;(sorry, I had to leave early so I do not know what else did Chris do!)&lt;/small&gt;&lt;/em&gt;&lt;/del&gt;&lt;/p&gt;
+ &lt;p&gt;Updated, 20 January: Chris told me he just managed to successfully connect to the Pebble watch using the bluetooth WebAPI. It requires two Gecko patches (one regression patch and one obvious logic error that he hasn’t filed yet). PROGRESS!&lt;/p&gt;
+ &lt;p&gt;~~~&lt;/p&gt;
+ &lt;p&gt;So as you can see we didn’t really get super far in just a day, and I even ended up with unusable hardware. BUT! we all learned something, and next time we know what NOT to do (or at least I DO KNOW what NOT to do!).&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;http://soledadpenades.com/?flattrss_redirect&amp;amp;id=6335&amp;amp;md5=40427d69faa3b9c2d1530732fd78e66d&quot; target=&quot;_blank&quot; title=&quot;Flattr&quot;&gt;&lt;img alt=&quot;flattr this!&quot; src=&quot;http://soledadpenades.com/wp-content/plugins/flattr/img/flattr-badge-large.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;</description>
+ <pubDate>Tue, 19 Jan 2016 13:31:55 +0000</pubDate>
+ <dc:creator>sole</dc:creator>
+ </item>
+ <item>
+ <title>Daniel Stenberg: “Subject: Urgent Warningâ€</title>
+ <guid isPermaLink="false">http://daniel.haxx.se/blog/?p=8544</guid>
+ <link>http://daniel.haxx.se/blog/2016/01/19/subject-urgent-warning/</link>
+ <description>&lt;p&gt;Back in December I got a desperate email from this person. A woman who said her Instagram had been hacked and since she found my contact info in the app she mailed me and asked for help. I of course replied and said that I have nothing to do with her being hacked but I also have nothing to do with Instagram other than that they use software I’ve written.&lt;/p&gt;
+ &lt;p&gt;Today she writes back. Clearly not convinced I told the truth before, and now she strikes back with more “evidence†of my wrongdoings.&lt;/p&gt;
+ &lt;p&gt;&lt;em&gt;Dear Daniel,&lt;/em&gt;&lt;/p&gt;
+ &lt;p&gt;&lt;em&gt;I had emailed you a couple months ago about my “screen dumps†aka screenshots and asked for your help with restoring my Instagram account since it had been hacked, my photos changed, and your name was included in the coding. You claimed to have no involvement whatsoever in developing a third party app for Instagram and could not help me salvage my original Instagram photos, pre-hacked, despite Instagram serving as my Photography portfolio and my career is a Photographer.&lt;/em&gt;&lt;/p&gt;
+ &lt;p&gt;&lt;em&gt;Since you weren’t aware that your name was attached to Instagram related hacking code, I thought you might want to know, in case you weren’t already aware, that your name is also included in Spotify terms and conditions. I came across this information using my Spotify which has also been hacked into and would love your help hacking out of Spotify. Also, I have yet to figure out how to unhack the hackers from my Instagram so if you change your mind and want to restore my Instagram to its original form as well as help me secure my account from future privacy breaches, I’d be extremely grateful. As you know, changing my passwords did nothing to resolve the problem. Please keep in mind that Facebook owns Instagram and these are big companies that you likely don’t want to have a trail of evidence that you are a part of an Instagram and Spotify hacking ring. Also, Spotify is a major partner of Spotify so you are likely familiar with the coding for all of these illegally developed third party apps. I’d be grateful for your help fixing this error immediately.&lt;/em&gt;&lt;/p&gt;
+ &lt;p&gt;&lt;em&gt;Thank you,&lt;/em&gt;&lt;/p&gt;
+ &lt;p&gt;[name redacted]&lt;/p&gt;
+ &lt;p&gt;&lt;em&gt;P.S. Please see attached screen dump for a screen shot of your contact info included in Spotify (or what more likely seems to be a hacked Spotify developed illegally by a third party).&lt;/em&gt;&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;http://daniel.haxx.se/blog/wp-content/uploads/2016/01/IMG_7393.png&quot; rel=&quot;attachment wp-att-8545&quot;&gt;&lt;img alt=&quot;Spotify credits screenshot&quot; class=&quot;aligncenter size-medium wp-image-8545&quot; height=&quot;450&quot; src=&quot;http://daniel.haxx.se/blog/wp-content/uploads/2016/01/IMG_7393-253x450.png&quot; width=&quot;253&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
+ &lt;p&gt;Here’s the Instagram screenshot she sent me in a previous email:&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;http://daniel.haxx.se/blog/wp-content/uploads/2016/01/IMG_2156.jpg&quot; rel=&quot;attachment wp-att-8546&quot;&gt;&lt;img alt=&quot;Instagram credits screenshot&quot; class=&quot;aligncenter size-medium wp-image-8546&quot; height=&quot;450&quot; src=&quot;http://daniel.haxx.se/blog/wp-content/uploads/2016/01/IMG_2156-253x450.jpg&quot; width=&quot;253&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
+ &lt;p&gt;I’ve tried to respond with calm and clear reasonable logic and technical details on why she’s seeing my name there. That clearly failed. What do I try next?&lt;/p&gt;</description>
+ <pubDate>Tue, 19 Jan 2016 08:37:32 +0000</pubDate>
+ <dc:creator>Daniel Stenberg</dc:creator>
+ </item>
+ <item>
+ <title>Emily Dunham: How much knowledge do you need to give a conference talk?</title>
+ <guid isPermaLink="true">http://edunham.net/2016/01/19/how_much_knowledge_do_you_need_to_give_a_conference_talk.html</guid>
+ <link>http://edunham.net/2016/01/19/how_much_knowledge_do_you_need_to_give_a_conference_talk.html</link>
+ <description>&lt;h3&gt;How much knowledge do you need to give a conference talk?&lt;/h3&gt;
+ &lt;p&gt;I was recently asked an excellent question when I promoted the &lt;a class=&quot;reference external&quot; href=&quot;http://www.linuxfestnorthwest.org/2016/present&quot;&gt;LFNW CFP&lt;/a&gt; on
+ IRC:&lt;/p&gt;
+ &lt;blockquote&gt;
+ &lt;div&gt;As someone who has never done a talk, but wants to, what kind of knowledge
+ do you need about a subject to give a talk on it?&lt;/div&gt;&lt;/blockquote&gt;
+ &lt;p&gt;If you answer “yes†to any of the following questions, you know enough to
+ propose a talk:&lt;/p&gt;
+ &lt;ul class=&quot;simple&quot;&gt;
+ &lt;li&gt;Do you have a &lt;strong&gt;hobby&lt;/strong&gt; that most tech people aren’t experts on? Talk
+ about applying a lesson or skill from that hobby to tech! For instance, I
+ turned a habit of reading about psychology into my &lt;a class=&quot;reference external&quot; href=&quot;http://talks.edunham.net/scale13x/#1&quot;&gt;Human Hacking&lt;/a&gt; talk.&lt;/li&gt;
+ &lt;li&gt;Have you ever spent a bunch of hours forcing two tools to work with each
+ other, because the documentation wasn’t very helpful and Googling didn’t get
+ you very far, and built something useful? “How to build ___ with ___†makes
+ a catchy talk title, if the &lt;strong&gt;thing you built&lt;/strong&gt; solves a common problem.&lt;/li&gt;
+ &lt;li&gt;Have you ever had a mentor sit down with you and explain a tool or
+ technique, and the new understanding improved the quality of your work or
+ code? Passing along useful &lt;strong&gt;lessons from your mentors&lt;/strong&gt; is a valuable talk,
+ because it allows others to benefit from the knowledge without taking as
+ much of your mentor’s time.&lt;/li&gt;
+ &lt;li&gt;Have you seen a dozen newbies ask the same question over the course of a few
+ months? When your &lt;strong&gt;answer to a common question&lt;/strong&gt; starts to feel like a
+ broken record, it’s time to compose it into a talk then link the newbies to
+ your slides or recording!&lt;/li&gt;
+ &lt;li&gt;Have you taken a really &lt;strong&gt;interesting class&lt;/strong&gt; lately? Can you distill part of it
+ into a 1-hour lesson that would appeal to nerds who don’t have the time or
+ resources to take the class themselves? (thanks &lt;a class=&quot;reference external&quot; href=&quot;http://lucywyman.me/&quot;&gt;lucyw&lt;/a&gt; for adding this to
+ the list!)&lt;/li&gt;
+ &lt;li&gt;Have you built a cool thing that over a dozen other people use? A &lt;strong&gt;tutorial
+ talk&lt;/strong&gt; can not only expand your community, but its recording can augment your
+ documentation and make the project more accessible for those who prefer to
+ learn directly from humans!&lt;/li&gt;
+ &lt;li&gt;Did you benefit from a really great introductory talk when you were learning
+ a tool? Consider doing your own tutorial! Any conference with beginners in
+ their target audience needs at least one Git lesson, an IRC talk, and some
+ discussions of how to use basic Unix utilities. These &lt;strong&gt;introductory talks&lt;/strong&gt;
+ are actually better when given by someone who learned the technology
+ relatively recently, because newer users remember what it’s like not to know
+ how to use it. Just remember to have a more expert user look over your slides
+ before you present, in case you made an incorrect assumption about the tool’s
+ more advanced functionality.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;I personally try to propose talks I want to hear, because the dealine of a
+ CFP or conference is great motivation to prioritize a cool project over
+ ordinary chores.&lt;/p&gt;</description>
+ <pubDate>Tue, 19 Jan 2016 08:00:00 +0000</pubDate>
+ </item>
+ <item>
+ <title>QMO: Aurora 45.0 Testday Results</title>
+ <guid isPermaLink="false">https://quality.mozilla.org/?p=49441</guid>
+ <link>https://quality.mozilla.org/2016/01/aurora-45-0-testday-results/</link>
+ <description>&lt;p&gt;Howdy mozillians!&lt;/p&gt;
+ &lt;p&gt;Last week – on &lt;em&gt;Friday, January 15th&lt;/em&gt; – we held &lt;a href=&quot;https://quality.mozilla.org/2016/01/firefox-45-0-aurora-testday-january-15th/&quot;&gt;Aurora 45.0 Testday&lt;/a&gt;; and, of course, it was another outstanding event!&lt;/p&gt;
+ &lt;p&gt;&lt;strong&gt;Thank you&lt;/strong&gt; all – &lt;span class=&quot;author-a-oz90z4z89zz89za7qfz70zda5z87zxz85z i&quot;&gt;&lt;i&gt;Mahmoudi Dris, Iryna Thompson, Chandrakant Dhutadmal, Preethi Dhinesh, Moin Shaikh, Ilse Macías, Hossain Al Ikram, Rezaul Huque Nayeem, Tahsan Chowdhury Akash, Kazi Nuzhat Tasnem, Fahmida Noor, Tazin Ahmed, Md. Ehsanul Hassan, Mohammad Maruf Islam, Kazi Sakib Ahmad, Khalid Syfullah Zaman, Asiful Kabir, Tabassum Mehnaz, Hasibul Hasan, Saddam Hossain, Mohammad Kamran Hossain, Amlan Biswas, Fazle Rabbi, Mohammed Jawad Ibne Ishaque, Asif Mahmud Shuvo, Nazir Ahmed Sabbir, Md. Raihan Ali, Md. Almas Hossain, Sadik Khan, Md. Faysal Alam Riyad, Faisal Mahmud, Md. Oliullah Sizan, Asif Mahmud Rony, Forhad Hossain &lt;/i&gt;and&lt;i&gt; Tanvir Rahman &lt;/i&gt;&lt;/span&gt;– for the participation!&lt;/p&gt;
+ &lt;p&gt;A big &lt;strong&gt;thank you&lt;/strong&gt; to all our active moderators too!&lt;/p&gt;
+ &lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;font-family: 'Open Sans', sans-serif;&quot;&gt;&lt;span style=&quot;font-size: medium;&quot;&gt;&lt;u&gt;Results:&lt;/u&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;font-family: 'Open Sans', sans-serif;&quot;&gt;&lt;span style=&quot;font-size: medium;&quot;&gt;&lt;strong&gt;15&lt;/strong&gt; issues were verified: &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;font-family: 'Open Sans', sans-serif;&quot;&gt;&lt;span style=&quot;font-size: medium;&quot;&gt; &lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1235821&quot;&gt;1235821&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1228518&quot;&gt;1228518&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1165637&quot;&gt;1165637&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1232647&quot;&gt;1232647&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1235379&quot;&gt;1235379&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=842356&quot;&gt;842356&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1222971&quot;&gt;1222971&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=915962&quot;&gt;915962&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1180761&quot;&gt;1180761&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1218455&quot;&gt;1218455&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1222747&quot;&gt;1222747&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1210752&quot;&gt;1210752&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1198450&quot;&gt;1198450&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1222820&quot;&gt;1222820&lt;/a&gt;, &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1225514&quot;&gt;1225514&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;strong&gt;1&lt;/strong&gt; bug was triaged: &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1230789&quot;&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;1230789&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;some failures were mentioned for &lt;i&gt;Search Refactoring &lt;/i&gt;feature in the etherpads (&lt;a href=&quot;https://public.etherpad-mozilla.org/p/testday-20160115&quot;&gt;link 1&lt;/a&gt; and &lt;a href=&quot;https://public.etherpad-mozilla.org/p/bangladesh.testday-15012016&quot;&gt;link 2&lt;/a&gt;); please feel free to add the requested details in the etherpads or, even better, join us on &lt;a href=&quot;http://widget01.mibbit.com/?server=irc.mozilla.org&amp;amp;channel=%23qa&quot; target=&quot;_blank&quot;&gt;#qa IRC channel&lt;/a&gt; and let’s figure them out&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;I &lt;strong&gt;strongly&lt;/strong&gt; advise everyone of you to reach out to us, the moderators, via &lt;a href=&quot;http://widget01.mibbit.com/?server=irc.mozilla.org&amp;amp;channel=%23qa&quot;&gt;#qa&lt;/a&gt; during the events when you encountered any kind of failures. Keep up the great work! \o/&lt;/p&gt;
+ &lt;p&gt;And keep an eye on QMO for upcoming events! &lt;img alt=&quot;😉&quot; class=&quot;wp-smiley&quot; src=&quot;https://s.w.org/images/core/emoji/72x72/1f609.png&quot; style=&quot;height: 1em;&quot; /&gt;&lt;/p&gt;</description>
+ <pubDate>Tue, 19 Jan 2016 07:51:57 +0000</pubDate>
+ <dc:creator>Alexandra Lucinet</dc:creator>
+ </item>
+ <item>
+ <title>Eitan Isaacson: It’s MLK Day and It’s Not Too Late to Do Something About It</title>
+ <guid isPermaLink="false">http://blog.monotonous.org/?p=678</guid>
+ <link>http://blog.monotonous.org/2016/01/18/its-mlk-day-and-its-not-too-late-to-do-something-about-it/</link>
+ <description>&lt;p&gt;For the last three years I have had the opportunity to send out a reminder to Mozilla staff that Martin Luther King Jr. Day is coming up, and that U.S. employees get the day off. It has turned into my MLK Day eve ritual. I read his letters, listen to speeches, and then I compose a belabored paragraph about Dr. King with some choice quotes.&lt;/p&gt;
+ &lt;p&gt;If you didn’t get a chance to celebrate Dr. King’s legacy and the movements he was a part of, you still have a chance:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Watch &lt;a href=&quot;http://www.imdb.com/title/tt1020072/&quot; target=&quot;_blank&quot;&gt;Selma.&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;Watch &lt;a href=&quot;http://www.imdb.com/title/tt1592527/&quot; target=&quot;_blank&quot;&gt;The Black Power Mixtape&lt;/a&gt; (it’s on Netflix).&lt;/li&gt;
+ &lt;li&gt;Read &lt;a href=&quot;http://www.africa.upenn.edu/Articles_Gen/Letter_Birmingham.html&quot; target=&quot;_blank&quot;&gt;A Letter from a Birmingham Jail&lt;/a&gt; (it’s really really good).&lt;/li&gt;
+ &lt;li&gt;Listen to his speech &lt;a href=&quot;https://www.youtube.com/watch?v=3Qf6x9_MLD0&quot; target=&quot;_blank&quot;&gt;Beyond Vietnam&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;Listen to his last speech &lt;a href=&quot;https://www.youtube.com/watch?v=IDl84vusXos&quot; target=&quot;_blank&quot;&gt;I Have Been To The Mountaintop&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;&lt;br /&gt; &lt;a href=&quot;http://feeds.wordpress.com/1.0/gocomments/blogdotmonotonousdotorg.wordpress.com/678/&quot; rel=&quot;nofollow&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; src=&quot;http://feeds.wordpress.com/1.0/comments/blogdotmonotonousdotorg.wordpress.com/678/&quot; /&gt;&lt;/a&gt; &lt;img alt=&quot;&quot; border=&quot;0&quot; height=&quot;1&quot; src=&quot;http://pixel.wp.com/b.gif?host=blog.monotonous.org&amp;amp;blog=34885741&amp;amp;post=678&amp;amp;subd=blogdotmonotonousdotorg&amp;amp;ref=&amp;amp;feed=1&quot; width=&quot;1&quot; /&gt;</description>
+ <pubDate>Mon, 18 Jan 2016 23:35:19 +0000</pubDate>
+ <dc:creator>Eitan</dc:creator>
+ </item>
+ <item>
+ <title>Nick Cameron: Libmacro</title>
+ <guid isPermaLink="false">http://www.ncameron.org/blog/rss/0e4d587c-380c-40ce-954a-7206f69bc1dd</guid>
+ <link>http://www.ncameron.org/blog/libmacro/</link>
+ <description>&lt;p&gt;As I outlined in an &lt;a href=&quot;http://ncameron.org/blog/procedural-macros-framework/&quot;&gt;earlier post&lt;/a&gt;, libmacro is a new crate designed to be used by procedural macro authors. It provides the basic API for procedural macros to interact with the compiler. I expect higher level functionality to be provided by library crates. In this post I'll go into a bit more detail about the API I think should be exposed here.&lt;/p&gt;
+
+ &lt;p&gt;This is a lot of stuff. I've probably missed something. If you use syntax extensions today and do something with libsyntax that would not be possible with libmacro, please let me know!&lt;/p&gt;
+
+ &lt;p&gt;I previously introduced &lt;code&gt;MacroContext&lt;/code&gt; as one of the gateways to libmacro. All procedural macros will have access to a &lt;code&gt;&amp;amp;mut MacroContext&lt;/code&gt;.&lt;/p&gt;
+
+ &lt;h3&gt;Tokens&lt;/h3&gt;
+
+ &lt;p&gt;I described the &lt;code&gt;tokens&lt;/code&gt; module in the last post, I won't repeat that here.&lt;/p&gt;
+
+ &lt;p&gt;There are a few more things I thought of. I mentioned a &lt;code&gt;TokenStream&lt;/code&gt; which is a sequence of tokens. We should also have &lt;code&gt;TokenSlice&lt;/code&gt; which is a borrowed slice of tokens (the slice to &lt;code&gt;TokenStream&lt;/code&gt;'s &lt;code&gt;Vec&lt;/code&gt;). These should implement the standard methods for sequences, in particular they support iteration, so can be &lt;code&gt;map&lt;/code&gt;ed, etc.&lt;/p&gt;
+
+ &lt;p&gt;In the earlier blog post, I talked about a token kind called &lt;code&gt;Delimited&lt;/code&gt; which contains a delimited sequence of tokens. I would like to rename that to &lt;code&gt;Sequence&lt;/code&gt; and add a &lt;code&gt;None&lt;/code&gt; variant to the &lt;code&gt;Delimiter&lt;/code&gt; enum. The &lt;code&gt;None&lt;/code&gt; option is so that we can have blocks of tokens without using delimiters. It will be used for noting unsafety and other properties of tokens. Furthermore, it is useful for macro expansion (replacing the interpolated AST tokens currently present). Although &lt;code&gt;None&lt;/code&gt; blocks do not affect scoping, they do affect precedence and parsing.&lt;/p&gt;
+
+ &lt;p&gt;We should provide API for creating tokens. By default these have no hygiene information and come with a span which has no place in the source code, but shows the source of the token to be the procedural macro itself (see below for how this interacts with expansion of the current macro). I expect a &lt;code&gt;make_&lt;/code&gt; function for each kind of token. We should also have API for creating macros in a given scope (which do the same thing but with provided hygiene information). This could be considered an over-rich API, since the hygiene information could be set after construction. However, since hygiene is fiddly and annoying to get right, we should make it as easy as possible to work with.&lt;/p&gt;
+
+ &lt;p&gt;There should also be a function for creating a token which is just a fresh name. This is useful for creating new identifiers. Although this can be done by interning a string and then creating a token around it, it is used frequently enough to deserve a helper function.&lt;/p&gt;
+
+ &lt;h3&gt;Emitting errors and warnings&lt;/h3&gt;
+
+ &lt;p&gt;Procedural macros should report errors, warnings, etc. via the &lt;code&gt;MacroContext&lt;/code&gt;. They should avoid panicking as much as possible since this will crash the compiler (once &lt;code&gt;catch_panic&lt;/code&gt; is available, we should use it to catch such panics and exit gracefully, however, they will certainly still meaning aborting compilation).&lt;/p&gt;
+
+ &lt;p&gt;Libmacro will 're-export' &lt;code&gt;DiagnosticBuilder&lt;/code&gt; from &lt;a href=&quot;https://dxr.mozilla.org/rust/source/src/libsyntax/errors/mod.rs&quot;&gt;syntax::errors&lt;/a&gt;. I don't actually expect this to be a literal re-export. We will use libmacro's version of &lt;code&gt;Span&lt;/code&gt;, for example.&lt;/p&gt;
+
+ &lt;pre&gt;&lt;code&gt;impl MacroContext {
+ pub fn struct_error(&amp;amp;self, &amp;amp;str) -&amp;gt; DiagnosticBuilder;
+ pub fn error(&amp;amp;self, Option&amp;lt;Span&amp;gt;, &amp;amp;str);
+ }
+
+ pub mod errors {
+ pub struct DiagnosticBuilder { ... }
+ impl DiagnosticBuilder { ... }
+ pub enum ErrorLevel { ... }
+ }
+ &lt;/code&gt;&lt;/pre&gt;
+
+ &lt;p&gt;There should be a macro &lt;code&gt;try_emit!&lt;/code&gt;, which reduces a &lt;code&gt;Result&amp;lt;T, ErrStruct&amp;gt;&lt;/code&gt; to a T or calls &lt;code&gt;emit()&lt;/code&gt; and then calls &lt;code&gt;unreachable!()&lt;/code&gt; (if the error is not fatal, then it should be upgraded to a fatal error).&lt;/p&gt;
+
+ &lt;h3&gt;Tokenising and quasi-quoting&lt;/h3&gt;
+
+ &lt;p&gt;The simplest function here is &lt;code&gt;tokenize&lt;/code&gt; which takes a string (&lt;code&gt;&amp;amp;str&lt;/code&gt;) and returns a &lt;code&gt;Result&amp;lt;TokenStream, ErrStruct&amp;gt;&lt;/code&gt;. The string is treated like source text. The success option is the tokenised version of the string. I expect this function must take a &lt;code&gt;MacroContext&lt;/code&gt; argument.&lt;/p&gt;
+
+ &lt;p&gt;We will offer a quasi-quoting macro. This will return a &lt;code&gt;TokenStream&lt;/code&gt; (in contrast to today's quasi-quoting which returns AST nodes), to be precise a &lt;code&gt;Result&amp;lt;TokenStream, ErrStruct&amp;gt;&lt;/code&gt;. The string which is quoted may include metavariables (&lt;code&gt;$x&lt;/code&gt;), and these are filled in with variables from the environment. The type of the variables should be either a &lt;code&gt;TokenStream&lt;/code&gt;, a &lt;code&gt;TokenTree&lt;/code&gt;, or a &lt;code&gt;Result&amp;lt;TokenStream, ErrStruct&amp;gt;&lt;/code&gt; (in this last case, if the variable is an error, then it is just returned by the macro). For example,&lt;/p&gt;
+
+ &lt;pre&gt;&lt;code&gt;fn foo(cx: &amp;amp;mut MacroContext, tokens: TokenStream) -&amp;gt; TokenStream {
+ quote!(cx, fn foo() { $tokens }).unwrap()
+ }
+ &lt;/code&gt;&lt;/pre&gt;
+
+ &lt;p&gt;The &lt;code&gt;quote!&lt;/code&gt; macro can also handle multiple tokens when the variable corresponding with the metavariable has type &lt;code&gt;[TokenStream]&lt;/code&gt; (or is dereferencable to it). In this case, the same syntax as used in macros-by-example can be used. For example, if &lt;code&gt;x: Vec&amp;lt;TokenStream&amp;gt;&lt;/code&gt; then &lt;code&gt;quote!(cx, ($x),*)&lt;/code&gt; will produce a &lt;code&gt;TokenStream&lt;/code&gt; of a comma-separated list of tokens from the elements of &lt;code&gt;x&lt;/code&gt;.&lt;/p&gt;
+
+ &lt;p&gt;Since the &lt;code&gt;tokenize&lt;/code&gt; function is a degenerate case of quasi-quoting, an alternative would be to always use &lt;code&gt;quote!&lt;/code&gt; and remove &lt;code&gt;tokenize&lt;/code&gt;. I believe there is utility in the simple function, and it must be used internally in any case.&lt;/p&gt;
+
+ &lt;p&gt;These functions and macros should create tokens with spans and hygiene information set as described above for making new tokens. We might also offer versions which takes a scope and uses that as the context for tokenising.&lt;/p&gt;
+
+ &lt;h3&gt;Parsing helper functions&lt;/h3&gt;
+
+ &lt;p&gt;There are some common patterns for tokens to follow in macros. In particular those used as arguments for attribute-like macros. We will offer some functions which attempt to parse tokens into these patterns. I expect there will be more of these in time; to start with:&lt;/p&gt;
+
+ &lt;pre&gt;&lt;code&gt;pub mod parsing {
+ // Expects `(foo = &quot;bar&quot;),*`
+ pub fn parse_keyed_values(&amp;amp;TokenSlice, &amp;amp;mut MacroContext) -&amp;gt; Result&amp;lt;Vec&amp;lt;(InternedString, String)&amp;gt;, ErrStruct&amp;gt;;
+ // Expects `&quot;bar&quot;`
+ pub fn parse_string(&amp;amp;TokenSlice, &amp;amp;mut MacroContext) -&amp;gt; Result&amp;lt;String, ErrStruct&amp;gt;;
+ }
+ &lt;/code&gt;&lt;/pre&gt;
+
+ &lt;p&gt;To be honest, given the token design in the last post, I think &lt;code&gt;parse_string&lt;/code&gt; is unnecessary, but I wanted to give more than one example of this kind of function. If &lt;code&gt;parse_keyed_values&lt;/code&gt; is the only one we end up with, then that is fine.&lt;/p&gt;
+
+ &lt;h3&gt;Pattern matching&lt;/h3&gt;
+
+ &lt;p&gt;The goal with the pattern matching API is to allow procedural macros to operate on tokens in the same way as macros-by-example. The pattern language is thus the same as that for macros-by-example.&lt;/p&gt;
+
+ &lt;p&gt;There is a single macro, which I propose calling &lt;code&gt;matches&lt;/code&gt;. Its first argument is the name of a &lt;code&gt;MacroContext&lt;/code&gt;. Its second argument is the input, which must be a &lt;code&gt;TokenSlice&lt;/code&gt; (or dereferencable to one). The third argument is a pattern definition. The macro produces a &lt;code&gt;Result&amp;lt;T, ErrStruct&amp;gt;&lt;/code&gt; where &lt;code&gt;T&lt;/code&gt; is the type produced by the pattern arms. If the pattern has multiple arms, then each arm must have the same type. An error is produced if none of the arms in the pattern are matched.&lt;/p&gt;
+
+ &lt;p&gt;The pattern language follows the language for defining macros-by-example (but is slightly stricter). There are two forms, a single pattern form and a multiple pattern form. If the first character is a &lt;code&gt;{&lt;/code&gt; then the pattern is treated as a multiple pattern form, if it starts with &lt;code&gt;(&lt;/code&gt; then as a single pattern form, otherwise an error (causes a panic with a &lt;code&gt;Bug&lt;/code&gt; error, as opposed to returning an &lt;code&gt;Err&lt;/code&gt;).&lt;/p&gt;
+
+ &lt;p&gt;The single pattern form is &lt;code&gt;(pattern) =&amp;gt; { code }&lt;/code&gt;. The multiple pattern form is &lt;code&gt;{(pattern) =&amp;gt; { code } (pattern) =&amp;gt; { code } ... (pattern) =&amp;gt; { code }}&lt;/code&gt;. &lt;code&gt;code&lt;/code&gt; is any old Rust code which is executed when the corresponding pattern is matched. The pattern follows from macros-by-example - it is a series of characters treated as literals, meta-variables indicated with &lt;code&gt;$&lt;/code&gt;, and the syntax for matching multiple variables. Any meta-variables are available as variables in the righthand side, e.g., &lt;code&gt;$x&lt;/code&gt; becomes available as &lt;code&gt;x&lt;/code&gt;. These variables have type &lt;code&gt;TokenStream&lt;/code&gt; if they appear singly or &lt;code&gt;Vec&amp;lt;TokenStream&amp;gt;&lt;/code&gt; if they appear multiply (or &lt;code&gt;Vec&amp;lt;Vec&amp;lt;TokenStream&amp;gt;&amp;gt;&lt;/code&gt; and so forth).&lt;/p&gt;
+
+ &lt;p&gt;Examples:&lt;/p&gt;
+
+ &lt;pre&gt;&lt;code&gt;matches!(cx, input, (foo($x:expr) bar) =&amp;gt; {quote(cx, foo_bar($x).unwrap()}).unwrap()
+
+ matches!(cx, input, {
+ () =&amp;gt; {
+ cx.err(&quot;No input?&quot;);
+ }
+ (foo($($x:ident),+ bar) =&amp;gt; {
+ println!(&quot;found {} idents&quot;, x.len());
+ quote!(($x);*).unwrap()
+ }
+ }
+ })
+ &lt;/code&gt;&lt;/pre&gt;
+
+ &lt;p&gt;Note that since we match AST items here, our backwards compatibility story is a bit complicated (though hopefully not much more so than with current macros).&lt;/p&gt;
+
+ &lt;h3&gt;Hygiene&lt;/h3&gt;
+
+ &lt;p&gt;The intention of the design is that the actual hygiene algorithm applied is irrelevant. Procedural macros should be able to use the same API if the hygiene algorithm changes (of course the result of applying the API might change). To this end, all hygiene objects are opaque and cannot be directly manipulated by macros.&lt;/p&gt;
+
+ &lt;p&gt;I propose one module (&lt;code&gt;hygiene&lt;/code&gt;) and two types: &lt;code&gt;Context&lt;/code&gt; and &lt;code&gt;Scope&lt;/code&gt;.&lt;/p&gt;
+
+ &lt;p&gt;A &lt;code&gt;Context&lt;/code&gt; is attached to each token and contains all hygiene information about that token. If two tokens have the same &lt;code&gt;Context&lt;/code&gt;, then they may be compared syntactically. The reverse is not true - two tokens can have different &lt;code&gt;Context&lt;/code&gt;s and still be equal. &lt;code&gt;Context&lt;/code&gt;s can only be created by applying the hygiene algorithm and cannot be manipulated, only moved and stored.&lt;/p&gt;
+
+ &lt;p&gt;&lt;code&gt;MacroContext&lt;/code&gt; has a method &lt;code&gt;fresh_hygiene_context&lt;/code&gt; for creating a new, fresh &lt;code&gt;Context&lt;/code&gt; (i.e., a &lt;code&gt;Context&lt;/code&gt; not shared with any other tokens).&lt;/p&gt;
+
+ &lt;p&gt;&lt;code&gt;MacroContext&lt;/code&gt; has a method &lt;code&gt;expansion_hygiene_context&lt;/code&gt; for getting the &lt;code&gt;Context&lt;/code&gt; where the macro is defined. This is equivalent to &lt;code&gt;.expansion_scope().direct_context()&lt;/code&gt;, but might be more efficient (and I expect it to be used a lot).&lt;/p&gt;
+
+ &lt;p&gt;A &lt;code&gt;Scope&lt;/code&gt; provides information about a position within an AST at a certain point during macro expansion. For example,&lt;/p&gt;
+
+ &lt;pre&gt;&lt;code&gt;fn foo() {
+ a
+ {
+ b
+ c
+ }
+ }
+ &lt;/code&gt;&lt;/pre&gt;
+
+ &lt;p&gt;&lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; will have different &lt;code&gt;Scope&lt;/code&gt;s. &lt;code&gt;b&lt;/code&gt; and &lt;code&gt;c&lt;/code&gt; will have the same &lt;code&gt;Scope&lt;/code&gt;s, even if &lt;code&gt;b&lt;/code&gt; was written in this position and &lt;code&gt;c&lt;/code&gt; is due to macro expansion. However, a &lt;code&gt;Scope&lt;/code&gt; may contain more information than just the syntactic scopes, for example, it may contain information about pending scopes yet to be applied by the hygiene algorithm (i.e., information about &lt;code&gt;let&lt;/code&gt; expressions which are in scope).&lt;/p&gt;
+
+ &lt;p&gt;Note that a &lt;code&gt;Scope&lt;/code&gt; means a scope in the macro hygiene sense, not the commonly used sense of a scope declared with &lt;code&gt;{}&lt;/code&gt;. In particular, each &lt;code&gt;let&lt;/code&gt; statement starts a new scope and the items and statements in a function body are in different scopes.&lt;/p&gt;
+
+ &lt;p&gt;The functions &lt;code&gt;lookup_item_scope&lt;/code&gt; and &lt;code&gt;lookup_statement_scope&lt;/code&gt; take a &lt;code&gt;MacroContext&lt;/code&gt; and a path, represented as a &lt;code&gt;TokenSlice&lt;/code&gt;, and return the &lt;code&gt;Scope&lt;/code&gt; which that item defines or an error if the path does not refer to an item, or the item does not define a scope of the right kind.&lt;/p&gt;
+
+ &lt;p&gt;The function &lt;code&gt;lookup_scope_for&lt;/code&gt; is similar, but returns the &lt;code&gt;Scope&lt;/code&gt; in which an item is declared.&lt;/p&gt;
+
+ &lt;p&gt;&lt;code&gt;MacroContext&lt;/code&gt; has a method &lt;code&gt;expansion_scope&lt;/code&gt; for getting the scope in which the current macro is being expanded.&lt;/p&gt;
+
+ &lt;p&gt;&lt;code&gt;Scope&lt;/code&gt; has a method &lt;code&gt;direct_context&lt;/code&gt; which returns a &lt;code&gt;Context&lt;/code&gt; for items declared directly (c.f., via macro expansion) in that &lt;code&gt;Scope&lt;/code&gt;.&lt;/p&gt;
+
+ &lt;p&gt;&lt;code&gt;Scope&lt;/code&gt; has a method &lt;code&gt;nested&lt;/code&gt; which creates a fresh &lt;code&gt;Scope&lt;/code&gt; nested within the receiver scope.&lt;/p&gt;
+
+ &lt;p&gt;&lt;code&gt;Scope&lt;/code&gt; has a static method &lt;code&gt;empty&lt;/code&gt; for creating an empty scope, that is one with no scope information at all (note that this is different from a top-level scope).&lt;/p&gt;
+
+ &lt;p&gt;I expect the exact API around &lt;code&gt;Scope&lt;/code&gt;s and &lt;code&gt;Context&lt;/code&gt;s will need some work. &lt;code&gt;Scope&lt;/code&gt; seems halfway between an intuitive, algorithm-neutral abstraction, and the scopes from the sets of scopes hygiene algorithm. I would prefer a &lt;code&gt;Scope&lt;/code&gt; should be more abstract, on the other hand, macro authors may want fine-grained control over hygiene application.&lt;/p&gt;
+
+ &lt;h4&gt;Manipulating hygiene information on tokens,&lt;/h4&gt;
+
+ &lt;pre&gt;&lt;code&gt;pub mod hygiene {
+ pub fn add(cx: &amp;amp;mut MacroContext, t: &amp;amp;Token, scope: &amp;amp;Scope) -&amp;gt; Token;
+ // Maybe unnecessary if we have direct access to Tokens.
+ pub fn set(t: &amp;amp;Token, cx: &amp;amp;Context) -&amp;gt; Token;
+ // Maybe unnecessary - can use set with cx.expansion_hygiene_context().
+ // Also, bad name.
+ pub fn current(cx: &amp;amp;MacroContext, t: &amp;amp;Token) -&amp;gt; Token;
+ }
+ &lt;/code&gt;&lt;/pre&gt;
+
+ &lt;p&gt;&lt;code&gt;add&lt;/code&gt; adds &lt;code&gt;scope&lt;/code&gt; to any context already on &lt;code&gt;t&lt;/code&gt; (&lt;code&gt;Context&lt;/code&gt; should have a similar method). Note that the implementation is a bit complex - the nature of the &lt;code&gt;Scope&lt;/code&gt; might mean we replace the old context completely, or add to it.&lt;/p&gt;
+
+ &lt;h4&gt;Applying hygiene when expanding the current macro&lt;/h4&gt;
+
+ &lt;p&gt;By default, the current macro will be expanded in the standard way, having hygiene applied as expected. Mechanically, hygiene information is added to tokens when the macro is expanded. Assuming the sets of scopes algorithm, scopes (for example, for the macro's definition, and for the introduction) are added to any scopes already present on the token. A token with no hygiene information will thus behave like a token in a macro-by-example macro. Hygiene due to nested scopes created by the macro do not need to be taken into account by the macro author, this is handled at expansion time.&lt;/p&gt;
+
+ &lt;p&gt;Procedural macro authors may want to customise hygiene application (it is common in Racket), for example, to introduce items that can be referred to by code in the call-site scope.&lt;/p&gt;
+
+ &lt;p&gt;We must provide an option to expand the current macro without applying hygiene; the macro author must then handle hygiene. For this to work, the macro must be able to access information about the scope in which it is applied (see &lt;code&gt;MacroContext::expansion_scope&lt;/code&gt;, above) and to supply a &lt;code&gt;Scope&lt;/code&gt; indicating scopes that should be added to tokens following the macro expansion.&lt;/p&gt;
+
+ &lt;pre&gt;&lt;code&gt;pub mod hygiene {
+ pub enum ExpansionMode {
+ Automatic,
+ Manual(Scope),
+ }
+ }
+
+ impl MacroContext {
+ pub fn set_hygienic_expansion(hygiene::ExpansionMode);
+ }
+ &lt;/code&gt;&lt;/pre&gt;
+
+ &lt;p&gt;We may wish to offer other modes for expansion which allow for tweaking hygiene application without requiring full manual application. One possible mode is where the author provides a &lt;code&gt;Scope&lt;/code&gt; for the macro definition (rather than using the scope where the macro is actually defined), but hygiene is otherwise applied automatically. We might wish to give the author the option of applying scopes due to the macro definition, but not the introduction scopes.&lt;/p&gt;
+
+ &lt;p&gt;On a related note, might we want to affect how spans are applied when the current macro is expanded? I can't think of a use case right now, but it seems like something that might be wanted.&lt;/p&gt;
+
+ &lt;p&gt;Blocks of tokens (that is a &lt;code&gt;Sequence&lt;/code&gt; token) may be marked (not sure how, exactly, perhaps using a distinguished context) such that it is expanded without any hygiene being applied or spans changed. There should be a function for creating such a &lt;code&gt;Sequence&lt;/code&gt; from a &lt;code&gt;TokenSlice&lt;/code&gt; in the &lt;code&gt;tokens&lt;/code&gt; module. The primary motivation for this is to handle the tokens representing the body on which an annotation-like macro is present. For a 'decorator' macro, these tokens will be untouched (passed through by the macro), and since they are not touched by the macro, they should appear untouched by it (in terms of hygiene and spans).&lt;/p&gt;
+
+ &lt;h3&gt;Applying macros&lt;/h3&gt;
+
+ &lt;p&gt;We provide functionality to expand a provided macro or to lookup and expand a macro.&lt;/p&gt;
+
+ &lt;pre&gt;&lt;code&gt;pub mod apply {
+ pub fn expand_macro(cx: &amp;amp;mut MacroContext,
+ expansion_scope: Scope,
+ macro: &amp;amp;TokenSlice,
+ macro_scope: Scope,
+ input: &amp;amp;TokenSlice)
+ -&amp;gt; Result&amp;lt;(TokenStream, Scope), ErrStruct&amp;gt;;
+ pub fn lookup_and_expand_macro(cx: &amp;amp;mut MacroContext,
+ expansion_scope: Scope,
+ macro: &amp;amp;TokenSlice,
+ input: &amp;amp;TokenSlice)
+ -&amp;gt; Result&amp;lt;(TokenStream, Scope), ErrStruct&amp;gt;;
+ }
+ &lt;/code&gt;&lt;/pre&gt;
+
+ &lt;p&gt;These functions apply macro hygiene in the usual way, with &lt;code&gt;expansion_scope&lt;/code&gt; dictating the scope into which the macro is expanded. Other spans and hygiene information is taken from the tokens. &lt;code&gt;expand_macro&lt;/code&gt; takes pending scopes from &lt;code&gt;macro_scope&lt;/code&gt;, &lt;code&gt;lookup_and_expand_macro&lt;/code&gt; uses the proper pending scopes. In order to apply the hygiene algorithm, the result of the macro must be parsable. The returned scope will contain pending scopes that can be applied by the macro to subsequent tokens.&lt;/p&gt;
+
+ &lt;p&gt;We could provide versions that don't take an &lt;code&gt;expansion_scope&lt;/code&gt; and use &lt;code&gt;cx.expansion_scope()&lt;/code&gt;. Probably unnecessary.&lt;/p&gt;
+
+ &lt;pre&gt;&lt;code&gt;pub mod apply {
+ pub fn expand_macro_unhygienic(cx: &amp;amp;mut MacroContext,
+ macro: &amp;amp;TokenSlice,
+ input: &amp;amp;TokenSlice)
+ -&amp;gt; Result&amp;lt;TokenStream, ErrStruct&amp;gt;;
+ pub fn lookup_and_expand_macro_unhygienic(cx: &amp;amp;mut MacroContext,
+ macro: &amp;amp;TokenSlice,
+ input: &amp;amp;TokenSlice)
+ -&amp;gt; Result&amp;lt;TokenStream, ErrStruct&amp;gt;;
+ }
+ &lt;/code&gt;&lt;/pre&gt;
+
+ &lt;p&gt;The &lt;code&gt;_unhygienic&lt;/code&gt; variants expand a macro as in the first functions, but do not apply the hygiene algorithm or change any hygiene information. Any hygiene information on tokens is preserved. I'm not sure if &lt;code&gt;_unhygienic&lt;/code&gt; are the right names - using these is not necessarily unhygienic, just that we are automatically applying the hygiene algorithm.&lt;/p&gt;
+
+ &lt;p&gt;Note that all these functions are doing an eager expansion of macros, or in Scheme terms they are &lt;code&gt;local-expand&lt;/code&gt; functions. &lt;/p&gt;
+
+ &lt;h3&gt;Looking up items&lt;/h3&gt;
+
+ &lt;p&gt;The function &lt;code&gt;lookup_item&lt;/code&gt; takes a &lt;code&gt;MacroContext&lt;/code&gt; and a path represented as a &lt;code&gt;TokenSlice&lt;/code&gt; and returns a &lt;code&gt;TokenStream&lt;/code&gt; for the item referred to by the path, or an error if name resolution failed. I'm not sure where this function should live.&lt;/p&gt;
+
+ &lt;h3&gt;Interned strings&lt;/h3&gt;
+
+ &lt;pre&gt;&lt;code&gt;pub mod strings {
+ pub struct InternedString;
+
+ impl InternedString {
+ pub fn get(&amp;amp;self) -&amp;gt; String;
+ }
+
+ pub fn intern(cx: &amp;amp;mut MacroContext, s: &amp;amp;str) -&amp;gt; Result&amp;lt;InternedString, ErrStruct&amp;gt;;
+ pub fn find(cx: &amp;amp;mut MacroContext, s: &amp;amp;str) -&amp;gt; Result&amp;lt;InternedString, ErrStruct&amp;gt;;
+ pub fn find_or_intern(cx: &amp;amp;mut MacroContext, s: &amp;amp;str) -&amp;gt; Result&amp;lt;InternedString, ErrStruct&amp;gt;;
+ }
+ &lt;/code&gt;&lt;/pre&gt;
+
+ &lt;p&gt;&lt;code&gt;intern&lt;/code&gt; interns a string and returns a fresh &lt;code&gt;InternedString&lt;/code&gt;. &lt;code&gt;find&lt;/code&gt; tries to find &lt;em&gt;an&lt;/em&gt; existing &lt;code&gt;InternedString&lt;/code&gt;.&lt;/p&gt;
+
+ &lt;h3&gt;Spans&lt;/h3&gt;
+
+ &lt;p&gt;A span gives information about where in the source code a token is defined. It also gives information about where the token came from (how it was generated, if it was generated code).&lt;/p&gt;
+
+ &lt;p&gt;There should be a &lt;code&gt;spans&lt;/code&gt; module in libmacro, which will include a &lt;code&gt;Span&lt;/code&gt; type which can be easily inter-converted with the &lt;code&gt;Span&lt;/code&gt; defined in libsyntax. Libsyntax spans currently include information about stability, this will not be present in libmacro spans.&lt;/p&gt;
+
+ &lt;p&gt;If the programmer does nothing special with spans, then they will be 'correct' by default. There are two important cases: tokens passed to the macro and tokens made fresh by the macro. The former will have the source span indicating where they were written and will include their history. The latter will have no source span and indicate they were created by the current macro. All tokens will have the history relating to expansion of the current macro added when the macro is expanded. At macro expansion, tokens with no source span will be given the macro use-site as their source.&lt;/p&gt;
+
+ &lt;p&gt;&lt;code&gt;Span&lt;/code&gt;s can be freely copied between tokens.&lt;/p&gt;
+
+ &lt;p&gt;It will probably useful to make it easy to manipulate spans. For example, rather than point at the macro's defining function, point at a helper function where the token is made. Or to set the origin to the current macro when the token was produced by another which should an implementation detail. I'm not sure what such an interface should look like (and is probably not necessary in an initial library).&lt;/p&gt;
+
+ &lt;h3&gt;Feature gates&lt;/h3&gt;
+
+ &lt;pre&gt;&lt;code&gt;pub mod features {
+ pub enum FeatureStatus {
+ // The feature gate is allowed.
+ Allowed,
+ // The feature gate has not been enabled.
+ Disallowed,
+ // Use of the feature is forbidden by the compiler.
+ Forbidden,
+ }
+
+ pub fn query_feature(cx: &amp;amp;MacroContext, feature: Token) -&amp;gt; Result&amp;lt;FeatureStatus, ErrStruct&amp;gt;;
+ pub fn query_feature_by_str(cx: &amp;amp;MacroContext, feature: &amp;amp;str) -&amp;gt; Result&amp;lt;FeatureStatus, ErrStruct&amp;gt;;
+ pub fn query_feature_unused(cx: &amp;amp;MacroContext, feature: Token) -&amp;gt; Result&amp;lt;FeatureStatus, ErrStruct&amp;gt;;
+ pub fn query_feature_by_str_unused(cx: &amp;amp;MacroContext, feature: &amp;amp;str) -&amp;gt; Result&amp;lt;FeatureStatus, ErrStruct&amp;gt;;
+
+ pub fn used_feature_gate(cx: &amp;amp;MacroContext, feature: Token) -&amp;gt; Result&amp;lt;(), ErrStruct&amp;gt;;
+ pub fn used_feature_by_str(cx: &amp;amp;MacroContext, feature: &amp;amp;str) -&amp;gt; Result&amp;lt;(), ErrStruct&amp;gt;;
+
+ pub fn allow_feature_gate(cx: &amp;amp;MacroContext, feature: Token) -&amp;gt; Result&amp;lt;(), ErrStruct&amp;gt;;
+ pub fn allow_feature_by_str(cx: &amp;amp;MacroContext, feature: &amp;amp;str) -&amp;gt; Result&amp;lt;(), ErrStruct&amp;gt;;
+ pub fn disallow_feature_gate(cx: &amp;amp;MacroContext, feature: Token) -&amp;gt; Result&amp;lt;(), ErrStruct&amp;gt;;
+ pub fn disallow_feature_by_str(cx: &amp;amp;MacroContext, feature: &amp;amp;str) -&amp;gt; Result&amp;lt;(), ErrStruct&amp;gt;;
+ }
+ &lt;/code&gt;&lt;/pre&gt;
+
+ &lt;p&gt;The &lt;code&gt;query_*&lt;/code&gt; functions query if a feature gate has been set. They return an error if the feature gate does not exist. The &lt;code&gt;_unused&lt;/code&gt; variants do not mark the feature gate as used. The &lt;code&gt;used_&lt;/code&gt; functions mark a feature gate as used, or return an error if it does not exist.&lt;/p&gt;
+
+ &lt;p&gt;The &lt;code&gt;allow_&lt;/code&gt; and &lt;code&gt;disallow_&lt;/code&gt; functions set a feature gate as allowed or disallowed for the current crate. These functions will only affect feature gates which take affect after parsing and expansion are complete. They do not affect feature gates which are checked during parsing or expansion.&lt;/p&gt;
+
+ &lt;p&gt;Question: do we need the &lt;code&gt;used_&lt;/code&gt; functions? Could just call &lt;code&gt;query_&lt;/code&gt; and ignore the result.&lt;/p&gt;
+
+ &lt;h3&gt;Attributes&lt;/h3&gt;
+
+ &lt;p&gt;We need some mechanism for setting attributes as used. I don't actually know how the unused attribute checking in the compiler works, so I can't spec this area. But, I expect &lt;code&gt;MacroContext&lt;/code&gt; to make available some interface for reading attributes on a macro use and marking them as used.&lt;/p&gt;</description>
+ <pubDate>Mon, 18 Jan 2016 21:40:42 +0000</pubDate>
+ <dc:creator>Nick Cameron</dc:creator>
+ </item>
+ <item>
+ <title>Seif Lotfy: Skizze progress and REPL</title>
+ <guid isPermaLink="false">http://geekyogre.com/rss/63eb682d-66b4-447d-8fb6-f4ed448019df</guid>
+ <link>http://geekyogre.com/skizze-progress-and-repl/</link>
+ <description>&lt;p&gt;&lt;img align=&quot;center&quot; height=&quot;190&quot; src=&quot;http://i.imgur.com/9z47NdA.png&quot; width=&quot;600&quot; /&gt; &lt;br /&gt;
+ &lt;br /&gt; &lt;br /&gt;
+ Over the last 3 weeks, based on feedback we proceeded fledging out the concepts and the code behind &lt;a href=&quot;https://github.com/skizzehq/skizze&quot;&gt;Skizze&lt;/a&gt;. &lt;br /&gt;
+ &lt;a href=&quot;https://medium.com/@njpatel/&quot;&gt;Neil Patel&lt;/a&gt; suggested the following:&lt;/p&gt;
+
+ &lt;hr /&gt;
+
+ &lt;p&gt;&lt;em&gt;So I've been thinking about the server API. I think we want to choose one thing and do it as well as possible, instead of having six ways to talk to the server. I think that helps to keep things sane and simple overall.&lt;/em&gt;&lt;/p&gt;
+
+ &lt;p&gt;&lt;em&gt;Thinking about usage, I can only really imagine Skizze in an environment like &lt;a href=&quot;https://xamarin.com/insights&quot;&gt;ours&lt;/a&gt;, which is high-throughput. I think that is it's 'home' and we should be optimising for that all day long.&lt;/em&gt;&lt;/p&gt;
+
+ &lt;p&gt;&lt;em&gt;Taking that into account, I believe we have two options:&lt;/em&gt;&lt;/p&gt;
+
+ &lt;ol&gt;
+ &lt;li&gt;&lt;p&gt;&lt;em&gt;We go the gRPC route, provide .proto files and let people use the existing gRPC tooling to build support for their favourite language. That means we can happily give Ruby/Node/C#/etc devs a real way to get started up with Skizze almost immediately, piggy-backing on the gRPC docs etc.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;p&gt;&lt;em&gt;We absorb the Redis Protocol. It does everything we need, is very lean, and we can (mostly) easily adapt it for what we need to do. The downside is that to get support from other libs, there will have to be actual libraries for every language. This could slow adoption, or it might be easy enough if people can reuse existing REDIS code. It's hard to tell how that would end up.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
+ &lt;/ol&gt;
+
+ &lt;p&gt;&lt;em&gt;gRPC is interesting because it's built already for distributed systems, across bad networks, and obviously is bi-directional etc. Without us having to spend time on the protocol, gRPC let's us easily add features that require streaming. Like, imagine a client being able to listen for changes in count/size and be notified instantly. That's something that gRPC is built for right now.&lt;/em&gt;&lt;/p&gt;
+
+ &lt;p&gt;&lt;em&gt;I think gRPC is a bit verbose, but I think it'll pay off for ease of third-party lib support and as things grow.&lt;/em&gt;&lt;/p&gt;
+
+ &lt;p&gt;&lt;em&gt;The CLI could easily be built to work with gRPC, including adding support for streaming stuff etc. Which could be pretty exciting.&lt;/em&gt;&lt;/p&gt;
+
+ &lt;hr /&gt;
+
+ &lt;p&gt;That being said, we gave Skizze &lt;a href=&quot;https://github.com/skizzehq/&quot;&gt;a new home&lt;/a&gt;, where based on feedback we developed .proto files and started rewriting big chunks of the code.&lt;/p&gt;
+
+ &lt;p&gt;We added a new wrapper called &quot;domain&quot; which represents a stream. It wraps around Count-Min-Log, Bloom Filter, Top-K and HyperLogLog++, so when feeding it values it feeds all the sketches. Later we intend to allow attaching and detaching sketches from &quot;domains&quot; (We need a better name).&lt;/p&gt;
+
+ &lt;p&gt;We also implemented a gRPC API which should allow easy wrapper creation in other languages.&lt;/p&gt;
+
+ &lt;p&gt;Special thanks go to &lt;a href=&quot;https://twitter.com/martinpintob&quot;&gt;Martin Pinto&lt;/a&gt; for helping out with unit tests and &lt;a href=&quot;http://dopeness.org&quot;&gt;Soren Macbeth&lt;/a&gt; for thorough feedback and ideas about the &quot;domain&quot; concept. &lt;br /&gt;
+ Take a look at our initial REPL work there:&lt;/p&gt;
+
+ &lt;p&gt;&lt;a href=&quot;http://geekyogre.com/content/images/2016/01/MBCY64aaKL.gif&quot;&gt;&lt;img alt=&quot;Link to this page&quot; border=&quot;0&quot; src=&quot;http://geekyogre.com/content/images/2016/01/skizze-1.png&quot; /&gt;&lt;/a&gt; &lt;br /&gt;
+ &lt;a href=&quot;http://geekyogre.com/content/images/2016/01/MBCY64aaKL.gif&quot;&gt;click for GIF&lt;/a&gt;&lt;/p&gt;</description>
+ <pubDate>Mon, 18 Jan 2016 17:41:43 +0000</pubDate>
+ <dc:creator>Seif Lotfy</dc:creator>
+ </item>
+ <item>
+ <title>Doug Belshaw: What a post-Persona landscape means for Open Badges</title>
+ <guid isPermaLink="false">http://dougbelshaw.com/blog/?p=39986</guid>
+ <link>http://dougbelshaw.com/blog/2016/01/18/open-badges-persona/</link>
+ <description>&lt;p&gt;&lt;em&gt;&lt;strong&gt;Note:&lt;/strong&gt; I don’t work for Mozilla any more, so (like &lt;a href=&quot;https://www.youtube.com/watch?v=YQHsXMglC9A&quot;&gt;Adele&lt;/a&gt;) these are my thoughts ‘from the outside’…&lt;/em&gt;&lt;/p&gt;
+ &lt;hr /&gt;
+ &lt;h3&gt;Introduction&lt;/h3&gt;
+ &lt;p&gt;&lt;a href=&quot;http://openbadges.org&quot;&gt;Open Badges&lt;/a&gt; is no longer a &lt;a href=&quot;http://mozilla.org&quot;&gt;Mozilla&lt;/a&gt; project. In fact, it hasn’t been for a while — the &lt;a href=&quot;http://badgealliance.org&quot;&gt;Badge Alliance&lt;/a&gt; was set up a couple of years ago to promote the specification on a both a technical and community basis. As I stated in a recent post, this is a &lt;strong&gt;good&lt;/strong&gt; thing and means that &lt;a href=&quot;http://dougbelshaw.com/blog/2015/11/08/bright-future-badges/&quot;&gt;the future is bright for Open Badges&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;However, Mozilla &lt;em&gt;is&lt;/em&gt; still involved with the Open Badges project: Mark Surman, Executive Director of the Mozilla Foundation, sits on the board of the Badge Alliance. Mozilla also pays for contractors to work on the &lt;a href=&quot;http://backpack.openbadges.org&quot;&gt;Open Badges backpack&lt;/a&gt; and there were badges earned at the &lt;a href=&quot;http://mozillafestival.org&quot;&gt;Mozilla Festival&lt;/a&gt; a few months ago.&lt;/p&gt;
+ &lt;p&gt;Although it may seem strange for those used to corporates interested purely in profit, Mozilla creates what the open web needs at any given time. Like any organisation, sometimes it gets these wrong, either because the concept was flawed, or because the execution was poor. Other times, I’d argue, Mozilla doesn’t give ideas and concepts enough time to gain traction.&lt;/p&gt;
+ &lt;h3&gt;The end of Persona at Mozilla&lt;/h3&gt;
+ &lt;p&gt;Open Badges, at its very essence, is a technical specification. It allows credentials with metadata hard-coded into them to be issued, exchanged, and displayed. This is done in a secure, standardised manner.&lt;/p&gt;
+ &lt;p&gt;&lt;img alt=&quot;OBI diagram&quot; class=&quot;alignnone wp-image-39987 size-full&quot; src=&quot;http://i1.wp.com/dougbelshaw.com/blog/wp-content/uploads/2016/01/obi-diagram.png?w=100%25&quot; /&gt;&lt;/p&gt;
+ &lt;p&gt;For users to be able to access their ‘backpack’ (i.e. the place they store badges) they needed a secure login system.Back in 2011 at the start of the Open Badges project it made sense to make use of Mozilla’s nascent &lt;a href=&quot;https://www.mozilla.org/en-US/persona/&quot;&gt;Persona&lt;/a&gt; project. This aimed to provide a way for users to easily sign into sites around the web without using their Facebook/Google logins. These ‘social’ sign-in methods mean that users are tracked around the web — something that Mozilla was obviously against.&lt;/p&gt;
+ &lt;p&gt;By 2014, Persona wasn’t seen to be having the kind of ‘growth trajectory’ that Mozilla wanted. The project was transferred to &lt;a href=&quot;http://identity.mozilla.com/post/78873831485/transitioning-persona-to-community-ownership&quot;&gt;community ownership&lt;/a&gt; and most of the team left Mozilla in 2015. It was &lt;a href=&quot;https://groups.google.com/forum/#!msg/mozilla.dev.identity/mibOQrD6K0c/kt0NdMWbEQAJ&quot;&gt;announced&lt;/a&gt; that Persona would be shutting down as a Mozilla service in November 2016. While Persona will exist as an open source project, it won’t be hosted by Mozilla.&lt;/p&gt;
+ &lt;h3&gt;What this means for Open Badges&lt;/h3&gt;
+ &lt;p&gt;Although I’m not aware of an official announcement from the Badge Alliance, I think it’s worth making three points here.&lt;/p&gt;
+ &lt;h5&gt;1. You can still use Persona&lt;/h5&gt;
+ &lt;p&gt;If you’re a developer, you can still use Persona. It’s open source. It works.&lt;/p&gt;
+ &lt;h5&gt;2. Persona is not central to the Open Badges Infrastructure&lt;/h5&gt;
+ &lt;p&gt;The Open Badges backpack is &lt;em&gt;one&lt;/em&gt; place where users can store their badges. There are others, including the &lt;a href=&quot;https://openbadgepassport.com/&quot;&gt;Open Badge Passport&lt;/a&gt; and &lt;a href=&quot;https://www.openbadgeacademy.com/&quot;&gt;Open Badge Academy&lt;/a&gt;. MacArthur, who seed-funded the Open Badges ecosystem, have a new platform launching through &lt;a href=&quot;https://www.lrng.org/&quot;&gt;LRNG&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;It is up to the organisations behind these various solutions as to how they allow users to authenticate. They may choose to allow social logins. They may force users to create logins based on their email address. They may decide to use an open source version of Persona. It’s entirely up to them.&lt;/p&gt;
+ &lt;h5&gt;3. A post-Persona badges system has its advantages&lt;/h5&gt;
+ &lt;p&gt;The Persona authentication system runs off email addresses. This means that transitioning &lt;em&gt;from&lt;/em&gt; Persona to another system is relatively straightforward. It has, however, meant that for the past few years we’ve had a recurrent problem: what do you do with people being issued badges to multiple email addresses?&lt;/p&gt;
+ &lt;p&gt;Tying badges to emails seemed like the easiest and fastest way to get to a critical mass in terms of Open Badge adoption. Now that’s worked, we need to think in a more nuanced way about allowing users to tie multiple identities to a single badge.&lt;/p&gt;
+ &lt;h4&gt;Conclusion&lt;/h4&gt;
+ &lt;p&gt;Persona was always a slightly awkward fit for Open Badges. Although, for a time, it made sense to use Persona for authentication to the Open Badges backpack, we’re now in a post-Persona landscape. This brings with it certain advantages.&lt;/p&gt;
+ &lt;p&gt;As Nate Otto wrote in his post &lt;a href=&quot;https://medium.com/badge-alliance/open-badges-in-2016-a-look-ahead-3cfe5c3c9878#.l5mhiztwx&quot;&gt;Open Badges in 2016: A Look Ahead&lt;/a&gt;, the project is growing up. It’s time to move beyond what was expedient at the dawn of Open Badges and look to the future. I’m sad to see the decline of Persona, but I’m excited what the future holds!&lt;/p&gt;
+ &lt;p style=&quot;text-align: right;&quot;&gt;&lt;em&gt;Header image CC BY-NC-SA &lt;a href=&quot;https://www.flickr.com/photos/blmiers2/6904758951/&quot;&gt;Barbara&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</description>
+ <pubDate>Mon, 18 Jan 2016 11:34:19 +0000</pubDate>
+ <dc:creator>Doug Belshaw</dc:creator>
+ </item>
+ <item>
+ <title>This Week In Rust: This Week in Rust 114</title>
+ <guid isPermaLink="false">tag:this-week-in-rust.org,2016-01-18:blog/2016/01/18/this-week-in-rust-114/</guid>
+ <link>http://this-week-in-rust.org/blog/2016/01/18/this-week-in-rust-114/</link>
+ <description>&lt;p&gt;Hello and welcome to another issue of &lt;em&gt;This Week in Rust&lt;/em&gt;!
+ &lt;a href=&quot;http://rust-lang.org&quot;&gt;Rust&lt;/a&gt; is a systems language pursuing the trifecta:
+ safety, concurrency, and speed. This is a weekly summary of its progress and
+ community. Want something mentioned? Tweet us at &lt;a href=&quot;https://twitter.com/ThisWeekInRust&quot;&gt;@ThisWeekInRust&lt;/a&gt; or &lt;a href=&quot;mailto:corey@octayn.net?subject=This%20Week%20in%20Rust%20Suggestion&quot;&gt;send us an
+ email&lt;/a&gt;!
+ Want to get involved? &lt;a href=&quot;https://github.com/rust-lang/rust/blob/master/CONTRIBUTING.md&quot;&gt;We love
+ contributions&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;&lt;em&gt;This Week in Rust&lt;/em&gt; is openly developed &lt;a href=&quot;https://github.com/cmr/this-week-in-rust&quot;&gt;on GitHub&lt;/a&gt;.
+ If you find any errors in this week's issue, &lt;a href=&quot;https://github.com/cmr/this-week-in-rust/pulls&quot;&gt;please submit a PR&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;This week's edition was edited by: &lt;a href=&quot;https://github.com/nasa42&quot;&gt;nasa42&lt;/a&gt;, &lt;a href=&quot;https://github.com/brson&quot;&gt;brson&lt;/a&gt;, and &lt;a href=&quot;https://github.com/llogiq&quot;&gt;llogiq&lt;/a&gt;.&lt;/p&gt;
+ &lt;h3&gt;Updates from Rust Community&lt;/h3&gt;
+ &lt;h4&gt;News &amp;amp; Blog Posts&lt;/h4&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://gregchapple.com/contributing-to-the-rust-compiler/&quot;&gt;Guide: Contributing to the Rust compiler&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.ncameron.org/blog/a-type-safe-and-zero-allocation-library-for-reading-and-navigating-elf-files/&quot;&gt;A type-safe and zero-allocation library for reading and navigating ELF files&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;[podcast] &lt;a href=&quot;http://www.newrustacean.com/show_notes/e009/&quot;&gt;New Rustacean podcast episode 09&lt;/a&gt;. Getting into the nitty-gritty with Rust's traits.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://jadpole.github.io/arcaders/arcaders-1-12/&quot;&gt;ArcadeRS 1.12: Brawl, at last&lt;/a&gt;! Part of the series &lt;a href=&quot;https://jadpole.github.io/arcaders/arcaders-1-0/&quot;&gt;ArcadeRS 1.0: The project&lt;/a&gt; - a series whose objective is to explore the Rust programming language and ecosystem through the development of a simple, old-school shooter.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://blog.thiago.me/raspberry-pi-bare-metal-programming-with-rust/&quot;&gt;Raspberry Pi bare metal programming with Rust&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://blog.servo.org/2016/01/11/twis-47/&quot;&gt;This week in Servo 47&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.redox-os.org/news/this-week-in-redox-10/&quot;&gt;This week in Redox OS 10&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h4&gt;Notable New Crates &amp;amp; Project Updates&lt;/h4&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/ebkalderon/amethyst&quot;&gt;Amethyst&lt;/a&gt;. Data-oriented game engine written in Rust.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://www.rust-lang.org/&quot;&gt;Rust website&lt;/a&gt; has received some &lt;a href=&quot;https://www.reddit.com/r/rust/comments/40zxey/major_website_updates/&quot;&gt;major updates&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://packages.debian.org/stretch/rustc&quot;&gt;Rust&lt;/a&gt; and &lt;a href=&quot;https://packages.debian.org/stretch/cargo&quot;&gt;Cargo&lt;/a&gt; are now available in Debian stretch.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://community.particle.io/t/rust-on-particle-call-for-contributors/19090&quot;&gt;Rust on Particle: Call for contributors&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://dwrensha.github.io/capnproto-rust/2016/01/11/async-rpc.html&quot;&gt;capnp-rpc-rust rewritten to use async I/O&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/Ogeon/palette&quot;&gt;Palette&lt;/a&gt;. A Rust library for linear color calculations and conversion.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;Updates from Rust Core&lt;/h3&gt;
+ &lt;p&gt;164 pull requests were &lt;a href=&quot;https://github.com/issues?q=is%3Apr+org%3Arust-lang+is%3Amerged+merged%3A2016-01-11..2016-01-18&quot;&gt;merged in the last week&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;See the &lt;a href=&quot;https://internals.rust-lang.org/t/triage-digest-tue-jan-05-2016/3052&quot;&gt;triage digest&lt;/a&gt; and &lt;a href=&quot;https://internals.rust-lang.org/t/subteam-reports-2016-01-08/3067&quot;&gt;subteam reports&lt;/a&gt; for more details.&lt;/p&gt;
+ &lt;h4&gt;Notable changes&lt;/h4&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30943&quot;&gt;std: Stabilize APIs for the 1.7 release&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/27807&quot;&gt;Refactor and improve: Arena, TypedArena&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/29498&quot;&gt;Let &lt;code&gt;str::replace&lt;/code&gt; take a pattern&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30295&quot;&gt;rustc_resolve: Fix bug in duplicate checking for extern crates&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30426&quot;&gt;Rewrite BTreeMap to use parent pointers&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30446&quot;&gt;Support generic associated consts&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30509&quot;&gt;Add an &lt;code&gt;impl&lt;/code&gt; for &lt;code&gt;Box&amp;lt;Error&amp;gt;&lt;/code&gt; from String&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30533&quot;&gt;Introduce &quot;obligation forest&quot; data structure into fulfillment to track backtraces&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30538&quot;&gt;Remove negate_unsigned feature gate&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30567&quot;&gt;llvm: Add support for vectorcall (X86_VectorCall) convention&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30676&quot;&gt;Make coherence more tolerant of error types&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30740&quot;&gt;Add fast path for ASCII in UTF-8 validation&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30753&quot;&gt;Downgrade unit struct match via S(..) warnings to errors&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust/pull/30930&quot;&gt;Move const block checks before lowering step&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h4&gt;New Contributors&lt;/h4&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Anton Blanchard&lt;/li&gt;
+ &lt;li&gt;Jonas Tepe&lt;/li&gt;
+ &lt;li&gt;Jörg Krause&lt;/li&gt;
+ &lt;li&gt;Joshua Olson&lt;/li&gt;
+ &lt;li&gt;kalita.alexey&lt;/li&gt;
+ &lt;li&gt;Pierre Krieger&lt;/li&gt;
+ &lt;li&gt;Sergey Veselkov&lt;/li&gt;
+ &lt;li&gt;Simon Martin&lt;/li&gt;
+ &lt;li&gt;Steffen&lt;/li&gt;
+ &lt;li&gt;tomaka&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h4&gt;Approved RFCs&lt;/h4&gt;
+ &lt;p&gt;Changes to Rust follow the Rust &lt;a href=&quot;https://github.com/rust-lang/rfcs#rust-rfcs&quot;&gt;RFC (request for comments)
+ process&lt;/a&gt;. These
+ are the RFCs that were approved for implementation this week:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1331&quot;&gt;RFC 1331: &lt;code&gt;src/grammar&lt;/code&gt; for the canonical grammar of the Rust language&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h4&gt;Final Comment Period&lt;/h4&gt;
+ &lt;p&gt;Every week &lt;a href=&quot;https://rust-lang.org/team.html&quot;&gt;the team&lt;/a&gt; announces the
+ 'final comment period' for RFCs and key PRs which are reaching a
+ decision. Express your opinions now. &lt;a href=&quot;https://github.com/rust-lang/rfcs/labels/final-comment-period&quot;&gt;This week's FCPs&lt;/a&gt; are:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1462&quot;&gt;Add &lt;code&gt;[&lt;/code&gt; to the FOLLOW(ty) in macro future-proofing rules&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1457&quot;&gt;Rewrite &lt;code&gt;for&lt;/code&gt; loop desugaring to use language items&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1320&quot;&gt;Amend 1192 (RangeInclusive) to use an enum&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/243&quot;&gt;Trait-based exception handling&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1361&quot;&gt;Improve Cargo target-specific dependencies&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1129&quot;&gt;Add a &lt;code&gt;IndexAssign&lt;/code&gt; trait that allows overloading &quot;indexed assignment&quot; expressions like &lt;code&gt;a[b] = c&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1196&quot;&gt;Allow eliding more type parameters&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1296&quot;&gt;Add an &lt;code&gt;alias&lt;/code&gt; attribute to &lt;code&gt;#[link]&lt;/code&gt; and &lt;code&gt;-l&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h4&gt;New RFCs&lt;/h4&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1459&quot;&gt;Add a used attribute to prevent symbols from being discarded&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1461&quot;&gt;Move some net2 functionality into libstd&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1465&quot;&gt;Add &lt;code&gt;some!&lt;/code&gt; macro for unwrapping Option more safely&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/1467&quot;&gt;Stabilize the &lt;code&gt;volatile_load&lt;/code&gt; and &lt;code&gt;volatile_store&lt;/code&gt; intrinsics as &lt;code&gt;ptr::volatile_read&lt;/code&gt; and &lt;code&gt;ptr::volatile_write&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;Upcoming Events&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.meetup.com/Rust-Meetup-Hamburg/events/227838367/&quot;&gt;1/19. Rust Hack and Learn Hamburg @ Ponton&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.meetup.com/Rust-Bay-Area/events/227841778/&quot;&gt;1/21. SF Bay Area: Rust Concurrency and Parallelism&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://www.meetup.com/opentechschool-berlin/&quot;&gt;1/27. OpenTechSchool Berlin: Rust Hack and Learn&lt;/a&gt;.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;If you are running a Rust event please add it to the &lt;a href=&quot;https://www.google.com/calendar/embed?src=apd9vmbc22egenmtu5l6c5jbfc%40group.calendar.google.com&quot;&gt;calendar&lt;/a&gt; to get
+ it mentioned here. Email &lt;a href=&quot;mailto:erick.tryzelaar@gmail.com&quot;&gt;Erick Tryzelaar&lt;/a&gt; or &lt;a href=&quot;mailto:banderson@mozilla.com&quot;&gt;Brian
+ Anderson&lt;/a&gt; for access.&lt;/p&gt;
+ &lt;h3&gt;fn work(on: RustProject) -&amp;gt; Money&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://maidsafe.net/rust_engineer.html&quot;&gt;Rust Engineer&lt;/a&gt; at MaidSafe.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://careers.mozilla.org/en-US/position/ozy21fwU&quot;&gt;Research Engineer - Servo&lt;/a&gt; at Mozilla.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://careers.mozilla.org/en-US/position/o0H41fww&quot;&gt;Senior Research Engineer - Rust&lt;/a&gt; at Mozilla.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://plv.mpi-sws.org/rustbelt/&quot;&gt;PhD and postdoc positions&lt;/a&gt; at MPI-SWS.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;&lt;em&gt;Tweet us at &lt;a href=&quot;https://twitter.com/ThisWeekInRust&quot;&gt;@ThisWeekInRust&lt;/a&gt; to get your job offers listed here!&lt;/em&gt;&lt;/p&gt;
+ &lt;h3&gt;Crate of the Week&lt;/h3&gt;
+ &lt;p&gt;This week's Crate of the Week is &lt;a href=&quot;https://github.com/alexcrichton/toml-rs&quot;&gt;toml&lt;/a&gt;, a crate for all our configuration needs, simple yet effective.&lt;/p&gt;
+ &lt;p&gt;Thanks to &lt;a href=&quot;https://users.rust-lang.org/users/stebalien&quot;&gt;Steven Allen&lt;/a&gt; for the suggestion.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;https://users.rust-lang.org/t/crate-of-the-week/2704&quot;&gt;Submit your suggestions for next week&lt;/a&gt;!&lt;/p&gt;
+ &lt;h3&gt;Quote of the Week&lt;/h3&gt;
+ &lt;blockquote&gt;
+ &lt;p&gt;Borrow/lifetime errors are usually Rust compiler bugs.
+ Typically, I will spend 20 minutes detailing the precise conditions of
+ the bug, using language that understates my immense knowledge, while
+ demonstrating sympathetic understanding of the pressures placed on a
+ Rust compiler developer, who is also probably studying for several exams
+ at the moment. The developer reading my bug report may not understand
+ this stuff as well as I do, so I will carefully trace the lifetimes of
+ each variable, where memory is allocated on the stack vs the heap, which
+ struct or function owns a value at any point in time, where borrows
+ begin and where they... oh yeah, actually that variable really doesn't
+ live long enough.&lt;/p&gt;
+ &lt;/blockquote&gt;
+ &lt;p&gt;— &lt;a href=&quot;https://www.reddit.com/r/rust/comments/4084yx/my_trick_when_i_get_stuck_as_a_beginner/cysqz3s&quot;&gt;peterjoel on /r/rust&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;Thanks to &lt;a href=&quot;https://users.rust-lang.org/users/WaDelma&quot;&gt;Wa Delma&lt;/a&gt; for the suggestion.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;http://users.rust-lang.org/t/twir-quote-of-the-week/328&quot;&gt;Submit your quotes for next week&lt;/a&gt;!&lt;/p&gt;</description>
+ <pubDate>Mon, 18 Jan 2016 05:00:00 +0000</pubDate>
+ <dc:creator>Corey Richardson</dc:creator>
+ </item>
+ <item>
+ <title>Nikki Bee: Okay, But What Does Your Work Actually Mean, Nikki? Part 2: The Fetch Standard and Servo</title>
+ <guid isPermaLink="true">http://nikkisquared.github.io/2016/01/17/what-does-your-work-mean-part-2.html</guid>
+ <link>http://nikkisquared.github.io/2016/01/17/what-does-your-work-mean-part-2.html</link>
+ <description>&lt;p&gt;In my previous post, I started discussing in more detail what my internship entails, by talking about my first contribution to Servo. As a refresher, my first contribution was as part of my application to Outreachy, which I later revisited during my internship after a change I introduced to the HTML Standard it relied on. I’m going to expand on that last point today- specifically, how easy it is to introduce changes in &lt;a href=&quot;https://wiki.whatwg.org/wiki/FAQ#What_is_the_WHATWG.3F&quot;&gt;WHATWG&lt;/a&gt;’s various standards. I’m also going to talk about how this accessibility to changing web standards affects how I can understand it, how I can help improve it, and my work on Servo.&lt;/p&gt;
+
+ &lt;h3&gt;Two Ways To Change&lt;/h3&gt;
+
+ &lt;p&gt;There are many ways to &lt;a href=&quot;https://wiki.whatwg.org/wiki/What_you_can_do&quot;&gt;get involved with WHATWG&lt;/a&gt;, but there are two that I’ve become the most familiar with: firstly, by opening a discussion about a perceived issue and asking how it should be resolved; secondly, by taking on an issue approved as needing change and making the desired change. I’ve almost entirely only done the former, and the latter only for some minor typos. Any changes that relate directly to my work, however minor, are significant for me though! Like I discussed in my previous post, I brought attention to &lt;a href=&quot;https://github.com/whatwg/html/issues/296&quot;&gt;an inconsistency&lt;/a&gt; that was resolved, giving me a new task of updating my first contribution to Servo to reflect the change in the HTML Standard. I’ve done that several times since, for the Fetch Standard.&lt;/p&gt;
+
+ &lt;h3&gt;Understanding Fetch&lt;/h3&gt;
+
+ &lt;p&gt;My first two weeks of my internship were spent on reading through the majority of the &lt;a href=&quot;https://fetch.spec.whatwg.org/&quot;&gt;Fetch Standard&lt;/a&gt;, primarily the various Fetch functions. I took many notes describing the steps to myself, annotated with questions I had and the answers I got from either other people on the Servo team who had worked with Fetch (including my internship mentor, of course!) or people from WHATWG who were involved in the Fetch Standard. Getting so familiar with Fetch meant a few things: I would notice minor errors (such as an out of date link) that I could submit a &lt;a href=&quot;https://github.com/whatwg/fetch/pull/173&quot;&gt;simple fix for&lt;/a&gt;, or a bigger issue that I couldn’t resolve myself.&lt;/p&gt;
+
+ &lt;h3&gt;Discussions &amp;amp; Resolutions&lt;/h3&gt;
+
+ &lt;p&gt;I’m going to go into more detail about some of those bigger issues. From my perspective, when I start a discussion about a piece of documentation (such as the Fetch Standard, or reading about a programming library Servo uses), I go into it thinking “Either this documentation is incorrect, or my understanding is incorrectâ€. Whichever the answer is, it doesn’t mean that the documentation is bad, or that I’m bad at reading comprehension. I understand best by building up a model of something in my head, putting that to practice, and asking a lot of questions along the way. I learn by getting things wrong and figuring out why I was wrong, and sometimes in the process I uncover a point that could be made more clear, or an inconsistency! I have good examples of both of the different outcomes I listed, which I’ll cover over the next two sections.&lt;/p&gt;
+
+ &lt;h5&gt;Looking For The Big Picture&lt;/h5&gt;
+
+ &lt;p&gt;Early on in my initial review of the Fetch Standard’s several protocols, I found a major step that seemed to have no use. I understood that since I was learning Fetch on a step-by-step basis, I did not have a view of the bigger picture, so I asked around what I was missing that would help me understand this. One of the people I work with on implementing Fetch agreed with me that the step seemed to have no purpose, and so we decided to &lt;a href=&quot;https://github.com/whatwg/fetch/issues/174&quot;&gt;open an issue&lt;/a&gt; asking about removing it from the standard. It turned out that I had actually missed the meaning of it, as we learned. However, instead of leaving it there, I shifted the issue into asking for some explanatory notes on why this step is needed, which was fulfilled. This meant that I would have a reference to go back to should I forget the significance of the step, and that people reading the Fetch Standard in the future would be much less likely to come to the same incorrect conclusion I had.&lt;/p&gt;
+
+ &lt;h5&gt;A Confusing Order&lt;/h5&gt;
+
+ &lt;p&gt;Shortly after I had first discovered that apparent issue, I found myself struggling to comprehend a sequence of actions in another Fetch protocol. The specification seemed to say that part of an early step was meant to only be done after the final step. I unfortunately don’t remember details of the discussion I had about this- if there was a reason for why it was organized like this, I forget what it was. Regardless, it was agreed that &lt;a href=&quot;https://github.com/whatwg/fetch/issues/176&quot;&gt;moving those sub-steps&lt;/a&gt; to be actually listed after the step they’re supposed to run after would be a good change. This meant that I would need to re-organize my notes to reflect the re-arranged sequence of actions, as well as have an easier time being able to follow this part of the Fetch Standard.&lt;/p&gt;
+
+ &lt;h3&gt;A Living Standard&lt;/h3&gt;
+
+ &lt;p&gt;Like I said at the start of this post, I’m going to talk about how changes in the Fetch Standard affects my work on Servo itself. What I’ve covered so far has mostly been how changes affect my understanding of the standard itself. A key aspect in understanding the Fetch protocols is reviewing them for updates that impact me. WHATWG labels every standard they author as a “&lt;a href=&quot;https://wiki.whatwg.org/wiki/FAQ#What_does_.22Living_Standard.22_mean.3F&quot;&gt;Living Standard&lt;/a&gt;†for good reason. It was one thing for me to learn how easy it is to introduce changes, while knowing exactly what’s going on, but it’s another for me to understand that anybody else can, and often does, make changes to the Fetch Standard!&lt;/p&gt;
+
+ &lt;h5&gt;Changes Over Time&lt;/h5&gt;
+
+ &lt;p&gt;When an update is made to the Fetch Standard, it’s not so difficult to deal with as one might imagine. The Fetch Standard always notes the last day it was updated at the top of the document, I follow a Twitter account that &lt;a href=&quot;https://twitter.com/fetchstandard&quot;&gt;posts about updates&lt;/a&gt;, and all the history can be &lt;a href=&quot;https://github.com/whatwg/fetch/commits&quot;&gt;seen on GitHub&lt;/a&gt; which will show me exactly what has been changed as well as some discussion on what the change does. All of these together alert me to the fact that the Fetch Standard has been modified, and I can quickly see what was revised. If it’s relevant to what I’m going to be implementing, I update my notes to match it. Occasionally, I need to change existing code to reflect the new Standard, which is also easily done by comparing my new notes to the Fetch implementation in Servo!&lt;/p&gt;
+
+ &lt;h5&gt;Snapshots&lt;/h5&gt;
+
+ &lt;p&gt;From all of this, it might sound like the Fetch Standard is unfinished, or unreliable/inconsistent. I don’t mean to misrepresent it- the many small improvements help make the Fetch Standard, like all of WHATWG’s standards, better and more reliable. You can think of the status of the Fetch Standard at any point in time as a single, working snapshot. If somebody implemented all of Fetch as it is now, they’d have something that works by itself correctly. A different snapshot of Fetch is just that- different. It will have an improvement or two, but that doesn’t obsolete anybody who implemented it previously. It just means if they revisit the implementation, they’ll have things to update.&lt;/p&gt;
+
+ &lt;p&gt;Third post over.&lt;/p&gt;</description>
+ <pubDate>Sun, 17 Jan 2016 20:20:27 +0000</pubDate>
+ </item>
+ <item>
+ <title>Kevin Ngo: How to Write an A-Frame VR Component</title>
+ <guid isPermaLink="true">http://ngokevin.com/blog/aframe-component/</guid>
+ <link>http://ngokevin.com/blog/aframe-component/</link>
+ <description>&lt;img align=&quot;left&quot; hspace=&quot;5&quot; src=&quot;http://thevrjump.com/assets/img/articles/aframe-system/aframe-example.jpg&quot; width=&quot;320&quot; /&gt;Abstract representation of components by @rubenmueller of thevrjump.com.
+
+ &lt;p&gt;&lt;a href=&quot;http://ngokevin.com/blog/aframe&quot;&gt;A-Frame&lt;/a&gt; is a WebVR framework that introduces the
+ &lt;a href=&quot;http://ngokevin.com/blog/aframe-vs-3dml&quot;&gt;entity-component system&lt;/a&gt; (&lt;a href=&quot;http://ngokevin.com/rss/docs&quot;&gt;docs&lt;/a&gt;) to the DOM. The
+ entity-component system treats every &lt;strong&gt;entity&lt;/strong&gt; in the scene as a placeholder
+ object which we apply and mix &lt;strong&gt;components&lt;/strong&gt; to in order to add appearance,
+ behavior, and functionality. A-Frame comes with some standard components out of
+ the box like camera, geometry, material, light, or sound. However, people can
+ write, publish, and register their own components to do &lt;strong&gt;whatever&lt;/strong&gt; they want
+ like have entities &lt;a href=&quot;https://github.com/dmarcos/a-invaders/tree/master/js/components&quot;&gt;collide/explode/spawn&lt;/a&gt;, be controlled by
+ &lt;a href=&quot;https://github.com/ngokevin/aframe-physics-components&quot;&gt;physics&lt;/a&gt;, or &lt;a href=&quot;https://jsbin.com/dasefeh/edit?html,output&quot;&gt;follow a path&lt;/a&gt;. Today, we'll be going through
+ how we can write our own A-Frame components.&lt;/p&gt;
+ &lt;blockquote&gt;
+ &lt;p&gt;Note that this tutorial will be covering the upcoming release of &lt;a href=&quot;https://github.com/aframevr/aframe/blob/dev/CHANGELOG.md#dev&quot;&gt;A-Frame
+ 0.2.0&lt;/a&gt; which vastly improves the component API.&lt;/p&gt;
+ &lt;/blockquote&gt;
+ &lt;h3&gt;Table of Contents&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#what-a-component-looks-like&quot;&gt;What a Component Looks Like&lt;/a&gt;&lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#from-the-dom&quot;&gt;From the DOM&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#under-the-hood&quot;&gt;Under the Hood&lt;/a&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#defining-the-schema&quot;&gt;Defining the Schema&lt;/a&gt;&lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#property-types&quot;&gt;Property Types&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#single-property-schemas&quot;&gt;Single-Property Schemas&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#multiple-property-schemas&quot;&gt;Multiple-Property Schemas&lt;/a&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#defining-the-lifecycle-methods&quot;&gt;Defining the Lifecycle Methods&lt;/a&gt;&lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#component-init-set-up&quot;&gt;Component.init() - Set Up&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#component-update-olddata-do-the-magic&quot;&gt;Component.update(oldData) - Do the Magic&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#component-remove-tear-down&quot;&gt;Component.remove() - Tear Down&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#component-tick-time-background-behavior&quot;&gt;Component.tick() - Background Behavior&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#component-pause-and-component-play-stop-and-go&quot;&gt;Component.pause() and Component.play() - Stop and Go&lt;/a&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#boilerplate&quot;&gt;Boilerplate&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#examples&quot;&gt;Examples&lt;/a&gt;&lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#text-component&quot;&gt;Text Component&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#physics-components&quot;&gt;Physics Components&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://ngokevin.com/rss/index.xml#layout-component&quot;&gt;Layout Component&lt;/a&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;What a Component Looks Like&lt;/h3&gt;
+ &lt;p&gt;A component contains a bucket of data in the form of component properties. This
+ data is used to modify the entity. For example, we might have an &lt;em&gt;engine&lt;/em&gt;
+ component. Possible properties might be &lt;em&gt;horsepower&lt;/em&gt; or &lt;em&gt;cylinders&lt;/em&gt;.&lt;/p&gt;
+ &lt;p&gt;&lt;img alt=&quot;&quot; src=&quot;http://thevrjump.com/assets/img/articles/aframe-system/aframe-system.jpg&quot; /&gt;
+ &lt;/p&gt;&lt;div class=&quot;page-caption&quot;&gt;&lt;span&gt;
+ Abstract representation of a component by @rubenmueller of thevrjump.com.
+ &lt;/span&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;
+ &lt;h4&gt;From the DOM&lt;/h4&gt;
+ &lt;p&gt;Let's first see what a component looks like from the DOM.&lt;/p&gt;
+ &lt;p&gt;For example, the &lt;a href=&quot;https://aframe.io/docs/components/light.html&quot;&gt;light component&lt;/a&gt; has properties such as type, color,
+ and intensity. In A-Frame, we register and configure a component to an entity
+ using an HTML attribute and a style-like syntax:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;a-entity&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;light&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;type: point; color: crimson; intensity: 2.5&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;a-entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;p&gt;This would give us a light in the scene. To demonstrate composability, we could
+ give the light a spherical representation by mixing in the &lt;a href=&quot;https://aframe.io/docs/components/geometry.html&quot;&gt;geometry
+ component&lt;/a&gt;.&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;a-entity&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;geometry&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;primitive: sphere; radius: 5&quot;&lt;/span&gt;
+ &lt;span class=&quot;na&quot;&gt;light&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;type: point; color: crimson; intensity: 2.5&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;a-entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;p&gt;Or we can configure the position component to move the light sphere a bit to the right.&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;a-entity&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;geometry&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;primitive: sphere; radius: 5&quot;&lt;/span&gt;
+ &lt;span class=&quot;na&quot;&gt;light&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;type: point; color: crimson; intensity: 2.5&quot;&lt;/span&gt;
+ &lt;span class=&quot;na&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;5 0 0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;a-entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;p&gt;Given the style-like syntax and that it modifies the appearance and behavior of
+ DOM nodes, component properties can be thought of as a rough analog to CSS. In
+ the near future, I can imagine component property stylesheets.&lt;/p&gt;
+ &lt;h4&gt;Under the Hood&lt;/h4&gt;
+ &lt;p&gt;Now let's see what a component looks like &lt;strong&gt;under the hood&lt;/strong&gt;. A-Frame's most
+ basic component is the &lt;a href=&quot;https://aframe.io/docs/components/position.html&quot;&gt;position component&lt;/a&gt;:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nx&quot;&gt;AFRAME&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;registerComponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'position'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;schema&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'vec3'&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
+
+ &lt;span class=&quot;nx&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;object3D&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;el&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;object3D&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
+ &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;object3D&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;z&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;p&gt;The position component uses only a tiny subset of the component API, but what
+ this does is register the component with the name &quot;position&quot;, define a &lt;code&gt;schema&lt;/code&gt;
+ where the component's value with be parsed to an &lt;code&gt;{x, y, z}&lt;/code&gt; object, and when
+ the component initializes or the component's data updates, set the position of
+ the entity with the &lt;code&gt;update&lt;/code&gt; callback. &lt;code&gt;this.el&lt;/code&gt; is a reference from the
+ component to the DOM element, or entity, and &lt;code&gt;object3D&lt;/code&gt; is the entity's
+ &lt;a href=&quot;http://threejs.org/&quot;&gt;three.js&lt;/a&gt;. Note that A-Frame is built on top of three.js so many
+ components will be using the three.js API.&lt;/p&gt;
+ &lt;p&gt;So we see that components consist of a name and a definition, and then they can
+ be registered to A-Frame. We saw the the position component definition defined
+ a &lt;code&gt;schema&lt;/code&gt; and an &lt;code&gt;update&lt;/code&gt; handler. Components simply consist of the &lt;code&gt;schema&lt;/code&gt;,
+ which defines the shape of the data, and several handlers for the component to
+ modify the entity in reaction to different types of events.&lt;/p&gt;
+ &lt;p&gt;Here is the current list of properties and methods of a component definition:&lt;/p&gt;
+ &lt;table class=&quot;pure-table-striped&quot;&gt;
+ &lt;tbody&gt;&lt;tr&gt;
+ &lt;th&gt;Property&lt;/th&gt;
+ &lt;th&gt;Description&lt;/th&gt;
+ &lt;/tr&gt;
+ &lt;tr&gt;
+ &lt;td&gt;data&lt;/td&gt;
+ &lt;td&gt;Data of the component derived from the schema default values, mixins, and the entity's attributes.&lt;/td&gt;
+ &lt;/tr&gt;
+ &lt;tr&gt;
+ &lt;td&gt;el&lt;/td&gt;
+ &lt;td&gt;Reference to the &lt;a href=&quot;https://aframe.io/docs/core/entity.html&quot;&gt;entity&lt;/a&gt; element.&lt;/td&gt;
+ &lt;/tr&gt;
+ &lt;tr&gt;
+ &lt;td&gt;schema&lt;/td&gt;
+ &lt;td&gt;Names, types, and default values of the component property value(s)&lt;/td&gt;
+ &lt;/tr&gt;
+ &lt;/tbody&gt;&lt;/table&gt;
+
+ &lt;table class=&quot;pure-table-striped&quot;&gt;
+ &lt;tbody&gt;&lt;tr&gt;&lt;th&gt;Method&lt;/th&gt;&lt;th&gt;Description&lt;/th&gt;&lt;/tr&gt;
+ &lt;tr&gt;
+ &lt;td&gt;init&lt;/td&gt;
+ &lt;td&gt;Called once when the component is initialized.&lt;/td&gt;
+ &lt;/tr&gt;
+ &lt;tr&gt;
+ &lt;td&gt;update&lt;/td&gt;
+ &lt;td&gt;Called both when the component is initialized and whenever the component's data changes (e.g, via &lt;i&gt;setAttribute&lt;/i&gt;).&lt;/td&gt;
+ &lt;/tr&gt;
+ &lt;tr&gt;
+ &lt;td&gt;remove&lt;/td&gt;
+ &lt;td&gt;Called when the component detaches from the element (e.g., via &lt;i&gt;removeAttribute&lt;/i&gt;).&lt;/td&gt;
+ &lt;/tr&gt;
+ &lt;tr&gt;
+ &lt;td&gt;tick&lt;/td&gt;
+ &lt;td&gt;Called on each render loop or tick of the scene.&lt;/td&gt;
+ &lt;/tr&gt;
+ &lt;tr&gt;
+ &lt;td&gt;play&lt;/td&gt;
+ &lt;td&gt;Called whenever the scene or entity plays to add any background or dynamic behavior.&lt;/td&gt;
+ &lt;/tr&gt;
+ &lt;tr&gt;
+ &lt;td&gt;pause&lt;/td&gt;
+ &lt;td&gt;Called whenever the scene or entity pauses to remove any background or dynamic behavior.&lt;/td&gt;
+ &lt;/tr&gt;
+ &lt;/tbody&gt;&lt;/table&gt;
+
+ &lt;h3&gt;Defining the Schema&lt;/h3&gt;
+ &lt;p&gt;The component's schema defines what type of data it takes. A component can
+ either be single-property or consist of multiple properties. And properties
+ have &lt;em&gt;property types&lt;/em&gt;. Note that single-property schemas and property types are
+ being released in A-Frame &lt;code&gt;v0.2.0&lt;/code&gt;.&lt;/p&gt;
+ &lt;p&gt;A property might look like:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'int'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;p&gt;And a schema consisting of multiple properties might look like:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'#FFF'&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'selector'&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;uv&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'1 1'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;' '&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;parseFloat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;p&gt;Since components in the entity-component system are just buckets of data that
+ are used to affect the appearance or behavior of the entity, the schema plays a
+ crucial role in the definition of the component.&lt;/p&gt;
+ &lt;h4&gt;Property Types&lt;/h4&gt;
+ &lt;p&gt;A-Frame comes with several built-in property types such as &lt;code&gt;boolean&lt;/code&gt;, &lt;code&gt;int&lt;/code&gt;,
+ &lt;code&gt;number&lt;/code&gt;, &lt;code&gt;selector&lt;/code&gt;, &lt;code&gt;string&lt;/code&gt;, or &lt;code&gt;vec3&lt;/code&gt;. Every single property is assigned a
+ type, whether explicitly through the &lt;code&gt;type&lt;/code&gt; key or implictly via inferring the
+ value. And each type is used to assign &lt;code&gt;parse&lt;/code&gt; and &lt;code&gt;stringify&lt;/code&gt; functions. The
+ parser deserializes the incoming string value from the DOM to be put into the
+ component's data object. The stringifier is used when using &lt;code&gt;setAttribute&lt;/code&gt; to
+ serialize back to the DOM.&lt;/p&gt;
+ &lt;p&gt;We can actually define and register our own property types:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nx&quot;&gt;AFRAME&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;registerPropertyType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'radians'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+
+ &lt;span class=&quot;c1&quot;&gt;// Default stringify is .toString().&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;h4&gt;Single-Property Schemas&lt;/h4&gt;
+ &lt;p&gt;If a component has only one property, then it must either have a &lt;code&gt;type&lt;/code&gt; or a
+ &lt;code&gt;default&lt;/code&gt; value. If the type is defined, then the type is used to parse and
+ coerce the string retrieved from the DOM (e.g., &lt;code&gt;getAttribute&lt;/code&gt;). Or if the
+ default value is defined, the default value is used to infer the type.&lt;/p&gt;
+ &lt;p&gt;Take for instance the &lt;a href=&quot;https://aframe.io/docs/components/visible.html&quot;&gt;visible component&lt;/a&gt;. The schema property
+ definition implicitly defines it as a boolean:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nx&quot;&gt;AFRAME&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;registerComponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'visible'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;schema&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;c1&quot;&gt;// Type will be inferred to be boolean.&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
+
+ &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;p&gt;Or the &lt;a href=&quot;https://aframe.io/docs/components/rotation.html&quot;&gt;rotation component&lt;/a&gt; which explicitly defines the value as a &lt;code&gt;vec3&lt;/code&gt;:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nx&quot;&gt;AFRAME&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;registerComponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'rotation'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;schema&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;c1&quot;&gt;// Default value will be 0, 0, 0 as defined by the vec3 property type.&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'vec3'&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+
+ &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;p&gt;Using these defined property types, schemas are processed by
+ &lt;code&gt;registerComponent&lt;/code&gt; to inject default values, parsers, and stringifiers for
+ each property. So if a default value is not defined, the default value will be
+ whatever the property type defines as the &quot;default default value&quot;.&lt;/p&gt;
+ &lt;h4&gt;Multiple-Property Schemas&lt;/h4&gt;
+ &lt;p&gt;If a component has multiple properties (or one named property), then it consists of
+ one or more property definitions, in the form described above, in an object keyed by
+ property name. For instance, a physics body component might define a schema:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nx&quot;&gt;AFRAME&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;registerComponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'physics-body'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;schema&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;boundingBox&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'vec3'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;z&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;mass&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;velocity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'vec3'&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;p&gt;Having multiple properties is what makes the component take the syntax in the
+ form of &lt;code&gt;physics=&quot;mass: 2; velocity: 1 1 1&quot;&lt;/code&gt;.&lt;/p&gt;
+ &lt;p&gt;With the schema defined, all data coming into the component will be passed
+ through the schema for parsing. Then in the lifecycle methods, the component
+ has access to &lt;code&gt;this.data&lt;/code&gt; which in a single-property schema is a value and in a
+ multiple-propery schema is an object.&lt;/p&gt;
+ &lt;h3&gt;Defining the Lifecycle Methods&lt;/h3&gt;
+ &lt;h4&gt;Component.init() - Set Up&lt;/h4&gt;
+ &lt;p&gt;&lt;code&gt;init&lt;/code&gt; is called once in the component's lifecycle when it is mounted to the
+ entity. &lt;code&gt;init&lt;/code&gt; is generally used to set up variables or members that may used
+ throughout the component or to set up state. Though not every component will
+ need to define an &lt;code&gt;init&lt;/code&gt; handler. Sort of like the component-equivalent method
+ to &lt;code&gt;createdCallback&lt;/code&gt; or &lt;code&gt;React.ComponentDidMount&lt;/code&gt;.&lt;/p&gt;
+ &lt;p&gt;For example, the &lt;code&gt;look-at&lt;/code&gt; component's &lt;code&gt;init&lt;/code&gt; handler sets up some variables:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nx&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;target3D&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;vector&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;THREE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Vector3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
+
+ &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;h4&gt;Component.update(oldData) - Do the Magic&lt;/h4&gt;
+ &lt;p&gt;The &lt;code&gt;update&lt;/code&gt; handler is called both at the beginning of the component's
+ lifecycle with the initial &lt;code&gt;this.data&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; every time the component's data
+ changes (generally during the entity's &lt;code&gt;attributeChangedCallback&lt;/code&gt; like with a
+ &lt;code&gt;setAttribute&lt;/code&gt;). The update handler gets access to the previous state of the
+ component data passed in through &lt;code&gt;oldData&lt;/code&gt;. The previous state of the component
+ can be used to tell exactly which properties changed to do more granular
+ updates.&lt;/p&gt;
+ &lt;p&gt;The update handler uses &lt;code&gt;this.data&lt;/code&gt; to modify the entity, usually interacting
+ with three.js APIs. One of the simplest update handlers is the
+ &lt;a href=&quot;https://aframe.io/docs/components/visible.html&quot;&gt;visible&lt;/a&gt; component's:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nx&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;el&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;object3D&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;visible&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;p&gt;A slightly more complex update handler might be the &lt;a href=&quot;https://aframe.io/docs/components/light.html&quot;&gt;light&lt;/a&gt; component's,
+ which we'll show via abbreviated code:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nx&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;oldData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;diffData&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;diff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;oldData&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{});&lt;/span&gt;
+
+ &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;light&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'type'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;diffData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;c1&quot;&gt;// If there is an existing light and the type hasn't changed, update light.&lt;/span&gt;
+ &lt;span class=&quot;nb&quot;&gt;Object&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;keys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;diffData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;nx&quot;&gt;light&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;diffData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;c1&quot;&gt;// No light exists yet or the type of light has changed, create a new light.&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;light&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getLight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
+
+ &lt;span class=&quot;c1&quot;&gt;// Register the object3D of type `light` to the entity.&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;el&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setObject3D&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'light'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;light&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;p&gt;The entity's &lt;code&gt;object3D&lt;/code&gt; is a plain THREE.Object3D. Other three.js object types
+ such as meshes, lights, and cameras can be set with &lt;code&gt;setObject3D&lt;/code&gt; where they
+ will be appeneded to the entity's &lt;code&gt;object3D&lt;/code&gt;.&lt;/p&gt;
+ &lt;h4&gt;Component.remove() - Tear Down&lt;/h4&gt;
+ &lt;p&gt;The &lt;code&gt;remove&lt;/code&gt; handler is called when the component detaches from the entity such
+ as with &lt;code&gt;removeAttribute&lt;/code&gt;. This is generally used to remove all modifications,
+ listeners, and behaviors to the entity that the component added.&lt;/p&gt;
+ &lt;p&gt;For example, when the &lt;a href=&quot;https://aframe.io/docs/components/light.html&quot;&gt;light component&lt;/a&gt; detaches, it removes the light
+ it previously attached from the entity and thus the scene:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nx&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;el&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;removeObject3D&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'light'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;h4&gt;Component.tick(time) - Background Behavior&lt;/h4&gt;
+ &lt;p&gt;The &lt;code&gt;tick&lt;/code&gt; handler is called on every single tick or render loop of the scene.
+ So expect it to run on the order of 60-120 times for second. The global uptime of
+ the scene in seconds is passed into the tick handler.&lt;/p&gt;
+ &lt;p&gt;For example, the &lt;a href=&quot;https://aframe.io/docs/components/look-at.html&quot;&gt;look-at&lt;/a&gt; component, which instructs an entity to
+ look at another target entity, uses the tick handler to update the rotation in
+ case the target entity changes its position:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nx&quot;&gt;tick&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;c1&quot;&gt;// target3D and vector are set from the update handler.&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;target3D&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;el&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;object3D&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lookAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;vector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setFromMatrixPosition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;target3D&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;matrixWorld&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;h4&gt;Component.pause() and Component.play() - Stop and Go&lt;/h4&gt;
+ &lt;p&gt;To support pause and play, just as with a video game or to toggle entities for
+ performance, components can implement &lt;code&gt;play&lt;/code&gt; and &lt;code&gt;pause&lt;/code&gt; handlers. These are
+ invoked when the component's entity runs its &lt;code&gt;play&lt;/code&gt; or &lt;code&gt;pause&lt;/code&gt; method. When an
+ entity plays or pauses, all of its child entities are also played or paused.&lt;/p&gt;
+ &lt;p&gt;Components should implement play or pause handlers if they register any
+ dynamic, asynchronous, or background behavior such as animations, event
+ listeners, or tick handlers.&lt;/p&gt;
+ &lt;p&gt;For example, the &lt;code&gt;look-controls&lt;/code&gt; component simply removes its event listeners
+ such that the camera does not move when the scene is paused, and it adds its
+ event listeners when the scene starts playing or is resumed:&lt;/p&gt;
+ &lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nx&quot;&gt;pause&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;removeEventListeners&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
+
+ &lt;span class=&quot;nx&quot;&gt;play&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
+ &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addEventListeners&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+
+
+ &lt;h3&gt;Boilerplate&lt;/h3&gt;
+ &lt;p&gt;I suggest that people start off with my &lt;a href=&quot;https://github.com/ngokevin/aframe-component-boilerplate&quot;&gt;component boilerplate&lt;/a&gt;,
+ even hardcore tool junkies. This will get you straight into building a
+ component and comes with everything you will need to publish your component
+ into the wild. The boilerplate handles creating a stubbed component, build
+ steps for both NPM and browser distribution files, and publishing to Github
+ Pages.&lt;/p&gt;
+ &lt;p&gt;Generally with boilerplates, it is better to start from scratch and build your
+ own boilerplate, but the A-Frame component boilerplate contains a lot of tribal
+ inside knowledge about A-Frame and is updated frequently to reflect new things
+ landing on A-Frame. The only possibly opinionated pieces about the boilerplate
+ is the development tools it internally uses that are hidden away by NPM
+ scripts.&lt;/p&gt;
+ &lt;h3&gt;Examples&lt;/h3&gt;
+ &lt;p&gt;Under construction. Stay tuned!&lt;/p&gt;
+ &lt;h4&gt;Text Component&lt;/h4&gt;
+ &lt;p&gt;&lt;a href=&quot;https://github.com/ngokevin/aframe-text-component&quot;&gt;Text component&lt;/a&gt;&lt;/p&gt;
+ &lt;h4&gt;Physics Components&lt;/h4&gt;
+ &lt;p&gt;&lt;a href=&quot;https://github.com/ngokevin/aframe-physics-components&quot;&gt;Physics components&lt;/a&gt;&lt;/p&gt;
+ &lt;h4&gt;Layout Component&lt;/h4&gt;
+ &lt;p&gt;&lt;a href=&quot;https://github.com/ngokevin/aframe-layout-component&quot;&gt;Layout component&lt;/a&gt;&lt;/p&gt;</description>
+ <pubDate>Sun, 17 Jan 2016 00:00:00 +0000</pubDate>
+ </item>
+ <item>
+ <title>Gervase Markham: Convenient… and Creepy</title>
+ <guid isPermaLink="false">http://blog.gerv.net/?p=3527</guid>
+ <link>http://feedproxy.google.com/~r/HackingForChrist/~3/DN054t04_dE/</link>
+ <description>&lt;p&gt;The last Mozilla All-Hands was at one of the hotels in the Walt Disney World Resort in Florida. Every attendee was issued with one of these (although their use was optional):&lt;br /&gt;
+ &lt;a href=&quot;http://blog.gerv.net/files/2016/01/Disneys_MagicBand.jpg&quot;&gt;&lt;img class=&quot;alignnone size-large wp-image-3530&quot; src=&quot;http://blog.gerv.net/files/2016/01/Disneys_MagicBand-1024x832.jpg&quot; width=&quot;292&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
+ &lt;p&gt;It’s called a “Magic Bandâ€. You register it online and connect it to your Disney account, and then it can be used for park entry, entry to pre-booked rides so you don’t have to queue (called “FastPass+â€), payment, picking up photos, as your room key, and all sorts of other convenient features. Note that it has no UI whatsoever – no lights, no buttons. Not even a battery compartment. (It does contain a battery, but it’s not replaceable.) These are specific design decisions – the aim is for ultra-simple convenience.&lt;/p&gt;
+ &lt;p&gt;One of the talks we had at the All Hands was from one of the Magic Band team. The audience reactions to some of the things he said was really interesting. He gave the example of Cinderella wishing you a Happy Birthday as you walk round the park. “Cinderella just knowsâ€, he said. Of course, in fact, her costume’s tech prompts her when it silently reads your Magic Band from a distance. This got some initial impressed applause, but it was noticeable that after a few moments, it wavered – people were thinking “Cool… er, but creepy?â€&lt;/p&gt;
+ &lt;p&gt;The Magic Band also has range sufficient that Disney can track you around the park. This enables some features which are good for both customers and Disney – for example, they can use it for load balancing. If one area of the park seems to be getting overcrowded, have some characters pop up in a neighbouring area to try and draw people away. But it means that they always know where you are and where you’ve been.&lt;/p&gt;
+ &lt;p&gt;My take-away from learning about the Magic Band is that it’s really hard to have a technical solution to this kind of requirement which allows all the Convenient features but not the Creepy features. Disney does offer an RFID-card-based solution for the privacy-conscious which does some of these things, but not all of them. And it’s easier to lose. It seems to me that the only way to distinguish the two types of feature, and get one and not the other, is policy – either the policy of the organization, or external restrictions on them (e.g. from a watchdog body’s code of conduct they sign up to, or from law). And it’s often not in the organization’s interest to limit themselves in this way.&lt;/p&gt;
+ &lt;img alt=&quot;&quot; height=&quot;1&quot; src=&quot;http://feeds.feedburner.com/~r/HackingForChrist/~4/DN054t04_dE&quot; width=&quot;1&quot; /&gt;</description>
+ <pubDate>Sat, 16 Jan 2016 12:18:38 +0000</pubDate>
+ <dc:creator>gerv</dc:creator>
+ </item>
+ <item>
+ <title>Christian Heilmann: Don’t tell me what my browser can’t do!</title>
+ <guid isPermaLink="false">https://www.christianheilmann.com/?p=4957</guid>
+ <link>https://www.christianheilmann.com/2016/01/16/dont-tell-me-what-my-browser-cant-do/</link>
+ <description>&lt;p&gt;&lt;em class=&quot;markup--em markup--p-em&quot;&gt;Chances are, your guess is wrong!&lt;/em&gt;&lt;/p&gt;
+
+ &lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt=&quot;you are obviously in the wrong place&quot; src=&quot;https://d262ilb51hltx0.cloudfront.net/max/800/1*l9jPbOyAl00kjPhyNYA-IQ.jpeg&quot; width=&quot;100%&quot; /&gt;Arrogance towards possible customers never pays out – as shown in “Pretty Womanâ€&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;
+
+ &lt;p&gt;There is nothing more frustrating than being capable of something and not getting a chance to do it. The same goes for being blocked out from something although you are capable of consuming it. Or you’re even willing to put some extra effort or even money in and you still don’t get to consume it.&lt;/p&gt;
+
+ &lt;p&gt;For example, I’d happily pay $50 a month to get access to Netflix’s world-wide library from any country I’m in. But the companies Netflix get their content from won’t go for that. Movies and TV show are budgeted by predicted revenue in different geographical markets with month-long breaks in between the releases. A world-wide network capable of delivering content in real time? Preposterous — let’s shut that down.&lt;/p&gt;
+
+ &lt;p&gt;On a less “let’s break a 100 year old monopoly†scale of annoyance, &lt;a href=&quot;https://twitter.com/codepo8/status/687616620529844224&quot;&gt;I tweeted yesterday something glib and apparently cruel&lt;/a&gt;:&lt;/p&gt;
+
+ &lt;p&gt;&lt;/p&gt;&lt;blockquote&gt;“Sorry, but your browser does not support WebGL!†– sorry, you are a shit coder.&lt;/blockquote&gt;&lt;p&gt;&lt;/p&gt;
+
+ &lt;p&gt;&lt;strong&gt;And I stand by this&lt;/strong&gt;. I went to a web site that promised me some cute, pointless animation and technological demo. I was using Firefox Nightly — a WebGL capable browser. I also went there with Microsoft Edge — another WebGL capable browser. Finally, using Chrome, I was able to delight in seeing an animation.&lt;/p&gt;
+
+ &lt;p&gt;&lt;strong&gt;I’m not saying the creators of that thing lack in development capabilities&lt;/strong&gt;. The demo was slick, beautiful and well coded. They still do lack in two things developers of &lt;em&gt;web products &lt;/em&gt;(and I count apps into that) should have: empathy for the end user and an understanding that they are not in control.&lt;/p&gt;
+
+ &lt;p&gt;Now, I am a pretty capable technical person. When you tell me that I might be lacking WebGL, I know what you mean. I don’t lack WebGL. I was blocked out because the web site did browser sniffing instead of capability testing. But I know what could be the problem.&lt;/p&gt;
+
+ &lt;p&gt;A normal user of the web has no idea what WebGL is and — if you’re lucky — will try to find it on an app store. If you’re not lucky all you did is confuse a person. A person who went through the effort to click a link, open a browser and wait for your thing to load. A person that feels stupid for using your product as they have no clue what WebGL is and won’t ask. Humans hate feeling stupid and we do anything not to appear it or show it.&lt;/p&gt;
+
+ &lt;p&gt;This is what I mean by empathy for the end user. Our problems should never become theirs.&lt;/p&gt;
+
+ &lt;p&gt;&lt;/p&gt;&lt;blockquote&gt;A cryptic error message telling the user that they lack some technology helps nobody and is sloppy development at best, sheer arrogance at worst.&lt;/blockquote&gt;&lt;p&gt;&lt;/p&gt;
+
+ &lt;p&gt;The web is, sadly enough, littered with unhelpful error messages and assumptions that it is the user’s fault when they can’t consume the thing we built.&lt;/p&gt;
+
+ &lt;p&gt;Here’s a reality check — this is what our users should have to do to consume the things we build:&lt;/p&gt;
+
+ &lt;p&gt;&lt;img alt=&quot;&quot; height=&quot;600&quot; src=&quot;https://d262ilb51hltx0.cloudfront.net/max/800/1*DXtRIWTu-UzRb0YB-h8SmA.png&quot; width=&quot;10&quot; /&gt;&lt;/p&gt;
+
+ &lt;p&gt;&lt;strong&gt;That’s right. Nothing&lt;/strong&gt;. This is the web. Everybody is invited to consume, contribute and create. This is what made it the success it is. This is what will make it outlive whatever other platform threatens it with shiny impressive interactions. Interactions at that time impossible to achieve with web technologies.&lt;/p&gt;
+
+ &lt;p&gt;Whenever I mention this, the knee-jerk reaction is the same:&lt;/p&gt;
+
+ &lt;p&gt;&lt;/p&gt;&lt;blockquote class=&quot;graf--blockquote graf-after--p&quot; id=&quot;79d6&quot; name=&quot;79d6&quot;&gt;How can you expect us to build delightful experiences close to magic (and whatever other soundbites were in the last Apple keynote) if we keep having to support old browsers and users with terrible setups?&lt;/blockquote&gt;&lt;p&gt;&lt;/p&gt;
+
+ &lt;p&gt;You don’t have to support old browsers and terrible setups. But you are not allowed to block them out. It is a simple matter of giving a usable interface to end users. A button that does nothing when you click it is not a good experience. Test if the functionality is available, then create or show the button. &lt;strong class=&quot;markup--strong markup--p-strong&quot;&gt;This is as simple as it is.&lt;/strong&gt;&lt;/p&gt;
+
+ &lt;p&gt;If you really have to rely on some technology then show people what they are missing out on and tell them how to upgrade. A screenshot or a video of a WebGL animation is still lovely to see. A message telling me I have no WebGL less so.&lt;/p&gt;
+
+ &lt;p&gt;Even more on the black and white scale, what the discussion boils down to is in essence:&lt;/p&gt;
+
+ &lt;p&gt;&lt;/p&gt;&lt;blockquote class=&quot;graf--blockquote graf-after--p&quot; id=&quot;a775&quot; name=&quot;a775&quot;&gt;But it is 2016 — surely we can expect people to have JavaScript enabled — it is after all “the assembly language of the webâ€&lt;/blockquote&gt;&lt;p&gt;&lt;/p&gt;
+
+ &lt;p&gt;Despite the cringe-worthy &lt;a href=&quot;http://www.hanselman.com/blog/JavaScriptIsAssemblyLanguageForTheWebSematicMarkupIsDeadCleanVsMachinecodedHTML.aspx&quot;&gt;misquote of the assembly language&lt;/a&gt; thing, here is a harsh truth:&lt;/p&gt;
+
+ &lt;p&gt;&lt;/p&gt;&lt;blockquote&gt;You can absolutely expect JavaScript to be available on your end users computers in 2016. At the same time it is painfully &lt;strong&gt;naive&lt;/strong&gt; to expect it to work under all circumstances.&lt;/blockquote&gt;&lt;p&gt;&lt;/p&gt;
+
+ &lt;p&gt;&lt;strong&gt;JavaScript is brittle&lt;/strong&gt;. &lt;span class=&quot;caps&quot;&gt;HTML&lt;/span&gt; and &lt;span class=&quot;caps&quot;&gt;CSS&lt;/span&gt; both are &lt;em&gt;fault tolerant&lt;/em&gt;. If something goes wrong in &lt;span class=&quot;caps&quot;&gt;HTML&lt;/span&gt;, browsers either display the content of the element or try to fix minor issues like unclosed elements for you. &lt;span class=&quot;caps&quot;&gt;CSS&lt;/span&gt; skips lines of code it can’t understand and merrily goes on its way to show the rest of it. JavaScript breaks on errors and tells you that something went wrong. It will not execute the rest of the script, but throws in the towel and tells you to get your house in order first.&lt;/p&gt;
+
+ &lt;p&gt;There &lt;a href=&quot;http://kryogenix.org/code/browser/everyonehasjs.html&quot;&gt;are many outside influences&lt;/a&gt; that will interfere with the execution of your JavaScript. That’s why a non-naive and non-arrogant — a dedicated and seasoned web developer — will never rely on it. Instead, you treat it as an enhancement and in an almost paranoid fashion test for the availability of everything before you access it.&lt;/p&gt;
+
+ &lt;p&gt;&lt;strong&gt;Sorry (not sorry) — this will never go away&lt;/strong&gt;. This is the nature of JavaScript. And it is a good thing. It means we can access new features of the language as they come along instead of getting stuck in a certain state. It means we have to think about using it every time instead of relying on libraries to do the work for us. It means that we need to keep evolving with the web — a living and constantly changing medium, and not a software platform. That’s just part of it.&lt;/p&gt;
+
+ &lt;p&gt;This is why the whole discussion about JavaScript enabled or disabled is a massive waste of time. It is not the availability of JavaScript we need to worry about. It is our products breaking in perfectly capable environments because we rely on perfect execution instead of writing defensive code. A tumblr like &lt;a class=&quot;markup--anchor markup--p-anchor&quot; href=&quot;http://sighjavascript.tumblr.com/&quot; rel=&quot;nofollow&quot;&gt;Sigh, JavaScript&lt;/a&gt; is fun, but is pithy finger-pointing.&lt;/p&gt;
+
+ &lt;p&gt;&lt;/p&gt;&lt;blockquote&gt;There is nothing wrong with using JavaScript to build things. Just be aware that the error handling is your responsibility.&lt;/blockquote&gt;&lt;p&gt;&lt;/p&gt;
+
+ &lt;p&gt;Any message telling the user that they have to turn on JavaScript to use a certain product is a proof that you care more about your developer convenience than your users.&lt;/p&gt;
+
+ &lt;p&gt;&lt;/p&gt;&lt;blockquote&gt;It is damn hard these days to turn off JavaScript – you are complaining about a almost non-existent issue and tell the confused user to do something they don’t know how to.&lt;/blockquote&gt;&lt;p&gt;&lt;/p&gt;
+
+ &lt;p&gt;The chance that something in the JavaScript execution of any of your dozens of dependencies went wrong is much higher – and this is your job to fix. This is why advice like &lt;a href=&quot;http://webdesign.tutsplus.com/tutorials/quick-tip-dont-forget-the-noscript-element--cms-25498&quot;&gt;using noscript to provide alternative content&lt;/a&gt; is terrible. It means you double your workload instead of enhancing what works. Who knows? If you start with something not JavaScript dependent (or running it server side) you might find that you don’t need the complex solution you started with in the first place. Faster, smaller, easier. Sounds good, right?&lt;/p&gt;
+
+ &lt;p&gt;So, please, stop sniffing my browser, you will fail and tell me lies. Stop pretending that working with a brittle technology is the user’s fault when something goes wrong.&lt;/p&gt;
+
+ &lt;p&gt;&lt;/p&gt;&lt;blockquote&gt;As web developers we work in the service industry. We deliver products to people. And keeping these people happy and non-worried is our job. Nothing more, nothing less.&lt;/blockquote&gt;&lt;p&gt;&lt;/p&gt;
+
+ &lt;p&gt;Without users, your product is nothing. Sure, we are better paid and well educated and we are not flipping burgers. But we have no right whatsoever to be arrogant and not understanding that our mistakes are not the fault of our end users.&lt;/p&gt;
+
+ &lt;p&gt;Our demeanor when complaining about how stupid our end users and their terrible setups are reminds me of &lt;a href=&quot;https://www.youtube.com/watch?v=CSj5stmFkQ0&quot;&gt;this Mitchell and Webb sketch&lt;/a&gt;.&lt;/p&gt;
+
+ &lt;p&gt;&lt;/p&gt;
+
+ &lt;p&gt;&lt;strong class=&quot;markup--strong markup--p-strong&quot;&gt;Don’t be that person. &lt;/strong&gt;Our job is to enable people to consume, participate and create the web. This is magic. This is beautiful. This is incredibly rewarding. The next markets we should care about are ready to be as excited about the web as we were when we first encountered it. Browsers are good these days. Use what they offer after testing for it and enjoy what you can achieve. Don’t tell the user when things go wrong – they can not fix what you messed up.&lt;/p&gt;
+
+
+ &lt;img alt=&quot;&quot; height=&quot;1&quot; src=&quot;http://feeds.feedburner.com/~r/chrisheilmann/~4/vqtqgcNQXy8&quot; width=&quot;1&quot; /&gt;</description>
+ <pubDate>Sat, 16 Jan 2016 11:28:10 +0000</pubDate>
+ <dc:creator>Chris Heilmann</dc:creator>
+ </item>
+ <item>
+ <title>Mike Hommey: Announcing git-cinnabar 0.3.1</title>
+ <guid isPermaLink="true">http://glandium.org/blog/?p=3510</guid>
+ <link>http://glandium.org/blog/?p=3510</link>
+ <description>&lt;p&gt;This is a brown paper bag release. It turns out I managed to break the upgrade&lt;br /&gt;
+ path only 10 commits before the release.&lt;/p&gt;
+ &lt;h3&gt;What’s new since 0.3.0?&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;code&gt;git cinnabar fsck&lt;/code&gt; doesn’t fail to upgrade metadata.&lt;/li&gt;
+ &lt;li&gt;The &lt;code&gt;remote.$remote.cinnabar-draft&lt;/code&gt; config works again.&lt;/li&gt;
+ &lt;li&gt;Don’t fail to clone an empty repository.&lt;/li&gt;
+ &lt;li&gt;Allow to specify mercurial configuration items in a .git/hgrc file.&lt;/li&gt;
+ &lt;/ul&gt;</description>
+ <pubDate>Sat, 16 Jan 2016 11:26:45 +0000</pubDate>
+ <dc:creator>glandium</dc:creator>
+ </item>
+ <item>
+ <title>Emily Dunham: Buildbot and EOFError</title>
+ <guid isPermaLink="true">http://edunham.net/2016/01/16/buildbot_and_eoferror.html</guid>
+ <link>http://edunham.net/2016/01/16/buildbot_and_eoferror.html</link>
+ <description>&lt;h3&gt;Buildbot and EOFError&lt;/h3&gt;
+ &lt;p&gt;More SEO-bait, after tracking down an poorly documented problem:&lt;/p&gt;
+ &lt;div class=&quot;highlight-python&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;# buildbot start master
+ Following twistd.log until startup finished..
+ 2016-01-17 04:35:49+0000 [-] Log opened.
+ 2016-01-17 04:35:49+0000 [-] twistd 14.0.2 (/usr/bin/python 2.7.6) starting up.
+ 2016-01-17 04:35:49+0000 [-] reactor class: twisted.internet.epollreactor.EPollReactor.
+ 2016-01-17 04:35:49+0000 [-] Starting BuildMaster -- buildbot.version: 0.8.12
+ 2016-01-17 04:35:49+0000 [-] Loading configuration from '/home/user/buildbot/master/master.cfg'
+ 2016-01-17 04:35:53+0000 [-] error while parsing config file:
+ Traceback (most recent call last):
+ File &quot;/usr/local/lib/python2.7/dist-packages/twisted/internet/defer.py&quot;, line 577, in _runCallbacks
+ current.result = callback(current.result, *args, **kw)
+ File &quot;/usr/local/lib/python2.7/dist-packages/twisted/internet/defer.py&quot;, line 1155, in gotResult
+ _inlineCallbacks(r, g, deferred)
+ File &quot;/usr/local/lib/python2.7/dist-packages/twisted/internet/defer.py&quot;, line 1099, in _inlineCallbacks
+ result = g.send(result)
+ File &quot;/usr/local/lib/python2.7/dist-packages/buildbot/master.py&quot;, line 189, in startService
+ self.configFileName)
+ --- &amp;lt;exception caught here&amp;gt; ---
+ File &quot;/usr/local/lib/python2.7/dist-packages/buildbot/config.py&quot;, line 156, in loadConfig
+ exec f in localDict
+ File &quot;/home/user/buildbot/master/master.cfg&quot;, line 415, in &amp;lt;module&amp;gt;
+ extra_post_params={'secret': HOMU_BUILDBOT_SECRET},
+ File &quot;/usr/local/lib/python2.7/dist-packages/buildbot/status/status_push.py&quot;, line 404, in __init__
+ secondaryQueue=DiskQueue(path, maxItems=maxDiskItems))
+ File &quot;/usr/local/lib/python2.7/dist-packages/buildbot/status/persistent_queue.py&quot;, line 286, in __init__
+ self.secondaryQueue.popChunk(self.primaryQueue.maxItems()))
+ File &quot;/usr/local/lib/python2.7/dist-packages/buildbot/status/persistent_queue.py&quot;, line 208, in popChunk
+ ret.append(self.unpickleFn(ReadFile(path)))
+ exceptions.EOFError:
+
+ 2016-01-17 04:35:53+0000 [-] Configuration Errors:
+ 2016-01-17 04:35:53+0000 [-] error while parsing config file: (traceback in logfile)
+ 2016-01-17 04:35:53+0000 [-] Halting master.
+ 2016-01-17 04:35:53+0000 [-] Main loop terminated.
+ 2016-01-17 04:35:53+0000 [-] Server Shut Down.
+ &lt;/pre&gt;&lt;/div&gt;
+ &lt;/div&gt;
+ &lt;p&gt;This happened after the buildmaster’s disk filled up and a bunch of stuff was
+ manually deleted. There were no changes to master.cfg since it worked
+ perfectly.&lt;/p&gt;
+ &lt;p&gt;The fix was to examine &lt;span class=&quot;docutils literal&quot;&gt;&lt;span class=&quot;pre&quot;&gt;master.cfg&lt;/span&gt;&lt;/span&gt; to see &lt;a class=&quot;reference external&quot; href=&quot;https://github.com/servo/saltfs/blob/master/buildbot/master/master.cfg#L413&quot;&gt;where the HttpStatusPush was
+ created&lt;/a&gt;,
+ of the form:&lt;/p&gt;
+ &lt;div class=&quot;highlight-python&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'status'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;HttpStatusPush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
+ &lt;span class=&quot;n&quot;&gt;serverUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'http://build.servo.org:54856/buildbot'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
+ &lt;span class=&quot;n&quot;&gt;extra_post_params&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'secret'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HOMU_BUILDBOT_SECRET&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+ &lt;/div&gt;
+ &lt;p&gt;Digging in the Buildbot source reveals that &lt;span class=&quot;docutils literal&quot;&gt;&lt;span class=&quot;pre&quot;&gt;persistent_queue.py&lt;/span&gt;&lt;/span&gt; wants to
+ unpickle a cache file from &lt;span class=&quot;docutils literal&quot;&gt;&lt;span class=&quot;pre&quot;&gt;/events_build.servo.org/-1&lt;/span&gt;&lt;/span&gt; if there was nothing
+ in &lt;span class=&quot;docutils literal&quot;&gt;&lt;span class=&quot;pre&quot;&gt;/events_build.servo.org/&lt;/span&gt;&lt;/span&gt;. To fix this the right way, create that file
+ and make sure Buildbot has &lt;span class=&quot;docutils literal&quot;&gt;&lt;span class=&quot;pre&quot;&gt;+rwx&lt;/span&gt;&lt;/span&gt; on it.&lt;/p&gt;
+ &lt;p&gt;Alternately, you can give up on writing your status push cache to disk
+ entirely by adding the line &lt;span class=&quot;docutils literal&quot;&gt;&lt;span class=&quot;pre&quot;&gt;maxDiskItems=0&lt;/span&gt;&lt;/span&gt; to the creation of the
+ HttpStatusPush, giving you:&lt;/p&gt;
+ &lt;div class=&quot;highlight-python&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'status'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;HttpStatusPush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
+ &lt;span class=&quot;n&quot;&gt;serverUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'http://build.servo.org:54856/buildbot'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
+ &lt;span class=&quot;n&quot;&gt;maxDiskItems&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
+ &lt;span class=&quot;n&quot;&gt;extra_post_params&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'secret'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HOMU_BUILDBOT_SECRET&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
+ &lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
+ &lt;/pre&gt;&lt;/div&gt;
+ &lt;/div&gt;
+ &lt;p&gt;The real moral of the story is “remember to use &lt;a class=&quot;reference external&quot; href=&quot;http://www.linuxcommand.org/man_pages/logrotate8.html&quot;&gt;logrotate&lt;/a&gt;.&lt;/p&gt;</description>
+ <pubDate>Sat, 16 Jan 2016 08:00:00 +0000</pubDate>
+ </item>
+ <item>
+ <title>Daniel Glazman: Ebook pagination and CSS</title>
+ <guid isPermaLink="false">urn:md5:41d039bb28fb15c761578cba0b1454fa</guid>
+ <link>http://www.glazman.org/weblog/dotclear/index.php?post/2016/01/16/Ebook-pagination-and-CSS</link>
+ <description>&lt;p&gt;Let's suppose you have a rather long document, for instance a book chapter, and you want to render it in your browser &lt;em&gt;à la&lt;/em&gt; iBooks/Kindle. That's rather easy with just a dash of CSS:&lt;/p&gt;
+ &lt;pre&gt;body {
+ height: calc(100vh - 24px);
+ column-width: 45vw;
+ overflow: hidden;
+ margin-left: calc(-50vw * attr(currentpage integer));
+ }&lt;/pre&gt;
+ &lt;p&gt;Yes, yes, I know that no browser implements that &lt;code&gt;attr()&lt;/code&gt;extended syntax. So put an inline style on your body for &lt;code&gt;margin-left: calc(-50vw * &lt;em&gt;&amp;lt;n&amp;gt;&lt;/em&gt;)&lt;/code&gt; where &lt;em&gt;&lt;code&gt;&amp;lt;n&amp;gt;&lt;/code&gt;&lt;/em&gt; is the page number you want minus 1.&lt;/p&gt;
+ &lt;p&gt;Then add the fixed positioned controls you need to let user change page, plus gesture detection. Add a transition on margin-left to make it nicer. Done. Works perfectly in Firefox, Safari, Chrome and Opera. I don't have a Windows box handy so I can't test on Edge.&lt;/p&gt;</description>
+ <pubDate>Sat, 16 Jan 2016 03:43:00 +0000</pubDate>
+ <dc:creator>glazou</dc:creator>
+ </item>
+ <item>
+ <title>Nicolas Mandil: Mozilla cultural revolution: from ‘radical participation’ to ‘radical user-centric’</title>
+ <guid isPermaLink="false">https://repeer.org/?p=48</guid>
+ <link>https://repeer.org/2016/01/16/mozilla-cultural-revolution-from-radical-participation-to-radical-user-centric/</link>
+ <description>&lt;p&gt;This post has been written about the &lt;a href=&quot;http://marksurman.commons.ca/2015/12/21/mofo2020/&quot;&gt;Mozilla Foundation (MoFo) 2020 strategy&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;The ideas developed in this post are in different levels: some are global, some focus on particular points of the proposed draft. But in my point of view, they all carry a transversal meaning: articulation (as piece connected to a structure allowing movement) with others and consistency with our mission.&lt;/p&gt;
+ &lt;h3&gt;Summary&lt;/h3&gt;
+ &lt;p&gt;On the way to &lt;a href=&quot;http://marksurman.commons.ca/2015/01/09/what-is-radical-participation/&quot;&gt;radical participation&lt;/a&gt;, Mozilla should be radical &lt;sup class=&quot;footnote&quot;&gt;&lt;a href=&quot;https://repeer.org/tag/mozilla/feed/#fn-48-1&quot; id=&quot;fnref-48-1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; user-centric. Mozilla should not go against the social understanding of the (tech and whole society) situation because it’s what is massively shared and what polarizes the prism of understanding of the society. &lt;strong&gt;We should built solutions for it and transform (develop and change) it on the way. Our responsibility is to build &lt;em&gt;inclusivity&lt;/em&gt; (inclusion strengths) everywhere, to gather for multiplying our impact.&lt;/strong&gt; We must build (progressive) victories instead of battles (of static positions and postures).&lt;br /&gt;
+ If we don’t do it, we go against users self-perceived need: use. We value our differences more than our commonalities and &lt;strong&gt;consider ethic more as an absolute objective than a concrete process&lt;/strong&gt;: we divide, separate, compete. Our solutions get irrelevant, we get rejected and marginalized, we reject compromises that improve the current situation for the ideal, we loose influence and therefore impact on the definition of the present and future. We already done it for the good and the bad in the past (H.264+Daala, pocket integration, Hello login, no Firefox for iOS, Google fishing vs Disconnect, FxOS Notes app which sync is evernote only, …).&lt;br /&gt;
+ To get a consistent and impactful ability to integrate and transform the social understanding, there are four domains where we can take and articulate (connected structure allowing movement) action:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;strong&gt;People&lt;/strong&gt;: identity is the key to grow consciousness, understanding, skills, voice, representation and to articulate global/local, personal/common. &lt;strong&gt;[Activate]&lt;/strong&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;strong&gt;Technology&lt;/strong&gt;: universality is key for a platform (for resilience) with interfaces (for modularity) where services, features and front-ends can plug-in and communicate to provide (inter)active support ; Decouple conditions of fulfillment with execution (content/appearance/policy ; material/immaterial) to support remix (policy continuity, consistency thought providers, …). &lt;strong&gt;[Unlock]&lt;/strong&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;strong&gt;Product&lt;/strong&gt;: persona and (current and emerging) use via user-agents are the keys. Be on all major platforms depending on use, ethical alignment and opportunities, emerging newness to provide continuity (task, device) to users and leading on new practices. Features should be about products parity and opening new possibilities carrying our values to the action at a massive scale. &lt;strong&gt;[Build]&lt;/strong&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;strong&gt;Organizations/institutions&lt;/strong&gt;: sociological innovation for participation is the key. Research on historical (evolution) and sociological (human organizations, social institutions and social behaviors) analysis based on social networks (link as social interactions), in the perspective of producing commons. &lt;strong&gt;[Drive]&lt;/strong&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;Our front has two sides: &lt;strong&gt;propose and protect&lt;/strong&gt;. But each of them are connected and can have different strategic expressions, if our actions generate improving (progressive) curves:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;For the &lt;strong&gt;action taking&lt;/strong&gt;: consciousness, understanding, symbolic actions, behavior change, behavior advocacy (evangelism)&lt;/li&gt;
+ &lt;li&gt;For the &lt;strong&gt;action mode&lt;/strong&gt;: promotion (spreading the idea), incitement (giving a competitive advantage to people involved), collaboration (open interactions to make a win-win exchange; process-centric), contractualization (formalize domains where a win-win exchange is made; object-centric), coercion (giving a competitive disadvantage to people not involved).&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;Social history is a history of social values.&lt;strong&gt; The way we understand and tell the problem determine the solution we can create&lt;/strong&gt;: we need, all the way long, a shared understanding. Tools and technologies are not tied, bound forever to their social value, which depends on people’s social representations that evolve over time.&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;strong&gt;The social behavior&lt;/strong&gt; is a first key. It is the narrative, and therefore its &lt;strong&gt;inclusion in the social history that we make, which converges the product with the values that it stands for&lt;/strong&gt;. Here is the articulation of product with people and technology, of product with leadership network and advocacy engine (it could be less persistent and inclusive: marketing).&lt;/li&gt;
+ &lt;li&gt;&lt;strong&gt;The social organization&lt;/strong&gt; is a second key. It is about how the process, the tools, the architecture, the governance and the opportunities/constraints have changed for Mozilla (org) and Mozillians (people). &lt;strong&gt;Here comes the question of being open&lt;/strong&gt;. It is not enough because it is about availability (passive) and not inclusivity (active). The high level of automation coming is a challenge. We should level-up the meaning to differentiate from others: &lt;strong&gt;Mozilla should activate and unlock societal progress to build fair technical progress&lt;/strong&gt;. Mozilla need to &lt;strong&gt;identify its resilient backbone&lt;/strong&gt; (not only a technology, the web, but something that articulate people, technology and products) and make it more universal (through people and products). But our goals can’t be absolutely achieved because they have to be considered in a dynamic context. However, the brand engagement is persistent, if it’s included in the product, visible, and centered on easing the user’s action.&lt;br /&gt;
+ Linked to the ‘being open’ question, the advocacy engine could be a thing to unlock societal progress. People are satisfied of narrow hills of choice until they understand it’s not socially neutral. It’s the case with technology: they accept things about technology to be build top-down. &lt;strong&gt;A successful advocacy, even one about technology, is always built bottom-up&lt;/strong&gt;, as its function is to give back the voice to the people, to get them involved, not to make them fulfill our predefined aims. The top-down method is too organization centric and administrative content centric: it can’t massively drive people that are not already committed to the org. It’s usually named advertisement or propaganda. &lt;strong&gt;If we want to have an impact, we should listen to people needs, not tell them to listen to ours&lt;/strong&gt;. People want (first) to be empowered, not to empower an org. We need to have content and user centric (not org and it’s process) tools/platform for advocates and leaders: let’s build the technology advocacy plan together. Yes it’s slower, but much more massive, inclusive and persistent. The impact will be higher because it will carry a meaning for people and it wont be too org centric. So it will be qualitatively better: not just an amount, &lt;strong&gt;accumulation is not our goal, but impact, that comes from articulation&lt;/strong&gt;. Likewise we should be careful to not use best practice as absolute solutions, but as solutions in a context, if we want to transpose them massively: when we unify we should avoid to homogenize. On the narrative side, our preoccupation should be about building short, medium and long term narrative to get action.&lt;/li&gt;
+ &lt;li&gt;&lt;strong&gt;The social institutions&lt;/strong&gt; are the third key. Here is the articulation of the leadership network with the advocacy engine. &lt;strong&gt;Leaders build new solutions (products) and Advocates new voices (rallying), they are both about personal development and empower commons.&lt;/strong&gt; Leadership=learn+create and advocacy=teach+spread commons. Leaders are projects/orgs leaders, the ones that traduce DNA (values) in products (concrete ability and availability). Advocates are values advocates, the ones that traduce DNA (values) in actions (behavior). As they are both targeting commons, they both produce the same social organization (collaboration instead of competition). They are both involved in creating (different) representations (institutions) and organizations (foundation/firms) but &lt;strong&gt;with a different DNA (values) processing&lt;/strong&gt;: from public good to personal benefit or from personal interest to public benefit. If Mozilla cares about public good resilience, &lt;strong&gt;the articulation of their domains of values is critical&lt;/strong&gt;. So, on the social organization side, their articulation’s expression and the revision process must be said and clear: from hierarchy or contract or different autonomy levels (internal incubation and external advocacy), or … to criteria to start a revision. About the narrative, and hence about the social behavior side, leaders carry a lot of legitimacy and avoid the stay-experimental or non-massive (unique) thoughts. And we need legitimacy to get impact. But this legitimacy is already present if we&lt;strong&gt; make clear that our actions are about commons&lt;/strong&gt;. We should name them creators (compositors or managers) to make it clear that the creative process is a collaboration, made by a team and that the public good do not have the same role in the process and outcome. Full circle.&lt;/li&gt;
+ &lt;li&gt;&lt;strong&gt;The social networks&lt;/strong&gt; are the keystone. Let’s shortly take an example based on social networks (link as social interactions) with the perspective of producing people, technological and product commons. &lt;strong&gt;We need better tools for collaboration and participation&lt;/strong&gt;: tools that merge discussion channels, capitalize on the discussion and preview the results to build a plan. From evolving the wiki discussion page to feature document production into peer-to-peer discussion.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;An analysis of the creation process is another way to the articulation of product with people and technology.&lt;br /&gt;
+ Platforms move closer to strict ‘walled garden’ ecosystems. We need bridges from lab to home that carry different mix of customization and reliability to support the emancipation curve. We need to build pathways thought audiences and thought IT layers (content, software, hardware, distant service). &lt;strong&gt;We should find a convergence between customization&lt;/strong&gt; (dev code patch to users add-ons) &lt;strong&gt;and reliability&lt;/strong&gt; (self made to mass product), &lt;strong&gt;between first time experience, support and add-ons thought all our users’ persona by building bridges, pathways&lt;/strong&gt;. Mozilla should find ways to &lt;strong&gt;integrate learning&lt;/strong&gt; in its products, in-content, as we have code comment on code: on-boarding levels, progression from simple to high level techniques, reproducible/universal next task/skill building.&lt;/p&gt;
+ &lt;h3&gt;Detailed discussion content&lt;/h3&gt;
+ &lt;p&gt;Here are the developed ideas, with more reference to our allies and detractors’ products.&lt;/p&gt;
+ &lt;h4&gt;People, the sociological side&lt;/h4&gt;
+ &lt;h5&gt;From focused to systemic action&lt;/h5&gt;
+ &lt;p&gt;First of all, I think &lt;strong&gt;the strategy move Mozilla is doing is the right one&lt;/strong&gt; as it embraces more our real life. People are not defined by one characteristic, we are complex: ex. we can be pedestrian, car driver, biker, Public Transport user… we think and do simultaneously. So why Mozilla should restrict its strategy by targeting people on skills, through education, thought better material only (the Mozilla Academy program). Education, even popular education, can’t do everything for the people to build change. &lt;strong&gt;We need a plan that balance intellectual and practical (abstraction/action, think/do) integrating progressive paths to massively scale so we get an impact: build change.&lt;/strong&gt;&lt;/p&gt;
+ &lt;h5&gt;Real life: Social history, individuals and institutions as an articulation founding the action.&lt;/h5&gt;
+ &lt;p&gt;Let’s start by some definitions based on my understanding of some &lt;a href=&quot;https://fr.wikipedia.org/wiki/Sociologie&quot;&gt;Wikipedia articles&lt;/a&gt;. Sociology is the study of the evolution of societies: human organizations and social institutions. It is about &lt;strong&gt;the impact of the social dimension on humans representations (ways of thinking) and behaviors (ways of acting)&lt;/strong&gt;. It allows to study the conceptions of social relations according to fundamental criteria (structuralism, functionalism, conventionalism, etc.) and the hooks to reality (interactionism, institutionalism, regulationisme, actionism, etc.), to think and shape the modernity. Currently (and this is key for Mozilla’s positioning), the combination of models replace the models’ unity, which aims to assume the multidimensionality. There are three major sociological paradigms, including one emerging:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;strong&gt;The holistic paradigm&lt;/strong&gt;: Society is a whole that is greater than the sum of its parts, it exists before the individual and individuals are governed by it. In this context, the Society includes the individual and the individual consciousness is seen only as a fragment of the collective consciousness. The emphasis is on the social fact, whose cause must be sought in earlier social facts. The social fact is part of a system of interlocking institutions that govern individuals. It is external to the individual and constraint it. Sociology is then the science of institutional invariants in which are the observable phenomenas.&lt;/li&gt;
+ &lt;li&gt;&lt;strong&gt;The atomistic paradigm&lt;/strong&gt;: each individual is a social atom. The atoms act according to self motives, interests, emotions and are linked to other atoms. A system of constant interaction between atoms produces and reproduces Society. The emphasis is on the cause of social actions and the meaning given by individuals to their actions. A horizon of meanings serve as reference instead of the arrangements of institutions. The institution is there but it serves the motives and interests of agents. Sociology is then the study of the social action.&lt;/li&gt;
+ &lt;li&gt;The recent emergence of a sociological analysis based on &lt;strong&gt;social networks&lt;/strong&gt; (which are a collection of individuals or organizations connected by regular social interactions) suggest lines of research &lt;strong&gt;beyond the opposition between the holistic and the atomistic approaches&lt;/strong&gt;. The theory of social networks conceives social relationships in terms of nodes and links. The nodes are usually social actors in the network but can also represent institutions, and links are the relationships between these nodes. There may be several kinds of links between nodes and their analysis determines social capital of the social actors.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;Consequently, Mozilla should build its strategy on &lt;strong&gt;historical&lt;/strong&gt; (evolution) and &lt;strong&gt;sociological&lt;/strong&gt; (human organizations, social institutions and social behaviors) analysis based on &lt;strong&gt;social networks&lt;/strong&gt; (links as social interactions), in the perspective of producing &lt;strong&gt;commons&lt;/strong&gt;. That is to say as an &lt;strong&gt;engine of transition from a model of value&lt;/strong&gt; on its last leg (rarity capitalism) to the emerging one (new articulation of the individual and the collective: commons).&lt;br /&gt;
+ It is important and strategic to propose a sociological articulation supporting our mission and its purpose (commons) since &lt;strong&gt;the sociological concept (the paradigm) reveals an ideological characteristic&lt;/strong&gt;: because it participates in societal movements made in the Society, it serves an ideal. The societal domain, what’s making society, a political object, should be a stake for Mozilla.&lt;/p&gt;
+ &lt;h5&gt;Build on a basement: current tech challenge articulated with current social meaning/perception&lt;/h5&gt;
+ &lt;p&gt;&lt;strong&gt;We should articulate ‘our real life’ with the nowadays tech challenge&lt;/strong&gt;: how to get back control over our data at the time of IoT, cloud, big data, convergence (multi-devices/form factor)? From a user point of view, we have devices and want them convenient, easy and nice. The big moves in the tech industry (IoT, cloud, big data, convergence) free us for somethings and lock us for others. The lock key is that our devices don’t compute anymore our data that are in silos. From a developer point of view, the innovation is going very fast and it’s hard to have a complete open source toolbox that we can share, mostly because we don’t lead: Open has turn to be more open-releasing.&lt;br /&gt;
+ We should articulate our new strategy with the tech industry moves: for example, &lt;strong&gt;as a user, how can I get (email) encryption on all my devices?&lt;/strong&gt; Should I follow (fragmented) different kind of howtos/tools/apps to achieve that? How do I know these are consistent together? How can I be sure it won’t brake my continuous workflow? (app silo? social silo? level of trust and reliability?)&lt;br /&gt;
+ Mozilla have the skills to answer this as we already faced and solved some of these issues on particular points: like how to ease the installation of Firefox for Android for Firefox desktop users, open and discoverable choice of search engines, synchronization across devices, …&lt;br /&gt;
+ &lt;strong&gt;Mozilla’s challenge is to not be marginalized by the change of practices. Having an impact is embracing the new practice and give it an alternative.&lt;/strong&gt; Mozilla already made that move by saying « &lt;em&gt;Firefox will go where users are&lt;/em&gt;« , by trying to balance the advertisement practice between adds companies and users, by integrating H.264 and developing Daala. But &lt;strong&gt;Mozilla never stated that clearly as a strategy&lt;/strong&gt;.&lt;/p&gt;
+ &lt;h5&gt;A backbone to make our mission resilient in it expressions&lt;/h5&gt;
+ &lt;p&gt;If we think about the &lt;strong&gt;Facebook’s strategy, they first built a network of people whiling to share&lt;/strong&gt; (no matter what they share) and then use this &lt;strong&gt;transversal backbone to power vertical business segments&lt;/strong&gt; (search, donation, local market selling, …). Google with its search engine and its open source policy have a similar (in a way) strategy. The difference here is that the backbone is people’s data and control over digital formats. In both cases, the level of use (of the social network, search engine, mobile OS, …) is the key (with fast innovation) to have an impact. And that’s a major obstacle to build successful alternatives.&lt;br /&gt;
+ The proposed Mozilla’s strategy is built in the opposite way, and that’s questioning. &lt;strong&gt;We try to build people network depending on some shared matters&lt;/strong&gt;. Then, is our strategy able to scale enough to compete against GAFAM, or are we trying to build a third way ?&lt;br /&gt;
+ For the products, the Mozilla’s strategy is still (and has always been) inclusive: everybody can use the product and then benefit of its open web values. A good product that answer people needs, plus giving people back/new power (allow new use) build a big community. For the network, should we build our global force of people based on concentric circles (of shared matters) or based on a (Mozilla own) transversal backbone (matter agnostic)? It seems to me the actual presentation of the strategy do not answer clearly enough this big question: &lt;strong&gt;which &lt;em&gt;inclusivity&lt;/em&gt; (inclusion strengths) mechanism in the strategy?&lt;/strong&gt;&lt;br /&gt;
+ And that &lt;strong&gt;call back to our product strategy&lt;/strong&gt;: build a community that shares values, that is used to spread outcomes (product) OR build a community that shares a product, that is used to spread values. This is not a question on what matters more (product VS values) but on the strategy to get to a point, an objective (many web citizens). Shouldn’t we use our product to built a people network backbone ? Back to GAFAM: what can we learn from the Google try with Google+?&lt;br /&gt;
+ If our core is not enough transversal (the backbone), more new web/tech market there will be, more we will be marginalized, because focused on our circles center not taking in account that the war front (the context) have changed. &lt;strong&gt;Mozilla have to be resilient: mutability of the means, stability in the objectives.&lt;/strong&gt;&lt;br /&gt;
+ The document is the MoFo strategy, and so it doesn’t say anything about ‘build Firefox’ (aka the product strategy) and so don’t articulate our main product (Firefox) with our main people network building effort and values sharing engine. We should do it: at a strategic scale and a particular scale (articulating the agenda-setting with main product features).&lt;/p&gt;
+ &lt;h5&gt;Brand engagement, a psychological backbone on the user side ?&lt;/h5&gt;
+ &lt;p&gt;It seems that our GAFAM challengers get big and have impact by not educating (that much) people, and that’s what makes them not involved in the web citizenship. Or only when they are pushed by their customers. At the opposite, making people aware about web citizenship at first, makes it hard to have that much people involved and so to have impact. However, there is &lt;strong&gt;an other prism that drive people: the brand perceived values&lt;/strong&gt;. Google is seen as a tech pioneer innovator and doing the good because of its open policy, free model, fast innovation… Facebook is seen as really cool firm trying to help people by connecting them…&lt;br /&gt;
+ Is the increase of marketing of Mozilla doing good enough to gains back users ? Is this resilient compared to the next-tech-thing coming ?&lt;br /&gt;
+ Most of the time when I meet Goggle Chrome users and ask then why they use it and don’t switch to Firefox, they answer about use allowed (sync thought devices, apps everywhere that run only on GC, …). Sometimes, they argue that they make effort on other areas, and that they want to keep they digital life simple. They &lt;strong&gt;experience is not centered in a product/brand, but more on the person&lt;/strong&gt;: on that Google Chrome with its Person (with one click ‘auto-login’ to all Google services) is far superior than Firefox.&lt;/p&gt;
+ &lt;h5&gt;User-agent or products ?&lt;/h5&gt;
+ &lt;p&gt;A user-agent is an intermediary acting on behalf of a supplier. As a representative, it is the contact point with customers; It’s role is to manage, to administer the affairs; it is entrusted with a mission by one or more persons; it both acts and produce an effect.&lt;br /&gt;
+ So, the user-agent can be describe with three criteria. It is: an intermediate (user/technology) ; a tool (used to manage and administrate depending on the user’s skills) ; a representative (mission bearer, values vector, for a group of people). It exceeds partly the contradiction between being active and passive.&lt;br /&gt;
+ A &lt;strong&gt;user-agent articulate personal-identity with technology-identity&lt;/strong&gt; and give informations about available skills over these domains. It’s much more universal than a product that is about featuring a user-agent. &lt;strong&gt;If we target resilience, user-agent should be the target&lt;/strong&gt;.&lt;/p&gt;
+ &lt;h4&gt;Social history, marketing: how we understand things to make choices&lt;/h4&gt;
+ &lt;h5&gt;History of the social value&lt;/h5&gt;
+ &lt;p&gt;The way we look at the past and current facts shape our understanding and determine if we open new ways to solve the issues identified. That’s the way to understand the challenges that come on the way and to agree on an adaptation of the strategy instead of splitting things. The way we understand and tell the problem determine the solution we can create: we need, all the way long, &lt;strong&gt;a shared understanding.&lt;/strong&gt;&lt;br /&gt;
+ &lt;strong&gt;Tools and technologies are not necessarily tied to their social value, which depends on social representations. The social value can be built upstream and evolve downstream.&lt;/strong&gt; It also depends on the perspective in which we look at it, on the understanding of the action and therefore on past or current history. Example: the social value of a weapon can be a potential danger or defense, creative (liberating) or destructive. The nuclear bomb is a weapon of mass destruction (negative), whose social value was (ingeniously built as) freedom (positive).&lt;/p&gt;
+ &lt;h5&gt;Impact in our strategy: a missing root&lt;/h5&gt;
+ &lt;p&gt;To engage the public, before to « &lt;em&gt;Focus on creative campaigns that use media + software to engage the public.&lt;/em&gt; » we need to step back, in our speeding world, for understanding together the big picture and the big movement.&lt;br /&gt;
+ Mozilla want to fuel a movement and propose a strong and consistent strategy. However, I think &lt;strong&gt;this plan miss a key point, a root point: build a common (hi)story.&lt;/strong&gt; This should be an objective, not just an action.&lt;br /&gt;
+ Also, that’s maybe a missing root for the State of the web report: how do we understand what we want to evaluate? But it’s not only a missing root for an (annual?) report (a ‘Reporters without borders’ Press-Freedom like?), it’s a missing root for a new grow of our products’ market share.&lt;br /&gt;
+ For example, I do think that most users don’t know and understand that Mozilla is a foundation, Firefox build by a community as a product to keep the web healthy: &lt;strong&gt;they don’t imagine any meaning about technology&lt;/strong&gt;, because they see it as a neutral tool at its root, so as a tool that should just fit they producing needs.&lt;br /&gt;
+ Firefox, its technologies and its features are not bound for ever. It is the narrative, and therefore their inclusion in the social history that we make, which converges Firefox with the values that it stand for. &lt;strong&gt;Stoping or changing the deep narrative means cutting the source of common understanding and making stronger other consistencies captured by other objects, turning as centrifugal forces for Firefox.&lt;/strong&gt;&lt;br /&gt;
+ Marketing is a way to change what we socially say about things: that’s why Google Chrome marketing campaign (and consistent features maturity) has been the decreasing starting point of Firefox. &lt;strong&gt;Our message has been scrambled.&lt;/strong&gt;&lt;/p&gt;
+ &lt;h4&gt;From participation to emancipation: values, people and org relationships&lt;/h4&gt;
+ &lt;p&gt;How to emancipate people in the digital world ?&lt;/p&gt;
+ &lt;h5&gt;Keeping the open open&lt;/h5&gt;
+ &lt;p&gt;Being open is not a thing we can achieve, it’s a constant process. « &lt;em&gt;Mozilla needs to engage on both fronts, tackling the big problems but also fuelling the next wave of open.&lt;/em&gt; » Yes, but &lt;strong&gt;Mozilla should say too how the next wave of open can stay under people’s control and rally new people&lt;/strong&gt;. Not only open code, but open participation, open governance, open organization. Being open is not a releasing policy about objects, it’s a mutation to participation process: a metamorphosis. It’s not reached by expanding, but by shifting. It’s not only about an amount, but about values: it’s qualitative.&lt;br /&gt;
+ Maybe &lt;strong&gt;open is not enough&lt;/strong&gt;, because it doesn’t say enough about who control and how, about the governance, and says too much about &lt;strong&gt;availability (passive)&lt;/strong&gt; and not enough &lt;strong&gt;about &lt;em&gt;inclusivity&lt;/em&gt; (active ; inclusion strengths)&lt;/strong&gt;. It doesn’t say how the power is organized and articulated to the people (ex. think about how closed is the open Android). We may need to change the wording: indie web, the web that fuel autonomy, is a try, but it doesn’t say enough about &lt;em&gt;inclusivity&lt;/em&gt; compared to openness &amp;amp; opportunity. Emancipation is the concept. It’s strategic because it says what is aligned to what, especially how to articulate values and uses. It’s important because it tells what are the sufficient conditions of realization to ‘open/indie’. That’s key to get ‘open/indie at small and large scales, from Internet people to Internet institutions, thought all ‘open/indie’ detractors in the always-current situation: a resilient ecosystem.&lt;br /&gt;
+ My intuition is that &lt;strong&gt;the leadership network and advocacy engine promoting open will be efficient if we clarify ‘open’ while keeping it universal&lt;/strong&gt;. We can do it by looking back at the raw material that we have worked for years, our DNA in action. Because after all, we are experts about it and wish others to become experts too. It does not mean to essentialize it (opposing its nature and its culture), &lt;strong&gt;but to define its conditions of continuous achievement in our social context&lt;/strong&gt;.&lt;/p&gt;
+ &lt;h5&gt;Starting point: exemplary projects that tell a lot about the evolution of our DNA in action&lt;/h5&gt;
+ &lt;p&gt;Clarifying the idea of ‘open’ is strategic to our action because it outlines the constitution of ‘open’, its high ‘rules’, like with laws in political regimes. It clarifies for all, if you are part of it or not, and it tells you what to change to get in. It can reinforce the brand by differentiating from the big players that are the GAFAM: &lt;strong&gt;it’s a way to drive, not to be driven by others lowering the meaning to catch the social impact. We should say that ‘open’ at Mozilla means more than ‘open’ at GAFAM&lt;/strong&gt;. I wish Mozilla to speak about its openness, not as an ‘equal in opportunity’ but as an ‘equal in participation’, because it fits openness not only for a moment (on boarding) or for a person, but during the whole process of people’s interaction.&lt;br /&gt;
+ &lt;a href=&quot;https://www.rust-lang.org/&quot;&gt;Rust&lt;/a&gt; and &lt;a href=&quot;https://servo.org/&quot;&gt;Servo&lt;/a&gt; or &lt;a href=&quot;https://firefoxos.mozilla.community/&quot;&gt;Firefox OS&lt;/a&gt; (since the Mozilla’s shift to radical participation) seem to be very good examples of projects with participation &amp;amp; impact centric rules, tools, process (RFC, new team and owners, …). Think about how Rust and &lt;a href=&quot;http://arc.applause.com/2015/03/27/google-dart-virtual-machine-chrome/&quot;&gt;Dart emerged and are evolving&lt;/a&gt;. Think about how stronger has been the locked-open Android with partnership than the open-locked FxOS. We should tell those stories, not as recipes that can be reproduced, but as process based on a Constitution (inclusive rules) that make a political regime (open) and define a mode of government (participation). That’s key to social understanding and therefore to transpose and advocate for it.&lt;br /&gt;
+ As projects&lt;strong&gt; compared to ‘original Mozilla’, Rust, Servo and FxOS could say a lot&lt;/strong&gt; about how different they implemented learning/interaction/participation at the roots of the project. How the process, the tools, the architecture, the governance and the opportunities/constraints have changed for Mozilla and participants. This could definitely help to setup our curriculum resources, database and workshop at a personal (e.g., “How to teach / facilitate / organize / lead in the open like Mozilla.â€) and orgs levels, with personal and orgs policies.&lt;/p&gt;
+ &lt;h5&gt;Spreading the high meanings in our strategy to consolidate it consistency&lt;/h5&gt;
+ &lt;p&gt;Clarifying the constitution of ‘open’ calls to clarify other related wordings.&lt;br /&gt;
+ I’m satisfied to read back (social) ‘movement’ instead of ‘community’, because it means that our goal can’t be achieve forever (is static), but we should protect it by acting. And it seems more inclusive, less ‘folds on itself’ and less ‘build the alternative beside’ than ‘community’: the alternative can be everywhere the actual system is. It can make a system. It can get global, convergent, continuous, … all at the same time. Because it’s roots are decentralized _and_ consistent, collaborating, …&lt;/p&gt;
+ &lt;p&gt;About participation, we should think too (again) about engagement VS contribute VS participate: how much am I engaged ? Free about defining and receiving cost/gains? What is the impact of my actions ? … &lt;strong&gt;These different words carry different ideas about how we connect the ‘open’&lt;/strong&gt;: spread is not enough because it diffuses, _be_ everywhere is more permanent. Applied to Mozilla’s own actions, &lt;strong&gt;funding open projects and leaders, is maybe not enough and there should be others areas where we can connect&lt;/strong&gt; inside products, technology, people and organizations that build emancipation. So that say something about getting control (who, how, …).&lt;/p&gt;
+ &lt;h5&gt;IA: a challenge for ‘open’&lt;/h5&gt;
+ &lt;p&gt;IA is first developed to help us by improving our interactions. However, this seems to start to shift into taking decisions instead of us. This is problematic because these are indirect and direct ways for us to loose control, to be locked. And that can be as far as computers smarter than humans. The problem is that technical progress is made without any consideration of the societal progress it should made.&lt;br /&gt;
+ That’s an other point, why open is not enough: automation should be build-in with superior humanization. &lt;strong&gt;Mozilla should activate and unlock societal progress to build fair technical progress.&lt;/strong&gt;&lt;/p&gt;
+ &lt;h5&gt;Digital integration &amp;amp; democracy&lt;/h5&gt;
+ &lt;p&gt;The digital (&amp;amp; virtual) world is gaining control over the physical world in many domains of our society (economy to finance, mail to email, automatic car, voting machine, …). It’s getting more and more integrated to our lives without getting back our (imperfect) democracy integrated into them. Public benefit and public good are turning ‘self benefit’ and ‘own sake’ because citizens don’t have control over private companies. &lt;strong&gt;We should build a digital democracy if we don’t want to loose at all the democratic governing of society.&lt;/strong&gt; We must overcome the poses and postures battles about private and public. We need to build.&lt;/p&gt;
+ &lt;h4&gt;‘Leader’ &amp;amp; ‘Leadership’ need a clarification&lt;/h4&gt;
+ &lt;h5&gt;Why a clarification?&lt;/h5&gt;
+ &lt;p&gt;At some level, I’m not the only one to ask this question:&lt;/p&gt;
+ &lt;blockquote&gt;&lt;p&gt;How do CRM requirements for Leadership and Advocacy overlap / differ? What’s our email management / communications platform for Leadership?&lt;/p&gt;&lt;/blockquote&gt;
+ &lt;p&gt;Connect leaders to lead what ? How ? To whose benefit ? Do we want to connect leaders or initiatives (people or orgs) ? Will the leaders be emerging ones (building new networks) or established ones (use they influence to rally more people)? Are Leaders leaders of something part of Mozilla (like can be Reps) or outside of Mozilla (leaders of project, companies, newspaper: tech leaders, news leaders, …) ? This is especially important depending on what is the desire for the leaders to become in the future. &lt;strong&gt;The MoFo’s document should be more precise&lt;/strong&gt; about this and go forward than « &lt;em&gt;Mozilla must attract, develop, and support a global network of diverse leaders who use their expertise to collaboratively advance points-of-view, policies and practices that maintain the overall health of the Internet.&lt;/em&gt; »&lt;br /&gt;
+ We should do it because &lt;strong&gt;the confusion about the leadership impact the advocacy engine&lt;/strong&gt;: « &lt;em&gt;The shared themes also provide explicit opportunities for our Leadership and Advocacy efforts to work together.&lt;/em&gt; » Regarding Mozilla, is the leaders role to be advocacy leaders ? It seems as they share themes and key initiatives (even if not worded the same sometimes). Or in other words, who Drives the Advocacy engine?&lt;/p&gt;
+ &lt;h5&gt;Iterations with the actual definition: creators&lt;/h5&gt;
+ &lt;p&gt;Here are my iterations on the definition of ‘Leaders’:&lt;/p&gt;
+ &lt;ul&gt;
+ &lt;li&gt;The Leaders could be the people platform (the community) and the advocacy engine the tool/themes/actions platform (the product).&lt;/li&gt;
+ &lt;li&gt;Leaders could build at the end new solutions (products) and Advocates new voices (rallying), that could be translated in a learning area divided like Leadership=learn+create and advocacy=teach+spread.&lt;/li&gt;
+ &lt;li&gt;Leadership: personal development to produce (turn into) new commons or add new facets to commons. Advocacy: personal development to protect established/identified commons.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;p&gt;With these definitions, then Leaders are maybe more a Lab, R&amp;amp;D place, incubation tool (if we think about start-up incubators, then it shows a tool-set that we will need to inspire for the future). But if we want to keep the emphasis on people, &lt;strong&gt;we could name them ‘creators’&lt;/strong&gt; (compositors or managers ; not commoners, because leaders and advocates are commoners ; yes, traditionally creators are craftspersons and intellectual designers). This make sens with the examples given in the MoFo 2020 strategy 0.8 document, where all persona are involved in a building-something-new process.&lt;/p&gt;
+ &lt;p&gt;However, it’s interesting to understand why we choose at first ‘Leaders’. &lt;strong&gt;Leaders build new solutions (products) and Advocates new voices (rallying), they are both about personal development and empower commons.&lt;/strong&gt; Leadership=learn+create and advocacy=teach+spread commons. Leaders are projects/orgs leaders, the ones that traduce DNA (values) in products (concrete ability and availability). Advocates are values advocates, the ones that traduce DNA (values) in actions (behavior). As they are both targeting commons, they both produce the same social organization (collaboration instead of competition). They are both involved to create (different) representation (institutions) and organization (foundation/firms) but &lt;strong&gt;with a different DNA (values) processing&lt;/strong&gt;: from public good to personal interest or the opposite. If Mozilla cares about public good resilience, &lt;strong&gt;the articulation of they domains of values is critical. So their articulation’s expression and the revision process must be said and clear&lt;/strong&gt;: from hierarchy vs contract vs different autonomy levels (internal incubation and external advocacy), vs … to criteria to start a revision.&lt;/p&gt;
+ &lt;h5&gt;The network effect&lt;/h5&gt;
+ &lt;p&gt;Another argument for the switch from Leader to Creator is that the Leader word it too much tight to a single-person-made innovation. &lt;strong&gt;Creator make more clear that the innovation is possible not because of one genius, but because of a team&lt;/strong&gt;, a group, a collective: personS (where there could also be genius). The value is made by the collaboration of people (especially in an open project, especially in a network).&lt;br /&gt;
+ That’s important because that could impact how well we do the convening part: not self-promoting, not-advertising, but sharing skills and knowledge for people and catalysing projects.&lt;br /&gt;
+ &lt;strong&gt;The same for the wording ‘talent’&lt;/strong&gt;: alone, a talent can do nothing that has an impact. At least, we need two talents, a team (plus some assistants at some point).&lt;/p&gt;
+ &lt;h5&gt;The cultural prism&lt;/h5&gt;
+ &lt;p&gt;Again, this seems to be an open question:&lt;/p&gt;
+ &lt;blockquote&gt;&lt;p&gt;Define and articulate “leadership.†Hone our story, ethos and definition for what we mean by “leadership development†(including cultural / localization aspects).&lt;/p&gt;&lt;/blockquote&gt;
+ &lt;p&gt;In my culture, Leader carry positive (take action) and negative (dominate) meanings. That’s another reason why I prefer another naming.&lt;br /&gt;
+ I understand too that it carries a lot of legitimacy (ex. market leader) in our societies and it avoids the stay-experimental or non-massive (unique) thoughts. And we need legitimacy to get impact.&lt;br /&gt;
+ But the way Mozilla has an impact thought all cultures, its &lt;strong&gt;legitimacy, is by creating or expanding a common&lt;/strong&gt;. To do this, depending on the maturity, Mozilla could follow the market proposing an alternative with superior usability OR opening a new market by adding a vertical segment.&lt;/p&gt;
+ &lt;h5&gt;Existing tool-set opportunities&lt;/h5&gt;
+ &lt;p&gt;If Leadership is « &lt;em&gt;a year-round MozFest + Lab&lt;/em&gt;« , so it’s a social network + an incubation place. Then, we already have a social network for people involved with Mozilla: Which kind of link should have the leadership network with &lt;strong&gt;mozillians.org&lt;/strong&gt; ? What can we learn from this project and other specialized social network projects (linkedin, viadeo, …) to build the leadership network ?&lt;/p&gt;
+ &lt;h4&gt;Advocacy engine: make it clear&lt;/h4&gt;
+ &lt;h5&gt;What it is &amp;amp; how it works&lt;/h5&gt;
+ &lt;p&gt;Mozilla is doing a great effort to build its advocacy engine on collaboration (« &lt;em&gt;Develop new partnerships and build on current partnerships&lt;/em&gt;« , « &lt;em&gt;begin collaboration&lt;/em&gt;« , « &lt;em&gt;build alliances with similar orgs&lt;/em&gt;« ) but at the same time affirms that Mozilla should be « &lt;em&gt;Part of a broader movement, be the boldest, loudest and most effective advocates&lt;/em&gt; » that could be seen as too centralized, too exclusive.&lt;br /&gt;
+ While this can be consistent (or contradictory), &lt;strong&gt;the consistency has to be explained&lt;/strong&gt; looking at orgs and people, global and local, abstract and real, with a complementarity/competitive grid.&lt;br /&gt;
+ First, &lt;strong&gt;the articulation with other orgs has to be explained&lt;/strong&gt;. What about others orgs doing things global (&lt;a href=&quot;https://eff.org/&quot;&gt;EFF&lt;/a&gt;, &lt;a href=&quot;https://fsf.org/&quot;&gt;FSF&lt;/a&gt;, …) and local (&lt;a href=&quot;http://www.laquadrature.net/&quot;&gt;Quadrature du net&lt;/a&gt;, CCC, …) ? What about the value they give and that Mozilla doesn’t have (juridic expertise for example) ? What about other advocate engines (&lt;a href=&quot;https://change.org/&quot;&gt;change.org&lt;/a&gt;, &lt;a href=&quot;https://secure.avaaz.org/&quot;&gt;Avaaz&lt;/a&gt;…) ? That should not be at an administrative level only like « &lt;em&gt;Develop an affiliate policy. Defining what MoFo does / does not offer to effectively govern relationships w. affiliated partners and networks (e.g., for issues like branding, fundraising, incentives, participation guidelines, in-kind resources.)&lt;/em&gt; »&lt;br /&gt;
+ Second, this is key for users to understand and &lt;strong&gt;articulate the global level of the brand engagement and their local preoccupations and engagement&lt;/strong&gt;. How the engine will be used for local (non-US) battles ? In the past Mozilla totally involved against PIPA, SOPA by taking action, and hesitate a lot to take position and just published a blog post (and too late to gain traction and get impact) against French spying law for example.&lt;br /&gt;
+ Third, &lt;strong&gt;the articulation ‘action(own agenda)/reaction’ should be clarified&lt;/strong&gt; in the objectives and functioning of the advocacy engine. Especially because other orgs, allies or detractors, try to to setup the social agenda. It’s important because it can change the social perception of our narrative (alternative promotion/issue fighting) and therefore people’s contributions.&lt;br /&gt;
+ People think the technology is socially neutral. People are satisfied of narrow hills of choice (not the meaning, the aim, but only the ability to show your favorite avatar). &lt;strong&gt;People don’t want to feel guilty or oppressed&lt;/strong&gt;, they don’t want new constraints, they are looking for solution only: they want to use, not to do more, they want they things to be done. Part of the problem is about understanding (literacy, education), part of it is about the personal/common duality, part of it is about being hopeless about having an impact, part of it is about expressing change as a positive goal and a new possible way (alternative), not a fight against an issue. About the advocacy engine, I think &lt;strong&gt;our preoccupation should be people-centric and the aim to give them a short, medium and long term narrative to get action without being individuals-centric&lt;/strong&gt;.&lt;/p&gt;
+ &lt;h5&gt;How we build it ?&lt;/h5&gt;
+ &lt;p&gt;How to build a social movement ? How it has been built in the past ? Is it the same today ? Can it be transposed to the digital domain from others social domains ? How strong are the cultural differences between nations? These are the main questions we should answer, and our pivot era gives us many examples in diverse domains (climate change advocates, Syriza &amp;amp; Podemos, NSA &amp;amp; surveillance services in Europe, empowered syndicates in Venezuela, &lt;a href=&quot;http://blogs.valvesoftware.com/economics/why-valve-or-what-do-we-need-corporations-for-and-how-does-valves-management-structure-fit-into-todays-corporate-world/#more-252&quot;&gt;Valve corp. internal organization&lt;/a&gt;…) to set a search terrain. However, I will go strait to my intuitive understanding below.&lt;br /&gt;
+ I’m kind of worried that it’s imagined to build the advocacy engine themes by a top-down method. &lt;strong&gt;I think a successful advocacy is always built bottom-up&lt;/strong&gt;, as its function is to give back the voice to the people, to get them involved, not to make them fulfill our predefined aims. The top-down method is too organization centric: it can’t massively drive people that are not already committed to the org. It’s usually named advertisement or propaganda. If we want to have impact, &lt;strong&gt;we should listen to people needs, not tell them to listen to ours. People want (first) to be empowered, not to empower an org&lt;/strong&gt;. So let’s organize the infrastructure, set the agenda and draw the horizon (strategic understanding) participative: make people fill them with content of their experience. It seems to me it is the only way, the only successful method, if we want to build a movement, and not just a shifting moment (that could be built by the top, with a good press campaign locally relayed for example ; that’s what happen in old style politics: the aim is short term, to cleave).&lt;br /&gt;
+ &lt;strong&gt;Isn’t the advocacy engine a new Drumbeat ?&lt;/strong&gt; We shifted from Drumbeat to Webmaker+web literacy to Mozilla Academy and now to Leadership plus advocacy: it could be good to tell that story now that we are shifting again and learn from it.&lt;br /&gt;
+ &lt;strong&gt;Mozilla should support, behave as a platform&lt;/strong&gt;, not define, not focus. Letting the people set the agenda makes them more involved and is a good way to build a network of shared aims with other orgs, that is not invasive or alienating, but a support relationship in a win-win move. The strength comes from the all agendas sewed. So at an org level, let’s on-board allies organizations as soon as plan building-time (now), to build it together. Yes it’s slower, but much more massive, inclusive and persistent.&lt;/p&gt;
+ &lt;h5&gt;How we evaluate it: cultural bias &amp;amp; qualitative analysis&lt;/h5&gt;
+ &lt;p&gt;First, about the agenda-setting KPI for 2016, should these KPI be an evaluation of the inclusion and rank in others strategic agendas, governance systems and productions (outcome/products) ? Others org could be from different domains: political, social, economy orgs.&lt;br /&gt;
+ Then, as a wide size audience KPI, Mozilla wants « &lt;em&gt;celebration of our campaigns with ‘headline KPIs’ including number of actions, and number of advocates.&lt;/em&gt;« . While doing this could be the right thing to do for some cultures, it could be the worst for others. I think that these KPI don’t carry a meaning for people and are too org centric. In a way, they are to generic: it’s just an amount. &lt;strong&gt;Accumulation is not our goal: we want impact that is the grow of articulated actions&lt;/strong&gt; made by diverse people toward the same aim. &lt;strong&gt;We need our massive KPI to be more qualitative&lt;/strong&gt;, or at least find a way to present them in a more qualitative way: interactive map ? a global to local prism that engages people for the next step ?&lt;/p&gt;
+ &lt;h5&gt;Best practices &amp;amp; massive impact&lt;/h5&gt;
+ &lt;p&gt;Selecting best practices are an appealing method when we want to have a fast and strong impact in a wide area. However, &lt;strong&gt;when we unify we should avoid to homogenize&lt;/strong&gt;. The gain in area by scaling-up is always at the cost of loosing local impact because it is not corresponding to local specificities, hence to local expectations. Federating instead of scaling-up is a way to solve this challenge. So we should be careful to not &lt;strong&gt;use best practice as absolute solutions, but as solutions in a context&lt;/strong&gt; if we want to transpose them massively.&lt;/p&gt;
+ &lt;h5&gt;Tools &amp;amp; platform balanced between user-centric and org-centric outcomes&lt;/h5&gt;
+ &lt;p&gt;It’s good to hear that we will build a advocacy platform. As we ‘had’ bugzilla+svn then mercurial (hg)+… and are going to the &lt;strong&gt;integrated&lt;/strong&gt;, &lt;strong&gt;pluggable&lt;/strong&gt; and &lt;strong&gt;content-centric&lt;/strong&gt; (but non-free; admin tools are closed source) github (targeting more coder than users, but with a lower entry price for users still), we need to be able to have the same kind of tool for advocates and leaders. Something inspired maybe at some levels by the remixing tools we built in Webmakers for web users.&lt;/p&gt;
+ &lt;h4&gt;From experiment to production: support (self made to mass product) + modularity (dev code patch to users add-ons).&lt;/h4&gt;
+ &lt;p&gt;&lt;strong&gt;We need pathways from lab to home that carry different mix of customization and reliability to support the emancipation curve.&lt;/strong&gt;&lt;br /&gt;
+ Users want things to work, because they want to use it. Geeks want to be able to modify a lot and accept to put their hands in the engine to build growing reliability. Advanced users want to customize their experience and keep control and understanding on working status. They want to be able to fix the reliability at a medium/low technical cost. They are OK to gain more control at these prices. Users want to use things to do what they need and want to trust a reliability maintained for them. They are OK to gain control at a no technical cost. Depending on the matter we all have different skill levels, so we are all geeks, advanced users and users depending on our position or on the moment. And depending on our aspirations, we all want to be able to move from one category to an other. That’s what we need to build: we don’t just need to « &lt;em&gt;better articulate the value to our audiences&lt;/em&gt;« , &lt;strong&gt;we need to build pathways thought audiences and thought IT layers&lt;/strong&gt; (content, software, hardware, distant service). &lt;strong&gt;We should find a convergence between customization and reliability, between first time experience, support and add-ons thought all our users’ persona by building bridges, pathways&lt;/strong&gt;. So, « &lt;em&gt;better articulate the value to our audiences&lt;/em&gt; » should not be restrained in our minds to the Mozilla Leadership Network.&lt;br /&gt;
+ &lt;strong&gt;Part of this is being done in other projects outside of Mozilla in the commons movement.&lt;/strong&gt; There are many, but let’s take just one example, the &lt;a href=&quot;https://www.fairphone.com/&quot;&gt;Fairphone&lt;/a&gt; project: modularity, howtos, … all this help to break the product-to-use walls and drive appropriation/emancipation. &lt;strong&gt;Products are less product and brand centric and more people/user centric&lt;/strong&gt;.&lt;br /&gt;
+ Part of this has been done inside Mozilla, like integrating learning in our products, in-content, as we have code comment on code. I think &lt;strong&gt;the &lt;a href=&quot;https://wiki.mozilla.org/Firefox_OS/Spark&quot;&gt;Spark&lt;/a&gt; project on Firefox OS is on a promising path&lt;/strong&gt;, even if maybe immature: it maybe has not been released mainstream because it misses bridges/pathways (on-boarding levels, progression from simple to high level techniques, and no or not enough reproducible/universal next task/skill building).&lt;br /&gt;
+ So some solutions start to emerge, the direction is here, but has never been conceived and implemented that globally, as there isn’t integrated pathways with choice and opportunity and a strategy embracing all products and technologies (platform, tools, …).&lt;/p&gt;
+ &lt;h4&gt;Better tools for collaboration and participation: task-centric to process-centric (use) infrastructure&lt;/h4&gt;
+ &lt;p&gt;&lt;strong&gt;The open community should definitely improve the collaboration tools and infrastructure to ease participation.&lt;/strong&gt;&lt;br /&gt;
+ &lt;strong&gt;&lt;a href=&quot;http://www.discourse.org&quot;&gt;Discourse&lt;/a&gt; ‘merged’ discussion channels&lt;/strong&gt;: email+forum(+instant, messaging, … and others peer-to-peer discussion?). &lt;strong&gt;&lt;a href=&quot;http://stackexchange.com&quot;&gt;Stack exchange&lt;/a&gt; merged the questioning/solving process&lt;/strong&gt; and added a vote mechanism to rank answers: it eased the collaboration on editing the statement and the results while staying synchronous with the discussion and keeping the discussion history. We need such kind of possibilities with discourse: &lt;strong&gt;capitalize on the discussion and preview the results to build a plan.&lt;/strong&gt;&lt;br /&gt;
+ This exist in document oriented software (that added collaboration editing tools), but not that much in collaboration software (that don’t produce documents). For example, while discussing the future plan for Fx/FxOS be supported to keep track on a doc about the proposals plans + criteria &amp;amp; dependencies. In action, it is from &lt;a href=&quot;https://mail.mozilla.org/pipermail/firefox-dev/2015-July/003063.html&quot;&gt;this&lt;/a&gt; plus all the discussion taking place to &lt;a href=&quot;https://mail.mozilla.org/pipermail/firefox-dev/2015-July/003119.html&quot;&gt;that&lt;/a&gt;.&lt;br /&gt;
+ This is maybe something like integrating Discourse+Wiki, maybe with the need to have competing and ranked (both for content and underlaying meaning of content=strategy?) plan/page proposals. &lt;strong&gt;From evolving the wiki discussion page to featuring document production into peer-to-peer discussion.&lt;/strong&gt;&lt;/p&gt;
+ &lt;h4&gt;A recovering strategy: from fail to win&lt;/h4&gt;
+ &lt;p&gt;There is maybe one thing that is in the shadow in this plan: &lt;strong&gt;what do we do when/if we (partially) fail ?&lt;/strong&gt;&lt;br /&gt;
+ I think at least we should say that &lt;strong&gt;we document&lt;/strong&gt; (keep research going on) to be able to outline and spread the outcomes of what we tried to fight against. So we still try to built consciousness to be ready for the next round.&lt;/p&gt;
+ &lt;p&gt; &lt;/p&gt;
+ &lt;p&gt;&lt;em&gt;If you see some contradiction in my thoughts, let’s say it’s my state of thinking right now: please voice them so we can go forward.&lt;/em&gt;&lt;br /&gt;
+ &lt;em&gt; The same for thoughts that are voiced definitive (like users are): take it as a first attempt with my bias: let’s state these bias to go forward.&lt;/em&gt;&lt;/p&gt;
+ &lt;div class=&quot;footnotes&quot; id=&quot;footnotes-48&quot;&gt;
+ &lt;div class=&quot;footnotedivider&quot;&gt;&lt;/div&gt;
+ &lt;ol&gt;
+ &lt;li id=&quot;fn-48-1&quot;&gt; ‘&lt;em&gt;Radical&lt;/em&gt;‘ can be in some cultures an euphemism to ‘&lt;em&gt;violent&lt;/em&gt;‘. Let’s be clear that the change by increasing violence is done to make a popular uprising of some part against others. While it does not help the majority to magically understand that the minority is right, it stigmatize the radical-violent-changers and in the way it discredits the alternative proposed. &lt;span class=&quot;footnotereverse&quot;&gt;&lt;a href=&quot;https://repeer.org/tag/mozilla/feed/#fnref-48-1&quot;&gt;↩&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
+ &lt;/ol&gt;
+ &lt;/div&gt;</description>
+ <pubDate>Sat, 16 Jan 2016 00:27:13 +0000</pubDate>
+ <dc:creator>Nicolas</dc:creator>
+ </item>
+ <item>
+ <title>Will Kahn-Greene: pyvideo status: January 15th, 2016</title>
+ <guid isPermaLink="true">http://bluesock.org/%7Ewillkg/blog/pyvideo/status_20160115.html</guid>
+ <link>http://bluesock.org/%7Ewillkg/blog/pyvideo/status_20160115.html</link>
+ <description>&lt;div class=&quot;section&quot; id=&quot;what-is-pyvideo-org&quot;&gt;
+ &lt;h3&gt;What is pyvideo.org&lt;/h3&gt;
+ &lt;p&gt;&lt;a class=&quot;reference external&quot; href=&quot;http://pyvideo.org/&quot;&gt;pyvideo.org&lt;/a&gt; is an index of Python-related conference and user-group videos on
+ the Internet. Saw a session you liked and want to share it? It's likely you can
+ find it, watch it, and share it with pyvideo.org.&lt;/p&gt;
+ &lt;p&gt;This is the latest status report for all things happening on the site.&lt;/p&gt;
+ &lt;p&gt;It's also an announcement about the end.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;http://bluesock.org/~willkg/blog/pyvideo/status_20160115.html&quot;&gt;Read more…&lt;/a&gt; (5 mins to read)&lt;/p&gt;&lt;/div&gt;</description>
+ <pubDate>Fri, 15 Jan 2016 23:30:00 +0000</pubDate>
+ <dc:creator>Will Kahn-Greene</dc:creator>
+ </item>
+ <item>
+ <title>Chris Cooper: RelEng &amp; RelOps Weekly Highlights - January 15, 2016</title>
+ <guid isPermaLink="true">http://coopcoopbware.tumblr.com/post/137371863755</guid>
+ <link>http://coopcoopbware.tumblr.com/post/137371863755</link>
+ <description>&lt;p&gt;One of releng’s big goals for Q1 is to deliver a beta via &lt;a href=&quot;https://bugzil.la/release-promotion&quot; target=&quot;_blank&quot;&gt;build promotion&lt;/a&gt;. It was great to have some tangible progress there this week with bouncer submission.&lt;/p&gt;
+
+ &lt;p&gt;Lots of other stuff in-flight, more details below!
+ &lt;/p&gt;&lt;p&gt;&lt;b&gt;Modernize infrastructure&lt;/b&gt;:&lt;/p&gt;
+
+ &lt;p&gt;Dustin worked with Armen and Joel Maher to run Firefox tests in TaskCluster on an older EC2 instance type where the tests seem to fail less often, perhaps because they are single-CPU or slower.&lt;/p&gt;
+
+ &lt;p&gt;&lt;b&gt;Improve CI pipeline&lt;/b&gt;:&lt;/p&gt;
+
+ &lt;p&gt;We turned off automation for b2g 2.2 builds this week, which allowed us to remove some code, reduce some complexity, and regain some small amount of capacity. Thanks to Vlad and Alin on buildduty for helping to land those patches. (&lt;a href=&quot;https://bugzil.la/1236835&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/1236835&lt;/a&gt; and &lt;a href=&quot;https://bugzil.la/1237985&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/1237985&lt;/a&gt;)&lt;/p&gt;
+
+ &lt;p&gt;In a similar vein, Callek landed code to disable all b2g desktop builds and tests on all trees. Another win for increased capacity and reduced complexity! (&lt;a href=&quot;https://bugzil.la/1236835&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/1236835&lt;/a&gt;)&lt;/p&gt;
+
+ &lt;p&gt;&lt;b&gt;Release&lt;/b&gt;:&lt;/p&gt;
+
+ &lt;p&gt;Kim finished integrating bouncer submission with our release promotion project. That’s one more blocker out of the way! (&lt;a href=&quot;https://bugzil.la/1215204&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/1215204&lt;/a&gt;)&lt;/p&gt;
+
+ &lt;p&gt;Ben landed several enhancements to our update server: adding aliases to update rules (&lt;a href=&quot;https://bugzil.la/1067402&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/1067402&lt;/a&gt;), and allowing fallbacks for rules with whitelists (&lt;a href=&quot;https://bugzil.la/1235073&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/1235073&lt;/a&gt;).&lt;/p&gt;
+
+ &lt;p&gt;&lt;b&gt;Operational&lt;/b&gt;:&lt;/p&gt;
+ &lt;p&gt;There was some excitement last Sunday when all the trees were closed due to timeouts connectivity issues between our SCL3 datacentre and AWS. (&lt;a href=&quot;https://bugzil.la/238369&quot; target=&quot;_blank&quot;&gt;https://bugzil.la/238369&lt;/a&gt;)&lt;/p&gt;
+
+ &lt;p&gt;&lt;b&gt;Build config&lt;/b&gt;:&lt;/p&gt;
+
+ &lt;p&gt;Mike released v0.7.4 of &lt;a href=&quot;http://gittup.org/tup/&quot; target=&quot;_blank&quot;&gt;tup&lt;/a&gt;, and is working on generating the tup backend from moz.build. We hope to offer tup as an alternative build backend sometime soon.&lt;/p&gt;
+
+ &lt;p&gt;See you all next week!&lt;/p&gt;</description>
+ <pubDate>Fri, 15 Jan 2016 22:44:13 +0000</pubDate>
+ </item>
+ <item>
+ <title>Air Mozilla: Webdev Beer and Tell: January 2016</title>
+ <guid isPermaLink="true">https://air.mozilla.org/webdev-beer-and-tell-january-2016/</guid>
+ <link>https://air.mozilla.org/webdev-beer-and-tell-january-2016/</link>
+ <description>&lt;p&gt;
+ &lt;img alt=&quot;Webdev Beer and Tell: January 2016&quot; class=&quot;wp-post-image&quot; height=&quot;90&quot; src=&quot;https://air.cdn.mozilla.net/media/cache/35/0f/350f246037ead3bab95fdbd4c2b77484.png&quot; width=&quot;160&quot; /&gt;
+ Once a month web developers across the Mozilla community get together (in person and virtually) to share what cool stuff we've been working on in...
+ &lt;/p&gt;</description>
+ <pubDate>Fri, 15 Jan 2016 22:00:00 +0000</pubDate>
+ <dc:creator>Air Mozilla</dc:creator>
+ </item>
+ <item>
+ <title>Support.Mozilla.Org: What’s up with SUMO – 15th January</title>
+ <guid isPermaLink="false">http://blog.mozilla.org/sumo/?p=3665</guid>
+ <link>https://blog.mozilla.org/sumo/2016/01/15/whats-up-with-sumo-15th-january/</link>
+ <description>&lt;p&gt;&lt;strong&gt;Hello, SUMO Nation!&lt;/strong&gt;&lt;/p&gt;
+ &lt;p&gt;The second post of the year is here. Have you had a good time in 2016 so far? Let us know in the comments!&lt;/p&gt;
+ &lt;p&gt;Now, let’s get going with the updates and activity summaries. It will be brief today, I promise.&lt;/p&gt;
+ &lt;h3&gt;&lt;strong class=&quot;author-name&quot;&gt;Welcome, new contributors!&lt;br /&gt;
+ &lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li class=&quot;author&quot;&gt;
+ &lt;div class=&quot;author&quot;&gt;&lt;a class=&quot;username&quot; href=&quot;https://support.mozilla.org/en-US/user/Andy.Yang&quot;&gt;Andy.Yang&lt;/a&gt;&lt;/div&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;div class=&quot;author&quot;&gt;After the massive influx over the last few weeks, we only had Andy introducing himself recently – the warmer the welcome for him!&lt;/div&gt;
+ &lt;div class=&quot;author&quot;&gt;&lt;/div&gt;
+ &lt;div class=&quot;author&quot;&gt;If you just joined us, don’t hesitate – come over and &lt;a href=&quot;https://support.mozilla.org/forums/buddies&quot; target=&quot;_blank&quot;&gt;say “hi†in the forums!&lt;/a&gt;&lt;/div&gt;
+ &lt;div class=&quot;author&quot;&gt;&lt;/div&gt;
+ &lt;div class=&quot;author&quot;&gt;
+ &lt;h3&gt;&lt;strong&gt;Contributors of the week&lt;br /&gt;
+ &lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://blog.mozilla.org/sumo/2016/01/08/whats-up-with-sumo-8th-january/&quot; target=&quot;_blank&quot;&gt;All the people who joined us in the winter season so far!&lt;/a&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid64&quot;&gt;
+ &lt;p&gt;&lt;strong&gt;&lt;span style=&quot;text-decoration: underline;&quot;&gt;We salute you!&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;author&quot;&gt;Don’t forget that if you are new to SUMO and someone helped you get started in a nice way you can &lt;a href=&quot;https://support.mozilla.org/forums/buddies/711364?last=65670&quot; target=&quot;_blank&quot;&gt;nominate them for the Buddy of the Month!&lt;/a&gt;&lt;/div&gt;
+ &lt;div class=&quot;author&quot;&gt;&lt;/div&gt;
+ &lt;/div&gt;
+ &lt;h3&gt;&lt;strong&gt;Most recent SUMO Community meeting&lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://public.etherpad-mozilla.org/p/sumo-2016-01-11&quot; target=&quot;_blank&quot;&gt;You can read the notes here&lt;/a&gt; and see the video on our &lt;a href=&quot;https://www.youtube.com/channel/UCaiposaIhA7HfMqH2NIciyA/videos&quot; target=&quot;_blank&quot;&gt;YouTube channel&lt;/a&gt; and &lt;a href=&quot;https://air.mozilla.org/search/?q=sumo&quot; target=&quot;_blank&quot;&gt;at AirMozilla&lt;/a&gt;.&lt;del&gt; &lt;/del&gt;&lt;del&gt;&lt;br /&gt;
+ &lt;/del&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;strong&gt;IMPORTANT: We are considering changing the way the meetings work. Help us figure out what’s best for you – join the discussion on the forums in this thread: &lt;a href=&quot;https://support.mozilla.org/en-US/forums/contributors/711752?last=67873&quot;&gt;(Monday) Community Meetings in 2016&lt;/a&gt;.&lt;/strong&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;&lt;strong&gt;The next SUMO Community meeting… &lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li style=&quot;text-align: left;&quot;&gt;is happening on &lt;a href=&quot;https://public.etherpad-mozilla.org/p/sumo-2016-01-18&quot; target=&quot;_blank&quot;&gt;Monday the 18th – join us&lt;/a&gt;!&lt;/li&gt;
+ &lt;li style=&quot;text-align: left;&quot;&gt;&lt;strong&gt;Reminder: if you want to add a discussion topic to the upcoming meeting agenda:&lt;/strong&gt;
+ &lt;ul&gt;
+ &lt;li style=&quot;text-align: left;&quot;&gt;Start a thread in the &lt;a href=&quot;https://support.mozilla.org/forums/contributors&quot; target=&quot;_blank&quot;&gt;Community Forums&lt;/a&gt;, so that everyone in the community can see what will be discussed and voice their opinion here before Monday (this will make it easier to have an efficient meeting).&lt;/li&gt;
+ &lt;li style=&quot;text-align: left;&quot;&gt;Please do so as soon as you can before the meeting, so that people have time to read, think, and reply (and also add it to the agenda).&lt;/li&gt;
+ &lt;li style=&quot;text-align: left;&quot;&gt;If you can, please attend the meeting in person (or via IRC), so we can follow up on your discussion topic during the meeting with your feedback.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;&lt;strong class=&quot;author-g-ivsra51ph44x461i&quot;&gt;Developers&lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;The new version of the Ask A Question page is here!&lt;/li&gt;
+ &lt;li&gt;The 2.0 version of the KPI dashboard is in the works.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;http://edwin.mozilla.io/t/sumo&quot; target=&quot;_blank&quot;&gt;You can see the current state of the backlog our developers are working on here&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://public.etherpad-mozilla.org/p/sumo-p-2016-01-14&quot; target=&quot;_blank&quot;&gt;The latest SUMO Platform meeting notes can be found here&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;Interested in learning how Kitsune (the engine behind SUMO) works? &lt;a href=&quot;http://kitsune.readthedocs.org/&quot; target=&quot;_blank&quot;&gt;Read more about it here&lt;/a&gt; and &lt;a href=&quot;https://github.com/mozilla/kitsune/&quot; target=&quot;_blank&quot;&gt;fork it on GitHub&lt;/a&gt;!&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;&lt;strong&gt;Community&lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Our awesome Bangladesh SUMO Warriors are on the road again! Follow their adventures on Twitter under this tag: &lt;a href=&quot;https://twitter.com/search?q=%23sumotourctg&quot; target=&quot;_blank&quot;&gt;#sumotourctg&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;
+ &lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://support.mozilla.org/forums/contributors/711729?last=67763&quot;&gt;Reminder: take a look at our Work Week Summary for Mozlando. We need your feedback for a few things there.&lt;/a&gt;&lt;/div&gt;
+ &lt;/li&gt;
+ &lt;li&gt;
+ &lt;div class=&quot;title&quot;&gt;Ongoing reminder: if you think you can benefit from getting &lt;a href=&quot;https://wiki.mozilla.org/Community_Hardware&quot; target=&quot;_blank&quot;&gt;a second-hand device&lt;/a&gt; to help you with contributing to SUMO, you know where to find us.&lt;/div&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;&lt;strong class=&quot;user-chip&quot; title=&quot;adriel0415&quot;&gt;Support Forum&lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Say hello to the new people on the forums!
+ &lt;ul&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-z87zkz70z39yz83zw7ykz89z3gz82zt&quot;&gt;&lt;a href=&quot;https://support.mozilla.org/user/Tomi55&quot; target=&quot;_blank&quot;&gt;Tomi55&lt;/a&gt; (Hungarian)&lt;/span&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-z87zkz70z39yz83zw7ykz89z3gz82zt&quot;&gt;&lt;a href=&quot;https://support.mozilla.org/user/jdc20181&quot; target=&quot;_blank&quot;&gt;jdc20181&lt;/a&gt; (English)&lt;/span&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-z87zkz70z39yz83zw7ykz89z3gz82zt&quot;&gt;&lt;a href=&quot;https://support.mozilla.org/user/andexi&quot; target=&quot;_blank&quot;&gt;andexi&lt;/a&gt; (Spanish)&lt;/span&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-z87zkz70z39yz83zw7ykz89z3gz82zt&quot;&gt;&lt;a href=&quot;https://support.mozilla.org/user/Qantas94Heavy&quot; target=&quot;_blank&quot;&gt;Qantas94Heavy&lt;/a&gt; (English)&lt;/span&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-z87zkz70z39yz83zw7ykz89z3gz82zt&quot;&gt;&lt;a href=&quot;https://support.mozilla.org/user/samuelms79&quot; target=&quot;_blank&quot;&gt;samuelms79&lt;/a&gt; (Brazilian-PT)&lt;/span&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-z87zkz70z39yz83zw7ykz89z3gz82zt&quot;&gt;&lt;a href=&quot;https://support.mozilla.org/user/jorgecomun&quot; target=&quot;_blank&quot;&gt;jorgecomun&lt;/a&gt; (Spanish)&lt;/span&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;div class=&quot;&quot;&gt;
+ &lt;h3&gt;&lt;strong class=&quot;author-g-ivsra51ph44x461i&quot;&gt;Knowledge Base&lt;/strong&gt;&lt;/h3&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid90&quot;&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid82&quot;&gt;
+ &lt;ul class=&quot;list-bullet1&quot;&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-z87zjz80zxwjz85z4z65zytdpz68zoz69z&quot;&gt;&lt;a href=&quot;https://support.mozilla.org/forums/knowledge-base-articles/711304#post-65289&quot; target=&quot;_blank&quot;&gt;Thanks to everyone who took part in the most recent KB Day!&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
+ &lt;li&gt;Version 44 updates should be live now.&lt;/li&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-w2dz70zaz70z7z89zqz78ziz69zz78zz85zz90zj&quot;&gt;&lt;a href=&quot;https://docs.google.com/spreadsheets/d/1lkpRPJp9P1P5MRU-c9dwbDC0w5bMmrMdu-BNMp1xe8w/edit#gid=6&quot; target=&quot;_blank&quot;&gt;Ongoing reminder: learn more about upcoming English article updates by clicking here&lt;/a&gt;&lt;/span&gt;.&lt;/li&gt;
+ &lt;li&gt;&lt;span style=&quot;text-decoration: underline;&quot;&gt;Ongoing reminder #2:&lt;a href=&quot;https://support.mozilla.org/forums/knowledge-base-articles/&quot; target=&quot;_blank&quot;&gt; do you have ideas about improving the KB guidelines and training materials? Let us know in the forums&lt;/a&gt;!&lt;/span&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid83&quot;&gt;
+ &lt;h3&gt;&lt;strong class=&quot;author-g-ivsra51ph44x461i&quot;&gt;Localization&lt;/strong&gt;&lt;/h3&gt;
+ &lt;/div&gt;
+ &lt;/div&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid95&quot;&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Thanks to everyone writing in with problems, ideas, reports of bugs – all your feedback matters!&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid75&quot;&gt;
+ &lt;h3&gt;&lt;strong&gt;Firefox&lt;br /&gt;
+ &lt;/strong&gt;&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;strong&gt;for Android&lt;/strong&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;a href=&quot;https://support.mozilla.org/forums/contributors/711712?last=67653&quot;&gt;Learn more about Firefox 43 for Android from the official thread with release notes / issues / discussions&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;
+ &lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://support.mozilla.org/forums/contributors/711718?last=67822&quot;&gt;Reminder: Roland is sharing Firefox 44 for Android release notes / issues / discussions&lt;/a&gt; with everyone in the forum.&lt;/div&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;strong&gt;for Desktop&lt;/strong&gt;
+ &lt;ul&gt;
+ &lt;li&gt;The &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1238620&quot; target=&quot;_blank&quot;&gt;uploading issues reported by many users are being tracked here.&lt;/a&gt;&lt;/li&gt;
+ &lt;li&gt;&lt;a href=&quot;https://support.mozilla.org/questions/firefox?tagged=bug1208145&amp;amp;show=all&quot; target=&quot;_blank&quot;&gt;The “show passwords†button has been removed from the password manager for the Beta of Version 44&lt;/a&gt;. The developers are looking into &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1208145&quot; target=&quot;_blank&quot;&gt;last minute fixes for that in this bug&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;Also in Version 44, the &lt;span class=&quot;author-a-kz88zz80zhz89z6hlz81znytez70zz66zz68z&quot;&gt;&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=606655&quot; target=&quot;_blank&quot;&gt;“ask me everytime†option for cookies will be removed from the privacy panel.&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;ul&gt;
+ &lt;li&gt;&lt;strong&gt;for iOS&lt;/strong&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid85&quot;&gt;
+ &lt;ul class=&quot;list-bullet1&quot;&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-107uz69zz81zhz78z0z78zz84zz66zz76zz82zz77zj&quot;&gt;&lt;a href=&quot;https://www.mozilla.org/en-US/firefox/ios/1.4/releasenotes/&quot; target=&quot;_blank&quot;&gt;Firefox for iOS 1.4 primarily with features for China is here&lt;/a&gt;.&lt;br /&gt;
+ &lt;/span&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+ &lt;div class=&quot;&quot; id=&quot;magicdomid86&quot;&gt;
+ &lt;ul class=&quot;list-bullet1&quot;&gt;
+ &lt;li&gt;&lt;span class=&quot;author-a-107uz69zz81zhz78z0z78zz84zz66zz76zz82zz77zj&quot;&gt;Firefox for iOS 2.0 is after 1.4 and hopefully sometime this quarter!&lt;/span&gt;&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+ &lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;/div&gt;
+ &lt;p&gt;Not that many updates this week, since we’re coming out of our winter slumber (even though winter will be here for a while, still) and plotting an awesome 2016 with you and for you. Take it easy, have a great weekend and see you around SUMO.&lt;/p&gt;</description>
+ <pubDate>Fri, 15 Jan 2016 19:38:51 +0000</pubDate>
+ <dc:creator>Michał</dc:creator>
+ </item>
+ <item>
+ <title>Air Mozilla: Paris Firefox OS Hackathon Presentations</title>
+ <guid isPermaLink="true">https://air.mozilla.org/paris-firefox-os-hackathon-presentations/</guid>
+ <link>https://air.mozilla.org/paris-firefox-os-hackathon-presentations/</link>
+ <description>&lt;p&gt;
+ &lt;img alt=&quot;Paris Firefox OS Hackathon Presentations&quot; class=&quot;wp-post-image&quot; height=&quot;90&quot; src=&quot;https://air.cdn.mozilla.net/media/cache/35/83/358305bfa246fff07d707061082134aa.png&quot; width=&quot;160&quot; /&gt;
+ As an introduction to this weekend's Firefox OS Hackathon in Paris we'll have two presentations: - Guillaume Marty will talk about the current state of...
+ &lt;/p&gt;</description>
+ <pubDate>Fri, 15 Jan 2016 18:00:00 +0000</pubDate>
+ <dc:creator>Air Mozilla</dc:creator>
+ </item>
+ <item>
+ <title>J.C. Jones: Renewing Let's Encrypt Certs (Nginx)</title>
+ <guid isPermaLink="false">https://tacticalsecret.com/tag/mozilla/rss/db7fec0c-34d3-4633-9904-79b98aab34e7</guid>
+ <link>https://tacticalsecret.com/renewing-lets-encrypt-certs-nginx/</link>
+ <description>&lt;p&gt;All the first &lt;a href=&quot;https://crt.sh/?id=10172479&quot;&gt;Let's Encrypt certs for my websites&lt;/a&gt; from the LE private beta began expiring last week, so it was time to work through the renewal tooling. I wanted a script that:&lt;/p&gt;
+
+ &lt;ol&gt;
+ &lt;li&gt;Would be okay to run daily, so there'd be plenty of retries if something went wrong, &lt;/li&gt;
+ &lt;li&gt;Wouldn't require extra config for me to forget about if I add a new site, &lt;/li&gt;
+ &lt;li&gt;Would only renew certificates expiring in the next few weeks.&lt;/li&gt;
+ &lt;/ol&gt;
+
+ &lt;p&gt;The official Let's Encrypt client team is hard at work producing a great renew tool to handle all this, but it's not released yet. Of course I could use &lt;a href=&quot;https://caddyserver.com/&quot;&gt;Caddy Server&lt;/a&gt; that &lt;a href=&quot;https://www.youtube.com/watch?v=nk4EWHvvZtI&quot;&gt;just handles all this&lt;/a&gt;, but I have a lot invested in Nginx here.&lt;/p&gt;
+
+ &lt;p&gt;So I wrote a short script and &lt;a href=&quot;https://gist.github.com/jcjones/432eeaa6a2bf25e2c746&quot;&gt;put it up in a Gist&lt;/a&gt;. &lt;/p&gt;
+
+ &lt;p&gt;The script is designed to run daily, with a random start between 00:00 and 02:00 to protect against load spikes at Let's Encrypt's infrastructure. It doesn't do any real reporting, though, except to maintain &lt;code&gt;/var/log/letsencrypt/renew.log&lt;/code&gt; as the most-recent failure if one fails.&lt;/p&gt;
+
+ &lt;p&gt;It's written to handle Nginx with Upstart's &lt;code&gt;service&lt;/code&gt; command. It's pretty modular though; you could make this operate any webserver, or use the webroot method quite easily. Feel free to use the OpenSSL SubjectAlternativeName processing code for whatever purposes you have.&lt;/p&gt;
+
+ &lt;p&gt;Happy renewing!&lt;/p&gt;</description>
+ <pubDate>Fri, 15 Jan 2016 16:01:19 +0000</pubDate>
+ <dc:creator>James 'J.C.' Jones</dc:creator>
+ </item>
+ <item>
+ <title>Yunier José Sosa Vázquez: Conoce los complementos destacados para enero</title>
+ <guid isPermaLink="false">http://firefoxmania.uci.cu/?p=15521</guid>
+ <link>http://firefoxmania.uci.cu/conoce-los-complementos-destacados-para-enero-2016/</link>
+ <description>&lt;p style=&quot;text-align: left;&quot;&gt;Comenzó un nuevo año y con él, te traemos nuevos e interesantes complementos para tu navegador preferido que mejoran con creces tu experiencia de navegación. Durante los próximos 6 meses estará trabajando nuevos miembros en el Add-ons Board Team, en la próxima selección desde Firefoxmanía te avisaremos.&lt;/p&gt;
+ &lt;h3 style=&quot;text-align: left;&quot;&gt;Elección del mes: uMatrix&lt;/h3&gt;
+ &lt;p&gt;uMatrix es muy parecido a un &lt;em&gt;firewall&lt;/em&gt; y desde una ventana fácilmente podrás controlar todos los lugares a donde tu navegador tiene permitido conectarse, qué tipo de datos pueden descargarse y cual puede ejecutar.&lt;/p&gt;
+ &lt;blockquote&gt;&lt;p&gt;Esta puede ser la extensión perfecta para el control avanzado de los usuarios.&lt;/p&gt;&lt;/blockquote&gt;
+ &lt;p&gt;&lt;span id=&quot;more-15521&quot;&gt;&lt;/span&gt;&lt;/p&gt;
+
+ &lt;a href=&quot;http://firefoxmania.uci.cu/conoce-los-complementos-destacados-para-enero-2016/umatrix/&quot;&gt;&lt;img alt=&quot;Interfaz principal de uMatrix&quot; class=&quot;attachment-thumbnail size-thumbnail&quot; height=&quot;160&quot; src=&quot;http://firefoxmania.uci.cu/wp-content/uploads/2016/01/uMatrix-160x160.png&quot; width=&quot;160&quot; /&gt;&lt;/a&gt;
+ &lt;a href=&quot;http://firefoxmania.uci.cu/conoce-los-complementos-destacados-para-enero-2016/umatrix2/&quot;&gt;&lt;img alt=&quot;Opciones de configuración de uMatrix&quot; class=&quot;attachment-thumbnail size-thumbnail&quot; height=&quot;160&quot; src=&quot;http://firefoxmania.uci.cu/wp-content/uploads/2016/01/uMatrix2-160x160.png&quot; width=&quot;160&quot; /&gt;&lt;/a&gt;
+
+ &lt;p&gt;&lt;em&gt;&lt;a href=&quot;http://addons.firefoxmania.uci.cu/umatrix/&quot; target=&quot;_blank&quot;&gt;Instalar uMatrix »&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
+ &lt;h3&gt;También te recomendamos&lt;/h3&gt;
+ &lt;p style=&quot;text-align: left;&quot;&gt;&lt;a href=&quot;http://addons.firefoxmania.uci.cu/https-everywhere/&quot; target=&quot;_blank&quot;&gt;⇒ HTTPS Everywhere&lt;/a&gt; por &lt;a href=&quot;https://addons.mozilla.org/en-US/firefox/user/eff-technologists/&quot; title=&quot;EFF Technologists&quot;&gt;EFF Technologists&lt;/a&gt;&lt;/p&gt;
+ &lt;p style=&quot;text-align: left;&quot;&gt;Protege tus comunicaciones habilitando la encriptación HTTPS automáticamente en los sitios conocidos que la soportan, incluso cuando navegas mediante sitios que no incluyen el prefijo “https†en la URL.&lt;/p&gt;
+ &lt;p style=&quot;text-align: left;&quot;&gt;&lt;a href=&quot;http://addons.firefoxmania.uci.cu/add-to-search-bar/&quot; target=&quot;_blank&quot;&gt;⇒ Add to Search Bar&lt;/a&gt; por &lt;a href=&quot;https://addons.mozilla.org/firefox/user/dr-evil/&quot; target=&quot;_blank&quot; title=&quot;AdblockLite&quot;&gt;Dr. Evil&lt;/a&gt;&lt;/p&gt;
+ &lt;p style=&quot;text-align: left;&quot;&gt;Hace posible que cualquier página con un formulario de búsqueda disponible pueda ser añadido fácilmente a la barra de búsqueda de Firefox.&lt;/p&gt;
+ &lt;div class=&quot;wp-caption aligncenter&quot; id=&quot;attachment_15528&quot; style=&quot;width: 262px;&quot;&gt;&lt;a href=&quot;http://firefoxmania.uci.cu/wp-content/uploads/2016/01/add_to_search_bar.png&quot; rel=&quot;attachment wp-att-15528&quot;&gt;&lt;img alt=&quot;add_to_search_bar&quot; class=&quot;wp-image-15528 size-medium&quot; height=&quot;226&quot; src=&quot;http://firefoxmania.uci.cu/wp-content/uploads/2016/01/add_to_search_bar-252x226.png&quot; width=&quot;252&quot; /&gt;&lt;/a&gt;&lt;p class=&quot;wp-caption-text&quot;&gt;Añadiendo la búsqueda de un sitio web a la barra de búsqueda&lt;/p&gt;&lt;/div&gt;
+ &lt;p style=&quot;text-align: left;&quot;&gt;&lt;a href=&quot;http://addons.firefoxmania.uci.cu/duplicate-tabs-closer/&quot; target=&quot;_blank&quot;&gt;⇒ Duplicate Tabs Closer&lt;/a&gt; por &lt;a href=&quot;https://addons.mozilla.org/firefox/user/peuj/&quot; target=&quot;_blank&quot; title=&quot;The 1-Click YouTube Video Download Team&quot;&gt;Peuj&lt;/a&gt;&lt;/p&gt;
+ &lt;p style=&quot;text-align: left;&quot;&gt;Detecta las pestañas duplicadas en tu navegador y automáticamente las cierra.&lt;/p&gt;
+ &lt;h3 style=&quot;text-align: left;&quot;&gt;Nomina tus complementos favoritos&lt;/h3&gt;
+ &lt;p style=&quot;text-align: left;&quot;&gt;A nosotros nos encantaría que &lt;strong&gt;fueras parte del proceso&lt;/strong&gt; de seleccionar los mejores complementos para Firefox y nos gustaría escucharte. &lt;em&gt;¿No sabes cómo?&lt;/em&gt; Sólo tienes que &lt;em&gt;enviar un correo electrónico&lt;/em&gt; a la dirección &lt;strong&gt;amo-featured@mozilla.org&lt;/strong&gt; con el nombre del complemento o el archivo de instalación y los miembros evaluarán tu recomendación.&lt;/p&gt;
+ &lt;p style=&quot;text-align: left;&quot;&gt;&lt;strong&gt;Fuente:&lt;/strong&gt; &lt;a href=&quot;https://blog.mozilla.org/addons/2016/01/01/january-2016-featured-add-ons/&quot; target=&quot;_blank&quot;&gt;Mozilla Add-ons Blog&lt;/a&gt;&lt;/p&gt;</description>
+ <pubDate>Fri, 15 Jan 2016 15:10:26 +0000</pubDate>
+ <dc:creator>Yunier J</dc:creator>
+ </item>
+ <item>
+ <title>Tim Taubert: Build Your Own Signal Desktop</title>
+ <guid isPermaLink="false">https://timtaubert.de/blog/2016/01/build-your-own-signal-desktop</guid>
+ <link>https://timtaubert.de/blog/2016/01/build-your-own-signal-desktop/</link>
+ <description>&lt;p&gt;The Signal Private Messenger is great. &lt;strong&gt;Use it.&lt;/strong&gt; It’s probably the best secure
+ messenger on the market. When recently a desktop app was announced people were
+ eager to join the beta and even happier when an invite finally showed up in
+ their inbox. So was I, it’s a great app and works surprisingly well for an early
+ version.&lt;/p&gt;
+
+ &lt;p&gt;The only problem is that it’s a Chrome App. Apart from excluding folks with
+ other browsers it’s also a shitty user experience. If you too want your
+ messaging app not tied to a browser then let’s just build our own standalone
+ variant of Signal Desktop.&lt;/p&gt;
+
+ &lt;h3&gt;NW.js beta with Chrome App support&lt;/h3&gt;
+
+ &lt;p&gt;Signal Desktop is a Chrome App, so the easiest way to turn it into a standalone
+ app is to use &lt;a href=&quot;http://nwjs.io/&quot;&gt;NW.js&lt;/a&gt;. Conveniently, their next release v0.13
+ will ship with Chrome App support and is available for download as a beta
+ version.&lt;/p&gt;
+
+ &lt;p&gt;First, make sure you have &lt;code&gt;git&lt;/code&gt; and &lt;code&gt;npm&lt;/code&gt; installed. Then open a terminal and
+ prepare a temporary build directory to which we can download a few things and
+ where we can build the app:&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;$ mkdir signal-build
+ $ cd signal-build
+ &lt;/pre&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;h3&gt;[OS X] Packaging Signal and NW.js&lt;/h3&gt;
+
+ &lt;p&gt;Download the latest beta of NW.js and &lt;code&gt;unzip&lt;/code&gt; it. We’ll extract the application
+ and use it as a template for our Signal clone. The NW.js project does
+ unfortunately not seem to provide a secure source (or at least hashes)
+ for their downloads.&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;$ wget http://dl.nwjs.io/v0.13.0-beta3/nwjs-sdk-v0.13.0-beta3-osx-x64.zip
+ $ unzip nwjs-sdk-v0.13.0-beta3-osx-x64.zip
+ $ cp -r nwjs-sdk-v0.13.0-beta3-osx-x64/nwjs.app SignalPrivateMessenger.app
+ &lt;/pre&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;Next, clone the Signal repository and use NPM to install the necessary modules.
+ Run the &lt;code&gt;grunt&lt;/code&gt; automation tool to build the application.&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;$ git clone https://github.com/WhisperSystems/Signal-Desktop.git
+ $ cd Signal-Desktop/
+ $ npm install
+ $ node_modules/grunt-cli/bin/grunt
+ &lt;/pre&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;Finally, simply to copy the &lt;code&gt;dist&lt;/code&gt; folder containing all the juicy Signal files
+ into the application template we created a few moments ago.&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;$ cp -r dist ../SignalPrivateMessenger.app/Contents/Resources/app.nw
+ $ open ..
+ &lt;/pre&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;The last command opens a Finder window. Move &lt;code&gt;SignalPrivateMessenger.app&lt;/code&gt; to
+ your Applications folder and launch it as usual. You should now see a welcome
+ page!&lt;/p&gt;
+
+ &lt;h3&gt;[Linux] Packaging Signal and NW.js&lt;/h3&gt;
+
+ &lt;p&gt;The build instructions for Linux aren’t too different but I’ll write them down,
+ if just for convenience. Start by cloning the Signal Desktop repository and
+ build.&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;$ git clone https://github.com/WhisperSystems/Signal-Desktop.git
+ $ cd Signal-Desktop/
+ $ npm install
+ $ node_modules/grunt-cli/bin/grunt
+ &lt;/pre&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;The &lt;code&gt;dist&lt;/code&gt; folder contains the app, ready to be launched. &lt;code&gt;zip&lt;/code&gt; it and place
+ the resulting package somewhere handy.&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;$ cd dist
+ $ zip -r ../../package.nw *
+ &lt;/pre&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;Back to the top. Download the NW.js binary, extract it, and change into the
+ newly created directory. Move the &lt;code&gt;package.nw&lt;/code&gt; file we created earlier next to
+ the &lt;code&gt;nw&lt;/code&gt; binary and we’re done. The &lt;code&gt;nwjs-sdk-v0.13.0-beta3-linux-x64&lt;/code&gt; folder
+ does now contain the standalone Signal app.&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;$ cd ../..
+ $ wget http://dl.nwjs.io/v0.13.0-beta3/nwjs-sdk-v0.13.0-beta3-linux-x64.tar.gz
+ $ tar xfz nwjs-sdk-v0.13.0-beta3-linux-x64.tar.gz
+ $ cd nwjs-sdk-v0.13.0-beta3-linux-x64
+ $ mv ../package.nw .
+ &lt;/pre&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;p&gt;Finally, launch NW.js. You should see a welcome page!&lt;/p&gt;
+
+ &lt;figure class=&quot;code&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;$ ./nw
+ &lt;/pre&gt;&lt;/div&gt;&lt;/figure&gt;
+
+
+ &lt;h3&gt;If you see something, file something&lt;/h3&gt;
+
+ &lt;p&gt;Our standalone Signal clone mostly works, but it’s far from perfect. We’re
+ pulling from master and that might bring breaking changes that weren’t
+ sufficiently tested.&lt;/p&gt;
+
+ &lt;p&gt;We don’t have the right icons. The app crashes when you click a media message.
+ It opens a blank popup when you click a link. It’s quite big because also NW.js
+ has bugs and so we have to use the SDK build for now. In the future it would be
+ great to have automatic updates, and maybe even signed builds.&lt;/p&gt;
+
+ &lt;p&gt;Remember, Signal Desktop is beta, and completely untested with NW.js. If you
+ want to help file bugs, but only after checking that those affect the Chrome
+ App too. If you want to fix a bug only occurring in the standalone version
+ it’s probably best to file a pull request and cross fingers.&lt;/p&gt;
+
+ &lt;h3&gt;Is this secure?&lt;/h3&gt;
+
+ &lt;p&gt;Great question! I don’t know. I would love to get some more insights from people
+ that know more about the NW.js security model and whether it comes with all the
+ protections Chromium can offer. Another interesting question is whether bundling
+ Signal Desktop with NW.js is in any way worse (from a security perspective) than
+ installing it as a Chrome extension. If you happen to have an opinion about
+ that, I would love to hear it.&lt;/p&gt;
+
+ &lt;p&gt;Another important thing to keep in mind is that when building Signal on your
+ own you will possibly miss automatic and signed security updates from the
+ Chrome Web Store. Keep an eye on the repository and rebuild your app from
+ time to time to not fall behind too much.&lt;/p&gt;</description>
+ <pubDate>Fri, 15 Jan 2016 14:00:00 +0000</pubDate>
+ </item>
+ <item>
+ <title>Mike Hommey: Announcing git-cinnabar 0.3.0</title>
+ <guid isPermaLink="true">http://glandium.org/blog/?p=3579</guid>
+ <link>http://glandium.org/blog/?p=3579</link>
+ <description>&lt;p&gt;Git-cinnabar is a git remote helper to interact with mercurial repositories. It allows to clone, pull and push from/to mercurial remote repositories, using git.&lt;/p&gt;
+ &lt;p&gt;&lt;a href=&quot;https://github.com/glandium/git-cinnabar&quot;&gt;Get it on github&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;These release notes are also &lt;a href=&quot;https://github.com/glandium/git-cinnabar/wiki/Release-Notes:-0.3.0&quot;&gt;available on the git-cinnabar wiki&lt;/a&gt;.&lt;/p&gt;
+ &lt;p&gt;Development had been stalled for a few months, with many improvements in the&lt;br /&gt;
+ &lt;code&gt;next&lt;/code&gt; branch without any new release. I used some time during the new year&lt;br /&gt;
+ break and after in order to straighten things up in order to create a new&lt;br /&gt;
+ release, delaying many of the originally planned changes to a future 0.4.0&lt;br /&gt;
+ release.&lt;/p&gt;
+ &lt;h3&gt;What’s new since 0.2.2?&lt;/h3&gt;
+ &lt;ul&gt;
+ &lt;li&gt;Speed and memory usage were improved when doing &lt;code&gt;git push&lt;/code&gt;.&lt;/li&gt;
+ &lt;li&gt;Now works on Windows, at least to some extent. See &lt;a href=&quot;http://glandium.org/blog/Windows-Support&quot;&gt;details&lt;/a&gt;.&lt;/li&gt;
+ &lt;li&gt;Support for pre-0.1.0 git-cinnabar repositories was removed. You must first&lt;br /&gt;
+ use a git-cinnabar version between 0.1.0 and 0.2.2 to upgrade its metadata.&lt;/li&gt;
+ &lt;li&gt;It is now possible to attach/graft git-cinnabar metadata to existing commits&lt;br /&gt;
+ matching mercurial changesets. This allows to migrate from some other&lt;br /&gt;
+ hg-to-git tool to git-cinnabar while preserving the existing git commits.&lt;br /&gt;
+ See &lt;a href=&quot;http://glandium.org/blog/Mozilla%3A-Using-a-git-clone-of-gecko%E2%80%90dev-to-push-to-mercurial&quot;&gt;an example of how this works with the git clone of the Gecko mercurial&lt;br /&gt;
+ repository&lt;/a&gt;
+ &lt;/li&gt;
+ &lt;li&gt;Avoid mercurial printing its progress bar, messing up with git-cinnabar’s&lt;br /&gt;
+ output.&lt;/li&gt;
+ &lt;li&gt;It is now possible to fetch from an incremental mercurial bundle (without&lt;br /&gt;
+ a root changeset).&lt;/li&gt;
+ &lt;li&gt;It is now possible to push to a new mercurial repository without &lt;code&gt;-f&lt;/code&gt;.&lt;/li&gt;
+ &lt;li&gt;By default, reject pushing a new root to a mercurial repository.&lt;/li&gt;
+ &lt;li&gt;Make the connection to a mercurial repository through ssh respect the&lt;br /&gt;
+ &lt;code&gt;GIT_SSH&lt;/code&gt; and &lt;code&gt;GIT_SSH_COMMAND&lt;/code&gt; environment variables.&lt;/li&gt;
+ &lt;li&gt;
+ &lt;code&gt;git cinnabar&lt;/code&gt; now has a proper argument parser for all its subcommands.&lt;/li&gt;
+ &lt;li&gt;
+ &lt;/li&gt;
+ &lt;li&gt;A new &lt;code&gt;git cinnabar python&lt;/code&gt; command allows to run python scripts or open a&lt;br /&gt;
+ python shell with the right sys.path to import the cinnabar module.&lt;/li&gt;
+ &lt;li&gt;All git-cinnabar metadata is now kept under a single ref (although for&lt;br /&gt;
+ convenience, other refs are created, but they can be derived if necessary).&lt;/li&gt;
+ &lt;li&gt;Consequently, a new &lt;code&gt;git cinnabar rollback&lt;/code&gt; command allows to roll back to&lt;br /&gt;
+ previous metadata states.&lt;/li&gt;
+ &lt;li&gt;git-cinnabar metadata now tracks the manifests DAG.&lt;/li&gt;
+ &lt;li&gt;A new &lt;code&gt;git cinnabar bundle&lt;/code&gt; command allows to create mercurial bundles,&lt;br /&gt;
+ mostly for debugging purposes, without requiring to hit a mercurial server.&lt;/li&gt;
+ &lt;li&gt;Updated git to 2.7.0 for the native helper.&lt;/li&gt;
+ &lt;/ul&gt;
+ &lt;h3&gt;Development process changes&lt;/h3&gt;
+ &lt;p&gt;Up to before this release closing in, the &lt;code&gt;master&lt;/code&gt; branch was dedicated to&lt;br /&gt;
+ releases, and development was happening on the &lt;code&gt;next&lt;/code&gt; branch, until a new&lt;br /&gt;
+ release happens.&lt;/p&gt;
+ &lt;p&gt;From now on, the &lt;code&gt;release&lt;/code&gt; branch will take dot-release fixes and new&lt;br /&gt;
+ releases, while the &lt;code&gt;master&lt;/code&gt; branch will receive all changes that are&lt;br /&gt;
+ validated through testing (currently semi-automatically tested with&lt;br /&gt;
+ out-of-tree tests based on four real-life mercurial repositories, with&lt;br /&gt;
+ some automated CI based on in-tree tests used in the future).&lt;/p&gt;
+ &lt;p&gt;The &lt;code&gt;next&lt;/code&gt; branch will receive changes to be tested in CI when things&lt;br /&gt;
+ will be hooked up, and may have rewritten history as a consequence of&lt;br /&gt;
+ wanting passing tests on every commit on &lt;code&gt;master&lt;/code&gt;.&lt;/p&gt;</description>
+ <pubDate>Fri, 15 Jan 2016 08:56:40 +0000</pubDate>
+ <dc:creator>glandium</dc:creator>
+ </item>
+ <item>
+ <title>Air Mozilla: Web QA Weekly Meeting, 14 Jan 2016</title>
+ <guid isPermaLink="true">https://air.mozilla.org/web-qa-weekly-meeting-20160114/</guid>
+ <link>https://air.mozilla.org/web-qa-weekly-meeting-20160114/</link>
+ <description>&lt;p&gt;
+ &lt;img alt=&quot;Web QA Weekly Meeting&quot; class=&quot;wp-post-image&quot; height=&quot;90&quot; src=&quot;https://air.cdn.mozilla.net/media/cache/f5/13/f5137857516694df0458e837c2d3a4be.png&quot; width=&quot;160&quot; /&gt;
+ This is our weekly gathering of Mozilla'a Web QA team filled with discussion on our current and future projects, ideas, demos, and fun facts.
+ &lt;/p&gt;</description>
+ <pubDate>Thu, 14 Jan 2016 17:00:00 +0000</pubDate>
+ <dc:creator>Air Mozilla</dc:creator>
+ </item>
+
+ </channel>
+</rss>
diff --git a/mobile/android/tests/background/junit4/resources/feed_rss_heise.xml b/mobile/android/tests/background/junit4/resources/feed_rss_heise.xml
new file mode 100644
index 0000000000..a23399bb85
--- /dev/null
+++ b/mobile/android/tests/background/junit4/resources/feed_rss_heise.xml
@@ -0,0 +1,1965 @@
+<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet type='text/xsl' href='http://heise.de.feedsportal.com/xsl/de/rss.xsl'?>
+<rss xmlns:content="http://purl.org/rss/1.0/modules/content/"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" version="2.0">
+ <channel>
+ <title>heise online News</title>
+ <link>http://www.heise.de/newsticker/</link>
+ <description>Nachrichten nicht nur aus der Welt der Computer</description>
+ <language>en</language>
+ <copyright>Copyright (c) 2016 Heise Medien</copyright>
+ <pubDate>Wed, 28 Jan 2016 17:36:00 GMT</pubDate>
+ <lastBuildDate>Wed, 27 Jan 2016 17:32:00 GMT</lastBuildDate>
+ <ttl>5</ttl>
+ <item>
+ <title>Google: “Dramatische Verbesserungen†für Chrome in iOS</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Google-Dramatische-Verbesserungen-fuer-Chrome-in-iOS-3085808.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Als Unterbau von iOS-Chrome kommt nun eine neuere, von Apple
+ bereitgestellte Rendering-Engine zum Einsatz, die ohne Leistungseinschränkungen
+ läuft. Manche Funktionen fallen dadurch allerdings weg.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389546558/u/0/f/653902/c/35207/s/4d2c8260/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389546558/u/0/f/653902/c/35207/s/4d2c8260/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389546558/u/0/f/653902/c/35207/s/4d2c8260/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389546558/u/0/f/653902/c/35207/s/4d2c8260/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389546558/u/0/f/653902/c/35207/s/4d2c8260/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389546558/u/0/f/653902/c/35207/s/4d2c8260/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389546558/u/0/f/653902/c/35207/s/4d2c8260/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389546558/u/0/f/653902/c/35207/s/4d2c8260/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389546558/u/0/f/653902/c/35207/s/4d2c8260/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2c8260/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 17:32:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085808</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Google-Dramatische-Verbesserungen-fuer-Chrome-in-iOS-3085808.html?wt_mc=rss.ho.beitrag.atom" title="Google: “Dramatische Verbesserungen†für Chrome in iOS"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/8/2/1/9/image-15fb016575d97662.jpeg" alt="Google Chrome für iOS" /> </a> <p>Als Unterbau von iOS-Chrome kommt nun eine neuere, von Apple bereitgestellte Rendering-Engine zum Einsatz, die ohne Leistungseinschränkungen läuft. Manche Funktionen fallen dadurch allerdings weg.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389546558/u/0/f/653902/c/35207/s/4d2c8260/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389546558/u/0/f/653902/c/35207/s/4d2c8260/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389546558/u/0/f/653902/c/35207/s/4d2c8260/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389546558/u/0/f/653902/c/35207/s/4d2c8260/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389546558/u/0/f/653902/c/35207/s/4d2c8260/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389546558/u/0/f/653902/c/35207/s/4d2c8260/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389546558/u/0/f/653902/c/35207/s/4d2c8260/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389546558/u/0/f/653902/c/35207/s/4d2c8260/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389546558/u/0/f/653902/c/35207/s/4d2c8260/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2c8260/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>IBM holt Ford-Chef in seinen Verwaltungsrat</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/IBM-holt-Ford-Chef-in-seinen-Verwaltungsrat-3085806.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Mark Fields, der seit 2014 den zweitgrößten US-amerikanischen
+ Autohersteller leitet, gehört nun dem IBM-Verwaltungsrat an.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389544988/u/0/f/653902/c/35207/s/4d2c54c4/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389544988/u/0/f/653902/c/35207/s/4d2c54c4/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389544988/u/0/f/653902/c/35207/s/4d2c54c4/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389544988/u/0/f/653902/c/35207/s/4d2c54c4/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389544988/u/0/f/653902/c/35207/s/4d2c54c4/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389544988/u/0/f/653902/c/35207/s/4d2c54c4/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389544988/u/0/f/653902/c/35207/s/4d2c54c4/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389544988/u/0/f/653902/c/35207/s/4d2c54c4/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389544988/u/0/f/653902/c/35207/s/4d2c54c4/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2c54c4/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 17:19:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085806</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/IBM-holt-Ford-Chef-in-seinen-Verwaltungsrat-3085806.html?wt_mc=rss.ho.beitrag.atom" title="IBM holt Ford-Chef in seinen Verwaltungsrat"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/8/2/1/7/MARK-FIELDS_SV3_4814-8dc07e7cdcc86a8f.jpeg" alt="IBM holt Ford-Chef in seinen Verwaltungsrat" /> </a> <p>Mark Fields, der seit 2014 den zweitgrößten US-amerikanischen Autohersteller leitet, gehört nun dem IBM-Verwaltungsrat an.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389544988/u/0/f/653902/c/35207/s/4d2c54c4/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389544988/u/0/f/653902/c/35207/s/4d2c54c4/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389544988/u/0/f/653902/c/35207/s/4d2c54c4/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389544988/u/0/f/653902/c/35207/s/4d2c54c4/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389544988/u/0/f/653902/c/35207/s/4d2c54c4/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389544988/u/0/f/653902/c/35207/s/4d2c54c4/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389544988/u/0/f/653902/c/35207/s/4d2c54c4/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389544988/u/0/f/653902/c/35207/s/4d2c54c4/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389544988/u/0/f/653902/c/35207/s/4d2c54c4/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2c54c4/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>News Pro: Microsoft bringt Nachrichten-App fürs iPhone</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/News-Pro-Microsoft-bringt-Nachrichten-App-fuers-iPhone-3085776.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Microsoft setzt besonders auf geschäftliche Nachrichten, die nach Branchen
+ sortiert sind. News Pro soll außerdem interessenbasierte Vorschläge unterbreiten.
+ Noch läuft die App als experimentelles Projekt.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389583008/u/0/f/653902/c/35207/s/4d2c039d/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389583008/u/0/f/653902/c/35207/s/4d2c039d/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389583008/u/0/f/653902/c/35207/s/4d2c039d/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389583008/u/0/f/653902/c/35207/s/4d2c039d/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389583008/u/0/f/653902/c/35207/s/4d2c039d/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389583008/u/0/f/653902/c/35207/s/4d2c039d/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389583008/u/0/f/653902/c/35207/s/4d2c039d/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389583008/u/0/f/653902/c/35207/s/4d2c039d/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389583008/u/0/f/653902/c/35207/s/4d2c039d/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2c039d/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 16:20:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085776</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/News-Pro-Microsoft-bringt-Nachrichten-App-fuers-iPhone-3085776.html?wt_mc=rss.ho.beitrag.atom" title="News Pro: Microsoft bringt Nachrichten-App fürs iPhone"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/8/1/9/1/Bildschirmfoto_2016-01-27_um_16-5f6322b07d567c2e.png" alt="News Pro" /> </a> <p>Microsoft setzt besonders auf geschäftliche Nachrichten, die nach Branchen sortiert sind. News Pro soll außerdem interessenbasierte Vorschläge unterbreiten. Noch läuft die App als experimentelles Projekt.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389583008/u/0/f/653902/c/35207/s/4d2c039d/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389583008/u/0/f/653902/c/35207/s/4d2c039d/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389583008/u/0/f/653902/c/35207/s/4d2c039d/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389583008/u/0/f/653902/c/35207/s/4d2c039d/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389583008/u/0/f/653902/c/35207/s/4d2c039d/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389583008/u/0/f/653902/c/35207/s/4d2c039d/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389583008/u/0/f/653902/c/35207/s/4d2c039d/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389583008/u/0/f/653902/c/35207/s/4d2c039d/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389583008/u/0/f/653902/c/35207/s/4d2c039d/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2c039d/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Supercomputer mit Xeon Phi Knights Landing wird im Frühsommer ausgeliefert
+ </title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Supercomputer-mit-Xeon-Phi-Knights-Landing-wird-im-Fruehsommer-ausgeliefert-3085672.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Im Juni soll Cray XC40-Systeme mit Xeon-E5 und Xeon Phi Knight Landing an
+ den britischen Wetterdienst liefern.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389581460/u/0/f/653902/c/35207/s/4d2bfd33/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389581460/u/0/f/653902/c/35207/s/4d2bfd33/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389581460/u/0/f/653902/c/35207/s/4d2bfd33/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389581460/u/0/f/653902/c/35207/s/4d2bfd33/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389581460/u/0/f/653902/c/35207/s/4d2bfd33/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389581460/u/0/f/653902/c/35207/s/4d2bfd33/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389581460/u/0/f/653902/c/35207/s/4d2bfd33/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389581460/u/0/f/653902/c/35207/s/4d2bfd33/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389581460/u/0/f/653902/c/35207/s/4d2bfd33/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2bfd33/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 15:58:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085672</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Supercomputer-mit-Xeon-Phi-Knights-Landing-wird-im-Fruehsommer-ausgeliefert-3085672.html?wt_mc=rss.ho.beitrag.atom" title="Supercomputer mit Xeon Phi Knights Landing wird im Frühsommer ausgeliefert"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/8/0/9/7/Cray_xc_40-d877918c20ec9586.jpeg" alt="Supercomputer mit Xeon Phi Knights Landing im Frühsommer" /> </a> <p>Im Juni soll Cray XC40-Systeme mit Xeon-E5 und Xeon Phi Knight Landing an den britischen Wetterdienst liefern.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389581460/u/0/f/653902/c/35207/s/4d2bfd33/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389581460/u/0/f/653902/c/35207/s/4d2bfd33/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389581460/u/0/f/653902/c/35207/s/4d2bfd33/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389581460/u/0/f/653902/c/35207/s/4d2bfd33/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389581460/u/0/f/653902/c/35207/s/4d2bfd33/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389581460/u/0/f/653902/c/35207/s/4d2bfd33/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389581460/u/0/f/653902/c/35207/s/4d2bfd33/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389581460/u/0/f/653902/c/35207/s/4d2bfd33/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389581460/u/0/f/653902/c/35207/s/4d2bfd33/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2bfd33/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>FDP reicht Verfassungsklage gegen Vorratsdatenspeicherung ein</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/FDP-reicht-Verfassungsklage-gegen-Vorratsdatenspeicherung-ein-3085660.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>&amp;quot;Dieser Angriff auf die Bürgerrechte darf nicht akzeptiert werden&amp;quot;,
+ kommentierte der FDP-Bundesvize Wolfgang Kubicki seinen Gang nach Karlsruhe.&lt;br
+ clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389571260/u/0/f/653902/c/35207/s/4d2bee54/sc/7/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389571260/u/0/f/653902/c/35207/s/4d2bee54/sc/7/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389571260/u/0/f/653902/c/35207/s/4d2bee54/sc/7/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389571260/u/0/f/653902/c/35207/s/4d2bee54/sc/7/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389571260/u/0/f/653902/c/35207/s/4d2bee54/sc/7/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389571260/u/0/f/653902/c/35207/s/4d2bee54/sc/7/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389571260/u/0/f/653902/c/35207/s/4d2bee54/sc/7/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389571260/u/0/f/653902/c/35207/s/4d2bee54/sc/7/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389571260/u/0/f/653902/c/35207/s/4d2bee54/sc/7/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2bee54/sc/7/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 15:51:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085660</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/FDP-reicht-Verfassungsklage-gegen-Vorratsdatenspeicherung-ein-3085660.html?wt_mc=rss.ho.beitrag.atom" title="FDP reicht Verfassungsklage gegen Vorratsdatenspeicherung ein"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/8/0/8/5/bawue652x368-0b25c1bfcd112865.jpeg" alt="FDP reicht Verfassungsklage gegen Vorratsdatenspeicherung ein" /> </a> <p>"Dieser Angriff auf die Bürgerrechte darf nicht akzeptiert werden", kommentierte der FDP-Bundesvize Wolfgang Kubicki seinen Gang nach Karlsruhe.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389571260/u/0/f/653902/c/35207/s/4d2bee54/sc/7/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389571260/u/0/f/653902/c/35207/s/4d2bee54/sc/7/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389571260/u/0/f/653902/c/35207/s/4d2bee54/sc/7/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389571260/u/0/f/653902/c/35207/s/4d2bee54/sc/7/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389571260/u/0/f/653902/c/35207/s/4d2bee54/sc/7/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389571260/u/0/f/653902/c/35207/s/4d2bee54/sc/7/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389571260/u/0/f/653902/c/35207/s/4d2bee54/sc/7/a2.htm"><img src="http://da.feedsportal.com/r/247389571260/u/0/f/653902/c/35207/s/4d2bee54/sc/7/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389571260/u/0/f/653902/c/35207/s/4d2bee54/sc/7/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2bee54/sc/7/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Adblocker werden immer populärer</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Adblocker-werden-immer-populaerer-3085744.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Zwei aktuelle Studien zeigen: Werbeblocker werden immer populärer. Gerade
+ mobil wollen Nutzer weniger Werbung sehen. Ausgerechnet ein Anbieter von
+ Videowerbung sieht sich als Musterbeispiel.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389571259/u/0/f/653902/c/35207/s/4d2bee52/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389571259/u/0/f/653902/c/35207/s/4d2bee52/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389571259/u/0/f/653902/c/35207/s/4d2bee52/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389571259/u/0/f/653902/c/35207/s/4d2bee52/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389571259/u/0/f/653902/c/35207/s/4d2bee52/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389571259/u/0/f/653902/c/35207/s/4d2bee52/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389571259/u/0/f/653902/c/35207/s/4d2bee52/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389571259/u/0/f/653902/c/35207/s/4d2bee52/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389571259/u/0/f/653902/c/35207/s/4d2bee52/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2bee52/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 15:49:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085744</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Adblocker-werden-immer-populaerer-3085744.html?wt_mc=rss.ho.beitrag.atom" title="Adblocker werden immer populärer"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/8/1/6/1/image-144229995483704-6d7835c4aaf4c6f4-53beb923785f5926-27ea36c432306085-4339c9834103f557.jpeg" alt="Adblocker immer populärer" /> </a> <p>Zwei aktuelle Studien zeigen: Werbeblocker werden immer populärer. Gerade mobil wollen Nutzer weniger Werbung sehen. Ausgerechnet ein Anbieter von Videowerbung sieht sich als Musterbeispiel. </p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389571259/u/0/f/653902/c/35207/s/4d2bee52/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389571259/u/0/f/653902/c/35207/s/4d2bee52/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389571259/u/0/f/653902/c/35207/s/4d2bee52/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389571259/u/0/f/653902/c/35207/s/4d2bee52/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389571259/u/0/f/653902/c/35207/s/4d2bee52/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389571259/u/0/f/653902/c/35207/s/4d2bee52/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389571259/u/0/f/653902/c/35207/s/4d2bee52/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389571259/u/0/f/653902/c/35207/s/4d2bee52/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389571259/u/0/f/653902/c/35207/s/4d2bee52/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2bee52/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Steuererklärung: Bundesfinanzhof bleibt beim häuslichen Arbeitszimmer hart
+ </title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Steuererklaerung-Bundesfinanzhof-bleibt-beim-haeuslichen-Arbeitszimmer-hart-3085652.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Der Bundesfinanzhof hält in einem Grundsatzurteil an den strengen Vorgaben
+ für die Absetzbarkeit des häuslichen Arbeitszimmers fest. Ein nur zeitweise für die
+ Arbeit genutzter Raum wird nicht anerkannt.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389578312/u/0/f/653902/c/35207/s/4d2bb5a4/sc/17/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389578312/u/0/f/653902/c/35207/s/4d2bb5a4/sc/17/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389578312/u/0/f/653902/c/35207/s/4d2bb5a4/sc/17/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389578312/u/0/f/653902/c/35207/s/4d2bb5a4/sc/17/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389578312/u/0/f/653902/c/35207/s/4d2bb5a4/sc/17/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389578312/u/0/f/653902/c/35207/s/4d2bb5a4/sc/17/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389578312/u/0/f/653902/c/35207/s/4d2bb5a4/sc/17/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389578312/u/0/f/653902/c/35207/s/4d2bb5a4/sc/17/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389578312/u/0/f/653902/c/35207/s/4d2bb5a4/sc/17/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2bb5a4/sc/17/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 15:37:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085652</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Steuererklaerung-Bundesfinanzhof-bleibt-beim-haeuslichen-Arbeitszimmer-hart-3085652.html?wt_mc=rss.ho.beitrag.atom" title="Steuererklärung: Bundesfinanzhof bleibt beim häuslichen Arbeitszimmer hart"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/8/0/7/7/home-office-picjumbo-e584e512dfc90c71.jpeg" alt="Steuererklärung: Bundesfinanzhof bleibt beim Arbeitszimmer hart" /> </a> <p>Der Bundesfinanzhof hält in einem Grundsatzurteil an den strengen Vorgaben für die Absetzbarkeit des häuslichen Arbeitszimmers fest. Ein nur zeitweise für die Arbeit genutzter Raum wird nicht anerkannt.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389578312/u/0/f/653902/c/35207/s/4d2bb5a4/sc/17/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389578312/u/0/f/653902/c/35207/s/4d2bb5a4/sc/17/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389578312/u/0/f/653902/c/35207/s/4d2bb5a4/sc/17/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389578312/u/0/f/653902/c/35207/s/4d2bb5a4/sc/17/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389578312/u/0/f/653902/c/35207/s/4d2bb5a4/sc/17/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389578312/u/0/f/653902/c/35207/s/4d2bb5a4/sc/17/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389578312/u/0/f/653902/c/35207/s/4d2bb5a4/sc/17/a2.htm"><img src="http://da.feedsportal.com/r/247389578312/u/0/f/653902/c/35207/s/4d2bb5a4/sc/17/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389578312/u/0/f/653902/c/35207/s/4d2bb5a4/sc/17/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2bb5a4/sc/17/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Olympus Pen F: Erster Eindruck von der spiegellosen Edel-Systemkamera</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Olympus-Pen-F-Erster-Eindruck-von-der-spiegellosen-Edel-Systemkamera-3085217.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Olympus möchte seine neue Pen F über das Lebensgefühl der 60er Jahre
+ verkaufen. Die spiegellose Systemkamera wirkt extrem durchgestylt und will
+ gleichzeitig ein mächtiges Werkzeug sein.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389561062/u/0/f/653902/c/35207/s/4d2b625e/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389561062/u/0/f/653902/c/35207/s/4d2b625e/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389561062/u/0/f/653902/c/35207/s/4d2b625e/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389561062/u/0/f/653902/c/35207/s/4d2b625e/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389561062/u/0/f/653902/c/35207/s/4d2b625e/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389561062/u/0/f/653902/c/35207/s/4d2b625e/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389561062/u/0/f/653902/c/35207/s/4d2b625e/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389561062/u/0/f/653902/c/35207/s/4d2b625e/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389561062/u/0/f/653902/c/35207/s/4d2b625e/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2b625e/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 14:18:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085217</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Olympus-Pen-F-Erster-Eindruck-von-der-spiegellosen-Edel-Systemkamera-3085217.html?wt_mc=rss.ho.beitrag.atom" title="Olympus Pen F: Erster Eindruck von der spiegellosen Edel-Systemkamera"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/7/6/9/PENF0071-aa9642e8826de713.jpeg" alt="Olympus Pen F" /> </a> <p>Olympus möchte seine neue Pen F über das Lebensgefühl der 60er Jahre verkaufen. Die spiegellose Systemkamera wirkt extrem durchgestylt und will gleichzeitig ein mächtiges Werkzeug sein. </p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389561062/u/0/f/653902/c/35207/s/4d2b625e/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389561062/u/0/f/653902/c/35207/s/4d2b625e/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389561062/u/0/f/653902/c/35207/s/4d2b625e/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389561062/u/0/f/653902/c/35207/s/4d2b625e/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389561062/u/0/f/653902/c/35207/s/4d2b625e/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389561062/u/0/f/653902/c/35207/s/4d2b625e/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389561062/u/0/f/653902/c/35207/s/4d2b625e/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389561062/u/0/f/653902/c/35207/s/4d2b625e/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389561062/u/0/f/653902/c/35207/s/4d2b625e/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2b625e/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>VW-Skandal: EU-Kommission verschärft Kfz-Aufsicht und Abgaskontrolle</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/VW-Skandal-EU-Kommission-verschaerft-Kfz-Aufsicht-und-Abgaskontrolle-3085529.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Per Verordnung will die EU-Kommission erreichen, dass sich
+ Automobilhersteller streng an die geltenden Sicherheits-, Umwelt- und
+ Fertigungsanforderungen halten. Softwareprotokolle für Kfz sollen zugänglich werden.&lt;br
+ clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389561061/u/0/f/653902/c/35207/s/4d2b625d/sc/3/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389561061/u/0/f/653902/c/35207/s/4d2b625d/sc/3/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389561061/u/0/f/653902/c/35207/s/4d2b625d/sc/3/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389561061/u/0/f/653902/c/35207/s/4d2b625d/sc/3/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389561061/u/0/f/653902/c/35207/s/4d2b625d/sc/3/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389561061/u/0/f/653902/c/35207/s/4d2b625d/sc/3/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389561061/u/0/f/653902/c/35207/s/4d2b625d/sc/3/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389561061/u/0/f/653902/c/35207/s/4d2b625d/sc/3/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389561061/u/0/f/653902/c/35207/s/4d2b625d/sc/3/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2b625d/sc/3/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 14:13:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085529</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/VW-Skandal-EU-Kommission-verschaerft-Kfz-Aufsicht-und-Abgaskontrolle-3085529.html?wt_mc=rss.ho.beitrag.atom" title="VW-Skandal: EU-Kommission verschärft Kfz-Aufsicht und Abgaskontrolle"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/9/9/9/41fec8622800cd459fa6eef48c5e80e2v1_max_755x425_b3535db83dc50e27c1bb1392364c95a2-ac47a5e9f14564b3.jpeg" alt="VW-Skandal: EU-Kommission verschärft Kfz-Aufsicht und Abgaskontrolle" /> </a> <p>Per Verordnung will die EU-Kommission erreichen, dass sich Automobilhersteller streng an die geltenden Sicherheits-, Umwelt- und Fertigungsanforderungen halten. Softwareprotokolle für Kfz sollen zugänglich werden.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389561061/u/0/f/653902/c/35207/s/4d2b625d/sc/3/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389561061/u/0/f/653902/c/35207/s/4d2b625d/sc/3/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389561061/u/0/f/653902/c/35207/s/4d2b625d/sc/3/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389561061/u/0/f/653902/c/35207/s/4d2b625d/sc/3/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389561061/u/0/f/653902/c/35207/s/4d2b625d/sc/3/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389561061/u/0/f/653902/c/35207/s/4d2b625d/sc/3/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389561061/u/0/f/653902/c/35207/s/4d2b625d/sc/3/a2.htm"><img src="http://da.feedsportal.com/r/247389561061/u/0/f/653902/c/35207/s/4d2b625d/sc/3/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389561061/u/0/f/653902/c/35207/s/4d2b625d/sc/3/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2b625d/sc/3/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>PC-Version von Rise of the Tomb Raider: Die schärfste Lara Croft</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/PC-Version-von-Rise-of-the-Tomb-Raider-Die-schaerfste-Lara-Croft-3084870.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Am 28. Januar erscheint die PC-Version von Rise of the Tomb Raider. Sie
+ besticht durch tolle Grafik und flüssige Bildraten. Allerdings gibt es auch ein paar
+ Probleme. heise online hat die PC-Version angetestet.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389561060/u/0/f/653902/c/35207/s/4d2b625c/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389561060/u/0/f/653902/c/35207/s/4d2b625c/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389561060/u/0/f/653902/c/35207/s/4d2b625c/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389561060/u/0/f/653902/c/35207/s/4d2b625c/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389561060/u/0/f/653902/c/35207/s/4d2b625c/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389561060/u/0/f/653902/c/35207/s/4d2b625c/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389561060/u/0/f/653902/c/35207/s/4d2b625c/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389561060/u/0/f/653902/c/35207/s/4d2b625c/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389561060/u/0/f/653902/c/35207/s/4d2b625c/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2b625c/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 14:09:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084870</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/PC-Version-von-Rise-of-the-Tomb-Raider-Die-schaerfste-Lara-Croft-3084870.html?wt_mc=rss.ho.beitrag.atom" title="PC-Version von Rise of the Tomb Raider: Die schärfste Lara Croft"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/5/2/2/2016-01-25_00088-6b74693f3821a422.jpeg" alt="Lara Croft" /> </a> <p>Am 28. Januar erscheint die PC-Version von Rise of the Tomb Raider. Sie besticht durch tolle Grafik und flüssige Bildraten. Allerdings gibt es auch ein paar Probleme. heise online hat die PC-Version angetestet.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389561060/u/0/f/653902/c/35207/s/4d2b625c/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389561060/u/0/f/653902/c/35207/s/4d2b625c/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389561060/u/0/f/653902/c/35207/s/4d2b625c/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389561060/u/0/f/653902/c/35207/s/4d2b625c/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389561060/u/0/f/653902/c/35207/s/4d2b625c/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389561060/u/0/f/653902/c/35207/s/4d2b625c/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389561060/u/0/f/653902/c/35207/s/4d2b625c/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389561060/u/0/f/653902/c/35207/s/4d2b625c/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389561060/u/0/f/653902/c/35207/s/4d2b625c/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2b625c/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>TP-Link-Router mit vorhersehbarem Standard-WLAN-Passwort</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/TP-Link-Router-mit-vorhersehbarem-Standard-WLAN-Passwort-3085482.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Angreifer können das werkseitige WLAN-Passwort von einer
+ TP-Link-Router-Serie vergleichsweise einfach herausfinden und sich so Zugang zum
+ Netzwerk verschaffen. Weitere Serien könnten ebenfalls betroffen sein.&lt;br
+ clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389559600/u/0/f/653902/c/35207/s/4d2b5c3d/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389559600/u/0/f/653902/c/35207/s/4d2b5c3d/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389559600/u/0/f/653902/c/35207/s/4d2b5c3d/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389559600/u/0/f/653902/c/35207/s/4d2b5c3d/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389559600/u/0/f/653902/c/35207/s/4d2b5c3d/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389559600/u/0/f/653902/c/35207/s/4d2b5c3d/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389559600/u/0/f/653902/c/35207/s/4d2b5c3d/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389559600/u/0/f/653902/c/35207/s/4d2b5c3d/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389559600/u/0/f/653902/c/35207/s/4d2b5c3d/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2b5c3d/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 14:06:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085482</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/TP-Link-Router-mit-vorhersehbarem-Standard-WLAN-Passwort-3085482.html?wt_mc=rss.ho.beitrag.atom" title="TP-Link-Router mit vorhersehbarem Standard-WLAN-Passwort"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/9/6/8/urn-newsml-dpa-com-20090101-150813-99-05231_large_4_3-fb565ab96dbaaf32.jpeg" alt="Hacker" /> </a> <p>Angreifer können das werkseitige WLAN-Passwort von einer TP-Link-Router-Serie vergleichsweise einfach herausfinden und sich so Zugang zum Netzwerk verschaffen. Weitere Serien könnten ebenfalls betroffen sein.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389559600/u/0/f/653902/c/35207/s/4d2b5c3d/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389559600/u/0/f/653902/c/35207/s/4d2b5c3d/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389559600/u/0/f/653902/c/35207/s/4d2b5c3d/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389559600/u/0/f/653902/c/35207/s/4d2b5c3d/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389559600/u/0/f/653902/c/35207/s/4d2b5c3d/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389559600/u/0/f/653902/c/35207/s/4d2b5c3d/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389559600/u/0/f/653902/c/35207/s/4d2b5c3d/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389559600/u/0/f/653902/c/35207/s/4d2b5c3d/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389559600/u/0/f/653902/c/35207/s/4d2b5c3d/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2b5c3d/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Tails 2.0: Das Anonymisierungs-OS im neuen Look</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Tails-2-0-Das-Anonymisierungs-OS-im-neuen-Look-3085312.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Die neueste Version der spezialisierten Linux-Distribution basiert auf
+ Debian Jessie und bringt Gnome Shell als neuen Desktop mit.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389521976/u/0/f/653902/c/35207/s/4d2b0a56/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389521976/u/0/f/653902/c/35207/s/4d2b0a56/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389521976/u/0/f/653902/c/35207/s/4d2b0a56/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389521976/u/0/f/653902/c/35207/s/4d2b0a56/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389521976/u/0/f/653902/c/35207/s/4d2b0a56/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389521976/u/0/f/653902/c/35207/s/4d2b0a56/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389521976/u/0/f/653902/c/35207/s/4d2b0a56/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389521976/u/0/f/653902/c/35207/s/4d2b0a56/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389521976/u/0/f/653902/c/35207/s/4d2b0a56/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2b0a56/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 13:47:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085312</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Tails-2-0-Das-Anonymisierungs-OS-im-neuen-Look-3085312.html?wt_mc=rss.ho.beitrag.atom" title="Tails 2.0: Das Anonymisierungs-OS im neuen Look"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/8/5/1/tails-2-3034d3a9e6bf582e.jpeg" alt="Tails 2.0" /> </a> <p>Die neueste Version der spezialisierten Linux-Distribution basiert auf Debian Jessie und bringt Gnome Shell als neuen Desktop mit.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389521976/u/0/f/653902/c/35207/s/4d2b0a56/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389521976/u/0/f/653902/c/35207/s/4d2b0a56/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389521976/u/0/f/653902/c/35207/s/4d2b0a56/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389521976/u/0/f/653902/c/35207/s/4d2b0a56/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389521976/u/0/f/653902/c/35207/s/4d2b0a56/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389521976/u/0/f/653902/c/35207/s/4d2b0a56/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389521976/u/0/f/653902/c/35207/s/4d2b0a56/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389521976/u/0/f/653902/c/35207/s/4d2b0a56/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389521976/u/0/f/653902/c/35207/s/4d2b0a56/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2b0a56/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>US-Fotograf Steve McCurry: "Die Selfie-Generation ist cool"</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/US-Fotograf-Steve-McCurry-Die-Selfie-Generation-ist-cool-3085412.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Der amerikanische Fotograf Steve McCurry ist für seine Aufnahme des &amp;quot;afghanischen
+ Mädchens&amp;quot;, welches auf dem Cover der National Geographic um die Welt ging,
+ bekannt. Nun sprach der 65-Jährige mit der Zeitung &amp;quot;Times of India&amp;quot;
+ unter anderem über Selfies.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389521975/u/0/f/653902/c/35207/s/4d2b0a54/sc/17/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389521975/u/0/f/653902/c/35207/s/4d2b0a54/sc/17/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389521975/u/0/f/653902/c/35207/s/4d2b0a54/sc/17/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389521975/u/0/f/653902/c/35207/s/4d2b0a54/sc/17/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389521975/u/0/f/653902/c/35207/s/4d2b0a54/sc/17/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389521975/u/0/f/653902/c/35207/s/4d2b0a54/sc/17/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389521975/u/0/f/653902/c/35207/s/4d2b0a54/sc/17/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389521975/u/0/f/653902/c/35207/s/4d2b0a54/sc/17/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389521975/u/0/f/653902/c/35207/s/4d2b0a54/sc/17/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2b0a54/sc/17/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 13:38:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085412</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/US-Fotograf-Steve-McCurry-Die-Selfie-Generation-ist-cool-3085412.html?wt_mc=rss.ho.beitrag.atom" title="US-Fotograf Steve McCurry: &quot;Die Selfie-Generation ist cool&quot;"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/9/2/0/selfie_Bild-3258a76bab7f85ec.png" alt="Selfie" /> </a> <p>Der amerikanische Fotograf Steve McCurry ist für seine Aufnahme des "afghanischen Mädchens", welches auf dem Cover der National Geographic um die Welt ging, bekannt. Nun sprach der 65-Jährige mit der Zeitung "Times of India" unter anderem über Selfies.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389521975/u/0/f/653902/c/35207/s/4d2b0a54/sc/17/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389521975/u/0/f/653902/c/35207/s/4d2b0a54/sc/17/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389521975/u/0/f/653902/c/35207/s/4d2b0a54/sc/17/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389521975/u/0/f/653902/c/35207/s/4d2b0a54/sc/17/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389521975/u/0/f/653902/c/35207/s/4d2b0a54/sc/17/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389521975/u/0/f/653902/c/35207/s/4d2b0a54/sc/17/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389521975/u/0/f/653902/c/35207/s/4d2b0a54/sc/17/a2.htm"><img src="http://da.feedsportal.com/r/247389521975/u/0/f/653902/c/35207/s/4d2b0a54/sc/17/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389521975/u/0/f/653902/c/35207/s/4d2b0a54/sc/17/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2b0a54/sc/17/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>AMDs GPUOpen: Zahlreiche SDKs und Tools auf GitHub im Source verfügbar</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/AMDs-GPUOpen-Zahlreiche-SDKs-und-Tools-auf-GitHub-im-Source-verfuegbar-3085309.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>AMDs Open-Source-Initiative spricht zum einen Spieleentwickler und zum
+ anderen Entwickler von HPC-Anwendungen an. Der Chiphersteller veröffentlicht nun
+ zahlreiche SDKs und Werkzeuge auf GitHub.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389556077/u/0/f/653902/c/35207/s/4d2b26a0/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389556077/u/0/f/653902/c/35207/s/4d2b26a0/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389556077/u/0/f/653902/c/35207/s/4d2b26a0/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389556077/u/0/f/653902/c/35207/s/4d2b26a0/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389556077/u/0/f/653902/c/35207/s/4d2b26a0/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389556077/u/0/f/653902/c/35207/s/4d2b26a0/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389556077/u/0/f/653902/c/35207/s/4d2b26a0/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389556077/u/0/f/653902/c/35207/s/4d2b26a0/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389556077/u/0/f/653902/c/35207/s/4d2b26a0/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2b26a0/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 13:36:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085309</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/AMDs-GPUOpen-Zahlreiche-SDKs-und-Tools-auf-GitHub-im-Source-verfuegbar-3085309.html?wt_mc=rss.ho.beitrag.atom" title="AMDs GPUOpen: Zahlreiche SDKs und Tools auf GitHub im Source verfügbar"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/8/4/8/aufmacher_ajax_7-7ba40e6cf8bbc8b9.jpeg" alt="AMDs GPUOpen: Zahlreiche SDKs und Tools auf GitHub im Source verfügbar" /> </a> <p>AMDs Open-Source-Initiative spricht zum einen Spieleentwickler und zum anderen Entwickler von HPC-Anwendungen an. Der Chiphersteller veröffentlicht nun zahlreiche SDKs und Werkzeuge auf GitHub.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389556077/u/0/f/653902/c/35207/s/4d2b26a0/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389556077/u/0/f/653902/c/35207/s/4d2b26a0/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389556077/u/0/f/653902/c/35207/s/4d2b26a0/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389556077/u/0/f/653902/c/35207/s/4d2b26a0/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389556077/u/0/f/653902/c/35207/s/4d2b26a0/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389556077/u/0/f/653902/c/35207/s/4d2b26a0/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389556077/u/0/f/653902/c/35207/s/4d2b26a0/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389556077/u/0/f/653902/c/35207/s/4d2b26a0/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389556077/u/0/f/653902/c/35207/s/4d2b26a0/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2b26a0/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>ZTE Axon Mini: Erstes Android-Smartphone mit Force Touch kommt nach Deutschland
+ </title>
+ <link>
+ http://www.heise.de/newsticker/meldung/ZTE-Axon-Mini-Erstes-Android-Smartphone-mit-Force-Touch-kommt-nach-Deutschland-3085296.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>ZTE bringt ein Smartphone auf den deutschen Markt, das je nach Fingerdruck
+ anders reagiert. Davon abgesehen ist das Axon Mini ein 5,2-Zöller mit guter
+ Ausstattung in gewöhnungsbedürftiger Farbe.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389556076/u/0/f/653902/c/35207/s/4d2b269f/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389556076/u/0/f/653902/c/35207/s/4d2b269f/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389556076/u/0/f/653902/c/35207/s/4d2b269f/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389556076/u/0/f/653902/c/35207/s/4d2b269f/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389556076/u/0/f/653902/c/35207/s/4d2b269f/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389556076/u/0/f/653902/c/35207/s/4d2b269f/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389556076/u/0/f/653902/c/35207/s/4d2b269f/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389556076/u/0/f/653902/c/35207/s/4d2b269f/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389556076/u/0/f/653902/c/35207/s/4d2b269f/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2b269f/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 13:34:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085296</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/ZTE-Axon-Mini-Erstes-Android-Smartphone-mit-Force-Touch-kommt-nach-Deutschland-3085296.html?wt_mc=rss.ho.beitrag.atom" title="ZTE Axon Mini: Erstes Android-Smartphone mit Force Touch kommt nach Deutschland"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/8/3/5/Axon_mini_gross-3904a6075b401bc4.png" alt="ZTE Axon Mini: Erstes Android-Smartphone mit Force Touch kommt nach Deutschland" /> </a> <p>ZTE bringt ein Smartphone auf den deutschen Markt, das je nach Fingerdruck anders reagiert. Davon abgesehen ist das Axon Mini ein 5,2-Zöller mit guter Ausstattung in gewöhnungsbedürftiger Farbe.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389556076/u/0/f/653902/c/35207/s/4d2b269f/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389556076/u/0/f/653902/c/35207/s/4d2b269f/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389556076/u/0/f/653902/c/35207/s/4d2b269f/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389556076/u/0/f/653902/c/35207/s/4d2b269f/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389556076/u/0/f/653902/c/35207/s/4d2b269f/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389556076/u/0/f/653902/c/35207/s/4d2b269f/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389556076/u/0/f/653902/c/35207/s/4d2b269f/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389556076/u/0/f/653902/c/35207/s/4d2b269f/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389556076/u/0/f/653902/c/35207/s/4d2b269f/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2b269f/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>#heiseshow: Die wöchentliche Dosis Technik-News und Netzpolitik</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/heiseshow-Die-woechentliche-Dosis-Technik-News-und-Netzpolitik-3085429.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Am Donnerstag um 16 Uhr starten wir ein neues Live-Videoformat: Mit Gästen
+ diskutiert das Team von heise online über aktuelle Ereignisse in der Hightech-Welt
+ und der Netzpolitik.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389554329/u/0/f/653902/c/35207/s/4d2ae4d2/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389554329/u/0/f/653902/c/35207/s/4d2ae4d2/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389554329/u/0/f/653902/c/35207/s/4d2ae4d2/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389554329/u/0/f/653902/c/35207/s/4d2ae4d2/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389554329/u/0/f/653902/c/35207/s/4d2ae4d2/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389554329/u/0/f/653902/c/35207/s/4d2ae4d2/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389554329/u/0/f/653902/c/35207/s/4d2ae4d2/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389554329/u/0/f/653902/c/35207/s/4d2ae4d2/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389554329/u/0/f/653902/c/35207/s/4d2ae4d2/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2ae4d2/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 13:21:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085429</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/heiseshow-Die-woechentliche-Dosis-Technik-News-und-Netzpolitik-3085429.html?wt_mc=rss.ho.beitrag.atom" title="#heiseshow: Die wöchentliche Dosis Technik-News und Netzpolitik "> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/9/3/1/heiseshow-3a3418ff4dee8a31.jpeg" alt="#heiseshow: Die wöchentliche Dosis Technik-News und Netzpolitik" /> </a> <p>Am Donnerstag um 16 Uhr starten wir ein neues Live-Videoformat: Mit Gästen diskutiert das Team von heise online über aktuelle Ereignisse in der Hightech-Welt und der Netzpolitik. </p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389554329/u/0/f/653902/c/35207/s/4d2ae4d2/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389554329/u/0/f/653902/c/35207/s/4d2ae4d2/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389554329/u/0/f/653902/c/35207/s/4d2ae4d2/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389554329/u/0/f/653902/c/35207/s/4d2ae4d2/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389554329/u/0/f/653902/c/35207/s/4d2ae4d2/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389554329/u/0/f/653902/c/35207/s/4d2ae4d2/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389554329/u/0/f/653902/c/35207/s/4d2ae4d2/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389554329/u/0/f/653902/c/35207/s/4d2ae4d2/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389554329/u/0/f/653902/c/35207/s/4d2ae4d2/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2ae4d2/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Wikipedianer lehnen sich gegen Wikimedia-Vorstand auf</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Wikipedianer-lehnen-sich-gegen-Wikimedia-Vorstand-auf-3085399.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Mit einem inoffiziellen Misstrauensvotum wollen Wikipedianer ein
+ Vorstandsmitglied zum Rücktritt drängen. Der ehemalige Google-Manager will jedoch
+ nicht gehen.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389550223/u/0/f/653902/c/35207/s/4d2ad413/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389550223/u/0/f/653902/c/35207/s/4d2ad413/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389550223/u/0/f/653902/c/35207/s/4d2ad413/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389550223/u/0/f/653902/c/35207/s/4d2ad413/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389550223/u/0/f/653902/c/35207/s/4d2ad413/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389550223/u/0/f/653902/c/35207/s/4d2ad413/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389550223/u/0/f/653902/c/35207/s/4d2ad413/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389550223/u/0/f/653902/c/35207/s/4d2ad413/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389550223/u/0/f/653902/c/35207/s/4d2ad413/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2ad413/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 12:29:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085399</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Wikipedianer-lehnen-sich-gegen-Wikimedia-Vorstand-auf-3085399.html?wt_mc=rss.ho.beitrag.atom" title="Wikipedianer lehnen sich gegen Wikimedia-Vorstand auf"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/9/1/3/Arnnon_Geshuri_-_January_2016_by_Myleen_Hollero-5e314ca8f617253b.jpeg" alt="Arnnon Geshuri" /> </a> <p>Mit einem inoffiziellen Misstrauensvotum wollen Wikipedianer ein Vorstandsmitglied zum Rücktritt drängen. Der ehemalige Google-Manager will jedoch nicht gehen. </p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389550223/u/0/f/653902/c/35207/s/4d2ad413/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389550223/u/0/f/653902/c/35207/s/4d2ad413/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389550223/u/0/f/653902/c/35207/s/4d2ad413/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389550223/u/0/f/653902/c/35207/s/4d2ad413/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389550223/u/0/f/653902/c/35207/s/4d2ad413/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389550223/u/0/f/653902/c/35207/s/4d2ad413/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389550223/u/0/f/653902/c/35207/s/4d2ad413/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389550223/u/0/f/653902/c/35207/s/4d2ad413/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389550223/u/0/f/653902/c/35207/s/4d2ad413/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2ad413/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Google senkt Preise für Smartphones Nexus 5X und 6P</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Google-senkt-Preise-fuer-Smartphones-Nexus-5X-und-6P-3085276.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Bis Mitte Februar verkauft Google seine Nexus-Smartphones günstiger: Das
+ Nexus 5X startet bei 349 Euro, das Nexus 6P bei 549 Euro.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389510599/u/0/f/653902/c/35207/s/4d2a8d5f/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389510599/u/0/f/653902/c/35207/s/4d2a8d5f/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389510599/u/0/f/653902/c/35207/s/4d2a8d5f/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389510599/u/0/f/653902/c/35207/s/4d2a8d5f/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389510599/u/0/f/653902/c/35207/s/4d2a8d5f/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389510599/u/0/f/653902/c/35207/s/4d2a8d5f/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389510599/u/0/f/653902/c/35207/s/4d2a8d5f/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389510599/u/0/f/653902/c/35207/s/4d2a8d5f/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389510599/u/0/f/653902/c/35207/s/4d2a8d5f/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2a8d5f/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 12:15:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085276</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Google-senkt-Preise-fuer-Smartphones-Nexus-5X-und-6P-3085276.html?wt_mc=rss.ho.beitrag.atom" title="Google senkt Preise für Smartphones Nexus 5X und 6P"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/8/1/9/c2040d5fb8bfa013564ddfad41be1c81_edited_102383421_731166a169-760c6bc66e60d0b9.jpeg" alt="Google senkt Preise für Nexus 5X und 6P" /> </a> <p>Bis Mitte Februar verkauft Google seine Nexus-Smartphones günstiger: Das Nexus 5X startet bei 349 Euro, das Nexus 6P bei 549 Euro.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389510599/u/0/f/653902/c/35207/s/4d2a8d5f/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389510599/u/0/f/653902/c/35207/s/4d2a8d5f/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389510599/u/0/f/653902/c/35207/s/4d2a8d5f/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389510599/u/0/f/653902/c/35207/s/4d2a8d5f/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389510599/u/0/f/653902/c/35207/s/4d2a8d5f/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389510599/u/0/f/653902/c/35207/s/4d2a8d5f/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389510599/u/0/f/653902/c/35207/s/4d2a8d5f/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389510599/u/0/f/653902/c/35207/s/4d2a8d5f/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389510599/u/0/f/653902/c/35207/s/4d2a8d5f/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2a8d5f/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Bundesregierung verlangt Glasfaserkabel entlang von Fernstraßen und anderer
+ Infrastruktur
+ </title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Bundesregierung-verlangt-Glasfaserkabel-entlang-von-Fernstrassen-und-anderer-Infrastruktur-3085354.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Das Bundeskabinett will &amp;quot;Voraussetzungen für die
+ Gigabit-Gesellschaft&amp;quot; schaffen und hat dafür ein Gesetz vorgelegt:
+ Öffentliche Versorgungsnetzbetreiber sollen ihre Infrastruktur für Breitband öffnen.&lt;br
+ clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389552422/u/0/f/653902/c/35207/s/4d2a7435/sc/3/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389552422/u/0/f/653902/c/35207/s/4d2a7435/sc/3/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389552422/u/0/f/653902/c/35207/s/4d2a7435/sc/3/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389552422/u/0/f/653902/c/35207/s/4d2a7435/sc/3/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389552422/u/0/f/653902/c/35207/s/4d2a7435/sc/3/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389552422/u/0/f/653902/c/35207/s/4d2a7435/sc/3/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389552422/u/0/f/653902/c/35207/s/4d2a7435/sc/3/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389552422/u/0/f/653902/c/35207/s/4d2a7435/sc/3/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389552422/u/0/f/653902/c/35207/s/4d2a7435/sc/3/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2a7435/sc/3/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 11:58:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085354</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Bundesregierung-verlangt-Glasfaserkabel-entlang-von-Fernstrassen-und-anderer-Infrastruktur-3085354.html?wt_mc=rss.ho.beitrag.atom" title="Bundesregierung verlangt Glasfaserkabel entlang von Fernstraßen und anderer Infrastruktur"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/8/8/5/Glasfaser4-7066ce2c9806ac68-51eff592570dcd6e.png" alt="Bundesregierung verlangt Glasfaserkabel entlang von Fernstraßen und anderer Infrastruktur" /> </a> <p>Das Bundeskabinett will "Voraussetzungen für die Gigabit-Gesellschaft" schaffen und hat dafür ein Gesetz vorgelegt: Öffentliche Versorgungsnetzbetreiber sollen ihre Infrastruktur für Breitband öffnen.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389552422/u/0/f/653902/c/35207/s/4d2a7435/sc/3/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389552422/u/0/f/653902/c/35207/s/4d2a7435/sc/3/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389552422/u/0/f/653902/c/35207/s/4d2a7435/sc/3/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389552422/u/0/f/653902/c/35207/s/4d2a7435/sc/3/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389552422/u/0/f/653902/c/35207/s/4d2a7435/sc/3/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389552422/u/0/f/653902/c/35207/s/4d2a7435/sc/3/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389552422/u/0/f/653902/c/35207/s/4d2a7435/sc/3/a2.htm"><img src="http://da.feedsportal.com/r/247389552422/u/0/f/653902/c/35207/s/4d2a7435/sc/3/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389552422/u/0/f/653902/c/35207/s/4d2a7435/sc/3/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2a7435/sc/3/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>iOS und OS X: Safari-Vorschläge legen Apples Webbrowser lahm</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/iOS-und-OS-X-Safari-Vorschlaege-legen-Apples-Webbrowser-lahm-3085337.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Eine Fehlkonfiguration bei Apples Suchhilfe scheint aktuell der Grund für
+ einen sofortigen Absturz von Safari, wenn Nutzer auf iPhone oder iPad die
+ Adresszeile auswählen oder eine Eingabe starten. Das Problem lässt sich umgehen.&lt;br
+ clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389542680/u/0/f/653902/c/35207/s/4d2a66f7/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389542680/u/0/f/653902/c/35207/s/4d2a66f7/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389542680/u/0/f/653902/c/35207/s/4d2a66f7/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389542680/u/0/f/653902/c/35207/s/4d2a66f7/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389542680/u/0/f/653902/c/35207/s/4d2a66f7/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389542680/u/0/f/653902/c/35207/s/4d2a66f7/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389542680/u/0/f/653902/c/35207/s/4d2a66f7/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389542680/u/0/f/653902/c/35207/s/4d2a66f7/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389542680/u/0/f/653902/c/35207/s/4d2a66f7/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2a66f7/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 11:38:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085337</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/iOS-und-OS-X-Safari-Vorschlaege-legen-Apples-Webbrowser-lahm-3085337.html?wt_mc=rss.ho.beitrag.atom" title="iOS und OS X: Safari-Vorschläge legen Apples Webbrowser lahm"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/8/7/4/urn-newsml-dpa-com-20090101-160127-99-251554_large_4_3-d71a5d70124b506d.jpeg" alt="Apple" /> </a> <p>Eine Fehlkonfiguration bei Apples Suchhilfe scheint aktuell der Grund für einen sofortigen Absturz von Safari, wenn Nutzer auf iPhone oder iPad die Adresszeile auswählen oder eine Eingabe starten. Das Problem lässt sich umgehen.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389542680/u/0/f/653902/c/35207/s/4d2a66f7/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389542680/u/0/f/653902/c/35207/s/4d2a66f7/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389542680/u/0/f/653902/c/35207/s/4d2a66f7/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389542680/u/0/f/653902/c/35207/s/4d2a66f7/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389542680/u/0/f/653902/c/35207/s/4d2a66f7/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389542680/u/0/f/653902/c/35207/s/4d2a66f7/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389542680/u/0/f/653902/c/35207/s/4d2a66f7/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389542680/u/0/f/653902/c/35207/s/4d2a66f7/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389542680/u/0/f/653902/c/35207/s/4d2a66f7/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2a66f7/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>VMware baut 800 Arbeitsplätze ab</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/VMware-baut-800-Arbeitsplaetze-ab-3085308.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Der Hersteller von Virtualisierungssoftware will sich umstrukturieren.
+ Dafür müssen zunächst 800 Mitarbeiter gehen.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389542679/u/0/f/653902/c/35207/s/4d2a66f6/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389542679/u/0/f/653902/c/35207/s/4d2a66f6/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389542679/u/0/f/653902/c/35207/s/4d2a66f6/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389542679/u/0/f/653902/c/35207/s/4d2a66f6/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389542679/u/0/f/653902/c/35207/s/4d2a66f6/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389542679/u/0/f/653902/c/35207/s/4d2a66f6/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389542679/u/0/f/653902/c/35207/s/4d2a66f6/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389542679/u/0/f/653902/c/35207/s/4d2a66f6/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389542679/u/0/f/653902/c/35207/s/4d2a66f6/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2a66f6/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 11:34:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085308</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/VMware-baut-800-Arbeitsplaetze-ab-3085308.html?wt_mc=rss.ho.beitrag.atom" title="VMware baut 800 Arbeitsplätze ab"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/8/4/7/logo-guidelines-f0ba93178fa3ecae.jpeg" alt="VMware baut 800 Arbeitsplätze ab" /> </a> <p>Der Hersteller von Virtualisierungssoftware will sich umstrukturieren. Dafür müssen zunächst 800 Mitarbeiter gehen.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389542679/u/0/f/653902/c/35207/s/4d2a66f6/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389542679/u/0/f/653902/c/35207/s/4d2a66f6/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389542679/u/0/f/653902/c/35207/s/4d2a66f6/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389542679/u/0/f/653902/c/35207/s/4d2a66f6/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389542679/u/0/f/653902/c/35207/s/4d2a66f6/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389542679/u/0/f/653902/c/35207/s/4d2a66f6/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389542679/u/0/f/653902/c/35207/s/4d2a66f6/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389542679/u/0/f/653902/c/35207/s/4d2a66f6/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389542679/u/0/f/653902/c/35207/s/4d2a66f6/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2a66f6/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Menschen-Rohrpost Hyperloop: Elon Musk lässt Aecom Teststrecke bauen</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Menschen-Rohrpost-Hyperloop-Elon-Musk-laesst-Aecom-Teststrecke-bauen-3085245.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Die Firma Aecom will noch im Frühling mit dem Bau einer
+ Hyperloop-Teststecke starten. Elon Musks Firma SpaceX hat das
+ Fortune-500-Unternehmen damit beauftragt. Studierende sollen helfen,
+ Kapsel-Prototypen zu bauen.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389503052/u/0/f/653902/c/35207/s/4d2a0cbe/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389503052/u/0/f/653902/c/35207/s/4d2a0cbe/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389503052/u/0/f/653902/c/35207/s/4d2a0cbe/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389503052/u/0/f/653902/c/35207/s/4d2a0cbe/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389503052/u/0/f/653902/c/35207/s/4d2a0cbe/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389503052/u/0/f/653902/c/35207/s/4d2a0cbe/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389503052/u/0/f/653902/c/35207/s/4d2a0cbe/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389503052/u/0/f/653902/c/35207/s/4d2a0cbe/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389503052/u/0/f/653902/c/35207/s/4d2a0cbe/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2a0cbe/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 11:13:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085245</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Menschen-Rohrpost-Hyperloop-Elon-Musk-laesst-Aecom-Teststrecke-bauen-3085245.html?wt_mc=rss.ho.beitrag.atom" title="Menschen-Rohrpost Hyperloop: Elon Musk lässt Aecom Teststrecke bauen"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/7/9/1/Hyperloop-0f7f26eb632745e4-61f3c13ae470a965.jpeg" alt="Hyperloop" /> </a> <p>Die Firma Aecom will noch im Frühling mit dem Bau einer Hyperloop-Teststecke starten. Elon Musks Firma SpaceX hat das Fortune-500-Unternehmen damit beauftragt. Studierende sollen helfen, Kapsel-Prototypen zu bauen. </p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389503052/u/0/f/653902/c/35207/s/4d2a0cbe/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389503052/u/0/f/653902/c/35207/s/4d2a0cbe/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389503052/u/0/f/653902/c/35207/s/4d2a0cbe/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389503052/u/0/f/653902/c/35207/s/4d2a0cbe/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389503052/u/0/f/653902/c/35207/s/4d2a0cbe/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389503052/u/0/f/653902/c/35207/s/4d2a0cbe/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389503052/u/0/f/653902/c/35207/s/4d2a0cbe/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389503052/u/0/f/653902/c/35207/s/4d2a0cbe/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389503052/u/0/f/653902/c/35207/s/4d2a0cbe/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2a0cbe/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Lenovos Datentausch-App Shareit: 12345678 als Standardpasswort</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Lenovos-Datentausch-App-Shareit-12345678-als-Standardpasswort-3085250.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>In der Shareit-Anwendung von Lenovo klaffen mehrere Schwachstellen, über
+ die Angreifer Nutzern unter anderem Schadcode unterjubeln können. Gefixte Version
+ sollen das unterbinden.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389501134/u/0/f/653902/c/35207/s/4d2a0506/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389501134/u/0/f/653902/c/35207/s/4d2a0506/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389501134/u/0/f/653902/c/35207/s/4d2a0506/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389501134/u/0/f/653902/c/35207/s/4d2a0506/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389501134/u/0/f/653902/c/35207/s/4d2a0506/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389501134/u/0/f/653902/c/35207/s/4d2a0506/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389501134/u/0/f/653902/c/35207/s/4d2a0506/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389501134/u/0/f/653902/c/35207/s/4d2a0506/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389501134/u/0/f/653902/c/35207/s/4d2a0506/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2a0506/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 10:54:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085250</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Lenovos-Datentausch-App-Shareit-12345678-als-Standardpasswort-3085250.html?wt_mc=rss.ho.beitrag.atom" title="Lenovos Datentausch-App Shareit: 12345678 als Standardpasswort"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/7/9/5/shareit-7a502640b4aca443.png" alt="Lenovos Datentausch-App Shareit mit 12345678 als Standardpasswort" /> </a> <p>In der Shareit-Anwendung von Lenovo klaffen mehrere Schwachstellen, über die Angreifer Nutzern unter anderem Schadcode unterjubeln können. Gefixte Version sollen das unterbinden.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389501134/u/0/f/653902/c/35207/s/4d2a0506/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389501134/u/0/f/653902/c/35207/s/4d2a0506/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389501134/u/0/f/653902/c/35207/s/4d2a0506/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389501134/u/0/f/653902/c/35207/s/4d2a0506/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389501134/u/0/f/653902/c/35207/s/4d2a0506/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389501134/u/0/f/653902/c/35207/s/4d2a0506/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389501134/u/0/f/653902/c/35207/s/4d2a0506/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389501134/u/0/f/653902/c/35207/s/4d2a0506/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389501134/u/0/f/653902/c/35207/s/4d2a0506/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2a0506/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Paketdrohne: Google lässt sich beweglichen Paketempfangsbehälter patentieren
+ </title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Paketdrohne-Google-laesst-sich-beweglichen-Paketempfangsbehaelter-patentieren-3085263.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Manche Orte können von unbemannten Fluggeräten nicht ohne Sicherheitsrisiko
+ angeflogen werden. Dafür haben sich Google-Entwickler eine Lösung ausgedacht.&lt;br
+ clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389501131/u/0/f/653902/c/35207/s/4d2a0504/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389501131/u/0/f/653902/c/35207/s/4d2a0504/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389501131/u/0/f/653902/c/35207/s/4d2a0504/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389501131/u/0/f/653902/c/35207/s/4d2a0504/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389501131/u/0/f/653902/c/35207/s/4d2a0504/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389501131/u/0/f/653902/c/35207/s/4d2a0504/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389501131/u/0/f/653902/c/35207/s/4d2a0504/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389501131/u/0/f/653902/c/35207/s/4d2a0504/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389501131/u/0/f/653902/c/35207/s/4d2a0504/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2a0504/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 10:53:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085263</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Paketdrohne-Google-laesst-sich-beweglichen-Paketempfangsbehaelter-patentieren-3085263.html?wt_mc=rss.ho.beitrag.atom" title="Paketdrohne: Google lässt sich beweglichen Paketempfangsbehälter patentieren"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/8/0/7/Bildschirmfoto_2016-01-27_um_11-8a774a671e812db7.jpeg" alt="Paketdrohne: Google lässt sich beweglichen Paketempfangsbehälter patentieren" /> </a> <p>Manche Orte können von unbemannten Fluggeräten nicht ohne Sicherheitsrisiko angeflogen werden. Dafür haben sich Google-Entwickler eine Lösung ausgedacht.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389501131/u/0/f/653902/c/35207/s/4d2a0504/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389501131/u/0/f/653902/c/35207/s/4d2a0504/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389501131/u/0/f/653902/c/35207/s/4d2a0504/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389501131/u/0/f/653902/c/35207/s/4d2a0504/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389501131/u/0/f/653902/c/35207/s/4d2a0504/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389501131/u/0/f/653902/c/35207/s/4d2a0504/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389501131/u/0/f/653902/c/35207/s/4d2a0504/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389501131/u/0/f/653902/c/35207/s/4d2a0504/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389501131/u/0/f/653902/c/35207/s/4d2a0504/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2a0504/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Webbrowser Firefox 44 mit verbesserten Push-Nachrichten</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Webbrowser-Firefox-44-mit-verbesserten-Push-Nachrichten-3085193.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Version 44 des Firefox-Browsers verschickt nun auch Push-Nachrichten von
+ Websites, die nicht geöffnet sind. Zudem haben die Entwickler die Fehlerseiten
+ verbessert und die RC4-Unterstützung entfernt.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389543121/u/0/f/653902/c/35207/s/4d29ec71/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389543121/u/0/f/653902/c/35207/s/4d29ec71/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389543121/u/0/f/653902/c/35207/s/4d29ec71/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389543121/u/0/f/653902/c/35207/s/4d29ec71/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389543121/u/0/f/653902/c/35207/s/4d29ec71/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389543121/u/0/f/653902/c/35207/s/4d29ec71/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389543121/u/0/f/653902/c/35207/s/4d29ec71/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389543121/u/0/f/653902/c/35207/s/4d29ec71/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389543121/u/0/f/653902/c/35207/s/4d29ec71/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d29ec71/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 10:42:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085193</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Webbrowser-Firefox-44-mit-verbesserten-Push-Nachrichten-3085193.html?wt_mc=rss.ho.beitrag.atom" title="Webbrowser Firefox 44 mit verbesserten Push-Nachrichten"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/7/4/8/mozz_39339342_original-4109b22bb9b70d97.jpeg" alt="Firefox" /> </a> <p>Version 44 des Firefox-Browsers verschickt nun auch Push-Nachrichten von Websites, die nicht geöffnet sind. Zudem haben die Entwickler die Fehlerseiten verbessert und die RC4-Unterstützung entfernt.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389543121/u/0/f/653902/c/35207/s/4d29ec71/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389543121/u/0/f/653902/c/35207/s/4d29ec71/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389543121/u/0/f/653902/c/35207/s/4d29ec71/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389543121/u/0/f/653902/c/35207/s/4d29ec71/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389543121/u/0/f/653902/c/35207/s/4d29ec71/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389543121/u/0/f/653902/c/35207/s/4d29ec71/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389543121/u/0/f/653902/c/35207/s/4d29ec71/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389543121/u/0/f/653902/c/35207/s/4d29ec71/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389543121/u/0/f/653902/c/35207/s/4d29ec71/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d29ec71/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>BMW steuert via IFTTT das Smart Home</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/BMW-steuert-via-IFTTT-das-Smart-Home-3085257.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Wer einen BMW mit ConnectedDrive Services fährt, kann jetzt mit einem
+ Widget für den Automatisierungsdienst IFTTT zum Beispiel das Garagentor hochfahren
+ lassen, wenn er auf dem Weg nach Hause ist.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389543120/u/0/f/653902/c/35207/s/4d29ec70/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389543120/u/0/f/653902/c/35207/s/4d29ec70/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389543120/u/0/f/653902/c/35207/s/4d29ec70/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389543120/u/0/f/653902/c/35207/s/4d29ec70/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389543120/u/0/f/653902/c/35207/s/4d29ec70/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389543120/u/0/f/653902/c/35207/s/4d29ec70/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389543120/u/0/f/653902/c/35207/s/4d29ec70/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389543120/u/0/f/653902/c/35207/s/4d29ec70/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389543120/u/0/f/653902/c/35207/s/4d29ec70/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d29ec70/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 10:38:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085257</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/BMW-steuert-via-IFTTT-das-Smart-Home-3085257.html?wt_mc=rss.ho.beitrag.atom" title="BMW steuert via IFTTT das Smart Home"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/8/0/1/IFTTT-8bf9c9011ff12a0d.png" alt="BMW steuert via IFTTT das Smart Home" /> </a> <p>Wer einen BMW mit ConnectedDrive Services fährt, kann jetzt mit einem Widget für den Automatisierungsdienst IFTTT zum Beispiel das Garagentor hochfahren lassen, wenn er auf dem Weg nach Hause ist.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389543120/u/0/f/653902/c/35207/s/4d29ec70/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389543120/u/0/f/653902/c/35207/s/4d29ec70/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389543120/u/0/f/653902/c/35207/s/4d29ec70/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389543120/u/0/f/653902/c/35207/s/4d29ec70/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389543120/u/0/f/653902/c/35207/s/4d29ec70/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389543120/u/0/f/653902/c/35207/s/4d29ec70/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389543120/u/0/f/653902/c/35207/s/4d29ec70/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389543120/u/0/f/653902/c/35207/s/4d29ec70/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389543120/u/0/f/653902/c/35207/s/4d29ec70/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d29ec70/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Eine Million Petenten protestieren gegen Elfenbeinhandel auf Yahoo Japan</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Eine-Million-Petenten-protestieren-gegen-Elfenbeinhandel-auf-Yahoo-Japan-3085214.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Jeden Tag werden auf der Welt hundert Elefanten wegen ihrer Stoßzähne
+ getötet. Die Nichtregierungsorganisation Avaaz will erreichen, dass Elfenbein nicht
+ mehr über Yahoo Japan verkauft werden kann.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389543119/u/0/f/653902/c/35207/s/4d29ec6e/sc/3/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389543119/u/0/f/653902/c/35207/s/4d29ec6e/sc/3/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389543119/u/0/f/653902/c/35207/s/4d29ec6e/sc/3/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389543119/u/0/f/653902/c/35207/s/4d29ec6e/sc/3/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389543119/u/0/f/653902/c/35207/s/4d29ec6e/sc/3/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389543119/u/0/f/653902/c/35207/s/4d29ec6e/sc/3/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389543119/u/0/f/653902/c/35207/s/4d29ec6e/sc/3/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389543119/u/0/f/653902/c/35207/s/4d29ec6e/sc/3/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389543119/u/0/f/653902/c/35207/s/4d29ec6e/sc/3/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d29ec6e/sc/3/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 10:21:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085214</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Eine-Million-Petenten-protestieren-gegen-Elfenbeinhandel-auf-Yahoo-Japan-3085214.html?wt_mc=rss.ho.beitrag.atom" title="Eine Million Petenten protestieren gegen Elfenbeinhandel auf Yahoo Japan"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/7/6/6/Bildschirmfoto_2016-01-27_um_11-2e865cc25538fb20.jpeg" alt="Eine Million Petitenten protestieren gegen Elfenbeinhandel auf Yahoo Japan" /> </a> <p>Jeden Tag werden auf der Welt hundert Elefanten wegen ihrer Stoßzähne getötet. Die Nichtregierungsorganisation Avaaz will erreichen, dass Elfenbein nicht mehr über Yahoo Japan verkauft werden kann.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389543119/u/0/f/653902/c/35207/s/4d29ec6e/sc/3/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389543119/u/0/f/653902/c/35207/s/4d29ec6e/sc/3/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389543119/u/0/f/653902/c/35207/s/4d29ec6e/sc/3/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389543119/u/0/f/653902/c/35207/s/4d29ec6e/sc/3/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389543119/u/0/f/653902/c/35207/s/4d29ec6e/sc/3/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389543119/u/0/f/653902/c/35207/s/4d29ec6e/sc/3/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389543119/u/0/f/653902/c/35207/s/4d29ec6e/sc/3/a2.htm"><img src="http://da.feedsportal.com/r/247389543119/u/0/f/653902/c/35207/s/4d29ec6e/sc/3/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389543119/u/0/f/653902/c/35207/s/4d29ec6e/sc/3/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d29ec6e/sc/3/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Eine Million Petitenten protestieren gegen Elfenbeinhandel auf Yahoo Japan
+ </title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Eine-Million-Petitenten-protestieren-gegen-Elfenbeinhandel-auf-Yahoo-Japan-3085214.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Jeden Tag werden auf der Welt hundert Elefanten wegen ihrer Stoßzähne
+ getötet. Die Nichtregierungsorganisation Avaaz will erreichen, dass Elfenbein nicht
+ mehr über Yahoo Japan verkauft werden kann.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389541116/u/0/f/653902/c/35207/s/4d29bd45/sc/3/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389541116/u/0/f/653902/c/35207/s/4d29bd45/sc/3/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389541116/u/0/f/653902/c/35207/s/4d29bd45/sc/3/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389541116/u/0/f/653902/c/35207/s/4d29bd45/sc/3/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389541116/u/0/f/653902/c/35207/s/4d29bd45/sc/3/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389541116/u/0/f/653902/c/35207/s/4d29bd45/sc/3/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389541116/u/0/f/653902/c/35207/s/4d29bd45/sc/3/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389541116/u/0/f/653902/c/35207/s/4d29bd45/sc/3/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389541116/u/0/f/653902/c/35207/s/4d29bd45/sc/3/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d29bd45/sc/3/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 10:21:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085214</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Eine-Million-Petitenten-protestieren-gegen-Elfenbeinhandel-auf-Yahoo-Japan-3085214.html?wt_mc=rss.ho.beitrag.atom" title="Eine Million Petitenten protestieren gegen Elfenbeinhandel auf Yahoo Japan"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/7/6/6/Bildschirmfoto_2016-01-27_um_11-2e865cc25538fb20.jpeg" alt="Eine Million Petitenten protestieren gegen Elfenbeinhandel auf Yahoo Japan" /> </a> <p>Jeden Tag werden auf der Welt hundert Elefanten wegen ihrer Stoßzähne getötet. Die Nichtregierungsorganisation Avaaz will erreichen, dass Elfenbein nicht mehr über Yahoo Japan verkauft werden kann.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389541116/u/0/f/653902/c/35207/s/4d29bd45/sc/3/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389541116/u/0/f/653902/c/35207/s/4d29bd45/sc/3/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389541116/u/0/f/653902/c/35207/s/4d29bd45/sc/3/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389541116/u/0/f/653902/c/35207/s/4d29bd45/sc/3/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389541116/u/0/f/653902/c/35207/s/4d29bd45/sc/3/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389541116/u/0/f/653902/c/35207/s/4d29bd45/sc/3/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389541116/u/0/f/653902/c/35207/s/4d29bd45/sc/3/a2.htm"><img src="http://da.feedsportal.com/r/247389541116/u/0/f/653902/c/35207/s/4d29bd45/sc/3/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389541116/u/0/f/653902/c/35207/s/4d29bd45/sc/3/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d29bd45/sc/3/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>China überholt die USA als größter Markt für Elektroautos</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/China-ueberholt-die-USA-als-groesster-Markt-fuer-Elektroautos-3085152.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Die Zahl der Neuzulassungen von E-Autos steigt – auch in Deutschland. Von
+ einem Leitmarkt ist die Bundesrepublik aber weit entfernt.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389529191/u/0/f/653902/c/35207/s/4d29a77c/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389529191/u/0/f/653902/c/35207/s/4d29a77c/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389529191/u/0/f/653902/c/35207/s/4d29a77c/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389529191/u/0/f/653902/c/35207/s/4d29a77c/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389529191/u/0/f/653902/c/35207/s/4d29a77c/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389529191/u/0/f/653902/c/35207/s/4d29a77c/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389529191/u/0/f/653902/c/35207/s/4d29a77c/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389529191/u/0/f/653902/c/35207/s/4d29a77c/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389529191/u/0/f/653902/c/35207/s/4d29a77c/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d29a77c/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 09:54:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085152</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/China-ueberholt-die-USA-als-groesster-Markt-fuer-Elektroautos-3085152.html?wt_mc=rss.ho.beitrag.atom" title="China überholt die USA als größter Markt für Elektroautos"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/7/1/0/Bildschirmfoto_2016-01-27_um_10-efc56d9f4ea5c1e1.jpeg" alt="Elektroauto" /> </a> <p>Die Zahl der Neuzulassungen von E-Autos steigt – auch in Deutschland. Von einem Leitmarkt ist die Bundesrepublik aber weit entfernt.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389529191/u/0/f/653902/c/35207/s/4d29a77c/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389529191/u/0/f/653902/c/35207/s/4d29a77c/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389529191/u/0/f/653902/c/35207/s/4d29a77c/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389529191/u/0/f/653902/c/35207/s/4d29a77c/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389529191/u/0/f/653902/c/35207/s/4d29a77c/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389529191/u/0/f/653902/c/35207/s/4d29a77c/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389529191/u/0/f/653902/c/35207/s/4d29a77c/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389529191/u/0/f/653902/c/35207/s/4d29a77c/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389529191/u/0/f/653902/c/35207/s/4d29a77c/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d29a77c/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Altermedia: Deutschlandweite Razzia gegen rechtsextreme Internetplattform – zwei
+ Festnahmen
+ </title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Altermedia-Deutschlandweite-Razzia-gegen-rechtsextreme-Internetplattform-zwei-Festnahmen-3085140.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>In einer bundesweiten Aktion gehen Ermittler der Bundesanwaltschaft gegen
+ führende Betreiber des rechtsextremen Internetportals &amp;quot;Altermedia&amp;quot;
+ vor. Ihnen wird die Gründung einer kriminellen Vereinigung vorgeworfen. Inzwischen
+ wurde die Vereinigung verboten.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389487846/u/0/f/653902/c/35207/s/4d293258/sc/3/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389487846/u/0/f/653902/c/35207/s/4d293258/sc/3/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389487846/u/0/f/653902/c/35207/s/4d293258/sc/3/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389487846/u/0/f/653902/c/35207/s/4d293258/sc/3/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389487846/u/0/f/653902/c/35207/s/4d293258/sc/3/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389487846/u/0/f/653902/c/35207/s/4d293258/sc/3/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389487846/u/0/f/653902/c/35207/s/4d293258/sc/3/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389487846/u/0/f/653902/c/35207/s/4d293258/sc/3/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389487846/u/0/f/653902/c/35207/s/4d293258/sc/3/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d293258/sc/3/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 09:10:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085140</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Altermedia-Deutschlandweite-Razzia-gegen-rechtsextreme-Internetplattform-zwei-Festnahmen-3085140.html?wt_mc=rss.ho.beitrag.atom" title="Altermedia: Deutschlandweite Razzia gegen rechtsextreme Internetplattform – zwei Festnahmen"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/6/9/8/urn-newsml-dpa-com-20090101-150414-99-04666_large_4_3-6f20e7a913e9ebef.jpeg" alt="Hacker" /> </a> <p>In einer bundesweiten Aktion gehen Ermittler der Bundesanwaltschaft gegen führende Betreiber des rechtsextremen Internetportals "Altermedia" vor. Ihnen wird die Gründung einer kriminellen Vereinigung vorgeworfen. Inzwischen wurde die Vereinigung verboten.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389487846/u/0/f/653902/c/35207/s/4d293258/sc/3/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389487846/u/0/f/653902/c/35207/s/4d293258/sc/3/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389487846/u/0/f/653902/c/35207/s/4d293258/sc/3/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389487846/u/0/f/653902/c/35207/s/4d293258/sc/3/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389487846/u/0/f/653902/c/35207/s/4d293258/sc/3/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389487846/u/0/f/653902/c/35207/s/4d293258/sc/3/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389487846/u/0/f/653902/c/35207/s/4d293258/sc/3/a2.htm"><img src="http://da.feedsportal.com/r/247389487846/u/0/f/653902/c/35207/s/4d293258/sc/3/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389487846/u/0/f/653902/c/35207/s/4d293258/sc/3/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d293258/sc/3/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Bundesdatenschutzbeauftragte warnt vor Dashcams</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Bundesdatenschutzbeauftragte-warnt-vor-Dashcams-3085120.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Im Ausland setzen Autofahrer vermehrt Videokameras ein, die hinter der
+ Windschutzscheibe postiert werden und das Geschehen vor dem Auto kontinuierlich
+ aufzeichnen. In Deutschland ist das aus Datenschutzgründen unzulässig, meint Andrea
+ Voßhoff.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389528982/u/0/f/653902/c/35207/s/4d293d2a/sc/3/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389528982/u/0/f/653902/c/35207/s/4d293d2a/sc/3/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389528982/u/0/f/653902/c/35207/s/4d293d2a/sc/3/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389528982/u/0/f/653902/c/35207/s/4d293d2a/sc/3/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389528982/u/0/f/653902/c/35207/s/4d293d2a/sc/3/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389528982/u/0/f/653902/c/35207/s/4d293d2a/sc/3/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389528982/u/0/f/653902/c/35207/s/4d293d2a/sc/3/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389528982/u/0/f/653902/c/35207/s/4d293d2a/sc/3/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389528982/u/0/f/653902/c/35207/s/4d293d2a/sc/3/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d293d2a/sc/3/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 08:52:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085120</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Bundesdatenschutzbeauftragte-warnt-vor-Dashcams-3085120.html?wt_mc=rss.ho.beitrag.atom" title="Bundesdatenschutzbeauftragte warnt vor Dashcams"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/6/8/2/Unbenannt-1-ccb6087fae248ce5.jpeg" alt="Dashcam" /> </a> <p>Im Ausland setzen Autofahrer vermehrt Videokameras ein, die hinter der Windschutzscheibe postiert werden und das Geschehen vor dem Auto kontinuierlich aufzeichnen. In Deutschland ist das aus Datenschutzgründen unzulässig, meint Andrea Voßhoff.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389528982/u/0/f/653902/c/35207/s/4d293d2a/sc/3/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389528982/u/0/f/653902/c/35207/s/4d293d2a/sc/3/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389528982/u/0/f/653902/c/35207/s/4d293d2a/sc/3/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389528982/u/0/f/653902/c/35207/s/4d293d2a/sc/3/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389528982/u/0/f/653902/c/35207/s/4d293d2a/sc/3/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389528982/u/0/f/653902/c/35207/s/4d293d2a/sc/3/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389528982/u/0/f/653902/c/35207/s/4d293d2a/sc/3/a2.htm"><img src="http://da.feedsportal.com/r/247389528982/u/0/f/653902/c/35207/s/4d293d2a/sc/3/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389528982/u/0/f/653902/c/35207/s/4d293d2a/sc/3/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d293d2a/sc/3/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Unitymedia bietet Kabelanschlüsse mit 400 MBit/s an</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Unitymedia-bietet-Kabelanschluesse-mit-400-MBit-s-an-3084956.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Der Kabelriese dreht an der Speed-Schraube: Rund 40 Prozent der Haushalte
+ im Verbreitungsgebiet in Nordrhein-Westfalen, Hessen und Baden-Württemberg können in
+ Kürze Anschlüsse mit bis zu 400 MBit/s buchen.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389519640/u/0/f/653902/c/35207/s/4d291cc3/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389519640/u/0/f/653902/c/35207/s/4d291cc3/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389519640/u/0/f/653902/c/35207/s/4d291cc3/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389519640/u/0/f/653902/c/35207/s/4d291cc3/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389519640/u/0/f/653902/c/35207/s/4d291cc3/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389519640/u/0/f/653902/c/35207/s/4d291cc3/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389519640/u/0/f/653902/c/35207/s/4d291cc3/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389519640/u/0/f/653902/c/35207/s/4d291cc3/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389519640/u/0/f/653902/c/35207/s/4d291cc3/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d291cc3/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 08:30:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084956</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Unitymedia-bietet-Kabelanschluesse-mit-400-MBit-s-an-3084956.html?wt_mc=rss.ho.beitrag.atom" title="Unitymedia bietet Kabelanschlüsse mit 400 MBit/s an"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/5/7/5/kdg_docsis_200-b8c485b1e19b9753-9d3ae9132349e0f6-d75e967a29e534a4.jpeg" alt="Unitymedia" /> </a> <p>Der Kabelriese dreht an der Speed-Schraube: Rund 40 Prozent der Haushalte im Verbreitungsgebiet in Nordrhein-Westfalen, Hessen und Baden-Württemberg können in Kürze Anschlüsse mit bis zu 400 MBit/s buchen.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389519640/u/0/f/653902/c/35207/s/4d291cc3/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389519640/u/0/f/653902/c/35207/s/4d291cc3/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389519640/u/0/f/653902/c/35207/s/4d291cc3/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389519640/u/0/f/653902/c/35207/s/4d291cc3/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389519640/u/0/f/653902/c/35207/s/4d291cc3/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389519640/u/0/f/653902/c/35207/s/4d291cc3/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389519640/u/0/f/653902/c/35207/s/4d291cc3/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389519640/u/0/f/653902/c/35207/s/4d291cc3/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389519640/u/0/f/653902/c/35207/s/4d291cc3/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d291cc3/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Tim Cook zu den Apple-Quartalszahlen: Weiterhin kein Billig-iPhone</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Tim-Cook-zu-den-Apple-Quartalszahlen-Weiterhin-kein-Billig-iPhone-3085095.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Der Apple-Chef hat sich im Gespräch mit Analysten zur künftigen
+ Geschäftsstrategie geäußert, da das geringe Wachstum beim iPhone der Börse Sorge
+ bereitet. Auch zum für Cupertino zunehmend bedeutenden Wachstumsmarkt China nannte
+ er Details.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389519639/u/0/f/653902/c/35207/s/4d291cc2/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389519639/u/0/f/653902/c/35207/s/4d291cc2/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389519639/u/0/f/653902/c/35207/s/4d291cc2/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389519639/u/0/f/653902/c/35207/s/4d291cc2/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389519639/u/0/f/653902/c/35207/s/4d291cc2/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389519639/u/0/f/653902/c/35207/s/4d291cc2/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389519639/u/0/f/653902/c/35207/s/4d291cc2/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389519639/u/0/f/653902/c/35207/s/4d291cc2/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389519639/u/0/f/653902/c/35207/s/4d291cc2/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d291cc2/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 08:12:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085095</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Tim-Cook-zu-den-Apple-Quartalszahlen-Weiterhin-kein-Billig-iPhone-3085095.html?wt_mc=rss.ho.beitrag.atom" title="Tim Cook zu den Apple-Quartalszahlen: Weiterhin kein Billig-iPhone"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/6/6/6/urn-newsml-dpa-com-20090101-151020-99-02535_large_4_3-44c62c7b61723c1b.jpeg" alt="Tim Cook" /> </a> <p>Der Apple-Chef hat sich im Gespräch mit Analysten zur künftigen Geschäftsstrategie geäußert, da das geringe Wachstum beim iPhone der Börse Sorge bereitet. Auch zum für Cupertino zunehmend bedeutenden Wachstumsmarkt China nannte er Details.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389519639/u/0/f/653902/c/35207/s/4d291cc2/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389519639/u/0/f/653902/c/35207/s/4d291cc2/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389519639/u/0/f/653902/c/35207/s/4d291cc2/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389519639/u/0/f/653902/c/35207/s/4d291cc2/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389519639/u/0/f/653902/c/35207/s/4d291cc2/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389519639/u/0/f/653902/c/35207/s/4d291cc2/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389519639/u/0/f/653902/c/35207/s/4d291cc2/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389519639/u/0/f/653902/c/35207/s/4d291cc2/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389519639/u/0/f/653902/c/35207/s/4d291cc2/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d291cc2/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Lufthansa und Drohnenhersteller DJI werden Partner</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Lufthansa-und-Drohnenhersteller-DJI-werden-Partner-3085067.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Die Lufthansa möchte neue Geschäftsfelder erobern. Zusammen mit dem
+ Drohnenhersteller DJI sollen Drohnen für Großkunden gefertigt werden. Diese könnten
+ mit den fliegenden Helfern etwa einfacher Windkraftanlagen überwachen oder
+ Baufortschritte verfolgen.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389516237/u/0/f/653902/c/35207/s/4d28e6a3/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389516237/u/0/f/653902/c/35207/s/4d28e6a3/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389516237/u/0/f/653902/c/35207/s/4d28e6a3/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389516237/u/0/f/653902/c/35207/s/4d28e6a3/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389516237/u/0/f/653902/c/35207/s/4d28e6a3/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389516237/u/0/f/653902/c/35207/s/4d28e6a3/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389516237/u/0/f/653902/c/35207/s/4d28e6a3/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389516237/u/0/f/653902/c/35207/s/4d28e6a3/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389516237/u/0/f/653902/c/35207/s/4d28e6a3/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d28e6a3/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 08:11:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085067</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Lufthansa-und-Drohnenhersteller-DJI-werden-Partner-3085067.html?wt_mc=rss.ho.beitrag.atom" title="Lufthansa und Drohnenhersteller DJI werden Partner "> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/6/4/5/d20ed2f1c559d64f6fd690e5c8b54932_edited_102368661_e1e4041693-ff2f5513b2065135.jpeg" alt="Drohne" /> </a> <p>Die Lufthansa möchte neue Geschäftsfelder erobern. Zusammen mit dem Drohnenhersteller DJI sollen Drohnen für Großkunden gefertigt werden. Diese könnten mit den fliegenden Helfern etwa einfacher Windkraftanlagen überwachen oder Baufortschritte verfolgen. </p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389516237/u/0/f/653902/c/35207/s/4d28e6a3/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389516237/u/0/f/653902/c/35207/s/4d28e6a3/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389516237/u/0/f/653902/c/35207/s/4d28e6a3/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389516237/u/0/f/653902/c/35207/s/4d28e6a3/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389516237/u/0/f/653902/c/35207/s/4d28e6a3/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389516237/u/0/f/653902/c/35207/s/4d28e6a3/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389516237/u/0/f/653902/c/35207/s/4d28e6a3/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389516237/u/0/f/653902/c/35207/s/4d28e6a3/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389516237/u/0/f/653902/c/35207/s/4d28e6a3/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d28e6a3/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Wasserknappheit bedroht Stromproduktion</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Wasserknappheit-bedroht-Stromproduktion-3084350.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Forscher fürchten, dass die globale Stromproduktion aufgrund des
+ Klimawandels in den nächsten 35 Jahren deutlich zurückgehen könnte. Der Grund: Dürre
+ macht Kühlung unmöglich.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389516745/u/0/f/653902/c/35207/s/4d2893ec/sc/3/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389516745/u/0/f/653902/c/35207/s/4d2893ec/sc/3/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389516745/u/0/f/653902/c/35207/s/4d2893ec/sc/3/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389516745/u/0/f/653902/c/35207/s/4d2893ec/sc/3/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389516745/u/0/f/653902/c/35207/s/4d2893ec/sc/3/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389516745/u/0/f/653902/c/35207/s/4d2893ec/sc/3/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389516745/u/0/f/653902/c/35207/s/4d2893ec/sc/3/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389516745/u/0/f/653902/c/35207/s/4d2893ec/sc/3/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389516745/u/0/f/653902/c/35207/s/4d2893ec/sc/3/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2893ec/sc/3/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 07:07:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084350</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Wasserknappheit-bedroht-Stromproduktion-3084350.html?wt_mc=rss.ho.beitrag.atom" title="Wasserknappheit bedroht Stromproduktion"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/1/6/4/Medupi_Power_Station_-_700-f13dbe6e74040f34.jpeg" alt="Kraftwerk" /> </a> <p>Forscher fürchten, dass die globale Stromproduktion aufgrund des Klimawandels in den nächsten 35 Jahren deutlich zurückgehen könnte. Der Grund: Dürre macht Kühlung unmöglich.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389516745/u/0/f/653902/c/35207/s/4d2893ec/sc/3/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389516745/u/0/f/653902/c/35207/s/4d2893ec/sc/3/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389516745/u/0/f/653902/c/35207/s/4d2893ec/sc/3/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389516745/u/0/f/653902/c/35207/s/4d2893ec/sc/3/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389516745/u/0/f/653902/c/35207/s/4d2893ec/sc/3/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389516745/u/0/f/653902/c/35207/s/4d2893ec/sc/3/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389516745/u/0/f/653902/c/35207/s/4d2893ec/sc/3/a2.htm"><img src="http://da.feedsportal.com/r/247389516745/u/0/f/653902/c/35207/s/4d2893ec/sc/3/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389516745/u/0/f/653902/c/35207/s/4d2893ec/sc/3/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2893ec/sc/3/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Studie: Soziale Netzwerke und schlechter Schlaf gehören zusammen</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Studie-Soziale-Netzwerke-und-schlechter-Schlaf-gehoeren-zusammen-3085003.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Verbringen junge Erwachsene Zeit in sozialen Netzwerken und greifen häufig
+ auf ihre Konten zu, leiden sie auch öfter unter Schlafstörungen. So lautet das
+ Ergebnis einer Studie von Forschern der University of Pittsburgh.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389514741/u/0/f/653902/c/35207/s/4d288bd0/sc/17/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389514741/u/0/f/653902/c/35207/s/4d288bd0/sc/17/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389514741/u/0/f/653902/c/35207/s/4d288bd0/sc/17/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389514741/u/0/f/653902/c/35207/s/4d288bd0/sc/17/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389514741/u/0/f/653902/c/35207/s/4d288bd0/sc/17/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389514741/u/0/f/653902/c/35207/s/4d288bd0/sc/17/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389514741/u/0/f/653902/c/35207/s/4d288bd0/sc/17/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389514741/u/0/f/653902/c/35207/s/4d288bd0/sc/17/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389514741/u/0/f/653902/c/35207/s/4d288bd0/sc/17/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d288bd0/sc/17/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 06:48:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085003</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Studie-Soziale-Netzwerke-und-schlechter-Schlaf-gehoeren-zusammen-3085003.html?wt_mc=rss.ho.beitrag.atom" title="Studie: Soziale Netzwerke und schlechter Schlaf gehören zusammen"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/6/0/8/urn-newsml-dpa-com-20090101-150914-99-09432_large_4_3-6b57417c781dba0f.jpeg" alt="Kind mit Tablet" /> </a> <p>Verbringen junge Erwachsene Zeit in sozialen Netzwerken und greifen häufig auf ihre Konten zu, leiden sie auch öfter unter Schlafstörungen. So lautet das Ergebnis einer Studie von Forschern der University of Pittsburgh.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389514741/u/0/f/653902/c/35207/s/4d288bd0/sc/17/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389514741/u/0/f/653902/c/35207/s/4d288bd0/sc/17/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389514741/u/0/f/653902/c/35207/s/4d288bd0/sc/17/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389514741/u/0/f/653902/c/35207/s/4d288bd0/sc/17/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389514741/u/0/f/653902/c/35207/s/4d288bd0/sc/17/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389514741/u/0/f/653902/c/35207/s/4d288bd0/sc/17/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389514741/u/0/f/653902/c/35207/s/4d288bd0/sc/17/a2.htm"><img src="http://da.feedsportal.com/r/247389514741/u/0/f/653902/c/35207/s/4d288bd0/sc/17/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389514741/u/0/f/653902/c/35207/s/4d288bd0/sc/17/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d288bd0/sc/17/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>136 Jahre nach Edisons Patent: Forscher arbeiten an einer Renaissance der
+ Glühbirne
+ </title>
+ <link>
+ http://www.heise.de/newsticker/meldung/136-Jahre-nach-Edisons-Patent-Forscher-arbeiten-an-einer-Renaissance-der-Gluehbirne-3084807.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Am 27. Januar 1880 erhielt Thomas A. Edison das Patent Nr. 223.898 für die
+ &amp;quot;Elektrische Lampe&amp;quot;. Heute wollen Forscher des MIT und der Purdue
+ University der totgesagten Glühbirne mit Nano-Beschichtung wieder eine Renaissance
+ bescheren.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389470313/u/0/f/653902/c/35207/s/4d28388a/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389470313/u/0/f/653902/c/35207/s/4d28388a/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389470313/u/0/f/653902/c/35207/s/4d28388a/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389470313/u/0/f/653902/c/35207/s/4d28388a/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389470313/u/0/f/653902/c/35207/s/4d28388a/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389470313/u/0/f/653902/c/35207/s/4d28388a/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389470313/u/0/f/653902/c/35207/s/4d28388a/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389470313/u/0/f/653902/c/35207/s/4d28388a/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389470313/u/0/f/653902/c/35207/s/4d28388a/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d28388a/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 06:30:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084807</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/136-Jahre-nach-Edisons-Patent-Forscher-arbeiten-an-einer-Renaissance-der-Gluehbirne-3084807.html?wt_mc=rss.ho.beitrag.atom" title="136 Jahre nach Edisons Patent: Forscher arbeiten an einer Renaissance der Glühbirne"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/4/8/1/b17ec7a6d8c60af156976d8240fd9726_edited_102352246_992ac921cb-4aec3b4d681674f7.jpeg" alt="Glühbirne" /> </a> <p>Am 27. Januar 1880 erhielt Thomas A. Edison das Patent Nr. 223.898 für die "Elektrische Lampe". Heute wollen Forscher des MIT und der Purdue University der totgesagten Glühbirne mit Nano-Beschichtung wieder eine Renaissance bescheren. </p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389470313/u/0/f/653902/c/35207/s/4d28388a/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389470313/u/0/f/653902/c/35207/s/4d28388a/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389470313/u/0/f/653902/c/35207/s/4d28388a/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389470313/u/0/f/653902/c/35207/s/4d28388a/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389470313/u/0/f/653902/c/35207/s/4d28388a/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389470313/u/0/f/653902/c/35207/s/4d28388a/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389470313/u/0/f/653902/c/35207/s/4d28388a/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389470313/u/0/f/653902/c/35207/s/4d28388a/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389470313/u/0/f/653902/c/35207/s/4d28388a/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d28388a/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Verschlüsselung: IETF standardisiert zwei weitere elliptische Kurven</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Verschluesselung-IETF-standardisiert-zwei-weitere-elliptische-Kurven-3084830.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Die IETF hat die beiden elliptischen Kurven Curve25519 und Curve448 als RFC
+ für Krypto-Funktionen offiziell abgesegnet. Eine Standardisierung der Kurven für den
+ Schlüsselaustausch bei TLS wird ebenfalls erwartet.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389510249/u/0/f/653902/c/35207/s/4d283e99/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389510249/u/0/f/653902/c/35207/s/4d283e99/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389510249/u/0/f/653902/c/35207/s/4d283e99/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389510249/u/0/f/653902/c/35207/s/4d283e99/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389510249/u/0/f/653902/c/35207/s/4d283e99/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389510249/u/0/f/653902/c/35207/s/4d283e99/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389510249/u/0/f/653902/c/35207/s/4d283e99/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389510249/u/0/f/653902/c/35207/s/4d283e99/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389510249/u/0/f/653902/c/35207/s/4d283e99/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d283e99/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 06:01:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084830</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Verschluesselung-IETF-standardisiert-zwei-weitere-elliptische-Kurven-3084830.html?wt_mc=rss.ho.beitrag.atom" title="Verschlüsselung: IETF standardisiert zwei weitere elliptische Kurven"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/5/0/0/curve25519-ecc1ddd469ad6cd4.png" alt="IETF verabschiedet zwei elliptische Kurven" /> </a> <p>Die IETF hat die beiden elliptischen Kurven Curve25519 und Curve448 als RFC für Krypto-Funktionen offiziell abgesegnet. Eine Standardisierung der Kurven für den Schlüsselaustausch bei TLS wird ebenfalls erwartet.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389510249/u/0/f/653902/c/35207/s/4d283e99/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389510249/u/0/f/653902/c/35207/s/4d283e99/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389510249/u/0/f/653902/c/35207/s/4d283e99/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389510249/u/0/f/653902/c/35207/s/4d283e99/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389510249/u/0/f/653902/c/35207/s/4d283e99/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389510249/u/0/f/653902/c/35207/s/4d283e99/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389510249/u/0/f/653902/c/35207/s/4d283e99/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389510249/u/0/f/653902/c/35207/s/4d283e99/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389510249/u/0/f/653902/c/35207/s/4d283e99/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d283e99/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Rekordgewinn: Apple trotzt dem starken Dollar</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Rekordgewinn-Apple-trotzt-dem-starken-Dollar-3085026.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Höchster Umsatz, höchster Reingewinn und eine Marge von 40 Prozent erzielte
+ Apple im Weihnachtsquartal. Weil frühere Weihnachtsquartale aber mehr Zuwachs
+ gebracht hatten, reagierte der Aktienmarkt leicht pikiert.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389507720/u/0/f/653902/c/35207/s/4d280d10/sc/3/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389507720/u/0/f/653902/c/35207/s/4d280d10/sc/3/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389507720/u/0/f/653902/c/35207/s/4d280d10/sc/3/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389507720/u/0/f/653902/c/35207/s/4d280d10/sc/3/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389507720/u/0/f/653902/c/35207/s/4d280d10/sc/3/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389507720/u/0/f/653902/c/35207/s/4d280d10/sc/3/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389507720/u/0/f/653902/c/35207/s/4d280d10/sc/3/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389507720/u/0/f/653902/c/35207/s/4d280d10/sc/3/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389507720/u/0/f/653902/c/35207/s/4d280d10/sc/3/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d280d10/sc/3/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 05:34:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3085026</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Rekordgewinn-Apple-trotzt-dem-starken-Dollar-3085026.html?wt_mc=rss.ho.beitrag.atom" title="Rekordgewinn: Apple trotzt dem starken Dollar"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/6/1/8/5bc9e08b71b608e7c32186a87b76aa6a_edited_102365868_95dcadf632-9fbfdf75e421933d.jpeg" alt="Abfallende Kurve" /> </a> <p>Höchster Umsatz, höchster Reingewinn und eine Marge von 40 Prozent erzielte Apple im Weihnachtsquartal. Weil frühere Weihnachtsquartale aber mehr Zuwachs gebracht hatten, reagierte der Aktienmarkt leicht pikiert.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389507720/u/0/f/653902/c/35207/s/4d280d10/sc/3/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389507720/u/0/f/653902/c/35207/s/4d280d10/sc/3/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389507720/u/0/f/653902/c/35207/s/4d280d10/sc/3/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389507720/u/0/f/653902/c/35207/s/4d280d10/sc/3/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389507720/u/0/f/653902/c/35207/s/4d280d10/sc/3/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389507720/u/0/f/653902/c/35207/s/4d280d10/sc/3/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389507720/u/0/f/653902/c/35207/s/4d280d10/sc/3/a2.htm"><img src="http://da.feedsportal.com/r/247389507720/u/0/f/653902/c/35207/s/4d280d10/sc/3/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389507720/u/0/f/653902/c/35207/s/4d280d10/sc/3/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d280d10/sc/3/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Dank Eigenbau-Exoskelett: Der menschliche Wagenheber</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Dank-Eigenbau-Exoskelett-Der-menschliche-Wagenheber-3084868.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Wie viel Krafttraining braucht man, um die Hinterachse eines Mini Coopers
+ vom Boden zu heben? Keines, jedenfalls wenn man das pneumatische Exoskelett benutzt,
+ das James Hobson selbst gebaut hat.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389498069/u/0/f/653902/c/35207/s/4d281348/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389498069/u/0/f/653902/c/35207/s/4d281348/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389498069/u/0/f/653902/c/35207/s/4d281348/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389498069/u/0/f/653902/c/35207/s/4d281348/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389498069/u/0/f/653902/c/35207/s/4d281348/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389498069/u/0/f/653902/c/35207/s/4d281348/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389498069/u/0/f/653902/c/35207/s/4d281348/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389498069/u/0/f/653902/c/35207/s/4d281348/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389498069/u/0/f/653902/c/35207/s/4d281348/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d281348/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Wed, 27 Jan 2016 05:00:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084868</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Dank-Eigenbau-Exoskelett-Der-menschliche-Wagenheber-3084868.html?wt_mc=rss.ho.beitrag.atom" title="Dank Eigenbau-Exoskelett: Der menschliche Wagenheber"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/5/2/0/Homemade_Exoskeleton_Lifts_Mini_Cooper__-_YouTube_2016-01-26_17-52-24-f2bb9d9aa796b6cb.png" alt="DIY Exoskelett hebt Auto" /> </a> <p>Wie viel Krafttraining braucht man, um die Hinterachse eines Mini Coopers vom Boden zu heben? Keines, jedenfalls wenn man das pneumatische Exoskelett benutzt, das James Hobson selbst gebaut hat.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389498069/u/0/f/653902/c/35207/s/4d281348/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389498069/u/0/f/653902/c/35207/s/4d281348/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389498069/u/0/f/653902/c/35207/s/4d281348/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389498069/u/0/f/653902/c/35207/s/4d281348/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389498069/u/0/f/653902/c/35207/s/4d281348/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389498069/u/0/f/653902/c/35207/s/4d281348/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389498069/u/0/f/653902/c/35207/s/4d281348/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389498069/u/0/f/653902/c/35207/s/4d281348/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389498069/u/0/f/653902/c/35207/s/4d281348/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d281348/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>"Online-Kriminellen nicht hinterherlaufen" – Mehr EU-Kooperation gegen
+ Cyberkriminalität
+ </title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Online-Kriminellen-nicht-hinterherlaufen-Mehr-EU-Kooperation-gegen-Cyberkriminalitaet-3084940.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Wer ein Verbrechen begeht, hinterlässt oft Spuren – das gilt auch im
+ Internet. Doch in der virtuellen Welt ist die Strafverfolgung schwierig. Was wenn
+ der Täter von einem weit entfernten Erdteil agiert oder Daten dort gespeichert sind?&lt;br
+ clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389422225/u/0/f/653902/c/35207/s/4d25a88b/sc/3/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389422225/u/0/f/653902/c/35207/s/4d25a88b/sc/3/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389422225/u/0/f/653902/c/35207/s/4d25a88b/sc/3/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389422225/u/0/f/653902/c/35207/s/4d25a88b/sc/3/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389422225/u/0/f/653902/c/35207/s/4d25a88b/sc/3/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389422225/u/0/f/653902/c/35207/s/4d25a88b/sc/3/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389422225/u/0/f/653902/c/35207/s/4d25a88b/sc/3/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389422225/u/0/f/653902/c/35207/s/4d25a88b/sc/3/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389422225/u/0/f/653902/c/35207/s/4d25a88b/sc/3/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d25a88b/sc/3/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 18:08:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084940</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Online-Kriminellen-nicht-hinterherlaufen-Mehr-EU-Kooperation-gegen-Cyberkriminalitaet-3084940.html?wt_mc=rss.ho.beitrag.atom" title="&quot;Online-Kriminellen nicht hinterherlaufen&quot; – Mehr EU-Kooperation gegen Cyberkriminalität"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/5/6/3/urn-newsml-dpa-com-20090101-150617-99-05723_large_4_3-6d024b06feefb739.jpeg" alt="&quot;Enter Password&quot;" /> </a> <p>Wer ein Verbrechen begeht, hinterlässt oft Spuren – das gilt auch im Internet. Doch in der virtuellen Welt ist die Strafverfolgung schwierig. Was wenn der Täter von einem weit entfernten Erdteil agiert oder Daten dort gespeichert sind?</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389422225/u/0/f/653902/c/35207/s/4d25a88b/sc/3/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389422225/u/0/f/653902/c/35207/s/4d25a88b/sc/3/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389422225/u/0/f/653902/c/35207/s/4d25a88b/sc/3/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389422225/u/0/f/653902/c/35207/s/4d25a88b/sc/3/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389422225/u/0/f/653902/c/35207/s/4d25a88b/sc/3/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389422225/u/0/f/653902/c/35207/s/4d25a88b/sc/3/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389422225/u/0/f/653902/c/35207/s/4d25a88b/sc/3/a2.htm"><img src="http://da.feedsportal.com/r/247389422225/u/0/f/653902/c/35207/s/4d25a88b/sc/3/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389422225/u/0/f/653902/c/35207/s/4d25a88b/sc/3/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d25a88b/sc/3/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Jolla: Update für Sailfish OS, Tablet-Abwicklung ungeklärt</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Jolla-Update-fuer-Sailfish-OS-Tablet-Abwicklung-ungeklaert-3084918.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Die Abwicklung des gescheiterten Tablet-Projekts verzögert sich bei Jolla
+ weiter. Dafür stellt das Start-up ein Update für sein Betriebssystem Sailfish OS
+ bereit und Partner Intex kündigt für den MWC ein Smartphone mit Sailfish OS an.&lt;br
+ clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389460756/u/0/f/653902/c/35207/s/4d256c68/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389460756/u/0/f/653902/c/35207/s/4d256c68/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389460756/u/0/f/653902/c/35207/s/4d256c68/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389460756/u/0/f/653902/c/35207/s/4d256c68/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389460756/u/0/f/653902/c/35207/s/4d256c68/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389460756/u/0/f/653902/c/35207/s/4d256c68/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389460756/u/0/f/653902/c/35207/s/4d256c68/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389460756/u/0/f/653902/c/35207/s/4d256c68/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389460756/u/0/f/653902/c/35207/s/4d256c68/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d256c68/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 17:41:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084918</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Jolla-Update-fuer-Sailfish-OS-Tablet-Abwicklung-ungeklaert-3084918.html?wt_mc=rss.ho.beitrag.atom" title="Jolla: Update für Sailfish OS, Tablet-Abwicklung ungeklärt"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/5/4/7/jolla-97b85423540dfde4.png" alt="Jolla: Update für Sailfish OS, Tablet-Abwicklung ungeklärt" /> </a> <p>Die Abwicklung des gescheiterten Tablet-Projekts verzögert sich bei Jolla weiter. Dafür stellt das Start-up ein Update für sein Betriebssystem Sailfish OS bereit und Partner Intex kündigt für den MWC ein Smartphone mit Sailfish OS an.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389460756/u/0/f/653902/c/35207/s/4d256c68/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389460756/u/0/f/653902/c/35207/s/4d256c68/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389460756/u/0/f/653902/c/35207/s/4d256c68/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389460756/u/0/f/653902/c/35207/s/4d256c68/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389460756/u/0/f/653902/c/35207/s/4d256c68/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389460756/u/0/f/653902/c/35207/s/4d256c68/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389460756/u/0/f/653902/c/35207/s/4d256c68/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389460756/u/0/f/653902/c/35207/s/4d256c68/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389460756/u/0/f/653902/c/35207/s/4d256c68/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d256c68/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Periscope ermöglicht Livestreaming mit GoPro-Kamera</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Periscope-ermoeglicht-Livestreaming-mit-GoPro-Kamera-3084901.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Die iPhone-App von Twitters Livestreaming-Dienstes unterstützt nun GoPro:
+ Die Aufnahme der Action-Cam lässt sich so unmittelbar ausstrahlen – im Wechsel mit
+ der iPhone-Kamera&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389416151/u/0/f/653902/c/35207/s/4d252b88/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389416151/u/0/f/653902/c/35207/s/4d252b88/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389416151/u/0/f/653902/c/35207/s/4d252b88/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389416151/u/0/f/653902/c/35207/s/4d252b88/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389416151/u/0/f/653902/c/35207/s/4d252b88/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389416151/u/0/f/653902/c/35207/s/4d252b88/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389416151/u/0/f/653902/c/35207/s/4d252b88/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389416151/u/0/f/653902/c/35207/s/4d252b88/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389416151/u/0/f/653902/c/35207/s/4d252b88/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d252b88/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 17:11:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084901</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Periscope-ermoeglicht-Livestreaming-mit-GoPro-Kamera-3084901.html?wt_mc=rss.ho.beitrag.atom" title="Periscope ermöglicht Livestreaming mit GoPro-Kamera"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/5/3/3/Bildschirmfoto_2016-01-26_um_17-5a21fc7e60faedc5.png" alt="Periscope GoPro" /> </a> <p>Die iPhone-App von Twitters Livestreaming-Dienstes unterstützt nun GoPro: Die Aufnahme der Action-Cam lässt sich so unmittelbar ausstrahlen – im Wechsel mit der iPhone-Kamera</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389416151/u/0/f/653902/c/35207/s/4d252b88/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389416151/u/0/f/653902/c/35207/s/4d252b88/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389416151/u/0/f/653902/c/35207/s/4d252b88/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389416151/u/0/f/653902/c/35207/s/4d252b88/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389416151/u/0/f/653902/c/35207/s/4d252b88/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389416151/u/0/f/653902/c/35207/s/4d252b88/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389416151/u/0/f/653902/c/35207/s/4d252b88/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389416151/u/0/f/653902/c/35207/s/4d252b88/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389416151/u/0/f/653902/c/35207/s/4d252b88/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d252b88/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Globales Satelliten-Internet: Airbus gründet Joint Venture mit OneWeb</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Globales-Satelliten-Internet-Airbus-gruendet-Joint-Venture-mit-OneWeb-3084840.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Der europäische Rüstungs- und Weltraumkonzern Airbus arbeitet jetzt noch
+ enger mit OneWeb zusammen bei dem Vorhaben, Hunderte Satelliten ins All zu schießen,
+ die die ganze Erde mit Internet versorgen sollen.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389454758/u/0/f/653902/c/35207/s/4d252c8b/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389454758/u/0/f/653902/c/35207/s/4d252c8b/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389454758/u/0/f/653902/c/35207/s/4d252c8b/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389454758/u/0/f/653902/c/35207/s/4d252c8b/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389454758/u/0/f/653902/c/35207/s/4d252c8b/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389454758/u/0/f/653902/c/35207/s/4d252c8b/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389454758/u/0/f/653902/c/35207/s/4d252c8b/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389454758/u/0/f/653902/c/35207/s/4d252c8b/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389454758/u/0/f/653902/c/35207/s/4d252c8b/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d252c8b/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 16:38:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084840</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Globales-Satelliten-Internet-Airbus-gruendet-Joint-Venture-mit-OneWeb-3084840.html?wt_mc=rss.ho.beitrag.atom" title="Globales Satelliten-Internet: Airbus gründet Joint Venture mit OneWeb"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/5/1/0/slide-2-42ff2dea3b808ad3.jpeg" alt="Globales Satelliten-Internet: Airbus gründet Joint Venture mit OneWeb" /> </a> <p>Der europäische Rüstungs- und Weltraumkonzern Airbus arbeitet jetzt noch enger mit OneWeb zusammen bei dem Vorhaben, Hunderte Satelliten ins All zu schießen, die die ganze Erde mit Internet versorgen sollen.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389454758/u/0/f/653902/c/35207/s/4d252c8b/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389454758/u/0/f/653902/c/35207/s/4d252c8b/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389454758/u/0/f/653902/c/35207/s/4d252c8b/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389454758/u/0/f/653902/c/35207/s/4d252c8b/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389454758/u/0/f/653902/c/35207/s/4d252c8b/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389454758/u/0/f/653902/c/35207/s/4d252c8b/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389454758/u/0/f/653902/c/35207/s/4d252c8b/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389454758/u/0/f/653902/c/35207/s/4d252c8b/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389454758/u/0/f/653902/c/35207/s/4d252c8b/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d252c8b/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Verfassungsgericht stoppt Vorratsdatenspeicherung vorerst nicht</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Verfassungsgericht-stoppt-Vorratsdatenspeicherung-vorerst-nicht-3084879.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Das Bundesverfassungsgericht hat einen Antrag aus einer
+ Verfassungsbeschwerde abgelehnt, wonach die neue Speicherpflicht für elektronische
+ Nutzerspuren zunächst gar nicht greifen sollte. In der Sache ist damit aber noch
+ nichts entschieden.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389454757/u/0/f/653902/c/35207/s/4d252c8a/sc/3/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389454757/u/0/f/653902/c/35207/s/4d252c8a/sc/3/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389454757/u/0/f/653902/c/35207/s/4d252c8a/sc/3/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389454757/u/0/f/653902/c/35207/s/4d252c8a/sc/3/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389454757/u/0/f/653902/c/35207/s/4d252c8a/sc/3/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389454757/u/0/f/653902/c/35207/s/4d252c8a/sc/3/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389454757/u/0/f/653902/c/35207/s/4d252c8a/sc/3/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389454757/u/0/f/653902/c/35207/s/4d252c8a/sc/3/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389454757/u/0/f/653902/c/35207/s/4d252c8a/sc/3/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d252c8a/sc/3/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 16:37:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084879</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Verfassungsgericht-stoppt-Vorratsdatenspeicherung-vorerst-nicht-3084879.html?wt_mc=rss.ho.beitrag.atom" title="Verfassungsgericht stoppt Vorratsdatenspeicherung vorerst nicht"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/5/2/7/Bildschirmfoto_2016-01-26_um_17-1ebffe496feca16d.jpeg" alt="Verfassungsgericht stoppt Vorratsdatenspeicherung vorerst nicht" /> </a> <p>Das Bundesverfassungsgericht hat einen Antrag aus einer Verfassungsbeschwerde abgelehnt, wonach die neue Speicherpflicht für elektronische Nutzerspuren zunächst gar nicht greifen sollte. In der Sache ist damit aber noch nichts entschieden.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389454757/u/0/f/653902/c/35207/s/4d252c8a/sc/3/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389454757/u/0/f/653902/c/35207/s/4d252c8a/sc/3/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389454757/u/0/f/653902/c/35207/s/4d252c8a/sc/3/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389454757/u/0/f/653902/c/35207/s/4d252c8a/sc/3/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389454757/u/0/f/653902/c/35207/s/4d252c8a/sc/3/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389454757/u/0/f/653902/c/35207/s/4d252c8a/sc/3/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389454757/u/0/f/653902/c/35207/s/4d252c8a/sc/3/a2.htm"><img src="http://da.feedsportal.com/r/247389454757/u/0/f/653902/c/35207/s/4d252c8a/sc/3/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389454757/u/0/f/653902/c/35207/s/4d252c8a/sc/3/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d252c8a/sc/3/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Intel enthüllt erste Skylake-Prozessoren mit Iris-Pro-Grafik</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Intel-enthuellt-erste-Skylake-Prozessoren-mit-Iris-Pro-Grafik-3084686.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>In Intels neustem Preislisten-Update finden sich etliche neue
+ Skylake-Modelle für Notebooks, darunter das neue Flaggschiff: der rund 1200
+ US-Dollar teure Xeon E3-1575M v5 mit Iris Pro P580.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389453021/u/0/f/653902/c/35207/s/4d24fe5f/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389453021/u/0/f/653902/c/35207/s/4d24fe5f/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389453021/u/0/f/653902/c/35207/s/4d24fe5f/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389453021/u/0/f/653902/c/35207/s/4d24fe5f/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389453021/u/0/f/653902/c/35207/s/4d24fe5f/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389453021/u/0/f/653902/c/35207/s/4d24fe5f/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389453021/u/0/f/653902/c/35207/s/4d24fe5f/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389453021/u/0/f/653902/c/35207/s/4d24fe5f/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389453021/u/0/f/653902/c/35207/s/4d24fe5f/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d24fe5f/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 16:27:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084686</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Intel-enthuellt-erste-Skylake-Prozessoren-mit-Iris-Pro-Grafik-3084686.html?wt_mc=rss.ho.beitrag.atom" title="Intel enthüllt erste Skylake-Prozessoren mit Iris-Pro-Grafik"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/4/0/9/Intel-Waferspin1-82c1b072cc5449c0.png" alt="Intel" /> </a> <p>In Intels neustem Preislisten-Update finden sich etliche neue Skylake-Modelle für Notebooks, darunter das neue Flaggschiff: der rund 1200 US-Dollar teure Xeon E3-1575M v5 mit Iris Pro P580.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389453021/u/0/f/653902/c/35207/s/4d24fe5f/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389453021/u/0/f/653902/c/35207/s/4d24fe5f/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389453021/u/0/f/653902/c/35207/s/4d24fe5f/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389453021/u/0/f/653902/c/35207/s/4d24fe5f/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389453021/u/0/f/653902/c/35207/s/4d24fe5f/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389453021/u/0/f/653902/c/35207/s/4d24fe5f/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389453021/u/0/f/653902/c/35207/s/4d24fe5f/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389453021/u/0/f/653902/c/35207/s/4d24fe5f/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389453021/u/0/f/653902/c/35207/s/4d24fe5f/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d24fe5f/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Pornografie-Vorwurf: Pakistan sperrt mehr als 400.000 Webseiten</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Pornografie-Vorwurf-Pakistan-sperrt-mehr-als-400-000-Webseiten-3084805.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Eine Woche nach der Wiederzulassung von YouTube hat Pakistans
+ Telekommunikationsbehörde angekündigt, eine halbe Million Webseiten wegen
+ angeblicher Pornografie zu löschen. Welche Seiten es letztlich trifft, ist noch
+ nicht abzusehen.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389441451/u/0/f/653902/c/35207/s/4d24d43d/sc/17/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389441451/u/0/f/653902/c/35207/s/4d24d43d/sc/17/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389441451/u/0/f/653902/c/35207/s/4d24d43d/sc/17/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389441451/u/0/f/653902/c/35207/s/4d24d43d/sc/17/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389441451/u/0/f/653902/c/35207/s/4d24d43d/sc/17/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389441451/u/0/f/653902/c/35207/s/4d24d43d/sc/17/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389441451/u/0/f/653902/c/35207/s/4d24d43d/sc/17/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389441451/u/0/f/653902/c/35207/s/4d24d43d/sc/17/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389441451/u/0/f/653902/c/35207/s/4d24d43d/sc/17/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d24d43d/sc/17/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 15:50:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084805</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Pornografie-Vorwurf-Pakistan-sperrt-mehr-als-400-000-Webseiten-3084805.html?wt_mc=rss.ho.beitrag.atom" title="Pornografie-Vorwurf: Pakistan sperrt mehr als 400.000 Webseiten"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/4/7/9/urn-newsml-dpa-com-20090101-150316-99-09245_large_4_3-bea5f029e8655dbf.jpeg" alt="Surfen im Internet" /> </a> <p>Eine Woche nach der Wiederzulassung von YouTube hat Pakistans Telekommunikationsbehörde angekündigt, eine halbe Million Webseiten wegen angeblicher Pornografie zu löschen. Welche Seiten es letztlich trifft, ist noch nicht abzusehen.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389441451/u/0/f/653902/c/35207/s/4d24d43d/sc/17/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389441451/u/0/f/653902/c/35207/s/4d24d43d/sc/17/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389441451/u/0/f/653902/c/35207/s/4d24d43d/sc/17/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389441451/u/0/f/653902/c/35207/s/4d24d43d/sc/17/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389441451/u/0/f/653902/c/35207/s/4d24d43d/sc/17/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389441451/u/0/f/653902/c/35207/s/4d24d43d/sc/17/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389441451/u/0/f/653902/c/35207/s/4d24d43d/sc/17/a2.htm"><img src="http://da.feedsportal.com/r/247389441451/u/0/f/653902/c/35207/s/4d24d43d/sc/17/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389441451/u/0/f/653902/c/35207/s/4d24d43d/sc/17/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d24d43d/sc/17/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Phishing-SMS: Identitätsdiebstahl bei Car2go-Kunden</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Phishing-SMS-Identitaetsdiebstahl-bei-Car2go-Kunden-3084730.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Über eine SMS wollen Betrüger Car2go-Nutzer auf eine Phishing-Webseite
+ locken und persönliche Daten abziehen.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389439795/u/0/f/653902/c/35207/s/4d24cd38/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389439795/u/0/f/653902/c/35207/s/4d24cd38/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389439795/u/0/f/653902/c/35207/s/4d24cd38/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389439795/u/0/f/653902/c/35207/s/4d24cd38/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389439795/u/0/f/653902/c/35207/s/4d24cd38/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389439795/u/0/f/653902/c/35207/s/4d24cd38/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389439795/u/0/f/653902/c/35207/s/4d24cd38/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389439795/u/0/f/653902/c/35207/s/4d24cd38/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389439795/u/0/f/653902/c/35207/s/4d24cd38/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d24cd38/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 15:39:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084730</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Phishing-SMS-Identitaetsdiebstahl-bei-Car2go-Kunden-3084730.html?wt_mc=rss.ho.beitrag.atom" title="Phishing-SMS: Identitätsdiebstahl bei Car2go-Kunden"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/4/3/4/urn-newsml-dpa-com-20090101-141016-99-05211_large_4_3-b82d617fe2ae9b7d.jpeg" alt="SMS" /> </a> <p>Über eine SMS wollen Betrüger Car2go-Nutzer auf eine Phishing-Webseite locken und persönliche Daten abziehen. </p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389439795/u/0/f/653902/c/35207/s/4d24cd38/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389439795/u/0/f/653902/c/35207/s/4d24cd38/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389439795/u/0/f/653902/c/35207/s/4d24cd38/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389439795/u/0/f/653902/c/35207/s/4d24cd38/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389439795/u/0/f/653902/c/35207/s/4d24cd38/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389439795/u/0/f/653902/c/35207/s/4d24cd38/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389439795/u/0/f/653902/c/35207/s/4d24cd38/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389439795/u/0/f/653902/c/35207/s/4d24cd38/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389439795/u/0/f/653902/c/35207/s/4d24cd38/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d24cd38/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Studie: Vernetzung von Autos schafft mehr Sicherheit – aber auch Skepsis</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Studie-Vernetzung-von-Autos-schafft-mehr-Sicherheit-aber-auch-Skepsis-3084679.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Analysten im Auftrag des Wirtschaftsministeriums kommen zu dem Ergebnis,
+ dass die Vernetzung von Fahrzeugen und Straßen Sicherheit und Komfort verbessert.
+ Die Angst vorm gläsernen Fahrer müsse aber bewältigt werden.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389446423/u/0/f/653902/c/35207/s/4d24bc09/sc/3/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389446423/u/0/f/653902/c/35207/s/4d24bc09/sc/3/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389446423/u/0/f/653902/c/35207/s/4d24bc09/sc/3/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389446423/u/0/f/653902/c/35207/s/4d24bc09/sc/3/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389446423/u/0/f/653902/c/35207/s/4d24bc09/sc/3/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389446423/u/0/f/653902/c/35207/s/4d24bc09/sc/3/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389446423/u/0/f/653902/c/35207/s/4d24bc09/sc/3/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389446423/u/0/f/653902/c/35207/s/4d24bc09/sc/3/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389446423/u/0/f/653902/c/35207/s/4d24bc09/sc/3/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d24bc09/sc/3/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 15:18:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084679</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Studie-Vernetzung-von-Autos-schafft-mehr-Sicherheit-aber-auch-Skepsis-3084679.html?wt_mc=rss.ho.beitrag.atom" title="Studie: Vernetzung von Autos schafft mehr Sicherheit – aber auch Skepsis"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/4/0/4/Bildschirmfoto_2016-01-26_um_15-4f813edda1a7e109.jpeg" alt="Vernetzte Autos" /> </a> <p>Analysten im Auftrag des Wirtschaftsministeriums kommen zu dem Ergebnis, dass die Vernetzung von Fahrzeugen und Straßen Sicherheit und Komfort verbessert. Die Angst vorm gläsernen Fahrer müsse aber bewältigt werden.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389446423/u/0/f/653902/c/35207/s/4d24bc09/sc/3/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389446423/u/0/f/653902/c/35207/s/4d24bc09/sc/3/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389446423/u/0/f/653902/c/35207/s/4d24bc09/sc/3/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389446423/u/0/f/653902/c/35207/s/4d24bc09/sc/3/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389446423/u/0/f/653902/c/35207/s/4d24bc09/sc/3/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389446423/u/0/f/653902/c/35207/s/4d24bc09/sc/3/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389446423/u/0/f/653902/c/35207/s/4d24bc09/sc/3/a2.htm"><img src="http://da.feedsportal.com/r/247389446423/u/0/f/653902/c/35207/s/4d24bc09/sc/3/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389446423/u/0/f/653902/c/35207/s/4d24bc09/sc/3/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d24bc09/sc/3/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Spezieller Einhandmodus für Microsofts iPhone-Tastatur</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Spezieller-Einhandmodus-fuer-Microsofts-iPhone-Tastatur-3084738.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Die geplante iOS-Version von Microsofts Word-Flow-Keyboard erhält einem
+ Bericht zufolge einen besonderen Modus für die einhändige Bedienung. Der öffentliche
+ Beta-Test der Tastatur soll bald beginnen.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389403553/u/0/f/653902/c/35207/s/4d2495f5/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389403553/u/0/f/653902/c/35207/s/4d2495f5/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389403553/u/0/f/653902/c/35207/s/4d2495f5/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389403553/u/0/f/653902/c/35207/s/4d2495f5/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389403553/u/0/f/653902/c/35207/s/4d2495f5/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389403553/u/0/f/653902/c/35207/s/4d2495f5/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389403553/u/0/f/653902/c/35207/s/4d2495f5/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389403553/u/0/f/653902/c/35207/s/4d2495f5/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389403553/u/0/f/653902/c/35207/s/4d2495f5/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2495f5/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 15:10:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084738</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Spezieller-Einhandmodus-fuer-Microsofts-iPhone-Tastatur-3084738.html?wt_mc=rss.ho.beitrag.atom" title="Spezieller Einhandmodus für Microsofts iPhone-Tastatur"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/4/4/2/Bildschirmfoto_2016-01-26_um_15-086c14c35aded06e.png" alt="Word Flow Microsoft" /> </a> <p>Die geplante iOS-Version von Microsofts Word-Flow-Keyboard erhält einem Bericht zufolge einen besonderen Modus für die einhändige Bedienung. Der öffentliche Beta-Test der Tastatur soll bald beginnen.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389403553/u/0/f/653902/c/35207/s/4d2495f5/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389403553/u/0/f/653902/c/35207/s/4d2495f5/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389403553/u/0/f/653902/c/35207/s/4d2495f5/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389403553/u/0/f/653902/c/35207/s/4d2495f5/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389403553/u/0/f/653902/c/35207/s/4d2495f5/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389403553/u/0/f/653902/c/35207/s/4d2495f5/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389403553/u/0/f/653902/c/35207/s/4d2495f5/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389403553/u/0/f/653902/c/35207/s/4d2495f5/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389403553/u/0/f/653902/c/35207/s/4d2495f5/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2495f5/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Groupon schließt in der Schweiz und in Österreich</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Groupon-schliesst-in-der-Schweiz-und-in-Oesterreich-3084595.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Das Rabatt-Portal Groupon reduziert sein internationales Angebot: Zum 25.
+ Januar hat das Unternehmen seine Geschäfte in Österreich und in der Schweiz
+ eingestellt.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389398598/u/0/f/653902/c/35207/s/4d245ae7/sc/3/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389398598/u/0/f/653902/c/35207/s/4d245ae7/sc/3/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389398598/u/0/f/653902/c/35207/s/4d245ae7/sc/3/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389398598/u/0/f/653902/c/35207/s/4d245ae7/sc/3/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389398598/u/0/f/653902/c/35207/s/4d245ae7/sc/3/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389398598/u/0/f/653902/c/35207/s/4d245ae7/sc/3/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389398598/u/0/f/653902/c/35207/s/4d245ae7/sc/3/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389398598/u/0/f/653902/c/35207/s/4d245ae7/sc/3/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389398598/u/0/f/653902/c/35207/s/4d245ae7/sc/3/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d245ae7/sc/3/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 14:30:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084595</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Groupon-schliesst-in-der-Schweiz-und-in-Oesterreich-3084595.html?wt_mc=rss.ho.beitrag.atom" title="Groupon schließt in der Schweiz und in Österreich"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/3/5/9/urn-newsml-dpa-com-20090101-150506-99-02759_large_4_3-0f1df47004187636.jpeg" alt="Groupon" /> </a> <p>Das Rabatt-Portal Groupon reduziert sein internationales Angebot: Zum 25. Januar hat das Unternehmen seine Geschäfte in Österreich und in der Schweiz eingestellt.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389398598/u/0/f/653902/c/35207/s/4d245ae7/sc/3/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389398598/u/0/f/653902/c/35207/s/4d245ae7/sc/3/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389398598/u/0/f/653902/c/35207/s/4d245ae7/sc/3/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389398598/u/0/f/653902/c/35207/s/4d245ae7/sc/3/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389398598/u/0/f/653902/c/35207/s/4d245ae7/sc/3/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389398598/u/0/f/653902/c/35207/s/4d245ae7/sc/3/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389398598/u/0/f/653902/c/35207/s/4d245ae7/sc/3/a2.htm"><img src="http://da.feedsportal.com/r/247389398598/u/0/f/653902/c/35207/s/4d245ae7/sc/3/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389398598/u/0/f/653902/c/35207/s/4d245ae7/sc/3/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d245ae7/sc/3/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Bilderkennung: Algorithmen aus Jena sollen Tiere auch in Bewegung bestimmen
+ können
+ </title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Bilderkennung-Algorithmen-aus-Jena-sollen-Tiere-auch-in-Bewegung-bestimmen-koennen-3084658.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>So manches Elternteil wird stutzen, wenn das Kind auf dem Waldspaziergang
+ nach dem Namen einer Pflanze oder eines Tieres fragt. Forscher der Uni Jena könnten
+ mit einem von ihnen entwickelten Verfahren vielleicht bald Apphilfe schaffen.&lt;br
+ clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389398597/u/0/f/653902/c/35207/s/4d245ae6/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389398597/u/0/f/653902/c/35207/s/4d245ae6/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389398597/u/0/f/653902/c/35207/s/4d245ae6/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389398597/u/0/f/653902/c/35207/s/4d245ae6/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389398597/u/0/f/653902/c/35207/s/4d245ae6/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389398597/u/0/f/653902/c/35207/s/4d245ae6/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389398597/u/0/f/653902/c/35207/s/4d245ae6/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389398597/u/0/f/653902/c/35207/s/4d245ae6/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389398597/u/0/f/653902/c/35207/s/4d245ae6/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d245ae6/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 14:20:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084658</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Bilderkennung-Algorithmen-aus-Jena-sollen-Tiere-auch-in-Bewegung-bestimmen-koennen-3084658.html?wt_mc=rss.ho.beitrag.atom" title="Bilderkennung: Algorithmen aus Jena sollen Tiere auch in Bewegung bestimmen können"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/3/9/0/Tabletamsel_ka-1-171b29b5d58fb7f5.jpeg" alt="Bilderkennung: Algorithmen aus Jena sollen Tiere auch in Bewegung bestimmen können" /> </a> <p>So manches Elternteil wird stutzen, wenn das Kind auf dem Waldspaziergang nach dem Namen einer Pflanze oder eines Tieres fragt. Forscher der Uni Jena könnten mit einem von ihnen entwickelten Verfahren vielleicht bald Apphilfe schaffen.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389398597/u/0/f/653902/c/35207/s/4d245ae6/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389398597/u/0/f/653902/c/35207/s/4d245ae6/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389398597/u/0/f/653902/c/35207/s/4d245ae6/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389398597/u/0/f/653902/c/35207/s/4d245ae6/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389398597/u/0/f/653902/c/35207/s/4d245ae6/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389398597/u/0/f/653902/c/35207/s/4d245ae6/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389398597/u/0/f/653902/c/35207/s/4d245ae6/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389398597/u/0/f/653902/c/35207/s/4d245ae6/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389398597/u/0/f/653902/c/35207/s/4d245ae6/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d245ae6/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Neue Folgen von Akte X: "Ich will es immer noch glauben"</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Neue-Folgen-von-Akte-X-Ich-will-es-immer-noch-glauben-3084426.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>13 Jahre nach ihrem Ende geht die US-Serie &amp;quot;Akte X&amp;quot;
+ weiter, als Fortsetzung des Klassikers in modernem Gewand und unserer Zeit.
+ Inhaltlich bleibt sie sich treu: An allem sind geheime Verschwörer schuld – eine
+ Einschätzung, die seltsam bekannt klingt.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389434588/u/0/f/653902/c/35207/s/4d240285/sc/17/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389434588/u/0/f/653902/c/35207/s/4d240285/sc/17/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389434588/u/0/f/653902/c/35207/s/4d240285/sc/17/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389434588/u/0/f/653902/c/35207/s/4d240285/sc/17/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389434588/u/0/f/653902/c/35207/s/4d240285/sc/17/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389434588/u/0/f/653902/c/35207/s/4d240285/sc/17/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389434588/u/0/f/653902/c/35207/s/4d240285/sc/17/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389434588/u/0/f/653902/c/35207/s/4d240285/sc/17/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389434588/u/0/f/653902/c/35207/s/4d240285/sc/17/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d240285/sc/17/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 13:35:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084426</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Neue-Folgen-von-Akte-X-Ich-will-es-immer-noch-glauben-3084426.html?wt_mc=rss.ho.beitrag.atom" title="Neue Folgen von Akte X: &quot;Ich will es immer noch glauben&quot;"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/2/2/4/vlcsnap-2016-01-26-12h57m41s255-7b7bc03717696405.jpeg" alt="Neue Folgen von Akte X: &quot;Ich will es nur glauben&quot;" /> </a> <p>13 Jahre nach ihrem Ende geht die US-Serie "Akte X" weiter, als Fortsetzung des Klassikers in modernem Gewand und unserer Zeit. Inhaltlich bleibt sie sich treu: An allem sind geheime Verschwörer schuld – eine Einschätzung, die seltsam bekannt klingt.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389434588/u/0/f/653902/c/35207/s/4d240285/sc/17/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389434588/u/0/f/653902/c/35207/s/4d240285/sc/17/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389434588/u/0/f/653902/c/35207/s/4d240285/sc/17/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389434588/u/0/f/653902/c/35207/s/4d240285/sc/17/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389434588/u/0/f/653902/c/35207/s/4d240285/sc/17/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389434588/u/0/f/653902/c/35207/s/4d240285/sc/17/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389434588/u/0/f/653902/c/35207/s/4d240285/sc/17/a2.htm"><img src="http://da.feedsportal.com/r/247389434588/u/0/f/653902/c/35207/s/4d240285/sc/17/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389434588/u/0/f/653902/c/35207/s/4d240285/sc/17/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d240285/sc/17/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Frag den Bundestag: Parlamentsgutachten sollen öffentlich werden</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Frag-den-Bundestag-Parlamentsgutachten-sollen-oeffentlich-werden-3084481.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Über das neue Portal &amp;quot;Frag den Bundestag&amp;quot; können Nutzer
+ per Knopfdruck Studien des Wissenschaftlichen Dienstes des Bundestags beantragen. So
+ soll es möglich werden, alle herausgegebenen Untersuchungen in einer
+ Online-Bibliothek zu veröffentlichen.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389434587/u/0/f/653902/c/35207/s/4d240284/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389434587/u/0/f/653902/c/35207/s/4d240284/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389434587/u/0/f/653902/c/35207/s/4d240284/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389434587/u/0/f/653902/c/35207/s/4d240284/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389434587/u/0/f/653902/c/35207/s/4d240284/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389434587/u/0/f/653902/c/35207/s/4d240284/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389434587/u/0/f/653902/c/35207/s/4d240284/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389434587/u/0/f/653902/c/35207/s/4d240284/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389434587/u/0/f/653902/c/35207/s/4d240284/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d240284/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 13:32:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084481</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Frag-den-Bundestag-Parlamentsgutachten-sollen-oeffentlich-werden-3084481.html?wt_mc=rss.ho.beitrag.atom" title="Frag den Bundestag: Parlamentsgutachten sollen öffentlich werden"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/2/7/0/Bildschirmfoto_2016-01-26_um_14-0c84fcc438275a89.jpeg" alt="Frag den Bundestag: Parlamentsgutachten sollen öffentlich werden" /> </a> <p>Über das neue Portal "Frag den Bundestag" können Nutzer per Knopfdruck Studien des Wissenschaftlichen Dienstes des Bundestags beantragen. So soll es möglich werden, alle herausgegebenen Untersuchungen in einer Online-Bibliothek zu veröffentlichen.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389434587/u/0/f/653902/c/35207/s/4d240284/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389434587/u/0/f/653902/c/35207/s/4d240284/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389434587/u/0/f/653902/c/35207/s/4d240284/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389434587/u/0/f/653902/c/35207/s/4d240284/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389434587/u/0/f/653902/c/35207/s/4d240284/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389434587/u/0/f/653902/c/35207/s/4d240284/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389434587/u/0/f/653902/c/35207/s/4d240284/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389434587/u/0/f/653902/c/35207/s/4d240284/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389434587/u/0/f/653902/c/35207/s/4d240284/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d240284/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>USA: 300.000 Hobbypiloten registrieren ihre Drohnen</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/USA-300-000-Hobbypiloten-registrieren-ihre-Drohnen-3084403.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Seit dem 21. Dezember 2015 müssen Hobbypiloten in den USA ihre kleinen
+ unbemannten Flugobjekte registrieren. Nun hat die Luftfahrtbehörde erste Zahlen
+ veröffentlicht. Bei einer Registrierung bis zum 20. Januar konnten Piloten mit einer
+ Belohnung rechnen.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389390571/u/0/f/653902/c/35207/s/4d23ebcb/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389390571/u/0/f/653902/c/35207/s/4d23ebcb/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389390571/u/0/f/653902/c/35207/s/4d23ebcb/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389390571/u/0/f/653902/c/35207/s/4d23ebcb/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389390571/u/0/f/653902/c/35207/s/4d23ebcb/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389390571/u/0/f/653902/c/35207/s/4d23ebcb/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389390571/u/0/f/653902/c/35207/s/4d23ebcb/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389390571/u/0/f/653902/c/35207/s/4d23ebcb/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389390571/u/0/f/653902/c/35207/s/4d23ebcb/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d23ebcb/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 13:06:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084403</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/USA-300-000-Hobbypiloten-registrieren-ihre-Drohnen-3084403.html?wt_mc=rss.ho.beitrag.atom" title="USA: 300.000 Hobbypiloten registrieren ihre Drohnen"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/2/0/5/phantom-dda03c5466cdeaba-a1ea68cfecc952e7.jpeg" alt="Drohne" /> </a> <p>Seit dem 21. Dezember 2015 müssen Hobbypiloten in den USA ihre kleinen unbemannten Flugobjekte registrieren. Nun hat die Luftfahrtbehörde erste Zahlen veröffentlicht. Bei einer Registrierung bis zum 20. Januar konnten Piloten mit einer Belohnung rechnen. </p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389390571/u/0/f/653902/c/35207/s/4d23ebcb/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389390571/u/0/f/653902/c/35207/s/4d23ebcb/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389390571/u/0/f/653902/c/35207/s/4d23ebcb/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389390571/u/0/f/653902/c/35207/s/4d23ebcb/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389390571/u/0/f/653902/c/35207/s/4d23ebcb/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389390571/u/0/f/653902/c/35207/s/4d23ebcb/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389390571/u/0/f/653902/c/35207/s/4d23ebcb/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389390571/u/0/f/653902/c/35207/s/4d23ebcb/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389390571/u/0/f/653902/c/35207/s/4d23ebcb/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d23ebcb/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Autoindustrie und Datenschützer: KfZ-Daten unterliegen dem Datenschutz</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Autoindustrie-und-Datenschuetzer-KfZ-Daten-unterliegen-dem-Datenschutz-3084253.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Alle Daten, die in einem Fahrzeug anfallen, gelten als personenbezogen,
+ sobald sie mit der Fahrzeugidentifikationsnummer oder dem Kfz-Kennzeichen verknüpft
+ sind. Darauf und auf mehr haben sich Industrie und Datenschutzbeauftragte geeinigt.&lt;br
+ clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389411781/u/0/f/653902/c/35207/s/4d2348fb/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389411781/u/0/f/653902/c/35207/s/4d2348fb/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389411781/u/0/f/653902/c/35207/s/4d2348fb/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389411781/u/0/f/653902/c/35207/s/4d2348fb/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389411781/u/0/f/653902/c/35207/s/4d2348fb/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389411781/u/0/f/653902/c/35207/s/4d2348fb/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389411781/u/0/f/653902/c/35207/s/4d2348fb/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389411781/u/0/f/653902/c/35207/s/4d2348fb/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389411781/u/0/f/653902/c/35207/s/4d2348fb/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2348fb/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 11:37:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084253</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Autoindustrie-und-Datenschuetzer-KfZ-Daten-unterliegen-dem-Datenschutz-3084253.html?wt_mc=rss.ho.beitrag.atom" title="Autoindustrie und Datenschützer: KfZ-Daten unterliegen dem Datenschutz"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/0/9/6/image-1408514071782004-db0fcdb2cb0a7832-20569746602bb050.jpeg" alt="Auto" /> </a> <p>Alle Daten, die in einem Fahrzeug anfallen, gelten als personenbezogen, sobald sie mit der Fahrzeugidentifikationsnummer oder dem Kfz-Kennzeichen verknüpft sind. Darauf und auf mehr haben sich Industrie und Datenschutzbeauftragte geeinigt.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389411781/u/0/f/653902/c/35207/s/4d2348fb/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389411781/u/0/f/653902/c/35207/s/4d2348fb/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389411781/u/0/f/653902/c/35207/s/4d2348fb/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389411781/u/0/f/653902/c/35207/s/4d2348fb/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389411781/u/0/f/653902/c/35207/s/4d2348fb/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389411781/u/0/f/653902/c/35207/s/4d2348fb/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389411781/u/0/f/653902/c/35207/s/4d2348fb/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389411781/u/0/f/653902/c/35207/s/4d2348fb/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389411781/u/0/f/653902/c/35207/s/4d2348fb/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2348fb/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Wirtschaftsministerium richtet Leseraum für TTIP-Dokumente ein</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Wirtschaftsministerium-richtet-Leseraum-fuer-TTIP-Dokumente-ein-3084344.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Die Bundesregierung kommt einer Forderung des Bundestags nach und gewährt
+ Abgeordneten und Ländervertretern Zugang zu vertraulichen Verhandlungspapieren rund
+ um das umstrittene Handelsabkommen TTIP.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389411780/u/0/f/653902/c/35207/s/4d2348fa/sc/3/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389411780/u/0/f/653902/c/35207/s/4d2348fa/sc/3/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389411780/u/0/f/653902/c/35207/s/4d2348fa/sc/3/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389411780/u/0/f/653902/c/35207/s/4d2348fa/sc/3/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389411780/u/0/f/653902/c/35207/s/4d2348fa/sc/3/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389411780/u/0/f/653902/c/35207/s/4d2348fa/sc/3/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389411780/u/0/f/653902/c/35207/s/4d2348fa/sc/3/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389411780/u/0/f/653902/c/35207/s/4d2348fa/sc/3/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389411780/u/0/f/653902/c/35207/s/4d2348fa/sc/3/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2348fa/sc/3/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 11:27:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084344</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Wirtschaftsministerium-richtet-Leseraum-fuer-TTIP-Dokumente-ein-3084344.html?wt_mc=rss.ho.beitrag.atom" title="Wirtschaftsministerium richtet Leseraum für TTIP-Dokumente ein"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/1/5/8/Bildschirmfoto_2016-01-26_um_12-1315b4276da79760.jpeg" alt="Wirtschaftsministerium richtet Leseraum für TTIP-Dokumente ein" /> </a> <p>Die Bundesregierung kommt einer Forderung des Bundestags nach und gewährt Abgeordneten und Ländervertretern Zugang zu vertraulichen Verhandlungspapieren rund um das umstrittene Handelsabkommen TTIP.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389411780/u/0/f/653902/c/35207/s/4d2348fa/sc/3/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389411780/u/0/f/653902/c/35207/s/4d2348fa/sc/3/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389411780/u/0/f/653902/c/35207/s/4d2348fa/sc/3/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389411780/u/0/f/653902/c/35207/s/4d2348fa/sc/3/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389411780/u/0/f/653902/c/35207/s/4d2348fa/sc/3/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389411780/u/0/f/653902/c/35207/s/4d2348fa/sc/3/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389411780/u/0/f/653902/c/35207/s/4d2348fa/sc/3/a2.htm"><img src="http://da.feedsportal.com/r/247389411780/u/0/f/653902/c/35207/s/4d2348fa/sc/3/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389411780/u/0/f/653902/c/35207/s/4d2348fa/sc/3/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d2348fa/sc/3/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Sony verlegt seine Playstation-Zentrale von Japan in die USA</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Sony-verlegt-seine-Playstation-Zentrale-von-Japan-in-die-USA-3084235.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Unter dem Dach der Sony Interactive Entertainment (SIE) sollen die
+ Geschäfte mit Hard- und Software sowie Inhalten und Netzwerkservices zusammengeführt
+ werden – und zwar in Kalifornien.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389402157/u/0/f/653902/c/35207/s/4d22c030/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389402157/u/0/f/653902/c/35207/s/4d22c030/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389402157/u/0/f/653902/c/35207/s/4d22c030/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389402157/u/0/f/653902/c/35207/s/4d22c030/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389402157/u/0/f/653902/c/35207/s/4d22c030/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389402157/u/0/f/653902/c/35207/s/4d22c030/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389402157/u/0/f/653902/c/35207/s/4d22c030/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389402157/u/0/f/653902/c/35207/s/4d22c030/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389402157/u/0/f/653902/c/35207/s/4d22c030/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d22c030/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 10:13:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084235</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Sony-verlegt-seine-Playstation-Zentrale-von-Japan-in-die-USA-3084235.html?wt_mc=rss.ho.beitrag.atom" title="Sony verlegt seine Playstation-Zentrale von Japan in die USA"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/0/7/8/ps4-afdf7ad4d65f4b20-52131920b12f35b6-f3a1e1c612c84968.jpeg" alt="Playstation" /> </a> <p>Unter dem Dach der Sony Interactive Entertainment (SIE) sollen die Geschäfte mit Hard- und Software sowie Inhalten und Netzwerkservices zusammengeführt werden – und zwar in Kalifornien.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389402157/u/0/f/653902/c/35207/s/4d22c030/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389402157/u/0/f/653902/c/35207/s/4d22c030/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389402157/u/0/f/653902/c/35207/s/4d22c030/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389402157/u/0/f/653902/c/35207/s/4d22c030/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389402157/u/0/f/653902/c/35207/s/4d22c030/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389402157/u/0/f/653902/c/35207/s/4d22c030/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389402157/u/0/f/653902/c/35207/s/4d22c030/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389402157/u/0/f/653902/c/35207/s/4d22c030/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389402157/u/0/f/653902/c/35207/s/4d22c030/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d22c030/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>Sicherheitsupdate für OpenSSL steht an</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/Sicherheitsupdate-fuer-OpenSSL-steht-an-3084227.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Neue OpenSSL-Versionen sollen zwei Sicherheitslücken schließen. Den
+ Schweregrad einer Schwachstelle stuft das OpenSSL-Team mit hoch ein.&lt;br
+ clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389402156/u/0/f/653902/c/35207/s/4d22c02f/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389402156/u/0/f/653902/c/35207/s/4d22c02f/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389402156/u/0/f/653902/c/35207/s/4d22c02f/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389402156/u/0/f/653902/c/35207/s/4d22c02f/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389402156/u/0/f/653902/c/35207/s/4d22c02f/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389402156/u/0/f/653902/c/35207/s/4d22c02f/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389402156/u/0/f/653902/c/35207/s/4d22c02f/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389402156/u/0/f/653902/c/35207/s/4d22c02f/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389402156/u/0/f/653902/c/35207/s/4d22c02f/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d22c02f/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 10:10:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084227</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/Sicherheitsupdate-fuer-OpenSSL-steht-an-3084227.html?wt_mc=rss.ho.beitrag.atom" title="Sicherheitsupdate für OpenSSL steht an"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/0/7/0/key-id-ec35f72fab481a99-9b59ced0bbaed03d-c4be465cb3c66623.jpeg" alt="Schlüssel" /> </a> <p>Neue OpenSSL-Versionen sollen zwei Sicherheitslücken schließen. Den Schweregrad einer Schwachstelle stuft das OpenSSL-Team mit hoch ein. </p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389402156/u/0/f/653902/c/35207/s/4d22c02f/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389402156/u/0/f/653902/c/35207/s/4d22c02f/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389402156/u/0/f/653902/c/35207/s/4d22c02f/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389402156/u/0/f/653902/c/35207/s/4d22c02f/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389402156/u/0/f/653902/c/35207/s/4d22c02f/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389402156/u/0/f/653902/c/35207/s/4d22c02f/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389402156/u/0/f/653902/c/35207/s/4d22c02f/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389402156/u/0/f/653902/c/35207/s/4d22c02f/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389402156/u/0/f/653902/c/35207/s/4d22c02f/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d22c02f/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ <item>
+ <title>PDF-Reader Foxit Reader für Schadcode anfällig</title>
+ <link>
+ http://www.heise.de/newsticker/meldung/PDF-Reader-Foxit-Reader-fuer-Schadcode-anfaellig-3084161.html?wt_mc=rss.ho.beitrag.atom
+ </link>
+ <description>Neue Versionen sichern Foxit PhantomPDF und Foxit Reader ab. Beide
+ Anwendungen lassen sich aus der Ferne attackieren und Angreifer können eigenen Code
+ auf Computer schleusen.&lt;br clear='all'/&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389406045/u/0/f/653902/c/35207/s/4d227a5f/sc/21/rc/1/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389406045/u/0/f/653902/c/35207/s/4d227a5f/sc/21/rc/1/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389406045/u/0/f/653902/c/35207/s/4d227a5f/sc/21/rc/2/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389406045/u/0/f/653902/c/35207/s/4d227a5f/sc/21/rc/2/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://rc.feedsportal.com/r/247389406045/u/0/f/653902/c/35207/s/4d227a5f/sc/21/rc/3/rc.htm"
+ rel="nofollow"&gt;&lt;img
+ src="http://rc.feedsportal.com/r/247389406045/u/0/f/653902/c/35207/s/4d227a5f/sc/21/rc/3/rc.img"
+ border="0"/&gt;&lt;/a&gt;&lt;br/&gt;&lt;br/&gt;&lt;a
+ href="http://da.feedsportal.com/r/247389406045/u/0/f/653902/c/35207/s/4d227a5f/sc/21/a2.htm"&gt;&lt;img
+ src="http://da.feedsportal.com/r/247389406045/u/0/f/653902/c/35207/s/4d227a5f/sc/21/a2.img"
+ border="0"/&gt;&lt;/a&gt;&lt;img width="1" height="1"
+ src="http://pi.feedsportal.com/r/247389406045/u/0/f/653902/c/35207/s/4d227a5f/sc/21/a2t.img"
+ border="0"/&gt;&lt;img width='1' height='1'
+ src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d227a5f/sc/21/mf.gif'
+ border='0'/&gt;</description>
+ <pubDate>Tue, 26 Jan 2016 09:53:00 GMT</pubDate>
+ <guid isPermaLink="false">http://heise.de/-3084161</guid>
+ <content:encoded>
+ <![CDATA[<div xmlns="http://www.w3.org/1999/xhtml"> <a href="http://www.heise.de/newsticker/meldung/PDF-Reader-Foxit-Reader-fuer-Schadcode-anfaellig-3084161.html?wt_mc=rss.ho.beitrag.atom" title="PDF-Reader Foxit Reader für Schadcode anfällig"> <img src="http://www.heise.de/scale/geometry/264/q80/imgs/18/1/7/3/7/0/2/4/img3File_thumb800-4b0bd095dcbcce5d.png" alt="Foxit Reader für Schadcode anfällig" /> </a> <p>Neue Versionen sichern Foxit PhantomPDF und Foxit Reader ab. Beide Anwendungen lassen sich aus der Ferne attackieren und Angreifer können eigenen Code auf Computer schleusen.</p> </div><br clear='all'/><br/><br/><a href="http://rc.feedsportal.com/r/247389406045/u/0/f/653902/c/35207/s/4d227a5f/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389406045/u/0/f/653902/c/35207/s/4d227a5f/sc/21/rc/1/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389406045/u/0/f/653902/c/35207/s/4d227a5f/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389406045/u/0/f/653902/c/35207/s/4d227a5f/sc/21/rc/2/rc.img" border="0"/></a><br/><br/><a href="http://rc.feedsportal.com/r/247389406045/u/0/f/653902/c/35207/s/4d227a5f/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://rc.feedsportal.com/r/247389406045/u/0/f/653902/c/35207/s/4d227a5f/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/247389406045/u/0/f/653902/c/35207/s/4d227a5f/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/247389406045/u/0/f/653902/c/35207/s/4d227a5f/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/247389406045/u/0/f/653902/c/35207/s/4d227a5f/sc/21/a2t.img" border="0"/><img width='1' height='1' src='http://heise.de.feedsportal.com/c/35207/f/653902/s/4d227a5f/sc/21/mf.gif' border='0'/>]]></content:encoded>
+ </item>
+ </channel>
+</rss>
diff --git a/mobile/android/tests/background/junit4/resources/feed_rss_medium.xml b/mobile/android/tests/background/junit4/resources/feed_rss_medium.xml
new file mode 100644
index 0000000000..0f5a20ab6d
--- /dev/null
+++ b/mobile/android/tests/background/junit4/resources/feed_rss_medium.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Snapshot from https://medium.com/feed/@Antlam/
+-->
+<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
+ <channel>
+ <title><![CDATA[Anthony Lam on Medium]]></title>
+ <description><![CDATA[Latest posts by Anthony Lam on Medium]]></description>
+ <link>https://medium.com/@antlam?source=rss-59f49b9e4b19------2</link>
+ <image>
+ <url>https://d262ilb51hltx0.cloudfront.net/fit/c/150/150/1*BNfAhhQ8TybWsu_gMMixWw.jpeg</url>
+ <title>Anthony Lam on Medium</title>
+ <link>https://medium.com/@antlam?source=rss-59f49b9e4b19------2</link>
+ </image>
+ <generator>RSS for Node</generator>
+ <lastBuildDate>Tue, 26 Jan 2016 17:06:09 GMT</lastBuildDate>
+ <atom:link href="https://medium.com/feed/@antlam" rel="self" type="application/rss+xml"/>
+ <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
+ <atom:link href="http://medium.superfeedr.com" rel="hub"/>
+ <item>
+ <title><![CDATA[UX thoughts for 2016]]></title>
+ <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@antlam/ux-thoughts-for-2016-1fc1d6e515e8?source=rss-59f49b9e4b19------2"><img src="https://d262ilb51hltx0.cloudfront.net/fit/c/600/200/1*mOiPSWfvCBoLUrfQlIrdVQ.png" width="600" height="200"></a></p><p class="medium-feed-snippet">And just like that, another year.</p><p class="medium-feed-link"><a href="https://medium.com/@antlam/ux-thoughts-for-2016-1fc1d6e515e8?source=rss-59f49b9e4b19------2">Continue reading on Medium »</a></p></div>]]></description>
+ <link>https://medium.com/@antlam/ux-thoughts-for-2016-1fc1d6e515e8?source=rss-59f49b9e4b19------2</link>
+ <guid isPermaLink="false">https://medium.com/p/1fc1d6e515e8</guid>
+ <dc:creator><![CDATA[Anthony Lam]]></dc:creator>
+ <pubDate>Mon, 11 Jan 2016 18:43:58 GMT</pubDate>
+ </item>
+ <item>
+ <title><![CDATA[A New Mobile Tabs tray]]></title>
+ <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@antlam/a-new-mobile-tabs-tray-327ac262eacb?source=rss-59f49b9e4b19------2"><img src="https://d262ilb51hltx0.cloudfront.net/fit/c/600/200/1*scpLRAL_zy9CcW6BLGZHng.png" width="600" height="200"></a></p><p class="medium-feed-snippet">Why we&#8217;re giving it an update in Firefox for Android</p><p class="medium-feed-link"><a href="https://medium.com/@antlam/a-new-mobile-tabs-tray-327ac262eacb?source=rss-59f49b9e4b19------2">Continue reading on Medium »</a></p></div>]]></description>
+ <link>https://medium.com/@antlam/a-new-mobile-tabs-tray-327ac262eacb?source=rss-59f49b9e4b19------2</link>
+ <guid isPermaLink="false">https://medium.com/p/327ac262eacb</guid>
+ <dc:creator><![CDATA[Anthony Lam]]></dc:creator>
+ <pubDate>Fri, 06 Nov 2015 17:30:55 GMT</pubDate>
+ </item>
+ <item>
+ <title><![CDATA[Quick search]]></title>
+ <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@antlam/quick-search-bdd374257e75?source=rss-59f49b9e4b19------2"><img src="https://d262ilb51hltx0.cloudfront.net/fit/c/600/200/1*DyBNARNADZsexPnxkMaEgQ.png" width="600" height="200"></a></p><p class="medium-feed-snippet">Instantly search with any of your search providers</p><p class="medium-feed-link"><a href="https://medium.com/@antlam/quick-search-bdd374257e75?source=rss-59f49b9e4b19------2">Continue reading on Medium »</a></p></div>]]></description>
+ <link>https://medium.com/@antlam/quick-search-bdd374257e75?source=rss-59f49b9e4b19------2</link>
+ <guid isPermaLink="false">https://medium.com/p/bdd374257e75</guid>
+ <dc:creator><![CDATA[Anthony Lam]]></dc:creator>
+ <pubDate>Tue, 01 Sep 2015 17:36:32 GMT</pubDate>
+ </item>
+ <item>
+ <title><![CDATA[Designing helpfulness]]></title>
+ <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@antlam/designing-helpfulness-c1777727faf?source=rss-59f49b9e4b19------2"><img src="https://d262ilb51hltx0.cloudfront.net/fit/c/600/200/1*Cg8wkgjwCH7A1Aotec1Okw.png" width="600" height="200"></a></p><p class="medium-feed-snippet">Being there, without being annoying</p><p class="medium-feed-link"><a href="https://medium.com/@antlam/designing-helpfulness-c1777727faf?source=rss-59f49b9e4b19------2">Continue reading on Medium »</a></p></div>]]></description>
+ <link>https://medium.com/@antlam/designing-helpfulness-c1777727faf?source=rss-59f49b9e4b19------2</link>
+ <guid isPermaLink="false">https://medium.com/p/c1777727faf</guid>
+ <dc:creator><![CDATA[Anthony Lam]]></dc:creator>
+ <pubDate>Tue, 11 Aug 2015 20:59:13 GMT</pubDate>
+ </item>
+ <item>
+ <title><![CDATA[Share to… Firefox?]]></title>
+ <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@antlam/share-to-firefox-245984b2da33?source=rss-59f49b9e4b19------2"><img src="https://d262ilb51hltx0.cloudfront.net/fit/c/600/200/1*H5wztlFTvbE1EM_5MRkuzg.png" width="600" height="200"></a></p><p class="medium-feed-snippet">You may remember this from such share intents as &#8220;Add to Firefox&#8221;</p><p class="medium-feed-link"><a href="https://medium.com/@antlam/share-to-firefox-245984b2da33?source=rss-59f49b9e4b19------2">Continue reading on Medium »</a></p></div>]]></description>
+ <link>https://medium.com/@antlam/share-to-firefox-245984b2da33?source=rss-59f49b9e4b19------2</link>
+ <guid isPermaLink="false">https://medium.com/p/245984b2da33</guid>
+ <dc:creator><![CDATA[Anthony Lam]]></dc:creator>
+ <pubDate>Fri, 08 May 2015 20:18:18 GMT</pubDate>
+ </item>
+ <item>
+ <title><![CDATA[Open multiple links]]></title>
+ <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@antlam/open-multiple-links-1ce475c47de3?source=rss-59f49b9e4b19------2"><img src="https://d262ilb51hltx0.cloudfront.net/fit/c/600/200/1*yEQ5DjomHZIgVzUN-FKx2A.jpeg" width="600" height="200"></a></p><p class="medium-feed-snippet">Queue links in Firefox instead of switching applications each time.</p><p class="medium-feed-link"><a href="https://medium.com/@antlam/open-multiple-links-1ce475c47de3?source=rss-59f49b9e4b19------2">Continue reading on Medium »</a></p></div>]]></description>
+ <link>https://medium.com/@antlam/open-multiple-links-1ce475c47de3?source=rss-59f49b9e4b19------2</link>
+ <guid isPermaLink="false">https://medium.com/p/1ce475c47de3</guid>
+ <dc:creator><![CDATA[Anthony Lam]]></dc:creator>
+ <pubDate>Tue, 14 Apr 2015 18:44:59 GMT</pubDate>
+ </item>
+ <item>
+ <title><![CDATA[Firefox for Android on Tablets]]></title>
+ <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@antlam/firefox-for-android-on-tablets-f67edc83dd46?source=rss-59f49b9e4b19------2"><img src="https://d262ilb51hltx0.cloudfront.net/fit/c/600/200/1*H5p3YQ1NGpfGPDryd22Ujg.png" width="600" height="200"></a></p><p class="medium-feed-snippet">Redesigning the browser interface&#8202;&#8212;&#8202;Part two</p><p class="medium-feed-link"><a href="https://medium.com/@antlam/firefox-for-android-on-tablets-f67edc83dd46?source=rss-59f49b9e4b19------2">Continue reading on Medium »</a></p></div>]]></description>
+ <link>https://medium.com/@antlam/firefox-for-android-on-tablets-f67edc83dd46?source=rss-59f49b9e4b19------2</link>
+ <guid isPermaLink="false">https://medium.com/p/f67edc83dd46</guid>
+ <dc:creator><![CDATA[Anthony Lam]]></dc:creator>
+ <pubDate>Tue, 03 Feb 2015 20:29:18 GMT</pubDate>
+ </item>
+ <item>
+ <title><![CDATA[Are big phones back?]]></title>
+ <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@antlam/are-big-phones-back-59550ba0f24e?source=rss-59f49b9e4b19------2"><img src="https://d262ilb51hltx0.cloudfront.net/fit/c/600/200/1*XArwkY9ZcmtCkEhDwl0aKA.jpeg" width="600" height="200"></a></p><p class="medium-feed-snippet">Some thoughts and impressions</p><p class="medium-feed-link"><a href="https://medium.com/@antlam/are-big-phones-back-59550ba0f24e?source=rss-59f49b9e4b19------2">Continue reading on Medium »</a></p></div>]]></description>
+ <link>https://medium.com/@antlam/are-big-phones-back-59550ba0f24e?source=rss-59f49b9e4b19------2</link>
+ <guid isPermaLink="false">https://medium.com/p/59550ba0f24e</guid>
+ <dc:creator><![CDATA[Anthony Lam]]></dc:creator>
+ <pubDate>Tue, 23 Dec 2014 20:05:36 GMT</pubDate>
+ </item>
+ <item>
+ <title><![CDATA[Firefox for Android looks a bit different]]></title>
+ <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@antlam/firefox-for-android-looks-a-bit-different-8ae8eba41b1f?source=rss-59f49b9e4b19------2"><img src="https://d262ilb51hltx0.cloudfront.net/fit/c/600/200/1*Lk31G3Qt2fs5WcOtBQQVgw.png" width="600" height="200"></a></p><p class="medium-feed-snippet">Redesigning the browser interface</p><p class="medium-feed-link"><a href="https://medium.com/@antlam/firefox-for-android-looks-a-bit-different-8ae8eba41b1f?source=rss-59f49b9e4b19------2">Continue reading on Medium »</a></p></div>]]></description>
+ <link>https://medium.com/@antlam/firefox-for-android-looks-a-bit-different-8ae8eba41b1f?source=rss-59f49b9e4b19------2</link>
+ <guid isPermaLink="false">https://medium.com/p/8ae8eba41b1f</guid>
+ <dc:creator><![CDATA[Anthony Lam]]></dc:creator>
+ <pubDate>Fri, 29 Aug 2014 23:38:07 GMT</pubDate>
+ </item>
+ <item>
+ <title><![CDATA[My fancy new watch]]></title>
+ <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@antlam/my-fancy-new-watch-4856162890a3?source=rss-59f49b9e4b19------2"><img src="https://d262ilb51hltx0.cloudfront.net/fit/c/600/200/1*_gsBD-Vw7qevrwLxXZe8jA.jpeg" width="600" height="200"></a></p><p class="medium-feed-snippet">Early thoughts</p><p class="medium-feed-link"><a href="https://medium.com/@antlam/my-fancy-new-watch-4856162890a3?source=rss-59f49b9e4b19------2">Continue reading on Medium »</a></p></div>]]></description>
+ <link>https://medium.com/@antlam/my-fancy-new-watch-4856162890a3?source=rss-59f49b9e4b19------2</link>
+ <guid isPermaLink="false">https://medium.com/p/4856162890a3</guid>
+ <dc:creator><![CDATA[Anthony Lam]]></dc:creator>
+ <pubDate>Tue, 22 Jul 2014 01:36:52 GMT</pubDate>
+ </item>
+ </channel> \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/resources/feed_rss_spon.xml b/mobile/android/tests/background/junit4/resources/feed_rss_spon.xml
new file mode 100644
index 0000000000..e5a81d5141
--- /dev/null
+++ b/mobile/android/tests/background/junit4/resources/feed_rss_spon.xml
@@ -0,0 +1,314 @@
+<?xml version="1.0" encoding="ISO-8859-1" standalone="yes"?>
+<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
+ <channel>
+ <title>SPIEGEL ONLINE - Schlagzeilen</title>
+ <link>http://www.spiegel.de</link>
+ <description>Deutschlands führende Nachrichtenseite. Alles Wichtige aus Politik, Wirtschaft, Sport, Kultur, Wissenschaft, Technik und mehr.</description>
+ <language>de</language>
+ <pubDate>Wed, 27 Jan 2016 18:20:31 +0100</pubDate>
+ <lastBuildDate>Wed, 27 Jan 2016 18:20:31 +0100</lastBuildDate>
+ <image>
+ <title>SPIEGEL ONLINE</title>
+ <link>http://www.spiegel.de</link>
+ <url>http://www.spiegel.de/static/sys/logo_120x61.gif</url>
+ </image>
+ <item>
+ <title>Angebliche Vergewaltigung einer 13-Jährigen: Steinmeier kanzelt russischen Minister Lawrow ab</title>
+ <link>http://www.spiegel.de/politik/ausland/steinmeier-kanzelt-lawrow-ab-aerger-um-angebliche-vergewaltigung-a-1074292.html#ref=rss</link>
+ <description>Der Ton wird scharf zwischen Berlin und Moskau: Frank-Walter Steinmeier wirft dem russischen Außenminister Lawrow politische Propaganda vor - es geht um die angebliche Vergewaltigung einer 13-Jährigen in Berlin.</description>
+ <category>Politik</category>
+ <pubDate>Wed, 27 Jan 2016 18:16:16 +0100</pubDate>
+ <guid>http://www.spiegel.de/politik/ausland/steinmeier-kanzelt-lawrow-ab-aerger-um-angebliche-vergewaltigung-a-1074292.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949492-thumbsmall-wtgq.jpg" hspace="5" align="left" >Der Ton wird scharf zwischen Berlin und Moskau: Frank-Walter Steinmeier wirft dem russischen Außenminister Lawrow politische Propaganda vor - es geht um die angebliche Vergewaltigung einer 13-Jährigen in Berlin.]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949492-thumbsmall-wtgq.jpg"/>
+ </item>
+ <item>
+ <title>Grafischer Überblick: Hier gibt es die wenigsten Herzinfarkte in Deutschland</title>
+ <link>http://www.spiegel.de/gesundheit/diagnose/ostdeutsche-sterben-deutlich-haeufiger-an-einem-herzinfarkt-a-1074231.html#ref=rss</link>
+ <description>Nirgendwo arbeiten mehr Kardiologen als in Hamburg - auch deshalb gibt es dort weniger Herzinfarkte als in anderen Bundesländern. Wie sieht es in Ihrer Gegend aus, wo ist die Sterblichkeit am höchsten? Der Überblick.</description>
+ <category>Gesundheit</category>
+ <pubDate>Wed, 27 Jan 2016 18:08:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/gesundheit/diagnose/ostdeutsche-sterben-deutlich-haeufiger-an-einem-herzinfarkt-a-1074231.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-566602-thumbsmall-bxcg.jpg" hspace="5" align="left" >Nirgendwo arbeiten mehr Kardiologen als in Hamburg - auch deshalb gibt es dort weniger Herzinfarkte als in anderen Bundesländern. Wie sieht es in Ihrer Gegend aus, wo ist die Sterblichkeit am höchsten? Der Überblick.]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-566602-thumbsmall-bxcg.jpg"/>
+ </item>
+ <item>
+ <title>Nachhilfe für gute Schüler: Lasst Eure Kinder auch das Scheitern lernen</title>
+ <link>http://www.spiegel.de/schulspiegel/nachhilfe-entspannte-schueler-brauchen-entspannte-eltern-a-1074197.html#ref=rss</link>
+ <description>Selbst gute Schüler werden von ihren Eltern zur Nachhilfe geschickt. Das schadet mehr, als es nützt - denn die Kinder lernen dabei vor allem eines: Dass sie nicht scheitern dürfen.</description>
+ <category>SchulSPIEGEL</category>
+ <pubDate>Wed, 27 Jan 2016 18:02:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/schulspiegel/nachhilfe-entspannte-schueler-brauchen-entspannte-eltern-a-1074197.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-396678-thumbsmall-dhws.jpg" hspace="5" align="left" >Selbst gute Schüler werden von ihren Eltern zur Nachhilfe geschickt. Das schadet mehr, als es nützt - denn die Kinder lernen dabei vor allem eines: Dass sie nicht scheitern dürfen.]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-396678-thumbsmall-dhws.jpg"/>
+ </item>
+ <item>
+ <title>Germanwings-Katastrophe: Arbeitsgruppe regt Schleuse zwischen Kabine und Cockpit an</title>
+ <link>http://www.spiegel.de/panorama/germanwings-katastrophe-arbeitsgruppe-legt-abschlussbericht-vor-a-1074294.html#ref=rss</link>
+ <description>Wie sicher muss ein Flugzeugcockpit sein? Nach dem Germanwings-Absturz mit 150 Toten hat sich eine Arbeitsgruppe mit dieser Frage befasst - und nun ihren Abschlussbericht vorgelegt.</description>
+ <category>Panorama</category>
+ <pubDate>Wed, 27 Jan 2016 17:52:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/panorama/germanwings-katastrophe-arbeitsgruppe-legt-abschlussbericht-vor-a-1074294.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-828832-thumbsmall-gjvf.jpg" hspace="5" align="left" >Wie sicher muss ein Flugzeugcockpit sein? Nach dem Germanwings-Absturz mit 150 Toten hat sich eine Arbeitsgruppe mit dieser Frage befasst - und nun ihren Abschlussbericht vorgelegt.]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-828832-thumbsmall-gjvf.jpg"/>
+ </item>
+ <item>
+ <title>Polen: Händler wehren sich gegen Supermarktsteuer</title>
+ <link>http://www.spiegel.de/wirtschaft/soziales/polen-will-internationale-einzelhaendler-hoeher-besteuern-a-1074101.html#ref=rss</link>
+ <description>Polens Regierung plant, Einzelhändler höher zu besteuern - und will mit den Einnahmen soziale Wohltaten finanzieren. Besonders trifft es ausländische Unternehmen wie Kaufland und Metro.</description>
+ <category>Wirtschaft</category>
+ <pubDate>Wed, 27 Jan 2016 17:46:10 +0100</pubDate>
+ <guid>http://www.spiegel.de/wirtschaft/soziales/polen-will-internationale-einzelhaendler-hoeher-besteuern-a-1074101.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949308-thumbsmall-wwsr.jpg" hspace="5" align="left" >Polens Regierung plant, Einzelhändler höher zu besteuern - und will mit den Einnahmen soziale Wohltaten finanzieren. Besonders trifft es ausländische Unternehmen wie Kaufland und Metro.]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949308-thumbsmall-wwsr.jpg"/>
+ </item>
+ <item>
+ <title>Übergriffe in Köln: Polizei erteilt Silvester-Verdächtigen Karnevalverbot</title>
+ <link>http://www.spiegel.de/panorama/justiz/koeln-polizei-erteilt-silvester-verdaechtigen-verbote-fuer-karneval-a-1074274.html#ref=rss</link>
+ <description>Kölns Polizei bereitet sich auf die Karnevalstage vor: Tatverdächtige aus der Silvesternacht sollen mit Zutrittsverboten von bestimmten Orten ferngehalten werden. Der Polizeipräsident forderte die Narren zudem auf, keine Spielzeugwaffen zu tragen.</description>
+ <category>Panorama</category>
+ <pubDate>Wed, 27 Jan 2016 17:23:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/panorama/justiz/koeln-polizei-erteilt-silvester-verdaechtigen-verbote-fuer-karneval-a-1074274.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-942482-thumbsmall-qyge.jpg" hspace="5" align="left" >Kölns Polizei bereitet sich auf die Karnevalstage vor: Tatverdächtige aus der Silvesternacht sollen mit Zutrittsverboten von bestimmten Orten ferngehalten werden. Der Polizeipräsident forderte die Narren zudem auf, keine Spielzeugwaffen zu tragen.]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-942482-thumbsmall-qyge.jpg"/>
+ </item>
+ <item>
+ <title>"The Hateful 8"-Stars im Interview: "Wahnsinn, was da alles passiert!"</title>
+ <link>http://www.spiegel.de/kultur/kino/the-hateful-8-quentin-tarantino-und-jennifer-jason-leigh-im-interview-a-1074202.html#ref=rss</link>
+ <description>Sieben Männer und eine Frau - in seinem Western "The Hateful 8" entwirft Quentin Tarantino ein brutales Abbild der US-Gesellschaft. Hier erklären der Regisseur und seine Hauptdarstellerin, warum es sich lohnt, den Film gleich mehrmals anzusehen.</description>
+ <category>Kultur</category>
+ <pubDate>Wed, 27 Jan 2016 17:08:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/kultur/kino/the-hateful-8-quentin-tarantino-und-jennifer-jason-leigh-im-interview-a-1074202.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949256-thumbsmall-hbrj.jpg" hspace="5" align="left" >Sieben Männer und eine Frau - in seinem Western "The Hateful 8" entwirft Quentin Tarantino ein brutales Abbild der US-Gesellschaft. Hier erklären der Regisseur und seine Hauptdarstellerin, warum es sich lohnt, den Film gleich mehrmals anzusehen.]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949256-thumbsmall-hbrj.jpg"/>
+ </item>
+ <item>
+ <title>Drama auf zugefrorenem Teich: Baby in akuter Lebensgefahr - Messer gefunden</title>
+ <link>http://www.spiegel.de/panorama/justiz/hamburg-vater-mit-baby-in-zugefrorenem-teich-eingebrochen-messer-gefunden-a-1074258.html#ref=rss</link>
+ <description>Der Fall gibt Rätsel auf: In Hamburg wird ein Vater mit seinem Baby aus einem zugefrorenen Teich gerettet - zwei Männer hätten ihn überfallen, sagt der 24-Jährige. Nun haben Ermittler in der Nähe des Gewässers ein Messer gefunden.</description>
+ <category>Panorama</category>
+ <pubDate>Wed, 27 Jan 2016 16:32:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/panorama/justiz/hamburg-vater-mit-baby-in-zugefrorenem-teich-eingebrochen-messer-gefunden-a-1074258.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949367-thumbsmall-vpsq.jpg" hspace="5" align="left" >Der Fall gibt Rätsel auf: In Hamburg wird ein Vater mit seinem Baby aus einem zugefrorenen Teich gerettet - zwei Männer hätten ihn überfallen, sagt der 24-Jährige. Nun haben Ermittler in der Nähe des Gewässers ein Messer gefunden.]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949367-thumbsmall-vpsq.jpg"/>
+ </item>
+ <item>
+ <title>Rheinland-Pfalz: Elefantenrunde soll mit sechs Parteien stattfinden</title>
+ <link>http://www.spiegel.de/politik/deutschland/swr-fernsehdebatte-jetzt-mit-sechs-parteien-a-1074262.html#ref=rss</link>
+ <description>Erst drei, dann eine, jetzt sechs Parteien: Die TV-Diskussion im Südwestrundfunk zur rheinland-pfälzischen Landtagswahl findet nun in ganz großer Runde statt.</description>
+ <category>Politik</category>
+ <pubDate>Wed, 27 Jan 2016 16:29:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/politik/deutschland/swr-fernsehdebatte-jetzt-mit-sechs-parteien-a-1074262.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-946925-thumbsmall-icoq.jpg" hspace="5" align="left" >Erst drei, dann eine, jetzt sechs Parteien: Die TV-Diskussion im Südwestrundfunk zur rheinland-pfälzischen Landtagswahl findet nun in ganz großer Runde statt.]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-946925-thumbsmall-icoq.jpg"/>
+ </item>
+ <item>
+ <title>Blauer Brief: Britische Schulleiterin mahnt Pyjama-Eltern ab</title>
+ <link>http://www.spiegel.de/schulspiegel/direktorin-appelliert-an-eltern-bringt-eure-kinder-nicht-im-schlafanzug-zur-schule-a-1074167.html#ref=rss</link>
+ <description>Rektorin Kate Chisholm hat genug: Ständig beobachtet sie Eltern, die noch im Schlafanzug stecken, wenn sie ihre Kinder an der Schule absetzen. Nun schrieb sie einen Brandbrief, die Elternschaft reagiert gespalten.</description>
+ <category>SchulSPIEGEL</category>
+ <pubDate>Wed, 27 Jan 2016 16:26:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/schulspiegel/direktorin-appelliert-an-eltern-bringt-eure-kinder-nicht-im-schlafanzug-zur-schule-a-1074167.html</guid>
+ <content:encoded><![CDATA[Rektorin Kate Chisholm hat genug: Ständig beobachtet sie Eltern, die noch im Schlafanzug stecken, wenn sie ihre Kinder an der Schule absetzen. Nun schrieb sie einen Brandbrief, die Elternschaft reagiert gespalten.]]></content:encoded>
+ </item>
+ <item>
+ <title>Zwischenruf bei Merkel-Besuch: Hochschule verwirft juristische Schritte gegen Stör-Professor</title>
+ <link>http://www.spiegel.de/unispiegel/studium/zwischenruf-bei-merkel-besuch-hochschule-verwirft-juristische-schritte-gegen-professor-a-1074208.html#ref=rss</link>
+ <description>Seine politische Stör-Aktion während einer Rede von Kanzlerin Merkel ist für einen Merseburger Professor glimpflich ausgegangen. Die Hochschule sieht von juristischen Schritten ab. </description>
+ <category>UniSPIEGEL</category>
+ <pubDate>Wed, 27 Jan 2016 16:08:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/unispiegel/studium/zwischenruf-bei-merkel-besuch-hochschule-verwirft-juristische-schritte-gegen-professor-a-1074208.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949364-thumbsmall-nnis.jpg" hspace="5" align="left" >Seine politische Stör-Aktion während einer Rede von Kanzlerin Merkel ist für einen Merseburger Professor glimpflich ausgegangen. Die Hochschule sieht von juristischen Schritten ab. ]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949364-thumbsmall-nnis.jpg"/>
+ </item>
+ <item>
+ <title>Bundesautobahngesellschaft: Verkehrsminister wehren sich gegen "Mammutbehörde"</title>
+ <link>http://www.spiegel.de/auto/aktuell/bundesautobahngesellschaft-widerstand-der-verkehrsminister-a-1074198.html#ref=rss</link>
+ <description>Der Bund könnte die Gründung einer Bundesautobahngesellschaft vorantreiben. Mehrere Verkehrsminister der Länder stellen sich dagegen - auch aus Furcht vor privaten Investoren beim Fernstraßenbau. </description>
+ <category>Auto</category>
+ <pubDate>Wed, 27 Jan 2016 15:54:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/auto/aktuell/bundesautobahngesellschaft-widerstand-der-verkehrsminister-a-1074198.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949241-thumbsmall-cksv.jpg" hspace="5" align="left" >Der Bund könnte die Gründung einer Bundesautobahngesellschaft vorantreiben. Mehrere Verkehrsminister der Länder stellen sich dagegen - auch aus Furcht vor privaten Investoren beim Fernstraßenbau. ]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949241-thumbsmall-cksv.jpg"/>
+ </item>
+ <item>
+ <title>Glasfaser-Ausbau: Dobrindt will schnelles Netz unter die Autobahnen legen</title>
+ <link>http://www.spiegel.de/netzwelt/netzpolitik/alexander-dobrindt-will-schnelles-netz-unter-die-autobahnen-legen-a-1074151.html#ref=rss</link>
+ <description>Jede Baustelle soll Bandbreite bringen: Die Bundesregierung beschließt, dass künftig beim Straßenbau automatisch Glasfaserkabel verlegt werden müssen. Kommt Deutschland so schneller ins Netz?</description>
+ <category>Netzwelt</category>
+ <pubDate>Wed, 27 Jan 2016 15:52:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/netzwelt/netzpolitik/alexander-dobrindt-will-schnelles-netz-unter-die-autobahnen-legen-a-1074151.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949166-thumbsmall-xrfd.jpg" hspace="5" align="left" >Jede Baustelle soll Bandbreite bringen: Die Bundesregierung beschließt, dass künftig beim Straßenbau automatisch Glasfaserkabel verlegt werden müssen. Kommt Deutschland so schneller ins Netz?]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949166-thumbsmall-xrfd.jpg"/>
+ </item>
+ <item>
+ <title>Handball-Insolvenz: Flensburg will den HSV verklagen</title>
+ <link>http://www.spiegel.de/sport/sonst/handball-bundesliga-sg-flensburg-handewitt-klagt-gegen-hsv-a-1074260.html#ref=rss</link>
+ <description>Einem nackten Mann kann man nicht in die Tasche greifen? Die SG Flensburg-Handewitt will den insolventen HSV Handball verklagen. Der Traditionsverein will Schadenersatz aus Hamburg haben.</description>
+ <category>Sport</category>
+ <pubDate>Wed, 27 Jan 2016 15:49:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/sport/sonst/handball-bundesliga-sg-flensburg-handewitt-klagt-gegen-hsv-a-1074260.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-948459-thumbsmall-banl.jpg" hspace="5" align="left" >Einem nackten Mann kann man nicht in die Tasche greifen? Die SG Flensburg-Handewitt will den insolventen HSV Handball verklagen. Der Traditionsverein will Schadenersatz aus Hamburg haben.]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-948459-thumbsmall-banl.jpg"/>
+ </item>
+ <item>
+ <title>Lage am Lageso: Berliner Senatsverwaltung bestreitet Tod eines Flüchtlings</title>
+ <link>http://www.spiegel.de/politik/deutschland/berlin-fluechtling-vom-lageso-tot-senat-widerspricht-a-1074255.html#ref=rss</link>
+ <description>Ist in Berlin ein syrischer Flüchtling gestorben, nachdem er lange vor dem Lageso warten musste? Der Helfer, der darüber berichtete, ist abgetaucht - die Senatsverwaltung widerspricht der Darstellung über den Todesfall.</description>
+ <category>Politik</category>
+ <pubDate>Wed, 27 Jan 2016 15:45:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/politik/deutschland/berlin-fluechtling-vom-lageso-tot-senat-widerspricht-a-1074255.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949380-thumbsmall-umdf.jpg" hspace="5" align="left" >Ist in Berlin ein syrischer Flüchtling gestorben, nachdem er lange vor dem Lageso warten musste? Der Helfer, der darüber berichtete, ist abgetaucht - die Senatsverwaltung widerspricht der Darstellung über den Todesfall.]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949380-thumbsmall-umdf.jpg"/>
+ </item>
+ <item>
+ <title>Tierarzt-Ikone: Evan Antin, der "heißeste Veterinär der Welt" </title>
+ <link>http://www.spiegel.de/panorama/leute/tierarzt-evan-antin-ist-der-sexiest-veterinaer-ever-laut-people-a-1074229.html#ref=rss</link>
+ <description>Evan Antin ist Tierarzt, Model und Personal Trainer. So irritierend gutaussehend, dass er zum "Sexiest Tierbetörer" avancierte. Trotz seiner Vorliebe für blutiges Gedärm und anatomische Anomalien.</description>
+ <category>Panorama</category>
+ <pubDate>Wed, 27 Jan 2016 15:31:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/panorama/leute/tierarzt-evan-antin-ist-der-sexiest-veterinaer-ever-laut-people-a-1074229.html</guid>
+ <content:encoded><![CDATA[Evan Antin ist Tierarzt, Model und Personal Trainer. So irritierend gutaussehend, dass er zum "Sexiest Tierbetörer" avancierte. Trotz seiner Vorliebe für blutiges Gedärm und anatomische Anomalien.]]></content:encoded>
+ </item>
+ <item>
+ <title>Verteidigungshaushalt: Schäuble will mehr Geld für Bundeswehr ausgeben</title>
+ <link>http://www.spiegel.de/politik/deutschland/wolfgang-schaeuble-einverstanden-mit-mehr-geld-fuer-ruestung-a-1074242.html#ref=rss</link>
+ <description>Drei Milliarden Euro mehr pro Jahr für Waffen: Finanzminister Schäuble sieht die Aufrüstungspläne der Verteidigungsministerin von der Leyen positiv.</description>
+ <category>Politik</category>
+ <pubDate>Wed, 27 Jan 2016 15:29:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/politik/deutschland/wolfgang-schaeuble-einverstanden-mit-mehr-geld-fuer-ruestung-a-1074242.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-947025-thumbsmall-qecb.jpg" hspace="5" align="left" >Drei Milliarden Euro mehr pro Jahr für Waffen: Finanzminister Schäuble sieht die Aufrüstungspläne der Verteidigungsministerin von der Leyen positiv.]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-947025-thumbsmall-qecb.jpg"/>
+ </item>
+ <item>
+ <title>Doping-Vorwürfe: NFL leitet Untersuchung gegen Manning ein</title>
+ <link>http://www.spiegel.de/sport/sonst/nfl-peyton-manning-unter-doping-verdacht-a-1074230.html#ref=rss</link>
+ <description>Die NFL prüft Vorwürfe gegen einen Superstar: Die Football-Liga geht jetzt offiziell den Doping-Gerüchten um Peyton Manning nach. Der Quarterback soll über seine Frau Hormone geordert haben.</description>
+ <category>Sport</category>
+ <pubDate>Wed, 27 Jan 2016 15:23:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/sport/sonst/nfl-peyton-manning-unter-doping-verdacht-a-1074230.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949287-thumbsmall-cwxj.jpg" hspace="5" align="left" >Die NFL prüft Vorwürfe gegen einen Superstar: Die Football-Liga geht jetzt offiziell den Doping-Gerüchten um Peyton Manning nach. Der Quarterback soll über seine Frau Hormone geordert haben.]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949287-thumbsmall-cwxj.jpg"/>
+ </item>
+ <item>
+ <title>Posse um SWR-Elefantenrunde: Feigheit vor dem Feind</title>
+ <link>http://www.spiegel.de/politik/deutschland/spd-und-swr-posse-um-tv-auftritt-in-rheinland-pfalz-kommentar-a-1074235.html#ref=rss</link>
+ <description>Die Sozialdemokraten weigern sich, an einer TV-Debatte mit der AfD teilzunehmen, jetzt geht das Spektakel in eine neue Runde. Statt der Ministerpräsidentin soll der SPD-Landeschef ran. Wie absurd ist das denn? </description>
+ <category>Politik</category>
+ <pubDate>Wed, 27 Jan 2016 15:23:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/politik/deutschland/spd-und-swr-posse-um-tv-auftritt-in-rheinland-pfalz-kommentar-a-1074235.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949309-thumbsmall-zhac.jpg" hspace="5" align="left" >Die Sozialdemokraten weigern sich, an einer TV-Debatte mit der AfD teilzunehmen, jetzt geht das Spektakel in eine neue Runde. Statt der Ministerpräsidentin soll der SPD-Landeschef ran. Wie absurd ist das denn? ]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949309-thumbsmall-zhac.jpg"/>
+ </item>
+ <item>
+ <title>Apple-Browser: Und plötzlich stürzt Safari ab</title>
+ <link>http://www.spiegel.de/netzwelt/apps/apple-safari-browser-stuerzt-ploetzlich-ab-a-1074217.html#ref=rss</link>
+ <description>Seit einigen Stunden haben Apple-Nutzer Probleme mit dem Safari-Browser. Wodurch die Abstürze ausgelöst werden, ist unklar. Doch mit einem kleinen Trick kann man sich helfen.</description>
+ <category>Netzwelt</category>
+ <pubDate>Wed, 27 Jan 2016 15:18:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/netzwelt/apps/apple-safari-browser-stuerzt-ploetzlich-ab-a-1074217.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949312-thumbsmall-jqlt.jpg" hspace="5" align="left" >Seit einigen Stunden haben Apple-Nutzer Probleme mit dem Safari-Browser. Wodurch die Abstürze ausgelöst werden, ist unklar. Doch mit einem kleinen Trick kann man sich helfen.]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949312-thumbsmall-jqlt.jpg"/>
+ </item>
+ <item>
+ <title>EU-Bericht zu Grenzsicherung: Griechenland droht Schengen-Rauswurf</title>
+ <link>http://www.spiegel.de/politik/ausland/griechenland-eu-kommission-droht-mit-schengen-rauswurf-a-1074201.html#ref=rss</link>
+ <description>Griechenland gerät in der Flüchtlingskrise unter massiven Druck der EU: Die Kommission will Athen Forderungen zum Grenzschutz schicken. Werden die nicht binnen drei Monaten erfüllt, droht der Ausschluss aus dem Schengen-Raum.</description>
+ <category>Politik</category>
+ <pubDate>Wed, 27 Jan 2016 15:04:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/politik/ausland/griechenland-eu-kommission-droht-mit-schengen-rauswurf-a-1074201.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949347-thumbsmall-vbmi.jpg" hspace="5" align="left" >Griechenland gerät in der Flüchtlingskrise unter massiven Druck der EU: Die Kommission will Athen Forderungen zum Grenzschutz schicken. Werden die nicht binnen drei Monaten erfüllt, droht der Ausschluss aus dem Schengen-Raum.]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949347-thumbsmall-vbmi.jpg"/>
+ </item>
+ <item>
+ <title>Protest gegen dänisches Asylrecht: Ai Weiwei schließt Ausstellung in Kopenhagen</title>
+ <link>http://www.spiegel.de/kultur/gesellschaft/protest-gegen-daenisches-asylrecht-ai-weiwei-schliesst-ausstellung-a-1074241.html#ref=rss</link>
+ <description>Ai Weiwei, Chinas wohl bekanntester Künstler, beendet seine Ausstellung in Kopenhagen vorzeitig. Grund ist eine Verschärfung der Asylregeln in Dänemark.</description>
+ <category>Kultur</category>
+ <pubDate>Wed, 27 Jan 2016 15:01:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/kultur/gesellschaft/protest-gegen-daenisches-asylrecht-ai-weiwei-schliesst-ausstellung-a-1074241.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949292-thumbsmall-uhox.jpg" hspace="5" align="left" >Ai Weiwei, Chinas wohl bekanntester Künstler, beendet seine Ausstellung in Kopenhagen vorzeitig. Grund ist eine Verschärfung der Asylregeln in Dänemark.]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949292-thumbsmall-uhox.jpg"/>
+ </item>
+ <item>
+ <title>Deutsche Handballer gegen Dänemark: Jetzt muss es schmutzig werden</title>
+ <link>http://www.spiegel.de/sport/sonst/handball-em-2016-jetzt-muss-es-schmutzig-werden-a-1074123.html#ref=rss</link>
+ <description>Noch ein Sieg bis zum Halbfinale: Die deutsche Handball-Nationalmannschaft hat bei der EM einen Lauf, sie ist gierig auf den Titel. Jetzt geht es gegen einen großen Favoriten - und Dänemark wirkt angeschlagen. </description>
+ <category>Sport</category>
+ <pubDate>Wed, 27 Jan 2016 14:59:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/sport/sonst/handball-em-2016-jetzt-muss-es-schmutzig-werden-a-1074123.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949116-thumbsmall-fpcj.jpg" hspace="5" align="left" >Noch ein Sieg bis zum Halbfinale: Die deutsche Handball-Nationalmannschaft hat bei der EM einen Lauf, sie ist gierig auf den Titel. Jetzt geht es gegen einen großen Favoriten - und Dänemark wirkt angeschlagen. ]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949116-thumbsmall-fpcj.jpg"/>
+ </item>
+ <item>
+ <title>Sicherheitslage nach Anschlag: Russland spricht Reisewarnung für Türkei aus</title>
+ <link>http://www.spiegel.de/reise/aktuell/russland-spricht-reisewarnung-fuer-tuerkei-aus-a-1074225.html#ref=rss</link>
+ <description>Die russische Regierung warnt ihre Bürger vor Reisen in die Türkei. Damit schränkt sie den Tourismus in das Land weiter ein. Nach dem Anschlag in Istanbul sind auch die Buchungen von deutschen Urlaubern eingebrochen.</description>
+ <category>Reise</category>
+ <pubDate>Wed, 27 Jan 2016 14:58:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/reise/aktuell/russland-spricht-reisewarnung-fuer-tuerkei-aus-a-1074225.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-378624-thumbsmall-xtnj.jpg" hspace="5" align="left" >Die russische Regierung warnt ihre Bürger vor Reisen in die Türkei. Damit schränkt sie den Tourismus in das Land weiter ein. Nach dem Anschlag in Istanbul sind auch die Buchungen von deutschen Urlaubern eingebrochen.]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-378624-thumbsmall-xtnj.jpg"/>
+ </item>
+ <item>
+ <title>Bayern-Deal mit Katar: Scheich Di!</title>
+ <link>http://www.spiegel.de/sport/fussball/fc-bayern-muenchen-und-der-katar-deal-in-der-pflicht-kommentar-a-1074187.html#ref=rss</link>
+ <description>Der FC Bayern schließt einen Sponsoren-Deal mit Katar ab, die Kritik darüber ist ebenso berechtigt wie vorhersehbar. Dabei darf es nicht bleiben. Nehmen wir den Rekordmeister doch beim Wort.</description>
+ <category>Sport</category>
+ <pubDate>Wed, 27 Jan 2016 14:52:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/sport/fussball/fc-bayern-muenchen-und-der-katar-deal-in-der-pflicht-kommentar-a-1074187.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949236-thumbsmall-hyyw.jpg" hspace="5" align="left" >Der FC Bayern schließt einen Sponsoren-Deal mit Katar ab, die Kritik darüber ist ebenso berechtigt wie vorhersehbar. Dabei darf es nicht bleiben. Nehmen wir den Rekordmeister doch beim Wort.]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949236-thumbsmall-hyyw.jpg"/>
+ </item>
+ <item>
+ <title>Flüchtlingskrise: EU wirft Griechenland schwere Mängel bei Grenzkontrolle vor</title>
+ <link>http://www.spiegel.de/politik/ausland/fluechtlinge-eu-wirft-griechenland-maengel-bei-grenzkontrolle-vor-a-1074219.html#ref=rss</link>
+ <description>Die griechische Regierung verletzt ihre Pflichten bei der Grenzsicherung - zu diesem Schluss kommt ein Untersuchungsbericht der EU-Kommission. Brüssel spricht nun eine klare Drohung gegen Athen aus.</description>
+ <category>Politik</category>
+ <pubDate>Wed, 27 Jan 2016 14:47:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/politik/ausland/fluechtlinge-eu-wirft-griechenland-maengel-bei-grenzkontrolle-vor-a-1074219.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949273-thumbsmall-ifxg.jpg" hspace="5" align="left" >Die griechische Regierung verletzt ihre Pflichten bei der Grenzsicherung - zu diesem Schluss kommt ein Untersuchungsbericht der EU-Kommission. Brüssel spricht nun eine klare Drohung gegen Athen aus.]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949273-thumbsmall-ifxg.jpg"/>
+ </item>
+ <item>
+ <title>Abgasaffäre: Brüssel will Autokonzerne bestrafen können</title>
+ <link>http://www.spiegel.de/wirtschaft/soziales/abgasaffaere-bruessel-will-autokonzerne-bestrafen-koennen-a-1074228.html#ref=rss</link>
+ <description>Im Abgas-Skandal ist der VW-Konzern Schuldiger - aber auch die Aufseher in Brüssel sind blamiert. Denn US-Behörden enthüllten, was ihnen jahrelang durch die Lappen ging. Die EU-Kommission will jetzt dafür sorgen, dass sich das nicht wiederholt.</description>
+ <category>Wirtschaft</category>
+ <pubDate>Wed, 27 Jan 2016 14:46:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/wirtschaft/soziales/abgasaffaere-bruessel-will-autokonzerne-bestrafen-koennen-a-1074228.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-938136-thumbsmall-hxbc.jpg" hspace="5" align="left" >Im Abgas-Skandal ist der VW-Konzern Schuldiger - aber auch die Aufseher in Brüssel sind blamiert. Denn US-Behörden enthüllten, was ihnen jahrelang durch die Lappen ging. Die EU-Kommission will jetzt dafür sorgen, dass sich das nicht wiederholt.]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-938136-thumbsmall-hxbc.jpg"/>
+ </item>
+ <item>
+ <title>Bundesverfassungsgericht: Wer alles gegen die Vorratsdatenspeicherung klagt</title>
+ <link>http://www.spiegel.de/netzwelt/netzpolitik/vorratsdatenspeicherung-wer-klagt-vor-dem-bundesverfassungsgericht-a-1074152.html#ref=rss</link>
+ <description>Widerstand gegen Vorratsdatenspeicherung: Die FDP hat Klage in Karlsruhe eingereicht, und auch weitere Gegner versuchen, das Gesetz vor Gericht noch zu kippen. Der Überblick. </description>
+ <category>Netzwelt</category>
+ <pubDate>Wed, 27 Jan 2016 14:42:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/netzwelt/netzpolitik/vorratsdatenspeicherung-wer-klagt-vor-dem-bundesverfassungsgericht-a-1074152.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-925932-thumbsmall-zbbn.jpg" hspace="5" align="left" >Widerstand gegen Vorratsdatenspeicherung: Die FDP hat Klage in Karlsruhe eingereicht, und auch weitere Gegner versuchen, das Gesetz vor Gericht noch zu kippen. Der Überblick. ]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-925932-thumbsmall-zbbn.jpg"/>
+ </item>
+ <item>
+ <title>Berliner Flüchtlingsamt: Das Scheitern des Lageso - das Protokoll</title>
+ <link>http://www.spiegel.de/politik/deutschland/berlin-das-scheitern-des-lageso-eine-chronik-a-1074186.html#ref=rss</link>
+ <description>Tagelanges Warten, keine Auszahlung von Essensgeld, Gewalt - jetzt angeblich ein Toter: Seit Monaten steht das Berliner Flüchtlingsamt Lageso in den Schlagzeilen. Die Chronologie des Scheiterns. </description>
+ <category>Politik</category>
+ <pubDate>Wed, 27 Jan 2016 14:22:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/politik/deutschland/berlin-das-scheitern-des-lageso-eine-chronik-a-1074186.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949137-thumbsmall-pgoa.jpg" hspace="5" align="left" >Tagelanges Warten, keine Auszahlung von Essensgeld, Gewalt - jetzt angeblich ein Toter: Seit Monaten steht das Berliner Flüchtlingsamt Lageso in den Schlagzeilen. Die Chronologie des Scheiterns. ]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949137-thumbsmall-pgoa.jpg"/>
+ </item>
+ <item>
+ <title>Apple-Gerüchte: So könnte das neue iPhone aussehen. Aber was wünschen Sie sich?</title>
+ <link>http://www.spiegel.de/netzwelt/gadgets/apple-so-gut-muss-das-iphone-7-sein-a-1074158.html#ref=rss</link>
+ <description>Ist der iPhone-Boom vorbei? Nicht unbedingt - das nächste Gerät könnte starke neue Features haben. Hier der Stand der Gerüchte. Und Sie können abstimmen: Was brauchen Sie wirklich?</description>
+ <category>Netzwelt</category>
+ <pubDate>Wed, 27 Jan 2016 14:17:00 +0100</pubDate>
+ <guid>http://www.spiegel.de/netzwelt/gadgets/apple-so-gut-muss-das-iphone-7-sein-a-1074158.html</guid>
+ <content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949140-thumbsmall-bdof.jpg" hspace="5" align="left" >Ist der iPhone-Boom vorbei? Nicht unbedingt - das nächste Gerät könnte starke neue Features haben. Hier der Stand der Gerüchte. Und Sie können abstimmen: Was brauchen Sie wirklich?]]></content:encoded>
+ <enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949140-thumbsmall-bdof.jpg"/>
+ </item>
+ </channel>
+</rss> \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/resources/feed_rss_tumblr.xml b/mobile/android/tests/background/junit4/resources/feed_rss_tumblr.xml
new file mode 100644
index 0000000000..15b20b6527
--- /dev/null
+++ b/mobile/android/tests/background/junit4/resources/feed_rss_tumblr.xml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0"><channel><description>Your reliable source for up-to-the-minute commodities pricing.</description><title>Tumblr Staff</title><generator>Tumblr (3.0; @staff)</generator><link>http://staff.tumblr.com/</link><item><title>hardyboyscovers:
+
+ Can Nancy Drew see things through and solve...</title><description>&lt;img src="http://41.media.tumblr.com/6dcceee090b2eef840cf694b205e1551/tumblr_o0r7mqDr5P1ug9yhzo1_400.jpg"/&gt;&lt;br/&gt;&lt;br/&gt;&lt;p&gt;&lt;a class="tumblr_blog" href="http://hardyboyscovers.tumblr.com/post/137036714181"&gt;hardyboyscovers&lt;/a&gt;:&lt;/p&gt;
+ &lt;blockquote&gt;
+ &lt;p&gt;Can Nancy Drew see things through and solve the case?&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
+ &lt;/blockquote&gt;
+
+ &lt;p&gt;Please horse, no&lt;/p&gt;</description><link>http://staff.tumblr.com/post/138124026275</link><guid>http://staff.tumblr.com/post/138124026275</guid><pubDate>Tue, 26 Jan 2016 21:30:12 -0500</pubDate><category>good blog</category><category>bad horse</category><category>book covers</category><category>book cover week</category></item><item><title>Chasing Storms at 17,500mph</title><description>&lt;p&gt;&lt;a class="tumblr_blog" href="http://stationcdrkelly.tumblr.com/post/138047038357"&gt;stationcdrkelly&lt;/a&gt;:&lt;/p&gt;
+ &lt;blockquote&gt;
+ &lt;p&gt;Flying 250 miles above the Earth aboard the International Space Station has given me the unique vantage point from which to view our planet. Spending a year in space has given me the unique opportunity to see a wide range of spectacular storm systems in space and on Earth. &lt;br/&gt;&lt;/p&gt;
+ &lt;p&gt;The recent blizzard was remarkably visible from space. I took several photos of the first big storm system on Earth of year 2016 as it moved across the East Coast, Chicago and Washington D.C. Since my time here on the space station began in March 2015, I’ve been able to capture an array of storms on Earth and in space, ranging from hurricanes and dust storms to solar storms and most recently a rare thunder snowstorm. &lt;/p&gt;
+ &lt;figure class="tmblr-full" data-orig-height="1065" data-orig-width="1600"&gt;&lt;img src="http://41.media.tumblr.com/ba62a8f18ceef5a20f60b700d14b39fe/tumblr_inline_o1hmtucs6o1tmec5e_540.jpg" data-orig-height="1065" data-orig-width="1600"/&gt;&lt;/figure&gt;&lt;p&gt;Blizzard 2016&lt;/p&gt;
+ &lt;figure class="tmblr-full" data-orig-height="1065" data-orig-width="1600"&gt;&lt;img src="http://40.media.tumblr.com/371eedcc0adf48ed07c5d1bd3f326ef1/tumblr_inline_o1hmuyAVw01tmec5e_540.jpg" data-orig-height="1065" data-orig-width="1600"/&gt;&lt;/figure&gt;&lt;p&gt;Hurricane Patricia 2015&lt;/p&gt;
+ &lt;figure class="tmblr-full" data-orig-height="1065" data-orig-width="1600"&gt;&lt;img src="http://40.media.tumblr.com/07cc92970eea9607b20264f589a94f59/tumblr_inline_o1hmwd9dIE1tmec5e_540.jpg" data-orig-height="1065" data-orig-width="1600"/&gt;&lt;/figure&gt;&lt;p&gt;Hurricane Joaquin 2015&lt;/p&gt;
+ &lt;figure class="tmblr-full" data-orig-height="682" data-orig-width="1024"&gt;&lt;img src="http://41.media.tumblr.com/ee5232cbf15d24b23932e0af0384f922/tumblr_inline_o1hmyeCO2I1tmec5e_540.jpg" data-orig-height="682" data-orig-width="1024"/&gt;&lt;/figure&gt;&lt;p&gt;Dust Storm in the Red Sea 2015&lt;/p&gt;
+ &lt;figure class="tmblr-full" data-orig-height="1065" data-orig-width="1600"&gt;&lt;img src="http://41.media.tumblr.com/e1c72c1a9be6394db409f0ffd0db31f8/tumblr_inline_o1hmzrMkBF1tmec5e_540.jpg" data-orig-height="1065" data-orig-width="1600"/&gt;&lt;/figure&gt;&lt;p&gt;Dust Storm of Gobi Desert 2015&lt;/p&gt;
+ &lt;figure class="tmblr-full" data-orig-height="1065" data-orig-width="1600"&gt;&lt;img src="http://40.media.tumblr.com/aa3a6d80dc603a9cc2cd5ceeb528ca55/tumblr_inline_o1hn1cI5xt1tmec5e_540.jpg" data-orig-height="1065" data-orig-width="1600"/&gt;&lt;/figure&gt;&lt;p&gt;Aurora Solar Storm 2015&lt;/p&gt;
+ &lt;figure class="tmblr-full" data-orig-height="1065" data-orig-width="1600"&gt;&lt;img src="http://40.media.tumblr.com/6d5c5715f629aa0a37908cc7ddc9e9a4/tumblr_inline_o1hn2ewElf1tmec5e_540.jpg" data-orig-height="1065" data-orig-width="1600"/&gt;&lt;/figure&gt;&lt;p&gt;Aurora Solar Storm 2016&lt;/p&gt;
+ &lt;figure class="tmblr-full" data-orig-height="1065" data-orig-width="1600"&gt;&lt;img src="http://41.media.tumblr.com/ec251e592ad4132c3141d3289344f6b0/tumblr_inline_o1hn7eMqGD1tmec5e_540.jpg" data-orig-height="1065" data-orig-width="1600"/&gt;&lt;/figure&gt;&lt;p&gt;Thunderstorm over Italy 2015&lt;/p&gt;
+ &lt;figure class="tmblr-full" data-orig-height="1067" data-orig-width="1600"&gt;&lt;img src="http://36.media.tumblr.com/d15b6033547c0e5c465a35614cdd753e/tumblr_inline_o1hnaxgi6r1tmec5e_540.jpg" data-orig-height="1067" data-orig-width="1600"/&gt;&lt;/figure&gt;&lt;p&gt;Lightning and Aurora 2016&lt;/p&gt;
+ &lt;figure class="tmblr-full" data-orig-height="1065" data-orig-width="1600"&gt;&lt;img src="http://36.media.tumblr.com/96f14b994bba66b9f690a6e086657333/tumblr_inline_o1hnc4GTCi1tmec5e_540.jpg" data-orig-height="1065" data-orig-width="1600"/&gt;&lt;/figure&gt;&lt;p&gt;Rare Thunder Snowstorm 2016&lt;/p&gt;
+ &lt;p&gt;Follow my Year In Space on Twitter, Facebook and Instagram. &lt;/p&gt;
+ &lt;/blockquote&gt;
+
+ &lt;p&gt;Astronaut Scott Kelly just signed up for Tumblr all the way from space. &lt;i&gt;Outer&lt;/i&gt; space. So far, this the coolest weather blog of all time. &lt;/p&gt;</description><link>http://staff.tumblr.com/post/138053124065</link><guid>http://staff.tumblr.com/post/138053124065</guid><pubDate>Mon, 25 Jan 2016 19:48:40 -0500</pubDate></item><item><title>digg:
+
+ first it starts out like a little spinning hexagonthen it spins more and gets biggermore...</title><description>&lt;p&gt;&lt;a href="http://digg.tumblr.com/post/137842908963/first-it-starts-out-like-a-little-spinning-hexagon" class="tumblr_blog"&gt;digg&lt;/a&gt;:&lt;/p&gt;
+
+ &lt;blockquote&gt;&lt;figure data-orig-width="473" data-orig-height="52" class="tmblr-full"&gt;&lt;img src="http://40.media.tumblr.com/0ec3a49eb7143dc50e7d3a7252bb707f/tumblr_inline_o1dnci9q4j1ro8h5m_540.png" data-orig-width="473" data-orig-height="52"/&gt;&lt;/figure&gt;&lt;p&gt;&lt;br/&gt;first it starts out like a little spinning hexagon&lt;/p&gt;&lt;figure data-orig-width="400" data-orig-height="396" class="tmblr-full"&gt;&lt;img src="http://38.media.tumblr.com/fb369bd707e311591f85de1d10b94bc6/tumblr_inline_o1dnbpkqXI1ro8h5m_500.gif" data-orig-width="400" data-orig-height="396"/&gt;&lt;/figure&gt;&lt;p&gt;then it spins more and gets bigger&lt;br/&gt;&lt;/p&gt;&lt;figure data-orig-width="400" data-orig-height="396" class="tmblr-full"&gt;&lt;img src="http://33.media.tumblr.com/5d5e6abd203bda735f5b798bd7d100cb/tumblr_inline_o1dnbnbv6r1ro8h5m_500.gif" data-orig-width="400" data-orig-height="396"/&gt;&lt;/figure&gt;&lt;p&gt;more spinning, the background gets red&lt;/p&gt;&lt;figure data-orig-width="400" data-orig-height="396" class="tmblr-full"&gt;&lt;img src="http://38.media.tumblr.com/7b11f8f9abec6211440880cee0c94a89/tumblr_inline_o1dnbnn1IM1ro8h5m_500.gif" data-orig-width="400" data-orig-height="396"/&gt;&lt;/figure&gt;&lt;p&gt;then it goes all over your driveway like this&lt;/p&gt;&lt;figure class="tmblr-full" data-orig-height="225" data-orig-width="400"&gt;&lt;img src="http://38.media.tumblr.com/6e3d06c9a97f3acc05c20d2e37f3f730/tumblr_inline_o1dney205v1ro8h5m_500.gif" data-orig-height="225" data-orig-width="400"/&gt;&lt;/figure&gt;&lt;/blockquote&gt;</description><link>http://staff.tumblr.com/post/137845368600</link><guid>http://staff.tumblr.com/post/137845368600</guid><pubDate>Fri, 22 Jan 2016 19:47:00 -0500</pubDate><category>have a good weekend tumblr</category><category>stay safe stay beautiful</category></item><item><title>This week in drama clubHigh School Musical: Bopped to the top 10...</title><description>&lt;img src="http://40.media.tumblr.com/9c69a850eece63c29d913b6e83dd49a7/tumblr_o1dcfztyzk1qz8q0ho1_500.png"/&gt;&lt;br/&gt;&lt;br/&gt;&lt;h2&gt;This week in drama club&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;b&gt;&lt;a href="http://tumblr.com/search/golden%20disk%20awards"&gt;&lt;/a&gt;&lt;/b&gt;&lt;a href="http://tumblr.com/search/hsm"&gt;&lt;i&gt;&lt;b&gt;High School Musical&lt;/b&gt;&lt;/i&gt;&lt;/a&gt;: Bopped to the top 10 years ago this week.&lt;br/&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="http://tumblr.com/search/golden%20disk%20awards"&gt;&lt;i&gt;&lt;b&gt;Golden Disk Awards&lt;/b&gt;&lt;/i&gt;&lt;/a&gt;: Baked to perfection. Thanks, Zeke!&lt;br/&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="http://tumblr.com/search/miraculous%20ladybug"&gt;&lt;b&gt;&lt;i&gt;Miraculous Ladybug&lt;/i&gt;&lt;/b&gt;&lt;/a&gt;: The Gabriella to Cat Noir’s Troy.&lt;br/&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="http://tumblr.com/search/agent%20carter"&gt;&lt;b&gt;&lt;i&gt;Agent Carter&lt;/i&gt;&lt;/b&gt;&lt;/a&gt;: The start of something new (season two).&lt;br/&gt;&lt;/li&gt;&lt;/ul&gt;&lt;figure data-orig-width="160" data-orig-height="180" data-tumblr-attribution="hunting-the-grievers:cPVhQbqjjwjt3Hpa2i0A3Q:ZDFbbm1f4dbEu" class="tmblr-full"&gt;&lt;img src="http://38.media.tumblr.com/d6c281341a84fd1367682d598b008af9/tumblr_nkrg1eqpXu1tl1wyoo1_250.gif" alt="image" data-orig-width="160" data-orig-height="180"/&gt;&lt;/figure&gt;&lt;h2&gt;Student government elections&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href="http://tumblr.com/search/sarah%20palin"&gt;&lt;b&gt;Sarah Palin&lt;/b&gt;&lt;/a&gt;: Song singin’, bitter clingin’, proud clingers of West Side Knights.&lt;br/&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="http://tumblr.com/search/democratic%20debate"&gt;&lt;b&gt;Democratic debate&lt;/b&gt;&lt;/a&gt;: A political decathlon.&lt;/li&gt;&lt;/ul&gt;&lt;h2&gt;Wildcat co-captains&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href="http://tumblr.com/search/matt%20the%20radar%20technician"&gt;&lt;b&gt;Matt the Radar Technician&lt;/b&gt;&lt;/a&gt;: Secretly Coach Bolton.&lt;br/&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="http://tumblr.com/search/jada%20pinkett%20smith"&gt;&lt;b&gt;Jada Pinkett Smith&lt;/b&gt;&lt;/a&gt;: Owns more hats than Ryan Evans.&lt;br/&gt;&lt;/li&gt;&lt;/ul&gt;&lt;figure data-orig-width="480" data-orig-height="270" data-tumblr-attribution="nbcsnl:B-Wm_cenWtc03w62FRwf1A:ZpdRYu201JEGk" class="tmblr-full"&gt;&lt;img src="http://31.media.tumblr.com/acd5b9d1d6ef051d12fabbedeb1bc023/tumblr_o13140hUBg1rdzuduo1_500.gif" alt="image" data-orig-width="480" data-orig-height="270"/&gt;&lt;/figure&gt;&lt;h2&gt;Get’cha head in the game&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href="http://tumblr.com/search/wwe"&gt;&lt;b&gt;WWE&lt;/b&gt;&lt;/a&gt;: We’re all in this Royal Rumble together.&lt;br/&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="http://tumblr.com/search/australian%20open"&gt;&lt;b&gt;Australian Open&lt;/b&gt;&lt;/a&gt;: Go Wildcats!&lt;br/&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h2&gt;Sticking to the status quo: Check out these Tumblrs&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;b&gt;Creative Capital&lt;/b&gt; (&lt;a href="http://tmblr.co/mcSeD8C3uF_aHrmt0C6VpUw"&gt;@creative-cap&lt;/a&gt;): Patron of the arts.&lt;br/&gt;&lt;/li&gt;&lt;li&gt;&lt;b&gt;Broadway Con&lt;/b&gt; (&lt;a href="http://tmblr.co/miT9FBjVCw7YfioPNppjryw"&gt;@bwaycon&lt;/a&gt;): What you’ve been looking for.&lt;br/&gt;&lt;/li&gt;&lt;li&gt;&lt;b&gt;Mall Goth Phase&lt;/b&gt; (&lt;a href="http://tmblr.co/mUHclMBjOwpJBi4M1bucs5Q"&gt;@mallgothphase&lt;/a&gt;): Hot, topical blog.&lt;br/&gt;&lt;/li&gt;&lt;/ul&gt;&lt;figure data-orig-width="367" data-orig-height="245" data-tumblr-attribution="wrestling-giffer:V0IfEVup_dIDxzIYMr5R0A:Z1UX8r1yd0krY" class="tmblr-full"&gt;&lt;img src="http://38.media.tumblr.com/5685bb235ec944c5084939da16c24958/tumblr_ny9xzoSQ5K1sbzhteo1_400.gif" alt="image" data-orig-width="367" data-orig-height="245"/&gt;&lt;/figure&gt;</description><link>http://staff.tumblr.com/post/137833222680</link><guid>http://staff.tumblr.com/post/137833222680</guid><pubDate>Fri, 22 Jan 2016 16:07:26 -0500</pubDate><category>trends</category></item><item><title>nationwideexposure:
+
+ I’M CRYING...</title><description>&lt;img src="http://40.media.tumblr.com/7ba6d036bd5939f087f8feee424dc337/tumblr_n1aawcsEOd1rr31p0o1_400.jpg"/&gt;&lt;br/&gt; &lt;br/&gt;&lt;img src="http://41.media.tumblr.com/8bfce3018cf868734dc73f11c9c84cce/tumblr_n1aawcsEOd1rr31p0o2_500.jpg"/&gt;&lt;br/&gt; &lt;br/&gt;&lt;img src="http://41.media.tumblr.com/68275fc41a5879e03f6b67401d21b9c6/tumblr_n1aawcsEOd1rr31p0o3_500.jpg"/&gt;&lt;br/&gt; &lt;br/&gt;&lt;img src="http://40.media.tumblr.com/4d987c36b13dc78ec66d9f7311f23ed8/tumblr_n1aawcsEOd1rr31p0o4_500.jpg"/&gt;&lt;br/&gt; &lt;br/&gt;&lt;img src="http://40.media.tumblr.com/052fb3ff421e40865f517b60e7a00af4/tumblr_n1aawcsEOd1rr31p0o5_500.jpg"/&gt;&lt;br/&gt; &lt;br/&gt;&lt;img src="http://40.media.tumblr.com/b4d65c50fed46518eef6091575171247/tumblr_n1aawcsEOd1rr31p0o6_250.jpg"/&gt;&lt;br/&gt; &lt;br/&gt;&lt;img src="http://41.media.tumblr.com/e989c0771b588d4b8e4068caa71c7764/tumblr_n1aawcsEOd1rr31p0o7_500.jpg"/&gt;&lt;br/&gt; &lt;br/&gt;&lt;img src="http://41.media.tumblr.com/3a9ca6beb713678743987b830ce2a5a6/tumblr_n1aawcsEOd1rr31p0o8_500.jpg"/&gt;&lt;br/&gt; &lt;br/&gt;&lt;img src="http://41.media.tumblr.com/72daba4a9a2598dcbc18f6d5b7c1ecaa/tumblr_n1aawcsEOd1rr31p0o9_500.jpg"/&gt;&lt;br/&gt; &lt;br/&gt;&lt;img src="http://40.media.tumblr.com/a9db431bf354b42fd035d44c9910bfe6/tumblr_n1aawcsEOd1rr31p0o10_500.jpg"/&gt;&lt;br/&gt; &lt;br/&gt;&lt;p&gt;&lt;a class="tumblr_blog" href="http://nationwideexposure.tumblr.com/post/77260363328"&gt;nationwideexposure&lt;/a&gt;:&lt;/p&gt;
+ &lt;blockquote&gt;
+ &lt;p&gt;I’M CRYING 😂😭😂😭😂&lt;/p&gt;
+ &lt;/blockquote&gt;
+ &lt;p&gt;——&lt;br/&gt;——&lt;br/&gt;——&lt;br/&gt;——&lt;/p&gt;&lt;p&gt; 👀&lt;br/&gt;——&lt;br/&gt;——&lt;br/&gt;——&lt;br/&gt;——&lt;/p&gt;</description><link>http://staff.tumblr.com/post/137788176890</link><guid>http://staff.tumblr.com/post/137788176890</guid><pubDate>Thu, 21 Jan 2016 22:00:16 -0500</pubDate><category>stock photo</category><category>stock photo week</category></item><item><title>risingfunk:
+
+ hey creationists,
+ if evolution isn’t real… explain...</title><description>&lt;img src="http://41.media.tumblr.com/9dd0ed0e50f1963930b55002f22d2569/tumblr_njt97mInHy1rz419eo3_500.jpg"/&gt;&lt;br/&gt; &lt;br/&gt;&lt;img src="http://41.media.tumblr.com/39362963e0d3bab55f84c7b9e7acc1b1/tumblr_njt97mInHy1rz419eo4_500.jpg"/&gt;&lt;br/&gt; &lt;br/&gt;&lt;img src="http://36.media.tumblr.com/f2e4d58b37bebdd48c0593b66c2fa910/tumblr_njt97mInHy1rz419eo2_500.jpg"/&gt;&lt;br/&gt; &lt;br/&gt;&lt;img src="http://40.media.tumblr.com/0e0fb52c15c625d46ee1fa108c102e41/tumblr_njt97mInHy1rz419eo1_500.jpg"/&gt;&lt;br/&gt; &lt;br/&gt;&lt;p&gt;&lt;a class="tumblr_blog" href="http://risingfunk.tumblr.com/post/111068384527"&gt;risingfunk&lt;/a&gt;:&lt;/p&gt;
+ &lt;blockquote&gt;
+ &lt;p&gt;hey creationists,&lt;/p&gt;
+ &lt;p&gt;if evolution isn’t real… explain &lt;b&gt;THIS&lt;/b&gt;&lt;br/&gt;&lt;/p&gt;
+ &lt;/blockquote&gt;
+
+ &lt;p&gt;We’d like anyone to explain this.&lt;/p&gt;</description><link>http://staff.tumblr.com/post/137724564137</link><guid>http://staff.tumblr.com/post/137724564137</guid><pubDate>Wed, 20 Jan 2016 22:00:29 -0500</pubDate><category>stock photo</category><category>stock photo week</category></item><item><title>I’ll Be There For You (Theme from…)</title><description>&lt;p&gt;&lt;a class="tumblr_blog" href="http://thefandometrics.tumblr.com/post/137713318764"&gt;thefandometrics&lt;/a&gt;:&lt;/p&gt;
+ &lt;blockquote&gt;
+ &lt;p&gt;&lt;figure class="tmblr-full" data-orig-height="100" data-orig-width="500"&gt;&lt;img src="http://36.media.tumblr.com/b528045dbfddacd8dcb1789ae1aabad8/tumblr_inline_o19xl51Nx51qz7tc0_540.png" data-orig-height="100" data-orig-width="500"/&gt;&lt;/figure&gt;&lt;/p&gt;&lt;h2&gt;
+ &lt;a href="http://thefandometrics.tumblr.com/post/137583618255/tv-shows-week-ending-january-18th-2016-steven"&gt;Television&lt;/a&gt;: So no one told you life was gonna be this way.&lt;/h2&gt;
+ &lt;blockquote&gt;&lt;p&gt;☆ &lt;b&gt;&lt;i&gt;&lt;a href="https://www.tumblr.com/search/Saturday+Night+Live"&gt;Saturday Night Live&lt;/a&gt;&lt;/i&gt;&lt;/b&gt; returns to No. 11 with a little help from Kyl–uh, Matt the Radar Technician.&lt;br/&gt; ⬆ &lt;b&gt;&lt;i&gt;&lt;a href="https://www.tumblr.com/search/Pretty%20Little%20Liars"&gt;Pretty Little Liars&lt;/a&gt;&lt;/i&gt;&lt;/b&gt; (No. 7) jumped five years in the future and eleven spots on our list.&lt;br/&gt;☆ &lt;b&gt;&lt;i&gt;&lt;a href="https://www.tumblr.com/search/Rick%20and%20Morty"&gt;Rick and Morty&lt;/a&gt;&lt;/i&gt;&lt;/b&gt; came back at a solid No. 18 while all of you played &lt;i&gt;Pocket Mortys&lt;/i&gt;. &lt;/p&gt;&lt;/blockquote&gt;
+ &lt;figure class="tmblr-full" data-orig-height="271" data-orig-width="480" data-tumblr-attribution="ren-rey-lo:3VGJxt125Qzd9mCpO3SEBg:ZZ1ZMi202CMhl"&gt;&lt;img src="http://38.media.tumblr.com/fd91b6634cfd45862f50b955a4dcecd6/tumblr_o13kbrUuy51v4pr6ko4_500.gif" data-orig-height="271" data-orig-width="480"/&gt;&lt;/figure&gt;&lt;h2&gt;
+ &lt;a href="http://thefandometrics.tumblr.com/post/137583047944/movies-week-ending-january-18th-2016-star-wars"&gt;Movies&lt;/a&gt;: ðŸ‘ðŸ‘ðŸ‘ðŸ‘.&lt;/h2&gt;
+ &lt;blockquote&gt;&lt;p&gt;⬆ &lt;b&gt;&lt;i&gt;&lt;a href="https://www.tumblr.com/search/Deadpool"&gt;Deadpool&lt;/a&gt;&lt;/i&gt;&lt;/b&gt;’s (No. 2) &lt;a href="http://www.herochan.com/post/137155502690/deadpool-movie-promotion-done-right"&gt;marketing&lt;/a&gt; team deserves a raise. &lt;br/&gt;☆ &lt;b&gt;&lt;i&gt;&lt;a href="https://www.tumblr.com/search/Labyrinth"&gt;Labyrinth&lt;/a&gt;&lt;/i&gt;&lt;/b&gt; debuted at No. 6. All hail The Goblin King. &lt;/p&gt;&lt;/blockquote&gt;
+ &lt;h2&gt;
+ &lt;a href="http://thefandometrics.tumblr.com/post/137582487533/musical-acts-week-ending-january-18th-2016-david"&gt;Music&lt;/a&gt;: Your life’s a joke, you’re broke.&lt;/h2&gt;
+ &lt;blockquote&gt;&lt;p&gt;☆ &lt;b&gt;&lt;a href="https://www.tumblr.com/search/David%20Bowie"&gt;David Bowie&lt;/a&gt;&lt;/b&gt; is No. 1, as he should be.&lt;br/&gt;⬆ &lt;b&gt;&lt;a href="https://www.tumblr.com/search/Panic%20at%20the%20Disco"&gt;Panic! at the Disco&lt;/a&gt;&lt;/b&gt; did the hustle up to No. 4.&lt;/p&gt;&lt;/blockquote&gt;
+ &lt;h2&gt;
+ &lt;a href="http://thefandometrics.tumblr.com/post/137581921455/celebrities-week-ending-january-18th-2016-alan"&gt;Celebrities&lt;/a&gt;: Your love life’s D.O.A.&lt;/h2&gt;
+ &lt;blockquote&gt;&lt;p&gt;☆ &lt;b&gt;&lt;a href="https://www.tumblr.com/search/Alan%20Rickman"&gt;Alan Rickman&lt;/a&gt;&lt;/b&gt; is No. 1, as he should be.&lt;br/&gt; ⬆ &lt;b&gt;&lt;a href="https://www.tumblr.com/search/Leonardo%20DiCaprio"&gt;Leonardo DiCaprio&lt;/a&gt;&lt;/b&gt; continues his climb, moves up to No. 5. A true revenant.&lt;br/&gt; ☆ &lt;b&gt;&lt;a href="https://www.tumblr.com/search/Gillian%20Anderson"&gt;Gillian Anderson&lt;/a&gt;&lt;/b&gt; debuted at No. 14. We want to believe in the reboot. &lt;/p&gt;&lt;/blockquote&gt;
+ &lt;figure class="tmblr-full" data-orig-height="211" data-orig-width="500" data-tumblr-attribution="szabcsiify:6F9QwBM8is-j-q9KKvjFOQ:Z5xAar1qg38kJ"&gt;&lt;img src="http://33.media.tumblr.com/ea080946a865dd802c098792fc303649/tumblr_ns7chfYxeU1sxov47o1_500.gif" data-orig-height="211" data-orig-width="500"/&gt;&lt;/figure&gt;&lt;h2&gt;
+ &lt;a href="http://thefandometrics.tumblr.com/post/137581349025/video-games-week-ending-january-18th-2016"&gt;Games&lt;/a&gt;: It’s like you’re always stuck in second gear. &lt;/h2&gt;
+ &lt;blockquote&gt;&lt;p&gt;⬆ &lt;b&gt;&lt;i&gt;&lt;a href="https://www.tumblr.com/search/Pok%C3%A9mon"&gt;Pokémon&lt;/a&gt;&lt;/i&gt;&lt;/b&gt; (No. 2) is twenty years old. Like, human earth years. Twenty of them.&lt;br/&gt; ⬆ &lt;b&gt;&lt;i&gt;&lt;a href="https://www.tumblr.com/search/Splatoon"&gt;Splatoon&lt;/a&gt;&lt;/i&gt;&lt;/b&gt;’s Splatfest was an inky success, propelling it to No. 10.&lt;/p&gt;&lt;/blockquote&gt;
+ &lt;h2&gt;
+ &lt;a href="http://thefandometrics.tumblr.com/post/137580830792/web-stuff-week-ending-january-18th-2016"&gt;Web stuff&lt;/a&gt;: When it hasn’t been your day, your week, your month, or even your year.&lt;/h2&gt;
+ &lt;blockquote&gt;&lt;p&gt;⬆ Vlogging savant &lt;b&gt;&lt;a href="https://www.tumblr.com/search/Troye%20Sivan"&gt;Troye Sivan&lt;/a&gt;&lt;/b&gt; moves up to No. 4.&lt;br/&gt; ⬇︎ Story time! &lt;b&gt;&lt;a href="https://www.tumblr.com/search/Thomas%20Sanders"&gt;Thomas Sanders&lt;/a&gt;&lt;/b&gt; fell seven spots to No. 19.&lt;/p&gt;&lt;/blockquote&gt;
+ &lt;/blockquote&gt;</description><link>http://staff.tumblr.com/post/137714050360</link><guid>http://staff.tumblr.com/post/137714050360</guid><pubDate>Wed, 20 Jan 2016 18:42:44 -0500</pubDate><category>week in review</category></item><item><title>Photo</title><description>&lt;img src="http://36.media.tumblr.com/9886e52dcd701c004da445cecd89e7a7/tumblr_ne2u5i2aWI1sq05vko1_400.png"/&gt;&lt;br/&gt;&lt;br/&gt;</description><link>http://staff.tumblr.com/post/137656822367</link><guid>http://staff.tumblr.com/post/137656822367</guid><pubDate>Tue, 19 Jan 2016 21:00:09 -0500</pubDate><category>stock</category><category>stock photo week</category><category>businessing</category><category>business</category></item><item><title>vedranmisic:
+
+ Happy Birthday, Dr. Martin Luther King Jr.“I HAVE...</title><description>&lt;img src="http://40.media.tumblr.com/01e16942f7f596872c5e7d73d41f989b/tumblr_o0z9nswYMm1re26ajo1_500.jpg"/&gt;&lt;br/&gt;&lt;br/&gt;&lt;p&gt;&lt;a href="http://vedranmisic.tumblr.com/post/137328304708/happy-birthday-dr-martin-luther-king-jr-i-have" class="tumblr_blog"&gt;vedranmisic&lt;/a&gt;:&lt;/p&gt;
+
+ &lt;blockquote&gt;&lt;p&gt;Happy Birthday, Dr. Martin Luther King Jr.&lt;br/&gt;“I HAVE A DREAM†(ink on paper, 11x14 inches)&lt;br/&gt;&lt;/p&gt;&lt;/blockquote&gt;</description><link>http://staff.tumblr.com/post/137556640935</link><guid>http://staff.tumblr.com/post/137556640935</guid><pubDate>Mon, 18 Jan 2016 11:20:27 -0500</pubDate><category>martin luther king jr</category></item><item><title>fruitsoftheweb:
+
+ Elastic and non-linear plastic effects are...</title><description>&lt;img src="http://45.media.tumblr.com/525d91fe69a7815bc84438e4dab4e8e0/tumblr_n7lvudTrkx1skn1oxo1_400.gif"/&gt;&lt;br/&gt;&lt;br/&gt;&lt;p&gt;&lt;a class="tumblr_blog" href="http://fruitsoftheweb.tumblr.com/post/89724820207"&gt;fruitsoftheweb&lt;/a&gt;:&lt;/p&gt;
+ &lt;blockquote&gt;
+ &lt;p class="p1"&gt;&lt;a href="https://www.youtube.com/watch?v=1Q_zb65SXt0"&gt;&lt;em&gt;Elastic and non-linear plastic effects are obtained by adding springs with varying rest length between particles.&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
+ &lt;/blockquote&gt;
+
+ &lt;p&gt;You can dress this description up any way you want, but this here link is a video about straight-up slime.&lt;/p&gt;</description><link>http://staff.tumblr.com/post/137382144100</link><guid>http://staff.tumblr.com/post/137382144100</guid><pubDate>Fri, 15 Jan 2016 21:00:30 -0500</pubDate><category>slime</category><category>slime week</category></item><item><title>Video</title><description>
+ &lt;video id='embed-56a8a4909ac31729864247' class='crt-video crt-skin-default' width='400' height='225' poster='http://media.tumblr.com/tumblr_o10q0s4lQw1qz8q0h_frame1.jpg' preload='none' data-crt-video data-crt-options='{"autoheight":null,"duration":299,"hdUrl":false,"filmstrip":{"url":"http:\/\/38.media.tumblr.com\/previews\/tumblr_o10q0s4lQw1qz8q0h_filmstrip.jpg","width":"200","height":"112"}}' &gt;
+ &lt;source src="http://staff.tumblr.com/video_file/137376306290/tumblr_o10q0s4lQw1qz8q0h" type="video/mp4"&gt;
+ &lt;/video&gt;
+ &lt;br/&gt;&lt;br/&gt;</description><link>http://staff.tumblr.com/post/137376306290</link><guid>http://staff.tumblr.com/post/137376306290</guid><pubDate>Fri, 15 Jan 2016 19:06:25 -0500</pubDate><category>have a relaxing weekend tumblr</category></item><item><title>Losing one was hard enough… David Bowie said his goodbye, we’re...</title><description>&lt;img src="http://40.media.tumblr.com/9c69a850eece63c29d913b6e83dd49a7/tumblr_o10jfzocXP1qz8q0ho1_500.png"/&gt;&lt;br/&gt;&lt;br/&gt;&lt;h2&gt;Losing one was hard enough… &lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href="http://tumblr.com/search/david%20bowie"&gt;&lt;b&gt;David Bowie&lt;/b&gt;&lt;/a&gt; said his goodbye, we’re all thinking of his love. &lt;br/&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="http://tumblr.com/search/alan%20rickman"&gt;&lt;b&gt;Alan Rickman&lt;/b&gt;&lt;/a&gt; is survived by his wife and his magic.&lt;br/&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Losing &lt;a href="http://tumblr.com/search/powerball"&gt;&lt;b&gt;POWERBALL&lt;/b&gt;&lt;/a&gt; didn’t help anything.&lt;/p&gt;&lt;figure data-orig-width="500" data-orig-height="278" data-tumblr-attribution="rustbeltjessie:Z20sTdCnR6V5dULG-Pk5FA:ZyRL1u1-svswa" class="tmblr-full"&gt;&lt;img src="http://33.media.tumblr.com/896e5f5e4ea77f08a930d01a39723805/tumblr_o0y90c1KPr1rift8jo1_500.gif" alt="image" data-orig-width="500" data-orig-height="278"/&gt;&lt;/figure&gt;&lt;h2&gt;It’s award season (again)&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href="http://tumblr.com/search/golden%20globes"&gt;&lt;b&gt;Golden Globes&lt;/b&gt;&lt;/a&gt; got a little loose with the juice.&lt;/li&gt;&lt;li&gt;&lt;a href="http://tumblr.com/search/leonardo%20dicaprio"&gt;&lt;b&gt;Leo DiCaprio&lt;/b&gt;&lt;/a&gt; got on Lady Gaga’s shit list.&lt;/li&gt;&lt;li&gt;And &lt;a href="http://tumblr.com/search/oscars%202016"&gt;&lt;b&gt;Oscars 2016&lt;/b&gt;&lt;/a&gt;: got heat for an all-white nomination slate.&lt;/li&gt;&lt;/ul&gt;&lt;h2&gt;Trends gonna trend&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href="http://tumblr.com/search/coming%20out"&gt;&lt;b&gt;Coming out&lt;/b&gt;&lt;/a&gt;: It’s all part of growing up.&lt;/li&gt;&lt;li&gt;&lt;a href="http://tumblr.com/search/the%20shannara%20chronicles"&gt;&lt;b&gt;&lt;i&gt;The Shannara Chronicles&lt;/i&gt;&lt;/b&gt;&lt;/a&gt;: A shoo be doo wop / bop bop doo wop.&lt;/li&gt;&lt;li&gt;&lt;a href="http://tumblr.com/search/shadowhunters"&gt;&lt;b&gt;&lt;i&gt;Shadowhunters&lt;/i&gt;&lt;/b&gt;&lt;/a&gt;: Best played on a sunny day.&lt;/li&gt;&lt;/ul&gt;&lt;figure data-orig-width="500" data-orig-height="281" data-tumblr-attribution="katiecouric:bvFuZWnCzpG75MylRNwCUA:ZPcBQw1-hna98" class="tmblr-full"&gt;&lt;img src="http://33.media.tumblr.com/1da9dfe557532a71323d45749dd6ea3c/tumblr_o0swib9HUI1r7kaf3o1_500.gif" alt="image" data-orig-width="500" data-orig-height="281"/&gt;&lt;/figure&gt;&lt;h2&gt;And these dynamite Tumblrs: &lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;b&gt;Plus Size Never Looked So Good&lt;/b&gt; (&lt;a href="http://tmblr.co/mpKfpxC4WXhPT0bbAU74L0w"&gt;@plussizeneverlookedsogood&lt;/a&gt;): Big girl swag.&lt;br/&gt;&lt;/li&gt;&lt;li&gt;&lt;b&gt;Amandla&lt;/b&gt; (&lt;a href="http://tmblr.co/mfy9cYbpNQiEgZnaWyfwSRg"&gt;@amandla&lt;/a&gt;): You know her as Rue, but you should see her graphic novels.&lt;br/&gt;&lt;/li&gt;&lt;li&gt;&lt;b&gt;Black Shop, White Writing &lt;/b&gt;(&lt;a href="http://tmblr.co/myTgmVRoFyIoi0hhKlWr0vw"&gt;@blackshopswhitewriting&lt;/a&gt;): Shades of gentrification&lt;br/&gt;&lt;/li&gt;&lt;/ul&gt;&lt;figure data-orig-width="500" data-orig-height="190" data-tumblr-attribution="ziggyreturns:LA35rxFHUTbzeMrpzP2h-w:ZfoOZm1mAh7Bv" class="tmblr-full"&gt;&lt;img src="http://33.media.tumblr.com/6bb8b39e6a9cd2c981fee71de1a281a6/tumblr_noi61wxvz31tlqitmo1_500.gif" alt="image" data-orig-width="500" data-orig-height="190"/&gt;&lt;/figure&gt;</description><link>http://staff.tumblr.com/post/137369118080</link><guid>http://staff.tumblr.com/post/137369118080</guid><pubDate>Fri, 15 Jan 2016 16:55:09 -0500</pubDate><category>trends</category></item><item><title>Is this satisfying or enraging? We can’t decide. </title><description>&lt;img src="http://49.media.tumblr.com/d022b6caad3f1b5ae988b94d7113b434/tumblr_nyv2d24zYP1v0kd8mo1_500.gif"/&gt;&lt;br/&gt;&lt;br/&gt;&lt;p&gt;Is this satisfying or enraging? We can’t decide. &lt;/p&gt;</description><link>http://staff.tumblr.com/post/137318516024</link><guid>http://staff.tumblr.com/post/137318516024</guid><pubDate>Thu, 14 Jan 2016 21:00:04 -0500</pubDate><category>slime</category><category>slime week</category><category>you decide</category></item><item><title>⬆ This great GIF button is now on iOS This button turns your...</title><description>&lt;img src="http://49.media.tumblr.com/8342edc0521edf0edcfabeed522825c9/tumblr_o0st80ul691qz8q0ho1_500.gif"/&gt;&lt;br/&gt;&lt;br/&gt;&lt;h2&gt;⬆ This great GIF button is now on iOS &lt;/h2&gt;&lt;p&gt;This button turns your feelings into GIFs, then inserts those GIFs into your posts. &lt;a href="http://staff.tumblr.com/post/120720833005/since-gifs-have-replaced-written-language-were"&gt;Web&lt;/a&gt; and &lt;a href="http://staff.tumblr.com/post/135263390205/a-treat-for-android-users-now-you-can-add-gifs-to"&gt;Android&lt;/a&gt; users have had it for a little while, and now the triumvirate is complete. &lt;/p&gt;&lt;p&gt;First, get the &lt;a href="https://itunes.apple.com/us/app/tumblr/id305343404?mt=8"&gt;latest version of the app&lt;/a&gt;. Next, open up a new post (or reblog!) and tap the GIF button. Search for something you want to express. Something like “potato.†Pick one, and into your post it goes:&lt;/p&gt;&lt;figure data-orig-width="500" data-orig-height="367" data-orig-src="http://38.media.tumblr.com/801919ec41dfc2f1c5ec1b5d62482ba4/tumblr_inline_o0yfc3XE0y1qzpzhj_500.gif" data-tumblr-attribution="addyisabutler:Hr9auQKKX-zyrYFX_K4zIQ:ZClLCj1wTXFo4" class="tmblr-full"&gt;&lt;img src="http://38.media.tumblr.com/801919ec41dfc2f1c5ec1b5d62482ba4/tumblr_inline_o0zac6myJV1qzpzhj_500.gif" alt="image" data-orig-width="500" data-orig-height="367" data-orig-src="http://38.media.tumblr.com/801919ec41dfc2f1c5ec1b5d62482ba4/tumblr_inline_o0yfc3XE0y1qzpzhj_500.gif"/&gt;&lt;/figure&gt;&lt;p&gt;What a marvelous potato! It communicates a passion that words cannot. Very nice. &lt;i&gt;Very nice indeed&lt;/i&gt;. &lt;/p&gt;</description><link>http://staff.tumblr.com/post/137292522640</link><guid>http://staff.tumblr.com/post/137292522640</guid><pubDate>Thu, 14 Jan 2016 13:00:03 -0500</pubDate><category>features</category></item><item><title>jellygummies:
+ Iridescent goo! (and the guy doesn’t even...</title><description>&lt;img src="http://45.media.tumblr.com/5c93c21e43e0e672a5ae97d7e93d34e7/tumblr_n3je4jy7zk1syjt2wo1_400.gif"/&gt;&lt;br/&gt;&lt;br/&gt;&lt;p&gt;&lt;a class="tumblr_blog" href="http://jellygummies.tumblr.com/post/81738200024"&gt;jellygummies&lt;/a&gt;:&lt;/p&gt;&lt;blockquote&gt;
+ &lt;p&gt;Iridescent goo! (and the guy doesn’t even flinch) &lt;/p&gt;
+ &lt;/blockquote&gt;</description><link>http://staff.tumblr.com/post/137259011190</link><guid>http://staff.tumblr.com/post/137259011190</guid><pubDate>Wed, 13 Jan 2016 22:15:43 -0500</pubDate><category>slime week</category><category>sorry</category><category>we hate this too</category><category>in a real appreciative way</category></item><item><title>Heavens to Betsy, it’s…</title><description>&lt;p&gt;&lt;a href="http://thefandometrics.tumblr.com/post/137250140754/heavens-to-betsy-its" class="tumblr_blog"&gt;thefandometrics&lt;/a&gt;:&lt;/p&gt;
+
+ &lt;blockquote&gt;&lt;figure data-orig-width="500" data-orig-height="100" class="tmblr-full"&gt;&lt;img src="http://41.media.tumblr.com/b528045dbfddacd8dcb1789ae1aabad8/tumblr_inline_o0wy2ln5Md1qz7tc0_540.png" alt="image" data-orig-width="500" data-orig-height="100"/&gt;&lt;/figure&gt;&lt;h2&gt;&lt;a href="http://thefandometrics.tumblr.com/post/137118390340/tv-shows-week-ending-january-11th-2016-steven"&gt;Television&lt;/a&gt;: This section adjusted to fit your screen.&lt;/h2&gt;&lt;blockquote&gt;&lt;p&gt;⬆ &lt;b&gt;&lt;i&gt;&lt;a href="https://www.tumblr.com/search/Teen+Wolf"&gt;Teen Wolf&lt;/a&gt;&lt;/i&gt;&lt;/b&gt; makes a hair-raising leap to No. 2.&lt;br/&gt;☆ &lt;b&gt;&lt;i&gt;&lt;a href="https://www.tumblr.com/search/The%20Golden%20Globes"&gt;The Golden Globes&lt;/a&gt;&lt;/i&gt;&lt;/b&gt; debuted at No. 3. Shoulda been an award for &lt;a href="http://mtv.tumblr.com/post/137063593090/leo"&gt;best grimace&lt;/a&gt;.&lt;br/&gt;☆ Some elves cannot be shelved. &lt;b&gt;&lt;i&gt;&lt;a href="https://www.tumblr.com/search/The%20Shannara%20Chronicles"&gt;The Shannara Chronicles&lt;/a&gt;&lt;/i&gt;&lt;/b&gt; debuted at No. 16.&lt;/p&gt;&lt;/blockquote&gt;&lt;figure data-orig-width="500" data-orig-height="281" data-tumblr-attribution="darkvoidpack:I6_HYH3Z998B28zTSV1YSQ:ZnIIfj1slKySo" class="tmblr-full"&gt;&lt;img src="http://38.media.tumblr.com/05b727c83aace22632b2d8474b2d112b/tumblr_ntlpl06Z3f1uesvvmo1_500.gif" alt="image" data-orig-width="500" data-orig-height="281"/&gt;&lt;/figure&gt;&lt;h2&gt;&lt;a href="http://thefandometrics.tumblr.com/post/137117810802/movies-week-ending-january-11th-2016-star-wars"&gt;Movies&lt;/a&gt;: Don’t forget to put on your 3D glasses for the next two sentences. &lt;/h2&gt;&lt;blockquote&gt;&lt;p&gt;⬆ Was &lt;b&gt;&lt;i&gt;&lt;a href="https://www.tumblr.com/search/Mad%20Max"&gt;Mad Max&lt;/a&gt;&lt;/i&gt;&lt;/b&gt; snubbed at the awards? Your passionate cries lifted it to No. 10.&lt;br/&gt;☆ &lt;b&gt;&lt;i&gt;&lt;a href="https://www.tumblr.com/search/Ghostbusters"&gt;Ghostbusters&lt;/a&gt;&lt;/i&gt;&lt;/b&gt;, a revamp of the breathtaking documentary &lt;i&gt;Ghostbusters&lt;/i&gt;, returns at No. 14.&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;&lt;a href="http://thefandometrics.tumblr.com/post/137117244608/musical-acts-week-ending-january-11th-2016-5"&gt;Music&lt;/a&gt;: Musicians without music.&lt;/h2&gt;&lt;blockquote&gt;&lt;p&gt;⬆ &lt;b&gt;&lt;a href="https://www.tumblr.com/search/Beyonce"&gt;Beyonce&lt;/a&gt;&lt;/b&gt; moved up to No. 8 without making a peep on &lt;i&gt;Lip Sync Battle&lt;/i&gt;.&lt;br/&gt;⬆ &lt;b&gt;&lt;a href="https://www.tumblr.com/search/Lady%20Gaga"&gt;Lady Gaga&lt;/a&gt;&lt;/b&gt; won best actress and the equally impressive rank of No. 9.&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;&lt;a href="http://thefandometrics.tumblr.com/post/137116652699/celebrities-week-ending-january-11th-2016-adam"&gt;Celebrities&lt;/a&gt;: Please don’t let a rich person win the Powerball.&lt;/h2&gt;&lt;blockquote&gt;&lt;p&gt;☆ &lt;b&gt;&lt;a href="https://www.tumblr.com/search/Leonardo%20DiCaprio"&gt;Leonardo DiCaprio&lt;/a&gt;&lt;/b&gt; battled a bear, won a Golden Globe, landed at No. 17. What a night.&lt;br/&gt;☆ Teen Queen &lt;b&gt;&lt;a href="https://www.tumblr.com/search/Amandla%20Stenberg"&gt;Amandla Stenberg&lt;/a&gt;&lt;/b&gt; debuted at No 18 after a &lt;a href="http://tmblr.co/mJ7SklFRPVSBBt8ifjeqy2w"&gt;@teenvogue&lt;/a&gt; Snapchat takeover.&lt;br/&gt;☆ OTP alert. &lt;b&gt;&lt;a href="https://www.tumblr.com/search/Rami%20Malek"&gt;Rami Malek&lt;/a&gt;&lt;/b&gt; cabooses our list at No. 20 after Christian Slater sang his praises.&lt;/p&gt;&lt;/blockquote&gt;&lt;figure class="tmblr-full" data-orig-height="250" data-orig-width="500" data-tumblr-attribution="tvwatercooler:lbxG1rwZpwLHmhy8rIunCQ:Z7loFw1tgtiVe"&gt;&lt;img src="http://38.media.tumblr.com/5f5e8438f58442fb65cee706a1631608/tumblr_nu9i5x036O1r9x12go1_500.gif" data-orig-height="250" data-orig-width="500"/&gt;&lt;/figure&gt;&lt;h2&gt;&lt;a href="http://thefandometrics.tumblr.com/post/137116069969/video-games-week-ending-january-11th-2016"&gt;Games&lt;/a&gt;: Don’t Wake Daddy.&lt;/h2&gt;&lt;blockquote&gt;&lt;p&gt;⬆ LOL, &lt;b&gt;&lt;i&gt;&lt;a href="https://www.tumblr.com/search/League%20of%20Legends"&gt;League of Legends&lt;/a&gt;&lt;/i&gt;&lt;/b&gt;&lt;b&gt;&lt;/b&gt; moved up to No. 7.&lt;br/&gt;⬆ Neither Hell Nohr high water could keep &lt;b&gt;&lt;i&gt;&lt;a href="https://www.tumblr.com/search/Fire%20Emblem%20Fates"&gt;Fire Emblem Fates&lt;/a&gt; &lt;/i&gt;&lt;/b&gt;(No. 19) off our list.&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;&lt;a href="http://thefandometrics.tumblr.com/post/137115545596/web-stuff-week-ending-january-11th-2016"&gt;Web stuff&lt;/a&gt;: The internet is cool, good bye. &lt;/h2&gt;&lt;blockquote&gt;&lt;p&gt;⬆ &lt;b&gt;&lt;a href="https://www.tumblr.com/search/Jacksepticeye"&gt;Jacksepticeye&lt;/a&gt;&lt;/b&gt; (No. 4) reached 8 million subscribers. Is that even a real number?&lt;br/&gt;⬇︎ Animator, voice actor, rapper, human male &lt;b&gt;&lt;a href="https://www.tumblr.com/search/Arin%20Hanson"&gt;Arin Hanson&lt;/a&gt;&lt;/b&gt; falls to No. 16.&lt;/p&gt;&lt;/blockquote&gt;&lt;/blockquote&gt;</description><link>http://staff.tumblr.com/post/137250668505</link><guid>http://staff.tumblr.com/post/137250668505</guid><pubDate>Wed, 13 Jan 2016 19:39:47 -0500</pubDate><category>week in review</category></item><item><title>Photo</title><description>&lt;img src="http://49.media.tumblr.com/2edae122171f1a99abd1ce4dc40fe2d1/tumblr_o0aokm7F9s1v1xodyo1_500.gif"/&gt;&lt;br/&gt;&lt;br/&gt;</description><link>http://staff.tumblr.com/post/137191044499</link><guid>http://staff.tumblr.com/post/137191044499</guid><pubDate>Tue, 12 Jan 2016 21:00:05 -0500</pubDate><category>welcome to the slime zone</category><category>slime week</category></item><item><title>dollychops:
+
+ ‘Time may change me’
+
+
+ Thank you, David Bowie, for...</title><description>&lt;img src="http://36.media.tumblr.com/6d3af2caaf3a1e444d8f22489e54fcf7/tumblr_nhuzifiESR1qaetdco1_500.png"/&gt;&lt;br/&gt;&lt;br/&gt;&lt;p&gt;&lt;a class="tumblr_blog" href="http://dollychops.tumblr.com/post/107623338570"&gt;dollychops&lt;/a&gt;:&lt;/p&gt;
+ &lt;blockquote&gt;
+ &lt;p&gt;‘Time may change me’&lt;/p&gt;
+ &lt;/blockquote&gt;
+
+ &lt;p&gt;Thank you, David Bowie, for everything you left on this planet.&lt;/p&gt;</description><link>http://staff.tumblr.com/post/137108708620</link><guid>http://staff.tumblr.com/post/137108708620</guid><pubDate>Mon, 11 Jan 2016 16:08:34 -0500</pubDate><category>âš¡ï¸</category></item><item><title>Photo</title><description>&lt;img src="http://41.media.tumblr.com/967aba4d63cfa34fb00ed3edbc2ef27e/tumblr_nxky79R9Xz1sflxizo1_500.png"/&gt;&lt;br/&gt;&lt;br/&gt;</description><link>http://staff.tumblr.com/post/136905518945</link><guid>http://staff.tumblr.com/post/136905518945</guid><pubDate>Fri, 08 Jan 2016 17:44:31 -0500</pubDate><category>have a good weekend tumblr</category></item><item><title>New year, new trends, new youBlowing up now: Graphic novelist...</title><description>&lt;img src="http://41.media.tumblr.com/9c69a850eece63c29d913b6e83dd49a7/tumblr_o0ne9sk7yG1qz8q0ho1_500.png"/&gt;&lt;br/&gt;&lt;br/&gt;&lt;h2&gt;New year, new trends, new you&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;Blowing up now: Graphic novelist &lt;b&gt;&lt;a href="http://tumblr.com/search/amandla%20stenberg"&gt;Amandla Stenberg’s&lt;/a&gt;&lt;/b&gt; (alias: Rue) &lt;a href="http://amandla.tumblr.com/post/136866720678/so-i-took-over-the-teen-vogue-snapchat-today"&gt;inspiring message&lt;/a&gt;. &lt;/li&gt;&lt;li&gt;It took Sherlock Holmes to figure out the &lt;a href="http://tumblr.com/search/sherlock"&gt;&lt;b&gt;&lt;i&gt;Sherlock&lt;/i&gt;&lt;/b&gt;&lt;/a&gt; special.&lt;/li&gt;&lt;li&gt;&lt;a href="http://tumblr.com/search/teen%20wolf"&gt;&lt;b&gt;&lt;i&gt;Teen Wolf&lt;/i&gt;&lt;/b&gt;&lt;/a&gt; and friends chased Taylor Swift &lt;a href="http://tumblr.com/search/out%20of%20the%20woods"&gt;&lt;b&gt;&lt;i&gt;Out of the Woods&lt;/i&gt;&lt;/b&gt;&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href="http://tumblr.com/search/oregon"&gt;&lt;b&gt;Oregon&lt;/b&gt;&lt;/a&gt; is hosting everyone’s drunk uncles.&lt;/li&gt;&lt;li&gt;POTUS announced new &lt;a href="http://tumblr.com/search/gun%20control"&gt;&lt;b&gt;gun control&lt;/b&gt;&lt;/a&gt; policies.&lt;/li&gt;&lt;li&gt;&lt;a href="http://tumblr.com/search/ces%202016"&gt;&lt;b&gt;CES 2016&lt;/b&gt;&lt;/a&gt;: for people who don’t get invited to &lt;a href="http://tumblr.com/search/pca%202016"&gt;&lt;b&gt;PCA 2016&lt;/b&gt;&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;figure data-orig-width="500" data-orig-height="333" data-tumblr-attribution="qualcomm:w-2kl3XPK9VC_h4I30lZQQ:ZAN2in1-NvniN" class="tmblr-full"&gt;&lt;img src="http://33.media.tumblr.com/86405300020cd161baa702cf983c6189/tumblr_o0juioRm5j1ta1bw6o1_500.gif" alt="image" data-orig-width="500" data-orig-height="333"/&gt;&lt;/figure&gt;&lt;ul&gt;&lt;li&gt;&lt;a href="http://tumblr.com/search/pvris"&gt;&lt;b&gt;PVRIS&lt;/b&gt;&lt;/a&gt;: untrilled as in “Morris.â€&lt;/li&gt;&lt;li&gt;And it’s definitely &lt;a href="http://tumblr.com/search/jailey"&gt;&lt;b&gt;Jailey&lt;/b&gt;&lt;/a&gt;. Bieldwin slays orcs in Enedwaith.&lt;/li&gt;&lt;/ul&gt;&lt;h2&gt;Once-around-the-sun Tumblrs&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;b&gt;Daily Overview &lt;/b&gt;(&lt;a href="http://tmblr.co/mfB7_qm9Di9WXyvW2jkKcDA"&gt;@dailyoverview&lt;/a&gt;): Cool stuff from above ground.&lt;/li&gt;&lt;li&gt;&lt;b&gt;Earth Blingg &lt;/b&gt;(&lt;a href="http://tmblr.co/mMwXAkW_6d4ddLGw7VhfWaQ"&gt;@earthblingg&lt;/a&gt;): Cool stuff from underground&lt;/li&gt;&lt;/ul&gt;</description><link>http://staff.tumblr.com/post/136898076525</link><guid>http://staff.tumblr.com/post/136898076525</guid><pubDate>Fri, 08 Jan 2016 15:36:23 -0500</pubDate><category>trends</category></item></channel></rss>
diff --git a/mobile/android/tests/background/junit4/resources/feed_rss_wikipedia.xml b/mobile/android/tests/background/junit4/resources/feed_rss_wikipedia.xml
new file mode 100644
index 0000000000..a20082231d
--- /dev/null
+++ b/mobile/android/tests/background/junit4/resources/feed_rss_wikipedia.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ https://en.wikipedia.org/wiki/RSS#Example
+-->
+<rss version="2.0">
+ <channel>
+ <title>RSS Title</title>
+ <description>This is an example of an RSS feed</description>
+ <link>http://www.example.com/main.html</link>
+ <lastBuildDate>Mon, 06 Sep 2010 00:01:00 +0000 </lastBuildDate>
+ <pubDate>Sun, 06 Sep 2009 16:20:00 +0000</pubDate>
+ <ttl>1800</ttl>
+ <item>
+ <title>Example entry</title>
+ <description>Here is some text containing an interesting description.</description>
+ <link>http://www.example.com/blog/post/1</link>
+ <guid isPermaLink="true">7bd204c6-1655-4c27-aeee-53f933c5395f</guid>
+ <pubDate>Sun, 06 Sep 2009 16:20:00 +0000</pubDate>
+ </item>
+ </channel>
+</rss>
diff --git a/mobile/android/tests/background/junit4/resources/feed_rss_wordpress.xml b/mobile/android/tests/background/junit4/resources/feed_rss_wordpress.xml
new file mode 100644
index 0000000000..7981eb1734
--- /dev/null
+++ b/mobile/android/tests/background/junit4/resources/feed_rss_wordpress.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
+ xmlns:content="http://purl.org/rss/1.0/modules/content/"
+ xmlns:wfw="http://wellformedweb.org/CommentAPI/"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:atom="http://www.w3.org/2005/Atom"
+ xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
+ xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
+ xmlns:georss="http://www.georss.org/georss" xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#" xmlns:media="http://search.yahoo.com/mrss/"
+ >
+
+ <channel>
+ <title>justasimpletest2016</title>
+ <atom:link href="https://justasimpletest2016.wordpress.com/feed/" rel="self" type="application/rss+xml" />
+ <link>https://justasimpletest2016.wordpress.com</link>
+ <description></description>
+ <lastBuildDate>Fri, 26 Feb 2016 22:08:00 +0000</lastBuildDate>
+ <language>en</language>
+ <sy:updatePeriod>hourly</sy:updatePeriod>
+ <sy:updateFrequency>1</sy:updateFrequency>
+ <generator>http://wordpress.com/</generator>
+ <cloud domain='justasimpletest2016.wordpress.com' port='80' path='/?rsscloud=notify' registerProcedure='' protocol='http-post' />
+ <image>
+ <url>https://s2.wp.com/i/buttonw-com.png</url>
+ <title>justasimpletest2016</title>
+ <link>https://justasimpletest2016.wordpress.com</link>
+ </image>
+ <atom:link rel="search" type="application/opensearchdescription+xml" href="https://justasimpletest2016.wordpress.com/osd.xml" title="justasimpletest2016" />
+ <atom:link rel='hub' href='https://justasimpletest2016.wordpress.com/?pushpress=hub'/>
+ <item>
+ <title>Hello World!</title>
+ <link>https://justasimpletest2016.wordpress.com/2016/02/26/hello-world/</link>
+ <comments>https://justasimpletest2016.wordpress.com/2016/02/26/hello-world/#respond</comments>
+ <pubDate>Fri, 26 Feb 2016 22:07:46 +0000</pubDate>
+ <dc:creator><![CDATA[justasimpletest2016]]></dc:creator>
+ <category><![CDATA[Uncategorized]]></category>
+
+ <guid isPermaLink="false">http://justasimpletest2016.wordpress.com/?p=6</guid>
+ <description><![CDATA[What&#8217;s up?<img alt="" border="0" src="https://pixel.wp.com/b.gif?host=justasimpletest2016.wordpress.com&#038;blog=107552275&#038;post=6&#038;subd=justasimpletest2016&#038;ref=&#038;feed=1" width="1" height="1" />]]></description>
+ <content:encoded><![CDATA[<p>What&#8217;s up?</p><br /> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gocomments/justasimpletest2016.wordpress.com/6/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/comments/justasimpletest2016.wordpress.com/6/" /></a> <img alt="" border="0" src="https://pixel.wp.com/b.gif?host=justasimpletest2016.wordpress.com&#038;blog=107552275&#038;post=6&#038;subd=justasimpletest2016&#038;ref=&#038;feed=1" width="1" height="1" />]]></content:encoded>
+ <wfw:commentRss>https://justasimpletest2016.wordpress.com/2016/02/26/hello-world/feed/</wfw:commentRss>
+ <slash:comments>0</slash:comments>
+
+ <media:content url="https://2.gravatar.com/avatar/ba82024e6f43884f4ae0e379273b0743?s=96&#38;d=identicon&#38;r=G" medium="image">
+ <media:title type="html">justasimpletest2016</media:title>
+ </media:content>
+ </item>
+ <item>
+ <title>The second post</title>
+ <link>https://justasimpletest2016.wordpress.com/2016/02/26/the-second-post/</link>
+ <comments>https://justasimpletest2016.wordpress.com/2016/02/26/the-second-post/#respond</comments>
+ <pubDate>Fri, 26 Feb 2016 00:23:04 +0000</pubDate>
+ <dc:creator><![CDATA[justasimpletest2016]]></dc:creator>
+ <category><![CDATA[Uncategorized]]></category>
+
+ <guid isPermaLink="false">http://justasimpletest2016.wordpress.com/2016/02/26/the-second-post/</guid>
+ <description><![CDATA[Hello.<img alt="" border="0" src="https://pixel.wp.com/b.gif?host=justasimpletest2016.wordpress.com&#038;blog=107552275&#038;post=4&#038;subd=justasimpletest2016&#038;ref=&#038;feed=1" width="1" height="1" />]]></description>
+ <content:encoded><![CDATA[<p>Hello.</p><br /> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gocomments/justasimpletest2016.wordpress.com/4/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/comments/justasimpletest2016.wordpress.com/4/" /></a> <img alt="" border="0" src="https://pixel.wp.com/b.gif?host=justasimpletest2016.wordpress.com&#038;blog=107552275&#038;post=4&#038;subd=justasimpletest2016&#038;ref=&#038;feed=1" width="1" height="1" />]]></content:encoded>
+ <wfw:commentRss>https://justasimpletest2016.wordpress.com/2016/02/26/the-second-post/feed/</wfw:commentRss>
+ <slash:comments>0</slash:comments>
+
+ <media:content url="https://2.gravatar.com/avatar/ba82024e6f43884f4ae0e379273b0743?s=96&#38;d=identicon&#38;r=G" medium="image">
+ <media:title type="html">justasimpletest2016</media:title>
+ </media:content>
+ </item>
+ <item>
+ <title>This is just a test</title>
+ <link>https://justasimpletest2016.wordpress.com/2016/02/26/this-is-just-a-test/</link>
+ <comments>https://justasimpletest2016.wordpress.com/2016/02/26/this-is-just-a-test/#respond</comments>
+ <pubDate>Fri, 26 Feb 2016 00:22:58 +0000</pubDate>
+ <dc:creator><![CDATA[justasimpletest2016]]></dc:creator>
+ <category><![CDATA[Uncategorized]]></category>
+
+ <guid isPermaLink="false">http://justasimpletest2016.wordpress.com/?p=2</guid>
+ <description><![CDATA[Hello World. First blog post from WordPress.<img alt="" border="0" src="https://pixel.wp.com/b.gif?host=justasimpletest2016.wordpress.com&#038;blog=107552275&#038;post=2&#038;subd=justasimpletest2016&#038;ref=&#038;feed=1" width="1" height="1" />]]></description>
+ <content:encoded><![CDATA[<p>Hello World. First blog post from WordPress.</p><br /> <a rel="nofollow" href="http://feeds.wordpress.com/1.0/gocomments/justasimpletest2016.wordpress.com/2/"><img alt="" border="0" src="http://feeds.wordpress.com/1.0/comments/justasimpletest2016.wordpress.com/2/" /></a> <img alt="" border="0" src="https://pixel.wp.com/b.gif?host=justasimpletest2016.wordpress.com&#038;blog=107552275&#038;post=2&#038;subd=justasimpletest2016&#038;ref=&#038;feed=1" width="1" height="1" />]]></content:encoded>
+ <wfw:commentRss>https://justasimpletest2016.wordpress.com/2016/02/26/this-is-just-a-test/feed/</wfw:commentRss>
+ <slash:comments>0</slash:comments>
+
+ <media:content url="https://2.gravatar.com/avatar/ba82024e6f43884f4ae0e379273b0743?s=96&#38;d=identicon&#38;r=G" medium="image">
+ <media:title type="html">justasimpletest2016</media:title>
+ </media:content>
+ </item>
+ </channel>
+</rss>
diff --git a/mobile/android/tests/background/junit4/resources/robolectric.properties b/mobile/android/tests/background/junit4/resources/robolectric.properties
new file mode 100644
index 0000000000..a809da730f
--- /dev/null
+++ b/mobile/android/tests/background/junit4/resources/robolectric.properties
@@ -0,0 +1,3 @@
+sdk=21
+constants=org.mozilla.gecko.BuildConfig
+packageName=org.mozilla.gecko
diff --git a/mobile/android/tests/background/junit4/src/com/keepsafe/switchboard/TestSwitchboard.java b/mobile/android/tests/background/junit4/src/com/keepsafe/switchboard/TestSwitchboard.java
new file mode 100644
index 0000000000..5726f12db0
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/com/keepsafe/switchboard/TestSwitchboard.java
@@ -0,0 +1,142 @@
+package com.keepsafe.switchboard;
+
+import android.content.Context;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.Experiments;
+import org.mozilla.gecko.util.IOUtils;
+import org.robolectric.RuntimeEnvironment;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.Assert.*;
+
+@RunWith(TestRunner.class)
+public class TestSwitchboard {
+
+ /**
+ * Create a JSON response from a JSON file.
+ */
+ private String readFromFile(String fileName) throws IOException {
+ URL url = getClass().getResource("/" + fileName);
+ if (url == null) {
+ throw new FileNotFoundException(fileName);
+ }
+
+ InputStream inputStream = null;
+ ByteArrayOutputStream outputStream = null;
+
+ try {
+ inputStream = new BufferedInputStream(new FileInputStream(url.getPath()));
+ InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
+ BufferedReader bufferReader = new BufferedReader(inputStreamReader, 8192);
+ String line;
+ StringBuilder resultContent = new StringBuilder();
+ while ((line = bufferReader.readLine()) != null) {
+ resultContent.append(line);
+ }
+ bufferReader.close();
+
+ return resultContent.toString();
+
+ } finally {
+ IOUtils.safeStreamClose(inputStream);
+ }
+ }
+
+ @Before
+ public void setUp() throws IOException {
+ final Context c = RuntimeEnvironment.application;
+ Preferences.setDynamicConfigJson(c, readFromFile("experiments.json"));
+ }
+
+ @Test
+ public void testDeviceUuidFactory() {
+ final Context c = RuntimeEnvironment.application;
+ final DeviceUuidFactory df = new DeviceUuidFactory(c);
+ final UUID uuid = df.getDeviceUuid();
+ assertNotNull("UUID is not null", uuid);
+ assertEquals("DeviceUuidFactory always returns the same UUID", df.getDeviceUuid(), uuid);
+ }
+
+ @Test
+ public void testIsInExperiment() {
+ final Context c = RuntimeEnvironment.application;
+ assertTrue("active-experiment is active", SwitchBoard.isInExperiment(c, "active-experiment"));
+ assertFalse("inactive-experiment is inactive", SwitchBoard.isInExperiment(c, "inactive-experiment"));
+ }
+
+ @Test
+ public void testExperimentValues() throws JSONException {
+ final Context c = RuntimeEnvironment.application;
+ assertTrue("active-experiment has values", SwitchBoard.hasExperimentValues(c, "active-experiment"));
+ assertFalse("inactive-experiment doesn't have values", SwitchBoard.hasExperimentValues(c, "inactive-experiment"));
+
+ final JSONObject values = SwitchBoard.getExperimentValuesFromJson(c, "active-experiment");
+ assertNotNull("active-experiment values are not null", values);
+ assertTrue("\"foo\" extra value is true", values.getBoolean("foo"));
+ }
+
+ @Test
+ public void testGetActiveExperiments() {
+ final Context c = RuntimeEnvironment.application;
+ final List<String> experiments = SwitchBoard.getActiveExperiments(c);
+ assertNotNull("List of active experiments is not null", experiments);
+
+ assertTrue("List of active experiments contains active-experiment", experiments.contains("active-experiment"));
+ assertFalse("List of active experiments does not contain inactive-experiment", experiments.contains("inactive-experiment"));
+ }
+
+ @Test
+ public void testOverride() {
+ final Context c = RuntimeEnvironment.application;
+
+ Experiments.setOverride(c, "active-experiment", false);
+ assertFalse("active-experiment is not active because of override", SwitchBoard.isInExperiment(c, "active-experiment"));
+ assertFalse("List of active experiments does not contain active-experiment", SwitchBoard.getActiveExperiments(c).contains("active-experiment"));
+
+ Experiments.clearOverride(c, "active-experiment");
+ assertTrue("active-experiment is active after override is cleared", SwitchBoard.isInExperiment(c, "active-experiment"));
+ assertTrue("List of active experiments contains active-experiment again", SwitchBoard.getActiveExperiments(c).contains("active-experiment"));
+
+ Experiments.setOverride(c, "inactive-experiment", true);
+ assertTrue("inactive-experiment is active because of override", SwitchBoard.isInExperiment(c, "inactive-experiment"));
+ assertTrue("List of active experiments contains inactive-experiment", SwitchBoard.getActiveExperiments(c).contains("inactive-experiment"));
+
+ Experiments.clearOverride(c, "inactive-experiment");
+ assertFalse("inactive-experiment is inactive after override is cleared", SwitchBoard.isInExperiment(c, "inactive-experiment"));
+ assertFalse("List of active experiments does not contain inactive-experiment again", SwitchBoard.getActiveExperiments(c).contains("inactive-experiment"));
+ }
+
+ @Test
+ public void testMatching() {
+ final Context c = RuntimeEnvironment.application;
+ assertTrue("is-experiment is matching", SwitchBoard.isInExperiment(c, "is-matching"));
+ assertFalse("is-not-matching is not matching", SwitchBoard.isInExperiment(c, "is-not-matching"));
+ }
+
+ @Test
+ public void testNotExisting() {
+ final Context c = RuntimeEnvironment.application;
+ assertFalse("F0O does not exists", SwitchBoard.isInExperiment(c, "F0O"));
+ assertFalse("BaAaz does not exists", SwitchBoard.hasExperimentValues(c, "BaAaz"));
+ }
+
+
+
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestBackoff.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestBackoff.java
new file mode 100644
index 0000000000..8e8152e364
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestBackoff.java
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.net.test;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
+import ch.boye.httpclientandroidlib.message.BasicStatusLine;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.android.sync.test.helpers.MockGlobalSessionCallback;
+import org.mozilla.gecko.background.testhelpers.MockGlobalSession;
+import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.SyncConfiguration;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+@RunWith(TestRunner.class)
+public class TestBackoff {
+ private final String TEST_USERNAME = "johndoe";
+ private final String TEST_PASSWORD = "password";
+ private final String TEST_SYNC_KEY = "abcdeabcdeabcdeabcdeabcdea";
+ private final long TEST_BACKOFF_IN_SECONDS = 1201;
+
+ /**
+ * Test that interpretHTTPFailure calls requestBackoff if
+ * X-Weave-Backoff is present.
+ */
+ @Test
+ public void testBackoffCalledIfBackoffHeaderPresent() {
+ try {
+ final MockGlobalSessionCallback callback = new MockGlobalSessionCallback();
+ SyncConfiguration config = new SyncConfiguration(TEST_USERNAME, new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD), new MockSharedPreferences(), new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY));
+ final GlobalSession session = new MockGlobalSession(config, callback);
+
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
+ response.addHeader("X-Weave-Backoff", Long.toString(TEST_BACKOFF_IN_SECONDS)); // Backoff given in seconds.
+
+ session.interpretHTTPFailure(response); // This is synchronous...
+
+ assertEquals(false, callback.calledSuccess); // ... so we can test immediately.
+ assertEquals(false, callback.calledError);
+ assertEquals(false, callback.calledAborted);
+ assertEquals(true, callback.calledRequestBackoff);
+ assertEquals(TEST_BACKOFF_IN_SECONDS * 1000, callback.weaveBackoff); // Backoff returned in milliseconds.
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail("Got exception.");
+ }
+ }
+
+ /**
+ * Test that interpretHTTPFailure does not call requestBackoff if
+ * X-Weave-Backoff is not present.
+ */
+ @Test
+ public void testBackoffNotCalledIfBackoffHeaderNotPresent() {
+ try {
+ final MockGlobalSessionCallback callback = new MockGlobalSessionCallback();
+ SyncConfiguration config = new SyncConfiguration(TEST_USERNAME, new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD), new MockSharedPreferences(), new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY));
+ final GlobalSession session = new MockGlobalSession(config, callback);
+
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
+
+ session.interpretHTTPFailure(response); // This is synchronous...
+
+ assertEquals(false, callback.calledSuccess); // ... so we can test immediately.
+ assertEquals(false, callback.calledError);
+ assertEquals(false, callback.calledAborted);
+ assertEquals(false, callback.calledRequestBackoff);
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail("Got exception.");
+ }
+ }
+
+ /**
+ * Test that interpretHTTPFailure calls requestBackoff with the
+ * largest specified value if X-Weave-Backoff and Retry-After are
+ * present.
+ */
+ @Test
+ public void testBackoffCalledIfMultipleBackoffHeadersPresent() {
+ try {
+ final MockGlobalSessionCallback callback = new MockGlobalSessionCallback();
+ SyncConfiguration config = new SyncConfiguration(TEST_USERNAME, new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD), new MockSharedPreferences(), new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY));
+ final GlobalSession session = new MockGlobalSession(config, callback);
+
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
+ response.addHeader("Retry-After", Long.toString(TEST_BACKOFF_IN_SECONDS)); // Backoff given in seconds.
+ response.addHeader("X-Weave-Backoff", Long.toString(TEST_BACKOFF_IN_SECONDS + 1)); // If we now add a second header, the larger should be returned.
+
+ session.interpretHTTPFailure(response); // This is synchronous...
+
+ assertEquals(false, callback.calledSuccess); // ... so we can test immediately.
+ assertEquals(false, callback.calledError);
+ assertEquals(false, callback.calledAborted);
+ assertEquals(true, callback.calledRequestBackoff);
+ assertEquals((TEST_BACKOFF_IN_SECONDS + 1) * 1000, callback.weaveBackoff); // Backoff returned in milliseconds.
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail("Got exception.");
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestBrowserIDAuthHeaderProvider.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestBrowserIDAuthHeaderProvider.java
new file mode 100644
index 0000000000..6a14c6d29c
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestBrowserIDAuthHeaderProvider.java
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.net.test;
+
+import ch.boye.httpclientandroidlib.Header;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.net.BrowserIDAuthHeaderProvider;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(TestRunner.class)
+public class TestBrowserIDAuthHeaderProvider {
+ @Test
+ public void testHeader() {
+ Header header = new BrowserIDAuthHeaderProvider("assertion").getAuthHeader(null, null, null);
+
+ assertEquals("authorization", header.getName().toLowerCase());
+ assertEquals("BrowserID assertion", header.getValue());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestClientsEngineStage.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestClientsEngineStage.java
new file mode 100644
index 0000000000..920cafb359
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestClientsEngineStage.java
@@ -0,0 +1,806 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.net.test;
+
+import ch.boye.httpclientandroidlib.HttpStatus;
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
+import org.mozilla.android.sync.test.helpers.MockGlobalSessionCallback;
+import org.mozilla.android.sync.test.helpers.MockServer;
+import org.mozilla.android.sync.test.helpers.MockSyncClientsEngineStage;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.testhelpers.CommandHelpers;
+import org.mozilla.gecko.background.testhelpers.MockClientsDataDelegate;
+import org.mozilla.gecko.background.testhelpers.MockClientsDatabaseAccessor;
+import org.mozilla.gecko.background.testhelpers.MockGlobalSession;
+import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.CollectionKeys;
+import org.mozilla.gecko.sync.CommandProcessor.Command;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.SyncConfiguration;
+import org.mozilla.gecko.sync.SyncConfigurationException;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
+import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
+import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
+import org.simpleframework.http.Request;
+import org.simpleframework.http.Response;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.math.BigDecimal;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Some tests in this class run client/server multi-threaded code but JUnit assertions triggered
+ * from background threads do not fail the test. If you see unexplained connection-related test failures,
+ * an assertion on the server may have been thrown. Unfortunately, it is non-trivial to get the background
+ * threads to transfer failures back to the test thread so we leave the tests in this state for now.
+ *
+ * One reason the server might throw an assertion is if you have not installed the crypto policies. See
+ * https://wiki.mozilla.org/Mobile/Fennec/Android/Testing#JUnit4_tests for more information.
+ */
+@RunWith(TestRunner.class)
+public class TestClientsEngineStage extends MockSyncClientsEngineStage {
+ public final static String LOG_TAG = "TestClientsEngSta";
+
+ public TestClientsEngineStage() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, CryptoException, URISyntaxException {
+ super();
+ session = initializeSession();
+ }
+
+ // Static so we can set it during the constructor. This is so evil.
+ private static MockGlobalSessionCallback callback;
+ private static GlobalSession initializeSession() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, CryptoException, URISyntaxException {
+ callback = new MockGlobalSessionCallback();
+ SyncConfiguration config = new SyncConfiguration(USERNAME, new BasicAuthHeaderProvider(USERNAME, PASSWORD), new MockSharedPreferences());
+ config.syncKeyBundle = new KeyBundle(USERNAME, SYNC_KEY);
+ GlobalSession session = new MockClientsGlobalSession(config, callback);
+ session.config.setClusterURL(new URI(TEST_SERVER));
+ session.config.setCollectionKeys(CollectionKeys.generateCollectionKeys());
+ return session;
+ }
+
+ private static final int TEST_PORT = HTTPServerTestHelper.getTestPort();
+ private static final String TEST_SERVER = "http://localhost:" + TEST_PORT;
+
+ private static final String USERNAME = "john";
+ private static final String PASSWORD = "password";
+ private static final String SYNC_KEY = "abcdeabcdeabcdeabcdeabcdea";
+
+ private HTTPServerTestHelper data = new HTTPServerTestHelper();
+ private int numRecordsFromGetRequest = 0;
+
+ private ArrayList<ClientRecord> expectedClients = new ArrayList<ClientRecord>();
+ private ArrayList<ClientRecord> downloadedClients = new ArrayList<ClientRecord>();
+
+ // For test purposes.
+ private ClientRecord lastComputedLocalClientRecord;
+ private ClientRecord uploadedRecord;
+ private String uploadBodyTimestamp;
+ private long uploadHeaderTimestamp;
+ private MockServer currentUploadMockServer;
+ private MockServer currentDownloadMockServer;
+
+ private boolean stubUpload = false;
+
+ protected static WaitHelper testWaiter() {
+ return WaitHelper.getTestWaiter();
+ }
+
+ @Override
+ protected ClientRecord newLocalClientRecord(ClientsDataDelegate delegate) {
+ lastComputedLocalClientRecord = super.newLocalClientRecord(delegate);
+ return lastComputedLocalClientRecord;
+ }
+
+ @After
+ public void teardown() {
+ stubUpload = false;
+ getMockDataAccessor().resetVars();
+ }
+
+ @Override
+ public synchronized ClientsDatabaseAccessor getClientsDatabaseAccessor() {
+ if (db == null) {
+ db = new MockClientsDatabaseAccessor();
+ }
+ return db;
+ }
+
+ // For test use.
+ private MockClientsDatabaseAccessor getMockDataAccessor() {
+ return (MockClientsDatabaseAccessor) getClientsDatabaseAccessor();
+ }
+
+ private synchronized boolean mockDataAccessorIsClosed() {
+ if (db == null) {
+ return true;
+ }
+ return ((MockClientsDatabaseAccessor) db).closed;
+ }
+
+ @Override
+ protected ClientDownloadDelegate makeClientDownloadDelegate() {
+ return clientDownloadDelegate;
+ }
+
+ @Override
+ protected void downloadClientRecords() {
+ BaseResource.rewriteLocalhost = false;
+ data.startHTTPServer(currentDownloadMockServer);
+ super.downloadClientRecords();
+ }
+
+ @Override
+ protected void uploadClientRecord(CryptoRecord record) {
+ BaseResource.rewriteLocalhost = false;
+ if (stubUpload) {
+ session.advance();
+ return;
+ }
+ data.startHTTPServer(currentUploadMockServer);
+ super.uploadClientRecord(record);
+ }
+
+ @Override
+ protected void uploadClientRecords(JSONArray records) {
+ BaseResource.rewriteLocalhost = false;
+ if (stubUpload) {
+ return;
+ }
+ data.startHTTPServer(currentUploadMockServer);
+ super.uploadClientRecords(records);
+ }
+
+ public static class MockClientsGlobalSession extends MockGlobalSession {
+ private ClientsDataDelegate clientsDataDelegate = new MockClientsDataDelegate();
+
+ public MockClientsGlobalSession(SyncConfiguration config,
+ GlobalSessionCallback callback)
+ throws SyncConfigurationException,
+ IllegalArgumentException,
+ IOException,
+ NonObjectJSONException {
+ super(config, callback);
+ }
+
+ @Override
+ public ClientsDataDelegate getClientsDelegate() {
+ return clientsDataDelegate;
+ }
+ }
+
+ public class TestSuccessClientDownloadDelegate extends TestClientDownloadDelegate {
+ public TestSuccessClientDownloadDelegate(HTTPServerTestHelper data) {
+ super(data);
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ super.handleRequestFailure(response);
+ assertTrue(getMockDataAccessor().closed);
+ fail("Should not error.");
+ }
+
+ @Override
+ public void handleRequestError(Exception ex) {
+ super.handleRequestError(ex);
+ assertTrue(getMockDataAccessor().closed);
+ fail("Should not fail.");
+ }
+
+ @Override
+ public void handleWBO(CryptoRecord record) {
+ ClientRecord r;
+ try {
+ r = (ClientRecord) factory.createRecord(record.decrypt());
+ downloadedClients.add(r);
+ numRecordsFromGetRequest++;
+ } catch (Exception e) {
+ fail("handleWBO failed.");
+ }
+ }
+ }
+
+ public class TestHandleWBODownloadDelegate extends TestClientDownloadDelegate {
+ public TestHandleWBODownloadDelegate(HTTPServerTestHelper data) {
+ super(data);
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ super.handleRequestFailure(response);
+ assertTrue(getMockDataAccessor().closed);
+ fail("Should not error.");
+ }
+
+ @Override
+ public void handleRequestError(Exception ex) {
+ super.handleRequestError(ex);
+ assertTrue(getMockDataAccessor().closed);
+ ex.printStackTrace();
+ fail("Should not fail.");
+ }
+ }
+
+ public class MockSuccessClientUploadDelegate extends MockClientUploadDelegate {
+ public MockSuccessClientUploadDelegate(HTTPServerTestHelper data) {
+ super(data);
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse response) {
+ uploadHeaderTimestamp = response.normalizedWeaveTimestamp();
+ super.handleRequestSuccess(response);
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ super.handleRequestFailure(response);
+ fail("Should not fail.");
+ }
+
+ @Override
+ public void handleRequestError(Exception ex) {
+ super.handleRequestError(ex);
+ ex.printStackTrace();
+ fail("Should not error.");
+ }
+ }
+
+ public class MockFailureClientUploadDelegate extends MockClientUploadDelegate {
+ public MockFailureClientUploadDelegate(HTTPServerTestHelper data) {
+ super(data);
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse response) {
+ super.handleRequestSuccess(response);
+ fail("Should not succeed.");
+ }
+
+ @Override
+ public void handleRequestError(Exception ex) {
+ super.handleRequestError(ex);
+ fail("Should not fail.");
+ }
+ }
+
+ public class UploadMockServer extends MockServer {
+ @SuppressWarnings("unchecked")
+ private String postBodyForRecord(ClientRecord cr) {
+ final long now = cr.lastModified;
+ final BigDecimal modified = Utils.millisecondsToDecimalSeconds(now);
+
+ Logger.debug(LOG_TAG, "Now is " + now + " (" + modified + ")");
+ final JSONArray idArray = new JSONArray();
+ idArray.add(cr.guid);
+
+ final JSONObject result = new JSONObject();
+ result.put("modified", modified);
+ result.put("success", idArray);
+ result.put("failed", new JSONObject());
+
+ uploadBodyTimestamp = modified.toString();
+ return result.toJSONString();
+ }
+
+ private String putBodyForRecord(ClientRecord cr) {
+ final String modified = Utils.millisecondsToDecimalSecondsString(cr.lastModified);
+ uploadBodyTimestamp = modified;
+ return modified;
+ }
+
+ protected void handleUploadPUT(Request request, Response response) throws Exception {
+ Logger.debug(LOG_TAG, "Handling PUT: " + request.getPath());
+
+ // Save uploadedRecord to test against.
+ CryptoRecord cryptoRecord = CryptoRecord.fromJSONRecord(request.getContent());
+ cryptoRecord.keyBundle = session.keyBundleForCollection(COLLECTION_NAME);
+ uploadedRecord = (ClientRecord) factory.createRecord(cryptoRecord.decrypt());
+
+ // Note: collection is not saved in CryptoRecord.toJSONObject() upon upload.
+ // So its value is null and is set here so ClientRecord.equals() may be used.
+ uploadedRecord.collection = lastComputedLocalClientRecord.collection;
+
+ // Create response body containing current timestamp.
+ long now = System.currentTimeMillis();
+ PrintStream bodyStream = this.handleBasicHeaders(request, response, 200, "application/json", now);
+ uploadedRecord.lastModified = now;
+
+ bodyStream.println(putBodyForRecord(uploadedRecord));
+ bodyStream.close();
+ }
+
+ protected void handleUploadPOST(Request request, Response response) throws Exception {
+ Logger.debug(LOG_TAG, "Handling POST: " + request.getPath());
+ String content = request.getContent();
+ Logger.debug(LOG_TAG, "Content is " + content);
+ JSONArray array = ExtendedJSONObject.parseJSONArray(content);
+
+ Logger.debug(LOG_TAG, "Content is " + array);
+
+ KeyBundle keyBundle = session.keyBundleForCollection(COLLECTION_NAME);
+ if (array.size() != 1) {
+ Logger.debug(LOG_TAG, "Expecting only one record! Fail!");
+ PrintStream bodyStream = this.handleBasicHeaders(request, response, 400, "text/plain");
+ bodyStream.println("Expecting only one record! Fail!");
+ bodyStream.close();
+ return;
+ }
+
+ CryptoRecord r = CryptoRecord.fromJSONRecord(new ExtendedJSONObject((JSONObject) array.get(0)));
+ r.keyBundle = keyBundle;
+ ClientRecord cr = (ClientRecord) factory.createRecord(r.decrypt());
+ cr.collection = lastComputedLocalClientRecord.collection;
+ uploadedRecord = cr;
+
+ Logger.debug(LOG_TAG, "Record is " + cr);
+ long now = System.currentTimeMillis();
+ PrintStream bodyStream = this.handleBasicHeaders(request, response, 200, "application/json", now);
+ cr.lastModified = now;
+ final String responseBody = postBodyForRecord(cr);
+ Logger.debug(LOG_TAG, "Response is " + responseBody);
+ bodyStream.println(responseBody);
+ bodyStream.close();
+ }
+
+ @Override
+ public void handle(Request request, Response response) {
+ try {
+ String method = request.getMethod();
+ Logger.debug(LOG_TAG, "Handling " + method);
+ if (method.equalsIgnoreCase("post")) {
+ handleUploadPOST(request, response);
+ } else if (method.equalsIgnoreCase("put")) {
+ handleUploadPUT(request, response);
+ } else {
+ PrintStream bodyStream = this.handleBasicHeaders(request, response, 404, "text/plain");
+ bodyStream.close();
+ }
+ } catch (Exception e) {
+ fail("Error handling uploaded client record in UploadMockServer.");
+ }
+ }
+ }
+
+ public class DownloadMockServer extends MockServer {
+ @Override
+ public void handle(Request request, Response response) {
+ try {
+ PrintStream bodyStream = this.handleBasicHeaders(request, response, 200, "application/newlines");
+ for (int i = 0; i < 5; i++) {
+ ClientRecord record = new ClientRecord();
+ if (i != 2) { // So we test null version.
+ record.version = Integer.toString(28 + i);
+ }
+ expectedClients.add(record);
+ CryptoRecord cryptoRecord = cryptoFromClient(record);
+ bodyStream.print(cryptoRecord.toJSONString() + "\n");
+ }
+ bodyStream.close();
+ } catch (IOException e) {
+ fail("Error handling downloaded client records in DownloadMockServer.");
+ }
+ }
+ }
+
+ public class DownloadLocalRecordMockServer extends MockServer {
+ @SuppressWarnings("unchecked")
+ @Override
+ public void handle(Request request, Response response) {
+ try {
+ PrintStream bodyStream = this.handleBasicHeaders(request, response, 200, "application/newlines");
+ ClientRecord record = new ClientRecord(session.getClientsDelegate().getAccountGUID());
+
+ // Timestamp on server is 10 seconds after local timestamp
+ // (would trigger 412 if upload was attempted).
+ CryptoRecord cryptoRecord = cryptoFromClient(record);
+ JSONObject object = cryptoRecord.toJSONObject();
+ final long modified = (setRecentClientRecordTimestamp() + 10000) / 1000;
+ Logger.debug(LOG_TAG, "Setting modified to " + modified);
+ object.put("modified", modified);
+ bodyStream.print(object.toJSONString() + "\n");
+ bodyStream.close();
+ } catch (IOException e) {
+ fail("Error handling downloaded client records in DownloadLocalRecordMockServer.");
+ }
+ }
+ }
+
+ private CryptoRecord cryptoFromClient(ClientRecord record) {
+ CryptoRecord cryptoRecord = record.getEnvelope();
+ cryptoRecord.keyBundle = clientDownloadDelegate.keyBundle();
+ try {
+ cryptoRecord.encrypt();
+ } catch (Exception e) {
+ fail("Cannot encrypt client record.");
+ }
+ return cryptoRecord;
+ }
+
+ private long setRecentClientRecordTimestamp() {
+ long timestamp = System.currentTimeMillis() - (CLIENTS_TTL_REFRESH - 1000);
+ session.config.persistServerClientRecordTimestamp(timestamp);
+ return timestamp;
+ }
+
+ private void performFailingUpload() {
+ // performNotify() occurs in MockGlobalSessionCallback.
+ testWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ clientUploadDelegate = new MockFailureClientUploadDelegate(data);
+ checkAndUpload();
+ }
+ });
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testShouldUploadNoCommandsToProcess() throws NullCursorException {
+ // shouldUpload() returns true.
+ assertEquals(0, session.config.getPersistedServerClientRecordTimestamp());
+ assertFalse(shouldUploadLocalRecord);
+ assertTrue(shouldUpload());
+
+ // Set the timestamp to be a little earlier than refresh time,
+ // so shouldUpload() returns false.
+ setRecentClientRecordTimestamp();
+ assertFalse(0 == session.config.getPersistedServerClientRecordTimestamp());
+ assertFalse(shouldUploadLocalRecord);
+ assertFalse(shouldUpload());
+
+ // Now simulate observing a client record with the incorrect version.
+
+ ClientRecord outdatedRecord = new ClientRecord("dontmatter12", "clients", System.currentTimeMillis(), false);
+
+ outdatedRecord.version = getLocalClientVersion();
+ outdatedRecord.protocols = getLocalClientProtocols();
+ handleDownloadedLocalRecord(outdatedRecord);
+
+ assertEquals(outdatedRecord.lastModified, session.config.getPersistedServerClientRecordTimestamp());
+ assertFalse(shouldUploadLocalRecord);
+ assertFalse(shouldUpload());
+
+ outdatedRecord.version = outdatedRecord.version + "a1";
+ handleDownloadedLocalRecord(outdatedRecord);
+
+ // Now we think we need to upload because the version is outdated.
+ assertTrue(shouldUploadLocalRecord);
+ assertTrue(shouldUpload());
+
+ shouldUploadLocalRecord = false;
+ assertFalse(shouldUpload());
+
+ // If the protocol list is missing or wrong, we should reupload.
+ outdatedRecord.protocols = new JSONArray();
+ handleDownloadedLocalRecord(outdatedRecord);
+ assertTrue(shouldUpload());
+
+ shouldUploadLocalRecord = false;
+ assertFalse(shouldUpload());
+
+ outdatedRecord.protocols.add("1.0");
+ handleDownloadedLocalRecord(outdatedRecord);
+ assertTrue(shouldUpload());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testShouldUploadProcessCommands() throws NullCursorException {
+ // shouldUpload() returns false since array is size 0 and
+ // it has not been long enough yet to require an upload.
+ processCommands(new JSONArray());
+ setRecentClientRecordTimestamp();
+ assertFalse(shouldUploadLocalRecord);
+ assertFalse(shouldUpload());
+
+ // shouldUpload() returns true since array is size 1 even though
+ // it has not been long enough yet to require an upload.
+ JSONArray commands = new JSONArray();
+ commands.add(new JSONObject());
+ processCommands(commands);
+ setRecentClientRecordTimestamp();
+ assertEquals(1, commands.size());
+ assertTrue(shouldUploadLocalRecord);
+ assertTrue(shouldUpload());
+ }
+
+ @Test
+ public void testWipeAndStoreShouldNotWipe() {
+ assertFalse(shouldWipe);
+ wipeAndStore(new ClientRecord());
+ assertFalse(shouldWipe);
+ assertFalse(getMockDataAccessor().clientsTableWiped);
+ assertTrue(getMockDataAccessor().storedRecord);
+ }
+
+ @Test
+ public void testWipeAndStoreShouldWipe() {
+ assertFalse(shouldWipe);
+ shouldWipe = true;
+ wipeAndStore(new ClientRecord());
+ assertFalse(shouldWipe);
+ assertTrue(getMockDataAccessor().clientsTableWiped);
+ assertTrue(getMockDataAccessor().storedRecord);
+ }
+
+ @Test
+ public void testDownloadClientRecord() {
+ // Make sure no upload occurs after a download so we can
+ // test download in isolation.
+ stubUpload = true;
+
+ currentDownloadMockServer = new DownloadMockServer();
+ // performNotify() occurs in MockGlobalSessionCallback.
+ testWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ clientDownloadDelegate = new TestSuccessClientDownloadDelegate(data);
+ downloadClientRecords();
+ }
+ });
+
+ assertEquals(expectedClients.size(), numRecordsFromGetRequest);
+ for (int i = 0; i < downloadedClients.size(); i++) {
+ final ClientRecord downloaded = downloadedClients.get(i);
+ final ClientRecord expected = expectedClients.get(i);
+ assertTrue(expected.guid.equals(downloaded.guid));
+ assertEquals(expected.version, downloaded.version);
+ }
+ assertTrue(mockDataAccessorIsClosed());
+ }
+
+ @Test
+ public void testCheckAndUploadClientRecord() {
+ uploadAttemptsCount.set(MAX_UPLOAD_FAILURE_COUNT);
+ assertFalse(shouldUploadLocalRecord);
+ assertEquals(0, session.config.getPersistedServerClientRecordTimestamp());
+ currentUploadMockServer = new UploadMockServer();
+ // performNotify() occurs in MockGlobalSessionCallback.
+ testWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ clientUploadDelegate = new MockSuccessClientUploadDelegate(data);
+ checkAndUpload();
+ }
+ });
+
+ // Test ClientUploadDelegate.handleRequestSuccess().
+ Logger.debug(LOG_TAG, "Last computed local client record: " + lastComputedLocalClientRecord.guid);
+ Logger.debug(LOG_TAG, "Uploaded client record: " + uploadedRecord.guid);
+ assertTrue(lastComputedLocalClientRecord.equalPayloads(uploadedRecord));
+ assertEquals(0, uploadAttemptsCount.get());
+ assertTrue(callback.calledSuccess);
+
+ assertFalse(0 == session.config.getPersistedServerClientRecordTimestamp());
+
+ // Body and header are the same.
+ assertEquals(Utils.decimalSecondsToMilliseconds(uploadBodyTimestamp),
+ session.config.getPersistedServerClientsTimestamp());
+ assertEquals(uploadedRecord.lastModified,
+ session.config.getPersistedServerClientRecordTimestamp());
+ assertEquals(uploadHeaderTimestamp, session.config.getPersistedServerClientsTimestamp());
+ }
+
+ @Test // client/server multi-threaded
+ public void testDownloadHasOurRecord() {
+ // Make sure no upload occurs after a download so we can
+ // test download in isolation.
+ stubUpload = true;
+
+ // We've uploaded our local record recently.
+ long initialTimestamp = setRecentClientRecordTimestamp();
+
+ currentDownloadMockServer = new DownloadLocalRecordMockServer();
+ // performNotify() occurs in MockGlobalSessionCallback.
+ testWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ clientDownloadDelegate = new TestHandleWBODownloadDelegate(data);
+ downloadClientRecords();
+ }
+ });
+
+ // Timestamp got updated (but not reset) since we downloaded our record
+ assertFalse(0 == session.config.getPersistedServerClientRecordTimestamp());
+ assertTrue(initialTimestamp < session.config.getPersistedServerClientRecordTimestamp());
+ assertTrue(mockDataAccessorIsClosed());
+ }
+
+ @Test
+ public void testResetTimestampOnDownload() {
+ // Make sure no upload occurs after a download so we can
+ // test download in isolation.
+ stubUpload = true;
+
+ currentDownloadMockServer = new DownloadMockServer();
+ // performNotify() occurs in MockGlobalSessionCallback.
+ testWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ clientDownloadDelegate = new TestHandleWBODownloadDelegate(data);
+ downloadClientRecords();
+ }
+ });
+
+ // Timestamp got reset since our record wasn't downloaded.
+ assertEquals(0, session.config.getPersistedServerClientRecordTimestamp());
+ assertTrue(mockDataAccessorIsClosed());
+ }
+
+ /**
+ * The following 8 tests are for ClientUploadDelegate.handleRequestFailure().
+ * for the varying values of uploadAttemptsCount, shouldUploadLocalRecord,
+ * and the type of server error.
+ *
+ * The first 4 are for 412 Precondition Failures.
+ * The second 4 represent the functionality given any other type of variable.
+ */
+ @Test
+ public void testHandle412UploadFailureLowCount() {
+ assertFalse(shouldUploadLocalRecord);
+ currentUploadMockServer = new MockServer(HttpStatus.SC_PRECONDITION_FAILED, null);
+ assertEquals(0, uploadAttemptsCount.get());
+ performFailingUpload();
+ assertEquals(0, uploadAttemptsCount.get());
+ assertTrue(callback.calledError);
+ }
+
+ @Test
+ public void testHandle412UploadFailureHighCount() {
+ assertFalse(shouldUploadLocalRecord);
+ currentUploadMockServer = new MockServer(HttpStatus.SC_PRECONDITION_FAILED, null);
+ uploadAttemptsCount.set(MAX_UPLOAD_FAILURE_COUNT);
+ performFailingUpload();
+ assertEquals(MAX_UPLOAD_FAILURE_COUNT, uploadAttemptsCount.get());
+ assertTrue(callback.calledError);
+ }
+
+ @Test
+ public void testHandle412UploadFailureLowCountWithCommand() {
+ shouldUploadLocalRecord = true;
+ currentUploadMockServer = new MockServer(HttpStatus.SC_PRECONDITION_FAILED, null);
+ assertEquals(0, uploadAttemptsCount.get());
+ performFailingUpload();
+ assertEquals(0, uploadAttemptsCount.get());
+ assertTrue(callback.calledError);
+ }
+
+ @Test
+ public void testHandle412UploadFailureHighCountWithCommand() {
+ shouldUploadLocalRecord = true;
+ currentUploadMockServer = new MockServer(HttpStatus.SC_PRECONDITION_FAILED, null);
+ uploadAttemptsCount.set(MAX_UPLOAD_FAILURE_COUNT);
+ performFailingUpload();
+ assertEquals(MAX_UPLOAD_FAILURE_COUNT, uploadAttemptsCount.get());
+ assertTrue(callback.calledError);
+ }
+
+ @Test
+ public void testHandleMiscUploadFailureLowCount() {
+ currentUploadMockServer = new MockServer(HttpStatus.SC_BAD_REQUEST, null);
+ assertFalse(shouldUploadLocalRecord);
+ assertEquals(0, uploadAttemptsCount.get());
+ performFailingUpload();
+ assertEquals(0, uploadAttemptsCount.get());
+ assertTrue(callback.calledError);
+ }
+
+ @Test
+ public void testHandleMiscUploadFailureHighCount() {
+ currentUploadMockServer = new MockServer(HttpStatus.SC_BAD_REQUEST, null);
+ assertFalse(shouldUploadLocalRecord);
+ uploadAttemptsCount.set(MAX_UPLOAD_FAILURE_COUNT);
+ performFailingUpload();
+ assertEquals(MAX_UPLOAD_FAILURE_COUNT, uploadAttemptsCount.get());
+ assertTrue(callback.calledError);
+ }
+
+ @Test
+ public void testHandleMiscUploadFailureHighCountWithCommands() {
+ currentUploadMockServer = new MockServer(HttpStatus.SC_BAD_REQUEST, null);
+ shouldUploadLocalRecord = true;
+ uploadAttemptsCount.set(MAX_UPLOAD_FAILURE_COUNT);
+ performFailingUpload();
+ assertEquals(MAX_UPLOAD_FAILURE_COUNT + 1, uploadAttemptsCount.get());
+ assertTrue(callback.calledError);
+ }
+
+ @Test
+ public void testHandleMiscUploadFailureMaxAttempts() {
+ currentUploadMockServer = new MockServer(HttpStatus.SC_BAD_REQUEST, null);
+ shouldUploadLocalRecord = true;
+ assertEquals(0, uploadAttemptsCount.get());
+ performFailingUpload();
+ assertEquals(MAX_UPLOAD_FAILURE_COUNT + 1, uploadAttemptsCount.get());
+ assertTrue(callback.calledError);
+ }
+
+ class TestAddCommandsMockClientsDatabaseAccessor extends MockClientsDatabaseAccessor {
+ @Override
+ public List<Command> fetchCommandsForClient(String accountGUID) throws NullCursorException {
+ List<Command> commands = new ArrayList<Command>();
+ commands.add(CommandHelpers.getCommand1());
+ commands.add(CommandHelpers.getCommand2());
+ commands.add(CommandHelpers.getCommand3());
+ commands.add(CommandHelpers.getCommand4());
+ return commands;
+ }
+ }
+
+ @Test
+ public void testAddCommandsToUnversionedClient() throws NullCursorException {
+ db = new TestAddCommandsMockClientsDatabaseAccessor();
+
+ final ClientRecord remoteRecord = new ClientRecord();
+ remoteRecord.version = null;
+ final String expectedGUID = remoteRecord.guid;
+
+ this.addCommands(remoteRecord);
+ assertEquals(1, modifiedClientsToUpload.size());
+
+ final ClientRecord recordToUpload = modifiedClientsToUpload.get(0);
+ assertEquals(4, recordToUpload.commands.size());
+ assertEquals(expectedGUID, recordToUpload.guid);
+ assertEquals(null, recordToUpload.version);
+ }
+
+ @Test
+ public void testAddCommandsToVersionedClient() throws NullCursorException {
+ db = new TestAddCommandsMockClientsDatabaseAccessor();
+
+ final ClientRecord remoteRecord = new ClientRecord();
+ remoteRecord.version = "12a1";
+ final String expectedGUID = remoteRecord.guid;
+
+ this.addCommands(remoteRecord);
+ assertEquals(1, modifiedClientsToUpload.size());
+
+ final ClientRecord recordToUpload = modifiedClientsToUpload.get(0);
+ assertEquals(4, recordToUpload.commands.size());
+ assertEquals(expectedGUID, recordToUpload.guid);
+ assertEquals("12a1", recordToUpload.version);
+ }
+
+ @Test
+ public void testLastModifiedTimestamp() throws NullCursorException {
+ // If we uploaded a record a moment ago, we shouldn't upload another.
+ final long now = System.currentTimeMillis() - 1;
+ session.config.persistServerClientRecordTimestamp(now);
+ assertEquals(now, session.config.getPersistedServerClientRecordTimestamp());
+ assertFalse(shouldUploadLocalRecord);
+ assertFalse(shouldUpload());
+
+ // But if we change our client data, we should upload.
+ session.getClientsDelegate().setClientName("new name", System.currentTimeMillis());
+ assertTrue(shouldUpload());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestCredentialsEndToEnd.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestCredentialsEndToEnd.java
new file mode 100644
index 0000000000..0f568a81ec
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestCredentialsEndToEnd.java
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.net.test;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+import ch.boye.httpclientandroidlib.Header;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Test the transfer of a UTF-8 string from desktop, and ensure that it results in the
+ * correct hashed Basic Auth header.
+ */
+@RunWith(TestRunner.class)
+public class TestCredentialsEndToEnd {
+
+ public static final String REAL_PASSWORD = "pïgéons1";
+ public static final String USERNAME = "utvm3mk6hnngiir2sp4jsxf2uvoycrv6";
+ public static final String DESKTOP_PASSWORD_JSON = "{\"password\":\"pïgéons1\"}";
+ public static final String BTOA_PASSWORD = "cMOvZ8Opb25zMQ==";
+ public static final int DESKTOP_ASSERTED_SIZE = 10;
+ public static final String DESKTOP_BASIC_AUTH = "Basic dXR2bTNtazZobm5naWlyMnNwNGpzeGYydXZveWNydjY6cMOvZ8Opb25zMQ==";
+
+ private String getCreds(String password) {
+ Header authenticate = new BasicAuthHeaderProvider(USERNAME, password).getAuthHeader(null, null, null);
+ return authenticate.getValue();
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testUTF8() throws UnsupportedEncodingException {
+ final String in = "pïgéons1";
+ final String out = "pïgéons1";
+ assertEquals(out, Utils.decodeUTF8(in));
+ }
+
+ @Test
+ public void testAuthHeaderFromPassword() throws NonObjectJSONException, IOException {
+ final ExtendedJSONObject parsed = new ExtendedJSONObject(DESKTOP_PASSWORD_JSON);
+
+ final String password = parsed.getString("password");
+ final String decoded = Utils.decodeUTF8(password);
+
+ final byte[] expectedBytes = Utils.decodeBase64(BTOA_PASSWORD);
+ final String expected = new String(expectedBytes, "UTF-8");
+
+ assertEquals(DESKTOP_ASSERTED_SIZE, password.length());
+ assertEquals(expected, decoded);
+
+ System.out.println("Retrieved password: " + password);
+ System.out.println("Expected password: " + expected);
+ System.out.println("Rescued password: " + decoded);
+
+ assertEquals(getCreds(expected), getCreds(decoded));
+ assertEquals(getCreds(decoded), DESKTOP_BASIC_AUTH);
+ }
+
+ // Note that we do *not* have a test for the J-PAKE setup process
+ // (SetupSyncActivity) that actually stores credentials and requires
+ // decodeUTF8. This will have to suffice.
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestGlobalSession.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestGlobalSession.java
new file mode 100644
index 0000000000..c00da9b26e
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestGlobalSession.java
@@ -0,0 +1,436 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.net.test;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
+import ch.boye.httpclientandroidlib.message.BasicStatusLine;
+import junit.framework.AssertionFailedError;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
+import org.mozilla.android.sync.test.helpers.MockGlobalSessionCallback;
+import org.mozilla.android.sync.test.helpers.MockResourceDelegate;
+import org.mozilla.android.sync.test.helpers.MockServer;
+import org.mozilla.gecko.background.testhelpers.MockAbstractNonRepositorySyncStage;
+import org.mozilla.gecko.background.testhelpers.MockGlobalSession;
+import org.mozilla.gecko.background.testhelpers.MockPrefsGlobalSession;
+import org.mozilla.gecko.background.testhelpers.MockServerSyncStage;
+import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.EngineSettings;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.MetaGlobal;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.SyncConfiguration;
+import org.mozilla.gecko.sync.SyncConfigurationException;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.sync.repositories.domain.VersionConstants;
+import org.mozilla.gecko.sync.stage.AndroidBrowserBookmarksServerSyncStage;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
+import org.mozilla.gecko.sync.stage.NoSuchStageException;
+import org.simpleframework.http.Request;
+import org.simpleframework.http.Response;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+@RunWith(TestRunner.class)
+public class TestGlobalSession {
+ private int TEST_PORT = HTTPServerTestHelper.getTestPort();
+ private final String TEST_CLUSTER_URL = "http://localhost:" + TEST_PORT;
+ private final String TEST_USERNAME = "johndoe";
+ private final String TEST_PASSWORD = "password";
+ private final String TEST_SYNC_KEY = "abcdeabcdeabcdeabcdeabcdea";
+ private final long TEST_BACKOFF_IN_SECONDS = 2401;
+
+ public static WaitHelper getTestWaiter() {
+ return WaitHelper.getTestWaiter();
+ }
+
+ @Test
+ public void testGetSyncStagesBy() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, CryptoException, NoSuchStageException {
+
+ final MockGlobalSessionCallback callback = new MockGlobalSessionCallback();
+ GlobalSession s = MockPrefsGlobalSession.getSession(TEST_USERNAME, TEST_PASSWORD,
+ new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY),
+ callback, /* context */ null, null);
+
+ assertTrue(s.getSyncStageByName(Stage.syncBookmarks) instanceof AndroidBrowserBookmarksServerSyncStage);
+
+ final Set<String> empty = new HashSet<String>();
+
+ final Set<String> bookmarksAndTabsNames = new HashSet<String>();
+ bookmarksAndTabsNames.add("bookmarks");
+ bookmarksAndTabsNames.add("tabs");
+
+ final Set<GlobalSyncStage> bookmarksAndTabsSyncStages = new HashSet<GlobalSyncStage>();
+ GlobalSyncStage bookmarksStage = s.getSyncStageByName("bookmarks");
+ GlobalSyncStage tabsStage = s.getSyncStageByName(Stage.syncTabs);
+ bookmarksAndTabsSyncStages.add(bookmarksStage);
+ bookmarksAndTabsSyncStages.add(tabsStage);
+
+ final Set<Stage> bookmarksAndTabsEnums = new HashSet<Stage>();
+ bookmarksAndTabsEnums.add(Stage.syncBookmarks);
+ bookmarksAndTabsEnums.add(Stage.syncTabs);
+
+ assertTrue(s.getSyncStagesByName(empty).isEmpty());
+ assertEquals(bookmarksAndTabsSyncStages, new HashSet<GlobalSyncStage>(s.getSyncStagesByName(bookmarksAndTabsNames)));
+ assertEquals(bookmarksAndTabsSyncStages, new HashSet<GlobalSyncStage>(s.getSyncStagesByEnum(bookmarksAndTabsEnums)));
+ }
+
+ /**
+ * Test that handleHTTPError does in fact backoff.
+ */
+ @Test
+ public void testBackoffCalledByHandleHTTPError() {
+ try {
+ final MockGlobalSessionCallback callback = new MockGlobalSessionCallback();
+ SyncConfiguration config = new SyncConfiguration(TEST_USERNAME, new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD), new MockSharedPreferences(), new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY));
+ final GlobalSession session = new MockGlobalSession(config, callback);
+
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 503, "Illegal method/protocol"));
+ response.setHeader("X-Weave-Backoff", Long.toString(TEST_BACKOFF_IN_SECONDS)); // Backoff given in seconds.
+
+ getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
+ @Override
+ public void run() {
+ session.handleHTTPError(new SyncStorageResponse(response), "Illegal method/protocol");
+ }
+ }));
+
+ assertEquals(false, callback.calledSuccess);
+ assertEquals(true, callback.calledError);
+ assertEquals(false, callback.calledAborted);
+ assertEquals(true, callback.calledRequestBackoff);
+ assertEquals(TEST_BACKOFF_IN_SECONDS * 1000, callback.weaveBackoff); // Backoff returned in milliseconds.
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail("Got exception.");
+ }
+ }
+
+ /**
+ * Test that a trivially successful GlobalSession does not fail or backoff.
+ */
+ @Test
+ public void testSuccessCalledAfterStages() {
+ try {
+ final MockGlobalSessionCallback callback = new MockGlobalSessionCallback();
+ SyncConfiguration config = new SyncConfiguration(TEST_USERNAME, new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD), new MockSharedPreferences(), new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY));
+ final GlobalSession session = new MockGlobalSession(config, callback);
+
+ getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ session.start();
+ } catch (Exception e) {
+ final AssertionFailedError error = new AssertionFailedError();
+ error.initCause(e);
+ getTestWaiter().performNotify(error);
+ }
+ }
+ }));
+
+ assertEquals(true, callback.calledSuccess);
+ assertEquals(false, callback.calledError);
+ assertEquals(false, callback.calledAborted);
+ assertEquals(false, callback.calledRequestBackoff);
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail("Got exception.");
+ }
+ }
+
+ /**
+ * Test that a failing GlobalSession does in fact fail and back off.
+ */
+ @Test
+ public void testBackoffCalledInStages() {
+ try {
+ final MockGlobalSessionCallback callback = new MockGlobalSessionCallback();
+
+ // Stage fakes a 503 and sets X-Weave-Backoff header to the given seconds.
+ final GlobalSyncStage stage = new MockAbstractNonRepositorySyncStage() {
+ @Override
+ public void execute() {
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 503, "Illegal method/protocol"));
+
+ response.addHeader("X-Weave-Backoff", Long.toString(TEST_BACKOFF_IN_SECONDS)); // Backoff given in seconds.
+ session.handleHTTPError(new SyncStorageResponse(response), "Failure fetching info/collections.");
+ }
+ };
+
+ // Session installs fake stage to fetch info/collections.
+ SyncConfiguration config = new SyncConfiguration(TEST_USERNAME, new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD), new MockSharedPreferences(), new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY));
+ final GlobalSession session = new MockGlobalSession(config, callback)
+ .withStage(Stage.fetchInfoCollections, stage);
+
+ getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ session.start();
+ } catch (Exception e) {
+ final AssertionFailedError error = new AssertionFailedError();
+ error.initCause(e);
+ getTestWaiter().performNotify(error);
+ }
+ }
+ }));
+
+ assertEquals(false, callback.calledSuccess);
+ assertEquals(true, callback.calledError);
+ assertEquals(false, callback.calledAborted);
+ assertEquals(true, callback.calledRequestBackoff);
+ assertEquals(TEST_BACKOFF_IN_SECONDS * 1000, callback.weaveBackoff); // Backoff returned in milliseconds.
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail("Got exception.");
+ }
+ }
+
+ private HTTPServerTestHelper data = new HTTPServerTestHelper();
+
+ @SuppressWarnings("static-method")
+ @Before
+ public void setUp() {
+ BaseResource.rewriteLocalhost = false;
+ }
+
+ public void doRequest() {
+ final WaitHelper innerWaitHelper = new WaitHelper();
+ innerWaitHelper.performWait(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ final BaseResource r = new BaseResource(TEST_CLUSTER_URL);
+ r.delegate = new MockResourceDelegate(innerWaitHelper);
+ r.get();
+ } catch (URISyntaxException e) {
+ innerWaitHelper.performNotify(e);
+ }
+ }
+ });
+ }
+
+ public MockGlobalSessionCallback doTestSuccess(final boolean stageShouldBackoff, final boolean stageShouldAdvance) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, CryptoException {
+ MockServer server = new MockServer() {
+ @Override
+ public void handle(Request request, Response response) {
+ if (stageShouldBackoff) {
+ response.addValue("X-Weave-Backoff", Long.toString(TEST_BACKOFF_IN_SECONDS));
+ }
+ super.handle(request, response);
+ }
+ };
+
+ final MockServerSyncStage stage = new MockServerSyncStage() {
+ @Override
+ public void execute() {
+ // We should have installed our HTTP response observer before starting the sync.
+ assertTrue(BaseResource.isHttpResponseObserver(session));
+
+ doRequest();
+ if (stageShouldAdvance) {
+ session.advance();
+ return;
+ }
+ session.abort(null, "Stage intentionally failed.");
+ }
+ };
+
+ final MockGlobalSessionCallback callback = new MockGlobalSessionCallback();
+ SyncConfiguration config = new SyncConfiguration(TEST_USERNAME, new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD), new MockSharedPreferences(), new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY));
+ final GlobalSession session = new MockGlobalSession(config, callback)
+ .withStage(Stage.syncBookmarks, stage);
+
+ data.startHTTPServer(server);
+ WaitHelper.getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ session.start();
+ } catch (Exception e) {
+ final AssertionFailedError error = new AssertionFailedError();
+ error.initCause(e);
+ WaitHelper.getTestWaiter().performNotify(error);
+ }
+ }
+ }));
+ data.stopHTTPServer();
+
+ // We should have uninstalled our HTTP response observer when the session is terminated.
+ assertFalse(BaseResource.isHttpResponseObserver(session));
+
+ return callback;
+ }
+
+ @Test
+ public void testOnSuccessBackoffAdvanced() throws SyncConfigurationException,
+ IllegalArgumentException, NonObjectJSONException, IOException,
+ CryptoException {
+ MockGlobalSessionCallback callback = doTestSuccess(true, true);
+
+ assertTrue(callback.calledError); // TODO: this should be calledAborted.
+ assertTrue(callback.calledRequestBackoff);
+ assertEquals(1000 * TEST_BACKOFF_IN_SECONDS, callback.weaveBackoff);
+ }
+
+ @Test
+ public void testOnSuccessBackoffAborted() throws SyncConfigurationException,
+ IllegalArgumentException, NonObjectJSONException, IOException,
+ CryptoException {
+ MockGlobalSessionCallback callback = doTestSuccess(true, false);
+
+ assertTrue(callback.calledError); // TODO: this should be calledAborted.
+ assertTrue(callback.calledRequestBackoff);
+ assertEquals(1000 * TEST_BACKOFF_IN_SECONDS, callback.weaveBackoff);
+ }
+
+ @Test
+ public void testOnSuccessNoBackoffAdvanced() throws SyncConfigurationException,
+ IllegalArgumentException, NonObjectJSONException, IOException,
+ CryptoException {
+ MockGlobalSessionCallback callback = doTestSuccess(false, true);
+
+ assertTrue(callback.calledSuccess);
+ assertFalse(callback.calledRequestBackoff);
+ }
+
+ @Test
+ public void testOnSuccessNoBackoffAborted() throws SyncConfigurationException,
+ IllegalArgumentException, NonObjectJSONException, IOException,
+ CryptoException {
+ MockGlobalSessionCallback callback = doTestSuccess(false, false);
+
+ assertTrue(callback.calledError); // TODO: this should be calledAborted.
+ assertFalse(callback.calledRequestBackoff);
+ }
+
+ @Test
+ public void testGenerateNewMetaGlobalNonePersisted() throws Exception {
+ final MockGlobalSessionCallback callback = new MockGlobalSessionCallback();
+ final GlobalSession session = MockPrefsGlobalSession.getSession(TEST_USERNAME, TEST_PASSWORD,
+ new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY), callback, null, null);
+
+ // Verify we fill in all of our known engines when none are persisted.
+ session.config.enabledEngineNames = null;
+ MetaGlobal mg = session.generateNewMetaGlobal();
+ assertEquals(Long.valueOf(GlobalSession.STORAGE_VERSION), mg.getStorageVersion());
+ assertEquals(VersionConstants.BOOKMARKS_ENGINE_VERSION, mg.getEngines().getObject("bookmarks").getIntegerSafely("version").intValue());
+ assertEquals(VersionConstants.CLIENTS_ENGINE_VERSION, mg.getEngines().getObject("clients").getIntegerSafely("version").intValue());
+
+ List<String> namesList = new ArrayList<String>(mg.getEnabledEngineNames());
+ Collections.sort(namesList);
+ String[] names = namesList.toArray(new String[namesList.size()]);
+ String[] expected = new String[] { "bookmarks", "clients", "forms", "history", "passwords", "tabs" };
+ assertArrayEquals(expected, names);
+ }
+
+ @Test
+ public void testGenerateNewMetaGlobalSomePersisted() throws Exception {
+ final MockGlobalSessionCallback callback = new MockGlobalSessionCallback();
+ final GlobalSession session = MockPrefsGlobalSession.getSession(TEST_USERNAME, TEST_PASSWORD,
+ new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY), callback, null, null);
+
+ // Verify we preserve engines with version 0 if some are persisted.
+ session.config.enabledEngineNames = new HashSet<String>();
+ session.config.enabledEngineNames.add("bookmarks");
+ session.config.enabledEngineNames.add("clients");
+ session.config.enabledEngineNames.add("addons");
+ session.config.enabledEngineNames.add("prefs");
+
+ MetaGlobal mg = session.generateNewMetaGlobal();
+ assertEquals(Long.valueOf(GlobalSession.STORAGE_VERSION), mg.getStorageVersion());
+ assertEquals(VersionConstants.BOOKMARKS_ENGINE_VERSION, mg.getEngines().getObject("bookmarks").getIntegerSafely("version").intValue());
+ assertEquals(VersionConstants.CLIENTS_ENGINE_VERSION, mg.getEngines().getObject("clients").getIntegerSafely("version").intValue());
+ assertEquals(0, mg.getEngines().getObject("addons").getIntegerSafely("version").intValue());
+ assertEquals(0, mg.getEngines().getObject("prefs").getIntegerSafely("version").intValue());
+
+ List<String> namesList = new ArrayList<String>(mg.getEnabledEngineNames());
+ Collections.sort(namesList);
+ String[] names = namesList.toArray(new String[namesList.size()]);
+ String[] expected = new String[] { "addons", "bookmarks", "clients", "prefs" };
+ assertArrayEquals(expected, names);
+ }
+
+ @Test
+ public void testUploadUpdatedMetaGlobal() throws Exception {
+ // Set up session with meta/global.
+ final MockGlobalSessionCallback callback = new MockGlobalSessionCallback();
+ final GlobalSession session = MockPrefsGlobalSession.getSession(TEST_USERNAME, TEST_PASSWORD,
+ new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY), callback, null, null);
+ session.config.metaGlobal = session.generateNewMetaGlobal();
+ session.enginesToUpdate.clear();
+
+ // Set enabledEngines in meta/global, including a "new engine."
+ String[] origEngines = new String[] { "bookmarks", "clients", "forms", "history", "tabs", "new-engine" };
+
+ ExtendedJSONObject origEnginesJSONObject = new ExtendedJSONObject();
+ for (String engineName : origEngines) {
+ EngineSettings mockEngineSettings = new EngineSettings(Utils.generateGuid(), Integer.valueOf(0));
+ origEnginesJSONObject.put(engineName, mockEngineSettings.toJSONObject());
+ }
+ session.config.metaGlobal.setEngines(origEnginesJSONObject);
+
+ // Engines to remove.
+ String[] toRemove = new String[] { "bookmarks", "tabs" };
+ for (String name : toRemove) {
+ session.removeEngineFromMetaGlobal(name);
+ }
+
+ // Engines to add.
+ String[] toAdd = new String[] { "passwords" };
+ for (String name : toAdd) {
+ String syncId = Utils.generateGuid();
+ session.recordForMetaGlobalUpdate(name, new EngineSettings(syncId, Integer.valueOf(1)));
+ }
+
+ // Update engines.
+ session.uploadUpdatedMetaGlobal();
+
+ // Check resulting enabledEngines.
+ Set<String> expected = new HashSet<String>();
+ for (String name : origEngines) {
+ expected.add(name);
+ }
+ for (String name : toRemove) {
+ expected.remove(name);
+ }
+ for (String name : toAdd) {
+ expected.add(name);
+ }
+ assertEquals(expected, session.config.metaGlobal.getEnabledEngineNames());
+ }
+
+ public void testStageAdvance() {
+ assertEquals(GlobalSession.nextStage(Stage.idle), Stage.checkPreconditions);
+ assertEquals(GlobalSession.nextStage(Stage.completed), Stage.idle);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestHeaderParsing.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestHeaderParsing.java
new file mode 100644
index 0000000000..532d60d133
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestHeaderParsing.java
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.net.test;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.Utils;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(TestRunner.class)
+public class TestHeaderParsing {
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testDecimalSecondsToMilliseconds() {
+ assertEquals(Utils.decimalSecondsToMilliseconds(""), -1);
+ assertEquals(Utils.decimalSecondsToMilliseconds("1234.1.1"), -1);
+ assertEquals(Utils.decimalSecondsToMilliseconds("1234"), 1234000);
+ assertEquals(Utils.decimalSecondsToMilliseconds("1234.123"), 1234123);
+ assertEquals(Utils.decimalSecondsToMilliseconds("1234.12"), 1234120);
+
+ assertEquals("1234.000", Utils.millisecondsToDecimalSecondsString(1234000));
+ assertEquals("1234.123", Utils.millisecondsToDecimalSecondsString(1234123));
+ assertEquals("1234.120", Utils.millisecondsToDecimalSecondsString(1234120));
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestLineByLineHandling.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestLineByLineHandling.java
new file mode 100644
index 0000000000..b7f11adbfe
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestLineByLineHandling.java
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.net.test;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
+import org.mozilla.android.sync.test.helpers.MockServer;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.SyncStorageCollectionRequest;
+import org.mozilla.gecko.sync.net.SyncStorageCollectionRequestDelegate;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.simpleframework.http.Request;
+import org.simpleframework.http.Response;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+@RunWith(TestRunner.class)
+public class TestLineByLineHandling {
+ private static final int TEST_PORT = HTTPServerTestHelper.getTestPort();
+ private static final String TEST_SERVER = "http://localhost:" + TEST_PORT;
+ private static final String LOG_TAG = "TestLineByLineHandling";
+ static String STORAGE_URL = TEST_SERVER + "/1.1/c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd/storage/lines";
+ private HTTPServerTestHelper data = new HTTPServerTestHelper();
+
+ public ArrayList<String> lines = new ArrayList<String>();
+
+ public class LineByLineMockServer extends MockServer {
+ public void handle(Request request, Response response) {
+ try {
+ System.out.println("Handling line-by-line request...");
+ PrintStream bodyStream = this.handleBasicHeaders(request, response, 200, "application/newlines");
+
+ bodyStream.print("First line.\n");
+ bodyStream.print("Second line.\n");
+ bodyStream.print("Third line.\n");
+ bodyStream.print("Fourth line.\n");
+ bodyStream.close();
+ } catch (IOException e) {
+ System.err.println("Oops.");
+ }
+ }
+ }
+
+ public class BaseLineByLineDelegate extends
+ SyncStorageCollectionRequestDelegate {
+
+ @Override
+ public void handleRequestProgress(String progress) {
+ lines.add(progress);
+ }
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return null;
+ }
+
+ @Override
+ public String ifUnmodifiedSince() {
+ return null;
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse res) {
+ Logger.info(LOG_TAG, "Request success.");
+ assertTrue(res.wasSuccessful());
+ assertTrue(res.httpResponse().containsHeader("X-Weave-Timestamp"));
+
+ assertEquals(lines.size(), 4);
+ assertEquals(lines.get(0), "First line.");
+ assertEquals(lines.get(1), "Second line.");
+ assertEquals(lines.get(2), "Third line.");
+ assertEquals(lines.get(3), "Fourth line.");
+ data.stopHTTPServer();
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ Logger.info(LOG_TAG, "Got request failure: " + response);
+ BaseResource.consumeEntity(response);
+ fail("Should not be called.");
+ }
+
+ @Override
+ public void handleRequestError(Exception ex) {
+ Logger.error(LOG_TAG, "Got request error: ", ex);
+ fail("Should not be called.");
+ }
+ }
+
+ @Test
+ public void testLineByLine() throws URISyntaxException {
+ BaseResource.rewriteLocalhost = false;
+
+ data.startHTTPServer(new LineByLineMockServer());
+ Logger.info(LOG_TAG, "Server started.");
+ SyncStorageCollectionRequest r = new SyncStorageCollectionRequest(new URI(STORAGE_URL));
+ SyncStorageCollectionRequestDelegate delegate = new BaseLineByLineDelegate();
+ r.delegate = delegate;
+ r.get();
+ // Server is stopped in the callback.
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestMetaGlobal.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestMetaGlobal.java
new file mode 100644
index 0000000000..ec4c038593
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestMetaGlobal.java
@@ -0,0 +1,347 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.net.test;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
+import org.mozilla.android.sync.test.helpers.MockServer;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.MetaGlobal;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.delegates.MetaGlobalDelegate;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.simpleframework.http.Request;
+import org.simpleframework.http.Response;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
+public class TestMetaGlobal {
+ private static final int TEST_PORT = HTTPServerTestHelper.getTestPort();
+ private static final String TEST_SERVER = "http://localhost:" + TEST_PORT;
+ private static final String TEST_SYNC_ID = "foobar";
+
+ public static final String USER_PASS = "c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd:password";
+ public static final String META_URL = TEST_SERVER + "/1.1/c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd/storage/meta/global";
+ private HTTPServerTestHelper data = new HTTPServerTestHelper();
+
+
+ public static final String TEST_DECLINED_META_GLOBAL_RESPONSE =
+ "{\"id\":\"global\"," +
+ "\"payload\":" +
+ "\"{\\\"syncID\\\":\\\"zPSQTm7WBVWB\\\"," +
+ "\\\"declined\\\":[\\\"bookmarks\\\"]," +
+ "\\\"storageVersion\\\":5," +
+ "\\\"engines\\\":{" +
+ "\\\"clients\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"fDg0MS5bDtV7\\\"}," +
+ "\\\"forms\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"GXF29AFprnvc\\\"}," +
+ "\\\"history\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"av75g4vm-_rp\\\"}," +
+ "\\\"passwords\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"LT_ACGpuKZ6a\\\"}," +
+ "\\\"prefs\\\":{\\\"version\\\":2,\\\"syncID\\\":\\\"-3nsksP9wSAs\\\"}," +
+ "\\\"tabs\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"W4H5lOMChkYA\\\"}}}\"," +
+ "\"username\":\"5817483\"," +
+ "\"modified\":1.32046073744E9}";
+
+ public static final String TEST_META_GLOBAL_RESPONSE =
+ "{\"id\":\"global\"," +
+ "\"payload\":" +
+ "\"{\\\"syncID\\\":\\\"zPSQTm7WBVWB\\\"," +
+ "\\\"storageVersion\\\":5," +
+ "\\\"engines\\\":{" +
+ "\\\"clients\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"fDg0MS5bDtV7\\\"}," +
+ "\\\"bookmarks\\\":{\\\"version\\\":2,\\\"syncID\\\":\\\"NNaQr6_F-9dm\\\"}," +
+ "\\\"forms\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"GXF29AFprnvc\\\"}," +
+ "\\\"history\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"av75g4vm-_rp\\\"}," +
+ "\\\"passwords\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"LT_ACGpuKZ6a\\\"}," +
+ "\\\"prefs\\\":{\\\"version\\\":2,\\\"syncID\\\":\\\"-3nsksP9wSAs\\\"}," +
+ "\\\"tabs\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"W4H5lOMChkYA\\\"}}}\"," +
+ "\"username\":\"5817483\"," +
+ "\"modified\":1.32046073744E9}";
+ public static final String TEST_META_GLOBAL_NO_PAYLOAD_RESPONSE = "{\"id\":\"global\"," +
+ "\"username\":\"5817483\",\"modified\":1.32046073744E9}";
+ public static final String TEST_META_GLOBAL_MALFORMED_PAYLOAD_RESPONSE = "{\"id\":\"global\"," +
+ "\"payload\":\"{!!!}\"," +
+ "\"username\":\"5817483\",\"modified\":1.32046073744E9}";
+ public static final String TEST_META_GLOBAL_EMPTY_PAYLOAD_RESPONSE = "{\"id\":\"global\"," +
+ "\"payload\":\"{}\"," +
+ "\"username\":\"5817483\",\"modified\":1.32046073744E9}";
+
+ public MetaGlobal global;
+
+ @SuppressWarnings("static-method")
+ @Before
+ public void setUp() {
+ BaseResource.rewriteLocalhost = false;
+ global = new MetaGlobal(META_URL, new BasicAuthHeaderProvider(USER_PASS));
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testSyncID() {
+ global.setSyncID("foobar");
+ assertEquals(global.getSyncID(), "foobar");
+ }
+
+ public class MockMetaGlobalFetchDelegate implements MetaGlobalDelegate {
+ boolean successCalled = false;
+ MetaGlobal successGlobal = null;
+ SyncStorageResponse successResponse = null;
+ boolean failureCalled = false;
+ SyncStorageResponse failureResponse = null;
+ boolean errorCalled = false;
+ Exception errorException = null;
+ boolean missingCalled = false;
+ MetaGlobal missingGlobal = null;
+ SyncStorageResponse missingResponse = null;
+
+ public void handleSuccess(MetaGlobal global, SyncStorageResponse response) {
+ successCalled = true;
+ successGlobal = global;
+ successResponse = response;
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ public void handleFailure(SyncStorageResponse response) {
+ failureCalled = true;
+ failureResponse = response;
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ public void handleError(Exception e) {
+ errorCalled = true;
+ errorException = e;
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ public void handleMissing(MetaGlobal global, SyncStorageResponse response) {
+ missingCalled = true;
+ missingGlobal = global;
+ missingResponse = response;
+ WaitHelper.getTestWaiter().performNotify();
+ }
+ }
+
+ public MockMetaGlobalFetchDelegate doFetch(final MetaGlobal global) {
+ final MockMetaGlobalFetchDelegate delegate = new MockMetaGlobalFetchDelegate();
+ WaitHelper.getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
+ @Override
+ public void run() {
+ global.fetch(delegate);
+ }
+ }));
+
+ return delegate;
+ }
+
+ @Test
+ public void testFetchMissing() {
+ MockServer missingMetaGlobalServer = new MockServer(404, "{}");
+ global.setSyncID(TEST_SYNC_ID);
+ assertEquals(TEST_SYNC_ID, global.getSyncID());
+
+ data.startHTTPServer(missingMetaGlobalServer);
+ final MockMetaGlobalFetchDelegate delegate = doFetch(global);
+ data.stopHTTPServer();
+
+ assertTrue(delegate.missingCalled);
+ assertEquals(404, delegate.missingResponse.getStatusCode());
+ assertEquals(TEST_SYNC_ID, delegate.missingGlobal.getSyncID());
+ }
+
+ @Test
+ public void testFetchExisting() {
+ MockServer existingMetaGlobalServer = new MockServer(200, TEST_META_GLOBAL_RESPONSE);
+ assertNull(global.getSyncID());
+ assertNull(global.getEngines());
+ assertNull(global.getStorageVersion());
+
+ data.startHTTPServer(existingMetaGlobalServer);
+ final MockMetaGlobalFetchDelegate delegate = doFetch(global);
+ data.stopHTTPServer();
+
+ assertTrue(delegate.successCalled);
+ assertEquals(200, delegate.successResponse.getStatusCode());
+ assertEquals("zPSQTm7WBVWB", global.getSyncID());
+ assertTrue(global.getEngines() instanceof ExtendedJSONObject);
+ assertEquals(Long.valueOf(5), global.getStorageVersion());
+ }
+
+ /**
+ * A record that is valid JSON but invalid as a meta/global record will be
+ * downloaded successfully, but will fail later.
+ */
+ @Test
+ public void testFetchNoPayload() {
+ MockServer existingMetaGlobalServer = new MockServer(200, TEST_META_GLOBAL_NO_PAYLOAD_RESPONSE);
+
+ data.startHTTPServer(existingMetaGlobalServer);
+ final MockMetaGlobalFetchDelegate delegate = doFetch(global);
+ data.stopHTTPServer();
+
+ assertTrue(delegate.successCalled);
+ }
+
+ @Test
+ public void testFetchEmptyPayload() {
+ MockServer existingMetaGlobalServer = new MockServer(200, TEST_META_GLOBAL_EMPTY_PAYLOAD_RESPONSE);
+
+ data.startHTTPServer(existingMetaGlobalServer);
+ final MockMetaGlobalFetchDelegate delegate = doFetch(global);
+ data.stopHTTPServer();
+
+ assertTrue(delegate.successCalled);
+ }
+
+ /**
+ * A record that is invalid JSON will fail to download at all.
+ */
+ @Test
+ public void testFetchMalformedPayload() {
+ MockServer existingMetaGlobalServer = new MockServer(200, TEST_META_GLOBAL_MALFORMED_PAYLOAD_RESPONSE);
+
+ data.startHTTPServer(existingMetaGlobalServer);
+ final MockMetaGlobalFetchDelegate delegate = doFetch(global);
+ data.stopHTTPServer();
+
+ assertTrue(delegate.errorCalled);
+ assertNotNull(delegate.errorException);
+ assertEquals(NonObjectJSONException.class, delegate.errorException.getClass());
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testSetFromRecord() throws Exception {
+ MetaGlobal mg = new MetaGlobal(null, null);
+ mg.setFromRecord(CryptoRecord.fromJSONRecord(TEST_META_GLOBAL_RESPONSE));
+ assertEquals("zPSQTm7WBVWB", mg.getSyncID());
+ assertTrue(mg.getEngines() instanceof ExtendedJSONObject);
+ assertEquals(Long.valueOf(5), mg.getStorageVersion());
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testAsCryptoRecord() throws Exception {
+ MetaGlobal mg = new MetaGlobal(null, null);
+ mg.setFromRecord(CryptoRecord.fromJSONRecord(TEST_META_GLOBAL_RESPONSE));
+ CryptoRecord rec = mg.asCryptoRecord();
+ assertEquals("global", rec.guid);
+ mg.setFromRecord(rec);
+ assertEquals("zPSQTm7WBVWB", mg.getSyncID());
+ assertTrue(mg.getEngines() instanceof ExtendedJSONObject);
+ assertEquals(Long.valueOf(5), mg.getStorageVersion());
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testGetEnabledEngineNames() throws Exception {
+ MetaGlobal mg = new MetaGlobal(null, null);
+ mg.setFromRecord(CryptoRecord.fromJSONRecord(TEST_META_GLOBAL_RESPONSE));
+ assertEquals("zPSQTm7WBVWB", mg.getSyncID());
+ final Set<String> actual = mg.getEnabledEngineNames();
+ final Set<String> expected = new HashSet<String>();
+ for (String name : new String[] { "bookmarks", "clients", "forms", "history", "passwords", "prefs", "tabs" }) {
+ expected.add(name);
+ }
+ assertEquals(expected, actual);
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testGetEmptyDeclinedEngineNames() throws Exception {
+ MetaGlobal mg = new MetaGlobal(null, null);
+ mg.setFromRecord(CryptoRecord.fromJSONRecord(TEST_META_GLOBAL_RESPONSE));
+ assertEquals(0, mg.getDeclinedEngineNames().size());
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testGetDeclinedEngineNames() throws Exception {
+ MetaGlobal mg = new MetaGlobal(null, null);
+ mg.setFromRecord(CryptoRecord.fromJSONRecord(TEST_DECLINED_META_GLOBAL_RESPONSE));
+ assertEquals(1, mg.getDeclinedEngineNames().size());
+ assertEquals("bookmarks", mg.getDeclinedEngineNames().iterator().next());
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testRoundtripDeclinedEngineNames() throws Exception {
+ MetaGlobal mg = new MetaGlobal(null, null);
+ mg.setFromRecord(CryptoRecord.fromJSONRecord(TEST_DECLINED_META_GLOBAL_RESPONSE));
+ assertEquals("bookmarks", mg.getDeclinedEngineNames().iterator().next());
+ assertEquals("bookmarks", mg.asCryptoRecord().payload.getArray("declined").get(0));
+ MetaGlobal again = new MetaGlobal(null, null);
+ again.setFromRecord(mg.asCryptoRecord());
+ assertEquals("bookmarks", again.getDeclinedEngineNames().iterator().next());
+ }
+
+
+ public MockMetaGlobalFetchDelegate doUpload(final MetaGlobal global) {
+ final MockMetaGlobalFetchDelegate delegate = new MockMetaGlobalFetchDelegate();
+ WaitHelper.getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
+ @Override
+ public void run() {
+ global.upload(delegate);
+ }
+ }));
+
+ return delegate;
+ }
+
+ @Test
+ public void testUpload() {
+ long TEST_STORAGE_VERSION = 111;
+ String TEST_SYNC_ID = "testSyncID";
+ global.setSyncID(TEST_SYNC_ID);
+ global.setStorageVersion(Long.valueOf(TEST_STORAGE_VERSION));
+
+ final AtomicBoolean mgUploaded = new AtomicBoolean(false);
+ final MetaGlobal uploadedMg = new MetaGlobal(null, null);
+
+ MockServer server = new MockServer() {
+ public void handle(Request request, Response response) {
+ if (request.getMethod().equals("PUT")) {
+ try {
+ ExtendedJSONObject body = new ExtendedJSONObject(request.getContent());
+ System.out.println(body.toJSONString());
+ assertTrue(body.containsKey("payload"));
+ assertFalse(body.containsKey("default"));
+
+ CryptoRecord rec = CryptoRecord.fromJSONRecord(body);
+ uploadedMg.setFromRecord(rec);
+ mgUploaded.set(true);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ this.handle(request, response, 200, "success");
+ return;
+ }
+ this.handle(request, response, 404, "missing");
+ }
+ };
+
+ data.startHTTPServer(server);
+ final MockMetaGlobalFetchDelegate delegate = doUpload(global);
+ data.stopHTTPServer();
+
+ assertTrue(delegate.successCalled);
+ assertTrue(mgUploaded.get());
+ assertEquals(TEST_SYNC_ID, uploadedMg.getSyncID());
+ assertEquals(TEST_STORAGE_VERSION, uploadedMg.getStorageVersion().longValue());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestResource.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestResource.java
new file mode 100644
index 0000000000..e53d02d337
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestResource.java
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.net.test;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
+import org.mozilla.android.sync.test.helpers.MockResourceDelegate;
+import org.mozilla.android.sync.test.helpers.MockServer;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.HttpResponseObserver;
+
+import java.net.URISyntaxException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
+public class TestResource {
+ private static final int TEST_PORT = HTTPServerTestHelper.getTestPort();
+ private static final String TEST_SERVER = "http://localhost:" + TEST_PORT;
+
+ private HTTPServerTestHelper data = new HTTPServerTestHelper();
+
+ @SuppressWarnings("static-method")
+ @Before
+ public void setUp() {
+ BaseResource.rewriteLocalhost = false;
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testLocalhostRewriting() throws URISyntaxException {
+ BaseResource r = new BaseResource("http://localhost:5000/foo/bar", true);
+ assertEquals("http://10.0.2.2:5000/foo/bar", r.getURI().toASCIIString());
+ }
+
+ @SuppressWarnings("static-method")
+ public MockResourceDelegate doGet() throws URISyntaxException {
+ final BaseResource r = new BaseResource(TEST_SERVER + "/foo/bar");
+ MockResourceDelegate delegate = new MockResourceDelegate();
+ r.delegate = delegate;
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ r.get();
+ }
+ });
+ return delegate;
+ }
+
+ @Test
+ public void testTrivialFetch() throws URISyntaxException {
+ MockServer server = data.startHTTPServer();
+ server.expectedBasicAuthHeader = MockResourceDelegate.EXPECT_BASIC;
+ MockResourceDelegate delegate = doGet();
+ assertTrue(delegate.handledHttpResponse);
+ data.stopHTTPServer();
+ }
+
+ public static class MockHttpResponseObserver implements HttpResponseObserver {
+ public HttpResponse response = null;
+
+ @Override
+ public void observeHttpResponse(HttpUriRequest request, HttpResponse response) {
+ this.response = response;
+ }
+ }
+
+ @Test
+ public void testObservers() throws URISyntaxException {
+ data.startHTTPServer();
+ // Check that null observer doesn't fail.
+ BaseResource.addHttpResponseObserver(null);
+ doGet(); // HTTP server stopped in callback.
+
+ // Check that multiple non-null observers gets called with reasonable HttpResponse.
+ MockHttpResponseObserver observers[] = { new MockHttpResponseObserver(), new MockHttpResponseObserver() };
+ for (MockHttpResponseObserver observer : observers) {
+ BaseResource.addHttpResponseObserver(observer);
+ assertTrue(BaseResource.isHttpResponseObserver(observer));
+ assertNull(observer.response);
+ }
+
+ doGet(); // HTTP server stopped in callback.
+
+ for (MockHttpResponseObserver observer : observers) {
+ assertNotNull(observer.response);
+ assertEquals(200, observer.response.getStatusLine().getStatusCode());
+ }
+
+ data.stopHTTPServer();
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestRetryAfter.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestRetryAfter.java
new file mode 100644
index 0000000000..429ad29d47
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestRetryAfter.java
@@ -0,0 +1,87 @@
+package org.mozilla.android.sync.net.test;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.impl.cookie.DateUtils;
+import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
+import ch.boye.httpclientandroidlib.message.BasicStatusLine;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.net.SyncResponse;
+
+import java.util.Date;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
+public class TestRetryAfter {
+ private int TEST_SECONDS = 120;
+
+ @Test
+ public void testRetryAfterParsesSeconds() {
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 503, "Illegal method/protocol"));
+ response.addHeader("Retry-After", Long.toString(TEST_SECONDS)); // Retry-After given in seconds.
+
+ final SyncResponse syncResponse = new SyncResponse(response);
+ assertEquals(TEST_SECONDS, syncResponse.retryAfterInSeconds());
+ }
+
+ @Test
+ public void testRetryAfterParsesHTTPDate() {
+ Date future = new Date(System.currentTimeMillis() + TEST_SECONDS * 1000);
+
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 503, "Illegal method/protocol"));
+ response.addHeader("Retry-After", DateUtils.formatDate(future));
+
+ final SyncResponse syncResponse = new SyncResponse(response);
+ assertTrue(syncResponse.retryAfterInSeconds() > TEST_SECONDS - 15);
+ assertTrue(syncResponse.retryAfterInSeconds() < TEST_SECONDS + 15);
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testRetryAfterParsesMalformed() {
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 503, "Illegal method/protocol"));
+ response.addHeader("Retry-After", "10X");
+
+ final SyncResponse syncResponse = new SyncResponse(response);
+ assertEquals(-1, syncResponse.retryAfterInSeconds());
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testRetryAfterParsesNeither() {
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 503, "Illegal method/protocol"));
+
+ final SyncResponse syncResponse = new SyncResponse(response);
+ assertEquals(-1, syncResponse.retryAfterInSeconds());
+ }
+
+ @Test
+ public void testRetryAfterParsesLargerRetryAfter() {
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 503, "Illegal method/protocol"));
+ response.addHeader("Retry-After", Long.toString(TEST_SECONDS + 1));
+ response.addHeader("X-Weave-Backoff", Long.toString(TEST_SECONDS));
+
+ final SyncResponse syncResponse = new SyncResponse(response);
+ assertEquals(1000 * (TEST_SECONDS + 1), syncResponse.totalBackoffInMilliseconds());
+ }
+
+ @Test
+ public void testRetryAfterParsesLargerXWeaveBackoff() {
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 503, "Illegal method/protocol"));
+ response.addHeader("Retry-After", Long.toString(TEST_SECONDS));
+ response.addHeader("X-Weave-Backoff", Long.toString(TEST_SECONDS + 1));
+
+ final SyncResponse syncResponse = new SyncResponse(response);
+ assertEquals(1000 * (TEST_SECONDS + 1), syncResponse.totalBackoffInMilliseconds());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestServer11Repository.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestServer11Repository.java
new file mode 100644
index 0000000000..3aa0a91ec2
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestServer11Repository.java
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.net.test;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.InfoCollections;
+import org.mozilla.gecko.sync.InfoConfiguration;
+import org.mozilla.gecko.sync.repositories.Server11Repository;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+@RunWith(TestRunner.class)
+public class TestServer11Repository {
+
+ private static final String COLLECTION = "bookmarks";
+ private static final String COLLECTION_URL = "http://foo.com/1.1/n6ec3u5bee3tixzp2asys7bs6fve4jfw/storage";
+
+ protected final InfoCollections infoCollections = new InfoCollections();
+ protected final InfoConfiguration infoConfiguration = new InfoConfiguration();
+
+ public static void assertQueryEquals(String expected, URI u) {
+ Assert.assertEquals(expected, u.getRawQuery());
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testCollectionURIFull() throws URISyntaxException {
+ Server11Repository r = new Server11Repository(COLLECTION, COLLECTION_URL, null, infoCollections, infoConfiguration);
+ assertQueryEquals("full=1&newer=5000.000", r.collectionURI(true, 5000000L, -1, null, null, null));
+ assertQueryEquals("newer=1230.000", r.collectionURI(false, 1230000L, -1, null, null, null));
+ assertQueryEquals("newer=5000.000&limit=10", r.collectionURI(false, 5000000L, 10, null, null, null));
+ assertQueryEquals("full=1&newer=5000.000&sort=index", r.collectionURI(true, 5000000L, 0, "index", null, null));
+ assertQueryEquals("full=1&ids=123,abc", r.collectionURI(true, -1L, -1, null, "123,abc", null));
+ }
+
+ @Test
+ public void testCollectionURI() throws URISyntaxException {
+ Server11Repository noTrailingSlash = new Server11Repository(COLLECTION, COLLECTION_URL, null, infoCollections, infoConfiguration);
+ Server11Repository trailingSlash = new Server11Repository(COLLECTION, COLLECTION_URL + "/", null, infoCollections, infoConfiguration);
+ Assert.assertEquals("http://foo.com/1.1/n6ec3u5bee3tixzp2asys7bs6fve4jfw/storage/bookmarks", noTrailingSlash.collectionURI().toASCIIString());
+ Assert.assertEquals("http://foo.com/1.1/n6ec3u5bee3tixzp2asys7bs6fve4jfw/storage/bookmarks", trailingSlash.collectionURI().toASCIIString());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestSyncStorageRequest.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestSyncStorageRequest.java
new file mode 100644
index 0000000000..0e6447c276
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestSyncStorageRequest.java
@@ -0,0 +1,269 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.net.test;
+
+import org.json.simple.JSONObject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.android.sync.test.helpers.BaseTestStorageRequestDelegate;
+import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
+import org.mozilla.android.sync.test.helpers.MockServer;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+import org.mozilla.gecko.sync.net.SyncStorageRecordRequest;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.simpleframework.http.Request;
+import org.simpleframework.http.Response;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+@RunWith(TestRunner.class)
+public class TestSyncStorageRequest {
+ private static final int TEST_PORT = HTTPServerTestHelper.getTestPort();
+ private static final String TEST_SERVER = "http://localhost:" + TEST_PORT;
+
+ private static final String LOCAL_META_URL = TEST_SERVER + "/1.1/c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd/storage/meta/global";
+ private static final String LOCAL_BAD_REQUEST_URL = TEST_SERVER + "/1.1/c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd/storage/bad";
+
+ private static final String EXPECTED_ERROR_CODE = "12";
+ private static final String EXPECTED_RETRY_AFTER_ERROR_MESSAGE = "{error:'informative error message'}";
+
+ // Corresponds to rnewman+testandroid@mozilla.com.
+ private static final String TEST_USERNAME = "c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd";
+ private static final String TEST_PASSWORD = "password";
+ private final AuthHeaderProvider authHeaderProvider = new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD);
+
+ private HTTPServerTestHelper data = new HTTPServerTestHelper();
+
+ public class TestSyncStorageRequestDelegate extends
+ BaseTestStorageRequestDelegate {
+ public TestSyncStorageRequestDelegate(AuthHeaderProvider authHeaderProvider) {
+ super(authHeaderProvider);
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse res) {
+ assertTrue(res.wasSuccessful());
+ assertTrue(res.httpResponse().containsHeader("X-Weave-Timestamp"));
+
+ // Make sure we consume the rest of the body, so we can reuse the
+ // connection. Even test code has to be correct in this regard!
+ try {
+ System.out.println("Success body: " + res.body());
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ BaseResource.consumeEntity(res);
+ data.stopHTTPServer();
+ }
+ }
+
+ public class TestBadSyncStorageRequestDelegate extends
+ BaseTestStorageRequestDelegate {
+
+ public TestBadSyncStorageRequestDelegate(AuthHeaderProvider authHeaderProvider) {
+ super(authHeaderProvider);
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse res) {
+ assertTrue(!res.wasSuccessful());
+ assertTrue(res.httpResponse().containsHeader("X-Weave-Timestamp"));
+ try {
+ String responseMessage = res.getErrorMessage();
+ String expectedMessage = SyncStorageResponse.SERVER_ERROR_MESSAGES.get(EXPECTED_ERROR_CODE);
+ assertEquals(expectedMessage, responseMessage);
+ } catch (Exception e) {
+ fail("Got exception fetching error message.");
+ }
+ BaseResource.consumeEntity(res);
+ data.stopHTTPServer();
+ }
+ }
+
+
+ @Test
+ public void testSyncStorageRequest() throws URISyntaxException, IOException {
+ BaseResource.rewriteLocalhost = false;
+ data.startHTTPServer();
+ SyncStorageRecordRequest r = new SyncStorageRecordRequest(new URI(LOCAL_META_URL));
+ TestSyncStorageRequestDelegate delegate = new TestSyncStorageRequestDelegate(authHeaderProvider);
+ r.delegate = delegate;
+ r.get();
+ // Server is stopped in the callback.
+ }
+
+ public class ErrorMockServer extends MockServer {
+ @Override
+ public void handle(Request request, Response response) {
+ super.handle(request, response, 400, EXPECTED_ERROR_CODE);
+ }
+ }
+
+ @Test
+ public void testErrorResponse() throws URISyntaxException {
+ BaseResource.rewriteLocalhost = false;
+ data.startHTTPServer(new ErrorMockServer());
+ SyncStorageRecordRequest r = new SyncStorageRecordRequest(new URI(LOCAL_BAD_REQUEST_URL));
+ TestBadSyncStorageRequestDelegate delegate = new TestBadSyncStorageRequestDelegate(authHeaderProvider);
+ r.delegate = delegate;
+ r.post(new JSONObject());
+ // Server is stopped in the callback.
+ }
+
+ // Test that the Retry-After header is correctly parsed and that handleRequestFailure
+ // is being called.
+ public class TestRetryAfterSyncStorageRequestDelegate extends BaseTestStorageRequestDelegate {
+
+ public TestRetryAfterSyncStorageRequestDelegate(AuthHeaderProvider authHeaderProvider) {
+ super(authHeaderProvider);
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse res) {
+ assertTrue(!res.wasSuccessful());
+ assertTrue(res.httpResponse().containsHeader("Retry-After"));
+ assertEquals(res.retryAfterInSeconds(), 3001);
+ try {
+ String responseMessage = res.getErrorMessage();
+ String expectedMessage = EXPECTED_RETRY_AFTER_ERROR_MESSAGE;
+ assertEquals(expectedMessage, responseMessage);
+ } catch (Exception e) {
+ fail("Got exception fetching error message.");
+ }
+ BaseResource.consumeEntity(res);
+ data.stopHTTPServer();
+ }
+ }
+
+ public class RetryAfterMockServer extends MockServer {
+ @Override
+ public void handle(Request request, Response response) {
+ String errorBody = EXPECTED_RETRY_AFTER_ERROR_MESSAGE;
+ response.setValue("Retry-After", "3001");
+ super.handle(request, response, 503, errorBody);
+ }
+ }
+
+ @Test
+ public void testRetryAfterResponse() throws URISyntaxException {
+ BaseResource.rewriteLocalhost = false;
+ data.startHTTPServer(new RetryAfterMockServer());
+ SyncStorageRecordRequest r = new SyncStorageRecordRequest(new URI(LOCAL_BAD_REQUEST_URL)); // URL not used -- we 503 every response
+ TestRetryAfterSyncStorageRequestDelegate delegate = new TestRetryAfterSyncStorageRequestDelegate(authHeaderProvider);
+ r.delegate = delegate;
+ r.post(new JSONObject());
+ // Server is stopped in the callback.
+ }
+
+ // Test that the X-Weave-Backoff header is correctly parsed and that handleRequestSuccess
+ // is still being called.
+ public class TestWeaveBackoffSyncStorageRequestDelegate extends
+ TestSyncStorageRequestDelegate {
+
+ public TestWeaveBackoffSyncStorageRequestDelegate(AuthHeaderProvider authHeaderProvider) {
+ super(authHeaderProvider);
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse res) {
+ assertTrue(res.httpResponse().containsHeader("X-Weave-Backoff"));
+ assertEquals(res.weaveBackoffInSeconds(), 1801);
+ super.handleRequestSuccess(res);
+ }
+ }
+
+ public class WeaveBackoffMockServer extends MockServer {
+ @Override
+ public void handle(Request request, Response response) {
+ response.setValue("X-Weave-Backoff", "1801");
+ super.handle(request, response);
+ }
+ }
+
+ @Test
+ public void testWeaveBackoffResponse() throws URISyntaxException {
+ BaseResource.rewriteLocalhost = false;
+ data.startHTTPServer(new WeaveBackoffMockServer());
+ SyncStorageRecordRequest r = new SyncStorageRecordRequest(new URI(LOCAL_META_URL)); // URL re-used -- we need any successful response
+ TestWeaveBackoffSyncStorageRequestDelegate delegate = new TestWeaveBackoffSyncStorageRequestDelegate(new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD));
+ r.delegate = delegate;
+ r.post(new JSONObject());
+ // Server is stopped in the callback.
+ }
+
+ // Test that the X-Weave-{Quota-Remaining, Alert, Records} headers are correctly parsed and
+ // that handleRequestSuccess is still being called.
+ public class TestHeadersSyncStorageRequestDelegate extends
+ TestSyncStorageRequestDelegate {
+
+ public TestHeadersSyncStorageRequestDelegate(AuthHeaderProvider authHeaderProvider) {
+ super(authHeaderProvider);
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse res) {
+ assertTrue(res.httpResponse().containsHeader("X-Weave-Quota-Remaining"));
+ assertTrue(res.httpResponse().containsHeader("X-Weave-Alert"));
+ assertTrue(res.httpResponse().containsHeader("X-Weave-Records"));
+ assertEquals(65536, res.weaveQuotaRemaining());
+ assertEquals("First weave alert string", res.weaveAlert());
+ assertEquals(50, res.weaveRecords());
+
+ super.handleRequestSuccess(res);
+ }
+ }
+
+ public class HeadersMockServer extends MockServer {
+ @Override
+ public void handle(Request request, Response response) {
+ response.setValue("X-Weave-Quota-Remaining", "65536");
+ response.setValue("X-Weave-Alert", "First weave alert string");
+ response.addValue("X-Weave-Alert", "Second weave alert string");
+ response.setValue("X-Weave-Records", "50");
+
+ super.handle(request, response);
+ }
+ }
+
+ @Test
+ public void testHeadersResponse() throws URISyntaxException {
+ BaseResource.rewriteLocalhost = false;
+ data.startHTTPServer(new HeadersMockServer());
+ SyncStorageRecordRequest r = new SyncStorageRecordRequest(new URI(LOCAL_META_URL)); // URL re-used -- we need any successful response
+ TestHeadersSyncStorageRequestDelegate delegate = new TestHeadersSyncStorageRequestDelegate(authHeaderProvider);
+ r.delegate = delegate;
+ r.post(new JSONObject());
+ // Server is stopped in the callback.
+ }
+
+ public class DeleteMockServer extends MockServer {
+ @Override
+ public void handle(Request request, Response response) {
+ assertNotNull(request.getValue("x-confirm-delete"));
+ assertEquals("1", request.getValue("x-confirm-delete"));
+ super.handle(request, response);
+ }
+ }
+
+ @Test
+ public void testDelete() throws URISyntaxException {
+ BaseResource.rewriteLocalhost = false;
+ data.startHTTPServer(new DeleteMockServer());
+ SyncStorageRecordRequest r = new SyncStorageRecordRequest(new URI(LOCAL_META_URL)); // URL re-used -- we need any successful response
+ TestSyncStorageRequestDelegate delegate = new TestSyncStorageRequestDelegate(authHeaderProvider);
+ r.delegate = delegate;
+ r.delete();
+ // Server is stopped in the callback.
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/SynchronizerHelpers.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/SynchronizerHelpers.java
new file mode 100644
index 0000000000..f67f7e3348
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/SynchronizerHelpers.java
@@ -0,0 +1,282 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import android.content.Context;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.testhelpers.WBORepository;
+import org.mozilla.gecko.sync.repositories.FetchFailedException;
+import org.mozilla.gecko.sync.repositories.InactiveSessionException;
+import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
+import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
+import org.mozilla.gecko.sync.repositories.StoreFailedException;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import java.util.ArrayList;
+import java.util.concurrent.ExecutorService;
+
+public class SynchronizerHelpers {
+ public static final String FAIL_SENTINEL = "Fail";
+
+ /**
+ * Store one at a time, failing if the guid contains FAIL_SENTINEL.
+ */
+ public static class FailFetchWBORepository extends WBORepository {
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ delegate.deferredCreationDelegate().onSessionCreated(new WBORepositorySession(this) {
+ @Override
+ public void fetchSince(long timestamp,
+ final RepositorySessionFetchRecordsDelegate delegate) {
+ super.fetchSince(timestamp, new RepositorySessionFetchRecordsDelegate() {
+ @Override
+ public void onFetchedRecord(Record record) {
+ if (record.guid.contains(FAIL_SENTINEL)) {
+ delegate.onFetchFailed(new FetchFailedException(), record);
+ } else {
+ delegate.onFetchedRecord(record);
+ }
+ }
+
+ @Override
+ public void onFetchFailed(Exception ex, Record record) {
+ delegate.onFetchFailed(ex, record);
+ }
+
+ @Override
+ public void onFetchCompleted(long fetchEnd) {
+ delegate.onFetchCompleted(fetchEnd);
+ }
+
+ @Override
+ public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService executor) {
+ return this;
+ }
+ });
+ }
+ });
+ }
+ }
+
+ /**
+ * Store one at a time, failing if the guid contains FAIL_SENTINEL.
+ */
+ public static class SerialFailStoreWBORepository extends WBORepository {
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ delegate.deferredCreationDelegate().onSessionCreated(new WBORepositorySession(this) {
+ @Override
+ public void store(final Record record) throws NoStoreDelegateException {
+ if (delegate == null) {
+ throw new NoStoreDelegateException();
+ }
+ if (record.guid.contains(FAIL_SENTINEL)) {
+ delegate.onRecordStoreFailed(new StoreFailedException(), record.guid);
+ } else {
+ super.store(record);
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Store in batches, failing if any of the batch guids contains "Fail".
+ * <p>
+ * This will drop the final batch.
+ */
+ public static class BatchFailStoreWBORepository extends WBORepository {
+ public final int batchSize;
+ public ArrayList<Record> batch = new ArrayList<Record>();
+ public boolean batchShouldFail = false;
+
+ public class BatchFailStoreWBORepositorySession extends WBORepositorySession {
+ public BatchFailStoreWBORepositorySession(WBORepository repository) {
+ super(repository);
+ }
+
+ public void superStore(final Record record) throws NoStoreDelegateException {
+ super.store(record);
+ }
+
+ @Override
+ public void store(final Record record) throws NoStoreDelegateException {
+ if (delegate == null) {
+ throw new NoStoreDelegateException();
+ }
+ synchronized (batch) {
+ batch.add(record);
+ if (record.guid.contains("Fail")) {
+ batchShouldFail = true;
+ }
+
+ if (batch.size() >= batchSize) {
+ flush();
+ }
+ }
+ }
+
+ public void flush() {
+ final ArrayList<Record> thisBatch = new ArrayList<Record>(batch);
+ final boolean thisBatchShouldFail = batchShouldFail;
+ batchShouldFail = false;
+ batch.clear();
+ storeWorkQueue.execute(new Runnable() {
+ @Override
+ public void run() {
+ Logger.trace("XXX", "Notifying about batch. Failure? " + thisBatchShouldFail);
+ for (Record batchRecord : thisBatch) {
+ if (thisBatchShouldFail) {
+ delegate.onRecordStoreFailed(new StoreFailedException(), batchRecord.guid);
+ } else {
+ try {
+ superStore(batchRecord);
+ } catch (NoStoreDelegateException e) {
+ delegate.onRecordStoreFailed(e, batchRecord.guid);
+ }
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public void storeDone() {
+ synchronized (batch) {
+ flush();
+ // Do this in a Runnable so that the timestamp is grabbed after any upload.
+ final Runnable r = new Runnable() {
+ @Override
+ public void run() {
+ synchronized (batch) {
+ Logger.trace("XXX", "Calling storeDone.");
+ storeDone(now());
+ }
+ }
+ };
+ storeWorkQueue.execute(r);
+ }
+ }
+ }
+ public BatchFailStoreWBORepository(int batchSize) {
+ super();
+ this.batchSize = batchSize;
+ }
+
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ delegate.deferredCreationDelegate().onSessionCreated(new BatchFailStoreWBORepositorySession(this));
+ }
+ }
+
+ public static class TrackingWBORepository extends WBORepository {
+ @Override
+ public synchronized boolean shouldTrack() {
+ return true;
+ }
+ }
+
+ public static class BeginFailedException extends Exception {
+ private static final long serialVersionUID = -2349459755976915096L;
+ }
+
+ public static class FinishFailedException extends Exception {
+ private static final long serialVersionUID = -4644528423867070934L;
+ }
+
+ public static class BeginErrorWBORepository extends TrackingWBORepository {
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ delegate.deferredCreationDelegate().onSessionCreated(new BeginErrorWBORepositorySession(this));
+ }
+
+ public class BeginErrorWBORepositorySession extends WBORepositorySession {
+ public BeginErrorWBORepositorySession(WBORepository repository) {
+ super(repository);
+ }
+
+ @Override
+ public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException {
+ delegate.onBeginFailed(new BeginFailedException());
+ }
+ }
+ }
+
+ public static class FinishErrorWBORepository extends TrackingWBORepository {
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ delegate.deferredCreationDelegate().onSessionCreated(new FinishErrorWBORepositorySession(this));
+ }
+
+ public class FinishErrorWBORepositorySession extends WBORepositorySession {
+ public FinishErrorWBORepositorySession(WBORepository repository) {
+ super(repository);
+ }
+
+ @Override
+ public void finish(final RepositorySessionFinishDelegate delegate) throws InactiveSessionException {
+ delegate.onFinishFailed(new FinishFailedException());
+ }
+ }
+ }
+
+ public static class DataAvailableWBORepository extends TrackingWBORepository {
+ public boolean dataAvailable = true;
+
+ public DataAvailableWBORepository(boolean dataAvailable) {
+ this.dataAvailable = dataAvailable;
+ }
+
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ delegate.deferredCreationDelegate().onSessionCreated(new DataAvailableWBORepositorySession(this));
+ }
+
+ public class DataAvailableWBORepositorySession extends WBORepositorySession {
+ public DataAvailableWBORepositorySession(WBORepository repository) {
+ super(repository);
+ }
+
+ @Override
+ public boolean dataAvailable() {
+ return dataAvailable;
+ }
+ }
+ }
+
+ public static class ShouldSkipWBORepository extends TrackingWBORepository {
+ public boolean shouldSkip = true;
+
+ public ShouldSkipWBORepository(boolean shouldSkip) {
+ this.shouldSkip = shouldSkip;
+ }
+
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ delegate.deferredCreationDelegate().onSessionCreated(new ShouldSkipWBORepositorySession(this));
+ }
+
+ public class ShouldSkipWBORepositorySession extends WBORepositorySession {
+ public ShouldSkipWBORepositorySession(WBORepository repository) {
+ super(repository);
+ }
+
+ @Override
+ public boolean shouldSkip() {
+ return shouldSkip;
+ }
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCollectionKeys.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCollectionKeys.java
new file mode 100644
index 0000000000..76791a6edc
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCollectionKeys.java
@@ -0,0 +1,197 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import org.json.simple.JSONArray;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.CollectionKeys;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.NoCollectionKeysSetException;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Set;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+@RunWith(TestRunner.class)
+public class TestCollectionKeys {
+
+ @Test
+ public void testDefaultKeys() throws CryptoException, NoCollectionKeysSetException {
+ CollectionKeys ck = new CollectionKeys();
+ try {
+ ck.defaultKeyBundle();
+ fail("defaultKeys should throw.");
+ } catch (NoCollectionKeysSetException ex) {
+ // Good.
+ }
+ KeyBundle testKeys = KeyBundle.withRandomKeys();
+ ck.setDefaultKeyBundle(testKeys);
+ assertEquals(testKeys, ck.defaultKeyBundle());
+ }
+
+ @Test
+ public void testKeyForCollection() throws CryptoException, NoCollectionKeysSetException {
+ CollectionKeys ck = new CollectionKeys();
+ try {
+ ck.keyBundleForCollection("test");
+ fail("keyForCollection should throw.");
+ } catch (NoCollectionKeysSetException ex) {
+ // Good.
+ }
+ KeyBundle testKeys = KeyBundle.withRandomKeys();
+ KeyBundle otherKeys = KeyBundle.withRandomKeys();
+
+ ck.setDefaultKeyBundle(testKeys);
+ assertEquals(testKeys, ck.defaultKeyBundle());
+ assertEquals(testKeys, ck.keyBundleForCollection("test")); // Returns default.
+
+ ck.setKeyBundleForCollection("test", otherKeys);
+ assertEquals(otherKeys, ck.keyBundleForCollection("test")); // Returns default.
+
+ }
+
+ public static void assertSame(byte[] arrayOne, byte[] arrayTwo) {
+ assertTrue(Arrays.equals(arrayOne, arrayTwo));
+ }
+
+
+ @Test
+ public void testSetKeysFromWBO() throws IOException, NonObjectJSONException, CryptoException, NoCollectionKeysSetException {
+ String json = "{\"default\":[\"3fI6k1exImMgAKjilmMaAWxGqEIzFX/9K5EjEgH99vc=\",\"/AMaoCX4hzic28WY94XtokNi7N4T0nv+moS1y5wlbug=\"],\"collections\":{},\"collection\":\"crypto\",\"id\":\"keys\"}";
+ CryptoRecord rec = new CryptoRecord(json);
+
+ KeyBundle syncKeyBundle = new KeyBundle("slyjcrjednxd6rf4cr63vqilmkus6zbe", "6m8mv8ex2brqnrmsb9fjuvfg7y");
+ rec.keyBundle = syncKeyBundle;
+
+ rec.encrypt();
+ CollectionKeys ck = new CollectionKeys();
+ ck.setKeyPairsFromWBO(rec, syncKeyBundle);
+ byte[] input = "3fI6k1exImMgAKjilmMaAWxGqEIzFX/9K5EjEgH99vc=".getBytes("UTF-8");
+ byte[] expected = Base64.decodeBase64(input);
+ assertSame(expected, ck.defaultKeyBundle().getEncryptionKey());
+ }
+
+ @Test
+ public void testCryptoRecordFromCollectionKeys() throws CryptoException, NoCollectionKeysSetException, IOException, NonObjectJSONException {
+ CollectionKeys ck1 = CollectionKeys.generateCollectionKeys();
+ assertNotNull(ck1.defaultKeyBundle());
+ assertEquals(ck1.keyBundleForCollection("foobar"), ck1.defaultKeyBundle());
+ CryptoRecord rec = ck1.asCryptoRecord();
+ assertEquals(rec.collection, "crypto");
+ assertEquals(rec.guid, "keys");
+ JSONArray defaultKey = (JSONArray) rec.payload.get("default");
+
+ assertSame(Base64.decodeBase64((String) (defaultKey.get(0))), ck1.defaultKeyBundle().getEncryptionKey());
+ CollectionKeys ck2 = new CollectionKeys();
+ ck2.setKeyPairsFromWBO(rec, null);
+ assertSame(ck1.defaultKeyBundle().getEncryptionKey(), ck2.defaultKeyBundle().getEncryptionKey());
+ }
+
+ @Test
+ public void testCreateKeysBundle() throws CryptoException, NonObjectJSONException, IOException, NoCollectionKeysSetException {
+ String username = "b6evr62dptbxz7fvebek7btljyu322wp";
+ String friendlyBase32SyncKey = "basuxv2426eqj7frhvpcwkavdi";
+
+ KeyBundle syncKeyBundle = new KeyBundle(username, friendlyBase32SyncKey);
+
+ CollectionKeys ck = CollectionKeys.generateCollectionKeys();
+ CryptoRecord unencrypted = ck.asCryptoRecord();
+ unencrypted.keyBundle = syncKeyBundle;
+ CryptoRecord encrypted = unencrypted.encrypt();
+
+ CollectionKeys ckDecrypted = new CollectionKeys();
+ ckDecrypted.setKeyPairsFromWBO(encrypted, syncKeyBundle);
+
+ // Compare decrypted keys to the keys that were set upon creation
+ assertArrayEquals(ck.defaultKeyBundle().getEncryptionKey(), ckDecrypted.defaultKeyBundle().getEncryptionKey());
+ assertArrayEquals(ck.defaultKeyBundle().getHMACKey(), ckDecrypted.defaultKeyBundle().getHMACKey());
+ }
+
+ @Test
+ public void testDifferences() throws Exception {
+ KeyBundle kb1 = KeyBundle.withRandomKeys();
+ KeyBundle kb2 = KeyBundle.withRandomKeys();
+ KeyBundle kb3 = KeyBundle.withRandomKeys();
+ CollectionKeys a = CollectionKeys.generateCollectionKeys();
+ CollectionKeys b = CollectionKeys.generateCollectionKeys();
+ Set<String> diffs;
+
+ a.setKeyBundleForCollection("1", kb1);
+ b.setKeyBundleForCollection("1", kb1);
+ diffs = CollectionKeys.differences(a, b);
+ assertTrue(diffs.isEmpty());
+
+ a.setKeyBundleForCollection("2", kb2);
+ diffs = CollectionKeys.differences(a, b);
+ assertArrayEquals(new String[] { "2" }, diffs.toArray(new String[diffs.size()]));
+
+ b.setKeyBundleForCollection("3", kb3);
+ diffs = CollectionKeys.differences(a, b);
+ assertEquals(2, diffs.size());
+ assertTrue(diffs.contains("2"));
+ assertTrue(diffs.contains("3"));
+
+ b.setKeyBundleForCollection("1", KeyBundle.withRandomKeys());
+ diffs = CollectionKeys.differences(a, b);
+ assertEquals(3, diffs.size());
+
+ // This tests that explicitly setting a default key works.
+ a = CollectionKeys.generateCollectionKeys();
+ b = CollectionKeys.generateCollectionKeys();
+ b.setDefaultKeyBundle(a.defaultKeyBundle());
+ a.setKeyBundleForCollection("a", a.defaultKeyBundle());
+ b.setKeyBundleForCollection("b", b.defaultKeyBundle());
+ assertTrue(CollectionKeys.differences(a, b).isEmpty());
+ assertTrue(CollectionKeys.differences(b, a).isEmpty());
+ }
+
+ @Test
+ public void testEquals() throws Exception {
+ KeyBundle kb1 = KeyBundle.withRandomKeys();
+ KeyBundle kb2 = KeyBundle.withRandomKeys();
+ CollectionKeys a = CollectionKeys.generateCollectionKeys();
+ CollectionKeys b = CollectionKeys.generateCollectionKeys();
+
+ // Random keys are different.
+ assertFalse(a.equals(b));
+ assertFalse(b.equals(a));
+
+ // keys with unset default key bundles are different.
+ b.setDefaultKeyBundle(null);
+ assertFalse(a.equals(b));
+
+ // keys with equal default key bundles and no other collections are the same.
+ b.setDefaultKeyBundle(a.defaultKeyBundle());
+ assertTrue(a.equals(b));
+
+ // keys with equal defaults and equal collections are the same.
+ a.setKeyBundleForCollection("1", kb1);
+ b.setKeyBundleForCollection("1", kb1);
+ assertTrue(a.equals(b));
+
+ // keys with equal defaults but some collection missing are different.
+ a.setKeyBundleForCollection("2", kb2);
+ assertFalse(a.equals(b));
+ assertFalse(b.equals(a));
+
+ // keys with equal defaults and some collection set to the default are the same.
+ a.setKeyBundleForCollection("2", a.defaultKeyBundle());
+ b.setKeyBundleForCollection("3", b.defaultKeyBundle());
+ assertTrue(a.equals(b));
+ assertTrue(b.equals(a));
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCommandProcessor.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCommandProcessor.java
new file mode 100644
index 0000000000..adab2d7380
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCommandProcessor.java
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.CommandProcessor;
+import org.mozilla.gecko.sync.CommandRunner;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
+public class TestCommandProcessor extends CommandProcessor {
+
+ public static final String commandType = "displayURI";
+ public static final String commandWithNoArgs = "{\"command\":\"displayURI\"}";
+ public static final String commandWithNoType = "{\"args\":[\"https://bugzilla.mozilla.org/show_bug.cgi?id=731341\",\"PKsljsuqYbGg\"]}";
+ public static final String wellFormedCommand = "{\"args\":[\"https://bugzilla.mozilla.org/show_bug.cgi?id=731341\",\"PKsljsuqYbGg\"],\"command\":\"displayURI\"}";
+ public static final String wellFormedCommandWithNullArgs = "{\"args\":[\"https://bugzilla.mozilla.org/show_bug.cgi?id=731341\",null,\"PKsljsuqYbGg\",null],\"command\":\"displayURI\"}";
+
+ private boolean commandExecuted;
+
+ // Session is not used in these tests.
+ protected final GlobalSession session = null;
+
+ public class MockCommandRunner extends CommandRunner {
+ public MockCommandRunner(int argCount) {
+ super(argCount);
+ }
+
+ @Override
+ public void executeCommand(final GlobalSession session, List<String> args) {
+ commandExecuted = true;
+ }
+ }
+
+ @Test
+ public void testRegisterCommand() throws NonObjectJSONException, IOException {
+ assertNull(commands.get(commandType));
+ this.registerCommand(commandType, new MockCommandRunner(1));
+ assertNotNull(commands.get(commandType));
+ }
+
+ @Test
+ public void testProcessRegisteredCommand() throws NonObjectJSONException, IOException {
+ commandExecuted = false;
+ ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(wellFormedCommand);
+ this.registerCommand(commandType, new MockCommandRunner(1));
+ this.processCommand(session, unparsedCommand);
+ assertTrue(commandExecuted);
+ }
+
+ @Test
+ public void testProcessUnregisteredCommand() throws NonObjectJSONException, IOException {
+ commandExecuted = false;
+ ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(wellFormedCommand);
+ this.processCommand(session, unparsedCommand);
+ assertFalse(commandExecuted);
+ }
+
+ @Test
+ public void testProcessInvalidCommand() throws NonObjectJSONException, IOException {
+ ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(commandWithNoType);
+ this.registerCommand(commandType, new MockCommandRunner(1));
+ this.processCommand(session, unparsedCommand);
+ assertFalse(commandExecuted);
+ }
+
+ @Test
+ public void testParseCommandNoType() throws NonObjectJSONException, IOException {
+ ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(commandWithNoType);
+ assertNull(CommandProcessor.parseCommand(unparsedCommand));
+ }
+
+ @Test
+ public void testParseCommandNoArgs() throws NonObjectJSONException, IOException {
+ ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(commandWithNoArgs);
+ assertNull(CommandProcessor.parseCommand(unparsedCommand));
+ }
+
+ @Test
+ public void testParseWellFormedCommand() throws NonObjectJSONException, IOException {
+ ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(wellFormedCommand);
+ Command parsedCommand = CommandProcessor.parseCommand(unparsedCommand);
+ assertNotNull(parsedCommand);
+ assertEquals(2, parsedCommand.args.size());
+ assertEquals(commandType, parsedCommand.commandType);
+ }
+
+ @Test
+ public void testParseCommandNullArg() throws NonObjectJSONException, IOException {
+ ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(wellFormedCommandWithNullArgs);
+ Command parsedCommand = CommandProcessor.parseCommand(unparsedCommand);
+ assertNotNull(parsedCommand);
+ assertEquals(4, parsedCommand.args.size());
+ assertEquals(commandType, parsedCommand.commandType);
+ final List<String> expectedArgs = new ArrayList<String>();
+ expectedArgs.add("https://bugzilla.mozilla.org/show_bug.cgi?id=731341");
+ expectedArgs.add(null);
+ expectedArgs.add("PKsljsuqYbGg");
+ expectedArgs.add(null);
+ assertEquals(expectedArgs, parsedCommand.getArgsList());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCryptoRecord.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCryptoRecord.java
new file mode 100644
index 0000000000..a6b91eaf8a
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCryptoRecord.java
@@ -0,0 +1,302 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
+import org.mozilla.gecko.sync.repositories.domain.HistoryRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
+public class TestCryptoRecord {
+ String base64EncryptionKey = "9K/wLdXdw+nrTtXo4ZpECyHFNr4d7aYHqeg3KW9+m6Q=";
+ String base64HmacKey = "MMntEfutgLTc8FlTLQFms8/xMPmCldqPlq/QQXEjx70=";
+
+ @Test
+ public void testBaseCryptoRecordEncrypt() throws IOException, NonObjectJSONException, CryptoException {
+
+ ExtendedJSONObject clearPayload = new ExtendedJSONObject("{\"id\":\"5qRsgXWRJZXr\"," +
+ "\"title\":\"Index of file:///Users/jason/Library/Application " +
+ "Support/Firefox/Profiles/ksgd7wpk.LocalSyncServer/weave/logs/\"," +
+ "\"histUri\":\"file:///Users/jason/Library/Application%20Support/Firefox/Profiles" +
+ "/ksgd7wpk.LocalSyncServer/weave/logs/\",\"visits\":[{\"type\":1," +
+ "\"date\":1319149012372425}]}");
+
+ CryptoRecord record = new CryptoRecord();
+ record.payload = clearPayload;
+ String expectedGUID = "5qRsgXWRJZXr";
+ record.guid = expectedGUID;
+ record.keyBundle = KeyBundle.fromBase64EncodedKeys(base64EncryptionKey, base64HmacKey);
+ record.encrypt();
+ assertTrue(record.payload.get("title") == null);
+ assertTrue(record.payload.get("ciphertext") != null);
+ assertEquals(expectedGUID, record.guid);
+ assertEquals(expectedGUID, record.toJSONObject().get("id"));
+ record.decrypt();
+ assertEquals(expectedGUID, record.toJSONObject().get("id"));
+ }
+
+ @Test
+ public void testEntireRecord() throws Exception {
+ // Check a raw JSON blob from a real Sync account.
+ String inputString = "{\"sortindex\": 131, \"payload\": \"{\\\"ciphertext\\\":\\\"YJB4dr0vZEIWPirfU2FCJvfzeSLiOP5QWasol2R6ILUxdHsJWuUuvTZVhxYQfTVNou6hVV67jfAvi5Cs+bqhhQsv7icZTiZhPTiTdVGt+uuMotxauVA5OryNGVEZgCCTvT3upzhDFdDbJzVd9O3/gU/b7r/CmAHykX8bTlthlbWeZ8oz6gwHJB5tPRU15nM/m/qW1vyKIw5pw/ZwtAy630AieRehGIGDk+33PWqsfyuT4EUFY9/Ly+8JlnqzxfiBCunIfuXGdLuqTjJOxgrK8mI4wccRFEdFEnmHvh5x7fjl1ID52qumFNQl8zkB75C8XK25alXqwvRR6/AQSP+BgQ==\\\",\\\"IV\\\":\\\"v/0BFgicqYQsd70T39rraA==\\\",\\\"hmac\\\":\\\"59605ed696f6e0e6e062a03510cff742bf6b50d695c042e8372a93f4c2d37dac\\\"}\", \"id\": \"0-P9fabp9vJD\", \"modified\": 1326254123.65}";
+ CryptoRecord record = CryptoRecord.fromJSONRecord(inputString);
+ assertEquals("0-P9fabp9vJD", record.guid);
+ assertEquals(1326254123650L, record.lastModified);
+ assertEquals(131, record.sortIndex);
+
+ String b64E = "0A7mU5SZ/tu7ZqwXW1og4qHVHN+zgEi4Xwfwjw+vEJw=";
+ String b64H = "11GN34O9QWXkjR06g8t0gWE1sGgQeWL0qxxWwl8Dmxs=";
+ record.keyBundle = KeyBundle.fromBase64EncodedKeys(b64E, b64H);
+ record.decrypt();
+
+ assertEquals("0-P9fabp9vJD", record.guid);
+ assertEquals(1326254123650L, record.lastModified);
+ assertEquals(131, record.sortIndex);
+
+ assertEquals("Customize Firefox", record.payload.get("title"));
+ assertEquals("0-P9fabp9vJD", record.payload.get("id"));
+ assertTrue(record.payload.get("tags") instanceof JSONArray);
+ }
+
+ @Test
+ public void testBaseCryptoRecordDecrypt() throws Exception {
+ String base64CipherText =
+ "NMsdnRulLwQsVcwxKW9XwaUe7ouJk5Wn"
+ + "80QhbD80l0HEcZGCynh45qIbeYBik0lg"
+ + "cHbKmlIxTJNwU+OeqipN+/j7MqhjKOGI"
+ + "lvbpiPQQLC6/ffF2vbzL0nzMUuSyvaQz"
+ + "yGGkSYM2xUFt06aNivoQTvU2GgGmUK6M"
+ + "vadoY38hhW2LCMkoZcNfgCqJ26lO1O0s"
+ + "EO6zHsk3IVz6vsKiJ2Hq6VCo7hu123wN"
+ + "egmujHWQSGyf8JeudZjKzfi0OFRRvvm4"
+ + "QAKyBWf0MgrW1F8SFDnVfkq8amCB7Nhd"
+ + "whgLWbN+21NitNwWYknoEWe1m6hmGZDg"
+ + "DT32uxzWxCV8QqqrpH/ZggViEr9uMgoy"
+ + "4lYaWqP7G5WKvvechc62aqnsNEYhH26A"
+ + "5QgzmlNyvB+KPFvPsYzxDnSCjOoRSLx7"
+ + "GG86wT59QZw=";
+ String base64IV = "GX8L37AAb2FZJMzIoXlX8w==";
+ String base16Hmac =
+ "b1e6c18ac30deb70236bc0d65a46f7a4"
+ + "dce3b8b0e02cf92182b914e3afa5eebc";
+
+ ExtendedJSONObject body = new ExtendedJSONObject();
+ ExtendedJSONObject payload = new ExtendedJSONObject();
+ payload.put("ciphertext", base64CipherText);
+ payload.put("IV", base64IV);
+ payload.put("hmac", base16Hmac);
+ body.put("payload", payload.toJSONString());
+ CryptoRecord record = CryptoRecord.fromJSONRecord(body);
+ byte[] decodedKey = Base64.decodeBase64(base64EncryptionKey.getBytes("UTF-8"));
+ byte[] decodedHMAC = Base64.decodeBase64(base64HmacKey.getBytes("UTF-8"));
+ record.keyBundle = new KeyBundle(decodedKey, decodedHMAC);
+
+ record.decrypt();
+ String id = (String) record.payload.get("id");
+ assertTrue(id.equals("5qRsgXWRJZXr"));
+ }
+
+ @Test
+ public void testBaseCryptoRecordSyncKeyBundle() throws UnsupportedEncodingException, CryptoException {
+ // These values pulled straight out of Firefox.
+ String key = "6m8mv8ex2brqnrmsb9fjuvfg7y";
+ String user = "c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd";
+
+ // Check our friendly base32 decoding.
+ assertTrue(Arrays.equals(Utils.decodeFriendlyBase32(key), Base64.decodeBase64("8xbKrJfQYwbFkguKmlSm/g==".getBytes("UTF-8"))));
+ KeyBundle bundle = new KeyBundle(user, key);
+ String expectedEncryptKeyBase64 = "/8RzbFT396htpZu5rwgIg2WKfyARgm7dLzsF5pwrVz8=";
+ String expectedHMACKeyBase64 = "NChGjrqoXYyw8vIYP2334cvmMtsjAMUZNqFwV2LGNkM=";
+ byte[] computedEncryptKey = bundle.getEncryptionKey();
+ byte[] computedHMACKey = bundle.getHMACKey();
+ assertTrue(Arrays.equals(computedEncryptKey, Base64.decodeBase64(expectedEncryptKeyBase64.getBytes("UTF-8"))));
+ assertTrue(Arrays.equals(computedHMACKey, Base64.decodeBase64(expectedHMACKeyBase64.getBytes("UTF-8"))));
+ }
+
+ @Test
+ public void testDecrypt() throws Exception {
+ String jsonInput = "{\"sortindex\": 90, \"payload\":" +
+ "\"{\\\"ciphertext\\\":\\\"F4ukf0" +
+ "LM+vhffiKyjaANXeUhfmOPPmQYX1XBoG" +
+ "Rh1LiHeKHB5rqjhzd7yAoxqgmFnkIgQF" +
+ "YPSqRAoCxWiAeGULTX+KM4MU5drbNyR/" +
+ "690JBWSyE1vQSiMGwNIbTKnOLGHKkQVY" +
+ "HDpajg5BNFfvHNQ5Jx7uM9uJcmuEjCI6" +
+ "GRMDKyKjhsTqCd99MONkY5rISutaWQ0e" +
+ "EXFgpA9RZPv4jgWlQhe+YrVnpcrTi20b" +
+ "NgKp3IfIeqEelrZ5FJd2WGZOA021d3e7" +
+ "P3Z4qptefH4Q9/hySrWsELWngBaydyn/" +
+ "IjsheZuKra3kJSST/4SvRZ7qXn\\\",\\" +
+ "\"IV\\\":\\\"GadPajeXhpk75K2YH+L" +
+ "y4w==\\\",\\\"hmac\\\":\\\"71442" +
+ "d946502e3ca475c70a633d3d37f4b4e9" +
+ "313a6d1041d0c0550cd354e7605\\\"}" +
+ "\", \"id\": \"hkZYpC-BH4Xi\", \"" +
+ "modified\": 1320183464.21}";
+ String base64EncryptionKey = "K8fV6PHG8RgugfHexGesbzTeOs2o12cr" +
+ "N/G3bz0Bx1M=";
+ String base64HmacKey = "nbceuI6w1RJbBzh+iCJHEs8p4lElsOma" +
+ "yUhx+OztVgM=";
+ String expectedDecryptedText = "{\"id\":\"hkZYpC-BH4Xi\",\"histU" +
+ "ri\":\"http://hathology.com/2008" +
+ "/06/how-to-edit-your-path-enviro" +
+ "nment-variables-on-mac-os-x/\",\"" +
+ "title\":\"How To Edit Your PATH " +
+ "Environment Variables On Mac OS " +
+ "X\",\"visits\":[{\"date\":131898" +
+ "2074310889,\"type\":1}]}";
+
+ KeyBundle keyBundle = KeyBundle.fromBase64EncodedKeys(base64EncryptionKey, base64HmacKey);
+
+ CryptoRecord encrypted = CryptoRecord.fromJSONRecord(jsonInput);
+ encrypted.keyBundle = keyBundle;
+ CryptoRecord decrypted = encrypted.decrypt();
+
+ // We don't necessarily produce exactly the same JSON but we do have the same values.
+ ExtendedJSONObject expectedJson = new ExtendedJSONObject(expectedDecryptedText);
+ assertEquals(expectedJson.get("id"), decrypted.payload.get("id"));
+ assertEquals(expectedJson.get("title"), decrypted.payload.get("title"));
+ assertEquals(expectedJson.get("histUri"), decrypted.payload.get("histUri"));
+ }
+
+ @Test
+ public void testEncryptDecrypt() throws Exception {
+ String originalText = "{\"id\":\"hkZYpC-BH4Xi\",\"histU" +
+ "ri\":\"http://hathology.com/2008" +
+ "/06/how-to-edit-your-path-enviro" +
+ "nment-variables-on-mac-os-x/\",\"" +
+ "title\":\"How To Edit Your PATH " +
+ "Environment Variables On Mac OS " +
+ "X\",\"visits\":[{\"date\":131898" +
+ "2074310889,\"type\":1}]}";
+ String base64EncryptionKey = "K8fV6PHG8RgugfHexGesbzTeOs2o12cr" +
+ "N/G3bz0Bx1M=";
+ String base64HmacKey = "nbceuI6w1RJbBzh+iCJHEs8p4lElsOma" +
+ "yUhx+OztVgM=";
+
+ KeyBundle keyBundle = KeyBundle.fromBase64EncodedKeys(base64EncryptionKey, base64HmacKey);
+
+ // Encrypt.
+ CryptoRecord unencrypted = new CryptoRecord(originalText);
+ unencrypted.keyBundle = keyBundle;
+ CryptoRecord encrypted = unencrypted.encrypt();
+
+ // Decrypt after round-trip through JSON.
+ CryptoRecord undecrypted = CryptoRecord.fromJSONRecord(encrypted.toJSONString());
+ undecrypted.keyBundle = keyBundle;
+ CryptoRecord decrypted = undecrypted.decrypt();
+
+ // We don't necessarily produce exactly the same JSON but we do have the same values.
+ ExtendedJSONObject expectedJson = new ExtendedJSONObject(originalText);
+ assertEquals(expectedJson.get("id"), decrypted.payload.get("id"));
+ assertEquals(expectedJson.get("title"), decrypted.payload.get("title"));
+ assertEquals(expectedJson.get("histUri"), decrypted.payload.get("histUri"));
+ }
+
+ @Test
+ public void testDecryptKeysBundle() throws Exception {
+ String jsonInput = "{\"payload\": \"{\\\"ciphertext\\" +
+ "\":\\\"L1yRyZBkVYKXC1cTpeUqqfmKg" +
+ "CinYV9YntGiG0PfYZSTLQ2s86WPI0VBb" +
+ "QbLZfx7udk6sf6CFE4w5EgiPx0XP3Fbj" +
+ "L7r4qIT0vjbAOrLKedZwA3cgiquc+PXM" +
+ "Etml8B4Dfm0crJK0iROlRkb+lePAYkzI" +
+ "iQn5Ba8mSWQEFoLy3zAcfCYXumA7E0Fj" +
+ "XYD+TqTG5bqYJY4zvPaB9mn9y3WHw==\\" +
+ "\",\\\"IV\\\":\\\"Jjb2oVI5uvvFfm" +
+ "ZYRY4GaA==\\\",\\\"hmac\\\":\\\"" +
+ "0b59731cb1aaedc85f54917b7058f361" +
+ "60826b70050b0d70cd42b0b609b1d717" +
+ "\\\"}\", \"id\": \"keys\", \"mod" +
+ "ified\": 1320183463.91}";
+ String username = "b6evr62dptbxz7fvebek7btljyu322wp";
+ String friendlyBase32SyncKey = "basuxv2426eqj7frhvpcwkavdi";
+ String expectedDecryptedText = "{\"default\":[\"K8fV6PHG8RgugfHe" +
+ "xGesbzTeOs2o12crN/G3bz0Bx1M=\",\"" +
+ "nbceuI6w1RJbBzh+iCJHEs8p4lElsOma" +
+ "yUhx+OztVgM=\"],\"collections\":" +
+ "{},\"collection\":\"crypto\",\"i" +
+ "d\":\"keys\"}";
+ String expectedBase64EncryptionKey = "K8fV6PHG8RgugfHexGesbzTeOs2o12cr" +
+ "N/G3bz0Bx1M=";
+ String expectedBase64HmacKey = "nbceuI6w1RJbBzh+iCJHEs8p4lElsOma" +
+ "yUhx+OztVgM=";
+
+ KeyBundle syncKeyBundle = new KeyBundle(username, friendlyBase32SyncKey);
+
+ ExtendedJSONObject json = new ExtendedJSONObject(jsonInput);
+ assertEquals("keys", json.get("id"));
+
+ CryptoRecord encrypted = CryptoRecord.fromJSONRecord(jsonInput);
+ encrypted.keyBundle = syncKeyBundle;
+ CryptoRecord decrypted = encrypted.decrypt();
+
+ // We don't necessarily produce exactly the same JSON but we do have the same values.
+ ExtendedJSONObject expectedJson = new ExtendedJSONObject(expectedDecryptedText);
+ assertEquals(expectedJson.get("id"), decrypted.payload.get("id"));
+ assertEquals(expectedJson.get("default"), decrypted.payload.get("default"));
+ assertEquals(expectedJson.get("collection"), decrypted.payload.get("collection"));
+ assertEquals(expectedJson.get("collections"), decrypted.payload.get("collections"));
+
+ // Check that the extracted keys were as expected.
+ JSONArray keys = new ExtendedJSONObject(decrypted.payload.toJSONString()).getArray("default");
+ KeyBundle keyBundle = KeyBundle.fromBase64EncodedKeys((String)keys.get(0), (String)keys.get(1));
+
+ assertArrayEquals(Base64.decodeBase64(expectedBase64EncryptionKey.getBytes("UTF-8")), keyBundle.getEncryptionKey());
+ assertArrayEquals(Base64.decodeBase64(expectedBase64HmacKey.getBytes("UTF-8")), keyBundle.getHMACKey());
+ }
+
+ @Test
+ public void testTTL() throws UnsupportedEncodingException, CryptoException {
+ Record historyRecord = new HistoryRecord();
+ CryptoRecord cryptoRecord = historyRecord.getEnvelope();
+ assertEquals(historyRecord.ttl, cryptoRecord.ttl);
+
+ // Very important that ttls are set in outbound envelopes.
+ JSONObject o = cryptoRecord.toJSONObject();
+ assertEquals(cryptoRecord.ttl, o.get("ttl"));
+
+ // Most important of all, outbound encrypted record envelopes.
+ KeyBundle keyBundle = KeyBundle.withRandomKeys();
+ cryptoRecord.keyBundle = keyBundle;
+ cryptoRecord.encrypt();
+ assertEquals(historyRecord.ttl, cryptoRecord.ttl); // Should be preserved.
+ o = cryptoRecord.toJSONObject();
+ assertEquals(cryptoRecord.ttl, o.get("ttl"));
+
+ // But we should ignore negative ttls.
+ Record clientRecord = new ClientRecord();
+ clientRecord.ttl = -1; // Don't ttl this record.
+ o = clientRecord.getEnvelope().toJSONObject();
+ assertNull(o.get("ttl"));
+
+ // But we should ignore negative ttls in outbound encrypted record envelopes.
+ cryptoRecord = clientRecord.getEnvelope();
+ cryptoRecord.keyBundle = keyBundle;
+ cryptoRecord.encrypt();
+ o = cryptoRecord.toJSONObject();
+ assertNull(o.get("ttl"));
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecord.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecord.java
new file mode 100644
index 0000000000..473534aac5
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecord.java
@@ -0,0 +1,330 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.db.Tab;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
+import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
+import org.mozilla.gecko.sync.repositories.domain.HistoryRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+import org.mozilla.gecko.sync.repositories.domain.RecordParseException;
+import org.mozilla.gecko.sync.repositories.domain.TabsRecord;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+@RunWith(TestRunner.class)
+public class TestRecord {
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testQueryRecord() throws NonObjectJSONException, IOException {
+ final String expectedGUID = "Bl3n3gpKag3s";
+ final String testRecord =
+ "{\"id\":\"" + expectedGUID + "\"," +
+ " \"type\":\"query\"," +
+ " \"title\":\"Downloads\"," +
+ " \"parentName\":\"\"," +
+ " \"bmkUri\":\"place:transition=7&sort=4\"," +
+ " \"tags\":[]," +
+ " \"keyword\":null," +
+ " \"description\":null," +
+ " \"loadInSidebar\":false," +
+ " \"parentid\":\"BxfRgGiNeITG\"}";
+
+ final ExtendedJSONObject o = new ExtendedJSONObject(testRecord);
+ final CryptoRecord cr = new CryptoRecord(o);
+ cr.guid = expectedGUID;
+ cr.lastModified = System.currentTimeMillis();
+ cr.collection = "bookmarks";
+
+ final BookmarkRecord r = new BookmarkRecord("Bl3n3gpKag3s", "bookmarks");
+ r.initFromEnvelope(cr);
+ assertEquals(expectedGUID, r.guid);
+ assertEquals("query", r.type);
+ assertEquals("places:uri=place%3Atransition%3D7%26sort%3D4", r.bookmarkURI);
+
+ // Check that we get the same bookmark URI out the other end,
+ // once we've parsed it into a CryptoRecord, a BookmarkRecord, then
+ // back into a CryptoRecord.
+ assertEquals("place:transition=7&sort=4", r.getEnvelope().payload.getString("bmkUri"));
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testRecordGUIDs() {
+ for (int i = 0; i < 50; ++i) {
+ CryptoRecord cryptoRecord = new HistoryRecord().getEnvelope();
+ assertEquals(12, cryptoRecord.guid.length());
+ }
+ }
+
+ @Test
+ public void testRecordEquality() {
+ long now = System.currentTimeMillis();
+ BookmarkRecord bOne = new BookmarkRecord("abcdefghijkl", "bookmarks", now , false);
+ BookmarkRecord bTwo = new BookmarkRecord("abcdefghijkl", "bookmarks", now , false);
+ HistoryRecord hOne = new HistoryRecord("mbcdefghijkm", "history", now , false);
+ HistoryRecord hTwo = new HistoryRecord("mbcdefghijkm", "history", now , false);
+
+ // Identical records.
+ assertFalse(bOne == bTwo);
+ assertTrue(bOne.equals(bTwo));
+ assertTrue(bOne.equalPayloads(bTwo));
+ assertTrue(bOne.congruentWith(bTwo));
+ assertTrue(bTwo.equals(bOne));
+ assertTrue(bTwo.equalPayloads(bOne));
+ assertTrue(bTwo.congruentWith(bOne));
+
+ // Null checking.
+ assertFalse(bOne.equals(null));
+ assertFalse(bOne.equalPayloads(null));
+ assertFalse(bOne.congruentWith(null));
+
+ // Different types.
+ hOne.guid = bOne.guid;
+ assertFalse(bOne.equals(hOne));
+ assertFalse(bOne.equalPayloads(hOne));
+ assertFalse(bOne.congruentWith(hOne));
+ hOne.guid = hTwo.guid;
+
+ // Congruent androidID.
+ bOne.androidID = 1;
+ assertFalse(bOne.equals(bTwo));
+ assertTrue(bOne.equalPayloads(bTwo));
+ assertTrue(bOne.congruentWith(bTwo));
+ assertFalse(bTwo.equals(bOne));
+ assertTrue(bTwo.equalPayloads(bOne));
+ assertTrue(bTwo.congruentWith(bOne));
+
+ // Non-congruent androidID.
+ bTwo.androidID = 2;
+ assertFalse(bOne.equals(bTwo));
+ assertTrue(bOne.equalPayloads(bTwo));
+ assertFalse(bOne.congruentWith(bTwo));
+ assertFalse(bTwo.equals(bOne));
+ assertTrue(bTwo.equalPayloads(bOne));
+ assertFalse(bTwo.congruentWith(bOne));
+
+ // Identical androidID.
+ bOne.androidID = 2;
+ assertTrue(bOne.equals(bTwo));
+ assertTrue(bOne.equalPayloads(bTwo));
+ assertTrue(bOne.congruentWith(bTwo));
+ assertTrue(bTwo.equals(bOne));
+ assertTrue(bTwo.equalPayloads(bOne));
+ assertTrue(bTwo.congruentWith(bOne));
+
+ // Different times.
+ bTwo.lastModified += 1000;
+ assertFalse(bOne.equals(bTwo));
+ assertTrue(bOne.equalPayloads(bTwo));
+ assertTrue(bOne.congruentWith(bTwo));
+ assertFalse(bTwo.equals(bOne));
+ assertTrue(bTwo.equalPayloads(bOne));
+ assertTrue(bTwo.congruentWith(bOne));
+
+ // Add some visits.
+ JSONObject v1 = fakeVisit(now - 1000);
+ JSONObject v2 = fakeVisit(now - 500);
+
+ hOne.fennecDateVisited = now + 2000;
+ hOne.fennecVisitCount = 1;
+ assertFalse(hOne.equals(hTwo));
+ assertTrue(hOne.equalPayloads(hTwo));
+ assertTrue(hOne.congruentWith(hTwo));
+ addVisit(hOne, v1);
+ assertFalse(hOne.equals(hTwo));
+ assertFalse(hOne.equalPayloads(hTwo));
+ assertTrue(hOne.congruentWith(hTwo));
+ addVisit(hTwo, v2);
+ assertFalse(hOne.equals(hTwo));
+ assertFalse(hOne.equalPayloads(hTwo));
+ assertTrue(hOne.congruentWith(hTwo));
+
+ // Now merge the visits.
+ addVisit(hTwo, v1);
+ addVisit(hOne, v2);
+ assertFalse(hOne.equals(hTwo));
+ assertTrue(hOne.equalPayloads(hTwo));
+ assertTrue(hOne.congruentWith(hTwo));
+ hTwo.fennecDateVisited = hOne.fennecDateVisited;
+ hTwo.fennecVisitCount = hOne.fennecVisitCount = 2;
+ assertTrue(hOne.equals(hTwo));
+ assertTrue(hOne.equalPayloads(hTwo));
+ assertTrue(hOne.congruentWith(hTwo));
+ }
+
+ @SuppressWarnings("unchecked")
+ private void addVisit(HistoryRecord r, JSONObject visit) {
+ if (r.visits == null) {
+ r.visits = new JSONArray();
+ }
+ r.visits.add(visit);
+ }
+
+ @SuppressWarnings("unchecked")
+ private JSONObject fakeVisit(long time) {
+ JSONObject object = new JSONObject();
+ object.put("type", 1L);
+ object.put("date", time * 1000);
+ return object;
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testTabParsing() throws Exception {
+ String json = "{\"title\":\"mozilla-central mozilla/browser/base/content/syncSetup.js\"," +
+ " \"urlHistory\":[\"http://mxr.mozilla.org/mozilla-central/source/browser/base/content/syncSetup.js#72\"]," +
+ " \"icon\":\"http://mxr.mozilla.org/mxr.png\"," +
+ " \"lastUsed\":\"1306374531\"}";
+ Tab tab = TabsRecord.tabFromJSONObject(new ExtendedJSONObject(json).object);
+
+ assertEquals("mozilla-central mozilla/browser/base/content/syncSetup.js", tab.title);
+ assertEquals("http://mxr.mozilla.org/mxr.png", tab.icon);
+ assertEquals("http://mxr.mozilla.org/mozilla-central/source/browser/base/content/syncSetup.js#72", tab.history.get(0));
+ assertEquals(1306374531000L, tab.lastUsed);
+
+ String zeroJSON = "{\"title\":\"a\"," +
+ " \"urlHistory\":[\"http://example.com\"]," +
+ " \"icon\":\"\"," +
+ " \"lastUsed\":0}";
+ Tab zero = TabsRecord.tabFromJSONObject(new ExtendedJSONObject(zeroJSON).object);
+
+ assertEquals("a", zero.title);
+ assertEquals("", zero.icon);
+ assertEquals("http://example.com", zero.history.get(0));
+ assertEquals(0L, zero.lastUsed);
+ }
+
+ @SuppressWarnings({ "unchecked", "static-method" })
+ @Test
+ public void testTabsRecordCreation() throws Exception {
+ final TabsRecord record = new TabsRecord("testGuid");
+ record.clientName = "test client name";
+
+ final JSONArray history1 = new JSONArray();
+ history1.add("http://test.com/test1.html");
+ final Tab tab1 = new Tab("test title 1", "http://test.com/test1.png", history1, 1000);
+
+ final JSONArray history2 = new JSONArray();
+ history2.add("http://test.com/test2.html#1");
+ history2.add("http://test.com/test2.html#2");
+ history2.add("http://test.com/test2.html#3");
+ final Tab tab2 = new Tab("test title 2", "http://test.com/test2.png", history2, 2000);
+
+ record.tabs = new ArrayList<Tab>();
+ record.tabs.add(tab1);
+ record.tabs.add(tab2);
+
+ final TabsRecord parsed = new TabsRecord();
+ parsed.initFromEnvelope(CryptoRecord.fromJSONRecord(record.getEnvelope().toJSONString()));
+
+ assertEquals(record.guid, parsed.guid);
+ assertEquals(record.clientName, parsed.clientName);
+ assertEquals(record.tabs, parsed.tabs);
+
+ // Verify that equality test doesn't always return true.
+ parsed.tabs.get(0).history.add("http://test.com/different.html");
+ assertFalse(record.tabs.equals(parsed.tabs));
+ }
+
+ public static class URITestBookmarkRecord extends BookmarkRecord {
+ public static void doTest() {
+ assertEquals("places:uri=abc%26def+baz&p1=123&p2=bar+baz",
+ encodeUnsupportedTypeURI("abc&def baz", "p1", "123", "p2", "bar baz"));
+ assertEquals("places:uri=abc%26def+baz&p1=123",
+ encodeUnsupportedTypeURI("abc&def baz", "p1", "123", null, "bar baz"));
+ assertEquals("places:p1=123",
+ encodeUnsupportedTypeURI(null, "p1", "123", "p2", null));
+ }
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testEncodeURI() {
+ URITestBookmarkRecord.doTest();
+ }
+
+ private static final String payload =
+ "{\"id\":\"M5bwUKK8hPyF\"," +
+ "\"type\":\"livemark\"," +
+ "\"siteUri\":\"http://www.bbc.co.uk/go/rss/int/news/-/news/\"," +
+ "\"feedUri\":\"http://fxfeeds.mozilla.com/en-US/firefox/headlines.xml\"," +
+ "\"parentName\":\"Bookmarks Toolbar\"," +
+ "\"parentid\":\"toolbar\"," +
+ "\"title\":\"Latest Headlines\"," +
+ "\"description\":\"\"," +
+ "\"children\":" +
+ "[\"7oBdEZB-8BMO\", \"SUd1wktMNCTB\", \"eZe4QWzo1BcY\", \"YNBhGwhVnQsN\"," +
+ "\"mNTdpgoRZMbW\", \"-L8Vci6CbkJY\", \"bVzudKSQERc1\", \"Gxl9lb4DXsmL\"," +
+ "\"3Qr13GucOtEh\"]}";
+
+ public class PayloadBookmarkRecord extends BookmarkRecord {
+ public PayloadBookmarkRecord() {
+ super("abcdefghijkl", "bookmarks", 1234, false);
+ }
+
+ public void doTest() throws NonObjectJSONException, IOException {
+ this.initFromPayload(new ExtendedJSONObject(payload));
+ assertEquals("abcdefghijkl", this.guid); // Ignores payload.
+ assertEquals("livemark", this.type);
+ assertEquals("Bookmarks Toolbar", this.parentName);
+ assertEquals("toolbar", this.parentID);
+ assertEquals("", this.description);
+ assertEquals(null, this.children);
+
+ final String encodedSite = "http%3A%2F%2Fwww.bbc.co.uk%2Fgo%2Frss%2Fint%2Fnews%2F-%2Fnews%2F";
+ final String encodedFeed = "http%3A%2F%2Ffxfeeds.mozilla.com%2Fen-US%2Ffirefox%2Fheadlines.xml";
+ final String expectedURI = "places:siteUri=" + encodedSite + "&feedUri=" + encodedFeed;
+ assertEquals(expectedURI, this.bookmarkURI);
+ }
+ }
+
+ @Test
+ public void testUnusualBookmarkRecords() throws NonObjectJSONException, IOException {
+ PayloadBookmarkRecord record = new PayloadBookmarkRecord();
+ record.doTest();
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testTTL() {
+ Record record = new HistoryRecord();
+ assertEquals(HistoryRecord.HISTORY_TTL, record.ttl);
+
+ // ClientRecords are transient, HistoryRecords are not.
+ Record clientRecord = new ClientRecord();
+ assertTrue(clientRecord.ttl < record.ttl);
+
+ CryptoRecord cryptoRecord = record.getEnvelope();
+ assertEquals(record.ttl, cryptoRecord.ttl);
+ }
+
+ @Test
+ public void testStringModified() throws Exception {
+ // modified member is a string, expected a floating point number with 2
+ // decimal digits.
+ String badJson = "{\"sortindex\":\"0\",\"payload\":\"{\\\"syncID\\\":\\\"ZJOqMBjhBthH\\\",\\\"storageVersion\\\":5,\\\"engines\\\":{\\\"clients\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"4oTBXG20rJH5\\\"},\\\"bookmarks\\\":{\\\"version\\\":2,\\\"syncID\\\":\\\"JiMJXy8xI3fr\\\"},\\\"forms\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"J17vSloroXBU\\\"},\\\"history\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"y1HgpbSc3LJT\\\"},\\\"passwords\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"v3y-RidcCuT5\\\"},\\\"prefs\\\":{\\\"version\\\":2,\\\"syncID\\\":\\\"LvfqmT7cUUm4\\\"},\\\"tabs\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"MKMRlBah2d9D\\\"},\\\"addons\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"Ih2hhRrcGjh4\\\"}}}\",\"id\":\"global\",\"modified\":\"1370689360.28\"}";
+ try {
+ CryptoRecord.fromJSONRecord(badJson);
+ fail("Expected exception.");
+ } catch (Exception e) {
+ assertTrue(e instanceof RecordParseException);
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecordsChannel.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecordsChannel.java
new file mode 100644
index 0000000000..69d3c32e7d
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecordsChannel.java
@@ -0,0 +1,229 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.android.sync.test.SynchronizerHelpers.FailFetchWBORepository;
+import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionCreationDelegate;
+import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionFinishDelegate;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.background.testhelpers.WBORepository;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.repositories.InactiveSessionException;
+import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
+import org.mozilla.gecko.sync.synchronizer.RecordsChannel;
+import org.mozilla.gecko.sync.synchronizer.RecordsChannelDelegate;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
+public class TestRecordsChannel {
+
+ protected WBORepository remote;
+ protected WBORepository local;
+
+ protected RepositorySession source;
+ protected RepositorySession sink;
+ protected RecordsChannelDelegate rcDelegate;
+
+ protected AtomicInteger numFlowFetchFailed;
+ protected AtomicInteger numFlowStoreFailed;
+ protected AtomicInteger numFlowCompleted;
+ protected AtomicBoolean flowBeginFailed;
+ protected AtomicBoolean flowFinishFailed;
+
+ public void doFlow(final Repository remote, final Repository local) throws Exception {
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ remote.createSession(new ExpectSuccessRepositorySessionCreationDelegate(WaitHelper.getTestWaiter()) {
+ @Override
+ public void onSessionCreated(RepositorySession session) {
+ source = session;
+ local.createSession(new ExpectSuccessRepositorySessionCreationDelegate(WaitHelper.getTestWaiter()) {
+ @Override
+ public void onSessionCreated(RepositorySession session) {
+ sink = session;
+ WaitHelper.getTestWaiter().performNotify();
+ }
+ }, null);
+ }
+ }, null);
+ }
+ });
+
+ assertNotNull(source);
+ assertNotNull(sink);
+
+ numFlowFetchFailed = new AtomicInteger(0);
+ numFlowStoreFailed = new AtomicInteger(0);
+ numFlowCompleted = new AtomicInteger(0);
+ flowBeginFailed = new AtomicBoolean(false);
+ flowFinishFailed = new AtomicBoolean(false);
+
+ rcDelegate = new RecordsChannelDelegate() {
+ @Override
+ public void onFlowFetchFailed(RecordsChannel recordsChannel, Exception ex) {
+ numFlowFetchFailed.incrementAndGet();
+ }
+
+ @Override
+ public void onFlowStoreFailed(RecordsChannel recordsChannel, Exception ex, String recordGuid) {
+ numFlowStoreFailed.incrementAndGet();
+ }
+
+ @Override
+ public void onFlowFinishFailed(RecordsChannel recordsChannel, Exception ex) {
+ flowFinishFailed.set(true);
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ @Override
+ public void onFlowCompleted(RecordsChannel recordsChannel, long fetchEnd, long storeEnd) {
+ numFlowCompleted.incrementAndGet();
+ try {
+ sink.finish(new ExpectSuccessRepositorySessionFinishDelegate(WaitHelper.getTestWaiter()) {
+ @Override
+ public void onFinishSucceeded(RepositorySession session, RepositorySessionBundle bundle) {
+ try {
+ source.finish(new ExpectSuccessRepositorySessionFinishDelegate(WaitHelper.getTestWaiter()) {
+ @Override
+ public void onFinishSucceeded(RepositorySession session, RepositorySessionBundle bundle) {
+ performNotify();
+ }
+ });
+ } catch (InactiveSessionException e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+ }
+ });
+ } catch (InactiveSessionException e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+ }
+
+ @Override
+ public void onFlowBeginFailed(RecordsChannel recordsChannel, Exception ex) {
+ flowBeginFailed.set(true);
+ WaitHelper.getTestWaiter().performNotify();
+ }
+ };
+
+ final RecordsChannel rc = new RecordsChannel(source, sink, rcDelegate);
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ rc.beginAndFlow();
+ } catch (InvalidSessionTransitionException e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+ }
+ });
+ }
+
+ public static final BookmarkRecord[] inbounds = new BookmarkRecord[] {
+ new BookmarkRecord("inboundSucc1", "bookmarks", 1, false),
+ new BookmarkRecord("inboundSucc2", "bookmarks", 1, false),
+ new BookmarkRecord("inboundFail1", "bookmarks", 1, false),
+ new BookmarkRecord("inboundSucc3", "bookmarks", 1, false),
+ new BookmarkRecord("inboundSucc4", "bookmarks", 1, false),
+ new BookmarkRecord("inboundFail2", "bookmarks", 1, false),
+ };
+ public static final BookmarkRecord[] outbounds = new BookmarkRecord[] {
+ new BookmarkRecord("outboundSucc1", "bookmarks", 1, false),
+ new BookmarkRecord("outboundSucc2", "bookmarks", 1, false),
+ new BookmarkRecord("outboundSucc3", "bookmarks", 1, false),
+ new BookmarkRecord("outboundSucc4", "bookmarks", 1, false),
+ new BookmarkRecord("outboundSucc5", "bookmarks", 1, false),
+ new BookmarkRecord("outboundFail6", "bookmarks", 1, false),
+ };
+
+ protected WBORepository empty() {
+ WBORepository repo = new SynchronizerHelpers.TrackingWBORepository();
+ return repo;
+ }
+
+ protected WBORepository full() {
+ WBORepository repo = new SynchronizerHelpers.TrackingWBORepository();
+ for (BookmarkRecord outbound : outbounds) {
+ repo.wbos.put(outbound.guid, outbound);
+ }
+ return repo;
+ }
+
+ protected WBORepository failingFetch() {
+ WBORepository repo = new FailFetchWBORepository();
+ for (BookmarkRecord outbound : outbounds) {
+ repo.wbos.put(outbound.guid, outbound);
+ }
+ return repo;
+ }
+
+ @Test
+ public void testSuccess() throws Exception {
+ WBORepository source = full();
+ WBORepository sink = empty();
+ doFlow(source, sink);
+ assertEquals(1, numFlowCompleted.get());
+ assertEquals(0, numFlowFetchFailed.get());
+ assertEquals(0, numFlowStoreFailed.get());
+ assertEquals(source.wbos, sink.wbos);
+ }
+
+ @Test
+ public void testFetchFail() throws Exception {
+ WBORepository source = failingFetch();
+ WBORepository sink = empty();
+ doFlow(source, sink);
+ assertEquals(1, numFlowCompleted.get());
+ assertTrue(numFlowFetchFailed.get() > 0);
+ assertEquals(0, numFlowStoreFailed.get());
+ assertTrue(sink.wbos.size() < 6);
+ }
+
+ @Test
+ public void testStoreSerialFail() throws Exception {
+ WBORepository source = full();
+ WBORepository sink = new SynchronizerHelpers.SerialFailStoreWBORepository();
+ doFlow(source, sink);
+ assertEquals(1, numFlowCompleted.get());
+ assertEquals(0, numFlowFetchFailed.get());
+ assertEquals(1, numFlowStoreFailed.get());
+ assertEquals(5, sink.wbos.size());
+ }
+
+ @Test
+ public void testStoreBatchesFail() throws Exception {
+ WBORepository source = full();
+ WBORepository sink = new SynchronizerHelpers.BatchFailStoreWBORepository(3);
+ doFlow(source, sink);
+ assertEquals(1, numFlowCompleted.get());
+ assertEquals(0, numFlowFetchFailed.get());
+ assertEquals(3, numFlowStoreFailed.get()); // One batch fails.
+ assertEquals(3, sink.wbos.size()); // One batch succeeds.
+ }
+
+
+ @Test
+ public void testStoreOneBigBatchFail() throws Exception {
+ WBORepository source = full();
+ WBORepository sink = new SynchronizerHelpers.BatchFailStoreWBORepository(50);
+ doFlow(source, sink);
+ assertEquals(1, numFlowCompleted.get());
+ assertEquals(0, numFlowFetchFailed.get());
+ assertEquals(6, numFlowStoreFailed.get()); // One (big) batch fails.
+ assertEquals(0, sink.wbos.size()); // No batches succeed.
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestResetCommands.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestResetCommands.java
new file mode 100644
index 0000000000..22bcc50936
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestResetCommands.java
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import android.content.SharedPreferences;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.DefaultGlobalSessionCallback;
+import org.mozilla.gecko.background.testhelpers.MockPrefsGlobalSession;
+import org.mozilla.gecko.background.testhelpers.MockServerSyncStage;
+import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.CommandProcessor;
+import org.mozilla.gecko.sync.EngineSettings;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.MetaGlobalException;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.SyncConfiguration;
+import org.mozilla.gecko.sync.SyncConfigurationException;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test that reset commands properly invoke the reset methods on the correct stage.
+ */
+@RunWith(TestRunner.class)
+public class TestResetCommands {
+ private static final String TEST_USERNAME = "johndoe";
+ private static final String TEST_PASSWORD = "password";
+ private static final String TEST_SYNC_KEY = "abcdeabcdeabcdeabcdeabcdea";
+
+ public static void performNotify() {
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ public static void performNotify(Throwable e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+
+ public static void performWait(Runnable runnable) {
+ WaitHelper.getTestWaiter().performWait(runnable);
+ }
+
+ @Before
+ public void setUp() {
+ assertTrue(WaitHelper.getTestWaiter().isIdle());
+ }
+
+ @Test
+ public void testHandleResetCommand() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, CryptoException {
+ // Create a global session.
+ // Set up stage mappings for a real stage name (because they're looked up by name
+ // in an enumeration) pointing to our fake stage.
+ // Send a reset command.
+ // Verify that reset is called on our stage.
+
+ class Result {
+ public boolean called = false;
+ }
+
+ final Result yes = new Result();
+ final Result no = new Result();
+ final GlobalSessionCallback callback = createGlobalSessionCallback();
+
+ // So we can poke at stages separately.
+ final HashMap<Stage, GlobalSyncStage> stagesToRun = new HashMap<Stage, GlobalSyncStage>();
+
+ // Side-effect: modifies global command processor.
+ final SharedPreferences prefs = new MockSharedPreferences();
+ final SyncConfiguration config = new SyncConfiguration(TEST_USERNAME, new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD), prefs);
+ config.syncKeyBundle = new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY);
+ final GlobalSession session = new MockPrefsGlobalSession(config, callback, null, null) {
+ @Override
+ public boolean isEngineRemotelyEnabled(String engineName,
+ EngineSettings engineSettings)
+ throws MetaGlobalException {
+ return true;
+ }
+
+ @Override
+ public void advance() {
+ // So we don't proceed and run other stages.
+ }
+
+ @Override
+ public void prepareStages() {
+ this.stages = stagesToRun;
+ }
+ };
+
+ final MockServerSyncStage stageGetsReset = new MockServerSyncStage() {
+ @Override
+ public void resetLocal() {
+ yes.called = true;
+ }
+ };
+
+ final MockServerSyncStage stageNotReset = new MockServerSyncStage() {
+ @Override
+ public void resetLocal() {
+ no.called = true;
+ }
+ };
+
+ stagesToRun.put(Stage.syncBookmarks, stageGetsReset);
+ stagesToRun.put(Stage.syncHistory, stageNotReset);
+
+ final String resetBookmarks = "{\"args\":[\"bookmarks\"],\"command\":\"resetEngine\"}";
+ ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(resetBookmarks);
+ CommandProcessor processor = CommandProcessor.getProcessor();
+ processor.processCommand(session, unparsedCommand);
+
+ assertTrue(yes.called);
+ assertFalse(no.called);
+ }
+
+ public void testHandleWipeCommand() {
+ // TODO
+ }
+
+ private static GlobalSessionCallback createGlobalSessionCallback() {
+ return new DefaultGlobalSessionCallback() {
+
+ @Override
+ public void handleAborted(GlobalSession globalSession, String reason) {
+ performNotify(new Exception("Aborted"));
+ }
+
+ @Override
+ public void handleError(GlobalSession globalSession, Exception ex) {
+ performNotify(ex);
+ }
+
+ @Override
+ public void handleSuccess(GlobalSession globalSession) {
+ }
+ };
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServer11RepositorySession.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServer11RepositorySession.java
new file mode 100644
index 0000000000..96a366c2d9
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServer11RepositorySession.java
@@ -0,0 +1,231 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.android.sync.test.SynchronizerHelpers.TrackingWBORepository;
+import org.mozilla.android.sync.test.helpers.BaseTestStorageRequestDelegate;
+import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
+import org.mozilla.android.sync.test.helpers.MockServer;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.InfoCollections;
+import org.mozilla.gecko.sync.InfoConfiguration;
+import org.mozilla.gecko.sync.JSONRecordFetcher;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.middleware.Crypto5MiddlewareRepository;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.sync.repositories.FetchFailedException;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.Server11Repository;
+import org.mozilla.gecko.sync.repositories.StoreFailedException;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecordFactory;
+import org.mozilla.gecko.sync.stage.SafeConstrainedServer11Repository;
+import org.mozilla.gecko.sync.synchronizer.ServerLocalSynchronizer;
+import org.mozilla.gecko.sync.synchronizer.Synchronizer;
+import org.simpleframework.http.ContentType;
+import org.simpleframework.http.Request;
+import org.simpleframework.http.Response;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
+public class TestServer11RepositorySession {
+
+ public class POSTMockServer extends MockServer {
+ @Override
+ public void handle(Request request, Response response) {
+ try {
+ String content = request.getContent();
+ System.out.println("Content:" + content);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ ContentType contentType = request.getContentType();
+ System.out.println("Content-Type:" + contentType);
+ super.handle(request, response, 200, "{success:[]}");
+ }
+ }
+
+ private static final int TEST_PORT = HTTPServerTestHelper.getTestPort();
+ private static final String TEST_SERVER = "http://localhost:" + TEST_PORT + "/";
+ static final String LOCAL_BASE_URL = TEST_SERVER + "1.1/n6ec3u5bee3tixzp2asys7bs6fve4jfw/";
+ static final String LOCAL_INFO_BASE_URL = LOCAL_BASE_URL + "info/";
+ static final String LOCAL_COUNTS_URL = LOCAL_INFO_BASE_URL + "collection_counts";
+
+ // Corresponds to rnewman+atest1@mozilla.com, local.
+ static final String TEST_USERNAME = "n6ec3u5bee3tixzp2asys7bs6fve4jfw";
+ static final String TEST_PASSWORD = "passowrd";
+ static final String SYNC_KEY = "eh7ppnb82iwr5kt3z3uyi5vr44";
+
+ public final AuthHeaderProvider authHeaderProvider = new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD);
+ protected final InfoCollections infoCollections = new InfoCollections();
+ protected final InfoConfiguration infoConfiguration = new InfoConfiguration();
+
+ // Few-second timeout so that our longer operations don't time out and cause spurious error-handling results.
+ private static final int SHORT_TIMEOUT = 10000;
+
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD);
+ }
+
+ private HTTPServerTestHelper data = new HTTPServerTestHelper();
+
+ public class TestSyncStorageRequestDelegate extends
+ BaseTestStorageRequestDelegate {
+ public TestSyncStorageRequestDelegate(String username, String password) {
+ super(username, password);
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse res) {
+ assertTrue(res.wasSuccessful());
+ assertTrue(res.httpResponse().containsHeader("X-Weave-Timestamp"));
+ BaseResource.consumeEntity(res);
+ data.stopHTTPServer();
+ }
+ }
+
+ @SuppressWarnings("static-method")
+ protected TrackingWBORepository getLocal(int numRecords) {
+ final TrackingWBORepository local = new TrackingWBORepository();
+ for (int i = 0; i < numRecords; i++) {
+ BookmarkRecord outbound = new BookmarkRecord("outboundFail" + i, "bookmarks", 1, false);
+ local.wbos.put(outbound.guid, outbound);
+ }
+ return local;
+ }
+
+ protected Exception doSynchronize(MockServer server) throws Exception {
+ final String COLLECTION = "test";
+
+ final TrackingWBORepository local = getLocal(100);
+ final Server11Repository remote = new Server11Repository(COLLECTION, getCollectionURL(COLLECTION), authHeaderProvider, infoCollections, infoConfiguration);
+ KeyBundle collectionKey = new KeyBundle(TEST_USERNAME, SYNC_KEY);
+ Crypto5MiddlewareRepository cryptoRepo = new Crypto5MiddlewareRepository(remote, collectionKey);
+ cryptoRepo.recordFactory = new BookmarkRecordFactory();
+
+ final Synchronizer synchronizer = new ServerLocalSynchronizer();
+ synchronizer.repositoryA = cryptoRepo;
+ synchronizer.repositoryB = local;
+
+ data.startHTTPServer(server);
+ try {
+ Exception e = TestServerLocalSynchronizer.doSynchronize(synchronizer);
+ return e;
+ } finally {
+ data.stopHTTPServer();
+ }
+ }
+
+ protected String getCollectionURL(String collection) {
+ return LOCAL_BASE_URL + "/storage/" + collection;
+ }
+
+ @Test
+ public void testFetchFailure() throws Exception {
+ MockServer server = new MockServer(404, "error");
+ Exception e = doSynchronize(server);
+ assertNotNull(e);
+ assertEquals(FetchFailedException.class, e.getClass());
+ }
+
+ @Test
+ public void testStorePostSuccessWithFailingRecords() throws Exception {
+ MockServer server = new MockServer(200, "{ modified: \" + " + Utils.millisecondsToDecimalSeconds(System.currentTimeMillis()) + ", " +
+ "success: []," +
+ "failed: { outboundFail2: [] } }");
+ Exception e = doSynchronize(server);
+ assertNotNull(e);
+ assertEquals(StoreFailedException.class, e.getClass());
+ }
+
+ @Test
+ public void testStorePostFailure() throws Exception {
+ MockServer server = new MockServer() {
+ @Override
+ public void handle(Request request, Response response) {
+ if (request.getMethod().equals("POST")) {
+ this.handle(request, response, 404, "missing");
+ }
+ this.handle(request, response, 200, "success");
+ return;
+ }
+ };
+
+ Exception e = doSynchronize(server);
+ assertNotNull(e);
+ assertEquals(StoreFailedException.class, e.getClass());
+ }
+
+ @Test
+ public void testConstraints() throws Exception {
+ MockServer server = new MockServer() {
+ @Override
+ public void handle(Request request, Response response) {
+ if (request.getMethod().equals("GET")) {
+ if (request.getPath().getPath().endsWith("/info/collection_counts")) {
+ this.handle(request, response, 200, "{\"bookmarks\": 5001}");
+ }
+ }
+ this.handle(request, response, 400, "NOOOO");
+ }
+ };
+ final JSONRecordFetcher countsFetcher = new JSONRecordFetcher(LOCAL_COUNTS_URL, getAuthHeaderProvider());
+ String collection = "bookmarks";
+ final SafeConstrainedServer11Repository remote = new SafeConstrainedServer11Repository(collection,
+ getCollectionURL(collection),
+ getAuthHeaderProvider(),
+ infoCollections,
+ infoConfiguration,
+ 5000, 5000, "sortindex", countsFetcher);
+
+ data.startHTTPServer(server);
+ final AtomicBoolean out = new AtomicBoolean(false);
+
+ // Verify that shouldSkip returns true due to a fetch of too large counts,
+ // rather than due to a timeout failure waiting to fetch counts.
+ try {
+ WaitHelper.getTestWaiter().performWait(
+ SHORT_TIMEOUT,
+ new Runnable() {
+ @Override
+ public void run() {
+ remote.createSession(new RepositorySessionCreationDelegate() {
+ @Override
+ public void onSessionCreated(RepositorySession session) {
+ out.set(session.shouldSkip());
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ @Override
+ public void onSessionCreateFailed(Exception ex) {
+ WaitHelper.getTestWaiter().performNotify(ex);
+ }
+
+ @Override
+ public RepositorySessionCreationDelegate deferredCreationDelegate() {
+ return this;
+ }
+ }, null);
+ }
+ });
+ assertTrue(out.get());
+ } finally {
+ data.stopHTTPServer();
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServerLocalSynchronizer.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServerLocalSynchronizer.java
new file mode 100644
index 0000000000..267798672c
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServerLocalSynchronizer.java
@@ -0,0 +1,237 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.android.sync.test.SynchronizerHelpers.BatchFailStoreWBORepository;
+import org.mozilla.android.sync.test.SynchronizerHelpers.BeginErrorWBORepository;
+import org.mozilla.android.sync.test.SynchronizerHelpers.BeginFailedException;
+import org.mozilla.android.sync.test.SynchronizerHelpers.FailFetchWBORepository;
+import org.mozilla.android.sync.test.SynchronizerHelpers.FinishErrorWBORepository;
+import org.mozilla.android.sync.test.SynchronizerHelpers.FinishFailedException;
+import org.mozilla.android.sync.test.SynchronizerHelpers.SerialFailStoreWBORepository;
+import org.mozilla.android.sync.test.SynchronizerHelpers.TrackingWBORepository;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.background.testhelpers.WBORepository;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.repositories.FetchFailedException;
+import org.mozilla.gecko.sync.repositories.StoreFailedException;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
+import org.mozilla.gecko.sync.synchronizer.ServerLocalSynchronizer;
+import org.mozilla.gecko.sync.synchronizer.Synchronizer;
+import org.mozilla.gecko.sync.synchronizer.SynchronizerDelegate;
+
+import java.util.ArrayList;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+@RunWith(TestRunner.class)
+public class TestServerLocalSynchronizer {
+ public static final String LOG_TAG = "TestServLocSync";
+
+ protected Synchronizer getSynchronizer(WBORepository remote, WBORepository local) {
+ BookmarkRecord[] inbounds = new BookmarkRecord[] {
+ new BookmarkRecord("inboundSucc1", "bookmarks", 1, false),
+ new BookmarkRecord("inboundSucc2", "bookmarks", 1, false),
+ new BookmarkRecord("inboundFail1", "bookmarks", 1, false),
+ new BookmarkRecord("inboundSucc3", "bookmarks", 1, false),
+ new BookmarkRecord("inboundFail2", "bookmarks", 1, false),
+ new BookmarkRecord("inboundFail3", "bookmarks", 1, false),
+ };
+ BookmarkRecord[] outbounds = new BookmarkRecord[] {
+ new BookmarkRecord("outboundFail1", "bookmarks", 1, false),
+ new BookmarkRecord("outboundFail2", "bookmarks", 1, false),
+ new BookmarkRecord("outboundFail3", "bookmarks", 1, false),
+ new BookmarkRecord("outboundFail4", "bookmarks", 1, false),
+ new BookmarkRecord("outboundFail5", "bookmarks", 1, false),
+ new BookmarkRecord("outboundFail6", "bookmarks", 1, false),
+ };
+ for (BookmarkRecord inbound : inbounds) {
+ remote.wbos.put(inbound.guid, inbound);
+ }
+ for (BookmarkRecord outbound : outbounds) {
+ local.wbos.put(outbound.guid, outbound);
+ }
+
+ final Synchronizer synchronizer = new ServerLocalSynchronizer();
+ synchronizer.repositoryA = remote;
+ synchronizer.repositoryB = local;
+ return synchronizer;
+ }
+
+ protected static Exception doSynchronize(final Synchronizer synchronizer) {
+ final ArrayList<Exception> a = new ArrayList<Exception>();
+
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ synchronizer.synchronize(null, new SynchronizerDelegate() {
+ @Override
+ public void onSynchronized(Synchronizer synchronizer) {
+ Logger.trace(LOG_TAG, "Got onSynchronized.");
+ a.add(null);
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ @Override
+ public void onSynchronizeFailed(Synchronizer synchronizer, Exception lastException, String reason) {
+ Logger.trace(LOG_TAG, "Got onSynchronizedFailed.");
+ a.add(lastException);
+ WaitHelper.getTestWaiter().performNotify();
+ }
+ });
+ }
+ });
+
+ assertEquals(1, a.size()); // Should not be called multiple times!
+ return a.get(0);
+ }
+
+ @Test
+ public void testNoErrors() {
+ WBORepository remote = new TrackingWBORepository();
+ WBORepository local = new TrackingWBORepository();
+
+ Synchronizer synchronizer = getSynchronizer(remote, local);
+ assertNull(doSynchronize(synchronizer));
+
+ assertEquals(12, local.wbos.size());
+ assertEquals(12, remote.wbos.size());
+ }
+
+ @Test
+ public void testLocalFetchErrors() {
+ WBORepository remote = new TrackingWBORepository();
+ WBORepository local = new FailFetchWBORepository();
+
+ Synchronizer synchronizer = getSynchronizer(remote, local);
+ Exception e = doSynchronize(synchronizer);
+ assertNotNull(e);
+ assertEquals(FetchFailedException.class, e.getClass());
+
+ // Neither session gets finished successfully, so all records are dropped.
+ assertEquals(6, local.wbos.size());
+ assertEquals(6, remote.wbos.size());
+ }
+
+ @Test
+ public void testRemoteFetchErrors() {
+ WBORepository remote = new FailFetchWBORepository();
+ WBORepository local = new TrackingWBORepository();
+
+ Synchronizer synchronizer = getSynchronizer(remote, local);
+ Exception e = doSynchronize(synchronizer);
+ assertNotNull(e);
+ assertEquals(FetchFailedException.class, e.getClass());
+
+ // Neither session gets finished successfully, so all records are dropped.
+ assertEquals(6, local.wbos.size());
+ assertEquals(6, remote.wbos.size());
+ }
+
+ @Test
+ public void testLocalSerialStoreErrorsAreIgnored() {
+ WBORepository remote = new TrackingWBORepository();
+ WBORepository local = new SerialFailStoreWBORepository();
+
+ Synchronizer synchronizer = getSynchronizer(remote, local);
+ assertNull(doSynchronize(synchronizer));
+
+ assertEquals(9, local.wbos.size());
+ assertEquals(12, remote.wbos.size());
+ }
+
+ @Test
+ public void testLocalBatchStoreErrorsAreIgnored() {
+ final int BATCH_SIZE = 3;
+
+ Synchronizer synchronizer = getSynchronizer(new TrackingWBORepository(), new BatchFailStoreWBORepository(BATCH_SIZE));
+
+ Exception e = doSynchronize(synchronizer);
+ assertNull(e);
+ }
+
+ @Test
+ public void testRemoteSerialStoreErrorsAreNotIgnored() throws Exception {
+ Synchronizer synchronizer = getSynchronizer(new SerialFailStoreWBORepository(), new TrackingWBORepository()); // Tracking so we don't send incoming records back.
+
+ Exception e = doSynchronize(synchronizer);
+ assertNotNull(e);
+ assertEquals(StoreFailedException.class, e.getClass());
+ }
+
+ @Test
+ public void testRemoteBatchStoreErrorsAreNotIgnoredManyBatches() throws Exception {
+ final int BATCH_SIZE = 3;
+
+ Synchronizer synchronizer = getSynchronizer(new BatchFailStoreWBORepository(BATCH_SIZE), new TrackingWBORepository()); // Tracking so we don't send incoming records back.
+
+ Exception e = doSynchronize(synchronizer);
+ assertNotNull(e);
+ assertEquals(StoreFailedException.class, e.getClass());
+ }
+
+ @Test
+ public void testRemoteBatchStoreErrorsAreNotIgnoredOneBigBatch() throws Exception {
+ final int BATCH_SIZE = 20;
+
+ Synchronizer synchronizer = getSynchronizer(new BatchFailStoreWBORepository(BATCH_SIZE), new TrackingWBORepository()); // Tracking so we don't send incoming records back.
+
+ Exception e = doSynchronize(synchronizer);
+ assertNotNull(e);
+ assertEquals(StoreFailedException.class, e.getClass());
+ }
+
+ @Test
+ public void testSessionRemoteBeginError() {
+ Synchronizer synchronizer = getSynchronizer(new BeginErrorWBORepository(), new TrackingWBORepository());
+ Exception e = doSynchronize(synchronizer);
+ assertNotNull(e);
+ assertEquals(BeginFailedException.class, e.getClass());
+ }
+
+ @Test
+ public void testSessionLocalBeginError() {
+ Synchronizer synchronizer = getSynchronizer(new TrackingWBORepository(), new BeginErrorWBORepository());
+ Exception e = doSynchronize(synchronizer);
+ assertNotNull(e);
+ assertEquals(BeginFailedException.class, e.getClass());
+ }
+
+ @Test
+ public void testSessionRemoteFinishError() {
+ Synchronizer synchronizer = getSynchronizer(new FinishErrorWBORepository(), new TrackingWBORepository());
+ Exception e = doSynchronize(synchronizer);
+ assertNotNull(e);
+ assertEquals(FinishFailedException.class, e.getClass());
+ }
+
+ @Test
+ public void testSessionLocalFinishError() {
+ Synchronizer synchronizer = getSynchronizer(new TrackingWBORepository(), new FinishErrorWBORepository());
+ Exception e = doSynchronize(synchronizer);
+ assertNotNull(e);
+ assertEquals(FinishFailedException.class, e.getClass());
+ }
+
+ @Test
+ public void testSessionBothBeginError() {
+ Synchronizer synchronizer = getSynchronizer(new BeginErrorWBORepository(), new BeginErrorWBORepository());
+ Exception e = doSynchronize(synchronizer);
+ assertNotNull(e);
+ assertEquals(BeginFailedException.class, e.getClass());
+ }
+
+ @Test
+ public void testSessionBothFinishError() {
+ Synchronizer synchronizer = getSynchronizer(new FinishErrorWBORepository(), new FinishErrorWBORepository());
+ Exception e = doSynchronize(synchronizer);
+ assertNotNull(e);
+ assertEquals(FinishFailedException.class, e.getClass());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSyncConfiguration.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSyncConfiguration.java
new file mode 100644
index 0000000000..974d799ded
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSyncConfiguration.java
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.Sync11Configuration;
+import org.mozilla.gecko.sync.SyncConfiguration;
+
+import java.net.URI;
+
+@RunWith(TestRunner.class)
+public class TestSyncConfiguration {
+ @Test
+ public void testURLs() throws Exception {
+ final MockSharedPreferences prefs = new MockSharedPreferences();
+
+ // N.B., the username isn't used in the cluster path.
+ SyncConfiguration fxaConfig = new SyncConfiguration("username", null, prefs);
+ fxaConfig.clusterURL = new URI("http://db1.oldsync.dev.lcip.org/1.1/174");
+ Assert.assertEquals("http://db1.oldsync.dev.lcip.org/1.1/174/info/collections", fxaConfig.infoCollectionsURL());
+ Assert.assertEquals("http://db1.oldsync.dev.lcip.org/1.1/174/info/collection_counts", fxaConfig.infoCollectionCountsURL());
+ Assert.assertEquals("http://db1.oldsync.dev.lcip.org/1.1/174/storage/meta/global", fxaConfig.metaURL());
+ Assert.assertEquals("http://db1.oldsync.dev.lcip.org/1.1/174/storage", fxaConfig.storageURL());
+ Assert.assertEquals("http://db1.oldsync.dev.lcip.org/1.1/174/storage/collection", fxaConfig.collectionURI("collection").toASCIIString());
+
+ SyncConfiguration oldConfig = new Sync11Configuration("username", null, prefs);
+ oldConfig.clusterURL = new URI("https://db.com/internal/");
+ Assert.assertEquals("https://db.com/internal/1.1/username/info/collections", oldConfig.infoCollectionsURL());
+ Assert.assertEquals("https://db.com/internal/1.1/username/info/collection_counts", oldConfig.infoCollectionCountsURL());
+ Assert.assertEquals("https://db.com/internal/1.1/username/storage/meta/global", oldConfig.metaURL());
+ Assert.assertEquals("https://db.com/internal/1.1/username/storage", oldConfig.storageURL());
+ Assert.assertEquals("https://db.com/internal/1.1/username/storage/collection", oldConfig.collectionURI("collection").toASCIIString());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSynchronizer.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSynchronizer.java
new file mode 100644
index 0000000000..65157beee3
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSynchronizer.java
@@ -0,0 +1,398 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import android.content.Context;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.android.sync.test.SynchronizerHelpers.TrackingWBORepository;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.background.testhelpers.WBORepository;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
+import org.mozilla.gecko.sync.synchronizer.Synchronizer;
+import org.mozilla.gecko.sync.synchronizer.SynchronizerDelegate;
+import org.mozilla.gecko.sync.synchronizer.SynchronizerSession;
+import org.mozilla.gecko.sync.synchronizer.SynchronizerSessionDelegate;
+
+import java.util.Date;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+@RunWith(TestRunner.class)
+public class TestSynchronizer {
+ public static final String LOG_TAG = "TestSynchronizer";
+
+ public static void assertInRangeInclusive(long earliest, long value, long latest) {
+ assertTrue(earliest <= value);
+ assertTrue(latest >= value);
+ }
+
+ public static void recordEquals(BookmarkRecord r, String guid, long lastModified, boolean deleted, String collection) {
+ assertEquals(r.guid, guid);
+ assertEquals(r.lastModified, lastModified);
+ assertEquals(r.deleted, deleted);
+ assertEquals(r.collection, collection);
+ }
+
+ public static void recordEquals(BookmarkRecord a, BookmarkRecord b) {
+ assertEquals(a.guid, b.guid);
+ assertEquals(a.lastModified, b.lastModified);
+ assertEquals(a.deleted, b.deleted);
+ assertEquals(a.collection, b.collection);
+ }
+
+ @Before
+ public void setUp() {
+ WaitHelper.resetTestWaiter();
+ }
+
+ @After
+ public void tearDown() {
+ WaitHelper.resetTestWaiter();
+ }
+
+ @Test
+ public void testSynchronizerSession() {
+ final Context context = null;
+ final WBORepository repoA = new TrackingWBORepository();
+ final WBORepository repoB = new TrackingWBORepository();
+
+ final String collection = "bookmarks";
+ final boolean deleted = false;
+ final String guidA = "abcdabcdabcd";
+ final String guidB = "ffffffffffff";
+ final String guidC = "xxxxxxxxxxxx";
+ final long lastModifiedA = 312345;
+ final long lastModifiedB = 412340;
+ final long lastModifiedC = 412345;
+ BookmarkRecord bookmarkRecordA = new BookmarkRecord(guidA, collection, lastModifiedA, deleted);
+ BookmarkRecord bookmarkRecordB = new BookmarkRecord(guidB, collection, lastModifiedB, deleted);
+ BookmarkRecord bookmarkRecordC = new BookmarkRecord(guidC, collection, lastModifiedC, deleted);
+
+ repoA.wbos.put(guidA, bookmarkRecordA);
+ repoB.wbos.put(guidB, bookmarkRecordB);
+ repoB.wbos.put(guidC, bookmarkRecordC);
+ Synchronizer synchronizer = new Synchronizer();
+ synchronizer.repositoryA = repoA;
+ synchronizer.repositoryB = repoB;
+ final SynchronizerSession syncSession = new SynchronizerSession(synchronizer, new SynchronizerSessionDelegate() {
+
+ @Override
+ public void onInitialized(SynchronizerSession session) {
+ assertFalse(repoA.wbos.containsKey(guidB));
+ assertFalse(repoA.wbos.containsKey(guidC));
+ assertFalse(repoB.wbos.containsKey(guidA));
+ assertTrue(repoA.wbos.containsKey(guidA));
+ assertTrue(repoB.wbos.containsKey(guidB));
+ assertTrue(repoB.wbos.containsKey(guidC));
+ session.synchronize();
+ }
+
+ @Override
+ public void onSynchronized(SynchronizerSession session) {
+ try {
+ assertEquals(1, session.getInboundCount());
+ assertEquals(2, session.getOutboundCount());
+ WaitHelper.getTestWaiter().performNotify();
+ } catch (Throwable e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+ }
+
+ @Override
+ public void onSynchronizeFailed(SynchronizerSession session,
+ Exception lastException, String reason) {
+ WaitHelper.getTestWaiter().performNotify(lastException);
+ }
+
+ @Override
+ public void onSynchronizeSkipped(SynchronizerSession synchronizerSession) {
+ WaitHelper.getTestWaiter().performNotify(new RuntimeException());
+ }
+ });
+
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ syncSession.init(context, new RepositorySessionBundle(0), new RepositorySessionBundle(0));
+ }
+ });
+
+ // Verify contents.
+ assertTrue(repoA.wbos.containsKey(guidA));
+ assertTrue(repoA.wbos.containsKey(guidB));
+ assertTrue(repoA.wbos.containsKey(guidC));
+ assertTrue(repoB.wbos.containsKey(guidA));
+ assertTrue(repoB.wbos.containsKey(guidB));
+ assertTrue(repoB.wbos.containsKey(guidC));
+ BookmarkRecord aa = (BookmarkRecord) repoA.wbos.get(guidA);
+ BookmarkRecord ab = (BookmarkRecord) repoA.wbos.get(guidB);
+ BookmarkRecord ac = (BookmarkRecord) repoA.wbos.get(guidC);
+ BookmarkRecord ba = (BookmarkRecord) repoB.wbos.get(guidA);
+ BookmarkRecord bb = (BookmarkRecord) repoB.wbos.get(guidB);
+ BookmarkRecord bc = (BookmarkRecord) repoB.wbos.get(guidC);
+ recordEquals(aa, guidA, lastModifiedA, deleted, collection);
+ recordEquals(ab, guidB, lastModifiedB, deleted, collection);
+ recordEquals(ac, guidC, lastModifiedC, deleted, collection);
+ recordEquals(ba, guidA, lastModifiedA, deleted, collection);
+ recordEquals(bb, guidB, lastModifiedB, deleted, collection);
+ recordEquals(bc, guidC, lastModifiedC, deleted, collection);
+ recordEquals(aa, ba);
+ recordEquals(ab, bb);
+ recordEquals(ac, bc);
+ }
+
+ public abstract class SuccessfulSynchronizerDelegate implements SynchronizerDelegate {
+ public long syncAOne = 0;
+ public long syncBOne = 0;
+
+ @Override
+ public void onSynchronizeFailed(Synchronizer synchronizer,
+ Exception lastException, String reason) {
+ fail("Should not fail.");
+ }
+ }
+
+ @Test
+ public void testSynchronizerPersists() {
+ final Object monitor = new Object();
+ final long earliest = new Date().getTime();
+
+ Context context = null;
+ final WBORepository repoA = new WBORepository();
+ final WBORepository repoB = new WBORepository();
+ Synchronizer synchronizer = new Synchronizer();
+ synchronizer.bundleA = new RepositorySessionBundle(0);
+ synchronizer.bundleB = new RepositorySessionBundle(0);
+ synchronizer.repositoryA = repoA;
+ synchronizer.repositoryB = repoB;
+
+ final SuccessfulSynchronizerDelegate delegateOne = new SuccessfulSynchronizerDelegate() {
+ @Override
+ public void onSynchronized(Synchronizer synchronizer) {
+ Logger.trace(LOG_TAG, "onSynchronized. Success!");
+ syncAOne = synchronizer.bundleA.getTimestamp();
+ syncBOne = synchronizer.bundleB.getTimestamp();
+ synchronized (monitor) {
+ monitor.notify();
+ }
+ }
+ };
+ final SuccessfulSynchronizerDelegate delegateTwo = new SuccessfulSynchronizerDelegate() {
+ @Override
+ public void onSynchronized(Synchronizer synchronizer) {
+ Logger.trace(LOG_TAG, "onSynchronized. Success!");
+ syncAOne = synchronizer.bundleA.getTimestamp();
+ syncBOne = synchronizer.bundleB.getTimestamp();
+ synchronized (monitor) {
+ monitor.notify();
+ }
+ }
+ };
+ synchronized (monitor) {
+ synchronizer.synchronize(context, delegateOne);
+ try {
+ monitor.wait();
+ } catch (InterruptedException e) {
+ fail("Interrupted.");
+ }
+ }
+ long now = new Date().getTime();
+ Logger.trace(LOG_TAG, "Earliest is " + earliest);
+ Logger.trace(LOG_TAG, "syncAOne is " + delegateOne.syncAOne);
+ Logger.trace(LOG_TAG, "syncBOne is " + delegateOne.syncBOne);
+ Logger.trace(LOG_TAG, "Now: " + now);
+ assertInRangeInclusive(earliest, delegateOne.syncAOne, now);
+ assertInRangeInclusive(earliest, delegateOne.syncBOne, now);
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ fail("Thread interrupted!");
+ }
+ synchronized (monitor) {
+ synchronizer.synchronize(context, delegateTwo);
+ try {
+ monitor.wait();
+ } catch (InterruptedException e) {
+ fail("Interrupted.");
+ }
+ }
+ now = new Date().getTime();
+ Logger.trace(LOG_TAG, "Earliest is " + earliest);
+ Logger.trace(LOG_TAG, "syncAOne is " + delegateTwo.syncAOne);
+ Logger.trace(LOG_TAG, "syncBOne is " + delegateTwo.syncBOne);
+ Logger.trace(LOG_TAG, "Now: " + now);
+ assertInRangeInclusive(earliest, delegateTwo.syncAOne, now);
+ assertInRangeInclusive(earliest, delegateTwo.syncBOne, now);
+ assertTrue(delegateTwo.syncAOne > delegateOne.syncAOne);
+ assertTrue(delegateTwo.syncBOne > delegateOne.syncBOne);
+ Logger.trace(LOG_TAG, "Reached end of test.");
+ }
+
+ private Synchronizer getTestSynchronizer(long tsA, long tsB) {
+ WBORepository repoA = new TrackingWBORepository();
+ WBORepository repoB = new TrackingWBORepository();
+ Synchronizer synchronizer = new Synchronizer();
+ synchronizer.bundleA = new RepositorySessionBundle(tsA);
+ synchronizer.bundleB = new RepositorySessionBundle(tsB);
+ synchronizer.repositoryA = repoA;
+ synchronizer.repositoryB = repoB;
+ return synchronizer;
+ }
+
+ /**
+ * Let's put data in two repos and synchronize them with last sync
+ * timestamps later than all of the records. Verify that no records
+ * are exchanged.
+ */
+ @Test
+ public void testSynchronizerFakeTimestamps() {
+ final Context context = null;
+
+ final String collection = "bookmarks";
+ final boolean deleted = false;
+ final String guidA = "abcdabcdabcd";
+ final String guidB = "ffffffffffff";
+ final long lastModifiedA = 312345;
+ final long lastModifiedB = 412345;
+ BookmarkRecord bookmarkRecordA = new BookmarkRecord(guidA, collection, lastModifiedA, deleted);
+ BookmarkRecord bookmarkRecordB = new BookmarkRecord(guidB, collection, lastModifiedB, deleted);
+
+ final Synchronizer synchronizer = getTestSynchronizer(lastModifiedA + 10, lastModifiedB + 10);
+ final WBORepository repoA = (WBORepository) synchronizer.repositoryA;
+ final WBORepository repoB = (WBORepository) synchronizer.repositoryB;
+
+ repoA.wbos.put(guidA, bookmarkRecordA);
+ repoB.wbos.put(guidB, bookmarkRecordB);
+
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ synchronizer.synchronize(context, new SynchronizerDelegate() {
+
+ @Override
+ public void onSynchronized(Synchronizer synchronizer) {
+ try {
+ // No records get sent either way.
+ final SynchronizerSession synchronizerSession = synchronizer.getSynchronizerSession();
+ assertNotNull(synchronizerSession);
+ assertEquals(0, synchronizerSession.getInboundCount());
+ assertEquals(0, synchronizerSession.getOutboundCount());
+ WaitHelper.getTestWaiter().performNotify();
+ } catch (Throwable e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+ }
+
+ @Override
+ public void onSynchronizeFailed(Synchronizer synchronizer,
+ Exception lastException, String reason) {
+ WaitHelper.getTestWaiter().performNotify(lastException);
+ }
+ });
+ }
+ });
+
+ // Verify contents.
+ assertTrue(repoA.wbos.containsKey(guidA));
+ assertTrue(repoB.wbos.containsKey(guidB));
+ assertFalse(repoB.wbos.containsKey(guidA));
+ assertFalse(repoA.wbos.containsKey(guidB));
+ BookmarkRecord aa = (BookmarkRecord) repoA.wbos.get(guidA);
+ BookmarkRecord ab = (BookmarkRecord) repoA.wbos.get(guidB);
+ BookmarkRecord ba = (BookmarkRecord) repoB.wbos.get(guidA);
+ BookmarkRecord bb = (BookmarkRecord) repoB.wbos.get(guidB);
+ assertNull(ab);
+ assertNull(ba);
+ recordEquals(aa, guidA, lastModifiedA, deleted, collection);
+ recordEquals(bb, guidB, lastModifiedB, deleted, collection);
+ }
+
+
+ @Test
+ public void testSynchronizer() {
+ final Context context = null;
+
+ final String collection = "bookmarks";
+ final boolean deleted = false;
+ final String guidA = "abcdabcdabcd";
+ final String guidB = "ffffffffffff";
+ final String guidC = "gggggggggggg";
+ final long lastModifiedA = 312345;
+ final long lastModifiedB = 412340;
+ final long lastModifiedC = 412345;
+ BookmarkRecord bookmarkRecordA = new BookmarkRecord(guidA, collection, lastModifiedA, deleted);
+ BookmarkRecord bookmarkRecordB = new BookmarkRecord(guidB, collection, lastModifiedB, deleted);
+ BookmarkRecord bookmarkRecordC = new BookmarkRecord(guidC, collection, lastModifiedC, deleted);
+
+ final Synchronizer synchronizer = getTestSynchronizer(0, 0);
+ final WBORepository repoA = (WBORepository) synchronizer.repositoryA;
+ final WBORepository repoB = (WBORepository) synchronizer.repositoryB;
+
+ repoA.wbos.put(guidA, bookmarkRecordA);
+ repoB.wbos.put(guidB, bookmarkRecordB);
+ repoB.wbos.put(guidC, bookmarkRecordC);
+
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ synchronizer.synchronize(context, new SynchronizerDelegate() {
+
+ @Override
+ public void onSynchronized(Synchronizer synchronizer) {
+ try {
+ // No records get sent either way.
+ final SynchronizerSession synchronizerSession = synchronizer.getSynchronizerSession();
+ assertNotNull(synchronizerSession);
+ assertEquals(1, synchronizerSession.getInboundCount());
+ assertEquals(2, synchronizerSession.getOutboundCount());
+ WaitHelper.getTestWaiter().performNotify();
+ } catch (Throwable e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+ }
+
+ @Override
+ public void onSynchronizeFailed(Synchronizer synchronizer,
+ Exception lastException, String reason) {
+ WaitHelper.getTestWaiter().performNotify(lastException);
+ }
+ });
+ }
+ });
+
+ // Verify contents.
+ assertTrue(repoA.wbos.containsKey(guidA));
+ assertTrue(repoA.wbos.containsKey(guidB));
+ assertTrue(repoA.wbos.containsKey(guidC));
+ assertTrue(repoB.wbos.containsKey(guidA));
+ assertTrue(repoB.wbos.containsKey(guidB));
+ assertTrue(repoB.wbos.containsKey(guidC));
+ BookmarkRecord aa = (BookmarkRecord) repoA.wbos.get(guidA);
+ BookmarkRecord ab = (BookmarkRecord) repoA.wbos.get(guidB);
+ BookmarkRecord ac = (BookmarkRecord) repoA.wbos.get(guidC);
+ BookmarkRecord ba = (BookmarkRecord) repoB.wbos.get(guidA);
+ BookmarkRecord bb = (BookmarkRecord) repoB.wbos.get(guidB);
+ BookmarkRecord bc = (BookmarkRecord) repoB.wbos.get(guidC);
+ recordEquals(aa, guidA, lastModifiedA, deleted, collection);
+ recordEquals(ab, guidB, lastModifiedB, deleted, collection);
+ recordEquals(ac, guidC, lastModifiedC, deleted, collection);
+ recordEquals(ba, guidA, lastModifiedA, deleted, collection);
+ recordEquals(bb, guidB, lastModifiedB, deleted, collection);
+ recordEquals(bc, guidC, lastModifiedC, deleted, collection);
+ recordEquals(aa, ba);
+ recordEquals(ab, bb);
+ recordEquals(ac, bc);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSynchronizerSession.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSynchronizerSession.java
new file mode 100644
index 0000000000..ddc3ae68e0
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSynchronizerSession.java
@@ -0,0 +1,306 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import android.content.Context;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.android.sync.test.SynchronizerHelpers.DataAvailableWBORepository;
+import org.mozilla.android.sync.test.SynchronizerHelpers.ShouldSkipWBORepository;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.background.testhelpers.WBORepository;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.SynchronizerConfiguration;
+import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+import org.mozilla.gecko.sync.synchronizer.Synchronizer;
+import org.mozilla.gecko.sync.synchronizer.SynchronizerSession;
+import org.mozilla.gecko.sync.synchronizer.SynchronizerSessionDelegate;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+@RunWith(TestRunner.class)
+public class TestSynchronizerSession {
+ public static final String LOG_TAG = TestSynchronizerSession.class.getSimpleName();
+
+ protected static void assertFirstContainsSecond(Map<String, Record> first, Map<String, Record> second) {
+ for (Entry<String, Record> entry : second.entrySet()) {
+ assertTrue("Expected key " + entry.getKey(), first.containsKey(entry.getKey()));
+ Record record = first.get(entry.getKey());
+ assertEquals(entry.getValue(), record);
+ }
+ }
+
+ protected static void assertFirstDoesNotContainSecond(Map<String, Record> first, Map<String, Record> second) {
+ for (Entry<String, Record> entry : second.entrySet()) {
+ assertFalse("Unexpected key " + entry.getKey(), first.containsKey(entry.getKey()));
+ }
+ }
+
+ protected WBORepository repoA = null;
+ protected WBORepository repoB = null;
+ protected SynchronizerSession syncSession = null;
+ protected Map<String, Record> originalWbosA = null;
+ protected Map<String, Record> originalWbosB = null;
+
+ @Before
+ public void setUp() {
+ repoA = new DataAvailableWBORepository(false);
+ repoB = new DataAvailableWBORepository(false);
+
+ final String collection = "bookmarks";
+ final boolean deleted = false;
+ final String guidA = "abcdabcdabcd";
+ final String guidB = "ffffffffffff";
+ final String guidC = "xxxxxxxxxxxx";
+ final long lastModifiedA = 312345;
+ final long lastModifiedB = 412340;
+ final long lastModifiedC = 412345;
+ final BookmarkRecord bookmarkRecordA = new BookmarkRecord(guidA, collection, lastModifiedA, deleted);
+ final BookmarkRecord bookmarkRecordB = new BookmarkRecord(guidB, collection, lastModifiedB, deleted);
+ final BookmarkRecord bookmarkRecordC = new BookmarkRecord(guidC, collection, lastModifiedC, deleted);
+
+ repoA.wbos.put(guidA, bookmarkRecordA);
+ repoB.wbos.put(guidB, bookmarkRecordB);
+ repoB.wbos.put(guidC, bookmarkRecordC);
+
+ originalWbosA = new HashMap<String, Record>(repoA.wbos);
+ originalWbosB = new HashMap<String, Record>(repoB.wbos);
+
+ Synchronizer synchronizer = new Synchronizer();
+ synchronizer.repositoryA = repoA;
+ synchronizer.repositoryB = repoB;
+ syncSession = new SynchronizerSession(synchronizer, new SynchronizerSessionDelegate() {
+ @Override
+ public void onInitialized(SynchronizerSession session) {
+ session.synchronize();
+ }
+
+ @Override
+ public void onSynchronized(SynchronizerSession session) {
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ @Override
+ public void onSynchronizeFailed(SynchronizerSession session, Exception lastException, String reason) {
+ WaitHelper.getTestWaiter().performNotify(lastException);
+ }
+
+ @Override
+ public void onSynchronizeSkipped(SynchronizerSession synchronizerSession) {
+ WaitHelper.getTestWaiter().performNotify(new RuntimeException("Not expecting onSynchronizeSkipped"));
+ }
+ });
+ }
+
+ protected void logStats() {
+ // Uncomment this line to print stats to console:
+ // Logger.startLoggingTo(new PrintLogWriter(new PrintWriter(System.out, true)));
+
+ Logger.debug(LOG_TAG, "Repo A fetch done: " + repoA.stats.fetchCompleted);
+ Logger.debug(LOG_TAG, "Repo B store done: " + repoB.stats.storeCompleted);
+ Logger.debug(LOG_TAG, "Repo B fetch done: " + repoB.stats.fetchCompleted);
+ Logger.debug(LOG_TAG, "Repo A store done: " + repoA.stats.storeCompleted);
+
+ SynchronizerConfiguration sc = syncSession.getSynchronizer().save();
+ Logger.debug(LOG_TAG, "Repo A timestamp: " + sc.remoteBundle.getTimestamp());
+ Logger.debug(LOG_TAG, "Repo B timestamp: " + sc.localBundle.getTimestamp());
+ }
+
+ protected void doTest(boolean remoteDataAvailable, boolean localDataAvailable) {
+ ((DataAvailableWBORepository) repoA).dataAvailable = remoteDataAvailable;
+ ((DataAvailableWBORepository) repoB).dataAvailable = localDataAvailable;
+
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ final Context context = null;
+ syncSession.init(context,
+ new RepositorySessionBundle(0),
+ new RepositorySessionBundle(0));
+ }
+ });
+
+ logStats();
+ }
+
+ @Test
+ public void testSynchronizerSessionBothHaveData() {
+ long before = System.currentTimeMillis();
+ boolean remoteDataAvailable = true;
+ boolean localDataAvailable = true;
+ doTest(remoteDataAvailable, localDataAvailable);
+ long after = System.currentTimeMillis();
+
+ assertEquals(1, syncSession.getInboundCount());
+ assertEquals(2, syncSession.getOutboundCount());
+
+ // Didn't lose any records.
+ assertFirstContainsSecond(repoA.wbos, originalWbosA);
+ assertFirstContainsSecond(repoB.wbos, originalWbosB);
+ // Got new records.
+ assertFirstContainsSecond(repoA.wbos, originalWbosB);
+ assertFirstContainsSecond(repoB.wbos, originalWbosA);
+
+ // Timestamps updated.
+ SynchronizerConfiguration sc = syncSession.getSynchronizer().save();
+ TestSynchronizer.assertInRangeInclusive(before, sc.localBundle.getTimestamp(), after);
+ TestSynchronizer.assertInRangeInclusive(before, sc.remoteBundle.getTimestamp(), after);
+ }
+
+ @Test
+ public void testSynchronizerSessionOnlyLocalHasData() {
+ long before = System.currentTimeMillis();
+ boolean remoteDataAvailable = false;
+ boolean localDataAvailable = true;
+ doTest(remoteDataAvailable, localDataAvailable);
+ long after = System.currentTimeMillis();
+
+ // Record counts updated.
+ assertEquals(0, syncSession.getInboundCount());
+ assertEquals(2, syncSession.getOutboundCount());
+
+ // Didn't lose any records.
+ assertFirstContainsSecond(repoA.wbos, originalWbosA);
+ assertFirstContainsSecond(repoB.wbos, originalWbosB);
+ // Got new records.
+ assertFirstContainsSecond(repoA.wbos, originalWbosB);
+ // Didn't get records we shouldn't have fetched.
+ assertFirstDoesNotContainSecond(repoB.wbos, originalWbosA);
+
+ // Timestamps updated.
+ SynchronizerConfiguration sc = syncSession.getSynchronizer().save();
+ TestSynchronizer.assertInRangeInclusive(before, sc.localBundle.getTimestamp(), after);
+ TestSynchronizer.assertInRangeInclusive(before, sc.remoteBundle.getTimestamp(), after);
+ }
+
+ @Test
+ public void testSynchronizerSessionOnlyRemoteHasData() {
+ long before = System.currentTimeMillis();
+ boolean remoteDataAvailable = true;
+ boolean localDataAvailable = false;
+ doTest(remoteDataAvailable, localDataAvailable);
+ long after = System.currentTimeMillis();
+
+ // Record counts updated.
+ assertEquals(1, syncSession.getInboundCount());
+ assertEquals(0, syncSession.getOutboundCount());
+
+ // Didn't lose any records.
+ assertFirstContainsSecond(repoA.wbos, originalWbosA);
+ assertFirstContainsSecond(repoB.wbos, originalWbosB);
+ // Got new records.
+ assertFirstContainsSecond(repoB.wbos, originalWbosA);
+ // Didn't get records we shouldn't have fetched.
+ assertFirstDoesNotContainSecond(repoA.wbos, originalWbosB);
+
+ // Timestamps updated.
+ SynchronizerConfiguration sc = syncSession.getSynchronizer().save();
+ TestSynchronizer.assertInRangeInclusive(before, sc.localBundle.getTimestamp(), after);
+ TestSynchronizer.assertInRangeInclusive(before, sc.remoteBundle.getTimestamp(), after);
+ }
+
+ @Test
+ public void testSynchronizerSessionNeitherHaveData() {
+ long before = System.currentTimeMillis();
+ boolean remoteDataAvailable = false;
+ boolean localDataAvailable = false;
+ doTest(remoteDataAvailable, localDataAvailable);
+ long after = System.currentTimeMillis();
+
+ // Record counts updated.
+ assertEquals(0, syncSession.getInboundCount());
+ assertEquals(0, syncSession.getOutboundCount());
+
+ // Didn't lose any records.
+ assertFirstContainsSecond(repoA.wbos, originalWbosA);
+ assertFirstContainsSecond(repoB.wbos, originalWbosB);
+ // Didn't get records we shouldn't have fetched.
+ assertFirstDoesNotContainSecond(repoA.wbos, originalWbosB);
+ assertFirstDoesNotContainSecond(repoB.wbos, originalWbosA);
+
+ // Timestamps updated.
+ SynchronizerConfiguration sc = syncSession.getSynchronizer().save();
+ TestSynchronizer.assertInRangeInclusive(before, sc.localBundle.getTimestamp(), after);
+ TestSynchronizer.assertInRangeInclusive(before, sc.remoteBundle.getTimestamp(), after);
+ }
+
+ protected void doSkipTest(boolean remoteShouldSkip, boolean localShouldSkip) {
+ repoA = new ShouldSkipWBORepository(remoteShouldSkip);
+ repoB = new ShouldSkipWBORepository(localShouldSkip);
+
+ Synchronizer synchronizer = new Synchronizer();
+ synchronizer.repositoryA = repoA;
+ synchronizer.repositoryB = repoB;
+
+ syncSession = new SynchronizerSession(synchronizer, new SynchronizerSessionDelegate() {
+ @Override
+ public void onInitialized(SynchronizerSession session) {
+ session.synchronize();
+ }
+
+ @Override
+ public void onSynchronized(SynchronizerSession session) {
+ WaitHelper.getTestWaiter().performNotify(new RuntimeException("Not expecting onSynchronized"));
+ }
+
+ @Override
+ public void onSynchronizeFailed(SynchronizerSession session, Exception lastException, String reason) {
+ WaitHelper.getTestWaiter().performNotify(lastException);
+ }
+
+ @Override
+ public void onSynchronizeSkipped(SynchronizerSession synchronizerSession) {
+ WaitHelper.getTestWaiter().performNotify();
+ }
+ });
+
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ final Context context = null;
+ syncSession.init(context,
+ new RepositorySessionBundle(100),
+ new RepositorySessionBundle(200));
+ }
+ });
+
+ // If we skip, we don't update timestamps or even un-bundle.
+ SynchronizerConfiguration sc = syncSession.getSynchronizer().save();
+ assertNotNull(sc);
+ assertNull(sc.localBundle);
+ assertNull(sc.remoteBundle);
+ assertEquals(-1, syncSession.getInboundCount());
+ assertEquals(-1, syncSession.getOutboundCount());
+ }
+
+ @Test
+ public void testSynchronizerSessionShouldSkip() {
+ // These combinations should all skip.
+ doSkipTest(true, false);
+
+ doSkipTest(false, true);
+ doSkipTest(true, true);
+
+ try {
+ doSkipTest(false, false);
+ fail("Expected exception.");
+ } catch (WaitHelper.InnerError e) {
+ assertTrue(e.innerError instanceof RuntimeException);
+ assertEquals("Not expecting onSynchronized", e.innerError.getMessage());
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestUtils.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestUtils.java
new file mode 100644
index 0000000000..bc9a99dae9
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestUtils.java
@@ -0,0 +1,154 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.SyncConstants;
+import org.mozilla.gecko.sync.Utils;
+
+import java.io.UnsupportedEncodingException;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
+public class TestUtils extends Utils {
+
+ @Test
+ public void testGenerateGUID() {
+ for (int i = 0; i < 1000; ++i) {
+ assertEquals(12, Utils.generateGuid().length());
+ }
+ }
+
+ public static final byte[][] BYTE_ARRS = {
+ new byte[] {' '}, // Tab.
+ new byte[] {'0'},
+ new byte[] {'A'},
+ new byte[] {'a'},
+ new byte[] {'I', 'U'},
+ new byte[] {'`', 'h', 'g', ' ', 's', '`'},
+ new byte[] {}
+ };
+ // Indices correspond with the above array.
+ public static final String[] STRING_ARR = {
+ "09",
+ "30",
+ "41",
+ "61",
+ "4955",
+ "606867207360",
+ ""
+ };
+
+ @Test
+ public void testByte2Hex() throws Exception {
+ for (int i = 0; i < BYTE_ARRS.length; ++i) {
+ final byte[] b = BYTE_ARRS[i];
+ final String expected = STRING_ARR[i];
+ assertEquals(expected, Utils.byte2Hex(b));
+ }
+ }
+
+ @Test
+ public void testHex2Byte() throws Exception {
+ for (int i = 0; i < STRING_ARR.length; ++i) {
+ final String s = STRING_ARR[i];
+ final byte[] expected = BYTE_ARRS[i];
+ assertTrue(Arrays.equals(expected, Utils.hex2Byte(s)));
+ }
+ }
+
+ @Test
+ public void testByte2Hex2ByteAndViceVersa() throws Exception { // There and back again!
+ for (int i = 0; i < BYTE_ARRS.length; ++i) {
+ // byte2Hex2Byte
+ final byte[] b = BYTE_ARRS[i];
+ final String s = Utils.byte2Hex(b);
+ assertTrue(Arrays.equals(b, Utils.hex2Byte(s)));
+ }
+
+ // hex2Byte2Hex
+ for (int i = 0; i < STRING_ARR.length; ++i) {
+ final String s = STRING_ARR[i];
+ final byte[] b = Utils.hex2Byte(s);
+ assertEquals(s, Utils.byte2Hex(b));
+ }
+ }
+
+ @Test
+ public void testByte2HexLength() throws Exception {
+ for (int i = 0; i < BYTE_ARRS.length; ++i) {
+ final byte[] b = BYTE_ARRS[i];
+ final String expected = STRING_ARR[i];
+ assertEquals(expected, Utils.byte2Hex(b, b.length));
+ assertEquals("0" + expected, Utils.byte2Hex(b, 2 * b.length + 1));
+ assertEquals("00" + expected, Utils.byte2Hex(b, 2 * b.length + 2));
+ }
+ }
+
+ @Test
+ public void testHex2ByteLength() throws Exception {
+ for (int i = 0; i < STRING_ARR.length; ++i) {
+ final String s = STRING_ARR[i];
+ final byte[] expected = BYTE_ARRS[i];
+ assertTrue(Arrays.equals(expected, Utils.hex2Byte(s)));
+ final byte[] expected1 = new byte[expected.length + 1];
+ System.arraycopy(expected, 0, expected1, 1, expected.length);
+ assertTrue(Arrays.equals(expected1, Utils.hex2Byte("00" + s)));
+ final byte[] expected2 = new byte[expected.length + 2];
+ System.arraycopy(expected, 0, expected2, 2, expected.length);
+ assertTrue(Arrays.equals(expected2, Utils.hex2Byte("0000" + s)));
+ }
+ }
+
+ @Test
+ public void testToCommaSeparatedString() {
+ ArrayList<String> xs = new ArrayList<String>();
+ assertEquals("", Utils.toCommaSeparatedString(null));
+ assertEquals("", Utils.toCommaSeparatedString(xs));
+ xs.add("test1");
+ assertEquals("test1", Utils.toCommaSeparatedString(xs));
+ xs.add("test2");
+ assertEquals("test1, test2", Utils.toCommaSeparatedString(xs));
+ xs.add("test3");
+ assertEquals("test1, test2, test3", Utils.toCommaSeparatedString(xs));
+ }
+
+ @Test
+ public void testUsernameFromAccount() throws NoSuchAlgorithmException, UnsupportedEncodingException {
+ assertEquals("xee7ffonluzpdp66l6xgpyh2v2w6ojkc", Utils.sha1Base32("foobar@baz.com"));
+ assertEquals("xee7ffonluzpdp66l6xgpyh2v2w6ojkc", Utils.usernameFromAccount("foobar@baz.com"));
+ assertEquals("xee7ffonluzpdp66l6xgpyh2v2w6ojkc", Utils.usernameFromAccount("FooBar@Baz.com"));
+ assertEquals("xee7ffonluzpdp66l6xgpyh2v2w6ojkc", Utils.usernameFromAccount("xee7ffonluzpdp66l6xgpyh2v2w6ojkc"));
+ assertEquals("foobar", Utils.usernameFromAccount("foobar"));
+ assertEquals("foobar", Utils.usernameFromAccount("FOOBAr"));
+ }
+
+ @Test
+ public void testGetPrefsPath() throws NoSuchAlgorithmException, UnsupportedEncodingException {
+ assertEquals("ore7dlrwqi6xr7honxdtpvmh6tly4r7k", Utils.sha1Base32("test.url.com:xee7ffonluzpdp66l6xgpyh2v2w6ojkc"));
+
+ assertEquals("sync.prefs.ore7dlrwqi6xr7honxdtpvmh6tly4r7k", Utils.getPrefsPath("product", "foobar@baz.com", "test.url.com", "default", 0));
+ assertEquals("sync.prefs.ore7dlrwqi6xr7honxdtpvmh6tly4r7k", Utils.getPrefsPath("org.mozilla.firefox_beta", "FooBar@Baz.com", "test.url.com", "default", 0));
+ assertEquals("sync.prefs.ore7dlrwqi6xr7honxdtpvmh6tly4r7k", Utils.getPrefsPath("org.mozilla.firefox", "xee7ffonluzpdp66l6xgpyh2v2w6ojkc", "test.url.com", "profile", 0));
+
+ assertEquals("sync.prefs.product.ore7dlrwqi6xr7honxdtpvmh6tly4r7k.default.1", Utils.getPrefsPath("product", "foobar@baz.com", "test.url.com", "default", 1));
+ assertEquals("sync.prefs.with!spaces_underbars!periods.ore7dlrwqi6xr7honxdtpvmh6tly4r7k.default.1", Utils.getPrefsPath("with spaces_underbars.periods", "foobar@baz.com", "test.url.com", "default", 1));
+ assertEquals("sync.prefs.org!mozilla!firefox_beta.ore7dlrwqi6xr7honxdtpvmh6tly4r7k.default.2", Utils.getPrefsPath("org.mozilla.firefox_beta", "FooBar@Baz.com", "test.url.com", "default", 2));
+ assertEquals("sync.prefs.org!mozilla!firefox.ore7dlrwqi6xr7honxdtpvmh6tly4r7k.profile.3", Utils.getPrefsPath("org.mozilla.firefox", "xee7ffonluzpdp66l6xgpyh2v2w6ojkc", "test.url.com", "profile", 3));
+ }
+
+ @Test
+ public void testObfuscateEmail() {
+ assertEquals("XXX@XXX.XXX", Utils.obfuscateEmail("foo@bar.com"));
+ assertEquals("XXXX@XXX.XXXX.XX", Utils.obfuscateEmail("foot@bar.test.ca"));
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/BaseTestStorageRequestDelegate.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/BaseTestStorageRequestDelegate.java
new file mode 100644
index 0000000000..a879256088
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/BaseTestStorageRequestDelegate.java
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+import java.io.IOException;
+
+import static org.junit.Assert.fail;
+
+public class BaseTestStorageRequestDelegate implements
+ SyncStorageRequestDelegate {
+
+ protected final AuthHeaderProvider authHeaderProvider;
+
+ public BaseTestStorageRequestDelegate(AuthHeaderProvider authHeaderProvider) {
+ this.authHeaderProvider = authHeaderProvider;
+ }
+
+ public BaseTestStorageRequestDelegate(String username, String password) {
+ this(new BasicAuthHeaderProvider(username, password));
+ }
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return authHeaderProvider;
+ }
+
+ @Override
+ public String ifUnmodifiedSince() {
+ return null;
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse response) {
+ BaseResource.consumeEntity(response);
+ fail("Should not be called.");
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ System.out.println("Response: " + response.httpResponse().getStatusLine().getStatusCode());
+ BaseResource.consumeEntity(response);
+ fail("Should not be called.");
+ }
+
+ @Override
+ public void handleRequestError(Exception e) {
+ if (e instanceof IOException) {
+ System.out.println("WARNING: TEST FAILURE IGNORED!");
+ // Assume that this is because Jenkins doesn't have network access.
+ return;
+ }
+ fail("Should not error.");
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessDelegate.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessDelegate.java
new file mode 100644
index 0000000000..cf3545c1ed
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessDelegate.java
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+
+public class ExpectSuccessDelegate {
+ public WaitHelper waitHelper;
+
+ public ExpectSuccessDelegate(WaitHelper waitHelper) {
+ this.waitHelper = waitHelper;
+ }
+
+ public void performNotify() {
+ this.waitHelper.performNotify();
+ }
+
+ public void performNotify(Throwable e) {
+ this.waitHelper.performNotify(e);
+ }
+
+ public String logTag() {
+ return this.getClass().getSimpleName();
+ }
+
+ public void log(String message) {
+ Logger.info(logTag(), message);
+ }
+
+ public void log(String message, Throwable throwable) {
+ Logger.warn(logTag(), message, throwable);
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionBeginDelegate.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionBeginDelegate.java
new file mode 100644
index 0000000000..d7cb186f82
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionBeginDelegate.java
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import junit.framework.AssertionFailedError;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
+
+import java.util.concurrent.ExecutorService;
+
+public class ExpectSuccessRepositorySessionBeginDelegate
+extends ExpectSuccessDelegate
+implements RepositorySessionBeginDelegate {
+
+ public ExpectSuccessRepositorySessionBeginDelegate(WaitHelper waitHelper) {
+ super(waitHelper);
+ }
+
+ @Override
+ public void onBeginFailed(Exception ex) {
+ log("Session begin failed.", ex);
+ performNotify(new AssertionFailedError("Session begin failed: " + ex.getMessage()));
+ }
+
+ @Override
+ public void onBeginSucceeded(RepositorySession session) {
+ log("Session begin succeeded.");
+ performNotify();
+ }
+
+ @Override
+ public RepositorySessionBeginDelegate deferredBeginDelegate(ExecutorService executor) {
+ log("Session begin delegate deferred.");
+ return this;
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionCreationDelegate.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionCreationDelegate.java
new file mode 100644
index 0000000000..8860baf774
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionCreationDelegate.java
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import junit.framework.AssertionFailedError;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+
+public class ExpectSuccessRepositorySessionCreationDelegate extends
+ ExpectSuccessDelegate implements RepositorySessionCreationDelegate {
+
+ public ExpectSuccessRepositorySessionCreationDelegate(WaitHelper waitHelper) {
+ super(waitHelper);
+ }
+
+ @Override
+ public void onSessionCreateFailed(Exception ex) {
+ log("Session creation failed.", ex);
+ performNotify(new AssertionFailedError("onSessionCreateFailed: session creation should not have failed."));
+ }
+
+ @Override
+ public void onSessionCreated(RepositorySession session) {
+ log("Session creation succeeded.");
+ performNotify();
+ }
+
+ @Override
+ public RepositorySessionCreationDelegate deferredCreationDelegate() {
+ log("Session creation deferred.");
+ return this;
+ }
+
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionFetchRecordsDelegate.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionFetchRecordsDelegate.java
new file mode 100644
index 0000000000..5f5cf89959
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionFetchRecordsDelegate.java
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import junit.framework.AssertionFailedError;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import java.util.ArrayList;
+import java.util.concurrent.ExecutorService;
+
+public class ExpectSuccessRepositorySessionFetchRecordsDelegate extends
+ ExpectSuccessDelegate implements RepositorySessionFetchRecordsDelegate {
+ public ArrayList<Record> fetchedRecords = new ArrayList<Record>();
+
+ public ExpectSuccessRepositorySessionFetchRecordsDelegate(WaitHelper waitHelper) {
+ super(waitHelper);
+ }
+
+ @Override
+ public void onFetchFailed(Exception ex, Record record) {
+ log("Fetch failed.", ex);
+ performNotify(new AssertionFailedError("onFetchFailed: fetch should not have failed."));
+ }
+
+ @Override
+ public void onFetchedRecord(Record record) {
+ fetchedRecords.add(record);
+ log("Fetched record with guid '" + record.guid + "'.");
+ }
+
+ @Override
+ public void onFetchCompleted(long end) {
+ log("Fetch completed.");
+ performNotify();
+ }
+
+ @Override
+ public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService executor) {
+ return this;
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionFinishDelegate.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionFinishDelegate.java
new file mode 100644
index 0000000000..4435d5fa2c
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionFinishDelegate.java
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import junit.framework.AssertionFailedError;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
+
+import java.util.concurrent.ExecutorService;
+
+public class ExpectSuccessRepositorySessionFinishDelegate extends
+ ExpectSuccessDelegate implements RepositorySessionFinishDelegate {
+
+ public ExpectSuccessRepositorySessionFinishDelegate(WaitHelper waitHelper) {
+ super(waitHelper);
+ }
+
+ @Override
+ public void onFinishFailed(Exception ex) {
+ log("Finish failed.", ex);
+ performNotify(new AssertionFailedError("onFinishFailed: finish should not have failed."));
+ }
+
+ @Override
+ public void onFinishSucceeded(RepositorySession session, RepositorySessionBundle bundle) {
+ log("Finish succeeded.");
+ performNotify();
+ }
+
+ @Override
+ public RepositorySessionFinishDelegate deferredFinishDelegate(ExecutorService executor) {
+ return this;
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionStoreDelegate.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionStoreDelegate.java
new file mode 100644
index 0000000000..cfca180fa1
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionStoreDelegate.java
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import junit.framework.AssertionFailedError;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
+
+import java.util.concurrent.ExecutorService;
+
+public class ExpectSuccessRepositorySessionStoreDelegate extends
+ ExpectSuccessDelegate implements RepositorySessionStoreDelegate {
+
+ public ExpectSuccessRepositorySessionStoreDelegate(WaitHelper waitHelper) {
+ super(waitHelper);
+ }
+
+ @Override
+ public void onRecordStoreFailed(Exception ex, String guid) {
+ log("Record store failed.", ex);
+ performNotify(new AssertionFailedError("onRecordStoreFailed: record store should not have failed."));
+ }
+
+ @Override
+ public void onRecordStoreSucceeded(String guid) {
+ log("Record store succeeded.");
+ }
+
+ @Override
+ public void onStoreCompleted(long storeEnd) {
+ log("Record store completed at " + storeEnd);
+ }
+
+ @Override
+ public RepositorySessionStoreDelegate deferredStoreDelegate(ExecutorService executor) {
+ return this;
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositoryWipeDelegate.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositoryWipeDelegate.java
new file mode 100644
index 0000000000..0f248dda7f
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositoryWipeDelegate.java
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import junit.framework.AssertionFailedError;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
+
+import java.util.concurrent.ExecutorService;
+
+public class ExpectSuccessRepositoryWipeDelegate extends ExpectSuccessDelegate
+ implements RepositorySessionWipeDelegate {
+
+ public ExpectSuccessRepositoryWipeDelegate(WaitHelper waitHelper) {
+ super(waitHelper);
+ }
+
+ @Override
+ public void onWipeSucceeded() {
+ log("Wipe succeeded.");
+ performNotify();
+ }
+
+ @Override
+ public void onWipeFailed(Exception ex) {
+ log("Wipe failed.", ex);
+ performNotify(new AssertionFailedError("onWipeFailed: wipe should not have failed."));
+ }
+
+ @Override
+ public RepositorySessionWipeDelegate deferredWipeDelegate(ExecutorService executor) {
+ log("Wipe deferred.");
+ return this;
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/HTTPServerTestHelper.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/HTTPServerTestHelper.java
new file mode 100644
index 0000000000..1829bdd128
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/HTTPServerTestHelper.java
@@ -0,0 +1,226 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BaseResourceDelegate;
+import org.simpleframework.http.core.ContainerSocketProcessor;
+import org.simpleframework.transport.connect.Connection;
+import org.simpleframework.transport.connect.SocketConnection;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+import static org.junit.Assert.fail;
+
+/**
+ * Test helper code to bind <code>MockServer</code> instances to ports.
+ * <p>
+ * Maintains a collection of running servers and (by default) throws helpful
+ * errors if two servers are started "on top" of each other. The
+ * <b>unchecked</b> exception thrown contains a stack trace pointing to where
+ * the new server is being created and where the pre-existing server was
+ * created.
+ * <p>
+ * Parses a system property to determine current test port, which is fixed for
+ * the duration of a test execution.
+ */
+public class HTTPServerTestHelper {
+ private static final String LOG_TAG = "HTTPServerTestHelper";
+
+ /**
+ * Port to run HTTP servers on during this test execution.
+ * <p>
+ * Lazily initialized on first call to {@link #getTestPort}.
+ */
+ public static Integer testPort = null;
+
+ public static final String LOCAL_HTTP_PORT_PROPERTY = "android.sync.local.http.port";
+ public static final int LOCAL_HTTP_PORT_DEFAULT = 15125;
+
+ public final int port;
+
+ public Connection connection;
+ public MockServer server;
+
+ /**
+ * Create a helper to bind <code>MockServer</code> instances.
+ * <p>
+ * Use {@link #getTestPort} to determine the port this helper will bind to.
+ */
+ public HTTPServerTestHelper() {
+ this.port = getTestPort();
+ }
+
+ // For testing only.
+ protected HTTPServerTestHelper(int port) {
+ this.port = port;
+ }
+
+ /**
+ * Lazily initialize test port for this test execution.
+ * <p>
+ * Only called from {@link #getTestPort}.
+ * <p>
+ * If the test port has not been determined, we try to parse it from a system
+ * property; if that fails, we return the default test port.
+ */
+ protected synchronized static void ensureTestPort() {
+ if (testPort != null) {
+ return;
+ }
+
+ String value = System.getProperty(LOCAL_HTTP_PORT_PROPERTY);
+ if (value != null) {
+ try {
+ testPort = Integer.valueOf(value);
+ } catch (NumberFormatException e) {
+ Logger.warn(LOG_TAG, "Got exception parsing local test port; ignoring. ", e);
+ }
+ }
+
+ if (testPort == null) {
+ testPort = Integer.valueOf(LOCAL_HTTP_PORT_DEFAULT);
+ }
+ }
+
+ /**
+ * The port to which all HTTP servers will be found for the duration of this
+ * test execution.
+ * <p>
+ * We try to parse the port from a system property; if that fails, we return
+ * the default test port.
+ *
+ * @return port number.
+ */
+ public synchronized static int getTestPort() {
+ if (testPort == null) {
+ ensureTestPort();
+ }
+
+ return testPort.intValue();
+ }
+
+ /**
+ * Used to maintain a stack trace pointing to where a server was started.
+ */
+ public static class HTTPServerStartedError extends Error {
+ private static final long serialVersionUID = -6778447718799087274L;
+
+ public final HTTPServerTestHelper httpServer;
+
+ public HTTPServerStartedError(HTTPServerTestHelper httpServer) {
+ this.httpServer = httpServer;
+ }
+ }
+
+ /**
+ * Thrown when a server is started "on top" of another server. The cause error
+ * will be an <code>HTTPServerStartedError</code> with a stack trace pointing
+ * to where the pre-existing server was started.
+ */
+ public static class HTTPServerAlreadyRunningError extends Error {
+ private static final long serialVersionUID = -6778447718799087275L;
+
+ public HTTPServerAlreadyRunningError(Throwable e) {
+ super(e);
+ }
+ }
+
+ /**
+ * Maintain a hash of running servers. Each value is an error with a stack
+ * traces pointing to where that server was started.
+ * <p>
+ * We don't key on the server itself because each server is a <it>helper</it>
+ * that may be started many times with different <code>MockServer</code>
+ * instances.
+ * <p>
+ * Synchronize access on the class.
+ */
+ protected static Map<Connection, HTTPServerStartedError> runningServers =
+ new IdentityHashMap<Connection, HTTPServerStartedError>();
+
+ protected synchronized static void throwIfServerAlreadyRunning() {
+ for (HTTPServerStartedError value : runningServers.values()) {
+ throw new HTTPServerAlreadyRunningError(value);
+ }
+ }
+
+ protected synchronized static void registerServerAsRunning(HTTPServerTestHelper httpServer) {
+ if (httpServer == null || httpServer.connection == null) {
+ throw new IllegalArgumentException("HTTPServerTestHelper or connection was null; perhaps server has not been started?");
+ }
+
+ HTTPServerStartedError old = runningServers.put(httpServer.connection, new HTTPServerStartedError(httpServer));
+ if (old != null) {
+ // Should never happen.
+ throw old;
+ }
+ }
+
+ protected synchronized static void unregisterServerAsRunning(HTTPServerTestHelper httpServer) {
+ if (httpServer == null || httpServer.connection == null) {
+ throw new IllegalArgumentException("HTTPServerTestHelper or connection was null; perhaps server has not been started?");
+ }
+
+ runningServers.remove(httpServer.connection);
+ }
+
+ public MockServer startHTTPServer(MockServer server, boolean allowMultipleServers) {
+ BaseResource.rewriteLocalhost = false; // No sense rewriting when we're running the unit tests.
+ BaseResourceDelegate.connectionTimeoutInMillis = 1000; // No sense waiting a long time for a local connection.
+
+ if (!allowMultipleServers) {
+ throwIfServerAlreadyRunning();
+ }
+
+ try {
+ this.server = server;
+ connection = new SocketConnection(new ContainerSocketProcessor(server));
+ SocketAddress address = new InetSocketAddress(port);
+ connection.connect(address);
+
+ registerServerAsRunning(this);
+
+ Logger.info(LOG_TAG, "Started HTTP server on port " + port + ".");
+ } catch (IOException ex) {
+ Logger.error(LOG_TAG, "Error starting HTTP server on port " + port + ".", ex);
+ fail(ex.toString());
+ }
+
+ return server;
+ }
+
+ public MockServer startHTTPServer(MockServer server) {
+ return startHTTPServer(server, false);
+ }
+
+ public MockServer startHTTPServer() {
+ return startHTTPServer(new MockServer());
+ }
+
+ public void stopHTTPServer() {
+ try {
+ if (connection != null) {
+ unregisterServerAsRunning(this);
+
+ connection.close();
+ }
+ server = null;
+ connection = null;
+
+ Logger.info(LOG_TAG, "Stopped HTTP server on port " + port + ".");
+
+ Logger.debug(LOG_TAG, "Closing connection pool...");
+ BaseResource.shutdownConnectionManager();
+ } catch (IOException ex) {
+ Logger.error(LOG_TAG, "Error stopping HTTP server on port " + port + ".", ex);
+ fail(ex.toString());
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockGlobalSessionCallback.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockGlobalSessionCallback.java
new file mode 100644
index 0000000000..5d7e8edd12
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockGlobalSessionCallback.java
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
+
+import java.net.URI;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * A callback for use with a GlobalSession that records what happens for later
+ * inspection.
+ *
+ * This callback is expected to be used from within the friendly confines of a
+ * WaitHelper performWait.
+ */
+public class MockGlobalSessionCallback implements GlobalSessionCallback {
+ protected WaitHelper testWaiter() {
+ return WaitHelper.getTestWaiter();
+ }
+
+ public int stageCounter = Stage.values().length - 1; // Exclude starting state.
+ public boolean calledSuccess = false;
+ public boolean calledError = false;
+ public Exception calledErrorException = null;
+ public boolean calledAborted = false;
+ public boolean calledRequestBackoff = false;
+ public boolean calledInformUnauthorizedResponse = false;
+ public boolean calledInformUpgradeRequiredResponse = false;
+ public boolean calledInformMigrated = false;
+ public URI calledInformUnauthorizedResponseClusterURL = null;
+ public long weaveBackoff = -1;
+
+ @Override
+ public void handleSuccess(GlobalSession globalSession) {
+ this.calledSuccess = true;
+ assertEquals(0, this.stageCounter);
+ this.testWaiter().performNotify();
+ }
+
+ @Override
+ public void handleAborted(GlobalSession globalSession, String reason) {
+ this.calledAborted = true;
+ this.testWaiter().performNotify();
+ }
+
+ @Override
+ public void handleError(GlobalSession globalSession, Exception ex) {
+ this.calledError = true;
+ this.calledErrorException = ex;
+ this.testWaiter().performNotify();
+ }
+
+ @Override
+ public void handleStageCompleted(Stage currentState,
+ GlobalSession globalSession) {
+ stageCounter--;
+ }
+
+ @Override
+ public void requestBackoff(long backoff) {
+ this.calledRequestBackoff = true;
+ this.weaveBackoff = backoff;
+ }
+
+ @Override
+ public void informUnauthorizedResponse(GlobalSession session, URI clusterURL) {
+ this.calledInformUnauthorizedResponse = true;
+ this.calledInformUnauthorizedResponseClusterURL = clusterURL;
+ }
+
+ @Override
+ public void informUpgradeRequiredResponse(GlobalSession session) {
+ this.calledInformUpgradeRequiredResponse = true;
+ }
+
+ @Override
+ public void informMigrated(GlobalSession session) {
+ this.calledInformMigrated = true;
+ }
+
+ @Override
+ public boolean shouldBackOffStorage() {
+ return false;
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockResourceDelegate.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockResourceDelegate.java
new file mode 100644
index 0000000000..2cac07904b
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockResourceDelegate.java
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+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.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+import org.mozilla.gecko.sync.net.ResourceDelegate;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+import static org.junit.Assert.assertEquals;
+
+public class MockResourceDelegate implements ResourceDelegate {
+ public WaitHelper waitHelper = null;
+ public static String USER_PASS = "john:password";
+ public static String EXPECT_BASIC = "Basic am9objpwYXNzd29yZA==";
+
+ public boolean handledHttpResponse = false;
+ public HttpResponse httpResponse = null;
+
+ public MockResourceDelegate(WaitHelper waitHelper) {
+ this.waitHelper = waitHelper;
+ }
+
+ public MockResourceDelegate() {
+ this.waitHelper = WaitHelper.getTestWaiter();
+ }
+
+ @Override
+ public String getUserAgent() {
+ return null;
+ }
+
+ @Override
+ public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
+ }
+
+ @Override
+ public int connectionTimeout() {
+ return 0;
+ }
+
+ @Override
+ public int socketTimeout() {
+ return 0;
+ }
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return new BasicAuthHeaderProvider(USER_PASS);
+ }
+
+ @Override
+ public void handleHttpProtocolException(ClientProtocolException e) {
+ waitHelper.performNotify(e);
+ }
+
+ @Override
+ public void handleHttpIOException(IOException e) {
+ waitHelper.performNotify(e);
+ }
+
+ @Override
+ public void handleTransportException(GeneralSecurityException e) {
+ waitHelper.performNotify(e);
+ }
+
+ @Override
+ public void handleHttpResponse(HttpResponse response) {
+ handledHttpResponse = true;
+ httpResponse = response;
+
+ assertEquals(response.getStatusLine().getStatusCode(), 200);
+ BaseResource.consumeEntity(response);
+ waitHelper.performNotify();
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockServer.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockServer.java
new file mode 100644
index 0000000000..4e1d6d7ad1
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockServer.java
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.Utils;
+import org.simpleframework.http.Request;
+import org.simpleframework.http.Response;
+import org.simpleframework.http.core.Container;
+
+import java.io.IOException;
+import java.io.PrintStream;
+
+import static org.junit.Assert.assertEquals;
+
+public class MockServer implements Container {
+ public static final String LOG_TAG = "MockServer";
+
+ public int statusCode = 200;
+ public String body = "Hello World";
+
+ public MockServer() {
+ }
+
+ public MockServer(int statusCode, String body) {
+ this.statusCode = statusCode;
+ this.body = body;
+ }
+
+ public String expectedBasicAuthHeader;
+
+ protected PrintStream handleBasicHeaders(Request request, Response response, int code, String contentType) throws IOException {
+ return this.handleBasicHeaders(request, response, code, contentType, System.currentTimeMillis());
+ }
+
+ protected PrintStream handleBasicHeaders(Request request, Response response, int code, String contentType, long time) throws IOException {
+ Logger.debug(LOG_TAG, "< Auth header: " + request.getValue("Authorization"));
+
+ PrintStream bodyStream = response.getPrintStream();
+ response.setCode(code);
+ response.setValue("Content-Type", contentType);
+ response.setValue("Server", "HelloWorld/1.0 (Simple 4.0)");
+ response.setDate("Date", time);
+ response.setDate("Last-Modified", time);
+
+ final String timestampHeader = Utils.millisecondsToDecimalSecondsString(time);
+ response.setValue("X-Weave-Timestamp", timestampHeader);
+ Logger.debug(LOG_TAG, "> X-Weave-Timestamp header: " + timestampHeader);
+ response.setValue("X-Last-Modified", "12345678");
+ return bodyStream;
+ }
+
+ protected void handle(Request request, Response response, int code, String body) {
+ try {
+ Logger.debug(LOG_TAG, "Handling request...");
+ PrintStream bodyStream = this.handleBasicHeaders(request, response, code, "application/json");
+
+ if (expectedBasicAuthHeader != null) {
+ Logger.debug(LOG_TAG, "Expecting auth header " + expectedBasicAuthHeader);
+ assertEquals(request.getValue("Authorization"), expectedBasicAuthHeader);
+ }
+
+ bodyStream.println(body);
+ bodyStream.close();
+ } catch (IOException e) {
+ Logger.error(LOG_TAG, "Oops.");
+ }
+ }
+ public void handle(Request request, Response response) {
+ this.handle(request, response, statusCode, body);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockSyncClientsEngineStage.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockSyncClientsEngineStage.java
new file mode 100644
index 0000000000..efd379e134
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockSyncClientsEngineStage.java
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.sync.stage.SyncClientsEngineStage;
+
+import static org.junit.Assert.assertTrue;
+
+public class MockSyncClientsEngineStage extends SyncClientsEngineStage {
+ public class MockClientUploadDelegate extends ClientUploadDelegate {
+ HTTPServerTestHelper data;
+
+ public MockClientUploadDelegate(HTTPServerTestHelper data) {
+ this.data = data;
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse response) {
+ assertTrue(response.wasSuccessful());
+ data.stopHTTPServer();
+ super.handleRequestSuccess(response);
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ BaseResource.consumeEntity(response);
+ data.stopHTTPServer();
+ super.handleRequestFailure(response);
+ }
+
+ @Override
+ public void handleRequestError(Exception ex) {
+ ex.printStackTrace();
+ data.stopHTTPServer();
+ super.handleRequestError(ex);
+ }
+ }
+
+ public class TestClientDownloadDelegate extends ClientDownloadDelegate {
+ HTTPServerTestHelper data;
+
+ public TestClientDownloadDelegate(HTTPServerTestHelper data) {
+ this.data = data;
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse response) {
+ assertTrue(response.wasSuccessful());
+ BaseResource.consumeEntity(response);
+ data.stopHTTPServer();
+ super.handleRequestSuccess(response);
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ BaseResource.consumeEntity(response);
+ super.handleRequestFailure(response);
+ data.stopHTTPServer();
+ }
+
+ @Override
+ public void handleRequestError(Exception ex) {
+ ex.printStackTrace();
+ super.handleRequestError(ex);
+ data.stopHTTPServer();
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockWBOServer.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockWBOServer.java
new file mode 100644
index 0000000000..164274bac1
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockWBOServer.java
@@ -0,0 +1,28 @@
+package org.mozilla.android.sync.test.helpers;
+
+import org.simpleframework.http.Path;
+import org.simpleframework.http.Request;
+import org.simpleframework.http.Response;
+
+import java.util.HashMap;
+
+/**
+ * A trivial server that collects and returns WBOs.
+ *
+ * @author rnewman
+ *
+ */
+public class MockWBOServer extends MockServer {
+ public HashMap<String, HashMap<String, String> > collections;
+
+ public MockWBOServer() {
+ collections = new HashMap<String, HashMap<String, String> >();
+ }
+
+ @Override
+ public void handle(Request request, Response response) {
+ Path path = request.getPath();
+ path.getPath(0);
+ // TODO
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/test/TestHTTPServerTestHelper.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/test/TestHTTPServerTestHelper.java
new file mode 100644
index 0000000000..d79998cc96
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/test/TestHTTPServerTestHelper.java
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers.test;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
+import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper.HTTPServerAlreadyRunningError;
+import org.mozilla.android.sync.test.helpers.MockServer;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+@RunWith(TestRunner.class)
+public class TestHTTPServerTestHelper {
+ public static final int TEST_PORT = HTTPServerTestHelper.getTestPort();
+
+ protected MockServer mockServer = new MockServer();
+
+ @Test
+ public void testStartStop() {
+ // Need to be able to start and stop multiple times.
+ for (int i = 0; i < 2; i++) {
+ HTTPServerTestHelper httpServer = new HTTPServerTestHelper();
+
+ assertNull(httpServer.connection);
+ httpServer.startHTTPServer(mockServer);
+
+ assertNotNull(httpServer.connection);
+ httpServer.stopHTTPServer();
+ }
+ }
+
+ public void startAgain() {
+ HTTPServerTestHelper httpServer = new HTTPServerTestHelper();
+ httpServer.startHTTPServer(mockServer);
+ }
+
+ @Test
+ public void testStartTwice() {
+ HTTPServerTestHelper httpServer = new HTTPServerTestHelper();
+
+ httpServer.startHTTPServer(mockServer);
+ assertNotNull(httpServer.connection);
+
+ // Should not be able to start multiple times.
+ try {
+ try {
+ startAgain();
+
+ fail("Expected exception.");
+ } catch (Throwable e) {
+ assertEquals(HTTPServerAlreadyRunningError.class, e.getClass());
+
+ StringWriter sw = new StringWriter();
+ e.printStackTrace(new PrintWriter(sw));
+ String s = sw.toString();
+
+ // Ensure we get a useful stack trace.
+ // We should have the method trying to start the server the second time...
+ assertTrue(s.contains("startAgain"));
+ // ... as well as the the method that started the server the first time.
+ assertTrue(s.contains("testStartTwice"));
+ }
+ } finally {
+ httpServer.stopHTTPServer();
+ }
+ }
+
+ protected static class LeakyHTTPServerTestHelper extends HTTPServerTestHelper {
+ // Make this constructor public, just for this test.
+ public LeakyHTTPServerTestHelper(int port) {
+ super(port);
+ }
+ }
+
+ @Test
+ public void testForceStartTwice() {
+ HTTPServerTestHelper httpServer1 = new HTTPServerTestHelper();
+ HTTPServerTestHelper httpServer2 = new LeakyHTTPServerTestHelper(httpServer1.port + 1);
+
+ // Should be able to start multiple times if we specify it.
+ try {
+ httpServer1.startHTTPServer(mockServer);
+ assertNotNull(httpServer1.connection);
+
+ httpServer2.startHTTPServer(mockServer, true);
+ assertNotNull(httpServer2.connection);
+ } finally {
+ httpServer1.stopHTTPServer();
+ httpServer2.stopHTTPServer();
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/GeckoNetworkManagerTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/GeckoNetworkManagerTest.java
new file mode 100644
index 0000000000..8b07d74483
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/GeckoNetworkManagerTest.java
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.GeckoNetworkManager.ManagerState;
+import org.mozilla.gecko.GeckoNetworkManager.ManagerEvent;
+
+import static org.junit.Assert.*;
+
+@RunWith(TestRunner.class)
+public class GeckoNetworkManagerTest {
+ /**
+ * Tests the transition matrix.
+ */
+ @Test
+ public void testGetNextState() {
+ ManagerState testingState;
+
+ testingState = ManagerState.OffNoListeners;
+ assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.disableNotifications));
+ assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.stop));
+ assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.receivedUpdate));
+ assertEquals(ManagerState.OnNoListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.start));
+ assertEquals(ManagerState.OffWithListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.enableNotifications));
+
+ testingState = ManagerState.OnNoListeners;
+ assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.start));
+ assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.disableNotifications));
+ assertEquals(ManagerState.OnWithListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.enableNotifications));
+ assertEquals(ManagerState.OffNoListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.stop));
+ assertEquals(ManagerState.OnNoListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.receivedUpdate));
+
+ testingState = ManagerState.OnWithListeners;
+ assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.start));
+ assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.enableNotifications));
+ assertEquals(ManagerState.OffWithListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.stop));
+ assertEquals(ManagerState.OnNoListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.disableNotifications));
+ assertEquals(ManagerState.OnWithListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.receivedUpdate));
+
+ testingState = ManagerState.OffWithListeners;
+ assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.stop));
+ assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.enableNotifications));
+ assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.receivedUpdate));
+ assertEquals(ManagerState.OnWithListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.start));
+ assertEquals(ManagerState.OffNoListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.disableNotifications));
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/GlobalPageMetadataTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/GlobalPageMetadataTest.java
new file mode 100644
index 0000000000..f30ee7a2c0
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/GlobalPageMetadataTest.java
@@ -0,0 +1,174 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko;
+
+import android.content.ContentProviderClient;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.os.RemoteException;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.db.DelegatingTestContentProvider;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserContract.PageMetadata;
+import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.BrowserProvider;
+import org.mozilla.gecko.db.LocalBrowserDB;
+import org.robolectric.shadows.ShadowContentResolver;
+
+import static org.junit.Assert.*;
+
+@RunWith(TestRunner.class)
+public class GlobalPageMetadataTest {
+ @Test
+ public void testQueueing() throws Exception {
+ BrowserDB db = new LocalBrowserDB("default");
+
+ BrowserProvider provider = new BrowserProvider();
+ try {
+ provider.onCreate();
+ ShadowContentResolver.registerProvider(BrowserContract.AUTHORITY, new DelegatingTestContentProvider(provider));
+
+ ShadowContentResolver cr = new ShadowContentResolver();
+ ContentProviderClient pageMetadataClient = cr.acquireContentProviderClient(PageMetadata.CONTENT_URI);
+
+ assertEquals(0, GlobalPageMetadata.getInstance().getMetadataQueueSize());
+
+ // There's not history record for this uri, so test that queueing works.
+ GlobalPageMetadata.getInstance().doAddOrQueue(db, pageMetadataClient, "https://mozilla.org", false, "{type: 'article'}");
+
+ assertPageMetadataCountForGUID(0, "guid1", pageMetadataClient);
+ assertEquals(1, GlobalPageMetadata.getInstance().getMetadataQueueSize());
+
+ // Test that queue doesn't duplicate metadata for the same history item.
+ GlobalPageMetadata.getInstance().doAddOrQueue(db, pageMetadataClient, "https://mozilla.org", false, "{type: 'article'}");
+ assertEquals(1, GlobalPageMetadata.getInstance().getMetadataQueueSize());
+
+ // Test that queue is limited to 15 metadata items.
+ for (int i = 0; i < 20; i++) {
+ GlobalPageMetadata.getInstance().doAddOrQueue(db, pageMetadataClient, "https://mozilla.org/" + i, false, "{type: 'article'}");
+ }
+ assertEquals(15, GlobalPageMetadata.getInstance().getMetadataQueueSize());
+ } finally {
+ provider.shutdown();
+ }
+ }
+
+ @Test
+ public void testInsertingMetadata() throws Exception {
+ BrowserDB db = new LocalBrowserDB("default");
+
+ // Start listening for events.
+ GlobalPageMetadata.getInstance().init();
+
+ BrowserProvider provider = new BrowserProvider();
+ try {
+ provider.onCreate();
+ ShadowContentResolver.registerProvider(BrowserContract.AUTHORITY, new DelegatingTestContentProvider(provider));
+
+ ShadowContentResolver cr = new ShadowContentResolver();
+ ContentProviderClient historyClient = cr.acquireContentProviderClient(BrowserContract.History.CONTENT_URI);
+ ContentProviderClient pageMetadataClient = cr.acquireContentProviderClient(PageMetadata.CONTENT_URI);
+
+ // Insert required history item...
+ ContentValues cv = new ContentValues();
+ cv.put(BrowserContract.History.GUID, "guid1");
+ cv.put(BrowserContract.History.URL, "https://mozilla.org");
+ historyClient.insert(BrowserContract.History.CONTENT_URI, cv);
+
+ // TODO: Main test runner thread finishes before EventDispatcher events are processed...
+ // Fire off a message saying that history has been inserted.
+ // Bundle message = new Bundle();
+ // message.putString(GlobalHistory.EVENT_PARAM_URI, "https://mozilla.org");
+ // EventDispatcher.getInstance().dispatch(GlobalHistory.EVENT_URI_AVAILABLE_IN_HISTORY, message);
+
+ // For now, let's just try inserting again.
+ GlobalPageMetadata.getInstance().doAddOrQueue(db, pageMetadataClient, "https://mozilla.org", false, "{type: 'article', description: 'test article'}");
+
+ assertPageMetadataCountForGUID(1, "guid1", pageMetadataClient);
+ assertPageMetadataValues(pageMetadataClient, "guid1", false, "{\"type\":\"article\",\"description\":\"test article\"}");
+
+ // Test that inserting empty metadata deletes existing metadata record.
+ GlobalPageMetadata.getInstance().doAddOrQueue(db, pageMetadataClient, "https://mozilla.org", false, "{}");
+ assertPageMetadataCountForGUID(0, "guid1", pageMetadataClient);
+
+ // Test that inserting new metadata overrides existing metadata record.
+ GlobalPageMetadata.getInstance().doAddOrQueue(db, pageMetadataClient, "https://mozilla.org", true, "{type: 'article', description: 'test article', image_url: 'https://example.com/test.png'}");
+ assertPageMetadataValues(pageMetadataClient, "guid1", true, "{\"type\":\"article\",\"description\":\"test article\",\"image_url\":\"https:\\/\\/example.com\\/test.png\"}");
+
+ // Insert another history item...
+ cv = new ContentValues();
+ cv.put(BrowserContract.History.GUID, "guid2");
+ cv.put(BrowserContract.History.URL, "https://planet.mozilla.org");
+ historyClient.insert(BrowserContract.History.CONTENT_URI, cv);
+ // Test that empty metadata doesn't get inserted for a new history.
+ GlobalPageMetadata.getInstance().doAddOrQueue(db, pageMetadataClient, "https://planet.mozilla.org", false, "{}");
+
+ assertPageMetadataCountForGUID(0, "guid2", pageMetadataClient);
+
+ } finally {
+ provider.shutdown();
+ }
+ }
+
+ /**
+ * Expects cursor to be at the correct position.
+ */
+ private void assertCursorValues(Cursor cursor, String json, int hasImage, String guid) {
+ assertNotNull(cursor);
+ assertEquals(json, cursor.getString(cursor.getColumnIndexOrThrow(PageMetadata.JSON)));
+ assertEquals(hasImage, cursor.getInt(cursor.getColumnIndexOrThrow(PageMetadata.HAS_IMAGE)));
+ assertEquals(guid, cursor.getString(cursor.getColumnIndexOrThrow(PageMetadata.HISTORY_GUID)));
+ }
+
+ private void assertPageMetadataValues(ContentProviderClient client, String guid, boolean hasImage, String json) {
+ final Cursor cursor;
+
+ try {
+ cursor = client.query(PageMetadata.CONTENT_URI, new String[]{
+ PageMetadata.HISTORY_GUID,
+ PageMetadata.HAS_IMAGE,
+ PageMetadata.JSON,
+ PageMetadata.DATE_CREATED
+ }, PageMetadata.HISTORY_GUID + " = ?", new String[]{guid}, null);
+ } catch (RemoteException e) {
+ fail();
+ return;
+ }
+
+ assertNotNull(cursor);
+ try {
+ assertEquals(1, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+ assertCursorValues(cursor, json, hasImage ? 1 : 0, guid);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private void assertPageMetadataCountForGUID(int expected, String guid, ContentProviderClient client) {
+ final Cursor cursor;
+
+ try {
+ cursor = client.query(PageMetadata.CONTENT_URI, new String[]{
+ PageMetadata.HISTORY_GUID,
+ PageMetadata.HAS_IMAGE,
+ PageMetadata.JSON,
+ PageMetadata.DATE_CREATED
+ }, PageMetadata.HISTORY_GUID + " = ?", new String[]{guid}, null);
+ } catch (RemoteException e) {
+ fail();
+ return;
+ }
+
+ assertNotNull(cursor);
+ try {
+ assertEquals(expected, cursor.getCount());
+ } finally {
+ cursor.close();
+ }
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/TestGeckoProfile.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/TestGeckoProfile.java
new file mode 100644
index 0000000000..c01c2d21a6
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/TestGeckoProfile.java
@@ -0,0 +1,254 @@
+/* -*- 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.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.util.FileUtils;
+import org.robolectric.RuntimeEnvironment;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.UUID;
+
+import static org.junit.Assert.*;
+
+/**
+ * Unit test methods of the GeckoProfile class.
+ */
+@RunWith(TestRunner.class)
+public class TestGeckoProfile {
+ private static final String PROFILE_NAME = "profileName";
+
+ private static final String CLIENT_ID_JSON_ATTR = "clientID";
+ private static final String PROFILE_CREATION_DATE_JSON_ATTR = "created";
+
+ @Rule
+ public TemporaryFolder dirContainingProfile = new TemporaryFolder();
+
+ private File profileDir;
+ private GeckoProfile profile;
+
+ private File clientIdFile;
+ private File timesFile;
+
+ @Before
+ public void setUp() throws IOException {
+ final Context context = RuntimeEnvironment.application;
+ profileDir = dirContainingProfile.newFolder();
+ profile = GeckoProfile.get(context, PROFILE_NAME, profileDir);
+
+ clientIdFile = new File(profileDir, "datareporting/state.json");
+ timesFile = new File(profileDir, "times.json");
+ }
+
+ public void assertValidClientId(final String clientId) {
+ // This isn't the method we use in the main GeckoProfile code, but it should be equivalent.
+ UUID.fromString(clientId); // assert: will throw if null or invalid UUID.
+ }
+
+ @Test
+ public void testGetDir() {
+ assertEquals("Profile dir argument during construction and returned value are equal",
+ profileDir, profile.getDir());
+ }
+
+ @Test
+ public void testGetClientIdFreshProfile() throws Exception {
+ assertFalse("client ID file does not exist", clientIdFile.exists());
+
+ // No existing client ID file: we're expected to create one.
+ final String clientId = profile.getClientId();
+ assertValidClientId(clientId);
+ assertTrue("client ID file exists", clientIdFile.exists());
+
+ assertEquals("Returned client ID is the same as the one previously returned", clientId, profile.getClientId());
+ assertEquals("clientID file format matches expectations", clientId, readClientIdFromFile(clientIdFile));
+ }
+
+ @Test
+ public void testGetClientIdFileAlreadyExists() throws Exception {
+ final String validClientId = "905de1c0-0ea6-4a43-95f9-6170035f5a82";
+ assertTrue("Created the parent dirs of the client ID file", clientIdFile.getParentFile().mkdirs());
+ writeClientIdToFile(clientIdFile, validClientId);
+
+ final String clientIdFromProfile = profile.getClientId();
+ assertEquals("Client ID from method matches ID written to disk", validClientId, clientIdFromProfile);
+ }
+
+ @Test
+ public void testGetClientIdMigrateFromFHR() throws Exception {
+ final File fhrClientIdFile = new File(profileDir, "healthreport/state.json");
+ final String fhrClientId = "905de1c0-0ea6-4a43-95f9-6170035f5a82";
+
+ assertFalse("client ID file does not exist", clientIdFile.exists());
+ assertTrue("Created FHR data directory", new File(profileDir, "healthreport").mkdirs());
+ writeClientIdToFile(fhrClientIdFile, fhrClientId);
+ assertEquals("Migrated Client ID equals FHR client ID", fhrClientId, profile.getClientId());
+
+ // Verify migration wrote to contemporary client ID file.
+ assertTrue("Client ID file created during migration", clientIdFile.exists());
+ assertEquals("Migrated client ID on disk equals value returned from method",
+ fhrClientId, readClientIdFromFile(clientIdFile));
+
+ assertTrue("Deleted FHR clientID file", fhrClientIdFile.delete());
+ assertEquals("Ensure method calls read from newly created client ID file & not FHR client ID file",
+ fhrClientId, profile.getClientId());
+ }
+
+ @Test
+ public void testGetClientIdInvalidIdOnDisk() throws Exception {
+ assertTrue("Created the parent dirs of the client ID file", clientIdFile.getParentFile().mkdirs());
+ writeClientIdToFile(clientIdFile, "");
+ final String clientIdForEmptyString = profile.getClientId();
+ assertValidClientId(clientIdForEmptyString);
+ assertNotEquals("A new client ID was created when the empty String was written to disk", "", clientIdForEmptyString);
+
+ writeClientIdToFile(clientIdFile, "invalidClientId");
+ final String clientIdForInvalidClientId = profile.getClientId();
+ assertValidClientId(clientIdForInvalidClientId);
+ assertNotEquals("A new client ID was created when an invalid client ID was written to disk",
+ "invalidClientId", clientIdForInvalidClientId);
+ }
+
+ @Test
+ public void testGetClientIdMissingClientIdJSONAttr() throws Exception {
+ final String validClientId = "905de1c0-0ea6-4a43-95f9-6170035f5a82";
+ final JSONObject objMissingClientId = new JSONObject();
+ objMissingClientId.put("irrelevantKey", validClientId);
+ assertTrue("Created the parent dirs of the client ID file", clientIdFile.getParentFile().mkdirs());
+ FileUtils.writeJSONObjectToFile(clientIdFile, objMissingClientId);
+
+ final String clientIdForMissingAttr = profile.getClientId();
+ assertValidClientId(clientIdForMissingAttr);
+ assertNotEquals("Did not use other attr when JSON attr was missing", validClientId, clientIdForMissingAttr);
+ }
+
+ @Test
+ public void testGetClientIdInvalidIdFileFormat() throws Exception {
+ final String validClientId = "905de1c0-0ea6-4a43-95f9-6170035f5a82";
+ assertTrue("Created the parent dirs of the client ID file", clientIdFile.getParentFile().mkdirs());
+ FileUtils.writeStringToFile(clientIdFile, "clientID: \"" + validClientId + "\"");
+
+ final String clientIdForInvalidFormat = profile.getClientId();
+ assertValidClientId(clientIdForInvalidFormat);
+ assertNotEquals("Created new ID when file format was invalid", validClientId, clientIdForInvalidFormat);
+ }
+
+ @Test
+ public void testEnsureParentDirs() {
+ final File grandParentDir = new File(profileDir, "grandParent");
+ final File parentDir = new File(grandParentDir, "parent");
+ final File childFile = new File(parentDir, "child");
+
+ // Assert initial state.
+ assertFalse("Topmost parent dir should not exist yet", grandParentDir.exists());
+ assertFalse("Bottommost parent dir should not exist yet", parentDir.exists());
+ assertFalse("Child file should not exist", childFile.exists());
+
+ final String fakeFullPath = "grandParent/parent/child";
+ assertTrue("Parent directories should be created", profile.ensureParentDirs(fakeFullPath));
+ assertTrue("Topmost parent dir should have been created", grandParentDir.exists());
+ assertTrue("Bottommost parent dir should have been created", parentDir.exists());
+ assertFalse("Child file should not have been created", childFile.exists());
+
+ // Parents already exist because this is the second time we're calling ensureParentDirs.
+ assertTrue("Expect true if parent directories already exist", profile.ensureParentDirs(fakeFullPath));
+
+ // Assert error condition.
+ assertTrue("Ensure we can change permissions on profile dir for testing", profileDir.setReadOnly());
+ assertFalse("Expect false if the parent dir could not be created", profile.ensureParentDirs("unwritableDir/child"));
+ }
+
+ @Test
+ public void testIsClientIdValid() {
+ final String[] validClientIds = new String[] {
+ "905de1c0-0ea6-4a43-95f9-6170035f5a82",
+ "905de1c0-0ea6-4a43-95f9-6170035f5a83",
+ "57472f82-453d-4c55-b59c-d3c0e97b76a1",
+ "895745d1-f31e-46c3-880e-b4dd72963d4f",
+ };
+ for (final String validClientId : validClientIds) {
+ assertTrue("Client ID, " + validClientId + ", is valid", profile.isClientIdValid(validClientId));
+ }
+
+ final String[] invalidClientIds = new String[] {
+ null,
+ "",
+ "a",
+ "anInvalidClientId",
+ "905de1c0-0ea6-4a43-95f9-6170035f5a820", // too long (last section)
+ "905de1c0-0ea6-4a43-95f9-6170035f5a8", // too short (last section)
+ "05de1c0-0ea6-4a43-95f9-6170035f5a82", // too short (first section)
+ "905de1c0-0ea6-4a43-95f9-6170035f5a8!", // contains a symbol
+ };
+ for (final String invalidClientId : invalidClientIds) {
+ assertFalse("Client ID, " + invalidClientId + ", is invalid", profile.isClientIdValid(invalidClientId));
+ }
+
+ // We generate client IDs using UUID - better make sure they're valid.
+ for (int i = 0; i < 30; ++i) {
+ final String generatedClientId = UUID.randomUUID().toString();
+ assertTrue("Generated client ID from UUID, " + generatedClientId + ", is valid",
+ profile.isClientIdValid(generatedClientId));
+ }
+ }
+
+ @Test
+ public void testGetProfileCreationDateFromTimesFile() throws Exception {
+ final long expectedDate = System.currentTimeMillis();
+ final JSONObject expectedObj = new JSONObject();
+ expectedObj.put(PROFILE_CREATION_DATE_JSON_ATTR, expectedDate);
+ FileUtils.writeJSONObjectToFile(timesFile, expectedObj);
+
+ final Context context = RuntimeEnvironment.application;
+ final long actualDate = profile.getAndPersistProfileCreationDate(context);
+ assertEquals("Date from disk equals date inserted to disk", expectedDate, actualDate);
+
+ final long actualDateFromDisk = readProfileCreationDateFromFile(timesFile);
+ assertEquals("Date in times.json has not changed after accessing profile creation date",
+ expectedDate, actualDateFromDisk);
+ }
+
+ @Test
+ public void testGetProfileCreationDateTimesFileDoesNotExist() throws Exception {
+ assertFalse("Times.json does not already exist", timesFile.exists());
+
+ final Context context = RuntimeEnvironment.application;
+ final long actualDate = profile.getAndPersistProfileCreationDate(context);
+ // I'd prefer to mock so we can return and verify a specific value but we can't mock
+ // GeckoProfile because it's final. Instead, we check if the value is at least reasonable.
+ assertTrue("Date from method is positive", actualDate >= 0);
+ assertTrue("Date from method is less than current time", actualDate < System.currentTimeMillis());
+
+ assertTrue("Times.json exists after getting profile", timesFile.exists());
+ final long actualDateFromDisk = readProfileCreationDateFromFile(timesFile);
+ assertEquals("Date from disk equals returned value", actualDate, actualDateFromDisk);
+ }
+
+ private static long readProfileCreationDateFromFile(final File file) throws Exception {
+ final JSONObject actualObj = FileUtils.readJSONObjectFromFile(file);
+ return actualObj.getLong(PROFILE_CREATION_DATE_JSON_ATTR);
+ }
+
+ private String readClientIdFromFile(final File file) throws Exception {
+ final JSONObject obj = FileUtils.readJSONObjectFromFile(file);
+ return obj.getString(CLIENT_ID_JSON_ATTR);
+ }
+
+ private void writeClientIdToFile(final File file, final String clientId) throws Exception {
+ final JSONObject obj = new JSONObject();
+ obj.put(CLIENT_ID_JSON_ATTR, clientId);
+ FileUtils.writeJSONObjectToFile(file, obj);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/activitystream/TestActivityStream.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/activitystream/TestActivityStream.java
new file mode 100644
index 0000000000..71f01b437f
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/activitystream/TestActivityStream.java
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.activitystream;
+
+import android.os.SystemClock;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.robolectric.Robolectric;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowLooper;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(TestRunner.class)
+public class TestActivityStream {
+ /**
+ * Unit tests for ActivityStream.extractLabel().
+ *
+ * Most test cases are based on this list:
+ * https://gist.github.com/nchapman/36502ad115e8825d522a66549971a3f0
+ */
+ @Test
+ public void testExtractLabelWithPath() {
+ // Empty values
+ assertLabelEquals("", "", true);
+ assertLabelEquals("", null, true);
+
+ // Without path
+ assertLabelEquals("news.ycombinator", "https://news.ycombinator.com/", true);
+ assertLabelEquals("sql.telemetry.mozilla", "https://sql.telemetry.mozilla.org/", true);
+ assertLabelEquals("sso.mozilla", "http://sso.mozilla.com/", true);
+ assertLabelEquals("youtube", "http://youtube.com/", true);
+ assertLabelEquals("images.google", "http://images.google.com/", true);
+ assertLabelEquals("smile.amazon", "http://smile.amazon.com/", true);
+ assertLabelEquals("localhost", "http://localhost:5000/", true);
+ assertLabelEquals("independent", "http://www.independent.co.uk/", true);
+
+ // With path
+ assertLabelEquals("firefox", "https://addons.mozilla.org/en-US/firefox/", true);
+ assertLabelEquals("activity-stream", "https://trello.com/b/KX3hV8XS/activity-stream", true);
+ assertLabelEquals("activity-stream", "https://github.com/mozilla/activity-stream", true);
+ assertLabelEquals("sidekiq", "https://dispatch-news.herokuapp.com/sidekiq", true);
+ assertLabelEquals("nchapman", "https://github.com/nchapman/", true);
+
+ // Unusable paths
+ assertLabelEquals("phonebook.mozilla","https://phonebook.mozilla.org/mellon/login?ReturnTo=https%3A%2F%2Fphonebook.mozilla.org%2F&IdP=http%3A%2F%2Fwww.okta.com", true);
+ assertLabelEquals("ipay.adp", "https://ipay.adp.com/iPay/index.jsf", true);
+ assertLabelEquals("calendar.google", "https://calendar.google.com/calendar/render?pli=1#main_7", true);
+ assertLabelEquals("myworkday", "https://www.myworkday.com/vhr_mozilla/d/home.htmld", true);
+ assertLabelEquals("mail.google", "https://mail.google.com/mail/u/1/#inbox", true);
+ assertLabelEquals("docs.google", "https://docs.google.com/presentation/d/11cyrcwhKTmBdEBIZ3szLO0-_Imrx2CGV2B9_LZHDrds/edit#slide=id.g15d41bb0f3_0_82", true);
+
+ // Special cases
+ assertLabelEquals("irccloud.mozilla", "https://irccloud.mozilla.com/#!/ircs://irc1.dmz.scl3.mozilla.com:6697/%23universal-search", true);
+ }
+
+ @Test
+ public void testExtractLabelWithoutPath() {
+ assertLabelEquals("addons.mozilla", "https://addons.mozilla.org/en-US/firefox/", false);
+ assertLabelEquals("trello", "https://trello.com/b/KX3hV8XS/activity-stream", false);
+ assertLabelEquals("github", "https://github.com/mozilla/activity-stream", false);
+ assertLabelEquals("dispatch-news", "https://dispatch-news.herokuapp.com/sidekiq", false);
+ assertLabelEquals("github", "https://github.com/nchapman/", false);
+ }
+
+ private void assertLabelEquals(String expectedLabel, String url, boolean usePath) {
+ final String[] actualLabel = new String[1];
+
+ ActivityStream.LabelCallback callback = new ActivityStream.LabelCallback() {
+ @Override
+ public void onLabelExtracted(String label) {
+ actualLabel[0] = label;
+ }
+ };
+
+ ActivityStream.extractLabel(RuntimeEnvironment.application, url, usePath, callback);
+
+ ShadowLooper.runUiThreadTasks();
+
+ assertEquals(expectedLabel, actualLabel[0]);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/common/log/writers/test/TestLogWriters.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/common/log/writers/test/TestLogWriters.java
new file mode 100644
index 0000000000..61dd339650
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/common/log/writers/test/TestLogWriters.java
@@ -0,0 +1,179 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.common.log.writers.test;
+
+import android.util.Log;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.common.log.writers.LevelFilteringLogWriter;
+import org.mozilla.gecko.background.common.log.writers.LogWriter;
+import org.mozilla.gecko.background.common.log.writers.PrintLogWriter;
+import org.mozilla.gecko.background.common.log.writers.SimpleTagLogWriter;
+import org.mozilla.gecko.background.common.log.writers.StringLogWriter;
+import org.mozilla.gecko.background.common.log.writers.ThreadLocalTagLogWriter;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
+public class TestLogWriters {
+
+ public static final String TEST_LOG_TAG_1 = "TestLogTag1";
+ public static final String TEST_LOG_TAG_2 = "TestLogTag2";
+
+ public static final String TEST_MESSAGE_1 = "LOG TEST MESSAGE one";
+ public static final String TEST_MESSAGE_2 = "LOG TEST MESSAGE two";
+ public static final String TEST_MESSAGE_3 = "LOG TEST MESSAGE three";
+
+ @Before
+ public void setUp() {
+ Logger.stopLoggingToAll();
+ }
+
+ @After
+ public void tearDown() {
+ Logger.stopLoggingToAll();
+ }
+
+ @Test
+ public void testStringLogWriter() {
+ StringLogWriter lw = new StringLogWriter();
+
+ Logger.error(TEST_LOG_TAG_1, TEST_MESSAGE_1, new RuntimeException());
+ Logger.startLoggingTo(lw);
+ Logger.error(TEST_LOG_TAG_1, TEST_MESSAGE_2);
+ Logger.warn(TEST_LOG_TAG_1, TEST_MESSAGE_2);
+ Logger.info(TEST_LOG_TAG_1, TEST_MESSAGE_2);
+ Logger.debug(TEST_LOG_TAG_1, TEST_MESSAGE_2);
+ Logger.trace(TEST_LOG_TAG_1, TEST_MESSAGE_2);
+ Logger.stopLoggingTo(lw);
+ Logger.error(TEST_LOG_TAG_2, TEST_MESSAGE_3, new RuntimeException());
+
+ String s = lw.toString();
+ assertFalse(s.contains("RuntimeException"));
+ assertFalse(s.contains(".java"));
+ assertTrue(s.contains(TEST_LOG_TAG_1));
+ assertFalse(s.contains(TEST_LOG_TAG_2));
+ assertFalse(s.contains(TEST_MESSAGE_1));
+ assertTrue(s.contains(TEST_MESSAGE_2));
+ assertFalse(s.contains(TEST_MESSAGE_3));
+ }
+
+ @Test
+ public void testSingleTagLogWriter() {
+ final String SINGLE_TAG = "XXX";
+ StringLogWriter lw = new StringLogWriter();
+
+ Logger.startLoggingTo(new SimpleTagLogWriter(SINGLE_TAG, lw));
+ Logger.error(TEST_LOG_TAG_1, TEST_MESSAGE_1);
+ Logger.warn(TEST_LOG_TAG_2, TEST_MESSAGE_2);
+
+ String s = lw.toString();
+ for (String line : s.split("\n")) {
+ assertTrue(line.startsWith(SINGLE_TAG));
+ }
+ assertTrue(s.startsWith(SINGLE_TAG + " :: E :: " + TEST_LOG_TAG_1));
+ }
+
+ @Test
+ public void testLevelFilteringLogWriter() {
+ StringLogWriter lw = new StringLogWriter();
+
+ assertFalse(new LevelFilteringLogWriter(Log.WARN, lw).shouldLogVerbose(TEST_LOG_TAG_1));
+ assertTrue(new LevelFilteringLogWriter(Log.VERBOSE, lw).shouldLogVerbose(TEST_LOG_TAG_1));
+
+ Logger.startLoggingTo(new LevelFilteringLogWriter(Log.WARN, lw));
+ Logger.error(TEST_LOG_TAG_1, TEST_MESSAGE_2);
+ Logger.warn(TEST_LOG_TAG_1, TEST_MESSAGE_2);
+ Logger.info(TEST_LOG_TAG_1, TEST_MESSAGE_2);
+ Logger.debug(TEST_LOG_TAG_1, TEST_MESSAGE_2);
+ Logger.trace(TEST_LOG_TAG_1, TEST_MESSAGE_2);
+
+ String s = lw.toString();
+ assertTrue(s.contains(PrintLogWriter.ERROR));
+ assertTrue(s.contains(PrintLogWriter.WARN));
+ assertFalse(s.contains(PrintLogWriter.INFO));
+ assertFalse(s.contains(PrintLogWriter.DEBUG));
+ assertFalse(s.contains(PrintLogWriter.VERBOSE));
+ }
+
+ @Test
+ public void testThreadLocalLogWriter() throws InterruptedException {
+ final InheritableThreadLocal<String> logTag = new InheritableThreadLocal<String>() {
+ @Override
+ protected String initialValue() {
+ return "PARENT";
+ }
+ };
+
+ final StringLogWriter stringLogWriter = new StringLogWriter();
+ final LogWriter logWriter = new ThreadLocalTagLogWriter(logTag, stringLogWriter);
+
+ try {
+ Logger.startLoggingTo(logWriter);
+
+ Logger.info("parent tag before", "parent message before");
+
+ int threads = 3;
+ final CountDownLatch latch = new CountDownLatch(threads);
+
+ for (int thread = 0; thread < threads; thread++) {
+ final int threadNumber = thread;
+
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ logTag.set("CHILD" + threadNumber);
+ Logger.info("child tag " + threadNumber, "child message " + threadNumber);
+ } finally {
+ latch.countDown();
+ }
+ }
+ }).start();
+ }
+
+ latch.await();
+
+ Logger.info("parent tag after", "parent message after");
+
+ String s = stringLogWriter.toString();
+ List<String> lines = Arrays.asList(s.split("\n"));
+
+ // Because tests are run in a multi-threaded environment, we get
+ // additional logs that are not generated by this test. So we test that we
+ // get all the messages in a reasonable order.
+ try {
+ int parent1 = lines.indexOf("PARENT :: I :: parent tag before :: parent message before");
+ int parent2 = lines.indexOf("PARENT :: I :: parent tag after :: parent message after");
+
+ assertTrue(parent1 >= 0);
+ assertTrue(parent2 >= 0);
+ assertTrue(parent1 < parent2);
+
+ for (int thread = 0; thread < threads; thread++) {
+ int child = lines.indexOf("CHILD" + thread + " :: I :: child tag " + thread + " :: child message " + thread);
+ assertTrue(child >= 0);
+ assertTrue(parent1 < child);
+ assertTrue(child < parent2);
+ }
+ } catch (Throwable e) {
+ // Shouldn't happen. Let's dump to aid debugging.
+ e.printStackTrace();
+ assertEquals("\0", s);
+ }
+ } finally {
+ Logger.stopLoggingTo(logWriter);
+ }
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/DelegatingTestContentProvider.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/DelegatingTestContentProvider.java
new file mode 100644
index 0000000000..91a36f7d14
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/DelegatingTestContentProvider.java
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.db;
+
+import android.content.ContentProvider;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentValues;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.net.Uri;
+
+import org.mozilla.gecko.db.BrowserContract;
+
+import java.util.ArrayList;
+
+/**
+ * Wrap a ContentProvider, appending &test=1 to all queries.
+ */
+public class DelegatingTestContentProvider extends ContentProvider {
+ protected final ContentProvider mTargetProvider;
+
+ protected static Uri appendUriParam(Uri uri, String param, String value) {
+ return uri.buildUpon().appendQueryParameter(param, value).build();
+ }
+
+ public DelegatingTestContentProvider(ContentProvider targetProvider) {
+ super();
+ mTargetProvider = targetProvider;
+ }
+
+ private Uri appendTestParam(Uri uri) {
+ return appendUriParam(uri, BrowserContract.PARAM_IS_TEST, "1");
+ }
+
+ @Override
+ public boolean onCreate() {
+ return mTargetProvider.onCreate();
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return mTargetProvider.getType(uri);
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return mTargetProvider.delete(appendTestParam(uri), selection, selectionArgs);
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ return mTargetProvider.insert(appendTestParam(uri), values);
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection,
+ String[] selectionArgs) {
+ return mTargetProvider.update(appendTestParam(uri), values,
+ selection, selectionArgs);
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ return mTargetProvider.query(appendTestParam(uri), projection, selection,
+ selectionArgs, sortOrder);
+ }
+
+ @Override
+ public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
+ throws OperationApplicationException {
+ return mTargetProvider.applyBatch(operations);
+ }
+
+ @Override
+ public int bulkInsert(Uri uri, ContentValues[] values) {
+ return mTargetProvider.bulkInsert(appendTestParam(uri), values);
+ }
+
+ public ContentProvider getTargetProvider() {
+ return mTargetProvider;
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/TestTabsProvider.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/TestTabsProvider.java
new file mode 100644
index 0000000000..f0d468e070
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/TestTabsProvider.java
@@ -0,0 +1,338 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.db;
+
+import android.content.ContentProviderClient;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+
+import org.json.simple.JSONArray;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.TabsProvider;
+import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
+import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository;
+import org.mozilla.gecko.sync.repositories.domain.TabsRecord;
+import org.robolectric.shadows.ShadowContentResolver;
+
+@RunWith(TestRunner.class)
+public class TestTabsProvider {
+ public static final String TEST_CLIENT_GUID = "test guid"; // Real GUIDs never contain spaces.
+ public static final String TEST_CLIENT_NAME = "test client name";
+
+ public static final String CLIENTS_GUID_IS = BrowserContract.Clients.GUID + " = ?";
+ public static final String TABS_CLIENT_GUID_IS = BrowserContract.Tabs.CLIENT_GUID + " = ?";
+
+ protected Tab testTab1;
+ protected Tab testTab2;
+ protected Tab testTab3;
+
+ protected TabsProvider provider;
+
+ @Before
+ public void setUp() {
+ provider = new TabsProvider();
+ provider.onCreate();
+ ShadowContentResolver.registerProvider(BrowserContract.TABS_AUTHORITY, new DelegatingTestContentProvider(provider));
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ provider.shutdown();
+ provider = null;
+ }
+
+ protected ContentProviderClient getClientsClient() {
+ final ShadowContentResolver cr = new ShadowContentResolver();
+ return cr.acquireContentProviderClient(BrowserContractHelpers.CLIENTS_CONTENT_URI);
+ }
+
+ protected ContentProviderClient getTabsClient() {
+ final ShadowContentResolver cr = new ShadowContentResolver();
+ return cr.acquireContentProviderClient(BrowserContractHelpers.TABS_CONTENT_URI);
+ }
+
+ protected int deleteTestClient(final ContentProviderClient clientsClient) throws RemoteException {
+ if (clientsClient == null) {
+ throw new IllegalStateException("Provided ContentProviderClient is null");
+ }
+ return clientsClient.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, CLIENTS_GUID_IS, new String[] { TEST_CLIENT_GUID });
+ }
+
+ protected int deleteAllTestTabs(final ContentProviderClient tabsClient) throws RemoteException {
+ if (tabsClient == null) {
+ throw new IllegalStateException("Provided ContentProviderClient is null");
+ }
+ return tabsClient.delete(BrowserContractHelpers.TABS_CONTENT_URI, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID });
+ }
+
+ protected void insertTestClient(final ContentProviderClient clientsClient) throws RemoteException {
+ ContentValues cv = new ContentValues();
+ cv.put(BrowserContract.Clients.GUID, TEST_CLIENT_GUID);
+ cv.put(BrowserContract.Clients.NAME, TEST_CLIENT_NAME);
+ clientsClient.insert(BrowserContractHelpers.CLIENTS_CONTENT_URI, cv);
+ }
+
+ @SuppressWarnings("unchecked")
+ protected void insertSomeTestTabs(ContentProviderClient tabsClient) throws RemoteException {
+ final JSONArray history1 = new JSONArray();
+ history1.add("http://test.com/test1.html");
+ testTab1 = new Tab("test title 1", "http://test.com/test1.png", history1, 1000);
+
+ final JSONArray history2 = new JSONArray();
+ history2.add("http://test.com/test2.html#1");
+ history2.add("http://test.com/test2.html#2");
+ history2.add("http://test.com/test2.html#3");
+ testTab2 = new Tab("test title 2", "http://test.com/test2.png", history2, 2000);
+
+ final JSONArray history3 = new JSONArray();
+ history3.add("http://test.com/test3.html#1");
+ history3.add("http://test.com/test3.html#2");
+ testTab3 = new Tab("test title 3", "http://test.com/test3.png", history3, 3000);
+
+ tabsClient.insert(BrowserContractHelpers.TABS_CONTENT_URI, testTab1.toContentValues(TEST_CLIENT_GUID, 0));
+ tabsClient.insert(BrowserContractHelpers.TABS_CONTENT_URI, testTab2.toContentValues(TEST_CLIENT_GUID, 1));
+ tabsClient.insert(BrowserContractHelpers.TABS_CONTENT_URI, testTab3.toContentValues(TEST_CLIENT_GUID, 2));
+ }
+
+ // Sanity.
+ @Test
+ public void testObtainCP() {
+ final ContentProviderClient clientsClient = getClientsClient();
+ Assert.assertNotNull(clientsClient);
+ clientsClient.release();
+
+ final ContentProviderClient tabsClient = getTabsClient();
+ Assert.assertNotNull(tabsClient);
+ tabsClient.release();
+ }
+
+ @Test
+ public void testDeleteEmptyClients() throws RemoteException {
+ final Uri uri = BrowserContractHelpers.CLIENTS_CONTENT_URI;
+ final ContentProviderClient clientsClient = getClientsClient();
+
+ // Have to ensure that it's empty…
+ clientsClient.delete(uri, null, null);
+
+ int deleted = clientsClient.delete(uri, null, null);
+ Assert.assertEquals(0, deleted);
+ }
+
+ @Test
+ public void testDeleteEmptyTabs() throws RemoteException {
+ final ContentProviderClient tabsClient = getTabsClient();
+
+ // Have to ensure that it's empty…
+ deleteAllTestTabs(tabsClient);
+
+ int deleted = deleteAllTestTabs(tabsClient);
+ Assert.assertEquals(0, deleted);
+ }
+
+ @Test
+ public void testStoreAndRetrieveClients() throws RemoteException {
+ final Uri uri = BrowserContractHelpers.CLIENTS_CONTENT_URI;
+ final ContentProviderClient clientsClient = getClientsClient();
+
+ // Have to ensure that it's empty…
+ clientsClient.delete(uri, null, null);
+
+ final long now = System.currentTimeMillis();
+ final ContentValues first = new ContentValues();
+ final ContentValues second = new ContentValues();
+ first.put(BrowserContract.Clients.GUID, "abcdefghijkl");
+ first.put(BrowserContract.Clients.NAME, "Frist Psot");
+ first.put(BrowserContract.Clients.LAST_MODIFIED, now + 1);
+ second.put(BrowserContract.Clients.GUID, "mnopqrstuvwx");
+ second.put(BrowserContract.Clients.NAME, "Second!!1!");
+ second.put(BrowserContract.Clients.LAST_MODIFIED, now + 2);
+
+ ContentValues[] values = new ContentValues[] { first, second };
+ final int inserted = clientsClient.bulkInsert(uri, values);
+ Assert.assertEquals(2, inserted);
+
+ final String since = BrowserContract.Clients.LAST_MODIFIED + " >= ?";
+ final String[] nowArg = new String[] { String.valueOf(now) };
+ final String guidAscending = BrowserContract.Clients.GUID + " ASC";
+ Cursor cursor = clientsClient.query(uri, null, since, nowArg, guidAscending);
+
+ Assert.assertNotNull(cursor);
+ try {
+ Assert.assertTrue(cursor.moveToFirst());
+ Assert.assertEquals(2, cursor.getCount());
+
+ final String g1 = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Clients.GUID));
+ final String n1 = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Clients.NAME));
+ final long m1 = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Clients.LAST_MODIFIED));
+ Assert.assertEquals(first.get(BrowserContract.Clients.GUID), g1);
+ Assert.assertEquals(first.get(BrowserContract.Clients.NAME), n1);
+ Assert.assertEquals(now + 1, m1);
+
+ Assert.assertTrue(cursor.moveToNext());
+ final String g2 = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Clients.GUID));
+ final String n2 = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Clients.NAME));
+ final long m2 = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Clients.LAST_MODIFIED));
+ Assert.assertEquals(second.get(BrowserContract.Clients.GUID), g2);
+ Assert.assertEquals(second.get(BrowserContract.Clients.NAME), n2);
+ Assert.assertEquals(now + 2, m2);
+
+ Assert.assertFalse(cursor.moveToNext());
+ } finally {
+ cursor.close();
+ }
+
+ int deleted = clientsClient.delete(uri, null, null);
+ Assert.assertEquals(2, deleted);
+ }
+
+ @Test
+ public void testTabFromCursor() throws Exception {
+ final ContentProviderClient tabsClient = getTabsClient();
+ final ContentProviderClient clientsClient = getClientsClient();
+
+ deleteAllTestTabs(tabsClient);
+ deleteTestClient(clientsClient);
+ insertTestClient(clientsClient);
+ insertSomeTestTabs(tabsClient);
+
+ final String positionAscending = BrowserContract.Tabs.POSITION + " ASC";
+ Cursor cursor = null;
+ try {
+ cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID }, positionAscending);
+ Assert.assertEquals(3, cursor.getCount());
+
+ cursor.moveToFirst();
+ final Tab parsed1 = Tab.fromCursor(cursor);
+ Assert.assertEquals(testTab1, parsed1);
+
+ cursor.moveToNext();
+ final Tab parsed2 = Tab.fromCursor(cursor);
+ Assert.assertEquals(testTab2, parsed2);
+
+ cursor.moveToPosition(2);
+ final Tab parsed3 = Tab.fromCursor(cursor);
+ Assert.assertEquals(testTab3, parsed3);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ @Test
+ public void testDeletingClientDeletesTabs() throws Exception {
+ final ContentProviderClient tabsClient = getTabsClient();
+ final ContentProviderClient clientsClient = getClientsClient();
+
+ deleteAllTestTabs(tabsClient);
+ deleteTestClient(clientsClient);
+ insertTestClient(clientsClient);
+ insertSomeTestTabs(tabsClient);
+
+ // Delete just the client...
+ clientsClient.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, CLIENTS_GUID_IS, new String [] { TEST_CLIENT_GUID });
+
+ Cursor cursor = null;
+ try {
+ cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID }, null);
+ // ... and all that client's tabs should be removed.
+ Assert.assertEquals(0, cursor.getCount());
+ } finally {
+ cursor.close();
+ }
+ }
+
+ @Test
+ public void testTabsRecordFromCursor() throws Exception {
+ final ContentProviderClient tabsClient = getTabsClient();
+
+ deleteAllTestTabs(tabsClient);
+ insertTestClient(getClientsClient());
+ insertSomeTestTabs(tabsClient);
+
+ final String positionAscending = BrowserContract.Tabs.POSITION + " ASC";
+ Cursor cursor = null;
+ try {
+ cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID }, positionAscending);
+ Assert.assertEquals(3, cursor.getCount());
+
+ cursor.moveToPosition(1);
+
+ final TabsRecord tabsRecord = FennecTabsRepository.tabsRecordFromCursor(cursor, TEST_CLIENT_GUID, TEST_CLIENT_NAME);
+
+ // Make sure we clean up after ourselves.
+ Assert.assertEquals(1, cursor.getPosition());
+
+ Assert.assertEquals(TEST_CLIENT_GUID, tabsRecord.guid);
+ Assert.assertEquals(TEST_CLIENT_NAME, tabsRecord.clientName);
+
+ Assert.assertEquals(3, tabsRecord.tabs.size());
+ Assert.assertEquals(testTab1, tabsRecord.tabs.get(0));
+ Assert.assertEquals(testTab2, tabsRecord.tabs.get(1));
+ Assert.assertEquals(testTab3, tabsRecord.tabs.get(2));
+
+ Assert.assertEquals(Math.max(Math.max(testTab1.lastUsed, testTab2.lastUsed), testTab3.lastUsed), tabsRecord.lastModified);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ // Verify that we can fetch a record when there are no local tabs at all.
+ @Test
+ public void testEmptyTabsRecordFromCursor() throws Exception {
+ final ContentProviderClient tabsClient = getTabsClient();
+
+ deleteAllTestTabs(tabsClient);
+
+ final String positionAscending = BrowserContract.Tabs.POSITION + " ASC";
+ Cursor cursor = null;
+ try {
+ cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID }, positionAscending);
+ Assert.assertEquals(0, cursor.getCount());
+
+ final TabsRecord tabsRecord = FennecTabsRepository.tabsRecordFromCursor(cursor, TEST_CLIENT_GUID, TEST_CLIENT_NAME);
+
+ Assert.assertEquals(TEST_CLIENT_GUID, tabsRecord.guid);
+ Assert.assertEquals(TEST_CLIENT_NAME, tabsRecord.clientName);
+
+ Assert.assertNotNull(tabsRecord.tabs);
+ Assert.assertEquals(0, tabsRecord.tabs.size());
+
+ Assert.assertEquals(0, tabsRecord.lastModified);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ // Not much of a test, but verifies the tabs record at least agrees with the
+ // disk data and doubles as a database inspector.
+ @Test
+ public void testLocalTabs() throws Exception {
+ final ContentProviderClient tabsClient = getTabsClient();
+
+ final String positionAscending = BrowserContract.Tabs.POSITION + " ASC";
+ Cursor cursor = null;
+ try {
+ // Keep this in sync with the Fennec schema.
+ cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, BrowserContract.Tabs.CLIENT_GUID + " IS NULL", null, positionAscending);
+ CursorDumper.dumpCursor(cursor);
+
+ final TabsRecord tabsRecord = FennecTabsRepository.tabsRecordFromCursor(cursor, TEST_CLIENT_GUID, TEST_CLIENT_NAME);
+
+ Assert.assertEquals(TEST_CLIENT_GUID, tabsRecord.guid);
+ Assert.assertEquals(TEST_CLIENT_NAME, tabsRecord.clientName);
+
+ Assert.assertNotNull(tabsRecord.tabs);
+ Assert.assertEquals(cursor.getCount(), tabsRecord.tabs.size());
+ } finally {
+ cursor.close();
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/TestTabsProviderRemoteTabs.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/TestTabsProviderRemoteTabs.java
new file mode 100644
index 0000000000..e63cb9b463
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/TestTabsProviderRemoteTabs.java
@@ -0,0 +1,244 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.db;
+
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.LocalTabsAccessor;
+import org.mozilla.gecko.db.RemoteClient;
+import org.mozilla.gecko.db.TabsProvider;
+import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.internal.runtime.RuntimeAdapter;
+import org.robolectric.shadows.ShadowContentResolver;
+
+import java.util.List;
+
+@RunWith(TestRunner.class)
+public class TestTabsProviderRemoteTabs {
+ 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;
+
+ protected TabsProvider provider;
+
+ @Before
+ public void setUp() {
+ provider = new TabsProvider();
+ provider.onCreate();
+ ShadowContentResolver.registerProvider(BrowserContract.TABS_AUTHORITY, new DelegatingTestContentProvider(provider));
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ provider.shutdown();
+ provider = null;
+ }
+
+ protected ContentProviderClient getClientsClient() {
+ final ShadowContentResolver cr = new ShadowContentResolver();
+ return cr.acquireContentProviderClient(BrowserContractHelpers.CLIENTS_CONTENT_URI);
+ }
+
+ @Test
+ public void testGetClientsWithoutTabsByRecencyFromCursor() throws Exception {
+ final Uri uri = BrowserContractHelpers.CLIENTS_CONTENT_URI;
+ final ContentProviderClient cpc = getClientsClient();
+ final LocalTabsAccessor accessor = new LocalTabsAccessor("test"); // The profile name given doesn't matter.
+
+ try {
+ // Delete all tabs to begin with.
+ cpc.delete(uri, null, null);
+ Cursor allClients = cpc.query(uri, null, null, null, null);
+ try {
+ Assert.assertEquals(0, allClients.getCount());
+ } finally {
+ allClients.close();
+ }
+
+ // Insert a local and remote1 client record, neither with tabs.
+ final long now = System.currentTimeMillis();
+ // Local client has GUID = null.
+ final ContentValues local = new ContentValues();
+ local.put(BrowserContract.Clients.NAME, "local");
+ local.put(BrowserContract.Clients.LAST_MODIFIED, now + 1);
+ // Remote clients have GUID != null.
+ final ContentValues remote1 = new ContentValues();
+ remote1.put(BrowserContract.Clients.GUID, "guid1");
+ remote1.put(BrowserContract.Clients.NAME, "remote1");
+ remote1.put(BrowserContract.Clients.LAST_MODIFIED, now + 2);
+
+ final ContentValues remote2 = new ContentValues();
+ remote2.put(BrowserContract.Clients.GUID, "guid2");
+ remote2.put(BrowserContract.Clients.NAME, "remote2");
+ remote2.put(BrowserContract.Clients.LAST_MODIFIED, now + 3);
+
+ ContentValues[] values = new ContentValues[]{local, remote1, remote2};
+ int inserted = cpc.bulkInsert(uri, values);
+ Assert.assertEquals(3, inserted);
+
+ allClients = cpc.query(BrowserContract.Clients.CONTENT_RECENCY_URI, null, null, null, null);
+ try {
+ CursorDumper.dumpCursor(allClients);
+ // The local client is not ignored.
+ Assert.assertEquals(3, allClients.getCount());
+ final List<RemoteClient> clients = accessor.getClientsWithoutTabsByRecencyFromCursor(allClients);
+ Assert.assertEquals(3, clients.size());
+ for (RemoteClient client : clients) {
+ // Each client should not have any tabs.
+ Assert.assertNotNull(client.tabs);
+ Assert.assertEquals(0, client.tabs.size());
+ }
+ // Since there are no tabs, the order should be based on last_modified.
+ Assert.assertEquals("guid2", clients.get(0).guid);
+ Assert.assertEquals("guid1", clients.get(1).guid);
+ Assert.assertEquals(null, clients.get(2).guid);
+ } finally {
+ allClients.close();
+ }
+
+ // Now let's add a few tabs to one client. The times are chosen so that one tab's
+ // last used is not relevant, and the other tab is the most recent used.
+ final ContentValues remoteTab1 = new ContentValues();
+ remoteTab1.put(BrowserContract.Tabs.CLIENT_GUID, "guid1");
+ remoteTab1.put(BrowserContract.Tabs.TITLE, "title1");
+ remoteTab1.put(BrowserContract.Tabs.URL, "http://test.com/test1");
+ remoteTab1.put(BrowserContract.Tabs.HISTORY, "[\"http://test.com/test1\"]");
+ remoteTab1.put(BrowserContract.Tabs.LAST_USED, now);
+ remoteTab1.put(BrowserContract.Tabs.POSITION, 0);
+
+ final ContentValues remoteTab2 = new ContentValues();
+ remoteTab2.put(BrowserContract.Tabs.CLIENT_GUID, "guid1");
+ remoteTab2.put(BrowserContract.Tabs.TITLE, "title2");
+ remoteTab2.put(BrowserContract.Tabs.URL, "http://test.com/test2");
+ remoteTab2.put(BrowserContract.Tabs.HISTORY, "[\"http://test.com/test2\"]");
+ remoteTab2.put(BrowserContract.Tabs.LAST_USED, now + 5);
+ remoteTab2.put(BrowserContract.Tabs.POSITION, 1);
+
+ values = new ContentValues[]{remoteTab1, remoteTab2};
+ inserted = cpc.bulkInsert(BrowserContract.Tabs.CONTENT_URI, values);
+ Assert.assertEquals(2, inserted);
+
+ allClients = cpc.query(BrowserContract.Clients.CONTENT_RECENCY_URI, null, BrowserContract.Clients.GUID + " IS NOT NULL", null, null);
+ try {
+ CursorDumper.dumpCursor(allClients);
+ // The local client is ignored.
+ Assert.assertEquals(2, allClients.getCount());
+ final List<RemoteClient> clients = accessor.getClientsWithoutTabsByRecencyFromCursor(allClients);
+ Assert.assertEquals(2, clients.size());
+ for (RemoteClient client : clients) {
+ // Each client should be remote and should not have any tabs.
+ Assert.assertNotNull(client.guid);
+ Assert.assertNotNull(client.tabs);
+ Assert.assertEquals(0, client.tabs.size());
+ }
+ // Since now there is a tab attached to the remote2 client more recent than the
+ // remote1 client modified time, it should be first.
+ Assert.assertEquals("guid1", clients.get(0).guid);
+ Assert.assertEquals("guid2", clients.get(1).guid);
+ } finally {
+ allClients.close();
+ }
+ } finally {
+ cpc.release();
+ }
+ }
+
+ @Test
+ public void testGetRecentRemoteClientsUpToOneWeekOld() throws Exception {
+ final Uri uri = BrowserContractHelpers.CLIENTS_CONTENT_URI;
+ final ContentProviderClient cpc = getClientsClient();
+ final LocalTabsAccessor accessor = new LocalTabsAccessor("test"); // The profile name given doesn't matter.
+ final Context context = RuntimeEnvironment.application.getApplicationContext();
+
+ try {
+ // Start Clean
+ cpc.delete(uri, null, null);
+ final Cursor allClients = cpc.query(uri, null, null, null, null);
+ try {
+ Assert.assertEquals(0, allClients.getCount());
+ } finally {
+ allClients.close();
+ }
+
+ // Insert a local and remote1 client record, neither with tabs.
+ final long now = System.currentTimeMillis();
+ // Local client has GUID = null.
+ final ContentValues local = new ContentValues();
+ local.put(BrowserContract.Clients.NAME, "local");
+ local.put(BrowserContract.Clients.LAST_MODIFIED, now + 1);
+ // Remote clients have GUID != null.
+ final ContentValues remote1 = new ContentValues();
+ remote1.put(BrowserContract.Clients.GUID, "guid1");
+ remote1.put(BrowserContract.Clients.NAME, "remote1");
+ remote1.put(BrowserContract.Clients.LAST_MODIFIED, now + 2);
+
+ // Insert a Remote Client that is 6 days old.
+ final ContentValues remote2 = new ContentValues();
+ remote2.put(BrowserContract.Clients.GUID, "guid2");
+ remote2.put(BrowserContract.Clients.NAME, "remote2");
+ remote2.put(BrowserContract.Clients.LAST_MODIFIED, now - ONE_WEEK_IN_MILLISECONDS + ONE_DAY_IN_MILLISECONDS);
+
+ // Insert a Remote Client with the same name as previous but with more than 3 weeks old
+ final ContentValues remote3 = new ContentValues();
+ remote3.put(BrowserContract.Clients.GUID, "guid21");
+ remote3.put(BrowserContract.Clients.NAME, "remote2");
+ remote3.put(BrowserContract.Clients.LAST_MODIFIED, now - THREE_WEEKS_IN_MILLISECONDS - ONE_DAY_IN_MILLISECONDS);
+
+ // Insert another remote client with the same name as previous but with 3 weeks - 1 day old.
+ final ContentValues remote4 = new ContentValues();
+ remote4.put(BrowserContract.Clients.GUID, "guid22");
+ remote4.put(BrowserContract.Clients.NAME, "remote2");
+ remote4.put(BrowserContract.Clients.LAST_MODIFIED, now - THREE_WEEKS_IN_MILLISECONDS + ONE_DAY_IN_MILLISECONDS);
+
+ // Insert a Remote Client that is exactly one week old.
+ final ContentValues remote5 = new ContentValues();
+ remote5.put(BrowserContract.Clients.GUID, "guid3");
+ remote5.put(BrowserContract.Clients.NAME, "remote3");
+ remote5.put(BrowserContract.Clients.LAST_MODIFIED, now - ONE_WEEK_IN_MILLISECONDS);
+
+ ContentValues[] values = new ContentValues[]{local, remote1, remote2, remote3, remote4, remote5};
+ int inserted = cpc.bulkInsert(uri, values);
+ Assert.assertEquals(values.length, inserted);
+
+ final Cursor remoteClients =
+ accessor.getRemoteClientsByRecencyCursor(context);
+
+ try {
+ CursorDumper.dumpCursor(remoteClients);
+ // Local client is not included.
+ // (remote1, guid1), (remote2, guid2), (remote3, guid3) are expected.
+ Assert.assertEquals(3, remoteClients.getCount());
+
+ // Check the inner data, according to recency.
+ List<RemoteClient> recentRemoteClientsList =
+ accessor.getClientsWithoutTabsByRecencyFromCursor(remoteClients);
+ Assert.assertEquals(3, recentRemoteClientsList.size());
+ Assert.assertEquals("remote1", recentRemoteClientsList.get(0).name);
+ Assert.assertEquals("guid1", recentRemoteClientsList.get(0).guid);
+ Assert.assertEquals("remote2", recentRemoteClientsList.get(1).name);
+ Assert.assertEquals("guid2", recentRemoteClientsList.get(1).guid);
+ Assert.assertEquals("remote3", recentRemoteClientsList.get(2).name);
+ Assert.assertEquals("guid3", recentRemoteClientsList.get(2).guid);
+ } finally {
+ remoteClients.close();
+ }
+ } finally {
+ cpc.release();
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/fxa/test/TestFxAccountClient20.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/fxa/test/TestFxAccountClient20.java
new file mode 100644
index 0000000000..d075cc0ec0
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/fxa/test/TestFxAccountClient20.java
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.fxa.test;
+
+import junit.framework.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.fxa.FxAccountClient20;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.net.BaseResource;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URISyntaxException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+@RunWith(TestRunner.class)
+public class TestFxAccountClient20 {
+ protected static class MockFxAccountClient20 extends FxAccountClient20 {
+ public MockFxAccountClient20(String serverURI, Executor executor) {
+ super(serverURI, executor);
+ }
+
+ // Public for testing.
+ @Override
+ public BaseResource getBaseResource(final String path, final String... queryParameters) throws UnsupportedEncodingException, URISyntaxException {
+ return super.getBaseResource(path, queryParameters);
+ }
+ }
+
+ @Test
+ public void testGetCreateAccountURI() throws Exception {
+ final String TEST_SERVER = "https://test.com:4430/inner/v1/";
+ final MockFxAccountClient20 client = new MockFxAccountClient20(TEST_SERVER, Executors.newSingleThreadExecutor());
+ Assert.assertEquals(TEST_SERVER + "account/create", client.getBaseResource("account/create").getURIString());
+ Assert.assertEquals(TEST_SERVER + "account/create?service=sync&keys=true", client.getBaseResource("account/create", "service", "sync", "keys", "true").getURIString());
+ Assert.assertEquals(TEST_SERVER + "account/create?service=two+words", client.getBaseResource("account/create", "service", "two words").getURIString());
+ Assert.assertEquals(TEST_SERVER + "account/create?service=symbols%2F%3A%3F%2B", client.getBaseResource("account/create", "service", "symbols/:?+").getURIString());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/fxa/test/TestFxAccountUtils.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/fxa/test/TestFxAccountUtils.java
new file mode 100644
index 0000000000..e6461776ea
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/fxa/test/TestFxAccountUtils.java
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.fxa.test;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.net.SRPConstants;
+
+import java.math.BigInteger;
+
+/**
+ * Test vectors from
+ * <a href="https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol#stretch-KDF">https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol#stretch-KDF</a>
+ * and
+ * <a href="https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol/5a9bc81e499306d769ca19b40b50fa60123df15d">https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol/5a9bc81e499306d769ca19b40b50fa60123df15d</a>.
+ */
+@RunWith(TestRunner.class)
+public class TestFxAccountUtils {
+ protected static void assertEncoding(String base16String, String utf8String) throws Exception {
+ Assert.assertEquals(base16String, FxAccountUtils.bytes(utf8String));
+ }
+
+ @Test
+ public void testUTF8Encoding() throws Exception {
+ assertEncoding("616e6472c3a9406578616d706c652e6f7267", "andré@example.org");
+ assertEncoding("70c3a4737377c3b67264", "pässwörd");
+ }
+
+ @Test
+ public void testHexModN() {
+ BigInteger N = BigInteger.valueOf(14);
+ Assert.assertEquals(4, N.bitLength());
+ Assert.assertEquals(1, (N.bitLength() + 7)/8);
+ Assert.assertEquals("00", FxAccountUtils.hexModN(BigInteger.valueOf(0), N));
+ Assert.assertEquals("05", FxAccountUtils.hexModN(BigInteger.valueOf(5), N));
+ Assert.assertEquals("0b", FxAccountUtils.hexModN(BigInteger.valueOf(11), N));
+ Assert.assertEquals("00", FxAccountUtils.hexModN(BigInteger.valueOf(14), N));
+ Assert.assertEquals("01", FxAccountUtils.hexModN(BigInteger.valueOf(15), N));
+ Assert.assertEquals("02", FxAccountUtils.hexModN(BigInteger.valueOf(16), N));
+ Assert.assertEquals("02", FxAccountUtils.hexModN(BigInteger.valueOf(30), N));
+
+ N = BigInteger.valueOf(260);
+ Assert.assertEquals("00ff", FxAccountUtils.hexModN(BigInteger.valueOf(255), N));
+ Assert.assertEquals("0100", FxAccountUtils.hexModN(BigInteger.valueOf(256), N));
+ Assert.assertEquals("0101", FxAccountUtils.hexModN(BigInteger.valueOf(257), N));
+ Assert.assertEquals("0001", FxAccountUtils.hexModN(BigInteger.valueOf(261), N));
+ }
+
+ @Test
+ public void testSRPVerifierFunctions() throws Exception {
+ byte[] emailUTF8Bytes = Utils.hex2Byte("616e6472c3a9406578616d706c652e6f7267");
+ byte[] srpPWBytes = Utils.hex2Byte("00f9b71800ab5337d51177d8fbc682a3653fa6dae5b87628eeec43a18af59a9d", 32);
+ byte[] srpSaltBytes = Utils.hex2Byte("00f1000000000000000000000000000000000000000000000000000000000179", 32);
+
+ String expectedX = "81925186909189958012481408070938147619474993903899664126296984459627523279550";
+ BigInteger x = FxAccountUtils.srpVerifierLowercaseX(emailUTF8Bytes, srpPWBytes, srpSaltBytes);
+ Assert.assertEquals(expectedX, x.toString(10));
+
+ String expectedV = "11464957230405843056840989945621595830717843959177257412217395741657995431613430369165714029818141919887853709633756255809680435884948698492811770122091692817955078535761033207000504846365974552196983218225819721112680718485091921646083608065626264424771606096544316730881455897489989950697705196721477608178869100211706638584538751009854562396937282582855620488967259498367841284829152987988548996842770025110751388952323221706639434861071834212055174768483159061566055471366772641252573641352721966728239512914666806496255304380341487975080159076396759492553066357163103546373216130193328802116982288883318596822";
+ BigInteger v = FxAccountUtils.srpVerifierLowercaseV(emailUTF8Bytes, srpPWBytes, srpSaltBytes, SRPConstants._2048.g, SRPConstants._2048.N);
+ Assert.assertEquals(expectedV, v.toString(10));
+
+ String expectedVHex = "00173ffa0263e63ccfd6791b8ee2a40f048ec94cd95aa8a3125726f9805e0c8283c658dc0b607fbb25db68e68e93f2658483049c68af7e8214c49fde2712a775b63e545160d64b00189a86708c69657da7a1678eda0cd79f86b8560ebdb1ffc221db360eab901d643a75bf1205070a5791230ae56466b8c3c1eb656e19b794f1ea0d2a077b3a755350208ea0118fec8c4b2ec344a05c66ae1449b32609ca7189451c259d65bd15b34d8729afdb5faff8af1f3437bbdc0c3d0b069a8ab2a959c90c5a43d42082c77490f3afcc10ef5648625c0605cdaace6c6fdc9e9a7e6635d619f50af7734522470502cab26a52a198f5b00a279858916507b0b4e9ef9524d6";
+ Assert.assertEquals(expectedVHex, FxAccountUtils.hexModN(v, SRPConstants._2048.N));
+ }
+
+ @Test
+ public void testGenerateSyncKeyBundle() throws Exception {
+ byte[] kB = Utils.hex2Byte("d02d8fe39f28b601159c543f2deeb8f72bdf2043e8279aa08496fbd9ebaea361");
+ KeyBundle bundle = FxAccountUtils.generateSyncKeyBundle(kB);
+ Assert.assertEquals("rsLwECkgPYeGbYl92e23FskfIbgld9TgeifEaB9ZwTI=", Base64.encodeBase64String(bundle.getEncryptionKey()));
+ Assert.assertEquals("fs75EseCD/VOLodlIGmwNabBjhTYBHFCe7CGIf0t8Tw=", Base64.encodeBase64String(bundle.getHMACKey()));
+ }
+
+ @Test
+ public void testGeneration() throws Exception {
+ byte[] quickStretchedPW = FxAccountUtils.generateQuickStretchedPW(
+ Utils.hex2Byte("616e6472c3a9406578616d706c652e6f7267"),
+ Utils.hex2Byte("70c3a4737377c3b67264"));
+ Assert.assertEquals("e4e8889bd8bd61ad6de6b95c059d56e7b50dacdaf62bd84644af7e2add84345d",
+ Utils.byte2Hex(quickStretchedPW));
+ Assert.assertEquals("247b675ffb4c46310bc87e26d712153abe5e1c90ef00a4784594f97ef54f2375",
+ Utils.byte2Hex(FxAccountUtils.generateAuthPW(quickStretchedPW)));
+ byte[] unwrapkB = FxAccountUtils.generateUnwrapBKey(quickStretchedPW);
+ Assert.assertEquals("de6a2648b78284fcb9ffa81ba95803309cfba7af583c01a8a1a63e567234dd28",
+ Utils.byte2Hex(unwrapkB));
+ byte[] wrapkB = Utils.hex2Byte("7effe354abecbcb234a8dfc2d7644b4ad339b525589738f2d27341bb8622ecd8");
+ Assert.assertEquals("a095c51c1c6e384e8d5777d97e3c487a4fc2128a00ab395a73d57fedf41631f0",
+ Utils.byte2Hex(FxAccountUtils.unwrapkB(unwrapkB, wrapkB)));
+ }
+
+ @Test
+ public void testClientState() throws Exception {
+ final String hexKB = "fd5c747806c07ce0b9d69dcfea144663e630b65ec4963596a22f24910d7dd15d";
+ final byte[] byteKB = Utils.hex2Byte(hexKB);
+ final String clientState = FxAccountUtils.computeClientState(byteKB);
+ final String expected = "6ae94683571c7a7c54dab4700aa3995f";
+ Assert.assertEquals(expected, clientState);
+ }
+
+ @Test
+ public void testGetAudienceForURL() throws Exception {
+ // Sub-domains and path components.
+ Assert.assertEquals("http://sub.test.com", FxAccountUtils.getAudienceForURL("http://sub.test.com"));
+ Assert.assertEquals("http://test.com", FxAccountUtils.getAudienceForURL("http://test.com/"));
+ Assert.assertEquals("http://test.com", FxAccountUtils.getAudienceForURL("http://test.com/path/component"));
+ Assert.assertEquals("http://test.com", FxAccountUtils.getAudienceForURL("http://test.com/path/component/"));
+
+ // No port and default port.
+ Assert.assertEquals("http://test.com", FxAccountUtils.getAudienceForURL("http://test.com"));
+ Assert.assertEquals("http://test.com:80", FxAccountUtils.getAudienceForURL("http://test.com:80"));
+
+ Assert.assertEquals("https://test.com", FxAccountUtils.getAudienceForURL("https://test.com"));
+ Assert.assertEquals("https://test.com:443", FxAccountUtils.getAudienceForURL("https://test.com:443"));
+
+ // Ports that are the default ports for a different scheme.
+ Assert.assertEquals("https://test.com:80", FxAccountUtils.getAudienceForURL("https://test.com:80"));
+ Assert.assertEquals("http://test.com:443", FxAccountUtils.getAudienceForURL("http://test.com:443"));
+
+ // Arbitrary ports.
+ Assert.assertEquals("http://test.com:8080", FxAccountUtils.getAudienceForURL("http://test.com:8080"));
+ Assert.assertEquals("https://test.com:4430", FxAccountUtils.getAudienceForURL("https://test.com:4430"));
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/test/EntityTestHelper.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/test/EntityTestHelper.java
new file mode 100644
index 0000000000..976a8eda19
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/test/EntityTestHelper.java
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.test;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class EntityTestHelper {
+ private static final int DEFAULT_SIZE = 1024;
+
+ public static byte[] bytesFromEntity(final HttpEntity entity) throws IOException {
+ final InputStream is = entity.getContent();
+
+ if (is instanceof ByteArrayInputStream) {
+ final int size = is.available();
+ final byte[] buffer = new byte[size];
+ is.read(buffer, 0, size);
+ return buffer;
+ }
+
+ final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ final byte[] buffer = new byte[DEFAULT_SIZE];
+ int len;
+ while ((len = is.read(buffer, 0, DEFAULT_SIZE)) != -1) {
+ bos.write(buffer, 0, len);
+ }
+ return bos.toByteArray();
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java
new file mode 100644
index 0000000000..d9aa936f04
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import org.mozilla.gecko.sync.NoCollectionKeysSetException;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.SynchronizerConfiguration;
+import org.mozilla.gecko.sync.repositories.RecordFactory;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.stage.ServerSyncStage;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+
+/**
+ * A stage that joins two Repositories with no wrapping.
+ */
+public abstract class BaseMockServerSyncStage extends ServerSyncStage {
+
+ public Repository local;
+ public Repository remote;
+ public String name;
+ public String collection;
+ public int version = 1;
+
+ @Override
+ public boolean isEnabled() {
+ return true;
+ }
+
+ @Override
+ protected String getCollection() {
+ return collection;
+ }
+
+ @Override
+ protected Repository getLocalRepository() {
+ return local;
+ }
+
+ @Override
+ protected Repository getRemoteRepository() throws URISyntaxException {
+ return remote;
+ }
+
+ @Override
+ protected String getEngineName() {
+ return name;
+ }
+
+ @Override
+ public Integer getStorageVersion() {
+ return version;
+ }
+
+ @Override
+ protected RecordFactory getRecordFactory() {
+ return null;
+ }
+
+ @Override
+ protected Repository wrappedServerRepo()
+ throws NoCollectionKeysSetException, URISyntaxException {
+ return getRemoteRepository();
+ }
+
+ public SynchronizerConfiguration leakConfig()
+ throws NonObjectJSONException, IOException {
+ return this.getConfig();
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/CommandHelpers.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/CommandHelpers.java
new file mode 100644
index 0000000000..48217f1b06
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/CommandHelpers.java
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import org.json.simple.JSONArray;
+import org.mozilla.gecko.sync.CommandProcessor.Command;
+
+public class CommandHelpers {
+
+ @SuppressWarnings("unchecked")
+ public static Command getCommand1() {
+ JSONArray args = new JSONArray();
+ args.add("argsA");
+ return new Command("displayURI", args);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Command getCommand2() {
+ JSONArray args = new JSONArray();
+ args.add("argsB");
+ return new Command("displayURI", args);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Command getCommand3() {
+ JSONArray args = new JSONArray();
+ args.add("argsC");
+ return new Command("displayURI", args);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Command getCommand4() {
+ JSONArray args = new JSONArray();
+ args.add("URI of Page");
+ args.add("Sender ID");
+ args.add("Title of Page");
+ return new Command("displayURI", args);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java
new file mode 100644
index 0000000000..373dd4eab7
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
+
+import java.net.URI;
+
+public class DefaultGlobalSessionCallback implements GlobalSessionCallback {
+
+ @Override
+ public void requestBackoff(long backoff) {
+ }
+
+ @Override
+ public void informUnauthorizedResponse(GlobalSession globalSession,
+ URI oldClusterURL) {
+ }
+ @Override
+ public void informUpgradeRequiredResponse(GlobalSession session) {
+ }
+
+ @Override
+ public void informMigrated(GlobalSession globalSession) {
+ }
+
+ @Override
+ public void handleAborted(GlobalSession globalSession, String reason) {
+ }
+
+ @Override
+ public void handleError(GlobalSession globalSession, Exception ex) {
+ }
+
+ @Override
+ public void handleSuccess(GlobalSession globalSession) {
+ }
+
+ @Override
+ public void handleStageCompleted(Stage currentState,
+ GlobalSession globalSession) {
+ }
+
+ @Override
+ public boolean shouldBackOffStorage() {
+ return false;
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockAbstractNonRepositorySyncStage.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockAbstractNonRepositorySyncStage.java
new file mode 100644
index 0000000000..d8380df972
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockAbstractNonRepositorySyncStage.java
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import org.mozilla.gecko.sync.stage.AbstractNonRepositorySyncStage;
+
+public class MockAbstractNonRepositorySyncStage extends AbstractNonRepositorySyncStage {
+ @Override
+ public void execute() {
+ session.advance();
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockClientsDataDelegate.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockClientsDataDelegate.java
new file mode 100644
index 0000000000..f4af51f648
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockClientsDataDelegate.java
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
+
+public class MockClientsDataDelegate implements ClientsDataDelegate {
+ private String accountGUID;
+ private String clientName;
+ private int clientsCount;
+ private long clientDataTimestamp = 0;
+
+ @Override
+ public synchronized String getAccountGUID() {
+ if (accountGUID == null) {
+ accountGUID = Utils.generateGuid();
+ }
+ return accountGUID;
+ }
+
+ @Override
+ public synchronized String getDefaultClientName() {
+ return "Default client";
+ }
+
+ @Override
+ public synchronized void setClientName(String clientName, long now) {
+ this.clientName = clientName;
+ this.clientDataTimestamp = now;
+ }
+
+ @Override
+ public synchronized String getClientName() {
+ if (clientName == null) {
+ setClientName(getDefaultClientName(), System.currentTimeMillis());
+ }
+ return clientName;
+ }
+
+ @Override
+ public synchronized void setClientsCount(int clientsCount) {
+ this.clientsCount = clientsCount;
+ }
+
+ @Override
+ public synchronized int getClientsCount() {
+ return clientsCount;
+ }
+
+ @Override
+ public synchronized boolean isLocalGUID(String guid) {
+ return getAccountGUID().equals(guid);
+ }
+
+ @Override
+ public synchronized long getLastModifiedTimestamp() {
+ return clientDataTimestamp;
+ }
+
+ @Override
+ public String getFormFactor() {
+ return "phone";
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockClientsDatabaseAccessor.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockClientsDatabaseAccessor.java
new file mode 100644
index 0000000000..b1aeb7cd16
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockClientsDatabaseAccessor.java
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import org.mozilla.gecko.sync.CommandProcessor.Command;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
+import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+public class MockClientsDatabaseAccessor extends ClientsDatabaseAccessor {
+ public boolean storedRecord = false;
+ public boolean dbWiped = false;
+ public boolean clientsTableWiped = false;
+ public boolean closed = false;
+ public boolean storedArrayList = false;
+ public boolean storedCommand;
+
+ @Override
+ public void store(ClientRecord record) {
+ storedRecord = true;
+ }
+
+ @Override
+ public void store(Collection<ClientRecord> records) {
+ storedArrayList = false;
+ }
+
+ @Override
+ public void store(String accountGUID, Command command) throws NullCursorException {
+ storedCommand = true;
+ }
+
+ @Override
+ public ClientRecord fetchClient(String profileID) throws NullCursorException {
+ return null;
+ }
+
+ @Override
+ public Map<String, ClientRecord> fetchAllClients() throws NullCursorException {
+ return null;
+ }
+
+ @Override
+ public List<Command> fetchCommandsForClient(String accountGUID) throws NullCursorException {
+ return null;
+ }
+
+ @Override
+ public int clientsCount() {
+ return 0;
+ }
+
+ @Override
+ public void wipeDB() {
+ dbWiped = true;
+ }
+
+ @Override
+ public void wipeClientsTable() {
+ clientsTableWiped = true;
+ }
+
+ @Override
+ public void close() {
+ closed = true;
+ }
+
+ public void resetVars() {
+ storedRecord = dbWiped = clientsTableWiped = closed = storedArrayList = false;
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java
new file mode 100644
index 0000000000..63afdd1ac0
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import org.mozilla.gecko.sync.EngineSettings;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.SyncConfiguration;
+import org.mozilla.gecko.sync.SyncConfigurationException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+import org.mozilla.gecko.sync.stage.CompletedStage;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+
+public class MockGlobalSession extends MockPrefsGlobalSession {
+
+ public MockGlobalSession(String username, String password, KeyBundle keyBundle, GlobalSessionCallback callback) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException {
+ this(new SyncConfiguration(username, new BasicAuthHeaderProvider(username, password), new MockSharedPreferences(), keyBundle), callback);
+ }
+
+ public MockGlobalSession(SyncConfiguration config, GlobalSessionCallback callback)
+ throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException {
+ super(config, callback, null, null);
+ }
+
+ @Override
+ public boolean isEngineRemotelyEnabled(String engine, EngineSettings engineSettings) {
+ return false;
+ }
+
+ @Override
+ protected void prepareStages() {
+ super.prepareStages();
+ HashMap<Stage, GlobalSyncStage> newStages = new HashMap<Stage, GlobalSyncStage>(this.stages);
+
+ for (Stage stage : this.stages.keySet()) {
+ newStages.put(stage, new MockServerSyncStage());
+ }
+
+ // This signals that the global session is complete.
+ newStages.put(Stage.completed, new CompletedStage());
+
+ this.stages = newStages;
+ }
+
+ public MockGlobalSession withStage(Stage stage, GlobalSyncStage syncStage) {
+ stages.put(stage, syncStage);
+
+ return this;
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java
new file mode 100644
index 0000000000..c864cdf80c
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.SyncConfiguration;
+import org.mozilla.gecko.sync.SyncConfigurationException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
+import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+
+import java.io.IOException;
+
+/**
+ * GlobalSession touches the Android prefs system. Stub that out.
+ */
+public class MockPrefsGlobalSession extends GlobalSession {
+
+ public MockSharedPreferences prefs;
+
+ public MockPrefsGlobalSession(
+ SyncConfiguration config, GlobalSessionCallback callback, Context context,
+ ClientsDataDelegate clientsDelegate)
+ throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException {
+ super(config, callback, context, clientsDelegate);
+ }
+
+ public static MockPrefsGlobalSession getSession(
+ String username, String password,
+ KeyBundle syncKeyBundle, GlobalSessionCallback callback, Context context,
+ ClientsDataDelegate clientsDelegate)
+ throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException {
+ return getSession(username, new BasicAuthHeaderProvider(username, password), null,
+ syncKeyBundle, callback, context, clientsDelegate);
+ }
+
+ public static MockPrefsGlobalSession getSession(
+ String username, AuthHeaderProvider authHeaderProvider, String prefsPath,
+ KeyBundle syncKeyBundle, GlobalSessionCallback callback, Context context,
+ ClientsDataDelegate clientsDelegate)
+ throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException {
+
+ final SharedPreferences prefs = new MockSharedPreferences();
+ final SyncConfiguration config = new SyncConfiguration(username, authHeaderProvider, prefs);
+ config.syncKeyBundle = syncKeyBundle;
+ return new MockPrefsGlobalSession(config, callback, context, clientsDelegate);
+ }
+
+ @Override
+ public Context getContext() {
+ return null;
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockRecord.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockRecord.java
new file mode 100644
index 0000000000..9876b78672
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockRecord.java
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import java.util.Random;
+
+public class MockRecord extends Record {
+ private final int payloadByteCount;
+ public MockRecord(String guid, String collection, long lastModified, boolean deleted) {
+ super(guid, collection, lastModified, deleted);
+ // Payload used to be "foo", so let's not stray too far.
+ // Perhaps some tests "depend" on that payload size.
+ payloadByteCount = 3;
+ }
+
+ public MockRecord(String guid, String collection, long lastModified, boolean deleted, int payloadByteCount) {
+ super(guid, collection, lastModified, deleted);
+ this.payloadByteCount = payloadByteCount;
+ }
+
+ @Override
+ protected void populatePayload(ExtendedJSONObject payload) {
+ }
+
+ @Override
+ protected void initFromPayload(ExtendedJSONObject payload) {
+ }
+
+ @Override
+ public Record copyWithIDs(String guid, long androidID) {
+ MockRecord r = new MockRecord(guid, this.collection, this.lastModified, this.deleted);
+ r.androidID = androidID;
+ return r;
+ }
+
+ @Override
+ public String toJSONString() {
+ // Build up a randomish payload string based on the length we were asked for.
+ final Random random = new Random();
+ final char[] payloadChars = new char[payloadByteCount];
+ for (int i = 0; i < payloadByteCount; i++) {
+ payloadChars[i] = (char) (random.nextInt(26) + 'a');
+ }
+ final String payloadString = new String(payloadChars);
+ return "{\"id\":\"" + guid + "\", \"payload\": \"" + payloadString+ "\"}";
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java
new file mode 100644
index 0000000000..28a4e58b9b
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java
@@ -0,0 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+public class MockServerSyncStage extends BaseMockServerSyncStage {
+ @Override
+ public void execute() {
+ session.advance();
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockSharedPreferences.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockSharedPreferences.java
new file mode 100644
index 0000000000..bc49fa7fba
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockSharedPreferences.java
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import android.content.SharedPreferences;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A programmable mock content provider.
+ */
+public class MockSharedPreferences implements SharedPreferences, SharedPreferences.Editor {
+ private HashMap<String, Object> mValues;
+ private HashMap<String, Object> mTempValues;
+
+ public MockSharedPreferences() {
+ mValues = new HashMap<String, Object>();
+ mTempValues = new HashMap<String, Object>();
+ }
+
+ public Editor edit() {
+ return this;
+ }
+
+ public boolean contains(String key) {
+ return mValues.containsKey(key);
+ }
+
+ public Map<String, ?> getAll() {
+ return new HashMap<String, Object>(mValues);
+ }
+
+ public boolean getBoolean(String key, boolean defValue) {
+ if (mValues.containsKey(key)) {
+ return ((Boolean)mValues.get(key)).booleanValue();
+ }
+ return defValue;
+ }
+
+ public float getFloat(String key, float defValue) {
+ if (mValues.containsKey(key)) {
+ return ((Float)mValues.get(key)).floatValue();
+ }
+ return defValue;
+ }
+
+ public int getInt(String key, int defValue) {
+ if (mValues.containsKey(key)) {
+ return ((Integer)mValues.get(key)).intValue();
+ }
+ return defValue;
+ }
+
+ public long getLong(String key, long defValue) {
+ if (mValues.containsKey(key)) {
+ return ((Long)mValues.get(key)).longValue();
+ }
+ return defValue;
+ }
+
+ public String getString(String key, String defValue) {
+ if (mValues.containsKey(key))
+ return (String)mValues.get(key);
+ return defValue;
+ }
+
+ @SuppressWarnings("unchecked")
+ public Set<String> getStringSet(String key, Set<String> defValues) {
+ if (mValues.containsKey(key)) {
+ return (Set<String>) mValues.get(key);
+ }
+ return defValues;
+ }
+
+ public void registerOnSharedPreferenceChangeListener(
+ OnSharedPreferenceChangeListener listener) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void unregisterOnSharedPreferenceChangeListener(
+ OnSharedPreferenceChangeListener listener) {
+ throw new UnsupportedOperationException();
+ }
+
+ public Editor putBoolean(String key, boolean value) {
+ mTempValues.put(key, Boolean.valueOf(value));
+ return this;
+ }
+
+ public Editor putFloat(String key, float value) {
+ mTempValues.put(key, value);
+ return this;
+ }
+
+ public Editor putInt(String key, int value) {
+ mTempValues.put(key, value);
+ return this;
+ }
+
+ public Editor putLong(String key, long value) {
+ mTempValues.put(key, value);
+ return this;
+ }
+
+ public Editor putString(String key, String value) {
+ mTempValues.put(key, value);
+ return this;
+ }
+
+ public Editor putStringSet(String key, Set<String> values) {
+ mTempValues.put(key, values);
+ return this;
+ }
+
+ public Editor remove(String key) {
+ mTempValues.remove(key);
+ return this;
+ }
+
+ public Editor clear() {
+ mTempValues.clear();
+ return this;
+ }
+
+ @SuppressWarnings("unchecked")
+ public boolean commit() {
+ mValues = (HashMap<String, Object>)mTempValues.clone();
+ return true;
+ }
+
+ public void apply() {
+ commit();
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/TestRunner.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/TestRunner.java
new file mode 100644
index 0000000000..ccb5276ed0
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/TestRunner.java
@@ -0,0 +1,125 @@
+/**
+ * The MIT License
+ *
+ * Copyright (c) 2010 Xtreme Labs and Pivotal Labs
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import org.junit.runners.model.InitializationError;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.manifest.AndroidManifest;
+import org.robolectric.res.FileFsFile;
+import org.robolectric.res.FsFile;
+import org.robolectric.util.Logger;
+import org.robolectric.util.ReflectionHelpers;
+
+/**
+ * Test runner customized for running unit tests either through the Gradle CLI or
+ * Android Studio. The runner uses the build type and build flavor to compute the
+ * resource, asset, and AndroidManifest paths.
+ *
+ * This test runner requires that you set the 'constants' field on the @Config
+ * annotation (or the org.robolectric.Config.properties file) for your tests.
+ *
+ * This is a modified version of
+ * https://github.com/robolectric/robolectric/blob/8676da2daa4c140679fb5903696b8191415cec8f/robolectric/src/main/java/org/robolectric/RobolectricGradleTestRunner.java
+ * that uses a Gradle `buildConfigField` to find build outputs.
+ * See https://github.com/robolectric/robolectric/issues/1648#issuecomment-113731011.
+ */
+public class TestRunner extends RobolectricTestRunner {
+ private FsFile buildFolder;
+
+ public TestRunner(Class<?> klass) throws InitializationError {
+ super(klass);
+ }
+
+ @Override
+ protected AndroidManifest getAppManifest(Config config) {
+ if (config.constants() == Void.class) {
+ Logger.error("Field 'constants' not specified in @Config annotation");
+ Logger.error("This is required when using RobolectricGradleTestRunner!");
+ throw new RuntimeException("No 'constants' field in @Config annotation!");
+ }
+
+ buildFolder = FileFsFile.from(getBuildDir(config)).join("intermediates");
+
+ final String type = getType(config);
+ final String flavor = getFlavor(config);
+ final String packageName = getPackageName(config);
+
+ final FsFile assets = buildFolder.join("assets", flavor, type);;
+ final FsFile manifest = buildFolder.join("manifests", "full", flavor, type, "AndroidManifest.xml");
+
+ final FsFile res;
+ if (buildFolder.join("res", "merged").exists()) {
+ res = buildFolder.join("res", "merged", flavor, type);
+ } else if(buildFolder.join("res").exists()) {
+ res = buildFolder.join("res", flavor, type);
+ } else {
+ throw new IllegalStateException("No resource folder found");
+ }
+
+ Logger.debug("Robolectric assets directory: " + assets.getPath());
+ Logger.debug(" Robolectric res directory: " + res.getPath());
+ Logger.debug(" Robolectric manifest path: " + manifest.getPath());
+ Logger.debug(" Robolectric package name: " + packageName);
+ return new AndroidManifest(manifest, res, assets, packageName);
+ }
+
+ private static String getType(Config config) {
+ try {
+ return ReflectionHelpers.getStaticField(config.constants(), "BUILD_TYPE");
+ } catch (Throwable e) {
+ return null;
+ }
+ }
+
+ private static String getFlavor(Config config) {
+ try {
+ return ReflectionHelpers.getStaticField(config.constants(), "FLAVOR");
+ } catch (Throwable e) {
+ return null;
+ }
+ }
+
+ private static String getPackageName(Config config) {
+ try {
+ final String packageName = config.packageName();
+ if (packageName != null && !packageName.isEmpty()) {
+ return packageName;
+ } else {
+ return ReflectionHelpers.getStaticField(config.constants(), "APPLICATION_ID");
+ }
+ } catch (Throwable e) {
+ return null;
+ }
+ }
+
+ private String getBuildDir(Config config) {
+ try {
+ return ReflectionHelpers.getStaticField(config.constants(), "BUILD_DIR");
+ } catch (Throwable e) {
+ return null;
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WBORepository.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WBORepository.java
new file mode 100644
index 0000000000..672b0a602b
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WBORepository.java
@@ -0,0 +1,230 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import android.content.Context;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.repositories.InactiveSessionException;
+import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
+import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
+import org.mozilla.gecko.sync.repositories.RecordFilter;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.StoreTrackingRepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class WBORepository extends Repository {
+
+ public class WBORepositoryStats {
+ public long created = -1;
+ public long begun = -1;
+ public long fetchBegan = -1;
+ public long fetchCompleted = -1;
+ public long storeBegan = -1;
+ public long storeCompleted = -1;
+ public long finished = -1;
+ }
+
+ public static final String LOG_TAG = "WBORepository";
+
+ // Access to stats is not guarded.
+ public WBORepositoryStats stats;
+
+ // Whether or not to increment the timestamp of stored records.
+ public final boolean bumpTimestamps;
+
+ public class WBORepositorySession extends StoreTrackingRepositorySession {
+
+ protected WBORepository wboRepository;
+ protected ExecutorService delegateExecutor = Executors.newSingleThreadExecutor();
+ public ConcurrentHashMap<String, Record> wbos;
+
+ public WBORepositorySession(WBORepository repository) {
+ super(repository);
+
+ wboRepository = repository;
+ wbos = new ConcurrentHashMap<String, Record>();
+ stats = new WBORepositoryStats();
+ stats.created = now();
+ }
+
+ @Override
+ protected synchronized void trackGUID(String guid) {
+ if (wboRepository.shouldTrack()) {
+ super.trackGUID(guid);
+ }
+ }
+
+ @Override
+ public void guidsSince(long timestamp,
+ RepositorySessionGuidsSinceDelegate delegate) {
+ throw new RuntimeException("guidsSince not implemented.");
+ }
+
+ @Override
+ public void fetchSince(long timestamp,
+ RepositorySessionFetchRecordsDelegate delegate) {
+ long fetchBegan = now();
+ stats.fetchBegan = fetchBegan;
+ RecordFilter filter = storeTracker.getFilter();
+
+ for (Entry<String, Record> entry : wbos.entrySet()) {
+ Record record = entry.getValue();
+ if (record.lastModified >= timestamp) {
+ if (filter != null &&
+ filter.excludeRecord(record)) {
+ Logger.debug(LOG_TAG, "Excluding record " + record.guid);
+ continue;
+ }
+ delegate.deferredFetchDelegate(delegateExecutor).onFetchedRecord(record);
+ }
+ }
+ long fetchCompleted = now();
+ stats.fetchCompleted = fetchCompleted;
+ delegate.deferredFetchDelegate(delegateExecutor).onFetchCompleted(fetchCompleted);
+ }
+
+ @Override
+ public void fetch(final String[] guids,
+ final RepositorySessionFetchRecordsDelegate delegate) {
+ long fetchBegan = now();
+ stats.fetchBegan = fetchBegan;
+ for (String guid : guids) {
+ if (wbos.containsKey(guid)) {
+ delegate.deferredFetchDelegate(delegateExecutor).onFetchedRecord(wbos.get(guid));
+ }
+ }
+ long fetchCompleted = now();
+ stats.fetchCompleted = fetchCompleted;
+ delegate.deferredFetchDelegate(delegateExecutor).onFetchCompleted(fetchCompleted);
+ }
+
+ @Override
+ public void fetchAll(final RepositorySessionFetchRecordsDelegate delegate) {
+ long fetchBegan = now();
+ stats.fetchBegan = fetchBegan;
+ for (Entry<String, Record> entry : wbos.entrySet()) {
+ Record record = entry.getValue();
+ delegate.deferredFetchDelegate(delegateExecutor).onFetchedRecord(record);
+ }
+ long fetchCompleted = now();
+ stats.fetchCompleted = fetchCompleted;
+ delegate.deferredFetchDelegate(delegateExecutor).onFetchCompleted(fetchCompleted);
+ }
+
+ @Override
+ public void store(final Record record) throws NoStoreDelegateException {
+ if (delegate == null) {
+ throw new NoStoreDelegateException();
+ }
+ final long now = now();
+ if (stats.storeBegan < 0) {
+ stats.storeBegan = now;
+ }
+ Record existing = wbos.get(record.guid);
+ Logger.debug(LOG_TAG, "Existing record is " + (existing == null ? "<null>" : (existing.guid + ", " + existing)));
+ if (existing != null &&
+ existing.lastModified > record.lastModified) {
+ Logger.debug(LOG_TAG, "Local record is newer. Not storing.");
+ delegate.deferredStoreDelegate(delegateExecutor).onRecordStoreSucceeded(record.guid);
+ return;
+ }
+ if (existing != null) {
+ Logger.debug(LOG_TAG, "Replacing local record.");
+ }
+
+ // Store a copy of the record with an updated modified time.
+ Record toStore = record.copyWithIDs(record.guid, record.androidID);
+ if (bumpTimestamps) {
+ toStore.lastModified = now;
+ }
+ wbos.put(record.guid, toStore);
+
+ trackRecord(toStore);
+ delegate.deferredStoreDelegate(delegateExecutor).onRecordStoreSucceeded(record.guid);
+ }
+
+ @Override
+ public void wipe(final RepositorySessionWipeDelegate delegate) {
+ if (!isActive()) {
+ delegate.onWipeFailed(new InactiveSessionException(null));
+ return;
+ }
+
+ Logger.info(LOG_TAG, "Wiping WBORepositorySession.");
+ this.wbos = new ConcurrentHashMap<String, Record>();
+
+ // Wipe immediately for the convenience of test code.
+ wboRepository.wbos = new ConcurrentHashMap<String, Record>();
+ delegate.deferredWipeDelegate(delegateExecutor).onWipeSucceeded();
+ }
+
+ @Override
+ public void finish(RepositorySessionFinishDelegate delegate) throws InactiveSessionException {
+ Logger.info(LOG_TAG, "Finishing WBORepositorySession: handing back " + this.wbos.size() + " WBOs.");
+ wboRepository.wbos = this.wbos;
+ stats.finished = now();
+ super.finish(delegate);
+ }
+
+ @Override
+ public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException {
+ this.wbos = wboRepository.cloneWBOs();
+ stats.begun = now();
+ super.begin(delegate);
+ }
+
+ @Override
+ public void storeDone(long end) {
+ // TODO: this is not guaranteed to be called after all of the record
+ // store callbacks have completed!
+ if (stats.storeBegan < 0) {
+ stats.storeBegan = end;
+ }
+ stats.storeCompleted = end;
+ delegate.deferredStoreDelegate(delegateExecutor).onStoreCompleted(end);
+ }
+ }
+
+ public ConcurrentHashMap<String, Record> wbos;
+
+ public WBORepository(boolean bumpTimestamps) {
+ super();
+ this.bumpTimestamps = bumpTimestamps;
+ this.wbos = new ConcurrentHashMap<String, Record>();
+ }
+
+ public WBORepository() {
+ this(false);
+ }
+
+ public synchronized boolean shouldTrack() {
+ return false;
+ }
+
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ delegate.deferredCreationDelegate().onSessionCreated(new WBORepositorySession(this));
+ }
+
+ public ConcurrentHashMap<String, Record> cloneWBOs() {
+ ConcurrentHashMap<String, Record> out = new ConcurrentHashMap<String, Record>();
+ for (Entry<String, Record> entry : wbos.entrySet()) {
+ out.put(entry.getKey(), entry.getValue()); // Assume that records are
+ // immutable.
+ }
+ return out;
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java
new file mode 100644
index 0000000000..dad748df1e
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java
@@ -0,0 +1,172 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import org.mozilla.gecko.background.common.log.Logger;
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Implements waiting for asynchronous test events.
+ *
+ * Call WaitHelper.getTestWaiter() to get the unique instance.
+ *
+ * Call performWait(runnable) to execute runnable synchronously.
+ * runnable *must* call performNotify() on all exit paths to signal to
+ * the TestWaiter that the runnable has completed.
+ *
+ * @author rnewman
+ * @author nalexander
+ */
+public class WaitHelper {
+
+ public static final String LOG_TAG = "WaitHelper";
+
+ public static class Result {
+ public Throwable error;
+ public Result() {
+ error = null;
+ }
+
+ public Result(Throwable error) {
+ this.error = error;
+ }
+ }
+
+ public static abstract class WaitHelperError extends Error {
+ private static final long serialVersionUID = 7074690961681883619L;
+ }
+
+ /**
+ * Immutable.
+ *
+ * @author rnewman
+ */
+ public static class TimeoutError extends WaitHelperError {
+ private static final long serialVersionUID = 8591672555848651736L;
+ public final int waitTimeInMillis;
+
+ public TimeoutError(int waitTimeInMillis) {
+ this.waitTimeInMillis = waitTimeInMillis;
+ }
+ }
+
+ public static class MultipleNotificationsError extends WaitHelperError {
+ private static final long serialVersionUID = -9072736521571635495L;
+ }
+
+ public static class InterruptedError extends WaitHelperError {
+ private static final long serialVersionUID = 8383948170038639308L;
+ }
+
+ public static class InnerError extends WaitHelperError {
+ private static final long serialVersionUID = 3008502618576773778L;
+ public Throwable innerError;
+
+ public InnerError(Throwable e) {
+ innerError = e;
+ if (e != null) {
+ // Eclipse prints the stack trace of the cause.
+ this.initCause(e);
+ }
+ }
+ }
+
+ public BlockingQueue<Result> queue = new ArrayBlockingQueue<Result>(1);
+
+ /**
+ * How long performWait should wait for, in milliseconds, with the
+ * convention that a negative value means "wait forever".
+ */
+ public static int defaultWaitTimeoutInMillis = -1;
+
+ public void performWait(Runnable action) throws WaitHelperError {
+ this.performWait(defaultWaitTimeoutInMillis, action);
+ }
+
+ public void performWait(int waitTimeoutInMillis, Runnable action) throws WaitHelperError {
+ Logger.debug(LOG_TAG, "performWait called.");
+
+ Result result = null;
+
+ try {
+ if (action != null) {
+ try {
+ action.run();
+ Logger.debug(LOG_TAG, "Action done.");
+ } catch (Exception ex) {
+ Logger.debug(LOG_TAG, "Performing action threw: " + ex.getMessage());
+ throw new InnerError(ex);
+ }
+ }
+
+ if (waitTimeoutInMillis < 0) {
+ result = queue.take();
+ } else {
+ result = queue.poll(waitTimeoutInMillis, TimeUnit.MILLISECONDS);
+ }
+ Logger.debug(LOG_TAG, "Got result from queue: " + result);
+ } catch (InterruptedException e) {
+ // We were interrupted.
+ Logger.debug(LOG_TAG, "performNotify interrupted with InterruptedException " + e);
+ final InterruptedError interruptedError = new InterruptedError();
+ interruptedError.initCause(e);
+ throw interruptedError;
+ }
+
+ if (result == null) {
+ // We timed out.
+ throw new TimeoutError(waitTimeoutInMillis);
+ } else if (result.error != null) {
+ Logger.debug(LOG_TAG, "Notified with error: " + result.error.getMessage());
+
+ // Rethrow any assertion with which we were notified.
+ InnerError innerError = new InnerError(result.error);
+ throw innerError;
+ }
+ // Success!
+ }
+
+ public void performNotify(final Throwable e) {
+ if (e != null) {
+ Logger.debug(LOG_TAG, "performNotify called with Throwable: " + e.getMessage());
+ } else {
+ Logger.debug(LOG_TAG, "performNotify called.");
+ }
+
+ if (!queue.offer(new Result(e))) {
+ // This could happen if performNotify is called multiple times (which is an error).
+ throw new MultipleNotificationsError();
+ }
+ }
+
+ public void performNotify() {
+ this.performNotify(null);
+ }
+
+ public static Runnable onThreadRunnable(final Runnable r) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ new Thread(r).start();
+ }
+ };
+ }
+
+ private static WaitHelper singleWaiter = new WaitHelper();
+ public static WaitHelper getTestWaiter() {
+ return singleWaiter;
+ }
+
+ public static void resetTestWaiter() {
+ singleWaiter = new WaitHelper();
+ }
+
+ public boolean isIdle() {
+ return queue.isEmpty();
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestASNUtils.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestASNUtils.java
new file mode 100644
index 0000000000..f0b1f98b5b
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestASNUtils.java
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.browserid.test;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.browserid.ASNUtils;
+import org.mozilla.gecko.sync.Utils;
+
+import java.math.BigInteger;
+
+@RunWith(TestRunner.class)
+public class TestASNUtils {
+ public void doTestEncodeDecodeArrays(int length1, int length2) {
+ if (4 + length1 + length2 > 127) {
+ throw new IllegalArgumentException("Total length must be < 128 - 4.");
+ }
+ byte[] first = Utils.generateRandomBytes(length1);
+ byte[] second = Utils.generateRandomBytes(length2);
+ byte[] encoded = ASNUtils.encodeTwoArraysToASN1(first, second);
+ byte[][] arrays = ASNUtils.decodeTwoArraysFromASN1(encoded);
+ Assert.assertArrayEquals(first, arrays[0]);
+ Assert.assertArrayEquals(second, arrays[1]);
+ }
+
+ @Test
+ public void testEncodeDecodeArrays() {
+ doTestEncodeDecodeArrays(0, 0);
+ doTestEncodeDecodeArrays(0, 10);
+ doTestEncodeDecodeArrays(10, 0);
+ doTestEncodeDecodeArrays(10, 10);
+ }
+
+ @Test
+ public void testEncodeDecodeRandomSizeArrays() {
+ for (int i = 0; i < 10; i++) {
+ int length1 = Utils.generateBigIntegerLessThan(BigInteger.valueOf(50)).intValue() + 10;
+ int length2 = Utils.generateBigIntegerLessThan(BigInteger.valueOf(50)).intValue() + 10;
+ doTestEncodeDecodeArrays(length1, length2);
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestDSACryptoImplementation.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestDSACryptoImplementation.java
new file mode 100644
index 0000000000..62427e5e1f
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestDSACryptoImplementation.java
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.browserid.test;
+
+import junit.framework.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.browserid.DSACryptoImplementation;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+import java.math.BigInteger;
+
+@RunWith(TestRunner.class)
+public class TestDSACryptoImplementation {
+ @Test
+ public void testToJSONObject() throws Exception {
+ BigInteger p = new BigInteger("fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae1617ae01f35b91a47e6df63413c5e12ed0899bcd132acd50d99151bdc43ee737592e17", 16);
+ BigInteger q = new BigInteger("962eddcc369cba8ebb260ee6b6a126d9346e38c5", 16);
+ BigInteger g = new BigInteger("678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d14271b9e35030b71fd73da179069b32e2935630e1c2062354d0da20a6c416e50be794ca4", 16);
+ BigInteger x = new BigInteger("9516d860392003db5a4f168444903265467614db", 16);
+ BigInteger y = new BigInteger("455152a0e499f5c9d11f9f1868c8b868b1443ca853843226a5a9552dd909b4bdba879acc504acb690df0348d60e63ea37e8c7f075302e0df5bcdc76a383888a0", 16);
+
+ BrowserIDKeyPair keyPair = new BrowserIDKeyPair(
+ DSACryptoImplementation.createPrivateKey(x, p, q, g),
+ DSACryptoImplementation.createPublicKey(y, p, q, g));
+
+ ExtendedJSONObject o = new ExtendedJSONObject("{\"publicKey\":{\"g\":\"678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d14271b9e35030b71fd73da179069b32e2935630e1c2062354d0da20a6c416e50be794ca4\",\"q\":\"962eddcc369cba8ebb260ee6b6a126d9346e38c5\",\"p\":\"fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae1617ae01f35b91a47e6df63413c5e12ed0899bcd132acd50d99151bdc43ee737592e17\",\"y\":\"455152a0e499f5c9d11f9f1868c8b868b1443ca853843226a5a9552dd909b4bdba879acc504acb690df0348d60e63ea37e8c7f075302e0df5bcdc76a383888a0\",\"algorithm\":\"DS\"},\"privateKey\":{\"g\":\"678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d14271b9e35030b71fd73da179069b32e2935630e1c2062354d0da20a6c416e50be794ca4\",\"q\":\"962eddcc369cba8ebb260ee6b6a126d9346e38c5\",\"p\":\"fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae1617ae01f35b91a47e6df63413c5e12ed0899bcd132acd50d99151bdc43ee737592e17\",\"x\":\"9516d860392003db5a4f168444903265467614db\",\"algorithm\":\"DS\"}}");
+ Assert.assertEquals(o.getObject("privateKey"), keyPair.toJSONObject().getObject("privateKey"));
+ Assert.assertEquals(o.getObject("publicKey"), keyPair.toJSONObject().getObject("publicKey"));
+ }
+
+ @Test
+ public void testFromJSONObject() throws Exception {
+ BigInteger p = new BigInteger("fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae1617ae01f35b91a47e6df63413c5e12ed0899bcd132acd50d99151bdc43ee737592e17", 16);
+ BigInteger q = new BigInteger("962eddcc369cba8ebb260ee6b6a126d9346e38c5", 16);
+ BigInteger g = new BigInteger("678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d14271b9e35030b71fd73da179069b32e2935630e1c2062354d0da20a6c416e50be794ca4", 16);
+ BigInteger x = new BigInteger("9516d860392003db5a4f168444903265467614db", 16);
+ BigInteger y = new BigInteger("455152a0e499f5c9d11f9f1868c8b868b1443ca853843226a5a9552dd909b4bdba879acc504acb690df0348d60e63ea37e8c7f075302e0df5bcdc76a383888a0", 16);
+
+ BrowserIDKeyPair keyPair = new BrowserIDKeyPair(
+ DSACryptoImplementation.createPrivateKey(x, p, q, g),
+ DSACryptoImplementation.createPublicKey(y, p, q, g));
+
+ ExtendedJSONObject o = new ExtendedJSONObject("{\"publicKey\":{\"g\":\"678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d14271b9e35030b71fd73da179069b32e2935630e1c2062354d0da20a6c416e50be794ca4\",\"q\":\"962eddcc369cba8ebb260ee6b6a126d9346e38c5\",\"p\":\"fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae1617ae01f35b91a47e6df63413c5e12ed0899bcd132acd50d99151bdc43ee737592e17\",\"y\":\"455152a0e499f5c9d11f9f1868c8b868b1443ca853843226a5a9552dd909b4bdba879acc504acb690df0348d60e63ea37e8c7f075302e0df5bcdc76a383888a0\",\"algorithm\":\"DS\"},\"privateKey\":{\"g\":\"678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d14271b9e35030b71fd73da179069b32e2935630e1c2062354d0da20a6c416e50be794ca4\",\"q\":\"962eddcc369cba8ebb260ee6b6a126d9346e38c5\",\"p\":\"fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae1617ae01f35b91a47e6df63413c5e12ed0899bcd132acd50d99151bdc43ee737592e17\",\"x\":\"9516d860392003db5a4f168444903265467614db\",\"algorithm\":\"DS\"}}");
+
+ Assert.assertEquals(keyPair.getPublic().toJSONObject(), DSACryptoImplementation.createPublicKey(o.getObject("publicKey")).toJSONObject());
+ Assert.assertEquals(keyPair.getPrivate().toJSONObject(), DSACryptoImplementation.createPrivateKey(o.getObject("privateKey")).toJSONObject());
+ }
+
+ @Test
+ public void testRoundTrip() throws Exception {
+ BrowserIDKeyPair keyPair = DSACryptoImplementation.generateKeyPair(512);
+ ExtendedJSONObject o = keyPair.toJSONObject();
+ BrowserIDKeyPair keyPair2 = DSACryptoImplementation.fromJSONObject(o);
+ Assert.assertEquals(o, keyPair2.toJSONObject());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestJSONWebTokenUtils.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestJSONWebTokenUtils.java
new file mode 100644
index 0000000000..7e1f9287ee
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestJSONWebTokenUtils.java
@@ -0,0 +1,151 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.browserid.test;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.browserid.DSACryptoImplementation;
+import org.mozilla.gecko.browserid.JSONWebTokenUtils;
+import org.mozilla.gecko.browserid.RSACryptoImplementation;
+import org.mozilla.gecko.browserid.SigningPrivateKey;
+import org.mozilla.gecko.browserid.VerifyingPublicKey;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+
+@RunWith(TestRunner.class)
+public class TestJSONWebTokenUtils {
+ public void doTestEncodeDecode(BrowserIDKeyPair keyPair) throws Exception {
+ SigningPrivateKey privateKey = keyPair.getPrivate();
+ VerifyingPublicKey publicKey = keyPair.getPublic();
+
+ ExtendedJSONObject o = new ExtendedJSONObject();
+ o.put("key", "value");
+
+ String token = JSONWebTokenUtils.encode(o.toJSONString(), privateKey);
+ Assert.assertNotNull(token);
+
+ String payload = JSONWebTokenUtils.decode(token, publicKey);
+ Assert.assertEquals(o.toJSONString(), payload);
+
+ try {
+ JSONWebTokenUtils.decode(token + "x", publicKey);
+ Assert.fail("Expected exception.");
+ } catch (GeneralSecurityException e) {
+ // Do nothing.
+ }
+ }
+
+ @Test
+ public void testEncodeDecodeSuccessRSA() throws Exception {
+ doTestEncodeDecode(RSACryptoImplementation.generateKeyPair(1024));
+ doTestEncodeDecode(RSACryptoImplementation.generateKeyPair(2048));
+ }
+
+ @Test
+ public void testEncodeDecodeSuccessDSA() throws Exception {
+ doTestEncodeDecode(DSACryptoImplementation.generateKeyPair(512));
+ doTestEncodeDecode(DSACryptoImplementation.generateKeyPair(1024));
+ }
+
+ public static String TEST_ASSERTION_ISSUER = "127.0.0.1";
+ public static String TEST_AUDIENCE = "http://localhost:8080";
+
+ @Test
+ public void testRSAGeneration() throws Exception {
+ // This test uses (now out-dated) MockMyID RSA data but doesn't rely on this
+ // data actually being MockMyID's data.
+ final BigInteger MOCKMYID_MODULUS = new BigInteger("15498874758090276039465094105837231567265546373975960480941122651107772824121527483107402353899846252489837024870191707394743196399582959425513904762996756672089693541009892030848825079649783086005554442490232900875792851786203948088457942416978976455297428077460890650409549242124655536986141363719589882160081480785048965686285142002320767066674879737238012064156675899512503143225481933864507793118457805792064445502834162315532113963746801770187685650408560424682654937744713813773896962263709692724630650952159596951348264005004375017610441835956073275708740239518011400991972811669493356682993446554779893834303");
+ final BigInteger MOCKMYID_PUBLIC_EXPONENT = new BigInteger("65537");
+ final BigInteger MOCKMYID_PRIVATE_EXPONENT = new BigInteger("6539906961872354450087244036236367269804254381890095841127085551577495913426869112377010004955160417265879626558436936025363204803913318582680951558904318308893730033158178650549970379367915856087364428530828396795995781364659413467784853435450762392157026962694408807947047846891301466649598749901605789115278274397848888140105306063608217776127549926721544215720872305194645129403056801987422794114703255989202755511523434098625000826968430077091984351410839837395828971692109391386427709263149504336916566097901771762648090880994773325283207496645630792248007805177873532441314470502254528486411726581424522838833");
+
+ BigInteger n = new BigInteger("20332459213245328760269530796942625317006933400814022542511832260333163206808672913301254872114045771215470352093046136365629411384688395020388553744886954869033696089099714200452682590914843971683468562019706059388121176435204818734091361033445697933682779095713376909412972373727850278295874361806633955236862180792787906413536305117030045164276955491725646610368132167655556353974515423042221261732084368978523747789654468953860772774078384556028728800902433401131226904244661160767916883680495122225202542023841606998867411022088440946301191503335932960267228470933599974787151449279465703844493353175088719018221");
+ BigInteger e = new BigInteger("65537");
+ BigInteger d = new BigInteger("9362542596354998418106014928820888151984912891492829581578681873633736656469965533631464203894863562319612803232737938923691416707617473868582415657005943574434271946791143554652502483003923911339605326222297167404896789026986450703532494518628015811567189641735787240372075015553947628033216297520493759267733018808392882741098489889488442349031883643894014316243251108104684754879103107764521172490019661792943030921873284592436328217485953770574054344056638447333651425231219150676837203185544359148474983670261712939626697233692596362322419559401320065488125670905499610998631622562652935873085671353890279911361");
+
+ long iat = 1352995809210L;
+ long dur = 60 * 60 * 1000;
+ long exp = iat + dur;
+
+ VerifyingPublicKey mockMyIdPublicKey = RSACryptoImplementation.createPublicKey(MOCKMYID_MODULUS, MOCKMYID_PUBLIC_EXPONENT);;
+ SigningPrivateKey mockMyIdPrivateKey = RSACryptoImplementation.createPrivateKey(MOCKMYID_MODULUS, MOCKMYID_PRIVATE_EXPONENT);
+ VerifyingPublicKey publicKeyToSign = RSACryptoImplementation.createPublicKey(n, e);
+ SigningPrivateKey privateKeyToSignWith = RSACryptoImplementation.createPrivateKey(n, d);
+
+ String certificate = JSONWebTokenUtils.createCertificate(publicKeyToSign, "test@mockmyid.com", "mockmyid.com", iat, exp, mockMyIdPrivateKey);
+ String assertion = JSONWebTokenUtils.createAssertion(privateKeyToSignWith, certificate, TEST_AUDIENCE, TEST_ASSERTION_ISSUER, iat, exp);
+ String payload = JSONWebTokenUtils.decode(certificate, mockMyIdPublicKey);
+
+ String EXPECTED_PAYLOAD = "{\"exp\":1352999409210,\"iat\":1352995809210,\"iss\":\"mockmyid.com\",\"principal\":{\"email\":\"test@mockmyid.com\"},\"public-key\":{\"e\":\"65537\",\"n\":\"20332459213245328760269530796942625317006933400814022542511832260333163206808672913301254872114045771215470352093046136365629411384688395020388553744886954869033696089099714200452682590914843971683468562019706059388121176435204818734091361033445697933682779095713376909412972373727850278295874361806633955236862180792787906413536305117030045164276955491725646610368132167655556353974515423042221261732084368978523747789654468953860772774078384556028728800902433401131226904244661160767916883680495122225202542023841606998867411022088440946301191503335932960267228470933599974787151449279465703844493353175088719018221\",\"algorithm\":\"RS\"}}";
+ Assert.assertEquals(EXPECTED_PAYLOAD, payload);
+
+ // Really(!) brittle tests below. The RSA signature algorithm is deterministic, so we can test the actual signature.
+ String EXPECTED_CERTIFICATE = "eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjEzNTI5OTk0MDkyMTAsImlhdCI6MTM1Mjk5NTgwOTIxMCwiaXNzIjoibW9ja215aWQuY29tIiwicHJpbmNpcGFsIjp7ImVtYWlsIjoidGVzdEBtb2NrbXlpZC5jb20ifSwicHVibGljLWtleSI6eyJlIjoiNjU1MzciLCJuIjoiMjAzMzI0NTkyMTMyNDUzMjg3NjAyNjk1MzA3OTY5NDI2MjUzMTcwMDY5MzM0MDA4MTQwMjI1NDI1MTE4MzIyNjAzMzMxNjMyMDY4MDg2NzI5MTMzMDEyNTQ4NzIxMTQwNDU3NzEyMTU0NzAzNTIwOTMwNDYxMzYzNjU2Mjk0MTEzODQ2ODgzOTUwMjAzODg1NTM3NDQ4ODY5NTQ4NjkwMzM2OTYwODkwOTk3MTQyMDA0NTI2ODI1OTA5MTQ4NDM5NzE2ODM0Njg1NjIwMTk3MDYwNTkzODgxMjExNzY0MzUyMDQ4MTg3MzQwOTEzNjEwMzM0NDU2OTc5MzM2ODI3NzkwOTU3MTMzNzY5MDk0MTI5NzIzNzM3Mjc4NTAyNzgyOTU4NzQzNjE4MDY2MzM5NTUyMzY4NjIxODA3OTI3ODc5MDY0MTM1MzYzMDUxMTcwMzAwNDUxNjQyNzY5NTU0OTE3MjU2NDY2MTAzNjgxMzIxNjc2NTU1NTYzNTM5NzQ1MTU0MjMwNDIyMjEyNjE3MzIwODQzNjg5Nzg1MjM3NDc3ODk2NTQ0Njg5NTM4NjA3NzI3NzQwNzgzODQ1NTYwMjg3Mjg4MDA5MDI0MzM0MDExMzEyMjY5MDQyNDQ2NjExNjA3Njc5MTY4ODM2ODA0OTUxMjIyMjUyMDI1NDIwMjM4NDE2MDY5OTg4Njc0MTEwMjIwODg0NDA5NDYzMDExOTE1MDMzMzU5MzI5NjAyNjcyMjg0NzA5MzM1OTk5NzQ3ODcxNTE0NDkyNzk0NjU3MDM4NDQ0OTMzNTMxNzUwODg3MTkwMTgyMjEiLCJhbGdvcml0aG0iOiJSUyJ9fQ.ZgT0ezITaE6rRQCxEA6OHkjwAsFdE-R8943UEmiCvKKpsbxlSlI1Iya1Oho2wrhet5bjBGM77EffzC2YwzD5qa7SrVpNwSCIW6AwnlJ6YePoNblkn0y7NQ_qThvLoaP4Vlk_XM0LbK_QPHqaWU7ldm8LF5Zp4oHgayMP4YhiyKYS2TwWWcvswT2g9IhU6YdYcF0TwT2YkJ4t3h7_sVn-OmQQu4k1KKGFLpT6HOj2EGaKmw-mzayHL0r7L3-5g_7Q83RMBe_k_4YeLG8InxO3M3GreqcaImv4XO5D-C__txfFuaLJjTzKBLrIIosckaNwp4JmN1Nf8x9t5RXHLCsrjw";
+ Assert.assertEquals(EXPECTED_CERTIFICATE, certificate);
+
+ String EXPECTED_ASSERTION = "eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjEzNTI5OTk0MDkyMTAsImlhdCI6MTM1Mjk5NTgwOTIxMCwiaXNzIjoibW9ja215aWQuY29tIiwicHJpbmNpcGFsIjp7ImVtYWlsIjoidGVzdEBtb2NrbXlpZC5jb20ifSwicHVibGljLWtleSI6eyJlIjoiNjU1MzciLCJuIjoiMjAzMzI0NTkyMTMyNDUzMjg3NjAyNjk1MzA3OTY5NDI2MjUzMTcwMDY5MzM0MDA4MTQwMjI1NDI1MTE4MzIyNjAzMzMxNjMyMDY4MDg2NzI5MTMzMDEyNTQ4NzIxMTQwNDU3NzEyMTU0NzAzNTIwOTMwNDYxMzYzNjU2Mjk0MTEzODQ2ODgzOTUwMjAzODg1NTM3NDQ4ODY5NTQ4NjkwMzM2OTYwODkwOTk3MTQyMDA0NTI2ODI1OTA5MTQ4NDM5NzE2ODM0Njg1NjIwMTk3MDYwNTkzODgxMjExNzY0MzUyMDQ4MTg3MzQwOTEzNjEwMzM0NDU2OTc5MzM2ODI3NzkwOTU3MTMzNzY5MDk0MTI5NzIzNzM3Mjc4NTAyNzgyOTU4NzQzNjE4MDY2MzM5NTUyMzY4NjIxODA3OTI3ODc5MDY0MTM1MzYzMDUxMTcwMzAwNDUxNjQyNzY5NTU0OTE3MjU2NDY2MTAzNjgxMzIxNjc2NTU1NTYzNTM5NzQ1MTU0MjMwNDIyMjEyNjE3MzIwODQzNjg5Nzg1MjM3NDc3ODk2NTQ0Njg5NTM4NjA3NzI3NzQwNzgzODQ1NTYwMjg3Mjg4MDA5MDI0MzM0MDExMzEyMjY5MDQyNDQ2NjExNjA3Njc5MTY4ODM2ODA0OTUxMjIyMjUyMDI1NDIwMjM4NDE2MDY5OTg4Njc0MTEwMjIwODg0NDA5NDYzMDExOTE1MDMzMzU5MzI5NjAyNjcyMjg0NzA5MzM1OTk5NzQ3ODcxNTE0NDkyNzk0NjU3MDM4NDQ0OTMzNTMxNzUwODg3MTkwMTgyMjEiLCJhbGdvcml0aG0iOiJSUyJ9fQ.ZgT0ezITaE6rRQCxEA6OHkjwAsFdE-R8943UEmiCvKKpsbxlSlI1Iya1Oho2wrhet5bjBGM77EffzC2YwzD5qa7SrVpNwSCIW6AwnlJ6YePoNblkn0y7NQ_qThvLoaP4Vlk_XM0LbK_QPHqaWU7ldm8LF5Zp4oHgayMP4YhiyKYS2TwWWcvswT2g9IhU6YdYcF0TwT2YkJ4t3h7_sVn-OmQQu4k1KKGFLpT6HOj2EGaKmw-mzayHL0r7L3-5g_7Q83RMBe_k_4YeLG8InxO3M3GreqcaImv4XO5D-C__txfFuaLJjTzKBLrIIosckaNwp4JmN1Nf8x9t5RXHLCsrjw~eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJodHRwOlwvXC9sb2NhbGhvc3Q6ODA4MCIsImV4cCI6MTM1Mjk5OTQwOTIxMCwiaWF0IjoxMzUyOTk1ODA5MjEwLCJpc3MiOiIxMjcuMC4wLjEifQ.gj5Q9KXR_mPEltn3SXKAjIHMOpQq0FP6NdPOB-Zu149LKhQrfXS90woVJYg8WpaasmiS6gjBFni3urq3adPktzw4RoMm1qVMvSRXXIRZzgsV_vHlSenIY0KlAk4140pAlAPcdJhB2bvKUPPDq0TLzlWHgQpheAAFMGPY1OGgwgHtsCQC_vyE2wFi8M58IGYQ-05KmWc6Zo33CJG6LjVvkTPvPTEzQKFYKwDQGc4NTkqZbCNZE6iRq4mlX9LGFddzEDiSUDmS53SwR4nfFzPQE6Q1xnU4a_BLhfNpdfOc-uHGoJGbm0ZJpLdKf7zadp34ImFA9IUBhjegingZhm2i5g";
+ Assert.assertEquals(EXPECTED_ASSERTION, assertion);
+ }
+
+ @Test
+ public void testDSAGeneration() throws Exception {
+ // This test uses MockMyID DSA data but doesn't rely on this data actually
+ // being MockMyID's data.
+ final BigInteger MOCKMYID_x = new BigInteger("385cb3509f086e110c5e24bdd395a84b335a09ae", 16);
+ final BigInteger MOCKMYID_y = new BigInteger("738ec929b559b604a232a9b55a5295afc368063bb9c20fac4e53a74970a4db7956d48e4c7ed523405f629b4cc83062f13029c4d615bbacb8b97f5e56f0c7ac9bc1d4e23809889fa061425c984061fca1826040c399715ce7ed385c4dd0d402256912451e03452d3c961614eb458f188e3e8d2782916c43dbe2e571251ce38262", 16);
+ final BigInteger MOCKMYID_p = new BigInteger("ff600483db6abfc5b45eab78594b3533d550d9f1bf2a992a7a8daa6dc34f8045ad4e6e0c429d334eeeaaefd7e23d4810be00e4cc1492cba325ba81ff2d5a5b305a8d17eb3bf4a06a349d392e00d329744a5179380344e82a18c47933438f891e22aeef812d69c8f75e326cb70ea000c3f776dfdbd604638c2ef717fc26d02e17", 16);
+ final BigInteger MOCKMYID_q = new BigInteger("e21e04f911d1ed7991008ecaab3bf775984309c3", 16);
+ final BigInteger MOCKMYID_g = new BigInteger("c52a4a0ff3b7e61fdf1867ce84138369a6154f4afa92966e3c827e25cfa6cf508b90e5de419e1337e07a2e9e2a3cd5dea704d175f8ebf6af397d69e110b96afb17c7a03259329e4829b0d03bbc7896b15b4ade53e130858cc34d96269aa89041f409136c7242a38895c9d5bccad4f389af1d7a4bd1398bd072dffa896233397a", 16);
+
+ BigInteger g = new BigInteger("f7e1a085d69b3ddecbbcab5c36b857b97994afbbfa3aea82f9574c0b3d0782675159578ebad4594fe67107108180b449167123e84c281613b7cf09328cc8a6e13c167a8b547c8d28e0a3ae1e2bb3a675916ea37f0bfa213562f1fb627a01243bcca4f1bea8519089a883dfe15ae59f06928b665e807b552564014c3bfecf492a", 16);
+ BigInteger q = new BigInteger("9760508f15230bccb292b982a2eb840bf0581cf5", 16);
+ BigInteger p = new BigInteger("fd7f53811d75122952df4a9c2eece4e7f611b7523cef4400c31e3f80b6512669455d402251fb593d8d58fabfc5f5ba30f6cb9b556cd7813b801d346ff26660b76b9950a5a49f9fe8047b1022c24fbba9d7feb7c61bf83b57e7c6a8a6150f04fb83f6d3c51ec3023554135a169132f675f3ae2b61d72aeff22203199dd14801c7", 16);
+ BigInteger x = new BigInteger("b137fc5b8faaa53b170563eb03c18b46b657bb6", 16);
+ BigInteger y = new BigInteger("ea809be508bc94485553efac8ef2a8debdcdb3545ce433e8bd5889ec9d0880a13b2a8af35451161e58229d1e2be69e74a7251465a394913e8e64b0c33fde39a637b6047d7370178cf4404c0a7b4c2ed31d9cfe03ab79dbcc64667e6e7bc244eb1c127c28d725db94aff29b858bdb636f1307bdf48b3c91f387c2ab588086b6c8", 16);
+
+ long iat = 1380070362995L;
+ long dur = 60 * 60 * 1000;
+ long exp = iat + dur;
+
+ VerifyingPublicKey mockMyIdPublicKey = DSACryptoImplementation.createPublicKey(MOCKMYID_y, MOCKMYID_p, MOCKMYID_q, MOCKMYID_g);
+ SigningPrivateKey mockMyIdPrivateKey = DSACryptoImplementation.createPrivateKey(MOCKMYID_x, MOCKMYID_p, MOCKMYID_q, MOCKMYID_g);
+ VerifyingPublicKey publicKeyToSign = DSACryptoImplementation.createPublicKey(y, p, q, g);
+ SigningPrivateKey privateKeyToSignWith = DSACryptoImplementation.createPrivateKey(x, p, q, g);
+
+ String certificate = JSONWebTokenUtils.createCertificate(publicKeyToSign, "test@mockmyid.com", "mockmyid.com", iat, exp, mockMyIdPrivateKey);
+ String assertion = JSONWebTokenUtils.createAssertion(privateKeyToSignWith, certificate, TEST_AUDIENCE, TEST_ASSERTION_ISSUER, iat, exp);
+ String payload = JSONWebTokenUtils.decode(certificate, mockMyIdPublicKey);
+
+ String EXPECTED_PAYLOAD = "{\"exp\":1380073962995,\"iat\":1380070362995,\"iss\":\"mockmyid.com\",\"principal\":{\"email\":\"test@mockmyid.com\"},\"public-key\":{\"g\":\"f7e1a085d69b3ddecbbcab5c36b857b97994afbbfa3aea82f9574c0b3d0782675159578ebad4594fe67107108180b449167123e84c281613b7cf09328cc8a6e13c167a8b547c8d28e0a3ae1e2bb3a675916ea37f0bfa213562f1fb627a01243bcca4f1bea8519089a883dfe15ae59f06928b665e807b552564014c3bfecf492a\",\"q\":\"9760508f15230bccb292b982a2eb840bf0581cf5\",\"p\":\"fd7f53811d75122952df4a9c2eece4e7f611b7523cef4400c31e3f80b6512669455d402251fb593d8d58fabfc5f5ba30f6cb9b556cd7813b801d346ff26660b76b9950a5a49f9fe8047b1022c24fbba9d7feb7c61bf83b57e7c6a8a6150f04fb83f6d3c51ec3023554135a169132f675f3ae2b61d72aeff22203199dd14801c7\",\"y\":\"ea809be508bc94485553efac8ef2a8debdcdb3545ce433e8bd5889ec9d0880a13b2a8af35451161e58229d1e2be69e74a7251465a394913e8e64b0c33fde39a637b6047d7370178cf4404c0a7b4c2ed31d9cfe03ab79dbcc64667e6e7bc244eb1c127c28d725db94aff29b858bdb636f1307bdf48b3c91f387c2ab588086b6c8\",\"algorithm\":\"DS\"}}";
+ Assert.assertEquals(EXPECTED_PAYLOAD, payload);
+
+ // Really(!) brittle tests below. The DSA signature algorithm is not deterministic, so we can't test the actual signature.
+ String EXPECTED_CERTIFICATE_PREFIX = "eyJhbGciOiJEUzEyOCJ9.eyJleHAiOjEzODAwNzM5NjI5OTUsImlhdCI6MTM4MDA3MDM2Mjk5NSwiaXNzIjoibW9ja215aWQuY29tIiwicHJpbmNpcGFsIjp7ImVtYWlsIjoidGVzdEBtb2NrbXlpZC5jb20ifSwicHVibGljLWtleSI6eyJnIjoiZjdlMWEwODVkNjliM2RkZWNiYmNhYjVjMzZiODU3Yjk3OTk0YWZiYmZhM2FlYTgyZjk1NzRjMGIzZDA3ODI2NzUxNTk1NzhlYmFkNDU5NGZlNjcxMDcxMDgxODBiNDQ5MTY3MTIzZTg0YzI4MTYxM2I3Y2YwOTMyOGNjOGE2ZTEzYzE2N2E4YjU0N2M4ZDI4ZTBhM2FlMWUyYmIzYTY3NTkxNmVhMzdmMGJmYTIxMzU2MmYxZmI2MjdhMDEyNDNiY2NhNGYxYmVhODUxOTA4OWE4ODNkZmUxNWFlNTlmMDY5MjhiNjY1ZTgwN2I1NTI1NjQwMTRjM2JmZWNmNDkyYSIsInEiOiI5NzYwNTA4ZjE1MjMwYmNjYjI5MmI5ODJhMmViODQwYmYwNTgxY2Y1IiwicCI6ImZkN2Y1MzgxMWQ3NTEyMjk1MmRmNGE5YzJlZWNlNGU3ZjYxMWI3NTIzY2VmNDQwMGMzMWUzZjgwYjY1MTI2Njk0NTVkNDAyMjUxZmI1OTNkOGQ1OGZhYmZjNWY1YmEzMGY2Y2I5YjU1NmNkNzgxM2I4MDFkMzQ2ZmYyNjY2MGI3NmI5OTUwYTVhNDlmOWZlODA0N2IxMDIyYzI0ZmJiYTlkN2ZlYjdjNjFiZjgzYjU3ZTdjNmE4YTYxNTBmMDRmYjgzZjZkM2M1MWVjMzAyMzU1NDEzNWExNjkxMzJmNjc1ZjNhZTJiNjFkNzJhZWZmMjIyMDMxOTlkZDE0ODAxYzciLCJ5IjoiZWE4MDliZTUwOGJjOTQ0ODU1NTNlZmFjOGVmMmE4ZGViZGNkYjM1NDVjZTQzM2U4YmQ1ODg5ZWM5ZDA4ODBhMTNiMmE4YWYzNTQ1MTE2MWU1ODIyOWQxZTJiZTY5ZTc0YTcyNTE0NjVhMzk0OTEzZThlNjRiMGMzM2ZkZTM5YTYzN2I2MDQ3ZDczNzAxNzhjZjQ0MDRjMGE3YjRjMmVkMzFkOWNmZTAzYWI3OWRiY2M2NDY2N2U2ZTdiYzI0NGViMWMxMjdjMjhkNzI1ZGI5NGFmZjI5Yjg1OGJkYjYzNmYxMzA3YmRmNDhiM2M5MWYzODdjMmFiNTg4MDg2YjZjOCIsImFsZ29yaXRobSI6IkRTIn19";
+ String[] expectedCertificateParts = EXPECTED_CERTIFICATE_PREFIX.split("\\.");
+ String[] certificateParts = certificate.split("\\.");
+ Assert.assertEquals(expectedCertificateParts[0], certificateParts[0]);
+ Assert.assertEquals(expectedCertificateParts[1], certificateParts[1]);
+
+ String EXPECTED_ASSERTION_FRAGMENT = "eyJhbGciOiJEUzEyOCJ9.eyJhdWQiOiJodHRwOlwvXC9sb2NhbGhvc3Q6ODA4MCIsImV4cCI6MTM4MDA3Mzk2Mjk5NSwiaWF0IjoxMzgwMDcwMzYyOTk1LCJpc3MiOiIxMjcuMC4wLjEifQ";
+ String[] expectedAssertionParts = EXPECTED_ASSERTION_FRAGMENT.split("\\.");
+ String[] assertionParts = assertion.split("~")[1].split("\\.");
+ Assert.assertEquals(expectedAssertionParts[0], assertionParts[0]);
+ Assert.assertEquals(expectedAssertionParts[1], assertionParts[1]);
+ }
+
+ @Test
+ public void testGetPayloadString() throws Exception {
+ String s;
+ s = JSONWebTokenUtils.getPayloadString("{}", "audience", "issuer", 1L, 2L);
+ Assert.assertEquals("{\"aud\":\"audience\",\"exp\":2,\"iat\":1,\"iss\":\"issuer\"}", s);
+
+ // Make sure we don't include null issuedAt.
+ s = JSONWebTokenUtils.getPayloadString("{}", "audience", "issuer", null, 3L);
+ Assert.assertEquals("{\"aud\":\"audience\",\"exp\":3,\"iss\":\"issuer\"}", s);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestRSACryptoImplementation.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestRSACryptoImplementation.java
new file mode 100644
index 0000000000..6dfa88ebf2
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestRSACryptoImplementation.java
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.browserid.test;
+
+import junit.framework.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.browserid.RSACryptoImplementation;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+import java.math.BigInteger;
+
+@RunWith(TestRunner.class)
+public class TestRSACryptoImplementation {
+ @Test
+ public void testToJSONObject() throws Exception {
+ BigInteger n = new BigInteger("7042170764319402120473546823641395184140303948430445023576085129538272863656735924617881022040465877164076593767104512065359975488480629290310209335113577");
+ BigInteger e = new BigInteger("65537");
+ BigInteger d = new BigInteger("2050102629239206449128199335463237235732683202345308155771672920433658970744825199440426256856862541525088288448769859770132714705204296375901885294992205");
+
+ BrowserIDKeyPair keyPair = new BrowserIDKeyPair(
+ RSACryptoImplementation.createPrivateKey(n, d),
+ RSACryptoImplementation.createPublicKey(n, e));
+
+ ExtendedJSONObject o = new ExtendedJSONObject("{\"publicKey\":{\"e\":\"65537\",\"n\":\"7042170764319402120473546823641395184140303948430445023576085129538272863656735924617881022040465877164076593767104512065359975488480629290310209335113577\",\"algorithm\":\"RS\"},\"privateKey\":{\"d\":\"2050102629239206449128199335463237235732683202345308155771672920433658970744825199440426256856862541525088288448769859770132714705204296375901885294992205\",\"n\":\"7042170764319402120473546823641395184140303948430445023576085129538272863656735924617881022040465877164076593767104512065359975488480629290310209335113577\",\"algorithm\":\"RS\"}}");
+ Assert.assertEquals(o.getObject("privateKey"), keyPair.toJSONObject().getObject("privateKey"));
+ Assert.assertEquals(o.getObject("publicKey"), keyPair.toJSONObject().getObject("publicKey"));
+ }
+
+ @Test
+ public void testFromJSONObject() throws Exception {
+ BigInteger n = new BigInteger("7042170764319402120473546823641395184140303948430445023576085129538272863656735924617881022040465877164076593767104512065359975488480629290310209335113577");
+ BigInteger e = new BigInteger("65537");
+ BigInteger d = new BigInteger("2050102629239206449128199335463237235732683202345308155771672920433658970744825199440426256856862541525088288448769859770132714705204296375901885294992205");
+
+ BrowserIDKeyPair keyPair = new BrowserIDKeyPair(
+ RSACryptoImplementation.createPrivateKey(n, d),
+ RSACryptoImplementation.createPublicKey(n, e));
+
+ ExtendedJSONObject o = new ExtendedJSONObject("{\"publicKey\":{\"e\":\"65537\",\"n\":\"7042170764319402120473546823641395184140303948430445023576085129538272863656735924617881022040465877164076593767104512065359975488480629290310209335113577\",\"algorithm\":\"RS\"},\"privateKey\":{\"d\":\"2050102629239206449128199335463237235732683202345308155771672920433658970744825199440426256856862541525088288448769859770132714705204296375901885294992205\",\"n\":\"7042170764319402120473546823641395184140303948430445023576085129538272863656735924617881022040465877164076593767104512065359975488480629290310209335113577\",\"algorithm\":\"RS\"}}");
+
+ Assert.assertEquals(keyPair.getPublic().toJSONObject(), RSACryptoImplementation.createPublicKey(o.getObject("publicKey")).toJSONObject());
+ Assert.assertEquals(keyPair.getPrivate().toJSONObject(), RSACryptoImplementation.createPrivateKey(o.getObject("privateKey")).toJSONObject());
+ }
+
+ @Test
+ public void testRoundTrip() throws Exception {
+ BrowserIDKeyPair keyPair = RSACryptoImplementation.generateKeyPair(512);
+ ExtendedJSONObject o = keyPair.toJSONObject();
+ BrowserIDKeyPair keyPair2 = RSACryptoImplementation.fromJSONObject(o);
+ Assert.assertEquals(o, keyPair2.toJSONObject());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/cleanup/TestFileCleanupController.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/cleanup/TestFileCleanupController.java
new file mode 100644
index 0000000000..6858b65a7f
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/cleanup/TestFileCleanupController.java
@@ -0,0 +1,92 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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 org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.ArrayList;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atMost;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests functionality of the {@link FileCleanupController}.
+ */
+@RunWith(TestRunner.class)
+public class TestFileCleanupController {
+
+ @Test
+ public void testStartIfReadyEmptySharedPrefsRunsCleanup() {
+ final Context context = mock(Context.class);
+ FileCleanupController.startIfReady(context, getSharedPreferences(), "");
+ verify(context).startService(any(Intent.class));
+ }
+
+ @Test
+ public void testStartIfReadyLastRunNowDoesNotRun() {
+ final SharedPreferences sharedPrefs = getSharedPreferences();
+ sharedPrefs.edit()
+ .putLong(FileCleanupController.PREF_LAST_CLEANUP_MILLIS, System.currentTimeMillis())
+ .commit(); // synchronous to finish before test runs.
+
+ final Context context = mock(Context.class);
+ FileCleanupController.startIfReady(context, sharedPrefs, "");
+
+ verify(context, never()).startService((any(Intent.class)));
+ }
+
+ /**
+ * Depends on {@link #testStartIfReadyEmptySharedPrefsRunsCleanup()} success –
+ * i.e. we expect the cleanup to run with empty prefs.
+ */
+ @Test
+ public void testStartIfReadyDoesNotRunTwiceInSuccession() {
+ final Context context = mock(Context.class);
+ final SharedPreferences sharedPrefs = getSharedPreferences();
+
+ FileCleanupController.startIfReady(context, sharedPrefs, "");
+ verify(context).startService(any(Intent.class));
+
+ // Note: the Controller relies on SharedPrefs.apply, but
+ // robolectric made this a synchronous call. Yay!
+ FileCleanupController.startIfReady(context, sharedPrefs, "");
+ verify(context, atMost(1)).startService(any(Intent.class));
+ }
+
+ @Test
+ public void testGetFilesToCleanupContainsProfilePath() {
+ final String profilePath = "/a/profile/path";
+ final ArrayList<String> fileList = FileCleanupController.getFilesToCleanup(profilePath);
+ assertNotNull("Returned file list is non-null", fileList);
+
+ boolean atLeastOneStartsWithProfilePath = false;
+ final String pathToCheck = profilePath + "/"; // Ensure the calling code adds a slash to divide the path.
+ for (final String path : fileList) {
+ if (path.startsWith(pathToCheck)) {
+ // It'd be great if we could assert these individually so
+ // we could display the Strings in console output.
+ atLeastOneStartsWithProfilePath = true;
+ }
+ }
+ assertTrue("At least one returned String starts with a profile path", atLeastOneStartsWithProfilePath);
+ }
+
+ private SharedPreferences getSharedPreferences() {
+ return RuntimeEnvironment.application.getSharedPreferences("TestFileCleanupController", 0);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/cleanup/TestFileCleanupService.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/cleanup/TestFileCleanupService.java
new file mode 100644
index 0000000000..0326adb6a3
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/cleanup/TestFileCleanupService.java
@@ -0,0 +1,106 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.Intent;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests the methods of {@link FileCleanupService}.
+ */
+@RunWith(TestRunner.class)
+public class TestFileCleanupService {
+ @Rule
+ public final TemporaryFolder tempFolder = new TemporaryFolder();
+
+ private void assertAllFilesExist(final List<File> fileList) {
+ for (final File file : fileList) {
+ assertTrue("File exists", file.exists());
+ }
+ }
+
+ private void assertAllFilesDoNotExist(final List<File> fileList) {
+ for (final File file : fileList) {
+ assertFalse("File does not exist", file.exists());
+ }
+ }
+
+ private void onHandleIntent(final ArrayList<String> filePaths) {
+ final FileCleanupService service = new FileCleanupService();
+ final Intent intent = new Intent(FileCleanupService.ACTION_DELETE_FILES);
+ intent.putExtra(FileCleanupService.EXTRA_FILE_PATHS_TO_DELETE, filePaths);
+ service.onHandleIntent(intent);
+ }
+
+ @Test
+ public void testOnHandleIntentDeleteSpecifiedFiles() throws Exception {
+ final int fileListCount = 3;
+ final ArrayList<File> filesToDelete = generateFileList(fileListCount);
+
+ final ArrayList<String> pathsToDelete = new ArrayList<>(fileListCount);
+ for (final File file : filesToDelete) {
+ pathsToDelete.add(file.getAbsolutePath());
+ }
+
+ assertAllFilesExist(filesToDelete);
+ onHandleIntent(pathsToDelete);
+ assertAllFilesDoNotExist(filesToDelete);
+ }
+
+ @Test
+ public void testOnHandleIntentDoesNotDeleteUnrelatedFiles() throws Exception {
+ final ArrayList<File> filesShouldNotBeDeleted = generateFileList(3);
+ assertAllFilesExist(filesShouldNotBeDeleted);
+ onHandleIntent(new ArrayList<String>());
+ assertAllFilesExist(filesShouldNotBeDeleted);
+ }
+
+ @Test
+ public void testOnHandleIntentDeletesEmptyDirectory() throws Exception {
+ final File dir = tempFolder.newFolder();
+ final ArrayList<String> filesToDelete = new ArrayList<>(1);
+ filesToDelete.add(dir.getAbsolutePath());
+
+ assertTrue("Empty directory exists", dir.exists());
+ onHandleIntent(filesToDelete);
+ assertFalse("Empty directory deleted by service", dir.exists());
+ }
+
+ @Test
+ public void testOnHandleIntentDoesNotDeleteNonEmptyDirectory() throws Exception {
+ final File dir = tempFolder.newFolder();
+ final ArrayList<String> filesCannotDelete = new ArrayList<>(1);
+ filesCannotDelete.add(dir.getAbsolutePath());
+ assertTrue("Directory exists", dir.exists());
+
+ final File fileInDir = new File(dir, "file_in_dir");
+ assertTrue("File in dir created", fileInDir.createNewFile());
+
+ onHandleIntent(filesCannotDelete);
+ assertTrue("Non-empty directory not deleted", dir.exists());
+ assertTrue("File in directory not deleted", fileInDir.exists());
+ }
+
+ private ArrayList<File> generateFileList(final int size) throws IOException {
+ final ArrayList<File> fileList = new ArrayList<>(size);
+ for (int i = 0; i < size; ++i) {
+ fileList.add(tempFolder.newFile());
+ }
+ return fileList;
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserContractTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserContractTest.java
new file mode 100644
index 0000000000..e36153d0e2
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserContractTest.java
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.db;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+import static org.junit.Assert.*;
+
+@RunWith(TestRunner.class)
+public class BrowserContractTest {
+ @Test
+ /**
+ * Test that bookmark and sorting order clauses are set correctly
+ */
+ public void testGetCombinedFrecencySortOrder() throws Exception {
+ String sqlNoBookmarksDesc = BrowserContract.getCombinedFrecencySortOrder(false, false);
+ String sqlNoBookmarksAsc = BrowserContract.getCombinedFrecencySortOrder(false, true);
+ String sqlBookmarksDesc = BrowserContract.getCombinedFrecencySortOrder(true, false);
+ String sqlBookmarksAsc = BrowserContract.getCombinedFrecencySortOrder(true, true);
+
+ assertTrue(sqlBookmarksAsc.endsWith(" ASC"));
+ assertTrue(sqlBookmarksDesc.endsWith(" DESC"));
+ assertTrue(sqlNoBookmarksAsc.endsWith(" ASC"));
+ assertTrue(sqlNoBookmarksDesc.endsWith(" DESC"));
+
+ assertTrue(sqlBookmarksAsc.startsWith("(CASE WHEN bookmark_id > -1 THEN 100 ELSE 0 END) + "));
+ assertTrue(sqlBookmarksDesc.startsWith("(CASE WHEN bookmark_id > -1 THEN 100 ELSE 0 END) + "));
+ }
+
+ @Test
+ /**
+ * Test that calculation string is correct for remote visits
+ * maxFrecency=1, scaleConst=110, correct sql params for visit count and last date
+ * and that time is converted to microseconds.
+ */
+ public void testGetRemoteFrecencySQL() throws Exception {
+ long now = 1;
+ String sql = BrowserContract.getRemoteFrecencySQL(now);
+ String ageExpr = "(" + now * 1000 + " - remoteDateLastVisited) / 86400000000";
+
+ assertEquals(
+ "remoteVisitCount * MAX(1, 100 * 110 / (" + ageExpr + " * " + ageExpr + " + 110))",
+ sql
+ );
+ }
+
+ @Test
+ /**
+ * Test that calculation string is correct for remote visits
+ * maxFrecency=2, scaleConst=225, correct sql params for visit count and last date
+ * and that time is converted to microseconds.
+ */
+ public void testGetLocalFrecencySQL() throws Exception {
+ long now = 1;
+ String sql = BrowserContract.getLocalFrecencySQL(now);
+ String ageExpr = "(" + now * 1000 + " - localDateLastVisited) / 86400000000";
+ String visitCountExpr = "(localVisitCount + 2) * (localVisitCount + 2)";
+
+ assertEquals(
+ visitCountExpr + " * MAX(2, 100 * 225 / (" + ageExpr + " * " + ageExpr + " + 225))",
+ sql
+ );
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHighlightsTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHighlightsTest.java
new file mode 100644
index 0000000000..f8af41e328
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHighlightsTest.java
@@ -0,0 +1,438 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.db;
+
+import android.content.ContentProviderClient;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
+import org.mozilla.gecko.sync.setup.Constants;
+
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import static org.mozilla.gecko.db.BrowserContract.PARAM_PROFILE;
+
+/**
+ * Unit tests for the highlights query (Activity Stream).
+ */
+@RunWith(TestRunner.class)
+public class BrowserProviderHighlightsTest extends BrowserProviderHistoryVisitsTestBase {
+ private ContentProviderClient highlightsClient;
+ private ContentProviderClient activityStreamBlocklistClient;
+ private ContentProviderClient bookmarksClient;
+
+ private Uri highlightsTestUri;
+ private Uri activityStreamBlocklistTestUri;
+ private Uri bookmarksTestUri;
+
+ private Uri expireHistoryNormalUri;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ final Uri highlightsClientUri = BrowserContract.Highlights.CONTENT_URI.buildUpon()
+ .appendQueryParameter(PARAM_PROFILE, Constants.DEFAULT_PROFILE)
+ .build();
+
+ final Uri activityStreamBlocklistClientUri = BrowserContract.ActivityStreamBlocklist.CONTENT_URI.buildUpon()
+ .appendQueryParameter(PARAM_PROFILE, Constants.DEFAULT_PROFILE)
+ .build();
+
+ highlightsClient = contentResolver.acquireContentProviderClient(highlightsClientUri);
+ activityStreamBlocklistClient = contentResolver.acquireContentProviderClient(activityStreamBlocklistClientUri);
+ bookmarksClient = contentResolver.acquireContentProviderClient(BrowserContractHelpers.BOOKMARKS_CONTENT_URI);
+
+ highlightsTestUri = testUri(BrowserContract.Highlights.CONTENT_URI);
+ activityStreamBlocklistTestUri = testUri(BrowserContract.ActivityStreamBlocklist.CONTENT_URI);
+ bookmarksTestUri = testUri(BrowserContract.Bookmarks.CONTENT_URI);
+
+ expireHistoryNormalUri = testUri(BrowserContract.History.CONTENT_OLD_URI).buildUpon()
+ .appendQueryParameter(
+ BrowserContract.PARAM_EXPIRE_PRIORITY,
+ BrowserContract.ExpirePriority.NORMAL.toString()
+ ).build();
+ }
+
+ @After
+ public void tearDown() {
+ highlightsClient.release();
+ activityStreamBlocklistClient.release();
+ bookmarksClient.release();
+
+ super.tearDown();
+ }
+
+ /**
+ * Scenario: Empty database, no history, no bookmarks.
+ *
+ * Assert that:
+ * - Empty cursor (not null) is returned.
+ */
+ @Test
+ public void testEmptyDatabase() throws Exception {
+ final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null);
+ Assert.assertNotNull(cursor);
+
+ Assert.assertEquals(0, cursor.getCount());
+
+ cursor.close();
+ }
+
+ /**
+ * Scenario: The database only contains very recent history (now, 5 minutes ago, 20 minutes).
+ *
+ * Assert that:
+ * - No highlight is returned from recent history.
+ */
+ @Test
+ public void testOnlyRecentHistory() throws Exception {
+ final long now = System.currentTimeMillis();
+ final long fiveMinutesAgo = now - 1000 * 60 * 5;
+ final long twentyMinutes = now - 1000 * 60 * 20;
+
+ insertHistoryItem(createUniqueUrl(), createGUID(), now, 1, createUniqueTitle());
+ insertHistoryItem(createUniqueUrl(), createGUID(), fiveMinutesAgo, 1, createUniqueTitle());
+ insertHistoryItem(createUniqueUrl(), createGUID(), twentyMinutes, 1, createUniqueTitle());
+
+ final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null);
+
+ Assert.assertNotNull(cursor);
+
+ Assert.assertEquals(0, cursor.getCount());
+
+ cursor.close();
+ }
+
+ /**
+ * Scenario: The database contains recent (but not too fresh) history (1 hour, 5 days).
+ *
+ * Assert that:
+ * - Highlights are returned from history.
+ */
+ @Test
+ public void testHighlightsArePickedFromHistory() throws Exception {
+ final String url1 = createUniqueUrl();
+ final String url2 = createUniqueUrl();
+ final String title1 = createUniqueTitle();
+ final String title2 = createUniqueTitle();
+
+ final long oneHourAgo = System.currentTimeMillis() - 1000 * 60 * 60;
+ final long fiveDaysAgo = System.currentTimeMillis() - 1000 * 60 * 60 * 24 * 5;
+
+ insertHistoryItem(url1, createGUID(), oneHourAgo, 1, title1);
+ insertHistoryItem(url2, createGUID(), fiveDaysAgo, 1, title2);
+
+ final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null);
+ Assert.assertNotNull(cursor);
+
+ Assert.assertEquals(2, cursor.getCount());
+
+ assertCursorContainsEntry(cursor, url1, title1);
+ assertCursorContainsEntry(cursor, url2, title2);
+
+ cursor.close();
+ }
+
+ /**
+ * Scenario: The database contains history that is visited frequently and rarely.
+ *
+ * Assert that:
+ * - Highlights are picked from rarely visited websites.
+ * - Highlights are not picked from frequently visited websites.
+ */
+ @Test
+ public void testOftenVisitedPagesAreNotPicked() throws Exception {
+ final String url1 = createUniqueUrl();
+ final String title1 = createUniqueTitle();
+
+ final long oneHourAgo = System.currentTimeMillis() - 1000 * 60 * 60;
+ final long fiveDaysAgo = System.currentTimeMillis() - 1000 * 60 * 60 * 24 * 5;
+
+ insertHistoryItem(url1, createGUID(), oneHourAgo, 2, title1);
+ insertHistoryItem(createUniqueUrl(), createGUID(), fiveDaysAgo, 25, createUniqueTitle());
+
+ final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null);
+ Assert.assertNotNull(cursor);
+
+ // Verify that only the first URL (with one visit) is picked and the second URL with 25 visits is ignored.
+
+ Assert.assertEquals(1, cursor.getCount());
+
+ cursor.moveToNext();
+ assertCursor(cursor, url1, title1);
+
+ cursor.close();
+ }
+
+ /**
+ * Scenario: The database contains history with and without titles.
+ *
+ * Assert that:
+ * - History without titles is not picked for highlights.
+ */
+ @Test
+ public void testHistoryWithoutTitlesIsNotPicked() throws Exception {
+ final String url1 = createUniqueUrl();
+ final String url2 = createUniqueUrl();
+ final String title1 = "";
+ final String title2 = createUniqueTitle();
+
+ final long oneHourAgo = System.currentTimeMillis() - 1000 * 60 * 60;
+ final long fiveDaysAgo = System.currentTimeMillis() - 1000 * 60 * 60 * 24 * 5;
+
+ insertHistoryItem(url1, createGUID(), oneHourAgo, 1, title1);
+ insertHistoryItem(url2, createGUID(), fiveDaysAgo, 1, title2);
+
+ final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null);
+ Assert.assertNotNull(cursor);
+
+ // Only one bookmark will be picked for highlights
+ Assert.assertEquals(1, cursor.getCount());
+
+ cursor.moveToNext();
+ assertCursor(cursor, url2, title2);
+
+ cursor.close();
+ }
+
+ /**
+ * Scenario: Database contains two bookmarks (unvisited).
+ *
+ * Assert that:
+ * - One bookmark is picked for highlights.
+ */
+ @Test
+ public void testPickingBookmarkForHighlights() throws Exception {
+ final long oneHourAgo = System.currentTimeMillis() - 1000 * 60 * 60;
+ final long fiveDaysAgo = System.currentTimeMillis() - 1000 * 60 * 60 * 24 * 5;
+
+ final String url1 = createUniqueUrl();
+ final String url2 = createUniqueUrl();
+ final String title1 = createUniqueTitle();
+ final String title2 = createUniqueTitle();
+
+ insertBookmarkItem(url1, title1, oneHourAgo);
+ insertBookmarkItem(url2, title2, fiveDaysAgo);
+
+ final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null);
+ Assert.assertNotNull(cursor);
+
+ Assert.assertEquals(1, cursor.getCount());
+
+ cursor.moveToNext();
+ assertCursor(cursor, url1, title1);
+
+ cursor.close();
+ }
+
+ /**
+ * Scenario: Database contains an often visited bookmark.
+ *
+ * Assert that:
+ * - Bookmark is not selected for highlights.
+ */
+ @Test
+ public void testOftenVisitedBookmarksWillNotBePicked() throws Exception {
+ final String url = createUniqueUrl();
+ final long oneHourAgo = System.currentTimeMillis() - 1000 * 60 * 60;
+
+ insertBookmarkItem(url, createUniqueTitle(), oneHourAgo);
+ insertHistoryItem(url, createGUID(), oneHourAgo, 25, createUniqueTitle());
+
+ final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null);
+ Assert.assertNotNull(cursor);
+
+ Assert.assertEquals(0, cursor.getCount());
+
+ cursor.close();
+ }
+
+ /**
+ * Scenario: Database contains URL as bookmark and in history (not visited often).
+ *
+ * Assert that:
+ * - URL is not picked twice (as bookmark and from history)
+ */
+ @Test
+ public void testSameUrlIsNotPickedFromHistoryAndBookmarks() throws Exception {
+ final String url = createUniqueUrl();
+
+ final long oneHourAgo = System.currentTimeMillis() - 1000 * 60 * 60;
+
+ // Insert bookmark that is picked for highlights
+ insertBookmarkItem(url, createUniqueTitle(), oneHourAgo);
+ // Insert history for same URL that would be picked for highlights too
+ insertHistoryItem(url, createGUID(), oneHourAgo, 2, createUniqueTitle());
+
+ final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null);
+ Assert.assertNotNull(cursor);
+
+ Assert.assertEquals(1, cursor.getCount());
+
+ cursor.close();
+ }
+
+ /**
+ * Scenario: Database contains only old bookmarks.
+ *
+ * Assert that:
+ * - Old bookmarks are not selected as highlight.
+ */
+ @Test
+ public void testVeryOldBookmarksAreNotSelected() throws Exception {
+ final long oneWeekAgo = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7);
+ final long oneMonthAgo = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31);
+ final long oneYearAgo = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(365);
+
+ insertBookmarkItem(createUniqueUrl(), createUniqueTitle(), oneWeekAgo);
+ insertBookmarkItem(createUniqueUrl(), createUniqueTitle(), oneMonthAgo);
+ insertBookmarkItem(createUniqueUrl(), createUniqueTitle(), oneYearAgo);
+
+ final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null);
+ Assert.assertNotNull(cursor);
+
+ Assert.assertEquals(0, cursor.getCount());
+
+ cursor.close();
+ }
+
+ @Test
+ public void testBlocklistItemsAreNotSelected() throws Exception {
+ final long oneDayAgo = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1);
+
+ final String blockURL = createUniqueUrl();
+
+ insertBookmarkItem(blockURL, createUniqueTitle(), oneDayAgo);
+
+ Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null);
+ Assert.assertNotNull(cursor);
+ Assert.assertEquals(1, cursor.getCount());
+ cursor.close();
+
+ insertBlocklistItem(blockURL);
+
+ cursor = highlightsClient.query(highlightsTestUri, null, null, null, null);
+ Assert.assertNotNull(cursor);
+ Assert.assertEquals(0, cursor.getCount());
+ cursor.close();
+ }
+
+ @Test
+ public void testBlocklistItemsExpire() throws Exception {
+ final long oneDayAgo = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1);
+
+ final String blockURL = createUniqueUrl();
+ final String blockTitle = createUniqueTitle();
+
+ insertBookmarkItem(blockURL, blockTitle, oneDayAgo);
+ insertBlocklistItem(blockURL);
+
+ {
+ final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null);
+ Assert.assertNotNull(cursor);
+ Assert.assertEquals(0, cursor.getCount());
+ cursor.close();
+ }
+
+ // Add (2000 / 10) items in the loop -> 201 items total
+ int itemsNeeded = BrowserProvider.DEFAULT_EXPIRY_RETAIN_COUNT / BrowserProvider.ACTIVITYSTREAM_BLOCKLIST_EXPIRY_FACTOR;
+ for (int i = 0; i < itemsNeeded; i++) {
+ insertBlocklistItem(createUniqueUrl());
+ }
+
+ // We still have zero highlights: the item is still blocked
+ {
+ final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null);
+ Assert.assertNotNull(cursor);
+ Assert.assertEquals(0, cursor.getCount());
+ cursor.close();
+ }
+
+ // expire the original blocked URL - only most recent 200 items are retained
+ historyClient.delete(expireHistoryNormalUri, null, null);
+
+ // And the original URL is now in highlights again (note: this shouldn't happen in real life,
+ // since the URL will no longer be eligible for highlights by the time we expire it)
+ {
+ final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null);
+ Assert.assertNotNull(cursor);
+ Assert.assertEquals(1, cursor.getCount());
+
+ cursor.moveToFirst();
+ assertCursor(cursor, blockURL, blockTitle);
+ cursor.close();
+ }
+ }
+
+ private void insertBookmarkItem(String url, String title, long createdAt) throws RemoteException {
+ ContentValues values = new ContentValues();
+
+ values.put(BrowserContract.Bookmarks.URL, url);
+ values.put(BrowserContract.Bookmarks.TITLE, title);
+ values.put(BrowserContract.Bookmarks.PARENT, 0);
+ values.put(BrowserContract.Bookmarks.TYPE, BrowserContract.Bookmarks.TYPE_BOOKMARK);
+ values.put(BrowserContract.Bookmarks.DATE_CREATED, createdAt);
+
+ bookmarksClient.insert(bookmarksTestUri, values);
+ }
+
+ private void insertBlocklistItem(String url) throws RemoteException {
+ final ContentValues values = new ContentValues();
+ values.put(BrowserContract.ActivityStreamBlocklist.URL, url);
+
+ activityStreamBlocklistClient.insert(activityStreamBlocklistTestUri, values);
+ }
+
+ private void assertCursor(Cursor cursor, String url, String title) {
+ final String actualTitle = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.TITLE));
+ Assert.assertEquals(title, actualTitle);
+
+ final String actualUrl = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
+ Assert.assertEquals(url, actualUrl);
+ }
+
+ private void assertCursorContainsEntry(Cursor cursor, String url, String title) {
+ cursor.moveToFirst();
+
+ do {
+ final String actualTitle = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.TITLE));
+ final String actualUrl = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
+
+ if (actualTitle.equals(title) && actualUrl.equals(url)) {
+ return;
+ }
+ } while (cursor.moveToNext());
+
+ Assert.fail("Could not find entry title=" + title + ", url=" + url);
+ }
+
+ private String createUniqueUrl() {
+ return new Uri.Builder()
+ .scheme("https")
+ .authority(UUID.randomUUID().toString() + ".example.org")
+ .appendPath(UUID.randomUUID().toString())
+ .appendPath(UUID.randomUUID().toString())
+ .build()
+ .toString();
+ }
+
+ private String createUniqueTitle() {
+ return "Title " + UUID.randomUUID().toString();
+ }
+
+ private String createGUID() {
+ return UUID.randomUUID().toString();
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryTest.java
new file mode 100644
index 0000000000..850841432d
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryTest.java
@@ -0,0 +1,341 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.ContentProviderClient;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.robolectric.shadows.ShadowContentResolver;
+
+import static org.junit.Assert.*;
+
+/**
+ * Testing functionality exposed by BrowserProvider ContentProvider (history, bookmarks, etc).
+ * This is WIP junit4 port of robocop tests at org.mozilla.gecko.tests.testBrowserProvider.
+ * See Bug 1269492
+ */
+@RunWith(TestRunner.class)
+public class BrowserProviderHistoryTest extends BrowserProviderHistoryVisitsTestBase {
+ private ContentProviderClient thumbnailClient;
+ private Uri thumbnailTestUri;
+ private Uri expireHistoryNormalUri;
+ private Uri expireHistoryAggressiveUri;
+
+ private static final long THREE_MONTHS = 1000L * 60L * 60L * 24L * 30L * 3L;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ final ShadowContentResolver cr = new ShadowContentResolver();
+ thumbnailClient = cr.acquireContentProviderClient(BrowserContract.Thumbnails.CONTENT_URI);
+ thumbnailTestUri = testUri(BrowserContract.Thumbnails.CONTENT_URI);
+ expireHistoryNormalUri = testUri(BrowserContract.History.CONTENT_OLD_URI).buildUpon()
+ .appendQueryParameter(
+ BrowserContract.PARAM_EXPIRE_PRIORITY,
+ BrowserContract.ExpirePriority.NORMAL.toString()
+ ).build();
+ expireHistoryAggressiveUri = testUri(BrowserContract.History.CONTENT_OLD_URI).buildUpon()
+ .appendQueryParameter(
+ BrowserContract.PARAM_EXPIRE_PRIORITY,
+ BrowserContract.ExpirePriority.AGGRESSIVE.toString()
+ ).build();
+ }
+
+ @After
+ @Override
+ public void tearDown() {
+ thumbnailClient.release();
+ super.tearDown();
+ }
+
+ /**
+ * Test aggressive expiration on new (recent) history items
+ */
+ @Test
+ public void testHistoryExpirationAggressiveNew() throws Exception {
+ final int historyItemsCount = 3000;
+ insertHistory(historyItemsCount, System.currentTimeMillis());
+
+ historyClient.delete(expireHistoryAggressiveUri, null, null);
+
+ /**
+ * Aggressive expiration should leave 500 history items
+ * See {@link BrowserProvider.AGGRESSIVE_EXPIRY_RETAIN_COUNT}
+ */
+ assertRowCount(historyClient, historyTestUri, 500);
+
+ /**
+ * Aggressive expiration should leave 15 thumbnails
+ * See {@link BrowserProvider.DEFAULT_EXPIRY_THUMBNAIL_COUNT}
+ */
+ assertRowCount(thumbnailClient, thumbnailTestUri, 15);
+ }
+
+ /**
+ * Test normal expiration on new (recent) history items
+ */
+ @Test
+ public void testHistoryExpirationNormalNew() throws Exception {
+ final int historyItemsCount = 3000;
+ insertHistory(historyItemsCount, System.currentTimeMillis());
+
+ historyClient.delete(expireHistoryNormalUri, null, null);
+
+ // Normal expiration shouldn't expire new items
+ assertRowCount(historyClient, historyTestUri, 3000);
+
+ /**
+ * Normal expiration should leave 15 thumbnails
+ * See {@link BrowserProvider.DEFAULT_EXPIRY_THUMBNAIL_COUNT}
+ */
+ assertRowCount(thumbnailClient, thumbnailTestUri, 15);
+ }
+
+ /**
+ * Test aggressive expiration on old history items
+ */
+ @Test
+ public void testHistoryExpirationAggressiveOld() throws Exception {
+ final int historyItemsCount = 3000;
+ insertHistory(historyItemsCount, System.currentTimeMillis() - THREE_MONTHS);
+
+ historyClient.delete(expireHistoryAggressiveUri, null, null);
+
+ /**
+ * Aggressive expiration should leave 500 history items
+ * See {@link BrowserProvider.AGGRESSIVE_EXPIRY_RETAIN_COUNT}
+ */
+ assertRowCount(historyClient, historyTestUri, 500);
+
+ /**
+ * Aggressive expiration should leave 15 thumbnails
+ * See {@link BrowserProvider.DEFAULT_EXPIRY_THUMBNAIL_COUNT}
+ */
+ assertRowCount(thumbnailClient, thumbnailTestUri, 15);
+ }
+
+ /**
+ * Test normal expiration on old history items
+ */
+ @Test
+ public void testHistoryExpirationNormalOld() throws Exception {
+ final int historyItemsCount = 3000;
+ insertHistory(historyItemsCount, System.currentTimeMillis() - THREE_MONTHS);
+
+ historyClient.delete(expireHistoryNormalUri, null, null);
+
+ /**
+ * Normal expiration of old items should retain at most 2000 items
+ * See {@link BrowserProvider.DEFAULT_EXPIRY_RETAIN_COUNT}
+ */
+ assertRowCount(historyClient, historyTestUri, 2000);
+
+ /**
+ * Normal expiration should leave 15 thumbnails
+ * See {@link BrowserProvider.DEFAULT_EXPIRY_THUMBNAIL_COUNT}
+ */
+ assertRowCount(thumbnailClient, thumbnailTestUri, 15);
+ }
+
+ /**
+ * Test that we update aggregates at the appropriate times. Local visit aggregates are only updated
+ * when updating history record with PARAM_INCREMENT_VISITS=true. Remote aggregate values are updated
+ * only if set directly. Aggregate values are not set when inserting a new history record via insertHistory.
+ * Local aggregate values are set when inserting a new history record via update.
+ * @throws Exception
+ */
+ @Test
+ public void testHistoryVisitAggregates() throws Exception {
+ final long baseDate = System.currentTimeMillis();
+ final String url = "https://www.mozilla.org";
+ final Uri historyIncrementVisitsUri = historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true")
+ .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build();
+
+ // Test default values
+ insertHistoryItem(url, null, baseDate, null);
+ assertHistoryAggregates(BrowserContract.History.URL + " = ?", new String[] {url},
+ 0, 0, 0, 0, 0);
+
+ // Test setting visit count on new history item creation
+ final String url2 = "https://www.eff.org";
+ insertHistoryItem(url2, null, baseDate, 17);
+ assertHistoryAggregates(BrowserContract.History.URL + " = ?", new String[] {url2},
+ 17, 0, 0, 0, 0);
+
+ // Test setting visit count on new history item creation via .update
+ final String url3 = "https://www.torproject.org";
+ final ContentValues cv = new ContentValues();
+ cv.put(BrowserContract.History.URL, url3);
+ cv.put(BrowserContract.History.VISITS, 13);
+ cv.put(BrowserContract.History.DATE_LAST_VISITED, baseDate);
+ historyClient.update(historyIncrementVisitsUri, cv, BrowserContract.History.URL + " = ?", new String[] {url3});
+ assertHistoryAggregates(BrowserContract.History.URL + " = ?", new String[] {url3},
+ 13, 13, baseDate, 0, 0);
+
+ // Test that updating meta doesn't touch aggregates
+ cv.clear();
+ cv.put(BrowserContract.History.TITLE, "New title");
+ historyClient.update(historyTestUri, cv, BrowserContract.History.URL + " = ?", new String[] {url});
+ assertHistoryAggregates(BrowserContract.History.URL + " = ?", new String[] {url},
+ 0, 0, 0, 0, 0);
+
+ // Test that incrementing visits without specifying visit count updates local aggregate values
+ final long lastVisited = System.currentTimeMillis();
+ cv.clear();
+ cv.put(BrowserContract.History.DATE_LAST_VISITED, lastVisited);
+ historyClient.update(historyIncrementVisitsUri,
+ cv, BrowserContract.History.URL + " = ?", new String[] {url});
+ assertHistoryAggregates(BrowserContract.History.URL + " = ?", new String[] {url},
+ 1, 1, lastVisited, 0, 0);
+
+ // Test that incrementing visits by a specified visit count updates local aggregate values
+ // We don't support bumping visit count by more than 1. This doesn't make sense when we keep
+ // detailed information about our individual visits.
+ final long lastVisited2 = System.currentTimeMillis();
+ cv.clear();
+ cv.put(BrowserContract.History.DATE_LAST_VISITED, lastVisited2);
+ cv.put(BrowserContract.History.VISITS, 10);
+ historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(),
+ cv, BrowserContract.History.URL + " = ?", new String[] {url});
+ assertHistoryAggregates(BrowserContract.History.URL + " = ?", new String[] {url},
+ 2, 2, lastVisited2, 0, 0);
+
+ // Test that we can directly update aggregate values
+ // NB: visits is unchanged (2)
+ final long lastVisited3 = System.currentTimeMillis();
+ cv.clear();
+ cv.put(BrowserContract.History.LOCAL_DATE_LAST_VISITED, lastVisited3);
+ cv.put(BrowserContract.History.LOCAL_VISITS, 19);
+ cv.put(BrowserContract.History.REMOTE_DATE_LAST_VISITED, lastVisited3 - 100);
+ cv.put(BrowserContract.History.REMOTE_VISITS, 3);
+ historyClient.update(historyTestUri, cv, BrowserContract.History.URL + " = ?", new String[] {url});
+ assertHistoryAggregates(BrowserContract.History.URL + " = ?", new String[] {url},
+ 2, 19, lastVisited3, 3, lastVisited3 - 100);
+
+ // Test that we can set remote aggregate count to a specific value
+ cv.clear();
+ cv.put(BrowserContract.History.REMOTE_VISITS, 5);
+ historyClient.update(historyTestUri, cv, BrowserContract.History.URL + " = ?", new String[] {url});
+ assertHistoryAggregates(BrowserContract.History.URL + " = ?", new String[] {url},
+ 2, 19, lastVisited3, 5, lastVisited3 - 100);
+
+ // Test that we can increment remote aggregate value by setting a query param in the URI
+ final Uri historyIncrementRemoteAggregateUri = historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_REMOTE_AGGREGATES, "true")
+ .build();
+ cv.clear();
+ cv.put(BrowserContract.History.REMOTE_DATE_LAST_VISITED, lastVisited3);
+ cv.put(BrowserContract.History.REMOTE_VISITS, 3);
+ historyClient.update(historyIncrementRemoteAggregateUri, cv, BrowserContract.History.URL + " = ?", new String[] {url});
+ // NB: remoteVisits=8. Previous value was 5, and we're incrementing by 3.
+ assertHistoryAggregates(BrowserContract.History.URL + " = ?", new String[] {url},
+ 2, 19, lastVisited3, 8, lastVisited3);
+
+ // Test that we throw when trying to increment REMOTE_VISITS without passing in "increment by" value
+ cv.clear();
+ try {
+ historyClient.update(historyIncrementRemoteAggregateUri, cv, BrowserContract.History.URL + " = ?", new String[]{url});
+ assertTrue("Expected to throw IllegalArgumentException", false);
+ } catch (IllegalArgumentException e) {
+ assertTrue(true);
+
+ // NB: same values as above, to ensure throwing update didn't actually change anything.
+ assertHistoryAggregates(BrowserContract.History.URL + " = ?", new String[] {url},
+ 2, 19, lastVisited3, 8, lastVisited3);
+ }
+ }
+
+ private void assertHistoryAggregates(String selection, String[] selectionArg, int visits, int localVisits, long localLastVisited, int remoteVisits, long remoteLastVisited) throws Exception {
+ final Cursor c = historyClient.query(historyTestUri, new String[] {
+ BrowserContract.History.VISITS,
+ BrowserContract.History.LOCAL_VISITS,
+ BrowserContract.History.REMOTE_VISITS,
+ BrowserContract.History.LOCAL_DATE_LAST_VISITED,
+ BrowserContract.History.REMOTE_DATE_LAST_VISITED
+ }, selection, selectionArg, null);
+
+ assertNotNull(c);
+ try {
+ assertTrue(c.moveToFirst());
+
+ final int visitsCol = c.getColumnIndexOrThrow(BrowserContract.History.VISITS);
+ final int localVisitsCol = c.getColumnIndexOrThrow(BrowserContract.History.LOCAL_VISITS);
+ final int remoteVisitsCol = c.getColumnIndexOrThrow(BrowserContract.History.REMOTE_VISITS);
+ final int localDateLastVisitedCol = c.getColumnIndexOrThrow(BrowserContract.History.LOCAL_DATE_LAST_VISITED);
+ final int remoteDateLastVisitedCol = c.getColumnIndexOrThrow(BrowserContract.History.REMOTE_DATE_LAST_VISITED);
+
+ assertEquals(visits, c.getInt(visitsCol));
+
+ assertEquals(localVisits, c.getInt(localVisitsCol));
+ assertEquals(localLastVisited, c.getLong(localDateLastVisitedCol));
+
+ assertEquals(remoteVisits, c.getInt(remoteVisitsCol));
+ assertEquals(remoteLastVisited, c.getLong(remoteDateLastVisitedCol));
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Insert <code>count</code> history records with thumbnails, and for a third of records insert a visit.
+ * Inserting visits only for some of the history records is in order to ensure we're correctly JOIN-ing
+ * History and Visits tables in the Combined view.
+ * Will ensure that date_created and date_modified for new records are the same as last visited date.
+ *
+ * @param count number of history records to insert
+ * @param baseTime timestamp which will be used as a basis for last visited date
+ * @throws RemoteException
+ */
+ private void insertHistory(int count, long baseTime) throws RemoteException {
+ Uri incrementUri = historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build();
+
+ for (int i = 0; i < count; i++) {
+ final String url = "https://www.mozilla" + i + ".org";
+ insertHistoryItem(url, "testGUID" + i, baseTime - i, null);
+ if (i % 3 == 0) {
+ assertEquals(1, historyClient.update(incrementUri, new ContentValues(), BrowserContract.History.URL + " = ?", new String[]{url}));
+ }
+
+ // inserting a new entry sets the date created and modified automatically, so let's reset them
+ ContentValues cv = new ContentValues();
+ cv.put(BrowserContract.History.DATE_CREATED, baseTime - i);
+ cv.put(BrowserContract.History.DATE_MODIFIED, baseTime - i);
+ assertEquals(1, historyClient.update(historyTestUri, cv, BrowserContract.History.URL + " = ?",
+ new String[] { "https://www.mozilla" + i + ".org" }));
+ }
+
+ // insert thumbnails for history items
+ ContentValues[] thumbs = new ContentValues[count];
+ for (int i = 0; i < count; i++) {
+ thumbs[i] = new ContentValues();
+ thumbs[i].put(BrowserContract.Thumbnails.DATA, i);
+ thumbs[i].put(BrowserContract.Thumbnails.URL, "https://www.mozilla" + i + ".org");
+ }
+ assertEquals(count, thumbnailClient.bulkInsert(thumbnailTestUri, thumbs));
+ }
+
+ private void assertRowCount(final ContentProviderClient client, final Uri uri, final int count) throws RemoteException {
+ final Cursor c = client.query(uri, null, null, null, null);
+ assertNotNull(c);
+ try {
+ assertEquals(count, c.getCount());
+ } finally {
+ c.close();
+ }
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryVisitsTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryVisitsTest.java
new file mode 100644
index 0000000000..71c21166d1
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryVisitsTest.java
@@ -0,0 +1,338 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.db;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+import org.mozilla.gecko.db.BrowserContract.History;
+
+import static org.junit.Assert.*;
+
+@RunWith(TestRunner.class)
+/**
+ * Testing insertion/deletion of visits as by-product of updating history records through BrowserProvider
+ */
+public class BrowserProviderHistoryVisitsTest extends BrowserProviderHistoryVisitsTestBase {
+ @Test
+ /**
+ * Testing updating history records without affecting visits
+ */
+ public void testUpdateNoVisit() throws Exception {
+ insertHistoryItem("https://www.mozilla.org", "testGUID");
+
+ Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(0, cursor.getCount());
+ cursor.close();
+
+ ContentValues historyUpdate = new ContentValues();
+ historyUpdate.put(History.TITLE, "Mozilla!");
+ assertEquals(1,
+ historyClient.update(
+ historyTestUri, historyUpdate, History.URL + " = ?", new String[] {"https://www.mozilla.org"}
+ )
+ );
+
+ cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(0, cursor.getCount());
+ cursor.close();
+
+ ContentValues historyToInsert = new ContentValues();
+ historyToInsert.put(History.URL, "https://www.eff.org");
+ assertEquals(1,
+ historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(),
+ historyToInsert, null, null
+ )
+ );
+
+ cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(0, cursor.getCount());
+ cursor.close();
+ }
+
+ @Test
+ /**
+ * Testing INCREMENT_VISITS flag for multiple history records at once
+ */
+ public void testUpdateMultipleHistoryIncrementVisit() throws Exception {
+ insertHistoryItem("https://www.mozilla.org", "testGUID");
+ insertHistoryItem("https://www.mozilla.org", "testGUID2");
+
+ // test that visits get inserted when updating existing history records
+ assertEquals(2, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(),
+ new ContentValues(), History.URL + " = ?", new String[] {"https://www.mozilla.org"}
+ ));
+
+ Cursor cursor = visitsClient.query(
+ visitsTestUri, new String[] {BrowserContract.Visits.HISTORY_GUID}, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(2, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+
+ String guid1 = cursor.getString(cursor.getColumnIndex(BrowserContract.Visits.HISTORY_GUID));
+ cursor.moveToNext();
+ String guid2 = cursor.getString(cursor.getColumnIndex(BrowserContract.Visits.HISTORY_GUID));
+ cursor.close();
+
+ assertNotEquals(guid1, guid2);
+
+ assertTrue(guid1.equals("testGUID") || guid1.equals("testGUID2"));
+ }
+
+ @Test
+ /**
+ * Testing INCREMENT_VISITS flag and its interplay with INSERT_IF_NEEDED
+ */
+ public void testUpdateHistoryIncrementVisit() throws Exception {
+ insertHistoryItem("https://www.mozilla.org", "testGUID");
+
+ // test that visit gets inserted when updating an existing histor record
+ assertEquals(1, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(),
+ new ContentValues(), History.URL + " = ?", new String[] {"https://www.mozilla.org"}
+ ));
+
+ Cursor cursor = visitsClient.query(
+ visitsTestUri, new String[] {BrowserContract.Visits.HISTORY_GUID}, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+ assertEquals(
+ "testGUID",
+ cursor.getString(cursor.getColumnIndex(BrowserContract.Visits.HISTORY_GUID))
+ );
+ cursor.close();
+
+ // test that visit gets inserted when updatingOrInserting a new history record
+ ContentValues historyItem = new ContentValues();
+ historyItem.put(History.URL, "https://www.eff.org");
+
+ assertEquals(1, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true")
+ .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(),
+ historyItem, null, null
+ ));
+
+ cursor = historyClient.query(
+ historyTestUri,
+ new String[] {History.GUID}, History.URL + " = ?", new String[] {"https://www.eff.org"}, null
+ );
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+ String insertedGUID = cursor.getString(cursor.getColumnIndex(History.GUID));
+ cursor.close();
+
+ cursor = visitsClient.query(
+ visitsTestUri, new String[] {BrowserContract.Visits.HISTORY_GUID}, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(2, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+ assertEquals(insertedGUID,
+ cursor.getString(cursor.getColumnIndex(BrowserContract.Visits.HISTORY_GUID))
+ );
+ cursor.close();
+ }
+
+ @Test
+ /**
+ * Test that for locally generated visits, we store their timestamps in microseconds, and not in
+ * milliseconds like history does.
+ */
+ public void testTimestampConversionOnInsertion() throws Exception {
+ insertHistoryItem("https://www.mozilla.org", "testGUID");
+
+ Long lastVisited = System.currentTimeMillis();
+ ContentValues updatedVisitedTime = new ContentValues();
+ updatedVisitedTime.put(History.DATE_LAST_VISITED, lastVisited);
+
+ // test with last visited date passed in
+ assertEquals(1, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(),
+ updatedVisitedTime, History.URL + " = ?", new String[] {"https://www.mozilla.org"}
+ ));
+
+ Cursor cursor = visitsClient.query(visitsTestUri, new String[] {BrowserContract.Visits.DATE_VISITED}, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+
+ assertEquals(lastVisited * 1000, cursor.getLong(cursor.getColumnIndex(BrowserContract.Visits.DATE_VISITED)));
+ cursor.close();
+
+ // test without last visited date
+ assertEquals(1, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(),
+ new ContentValues(), History.URL + " = ?", new String[] {"https://www.mozilla.org"}
+ ));
+
+ cursor = visitsClient.query(visitsTestUri, new String[] {BrowserContract.Visits.DATE_VISITED}, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(2, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+
+ // CP should generate time off of current time upon insertion and convert to microseconds.
+ // This also tests correct ordering (DESC on date).
+ assertTrue(lastVisited * 1000 < cursor.getLong(cursor.getColumnIndex(BrowserContract.Visits.DATE_VISITED)));
+ cursor.close();
+ }
+
+ @Test
+ /**
+ * This should perform `DELETE FROM visits WHERE history_guid in IN (?, ?, ?, ..., ?)` sort of statement
+ * SQLite has a variable count limit (999 by default), so we're testing here that our deletion
+ * code does the right thing and chunks deletes to account for this limitation.
+ */
+ public void testDeletingLotsOfHistory() throws Exception {
+ Uri incrementUri = historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build();
+
+ // insert bunch of history records, and for each insert a visit
+ for (int i = 0; i < 2100; i++) {
+ final String url = "https://www.mozilla" + i + ".org";
+ insertHistoryItem(url, "testGUID" + i);
+ assertEquals(1, historyClient.update(incrementUri, new ContentValues(), History.URL + " = ?", new String[] {url}));
+ }
+
+ // sanity check
+ Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(2100, cursor.getCount());
+ cursor.close();
+
+ // delete all of the history items - this will trigger chunked deletion of visits as well
+ assertEquals(2100,
+ historyClient.delete(historyTestUri, null, null)
+ );
+
+ // check that all visits where deleted
+ cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(0, cursor.getCount());
+ cursor.close();
+ }
+
+ @Test
+ /**
+ * Test visit deletion as by-product of history deletion - both explicit (from outside of Sync),
+ * and implicit (cascaded, from Sync).
+ */
+ public void testDeletingHistory() throws Exception {
+ insertHistoryItem("https://www.mozilla.org", "testGUID");
+ insertHistoryItem("https://www.eff.org", "testGUID2");
+
+ // insert some visits
+ assertEquals(1, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(),
+ new ContentValues(), History.URL + " = ?", new String[] {"https://www.mozilla.org"}
+ ));
+ assertEquals(1, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(),
+ new ContentValues(), History.URL + " = ?", new String[] {"https://www.mozilla.org"}
+ ));
+ assertEquals(1, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(),
+ new ContentValues(), History.URL + " = ?", new String[] {"https://www.eff.org"}
+ ));
+
+ Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(3, cursor.getCount());
+ cursor.close();
+
+ // test that corresponding visit records are deleted if Sync isn't involved
+ assertEquals(1,
+ historyClient.delete(historyTestUri, History.URL + " = ?", new String[] {"https://www.mozilla.org"})
+ );
+
+ cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ cursor.close();
+
+ // test that corresponding visit records are deleted if Sync is involved
+ // insert some more visits
+ ContentValues moz = new ContentValues();
+ moz.put(History.URL, "https://www.mozilla.org");
+ moz.put(History.GUID, "testGUID3");
+ assertEquals(1, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true")
+ .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(),
+ moz, History.URL + " = ?", new String[] {"https://www.mozilla.org"}
+ ));
+ assertEquals(1, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true")
+ .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(),
+ new ContentValues(), History.URL + " = ?", new String[] {"https://www.eff.org"}
+ ));
+
+ assertEquals(1,
+ historyClient.delete(
+ historyTestUri.buildUpon().appendQueryParameter(BrowserContract.PARAM_IS_SYNC, "true").build(),
+ History.URL + " = ?", new String[] {"https://www.eff.org"})
+ );
+
+ cursor = visitsClient.query(visitsTestUri, new String[] {BrowserContract.Visits.HISTORY_GUID}, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+ assertEquals("testGUID3", cursor.getString(cursor.getColumnIndex(BrowserContract.Visits.HISTORY_GUID)));
+ cursor.close();
+ }
+
+ @Test
+ /**
+ * Test that changes to History GUID are cascaded to individual visits.
+ * See UPDATE CASCADED on Visit's HISTORY_GUID foreign key.
+ */
+ public void testHistoryGUIDUpdate() throws Exception {
+ insertHistoryItem("https://www.mozilla.org", "testGUID");
+ insertHistoryItem("https://www.eff.org", "testGUID2");
+
+ // insert some visits
+ assertEquals(1, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(),
+ new ContentValues(), History.URL + " = ?", new String[] {"https://www.mozilla.org"}
+ ));
+ assertEquals(1, historyClient.update(
+ historyTestUri.buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(),
+ new ContentValues(), History.URL + " = ?", new String[] {"https://www.mozilla.org"}
+ ));
+
+ // change testGUID -> testGUIDNew
+ ContentValues newGuid = new ContentValues();
+ newGuid.put(History.GUID, "testGUIDNew");
+ assertEquals(1, historyClient.update(
+ historyTestUri, newGuid, History.URL + " = ?", new String[] {"https://www.mozilla.org"}
+ ));
+
+ Cursor cursor = visitsClient.query(visitsTestUri, null, BrowserContract.Visits.HISTORY_GUID + " = ?", new String[] {"testGUIDNew"}, null);
+ assertNotNull(cursor);
+ assertEquals(2, cursor.getCount());
+ cursor.close();
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryVisitsTestBase.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryVisitsTestBase.java
new file mode 100644
index 0000000000..b8ee0bb362
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryVisitsTestBase.java
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.db;
+
+import android.content.ContentProviderClient;
+import android.content.ContentValues;
+import android.net.Uri;
+import android.os.RemoteException;
+
+import org.junit.After;
+import org.junit.Before;
+import org.mozilla.gecko.background.db.DelegatingTestContentProvider;
+import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
+import org.robolectric.shadows.ShadowContentResolver;
+
+import java.util.UUID;
+
+public class BrowserProviderHistoryVisitsTestBase {
+ /* package-private */ ShadowContentResolver contentResolver;
+ /* package-private */ ContentProviderClient historyClient;
+ /* package-private */ ContentProviderClient visitsClient;
+ /* package-private */ Uri historyTestUri;
+ /* package-private */ Uri visitsTestUri;
+
+ private BrowserProvider provider;
+
+ @Before
+ public void setUp() throws Exception {
+ provider = new BrowserProvider();
+ provider.onCreate();
+ ShadowContentResolver.registerProvider(BrowserContract.AUTHORITY, new DelegatingTestContentProvider(provider));
+
+ contentResolver = new ShadowContentResolver();
+ historyClient = contentResolver.acquireContentProviderClient(BrowserContractHelpers.HISTORY_CONTENT_URI);
+ visitsClient = contentResolver.acquireContentProviderClient(BrowserContractHelpers.VISITS_CONTENT_URI);
+
+ historyTestUri = testUri(BrowserContract.History.CONTENT_URI);
+ visitsTestUri = testUri(BrowserContract.Visits.CONTENT_URI);
+ }
+
+ @After
+ public void tearDown() {
+ historyClient.release();
+ visitsClient.release();
+ provider.shutdown();
+ }
+
+ /* package-private */ Uri testUri(Uri baseUri) {
+ return baseUri.buildUpon().appendQueryParameter(BrowserContract.PARAM_IS_TEST, "1").build();
+ }
+
+ /* package-private */ Uri insertHistoryItem(String url, String guid) throws RemoteException {
+ return insertHistoryItem(url, guid, System.currentTimeMillis(), null, null);
+ }
+
+ /* package-private */ Uri insertHistoryItem(String url, String guid, Long lastVisited, Integer visitCount) throws RemoteException {
+ return insertHistoryItem(url, guid, lastVisited, visitCount, null);
+ }
+
+ /* package-private */ Uri insertHistoryItem(String url, String guid, Long lastVisited, Integer visitCount, String title) throws RemoteException {
+ ContentValues historyItem = new ContentValues();
+ historyItem.put(BrowserContract.History.URL, url);
+ if (guid != null) {
+ historyItem.put(BrowserContract.History.GUID, guid);
+ }
+ if (visitCount != null) {
+ historyItem.put(BrowserContract.History.VISITS, visitCount);
+ }
+ historyItem.put(BrowserContract.History.DATE_LAST_VISITED, lastVisited);
+ if (title != null) {
+ historyItem.put(BrowserContract.History.TITLE, title);
+ }
+
+ return historyClient.insert(historyTestUri, historyItem);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderVisitsTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderVisitsTest.java
new file mode 100644
index 0000000000..928657e82a
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderVisitsTest.java
@@ -0,0 +1,301 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.db;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.db.BrowserContract.Visits;
+
+@RunWith(TestRunner.class)
+/**
+ * Testing direct interactions with visits through BrowserProvider
+ */
+public class BrowserProviderVisitsTest extends BrowserProviderHistoryVisitsTestBase {
+ @Test
+ /**
+ * Test that default visit parameters are set on insert.
+ */
+ public void testDefaultVisit() throws RemoteException {
+ String url = "https://www.mozilla.org";
+ String guid = "testGuid";
+
+ assertNotNull(insertHistoryItem(url, guid));
+
+ ContentValues visitItem = new ContentValues();
+ Long visitedDate = System.currentTimeMillis();
+ visitItem.put(Visits.HISTORY_GUID, guid);
+ visitItem.put(Visits.DATE_VISITED, visitedDate);
+ Uri insertedVisitUri = visitsClient.insert(visitsTestUri, visitItem);
+ assertNotNull(insertedVisitUri);
+
+ Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ try {
+ assertTrue(cursor.moveToFirst());
+ String insertedGuid = cursor.getString(cursor.getColumnIndex(Visits.HISTORY_GUID));
+ assertEquals(guid, insertedGuid);
+
+ Long insertedDate = cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED));
+ assertEquals(visitedDate, insertedDate);
+
+ Integer insertedType = cursor.getInt(cursor.getColumnIndex(Visits.VISIT_TYPE));
+ assertEquals(insertedType, Integer.valueOf(1));
+
+ Integer insertedIsLocal = cursor.getInt(cursor.getColumnIndex(Visits.IS_LOCAL));
+ assertEquals(insertedIsLocal, Integer.valueOf(1));
+ } finally {
+ cursor.close();
+ }
+ }
+
+ @Test
+ /**
+ * Test that we can't insert visit for non-existing GUID.
+ */
+ public void testMissingHistoryGuid() throws RemoteException {
+ ContentValues visitItem = new ContentValues();
+ visitItem.put(Visits.HISTORY_GUID, "blah");
+ visitItem.put(Visits.DATE_VISITED, System.currentTimeMillis());
+ assertNull(visitsClient.insert(visitsTestUri, visitItem));
+ }
+
+ @Test
+ /**
+ * Test that visit insert uses non-conflict insert.
+ */
+ public void testNonConflictInsert() throws RemoteException {
+ String url = "https://www.mozilla.org";
+ String guid = "testGuid";
+
+ assertNotNull(insertHistoryItem(url, guid));
+
+ ContentValues visitItem = new ContentValues();
+ Long visitedDate = System.currentTimeMillis();
+ visitItem.put(Visits.HISTORY_GUID, guid);
+ visitItem.put(Visits.DATE_VISITED, visitedDate);
+ Uri insertedVisitUri = visitsClient.insert(visitsTestUri, visitItem);
+ assertNotNull(insertedVisitUri);
+
+ Uri insertedVisitUri2 = visitsClient.insert(visitsTestUri, visitItem);
+ assertEquals(insertedVisitUri, insertedVisitUri2);
+ }
+
+ @Test
+ /**
+ * Test that non-default visit parameters won't get overridden.
+ */
+ public void testNonDefaultInsert() throws RemoteException {
+ assertNotNull(insertHistoryItem("https://www.mozilla.org", "testGuid"));
+
+ Integer typeToInsert = 5;
+ Integer isLocalToInsert = 0;
+
+ ContentValues visitItem = new ContentValues();
+ visitItem.put(Visits.HISTORY_GUID, "testGuid");
+ visitItem.put(Visits.DATE_VISITED, System.currentTimeMillis());
+ visitItem.put(Visits.VISIT_TYPE, typeToInsert);
+ visitItem.put(Visits.IS_LOCAL, isLocalToInsert);
+
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem));
+
+ Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ try {
+ assertTrue(cursor.moveToFirst());
+
+ Integer insertedVisitType = cursor.getInt(cursor.getColumnIndex(Visits.VISIT_TYPE));
+ assertEquals(typeToInsert, insertedVisitType);
+
+ Integer insertedIsLocal = cursor.getInt(cursor.getColumnIndex(Visits.IS_LOCAL));
+ assertEquals(isLocalToInsert, insertedIsLocal);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ @Test
+ /**
+ * Test that default sorting order (DATE_VISITED DESC) is set if we don't specify any sorting params
+ */
+ public void testDefaultSortingOrder() throws RemoteException {
+ assertNotNull(insertHistoryItem("https://www.mozilla.org", "testGuid"));
+
+ Long time1 = System.currentTimeMillis();
+ Long time2 = time1 + 100;
+ Long time3 = time1 + 200;
+
+ ContentValues visitItem = new ContentValues();
+ visitItem.put(Visits.DATE_VISITED, time1);
+ visitItem.put(Visits.HISTORY_GUID, "testGuid");
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem));
+
+ visitItem.put(Visits.DATE_VISITED, time3);
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem));
+
+ visitItem.put(Visits.DATE_VISITED, time2);
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem));
+
+ Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ try {
+ assertEquals(3, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+
+ Long timeInserted = cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED));
+ assertEquals(time3, timeInserted);
+
+ cursor.moveToNext();
+
+ timeInserted = cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED));
+ assertEquals(time2, timeInserted);
+
+ cursor.moveToNext();
+
+ timeInserted = cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED));
+ assertEquals(time1, timeInserted);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ @Test
+ /**
+ * Test that if we pass sorting params, they're not overridden
+ */
+ public void testNonDefaultSortingOrder() throws RemoteException {
+ assertNotNull(insertHistoryItem("https://www.mozilla.org", "testGuid"));
+
+ Long time1 = System.currentTimeMillis();
+ Long time2 = time1 + 100;
+ Long time3 = time1 + 200;
+
+ ContentValues visitItem = new ContentValues();
+ visitItem.put(Visits.DATE_VISITED, time1);
+ visitItem.put(Visits.HISTORY_GUID, "testGuid");
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem));
+
+ visitItem.put(Visits.DATE_VISITED, time3);
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem));
+
+ visitItem.put(Visits.DATE_VISITED, time2);
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem));
+
+ Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, Visits.DATE_VISITED + " ASC");
+ assertNotNull(cursor);
+ assertEquals(3, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+
+ Long timeInserted = cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED));
+ assertEquals(time1, timeInserted);
+
+ cursor.moveToNext();
+
+ timeInserted = cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED));
+ assertEquals(time2, timeInserted);
+
+ cursor.moveToNext();
+
+ timeInserted = cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED));
+ assertEquals(time3, timeInserted);
+
+ cursor.close();
+ }
+
+ @Test
+ /**
+ * Tests deletion of all visits, and by some selection (GUID, IS_LOCAL)
+ */
+ public void testVisitDeletion() throws RemoteException {
+ assertNotNull(insertHistoryItem("https://www.mozilla.org", "testGuid"));
+ assertNotNull(insertHistoryItem("https://www.eff.org", "testGuid2"));
+
+ Long time1 = System.currentTimeMillis();
+
+ ContentValues visitItem = new ContentValues();
+ visitItem.put(Visits.DATE_VISITED, time1);
+ visitItem.put(Visits.HISTORY_GUID, "testGuid");
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem));
+
+ visitItem = new ContentValues();
+ visitItem.put(Visits.DATE_VISITED, time1 + 100);
+ visitItem.put(Visits.HISTORY_GUID, "testGuid");
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem));
+
+ ContentValues visitItem2 = new ContentValues();
+ visitItem2.put(Visits.DATE_VISITED, time1);
+ visitItem2.put(Visits.HISTORY_GUID, "testGuid2");
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem2));
+
+ Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(3, cursor.getCount());
+ cursor.close();
+
+ assertEquals(3, visitsClient.delete(visitsTestUri, null, null));
+
+ cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(0, cursor.getCount());
+ cursor.close();
+
+ // test selective deletion - by IS_LOCAL
+ visitItem = new ContentValues();
+ visitItem.put(Visits.DATE_VISITED, time1);
+ visitItem.put(Visits.HISTORY_GUID, "testGuid");
+ visitItem.put(Visits.IS_LOCAL, 0);
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem));
+
+ visitItem = new ContentValues();
+ visitItem.put(Visits.DATE_VISITED, time1 + 100);
+ visitItem.put(Visits.HISTORY_GUID, "testGuid");
+ visitItem.put(Visits.IS_LOCAL, 1);
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem));
+
+ visitItem2 = new ContentValues();
+ visitItem2.put(Visits.DATE_VISITED, time1);
+ visitItem2.put(Visits.HISTORY_GUID, "testGuid2");
+ visitItem2.put(Visits.IS_LOCAL, 0);
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem2));
+
+ cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(3, cursor.getCount());
+ cursor.close();
+
+ assertEquals(2,
+ visitsClient.delete(visitsTestUri, Visits.IS_LOCAL + " = ?", new String[]{"0"}));
+ cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+ assertEquals(time1 + 100, cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED)));
+ assertEquals("testGuid", cursor.getString(cursor.getColumnIndex(Visits.HISTORY_GUID)));
+ assertEquals(1, cursor.getInt(cursor.getColumnIndex(Visits.IS_LOCAL)));
+ cursor.close();
+
+ // test selective deletion - by HISTORY_GUID
+ assertNotNull(visitsClient.insert(visitsTestUri, visitItem2));
+ cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(2, cursor.getCount());
+ cursor.close();
+
+ assertEquals(1,
+ visitsClient.delete(visitsTestUri, Visits.HISTORY_GUID + " = ?", new String[]{"testGuid"}));
+ cursor = visitsClient.query(visitsTestUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+ assertEquals("testGuid2", cursor.getString(cursor.getColumnIndex(Visits.HISTORY_GUID)));
+ cursor.close();
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/distribution/TestReferrerDescriptor.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/distribution/TestReferrerDescriptor.java
new file mode 100644
index 0000000000..9e553cf442
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/distribution/TestReferrerDescriptor.java
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+@RunWith(TestRunner.class)
+public class TestReferrerDescriptor {
+ @Test
+ public void testReferrerDescriptor() {
+ String referrerString1 = "utm_source%3Dsource%26utm_content%3Dcontent%26utm_campaign%3Dcampaign%26utm_medium%3Dmedium%26utm_term%3Dterm";
+ String referrerString2 = "utm_source=source&utm_content=content&utm_campaign=campaign&utm_medium=medium&utm_term=term";
+ ReferrerDescriptor referrer1 = new ReferrerDescriptor(referrerString1);
+ Assert.assertNotNull(referrer1);
+ Assert.assertEquals(referrer1.source, "source");
+ Assert.assertEquals(referrer1.content, "content");
+ Assert.assertEquals(referrer1.campaign, "campaign");
+ Assert.assertEquals(referrer1.medium, "medium");
+ Assert.assertEquals(referrer1.term, "term");
+ ReferrerDescriptor referrer2 = new ReferrerDescriptor(referrerString2);
+ Assert.assertNotNull(referrer2);
+ Assert.assertEquals(referrer2.source, "source");
+ Assert.assertEquals(referrer2.content, "content");
+ Assert.assertEquals(referrer2.campaign, "campaign");
+ Assert.assertEquals(referrer2.medium, "medium");
+ Assert.assertEquals(referrer2.term, "term");
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestDownloadAction.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestDownloadAction.java
new file mode 100644
index 0000000000..2252c90c83
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestDownloadAction.java
@@ -0,0 +1,607 @@
+/* -*- 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.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.dlc.catalog.DownloadContent;
+import org.mozilla.gecko.dlc.catalog.DownloadContentBuilder;
+import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog;
+import org.robolectric.RuntimeEnvironment;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * DownloadAction: Download content that has been scheduled during "study" or "verify".
+ */
+@RunWith(TestRunner.class)
+public class TestDownloadAction {
+ private static final String TEST_URL = "http://example.org";
+
+ private static final int STATUS_OK = 200;
+ private static final int STATUS_PARTIAL_CONTENT = 206;
+
+ /**
+ * Scenario: The current network is metered.
+ *
+ * Verify that:
+ * * No download is performed on a metered network
+ */
+ @Test
+ public void testNothingIsDoneOnMeteredNetwork() throws Exception {
+ DownloadAction action = spy(new DownloadAction(null));
+ doReturn(true).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
+
+ action.perform(RuntimeEnvironment.application, null);
+
+ verify(action, never()).buildHttpURLConnection(anyString());
+ verify(action, never()).download(anyString(), any(File.class));
+ }
+
+ /**
+ * Scenario: No (connected) network is available.
+ *
+ * Verify that:
+ * * No download is performed
+ */
+ @Test
+ public void testNothingIsDoneIfNoNetworkIsAvailable() throws Exception {
+ DownloadAction action = spy(new DownloadAction(null));
+ doReturn(false).when(action).isConnectedToNetwork(RuntimeEnvironment.application);
+
+ action.perform(RuntimeEnvironment.application, null);
+
+ verify(action, never()).isActiveNetworkMetered(any(Context.class));
+ verify(action, never()).buildHttpURLConnection(anyString());
+ verify(action, never()).download(anyString(), any(File.class));
+ }
+
+ /**
+ * Scenario: Content is scheduled for download but already exists locally (with correct checksum).
+ *
+ * Verify that:
+ * * No download is performed for existing file
+ * * Content is marked as downloaded in the catalog
+ */
+ @Test
+ public void testExistingAndVerifiedFilesAreNotDownloadedAgain() throws Exception {
+ DownloadContent content = new DownloadContentBuilder().build();
+
+ DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
+ doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads();
+
+ DownloadAction action = spy(new DownloadAction(null));
+ doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
+
+ File file = mock(File.class);
+ doReturn(true).when(file).exists();
+ doReturn(file).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
+ doReturn(file).when(action).getDestinationFile(RuntimeEnvironment.application, content);
+ doReturn(true).when(action).verify(eq(file), anyString());
+
+ action.perform(RuntimeEnvironment.application, catalog);
+
+ verify(action, never()).download(anyString(), any(File.class));
+ verify(catalog).markAsDownloaded(content);
+ }
+
+ /**
+ * Scenario: Server returns a server error (HTTP 500).
+ *
+ * Verify that:
+ * * Situation is treated as recoverable (RecoverableDownloadContentException)
+ */
+ @Test(expected=BaseAction.RecoverableDownloadContentException.class)
+ public void testServerErrorsAreRecoverable() throws Exception {
+ HttpURLConnection connection = mockHttpURLConnection(500, "");
+
+ File temporaryFile = mock(File.class);
+ doReturn(false).when(temporaryFile).exists();
+
+ DownloadAction action = spy(new DownloadAction(null));
+ doReturn(connection).when(action).buildHttpURLConnection(anyString());
+ action.download(TEST_URL, temporaryFile);
+
+ verify(connection).getInputStream();
+ }
+
+ /**
+ * Scenario: Server returns a client error (HTTP 404).
+ *
+ * Verify that:
+ * * Situation is treated as unrecoverable (UnrecoverableDownloadContentException)
+ */
+ @Test(expected=BaseAction.UnrecoverableDownloadContentException.class)
+ public void testClientErrorsAreUnrecoverable() throws Exception {
+ HttpURLConnection connection = mockHttpURLConnection(404, "");
+
+ File temporaryFile = mock(File.class);
+ doReturn(false).when(temporaryFile).exists();
+
+ DownloadAction action = spy(new DownloadAction(null));
+ doReturn(connection).when(action).buildHttpURLConnection(anyString());
+ action.download(TEST_URL, temporaryFile);
+
+ verify(connection).getInputStream();
+ }
+
+ /**
+ * Scenario: A successful download has been performed.
+ *
+ * Verify that:
+ * * The content will be extracted to the destination
+ * * The content is marked as downloaded in the catalog
+ */
+ @Test
+ public void testSuccessfulDownloadsAreMarkedAsDownloaded() throws Exception {
+ DownloadContent content = new DownloadContentBuilder()
+ .setKind(DownloadContent.KIND_FONT)
+ .setType(DownloadContent.TYPE_ASSET_ARCHIVE)
+ .build();
+
+ DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
+ doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads();
+
+ DownloadAction action = spy(new DownloadAction(null));
+ doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
+
+ File file = mockNotExistingFile();
+ doReturn(file).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
+ doReturn(file).when(action).getDestinationFile(RuntimeEnvironment.application, content);
+
+ doReturn(false).when(action).verify(eq(file), anyString());
+ doNothing().when(action).download(anyString(), eq(file));
+ doReturn(true).when(action).verify(eq(file), anyString());
+ doNothing().when(action).extract(eq(file), eq(file), anyString());
+
+ action.perform(RuntimeEnvironment.application, catalog);
+
+ verify(action).download(anyString(), eq(file));
+ verify(action).extract(eq(file), eq(file), anyString());
+ verify(catalog).markAsDownloaded(content);
+ }
+
+ /**
+ * Scenario: Pretend a partially downloaded file already exists.
+ *
+ * Verify that:
+ * * Range header is set in request
+ * * Content will be appended to existing file
+ * * Content will be marked as downloaded in catalog
+ */
+ @Test
+ public void testResumingDownloadFromExistingFile() throws Exception {
+ DownloadContent content = new DownloadContentBuilder()
+ .setKind(DownloadContent.KIND_FONT)
+ .setType(DownloadContent.TYPE_ASSET_ARCHIVE)
+ .setSize(4223)
+ .build();
+
+ DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
+ doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads();
+
+ DownloadAction action = spy(new DownloadAction(null));
+ doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
+
+ File temporaryFile = mockFileWithSize(1337L);
+ doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
+
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ doReturn(outputStream).when(action).openFile(eq(temporaryFile), anyBoolean());
+
+ HttpURLConnection connection = mockHttpURLConnection(STATUS_PARTIAL_CONTENT, "HelloWorld");
+ doReturn(connection).when(action).buildHttpURLConnection(anyString());
+
+ File destinationFile = mockNotExistingFile();
+ doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content);
+
+ doReturn(true).when(action).verify(eq(temporaryFile), anyString());
+ doNothing().when(action).extract(eq(temporaryFile), eq(destinationFile), anyString());
+
+ action.perform(RuntimeEnvironment.application, catalog);
+
+ verify(connection).getInputStream();
+ verify(connection).setRequestProperty("Range", "bytes=1337-");
+
+ Assert.assertEquals("HelloWorld", new String(outputStream.toByteArray(), "UTF-8"));
+
+ verify(action).openFile(eq(temporaryFile), eq(true));
+ verify(catalog).markAsDownloaded(content);
+ verify(temporaryFile).delete();
+ }
+
+ /**
+ * Scenario: Download fails with IOException.
+ *
+ * Verify that:
+ * * Partially downloaded file will not be deleted
+ * * Content will not be marked as downloaded in catalog
+ */
+ @Test
+ public void testTemporaryFileIsNotDeletedAfterDownloadAborted() throws Exception {
+ DownloadContent content = new DownloadContentBuilder()
+ .setKind(DownloadContent.KIND_FONT)
+ .setType(DownloadContent.TYPE_ASSET_ARCHIVE)
+ .setSize(4223)
+ .build();
+
+ DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
+ doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads();
+
+ DownloadAction action = spy(new DownloadAction(null));
+ doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
+
+ File temporaryFile = mockFileWithSize(1337L);
+ doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
+
+ ByteArrayOutputStream outputStream = spy(new ByteArrayOutputStream());
+ doReturn(outputStream).when(action).openFile(eq(temporaryFile), anyBoolean());
+ doThrow(IOException.class).when(outputStream).write(any(byte[].class), anyInt(), anyInt());
+
+ HttpURLConnection connection = mockHttpURLConnection(STATUS_PARTIAL_CONTENT, "HelloWorld");
+ doReturn(connection).when(action).buildHttpURLConnection(anyString());
+
+ doReturn(mockNotExistingFile()).when(action).getDestinationFile(RuntimeEnvironment.application, content);
+
+ action.perform(RuntimeEnvironment.application, catalog);
+
+ verify(catalog, never()).markAsDownloaded(content);
+ verify(action, never()).verify(any(File.class), anyString());
+ verify(temporaryFile, never()).delete();
+ }
+
+ /**
+ * Scenario: Partially downloaded file is already complete.
+ *
+ * Verify that:
+ * * No download request is made
+ * * File is treated as completed and will be verified and extracted
+ * * Content is marked as downloaded in catalog
+ */
+ @Test
+ public void testNoRequestIsSentIfFileIsAlreadyComplete() throws Exception {
+ DownloadContent content = new DownloadContentBuilder()
+ .setKind(DownloadContent.KIND_FONT)
+ .setType(DownloadContent.TYPE_ASSET_ARCHIVE)
+ .setSize(1337L)
+ .build();
+
+ DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
+ doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads();
+
+ DownloadAction action = spy(new DownloadAction(null));
+ doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
+
+ File temporaryFile = mockFileWithSize(1337L);
+ doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
+
+ File destinationFile = mockNotExistingFile();
+ doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content);
+
+ doReturn(true).when(action).verify(eq(temporaryFile), anyString());
+ doNothing().when(action).extract(eq(temporaryFile), eq(destinationFile), anyString());
+
+ action.perform(RuntimeEnvironment.application, catalog);
+
+ verify(action, never()).download(anyString(), eq(temporaryFile));
+ verify(action).verify(eq(temporaryFile), anyString());
+ verify(action).extract(eq(temporaryFile), eq(destinationFile), anyString());
+ verify(catalog).markAsDownloaded(content);
+ }
+
+ /**
+ * Scenario: Download is completed but verification (checksum) failed.
+ *
+ * Verify that:
+ * * Downloaded file is deleted
+ * * File will not be extracted
+ * * Content is not marked as downloaded in the catalog
+ */
+ @Test
+ public void testTemporaryFileWillBeDeletedIfVerificationFails() throws Exception {
+ DownloadContent content = new DownloadContentBuilder()
+ .setKind(DownloadContent.KIND_FONT)
+ .setType(DownloadContent.TYPE_ASSET_ARCHIVE)
+ .setSize(1337L)
+ .build();
+
+ DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
+ doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads();
+
+ DownloadAction action = spy(new DownloadAction(null));
+ doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
+ doNothing().when(action).download(anyString(), any(File.class));
+ doReturn(false).when(action).verify(any(File.class), anyString());
+
+ File temporaryFile = mockNotExistingFile();
+ doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
+
+ File destinationFile = mockNotExistingFile();
+ doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content);
+
+ action.perform(RuntimeEnvironment.application, catalog);
+
+ verify(temporaryFile).delete();
+ verify(action, never()).extract(any(File.class), any(File.class), anyString());
+ verify(catalog, never()).markAsDownloaded(content);
+ }
+
+ /**
+ * Scenario: Not enough storage space for content is available.
+ *
+ * Verify that:
+ * * No download will per performed
+ */
+ @Test
+ public void testNoDownloadIsPerformedIfNotEnoughStorageIsAvailable() throws Exception {
+ DownloadContent content = createFontWithSize(1337L);
+ DownloadContentCatalog catalog = mockCatalogWithScheduledDownloads(content);
+
+ DownloadAction action = spy(new DownloadAction(null));
+ doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
+ doReturn(true).when(action).isConnectedToNetwork(RuntimeEnvironment.application);
+
+ File temporaryFile = mockNotExistingFile();
+ doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
+
+ File destinationFile = mockNotExistingFile();
+ doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content);
+
+ doReturn(true).when(action).hasEnoughDiskSpace(content, destinationFile, temporaryFile);
+
+ verify(action, never()).buildHttpURLConnection(anyString());
+ verify(action, never()).download(anyString(), any(File.class));
+ verify(action, never()).verify(any(File.class), anyString());
+ verify(catalog, never()).markAsDownloaded(content);
+ }
+
+ /**
+ * Scenario: Not enough storage space for temporary file available.
+ *
+ * Verify that:
+ * * hasEnoughDiskSpace() returns false
+ */
+ @Test
+ public void testWithNotEnoughSpaceForTemporaryFile() throws Exception{
+ DownloadContent content = createFontWithSize(2048);
+ File destinationFile = mockNotExistingFile();
+ File temporaryFile = mockNotExistingFileWithUsableSpace(1024);
+
+ DownloadAction action = new DownloadAction(null);
+ Assert.assertFalse(action.hasEnoughDiskSpace(content, destinationFile, temporaryFile));
+ }
+
+ /**
+ * Scenario: Not enough storage space for destination file available.
+ *
+ * Verify that:
+ * * hasEnoughDiskSpace() returns false
+ */
+ @Test
+ public void testWithNotEnoughSpaceForDestinationFile() throws Exception {
+ DownloadContent content = createFontWithSize(2048);
+ File destinationFile = mockNotExistingFileWithUsableSpace(1024);
+ File temporaryFile = mockNotExistingFile();
+
+ DownloadAction action = new DownloadAction(null);
+ Assert.assertFalse(action.hasEnoughDiskSpace(content, destinationFile, temporaryFile));
+ }
+
+ /**
+ * Scenario: Enough storage space for temporary and destination file available.
+ *
+ * Verify that:
+ * * hasEnoughDiskSpace() returns true
+ */
+ @Test
+ public void testWithEnoughSpaceForEverything() throws Exception {
+ DownloadContent content = createFontWithSize(2048);
+ File destinationFile = mockNotExistingFileWithUsableSpace(4096);
+ File temporaryFile = mockNotExistingFileWithUsableSpace(4096);
+
+ DownloadAction action = new DownloadAction(null);
+ Assert.assertTrue(action.hasEnoughDiskSpace(content, destinationFile, temporaryFile));
+ }
+
+ /**
+ * Scenario: Download failed with network I/O error.
+ *
+ * Verify that:
+ * * Error is not counted as failure
+ */
+ @Test
+ public void testNetworkErrorIsNotCountedAsFailure() throws Exception {
+ DownloadContent content = createFont();
+ DownloadContentCatalog catalog = mockCatalogWithScheduledDownloads(content);
+
+ DownloadAction action = spy(new DownloadAction(null));
+ doReturn(true).when(action).isConnectedToNetwork(RuntimeEnvironment.application);
+ doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
+ doReturn(mockNotExistingFile()).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
+ doReturn(mockNotExistingFile()).when(action).getDestinationFile(RuntimeEnvironment.application, content);
+ doReturn(true).when(action).hasEnoughDiskSpace(eq(content), any(File.class), any(File.class));
+
+ HttpURLConnection connection = mockHttpURLConnection(STATUS_OK, "");
+ doThrow(IOException.class).when(connection).getInputStream();
+ doReturn(connection).when(action).buildHttpURLConnection(anyString());
+
+ action.perform(RuntimeEnvironment.application, catalog);
+
+ verify(catalog, never()).rememberFailure(eq(content), anyInt());
+ verify(catalog, never()).markAsDownloaded(content);
+ }
+
+ /**
+ * Scenario: Disk IO Error when extracting file.
+ *
+ * Verify that:
+ * * Error is counted as failure
+ * * After multiple errors the content is marked as permanently failed
+ */
+ @Test
+ public void testDiskIOErrorIsCountedAsFailure() throws Exception {
+ DownloadContent content = createFont();
+ DownloadContentCatalog catalog = mockCatalogWithScheduledDownloads(content);
+ doCallRealMethod().when(catalog).rememberFailure(eq(content), anyInt());
+ doCallRealMethod().when(catalog).markAsPermanentlyFailed(content);
+
+ Assert.assertEquals(DownloadContent.STATE_NONE, content.getState());
+
+ DownloadAction action = spy(new DownloadAction(null));
+ doReturn(true).when(action).isConnectedToNetwork(RuntimeEnvironment.application);
+ doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
+ doReturn(mockNotExistingFile()).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
+ doReturn(mockNotExistingFile()).when(action).getDestinationFile(RuntimeEnvironment.application, content);
+ doReturn(true).when(action).hasEnoughDiskSpace(eq(content), any(File.class), any(File.class));
+ doNothing().when(action).download(anyString(), any(File.class));
+ doReturn(true).when(action).verify(any(File.class), anyString());
+
+ File destinationFile = mock(File.class);
+ doReturn(false).when(destinationFile).exists();
+ File parentFile = mock(File.class);
+ doReturn(false).when(parentFile).mkdirs();
+ doReturn(false).when(parentFile).exists();
+ doReturn(parentFile).when(destinationFile).getParentFile();
+ doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content);
+
+ for (int i = 0; i < 10; i++) {
+ action.perform(RuntimeEnvironment.application, catalog);
+
+ Assert.assertEquals(DownloadContent.STATE_NONE, content.getState());
+ }
+
+ action.perform(RuntimeEnvironment.application, catalog);
+
+ Assert.assertEquals(DownloadContent.STATE_FAILED, content.getState());
+ verify(catalog, times(11)).rememberFailure(eq(content), anyInt());
+ }
+
+ /**
+ * Scenario: If the file to be downloaded is of kind - "hyphenation"
+ *
+ * Verify that:
+ * * isHyphenationDictionary returns true for a download content with kind "hyphenation"
+ * * isHyphenationDictionary returns false for a download content with unknown/different kind like "Font"
+ */
+ @Test
+ public void testIsHyphenationDictionary() throws Exception {
+ DownloadContent hyphenationContent = createHyphenationDictionary();
+ Assert.assertTrue(hyphenationContent.isHyphenationDictionary());
+ DownloadContent fontContent = createFont();
+ Assert.assertFalse(fontContent.isHyphenationDictionary());
+ DownloadContent unknownContent = createUnknownContent(1024L);
+ Assert.assertFalse(unknownContent.isHyphenationDictionary());
+ }
+
+ /**
+ * Scenario: If the content to be downloaded is known
+ *
+ * Verify that:
+ * * isKnownContent returns true for a downloadable content with a known kind and type.
+ * * isKnownContent returns false for a downloadable content with unknown kind and type.
+ */
+ @Test
+ public void testIsKnownContent() throws Exception {
+ DownloadContent fontContent = createFontWithSize(1024L);
+ DownloadContent hyphenationContent = createHyphenationDictionaryWithSize(1024L);
+ DownloadContent unknownContent = createUnknownContent(1024L);
+ DownloadContent contentWithUnknownType = createContentWithoutType(1024L);
+
+ Assert.assertTrue(fontContent.isKnownContent());
+ Assert.assertTrue(hyphenationContent.isKnownContent());
+ Assert.assertFalse(unknownContent.isKnownContent());
+ Assert.assertFalse(contentWithUnknownType.isKnownContent());
+ }
+
+ private DownloadContent createUnknownContent(long size) {
+ return new DownloadContentBuilder()
+ .setSize(size)
+ .build();
+ }
+
+ private DownloadContent createContentWithoutType(long size) {
+ return new DownloadContentBuilder()
+ .setKind(DownloadContent.KIND_HYPHENATION_DICTIONARY)
+ .setSize(size)
+ .build();
+ }
+
+ private DownloadContent createFont() {
+ return createFontWithSize(102400L);
+ }
+
+ private DownloadContent createFontWithSize(long size) {
+ return new DownloadContentBuilder()
+ .setKind(DownloadContent.KIND_FONT)
+ .setType(DownloadContent.TYPE_ASSET_ARCHIVE)
+ .setSize(size)
+ .build();
+ }
+
+ private DownloadContent createHyphenationDictionary() {
+ return createHyphenationDictionaryWithSize(102400L);
+ }
+
+ private DownloadContent createHyphenationDictionaryWithSize(long size) {
+ return new DownloadContentBuilder()
+ .setKind(DownloadContent.KIND_HYPHENATION_DICTIONARY)
+ .setType(DownloadContent.TYPE_ASSET_ARCHIVE)
+ .setSize(size)
+ .build();
+ }
+
+ private DownloadContentCatalog mockCatalogWithScheduledDownloads(DownloadContent... content) {
+ DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
+ doReturn(Arrays.asList(content)).when(catalog).getScheduledDownloads();
+ return catalog;
+ }
+
+ private static File mockNotExistingFile() {
+ return mockFileWithUsableSpace(false, 0, Long.MAX_VALUE);
+ }
+
+ private static File mockNotExistingFileWithUsableSpace(long usableSpace) {
+ return mockFileWithUsableSpace(false, 0, usableSpace);
+ }
+
+ private static File mockFileWithSize(long length) {
+ return mockFileWithUsableSpace(true, length, Long.MAX_VALUE);
+ }
+
+ private static File mockFileWithUsableSpace(boolean exists, long length, long usableSpace) {
+ File file = mock(File.class);
+ doReturn(exists).when(file).exists();
+ doReturn(length).when(file).length();
+
+ File parentFile = mock(File.class);
+ doReturn(usableSpace).when(parentFile).getUsableSpace();
+ doReturn(parentFile).when(file).getParentFile();
+
+ return file;
+ }
+
+ private static HttpURLConnection mockHttpURLConnection(int statusCode, String content) throws Exception {
+ HttpURLConnection connection = mock(HttpURLConnection.class);
+
+ doReturn(statusCode).when(connection).getResponseCode();
+ doReturn(new ByteArrayInputStream(content.getBytes("UTF-8"))).when(connection).getInputStream();
+
+ return connection;
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestStudyAction.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestStudyAction.java
new file mode 100644
index 0000000000..6b2ce83df5
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestStudyAction.java
@@ -0,0 +1,119 @@
+/* -*- 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.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.dlc.catalog.DownloadContent;
+import org.mozilla.gecko.dlc.catalog.DownloadContentBuilder;
+import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * StudyAction: Scan the catalog for "new" content available for download.
+ */
+@RunWith(TestRunner.class)
+public class TestStudyAction {
+ /**
+ * Scenario: Catalog is empty.
+ *
+ * Verify that:
+ * * No download is scheduled
+ * * Download action is not started
+ */
+ @Test
+ public void testPerformWithEmptyCatalog() {
+ DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
+ when(catalog.getContentToStudy()).thenReturn(new ArrayList<DownloadContent>());
+
+ StudyAction action = spy(new StudyAction());
+ action.perform(RuntimeEnvironment.application, catalog);
+
+ verify(catalog).getContentToStudy();
+ verify(catalog, never()).markAsDownloaded(any(DownloadContent.class));
+ verify(action, never()).startDownloads(any(Context.class));
+ }
+
+ /**
+ * Scenario: Catalog contains two items that have not been downloaded yet.
+ *
+ * Verify that:
+ * * Both items are scheduled to be downloaded
+ */
+ @Test
+ public void testPerformWithNewContent() {
+ DownloadContent content1 = new DownloadContentBuilder()
+ .setType(DownloadContent.TYPE_ASSET_ARCHIVE)
+ .setKind(DownloadContent.KIND_FONT)
+ .build();
+ DownloadContent content2 = new DownloadContentBuilder()
+ .setType(DownloadContent.TYPE_ASSET_ARCHIVE)
+ .setKind(DownloadContent.KIND_FONT)
+ .build();
+
+ DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
+ when(catalog.getContentToStudy()).thenReturn(Arrays.asList(content1, content2));
+
+ StudyAction action = spy(new StudyAction());
+ action.perform(RuntimeEnvironment.application, catalog);
+
+ verify(catalog).scheduleDownload(content1);
+ verify(catalog).scheduleDownload(content2);
+ }
+
+ /**
+ * Scenario: Catalog contains item that are scheduled for download.
+ *
+ * Verify that:
+ * * Download action is started
+ */
+ @Test
+ public void testStartingDownloadsAfterScheduling() {
+ DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
+ when(catalog.hasScheduledDownloads()).thenReturn(true);
+
+ StudyAction action = spy(new StudyAction());
+ action.perform(RuntimeEnvironment.application, catalog);
+
+ verify(action).startDownloads(any(Context.class));
+ }
+
+ /**
+ * Scenario: Catalog contains unknown content.
+ *
+ * Verify that:
+ * * Unknown content is not scheduled for download.
+ */
+ @Test
+ public void testPerformWithUnknownContent() {
+ DownloadContent content = new DownloadContentBuilder()
+ .setType("Unknown-Type")
+ .setKind("Unknown-Kind")
+ .build();
+
+ DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
+ when(catalog.getContentToStudy()).thenReturn(Collections.singletonList(content));
+
+ StudyAction action = spy(new StudyAction());
+ action.perform(RuntimeEnvironment.application, catalog);
+
+ verify(catalog, never()).scheduleDownload(content);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestSyncAction.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestSyncAction.java
new file mode 100644
index 0000000000..1e494975ed
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestSyncAction.java
@@ -0,0 +1,276 @@
+/* -*- 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.v4.util.ArrayMap;
+import android.support.v4.util.AtomicFile;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+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.util.IOUtils;
+import org.robolectric.RuntimeEnvironment;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.List;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyLong;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+/**
+ * SyncAction: Synchronize catalog from a (mocked) Kinto instance.
+ */
+@RunWith(TestRunner.class)
+public class TestSyncAction {
+ /**
+ * Scenario: The server returns an empty record set.
+ */
+ @Test
+ public void testEmptyResult() throws Exception {
+ SyncAction action = spy(new SyncAction());
+ doReturn(true).when(action).isSyncEnabledForClient(RuntimeEnvironment.application);
+ doReturn(new JSONArray()).when(action).fetchRawCatalog(anyLong());
+
+ action.perform(RuntimeEnvironment.application, mockCatalog());
+
+ verify(action, never()).createContent(anyCatalog(), anyJSONObject());
+ verify(action, never()).updateContent(anyCatalog(), anyJSONObject(), anyContent());
+ verify(action, never()).deleteContent(anyCatalog(), anyString());
+
+ verify(action, never()).startStudyAction(anyContext());
+ }
+
+ /**
+ * Scenario: The server returns an item that is not in the catalog yet.
+ */
+ @Test
+ public void testAddingNewContent() throws Exception {
+ SyncAction action = spy(new SyncAction());
+ doReturn(true).when(action).isSyncEnabledForClient(RuntimeEnvironment.application);
+ doReturn(fromFile("dlc_sync_single_font.json")).when(action).fetchRawCatalog(anyLong());
+
+ DownloadContentCatalog catalog = mockCatalog();
+
+ action.perform(RuntimeEnvironment.application, catalog);
+
+ // A new content item has been created
+ verify(action).createContent(anyCatalog(), anyJSONObject());
+
+ // No content item has been updated or deleted
+ verify(action, never()).updateContent(anyCatalog(), anyJSONObject(), anyContent());
+ verify(action, never()).deleteContent(anyCatalog(), anyString());
+
+ // A new item has been added to the catalog
+ ArgumentCaptor<DownloadContent> captor = ArgumentCaptor.forClass(DownloadContent.class);
+ verify(catalog).add(captor.capture());
+
+ // The item matches the values from the server response
+ DownloadContent content = captor.getValue();
+ Assert.assertEquals("c906275c-3747-fe27-426f-6187526a6f06", content.getId());
+ Assert.assertEquals("4ed509317f1bb441b185ea13bf1c9d19d1a0b396962efa3b5dc3190ad88f2067", content.getChecksum());
+ Assert.assertEquals("960be4fc5a92c1dc488582b215d5d75429fd4ffbee463105d29992cd792a912e", content.getDownloadChecksum());
+ Assert.assertEquals("CharisSILCompact-R.ttf", content.getFilename());
+ Assert.assertEquals(DownloadContent.KIND_FONT, content.getKind());
+ Assert.assertEquals("/attachments/0d28a72d-a51f-46f8-9e5a-f95c61de904e.gz", content.getLocation());
+ Assert.assertEquals(DownloadContent.TYPE_ASSET_ARCHIVE, content.getType());
+ Assert.assertEquals(1455710632607L, content.getLastModified());
+ Assert.assertEquals(1727656L, content.getSize());
+ Assert.assertEquals(DownloadContent.STATE_NONE, content.getState());
+ }
+
+ /**
+ * Scenario: The catalog is using the old format, we want to make sure we abort cleanly.
+ */
+ @Test
+ public void testUpdatingWithOldCatalog() throws Exception{
+ SyncAction action = spy(new SyncAction());
+ doReturn(true).when(action).isSyncEnabledForClient(RuntimeEnvironment.application);
+ doReturn(fromFile("dlc_sync_old_format.json")).when(action).fetchRawCatalog(anyLong());
+
+ DownloadContent existingContent = createTestContent("c906275c-3747-fe27-426f-6187526a6f06");
+ DownloadContentCatalog catalog = spy(new MockedContentCatalog(existingContent));
+
+ action.perform(RuntimeEnvironment.application, catalog);
+
+ // make sure nothing was done
+ verify(action, never()).createContent(anyCatalog(), anyJSONObject());
+ verify(action, never()).updateContent(anyCatalog(), anyJSONObject(), anyContent());
+ verify(action, never()).deleteContent(anyCatalog(), anyString());
+ verify(action, never()).startStudyAction(anyContext());
+ }
+
+
+ /**
+ * Scenario: The catalog contains one item and the server returns a new version.
+ */
+ @Test
+ public void testUpdatingExistingContent() throws Exception{
+ SyncAction action = spy(new SyncAction());
+ doReturn(true).when(action).isSyncEnabledForClient(RuntimeEnvironment.application);
+ doReturn(fromFile("dlc_sync_single_font.json")).when(action).fetchRawCatalog(anyLong());
+
+ DownloadContent existingContent = createTestContent("c906275c-3747-fe27-426f-6187526a6f06");
+ DownloadContentCatalog catalog = spy(new MockedContentCatalog(existingContent));
+
+ action.perform(RuntimeEnvironment.application, catalog);
+
+ // A content item has been updated
+ verify(action).updateContent(anyCatalog(), anyJSONObject(), eq(existingContent));
+
+ // No content item has been created or deleted
+ verify(action, never()).createContent(anyCatalog(), anyJSONObject());
+ verify(action, never()).deleteContent(anyCatalog(), anyString());
+
+ // An item has been updated in the catalog
+ ArgumentCaptor<DownloadContent> captor = ArgumentCaptor.forClass(DownloadContent.class);
+ verify(catalog).update(captor.capture());
+
+ // The item has the new values from the sever response
+ DownloadContent content = captor.getValue();
+ Assert.assertEquals("c906275c-3747-fe27-426f-6187526a6f06", content.getId());
+ Assert.assertEquals("4ed509317f1bb441b185ea13bf1c9d19d1a0b396962efa3b5dc3190ad88f2067", content.getChecksum());
+ Assert.assertEquals("960be4fc5a92c1dc488582b215d5d75429fd4ffbee463105d29992cd792a912e", content.getDownloadChecksum());
+ Assert.assertEquals("CharisSILCompact-R.ttf", content.getFilename());
+ Assert.assertEquals(DownloadContent.KIND_FONT, content.getKind());
+ Assert.assertEquals("/attachments/0d28a72d-a51f-46f8-9e5a-f95c61de904e.gz", content.getLocation());
+ Assert.assertEquals(DownloadContent.TYPE_ASSET_ARCHIVE, content.getType());
+ Assert.assertEquals(1455710632607L, content.getLastModified());
+ Assert.assertEquals(1727656L, content.getSize());
+ Assert.assertEquals(DownloadContent.STATE_UPDATED, content.getState());
+ }
+
+ /**
+ * Scenario: Catalog contains one item and the server returns that it has been deleted.
+ */
+ @Test
+ public void testDeletingExistingContent() throws Exception {
+ SyncAction action = spy(new SyncAction());
+ doReturn(true).when(action).isSyncEnabledForClient(RuntimeEnvironment.application);
+ doReturn(fromFile("dlc_sync_deleted_item.json")).when(action).fetchRawCatalog(anyLong());
+
+ final String id = "c906275c-3747-fe27-426f-6187526a6f06";
+ DownloadContent existingContent = createTestContent(id);
+ DownloadContentCatalog catalog = spy(new MockedContentCatalog(existingContent));
+
+ action.perform(RuntimeEnvironment.application, catalog);
+
+ // A content item has been deleted
+ verify(action).deleteContent(anyCatalog(), eq(id));
+
+ // No content item has been created or updated
+ verify(action, never()).createContent(anyCatalog(), anyJSONObject());
+ verify(action, never()).updateContent(anyCatalog(), anyJSONObject(), anyContent());
+
+ // An item has been marked for deletion in the catalog
+ ArgumentCaptor<DownloadContent> captor = ArgumentCaptor.forClass(DownloadContent.class);
+ verify(catalog).markAsDeleted(captor.capture());
+
+ DownloadContent content = captor.getValue();
+ Assert.assertEquals(id, content.getId());
+
+ List<DownloadContent> contentToDelete = catalog.getContentToDelete();
+ Assert.assertEquals(1, contentToDelete.size());
+ Assert.assertEquals(id, contentToDelete.get(0).getId());
+ }
+
+ /**
+ * Create a DownloadContent object with arbitrary data.
+ */
+ private DownloadContent createTestContent(String id) {
+ return new DownloadContentBuilder()
+ .setId(id)
+ .setLocation("/somewhere/something")
+ .setFilename("some.file")
+ .setChecksum("Some-checksum")
+ .setDownloadChecksum("Some-download-checksum")
+ .setLastModified(4223)
+ .setType("Some-type")
+ .setKind("Some-kind")
+ .setSize(27)
+ .setState(DownloadContent.STATE_SCHEDULED)
+ .build();
+ }
+
+ /**
+ * Create a Kinto response from a JSON file.
+ */
+ private JSONArray fromFile(String fileName) throws IOException, JSONException {
+ URL url = getClass().getResource("/" + fileName);
+ if (url == null) {
+ throw new FileNotFoundException(fileName);
+ }
+
+ InputStream inputStream = null;
+ ByteArrayOutputStream outputStream = null;
+
+ try {
+ inputStream = new BufferedInputStream(new FileInputStream(url.getPath()));
+ outputStream = new ByteArrayOutputStream();
+
+ IOUtils.copy(inputStream, outputStream);
+
+ JSONObject object = new JSONObject(outputStream.toString());
+
+ return object.getJSONArray("data");
+ } finally {
+ IOUtils.safeStreamClose(inputStream);
+ IOUtils.safeStreamClose(outputStream);
+ }
+ }
+
+ private static class MockedContentCatalog extends DownloadContentCatalog {
+ public MockedContentCatalog(DownloadContent content) {
+ super(mock(AtomicFile.class));
+
+ ArrayMap<String, DownloadContent> map = new ArrayMap<>();
+ map.put(content.getId(), content);
+
+ onCatalogLoaded(map);
+ }
+ }
+
+ private DownloadContentCatalog mockCatalog() {
+ return mock(DownloadContentCatalog.class);
+ }
+
+ private DownloadContentCatalog anyCatalog() {
+ return any(DownloadContentCatalog.class);
+ }
+
+ private JSONObject anyJSONObject() {
+ return any(JSONObject.class);
+ }
+
+ private DownloadContent anyContent() {
+ return any(DownloadContent.class);
+ }
+
+ private Context anyContext() {
+ return any(Context.class);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestVerifyAction.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestVerifyAction.java
new file mode 100644
index 0000000000..6a347376e9
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestVerifyAction.java
@@ -0,0 +1,123 @@
+/* -*- 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.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.dlc.catalog.DownloadContent;
+import org.mozilla.gecko.dlc.catalog.DownloadContentBuilder;
+import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog;
+import org.robolectric.RuntimeEnvironment;
+
+import java.io.File;
+import java.util.Collections;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * VerifyAction: Validate downloaded content. Does it still exist and does it have the correct checksum?
+ */
+@RunWith(TestRunner.class)
+public class TestVerifyAction {
+ /**
+ * Scenario: Downloaded file does not exist anymore.
+ *
+ * Verify that:
+ * * Content is re-scheduled for download.
+ */
+ @Test
+ public void testReschedulingIfFileDoesNotExist() throws Exception {
+ DownloadContent content = new DownloadContentBuilder().build();
+ DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
+ when(catalog.getDownloadedContent()).thenReturn(Collections.singletonList(content));
+
+ File file = mock(File.class);
+ when(file.exists()).thenReturn(false);
+
+ VerifyAction action = spy(new VerifyAction());
+ doReturn(file).when(action).getDestinationFile(RuntimeEnvironment.application, content);
+
+ action.perform(RuntimeEnvironment.application, catalog);
+
+ verify(catalog).scheduleDownload(content);
+ }
+
+ /**
+ * Scenario: Content has been scheduled for download.
+ *
+ * Verify that:
+ * * Download action is started
+ */
+ @Test
+ public void testStartingDownloadsAfterScheduling() {
+ DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
+ when(catalog.hasScheduledDownloads()).thenReturn(true);
+
+ VerifyAction action = spy(new VerifyAction());
+ action.perform(RuntimeEnvironment.application, catalog);
+
+ verify(action).startDownloads(any(Context.class));
+ }
+
+ /**
+ * Scenario: Checksum of existing file does not match expectation.
+ *
+ * Verify that:
+ * * Content is re-scheduled for download.
+ */
+ @Test
+ public void testReschedulingIfVerificationFailed() throws Exception {
+ DownloadContent content = new DownloadContentBuilder().build();
+ DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
+ when(catalog.getDownloadedContent()).thenReturn(Collections.singletonList(content));
+
+ File file = mock(File.class);
+ when(file.exists()).thenReturn(true);
+
+ VerifyAction action = spy(new VerifyAction());
+ doReturn(file).when(action).getDestinationFile(RuntimeEnvironment.application, content);
+ doReturn(false).when(action).verify(eq(file), anyString());
+
+ action.perform(RuntimeEnvironment.application, catalog);
+
+ verify(catalog).scheduleDownload(content);
+ }
+
+ /**
+ * Scenario: Downloaded file exists and has the correct checksum.
+ *
+ * Verify that:
+ * * No download is scheduled
+ * * Download action is not started
+ */
+ @Test
+ public void testSuccessfulVerification() throws Exception {
+ DownloadContent content = new DownloadContentBuilder().build();
+ DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
+ when(catalog.getDownloadedContent()).thenReturn(Collections.singletonList(content));
+
+ File file = mock(File.class);
+ when(file.exists()).thenReturn(true);
+
+ VerifyAction action = spy(new VerifyAction());
+ doReturn(file).when(action).getDestinationFile(RuntimeEnvironment.application, content);
+ doReturn(true).when(action).verify(eq(file), anyString());
+
+ verify(catalog, never()).scheduleDownload(content);
+ verify(action, never()).startDownloads(RuntimeEnvironment.application);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/catalog/TestDownloadContentBuilder.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/catalog/TestDownloadContentBuilder.java
new file mode 100644
index 0000000000..147b5da5bb
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/catalog/TestDownloadContentBuilder.java
@@ -0,0 +1,69 @@
+/* -*- 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 org.json.JSONException;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+@RunWith(TestRunner.class)
+public class TestDownloadContentBuilder {
+ /**
+ * Verify that the values passed to the builder are all set on the DownloadContent object.
+ */
+ @Test
+ public void testBuilder() {
+ DownloadContent content = createTestContent();
+
+ Assert.assertEquals("Some-ID", content.getId());
+ Assert.assertEquals("/somewhere/something", content.getLocation());
+ Assert.assertEquals("some.file", content.getFilename());
+ Assert.assertEquals("Some-checksum", content.getChecksum());
+ Assert.assertEquals("Some-download-checksum", content.getDownloadChecksum());
+ Assert.assertEquals(4223, content.getLastModified());
+ Assert.assertEquals("Some-type", content.getType());
+ Assert.assertEquals("Some-kind", content.getKind());
+ Assert.assertEquals(27, content.getSize());
+ Assert.assertEquals(DownloadContent.STATE_SCHEDULED, content.getState());
+ }
+
+ /**
+ * Verify that a DownloadContent object exported to JSON and re-imported from JSON does not change.
+ */
+ public void testJSONSerializationAndDeserialization() throws JSONException {
+ DownloadContent content = DownloadContentBuilder.fromJSON(DownloadContentBuilder.toJSON(createTestContent()));
+
+ Assert.assertEquals("Some-ID", content.getId());
+ Assert.assertEquals("/somewhere/something", content.getLocation());
+ Assert.assertEquals("some.file", content.getFilename());
+ Assert.assertEquals("Some-checksum", content.getChecksum());
+ Assert.assertEquals("Some-download-checksum", content.getDownloadChecksum());
+ Assert.assertEquals(4223, content.getLastModified());
+ Assert.assertEquals("Some-type", content.getType());
+ Assert.assertEquals("Some-kind", content.getKind());
+ Assert.assertEquals(27, content.getSize());
+ Assert.assertEquals(DownloadContent.STATE_SCHEDULED, content.getState());
+ }
+
+ /**
+ * Create a DownloadContent object with arbitrary data.
+ */
+ private DownloadContent createTestContent() {
+ return new DownloadContentBuilder()
+ .setId("Some-ID")
+ .setLocation("/somewhere/something")
+ .setFilename("some.file")
+ .setChecksum("Some-checksum")
+ .setDownloadChecksum("Some-download-checksum")
+ .setLastModified(4223)
+ .setType("Some-type")
+ .setKind("Some-kind")
+ .setSize(27)
+ .setState(DownloadContent.STATE_SCHEDULED)
+ .build();
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/catalog/TestDownloadContentCatalog.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/catalog/TestDownloadContentCatalog.java
new file mode 100644
index 0000000000..5b5912cdda
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/catalog/TestDownloadContentCatalog.java
@@ -0,0 +1,262 @@
+/* -*- 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 android.support.v4.util.AtomicFile;
+
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+@RunWith(TestRunner.class)
+public class TestDownloadContentCatalog {
+ /**
+ * Scenario: Create a new, fresh catalog.
+ *
+ * Verify that:
+ * * Catalog has not changed
+ * * Unchanged catalog will not be saved to disk
+ */
+ @Test
+ public void testUntouchedCatalogHasNotChangedAndWillNotBePersisted() throws Exception {
+ AtomicFile file = mock(AtomicFile.class);
+ doReturn("{content:[]}".getBytes("UTF-8")).when(file).readFully();
+
+ DownloadContentCatalog catalog = spy(new DownloadContentCatalog(file));
+ catalog.loadFromDisk();
+
+ Assert.assertFalse("Catalog has not changed", catalog.hasCatalogChanged());
+
+ catalog.writeToDisk();
+
+ Assert.assertFalse("Catalog has not changed", catalog.hasCatalogChanged());
+
+ verify(file, never()).startWrite();
+ }
+
+ /**
+ * Scenario: Create a new, fresh catalog.
+ *
+ * Verify that:
+ * * Catalog is bootstrapped with items.
+ */
+ @Test
+ public void testCatalogIsBootstrappedIfFileDoesNotExist() throws Exception {
+ // The catalog is only bootstrapped if fonts are excluded from the build. If this is a build
+ // with fonts included then ignore this test.
+ Assume.assumeTrue("Fonts are excluded from build", AppConstants.MOZ_ANDROID_EXCLUDE_FONTS);
+
+ AtomicFile file = mock(AtomicFile.class);
+ doThrow(FileNotFoundException.class).when(file).readFully();
+
+ DownloadContentCatalog catalog = spy(new DownloadContentCatalog(file));
+ catalog.loadFromDisk();
+
+ Assert.assertTrue("Catalog is not empty", catalog.getContentToStudy().size() > 0);
+ }
+
+ /**
+ * Scenario: Schedule downloading an item from the catalog.
+ *
+ * Verify that:
+ * * Catalog has changed
+ */
+ @Test
+ public void testCatalogHasChangedWhenDownloadIsScheduled() throws Exception {
+ DownloadContentCatalog catalog = spy(new DownloadContentCatalog(mock(AtomicFile.class)));
+ DownloadContent content = new DownloadContentBuilder().build();
+ catalog.onCatalogLoaded(createMapOfContent(content));
+
+ Assert.assertFalse("Catalog has not changed", catalog.hasCatalogChanged());
+
+ catalog.scheduleDownload(content);
+
+ Assert.assertTrue("Catalog has changed", catalog.hasCatalogChanged());
+ }
+
+ /**
+ * Scenario: Mark an item in the catalog as downloaded.
+ *
+ * Verify that:
+ * * Catalog has changed
+ */
+ @Test
+ public void testCatalogHasChangedWhenContentIsDownloaded() throws Exception {
+ DownloadContentCatalog catalog = spy(new DownloadContentCatalog(mock(AtomicFile.class)));
+ DownloadContent content = new DownloadContentBuilder().build();
+ catalog.onCatalogLoaded(createMapOfContent(content));
+
+ Assert.assertFalse("Catalog has not changed", catalog.hasCatalogChanged());
+
+ catalog.markAsDownloaded(content);
+
+ Assert.assertTrue("Catalog has changed", catalog.hasCatalogChanged());
+ }
+
+ /**
+ * Scenario: Mark an item in the catalog as permanently failed.
+ *
+ * Verify that:
+ * * Catalog has changed
+ */
+ @Test
+ public void testCatalogHasChangedIfDownloadHasFailedPermanently() throws Exception {
+ DownloadContentCatalog catalog = spy(new DownloadContentCatalog(mock(AtomicFile.class)));
+ DownloadContent content = new DownloadContentBuilder().build();
+ catalog.onCatalogLoaded(createMapOfContent(content));
+
+ Assert.assertFalse("Catalog has not changed", catalog.hasCatalogChanged());
+
+ catalog.markAsPermanentlyFailed(content);
+
+ Assert.assertTrue("Catalog has changed", catalog.hasCatalogChanged());
+ }
+
+ /**
+ * Scenario: A changed catalog is written to disk.
+ *
+ * Verify that:
+ * * Before write: Catalog has changed
+ * * After write: Catalog has not changed.
+ */
+ @Test
+ public void testCatalogHasNotChangedAfterWritingToDisk() throws Exception {
+ AtomicFile file = mock(AtomicFile.class);
+ doReturn(mock(FileOutputStream.class)).when(file).startWrite();
+
+ DownloadContentCatalog catalog = spy(new DownloadContentCatalog(file));
+ DownloadContent content = new DownloadContentBuilder().build();
+ catalog.onCatalogLoaded(createMapOfContent(content));
+
+ catalog.scheduleDownload(content);
+
+ Assert.assertTrue("Catalog has changed", catalog.hasCatalogChanged());
+
+ catalog.writeToDisk();
+
+ Assert.assertFalse("Catalog has not changed", catalog.hasCatalogChanged());
+ }
+
+ /**
+ * Scenario: A catalog with multiple items in different states.
+ *
+ * Verify that:
+ * * getContentWithoutState(), getDownloadedContent() and getScheduledDownloads() returns
+ * the correct items depenending on their state.
+ */
+ @Test
+ public void testContentClassification() {
+ DownloadContentCatalog catalog = spy(new DownloadContentCatalog(mock(AtomicFile.class)));
+
+ DownloadContent content1 = new DownloadContentBuilder().setId("A").setState(DownloadContent.STATE_NONE).build();
+ DownloadContent content2 = new DownloadContentBuilder().setId("B").setState(DownloadContent.STATE_NONE).build();
+ DownloadContent content3 = new DownloadContentBuilder().setId("C").setState(DownloadContent.STATE_SCHEDULED).build();
+ DownloadContent content4 = new DownloadContentBuilder().setId("D").setState(DownloadContent.STATE_SCHEDULED).build();
+ DownloadContent content5 = new DownloadContentBuilder().setId("E").setState(DownloadContent.STATE_SCHEDULED).build();
+ DownloadContent content6 = new DownloadContentBuilder().setId("F").setState(DownloadContent.STATE_DOWNLOADED).build();
+ DownloadContent content7 = new DownloadContentBuilder().setId("G").setState(DownloadContent.STATE_FAILED).build();
+ DownloadContent content8 = new DownloadContentBuilder().setId("H").setState(DownloadContent.STATE_UPDATED).build();
+ DownloadContent content9 = new DownloadContentBuilder().setId("I").setState(DownloadContent.STATE_DELETED).build();
+ DownloadContent content10 = new DownloadContentBuilder().setId("J").setState(DownloadContent.STATE_DELETED).build();
+
+ catalog.onCatalogLoaded(createMapOfContent(content1, content2, content3, content4, content5, content6,
+ content7, content8, content9, content10));
+
+ Assert.assertTrue(catalog.hasScheduledDownloads());
+
+ Assert.assertEquals(3, catalog.getContentToStudy().size());
+ Assert.assertEquals(1, catalog.getDownloadedContent().size());
+ Assert.assertEquals(3, catalog.getScheduledDownloads().size());
+ Assert.assertEquals(2, catalog.getContentToDelete().size());
+
+ Assert.assertTrue(catalog.getContentToStudy().contains(content1));
+ Assert.assertTrue(catalog.getContentToStudy().contains(content2));
+ Assert.assertTrue(catalog.getContentToStudy().contains(content8));
+
+ Assert.assertTrue(catalog.getDownloadedContent().contains(content6));
+
+ Assert.assertTrue(catalog.getScheduledDownloads().contains(content3));
+ Assert.assertTrue(catalog.getScheduledDownloads().contains(content4));
+ Assert.assertTrue(catalog.getScheduledDownloads().contains(content5));
+
+ Assert.assertTrue(catalog.getContentToDelete().contains(content9));
+ Assert.assertTrue(catalog.getContentToDelete().contains(content10));
+ }
+
+ /**
+ * Scenario: Calling rememberFailure() on a catalog with varying values
+ */
+ @Test
+ public void testRememberingFailures() {
+ DownloadContentCatalog catalog = new DownloadContentCatalog(mock(AtomicFile.class));
+ Assert.assertFalse(catalog.hasCatalogChanged());
+
+ DownloadContent content = new DownloadContentBuilder().build();
+ Assert.assertEquals(0, content.getFailures());
+
+ catalog.rememberFailure(content, 42);
+ Assert.assertEquals(1, content.getFailures());
+ Assert.assertTrue(catalog.hasCatalogChanged());
+
+ catalog.rememberFailure(content, 42);
+ Assert.assertEquals(2, content.getFailures());
+
+ // Failure counter is reset if different failure has been reported
+ catalog.rememberFailure(content, 23);
+ Assert.assertEquals(1, content.getFailures());
+
+ // Failure counter is reset after successful download
+ catalog.markAsDownloaded(content);
+ Assert.assertEquals(0, content.getFailures());
+ }
+
+ /**
+ * Scenario: Content has failed multiple times with the same failure type.
+ *
+ * Verify that:
+ * * Content is marked as permanently failed
+ */
+ @Test
+ public void testContentWillBeMarkedAsPermanentlyFailedAfterMultipleFailures() {
+ DownloadContentCatalog catalog = new DownloadContentCatalog(mock(AtomicFile.class));
+
+ DownloadContent content = new DownloadContentBuilder().build();
+ Assert.assertEquals(DownloadContent.STATE_NONE, content.getState());
+
+ for (int i = 0; i < 10; i++) {
+ catalog.rememberFailure(content, 42);
+
+ Assert.assertEquals(i + 1, content.getFailures());
+ Assert.assertEquals(DownloadContent.STATE_NONE, content.getState());
+ }
+
+ catalog.rememberFailure(content, 42);
+ Assert.assertEquals(10, content.getFailures());
+ Assert.assertEquals(DownloadContent.STATE_FAILED, content.getState());
+ }
+
+ private ArrayMap<String, DownloadContent> createMapOfContent(DownloadContent... content) {
+ ArrayMap<String, DownloadContent> map = new ArrayMap<>();
+ for (DownloadContent currentContent : content) {
+ map.put(currentContent.getId(), currentContent);
+ }
+ return map;
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/knownsites/TestKnownSiteBlogger.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/knownsites/TestKnownSiteBlogger.java
new file mode 100644
index 0000000000..628b572ce7
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/knownsites/TestKnownSiteBlogger.java
@@ -0,0 +1,74 @@
+/* -*- 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 org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.helpers.AssertUtil;
+
+@RunWith(TestRunner.class)
+public class TestKnownSiteBlogger {
+ /**
+ * Test that the search string is a substring of some known URLs.
+ */
+ @Test
+ public void testURLSearchString() {
+ final KnownSite blogger = new KnownSiteBlogger();
+ final String searchString = blogger.getURLSearchString();
+
+ AssertUtil.assertContains(
+ "http://mykzilla.blogspot.com/",
+ searchString);
+
+ AssertUtil.assertContains(
+ "http://example.blogspot.com",
+ searchString);
+
+ AssertUtil.assertContains(
+ "https://mykzilla.blogspot.com/2015/06/introducing-pluotsorbet.html",
+ searchString);
+
+ AssertUtil.assertContains(
+ "http://android-developers.blogspot.com/2016/02/android-support-library-232.html",
+ searchString);
+
+ AssertUtil.assertContainsNot(
+ "http://www.mozilla.org",
+ searchString);
+ }
+
+ /**
+ * Test that we get a feed URL for valid Blogger URLs.
+ */
+ @Test
+ public void testGettingFeedFromURL() {
+ final KnownSite blogger = new KnownSiteBlogger();
+
+ Assert.assertEquals(
+ "https://mykzilla.blogspot.com/feeds/posts/default",
+ blogger.getFeedFromURL("http://mykzilla.blogspot.com/"));
+
+ Assert.assertEquals(
+ "https://example.blogspot.com/feeds/posts/default",
+ blogger.getFeedFromURL("http://example.blogspot.com"));
+
+ Assert.assertEquals(
+ "https://mykzilla.blogspot.com/feeds/posts/default",
+ blogger.getFeedFromURL("https://mykzilla.blogspot.com/2015/06/introducing-pluotsorbet.html"));
+
+ Assert.assertEquals(
+ "https://android-developers.blogspot.com/feeds/posts/default",
+ blogger.getFeedFromURL("http://android-developers.blogspot.com/2016/02/android-support-library-232.html"));
+
+ Assert.assertEquals(
+ "https://example.blogspot.com/feeds/posts/default",
+ blogger.getFeedFromURL("http://example.blogspot.com/2016/03/i-moved-to-example.blogspot.com"));
+
+ Assert.assertNull(blogger.getFeedFromURL("http://www.mozilla.org"));
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/knownsites/TestKnownSiteMedium.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/knownsites/TestKnownSiteMedium.java
new file mode 100644
index 0000000000..77f05e0d01
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/knownsites/TestKnownSiteMedium.java
@@ -0,0 +1,66 @@
+/* -*- 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 org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.helpers.AssertUtil;
+
+@RunWith(TestRunner.class)
+public class TestKnownSiteMedium {
+ /**
+ * Test that the search string is a substring of some known URLs.
+ */
+ @Test
+ public void testURLSearchString() {
+ final KnownSite medium = new KnownSiteMedium();
+ final String searchString = medium.getURLSearchString();
+
+ AssertUtil.assertContains(
+ "https://medium.com/@Antlam/",
+ searchString);
+
+ AssertUtil.assertContains(
+ "https://medium.com/google-developers",
+ searchString);
+
+ AssertUtil.assertContains(
+ "http://medium.com/@brandonshin/how-slackbot-forced-us-to-workout-7b4741a2de73",
+ searchString
+ );
+
+ AssertUtil.assertContainsNot(
+ "http://www.mozilla.org",
+ searchString);
+ }
+
+ /**
+ * Test that we get a feed URL for valid Medium URLs.
+ */
+ @Test
+ public void testGettingFeedFromURL() {
+ final KnownSite medium = new KnownSiteMedium();
+
+ Assert.assertEquals(
+ "https://medium.com/feed/@Antlam",
+ medium.getFeedFromURL("https://medium.com/@Antlam/")
+ );
+
+ Assert.assertEquals(
+ "https://medium.com/feed/google-developers",
+ medium.getFeedFromURL("https://medium.com/google-developers")
+ );
+
+ Assert.assertEquals(
+ "https://medium.com/feed/@brandonshin",
+ medium.getFeedFromURL("http://medium.com/@brandonshin/how-slackbot-forced-us-to-workout-7b4741a2de73")
+ );
+
+ Assert.assertNull(medium.getFeedFromURL("http://www.mozilla.org"));
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/knownsites/TestKnownSiteTumblr.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/knownsites/TestKnownSiteTumblr.java
new file mode 100644
index 0000000000..f83272f822
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/knownsites/TestKnownSiteTumblr.java
@@ -0,0 +1,62 @@
+/* -*- 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 org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.helpers.AssertUtil;
+
+@RunWith(TestRunner.class)
+public class TestKnownSiteTumblr {
+ /**
+ * Test that the search string is a substring of some known URLs.
+ */
+ @Test
+ public void testURLSearchString() {
+ final KnownSite tumblr = new KnownSiteTumblr();
+ final String searchString = tumblr.getURLSearchString();
+
+ AssertUtil.assertContains(
+ "http://contentnotifications.tumblr.com/",
+ searchString);
+
+ AssertUtil.assertContains(
+ "https://contentnotifications.tumblr.com",
+ searchString);
+
+ AssertUtil.assertContains(
+ "http://contentnotifications.tumblr.com/post/142684202402/content-notification-firefox-for-android-480",
+ searchString);
+
+ AssertUtil.assertContainsNot(
+ "http://www.mozilla.org",
+ searchString);
+ }
+
+ /**
+ * Test that we get a feed URL for valid Medium URLs.
+ */
+ @Test
+ public void testGettingFeedFromURL() {
+ final KnownSite tumblr = new KnownSiteTumblr();
+
+ Assert.assertEquals(
+ "http://contentnotifications.tumblr.com/rss",
+ tumblr.getFeedFromURL("http://contentnotifications.tumblr.com/")
+ );
+
+ Assert.assertEquals(
+ "http://staff.tumblr.com/rss",
+ tumblr.getFeedFromURL("https://staff.tumblr.com/post/141928246566/replies-are-back-and-the-sun-is-shining-on-the")
+ );
+
+ Assert.assertNull(tumblr.getFeedFromURL("https://www.tumblr.com"));
+
+ Assert.assertNull(tumblr.getFeedFromURL("http://www.mozilla.org"));
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/parser/TestSimpleFeedParser.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/parser/TestSimpleFeedParser.java
new file mode 100644
index 0000000000..fa2fffbad4
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/parser/TestSimpleFeedParser.java
@@ -0,0 +1,323 @@
+/* -*- 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 org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+
+@RunWith(TestRunner.class)
+public class TestSimpleFeedParser {
+ /**
+ * Parse and verify the RSS example from Wikipedia:
+ * https://en.wikipedia.org/wiki/RSS#Example
+ */
+ @Test
+ public void testRSSExample() throws Exception {
+ InputStream stream = openFeed("feed_rss_wikipedia.xml");
+
+ SimpleFeedParser parser = new SimpleFeedParser();
+ Feed feed = parser.parse(stream);
+
+ Assert.assertNotNull(feed);
+ Assert.assertEquals("RSS Title", feed.getTitle());
+ Assert.assertEquals("http://www.example.com/main.html", feed.getWebsiteURL());
+ Assert.assertNull(feed.getFeedURL());
+ Assert.assertTrue(feed.isSufficientlyComplete());
+
+ Item item = feed.getLastItem();
+
+ Assert.assertNotNull(item);
+ Assert.assertEquals("Example entry", item.getTitle());
+ Assert.assertEquals("http://www.example.com/blog/post/1", item.getURL());
+ Assert.assertEquals(1252254000000L, item.getTimestamp());
+ }
+
+ /**
+ * Parse and verify the ATOM example from Wikipedia:
+ * https://en.wikipedia.org/wiki/Atom_%28standard%29#Example_of_an_Atom_1.0_feed
+ */
+ @Test
+ public void testATOMExample() throws Exception {
+ InputStream stream = openFeed("feed_atom_wikipedia.xml");
+
+ SimpleFeedParser parser = new SimpleFeedParser();
+ Feed feed = parser.parse(stream);
+
+ Assert.assertNotNull(feed);
+ Assert.assertEquals("Example Feed", feed.getTitle());
+ Assert.assertEquals("http://example.org/", feed.getWebsiteURL());
+ Assert.assertEquals("http://example.org/feed/", feed.getFeedURL());
+ Assert.assertTrue(feed.isSufficientlyComplete());
+
+ Item item = feed.getLastItem();
+
+ Assert.assertNotNull(item);
+ Assert.assertEquals("Atom-Powered Robots Run Amok", item.getTitle());
+ Assert.assertEquals("http://example.org/2003/12/13/atom03.html", item.getURL());
+ Assert.assertEquals(1071340202000L, item.getTimestamp());
+ }
+
+ /**
+ * Parse and verify a snapshot of a Medium feed.
+ */
+ @Test
+ public void testMediumFeed() throws Exception {
+ InputStream stream = openFeed("feed_rss_medium.xml");
+
+ SimpleFeedParser parser = new SimpleFeedParser();
+ Feed feed = parser.parse(stream);
+
+ Assert.assertNotNull(feed);
+ Assert.assertEquals("Anthony Lam on Medium", feed.getTitle());
+ Assert.assertEquals("https://medium.com/@antlam?source=rss-59f49b9e4b19------2", feed.getWebsiteURL());
+ Assert.assertEquals("https://medium.com/feed/@antlam", feed.getFeedURL());
+ Assert.assertTrue(feed.isSufficientlyComplete());
+
+ Item item = feed.getLastItem();
+
+ Assert.assertNotNull(item);
+ Assert.assertEquals("UX thoughts for 2016", item.getTitle());
+ Assert.assertEquals("https://medium.com/@antlam/ux-thoughts-for-2016-1fc1d6e515e8?source=rss-59f49b9e4b19------2", item.getURL());
+ Assert.assertEquals(1452537838000L, item.getTimestamp());
+ }
+
+ /**
+ * Parse and verify a snapshot of planet.mozilla.org ATOM feed.
+ */
+ @Test
+ public void testPlanetMozillaATOMFeed() throws Exception {
+ InputStream stream = openFeed("feed_atom_planetmozilla.xml");
+
+ SimpleFeedParser parser = new SimpleFeedParser();
+ Feed feed = parser.parse(stream);
+
+ Assert.assertNotNull(feed);
+ Assert.assertEquals("Planet Mozilla", feed.getTitle());
+ Assert.assertEquals("http://planet.mozilla.org/", feed.getWebsiteURL());
+ Assert.assertEquals("http://planet.mozilla.org/atom.xml", feed.getFeedURL());
+ Assert.assertTrue(feed.isSufficientlyComplete());
+
+ Item item = feed.getLastItem();
+
+ Assert.assertNotNull(item);
+ Assert.assertEquals("Firefox 45.0 Beta 3 Testday, February 5th", item.getTitle());
+ Assert.assertEquals("https://quality.mozilla.org/2016/01/firefox-45-0-beta-3-testday-february-5th/", item.getURL());
+ Assert.assertEquals(1453819255000L, item.getTimestamp());
+ }
+
+ /**
+ * Parse and verify a snapshot of planet.mozilla.org RSS 2.0 feed.
+ */
+ @Test
+ public void testPlanetMozillaRSS20Feed() throws Exception {
+ InputStream stream = openFeed("feed_rss20_planetmozilla.xml");
+
+ SimpleFeedParser parser = new SimpleFeedParser();
+ Feed feed = parser.parse(stream);
+
+ Assert.assertNotNull(feed);
+ Assert.assertEquals("Planet Mozilla", feed.getTitle());
+ Assert.assertEquals("http://planet.mozilla.org/", feed.getWebsiteURL());
+ Assert.assertEquals("http://planet.mozilla.org/rss20.xml", feed.getFeedURL());
+ Assert.assertTrue(feed.isSufficientlyComplete());
+
+ Item item = feed.getLastItem();
+
+ Assert.assertNotNull(item);
+ Assert.assertEquals("Aaron Klotz: Announcing Mozdbgext", item.getTitle());
+ Assert.assertEquals("http://dblohm7.ca/blog/2016/01/26/announcing-mozdbgext/", item.getURL());
+ Assert.assertEquals(1453837500000L, item.getTimestamp());
+ }
+
+ /**
+ * Parse and verify a snapshot of planet.mozilla.org RSS 1.0 feed.
+ */
+ @Test
+ public void testPlanetMozillaRSS10Feed() throws Exception {
+ InputStream stream = openFeed("feed_rss10_planetmozilla.xml");
+
+ SimpleFeedParser parser = new SimpleFeedParser();
+ Feed feed = parser.parse(stream);
+
+ Assert.assertNotNull(feed);
+ Assert.assertEquals("Planet Mozilla", feed.getTitle());
+ Assert.assertEquals("http://planet.mozilla.org/", feed.getWebsiteURL());
+ Assert.assertEquals("http://planet.mozilla.org/rss10.xml", feed.getFeedURL());
+ Assert.assertTrue(feed.isSufficientlyComplete());
+
+ Item item = feed.getLastItem();
+
+ Assert.assertNotNull(item);
+ Assert.assertEquals("Aaron Klotz: Announcing Mozdbgext", item.getTitle());
+ Assert.assertEquals("http://dblohm7.ca/blog/2016/01/26/announcing-mozdbgext/", item.getURL());
+ Assert.assertEquals(1453837500000L, item.getTimestamp());
+ }
+
+ /**
+ * Parse an verify a snapshot of a feedburner ATOM feed.
+ */
+ @Test
+ public void testFeedburnerAtomFeed() throws Exception {
+ InputStream stream = openFeed("feed_atom_feedburner.xml");
+
+ SimpleFeedParser parser = new SimpleFeedParser();
+ Feed feed = parser.parse(stream);
+
+ Assert.assertNotNull(feed);
+ Assert.assertEquals("Android Zeitgeist", feed.getTitle());
+ Assert.assertEquals("http://www.androidzeitgeist.com/", feed.getWebsiteURL());
+ Assert.assertEquals("http://feeds.feedburner.com/AndroidZeitgeist", feed.getFeedURL());
+ Assert.assertTrue(feed.isSufficientlyComplete());
+
+ Item item = feed.getLastItem();
+
+ Assert.assertNotNull(item);
+ Assert.assertEquals("Support for restricted profiles in Firefox 42", item.getTitle());
+ Assert.assertEquals("http://feedproxy.google.com/~r/AndroidZeitgeist/~3/xaSicfGuwOU/support-restricted-profiles-firefox.html", item.getURL());
+ Assert.assertEquals(1442511968239L, item.getTimestamp());
+ }
+
+ /**
+ * Parse and verify a snapshot of a Tumblr RSS feed.
+ */
+ @Test
+ public void testTumblrRssFeed() throws Exception {
+ InputStream stream = openFeed("feed_rss_tumblr.xml");
+
+ SimpleFeedParser parser = new SimpleFeedParser();
+ Feed feed = parser.parse(stream);
+
+ Assert.assertNotNull(feed);
+ Assert.assertEquals("Tumblr Staff", feed.getTitle());
+ Assert.assertEquals("http://staff.tumblr.com/", feed.getWebsiteURL());
+ Assert.assertNull(feed.getFeedURL());
+ Assert.assertTrue(feed.isSufficientlyComplete());
+
+ Item item = feed.getLastItem();
+
+ Assert.assertNotNull(item);
+ Assert.assertEquals("hardyboyscovers: Can Nancy Drew see things through and solve...", item.getTitle());
+ Assert.assertEquals("http://staff.tumblr.com/post/138124026275", item.getURL());
+ Assert.assertEquals(1453861812000L, item.getTimestamp());
+ }
+
+ /**
+ * Parse and verify a snapshot of a Spiegel (German news magazine) RSS feed.
+ */
+ @Test
+ public void testSpiegelRssFeed() throws Exception {
+ InputStream stream = openFeed("feed_rss_spon.xml");
+
+ SimpleFeedParser parser = new SimpleFeedParser();
+ Feed feed = parser.parse(stream);
+
+ Assert.assertNotNull(feed);
+ Assert.assertEquals("SPIEGEL ONLINE - Schlagzeilen", feed.getTitle());
+ Assert.assertEquals("http://www.spiegel.de", feed.getWebsiteURL());
+ Assert.assertNull(feed.getFeedURL());
+ Assert.assertTrue(feed.isSufficientlyComplete());
+
+ Item item = feed.getLastItem();
+
+ Assert.assertNotNull(item);
+ Assert.assertEquals("Angebliche Vergewaltigung einer 13-Jährigen: Steinmeier kanzelt russischen Minister Lawrow ab", item.getTitle());
+ Assert.assertEquals("http://www.spiegel.de/politik/ausland/steinmeier-kanzelt-lawrow-ab-aerger-um-angebliche-vergewaltigung-a-1074292.html#ref=rss", item.getURL());
+ Assert.assertEquals(1453914976000L, item.getTimestamp());
+ }
+
+ /**
+ * Parse and verify a snapshot of a Heise (German tech news) RSS feed.
+ */
+ @Test
+ public void testHeiseRssFeed() throws Exception {
+ InputStream stream = openFeed("feed_rss_heise.xml");
+
+ SimpleFeedParser parser = new SimpleFeedParser();
+ Feed feed = parser.parse(stream);
+
+ Assert.assertNotNull(feed);
+ Assert.assertEquals("heise online News", feed.getTitle());
+ Assert.assertEquals("http://www.heise.de/newsticker/", feed.getWebsiteURL());
+ Assert.assertNull(feed.getFeedURL());
+ Assert.assertTrue(feed.isSufficientlyComplete());
+
+ Item item = feed.getLastItem();
+
+ Assert.assertNotNull(item);
+ Assert.assertEquals("Google: “Dramatische Verbesserungen†für Chrome in iOS", item.getTitle());
+ Assert.assertEquals("http://www.heise.de/newsticker/meldung/Google-Dramatische-Verbesserungen-fuer-Chrome-in-iOS-3085808.html?wt_mc=rss.ho.beitrag.atom", item.getURL());
+ Assert.assertEquals(1453915920000L, item.getTimestamp());
+ }
+
+ @Test
+ public void testWordpressFeed() throws Exception {
+ InputStream stream = openFeed("feed_rss_wordpress.xml");
+
+ SimpleFeedParser parser = new SimpleFeedParser();
+ Feed feed = parser.parse(stream);
+
+ Assert.assertNotNull(feed);
+ Assert.assertEquals("justasimpletest2016", feed.getTitle());
+ Assert.assertEquals("https://justasimpletest2016.wordpress.com", feed.getWebsiteURL());
+ Assert.assertEquals("https://justasimpletest2016.wordpress.com/feed/", feed.getFeedURL());
+ Assert.assertTrue(feed.isSufficientlyComplete());
+
+ Item item = feed.getLastItem();
+
+ Assert.assertNotNull(item);
+ Assert.assertEquals("Hello World!", item.getTitle());
+ Assert.assertEquals("https://justasimpletest2016.wordpress.com/2016/02/26/hello-world/", item.getURL());
+ Assert.assertEquals(1456524466000L, item.getTimestamp());
+ }
+
+ /**
+ * Parse and test a snapshot of mykzilla.blogspot.com
+ */
+ @Test
+ public void testBloggerFeed() throws Exception {
+ InputStream stream = openFeed("feed_atom_blogger.xml");
+
+ SimpleFeedParser parser = new SimpleFeedParser();
+ Feed feed = parser.parse(stream);
+
+ Assert.assertNotNull(feed);
+ Assert.assertEquals("mykzilla", feed.getTitle());
+ Assert.assertEquals("http://mykzilla.blogspot.com/", feed.getWebsiteURL());
+ Assert.assertEquals("http://www.blogger.com/feeds/18929277/posts/default", feed.getFeedURL());
+ Assert.assertTrue(feed.isSufficientlyComplete());
+
+ Item item = feed.getLastItem();
+
+ Assert.assertNotNull(item);
+ Assert.assertEquals("URL Has Been Changed", item.getTitle());
+ Assert.assertEquals("http://mykzilla.blogspot.com/2016/01/url-has-been-changed.html", item.getURL());
+ Assert.assertEquals(1452531451366L, item.getTimestamp());
+ }
+
+ private InputStream openFeed(String fileName) throws URISyntaxException, FileNotFoundException, UnsupportedEncodingException {
+ URL url = getClass().getResource("/" + fileName);
+ if (url == null) {
+ throw new FileNotFoundException(fileName);
+ }
+
+ return new BufferedInputStream(new FileInputStream(url.getPath()));
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/TestSkewHandler.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/TestSkewHandler.java
new file mode 100644
index 0000000000..2b4fe3e034
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/TestSkewHandler.java
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa;
+
+import ch.boye.httpclientandroidlib.impl.cookie.DateUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.fxa.SkewHandler;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.net.BaseResource;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
+public class TestSkewHandler {
+ public TestSkewHandler() {
+ }
+
+ @Test
+ public void testSkewUpdating() throws Throwable {
+ SkewHandler h = new SkewHandler("foo.com");
+ assertEquals(0L, h.getSkewInSeconds());
+ assertEquals(0L, h.getSkewInMillis());
+
+ long server = 1390101197865L;
+ long local = server - 4500L;
+ h.updateSkewFromServerMillis(server, local);
+ assertEquals(4500L, h.getSkewInMillis());
+ assertEquals(4L, h.getSkewInSeconds());
+
+ local = server;
+ h.updateSkewFromServerMillis(server, local);
+ assertEquals(0L, h.getSkewInMillis());
+ assertEquals(0L, h.getSkewInSeconds());
+
+ local = server + 500L;
+ h.updateSkewFromServerMillis(server, local);
+ assertEquals(-500L, h.getSkewInMillis());
+ assertEquals(0L, h.getSkewInSeconds());
+
+ String date = "Sat, 18 Jan 2014 19:16:52 PST";
+ long dateInMillis = 1390101412000L; // Obviously this can differ somewhat due to precision.
+ long parsed = DateUtils.parseDate(date).getTime();
+ assertEquals(parsed, dateInMillis);
+
+ h.updateSkewFromHTTPDateString(date, dateInMillis);
+ assertEquals(0L, h.getSkewInMillis());
+ assertEquals(0L, h.getSkewInSeconds());
+
+ h.updateSkewFromHTTPDateString(date, dateInMillis + 1100L);
+ assertEquals(-1100L, h.getSkewInMillis());
+ assertEquals(Math.round(-1100L / 1000L), h.getSkewInSeconds());
+ }
+
+ @Test
+ public void testSkewSingleton() throws Exception {
+ SkewHandler h1 = SkewHandler.getSkewHandlerFromEndpointString("http://foo.com/bar");
+ SkewHandler h2 = SkewHandler.getSkewHandlerForHostname("foo.com");
+ SkewHandler h3 = SkewHandler.getSkewHandlerForResource(new BaseResource("http://foo.com/baz"));
+ assertTrue(h1 == h2);
+ assertTrue(h1 == h3);
+
+ SkewHandler.getSkewHandlerForHostname("foo.com").updateSkewFromServerMillis(1390101412000L, 1390001412000L);
+ final long actual = SkewHandler.getSkewHandlerForHostname("foo.com").getSkewInMillis();
+ assertEquals(100000000L, actual);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/MockFxAccountClient.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/MockFxAccountClient.java
new file mode 100644
index 0000000000..868e90cd2f
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/MockFxAccountClient.java
@@ -0,0 +1,226 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.fxa.login;
+
+import android.text.TextUtils;
+
+import org.mozilla.gecko.background.fxa.FxAccountClient;
+import org.mozilla.gecko.background.fxa.FxAccountClient20;
+import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse;
+import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate;
+import org.mozilla.gecko.background.fxa.FxAccountClient20.RecoveryEmailStatusResponse;
+import org.mozilla.gecko.background.fxa.FxAccountClient20.TwoKeys;
+import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
+import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
+import org.mozilla.gecko.background.fxa.FxAccountRemoteError;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.fxa.FxAccountDevice;
+import org.mozilla.gecko.browserid.MockMyIDTokenFactory;
+import org.mozilla.gecko.browserid.RSACryptoImplementation;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.Utils;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.entity.StringEntity;
+import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
+
+public class MockFxAccountClient implements FxAccountClient {
+ protected static MockMyIDTokenFactory mockMyIdTokenFactory = new MockMyIDTokenFactory();
+
+ public final String serverURI = "http://testServer.com";
+
+ public final Map<String, User> users = new HashMap<String, User>();
+ public final Map<String, String> sessionTokens = new HashMap<String, String>();
+ public final Map<String, String> keyFetchTokens = new HashMap<String, String>();
+
+ public static class User {
+ public final String email;
+ public final byte[] quickStretchedPW;
+ public final String uid;
+ public boolean verified;
+ public final byte[] kA;
+ public final byte[] wrapkB;
+ public final Map<String, FxAccountDevice> devices;
+
+ public User(String email, byte[] quickStretchedPW) {
+ this.email = email;
+ this.quickStretchedPW = quickStretchedPW;
+ this.uid = "uid/" + this.email;
+ this.verified = false;
+ this.kA = Utils.generateRandomBytes(FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES);
+ this.wrapkB = Utils.generateRandomBytes(FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES);
+ this.devices = new HashMap<String, FxAccountDevice>();
+ }
+ }
+
+ protected LoginResponse addLogin(User user, byte[] sessionToken, byte[] keyFetchToken) {
+ // byte[] sessionToken = Utils.generateRandomBytes(8);
+ if (sessionToken != null) {
+ sessionTokens.put(Utils.byte2Hex(sessionToken), user.email);
+ }
+ // byte[] keyFetchToken = Utils.generateRandomBytes(8);
+ if (keyFetchToken != null) {
+ keyFetchTokens.put(Utils.byte2Hex(keyFetchToken), user.email);
+ }
+ return new LoginResponse(user.email, user.uid, user.verified, sessionToken, keyFetchToken);
+ }
+
+ public void addUser(String email, byte[] quickStretchedPW, boolean verified, byte[] sessionToken, byte[] keyFetchToken) {
+ User user = new User(email, quickStretchedPW);
+ users.put(email, user);
+ if (verified) {
+ verifyUser(email);
+ }
+ addLogin(user, sessionToken, keyFetchToken);
+ }
+
+ public void verifyUser(String email) {
+ users.get(email).verified = true;
+ }
+
+ public void clearAllUserTokens() throws UnsupportedEncodingException {
+ sessionTokens.clear();
+ keyFetchTokens.clear();
+ }
+
+ protected BasicHttpResponse makeHttpResponse(int statusCode, String body) {
+ BasicHttpResponse httpResponse = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), statusCode, body);
+ httpResponse.setEntity(new StringEntity(body, "UTF-8"));
+ return httpResponse;
+ }
+
+ protected <T> void handleFailure(RequestDelegate<T> requestDelegate, int code, int errno, String message) {
+ requestDelegate.handleFailure(new FxAccountClientRemoteException(makeHttpResponse(code, message),
+ code, errno, "Bad authorization", message, null, new ExtendedJSONObject()));
+ }
+
+ @Override
+ public void accountStatus(String uid, RequestDelegate<AccountStatusResponse> requestDelegate) {
+ boolean userFound = false;
+ for (User user : users.values()) {
+ if (user.uid.equals(uid)) {
+ userFound = true;
+ break;
+ }
+ }
+ requestDelegate.handleSuccess(new AccountStatusResponse(userFound));
+ }
+
+ @Override
+ public void recoveryEmailStatus(byte[] sessionToken, RequestDelegate<RecoveryEmailStatusResponse> requestDelegate) {
+ String email = sessionTokens.get(Utils.byte2Hex(sessionToken));
+ User user = users.get(email);
+ if (email == null || user == null) {
+ handleFailure(requestDelegate, HttpStatus.SC_UNAUTHORIZED, FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN, "invalid sessionToken");
+ return;
+ }
+ requestDelegate.handleSuccess(new RecoveryEmailStatusResponse(email, user.verified));
+ }
+
+ @Override
+ public void keys(byte[] keyFetchToken, RequestDelegate<TwoKeys> requestDelegate) {
+ String email = keyFetchTokens.get(Utils.byte2Hex(keyFetchToken));
+ User user = users.get(email);
+ if (email == null || user == null) {
+ handleFailure(requestDelegate, HttpStatus.SC_UNAUTHORIZED, FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN, "invalid keyFetchToken");
+ return;
+ }
+ if (!user.verified) {
+ handleFailure(requestDelegate, HttpStatus.SC_BAD_REQUEST, FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT, "user is unverified");
+ return;
+ }
+ requestDelegate.handleSuccess(new TwoKeys(user.kA, user.wrapkB));
+ }
+
+ @Override
+ public void sign(byte[] sessionToken, ExtendedJSONObject publicKey, long certificateDurationInMilliseconds, RequestDelegate<String> requestDelegate) {
+ String email = sessionTokens.get(Utils.byte2Hex(sessionToken));
+ User user = users.get(email);
+ if (email == null || user == null) {
+ handleFailure(requestDelegate, HttpStatus.SC_UNAUTHORIZED, FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN, "invalid sessionToken");
+ return;
+ }
+ if (!user.verified) {
+ handleFailure(requestDelegate, HttpStatus.SC_BAD_REQUEST, FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT, "user is unverified");
+ return;
+ }
+ try {
+ final long iat = System.currentTimeMillis();
+ final long dur = certificateDurationInMilliseconds;
+ final long exp = iat + dur;
+ String certificate = mockMyIdTokenFactory.createMockMyIDCertificate(RSACryptoImplementation.createPublicKey(publicKey), "test", iat, exp);
+ requestDelegate.handleSuccess(certificate);
+ } catch (Exception e) {
+ requestDelegate.handleError(e);
+ }
+ }
+
+ @Override
+ public void registerOrUpdateDevice(byte[] sessionToken, FxAccountDevice deviceToRegister, RequestDelegate<FxAccountDevice> requestDelegate) {
+ String email = sessionTokens.get(Utils.byte2Hex(sessionToken));
+ User user = users.get(email);
+ if (email == null || user == null) {
+ handleFailure(requestDelegate, HttpStatus.SC_UNAUTHORIZED, FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN, "invalid sessionToken");
+ return;
+ }
+ if (!user.verified) {
+ handleFailure(requestDelegate, HttpStatus.SC_BAD_REQUEST, FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT, "user is unverified");
+ return;
+ }
+ try {
+ String deviceId = deviceToRegister.id;
+ if (TextUtils.isEmpty(deviceId)) { // Create
+ deviceId = UUID.randomUUID().toString();
+ FxAccountDevice device = new FxAccountDevice(deviceToRegister.name, deviceId, deviceToRegister.type, null, null, null, null);
+ requestDelegate.handleSuccess(device);
+ } else { // Update
+ FxAccountDevice existingDevice = user.devices.get(deviceId);
+ if (existingDevice != null) {
+ String deviceName = existingDevice.name;
+ if (!TextUtils.isEmpty(deviceToRegister.name)) {
+ deviceName = deviceToRegister.name;
+ } // We could also update the other fields..
+ FxAccountDevice device = new FxAccountDevice(deviceName, existingDevice.id, existingDevice.type,
+ existingDevice.isCurrentDevice, existingDevice.pushCallback, existingDevice.pushPublicKey,existingDevice.pushAuthKey);
+ requestDelegate.handleSuccess(device);
+ } else { // Device unknown
+ handleFailure(requestDelegate, HttpStatus.SC_BAD_REQUEST, FxAccountRemoteError.UNKNOWN_DEVICE, "device is unknown");
+ return;
+ }
+ }
+ } catch (Exception e) {
+ requestDelegate.handleError(e);
+ }
+ }
+
+ @Override
+ public void deviceList(byte[] sessionToken, RequestDelegate<FxAccountDevice[]> requestDelegate) {
+ String email = sessionTokens.get(Utils.byte2Hex(sessionToken));
+ User user = users.get(email);
+ if (email == null || user == null) {
+ handleFailure(requestDelegate, HttpStatus.SC_UNAUTHORIZED, FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN, "invalid sessionToken");
+ return;
+ }
+ if (!user.verified) {
+ handleFailure(requestDelegate, HttpStatus.SC_BAD_REQUEST, FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT, "user is unverified");
+ return;
+ }
+ Collection<FxAccountDevice> devices = user.devices.values();
+ FxAccountDevice[] devicesArray = devices.toArray(new FxAccountDevice[devices.size()]);
+ requestDelegate.handleSuccess(devicesArray);
+ }
+
+ @Override
+ public void notifyDevices(byte[] sessionToken, List<String> deviceIds, ExtendedJSONObject payload, Long TTL, RequestDelegate<ExtendedJSONObject> requestDelegate) {
+ requestDelegate.handleSuccess(new ExtendedJSONObject());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/TestFxAccountLoginStateMachine.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/TestFxAccountLoginStateMachine.java
new file mode 100644
index 0000000000..1496f6d79b
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/TestFxAccountLoginStateMachine.java
@@ -0,0 +1,205 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.fxa.login;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.fxa.FxAccountClient;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.browserid.RSACryptoImplementation;
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
+import org.mozilla.gecko.fxa.login.State.StateLabel;
+import org.mozilla.gecko.sync.Utils;
+
+import java.security.NoSuchAlgorithmException;
+import java.util.LinkedList;
+
+@RunWith(TestRunner.class)
+public class TestFxAccountLoginStateMachine {
+ // private static final String TEST_AUDIENCE = "http://testAudience.com";
+ private static final String TEST_EMAIL = "test@test.com";
+ private static byte[] TEST_EMAIL_UTF8;
+ private static final String TEST_PASSWORD = "testtest";
+ private static byte[] TEST_PASSWORD_UTF8;
+ private static byte[] TEST_QUICK_STRETCHED_PW;
+ private static byte[] TEST_UNWRAPKB;
+ private static final byte[] TEST_SESSION_TOKEN = Utils.generateRandomBytes(32);
+ private static final byte[] TEST_KEY_FETCH_TOKEN = Utils.generateRandomBytes(32);
+
+ protected MockFxAccountClient client;
+ protected FxAccountLoginStateMachine sm;
+
+ @Before
+ public void setUp() throws Exception {
+ if (TEST_EMAIL_UTF8 == null) {
+ TEST_EMAIL_UTF8 = TEST_EMAIL.getBytes("UTF-8");
+ }
+ if (TEST_PASSWORD_UTF8 == null) {
+ TEST_PASSWORD_UTF8 = TEST_PASSWORD.getBytes("UTF-8");
+ }
+ if (TEST_QUICK_STRETCHED_PW == null) {
+ TEST_QUICK_STRETCHED_PW = FxAccountUtils.generateQuickStretchedPW(TEST_EMAIL_UTF8, TEST_PASSWORD_UTF8);
+ }
+ if (TEST_UNWRAPKB == null) {
+ TEST_UNWRAPKB = FxAccountUtils.generateUnwrapBKey(TEST_QUICK_STRETCHED_PW);
+ }
+ client = new MockFxAccountClient();
+ sm = new FxAccountLoginStateMachine();
+ }
+
+ protected static class Trace {
+ public final LinkedList<State> states;
+ public final LinkedList<Transition> transitions;
+
+ public Trace(LinkedList<State> states, LinkedList<Transition> transitions) {
+ this.states = states;
+ this.transitions = transitions;
+ }
+
+ public void assertEquals(String string) {
+ Assert.assertArrayEquals(string.split(", "), toString().split(", "));
+ }
+
+ @Override
+ public String toString() {
+ final LinkedList<State> states = new LinkedList<State>(this.states);
+ final LinkedList<Transition> transitions = new LinkedList<Transition>(this.transitions);
+ LinkedList<String> names = new LinkedList<String>();
+ State state;
+ while ((state = states.pollFirst()) != null) {
+ names.add(state.getStateLabel().name());
+ Transition transition = transitions.pollFirst();
+ if (transition != null) {
+ names.add(">" + transition.toString());
+ }
+ }
+ return names.toString();
+ }
+
+ public String stateString() {
+ LinkedList<String> names = new LinkedList<String>();
+ for (State state : states) {
+ names.add(state.getStateLabel().name());
+ }
+ return names.toString();
+ }
+
+ public String transitionString() {
+ LinkedList<String> names = new LinkedList<String>();
+ for (Transition transition : transitions) {
+ names.add(transition.toString());
+ }
+ return names.toString();
+ }
+ }
+
+ protected Trace trace(final State initialState, final StateLabel desiredState) {
+ final LinkedList<Transition> transitions = new LinkedList<Transition>();
+ final LinkedList<State> states = new LinkedList<State>();
+ states.add(initialState);
+
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ sm.advance(initialState, desiredState, new LoginStateMachineDelegate() {
+ @Override
+ public void handleTransition(Transition transition, State state) {
+ transitions.add(transition);
+ states.add(state);
+ }
+
+ @Override
+ public void handleFinal(State state) {
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ @Override
+ public FxAccountClient getClient() {
+ return client;
+ }
+
+ @Override
+ public long getCertificateDurationInMilliseconds() {
+ return 30 * 1000;
+ }
+
+ @Override
+ public long getAssertionDurationInMilliseconds() {
+ return 10 * 1000;
+ }
+
+ @Override
+ public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
+ return RSACryptoImplementation.generateKeyPair(512);
+ }
+ });
+ }
+ });
+
+ return new Trace(states, transitions);
+ }
+
+ @Test
+ public void testEnagedUnverified() throws Exception {
+ client.addUser(TEST_EMAIL, TEST_QUICK_STRETCHED_PW, false, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN);
+ Trace trace = trace(new Engaged(TEST_EMAIL, "uid", true, TEST_UNWRAPKB, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN), StateLabel.Married);
+ trace.assertEquals("[Engaged, >AccountNeedsVerification, Engaged]");
+ }
+
+ @Test
+ public void testEngagedTransitionToAccountVerified() throws Exception {
+ client.addUser(TEST_EMAIL, TEST_QUICK_STRETCHED_PW, true, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN);
+ Trace trace = trace(new Engaged(TEST_EMAIL, "uid", false, TEST_UNWRAPKB, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN), StateLabel.Married);
+ trace.assertEquals("[Engaged, >AccountVerified, Cohabiting, >LogMessage('sign succeeded'), Married]");
+ }
+
+ @Test
+ public void testEngagedVerified() throws Exception {
+ client.addUser(TEST_EMAIL, TEST_QUICK_STRETCHED_PW, true, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN);
+ Trace trace = trace(new Engaged(TEST_EMAIL, "uid", true, TEST_UNWRAPKB, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN), StateLabel.Married);
+ trace.assertEquals("[Engaged, >LogMessage('keys succeeded'), Cohabiting, >LogMessage('sign succeeded'), Married]");
+ }
+
+ @Test
+ public void testPartial() throws Exception {
+ client.addUser(TEST_EMAIL, TEST_QUICK_STRETCHED_PW, true, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN);
+ // What if we stop at Cohabiting?
+ Trace trace = trace(new Engaged(TEST_EMAIL, "uid", true, TEST_UNWRAPKB, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN), StateLabel.Cohabiting);
+ trace.assertEquals("[Engaged, >LogMessage('keys succeeded'), Cohabiting]");
+ }
+
+ @Test
+ public void testBadSessionToken() throws Exception {
+ client.addUser(TEST_EMAIL, TEST_QUICK_STRETCHED_PW, true, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN);
+ client.sessionTokens.clear();
+ Trace trace = trace(new Engaged(TEST_EMAIL, "uid", true, TEST_UNWRAPKB, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN), StateLabel.Married);
+ trace.assertEquals("[Engaged, >LogMessage('keys succeeded'), Cohabiting, >Log(<FxAccountClientRemoteException 401 [110]: invalid sessionToken>), Separated, >PasswordRequired, Separated]");
+ }
+
+ @Test
+ public void testBadKeyFetchToken() throws Exception {
+ client.addUser(TEST_EMAIL, TEST_QUICK_STRETCHED_PW, true, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN);
+ client.keyFetchTokens.clear();
+ Trace trace = trace(new Engaged(TEST_EMAIL, "uid", true, TEST_UNWRAPKB, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN), StateLabel.Married);
+ trace.assertEquals("[Engaged, >Log(<FxAccountClientRemoteException 401 [110]: invalid keyFetchToken>), Separated, >PasswordRequired, Separated]");
+ }
+
+ @Test
+ public void testMarried() throws Exception {
+ client.addUser(TEST_EMAIL, TEST_QUICK_STRETCHED_PW, true, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN);
+ Trace trace = trace(new Engaged(TEST_EMAIL, "uid", true, TEST_UNWRAPKB, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN), StateLabel.Married);
+ trace.assertEquals("[Engaged, >LogMessage('keys succeeded'), Cohabiting, >LogMessage('sign succeeded'), Married]");
+ // What if we're already in the desired state?
+ State married = trace.states.getLast();
+ Assert.assertEquals(StateLabel.Married, married.getStateLabel());
+ trace = trace(married, StateLabel.Married);
+ trace.assertEquals("[Married]");
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/TestStateFactory.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/TestStateFactory.java
new file mode 100644
index 0000000000..80d7d7f9f3
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/TestStateFactory.java
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.fxa.login;
+
+import junit.framework.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.browserid.DSACryptoImplementation;
+import org.mozilla.gecko.fxa.login.State.StateLabel;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.Utils;
+
+@RunWith(TestRunner.class)
+public class TestStateFactory {
+ @Test
+ public void testGetStateV3() throws Exception {
+ MigratedFromSync11 migrated = new MigratedFromSync11("email", "uid", true, "password");
+
+ // For the current version, we expect to read back what we wrote.
+ ExtendedJSONObject o;
+ State state;
+
+ o = migrated.toJSONObject();
+ Assert.assertEquals(3, o.getLong("version").intValue());
+ state = StateFactory.fromJSONObject(migrated.stateLabel, o);
+ Assert.assertEquals(StateLabel.MigratedFromSync11, state.stateLabel);
+ Assert.assertEquals(o, state.toJSONObject());
+
+ // Null passwords are OK.
+ MigratedFromSync11 migratedNullPassword = new MigratedFromSync11("email", "uid", true, null);
+
+ o = migratedNullPassword.toJSONObject();
+ Assert.assertEquals(3, o.getLong("version").intValue());
+ state = StateFactory.fromJSONObject(migratedNullPassword.stateLabel, o);
+ Assert.assertEquals(StateLabel.MigratedFromSync11, state.stateLabel);
+ Assert.assertEquals(o, state.toJSONObject());
+ }
+
+ @Test
+ public void testGetStateV2() throws Exception {
+ byte[] sessionToken = Utils.generateRandomBytes(32);
+ byte[] kA = Utils.generateRandomBytes(32);
+ byte[] kB = Utils.generateRandomBytes(32);
+ BrowserIDKeyPair keyPair = DSACryptoImplementation.generateKeyPair(512);
+ Cohabiting cohabiting = new Cohabiting("email", "uid", sessionToken, kA, kB, keyPair);
+ String certificate = "certificate";
+ Married married = new Married("email", "uid", sessionToken, kA, kB, keyPair, certificate);
+
+ // For the current version, we expect to read back what we wrote.
+ ExtendedJSONObject o;
+ State state;
+
+ o = married.toJSONObject();
+ Assert.assertEquals(3, o.getLong("version").intValue());
+ state = StateFactory.fromJSONObject(married.stateLabel, o);
+ Assert.assertEquals(StateLabel.Married, state.stateLabel);
+ Assert.assertEquals(o, state.toJSONObject());
+
+ o = cohabiting.toJSONObject();
+ Assert.assertEquals(3, o.getLong("version").intValue());
+ state = StateFactory.fromJSONObject(cohabiting.stateLabel, o);
+ Assert.assertEquals(StateLabel.Cohabiting, state.stateLabel);
+ Assert.assertEquals(o, state.toJSONObject());
+ }
+
+ @Test
+ public void testGetStateV1() throws Exception {
+ // We can't rely on generating correct V1 objects (since the generation code
+ // may change); so we hard code a few test examples here. These examples
+ // have RSA key pairs; when they're parsed, we return DSA key pairs.
+ ExtendedJSONObject o = new ExtendedJSONObject("{\"uid\":\"uid\",\"sessionToken\":\"4e2830da6ce466ddb401fbca25b96a621209eea83851254800f84cc4069ef011\",\"certificate\":\"certificate\",\"keyPair\":{\"publicKey\":{\"e\":\"65537\",\"n\":\"7598360104379019497828904063491254083855849024432238665262988260947462372141971045236693389494635158997975098558915846889960089362159921622822266839560631\",\"algorithm\":\"RS\"},\"privateKey\":{\"d\":\"6807533330618101360064115400338014782301295929300445938471117364691566605775022173055292460962170873583673516346599808612503093914221141089102289381448225\",\"n\":\"7598360104379019497828904063491254083855849024432238665262988260947462372141971045236693389494635158997975098558915846889960089362159921622822266839560631\",\"algorithm\":\"RS\"}},\"email\":\"email\",\"verified\":true,\"kB\":\"0b048f285c19067f200da7bfbe734ed213cefcd8f543f0fdd4a8ccab48cbbc89\",\"kA\":\"59a9edf2d41de8b24e69df9133bc88e96913baa75421882f4c55d842d18fc8a1\",\"version\":1}");
+ // A Married state is regressed to a Cohabited state.
+ Cohabiting state = (Cohabiting) StateFactory.fromJSONObject(StateLabel.Married, o);
+
+ Assert.assertEquals(StateLabel.Cohabiting, state.stateLabel);
+ Assert.assertEquals("uid", state.uid);
+ Assert.assertEquals("4e2830da6ce466ddb401fbca25b96a621209eea83851254800f84cc4069ef011", Utils.byte2Hex(state.sessionToken));
+ Assert.assertEquals("DS128", state.keyPair.getPrivate().getAlgorithm());
+
+ o = new ExtendedJSONObject("{\"uid\":\"uid\",\"sessionToken\":\"4e2830da6ce466ddb401fbca25b96a621209eea83851254800f84cc4069ef011\",\"keyPair\":{\"publicKey\":{\"e\":\"65537\",\"n\":\"7598360104379019497828904063491254083855849024432238665262988260947462372141971045236693389494635158997975098558915846889960089362159921622822266839560631\",\"algorithm\":\"RS\"},\"privateKey\":{\"d\":\"6807533330618101360064115400338014782301295929300445938471117364691566605775022173055292460962170873583673516346599808612503093914221141089102289381448225\",\"n\":\"7598360104379019497828904063491254083855849024432238665262988260947462372141971045236693389494635158997975098558915846889960089362159921622822266839560631\",\"algorithm\":\"RS\"}},\"email\":\"email\",\"verified\":true,\"kB\":\"0b048f285c19067f200da7bfbe734ed213cefcd8f543f0fdd4a8ccab48cbbc89\",\"kA\":\"59a9edf2d41de8b24e69df9133bc88e96913baa75421882f4c55d842d18fc8a1\",\"version\":1}");
+ state = (Cohabiting) StateFactory.fromJSONObject(StateLabel.Cohabiting, o);
+
+ Assert.assertEquals(StateLabel.Cohabiting, state.stateLabel);
+ Assert.assertEquals("uid", state.uid);
+ Assert.assertEquals("4e2830da6ce466ddb401fbca25b96a621209eea83851254800f84cc4069ef011", Utils.byte2Hex(state.sessionToken));
+ Assert.assertEquals("DS128", state.keyPair.getPrivate().getAlgorithm());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/helpers/AssertUtil.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/helpers/AssertUtil.java
new file mode 100644
index 0000000000..8102bf1eec
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/helpers/AssertUtil.java
@@ -0,0 +1,29 @@
+/* -*- 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.helpers;
+
+import org.junit.Assert;
+
+/**
+ * Some additional assert methods on top of org.junit.Assert.
+ */
+public class AssertUtil {
+ /**
+ * Asserts that the String {@code text} contains the String {@code sequence}. If it doesn't then
+ * an {@link AssertionError} will be thrown.
+ */
+ public static void assertContains(String text, String sequence) {
+ Assert.assertTrue(text.contains(sequence));
+ }
+
+ /**
+ * Asserts that the String {@code text} contains not the String {@code sequence}. If it does
+ * then an {@link AssertionError} will be thrown.
+ */
+ public static void assertContainsNot(String text, String sequence) {
+ Assert.assertFalse(text.contains(sequence));
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/home/TestHomeConfigPrefsBackendMigration.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/home/TestHomeConfigPrefsBackendMigration.java
new file mode 100644
index 0000000000..b6f12a05ed
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/home/TestHomeConfigPrefsBackendMigration.java
@@ -0,0 +1,264 @@
+/* -*- 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.Pair;
+import android.util.SparseArray;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.home.HomeConfig.PanelConfig;
+import org.mozilla.gecko.home.HomeConfig.PanelType;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.Assert.*;
+
+@RunWith(TestRunner.class)
+public class TestHomeConfigPrefsBackendMigration {
+
+ // Each Pair consists of a list of panels that exist going into a given migration, and a list containing
+ // the expected default output panel corresponding to each given input panel in the list of existing panels.
+ // E.g. if a given N->N+1 migration starts with panels Foo and Bar, and removes Bar, the two lists would
+ // be {Foo, Bar} and {Foo, Foo}.
+ // Note: the index where each pair is inserted corresponds to the HomeConfig version before the migration.
+ // The final item in this list denotes the current HomeCOnfig version, and therefore only needs to contain
+ // the list of panel types that are expected by default (but no list for after the non-existent migration).
+ final SparseArray<Pair<PanelType[], PanelType[]>> migrationConstellations = new SparseArray<>();
+ {
+ // 6->7: the recent tabs panel was merged into the combined history panel
+ migrationConstellations.put(6, new Pair<>(
+ /* Panels that are expected to exist before this migration happens */
+ new PanelType[] {
+ PanelType.TOP_SITES,
+ PanelType.BOOKMARKS,
+ PanelType.COMBINED_HISTORY,
+ PanelType.DEPRECATED_RECENT_TABS
+ },
+ /* The expected default panel that is expected after the migration */
+ new PanelType[] {
+ PanelType.TOP_SITES, /* TOP_SITES remains the default if it was previously the default */
+ PanelType.BOOKMARKS, /* same as TOP_SITES */
+ PanelType.COMBINED_HISTORY, /* same as TOP_SITES */
+ PanelType.COMBINED_HISTORY /* DEPRECATED_RECENT_TABS is replaced by COMBINED_HISTORY during this migration and is therefore the new default */
+ }
+ ));
+
+ // 7->8: no changes, this was a fixup migration since 6->7 was previously botched
+ migrationConstellations.put(7, new Pair<>(
+ new PanelType[] {
+ PanelType.TOP_SITES,
+ PanelType.BOOKMARKS,
+ PanelType.COMBINED_HISTORY,
+ },
+ new PanelType[] {
+ PanelType.TOP_SITES,
+ PanelType.BOOKMARKS,
+ PanelType.COMBINED_HISTORY,
+ }
+ ));
+
+ migrationConstellations.put(8, new Pair<>(
+ new PanelType[] {
+ PanelType.TOP_SITES,
+ PanelType.BOOKMARKS,
+ PanelType.COMBINED_HISTORY,
+ },
+ new PanelType[] {
+ // Last version: no migration exists yet, we only need to define a list
+ // of expected panels.
+ }
+ ));
+ }
+
+ private JSONArray createDisabledConfigsForList(Context context,
+ PanelType[] panels) throws JSONException {
+ final JSONArray jsonPanels = new JSONArray();
+
+ for (int i = 0; i < panels.length; i++) {
+ final PanelType panel = panels[i];
+
+ jsonPanels.put(HomeConfig.createBuiltinPanelConfig(context, panel,
+ EnumSet.of(PanelConfig.Flags.DISABLED_PANEL)).toJSON());
+ }
+
+ return jsonPanels;
+
+ }
+
+
+ private JSONArray createConfigsForList(Context context, PanelType[] panels,
+ int defaultIndex) throws JSONException {
+ if (defaultIndex < 0 || defaultIndex >= panels.length) {
+ throw new IllegalArgumentException("defaultIndex must point to panel in the array");
+ }
+
+ final JSONArray jsonPanels = new JSONArray();
+
+ for (int i = 0; i < panels.length; i++) {
+ final PanelType panel = panels[i];
+ final PanelConfig config;
+
+ if (i == defaultIndex) {
+ config = HomeConfig.createBuiltinPanelConfig(context, panel,
+ EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL));
+ } else {
+ config = HomeConfig.createBuiltinPanelConfig(context, panel);
+ }
+
+ jsonPanels.put(config.toJSON());
+ }
+
+ return jsonPanels;
+ }
+
+ private PanelType getDefaultPanel(final JSONArray jsonPanels) throws JSONException {
+ assertTrue("panel list must not be empty", jsonPanels.length() > 0);
+
+ for (int i = 0; i < jsonPanels.length(); i++) {
+ final JSONObject jsonPanelConfig = jsonPanels.getJSONObject(i);
+ final PanelConfig panelConfig = new PanelConfig(jsonPanelConfig);
+
+ if (panelConfig.isDefault()) {
+ return panelConfig.getType();
+ }
+ }
+
+ return null;
+ }
+
+ private void checkAllPanelsAreDisabled(JSONArray jsonPanels) throws JSONException {
+ for (int i = 0; i < jsonPanels.length(); i++) {
+ final JSONObject jsonPanelConfig = jsonPanels.getJSONObject(i);
+ final PanelConfig config = new PanelConfig(jsonPanelConfig);
+
+ assertTrue("Non disabled panel \"" + config.getType().name() + "\" found in list, excpected all panels to be disabled", config.isDisabled());
+ }
+ }
+
+ private void checkListContainsExpectedPanels(JSONArray jsonPanels,
+ PanelType[] expected) throws JSONException {
+ // Given the short lists we have here an ArraySet might be more appropriate, but it requires API >= 23.
+ final Set<PanelType> expectedSet = new HashSet<>();
+ for (PanelType panelType : expected) {
+ expectedSet.add(panelType);
+ }
+
+ for (int i = 0; i < jsonPanels.length(); i++) {
+ final JSONObject jsonPanelConfig = jsonPanels.getJSONObject(i);
+ final PanelType panelType = new PanelConfig(jsonPanelConfig).getType();
+
+ assertTrue("Unexpected panel of type " + panelType.name() + " found in list",
+ expectedSet.contains(panelType));
+
+ expectedSet.remove(panelType);
+ }
+
+ assertEquals("Expected panels not contained in list",
+ 0, expectedSet.size());
+ }
+
+ @Test
+ public void testMigrationRetainsDefaultAfter6() throws JSONException {
+ final Context context = RuntimeEnvironment.application;
+
+ final Pair<PanelType[], PanelType[]> finalConstellation = migrationConstellations.get(HomeConfigPrefsBackend.VERSION);
+ assertNotNull("It looks like you added a HomeConfig migration, please add an appropriate entry to migrationConstellations",
+ finalConstellation);
+
+ // We want to calculate the number of iterations here to make sure we cover all provided constellations.
+ // Iterating over the array and manually checking for each version could result in constellations
+ // being skipped if there are any gaps in the array
+ final int firstTestedVersion = HomeConfigPrefsBackend.VERSION - (migrationConstellations.size() - 1);
+
+ // The last constellation is only used for the counts / expected outputs, hence we start
+ // with the second-last constellation
+ for (int testVersion = HomeConfigPrefsBackend.VERSION - 1; testVersion >= firstTestedVersion; testVersion--) {
+
+ final Pair<PanelType[], PanelType[]> currentConstellation = migrationConstellations.get(testVersion);
+ assertNotNull("No constellation for version " + testVersion + " - you must provide a constellation for every version upgrade in the list",
+ currentConstellation);
+
+ final PanelType[] inputList = currentConstellation.first;
+ final PanelType[] expectedDefaults = currentConstellation.second;
+
+ for (int i = 0; i < inputList.length; i++) {
+ JSONArray jsonPanels = createConfigsForList(context, inputList, i);
+
+
+ // Verify that we still have a default panel, and that it is the expected default panel
+
+ // No need to pass in the prefsEditor since that is only used for the 0->1 migration
+ jsonPanels = HomeConfigPrefsBackend.migratePrefsFromVersionToVersion(context, testVersion, testVersion + 1, jsonPanels, null);
+
+ final PanelType oldDefaultPanelType = inputList[i];
+ final PanelType expectedNewDefaultPanelType = expectedDefaults[i];
+ final PanelType newDefaultPanelType = getDefaultPanel(jsonPanels);
+
+ assertNotNull("No default panel set when migrating from " + testVersion + " to " + testVersion + 1 + ", with previous default as " + oldDefaultPanelType.name(),
+ newDefaultPanelType);
+
+ assertEquals("Migration changed to unexpected default panel - migrating from " + oldDefaultPanelType.name() + ", expected " + expectedNewDefaultPanelType.name() + " but got " + newDefaultPanelType.name(),
+ newDefaultPanelType, expectedNewDefaultPanelType);
+
+
+ // Verify that the panels remaining after the migration correspond to the input panels
+ // for the next migration
+ final PanelType[] expectedOutputList = migrationConstellations.get(testVersion + 1).first;
+
+ assertEquals("Number of panels after migration doesn't match expected count",
+ jsonPanels.length(), expectedOutputList.length);
+
+ checkListContainsExpectedPanels(jsonPanels, expectedOutputList);
+ }
+ }
+ }
+
+ // Test that if all panels are disabled, the migration retains all panels as being disabled
+ // (in addition to correctly removing panels as necessary).
+ @Test
+ public void testMigrationRetainsAllPanelsHiddenAfter6() throws JSONException {
+ final Context context = RuntimeEnvironment.application;
+
+ final Pair<PanelType[], PanelType[]> finalConstellation = migrationConstellations.get(HomeConfigPrefsBackend.VERSION);
+ assertNotNull("It looks like you added a HomeConfig migration, please add an appropriate entry to migrationConstellations",
+ finalConstellation);
+
+ final int firstTestedVersion = HomeConfigPrefsBackend.VERSION - (migrationConstellations.size() - 1);
+
+ for (int testVersion = HomeConfigPrefsBackend.VERSION - 1; testVersion >= firstTestedVersion; testVersion--) {
+ final Pair<PanelType[], PanelType[]> currentConstellation = migrationConstellations.get(testVersion);
+ assertNotNull("No constellation for version " + testVersion + " - you must provide a constellation for every version upgrade in the list",
+ currentConstellation);
+
+ final PanelType[] inputList = currentConstellation.first;
+
+ JSONArray jsonPanels = createDisabledConfigsForList(context, inputList);
+
+ jsonPanels = HomeConfigPrefsBackend.migratePrefsFromVersionToVersion(context, testVersion, testVersion + 1, jsonPanels, null);
+
+ // All panels should remain disabled after the migration
+ checkAllPanelsAreDisabled(jsonPanels);
+
+ // Duplicated from previous test:
+ // Verify that the panels remaining after the migration correspond to the input panels
+ // for the next migration
+ final PanelType[] expectedOutputList = migrationConstellations.get(testVersion + 1).first;
+
+ assertEquals("Number of panels after migration doesn't match expected count",
+ jsonPanels.length(), expectedOutputList.length);
+
+ checkListContainsExpectedPanels(jsonPanels, expectedOutputList);
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconDescriptor.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconDescriptor.java
new file mode 100644
index 0000000000..05e4576e52
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconDescriptor.java
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.icons;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+@RunWith(TestRunner.class)
+public class TestIconDescriptor {
+ private static final String ICON_URL = "https://www.mozilla.org/favicon.ico";
+ private static final String MIME_TYPE = "image/png";
+ private static final int ICON_SIZE = 64;
+
+ @Test
+ public void testGenericIconDescriptor() {
+ final IconDescriptor descriptor = IconDescriptor.createGenericIcon(ICON_URL);
+
+ Assert.assertEquals(ICON_URL, descriptor.getUrl());
+ Assert.assertNull(descriptor.getMimeType());
+ Assert.assertEquals(0, descriptor.getSize());
+ Assert.assertEquals(IconDescriptor.TYPE_GENERIC, descriptor.getType());
+ }
+
+ @Test
+ public void testFaviconIconDescriptor() {
+ final IconDescriptor descriptor = IconDescriptor.createFavicon(ICON_URL, ICON_SIZE, MIME_TYPE);
+
+ Assert.assertEquals(ICON_URL, descriptor.getUrl());
+ Assert.assertEquals(MIME_TYPE, descriptor.getMimeType());
+ Assert.assertEquals(ICON_SIZE, descriptor.getSize());
+ Assert.assertEquals(IconDescriptor.TYPE_FAVICON, descriptor.getType());
+ }
+
+ @Test
+ public void testTouchIconDescriptor() {
+ final IconDescriptor descriptor = IconDescriptor.createTouchicon(ICON_URL, ICON_SIZE, MIME_TYPE);
+
+ Assert.assertEquals(ICON_URL, descriptor.getUrl());
+ Assert.assertEquals(MIME_TYPE, descriptor.getMimeType());
+ Assert.assertEquals(ICON_SIZE, descriptor.getSize());
+ Assert.assertEquals(IconDescriptor.TYPE_TOUCHICON, descriptor.getType());
+ }
+
+ @Test
+ public void testLookupIconDescriptor() {
+ final IconDescriptor descriptor = IconDescriptor.createLookupIcon(ICON_URL);
+
+ Assert.assertEquals(ICON_URL, descriptor.getUrl());
+ Assert.assertNull(descriptor.getMimeType());
+ Assert.assertEquals(0, descriptor.getSize());
+ Assert.assertEquals(IconDescriptor.TYPE_LOOKUP, descriptor.getType());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconDescriptorComparator.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconDescriptorComparator.java
new file mode 100644
index 0000000000..1f4664d08d
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconDescriptorComparator.java
@@ -0,0 +1,152 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.icons;
+
+import org.junit.Assert;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+import java.util.TreeSet;
+
+@RunWith(TestRunner.class)
+public class TestIconDescriptorComparator {
+ private static final String TEST_ICON_URL_1 = "http://www.mozilla.org/favicon.ico";
+ private static final String TEST_ICON_URL_2 = "http://www.example.org/favicon.ico";
+ private static final String TEST_ICON_URL_3 = "http://www.example.com/favicon.ico";
+
+ private static final String TEST_MIME_TYPE = "image/png";
+ private static final int TEST_SIZE = 32;
+
+ @Test
+ public void testIconsWithTheSameUrlAreTreatedAsEqual() {
+ final IconDescriptor descriptor1 = IconDescriptor.createGenericIcon(TEST_ICON_URL_1);
+ final IconDescriptor descriptor2 = IconDescriptor.createGenericIcon(TEST_ICON_URL_1);
+
+ final IconDescriptorComparator comparator = new IconDescriptorComparator();
+
+ Assert.assertEquals(0, comparator.compare(descriptor1, descriptor2));
+ Assert.assertEquals(0, comparator.compare(descriptor2, descriptor1));
+ }
+
+ @Test
+ public void testTouchIconsAreRankedHigherThanFavicons() {
+ final IconDescriptor faviconDescriptor = IconDescriptor.createFavicon(TEST_ICON_URL_1, TEST_SIZE, TEST_MIME_TYPE);
+ final IconDescriptor touchIconDescriptor = IconDescriptor.createTouchicon(TEST_ICON_URL_2, TEST_SIZE, TEST_MIME_TYPE);
+
+ final IconDescriptorComparator comparator = new IconDescriptorComparator();
+
+ Assert.assertEquals(1, comparator.compare(faviconDescriptor, touchIconDescriptor));
+ Assert.assertEquals(-1, comparator.compare(touchIconDescriptor, faviconDescriptor));
+ }
+
+ @Test
+ public void testFaviconsAndTouchIconsAreRankedHigherThanGenericIcons() {
+ final IconDescriptor genericDescriptor = IconDescriptor.createGenericIcon(TEST_ICON_URL_1);
+
+ final IconDescriptor faviconDescriptor = IconDescriptor.createFavicon(TEST_ICON_URL_2, TEST_SIZE, TEST_MIME_TYPE);
+ final IconDescriptor touchIconDescriptor = IconDescriptor.createTouchicon(TEST_ICON_URL_3, TEST_SIZE, TEST_MIME_TYPE);
+
+ final IconDescriptorComparator comparator = new IconDescriptorComparator();
+
+ Assert.assertEquals(1, comparator.compare(genericDescriptor, faviconDescriptor));
+ Assert.assertEquals(-1, comparator.compare(faviconDescriptor, genericDescriptor));
+
+ Assert.assertEquals(1, comparator.compare(genericDescriptor, touchIconDescriptor));
+ Assert.assertEquals(-1, comparator.compare(touchIconDescriptor, genericDescriptor));
+ }
+
+ @Test
+ public void testLookupIconsAreRankedHigherThanGenericIcons() {
+ final IconDescriptor genericDescriptor = IconDescriptor.createGenericIcon(TEST_ICON_URL_1);
+ final IconDescriptor lookupDescriptor = IconDescriptor.createLookupIcon(TEST_ICON_URL_2);
+
+ final IconDescriptorComparator comparator = new IconDescriptorComparator();
+
+ Assert.assertEquals(1, comparator.compare(genericDescriptor, lookupDescriptor));
+ Assert.assertEquals(-1, comparator.compare(lookupDescriptor, genericDescriptor));
+ }
+
+ @Test
+ public void testFaviconsAndTouchIconsAreRankedHigherThanLookupIcons() {
+ final IconDescriptor lookupDescriptor = IconDescriptor.createLookupIcon(TEST_ICON_URL_1);
+
+ final IconDescriptor faviconDescriptor = IconDescriptor.createFavicon(TEST_ICON_URL_2, TEST_SIZE, TEST_MIME_TYPE);
+ final IconDescriptor touchIconDescriptor = IconDescriptor.createTouchicon(TEST_ICON_URL_3, TEST_SIZE, TEST_MIME_TYPE);
+
+ final IconDescriptorComparator comparator = new IconDescriptorComparator();
+
+ Assert.assertEquals(1, comparator.compare(lookupDescriptor, faviconDescriptor));
+ Assert.assertEquals(-1, comparator.compare(faviconDescriptor, lookupDescriptor));
+
+ Assert.assertEquals(1, comparator.compare(lookupDescriptor, touchIconDescriptor));
+ Assert.assertEquals(-1, comparator.compare(touchIconDescriptor, lookupDescriptor));
+ }
+
+ @Test
+ public void testLargestIconOfSameTypeIsSelected() {
+ final IconDescriptor smallDescriptor = IconDescriptor.createFavicon(TEST_ICON_URL_1, 16, TEST_MIME_TYPE);
+ final IconDescriptor largeDescriptor = IconDescriptor.createFavicon(TEST_ICON_URL_2, 128, TEST_MIME_TYPE);
+
+ final IconDescriptorComparator comparator = new IconDescriptorComparator();
+
+ Assert.assertEquals(1, comparator.compare(smallDescriptor, largeDescriptor));
+ Assert.assertEquals(-1, comparator.compare(largeDescriptor, smallDescriptor));
+ }
+
+ @Test
+ public void testContainerTypesArePreferred() {
+ final IconDescriptor containerDescriptor = IconDescriptor.createFavicon(TEST_ICON_URL_1, TEST_SIZE, "image/x-icon");
+ final IconDescriptor faviconDescriptor = IconDescriptor.createFavicon(TEST_ICON_URL_2, TEST_SIZE, "image/png");
+
+ final IconDescriptorComparator comparator = new IconDescriptorComparator();
+
+ Assert.assertEquals(1, comparator.compare(faviconDescriptor, containerDescriptor));
+ Assert.assertEquals(-1, comparator.compare(containerDescriptor, faviconDescriptor));
+ }
+
+ @Test
+ public void testWithNoDifferences() {
+ final IconDescriptor descriptor1 = IconDescriptor.createFavicon(TEST_ICON_URL_1, TEST_SIZE, TEST_MIME_TYPE);
+ final IconDescriptor descriptor2 = IconDescriptor.createFavicon(TEST_ICON_URL_2, TEST_SIZE, TEST_MIME_TYPE);
+
+ final IconDescriptorComparator comparator = new IconDescriptorComparator();
+
+ Assert.assertNotEquals(0, comparator.compare(descriptor1, descriptor2));
+ Assert.assertNotEquals(0, comparator.compare(descriptor2, descriptor1));
+ }
+
+ @Test
+ public void testWithSameObject() {
+ final IconDescriptor descriptor = IconDescriptor.createTouchicon(TEST_ICON_URL_1, TEST_SIZE, TEST_MIME_TYPE);
+
+ final IconDescriptorComparator comparator = new IconDescriptorComparator();
+ Assert.assertEquals(0, comparator.compare(descriptor, descriptor));
+ }
+
+ /**
+ * This test reconstructs the scenario from bug 1331808. A comparator implementation that does
+ * not return a consistent order can break the implementation of remove() of the TreeSet class.
+ */
+ @Test
+ public void testBug1331808() {
+ TreeSet<IconDescriptor> set = new TreeSet<>(new IconDescriptorComparator());
+
+ set.add(IconDescriptor.createFavicon("http://example.org/new-logo32.jpg", 0, ""));
+ set.add(IconDescriptor.createTouchicon("http://example.org/new-logo57.jpg", 0, ""));
+ set.add(IconDescriptor.createTouchicon("http://example.org/new-logo76.jpg", 76, ""));
+ set.add(IconDescriptor.createTouchicon("http://example.org/new-logo120.jpg", 120, ""));
+ set.add(IconDescriptor.createTouchicon("http://example.org/new-logo152.jpg", 114, ""));
+ set.add(IconDescriptor.createFavicon("http://example.org/02.png", 32, ""));
+ set.add(IconDescriptor.createFavicon("http://example.org/01.png", 192, ""));
+ set.add(IconDescriptor.createTouchicon("http://example.org/03.png", 0, ""));
+
+ for (int i = 8; i > 0; i--) {
+ Assert.assertEquals("items in set before deleting: " + i, i, set.size());
+ Assert.assertTrue("item removed successfully: " + i, set.remove(set.first()));
+ Assert.assertEquals("items in set after deleting: " + i, i - 1, set.size());
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconRequest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconRequest.java
new file mode 100644
index 0000000000..d77ad6a534
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconRequest.java
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.icons;
+
+import junit.framework.Assert;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.TreeSet;
+
+import static org.hamcrest.Matchers.any;
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+@RunWith(TestRunner.class)
+public class TestIconRequest {
+ private static final String TEST_PAGE_URL = "http://www.mozilla.org";
+ private static final String TEST_ICON_URL_1 = "http://www.mozilla.org/favicon.ico";
+ private static final String TEST_ICON_URL_2 = "http://www.example.org/favicon.ico";
+
+ @Test
+ public void testIconHandling() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .build();
+
+ Assert.assertEquals(0, request.getIconCount());
+ Assert.assertFalse(request.hasIconDescriptors());
+
+ request.modify()
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL_1))
+ .deferBuild();
+
+ Assert.assertEquals(1, request.getIconCount());
+ Assert.assertTrue(request.hasIconDescriptors());
+
+ request.modify()
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL_2))
+ .deferBuild();
+
+ Assert.assertEquals(2, request.getIconCount());
+ Assert.assertTrue(request.hasIconDescriptors());
+
+ Assert.assertEquals(TEST_ICON_URL_2, request.getBestIcon().getUrl());
+
+ request.moveToNextIcon();
+
+ Assert.assertEquals(1, request.getIconCount());
+ Assert.assertTrue(request.hasIconDescriptors());
+
+ Assert.assertEquals(TEST_ICON_URL_1, request.getBestIcon().getUrl());
+
+ request.moveToNextIcon();
+
+ Assert.assertEquals(0, request.getIconCount());
+ Assert.assertFalse(request.hasIconDescriptors());
+ }
+
+ /**
+ * If removing an icon from the internal set failed then we want to throw an exception.
+ */
+ @Test(expected = IllegalStateException.class)
+ public void testMoveToNextIconThrowsException() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .build();
+
+ //noinspection unchecked - Creating a mock of a generic type
+ request.icons = (TreeSet<IconDescriptor>) mock(TreeSet.class);
+
+ //noinspection SuspiciousMethodCalls
+ doReturn(false).when(request.icons).remove(anyObject());
+
+ request.moveToNextIcon();
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconRequestBuilder.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconRequestBuilder.java
new file mode 100644
index 0000000000..0743b42d8e
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconRequestBuilder.java
@@ -0,0 +1,159 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.icons;
+
+import org.junit.Assert;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(TestRunner.class)
+public class TestIconRequestBuilder {
+ private static final String TEST_PAGE_URL_1 = "http://www.mozilla.org";
+ private static final String TEST_PAGE_URL_2 = "http://www.example.org";
+ private static final String TEST_ICON_URL_1 = "http://www.mozilla.org/favicon.ico";
+ private static final String TEST_ICON_URL_2 = "http://www.example.org/favicon.ico";
+
+ @Test
+ public void testPrivileged() {
+ IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL_1)
+ .build();
+
+ Assert.assertFalse(request.isPrivileged());
+
+ request.modify()
+ .privileged(true)
+ .deferBuild();
+
+ Assert.assertTrue(request.isPrivileged());
+ }
+
+ @Test
+ public void testPageUrl() {
+ IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL_1)
+ .build();
+
+ Assert.assertEquals(TEST_PAGE_URL_1, request.getPageUrl());
+
+ request.modify()
+ .pageUrl(TEST_PAGE_URL_2)
+ .deferBuild();
+
+ Assert.assertEquals(TEST_PAGE_URL_2, request.getPageUrl());
+ }
+
+ @Test
+ public void testIcons() {
+ // Initially a request is empty.
+ IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL_1)
+ .build();
+
+ Assert.assertEquals(0, request.getIconCount());
+
+ // Adding one icon URL.
+ request.modify()
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL_1))
+ .deferBuild();
+
+ Assert.assertEquals(1, request.getIconCount());
+
+ // Adding the same icon URL again is ignored.
+ request.modify()
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL_1))
+ .deferBuild();
+
+ Assert.assertEquals(1, request.getIconCount());
+
+ // Adding another new icon URL.
+ request.modify()
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL_2))
+ .deferBuild();
+
+ Assert.assertEquals(2, request.getIconCount());
+ }
+
+ @Test
+ public void testSkipNetwork() {
+ IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL_1)
+ .build();
+
+ Assert.assertFalse(request.shouldSkipNetwork());
+
+ request.modify()
+ .skipNetwork()
+ .deferBuild();
+
+ Assert.assertTrue(request.shouldSkipNetwork());
+ }
+
+ @Test
+ public void testSkipDisk() {
+ IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL_1)
+ .build();
+
+ Assert.assertFalse(request.shouldSkipDisk());
+
+ request.modify()
+ .skipDisk()
+ .deferBuild();
+
+ Assert.assertTrue(request.shouldSkipDisk());
+ }
+
+ @Test
+ public void testSkipMemory() {
+ IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL_1)
+ .build();
+
+ Assert.assertFalse(request.shouldSkipMemory());
+
+ request.modify()
+ .skipMemory()
+ .deferBuild();
+
+ Assert.assertTrue(request.shouldSkipMemory());
+ }
+
+ @Test
+ public void testExecutionOnBackgroundThread() {
+ IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL_1)
+ .build();
+
+ Assert.assertFalse(request.shouldRunOnBackgroundThread());
+
+ request.modify()
+ .executeCallbackOnBackgroundThread()
+ .deferBuild();
+
+ Assert.assertTrue(request.shouldRunOnBackgroundThread());
+ }
+
+ @Test
+ public void testForLauncherIcon() {
+ // This code will call into GeckoAppShell to determine the launcher icon size for this configuration
+ GeckoAppShell.setApplicationContext(RuntimeEnvironment.application);
+
+ IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL_1)
+ .build();
+
+ Assert.assertEquals(32, request.getTargetSize());
+
+ request.modify()
+ .forLauncherIcon()
+ .deferBuild();
+
+ Assert.assertEquals(48, request.getTargetSize());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconResponse.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconResponse.java
new file mode 100644
index 0000000000..4c7faa4f8a
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconResponse.java
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.icons;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+import static org.mockito.Mockito.mock;
+
+@RunWith(TestRunner.class)
+public class TestIconResponse {
+ private static final String ICON_URL = "http://www.mozilla.org/favicon.ico";
+
+ @Test
+ public void testDefaultResponse() {
+ final Bitmap bitmap = mock(Bitmap.class);
+
+ final IconResponse response = IconResponse.create(bitmap);
+
+ Assert.assertEquals(bitmap, response.getBitmap());
+ Assert.assertFalse(response.hasUrl());
+ Assert.assertNull(response.getUrl());
+
+ Assert.assertFalse(response.hasColor());
+ Assert.assertEquals(0, response.getColor());
+
+ Assert.assertFalse(response.isGenerated());
+ Assert.assertFalse(response.isFromNetwork());
+ Assert.assertFalse(response.isFromDisk());
+ Assert.assertFalse(response.isFromMemory());
+ }
+
+ @Test
+ public void testNetworkResponse() {
+ final Bitmap bitmap = mock(Bitmap.class);
+
+ final IconResponse response = IconResponse.createFromNetwork(bitmap, ICON_URL);
+
+ Assert.assertEquals(bitmap, response.getBitmap());
+ Assert.assertTrue(response.hasUrl());
+ Assert.assertEquals(ICON_URL, response.getUrl());
+
+ Assert.assertFalse(response.hasColor());
+ Assert.assertEquals(0, response.getColor());
+
+ Assert.assertFalse(response.isGenerated());
+ Assert.assertTrue(response.isFromNetwork());
+ Assert.assertFalse(response.isFromDisk());
+ Assert.assertFalse(response.isFromMemory());
+ }
+
+ @Test
+ public void testGeneratedResponse() {
+ final Bitmap bitmap = mock(Bitmap.class);
+
+ final IconResponse response = IconResponse.createGenerated(bitmap, Color.CYAN);
+
+ Assert.assertEquals(bitmap, response.getBitmap());
+ Assert.assertFalse(response.hasUrl());
+ Assert.assertNull(response.getUrl());
+
+ Assert.assertTrue(response.hasColor());
+ Assert.assertEquals(Color.CYAN, response.getColor());
+
+ Assert.assertTrue(response.isGenerated());
+ Assert.assertFalse(response.isFromNetwork());
+ Assert.assertFalse(response.isFromDisk());
+ Assert.assertFalse(response.isFromMemory());
+ }
+
+ @Test
+ public void testMemoryResponse() {
+ final Bitmap bitmap = mock(Bitmap.class);
+
+ final IconResponse response = IconResponse.createFromMemory(bitmap, ICON_URL, Color.CYAN);
+
+ Assert.assertEquals(bitmap, response.getBitmap());
+ Assert.assertTrue(response.hasUrl());
+ Assert.assertEquals(ICON_URL, response.getUrl());
+
+ Assert.assertTrue(response.hasColor());
+ Assert.assertEquals(Color.CYAN, response.getColor());
+
+ Assert.assertFalse(response.isGenerated());
+ Assert.assertFalse(response.isFromNetwork());
+ Assert.assertFalse(response.isFromDisk());
+ Assert.assertTrue(response.isFromMemory());
+ }
+
+ @Test
+ public void testDiskResponse() {
+ final Bitmap bitmap = mock(Bitmap.class);
+
+ final IconResponse response = IconResponse.createFromDisk(bitmap, ICON_URL);
+
+ Assert.assertEquals(bitmap, response.getBitmap());
+ Assert.assertTrue(response.hasUrl());
+ Assert.assertEquals(ICON_URL, response.getUrl());
+
+ Assert.assertFalse(response.hasColor());
+ Assert.assertEquals(0, response.getColor());
+
+ Assert.assertFalse(response.isGenerated());
+ Assert.assertFalse(response.isFromNetwork());
+ Assert.assertTrue(response.isFromDisk());
+ Assert.assertFalse(response.isFromMemory());
+ }
+
+ @Test
+ public void testUpdatingColor() {
+ final IconResponse response = IconResponse.create(mock(Bitmap.class));
+
+ Assert.assertFalse(response.hasColor());
+ Assert.assertEquals(0, response.getColor());
+
+ response.updateColor(Color.YELLOW);
+
+ Assert.assertTrue(response.hasColor());
+ Assert.assertEquals(Color.YELLOW, response.getColor());
+
+ response.updateColor(Color.MAGENTA);
+
+ Assert.assertTrue(response.hasColor());
+ Assert.assertEquals(Color.MAGENTA, response.getColor());
+ }
+
+ @Test
+ public void testUpdatingBitmap() {
+ final Bitmap originalBitmap = mock(Bitmap.class);
+ final Bitmap updatedBitmap = mock(Bitmap.class);
+
+ final IconResponse response = IconResponse.create(originalBitmap);
+
+ Assert.assertEquals(originalBitmap, response.getBitmap());
+ Assert.assertNotEquals(updatedBitmap, response.getBitmap());
+
+ response.updateBitmap(updatedBitmap);
+
+ Assert.assertNotEquals(originalBitmap, response.getBitmap());
+ Assert.assertEquals(updatedBitmap, response.getBitmap());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconTask.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconTask.java
new file mode 100644
index 0000000000..77a801988f
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconTask.java
@@ -0,0 +1,575 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.icons;
+
+import android.graphics.Bitmap;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.icons.loader.IconLoader;
+import org.mozilla.gecko.icons.preparation.Preparer;
+import org.mozilla.gecko.icons.processing.Processor;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+@RunWith(TestRunner.class)
+public class TestIconTask {
+ @Test
+ public void testGeneratorIsInvokedIfAllLoadersFail() {
+ final List<IconLoader> loaders = Arrays.asList(
+ createFailingLoader(),
+ createFailingLoader(),
+ createFailingLoader());
+
+ final Bitmap bitmap = mock(Bitmap.class);
+ final IconLoader generator = createSuccessfulLoader(bitmap);
+
+ final IconRequest request = createIconRequest();
+
+ final IconTask task = new IconTask(
+ request,
+ Collections.<Preparer>emptyList(),
+ loaders,
+ Collections.<Processor>emptyList(),
+ generator);
+
+ final IconResponse response = task.call();
+
+ // Verify all loaders have been tried
+ for (IconLoader loader : loaders) {
+ verify(loader).load(request);
+ }
+
+ // Verify generator was called
+ verify(generator).load(request);
+
+ // Verify response contains generated bitmap
+ Assert.assertEquals(bitmap, response.getBitmap());
+ }
+
+ @Test
+ public void testGeneratorIsNotCalledIfOneLoaderWasSuccessful() {
+ final List<IconLoader> loaders = Collections.singletonList(
+ createSuccessfulLoader(mock(Bitmap.class)));
+
+ final IconLoader generator = createSuccessfulLoader(mock(Bitmap.class));
+
+ final IconRequest request = createIconRequest();
+
+ final IconTask task = new IconTask(
+ request,
+ Collections.<Preparer>emptyList(),
+ loaders,
+ Collections.<Processor>emptyList(),
+ generator);
+
+ final IconResponse response = task.call();
+
+ // Verify all loaders have been tried
+ for (IconLoader loader : loaders) {
+ verify(loader).load(request);
+ }
+
+ // Verify generator was NOT called
+ verify(generator, never()).load(request);
+
+ Assert.assertNotNull(response);
+ }
+
+ @Test
+ public void testNoLoaderIsInvokedForRequestWithoutUrls() {
+ final List<IconLoader> loaders = Collections.singletonList(
+ createSuccessfulLoader(mock(Bitmap.class)));
+
+ final Bitmap bitmap = mock(Bitmap.class);
+ final IconLoader generator = createSuccessfulLoader(bitmap);
+
+ final IconRequest request = createIconRequestWithoutUrls();
+
+ final IconTask task = new IconTask(
+ request,
+ Collections.<Preparer>emptyList(),
+ loaders,
+ Collections.<Processor>emptyList(),
+ generator);
+
+ final IconResponse response = task.call();
+
+ // Verify NO loaders have been called
+ for (IconLoader loader : loaders) {
+ verify(loader, never()).load(request);
+ }
+
+ // Verify generator was called
+ verify(generator).load(request);
+
+ // Verify response contains generated bitmap
+ Assert.assertEquals(bitmap, response.getBitmap());
+ }
+
+ @Test
+ public void testAllPreparersAreCalledBeforeLoading() {
+ final List<Preparer> preparers = Arrays.asList(
+ mock(Preparer.class),
+ mock(Preparer.class),
+ mock(Preparer.class),
+ mock(Preparer.class),
+ mock(Preparer.class),
+ mock(Preparer.class));
+
+ final IconRequest request = createIconRequest();
+
+ final IconTask task = new IconTask(
+ request,
+ preparers,
+ createListWithSuccessfulLoader(),
+ Collections.<Processor>emptyList(),
+ createGenerator());
+
+ task.call();
+
+ // Verify all preparers have been called
+ for (Preparer preparer : preparers) {
+ verify(preparer).prepare(request);
+ }
+ }
+
+ @Test
+ public void testSubsequentLoadersAreNotCalledAfterSuccessfulLoad() {
+ final Bitmap bitmap = mock(Bitmap.class);
+
+ final List<IconLoader> loaders = Arrays.asList(
+ createFailingLoader(),
+ createFailingLoader(),
+ createSuccessfulLoader(bitmap),
+ createSuccessfulLoader(mock(Bitmap.class)),
+ createFailingLoader(),
+ createSuccessfulLoader(mock(Bitmap.class)));
+
+ final IconRequest request = createIconRequest();
+
+ final IconTask task = new IconTask(
+ request,
+ Collections.<Preparer>emptyList(),
+ loaders,
+ Collections.<Processor>emptyList(),
+ createGenerator());
+
+ final IconResponse response = task.call();
+
+ // First loaders are called
+ verify(loaders.get(0)).load(request);
+ verify(loaders.get(1)).load(request);
+ verify(loaders.get(2)).load(request);
+
+ // Loaders after successful load are not called
+ verify(loaders.get(3), never()).load(request);
+ verify(loaders.get(4), never()).load(request);
+ verify(loaders.get(5), never()).load(request);
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(bitmap, response.getBitmap());
+ }
+
+ @Test
+ public void testNoProcessorIsCalledForUnsuccessfulLoads() {
+ final IconRequest request = createIconRequest();
+
+ final List<IconLoader> loaders = createListWithFailingLoaders();
+
+ final List<Processor> processors = Arrays.asList(
+ createProcessor(),
+ createProcessor(),
+ createProcessor());
+
+ final IconTask task = new IconTask(
+ request,
+ Collections.<Preparer>emptyList(),
+ loaders,
+ processors,
+ createFailingLoader());
+
+ task.call();
+
+ // Verify all loaders have been tried
+ for (IconLoader loader : loaders) {
+ verify(loader).load(request);
+ }
+
+ // Verify no processor was called
+ for (Processor processor : processors) {
+ verify(processor, never()).process(any(IconRequest.class), any(IconResponse.class));
+ }
+ }
+
+ @Test
+ public void testAllProcessorsAreCalledAfterSuccessfulLoad() {
+ final IconRequest request = createIconRequest();
+
+ final List<Processor> processors = Arrays.asList(
+ createProcessor(),
+ createProcessor(),
+ createProcessor());
+
+ final IconTask task = new IconTask(
+ request,
+ Collections.<Preparer>emptyList(),
+ createListWithSuccessfulLoader(),
+ processors,
+ createGenerator());
+
+ IconResponse response = task.call();
+
+ Assert.assertNotNull(response);
+
+ // Verify that all processors have been called
+ for (Processor processor : processors) {
+ verify(processor).process(request, response);
+ }
+ }
+
+ @Test
+ public void testCallbackIsExecutedForSuccessfulLoads() {
+ final IconCallback callback = mock(IconCallback.class);
+
+ final IconRequest request = createIconRequest();
+ request.setCallback(callback);
+
+ final IconTask task = new IconTask(
+ request,
+ Collections.<Preparer>emptyList(),
+ createListWithSuccessfulLoader(),
+ Collections.<Processor>emptyList(),
+ createGenerator());
+
+ final IconResponse response = task.call();
+
+ verify(callback).onIconResponse(response);
+ }
+
+ @Test
+ public void testCallbackIsNotExecutedIfLoadingFailed() {
+ final IconCallback callback = mock(IconCallback.class);
+
+ final IconRequest request = createIconRequest();
+ request.setCallback(callback);
+
+ final IconTask task = new IconTask(
+ request,
+ Collections.<Preparer>emptyList(),
+ createListWithFailingLoaders(),
+ Collections.<Processor>emptyList(),
+ createFailingLoader());
+
+ task.call();
+
+ verify(callback, never()).onIconResponse(any(IconResponse.class));
+ }
+
+ @Test
+ public void testCallbackIsExecutedWithGeneratorResult() {
+ final IconCallback callback = mock(IconCallback.class);
+
+ final IconRequest request = createIconRequest();
+ request.setCallback(callback);
+
+ final IconTask task = new IconTask(
+ request,
+ Collections.<Preparer>emptyList(),
+ createListWithFailingLoaders(),
+ Collections.<Processor>emptyList(),
+ createGenerator());
+
+ final IconResponse response = task.call();
+
+ verify(callback).onIconResponse(response);
+ }
+
+ @Test
+ public void testTaskCancellationWhileLoading() {
+ // We simulate the cancellation by injecting a loader that interrupts the thread.
+ final IconLoader cancellingLoader = spy(new IconLoader() {
+ @Override
+ public IconResponse load(IconRequest request) {
+ Thread.currentThread().interrupt();
+ return null;
+ }
+ });
+
+ final List<Preparer> preparers = createListOfPreparers();
+ final List<Processor> processors = createListOfProcessors();
+
+ final List<IconLoader> loaders = Arrays.asList(
+ createFailingLoader(),
+ createFailingLoader(),
+ cancellingLoader,
+ createFailingLoader(),
+ createSuccessfulLoader(mock(Bitmap.class)));
+
+ final IconRequest request = createIconRequest();
+
+ final IconTask task = new IconTask(
+ request,
+ preparers,
+ loaders,
+ processors,
+ createGenerator());
+
+ final IconResponse response = task.call();
+ Assert.assertNull(response);
+
+ // Verify that all preparers are called
+ for (Preparer preparer : preparers) {
+ verify(preparer).prepare(request);
+ }
+
+ // Verify that first loaders are called
+ verify(loaders.get(0)).load(request);
+ verify(loaders.get(1)).load(request);
+
+ // Verify that our loader that interrupts the thread is called
+ verify(loaders.get(2)).load(request);
+
+ // Verify that all other loaders are not called
+ verify(loaders.get(3), never()).load(request);
+ verify(loaders.get(4), never()).load(request);
+
+ // Verify that no processors are called
+ for (Processor processor : processors) {
+ verify(processor, never()).process(eq(request), any(IconResponse.class));
+ }
+ }
+
+ @Test
+ public void testTaskCancellationWhileProcessing() {
+ final Processor cancellingProcessor = spy(new Processor() {
+ @Override
+ public void process(IconRequest request, IconResponse response) {
+ Thread.currentThread().interrupt();
+ }
+ });
+
+ final List<Preparer> preparers = createListOfPreparers();
+
+ final List<IconLoader> loaders = Arrays.asList(
+ createFailingLoader(),
+ createFailingLoader(),
+ createSuccessfulLoader(mock(Bitmap.class)));
+
+ final List<Processor> processors = Arrays.asList(
+ createProcessor(),
+ createProcessor(),
+ cancellingProcessor,
+ createProcessor(),
+ createProcessor());
+
+ final IconRequest request = createIconRequest();
+
+ final IconTask task = new IconTask(
+ request,
+ preparers,
+ loaders,
+ processors,
+ createGenerator());
+
+ final IconResponse response = task.call();
+ Assert.assertNull(response);
+
+ // Verify that all preparers are called
+ for (Preparer preparer : preparers) {
+ verify(preparer).prepare(request);
+ }
+
+ // Verify that all loaders are called
+ for (IconLoader loader : loaders) {
+ verify(loader).load(request);
+ }
+
+ // Verify that first processors are called
+ verify(processors.get(0)).process(eq(request), any(IconResponse.class));
+ verify(processors.get(1)).process(eq(request), any(IconResponse.class));
+
+ // Verify that cancelling processor is called
+ verify(processors.get(2)).process(eq(request), any(IconResponse.class));
+
+ // Verify that subsequent processors are not called
+ verify(processors.get(3), never()).process(eq(request), any(IconResponse.class));
+ verify(processors.get(4), never()).process(eq(request), any(IconResponse.class));
+ }
+
+ @Test
+ public void testTaskCancellationWhilePerparing() {
+ final Preparer failingPreparer = spy(new Preparer() {
+ @Override
+ public void prepare(IconRequest request) {
+ Thread.currentThread().interrupt();
+ }
+ });
+
+ final List<Preparer> preparers = Arrays.asList(
+ mock(Preparer.class),
+ mock(Preparer.class),
+ failingPreparer,
+ mock(Preparer.class),
+ mock(Preparer.class));
+
+ final List<IconLoader> loaders = createListWithSuccessfulLoader();
+ final List<Processor> processors = createListOfProcessors();
+
+ final IconRequest request = createIconRequest();
+
+ final IconTask task = new IconTask(
+ request,
+ preparers,
+ loaders,
+ processors,
+ createGenerator());
+
+ final IconResponse response = task.call();
+ Assert.assertNull(response);
+
+ // Verify that first preparers are called
+ verify(preparers.get(0)).prepare(request);
+ verify(preparers.get(1)).prepare(request);
+
+ // Verify that cancelling preparer is called
+ verify(preparers.get(2)).prepare(request);
+
+ // Verify that subsequent preparers are not called
+ verify(preparers.get(3), never()).prepare(request);
+ verify(preparers.get(4), never()).prepare(request);
+
+ // Verify that no loaders are called
+ for (IconLoader loader : loaders) {
+ verify(loader, never()).load(request);
+ }
+
+ // Verify that no processors are called
+ for (Processor processor : processors) {
+ verify(processor, never()).process(eq(request), any(IconResponse.class));
+ }
+ }
+
+ @Test
+ public void testNoLoadersOrProcessorsAreExecutedForPrepareOnlyTasks() {
+ final List<Preparer> preparers = createListOfPreparers();
+ final List<IconLoader> loaders = createListWithSuccessfulLoader();
+ final List<Processor> processors = createListOfProcessors();
+ final IconLoader generator = createGenerator();
+
+ final IconRequest request = createIconRequest()
+ .modify()
+ .prepareOnly()
+ .build();
+
+ final IconTask task = new IconTask(
+ request,
+ preparers,
+ loaders,
+ processors,
+ generator);
+
+ IconResponse response = task.call();
+
+ Assert.assertNull(response);
+
+ // Verify that all preparers are called
+ for (Preparer preparer : preparers) {
+ verify(preparer).prepare(request);
+ }
+
+ // Verify that no loaders are called
+ for (IconLoader loader : loaders) {
+ verify(loader, never()).load(request);
+ }
+
+ // Verify that no processors are called
+ for (Processor processor : processors) {
+ verify(processor, never()).process(eq(request), any(IconResponse.class));
+ }
+ }
+
+ public List<IconLoader> createListWithSuccessfulLoader() {
+ return Arrays.asList(
+ createFailingLoader(),
+ createFailingLoader(),
+ createSuccessfulLoader(mock(Bitmap.class)),
+ createFailingLoader());
+ }
+
+ public List<IconLoader> createListWithFailingLoaders() {
+ return Arrays.asList(
+ createFailingLoader(),
+ createFailingLoader(),
+ createFailingLoader(),
+ createFailingLoader(),
+ createFailingLoader());
+ }
+
+ public List<Preparer> createListOfPreparers() {
+ return Arrays.asList(
+ mock(Preparer.class),
+ mock(Preparer.class),
+ mock(Preparer.class),
+ mock(Preparer.class),
+ mock(Preparer.class));
+ }
+
+ public IconLoader createFailingLoader() {
+ final IconLoader loader = mock(IconLoader.class);
+ doReturn(null).when(loader).load(any(IconRequest.class));
+ return loader;
+ }
+
+ public IconLoader createSuccessfulLoader(Bitmap bitmap) {
+ IconResponse response = IconResponse.create(bitmap);
+
+ final IconLoader loader = mock(IconLoader.class);
+ doReturn(response).when(loader).load(any(IconRequest.class));
+ return loader;
+ }
+
+ public List<Processor> createListOfProcessors() {
+ return Arrays.asList(
+ mock(Processor.class),
+ mock(Processor.class),
+ mock(Processor.class),
+ mock(Processor.class),
+ mock(Processor.class));
+ }
+
+ public IconRequest createIconRequest() {
+ return Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createGenericIcon("http://www.mozilla.org/favicon.ico"))
+ .build();
+ }
+
+ public IconRequest createIconRequestWithoutUrls() {
+ return Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .build();
+ }
+
+ public IconLoader createGenerator() {
+ return createSuccessfulLoader(mock(Bitmap.class));
+ }
+
+ public Processor createProcessor() {
+ return mock(Processor.class);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconsHelper.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconsHelper.java
new file mode 100644
index 0000000000..f40e2f6290
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconsHelper.java
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.icons;
+
+import android.annotation.SuppressLint;
+
+import junit.framework.Assert;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.util.GeckoJarReader;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(TestRunner.class)
+public class TestIconsHelper {
+ @SuppressLint("AuthLeak") // Lint and Android Studio try to prevent developers from writing code
+ // with credentials in the URL (user:password@host). But in this case
+ // we explicitly want to do that, so we suppress the warnings.
+ @Test
+ public void testGuessDefaultFaviconURL() {
+ // Empty values
+
+ Assert.assertNull(IconsHelper.guessDefaultFaviconURL(null));
+ Assert.assertNull(IconsHelper.guessDefaultFaviconURL(""));
+ Assert.assertNull(IconsHelper.guessDefaultFaviconURL(" "));
+
+ // Special about: URLs.
+
+ Assert.assertEquals(
+ "about:home",
+ IconsHelper.guessDefaultFaviconURL("about:home"));
+
+ Assert.assertEquals(
+ "about:",
+ IconsHelper.guessDefaultFaviconURL("about:"));
+
+ Assert.assertEquals(
+ "about:addons",
+ IconsHelper.guessDefaultFaviconURL("about:addons"));
+
+ // Non http(s) URLS
+
+ final String jarUrl = GeckoJarReader.getJarURL(RuntimeEnvironment.application, "chrome/chrome/content/branding/favicon64.png");
+ Assert.assertEquals(jarUrl, IconsHelper.guessDefaultFaviconURL(jarUrl));
+
+ Assert.assertNull(IconsHelper.guessDefaultFaviconURL("content://some.random.provider/icons"));
+
+ Assert.assertNull(IconsHelper.guessDefaultFaviconURL("ftp://ftp.public.mozilla.org/this/is/made/up"));
+
+ Assert.assertNull(IconsHelper.guessDefaultFaviconURL("file:///"));
+
+ Assert.assertNull(IconsHelper.guessDefaultFaviconURL("file:///system/path"));
+
+ // Various http(s) URLs
+
+ Assert.assertEquals("http://www.mozilla.org/favicon.ico",
+ IconsHelper.guessDefaultFaviconURL("http://www.mozilla.org/"));
+
+ Assert.assertEquals("https://www.mozilla.org/favicon.ico",
+ IconsHelper.guessDefaultFaviconURL("https://www.mozilla.org/en-US/firefox/products/"));
+
+ Assert.assertEquals("https://example.org/favicon.ico",
+ IconsHelper.guessDefaultFaviconURL("https://example.org"));
+
+ Assert.assertEquals("http://user:password@example.org:9991/favicon.ico",
+ IconsHelper.guessDefaultFaviconURL("http://user:password@example.org:9991/status/760492829949001728"));
+
+ Assert.assertEquals("https://localhost:8888/favicon.ico",
+ IconsHelper.guessDefaultFaviconURL("https://localhost:8888/path/folder/file?some=query&params=none"));
+
+ Assert.assertEquals("http://192.168.0.1/favicon.ico",
+ IconsHelper.guessDefaultFaviconURL("http://192.168.0.1/local/action.cgi"));
+
+ Assert.assertEquals("https://medium.com/favicon.ico",
+ IconsHelper.guessDefaultFaviconURL("https://medium.com/firefox-mobile-engineering/firefox-for-android-hack-week-recap-f1ab12f5cc44#.rpmzz15ia"));
+
+ // Some broken, partial URLs
+
+ Assert.assertNull(IconsHelper.guessDefaultFaviconURL("http:"));
+ Assert.assertNull(IconsHelper.guessDefaultFaviconURL("http://"));
+ Assert.assertNull(IconsHelper.guessDefaultFaviconURL("https:/"));
+ }
+
+ @Test
+ public void testIsContainerType() {
+ // Empty values
+ Assert.assertFalse(IconsHelper.isContainerType(null));
+ Assert.assertFalse(IconsHelper.isContainerType(""));
+ Assert.assertFalse(IconsHelper.isContainerType(" "));
+
+ // Values that don't make any sense.
+ Assert.assertFalse(IconsHelper.isContainerType("Hello World"));
+ Assert.assertFalse(IconsHelper.isContainerType("no/no/no"));
+ Assert.assertFalse(IconsHelper.isContainerType("42"));
+
+ // Actual image MIME types that are not container types
+ Assert.assertFalse(IconsHelper.isContainerType("image/png"));
+ Assert.assertFalse(IconsHelper.isContainerType("application/bmp"));
+ Assert.assertFalse(IconsHelper.isContainerType("image/gif"));
+ Assert.assertFalse(IconsHelper.isContainerType("image/x-windows-bitmap"));
+ Assert.assertFalse(IconsHelper.isContainerType("image/jpeg"));
+ Assert.assertFalse(IconsHelper.isContainerType("application/x-png"));
+
+ // MIME types of image container
+ Assert.assertTrue(IconsHelper.isContainerType("image/vnd.microsoft.icon"));
+ Assert.assertTrue(IconsHelper.isContainerType("image/ico"));
+ Assert.assertTrue(IconsHelper.isContainerType("image/icon"));
+ Assert.assertTrue(IconsHelper.isContainerType("image/x-icon"));
+ Assert.assertTrue(IconsHelper.isContainerType("text/ico"));
+ Assert.assertTrue(IconsHelper.isContainerType("application/ico"));
+ }
+
+ @Test
+ public void testCanDecodeType() {
+ // Empty values
+ Assert.assertFalse(IconsHelper.canDecodeType(null));
+ Assert.assertFalse(IconsHelper.canDecodeType(""));
+ Assert.assertFalse(IconsHelper.canDecodeType(" "));
+
+ // Some things we can't decode (or that just aren't images)
+ Assert.assertFalse(IconsHelper.canDecodeType("image/svg+xml"));
+ Assert.assertFalse(IconsHelper.canDecodeType("video/avi"));
+ Assert.assertFalse(IconsHelper.canDecodeType("text/plain"));
+ Assert.assertFalse(IconsHelper.canDecodeType("image/x-quicktime"));
+ Assert.assertFalse(IconsHelper.canDecodeType("image/tiff"));
+ Assert.assertFalse(IconsHelper.canDecodeType("application/zip"));
+
+ // Some image MIME types we definitely can decode
+ Assert.assertTrue(IconsHelper.canDecodeType("image/bmp"));
+ Assert.assertTrue(IconsHelper.canDecodeType("image/x-icon"));
+ Assert.assertTrue(IconsHelper.canDecodeType("image/png"));
+ Assert.assertTrue(IconsHelper.canDecodeType("image/jpg"));
+ Assert.assertTrue(IconsHelper.canDecodeType("image/jpeg"));
+ Assert.assertTrue(IconsHelper.canDecodeType("image/ico"));
+ Assert.assertTrue(IconsHelper.canDecodeType("image/icon"));
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestContentProviderLoader.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestContentProviderLoader.java
new file mode 100644
index 0000000000..58bb3ddf94
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestContentProviderLoader.java
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.icons.loader;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.icons.IconDescriptor;
+import org.mozilla.gecko.icons.IconRequest;
+import org.mozilla.gecko.icons.IconResponse;
+import org.mozilla.gecko.icons.Icons;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(TestRunner.class)
+public class TestContentProviderLoader {
+ @Test
+ public void testNothingIsLoadedForHttpUrls() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createGenericIcon(
+ "https://www.mozilla.org/media/img/favicon/apple-touch-icon-180x180.00050c5b754e.png"))
+ .build();
+
+ IconLoader loader = new ContentProviderLoader();
+ IconResponse response = loader.load(request);
+
+ Assert.assertNull(response);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestDataUriLoader.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestDataUriLoader.java
new file mode 100644
index 0000000000..1fe6ad1a7c
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestDataUriLoader.java
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.icons.loader;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.icons.IconDescriptor;
+import org.mozilla.gecko.icons.IconRequest;
+import org.mozilla.gecko.icons.IconResponse;
+import org.mozilla.gecko.icons.Icons;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(TestRunner.class)
+public class TestDataUriLoader {
+ @Test
+ public void testNothingIsLoadedForHttpUrls() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createGenericIcon(
+ "https://www.mozilla.org/media/img/favicon/apple-touch-icon-180x180.00050c5b754e.png"))
+ .build();
+
+ IconLoader loader = new DataUriLoader();
+ IconResponse response = loader.load(request);
+
+ Assert.assertNull(response);
+ }
+
+ @Test
+ public void testIconIsLoadedFromDataUri() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createGenericIcon(
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAEklEQVR4AWP4z8AAxCDiP8N/AB3wBPxcBee7AAAAAElFTkSuQmCC"))
+ .build();
+
+ IconLoader loader = new DataUriLoader();
+ IconResponse response = loader.load(request);
+
+ Assert.assertNotNull(response);
+ Assert.assertNotNull(response.getBitmap());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestDiskLoader.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestDiskLoader.java
new file mode 100644
index 0000000000..809c351027
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestDiskLoader.java
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.icons.loader;
+
+import android.graphics.Bitmap;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.icons.IconDescriptor;
+import org.mozilla.gecko.icons.IconRequest;
+import org.mozilla.gecko.icons.IconResponse;
+import org.mozilla.gecko.icons.Icons;
+import org.mozilla.gecko.icons.storage.DiskStorage;
+import org.robolectric.RuntimeEnvironment;
+
+import java.io.OutputStream;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+@RunWith(TestRunner.class)
+public class TestDiskLoader {
+ private static final String TEST_PAGE_URL = "http://www.mozilla.org";
+ private static final String TEST_ICON_URL = "https://example.org/favicon.ico";
+
+ @Test
+ public void testLoadingFromEmptyCache() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL))
+ .build();
+
+ final IconLoader loader = new DiskLoader();
+ final IconResponse response = loader.load(request);
+
+ Assert.assertNull(response);
+ }
+
+ @Test
+ public void testLoadingAfterAddingEntry() {
+ final Bitmap bitmap = createMockedBitmap();
+ final IconResponse originalResponse = IconResponse.createFromNetwork(bitmap, TEST_ICON_URL);
+
+ DiskStorage.get(RuntimeEnvironment.application)
+ .putIcon(originalResponse);
+
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL))
+ .build();
+
+ final IconLoader loader = new DiskLoader();
+ final IconResponse loadedResponse = loader.load(request);
+
+ Assert.assertNotNull(loadedResponse);
+
+ // The responses are not the same: The original response was stored to disk and loaded from
+ // disk again. It's a copy effectively.
+ Assert.assertNotEquals(originalResponse, loadedResponse);
+ }
+
+ @Test
+ public void testNothingIsLoadedIfDiskShouldBeSkipped() {
+ final Bitmap bitmap = createMockedBitmap();
+ final IconResponse originalResponse = IconResponse.createFromNetwork(bitmap, TEST_ICON_URL);
+
+ DiskStorage.get(RuntimeEnvironment.application)
+ .putIcon(originalResponse);
+
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL))
+ .skipDisk()
+ .build();
+
+ final IconLoader loader = new DiskLoader();
+ final IconResponse loadedResponse = loader.load(request);
+
+ Assert.assertNull(loadedResponse);
+ }
+
+ private Bitmap createMockedBitmap() {
+ final Bitmap bitmap = mock(Bitmap.class);
+
+ doReturn(true).when(bitmap).compress(any(Bitmap.CompressFormat.class), anyInt(), any(OutputStream.class));
+
+ return bitmap;
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestIconDownloader.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestIconDownloader.java
new file mode 100644
index 0000000000..533f14395f
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestIconDownloader.java
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.icons.loader;
+
+import android.content.Context;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.icons.IconDescriptor;
+import org.mozilla.gecko.icons.IconRequest;
+import org.mozilla.gecko.icons.IconResponse;
+import org.mozilla.gecko.icons.Icons;
+import org.mozilla.gecko.icons.storage.FailureCache;
+import org.robolectric.RuntimeEnvironment;
+
+import java.net.HttpURLConnection;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+@RunWith(TestRunner.class)
+public class TestIconDownloader {
+ /**
+ * Scenario: A request with a non HTTP URL (data:image/*) is executed.
+ *
+ * Verify that:
+ * * No download is performed.
+ */
+ @Test
+ public void testDownloaderDoesNothingForNonHttpUrls() throws Exception {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createGenericIcon(
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAEklEQVR4AWP4z8AAxCDiP8N/AB3wBPxcBee7AAAAAElFTkSuQmCC"))
+ .build();
+
+ final IconDownloader downloader = spy(new IconDownloader());
+ IconResponse response = downloader.load(request);
+
+ Assert.assertNull(response);
+
+ verify(downloader, never()).downloadAndDecodeImage(any(Context.class), anyString());
+ verify(downloader, never()).connectTo(anyString());
+ }
+
+ /**
+ * Scenario: Request contains an URL and server returns 301 with location header (always the same URL).
+ *
+ * Verify that:
+ * * Download code stops and does not loop forever.
+ */
+ @Test
+ public void testRedirectsAreFollowedButNotInCircles() throws Exception {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createFavicon(
+ "https://www.mozilla.org/media/img/favicon.52506929be4c.ico",
+ 32,
+ "image/x-icon"))
+ .build();
+
+ HttpURLConnection mockedConnection = mock(HttpURLConnection.class);
+ doReturn(301).when(mockedConnection).getResponseCode();
+ doReturn("http://example.org/favicon.ico").when(mockedConnection).getHeaderField("Location");
+
+ final IconDownloader downloader = spy(new IconDownloader());
+ doReturn(mockedConnection).when(downloader).connectTo(anyString());
+ IconResponse response = downloader.load(request);
+
+ Assert.assertNull(response);
+
+ verify(downloader).connectTo("https://www.mozilla.org/media/img/favicon.52506929be4c.ico");
+ verify(downloader).connectTo("http://example.org/favicon.ico");
+ }
+
+ /**
+ * Scenario: Request contains an URL and server returns HTTP 404.
+ *
+ * Verify that:
+ * * URL is added to failure cache.
+ */
+ @Test
+ public void testUrlIsAddedToFailureCacheIfServerReturnsClientError() throws Exception {
+ final String faviconUrl = "https://www.mozilla.org/404.ico";
+
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createFavicon(faviconUrl, 32, "image/x-icon"))
+ .build();
+
+ HttpURLConnection mockedConnection = mock(HttpURLConnection.class);
+ doReturn(404).when(mockedConnection).getResponseCode();
+
+ Assert.assertFalse(FailureCache.get().isKnownFailure(faviconUrl));
+
+ final IconDownloader downloader = spy(new IconDownloader());
+ doReturn(mockedConnection).when(downloader).connectTo(anyString());
+ IconResponse response = downloader.load(request);
+
+ Assert.assertNull(response);
+
+ Assert.assertTrue(FailureCache.get().isKnownFailure(faviconUrl));
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestIconGenerator.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestIconGenerator.java
new file mode 100644
index 0000000000..70e3413659
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestIconGenerator.java
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.icons.loader;
+
+import android.graphics.Bitmap;
+
+import org.junit.Assert;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.icons.IconDescriptor;
+import org.mozilla.gecko.icons.IconRequest;
+import org.mozilla.gecko.icons.IconResponse;
+import org.mozilla.gecko.icons.Icons;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(TestRunner.class)
+public class TestIconGenerator {
+ @Test
+ public void testNoIconIsGeneratorIfThereAreIconUrlsToLoadFrom() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createGenericIcon(
+ "https://www.mozilla.org/media/img/favicon/apple-touch-icon-180x180.00050c5b754e.png"))
+ .icon(IconDescriptor.createGenericIcon(
+ "https://www.mozilla.org/media/img/favicon.52506929be4c.ico"))
+ .build();
+
+ IconLoader loader = new IconGenerator();
+ IconResponse response = loader.load(request);
+
+ Assert.assertNull(response);
+ }
+
+ @Test
+ public void testIconIsGeneratedForLastUrl() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createGenericIcon(
+ "https://www.mozilla.org/media/img/favicon/apple-touch-icon-180x180.00050c5b754e.png"))
+ .build();
+
+ IconLoader loader = new IconGenerator();
+ IconResponse response = loader.load(request);
+
+ Assert.assertNotNull(response);
+ Assert.assertNotNull(response.getBitmap());
+ }
+
+ @Test
+ public void testRepresentativeCharacter() {
+ Assert.assertEquals("M", IconGenerator.getRepresentativeCharacter("https://mozilla.org"));
+ Assert.assertEquals("W", IconGenerator.getRepresentativeCharacter("http://wikipedia.org"));
+ Assert.assertEquals("P", IconGenerator.getRepresentativeCharacter("http://plus.google.com"));
+ Assert.assertEquals("E", IconGenerator.getRepresentativeCharacter("https://en.m.wikipedia.org/wiki/Main_Page"));
+
+ // Stripping common prefixes
+ Assert.assertEquals("T", IconGenerator.getRepresentativeCharacter("http://www.theverge.com"));
+ Assert.assertEquals("F", IconGenerator.getRepresentativeCharacter("https://m.facebook.com"));
+ Assert.assertEquals("T", IconGenerator.getRepresentativeCharacter("https://mobile.twitter.com"));
+
+ // Special urls
+ Assert.assertEquals("?", IconGenerator.getRepresentativeCharacter("file:///"));
+ Assert.assertEquals("S", IconGenerator.getRepresentativeCharacter("file:///system/"));
+ Assert.assertEquals("P", IconGenerator.getRepresentativeCharacter("ftp://people.mozilla.org/test"));
+
+ // No values
+ Assert.assertEquals("?", IconGenerator.getRepresentativeCharacter(""));
+ Assert.assertEquals("?", IconGenerator.getRepresentativeCharacter(null));
+
+ // Rubbish
+ Assert.assertEquals("Z", IconGenerator.getRepresentativeCharacter("zZz"));
+ Assert.assertEquals("Ö", IconGenerator.getRepresentativeCharacter("ölkfdpou3rkjaslfdköasdfo8"));
+ Assert.assertEquals("?", IconGenerator.getRepresentativeCharacter("_*+*'##"));
+ Assert.assertEquals("ツ", IconGenerator.getRepresentativeCharacter("¯\\_(ツ)_/¯"));
+ Assert.assertEquals("ಠ", IconGenerator.getRepresentativeCharacter("ಠ_ಠ Look of Disapproval"));
+
+ // Non-ASCII
+ Assert.assertEquals("Ä", IconGenerator.getRepresentativeCharacter("http://www.ätzend.de"));
+ Assert.assertEquals("å", IconGenerator.getRepresentativeCharacter("http://åãŒãƒ‰ãƒ¡ã‚¤ãƒ³.com"));
+ Assert.assertEquals("C", IconGenerator.getRepresentativeCharacter("http://√.com"));
+ Assert.assertEquals("ß", IconGenerator.getRepresentativeCharacter("http://ß.de"));
+ Assert.assertEquals("Ԛ", IconGenerator.getRepresentativeCharacter("http://ԛәлп.com/")); // cyrillic
+
+ // Punycode
+ Assert.assertEquals("X", IconGenerator.getRepresentativeCharacter("http://xn--tzend-fra.de")); // ätzend.de
+ Assert.assertEquals("X", IconGenerator.getRepresentativeCharacter("http://xn--V8jxj3d1dzdz08w.com")); // åãŒãƒ‰ãƒ¡ã‚¤ãƒ³.com
+
+ // Numbers
+ Assert.assertEquals("1", IconGenerator.getRepresentativeCharacter("https://www.1and1.com/"));
+
+ // IP
+ Assert.assertEquals("1", IconGenerator.getRepresentativeCharacter("https://192.168.0.1"));
+ }
+
+ @Test
+ public void testPickColor() {
+ final int color = IconGenerator.pickColor("http://m.facebook.com");
+
+ // Color does not change
+ for (int i = 0; i < 100; i++) {
+ Assert.assertEquals(color, IconGenerator.pickColor("http://m.facebook.com"));
+ }
+
+ // Color is stable for "similar" hosts.
+ Assert.assertEquals(color, IconGenerator.pickColor("https://m.facebook.com"));
+ Assert.assertEquals(color, IconGenerator.pickColor("http://facebook.com"));
+ Assert.assertEquals(color, IconGenerator.pickColor("http://www.facebook.com"));
+ Assert.assertEquals(color, IconGenerator.pickColor("http://www.facebook.com/foo/bar/foobar?mobile=1"));
+ }
+
+ @Test
+ public void testGeneratingFavicon() {
+ final IconResponse response = IconGenerator.generate(RuntimeEnvironment.application, "http://m.facebook.com");
+ final Bitmap bitmap = response.getBitmap();
+
+ Assert.assertNotNull(bitmap);
+
+ final int size = RuntimeEnvironment.application.getResources().getDimensionPixelSize(R.dimen.favicon_bg);
+ Assert.assertEquals(size, bitmap.getWidth());
+ Assert.assertEquals(size, bitmap.getHeight());
+
+ Assert.assertEquals(Bitmap.Config.ARGB_8888, bitmap.getConfig());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestJarLoader.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestJarLoader.java
new file mode 100644
index 0000000000..48f0c26ebf
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestJarLoader.java
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.icons.loader;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.icons.IconDescriptor;
+import org.mozilla.gecko.icons.IconRequest;
+import org.mozilla.gecko.icons.IconResponse;
+import org.mozilla.gecko.icons.Icons;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(TestRunner.class)
+public class TestJarLoader {
+ @Test
+ public void testNothingIsLoadedForHttpUrls() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createGenericIcon(
+ "https://www.mozilla.org/media/img/favicon/apple-touch-icon-180x180.00050c5b754e.png"))
+ .build();
+
+ IconLoader loader = new JarLoader();
+ IconResponse response = loader.load(request);
+
+ Assert.assertNull(response);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestLegacyLoader.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestLegacyLoader.java
new file mode 100644
index 0000000000..eecf76788c
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestLegacyLoader.java
@@ -0,0 +1,152 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.icons.loader;
+
+import android.graphics.Bitmap;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.background.db.DelegatingTestContentProvider;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.BrowserProvider;
+import org.mozilla.gecko.icons.IconDescriptor;
+import org.mozilla.gecko.icons.IconRequest;
+import org.mozilla.gecko.icons.IconResponse;
+import org.mozilla.gecko.icons.Icons;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowContentResolver;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.Iterator;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+@RunWith(TestRunner.class)
+public class TestLegacyLoader {
+ private static final String TEST_PAGE_URL = "http://www.mozilla.org";
+ private static final String TEST_ICON_URL = "https://example.org/favicon.ico";
+ private static final String TEST_ICON_URL_2 = "https://example.com/page/favicon.ico";
+ private static final String TEST_ICON_URL_3 = "https://example.net/icon/favicon.ico";
+
+ @Test
+ public void testDatabaseIsQueriesForNormalRequestsWithNetworkSkipped() {
+ // We're going to query BrowserProvider via LegacyLoader, and will access a database.
+ // We need to ensure we close our db connection properly.
+ // This is the only test in this class that actually accesses a database. If that changes,
+ // move BrowserProvider registration into a @Before method, and provider.shutdown into @After.
+ final BrowserProvider provider = new BrowserProvider();
+ provider.onCreate();
+ ShadowContentResolver.registerProvider(BrowserContract.AUTHORITY, new DelegatingTestContentProvider(provider));
+ try {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL))
+ .skipNetwork()
+ .build();
+
+ final LegacyLoader loader = spy(new LegacyLoader());
+ final IconResponse response = loader.load(request);
+
+ verify(loader).loadBitmapFromDatabase(request);
+ Assert.assertNull(response);
+ // Close any open db connections.
+ } finally {
+ provider.shutdown();
+ }
+ }
+
+ @Test
+ public void testNothingIsLoadedIfNetworkIsNotSkipped() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL))
+ .build();
+
+ final LegacyLoader loader = spy(new LegacyLoader());
+ final IconResponse response = loader.load(request);
+
+ verify(loader, never()).loadBitmapFromDatabase(request);
+
+ Assert.assertNull(response);
+ }
+
+ @Test
+ public void testNothingIsLoadedIfDiskSHouldBeSkipped() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL))
+ .skipDisk()
+ .build();
+
+ final LegacyLoader loader = spy(new LegacyLoader());
+ final IconResponse response = loader.load(request);
+
+ verify(loader, never()).loadBitmapFromDatabase(request);
+
+ Assert.assertNull(response);
+ }
+
+ @Test
+ public void testLoadedBitmapIsReturnedAsResponse() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL))
+ .skipNetwork()
+ .build();
+
+ final Bitmap bitmap = mock(Bitmap.class);
+
+ final LegacyLoader loader = spy(new LegacyLoader());
+ doReturn(bitmap).when(loader).loadBitmapFromDatabase(request);
+
+ final IconResponse response = loader.load(request);
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(bitmap, response.getBitmap());
+ }
+
+ @Test
+ public void testLoaderOnlyLoadsIfThereIsOneIconLeft() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL))
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL_2))
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL_3))
+ .skipNetwork()
+ .build();
+
+ final LegacyLoader loader = spy(new LegacyLoader());
+ doReturn(mock(Bitmap.class)).when(loader).loadBitmapFromDatabase(request);
+
+ // First load doesn't load an icon.
+ Assert.assertNull(loader.load(request));
+
+ // Second load doesn't load an icon.
+ removeFirstIcon(request);
+ Assert.assertNull(loader.load(request));
+
+ // Now only one icon is left and a response will be returned.
+ removeFirstIcon(request);
+ Assert.assertNotNull(loader.load(request));
+ }
+
+ private void removeFirstIcon(IconRequest request) {
+ final Iterator<IconDescriptor> iterator = request.getIconIterator();
+ if (iterator.hasNext()) {
+ iterator.next();
+ iterator.remove();
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestMemoryLoader.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestMemoryLoader.java
new file mode 100644
index 0000000000..414ac8cc7f
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestMemoryLoader.java
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.icons.loader;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+
+import junit.framework.Assert;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.icons.IconDescriptor;
+import org.mozilla.gecko.icons.IconRequest;
+import org.mozilla.gecko.icons.IconResponse;
+import org.mozilla.gecko.icons.Icons;
+import org.mozilla.gecko.icons.storage.MemoryStorage;
+import org.robolectric.RuntimeEnvironment;
+
+import static org.mockito.Mockito.mock;
+
+@RunWith(TestRunner.class)
+public class TestMemoryLoader {
+ private static final String TEST_PAGE_URL = "http://www.mozilla.org";
+ private static final String TEST_ICON_URL = "https://example.org/favicon.ico";
+
+ @Before
+ public void setUp() {
+ // Make sure to start with an empty memory cache.
+ MemoryStorage.get().evictAll();
+ }
+
+ @Test
+ public void testStoringAndLoadingFromMemory() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL))
+ .build();
+
+ final IconLoader loader = new MemoryLoader();
+
+ Assert.assertNull(loader.load(request));
+
+ final Bitmap bitmap = mock(Bitmap.class);
+ final IconResponse response = IconResponse.create(bitmap);
+ response.updateColor(Color.MAGENTA);
+
+ MemoryStorage.get().putIcon(TEST_ICON_URL, response);
+
+ final IconResponse loadedResponse = loader.load(request);
+
+ Assert.assertNotNull(loadedResponse);
+ Assert.assertEquals(bitmap, loadedResponse.getBitmap());
+ Assert.assertEquals(Color.MAGENTA, loadedResponse.getColor());
+ }
+
+ @Test
+ public void testNothingIsLoadedIfMemoryShouldBeSkipped() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL))
+ .skipMemory()
+ .build();
+
+ final IconLoader loader = new MemoryLoader();
+
+ Assert.assertNull(loader.load(request));
+
+ final Bitmap bitmap = mock(Bitmap.class);
+ final IconResponse response = IconResponse.create(bitmap);
+
+ MemoryStorage.get().putIcon(TEST_ICON_URL, response);
+
+ Assert.assertNull(loader.load(request));
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestAboutPagesPreparer.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestAboutPagesPreparer.java
new file mode 100644
index 0000000000..f0d4cb7e2c
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestAboutPagesPreparer.java
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+package org.mozilla.gecko.icons.preparation;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.AboutPages;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.icons.IconRequest;
+import org.mozilla.gecko.icons.Icons;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(TestRunner.class)
+public class TestAboutPagesPreparer {
+ private static final String[] ABOUT_PAGES = {
+ AboutPages.ACCOUNTS,
+ AboutPages.ADDONS,
+ AboutPages.CONFIG,
+ AboutPages.DOWNLOADS,
+ AboutPages.FIREFOX,
+ AboutPages.HEALTHREPORT,
+ AboutPages.HOME,
+ AboutPages.UPDATER
+ };
+
+ @Test
+ public void testPreparerAddsUrlsForAllAboutPages() {
+ final Preparer preparer = new AboutPagesPreparer();
+
+ for (String url : ABOUT_PAGES) {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(url)
+ .build();
+
+ Assert.assertEquals(0, request.getIconCount());
+
+ preparer.prepare(request);
+
+ Assert.assertEquals("Added icon URL for URL: " + url, 1, request.getIconCount());
+ }
+ }
+
+ @Test
+ public void testPrepareDoesNotAddUrlForGenericHttpUrl() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .build();
+
+ Assert.assertEquals(0, request.getIconCount());
+
+ final Preparer preparer = new AboutPagesPreparer();
+ preparer.prepare(request);
+
+ Assert.assertEquals(0, request.getIconCount());
+ }
+
+ @Test
+ public void testAddedUrlHasJarScheme() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(AboutPages.DOWNLOADS)
+ .build();
+
+ final Preparer preparer = new AboutPagesPreparer();
+ preparer.prepare(request);
+
+ Assert.assertEquals(1, request.getIconCount());
+
+ final String url = request.getBestIcon().getUrl();
+ Assert.assertNotNull(url);
+ Assert.assertTrue(url.startsWith("jar:jar:"));
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestAddDefaultIconUrl.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestAddDefaultIconUrl.java
new file mode 100644
index 0000000000..ce5e82d0b0
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestAddDefaultIconUrl.java
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.icons.preparation;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.icons.IconDescriptor;
+import org.mozilla.gecko.icons.IconRequest;
+import org.mozilla.gecko.icons.Icons;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.Iterator;
+
+@RunWith(TestRunner.class)
+public class TestAddDefaultIconUrl {
+ @Test
+ public void testAddingDefaultUrl() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createTouchicon(
+ "https://www.mozilla.org/media/img/favicon/apple-touch-icon-180x180.00050c5b754e.png",
+ 180,
+ "image/png"))
+ .icon(IconDescriptor.createFavicon(
+ "https://www.mozilla.org/media/img/favicon.52506929be4c.ico",
+ 32,
+ "image/x-icon"))
+ .icon(IconDescriptor.createFavicon(
+ "jar:jar:wtf.png",
+ 16,
+ "image/png"))
+ .build();
+
+
+ Assert.assertEquals(3, request.getIconCount());
+ Assert.assertFalse(containsUrl(request, "http://www.mozilla.org/favicon.ico"));
+
+ Preparer preparer = new AddDefaultIconUrl();
+ preparer.prepare(request);
+
+ Assert.assertEquals(4, request.getIconCount());
+ Assert.assertTrue(containsUrl(request, "http://www.mozilla.org/favicon.ico"));
+ }
+
+ @Test
+ public void testDefaultUrlIsNotAddedIfItAlreadyExists() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl("http://www.mozilla.org")
+ .icon(IconDescriptor.createFavicon(
+ "http://www.mozilla.org/favicon.ico",
+ 32,
+ "image/x-icon"))
+ .build();
+
+ Assert.assertEquals(1, request.getIconCount());
+
+ Preparer preparer = new AddDefaultIconUrl();
+ preparer.prepare(request);
+
+ Assert.assertEquals(1, request.getIconCount());
+ }
+
+ private boolean containsUrl(IconRequest request, String url) {
+ final Iterator<IconDescriptor> iterator = request.getIconIterator();
+
+ while (iterator.hasNext()) {
+ IconDescriptor descriptor = iterator.next();
+
+ if (descriptor.getUrl().equals(url)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestFilterKnownFailureUrls.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestFilterKnownFailureUrls.java
new file mode 100644
index 0000000000..67584c4cfc
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestFilterKnownFailureUrls.java
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.icons.preparation;
+
+import junit.framework.Assert;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.icons.IconDescriptor;
+import org.mozilla.gecko.icons.IconRequest;
+import org.mozilla.gecko.icons.Icons;
+import org.mozilla.gecko.icons.storage.FailureCache;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(TestRunner.class)
+public class TestFilterKnownFailureUrls {
+ private static final String TEST_PAGE_URL = "http://www.mozilla.org";
+ private static final String TEST_ICON_URL = "https://example.org/favicon.ico";
+
+ @Before
+ public void setUp() {
+ // Make sure we always start with an empty cache.
+ FailureCache.get().evictAll();
+ }
+
+ @Test
+ public void testFilterDoesNothingByDefault() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL))
+ .build();
+
+ Assert.assertEquals(1, request.getIconCount());
+
+ final Preparer preparer = new FilterKnownFailureUrls();
+ preparer.prepare(request);
+
+ Assert.assertEquals(1, request.getIconCount());
+ }
+
+ @Test
+ public void testFilterKnownFailureUrls() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL))
+ .build();
+
+ Assert.assertEquals(1, request.getIconCount());
+
+ FailureCache.get().rememberFailure(TEST_ICON_URL);
+
+ final Preparer preparer = new FilterKnownFailureUrls();
+ preparer.prepare(request);
+
+ Assert.assertEquals(0, request.getIconCount());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestFilterMimeTypes.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestFilterMimeTypes.java
new file mode 100644
index 0000000000..e8339b4e98
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestFilterMimeTypes.java
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.icons.preparation;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.icons.IconDescriptor;
+import org.mozilla.gecko.icons.IconRequest;
+import org.mozilla.gecko.icons.Icons;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(TestRunner.class)
+public class TestFilterMimeTypes {
+ private static final String TEST_PAGE_URL = "http://www.mozilla.org";
+ private static final String TEST_ICON_URL = "https://example.org/favicon.ico";
+ private static final String TEST_ICON_URL_2 = "https://mozilla.org/favicon.ico";
+
+ @Test
+ public void testUrlsWithoutMimeTypesAreNotFiltered() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL))
+ .build();
+
+ Assert.assertEquals(1, request.getIconCount());
+
+ final Preparer preparer = new FilterMimeTypes();
+ preparer.prepare(request);
+
+ Assert.assertEquals(1, request.getIconCount());
+ }
+
+ @Test
+ public void testUnknownMimeTypesAreFiltered() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createFavicon(TEST_ICON_URL, 256, "image/zaphod"))
+ .icon(IconDescriptor.createFavicon(TEST_ICON_URL_2, 128, "audio/mpeg"))
+ .build();
+
+ Assert.assertEquals(2, request.getIconCount());
+
+ final Preparer preparer = new FilterMimeTypes();
+ preparer.prepare(request);
+
+ Assert.assertEquals(0, request.getIconCount());
+ }
+
+ @Test
+ public void testKnownMimeTypesAreNotFiltered() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createFavicon(TEST_ICON_URL, 256, "image/x-icon"))
+ .icon(IconDescriptor.createFavicon(TEST_ICON_URL_2, 128, "image/png"))
+ .build();
+
+ Assert.assertEquals(2, request.getIconCount());
+
+ final Preparer preparer = new FilterMimeTypes();
+ preparer.prepare(request);
+
+ Assert.assertEquals(2, request.getIconCount());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestFilterPrivilegedUrls.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestFilterPrivilegedUrls.java
new file mode 100644
index 0000000000..53fcbd05a1
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestFilterPrivilegedUrls.java
@@ -0,0 +1,86 @@
+package org.mozilla.gecko.icons.preparation;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.icons.IconDescriptor;
+import org.mozilla.gecko.icons.IconRequest;
+import org.mozilla.gecko.icons.Icons;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.Iterator;
+
+@RunWith(TestRunner.class)
+public class TestFilterPrivilegedUrls {
+ private static final String TEST_PAGE_URL = "http://www.mozilla.org";
+
+ private static final String TEST_ICON_HTTP_URL = "https://www.mozilla.org/media/img/favicon/apple-touch-icon-180x180.00050c5b754e.png";
+ private static final String TEST_ICON_HTTP_URL_2 = "https://www.mozilla.org/media/img/favicon.52506929be4c.ico";
+ private static final String TEST_ICON_JAR_URL = "jar:jar:wtf.png";
+
+ @Test
+ public void testFiltering() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_HTTP_URL))
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_HTTP_URL_2))
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_JAR_URL))
+ .build();
+
+ Assert.assertEquals(3, request.getIconCount());
+
+ Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL));
+ Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL_2));
+ Assert.assertTrue(containsUrl(request, TEST_ICON_JAR_URL));
+
+ Preparer preparer = new FilterPrivilegedUrls();
+ preparer.prepare(request);
+
+ Assert.assertEquals(2, request.getIconCount());
+
+ Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL));
+ Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL_2));
+ Assert.assertFalse(containsUrl(request, TEST_ICON_JAR_URL));
+ }
+
+ @Test
+ public void testNothingIsFilteredForPrivilegedRequests() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_HTTP_URL))
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_HTTP_URL_2))
+ .icon(IconDescriptor.createGenericIcon(TEST_ICON_JAR_URL))
+ .privileged(true)
+ .build();
+
+ Assert.assertEquals(3, request.getIconCount());
+
+ Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL));
+ Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL_2));
+ Assert.assertTrue(containsUrl(request, TEST_ICON_JAR_URL));
+
+ Preparer preparer = new FilterPrivilegedUrls();
+ preparer.prepare(request);
+
+ Assert.assertEquals(3, request.getIconCount());
+
+ Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL));
+ Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL_2));
+ Assert.assertTrue(containsUrl(request, TEST_ICON_JAR_URL));
+ }
+
+ private boolean containsUrl(IconRequest request, String url) {
+ final Iterator<IconDescriptor> iterator = request.getIconIterator();
+
+ while (iterator.hasNext()) {
+ IconDescriptor descriptor = iterator.next();
+
+ if (descriptor.getUrl().equals(url)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestLookupIconUrl.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestLookupIconUrl.java
new file mode 100644
index 0000000000..99bac076bb
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestLookupIconUrl.java
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.icons.preparation;
+
+import junit.framework.Assert;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.icons.IconRequest;
+import org.mozilla.gecko.icons.Icons;
+import org.mozilla.gecko.icons.storage.DiskStorage;
+import org.mozilla.gecko.icons.storage.MemoryStorage;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(TestRunner.class)
+public class TestLookupIconUrl {
+ private static final String TEST_PAGE_URL = "http://www.mozilla.org";
+
+ private static final String TEST_ICON_URL_1 = "http://www.mozilla.org/favicon.ico";
+ private static final String TEST_ICON_URL_2 = "http://example.org/favicon.ico";
+ private static final String TEST_ICON_URL_3 = "http://example.com/favicon.ico";
+ private static final String TEST_ICON_URL_4 = "http://example.net/favicon.ico";
+
+
+ @Before
+ public void setUp() {
+ MemoryStorage.get().evictAll();
+ }
+
+ @Test
+ public void testNoIconUrlIsAddedByDefault() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .build();
+
+ Assert.assertEquals(0, request.getIconCount());
+
+ Preparer preparer = new LookupIconUrl();
+ preparer.prepare(request);
+
+ Assert.assertEquals(0, request.getIconCount());
+ }
+
+ @Test
+ public void testIconUrlIsAddedFromMemory() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .build();
+
+ MemoryStorage.get().putMapping(request, TEST_ICON_URL_1);
+
+ Assert.assertEquals(0, request.getIconCount());
+
+ Preparer preparer = new LookupIconUrl();
+ preparer.prepare(request);
+
+ Assert.assertEquals(1, request.getIconCount());
+
+ Assert.assertEquals(TEST_ICON_URL_1, request.getBestIcon().getUrl());
+ }
+
+ @Test
+ public void testIconUrlIsAddedFromDisk() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .build();
+
+ DiskStorage.get(RuntimeEnvironment.application).putMapping(request, TEST_ICON_URL_2);
+
+ Assert.assertEquals(0, request.getIconCount());
+
+ Preparer preparer = new LookupIconUrl();
+ preparer.prepare(request);
+
+ Assert.assertEquals(1, request.getIconCount());
+
+ Assert.assertEquals(TEST_ICON_URL_2, request.getBestIcon().getUrl());
+ }
+
+ @Test
+ public void testIconUrlIsAddedFromMemoryBeforeUsingDiskStorage() {
+ final IconRequest request = Icons.with(RuntimeEnvironment.application)
+ .pageUrl(TEST_PAGE_URL)
+ .build();
+
+ MemoryStorage.get().putMapping(request, TEST_ICON_URL_3);
+ DiskStorage.get(RuntimeEnvironment.application).putMapping(request, TEST_ICON_URL_4);
+
+ Assert.assertEquals(0, request.getIconCount());
+
+ Preparer preparer = new LookupIconUrl();
+ preparer.prepare(request);
+
+ Assert.assertEquals(1, request.getIconCount());
+
+ Assert.assertEquals(TEST_ICON_URL_3, request.getBestIcon().getUrl());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestColorProcessor.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestColorProcessor.java
new file mode 100644
index 0000000000..6057c07762
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestColorProcessor.java
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.icons.processing;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.icons.IconResponse;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+@RunWith(TestRunner.class)
+public class TestColorProcessor {
+ @Test
+ public void testExtractingColor() {
+ final IconResponse response = IconResponse.create(createRedBitmapMock());
+
+ Assert.assertFalse(response.hasColor());
+ Assert.assertEquals(0, response.getColor());
+
+ final Processor processor = new ColorProcessor();
+ processor.process(null, response);
+
+ Assert.assertTrue(response.hasColor());
+ Assert.assertEquals(Color.RED, response.getColor());
+ }
+
+ private Bitmap createRedBitmapMock() {
+ final Bitmap bitmap = mock(Bitmap.class);
+
+ doReturn(1).when(bitmap).getWidth();
+ doReturn(1).when(bitmap).getHeight();
+
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) throws Throwable {
+ Object[] args = invocation.getArguments();
+ int[] pixels = (int[]) args[0];
+ for (int i = 0; i < pixels.length; i++) {
+ pixels[i] = Color.RED;
+ }
+ return null;
+ }
+ }).when(bitmap).getPixels(any(int[].class), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt());
+
+ return bitmap;
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestDiskProcessor.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestDiskProcessor.java
new file mode 100644
index 0000000000..eea5c9bf69
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestDiskProcessor.java
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.icons.processing;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+
+import junit.framework.Assert;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.icons.IconDescriptor;
+import org.mozilla.gecko.icons.IconRequest;
+import org.mozilla.gecko.icons.IconResponse;
+import org.mozilla.gecko.icons.Icons;
+import org.mozilla.gecko.icons.storage.DiskStorage;
+import org.robolectric.RuntimeEnvironment;
+
+import java.io.OutputStream;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+@RunWith(TestRunner.class)
+public class TestDiskProcessor {
+ private static final String PAGE_URL = "https://www.mozilla.org";
+ private static final String ICON_URL = "https://www.mozilla.org/favicon.ico";
+
+ @Test
+ public void testNetworkResponseIsStoredInCache() {
+ final IconRequest request = createTestRequest();
+ final IconResponse response = createTestNetworkResponse();
+
+ final DiskStorage storage = DiskStorage.get(RuntimeEnvironment.application);
+ Assert.assertNull(storage.getIcon(ICON_URL));
+
+ final Processor processor = new DiskProcessor();
+ processor.process(request, response);
+
+ Assert.assertNotNull(storage.getIcon(ICON_URL));
+ }
+
+ @Test
+ public void testGeneratedResponseIsNotStored() {
+ final IconRequest request = createTestRequest();
+ final IconResponse response = createGeneratedResponse();
+
+ final DiskStorage storage = DiskStorage.get(RuntimeEnvironment.application);
+ Assert.assertNull(storage.getIcon(ICON_URL));
+
+ final Processor processor = new DiskProcessor();
+ processor.process(request, response);
+
+ Assert.assertNull(storage.getIcon(ICON_URL));
+ }
+
+ @Test
+ public void testNothingIsStoredIfDiskShouldBeSkipped() {
+ final IconRequest request = createTestRequest()
+ .modify()
+ .skipDisk()
+ .build();
+ final IconResponse response = createTestNetworkResponse();
+
+ final DiskStorage storage = DiskStorage.get(RuntimeEnvironment.application);
+ Assert.assertNull(storage.getIcon(ICON_URL));
+
+ final Processor processor = new DiskProcessor();
+ processor.process(request, response);
+
+ Assert.assertNull(storage.getIcon(ICON_URL));
+ }
+
+ private IconRequest createTestRequest() {
+ return Icons.with(RuntimeEnvironment.application)
+ .pageUrl(PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(ICON_URL))
+ .build();
+ }
+
+ public IconResponse createTestNetworkResponse() {
+ return IconResponse.createFromNetwork(createMockedBitmap(), ICON_URL);
+ }
+
+ public IconResponse createGeneratedResponse() {
+ return IconResponse.createGenerated(createMockedBitmap(), Color.WHITE);
+ }
+
+ private Bitmap createMockedBitmap() {
+ final Bitmap bitmap = mock(Bitmap.class);
+
+ doReturn(true).when(bitmap).compress(any(Bitmap.CompressFormat.class), anyInt(), any(OutputStream.class));
+
+ return bitmap;
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestMemoryProcessor.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestMemoryProcessor.java
new file mode 100644
index 0000000000..fbc1e0baf4
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestMemoryProcessor.java
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.icons.processing;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.icons.IconDescriptor;
+import org.mozilla.gecko.icons.IconRequest;
+import org.mozilla.gecko.icons.IconResponse;
+import org.mozilla.gecko.icons.Icons;
+import org.mozilla.gecko.icons.storage.MemoryStorage;
+import org.robolectric.RuntimeEnvironment;
+
+import static org.mockito.Mockito.mock;
+
+@RunWith(TestRunner.class)
+public class TestMemoryProcessor {
+ private static final String PAGE_URL = "https://www.mozilla.org";
+ private static final String ICON_URL = "https://www.mozilla.org/favicon.ico";
+ private static final String DATA_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAEklEQVR4AWP4z8AAxCDiP8N/AB3wBPxcBee7AAAAAElFTkSuQmCC";
+
+ @Before
+ public void setUp() {
+ MemoryStorage.get().evictAll();
+ }
+
+ @Test
+ public void testResponsesAreStoredInMemory() {
+ final IconRequest request = createTestRequest();
+
+ Assert.assertNull(MemoryStorage.get().getIcon(ICON_URL));
+ Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL));
+
+ final Processor processor = new MemoryProcessor();
+ processor.process(request, createTestResponse());
+
+ Assert.assertNotNull(MemoryStorage.get().getIcon(ICON_URL));
+ Assert.assertNotNull(MemoryStorage.get().getMapping(PAGE_URL));
+ }
+
+ @Test
+ public void testNothingIsStoredIfMemoryShouldBeSkipped() {
+ final IconRequest request = createTestRequest()
+ .modify()
+ .skipMemory()
+ .build();
+
+ Assert.assertNull(MemoryStorage.get().getIcon(ICON_URL));
+ Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL));
+
+ final Processor processor = new MemoryProcessor();
+ processor.process(request, createTestResponse());
+
+ Assert.assertNull(MemoryStorage.get().getIcon(ICON_URL));
+ Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL));
+ }
+
+ @Test
+ public void testNothingIsStoredForRequestsWithoutUrl() {
+ final IconRequest request = createTestRequestWithoutIconUrl();
+
+ Assert.assertNull(MemoryStorage.get().getIcon(ICON_URL));
+ Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL));
+
+ final Processor processor = new MemoryProcessor();
+ processor.process(request, createTestResponse());
+
+ Assert.assertNull(MemoryStorage.get().getIcon(ICON_URL));
+ Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL));
+ }
+
+ @Test
+ public void testNothingIsStoredForGeneratedResponses() {
+ final IconRequest request = createTestRequest();
+
+ Assert.assertNull(MemoryStorage.get().getIcon(ICON_URL));
+ Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL));
+
+ final Processor processor = new MemoryProcessor();
+ processor.process(request, createGeneratedTestResponse());
+
+ Assert.assertNull(MemoryStorage.get().getIcon(ICON_URL));
+ Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL));
+ }
+
+ @Test
+ public void testNothingIsStoredForDataUris() {
+ final IconRequest request = createDataUriTestRequest();
+
+ Assert.assertNull(MemoryStorage.get().getIcon(DATA_URL));
+ Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL));
+
+ final Processor processor = new MemoryProcessor();
+ processor.process(request, createTestResponse());
+
+ Assert.assertNull(MemoryStorage.get().getIcon(DATA_URL));
+ Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL));
+ }
+
+ private IconRequest createTestRequest() {
+ return Icons.with(RuntimeEnvironment.application)
+ .pageUrl(PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(ICON_URL))
+ .build();
+ }
+
+ private IconRequest createTestRequestWithoutIconUrl() {
+ return Icons.with(RuntimeEnvironment.application)
+ .pageUrl(PAGE_URL)
+ .build();
+ }
+
+ private IconRequest createDataUriTestRequest() {
+ return Icons.with(RuntimeEnvironment.application)
+ .pageUrl(PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(DATA_URL))
+ .build();
+ }
+
+ private IconResponse createTestResponse() {
+ return IconResponse.create(mock(Bitmap.class));
+ }
+
+ private IconResponse createGeneratedTestResponse() {
+ return IconResponse.createGenerated(mock(Bitmap.class), Color.GREEN);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestResizingProcessor.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestResizingProcessor.java
new file mode 100644
index 0000000000..dbcb4e2eeb
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestResizingProcessor.java
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.icons.processing;
+
+import android.graphics.Bitmap;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.icons.IconDescriptor;
+import org.mozilla.gecko.icons.IconRequest;
+import org.mozilla.gecko.icons.IconResponse;
+import org.mozilla.gecko.icons.Icons;
+import org.robolectric.RuntimeEnvironment;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+@RunWith(TestRunner.class)
+public class TestResizingProcessor {
+ private static final String PAGE_URL = "https://www.mozilla.org";
+ private static final String ICON_URL = "https://www.mozilla.org/favicon.ico";
+
+ @Test
+ public void testBitmapIsNotResizedIfItAlreadyHasTheTargetSize() {
+ final IconRequest request = createTestRequest();
+
+ final Bitmap bitmap = createBitmapMock(request.getTargetSize());
+ final IconResponse response = spy(IconResponse.create(bitmap));
+
+ final ResizingProcessor processor = spy(new ResizingProcessor());
+ processor.process(request, response);
+
+ verify(processor, never()).resize(any(Bitmap.class), anyInt());
+ verify(bitmap, never()).recycle();
+ verify(response, never()).updateBitmap(any(Bitmap.class));
+ }
+
+ @Test
+ public void testLargerBitmapsAreResized() {
+ final IconRequest request = createTestRequest();
+
+ final Bitmap bitmap = createBitmapMock(request.getTargetSize() * 2);
+ final IconResponse response = spy(IconResponse.create(bitmap));
+
+ final ResizingProcessor processor = spy(new ResizingProcessor());
+ final Bitmap resizedBitmap = mock(Bitmap.class);
+ doReturn(resizedBitmap).when(processor).resize(any(Bitmap.class), anyInt());
+ processor.process(request, response);
+
+ verify(processor).resize(bitmap, request.getTargetSize());
+ verify(bitmap).recycle();
+ verify(response).updateBitmap(resizedBitmap);
+ }
+
+ @Test
+ public void testBitmapIsUpscaledToTargetSize() {
+ final IconRequest request = createTestRequest();
+
+ final Bitmap bitmap = createBitmapMock(request.getTargetSize() / 2 + 1);
+ final IconResponse response = spy(IconResponse.create(bitmap));
+
+ final ResizingProcessor processor = spy(new ResizingProcessor());
+ final Bitmap resizedBitmap = mock(Bitmap.class);
+ doReturn(resizedBitmap).when(processor).resize(any(Bitmap.class), anyInt());
+ processor.process(request, response);
+
+ verify(processor).resize(bitmap, request.getTargetSize());
+ verify(bitmap).recycle();
+ verify(response).updateBitmap(resizedBitmap);
+ }
+
+ @Test
+ public void testBitmapIsNotScaledMoreThanTwoTimesTheSize() {
+ final IconRequest request = createTestRequest();
+
+ final Bitmap bitmap = createBitmapMock(5);
+ final IconResponse response = spy(IconResponse.create(bitmap));
+
+ final ResizingProcessor processor = spy(new ResizingProcessor());
+ final Bitmap resizedBitmap = mock(Bitmap.class);
+ doReturn(resizedBitmap).when(processor).resize(any(Bitmap.class), anyInt());
+ processor.process(request, response);
+
+ verify(processor).resize(bitmap, 10);
+ verify(bitmap).recycle();
+ verify(response).updateBitmap(resizedBitmap);
+ }
+
+ private IconRequest createTestRequest() {
+ return Icons.with(RuntimeEnvironment.application)
+ .pageUrl(PAGE_URL)
+ .icon(IconDescriptor.createGenericIcon(ICON_URL))
+ .build();
+ }
+
+ private Bitmap createBitmapMock(int size) {
+ final Bitmap bitmap = mock(Bitmap.class);
+
+ doReturn(size).when(bitmap).getWidth();
+ doReturn(size).when(bitmap).getHeight();
+
+ return bitmap;
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/permissions/TestPermissions.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/permissions/TestPermissions.java
new file mode 100644
index 0000000000..07fbab4933
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/permissions/TestPermissions.java
@@ -0,0 +1,253 @@
+/* -*- 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.permissions;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Matchers;
+
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+import static org.mockito.Mockito.*;
+
+@RunWith(TestRunner.class)
+public class TestPermissions {
+ @Test
+ public void testSuccessRunnableIsExecutedIfPermissionsAreGranted() {
+ Permissions.setPermissionHelper(mockGrantingHelper());
+
+ Runnable onPermissionsGranted = mock(Runnable.class);
+ Runnable onPermissionsDenied = mock(Runnable.class);
+
+ Permissions.from(mockActivity())
+ .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ .andFallback(onPermissionsDenied)
+ .run(onPermissionsGranted);
+
+ verify(onPermissionsDenied, never()).run();
+ verify(onPermissionsGranted).run();
+ }
+
+ @Test
+ public void testFallbackRunnableIsExecutedIfPermissionsAreDenied() {
+ Permissions.setPermissionHelper(mockDenyingHelper());
+
+ Runnable onPermissionsGranted = mock(Runnable.class);
+ Runnable onPermissionsDenied = mock(Runnable.class);
+
+ Activity activity = mockActivity();
+
+ Permissions.from(activity)
+ .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ .andFallback(onPermissionsDenied)
+ .run(onPermissionsGranted);
+
+ Permissions.onRequestPermissionsResult(activity, new String[]{
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
+ }, new int[]{
+ PackageManager.PERMISSION_DENIED
+ });
+
+ verify(onPermissionsDenied).run();
+ verify(onPermissionsGranted, never()).run();
+ }
+
+ @Test
+ public void testPromptingForNotGrantedPermissions() {
+ Activity activity = mockActivity();
+
+ PermissionsHelper helper = mockDenyingHelper();
+ Permissions.setPermissionHelper(helper);
+
+ Permissions.from(activity)
+ .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ .andFallback(mock(Runnable.class))
+ .run(mock(Runnable.class));
+
+ verify(helper).prompt(anyActivity(), any(String[].class));
+
+ Permissions.onRequestPermissionsResult(activity, new String[0], new int[0]);
+ }
+
+ @Test
+ public void testMultipleRequestsAreQueuedAndDispatchedSequentially() {
+ Activity activity = mockActivity();
+
+ PermissionsHelper helper = mockDenyingHelper();
+ Permissions.setPermissionHelper(helper);
+
+ Runnable onFirstPermissionGranted = mock(Runnable.class);
+ Runnable onSecondPermissionDenied = mock(Runnable.class);
+
+ Permissions.from(activity)
+ .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ .andFallback(mock(Runnable.class))
+ .run(onFirstPermissionGranted);
+
+ Permissions.from(activity)
+ .withPermissions(Manifest.permission.CAMERA)
+ .andFallback(onSecondPermissionDenied)
+ .run(mock(Runnable.class));
+
+
+ Permissions.onRequestPermissionsResult(activity, new String[] {
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
+ }, new int[] {
+ PackageManager.PERMISSION_GRANTED
+ });
+
+ verify(onFirstPermissionGranted).run();
+ verify(onSecondPermissionDenied, never()).run(); // Second request is queued but not executed yet
+
+ Permissions.onRequestPermissionsResult(activity, new String[]{
+ Manifest.permission.CAMERA
+ }, new int[]{
+ PackageManager.PERMISSION_DENIED
+ });
+
+ verify(onFirstPermissionGranted).run();
+ verify(onSecondPermissionDenied).run();
+
+ verify(helper, times(2)).prompt(anyActivity(), any(String[].class));
+ }
+
+ @Test
+ public void testSecondRequestWillNotPromptIfPermissionHasBeenGranted() {
+ Activity activity = mockActivity();
+
+ PermissionsHelper helper = mock(PermissionsHelper.class);
+ Permissions.setPermissionHelper(helper);
+ when(helper.hasPermissions(anyContext(), anyPermissions()))
+ .thenReturn(false)
+ .thenReturn(false)
+ .thenReturn(true); // Revaluation is successful
+
+ Permissions.from(activity)
+ .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ .andFallback(mock(Runnable.class))
+ .run(mock(Runnable.class));
+
+ Permissions.from(activity)
+ .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ .andFallback(mock(Runnable.class))
+ .run(mock(Runnable.class));
+
+ Permissions.onRequestPermissionsResult(activity, new String[]{
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
+ }, new int[]{
+ PackageManager.PERMISSION_GRANTED
+ });
+
+ verify(helper, times(1)).prompt(anyActivity(), any(String[].class));
+ }
+
+ @Test
+ public void testEmptyPermissionsArrayWillExecuteRunnableAndNotTryToPrompt() {
+ PermissionsHelper helper = spy(new PermissionsHelper());
+ Permissions.setPermissionHelper(helper);
+
+ Runnable onPermissionGranted = mock(Runnable.class);
+ Runnable onPermissionDenied = mock(Runnable.class);
+
+ Permissions.from(mockActivity())
+ .withPermissions()
+ .andFallback(onPermissionDenied)
+ .run(onPermissionGranted);
+
+ verify(onPermissionGranted).run();
+ verify(onPermissionDenied, never()).run();
+ verify(helper, never()).prompt(anyActivity(), any(String[].class));
+ }
+
+ @Test
+ public void testDoNotPromptBehavior() {
+ PermissionsHelper helper = mockDenyingHelper();
+ Permissions.setPermissionHelper(helper);
+
+ Permissions.from(mockActivity())
+ .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ .doNotPrompt()
+ .andFallback(mock(Runnable.class))
+ .run(mock(Runnable.class));
+
+ verify(helper, never()).prompt(anyActivity(), any(String[].class));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testThrowsExceptionIfNeedstoPromptWithNonActivityContext() {
+ Permissions.setPermissionHelper(mockDenyingHelper());
+
+ Permissions.from(mock(Context.class))
+ .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ .andFallback(mock(Runnable.class))
+ .run(mock(Runnable.class));
+ }
+
+ @Test
+ public void testDoNotPromptIfFalse() {
+ Activity activity = mockActivity();
+
+ PermissionsHelper helper = mockDenyingHelper();
+ Permissions.setPermissionHelper(helper);
+
+ Permissions.from(activity)
+ .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ .doNotPromptIf(false)
+ .andFallback(mock(Runnable.class))
+ .run(mock(Runnable.class));
+
+ verify(helper).prompt(anyActivity(), any(String[].class));
+
+ Permissions.onRequestPermissionsResult(activity, new String[0], new int[0]);
+ }
+
+ @Test
+ public void testDoNotPromptIfTrue() {
+ PermissionsHelper helper = mockDenyingHelper();
+ Permissions.setPermissionHelper(helper);
+
+ Permissions.from(mockActivity())
+ .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ .doNotPromptIf(true)
+ .andFallback(mock(Runnable.class))
+ .run(mock(Runnable.class));
+
+ verify(helper, never()).prompt(anyActivity(), any(String[].class));
+ }
+
+ private Activity mockActivity() {
+ return mock(Activity.class);
+ }
+
+ private PermissionsHelper mockGrantingHelper() {
+ PermissionsHelper helper = mock(PermissionsHelper.class);
+ doReturn(true).when(helper).hasPermissions(any(Context.class), anyPermissions());
+ return helper;
+ }
+
+ private PermissionsHelper mockDenyingHelper() {
+ PermissionsHelper helper = mock(PermissionsHelper.class);
+ doReturn(false).when(helper).hasPermissions(any(Context.class), anyPermissions());
+ return helper;
+ }
+
+ private String anyPermissions() {
+ return Matchers.anyVararg();
+ }
+
+ private Activity anyActivity() {
+ return any(Activity.class);
+ }
+
+ private Context anyContext() {
+ return any(Context.class);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/TestPushManager.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/TestPushManager.java
new file mode 100644
index 0000000000..42ae0f543f
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/TestPushManager.java
@@ -0,0 +1,238 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.push;
+
+import org.json.JSONObject;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.gcm.GcmTokenClient;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.UUID;
+
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Matchers.isNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+
+@RunWith(TestRunner.class)
+public class TestPushManager {
+ private PushState state;
+ private GcmTokenClient gcmTokenClient;
+ private PushClient pushClient;
+ private PushManager manager;
+
+ @Before
+ public void setUp() throws Exception {
+ state = new PushState(RuntimeEnvironment.application, "test.json");
+ gcmTokenClient = mock(GcmTokenClient.class);
+ doReturn(new Fetched("opaque-gcm-token", System.currentTimeMillis())).when(gcmTokenClient).getToken(anyString(), anyBoolean());
+
+ // Configure a mock PushClient.
+ pushClient = mock(PushClient.class);
+ doReturn(new RegisterUserAgentResponse("opaque-uaid", "opaque-secret"))
+ .when(pushClient)
+ .registerUserAgent(anyString());
+
+ doReturn(new SubscribeChannelResponse("opaque-chid", "https://localhost:8085/opaque-push-endpoint"))
+ .when(pushClient)
+ .subscribeChannel(anyString(), anyString(), isNull(String.class));
+
+ PushManager.PushClientFactory pushClientFactory = mock(PushManager.PushClientFactory.class);
+ doReturn(pushClient).when(pushClientFactory).getPushClient(anyString(), anyBoolean());
+
+ manager = new PushManager(state, gcmTokenClient, pushClientFactory);
+ }
+
+ private void assertOnlyConfigured(PushRegistration registration, String endpoint, boolean debug) {
+ Assert.assertNotNull(registration);
+ Assert.assertEquals(registration.autopushEndpoint, endpoint);
+ Assert.assertEquals(registration.debug, debug);
+ Assert.assertNull(registration.uaid.value);
+ }
+
+ private void assertRegistered(PushRegistration registration, String endpoint, boolean debug) {
+ Assert.assertNotNull(registration);
+ Assert.assertEquals(registration.autopushEndpoint, endpoint);
+ Assert.assertEquals(registration.debug, debug);
+ Assert.assertNotNull(registration.uaid.value);
+ }
+
+ private void assertSubscribed(PushSubscription subscription) {
+ Assert.assertNotNull(subscription);
+ Assert.assertNotNull(subscription.chid);
+ }
+
+ @Test
+ public void testConfigure() throws Exception {
+ PushRegistration registration = manager.configure("default", "http://localhost:8081", false, System.currentTimeMillis());
+ assertOnlyConfigured(registration, "http://localhost:8081", false);
+
+ registration = manager.configure("default", "http://localhost:8082", true, System.currentTimeMillis());
+ assertOnlyConfigured(registration, "http://localhost:8082", true);
+ }
+
+ @Test(expected=PushManager.ProfileNeedsConfigurationException.class)
+ public void testRegisterBeforeConfigure() throws Exception {
+ PushRegistration registration = state.getRegistration("default");
+ Assert.assertNull(registration);
+
+ // Trying to register a User Agent fails before configuration.
+ manager.registerUserAgent("default", System.currentTimeMillis());
+ }
+
+ @Test
+ public void testRegister() throws Exception {
+ PushRegistration registration = manager.configure("default", "http://localhost:8082", false, System.currentTimeMillis());
+ assertOnlyConfigured(registration, "http://localhost:8082", false);
+
+ // Let's register a User Agent, so that we can witness unregistration.
+ registration = manager.registerUserAgent("default", System.currentTimeMillis());
+ assertRegistered(registration, "http://localhost:8082", false);
+
+ // Changing the debug flag should update but not try to unregister the User Agent.
+ registration = manager.configure("default", "http://localhost:8082", true, System.currentTimeMillis());
+ assertRegistered(registration, "http://localhost:8082", true);
+
+ // Changing the configuration endpoint should update and try to unregister the User Agent.
+ registration = manager.configure("default", "http://localhost:8083", true, System.currentTimeMillis());
+ assertOnlyConfigured(registration, "http://localhost:8083", true);
+ }
+
+ @Test
+ public void testRegisterMultipleProfiles() throws Exception {
+ PushRegistration registration1 = manager.configure("default1", "http://localhost:8081", true, System.currentTimeMillis());
+ PushRegistration registration2 = manager.configure("default2", "http://localhost:8082", true, System.currentTimeMillis());
+ assertOnlyConfigured(registration1, "http://localhost:8081", true);
+ assertOnlyConfigured(registration2, "http://localhost:8082", true);
+ verify(gcmTokenClient, times(0)).getToken(anyString(), anyBoolean());
+
+ registration1 = manager.registerUserAgent("default1", System.currentTimeMillis());
+ assertRegistered(registration1, "http://localhost:8081", true);
+
+ registration2 = manager.registerUserAgent("default2", System.currentTimeMillis());
+ assertRegistered(registration2, "http://localhost:8082", true);
+
+ // Just the debug flag should not unregister the User Agent.
+ registration1 = manager.configure("default1", "http://localhost:8081", false, System.currentTimeMillis());
+ assertRegistered(registration1, "http://localhost:8081", false);
+
+ // But the configuration endpoint should unregister the correct User Agent.
+ registration2 = manager.configure("default2", "http://localhost:8083", false, System.currentTimeMillis());
+ }
+
+ @Test
+ public void testSubscribeChannel() throws Exception {
+ manager.configure("default", "http://localhost:8080", false, System.currentTimeMillis());
+ PushRegistration registration = manager.registerUserAgent("default", System.currentTimeMillis());
+ assertRegistered(registration, "http://localhost:8080", false);
+
+ // We should be able to register with non-null serviceData.
+ final JSONObject webpushData = new JSONObject();
+ webpushData.put("version", 5);
+ PushSubscription subscription = manager.subscribeChannel("default", "webpush", webpushData, null, System.currentTimeMillis());
+ assertSubscribed(subscription);
+
+ subscription = manager.registrationForSubscription(subscription.chid).getSubscription(subscription.chid);
+ Assert.assertNotNull(subscription);
+ Assert.assertEquals(5, subscription.serviceData.get("version"));
+
+ // We should be able to register with null serviceData.
+ subscription = manager.subscribeChannel("default", "sync", null, null, System.currentTimeMillis());
+ assertSubscribed(subscription);
+
+ subscription = manager.registrationForSubscription(subscription.chid).getSubscription(subscription.chid);
+ Assert.assertNotNull(subscription);
+ Assert.assertNull(subscription.serviceData);
+ }
+
+ @Test
+ public void testUnsubscribeChannel() throws Exception {
+ manager.configure("default", "http://localhost:8080", false, System.currentTimeMillis());
+ PushRegistration registration = manager.registerUserAgent("default", System.currentTimeMillis());
+ assertRegistered(registration, "http://localhost:8080", false);
+
+ // We should be able to register with non-null serviceData.
+ final JSONObject webpushData = new JSONObject();
+ webpushData.put("version", 5);
+ PushSubscription subscription = manager.subscribeChannel("default", "webpush", webpushData, null, System.currentTimeMillis());
+ assertSubscribed(subscription);
+
+ // No exception is success.
+ manager.unsubscribeChannel(subscription.chid);
+ }
+
+ public void testUnsubscribeUnknownChannel() throws Exception {
+ manager.configure("default", "http://localhost:8080", false, System.currentTimeMillis());
+ PushRegistration registration = manager.registerUserAgent("default", System.currentTimeMillis());
+ assertRegistered(registration, "http://localhost:8080", false);
+
+ doThrow(new RuntimeException())
+ .when(pushClient)
+ .unsubscribeChannel(anyString(), anyString(), anyString());
+
+ // Un-subscribing from an unknown channel succeeds: we just ignore the request.
+ manager.unsubscribeChannel(UUID.randomUUID().toString());
+ }
+
+ @Test
+ public void testStartupBeforeConfiguration() throws Exception {
+ verify(gcmTokenClient, never()).getToken(anyString(), anyBoolean());
+ manager.startup(System.currentTimeMillis());
+ verify(gcmTokenClient, times(1)).getToken(AppConstants.MOZ_ANDROID_GCM_SENDERID, false);
+ }
+
+ @Test
+ public void testStartupBeforeRegistration() throws Exception {
+ PushRegistration registration = manager.configure("default", "http://localhost:8080", true, System.currentTimeMillis());
+ assertOnlyConfigured(registration, "http://localhost:8080", true);
+
+ manager.startup(System.currentTimeMillis());
+ verify(gcmTokenClient, times(1)).getToken(anyString(), anyBoolean());
+ }
+
+ @Test
+ public void testStartupAfterRegistration() throws Exception {
+ PushRegistration registration = manager.configure("default", "http://localhost:8080", true, System.currentTimeMillis());
+ assertOnlyConfigured(registration, "http://localhost:8080", true);
+
+ registration = manager.registerUserAgent("default", System.currentTimeMillis());
+ assertRegistered(registration, "http://localhost:8080", true);
+
+ manager.startup(System.currentTimeMillis());
+
+ // Rather tautological.
+ PushRegistration updatedRegistration = manager.state.getRegistration("default");
+ Assert.assertEquals(registration.uaid, updatedRegistration.uaid);
+ }
+
+ @Test
+ public void testStartupAfterSubscription() throws Exception {
+ PushRegistration registration = manager.configure("default", "http://localhost:8080", true, System.currentTimeMillis());
+ assertOnlyConfigured(registration, "http://localhost:8080", true);
+
+ registration = manager.registerUserAgent("default", System.currentTimeMillis());
+ assertRegistered(registration, "http://localhost:8080", true);
+
+ PushSubscription subscription = manager.subscribeChannel("default", "webpush", null, null, System.currentTimeMillis());
+ assertSubscribed(subscription);
+
+ manager.startup(System.currentTimeMillis());
+
+ // Rather tautological.
+ registration = manager.registrationForSubscription(subscription.chid);
+ PushSubscription updatedSubscription = registration.getSubscription(subscription.chid);
+ Assert.assertEquals(subscription.chid, updatedSubscription.chid);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/TestPushState.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/TestPushState.java
new file mode 100644
index 0000000000..cb7c7ec68b
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/TestPushState.java
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.push;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.io.File;
+import java.io.FileOutputStream;
+
+@RunWith(TestRunner.class)
+public class TestPushState {
+ @Test
+ public void testRoundTrip() throws Exception {
+ final PushState state = new PushState(RuntimeEnvironment.application, "test.json");
+ // Fresh state should have no registrations (and no subscriptions).
+ Assert.assertTrue(state.registrations.isEmpty());
+
+ final PushRegistration registration = new PushRegistration("endpoint", true, Fetched.now("uaid"), "secret");
+ final PushSubscription subscription = new PushSubscription("chid", "profileName", "webpushEndpoint", "service", null);
+ registration.putSubscription("chid", subscription);
+ state.putRegistration("profileName", registration);
+ Assert.assertEquals(1, state.registrations.size());
+ state.checkpoint();
+
+ final PushState readState = new PushState(RuntimeEnvironment.application, "test.json");
+ Assert.assertEquals(1, readState.registrations.size());
+ final PushRegistration storedRegistration = readState.getRegistration("profileName");
+ Assert.assertEquals(registration, storedRegistration);
+
+ Assert.assertEquals(1, storedRegistration.subscriptions.size());
+ final PushSubscription storedSubscription = storedRegistration.getSubscription("chid");
+ Assert.assertEquals(subscription, storedSubscription);
+ }
+
+ @Test
+ public void testMissingRegistration() throws Exception {
+ final PushState state = new PushState(RuntimeEnvironment.application, "testMissingRegistration.json");
+ Assert.assertNull(state.getRegistration("missingProfileName"));
+ }
+
+ @Test
+ public void testMissingSubscription() throws Exception {
+ final PushRegistration registration = new PushRegistration("endpoint", true, Fetched.now("uaid"), "secret");
+ Assert.assertNull(registration.getSubscription("missingChid"));
+ }
+
+ @Test
+ public void testCorruptedJSON() throws Exception {
+ // Write some malformed JSON.
+ // TODO: use mcomella's helpers!
+ final File file = new File(RuntimeEnvironment.application.getApplicationInfo().dataDir, "testCorruptedJSON.json");
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(file);
+ fos.write("}".getBytes("UTF-8"));
+ } finally {
+ if (fos != null) {
+ fos.close();
+ }
+ }
+
+ final PushState state = new PushState(RuntimeEnvironment.application, "testCorruptedJSON.json");
+ Assert.assertTrue(state.getRegistrations().isEmpty());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestAutopushClient.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestAutopushClient.java
new file mode 100644
index 0000000000..93e0d14e5f
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestAutopushClient.java
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.push.autopush.test;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.push.autopush.AutopushClient;
+import org.mozilla.gecko.push.autopush.AutopushClientException;
+import org.mozilla.gecko.sync.Utils;
+
+@RunWith(TestRunner.class)
+public class TestAutopushClient {
+ @Test
+ public void testGetSenderID() throws Exception {
+ final AutopushClient client = new AutopushClient("https://updates-autopush-dev.stage.mozaws.net/v1/gcm/829133274407",
+ Utils.newSynchronousExecutor());
+ Assert.assertEquals("829133274407", client.getSenderIDFromServerURI());
+ }
+
+ @Test(expected=AutopushClientException.class)
+ public void testGetNoSenderID() throws Exception {
+ final AutopushClient client = new AutopushClient("https://updates-autopush-dev.stage.mozaws.net/v1/gcm",
+ Utils.newSynchronousExecutor());
+ client.getSenderIDFromServerURI();
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestLiveAutopushClient.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestLiveAutopushClient.java
new file mode 100644
index 0000000000..102ea34e48
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestLiveAutopushClient.java
@@ -0,0 +1,171 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.push.autopush.test;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PublicKey;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+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.AutopushClient.RequestDelegate;
+import org.mozilla.gecko.push.autopush.AutopushClientException;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.net.BaseResource;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.startsWith;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+/**
+ * This test straddles an awkward line: it uses Mockito, but doesn't actually mock the service
+ * endpoint. That's why it's a <b>live</b> test: most of its value is checking that the client
+ * implementation and the upstream server implementation are corresponding correctly.
+ */
+@RunWith(TestRunner.class)
+@Ignore("Live test that requires network connection -- remove this line to run this test.")
+public class TestLiveAutopushClient {
+ final String serverURL = "https://updates-autopush.stage.mozaws.net/v1/gcm/829133274407";
+
+ protected AutopushClient client;
+
+ @Before
+ public void setUp() throws Exception {
+ BaseResource.rewriteLocalhost = false;
+ client = new AutopushClient(serverURL, Utils.newSynchronousExecutor());
+ }
+
+ protected <T> T assertSuccess(RequestDelegate<T> delegate, Class<T> klass) {
+ verify(delegate, never()).handleError(any(Exception.class));
+ verify(delegate, never()).handleFailure(any(AutopushClientException.class));
+
+ final ArgumentCaptor<T> register = ArgumentCaptor.forClass(klass);
+ verify(delegate).handleSuccess(register.capture());
+
+ return register.getValue();
+ }
+
+ protected <T> AutopushClientException assertFailure(RequestDelegate<T> delegate, Class<T> klass) {
+ verify(delegate, never()).handleError(any(Exception.class));
+ verify(delegate, never()).handleSuccess(any(klass));
+
+ final ArgumentCaptor<AutopushClientException> failure = ArgumentCaptor.forClass(AutopushClientException.class);
+ verify(delegate).handleFailure(failure.capture());
+
+ return failure.getValue();
+ }
+
+ @Test
+ public void testUserAgent() throws Exception {
+ final RequestDelegate<RegisterUserAgentResponse> registerDelegate = mock(RequestDelegate.class);
+ client.registerUserAgent(Utils.generateGuid(), registerDelegate);
+
+ final RegisterUserAgentResponse registerResponse = assertSuccess(registerDelegate, RegisterUserAgentResponse.class);
+ Assert.assertNotNull(registerResponse);
+ Assert.assertNotNull(registerResponse.uaid);
+ Assert.assertNotNull(registerResponse.secret);
+
+ // Reregistering with a new GUID should succeed.
+ final RequestDelegate<Void> reregisterDelegate = mock(RequestDelegate.class);
+ client.reregisterUserAgent(registerResponse.uaid, registerResponse.secret, Utils.generateGuid(), reregisterDelegate);
+
+ Assert.assertNull(assertSuccess(reregisterDelegate, Void.class));
+
+ // Unregistering should succeed.
+ final RequestDelegate<Void> unregisterDelegate = mock(RequestDelegate.class);
+ client.unregisterUserAgent(registerResponse.uaid, registerResponse.secret, unregisterDelegate);
+
+ Assert.assertNull(assertSuccess(unregisterDelegate, Void.class));
+
+ // Trying to unregister a second time should give a 404.
+ final RequestDelegate<Void> reunregisterDelegate = mock(RequestDelegate.class);
+ client.unregisterUserAgent(registerResponse.uaid, registerResponse.secret, reunregisterDelegate);
+
+ final AutopushClientException failureException = assertFailure(reunregisterDelegate, Void.class);
+ Assert.assertThat(failureException, instanceOf(AutopushClientException.AutopushClientRemoteException.class));
+ Assert.assertTrue(((AutopushClientException.AutopushClientRemoteException) failureException).isGone());
+ }
+
+ @Test
+ public void testChannel() throws Exception {
+ final RequestDelegate<RegisterUserAgentResponse> registerDelegate = mock(RequestDelegate.class);
+ client.registerUserAgent(Utils.generateGuid(), registerDelegate);
+
+ final RegisterUserAgentResponse registerResponse = assertSuccess(registerDelegate, RegisterUserAgentResponse.class);
+ Assert.assertNotNull(registerResponse);
+ Assert.assertNotNull(registerResponse.uaid);
+ Assert.assertNotNull(registerResponse.secret);
+
+ // We should be able to subscribe to a channel.
+ final RequestDelegate<SubscribeChannelResponse> subscribeDelegate = mock(RequestDelegate.class);
+ client.subscribeChannel(registerResponse.uaid, registerResponse.secret, null, subscribeDelegate);
+
+ final SubscribeChannelResponse subscribeResponse = assertSuccess(subscribeDelegate, SubscribeChannelResponse.class);
+ Assert.assertNotNull(subscribeResponse);
+ Assert.assertNotNull(subscribeResponse.channelID);
+ Assert.assertNotNull(subscribeResponse.endpoint);
+ Assert.assertThat(subscribeResponse.endpoint, startsWith(FxAccountUtils.getAudienceForURL(serverURL)));
+ Assert.assertThat(subscribeResponse.endpoint, containsString("/v1/"));
+
+ // And we should be able to unsubscribe.
+ final RequestDelegate<Void> unsubscribeDelegate = mock(RequestDelegate.class);
+ client.unsubscribeChannel(registerResponse.uaid, registerResponse.secret, subscribeResponse.channelID, unsubscribeDelegate);
+
+ Assert.assertNull(assertSuccess(unsubscribeDelegate, Void.class));
+
+ // We should be able to create a restricted subscription by specifying
+ // an ECDSA public key using the P-256 curve.
+ final RequestDelegate<SubscribeChannelResponse> subscribeWithKeyDelegate = mock(RequestDelegate.class);
+ final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ECDSA");
+ keyPairGenerator.initialize(256);
+ final KeyPair keyPair = keyPairGenerator.generateKeyPair();
+ final PublicKey publicKey = keyPair.getPublic();
+ String appServerKey = Base64.encodeBase64URLSafeString(publicKey.getEncoded());
+ client.subscribeChannel(registerResponse.uaid, registerResponse.secret, appServerKey, subscribeWithKeyDelegate);
+
+ final SubscribeChannelResponse subscribeWithKeyResponse = assertSuccess(subscribeWithKeyDelegate, SubscribeChannelResponse.class);
+ Assert.assertNotNull(subscribeWithKeyResponse);
+ Assert.assertNotNull(subscribeWithKeyResponse.channelID);
+ Assert.assertNotNull(subscribeWithKeyResponse.endpoint);
+ Assert.assertThat(subscribeWithKeyResponse.endpoint, startsWith(FxAccountUtils.getAudienceForURL(serverURL)));
+ Assert.assertThat(subscribeWithKeyResponse.endpoint, containsString("/v2/"));
+
+ // And we should be able to drop the restricted subscription.
+ final RequestDelegate<Void> unsubscribeWithKeyDelegate = mock(RequestDelegate.class);
+ client.unsubscribeChannel(registerResponse.uaid, registerResponse.secret, subscribeWithKeyResponse.channelID, unsubscribeWithKeyDelegate);
+
+ Assert.assertNull(assertSuccess(unsubscribeWithKeyDelegate, Void.class));
+
+ // Trying to unsubscribe a second time should give a 410.
+ final RequestDelegate<Void> reunsubscribeDelegate = mock(RequestDelegate.class);
+ client.unsubscribeChannel(registerResponse.uaid, registerResponse.secret, subscribeResponse.channelID, reunsubscribeDelegate);
+
+ final AutopushClientException reunsubscribeFailureException = assertFailure(reunsubscribeDelegate, Void.class);
+ Assert.assertThat(reunsubscribeFailureException, instanceOf(AutopushClientException.AutopushClientRemoteException.class));
+ Assert.assertTrue(((AutopushClientException.AutopushClientRemoteException) reunsubscribeFailureException).isGone());
+
+ // Trying to unsubscribe from a non-existent channel should give a 404. Right now it gives a 401!
+ final RequestDelegate<Void> badUnsubscribeDelegate = mock(RequestDelegate.class);
+ client.unsubscribeChannel(registerResponse.uaid + "BAD", registerResponse.secret, subscribeResponse.channelID, badUnsubscribeDelegate);
+
+ final AutopushClientException badUnsubscribeFailureException = assertFailure(badUnsubscribeDelegate, Void.class);
+ Assert.assertThat(badUnsubscribeFailureException, instanceOf(AutopushClientException.AutopushClientRemoteException.class));
+ Assert.assertTrue(((AutopushClientException.AutopushClientRemoteException) badUnsubscribeFailureException).isInvalidAuthentication());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestBase32.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestBase32.java
new file mode 100644
index 0000000000..7047d67d33
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestBase32.java
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.crypto.test;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.apache.commons.codec.binary.Base32;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.Utils;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
+public class TestBase32 {
+
+ public static void assertSame(byte[] arrayOne, byte[] arrayTwo) {
+ assertTrue(Arrays.equals(arrayOne, arrayTwo));
+ }
+
+ @Test
+ public void testBase32() throws UnsupportedEncodingException {
+ byte[] decoded = new Base32().decode("MZXW6YTBOI======");
+ byte[] expected = "foobar".getBytes();
+ assertSame(decoded, expected);
+
+ byte[] encoded = new Base32().encode("fooba".getBytes());
+ expected = "MZXW6YTB".getBytes();
+ assertSame(encoded, expected);
+ }
+
+ @Test
+ public void testFriendlyBase32() {
+ // These checks are drawn from Firefox, test_utils_encodeBase32.js.
+ byte[] decoded = Utils.decodeFriendlyBase32("mzxw6ytb9jrgcztpn5rgc4tcme");
+ byte[] expected = "foobarbafoobarba".getBytes();
+ assertEquals(decoded.length, 16);
+ assertSame(decoded, expected);
+
+ // These are real values extracted from the Service object in a Firefox profile.
+ String base32Key = "6m8mv8ex2brqnrmsb9fjuvfg7y";
+ String expectedHex = "f316caac97d06306c5920b8a9a54a6fe";
+
+ byte[] computedBytes = Utils.decodeFriendlyBase32(base32Key);
+ byte[] expectedBytes = Utils.hex2Byte(expectedHex);
+
+ assertSame(computedBytes, expectedBytes);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestCryptoInfo.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestCryptoInfo.java
new file mode 100644
index 0000000000..3e8d90e2fd
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestCryptoInfo.java
@@ -0,0 +1,144 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.crypto.test;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.CryptoInfo;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
+public class TestCryptoInfo {
+
+ @Test
+ public void testEncryptedHMACIsSet() throws CryptoException, UnsupportedEncodingException, InvalidKeyException, NoSuchAlgorithmException {
+ KeyBundle kb = KeyBundle.withRandomKeys();
+ CryptoInfo encrypted = CryptoInfo.encrypt("plaintext".getBytes("UTF-8"), kb);
+ assertSame(kb, encrypted.getKeys());
+ assertTrue(encrypted.generatedHMACIsHMAC());
+ }
+
+ @Test
+ public void testRandomEncryptedDecrypted() throws CryptoException, UnsupportedEncodingException, InvalidKeyException, NoSuchAlgorithmException {
+ KeyBundle kb = KeyBundle.withRandomKeys();
+ byte[] plaintext = "plaintext".getBytes("UTF-8");
+ CryptoInfo info = CryptoInfo.encrypt(plaintext, kb);
+ byte[] iv = info.getIV();
+ info.decrypt();
+ assertArrayEquals(plaintext, info.getMessage());
+ assertSame(null, info.getHMAC());
+ assertArrayEquals(iv, info.getIV());
+ assertSame(kb, info.getKeys());
+ }
+
+ @Test
+ public void testDecrypt() throws CryptoException {
+ String base64CipherText = "NMsdnRulLwQsVcwxKW9XwaUe7ouJk5Wn" +
+ "80QhbD80l0HEcZGCynh45qIbeYBik0lg" +
+ "cHbKmlIxTJNwU+OeqipN+/j7MqhjKOGI" +
+ "lvbpiPQQLC6/ffF2vbzL0nzMUuSyvaQz" +
+ "yGGkSYM2xUFt06aNivoQTvU2GgGmUK6M" +
+ "vadoY38hhW2LCMkoZcNfgCqJ26lO1O0s" +
+ "EO6zHsk3IVz6vsKiJ2Hq6VCo7hu123wN" +
+ "egmujHWQSGyf8JeudZjKzfi0OFRRvvm4" +
+ "QAKyBWf0MgrW1F8SFDnVfkq8amCB7Nhd" +
+ "whgLWbN+21NitNwWYknoEWe1m6hmGZDg" +
+ "DT32uxzWxCV8QqqrpH/ZggViEr9uMgoy" +
+ "4lYaWqP7G5WKvvechc62aqnsNEYhH26A" +
+ "5QgzmlNyvB+KPFvPsYzxDnSCjOoRSLx7" +
+ "GG86wT59QZw=";
+ String base64IV = "GX8L37AAb2FZJMzIoXlX8w==";
+ String base16Hmac = "b1e6c18ac30deb70236bc0d65a46f7a4" +
+ "dce3b8b0e02cf92182b914e3afa5eebc";
+ String base64EncryptionKey = "9K/wLdXdw+nrTtXo4ZpECyHFNr4d7aYH" +
+ "qeg3KW9+m6Q=";
+ String base64HmacKey = "MMntEfutgLTc8FlTLQFms8/xMPmCldqP" +
+ "lq/QQXEjx70=";
+ String base64ExpectedBytes = "eyJpZCI6IjVxUnNnWFdSSlpYciIsImhp" +
+ "c3RVcmkiOiJmaWxlOi8vL1VzZXJzL2ph" +
+ "c29uL0xpYnJhcnkvQXBwbGljYXRpb24l" +
+ "MjBTdXBwb3J0L0ZpcmVmb3gvUHJvZmls" +
+ "ZXMva3NnZDd3cGsuTG9jYWxTeW5jU2Vy" +
+ "dmVyL3dlYXZlL2xvZ3MvIiwidGl0bGUi" +
+ "OiJJbmRleCBvZiBmaWxlOi8vL1VzZXJz" +
+ "L2phc29uL0xpYnJhcnkvQXBwbGljYXRp" +
+ "b24gU3VwcG9ydC9GaXJlZm94L1Byb2Zp" +
+ "bGVzL2tzZ2Q3d3BrLkxvY2FsU3luY1Nl" +
+ "cnZlci93ZWF2ZS9sb2dzLyIsInZpc2l0" +
+ "cyI6W3siZGF0ZSI6MTMxOTE0OTAxMjM3" +
+ "MjQyNSwidHlwZSI6MX1dfQ==";
+
+ CryptoInfo decrypted = CryptoInfo.decrypt(
+ Base64.decodeBase64(base64CipherText),
+ Base64.decodeBase64(base64IV),
+ Utils.hex2Byte(base16Hmac),
+ new KeyBundle(
+ Base64.decodeBase64(base64EncryptionKey),
+ Base64.decodeBase64(base64HmacKey))
+ );
+
+ assertArrayEquals(decrypted.getMessage(), Base64.decodeBase64(base64ExpectedBytes));
+ }
+
+ @Test
+ public void testEncrypt() throws CryptoException {
+ String base64CipherText = "NMsdnRulLwQsVcwxKW9XwaUe7ouJk5Wn" +
+ "80QhbD80l0HEcZGCynh45qIbeYBik0lg" +
+ "cHbKmlIxTJNwU+OeqipN+/j7MqhjKOGI" +
+ "lvbpiPQQLC6/ffF2vbzL0nzMUuSyvaQz" +
+ "yGGkSYM2xUFt06aNivoQTvU2GgGmUK6M" +
+ "vadoY38hhW2LCMkoZcNfgCqJ26lO1O0s" +
+ "EO6zHsk3IVz6vsKiJ2Hq6VCo7hu123wN" +
+ "egmujHWQSGyf8JeudZjKzfi0OFRRvvm4" +
+ "QAKyBWf0MgrW1F8SFDnVfkq8amCB7Nhd" +
+ "whgLWbN+21NitNwWYknoEWe1m6hmGZDg" +
+ "DT32uxzWxCV8QqqrpH/ZggViEr9uMgoy" +
+ "4lYaWqP7G5WKvvechc62aqnsNEYhH26A" +
+ "5QgzmlNyvB+KPFvPsYzxDnSCjOoRSLx7" +
+ "GG86wT59QZw=";
+ String base64IV = "GX8L37AAb2FZJMzIoXlX8w==";
+ String base16Hmac = "b1e6c18ac30deb70236bc0d65a46f7a4" +
+ "dce3b8b0e02cf92182b914e3afa5eebc";
+ String base64EncryptionKey = "9K/wLdXdw+nrTtXo4ZpECyHFNr4d7aYH" +
+ "qeg3KW9+m6Q=";
+ String base64HmacKey = "MMntEfutgLTc8FlTLQFms8/xMPmCldqP" +
+ "lq/QQXEjx70=";
+ String base64ExpectedBytes = "eyJpZCI6IjVxUnNnWFdSSlpYciIsImhp" +
+ "c3RVcmkiOiJmaWxlOi8vL1VzZXJzL2ph" +
+ "c29uL0xpYnJhcnkvQXBwbGljYXRpb24l" +
+ "MjBTdXBwb3J0L0ZpcmVmb3gvUHJvZmls" +
+ "ZXMva3NnZDd3cGsuTG9jYWxTeW5jU2Vy" +
+ "dmVyL3dlYXZlL2xvZ3MvIiwidGl0bGUi" +
+ "OiJJbmRleCBvZiBmaWxlOi8vL1VzZXJz" +
+ "L2phc29uL0xpYnJhcnkvQXBwbGljYXRp" +
+ "b24gU3VwcG9ydC9GaXJlZm94L1Byb2Zp" +
+ "bGVzL2tzZ2Q3d3BrLkxvY2FsU3luY1Nl" +
+ "cnZlci93ZWF2ZS9sb2dzLyIsInZpc2l0" +
+ "cyI6W3siZGF0ZSI6MTMxOTE0OTAxMjM3" +
+ "MjQyNSwidHlwZSI6MX1dfQ==";
+
+ CryptoInfo encrypted = CryptoInfo.encrypt(
+ Base64.decodeBase64(base64ExpectedBytes),
+ Base64.decodeBase64(base64IV),
+ new KeyBundle(
+ Base64.decodeBase64(base64EncryptionKey),
+ Base64.decodeBase64(base64HmacKey))
+ );
+
+ assertArrayEquals(Base64.decodeBase64(base64CipherText), encrypted.getMessage());
+ assertArrayEquals(Utils.hex2Byte(base16Hmac), encrypted.getHMAC());
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestHKDF.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestHKDF.java
new file mode 100644
index 0000000000..09973eeff4
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestHKDF.java
@@ -0,0 +1,143 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.crypto.test;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.crypto.HKDF;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+
+import java.util.Arrays;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/*
+ * This class tests the HKDF.java class.
+ * The tests are the 3 HMAC-based test cases
+ * from the RFC 5869 specification.
+ */
+@RunWith(TestRunner.class)
+public class TestHKDF {
+ @Test
+ public void testCase1() {
+ String IKM = "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b";
+ String salt = "000102030405060708090a0b0c";
+ String info = "f0f1f2f3f4f5f6f7f8f9";
+ int L = 42;
+ String PRK = "077709362c2e32df0ddc3f0dc47bba63" +
+ "90b6c73bb50f9c3122ec844ad7c2b3e5";
+ String OKM = "3cb25f25faacd57a90434f64d0362f2a" +
+ "2d2d0a90cf1a5a4c5db02d56ecc4c5bf" +
+ "34007208d5b887185865";
+
+ assertTrue(doStep1(IKM, salt, PRK));
+ assertTrue(doStep2(PRK, info, L, OKM));
+ }
+
+ @Test
+ public void testCase2() {
+ String IKM = "000102030405060708090a0b0c0d0e0f" +
+ "101112131415161718191a1b1c1d1e1f" +
+ "202122232425262728292a2b2c2d2e2f" +
+ "303132333435363738393a3b3c3d3e3f" +
+ "404142434445464748494a4b4c4d4e4f";
+ String salt = "606162636465666768696a6b6c6d6e6f" +
+ "707172737475767778797a7b7c7d7e7f" +
+ "808182838485868788898a8b8c8d8e8f" +
+ "909192939495969798999a9b9c9d9e9f" +
+ "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf";
+ String info = "b0b1b2b3b4b5b6b7b8b9babbbcbdbebf" +
+ "c0c1c2c3c4c5c6c7c8c9cacbcccdcecf" +
+ "d0d1d2d3d4d5d6d7d8d9dadbdcdddedf" +
+ "e0e1e2e3e4e5e6e7e8e9eaebecedeeef" +
+ "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff";
+ int L = 82;
+ String PRK = "06a6b88c5853361a06104c9ceb35b45c" +
+ "ef760014904671014a193f40c15fc244";
+ String OKM = "b11e398dc80327a1c8e7f78c596a4934" +
+ "4f012eda2d4efad8a050cc4c19afa97c" +
+ "59045a99cac7827271cb41c65e590e09" +
+ "da3275600c2f09b8367793a9aca3db71" +
+ "cc30c58179ec3e87c14c01d5c1f3434f" +
+ "1d87";
+
+ assertTrue(doStep1(IKM, salt, PRK));
+ assertTrue(doStep2(PRK, info, L, OKM));
+ }
+
+ @Test
+ public void testCase3() {
+ String IKM = "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b";
+ String salt = "";
+ String info = "";
+ int L = 42;
+ String PRK = "19ef24a32c717b167f33a91d6f648bdf" +
+ "96596776afdb6377ac434c1c293ccb04";
+ String OKM = "8da4e775a563c18f715f802a063c5a31" +
+ "b8a11f5c5ee1879ec3454e5f3c738d2d" +
+ "9d201395faa4b61a96c8";
+
+ assertTrue(doStep1(IKM, salt, PRK));
+ assertTrue(doStep2(PRK, info, L, OKM));
+ }
+
+ /*
+ * Tests the code for getting the keys necessary to
+ * decrypt the crypto keys bundle for Mozilla Sync.
+ *
+ * This operation is just a tailored version of the
+ * standard to get only the 2 keys we need.
+ */
+ @Test
+ public void testGetCryptoKeysBundleKeys() {
+ String username = "smqvooxj664hmrkrv6bw4r4vkegjhkns";
+ String friendlyBase32SyncKey = "gbh7teqqcgyzd65svjgibd7tqy";
+ String base64EncryptionKey = "069EnS3EtDK4y1tZ1AyKX+U7WEsWRp9bRIKLdW/7aoE=";
+ String base64HmacKey = "LF2YCS1QCgSNCf0BCQvQ06SGH8jqJDi9dKj0O+b0fwI=";
+
+ KeyBundle bundle = null;
+ try {
+ bundle = new KeyBundle(username, friendlyBase32SyncKey);
+ } catch (Exception e) {
+ fail("Unexpected exception " + e);
+ }
+
+ byte[] expectedEncryptionKey = Base64.decodeBase64(base64EncryptionKey);
+ byte[] expectedHMACKey = Base64.decodeBase64(base64HmacKey);
+ assertTrue(Arrays.equals(bundle.getEncryptionKey(), expectedEncryptionKey));
+ assertTrue(Arrays.equals(bundle.getHMACKey(), expectedHMACKey));
+ }
+
+ /*
+ * Helper to do step 1 of RFC 5869.
+ */
+ private boolean doStep1(String IKM, String salt, String PRK) {
+ try {
+ byte[] prkResult = HKDF.hkdfExtract(Utils.hex2Byte(salt), Utils.hex2Byte(IKM));
+ byte[] prkExpect = Utils.hex2Byte(PRK);
+ return Arrays.equals(prkResult, prkExpect);
+ } catch (Exception e) {
+ fail("Unexpected exception " + e);
+ }
+ return false;
+ }
+
+ /*
+ * Helper to do step 2 of RFC 5869.
+ */
+ private boolean doStep2(String PRK, String info, int L, String OKM) {
+ try {
+ byte[] okmResult = HKDF.hkdfExpand(Utils.hex2Byte(PRK), Utils.hex2Byte(info), L);
+ byte[] okmExpect = Utils.hex2Byte(OKM);
+ return Arrays.equals(okmResult, okmExpect);
+ } catch (Exception e) {
+ fail("Unexpected exception " + e);
+ }
+ return false;
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestKeyBundle.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestKeyBundle.java
new file mode 100644
index 0000000000..3c3edb9f83
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestKeyBundle.java
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.crypto.test;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
+public class TestKeyBundle {
+ @Test
+ public void testCreateKeyBundle() throws UnsupportedEncodingException, CryptoException {
+ String username = "smqvooxj664hmrkrv6bw4r4vkegjhkns";
+ String friendlyBase32SyncKey = "gbh7teqqcgyzd65svjgibd7tqy";
+ String base64EncryptionKey = "069EnS3EtDK4y1tZ1AyKX+U7WEsWRp9b" +
+ "RIKLdW/7aoE=";
+ String base64HmacKey = "LF2YCS1QCgSNCf0BCQvQ06SGH8jqJDi9" +
+ "dKj0O+b0fwI=";
+
+ KeyBundle keys = new KeyBundle(username, friendlyBase32SyncKey);
+ assertArrayEquals(keys.getEncryptionKey(), Base64.decodeBase64(base64EncryptionKey.getBytes("UTF-8")));
+ assertArrayEquals(keys.getHMACKey(), Base64.decodeBase64(base64HmacKey.getBytes("UTF-8")));
+ }
+
+ /*
+ * Basic sanity check to make sure length of keys is correct (32 bytes).
+ * Also make sure that the two keys are different.
+ */
+ @Test
+ public void testGenerateRandomKeys() throws CryptoException {
+ KeyBundle keys = KeyBundle.withRandomKeys();
+
+ assertEquals(32, keys.getEncryptionKey().length);
+ assertEquals(32, keys.getHMACKey().length);
+
+ boolean equal = Arrays.equals(keys.getEncryptionKey(), keys.getHMACKey());
+ assertEquals(false, equal);
+ }
+
+ @Test
+ public void testEquals() throws CryptoException {
+ KeyBundle k = KeyBundle.withRandomKeys();
+ KeyBundle o = KeyBundle.withRandomKeys();
+ assertFalse(k.equals("test"));
+ assertFalse(k.equals(o));
+ assertTrue(k.equals(k));
+ assertTrue(o.equals(o));
+ o.setHMACKey(k.getHMACKey());
+ assertFalse(o.equals(k));
+ o.setEncryptionKey(k.getEncryptionKey());
+ assertTrue(o.equals(k));
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestPBKDF2.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestPBKDF2.java
new file mode 100644
index 0000000000..d2d1d8271c
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestPBKDF2.java
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.crypto.test;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.crypto.PBKDF2;
+
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * Test PBKDF2 implementations against vectors from
+ * <dl>
+ * <dt>SHA-256</dt>
+ * <dd><a href="https://github.com/ircmaxell/PHP-PasswordLib/blob/master/test/Data/Vectors/pbkdf2-draft-josefsson-sha256.test-vectors">https://github.com/ircmaxell/PHP-PasswordLib/blob/master/test/Data/Vectors/pbkdf2-draft-josefsson-sha256.test-vectors</a></dd>
+ * <dd><a href="https://gitorious.org/scrypt/nettle-scrypt/blobs/37c0d5288e991604fe33dba2f1724986a8dddf56/testsuite/pbkdf2-test.c">https://gitorious.org/scrypt/nettle-scrypt/blobs/37c0d5288e991604fe33dba2f1724986a8dddf56/testsuite/pbkdf2-test.c</a></dd>
+ * </dl>
+ */
+@RunWith(TestRunner.class)
+public class TestPBKDF2 {
+
+ @Test
+ public final void testPBKDF2SHA256A() throws UnsupportedEncodingException, GeneralSecurityException {
+ String p = "password";
+ String s = "salt";
+ int dkLen = 32;
+
+ checkPBKDF2SHA256(p, s, 1, dkLen, "120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b");
+ checkPBKDF2SHA256(p, s, 4096, dkLen, "c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a");
+ }
+
+ @Test
+ public final void testPBKDF2SHA256B() throws UnsupportedEncodingException, GeneralSecurityException {
+ String p = "passwordPASSWORDpassword";
+ String s = "saltSALTsaltSALTsaltSALTsaltSALTsalt";
+ int dkLen = 40;
+
+ checkPBKDF2SHA256(p, s, 4096, dkLen, "348c89dbcbd32b2f32d814b8116e84cf2b17347ebc1800181c4e2a1fb8dd53e1c635518c7dac47e9");
+ }
+
+ @Test
+ public final void testPBKDF2SHA256scryptA() throws UnsupportedEncodingException, GeneralSecurityException {
+ String p = "passwd";
+ String s = "salt";
+ int dkLen = 64;
+
+ checkPBKDF2SHA256(p, s, 1, dkLen, "55ac046e56e3089fec1691c22544b605f94185216dde0465e68b9d57c20dacbc49ca9cccf179b645991664b39d77ef317c71b845b1e30bd509112041d3a19783");
+ }
+
+ /*
+ // This test takes eight seconds or so to run, so we don't run it.
+ @Test
+ public final void testPBKDF2SHA256scryptB() throws UnsupportedEncodingException, GeneralSecurityException {
+ String p = "Password";
+ String s = "NaCl";
+ int dkLen = 64;
+
+ checkPBKDF2SHA256(p, s, 80000, dkLen, "4ddcd8f60b98be21830cee5ef22701f9641a4418d04c0414aeff08876b34ab56a1d425a1225833549adb841b51c9b3176a272bdebba1d078478f62b397f33c8d");
+ }
+ */
+
+ @Test
+ public final void testPBKDF2SHA256C() throws UnsupportedEncodingException, GeneralSecurityException {
+ String p = "pass\0word";
+ String s = "sa\0lt";
+ int dkLen = 16;
+
+ checkPBKDF2SHA256(p, s, 4096, dkLen, "89b69d0516f829893c696226650a8687");
+ }
+
+ /*
+ // This test takes two or three minutes to run, so we don't run it.
+ public final void testPBKDF2SHA256D() throws UnsupportedEncodingException, GeneralSecurityException {
+ String p = "password";
+ String s = "salt";
+ int dkLen = 32;
+
+ checkPBKDF2SHA256(p, s, 16777216, dkLen, "cf81c66fe8cfc04d1f31ecb65dab4089f7f179e89b3b0bcb17ad10e3ac6eba46");
+ }
+ */
+
+ /*
+ // This test takes eight seconds or so to run, so we don't run it.
+ @Test
+ public final void testTimePBKDF2SHA256() throws UnsupportedEncodingException, GeneralSecurityException {
+ checkPBKDF2SHA256("password", "salt", 80000, 32, null);
+ }
+ */
+
+ private void checkPBKDF2SHA256(String p, String s, int c, int dkLen,
+ final String expectedStr)
+ throws GeneralSecurityException, UnsupportedEncodingException {
+ long start = System.currentTimeMillis();
+ byte[] key = PBKDF2.pbkdf2SHA256(p.getBytes("US-ASCII"), s.getBytes("US-ASCII"), c, dkLen);
+ assertNotNull(key);
+
+ long end = System.currentTimeMillis();
+
+ System.err.println("SHA-256 " + c + " took " + (end - start) + "ms");
+ if (expectedStr == null) {
+ return;
+ }
+
+ assertEquals(dkLen, Utils.hex2Byte(expectedStr).length);
+ assertExpectedBytes(expectedStr, key);
+ }
+
+ public static void assertExpectedBytes(final String expectedStr, byte[] key) {
+ assertEquals(expectedStr, Utils.byte2Hex(key));
+ byte[] expected = Utils.hex2Byte(expectedStr);
+
+ assertEquals(expected.length, key.length);
+ for (int i = 0; i < key.length; i++) {
+ assertEquals(expected[i], key[i]);
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestPersistedCrypto5Keys.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestPersistedCrypto5Keys.java
new file mode 100644
index 0000000000..f5ffc5a8a4
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestPersistedCrypto5Keys.java
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.crypto.test;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.CollectionKeys;
+import org.mozilla.gecko.sync.NoCollectionKeysSetException;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.crypto.PersistedCrypto5Keys;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+@RunWith(TestRunner.class)
+public class TestPersistedCrypto5Keys {
+ MockSharedPreferences prefs = null;
+
+ @Before
+ public void setUp() {
+ prefs = new MockSharedPreferences();
+ }
+
+ @Test
+ public void testPersistLastModified() throws CryptoException, NoCollectionKeysSetException {
+ long LAST_MODIFIED = System.currentTimeMillis();
+ KeyBundle syncKeyBundle = KeyBundle.withRandomKeys();
+ PersistedCrypto5Keys persisted = new PersistedCrypto5Keys(prefs, syncKeyBundle);
+
+ // Test fresh start.
+ assertEquals(-1, persisted.lastModified());
+
+ // Test persisting.
+ persisted.persistLastModified(LAST_MODIFIED);
+ assertEquals(LAST_MODIFIED, persisted.lastModified());
+
+ // Test clearing.
+ persisted.persistLastModified(0);
+ assertEquals(-1, persisted.lastModified());
+ }
+
+ @Test
+ public void testPersistKeys() throws CryptoException, NoCollectionKeysSetException {
+ KeyBundle syncKeyBundle = KeyBundle.withRandomKeys();
+ KeyBundle testKeyBundle = KeyBundle.withRandomKeys();
+
+ PersistedCrypto5Keys persisted = new PersistedCrypto5Keys(prefs, syncKeyBundle);
+
+ // Test fresh start.
+ assertNull(persisted.keys());
+
+ // Test persisting.
+ CollectionKeys keys = new CollectionKeys();
+ keys.setDefaultKeyBundle(syncKeyBundle);
+ keys.setKeyBundleForCollection("test", testKeyBundle);
+ persisted.persistKeys(keys);
+
+ CollectionKeys persistedKeys = persisted.keys();
+ assertNotNull(persistedKeys);
+ assertArrayEquals(syncKeyBundle.getEncryptionKey(), persistedKeys.defaultKeyBundle().getEncryptionKey());
+ assertArrayEquals(syncKeyBundle.getHMACKey(), persistedKeys.defaultKeyBundle().getHMACKey());
+ assertArrayEquals(testKeyBundle.getEncryptionKey(), persistedKeys.keyBundleForCollection("test").getEncryptionKey());
+ assertArrayEquals(testKeyBundle.getHMACKey(), persistedKeys.keyBundleForCollection("test").getHMACKey());
+
+ // Test clearing.
+ persisted.persistKeys(null);
+ assertNull(persisted.keys());
+
+ // Test loading a persisted bundle with wrong syncKeyBundle.
+ persisted.persistKeys(keys);
+ assertNotNull(persisted.keys());
+
+ persisted = new PersistedCrypto5Keys(prefs, testKeyBundle);
+ assertNull(persisted.keys());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestSRPConstants.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestSRPConstants.java
new file mode 100644
index 0000000000..9188bba244
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestSRPConstants.java
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.crypto.test;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.net.SRPConstants;
+
+import java.math.BigInteger;
+
+@RunWith(TestRunner.class)
+public class TestSRPConstants extends SRPConstants {
+ public void assertSRPConstants(SRPConstants.Parameters params, int bitLength) {
+ Assert.assertNotNull(params.g);
+ Assert.assertNotNull(params.N);
+ Assert.assertEquals(bitLength, bitLength);
+ Assert.assertEquals(bitLength / 8, params.byteLength);
+ Assert.assertEquals(bitLength / 4, params.hexLength);
+ BigInteger N = params.N;
+ BigInteger g = params.g;
+ // Each prime N is of the form 2*q + 1, with q also prime.
+ BigInteger q = N.subtract(new BigInteger("1")).divide(new BigInteger("2"));
+ // Check that g is a generator: the order of g is exactly 2*q (not 2, not q).
+ Assert.assertFalse(new BigInteger("1").equals(g.modPow(new BigInteger("2"), N)));
+ Assert.assertFalse(new BigInteger("1").equals(g.modPow(q, N)));
+ Assert.assertTrue(new BigInteger("1").equals(g.modPow((N.subtract(new BigInteger("1"))), N)));
+ // Even probable primality checking is too expensive to do here.
+ // Assert.assertTrue(N.isProbablePrime(3));
+ // Assert.assertTrue(q.isProbablePrime(3));
+ }
+
+ @Test
+ public void testConstants() {
+ assertSRPConstants(SRPConstants._1024, 1024);
+ assertSRPConstants(SRPConstants._1536, 1536);
+ assertSRPConstants(SRPConstants._2048, 2048);
+ assertSRPConstants(SRPConstants._3072, 3072);
+ assertSRPConstants(SRPConstants._4096, 4096);
+ assertSRPConstants(SRPConstants._6144, 6144);
+ assertSRPConstants(SRPConstants._8192, 8192);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/middleware/test/TestCrypto5MiddlewareRepositorySession.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/middleware/test/TestCrypto5MiddlewareRepositorySession.java
new file mode 100644
index 0000000000..d38a4caf2f
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/middleware/test/TestCrypto5MiddlewareRepositorySession.java
@@ -0,0 +1,291 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.middleware.test;
+
+import junit.framework.AssertionFailedError;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionBeginDelegate;
+import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionCreationDelegate;
+import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionFetchRecordsDelegate;
+import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionFinishDelegate;
+import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionStoreDelegate;
+import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositoryWipeDelegate;
+import org.mozilla.gecko.background.testhelpers.MockRecord;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.background.testhelpers.WBORepository;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.middleware.Crypto5MiddlewareRepository;
+import org.mozilla.gecko.sync.middleware.Crypto5MiddlewareRepositorySession;
+import org.mozilla.gecko.sync.repositories.InactiveSessionException;
+import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
+import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
+public class TestCrypto5MiddlewareRepositorySession {
+ public static WaitHelper getTestWaiter() {
+ return WaitHelper.getTestWaiter();
+ }
+
+ public static void performWait(Runnable runnable) {
+ getTestWaiter().performWait(runnable);
+ }
+
+ protected static void performNotify(InactiveSessionException e) {
+ final AssertionFailedError failed = new AssertionFailedError("Inactive session.");
+ failed.initCause(e);
+ getTestWaiter().performNotify(failed);
+ }
+
+ protected static void performNotify(InvalidSessionTransitionException e) {
+ final AssertionFailedError failed = new AssertionFailedError("Invalid session transition.");
+ failed.initCause(e);
+ getTestWaiter().performNotify(failed);
+ }
+
+ public Runnable onThreadRunnable(Runnable runnable) {
+ return WaitHelper.onThreadRunnable(runnable);
+ }
+
+ public WBORepository wboRepo;
+ public KeyBundle keyBundle;
+ public Crypto5MiddlewareRepository cmwRepo;
+ public Crypto5MiddlewareRepositorySession cmwSession;
+
+ @Before
+ public void setUp() throws CryptoException {
+ wboRepo = new WBORepository();
+ keyBundle = KeyBundle.withRandomKeys();
+ cmwRepo = new Crypto5MiddlewareRepository(wboRepo, keyBundle);
+ cmwSession = null;
+ }
+
+ /**
+ * Run `runnable` in performWait(... onBeginSucceeded { } ).
+ *
+ * The Crypto5MiddlewareRepositorySession is available in self.cmwSession.
+ *
+ * @param runnable
+ */
+ public void runInOnBeginSucceeded(final Runnable runnable) {
+ final TestCrypto5MiddlewareRepositorySession self = this;
+ performWait(onThreadRunnable(new Runnable() {
+ @Override
+ public void run() {
+ cmwRepo.createSession(new ExpectSuccessRepositorySessionCreationDelegate(getTestWaiter()) {
+ @Override
+ public void onSessionCreated(RepositorySession session) {
+ self.cmwSession = (Crypto5MiddlewareRepositorySession)session;
+ assertSame(RepositorySession.SessionStatus.UNSTARTED, cmwSession.getStatus());
+
+ try {
+ session.begin(new ExpectSuccessRepositorySessionBeginDelegate(getTestWaiter()) {
+ @Override
+ public void onBeginSucceeded(RepositorySession _session) {
+ assertSame(self.cmwSession, _session);
+ runnable.run();
+ }
+ });
+ } catch (InvalidSessionTransitionException e) {
+ TestCrypto5MiddlewareRepositorySession.performNotify(e);
+ }
+ }
+ }, null);
+ }
+ }));
+ }
+
+ @Test
+ /**
+ * Verify that the status is actually being advanced.
+ */
+ public void testStatus() {
+ runInOnBeginSucceeded(new Runnable() {
+ @Override public void run() {
+ assertSame(RepositorySession.SessionStatus.ACTIVE, cmwSession.getStatus());
+ try {
+ cmwSession.finish(new ExpectSuccessRepositorySessionFinishDelegate(getTestWaiter()));
+ } catch (InactiveSessionException e) {
+ performNotify(e);
+ }
+ }
+ });
+ assertSame(RepositorySession.SessionStatus.DONE, cmwSession.getStatus());
+ }
+
+ @Test
+ /**
+ * Verify that wipe is actually wiping the underlying repository.
+ */
+ public void testWipe() {
+ Record record = new MockRecord("nncdefghiaaa", "coll", System.currentTimeMillis(), false);
+ wboRepo.wbos.put(record.guid, record);
+ assertEquals(1, wboRepo.wbos.size());
+
+ runInOnBeginSucceeded(new Runnable() {
+ @Override public void run() {
+ cmwSession.wipe(new ExpectSuccessRepositoryWipeDelegate(getTestWaiter()));
+ }
+ });
+ performWait(onThreadRunnable(new Runnable() {
+ @Override public void run() {
+ try {
+ cmwSession.finish(new ExpectSuccessRepositorySessionFinishDelegate(getTestWaiter()));
+ } catch (InactiveSessionException e) {
+ performNotify(e);
+ }
+ }
+ }));
+ assertEquals(0, wboRepo.wbos.size());
+ }
+
+ @Test
+ /**
+ * Verify that store is actually writing encrypted data to the underlying repository.
+ */
+ public void testStoreEncrypts() throws NonObjectJSONException, CryptoException, IOException {
+ final BookmarkRecord record = new BookmarkRecord("nncdefghiaaa", "coll", System.currentTimeMillis(), false);
+ record.title = "unencrypted title";
+
+ runInOnBeginSucceeded(new Runnable() {
+ @Override public void run() {
+ try {
+ try {
+ cmwSession.setStoreDelegate(new ExpectSuccessRepositorySessionStoreDelegate(getTestWaiter()));
+ cmwSession.store(record);
+ } catch (NoStoreDelegateException e) {
+ getTestWaiter().performNotify(new AssertionFailedError("Should not happen."));
+ }
+ cmwSession.storeDone();
+ cmwSession.finish(new ExpectSuccessRepositorySessionFinishDelegate(getTestWaiter()));
+ } catch (InactiveSessionException e) {
+ performNotify(e);
+ }
+ }
+ });
+ assertEquals(1, wboRepo.wbos.size());
+ assertTrue(wboRepo.wbos.containsKey(record.guid));
+
+ Record storedRecord = wboRepo.wbos.get(record.guid);
+ CryptoRecord cryptoRecord = (CryptoRecord)storedRecord;
+ assertSame(cryptoRecord.keyBundle, keyBundle);
+
+ cryptoRecord = cryptoRecord.decrypt();
+ BookmarkRecord decryptedRecord = new BookmarkRecord();
+ decryptedRecord.initFromEnvelope(cryptoRecord);
+ assertEquals(record.title, decryptedRecord.title);
+ }
+
+ @Test
+ /**
+ * Verify that fetch is actually retrieving encrypted data from the underlying repository and is correctly decrypting it.
+ */
+ public void testFetchDecrypts() throws UnsupportedEncodingException, CryptoException {
+ final BookmarkRecord record1 = new BookmarkRecord("nncdefghiaaa", "coll", System.currentTimeMillis(), false);
+ record1.title = "unencrypted title";
+ final BookmarkRecord record2 = new BookmarkRecord("XXXXXXXXXXXX", "coll", System.currentTimeMillis(), false);
+ record2.title = "unencrypted second title";
+
+ CryptoRecord encryptedRecord1 = record1.getEnvelope();
+ encryptedRecord1.keyBundle = keyBundle;
+ encryptedRecord1 = encryptedRecord1.encrypt();
+ wboRepo.wbos.put(record1.guid, encryptedRecord1);
+
+ CryptoRecord encryptedRecord2 = record2.getEnvelope();
+ encryptedRecord2.keyBundle = keyBundle;
+ encryptedRecord2 = encryptedRecord2.encrypt();
+ wboRepo.wbos.put(record2.guid, encryptedRecord2);
+
+ final ExpectSuccessRepositorySessionFetchRecordsDelegate fetchRecordsDelegate = new ExpectSuccessRepositorySessionFetchRecordsDelegate(getTestWaiter());
+ runInOnBeginSucceeded(new Runnable() {
+ @Override public void run() {
+ try {
+ cmwSession.fetch(new String[] { record1.guid }, fetchRecordsDelegate);
+ } catch (InactiveSessionException e) {
+ performNotify(e);
+ }
+ }
+ });
+ performWait(onThreadRunnable(new Runnable() {
+ @Override public void run() {
+ try {
+ cmwSession.finish(new ExpectSuccessRepositorySessionFinishDelegate(getTestWaiter()));
+ } catch (InactiveSessionException e) {
+ performNotify(e);
+ }
+ }
+ }));
+
+ assertEquals(1, fetchRecordsDelegate.fetchedRecords.size());
+ BookmarkRecord decryptedRecord = new BookmarkRecord();
+ decryptedRecord.initFromEnvelope((CryptoRecord)fetchRecordsDelegate.fetchedRecords.get(0));
+ assertEquals(record1.title, decryptedRecord.title);
+ }
+
+ @Test
+ /**
+ * Verify that fetchAll is actually retrieving encrypted data from the underlying repository and is correctly decrypting it.
+ */
+ public void testFetchAllDecrypts() throws UnsupportedEncodingException, CryptoException {
+ final BookmarkRecord record1 = new BookmarkRecord("nncdefghiaaa", "coll", System.currentTimeMillis(), false);
+ record1.title = "unencrypted title";
+ final BookmarkRecord record2 = new BookmarkRecord("XXXXXXXXXXXX", "coll", System.currentTimeMillis(), false);
+ record2.title = "unencrypted second title";
+
+ CryptoRecord encryptedRecord1 = record1.getEnvelope();
+ encryptedRecord1.keyBundle = keyBundle;
+ encryptedRecord1 = encryptedRecord1.encrypt();
+ wboRepo.wbos.put(record1.guid, encryptedRecord1);
+
+ CryptoRecord encryptedRecord2 = record2.getEnvelope();
+ encryptedRecord2.keyBundle = keyBundle;
+ encryptedRecord2 = encryptedRecord2.encrypt();
+ wboRepo.wbos.put(record2.guid, encryptedRecord2);
+
+ final ExpectSuccessRepositorySessionFetchRecordsDelegate fetchAllRecordsDelegate = new ExpectSuccessRepositorySessionFetchRecordsDelegate(getTestWaiter());
+ runInOnBeginSucceeded(new Runnable() {
+ @Override public void run() {
+ cmwSession.fetchAll(fetchAllRecordsDelegate);
+ }
+ });
+ performWait(onThreadRunnable(new Runnable() {
+ @Override public void run() {
+ try {
+ cmwSession.finish(new ExpectSuccessRepositorySessionFinishDelegate(getTestWaiter()));
+ } catch (InactiveSessionException e) {
+ performNotify(e);
+ }
+ }
+ }));
+
+ assertEquals(2, fetchAllRecordsDelegate.fetchedRecords.size());
+ BookmarkRecord decryptedRecord1 = new BookmarkRecord();
+ decryptedRecord1.initFromEnvelope((CryptoRecord)fetchAllRecordsDelegate.fetchedRecords.get(0));
+ BookmarkRecord decryptedRecord2 = new BookmarkRecord();
+ decryptedRecord2.initFromEnvelope((CryptoRecord)fetchAllRecordsDelegate.fetchedRecords.get(1));
+
+ // We should get two different decrypted records
+ assertFalse(decryptedRecord1.guid.equals(decryptedRecord2.guid));
+ assertFalse(decryptedRecord1.title.equals(decryptedRecord2.title));
+ // And we should know about both.
+ assertTrue(record1.title.equals(decryptedRecord1.title) || record1.title.equals(decryptedRecord2.title));
+ assertTrue(record2.title.equals(decryptedRecord1.title) || record2.title.equals(decryptedRecord2.title));
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestHMACAuthHeaderProvider.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestHMACAuthHeaderProvider.java
new file mode 100644
index 0000000000..675351be93
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestHMACAuthHeaderProvider.java
@@ -0,0 +1,165 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.net.test;
+
+import ch.boye.httpclientandroidlib.client.methods.HttpGet;
+import ch.boye.httpclientandroidlib.client.methods.HttpPost;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.net.HMACAuthHeaderProvider;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(TestRunner.class)
+public class TestHMACAuthHeaderProvider {
+ // Expose a few protected static member functions as public for testing.
+ protected static class LeakyHMACAuthHeaderProvider extends HMACAuthHeaderProvider {
+ public LeakyHMACAuthHeaderProvider(String identifier, String key) {
+ super(identifier, key);
+ }
+
+ public static String getRequestString(HttpUriRequest request, long timestampInSeconds, String nonce, String extra) {
+ return HMACAuthHeaderProvider.getRequestString(request, timestampInSeconds, nonce, extra);
+ }
+
+ public static String getSignature(String requestString, String key)
+ throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
+ return HMACAuthHeaderProvider.getSignature(requestString, key);
+ }
+ }
+
+ @Test
+ public void testGetRequestStringSpecExample1() throws Exception {
+ long timestamp = 1336363200;
+ String nonceString = "dj83hs9s";
+ String extra = "";
+ URI uri = new URI("http://example.com/resource/1?b=1&a=2");
+
+ HttpUriRequest req = new HttpGet(uri);
+
+ String expected = "1336363200\n" +
+ "dj83hs9s\n" +
+ "GET\n" +
+ "/resource/1?b=1&a=2\n" +
+ "example.com\n" +
+ "80\n" +
+ "\n";
+
+ assertEquals(expected, LeakyHMACAuthHeaderProvider.getRequestString(req, timestamp, nonceString, extra));
+ }
+
+ @Test
+ public void testGetRequestStringSpecExample2() throws Exception {
+ long timestamp = 264095;
+ String nonceString = "7d8f3e4a";
+ String extra = "a,b,c";
+ URI uri = new URI("http://example.com/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b&c2&a3=2+q");
+
+ HttpUriRequest req = new HttpPost(uri);
+
+ String expected = "264095\n" +
+ "7d8f3e4a\n" +
+ "POST\n" +
+ "/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b&c2&a3=2+q\n" +
+ "example.com\n" +
+ "80\n" +
+ "a,b,c\n";
+
+ assertEquals(expected, LeakyHMACAuthHeaderProvider.getRequestString(req, timestamp, nonceString, extra));
+ }
+
+ @Test
+ public void testPort() throws Exception {
+ long timestamp = 264095;
+ String nonceString = "7d8f3e4a";
+ String extra = "a,b,c";
+ URI uri = new URI("http://example.com:88/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b&c2&a3=2+q");
+
+ HttpUriRequest req = new HttpPost(uri);
+
+ String expected = "264095\n" +
+ "7d8f3e4a\n" +
+ "POST\n" +
+ "/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b&c2&a3=2+q\n" +
+ "example.com\n" +
+ "88\n" +
+ "a,b,c\n";
+
+ assertEquals(expected, LeakyHMACAuthHeaderProvider.getRequestString(req, timestamp, nonceString, extra));
+ }
+
+ @Test
+ public void testHTTPS() throws Exception {
+ long timestamp = 264095;
+ String nonceString = "7d8f3e4a";
+ String extra = "a,b,c";
+ URI uri = new URI("https://example.com/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b&c2&a3=2+q");
+
+ HttpUriRequest req = new HttpPost(uri);
+
+ String expected = "264095\n" +
+ "7d8f3e4a\n" +
+ "POST\n" +
+ "/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b&c2&a3=2+q\n" +
+ "example.com\n" +
+ "443\n" +
+ "a,b,c\n";
+
+ assertEquals(expected, LeakyHMACAuthHeaderProvider.getRequestString(req, timestamp, nonceString, extra));
+ }
+
+ @Test
+ public void testSpecSignatureExample() throws Exception {
+ String extra = "";
+ long timestampInSeconds = 1336363200;
+ String nonceString = "dj83hs9s";
+
+ URI uri = new URI("http://example.com/resource/1?b=1&a=2");
+ HttpRequestBase req = new HttpGet(uri);
+
+ String requestString = LeakyHMACAuthHeaderProvider.getRequestString(req, timestampInSeconds, nonceString, extra);
+
+ String expected = "1336363200\n" +
+ "dj83hs9s\n" +
+ "GET\n" +
+ "/resource/1?b=1&a=2\n" +
+ "example.com\n" +
+ "80\n" +
+ "\n";
+
+ assertEquals(expected, requestString);
+
+ // There appears to be an error in the current spec.
+ // Spec is at https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-1.1
+ // Error is reported at http://www.ietf.org/mail-archive/web/oauth/current/msg09741.html
+ // assertEquals("bhCQXTVyfj5cmA9uKkPFx1zeOXM=", HMACAuthHeaderProvider.getSignature(requestString, keyString));
+ }
+
+ @Test
+ public void testCompatibleWithDesktopFirefox() throws Exception {
+ // These are test values used in the FF Sync Client testsuite.
+
+ // String identifier = "vmo1txkttblmn51u2p3zk2xiy16hgvm5ok8qiv1yyi86ffjzy9zj0ez9x6wnvbx7";
+ String keyString = "b8u1cc5iiio5o319og7hh8faf2gi5ym4aq0zwf112cv1287an65fudu5zj7zo7dz";
+
+ String extra = "";
+ long timestampInSeconds = 1329181221;
+ String nonceString = "wGX71";
+
+ URI uri = new URI("http://10.250.2.176/alias/");
+ HttpRequestBase req = new HttpGet(uri);
+
+ String requestString = LeakyHMACAuthHeaderProvider.getRequestString(req, timestampInSeconds, nonceString, extra);
+
+ assertEquals("jzh5chjQc2zFEvLbyHnPdX11Yck=", LeakyHMACAuthHeaderProvider.getSignature(requestString, keyString));
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestHawkAuthHeaderProvider.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestHawkAuthHeaderProvider.java
new file mode 100644
index 0000000000..3dab313a09
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestHawkAuthHeaderProvider.java
@@ -0,0 +1,145 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.net.test;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.client.methods.HttpGet;
+import ch.boye.httpclientandroidlib.client.methods.HttpPost;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.entity.StringEntity;
+import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
+import ch.boye.httpclientandroidlib.message.BasicHeader;
+import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * These test vectors were taken from
+ * <a href="https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/README.md">https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/README.md</a>.
+ */
+@RunWith(TestRunner.class)
+public class TestHawkAuthHeaderProvider {
+ // Expose a few protected static member functions as public for testing.
+ protected static class LeakyHawkAuthHeaderProvider extends HawkAuthHeaderProvider {
+ public LeakyHawkAuthHeaderProvider(String tokenId, byte[] reqHMACKey) {
+ // getAuthHeader takes includePayloadHash as a parameter.
+ super(tokenId, reqHMACKey, false, 0L);
+ }
+
+ // Public for testing.
+ public static String getRequestString(HttpUriRequest request, String type, long timestamp, String nonce, String hash, String extra, String app, String dlg) {
+ return HawkAuthHeaderProvider.getRequestString(request, type, timestamp, nonce, hash, extra, app, dlg);
+ }
+
+ // Public for testing.
+ public static String getSignature(String requestString, String key)
+ throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
+ return HawkAuthHeaderProvider.getSignature(requestString.getBytes("UTF-8"), key.getBytes("UTF-8"));
+ }
+
+ // Public for testing.
+ @Override
+ public Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client,
+ long timestamp, String nonce, String extra, boolean includePayloadHash)
+ throws InvalidKeyException, NoSuchAlgorithmException, IOException {
+ return super.getAuthHeader(request, context, client, timestamp, nonce, extra, includePayloadHash);
+ }
+
+ // Public for testing.
+ public static String getBaseContentType(Header contentTypeHeader) {
+ return HawkAuthHeaderProvider.getBaseContentType(contentTypeHeader);
+ }
+ }
+
+ @Test
+ public void testSpecRequestString() throws Exception {
+ long timestamp = 1353832234;
+ String nonce = "j4h3g2";
+ String extra = "some-app-ext-data";
+ String hash = null;
+ String app = null;
+ String dlg = null;
+
+ URI uri = new URI("http://example.com:8000/resource/1?b=1&a=2");
+ HttpUriRequest req = new HttpGet(uri);
+
+ String expected = "hawk.1.header\n" +
+ "1353832234\n" +
+ "j4h3g2\n" +
+ "GET\n" +
+ "/resource/1?b=1&a=2\n" +
+ "example.com\n" +
+ "8000\n" +
+ "\n" +
+ "some-app-ext-data\n";
+
+ // LeakyHawkAuthHeaderProvider.
+ assertEquals(expected, LeakyHawkAuthHeaderProvider.getRequestString(req, "header", timestamp, nonce, hash, extra, app, dlg));
+ }
+
+ @Test
+ public void testSpecSignatureExample() throws Exception {
+ String input = "hawk.1.header\n" +
+ "1353832234\n" +
+ "j4h3g2\n" +
+ "GET\n" +
+ "/resource/1?b=1&a=2\n" +
+ "example.com\n" +
+ "8000\n" +
+ "\n" +
+ "some-app-ext-data\n";
+
+ assertEquals("6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=", LeakyHawkAuthHeaderProvider.getSignature(input, "werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn"));
+ }
+
+ @Test
+ public void testSpecPayloadExample() throws Exception {
+ LeakyHawkAuthHeaderProvider provider = new LeakyHawkAuthHeaderProvider("dh37fgj492je", "werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn".getBytes("UTF-8"));
+ URI uri = new URI("http://example.com:8000/resource/1?b=1&a=2");
+ HttpPost req = new HttpPost(uri);
+ String body = "Thank you for flying Hawk";
+ req.setEntity(new StringEntity(body));
+ Header header = provider.getAuthHeader(req, null, null, 1353832234L, "j4h3g2", "some-app-ext-data", true);
+ String expected = "Hawk id=\"dh37fgj492je\", ts=\"1353832234\", nonce=\"j4h3g2\", hash=\"Yi9LfIIFRtBEPt74PVmbTF/xVAwPn7ub15ePICfgnuY=\", ext=\"some-app-ext-data\", mac=\"aSe1DERmZuRl3pI36/9BdZmnErTw3sNzOOAUlfeKjVw=\"";
+ assertEquals("Authorization", header.getName());
+ assertEquals(expected, header.getValue());
+ }
+
+ @Test
+ public void testSpecAuthorizationHeader() throws Exception {
+ LeakyHawkAuthHeaderProvider provider = new LeakyHawkAuthHeaderProvider("dh37fgj492je", "werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn".getBytes("UTF-8"));
+ URI uri = new URI("http://example.com:8000/resource/1?b=1&a=2");
+ HttpGet req = new HttpGet(uri);
+ Header header = provider.getAuthHeader(req, null, null, 1353832234L, "j4h3g2", "some-app-ext-data", false);
+ String expected = "Hawk id=\"dh37fgj492je\", ts=\"1353832234\", nonce=\"j4h3g2\", ext=\"some-app-ext-data\", mac=\"6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=\"";
+ assertEquals("Authorization", header.getName());
+ assertEquals(expected, header.getValue());
+
+ // For a non-POST, non-PUT request, a request to include the payload verification hash is silently ignored.
+ header = provider.getAuthHeader(req, null, null, 1353832234L, "j4h3g2", "some-app-ext-data", true);
+ assertEquals("Authorization", header.getName());
+ assertEquals(expected, header.getValue());
+ }
+
+ @Test
+ public void testGetBaseContentType() throws Exception {
+ assertEquals("text/plain", LeakyHawkAuthHeaderProvider.getBaseContentType(new BasicHeader("Content-Type", "text/plain")));
+ assertEquals("text/plain", LeakyHawkAuthHeaderProvider.getBaseContentType(new BasicHeader("Content-Type", "text/plain;one")));
+ assertEquals("text/plain", LeakyHawkAuthHeaderProvider.getBaseContentType(new BasicHeader("Content-Type", "text/plain;one;two")));
+ assertEquals("text/html", LeakyHawkAuthHeaderProvider.getBaseContentType(new BasicHeader("Content-Type", "text/html;charset=UTF-8")));
+ assertEquals("text/html", LeakyHawkAuthHeaderProvider.getBaseContentType(new BasicHeader("Content-Type", "text/html; charset=UTF-8")));
+ assertEquals("text/html", LeakyHawkAuthHeaderProvider.getBaseContentType(new BasicHeader("Content-Type", "text/html ;charset=UTF-8")));
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestLiveHawkAuth.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestLiveHawkAuth.java
new file mode 100644
index 0000000000..8f136e3d09
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestLiveHawkAuth.java
@@ -0,0 +1,181 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.net.test;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
+import ch.boye.httpclientandroidlib.entity.StringEntity;
+import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
+import ch.boye.httpclientandroidlib.message.BasicHeader;
+import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
+import org.junit.Assert;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BaseResourceDelegate;
+import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider;
+import org.mozilla.gecko.sync.net.Resource;
+import org.mozilla.gecko.sync.net.SyncResponse;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+
+public class TestLiveHawkAuth {
+ /**
+ * Hawk comes with an example/usage.js server. Modify it to serve indefinitely,
+ * un-comment the following line, and verify that the port and credentials
+ * have not changed; then the following test should pass.
+ */
+ // @org.junit.Test
+ public void testHawkUsage() throws Exception {
+ // Id and credentials are hard-coded in example/usage.js.
+ final String id = "dh37fgj492je";
+ final byte[] key = "werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn".getBytes("UTF-8");
+ final BaseResource resource = new BaseResource("http://localhost:8000/", false);
+
+ // Basic GET.
+ resource.delegate = new TestBaseResourceDelegate(resource, new HawkAuthHeaderProvider(id, key, false, 0L));
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ resource.get();
+ }
+ });
+
+ // PUT with payload verification.
+ resource.delegate = new TestBaseResourceDelegate(resource, new HawkAuthHeaderProvider(id, key, true, 0L));
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ resource.put(new StringEntity("Thank you for flying Hawk"));
+ } catch (UnsupportedEncodingException e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+ }
+ });
+
+ // PUT with a large (32k or so) body and payload verification.
+ final StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < 16000; i++) {
+ sb.append(Integer.valueOf(i % 100).toString());
+ }
+ resource.delegate = new TestBaseResourceDelegate(resource, new HawkAuthHeaderProvider(id, key, true, 0L));
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ resource.put(new StringEntity(sb.toString()));
+ } catch (UnsupportedEncodingException e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+ }
+ });
+
+ // PUT without payload verification.
+ resource.delegate = new TestBaseResourceDelegate(resource, new HawkAuthHeaderProvider(id, key, false, 0L));
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ resource.put(new StringEntity("Thank you for flying Hawk"));
+ } catch (UnsupportedEncodingException e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+ }
+ });
+
+ // PUT with *bad* payload verification.
+ HawkAuthHeaderProvider provider = new HawkAuthHeaderProvider(id, key, true, 0L) {
+ @Override
+ public Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client) throws GeneralSecurityException {
+ Header header = super.getAuthHeader(request, context, client);
+ // Here's a cheap way of breaking the hash.
+ String newValue = header.getValue().replaceAll("hash=\"....", "hash=\"XXXX");
+ return new BasicHeader(header.getName(), newValue);
+ }
+ };
+
+ resource.delegate = new TestBaseResourceDelegate(resource, provider);
+ try {
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ resource.put(new StringEntity("Thank you for flying Hawk"));
+ } catch (UnsupportedEncodingException e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+ }
+ });
+ fail("Expected assertion after 401 response.");
+ } catch (WaitHelper.InnerError e) {
+ assertTrue(e.innerError instanceof AssertionError);
+ assertEquals("expected:<200> but was:<401>", ((AssertionError) e.innerError).getMessage());
+ }
+ }
+
+ protected final class TestBaseResourceDelegate extends BaseResourceDelegate {
+ protected final HawkAuthHeaderProvider provider;
+
+ protected TestBaseResourceDelegate(Resource resource, HawkAuthHeaderProvider provider) throws UnsupportedEncodingException {
+ super(resource);
+ this.provider = provider;
+ }
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return provider;
+ }
+
+ @Override
+ public int connectionTimeout() {
+ return 1000;
+ }
+
+ @Override
+ public int socketTimeout() {
+ return 1000;
+ }
+
+ @Override
+ public void handleHttpResponse(HttpResponse response) {
+ SyncResponse res = new SyncResponse(response);
+ try {
+ Assert.assertEquals(200, res.getStatusCode());
+ WaitHelper.getTestWaiter().performNotify();
+ } catch (Throwable e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+ }
+
+ @Override
+ public void handleTransportException(GeneralSecurityException e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+
+ @Override
+ public void handleHttpProtocolException(ClientProtocolException e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+
+ @Override
+ public void handleHttpIOException(IOException e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+
+ @Override
+ public String getUserAgent() {
+ return null;
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestUserAgentHeaders.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestUserAgentHeaders.java
new file mode 100644
index 0000000000..30b8a38eca
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestUserAgentHeaders.java
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.net.test;
+
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import junit.framework.Assert;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
+import org.mozilla.android.sync.test.helpers.MockServer;
+import org.mozilla.gecko.background.fxa.FxAccountClient20;
+import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate;
+import org.mozilla.gecko.background.fxa.FxAccountClient20.RecoveryEmailStatusResponse;
+import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.sync.SyncConstants;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.SyncStorageRecordRequest;
+import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.simpleframework.http.Request;
+import org.simpleframework.http.Response;
+
+import java.util.concurrent.Executors;
+
+@RunWith(TestRunner.class)
+public class TestUserAgentHeaders {
+ private static final int TEST_PORT = HTTPServerTestHelper.getTestPort();
+ private static final String TEST_SERVER = "http://localhost:" + TEST_PORT;
+
+ protected final HTTPServerTestHelper data = new HTTPServerTestHelper();
+
+ protected class UserAgentServer extends MockServer {
+ public String lastUserAgent = null;
+
+ @Override
+ public void handle(Request request, Response response) {
+ lastUserAgent = request.getValue(HTTP.USER_AGENT);
+ super.handle(request, response);
+ }
+ }
+
+ protected final UserAgentServer userAgentServer = new UserAgentServer();
+
+ @Before
+ public void setUp() {
+ BaseResource.rewriteLocalhost = false;
+ data.startHTTPServer(userAgentServer);
+ }
+
+ @After
+ public void tearDown() {
+ data.stopHTTPServer();
+ }
+
+ @Test
+ public void testSyncUserAgent() throws Exception {
+ final SyncStorageRecordRequest request = new SyncStorageRecordRequest(TEST_SERVER);
+ request.delegate = new SyncStorageRequestDelegate() {
+ @Override
+ public String ifUnmodifiedSince() {
+ return null;
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse response) {
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ @Override
+ public void handleRequestError(Exception ex) {
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return null;
+ }
+ };
+
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ request.get();
+ }
+ });
+
+ // Verify that we're getting the value from the correct place.
+ Assert.assertEquals(SyncConstants.USER_AGENT, userAgentServer.lastUserAgent);
+ }
+
+ @Test
+ public void testFxAccountClientUserAgent() throws Exception {
+ final FxAccountClient20 client = new FxAccountClient20(TEST_SERVER, Executors.newSingleThreadExecutor());
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ client.recoveryEmailStatus(new byte[] { 0 }, new RequestDelegate<RecoveryEmailStatusResponse>() {
+ @Override
+ public void handleSuccess(RecoveryEmailStatusResponse result) {
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ @Override
+ public void handleFailure(FxAccountClientRemoteException e) {
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ @Override
+ public void handleError(Exception e) {
+ WaitHelper.getTestWaiter().performNotify();
+ }
+ });
+ }
+ });
+
+ // Verify that we're getting the value from the correct place.
+ Assert.assertEquals(FxAccountConstants.USER_AGENT, userAgentServer.lastUserAgent);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/BrowserContractHelpersTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/BrowserContractHelpersTest.java
new file mode 100644
index 0000000000..eecfa8dc2b
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/BrowserContractHelpersTest.java
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.repositories.android;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+import static org.junit.Assert.*;
+
+@RunWith(TestRunner.class)
+public class BrowserContractHelpersTest {
+ @Test
+ public void testBookmarkCodes() {
+ final String[] strings = {
+ // Observe omissions: "microsummary", "item".
+ "folder", "bookmark", "separator", "livemark", "query"
+ };
+ for (int i = 0; i < strings.length; ++i) {
+ assertEquals(strings[i], BrowserContractHelpers.typeStringForCode(i));
+ assertEquals(i, BrowserContractHelpers.typeCodeForString(strings[i]));
+ }
+ assertEquals(null, BrowserContractHelpers.typeStringForCode(-1));
+ assertEquals(null, BrowserContractHelpers.typeStringForCode(100));
+
+ assertEquals(-1, BrowserContractHelpers.typeCodeForString(null));
+ assertEquals(-1, BrowserContractHelpers.typeCodeForString("folder "));
+ assertEquals(-1, BrowserContractHelpers.typeCodeForString("FOLDER"));
+ assertEquals(-1, BrowserContractHelpers.typeCodeForString(""));
+ assertEquals(-1, BrowserContractHelpers.typeCodeForString("nope"));
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/VisitsHelperTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/VisitsHelperTest.java
new file mode 100644
index 0000000000..67bbca0898
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/VisitsHelperTest.java
@@ -0,0 +1,144 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.repositories.android;
+
+import android.content.ContentProviderClient;
+import android.content.ContentValues;
+import android.net.Uri;
+
+import junit.framework.Assert;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.db.DelegatingTestContentProvider;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserProvider;
+import org.robolectric.shadows.ShadowContentResolver;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
+public class VisitsHelperTest {
+ @Test
+ public void testBulkInsertRemoteVisits() throws Exception {
+ JSONArray toInsert = new JSONArray();
+ Assert.assertEquals(0, VisitsHelper.getVisitsContentValues("testGUID", toInsert).length);
+
+ JSONObject visit = new JSONObject();
+ Long date = Long.valueOf(123432552344l);
+ visit.put("date", date);
+ visit.put("type", 2l);
+ toInsert.add(visit);
+
+ JSONObject visit2 = new JSONObject();
+ visit2.put("date", date + 1000);
+ visit2.put("type", 5l);
+ toInsert.add(visit2);
+
+ ContentValues[] cvs = VisitsHelper.getVisitsContentValues("testGUID", toInsert);
+ Assert.assertEquals(2, cvs.length);
+ ContentValues cv1 = cvs[0];
+ ContentValues cv2 = cvs[1];
+ Assert.assertEquals(Integer.valueOf(2), cv1.getAsInteger(BrowserContract.Visits.VISIT_TYPE));
+ Assert.assertEquals(Integer.valueOf(5), cv2.getAsInteger(BrowserContract.Visits.VISIT_TYPE));
+
+ Assert.assertEquals(date, cv1.getAsLong("date"));
+ Assert.assertEquals(Long.valueOf(date + 1000), cv2.getAsLong(BrowserContract.Visits.DATE_VISITED));
+ }
+
+ @Test
+ public void testGetRecentHistoryVisitsForGUID() throws Exception {
+ Uri historyTestUri = testUri(BrowserContract.History.CONTENT_URI);
+ Uri visitsTestUri = testUri(BrowserContract.Visits.CONTENT_URI);
+
+ BrowserProvider provider = new BrowserProvider();
+ try {
+ provider.onCreate();
+ ShadowContentResolver.registerProvider(BrowserContract.AUTHORITY, new DelegatingTestContentProvider(provider));
+
+ final ShadowContentResolver cr = new ShadowContentResolver();
+ ContentProviderClient historyClient = cr.acquireContentProviderClient(BrowserContractHelpers.HISTORY_CONTENT_URI);
+ ContentProviderClient visitsClient = cr.acquireContentProviderClient(BrowserContractHelpers.VISITS_CONTENT_URI);
+
+ ContentValues historyItem = new ContentValues();
+ historyItem.put(BrowserContract.History.URL, "https://www.mozilla.org");
+ historyItem.put(BrowserContract.History.GUID, "testGUID");
+ historyClient.insert(historyTestUri, historyItem);
+
+ Long baseDate = System.currentTimeMillis();
+ for (int i = 0; i < 30; i++) {
+ ContentValues visitItem = new ContentValues();
+ visitItem.put(BrowserContract.Visits.HISTORY_GUID, "testGUID");
+ visitItem.put(BrowserContract.Visits.DATE_VISITED, baseDate - i * 100);
+ visitItem.put(BrowserContract.Visits.VISIT_TYPE, 1);
+ visitItem.put(BrowserContract.Visits.IS_LOCAL, 1);
+ visitsClient.insert(visitsTestUri, visitItem);
+ }
+
+ // test that limit worked, that sorting is correct, and that both date and type are present
+ JSONArray recentVisits = VisitsHelper.getRecentHistoryVisitsForGUID(visitsClient, "testGUID", 10);
+ Assert.assertEquals(10, recentVisits.size());
+ for (int i = 0; i < recentVisits.size(); i++) {
+ JSONObject v = (JSONObject) recentVisits.get(i);
+ Long date = (Long) v.get("date");
+ Long type = (Long) v.get("type");
+ Assert.assertEquals(Long.valueOf(baseDate - i * 100), date);
+ Assert.assertEquals(Long.valueOf(1), type);
+ }
+ } finally {
+ provider.shutdown();
+ }
+ }
+
+ @Test
+ public void testGetVisitContentValues() throws Exception {
+ JSONObject visit = new JSONObject();
+ Long date = Long.valueOf(123432552344l);
+ visit.put("date", date);
+ visit.put("type", Long.valueOf(2));
+
+ ContentValues cv = VisitsHelper.getVisitContentValues("testGUID", visit, true);
+ assertTrue(cv.containsKey(BrowserContract.Visits.VISIT_TYPE));
+ assertTrue(cv.containsKey(BrowserContract.Visits.DATE_VISITED));
+ assertTrue(cv.containsKey(BrowserContract.Visits.HISTORY_GUID));
+ assertTrue(cv.containsKey(BrowserContract.Visits.IS_LOCAL));
+ assertEquals(4, cv.size());
+
+ assertEquals(date, cv.getAsLong(BrowserContract.Visits.DATE_VISITED));
+ assertEquals(Long.valueOf(2), cv.getAsLong(BrowserContract.Visits.VISIT_TYPE));
+ assertEquals("testGUID", cv.getAsString(BrowserContract.Visits.HISTORY_GUID));
+ assertEquals(Integer.valueOf(1), cv.getAsInteger(BrowserContract.Visits.IS_LOCAL));
+
+ cv = VisitsHelper.getVisitContentValues("testGUID", visit, false);
+ assertEquals(Integer.valueOf(0), cv.getAsInteger(BrowserContract.Visits.IS_LOCAL));
+
+ try {
+ JSONObject visit2 = new JSONObject();
+ visit.put("date", date);
+ VisitsHelper.getVisitContentValues("testGUID", visit2, false);
+ assertTrue("Must check that visit type key is present", false);
+ } catch (IllegalArgumentException e) {}
+
+ try {
+ JSONObject visit3 = new JSONObject();
+ visit.put("type", Long.valueOf(2));
+ VisitsHelper.getVisitContentValues("testGUID", visit3, false);
+ assertTrue("Must check that visit date key is present", false);
+ } catch (IllegalArgumentException e) {}
+
+ try {
+ JSONObject visit4 = new JSONObject();
+ VisitsHelper.getVisitContentValues("testGUID", visit4, false);
+ assertTrue("Must check that visit type and date keys are present", false);
+ } catch (IllegalArgumentException e) {}
+ }
+
+ private Uri testUri(Uri baseUri) {
+ return baseUri.buildUpon().appendQueryParameter(BrowserContract.PARAM_IS_TEST, "1").build();
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/test/TestBookmarksInsertionManager.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/test/TestBookmarksInsertionManager.java
new file mode 100644
index 0000000000..cbf5c37d3a
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/test/TestBookmarksInsertionManager.java
@@ -0,0 +1,221 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.repositories.android.test;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.repositories.android.BookmarksInsertionManager;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
+public class TestBookmarksInsertionManager {
+ public BookmarksInsertionManager manager;
+ public ArrayList<String[]> insertions;
+
+ @Before
+ public void setUp() {
+ insertions = new ArrayList<String[]>();
+ Set<String> writtenFolders = new HashSet<String>();
+ writtenFolders.add("mobile");
+
+ BookmarksInsertionManager.BookmarkInserter inserter = new BookmarksInsertionManager.BookmarkInserter() {
+ @Override
+ public boolean insertFolder(BookmarkRecord record) {
+ if (record.guid == "fail") {
+ return false;
+ }
+ Logger.debug(BookmarksInsertionManager.LOG_TAG, "Inserted folder (" + record.guid + ").");
+ insertions.add(new String[] { record.guid });
+ return true;
+ }
+
+ @Override
+ public void bulkInsertNonFolders(Collection<BookmarkRecord> records) {
+ ArrayList<String> guids = new ArrayList<String>();
+ for (BookmarkRecord record : records) {
+ guids.add(record.guid);
+ }
+ String[] guidList = guids.toArray(new String[guids.size()]);
+ insertions.add(guidList);
+ Logger.debug(BookmarksInsertionManager.LOG_TAG, "Inserted non-folders (" + Utils.toCommaSeparatedString(guids) + ").");
+ }
+ };
+ manager = new BookmarksInsertionManager(3, writtenFolders, inserter);
+ BookmarksInsertionManager.DEBUG = true;
+ }
+
+ protected static BookmarkRecord bookmark(String guid, String parent) {
+ BookmarkRecord bookmark = new BookmarkRecord(guid);
+ bookmark.type = "bookmark";
+ bookmark.parentID = parent;
+ return bookmark;
+ }
+
+ protected static BookmarkRecord folder(String guid, String parent) {
+ BookmarkRecord bookmark = new BookmarkRecord(guid);
+ bookmark.type = "folder";
+ bookmark.parentID = parent;
+ return bookmark;
+ }
+
+ @Test
+ public void testChildrenBeforeFolder() {
+ BookmarkRecord folder = folder("folder", "mobile");
+ BookmarkRecord child1 = bookmark("child1", "folder");
+ BookmarkRecord child2 = bookmark("child2", "folder");
+
+ manager.enqueueRecord(child1);
+ assertTrue(insertions.isEmpty());
+ manager.enqueueRecord(child2);
+ assertTrue(insertions.isEmpty());
+ manager.enqueueRecord(folder);
+ assertEquals(1, insertions.size());
+ manager.finishUp();
+ assertTrue(manager.isClear());
+ assertEquals(2, insertions.size());
+ assertArrayEquals(new String[] { "folder" }, insertions.get(0));
+ assertArrayEquals(new String[] { "child1", "child2" }, insertions.get(1));
+ }
+
+ @Test
+ public void testChildAfterFolder() {
+ BookmarkRecord folder = folder("folder", "mobile");
+ BookmarkRecord child1 = bookmark("child1", "folder");
+ BookmarkRecord child2 = bookmark("child2", "folder");
+
+ manager.enqueueRecord(child1);
+ assertTrue(insertions.isEmpty());
+ manager.enqueueRecord(folder);
+ assertEquals(1, insertions.size());
+ manager.enqueueRecord(child2);
+ assertEquals(1, insertions.size());
+ manager.finishUp();
+ assertTrue(manager.isClear());
+ assertEquals(2, insertions.size());
+ assertArrayEquals(new String[] { "folder" }, insertions.get(0));
+ assertArrayEquals(new String[] { "child1", "child2" }, insertions.get(1));
+ }
+
+ @Test
+ public void testFolderAfterFolder() {
+ manager.enqueueRecord(bookmark("child1", "folder1"));
+ assertEquals(0, insertions.size());
+ manager.enqueueRecord(folder("folder1", "mobile"));
+ assertEquals(1, insertions.size());
+ manager.enqueueRecord(bookmark("child2", "folder2"));
+ assertEquals(1, insertions.size());
+ manager.enqueueRecord(folder("folder2", "folder1"));
+ assertEquals(2, insertions.size());
+ manager.enqueueRecord(bookmark("child3", "folder1"));
+ manager.enqueueRecord(bookmark("child4", "folder2"));
+ assertEquals(3, insertions.size());
+
+ manager.finishUp();
+ assertTrue(manager.isClear());
+ assertEquals(4, insertions.size());
+ assertArrayEquals(new String[] { "folder1" }, insertions.get(0));
+ assertArrayEquals(new String[] { "folder2" }, insertions.get(1));
+ assertArrayEquals(new String[] { "child1", "child2", "child3" }, insertions.get(2));
+ assertArrayEquals(new String[] { "child4" }, insertions.get(3));
+ }
+
+ @Test
+ public void testFolderRecursion() {
+ manager.enqueueRecord(folder("1", "mobile"));
+ manager.enqueueRecord(folder("2", "1"));
+ assertEquals(2, insertions.size());
+ manager.enqueueRecord(bookmark("3a", "3"));
+ manager.enqueueRecord(bookmark("3b", "3"));
+ manager.enqueueRecord(bookmark("3c", "3"));
+ manager.enqueueRecord(bookmark("4a", "4"));
+ manager.enqueueRecord(bookmark("4b", "4"));
+ manager.enqueueRecord(bookmark("4c", "4"));
+ assertEquals(2, insertions.size());
+ manager.enqueueRecord(folder("3", "2"));
+ assertEquals(4, insertions.size());
+ manager.enqueueRecord(folder("4", "2"));
+ assertEquals(6, insertions.size());
+
+ assertTrue(manager.isClear());
+ manager.finishUp();
+ assertTrue(manager.isClear());
+ // Folders in order.
+ assertArrayEquals(new String[] { "1" }, insertions.get(0));
+ assertArrayEquals(new String[] { "2" }, insertions.get(1));
+ assertArrayEquals(new String[] { "3" }, insertions.get(2));
+ // Then children in batches of 3.
+ assertArrayEquals(new String[] { "3a", "3b", "3c" }, insertions.get(3));
+ // Then last folder.
+ assertArrayEquals(new String[] { "4" }, insertions.get(4));
+ assertArrayEquals(new String[] { "4a", "4b", "4c" }, insertions.get(5));
+ }
+
+ @Test
+ public void testFailedFolderInsertion() {
+ manager.enqueueRecord(bookmark("failA", "fail"));
+ manager.enqueueRecord(bookmark("failB", "fail"));
+ assertEquals(0, insertions.size());
+ manager.enqueueRecord(folder("fail", "mobile"));
+ assertEquals(0, insertions.size());
+ manager.enqueueRecord(bookmark("failC", "fail"));
+ assertEquals(0, insertions.size());
+ manager.finishUp(); // Children inserted at the end; they will be treated as orphans.
+ assertTrue(manager.isClear());
+ assertEquals(1, insertions.size());
+ assertArrayEquals(new String[] { "failA", "failB", "failC" }, insertions.get(0));
+ }
+
+ @Test
+ public void testIncrementalFlush() {
+ manager.enqueueRecord(bookmark("a", "1"));
+ manager.enqueueRecord(bookmark("b", "1"));
+ manager.enqueueRecord(folder("1", "mobile"));
+ assertEquals(1, insertions.size());
+ manager.enqueueRecord(bookmark("c", "1"));
+ assertEquals(2, insertions.size());
+ manager.enqueueRecord(bookmark("d", "1"));
+ manager.enqueueRecord(bookmark("e", "1"));
+ manager.enqueueRecord(bookmark("f", "1"));
+ assertEquals(3, insertions.size());
+ manager.enqueueRecord(bookmark("g", "1")); // Start of new batch.
+ assertEquals(3, insertions.size());
+ manager.finishUp(); // Children inserted at the end; they will be treated as orphans.
+ assertTrue(manager.isClear());
+ assertEquals(4, insertions.size());
+ assertArrayEquals(new String[] { "1" }, insertions.get(0));
+ assertArrayEquals(new String[] { "a", "b", "c"}, insertions.get(1));
+ assertArrayEquals(new String[] { "d", "e", "f"}, insertions.get(2));
+ assertArrayEquals(new String[] { "g" }, insertions.get(3));
+ }
+
+ @Test
+ public void testFinishUp() {
+ manager.enqueueRecord(bookmark("a", "1"));
+ manager.enqueueRecord(bookmark("b", "1"));
+ manager.enqueueRecord(folder("2", "1"));
+ manager.enqueueRecord(bookmark("c", "1"));
+ manager.enqueueRecord(bookmark("d", "1"));
+ manager.enqueueRecord(folder("3", "1"));
+ assertEquals(0, insertions.size());
+ manager.finishUp(); // Children inserted at the end; they will be treated as orphans.
+ assertTrue(manager.isClear());
+ assertEquals(3, insertions.size());
+ assertArrayEquals(new String[] { "2" }, insertions.get(0));
+ assertArrayEquals(new String[] { "3" }, insertions.get(1));
+ assertArrayEquals(new String[] { "a", "b", "c", "d" }, insertions.get(2)); // Last insertion could be big.
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/domain/TestClientRecord.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/domain/TestClientRecord.java
new file mode 100644
index 0000000000..790d476208
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/domain/TestClientRecord.java
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.repositories.domain;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.Utils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
+public class TestClientRecord {
+
+ @Test
+ public void testEnsureDefaults() {
+ // Ensure defaults.
+ ClientRecord record = new ClientRecord();
+ assertEquals(ClientRecord.COLLECTION_NAME, record.collection);
+ assertEquals(0, record.lastModified);
+ assertEquals(false, record.deleted);
+ assertEquals("Default Name", record.name);
+ assertEquals(ClientRecord.CLIENT_TYPE, record.type);
+ assertTrue(null == record.os);
+ assertTrue(null == record.device);
+ assertTrue(null == record.application);
+ assertTrue(null == record.appPackage);
+ assertTrue(null == record.formfactor);
+ }
+
+ @Test
+ public void testGetPayload() {
+ // Test ClientRecord.getPayload().
+ ClientRecord record = new ClientRecord();
+ CryptoRecord cryptoRecord = record.getEnvelope();
+ assertEquals(record.guid, cryptoRecord.payload.get("id"));
+ assertEquals(null, cryptoRecord.payload.get("collection"));
+ assertEquals(null, cryptoRecord.payload.get("lastModified"));
+ assertEquals(null, cryptoRecord.payload.get("deleted"));
+ assertEquals(null, cryptoRecord.payload.get("version"));
+ assertEquals(record.name, cryptoRecord.payload.get("name"));
+ assertEquals(record.type, cryptoRecord.payload.get("type"));
+ }
+
+ @Test
+ public void testInitFromPayload() {
+ // Test ClientRecord.initFromPayload() in ClientRecordFactory.
+ ClientRecord record1 = new ClientRecord();
+ CryptoRecord cryptoRecord = record1.getEnvelope();
+ ClientRecordFactory factory = new ClientRecordFactory();
+ ClientRecord record2 = (ClientRecord) factory.createRecord(cryptoRecord);
+ assertEquals(cryptoRecord.payload.get("id"), record2.guid);
+ assertEquals(ClientRecord.COLLECTION_NAME, record2.collection);
+ assertEquals(0, record2.lastModified);
+ assertEquals(false, record2.deleted);
+ assertEquals(cryptoRecord.payload.get("name"), record2.name);
+ assertEquals(cryptoRecord.payload.get("type"), record2.type);
+ }
+
+ @Test
+ public void testCopyWithIDs() {
+ // Test ClientRecord.copyWithIDs.
+ ClientRecord record1 = new ClientRecord();
+ record1.version = "20";
+ String newGUID = Utils.generateGuid();
+ ClientRecord record2 = (ClientRecord) record1.copyWithIDs(newGUID, 0);
+ assertEquals(newGUID, record2.guid);
+ assertEquals(0, record2.androidID);
+ assertEquals(record1.collection, record2.collection);
+ assertEquals(record1.lastModified, record2.lastModified);
+ assertEquals(record1.deleted, record2.deleted);
+ assertEquals(record1.name, record2.name);
+ assertEquals(record1.type, record2.type);
+ assertEquals(record1.version, record2.version);
+ }
+
+ @Test
+ public void testEquals() {
+ // Test ClientRecord.equals().
+ ClientRecord record1 = new ClientRecord();
+ ClientRecord record2 = new ClientRecord();
+ record2.guid = record1.guid;
+ record2.version = "20";
+ record1.version = null;
+
+ ClientRecord record3 = new ClientRecord(Utils.generateGuid());
+ record3.name = "New Name";
+
+ ClientRecord record4 = new ClientRecord(Utils.generateGuid());
+ record4.name = ClientRecord.DEFAULT_CLIENT_NAME;
+ record4.type = "desktop";
+
+ assertTrue(record2.equals(record1));
+ assertFalse(record3.equals(record1));
+ assertFalse(record3.equals(record2));
+ assertFalse(record4.equals(record1));
+ assertFalse(record4.equals(record2));
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/domain/test/TestFormHistoryRecord.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/domain/test/TestFormHistoryRecord.java
new file mode 100644
index 0000000000..c0682e90e7
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/domain/test/TestFormHistoryRecord.java
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.repositories.domain.test;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.repositories.domain.FormHistoryRecord;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
+public class TestFormHistoryRecord {
+ public static FormHistoryRecord withIdFieldNameAndValue(long id, String fieldName, String value) {
+ FormHistoryRecord fr = new FormHistoryRecord();
+ fr.androidID = id;
+ fr.fieldName = fieldName;
+ fr.fieldValue = value;
+
+ return fr;
+ }
+
+ @Test
+ public void testCollection() {
+ FormHistoryRecord fr = new FormHistoryRecord();
+ assertEquals("forms", fr.collection);
+ }
+
+ @Test
+ public void testGetPayload() {
+ FormHistoryRecord fr = withIdFieldNameAndValue(0, "username", "aUsername");
+ CryptoRecord rec = fr.getEnvelope();
+ assertEquals("username", rec.payload.get("name"));
+ assertEquals("aUsername", rec.payload.get("value"));
+ }
+
+ @Test
+ public void testCopyWithIDs() {
+ FormHistoryRecord fr = withIdFieldNameAndValue(0, "username", "aUsername");
+ String guid = Utils.generateGuid();
+ FormHistoryRecord fr2 = (FormHistoryRecord)fr.copyWithIDs(guid, 9999);
+ assertEquals(guid, fr2.guid);
+ assertEquals(9999, fr2.androidID);
+ assertEquals(fr.fieldName, fr2.fieldName);
+ assertEquals(fr.fieldValue, fr2.fieldValue);
+ }
+
+ @Test
+ public void testEquals() {
+ FormHistoryRecord fr1a = withIdFieldNameAndValue(0, "username1", "Alice");
+ FormHistoryRecord fr1b = withIdFieldNameAndValue(0, "username1", "Bob");
+ FormHistoryRecord fr2a = withIdFieldNameAndValue(0, "username2", "Alice");
+ FormHistoryRecord fr2b = withIdFieldNameAndValue(0, "username2", "Bob");
+
+ assertFalse(fr1a.equals(fr1b));
+ assertFalse(fr1a.equals(fr2a));
+ assertFalse(fr1a.equals(fr2b));
+ assertFalse(fr1b.equals(fr2a));
+ assertFalse(fr1b.equals(fr2b));
+ assertFalse(fr2a.equals(fr2b));
+
+ assertFalse(fr1a.equals(withIdFieldNameAndValue(fr1a.androidID, fr1a.fieldName, fr1b.fieldValue)));
+ assertFalse(fr1a.equals(fr1a.copyWithIDs(fr2a.guid, 9999)));
+ assertTrue(fr1a.equals(fr1a));
+ }
+
+ @Test
+ public void testEqualsForDeleted() {
+ FormHistoryRecord fr1 = withIdFieldNameAndValue(0, "username1", "Alice");
+ FormHistoryRecord fr2 = (FormHistoryRecord)fr1.copyWithIDs(fr1.guid, fr1.androidID);
+ assertTrue(fr1.equals(fr2));
+ fr1.deleted = true;
+ assertFalse(fr1.equals(fr2));
+ fr2.deleted = true;
+ assertTrue(fr1.equals(fr2));
+ FormHistoryRecord fr3 = (FormHistoryRecord)fr2.copyWithIDs(Utils.generateGuid(), 9999);
+ assertFalse(fr2.equals(fr3));
+ }
+
+ @Test
+ public void testTTL() {
+ FormHistoryRecord fr = withIdFieldNameAndValue(0, "username", "aUsername");
+ assertEquals(FormHistoryRecord.FORMS_TTL, fr.ttl);
+ CryptoRecord rec = fr.getEnvelope();
+ assertEquals(FormHistoryRecord.FORMS_TTL, rec.ttl);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderDelegateTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderDelegateTest.java
new file mode 100644
index 0000000000..da2bbac188
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderDelegateTest.java
@@ -0,0 +1,186 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.downloaders;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.HTTPFailureException;
+import org.mozilla.gecko.sync.InfoCollections;
+import org.mozilla.gecko.sync.InfoConfiguration;
+import org.mozilla.gecko.sync.net.SyncResponse;
+import org.mozilla.gecko.sync.net.SyncStorageCollectionRequest;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.sync.repositories.Server11Repository;
+import org.mozilla.gecko.sync.repositories.Server11RepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import java.net.URI;
+import java.util.concurrent.ExecutorService;
+
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
+import ch.boye.httpclientandroidlib.message.BasicStatusLine;
+
+import static org.junit.Assert.*;
+
+@RunWith(TestRunner.class)
+public class BatchingDownloaderDelegateTest {
+ private Server11Repository server11Repository;
+ private Server11RepositorySession repositorySession;
+ private MockDownloader mockDownloader;
+ private String DEFAULT_COLLECTION_URL = "http://dummy.url/";
+
+ class MockDownloader extends BatchingDownloader {
+ public boolean isSuccess = false;
+ public boolean isFetched = false;
+ public boolean isFailure = false;
+ public Exception ex;
+
+ public MockDownloader(Server11Repository repository, Server11RepositorySession repositorySession) {
+ super(repository, repositorySession);
+ }
+
+ @Override
+ public void onFetchCompleted(SyncStorageResponse response,
+ final RepositorySessionFetchRecordsDelegate fetchRecordsDelegate,
+ final SyncStorageCollectionRequest request,
+ long l, long newerTimestamp, boolean full, String sort, String ids) {
+ this.isSuccess = true;
+ }
+
+ @Override
+ public void onFetchFailed(final Exception ex,
+ final RepositorySessionFetchRecordsDelegate fetchRecordsDelegate,
+ final SyncStorageCollectionRequest request) {
+ this.isFailure = true;
+ this.ex = ex;
+ }
+
+ @Override
+ public void onFetchedRecord(CryptoRecord record,
+ RepositorySessionFetchRecordsDelegate fetchRecordsDelegate) {
+ this.isFetched = true;
+ }
+ }
+
+ class SimpleSessionFetchRecordsDelegate implements RepositorySessionFetchRecordsDelegate {
+ @Override
+ public void onFetchFailed(Exception ex, Record record) {
+
+ }
+
+ @Override
+ public void onFetchedRecord(Record record) {
+
+ }
+
+ @Override
+ public void onFetchCompleted(long fetchEnd) {
+
+ }
+
+ @Override
+ public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService executor) {
+ return null;
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ server11Repository = new Server11Repository(
+ "dummyCollection",
+ DEFAULT_COLLECTION_URL,
+ null,
+ new InfoCollections(),
+ new InfoConfiguration());
+ repositorySession = new Server11RepositorySession(server11Repository);
+ mockDownloader = new MockDownloader(server11Repository, repositorySession);
+ }
+
+ @Test
+ public void testIfUnmodifiedSince() throws Exception {
+ BatchingDownloader downloader = new BatchingDownloader(server11Repository, repositorySession);
+ RepositorySessionFetchRecordsDelegate delegate = new SimpleSessionFetchRecordsDelegate();
+ BatchingDownloaderDelegate downloaderDelegate = new BatchingDownloaderDelegate(downloader, delegate,
+ new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL)), 0, 0, true, null, null);
+ String lastModified = "12345678";
+ SyncStorageResponse response = makeSyncStorageResponse(200, lastModified);
+ downloaderDelegate.handleRequestSuccess(response);
+ assertEquals(lastModified, downloaderDelegate.ifUnmodifiedSince());
+ }
+
+ @Test
+ public void testSuccess() throws Exception {
+ BatchingDownloaderDelegate downloaderDelegate = new BatchingDownloaderDelegate(mockDownloader, null,
+ new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL)), 0, 0, true, null, null);
+ SyncStorageResponse response = makeSyncStorageResponse(200, "12345678");
+ downloaderDelegate.handleRequestSuccess(response);
+ assertTrue(mockDownloader.isSuccess);
+ assertFalse(mockDownloader.isFailure);
+ assertFalse(mockDownloader.isFetched);
+ }
+
+ @Test
+ public void testFailureMissingLMHeader() throws Exception {
+ BatchingDownloaderDelegate downloaderDelegate = new BatchingDownloaderDelegate(mockDownloader, null,
+ new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL)), 0, 0, true, null, null);
+ SyncStorageResponse response = makeSyncStorageResponse(200, null);
+ downloaderDelegate.handleRequestSuccess(response);
+ assertTrue(mockDownloader.isFailure);
+ assertEquals(IllegalStateException.class, mockDownloader.ex.getClass());
+ assertFalse(mockDownloader.isSuccess);
+ assertFalse(mockDownloader.isFetched);
+ }
+
+ @Test
+ public void testFailureHTTPException() throws Exception {
+ BatchingDownloaderDelegate downloaderDelegate = new BatchingDownloaderDelegate(mockDownloader, null,
+ new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL)), 0, 0, true, null, null);
+ SyncStorageResponse response = makeSyncStorageResponse(400, null);
+ downloaderDelegate.handleRequestFailure(response);
+ assertTrue(mockDownloader.isFailure);
+ assertEquals(HTTPFailureException.class, mockDownloader.ex.getClass());
+ assertFalse(mockDownloader.isSuccess);
+ assertFalse(mockDownloader.isFetched);
+ }
+
+ @Test
+ public void testFailureRequestError() throws Exception {
+ BatchingDownloaderDelegate downloaderDelegate = new BatchingDownloaderDelegate(mockDownloader, null,
+ new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL)), 0, 0, true, null, null);
+ downloaderDelegate.handleRequestError(new ClientProtocolException());
+ assertTrue(mockDownloader.isFailure);
+ assertEquals(ClientProtocolException.class, mockDownloader.ex.getClass());
+ assertFalse(mockDownloader.isSuccess);
+ assertFalse(mockDownloader.isFetched);
+ }
+
+ @Test
+ public void testFetchRecord() throws Exception {
+ BatchingDownloaderDelegate downloaderDelegate = new BatchingDownloaderDelegate(mockDownloader, null,
+ new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL)), 0, 0, true, null, null);
+ CryptoRecord record = new CryptoRecord();
+ downloaderDelegate.handleWBO(record);
+ assertTrue(mockDownloader.isFetched);
+ assertFalse(mockDownloader.isSuccess);
+ assertFalse(mockDownloader.isFailure);
+ }
+
+ private SyncStorageResponse makeSyncStorageResponse(int code, String lastModified) {
+ BasicHttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), code, null));
+
+ if (lastModified != null) {
+ response.addHeader(SyncResponse.X_LAST_MODIFIED, lastModified);
+ }
+
+ return new SyncStorageResponse(response);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderTest.java
new file mode 100644
index 0000000000..fbbd9cae92
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderTest.java
@@ -0,0 +1,543 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.downloaders;
+
+import android.support.annotation.NonNull;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.InfoCollections;
+import org.mozilla.gecko.sync.InfoConfiguration;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.SyncResponse;
+import org.mozilla.gecko.sync.net.SyncStorageCollectionRequest;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.Server11Repository;
+import org.mozilla.gecko.sync.repositories.Server11RepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.concurrent.ExecutorService;
+
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
+import ch.boye.httpclientandroidlib.message.BasicStatusLine;
+
+import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+
+@RunWith(TestRunner.class)
+public class BatchingDownloaderTest {
+ private MockSever11Repository serverRepository;
+ private Server11RepositorySession repositorySession;
+ private MockSessionFetchRecordsDelegate sessionFetchRecordsDelegate;
+ private MockDownloader mockDownloader;
+ private String DEFAULT_COLLECTION_NAME = "dummyCollection";
+ private String DEFAULT_COLLECTION_URL = "http://dummy.url/";
+ private long DEFAULT_NEWER = 1;
+ private String DEFAULT_SORT = "index";
+ private String DEFAULT_IDS = "1";
+ private String DEFAULT_LMHEADER = "12345678";
+
+ class MockSessionFetchRecordsDelegate implements RepositorySessionFetchRecordsDelegate {
+ public boolean isFailure;
+ public boolean isFetched;
+ public boolean isSuccess;
+ public Exception ex;
+ public Record record;
+
+ @Override
+ public void onFetchFailed(Exception ex, Record record) {
+ this.isFailure = true;
+ this.ex = ex;
+ this.record = record;
+ }
+
+ @Override
+ public void onFetchedRecord(Record record) {
+ this.isFetched = true;
+ this.record = record;
+ }
+
+ @Override
+ public void onFetchCompleted(long fetchEnd) {
+ this.isSuccess = true;
+ }
+
+ @Override
+ public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService executor) {
+ return null;
+ }
+ }
+
+ class MockRequest extends SyncStorageCollectionRequest {
+
+ public MockRequest(URI uri) {
+ super(uri);
+ }
+
+ @Override
+ public void get() {
+
+ }
+ }
+
+ class MockDownloader extends BatchingDownloader {
+ public long newer;
+ public long limit;
+ public boolean full;
+ public String sort;
+ public String ids;
+ public String offset;
+ public boolean abort;
+
+ public MockDownloader(Server11Repository repository, Server11RepositorySession repositorySession) {
+ super(repository, repositorySession);
+ }
+
+ @Override
+ public void fetchWithParameters(long newer,
+ long batchLimit,
+ boolean full,
+ String sort,
+ String ids,
+ SyncStorageCollectionRequest request,
+ RepositorySessionFetchRecordsDelegate fetchRecordsDelegate)
+ throws UnsupportedEncodingException, URISyntaxException {
+ this.newer = newer;
+ this.limit = batchLimit;
+ this.full = full;
+ this.sort = sort;
+ this.ids = ids;
+ MockRequest mockRequest = new MockRequest(new URI(DEFAULT_COLLECTION_URL));
+ super.fetchWithParameters(newer, batchLimit, full, sort, ids, mockRequest, fetchRecordsDelegate);
+ }
+
+ @Override
+ public void abortRequests() {
+ this.abort = true;
+ }
+
+ @Override
+ public SyncStorageCollectionRequest makeSyncStorageCollectionRequest(long newer,
+ long batchLimit,
+ boolean full,
+ String sort,
+ String ids,
+ String offset)
+ throws URISyntaxException, UnsupportedEncodingException {
+ this.offset = offset;
+ return super.makeSyncStorageCollectionRequest(newer, batchLimit, full, sort, ids, offset);
+ }
+ }
+
+ class MockSever11Repository extends Server11Repository {
+ public MockSever11Repository(@NonNull String collection, @NonNull String storageURL,
+ AuthHeaderProvider authHeaderProvider, @NonNull InfoCollections infoCollections,
+ @NonNull InfoConfiguration infoConfiguration) throws URISyntaxException {
+ super(collection, storageURL, authHeaderProvider, infoCollections, infoConfiguration);
+ }
+
+ @Override
+ public long getDefaultTotalLimit() {
+ return 200;
+ }
+ }
+
+ class MockRepositorySession extends Server11RepositorySession {
+ public boolean abort;
+
+ public MockRepositorySession(Repository repository) {
+ super(repository);
+ }
+
+ @Override
+ public void abort() {
+ this.abort = true;
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ sessionFetchRecordsDelegate = new MockSessionFetchRecordsDelegate();
+
+ serverRepository = new MockSever11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL, null,
+ new InfoCollections(), new InfoConfiguration());
+ repositorySession = new Server11RepositorySession(serverRepository);
+ mockDownloader = new MockDownloader(serverRepository, repositorySession);
+ }
+
+ @Test
+ public void testFlattenId() {
+ String[] emptyGuid = new String[]{};
+ String flatten = BatchingDownloader.flattenIDs(emptyGuid);
+ assertEquals("", flatten);
+
+ String guid0 = "123456789abc";
+ String[] singleGuid = new String[1];
+ singleGuid[0] = guid0;
+ flatten = BatchingDownloader.flattenIDs(singleGuid);
+ assertEquals("123456789abc", flatten);
+
+ String guid1 = "456789abc";
+ String guid2 = "789abc";
+ String[] multiGuid = new String[3];
+ multiGuid[0] = guid0;
+ multiGuid[1] = guid1;
+ multiGuid[2] = guid2;
+ flatten = BatchingDownloader.flattenIDs(multiGuid);
+ assertEquals("123456789abc,456789abc,789abc", flatten);
+ }
+
+ @Test
+ public void testEncodeParam() throws Exception {
+ String param = "123&123";
+ String encodedParam = mockDownloader.encodeParam(param);
+ assertEquals("123%26123", encodedParam);
+ }
+
+ @Test(expected=IllegalArgumentException.class)
+ public void testOverTotalLimit() throws Exception {
+ // Per-batch limits exceed total.
+ Server11Repository repository = new Server11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL,
+ null, new InfoCollections(), new InfoConfiguration()) {
+ @Override
+ public long getDefaultTotalLimit() {
+ return 100;
+ }
+ @Override
+ public long getDefaultBatchLimit() {
+ return 200;
+ }
+ };
+ MockDownloader mockDownloader = new MockDownloader(repository, repositorySession);
+
+ assertNull(mockDownloader.getLastModified());
+ mockDownloader.fetchSince(DEFAULT_NEWER, sessionFetchRecordsDelegate);
+ }
+
+ @Test
+ public void testTotalLimit() throws Exception {
+ // Total and per-batch limits are the same.
+ Server11Repository repository = new Server11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL,
+ null, new InfoCollections(), new InfoConfiguration()) {
+ @Override
+ public long getDefaultTotalLimit() {
+ return 100;
+ }
+ @Override
+ public long getDefaultBatchLimit() {
+ return 100;
+ }
+ };
+ MockDownloader mockDownloader = new MockDownloader(repository, repositorySession);
+
+ assertNull(mockDownloader.getLastModified());
+ mockDownloader.fetchSince(DEFAULT_NEWER, sessionFetchRecordsDelegate);
+
+ SyncStorageResponse response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, "100", "100");
+ SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL));
+ long limit = repository.getDefaultBatchLimit();
+ mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request,
+ DEFAULT_NEWER, limit, true, DEFAULT_SORT, DEFAULT_IDS);
+
+ assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
+ assertTrue(sessionFetchRecordsDelegate.isSuccess);
+ assertFalse(sessionFetchRecordsDelegate.isFetched);
+ assertFalse(sessionFetchRecordsDelegate.isFailure);
+ }
+
+ @Test
+ public void testOverHalfOfTotalLimit() throws Exception {
+ // Per-batch limit is just a bit lower than total.
+ Server11Repository repository = new Server11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL,
+ null, new InfoCollections(), new InfoConfiguration()) {
+ @Override
+ public long getDefaultTotalLimit() {
+ return 100;
+ }
+ @Override
+ public long getDefaultBatchLimit() {
+ return 75;
+ }
+ };
+ MockDownloader mockDownloader = new MockDownloader(repository, repositorySession);
+
+ assertNull(mockDownloader.getLastModified());
+ mockDownloader.fetchSince(DEFAULT_NEWER, sessionFetchRecordsDelegate);
+
+ String offsetHeader = "75";
+ String recordsHeader = "75";
+ SyncStorageResponse response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader);
+ SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL));
+ long limit = repository.getDefaultBatchLimit();
+
+ mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
+ limit, true, DEFAULT_SORT, DEFAULT_IDS);
+
+ assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
+ // Verify the same parameters are used in the next fetch.
+ assertSameParameters(mockDownloader, limit);
+ assertEquals(offsetHeader, mockDownloader.offset);
+ assertFalse(sessionFetchRecordsDelegate.isSuccess);
+ assertFalse(sessionFetchRecordsDelegate.isFetched);
+ assertFalse(sessionFetchRecordsDelegate.isFailure);
+
+ // The next batch, we still have an offset token but we complete our fetch since we have reached the total limit.
+ offsetHeader = "150";
+ response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader);
+ mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
+ limit, true, DEFAULT_SORT, DEFAULT_IDS);
+
+ assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
+ assertTrue(sessionFetchRecordsDelegate.isSuccess);
+ assertFalse(sessionFetchRecordsDelegate.isFetched);
+ assertFalse(sessionFetchRecordsDelegate.isFailure);
+ }
+
+ @Test
+ public void testHalfOfTotalLimit() throws Exception {
+ // Per-batch limit is half of total.
+ Server11Repository repository = new Server11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL,
+ null, new InfoCollections(), new InfoConfiguration()) {
+ @Override
+ public long getDefaultTotalLimit() {
+ return 100;
+ }
+ @Override
+ public long getDefaultBatchLimit() {
+ return 50;
+ }
+ };
+ mockDownloader = new MockDownloader(repository, repositorySession);
+
+ assertNull(mockDownloader.getLastModified());
+ mockDownloader.fetchSince(DEFAULT_NEWER, sessionFetchRecordsDelegate);
+
+ String offsetHeader = "50";
+ String recordsHeader = "50";
+ SyncStorageResponse response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader);
+ SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL));
+ long limit = repository.getDefaultBatchLimit();
+ mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
+ limit, true, DEFAULT_SORT, DEFAULT_IDS);
+
+ assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
+ // Verify the same parameters are used in the next fetch.
+ assertSameParameters(mockDownloader, limit);
+ assertEquals(offsetHeader, mockDownloader.offset);
+ assertFalse(sessionFetchRecordsDelegate.isSuccess);
+ assertFalse(sessionFetchRecordsDelegate.isFetched);
+ assertFalse(sessionFetchRecordsDelegate.isFailure);
+
+ // The next batch, we still have an offset token but we complete our fetch since we have reached the total limit.
+ offsetHeader = "100";
+ response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader);
+ mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
+ limit, true, DEFAULT_SORT, DEFAULT_IDS);
+
+ assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
+ assertTrue(sessionFetchRecordsDelegate.isSuccess);
+ assertFalse(sessionFetchRecordsDelegate.isFetched);
+ assertFalse(sessionFetchRecordsDelegate.isFailure);
+ }
+
+ @Test
+ public void testFractionOfTotalLimit() throws Exception {
+ // Per-batch limit is a small fraction of the total.
+ Server11Repository repository = new Server11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL,
+ null, new InfoCollections(), new InfoConfiguration()) {
+ @Override
+ public long getDefaultTotalLimit() {
+ return 100;
+ }
+ @Override
+ public long getDefaultBatchLimit() {
+ return 25;
+ }
+ };
+ mockDownloader = new MockDownloader(repository, repositorySession);
+
+ assertNull(mockDownloader.getLastModified());
+ mockDownloader.fetchSince(DEFAULT_NEWER, sessionFetchRecordsDelegate);
+
+ String offsetHeader = "25";
+ String recordsHeader = "25";
+ SyncStorageResponse response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader);
+ SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL));
+ long limit = repository.getDefaultBatchLimit();
+ mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
+ limit, true, DEFAULT_SORT, DEFAULT_IDS);
+
+ assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
+ // Verify the same parameters are used in the next fetch.
+ assertSameParameters(mockDownloader, limit);
+ assertEquals(offsetHeader, mockDownloader.offset);
+ assertFalse(sessionFetchRecordsDelegate.isSuccess);
+ assertFalse(sessionFetchRecordsDelegate.isFetched);
+ assertFalse(sessionFetchRecordsDelegate.isFailure);
+
+ // The next batch, we still have an offset token and has not exceed the total limit.
+ offsetHeader = "50";
+ response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader);
+ mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
+ limit, true, DEFAULT_SORT, DEFAULT_IDS);
+
+ assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
+ // Verify the same parameters are used in the next fetch.
+ assertSameParameters(mockDownloader, limit);
+ assertEquals(offsetHeader, mockDownloader.offset);
+ assertFalse(sessionFetchRecordsDelegate.isSuccess);
+ assertFalse(sessionFetchRecordsDelegate.isFetched);
+ assertFalse(sessionFetchRecordsDelegate.isFailure);
+
+ // The next batch, we still have an offset token and has not exceed the total limit.
+ offsetHeader = "75";
+ response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader);
+ mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
+ limit, true, DEFAULT_SORT, DEFAULT_IDS);
+
+ assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
+ // Verify the same parameters are used in the next fetch.
+ assertSameParameters(mockDownloader, limit);
+ assertEquals(offsetHeader, mockDownloader.offset);
+ assertFalse(sessionFetchRecordsDelegate.isSuccess);
+ assertFalse(sessionFetchRecordsDelegate.isFetched);
+ assertFalse(sessionFetchRecordsDelegate.isFailure);
+
+ // The next batch, we still have an offset token but we complete our fetch since we have reached the total limit.
+ offsetHeader = "100";
+ response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader);
+ mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
+ limit, true, DEFAULT_SORT, DEFAULT_IDS);
+
+ assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
+ assertTrue(sessionFetchRecordsDelegate.isSuccess);
+ assertFalse(sessionFetchRecordsDelegate.isFetched);
+ assertFalse(sessionFetchRecordsDelegate.isFailure);
+ }
+
+ @Test
+ public void testFailureLMChangedMultiBatch() throws Exception {
+ assertNull(mockDownloader.getLastModified());
+
+ String lmHeader = "12345678";
+ String offsetHeader = "100";
+ SyncStorageResponse response = makeSyncStorageResponse(200, lmHeader, offsetHeader, null);
+ SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI("http://dummy.url"));
+ long limit = 1;
+ mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
+ limit, true, DEFAULT_SORT, DEFAULT_IDS);
+
+ assertEquals(lmHeader, mockDownloader.getLastModified());
+ // Verify the same parameters are used in the next fetch.
+ assertEquals(DEFAULT_NEWER, mockDownloader.newer);
+ assertEquals(limit, mockDownloader.limit);
+ assertTrue(mockDownloader.full);
+ assertEquals(DEFAULT_SORT, mockDownloader.sort);
+ assertEquals(DEFAULT_IDS, mockDownloader.ids);
+ assertEquals(offsetHeader, mockDownloader.offset);
+ assertFalse(sessionFetchRecordsDelegate.isSuccess);
+ assertFalse(sessionFetchRecordsDelegate.isFetched);
+ assertFalse(sessionFetchRecordsDelegate.isFailure);
+
+ // Last modified header somehow changed.
+ lmHeader = "10000000";
+ response = makeSyncStorageResponse(200, lmHeader, offsetHeader, null);
+ mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
+ limit, true, DEFAULT_SORT, DEFAULT_IDS);
+
+ assertNotEquals(lmHeader, mockDownloader.getLastModified());
+ assertTrue(mockDownloader.abort);
+ assertFalse(sessionFetchRecordsDelegate.isSuccess);
+ assertFalse(sessionFetchRecordsDelegate.isFetched);
+ assertTrue(sessionFetchRecordsDelegate.isFailure);
+ }
+
+ @Test
+ public void testFailureMissingLMMultiBatch() throws Exception {
+ assertNull(mockDownloader.getLastModified());
+
+ String offsetHeader = "100";
+ SyncStorageResponse response = makeSyncStorageResponse(200, null, offsetHeader, null);
+ SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI("http://dummy.url"));
+ mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
+ 1, true, DEFAULT_SORT, DEFAULT_IDS);
+
+ // Last modified header somehow missing from response.
+ assertNull(null, mockDownloader.getLastModified());
+ assertTrue(mockDownloader.abort);
+ assertFalse(sessionFetchRecordsDelegate.isSuccess);
+ assertFalse(sessionFetchRecordsDelegate.isFetched);
+ assertTrue(sessionFetchRecordsDelegate.isFailure);
+ }
+
+ @Test
+ public void testFailureException() throws Exception {
+ Exception ex = new IllegalStateException();
+ SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI("http://dummy.url"));
+ mockDownloader.onFetchFailed(ex, sessionFetchRecordsDelegate, request);
+
+ assertFalse(sessionFetchRecordsDelegate.isSuccess);
+ assertFalse(sessionFetchRecordsDelegate.isFetched);
+ assertTrue(sessionFetchRecordsDelegate.isFailure);
+ assertEquals(ex.getClass(), sessionFetchRecordsDelegate.ex.getClass());
+ assertNull(sessionFetchRecordsDelegate.record);
+ }
+
+ @Test
+ public void testFetchRecord() {
+ CryptoRecord record = new CryptoRecord();
+ mockDownloader.onFetchedRecord(record, sessionFetchRecordsDelegate);
+
+ assertTrue(sessionFetchRecordsDelegate.isFetched);
+ assertFalse(sessionFetchRecordsDelegate.isSuccess);
+ assertFalse(sessionFetchRecordsDelegate.isFailure);
+ assertEquals(record, sessionFetchRecordsDelegate.record);
+ }
+
+ @Test
+ public void testAbortRequests() {
+ MockRepositorySession mockRepositorySession = new MockRepositorySession(serverRepository);
+ BatchingDownloader downloader = new BatchingDownloader(serverRepository, mockRepositorySession);
+ assertFalse(mockRepositorySession.abort);
+ downloader.abortRequests();
+ assertTrue(mockRepositorySession.abort);
+ }
+
+ private void assertSameParameters(MockDownloader mockDownloader, long limit) {
+ assertEquals(DEFAULT_NEWER, mockDownloader.newer);
+ assertEquals(limit, mockDownloader.limit);
+ assertTrue(mockDownloader.full);
+ assertEquals(DEFAULT_SORT, mockDownloader.sort);
+ assertEquals(DEFAULT_IDS, mockDownloader.ids);
+ }
+
+ private SyncStorageResponse makeSyncStorageResponse(int code, String lastModified, String offset, String records) {
+ BasicHttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), code, null));
+
+ if (lastModified != null) {
+ response.addHeader(SyncResponse.X_LAST_MODIFIED, lastModified);
+ }
+
+ if (offset != null) {
+ response.addHeader(SyncResponse.X_WEAVE_NEXT_OFFSET, offset);
+ }
+
+ if (records != null) {
+ response.addHeader(SyncResponse.X_WEAVE_RECORDS, records);
+ }
+
+ return new SyncStorageResponse(response);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/test/TestRepositorySessionBundle.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/test/TestRepositorySessionBundle.java
new file mode 100644
index 0000000000..e81d13640f
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/test/TestRepositorySessionBundle.java
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.repositories.test;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@RunWith(TestRunner.class)
+public class TestRepositorySessionBundle {
+ @Test
+ public void testSetGetTimestamp() {
+ RepositorySessionBundle bundle = new RepositorySessionBundle(-1);
+ assertEquals(-1, bundle.getTimestamp());
+
+ bundle.setTimestamp(10);
+ assertEquals(10, bundle.getTimestamp());
+ }
+
+ @Test
+ public void testBumpTimestamp() {
+ RepositorySessionBundle bundle = new RepositorySessionBundle(50);
+ assertEquals(50, bundle.getTimestamp());
+
+ bundle.bumpTimestamp(20);
+ assertEquals(50, bundle.getTimestamp());
+
+ bundle.bumpTimestamp(80);
+ assertEquals(80, bundle.getTimestamp());
+ }
+
+ @Test
+ public void testSerialize() throws Exception {
+ RepositorySessionBundle bundle = new RepositorySessionBundle(50);
+
+ String json = bundle.toJSONString();
+ assertNotNull(json);
+
+ RepositorySessionBundle read = new RepositorySessionBundle(json);
+ assertEquals(50, read.getTimestamp());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/test/TestSafeConstrainedServer11Repository.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/test/TestSafeConstrainedServer11Repository.java
new file mode 100644
index 0000000000..249a7831a5
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/test/TestSafeConstrainedServer11Repository.java
@@ -0,0 +1,144 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.repositories.test;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
+import org.mozilla.android.sync.test.helpers.MockServer;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.InfoCollections;
+import org.mozilla.gecko.sync.InfoConfiguration;
+import org.mozilla.gecko.sync.JSONRecordFetcher;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+import org.mozilla.gecko.sync.stage.SafeConstrainedServer11Repository;
+import org.simpleframework.http.Request;
+import org.simpleframework.http.Response;
+
+import java.net.URISyntaxException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@RunWith(TestRunner.class)
+public class TestSafeConstrainedServer11Repository {
+ private static final int TEST_PORT = HTTPServerTestHelper.getTestPort();
+ private static final String TEST_SERVER = "http://localhost:" + TEST_PORT + "/";
+ private static final String TEST_USERNAME = "c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd";
+ private static final String TEST_BASE_PATH = "/1.1/" + TEST_USERNAME + "/";
+
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return null;
+ }
+
+ protected final InfoCollections infoCollections = new InfoCollections();
+ protected final InfoConfiguration infoConfiguration = new InfoConfiguration();
+
+ private class CountsMockServer extends MockServer {
+ public final AtomicInteger count = new AtomicInteger(0);
+ public final AtomicBoolean error = new AtomicBoolean(false);
+
+ @Override
+ public void handle(Request request, Response response) {
+ final String path = request.getPath().getPath();
+ if (error.get()) {
+ super.handle(request, response, 503, "Unavailable.");
+ return;
+ }
+
+ if (!path.startsWith(TEST_BASE_PATH)) {
+ super.handle(request, response, 404, "Not Found");
+ return;
+ }
+
+ if (path.endsWith("info/collections")) {
+ super.handle(request, response, 200, "{\"rotary\": 123456789}");
+ return;
+ }
+
+ if (path.endsWith("info/collection_counts")) {
+ super.handle(request, response, 200, "{\"rotary\": " + count.get() + "}");
+ return;
+ }
+
+ super.handle(request, response);
+ }
+ }
+
+ /**
+ * Ensure that a {@link SafeConstrainedServer11Repository} will advise
+ * skipping if the reported collection counts are higher than its limit.
+ */
+ @Test
+ public void testShouldSkip() throws URISyntaxException {
+ final HTTPServerTestHelper data = new HTTPServerTestHelper();
+ final CountsMockServer server = new CountsMockServer();
+ data.startHTTPServer(server);
+
+ try {
+ String countsURL = TEST_SERVER + TEST_BASE_PATH + "info/collection_counts";
+ JSONRecordFetcher countFetcher = new JSONRecordFetcher(countsURL, getAuthHeaderProvider());
+ String sort = "sortindex";
+ String collection = "rotary";
+
+ final int TEST_LIMIT = 1000;
+ final SafeConstrainedServer11Repository repo = new SafeConstrainedServer11Repository(
+ collection, getCollectionURL(collection), null, infoCollections, infoConfiguration,
+ TEST_LIMIT, TEST_LIMIT, sort, countFetcher);
+
+ final AtomicBoolean shouldSkipLots = new AtomicBoolean(false);
+ final AtomicBoolean shouldSkipFew = new AtomicBoolean(true);
+ final AtomicBoolean shouldSkip503 = new AtomicBoolean (false);
+
+ WaitHelper.getTestWaiter().performWait(2000, new Runnable() {
+ @Override
+ public void run() {
+ repo.createSession(new RepositorySessionCreationDelegate() {
+ @Override
+ public void onSessionCreated(RepositorySession session) {
+ // Try with too many items.
+ server.count.set(TEST_LIMIT + 1);
+ shouldSkipLots.set(session.shouldSkip());
+
+ // … and few enough that we should sync.
+ server.count.set(TEST_LIMIT - 1);
+ shouldSkipFew.set(session.shouldSkip());
+
+ // Now try with an error response. We advise skipping if we can't
+ // fetch counts, because we'll try again later.
+ server.error.set(true);
+ shouldSkip503.set(session.shouldSkip());
+
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ @Override
+ public void onSessionCreateFailed(Exception ex) {
+ WaitHelper.getTestWaiter().performNotify(ex);
+ }
+
+ @Override
+ public RepositorySessionCreationDelegate deferredCreationDelegate() {
+ return this;
+ }
+ }, null);
+ }
+ });
+
+ Assert.assertTrue(shouldSkipLots.get());
+ Assert.assertFalse(shouldSkipFew.get());
+ Assert.assertTrue(shouldSkip503.get());
+
+ } finally {
+ data.stopHTTPServer();
+ }
+ }
+
+ protected String getCollectionURL(String collection) {
+ return TEST_BASE_PATH + "/storage/" + collection;
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/BatchMetaTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/BatchMetaTest.java
new file mode 100644
index 0000000000..2e136c1176
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/BatchMetaTest.java
@@ -0,0 +1,282 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.repositories.uploaders;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+import static org.junit.Assert.*;
+
+@RunWith(TestRunner.class)
+public class BatchMetaTest {
+ private BatchMeta batchMeta;
+ private long byteLimit = 1024;
+ private long recordLimit = 5;
+ private Object lock = new Object();
+ private Long collectionLastModified = 123L;
+
+ @Before
+ public void setUp() throws Exception {
+ batchMeta = new BatchMeta(lock, byteLimit, recordLimit, collectionLastModified);
+ }
+
+ @Test
+ public void testConstructor() {
+ assertEquals(batchMeta.collectionLastModified, collectionLastModified);
+
+ BatchMeta otherBatchMeta = new BatchMeta(lock, byteLimit, recordLimit, null);
+ assertNull(otherBatchMeta.collectionLastModified);
+ }
+
+ @Test
+ public void testGetLastModified() {
+ // Defaults to collection L-M
+ assertEquals(batchMeta.getLastModified(), Long.valueOf(123L));
+
+ try {
+ batchMeta.setLastModified(333L, true);
+ } catch (BatchingUploader.LastModifiedChangedUnexpectedly e) {
+ } catch (BatchingUploader.LastModifiedDidNotChange e) {}
+
+ assertEquals(batchMeta.getLastModified(), Long.valueOf(333L));
+ }
+
+ @Test
+ public void testSetLastModified() {
+ assertEquals(batchMeta.getLastModified(), collectionLastModified);
+
+ try {
+ batchMeta.setLastModified(123L, true);
+ assertEquals(batchMeta.getLastModified(), Long.valueOf(123L));
+ } catch (BatchingUploader.LastModifiedChangedUnexpectedly e) {
+ fail("Should not check for modifications on first L-M set");
+ } catch (BatchingUploader.LastModifiedDidNotChange e) {
+ fail("Should not check for modifications on first L-M set");
+ }
+
+ // Now the same, but passing in 'false' for "expecting to change".
+ batchMeta.reset();
+ assertEquals(batchMeta.getLastModified(), collectionLastModified);
+
+ try {
+ batchMeta.setLastModified(123L, false);
+ assertEquals(batchMeta.getLastModified(), Long.valueOf(123L));
+ } catch (BatchingUploader.LastModifiedChangedUnexpectedly e) {
+ fail("Should not check for modifications on first L-M set");
+ } catch (BatchingUploader.LastModifiedDidNotChange e) {
+ fail("Should not check for modifications on first L-M set");
+ }
+
+ // Test that we can't modify L-M when we're not expecting to
+ try {
+ batchMeta.setLastModified(333L, false);
+ } catch (BatchingUploader.LastModifiedChangedUnexpectedly e) {
+ assertTrue("Must throw when L-M changes unexpectedly", true);
+ } catch (BatchingUploader.LastModifiedDidNotChange e) {
+ fail("Not expecting did-not-change throw");
+ }
+ assertEquals(batchMeta.getLastModified(), Long.valueOf(123L));
+
+ // Test that we can modify L-M when we're expecting to
+ try {
+ batchMeta.setLastModified(333L, true);
+ } catch (BatchingUploader.LastModifiedChangedUnexpectedly e) {
+ fail("Not expecting changed-unexpectedly throw");
+ } catch (BatchingUploader.LastModifiedDidNotChange e) {
+ fail("Not expecting did-not-change throw");
+ }
+ assertEquals(batchMeta.getLastModified(), Long.valueOf(333L));
+
+ // Test that we catch L-M modifications that expect to change but actually don't
+ try {
+ batchMeta.setLastModified(333L, true);
+ } catch (BatchingUploader.LastModifiedChangedUnexpectedly e) {
+ fail("Not expecting changed-unexpectedly throw");
+ } catch (BatchingUploader.LastModifiedDidNotChange e) {
+ assertTrue("Expected-to-change-but-did-not-change didn't throw", true);
+ }
+ assertEquals(batchMeta.getLastModified(), Long.valueOf(333));
+ }
+
+ @Test
+ public void testSetToken() {
+ assertNull(batchMeta.getToken());
+
+ try {
+ batchMeta.setToken("MTIzNA", false);
+ } catch (BatchingUploader.TokenModifiedException e) {
+ fail("Should be able to set token for the first time");
+ }
+ assertEquals("MTIzNA", batchMeta.getToken());
+
+ try {
+ batchMeta.setToken("XYCvNA", false);
+ } catch (BatchingUploader.TokenModifiedException e) {
+ assertTrue("Should not be able to modify a token", true);
+ }
+ assertEquals("MTIzNA", batchMeta.getToken());
+
+ try {
+ batchMeta.setToken("XYCvNA", true);
+ } catch (BatchingUploader.TokenModifiedException e) {
+ assertTrue("Should catch non-null tokens during onCommit sets", true);
+ }
+ assertEquals("MTIzNA", batchMeta.getToken());
+
+ try {
+ batchMeta.setToken(null, true);
+ } catch (BatchingUploader.TokenModifiedException e) {
+ fail("Should be able to set token to null during onCommit set");
+ }
+ assertNull(batchMeta.getToken());
+ }
+
+ @Test
+ public void testRecordSucceeded() {
+ assertTrue(batchMeta.getSuccessRecordGuids().isEmpty());
+
+ batchMeta.recordSucceeded("guid1");
+
+ assertTrue(batchMeta.getSuccessRecordGuids().size() == 1);
+ assertTrue(batchMeta.getSuccessRecordGuids().contains("guid1"));
+
+ try {
+ batchMeta.recordSucceeded(null);
+ fail();
+ } catch (IllegalStateException e) {
+ assertTrue("Should not be able to 'succeed' a null guid", true);
+ }
+ }
+
+ @Test
+ public void testByteLimits() {
+ assertTrue(batchMeta.canFit(0));
+
+ // Should just fit
+ assertTrue(batchMeta.canFit(byteLimit - BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT));
+
+ // Can't fit a record due to payload overhead.
+ assertFalse(batchMeta.canFit(byteLimit));
+
+ assertFalse(batchMeta.canFit(byteLimit + BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT));
+ assertFalse(batchMeta.canFit(byteLimit * 1000));
+
+ long recordDelta = byteLimit / 2;
+ assertFalse(batchMeta.addAndEstimateIfFull(recordDelta));
+
+ // Record delta shouldn't fit due to payload overhead.
+ assertFalse(batchMeta.canFit(recordDelta));
+ }
+
+ @Test
+ public void testCountLimits() {
+ // Our record limit is 5, let's add 4.
+ assertFalse(batchMeta.addAndEstimateIfFull(1));
+ assertFalse(batchMeta.addAndEstimateIfFull(1));
+ assertFalse(batchMeta.addAndEstimateIfFull(1));
+ assertFalse(batchMeta.addAndEstimateIfFull(1));
+
+ // 5th record still fits in
+ assertTrue(batchMeta.canFit(1));
+
+ // Add the 5th record
+ assertTrue(batchMeta.addAndEstimateIfFull(1));
+
+ // 6th record won't fit
+ assertFalse(batchMeta.canFit(1));
+ }
+
+ @Test
+ public void testNeedCommit() {
+ assertFalse(batchMeta.needToCommit());
+
+ assertFalse(batchMeta.addAndEstimateIfFull(1));
+
+ assertTrue(batchMeta.needToCommit());
+
+ assertFalse(batchMeta.addAndEstimateIfFull(1));
+ assertFalse(batchMeta.addAndEstimateIfFull(1));
+ assertFalse(batchMeta.addAndEstimateIfFull(1));
+
+ assertTrue(batchMeta.needToCommit());
+
+ batchMeta.reset();
+
+ assertFalse(batchMeta.needToCommit());
+ }
+
+ @Test
+ public void testAdd() {
+ // Ensure we account for payload overhead twice when the batch is empty.
+ // Payload overhead is either RECORDS_START or RECORDS_END, and for an empty payload
+ // we need both.
+ assertTrue(batchMeta.getByteCount() == 2 * BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT);
+ assertTrue(batchMeta.getRecordCount() == 0);
+
+ assertFalse(batchMeta.addAndEstimateIfFull(1));
+
+ assertTrue(batchMeta.getByteCount() == (1 + BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT));
+ assertTrue(batchMeta.getRecordCount() == 1);
+
+ assertFalse(batchMeta.addAndEstimateIfFull(1));
+ assertFalse(batchMeta.addAndEstimateIfFull(1));
+ assertFalse(batchMeta.addAndEstimateIfFull(1));
+
+ assertTrue(batchMeta.getByteCount() == (4 + BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT));
+ assertTrue(batchMeta.getRecordCount() == 4);
+
+ assertTrue(batchMeta.addAndEstimateIfFull(1));
+
+ try {
+ assertTrue(batchMeta.addAndEstimateIfFull(1));
+ fail("BatchMeta should not let us insert records that won't fit");
+ } catch (IllegalStateException e) {
+ assertTrue(true);
+ }
+ }
+
+ @Test
+ public void testReset() {
+ assertTrue(batchMeta.getByteCount() == 2 * BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT);
+ assertTrue(batchMeta.getRecordCount() == 0);
+ assertTrue(batchMeta.getSuccessRecordGuids().isEmpty());
+
+ // Shouldn't throw even if already empty
+ batchMeta.reset();
+ assertTrue(batchMeta.getByteCount() == 2 * BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT);
+ assertTrue(batchMeta.getRecordCount() == 0);
+ assertTrue(batchMeta.getSuccessRecordGuids().isEmpty());
+
+ assertFalse(batchMeta.addAndEstimateIfFull(1));
+ batchMeta.recordSucceeded("guid1");
+ try {
+ batchMeta.setToken("MTIzNA", false);
+ } catch (BatchingUploader.TokenModifiedException e) {}
+ try {
+ batchMeta.setLastModified(333L, true);
+ } catch (BatchingUploader.LastModifiedChangedUnexpectedly e) {
+ } catch (BatchingUploader.LastModifiedDidNotChange e) {}
+ assertEquals(Long.valueOf(333L), batchMeta.getLastModified());
+ assertEquals("MTIzNA", batchMeta.getToken());
+ assertTrue(batchMeta.getSuccessRecordGuids().size() == 1);
+
+ batchMeta.reset();
+
+ // Counts must be reset
+ assertTrue(batchMeta.getByteCount() == 2 * BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT);
+ assertTrue(batchMeta.getRecordCount() == 0);
+ assertTrue(batchMeta.getSuccessRecordGuids().isEmpty());
+
+ // Collection L-M shouldn't change
+ assertEquals(batchMeta.collectionLastModified, collectionLastModified);
+
+ // Token must be reset
+ assertNull(batchMeta.getToken());
+
+ // L-M must be reverted to collection L-M
+ assertEquals(batchMeta.getLastModified(), collectionLastModified);
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploaderTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploaderTest.java
new file mode 100644
index 0000000000..5ce94b222a
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploaderTest.java
@@ -0,0 +1,441 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.repositories.uploaders;
+
+import android.support.annotation.NonNull;
+
+import static org.junit.Assert.*;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.MockRecord;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.InfoCollections;
+import org.mozilla.gecko.sync.InfoConfiguration;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.repositories.Server11Repository;
+import org.mozilla.gecko.sync.repositories.Server11RepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
+
+import java.net.URISyntaxException;
+import java.util.Random;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+
+@RunWith(TestRunner.class)
+public class BatchingUploaderTest {
+ class MockExecutorService implements Executor {
+ public int totalPayloads = 0;
+ public int commitPayloads = 0;
+
+ @Override
+ public void execute(@NonNull Runnable command) {
+ ++totalPayloads;
+ if (((RecordUploadRunnable) command).isCommit) {
+ ++commitPayloads;
+ }
+ }
+ }
+
+ class MockStoreDelegate implements RepositorySessionStoreDelegate {
+ public int storeFailed = 0;
+ public int storeSucceeded = 0;
+ public int storeCompleted = 0;
+
+ @Override
+ public void onRecordStoreFailed(Exception ex, String recordGuid) {
+ ++storeFailed;
+ }
+
+ @Override
+ public void onRecordStoreSucceeded(String guid) {
+ ++storeSucceeded;
+ }
+
+ @Override
+ public void onStoreCompleted(long storeEnd) {
+ ++storeCompleted;
+ }
+
+ @Override
+ public RepositorySessionStoreDelegate deferredStoreDelegate(ExecutorService executor) {
+ return null;
+ }
+ }
+
+ private Executor workQueue;
+ private RepositorySessionStoreDelegate storeDelegate;
+
+ @Before
+ public void setUp() throws Exception {
+ workQueue = new MockExecutorService();
+ storeDelegate = new MockStoreDelegate();
+ }
+
+ @Test
+ public void testProcessEvenPayloadBatch() {
+ BatchingUploader uploader = makeConstrainedUploader(2, 4);
+
+ MockRecord record = new MockRecord(Utils.generateGuid(), null, 0, false);
+ // 1st
+ uploader.process(record);
+ assertEquals(0, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(0, ((MockExecutorService) workQueue).commitPayloads);
+ // 2nd -> payload full
+ uploader.process(record);
+ assertEquals(1, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(0, ((MockExecutorService) workQueue).commitPayloads);
+ // 3rd
+ uploader.process(record);
+ assertEquals(1, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(0, ((MockExecutorService) workQueue).commitPayloads);
+ // 4th -> batch & payload full
+ uploader.process(record);
+ assertEquals(2, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(1, ((MockExecutorService) workQueue).commitPayloads);
+ // 5th
+ uploader.process(record);
+ assertEquals(2, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(1, ((MockExecutorService) workQueue).commitPayloads);
+ // 6th -> payload full
+ uploader.process(record);
+ assertEquals(3, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(1, ((MockExecutorService) workQueue).commitPayloads);
+ // 7th
+ uploader.process(record);
+ assertEquals(3, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(1, ((MockExecutorService) workQueue).commitPayloads);
+ // 8th -> batch & payload full
+ uploader.process(record);
+ assertEquals(4, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(2, ((MockExecutorService) workQueue).commitPayloads);
+ // 9th
+ uploader.process(record);
+ assertEquals(4, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(2, ((MockExecutorService) workQueue).commitPayloads);
+ // 10th -> payload full
+ uploader.process(record);
+ assertEquals(5, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(2, ((MockExecutorService) workQueue).commitPayloads);
+ // 11th
+ uploader.process(record);
+ assertEquals(5, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(2, ((MockExecutorService) workQueue).commitPayloads);
+ // 12th -> batch & payload full
+ uploader.process(record);
+ assertEquals(6, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(3, ((MockExecutorService) workQueue).commitPayloads);
+ // 13th
+ uploader.process(record);
+ assertEquals(6, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(3, ((MockExecutorService) workQueue).commitPayloads);
+ }
+
+ @Test
+ public void testProcessUnevenPayloadBatch() {
+ BatchingUploader uploader = makeConstrainedUploader(2, 5);
+
+ MockRecord record = new MockRecord(Utils.generateGuid(), null, 0, false);
+ // 1st
+ uploader.process(record);
+ assertEquals(0, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(0, ((MockExecutorService) workQueue).commitPayloads);
+ // 2nd -> payload full
+ uploader.process(record);
+ assertEquals(1, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(0, ((MockExecutorService) workQueue).commitPayloads);
+ // 3rd
+ uploader.process(record);
+ assertEquals(1, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(0, ((MockExecutorService) workQueue).commitPayloads);
+ // 4th -> payload full
+ uploader.process(record);
+ assertEquals(2, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(0, ((MockExecutorService) workQueue).commitPayloads);
+ // 5th -> batch full
+ uploader.process(record);
+ assertEquals(3, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(1, ((MockExecutorService) workQueue).commitPayloads);
+ // 6th -> starts new batch
+ uploader.process(record);
+ assertEquals(3, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(1, ((MockExecutorService) workQueue).commitPayloads);
+ // 7th -> payload full
+ uploader.process(record);
+ assertEquals(4, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(1, ((MockExecutorService) workQueue).commitPayloads);
+ // 8th
+ uploader.process(record);
+ assertEquals(4, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(1, ((MockExecutorService) workQueue).commitPayloads);
+ // 9th -> payload full
+ uploader.process(record);
+ assertEquals(5, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(1, ((MockExecutorService) workQueue).commitPayloads);
+ // 10th -> batch full
+ uploader.process(record);
+ assertEquals(6, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(2, ((MockExecutorService) workQueue).commitPayloads);
+ // 11th -> starts new batch
+ uploader.process(record);
+ assertEquals(6, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(2, ((MockExecutorService) workQueue).commitPayloads);
+ }
+
+ @Test
+ public void testNonBatchingOptimization() {
+ BatchingUploader uploader = makeConstrainedUploader(2, 4);
+
+ MockRecord record = new MockRecord(Utils.generateGuid(), null, 0, false);
+ // 1st
+ uploader.process(record);
+ assertEquals(0, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(0, ((MockExecutorService) workQueue).commitPayloads);
+ // 2nd
+ uploader.process(record);
+ assertEquals(1, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(0, ((MockExecutorService) workQueue).commitPayloads);
+ // 3rd
+ uploader.process(record);
+ assertEquals(1, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(0, ((MockExecutorService) workQueue).commitPayloads);
+ // 4th
+ uploader.process(record);
+ assertEquals(2, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(1, ((MockExecutorService) workQueue).commitPayloads);
+
+ // 5th
+ uploader.process(record);
+ assertEquals(2, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(1, ((MockExecutorService) workQueue).commitPayloads);
+
+ // And now we tell uploader that batching isn't supported.
+ // It shouldn't bother with batches from now on, just payloads.
+ uploader.setInBatchingMode(false);
+
+ // 6th
+ uploader.process(record);
+ assertEquals(3, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(1, ((MockExecutorService) workQueue).commitPayloads);
+
+ // 7th
+ uploader.process(record);
+ assertEquals(3, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(1, ((MockExecutorService) workQueue).commitPayloads);
+
+ // 8th
+ uploader.process(record);
+ assertEquals(4, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(1, ((MockExecutorService) workQueue).commitPayloads);
+
+ // 9th
+ uploader.process(record);
+ assertEquals(4, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(1, ((MockExecutorService) workQueue).commitPayloads);
+
+ // 10th
+ uploader.process(record);
+ assertEquals(5, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(1, ((MockExecutorService) workQueue).commitPayloads);
+ }
+
+ @Test
+ public void testPreemtiveUploadByteCounts() {
+ // While processing a record, if we know for sure that another one won't fit,
+ // we upload the payload.
+ BatchingUploader uploader = makeConstrainedUploader(3, 6);
+
+ // Payload byte max: 1024; batch byte max: 4096
+ MockRecord record = new MockRecord(Utils.generateGuid(), null, 0, false, 400);
+
+ uploader.process(record);
+ assertEquals(0, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(0, ((MockExecutorService) workQueue).commitPayloads);
+
+ // After 2nd record, byte count is at 800+overhead. Our payload max is 1024, so it's unlikely
+ // we can fit another record at this pace. Expect payload to be uploaded.
+ uploader.process(record);
+ assertEquals(1, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(0, ((MockExecutorService) workQueue).commitPayloads);
+
+ // After this record, we'll have less than 124 bytes of room left in the payload. Expect upload.
+ record = new MockRecord(Utils.generateGuid(), null, 0, false, 970);
+ uploader.process(record);
+ assertEquals(2, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(0, ((MockExecutorService) workQueue).commitPayloads);
+
+ uploader.process(record);
+ assertEquals(3, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(0, ((MockExecutorService) workQueue).commitPayloads);
+
+ // At this point our byte count for the batch is at 3600+overhead;
+ // since we have just 496 bytes left in the batch, it's unlikely we'll fit another record.
+ // Expect a batch commit
+ uploader.process(record);
+ assertEquals(4, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(1, ((MockExecutorService) workQueue).commitPayloads);
+ }
+
+ @Test
+ public void testRandomPayloadSizesBatching() {
+ BatchingUploader uploader = makeConstrainedUploader(2, 4);
+
+ final Random random = new Random();
+ for (int i = 0; i < 15000; i++) {
+ uploader.process(new MockRecord(Utils.generateGuid(), null, 0, false, random.nextInt(15000)));
+ }
+ }
+
+ @Test
+ public void testRandomPayloadSizesNonBatching() {
+ BatchingUploader uploader = makeConstrainedUploader(2, 4);
+
+ final Random random = new Random();
+ uploader.setInBatchingMode(false);
+ for (int i = 0; i < 15000; i++) {
+ uploader.process(new MockRecord(Utils.generateGuid(), null, 0, false, random.nextInt(15000)));
+ }
+ }
+
+ @Test
+ public void testRandomPayloadSizesNonBatchingDelayed() {
+ BatchingUploader uploader = makeConstrainedUploader(2, 4);
+
+ final Random random = new Random();
+ // Delay telling uploader that batching isn't supported.
+ // Randomize how many records we wait for.
+ final int delay = random.nextInt(20);
+ for (int i = 0; i < 15000; i++) {
+ if (delay == i) {
+ uploader.setInBatchingMode(false);
+ }
+ uploader.process(new MockRecord(Utils.generateGuid(), null, 0, false, random.nextInt(15000)));
+ }
+ }
+
+ @Test
+ public void testNoMoreRecordsAfterPayloadPost() {
+ BatchingUploader uploader = makeConstrainedUploader(2, 4);
+
+ // Process two records (payload limit is also two, batch is four),
+ // and ensure that 'no more records' commits.
+ MockRecord record = new MockRecord(Utils.generateGuid(), null, 0, false);
+ uploader.process(record);
+ uploader.process(record);
+ uploader.setInBatchingMode(true);
+ uploader.commitIfNecessaryAfterLastPayload();
+ // One will be a payload post, the other one is batch commit (empty payload)
+ assertEquals(2, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(1, ((MockExecutorService) workQueue).commitPayloads);
+ }
+
+ @Test
+ public void testNoMoreRecordsAfterPayloadPostWithOneRecordLeft() {
+ BatchingUploader uploader = makeConstrainedUploader(2, 4);
+
+ // Process two records (payload limit is also two, batch is four),
+ // and ensure that 'no more records' commits.
+ MockRecord record = new MockRecord(Utils.generateGuid(), null, 0, false);
+ uploader.process(record);
+ uploader.process(record);
+ uploader.process(record);
+ uploader.commitIfNecessaryAfterLastPayload();
+ // One will be a payload post, the other one is batch commit (one record payload)
+ assertEquals(2, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(1, ((MockExecutorService) workQueue).commitPayloads);
+ }
+
+ @Test
+ public void testNoMoreRecordsNoOp() {
+ BatchingUploader uploader = makeConstrainedUploader(2, 4);
+
+ uploader.commitIfNecessaryAfterLastPayload();
+ assertEquals(0, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(0, ((MockExecutorService) workQueue).commitPayloads);
+ }
+
+ @Test
+ public void testNoMoreRecordsNoOpAfterCommit() {
+ BatchingUploader uploader = makeConstrainedUploader(2, 4);
+
+ MockRecord record = new MockRecord(Utils.generateGuid(), null, 0, false);
+ uploader.process(record);
+ uploader.process(record);
+ uploader.process(record);
+ uploader.process(record);
+ assertEquals(2, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(1, ((MockExecutorService) workQueue).commitPayloads);
+
+ uploader.commitIfNecessaryAfterLastPayload();
+ assertEquals(2, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(1, ((MockExecutorService) workQueue).commitPayloads);
+ }
+
+ @Test
+ public void testNoMoreRecordsEvenNonBatching() {
+ BatchingUploader uploader = makeConstrainedUploader(2, 4);
+
+ // Process two records (payload limit is also two, batch is four),
+ // set non-batching mode, and ensure that 'no more records' doesn't commit.
+ MockRecord record = new MockRecord(Utils.generateGuid(), null, 0, false);
+ uploader.process(record);
+ uploader.process(record);
+ uploader.setInBatchingMode(false);
+ uploader.commitIfNecessaryAfterLastPayload();
+ // One will be a payload post, the other one is batch commit (one record payload)
+ assertEquals(1, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(0, ((MockExecutorService) workQueue).commitPayloads);
+ }
+
+ @Test
+ public void testNoMoreRecordsIncompletePayload() {
+ BatchingUploader uploader = makeConstrainedUploader(2, 4);
+
+ // We have one record (payload limit is 2), and "no-more-records" signal should commit it.
+ MockRecord record = new MockRecord(Utils.generateGuid(), null, 0, false);
+ uploader.process(record);
+
+ uploader.commitIfNecessaryAfterLastPayload();
+ assertEquals(1, ((MockExecutorService) workQueue).totalPayloads);
+ assertEquals(1, ((MockExecutorService) workQueue).commitPayloads);
+ }
+
+ private BatchingUploader makeConstrainedUploader(long maxPostRecords, long maxTotalRecords) {
+ Server11RepositorySession server11RepositorySession = new Server11RepositorySession(
+ makeCountConstrainedRepository(maxPostRecords, maxTotalRecords)
+ );
+ server11RepositorySession.setStoreDelegate(storeDelegate);
+ return new BatchingUploader(server11RepositorySession, workQueue, storeDelegate);
+ }
+
+ private Server11Repository makeCountConstrainedRepository(long maxPostRecords, long maxTotalRecords) {
+ return makeConstrainedRepository(1024, 1024, maxPostRecords, 4096, maxTotalRecords);
+ }
+
+ private Server11Repository makeConstrainedRepository(long maxRequestBytes, long maxPostBytes, long maxPostRecords, long maxTotalBytes, long maxTotalRecords) {
+ ExtendedJSONObject infoConfigurationJSON = new ExtendedJSONObject();
+ infoConfigurationJSON.put(InfoConfiguration.MAX_TOTAL_BYTES, maxTotalBytes);
+ infoConfigurationJSON.put(InfoConfiguration.MAX_TOTAL_RECORDS, maxTotalRecords);
+ infoConfigurationJSON.put(InfoConfiguration.MAX_POST_RECORDS, maxPostRecords);
+ infoConfigurationJSON.put(InfoConfiguration.MAX_POST_BYTES, maxPostBytes);
+ infoConfigurationJSON.put(InfoConfiguration.MAX_REQUEST_BYTES, maxRequestBytes);
+
+ InfoConfiguration infoConfiguration = new InfoConfiguration(infoConfigurationJSON);
+
+ try {
+ return new Server11Repository(
+ "dummyCollection",
+ "http://dummy.url/",
+ null,
+ new InfoCollections(),
+ infoConfiguration
+ );
+ } catch (URISyntaxException e) {
+ // Won't throw, and this won't happen.
+ return null;
+ }
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/PayloadTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/PayloadTest.java
new file mode 100644
index 0000000000..b1d6dd9d08
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/PayloadTest.java
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.repositories.uploaders;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+import static org.junit.Assert.*;
+
+@RunWith(TestRunner.class)
+public class PayloadTest {
+ private Payload payload;
+ private long byteLimit = 1024;
+ private long recordLimit = 5;
+ private Object lock = new Object();
+
+ @Before
+ public void setUp() throws Exception {
+ payload = new Payload(lock, byteLimit, recordLimit);
+ }
+
+ @Test
+ public void testByteLimits() {
+ assertTrue(payload.canFit(0));
+
+ // Should just fit
+ assertTrue(payload.canFit(byteLimit - BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT));
+
+ // Can't fit a record due to payload overhead.
+ assertFalse(payload.canFit(byteLimit));
+
+ assertFalse(payload.canFit(byteLimit + BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT));
+ assertFalse(payload.canFit(byteLimit * 1000));
+
+ long recordDelta = byteLimit / 2;
+ assertFalse(payload.addAndEstimateIfFull(recordDelta, new byte[0], null));
+
+ // Record delta shouldn't fit due to payload overhead.
+ assertFalse(payload.canFit(recordDelta));
+ }
+
+ @Test
+ public void testCountLimits() {
+ byte[] bytes = new byte[0];
+
+ // Our record limit is 5, let's add 4.
+ assertFalse(payload.addAndEstimateIfFull(1, bytes, null));
+ assertFalse(payload.addAndEstimateIfFull(1, bytes, null));
+ assertFalse(payload.addAndEstimateIfFull(1, bytes, null));
+ assertFalse(payload.addAndEstimateIfFull(1, bytes, null));
+
+ // 5th record still fits in
+ assertTrue(payload.canFit(1));
+
+ // Add the 5th record
+ assertTrue(payload.addAndEstimateIfFull(1, bytes, null));
+
+ // 6th record won't fit
+ assertFalse(payload.canFit(1));
+ }
+
+ @Test
+ public void testAdd() {
+ assertTrue(payload.getByteCount() == 2 * BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT);
+ assertTrue(payload.getRecordCount() == 0);
+ assertTrue(payload.isEmpty());
+ assertTrue(payload.getRecordsBuffer().isEmpty());
+ assertTrue(payload.getRecordGuidsBuffer().isEmpty());
+
+ try {
+ payload.addAndEstimateIfFull(1024);
+ fail("Simple add is not supported");
+ } catch (UnsupportedOperationException e) {
+ assertTrue(true);
+ }
+
+ byte[] recordBytes1 = new byte[100];
+ assertFalse(payload.addAndEstimateIfFull(1, recordBytes1, "guid1"));
+
+ assertTrue(payload.getRecordsBuffer().size() == 1);
+ assertTrue(payload.getRecordGuidsBuffer().size() == 1);
+ assertTrue(payload.getRecordGuidsBuffer().contains("guid1"));
+ assertTrue(payload.getRecordsBuffer().contains(recordBytes1));
+
+ assertTrue(payload.getByteCount() == (1 + BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT));
+ assertTrue(payload.getRecordCount() == 1);
+
+ assertFalse(payload.isEmpty());
+
+ assertFalse(payload.addAndEstimateIfFull(1, recordBytes1, "guid2"));
+ assertFalse(payload.addAndEstimateIfFull(1, recordBytes1, "guid3"));
+ assertFalse(payload.addAndEstimateIfFull(1, recordBytes1, "guid4"));
+
+ assertTrue(payload.getByteCount() == (4 + BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT));
+ assertTrue(payload.getRecordCount() == 4);
+
+ assertTrue(payload.addAndEstimateIfFull(1, recordBytes1, "guid5"));
+
+ try {
+ assertTrue(payload.addAndEstimateIfFull(1, recordBytes1, "guid6"));
+ fail("Payload should not let us insert records that won't fit");
+ } catch (IllegalStateException e) {
+ assertTrue(true);
+ }
+ }
+
+ @Test
+ public void testReset() {
+ assertTrue(payload.getByteCount() == 2 * BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT);
+ assertTrue(payload.getRecordCount() == 0);
+ assertTrue(payload.getRecordsBuffer().isEmpty());
+ assertTrue(payload.getRecordGuidsBuffer().isEmpty());
+ assertTrue(payload.isEmpty());
+
+ // Shouldn't throw even if already empty
+ payload.reset();
+ assertTrue(payload.getByteCount() == 2 * BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT);
+ assertTrue(payload.getRecordCount() == 0);
+ assertTrue(payload.getRecordsBuffer().isEmpty());
+ assertTrue(payload.getRecordGuidsBuffer().isEmpty());
+ assertTrue(payload.isEmpty());
+
+ byte[] recordBytes1 = new byte[100];
+ assertFalse(payload.addAndEstimateIfFull(1, recordBytes1, "guid1"));
+ assertFalse(payload.isEmpty());
+ payload.reset();
+
+ assertTrue(payload.getByteCount() == 2 * BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT);
+ assertTrue(payload.getRecordCount() == 0);
+ assertTrue(payload.getRecordsBuffer().isEmpty());
+ assertTrue(payload.getRecordGuidsBuffer().isEmpty());
+ assertTrue(payload.isEmpty());
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegateTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegateTest.java
new file mode 100644
index 0000000000..fc43c2f5e5
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegateTest.java
@@ -0,0 +1,404 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.repositories.uploaders;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.HTTPFailureException;
+import org.mozilla.gecko.sync.InfoCollections;
+import org.mozilla.gecko.sync.InfoConfiguration;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.net.SyncResponse;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.sync.repositories.Server11Repository;
+import org.mozilla.gecko.sync.repositories.Server11RepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
+
+import java.io.ByteArrayInputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.concurrent.Executor;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.entity.BasicHttpEntity;
+import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
+import ch.boye.httpclientandroidlib.message.BasicStatusLine;
+
+import static org.junit.Assert.*;
+
+@RunWith(TestRunner.class)
+public class PayloadUploadDelegateTest {
+ private BatchingUploader batchingUploader;
+
+ class MockUploader extends BatchingUploader {
+ public final ArrayList<String> successRecords = new ArrayList<>();
+ public final HashMap<String, Exception> failedRecords = new HashMap<>();
+ public boolean didLastPayloadFail = false;
+
+ public ArrayList<SyncStorageResponse> successResponses = new ArrayList<>();
+ public int commitPayloadsSucceeded = 0;
+ public int lastPayloadsSucceeded = 0;
+
+ public MockUploader(final Server11RepositorySession repositorySession, final Executor workQueue, final RepositorySessionStoreDelegate sessionStoreDelegate) {
+ super(repositorySession, workQueue, sessionStoreDelegate);
+ }
+
+ @Override
+ public void payloadSucceeded(final SyncStorageResponse response, final boolean isCommit, final boolean isLastPayload) {
+ successResponses.add(response);
+ if (isCommit) {
+ ++commitPayloadsSucceeded;
+ }
+ if (isLastPayload) {
+ ++lastPayloadsSucceeded;
+ }
+ }
+
+ @Override
+ public void recordSucceeded(final String recordGuid) {
+ successRecords.add(recordGuid);
+ }
+
+ @Override
+ public void recordFailed(final String recordGuid) {
+ recordFailed(new Exception(), recordGuid);
+ }
+
+ @Override
+ public void recordFailed(final Exception e, final String recordGuid) {
+ failedRecords.put(recordGuid, e);
+ }
+
+ @Override
+ public void lastPayloadFailed() {
+ didLastPayloadFail = true;
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ Server11Repository server11Repository = new Server11Repository(
+ "dummyCollection",
+ "http://dummy.url/",
+ null,
+ new InfoCollections(),
+ new InfoConfiguration()
+ );
+ batchingUploader = new MockUploader(
+ new Server11RepositorySession(server11Repository),
+ null,
+ null
+ );
+ }
+
+ @Test
+ public void testHandleRequestSuccessNonSuccess() {
+ ArrayList<String> postedGuids = new ArrayList<>(2);
+ postedGuids.add("testGuid1");
+ postedGuids.add("testGuid2");
+ PayloadUploadDelegate payloadUploadDelegate = new PayloadUploadDelegate(
+ batchingUploader, postedGuids, false, false);
+
+ // Test that non-2* responses aren't processed
+ payloadUploadDelegate.handleRequestSuccess(makeSyncStorageResponse(404, null, null));
+ assertEquals(2, ((MockUploader) batchingUploader).failedRecords.size());
+ assertFalse(((MockUploader) batchingUploader).didLastPayloadFail);
+ assertEquals(IllegalStateException.class,
+ ((MockUploader) batchingUploader).failedRecords.get("testGuid1").getClass());
+ assertEquals(IllegalStateException.class,
+ ((MockUploader) batchingUploader).failedRecords.get("testGuid2").getClass());
+ }
+
+ @Test
+ public void testHandleRequestSuccessNoHeaders() {
+ ArrayList<String> postedGuids = new ArrayList<>(2);
+ postedGuids.add("testGuid1");
+ postedGuids.add("testGuid2");
+ PayloadUploadDelegate payloadUploadDelegate = new PayloadUploadDelegate(
+ batchingUploader, postedGuids, false, false);
+
+ // Test that responses without X-Last-Modified header aren't processed
+ payloadUploadDelegate.handleRequestSuccess(makeSyncStorageResponse(200, null, null));
+ assertEquals(2, ((MockUploader) batchingUploader).failedRecords.size());
+ assertFalse(((MockUploader) batchingUploader).didLastPayloadFail);
+ assertEquals(IllegalStateException.class,
+ ((MockUploader) batchingUploader).failedRecords.get("testGuid1").getClass());
+ assertEquals(IllegalStateException.class,
+ ((MockUploader) batchingUploader).failedRecords.get("testGuid2").getClass());
+ }
+
+ @Test
+ public void testHandleRequestSuccessBadBody() {
+ ArrayList<String> postedGuids = new ArrayList<>(2);
+ postedGuids.add("testGuid1");
+ postedGuids.add("testGuid2");
+ PayloadUploadDelegate payloadUploadDelegate = new PayloadUploadDelegate(
+ batchingUploader, postedGuids, false, true);
+
+ // Test that we catch json processing errors
+ payloadUploadDelegate.handleRequestSuccess(makeSyncStorageResponse(200, "non json body", "123"));
+ assertEquals(2, ((MockUploader) batchingUploader).failedRecords.size());
+ assertTrue(((MockUploader) batchingUploader).didLastPayloadFail);
+ assertEquals(NonObjectJSONException.class,
+ ((MockUploader) batchingUploader).failedRecords.get("testGuid1").getClass());
+ assertEquals(NonObjectJSONException.class,
+ ((MockUploader) batchingUploader).failedRecords.get("testGuid2").getClass());
+ }
+
+ @Test
+ public void testHandleRequestSuccess202NoToken() {
+ ArrayList<String> postedGuids = new ArrayList<>(1);
+ postedGuids.add("testGuid1");
+ PayloadUploadDelegate payloadUploadDelegate = new PayloadUploadDelegate(
+ batchingUploader, postedGuids, false, true);
+
+ // Test that we catch absent tokens in 202 responses
+ payloadUploadDelegate.handleRequestSuccess(makeSyncStorageResponse(202, "{\"success\": []}", "123"));
+ assertEquals(1, ((MockUploader) batchingUploader).failedRecords.size());
+ assertEquals(IllegalStateException.class,
+ ((MockUploader) batchingUploader).failedRecords.get("testGuid1").getClass());
+ }
+
+ @Test
+ public void testHandleRequestSuccessBad200() {
+ ArrayList<String> postedGuids = new ArrayList<>(1);
+ postedGuids.add("testGuid1");
+
+ PayloadUploadDelegate payloadUploadDelegate = new PayloadUploadDelegate(
+ batchingUploader, postedGuids, false, false);
+
+ // Test that if in batching mode and saw the token, 200 must be a response to a commit
+ try {
+ batchingUploader.getCurrentBatch().setToken("MTIzNA", true);
+ } catch (BatchingUploader.BatchingUploaderException e) {}
+ batchingUploader.setInBatchingMode(true);
+
+ // not a commit, so should fail
+ payloadUploadDelegate.handleRequestSuccess(makeSyncStorageResponse(200, "{\"success\": []}", "123"));
+ assertEquals(1, ((MockUploader) batchingUploader).failedRecords.size());
+ assertEquals(IllegalStateException.class,
+ ((MockUploader) batchingUploader).failedRecords.get("testGuid1").getClass());
+ }
+
+ @Test
+ public void testHandleRequestSuccessNonBatchingFailedLM() {
+ ArrayList<String> postedGuids = new ArrayList<>(1);
+ postedGuids.add("guid1");
+ postedGuids.add("guid2");
+ postedGuids.add("guid3");
+ PayloadUploadDelegate payloadUploadDelegate = new PayloadUploadDelegate(
+ batchingUploader, postedGuids, false, false);
+
+ payloadUploadDelegate.handleRequestSuccess(
+ makeSyncStorageResponse(200, "{\"success\": [\"guid1\", \"guid2\", \"guid3\"]}", "123"));
+ assertEquals(0, ((MockUploader) batchingUploader).failedRecords.size());
+ assertEquals(3, ((MockUploader) batchingUploader).successRecords.size());
+ assertFalse(((MockUploader) batchingUploader).didLastPayloadFail);
+ assertEquals(1, ((MockUploader) batchingUploader).successResponses.size());
+ assertEquals(0, ((MockUploader) batchingUploader).commitPayloadsSucceeded);
+ assertEquals(0, ((MockUploader) batchingUploader).lastPayloadsSucceeded);
+
+ // These should fail, because we're returning a non-changed L-M in a non-batching mode
+ postedGuids.add("guid4");
+ postedGuids.add("guid6");
+ payloadUploadDelegate = new PayloadUploadDelegate(
+ batchingUploader, postedGuids, false, false);
+ payloadUploadDelegate.handleRequestSuccess(
+ makeSyncStorageResponse(200, "{\"success\": [\"guid4\", 5, \"guid6\"]}", "123"));
+ assertEquals(5, ((MockUploader) batchingUploader).failedRecords.size());
+ assertEquals(3, ((MockUploader) batchingUploader).successRecords.size());
+ assertFalse(((MockUploader) batchingUploader).didLastPayloadFail);
+ assertEquals(1, ((MockUploader) batchingUploader).successResponses.size());
+ assertEquals(0, ((MockUploader) batchingUploader).commitPayloadsSucceeded);
+ assertEquals(0, ((MockUploader) batchingUploader).lastPayloadsSucceeded);
+ assertEquals(BatchingUploader.LastModifiedDidNotChange.class,
+ ((MockUploader) batchingUploader).failedRecords.get("guid4").getClass());
+ }
+
+ @Test
+ public void testHandleRequestSuccessNonBatching() {
+ ArrayList<String> postedGuids = new ArrayList<>();
+ postedGuids.add("guid1");
+ postedGuids.add("guid2");
+ postedGuids.add("guid3");
+ PayloadUploadDelegate payloadUploadDelegate = new PayloadUploadDelegate(
+ batchingUploader, postedGuids, false, false);
+ payloadUploadDelegate.handleRequestSuccess(
+ makeSyncStorageResponse(200, "{\"success\": [\"guid1\", \"guid2\", \"guid3\"], \"failed\": {}}", "123"));
+
+ postedGuids = new ArrayList<>();
+ postedGuids.add("guid4");
+ postedGuids.add("guid5");
+ payloadUploadDelegate = new PayloadUploadDelegate(
+ batchingUploader, postedGuids, false, false);
+ payloadUploadDelegate.handleRequestSuccess(
+ makeSyncStorageResponse(200, "{\"success\": [\"guid4\", \"guid5\"], \"failed\": {}}", "333"));
+
+ postedGuids = new ArrayList<>();
+ postedGuids.add("guid6");
+ payloadUploadDelegate = new PayloadUploadDelegate(
+ batchingUploader, postedGuids, false, true);
+ payloadUploadDelegate.handleRequestSuccess(
+ makeSyncStorageResponse(200, "{\"success\": [\"guid6\"], \"failed\": {}}", "444"));
+
+ assertEquals(0, ((MockUploader) batchingUploader).failedRecords.size());
+ assertEquals(6, ((MockUploader) batchingUploader).successRecords.size());
+ assertFalse(((MockUploader) batchingUploader).didLastPayloadFail);
+ assertEquals(3, ((MockUploader) batchingUploader).successResponses.size());
+ assertEquals(0, ((MockUploader) batchingUploader).commitPayloadsSucceeded);
+ assertEquals(1, ((MockUploader) batchingUploader).lastPayloadsSucceeded);
+ assertFalse(batchingUploader.getInBatchingMode());
+
+ postedGuids = new ArrayList<>();
+ postedGuids.add("guid7");
+ postedGuids.add("guid8");
+ payloadUploadDelegate = new PayloadUploadDelegate(
+ batchingUploader, postedGuids, false, true);
+ payloadUploadDelegate.handleRequestSuccess(
+ makeSyncStorageResponse(200, "{\"success\": [\"guid8\"], \"failed\": {\"guid7\": \"reason\"}}", "555"));
+ assertEquals(1, ((MockUploader) batchingUploader).failedRecords.size());
+ assertTrue(((MockUploader) batchingUploader).failedRecords.containsKey("guid7"));
+ assertEquals(7, ((MockUploader) batchingUploader).successRecords.size());
+ assertFalse(((MockUploader) batchingUploader).didLastPayloadFail);
+ assertEquals(4, ((MockUploader) batchingUploader).successResponses.size());
+ assertEquals(0, ((MockUploader) batchingUploader).commitPayloadsSucceeded);
+ assertEquals(2, ((MockUploader) batchingUploader).lastPayloadsSucceeded);
+ assertFalse(batchingUploader.getInBatchingMode());
+ }
+
+ @Test
+ public void testHandleRequestSuccessBatching() {
+ ArrayList<String> postedGuids = new ArrayList<>();
+ postedGuids.add("guid1");
+ postedGuids.add("guid2");
+ postedGuids.add("guid3");
+ PayloadUploadDelegate payloadUploadDelegate = new PayloadUploadDelegate(
+ batchingUploader, postedGuids, false, false);
+ payloadUploadDelegate.handleRequestSuccess(
+ makeSyncStorageResponse(202, "{\"batch\": \"MTIzNA\", \"success\": [\"guid1\", \"guid2\", \"guid3\"], \"failed\": {}}", "123"));
+
+ assertTrue(batchingUploader.getInBatchingMode());
+ assertEquals("MTIzNA", batchingUploader.getCurrentBatch().getToken());
+
+ postedGuids = new ArrayList<>();
+ postedGuids.add("guid4");
+ postedGuids.add("guid5");
+ postedGuids.add("guid6");
+ payloadUploadDelegate = new PayloadUploadDelegate(
+ batchingUploader, postedGuids, false, false);
+ payloadUploadDelegate.handleRequestSuccess(
+ makeSyncStorageResponse(202, "{\"batch\": \"MTIzNA\", \"success\": [\"guid4\", \"guid5\", \"guid6\"], \"failed\": {}}", "123"));
+
+ assertTrue(batchingUploader.getInBatchingMode());
+ assertEquals("MTIzNA", batchingUploader.getCurrentBatch().getToken());
+
+ postedGuids = new ArrayList<>();
+ postedGuids.add("guid7");
+ payloadUploadDelegate = new PayloadUploadDelegate(
+ batchingUploader, postedGuids, true, false);
+ payloadUploadDelegate.handleRequestSuccess(
+ makeSyncStorageResponse(200, "{\"success\": [\"guid6\"], \"failed\": {}}", "222"));
+
+ // Even though everything indicates we're not in a batching, we were, so test that
+ // we don't reset the flag.
+ assertTrue(batchingUploader.getInBatchingMode());
+ assertNull(batchingUploader.getCurrentBatch().getToken());
+
+ postedGuids = new ArrayList<>();
+ postedGuids.add("guid8");
+ payloadUploadDelegate = new PayloadUploadDelegate(
+ batchingUploader, postedGuids, true, true);
+ payloadUploadDelegate.handleRequestSuccess(
+ makeSyncStorageResponse(200, "{\"success\": [\"guid7\"], \"failed\": {}}", "333"));
+
+ assertEquals(0, ((MockUploader) batchingUploader).failedRecords.size());
+ assertEquals(8, ((MockUploader) batchingUploader).successRecords.size());
+ assertFalse(((MockUploader) batchingUploader).didLastPayloadFail);
+ assertEquals(4, ((MockUploader) batchingUploader).successResponses.size());
+ assertEquals(2, ((MockUploader) batchingUploader).commitPayloadsSucceeded);
+ assertEquals(1, ((MockUploader) batchingUploader).lastPayloadsSucceeded);
+ assertTrue(batchingUploader.getInBatchingMode());
+ }
+
+ @Test
+ public void testHandleRequestError() {
+ ArrayList<String> postedGuids = new ArrayList<>(3);
+ postedGuids.add("testGuid1");
+ postedGuids.add("testGuid2");
+ postedGuids.add("testGuid3");
+ PayloadUploadDelegate payloadUploadDelegate = new PayloadUploadDelegate(batchingUploader, postedGuids, false, false);
+
+ IllegalStateException e = new IllegalStateException();
+ payloadUploadDelegate.handleRequestError(e);
+
+ assertEquals(3, ((MockUploader) batchingUploader).failedRecords.size());
+ assertEquals(e, ((MockUploader) batchingUploader).failedRecords.get("testGuid1"));
+ assertEquals(e, ((MockUploader) batchingUploader).failedRecords.get("testGuid2"));
+ assertEquals(e, ((MockUploader) batchingUploader).failedRecords.get("testGuid3"));
+ assertFalse(((MockUploader) batchingUploader).didLastPayloadFail);
+
+ payloadUploadDelegate = new PayloadUploadDelegate(batchingUploader, postedGuids, false, true);
+ payloadUploadDelegate.handleRequestError(e);
+ assertEquals(3, ((MockUploader) batchingUploader).failedRecords.size());
+ assertTrue(((MockUploader) batchingUploader).didLastPayloadFail);
+ }
+
+ @Test
+ public void testHandleRequestFailure() {
+ ArrayList<String> postedGuids = new ArrayList<>(3);
+ postedGuids.add("testGuid1");
+ postedGuids.add("testGuid2");
+ postedGuids.add("testGuid3");
+ PayloadUploadDelegate payloadUploadDelegate = new PayloadUploadDelegate(batchingUploader, postedGuids, false, false);
+
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 503, "Illegal method/protocol"));
+ payloadUploadDelegate.handleRequestFailure(new SyncStorageResponse(response));
+ assertEquals(3, ((MockUploader) batchingUploader).failedRecords.size());
+ assertEquals(HTTPFailureException.class,
+ ((MockUploader) batchingUploader).failedRecords.get("testGuid1").getClass());
+ assertEquals(HTTPFailureException.class,
+ ((MockUploader) batchingUploader).failedRecords.get("testGuid2").getClass());
+ assertEquals(HTTPFailureException.class,
+ ((MockUploader) batchingUploader).failedRecords.get("testGuid3").getClass());
+
+ payloadUploadDelegate = new PayloadUploadDelegate(batchingUploader, postedGuids, false, true);
+ payloadUploadDelegate.handleRequestFailure(new SyncStorageResponse(response));
+ assertEquals(3, ((MockUploader) batchingUploader).failedRecords.size());
+ assertTrue(((MockUploader) batchingUploader).didLastPayloadFail);
+ }
+
+ @Test
+ public void testIfUnmodifiedSince() {
+ PayloadUploadDelegate payloadUploadDelegate = new PayloadUploadDelegate(
+ batchingUploader, new ArrayList<String>(), false, false);
+
+ assertNull(payloadUploadDelegate.ifUnmodifiedSince());
+
+ try {
+ batchingUploader.getCurrentBatch().setLastModified(1471645412480L, true);
+ } catch (BatchingUploader.BatchingUploaderException e) {}
+
+ assertEquals("1471645412.480", payloadUploadDelegate.ifUnmodifiedSince());
+ }
+
+ private SyncStorageResponse makeSyncStorageResponse(int code, String body, String lastModified) {
+ BasicHttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), code, null));
+
+ if (body != null) {
+ BasicHttpEntity entity = new BasicHttpEntity();
+ entity.setContent(new ByteArrayInputStream(body.getBytes()));
+ response.setEntity(entity);
+ }
+
+ if (lastModified != null) {
+ response.addHeader(SyncResponse.X_LAST_MODIFIED, lastModified);
+ }
+ return new SyncStorageResponse(response);
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/RecordUploadRunnableTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/RecordUploadRunnableTest.java
new file mode 100644
index 0000000000..269c253628
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/RecordUploadRunnableTest.java
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.repositories.uploaders;
+
+import android.net.Uri;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+import java.net.URI;
+
+import static org.junit.Assert.*;
+
+@RunWith(TestRunner.class)
+public class RecordUploadRunnableTest {
+ @Test
+ public void testBuildPostURI() throws Exception {
+ BatchMeta batchMeta = new BatchMeta(new Object(), 1, 1, null);
+ URI postURI = RecordUploadRunnable.buildPostURI(
+ false, batchMeta, Uri.parse("http://example.com/"));
+ assertEquals("http://example.com/?batch=true", postURI.toString());
+
+ postURI = RecordUploadRunnable.buildPostURI(
+ true, batchMeta, Uri.parse("http://example.com/"));
+ assertEquals("http://example.com/?batch=true&commit=true", postURI.toString());
+
+ batchMeta.setToken("MTIzNA", false);
+ postURI = RecordUploadRunnable.buildPostURI(
+ false, batchMeta, Uri.parse("http://example.com/"));
+ assertEquals("http://example.com/?batch=MTIzNA", postURI.toString());
+
+ postURI = RecordUploadRunnable.buildPostURI(
+ true, batchMeta, Uri.parse("http://example.com/"));
+ assertEquals("http://example.com/?batch=MTIzNA&commit=true", postURI.toString());
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestEnsureCrypto5KeysStage.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestEnsureCrypto5KeysStage.java
new file mode 100644
index 0000000000..cb74b427ba
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestEnsureCrypto5KeysStage.java
@@ -0,0 +1,237 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.stage.test;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
+import org.mozilla.android.sync.test.helpers.MockGlobalSessionCallback;
+import org.mozilla.android.sync.test.helpers.MockServer;
+import org.mozilla.gecko.background.testhelpers.MockGlobalSession;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.AlreadySyncingException;
+import org.mozilla.gecko.sync.CollectionKeys;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.InfoCollections;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.stage.EnsureCrypto5KeysStage;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
+import org.simpleframework.http.Request;
+import org.simpleframework.http.Response;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collection;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
+public class TestEnsureCrypto5KeysStage {
+ private int TEST_PORT = HTTPServerTestHelper.getTestPort();
+ private final String TEST_CLUSTER_URL = "http://localhost:" + TEST_PORT;
+ private final String TEST_USERNAME = "johndoe";
+ private final String TEST_PASSWORD = "password";
+ private final String TEST_SYNC_KEY = "abcdeabcdeabcdeabcdeabcdea";
+
+ private final String TEST_JSON_NO_CRYPTO =
+ "{\"history\":1.3319567131E9}";
+ private final String TEST_JSON_OLD_CRYPTO =
+ "{\"history\":1.3319567131E9,\"crypto\":1.1E9}";
+ private final String TEST_JSON_NEW_CRYPTO =
+ "{\"history\":1.3319567131E9,\"crypto\":3.1E9}";
+
+ private HTTPServerTestHelper data = new HTTPServerTestHelper();
+
+ private KeyBundle syncKeyBundle;
+ private MockGlobalSessionCallback callback;
+ private GlobalSession session;
+
+ private boolean calledResetStages;
+ private Collection<String> stagesReset;
+
+ @Before
+ public void setUp() throws Exception {
+ syncKeyBundle = new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY);
+ callback = new MockGlobalSessionCallback();
+ session = new MockGlobalSession(TEST_USERNAME, TEST_PASSWORD,
+ syncKeyBundle, callback) {
+ @Override
+ protected void prepareStages() {
+ super.prepareStages();
+ withStage(Stage.ensureKeysStage, new EnsureCrypto5KeysStage());
+ }
+
+ @Override
+ public void resetStagesByEnum(Collection<Stage> stages) {
+ calledResetStages = true;
+ stagesReset = new ArrayList<String>();
+ for (Stage stage : stages) {
+ stagesReset.add(stage.name());
+ }
+ }
+
+ @Override
+ public void resetStagesByName(Collection<String> names) {
+ calledResetStages = true;
+ stagesReset = names;
+ }
+ };
+ session.config.setClusterURL(new URI(TEST_CLUSTER_URL));
+
+ // Set info collections to not have crypto.
+ final ExtendedJSONObject noCrypto = new ExtendedJSONObject(TEST_JSON_NO_CRYPTO);
+ session.config.infoCollections = new InfoCollections(noCrypto);
+ calledResetStages = false;
+ stagesReset = null;
+ }
+
+ public void doSession(MockServer server) {
+ data.startHTTPServer(server);
+ try {
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ session.start();
+ } catch (AlreadySyncingException e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+ }
+ });
+ } finally {
+ data.stopHTTPServer();
+ }
+ }
+
+ @Test
+ public void testDownloadUsesPersisted() throws Exception {
+ session.config.infoCollections = new InfoCollections(new ExtendedJSONObject
+ (TEST_JSON_OLD_CRYPTO));
+ session.config.persistedCryptoKeys().persistLastModified(System.currentTimeMillis());
+
+ assertNull(session.config.collectionKeys);
+ final CollectionKeys keys = CollectionKeys.generateCollectionKeys();
+ keys.setDefaultKeyBundle(syncKeyBundle);
+ session.config.persistedCryptoKeys().persistKeys(keys);
+
+ MockServer server = new MockServer() {
+ public void handle(Request request, Response response) {
+ this.handle(request, response, 404, "should not be called!");
+ }
+ };
+
+ doSession(server);
+
+ assertTrue(callback.calledSuccess);
+ assertNotNull(session.config.collectionKeys);
+ assertTrue(CollectionKeys.differences(session.config.collectionKeys, keys).isEmpty());
+ }
+
+ @Test
+ public void testDownloadFetchesNew() throws Exception {
+ session.config.infoCollections = new InfoCollections(new ExtendedJSONObject(TEST_JSON_NEW_CRYPTO));
+ session.config.persistedCryptoKeys().persistLastModified(System.currentTimeMillis());
+
+ assertNull(session.config.collectionKeys);
+ final CollectionKeys keys = CollectionKeys.generateCollectionKeys();
+ keys.setDefaultKeyBundle(syncKeyBundle);
+ session.config.persistedCryptoKeys().persistKeys(keys);
+
+ MockServer server = new MockServer() {
+ public void handle(Request request, Response response) {
+ try {
+ CryptoRecord rec = keys.asCryptoRecord();
+ rec.keyBundle = syncKeyBundle;
+ rec.encrypt();
+ this.handle(request, response, 200, rec.toJSONString());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+
+ doSession(server);
+
+ assertTrue(callback.calledSuccess);
+ assertNotNull(session.config.collectionKeys);
+ assertTrue(session.config.collectionKeys.equals(keys));
+ }
+
+ /**
+ * Change the default key but keep one collection key the same. Should reset
+ * all but that one collection.
+ */
+ @Test
+ public void testDownloadResetsOnDifferentDefaultKey() throws Exception {
+ String TEST_COLLECTION = "bookmarks";
+
+ session.config.infoCollections = new InfoCollections(new ExtendedJSONObject(TEST_JSON_NEW_CRYPTO));
+ session.config.persistedCryptoKeys().persistLastModified(System.currentTimeMillis());
+
+ KeyBundle keyBundle = KeyBundle.withRandomKeys();
+ assertNull(session.config.collectionKeys);
+ final CollectionKeys keys = CollectionKeys.generateCollectionKeys();
+ keys.setKeyBundleForCollection(TEST_COLLECTION, keyBundle);
+ session.config.persistedCryptoKeys().persistKeys(keys);
+ keys.setDefaultKeyBundle(syncKeyBundle); // Change the default key bundle, but keep "bookmarks" the same.
+
+ MockServer server = new MockServer() {
+ public void handle(Request request, Response response) {
+ try {
+ CryptoRecord rec = keys.asCryptoRecord();
+ rec.keyBundle = syncKeyBundle;
+ rec.encrypt();
+ this.handle(request, response, 200, rec.toJSONString());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+
+ doSession(server);
+
+ assertTrue(calledResetStages);
+ Collection<String> allButCollection = new ArrayList<String>();
+ for (Stage stage : Stage.getNamedStages()) {
+ allButCollection.add(stage.getRepositoryName());
+ }
+ allButCollection.remove(TEST_COLLECTION);
+ assertTrue(stagesReset.containsAll(allButCollection));
+ assertTrue(allButCollection.containsAll(stagesReset));
+ assertTrue(callback.calledError);
+ }
+
+ @Test
+ public void testDownloadResetsEngineOnDifferentKey() throws Exception {
+ final String TEST_COLLECTION = "history";
+
+ session.config.infoCollections = new InfoCollections(new ExtendedJSONObject(TEST_JSON_NEW_CRYPTO));
+ session.config.persistedCryptoKeys().persistLastModified(System.currentTimeMillis());
+
+ assertNull(session.config.collectionKeys);
+ final CollectionKeys keys = CollectionKeys.generateCollectionKeys();
+ session.config.persistedCryptoKeys().persistKeys(keys);
+ keys.setKeyBundleForCollection(TEST_COLLECTION, syncKeyBundle); // Change one key bundle.
+
+ CryptoRecord rec = keys.asCryptoRecord();
+ rec.keyBundle = syncKeyBundle;
+ rec.encrypt();
+ MockServer server = new MockServer(200, rec.toJSONString());
+
+ doSession(server);
+
+ assertTrue(calledResetStages);
+ assertNotNull(stagesReset);
+ assertEquals(1, stagesReset.size());
+ assertTrue(stagesReset.contains(TEST_COLLECTION));
+ assertTrue(callback.calledError);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestFetchMetaGlobalStage.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestFetchMetaGlobalStage.java
new file mode 100644
index 0000000000..f7ed7a559b
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestFetchMetaGlobalStage.java
@@ -0,0 +1,391 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.stage.test;
+
+import org.json.simple.JSONArray;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.android.sync.net.test.TestMetaGlobal;
+import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
+import org.mozilla.android.sync.test.helpers.MockGlobalSessionCallback;
+import org.mozilla.android.sync.test.helpers.MockServer;
+import org.mozilla.gecko.background.testhelpers.MockGlobalSession;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.AlreadySyncingException;
+import org.mozilla.gecko.sync.CollectionKeys;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.InfoCollections;
+import org.mozilla.gecko.sync.MetaGlobal;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.SyncConfigurationException;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.delegates.FreshStartDelegate;
+import org.mozilla.gecko.sync.delegates.KeyUploadDelegate;
+import org.mozilla.gecko.sync.delegates.WipeServerDelegate;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.stage.FetchMetaGlobalStage;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
+import org.simpleframework.http.Request;
+import org.simpleframework.http.Response;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
+public class TestFetchMetaGlobalStage {
+ @SuppressWarnings("unused")
+ private static final String LOG_TAG = "TestMetaGlobalStage";
+
+ private static final int TEST_PORT = HTTPServerTestHelper.getTestPort();
+ private static final String TEST_SERVER = "http://localhost:" + TEST_PORT + "/";
+ private static final String TEST_CLUSTER_URL = TEST_SERVER + "cluster/";
+ private HTTPServerTestHelper data = new HTTPServerTestHelper();
+
+ private final String TEST_USERNAME = "johndoe";
+ private final String TEST_PASSWORD = "password";
+ private final String TEST_SYNC_KEY = "abcdeabcdeabcdeabcdeabcdea";
+
+ private final String TEST_INFO_COLLECTIONS_JSON = "{}";
+
+ private static final String TEST_SYNC_ID = "testSyncID";
+ private static final long TEST_STORAGE_VERSION = GlobalSession.STORAGE_VERSION;
+
+ private InfoCollections infoCollections;
+ private KeyBundle syncKeyBundle;
+ private MockGlobalSessionCallback callback;
+ private GlobalSession session;
+
+ private boolean calledRequiresUpgrade = false;
+ private boolean calledProcessMissingMetaGlobal = false;
+ private boolean calledFreshStart = false;
+ private boolean calledWipeServer = false;
+ private boolean calledUploadKeys = false;
+ private boolean calledResetAllStages = false;
+
+ private static void assertSameContents(JSONArray expected, Set<String> actual) {
+ assertEquals(expected.size(), actual.size());
+ for (Object o : expected) {
+ assertTrue(actual.contains(o));
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ calledRequiresUpgrade = false;
+ calledProcessMissingMetaGlobal = false;
+ calledFreshStart = false;
+ calledWipeServer = false;
+ calledUploadKeys = false;
+ calledResetAllStages = false;
+
+ // Set info collections to not have crypto.
+ infoCollections = new InfoCollections(new ExtendedJSONObject(TEST_INFO_COLLECTIONS_JSON));
+
+ syncKeyBundle = new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY);
+ callback = new MockGlobalSessionCallback();
+ session = new MockGlobalSession(TEST_USERNAME, TEST_PASSWORD,
+ syncKeyBundle, callback) {
+ @Override
+ protected void prepareStages() {
+ super.prepareStages();
+ withStage(Stage.fetchMetaGlobal, new FetchMetaGlobalStage());
+ }
+
+ @Override
+ public void requiresUpgrade() {
+ calledRequiresUpgrade = true;
+ this.abort(null, "Requires upgrade");
+ }
+
+ @Override
+ public void processMissingMetaGlobal(MetaGlobal mg) {
+ calledProcessMissingMetaGlobal = true;
+ this.abort(null, "Missing meta/global");
+ }
+
+ // Don't really uploadKeys.
+ @Override
+ public void uploadKeys(CollectionKeys keys, KeyUploadDelegate keyUploadDelegate) {
+ calledUploadKeys = true;
+ keyUploadDelegate.onKeysUploaded();
+ }
+
+ // On fresh start completed, just stop.
+ @Override
+ public void freshStart() {
+ calledFreshStart = true;
+ freshStart(this, new FreshStartDelegate() {
+ @Override
+ public void onFreshStartFailed(Exception e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+
+ @Override
+ public void onFreshStart() {
+ WaitHelper.getTestWaiter().performNotify();
+ }
+ });
+ }
+
+ // Don't really wipeServer.
+ @Override
+ protected void wipeServer(final AuthHeaderProvider authHeaderProvider, final WipeServerDelegate wipeDelegate) {
+ calledWipeServer = true;
+ wipeDelegate.onWiped(System.currentTimeMillis());
+ }
+
+ // Don't really resetAllStages.
+ @Override
+ public void resetAllStages() {
+ calledResetAllStages = true;
+ }
+ };
+ session.config.setClusterURL(new URI(TEST_CLUSTER_URL));
+ session.config.infoCollections = infoCollections;
+ }
+
+ protected void doSession(MockServer server) {
+ data.startHTTPServer(server);
+ WaitHelper.getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ session.start();
+ } catch (AlreadySyncingException e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+ }
+ }));
+ data.stopHTTPServer();
+ }
+
+ @Test
+ public void testFetchRequiresUpgrade() throws Exception {
+ MetaGlobal mg = new MetaGlobal(null, null);
+ mg.setSyncID(TEST_SYNC_ID);
+ mg.setStorageVersion(Long.valueOf(TEST_STORAGE_VERSION + 1));
+
+ MockServer server = new MockServer(200, mg.asCryptoRecord().toJSONString());
+ doSession(server);
+
+ assertEquals(true, callback.calledError);
+ assertTrue(calledRequiresUpgrade);
+ }
+
+ @SuppressWarnings("unchecked")
+ private JSONArray makeTestDeclinedArray() {
+ final JSONArray declined = new JSONArray();
+ declined.add("foobar");
+ return declined;
+ }
+
+ /**
+ * Verify that a fetched meta/global with remote syncID == local syncID does
+ * not reset.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testFetchSuccessWithSameSyncID() throws Exception {
+ session.config.syncID = TEST_SYNC_ID;
+
+ MetaGlobal mg = new MetaGlobal(null, null);
+ mg.setSyncID(TEST_SYNC_ID);
+ mg.setStorageVersion(Long.valueOf(TEST_STORAGE_VERSION));
+
+ // Set declined engines in the server object.
+ final JSONArray testingDeclinedEngines = makeTestDeclinedArray();
+ mg.setDeclinedEngineNames(testingDeclinedEngines);
+
+ MockServer server = new MockServer(200, mg.asCryptoRecord().toJSONString());
+ doSession(server);
+
+ assertTrue(callback.calledSuccess);
+ assertFalse(calledProcessMissingMetaGlobal);
+ assertFalse(calledResetAllStages);
+ assertEquals(TEST_SYNC_ID, session.config.metaGlobal.getSyncID());
+ assertEquals(TEST_STORAGE_VERSION, session.config.metaGlobal.getStorageVersion().longValue());
+ assertEquals(TEST_SYNC_ID, session.config.syncID);
+
+ // Declined engines propagate from the server meta/global.
+ final Set<String> actual = session.config.metaGlobal.getDeclinedEngineNames();
+ assertSameContents(testingDeclinedEngines, actual);
+ }
+
+ /**
+ * Verify that a fetched meta/global with remote syncID != local syncID resets
+ * local and retains remote syncID.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testFetchSuccessWithDifferentSyncID() throws Exception {
+ session.config.syncID = "NOT TEST SYNC ID";
+
+ MetaGlobal mg = new MetaGlobal(null, null);
+ mg.setSyncID(TEST_SYNC_ID);
+ mg.setStorageVersion(Long.valueOf(TEST_STORAGE_VERSION));
+
+ // Set declined engines in the server object.
+ final JSONArray testingDeclinedEngines = makeTestDeclinedArray();
+ mg.setDeclinedEngineNames(testingDeclinedEngines);
+
+ MockServer server = new MockServer(200, mg.asCryptoRecord().toJSONString());
+ doSession(server);
+
+ assertEquals(true, callback.calledSuccess);
+ assertFalse(calledProcessMissingMetaGlobal);
+ assertTrue(calledResetAllStages);
+ assertEquals(TEST_SYNC_ID, session.config.metaGlobal.getSyncID());
+ assertEquals(TEST_STORAGE_VERSION, session.config.metaGlobal.getStorageVersion().longValue());
+ assertEquals(TEST_SYNC_ID, session.config.syncID);
+
+ // Declined engines propagate from the server meta/global.
+ final Set<String> actual = session.config.metaGlobal.getDeclinedEngineNames();
+ assertSameContents(testingDeclinedEngines, actual);
+ }
+
+ /**
+ * Verify that a fetched meta/global does not merge declined engines.
+ * TODO: eventually it should!
+ */
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testFetchSuccessWithDifferentSyncIDMergesDeclined() throws Exception {
+ session.config.syncID = "NOT TEST SYNC ID";
+
+ // Fake the local declined engine names.
+ session.config.declinedEngineNames = new HashSet<String>();
+ session.config.declinedEngineNames.add("baznoo");
+
+ MetaGlobal mg = new MetaGlobal(null, null);
+ mg.setSyncID(TEST_SYNC_ID);
+ mg.setStorageVersion(Long.valueOf(TEST_STORAGE_VERSION));
+
+ // Set declined engines in the server object.
+ final JSONArray testingDeclinedEngines = makeTestDeclinedArray();
+ mg.setDeclinedEngineNames(testingDeclinedEngines);
+
+ MockServer server = new MockServer(200, mg.asCryptoRecord().toJSONString());
+ doSession(server);
+
+ // Declined engines propagate from the server meta/global, and are NOT merged.
+ final Set<String> expected = new HashSet<String>(testingDeclinedEngines);
+ // expected.add("baznoo"); // Not until we merge. Local is lost.
+
+ final Set<String> newDeclined = session.config.metaGlobal.getDeclinedEngineNames();
+ assertEquals(expected, newDeclined);
+ }
+
+ @Test
+ public void testFetchMissing() throws Exception {
+ MockServer server = new MockServer(404, "missing");
+ doSession(server);
+
+ assertEquals(true, callback.calledError);
+ assertTrue(calledProcessMissingMetaGlobal);
+ }
+
+ /**
+ * Empty payload object has no syncID or storageVersion and should call freshStart.
+ * @throws Exception
+ */
+ @Test
+ public void testFetchEmptyPayload() throws Exception {
+ MockServer server = new MockServer(200, TestMetaGlobal.TEST_META_GLOBAL_EMPTY_PAYLOAD_RESPONSE);
+ doSession(server);
+
+ assertTrue(calledFreshStart);
+ }
+
+ /**
+ * No payload means no syncID or storageVersion and therefore we should call freshStart.
+ * @throws Exception
+ */
+ @Test
+ public void testFetchNoPayload() throws Exception {
+ MockServer server = new MockServer(200, TestMetaGlobal.TEST_META_GLOBAL_NO_PAYLOAD_RESPONSE);
+ doSession(server);
+
+ assertTrue(calledFreshStart);
+ }
+
+ /**
+ * Malformed payload is a server response issue, not a meta/global record
+ * issue. This should error out of the sync.
+ * @throws Exception
+ */
+ @Test
+ public void testFetchMalformedPayload() throws Exception {
+ MockServer server = new MockServer(200, TestMetaGlobal.TEST_META_GLOBAL_MALFORMED_PAYLOAD_RESPONSE);
+ doSession(server);
+
+ assertEquals(true, callback.calledError);
+ assertEquals(NonObjectJSONException.class, callback.calledErrorException.getClass());
+ }
+
+ protected void doFreshStart(MockServer server) {
+ data.startHTTPServer(server);
+ WaitHelper.getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
+ @Override
+ public void run() {
+ session.freshStart();
+ }
+ }));
+ data.stopHTTPServer();
+ }
+
+ @Test
+ public void testFreshStart() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, CryptoException {
+ final AtomicBoolean mgUploaded = new AtomicBoolean(false);
+ final AtomicBoolean mgDownloaded = new AtomicBoolean(false);
+ final MetaGlobal uploadedMg = new MetaGlobal(null, null);
+
+ MockServer server = new MockServer() {
+ @Override
+ public void handle(Request request, Response response) {
+ if (request.getMethod().equals("PUT")) {
+ try {
+ ExtendedJSONObject body = new ExtendedJSONObject(request.getContent());
+ assertTrue(body.containsKey("payload"));
+ assertFalse(body.containsKey("default"));
+
+ CryptoRecord rec = CryptoRecord.fromJSONRecord(body);
+ uploadedMg.setFromRecord(rec);
+ mgUploaded.set(true);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ this.handle(request, response, 200, "success");
+ return;
+ }
+ if (mgUploaded.get()) {
+ // We shouldn't be trying to download anything after uploading meta/global.
+ mgDownloaded.set(true);
+ }
+ this.handle(request, response, 404, "missing");
+ }
+ };
+ doFreshStart(server);
+
+ assertTrue(this.calledFreshStart);
+ assertTrue(this.calledWipeServer);
+ assertTrue(this.calledUploadKeys);
+ assertTrue(mgUploaded.get());
+ assertFalse(mgDownloaded.get());
+ assertEquals(GlobalSession.STORAGE_VERSION, uploadedMg.getStorageVersion().longValue());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestStageLookup.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestStageLookup.java
new file mode 100644
index 0000000000..86829844f9
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestStageLookup.java
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.stage.test;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(TestRunner.class)
+public class TestStageLookup {
+
+ @Test
+ public void testStageLookupByName() {
+ Set<Stage> namedStages = new HashSet<Stage>(Stage.getNamedStages());
+ Set<Stage> expected = new HashSet<Stage>();
+ expected.add(Stage.syncClientsEngine);
+ expected.add(Stage.syncBookmarks);
+ expected.add(Stage.syncTabs);
+ expected.add(Stage.syncFormHistory);
+ expected.add(Stage.syncHistory);
+ expected.add(Stage.syncPasswords);
+
+ assertEquals(expected, namedStages);
+ assertEquals(Stage.syncClientsEngine, Stage.byName("clients"));
+ assertEquals(Stage.syncTabs, Stage.byName("tabs"));
+ assertEquals(Stage.syncBookmarks, Stage.byName("bookmarks"));
+ assertEquals(Stage.syncFormHistory, Stage.byName("forms"));
+ assertEquals(Stage.syncHistory, Stage.byName("history"));
+ assertEquals(Stage.syncPasswords, Stage.byName("passwords"));
+
+ assertEquals(null, Stage.byName("foobar"));
+ assertEquals(null, Stage.byName(null));
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestExtendedJSONObject.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestExtendedJSONObject.java
new file mode 100644
index 0000000000..cff9287df5
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestExtendedJSONObject.java
@@ -0,0 +1,203 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.test;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonArrayJSONException;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.UnexpectedJSONException.BadRequiredFieldJSONException;
+
+import java.io.IOException;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+@RunWith(TestRunner.class)
+public class TestExtendedJSONObject {
+ public static String exampleJSON = "{\"modified\":1233702554.25,\"success\":[\"{GXS58IDC}12\",\"{GXS58IDC}13\",\"{GXS58IDC}15\",\"{GXS58IDC}16\",\"{GXS58IDC}18\",\"{GXS58IDC}19\"],\"failed\":{\"{GXS58IDC}11\":[\"invalid parentid\"],\"{GXS58IDC}14\":[\"invalid parentid\"],\"{GXS58IDC}17\":[\"invalid parentid\"],\"{GXS58IDC}20\":[\"invalid parentid\"]}}";
+ public static String exampleIntegral = "{\"modified\":1233702554,}";
+
+ @Test
+ public void testFractional() throws IOException, NonObjectJSONException {
+ ExtendedJSONObject o = new ExtendedJSONObject(exampleJSON);
+ assertTrue(o.containsKey("modified"));
+ assertTrue(o.containsKey("success"));
+ assertTrue(o.containsKey("failed"));
+ assertFalse(o.containsKey(" "));
+ assertFalse(o.containsKey(""));
+ assertFalse(o.containsKey("foo"));
+ assertTrue(o.get("modified") instanceof Number);
+ assertTrue(o.get("modified").equals(Double.parseDouble("1233702554.25")));
+ assertEquals(Long.valueOf(1233702554250L), o.getTimestamp("modified"));
+ assertEquals(null, o.getTimestamp("foo"));
+ }
+
+ @Test
+ public void testIntegral() throws IOException, NonObjectJSONException {
+ ExtendedJSONObject o = new ExtendedJSONObject(exampleIntegral);
+ assertTrue(o.containsKey("modified"));
+ assertFalse(o.containsKey("success"));
+ assertTrue(o.get("modified") instanceof Number);
+ assertTrue(o.get("modified").equals(Long.parseLong("1233702554")));
+ assertEquals(Long.valueOf(1233702554000L), o.getTimestamp("modified"));
+ assertEquals(null, o.getTimestamp("foo"));
+ }
+
+ @Test
+ public void testSafeInteger() {
+ ExtendedJSONObject o = new ExtendedJSONObject();
+ o.put("integer", Integer.valueOf(5));
+ o.put("string", "66");
+ o.put("object", new ExtendedJSONObject());
+ o.put("null", (JSONArray) null);
+
+ assertEquals(Integer.valueOf(5), o.getIntegerSafely("integer"));
+ assertEquals(Integer.valueOf(66), o.getIntegerSafely("string"));
+ assertNull(o.getIntegerSafely(null));
+ }
+
+ @Test
+ public void testParseJSONArray() throws Exception {
+ JSONArray result = ExtendedJSONObject.parseJSONArray("[0, 1, {\"test\": 2}]");
+ assertNotNull(result);
+
+ assertThat((Long) result.get(0), is(equalTo(0L)));
+ assertThat((Long) result.get(1), is(equalTo(1L)));
+ assertThat((Long) ((JSONObject) result.get(2)).get("test"), is(equalTo(2L)));
+ }
+
+ @Test
+ public void testBadParseJSONArray() throws Exception {
+ try {
+ ExtendedJSONObject.parseJSONArray("[0, ");
+ fail();
+ } catch (NonArrayJSONException e) {
+ // Do nothing.
+ }
+
+ try {
+ ExtendedJSONObject.parseJSONArray("{}");
+ fail();
+ } catch (NonArrayJSONException e) {
+ // Do nothing.
+ }
+ }
+
+ @Test
+ public void testParseUTF8AsJSONObject() throws Exception {
+ String TEST = "{\"key\":\"value\"}";
+
+ ExtendedJSONObject o = ExtendedJSONObject.parseUTF8AsJSONObject(TEST.getBytes("UTF-8"));
+ assertNotNull(o);
+ assertEquals("value", o.getString("key"));
+ }
+
+ @Test
+ public void testBadParseUTF8AsJSONObject() throws Exception {
+ try {
+ ExtendedJSONObject.parseUTF8AsJSONObject("{}".getBytes("UTF-16"));
+ fail();
+ } catch (NonObjectJSONException e) {
+ // Do nothing.
+ }
+
+ try {
+ ExtendedJSONObject.parseUTF8AsJSONObject("{".getBytes("UTF-8"));
+ fail();
+ } catch (NonObjectJSONException e) {
+ // Do nothing.
+ }
+ }
+
+ @Test
+ public void testHashCode() throws Exception {
+ ExtendedJSONObject o = new ExtendedJSONObject(exampleJSON);
+ assertEquals(o.hashCode(), o.hashCode());
+ ExtendedJSONObject p = new ExtendedJSONObject(exampleJSON);
+ assertEquals(o.hashCode(), p.hashCode());
+
+ ExtendedJSONObject q = new ExtendedJSONObject(exampleJSON);
+ q.put("modified", 0);
+ assertNotSame(o.hashCode(), q.hashCode());
+ }
+
+ @Test
+ public void testEquals() throws Exception {
+ ExtendedJSONObject o = new ExtendedJSONObject(exampleJSON);
+ ExtendedJSONObject p = new ExtendedJSONObject(exampleJSON);
+ assertEquals(o, p);
+
+ ExtendedJSONObject q = new ExtendedJSONObject(exampleJSON);
+ q.put("modified", 0);
+ assertNotSame(o, q);
+ assertNotEquals(o, q);
+ }
+
+ @Test
+ public void testGetBoolean() throws Exception {
+ ExtendedJSONObject o = new ExtendedJSONObject("{\"truekey\":true, \"falsekey\":false, \"stringkey\":\"string\"}");
+ assertEquals(true, o.getBoolean("truekey"));
+ assertEquals(false, o.getBoolean("falsekey"));
+ try {
+ o.getBoolean("stringkey");
+ fail();
+ } catch (Exception e) {
+ assertTrue(e instanceof ClassCastException);
+ }
+ assertEquals(null, o.getBoolean("missingkey"));
+ }
+
+ @Test
+ public void testNullLong() throws Exception {
+ ExtendedJSONObject o = new ExtendedJSONObject("{\"x\": null}");
+ Long x = o.getLong("x");
+ assertNull(x);
+
+ long y = o.getLong("x", 5L);
+ assertEquals(5L, y);
+ }
+
+ protected void assertException(ExtendedJSONObject o, String[] requiredFields, Class<?> requiredFieldClass) {
+ try {
+ o.throwIfFieldsMissingOrMisTyped(requiredFields, requiredFieldClass);
+ fail();
+ } catch (Exception e) {
+ assertTrue(e instanceof BadRequiredFieldJSONException);
+ }
+ }
+
+ @Test
+ public void testThrow() throws Exception {
+ ExtendedJSONObject o = new ExtendedJSONObject("{\"true\":true, \"false\":false, \"string\":\"string\", \"long\":40000000000, \"int\":40, \"nested\":{\"inner\":10}}");
+ o.throwIfFieldsMissingOrMisTyped(new String[] { "true", "false" }, Boolean.class);
+ o.throwIfFieldsMissingOrMisTyped(new String[] { "string" }, String.class);
+ o.throwIfFieldsMissingOrMisTyped(new String[] { "long" }, Long.class);
+ o.throwIfFieldsMissingOrMisTyped(new String[] { "int" }, Long.class);
+ o.throwIfFieldsMissingOrMisTyped(new String[] { "int" }, null);
+
+ // Perhaps a bit unexpected, but we'll document it here.
+ o.throwIfFieldsMissingOrMisTyped(new String[] { "nested" }, JSONObject.class);
+
+ // Should fail.
+ assertException(o, new String[] { "int" }, Integer.class); // Irritating, but...
+ assertException(o, new String[] { "long" }, Integer.class); // Ditto.
+ assertException(o, new String[] { "missing" }, String.class);
+ assertException(o, new String[] { "missing" }, null);
+ assertException(o, new String[] { "string", "int" }, String.class); // Irritating, but...
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestInfoCollections.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestInfoCollections.java
new file mode 100644
index 0000000000..d850ccc568
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestInfoCollections.java
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.test;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.InfoCollections;
+import org.mozilla.gecko.sync.InfoCounts;
+import org.mozilla.gecko.sync.Utils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test both info/collections and info/collection_counts.
+ */
+@RunWith(TestRunner.class)
+public class TestInfoCollections {
+ public static final String TEST_COLLECTIONS_JSON =
+ "{\"history\":1.3319567131E9, " +
+ " \"bookmarks\":1.33195669592E9, " +
+ " \"prefs\":1.33115408641E9, " +
+ " \"crypto\":1.32046063664E9, " +
+ " \"meta\":1.321E9, " +
+ " \"forms\":1.33136685374E9, " +
+ " \"clients\":1.3313667619E9, " +
+ " \"tabs\":1.35E9" +
+ "}";
+
+
+ public static final String TEST_COUNTS_JSON =
+ "{\"passwords\": 390, " +
+ " \"clients\": 2, " +
+ " \"crypto\": 1, " +
+ " \"forms\": 1019, " +
+ " \"bookmarks\": 766, " +
+ " \"prefs\": 1, " +
+ " \"history\": 9278" +
+ "}";
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testSetCountsFromRecord() throws Exception {
+ InfoCounts infoCountsEmpty = new InfoCounts(new ExtendedJSONObject("{}"));
+ assertEquals(null, infoCountsEmpty.getCount("bookmarks"));
+
+ ExtendedJSONObject record = new ExtendedJSONObject(TEST_COUNTS_JSON);
+ InfoCounts infoCountsFull = new InfoCounts(record);
+ assertEquals(Integer.valueOf(766), infoCountsFull.getCount("bookmarks"));
+ assertEquals(null, infoCountsFull.getCount("notpresent"));
+ }
+
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testSetCollectionsFromRecord() throws Exception {
+ ExtendedJSONObject record = new ExtendedJSONObject(TEST_COLLECTIONS_JSON);
+ InfoCollections infoCollections = new InfoCollections(record);
+
+ assertEquals(Utils.decimalSecondsToMilliseconds(1.3319567131E9), infoCollections.getTimestamp("history").longValue());
+ assertEquals(Utils.decimalSecondsToMilliseconds(1.321E9), infoCollections.getTimestamp("meta").longValue());
+ assertEquals(Utils.decimalSecondsToMilliseconds(1.35E9), infoCollections.getTimestamp("tabs").longValue());
+ assertNull(infoCollections.getTimestamp("missing"));
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testUpdateNeeded() throws Exception {
+ ExtendedJSONObject record = new ExtendedJSONObject(TEST_COLLECTIONS_JSON);
+ InfoCollections infoCollections = new InfoCollections(record);
+
+ long none = -1;
+ long past = Utils.decimalSecondsToMilliseconds(1.3E9);
+ long same = Utils.decimalSecondsToMilliseconds(1.35E9);
+ long future = Utils.decimalSecondsToMilliseconds(1.4E9);
+
+
+ // Test with no local timestamp set.
+ assertTrue(infoCollections.updateNeeded("tabs", none));
+
+ // Test with local timestamp set in the past.
+ assertTrue(infoCollections.updateNeeded("tabs", past));
+
+ // Test with same timestamp.
+ assertFalse(infoCollections.updateNeeded("tabs", same));
+
+ // Test with local timestamp set in the future.
+ assertFalse(infoCollections.updateNeeded("tabs", future));
+
+ // Test with no collection.
+ assertTrue(infoCollections.updateNeeded("missing", none));
+ assertTrue(infoCollections.updateNeeded("missing", past));
+ assertTrue(infoCollections.updateNeeded("missing", same));
+ assertTrue(infoCollections.updateNeeded("missing", future));
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestPersistedMetaGlobal.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestPersistedMetaGlobal.java
new file mode 100644
index 0000000000..d1b6cadef0
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestPersistedMetaGlobal.java
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.sync.test;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.MetaGlobal;
+import org.mozilla.gecko.sync.NoCollectionKeysSetException;
+import org.mozilla.gecko.sync.PersistedMetaGlobal;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
+public class TestPersistedMetaGlobal {
+ MockSharedPreferences prefs = null;
+ private final String TEST_META_URL = "metaURL";
+ private final String TEST_CREDENTIALS = "credentials";
+
+ @Before
+ public void setUp() {
+ prefs = new MockSharedPreferences();
+ }
+
+ @Test
+ public void testPersistLastModified() throws CryptoException, NoCollectionKeysSetException {
+ long LAST_MODIFIED = System.currentTimeMillis();
+ PersistedMetaGlobal persisted = new PersistedMetaGlobal(prefs);
+
+ // Test fresh start.
+ assertEquals(-1, persisted.lastModified());
+
+ // Test persisting.
+ persisted.persistLastModified(LAST_MODIFIED);
+ assertEquals(LAST_MODIFIED, persisted.lastModified());
+
+ // Test clearing.
+ persisted.persistLastModified(0);
+ assertEquals(-1, persisted.lastModified());
+ }
+
+ @Test
+ public void testPersistMetaGlobal() throws Exception {
+ PersistedMetaGlobal persisted = new PersistedMetaGlobal(prefs);
+ AuthHeaderProvider authHeaderProvider = new BasicAuthHeaderProvider(TEST_CREDENTIALS);
+
+ // Test fresh start.
+ assertNull(persisted.metaGlobal(TEST_META_URL, authHeaderProvider));
+
+ // Test persisting.
+ String body = "{\"id\":\"global\",\"payload\":\"{\\\"syncID\\\":\\\"zPSQTm7WBVWB\\\",\\\"storageVersion\\\":5,\\\"engines\\\":{\\\"clients\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"fDg0MS5bDtV7\\\"},\\\"bookmarks\\\":{\\\"version\\\":2,\\\"syncID\\\":\\\"NNaQr6_F-9dm\\\"},\\\"forms\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"GXF29AFprnvc\\\"},\\\"history\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"av75g4vm-_rp\\\"},\\\"passwords\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"LT_ACGpuKZ6a\\\"},\\\"prefs\\\":{\\\"version\\\":2,\\\"syncID\\\":\\\"-3nsksP9wSAs\\\"},\\\"tabs\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"W4H5lOMChkYA\\\"}}}\",\"username\":\"5817483\",\"modified\":1.32046073744E9}";
+ MetaGlobal mg = new MetaGlobal(TEST_META_URL, authHeaderProvider);
+ mg.setFromRecord(CryptoRecord.fromJSONRecord(body));
+ persisted.persistMetaGlobal(mg);
+
+ MetaGlobal persistedGlobal = persisted.metaGlobal(TEST_META_URL, authHeaderProvider);
+ assertNotNull(persistedGlobal);
+ assertEquals("zPSQTm7WBVWB", persistedGlobal.getSyncID());
+ assertTrue(persistedGlobal.getEngines() instanceof ExtendedJSONObject);
+ assertEquals(Long.valueOf(5), persistedGlobal.getStorageVersion());
+
+ // Test clearing.
+ persisted.persistMetaGlobal(null);
+ assertNull(persisted.metaGlobal(null, null));
+ }
+
+ @Test
+ public void testPersistDeclinedEngines() throws Exception {
+ PersistedMetaGlobal persisted = new PersistedMetaGlobal(prefs);
+ AuthHeaderProvider authHeaderProvider = new BasicAuthHeaderProvider(TEST_CREDENTIALS);
+
+ // Test fresh start.
+ assertNull(persisted.metaGlobal(TEST_META_URL, authHeaderProvider));
+
+ // Test persisting.
+ String body = "{\"id\":\"global\",\"payload\":\"{\\\"declined\\\":[\\\"bookmarks\\\",\\\"addons\\\"],\\\"syncID\\\":\\\"zPSQTm7WBVWB\\\",\\\"storageVersion\\\":5,\\\"engines\\\":{\\\"clients\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"fDg0MS5bDtV7\\\"},,\\\"forms\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"GXF29AFprnvc\\\"},\\\"history\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"av75g4vm-_rp\\\"},\\\"passwords\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"LT_ACGpuKZ6a\\\"},\\\"prefs\\\":{\\\"version\\\":2,\\\"syncID\\\":\\\"-3nsksP9wSAs\\\"},\\\"tabs\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"W4H5lOMChkYA\\\"}}}\",\"username\":\"5817483\",\"modified\":1.32046073744E9}";
+ MetaGlobal mg = new MetaGlobal(TEST_META_URL, authHeaderProvider);
+ mg.setFromRecord(CryptoRecord.fromJSONRecord(body));
+ persisted.persistMetaGlobal(mg);
+
+ MetaGlobal persistedGlobal = persisted.metaGlobal(TEST_META_URL, authHeaderProvider);
+ assertNotNull(persistedGlobal);
+ Set<String> declined = persistedGlobal.getDeclinedEngineNames();
+ assertEquals(2, declined.size());
+ assertTrue(declined.contains("bookmarks"));
+ assertTrue(declined.contains("addons"));
+
+ // Test clearing.
+ persisted.persistMetaGlobal(null);
+ assertNull(persisted.metaGlobal(null, null));
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/measurements/TestSearchCountMeasurements.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/measurements/TestSearchCountMeasurements.java
new file mode 100644
index 0000000000..058461f8e5
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/measurements/TestSearchCountMeasurements.java
@@ -0,0 +1,161 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.telemetry.measurements;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests for the class that stores search count measurements.
+ */
+@RunWith(TestRunner.class)
+public class TestSearchCountMeasurements {
+
+ private SharedPreferences sharedPrefs;
+
+ @Before
+ public void setUp() throws Exception {
+ sharedPrefs = RuntimeEnvironment.application.getSharedPreferences(
+ TestSearchCountMeasurements.class.getSimpleName(), Context.MODE_PRIVATE);
+ }
+
+ private void assertNewValueInsertedNoIncrementedValues(final int expectedKeyCount) {
+ assertEquals("Shared prefs key count has incremented", expectedKeyCount, sharedPrefs.getAll().size());
+ assertTrue("Shared prefs still contains non-incremented initial value", sharedPrefs.getAll().containsValue(1));
+ assertFalse("Shared prefs has not incremented any values", sharedPrefs.getAll().containsValue(2));
+ }
+
+ @Test
+ public void testIncrementSearchCanRecreateEngineAndWhere() throws Exception {
+ final String expectedIdentifier = "google";
+ final String expectedWhere = "suggestbar";
+
+ SearchCountMeasurements.incrementSearch(sharedPrefs, expectedIdentifier, expectedWhere);
+ assertFalse("Shared prefs has some values", sharedPrefs.getAll().isEmpty());
+ assertTrue("Shared prefs contains initial value", sharedPrefs.getAll().containsValue(1));
+
+ boolean foundEngine = false;
+ for (final String key : sharedPrefs.getAll().keySet()) {
+ // We could try to match the exact key, but that's more fragile.
+ if (key.contains(expectedIdentifier) && key.contains(expectedWhere)) {
+ foundEngine = true;
+ }
+ }
+ assertTrue("SharedPrefs keyset contains enough info to recreate engine & where", foundEngine);
+ }
+
+ @Test
+ public void testIncrementSearchCalledMultipleTimesSameEngine() throws Exception {
+ final String engineIdentifier = "whatever";
+ final String where = "wherever";
+
+ SearchCountMeasurements.incrementSearch(sharedPrefs, engineIdentifier, where);
+ assertFalse("Shared prefs has some values", sharedPrefs.getAll().isEmpty());
+ assertTrue("Shared prefs contains initial value", sharedPrefs.getAll().containsValue(1));
+
+ // The initial key count storage saves metadata so we can't verify the number of keys is only 1. However,
+ // we assume subsequent calls won't add additional metadata and use it to verify the key count.
+ final int keyCountAfterFirst = sharedPrefs.getAll().size();
+ for (int i = 2; i <= 3; ++i) {
+ SearchCountMeasurements.incrementSearch(sharedPrefs, engineIdentifier, where);
+ assertEquals("Shared prefs key count has not changed", keyCountAfterFirst, sharedPrefs.getAll().size());
+ assertTrue("Shared prefs incremented", sharedPrefs.getAll().containsValue(i));
+ }
+ }
+
+ @Test
+ public void testIncrementSearchCalledMultipleTimesSameEngineDifferentWhere() throws Exception {
+ final String engineIdenfitier = "whatever";
+
+ SearchCountMeasurements.incrementSearch(sharedPrefs, engineIdenfitier, "one place");
+ assertFalse("Shared prefs has some values", sharedPrefs.getAll().isEmpty());
+ assertTrue("Shared prefs contains initial value", sharedPrefs.getAll().containsValue(1));
+
+ // The initial key count storage saves metadata so we can't verify the number of keys is only 1. However,
+ // we assume subsequent calls won't add additional metadata and use it to verify the key count.
+ final int keyCountAfterFirst = sharedPrefs.getAll().size();
+ for (int i = 1; i <= 2; ++i) {
+ SearchCountMeasurements.incrementSearch(sharedPrefs, engineIdenfitier, "another place " + i);
+ assertNewValueInsertedNoIncrementedValues(keyCountAfterFirst + i);
+ }
+ }
+
+ @Test
+ public void testIncrementSearchCalledMultipleTimesDifferentEngines() throws Exception {
+ final String where = "wherever";
+
+ SearchCountMeasurements.incrementSearch(sharedPrefs, "steam engine", where);
+ assertFalse("Shared prefs has some values", sharedPrefs.getAll().isEmpty());
+ assertTrue("Shared prefs contains initial value", sharedPrefs.getAll().containsValue(1));
+
+ // The initial key count storage saves metadata so we can't verify the number of keys is only 1. However,
+ // we assume subsequent calls won't add additional metadata and use it to verify the key count.
+ final int keyCountAfterFirst = sharedPrefs.getAll().size();
+ for (int i = 1; i <= 2; ++i) {
+ SearchCountMeasurements.incrementSearch(sharedPrefs, "combustion engine" + i, where);
+ assertNewValueInsertedNoIncrementedValues(keyCountAfterFirst + i);
+ }
+ }
+
+ @Test // assumes the format saved in SharedPrefs to store test data
+ public void testGetAndZeroSearchDeletesPrefs() throws Exception {
+ assertTrue("Shared prefs is empty", sharedPrefs.getAll().isEmpty());
+
+ final SharedPreferences.Editor editor = sharedPrefs.edit();
+ final Set<String> engineKeys = new HashSet<>(Arrays.asList("whatever.yeah", "lol.what"));
+ editor.putStringSet(SearchCountMeasurements.PREF_SEARCH_KEYSET, engineKeys);
+ for (final String key : engineKeys) {
+ editor.putInt(getEngineSearchCountKey(key), 1);
+ }
+ editor.apply();
+ assertFalse("Shared prefs is not empty after test data inserted", sharedPrefs.getAll().isEmpty());
+
+ SearchCountMeasurements.getAndZeroSearch(sharedPrefs);
+ assertTrue("Shared prefs is empty after zero", sharedPrefs.getAll().isEmpty());
+ }
+
+ @Test // assumes the format saved in SharedPrefs to store test data
+ public void testGetAndZeroSearchVerifyReturnedData() throws Exception {
+ final HashMap<String, Integer> expected = new HashMap<>();
+ expected.put("steamengine.here", 1337);
+ expected.put("combustionengine.there", 10);
+
+ final SharedPreferences.Editor editor = sharedPrefs.edit();
+ editor.putStringSet(SearchCountMeasurements.PREF_SEARCH_KEYSET, expected.keySet());
+ for (final String key : expected.keySet()) {
+ editor.putInt(SearchCountMeasurements.getEngineSearchCountKey(key), expected.get(key));
+ }
+ editor.apply();
+ assertFalse("Shared prefs is not empty after test data inserted", sharedPrefs.getAll().isEmpty());
+
+ final ExtendedJSONObject actual = SearchCountMeasurements.getAndZeroSearch(sharedPrefs);
+ assertEquals("Returned JSON contains number of items inserted", expected.size(), actual.size());
+ for (final String key : expected.keySet()) {
+ assertEquals("Returned JSON contains inserted value", expected.get(key), (Integer) actual.getIntegerSafely(key));
+ }
+ }
+
+ @Test
+ public void testGetAndZeroSearchNoData() throws Exception {
+ final ExtendedJSONObject actual = SearchCountMeasurements.getAndZeroSearch(sharedPrefs);
+ assertEquals("Returned json is empty", 0, actual.size());
+ }
+
+ private String getEngineSearchCountKey(final String engineWhereStr) {
+ return SearchCountMeasurements.getEngineSearchCountKey(engineWhereStr);
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/measurements/TestSessionMeasurements.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/measurements/TestSessionMeasurements.java
new file mode 100644
index 0000000000..a5d3ce5510
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/measurements/TestSessionMeasurements.java
@@ -0,0 +1,124 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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 org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.telemetry.measurements.SessionMeasurements.SessionMeasurementsContainer;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * Tests the session measurements class.
+ */
+@RunWith(TestRunner.class)
+public class TestSessionMeasurements {
+
+ private SessionMeasurements testMeasurements;
+ private SharedPreferences sharedPrefs;
+ private Context context;
+
+ @Before
+ public void setUp() throws Exception {
+ testMeasurements = spy(SessionMeasurements.class);
+ sharedPrefs = RuntimeEnvironment.application.getSharedPreferences(
+ TestSessionMeasurements.class.getSimpleName(), Context.MODE_PRIVATE);
+ doReturn(sharedPrefs).when(testMeasurements).getSharedPreferences(any(Context.class));
+
+ context = RuntimeEnvironment.application;
+ }
+
+ private void assertSessionCount(final String postfix, final int expectedSessionCount) {
+ final int actual = sharedPrefs.getInt(SessionMeasurements.PREF_SESSION_COUNT, -1);
+ assertEquals("Expected number of sessions occurred " + postfix, expectedSessionCount, actual);
+ }
+
+ private void assertSessionDuration(final String postfix, final long expectedSessionDuration) {
+ final long actual = sharedPrefs.getLong(SessionMeasurements.PREF_SESSION_DURATION, -1);
+ assertEquals("Expected session duration received " + postfix, expectedSessionDuration, actual);
+ }
+
+ private void mockGetSystemTimeNanosToReturn(final long value) {
+ doReturn(value).when(testMeasurements).getSystemTimeNano();
+ }
+
+ @Test
+ public void testRecordSessionStartAndEndCalledOnce() throws Exception {
+ final long expectedElapsedSeconds = 4;
+ mockGetSystemTimeNanosToReturn(0);
+ testMeasurements.recordSessionStart();
+ mockGetSystemTimeNanosToReturn(TimeUnit.SECONDS.toNanos(expectedElapsedSeconds));
+ testMeasurements.recordSessionEnd(context);
+
+ final String postfix = "after recordSessionStart/End called once";
+ assertSessionCount(postfix, 1);
+ assertSessionDuration(postfix, expectedElapsedSeconds);
+ }
+
+ @Test
+ public void testRecordSessionStartAndEndCalledTwice() throws Exception {
+ final long expectedElapsedSeconds = 100;
+ mockGetSystemTimeNanosToReturn(0L);
+ for (int i = 1; i <= 2; ++i) {
+ testMeasurements.recordSessionStart();
+ mockGetSystemTimeNanosToReturn(TimeUnit.SECONDS.toNanos((expectedElapsedSeconds / 2) * i));
+ testMeasurements.recordSessionEnd(context);
+ }
+
+ final String postfix = "after recordSessionStart/End called twice";
+ assertSessionCount(postfix, 2);
+ assertSessionDuration(postfix, expectedElapsedSeconds);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testRecordSessionStartThrowsIfSessionAlreadyStarted() throws Exception {
+ // First call will start the session, next expected to throw.
+ for (int i = 0; i < 2; ++i) {
+ testMeasurements.recordSessionStart();
+ }
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testRecordSessionEndThrowsIfCalledBeforeSessionStarted() {
+ testMeasurements.recordSessionEnd(context);
+ }
+
+ @Test // assumes the underlying format in SessionMeasurements
+ public void testGetAndResetSessionMeasurementsReturnsSetData() throws Exception {
+ final int expectedSessionCount = 42;
+ final long expectedSessionDuration = 1234567890;
+ sharedPrefs.edit()
+ .putInt(SessionMeasurements.PREF_SESSION_COUNT, expectedSessionCount)
+ .putLong(SessionMeasurements.PREF_SESSION_DURATION, expectedSessionDuration)
+ .apply();
+
+ final SessionMeasurementsContainer actual = testMeasurements.getAndResetSessionMeasurements(context);
+ assertEquals("Returned session count matches expected", expectedSessionCount, actual.sessionCount);
+ assertEquals("Returned session duration matches expected", expectedSessionDuration, actual.elapsedSeconds);
+ }
+
+ @Test
+ public void testGetAndResetSessionMeasurementsResetsData() throws Exception {
+ sharedPrefs.edit()
+ .putInt(SessionMeasurements.PREF_SESSION_COUNT, 10)
+ .putLong(SessionMeasurements.PREF_SESSION_DURATION, 10)
+ .apply();
+
+ testMeasurements.getAndResetSessionMeasurements(context);
+ final String postfix = "is reset after retrieval";
+ assertSessionCount(postfix, 0);
+ assertSessionDuration(postfix, 0);
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/pingbuilders/TestTelemetryPingBuilder.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/pingbuilders/TestTelemetryPingBuilder.java
new file mode 100644
index 0000000000..ca01241218
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/pingbuilders/TestTelemetryPingBuilder.java
@@ -0,0 +1,84 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+import static org.junit.Assert.*;
+
+/**
+ * Unit test methods of the {@link TelemetryPingBuilder} class.
+ */
+@RunWith(TestRunner.class)
+public class TestTelemetryPingBuilder {
+ @Test
+ public void testMandatoryFieldsNone() {
+ final NoMandatoryFieldsBuilder builder = new NoMandatoryFieldsBuilder();
+ builder.setNonMandatoryField();
+ assertNotNull("Builder does not throw and returns a non-null value", builder.build());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testMandatoryFieldsMissing() {
+ final MandatoryFieldsBuilder builder = new MandatoryFieldsBuilder();
+ builder.setNonMandatoryField()
+ .build(); // should throw
+ }
+
+ @Test
+ public void testMandatoryFieldsIncluded() {
+ final MandatoryFieldsBuilder builder = new MandatoryFieldsBuilder();
+ builder.setNonMandatoryField()
+ .setMandatoryField();
+ assertNotNull("Builder does not throw and returns non-null value", builder.build());
+ }
+
+ private static class NoMandatoryFieldsBuilder extends TelemetryPingBuilder {
+ @Override
+ public String getDocType() {
+ return "";
+ }
+
+ @Override
+ public String[] getMandatoryFields() {
+ return new String[0];
+ }
+
+ public NoMandatoryFieldsBuilder setNonMandatoryField() {
+ payload.put("non-mandatory", true);
+ return this;
+ }
+ }
+
+ private static class MandatoryFieldsBuilder extends TelemetryPingBuilder {
+ private static final String MANDATORY_FIELD = "mandatory-field";
+
+ @Override
+ public String getDocType() {
+ return "";
+ }
+
+ @Override
+ public String[] getMandatoryFields() {
+ return new String[] {
+ MANDATORY_FIELD,
+ };
+ }
+
+ public MandatoryFieldsBuilder setNonMandatoryField() {
+ payload.put("non-mandatory", true);
+ return this;
+ }
+
+ public MandatoryFieldsBuilder setMandatoryField() {
+ payload.put(MANDATORY_FIELD, true);
+ return this;
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/schedulers/TestTelemetryUploadAllPingsImmediatelyScheduler.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/schedulers/TestTelemetryUploadAllPingsImmediatelyScheduler.java
new file mode 100644
index 0000000000..8093040eed
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/schedulers/TestTelemetryUploadAllPingsImmediatelyScheduler.java
@@ -0,0 +1,59 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.telemetry.TelemetryUploadService;
+import org.mozilla.gecko.telemetry.stores.TelemetryPingStore;
+
+import static junit.framework.Assert.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * Unit tests for the upload immediately scheduler.
+ *
+ * When we add more schedulers, we'll likely change the interface
+ * (e.g. pass in current time) and these tests will be more useful.
+ */
+@RunWith(TestRunner.class)
+public class TestTelemetryUploadAllPingsImmediatelyScheduler {
+
+ private TelemetryUploadAllPingsImmediatelyScheduler testScheduler;
+ private TelemetryPingStore testStore;
+
+ @Before
+ public void setUp() {
+ testScheduler = new TelemetryUploadAllPingsImmediatelyScheduler();
+ testStore = mock(TelemetryPingStore.class);
+ }
+
+ @Test
+ public void testReadyToUpload() {
+ assertTrue("Scheduler is always ready to upload", testScheduler.isReadyToUpload(testStore));
+ }
+
+ @Test
+ public void testScheduleUpload() {
+ final Context context = mock(Context.class);
+
+ testScheduler.scheduleUpload(context, testStore);
+
+ final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(context).startService(intentCaptor.capture());
+ final Intent actualIntent = intentCaptor.getValue();
+ assertEquals("Intent action is upload", TelemetryUploadService.ACTION_UPLOAD, actualIntent.getAction());
+ assertTrue("Intent contains store", actualIntent.hasExtra(TelemetryUploadService.EXTRA_STORE));
+ assertEquals("Intent class target is upload service",
+ TelemetryUploadService.class.getName(), actualIntent.getComponent().getClassName());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/stores/TestTelemetryJSONFilePingStore.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/stores/TestTelemetryJSONFilePingStore.java
new file mode 100644
index 0000000000..a95a8b292b
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/stores/TestTelemetryJSONFilePingStore.java
@@ -0,0 +1,250 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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 org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.telemetry.TelemetryPing;
+import org.mozilla.gecko.util.FileUtils;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FilenameFilter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+
+import static org.junit.Assert.*;
+
+/**
+ * Unit test methods of the {@link TelemetryJSONFilePingStore} class.
+ */
+@RunWith(TestRunner.class)
+public class TestTelemetryJSONFilePingStore {
+
+ @Rule
+ public TemporaryFolder tempDir = new TemporaryFolder();
+ private File testDir;
+ private TelemetryJSONFilePingStore testStore;
+
+ @Before
+ public void setUp() throws Exception {
+ testDir = tempDir.newFolder();
+ testStore = new TelemetryJSONFilePingStore(testDir, "");
+ }
+
+ private ExtendedJSONObject generateTelemetryPayload() {
+ final ExtendedJSONObject out = new ExtendedJSONObject();
+ out.put("str", "a String");
+ out.put("int", 42);
+ out.put("null", (ExtendedJSONObject) null);
+ return out;
+ }
+
+ private void assertIsGeneratedPayload(final ExtendedJSONObject actual) throws Exception {
+ assertNull("Null field is null", actual.getObject("null"));
+ assertEquals("int field is correct", 42, (int) actual.getIntegerSafely("int"));
+ assertEquals("str field is correct", "a String", actual.getString("str"));
+ }
+
+ private void assertStoreFileCount(final int expectedCount) {
+ assertEquals("Store contains " + expectedCount + " item(s)", expectedCount, testDir.list().length);
+ }
+
+ @Test
+ public void testConstructorOnlyWritesToGivenDir() throws Exception {
+ // Constructor is called in @Before method
+ assertTrue("Store dir exists", testDir.exists());
+ assertEquals("Temp dir contains one dir (the store dir)", 1, tempDir.getRoot().list().length);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testConstructorStoreAlreadyExistsAsNonDirectory() throws Exception {
+ final File file = tempDir.newFile();
+ new TelemetryJSONFilePingStore(file, "profileName"); // expected to throw.
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testConstructorDirIsNotReadable() throws Exception {
+ final File dir = tempDir.newFolder();
+ dir.setReadable(false);
+ new TelemetryJSONFilePingStore(dir, "profileName"); // expected to throw.
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testConstructorDirIsNotWritable() throws Exception {
+ final File dir = tempDir.newFolder();
+ dir.setWritable(false);
+ new TelemetryJSONFilePingStore(dir, "profileName"); // expected to throw.
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testConstructorDirIsNotExecutable() throws Exception {
+ final File dir = tempDir.newFolder();
+ dir.setExecutable(false);
+ new TelemetryJSONFilePingStore(dir, "profileName"); // expected to throw.
+ }
+
+ @Test
+ public void testStorePingStoresCorrectData() throws Exception {
+ assertStoreFileCount(0);
+
+ final String expectedID = getDocID();
+ final TelemetryPing expectedPing = new TelemetryPing("a/server/url", generateTelemetryPayload(), expectedID);
+ testStore.storePing(expectedPing);
+
+ assertStoreFileCount(1);
+ final String filename = testDir.list()[0];
+ assertTrue("Filename contains expected ID", filename.equals(expectedID));
+ final JSONObject actual = FileUtils.readJSONObjectFromFile(new File(testDir, filename));
+ assertEquals("Ping url paths are equal", expectedPing.getURLPath(), actual.getString(TelemetryJSONFilePingStore.KEY_URL_PATH));
+ assertIsGeneratedPayload(new ExtendedJSONObject(actual.getString(TelemetryJSONFilePingStore.KEY_PAYLOAD)));
+ }
+
+ @Test
+ public void testStorePingMultiplePingsStoreSeparateFiles() throws Exception {
+ assertStoreFileCount(0);
+ for (int i = 1; i < 10; ++i) {
+ testStore.storePing(new TelemetryPing("server", generateTelemetryPayload(), getDocID()));
+ assertStoreFileCount(i);
+ }
+ }
+
+ @Test
+ public void testStorePingReleasesFileLock() throws Exception {
+ assertStoreFileCount(0);
+ testStore.storePing(new TelemetryPing("server", generateTelemetryPayload(), getDocID()));
+ assertStoreFileCount(1);
+ final File file = new File(testDir, testDir.list()[0]);
+ final FileOutputStream stream = new FileOutputStream(file);
+ try {
+ assertNotNull("File lock is released after store write", stream.getChannel().tryLock());
+ } finally {
+ stream.close(); // releases lock
+ }
+ }
+
+ @Test
+ public void testGetAllPingsSavesData() throws Exception {
+ final String urlPrefix = "url";
+ writeTestPingsToStore(3, urlPrefix);
+
+ final ArrayList<TelemetryPing> pings = testStore.getAllPings();
+ for (final TelemetryPing ping : pings) {
+ assertEquals("Expected url path value received", urlPrefix + ping.getDocID(), ping.getURLPath());
+ assertIsGeneratedPayload(ping.getPayload());
+ }
+ }
+
+ @Test
+ public void testGetAllPingsIsSorted() throws Exception {
+ final List<String> storedDocIDs = writeTestPingsToStore(3, "urlPrefix");
+
+ final ArrayList<TelemetryPing> pings = testStore.getAllPings();
+ for (int i = 0; i < pings.size(); ++i) {
+ final String expectedDocID = storedDocIDs.get(i);
+ final TelemetryPing ping = pings.get(i);
+
+ assertEquals("Stored ping " + i + " retrieved in order", expectedDocID, ping.getDocID());
+ }
+ }
+
+ @Test // regression test: bug 1272817
+ public void testGetAllPingsHandlesEmptyFiles() throws Exception {
+ final int expectedPingCount = 3;
+ writeTestPingsToStore(expectedPingCount, "whatever");
+ assertTrue("Empty file is created", testStore.getPingFile(getDocID()).createNewFile());
+ assertEquals("Returned pings only contains valid files", expectedPingCount, testStore.getAllPings().size());
+ }
+
+ @Test
+ public void testMaybePrunePingsDoesNothingIfAtMax() throws Exception {
+ final int pingCount = TelemetryJSONFilePingStore.MAX_PING_COUNT;
+ writeTestPingsToStore(pingCount, "whatever");
+ assertStoreFileCount(pingCount);
+ testStore.maybePrunePings();
+ assertStoreFileCount(pingCount);
+ }
+
+ @Test
+ public void testMaybePrunePingsPrunesIfAboveMax() throws Exception {
+ final int pingCount = TelemetryJSONFilePingStore.MAX_PING_COUNT + 1;
+ final List<String> expectedDocIDs = writeTestPingsToStore(pingCount, "whatever");
+ assertStoreFileCount(pingCount);
+ testStore.maybePrunePings();
+ assertStoreFileCount(TelemetryJSONFilePingStore.MAX_PING_COUNT);
+
+ final HashSet<String> existingIDs = new HashSet<>(Arrays.asList(testDir.list()));
+ assertFalse("Oldest ping was removed", existingIDs.contains(expectedDocIDs.get(0)));
+ }
+
+ @Test
+ public void testOnUploadAttemptCompleted() throws Exception {
+ final List<String> savedDocIDs = writeTestPingsToStore(10, "url");
+ final int halfSize = savedDocIDs.size() / 2;
+ final Set<String> unuploadedPingIDs = new HashSet<>(savedDocIDs.subList(0, halfSize));
+ final Set<String> removedPingIDs = new HashSet<>(savedDocIDs.subList(halfSize, savedDocIDs.size()));
+ testStore.onUploadAttemptComplete(removedPingIDs);
+
+ for (final String unuploadedDocID : testDir.list()) {
+ assertFalse("Unuploaded ID is not in removed ping IDs", removedPingIDs.contains(unuploadedDocID));
+ assertTrue("Unuploaded ID is in unuploaded ping IDs", unuploadedPingIDs.contains(unuploadedDocID));
+ unuploadedPingIDs.remove(unuploadedDocID);
+ }
+ assertTrue("All non-successful-upload ping IDs were matched", unuploadedPingIDs.isEmpty());
+ }
+
+ @Test
+ public void testGetPingFileIsDocID() throws Exception {
+ final String docID = getDocID();
+ final File file = testStore.getPingFile(docID);
+ assertTrue("Ping filename contains ID", file.getName().equals(docID));
+ }
+
+ /**
+ * Writes pings to store without using store API with:
+ * server = urlPrefix + docID
+ * payload = generated payload
+ *
+ * The docID is stored as the filename.
+ *
+ * Note: assumes {@link TelemetryJSONFilePingStore#getPingFile(String)} works.
+ *
+ * @return a list of doc IDs saved to disk in ascending order of last modified date
+ */
+ private List<String> writeTestPingsToStore(final int count, final String urlPrefix) throws Exception {
+ final List<String> savedDocIDs = new ArrayList<>(count);
+ final long now = System.currentTimeMillis();
+ for (int i = 1; i <= count; ++i) {
+ final String docID = getDocID();
+ final JSONObject obj = new JSONObject()
+ .put(TelemetryJSONFilePingStore.KEY_URL_PATH, urlPrefix + docID)
+ .put(TelemetryJSONFilePingStore.KEY_PAYLOAD, generateTelemetryPayload());
+ final File pingFile = testStore.getPingFile(docID);
+ FileUtils.writeJSONObjectToFile(pingFile, obj);
+
+ // If we don't set an explicit time, the modified times are all equal.
+ // Also, last modified times are rounded by second.
+ assertTrue("Able to set last modified time", pingFile.setLastModified(now - (count * 10_000) + i * 10_000));
+ savedDocIDs.add(docID);
+ }
+ return savedDocIDs;
+ }
+
+ private String getDocID() {
+ return UUID.randomUUID().toString();
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/tokenserver/test/TestTokenServerClient.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/tokenserver/test/TestTokenServerClient.java
new file mode 100644
index 0000000000..03fe677947
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/tokenserver/test/TestTokenServerClient.java
@@ -0,0 +1,335 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.tokenserver.test;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.client.methods.HttpGet;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
+import ch.boye.httpclientandroidlib.entity.StringEntity;
+import ch.boye.httpclientandroidlib.message.BasicHeader;
+import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
+import ch.boye.httpclientandroidlib.message.BasicStatusLine;
+import junit.framework.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.common.log.writers.StringLogWriter;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.SyncResponse;
+import org.mozilla.gecko.tokenserver.TokenServerClient;
+import org.mozilla.gecko.tokenserver.TokenServerClient.TokenFetchResourceDelegate;
+import org.mozilla.gecko.tokenserver.TokenServerClientDelegate;
+import org.mozilla.gecko.tokenserver.TokenServerException;
+import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerConditionsRequiredException;
+import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerInvalidCredentialsException;
+import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedRequestException;
+import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedResponseException;
+import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerUnknownServiceException;
+import org.mozilla.gecko.tokenserver.TokenServerToken;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+@RunWith(TestRunner.class)
+public class TestTokenServerClient {
+ public static final String JSON = "application/json";
+ public static final String TEXT = "text/plain";
+
+ public static final String TEST_TOKEN_RESPONSE = "{\"api_endpoint\": \"https://stage-aitc1.services.mozilla.com/1.0/1659259\"," +
+ "\"duration\": 300," +
+ "\"id\": \"eySHORTENED\"," +
+ "\"key\": \"-plSHORTENED\"," +
+ "\"uid\": 1659259}";
+
+ public static final String TEST_CONDITIONS_RESPONSE = "{\"errors\":[{" +
+ "\"location\":\"header\"," +
+ "\"description\":\"Need to accept conditions\"," +
+ "\"condition_urls\":{\"tos\":\"http://url-to-tos.com\"}," +
+ "\"name\":\"X-Conditions-Accepted\"}]," +
+ "\"status\":\"error\"}";
+
+ public static final String TEST_ERROR_RESPONSE = "{\"status\": \"error\"," +
+ "\"errors\": [{\"location\": \"body\", \"name\": \"\", \"description\": \"Unauthorized EXTENDED\"}]}";
+
+ public static final String TEST_INVALID_TIMESTAMP_RESPONSE = "{\"status\": \"invalid-timestamp\", " +
+ "\"errors\": [{\"location\": \"body\", \"name\": \"\", \"description\": \"Unauthorized\"}]}";
+
+ protected TokenServerClient client;
+
+ @Before
+ public void setUp() throws Exception {
+ this.client = new TokenServerClient(new URI("http://unused.com"), Executors.newSingleThreadExecutor());
+ }
+
+ protected TokenServerToken doProcessResponse(int statusCode, String contentType, Object token)
+ throws UnsupportedEncodingException, TokenServerException {
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), statusCode, "OK"));
+
+ StringEntity stringEntity = new StringEntity(token.toString());
+ stringEntity.setContentType(contentType);
+ response.setEntity(stringEntity);
+
+ return client.processResponse(new SyncResponse(response));
+ }
+
+ @SuppressWarnings("rawtypes")
+ protected TokenServerException expectProcessResponseFailure(int statusCode, String contentType, Object token, Class klass)
+ throws TokenServerException, UnsupportedEncodingException {
+ try {
+ doProcessResponse(statusCode, contentType, token.toString());
+ fail("Expected exception of type " + klass + ".");
+
+ return null;
+ } catch (TokenServerException e) {
+ assertEquals(klass, e.getClass());
+
+ return e;
+ }
+ }
+
+ @SuppressWarnings("rawtypes")
+ protected TokenServerException expectProcessResponseFailure(Object token, Class klass)
+ throws UnsupportedEncodingException, TokenServerException {
+ return expectProcessResponseFailure(200, "application/json", token, klass);
+ }
+
+ @Test
+ public void testProcessResponseSuccess() throws Exception {
+ TokenServerToken token = doProcessResponse(200, "application/json", TEST_TOKEN_RESPONSE);
+
+ assertEquals("eySHORTENED", token.id);
+ assertEquals("-plSHORTENED", token.key);
+ assertEquals("1659259", token.uid);
+ assertEquals("https://stage-aitc1.services.mozilla.com/1.0/1659259", token.endpoint);
+ }
+
+ @Test
+ public void testProcessResponseFailure() throws Exception {
+ // Wrong Content-Type.
+ expectProcessResponseFailure(200, TEXT, new ExtendedJSONObject(), TokenServerMalformedResponseException.class);
+
+ // Not valid JSON.
+ expectProcessResponseFailure("#!", TokenServerMalformedResponseException.class);
+
+ // Status code 400.
+ expectProcessResponseFailure(400, JSON, new ExtendedJSONObject(), TokenServerMalformedRequestException.class);
+
+ // Status code 401.
+ expectProcessResponseFailure(401, JSON, new ExtendedJSONObject(), TokenServerInvalidCredentialsException.class);
+ expectProcessResponseFailure(401, JSON, TEST_INVALID_TIMESTAMP_RESPONSE, TokenServerInvalidCredentialsException.class);
+
+ // Status code 404.
+ expectProcessResponseFailure(404, JSON, new ExtendedJSONObject(), TokenServerUnknownServiceException.class);
+
+ // Status code 406, which is not specially handled, but with errors. We take
+ // care that errors are actually printed because we're going to want this to
+ // work when things go wrong.
+ StringLogWriter logWriter = new StringLogWriter();
+
+ Logger.startLoggingTo(logWriter);
+ try {
+ expectProcessResponseFailure(406, JSON, TEST_ERROR_RESPONSE, TokenServerException.class);
+
+ assertTrue(logWriter.toString().contains("Unauthorized EXTENDED"));
+ } finally {
+ Logger.stopLoggingTo(logWriter);
+ }
+
+ // Status code 503.
+ expectProcessResponseFailure(503, JSON, new ExtendedJSONObject(), TokenServerException.class);
+ }
+
+ @Test
+ public void testProcessResponseConditionsRequired() throws Exception {
+
+ // Status code 403: conditions need to be accepted, but malformed (no urls).
+ expectProcessResponseFailure(403, JSON, new ExtendedJSONObject(), TokenServerMalformedResponseException.class);
+
+ // Status code 403, with urls.
+ TokenServerConditionsRequiredException e = (TokenServerConditionsRequiredException)
+ expectProcessResponseFailure(403, JSON, TEST_CONDITIONS_RESPONSE, TokenServerConditionsRequiredException.class);
+
+ ExtendedJSONObject expectedUrls = new ExtendedJSONObject();
+ expectedUrls.put("tos", "http://url-to-tos.com");
+ assertEquals(expectedUrls.toString(), e.conditionUrls.toString());
+ }
+
+ @Test
+ public void testProcessResponseMalformedToken() throws Exception {
+ ExtendedJSONObject token;
+
+ // Missing key.
+ token = new ExtendedJSONObject(TEST_TOKEN_RESPONSE);
+ token.remove("api_endpoint");
+ expectProcessResponseFailure(token, TokenServerMalformedResponseException.class);
+
+ // Key has wrong type; expected String.
+ token = new ExtendedJSONObject(TEST_TOKEN_RESPONSE);
+ token.put("api_endpoint", new ExtendedJSONObject());
+ expectProcessResponseFailure(token, TokenServerMalformedResponseException.class);
+
+ // Key has wrong type; expected number.
+ token = new ExtendedJSONObject(TEST_TOKEN_RESPONSE);
+ token.put("uid", "NON NUMERIC");
+ expectProcessResponseFailure(token, TokenServerMalformedResponseException.class);
+ }
+
+ private class MockBaseResource extends BaseResource {
+ public MockBaseResource(String uri) throws URISyntaxException {
+ super(uri);
+ this.request = new HttpGet(this.uri);
+ }
+
+ public HttpRequestBase prepareHeadersAndReturn() throws Exception {
+ super.prepareClient();
+ return request;
+ }
+ }
+
+ @Test
+ public void testClientStateHeader() throws Exception {
+ String assertion = "assertion";
+ String clientState = "abcdef";
+ MockBaseResource resource = new MockBaseResource("http://unused.local/");
+
+ TokenServerClientDelegate delegate = new TokenServerClientDelegate() {
+ @Override
+ public void handleSuccess(TokenServerToken token) {
+ }
+
+ @Override
+ public void handleFailure(TokenServerException e) {
+ }
+
+ @Override
+ public void handleError(Exception e) {
+ }
+
+ @Override
+ public void handleBackoff(int backoffSeconds) {
+ }
+
+ @Override
+ public String getUserAgent() {
+ return null;
+ }
+ };
+
+ resource.delegate = new TokenServerClient.TokenFetchResourceDelegate(client, resource, delegate, assertion, clientState , true) {
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return null;
+ }
+ };
+
+ HttpRequestBase request = resource.prepareHeadersAndReturn();
+ Assert.assertEquals("abcdef", request.getFirstHeader("X-Client-State").getValue());
+ Assert.assertEquals("1", request.getFirstHeader("X-Conditions-Accepted").getValue());
+ }
+
+ public static class MockTokenServerClient extends TokenServerClient {
+ public MockTokenServerClient(URI uri, Executor executor) {
+ super(uri, executor);
+ }
+ }
+
+ public static final class MockTokenServerClientDelegate implements
+ TokenServerClientDelegate {
+ public volatile boolean backoffCalled;
+ public volatile boolean succeeded;
+ public volatile int backoff;
+
+ @Override
+ public String getUserAgent() {
+ return null;
+ }
+
+ @Override
+ public void handleSuccess(TokenServerToken token) {
+ succeeded = true;
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ @Override
+ public void handleFailure(TokenServerException e) {
+ succeeded = false;
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ @Override
+ public void handleError(Exception e) {
+ succeeded = false;
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+
+ @Override
+ public void handleBackoff(int backoffSeconds) {
+ backoffCalled = true;
+ backoff = backoffSeconds;
+ }
+ }
+
+ private void expectDelegateCalls(URI uri, MockTokenServerClient client, int code, Header header, String body, boolean succeeded, long backoff, boolean expectBackoff) throws UnsupportedEncodingException {
+ final BaseResource resource = new BaseResource(uri);
+ final String assertion = "someassertion";
+ final String clientState = "abcdefabcdefabcdefabcdefabcdefab";
+ final boolean conditionsAccepted = true;
+
+ MockTokenServerClientDelegate delegate = new MockTokenServerClientDelegate();
+
+ final TokenFetchResourceDelegate tokenFetchResourceDelegate = new TokenServerClient.TokenFetchResourceDelegate(client, resource, delegate, assertion, clientState, conditionsAccepted);
+
+ final BasicStatusLine statusline = new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), code, "Whatever");
+ final HttpResponse response = new BasicHttpResponse(statusline);
+ response.setHeader(header);
+ if (body != null) {
+ final StringEntity entity = new StringEntity(body);
+ entity.setContentType("application/json");
+ response.setEntity(entity);
+ }
+
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ tokenFetchResourceDelegate.handleHttpResponse(response);
+ }
+ });
+
+ assertEquals(expectBackoff, delegate.backoffCalled);
+ assertEquals(backoff, delegate.backoff);
+ assertEquals(succeeded, delegate.succeeded);
+ }
+
+ @Test
+ public void testBackoffHandling() throws URISyntaxException, UnsupportedEncodingException {
+ final URI uri = new URI("http://unused.com");
+ final MockTokenServerClient client = new MockTokenServerClient(uri, Executors.newSingleThreadExecutor());
+
+ // Even the 200 code here is false because the body is malformed.
+ expectDelegateCalls(uri, client, 200, new BasicHeader("x-backoff", "13"), "baaaa", false, 13, true);
+ expectDelegateCalls(uri, client, 400, new BasicHeader("X-Weave-Backoff", "15"), null, false, 15, true);
+ expectDelegateCalls(uri, client, 503, new BasicHeader("retry-after", "3"), null, false, 3, true);
+
+ // Retry-After is only processed on 503.
+ expectDelegateCalls(uri, client, 200, new BasicHeader("retry-after", "13"), null, false, 0, false);
+
+ // Now let's try one with a valid body.
+ expectDelegateCalls(uri, client, 200, new BasicHeader("X-Backoff", "1234"), TEST_TOKEN_RESPONSE, true, 1234, true);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/NetworkUtilsTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/NetworkUtilsTest.java
new file mode 100644
index 0000000000..fb2cffc920
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/NetworkUtilsTest.java
@@ -0,0 +1,185 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.util;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.telephony.TelephonyManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.util.NetworkUtils.*;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.internal.ShadowExtractor;
+import org.robolectric.shadows.ShadowConnectivityManager;
+import org.robolectric.shadows.ShadowNetworkInfo;
+
+import static org.junit.Assert.*;
+
+@RunWith(TestRunner.class)
+public class NetworkUtilsTest {
+ private ConnectivityManager connectivityManager;
+ private ShadowConnectivityManager shadowConnectivityManager;
+
+ @Before
+ public void setUp() {
+ connectivityManager = (ConnectivityManager) RuntimeEnvironment.application.getSystemService(Context.CONNECTIVITY_SERVICE);
+
+ // Not using Shadows.shadowOf(connectivityManager) because of Robolectric bug when using API23+
+ // See: https://github.com/robolectric/robolectric/issues/1862
+ shadowConnectivityManager = (ShadowConnectivityManager) ShadowExtractor.extract(connectivityManager);
+ }
+
+ @Test
+ public void testIsConnected() throws Exception {
+ assertFalse(NetworkUtils.isConnected((ConnectivityManager) null));
+
+ shadowConnectivityManager.setActiveNetworkInfo(null);
+ assertFalse(NetworkUtils.isConnected(connectivityManager));
+
+ shadowConnectivityManager.setActiveNetworkInfo(
+ ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_WIFI, 0, true, true)
+ );
+ assertTrue(NetworkUtils.isConnected(connectivityManager));
+
+ shadowConnectivityManager.setActiveNetworkInfo(
+ ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.DISCONNECTED, ConnectivityManager.TYPE_WIFI, 0, true, false)
+ );
+ assertFalse(NetworkUtils.isConnected(connectivityManager));
+ }
+
+ @Test
+ public void testGetConnectionSubType() throws Exception {
+ assertEquals(ConnectionSubType.UNKNOWN, NetworkUtils.getConnectionSubType(null));
+
+ shadowConnectivityManager.setActiveNetworkInfo(null);
+ assertEquals(ConnectionSubType.UNKNOWN, NetworkUtils.getConnectionSubType(connectivityManager));
+
+ // We don't seem to care about figuring out all connection types. So...
+ shadowConnectivityManager.setActiveNetworkInfo(
+ ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_VPN, 0, true, true)
+ );
+ assertEquals(ConnectionSubType.UNKNOWN, NetworkUtils.getConnectionSubType(connectivityManager));
+
+ // But anything below we should recognize.
+ shadowConnectivityManager.setActiveNetworkInfo(
+ ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_ETHERNET, 0, true, true)
+ );
+ assertEquals(ConnectionSubType.ETHERNET, NetworkUtils.getConnectionSubType(connectivityManager));
+
+ shadowConnectivityManager.setActiveNetworkInfo(
+ ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_WIFI, 0, true, true)
+ );
+ assertEquals(ConnectionSubType.WIFI, NetworkUtils.getConnectionSubType(connectivityManager));
+
+ shadowConnectivityManager.setActiveNetworkInfo(
+ ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_WIMAX, 0, true, true)
+ );
+ assertEquals(ConnectionSubType.WIMAX, NetworkUtils.getConnectionSubType(connectivityManager));
+
+ // Unknown mobile
+ shadowConnectivityManager.setActiveNetworkInfo(
+ ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_UNKNOWN, true, true)
+ );
+ assertEquals(ConnectionSubType.UNKNOWN, NetworkUtils.getConnectionSubType(connectivityManager));
+
+ // 2G mobile types
+ int[] cell2gTypes = new int[] {
+ TelephonyManager.NETWORK_TYPE_GPRS,
+ TelephonyManager.NETWORK_TYPE_EDGE,
+ TelephonyManager.NETWORK_TYPE_CDMA,
+ TelephonyManager.NETWORK_TYPE_1xRTT,
+ TelephonyManager.NETWORK_TYPE_IDEN
+ };
+ for (int i = 0; i < cell2gTypes.length; i++) {
+ shadowConnectivityManager.setActiveNetworkInfo(
+ ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_MOBILE, cell2gTypes[i], true, true)
+ );
+ assertEquals(ConnectionSubType.CELL_2G, NetworkUtils.getConnectionSubType(connectivityManager));
+ }
+
+ // 3G mobile types
+ int[] cell3gTypes = new int[] {
+ TelephonyManager.NETWORK_TYPE_UMTS,
+ TelephonyManager.NETWORK_TYPE_EVDO_0,
+ TelephonyManager.NETWORK_TYPE_EVDO_A,
+ TelephonyManager.NETWORK_TYPE_HSDPA,
+ TelephonyManager.NETWORK_TYPE_HSUPA,
+ TelephonyManager.NETWORK_TYPE_HSPA,
+ TelephonyManager.NETWORK_TYPE_EVDO_B,
+ TelephonyManager.NETWORK_TYPE_EHRPD,
+ TelephonyManager.NETWORK_TYPE_HSPAP
+ };
+ for (int i = 0; i < cell3gTypes.length; i++) {
+ shadowConnectivityManager.setActiveNetworkInfo(
+ ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_MOBILE, cell3gTypes[i], true, true)
+ );
+ assertEquals(ConnectionSubType.CELL_3G, NetworkUtils.getConnectionSubType(connectivityManager));
+ }
+
+ // 4G mobile type
+ shadowConnectivityManager.setActiveNetworkInfo(
+ ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_LTE, true, true)
+ );
+ assertEquals(ConnectionSubType.CELL_4G, NetworkUtils.getConnectionSubType(connectivityManager));
+ }
+
+ @Test
+ public void testGetConnectionType() {
+ shadowConnectivityManager.setActiveNetworkInfo(null);
+ assertEquals(ConnectionType.NONE, NetworkUtils.getConnectionType(connectivityManager));
+ assertEquals(ConnectionType.NONE, NetworkUtils.getConnectionType(null));
+
+ shadowConnectivityManager.setActiveNetworkInfo(
+ ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_VPN, 0, true, true)
+ );
+ assertEquals(ConnectionType.OTHER, NetworkUtils.getConnectionType(connectivityManager));
+
+ shadowConnectivityManager.setActiveNetworkInfo(
+ ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_WIFI, 0, true, true)
+ );
+ assertEquals(ConnectionType.WIFI, NetworkUtils.getConnectionType(connectivityManager));
+
+ shadowConnectivityManager.setActiveNetworkInfo(
+ ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_MOBILE, 0, true, true)
+ );
+ assertEquals(ConnectionType.CELLULAR, NetworkUtils.getConnectionType(connectivityManager));
+
+ shadowConnectivityManager.setActiveNetworkInfo(
+ ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_ETHERNET, 0, true, true)
+ );
+ assertEquals(ConnectionType.ETHERNET, NetworkUtils.getConnectionType(connectivityManager));
+
+ shadowConnectivityManager.setActiveNetworkInfo(
+ ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_BLUETOOTH, 0, true, true)
+ );
+ assertEquals(ConnectionType.BLUETOOTH, NetworkUtils.getConnectionType(connectivityManager));
+
+ shadowConnectivityManager.setActiveNetworkInfo(
+ ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_WIMAX, 0, true, true)
+ );
+ assertEquals(ConnectionType.CELLULAR, NetworkUtils.getConnectionType(connectivityManager));
+ }
+
+ @Test
+ public void testGetNetworkStatus() {
+ assertEquals(NetworkStatus.UNKNOWN, NetworkUtils.getNetworkStatus(null));
+
+ shadowConnectivityManager.setActiveNetworkInfo(null);
+ assertEquals(NetworkStatus.DOWN, NetworkUtils.getNetworkStatus(connectivityManager));
+
+ shadowConnectivityManager.setActiveNetworkInfo(
+ ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTING, ConnectivityManager.TYPE_MOBILE, 0, true, false)
+ );
+ assertEquals(NetworkStatus.DOWN, NetworkUtils.getNetworkStatus(connectivityManager));
+
+ shadowConnectivityManager.setActiveNetworkInfo(
+ ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_MOBILE, 0, true, true)
+ );
+ assertEquals(NetworkStatus.UP, NetworkUtils.getNetworkStatus(connectivityManager));
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestContextUtils.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestContextUtils.java
new file mode 100644
index 0000000000..56b69b6846
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestContextUtils.java
@@ -0,0 +1,38 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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 org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import static org.junit.Assert.*;
+
+/**
+ * Unit test methods of the ContextUtils class.
+ */
+@RunWith(TestRunner.class)
+public class TestContextUtils {
+
+ private Context context;
+
+ @Before
+ public void setUp() {
+ context = RuntimeEnvironment.application;
+ }
+
+ @Test
+ public void testGetPackageInstallTimeReturnsReasonableValue() throws Exception {
+ // At the time of writing, Robolectric's value is 0, which is reasonable.
+ final long installTime = ContextUtils.getCurrentPackageInfo(context).firstInstallTime;
+ assertTrue("Package install time is positive", installTime >= 0);
+ assertTrue("Package install time is less than current time", installTime < System.currentTimeMillis());
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestDateUtil.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestDateUtil.java
new file mode 100644
index 0000000000..a93c81ef0f
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestDateUtil.java
@@ -0,0 +1,89 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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 org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Unit tests for date utilities.
+ */
+@RunWith(TestRunner.class)
+public class TestDateUtil {
+ @Test
+ public void testGetDateInHTTPFormatGMT() {
+ final TimeZone gmt = TimeZone.getTimeZone("GMT");
+ final GregorianCalendar calendar = new GregorianCalendar(gmt, Locale.US);
+ calendar.set(2011, Calendar.FEBRUARY, 1, 14, 0, 0);
+ final String expectedDate = "Tue, 01 Feb 2011 14:00:00 GMT";
+
+ final String actualDate = DateUtil.getDateInHTTPFormat(calendar.getTime());
+ assertEquals("Returned date is expected", expectedDate, actualDate);
+ }
+
+ @Test
+ public void testGetDateInHTTPFormatNonGMT() {
+ final TimeZone kst = TimeZone.getTimeZone("Asia/Seoul"); // no daylight savings time.
+ final GregorianCalendar calendar = new GregorianCalendar(kst, Locale.US);
+ calendar.set(2011, Calendar.FEBRUARY, 1, 14, 0, 0);
+ final String expectedDate = "Tue, 01 Feb 2011 05:00:00 GMT";
+
+ final String actualDate = DateUtil.getDateInHTTPFormat(calendar.getTime());
+ assertEquals("Returned date is expected", expectedDate, actualDate);
+ }
+
+ @Test
+ public void testGetTimezoneOffsetInMinutes() {
+ assertEquals("GMT has no offset", 0, DateUtil.getTimezoneOffsetInMinutes(TimeZone.getTimeZone("GMT")));
+
+ // We use custom timezones because they don't have daylight savings time.
+ assertEquals("Offset for GMT-8 is correct",
+ -480, DateUtil.getTimezoneOffsetInMinutes(TimeZone.getTimeZone("GMT-8")));
+ assertEquals("Offset for GMT+12:45 is correct",
+ 765, DateUtil.getTimezoneOffsetInMinutes(TimeZone.getTimeZone("GMT+12:45")));
+
+ // We use a non-custom timezone without DST.
+ assertEquals("Offset for KST is correct",
+ 540, DateUtil.getTimezoneOffsetInMinutes(TimeZone.getTimeZone("Asia/Seoul")));
+ }
+
+ @Test
+ public void testGetTimezoneOffsetInMinutesForGivenDateNoDaylightSavingsTime() {
+ final TimeZone kst = TimeZone.getTimeZone("Asia/Seoul");
+ final Calendar[] calendars =
+ new Calendar[] { getCalendarForMonth(Calendar.DECEMBER), getCalendarForMonth(Calendar.AUGUST) };
+ for (final Calendar cal : calendars) {
+ cal.setTimeZone(kst);
+ assertEquals("Offset for KST does not change with daylight savings time",
+ 540, DateUtil.getTimezoneOffsetInMinutesForGivenDate(cal));
+ }
+ }
+
+ @Test
+ public void testGetTimezoneOffsetInMinutesForGivenDateDaylightSavingsTime() {
+ final TimeZone pacificTimeZone = TimeZone.getTimeZone("America/Los_Angeles");
+ final Calendar pstCalendar = getCalendarForMonth(Calendar.DECEMBER);
+ final Calendar pdtCalendar = getCalendarForMonth(Calendar.AUGUST);
+ pstCalendar.setTimeZone(pacificTimeZone);
+ pdtCalendar.setTimeZone(pacificTimeZone);
+ assertEquals("Offset for PST is correct", -480, DateUtil.getTimezoneOffsetInMinutesForGivenDate(pstCalendar));
+ assertEquals("Offset for PDT is correct", -420, DateUtil.getTimezoneOffsetInMinutesForGivenDate(pdtCalendar));
+
+ }
+
+ private Calendar getCalendarForMonth(final int month) {
+ return new GregorianCalendar(2000, month, 1);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestFileUtils.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestFileUtils.java
new file mode 100644
index 0000000000..88fa7307d0
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestFileUtils.java
@@ -0,0 +1,339 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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 org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.util.FileUtils.FileLastModifiedComparator;
+import org.mozilla.gecko.util.FileUtils.FilenameRegexFilter;
+import org.mozilla.gecko.util.FileUtils.FilenameWhitelistFilter;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import static junit.framework.Assert.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * Tests the utilities in {@link FileUtils}.
+ */
+@RunWith(TestRunner.class)
+public class TestFileUtils {
+
+ private static final Charset CHARSET = Charset.forName("UTF-8");
+
+ @Rule
+ public TemporaryFolder tempDir = new TemporaryFolder();
+ public File testFile;
+ public File nonExistentFile;
+
+ @Before
+ public void setUp() throws Exception {
+ testFile = tempDir.newFile();
+ nonExistentFile = new File(tempDir.getRoot(), "non-existent-file");
+ }
+
+ @Test
+ public void testReadJSONObjectFromFile() throws Exception {
+ final JSONObject expected = new JSONObject("{\"str\": \"some str\"}");
+ writeStringToFile(testFile, expected.toString());
+
+ final JSONObject actual = FileUtils.readJSONObjectFromFile(testFile);
+ assertEquals("JSON contains expected str", expected.getString("str"), actual.getString("str"));
+ }
+
+ @Test(expected=IOException.class)
+ public void testReadJSONObjectFromFileEmptyFile() throws Exception {
+ assertEquals("Test file is empty", 0, testFile.length());
+ FileUtils.readJSONObjectFromFile(testFile); // expected to throw
+ }
+
+ @Test(expected=JSONException.class)
+ public void testReadJSONObjectFromFileInvalidJSON() throws Exception {
+ writeStringToFile(testFile, "not a json str");
+ FileUtils.readJSONObjectFromFile(testFile); // expected to throw
+ }
+
+ @Test
+ public void testReadStringFromFileReadsData() throws Exception {
+ final String expected = "String to write contains hard characters: !\n \\s..\"'\u00f1";
+ writeStringToFile(testFile, expected);
+
+ final String actual = FileUtils.readStringFromFile(testFile);
+ assertEquals("Read content matches written content", expected, actual);
+ }
+
+ @Test
+ public void testReadStringFromFileEmptyFile() throws Exception {
+ assertEquals("Test file is empty", 0, testFile.length());
+
+ final String actual = FileUtils.readStringFromFile(testFile);
+ assertEquals("Read content is empty", "", actual);
+ }
+
+ @Test(expected=FileNotFoundException.class)
+ public void testReadStringFromNonExistentFile() throws Exception {
+ assertFalse("File does not exist", nonExistentFile.exists());
+ FileUtils.readStringFromFile(nonExistentFile);
+ }
+
+ @Test
+ public void testReadStringFromInputStreamAndCloseStreamBufferLenIsFileLen() throws Exception {
+ final String expected = "String to write contains hard characters: !\n \\s..\"'\u00f1";
+ writeStringToFile(testFile, expected);
+
+ final FileInputStream stream = new FileInputStream(testFile);
+ final String actual = FileUtils.readStringFromInputStreamAndCloseStream(stream, expected.length());
+ assertEquals("Read content matches written content", expected, actual);
+ }
+
+ @Test
+ public void testReadStringFromInputStreamAndCloseStreamBufferLenIsBiggerThanFile() throws Exception {
+ final String expected = "aoeuhtns";
+ writeStringToFile(testFile, expected);
+
+ final FileInputStream stream = new FileInputStream(testFile);
+ final String actual = FileUtils.readStringFromInputStreamAndCloseStream(stream, expected.length() + 1024);
+ assertEquals("Read content matches written content", expected, actual);
+ }
+
+ @Test
+ public void testReadStringFromInputStreamAndCloseStreamBufferLenIsSmallerThanFile() throws Exception {
+ final String expected = "aoeuhtns aoeusth aoeusth aoeusnth aoeusth aoeusnth aoesuth";
+ writeStringToFile(testFile, expected);
+
+ final FileInputStream stream = new FileInputStream(testFile);
+ final String actual = FileUtils.readStringFromInputStreamAndCloseStream(stream, 8);
+ assertEquals("Read content matches written content", expected, actual);
+ }
+
+ @Test(expected=IllegalArgumentException.class)
+ public void testReadStringFromInputStreamAndCloseStreamBufferLenIsZero() throws Exception {
+ final String expected = "aoeuhtns aoeusth aoeusth aoeusnth aoeusth aoeusnth aoesuth";
+ writeStringToFile(testFile, expected);
+
+ final FileInputStream stream = new FileInputStream(testFile);
+ FileUtils.readStringFromInputStreamAndCloseStream(stream, 0); // expected to throw.
+ }
+
+ @Test
+ public void testReadStringFromInputStreamAndCloseStreamIsEmptyStream() throws Exception {
+ assertTrue("Test file exists", testFile.exists());
+ assertEquals("Test file is empty", 0, testFile.length());
+
+ final FileInputStream stream = new FileInputStream(testFile);
+ final String actual = FileUtils.readStringFromInputStreamAndCloseStream(stream, 8);
+ assertEquals("Read content from stream is empty", "", actual);
+ }
+
+ @Test(expected=IOException.class)
+ public void testReadStringFromInputStreamAndCloseStreamClosesStream() throws Exception {
+ final String expected = "String to write contains hard characters: !\n \\s..\"'\u00f1";
+ writeStringToFile(testFile, expected);
+
+ final FileInputStream stream = new FileInputStream(testFile);
+ try {
+ stream.read(); // should not throw because stream is open.
+ FileUtils.readStringFromInputStreamAndCloseStream(stream, expected.length());
+ } catch (final IOException e) {
+ fail("Did not expect method to throw when writing file: " + e);
+ }
+
+ stream.read(); // expected to throw because stream is closed.
+ }
+
+ @Test
+ public void testWriteStringToOutputStreamAndCloseStreamWritesData() throws Exception {
+ final String expected = "A string with some data in it! \u00f1 \n";
+ final FileOutputStream fos = new FileOutputStream(testFile, false);
+ FileUtils.writeStringToOutputStreamAndCloseStream(fos, expected);
+
+ assertTrue("Written file exists", testFile.exists());
+ assertEquals("Read data equals written data", expected, readStringFromFile(testFile, expected.length()));
+ }
+
+ @Test(expected=IOException.class)
+ public void testWriteStringToOutputStreamAndCloseStreamClosesStream() throws Exception {
+ final FileOutputStream fos = new FileOutputStream(testFile, false);
+ try {
+ fos.write('c'); // should not throw because stream is open.
+ FileUtils.writeStringToOutputStreamAndCloseStream(fos, "some string with data");
+ } catch (final IOException e) {
+ fail("Did not expect method to throw when writing file: " + e);
+ }
+
+ fos.write('c'); // expected to throw because stream is closed.
+ }
+
+ /**
+ * The Writer we wrap our stream in can throw in .close(), preventing the underlying stream from closing.
+ * I added code to prevent ensure we close if the writer .close() throws.
+ *
+ * I wrote this test to test that code, however, we'd have to mock the writer [1] and that isn't straight-forward.
+ * I left this test around because it's a good test of other code.
+ *
+ * [1]: We thought we could mock FileOutputStream.flush but it's only flushed if the Writer thinks it should be
+ * flushed. We can write directly to the Stream, but that doesn't change the Writer state and doesn't affect whether
+ * it thinks it should be flushed.
+ */
+ @Test(expected=IOException.class)
+ public void testWriteStringToOutputStreamAndCloseStreamClosesStreamIfWriterThrows() throws Exception {
+ final FileOutputStream fos = mock(FileOutputStream.class);
+ doThrow(IOException.class).when(fos).write(any(byte[].class), anyInt(), anyInt());
+ doThrow(IOException.class).when(fos).write(anyInt());
+ doThrow(IOException.class).when(fos).write(any(byte[].class));
+
+ boolean exceptionCaught = false;
+ try {
+ FileUtils.writeStringToOutputStreamAndCloseStream(fos, "some string with data");
+ } catch (final IOException e) {
+ exceptionCaught = true;
+ }
+ assertTrue("Exception caught during tested method", exceptionCaught); // not strictly necessary but documents assumptions
+
+ fos.write('c'); // expected to throw because stream is closed.
+ }
+
+ @Test
+ public void testWriteStringToFile() throws Exception {
+ final String expected = "String to write contains hard characters: !\n \\s..\"'\u00f1";
+ FileUtils.writeStringToFile(testFile, expected);
+
+ assertTrue("Written file exists", testFile.exists());
+ assertEquals("Read data equals written data", expected, readStringFromFile(testFile, expected.length()));
+ }
+
+ @Test
+ public void testWriteStringToFileEmptyString() throws Exception {
+ final String expected = "";
+ FileUtils.writeStringToFile(testFile, expected);
+
+ assertTrue("Written file exists", testFile.exists());
+ assertEquals("Written file is empty", 0, testFile.length());
+ assertEquals("Read data equals written (empty) data", expected, readStringFromFile(testFile, expected.length()));
+ }
+
+ @Test
+ public void testWriteStringToFileCreatesNewFile() throws Exception {
+ final String expected = "some str to write";
+ assertFalse("Non existent file does not exist", nonExistentFile.exists());
+ FileUtils.writeStringToFile(nonExistentFile, expected); // expected to create file
+
+ assertTrue("Written file was created", nonExistentFile.exists());
+ assertEquals("Read data equals written data", expected, readStringFromFile(nonExistentFile, (int) nonExistentFile.length()));
+ }
+
+ @Test
+ public void testWriteStringToFileOverwritesFile() throws Exception {
+ writeStringToFile(testFile, "data");
+
+ final String expected = "some str to write";
+ FileUtils.writeStringToFile(testFile, expected);
+
+ assertTrue("Written file was created", testFile.exists());
+ assertEquals("Read data equals written data", expected, readStringFromFile(testFile, (int) testFile.length()));
+ }
+
+ @Test
+ public void testWriteJSONObjectToFile() throws Exception {
+ final JSONObject expected = new JSONObject()
+ .put("int", 1)
+ .put("str", "1")
+ .put("bool", true)
+ .put("null", JSONObject.NULL)
+ .put("raw null", null);
+ FileUtils.writeJSONObjectToFile(testFile, expected);
+
+ assertTrue("Written file exists", testFile.exists());
+
+ // JSONObject.equals compares references so we have to assert each key individually. >:(
+ final JSONObject actual = new JSONObject(readStringFromFile(testFile, (int) testFile.length()));
+ assertEquals(1, actual.getInt("int"));
+ assertEquals("1", actual.getString("str"));
+ assertEquals(true, actual.getBoolean("bool"));
+ assertEquals(JSONObject.NULL, actual.get("null"));
+ assertFalse(actual.has("raw null"));
+ }
+
+ // Since the read methods may not be tested yet.
+ private static String readStringFromFile(final File file, final int bufferLen) throws IOException {
+ final char[] buffer = new char[bufferLen];
+ try (InputStreamReader reader = new InputStreamReader(new FileInputStream(file), Charset.forName("UTF-8"))) {
+ reader.read(buffer, 0, buffer.length);
+ }
+ return new String(buffer);
+ }
+
+ // Since the write methods may not be tested yet.
+ private static void writeStringToFile(final File file, final String str) throws IOException {
+ try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file, false), CHARSET)) {
+ writer.write(str);
+ }
+ assertTrue("Written file from helper method exists", file.exists());
+ }
+
+ @Test
+ public void testFilenameWhitelistFilter() {
+ final String[] expectedToAccept = new String[] { "one", "two", "three" };
+ final Set<String> whitelist = new HashSet<>(Arrays.asList(expectedToAccept));
+ final FilenameWhitelistFilter testFilter = new FilenameWhitelistFilter(whitelist);
+ for (final String str : expectedToAccept) {
+ assertTrue("Filename, " + str + ", in whitelist is accepted", testFilter.accept(testFile, str));
+ }
+
+ final String[] notExpectedToAccept = new String[] { "not-in-whitelist", "meh", "whatever" };
+ for (final String str : notExpectedToAccept) {
+ assertFalse("Filename, " + str + ", not in whitelist is not accepted", testFilter.accept(testFile, str));
+ }
+ }
+
+ @Test
+ public void testFilenameRegexFilter() {
+ final Pattern pattern = Pattern.compile("[a-z]{1,6}");
+ final FilenameRegexFilter testFilter = new FilenameRegexFilter(pattern);
+ final String[] expectedToAccept = new String[] { "duckie", "goes", "quack" };
+ for (final String str : expectedToAccept) {
+ assertTrue("Filename, " + str + ", matching regex expected to accept", testFilter.accept(testFile, str));
+ }
+
+ final String[] notExpectedToAccept = new String[] { "DUCKIE", "1337", "2fast" };
+ for (final String str : notExpectedToAccept) {
+ assertFalse("Filename, " + str + ", not matching regex not expected to accept", testFilter.accept(testFile, str));
+ }
+ }
+
+ @Test
+ public void testFileLastModifiedComparator() {
+ final FileLastModifiedComparator testComparator = new FileLastModifiedComparator();
+ final File oldFile = mock(File.class);
+ final File newFile = mock(File.class);
+ final File equallyNewFile = mock(File.class);
+ when(oldFile.lastModified()).thenReturn(10L);
+ when(newFile.lastModified()).thenReturn(100L);
+ when(equallyNewFile.lastModified()).thenReturn(100L);
+
+ assertTrue("Old file is less than new file", testComparator.compare(oldFile, newFile) < 0);
+ assertTrue("New file is greater than old file", testComparator.compare(newFile, oldFile) > 0);
+ assertTrue("New files are equal", testComparator.compare(newFile, equallyNewFile) == 0);
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestIntentUtils.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestIntentUtils.java
new file mode 100644
index 0000000000..1868214512
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestIntentUtils.java
@@ -0,0 +1,73 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.Intent;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.mozglue.SafeIntent;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests for the Intent utilities.
+ */
+@RunWith(TestRunner.class)
+public class TestIntentUtils {
+
+ private static final Map<String, String> TEST_ENV_VAR_MAP;
+ static {
+ final HashMap<String, String> tempMap = new HashMap<>();
+ tempMap.put("ZERO", "0");
+ tempMap.put("ONE", "1");
+ tempMap.put("STRING", "TEXT");
+ tempMap.put("L_WHITESPACE", " LEFT");
+ tempMap.put("R_WHITESPACE", "RIGHT ");
+ tempMap.put("ALL_WHITESPACE", " ALL ");
+ tempMap.put("WHITESPACE_IN_VALUE", "IN THE MIDDLE");
+ tempMap.put("WHITESPACE IN KEY", "IS_PROBABLY_NOT_VALID_ANYWAY");
+ tempMap.put("BLANK_VAL", "");
+ TEST_ENV_VAR_MAP = Collections.unmodifiableMap(tempMap);
+ }
+
+ private Intent testIntent;
+
+ @Before
+ public void setUp() throws Exception {
+ testIntent = getIntentWithTestData();
+ }
+
+ private static Intent getIntentWithTestData() {
+ final Intent out = new Intent(Intent.ACTION_VIEW);
+ int i = 0;
+ for (final String key : TEST_ENV_VAR_MAP.keySet()) {
+ final String value = key + "=" + TEST_ENV_VAR_MAP.get(key);
+ out.putExtra("env" + i, value);
+ i += 1;
+ }
+ return out;
+ }
+
+ @Test
+ public void testGetEnvVarMap() throws Exception {
+ final HashMap<String, String> actual = IntentUtils.getEnvVarMap(new SafeIntent(testIntent));
+ for (final String actualEnvVarName : actual.keySet()) {
+ assertTrue("Actual key exists in test data: " + actualEnvVarName,
+ TEST_ENV_VAR_MAP.containsKey(actualEnvVarName));
+
+ final String expectedValue = TEST_ENV_VAR_MAP.get(actualEnvVarName);
+ final String actualValue = actual.get(actualEnvVarName);
+ assertEquals("Actual env var value matches test data", expectedValue, actualValue);
+ }
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestStringUtils.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestStringUtils.java
new file mode 100644
index 0000000000..ee0a705c75
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestStringUtils.java
@@ -0,0 +1,122 @@
+/* -*- 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 org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
+public class TestStringUtils {
+ @Test
+ public void testIsHttpOrHttps() {
+ // No value
+ assertFalse(StringUtils.isHttpOrHttps(null));
+ assertFalse(StringUtils.isHttpOrHttps(""));
+
+ // Garbage
+ assertFalse(StringUtils.isHttpOrHttps("lksdjflasuf"));
+
+ // URLs with http/https
+ assertTrue(StringUtils.isHttpOrHttps("https://www.google.com"));
+ assertTrue(StringUtils.isHttpOrHttps("http://www.facebook.com"));
+ assertTrue(StringUtils.isHttpOrHttps("https://mozilla.org/en-US/firefox/products/"));
+
+ // IP addresses
+ assertTrue(StringUtils.isHttpOrHttps("https://192.168.0.1"));
+ assertTrue(StringUtils.isHttpOrHttps("http://63.245.215.20/en-US/firefox/products"));
+
+ // Other protocols
+ assertFalse(StringUtils.isHttpOrHttps("ftp://people.mozilla.org"));
+ assertFalse(StringUtils.isHttpOrHttps("javascript:window.google.com"));
+ assertFalse(StringUtils.isHttpOrHttps("tel://1234567890"));
+
+ // No scheme
+ assertFalse(StringUtils.isHttpOrHttps("google.com"));
+ assertFalse(StringUtils.isHttpOrHttps("git@github.com:mozilla/gecko-dev.git"));
+ }
+
+ @Test
+ public void testStripRef() {
+ assertEquals(StringUtils.stripRef(null), null);
+ assertEquals(StringUtils.stripRef(""), "");
+
+ assertEquals(StringUtils.stripRef("??AAABBBCCC"), "??AAABBBCCC");
+ assertEquals(StringUtils.stripRef("https://mozilla.org"), "https://mozilla.org");
+ assertEquals(StringUtils.stripRef("https://mozilla.org#BBBB"), "https://mozilla.org");
+ assertEquals(StringUtils.stripRef("https://mozilla.org/#BBBB"), "https://mozilla.org/");
+ }
+
+ @Test
+ public void testStripScheme() {
+ assertEquals("mozilla.org", StringUtils.stripScheme("http://mozilla.org"));
+ assertEquals("mozilla.org", StringUtils.stripScheme("http://mozilla.org/"));
+ assertEquals("https://mozilla.org", StringUtils.stripScheme("https://mozilla.org"));
+ assertEquals("https://mozilla.org", StringUtils.stripScheme("https://mozilla.org/"));
+ assertEquals("mozilla.org", StringUtils.stripScheme("https://mozilla.org/", StringUtils.UrlFlags.STRIP_HTTPS));
+ assertEquals("mozilla.org", StringUtils.stripScheme("https://mozilla.org", StringUtils.UrlFlags.STRIP_HTTPS));
+ assertEquals("", StringUtils.stripScheme("http://"));
+ assertEquals("", StringUtils.stripScheme("https://", StringUtils.UrlFlags.STRIP_HTTPS));
+ // This edge case is not handled properly yet
+// assertEquals(StringUtils.stripScheme("https://"), "");
+ assertEquals(null, StringUtils.stripScheme(null));
+ }
+
+ @Test
+ public void testIsRTL() {
+ assertFalse(StringUtils.isRTL("mozilla.org"));
+ assertFalse(StringUtils.isRTL("something.عربي"));
+
+ assertTrue(StringUtils.isRTL("عربي"));
+ assertTrue(StringUtils.isRTL("عربي.org"));
+
+ // Text with LTR mark
+ assertFalse(StringUtils.isRTL("\u200EHello"));
+ assertFalse(StringUtils.isRTL("\u200Eعربي"));
+ }
+
+ @Test
+ public void testForceLTR() {
+ assertFalse(StringUtils.isRTL(StringUtils.forceLTR("عربي")));
+ assertFalse(StringUtils.isRTL(StringUtils.forceLTR("عربي.org")));
+
+ // Strings that are already LTR are not modified
+ final String someLtrString = "HelloWorld";
+ assertEquals(someLtrString, StringUtils.forceLTR(someLtrString));
+
+ // We add the LTR mark only once
+ final String someRtlString = "عربي";
+ assertEquals(4, someRtlString.length());
+ final String forcedLtrString = StringUtils.forceLTR(someRtlString);
+ assertEquals(5, forcedLtrString.length());
+ final String forcedAgainLtrString = StringUtils.forceLTR(forcedLtrString);
+ assertEquals(5, forcedAgainLtrString.length());
+ }
+
+ @Test
+ public void testJoin() {
+ assertEquals("", StringUtils.join("", Collections.<String>emptyList()));
+ assertEquals("", StringUtils.join("-", Collections.<String>emptyList()));
+ assertEquals("", StringUtils.join("", Collections.singletonList("")));
+ assertEquals("", StringUtils.join(".", Collections.singletonList("")));
+
+ assertEquals("192.168.0.1", StringUtils.join(".", Arrays.asList("192", "168", "0", "1")));
+ assertEquals("www.mozilla.org", StringUtils.join(".", Arrays.asList("www", "mozilla", "org")));
+
+ assertEquals("hello", StringUtils.join("", Collections.singletonList("hello")));
+ assertEquals("helloworld", StringUtils.join("", Arrays.asList("hello", "world")));
+ assertEquals("hello world", StringUtils.join(" ", Arrays.asList("hello", "world")));
+
+ assertEquals("m::o::z::i::l::l::a", StringUtils.join("::", Arrays.asList("m", "o", "z", "i", "l", "l", "a")));
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestUUIDUtil.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestUUIDUtil.java
new file mode 100644
index 0000000000..732dd21b99
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestUUIDUtil.java
@@ -0,0 +1,51 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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 org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests for uuid utils.
+ */
+@RunWith(TestRunner.class)
+public class TestUUIDUtil {
+ private static final String[] validUUIDs = {
+ "904cd9f8-af63-4525-8ce0-b9127e5364fa",
+ "8d584bd2-00ea-4043-a617-ed4ce7018ed0",
+ "3abad327-2669-4f68-b9ef-7ace8c5314d6",
+ };
+
+ private static final String[] invalidUUIDs = {
+ "its-not-a-uuid-mate",
+ "904cd9f8-af63-4525-8ce0-b9127e5364falol",
+ "904cd9f8-af63-4525-8ce0-b9127e5364f",
+ };
+
+ @Test
+ public void testUUIDRegex() {
+ for (final String uuid : validUUIDs) {
+ assertTrue("Valid UUID matches UUID-regex", uuid.matches(UUIDUtil.UUID_REGEX));
+ }
+ for (final String uuid : invalidUUIDs) {
+ assertFalse("Invalid UUID does not match UUID-regex", uuid.matches(UUIDUtil.UUID_REGEX));
+ }
+ }
+
+ @Test
+ public void testUUIDPattern() {
+ for (final String uuid : validUUIDs) {
+ assertTrue("Valid UUID matches UUID-regex", UUIDUtil.UUID_PATTERN.matcher(uuid).matches());
+ }
+ for (final String uuid : invalidUUIDs) {
+ assertFalse("Invalid UUID does not match UUID-regex", UUIDUtil.UUID_PATTERN.matcher(uuid).matches());
+ }
+ }
+}
diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/publicsuffix/TestPublicSuffix.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/publicsuffix/TestPublicSuffix.java
new file mode 100644
index 0000000000..e47d361c06
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/publicsuffix/TestPublicSuffix.java
@@ -0,0 +1,62 @@
+package org.mozilla.gecko.util.publicsuffix;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(TestRunner.class)
+public class TestPublicSuffix {
+ @Test
+ public void testStripPublicSuffix() {
+ // Test empty value
+ Assert.assertEquals("",
+ PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, ""));
+
+ // Test domains with public suffix
+ Assert.assertEquals("www.mozilla",
+ PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "www.mozilla.org"));
+ Assert.assertEquals("www.google",
+ PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "www.google.com"));
+ Assert.assertEquals("foobar",
+ PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "foobar.blogspot.com"));
+ Assert.assertEquals("independent",
+ PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "independent.co.uk"));
+ Assert.assertEquals("biz",
+ PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "biz.com.ua"));
+ Assert.assertEquals("example",
+ PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "example.org"));
+ Assert.assertEquals("example",
+ PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "example.pvt.k12.ma.us"));
+
+ // Test domain without public suffix
+ Assert.assertEquals("localhost",
+ PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "localhost"));
+ Assert.assertEquals("firefox.mozilla",
+ PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "firefox.mozilla"));
+
+ // IDN domains
+ Assert.assertEquals("ουτοπία.δπθ",
+ PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "ουτοπία.δπθ.gr"));
+ Assert.assertEquals("a网络A",
+ PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "a网络A.网络.Cn"));
+
+ // Other non-domain values
+ Assert.assertEquals("192.168.0.1",
+ PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "192.168.0.1"));
+ Assert.assertEquals("asdflkj9uahsd",
+ PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "asdflkj9uahsd"));
+
+ // Other trailing and other types of dots
+ Assert.assertEquals("www.mozilla。home.example",
+ PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "www.mozilla。home.example。org"));
+ Assert.assertEquals("example",
+ PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "example.org"));
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void testStripPublicSuffixThrowsException() {
+ PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, null);
+ }
+}
diff --git a/mobile/android/tests/background/moz.build b/mobile/android/tests/background/moz.build
new file mode 100644
index 0000000000..891477f321
--- /dev/null
+++ b/mobile/android/tests/background/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+TEST_DIRS += [
+ 'junit3',
+]
diff --git a/mobile/android/tests/browser/chrome/basic_article.html b/mobile/android/tests/browser/chrome/basic_article.html
new file mode 100644
index 0000000000..f34cbece4e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/basic_article.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Article title</title>
+<meta name="description" content="This is the article description." />
+</head>
+<body>
+<header>Site header</header>
+<div>
+<h1>Article title</h1>
+<h2 class="author">by Jane Doe</h2>
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+</div>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/basic_article_mobile.html b/mobile/android/tests/browser/chrome/basic_article_mobile.html
new file mode 100644
index 0000000000..d89ff248db
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/basic_article_mobile.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Article title</title>
+<meta name="description" content="This is the article description." />
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+</head>
+<body>
+<header>Site header</header>
+<div>
+<h1>Article title</h1>
+<h2 class="author">by Jane Doe</h2>
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+</div>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/chrome.ini b/mobile/android/tests/browser/chrome/chrome.ini
new file mode 100644
index 0000000000..f190d61998
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/chrome.ini
@@ -0,0 +1,49 @@
+[DEFAULT]
+skip-if = os != 'android'
+support-files =
+ basic_article.html
+ basic_article_mobile.html
+ desktopmode_user_agent.sjs
+ devicesearch.xml
+ head.js
+ head_search.js
+ session_formdata_sample.html
+ simpleservice.xml
+ video_controls.html
+ video_discovery.html
+ video_discovery.sjs
+ web_channel.html
+ tp5/**
+
+[test_about_logins.html]
+[test_accounts.html]
+[test_android_log.html]
+[test_app_constants.html]
+[test_awsy_lite.html]
+# historically, we only run awsy on opt; gc times out on debug
+skip-if = debug
+[test_debugger_server.html]
+[test_desktop_useragent.html]
+[test_device_search_engine.html]
+[test_get_last_visited.html]
+[test_home_provider.html]
+[test_hidden_select_option.html]
+[test_identity_mode.html]
+[test_java_addons.html]
+[test_jni.html]
+[test_migrate_ui.html]
+[test_network_manager.html]
+[test_offline_page.html]
+skip-if = true # Bug 1241478
+[test_reader_view.html]
+[test_resource_substitutions.html]
+[test_restricted_profiles.html]
+[test_select_disabled.html]
+[test_selectoraddtab.html]
+[test_session_form_data.html]
+[test_session_scroll_position.html]
+[test_session_zombification.html]
+[test_shared_preferences.html]
+[test_simple_discovery.html]
+[test_video_discovery.html]
+[test_web_channel.html]
diff --git a/mobile/android/tests/browser/chrome/desktopmode_user_agent.sjs b/mobile/android/tests/browser/chrome/desktopmode_user_agent.sjs
new file mode 100644
index 0000000000..88cfb8f7e4
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/desktopmode_user_agent.sjs
@@ -0,0 +1,11 @@
+function handleRequest(request, response)
+{
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+
+ // used by mobile/desktop user agent tests
+ response.write(request.getHeader("User-Agent"));
+}
+
diff --git a/mobile/android/tests/browser/chrome/devicesearch.xml b/mobile/android/tests/browser/chrome/devicesearch.xml
new file mode 100644
index 0000000000..5b472acf5c
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/devicesearch.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Test search engine</ShortName>
+<Description>A test search engine (based on Google search)</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA</Image>
+<Url type="text/html" method="GET" template="http://example.com/search">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<Url type="application/x-moz-tabletsearch" method="GET" template="http://example.com/search/tablet">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<Url type="application/x-moz-phonesearch" method="GET" template="http://example.com/search/phone">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+<SearchForm>http://example.com/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/android/tests/browser/chrome/head.js b/mobile/android/tests/browser/chrome/head.js
new file mode 100644
index 0000000000..0ac8ed0107
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/head.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function fuzzyEquals(a, b) {
+ return (Math.abs(a - b) < 1e-6);
+}
+
+function promiseBrowserEvent(browser, eventType) {
+ return new Promise((resolve) => {
+ function handle(event) {
+ // Since we'll be redirecting, don't make assumptions about the given URL and the loaded URL
+ if (event.target != browser.contentDocument || event.target.location.href == "about:blank") {
+ info("Skipping spurious '" + eventType + "' event" + " for " + event.target.location.href);
+ return;
+ }
+ info("Received event " + eventType + " from browser");
+ browser.removeEventListener(eventType, handle, true);
+ resolve(event);
+ }
+
+ browser.addEventListener(eventType, handle, true);
+ info("Now waiting for " + eventType + " event from browser");
+ });
+}
+
+function promiseTabEvent(container, eventType) {
+ return new Promise((resolve) => {
+ function handle(event) {
+ info("Received event " + eventType + " from container");
+ container.removeEventListener(eventType, handle, true);
+ resolve(event);
+ }
+
+ container.addEventListener(eventType, handle, true);
+ info("Now waiting for " + eventType + " event from container");
+ });
+}
+
+function promiseNotification(topic) {
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ return new Promise((resolve, reject) => {
+ function observe(subject, topic, data) {
+ info("Received " + topic + " notification from Gecko");
+ Services.obs.removeObserver(observe, topic);
+ resolve();
+ }
+ Services.obs.addObserver(observe, topic, false);
+ info("Now waiting for " + topic + " notification from Gecko");
+ });
+}
+
+function promiseLinkVisit(url) {
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ var topic = "link-visited";
+ return new Promise((resolve, reject) => {
+ function observe(subject, topic, data) {
+ info("Received " + topic + " notification from Gecko");
+ var uri = subject.QueryInterface(Ci.nsIURI);
+ if (uri.spec != url) {
+ info("Visited URL " + uri.spec + " is not desired URL " + url + "; ignoring.");
+ return;
+ }
+ info("Visited URL " + uri.spec + " is desired URL " + url);
+ Services.obs.removeObserver(observe, topic);
+ resolve();
+ };
+ Services.obs.addObserver(observe, topic, false);
+ info("Now waiting for " + topic + " notification from Gecko with URL " + url);
+ });
+}
diff --git a/mobile/android/tests/browser/chrome/head_search.js b/mobile/android/tests/browser/chrome/head_search.js
new file mode 100644
index 0000000000..b6fb944494
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/head_search.js
@@ -0,0 +1,46 @@
+// Bits and pieces copied from toolkit/components/search/tests/xpcshell/head_search.js
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+/**
+ * Adds test engines and returns a promise resolved when they are installed.
+ *
+ * The engines are added in the given order.
+ *
+ * @param aItems
+ * Array of objects with the following properties:
+ * {
+ * name: Engine name, used to wait for it to be loaded.
+ * details: Array containing the parameters of addEngineWithDetails,
+ * except for the engine name. Alternative to xmlFileName.
+ * }
+ */
+var addTestEngines = Task.async(function* (aItems) {
+ let engines = [];
+
+ for (let item of aItems) {
+ yield new Promise((resolve, reject) => {
+ Services.obs.addObserver(function obs(subject, topic, data) {
+ try {
+ let engine = subject.QueryInterface(Ci.nsISearchEngine);
+ if (data != "engine-added" || engine.name != item.name) {
+ return;
+ }
+
+ Services.obs.removeObserver(obs, "browser-search-engine-modified");
+ engines.push(engine);
+ resolve();
+ } catch (ex) {
+ reject(ex);
+ }
+ }, "browser-search-engine-modified", false);
+
+ Services.search.addEngineWithDetails(item.name, ...item.details);
+ });
+ }
+
+ return engines;
+});
diff --git a/mobile/android/tests/browser/chrome/memory_page_1.html b/mobile/android/tests/browser/chrome/memory_page_1.html
new file mode 100644
index 0000000000..f34cbece4e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/memory_page_1.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Article title</title>
+<meta name="description" content="This is the article description." />
+</head>
+<body>
+<header>Site header</header>
+<div>
+<h1>Article title</h1>
+<h2 class="author">by Jane Doe</h2>
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+</div>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/memory_page_2.html b/mobile/android/tests/browser/chrome/memory_page_2.html
new file mode 100644
index 0000000000..f34cbece4e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/memory_page_2.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Article title</title>
+<meta name="description" content="This is the article description." />
+</head>
+<body>
+<header>Site header</header>
+<div>
+<h1>Article title</h1>
+<h2 class="author">by Jane Doe</h2>
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+</div>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/memory_page_3.html b/mobile/android/tests/browser/chrome/memory_page_3.html
new file mode 100644
index 0000000000..f34cbece4e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/memory_page_3.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Article title</title>
+<meta name="description" content="This is the article description." />
+</head>
+<body>
+<header>Site header</header>
+<div>
+<h1>Article title</h1>
+<h2 class="author">by Jane Doe</h2>
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+</div>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/memory_page_4.html b/mobile/android/tests/browser/chrome/memory_page_4.html
new file mode 100644
index 0000000000..f34cbece4e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/memory_page_4.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Article title</title>
+<meta name="description" content="This is the article description." />
+</head>
+<body>
+<header>Site header</header>
+<div>
+<h1>Article title</h1>
+<h2 class="author">by Jane Doe</h2>
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+</div>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/session_formdata_sample.html b/mobile/android/tests/browser/chrome/session_formdata_sample.html
new file mode 100644
index 0000000000..f88e8668f1
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/session_formdata_sample.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>session_formdata_sample.html</title>
+ </head>
+ <body>
+ <input id="txt" />
+
+ <script type="text/javascript;version=1.8">
+ let isOuter = window == window.top;
+
+ if (isOuter) {
+ let iframe = document.createElement("iframe");
+ iframe.setAttribute("src", "https://example.com" + location.pathname);
+ document.body.appendChild(iframe);
+ }
+ </script>
+ </body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/simpleservice.xml b/mobile/android/tests/browser/chrome/simpleservice.xml
new file mode 100644
index 0000000000..f20becf1cf
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/simpleservice.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0"?>
+<root xmlns="urn:schemas-upnp-org:device-1-0">
+ <specVersion>
+ <major>1</major>
+ <minor>0</minor>
+ </specVersion>
+ <URLBase>http://example.com</URLBase>
+ <device>
+ <deviceType>urn:dial-multiscreen-org:device:dial:1</deviceType>
+ <friendlyName>Pretend Device</friendlyName>
+ <manufacturer>Copy Cat Inc.</manufacturer>
+ <modelName>Eureka Dongle</modelName>
+ <UDN>uuid:5ec9ff92-e8b2-4a94-a72c-76b34e6dabb1</UDN>
+ </device>
+</root>
diff --git a/mobile/android/tests/browser/chrome/test_about_logins.html b/mobile/android/tests/browser/chrome/test_about_logins.html
new file mode 100644
index 0000000000..8e7b404fd0
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_about_logins.html
@@ -0,0 +1,106 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1136477
+Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1136477</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript;version=1.8" src="head.js"></script>
+ <script type="application/javascript;version=1.8">
+
+ "use strict";
+
+ const { interfaces: Ci, utils: Cu } = Components;
+
+ Cu.import("resource://gre/modules/AppConstants.jsm");
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ const LOGIN_FIELDS = {
+ hostname: "http://example.org/tests/robocop/robocop_blank_01.html",
+ formSubmitUrl: "",
+ realmAny: null,
+ username: "username1",
+ password: "password1",
+ usernameField: "",
+ passwordField: ""
+ };
+
+ const LoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1", "nsILoginInfo", "init");
+
+ function add_login(login) {
+ let newLogin = new LoginInfo(login.hostname,
+ login.formSubmitUrl,
+ login.realmAny,
+ login.username,
+ login.password,
+ login.usernameField,
+ login.passwordField);
+
+ Services.logins.addLogin(newLogin);
+ }
+
+ add_task(function* test_passwords_list() {
+ add_login(LOGIN_FIELDS);
+
+ // Load about:logins.
+ let BrowserApp = Services.wm.getMostRecentWindow("navigator:browser").BrowserApp;
+ let browser = BrowserApp.addTab("about:logins", { selected: true, parentId: BrowserApp.selectedTab.id }).browser;
+
+ yield promiseBrowserEvent(browser, "load");
+
+ let logins_list_parent = browser.contentDocument.getElementById("logins-list").parentNode;
+
+ let waitForLoginToBeAdded = new Promise((resolve) => {
+ let observer = new MutationObserver((mutations) => {
+ for (let mutation of mutations) {
+ for (let node of mutation.addedNodes) {
+ if (node.id == 'logins-list') {
+ info("Received mutation replacing 'logins-list'");
+ resolve();
+ return;
+ }
+ }
+ }
+ info("Skipping spurious mutation not replacing 'logins-list'");
+ });
+ observer.observe(logins_list_parent, {
+ childList: true,
+ });
+ info("Now waiting for mutation to replace 'logins-list'");
+ });
+
+ yield waitForLoginToBeAdded;
+
+ let logins_list = browser.contentDocument.getElementById("logins-list");
+
+ // Test that the (single) entry added in setup is correct.
+ let hostname = logins_list.querySelector(".hostname");
+ is(hostname.textContent, LOGIN_FIELDS.hostname, "hostname is correct");
+
+ let username = logins_list.querySelector(".username");
+ is(username.textContent, LOGIN_FIELDS.username, "username is correct");
+
+ // Cleanup: close about:logins, opened in password_setup()
+ BrowserApp.closeTab(BrowserApp.selectedTab);
+ });
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1136477">Mozilla Bug 1136477</a>
+<br>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184186">Migrated from Robocop testAboutLogins</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_accounts.html b/mobile/android/tests/browser/chrome/test_accounts.html
new file mode 100644
index 0000000000..1f7b4469a8
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_accounts.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=917942
+Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 917942</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript;version=1.7">
+
+ Components.utils.import("resource://gre/modules/Accounts.jsm");
+
+ add_task(function* () {
+ let syncExists = yield Accounts.syncAccountsExist();
+ info("Sync account exists? " + syncExists + "\n");
+ let firefoxExists = yield Accounts.firefoxAccountsExist();
+ info("Firefox account exists? " + firefoxExists + "\n");
+ let anyExists = yield Accounts.anySyncAccountsExist();
+ info("Any accounts exist? " + anyExists + "\n");
+
+ // Only one account should exist.
+ ok(!syncExists || !firefoxExists, "at least one account does not exist");
+ is(anyExists, firefoxExists || syncExists, "sync/firefox account existence consistent with any existence");
+
+ // TODO: How can this be cleaned up?
+ //info("Launching setup.\n");
+ //Accounts.launchSetup();
+ });
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=917942">Mozilla Bug 917942</a>
+<br>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184186">Migrated from Robocop testAccounts</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_android_log.html b/mobile/android/tests/browser/chrome/test_android_log.html
new file mode 100644
index 0000000000..6048b3eb16
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_android_log.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1004825
+Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1004825</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript;version=1.7">
+
+ /*globals AndroidLog */
+
+ const TAG = "AndroidLogTest";
+
+ const VERBOSE_MESSAGE = "This is a verbose message.";
+ const DEBUG_MESSAGE = "This is a debug message.";
+ const INFO_MESSAGE = "This is an info message.";
+ const WARNING_MESSAGE = "This is a warning message.";
+ const ERROR_MESSAGE = "This is an error message.";
+
+ // Number of bytes we expect to log. This isn't equivalent to the number
+ // of characters, although the difference is consistent, so we can calculate it
+ // from the lengths of the messages and tag. We include the length of "Gecko"
+ // because the module prepends it to the tag.
+ const VERBOSE_BYTES = "Gecko".length + TAG.length + VERBOSE_MESSAGE.length + 3;
+ const DEBUG_BYTES = "Gecko".length + TAG.length + DEBUG_MESSAGE.length + 3;
+ const INFO_BYTES = "Gecko".length + TAG.length + INFO_MESSAGE.length + 3;
+ const WARNING_BYTES = "Gecko".length + TAG.length + WARNING_MESSAGE.length + 3;
+ const ERROR_BYTES = "Gecko".length + TAG.length + ERROR_MESSAGE.length + 3;
+
+ Components.utils.import("resource://gre/modules/AndroidLog.jsm");
+
+ ok(!!AndroidLog, "AndroidLog is defined");
+
+ ok("v" in AndroidLog && typeof AndroidLog.v == "function", "v function found");
+ ok("d" in AndroidLog && typeof AndroidLog.d == "function", "d function found");
+ ok("i" in AndroidLog && typeof AndroidLog.i == "function", "i function found");
+ ok("w" in AndroidLog && typeof AndroidLog.w == "function", "w function found");
+ ok("e" in AndroidLog && typeof AndroidLog.e == "function", "e function found");
+
+ // Ensure that the functions don't cause the test process to crash
+ // (because of some change to the native object being accessed via ctypes)
+ // and return the right values (the number of bytes logged).
+ // XXX Ensure that these messages actually make it to the log (bug 1046096).
+ is(VERBOSE_BYTES, AndroidLog.v(TAG, VERBOSE_MESSAGE), "verbose bytes correct");
+ is(DEBUG_BYTES, AndroidLog.d(TAG, DEBUG_MESSAGE), "debug bytes correct");
+ is(INFO_BYTES, AndroidLog.i(TAG, INFO_MESSAGE), "info bytes correct");
+ is(WARNING_BYTES, AndroidLog.w(TAG, WARNING_MESSAGE), "warning bytes correct");
+ is(ERROR_BYTES, AndroidLog.e(TAG, ERROR_MESSAGE), "error bytes correct");
+
+ // Ensure the functions work when bound with null value for thisArg parameter.
+ is(VERBOSE_BYTES, AndroidLog.v.bind(null, TAG)(VERBOSE_MESSAGE), "verbose bytes correct with bind");
+ is(DEBUG_BYTES, AndroidLog.d.bind(null, TAG)(DEBUG_MESSAGE), "debug bytes correct with bind");
+ is(INFO_BYTES, AndroidLog.i.bind(null, TAG)(INFO_MESSAGE), "info bytes correct with bind");
+ is(WARNING_BYTES, AndroidLog.w.bind(null, TAG)(WARNING_MESSAGE), "warning bytes correct with bind");
+ is(ERROR_BYTES, AndroidLog.e.bind(null, TAG)(ERROR_MESSAGE), "error bytes correct with bind");
+
+ // Ensure the functions work when the module object is "bound" to a tag.
+ let Log = AndroidLog.bind(TAG);
+ is(VERBOSE_BYTES, Log.v(VERBOSE_MESSAGE), "verbose bytes correct after bind");
+ is(DEBUG_BYTES, Log.d(DEBUG_MESSAGE), "debug bytes correct after bind");
+ is(INFO_BYTES, Log.i(INFO_MESSAGE), "info bytes correct after bind");
+ is(WARNING_BYTES, Log.w(WARNING_MESSAGE), "warning bytes correct after bind");
+ is(ERROR_BYTES, Log.e(ERROR_MESSAGE), "error bytes correct after bind");
+
+ // Ensure the functions work when the tag length is greater than the maximum
+ // tag length.
+ let tag = "X".repeat(AndroidLog.MAX_TAG_LENGTH + 1);
+ is(AndroidLog.MAX_TAG_LENGTH + 54, AndroidLog.v(tag, "This is a verbose message with a too-long tag."), "verbose message with too-long tag");
+ is(AndroidLog.MAX_TAG_LENGTH + 52, AndroidLog.d(tag, "This is a debug message with a too-long tag."), "debug message with too-long tag");
+ is(AndroidLog.MAX_TAG_LENGTH + 52, AndroidLog.i(tag, "This is an info message with a too-long tag."), "info message with too-long tag");
+ is(AndroidLog.MAX_TAG_LENGTH + 54, AndroidLog.w(tag, "This is a warning message with a too-long tag."), "warning message with too-long tag");
+ is(AndroidLog.MAX_TAG_LENGTH + 53, AndroidLog.e(tag, "This is an error message with a too-long tag."), "error message with too-long tag");
+
+ // We should also ensure that the module is accessible from a ChromeWorker,
+ // but there doesn't seem to be a way to load a ChromeWorker from this test.
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1004825">Mozilla Bug 1004825</a>
+<br>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184186">Migrated from Robocop testAndroidLog</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_app_constants.html b/mobile/android/tests/browser/chrome/test_app_constants.html
new file mode 100644
index 0000000000..7989ca1b6c
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_app_constants.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1130872
+Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1130872</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ Components.utils.import("resource://gre/modules/AppConstants.jsm");
+
+ var packageName = AppConstants.ANDROID_PACKAGE_NAME;
+
+ ok(packageName != "@ANDROID_PACKAGE_NAME@", "package name is not placeholder");
+ ok(packageName.length > 0, "package name is not empty");
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1130872">Mozilla Bug 1130872</a>
+<br>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184186">Migrated from Robocop testAppConstants</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_awsy_lite.html b/mobile/android/tests/browser/chrome/test_awsy_lite.html
new file mode 100644
index 0000000000..066aaf2ea2
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_awsy_lite.html
@@ -0,0 +1,258 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ This test reports Firefox memory use to Perfherder.
+
+ Inspired by https://areweslimyet.com/mobile
+
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1233220
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1233220</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/MemoryStats.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript;version=1.8" src="head.js"></script>
+ <script type="application/javascript;version=1.8">
+
+ "use strict";
+
+ const { interfaces: Ci, utils: Cu, classes: Cc } = Components;
+
+ var kUrls = [
+ "http://mochi.test:8888/chrome/mobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/s@wd=mozilla.html",
+ "http://mochi.test:8888/chrome/mobile/android/tests/browser/chrome/tp5/twitter.com/twitter.com/ICHCheezburger.html",
+ "http://mochi.test:8888/chrome/mobile/android/tests/browser/chrome/tp5/msn.com/www.msn.com/index.html",
+ "http://mochi.test:8888/chrome/mobile/android/tests/browser/chrome/tp5/163.com/www.163.com/index.html",
+ "http://mochi.test:8888/chrome/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/www.bbc.co.uk/news/index.html"
+ ];
+
+ var gTabsOpened = 0;
+ var gWindow = null;
+ var gLastTab = null;
+ var gResults = [];
+
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ var BrowserApp = Services.wm.getMostRecentWindow("navigator:browser").BrowserApp;
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.requestLongerTimeout(3); // several long waits and GCs make for a long-running test
+ SimpleTest.requestCompleteLog(); // so that "PERFHERDER_DATA" can be scraped from the log
+
+ function checkpoint(aName) {
+ var mrm = Cc["@mozilla.org/memory-reporter-manager;1"].getService(Ci.nsIMemoryReporterManager);
+ gResults.push( { name: aName, resident: mrm.resident } );
+ info(`${aName} | Resident Memory: ${mrm.resident}`);
+ }
+
+ var browserListener = {
+ onOpenWindow: function(aWindow) {
+ var win = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
+ win.addEventListener("UIReady", function listener(aEvent) {
+ win.removeEventListener("UIReady", listener, false);
+ attachTo(win);
+ }, false);
+ },
+
+ onCloseWindow: function(aWindow) {
+ detachFrom(aWindow);
+ },
+
+ onWindowTitleChange: function(aWindow, aTitle) {
+ }
+ };
+
+ function doFullGc(aCallback, aIterations) {
+ var threadMan = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
+ var domWindowUtils = gWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowUtils);
+
+ function runSoon(f) {
+ threadMan.mainThread.dispatch({ run: f }, Ci.nsIThread.DISPATCH_NORMAL);
+ }
+
+ function cc() {
+ if (domWindowUtils.cycleCollect) {
+ domWindowUtils.cycleCollect();
+ }
+ Services.obs.notifyObservers(null, "child-cc-request", null);
+ }
+
+ function minimizeInner() {
+ // In order of preference: schedulePreciseShrinkingGC, schedulePreciseGC
+ // garbageCollect
+ if (++j <= aIterations) {
+ var schedGC = Cu.schedulePreciseShrinkingGC;
+ if (!schedGC) {
+ schedGC = Cu.schedulePreciseGC;
+ }
+
+ Services.obs.notifyObservers(null, "child-gc-request", null);
+
+ if (schedGC) {
+ schedGC.call(Cu, { callback: function () {
+ runSoon(function () { cc(); runSoon(minimizeInner); });
+ } });
+ } else {
+ if (domWindowUtils.garbageCollect) {
+ domWindowUtils.garbageCollect();
+ }
+ runSoon(function () { cc(); runSoon(minimizeInner); });
+ }
+ } else {
+ runSoon(aCallback);
+ }
+ }
+
+ var j = 0;
+ minimizeInner();
+ }
+
+ function attachTo(aWindow) {
+ if (gWindow != null) {
+ info("attempting to attach to a second window [" + aWindow + "] while already attached to one window [" + gWindow + "]");
+ return;
+ }
+ gWindow = aWindow;
+ setTimeout(startTest, 0);
+ }
+
+ function detachFrom(aWindow) {
+ if (gWindow == aWindow) {
+ gWindow = null;
+ }
+ }
+
+ function startup() {
+ var enumerator = Services.wm.getEnumerator("navigator:browser");
+ while (enumerator.hasMoreElements()) {
+ // potential race condition here - the window may not be ready yet at
+ // this point, so ideally we would test for that. but i can't find a
+ // property that reflects whether or not UIReady has been fired, so
+ // for now just assume the window is ready
+ attachTo(enumerator.getNext().QueryInterface(Ci.nsIDOMWindow));
+ }
+ Services.wm.addListener(browserListener);
+ }
+
+ function startTest() {
+ checkpoint("Fresh start");
+ setTimeout(settle, 30000);
+ }
+
+ function settle() {
+ checkpoint("Fresh start [+30s]");
+ openTab();
+ }
+
+ function openTab() {
+ var urlIndex = gTabsOpened++;
+ if (urlIndex >= kUrls.length) {
+ checkpoint("After tabs");
+ setTimeout(postOpening, 30000);
+ return;
+ }
+
+ info("opening tab with url [" + kUrls[urlIndex] + "]");
+ gLastTab = BrowserApp.addTab(kUrls[urlIndex], { selected: true });
+ setTimeout(waitForTab, 10000);
+ }
+
+ function waitForTab() {
+ if (gLastTab.browser.contentDocument.readyState === "complete") {
+ gLastTab = null;
+ openTab();
+ } else {
+ setTimeout(waitForTab, 10000);
+ }
+ }
+
+ function postOpening() {
+ checkpoint("After tabs [+30s]");
+ doFullGc(() => closeTabs());
+ }
+
+ function closeTabs() {
+ checkpoint("After tabs [+30s, forced GC]");
+ var tabCount = BrowserApp.tabs.length;
+ for (var i = 1; i < tabCount; i++) {
+ BrowserApp.closeTab(BrowserApp.tabs[i]);
+ }
+
+ var closeListener = {
+ observe: function(aSubject, aTopic, aData) {
+ tabCount--;
+ dump("tab count dropped to [" + tabCount + "]");
+ if (tabCount == 1) {
+ Services.obs.removeObserver(this, "Tab:Closed", false);
+ setTimeout(tabsClosed, 0);
+ }
+ }
+ };
+ Services.obs.addObserver(closeListener, "Tab:Closed", false);
+ }
+
+ function tabsClosed() {
+ checkpoint("Tabs closed");
+ setTimeout(postClosing, 30000);
+ }
+
+ function postClosing() {
+ checkpoint("Tabs closed [+30s]");
+ doFullGc(() => {
+ checkpoint("Tabs closed [+30s, forced GC]");
+ finalReport();
+ ok(true, "memory logging complete -- view results in Perfherder");
+ SimpleTest.finish();
+ });
+ }
+
+ function geomean(aProperty) {
+ // https://en.wikipedia.org/wiki/Geometric_mean#Relationship_with_arithmetic_mean_of_logarithms
+ var logsum = 0;
+ var i;
+ for (i = 0; i < gResults.length; i++) {
+ var result = gResults[i];
+ logsum += Math.log(result[aProperty]);
+ }
+ return Math.round(Math.exp(logsum/gResults.length));
+ }
+
+ function finalReport() {
+ var i;
+ var perfherder = "PERFHERDER_DATA: ";
+ perfherder += "{\"framework\": {\"name\": \"awsy\"}, ";
+ perfherder += "\"suites\": [";
+ perfherder += "{\"name\": \"Resident Memory\", ";
+ perfherder += "\"subtests\": [";
+ for (i = 0; i < gResults.length; i++) {
+ var result = gResults[i];
+ if (i > 0) {
+ perfherder += ", ";
+ }
+ perfherder += `{\"name\": \"${result.name}\", \"value\": ${result.resident}}`;
+ }
+ perfherder += "], "; // end subtests
+ perfherder += "\"value\": "+geomean("resident");
+ perfherder += "}"; // end Resident Memory suite
+ perfherder += "]"; // end suites
+ perfherder += "}"; // end PERFHERDER_DATA
+ info(perfherder);
+ }
+
+ startup();
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1233220">Mozilla Bug 1233220</a>
+<br>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_debugger_server.html b/mobile/android/tests/browser/chrome/test_debugger_server.html
new file mode 100644
index 0000000000..a7b49ede69
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_debugger_server.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1010750
+Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1010750</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript;version=1.7">
+
+ "use strict";
+
+ const { utils: Cu } = Components;
+
+ const DEBUGGER_USB_ENABLED = "devtools.remote.usb.enabled";
+
+ Cu.import("resource://gre/modules/Services.jsm");
+ const { require } =
+ Cu.import("resource://devtools/shared/Loader.jsm", {});
+ const { DebuggerServer } = require("devtools/server/main");
+
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+
+ win.RemoteDebugger.init();
+
+ SimpleTest.registerCleanupFunction(function() {
+ Services.prefs.clearUserPref(DEBUGGER_USB_ENABLED);
+ });
+
+ // Enable the debugger via the pref it listens for
+ Services.prefs.setBoolPref(DEBUGGER_USB_ENABLED, true);
+
+ ok(DebuggerServer.initialized, "initialized");
+ is(DebuggerServer.listeningSockets, 1, "1 listening socket");
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1010750">Mozilla Bug 1010750</a>
+<br>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184186">Migrated from Robocop testDebuggerServer</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_desktop_useragent.html b/mobile/android/tests/browser/chrome/test_desktop_useragent.html
new file mode 100644
index 0000000000..cfa82659a2
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_desktop_useragent.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1157319
+Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1157319</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+ <script type="application/javascript;version=1.7">
+
+ "use strict";
+
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ // Load a custom sjs script that echos our "User-Agent" header back at us
+ const TestURI = Services.io.newURI("http://mochi.test:8888/chrome/mobile/android/tests/browser/chrome/desktopmode_user_agent.sjs", null, null);
+
+ add_task(function* test_desktopmode() {
+ let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let BrowserApp = chromeWin.BrowserApp;
+
+ // Add a new 'desktop mode' tab with our test page
+ let desktopTab = BrowserApp.addTab(TestURI.spec, { selected: true, parentId: BrowserApp.selectedTab.id, desktopMode: true });
+ let desktopBrowser = desktopTab.browser;
+ yield promiseBrowserEvent(desktopBrowser, "load");
+
+ // Some debugging
+ info("desktop: " + desktopBrowser.contentWindow.navigator.userAgent);
+ info("desktop: " + desktopBrowser.contentDocument.body.innerHTML);
+
+ // Check the server UA and the navigator UA for 'desktop'
+ ok(desktopBrowser.contentWindow.navigator.userAgent.indexOf("Linux x86_64") != -1, "window.navigator.userAgent has 'Linux' in it");
+ ok(desktopBrowser.contentDocument.body.innerHTML.indexOf("Linux x86_64") != -1, "HTTP header 'User-Agent' has 'Linux' in it");
+
+ BrowserApp.closeTab(desktopTab);
+
+ // Add a new 'mobile mode' tab with our test page
+ let mobileTab = BrowserApp.addTab(TestURI.spec, { selected: true, parentId: BrowserApp.selectedTab.id });
+ let mobileBrowser = mobileTab.browser;
+ yield promiseBrowserEvent(mobileBrowser, "load");
+
+ // Some debugging
+ info("mobile: " + mobileBrowser.contentWindow.navigator.userAgent);
+ info("mobile: " + mobileBrowser.contentDocument.body.innerHTML);
+
+ // Check the server UA and the navigator UA for 'mobile'
+ // We only check for 'Android' because we don't know the version or if it's phone or tablet
+ ok(mobileBrowser.contentWindow.navigator.userAgent.indexOf("Android") != -1, "window.navigator.userAgent has 'Android' in it");
+ ok(mobileBrowser.contentDocument.body.innerHTML.indexOf("Android") != -1, "HTTP header 'User-Agent' has 'Android' in it");
+
+ BrowserApp.closeTab(mobileTab);
+ });
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1157319">Mozilla Bug 1157319</a>
+<br>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184186">Migrated from Robocop testDesktopUseragent</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_device_search_engine.html b/mobile/android/tests/browser/chrome/test_device_search_engine.html
new file mode 100644
index 0000000000..bb67548cd3
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_device_search_engine.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=861164
+Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 861164</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript;version=1.7">
+
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ var Cc = Components.classes;
+ var Ci = Components.interfaces;
+
+ SimpleTest.waitForExplicitFinish();
+
+ function search_observer(aSubject, aTopic, aData) {
+ let engine = aSubject.QueryInterface(Ci.nsISearchEngine);
+ info("Observer: " + aData + " for " + engine.name);
+
+ if (aData != "engine-added")
+ return;
+
+ if (engine.name != "Test search engine")
+ return;
+
+ function check_submission(aExpected, aSearchTerm, aType) {
+ is(engine.getSubmission(aSearchTerm, aType).uri.spec, "http://example.com/search" + aExpected, "submission matches");
+ }
+
+ // Force the type and check for the expected URL
+ check_submission("?q=foo", "foo", "text/html");
+ check_submission("/tablet?q=foo", "foo", "application/x-moz-tabletsearch");
+ check_submission("/phone?q=foo", "foo", "application/x-moz-phonesearch");
+
+ // Let the service pick the appropriate type based on the device
+ // and check for expected URL
+ let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
+ if (sysInfo.get("tablet")) {
+ info("Device: tablet");
+ check_submission("/tablet?q=foo", "foo", null);
+ } else {
+ info("Device: phone");
+ check_submission("/phone?q=foo", "foo", null);
+ }
+
+ SimpleTest.finish();
+ };
+
+ SimpleTest.registerCleanupFunction(function() {
+ Services.obs.removeObserver(search_observer, "browser-search-engine-modified");
+ });
+ Services.obs.addObserver(search_observer, "browser-search-engine-modified", false);
+ info("Loading search engine");
+ Services.search.addEngine("http://mochi.test:8888/chrome/mobile/android/tests/browser/chrome/devicesearch.xml", Ci.nsISearchEngine.DATA_XML, null, false);
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=861164">Mozilla Bug 861164</a>
+<br>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184186">Migrated from Robocop testDeviceSearchEngine</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_get_last_visited.html b/mobile/android/tests/browser/chrome/test_get_last_visited.html
new file mode 100644
index 0000000000..da8a0fbfbc
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_get_last_visited.html
@@ -0,0 +1,106 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1214366
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1214366</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+ <script type="application/javascript;version=1.7">
+
+ "use strict";
+
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/Messaging.jsm");
+ Cu.import("resource://gre/modules/Task.jsm");
+
+ let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let BrowserApp = chromeWin.BrowserApp;
+
+ function get_last_visited(prePath) {
+ return Messaging.sendRequestForResult({
+ type: "History:GetPrePathLastVisitedTimeMilliseconds",
+ prePath: prePath,
+ });
+ };
+
+ var browser = BrowserApp.addTab("about:blank").browser;
+
+ // It's useful to see *all* "link-visited" events in the face of intermittent failures.
+ let observe = function(subject, topic, data) {
+ var uri = subject.QueryInterface(Ci.nsIURI);
+ info("Witnessed " + topic + " notification from Gecko with URI " + uri.spec);
+ }
+ Services.obs.addObserver(observe, "link-visited", false);
+
+ SimpleTest.registerCleanupFunction(function cleanup() {
+ BrowserApp.closeTab(BrowserApp.getTabForBrowser(browser));
+ Services.obs.removeObserver(observe, "link-visited");
+ });
+
+ // N.b.: the write to the Fennec DB happens before the Gecko notification
+ // is fired. This is delicate.
+ function add_history_visit(url) {
+ browser.loadURI(url, null, null);
+ return promiseLinkVisit(url);
+ };
+
+ // Be aware that some paths under mochi.test and example.org redirect. The
+ // blank robocop pages appear to not. Redirects can impact this test, since
+ // they can write to the history database.
+
+ // The apparent mis-ordering here just uses simpler pages (01 and 03) for the
+ // real test, and a more complex page (02) for a final delay. See comment below.
+ const url1 = "http://example.org/tests/robocop/robocop_blank_01.html";
+ const url2 = "http://example.org/tests/robocop/robocop_blank_03.html";
+ const url3 = "http://example.org/tests/robocop/robocop_blank_02.html";
+
+ add_task(function* test_get_last_visited() {
+ var v = yield get_last_visited("https://random.com/");
+ is(v, 0, `Last visited timestamp is 0 for unknown prePath: ${v}`);
+
+ let prePath = Services.io.newURI(url1, null, null).prePath + "/";
+ is(prePath, Services.io.newURI(url2, null, null).prePath + "/", "url1 and url2 have the same prePath");
+
+ let t0 = Date.now();
+ yield add_history_visit(url1);
+ v = yield get_last_visited(prePath);
+ let t1 = Date.now();
+ ok(t0 <= v, `Last visited timestamp is after visit: ${t0} <= ${v}.`);
+ ok(v <= t1, `Last visited timestamp is before present ${v} <= ${t1}.`);
+
+ let t2 = Date.now();
+ yield add_history_visit(url1);
+ v = yield get_last_visited(prePath);
+ ok(t2 <= v, `Last visited timestamp is updated after visit: ${t2} <= ${v}`);
+
+ let t3 = Date.now();
+ yield add_history_visit(url2);
+ v = yield get_last_visited(prePath);
+ ok(t3 <= v, `Last visited timestamp is updated after visit to URL with same prePath: ${t3} <= ${v}`);
+
+ // This whole system is flaky, so we wait for an unrelated visit, so that we
+ // can witness "link-visited" events a little after the test completes
+ // while debugging.
+ yield add_history_visit(url3);
+ });
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1214366">Mozilla Bug 1214366</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_hidden_select_option.html b/mobile/android/tests/browser/chrome/test_hidden_select_option.html
new file mode 100644
index 0000000000..ecdfe710e6
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_hidden_select_option.html
@@ -0,0 +1,103 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1178722
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1178722</title>
+ <style>
+ .hideme {display:none}
+ </style>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+ <script type="application/javascript;version=1.7">
+ "use strict";
+
+ const VISIBLE_OPTION_COUNT = 5;
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+ let SelectHelper = win.SelectHelper;
+
+ // Returns whether an element should be visible according to its text content.
+ function shouldBeVisible(e){
+ return e.label.indexOf("visible") > 0;
+ }
+
+ // Returns an object for the callback method that would normally be created by Prompt.java's
+ // addListResult(..) method.
+ function createCallBackDummyData(select){
+ var dummyList = new Array();
+ let listElements = SelectHelper.getListForElement(select);
+ for (var i = 0; i < listElements.length; i++) {
+ dummyList.push(i);
+ }
+ return {list:dummyList};
+ }
+
+ // Wait until the page has loaded so that we can access the DOM.
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function () {
+ let select = document.getElementById("sample-select");
+
+ // ##############################################
+ // ### Testing SelectHelper.getListForElement ###
+ // ##############################################
+
+ // Check that SelectHelper.getListForElement only includes visible options...
+ let listElements = SelectHelper.getListForElement(select);
+ for (var i = 0; i < listElements.length; i++) {
+ ok(shouldBeVisible(listElements[i]), "Element should be visible: " + listElements[i]);
+ }
+
+ // Check SelectHelper.getListForElement does not include additional options...
+ is(listElements.length, VISIBLE_OPTION_COUNT, "Correct number of elements were returned.");
+
+ // ############################################
+ // ### Testing SelectHelper._promptCallBack ###
+ // ############################################
+
+ // We will simulate "selecting" (ie choosing via the prompt) all the visible options...
+ is(select.selectedOptions.length, 0, "No options selected yet.");
+ let dummyData = createCallBackDummyData(select);
+ SelectHelper._promptCallBack(dummyData,select);
+
+ // Check that only the visible options had the "selected" attribute set...
+ let selectedOptions = select.selectedOptions;
+ for (var i = 0; i < selectedOptions.length; i++) {
+ ok(shouldBeVisible(selectedOptions[i]), "Element should be visible.");
+ }
+
+ // Check that no additional options had the "selected" attribute set...
+ is(selectedOptions.length, VISIBLE_OPTION_COUNT, "Correct number of options were selected.");
+
+ SimpleTest.finish();
+ }
+ </script>
+</head>
+<body>
+
+<p id="display">
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1178722">Mozilla Bug 1178722</a>
+ <select multiple id="sample-select">
+ <option value="1">1 - visible</option> 0
+ <option value="2" style="display: none">2 - hidden</option> 1
+ <option value="3">3 - visible</option> 2
+ <option value="4" style="display: nOnE">4 - hidden </option> 3
+ <option value="5">5 - visible</option> 4
+ <option value="6" class="hideme">6 - hidden</option> 5
+ <option value="7">7 - visible</option> 6
+ <option value="8" hiddEn>8 - hidden</option> 7
+ <option value="9">9 - visible</option> 8
+ </select>
+</p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+</body>
+</html> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/test_home_provider.html b/mobile/android/tests/browser/chrome/test_home_provider.html
new file mode 100644
index 0000000000..542cd82c0a
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_home_provider.html
@@ -0,0 +1,165 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=942288
+Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 942288</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript;version=1.7">
+
+ const { utils: Cu } = Components;
+
+ Cu.import("resource://gre/modules/HomeProvider.jsm");
+ Cu.import("resource://gre/modules/osfile.jsm");
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/Sqlite.jsm");
+ Cu.import("resource://gre/modules/Task.jsm");
+
+ const TEST_DATASET_ID = "test-dataset-id";
+ const TEST_URL = "http://test.com";
+ const TEST_TITLE = "Test";
+ const TEST_BACKGROUND_URL = "http://example.com/background";
+ const TEST_BACKGROUND_COLOR = "#FF9500";
+
+ const PREF_SYNC_CHECK_INTERVAL_SECS = "home.sync.checkIntervalSecs";
+ const TEST_INTERVAL_SECS = 1;
+
+ const DB_PATH = OS.Path.join(OS.Constants.Path.profileDir, "home.sqlite");
+
+ test_request_sync();
+ test_periodic_sync();
+
+ function test_request_sync() {
+ // The current implementation of requestSync is synchronous.
+ let success = HomeProvider.requestSync(TEST_DATASET_ID, function callback(datasetId) {
+ is(datasetId, TEST_DATASET_ID, "expected dataset ID");
+ });
+
+ ok(success, "requestSync success");
+ }
+
+ function test_periodic_sync() {
+ SimpleTest.registerCleanupFunction(function() {
+ Services.prefs.clearUserPref(PREF_SYNC_CHECK_INTERVAL_SECS);
+ HomeProvider.removePeriodicSync(TEST_DATASET_ID);
+ });
+
+ // Lower the check interval for testing purposes.
+ Services.prefs.setIntPref(PREF_SYNC_CHECK_INTERVAL_SECS, TEST_INTERVAL_SECS);
+
+ HomeProvider.addPeriodicSync(TEST_DATASET_ID, TEST_INTERVAL_SECS, function callback(datasetId) {
+ is(datasetId, TEST_DATASET_ID, "expected dataset ID");
+ });
+ }
+
+ add_task(function* test_save_and_delete() {
+ // Use the HomeProvider API to save some data.
+ let storage = HomeProvider.getStorage(TEST_DATASET_ID);
+ yield storage.save([{
+ title: TEST_TITLE,
+ url: TEST_URL,
+ background_url: TEST_BACKGROUND_URL,
+ background_color: TEST_BACKGROUND_COLOR
+ }]);
+
+ // Peek in the DB to make sure we have the right data.
+ let db = yield Sqlite.openConnection({ path: DB_PATH });
+
+ // Make sure the items table was created.
+ ok((yield db.tableExists("items")), "items table exists");
+
+ // Make sure the correct values for the item ended up in there.
+ let result = yield db.execute("SELECT * FROM items", null, function onRow(row){
+ is(row.getResultByName("dataset_id"), TEST_DATASET_ID, "expected dataset ID");
+ is(row.getResultByName("url"), TEST_URL, "expected test url");
+ is(row.getResultByName("background_url"), TEST_BACKGROUND_URL, "expected background url");
+ is(row.getResultByName("background_color"), TEST_BACKGROUND_COLOR, "expected background color");
+ });
+
+ // Use the HomeProvider API to delete the data.
+ yield storage.deleteAll();
+
+ // Make sure the data was deleted.
+ result = yield db.execute("SELECT * FROM items");
+ is(result.length, 0, "length is 0");
+
+ db.close();
+ });
+
+ add_task(function* test_row_validation() {
+ // Use the HomeProvider API to save some data.
+ let storage = HomeProvider.getStorage(TEST_DATASET_ID);
+
+ let invalidRows = [
+ { url: "url" },
+ { title: "title" },
+ { description: "description" },
+ { image_url: "image_url" }
+ ];
+
+ // None of these save calls should save anything
+ for (let row of invalidRows) {
+ try {
+ yield storage.save([row]);
+ } catch (e if e instanceof HomeProvider.ValidationError) {
+ // Just catch and ignore validation errors
+ }
+ }
+
+ // Peek in the DB to make sure we have the right data.
+ let db = yield Sqlite.openConnection({ path: DB_PATH });
+
+ // Make sure no data has been saved.
+ let result = yield db.execute("SELECT * FROM items");
+ is(result.length, 0, "length is 0");
+
+ db.close();
+ });
+
+ add_task(function* test_save_transaction() {
+ // Use the HomeProvider API to save some data.
+ let storage = HomeProvider.getStorage(TEST_DATASET_ID);
+
+ // One valid, one invalid
+ let rows = [
+ { title: TEST_TITLE, url: TEST_URL },
+ { image_url: "image_url" }
+ ];
+
+ // Try to save all the rows at once
+ try {
+ yield storage.save(rows);
+ } catch (e if e instanceof HomeProvider.ValidationError) {
+ // Just catch and ignore validation errors
+ }
+
+ // Peek in the DB to make sure we have the right data.
+ let db = yield Sqlite.openConnection({ path: DB_PATH });
+
+ // Make sure no data has been saved.
+ let result = yield db.execute("SELECT * FROM items");
+ is(result.length, 0, "length is 0");
+
+ db.close();
+ });
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=942288">Mozilla Bug 942288</a>
+<br>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184186">Migrated from Robocop testHomeProvider</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_identity_mode.html b/mobile/android/tests/browser/chrome/test_identity_mode.html
new file mode 100644
index 0000000000..5c41489a4d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_identity_mode.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1099088
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for getIdentityMode</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+ <script type="application/javascript;version=1.7">
+
+ "use strict";
+
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let IdentityHandler = chromeWin.IdentityHandler;
+
+ ok(IdentityHandler.getIdentityMode(0, Services.io.newURI("about:", null, null)) == IdentityHandler.IDENTITY_MODE_CHROMEUI,
+ "'about:' is a verified internal page");
+ ok(IdentityHandler.getIdentityMode(0, Services.io.newURI("about:config", null, null)) == IdentityHandler.IDENTITY_MODE_CHROMEUI,
+ "'about:config' is a verified internal page");
+ ok(IdentityHandler.getIdentityMode(0, Services.io.newURI("about:accounts", null, null)) == IdentityHandler.IDENTITY_MODE_CHROMEUI,
+ "'about:accounts is a verified internal page");
+ ok(IdentityHandler.getIdentityMode(0, Services.io.newURI("about:addonss", null, null)) == IdentityHandler.IDENTITY_MODE_UNKNOWN,
+ "'about:addonss is not a verified internal page");
+ ok(IdentityHandler.getIdentityMode(0, Services.io.newURI("about:accountss", null, null)) == IdentityHandler.IDENTITY_MODE_UNKNOWN,
+ "'about:accountss is not a verified internal page");
+ ok(IdentityHandler.getIdentityMode(0, Services.io.newURI("about:accounts?action=signup", null, null)) == IdentityHandler.IDENTITY_MODE_CHROMEUI,
+ "'about:accounts?action=signup is a verified internal page");
+ ok(IdentityHandler.getIdentityMode(0, Services.io.newURI("about:evil_extension_page", null, null)) == IdentityHandler.IDENTITY_MODE_UNKNOWN,
+ "'about:evil_extension_page' is not a verified internal page");
+
+ ok(IdentityHandler.getIdentityMode(0, Services.io.newURI("http://mozilla.com", null, null)) == IdentityHandler.IDENTITY_MODE_UNKNOWN,
+ "http://mozilla.com is an unknown page");
+ ok(IdentityHandler.getIdentityMode(0, Services.io.newURI("https://mozilla.com", null, null)) == IdentityHandler.IDENTITY_MODE_UNKNOWN,
+ "https://mozilla.com over an insecure connection is an unknown page");
+ ok(IdentityHandler.getIdentityMode(Ci.nsIWebProgressListener.STATE_IS_SECURE, Services.io.newURI("https://mozilla.com", null, null)) == IdentityHandler.IDENTITY_MODE_IDENTIFIED,
+ "https://mozilla.com over a secure connection is a verified page");
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1099088">Mozilla Bug 1099088</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_java_addons.html b/mobile/android/tests/browser/chrome/test_java_addons.html
new file mode 100644
index 0000000000..7a656671ae
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_java_addons.html
@@ -0,0 +1,118 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1168407
+Migrated from Robocop https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1168407</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript;version=1.7">
+
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+ Cu.import("resource://gre/modules/JavaAddonManager.jsm"); /*global JavaAddonManager */
+ Cu.import("resource://gre/modules/Promise.jsm"); /*global Promise */
+ Cu.import("resource://gre/modules/Task.jsm"); /*global Task */
+
+ const DEX_FILE = "chrome://roboextender/content/javaaddons-test.apk";
+ const CLASS = "org.mozilla.javaaddons.test.JavaAddonV1";
+
+ const MESSAGE = "JavaAddon:V1";
+
+ add_task(function* testFailureCases() {
+ info("Loading Java Addon from non-existent class.");
+ let gotError1 = yield JavaAddonManager.classInstanceFromFile(CLASS + "GARBAGE", DEX_FILE)
+ .then((result) => false)
+ .catch((error) => true);
+ is(gotError1, true, "got expected error for non-existent class");
+
+ info("Loading Java Addon from non-existent DEX file.");
+ let gotError2 = yield JavaAddonManager.classInstanceFromFile(CLASS, DEX_FILE + "GARBAGE")
+ .then((result) => false)
+ .catch((error) => true);
+ is(gotError2, true, "got expected error for non-existent DEX file");
+ });
+
+ // Make a request to a dynamically loaded Java Addon; wait for a response.
+ // Then expect the add-on to make a request; respond.
+ // Then expect the add-on to make a second request; use it to verify the response to the first request.
+ add_task(function* testJavaAddonV1() {
+ info("Loading Java Addon from: " + DEX_FILE);
+
+ let javaAddon = yield JavaAddonManager.classInstanceFromFile(CLASS, DEX_FILE);
+ isnot(javaAddon, null, "addon is not null");
+ isnot(javaAddon._guid, null, "guid is not null");
+ is(javaAddon._classname, CLASS, "got expected class");
+ is(javaAddon._loaded, true, "addon is loaded");
+
+ let messagePromise = Promise.defer();
+ var count = 0;
+ function listener(data) {
+ info("Got request initiated from Java Addon: " + data + ", " + typeof(data) + ", " + JSON.stringify(data));
+ count += 1;
+ messagePromise.resolve(); // It's okay to resolve before returning: we'll wait on the verification promise no matter what.
+ return {
+ outputStringKey: "inputStringKey=" + data.inputStringKey,
+ outputIntKey: data.inputIntKey - 1
+ };
+ }
+ javaAddon.addListener(listener, "JavaAddon:V1:Request");
+
+ let verifierPromise = Promise.defer();
+ function verifier(data) {
+ info("Got verification request initiated from Java Addon: " + data + ", " + typeof(data) + ", " + JSON.stringify(data));
+ // These values are from the test Java Addon, after being processed by the :Request listener above.
+ is(data.outputStringKey, "inputStringKey=raw", "got expected outputStringKey");
+ is(data.outputIntKey, 2, "got expected outputIntKey");
+ verifierPromise.resolve();
+ return {};
+ }
+ javaAddon.addListener(verifier, "JavaAddon:V1:VerificationRequest");
+
+ let message = {type: MESSAGE, inputStringKey: "test", inputIntKey: 5};
+ info("Sending request to Java Addon: " + JSON.stringify(message));
+ let output = yield javaAddon.sendRequestForResult(message);
+
+ info("Got response from Java Addon: " + output + ", " + typeof(output) + ", " + JSON.stringify(output));
+ is(output.outputStringKey, "inputStringKey=test", "got expected outputStringKey");
+ is(output.outputIntKey, 6, "got expected outputIntKey");
+
+ // We don't worry about timing out: the harness will (very much later)
+ // kill us if we don't see the expected messages.
+
+ info("Waiting for request initiated from Java Addon.");
+ yield messagePromise.promise;
+ is(count, 1, "count is 1");
+
+ info("Send request for result 2 for request initiated from Java Addon.");
+
+ // The JavaAddon should have removed its listener, so we shouldn't get a response and count should stay the same.
+ let gotError = yield javaAddon.sendRequestForResult(message)
+ .then((result) => false)
+ .catch((error) => true);
+ is(gotError, true, "got expected error");
+ is(count, 1, "count is still 1");
+
+ info("Waiting for verification request initiated from Java Addon.");
+ yield verifierPromise.promise;
+ });
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1168407">Mozilla Bug 1168407</a>
+<br>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184186">Migrated from Robocop testJavaAddons</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_jni.html b/mobile/android/tests/browser/chrome/test_jni.html
new file mode 100644
index 0000000000..5e0d045dc8
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_jni.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=873569
+Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 873569</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript;version=1.7">
+
+ Components.utils.import("resource://gre/modules/ctypes.jsm");
+ Components.utils.import("resource://gre/modules/JNI.jsm");
+
+ function test_JNI() {
+ var jenv = null;
+ try {
+ jenv = JNI.GetForThread();
+
+ // Test a simple static method.
+ var geckoAppShell = JNI.LoadClass(jenv, "org.mozilla.gecko.GeckoAppShell", {
+ static_methods: [
+ { name: "getPreferredIconSize", sig: "()I" },
+ { name: "getContext", sig: "()Landroid/content/Context;" },
+ ],
+ });
+
+ let iconSize = -1;
+ iconSize = geckoAppShell.getPreferredIconSize();
+ isnot(iconSize, -1, "icon size is valid");
+
+ // Test GeckoNetworkManager methods that are accessed by PaymentsUI.js.
+ // The return values can vary, so we can't test for equivalence, but we
+ // can ensure that the method calls return values of the correct type.
+ let jGeckoNetworkManager = JNI.LoadClass(jenv, "org/mozilla/gecko/GeckoNetworkManager", {
+ static_methods: [
+ { name: "getMNC", sig: "()I" },
+ { name: "getMCC", sig: "()I" },
+ ],
+ });
+ is(typeof jGeckoNetworkManager.getMNC(), "number", "typeof getMNC is number");
+ is(typeof jGeckoNetworkManager.getMCC(), "number", "typeof getMCC is number");
+
+ // Test retrieving the context's class's name, which tests dynamic method
+ // invocation as well as converting a Java string to JavaScript.
+ JNI.LoadClass(jenv, "android.content.Context", {
+ methods: [
+ { name: "getClass", sig: "()Ljava/lang/Class;" },
+ ],
+ });
+ JNI.LoadClass(jenv, "java.lang.Class", {
+ methods: [
+ { name: "getName", sig: "()Ljava/lang/String;" },
+ ],
+ });
+ is("org.mozilla.gecko.BrowserApp", JNI.ReadString(jenv, geckoAppShell.getContext().getClass().getName()), "class name is correct");
+ } finally {
+ if (jenv) {
+ JNI.UnloadClasses(jenv);
+ }
+ }
+ }
+
+ test_JNI();
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=873569">Mozilla Bug 873569</a>
+<br>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184186">Migrated from Robocop testJNI</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_migrate_ui.html b/mobile/android/tests/browser/chrome/test_migrate_ui.html
new file mode 100644
index 0000000000..124b4b4f42
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_migrate_ui.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1154504
+Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1154504</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head_search.js"></script>
+ <script type="application/javascript;version=1.7">
+
+ Services.prefs.clearUserPref("browser.migration.version");
+ Services.prefs.setBoolPref("nglayout.debug.paint_flashing", true);
+
+ addTestEngines([
+ { name: "bacon", details: ["", "bacon", "Search Bacon", "GET",
+ "http://www.bacon.moz/?search={searchTerms}"] },
+ ]).then(engines => {
+ Services.prefs.setCharPref("browser.search.defaultenginename", engines[0].name);
+
+ let BrowserApp = Services.wm.getMostRecentWindow("navigator:browser").BrowserApp;
+
+ // This performs the serach migration asynchronously, but the search service is already initialized
+ // by `addTestEngines`, so we don't need to worry about waiting before checking the new engine.
+ BrowserApp._migrateUI();
+
+ // Check that migration version increased.
+ is(Services.prefs.getIntPref("browser.migration.version"), 3, "found expected version");
+
+ // Check that user pref value was reset.
+ is(Services.prefs.prefHasUserValue("nglayout.debug.paint_flashing"), false, "found expected user value");
+
+ function searchObserver(s, t, d) {
+ Services.obs.removeObserver(searchObserver, "default-search-engine-migrated");
+ is(Services.search.defaultEngine.name, engines[0].name, "found expected default search engine");
+ }
+ Services.obs.addObserver(searchObserver, "default-search-engine-migrated", false);
+ });
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1154504">Mozilla Bug 1154504</a>
+<br>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184186">Migrated from Robocop testMigrateUI</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_network_manager.html b/mobile/android/tests/browser/chrome/test_network_manager.html
new file mode 100644
index 0000000000..f21c68e72f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_network_manager.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=895775
+Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 895775</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript;version=1.7">
+
+ "use strict";
+
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ // Let's exercise the interface. Even if the network is not up, we can make sure nothing blows up.
+ let network = Cc["@mozilla.org/network/network-link-service;1"].getService(Ci.nsINetworkLinkService);
+ if (network.isLinkUp) {
+ ok(network.linkType != Ci.nsINetworkLinkService.LINK_TYPE_UNKNOWN, "LinkType is not UNKNOWN");
+ } else {
+ ok(network.linkType == Ci.nsINetworkLinkService.LINK_TYPE_UNKNOWN, "LinkType is UNKNOWN");
+ }
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=895775">Mozilla Bug 895775</a>
+<br>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184186">Migrated from Robocop testNetworkManager</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_offline_page.html b/mobile/android/tests/browser/chrome/test_offline_page.html
new file mode 100644
index 0000000000..e1b7232669
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_offline_page.html
@@ -0,0 +1,111 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1089190
+Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1089190</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+ <script type="application/javascript;version=1.7">
+
+ "use strict";
+
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/Messaging.jsm");
+ Cu.import("resource://gre/modules/Task.jsm");
+
+ // Provide a helper to yield until we are sure the offline state has changed
+ function promiseOffline(isOffline) {
+ return new Promise((resolve, reject) => {
+ function observe(subject, topic, data) {
+ info("Received topic: " + topic);
+ Services.obs.removeObserver(observe, "network:offline-status-changed");
+ resolve();
+ }
+ Services.obs.addObserver(observe, "network:offline-status-changed", false);
+ Services.io.offline = isOffline;
+ });
+ }
+
+ // The chrome window
+ let chromeWin;
+
+ // Track the <browser> where the tests are happening
+ let browser;
+
+ // The proxy setting
+ let proxyPrefValue;
+
+ const kUniqueURI = Services.io.newURI("http://mochi.test:8888/chrome/mobile/android/tests/browser/chrome/video_controls.html", null, null);
+
+ add_task(function* test_offline() {
+ // Tests always connect to localhost, and per bug 87717, localhost is now
+ // reachable in offline mode. To avoid this, disable any proxy.
+ proxyPrefValue = Services.prefs.getIntPref("network.proxy.type");
+ Services.prefs.setIntPref("network.proxy.type", 0);
+
+ // Clear network cache.
+ Cc["@mozilla.org/netwerk/cache-storage-service;1"].getService(Ci.nsICacheStorageService).clear();
+
+ chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let BrowserApp = chromeWin.BrowserApp;
+
+ // Add a new tab with a blank page so we can better control the real page load and the offline state
+ browser = BrowserApp.addTab("about:blank", { selected: true, parentId: BrowserApp.selectedTab.id }).browser;
+
+ SimpleTest.registerCleanupFunction(function() {
+ BrowserApp.closeTab(BrowserApp.getTabForBrowser(browser));
+ Services.prefs.setIntPref("network.proxy.type", proxyPrefValue);
+ Services.io.offline = false;
+ });
+
+ // Go offline, expecting the error page.
+ yield promiseOffline(true);
+
+ // Load our test web page
+ browser.loadURI(kUniqueURI.spec, null, null)
+ yield promiseBrowserEvent(browser, "DOMContentLoaded");
+
+ // This is an error page.
+ is(browser.contentDocument.documentURI.substring(0, 27), "about:neterror?e=netOffline", "Document URI is the error page.");
+
+ // But location bar should show the original request.
+ is(browser.contentWindow.location.href, kUniqueURI.spec, "Docshell URI is the original URI.");
+
+ Services.prefs.setIntPref("network.proxy.type", proxyPrefValue);
+
+ // Go online and try to load the page again
+ yield promiseOffline(false);
+
+ ok(browser.contentDocument.getElementById("errorTryAgain"), "The error page has got a #errorTryAgain element");
+
+ // Click "Try Again" button to start the page load
+ browser.contentDocument.getElementById("errorTryAgain").click();
+ yield promiseBrowserEvent(browser, "DOMContentLoaded");
+
+ // This is not an error page.
+ is(browser.contentDocument.documentURI, kUniqueURI.spec, "Document URI is not the offline-error page, but the original URI.");
+ });
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1089190">Mozilla Bug 1089190</a>
+<br>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184186">Migrated from Robocop testOfflinePage</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_reader_view.html b/mobile/android/tests/browser/chrome/test_reader_view.html
new file mode 100644
index 0000000000..05b74e1640
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_reader_view.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1158885
+Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1158885</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+ <script type="application/javascript;version=1.7">
+
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ add_task(function* test_reader_view_visibility() {
+ let gWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let BrowserApp = gWin.BrowserApp;
+
+ let url = "http://mochi.test:8888/chrome/mobile/android/tests/browser/chrome/basic_article.html";
+ let browser = BrowserApp.addTab("about:reader?url=" + url).browser;
+
+ SimpleTest.registerCleanupFunction(function() {
+ BrowserApp.closeTab(BrowserApp.getTabForBrowser(browser));
+ });
+
+ yield promiseBrowserEvent(browser, "load");
+
+ let doc = browser.contentDocument;
+ let title = doc.getElementById("reader-title");
+
+ // We need to wait for reader content to appear because AboutReader.jsm
+ // asynchronously fetches the content after about:reader loads.
+ yield promiseNotification("AboutReader:Ready");
+ is(title.textContent, "Article title", "found expected content");
+ });
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1158885">Mozilla Bug 1158885</a>
+<br>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184186">Migrated from Robocop testReaderView</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_resource_substitutions.html b/mobile/android/tests/browser/chrome/test_resource_substitutions.html
new file mode 100644
index 0000000000..709ac112e2
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_resource_substitutions.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=948465
+Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 948465</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript;version=1.7">
+
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+ Cu.import("resource://gre/modules/Promise.jsm"); /*global Promise */
+ Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
+ Cu.import("resource://gre/modules/NetUtil.jsm"); /*global NetUtil */
+
+ function readChannel(url) {
+ let deferred = Promise.defer();
+
+ let channel = NetUtil.newChannel({uri: url, loadUsingSystemPrincipal: true});
+
+ channel.contentType = "text/plain";
+
+ NetUtil.asyncFetch(channel, function(inputStream, status) {
+ if (!Components.isSuccessCode(status)) {
+ deferred.reject();
+ return;
+ }
+
+ let content = NetUtil.readInputStreamToString(inputStream, inputStream.available());
+ deferred.resolve(content);
+ });
+
+ return deferred.promise;
+ }
+
+ add_task(function* () {
+ let protocolHandler = Services.io
+ .getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+
+ ok(protocolHandler.hasSubstitution("android"));
+
+ // This can be any file that we know exists in the root of every APK.
+ let packageName = yield readChannel("resource://android/package-name.txt");
+ info(packageName);
+
+ // It's difficult to fish ANDROID_PACKAGE_NAME from JavaScript, so we test the
+ // following weaker condition.
+ let expectedPrefix = "org.mozilla.";
+ is(packageName.substring(0, expectedPrefix.length), expectedPrefix);
+ });
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=948465">Mozilla Bug 948465</a>
+<br>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184186">Migrated from Robocop testResourceSubstitutions</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_restricted_profiles.html b/mobile/android/tests/browser/chrome/test_restricted_profiles.html
new file mode 100644
index 0000000000..d699176b5f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_restricted_profiles.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1042715
+Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1042715</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript;version=1.7">
+
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+ Cu.import("resource://gre/modules/ctypes.jsm");
+ Cu.import("resource://gre/modules/JNI.jsm");
+ Cu.import("resource://gre/modules/Task.jsm");
+
+ function test_isUserRestricted() {
+ // Make sure the parental controls service is available
+ ok("@mozilla.org/parental-controls-service;1" in Cc);
+
+ let pc = Cc["@mozilla.org/parental-controls-service;1"].createInstance(Ci.nsIParentalControlsService);
+
+ // In an admin profile, like the tests: enabled = false
+ // In a restricted profile: enabled = true
+ ok(!pc.parentalControlsEnabled);
+ ok(!pc.blockFileDownloadsEnabled);
+
+ ok(pc.isAllowed(Ci.nsIParentalControlsService.DOWNLOAD));
+ ok(pc.isAllowed(Ci.nsIParentalControlsService.INSTALL_EXTENSION));
+ ok(pc.isAllowed(Ci.nsIParentalControlsService.INSTALL_APP));
+ ok(pc.isAllowed(Ci.nsIParentalControlsService.BROWSE));
+ ok(pc.isAllowed(Ci.nsIParentalControlsService.SHARE));
+ ok(pc.isAllowed(Ci.nsIParentalControlsService.BOOKMARK));
+ ok(pc.isAllowed(Ci.nsIParentalControlsService.INSTALL_EXTENSION));
+ ok(pc.isAllowed(Ci.nsIParentalControlsService.MODIFY_ACCOUNTS));
+ }
+
+ test_isUserRestricted();
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1042715">Mozilla Bug 1042715</a>
+<br>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184186">Migrated from Robocop testRestrictedProfiles</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_select_disabled.html b/mobile/android/tests/browser/chrome/test_select_disabled.html
new file mode 100644
index 0000000000..d241f60ae0
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_select_disabled.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1263589
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1263589</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+ <script type="application/javascript;version=1.7">
+ "use strict";
+
+ const VISIBLE_OPTION_COUNT = 5;
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+ let SelectHelper = win.SelectHelper;
+
+ // Wait until the page has loaded so that we can access the DOM.
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function () {
+ // test options are not incorrectly disabled...
+ let isEnabled1 = document.getElementById("is_enabled_1");
+ let isEnabled2 = document.getElementById("is_enabled_2");
+ ok(!SelectHelper._isDisabledElement(isEnabled1),"input with name=\"disabled\" should not disable options (bug 1263589)");
+ ok(!SelectHelper._isDisabledElement(isEnabled2),"<form disabled> is not valid and will have no effect.");
+
+ // test options are disabled when expected...
+ let isNotEnabled1 = document.getElementById("is_not_enabled_1");
+ let isNotEnabled2 = document.getElementById("is_not_enabled_2");
+ let isNotEnabled3 = document.getElementById("is_not_enabled_2");
+ ok(SelectHelper._isDisabledElement(isNotEnabled1),"<option disabled> is disabled.");
+ ok(SelectHelper._isDisabledElement(isNotEnabled2),"<optelement disabled> will have disabled children.");
+ ok(SelectHelper._isDisabledElement(isNotEnabled3),"<fieldset disabled> will have disabled children.");
+
+ SimpleTest.finish();
+ }
+
+ </script>
+</head>
+<body>
+
+<p id="display">
+
+<form>
+ <!-- This input field is to confused SelectHelper._isDisabledElement(e). See bug 1263589 for details.-->
+ <input type="text" id="disabled" name="disabled" value="disabled" disabled="disabled">
+
+ <select>
+ <option id="is_enabled_1">A</option>
+ <option disabled id="is_not_enabled_1">C</option>
+ <optgroup disabled>
+ <option id="is_not_enabled_2">B</option>
+ <option>C</option>
+ </optgroup>
+ </select>
+
+ <fieldset disabled>
+ <select>
+ <option>F</option>
+ <option id="is_not_enabled_3">G</option>
+ </select>
+ </fieldset>
+</form>
+
+
+<form disabled>
+ <!-- "Disabled" is not a valid attribute for <form> and so fields should not be disabled -->
+ <select>
+ <option id="is_enabled_2">D</option>
+ <option>E</option>
+ </select>
+</form>
+
+
+</p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+</body>
+</html> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/test_selectoraddtab.html b/mobile/android/tests/browser/chrome/test_selectoraddtab.html
new file mode 100644
index 0000000000..b2c4ececa2
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_selectoraddtab.html
@@ -0,0 +1,92 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1216047
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1216047</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+ <script type="application/javascript;version=1.7">
+
+ "use strict";
+
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/Messaging.jsm");
+ Cu.import("resource://gre/modules/Task.jsm");
+
+ // The chrome window
+ let chromeWin;
+
+ // Track the <browser>s where the tests are happening
+ let browserBlank;
+ let browserTest;
+
+ const kTestPage = "http://mochi.test:8888/chrome/mobile/android/tests/browser/chrome/basic_article.html";
+
+ add_task(function* test_selectOrAdd() {
+ chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let BrowserApp = chromeWin.BrowserApp;
+
+ SimpleTest.registerCleanupFunction(function() {
+ BrowserApp.closeTab(BrowserApp.getTabForBrowser(browserBlank));
+ BrowserApp.closeTab(BrowserApp.getTabForBrowser(browserTest));
+ });
+
+ // Add a new tab with a blank page
+ browserBlank = BrowserApp.addTab("about:blank", { selected: true, parentId: BrowserApp.selectedTab.id }).browser;
+
+ // Now, let's force the target browser to be added
+ browserTest = BrowserApp.selectOrAddTab(kTestPage, { selected: true, parentId: BrowserApp.selectedTab.id }).browser;
+ yield promiseBrowserEvent(browserTest, "DOMContentLoaded");
+
+ // Check that basic_article is now selected
+ is(BrowserApp.selectedBrowser, browserTest, "Target browser is selected after being added.");
+
+ // Switch back to about:blank
+ BrowserApp.selectTab(BrowserApp.getTabForBrowser(browserBlank));
+ yield promiseTabEvent(BrowserApp.deck, "TabSelect");
+
+ // Check that about:blank is selected
+ is(BrowserApp.selectedBrowser, browserBlank, "about:blank is selected.");
+
+ // Use selectOrAddTab to select the existing tab
+ BrowserApp.selectOrAddTab(kTestPage, { selected: true, parentId: BrowserApp.selectedTab.id }).browser;
+ yield promiseTabEvent(BrowserApp.deck, "TabSelect");
+
+ // Check that basic_article is now selected
+ is(BrowserApp.selectedBrowser, browserTest, "Target browser is selected.");
+
+ // Switch back to about:blank
+ BrowserApp.selectTab(BrowserApp.getTabForBrowser(browserBlank));
+ yield promiseTabEvent(BrowserApp.deck, "TabSelect");
+
+ // Check that about:blank is selected
+ is(BrowserApp.selectedBrowser, browserBlank, "about:blank is selected.");
+
+ // Use selectOrAddTab to select the existing tab using the startsWith flag
+ BrowserApp.selectOrAddTab(kTestPage, { selected: true, parentId: BrowserApp.selectedTab.id }, { startsWith: kTestPage }).browser;
+ yield promiseTabEvent(BrowserApp.deck, "TabSelect");
+
+ // Check that basic_article is now selected
+ is(BrowserApp.selectedBrowser, browserTest, "Target browser is selected.");
+ });
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1216047">Mozilla Bug 1216047</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_session_form_data.html b/mobile/android/tests/browser/chrome/test_session_form_data.html
new file mode 100644
index 0000000000..cf09350c7d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_session_form_data.html
@@ -0,0 +1,274 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=671993
+https://bugzilla.mozilla.org/show_bug.cgi?id=1261225
+Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bugs 671993, 1261225</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+ <script type="application/javascript;version=1.7">
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+let gChromeWin;
+let gBrowserApp;
+
+// Waiting for a tab to load or restore can be slow on the emulator.
+SimpleTest.requestLongerTimeout(2);
+
+setup_browser();
+
+function queryElement(contentWindow, data) {
+ let frame = contentWindow;
+ if (data.hasOwnProperty("frame")) {
+ frame = contentWindow.frames[data.frame];
+ }
+
+ let doc = frame.document;
+
+ if (data.hasOwnProperty("id")) {
+ return doc.getElementById(data.id);
+ }
+
+ if (data.hasOwnProperty("selector")) {
+ return doc.querySelector(data.selector);
+ }
+
+ if (data.hasOwnProperty("xpath")) {
+ let xptype = Ci.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE;
+ return doc.evaluate(data.xpath, doc, null, xptype, null).singleNodeValue;
+ }
+
+ throw new Error("couldn't query element");
+}
+
+function dispatchUIEvent(input, type) {
+ let event = input.ownerDocument.createEvent("UIEvents");
+ event.initUIEvent(type, true, true, input.ownerDocument.defaultView, 0);
+ input.dispatchEvent(event);
+}
+
+function setInputValue(browser, data) {
+ let input = queryElement(browser.contentWindow, data);
+ input.value = data.value;
+ dispatchUIEvent(input, "input");
+}
+
+function getInputValue(browser, data) {
+ let input = queryElement(browser.contentWindow, data);
+ return input.value;
+}
+
+let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+
+function setup_browser() {
+ gChromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ gBrowserApp = gChromeWin.BrowserApp;
+}
+
+/**
+ * This test ensures that form data collection respects the privacy level as
+ * set by the user.
+ */
+add_task(function* test_formdata() {
+ const URL = "http://example.org/chrome/mobile/android/tests/browser/chrome/session_formdata_sample.html";
+
+ const OUTER_VALUE = "browser_formdata_" + Math.random();
+ const INNER_VALUE = "browser_formdata_" + Math.random();
+
+ // Creates a tab, loads a page with some form fields,
+ // modifies their values and closes the tab.
+ function createAndRemoveTab() {
+ return Task.spawn(function () {
+ // Create a new tab.
+ let tab = gBrowserApp.addTab(URL);
+ let browser = tab.browser;
+ yield promiseBrowserEvent(browser, "load");
+
+ // Modify form data.
+ setInputValue(browser, {id: "txt", value: OUTER_VALUE});
+ setInputValue(browser, {id: "txt", value: INNER_VALUE, frame: 0});
+
+ // Remove the tab.
+ gBrowserApp.closeTab(tab);
+ yield promiseTabEvent(browser, "SSTabCloseProcessed");
+ });
+ }
+
+ yield createAndRemoveTab();
+ let state = ss.getClosedTabs(gChromeWin);
+ let [{formdata}] = state;
+ is(formdata.id.txt, OUTER_VALUE, "outer value is correct");
+ is(formdata.children[0].id.txt, INNER_VALUE, "inner value is correct");
+
+ // Disable saving data for encrypted sites.
+ Services.prefs.setIntPref("browser.sessionstore.privacy_level", 1);
+
+ yield createAndRemoveTab();
+ state = ss.getClosedTabs(gChromeWin);
+ [{formdata}] = state;
+ is(formdata.id.txt, OUTER_VALUE, "outer value is correct");
+ ok(!formdata.children, "inner value was *not* stored");
+
+ // Disable saving data for any site.
+ Services.prefs.setIntPref("browser.sessionstore.privacy_level", 2);
+
+ yield createAndRemoveTab();
+ state = ss.getClosedTabs(gChromeWin);
+ [{formdata}] = state;
+ ok(!formdata, "form data has *not* been stored");
+
+ // Restore the default privacy level.
+ Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
+});
+
+/**
+ * This test ensures that form data collection restores correctly.
+ */
+add_task(function* test_formdata2() {
+ const URL = "http://example.org/chrome/mobile/android/tests/browser/chrome/session_formdata_sample.html";
+
+ const OUTER_VALUE = "browser_formdata_" + Math.random();
+ const INNER_VALUE = "browser_formdata_" + Math.random();
+
+ // Creates a tab, loads a page with some form fields,
+ // modifies their values and closes the tab.
+ function createAndRemoveTab() {
+ return Task.spawn(function () {
+ // Create a new tab.
+ let tab = gBrowserApp.addTab(URL);
+ let browser = tab.browser;
+ yield promiseBrowserEvent(browser, "load");
+
+ // Modify form data.
+ setInputValue(browser, {id: "txt", value: OUTER_VALUE});
+ setInputValue(browser, {id: "txt", value: INNER_VALUE, frame: 0});
+
+ // Remove the tab.
+ gBrowserApp.closeTab(tab);
+ yield promiseTabEvent(browser, "SSTabCloseProcessed");
+ });
+ }
+
+ yield createAndRemoveTab();
+ let state = ss.getClosedTabs(gChromeWin);
+ let [{formdata}] = state;
+ is(formdata.id.txt, OUTER_VALUE, "outer value is correct");
+ is(formdata.children[0].id.txt, INNER_VALUE, "inner value is correct");
+
+ // Restore the closed tab.
+ let closedTabData = ss.getClosedTabs(gChromeWin)[0];
+ let browser = ss.undoCloseTab(gChromeWin, closedTabData);
+ yield promiseBrowserEvent(browser, "load");
+
+ // Check the form data.
+ is(getInputValue(browser, {id: "txt"}), OUTER_VALUE, "outer value restored correctly");
+ is(getInputValue(browser, {id: "txt", frame: 0}), INNER_VALUE, "inner value restored correctly");
+
+ // Remove the tab.
+ gBrowserApp.closeTab(gBrowserApp.getTabForBrowser(browser));
+});
+
+/**
+ * This test ensures that form data collection restores correctly even after
+ * navigating to a different page and then returning via hitting back.
+ */
+add_task(function* test_formdata_navigation() {
+ const URL = "http://example.org/chrome/mobile/android/tests/browser/chrome/session_formdata_sample.html";
+ const otherURL = "http://example.org/chrome/mobile/android/tests/browser/chrome/basic_article.html";
+
+ const OUTER_VALUE = "browser_formdata_" + Math.random();
+ const INNER_VALUE = "browser_formdata_" + Math.random();
+
+ // Make sure the bfcache remains enabled during this test,
+ // otherwise the inner value will not be restored correctly.
+ Services.prefs.setBoolPref("browser.sessionhistory.bfcacheIgnoreMemoryPressure", true);
+ Services.prefs.setIntPref("browser.sessionhistory.max_total_viewers", 1);
+
+ SimpleTest.registerCleanupFunction(function() {
+ // Turn the bfcache memory pressure protection back off once we're finished.
+ Services.prefs.clearUserPref("browser.sessionhistory.bfcacheIgnoreMemoryPressure");
+ Services.prefs.clearUserPref("browser.sessionhistory.max_total_viewers");
+ });
+
+ // Creates a tab, loads a page with some form fields, modifies their values,
+ // navigates to a different page and back again and closes the tab.
+ function createNavigateAndRemoveTab() {
+ return Task.spawn(function () {
+ // Create a new tab.
+ let tab = gBrowserApp.addTab(URL);
+ let browser = tab.browser;
+ yield promiseBrowserEvent(browser, "load");
+
+ // Modify form data.
+ setInputValue(browser, {id: "txt", value: OUTER_VALUE});
+ setInputValue(browser, {id: "txt", value: INNER_VALUE, frame: 0});
+
+ // Visit a different page.
+ gBrowserApp.loadURI(otherURL, browser);
+ yield promiseBrowserEvent(browser, "DOMContentLoaded");
+ is(browser.currentURI.spec, otherURL, "navigated to a different page");
+
+ // Go back.
+ is(browser.canGoBack, true, "can go back");
+ browser.goBack();
+ yield promiseTabEvent(browser, "SSTabDataUpdated");
+ is(browser.currentURI.spec, URL, "navigated back to form data page");
+
+ // Make sure form data is still present.
+ is(getInputValue(browser, {id: "txt"}), OUTER_VALUE, "outer value present after navigation");
+ is(getInputValue(browser, {id: "txt", frame: 0}), INNER_VALUE, "inner value present after navigation");
+
+ // Remove the tab.
+ gBrowserApp.closeTab(tab);
+ yield promiseTabEvent(browser, "SSTabCloseProcessed");
+ });
+ }
+
+ yield createNavigateAndRemoveTab();
+ let state = ss.getClosedTabs(gChromeWin);
+ let [{formdata}] = state;
+ is(formdata.id.txt, OUTER_VALUE, "outer value is correct");
+ is(formdata.children[0].id.txt, INNER_VALUE, "inner value is correct");
+
+ // Restore the closed tab.
+ let closedTabData = ss.getClosedTabs(gChromeWin)[0];
+ let browser = ss.undoCloseTab(gChromeWin, closedTabData);
+ yield promiseBrowserEvent(browser, "load");
+
+ // Check the form data.
+ is(getInputValue(browser, {id: "txt"}), OUTER_VALUE, "outer value restored correctly");
+ is(getInputValue(browser, {id: "txt", frame: 0}), INNER_VALUE, "inner value restored correctly");
+
+ // Remove the tab.
+ gBrowserApp.closeTab(gBrowserApp.getTabForBrowser(browser));
+});
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=671993">Mozilla Bug 671993</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1261225">Mozilla Bug 1261225</a>
+<br>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184186">Migrated from Robocop testSessionFormData</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_session_scroll_position.html b/mobile/android/tests/browser/chrome/test_session_scroll_position.html
new file mode 100644
index 0000000000..cfbeb5164b
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_session_scroll_position.html
@@ -0,0 +1,310 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=810981
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 810981</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+ <script type="application/javascript;version=1.7">
+
+ /** Test for Bug 810981 **/
+
+ "use strict";
+
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/Messaging.jsm");
+ Cu.import("resource://gre/modules/Task.jsm");
+
+ // The chrome window.
+ let chromeWin;
+
+ // Track the tabs where the tests are happening.
+ let tabScroll;
+
+ // Use something with enough content to allow for scrolling.
+ const URL = "http://example.org/chrome/mobile/android/tests/browser/chrome/basic_article_mobile.html";
+ // Something to test the zoom level scaling on rotation with.
+ const URL_desktop = "http://example.org/chrome/mobile/android/tests/browser/chrome/basic_article.html";
+ // Reader mode URL
+ const URL_reader = "about:reader?url=http%3A%2F%2Fexample.org%2Fchrome%2Fmobile%2Fandroid%2Ftests%2Fbrowser%2Fchrome%2Fbasic_article_mobile.html";
+
+ function dispatchUIEvent(browser, type) {
+ let event = browser.contentDocument.createEvent("UIEvents");
+ event.initUIEvent(type, true, false, browser.contentDocument.defaultView, 0);
+ browser.dispatchEvent(event);
+ }
+
+ function setScrollPosition(browser, x, y) {
+ browser.contentWindow.scrollTo(x, y);
+ dispatchUIEvent(browser, "scroll");
+ }
+
+ function setZoomLevel(browser, zoom) {
+ browser.contentWindow.QueryInterface(
+ Ci.nsIInterfaceRequestor).getInterface(
+ Ci.nsIDOMWindowUtils).setResolutionAndScaleTo(zoom);
+ }
+
+ let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+
+ add_task(function* test_sessionStoreScrollPosition() {
+ const SCROLL_X = 0;
+ const SCROLL_Y = 38;
+
+ chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let BrowserApp = chromeWin.BrowserApp;
+
+ // Creates a tab, sets a scroll position and closes the tab.
+ function createAndRemoveTab() {
+ return Task.spawn(function () {
+ // Create a new tab.
+ tabScroll = BrowserApp.addTab(URL);
+ let browser = tabScroll.browser;
+ yield promiseBrowserEvent(browser, "pageshow");
+
+ // Modify scroll position.
+ setScrollPosition(browser, SCROLL_X, SCROLL_Y);
+ yield promiseTabEvent(browser, "SSTabScrollCaptured");
+
+ // Check that we've actually scrolled.
+ let ifreq = browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor);
+ let utils = ifreq.getInterface(Ci.nsIDOMWindowUtils);
+ let scrollX = {}, scrollY = {};
+ utils.getScrollXY(false, scrollX, scrollY);
+ is(scrollX.value, SCROLL_X, "scrollX set correctly");
+ is(scrollY.value, SCROLL_Y, "scrollY set correctly");
+
+ // Remove the tab.
+ BrowserApp.closeTab(tabScroll);
+ yield promiseTabEvent(browser, "SSTabCloseProcessed");
+ });
+ }
+
+ yield createAndRemoveTab();
+ let state = ss.getClosedTabs(chromeWin);
+ let [{scrolldata}] = state;
+ is(scrolldata.scroll, SCROLL_X + "," + SCROLL_Y, "stored scroll position is correct");
+
+ // Restore the closed tab.
+ let closedTabData = ss.getClosedTabs(chromeWin)[0];
+ let browser = ss.undoCloseTab(chromeWin, closedTabData);
+ yield promiseBrowserEvent(browser, "pageshow");
+
+ // Check the scroll position.
+ let ifreq = browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor);
+ let utils = ifreq.getInterface(Ci.nsIDOMWindowUtils);
+ let scrollX = {}, scrollY = {};
+ utils.getScrollXY(false, scrollX, scrollY);
+ is(scrollX.value, SCROLL_X, "scrollX restored correctly");
+ is(scrollY.value, SCROLL_Y, "scrollY restored correctly");
+
+ // Remove the tab.
+ BrowserApp.closeTab(BrowserApp.getTabForBrowser(browser));
+ });
+
+ add_task(function* test_sessionStoreScrollPositionReaderMode() {
+ const SCROLL_X = 0;
+ const SCROLL_Y = 44;
+
+ chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let BrowserApp = chromeWin.BrowserApp;
+
+ // Creates a tab, sets a scroll position and closes the tab.
+ function createAndRemoveReaderTab() {
+ return Task.spawn(function () {
+ // Create a new tab.
+ tabScroll = BrowserApp.addTab(URL_reader);
+ let browser = tabScroll.browser;
+ yield promiseBrowserEvent(browser, "AboutReaderContentReady");
+
+ // Modify scroll position.
+ setScrollPosition(browser, SCROLL_X, SCROLL_Y);
+ yield promiseTabEvent(browser, "SSTabScrollCaptured");
+
+ // Check that we've actually scrolled.
+ let ifreq = browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor);
+ let utils = ifreq.getInterface(Ci.nsIDOMWindowUtils);
+ let scrollX = {}, scrollY = {};
+ utils.getScrollXY(false, scrollX, scrollY);
+ is(scrollX.value, SCROLL_X, "scrollX set correctly");
+ is(scrollY.value, SCROLL_Y, "scrollY set correctly");
+
+ // Remove the tab.
+ BrowserApp.closeTab(tabScroll);
+ yield promiseTabEvent(browser, "SSTabCloseProcessed");
+ });
+ }
+
+ yield createAndRemoveReaderTab();
+ let state = ss.getClosedTabs(chromeWin);
+ let [{scrolldata}] = state;
+ is(scrolldata.scroll, SCROLL_X + "," + SCROLL_Y, "stored scroll position is correct");
+
+ // Restore the closed tab.
+ let closedTabData = ss.getClosedTabs(chromeWin)[0];
+ let browser = ss.undoCloseTab(chromeWin, closedTabData);
+ yield promiseBrowserEvent(browser, "AboutReaderContentReady");
+
+ // Check the scroll position.
+ let ifreq = browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor);
+ let utils = ifreq.getInterface(Ci.nsIDOMWindowUtils);
+ let scrollX = {}, scrollY = {};
+ utils.getScrollXY(false, scrollX, scrollY);
+ is(scrollX.value, SCROLL_X, "scrollX restored correctly");
+ is(scrollY.value, SCROLL_Y, "scrollY restored correctly");
+
+ // Remove the tab.
+ BrowserApp.closeTab(BrowserApp.getTabForBrowser(browser));
+ });
+
+ add_task(function* test_sessionStoreZoomLevel() {
+ const ZOOM = 4.2;
+ const SCROLL_X = 42;
+ const SCROLL_Y = 42;
+
+ chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let BrowserApp = chromeWin.BrowserApp;
+
+ // Creates a tab, sets a scroll position and zoom level and closes the tab.
+ function createAndRemoveTab() {
+ return Task.spawn(function () {
+ // Create a new tab.
+ tabScroll = BrowserApp.addTab(URL);
+ let browser = tabScroll.browser;
+ yield promiseBrowserEvent(browser, "pageshow");
+
+ // Modify scroll position and zoom level.
+ setZoomLevel(browser, ZOOM);
+ setScrollPosition(browser, SCROLL_X, SCROLL_Y);
+ yield promiseTabEvent(browser, "SSTabScrollCaptured");
+
+ // Check that we've actually scrolled and zoomed.
+ let ifreq = browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor);
+ let utils = ifreq.getInterface(Ci.nsIDOMWindowUtils);
+ let scrollX = {}, scrollY = {}, zoom = {};
+ utils.getResolution(zoom);
+ utils.getScrollXY(false, scrollX, scrollY);
+ ok(fuzzyEquals(zoom.value, ZOOM), "zoom set correctly");
+ is(scrollX.value, SCROLL_X, "scrollX set correctly");
+ is(scrollY.value, SCROLL_Y, "scrollY set correctly");
+
+ // Remove the tab.
+ BrowserApp.closeTab(tabScroll);
+ yield promiseTabEvent(browser, "SSTabCloseProcessed");
+ });
+ }
+
+ yield createAndRemoveTab();
+ let state = ss.getClosedTabs(chromeWin);
+ let [{scrolldata}] = state;
+ is(scrolldata.scroll, SCROLL_X + "," + SCROLL_Y, "stored scroll position is correct");
+ ok(fuzzyEquals(scrolldata.zoom.resolution, ZOOM), "stored zoom level is correct");
+
+ // Restore the closed tab.
+ let closedTabData = ss.getClosedTabs(chromeWin)[0];
+ let browser = ss.undoCloseTab(chromeWin, closedTabData);
+ yield promiseBrowserEvent(browser, "pageshow");
+
+ // Check the scroll position and zoom level.
+ let ifreq = browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor);
+ let utils = ifreq.getInterface(Ci.nsIDOMWindowUtils);
+ let scrollX = {}, scrollY = {}, zoom = {};
+ utils.getResolution(zoom);
+ utils.getScrollXY(false, scrollX, scrollY);
+ ok(fuzzyEquals(zoom.value, ZOOM), "zoom restored correctly");
+ is(scrollX.value, SCROLL_X, "scrollX restored correctly");
+ is(scrollY.value, SCROLL_Y, "scrollY restored correctly");
+
+ // Remove the tab.
+ BrowserApp.closeTab(BrowserApp.getTabForBrowser(browser));
+ });
+
+ add_task(function* test_sessionStoreZoomLevelRecalc() {
+ const ZOOM = 4.2;
+ const SCROLL_X = 42;
+ const SCROLL_Y = 42;
+
+ chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let BrowserApp = chromeWin.BrowserApp;
+
+ // Creates a tab, sets a scroll position and zoom level and closes the tab.
+ function createAndRemoveTab() {
+ return Task.spawn(function () {
+ // Create a new tab.
+ tabScroll = BrowserApp.addTab(URL_desktop);
+ let browser = tabScroll.browser;
+ yield promiseBrowserEvent(browser, "pageshow");
+
+ // Modify scroll position and zoom level.
+ setZoomLevel(browser, ZOOM);
+ setScrollPosition(browser, SCROLL_X, SCROLL_Y);
+ yield promiseTabEvent(browser, "SSTabScrollCaptured");
+
+ // Check that we've actually scrolled and zoomed.
+ let ifreq = browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor);
+ let utils = ifreq.getInterface(Ci.nsIDOMWindowUtils);
+ let scrollX = {}, scrollY = {}, zoom = {};
+ utils.getResolution(zoom);
+ utils.getScrollXY(false, scrollX, scrollY);
+ ok(fuzzyEquals(zoom.value, ZOOM), "zoom set correctly");
+ is(scrollX.value, SCROLL_X, "scrollX set correctly");
+ is(scrollY.value, SCROLL_Y, "scrollY set correctly");
+
+ // Remove the tab.
+ BrowserApp.closeTab(tabScroll);
+ yield promiseTabEvent(browser, "SSTabCloseProcessed");
+ });
+ }
+
+ yield createAndRemoveTab();
+ let state = ss.getClosedTabs(chromeWin);
+ let [{scrolldata}] = state;
+ is(scrolldata.scroll, SCROLL_X + "," + SCROLL_Y, "stored scroll position is correct");
+ ok(fuzzyEquals(scrolldata.zoom.resolution, ZOOM), "stored zoom level is correct");
+
+ // Pretend the zoom level was originally saved on a rotated device.
+ let closedTabData = ss.getClosedTabs(chromeWin)[0];
+ let displayWidth = scrolldata.zoom.displaySize.width;
+ let displayHeight = scrolldata.zoom.displaySize.height;
+ closedTabData.scrolldata.zoom.displaySize.width = displayHeight;
+ closedTabData.scrolldata.zoom.displaySize.height = displayWidth;
+
+ // Restore the closed tab.
+ let browser = ss.undoCloseTab(chromeWin, closedTabData);
+ yield promiseBrowserEvent(browser, "pageshow");
+
+ // Check the scroll position and zoom level.
+ let ifreq = browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor);
+ let utils = ifreq.getInterface(Ci.nsIDOMWindowUtils);
+ let scrollX = {}, scrollY = {}, zoom = {};
+ utils.getResolution(zoom);
+ utils.getScrollXY(false, scrollX, scrollY);
+ ok(fuzzyEquals(zoom.value, ZOOM * displayWidth / displayHeight), "recalculated zoom restored correctly");
+ is(scrollX.value, SCROLL_X, "scrollX restored correctly");
+ is(scrollY.value, SCROLL_Y, "scrollY restored correctly");
+
+ // Remove the tab.
+ BrowserApp.closeTab(BrowserApp.getTabForBrowser(browser));
+ });
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=810981">Mozilla Bug 810981</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_session_zombification.html b/mobile/android/tests/browser/chrome/test_session_zombification.html
new file mode 100644
index 0000000000..eba255ff62
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_session_zombification.html
@@ -0,0 +1,185 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1044556
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1044556</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+ <script type="application/javascript">
+
+ /** Test for Bug 1044556 **/
+
+ "use strict";
+
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/Messaging.jsm");
+ Cu.import("resource://gre/modules/Task.jsm");
+
+ // Import the MemoryObserver
+ Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+ XPCOMUtils.defineLazyGetter(this, "MemoryObserver", function() {
+ let sandbox = {};
+ Services.scriptloader.loadSubScript("chrome://browser/content/MemoryObserver.js", sandbox);
+ return sandbox["MemoryObserver"];
+ });
+
+ // The chrome window
+ let chromeWin;
+
+ // Track the tabs where the tests are happening
+ let tabBlank;
+ let tabTest;
+
+ const url1 = "data:text/html;charset=utf-8,It%20was%20a%20dark%20and%20stormy%20night.";
+ const url2 = "data:text/html;charset=utf-8,Suddenly%2C%20a%20tab%20was%20zombified.";
+
+ add_task(function* test_sessionStoreZombify() {
+ chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let BrowserApp = chromeWin.BrowserApp;
+
+ SimpleTest.registerCleanupFunction(function() {
+ BrowserApp.closeTab(tabBlank);
+ BrowserApp.closeTab(tabTest);
+ });
+
+ // Add a new tab with some content
+ tabTest = BrowserApp.addTab(url1 , { selected: true, parentId: BrowserApp.selectedTab.id });
+ yield promiseBrowserEvent(tabTest.browser, "DOMContentLoaded");
+
+ // Add a new tab with a blank page
+ tabBlank = BrowserApp.addTab("about:blank", { selected: true, parentId: BrowserApp.selectedTab.id });
+ is(BrowserApp.selectedTab, tabBlank, "Test tab is in background.");
+
+ // Zombify the backgrounded test tab
+ MemoryObserver.zombify(tabTest);
+
+ // Check that the test tab is actually zombified
+ ok(tabTest.browser.__SS_restore, "Test tab is set for delay loading.");
+
+ // Switch back to the test tab and wait for it to reload
+ BrowserApp.selectTab(tabTest);
+ yield promiseBrowserEvent(tabTest.browser, "DOMContentLoaded");
+
+ // Check that the test tab has loaded the correct url
+ is(tabTest.browser.currentURI.spec, url1, "Test tab is showing the first URL.");
+
+ // Navigate to some other content
+ BrowserApp.loadURI(url2, tabTest.browser);
+ yield promiseBrowserEvent(tabTest.browser, "DOMContentLoaded");
+ is(tabTest.browser.currentURI.spec, url2, "Test tab is showing the second URL.");
+
+ // Switch to the other tab
+ BrowserApp.selectTab(tabBlank);
+ yield promiseTabEvent(BrowserApp.deck, "TabSelect");
+ is(BrowserApp.selectedTab, tabBlank, "Test tab is in background.");
+
+ // Zombify the backgrounded test tab again
+ MemoryObserver.zombify(tabTest);
+
+ // Check that the test tab is actually zombified
+ ok(tabTest.browser.__SS_restore, "Test tab is set for delay loading.");
+
+ // Switch back to the test tab and wait for it to reload
+ BrowserApp.selectTab(tabTest);
+ yield promiseBrowserEvent(tabTest.browser, "DOMContentLoaded");
+
+ // Check that the test tab has loaded the correct url
+ is(tabTest.browser.currentURI.spec, url2, "Test tab is showing the second URL.");
+ });
+
+ add_task(function* test_sessionStoreKeepAsZombie() {
+ chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let BrowserApp = chromeWin.BrowserApp;
+ let observerService = Services.obs;
+
+ SimpleTest.registerCleanupFunction(function() {
+ BrowserApp.closeTab(tabBlank);
+ BrowserApp.closeTab(tabTest);
+ });
+
+ // Add a new tab with some content
+ tabTest = BrowserApp.addTab(url1 , { selected: true, parentId: BrowserApp.selectedTab.id });
+ yield promiseBrowserEvent(tabTest.browser, "DOMContentLoaded");
+
+ // Add a new tab with a blank page
+ tabBlank = BrowserApp.addTab("about:blank", { selected: true, parentId: BrowserApp.selectedTab.id });
+ yield promiseTabEvent(BrowserApp.deck, "TabSelect");
+ is(BrowserApp.selectedTab, tabBlank, "Test tab is in background.");
+
+ // Zombify the backgrounded test tab
+ MemoryObserver.zombify(tabTest);
+
+ // Check that the test tab is actually zombified
+ ok(tabTest.browser.__SS_restore, "Test tab is set for delay loading.");
+ is(tabTest.browser.currentURI.spec, "about:blank", "Test tab is zombified.");
+
+ // Tell the session store that it shouldn't restore that tab on selecting
+ observerService.notifyObservers(null, "Tab:KeepZombified", tabTest.id);
+
+ // Switch back to the test tab and check that it remains zombified
+ BrowserApp.selectTab(tabTest);
+ yield promiseTabEvent(BrowserApp.deck, "TabSelect");
+ is(BrowserApp.selectedTab, tabTest, "Test tab is selected.");
+ ok(tabTest.browser.__SS_restore, "Test tab is still set for delay loading.");
+
+ // Switch to the other tab and back again
+ BrowserApp.selectTab(tabBlank);
+ yield promiseTabEvent(BrowserApp.deck, "TabSelect");
+ is(BrowserApp.selectedTab, tabBlank, "Test tab is in background.");
+ BrowserApp.selectTab(tabTest);
+
+ // "Tab:KeepZombified should be good for one TabSelect only
+ yield promiseBrowserEvent(tabTest.browser, "DOMContentLoaded");
+ is(BrowserApp.selectedTab, tabTest, "Test tab is selected.");
+
+ // Check that the test tab is no longer a zombie and has loaded the correct url
+ ok(!tabTest.browser.__SS_restore, "Test tab is no longer set for delay loading.");
+ is(tabTest.browser.currentURI.spec, url1, "Test tab is showing the test URL.");
+
+ // Zombify the test tab again
+ BrowserApp.selectTab(tabBlank);
+ yield promiseTabEvent(BrowserApp.deck, "TabSelect");
+ is(BrowserApp.selectedTab, tabBlank, "Test tab is in background.");
+ MemoryObserver.zombify(tabTest);
+ ok(tabTest.browser.__SS_restore, "Test tab is set for delay loading.");
+ is(tabTest.browser.currentURI.spec, "about:blank", "Test tab is zombified.");
+
+ // Tell the session store that it shouldn't restore that tab on selecting
+ observerService.notifyObservers(null, "Tab:KeepZombified", tabTest.id);
+
+ // Switch back to the test tab and check that it remains zombified
+ BrowserApp.selectTab(tabTest);
+ yield promiseTabEvent(BrowserApp.deck, "TabSelect");
+ is(BrowserApp.selectedTab, tabTest, "Test tab is selected.");
+ ok(tabTest.browser.__SS_restore, "Test tab is still set for delay loading.");
+
+ // Fake an "application-foreground" notification
+ observerService.notifyObservers(null, "application-foreground", null);
+
+ // The test tab should now start reloading
+ yield promiseBrowserEvent(tabTest.browser, "DOMContentLoaded");
+ ok(!tabTest.browser.__SS_restore, "Test tab is no longer set for delay loading.");
+ is(tabTest.browser.currentURI.spec, url1, "Test tab is showing the test URL.");
+ });
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1044556">Mozilla Bug 1044556</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_shared_preferences.html b/mobile/android/tests/browser/chrome/test_shared_preferences.html
new file mode 100644
index 0000000000..b1ed69e662
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_shared_preferences.html
@@ -0,0 +1,255 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=866271
+Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 866271</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript;version=1.7">
+
+ Components.utils.import("resource://gre/modules/SharedPreferences.jsm");
+ Components.utils.import("resource://gre/modules/Promise.jsm");
+ Components.utils.import("resource://gre/modules/Task.jsm");
+
+ let _observerId = 0;
+
+ function makeObserver() {
+ let deferred = Promise.defer();
+
+ let ret = {
+ id: _observerId++,
+ count: 0,
+ promise: deferred.promise,
+ observe: function (subject, topic, data) {
+ ret.count += 1;
+ let msg = { subject: subject,
+ topic: topic,
+ data: data };
+ deferred.resolve(msg);
+ },
+ };
+
+ return ret;
+ };
+
+ add_task(function* test_get_set() {
+ let branch = SharedPreferences.forAndroid("test");
+
+ branch.setBoolPref("boolKey", true);
+ branch.setCharPref("charKey", "string value");
+ branch.setIntPref("intKey", 1000);
+
+ is(branch.getBoolPref("boolKey"), true);
+ is(branch.getCharPref("charKey"), "string value");
+ is(branch.getIntPref("intKey"), 1000);
+
+ branch.setBoolPref("boolKey", false);
+ branch.setCharPref("charKey", "different string value");
+ branch.setIntPref("intKey", -2000);
+
+ is(branch.getBoolPref("boolKey"), false);
+ is(branch.getCharPref("charKey"), "different string value");
+ is(branch.getIntPref("intKey"), -2000);
+
+ is(typeof(branch.getBoolPref("boolKey")), "boolean");
+ is(typeof(branch.getCharPref("charKey")), "string");
+ is(typeof(branch.getIntPref("intKey")), "number");
+ });
+
+ add_task(function* test_default() {
+ let branch = SharedPreferences.forAndroid();
+
+ branch.setBoolPref("boolKey", true);
+ branch.setCharPref("charKey", "string value");
+ branch.setIntPref("intKey", 1000);
+
+ is(branch.getBoolPref("boolKey"), true);
+ is(branch.getCharPref("charKey"), "string value");
+ is(branch.getIntPref("intKey"), 1000);
+
+ branch.setBoolPref("boolKey", false);
+ branch.setCharPref("charKey", "different string value");
+ branch.setIntPref("intKey", -2000);
+
+ is(branch.getBoolPref("boolKey"), false);
+ is(branch.getCharPref("charKey"), "different string value");
+ is(branch.getIntPref("intKey"), -2000);
+
+ is(typeof(branch.getBoolPref("boolKey")), "boolean");
+ is(typeof(branch.getCharPref("charKey")), "string");
+ is(typeof(branch.getIntPref("intKey")), "number");
+ });
+
+ add_task(function* test_multiple_branches() {
+ let branch1 = SharedPreferences.forAndroid("test1");
+ let branch2 = SharedPreferences.forAndroid("test2");
+
+ branch1.setBoolPref("boolKey", true);
+ branch2.setBoolPref("boolKey", false);
+
+ is(branch1.getBoolPref("boolKey"), true);
+ is(branch2.getBoolPref("boolKey"), false);
+
+ branch1.setCharPref("charKey", "a value");
+ branch2.setCharPref("charKey", "a different value");
+
+ is(branch1.getCharPref("charKey"), "a value");
+ is(branch2.getCharPref("charKey"), "a different value");
+ });
+
+ add_task(function* test_add_remove_observer() {
+ let branch = SharedPreferences.forAndroid("test");
+
+ branch.setBoolPref("boolKey", false);
+ is(branch.getBoolPref("boolKey"), false);
+
+ let obs1 = makeObserver();
+ branch.addObserver("boolKey", obs1);
+
+ try {
+ branch.setBoolPref("boolKey", true);
+ is(branch.getBoolPref("boolKey"), true);
+
+ let value1 = yield obs1.promise;
+ is(obs1.count, 1);
+
+ is(value1.subject, obs1);
+ is(value1.topic, "boolKey");
+ is(typeof(value1.data), "boolean");
+ is(value1.data, true);
+ } finally {
+ branch.removeObserver("boolKey", obs1);
+ }
+
+ // Make sure the original observer is really gone, or as close as
+ // we: install a second observer, wait for it to be notified, and
+ // then verify the original observer was *not* notified. This
+ // depends, of course, on the order that observers are notified, but
+ // is better than nothing.
+
+ let obs2 = makeObserver();
+ branch.addObserver("boolKey", obs2);
+
+ try {
+ branch.setBoolPref("boolKey", false);
+ is(branch.getBoolPref("boolKey"), false);
+
+ let value2 = yield obs2.promise;
+ is(obs2.count, 1);
+
+ is(value2.subject, obs2);
+ is(value2.topic, "boolKey");
+ is(typeof(value2.data), "boolean");
+ is(value2.data, false);
+
+ // Original observer count is preserved.
+ is(obs1.count, 1);
+ } finally {
+ branch.removeObserver("boolKey", obs2);
+ }
+ });
+
+ add_task(function* test_observer_ignores() {
+ let branch = SharedPreferences.forAndroid("test");
+
+ branch.setCharPref("charKey", "first value");
+ is(branch.getCharPref("charKey"), "first value");
+
+ let obs = makeObserver();
+ branch.addObserver("charKey", obs);
+
+ try {
+ // These should all be ignored.
+ branch.setBoolPref("boolKey", true);
+ branch.setBoolPref("boolKey", false);
+ branch.setIntPref("intKey", -3000);
+ branch.setIntPref("intKey", 4000);
+
+ branch.setCharPref("charKey", "a value");
+ let value = yield obs.promise;
+
+ // Observer should have been notified exactly once.
+ is(obs.count, 1);
+
+ is(value.subject, obs);
+ is(value.topic, "charKey");
+ is(typeof(value.data), "string");
+ is(value.data, "a value");
+ } finally {
+ branch.removeObserver("charKey", obs);
+ }
+ });
+
+ add_task(function* test_observer_ignores_branches() {
+ let branch = SharedPreferences.forAndroid("test");
+
+ branch.setCharPref("charKey", "first value");
+ is(branch.getCharPref("charKey"), "first value");
+
+ let obs = makeObserver();
+ branch.addObserver("charKey", obs);
+
+ try {
+ // These should all be ignored.
+ let branch2 = SharedPreferences.forAndroid("test2");
+ branch2.setCharPref("charKey", "a wrong value");
+ let branch3 = SharedPreferences.forAndroid("test.2");
+ branch3.setCharPref("charKey", "a different wrong value");
+
+ // This should not be ignored.
+ branch.setCharPref("charKey", "a value");
+
+ let value = yield obs.promise;
+
+ // Observer should have been notified exactly once.
+ is(obs.count, 1);
+
+ is(value.subject, obs);
+ is(value.topic, "charKey");
+ is(typeof(value.data), "string");
+ is(value.data, "a value");
+ } finally {
+ branch.removeObserver("charKey", obs);
+ }
+ });
+
+ add_task(function* test_scopes() {
+ let forApp = SharedPreferences.forApp();
+ let forProfile = SharedPreferences.forProfile();
+ let forProfileName = SharedPreferences.forProfileName("testProfile");
+ let forAndroidDefault = SharedPreferences.forAndroid();
+ let forAndroidBranch = SharedPreferences.forAndroid("testBranch");
+
+ forApp.setCharPref("charKey", "forApp");
+ forProfile.setCharPref("charKey", "forProfile");
+ forProfileName.setCharPref("charKey", "forProfileName");
+ forAndroidDefault.setCharPref("charKey", "forAndroidDefault");
+ forAndroidBranch.setCharPref("charKey", "forAndroidBranch");
+
+ is(forApp.getCharPref("charKey"), "forApp");
+ is(forProfile.getCharPref("charKey"), "forProfile");
+ is(forProfileName.getCharPref("charKey"), "forProfileName");
+ is(forAndroidDefault.getCharPref("charKey"), "forAndroidDefault");
+ is(forAndroidBranch.getCharPref("charKey"), "forAndroidBranch");
+ });
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=866271">Mozilla Bug 866271</a>
+<br>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184186">Migrated from Robocop testSharedPreferences</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_simple_discovery.html b/mobile/android/tests/browser/chrome/test_simple_discovery.html
new file mode 100644
index 0000000000..a8d84cfe52
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_simple_discovery.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=938571
+Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 938571</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript;version=1.7">
+
+ "use strict";
+
+ /*globals SimpleServiceDiscovery */
+
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/SimpleServiceDiscovery.jsm");
+
+ function discovery_observer(subject, topic, data) {
+ dump("Observer: " + data);
+
+ let service = SimpleServiceDiscovery.findServiceForID(data);
+ if (!service)
+ return;
+
+ is(service.friendlyName, "Pretend Device");
+ is(service.uuid, "uuid:5ec9ff92-e8b2-4a94-a72c-76b34e6dabb1");
+ is(service.manufacturer, "Copy Cat Inc.");
+ is(service.modelName, "Eureka Dongle");
+
+ SimpleTest.finish();
+ };
+
+ var testDevice = {
+ id: "test:dummy",
+ target: "test:service",
+ factory: function(service) { /* dummy */ },
+ types: ["video/mp4"],
+ extensions: ["mp4"]
+ };
+
+ function test_default() {
+ SimpleTest.registerCleanupFunction(function cleanup() {
+ SimpleServiceDiscovery.unregisterDevice(testDevice);
+ Services.obs.removeObserver(discovery_observer, "ssdp-service-found");
+ });
+
+ Services.obs.addObserver(discovery_observer, "ssdp-service-found", false);
+
+ // We need to register a device or processService will ignore us
+ SimpleServiceDiscovery.registerDevice(testDevice);
+
+ // Create a pretend service
+ let service = {
+ location: "http://mochi.test:8888/chrome/mobile/android/tests/browser/chrome/simpleservice.xml",
+ target: "test:service"
+ };
+
+ dump("Force a detailed ping from a pretend service");
+
+ // Poke the service directly to get the discovery to happen
+ SimpleServiceDiscovery._processService(service);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ test_default();
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=938571">Mozilla Bug 938571</a>
+<br>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184186">Migrated from Robocop testSimpleDiscovery</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_video_discovery.html b/mobile/android/tests/browser/chrome/test_video_discovery.html
new file mode 100644
index 0000000000..f6fb60bbe5
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_video_discovery.html
@@ -0,0 +1,154 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=953381
+Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 953381</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript;version=1.7">
+
+ "use strict";
+
+ /*globals SimpleServiceDiscovery */
+
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/SimpleServiceDiscovery.jsm");
+
+ // The chrome window
+ let chromeWin;
+
+ // Track the <browser> where the tests are happening
+ let browser;
+
+ function middle(element) {
+ let rect = element.getBoundingClientRect();
+ let x = (rect.right - rect.left) / 2 + rect.left;
+ let y = (rect.bottom - rect.top) / 2 + rect.top;
+ return [x, y];
+ }
+
+ // We must register a device and make a "mock" service for the device
+ var testDevice = {
+ id: "test:dummy",
+ target: "test:service",
+ factory: function(service) { /* dummy */ },
+ types: ["video/mp4", "video/webm"],
+ extensions: ["mp4", "webm"]
+ };
+
+ function setup_browser() {
+ chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let BrowserApp = chromeWin.BrowserApp;
+
+ SimpleTest.registerCleanupFunction(function cleanup() {
+ BrowserApp.closeTab(BrowserApp.getTabForBrowser(browser));
+ SimpleServiceDiscovery.unregisterDevice(testDevice);
+ });
+
+ // We need to register a device or processService will ignore us
+ SimpleServiceDiscovery.registerDevice(testDevice);
+
+ // Create a pretend service
+ let service = {
+ location: "http://mochi.test:8888/chrome/mobile/android/tests/browser/chrome/simpleservice.xml",
+ target: "test:service"
+ };
+
+ dump("Force a detailed ping from a pretend service");
+
+ // Poke the service directly to get the discovery to happen
+ SimpleServiceDiscovery._processService(service);
+
+ // Load our test web page with <video> elements
+ let url = "http://mochi.test:8888/chrome/mobile/android/tests/browser/chrome/video_discovery.html";
+ browser = BrowserApp.addTab(url, { selected: true, parentId: BrowserApp.selectedTab.id }).browser;
+ browser.addEventListener("load", function startTests(event) {
+ browser.removeEventListener("load", startTests, true);
+ Services.tm.mainThread.dispatch(test_video, Ci.nsIThread.DISPATCH_NORMAL);
+ }, true);
+ }
+
+ let videoDiscoveryTests = [
+ { id: "simple-mp4", source: "http://mochi.test:8888/simple.mp4", poster: "http://mochi.test:8888/simple.png", text: "simple video with mp4 src" },
+ { id: "simple-fail", pass: false, text: "simple video with no mp4 src" },
+ { id: "with-sources-mp4", source: "http://mochi.test:8888/simple.mp4", text: "video with mp4 extension source child" },
+ { id: "with-sources-webm", source: "http://mochi.test:8888/simple.webm", text: "video with webm extension source child" },
+ { id: "with-sources-fail", pass: false, text: "video with no mp4 extension source child" },
+ { id: "with-sources-mimetype-mp4", source: "http://mochi.test:8888/simple-video-mp4", text: "video with mp4 mimetype source child" },
+ { id: "with-sources-mimetype-webm", source: "http://mochi.test:8888/simple-video-webm", text: "video with webm mimetype source child" },
+ { id: "simple-fetch-pass", source: "http://mochi.test:8888/chrome/mobile/android/tests/browser/chrome/video_discovery.sjs?type=video/mp4", text: "simple video with mp4 mimetype fetched from server" },
+ { id: "simple-fetch-fail", pass: false, text: "simple video with non-video mimetype fetched from server" },
+ { id: "video-overlay", source: "http://mochi.test:8888/simple.mp4", text: "div overlay covering a simple video with mp4 src" }
+ ];
+
+ let expectedTests = videoDiscoveryTests.length;
+
+ function execute_video_test(test) {
+ let element = browser.contentDocument.getElementById(test.id);
+ if (element) {
+ let [x, y] = middle(element);
+ dump("Starting to getVideo");
+ chromeWin.CastingApps.getVideo(element, x, y, (video) => {
+ dump("Completed getVideo");
+ if (video) {
+ dump("video source: " + video.source);
+
+ let matchPoster = (test.poster == video.poster);
+ let matchSource = (test.source == video.source);
+ ok(matchPoster && matchSource && test.pass, test.text);
+ } else {
+ ok(!test.pass, test.text);
+ }
+ expectedTests--;
+ if (expectedTests == 0) {
+ SimpleTest.finish();
+ }
+ });
+ } else {
+ ok(false, "test element not found: [" + test.id + "]");
+ SimpleTest.finish();
+ }
+ }
+
+ function test_video() {
+ let videoTest;
+ while ((videoTest = videoDiscoveryTests.shift())) {
+ if (!("poster" in videoTest)) {
+ videoTest.poster = "";
+ }
+ if (!("pass" in videoTest)) {
+ videoTest.pass = true;
+ }
+ execute_video_test(videoTest);
+ }
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ // On debug runs, 10 assertions typically observed; 5 each of:
+ // - ASSERTION: cancel with non-failure status code: 'NS_FAILED(status)'
+ // - ASSERTION: OnDataAvailable implementation consumed no data: 'Error'
+ SimpleTest.expectAssertions(0,10);
+ setup_browser();
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=953381">Mozilla Bug 953381</a>
+<br>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184186">Migrated from Robocop testVideoDiscovery</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/test_web_channel.html b/mobile/android/tests/browser/chrome/test_web_channel.html
new file mode 100644
index 0000000000..3eeb5b5274
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_web_channel.html
@@ -0,0 +1,121 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1174458
+Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1174458</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript;version=1.7">
+
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components; /*global Components */
+
+ Cu.import("resource://gre/modules/Promise.jsm"); /*global Promise */
+ Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
+ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /*global XPCOMUtils */
+ Cu.import("resource://gre/modules/Task.jsm"); /*global Task */
+ XPCOMUtils.defineLazyModuleGetter(this, "WebChannel",
+ "resource://gre/modules/WebChannel.jsm"); /*global WebChannel */
+
+ const HTTP_PATH = "http://mochi.test:8888";
+ const HTTP_ENDPOINT = "/chrome/mobile/android/tests/browser/chrome/web_channel.html";
+
+ const gChromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let BrowserApp = gChromeWin.BrowserApp;
+
+ // TODO: consider if we want to run the original test in browser-chrome instead
+
+ // Keep this synced with /browser/base/content/test/general/browser_web_channel.js
+ // as much as possible. (We only have this since we can't run browser chrome
+ // tests on Android. Yet?)
+ let gTests = [
+ {
+ desc: "WebChannel generic message",
+ run: function* () {
+ return new Promise(function(resolve, reject) {
+ let tab;
+ let channel = new WebChannel("generic", Services.io.newURI(HTTP_PATH, null, null));
+ channel.listen(function (id, message, target) {
+ is(id, "generic");
+ is(message.something.nested, "hello");
+ channel.stopListening();
+ BrowserApp.closeTab(tab);
+ resolve();
+ });
+
+ tab = BrowserApp.addTab(HTTP_PATH + HTTP_ENDPOINT + "?generic");
+ });
+ }
+ },
+ {
+ desc: "WebChannel two way communication",
+ run: function* () {
+ return new Promise(function(resolve, reject) {
+ let tab;
+ let channel = new WebChannel("twoway", Services.io.newURI(HTTP_PATH, null, null));
+
+ channel.listen(function (id, message, sender) {
+ is(id, "twoway");
+ ok(message.command);
+
+ if (message.command === "one") {
+ channel.send({ data: { nested: true } }, sender);
+ }
+
+ if (message.command === "two") {
+ is(message.detail.data.nested, true);
+ channel.stopListening();
+ BrowserApp.closeTab(tab);
+ resolve();
+ }
+ });
+
+ tab = BrowserApp.addTab(HTTP_PATH + HTTP_ENDPOINT + "?twoway");
+ });
+ }
+ },
+ {
+ desc: "WebChannel multichannel",
+ run: function* () {
+ return new Promise(function(resolve, reject) {
+ let tab;
+ let channel = new WebChannel("multichannel", Services.io.newURI(HTTP_PATH, null, null));
+
+ channel.listen(function (id, message, sender) {
+ is(id, "multichannel");
+ BrowserApp.closeTab(tab);
+ resolve();
+ });
+
+ tab = BrowserApp.addTab(HTTP_PATH + HTTP_ENDPOINT + "?multichannel");
+ });
+ }
+ }
+ ]; // gTests
+
+ add_task(function* test() {
+ for (let test of gTests) {
+ info("Running: " + test.desc);
+ yield test.run();
+ }
+ });
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1174458">Mozilla Bug 1174458</a>
+<br>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184186">Migrated from Robocop testWebChannel</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/163.wrating.com/a.gif@a=&c=860010-0503010000 b/mobile/android/tests/browser/chrome/tp5/163.com/163.wrating.com/a.gif@a=&c=860010-0503010000
new file mode 100755
index 0000000000..35d42e808f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/163.wrating.com/a.gif@a=&c=860010-0503010000
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/163.wrating.com/a1.js b/mobile/android/tests/browser/chrome/tp5/163.com/163.wrating.com/a1.js
new file mode 100755
index 0000000000..9459267700
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/163.wrating.com/a1.js
@@ -0,0 +1 @@
+var vjAcc="";var wrUrl="httpdisabled://c.wrating.com/";var wrSv=0;function vjTrack(C){var B=vjValidateTrack();if(B===false){return }var A=wrUrl+"a.gif"+vjGetTrackImgUrl(C);void('<div style="display:none"><img src="'+A+'" id="wrTagImage" width="1" height="1"/></div>');vjSurveyCheck()}function vjEventTrack(D){var C=vjValidateTrack();if(C===false){return }var B=wrUrl+"a.gif"+vjGetTrackImgUrl(D);var A=new Image();A.src=B;A.onloaddisabled=function(){}}function vjValidateTrack(){if(document.location.protocol=="file:"){return false}if(vjAcc==""){return false}else{if(wrUrl.substr(wrUrl.length-1,1)!="/"){wrUrl+="/"}}return true}function vjGetTrackImgUrl(S){var M=0;var N="expires=Fri, 1 Jan 2038 00:00:00 GMT;";var T=document.location;var P=document.referrer.toString();var D;var H=vjGetDomainFromUrl(T);var K;var V;var Y="";var L=vjFlash();var G="";var Z="";var J="";var O=navigator.appName+" "+navigator.appVersion;var F=new Date();var X=F.getTimezoneOffset()/-60;var A=0;var U="";var R="";if(typeof (H[1])!="undefined"){V=H[1]}else{if(typeof (H[0])!="undefined"){V=H[0]}}if(P!=""){Y=vjGetKeyword(P)}else{if((O.indexOf("MSIE")>=0)&&(parseInt(O.substr(O.indexOf("MSIE")+5),4)>=5)&&(O.indexOf("Mac")==-1)&&(navigator.userAgent.indexOf("Opera")==-1)){try{document.documentElement.addBehavior("#default#homePage");if(document.documentElement.isHomePage(location.href)){P="ishomepage"}}catch(W){}}}if(navigator.cookieEnabled){M=1}if(self.screen){G=screen.width+"x"+screen.height+"x"+screen.colorDepth}else{if(self.java){var Q=java.awt.Toolkit.getDefaultToolkit().getScreenSize();G=Q.width+"x"+Q.height+"x0"}}if(navigator.language){Z=navigator.language.toLowerCase()}else{if(navigator.browserLanguage){Z=navigator.browserLanguage.toLowerCase()}else{Z="-"}}if(navigator.javaEnabled()){A=1}if(M==1){D=document.cookie;if(D.indexOf("vjuids=")<0){K=vjVisitorID();document.cookie="vjuids="+escape(K)+";"+N+";domain="+V+";path=/;"}else{K=vjGetCookie("vjuids")}if(D.indexOf("vjlast=")<0){U="30";var E=vjGetTimestamp(F.getTime()).toString();R=E+"."+E+".30"}else{var a=vjGetCookie("vjlast");var C=a.split(".");var B="";if(typeof (C[0])!="undefined"){R=C[0].toString()}else{R=vjGetTimestamp(F.getTime()).toString()}if(typeof (C[1])!="undefined"){var I=new Date(parseInt(C[1])*1000);if(I.toDateString()!=F.toDateString()){R+="."+vjGetTimestamp(F.getTime()).toString();if(parseInt(vjGetTimestamp(F.getTime())-parseInt(C[1]))/86400>30){U="2"}else{U="1"}if(typeof (C[2])!="undefined"){U+=C[2].substr(0,1)}else{U+="0"}}else{R+="."+C[1].toString();if(typeof (C[2])!="undefined"){U+=C[2]}else{U="10"}}}else{R+="."+vjGetTimestamp(F.getTime()).toString();if(typeof (C[2])!="undefined"){U+=C[2]}else{U="10"}}R+="."+U}document.cookie="vjlast="+R+";"+N+";domain="+V+";path=/;"}J="?a="+F.getTime().toString(16)+"&t=&i="+escape(K);J+="&b="+escape(T)+"&c="+vjAcc;J+="&s="+G+"&l="+Z;J+="&z="+X+"&j="+A+"&f="+escape(L);if(P!=""){J+="&r="+escape(P)+"&kw="+Y}J+="&ut="+U+"&n=";if(typeof (S)=="undefined"){J+="&js="}else{J+="&js="+escape(S)}J+="&ck="+M;return J}function vjGetTimestamp(A){return Math.round(A/1000)}function vjGetKeyword(C){var A=[["baidu","wd"],["baidu","q1"],["google","q"],["google","as_q"],["yahoo","p"],["msn","q"],["live","q"],["sogou","query"],["youdao","q"],["soso","w"],["zhongsou","w"],["zhongsou","w1"]];var B=vjGetDomainFromUrl(C.toString().toLowerCase());var D=-1;var E="";if(typeof (B[0])=="undefined"){return""}for(i=0;i<A.length;i++){if(B[0].indexOf("."+A[i][0]+".")>=0){D=-1;D=C.indexOf("&"+A[i][1]+"=");if(D<0){D=C.indexOf("?"+A[i][1]+"=")}if(D>=0){E=C.substr(D+A[i][1].length+2,C.length-(D+A[i][1].length+2));D=E.indexOf("&");if(D>=0){E=E.substr(0,D)}if(E==""){return""}else{return A[i][0]+"|"+E}}}}return""}function vjGetDomainFromUrl(E){if(E==""){return false}E=E.toString().toLowerCase();var F=[];var C=E.indexOf("//")+2;var B=E.substr(C,E.length-C);var A=B.indexOf("/");if(A>=0){F[0]=B.substr(0,A)}else{F[0]=B}var D=F[0].match(/[^.]+\.(com.cn|net.cn|gov.cn|cn|com|net|org|gov|cc|biz|info)+$/);if(D){if(typeof (D[0])!="undefined"){F[1]=D[0]}}return F}function vjVisitorID(){var A=vjHash(document.location+document.cookie+document.referrer).toString(16);var B=new Date();return A+"."+B.getTime().toString(16)+"."+Math.random().toString(16)}function vjHash(C){if(!C||C==""){return 0}var B=0;for(var A=C.length-1;A>=0;A--){var D=parseInt(C.charCodeAt(A));B=(B<<5)+B+D}return B}function vjGetCookie(D){var B=D+"=";var F=B.length;var A=document.cookie.length;var E=0;while(E<A){var C=E+F;if(document.cookie.substring(E,C)==B){return vjGetCookieVal(C)}E=document.cookie.indexOf(" ",E)+1;if(E==1){break}}return null}function vjGetCookieVal(B){var A=document.cookie.indexOf(";",B);if(A==-1){A=document.cookie.length}return unescape(document.cookie.substring(B,A))}function vjFlash(){var _flashVer="-";var _navigator=navigator;if(_navigator.plugins&&_navigator.plugins.length){for(var ii=0;ii<_navigator.plugins.length;ii++){if(_navigator.plugins[ii].name.indexOf("Shockwave Flash")!=-1){_flashVer=_navigator.plugins[ii].description.split("Shockwave Flash ")[1];break}}}else{if(window.ActiveXObject){for(var ii=10;ii>=2;ii--){try{var fl=eval("new ActiveXObject('ShockwaveFlash.ShockwaveFlash."+ii+"');");if(fl){_flashVer=ii+".0";break}}catch(e){}}}}return _flashVer}function vjSurveyCheck(){if(wrSv<=0){return }var C=new Date();var A=C.getTime();var D=Math.random(A);if(D<=parseFloat(1/wrSv)){var B=document.createElement("script");B.type="text/javascript";B.id="wratingSuevey";B.src="httpdisabled://tongji.wrating.com/survey/check.php?c="+vjAcc;document.getElementsByTagName("head")[0].appendChild(B)}}; \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/adgeo.163.com/ad_cookies b/mobile/android/tests/browser/chrome/tp5/163.com/adgeo.163.com/ad_cookies
new file mode 100755
index 0000000000..e69de29bb2
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/adgeo.163.com/ad_cookies
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/analytics.163.com/ntes.js b/mobile/android/tests/browser/chrome/tp5/163.com/analytics.163.com/ntes.js
new file mode 100755
index 0000000000..2654147afc
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/analytics.163.com/ntes.js
@@ -0,0 +1 @@
+var _ntes_nacc,_ntes_nvid="VISITOR_CLIENT_NO_COOKIE_SUPPORT",_ntes_nvtm=0,_ntes_nvfi=0,_ntes_nvsf=0,_ntes_nstm=0,_ntes_nurl="",_ntes_ntit="",_ntes_nref="",_ntes_nres="",_ntes_nlag="",_ntes_nscd="",_ntes_nlmf=0,_ntes_flsh="",_ntes_nssn="",_ntes_surv=0;function _ntes_void(){}var _ntes_domain_array=['163.com','188.com','netease.com','126.com','126.net','nease.net','yeah.net','gz2010.cn','co188.com','warcraftchina.com','youdao.com','leihuo.net','gzapg2010.cn'],_ntes_cdmn=ntes_get_domain(),_ntes_src_addr="//analytics.163.com";function neteaseTracker(){_ntes_nurl=escape(document.location);_ntes_ntit=escape(document.title);_ntes_nref=escape(document.referrer);_ntes_flsh=ntes_get_flashver();var now=(new Date()).getTime();document.cookie="__ntes__test__cookies="+now;var _ntes_cookie_enabled=(ntes_get_cookie("__ntes__test__cookies")==now)?true:false;if(_ntes_nacc=="undefined"||!_ntes_nacc)return;if(_ntes_nurl.indexOf("httpdisabled")!=0)return;var run_flag=0;for(i=0;i<_ntes_domain_array.length;i++){if(_ntes_cdmn=="."+_ntes_domain_array[i]){run_flag=1;break}}if(run_flag==1){var _ck_str=ntes_get_cookie("_ntes_nnid");if(_ck_str==-1){if(_ntes_cookie_enabled){_ntes_nvid=fetch_visitor_hash();_ntes_nvfi=1;ntes_set_cookie_long("_ntes_nnid",_ntes_nvid+",0")}}else{var _id_pos=_ck_str.indexOf(","),_suv_pos=_ck_str.indexOf("|");if(_suv_pos==-1)_suv_pos=_ck_str.length;_ntes_nvid=_ck_str.substr(0,_id_pos);_ntes_surv=_ck_str.substr(_id_pos+1,_suv_pos-_id_pos-1);if(_ntes_surv==''||(_ntes_surv!=0&&(now-_ntes_surv)>365*86400)){_ntes_surv=0}ntes_set_cookie_long("_ntes_nnid",_ntes_nvid+","+_ntes_surv)}_ntes_nssn=ntes_get_cookie("P_INFO");if(_ntes_nssn==-1){_ntes_nssn=""}else{_ntes_nssn=_ntes_nssn.substr(0,_ntes_nssn.indexOf("|"))}ntes_get_navigation_info();var _ntes_q="_nacc="+_ntes_nacc;_ntes_q+="&_nvid="+_ntes_nvid+"&_nvtm="+_ntes_nvtm;_ntes_q+="&_nvsf="+_ntes_nvsf+"&_nvfi="+_ntes_nvfi;_ntes_q+="&_nlag="+_ntes_nlag+"&_nlmf="+_ntes_nlmf;_ntes_q+="&_nres="+_ntes_nres+"&_nscd="+_ntes_nscd;_ntes_q+="&_nstm="+_ntes_nstm;_ntes_q+="&_nurl="+_ntes_nurl+"&_ntit="+_ntes_ntit;_ntes_q+="&_nref="+_ntes_nref+"&_nfla="+_ntes_flsh;_ntes_q+="&_nssn="+_ntes_nssn;_ntes_q+="&_nxkey="+(Number(new Date())+''+Math.random()).substring(6,20)+"&_end1";var _img=new Image();_img.src=_ntes_src_addr+"/ntes?"+_ntes_q;_img.onloaddisabled=function(){_ntes_void()};if(1){if(_ntes_nacc!="analytics"&&_ntes_nacc!="research"&&_ntes_nurl.indexOf("httpdisabledsdisabled")!=0){ntes_survey_popup()}}}}function ntes_survey_popup(){if(_ntes_surv==0){if(typeof(_ntes_nvid)=="undefined"||_ntes_nvid.length!=32||_ntes_nvid.substr(30,2)!="23")return;var _ntes_survey_url="//research.163.com/survey/";_ntes_survey_url=_ntes_survey_url+"?_nacc="+_ntes_nacc+"&_nvid="+_ntes_nvid;void(_ntes_survey_url,'','width=680,height=450,top=100,left=120,scrollbars=yes');ntes_set_cookie_long("_ntes_nnid",_ntes_nvid+","+(new Date).getTime())}}function ntes_get_navigation_info(){_ntes_nres="-";_ntes_nscd="-";_ntes_nlag="-";if(self.screen){_ntes_nres=screen.width+"x"+screen.height;_ntes_nscd=screen.colorDepth+"-bit"}else if(self.java){var j=java.awt.Toolkit.getDefaultToolkit(),s=j.getScreenSize();_ntes_nres=s.width+"x"+s.height}if(navigator.language){_ntes_nlag=navigator.language.toLowerCase()}else if(navigator.browserLanguage){_ntes_nlag=navigator.browserLanguage.toLowerCase()}var d=new Date(document.lastModified);_ntes_nlmf=d.getTime()/1000}function fetch_visitor_hash(){var d=new Date(),xy=document.body.clientWidth+":"+document.body.clientHeight,s=str_to_ent(d.getTime()+Math.random()+document.location+document.referrer+screen.width+screen.height+navigator.userAgent+document.cookie+xy);return ntes_hex_md5(s)}function ntes_get_domain(){var domain_name=document.domain,arr_domain_name=domain_name.split("."),domain_name_length=arr_domain_name.length,pattern=/^\d+$/g;if(pattern.test(arr_domain_name[domain_name_length-1])){return domain_name}if(arr_domain_name.length<3){return "."+domain_name}var domain_suffixes=['com','net','org','gov','co'],i,suffix_found=false;for(i=0;i<domain_suffixes.length;i++){if(arr_domain_name[domain_name_length-2]==domain_suffixes[i]){suffix_found=true}}if(!suffix_found){return "."+arr_domain_name[domain_name_length-2]+"."+arr_domain_name[domain_name_length-1]}else{return "."+arr_domain_name[domain_name_length-3]+"."+arr_domain_name[domain_name_length-2]+"."+arr_domain_name[domain_name_length-1]}}function ntes_set_cookie_long(name,value){var _ntes_epd=new Date();_ntes_epd.setTime(_ntes_epd.getTime()+1000*60*60*24*365*100);document.cookie=name+"="+value+";expires="+_ntes_epd.toGMTString()+";path=/;domain="+_ntes_cdmn}function ntes_set_cookie(name,value){var _ntes_epd=new Date();_ntes_epd.setTime(_ntes_epd.getTime()+0);document.cookie=name+"="+value+";path=/;domain="+_ntes_cdmn}function ntes_set_cookie_new(name,value,expires){if(!expires||expires==""){expires=1000*60*60*24*365*1}var _ntes_epd=new Date();_ntes_epd.setTime(_ntes_epd.getTime()+expires);document.cookie=name+"="+value+";expires="+_ntes_epd.toGMTString()+";path=/;domain="+_ntes_cdmn}function ntes_get_cookie(name){var _ntes_dc=document.cookie,_ntes_cname=name+"=",_ntes_clen=_ntes_dc.length,_ntes_cbegin=0;while(_ntes_cbegin<_ntes_clen){var _ntes_vbegin=_ntes_cbegin+_ntes_cname.length;if(_ntes_dc.substring(_ntes_cbegin,_ntes_vbegin)==_ntes_cname){var _ntes_vend=_ntes_dc.indexOf(";",_ntes_vbegin);if(_ntes_vend==-1)_ntes_vend=_ntes_clen;return unescape(_ntes_dc.substring(_ntes_vbegin,_ntes_vend))}_ntes_cbegin=_ntes_dc.indexOf(" ",_ntes_cbegin)+1;if(_ntes_cbegin==0)break}return-1}function ntes_get_flashver(){var f="",n=navigator;if(n.plugins&&n.plugins.length){for(var ii=0;ii<n.plugins.length;ii++){if(n.plugins[ii].name.indexOf('Shockwave Flash')!=-1){f=n.plugins[ii].description.split('Shockwave Flash')[1];break}}}else if(window.ActiveXObject){for(var ii=10;ii>=2;ii--){try{var fl=eval("new ActiveXObject('ShockwaveFlash.ShockwaveFlash."+ii+"');");if(fl){f=ii+'.0';break}}catch(e){}}}return f}var _ntes_hexcase=0,_ntes_chrsz=8;function ntes_hex_md5(s){return binl2hex(ntes_core_md5(str2binl(s),s.length*_ntes_chrsz))}function ntes_core_md5(x,len){x[len>>5]|=0x80<<((len)%32);x[(((len+64)>>>9)<<4)+14]=len;var a=1732584193,b=-271733879,c=-1732584194,d=271733878;for(var i=0;i<x.length;i+=16){var olda=a,oldb=b,oldc=c,oldd=d;a=md5_ff(a,b,c,d,x[i+0],7,-680876936);d=md5_ff(d,a,b,c,x[i+1],12,-389564586);c=md5_ff(c,d,a,b,x[i+2],17,606105819);b=md5_ff(b,c,d,a,x[i+3],22,-1044525330);a=md5_ff(a,b,c,d,x[i+4],7,-176418897);d=md5_ff(d,a,b,c,x[i+5],12,1200080426);c=md5_ff(c,d,a,b,x[i+6],17,-1473231341);b=md5_ff(b,c,d,a,x[i+7],22,-45705983);a=md5_ff(a,b,c,d,x[i+8],7,1770035416);d=md5_ff(d,a,b,c,x[i+9],12,-1958414417);c=md5_ff(c,d,a,b,x[i+10],17,-42063);b=md5_ff(b,c,d,a,x[i+11],22,-1990404162);a=md5_ff(a,b,c,d,x[i+12],7,1804603682);d=md5_ff(d,a,b,c,x[i+13],12,-40341101);c=md5_ff(c,d,a,b,x[i+14],17,-1502002290);b=md5_ff(b,c,d,a,x[i+15],22,1236535329);a=md5_gg(a,b,c,d,x[i+1],5,-165796510);d=md5_gg(d,a,b,c,x[i+6],9,-1069501632);c=md5_gg(c,d,a,b,x[i+11],14,643717713);b=md5_gg(b,c,d,a,x[i+0],20,-373897302);a=md5_gg(a,b,c,d,x[i+5],5,-701558691);d=md5_gg(d,a,b,c,x[i+10],9,38016083);c=md5_gg(c,d,a,b,x[i+15],14,-660478335);b=md5_gg(b,c,d,a,x[i+4],20,-405537848);a=md5_gg(a,b,c,d,x[i+9],5,568446438);d=md5_gg(d,a,b,c,x[i+14],9,-1019803690);c=md5_gg(c,d,a,b,x[i+3],14,-187363961);b=md5_gg(b,c,d,a,x[i+8],20,1163531501);a=md5_gg(a,b,c,d,x[i+13],5,-1444681467);d=md5_gg(d,a,b,c,x[i+2],9,-51403784);c=md5_gg(c,d,a,b,x[i+7],14,1735328473);b=md5_gg(b,c,d,a,x[i+12],20,-1926607734);a=md5_hh(a,b,c,d,x[i+5],4,-378558);d=md5_hh(d,a,b,c,x[i+8],11,-2022574463);c=md5_hh(c,d,a,b,x[i+11],16,1839030562);b=md5_hh(b,c,d,a,x[i+14],23,-35309556);a=md5_hh(a,b,c,d,x[i+1],4,-1530992060);d=md5_hh(d,a,b,c,x[i+4],11,1272893353);c=md5_hh(c,d,a,b,x[i+7],16,-155497632);b=md5_hh(b,c,d,a,x[i+10],23,-1094730640);a=md5_hh(a,b,c,d,x[i+13],4,681279174);d=md5_hh(d,a,b,c,x[i+0],11,-358537222);c=md5_hh(c,d,a,b,x[i+3],16,-722521979);b=md5_hh(b,c,d,a,x[i+6],23,76029189);a=md5_hh(a,b,c,d,x[i+9],4,-640364487);d=md5_hh(d,a,b,c,x[i+12],11,-421815835);c=md5_hh(c,d,a,b,x[i+15],16,530742520);b=md5_hh(b,c,d,a,x[i+2],23,-995338651);a=md5_ii(a,b,c,d,x[i+0],6,-198630844);d=md5_ii(d,a,b,c,x[i+7],10,1126891415);c=md5_ii(c,d,a,b,x[i+14],15,-1416354905);b=md5_ii(b,c,d,a,x[i+5],21,-57434055);a=md5_ii(a,b,c,d,x[i+12],6,1700485571);d=md5_ii(d,a,b,c,x[i+3],10,-1894986606);c=md5_ii(c,d,a,b,x[i+10],15,-1051523);b=md5_ii(b,c,d,a,x[i+1],21,-2054922799);a=md5_ii(a,b,c,d,x[i+8],6,1873313359);d=md5_ii(d,a,b,c,x[i+15],10,-30611744);c=md5_ii(c,d,a,b,x[i+6],15,-1560198380);b=md5_ii(b,c,d,a,x[i+13],21,1309151649);a=md5_ii(a,b,c,d,x[i+4],6,-145523070);d=md5_ii(d,a,b,c,x[i+11],10,-1120210379);c=md5_ii(c,d,a,b,x[i+2],15,718787259);b=md5_ii(b,c,d,a,x[i+9],21,-343485551);a=safe_add(a,olda);b=safe_add(b,oldb);c=safe_add(c,oldc);d=safe_add(d,oldd)}return Array(a,b,c,d)}function md5_cmn(q,a,b,x,s,t){return safe_add(bit_rol(safe_add(safe_add(a,q),safe_add(x,t)),s),b)}function md5_ff(a,b,c,d,x,s,t){return md5_cmn((b&c)|((~b)&d),a,b,x,s,t)}function md5_gg(a,b,c,d,x,s,t){return md5_cmn((b&d)|(c&(~d)),a,b,x,s,t)}function md5_hh(a,b,c,d,x,s,t){return md5_cmn(b^c^d,a,b,x,s,t)}function md5_ii(a,b,c,d,x,s,t){return md5_cmn(c^(b|(~d)),a,b,x,s,t)}function safe_add(x,y){var lsw=(x&0xFFFF)+(y&0xFFFF),msw=(x>>16)+(y>>16)+(lsw>>16);return(msw<<16)|(lsw&0xFFFF)}function bit_rol(num,cnt){return(num<<cnt)|(num>>>(32-cnt))}function str2binl(str){var bin=new Array(),mask=(1<<_ntes_chrsz)-1;for(var i=0;i<str.length*_ntes_chrsz;i+=_ntes_chrsz)bin[i>>5]|=(str.charCodeAt(i/_ntes_chrsz)&mask)<<(i%32);return bin}function binl2hex(binarray){var hex_tab=_ntes_hexcase?"0123456789ABCDEF":"0123456789abcdef",str="";for(var i=0;i<binarray.length*4;i++){str+=hex_tab.charAt((binarray[i>>2]>>((i%4)*8+4))&0xF)+hex_tab.charAt((binarray[i>>2]>>((i%4)*8))&0xF)}return str}function str_to_ent(str){var result='',i;for(i=0;i<str.length;i++){var c=str.charCodeAt(i),tmp='';if(c>255){while(c>=1){tmp="0123456789".charAt(c%10)+tmp;c=c/10}if(tmp==''){tmp="0"}tmp="#"+tmp;tmp="&"+tmp;tmp=tmp+";";result+=tmp}else{result+=str.charAt(i)}}return result}function ntes_page_click_stat(obj){var _ntes_a_h=escape(this.href),_ntes_a_t=escape(this.innerHTML),_ntes_p_url=escape(document.location),_ncw=0,_nmx=0,_nmy=0;if(document.documentElement&&document.documentElement.clientWidth){_ncw=document.documentElement.clientWidth}var evt=obj||window.event;if(evt.clientX&&document.documentElement){_nmx=evt.clientX+document.documentElement.scrollLeft;_nmy=evt.clientY+document.documentElement.scrollTop}var _ntes_p_q="_nacc="+_ntes_nacc+"&_npurl="+_ntes_p_url;_ntes_p_q+="&_nah="+_ntes_a_h+"&_nat="+_ntes_a_t;_ntes_p_q+="&_ncw="+_ncw+"&_nmx="+_nmx+"&_nmy="+_nmy;_ntes_p_q+="&_end";var i=new Image();i.src=_ntes_src_addr+"/ntes_p?"+_ntes_p_q;i.onloaddisabled=function(){_ntes_void()};return true}function neteaseClickStat(){if(typeof(_ntes_nacc)=="undefined"||!_ntes_nacc){return}var _ntes_r=Math.random();_ntes_r=Math.round(_ntes_r*30);if(_ntes_r!=15)return;if(document.all&&navigator.userAgent.match(/msie/gi)){var _ntes_a_tag_array=document.getElementsByTagName('a');for(i in _ntes_a_tag_array){if(_ntes_a_tag_array[i].onclick==null){_ntes_a_tag_array[i].onclick=ntes_page_click_stat}}}}function recordAction(aName,vAddr,vName,p1,p2){var _q="";_q+="s="+_ntes_nacc;_q+="&u="+_ntes_nvid;_q+="&a="+escape(aName);_q+="&v="+escape(vAddr);_q+="&n="+escape(vName);_q+="&p1="+p1;if(p2!=undefined)_q+="&p2="+p2;_q+="&r="+_ntes_nurl;_q+="&_nxkey="+(Number(new Date())+''+Math.random()).substring(6,20)+"&_end1";var _img=new Image();_img.src=_ntes_src_addr+"/ntesv?"+_q;_img.onloaddisabled=function(){_ntes_void()}} \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/jr@site=netease&affiliate=homepage&cat=homepage&type=adend&location=1 b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/jr@site=netease&affiliate=homepage&cat=homepage&type=adend&location=1
new file mode 100755
index 0000000000..8b0c6b7633
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/jr@site=netease&affiliate=homepage&cat=homepage&type=adend&location=1
@@ -0,0 +1 @@
+var adrichend; \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/jr@site=netease&affiliate=homepage&cat=homepage&type=popup&location=1 b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/jr@site=netease&affiliate=homepage&cat=homepage&type=popup&location=1
new file mode 100755
index 0000000000..1662930217
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/jr@site=netease&affiliate=homepage&cat=homepage&type=popup&location=1
@@ -0,0 +1 @@
+a=1; \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=banner360x65&location=1.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=banner360x65&location=1.html
new file mode 100755
index 0000000000..b6042af085
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=banner360x65&location=1.html
@@ -0,0 +1 @@
+<a target="_blank" href="httpdisabled://g.163.com/c?AID=30417&FlightID=739&Values=1131919305&Redirect=http://l.163.com/indi/april.html"><img src="../img3.126.net/techpro/shangpin/20110331/36-65.jpg" border=0 height=65 width=360 alt=""></a> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=banner360x65&location=2.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=banner360x65&location=2.html
new file mode 100755
index 0000000000..fbcd8872a0
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=banner360x65&location=2.html
@@ -0,0 +1 @@
+<a target="_blank" href="httpdisabled://g.163.com/c?AID=33940&FlightID=740&Values=2875939796&Redirect=http://t.163.com/zt/2011"><img src="../img2.126.net/xoimages/sales/2011/04/wb/360x65.jpg" border=0 height=65 width=360 alt=""></a> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column360x100&location=1.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column360x100&location=1.html
new file mode 100755
index 0000000000..8425685c36
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column360x100&location=1.html
@@ -0,0 +1 @@
+<a target="_blank" href="httpdisabled://g.163.com/c?AID=34355&FlightID=762&Values=868630560&Redirect=http://hr.163.com/hangzhou/"><img src="../img2.126.net/xoimages/hr/20110216/hz/360x100.jpg" border=0 height=100 width=360 alt=""></a> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column360x100&location=2.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column360x100&location=2.html
new file mode 100755
index 0000000000..26926d4089
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column360x100&location=2.html
@@ -0,0 +1,43 @@
+<!-- Sniffer Code for Flash version=80 -->
+<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
+<SCRIPT LANGUAGE=JavaScript>
+<!--
+var swf_click = "httpdisabled://g.163.com/c?AID=33867&FlightID=747&Values=1741461550&Redirect=http://cps.mbaobao.com/cps/25892";
+var dcswf_click = escape(swf_click);
+var ShockMode = 0;
+var plugin = (navigator.mimeTypes && navigator.mimeTypes["application/x-shockwave-flash"]) ? navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin : 0;
+
+if (plugin && parseInt(plugin.description.substring(plugin.description.indexOf(".")-2)) >= 8)
+{
+ShockMode = 1;
+}
+else if (navigator.userAgent && navigator.userAgent.indexOf("MSIE")>=0
+&& navigator.userAgent.indexOf("Opera")<0) {
+void('<SCRIPT LANGUAGE=VBScript\> \n');
+void('on error resume next \n');
+void('ShockMode = (IsObject(CreateObject("ShockwaveFlash.ShockwaveFlash.8")))\n');
+void('<\/SCRIPT\> \n');
+}
+if ( ShockMode ) {
+void('<objectdisabled classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"');
+void(' codebase="httpdisabled://downloaddisabled.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0"');
+void(' ID=flashad WIDTH=360 HEIGHT=100>');
+void(' <PARAM NAME=movie VALUE="httpdisabled://img1.126.net/channel5/008995/mbb360100_110406.swf?clickTag='+dcswf_click+'"> ');
+void(' <PARAM NAME=quality VALUE=autohigh> ');
+void(' <PARAM NAME=wmode VALUE=opaque> ');
+void(' <embeddisabled SRC="httpdisabled://img1.126.net/channel5/008995/mbb360100_110406.swf?clickTag='+dcswf_click+'" QUALITY=autohigh wmode=opaque');
+void(' NAME=flashad swLiveConnect=TRUE WIDTH=360 HEIGHT=100');
+void(' TYPE="application/x-shockwave-flash" PLUGINSPAGE="httpdisabled://www.macromedia.com/shockwave/downloaddisabled/index.cgi?P1_Prod_Version=ShockwaveFlash">');
+void('</EMBED>');
+void('</OBJECT>');
+} else if (!(navigator.appName && navigator.appName.indexOf("Netscape")>=0 && navigator.appVersion.indexOf("2.")>=0)){
+void('<A HREF="httpdisabled://g.163.com/c?AID=33867&FlightID=747&Values=1741461550&Redirect=http://cps.mbaobao.com/cps/25892" target="_blank"><IMG SRC="httpdisabled://img1.126.net/channel5/008995/mbb360100_110406.swf" WIDTH=360 HEIGHT=100 BORDER=0></A>');
+}
+//-->
+</SCRIPT>
+<NOEMBED>
+<A HREF="httpdisabled://g.163.com/c?AID=33867&FlightID=747&Values=1741461550&Redirect=http://cps.mbaobao.com/cps/25892" target="_blank"><IMG SRC="../img1.126.net/channel5/008995/mbb360100_110406.swf" WIDTH=360 HEIGHT=100 BORDER=0></A>
+</NOEMBED>
+<NOSCRIPT>
+<A HREF="httpdisabled://g.163.com/c?AID=33867&FlightID=747&Values=1741461550&Redirect=http://cps.mbaobao.com/cps/25892" target="_blank"><IMG SRC="../img1.126.net/channel5/008995/mbb360100_110406.swf" WIDTH=360 HEIGHT=100 BORDER=0></A>
+</NOSCRIPT> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column360x100&location=3.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column360x100&location=3.html
new file mode 100755
index 0000000000..995a540329
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column360x100&location=3.html
@@ -0,0 +1,43 @@
+<!-- Sniffer Code for Flash version=80 -->
+<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
+<SCRIPT LANGUAGE=JavaScript>
+<!--
+var swf_click = "httpdisabled://g.163.com/c?AID=33925&FlightID=764&Values=3275702516&Redirect=http://a814.oadz.com/link/C/814/88700/Is4Y9DtmCqUv0pLFAc-xPJ0f8ts_/a/108/http://www.m18.com/market/front.aspx?pno=wan-gm-hp-00zty&url=/im";
+var dcswf_click = escape(swf_click);
+var ShockMode = 0;
+var plugin = (navigator.mimeTypes && navigator.mimeTypes["application/x-shockwave-flash"]) ? navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin : 0;
+
+if (plugin && parseInt(plugin.description.substring(plugin.description.indexOf(".")-2)) >= 8)
+{
+ShockMode = 1;
+}
+else if (navigator.userAgent && navigator.userAgent.indexOf("MSIE")>=0
+&& navigator.userAgent.indexOf("Opera")<0) {
+void('<SCRIPT LANGUAGE=VBScript\> \n');
+void('on error resume next \n');
+void('ShockMode = (IsObject(CreateObject("ShockwaveFlash.ShockwaveFlash.8")))\n');
+void('<\/SCRIPT\> \n');
+}
+if ( ShockMode ) {
+void('<objectdisabled classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"');
+void(' codebase="httpdisabled://downloaddisabled.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0"');
+void(' ID=flashad WIDTH=360 HEIGHT=100>');
+void(' <PARAM NAME=movie VALUE="httpdisabled://img1.126.net/channel5/008969/mai360100_110401.swf?clickTag='+dcswf_click+'"> ');
+void(' <PARAM NAME=quality VALUE=autohigh> ');
+void(' <PARAM NAME=wmode VALUE=opaque> ');
+void(' <embeddisabled SRC="httpdisabled://img1.126.net/channel5/008969/mai360100_110401.swf?clickTag='+dcswf_click+'" QUALITY=autohigh wmode=opaque');
+void(' NAME=flashad swLiveConnect=TRUE WIDTH=360 HEIGHT=100');
+void(' TYPE="application/x-shockwave-flash" PLUGINSPAGE="httpdisabled://www.macromedia.com/shockwave/downloaddisabled/index.cgi?P1_Prod_Version=ShockwaveFlash">');
+void('</EMBED>');
+void('</OBJECT>');
+} else if (!(navigator.appName && navigator.appName.indexOf("Netscape")>=0 && navigator.appVersion.indexOf("2.")>=0)){
+void('<A HREF="httpdisabled://g.163.com/c?AID=33925&FlightID=764&Values=3275702516&Redirect=http://a814.oadz.com/link/C/814/88700/Is4Y9DtmCqUv0pLFAc-xPJ0f8ts_/a/108/http://www.m18.com/market/front.aspx?pno=wan-gm-hp-00zty&url=/im" target="_blank"><IMG SRC="httpdisabled://img1.126.net/channel5/008969/mai360100_110401.swf" WIDTH=360 HEIGHT=100 BORDER=0></A>');
+}
+//-->
+</SCRIPT>
+<NOEMBED>
+<A HREF="httpdisabled://g.163.com/c?AID=33925&FlightID=764&Values=3275702516&Redirect=http://a814.oadz.com/link/C/814/88700/Is4Y9DtmCqUv0pLFAc-xPJ0f8ts_/a/108/http://www.m18.com/market/front.aspx?pno=wan-gm-hp-00zty&url=/im" target="_blank"><IMG SRC="../img1.126.net/channel5/008969/mai360100_110401.swf" WIDTH=360 HEIGHT=100 BORDER=0></A>
+</NOEMBED>
+<NOSCRIPT>
+<A HREF="httpdisabled://g.163.com/c?AID=33925&FlightID=764&Values=3275702516&Redirect=http://a814.oadz.com/link/C/814/88700/Is4Y9DtmCqUv0pLFAc-xPJ0f8ts_/a/108/http://www.m18.com/market/front.aspx?pno=wan-gm-hp-00zty&url=/im" target="_blank"><IMG SRC="../img1.126.net/channel5/008969/mai360100_110401.swf" WIDTH=360 HEIGHT=100 BORDER=0></A>
+</NOSCRIPT> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column360x100&location=5.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column360x100&location=5.html
new file mode 100755
index 0000000000..e6a0479387
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column360x100&location=5.html
@@ -0,0 +1,43 @@
+<!-- Sniffer Code for Flash version=80 -->
+<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
+<SCRIPT LANGUAGE=JavaScript>
+<!--
+var swf_click = "httpdisabled://g.163.com/c?AID=33914&FlightID=749&Values=1855147922&Redirect=http://a1419.oadz.com/link/C/1419/33/T3dnBK9qi69zlngdBjwKaRYj-ms_/a/2/http://www.topshoes.cn/";
+var dcswf_click = escape(swf_click);
+var ShockMode = 0;
+var plugin = (navigator.mimeTypes && navigator.mimeTypes["application/x-shockwave-flash"]) ? navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin : 0;
+
+if (plugin && parseInt(plugin.description.substring(plugin.description.indexOf(".")-2)) >= 8)
+{
+ShockMode = 1;
+}
+else if (navigator.userAgent && navigator.userAgent.indexOf("MSIE")>=0
+&& navigator.userAgent.indexOf("Opera")<0) {
+void('<SCRIPT LANGUAGE=VBScript\> \n');
+void('on error resume next \n');
+void('ShockMode = (IsObject(CreateObject("ShockwaveFlash.ShockwaveFlash.8")))\n');
+void('<\/SCRIPT\> \n');
+}
+if ( ShockMode ) {
+void('<objectdisabled classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"');
+void(' codebase="httpdisabled://downloaddisabled.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0"');
+void(' ID=flashad WIDTH=360 HEIGHT=100>');
+void(' <PARAM NAME=movie VALUE="httpdisabled://img1.126.net/channel5/008995/bl360100_110402.swf?clickTag='+dcswf_click+'"> ');
+void(' <PARAM NAME=quality VALUE=autohigh> ');
+void(' <PARAM NAME=wmode VALUE=opaque> ');
+void(' <embeddisabled SRC="httpdisabled://img1.126.net/channel5/008995/bl360100_110402.swf?clickTag='+dcswf_click+'" QUALITY=autohigh wmode=opaque');
+void(' NAME=flashad swLiveConnect=TRUE WIDTH=360 HEIGHT=100');
+void(' TYPE="application/x-shockwave-flash" PLUGINSPAGE="httpdisabled://www.macromedia.com/shockwave/downloaddisabled/index.cgi?P1_Prod_Version=ShockwaveFlash">');
+void('</EMBED>');
+void('</OBJECT>');
+} else if (!(navigator.appName && navigator.appName.indexOf("Netscape")>=0 && navigator.appVersion.indexOf("2.")>=0)){
+void('<A HREF="httpdisabled://g.163.com/c?AID=33914&FlightID=749&Values=1855147922&Redirect=http://a1419.oadz.com/link/C/1419/33/T3dnBK9qi69zlngdBjwKaRYj-ms_/a/2/http://www.topshoes.cn/" target="_blank"><IMG SRC="httpdisabled://img1.126.net/channel5/008995/bl360100_110402.swf" WIDTH=360 HEIGHT=100 BORDER=0></A>');
+}
+//-->
+</SCRIPT>
+<NOEMBED>
+<A HREF="httpdisabled://g.163.com/c?AID=33914&FlightID=749&Values=1855147922&Redirect=http://a1419.oadz.com/link/C/1419/33/T3dnBK9qi69zlngdBjwKaRYj-ms_/a/2/http://www.topshoes.cn/" target="_blank"><IMG SRC="../img1.126.net/channel5/008995/bl360100_110402.swf" WIDTH=360 HEIGHT=100 BORDER=0></A>
+</NOEMBED>
+<NOSCRIPT>
+<A HREF="httpdisabled://g.163.com/c?AID=33914&FlightID=749&Values=1855147922&Redirect=http://a1419.oadz.com/link/C/1419/33/T3dnBK9qi69zlngdBjwKaRYj-ms_/a/2/http://www.topshoes.cn/" target="_blank"><IMG SRC="../img1.126.net/channel5/008995/bl360100_110402.swf" WIDTH=360 HEIGHT=100 BORDER=0></A>
+</NOSCRIPT> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column360x100&location=6.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column360x100&location=6.html
new file mode 100755
index 0000000000..e1723fc0f7
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column360x100&location=6.html
@@ -0,0 +1,43 @@
+<!-- Sniffer Code for Flash version=80 -->
+<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
+<SCRIPT LANGUAGE=JavaScript>
+<!--
+var swf_click = "httpdisabled://g.163.com/c?AID=33816&FlightID=766&Values=68259114&Redirect=http://click.moonbasa.com/transfer.aspx?cn=1699&type=0&adsiteid=10000007&url=http://www.moonbasa.com/?oid=4";
+var dcswf_click = escape(swf_click);
+var ShockMode = 0;
+var plugin = (navigator.mimeTypes && navigator.mimeTypes["application/x-shockwave-flash"]) ? navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin : 0;
+
+if (plugin && parseInt(plugin.description.substring(plugin.description.indexOf(".")-2)) >= 8)
+{
+ShockMode = 1;
+}
+else if (navigator.userAgent && navigator.userAgent.indexOf("MSIE")>=0
+&& navigator.userAgent.indexOf("Opera")<0) {
+void('<SCRIPT LANGUAGE=VBScript\> \n');
+void('on error resume next \n');
+void('ShockMode = (IsObject(CreateObject("ShockwaveFlash.ShockwaveFlash.8")))\n');
+void('<\/SCRIPT\> \n');
+}
+if ( ShockMode ) {
+void('<objectdisabled classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"');
+void(' codebase="httpdisabled://downloaddisabled.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0"');
+void(' ID=flashad WIDTH=360 HEIGHT=100>');
+void(' <PARAM NAME=movie VALUE="httpdisabled://img1.126.net/channel5/008984/liangxie360100_110402.swf?clickTag='+dcswf_click+'"> ');
+void(' <PARAM NAME=quality VALUE=autohigh> ');
+void(' <PARAM NAME=wmode VALUE=opaque> ');
+void(' <embeddisabled SRC="httpdisabled://img1.126.net/channel5/008984/liangxie360100_110402.swf?clickTag='+dcswf_click+'" QUALITY=autohigh wmode=opaque');
+void(' NAME=flashad swLiveConnect=TRUE WIDTH=360 HEIGHT=100');
+void(' TYPE="application/x-shockwave-flash" PLUGINSPAGE="httpdisabled://www.macromedia.com/shockwave/downloaddisabled/index.cgi?P1_Prod_Version=ShockwaveFlash">');
+void('</EMBED>');
+void('</OBJECT>');
+} else if (!(navigator.appName && navigator.appName.indexOf("Netscape")>=0 && navigator.appVersion.indexOf("2.")>=0)){
+void('<A HREF="httpdisabled://g.163.com/c?AID=33816&FlightID=766&Values=68259114&Redirect=http://click.moonbasa.com/transfer.aspx?cn=1699&type=0&adsiteid=10000007&url=http://www.moonbasa.com/?oid=4" target="_blank"><IMG SRC="httpdisabled://img1.126.net/channel5/008984/liangxie360100_110402.swf" WIDTH=360 HEIGHT=100 BORDER=0></A>');
+}
+//-->
+</SCRIPT>
+<NOEMBED>
+<A HREF="httpdisabled://g.163.com/c?AID=33816&FlightID=766&Values=68259114&Redirect=http://click.moonbasa.com/transfer.aspx?cn=1699&type=0&adsiteid=10000007&url=http://www.moonbasa.com/?oid=4" target="_blank"><IMG SRC="../img1.126.net/channel5/008984/liangxie360100_110402.swf" WIDTH=360 HEIGHT=100 BORDER=0></A>
+</NOEMBED>
+<NOSCRIPT>
+<A HREF="httpdisabled://g.163.com/c?AID=33816&FlightID=766&Values=68259114&Redirect=http://click.moonbasa.com/transfer.aspx?cn=1699&type=0&adsiteid=10000007&url=http://www.moonbasa.com/?oid=4" target="_blank"><IMG SRC="../img1.126.net/channel5/008984/liangxie360100_110402.swf" WIDTH=360 HEIGHT=100 BORDER=0></A>
+</NOSCRIPT> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=1.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=1.html
new file mode 100755
index 0000000000..adb1ca8dc4
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=1.html
@@ -0,0 +1 @@
+<a target="_blank" href="httpdisabled://g.163.com/c?AID=34342&FlightID=761&Values=2612011559&Redirect=http://allyes.nie.163.com/main/adfclick?db=afanie&bid=38403,18618,355&cid=637,4,1&sid=37953&show=ignore&url=http://qn.163.com/yr/"><img src="../img2.126.net/xoimages/game/20110216/ql/x/390x100.jpg" border=0 height=100 width=390 alt=""></a> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=2.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=2.html
new file mode 100755
index 0000000000..a49ab10789
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=2.html
@@ -0,0 +1 @@
+<a target="_blank" href="httpdisabled://g.163.com/c?AID=34354&FlightID=744&Values=85863423&Redirect=http://travel.163.com/hellocity/"><img src="../img2.126.net/xoimages/sales/2011/03/ly/390x100.jpg" border=0 height=100 width=390 alt=""></a> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=3.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=3.html
new file mode 100755
index 0000000000..29e09983c2
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=3.html
@@ -0,0 +1,43 @@
+<!-- Sniffer Code for Flash version=80 -->
+<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
+<SCRIPT LANGUAGE=JavaScript>
+<!--
+var swf_click = "httpdisabled://g.163.com/c?AID=33814&FlightID=763&Values=3968994465&Redirect=http://click.moonbasa.com/transfer.aspx?cn=1698&type=0&adsiteid=10000007&url=http://www.moonbasa.com/?oid=3";
+var dcswf_click = escape(swf_click);
+var ShockMode = 0;
+var plugin = (navigator.mimeTypes && navigator.mimeTypes["application/x-shockwave-flash"]) ? navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin : 0;
+
+if (plugin && parseInt(plugin.description.substring(plugin.description.indexOf(".")-2)) >= 8)
+{
+ShockMode = 1;
+}
+else if (navigator.userAgent && navigator.userAgent.indexOf("MSIE")>=0
+&& navigator.userAgent.indexOf("Opera")<0) {
+void('<SCRIPT LANGUAGE=VBScript\> \n');
+void('on error resume next \n');
+void('ShockMode = (IsObject(CreateObject("ShockwaveFlash.ShockwaveFlash.8")))\n');
+void('<\/SCRIPT\> \n');
+}
+if ( ShockMode ) {
+void('<objectdisabled classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"');
+void(' codebase="httpdisabled://downloaddisabled.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0"');
+void(' ID=flashad WIDTH=390 HEIGHT=100>');
+void(' <PARAM NAME=movie VALUE="httpdisabled://img1.126.net/channel5/008984/xizhuang390100_110402.swf?clickTag='+dcswf_click+'"> ');
+void(' <PARAM NAME=quality VALUE=autohigh> ');
+void(' <PARAM NAME=wmode VALUE=opaque> ');
+void(' <embeddisabled SRC="httpdisabled://img1.126.net/channel5/008984/xizhuang390100_110402.swf?clickTag='+dcswf_click+'" QUALITY=autohigh wmode=opaque');
+void(' NAME=flashad swLiveConnect=TRUE WIDTH=390 HEIGHT=100');
+void(' TYPE="application/x-shockwave-flash" PLUGINSPAGE="httpdisabled://www.macromedia.com/shockwave/downloaddisabled/index.cgi?P1_Prod_Version=ShockwaveFlash">');
+void('</EMBED>');
+void('</OBJECT>');
+} else if (!(navigator.appName && navigator.appName.indexOf("Netscape")>=0 && navigator.appVersion.indexOf("2.")>=0)){
+void('<A HREF="httpdisabled://g.163.com/c?AID=33814&FlightID=763&Values=3968994465&Redirect=http://click.moonbasa.com/transfer.aspx?cn=1698&type=0&adsiteid=10000007&url=http://www.moonbasa.com/?oid=3" target="_blank"><IMG SRC="httpdisabled://img1.126.net/channel5/008984/xizhuang390100_110402.swf" WIDTH=390 HEIGHT=100 BORDER=0></A>');
+}
+//-->
+</SCRIPT>
+<NOEMBED>
+<A HREF="httpdisabled://g.163.com/c?AID=33814&FlightID=763&Values=3968994465&Redirect=http://click.moonbasa.com/transfer.aspx?cn=1698&type=0&adsiteid=10000007&url=http://www.moonbasa.com/?oid=3" target="_blank"><IMG SRC="../img1.126.net/channel5/008984/xizhuang390100_110402.swf" WIDTH=390 HEIGHT=100 BORDER=0></A>
+</NOEMBED>
+<NOSCRIPT>
+<A HREF="httpdisabled://g.163.com/c?AID=33814&FlightID=763&Values=3968994465&Redirect=http://click.moonbasa.com/transfer.aspx?cn=1698&type=0&adsiteid=10000007&url=http://www.moonbasa.com/?oid=3" target="_blank"><IMG SRC="../img1.126.net/channel5/008984/xizhuang390100_110402.swf" WIDTH=390 HEIGHT=100 BORDER=0></A>
+</NOSCRIPT> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=4.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=4.html
new file mode 100755
index 0000000000..898bf062ce
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=4.html
@@ -0,0 +1,43 @@
+<!-- Sniffer Code for Flash version=80 -->
+<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
+<SCRIPT LANGUAGE=JavaScript>
+<!--
+var swf_click = "httpdisabled://g.163.com/c?AID=34196&FlightID=745&Values=3301780629&Redirect=http://a1052.oadz.com/link/C/1052/39/TNCPMVEIYp-TRrZLJ27yq2NMhXs_/p032/6/http://www.olomo.com/position/?u=1894274&ompz=4454";
+var dcswf_click = escape(swf_click);
+var ShockMode = 0;
+var plugin = (navigator.mimeTypes && navigator.mimeTypes["application/x-shockwave-flash"]) ? navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin : 0;
+
+if (plugin && parseInt(plugin.description.substring(plugin.description.indexOf(".")-2)) >= 8)
+{
+ShockMode = 1;
+}
+else if (navigator.userAgent && navigator.userAgent.indexOf("MSIE")>=0
+&& navigator.userAgent.indexOf("Opera")<0) {
+void('<SCRIPT LANGUAGE=VBScript\> \n');
+void('on error resume next \n');
+void('ShockMode = (IsObject(CreateObject("ShockwaveFlash.ShockwaveFlash.8")))\n');
+void('<\/SCRIPT\> \n');
+}
+if ( ShockMode ) {
+void('<objectdisabled classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"');
+void(' codebase="httpdisabled://downloaddisabled.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0"');
+void(' ID=flashad WIDTH=390 HEIGHT=100>');
+void(' <PARAM NAME=movie VALUE="httpdisabled://img1.126.net/channel5/008995/ou390100_110408.swf?clickTag='+dcswf_click+'"> ');
+void(' <PARAM NAME=quality VALUE=autohigh> ');
+void(' <PARAM NAME=wmode VALUE=opaque> ');
+void(' <embeddisabled SRC="httpdisabled://img1.126.net/channel5/008995/ou390100_110408.swf?clickTag='+dcswf_click+'" QUALITY=autohigh wmode=opaque');
+void(' NAME=flashad swLiveConnect=TRUE WIDTH=390 HEIGHT=100');
+void(' TYPE="application/x-shockwave-flash" PLUGINSPAGE="httpdisabled://www.macromedia.com/shockwave/downloaddisabled/index.cgi?P1_Prod_Version=ShockwaveFlash">');
+void('</EMBED>');
+void('</OBJECT>');
+} else if (!(navigator.appName && navigator.appName.indexOf("Netscape")>=0 && navigator.appVersion.indexOf("2.")>=0)){
+void('<A HREF="httpdisabled://g.163.com/c?AID=34196&FlightID=745&Values=3301780629&Redirect=http://a1052.oadz.com/link/C/1052/39/TNCPMVEIYp-TRrZLJ27yq2NMhXs_/p032/6/http://www.olomo.com/position/?u=1894274&ompz=4454" target="_blank"><IMG SRC="httpdisabled://img1.126.net/channel5/008995/ou390100_110408.swf" WIDTH=390 HEIGHT=100 BORDER=0></A>');
+}
+//-->
+</SCRIPT>
+<NOEMBED>
+<A HREF="httpdisabled://g.163.com/c?AID=34196&FlightID=745&Values=3301780629&Redirect=http://a1052.oadz.com/link/C/1052/39/TNCPMVEIYp-TRrZLJ27yq2NMhXs_/p032/6/http://www.olomo.com/position/?u=1894274&ompz=4454" target="_blank"><IMG SRC="../img1.126.net/channel5/008995/ou390100_110408.swf" WIDTH=390 HEIGHT=100 BORDER=0></A>
+</NOEMBED>
+<NOSCRIPT>
+<A HREF="httpdisabled://g.163.com/c?AID=34196&FlightID=745&Values=3301780629&Redirect=http://a1052.oadz.com/link/C/1052/39/TNCPMVEIYp-TRrZLJ27yq2NMhXs_/p032/6/http://www.olomo.com/position/?u=1894274&ompz=4454" target="_blank"><IMG SRC="../img1.126.net/channel5/008995/ou390100_110408.swf" WIDTH=390 HEIGHT=100 BORDER=0></A>
+</NOSCRIPT> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=5.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=5.html
new file mode 100755
index 0000000000..5edb8dfa42
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=5.html
@@ -0,0 +1,43 @@
+<!-- Sniffer Code for Flash version=80 -->
+<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
+<SCRIPT LANGUAGE=JavaScript>
+<!--
+var swf_click = "httpdisabled://g.163.com/c?AID=33871&FlightID=746&Values=478684510&Redirect=http://mso.allyes.com/main/c?db=mso&bid=63394,31657,2624&cid=62875,2992,149&sid=63319&show=ignore&url=http://www.vancl.com/?source=wy230703syzt5";
+var dcswf_click = escape(swf_click);
+var ShockMode = 0;
+var plugin = (navigator.mimeTypes && navigator.mimeTypes["application/x-shockwave-flash"]) ? navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin : 0;
+
+if (plugin && parseInt(plugin.description.substring(plugin.description.indexOf(".")-2)) >= 8)
+{
+ShockMode = 1;
+}
+else if (navigator.userAgent && navigator.userAgent.indexOf("MSIE")>=0
+&& navigator.userAgent.indexOf("Opera")<0) {
+void('<SCRIPT LANGUAGE=VBScript\> \n');
+void('on error resume next \n');
+void('ShockMode = (IsObject(CreateObject("ShockwaveFlash.ShockwaveFlash.8")))\n');
+void('<\/SCRIPT\> \n');
+}
+if ( ShockMode ) {
+void('<objectdisabled classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"');
+void(' codebase="httpdisabled://downloaddisabled.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0"');
+void(' ID=flashad WIDTH=390 HEIGHT=100>');
+void(' <PARAM NAME=movie VALUE="httpdisabled://img1.126.net/channel5/008977/5v390100_110406.swf?clickTag='+dcswf_click+'"> ');
+void(' <PARAM NAME=quality VALUE=autohigh> ');
+void(' <PARAM NAME=wmode VALUE=opaque> ');
+void(' <embeddisabled SRC="httpdisabled://img1.126.net/channel5/008977/5v390100_110406.swf?clickTag='+dcswf_click+'" QUALITY=autohigh wmode=opaque');
+void(' NAME=flashad swLiveConnect=TRUE WIDTH=390 HEIGHT=100');
+void(' TYPE="application/x-shockwave-flash" PLUGINSPAGE="httpdisabled://www.macromedia.com/shockwave/downloaddisabled/index.cgi?P1_Prod_Version=ShockwaveFlash">');
+void('</EMBED>');
+void('</OBJECT>');
+} else if (!(navigator.appName && navigator.appName.indexOf("Netscape")>=0 && navigator.appVersion.indexOf("2.")>=0)){
+void('<A HREF="httpdisabled://g.163.com/c?AID=33871&FlightID=746&Values=478684510&Redirect=http://mso.allyes.com/main/c?db=mso&bid=63394,31657,2624&cid=62875,2992,149&sid=63319&show=ignore&url=http://www.vancl.com/?source=wy230703syzt5" target="_blank"><IMG SRC="httpdisabled://img1.126.net/channel5/008977/5v390100_110406.swf" WIDTH=390 HEIGHT=100 BORDER=0></A>');
+}
+//-->
+</SCRIPT>
+<NOEMBED>
+<A HREF="httpdisabled://g.163.com/c?AID=33871&FlightID=746&Values=478684510&Redirect=http://mso.allyes.com/main/c?db=mso&bid=63394,31657,2624&cid=62875,2992,149&sid=63319&show=ignore&url=http://www.vancl.com/?source=wy230703syzt5" target="_blank"><IMG SRC="../img1.126.net/channel5/008977/5v390100_110406.swf" WIDTH=390 HEIGHT=100 BORDER=0></A>
+</NOEMBED>
+<NOSCRIPT>
+<A HREF="httpdisabled://g.163.com/c?AID=33871&FlightID=746&Values=478684510&Redirect=http://mso.allyes.com/main/c?db=mso&bid=63394,31657,2624&cid=62875,2992,149&sid=63319&show=ignore&url=http://www.vancl.com/?source=wy230703syzt5" target="_blank"><IMG SRC="../img1.126.net/channel5/008977/5v390100_110406.swf" WIDTH=390 HEIGHT=100 BORDER=0></A>
+</NOSCRIPT> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=6.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=6.html
new file mode 100755
index 0000000000..44952eaec0
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column390x100&location=6.html
@@ -0,0 +1,43 @@
+<!-- Sniffer Code for Flash version=80 -->
+<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
+<SCRIPT LANGUAGE=JavaScript>
+<!--
+var swf_click = "httpdisabled://g.163.com/c?AID=33888&FlightID=765&Values=2426328330&Redirect=http://a777.oadz.com/link/C/777/2972/Z6nbl6URgyAsEQVVzZ5skDUHOQ0_/a/126/http://www.fuocoo.com";
+var dcswf_click = escape(swf_click);
+var ShockMode = 0;
+var plugin = (navigator.mimeTypes && navigator.mimeTypes["application/x-shockwave-flash"]) ? navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin : 0;
+
+if (plugin && parseInt(plugin.description.substring(plugin.description.indexOf(".")-2)) >= 8)
+{
+ShockMode = 1;
+}
+else if (navigator.userAgent && navigator.userAgent.indexOf("MSIE")>=0
+&& navigator.userAgent.indexOf("Opera")<0) {
+void('<SCRIPT LANGUAGE=VBScript\> \n');
+void('on error resume next \n');
+void('ShockMode = (IsObject(CreateObject("ShockwaveFlash.ShockwaveFlash.8")))\n');
+void('<\/SCRIPT\> \n');
+}
+if ( ShockMode ) {
+void('<objectdisabled classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"');
+void(' codebase="httpdisabled://downloaddisabled.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0"');
+void(' ID=flashad WIDTH=390 HEIGHT=100>');
+void(' <PARAM NAME=movie VALUE="httpdisabled://img1.126.net/channel5/008995/fk390100_110331.swf?clickTag='+dcswf_click+'"> ');
+void(' <PARAM NAME=quality VALUE=autohigh> ');
+void(' <PARAM NAME=wmode VALUE=opaque> ');
+void(' <embeddisabled SRC="httpdisabled://img1.126.net/channel5/008995/fk390100_110331.swf?clickTag='+dcswf_click+'" QUALITY=autohigh wmode=opaque');
+void(' NAME=flashad swLiveConnect=TRUE WIDTH=390 HEIGHT=100');
+void(' TYPE="application/x-shockwave-flash" PLUGINSPAGE="httpdisabled://www.macromedia.com/shockwave/downloaddisabled/index.cgi?P1_Prod_Version=ShockwaveFlash">');
+void('</EMBED>');
+void('</OBJECT>');
+} else if (!(navigator.appName && navigator.appName.indexOf("Netscape")>=0 && navigator.appVersion.indexOf("2.")>=0)){
+void('<A HREF="httpdisabled://g.163.com/c?AID=33888&FlightID=765&Values=2426328330&Redirect=http://a777.oadz.com/link/C/777/2972/Z6nbl6URgyAsEQVVzZ5skDUHOQ0_/a/126/http://www.fuocoo.com" target="_blank"><IMG SRC="httpdisabled://img1.126.net/channel5/008995/fk390100_110331.swf" WIDTH=390 HEIGHT=100 BORDER=0></A>');
+}
+//-->
+</SCRIPT>
+<NOEMBED>
+<A HREF="httpdisabled://g.163.com/c?AID=33888&FlightID=765&Values=2426328330&Redirect=http://a777.oadz.com/link/C/777/2972/Z6nbl6URgyAsEQVVzZ5skDUHOQ0_/a/126/http://www.fuocoo.com" target="_blank"><IMG SRC="../img1.126.net/channel5/008995/fk390100_110331.swf" WIDTH=390 HEIGHT=100 BORDER=0></A>
+</NOEMBED>
+<NOSCRIPT>
+<A HREF="httpdisabled://g.163.com/c?AID=33888&FlightID=765&Values=2426328330&Redirect=http://a777.oadz.com/link/C/777/2972/Z6nbl6URgyAsEQVVzZ5skDUHOQ0_/a/126/http://www.fuocoo.com" target="_blank"><IMG SRC="../img1.126.net/channel5/008995/fk390100_110331.swf" WIDTH=390 HEIGHT=100 BORDER=0></A>
+</NOSCRIPT> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column600x80&location=1.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column600x80&location=1.html
new file mode 100755
index 0000000000..b0e6dcf26e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=column600x80&location=1.html
@@ -0,0 +1 @@
+<a target="_blank" href="httpdisabled://g.163.com/c?AID=33904&FlightID=21&Values=1257549911&Redirect=http://allyes.nie.163.com/main/adfclick?db=afanie&bid=38403,18618,355&cid=637,4,1&sid=37953&show=ignore&url=http://qn.163.com/yr/"><img src="../img2.126.net/xoimages/game/20110216/ql/x/600x80.gif" border=0 height=80 width=600 alt=""></a> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x100&location=1.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x100&location=1.html
new file mode 100755
index 0000000000..b99570776e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x100&location=1.html
@@ -0,0 +1 @@
+<a target="_blank" href="httpdisabled://g.163.com/c?AID=34413&FlightID=750&Values=4252749124&Redirect=http://media.163.com/11/0407/12/711MA88S00762H91.html"><img src="../img2.126.net/xoimages/sales/2011/04/hy/190x100.jpg" border=0 height=100 width=190 alt=""></a> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x100&location=2.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x100&location=2.html
new file mode 100755
index 0000000000..e6f26ed548
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x100&location=2.html
@@ -0,0 +1,43 @@
+<!-- Sniffer Code for Flash version=80 -->
+<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
+<SCRIPT LANGUAGE=JavaScript>
+<!--
+var swf_click = "httpdisabled://g.163.com/c?AID=33896&FlightID=751&Values=2829007648&Redirect=http://www.k121.com/?from=163w2";
+var dcswf_click = escape(swf_click);
+var ShockMode = 0;
+var plugin = (navigator.mimeTypes && navigator.mimeTypes["application/x-shockwave-flash"]) ? navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin : 0;
+
+if (plugin && parseInt(plugin.description.substring(plugin.description.indexOf(".")-2)) >= 8)
+{
+ShockMode = 1;
+}
+else if (navigator.userAgent && navigator.userAgent.indexOf("MSIE")>=0
+&& navigator.userAgent.indexOf("Opera")<0) {
+void('<SCRIPT LANGUAGE=VBScript\> \n');
+void('on error resume next \n');
+void('ShockMode = (IsObject(CreateObject("ShockwaveFlash.ShockwaveFlash.8")))\n');
+void('<\/SCRIPT\> \n');
+}
+if ( ShockMode ) {
+void('<objectdisabled classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"');
+void(' codebase="httpdisabled://downloaddisabled.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0"');
+void(' ID=flashad WIDTH=190 HEIGHT=100>');
+void(' <PARAM NAME=movie VALUE="httpdisabled://img1.126.net/channel5/008981/kyd2_190100_110402.swf?clickTag='+dcswf_click+'"> ');
+void(' <PARAM NAME=quality VALUE=autohigh> ');
+void(' <PARAM NAME=wmode VALUE=opaque> ');
+void(' <embeddisabled SRC="httpdisabled://img1.126.net/channel5/008981/kyd2_190100_110402.swf?clickTag='+dcswf_click+'" QUALITY=autohigh wmode=opaque');
+void(' NAME=flashad swLiveConnect=TRUE WIDTH=190 HEIGHT=100');
+void(' TYPE="application/x-shockwave-flash" PLUGINSPAGE="httpdisabled://www.macromedia.com/shockwave/downloaddisabled/index.cgi?P1_Prod_Version=ShockwaveFlash">');
+void('</EMBED>');
+void('</OBJECT>');
+} else if (!(navigator.appName && navigator.appName.indexOf("Netscape")>=0 && navigator.appVersion.indexOf("2.")>=0)){
+void('<A HREF="httpdisabled://g.163.com/c?AID=33896&FlightID=751&Values=2829007648&Redirect=http://www.k121.com/?from=163w2" target="_blank"><IMG SRC="httpdisabled://img1.126.net/channel5/008981/kyd2_190100_110402.swf" WIDTH=190 HEIGHT=100 BORDER=0></A>');
+}
+//-->
+</SCRIPT>
+<NOEMBED>
+<A HREF="httpdisabled://g.163.com/c?AID=33896&FlightID=751&Values=2829007648&Redirect=http://www.k121.com/?from=163w2" target="_blank"><IMG SRC="../img1.126.net/channel5/008981/kyd2_190100_110402.swf" WIDTH=190 HEIGHT=100 BORDER=0></A>
+</NOEMBED>
+<NOSCRIPT>
+<A HREF="httpdisabled://g.163.com/c?AID=33896&FlightID=751&Values=2829007648&Redirect=http://www.k121.com/?from=163w2" target="_blank"><IMG SRC="../img1.126.net/channel5/008981/kyd2_190100_110402.swf" WIDTH=190 HEIGHT=100 BORDER=0></A>
+</NOSCRIPT> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x180&location=1.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x180&location=1.html
new file mode 100755
index 0000000000..fccb481ca1
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x180&location=1.html
@@ -0,0 +1,43 @@
+<!-- Sniffer Code for Flash version=80 -->
+<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
+<SCRIPT LANGUAGE=JavaScript>
+<!--
+var swf_click = "httpdisabled://g.163.com/c?AID=33829&FlightID=752&Values=2077038183&Redirect=http://nimafa7.allyes.com/main/c?db=nimafa7&bid=17117,7729,378&cid=6808,162,1&sid=17997&show=ignore&url=http://minisite.163.com/2011/0214/tries/";
+var dcswf_click = escape(swf_click);
+var ShockMode = 0;
+var plugin = (navigator.mimeTypes && navigator.mimeTypes["application/x-shockwave-flash"]) ? navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin : 0;
+
+if (plugin && parseInt(plugin.description.substring(plugin.description.indexOf(".")-2)) >= 8)
+{
+ShockMode = 1;
+}
+else if (navigator.userAgent && navigator.userAgent.indexOf("MSIE")>=0
+&& navigator.userAgent.indexOf("Opera")<0) {
+void('<SCRIPT LANGUAGE=VBScript\> \n');
+void('on error resume next \n');
+void('ShockMode = (IsObject(CreateObject("ShockwaveFlash.ShockwaveFlash.8")))\n');
+void('<\/SCRIPT\> \n');
+}
+if ( ShockMode ) {
+void('<objectdisabled classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"');
+void(' codebase="httpdisabled://downloaddisabled.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0"');
+void(' ID=flashad WIDTH=190 HEIGHT=180>');
+void(' <PARAM NAME=movie VALUE="httpdisabled://img1.126.net/channel5/009400/caizi190180_110408.swf?clickTag='+dcswf_click+'"> ');
+void(' <PARAM NAME=quality VALUE=autohigh> ');
+void(' <PARAM NAME=wmode VALUE=opaque> ');
+void(' <embeddisabled SRC="httpdisabled://img1.126.net/channel5/009400/caizi190180_110408.swf?clickTag='+dcswf_click+'" QUALITY=autohigh wmode=opaque');
+void(' NAME=flashad swLiveConnect=TRUE WIDTH=190 HEIGHT=180');
+void(' TYPE="application/x-shockwave-flash" PLUGINSPAGE="httpdisabled://www.macromedia.com/shockwave/downloaddisabled/index.cgi?P1_Prod_Version=ShockwaveFlash">');
+void('</EMBED>');
+void('</OBJECT>');
+} else if (!(navigator.appName && navigator.appName.indexOf("Netscape")>=0 && navigator.appVersion.indexOf("2.")>=0)){
+void('<A HREF="httpdisabled://g.163.com/c?AID=33829&FlightID=752&Values=2077038183&Redirect=http://nimafa7.allyes.com/main/c?db=nimafa7&bid=17117,7729,378&cid=6808,162,1&sid=17997&show=ignore&url=http://minisite.163.com/2011/0214/tries/" target="_blank"><IMG SRC="httpdisabled://img1.126.net/channel5/009400/caizi190180_110408.swf" WIDTH=190 HEIGHT=180 BORDER=0></A>');
+}
+//-->
+</SCRIPT>
+<NOEMBED>
+<A HREF="httpdisabled://g.163.com/c?AID=33829&FlightID=752&Values=2077038183&Redirect=http://nimafa7.allyes.com/main/c?db=nimafa7&bid=17117,7729,378&cid=6808,162,1&sid=17997&show=ignore&url=http://minisite.163.com/2011/0214/tries/" target="_blank"><IMG SRC="../img1.126.net/channel5/009400/caizi190180_110408.swf" WIDTH=190 HEIGHT=180 BORDER=0></A>
+</NOEMBED>
+<NOSCRIPT>
+<A HREF="httpdisabled://g.163.com/c?AID=33829&FlightID=752&Values=2077038183&Redirect=http://nimafa7.allyes.com/main/c?db=nimafa7&bid=17117,7729,378&cid=6808,162,1&sid=17997&show=ignore&url=http://minisite.163.com/2011/0214/tries/" target="_blank"><IMG SRC="../img1.126.net/channel5/009400/caizi190180_110408.swf" WIDTH=190 HEIGHT=180 BORDER=0></A>
+</NOSCRIPT> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x180&location=2.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x180&location=2.html
new file mode 100755
index 0000000000..69fcd38669
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x180&location=2.html
@@ -0,0 +1 @@
+<a target="_blank" href="httpdisabled://g.163.com/c?AID=34156&FlightID=753&Values=2278629106&Redirect=http://cidian.youdao.com/pingtian/"><img src="../img2.126.net/xoimages/sales/2011/04/yd/190x180.jpg" border=0 height=180 width=190 alt=""></a> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x180&location=3.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x180&location=3.html
new file mode 100755
index 0000000000..308fbd44a9
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x180&location=3.html
@@ -0,0 +1,43 @@
+<!-- Sniffer Code for Flash version=80 -->
+<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
+<SCRIPT LANGUAGE=JavaScript>
+<!--
+var swf_click = "httpdisabled://g.163.com/c?AID=33920&FlightID=754&Values=743829921&Redirect=http://www.voidg.com/voidg2/lp/voidg/voidg.php?CID=CN_DIS_121_3_1_700";
+var dcswf_click = escape(swf_click);
+var ShockMode = 0;
+var plugin = (navigator.mimeTypes && navigator.mimeTypes["application/x-shockwave-flash"]) ? navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin : 0;
+
+if (plugin && parseInt(plugin.description.substring(plugin.description.indexOf(".")-2)) >= 8)
+{
+ShockMode = 1;
+}
+else if (navigator.userAgent && navigator.userAgent.indexOf("MSIE")>=0
+&& navigator.userAgent.indexOf("Opera")<0) {
+void('<SCRIPT LANGUAGE=VBScript\> \n');
+void('on error resume next \n');
+void('ShockMode = (IsObject(CreateObject("ShockwaveFlash.ShockwaveFlash.8")))\n');
+void('<\/SCRIPT\> \n');
+}
+if ( ShockMode ) {
+void('<objectdisabled classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"');
+void(' codebase="httpdisabled://downloaddisabled.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0"');
+void(' ID=flashad WIDTH=190 HEIGHT=180>');
+void(' <PARAM NAME=movie VALUE="httpdisabled://img1.126.net/channel5/008995/voidg190180_110407.swf?clickTag='+dcswf_click+'"> ');
+void(' <PARAM NAME=quality VALUE=autohigh> ');
+void(' <PARAM NAME=wmode VALUE=opaque> ');
+void(' <embeddisabled SRC="httpdisabled://img1.126.net/channel5/008995/voidg190180_110407.swf?clickTag='+dcswf_click+'" QUALITY=autohigh wmode=opaque');
+void(' NAME=flashad swLiveConnect=TRUE WIDTH=190 HEIGHT=180');
+void(' TYPE="application/x-shockwave-flash" PLUGINSPAGE="httpdisabled://www.macromedia.com/shockwave/downloaddisabled/index.cgi?P1_Prod_Version=ShockwaveFlash">');
+void('</EMBED>');
+void('</OBJECT>');
+} else if (!(navigator.appName && navigator.appName.indexOf("Netscape")>=0 && navigator.appVersion.indexOf("2.")>=0)){
+void('<A HREF="httpdisabled://g.163.com/c?AID=33920&FlightID=754&Values=743829921&Redirect=http://www.voidg.com/voidg2/lp/voidg/voidg.php?CID=CN_DIS_121_3_1_700" target="_blank"><IMG SRC="httpdisabled://img1.126.net/channel5/008995/voidg190180_110407.swf" WIDTH=190 HEIGHT=180 BORDER=0></A>');
+}
+//-->
+</SCRIPT>
+<NOEMBED>
+<A HREF="httpdisabled://g.163.com/c?AID=33920&FlightID=754&Values=743829921&Redirect=http://www.voidg.com/voidg2/lp/voidg/voidg.php?CID=CN_DIS_121_3_1_700" target="_blank"><IMG SRC="../img1.126.net/channel5/008995/voidg190180_110407.swf" WIDTH=190 HEIGHT=180 BORDER=0></A>
+</NOEMBED>
+<NOSCRIPT>
+<A HREF="httpdisabled://g.163.com/c?AID=33920&FlightID=754&Values=743829921&Redirect=http://www.voidg.com/voidg2/lp/voidg/voidg.php?CID=CN_DIS_121_3_1_700" target="_blank"><IMG SRC="../img1.126.net/channel5/008995/voidg190180_110407.swf" WIDTH=190 HEIGHT=180 BORDER=0></A>
+</NOSCRIPT> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x180&location=4.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x180&location=4.html
new file mode 100755
index 0000000000..0dfc0433b1
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x180&location=4.html
@@ -0,0 +1 @@
+<script type='text/javascript' src='../zjs.ipinyou.com/2011032517331513260_2342_190180.js'></script> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x300&location=1.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x300&location=1.html
new file mode 100755
index 0000000000..7dfda08024
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x300&location=1.html
@@ -0,0 +1,43 @@
+<!-- Sniffer Code for Flash version=80 -->
+<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
+<SCRIPT LANGUAGE=JavaScript>
+<!--
+var swf_click = "httpdisabled://g.163.com/c?AID=33876&FlightID=756&Values=3536047884&Redirect=http://a814.oadz.com/link/C/814/88637/UoFr9DZA7GBQz2OagEiruxYJZCc_/a/108/http://www.m18.com/market/front.aspx?pno=wan-hp-s1&url=/im";
+var dcswf_click = escape(swf_click);
+var ShockMode = 0;
+var plugin = (navigator.mimeTypes && navigator.mimeTypes["application/x-shockwave-flash"]) ? navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin : 0;
+
+if (plugin && parseInt(plugin.description.substring(plugin.description.indexOf(".")-2)) >= 8)
+{
+ShockMode = 1;
+}
+else if (navigator.userAgent && navigator.userAgent.indexOf("MSIE")>=0
+&& navigator.userAgent.indexOf("Opera")<0) {
+void('<SCRIPT LANGUAGE=VBScript\> \n');
+void('on error resume next \n');
+void('ShockMode = (IsObject(CreateObject("ShockwaveFlash.ShockwaveFlash.8")))\n');
+void('<\/SCRIPT\> \n');
+}
+if ( ShockMode ) {
+void('<objectdisabled classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"');
+void(' codebase="httpdisabled://downloaddisabled.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0"');
+void(' ID=flashad WIDTH=190 HEIGHT=300>');
+void(' <PARAM NAME=movie VALUE="httpdisabled://img1.126.net/channel5/008981/190300a_110406.swf?clickTag='+dcswf_click+'"> ');
+void(' <PARAM NAME=quality VALUE=autohigh> ');
+void(' <PARAM NAME=wmode VALUE=opaque> ');
+void(' <embeddisabled SRC="httpdisabled://img1.126.net/channel5/008981/190300a_110406.swf?clickTag='+dcswf_click+'" QUALITY=autohigh wmode=opaque');
+void(' NAME=flashad swLiveConnect=TRUE WIDTH=190 HEIGHT=300');
+void(' TYPE="application/x-shockwave-flash" PLUGINSPAGE="httpdisabled://www.macromedia.com/shockwave/downloaddisabled/index.cgi?P1_Prod_Version=ShockwaveFlash">');
+void('</EMBED>');
+void('</OBJECT>');
+} else if (!(navigator.appName && navigator.appName.indexOf("Netscape")>=0 && navigator.appVersion.indexOf("2.")>=0)){
+void('<A HREF="httpdisabled://g.163.com/c?AID=33876&FlightID=756&Values=3536047884&Redirect=http://a814.oadz.com/link/C/814/88637/UoFr9DZA7GBQz2OagEiruxYJZCc_/a/108/http://www.m18.com/market/front.aspx?pno=wan-hp-s1&url=/im" target="_blank"><IMG SRC="httpdisabled://img1.126.net/channel5/008981/190300a_110406.swf" WIDTH=190 HEIGHT=300 BORDER=0></A>');
+}
+//-->
+</SCRIPT>
+<NOEMBED>
+<A HREF="httpdisabled://g.163.com/c?AID=33876&FlightID=756&Values=3536047884&Redirect=http://a814.oadz.com/link/C/814/88637/UoFr9DZA7GBQz2OagEiruxYJZCc_/a/108/http://www.m18.com/market/front.aspx?pno=wan-hp-s1&url=/im" target="_blank"><IMG SRC="../img1.126.net/channel5/008981/190300a_110406.swf" WIDTH=190 HEIGHT=300 BORDER=0></A>
+</NOEMBED>
+<NOSCRIPT>
+<A HREF="httpdisabled://g.163.com/c?AID=33876&FlightID=756&Values=3536047884&Redirect=http://a814.oadz.com/link/C/814/88637/UoFr9DZA7GBQz2OagEiruxYJZCc_/a/108/http://www.m18.com/market/front.aspx?pno=wan-hp-s1&url=/im" target="_blank"><IMG SRC="../img1.126.net/channel5/008981/190300a_110406.swf" WIDTH=190 HEIGHT=300 BORDER=0></A>
+</NOSCRIPT> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x300&location=2.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x300&location=2.html
new file mode 100755
index 0000000000..30e84fdd6e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=logo190x300&location=2.html
@@ -0,0 +1 @@
+<html><head></head><body></body></html>
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=textlinkhouse&location=1.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=textlinkhouse&location=1.html
new file mode 100755
index 0000000000..3ab2840d4e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=textlinkhouse&location=1.html
@@ -0,0 +1,15 @@
+<style>
+body {background:#fff; font-size:12px;}
+a,a:visited {float:left;line-height:21px;overflow:hidden;width:120px;color:#1E50A2;text-decoration:none;}
+a:hover {color:#ba2636;}
+</style>
+<script type="text/javascript" src="../img3.126.net/rpic/fld3/flsclasses.js"></script>
+<script type="text/javascript" src="../img3.126.net/rpic/fld3/fld_homepage.js"></script>
+<script type="text/javascript">
+<!--
+if(typeof(qita)!=='undefined'){
+ var prov=qita;
+ echoa();
+}
+//-->
+</script>
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=textlinkhouse&location=2.html b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=textlinkhouse&location=2.html
new file mode 100755
index 0000000000..9f7a6d7fcd
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/g.163.com/r@site=netease&affiliate=homepage&cat=homepage&type=textlinkhouse&location=2.html
@@ -0,0 +1,15 @@
+<style>
+body {background:#fff; font-size:12px;}
+a,a:visited {float:left;line-height:21px;overflow:hidden;width:120px;color:#1E50A2;text-decoration:none;}
+a:hover {color:#ba2636;}
+</style>
+<script type="text/javascript" src="../img3.126.net/rpic/fld3/flsclasses.js"></script>
+<script type="text/javascript" src="../img3.126.net/rpic/fld3/fld_homepage.js"></script>
+<script type="text/javascript">
+<!--
+if(typeof(qita)!=='undefined'){
+ var prov=qita;
+ echob();
+}
+//-->
+</script>
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.126.net/channel1/55x20_bai.gif b/mobile/android/tests/browser/chrome/tp5/163.com/img1.126.net/channel1/55x20_bai.gif
new file mode 100755
index 0000000000..456c1ace4e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.126.net/channel1/55x20_bai.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.126.net/channel1/55x20_lan.gif b/mobile/android/tests/browser/chrome/tp5/163.com/img1.126.net/channel1/55x20_lan.gif
new file mode 100755
index 0000000000..8ec96875f4
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.126.net/channel1/55x20_lan.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.126.net/channel5/008976/bolon_110302.png b/mobile/android/tests/browser/chrome/tp5/163.com/img1.126.net/channel5/008976/bolon_110302.png
new file mode 100755
index 0000000000..895a9cdfc0
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.126.net/channel5/008976/bolon_110302.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.126.net/channel5/360/360100_110318.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img1.126.net/channel5/360/360100_110318.jpg
new file mode 100755
index 0000000000..db9b316ca5
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.126.net/channel5/360/360100_110318.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/6/20110406182512d4541.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/6/20110406182512d4541.jpg
new file mode 100755
index 0000000000..07620cc2f8
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/6/20110406182512d4541.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/20110408075741e084c.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/20110408075741e084c.jpg
new file mode 100755
index 0000000000..2ddd7ddf11
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/20110408075741e084c.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/2011040808080199ae7.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/2011040808080199ae7.jpg
new file mode 100755
index 0000000000..f6845bd03f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/2011040808080199ae7.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/201104080835397174e.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/201104080835397174e.jpg
new file mode 100755
index 0000000000..12a5059724
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/201104080835397174e.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/201104080847137e997.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/201104080847137e997.jpg
new file mode 100755
index 0000000000..39a395822b
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/201104080847137e997.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/20110408085323b9296.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/20110408085323b9296.jpg
new file mode 100755
index 0000000000..53386b1e88
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/20110408085323b9296.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/20110408092834ed61d.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/20110408092834ed61d.jpg
new file mode 100755
index 0000000000..738834c3d6
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/20110408092834ed61d.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/201104080930016f866.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/201104080930016f866.jpg
new file mode 100755
index 0000000000..d1905964fc
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/201104080930016f866.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/201104080934433598e.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/201104080934433598e.jpg
new file mode 100755
index 0000000000..b3581ab927
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/201104080934433598e.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/2011040809550649773.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/2011040809550649773.jpg
new file mode 100755
index 0000000000..9d23f2ddbd
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/2011040809550649773.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/20110408104255a47ce.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/20110408104255a47ce.jpg
new file mode 100755
index 0000000000..3df984301d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/20110408104255a47ce.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/201104081119113f37f.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/201104081119113f37f.jpg
new file mode 100755
index 0000000000..da1e5d8874
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/201104081119113f37f.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/2011040811445023471.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/2011040811445023471.jpg
new file mode 100755
index 0000000000..ddf8cc0475
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/2011040811445023471.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/2011040814544385564.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/2011040814544385564.jpg
new file mode 100755
index 0000000000..3e770a2c83
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/2011040814544385564.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/2011040815090608fd9.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/2011040815090608fd9.jpg
new file mode 100755
index 0000000000..5f01a91e04
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/8/2011040815090608fd9.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/9/20110409022720f974c.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/9/20110409022720f974c.jpg
new file mode 100755
index 0000000000..9e06a8f0c5
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/2011/4/9/20110409022720f974c.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/netease/wzdzbs.gif b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/netease/wzdzbs.gif
new file mode 100755
index 0000000000..b224d7532f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/cnews/netease/wzdzbs.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/digi/linzj/1102/03/191.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/digi/linzj/1102/03/191.jpg
new file mode 100755
index 0000000000..b8ac590425
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/digi/linzj/1102/03/191.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/img09/icon/icon.png b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/img09/icon/icon.png
new file mode 100755
index 0000000000..6c53687d59
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/img09/icon/icon.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/attr.png b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/attr.png
new file mode 100755
index 0000000000..f4e5da8d48
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/attr.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/icon_product_listv0.0.3.png b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/icon_product_listv0.0.3.png
new file mode 100755
index 0000000000..96302b0041
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/icon_product_listv0.0.3.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/iconv0.0.7.png b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/iconv0.0.7.png
new file mode 100755
index 0000000000..1c194a320a
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/iconv0.0.7.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/neteasy_mallv0.0.1.png b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/neteasy_mallv0.0.1.png
new file mode 100755
index 0000000000..b3ca15626e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/neteasy_mallv0.0.1.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/theme_blue.png b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/theme_blue.png
new file mode 100755
index 0000000000..ee56407d78
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/theme_blue.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/yodao_bg_blue.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/yodao_bg_blue.jpg
new file mode 100755
index 0000000000..5a0b12ac57
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img1.cache.netease.com/www/v2011/img/yodao_bg_blue.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/game/20110216/ql/x/390x100.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/game/20110216/ql/x/390x100.jpg
new file mode 100755
index 0000000000..4d66e1bcb5
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/game/20110216/ql/x/390x100.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/game/20110216/ql/x/600x80.gif b/mobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/game/20110216/ql/x/600x80.gif
new file mode 100755
index 0000000000..9a4b4d09f1
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/game/20110216/ql/x/600x80.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/hr/20110216/hz/360x100.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/hr/20110216/hz/360x100.jpg
new file mode 100755
index 0000000000..6a07904531
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/hr/20110216/hz/360x100.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/sales/2011/03/ly/390x100.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/sales/2011/03/ly/390x100.jpg
new file mode 100755
index 0000000000..69dca1a0db
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/sales/2011/03/ly/390x100.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/sales/2011/04/hy/190x100.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/sales/2011/04/hy/190x100.jpg
new file mode 100755
index 0000000000..42b7b50889
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/sales/2011/04/hy/190x100.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/sales/2011/04/wb/360x65.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/sales/2011/04/wb/360x65.jpg
new file mode 100755
index 0000000000..9fcb0fbd98
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/sales/2011/04/wb/360x65.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/sales/2011/04/yd/190x180.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/sales/2011/04/yd/190x180.jpg
new file mode 100755
index 0000000000..738bf4f528
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.126.net/xoimages/sales/2011/04/yd/190x180.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/7/20110407093718ef414.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/7/20110407093718ef414.jpg
new file mode 100755
index 0000000000..7305d4c89d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/7/20110407093718ef414.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/7/20110407202028db993.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/7/20110407202028db993.jpg
new file mode 100755
index 0000000000..d2b333d6f0
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/7/20110407202028db993.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/201104080728304dcb2.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/201104080728304dcb2.jpg
new file mode 100755
index 0000000000..cf3d891a51
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/201104080728304dcb2.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408082635b6897.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408082635b6897.jpg
new file mode 100755
index 0000000000..a388b07ff5
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408082635b6897.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/201104080828458908d.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/201104080828458908d.jpg
new file mode 100755
index 0000000000..61d31fdd97
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/201104080828458908d.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/2011040808393075049.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/2011040808393075049.jpg
new file mode 100755
index 0000000000..d58ddb9f1d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/2011040808393075049.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/2011040809433960d68.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/2011040809433960d68.jpg
new file mode 100755
index 0000000000..7c4ff54474
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/2011040809433960d68.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408100357df2b1.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408100357df2b1.jpg
new file mode 100755
index 0000000000..80d2d32da7
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408100357df2b1.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408115631ad273.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408115631ad273.jpg
new file mode 100755
index 0000000000..abb727cf26
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408115631ad273.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408120203d0f08.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408120203d0f08.jpg
new file mode 100755
index 0000000000..1370087a38
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408120203d0f08.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/201104081242198a4ba.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/201104081242198a4ba.jpg
new file mode 100755
index 0000000000..0308b659c2
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/201104081242198a4ba.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/2011040812525484a8f.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/2011040812525484a8f.jpg
new file mode 100755
index 0000000000..86e9a6af23
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/2011040812525484a8f.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408125931e0a79.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408125931e0a79.jpg
new file mode 100755
index 0000000000..2208ff048f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408125931e0a79.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408140704d246b.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408140704d246b.jpg
new file mode 100755
index 0000000000..86e50ca7f1
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408140704d246b.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408144428d419d.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408144428d419d.jpg
new file mode 100755
index 0000000000..0f0cd9d38c
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/20110408144428d419d.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/2011040814452013ef7.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/2011040814452013ef7.jpg
new file mode 100755
index 0000000000..b45184d5a7
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/2011040814452013ef7.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/2011040814525199c07.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/2011040814525199c07.jpg
new file mode 100755
index 0000000000..d177507116
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/2011040814525199c07.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/201104082245192ae96.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/201104082245192ae96.jpg
new file mode 100755
index 0000000000..7af2c40005
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/cnews/2011/4/8/201104082245192ae96.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/www/v2011/css/theme_blue1227.css b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/www/v2011/css/theme_blue1227.css
new file mode 100755
index 0000000000..6605aff4a9
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/www/v2011/css/theme_blue1227.css
@@ -0,0 +1 @@
+/* theme blue */ .mod .hd,.wgt-yodao-search .ui-btn-submit,.weather-location,.search-category-more,.yodao-search-category .current,.search-category-item a:hover,.yodao-dialog .ui-btn-submit,.yodao-dialog-close,.mod .hd,.tab-hd,.mod-function,.function-close,.product-list li {background:url("../../../../img1.cache.netease.com/www/v2011/img/theme_blue.png") no-repeat;} /* mod color */ .jstxlan,.c-entry,a.c-entry:visited,.c-entry a,.c-entry a:visited,.hd,.hd a,.hd a:visited,.tab-hd,.tab-hd a,.tab-hd a:visited,.search-category-item a {color:#1E50A2;} a.c-entry:hover,.c-entry a:hover,.hd a:hover,.jstxlan:hover{color:#ba2636;} .mod .bd,.mod .hd,.mod-function,.aboutNetease,.ntes-yodao {border:1px solid #B1C8D7;} .mod .bd {border-top:none;} .tab-hd {border-left:1px solid #B1C8D7;} .tab-hd .tab-u {border-top:1px solid #B1C8D7; border-right:1px solid #B1C8D7;} .area-sub .bd,.tab-u-5 .current,.product-tab .current,.aboutNetease {background-color:#F5F8FC;} .mod .hd,.tab-hd,.mod-function,.tab-hd-gg-left li,.tab-hd-gg-right li {background-color:#E6EEF7;} .tab-hd-gg-left .current,.tab-hd-gg-right .current {background-color:#D4E6F5;} /* yodao color */ .ntes-yodao {background:url("../../../../img1.cache.netease.com/www/v2011/img/yodao_bg_blue.jpg") left top no-repeat #D4E6F5;} .wgt-yodao-search .ui-btn-submit {color:#fff; background-color:#3981BD;} .yodao-search-category .current,.search-category-item a:hover {font-weight:bold; color:#fff;} .search-category-item a:hover {color:#fff;} .yodao-dialog {border:1px solid #B1C8D7; background:#fff;} .yodao-dialog .hd {background:#D4E6F5;} .yodao-dialog .bd {background:#E6EEF7;} .category-more-list {border:1px solid #B1C8D7; background:#fff;} .category-more-list a:hover {color:#fff; background:#1E50A2;} .aa_highlight {color:#fff;background:#1E50A2;} \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/www/v2011/img/tg_news.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/www/v2011/img/tg_news.jpg
new file mode 100755
index 0000000000..e4cd6bf2b2
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img2.cache.netease.com/www/v2011/img/tg_news.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/163homepage/biaoshi.gif b/mobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/163homepage/biaoshi.gif
new file mode 100755
index 0000000000..b8be1daefa
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/163homepage/biaoshi.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/163homepage/bj110.gif b/mobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/163homepage/bj110.gif
new file mode 100755
index 0000000000..2c9b488ee9
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/163homepage/bj110.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/rpic/fld3/fld_homepage.js b/mobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/rpic/fld3/fld_homepage.js
new file mode 100755
index 0000000000..45a11363b8
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/rpic/fld3/fld_homepage.js
@@ -0,0 +1,987 @@
+
+var quanguo=new section(
+
+'³ÄÜÁìÐã³ÇÂþɽÏãÊû',
+'http://g.163.com/a?CID=1631&Values=979604783&Redirect=http://jn.house.163.com/topic/hz/lxcmsxs/index.shtml',
+
+'µ½ÎÞÎýÌì¶ìºþ³­µ×È¥',
+'http://g.163.com/a?CID=1632&Values=481254470&Redirect=http://wx.house.163.com/topic/hz/wxteh/',
+
+'ÎÞÎýµØ±êµÄ¼ÛֵͶ×Ê',
+'http://g.163.com/a?CID=1633&Values=3231158258&Redirect=http://wx.house.163.com/topic/hz/wxhd/',
+
+'ÌÆɽÍò´ïÏÖ·¿ÈÈÏú',
+'http://g.163.com/a?CID=1634&Values=3016013117&Redirect=http://www.tangshanwanda.com/',
+
+'µÚÒ»´Î¹º·¿Î¨Ñ¡Íò¿Æ',
+'http://g.163.com/a?CID=1635&Values=1850202326&Redirect=http://wh.vanke.com/Decoration/Index.aspx',
+
+'´óÏóȺרע»¥¶¯ÐÐÏú',
+'http://g.163.com/a?CID=1636&Values=2652887900&Redirect=http://www.daxiangqun.com/',
+
+'ÊÀ½ç±­¾º²ÂÓ®µçÄÔ',
+'http://g.163.com/a?CID=1637&Values=1890633164&Redirect=http://beijing.chineseoffice.com.cn',
+
+'ËæʱËæµØÊÕ·¢Óʼþ',
+'http://dxyy.mail.163.com/smspack/userconf/dxtz.do',
+
+'·¿ÀÏ´ó£¬Ô²ÄãÓмÒÃÎ',
+'http://g.163.com/a?CID=1639&Values=2962722568&Redirect=http://www.foloda.com',
+
+'´óÏóȺ רע»¥¶¯ÐÐÏú',
+'http://g.163.com/a?CID=1640&Values=1297729454&Redirect=http://www.daxiangqun.com/'
+
+);
+
+var anhui=new section(
+
+'ÍøÒ×·¿²úºÏ·Ê³ÏƸ',
+'http://g.163.com/a?CID=1431&Values=140707824&Redirect=http://hf.house.163.com/news2/101019/15/715993-1.shtml',
+
+'Íò´ïÕÐÉÌÈ«ÃæÆô¶¯',
+'http://g.163.com/a?CID=1432&Values=3444093944&Redirect=http://xm.house.163.com/topic/hz/xmwd/index.html',
+
+'Íò´ïºÀÕ¬ ÔÙÒ«ºÏ·Ê',
+'http://g.163.com/a?CID=1433&Values=3930620465&Redirect=http://hf.house.163.com/topic/hz/hfwdgg2/',
+
+'ÉÌÒµµØ²úͶ×ÊÇ÷»ð±¬',
+'http://g.163.com/a?CID=1434&Values=3400740464&Redirect=http://ts.house.163.com//topic/hz/tswd001/index1.html',
+
+'Íò´ïÖÐÐÄÉÌÎñÇøÆô¶¯',
+'http://g.163.com/a?CID=1435&Values=3330699301&Redirect=http://nj.house.163.com/topic/hz/njwd/',
+
+'Ê׸¶15ÍòÍò¿Æ×°ÐÞ·¿',
+'http://g.163.com/a?CID=1436&Values=2716708116&Redirect=http://wh.vanke.com/indexad.asp?Title=0221163wzl&UrlTo=/house/goldencity/register.asp',
+
+'Íò´ïÔ¼»áÉãÓ°¡°´ï¡±ÈË',
+'http://g.163.com/a?CID=1437&Values=665590924&Redirect=http://hf.house.163.com/news2/110303/1/770167-1.shtml',
+
+'ÁìÐã³Ç×¼ÏÖ·¿·¢ÊÛÖÐ',
+'http://g.163.com/a?CID=1438&Values=2211550380&Redirect=http://jn.house.163.com/topic/hz/lnzygy/',
+
+'¹ú¼ÊÍò´ï£¬ÔìÐĽ­Òõ',
+'http://g.163.com/a?CID=1439&Values=305957742&Redirect=http://wx.house.163.com/topic/hz/jywd/',
+
+'ËÕÖݹųǺËÐĽÖÆÌ',
+'http://g.163.com/a?CID=1440&Values=528917216&Redirect=http://suzhou.house.163.com/topic/suzhou/xintiandidd/index.htm'
+
+);
+
+var chongqing=new section(
+
+'÷ÈÁ¦Ö®³ÇÉý¼¶°æ£¡',
+'http://g.163.com/a?CID=1501&Values=1183866730&Redirect=http://wh.vanke.com/indexad.asp?Title=wangyiyouxiang&UrlTo=/house/usonian/register.asp',
+
+'¡°µ÷¿Ø¡±or¡°µ÷Ï·¡±',
+'http://g.163.com/a?CID=1502&Values=2836950569&Redirect=http://cq.house.163.com/topic/cq/tk/index.html',
+
+'°ëɽ»ª¸®2ÆÚ½«ÆôÄ»',
+'http://g.163.com/a?CID=1503&Values=1431490966&Redirect=http://cq.house.163.com/topic/cq/bshf110401/index.html',
+
+'Çá¹ì¸Ä±äÖØÇìÂ¥ÊÐ',
+'http://g.163.com/a?CID=1504&Values=936064134&Redirect=http://cq.house.163.com/topic/cq/qggbsh/index.html',
+
+'ÍøÒ×·¿²ú³É¶¼³ÏƸ',
+'http://g.163.com/a?CID=1505&Values=4093663140&Redirect=http://cq.house.163.com/news2/101019/8/715995-1.shtml',
+
+'ÏÞ¹º·ç±©Ç¿ÊÆÀ´Ï®',
+'http://g.163.com/a?CID=1506&Values=3046797410&Redirect=http://cq.house.163.com/topic/cq/xgl/index.html',
+
+'Íò¿Æ½ðÓòÀ¶Í彫¿ªÅÌ',
+'http://g.163.com/a?CID=1507&Values=2696576883&Redirect=http://wh.vanke.com/indexad.asp?Title=163sywzl0321&UrlTo=/house/paradiso/register.asp',
+
+'±±Â´Ô­¿ªÅÌÈÈÏúÖÐ',
+'http://g.163.com/a?CID=1508&Values=2037757007&Redirect=http://cq.house.163.com/topic/cq/zybly110314/index.html',
+
+'ÍøÒ×·¿²úÖØÇìÕ¾ÕÐƸ',
+'http://g.163.com/a?CID=1509&Values=3615659065&Redirect=http://cq.house.163.com/topic/cq/cqzp/index.html',
+
+'¶þÊÖ·¿×â·¿--·¿ÀÏ´ó',
+'http://g.163.com/a?CID=1510&Values=3155480230&Redirect=http://www.foloda.com'
+
+);
+
+var fujian=new section(
+
+'ÍøÒ×·¿²ú¸£ÖݳÏƸ',
+'http://g.163.com/a?CID=1391&Values=440153523&Redirect=http://fz.house.163.com/news2/110106/8/749421-1.shtml',
+
+'Íò´ïÕÐÉÌÈ«ÃæÆô¶¯',
+'http://g.163.com/a?CID=1392&Values=875135822&Redirect=http://xm.house.163.com/topic/hz/xmwd/index.html',
+
+'Ê¢ÊÀÍò´ï ÕÀ·ÅÕÄÖÝ',
+'http://g.163.com/a?CID=1393&Values=1352019128&Redirect=http://xm.house.163.com/topic/xm/zhangzhouwdgcdianji/zhangzhouwdgcdianji/index.html',
+
+'ÄÏÖйú¼Ò×å´óÕ¬',
+'http://g.163.com/a?CID=1394&Values=3905919256&Redirect=http://xm.house.163.com/topic/xm/ydzzyj/ydzz.htm',
+
+'פ¾©°ìÊ´¦Ñ¡Ö·´óÈ«',
+'http://g.163.com/a?CID=1395&Values=3761932044&Redirect=http://www.chineseoffice.com.cn/',
+
+'ÍøÒ×·¿²úÏÃÃųÏƸ',
+'http://g.163.com/a?CID=1396&Values=1528249410&Redirect=http://xm.house.163.com/news2/110215/8/762587-1.shtml',
+
+'ȪÖÝÆÖÎ÷Íò´ï¹ã³¡',
+'http://g.163.com/a?CID=1397&Values=168539782&Redirect=http://xm.house.163.com/topic/xm/qzpxwdgc/',
+
+'ÐǾÛÍò´ï ÁìÒ«º£Î÷',
+'http://g.163.com/a?CID=1398&Values=942518717&Redirect=http://xm.house.163.com/topic/xm/wdzc/',
+
+'ÕÄÖÝ·¿Ô´ Ò»Íø´ò¾¡',
+'http://g.163.com/a?CID=1399&Values=658166132&Redirect=http://zhangzhou.house.163.com',
+
+'ËÕÖݹųǺËÐĽÖÆÌ',
+'http://g.163.com/a?CID=1400&Values=2760702008&Redirect=http://suzhou.house.163.com/topic/suzhou/xintiandidd/index.htm'
+
+);
+
+var gansu=new section(
+
+'ÍøÒ×·¿²úÀ¼ÖݳÏƸ',
+'http://g.163.com/a?CID=1591&Values=696978067&Redirect=http://lz.house.163.com/news2/101020/3/716104-1.shtml',
+
+'Íò´ïÕÐÉÌÈ«ÃæÆô¶¯',
+'http://g.163.com/a?CID=1592&Values=339419003&Redirect=http://xm.house.163.com/topic/hz/xmwd/index.html',
+
+'Ê×´´¹ú¼Ê³ÇÁìÏα±³Ç',
+'http://g.163.com/a?CID=1593&Values=3649096630&Redirect=http://km.house.163.com/topic/xa/scgjcba/',
+
+'ÉÌÒµµØ²úͶ×ÊÇ÷»ð±¬',
+'http://g.163.com/a?CID=1594&Values=1495066886&Redirect=http://ts.house.163.com//topic/hz/tswd001/index1.html',
+
+'ÁìÐã³Ç×¼ÏÖ·¿·¢ÊÛÖÐ',
+'http://g.163.com/a?CID=1595&Values=3004257764&Redirect=http://jn.house.163.com/topic/hz/lnzygy/',
+
+'Íò¿Æ½ðÓòÀ¶Í彫¿ªÅÌ',
+'http://g.163.com/a?CID=1596&Values=3723417916&Redirect=http://wh.vanke.com/indexad.asp?Title=163sywzl0321&UrlTo=/house/paradiso/register.asp',
+
+'Íò´ïÖÐÐÄÉÌÎñÇøÆô¶¯',
+'http://g.163.com/a?CID=1597&Values=3731340378&Redirect=http://nj.house.163.com/topic/hz/njwd/',
+
+'Íò´ïºÀÕ¬ ÔÙÒ«ºÏ·Ê',
+'http://g.163.com/a?CID=1598&Values=1663707362&Redirect=http://hf.house.163.com/topic/hz/hfwdgg2/',
+
+'פ¾©°ìÊ´¦Ñ¡Ö·´óÈ«',
+'http://g.163.com/a?CID=1599&Values=621830313&Redirect=http://beijing.chineseoffice.com.cn',
+
+'¶þÊÖ·¿×â·¿--·¿ÀÏ´ó',
+'http://g.163.com/a?CID=1600&Values=1511589405&Redirect=http://www.foloda.com'
+
+);
+
+var guangxi=new section(
+
+'ÍøÒ×·¿²úÄÏÄþ³ÏƸ',
+'http://g.163.com/a?CID=1531&Values=1077111064&Redirect=http://nn.house.163.com/news2/101019/15/716001-1.shtml',
+
+'¹ú¼ÊÍò´ï£¬ÔìÐĽ­Òõ',
+'http://g.163.com/a?CID=1532&Values=2705180565&Redirect=http://wx.house.163.com/topic/hz/jywd/',
+
+'ÉÌÒµµØ²úͶ×ÊÇ÷»ð±¬',
+'http://g.163.com/a?CID=1533&Values=972094116&Redirect=http://ts.house.163.com//topic/hz/tswd001/index1.html',
+
+'Àì½­À¶ÍåÁëÓòµ½À¶Íå',
+'http://g.163.com/a?CID=1534&Values=1337672369&Redirect=http://www.v9666.com',
+
+'°ëɽ»ª¸®2ÆÚ½«ÆôÄ»',
+'http://g.163.com/a?CID=1535&Values=440431970&Redirect=http://cq.house.163.com/topic/cq/bshf110401/index.html',
+
+'÷ÈÁ¦Ö®³ÇÉý¼¶°æ£¡',
+'http://g.163.com/a?CID=1536&Values=554921589&Redirect=http://wh.vanke.com/indexad.asp?Title=wangyiyouxiang&UrlTo=/house/usonian/register.asp',
+
+'Íò¿Æ½ðÓòÀ¶Í彫¿ªÅÌ',
+'http://g.163.com/a?CID=1537&Values=1603859496&Redirect=http://wh.vanke.com/indexad.asp?Title=163sywzl0321&UrlTo=/house/paradiso/register.asp',
+
+'±±Â´Ô­¿ªÅÌÈÈÏúÖÐ',
+'http://g.163.com/a?CID=1538&Values=4242251342&Redirect=http://cq.house.163.com/topic/cq/zybly110314/index.html',
+
+'פ¾©°ìÊ´¦Ñ¡Ö·´óÈ«',
+'http://g.163.com/a?CID=1539&Values=974017740&Redirect=http://beijing.chineseoffice.com.cn',
+
+'¶þÊÖ·¿×â·¿--·¿ÀÏ´ó',
+'http://g.163.com/a?CID=1540&Values=722714999&Redirect=http://www.foloda.com'
+
+);
+
+var guizhou=new section(
+
+'ÍøÒ×·¿²ú¹óÑô³ÏƸ',
+'http://g.163.com/a?CID=1511&Values=1125427351&Redirect=http://gy.house.163.com/news2/101019/15/715994-1.shtml',
+
+'¹ú¼ÊÍò´ï£¬ÔìÐĽ­Òõ',
+'http://g.163.com/a?CID=1512&Values=2565614707&Redirect=http://wx.house.163.com/topic/hz/jywd/',
+
+'ÉÌÒµµØ²úͶ×ÊÇ÷»ð±¬',
+'http://g.163.com/a?CID=1513&Values=2983829438&Redirect=http://ts.house.163.com//topic/hz/tswd001/index1.html',
+
+'Àì½­À¶ÍåÁëÓòµ½À¶Íå',
+'http://g.163.com/a?CID=1514&Values=1065898650&Redirect=http://www.v9666.com',
+
+'°ëɽ»ª¸®2ÆÚ½«ÆôÄ»',
+'http://g.163.com/a?CID=1515&Values=4068513241&Redirect=http://cq.house.163.com/topic/cq/bshf110401/index.html',
+
+'÷ÈÁ¦Ö®³ÇÉý¼¶°æ£¡',
+'http://g.163.com/a?CID=1516&Values=917345527&Redirect=http://wh.vanke.com/indexad.asp?Title=wangyiyouxiang&UrlTo=/house/usonian/register.asp',
+
+'Íò¿Æ½ðÓòÀ¶Í彫¿ªÅÌ',
+'http://g.163.com/a?CID=1517&Values=3140966748&Redirect=http://wh.vanke.com/indexad.asp?Title=163sywzl0321&UrlTo=/house/paradiso/register.asp',
+
+'±±Â´Ô­¿ªÅÌÈÈÏúÖÐ',
+'http://g.163.com/a?CID=1518&Values=2970839809&Redirect=http://cq.house.163.com/topic/cq/zybly110314/index.html',
+
+'פ¾©°ìÊ´¦Ñ¡Ö·´óÈ«',
+'http://g.163.com/a?CID=1519&Values=1000416879&Redirect=http://beijing.chineseoffice.com.cn',
+
+'¶þÊÖ·¿×â·¿--·¿ÀÏ´ó',
+'http://g.163.com/a?CID=1520&Values=3039296944&Redirect=http://www.foloda.com'
+
+);
+
+var hebei=new section(
+
+'´óÏóȺרעµØ²ú»¥¶¯',
+'http://g.163.com/a?CID=1481&Values=3857846246&Redirect=http://www.daxiangqun.com/',
+
+'ÉÌÒµµØ²úͶ×ÊÇ÷»ð±¬',
+'http://g.163.com/a?CID=1482&Values=3641079257&Redirect=http://ts.house.163.com//topic/hz/tswd001/index1.html',
+
+'ÁìÐã³Ç×¼ÏÖ·¿·¢ÊÛÖÐ',
+'http://g.163.com/a?CID=1483&Values=2276506178&Redirect=http://jn.house.163.com/topic/hz/lnzygy/',
+
+'Íò´ïÕÐÉÌÈ«ÃæÆô¶¯',
+'http://g.163.com/a?CID=1484&Values=1308762028&Redirect=http://xm.house.163.com/topic/hz/xmwd/index.html',
+
+'È¥ÄÏ´÷ºÓ ÁÙº£ÌýÌÎ',
+'http://g.163.com/a?CID=1485&Values=674631556&Redirect=http://qhd.house.163.com/topic/hz/lhtt0331/',
+
+'Ê׸¶15ÍòÍò¿Æ×°ÐÞ·¿',
+'http://g.163.com/a?CID=1486&Values=185257524&Redirect=http://wh.vanke.com/indexad.asp?Title=0221163wzl&UrlTo=/house/goldencity/register.asp',
+
+'Íò´ïÖÐÐÄÉÌÎñÇøÆô¶¯',
+'http://g.163.com/a?CID=1487&Values=2245822886&Redirect=http://nj.house.163.com/topic/hz/njwd/',
+
+'Íò´ïºÀÕ¬ ÔÙÒ«ºÏ·Ê',
+'http://g.163.com/a?CID=1488&Values=2492828518&Redirect=http://hf.house.163.com/topic/hz/hfwdgg2/',
+
+'פ¾©°ìÊ´¦Ñ¡Ö·´óÈ«',
+'http://g.163.com/a?CID=1489&Values=709633319&Redirect=http://beijing.chineseoffice.com.cn',
+
+'Íò¿Æ½ðÓòÀ¶Í彫¿ªÅÌ',
+'http://g.163.com/a?CID=1490&Values=1922383439&Redirect=http://wh.vanke.com/indexad.asp?Title=163sywzl0321&UrlTo=/house/paradiso/register.asp'
+
+);
+
+var heilongjiang=new section(
+
+'Íò´ï¹«¹Ý ºÀÕ¬µä·¶',
+'http://g.163.com/a?CID=1561&Values=3878478489&Redirect=http://www.wandamansion.com',
+
+'Íò´ïÕÐÉÌÈ«ÃæÆô¶¯',
+'http://g.163.com/a?CID=1562&Values=3205384429&Redirect=http://xm.house.163.com/topic/hz/xmwd/index.html',
+
+'Ê×´´¹ú¼Ê³ÇÁìÏα±³Ç',
+'http://g.163.com/a?CID=1563&Values=2478625523&Redirect=http://km.house.163.com/topic/xa/scgjcba/',
+
+'Ô¶Ñóʱ´ú³ÇÐÂÆ·³ö»÷',
+'http://g.163.com/a?CID=1564&Values=119944109&Redirect=http://www.yysdc.com/',
+
+'ÁìÐã³Ç×¼ÏÖ·¿·¢ÊÛÖÐ',
+'http://g.163.com/a?CID=1565&Values=34746919&Redirect=http://jn.house.163.com/topic/hz/lnzygy/',
+
+'´óÏóȺרעµØ²ú»¥¶¯',
+'http://g.163.com/a?CID=1566&Values=3771046252&Redirect=http://www.daxiangqun.com/',
+
+'ËÕÖݹųǺËÐĽÖÆÌ',
+'http://g.163.com/a?CID=1567&Values=3020440879&Redirect=http://suzhou.house.163.com/topic/suzhou/xintiandidd/index.htm',
+
+'Íò´ïºÀÕ¬ ÔÙÒ«ºÏ·Ê',
+'http://g.163.com/a?CID=1568&Values=1549320734&Redirect=http://hf.house.163.com/topic/hz/hfwdgg2/',
+
+'È¥ÄÏ´÷ºÓ ÁÙº£ÌýÌÎ',
+'http://g.163.com/a?CID=1569&Values=117217106&Redirect=http://qhd.house.163.com/topic/hz/lhtt0331/',
+
+'Íò¿Æ½ðÓòÀ¶Í彫¿ªÅÌ',
+'http://g.163.com/a?CID=1570&Values=1423017304&Redirect=http://wh.vanke.com/indexad.asp?Title=163sywzl0321&UrlTo=/house/paradiso/register.asp'
+
+);
+
+var henan=new section(
+
+'ÍøÒ×·¿²úÖ£ÖÝÕ¾ÕÐƸ',
+'http://g.163.com/a?CID=1471&Values=2531271616&Redirect=http://zz.house.163.com/news2/101020/1/716068-1.shtml',
+
+'Íò´ïÕÐÉÌÈ«ÃæÆô¶¯',
+'http://g.163.com/a?CID=1472&Values=2927214491&Redirect=http://xm.house.163.com/topic/hz/xmwd/index.html',
+
+'Íò´ï¾«×°SOHO½«Èϳï',
+'http://g.163.com/a?CID=1473&Values=303570349&Redirect=http://zz.house.163.com/topic/hz/zywanda/',
+
+'ÉÌÒµµØ²úͶ×ÊÇ÷»ð±¬',
+'http://g.163.com/a?CID=1474&Values=2822872691&Redirect=http://ts.house.163.com//topic/hz/tswd001/index1.html',
+
+'Ö£ÖÝÂò·¿£¬ÍøÒ×°ïæ',
+'http://g.163.com/a?CID=1475&Values=846902695&Redirect=http://zz.house.163.com/topic/hz/zzhx/',
+
+'Íò´ïºÀÕ¬ ÔÙÒ«ºÏ·Ê',
+'http://g.163.com/a?CID=1476&Values=3992464607&Redirect=http://hf.house.163.com/topic/hz/hfwdgg2/',
+
+'Íò´ïÖÐÐÄÉÌÎñÇøÆô¶¯',
+'http://g.163.com/a?CID=1477&Values=2041088546&Redirect=http://nj.house.163.com/topic/hz/njwd/',
+
+'ÁìÐã³Ç×¼ÏÖ·¿·¢ÊÛÖÐ',
+'http://g.163.com/a?CID=1478&Values=95266873&Redirect=http://jn.house.163.com/topic/hz/lnzygy/',
+
+'¹ú¼ÊÍò´ï£¬ÔìÐĽ­Òõ',
+'http://g.163.com/a?CID=1479&Values=1720943188&Redirect=http://wx.house.163.com/topic/hz/jywd/',
+
+'Íò¿Æ½ðÓòÀ¶Í彫¿ªÅÌ',
+'http://g.163.com/a?CID=1480&Values=4032443472&Redirect=http://wh.vanke.com/indexad.asp?Title=163sywzl0321&UrlTo=/house/paradiso/register.asp'
+
+);
+
+var hubei=new section(
+
+'Íò¿Æ½ðÓòÀ¶Í彫¿ªÅÌ',
+'http://g.163.com/a?CID=1371&Values=1761110635&Redirect=http://wh.vanke.com/indexad.asp?Title=163sywzl0321&UrlTo=/house/paradiso/register.asp',
+
+'ËÕÖݹųǺËÐĽÖÆÌ',
+'http://g.163.com/a?CID=1372&Values=1606976687&Redirect=http://suzhou.house.163.com/topic/suzhou/xintiandidd/index.htm',
+
+'÷ÈÁ¦Ö®³ÇÉý¼¶°æ£¡',
+'http://g.163.com/a?CID=1373&Values=1784622907&Redirect=http://wh.vanke.com/indexad.asp?Title=wangyiyouxiang&UrlTo=/house/usonian/register.asp',
+
+'2011ÎÒÃÇÒ»ÆðÂò·¿°É',
+'http://g.163.com/a?CID=1374&Values=3122894164&Redirect=http://wh.house.163.com/topic/wh/wankezh0317/index.htm',
+
+'°ëɽ»ª¸®2ÆÚ½«ÆôÄ»',
+'http://g.163.com/a?CID=1375&Values=767698364&Redirect=http://cq.house.163.com/topic/cq/bshf110401/index.html',
+
+'±±Â´Ô­3ÔÂ26ÈÕ¿ªÅÌ',
+'http://g.163.com/a?CID=1376&Values=3250660165&Redirect=http://cq.house.163.com/topic/cq/zybly110314/index.html',
+
+'Íò´ïÕÐÉÌÈ«ÃæÆô¶¯',
+'http://g.163.com/a?CID=1377&Values=3657120673&Redirect=http://xm.house.163.com/topic/hz/xmwd/index.html',
+
+'ÍøÒ×·¿²úÎ人վÕÐƸ',
+'http://g.163.com/a?CID=1378&Values=1352200508&Redirect=http://wh.house.163.com/topic/hz/whzp/index.html',
+
+'פ¾©°ìÊ´¦Ñ¡Ö·´óÈ«',
+'http://g.163.com/a?CID=1379&Values=3241571470&Redirect=http://beijing.chineseoffice.com.cn',
+
+'¶þÊÖ·¿×â·¿--·¿ÀÏ´ó',
+'http://g.163.com/a?CID=1380&Values=2788700399&Redirect=http://www.foloda.com'
+
+);
+
+var hunan=new section(
+
+'ÍøÒ×·¿²úºþÄÏÕÐƸ',
+'http://g.163.com/a?CID=1491&Values=172820044&Redirect=http://cs.house.163.com/news2/101207/13/737830-1.shtml',
+
+'ºã´óÂÌÖÞ ÐÂÆ·Ò«ÊÀ',
+'http://g.163.com/a?CID=1492&Values=3746216837&Redirect=http://ad.foloda.com/11dy/cshd/',
+
+'³¤É³Íò¿Æ ÐÂÆ··¢²¼',
+'http://g.163.com/a?CID=1493&Values=3517398304&Redirect=http://ad.foloda.com/10dy/cswkc',
+
+'³¤É³Íò´ï Ê×ϯºÀÕ¬',
+'http://g.163.com/a?CID=1494&Values=1457375266&Redirect=http://www.cswdgg.com/',
+
+'ÖÐÁ¸µØ²úÂÌÉ«¼ÎÄ껪',
+'http://g.163.com/a?CID=1495&Values=3615474435&Redirect=http://cs.house.163.com/news2/110323/4/779240-1.shtml',
+
+'±±Â´Ô­3ÔÂ26ÈÕ¿ªÅÌ',
+'http://g.163.com/a?CID=1496&Values=524253096&Redirect=http://cq.house.163.com/topic/cq/zybly110314/index.html',
+
+'Ïæ½­ÊÀ¼Í³Ç»¨Ô°´óÕ¬',
+'http://g.163.com/a?CID=1497&Values=599632246&Redirect=http://ad.foloda.com/11dy/xjsjc/',
+
+'ÉÏÎå¿ó΢²©£¬Ó®IPAD',
+'http://g.163.com/a?CID=1498&Values=133994750&Redirect=http://cs.house.163.com/news2/110321/4/777990-1.shtml',
+
+'¹ú¼ÊÍò´ï£¬ÔìÐĽ­Òõ',
+'http://g.163.com/a?CID=1499&Values=2128022708&Redirect=http://wx.house.163.com/topic/hz/jywd/',
+
+'Íò¿Æ½ðÓòÀ¶Í彫¿ªÅÌ',
+'http://g.163.com/a?CID=1500&Values=728830475&Redirect=http://wh.vanke.com/indexad.asp?Title=163sywzl0321&UrlTo=/house/paradiso/register.asp'
+
+);
+
+var jiangsu=new section(
+
+'ÍøÒ×·¿²ú½­ËÕ³ÏƸ',
+'http://g.163.com/a?CID=1421&Values=1720134227&Redirect=http://nj.house.163.com/news2/101019/15/715992-1.shtml',
+
+'ÄϾ©Íò´ïÖÐÐÄÆô¶¯ÖÐ',
+'http://g.163.com/a?CID=1422&Values=909788656&Redirect=http://nj.house.163.com/topic/hz/njwd/',
+
+'¹ú¼ÊÍò´ï£¬ÔìÐĽ­Òõ',
+'http://g.163.com/a?CID=1423&Values=3971362695&Redirect=http://wx.house.163.com/topic/hz/jywd/',
+
+'פ¾©°ìÊ´¦Ñ¡Ö·´óÈ«',
+'http://g.163.com/a?CID=1424&Values=336124741&Redirect=http://www.chineseoffice.com.cn/',
+
+'ËÕÖÝÂ¥ÅÌÐÅÏ¢Ò»ÀÀ',
+'http://g.163.com/a?CID=1425&Values=1954580977&Redirect=http://xf.house.163.com/suzhou/search!xfs.action',
+
+'´óÏóȺרעµØ²ú»¥¶¯',
+'http://g.163.com/a?CID=1426&Values=1687166304&Redirect=http://www.daxiangqun.com/',
+
+'Ͷ×ʾÍÒª´Ó³­µ×¿ªÊ¼',
+'http://g.163.com/a?CID=1427&Values=723479546&Redirect=http://suzhou.house.163.com/topic/suzhou/ganglongcaizhi/index.html',
+
+'¶þÊÖ·¿×â·¿--·¿ÀÏ´ó',
+'http://g.163.com/a?CID=1428&Values=199811649&Redirect=http://www.foloda.com',
+
+'Íò´ïºÀÕ¬ ÔÙÒ«ºÏ·Ê',
+'http://g.163.com/a?CID=1429&Values=4148656357&Redirect=http://hf.house.163.com/topic/hz/hfwdgg2/',
+
+'ËÕÖÝ×îз¿²ú×ÊѶ',
+'http://g.163.com/a?CID=1430&Values=1245676825&Redirect=http://suzhou.house.163.com/'
+
+);
+
+var jiangxi=new section(
+
+'ÍøÒ×·¿²úÄϲý³ÏƸ',
+'http://g.163.com/a?CID=1401&Values=650836040&Redirect=http://nc.house.163.com/news2/101020/8/716061-1.shtml',
+
+'Íò´ïÕÐÉÌÈ«ÃæÆô¶¯',
+'http://g.163.com/a?CID=1402&Values=2175178201&Redirect=http://xm.house.163.com/topic/hz/xmwd/index.html',
+
+'¹ú¼ÊÍò´ï£¬ÔìÐĽ­Òõ',
+'http://g.163.com/a?CID=1403&Values=2501733054&Redirect=http://wx.house.163.com/topic/hz/jywd/',
+
+'ÉÌÒµµØ²úͶ×ÊÇ÷»ð±¬',
+'http://g.163.com/a?CID=1404&Values=3569609227&Redirect=http://ts.house.163.com//topic/hz/tswd001/index1.html',
+
+'ÁìÐã³Ç×¼ÏÖ·¿·¢ÊÛÖÐ',
+'http://g.163.com/a?CID=1405&Values=1627759603&Redirect=http://jn.house.163.com/topic/hz/lnzygy/',
+
+'Ê׸¶15ÍòÍò¿Æ×°ÐÞ·¿',
+'http://g.163.com/a?CID=1406&Values=2762263709&Redirect=http://wh.vanke.com/indexad.asp?Title=0221163wzl&UrlTo=/house/goldencity/register.asp',
+
+'Íò´ïÖÐÐÄÉÌÎñÇøÆô¶¯',
+'http://g.163.com/a?CID=1407&Values=658658995&Redirect=http://nj.house.163.com/topic/hz/njwd/',
+
+'Íò´ïºÀÕ¬ ÔÙÒ«ºÏ·Ê',
+'http://g.163.com/a?CID=1408&Values=2474916214&Redirect=http://hf.house.163.com/topic/hz/hfwdgg2/',
+
+'Íò¿Æ½ðÓòÀ¶Í彫¿ªÅÌ',
+'http://g.163.com/a?CID=1409&Values=3955873125&Redirect=http://wh.vanke.com/indexad.asp?Title=163sywzl0321&UrlTo=/house/paradiso/register.asp',
+
+'ËÕÖݹųǺËÐĽÖÆÌ',
+'http://g.163.com/a?CID=1410&Values=1856220854&Redirect=http://suzhou.house.163.com/topic/suzhou/xintiandidd/index.htm'
+
+);
+
+var jilin=new section(
+
+'ÍøÒ×·¿²ú³¤´º³ÏƸ',
+'http://g.163.com/a?CID=1551&Values=1375398756&Redirect=http://cc.house.163.com/news2/101019/13/716002-1.shtml',
+
+'Íò´ïÕÐÉÌÈ«ÃæÆô¶¯',
+'http://g.163.com/a?CID=1552&Values=1562763119&Redirect=http://xm.house.163.com/topic/hz/xmwd/index.html',
+
+'Íò´ï¹«¹Ý ºÀÕ¬µä·¶',
+'http://g.163.com/a?CID=1553&Values=3787022975&Redirect=http://www.wandamansion.com',
+
+'Ô¶Ñóʱ´ú³ÇÐÂÆ·³ö»÷',
+'http://g.163.com/a?CID=1554&Values=1814382010&Redirect=http://www.yysdc.com/',
+
+'ÁìÐã³Ç×¼ÏÖ·¿·¢ÊÛÖÐ',
+'http://g.163.com/a?CID=1555&Values=2362854501&Redirect=http://jn.house.163.com/topic/hz/lnzygy/',
+
+'´óÏóȺרעµØ²ú»¥¶¯',
+'http://g.163.com/a?CID=1556&Values=326828771&Redirect=http://www.daxiangqun.com/',
+
+'ËÕÖݹųǺËÐĽÖÆÌ',
+'http://g.163.com/a?CID=1557&Values=1762785357&Redirect=http://suzhou.house.163.com/topic/suzhou/xintiandidd/index.htm',
+
+'Íò´ïºÀÕ¬ ÔÙÒ«ºÏ·Ê',
+'http://g.163.com/a?CID=1558&Values=3876685191&Redirect=http://hf.house.163.com/topic/hz/hfwdgg2/',
+
+'È¥ÄÏ´÷ºÓ ÁÙº£ÌýÌÎ',
+'http://g.163.com/a?CID=1559&Values=2864408708&Redirect=http://qhd.house.163.com/topic/hz/lhtt0331/',
+
+'Íò¿Æ½ðÓòÀ¶Í彫¿ªÅÌ',
+'http://g.163.com/a?CID=1560&Values=2519885896&Redirect=http://wh.vanke.com/indexad.asp?Title=163sywzl0321&UrlTo=/house/paradiso/register.asp'
+
+);
+
+var liaoning=new section(
+
+'Íò´ï¹«¹Ý ºÀÕ¬µä·¶',
+'http://g.163.com/a?CID=1541&Values=2048003453&Redirect=http://www.wandamansion.com',
+
+'Ô¶Ñóʱ´ú³ÇÐÂÆ·³ö»÷',
+'http://g.163.com/a?CID=1542&Values=540544072&Redirect=http://www.yysdc.com/',
+
+'ÍøÒ×·¿²ú³ÏƸӢ²Å',
+'http://g.163.com/a?CID=1543&Values=2504234005&Redirect=http://sy.house.163.com/news2/110325/13/780322-1.shtml',
+
+'»ª¸®µ¤¿¤¼´½«ÆôÄ»',
+'http://g.163.com/a?CID=1544&Values=4239144350&Redirect=http://xf.house.163.com/sy/0KNQ.html',
+
+'ÉÌÒµµØ²úͶ×ÊÇ÷»ð±¬',
+'http://g.163.com/a?CID=1545&Values=3531852737&Redirect=http://ts.house.163.com//topic/hz/tswd001/index1.html',
+
+'Íò´ïÕÐÉÌÈ«ÃæÆô¶¯',
+'http://g.163.com/a?CID=1546&Values=432142392&Redirect=http://xm.house.163.com/topic/hz/xmwd/index.html',
+
+'ËÕÖݹųǺËÐĽÖÆÌ',
+'http://g.163.com/a?CID=1547&Values=3758417274&Redirect=http://suzhou.house.163.com/topic/suzhou/xintiandidd/index.htm',
+
+'Íò´ïºÀÕ¬ ÔÙÒ«ºÏ·Ê',
+'http://g.163.com/a?CID=1548&Values=1006342877&Redirect=http://hf.house.163.com/topic/hz/hfwdgg2/',
+
+'È¥ÄÏ´÷ºÓ ÁÙº£ÌýÌÎ',
+'http://g.163.com/a?CID=1549&Values=1969115744&Redirect=http://qhd.house.163.com/topic/hz/lhtt0331/',
+
+'Íò¿Æ½ðÓòÀ¶Í彫¿ªÅÌ',
+'http://g.163.com/a?CID=1550&Values=3058321033&Redirect=http://wh.vanke.com/indexad.asp?Title=163sywzl0321&UrlTo=/house/paradiso/register.asp'
+
+);
+
+var neimenggu=new section(
+
+'Íò´ï¹«¹Ý ºÀÕ¬µä·¶',
+'http://g.163.com/a?CID=1571&Values=4261276105&Redirect=http://www.wandamansion.com',
+
+'Íò´ïÕÐÉÌÈ«ÃæÆô¶¯',
+'http://g.163.com/a?CID=1572&Values=3151751761&Redirect=http://xm.house.163.com/topic/hz/xmwd/index.html',
+
+'Ê×´´¹ú¼Ê³ÇÁìÏα±³Ç',
+'http://g.163.com/a?CID=1573&Values=2511114005&Redirect=http://km.house.163.com/topic/xa/scgjcba/',
+
+'ÉÌÒµµØ²úͶ×ÊÇ÷»ð±¬',
+'http://g.163.com/a?CID=1574&Values=627933417&Redirect=http://ts.house.163.com//topic/hz/tswd001/index1.html',
+
+'ÁìÐã³Ç×¼ÏÖ·¿·¢ÊÛÖÐ',
+'http://g.163.com/a?CID=1575&Values=1216993560&Redirect=http://jn.house.163.com/topic/hz/lnzygy/',
+
+'Íò¿Æ½ðÓòÀ¶Í彫¿ªÅÌ',
+'http://g.163.com/a?CID=1576&Values=553663321&Redirect=http://wh.vanke.com/indexad.asp?Title=163sywzl0321&UrlTo=/house/paradiso/register.asp',
+
+'Íò´ïÖÐÐÄÉÌÎñÇøÆô¶¯',
+'http://g.163.com/a?CID=1577&Values=357376222&Redirect=http://nj.house.163.com/topic/hz/njwd/',
+
+'Íò´ïºÀÕ¬ ÔÙÒ«ºÏ·Ê',
+'http://g.163.com/a?CID=1578&Values=759167894&Redirect=http://hf.house.163.com/topic/hz/hfwdgg2/',
+
+'פ¾©°ìÊ´¦Ñ¡Ö·´óÈ«',
+'http://g.163.com/a?CID=1579&Values=1481138169&Redirect=http://beijing.chineseoffice.com.cn',
+
+'¶þÊÖ·¿×â·¿--·¿ÀÏ´ó',
+'http://g.163.com/a?CID=1580&Values=4211744630&Redirect=http://www.foloda.com'
+
+);
+
+var ningxia=new section(
+
+'Íò´ï¹«¹Ý ºÀÕ¬µä·¶',
+'http://g.163.com/a?CID=1601&Values=600487394&Redirect=http://www.wandamansion.com',
+
+'Íò´ïÕÐÉÌÈ«ÃæÆô¶¯',
+'http://g.163.com/a?CID=1602&Values=4056996756&Redirect=http://xm.house.163.com/topic/hz/xmwd/index.html',
+
+'Ê×´´¹ú¼Ê³ÇÁìÏα±³Ç',
+'http://g.163.com/a?CID=1603&Values=3107658362&Redirect=http://km.house.163.com/topic/xa/scgjcba/',
+
+'ÉÌÒµµØ²úͶ×ÊÇ÷»ð±¬',
+'http://g.163.com/a?CID=1604&Values=21683722&Redirect=http://ts.house.163.com//topic/hz/tswd001/index1.html',
+
+'ÁìÐã³Ç×¼ÏÖ·¿·¢ÊÛÖÐ',
+'http://g.163.com/a?CID=1605&Values=152826465&Redirect=http://jn.house.163.com/topic/hz/lnzygy/',
+
+'Íò¿Æ½ðÓòÀ¶Í彫¿ªÅÌ',
+'http://g.163.com/a?CID=1606&Values=2002593813&Redirect=http://wh.vanke.com/indexad.asp?Title=163sywzl0321&UrlTo=/house/paradiso/register.asp',
+
+'Íò´ïÖÐÐÄÉÌÎñÇøÆô¶¯',
+'http://g.163.com/a?CID=1607&Values=1061836462&Redirect=http://nj.house.163.com/topic/hz/njwd/',
+
+'Íò´ïºÀÕ¬ ÔÙÒ«ºÏ·Ê',
+'http://g.163.com/a?CID=1608&Values=1151735200&Redirect=http://hf.house.163.com/topic/hz/hfwdgg2/',
+
+'פ¾©°ìÊ´¦Ñ¡Ö·´óÈ«',
+'http://g.163.com/a?CID=1609&Values=2910377874&Redirect=http://beijing.chineseoffice.com.cn',
+
+'¶þÊÖ·¿×â·¿--·¿ÀÏ´ó',
+'http://g.163.com/a?CID=1610&Values=3427746272&Redirect=http://www.foloda.com'
+
+);
+
+var qinghai=new section(
+
+'Íò´ï¹«¹Ý ºÀÕ¬µä·¶',
+'http://g.163.com/a?CID=1611&Values=1125876318&Redirect=http://www.wandamansion.com',
+
+'Íò´ïÕÐÉÌÈ«ÃæÆô¶¯',
+'http://g.163.com/a?CID=1612&Values=422600227&Redirect=http://xm.house.163.com/topic/hz/xmwd/index.html',
+
+'Ê×´´¹ú¼Ê³ÇÁìÏα±³Ç',
+'http://g.163.com/a?CID=1613&Values=2302037667&Redirect=http://km.house.163.com/topic/xa/scgjcba/',
+
+'ÉÌÒµµØ²úͶ×ÊÇ÷»ð±¬',
+'http://g.163.com/a?CID=1614&Values=3372736983&Redirect=http://ts.house.163.com//topic/hz/tswd001/index1.html',
+
+'ÁìÐã³Ç×¼ÏÖ·¿·¢ÊÛÖÐ',
+'http://g.163.com/a?CID=1615&Values=1038548139&Redirect=http://jn.house.163.com/topic/hz/lnzygy/',
+
+'Íò¿Æ½ðÓòÀ¶Í彫¿ªÅÌ',
+'http://g.163.com/a?CID=1616&Values=2301270961&Redirect=http://wh.vanke.com/indexad.asp?Title=163sywzl0321&UrlTo=/house/paradiso/register.asp',
+
+'Íò´ïÖÐÐÄÉÌÎñÇøÆô¶¯',
+'http://g.163.com/a?CID=1617&Values=2904508348&Redirect=http://nj.house.163.com/topic/hz/njwd/',
+
+'Íò´ïºÀÕ¬ ÔÙÒ«ºÏ·Ê',
+'http://g.163.com/a?CID=1618&Values=410602887&Redirect=http://hf.house.163.com/topic/hz/hfwdgg2/',
+
+'פ¾©°ìÊ´¦Ñ¡Ö·´óÈ«',
+'http://g.163.com/a?CID=1619&Values=4248627044&Redirect=http://beijing.chineseoffice.com.cn',
+
+'¶þÊÖ·¿×â·¿--·¿ÀÏ´ó',
+'http://g.163.com/a?CID=1620&Values=1258697523&Redirect=http://www.foloda.com'
+
+);
+
+var qita=new section(
+
+'΢²©ÊÖ»ú¿Í»§¶Ë·¢²¼',
+'http://t.163.com/mobile',
+
+'ÊÖ»úËæʱÊÕ·¢Óʼþ',
+'http://mail.blog.163.com/blog/static/822094242010829103528389/',
+
+'ÍøÒ×2010ÄêÖղ߻®',
+'http://news.163.com/special/2010ending/',
+
+'ÓÊÏä13ÄêÔ¼»á°Éר³¡',
+'http://mail.blog.163.com/blog/static/822094242010112823415891/',
+
+'163/126¼æÈÝiPhone',
+'http://help.163.com/special/007525G0/163mail_guide.html?id=2716',
+
+'Íø¾Û°®µÄÁ¦Á¿°ïº¢×Ó',
+'http://gongyi.163.com/love365?mailsignresult=-1',
+
+'¿ìÀ´ÁìÏã¸ÛË«·ÉÓÎ',
+'http://quan.123.163.com/?from=163wenzilian',
+
+'ÊÖ»ú¿´¹ÉƱÿÈÕÕÇÍ£',
+'http://help.3g.163.com/stock/',
+
+'ÍøÒ×аæÊÖ»úÓÊÈí¼þ',
+'http://m.123.163.com/?sjysc1108',
+
+'½áÊøµ¥Éí±Ø±¸Èí¼þ',
+'http://bafang.163.com/'
+
+);
+
+var shan3xi=new section(
+
+'Ê×´´¹ú¼Ê³ÇÁìÏα±³Ç',
+'http://g.163.com/a?CID=1461&Values=2136197145&Redirect=http://km.house.163.com/topic/xa/scgjcba/',
+
+'ÍøÒ×·¿²úÎ÷°²ÕÐƸ',
+'http://g.163.com/a?CID=1462&Values=3316917425&Redirect=http://xa.house.163.com/topic/hz/xazp/',
+
+'×ðÏíÎ÷°²ºþ¾ÓÉú»î',
+'http://g.163.com/a?CID=1463&Values=149345275&Redirect=http://km.house.163.com/topic/xa/scgjcba/',
+
+'2011´º¼¾Î÷°²×¡²©»á',
+'http://g.163.com/a?CID=1464&Values=3712988329&Redirect=http://xa.house.163.com/news2/110118/8/753783-1.shtml',
+
+'Î÷°²ÈȵºÂò·¿Õýµ±Ê±',
+'http://g.163.com/a?CID=1465&Values=697392130&Redirect=http://xa.house.163.com/topic/xa/gxzt/',
+
+'´óÏóȺרעµØ²ú»¥¶¯',
+'http://g.163.com/a?CID=1466&Values=238246322&Redirect=http://www.daxiangqun.com/',
+
+'Íò´ïÖÐÐÄÉÌÎñÇøÆô¶¯',
+'http://g.163.com/a?CID=1467&Values=3040981522&Redirect=http://nj.house.163.com/topic/hz/njwd/',
+
+'Íò´ïºÀÕ¬ ÔÙÒ«ºÏ·Ê',
+'http://g.163.com/a?CID=1468&Values=3374925437&Redirect=http://hf.house.163.com/topic/hz/hfwdgg2/',
+
+'¹ú¼ÊÍò´ï£¬ÔìÐĽ­Òõ',
+'http://g.163.com/a?CID=1469&Values=169976250&Redirect=http://wx.house.163.com/topic/hz/jywd/',
+
+'´ó¶¼ÊÐÇ×ˮʫÒâÉú»î',
+'http://g.163.com/a?CID=1470&Values=793011485&Redirect=http://xa.house.163.com/topic/hz/zsej/'
+
+);
+
+var shandong=new section(
+
+'½üÍò¸»ºÀÆë¾ÛÈýÑÇ',
+'http://g.163.com/a?CID=1441&Values=1689407094&Redirect=http://house.qingdaonews.com/content/2011-03/23/content_8710368.htm',
+
+'Ϋ·»Ãâ·Ñ¿´·¿',
+'http://g.163.com/a?CID=1442&Values=988792884&Redirect=http://house.weifang.hiao.com/content/2011-03/03/content_8685275.htm',
+
+'Íò´ïÖÐÐÄÉÌÎñÇøÆô¶¯',
+'http://g.163.com/a?CID=1443&Values=2706386599&Redirect=http://nj.house.163.com/topic/hz/njwd/',
+
+'ÉÌÒµµØ²úͶ×ÊÇ÷»ð±¬',
+'http://g.163.com/a?CID=1444&Values=3392035014&Redirect=http://ts.house.163.com//topic/hz/tswd001/index1.html',
+
+'ɹÃÎÏëӮǧԪ´ó½±',
+'http://g.163.com/a?CID=1445&Values=2614083527&Redirect=http://i.hiao.com/hd/index190.html',
+
+'Íò´ïÕÐÉÌÈ«ÃæÆô¶¯',
+'http://g.163.com/a?CID=1446&Values=436911461&Redirect=http://xm.house.163.com/topic/hz/xmwd/index.html',
+
+'ÁìÐã³Ç×¼ÏÖ·¿·¢ÊÛÖÐ',
+'http://g.163.com/a?CID=1447&Values=1215367967&Redirect=http://jn.house.163.com/topic/hz/lnzygy/',
+
+'Çൺ¥ÊÐ315',
+'http://g.163.com/a?CID=1448&Values=112924560&Redirect=http://house.qingdaonews.com/node/node_42308.htm',
+
+'³ÇÊйۺ£±ðÊû',
+'http://g.163.com/a?CID=1449&Values=2667970567&Redirect=http://house.qingdaonews.com/gb/content/2011-03/15/content_8699694.htm',
+
+'Íþº£Ãâ·Ñ¿´·¿',
+'http://g.163.com/a?CID=1450&Values=1035911948&Redirect=http://house.weihai.hiao.com/node/node_39337.htm'
+
+);
+
+var shanghai=new section(
+
+'ÄÚ»·ÅÔ¼õ8Íò-10Íò',
+'http://g.163.com/a?CID=1381&Values=3418458884&Redirect=http://163.foloda.com/topic/sh/ycgg100913/',
+
+'ÕÐÉÌÍò¿Æ ÙÜɽ´óÖø',
+'http://g.163.com/a?CID=1382&Values=1445599854&Redirect=http://163.foloda.com/topic/sh/zsssly1231/',
+
+'ÖÐÓ¥ºÚÉ­Á־Ƶ깫Ԣ',
+'http://g.163.com/a?CID=1383&Values=1157074672&Redirect=http://163.foloda.com/topic/sh/hsl/',
+
+'ÐÇÔ¹ú¼ÊÉÌÎñÆì½¢',
+'http://g.163.com/a?CID=1384&Values=1190500672&Redirect=http://163.foloda.com/topic/sh/xygj313/',
+
+'ÒÕÊõÆ·¼øÀ¿´äÔ·',
+'http://g.163.com/a?CID=1385&Values=2729506797&Redirect=http://myforest-lcy.com/',
+
+'Öл·±ÌÔƾ«×°·¿78Íò',
+'http://g.163.com/a?CID=1386&Values=1105865289&Redirect=http://163.foloda.com/topic/sh/yzlj1129/',
+
+'סլÉý¼¶ ÇÄÈ»¶øÖÁ',
+'http://g.163.com/a?CID=1387&Values=2851867226&Redirect=http://kunshan.house.163.com/news2/110407/3/785623-1.shtml',
+
+'¹ú¼ÊÍò´ï£¬ÔìÐĽ­Òõ',
+'http://g.163.com/a?CID=1388&Values=1103941632&Redirect=http://wx.house.163.com/topic/hz/jywd/',
+
+'ÁúºþºÃÍûɽµÇ¶¥ÙÜɽ',
+'http://g.163.com/a?CID=1389&Values=2072858580&Redirect=http://163.foloda.com/topic/sh/longhu323/',
+
+'ºçÇÅÉÌ°ì14800Ôª/©O',
+'http://g.163.com/a?CID=1390&Values=2853946711&Redirect=http://163.foloda.com/topic/sh/bswd316/'
+
+);
+
+var shanxi=new section(
+
+'ÍøÒ×·¿²úÌ«Ô­³ÏƸ',
+'http://g.163.com/a?CID=1451&Values=2703525003&Redirect=http://ty.house.163.com/news2/101019/1/715996-1.shtml',
+
+'Íò´ïÕÐÉÌÈ«ÃæÆô¶¯',
+'http://g.163.com/a?CID=1452&Values=1268981401&Redirect=http://xm.house.163.com/topic/hz/xmwd/index.html',
+
+'ÁìÐã³Ç×¼ÏÖ·¿·¢ÊÛÖÐ',
+'http://g.163.com/a?CID=1453&Values=1884424223&Redirect=http://jn.house.163.com/topic/hz/lnzygy/',
+
+'ÉÌÒµµØ²úͶ×ÊÇ÷»ð±¬',
+'http://g.163.com/a?CID=1454&Values=2833138962&Redirect=http://ts.house.163.com//topic/hz/tswd001/index1.html',
+
+'Íò´ïÖÐÐÄÉÌÎñÇøÆô¶¯',
+'http://g.163.com/a?CID=1455&Values=258898259&Redirect=http://nj.house.163.com/topic/hz/njwd/',
+
+'Ê׸¶15ÍòÍò¿Æ×°ÐÞ·¿',
+'http://g.163.com/a?CID=1456&Values=4253558500&Redirect=http://wh.vanke.com/indexad.asp?Title=0221163wzl&UrlTo=/house/goldencity/register.asp',
+
+'ËÕÖݹųǺËÐĽÖÆÌ',
+'http://g.163.com/a?CID=1457&Values=4069211149&Redirect=http://suzhou.house.163.com/topic/suzhou/xintiandidd/index.htm',
+
+'Íò´ïºÀÕ¬ ÔÙÒ«ºÏ·Ê',
+'http://g.163.com/a?CID=1458&Values=2510006128&Redirect=http://hf.house.163.com/topic/hz/hfwdgg2/',
+
+'פ¾©°ìÊ´¦Ñ¡Ö·´óÈ«',
+'http://g.163.com/a?CID=1459&Values=1167961316&Redirect=http://www.chineseoffice.com.cn/',
+
+'Íò¿Æ½ðÓòÀ¶Í彫¿ªÅÌ',
+'http://g.163.com/a?CID=1460&Values=1466984727&Redirect=http://wh.vanke.com/indexad.asp?Title=163sywzl0321&UrlTo=/house/paradiso/register.asp'
+
+);
+
+var tianjin=new section(
+
+'ÍøÒ×Ìì½òÕ¾ÕÐƸ',
+'http://g.163.com/a?CID=1361&Values=2929173111&Redirect=http://tj.house.163.com/news2/101019/1/715997-1.shtml',
+
+'¹ú¼ÊÍò´ï£¬ÔìÐĽ­Òõ',
+'http://g.163.com/a?CID=1362&Values=3777304491&Redirect=http://wx.house.163.com/topic/hz/jywd/',
+
+'Ê׸¶15ÍòÍò¿Æ×°ÐÞ·¿',
+'http://g.163.com/a?CID=1363&Values=3440031390&Redirect=http://wh.vanke.com/indexad.asp?Title=0221163wzl&UrlTo=/house/goldencity/register.asp',
+
+'ÉÌÒµµØ²úͶ×ÊÇ÷»ð±¬',
+'http://g.163.com/a?CID=1364&Values=929124833&Redirect=http://ts.house.163.com//topic/hz/tswd001/index1.html',
+
+'ÁìÐã³Ç×¼ÏÖ·¿·¢ÊÛÖÐ',
+'http://g.163.com/a?CID=1365&Values=90205709&Redirect=http://jn.house.163.com/topic/hz/lnzygy/',
+
+'Íò´ïºÀÕ¬ ÔÙÒ«ºÏ·Ê',
+'http://g.163.com/a?CID=1366&Values=3689197257&Redirect=http://hf.house.163.com/topic/hz/hfwdgg2/',
+
+'Íò´ïÖÐÐÄÉÌÎñÇøÆô¶¯',
+'http://g.163.com/a?CID=1367&Values=1968599507&Redirect=http://nj.house.163.com/topic/hz/njwd/',
+
+'ËÕÖݹųǺËÐĽÖÆÌ',
+'http://g.163.com/a?CID=1368&Values=810850245&Redirect=http://suzhou.house.163.com/topic/suzhou/xintiandidd/index.htm',
+
+'פ¾©°ìÊ´¦Ñ¡Ö·´óÈ«',
+'http://g.163.com/a?CID=1369&Values=1611213978&Redirect=http://beijing.chineseoffice.com.cn',
+
+'¶þÊÖ·¿×â·¿--·¿ÀÏ´ó',
+'http://g.163.com/a?CID=1370&Values=860811041&Redirect=http://www.foloda.com'
+
+);
+
+var xinjiang=new section(
+
+'Íò´ï¹«¹Ý ºÀÕ¬µä·¶',
+'http://g.163.com/a?CID=1581&Values=3365491210&Redirect=http://www.wandamansion.com',
+
+'Íò´ïÕÐÉÌÈ«ÃæÆô¶¯',
+'http://g.163.com/a?CID=1582&Values=336470330&Redirect=http://xm.house.163.com/topic/hz/xmwd/index.html',
+
+'Ê×´´¹ú¼Ê³ÇÁìÏα±³Ç',
+'http://g.163.com/a?CID=1583&Values=3472861197&Redirect=http://km.house.163.com/topic/xa/scgjcba/',
+
+'ÉÌÒµµØ²úͶ×ÊÇ÷»ð±¬',
+'http://g.163.com/a?CID=1584&Values=1804131147&Redirect=http://ts.house.163.com//topic/hz/tswd001/index1.html',
+
+'ÁìÐã³Ç×¼ÏÖ·¿·¢ÊÛÖÐ',
+'http://g.163.com/a?CID=1585&Values=3609375102&Redirect=http://jn.house.163.com/topic/hz/lnzygy/',
+
+'Íò¿Æ½ðÓòÀ¶Í彫¿ªÅÌ',
+'http://g.163.com/a?CID=1586&Values=993300697&Redirect=http://wh.vanke.com/indexad.asp?Title=163sywzl0321&UrlTo=/house/paradiso/register.asp',
+
+'Íò´ïÖÐÐÄÉÌÎñÇøÆô¶¯',
+'http://g.163.com/a?CID=1587&Values=460987308&Redirect=http://nj.house.163.com/topic/hz/njwd/',
+
+'Íò´ïºÀÕ¬ ÔÙÒ«ºÏ·Ê',
+'http://g.163.com/a?CID=1588&Values=1431906569&Redirect=http://hf.house.163.com/topic/hz/hfwdgg2/',
+
+'פ¾©°ìÊ´¦Ñ¡Ö·´óÈ«',
+'http://g.163.com/a?CID=1589&Values=956197227&Redirect=http://beijing.chineseoffice.com.cn',
+
+'¶þÊÖ·¿×â·¿--·¿ÀÏ´ó',
+'http://g.163.com/a?CID=1590&Values=4241274752&Redirect=http://www.foloda.com'
+
+);
+
+var xizang=new section(
+
+'Íò´ï¹«¹Ý ºÀÕ¬µä·¶',
+'http://g.163.com/a?CID=1621&Values=1130191017&Redirect=http://www.wandamansion.com',
+
+'Íò´ïÕÐÉÌÈ«ÃæÆô¶¯',
+'http://g.163.com/a?CID=1622&Values=1292665924&Redirect=http://xm.house.163.com/topic/hz/xmwd/index.html',
+
+'Ê×´´¹ú¼Ê³ÇÁìÏα±³Ç',
+'http://g.163.com/a?CID=1623&Values=2757938531&Redirect=http://km.house.163.com/topic/xa/scgjcba/',
+
+'ÉÌÒµµØ²úͶ×ÊÇ÷»ð±¬',
+'http://g.163.com/a?CID=1624&Values=1197009682&Redirect=http://ts.house.163.com//topic/hz/tswd001/index1.html',
+
+'ÁìÐã³Ç×¼ÏÖ·¿·¢ÊÛÖÐ',
+'http://g.163.com/a?CID=1625&Values=3326422532&Redirect=http://jn.house.163.com/topic/hz/lnzygy/',
+
+'Íò¿Æ½ðÓòÀ¶Í彫¿ªÅÌ',
+'http://g.163.com/a?CID=1626&Values=2856965099&Redirect=http://wh.vanke.com/indexad.asp?Title=163sywzl0321&UrlTo=/house/paradiso/register.asp',
+
+'Íò´ïÖÐÐÄÉÌÎñÇøÆô¶¯',
+'http://g.163.com/a?CID=1627&Values=3569650124&Redirect=http://nj.house.163.com/topic/hz/njwd/',
+
+'Íò´ïºÀÕ¬ ÔÙÒ«ºÏ·Ê',
+'http://g.163.com/a?CID=1628&Values=2300324860&Redirect=http://hf.house.163.com/topic/hz/hfwdgg2/',
+
+'פ¾©°ìÊ´¦Ñ¡Ö·´óÈ«',
+'http://g.163.com/a?CID=1629&Values=3612030918&Redirect=http://beijing.chineseoffice.com.cn',
+
+'¶þÊÖ·¿×â·¿--·¿ÀÏ´ó',
+'http://g.163.com/a?CID=1630&Values=3299402376&Redirect=http://www.foloda.com'
+
+);
+
+var yunnan=new section(
+
+'ÍøÒ×·¿²úÀ¥Ã÷³ÏƸ',
+'http://g.163.com/a?CID=1521&Values=631016055&Redirect=http://km.house.163.com/news2/101019/15/716000-1.shtml',
+
+'¹ú¼ÊÍò´ï£¬ÔìÐĽ­Òõ',
+'http://g.163.com/a?CID=1522&Values=1485880991&Redirect=http://wx.house.163.com/topic/hz/jywd/',
+
+'ÉÌÒµµØ²úͶ×ÊÇ÷»ð±¬',
+'http://g.163.com/a?CID=1523&Values=2669915245&Redirect=http://ts.house.163.com//topic/hz/tswd001/index1.html',
+
+'Àì½­À¶ÍåÁëÓòµ½À¶Íå',
+'http://g.163.com/a?CID=1524&Values=2351486230&Redirect=http://www.v9666.com',
+
+'°ëɽ»ª¸®2ÆÚ½«ÆôÄ»',
+'http://g.163.com/a?CID=1525&Values=1629509434&Redirect=http://cq.house.163.com/topic/cq/bshf110401/index.html',
+
+'÷ÈÁ¦Ö®³ÇÉý¼¶°æ£¡',
+'http://g.163.com/a?CID=1526&Values=497803574&Redirect=http://wh.vanke.com/indexad.asp?Title=wangyiyouxiang&UrlTo=/house/usonian/register.asp',
+
+'Íò¿Æ½ðÓòÀ¶Í彫¿ªÅÌ',
+'http://g.163.com/a?CID=1527&Values=3247905635&Redirect=http://wh.vanke.com/indexad.asp?Title=163sywzl0321&UrlTo=/house/paradiso/register.asp',
+
+'±±Â´Ô­¿ªÅÌÈÈÏúÖÐ',
+'http://g.163.com/a?CID=1528&Values=1569929022&Redirect=http://cq.house.163.com/topic/cq/zybly110314/index.html',
+
+'פ¾©°ìÊ´¦Ñ¡Ö·´óÈ«',
+'http://g.163.com/a?CID=1529&Values=533233735&Redirect=http://beijing.chineseoffice.com.cn',
+
+'¶þÊÖ·¿×â·¿--·¿ÀÏ´ó',
+'http://g.163.com/a?CID=1530&Values=2870090898&Redirect=http://www.foloda.com'
+
+);
+
+var zhejiang=new section(
+
+'ÕãÉ̲Ƹ»ÖÐÐÄÔ¤¿ªÅÌ',
+'http://g.163.com/a?CID=1411&Values=3567176511&Redirect=http://hz.house.163.com/topic/nb/morect/index.html',
+
+'Íò´ïÕÐÉÌÈ«ÃæÆô¶¯',
+'http://g.163.com/a?CID=1412&Values=1748498680&Redirect=http://xm.house.163.com/topic/hz/xmwd/index.html',
+
+'±ÈÈËÆøÓ®IPHONE4£¡',
+'http://g.163.com/a?CID=1413&Values=3953215278&Redirect=http://www.xici.net/d143476647.htm',
+
+'ÉÌÒµµØ²úͶ×ÊÇ÷»ð±¬',
+'http://g.163.com/a?CID=1414&Values=958729281&Redirect=http://ts.house.163.com//topic/hz/tswd001/index1.html',
+
+'ÁìÐã³Ç×¼ÏÖ·¿·¢ÊÛÖÐ',
+'http://g.163.com/a?CID=1415&Values=2306815090&Redirect=http://jn.house.163.com/topic/hz/lnzygy/',
+
+'¹ú¼ÊÍò´ï£¬ÔìÐĽ­Òõ',
+'http://g.163.com/a?CID=1416&Values=3583016750&Redirect=http://wx.house.163.com/topic/hz/jywd/',
+
+'Íò´ïÖÐÐÄÉÌÎñÇøÆô¶¯',
+'http://g.163.com/a?CID=1417&Values=1066762875&Redirect=http://nj.house.163.com/topic/hz/njwd/',
+
+'Íò´ïºÀÕ¬ ÔÙÒ«ºÏ·Ê',
+'http://g.163.com/a?CID=1418&Values=3834533750&Redirect=http://hf.house.163.com/topic/hz/hfwdgg2/',
+
+'ËÕÖݹųǺËÐĽÖÆÌ',
+'http://g.163.com/a?CID=1419&Values=2659532313&Redirect=http://suzhou.house.163.com/topic/suzhou/xintiandidd/index.htm',
+
+'Ê×´´¹ú¼Ê³ÇÁìÏα±³Ç',
+'http://g.163.com/a?CID=1420&Values=3110216503&Redirect=http://km.house.163.com/topic/xa/scgjcba/'
+
+);
+
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/rpic/fld3/flsclasses.js b/mobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/rpic/fld3/flsclasses.js
new file mode 100755
index 0000000000..e3e5d8082f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/rpic/fld3/flsclasses.js
@@ -0,0 +1,30 @@
+//ÀàÄ£°å
+function section(a1,a2,b1,b2,c1,c2,d1,d2,e1,e2,f1,f2,g1,g2,h1,h2,k1,k2,p1,p2){
+ this.string0=a1; this.link0=a2;
+ this.string1=b1; this.link1=b2;
+ this.string2=c1; this.link2=c2;
+ this.string3=d1; this.link3=d2;
+ this.string4=e1; this.link4=e2;
+ this.string5=f1; this.link5=f2;
+ this.string6=g1; this.link6=g2;
+ this.string7=h1; this.link7=h2;
+ this.string8=k1; this.link8=k2;
+ this.string9=p1; this.link9=p2;
+}
+//Êä³öº¯Êý
+function echoa(clicks){
+ //Êä³öÎÄ×Ö
+ void('<a href="' + prov.link0 + '" target="_blank">' + prov.string0 + '</a>');
+ void('<a href="' + prov.link1 + '" target="_blank">' + prov.string1 + '</a>');
+ void('<a href="' + prov.link2 + '" target="_blank">' + prov.string2 + '</a>');
+ void('<a href="' + prov.link3 + '" target="_blank">' + prov.string3 + '</a>');
+ void('<a href="' + prov.link4 + '" target="_blank">' + prov.string4 + '</a>');
+}
+function echob(clicks){
+ //Êä³öÎÄ×Ö
+ void('<a href="' + prov.link5 + '" target="_blank">' + prov.string5 + '</a>');
+ void('<a href="' + prov.link6 + '" target="_blank">' + prov.string6 + '</a>');
+ void('<a href="' + prov.link7 + '" target="_blank">' + prov.string7 + '</a>');
+ void('<a href="' + prov.link8 + '" target="_blank">' + prov.string8 + '</a>');
+ void('<a href="' + prov.link9 + '" target="_blank">' + prov.string9 + '</a>');
+} \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/techpro/shangpin/20110331/36-65.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/techpro/shangpin/20110331/36-65.jpg
new file mode 100755
index 0000000000..5e63909a81
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/techpro/shangpin/20110331/36-65.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/techpro/tuangou/20110218/170-80.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/techpro/tuangou/20110218/170-80.jpg
new file mode 100755
index 0000000000..b5e7492cf4
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/techpro/tuangou/20110218/170-80.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/yodaoimages/pack.r091221/scripts/autocomplete.163.165290.js b/mobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/yodaoimages/pack.r091221/scripts/autocomplete.163.165290.js
new file mode 100755
index 0000000000..69ad0da6f0
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.126.net/yodaoimages/pack.r091221/scripts/autocomplete.163.165290.js
@@ -0,0 +1 @@
+var SC={create:function(){return function(){this.initialize.apply(this,arguments)}}};function $S(){var _=[],$;for(var A=0;A<arguments.length;A++){$=arguments[A];if(typeof $=="string")$=document.getElementById($);_.push($)}return _.length<2?_[0]:_}function $SA(_){if(!_)return[];if(_.toArray)return _.toArray();else{var $=[];for(var A=0;A<_.length;A++)$.push(_[A]);return $}}Object.sextend=function(A,$){for(var _ in $)A[_]=$[_];return A};Function.prototype.sbind=function(){var $=this,_=$SA(arguments),A=_.shift();return function(){return $.apply(A,_.concat($SA(arguments)))}};Function.prototype.sbAEListener=function(_){var $=this;return function(A){return $.call(_,A||window.event)}};var SElement=new Object();SElement.Methods={visible:function($){return $S($).style.display!="none"},hide:function(){for(var _=0;_<arguments.length;_++){var $=$S(arguments[_]);$.style.display="none"}},show:function(){for(var _=0;_<arguments.length;_++){var $=$S(arguments[_]);$.style.display=""}},getHeight:function($){$=$S($);return $.offsetHeight},addClassName:function($,_){if(!($=$S($)))return;$.className=(""==$.className)?_:($.className+" "+_)},removeClassName:function($,_){if(!($=$S($)))return;var A=new RegExp("(^| )"+_+"( |$)");$.className=$.className.replace(A,"$1").replace(/ $/,"")}};Object.sextend(SElement,SElement.Methods);var SEvent=new Object();SEvent.Methods={element:function($){return $.target||$.srcElement},observers:false,_observeAndCache:function(_,A,B,$){if(!this.observers)this.observers=[];if(_.addEventListener){this.observers.push([_,A,B,$]);_.addEventListener(A,B,$)}else if(_.attachEvent){this.observers.push([_,A,B,$]);_.attachEvent("on"+A,B)}},unloaddisabledCache:function(){if(!SEvent.observers)return;for(var $=0;$<SEvent.observers.length;$++){SEvent.stopObserving.apply(this,SEvent.observers[$]);SEvent.observers[$][0]=null}SEvent.observers=false},observe:function(_,A,B,$){var _=$S(_);$=$||false;if(A=="keypress"&&(navigator.appVersion.match(/Konqueror|Safari|KHTML/)||_.attachEvent))A="keydown";this._observeAndCache(_,A,B,$)},stopObserving:function(_,A,B,$){var _=$S(_);$=$||false;if(A=="keypress"&&(navigator.appVersion.match(/Konqueror|Safari|KHTML/)||_.detachEvent))A="keydown";if(_.removeEventListener)_.removeEventListener(A,B,$);else if(_.detachEvent)_.detachEvent("on"+A,B)}};Object.sextend(SEvent,SEvent.Methods);if(navigator.appVersion.match(/\bMSIE\b/))SEvent.observe(window,"unloaddisabled",SEvent.unloaddisabledCache,false);var SP={cumOffset:function(_){var $=0,A=0;do{$+=_.offsetTop||0;A+=_.offsetLeft||0;_=_.offsetParent}while(_);return[A,$]}},SK=SC.create();Object.sextend(SK,{BACKSPACE:8,TAB:9,RETURN:13,ESC:27,LEFT:37,UP:38,RIGHT:39,DOWN:40,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,INSERT:45,SHIFT:16,CTRL:17,ALT:18});var AutoComplete=SC.create();AutoComplete.prototype={initialize:function(E,_,C,$,A,B,D,F){SEvent.observe(document,"click",this.hide2.sbAEListener(this));SEvent.observe(document,"blur",this.hide2.sbAEListener(this));this.oN=this.defSugName;if(C)this.oN=C;this.IE=(navigator&&navigator.userAgent.toLowerCase().indexOf("msie")!=-1);this.browserInfo();if($)this.hI=true;else this.hI=false;if(A)this.hC=true;else this.hC=false;this.iconUrl=this.defSugIconUrl;if(B)this.iconUrl=B;this.box=$S(E);this.frameBox=$S(_);if(this.box){SEvent.observe(this.box,"keypress",this.onkeydown.sbAEListener(this));this.box.onblur=this.onblur.sbAEListener(this);SEvent.observe(this.box,"dblclick",this.dblClick.sbAEListener(this));if(!this.hI)this.setSugIcon(this.iconUrl)}this.count=0;this.sugServ=this.defSugServ;if(F)this.sugServ=F;this.sugMoreParams="";this.logServ=this.defSugServ;this.searchServ=this.defSearchServ;this.searchParamName=this.defSearchParamName;this.searchMoreParams="";this.kf=this.defKeyfrom+this.KEYFROM_POST;this.hcb=null;this.scb=null;this.sugFlag=true;this.clickEnabled=true;this.sptDiv=document.createElement("div");document.body.appendChild(this.sptDiv);this.sdiv=document.createElement("div");this.sdiv.style.position="absolute";this.sdiv.style.zIndex=10000;SElement.hide(this.sdiv);document.body.appendChild(this.sdiv);this.bdiv=document.createElement("div");this.vis=false;this.lq="";this.initVal="";if(this.box&&this.box.value!="")this.initVal=this.box.value;this.oldVal=this.initVal;this.oldValForCtrlZ=new Array(this.CZNUM);this.oldValForCtrlZNum=0;this.ctrlZFlag=false;this.upDownTag=false;this.blurCount=0;window.onresize=this.winResize.sbind(this);this.doReq("");this.clean();if(!D)this.timeoutId=setTimeout(this.sugReq.sbind(this),this.REQUEST_TIMEOUT)},setInputId:function($){this.box=$S($);if(this.box){SEvent.observe(this.box,"keypress",this.onkeydown.sbAEListener(this));this.box.onblur=this.onblur.sbAEListener(this);SEvent.observe(this.box,"dblclick",this.dblClick.sbAEListener(this));if(!this.hI)this.setSugIcon(this.iconUrl)}},start:function(){this.timeoutId=setTimeout(this.sugReq.sbind(this),this.REQUEST_TIMEOUT);if(this.box&&this.box.value!=""){this.initVal=this.box.value;this.oldVal=this.initVal}},setObjectName:function($){this.oN=$},setSelectCallBack:function($){this.scb=$.sbind(this)},setHoverCallBack:function($){this.hcb=$.sbind(this)},setSugServer:function($){this.sugServ=$;this.logServ=$;this.doReq("");this.clean()},setSugMoreParams:function($){this.sugMoreParams=$},setLogServer:function($){this.logServ=$},setSearchServer:function($){this.searchServ=$},setSearchParamName:function($){this.searchParamName=$},setSearchMoreParams:function($){this.searchMoreParams=$},setKeyFrom:function($){if($.indexOf(this.KEYFROM_POST)>0)this.kf=$;else this.kf=$+this.KEYFROM_POST},getSearchUrl:function($){return encodeURI(this.searchServ+this.searchParamName+"="+$+"&keyfrom="+this.kf+this.searchMoreParams)},getSugQueryUrl:function(_,A,$){return encodeURI(this.sugServ+this.S_QUERY_URL_POST+_+"&o="+this.oN+"&count="+$+"&keyfrom="+this.kf+this.sugMoreParams+this.time())},log:function(D,C,B,_,$){var A="";if(C)A+=C;if(B)A+=B;if(_)A+=_;if($)A+=$;var E=new Image();E.src=encodeURI(this.logServ+this.S_LOG_URL_POST+D+A+this.time());return true},setSugIcon:function(C){var B=this.oN+"_icon";if(document.getElementById(B))document.body.removeChild(document.getElementById(B));this.icon=document.createElement("img");this.icon.id=B;this.icon.src=C;this.icon.style.position="absolute";this.icon.style.zIndex="99";this.icon.style.width="13px";this.icon.style.height="10px";this.icon.style.cursor="pointer";var $=this.frameBox,A=SP.cumOffset($),_=0;this.icon.style.left=(A[0]+_+$.offsetWidth-13*1.5)+"px";this.icon.style.top=(A[1]+($.offsetHeight-10)/2)+"px";this.icon.style.display="";document.body.appendChild(this.icon);SEvent.observe(this.icon,"click",this.pressPoint.sbAEListener(this));SEvent.observe(this.icon,"mouseover",this.onmouseover2.sbAEListener(this));SEvent.observe(this.icon,"mouseout",this.onmouseout2.sbAEListener(this))},dblClick:function(){if(this.box.createTextRange){var $=this.box.createTextRange();$.moveStart("character",0);$.select()}else if(this.box.setSelectionRange)this.box.setSelectionRange(0,this.box.value.length);if(this.sugFlag){if(this.box.value!=""){if(this.lq==this.box.value){if(this.sdiv.childNodes.length>0)if(!this.vis)this.show();else this.hide();return}this.doReq()}}else if(this.box.value!="")this.insertSugHint()},winResize:function(){if(this.vis)this.show();if(!this.hI)this.setSugIcon(this.iconUrl)},storeOldValue:function(){if(this.oldValForCtrlZNum<this.CZNUM){this.oldValForCtrlZ[this.oldValForCtrlZNum]=this.oldVal;this.oldValForCtrlZNum++}else{for(var $=0;$<this.CZNUM-1;++$)this.oldValForCtrlZ[$]=this.oldValForCtrlZ[$+1];this.oldValForCtrlZ[this.CZNUM-1]=this.oldVal}},clearOldValue:function(){this.oldValForCtrlZNum=0},onkeydown:function(_){if(_.ctrlKey){var $=_.keyCode;if($==0)$=_.charCode;if($==90||$==122){if(this.oldValForCtrlZNum>0){this.box.value=this.oldValForCtrlZ[--this.oldValForCtrlZNum];if(this.box.value!="")this.ctrlZFlag=true;else this.oldVal="";this.upDownTag=false}if(this.IE)_.returnValue=false;else _.preventDefault();return false}return true}switch(_.keyCode){case SK.PAGE_UP:case SK.PAGE_DOWN:case SK.END:case SK.HOME:case SK.INSERT:case SK.CTRL:case SK.ALT:case SK.LEFT:case SK.RIGHT:case SK.SHIFT:case SK.TAB:return true;case SK.ESC:this.hide();return false;case SK.UP:if(this.vis&&this.sugFlag){this.upDownTag=true;this.up()}else{if(this.sdiv.childNodes.length>1)if(this.lq==this.box.value)if(this.sugFlag){this.show();return false}if(this.box.value!="")this.doReq()}if(this.IE)_.returnValue=false;else _.preventDefault();return false;case SK.DOWN:if(this.vis&&this.sugFlag){this.upDownTag=true;this.down()}else{if(this.sdiv.childNodes.length>1)if(this.lq==this.box.value)if(this.sugFlag){this.show();return false}if(this.box.value!="")this.doReq()}if(this.IE)_.returnValue=false;else _.preventDefault();return false;case SK.RETURN:if(this.vis&&this.curNodeIdx>-1)if(!this.select()){if(this.IE)_.returnValue=false;else _.preventDefault();return false}return true;case SK.BACKSPACE:if(this.box.value.length==1){this.storeOldValue();this.oldVal=""}default:this.upDownTag=false;return true}},sugReq:function(){if(this.box.value!=""&&this.box.value!=this.initVal){this.initVal="";if(this.lq!=this.box.value)if(!this.upDownTag)if(typeof(isYdDefault)!=undefined){if(!isYdDefault())this.doReq()}else this.doReq()}else if(this.lq!=""){this.lq="";if(this.vis){this.hide();this.clean()}}if(this.timeoutId!=0)clearTimeout(this.timeoutId);this.timeoutId=setTimeout(this.sugReq.sbind(this),this.REQUEST_TIMEOUT)},getSiteResult:function(B){var D=new RegExp("<[s][p][a][n].*>.*</[s][p][a][n]>");m=D.exec(B);if(m==null){D=new RegExp("<[aA].*>.*</[aA]>");m=D.exec(B)}if(m==null){var F=B.indexOf("HREF=\"");if(F!=-1){var E=B.indexOf("\"",F+6),_=B.substring(F+6,E);return _}return null}else{var $=new RegExp("[hH][rR][eE][fF]=.*><[fF][oO][nN][tT]"),C=$.exec(m);if(C[0].length<=13)return null;var A=C[0].split(" "),_;if(A.length>1)_=A[0].substr(6,A[0].length-7);else _=A[0].substr(6,A[0].length-13);return _}},getSelValue:function($){return $.replace(/this.txtBox.value=/,"").replace(/\'/g,"")},select:function($){if($){if(this.getCurNode()){var _=this.getCurNode().innerHTML,A=this.getSiteResult(_);if(A!=null){this.log(this.LOG_MOUSE_SELECT,"&q="+this.oldVal,"&index=0","&select="+A,"&direct=true");this.hide();void(A,"_blank")}else{try{var D=this.getCurNode().getAttribute(this.ITEM_SEL_ATTR_NAME),C=this.getSelValue(D);if(this.oldVal!=C)this.storeOldValue();this.log(this.LOG_MOUSE_SELECT,"&q="+this.oldVal,"&index="+this.curNodeIdx,"&select="+C);this.oldVal=C;this.hide();if(this.scb!=null)this.scb(C,this);else{this.clearOldValue();var B=this.getSearchUrl(C);void(B,"_blank")}}catch($){}}}}else if(this.getCurNode()){_=this.getCurNode().innerHTML,A=this.getSiteResult(_);if(A!=null){this.log(this.LOG_KEY_SELECT,"&q="+this.oldVal,"&index=0","&select="+A,"&direct=true");this.hide();void(A,"_blank")}else{try{D=this.getCurNode().getAttribute(this.ITEM_SEL_ATTR_NAME),C=this.getSelValue(D);if(this.oldVal!=C)this.storeOldValue();if(this.box.value!=C)return true;else this.log(this.LOG_KEY_SELECT,"&q="+this.oldVal,"&index="+this.curNodeIdx,"&select="+C);this.oldVal=C;this.hide();if(this.scb!=null)this.scb(this.box.value,this);else{this.clearOldValue();B=this.getSearchUrl(C);void(B,"_blank")}}catch($){}}}return false},doReq:function($){if(!this.sugFlag)return;if($=="undefined"||$==null){if(this.oldVal!=this.box.value&&!this.ctrlZFlag)this.storeOldValue();this.oldVal=this.box.value;this.ctrlZFlag=false;this.lq=this.box.value;var $=this.box.value}this.count++;var A=encodeURIComponent(document.URL),_=this.getSugQueryUrl($,A,this.count);this.excuteCall(_)},clean:function(){this.size=0;this.curNodeIdx=-1;this.sdiv.innerHTML="";this.bdiv.innerHTML=""},onComplete:function(){setTimeout(this.updateContent.sbind(this,arguments[0]),5)},cleanScript:function(){while(this.sptDiv.childNodes.length>0)this.sptDiv.removeChild(this.sptDiv.firstChild)},isValidNode:function($){return($.nodeType==1)&&($.getAttribute(this.ITEM_SEL_ATTR_NAME))},getReqStr:function($){if($&&$.getElementsByTagName("div").length>0)return $.getElementsByTagName("div")[0].getAttribute("id");return null},updateContent:function(){this.cleanScript();var C=this.box.value;if(this.bdiv.innerHTML=="")if(this.sdiv.innerHTML!=""&&this.getReqStr(this.sdiv)==C)return;else{this.hide();this.clean();return}if(this.getReqStr(this.bdiv)!=C)if(this.sdiv.innerHTML!=""&&this.getReqStr(this.sdiv)==C)return;else{this.hide();return}var A,_=false,B=(((this.bdiv.getElementsByTagName("table"))[1]).getElementsByTagName("tr"));for(var D=0;D<B.length;D++){A=B[D];if(this.isValidNode(A)){_=true;break}}if(_){this.sdiv.innerHTML=this.bdiv.innerHTML;var $=this.sdiv.getElementsByTagName("table");B=$[1].getElementsByTagName("tr");this.size=0;this.childs=new Array();for(D=0;D<B.length;D++){A=B[D];if(this.isValidNode(A)){A.setAttribute(this.ITEM_INDEX_ATTR_NAME,this.size);SEvent.observe(A,"mousemove",this.onmousemove.sbAEListener(this));SEvent.observe(A,"mouseover",this.onmouseover.sbAEListener(this));SEvent.observe(A,"mouseout",this.onmouseout.sbAEListener(this));SEvent.observe(A,"click",this.select.sbAEListener(this));this.childs.push(A);this.size++}}if(Number($.length)>=3)this.bindATagWithMouseEvent($[2],false);this.show();this.mouseTag=false}else{this.hide();this.clean()}},showContent:function(){var $=this.frameBox,A=SP.cumOffset($),B=0;this.sdiv.style.top=A[1]+($.offsetHeight+B)+"px";var _=0;if(this.bName=="IE");else _=1;this.sdiv.style.left=A[0]+_+"px";this.sdiv.style.cursor="default";this.sdiv.style.width=$.offsetWidth+"px";SElement.show(this.sdiv);this.vis=true;this.curNodeIdx=-1},show:function(){if(this.sdiv.childNodes.length<1)return;if(this.sugFlag)if(this.getReqStr(this.sdiv)!=this.box.value)return;this.showContent()},hide:function(){this.hlOff();SElement.hide(this.sdiv);this.curNodeIdx=-1;this.vis=false},hide2:function(){if(this.clickEnabled){this.hide();this.clickEnabled=false;setTimeout(this.enableClick.sbind(this),60)}},onblur:function(){this.hide();var $=true;if(this.IE)if(this.blurCount==0){$=false;this.blurCount++}if(typeof(ydInputBlur)!=undefined&&$)ydInputBlur(this.box)},enableClick:function(){this.clickEnabled=true},onmousemove:function($){this.mouseTag=true;this.onmouseover($)},onmouseover:function(_){this.box.onblur=null;if(!this.mouseTag){this.mouseTag=true;return}var A=SEvent.element(_);while(A.parentNode&&(!A.tagName||(A.getAttribute(this.ITEM_INDEX_ATTR_NAME)==null)))A=A.parentNode;var $=(A.tagName)?A.getAttribute(this.ITEM_INDEX_ATTR_NAME):-1;if($==-1||$==this.curNodeIdx)return;this.hlOff();this.curNodeIdx=Number($);this.hlOn(false)},onmouseout:function(){this.hlOff();this.curNodeIdx=-1;this.box.onblur=this.onblur.sbAEListener(this)},getNode:function($){if(this.childs&&($>=0&&$<this.childs.length))return this.childs[$];else return undefined},getCurNode:function(){return this.getNode(this.curNodeIdx)},hover:function($,_){if(this.hcb!=null)this.hcb($,_,this);else if(!$)this.box.value=_},hlOn:function(_){if(this.getCurNode()){var B=this.getCurNode().getElementsByTagName("td");this.procInstantResult();for(var C=0;C<B.length;++C)SElement.addClassName(B[C],this.ITEM_HIGHLIGHT_STYLE);try{var A=this.getCurNode().getAttribute(this.ITEM_SEL_ATTR_NAME);this.hover(!_,this.getSelValue(A))}catch($){}}},hlOff:function(){if(this.getCurNode()){var $=this.getCurNode().getElementsByTagName("td");for(var _=0;_<$.length;++_)SElement.removeClassName($[_],this.ITEM_HIGHLIGHT_STYLE);this.procInstantResultBack()}},procInstantResult:function(){var _=this.getCurNode().innerHTML;if(_.indexOf("red_font")==-1)return;var $=document.getElementById("red_font");if($)$.setAttribute("color","#ffffff");$=document.getElementById("gray_font");if($)$.setAttribute("color","#ffffff")},procInstantResultBack:function(){var _=this.getCurNode().innerHTML;if(_.indexOf("red_font")==-1)return;var $=document.getElementById("red_font");if($)$.setAttribute("color","red");$=document.getElementById("gray_font");if($)$.setAttribute("color","#008000")},up:function(){var $=this.curNodeIdx;if(this.curNodeIdx>0){this.hlOff();this.curNodeIdx=$-1;this.hlOn(true)}else if(this.curNodeIdx==0){this.hlOff();this.curNodeIdx=$-1;this.box.value=this.oldVal}else{this.curNodeIdx=this.size-1;this.hlOn(true)}},down:function(){var $=this.curNodeIdx;if(this.curNodeIdx<0){this.curNodeIdx=$+1;this.hlOn(true)}else if(this.curNodeIdx<(this.size-1)){this.hlOff();this.curNodeIdx=$+1;this.hlOn(true)}else{this.hlOff();this.curNodeIdx=-1;this.box.value=this.oldVal}},excuteCall:function(_){var $=document.createElement("script");$.src=_;this.sptDiv.appendChild($)},updateCall:function($){$=unescape($);$=$.replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&quot;/g,"\"").replace(/&amp;/g,"&").replace(/&#39;/g,"'");this.bdiv.innerHTML=$;if(this.bdiv.childNodes.length<2)this.bdiv.innerHTML="";this.onComplete()},closeSuggest:function($){this.sugFlag=false},focusBox:function(){this.box.focus();if(this.box.createTextRange){var $=this.box.createTextRange();$.moveStart("character",this.box.value.length);$.select()}else if(this.box.setSelectionRange)this.box.setSelectionRange(this.box.value.length,this.box.value.length)},pressPoint:function($){if(this.clickEnabled){this.clickEnabled=false;setTimeout(this.enableClick.sbind(this),20);this.log(this.LOG_ICON_PRESS,"&q="+this.box.value,"&visible="+this.vis);this.focusBox();if(!this.vis){if(this.sugFlag){if(this.box.value=="")this.insertInputHint();else{if(typeof(ydInputFocus)!=undefined)if(ydInputFocus(this.box))return;if(this.lq!=this.box.value){this.doReq();setTimeout(this.showNoSug.sbind(this),200)}else if(this.sdiv.innerHTML==""){this.doReq();setTimeout(this.showNoSug.sbind(this),200)}else if(this.sdiv.childNodes.length<2)this.insertNoSugHint();else this.show()}}else this.insertSugHint()}else this.hide()}},showNoSug:function(){if(this.sdiv.childNodes.length<1)this.insertNoSugHint()},showSugHint:function(){if(this.sdiv.childNodes.length<1)return;this.showContent()},onCompleteHint:function(){setTimeout(this.showSugHint.sbind(this,arguments[0]),5)},onmouseover2:function($){this.box.onblur=null},onmouseout2:function(){this.box.onblur=this.onblur.sbAEListener(this)},bindATagWithMouseEvent:function(C,_){try{if(this.hC)if(C.parentNode){C.parentNode.removeChild(C);return}}catch(A){}var $=C.getElementsByTagName("A");if($.length==0)$=C.getElementsByTagName("a");var B=$[0];if(_)SEvent.observe(B,"click",this.turnOnSuggest.sbAEListener(this));else SEvent.observe(B,"click",this.turnOffSuggest.sbAEListener(this));SEvent.observe(B,"mouseover",this.onmouseover2.sbAEListener(this));SEvent.observe(B,"mouseout",this.onmouseout2.sbAEListener(this))},insertSugHint:function(){this.insertHint("\u63d0\u793a\u529f\u80fd\u5df2\u5173\u95ed","\u6253\u5f00\u63d0\u793a\u529f\u80fd",true)},insertInputHint:function(){this.insertHint("\u5728\u641c\u7d22\u6846\u4e2d\u8f93\u5165\u5173\u952e\u5b57\uff0c\u5373\u4f1a\u5728\u8fd9\u91cc\u51fa\u73b0\u63d0\u793a","\u5173\u95ed\u63d0\u793a\u529f\u80fd",false)},insertNoSugHint:function(){this.insertHint("\u6ca1\u6709\u53ef\u7528\u7684\u63d0\u793a","\u5173\u95ed\u63d0\u793a\u529f\u80fd",false)},insertHint:function(_,A,$){this.sdiv.innerHTML=this.hintCode1+_+this.hintCode2+A+this.hintCode3;var B=this.sdiv.getElementsByTagName("table")[2];this.bindATagWithMouseEvent(B,$);this.onCompleteHint()},turnOnSuggest:function(){var $=this.sugServ+this.S_PREF_URL_POST+"suggest=suggest"+"&o="+this.oN+this.time(),_=new Image();_.src=encodeURI($);this.sugFlag=true;this.lq="";this.initVal=this.box.value;this.oldVal=this.initVal;this.upDownTag=false;if(this.vis)this.hide();this.clean();return false},turnOffSuggest:function(){var $=this.sugServ+this.S_PREF_URL_POST+"&o="+this.oN+this.time(),_=new Image();_.src=encodeURI($);if(this.vis)this.hide();this.clean();this.sugFlag=false;return false},time:function(){return"&time="+new Date()},browserInfo:function(){this.bName="";this.bVer="";var _=navigator.userAgent,A=/MS(IE)\s([^.]+)/i,$=_.match(A);if($==null){A=/(Firefox)\/([^.]+)/i;$=_.match(A);if($==null)return}this.bName=$[1];this.bVer=$[2]},LOG_MOUSE_SELECT:"mouseSelect",LOG_KEY_SELECT:"keySelect",LOG_ICON_PRESS:"iconPress",hintCode1:"<div><table cellpadding=0 cellspacing=1 border=0 width=100% bgcolor=#979797 align=center><tr><td valign=top><table cellpadding=0 cellspacing=0 border=0 width=100% align=center><tr><td align=left bgcolor=white class=remindtt752>",hintCode2:"</td></tr></table><table cellpadding=0 cellspacing=0 border=0 width=100% align=center><tr><td height=1px bgcolor=#DDDDDD></td></tr><tr><td align=right height=17px bgcolor=#ECF0EF class=jstxhuitiaoyou><a class=jstxlan onclick=\"javascript:return false;\">",hintCode3:"</a></td></tr></table></td></tr></table></div>",REQUEST_TIMEOUT:7,ITEM_INDEX_ATTR_NAME:"s_index",ITEM_HIGHLIGHT_STYLE:"aa_highlight",ITEM_SEL_ATTR_NAME:"onSelect",CZNUM:10,KEYFROM_POST:".suggest",S_QUERY_URL_POST:"/suggest/suggest.s?query=",S_LOG_URL_POST:"/suggest/clog.s?type=",S_PREF_URL_POST:"/suggest/setpref.s?",defSugServ:"httpdisabled://"+document.domain,defSearchServ:"httpdisabled://"+document.domain+"/search?",defSearchParamName:"q",defKeyfrom:document.domain.replace(/.youdao.com/,""),defSugName:"aa",defSugIconUrl:"httpdisabled://shared.youdao.com/images/downarrow.gif"};function turnOffSuggest(){return true}function closeSuggest($){if($==null||$=="undefined")$=AutoComplete.defSugName;if(typeof $!="object")return;$.closeSuggest();return true} \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/auto/2011/3/30/20110330215354a8c7a.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/auto/2011/3/30/20110330215354a8c7a.jpg
new file mode 100755
index 0000000000..d02a1b83eb
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/auto/2011/3/30/20110330215354a8c7a.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/book/2011/4/7/201104071025387042e.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/book/2011/4/7/201104071025387042e.jpg
new file mode 100755
index 0000000000..10c4d2750c
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/book/2011/4/7/201104071025387042e.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/book/2011/4/7/20110407103153df111.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/book/2011/4/7/20110407103153df111.jpg
new file mode 100755
index 0000000000..0ddd023752
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/book/2011/4/7/20110407103153df111.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/book/2011/4/8/20110408105903d5d53.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/book/2011/4/8/20110408105903d5d53.jpg
new file mode 100755
index 0000000000..58ddf9f21b
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/book/2011/4/8/20110408105903d5d53.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/book/2011/4/8/20110408110145beb70.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/book/2011/4/8/20110408110145beb70.jpg
new file mode 100755
index 0000000000..07dff8fea1
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/book/2011/4/8/20110408110145beb70.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/cnews/js/ntes_jslib_1.x.js b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/cnews/js/ntes_jslib_1.x.js
new file mode 100755
index 0000000000..a5616b342f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/cnews/js/ntes_jslib_1.x.js
@@ -0,0 +1,14 @@
+/*
+ * NetEase Javascript Library v1.2.5
+ *
+ * Modified from
+ * jRaiser Javascript Library
+ * http://code.google.com/p/jraiser/
+ * Copyright 2008-2010 Heero.Luo (http://heeroluo.net/)
+ *
+ * licensed under MIT license
+ *
+ * Creation date: 2008/2/6
+ * Modified date: 2010/8/4
+ */
+(function(R,f){var b="1.2.5 Build 201008041550",_="NTES";if(R[_]&&R[_].version>=b)return;var A=R.$,c=R.document,T=R[_]=R.$=function($,_){if(!$)return $;"string"===typeof $&&($=L($,_));return M($)};T.one=function($,_){return M(L($,_,1))};T.all=function($,_){return M(L($,_,0))};function L(_,B,$){var A=s.exec(_,B||c);if($!==f)if(A){var C=T.util.isArray(A);if(1===$&&C)return A[0];else if(0===$&&!C)return[A]}else if(0===$)return[];return A}function M(A){if(A&&!A[_])if(A.nodeType){if("unknown"!==typeof A.getAttribute)for(var $ in T.element)f===A[$]&&(A[$]=T.element[$])}else A=T.util.extend(T.util.toArray(A),T.element);return A}T.version=b;T.resume=function(){A=R.$;R.$=R[_]=T;return T};T.retire=function(){R.$=A;return A};var O=c.createElement("div");O.innerHTML="<p class='TEST'></p>";var s={SPACE:/\s*([\s>~+,])\s*/g,ISSIMPLE:/^#?[\w\u00c0-\uFFFF_-]+$/,IMPLIEDALL:/([>\s~\+,]|^)([#\.\[:])/g,ATTRVALUES:/=(["'])([^'"]*)\1]/g,ATTR:/\[\s*([\w\u00c0-\uFFFF_-]+)\s*(?:(\S?\=)\s*(.*?))?\s*\]/g,PSEUDOSEQ:/\(([^\(\)]*)\)$/g,BEGINIDAPART:/^(?:\*#([\w\u00c0-\uFFFF_-]+))/,STANDARD:/^[>\s~\+:]/,STREAM:/[#\.>\s\[\]:~\+]+|[^#\.>\s\[\]:~\+]+/g,ISINT:/^\d+$/,enableQuerySelector:O.querySelectorAll&&O.querySelectorAll(".TEST").length>0,tempAttrValues:[],tempAttrs:[],idName:_+"UniqueId",id:0,exec:function($,I){var _,G,E,C,B,J,K,F,H,L,D=this;$=$.trim();if(""===$)return;if(D.ISSIMPLE.test($))if(0===$.indexOf("#")&&typeof I.getElementById!=="undefined")return D.getElemById(I,$.substr(1));else if(typeof I.getElementsByTagName!=="undefined")return T.util.toArray(I.getElementsByTagName($));if(D.enableQuerySelector&&I.nodeType){try{return T.util.toArray(I.querySelectorAll($))}catch(A){}}I=I.nodeType?[I]:T.util.toArray(I);G=$.replace(D.SPACE,"$1").replace(D.ATTRVALUES,D.analyzeAttrValues).replace(D.ATTR,D.analyzeAttrs).replace(D.IMPLIEDALL,"$1*$2").split(",");E=G.length;C=-1;_=[];while(++C<E){J=I;$=G[C];if(D.BEGINIDAPART.test($))if(typeof I[0].getElementById!=="undefined"){J=[D.getElemById(I[0],RegExp.$1)];if(!J[0])continue;$=RegExp.rightContext}else $=G[C];if($!==""){if(!D.STANDARD.test($))$=" "+$;K=$.match(D.STREAM)||[];F=K.length;B=0;while(B<F){H=K[B++];L=K[B++];J=D.operators[H]?D.operators[H](J,L):[];if(0===J.length)break}}T.util.merge(_,J)}D.tempAttrValues.length=D.tempAttrs.length=0;return _.length>1?D.unique(_):_},analyzeAttrs:function(_,B,A,$){return"[]"+(s.tempAttrs.push([B,A,$])-1)},analyzeAttrValues:function($,A,_){return"="+(s.tempAttrValues.push(_)-1)+"]"},generateId:function(_){var B=this.idName,$;try{$=_[B]=_[B]||new Number(++this.id)}catch(A){$=_.getAttribute(B);if(!$){$=new Number(++this.id);_.setAttribute(B,$)}}return $.valueOf()},unique:function(C){var A=[],D=0,B={},_,$;while(_=C[D++])if(1===_.nodeType){$=this.generateId(_);if(!B[$]){B[$]=true;A.push(_)}}return A},attrMap:{"class":"className","for":"htmlFor"},getAttribute:function($,A){var _=this.attrMap[A]||A,B=$[_];if("string"!==typeof B)if("undefined"!==typeof $.getAttributeNode){B=$.getAttributeNode(A);B=f==B?B:B.value}else if($.attributes)B=String($.attributes[A]);return null==B?"":B},getElemById:function(A,$){var _=A.getElementById($);if(_&&_.id!==$&&A.all){_=A.all[$];if(_){_.nodeType&&(_=[_]);for(var B=0;B<_.length;B++)if(this.getAttribute(_[B],"id")===$)return _[B]}}else return _},getElemsByTagName:function(F,H,E,D,_){var A=[],I=-1,G=F.length,$,C,B;D!=="*"&&(B=D.toUpperCase());while(++I<G){$=F[I][H];C=0;while($&&(!_||C<_)){if(1===$.nodeType){($.nodeName.toUpperCase()===B||!B)&&A.push($);C++}$=$[E]}}return A},checkElemPosition:function(G,H,J,A){var $=[];if(!isNaN(H)){var C=G.length,D=-1,_={},B,E,I,F;while(++D<C){B=G[D].parentNode;E=this.generateId(B);if(f===_[E]){I=0;F=B[J];while(F){1===F.nodeType&&I++;if(I<H)F=F[A];else break}_[E]=F||0}else F=_[E];G[D]===F&&$.push(G[D])}}return $},getElemsByPosition:function(A,C,_){var D=C,B=A.length,$=[];while(D>=0&&D<B){$.push(A[D]);D+=_}return $},getElemsByAttribute:function(B,D){var _=[],$,E=0,A=this.attrOperators[D[1]||""],C="~="===D[1]?" "+D[2]+" ":D[2];if(A)while($=B[E++])A(this.getAttribute($,D[0]),C)&&_.push($);return _},operators:{"#":function(_,$){return s.getElemsByAttribute(_,["id","=",$])}," ":function(A,_){var B=A.length;if(1===B)return A[0].getElementsByTagName(_);else{var $=[],C=-1;while(++C<B)T.util.merge($,A[C].getElementsByTagName(_));return $}},".":function($,_){return s.getElemsByAttribute($,["class","~=",_])},">":function(_,$){return s.getElemsByTagName(_,"firstChild","nextSibling",$)},"+":function(_,$){return s.getElemsByTagName(_,"nextSibling","nextSibling",$,1)},"~":function(_,$){return s.getElemsByTagName(_,"nextSibling","nextSibling",$)},"[]":function($,_){_=s.tempAttrs[_];if(_){if(s.ISINT.test(_[2]))_[2]=s.tempAttrValues[_[2]];return s.getElemsByAttribute($,_)}else return $},":":function(_,A){var $;if(s.PSEUDOSEQ.test(A)){$=parseInt(RegExp.$1);A=RegExp.leftContext}return s.pseOperators[A]?s.pseOperators[A](_,$):[]}},attrOperators:{"":function($){return $!==""},"=":function(_,$){return $===_},"~=":function(_,$){return(" "+_+" ").indexOf($)>=0},"!=":function(_,$){return $!==_},"^=":function(_,$){return _.indexOf($)===0},"$=":function(_,$){return _.substr(_.length-$.length)===$},"*=":function(_,$){return _.indexOf($)>=0}},pseOperators:{"first-child":function($){return s.checkElemPosition($,1,"firstChild","nextSibling")},"nth-child":function(_,$){return s.checkElemPosition(_,$,"firstChild","nextSibling")},"last-child":function($){return s.checkElemPosition($,1,"lastChild","previousSibling")},"nth-last-child":function(_,$){return s.checkElemPosition(_,$,"lastChild","previousSibling")},"odd":function($){return s.getElemsByPosition($,0,2)},"even":function($){return s.getElemsByPosition($,1,2)},"lt":function(_,$){return s.getElemsByPosition(_,$-1,-1)},"gt":function(_,$){return s.getElemsByPosition(_,$+1,1)}}};T.element={get:function($){return this.nodeType===f?this[$]:(0==$?this:f)},$:function($){return T("number"===typeof $?this.get($):$,this)},hasClass:function($){return T.style.hasClass(this,$)},addCss:function($){return T.style.addCss(this,$)},removeCss:function($){return T.style.removeCss(this,$)},addEvent:function(_,A,$){return T.event.addEvent(this,_,A,$)},removeEvent:function($,_){return T.event.removeEvent(this,$,_)},attr:function(A,B){var _=this;A=s.attrMap[A]||A;if(B!==f)return T.dom.eachNode(_,function($,_){this[$]=T.util.isFunction(_)?_.call(this):_},arguments);else{var $=this.get(0);return $?$[A]:f}},each:function($){return T.dom.eachNode(this,$)}};T.element[_]=T.element.$;R.addEvent=c.addEvent=T.element.addEvent;R.removeEvent=c.removeEvent=T.element.removeEvent;var W={},r=Array.prototype.slice,S=Object.prototype.toString;T.util={isArray:function($){return S.call($)==="[object Array]"},isFunction:function($){return S.call($)==="[object Function]"},toArray:function($){if(T.util.isArray($))return $;var A;try{A=r.call($)}catch(_){A=[];var B=$.length;while(B)A[--B]=$[B]}return A},merge:function(_,$){var B=$.length,A=_.length;while(--B>=0)_[A+B]=$[B];return _},parseTpl:function(A,$,_){if(null==A)return;if(null==$)return A;var B=W[A];if(!B){B=new Function("obj","var _=[];with(obj){_.push('"+A.replace(/[\r\t\n]/g," ").replace(/'(?=[^#]*#>)/g,"\t").split("'").join("\\'").split("\t").join("'").replace(/<#=(.+?)#>/g,"',$1,'").split("<#").join("');").split("#>").join("_.push('")+"');}return _.join('');");_!==false&&(W[A]=B)}return B($)},extend:function($,A){for(var _ in A)$[_]=A[_];return $},each:function(A,_,$){var D=-1,B=A.length,C=B===f||T.util.isFunction(A);if($){if(C){for(D in A)if(false===_.apply(A[D],$))break}else while(++D<B)if(false===_.apply(A[D],$))break}else if(C){for(D in A)if(false===_.call(A[D],D,A[D]))break}else while(++D<B)if(false===_.call(A[D],D,A[D]))break;return A}};T.parseTpl=T.util.parseTpl;T.each=T.util.each;var V=[],C,$;if(c.addEventListener)$=function(){c.removeEventListener("DOMContentLoaded",$,false);j()};else if(c.attachEvent)$=function(){if("complete"===c.readyState){c.detachEvent("onreadystatechange",$);j()}};function X(){if(T.dom.isReady)return;try{c.documentElement.doScroll("left")}catch($){setTimeout(X,1);return}j()}function j(){if(!T.dom.isReady){if(!c.body)return setTimeout(j,13);T.dom.isReady=true;if(V){var _=-1,$=V.length;while(++_<$)V[_].call(c,T);V=null}}}function Q(){if(C)return;if("complete"===c.readyState)return j();if(c.addEventListener){c.addEventListener("DOMContentLoaded",j,false);R.addEventListener("loaddisabled",j,false)}else if(c.attachEvent){c.attachEvent("onreadystatechange",j);R.attachEvent("onloaddisabled",j);var _;try{_=R.frameElement==null}catch($){}c.documentElement.doScroll&&_&&X()}C=true}T.dom={wrapByArray:function($){if($)if($.nodeType!==f||$.setInterval)return[$];else if($.length)return T.util.toArray($);return[]},eachNode:function(_,A,$){T.each(T.dom.wrapByArray(_),A,$);return _},ready:function($){Q();if(T.dom.isReady)$.call(c,T);else V.push($);return this}};T.ready=T.dom.ready;var k=/\s*([:;])\s*/g,a=/[^:;]+?(?=:)/g,J=/[^:;]+/g,d=/[^\s]+/g,E=/-([a-z])/gi,Z=O.style.styleFloat!==f?"styleFloat":"cssFloat",o=/^float$/i;function u($,B,_){if(this.className){var A=" "+this.className+" ",C=-1;while(++C<B)-1===A.indexOf(" "+$[C]+" ")&&(A+=($[C]+" "));this.className=A.trim()}else this.className=_}function h($,B,_){switch(this.className){case _:this.className="";break;case"":return;break;default:var A=" "+this.className+" ",C=-1;while(++C<B)A=A.replace(" "+$[C]+" "," ");this.className=A.trim();break}}function t(A,_){if(""===this.style.cssText&&"string"===typeof _)this.style.cssText=_;else for(var $ in A)this.style[$]!==f&&(this.style[$]=A[$])}function m(_){for(var $ in _)this.style[$]!==f&&(this.style[$]="")}T.style={fixStyleName:function($){return o.test($)?Z:$.replace(E,function(_,$){return $.toUpperCase()})},hasClass:function(_,$){_=T.dom.wrapByArray(_);var A=_.length;if(A>0){$=" "+$+" ";while(--A>=0)if((" "+_[A].className+" ").indexOf($)>=0)return true}return false},parse:function(C){if("string"===typeof C){var B=C.indexOf(";")>=0,_=C.indexOf(":")>=0,$;if(B||_){$={};C=C.trim().replace(k,"$1").replace(_?a:J,T.style.fixStyleName).match(J);var A=C.length,D=0;if(_){if(A%2!==0)throw"invalid inline style";while(D<A)$[C[D++]]=C[D++]}else while(D<A)$[C[D++]]=""}else $=C.match(d)||[];return $}return C},addCss:function(_,A){var $=T.style.parse(A);if(T.util.isArray($))T.dom.eachNode(_,u,[$,$.length,A]);else T.dom.eachNode(_,t,[$,A]);return _},removeCss:function(_,A){var $=T.style.parse(A);if(T.util.isArray($))T.dom.eachNode(_,h,[$,$.length,A]);else T.dom.eachNode(_,m,[$]);return _},getCurrentStyle:function(A,_,$){if(!A)return f;!A.nodeType&&(A=A[0]);_=T.style.fixStyleName(_);return A.style[_]||((A.currentStyle||($||R).getComputedStyle(A,null))[_])}};function n(A,B,_){var $=this;B=T.event.delegate($,A,B,_);if($.attachEvent)$.attachEvent("on"+A,B);else if($.addEventListener)$.addEventListener(A,B,false)}function g(_,A){var $=this;A=T.event.getDelegate($,_,A);if($.detachEvent)$.detachEvent("on"+_,A);else if($.removeEventListener)$.removeEventListener(_,A,false)}var H=/\s*,\s*/,D=0;T.event={idName:_+"EventId",eventSpace:_+"Events",addEvent:function(_,A,C,$){A=A.split(H);var B=A.length;while(--B>=0)T.dom.eachNode(_,n,[A[B],C,$]);return _},removeEvent:function($,_,B){_=_.split(H);var A=_.length;while(--A>=0)T.dom.eachNode($,g,[_[A],B]);return $},delegate:function(_,E,G,C){var A=T.event,B=_[A.eventSpace]=_[A.eventSpace]||{},$=G[A.idName]=G[A.idName]||++D;B[E]=B[E]||{};var F=B[E][$];if(!F){F=function($){$=A.fix($);var B=G.call(_,$,C);false===B&&$.preventDefault();return B};B[E][$]=F}return F},getDelegate:function($,B,C){var A=T.event;try{return $[A.eventSpace][B][C[A.idName]]}catch(_){}return C},fix:function(_){!_.target&&(_.target=_.srcElement||c);3==_.target.nodeType&&(_.target=_.target.parentNode);null==_.timeStamp&&(_.timeStamp=Date.now());_.preventDefault=_.preventDefault||function(){this.returnValue=false};_.stopPropagation=_.stopPropagation||function(){this.cancelBubble=true};if(f===_.pageX&&f!==_.clientX){var A=c.documentElement,$=c.body;_.pageX=_.clientX+(A.scrollLeft||$.scrollLeft||0)-(A.clientLeft||0);_.pageY=_.clientY+(A.scrollTop||$.scrollTop||0)-(A.clientTop||0)}if(!_.which&&((_.charCode||_.charCode===0)?_.charCode:_.keyCode))_.which=_.charCode||_.keyCode;if(!_.which&&_.button!==f)_.which=(_.button&1?1:(_.button&2?3:(_.button&4?2:0)));return _}};var q=R.navigator.userAgent.toLowerCase(),i=/(webkit)[ \/]([\w.]+)/.exec(q)||/(opera)(?:.*version)?[ \/]([\w.]+)/.exec(q)||/(msie) ([\w.]+)/.exec(q)||!/compatible/.test(q)&&/(mozilla)(?:.*? rv:([\w.]+))?/.exec(R.navigator.userAgent.toLowerCase());T.browser={};if(i){T.browser[i[1]||""]=true;T.browser.version=i[2]||"0"}T.ajax={createXhr:function(){var _;try{_=R.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest()}catch($){}if(!_)throw"failed to create XMLHttpRequest object";return _},send:function(G,C,B,H,A){A=A||T.ajax.createXhr();var E;"string"===typeof C&&(C=C.toUpperCase());C=C!=="GET"&&C!=="POST"?"GET":C;H=H||{};H.async="boolean"===typeof H.async?H.async:true;var _;if(B){_=[];for(var $ in B)B[$]!=null&&_.push($+"="+encodeURIComponent(B[$]));_=_.join("&").replace(/%20/g,"+");if("GET"===C){G+=("?"+_);_=f}}H.async&&!isNaN(H.timeout)&&H.timeout>0&&setTimeout(function(){if(!E){A.abort();H.onTimeout&&H.onTimeout(A)}},H.timeout);A.onreadystatechange=function(){if(4==A.readyState){E=true;var $=200==A.status?"onSuccess":"onError";H[$]&&H[$](A)}};void(C,G,H.async,H.username,H.password);var D=[];"POST"===C&&D.push("application/x-www-form-urlencoded");A.setRequestHeader("X-Requested-With","XMLHttpRequest");if(H.headers)for(var F in H.headers)if("content-type"===F.toLowerCase())D.push(H.headers[F]);else A.setRequestHeader(F,H.headers[F]);D.length&&A.setRequestHeader("Content-Type",D.join(";").replace(/;+/g,";").replace(/;$/,""));A.send(_);return A},importJs:function(C,$,A,_){_=_||c;var B=_.createElement("script");B.language="javascript";B.type="text/javascript";A&&(B.charset=A);B.onloaddisabled=B.onreadystatechange=function(){if(!B.readyState||"loaddisableded"==B.readyState||"complete"==B.readyState){$&&$();B.onloaddisabled=B.onreadystatechange=null;B.parentNode.removeChild(B)}};B.src=C;T.one("head",_).appendChild(B)}};var v=/[smhdMy]$/,K={s:1,m:60,h:60*60,d:24*60*60,M:30*24*60*60,y:365*24*60*60};T.cookie={encoder:R.encodeURIComponent,decoder:R.decodeURIComponent,get:function(B,D){var _=T.cookie;B=_.encoder(B)+"=";var $=c.cookie,A=$.indexOf(B),C;if(-1===A)return D?f:"";A+=B.length;C=$.indexOf(";",A);if(C===-1)C=$.length;return _.decoder($.substring(A,C))},set:function(C,G,A,F,E,D){var _=T.cookie,B=[_.encoder(C)+"="+_.encoder(G)];if(A){var H,$;if("[object Date]"===S.call(A))H=A;else{if("string"===typeof A&&v.test(A)){A=A.substring(0,A.length-1);$=RegExp.lastMatch}if(!isNaN(A)){H=new Date();H.setTime(H.getTime()+A*K[$||"m"]*1000)}}H&&B.push("expires="+H.toUTCString())}E&&B.push("path="+E);F&&B.push("domain="+F);D&&B.push("secure");c.cookie=B.join(";")},del:function($,A,_){c.cookie=T.cookie.encoder($)+"="+(_?";path="+_:"")+(A?";domain="+A:"")+";expires=Thu, 01-Jan-1970 00:00:01 GMT"}};var N=/^\s+|\s+$/g;!String.prototype.trim&&(String.prototype.trim=function(){return this.replace(N,"")});String.prototype.left=function($){return this.substr(0,$)};String.prototype.right=function($){return this.slice(-$)};String.format=function($){var _=arguments,A=new RegExp("%([1-"+_.length+"])","g");return String($).replace(A,function(A,$){return _[$]})};Function.prototype.bind=function(){if(!arguments.length)return this;var _=this,$=r.call(arguments),A=$.shift();return function(){return _.apply(A,$.concat(r.call(arguments)))}};!Array.prototype.indexOf&&(Array.prototype.indexOf=function(A,_){var $=this.length,_=Number(_)||0;_=_<0?Math.ceil(_):Math.floor(_);_<0&&(_+=$);for(;_<$;_++)if(this[_]===A)return _;return-1});Array.prototype.remove=function($){$>=0&&this.splice($,1);return this};function B($){return $<10?"0"+$:$}var U,F,p,P,Y,G;function l($){switch($){case"yyyy":return U;case"yy":return U.toString().slice(-2);case"MM":return B(F);case"M":return F;case"dd":return B(p);case"d":return p;case"HH":return B(P);case"H":return P;case"hh":return B(P>12?P-12:P);case"h":return P>12?P-12:P;case"mm":return B(Y);case"m":return Y;case"ss":return B(G);case"s":return G;default:return $}}Date.now=Date.now||function(){return+new Date};Date.prototype.format=function($){U=this.getFullYear();F=this.getMonth()+1;p=this.getDate();P=this.getHours();Y=this.getMinutes();G=this.getSeconds();return $.replace(/y+|m+|d+|h+|s+|H+|M+/g,l)};O=null;T.ui={};function e(_,$){return(_+1)%$}function I(_,$){return _<=0?$-1:(_-1)%$}T.ui.Slide=function(_,B,G,E,A,F){if(!arguments.length)return;var $=this;$.total=B.length;if(_&&$.total!==_.length)throw"can not match ctrls("+_.length+") and contents("+$.total+")";$.constructor=arguments.callee;$._curIndex=-1;$._ctrls=_;$._contents=B;$._css=G;$._eventName=E;$.interval=A;$.playMode=e;$.rollbackMode=I;$.delay=F;if($._ctrls&&$._ctrls.length&&$._eventName){var D,C;if(F){D=function(_,$){!this._delayTimer&&(this._delayTimer=setTimeout(this.show.bind(this,$),this.delay));_.preventDefault()}.bind($);C=function(){if(this._delayTimer){clearTimeout(this._delayTimer);delete this._delayTimer}}.bind($)}else D=function(_,$){this.show($);_.preventDefault()}.bind($);for(var H=$.total-1;H>=0;H--){T.event.addEvent($._ctrls[H],E,D,new Number(H));C&&T.event.addEvent($._ctrls[H],"mouseout",C)}}$.interval&&$.play()};T.ui.Slide.prototype={show:function(_){var A=this;_=_<0?0:_>=A.total?A.total-1:_;var B=A._ctrls?A._ctrls[_]:null,$=A._contents[_];if(-1===A._curIndex)A._curIndex=0;T.style.removeCss(A._ctrls,A._css);T.style.removeCss(A._contents,A._css);T.style.addCss(B,A._css);T.style.addCss($,A._css);A.onShow&&A.onShow(_,B,$);A._curIndex=_},showNext:function(){this.show(this.playMode(this._curIndex,this.total))},showPrevious:function(){this.show(this.rollbackMode(this._curIndex,this.total))},play:function(A){var _=this;if(!isNaN(A))_.interval=parseInt(A);if(!_._playTimer){if(!_._hasEvent){var $=_.pause.bind(_),B=_.play.bind(_);T.event.addEvent(_._ctrls,"mouseover",$);T.event.addEvent(_._ctrls,"mouseout",B);T.event.addEvent(_._contents,"mouseover",$);T.event.addEvent(_._contents,"mouseout",B);_._hasEvent=1}_._playTimer=setInterval(_.showNext.bind(_),_.interval)}},pause:function(){var _=this;if(_._playTimer){clearInterval(_._playTimer);delete _._playTimer;if(_.onStop){var $=_._curIndex;_.onStop($,_._ctrls[$],_._contents[$])}}}}})(window) \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/edu/2011/4/6/20110406220601277f0.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/edu/2011/4/6/20110406220601277f0.jpg
new file mode 100755
index 0000000000..5b5196e037
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/edu/2011/4/6/20110406220601277f0.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/edu/2011/4/9/20110409001451f646c.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/edu/2011/4/9/20110409001451f646c.jpg
new file mode 100755
index 0000000000..70ed0e1d45
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/edu/2011/4/9/20110409001451f646c.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/ent/2011/4/8/20110408183341f6142.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/ent/2011/4/8/20110408183341f6142.jpg
new file mode 100755
index 0000000000..dea0d81519
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/ent/2011/4/8/20110408183341f6142.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/game/2011/4/8/20110408091923ca1d8.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/game/2011/4/8/20110408091923ca1d8.jpg
new file mode 100755
index 0000000000..96ffed42fa
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/game/2011/4/8/20110408091923ca1d8.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/game/2011/4/8/20110408100456977e5.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/game/2011/4/8/20110408100456977e5.jpg
new file mode 100755
index 0000000000..4cc85bb490
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/game/2011/4/8/20110408100456977e5.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/game/2011/4/8/2011040810253254779.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/game/2011/4/8/2011040810253254779.jpg
new file mode 100755
index 0000000000..bca502e559
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/game/2011/4/8/2011040810253254779.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/house/2011/4/7/201104070846149dec5.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/house/2011/4/7/201104070846149dec5.jpg
new file mode 100755
index 0000000000..055085914e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/house/2011/4/7/201104070846149dec5.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/house/2011/4/8/20110408094024dfb90.gif b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/house/2011/4/8/20110408094024dfb90.gif
new file mode 100755
index 0000000000..99437cf5c2
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/house/2011/4/8/20110408094024dfb90.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/lady/2011/4/7/20110407235235eb565.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/lady/2011/4/7/20110407235235eb565.jpg
new file mode 100755
index 0000000000..f713b42e5a
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/lady/2011/4/7/20110407235235eb565.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/lady/2011/4/8/20110408082553b8653.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/lady/2011/4/8/20110408082553b8653.jpg
new file mode 100755
index 0000000000..9c0c5c07ff
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/lady/2011/4/8/20110408082553b8653.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/life/2011/2/24/20110224214610e49c1.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/life/2011/2/24/20110224214610e49c1.jpg
new file mode 100755
index 0000000000..9bc68eb6d4
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/life/2011/2/24/20110224214610e49c1.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/life/2011/4/1/20110401105148c65f3.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/life/2011/4/1/20110401105148c65f3.jpg
new file mode 100755
index 0000000000..801b96f5ab
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/life/2011/4/1/20110401105148c65f3.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/life/2011/4/6/20110406140048c8dea.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/life/2011/4/6/20110406140048c8dea.jpg
new file mode 100755
index 0000000000..d7f80b1b70
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/life/2011/4/6/20110406140048c8dea.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/life/2011/4/6/201104061402503e782.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/life/2011/4/6/201104061402503e782.jpg
new file mode 100755
index 0000000000..8aec592cb2
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/life/2011/4/6/201104061402503e782.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/life/2011/4/8/20110408175702d86a7.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/life/2011/4/8/20110408175702d86a7.jpg
new file mode 100755
index 0000000000..96f759352c
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/life/2011/4/8/20110408175702d86a7.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/mobile/2011/4/8/201104080904537def0.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/mobile/2011/4/8/201104080904537def0.jpg
new file mode 100755
index 0000000000..b392262335
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/mobile/2011/4/8/201104080904537def0.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/sports/2011/4/8/20110408164530e0dfd.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/sports/2011/4/8/20110408164530e0dfd.jpg
new file mode 100755
index 0000000000..63bdc7ec5b
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/sports/2011/4/8/20110408164530e0dfd.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/sports/2011/4/8/20110408224146ca253.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/sports/2011/4/8/20110408224146ca253.jpg
new file mode 100755
index 0000000000..2490e0049f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/sports/2011/4/8/20110408224146ca253.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/sports/2011/4/8/20110408234759dabf8.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/sports/2011/4/8/20110408234759dabf8.jpg
new file mode 100755
index 0000000000..1e5070f86e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/sports/2011/4/8/20110408234759dabf8.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/travel/2011/4/7/2011040719553034b7b.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/travel/2011/4/7/2011040719553034b7b.jpg
new file mode 100755
index 0000000000..96d79968b2
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/travel/2011/4/7/2011040719553034b7b.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/video/2011/4/8/20110408143144afad3.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/video/2011/4/8/20110408143144afad3.jpg
new file mode 100755
index 0000000000..ca696080d4
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/video/2011/4/8/20110408143144afad3.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/www/logo/logo_png.png b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/www/logo/logo_png.png
new file mode 100755
index 0000000000..ab64111bd4
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img3.cache.netease.com/www/logo/logo_png.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/auto/2011/4/8/20110408091859b1da7.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/auto/2011/4/8/20110408091859b1da7.jpg
new file mode 100755
index 0000000000..8597930899
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/auto/2011/4/8/20110408091859b1da7.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/auto/2011/4/8/201104080930543aaa8.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/auto/2011/4/8/201104080930543aaa8.jpg
new file mode 100755
index 0000000000..117db64006
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/auto/2011/4/8/201104080930543aaa8.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/book/2011/4/8/20110408102221db369.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/book/2011/4/8/20110408102221db369.jpg
new file mode 100755
index 0000000000..56d8aa80de
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/book/2011/4/8/20110408102221db369.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/digi/2011/4/8/20110408144717d8da9.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/digi/2011/4/8/20110408144717d8da9.jpg
new file mode 100755
index 0000000000..3fcd833e8a
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/digi/2011/4/8/20110408144717d8da9.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/ent/2011/4/8/20110408074407aed87.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/ent/2011/4/8/20110408074407aed87.jpg
new file mode 100755
index 0000000000..70e4f432ff
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/ent/2011/4/8/20110408074407aed87.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/ent/2011/4/8/201104080804383b8a7.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/ent/2011/4/8/201104080804383b8a7.jpg
new file mode 100755
index 0000000000..e69632d7e8
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/ent/2011/4/8/201104080804383b8a7.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/ent/2011/4/8/2011040809044637924.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/ent/2011/4/8/2011040809044637924.jpg
new file mode 100755
index 0000000000..48d1cbeec1
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/ent/2011/4/8/2011040809044637924.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/game/2011/4/5/2011040502293054a8f.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/game/2011/4/5/2011040502293054a8f.jpg
new file mode 100755
index 0000000000..22ea2359b1
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/game/2011/4/5/2011040502293054a8f.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/game/2011/4/8/201104081007164a116.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/game/2011/4/8/201104081007164a116.jpg
new file mode 100755
index 0000000000..1bc323fb79
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/game/2011/4/8/201104081007164a116.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/game/2011/4/8/201104081009084803f.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/game/2011/4/8/201104081009084803f.jpg
new file mode 100755
index 0000000000..469cd947f1
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/game/2011/4/8/201104081009084803f.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/game/2011/4/8/2011040811265683661.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/game/2011/4/8/2011040811265683661.jpg
new file mode 100755
index 0000000000..8ade4a76e5
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/game/2011/4/8/2011040811265683661.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/home/2011/4/7/20110407131936bb4ec.png b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/home/2011/4/7/20110407131936bb4ec.png
new file mode 100755
index 0000000000..67dc83f75c
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/home/2011/4/7/20110407131936bb4ec.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/house/2011/4/8/201104080927161a54f.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/house/2011/4/8/201104080927161a54f.jpg
new file mode 100755
index 0000000000..bad3e99ca0
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/house/2011/4/8/201104080927161a54f.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/lady/2011/4/7/2011040711484089cba.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/lady/2011/4/7/2011040711484089cba.jpg
new file mode 100755
index 0000000000..c9af6f5536
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/lady/2011/4/7/2011040711484089cba.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/lady/2011/4/8/20110408014720d3fc0.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/lady/2011/4/8/20110408014720d3fc0.jpg
new file mode 100755
index 0000000000..095fa915f7
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/lady/2011/4/8/20110408014720d3fc0.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/lady/2011/4/8/20110408224817711dd.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/lady/2011/4/8/20110408224817711dd.jpg
new file mode 100755
index 0000000000..01aa867574
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/lady/2011/4/8/20110408224817711dd.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/life/2011/3/7/20110307134125752e1.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/life/2011/3/7/20110307134125752e1.jpg
new file mode 100755
index 0000000000..c2fb3a188c
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/life/2011/3/7/20110307134125752e1.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/mobile/2011/4/8/2011040809135520264.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/mobile/2011/4/8/2011040809135520264.jpg
new file mode 100755
index 0000000000..071c5b508d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/mobile/2011/4/8/2011040809135520264.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/photo/0008/2010-01-30/120x90_5U980MMS294H0008.JPG b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/photo/0008/2010-01-30/120x90_5U980MMS294H0008.JPG
new file mode 100755
index 0000000000..c423c7d241
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/photo/0008/2010-01-30/120x90_5U980MMS294H0008.JPG
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/sports/2011/4/8/20110408211535eae49.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/sports/2011/4/8/20110408211535eae49.jpg
new file mode 100755
index 0000000000..49c8d3d8f8
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/sports/2011/4/8/20110408211535eae49.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/stock/2011/3/1/201103010846298829b.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/stock/2011/3/1/201103010846298829b.jpg
new file mode 100755
index 0000000000..3dd6b74292
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/stock/2011/3/1/201103010846298829b.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/stock/2011/4/8/201104080929109dd6d.png b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/stock/2011/4/8/201104080929109dd6d.png
new file mode 100755
index 0000000000..be9ceb616d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/stock/2011/4/8/201104080929109dd6d.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/stock/2011/4/8/20110408121505602ea.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/stock/2011/4/8/20110408121505602ea.jpg
new file mode 100755
index 0000000000..6f965185f5
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/stock/2011/4/8/20110408121505602ea.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/stock/2011/4/8/20110408183832fdfa0.png b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/stock/2011/4/8/20110408183832fdfa0.png
new file mode 100755
index 0000000000..0594e7eaa4
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/stock/2011/4/8/20110408183832fdfa0.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/video/2011/4/7/20110407105038a01d2.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/video/2011/4/7/20110407105038a01d2.jpg
new file mode 100755
index 0000000000..d74033eff8
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/video/2011/4/7/20110407105038a01d2.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/video/2011/4/7/2011040715531564880.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/video/2011/4/7/2011040715531564880.jpg
new file mode 100755
index 0000000000..82ba96528f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/video/2011/4/7/2011040715531564880.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/video/2011/4/8/2011040809594909a0a.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/video/2011/4/8/2011040809594909a0a.jpg
new file mode 100755
index 0000000000..777b26f58a
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/img4.cache.netease.com/video/2011/4/8/2011040809594909a0a.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/imgrc.ph.126.net/40YCPhfL6uaLg3xA4ISWew==/4227754150194064440.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/imgrc.ph.126.net/40YCPhfL6uaLg3xA4ISWew==/4227754150194064440.jpg
new file mode 100755
index 0000000000..cd8aab98ee
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/imgrc.ph.126.net/40YCPhfL6uaLg3xA4ISWew==/4227754150194064440.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/imgrc.ph.126.net/F4Oc-9fe_HYFRsSk0SRMmA==/4223532025543403580.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/imgrc.ph.126.net/F4Oc-9fe_HYFRsSk0SRMmA==/4223532025543403580.jpg
new file mode 100755
index 0000000000..670faae54b
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/imgrc.ph.126.net/F4Oc-9fe_HYFRsSk0SRMmA==/4223532025543403580.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/imgrc.ph.126.net/VfPeCwJ6ufovjjY9ueyUxA==/4224939400426958880.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/imgrc.ph.126.net/VfPeCwJ6ufovjjY9ueyUxA==/4224939400426958880.jpg
new file mode 100755
index 0000000000..e38c1a9cb9
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/imgrc.ph.126.net/VfPeCwJ6ufovjjY9ueyUxA==/4224939400426958880.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/imgrc.ph.126.net/chRhUK9Mxz9gdCzkEUzn5w==/4226346775310512150.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/imgrc.ph.126.net/chRhUK9Mxz9gdCzkEUzn5w==/4226346775310512150.jpg
new file mode 100755
index 0000000000..7810ff8727
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/imgrc.ph.126.net/chRhUK9Mxz9gdCzkEUzn5w==/4226346775310512150.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/imgrc.ph.126.net/nSvNUs-5vbkySqbYp-lnLw==/4226628250287222807.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/imgrc.ph.126.net/nSvNUs-5vbkySqbYp-lnLw==/4226628250287222807.jpg
new file mode 100755
index 0000000000..f4688f176e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/imgrc.ph.126.net/nSvNUs-5vbkySqbYp-lnLw==/4226628250287222807.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/oimagea4.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2FPjU3g b/mobile/android/tests/browser/chrome/tp5/163.com/oimagea4.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2FPjU3g
new file mode 100755
index 0000000000..f74d4c71bf
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/oimagea4.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2FPjU3g
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/oimagea8.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2F2WEnFW b/mobile/android/tests/browser/chrome/tp5/163.com/oimagea8.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2F2WEnFW
new file mode 100755
index 0000000000..6f53696ad6
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/oimagea8.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2F2WEnFW
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/oimagea8.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2F2x2iAO b/mobile/android/tests/browser/chrome/tp5/163.com/oimagea8.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2F2x2iAO
new file mode 100755
index 0000000000..a9ab30ed28
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/oimagea8.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2F2x2iAO
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/oimagea8.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2F40hcYl b/mobile/android/tests/browser/chrome/tp5/163.com/oimagea8.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2F40hcYl
new file mode 100755
index 0000000000..e4a9ce855d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/oimagea8.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2F40hcYl
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/oimageb2.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2F46NVMe b/mobile/android/tests/browser/chrome/tp5/163.com/oimageb2.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2F46NVMe
new file mode 100755
index 0000000000..f54b9dfbcf
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/oimageb2.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2F46NVMe
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/oimageb3.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2FTyjFq b/mobile/android/tests/browser/chrome/tp5/163.com/oimageb3.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2FTyjFq
new file mode 100755
index 0000000000..48677fe17f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/oimageb3.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2FTyjFq
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/oimagec1.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2F3SWBUh b/mobile/android/tests/browser/chrome/tp5/163.com/oimagec1.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2F3SWBUh
new file mode 100755
index 0000000000..eedfe0d686
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/oimagec1.ydstatic.com/image@w=80&h=80&url=http%3A%2F%2F126.fm%2F3SWBUh
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/oimagec7.ydstatic.com/image@w=128&h=128&url=http%3A%2F%2F126.fm%2F3cAjJD b/mobile/android/tests/browser/chrome/tp5/163.com/oimagec7.ydstatic.com/image@w=128&h=128&url=http%3A%2F%2F126.fm%2F3cAjJD
new file mode 100755
index 0000000000..834f897ac3
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/oimagec7.ydstatic.com/image@w=128&h=128&url=http%3A%2F%2F126.fm%2F3cAjJD
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/p.mail.163.com/mailinfo/img/mail1.gif b/mobile/android/tests/browser/chrome/tp5/163.com/p.mail.163.com/mailinfo/img/mail1.gif
new file mode 100755
index 0000000000..9d43acd77e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/p.mail.163.com/mailinfo/img/mail1.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/p.mail.163.com/mailinfo/img/mail2.gif b/mobile/android/tests/browser/chrome/tp5/163.com/p.mail.163.com/mailinfo/img/mail2.gif
new file mode 100755
index 0000000000..8336f8bb96
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/p.mail.163.com/mailinfo/img/mail2.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/p.mail.163.com/mailinfo/ntes_mail_info_www_1222.js b/mobile/android/tests/browser/chrome/tp5/163.com/p.mail.163.com/mailinfo/ntes_mail_info_www_1222.js
new file mode 100755
index 0000000000..6d0e40559e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/p.mail.163.com/mailinfo/ntes_mail_info_www_1222.js
@@ -0,0 +1,156 @@
+/**
+ * ÍøÒ×ÓÊÏäͨÓÃÏÔʾÓʼþÊý,¿ÉÒÔÌí¼Óµ½ÈÎÒâ163ÓòϵÄÒ³Ãæ
+ */
+function NtesMailInfo(){
+ // ¸ù¾ÝÏÔʾ²»Í¬ÐèÇóÅäÖÃ
+ // ÏÔʾÕʺÅ
+ this.showAccount = true;
+ // ÏÔʾÓʼþÊý
+ this.showMsgCount = true;
+ // ÏÔʾÍ˳ö|µÇ¼
+ this.showLoginout = true;
+ // ÏÔʾÕʺźó׺
+ this.showAccountSuffix = true;
+ // ÏÔʾÕʺÅ×î¶àµÄ³¤¶È
+ this.maxAccountLength = 7;
+
+ this.getCookie = function (sName){
+ var sSearch = sName + "=";
+ if(document.cookie.length > 0) {
+ var sOffset = document.cookie.indexOf(sSearch);
+ if(sOffset != -1) {
+ sOffset += sSearch.length;
+ sEnd = document.cookie.indexOf(";", sOffset);
+ if(sEnd == -1) sEnd = document.cookie.length;
+ return unescape(document.cookie.substring(sOffset, sEnd));
+ }
+ else return "";
+ }
+ };
+ // ³õʼ»¯
+ this.init = function (){
+ if(this.P_INFO){ // Èç¹ûcookieÓÐpinfo,»ñÈ¡ÕʺÅ
+ var arr = this.P_INFO.split("|");
+ this.username = arr[0];
+ if(this.username.indexOf("@126.com") > -1){
+ this.domain = "126.com";
+ }
+ if(this.username.indexOf("@yeah.net") > -1){
+ this.domain = "yeah.net";
+ }
+ if(this.username.indexOf("@163.com") > -1){
+ this.domain = "163.com";
+ }
+ /*if(this.username.indexOf("@188.com") > -1){
+ this.domain = "188.com";
+ }
+ if(this.username.indexOf("@vip.163.com") > -1){
+ this.domain = "vip.163.com";
+ }*/
+ if(arr[2] == 1){
+ this.isLogin = true;
+ }
+ }else{// ·ñÔò·µ»Ø
+ return;
+ }
+ if(this.cm_newmsg){// Èç¹ûÓÐÐÂÓʼþÊýÄ¿cookie,²¢ÇÒÕʺźÍpinfoÀïÃæµÄÒ»ÖÂ,ÉèÖÃhasnew±ê¼ÇΪtrue
+ var arr = this.cm_newmsg.split("&");
+ if(arr[0]){
+ var sUserName = arr[0].substr(5);
+ if(sUserName == this.username){
+ this.hasnew = true;
+ if(arr[1]){
+ this.newCount = arr[1].substr(4);
+ }
+ }
+ }
+ }
+ };
+ // Éú³Éhtml
+ this.render = function (){
+ if(this.domain == "") return;
+ if(this.hasnew){
+ var sUserName = this.username;
+ if(sUserName.indexOf("@") > -1){
+ sUserName = sUserName.split("@")[0];
+ }
+ if(sUserName.length > this.maxAccountLength){
+ sUserName = sUserName.substring(0, this.maxAccountLength) + "..";
+ }
+ this.$("dvNewMsg").style.display = ""; // ÏÔʾ½çÃæ
+
+ this.$("imgNewMsg").title = "ÄúÓÐ"+ this.newCount +"·âδ¶ÁÓʼþ";
+ this.$("lnkNewMsg").innerHTML = this.newCount > 999 ? "999+" : this.newCount; // ÐÂÓʼþÊýÄ¿
+ if(this.newCount == 0){ // ÓʼþÊýΪ0»òÕß´óÓÚ0ÊÇÏÔʾ²»Í¬µÄͼ±ê
+ this.$("imgNoNewMsg").style.display = "";
+ this.$("imgNewMsg").style.display = "none";
+ }else{
+ this.$("imgNoNewMsg").style.display = "none";
+ this.$("imgNewMsg").style.display = "";
+ }
+
+ this.$("lnkNewMsg").href = this.$("lnkMsgImg").href = this.getLoginUrl(); // ÉèÖÃÓʼþÊýµÄÁ´½Ó
+ }else{
+ if(this.domain != "163.com" && location.hostname.indexOf("163.com") > -1){
+ location.href = this.getShowNewMsgUrl();
+ }else{
+ if(!window.gGetNewCount){
+ window.gGetNewCount = true;
+ void('<iframe src="about:blank" style="display:none;" id="ifrmNtesMailInfo" onloaddisabled="gNtesMailInfo=new NtesMailInfo();"></iframe>');
+ this.$("ifrmNtesMailInfo").src = this.getNewCountUrl();
+ }
+ }
+ }
+ };
+ this.redirect = function (bType){
+ if(this.redirected) return;
+ this.redirected = true;
+ if(bType == "iframe"){
+ this.$("ifrmNtesMailInfo").src = this.getShowNewMsgUrl();
+ }else{
+ location.href = this.getShowNewMsgUrl();
+ }
+ };
+ this.$ = function (sId){
+ return document.getElementById(sId);
+ };
+ this.P_INFO = this.getCookie("P_INFO"); // »ñÈ¡pinfocookie
+ this.cm_newmsg = this.getCookie("cm_newmsg"); // »ñÈ¡ÐÂÓʼþÊýÄ¿cookie
+ this.isLogin = this.getCookie("S_INFO") ? true : false; // µ±Ç°ÊÇ·ñµÇ¼urs
+ this.username = ""; // ÕʺÅ
+ this.hasnew = false; // cookieÊÇ·ñÓÐÐÂÓʼþÊýÄ¿
+ this.domain = ""; // ÓòÃû
+ this.newCount = 0; // ÐÂÓʼþÊýÄ¿
+ this.redirected = false; // ÊÇ·ñredirect
+ this.isHomePage = location.hostname == "www.163.com" ? true : false;
+ // ÀàÐÍ,show:ÏÔʾÊýÄ¿Ò³Ãæ, crossdomain:¿çÓòÌøתҳÃæ, init:ÒýÓÃjsµÄ163ƵµÀÒ³Ãæ
+ this.type = (location.href.indexOf("/mailinfo/shownewmsg_0225.htm") > -1 ? "show" : (location.href.indexOf("/mailinfo/crossdomain_0225.htm") > -1 ? "crossdomain" : "init"));
+
+ this.getShowNewMsgUrl = function (){// ÏÔʾÓʼþÊýÄ¿ÐÅÏ¢Ò³Ãæ
+ return "httpdisabled://p.mail."+ this.domain +"/mailinfo/shownewmsg_www_1222.htm";
+ };
+
+ this.getNewCountUrl = function (){ // »ñÈ¡ÐÂÓʼþ½Ó¿Úurl
+ return "httpdisabled://msg.mail."+ this.domain +"/cgi/mc?funcid=getusrnewmsgcnt&fid=1&addSubFdrs=1&language=0&style=0&template=newmsgres_setcookie.htm&username=" + this.username;
+ };
+ this.getLoginUrl = function (){ // »ñÈ¡µÇ¼url
+ var oEntryUrl = {
+ "163.com" : "httpdisabled://entry.mail.163.com/coremail/fcg/ntesdoor2?lightweight=1&verifycookie=1&language=-1&style=-1&from=newmsg_www",
+ "126.com" : "httpdisabled://entry.mail.126.com/cgi/ntesdoor?lightweight=1&verifycookie=1&language=-1&style=-1&from=newmsg_www",
+ "yeah.net" : "httpdisabled://entry.mail.yeah.net/cgi/ntesdoor?lightweight=1&verifycookie=1&language=-1&style=-1&from=newmsg_www"
+ };
+ if(!this.isLogin){
+ oEntryUrl = {
+ "163.com" : "httpdisabled://email.163.com/?from=newmsg#163",
+ "126.com" : "httpdisabled://email.163.com/?from=newmsg#126",
+ "yeah.net" : "httpdisabled://email.163.com/?from=newmsg#yeah"
+ }
+ }
+ return oEntryUrl[this.domain];
+ };
+
+ // if(!this.isLogin) return; // Èç¹ûûÓеǼֱ½Ó·µ»Ø
+ this.init(); // ³õʼ»¯
+ this.render();// Éú³Éhtml
+}
+var gNtesMailInfo = new NtesMailInfo(); \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/p.mail.163.com/mailinfo/shownewmsg_www_1222.htm.html b/mobile/android/tests/browser/chrome/tp5/163.com/p.mail.163.com/mailinfo/shownewmsg_www_1222.htm.html
new file mode 100755
index 0000000000..242b68253b
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/p.mail.163.com/mailinfo/shownewmsg_www_1222.htm.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "httpdisabled://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="httpdisabled://www.w3.org/1999/xhtml">
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
+<title>ÍøÒ×ÓÊÏä</title>
+
+<style type="text/css">
+<!--
+
+body{ margin:0; padding:0; font:normal 12px Arial; line-height:18px; padding-top:5px}
+.gray{color:#666}
+img{vertical-align:middle}
+a{color:#727171; text-decoration:none}
+a:hover{ color:#ba2636; text-decoration:underline}
+a.blueLink:link,a.blueLink:visited{color:#1e50a2}
+a.blueLink:hover{color:#ba2636}
+-->
+</style></head>
+
+<body>
+<div id="dvNewMsg" style="display:none;">(&nbsp;<a href="shownewmsg_www_1222.htm.html#" id="lnkMsgImg" target="_blank"><img src="img/mail2.gif" id="imgNoNewMsg" style="display:none" alt="ûÓÐÐÂÓʼþ" width="16" height="16" align="absmiddle" border="0" /><img src="img/mail1.gif" id="imgNewMsg" alt="" width="16" height="16" align="absmiddle" border="0" /></a> <b><a class="blueLink" href="shownewmsg_www_1222.htm.html#" id="lnkNewMsg" target="_blank"></a></b>&nbsp;)
+ </div>
+</body>
+<SCRIPT LANGUAGE="JavaScript" src="ntes_mail_info_www_1222.js"></SCRIPT>
+</html>
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/pro.163.com/js.ng/site=netease&affiliate=homepage&cat=homepage&type=flash&location=1 b/mobile/android/tests/browser/chrome/tp5/163.com/pro.163.com/js.ng/site=netease&affiliate=homepage&cat=homepage&type=flash&location=1
new file mode 100755
index 0000000000..5cd6aafc7d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/pro.163.com/js.ng/site=netease&affiliate=homepage&cat=homepage&type=flash&location=1
@@ -0,0 +1 @@
+ void('<HTML>\n <BODY>\n'); void('<script src=\'http://pre.ra.icast.cn/a/1/6/5/5/16206.js\'></script>'); void('\n </BODY>\n</HTML>'); \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/qn.163.com/images/qnyh20110411.jpg b/mobile/android/tests/browser/chrome/tp5/163.com/qn.163.com/images/qnyh20110411.jpg
new file mode 100755
index 0000000000..9fcba13733
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/qn.163.com/images/qnyh20110411.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/show.mediav.com/s@type=1&db=mediav&pub=118_2620_36413&cus=0_0_0_0_0&wh=360x100&btype=1&js=1.html b/mobile/android/tests/browser/chrome/tp5/163.com/show.mediav.com/s@type=1&db=mediav&pub=118_2620_36413&cus=0_0_0_0_0&wh=360x100&btype=1&js=1.html
new file mode 100755
index 0000000000..efee431543
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/show.mediav.com/s@type=1&db=mediav&pub=118_2620_36413&cus=0_0_0_0_0&wh=360x100&btype=1&js=1.html
@@ -0,0 +1,8 @@
+void("<SCRIPT LANGUAGE='Javascript'>");
+void("mvas_14576=36413;mv_acquire=1;mv_bid=14576;");
+void("mvcu_14576='http://show.mediav.com/c?type=2&db=mediav&pub=118_2620_36413&cus=6_221_7977_14576_0&ref=http://www.163.com/&url=http://www.masamaso.com/interface.php?id=103387&url=http://www.masamaso.com?from=r_wy_sy04ztlfdx';");
+void("</SCR"+"IPT>");
+
+function getbrowse(){var info=navigator.userAgent.toLowerCase();if(info.indexOf('msie')!=-1)return 'IE';if(info.indexOf('firefox')!=-1)return 'FF';if(info.indexOf('opera')!=-1)return 'OP';if(info.indexOf('chrome')!=-1)return 'CHROME';if(info.indexOf('safari')!=-1)return 'SAFARI';}function mvflash_make(){var swfroot=arguments[0];if(swfroot=="body")swfroot=(document.compatMode&&document.compatMode!="BackCompat")?document.documentElement:document.body;var swfid=arguments[1];var swfwidth=arguments[2];var swfheight=arguments[3];var swfsrc=arguments[4];var clickurl=arguments[5];var swfwmode=arguments[6];var type=arguments[7];var strflash='<objectdisabled id='+swfid+' codeBase="httpdisabled://downloaddisabled.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=7" classid=clsid:D27CDB6E-AE6D-11cf-96B8-444553540000 width='+swfwidth+' height='+swfheight+' type=application/x-shockwave-flash><PARAM NAME="Movie" VALUE="'+swfsrc+'"><PARAM NAME="FlashVars" VALUE="mv_clickurl='+escape(clickurl)+'"><PARAM NAME="WMode" VALUE="'+swfwmode+'"><PARAM NAME="Quality" VALUE="High"><PARAM NAME="AllowScriptAccess" VALUE="always"><PARAM NAME="Scale" VALUE="ShowAll"><PARAM NAME="AllowNetworking" VALUE="all"><PARAM NAME="AllowFullScreen" VALUE="false"><embeddisabled id="'+swfid+'" width="'+swfwidth+'px" height="'+swfheight+'px" src="httpdisabled://show.mediav.com/'+swfsrc+'" quality="High" pluginspage="httpdisabled://www.macromedia.com/go/getflashplayer" type="application/x-shockwave-flash" wmode="'+swfwmode+'" allowscriptaccess="always" FlashVars="mv_clickurl='+escape(clickurl)+'"></embed></OBJECT>';if(type==1){var strdiv=strflash;}else{var strdiv='<div style="position: relative; z-index: 1; width:'+swfwidth+'px; height:'+swfheight+'px;"><div style="position: absolute; left: 0pt; top: 0pt; z-index: 2; width:'+swfwidth+'px; height:'+swfheight+'px;">';strdiv+=strflash;strdiv+='</div><a id="mvclicka" target="_blank" href="httpdisabled://show.mediav.com/'+clickurl+'"><img border="0" style="position: absolute; left: 0px; top: 0px; z-index: 3; width:'+swfwidth+'px;height:'+swfheight+'px;" src="httpdisabled://static.mediav.com/1x1.gif"/></a></div>';}if(swfroot==""){void(strdiv);}else{swfroot.innerHTML=strdiv;}}function mvflash_make_button(){var swfroot=arguments[0];if(swfroot=="body")swfroot=(document.compatMode&&document.compatMode!="BackCompat")?document.documentElement:document.body;var swfid=arguments[1];var swfwidth=arguments[2];var swfheight=arguments[3];var swfsrc=arguments[4];var clickurl=arguments[5];var swfwmode=arguments[6];var type=arguments[7];var strflash='<objectdisabled id='+swfid+' codeBase="httpdisabled://downloaddisabled.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=7" classid=clsid:D27CDB6E-AE6D-11cf-96B8-444553540000 width='+swfwidth+' height='+swfheight+' type=application/x-shockwave-flash><PARAM NAME="Movie" VALUE="'+swfsrc+'"><PARAM NAME="FlashVars" VALUE="mv_clickurl='+escape(clickurl)+'"><PARAM NAME="WMode" VALUE="'+swfwmode+'"><PARAM NAME="Quality" VALUE="High"><PARAM NAME="AllowScriptAccess" VALUE="always"><PARAM NAME="Scale" VALUE="ShowAll"><PARAM NAME="AllowNetworking" VALUE="all"><PARAM NAME="AllowFullScreen" VALUE="false"><embeddisabled id="'+swfid+'" width="'+swfwidth+'px" height="'+swfheight+'px" src="httpdisabled://show.mediav.com/'+swfsrc+'" quality="High" pluginspage="httpdisabled://www.macromedia.com/go/getflashplayer" type="application/x-shockwave-flash" wmode="'+swfwmode+'" allowscriptaccess="always" FlashVars="mv_clickurl='+escape(clickurl)+'"></embed></OBJECT>';if(type==1){var strdiv=strflash;}else{var strdiv='<a href="httpdisabled://show.mediav.com/'+clickurl+'" target="_blank" hidefocus>';if(getbrowse()=="IE"){strdiv+='<button disabled style="width:'+swfwidth+';height:'+swfheight+';border:none">';}strdiv+=strflash;strdiv+='</a>';}if(swfroot==""){void(strdiv);}else{swfroot.innerHTML=strdiv;}}eval('mediav_fini'+mvas_14576+'=1');var mediav_fini2010010688=1;void("<SCRIPT LANGUAGE=\"JavaScript\">\n");
+void(" if(typeof(sinamvadflag0688)!=\"number\"){mvflash_make(\"\",\"mv_14576\",360,100,\"httpdisabled://img1.126.net/channel5/360/masa_360100_10406.swf\",mvcu_14576,\"Opaque\",2);}\n");
+void("</SCR"+"IPT>");
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/www.163.com/index.html b/mobile/android/tests/browser/chrome/tp5/163.com/www.163.com/index.html
new file mode 100755
index 0000000000..7d457afda0
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/www.163.com/index.html
@@ -0,0 +1,4024 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
+<title>ÍøÒ×</title>
+<base target="_blank" />
+<meta content="initial-scale=1.0, maximum-scale=2.0, minimum-scale=1.0, user-scalable=yes, width=device-width" name="viewport">
+<meta name="Keywords" content="ÍøÒ×,ÓÊÏä,ÓÎÏ·,ÐÂÎÅ,ÌåÓý,ÓéÀÖ,Å®ÐÔ,ÑÇÔË,ÂÛ̳,¶ÌÐÅ,ÊýÂë,Æû³µ,ÊÖ»ú,²Æ¾­,¿Æ¼¼,Ïà²á" />
+<meta name="Description" content="ÍøÒ×ÊÇÖйúÁìÏȵĻ¥ÁªÍø¼¼Êõ¹«Ë¾£¬ÎªÓû§ÌṩÃâ·ÑÓÊÏä¡¢ÓÎÏ·¡¢ËÑË÷ÒýÇæ·þÎñ£¬¿ªÉèÐÂÎÅ¡¢ÓéÀÖ¡¢ÌåÓýµÈ30¶à¸öÄÚÈÝƵµÀ£¬¼°²©¿Í¡¢ÊÓƵ¡¢ÂÛ̳µÈ»¥¶¯½»Á÷£¬Íø¾ÛÈ˵ÄÁ¦Á¿¡£" />
+<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7" />
+<meta name="robots" content="index, follow" />
+<meta name="googlebot" content="index, follow" />
+<link rel="apple-touch-icon-precomposed" href="httpdisabled://img1.cache.netease.com/www/logo/logo-ipad-icon.png" >
+<script charset="gb2312" src="../img3.cache.netease.com/cnews/js/ntes_jslib_1.x.js" language="javascript" type="text/javascript"></script>
+<link id="setSkin" type="text/css" rel="stylesheet" href="../img2.cache.netease.com/www/v2011/css/theme_blue1227.css" />
+<style type="text/css">
+html {overflow-y:scroll;}
+body {margin:0; padding:29px 0 0; font:12px "\5B8B\4F53",san-serif;background:#ffffff;}
+div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,textarea,blockquote,p{padding:0; margin:0;}
+table,td,tr,th{font-size:12px;}
+li{list-style-type:none;}
+img{vertical-align:top;border:0;}
+ol,ul {list-style:none;}
+h1,h2,h3,h4,h5,h6 {font-size:12px; font-weight:normal;}
+address,cite,code,em,th {font-weight:normal; font-style:normal;}
+.ntes-passport a {color:#727171;}
+.ntes-passport a:hover {color:#ba2636;}
+.fB{font-weight:bold;}
+.f12px{font-size:12px;}
+.f14px{font-size:14px;}
+.left{float:left;}
+.right{float:right;}
+.I_V_ {background:url(../img1.cache.netease.com/img09/icon/icon.png) left center no-repeat; padding-left:18px;}
+.I_M_ {background:url(../img1.cache.netease.com/img09/icon/icon.png) -457px center no-repeat; padding-left:18px;}
+.I_cleardot_ {background:#fff; padding:0 0 0 10px; margin:0 6px 0 -10px; float:left; vertical-align:middle; cursor:pointer;}
+.I_cleardot_:hover {text-decoration:underline;}
+.foot{margin:0 auto; width:960px; line-height:21px; clear:both; color:#4d4d4d;}
+.foot .text{padding:4px 0 5px; border-bottom:1px solid #4d4d4d; margin:0 0 5px;}
+.foot a,.foot a:visited{color:#4d4d4d;}
+.foot a:hover {color:#ba2636;}
+/* ntes grid u */
+.grid-u-5,.grid-u-9,.grid-u-10,.grid-u-19 {float:left;_display:inline;}
+.grid-u-5,.area-sub {float:left; width:200px; _display:inline;}
+.grid-u-19,.area-main {float:right; width:760px;}
+.grid-u-9,.main-col-9 {float:left; width:360px;}
+.grid-u-10,.main-col-10 {float:left; width:400px;}
+.clearfix,.area,.header,.footer,.content,.area-sub,.area-main,.main-col-10,.main-col-9,.tab-con {*zoom:1;}
+.clearfix:after,.area:after,.header:after,.footer:after,.content:after,.area-sub:after,.area-main:after,.main-col-10:after,.main-col-9:after,.tab-con:after {display:block; overflow:hidden; clear:both; height:0; visibility:hidden; content:".";}
+.header,.content,.footer {clear:both; width:960px; margin:0 auto;}
+.header,.footer {margin-bottom:10px;}
+.main-col-10 .mod,.area-sub .mod {margin-right:10px;}
+/* link css */
+a {color:#2b2b2b; text-decoration:none;}
+a:visited {text-decoration:none;}
+a:hover {color:#ba2636;text-decoration:underline;}
+a:active {color:#ba2636;}
+.sub-list a,.sub-list a:visited {color:#585858;}
+.cDGray,.cDGray:visited,.cDGray a{color:#727171;}
+.cBlue,.cBlue:visited,.cBlue a{color:#1E50A2;}
+.cDRed,.cDRed:visited,.cDRed a{color:#ba2636;}
+.cRed {color:#ff0000;}
+.tab-u a:hover,.cRed a:hover,a.cRed:hover,.sub-list a:hover,.cBlue a:hover,a.cBlue:hover,.cDRed a:hover,a.cDRed:hover,.cGray a:hover,a.cGray:hover,.cDGray a:hover,a.cDGray:hover,.cWhite a:hover,a.cWhite:hover,.cBlack a:hover,a.cBlack:hover,.cGreen a:hover,a.cGreen:hover,.cYellow a:hover,a.cYellow:hover{color:#ba2636;}
+/* templet */
+.imgText-temp-1 {overflow:hidden; padding-left:132px; margin-bottom:3px;}
+.imgText-temp-1 .main-img {position:absolute; _display:inline; margin-left:-132px;}
+.imgText-temp-1 .main-list {float:left;}
+.imgText-temp-2 {overflow:hidden; padding:0 0 10px 80px;}
+.imgText-temp-2 .imgText-img {float:left; _display:inline; margin-left:-80px; border:1px solid #dcdddd;}
+.imgText-temp-2 .imgText-titleTop {margin:6px 0 3px -80px; line-height:21px; font-weight:bold;}
+.imgText-temp-2 .sub-list {float:left;}
+.imgText-temp-3 {overflow:hidden; padding-left:132px; margin-bottom:15px;}
+.imgText-temp-3 .main-img {float:left; _display:inline; margin-left:-132px;}
+.imgText-temp-3 .main-list {float:left;}
+.temp-1-2 {margin-top:6px;}
+.temp-1-2 .temp-u {_display:inline; width:180px;}
+/* ntes modules mod */
+.user-logined,.login-name,.logined-begin,.logined-after,.main-list li,.specialTopic-list li,.sub-list li,.stock-search .ui-btn-submit,.i-setIndex,.i-help,.i-rss,.ui-select-box,.ui-btn-login,.ntes-passport,.product-item-mail .icon,.product-item-game .icon,.product-item-serv .icon,.product-item-reco .icon,.theme-blue,.theme-gray,.theme-green,.theme-pink,.theme-yellow {background:url("../img1.cache.netease.com/www/v2011/img/iconv0.0.7.png") no-repeat;}
+.mod .hd,.mod-function {background-position:left -30px;}
+.mod {margin-bottom:10px;}
+.mod .hd {height:24px; line-height:24px;}
+.mod .bd {overflow:hidden;}
+.mod-title {float:left; font-weight:bold; text-indent:10px;}
+.mod-entry {float:right; padding-right:6px;}
+ .area-main .bd {padding:12px 8px 0 10px;}
+ .area-sub .bd {padding:12px 9px 0;}
+.main-title {height:22px; line-height:25px; margin-bottom:3px; overflow:hidden; fo nt-family:\9ED1\4F53; font-size:14px; font-weight:bold;}
+.main-subtitle {height:19px; overflow:hidden; font-weight:bold; font-size:14px; line-height:19px;}
+.mod-list {margin-bottom:4px; width:100%;}
+.mod-list li {overflow:hidden; clear:both;}
+.mod-list .dotline {margin-bottom:4px; padding-bottom:4px;}
+ .main-list li,.specialTopic-list li {padding-left:12px; height:25px; font-size:14px; line-height:25px;}
+ .main-list li {background-position:-124px -80px;}
+ .specialTopic-list li {background-position:-123px -112px;}
+ .sub-list li {padding-left:12px; height:21px; line-height:21px; background-position:-124px -82px;}
+ .sub-list .title {background:none; padding-left:0; font-weight:bold;}
+ .trends-list li {height:21px; line-height:21px;}
+.mod-img img {clear:both;}
+.mod-img p {margin:1px 0 0; line-height:19px;}
+.mod-img cite {clear:both; float:left; width:100%; height:14px; overflow:hidden; margin-top:9px; cursor:pointer;}
+ .main-img {overflow:hidden; width:122px;}
+ .main-img img { border:1px solid #dcdddd;}
+ .main-img .stock-img {border:none;}
+ .sub-img {overflow:hidden; width:170px; margin-bottom:9px;}
+.mod-imgText {clear:both; margin:0 0 4px; line-height:0; zoom:1;}
+ .imgText-img {float:left; line-height:1em;}
+ .imgText-title {line-height:1.5; font-weight:bold;}
+ .imgText-digest {margin:3px 0 0;line-height:21px;text-indent:2em;}
+.mod-imgList {float:left; _display:inline;}
+.mod-imgList li {float:left; overflow:hidden; _display:inline;}
+.mod-imgList img {border:1px solid #dcdddd;}
+.mod-imgList p {line-height:19px; margin-top:4px;}
+ .imgList-w80 {margin:0 0 4px -14px;}
+ .imgList-w80 li {margin:0 0 13px 15px; width:80px; height:103px;}
+ .imgList-w80 img {border:none;}
+ .imgList-w80_1 {margin:9px 0 4px -14px;}
+ .imgList-w80_1 li {margin:0 0 5px 15px; width:80px; height:103px;}
+ .imgList-w80_1 img {border:none;}
+ .imgList-w90 {margin:0 0 4px -27px;}
+ .imgList-w90 li {margin:0 0 4px 29px; width:92px; height:115px;}
+ .imgList-w120 {margin-left:-2px;}
+ .imgList-w120 li {margin:0 0 25px 2px; width:122px; height:115px;}
+ .imgList-w160 {margin-left:-16px;}
+ .imgList-w160 li {margin:0 0 4px 16px; width:162px; height:115px;}
+ .imgList-w160-2 {margin-left:-16px; margin-bottom:5px;}
+ .imgList-w160-2 li {margin:0 0 4px 16px; width:162px; height:123px;}
+.mod-keyword {clear:both;}
+ .keyword-hd {margin-bottom:3px; line-height:21px;}
+ .keyword-bd {float:left; _display:inline; margin-left:-25px; line-height:23px;}
+ .keyword-bd a {float:left; _display:inline; white-space:nowrap; margin-left:25px; font-size:14px;}
+.custom-mod-hd {height:26px; line-height:26px;}
+.custom-title {float:left;}
+.custom-entry {float:right;}
+.specialTopic {margin-top:4px;}
+.specialTopic-hd {margin-bottom:6px;}
+.specialTopic-title {height:20px; overflow:hidden; padding-top:5px;}
+.product-mod {margin-bottom:10px;}
+.product-mod .bd {padding:0;}
+.trends-mod .bd {padding:5px 9px;}
+.mod-function {height:19px; overflow:hidden; margin-bottom:6px; line-height:20px;}
+.function-date {float:left; _display:inline; margin:0 0 0 6px;}
+.function-close,.function-info {float:right;}
+.function-close {cursor:pointer; _display:inline; width:47px; height:17px; line-height:17px; overflow:hidden; visibility:hidden; text-indent:12px; margin:1px 1px 0 6px; background-position:-101px 0;}
+/* ntes widget wgt */
+.wgt-tab {clear:both;}
+.tab-u {float:left; text-align:center; cursor:pointer; font-family:Verdana,\5B8B\4F53,san-serif;}
+.tab-hd {overflow:hidden; height:26px; line-height:25px; background-position:right -29px;}
+.tab-hd .current{font-weight:bold;}
+.tab-u-10 .tab-u {width:96px; font-size:14px;}
+.tab-u-10 .current {width:97px; background:#fff;}
+.tab-u-5 .tab-u {width:46px;}
+.tab-u-5 .current {width:47px;}
+.tab-u-9 .tab-u {width:89px; font-size:14px;}
+.tab-u-9 .current {width:88px; background:#fff;}
+.product-tab .tab-u {width:93px;}
+.product-tab .current {width:94px;}
+.stock-search {clear:both; width:100%; height:19px;}
+.stock-search label {position:absolute; left:0; top:0; color:#999; line-height:19px; text-indent:3px;}
+.stock-search-input {float:left; height:17px; width:100px; padding-right:20px;}
+.stock-search .ui-btn-submit {float:left; width:16px; height:13px; margin:3px 0 0 -19px; text-indent:-100em; overflow:hidden; background-position:-46px -190px;}
+/* common */
+.main-title .I_V_ {font-weight:bold;}
+.imgText-title .I_V_ {font-weight:bold;}
+.entry {line-height:21px;}
+.entry a {height:21px; overflow:hidden;}
+.display-control .tab-con {display:none;}
+.display-control .current {display:block;}
+.dotline {border-bottom:1px dotted #dcdddd;}
+.new {text-decoration:underline;}
+.attitude {float:left; padding-right:63px; background:url("../img1.cache.netease.com/www/v2011/img/attr.png") no-repeat scroll right 1px transparent;}
+.gg,.gg_reset {background:#E6F4FF;}
+.gg {margin-bottom:10px;}
+.mb-6 {margin-bottom:6px;}
+.mb-12 {margin-bottom:12px;}
+.blank3 {clear:both; height:3px; overflow:hidden; display:block;}
+.blank6 {clear:both; height:6px; overflow:hidden; display:block;}
+.code-en {font:13px "Verdana";}
+.area-sub .gg {width:190px;}
+.main-col-9 .gg {width:360px;}
+.main-col-10 .gg {width:390px;}
+.gg-h65 {height:65px;}
+.gg-h180 {height:180px;}
+.gg-h100 {height:100px;}
+.gg-h300 {height:300px;}
+.gg-h290 {height:290px;}
+.gg-h65,.gg-h100,.gg-h180,.gg-h290,.gg-h300 {overflow:hidden; clear:both;}
+.ggw600h80 {width:600px; height:80px; overflow:hidden;}
+.area-main ul.dotline {padding-bottom:4px;}
+.area-sub ul.dotline {padding-bottom:4px;}
+.main-bn-blog {margin-bottom:18px;}
+/* ntes snippet */
+ h1 {display:none;}
+ .logo {float:left; _display:inline; margin: 19px 7px 0 16px;}
+ .channel {float:right; height:80px; overflow:hidden;}
+ .channel-col {float:left; _display:inline; margin:10px 0 0 -1px; padding:0 0 0 16px; line-height:20px; border-left:1px solid #dcdddd;}
+ .channel-col a {float:left; margin:0 6px 0 0; width:24px;}
+ .channel-col .w3 {float:left; margin:0 6px 0 0; width:36px;}
+ .channel-col strong {float:left; margin-right:6px;}
+ .channel-col strong a {width:26px;}
+.col-w .nowrap {float:left; width:138px;}
+.col .nowrap {float:left; width:108px;}
+.col-w3 .nowrap {float:left; width:120px;}
+/* ntes snippet passport */
+.ui-pos-rel {position:relative; z-index:1;}
+.ui-pos-abs {position:absolute; display:none;}
+.ui-pos-active {visibility:visible;}
+.ui-ipt-enter[data-state="disable"] {background:#fbfbfb; border:1px solid #e3e3e3;}
+.ui-ipt-enter[data-state="error"] {background:#FFFCF0; border:1px solid #e3e3e3;}
+.ui-ipt-enter {border-style:solid; border-width:1px; border-color:#BABABA #E3E3E3 #E3E3E3 #BABABA; font-family:Verdana,san-serif,\5B8B\4F53; color:#2b2b2b; background:#fff;}
+.ui-btn-submit {padding:0; border:none; cursor:pointer;}
+.ui-btn-submit[data-state="disable"] {border:1px solid #ccc; color:#727171; cursor:default; background:#f3f3f3;}
+.ntes-passport {position:absolute; left:0; top:0; z-index:2; clear:both; width:100%; min-width:960px; height:29px; color:#727171; background-position:left bottom; background-repeat:repeat-x;}
+ .passport-main {width:960px; margin:0 auto;}
+ .passport-login {float:left;}
+ .login-info {line-height:29px;}
+ .passport-login .label {overflow:hidden; float:left; margin:4px 0 0; height:20px; padding:2px 0 0; line-height:16px;}
+ .passport-entry {float:right; line-height:29px;}
+ .i-setIndex {padding:2px 0 0 15px; cursor:pointer; background-position:-117px -28px;}
+ .i-help {padding:2px 0 0 20px; background-position:-113px 3px;}
+ .i-rss {float:right; margin:7px 0 0 6px; width:23px; height:13px; overflow:hidden; text-indent:-100em; background-position:left -90px;}
+.ui-select {float:left;}
+ .form-ipt {float:left; _display:inline; overflow:hidden; margin:5px 5px 0; font-size:12px;}
+ .form-ipt input {float:left; overflow:hidden; font-size:12px; height:16px; line-height:16px;}
+ .form-ipt-user,.form-ipt-pwd {width:100px;}
+ .ui-btn-login {_display:inline; float:left; width:39px; height:18px; margin:5px 11px 0 6px; font-size:12px; color:#727171; cursor:pointer; border:none; background-position:-5px -190px;}
+ .ui-select-box {position:relative; z-index:1; width:79px; height:18px; margin:5px 0 0 0; line-height:18px; text-align:left; background-position:left -150px;}
+ .ui-select-selected {display:block; overflow:hidden; width:79px; height:18px; text-indent:3px; cursor:pointer;}
+ .ui-select-list {display:none; width:112px; position:absolute; top:17px; left:0; border:1px solid #b4b4b4; cursor:pointer; background:#fff;}
+ .ui-select-list li {margin:1px; text-indent:5px; line-height:21px; color:#4b4b4b;}
+ .ui-select-list .disable {color:#ccc; cursor:default;}
+ .ui-select-list .interval {border-bottom:1px solid #c4c4c4;}
+ .ui-select-list a {clear:both; display:block; width:110px; height:18px; text-indent:2px; cursor:pointer; background:#fff; color:#2b2b2b;}
+ .ui-select-list a:visited {color:#2b2b2b;}
+ .ui-select-list a:hover {text-decoration:none; color:#fff; background:#85B6EA;}
+.product-item-mail,.product-item-game,.product-item-serv,.product-item-reco {clear:both; padding:7px 0 7px 45px;}
+.product-item-mail strong,.product-item-game strong,.product-item-serv strong,.product-item-reco strong {float:left; _display:inline; width:40px; height:100%; margin-left:-45px; overflow:hidden;}
+.product-item-mail a,.product-item-game a,.product-item-serv a,.product-item-reco a {float:left; width:70px; overflow:hidden; line-height:19px;}
+ .product-item-mail .icon,.product-item-game .icon,.product-item-serv .icon,.product-item-reco .icon {width:30px;}
+ .product-item-mail .icon {margin:1px 0 0 9px; padding-top:18px; background-position:1px 2px;}
+ .product-item-game .icon {margin:20px 0 0 8px; padding-top:21px; background-position:left -50px;}
+ .product-item-serv .icon {margin:0 0 0 7px; padding-top:24px; background-position:-28px top;}
+ .product-item-reco .icon {margin:10px 0 0 7px; padding-top:20px; background-position:-27px -50px;}
+ .product-item-game,.product-item-reco {background:#fff;}
+ .product-item-serv {height:45px; overflow:hidden; padding-bottom:0;}
+ .product-item-reco {height:82px; overflow:hidden;}
+.product-list li {float:left; width:100%; height:27px; overflow:hidden; background-position:-202px -2px; }
+.item-mail,.item-microBlog,.item-blog,.item-photo,.item-money,.item-tie,.item-wan,.item-book,.item-yuehui,.item-caipiao
+{display:block; clear:both; float:left; padding-left:30px; height:28px; line-height:28px; background:url("../img1.cache.netease.com/www/v2011/img/icon_product_listv0.0.3.png") left top no-repeat;}
+.item-mail {background-position:6px 8px;}
+.item-microBlog {background-position:6px -19px;}
+.item-blog {background-position:6px -47px;}
+.item-photo {background-position:6px -74px;}
+.item-money {background-position:6px -103px;}
+.item-tie {background-position:4px -132px;}
+.item-wan {background-position:4px -160px;}
+.item-book {background-position:4px -186px;}
+.item-yuehui {background-position:4px -215px;}
+.item-caipiao {background-position:4px -243px;}
+.product-entry {_display:inline; clear:both; float:right; margin-right:6px; height:30px; line-height:30px;}
+/* end */
+.ntes-yodao {position:relative; clear:both; z-index:1; height:63px; width:958px; border-style:solid; border-width:1px;}
+.yodao-main {float:right; width:818px;}
+.yodao-sub {float:left; _display:inline; width:140px;}
+ .yodao-logo {float:left; width:114px; height:23px; overflow:hidden; margin-top:29px; text-indent:-100em;}
+ .wgt-yodao-weather {visibility:hidden; float:left; width:140px; margin:9px 0 0;}
+ .yodao-weather-info {width:70px; float:left;}
+ .weather-location {padding-right:10px; float:right; clear:both; cursor:pointer; height:21px; overflow:hidden; line-height:21px; background-position:right -75px;}
+ .weather-temperature {float:right; width:100%; text-align:right; clear:both; line-height:16px;}
+ .yodao-weather-icon {width:65px; padding-left:5px; line-height:20px; text-align:center; float:left;}
+ .wgt-yodao-search {float:left; _display:inline; width:460px; margin:6px 0 0 4px;}
+ .wgt-yodao-search label {display:none;}
+ .wgt-yodao-search .ui-ipt-enter {float:left; padding:3px 0 0 3px; width:380px; height:21px; font-size:14px; border-left:1px solid #8c8c8c; border-top:1px solid #8c8c8c; border-bottom:1px solid #cbccce; border-right:none;}
+ .wgt-yodao-search .ui-btn-submit {float:left; width:67px; height:26px; text-align:center; line-height:26px; font-size:14px; font-weight:bold; background-position:left top;}
+ .yodao-search-category {height:20px; margin-bottom:3px;}
+ .yodao-search-category .current,.search-category-item a:hover {font-weight:bold; color:#fff; background-position:-67px top;}
+ .search-category-item a:hover {text-decoration:none;}
+ .search-category-item a {float:left; padding:2px 0 4px; width:34px; text-align:center; margin-right:8px; cursor:pointer;}
+ .search-category-more {float:left; margin:2px 0 0 5px; padding-right:10px; margin-right:8px; cursor:pointer; background-position:right -78px;}
+ .yodao-search-category label {display:none;}
+ .category-more-list {width:72px; left:0; top:15px; zoom:1; z-index:2;}
+ .category-more-list a {clear:both; width:100%; _height:170px; float:left; height:21px; line-height:21px; text-indent:3px;}
+ .category-more-list a:hover {color:#fff; text-decoration:none;}
+ .category-more-list .interval {border-bottom:1px solid #dcdddd;}
+ .yodao-entry {float:right; width:225px;}
+ .yodao-entry-mine {display:block; width:110px; height:19px; text-align:center; line-height:19px;}
+ .yodao-entry-link {visibility:hidden; margin:13px 0 0; height:14px; overflow:hidden;}
+ .wgt-theme {position:absolute; right:10px; top:6px; height:10px; overflow:hidden;}
+ .theme-blue,.theme-gray,.theme-green,.theme-pink,.theme-yellow {_display:inline; float:left; width:11px; height:21px; overflow:hidden; margin-left:8px; cursor:pointer;}
+ .theme-blue {background-position:-26px -168px;}
+ .theme-gray {background-position:-15px -168px;}
+ .theme-green {background-position:-4px -168px;}
+ .theme-pink {background-position:-48px -168px;}
+ .theme-yellow {background-position:-37px -168px;}
+.wgt-theme .current {margin-top:-11px;}
+.yodao-dialog {width:135px; left:-1px; top:21px;}
+.yodao-dialog .hd {height:20px; padding:0 3px; line-height:20px;}
+ .yodao-dialog-title {float:left;}
+ .yodao-dialog-close {float:right; _display:inline; width:8px; height:12px; text-indent:-100em; overflow:hidden; margin:6px 2px 0 0; cursor:pointer; background-position:-139px -17px;}
+.yodao-dialog .bd {height:76px; padding:6px 12px 12px;}
+ .yodao-dialog-item {float:left; display:block; height:20px; margin:3px 0 0;}
+ .yodao-dialog select {width:64px;}
+.yodao-dialog .ui-btn-submit {width:54px; height:21px; margin:9px 0 0 36px; background-position:-148px top;}
+.aboutNetease{height:25px; margin-bottom:10px;}
+.aboutNetease li{padding:4px 0 0; color:#1E50A2;}
+.aboutNetease li a{color:#1E50A2; padding:0 5px;}
+.aboutNetease li a:visited{ color:#1E50A2;}
+.footer {padding:0 0 5px; text-align:center; line-height:21px; color:#000;}
+.footer a,.footer a:visited {color:#1E50A2;}
+.footer img{margin:4px 0 0;}
+.gg-g24 {clear:both; height:139px; overflow:hidden; margin:0 auto 10px; text-align:left; border:1px solid #dcdddd;}
+ .tab-hd-gg-left,.tab-hd-gg-right {_display:inline; width:22px; height:138px; overflow:hidden;}
+ .tab-hd-gg-left .current,.tab-hd-gg-right .current {background-image:url("../img1.cache.netease.com/www/v2011/img/iconv0.0.7.png"); background-repeat:no-repeat;}
+ .tab-hd-gg-left li,.tab-hd-gg-right li {width:11px; overflow:hidden; overflow:hidden; margin-top:1px; line-height:14px; cursor:pointer;}
+ .tab-hd-gg-left {float:left; margin-left:1px;}
+ .tab-hd-gg-left li {padding:8px 4px 9px 3px; text-align:left;}
+ .tab-hd-gg-left .current {padding-right:8px; background-position:18px -168px;}
+ .tab-hd-gg-right {float:right; margin-right:1px;}
+ .tab-hd-gg-right li {float:right; padding:8px 3px 9px 4px; text-align:right;}
+ .tab-hd-gg-right .current {padding-left:8px; background-position:-126px -168px;}
+ .link_gg {line-height:21px;}
+.tab-bd-gg,.gg-g24-main {float:left; overflow:hidden;}
+.tab-bd-gg {width:137px; margin:7px 0 0 5px;}
+.tab-bd-gg {line-height:21px;}
+.gg-g24-main {width:600px; height:129px; margin:5px 10px;}
+ .g24-main-textTop{display:block; overflow:hidden; margin:2px 0 1px; height:21px;}
+ .g24-main-textBottom{display:block; overflow:hidden; margin:3px 0 0 0; height:21px;}
+ .g24-main-textTop a,.g24-main-textBottom a {float:left; overflow:hidden; width:120px; line-height:21px;}
+#g5n1 .bd,#gy .bd,#g5n2 .bd,#recommend .bd {height:261px;}
+#g5n3 .bd {height:261px;}
+#product .bd {height:300px;}
+#news .bd {height:598px;}
+#vedio .bd {height:429px;}
+#moneySub .bd {height:223px;}
+#nie .bd {height:269px;}
+#trends .bd {height:63px;}
+#sportSub .bd,#gameSub .bd {height:221px;}
+#money .bd,#auto .bd,#sports .bd,#house .bd,#ent .bd,#game .bd,#tech .bd,#book .bd,#lady .bd,#t .bd {height:341px;}
+#market .bd,#blog .bd,#hz .bd,#bbs .bd,#marketSub .bd,#siteRank .bd {height:261px;}
+/* neteasy_mall */
+#neteasy_mall{height:273px;line-height:26px;padding-right:0;}
+#neteasy_mall .dotline{ margin-bottom:5px;width:170px;line-height:1px;font-size:1px;height:1px;}
+.neteasy_mall_title{color:#2B2B2B;font-weight:bold;line-height:18px;margin-top:-5px;}
+.neteasy_mall_phone,.neteasy_mall_value{width:93px;height:19px;border:1px solid #86A2BD;color:#727171;padding-left:5px;}
+.neteasy_mall_phone{*padding-top:2px;*height:17px;}
+.neteasy_mall_value{width:60px;height:20px;padding-left:0}
+.neteasy_mall_submit{width:35px;height:20px;background:url("../img1.cache.netease.com/www/v2011/img/neteasy_mallv0.0.1.png") no-repeat;border:none;color:#BA2636;cursor:pointer;margin-left:5px;}
+#phone_err{width:14px;height:14px;line-height:1px;font-size:1px;display:inline-block;background:url("../img1.cache.netease.com/www/v2011/img/neteasy_mallv0.0.1.png") -39px 0 no-repeat;margin-left:3px;}
+.mall_flash{margin-bottom:8px;}
+.mall_price{_line-height:30px}
+.mall_img{margin-top:-2px;*margin-top:7px;}
+/* youdao start by jiemengen */
+.remindtt75 {font-size:100%; padding:2px;}
+.remindtt752 {font-size:100%; padding-left:2px; padding-right:2px;color:#9E9E9E}
+.jstxhuitiaozuo {background-color:#eaf1fd; color:#8D9DBE; font-size:100%; padding-left:5px; padding-top:1px;}
+.jstxhuitiaoyou {font-size:100%;padding-right:5px;}
+.jstxlan {font-size:100%;cursor:pointer}
+.jstxlan:hover {text-decoration:underline;}
+.aa_highlight {color:#fff;}
+/* ¹ÉƱËÑË÷µ¯´° */
+.tcbox { float:left; position: absolute; width: 270px; z-index: 99; top: 25px; left: 0; margin-left:0px; margin-top:6px; border: 1px solid #DCDDDD; background:#FFFFFF;}
+.tbText { border-collapse: collapse; line-height: 20px; font-size: 12px; text-align: left; color:#2B2B2B; width: 268px; margin-top:3px;}
+.tbText th,.tbText td { height: 20px; padding: 0 6px; text-align: left;}
+.tbText th { background: #E2F7FD; color:#1E50A2; font-weight:normal;}
+.tbText .nline {padding: 1px; color:#2B2B2B; display: block; outline-color: -moz-use-text-color; outline-style: none; outline-width: medium; text-decoration: none; width: 100%;}
+.tbText tr.alter td { background: #CCEDF7; }
+.tbText tr:hover {background: #CCEDF7;}
+.login-after-inner {position:relative; z-index:10; float:left; margin-right:9px;}
+.login-after-select {display:none; position:absolute; width:100%; _width:170px; left:0; top:23px; overflow:hidden; background:#fff; line-height:18px; text-align:left; color:#2b2b2b;}
+.login-after-select .user-entry {display:block; border:1px solid #dcdddd; padding:1px;}
+.login-after-select a {display:block; clear:both; width:100%; _width:166px; height:21px; line-height:21px; text-indent:3px; font-weight:normal; color:#2b2b2b;}
+.login-after-select a:hover{ background-color:#4472AE; color:#fff; text-decoration: none; }
+#NTES_Login { position:relative; }
+.login-tips { background:#FFFCF0; border:1px solid #dcdddd; padding:0px 2px; height:16px; line-height:16px; position:absolute; top: 22px; left: 29px; display: none; }
+.login-tips a.cBlue{ color: #1E50A2; }
+.login-tips a.cBlue:hover { color:#BA2636; }
+.login-tips a.login-tips-close{ color: #999; }
+.login-tips a.login-tips-close:hover{ color: #000; text-decoration: none; }
+.login-auto-list{ border-collapse: collapse; background: #fff; position: absolute; left: 29px; top: 22px; border: 1px solid #E3E3E3; z-index: 10; }
+.login-auto-list th,.login-auto-list td{ line-height: 22px; height: 22px; padding: 0 5px; font-family: Verdana,san-serif,\5B8B\4F53; }
+.login-auto-list td{cursor:pointer;}
+.login-auto-list th{text-align:left; background-color:#F6F6F6;}
+.login-auto-list td.hover{ background-color:#4472AE; color:#fff; }
+.ui-select-list li.hover{ background-color:#4472AE; color:#fff; }
+.ui-select-list, .ui-select-box{ }
+.login-tips-username{ z-index: 5; font-family: Verdana,san-serif,\5B8B\4F53; }
+.login-tips-password{ z-index: 5; left: 142px; }
+.login-tips-mobile{ z-index: 1; }
+/* webkit */
+.ui-ipt-enter,.ui-btn-submit{-webkit-appearance: none !important;}
+.user-logined {float:left; height:21px; margin-top:3px; padding-left:6px; line-height:21px; font-family:Verdana,\5B8B\4F53,san-serif; background-position:left -213px;}
+.login-name {float:left; cursor:pointer; padding-right:18px; background-position:right -233px;}
+.user-logined:hover {background-position:left -253px;}
+.user-logined:hover .login-name {background-position:right -273px;}
+/* gg style b31t9e41 */
+.b31t9e41 {background:url('../img1.126.net/channel5/008976/bolon_110302.png') right 50% no-repeat; padding-right:60px;}
+.tg_news {display:block; text-indent:-100em; overflow:hidden; width:250px; height:25px; background:url("../img2.cache.netease.com/www/v2011/img/tg_news.jpg") left top no-repeat;}
+/* gg ¼Òµç */
+.homeAppliances{display:inline;background: url(../img1.126.net/channel1/55x20_lan.gif) no-repeat right center; padding:0 55px 0 3px;}
+.current .homeAppliances{ background-image:url(../img1.126.net/channel1/55x20_bai.gif);}
+/* end */
+</style>
+</head>
+<body id="warpperBody">
+<div class="ntes-passport">
+ <div class="passport-main">
+ <div class="passport-login" id="NTES_Login">
+ <form id="login_form" class="left" method="POST" name="loginForm" target="_blank" action="httpdisabledsdisabled://reg.163.com/logins.jsp">
+ <input name="product" value="163" type="hidden">
+ <input name="type" value="1" type="hidden">
+ <input name="ursname" value="" type="hidden">
+ <input name="selected" value="" type="hidden">
+ <label class="label" for="login_username">ÕʺÅ</label>
+ <div class="form-ipt"><input id="login_username" class="form-ipt-user ui-ipt-enter" name="username" autocomplete="off" data-state="disable" type="text"></div>
+ <label class="label" for="login_password">ÃÜÂë</label>
+ <div class="form-ipt"><input id="login_password" class="form-ipt-pwd ui-ipt-enter" name="password" data-state="disable" type="password"></div>
+ <div id="login_select" class="ui-select passport-select">
+ <div id="login_select_area" class="ui-select-box">
+ <span id="login_selected" class="ui-select-selected">Ñ¡ÔñÈ¥Ïò</span>
+ <ul id="login_select_main" class="ui-select-list">
+ <li class="interval">ÍøÒ×ͨÐÐÖ¤</li>
+ <li title="@163.com">163ÓÊÏä</li>
+ <li title="@126.com">126ÓÊÏä</li>
+ <li title="@vip.126.com">VIP126ÓÊÏä</li>
+ <li title="@yeah.net">YeahÓÊÏä</li>
+ <li title="@188.com">188²Æ¸»ÓÊ</li>
+ <li title="@vip.163.com" class="interval">vipÓÊÏä</li>
+ <li>ÍøÒײ©¿Í</li>
+ <li>ÍøÒ×Ïà²á</li>
+ <li>ͬ³ÇÔ¼»á</li>
+ <li>ÍøÒ×ÂÛ̳</li>
+ <li>ÍøÒ×΢²©</li>
+ </ul>
+
+ </div>
+ </div>
+ <input value="怬" class="ui-btn-submit ui-btn-login" data-state="disable" type="submit">
+ </form>
+ <iframe style="display: none;" name="loginFrame" id="loginFrame" src="about:blank"></iframe>
+ <span id="login_before" class="login-info"><a href="httpdisabled://reg.163.com/reg/reg.jsp?product=163&url=http://www.163.com&loginurl=http://www.163.com">×¢²áͨÐÐÖ¤</a> | <a href="httpdisabled://reg.email.163.com/mailregAll/reg0.jsp?from=163">×¢²áÃâ·ÑÓÊÏä</a></span>
+ <span id="login_after" class="login-info" style="display:none;"><span class="left">»¶Ó­Ä㣬</span>
+ <div class="login-after-inner">
+ <div class="user-logined">
+ <strong id="login_after_username" class="login-name">pInfo</strong>
+ </div>
+ <div id="login_after_select" class="login-after-select">
+ <span class="user-entry">
+ <a class="popo-link others-link" href="httpdisabled://reg.163.com/Main.jsp?username=pInfo">½øÈëͨÐÐÖ¤</a>
+ <a class="select-mail-link" href="httpdisabled://entry.mail.163.com/coremail/fcg/ntesdoor2?verifycookie=1&lightweight=1">½øÈëÎÒµÄÓÊÏä</a>
+ <a class="select-mail-link" href="httpdisabled://entry.mail.126.com/cgi/ntesdoor?verifycookie=1&lightweight=1&style=-1">½øÈëÎÒµÄÓÊÏä</a>
+ <a class="select-mail-link" href="httpdisabled://reg.vip.126.com/enterMail.m">½øÈëÎÒµÄÓÊÏä</a>
+ <a class="select-mail-link" href="httpdisabled://entry.yeah.net/cgi/ntesdoor?verifycookie=1&lightweight=1&style=-1">½øÈëÎÒµÄÓÊÏä</a>
+ <a class="select-mail-link" href="httpdisabled://reg.mail.188.com/servlet/enter">½øÈëÎÒµÄÓÊÏä</a>
+ <a class="select-mail-link" href="httpdisabled://reg.vip.163.com/enterMail.m?enterVip=true-----------">½øÈëÎÒµÄÓÊÏä</a>
+ <a class="popo-link" href="httpdisabled://blog.163.com/passportIn.do?entry=163">½øÈëÎҵIJ©¿Í</a>
+ <a class="popo-link" href="httpdisabled://photo.163.com/?username=pInfo">½øÈëÎÒµÄÏà²á</a>
+ <a class="popo-link others-link" href="httpdisabled://yuehui.163.com/">½øÈëÎÒµÄÔ¼»á</a>
+ <a class="others-link" href="httpdisabled://t.163.com">½øÈëÎÒµÄ΢²©</a>
+ </span>
+ </div>
+ </div>
+ <iframe allowTransparency="true" style="width: 61px; height:26px; float:left; mmargin-left: 2px; vertical-align: middle" id="ifrmNtesMailInfo" border="0" src="../p.mail.163.com/mailinfo/shownewmsg_www_1222.htm.html" frameBorder="0" scrolling="no"></iframe>
+ | <a id="login_after_logout" href="httpdisabled://reg.163.com/Logout.jsp?username=accountName&url=http://www.163.com/" target="_self">°²È«Í˳ö </a>
+ </span>
+ </div>
+ <div class="passport-entry">
+ <a href="httpdisabled://www.163.com/rss/" class="i-rss">rss</a> <span class="right"><span class="cDRed"><a href="httpdisabled://email.163.com/">Ãâ·ÑÓÊÏä</a></span> <a href="httpdisabled://vipmail.163.com/">VIPÓÊÏä</a> | Ò»¿¨Í¨£º<a href="httpdisabled://pay.163.com/">³äÖµ</a> <a href="httpdisabled://ecard.163.com/">¹ºÂò</a> <a class="i-setIndex" target="_self" href="index.html" onClick="this.style.behavior='url(#default#homepage)';this.setHomePage('http://www.163.com/');" title="°ÑÍøÒ×ÉèΪÊ×Ò³">ÉèΪÊ×Ò³</a> <a href="httpdisabled://help.163.com?b01abh1" class="i-help">°ïÖúÖÐÐÄ</a></span>
+ </div>
+ </div>
+</div>
+<!-- ±³¾°¹ã¸æ -->
+<div class="header">
+ <h1>ÍøÒ×</h1>
+<div class="logo">
+ <a href="index.html"><img src="../img3.cache.netease.com/www/logo/logo_png.png" alt="ÍøÒ×" title="ÍøÒ×" border="0" height="37" width="118" /></a>
+ </div>
+ <div class="channel">
+ <div class="channel-col col-w">
+ <span class="nowrap"><strong><a href="httpdisabled://news.163.com/">ÐÂÎÅ</a></strong> <a href="httpdisabled://war.news.163.com/">¾üÊÂ</a> <a href="httpdisabled://news.163.com/review/">ÆÀÂÛ</a> <a href="httpdisabled://news.163.com/photo/">ͼƬ</a>
+ <strong><a href="httpdisabled://sports.163.com/">ÌåÓý</a></strong> <a href="httpdisabled://sports.163.com/nba/">NBA</a> <a href="httpdisabled://cbachina.163.com/">CBA</a> <a href="httpdisabled://sports.163.com/yc/">Ó¢³¬</a>
+ <strong><a href="httpdisabled://ent.163.com/">ÓéÀÖ</a></strong> <a href="httpdisabled://ent.163.com/movie/">µçÓ°</a> <a href="httpdisabled://ent.163.com/tv/">µçÊÓ</a> <a href="httpdisabled://ent.163.com/music/">ÒôÀÖ</a></span>
+ </div>
+ <div class="channel-col col-w">
+ <span class="nowrap">
+ <strong><a href="httpdisabled://money.163.com/">²Æ¾­</a></strong> <a href="httpdisabled://money.163.com/stock/">¹ÉƱ</a> <a href="httpdisabled://money.163.com/fund/">»ù½ð</a> <a href="httpdisabled://biz.163.com/">ÉÌÒµ</a>
+ <strong><a href="httpdisabled://v.163.com/">ÊÓƵ</a></strong> <a href="httpdisabled://v.163.com/focus/">Èȵã</a> <a href="httpdisabled://v.163.com/zongyi/">×ÛÒÕ</a> <a href="httpdisabled://v.163.com/doc/">¼Íʵ</a>
+ <strong><a href="httpdisabled://lady.163.com/">Å®ÈË</a></strong> <a href="httpdisabled://fashion.163.com/">ʱÉÐ</a> <a href="httpdisabled://lady.163.com/beauty/">ÃÀÈÝ</a> <a href="httpdisabled://lady.163.com/sense/">Çé°®</a>
+ </span>
+ </div>
+ <div class="channel-col col-w3">
+ <span class="nowrap">
+ <strong><a href="httpdisabled://tech.163.com/">¿Æ¼¼</a></strong> <a href="httpdisabled://money.163.com/hkstock/">¸Û¹É</a> <a href="httpdisabled://tech.163.com/cnstock/" class="w3">¸ÅÄî¹É</a>
+ <strong><a href="httpdisabled://mobile.163.com/">ÊÖ»ú</a></strong> <a href="httpdisabled://tech.163.com/3G/">3G</a> <a href="httpdisabled://product.tech.163.com/mobile/" class="w3">ÊÖ»ú¿â</a>
+ <strong><a href="httpdisabled://digi.163.com/">ÊýÂë</a></strong> <a href="httpdisabled://hea.163.com/">¼Òµç</a> <a href="httpdisabled://tech.163.com/digi/nb/" class="w3">±Ê¼Ç±¾</a>
+ </span>
+ </div>
+ <div class="channel-col col">
+ <span class="nowrap">
+ <strong><a href="httpdisabled://auto.163.com/">Æû³µ</a></strong> <a href="httpdisabled://auto.163.com/buy/">¹º³µ</a> <a href="httpdisabled://product.auto.163.com/">Ëѳµ</a>
+ <strong><a href="httpdisabled://travel.163.com/">ÂÃÓÎ</a></strong> <a href="httpdisabled://discovery.163.com/">̽Ë÷</a> <a href="httpdisabled://sports.163.com/lottery/">²ÊƱ</a>
+ <strong><a href="httpdisabled://house.163.com/" id="houseUrl">·¿²ú</a></strong> <a href="httpdisabled://home.163.com/">¼Ò¾Ó</a> <a href="httpdisabled://xf.house.163.com/gz/">Âò·¿</a>
+ </span>
+ </div>
+ <div class="channel-col col">
+ <span class="nowrap">
+ <strong><a href="httpdisabled://bbs.163.com/">ÂÛ̳</a></strong> <a href="httpdisabled://bbs.163.com/rank/">ÈÈÌû</a> <a href="httpdisabled://photo.163.com/">ÉãÓ°</a>
+ <strong><a href="httpdisabled://blog.163.com/?fromNavigation">²©¿Í</a></strong> <a href="httpdisabled://blog.163.com/blogger.html">Ãû²©</a> <a href="httpdisabled://edu.163.com/">½ÌÓý</a>
+ <strong><a href="httpdisabled://game.163.com/">ÓÎÏ·</a></strong> <a href="httpdisabled://wan.163.com/">Ò³ÓÎ</a> <a href="httpdisabled://book.163.com/">¶ÁÊé</a>
+ </span>
+ </div>
+ <div class="channel-col col">
+ <span class="nowrap">
+ <strong><a href="httpdisabled://t.163.com/" style="color:#ba2636;">΢²©</a></strong> <a href="httpdisabled://t.163.com/rank/daren">ÈËÎï</a> <a href="httpdisabled://t.163.com/rank">Èȵã</a>
+ <strong><a href="httpdisabled://fushi.163.com/">·þÊÎ</a></strong> <a href="httpdisabled://baby.163.com/">Ç××Ó</a> <a href="httpdisabled://gongyi.163.com/">¹«Òæ</a>
+ <strong><a href="httpdisabled://m.163.com/">Ó¦ÓÃ</a></strong> <a href="httpdisabled://mall.163.com/">É̳Ç</a> <a href="httpdisabled://media.163.com/">´«Ã½</a>
+ </span>
+ </div>
+ </div>
+ <div class="gg-g24 clearfix">
+<div id="" class="wgt-tab-gg left">
+ <div class="tab-hd-gg-left">
+ <ul><li class="tab-u current">ÈÈÏú</li><li class="tab-u">´òÕÛ</li><li class="tab-u">ÕÐÉú</li></ul>
+ </div>
+ <div class="left tab-bd-gg display-control">
+ <div class="tab-con cBlue current">
+<a href="httpdisabled://g.163.com/a?CID=6261&Values=1096445235&Redirect=http://xf.house.163.com/hn/0IBQ.html">ÑžÓÀÖÇåË®Í幫Ԣ¿ªÊÛ</a><br />
+<a href="httpdisabled://g.163.com/a?CID=241&Values=2504187228&Redirect=http://mail.188.com/news/v4/188intro_different.htm?vip03">ÐÒÔËÓòÃûÍøÒ×188ÓÊ</a><br />
+<a href="httpdisabled://g.163.com/a?CID=5936&Values=4083566547&Redirect=http://l.163.com">½ø¿ÚÃûÆ·2ÕÛ¾¢±¬ÇÀ¹º</a><br />
+<a href="httpdisabled://caipiao.163.com/">À´ÍøÒײÊƱÄÃǧÍò´ó½±</a><br />
+<a href="httpdisabledsdisabled://epay.163.com/notice/chongzhi.jsp">ÊÖ»ú¿¨¹ºµã¿¨±ã½Ý°²È«</a><br />
+<a href="httpdisabled://hn.house.163.com/11/0302/13/6U55TBSB0206009S.html">º£ÄÏ·¿²úÕмÇÕßÈô¸ÉÃû</a><br />
+ </div>
+ <div class="tab-con cBlue">
+<a href="httpdisabled://g.163.com/a?CID=242&Values=2202967901&Redirect=http://activity.vip.163.com/activity/art/index.mx?theme=Citroen&c5">Ãâ·ÑÓ®iPad¿´Îè¾ç</a><br />
+<a href="httpdisabledsdisabled://epay.163.com/notice/chongzhi.jsp">ÊÖ»ú¿¨¹ºµã¿¨±ã½Ý°²È«</a><br />
+<a href="httpdisabled://gz.house.163.com/special/bbs_2011wedding/">ɹ»éÉ´ÕÕ Ëͺ£ÄÏË«·É</a><br />
+<a href="httpdisabled://help.3g.163.com/news/">ÊÖ»úÉÏÿÈÕ²éÐÇ×ùÔËÊÆ</a><br />
+<a href="httpdisabled://game.163.com/">ÍøÒ׵羺ƵµÀÉÏÏß</a><br />
+<a href="httpdisabled://haoma.163.com">¹Å¶­¼¶ÍøÒ×Õ˺ÅÅÄÂô</a><br />
+ </div>
+ <div class="tab-con cBlue">
+<a href="httpdisabled://blog.163.com/activities/blogbbfg/blogbbfg.do">ǧÍò²©¿Í»»·ô²»ÔÙ¼èÄÑ</a><br />
+<a href="httpdisabled://hr.163.com/job/loc_info.jsp?id=782">ÍøÒ×±±¾©Æ¸²úÆ·Éè¼Æʦ</a><br />
+<a href="httpdisabled://fm.163.com/?sdysc1108">°ÙÍò°×Áì±Ø±¸ÓÊÏä¹Ü¼Ò</a><br />
+<a href="httpdisabled://mail.blog.163.com/blog/static/822094242010131169929/edit/?mode=prev">ÍøÒ×ÓÊÏä¡°Ò»ÏäË«ºÅ¡±</a><br />
+<a href="httpdisabled://tech.163.com/digi/buy/">Íò¿îÊýÂë²úÆ·Ñ¡¹ºÖ¸ÄÏ</a><br />
+<a href="httpdisabled://lady.163.com/">ʱÉÐÃÀÀö¾¡ÔÚÍøÒ×Å®ÈË</a><br />
+ </div>
+ </div>
+ </div>
+<div class="gg-g24-main cBlue">
+ <div class="g24-main-textTop">
+ <iframe src="../g.163.com/r@site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=textlinkhouse&amp;location=1.html" width="600" height="21" frameborder="0" border="0" marginwidth="0" marginheight="0" scrolling="no" ></iframe>
+ </div>
+ <div class="ggw600h80"><iframe id="iframe_banner1" name="iframe_banner1" src="../g.163.com/r@site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=column600x80&amp;location=1.html" width="600" height="80" frameborder="0" border="0" marginwidth="0" marginheight="0" scrolling="no" ></iframe>
+</div>
+ <span class="g24-main-textBottom"><iframe src="../g.163.com/r@site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=textlinkhouse&amp;location=2.html" width="600" height="21" frameborder="0" border="0" marginwidth="0" marginheight="0" scrolling="no" ></iframe>
+ </span>
+ </div>
+<div id="cAn1_r" class="wgt-tab-gg right">
+ <div class="tab-hd-gg-right">
+ <ul><li class="tab-u current">×îÐÂ</li><li class="tab-u">½ÌÓý</li><li class="tab-u">ÕÐÉÌ</li></ul>
+ </div>
+ <div class="right tab-bd-gg display-control">
+ <div class="tab-con cBlue current">
+<a href="httpdisabled://g.163.com/a?CID=6669&Values=806667841&Redirect=http://as.kejet.com/afaclick?u/NjM4Q0I1RjgwQzMwN0Mx/o/NzgyM0VCRUE3MkJDQ0RE/m/MDRGNzE2Mjk0NTBGQkU1?http://auto.163.com/11/0402/16/70LAUH6N00081G98.html">±¼ÌÚµ¼º½ÀñçÍ·×ϲÁ¬Á¬</a><br />
+<a href="httpdisabled://g.163.com/a?CID=242&Values=2202967901&Redirect=http://activity.vip.163.com/activity/art/index.mx?theme=Citroen&c5">Ãâ·ÑÓ®iPad¿´Îè¾ç</a><br />
+<a href="httpdisabled://mail.blog.163.com/blog/static/822094242011231101848784/">ÍøÒ×ÓÊÏäEmail°ÙÇéÊé</a><br />
+<a href="httpdisabled://reg.mail.188.com/servlet/regist?vip08">ÍøÒ×VIP¸ß¶Ë·þÎñƽ̨</a><br />
+<a href="httpdisabled://g.163.com/a?CID=247&Values=1421022587&Redirect=http://survey2.163.com/html/dict_youdao2011q1/paper.html">ÇáµãÊó±êÓ®¾ªÏ²´ó½±</a><br />
+<a href="httpdisabled://pmxj.wan.163.com/">Æ®Ãì¶à·çÔÆÏɽ£ÏÔÎäÁÖ</a><br />
+ </div>
+ <div class="tab-con cBlue">
+<a href="httpdisabled://hn.house.163.com/11/0302/13/6U55TBSB0206009S.html">º£ÄÏ·¿²úÕмÇÕßÈô¸ÉÃû</a><br />
+<a href="httpdisabled://g.163.com/a?CID=5943&Values=3636039039&Redirect=http://www.wtqx.net/vote/">ͶƱѡ³ö×îÃÀÀöУ԰</a><br />
+<a href="httpdisabled://yuehui.163.com/">2011·¢ÊIJ»ÔÙ×öʣŮ</a><br />
+<a href="httpdisabled://caipiao.163.com">À´ÍøÒײÊƱÄÃǧÍò´ó½±</a><br />
+<a href="httpdisabled://so.auto.163.com/">º£Á¿³µÑ¶¡°ËѳµÓеÀ¡±</a><br />
+<a href="httpdisabled://mail.163.com/html/110127_imap/index.htm">ÊÓƵ½ÌÄãÉèÖÃÓÊÏäIMAP</a><br />
+ </div>
+ <div class="tab-con cBlue">
+<a href="httpdisabled://yxp.163.com/photo/ep.html">ÕÕƬ³åÓ¡½ö0.45Ôª/ÕÅ</a><br />
+<a href="httpdisabled://fm.163.com/?100208fmgwwzl01">ÍøÒ×Ê׿î×ÀÃæÓÊÏäÈí¼þ</a><br />
+<a href="httpdisabled://bafang.163.com/">ÊÖ»ú²é¿´taÔÚÄÄÀï</a><br />
+<a href="httpdisabled://gongyi.163.com/love365?mailsignresult=-1">Ï£Íû¹¤³Ì365°®ÐÄÐж¯</a><br />
+<a href="httpdisabled://help.3g.163.com/stock/">ÊÖ»ú¿´¹ÉƱÿÈÕÕÇÍ£</a><br />
+<a href="httpdisabled://qn.163.com">ٻŮÓÄ»êÈÈ·¢¼¤»îÂë</a><br />
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="ntes-yodao">
+ <div class="yodao-main">
+ <a href="httpdisabled://www.youdao.com/" class="yodao-logo" title="µã»÷½øÈëÓеÀÊ×Ò³">ÍøÒ×ÓеÀ</a>
+ <form id="ydForm" method="get" action="httpdisabled://www.youdao.com/search" name="ydForm">
+ <div class="wgt-yodao-search">
+ <div class="yodao-search-category">
+ <span class="search-category-item">
+ <a class="current" onClick="return false">ÍøÒ³</a>
+ <a href="httpdisabled://image.youdao.com" onClick="changeProduct('image');return false" target="_blank">ͼƬ</a>
+ <a href="httpdisabled://news.youdao.com" onClick="changeProduct('news');return false" target="_blank">ÈÈÎÅ</a>
+ <a href="httpdisabled://gouwu.youdao.com" onClick="changeProduct('gouwu');return false" target="_blank">¹ºÎï</a>
+ <a href="httpdisabled://mp3.youdao.com" onClick="changeProduct('mp3');return false" target="_blank">ÒôÀÖ</a>
+ <a href="httpdisabled://video.youdao.com" onClick="changeProduct('video');return false" target="_blank">ÊÓƵ</a>
+ <a href="httpdisabled://dict.youdao.com" onClick="changeProduct('dict');return false" target="_blank">´Êµä</a>
+ <a href="httpdisabled://fanyi.youdao.com" target="_blank">·­Òë</a>
+ </span>
+ <div class="search-category-more ui-pos-rel c-entry" id="yodaoMore"><span>¸ü¶à</span>
+ <span id="categoryMore" class="category-more-list ui-pos-abs"><a href="httpdisabled://blog.youdao.com" onClick="changeProduct('blog');return false">²©¿Í</a><a href="httpdisabled://tie.youdao.com" target="_blank">¿ìÌù</a><a href="httpdisabled://map.youdao.com" onClick="changeProduct('map');return false" target="_blank">µØͼ</a><a class="interval" href="httpdisabled://reader.youdao.com/" target="_blank">ÔĶÁ</a><a href="httpdisabled://m.youdao.com/help" target="_blank">ÊÖ»ú</a><a href="httpdisabled://shuqian.youdao.com" target="_blank">ÊéÇ©</a><a class="interval" href="httpdisabled://cidian.youdao.com" target="_blank">×ÀÃæ´Êµä</a><a href="httpdisabled://www.youdao.com/about/productlist.html" target="_blank">È«²¿²úÆ·</a></span>
+ </div>
+ </div>
+ <label for="yodaoSearch">ÓеÀËÑË÷</label>
+ <div id="ydQuery">
+ <input id="query" class="ui-ipt-enter" type="text" name="q" autocomplete="off" /><button class="ui-btn-submit" type="submit" id="ydSubmit">ËÑ Ë÷</button>
+ <input name="ue" value="gbk" type="hidden">
+ <input name="keyfrom" value="163.index" type="hidden">
+ </div>
+ </div>
+ </form>
+ <div class="yodao-entry c-entry">
+ <span class="yodao-entry-mine">
+ <a href="httpdisabled://www.youdao.com/i?keyfrom=163.index" title="ÉÏÍøÊ×Ò³×Ô¼ºÔ죡">ÓеÀ¸öÐÔÊ×Ò³ <span class="code-en">&raquo;</span> </a>
+ </span>
+ <p id="ydHotKeys" class="yodao-entry-link"></p>
+ </div>
+ <div id="changeSkin" class="wgt-theme">
+ <span class="theme-blue current" title="À¶É«Ö÷Ìâ"> </span>
+ <span class="theme-gray" title="»ÒÉ«Ö÷Ìâ"> </span>
+ <span class="theme-green" title="ÂÌÉ«Ö÷Ìâ"> </span>
+ <span class="theme-pink" title="·ÛÉ«Ö÷Ìâ"> </span>
+ <span class="theme-yellow" title="»ÆÉ«Ö÷Ìâ"> </span>
+ </div>
+ </div>
+ <div class="yodao-sub">
+ <div id="wgt_weather" class="wgt-yodao-weather">
+ <div class="yodao-weather-info c-entry">
+ <div class="ui-pos-rel weather-area">
+ <div id="weather">
+ <span class="weather-location" id="setChange"></span>
+ </div>
+ <div id="ydAreas" class="ui-pos-abs yodao-dialog">
+ <div class="hd">
+ <span class="yodao-dialog-title">ÇëÑ¡Ôñ³ÇÊÐ</span>
+ <span id="closeWeather" class="yodao-dialog-close">X</span>
+ </div>
+ <div class="bd">
+ <label class="yodao-dialog-item">
+ Ê¡·Ý£º<select id="selectProvince">
+ <option>ÇëÑ¡Ôñ</option>
+ </select>
+ </label>
+ <label class="yodao-dialog-item">
+ ³ÇÊУº<select id="selectCity">
+ <option value="">ÇëÑ¡Ôñ</option>
+ </select>
+ </label>
+ <button id="ydaSubmit" class="ui-btn-submit">±£´æ</button>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="weatherIcon" class="yodao-weather-icon c-entry"></div>
+ </div>
+ </div>
+ </div>
+</div>
+<script type="text/javascript">
+//<![CDATA[
+//widget theme
+var temp_value = [" http://img2.cache.netease.com/www/v2011/css/theme_blue1227.css", "httpdisabled://img2.cache.netease.com/www/v2011/css/theme_bluegray1227.css", "httpdisabled://img2.cache.netease.com/www/v2011/css/theme_green1227.css", "httpdisabled://img2.cache.netease.com/www/v2011/css/theme_pink1227.css", "httpdisabled://img2.cache.netease.com/www/v2011/css/theme_yellow1227.css"];
+var skin = {_skinRef: $("#setSkin"),_ctrls: $("#changeSkin > span"),_srcs: temp_value, _cookieName: "NTES_SKIN",save: function (i){ NTES.cookie.set(this._cookieName, i, 30 * 24 * 60);},change:function (i){var t = this, pos = isNaN(i) ? t._ctrls.indexOf(i):i;if (pos >= 0 && pos < t._srcs.length){NTES.style.removeCss(t._ctrls, "current"); NTES.style.addCss(t._ctrls[pos], "current");t._skinRef.href = t._srcs[pos];t.save(pos);}},init:function (){var t = this;if(NTES.cookie.get(t._cookieName)){var pos = parseInt(NTES.cookie.get(t._cookieName));}if(isNaN(pos)){t._skinRef.href = "httpdisabled://img2.cache.netease.com/www/v2011/css/theme_blue1227.css";};if(!isNaN(pos)){t.change(pos);}t._ctrls.addEvent("click",function(e){e.preventDefault();t.change(this); });}};skin.init();
+//]]>
+</script>
+<div class="content">
+<!-- news & vedio -->
+<div class="area">
+ <div class="area-main">
+ <div class="main-col-10">
+ <div id="news" class="mod wgt-tab">
+ <h2 class="tab-hd tab-u-10 clearfix">
+ <span class="tab-u current"><a href="httpdisabled://news.163.com/">ÐÂÎÅ</a></span>
+ <span class="tab-u"><a href="httpdisabled://news.163.com/photo/" class="b31t9e41">ͼƬ</a></span>
+ <span class="tab-u"><a href="httpdisabled://focus.163.com/">Éî¶È</a></span>
+ <span class="tab-u"><a href="httpdisabled://war.news.163.com/">¾üÊÂ</a></span>
+ </h2>
+ <div class="bd tab-bd display-control">
+ <div class="tab-con current">
+ <div class="imgText-temp-1 dotline clearfix" style="height:139px;">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://news.163.com/photoview/4JC70001/13961.html"><img src="../img1.cache.netease.com/cnews/2011/4/8/20110408085323b9296.jpg" alt="ÈÕ±¾·¢Éú7.1¼¶µØÕð" title="ÈÕ±¾·¢Éú7.1¼¶µØÕð" height="90" width="120" /><cite>ÈÕ±¾·¢Éú7.1¼¶µØÕð</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://news.163.com/special/rbdblhddz/">Öйú¹ØÇÐÈÕ±¾Ïò̫ƽÑóÅŷź˷ÏÒº</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://v.news.163.com/video/2011/4/D/2/V70606SD2.html"><em class='I_V_'>µØÕð˲¼ä</em></a> <a href="httpdisabled://news.163.com/11/0408/08/713R20NL0001121M.html"><em class='I_V_'>ÕðºóÌì¿ÕÏÖ¹Ö¹â</em></a></li>
+ <li><a href="httpdisabled://news.163.com/11/0408/23/715ERLUE00014JB6.html">¸»Ê¿É½µÈ20»ðɽ»ò±¬·¢</a> <a href="httpdisabled://news.163.com/11/0409/00/715ILM5U00014JB5.html">ÈÕÆó¹ºÊß²Ë</a></li>
+ <li><a href="httpdisabled://news.163.com/11/0407/23/712SQFMQ00014JB6.html">¸£µººËµçÕ¾Õý³£</a> <a href="httpdisabled://news.163.com/photoview/00AO0001/13968.html">º«¹ú½µ"·øÉäÓê"</a></li>
+ <li><a href="httpdisabled://news.163.com/11/0407/20/712J2PR000014JB5.html">½­Ëչ㶫Êß²Ë</a> <a target="_blank" href="httpdisabled://news.163.com/11/0408/06/713L1DH50001124J.html">ɽÎ÷µØ±íË®ÏÖ·ÅÉäÎï</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list dotline">
+ <li><a href="httpdisabled://news.163.com/11/0409/04/7162TAHI00014JB6.html" data-t-h="05">ÃÀ¹ú·¢±í2010ÄêÈËȨ±¨¸æ ³ÆÖйúÈËȨ״¿ö¶ñ»¯</a></li>
+ <li><a href="httpdisabled://news.163.com/11/0409/02/715QH3GC00014AED.html" data-t-h="02">ÃÀ¹úÁª°îÕþ¸®¹ØÃŵ¹¼Æʱ 80Íò¹«ÎñÔ±»ò·Å¼Ù</a></li>
+ <li><a href="httpdisabled://news.163.com/11/0409/01/715NJRRE0001124J.html" data-t-h="01">ºÓÄÏÊÝÈ⾫°¸Ï¸½ÚÆعâ Ö÷ÒªÒÉ·¸2007Äêºó±©¸»</a></li>
+ <li><a href="httpdisabled://news.163.com/11/0409/02/715RGS6V00014JB5.html" data-t-h="03">ÐðÀûÑǶàµØÃñÖÚÓÎÐÐʾÍþ ÒªÇóÀ©´óÃñÖ÷³ÍÖθ¯°Ü</a></li>
+ <li><a href="httpdisabled://news.163.com/11/0409/03/715TTHDR00011229.html" data-t-h="03">¾¯³µ×²ËÀÈËÒý·¢´åÃñ¶Â· ¹Ù·½³ÆÎó½âÒÑÏû³ý</a></li>
+ <li><a href="httpdisabled://news.163.com/special/party90/"><em class='cDRed'>µ³Ê·½ñÌì:Öܶ÷À´ÓëÕÅѧÁ¼»á̸</em></a> <a href="httpdisabled://news.163.com/11/0407/19/712HBQS800014JB5.html "><em class='cDRed'>Ê®¶þÎå</em></a> <a href="httpdisabled://news.163.com/11/0408/15/714KU3PH00014JB6.html"><em class='cDRed'>¹úаìÁÁÏàiPad</em></a></li>
+ </ul>
+ <ul class="mod-list main-list dotline">
+ <li><a href="httpdisabled://news.163.com/11/0409/04/7162E16400011229.html" data-t-h="04">¹ã¶«ÖÐɽ¾¯·½»ØÓ¦·ò¸¾Âã±¼º°Ô©Ê¼þ:²»´æÔÚ°ü±Ó</a></li>
+ <li><a href="httpdisabled://news.163.com/11/0409/01/715NKCS300014AED.html" data-t-h="02">ÄÐ×ÓÁ¬Í±7Å®ÐÔ±»¾Ð ¾¯·½³ÆÆäÒò˼ÄîÇ°Å®ÓÑÐÐÐ×</a></li>
+ <li><a href="httpdisabled://news.163.com/special/libiyawar/">ÃÀ¹ú½«¾ü³Æ¿¼ÂÇÏòÀû±ÈÑÇÅÉDzµØÃ沿¶Ó</a></li>
+ <li><a href="httpdisabled://news.163.com/11/0409/01/715NCEBD00014AED.html" data-t-h="02">½¯·½ÖÛ³ÆÇ廪ѧÉúΪ¼ÈµÃÀûÒæÕß Ð£·½³ÆÓ¦·´Ë¼</a></li>
+ <li><a href="httpdisabled://news.163.com/11/0409/01/715NDEAK00014AED.html" data-t-h="02">±±¾©ÊÐÃñɹ¡°¹«³µÍ£³µÖ¤¡± ³ÆÈ·ÔøÏíÊÜÃâ·Ñ´ýÓö</a></li>
+ <li><a href="httpdisabled://news.163.com/11/0408/14/714HOGUR0001124J.html">¹óÖÝÎÀÊÓ¡¶ÈËÉú¡·À¸Ä¿Òò·Å´óÒþ˽±»ÓÀ¾ÃÍ£²¥</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://news.163.com/" class="fB attitude left">ÐÂÎÅ</a> <a href="httpdisabled://m.163.com/newsapp/" class="right tg_news">ÍøÒ×ÐÂÎÅÊÖ»ú¿Í»§¶Ë</a></li>
+ <li><a href="httpdisabled://news.163.com/special/163theotherside/" class="fB">ÁíÒ»Ãæ</a> | <a href="httpdisabled://news.163.com/special/reviews/governmentshutdown.html">»¨Ç®¹ý¶È£¬Õþ¸®¹ØÃÅ</a> | <a href="httpdisabled://news.163.com/special/reviews/binzangbaoli.html">éëÔᱩÀû£ºÉúÄÑ»¶ËÀ¶à¿à</a> </li>
+ <li><a href="httpdisabled://discover.news.163.com/special/00014INC/discoverer.html" class="fB">·¢ÏÖÕß</a> | <a href="httpdisabled://discover.news.163.com/special/formerhome/">Ó¢¹úÈËÈçºÎ±£»¤¹Ê¾Ó</a> | <a href="httpdisabled://discover.news.163.com/special/godzilla/">¸£µººËÎÛË®È뺣֮ÓÇ</a> </li>
+ <li><a href="httpdisabled://news.163.com/special/000113C4/163kanke.html" class="fB">¿´¿Í</a> | <a href="httpdisabled://news.163.com/photoview/3R710001/13844.html">¿¨Ôú·ÆµÄ·¨¹úÇé³ð</a> | <a href="httpdisabled://news.163.com/photoview/19BR0001/13895.html">Ò»ÖÜͼƬ¾«Ñ¡µÚ69ÆÚ</a> </li>
+ <li><a href="httpdisabled://focus.163.com/" class="fB">Éî¶È</a> | <a href="httpdisabled://focus.news.163.com/11/0408/13/714EJNHS00011SM9.html">´óѧÉú´å¹ÙµÄ¡°Éý¹ÙÖ®µÀ¡±</a> | <a href="httpdisabled://focus.news.163.com/11/0407/18/712DHM9100011SM9.html">Òþ²Ø°ëÊÀ¼ÍµÄÔ®³¯Ó¢ÐÛ</a> </li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <ul class="mod-imgList imgList-w120 clearfix">
+ <li><a href="httpdisabled://news.163.com/photoview/00AO0001/13966.html"><img src="../img2.cache.netease.com/cnews/2011/4/8/2011040812525484a8f.jpg" alt="°ÍÎ÷У԰ǹս13ÈËËÀ" title="°ÍÎ÷У԰ǹս13ÈËËÀ" height="90" width="120"></a><p><a href="httpdisabled://news.163.com/photoview/00AO0001/13966.html">°ÍÎ÷У԰ǹս13ÈËËÀ</a></p></li>
+ <li><a href="httpdisabled://news.163.com/photoview/00AO0001/13969.html"><img src="../img2.cache.netease.com/cnews/2011/4/8/20110408120203d0f08.jpg" alt="·¨¾ü½â¾ÈÈÕ±»À§´óʹ" title="·¨¾ü½â¾ÈÈÕ±»À§´óʹ" height="90" width="120"></a><p><a href="httpdisabled://news.163.com/photoview/00AO0001/13969.html">·¨¾ü½â¾ÈÈÕ±»À§´óʹ</a></p></li>
+ <li><a href="httpdisabled://news.163.com/photoview/00AN0001/13978.html"><img src="../img2.cache.netease.com/cnews/2011/4/8/201104082245192ae96.jpg" alt="¸ÊËàËíµÀÓ͹޳µ±¬Õ¨" title="¸ÊËàËíµÀÓ͹޳µ±¬Õ¨" height="90" width="120"></a><p><a href="httpdisabled://news.163.com/photoview/00AN0001/13978.html">¸ÊËàËíµÀÓ͹޳µ±¬Õ¨</a></p></li>
+ <li><a href="httpdisabled://news.163.com/photoview/00AO0001/13968.html"><img src="../img2.cache.netease.com/cnews/2011/4/8/20110408115631ad273.jpg" alt="º«¹úÍ£¿Î±Ü&quot;·øÉäÓê&quot;" title="º«¹úÍ£¿Î±Ü&quot;·øÉäÓê&quot;" height="90" width="120"></a><p><a href="httpdisabled://news.163.com/photoview/00AO0001/13968.html">º«¹úÍ£¿Î±Ü"·øÉäÓê"</a></p></li>
+ <li><a href="httpdisabled://news.163.com/photoview/00AP0001/13962.html"><img src="../img2.cache.netease.com/cnews/2011/4/8/20110408100357df2b1.jpg" alt="¶«Ý¸Í»¼ìÉæ»Æ³¡Ëù" title="¶«Ý¸Í»¼ìÉæ»Æ³¡Ëù" height="90" width="120"></a><p><a href="httpdisabled://news.163.com/photoview/00AP0001/13962.html">¶«Ý¸Í»¼ìÉæ»Æ³¡Ëù</a></p></li>
+ <li><a href="httpdisabled://news.163.com/photoview/00AO0001/13970.html"><img src="../img2.cache.netease.com/cnews/2011/4/8/20110408140704d246b.jpg" alt="ÒÔÉ«ÁÐÁ½»ð³µÏàײ" title="ÒÔÉ«ÁÐÁ½»ð³µÏàײ" height="90" width="120"></a><p><a href="httpdisabled://news.163.com/photoview/00AO0001/13970.html">ÒÔÉ«ÁÐÁ½»ð³µÏàײ</a></p></li>
+ <li><a href="httpdisabled://news.163.com/11/0408/01/71364DV200014AED.html"><img src="../img1.cache.netease.com/cnews/2011/4/8/20110408104255a47ce.jpg" alt="ýÌåÆعâµØÏÂѪÍø" title="ýÌåÆعâµØÏÂѪÍø" height="90" width="120"></a><p><a href="httpdisabled://news.163.com/11/0408/01/71364DV200014AED.html">ýÌåÆعâµØÏÂѪÍø</a></p></li>
+ <li><a href="httpdisabled://news.163.com/photoview/05RQ0001/13971.html"><img src="../img2.cache.netease.com/cnews/2011/4/8/2011040814525199c07.jpg" alt="ÈÕ±¾Ôֺ󽨼òÒ×סլ" title="ÈÕ±¾Ôֺ󽨼òÒ×סլ" height="90" width="120"></a><p><a href="httpdisabled://news.163.com/photoview/05RQ0001/13971.html">ÈÕ±¾Ôֺ󽨼òÒ×סլ</a></p></li>
+ <li><a href="httpdisabled://news.163.com/photoview/00AQ0001/13959.html"><img src="../img1.cache.netease.com/cnews/2011/4/8/2011040809550649773.jpg" alt="Ö±»÷Öйú¾ü¶ÓCSÁ·±ø" title="Ö±»÷Öйú¾ü¶ÓCSÁ·±ø" height="90" width="120"></a><p><a href="httpdisabled://news.163.com/photoview/00AQ0001/13959.html">Ö±»÷Öйú¾ü¶ÓCSÁ·±ø</a></p></li>
+ <li><a href="httpdisabled://news.163.com/photoview/00AN0001/13967.html"><img src="../img1.cache.netease.com/cnews/2011/4/8/201104081119113f37f.jpg" alt="ÃÀ´óʹÔÚÉϺ£Æï¹þÀ×" title="ÃÀ´óʹÔÚÉϺ£Æï¹þÀ×" height="90" width="120"></a><p><a href="httpdisabled://news.163.com/photoview/00AN0001/13967.html">ÃÀ´óʹÔÚÉϺ£Æï¹þÀ×</a></p></li>
+ <li><a href="httpdisabled://news.163.com/11/0408/12/714AP6C900014AEF.html"><img src="../img2.cache.netease.com/cnews/2011/4/8/201104081242198a4ba.jpg" alt="ÄÐ×ÓÔÚÆÞ×ÓÁ³ÉÏ¿Ì×Ö" title="ÄÐ×ÓÔÚÆÞ×ÓÁ³ÉÏ¿Ì×Ö" height="90" width="120"></a><p><a href="httpdisabled://news.163.com/11/0408/12/714AP6C900014AEF.html">ÄÐ×ÓÔÚÆÞ×ÓÁ³ÉÏ¿Ì×Ö</a></p></li>
+ <li><a href="httpdisabled://war.news.163.com/photoview/00AQ0001/13954.html"><img src="../img1.cache.netease.com/cnews/2011/4/8/2011040814544385564.jpg" alt="¶íÅ®¼äµýʱװÖÜ×ßÐã" title="¶íÅ®¼äµýʱװÖÜ×ßÐã" height="90" width="120"></a><p><a href="httpdisabled://war.news.163.com/photoview/00AQ0001/13954.html">¶íÅ®¼äµýʱװÖÜ×ßÐã</a></p></li>
+ </ul>
+ <p class="entry c-entry right"><a href="httpdisabled://news.163.com/photo/">¸ü¶à <span class="code-en">&raquo;</span> </a></p>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://news.163.com/special/reviews/governmentshutdown.html"><img src="../img1.cache.netease.com/cnews/2011/4/9/20110409022720f974c.jpg" alt="»¨Ç®¹ý¶È£¬Õþ¸®¹ØÃÅ" title="»¨Ç®¹ý¶È£¬Õþ¸®¹ØÃÅ" height="90" width="120" /><cite>»¨Ç®¹ý¶È£¬Õþ¸®¹ØÃÅ</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://focus.news.163.com/11/0408/13/714EJNHS00011SM9.html">´óѧÉú´å¹ÙµÄ¡°Éý¹ÙÖ®µÀ¡±</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://focus.news.163.com/11/0408/13/714F3NLI00011SM9.html">±«²ª¡¤µÏÂ×µÄÖÇÁ¦</a></li>
+ <li><a href="httpdisabled://focus.news.163.com/11/0408/12/714B46B300011SM9.html">¡°Õê²ÙÊÇ×îºÃÅã¼Þ¡±</a></li>
+ <li><a href="httpdisabled://discover.news.163.com/11/0408/09/71411PAP00012IH2.html">Ó¡¶ÈÔÚÒûÓÃË®Öз¢ÏÖ³¬¼¶Ï¸¾ú</a></li>
+ <li><a href="httpdisabled://discover.news.163.com/special/formerhome/">Ó¢¹úÃûÈ˹ʾӱ£»¤:ÎÄ»¯ÓëµØ²ú²©ÞÄ</a></li>
+ </ul>
+ </div>
+ <div class="temp-1-2 dotline clearfix">
+ <h3 class="main-subtitle c-entry"><a href="httpdisabled://focus.news.163.com/">Éî¶È</a></h3>
+ <div class="temp-u left">
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://focus.news.163.com/11/0407/18/712DHM9100011SM9.html">Òþ²ØÁË°ëÊÀ¼ÍµÄÔ®³¯Ó¢ÐÛ</a></li>
+ <li><a href="httpdisabled://focus.news.163.com/11/0407/18/712CSAB900011SM9.html">¾È¾ÈÎÒÃǵijÇÊÐ</a></li>
+ <li><a href="httpdisabled://focus.news.163.com/11/0407/17/71290G4V00011SM9.html">±±¾©µÚÒ»ÀÃβ¥ÄÚÄ»</a></li>
+ <li><a href="httpdisabled://focus.news.163.com/11/0407/16/712707RP00011SM9.html">ְУÄù˜„</a></li>
+ <li><a href="httpdisabled://focus.news.163.com/11/0406/22/71092VNS00011SM9.html">ÇൺµÄ¸çάȨ¼Ç</a></li>
+ </div>
+ <div class="temp-u right">
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://focus.news.163.com/11/0406/19/70VUIN4E00011SM9.html">½­Î÷°ÙÈËÒò³¾·Î²¡ÖÂËÀ </a></li>
+ <li><a href="httpdisabled://focus.news.163.com/11/0406/19/70VTHTGK00011SM9.html">Öйú¼Í¼ƬÊг¡ÏÖ×´</a></li>
+ <li><a href="httpdisabled://focus.news.163.com/11/0406/19/70VT0F1500011SM9.html">¶«¾©µçÁ¦¹«Ë¾¡°°ó¼Ü¡±ÈÕ±¾</a></li>
+ <li><a href="httpdisabled://focus.news.163.com/11/0406/11/70V31TR900011SM9.html">ÎåÄ꣬ȫÃæ¿ØÑÌ£¿</a></li>
+ <li><a href="httpdisabled://focus.news.163.com/11/0406/11/70V2KKR700011SM9.html">¿ÓÑʹÊƵ·¢»§Íâ¹ÜÀíÍѽÚ</a></li>
+ </ul>
+ </div>
+ </div>
+ <div class="temp-1-2 dotline clearfix">
+ <h3 class="main-subtitle c-entry"><a href="httpdisabled://news.163.com/review/">ÆÀÂÛ</a></h3>
+ <div class="temp-u left">
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://news.163.com/11/0408/18/714UHI5500012Q9L.html">½À»ðÍÈÍì²»»ØÏû·ÑÕßµÄÐÅÈÎ</a></li>
+ <li><a href="httpdisabled://news.163.com/11/0408/18/714UE3I600012Q9L.html">Ó¦ÖØÊÓ³Ѹ֮×ӵĸöÈ˼ÛÖµ</a></li>
+ <li><a href="httpdisabled://news.163.com/11/0408/18/714U80N800012Q9L.html">Ç¿ÖÆÖÖÆÏÌÑÊÇΨGDPÂÛ</a></li>
+ <li><a href="httpdisabled://news.163.com/11/0408/18/714U1KSO00012Q9L.html">Ï×Ѫָ±êÑøÓý¡°ÑªÍ·¡±ÓÄÁé</a></li>
+ <li><a href="httpdisabled://news.163.com/11/0408/18/714TOSL400012Q9L.html">½¡È«Öƶȱ£»¤²¡È˵ÄÀûÒæ</a></li>
+ </div>
+ <div class="temp-u right">
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://news.163.com/11/0407/21/712N21BO00012Q9L.html">ÑëÐмÓÏ¢Ö®ÓÇÂǺ͡°»Ã¾õ¡±</a></li>
+ <li><a href="httpdisabled://news.163.com/11/0407/21/712MTPIA00012Q9L.html">ÖιÙÔ±³¬±à²»¿É¿¿¡°Î²¹¡±</a></li>
+ <li><a href="httpdisabled://news.163.com/11/0407/21/712MIPA000012Q9L.html">²»×ðÖØÇîÈ˲ÅÊǸßѧÀúÖ®³Ü</a></li>
+ <li><a href="httpdisabled://news.163.com/11/0407/17/712A9KRR00012Q9L.html">²ðÒû±ùÊÒ£ººóÈ˸´°§ºóÈËÒ²</a></li>
+ <li><a href="httpdisabled://news.163.com/11/0407/17/71297UM000012Q9L.html">¼ÓÏ¢»òÊÇ·¿¼Ûµ÷ÕûµÄÈ󻬼Á</a></li>
+ </ul>
+ </div>
+ </div>
+ <div class="temp-1-2 dotline clearfix">
+ <h3 class="main-subtitle c-entry"><a href="httpdisabled://discovery.163.com/">̽Ë÷</a></h3>
+ <div class="temp-u left">
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://discover.news.163.com/11/0408/10/7142PDP6000125LI.html">Èâ··ÓÃÓж¾·ÛÄ©Í¿ÖíÈâ±£ÏÊ</a></li>
+ <li><a href="httpdisabled://discover.news.163.com/11/0408/10/71429VJL000125LI.html">Ѫͷ×é֯ѧÉúÃñ¹¤ÂôѪIJÀû</a></li>
+ <li><a href="httpdisabled://discover.news.163.com/11/0408/09/71411PAP00012IH2.html">Ó¡¶ÈÔÚÒûÓÃË®Öз¢ÏÖ³¬¼¶Ï¸¾ú</a></li>
+ <li><a href="httpdisabled://discover.news.163.com/11/0408/10/7142RCVR000125LI.html">ÈÕ±¾ºË·øÉäÐÎÊÆÆÀ¹À</a></li>
+ <li><a href="httpdisabled://discover.news.163.com/11/0408/10/7143N0AT000125LI.html">·ÂÉúÑÛ¾µÖúäÈËÖØ»ñ¹âÃ÷</a></li>
+ </div>
+ <div class="temp-u right">
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://discover.news.163.com/11/0407/10/711FPHRM000125LI.html">ר¼Ò³ÆÓ¦ÆÀ¹ÀÖйúº£Ð¥·çÏÕ</a></li>
+ <li><a href="httpdisabled://discover.news.163.com/11/0407/10/711HMTF0000125LI.html">ÃÀ¾ÞÐÍ»ð¼ý¿ÉËÍÈËÀàÉÏ»ðÐÇ</a></li>
+ <li><a href="httpdisabled://discover.news.163.com/11/0407/11/711JEUKO000125LI.html">³¬¼¶Ï¸¾úÖ®ÍõCRKPÏÖÉíÃÀ¹ú</a></li>
+ <li><a href="httpdisabled://discover.news.163.com/11/0407/11/711LA7RJ000125LI.html">µÂÅ®º¢°ÑĸţѵÁ·³É"ÈüÂí"</a></li>
+ <li><a href="httpdisabled://discover.news.163.com/11/0407/10/711GS2DT000125LI.html">ÈËÔìÓãβÈòм²È˱äÃÀÈËÓã</a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://war.news.163.com/photoview/00AQ0001/13959.html"><img src="../img2.cache.netease.com/cnews/2011/4/7/20110407202028db993.jpg" alt="ÄϾ©¾üÇøij²¿ÒÔCSÁ·±ø" title="ÄϾ©¾üÇøij²¿ÒÔCSÁ·±ø" height="90" width="120" /><cite>ÄϾ©¾üÇøij²¿ÒÔCSÁ·±ø</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://war.news.163.com/11/0407/20/712KL6ED00014J0G.html">ÍßÁ¼¸ñ½«³ÉΪÖйúÊ×ËÒʵսÐͺ½Ä¸</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://t.163.com/wxsun/status/-5374398591249521056#retweet">ËïÀñ£ºÀû·´¶ÔÅÉÊ×ÅúʯÓÍÔËÍùÖйú</a></li>
+ <li><a href="httpdisabled://t.163.com/fyjsyfy/status/-9113848012304784652#retweet">ÔÆ·ÉÑÓÖÒ»ËÒ054A»¤ÎÀ½¢³É¾üÁË</a></li>
+ <li><a href="httpdisabled://t.163.com/3580035945/status/-6113039237267459001#retweet">ÕÅÃ÷£º¹ÒÔØ4öӥ»÷83µÄ·É±ª(ͼ)</a></li>
+ <li><a href="httpdisabled://t.163.com/sedna/status/-7903894352043047591#retweet">ËÉÊó£ºÒÔÉ«ÁÐÔÚ×·Çó¡°¾ø¶Ô°²È«¡±</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list dotline">
+ <li><a href="httpdisabled://war.news.163.com/11/0408/10/714232PL00013COV.html">̨¾ü£º´ó½Ê×ËÒº½Ä¸²¿ÊðÄϺ£ Ó¦¶ÔÍ»·¢Õù¶Ë</a></li>
+ <li><a href="httpdisabled://war.news.163.com/11/0408/10/7142MFUD00011MTO.html">ÃÀ˾Áî³ÆÀû·´¶ÔÅÉÍÆ·­¿¨Ôú·Æ¿ÉÄÜÐÔºÜС</a> <a target="_blank" href="httpdisabled://news.163.com/special/libiyawar/">רÌâ</a></li>
+ <li><a href="httpdisabled://war.news.163.com/11/0408/00/7130CEUT00014JB5.html">ÃÀÌØʹ£º¿¨Ôú·ÆÍËλ¿ÉÒÔ»»È¡·ÇÃËÈÙÓþÖ÷ϯ</a></li>
+ <li><a href="httpdisabled://war.news.163.com/11/0408/10/7141QADN00011MTO.html">ÒÔÉ«ÁÐÌúñ·ÏµÍ³Ê×´ÎʵսÀ¹½Ø»ð¼ýµ¯³É¹¦(ͼ)</a></li>
+ <li><a href="httpdisabled://war.news.163.com/11/0408/09/71415JND00011MTO.html">ÖйúÖÇÄܵ¯Ò©£ºÅÚÉä"Ä©Ãôµ¯"¹Ø¼ü¼¼Êõ»ñÍ»ÆÆ</a></li>
+ <li><a href="httpdisabled://war.news.163.com/11/0407/18/712DHM9100011SM9.html">¡¶Ó¢ÐÛ¶ùÅ®¡·Íõ³ÉÔ­ÐÍÒòÔø±»·ýÂñÃû50Äê(ͼ)</a></li>
+ </ul>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://war.news.163.com/11/0408/11/7146U0O400011MTO.html">ÃÀ¾üÁ½¿ÅSTSSÎÀÐdzɹ¦ÊµÏÖ¶Ôµ¼µ¯Á¢Ìå¸ú×Ù</a></li>
+ <li><a href="httpdisabled://war.news.163.com/11/0408/11/7146D6O500011MTO.html">ÃÀý×ܽáÃÀ¹úºÃÕ½Îå´óÔ­Òò£ºÃ»ÓÐÕæÕýµÐÈË</a></li>
+ <li><a href="httpdisabled://war.news.163.com/11/0408/10/714475KD00011MTO.html">Ӣý£º½â·Å¾ü¸ß½Ì»ú¾º±êÁ½ÐÍ·É»ú¾ù²»ÍêÉÆ</a></li>
+ <li><a href="httpdisabled://war.news.163.com/11/0408/10/7142EVJ600011MTO.html">¼òÊÏ£ºÖйúÓµÓÐÎäÆ÷Ñз¢È¨ÆóÒµ2/3ϵÃñÆó</a></li>
+ <li><a href="httpdisabled://war.news.163.com/11/0408/11/7147TN1N00011MTO.html">Ì©¹ú½¾ü¹ºÂò200Á¾ÎÚ¿ËÀ¼"±¤ÀÝ"Ö÷ս̹¿Ë</a></li>
+ <li><a href="httpdisabled://war.news.163.com/11/0408/10/71440DDQ00011MTO.html">ÈÕý³ÆÖйú½«¸üÒÀÀµ¾üʼ°¾­¼ÃʵÁ¦½â¾öÕù¶Ë</a></li>
+ </ul>
+ <ul class="temp-1-2 clearfix">
+ <li class="temp-u left">
+ <h3 class="main-subtitle cBlue"><a href="httpdisabled://war.news.163.com/">¾üʲ©¿Í</a></h3>
+ <div class="mod-imgText imgText-temp-2 clearfix">
+ <h4 class="imgText-titleTop"><a href="httpdisabled://zhouf601117.blog.163.com/blog/static/12655106620113603443750/">Ë®²´·ÇÖÞ£ºÎÞ½âµÄË÷ÂíÀﺣµÁ</a></h4>
+ <a href="httpdisabled://zhouf601117.blog.163.com/blog/static/12655106620113603443750/"><img class="imgText-img" src="../img2.cache.netease.com/cnews/2011/4/8/2011040814452013ef7.jpg" alt="Ë®²´·ÇÖÞ£ºÎÞ½âµÄË÷ÂíÀﺣµÁ" title="Ë®²´·ÇÖÞ£ºÎÞ½âµÄË÷ÂíÀﺣµÁ" height="70" width="70" /></a>
+ <p class="imgText-digest">ÖÁ½ñÈÔûÓÐÄĸö¹ú¼ÒÔ¸ÒâÁìÏδò»÷º£µÁ¡£<span class="cDRed"><a href="httpdisabled://zhouf601117.blog.163.com/blog/static/12655106620113603443750/">[Ïêϸ]</a></span></p>
+ </div>
+ </li>
+ <li class="temp-u right">
+ <h3 class="main-subtitle cBlue"><a href="httpdisabled://war.news.163.com/">¾üÇé¹Û²ìÊÒ</a></h3>
+ <div class="mod-imgText imgText-temp-2 clearfix">
+ <h4 class="imgText-titleTop"><a href="httpdisabled://war.news.163.com/11/0404/13/70Q5IQA200014J0G.html">¶«·ç16½«´ó·ùÌáÉý·´½éÈëÄÜÁ¦</a></h4>
+ <a href="httpdisabled://war.news.163.com/11/0404/13/70Q5IQA200014J0G.html"><img class="imgText-img" src="../img2.cache.netease.com/cnews/2011/4/7/20110407093718ef414.jpg" alt="¶«·ç16½«´ó·ùÌáÉý·´½éÈëÄÜÁ¦" title="¶«·ç16½«´ó·ùÌáÉý·´½éÈëÄÜÁ¦" height="70" width="70" /></a>
+ <p class="imgText-digest">¶«·ç16ÄܽϿìÐγɶԳåÉþÃÀ¾üѹÖÆÄÜÁ¦¡£<span class="cDRed"><a href="httpdisabled://war.news.163.com/11/0404/13/70Q5IQA200014J0G.html">[Ïêϸ]</a></span></p>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="main-col-9">
+ <div class="mod-function">
+ <span class="function-date">04ÔÂ09ÈÕ ÐÇÆÚÁù</span>
+ <span class="function-close" id="updateBtn"></span>
+ <span class="function-info" id="updateInfo"></span>
+ </div>
+<div class="gg_reset mb-6 gg-h65"><iframe src="../g.163.com/r@site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=banner360x65&amp;location=1.html" width="360" height="65" frameborder="0" border="0" marginwidth="0" marginheight="0" scrolling="no" ></iframe>
+</div>
+ <div class="gg_reset mb-6 gg-h65"><iframe src="../g.163.com/r@site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=banner360x65&amp;location=2.html" width="360" height="65" frameborder="0" border="0" marginwidth="0" marginheight="0" scrolling="no" ></iframe>
+</div>
+ <div id="vedio" class="mod wgt-tab">
+ <h2 class="tab-hd tab-u-9 clearfix">
+ <span class="tab-u current"><a href="httpdisabled://v.163.com/">ÊÓƵ</a></span>
+ <span class="tab-u"><a href="httpdisabled://v.163.com/focus/">Èȵã</a>¡¤<a href="httpdisabled://v.163.com/zongyi/">×ÛÒÕ</a></span>
+ <span class="tab-u"><a href="httpdisabled://v.163.com/doc/">¼Íʵ</a>¡¤<a href="httpdisabled://v.163.com/fashion/">·çÉÐ</a></span>
+ <span class="tab-u"><a href="httpdisabled://v.163.void/">ÍøÒ×¹«¿ª¿Î</a></span>
+ </h2>
+ <div class="bd tab-bd display-control">
+ <div class="tab-con current">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://v.163.com/special/nvtop34/"><img src="../img1.cache.netease.com/cnews/2011/4/6/20110406182512d4541.jpg" alt="±¾ÖÜ×î»ðÍøÂçÊÓƵtop5" title="±¾ÖÜ×î»ðÍøÂçÊÓƵtop5" height="90" width="120" /><cite>±¾ÖÜ×î»ðÍøÂçÊÓƵtop5</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://v.163.com/special/issue201053/">¡°ËÀÎÞÔáÉíÖ®µØ¡±³ÉΪÏÖʵ£¿</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://v.163.com/video/2011/4/B/F/V70633PBF.html#ld=V6VAOISBD">ÈÕ±¾ÔÙÔâÇ¿µØÕðÏ®»÷Ìì¿ÕÏÖÇ¿¹â</a></li>
+ <li><a href="httpdisabled://v.163.com/video/2011/4/0/2/V70647J02.html#ld=V68F91IG1">Ò©¼Òöο´ÊØËùÉîÇéÑÝÒ´«Ææ¡·</a></li>
+ <li><a href="httpdisabled://v.163.com/video/2011/4/4/B/V7066OF4B.html#ld=V6VAOISBD">ʵÅÄ£º×íººµ÷Ï·Çå´¿Å®ÉúÔâȺŹ</a></li>
+ <li><a href="httpdisabled://v.163.com/video/2011/4/B/7/V7064C0B7.html#ld=V63F88H2T">ÈÕ±¾"·­Á³¹·"±¬ºìÒ»ÎÕÊ־ͱäÁ³</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list dotline">
+ <li><a href="httpdisabled://v.163.com/nv/"><em class=' I_V_'>[ÍøÊÓ]</em></a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/D/0/V704BTTD0.html">¶ñ¸ã£º±±¾©µÄ·¿×ÓÄã×â²»Æð</a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/Q/T/V7043J6QT.html">Éú»î¸ãЦ˲¼ä</a></li>
+ <li><a href="httpdisabled://v.163.com/nv/"><em class=' I_V_'>[Èȵã]</em></a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/T/T/V6VND1KTT.html#ld=V68F91IG1">ºÏÉùÆ÷³¬¸øÁ¦ÓÎÏ·ÒôÀÖ</a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/H/B/V70126QHB.html#ld=V68F7PNND">Èý¹úɱ×îÇ¿ÄÚ¼é</a></li>
+ <li><a href="httpdisabled://v.163.com/fashion/"><em class='I_V_'>[·çÉÐ]</em></a> <a href="httpdisabled://v.163.com/video/2011/4/O/I/V701VL5OI.html#ld=V675K0B8K">³Â¹ÚÏ£:ÎÒµ±Å¼ÏñÒ²ºÜ¿à</a> <a href="httpdisabled://v.163.com/video/2011/4/8/C/V70468T8C.html#ld=V675K0B8K">Ó¾×°·è¿ñParty</a></li>
+ <li><a href="httpdisabled://v.163.com/doc/"><em class='I_V_'>[¼Íʵ]</em></a> <a href="httpdisabled://v.163.com/video/2011/3/R/H/V6V3FL1RH.html#ld=V5G0IPI1S">ÓɵÁĹÒý·¢µÄÁ¬»·Ñª°¸</a> <a href="httpdisabled://v.163.com/video/2011/3/7/H/V6VGDIF7H.html#ld=V5G0IPI1S">½ÒÃØ¡°ÎüÐǴ󷨡±</a></li>
+ <li><a href="httpdisabled://v.163.void/"><em class=' I_V_'>[¹«¿ª¿Î]</em></a> <a target="_blank" href="httpdisabled://v.163.com/special/programming/">¡¶±à³Ì·½·¨Ñ§¡·µÚ14¿Î£º¼ÆËã»úÄÚ´æ</a></li>
+ <li><a href="httpdisabled://v.163.void/"><em class=' I_V_'>[¹«¿ª¿Î]</em></a> <a target="_blank" href="httpdisabled://v.163.com/voidcourse/equations.html">пÎÍƼö£ºÂéÊ¡¡¶Î¢·Ö·½³Ì¡·1-3¿Î</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://v.163.com/special/weekly_e/"><em class='cBlack fB'>Ò×ÖÜ¿¯</em></a> | <a target="_blank" href="httpdisabled://v.163.com/special/issue201052/">Èç¹ûÄãÖÐÁËһǧÍò</a> | <a target="_blank" href="httpdisabled://v.163.com/special/issue201051/">¿åµôµÄºÃѧÉúÒ©¼ÒöÎ</a></li>
+ <li><a href="httpdisabled://v.163.com/special/nvtop34/"><em class='cBlack fB'>·çÔÆ°ñ</em></a> | <a target="_blank" href="httpdisabled://v.163.com/special/nvtop34/">öµÑÀ¸çÍøÂ籬ºì</a> | <a target="_blank" href="httpdisabled://v.163.com/special/nvtop33/">ÄÐ×Ó±»±ëº·Å®ÈËÇ¿ÎÇ</a></li>
+ <li><a href="httpdisabled://v.163.com/zongyi/"><em class='fB'>×ÛÒÕ</em></a> | <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/R/4/V705BELR4.html#ld=V5MC3DG9J">¡¶ladyßÉßÉ¡·¡°Ã÷ÐÇÀ±Â衱ÁõÜ¿ º¢×ÓÊ×Æعâ</a></li>
+ <li><a href="httpdisabled://v.163.com/zongyi/"><em class='cBlack fB'>×ÛÒÕ</em></a> | <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/F/U/V7059EDFU.html#ld=V5MC3DG9J">¡¶Ëµ³öÄãµÄ¹ÊÊ¡·ÖÜѸÒä³É³¤ ÔøΪÀîÃ×Í´¿Þ</a></li>
+ <li><a href="httpdisabled://v.163.void/"><em class='fB'>°®ÉϹ«¿ª¿Î</em></a> | <a target="_blank" href="httpdisabled://v.163.com/special/fudanseminars4/"><em class='cDRed'>¸´µ©Ê׿ªÍøÂ繫¿ª¿Î¡¶Ö´ÞֵĵÍÒô¡·</em></a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://v.163.com/video/2011/4/0/2/V70647J02.html#ld=V68F91IG1"><img src="../img1.cache.netease.com/cnews/2011/4/8/201104080934433598e.jpg" alt="Ò©¼Òöο´ÊØËù³ª" title="Ò©¼Òöο´ÊØËù³ª" height="90" width="120" /><cite>Ò©¼Òöο´ÊØËù³ª<´«Ææ></cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://v.163.com/video/2011/4/G/P/V7066ANGP.html#ld=V6VAOISBD">ʵÅÄ£ºÍµ³µÔô±»×¥ÔâÊÐÃñȺŹ</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://v.163.com/video/2011/4/O/7/V703EJFO7.html#ld=V6VAOISBD">À¬»øÏ侪ÏÖÈ˽ŠÒɽØÖ«ºó¶ªÆú</a></li>
+ <li><a href="httpdisabled://v.163.com/video/2011/4/T/U/V703FSBTU.html#ld=V6VAOISBD">ΪŮÓÑÂòÀñÎïС»ï¾¹´ò½Ù³¬ÊÐ</a></li>
+ <li><a href="httpdisabled://v.163.com/video/2011/4/C/T/V703EB4CT.html#ld=V661IGBQL">¹úÍⳬÏÖʵ¶ÌƬ¡¶ÉúËÀÈý·ÖÖÓ¡·</a></li>
+ <li><a href="httpdisabled://v.163.com/video/2011/4/G/T/V703JHKGT.html#ld=V6VAOJSVO">˾»úÒò30ԪŹ´òÊÕ·ÑÔ±ÄðѪ°¸ </a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list dotline">
+ <li><a href="httpdisabled://v.163.com/special/008546TE/jinyeyouxi.html"><em class=' I_V_'>[½ñÒ¹ÓÐÏ·]</em></a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/8/I/V705AML8I.html#ld=V5MC3DG9J">ÕŹúÇ¿ËÄʮһ ½âÃÜÖÐÄêÄÐÈËÃÜÂë</a></li>
+ <li><a href="httpdisabled://v.163.com/special/008546TE/zuijiaxianchang.html"><em class=' I_V_'>[×î¼ÑÏÖ³¡]</em></a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/K/L/V705BEIKL.html#ld=V5MC3DG9J">´¢ÖDz© Íõ¾²Ò»¶ÔºÃÅóÓѵÄϲÀÖ¹ÊÊÂ</a></li>
+ <li><a href="httpdisabled://v.163.com/special/scndgs/"><em class=' I_V_'>[˵³öÄãµÄ¹ÊÊÂ]</em></a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/R/G/V702S4ARG.html#ld=V5MC3DG9J">ÖÜѸÒä³É³¤Àú³Ì ÔøΪÀîÃ×Í´¿Þ</a></li>
+ <li><a href="httpdisabled://v.163.com/special/fnms/"><em class=' I_V_'>[·ÇÄãĪÊô]</em></a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/G/U/V6VR12VGU.html#ld=V6VD27JAE">ÑÇÖÞС½ãºÎÑÇÃÈÇóÖ° ÀÏ°å·è¿ñÇÀ¶á</a></li>
+ <li><a href="httpdisabled://v.163.com/special/008546TD/fcwrzh.html"><em class=' I_V_'>[·Ç³ÏÎðÈÅ]</em></a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/Q/2/V6VPDSQQ2.html#ld=V6VD26AIN">"Èî¾­Ìì"ÒýÕù¶áÕ½ ÀÖ¼ÎÆØÁµ°®¾­Àú</a></li>
+ <li><a href="httpdisabled://v.163.com/special/008546TE/yanglan.html"><em class=' I_V_'>[ÑîÀ½·Ã̸¼]</em></a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/8/9/V6VOI2089.html#ld=V5MC3DG9J">´ÈÉƼұ˵ᤰͷÆÌØ£º×öÄã×Ô¼º</a></li>
+ </ul>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://v.163.com/special/00853MGO/cqgs.html"><em class=' I_V_'>[´«Ææ¹ÊÊÂ]</em></a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/C/J/V702JAJCJ.html#ld=V5HPAJTGO">Å®×Óлé1ÄêÔâÕÉ·òÅ°´ýÖÂËÀ ÉÏ</a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/S/2/V7055T0S2.html#ld=V5HPAJTGO">ÏÂ</a></li>
+ <li><a href="httpdisabled://v.163.com/special/00853MGO/wgjgs"><em class=' I_V_'>[Íõ¸Õ½²¹ÊÊÂ]</em></a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/U/D/V702JFSUD.html#ld=V6MUD7LC2">ÏÊ»¨±³ºóµÄѪ°¸</a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/6/3/V7058U863.html#ld=V6MUD7LC2">²¡Î£ÕÉ·òµÄÐÁËáÊÂ</a></li>
+ <li><a href="httpdisabled://v.163.com/special/00853MGO/tv_fzjxs.html"><em class=' I_V_'>[·¨ÖνøÐÐʱ]</em></a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/L/P/V701DKSLP.html#ld=V5G0COMVI">ÇàÄêÄÐÅ®¾ÆµêÄÚÎü¶¾ÒùÂÒ°ËÈ˱»ÇÜ</a></li>
+ <li><a href="httpdisabled://v.163.com/special/ssll/"><em class=' I_V_'>[˵ÊÂÀ­Àí]</em></a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/B/R/V702LN9BR.html#ld=V6TN5CABJ">¸É²¿¡°Ç¿¼é¡±Å®ÑÝÔ±25Äêºó±»ÅÐÎÞ×ï</a></li>
+ <li><a href="httpdisabled://v.163.com/special/gszx/"><em class=' I_V_'>[¹ÉÊÐÔÚÏß]</em></a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/0/M/V703MO50M.html#ld=V6UODFJCN">ͨÕͱ³¾°ÏÂÑ¡¹É</a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/M/7/V703O55M7.html#ld=V6UODFJCN">3000µã³ÉΪÐÂÆðµã</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://v.163.com/video/2011/4/F/7/V701QHKF7.html#ld=V675K0B8K"><img src="../img4.cache.netease.com/video/2011/4/8/2011040809594909a0a.jpg" alt="93ÄêͯÐÇÈë½­É´ç±Ð´Õæ" title="93ÄêͯÐÇÈë½­É´ç±Ð´Õæ" height="90" width="120" /><cite>93ÄêͯÐÇÈë½­É´ç±Ð´Õæ</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://v.163.com/video/2011/4/L/I/V705D74LI.html#ld=V62BI5Q6R">¿´Ã÷ÐDZÙÒ¥¸ßÕдò±£ÎÀÕ½ A</a> <a href="httpdisabled://v.163.com/video/2011/4/H/S/V705DCNHS.html#ld=V62BI5Q6R">B</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://v.163.com/video/2011/4/H/R/V703T9UHR.html#ld=V5PO8FVQG">¼õ·ÊÃÀÌå¶ÇƤÎè</a> <a href="httpdisabled://v.163.com/video/2011/4/M/T/V701PBHMT.html#ld=V5PO8FVQG">ÊÝÍÈ·¨Èý²¿Çú</a></li>
+ <li><a href="httpdisabled://v.163.com/video/2011/4/K/4/V7045LTK4.html#ld=V5PO8E0TJ">½ñ´º±Ø°ÜÏÔÊÝÆ· Ï°ëÉí³¬Îü¾¦</a></li>
+ <li><a href="httpdisabled://v.163.com/video/2011/3/A/J/V6VAD2AAJ.html#ld=V6MRLT82B">Õâ±²×ÓÒ»¶¨ÒªÈ¥µÄ³¬ÃÀÃλþ°µã</a></li>
+ <li><a href="httpdisabled://v.163.com/video/2011/4/M/B/V702I9JMB.html#ld=V6PBSMULG">ÎÒ°®ËÀÈ¥½ã½ãµÄÀϹ«ÒªÓëËû½á»é</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list dotline">
+ <li><a href="httpdisabled://v.163.com/fashion/"><em class='I_V_'>[·çÉÐ]</em></a> <a href="httpdisabled://v.163.com/video/2011/4/A/6/V701RGNA6.html#ld=V675K0B8K">Ella²àÅĺÿɰ®</a> <a href="httpdisabled://v.163.com/video/2011/4/H/8/V6VT89HH8.html#ld=V7012O1MC">Íø¹ºÉݳÞÆ·¿Éר¹ñÑé»õ?</a></li>
+ <li><a href="httpdisabled://v.163.com/fashion/"><em class=' I_V_'>[¸öÐÔ]</em></a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/D/3/V704A8UD3.html#ld=V675K0B8K">AmandaËÄÔÂÒýÁìÀÙË¿·ç³±</a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/A/1/V704AQ2A1.html#ld=V675K0B8K">±±Å·É­Å®·ç¸ñ</a></li>
+ <li><a href="httpdisabled://v.163.com/fashion/"><em class=' I_V_'>[·ÖÏí]</em></a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/L/E/V704AD6LE.html#ld=V62LNRVBJ">ÊÝСÄÐÉúµÄ´©ÒÂÐÄ»ú</a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/7/M/V703S547M.html#ld=V62LNRVBJ">Ã×Ъ¶û×îвÊ×±°Ü¼Ò</a></li>
+ <li><a href="httpdisabled://v.163.com/special/nrwzd/"><em class=' I_V_'>[Å®ÈËÎÒ×î´ó]</em></a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/4/Q/V701K1I4Q.html#ld=V5PO8E0TJ">³¬ÈËÆø£¡´ºÏÄÈÈÂôÈÕϵÉÌÆ·Ô¤¸æ</a></li>
+ <li><a href="httpdisabled://v.163.com/special/mlthg/"><em class=' I_V_'>[ÂéÀ±Ììºó¹¬]</em></a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/J/6/V7045AJJ6.html#ld=V69VV7M1S">ÎÞÂÛСÈýÔõôÏùÕÅÆÛ¸º ÎÒ»¹ÒªÍì»Ø</a></li>
+ <li><a href="httpdisabled://v.163.com/special/shgj/"><em class=' I_V_'>[Éú»î¹ã½Ç]</em></a> <a target="_blank" href="httpdisabled://v.163.com/video/2011/4/I/1/V702U0SI1.html#ld=V6KJJ2B07">Ç×Éú½ãµÜÓÉ°®×ªºÞ ¾Þ¶îË÷ÅâµÄÕæÏà</a></li>
+ </ul>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://v.163.com/doc/"><em class='I_V_'>[¼Íʵ]</em></a> <a href="httpdisabled://v.163.com/video/2011/4/B/N/V6VQPQABN.html#ld=V5O75D4ND">÷ÑÞ·¼ÒòÕŹúÈÙ¶øËÀ£¿</a> <a href="httpdisabled://v.163.com/video/2011/4/F/0/V701J90F0.html#ld=V5G0IPI1S">¹Ö²¡²øÉíËļ¾ÍÑƤ</a></li>
+ <li><a href="httpdisabled://v.163.com/doc/"><em class='I_V_'>[ÈËÎï]</em></a> <a href="httpdisabled://v.163.com/video/2011/3/V/8/V6VBJ1TV8.html#ld=V5O75D4ND">Ò»´úèÉÐÛÕÅ×÷ÁØ</a> <a href="httpdisabled://v.163.com/video/2011/4/F/G/V6VKMPNFG.html#ld=V5PQKE4F5">ÆÞ×ÓÓò˵¶¿³ÕÉ·ò16µ¶</a></li>
+ <li><a href="httpdisabled://v.163.com/doc/"><em class='I_V_'>[¿Ö²À]</em></a> <a href="httpdisabled://v.163.com/video/2011/3/N/G/V6VE9MGNG.html#ld=V5G0IPI1S">ÍâÐÇÎüѪ¹í·ÃµØÇò</a> <a href="httpdisabled://v.163.com/video/2011/3/7/A/V6VFHVH7A.html#ld=V5O75D4ND">¸üÔ©¸ü²Ò!ÑîÈý½ã¸æ×´</a></li>
+ <li><a href="httpdisabled://v.163.com/doc/"><em class='I_V_'>[ÀúÊ·]</em></a> <a href="httpdisabled://v.163.com/video/2011/3/8/B/V6UUJ0R8B.html#ld=V5O75AD5H">Õë¶ÔÃÀ¹úµÄ¿Ö²À¿ÕÏ®</a> <a href="httpdisabled://v.163.com/video/2011/3/A/S/V6V5DGJAS.html#ld=V5G0IPI1S">¼ûѪ·âºíÆæÒì¹ÖÊ÷</a></li>
+ <li><a href="httpdisabled://v.163.com/doc/"><em class='I_V_'>[̽Ë÷]</em></a> <a href="httpdisabled://v.163.com/video/2011/3/N/G/V6V02Q0NG.html#ld=V5O75D4ND">ÈÕ±¾µÚÒ»ÉñÃØÅ®¼äµý</a> <a href="httpdisabled://v.163.com/video/2011/3/L/I/V6V08VQLI.html#ld=V5G0IPI1S">ÔâÓöµØÇòֹͣת¶¯£¡</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-3 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://v.163.com/special/introductiontopsychology/"><img src="../img3.cache.netease.com/video/2011/4/8/20110408143144afad3.jpg" alt="Ү³´óѧ£ºÐÄÀíѧµ¼ÂÛ" title="Ү³´óѧ£ºÐÄÀíѧµ¼ÂÛ" height="90" width="120" /><cite>Ү³´óѧ£ºÐÄÀíѧµ¼ÂÛ</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://v.163.com/special/programming/"><em class='cDRed'>ÐÂÍÆ£ºË¹Ì¹¸£¡¶±à³Ì·½·¨Ñ§¡·</em></a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://v.163.void/">[ÈËÎï]</a> <a href="httpdisabled://v.163.com/special/milton/">ÃÖ¶û¶Ù£ºÓ¢¹úÖøÃûÊ«ÈË</a></li>
+ <li><a href="httpdisabled://v.163.void/">[½ðÈÚ]</a> <a href="httpdisabled://v.163.com/special/financialmarkets/">½ðÈÚÊг¡£ºÓëÇ®¸ü½Ó½ü</a></li>
+ <li><a href="httpdisabled://v.163.void/">[ÒÕÊõ]</a> <a href="httpdisabled://v.163.com/special/listeningtomusic/">ñöÌýÒôÀÖ£ºáäáàÔÚÌìÌÃ</a></li>
+ <li><a href="httpdisabled://v.163.void/">[ÕÜѧ]</a> <a href="httpdisabled://v.163.com/special/justice/">¹«Õý£ºÌ½¾¿¹ÌÓйÛÄî</a></li>
+ </ul>
+ </div>
+ <div class="imgText-temp-3 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://v.163.com/voidcourse/cs50.html"><img src="../img4.cache.netease.com/video/2011/4/7/2011040715531564880.jpg" alt="¹þ·ð£º¼ÆËã»ú¿ÆѧCS50" title="¹þ·ð£º¼ÆËã»ú¿ÆѧCS50" height="90" width="120" /><cite>¹þ·ð£º¼ÆËã»ú¿ÆѧCS50</cite></a>
+</div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://v.163.com/10/1021/12/6JH5JG3V008563G9.html">ÍøÒ×¹«¿ª¿Î×îз­Òë½ø¶È</a></li>
+ <li><a href="httpdisabled://v.163.com/special/programming/">±à³Ì·½·¨Ñ§(14)</a> <a target="_blank" href="httpdisabled://v.163.com/voidcourse/algorithms.html">Ëã·¨µ¼ÂÛ(2)</a></li>
+ <li><a href="httpdisabled://v.163.com/special/sp/singlevariablecalculus.html">µ¥±äÁ¿Î¢»ý·Ö(11)</a> <a target="_blank" href="httpdisabled://v.163.com/voidcourse/knowledgewharton.html">ÎÖ¶ÙѧÎÊ(1)</a></li>
+ <li><a href="httpdisabled://v.163.com/voidcourse/weijifen.html">΢»ý·ÖÖصã(10)</a> <a target="_blank" href="httpdisabled://v.163.com/voidcourse/classicalmechanics.html">¾­µäÁ¦Ñ§(30)</a></li>
+ <li><a href="httpdisabled://v.163.com/voidcourse/iphonekaifa.html">iphone¿ª·¢½Ì³Ì(3)</a> <a target="_blank" href="httpdisabled://v.163.com/voidcourse/robotics.html">»úÆ÷ÈË(1)</a></li>
+ </ul>
+ </div>
+ <div class="imgText-temp-3 clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://img4.cache.netease.com/video/2011/4/8/20110408152901a2026.jpg"><img src="../img4.cache.netease.com/video/2011/4/7/20110407105038a01d2.jpg" alt="¡¶ÅúÅÐÐÔÍÆÀíÈëÃÅ¡·" title="¡¶ÅúÅÐÐÔÍÆÀíÈëÃÅ¡·" height="90" width="120" /><cite>¡¶ÅúÅÐÐÔÍÆÀíÈëÃÅ¡·</cite></a>
+</div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://v.163.com/voidcourse/equations.html">Ð¿ΣºÂéÊ¡¡¶Î¢·Ö·½³Ì¡·1-3¿Î</a></li>
+ <li><a href="httpdisabled://v.163.com/special/innercore/">ÈËÐÔ</a> | <a target="_blank" href="httpdisabled://v.163.com/special/philosophy-death/">ËÀÍö</a> | <a target="_blank" href="httpdisabled://v.163.com/special/justice/">¹«Õý</a> | <a target="_blank" href="httpdisabled://v.163.com/voidcourse/anthropology.html">ÈËÀàѧ</a></li>
+ <li><a href="httpdisabled://v.163.com/special/introductiontotheoldtestament/">¾ÉԼȫÊéµ¼ÂÛ</a> | <a href="httpdisabled://v.163.com/special/introductiontopsychology/">ÐÄÀíѧµ¼ÂÛ</a></li>
+ <li><a href="httpdisabled://v.163.com/special/socialcognition/">Éç»áÈÏÖªÐÄÀíѧ</a> | <a target="_blank" href="httpdisabled://v.163.com/special/profilesinleadership/">Áìµ¼ÄÜÁ¦</a></li>
+ <li><a href="httpdisabled://v.163.com/10/1019/15/6JCC3DT8008563GR.html"><em class='cDRed'>¹«¿ª¿ÎƵµÀ³¤ÆÚÕÐļ·­ÒëÈËÔ±</em></a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="area-sub">
+ <div id="product" class="mod product-mod">
+ <h2 class="hd clearfix">
+ <span class="mod-title"><a href="httpdisabled://sitemap.163.com/">ÍøÒ×ÍƼö</a></span>
+ </h2>
+ <div class="bd tab-bd display-control">
+ <div class="tab-con current">
+ <div class="product-map cBlue">
+ <div class="product-item-mail clearfix">
+ <strong><a class="icon" href="httpdisabled://email.163.com/">ÓÊÏä</a></strong>
+ <a href="httpdisabled://email.163.com/">Ãâ·ÑÓÊÏä</a> <a href="httpdisabled://vipmail.163.com/">VIPÓÊÏä</a> <a href="httpdisabled://qiye.163.com/">ÆóÒµÓÊÏä</a> <a href="httpdisabled://fm.163.com/client">ÓÊÏä¿Í»§¶Ë</a>
+ </div>
+ <div class="product-item-game clearfix">
+ <strong><a href="httpdisabled://nie.163.com/" class="icon">ÓÎÏ·</a></strong>
+ <a href="httpdisabled://xyq.163.com/">ÃλÃÎ÷ÓÎ</a> <a href="httpdisabled://xy2.163.com/">´ó»°Î÷Ó΢ò</a> <a href="httpdisabled://tx2.163.com/">ÌìÏ·¡</a> <a href="httpdisabled://xy3.163.com/">´ó»°Î÷ÓÎ3</a> <a href="httpdisabled://www.warcraftchina.com/">ħÊÞÊÀ½ç</a> <a href="httpdisabled://pk.163.com/">Õ½¸è</a> <a href="httpdisabled://ff.163.com/">зɷÉ</a> <a href="httpdisabled://dt2.163.com/fab.html?from=163">´óÌÆÎÞË«</a> <a href="httpdisabled://csxy.163.com/">´´ÊÀÎ÷ÓÎ</a> <a href="httpdisabled://qn.163.com/" style="color:#ba2636;">ٻŮÓÄ»ê</a>
+ </div>
+ <div class="product-item-serv clearfix">
+ <strong><a href="httpdisabled://sitemap.163.com/" class="icon">ÉçÇø</a></strong>
+ <a href="httpdisabled://blog.163.com/?fromService">²©¿Í</a> <a href="httpdisabled://photo.163.com/">Ïà²á</a> <a href="httpdisabled://dream.163.com/">ÃλÃÈËÉú</a> <a href="httpdisabled://yuehui.163.com/">ͬ³ÇÔ¼»á</a>
+ </div>
+ <div class="product-item-reco clearfix">
+ <strong><a href="httpdisabled://sitemap.163.com/" class="icon">ÍƼö</a></strong>
+ <a href="httpdisabled://mall.163.com/">ÍøÒ×É̳Ç</a> <a href="httpdisabled://fanxian.163.com/?keyfrom=163indexleft.fanxian">¹ºÎï·µÏÖ</a> <a href="httpdisabled://caipiao.163.com/index.html?from=www">ÍøÒײÊƱ</a> <a href="httpdisabled://tuan.163.com/">ÍøÒ×ÍŹº</a> <a href="httpdisabled://cidian.youdao.com/">ÓеÀ´Êµä</a> <a href="httpdisabled://bafang.163.com/">ÍøÒ×°Ë·½</a> <a href="httpdisabled://L.163.com/">ÍøÒ×ÉÐÆ·</a> <a href="httpdisabled://m.163.com/newsapp/">ÐÂÎÅ¿Í»§¶Ë</a>
+ </div>
+ </div>
+ </div>
+ <div class="tab-con">
+ <ul class="product-list">
+ <li><span><a href="httpdisabled://mail.163.com/" class="item-mail c-entry">163ÓÊÏä</a></span></li>
+ <li><span><a href="httpdisabled://t.163.com/" class="item-microBlog c-entry">΢²©</a></span></li>
+ <li><span><a href="httpdisabled://blog.163.com/passportIn.do?entry=163" class="item-blog c-entry">²©¿Í</a></span></li>
+ <li><span><a href="httpdisabled://photo.163.com/?username=pInfo" class="item-photo c-entry">Ïà²á</a></span></li>
+ <li><span><a href="httpdisabled://tie.163.com/reply/myaction.jsp?action=reply&username=pInfo" class="item-tie c-entry">¸úÌù</a></span></li>
+ <li><span><a href="httpdisabled://i.money.163.com/" class="item-money c-entry">Ͷ×Ê</a></span></li>
+ <li><span><a href="httpdisabled://wan.163.com/" class="item-wan c-entry">ÍøÒ³ÓÎÏ·µ¼º½</a></span></li>
+ <li><span><a href="httpdisabled://book.163.com/" class="item-book c-entry">¶ÁÊéÖÐÐÄ</a></span></li>
+ <li><span><a href="httpdisabled://yuehui.163.com/myidate.do" class="item-yuehui c-entry">ͬ³ÇÔ¼»á</a></span></li>
+ <li><span><a href="httpdisabled://caipiao.163.com/" class="item-caipiao c-entry">²ÊƱ</a></span></li>
+ </ul>
+ <span class="product-entry"><a href="httpdisabled://reg.163.com/" class="c-entry">½øÈëÍøÒ×ͨÐÐÖ¤<span class="code-en">&raquo;</span> </a></span>
+ </div>
+ </div>
+ </div>
+<div id="g5n1" class="mod wgt-tab">
+<div class="tab-hd tab-u-5 clearfix">
+ <span class="tab-u current">×ÊѶ</span>
+ <span class="tab-u">»î¶¯</span>
+ <span class="tab-u">ÍƼö</span>
+ <span class="tab-u">¾«Æ·</span>
+ </div>
+ <div class="bd display-control">
+ <div class="tab-con current">
+<ul class="mod-list sub-list">
+<li class="title"><a href="httpdisabledsdisabled://epay.163.com/notice/chongzhi.jsp">ÊÖ»ú¿¨³äÖµÒ²ÄÜÍø¹ºÀ²</a></li>
+<li><a href="httpdisabled://lady.163.com/special/00261ID9/2009DeluxeReport.html">×îÊÜÉÌÎñÈËÊ¿ÖÓ°®Æ·ÅÆ</a></li>
+<li><a href="httpdisabled://survey2.163.com/html/dict_youdao2011q1/paper.html">ÇáµãÊó±êÓ®¾ªÏ²´ó½±</a></li>
+<li><a href="httpdisabled://mail.163.com/html/110127_imap/index.htm">ÊÓƵ½ÌÄãÉèÖÃÓÊÏäIMAP</a></li>
+<li><a href="httpdisabled://pmxj.wan.163.com/">Æ®Ãì¶à·çÔÆÏɽ£ÏÔÎäÁÖ</a></li>
+<li><a href="httpdisabled://money.163.com/2011NAEC/">2011ÍøÒ×¾­¼Ãѧ¼ÒÄê»á</a></li>
+ </ul>
+ <ul class="mod-list sub-list">
+<li><a href="httpdisabled://tech.163.com/11/0322/10/6VO99FLI000915BF.html">ÍøÒ×ÐÂÎÅ¿Í»§¶Ë¡ÖØÉÏÏß</a></li>
+<li><a href="httpdisabled://g.163.com/a?CID=240&Values=53963900&Redirect=http://mhxx.dream.163.com?101222mhxx002">¹úÄÚÊ׿î2Dºá°æÍøÓÎ</a></li>
+<li><a href="httpdisabled://xyq.163.com/xyq?acctsn=MH22-2128-3554-7248">ºÍËûÒ»Æð×ö×îÀËÂþµÄÊÂ</a></li>
+<li><a href="httpdisabled://yuehui.163.com/">ллÍøÒ×ÈÃÎÒÓö¼ûÁËËû</a></li>
+<li><a href="httpdisabled://bafang.163.com/">²é¿´Å®ÓѾßÌåλÖà </a></li>
+<li><a href="httpdisabled://v.163.void/">ÍøÒ×¹«¿ª¿Î ºÃ¿ÎÃâ·ÑÌý</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ </div>
+ <div class="tab-con">
+ </div>
+ <div class="tab-con">
+ </div>
+ </div>
+ </div>
+ <script>
+ NTES.ready( function(){
+ var aChange = new AChange({
+ temp: "/special/00774IHC/11zy1-",
+ content: "#g5n1",
+ num: "5"
+ });
+});
+ </script>
+ </div>
+</div>
+<!-- end -->
+<div class="area">
+ <div class="clearfix">
+ <div class="area-sub">
+ <div class="gg gg-h100"><iframe src="../g.163.com/r@site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=logo190x100&amp;location=1.html" width="190" height="100" frameborder="0" border="0" marginwidth="0" marginheight="0" scrolling="no" ></iframe>
+</div>
+ </div>
+<div class="area-main">
+ <div class="main-col-10">
+ <iframe src="../g.163.com/r@site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=column390x100&amp;location=1.html" width="390" height="100" frameborder="0" border="0" marginwidth="0" marginheight="0" scrolling="no" ></iframe>
+
+ </div>
+ <div class="main-col-9">
+ <iframe src="../g.163.com/r@site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=column360x100&amp;location=1.html" width="360" height="100" frameborder="0" border="0" marginwidth="0" marginheight="0" scrolling="no" ></iframe>
+
+ </div>
+ </div>
+ </div>
+</div>
+<!-- sports & ent -->
+<div class="area">
+ <div class="area-main">
+ <div class="main-col-10">
+ <div id="sports" class="mod wgt-tab">
+ <h2 class="tab-hd tab-u-10 clearfix">
+ <span class="tab-u current"><a href="httpdisabled://sports.163.com/">ÌåÓý</a></span>
+ <span class="tab-u"><a href="httpdisabled://sports.163.com/nba/">NBA</a></span>
+ <span class="tab-u"><a href="httpdisabled://cbachina.163.com/">CBA</a></span>
+ <span class="tab-u"><a href="httpdisabled://sports.163.com/world/">¹ú¼Ê×ãÇò</a></span>
+ </h2>
+ <div class="bd tab-bd display-control">
+ <div class="tab-con current">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://sports.163.com/photoview/0AI90005/66272.html#p=714GEI7O0AI90005"><img src="../img4.cache.netease.com/sports/2011/4/8/20110408211535eae49.jpg" alt="ÉíÅûºþÈËÕ½ÅÛµÄÃ÷ÐÇ" title="ÉíÅûºþÈËÕ½ÅÛµÄÃ÷ÐÇ" height="90" width="120" /><cite>ÉíÅûºþÈËÕ½ÅÛµÄÃ÷ÐÇ</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://live.cbachina.163.com/2010/match/report/2010291.html">CBA-¹ã¶«ÏÕʤ×ܱȷÖ3-0½ú¼¶×ܾöÈü</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://live.cbachina.163.com/2010/match/report/2010290.html">н®Ê¤½­ËÕ </a> <a target="_blank" href="httpdisabled://cbachina.163.com/11/0408/22/715D57FI00052UUC.html">×ܾöÈüÁ¬ÐøÈýÄêÕ½¹ã¶«</a></li>
+ <li><a href="httpdisabled://cbachina.163.com/11/0408/10/7143UFAR00052UUC.html">ÄÐÀº¼¯ÑµÃûµ¥¹«²¼:Ò¦Ã÷ÍõÖÎÛ¤ÔÚÁÐ</a></li>
+ <li><a href="httpdisabled://t.163.com/zt/sports/ztqftdr">[΢·Ã̸]ÕÅÌúȪ:ÌìϵÚÒ»¿¿´ò³öÀ´</a></li>
+ <li><a href="httpdisabled://nba.sports.163.com/2010/match/preview/11887.html">7:30²¥ÈÈ»ð-ɽè</a> <a target="_blank" href="httpdisabled://nba.sports.163.com/2010/match/preview/11894.html">10:00ºþÈË-¿ªÍØÕß</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://sports.163.com/11/0409/00/715IAOR600051CA1.html" data-t-h="22">ÀºÍøÐû²¼µÂ¡½«½ÓÊÜÊÖÍóÊÖÊõ</a> <a target="_blank" href="httpdisabled://sports.163.com/11/0408/22/715CRAHE00051CA1.html">¾ôÊ¿ÖзæÄ´Ö¸¹ÇÁÑÈü¼¾±¨Ïú</a></li>
+ <li><a href="httpdisabled://sports.163.com/11/0408/12/71499QKD00051CA1.html">½Ü¿ËÑ·ÆؿƱÈÔøµ±ÃæÌôÐÆÇǵ¤</a> <a target="_blank" href="httpdisabled://sports.163.com/11/0408/18/7150FJDD00051CA1.html">ĪÀ×Ã÷È·±í̬ÓûÐøÔ¼Ò¦Ã÷</a></li>
+ <li><a href="httpdisabled://sports.163.com/11/0408/22/715CUOLQ00051CD5.html">ÆعúÃ×½«³öÊÛ˹ÄÚµÂÈûÈø¶ûÇóС·¨</a> <a target="_blank" href="httpdisabled://sports.163.com/11/0408/09/713UVQ5M00051CD5.html">ÆØÌØά˹ÃÜ»á¹úÃ׸߲ã</a></li>
+ <li><a href="httpdisabled://sports.163.com/11/0408/17/714R4JPT00051CCL.html">ÀûÎïÆÖÐû²¼½ÜÀ­µÂÈü¼¾±¨Ïú</a> <a target="_blank" href="httpdisabled://sports.163.com/11/0408/09/713UR5OM00051C8V.html">¸¥¸ñÉ­Óû´Ó»ÊÂíÍÚÆë´ïÄÚ°®×Ó</a></li>
+ <li><a href="httpdisabled://sports.163.com/11/0408/21/7157SB9S00051CAQ.html">É£À¼¸°ÃÀ´ò¹Ù˾Ë÷ÅâÒ»ÒÚÃÀÔª</a> <a target="_blank" href="httpdisabled://sports.163.com/photoview/0B6P0005/66274.html#p=714HUFL60B6P0005">ͼ¼¯:ÍôáÔÓë»·ÇòС½ã·ÖÊÖ</a></li>
+ <li><a href="httpdisabled://sports.163.com/photoview/00800005/66264.html#p=71400JTG00800005">ͼ¼¯:С±´·ò¸¾Íâ³ö¾Í²Í À±ÃÃ"ÔÐζ"È«ÎÞ</a> <a target="_blank" href="httpdisabled://sports.163.com/photoview/00DE0005/66263.html#p=713SVUCU00DE0005">CÂÞ¹Û¿´ÀºÇòÈü</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://sports.163.com/special/000502KO/lingdujiao.html" class="fB attitude">ÌåÓý</a> | <a href="httpdisabled://sports.163.com/special/000502KO/lingdujiao.html"><em class='cBlack fB'>Áã¶È½Ç</em></a> | <a target="_blank" href="httpdisabled://sports.163.com/special/nbabusiness/">NBAÇòÐǵÄÉÌÒµµÛ¹ú</a></li>
+ <li><a href="httpdisabled://sports.163.com/pl/"><em class='cBlack fB'>µ¥µ¶</em></a> | <a target="_blank" href="httpdisabled://tiyuriping.blog.163.com/blog/static/1636490272011388520798/">Å©ÃñÊÔѵǹÊÖÖ»ÊǸöÃÎ</a> | <a target="_blank" href="httpdisabled://litong1976.blog.163.com/blog/static/1211929201137115723970/">Öйú×ãÇò½øÈë´µNBʱ´ú</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://sports.163.com/photoview/0AI90005/66278.html#p=714P08VJ0AI90005"><img src="../img3.cache.netease.com/sports/2011/4/8/20110408224146ca253.jpg" alt="»ÊµÛ±³ºó¶àʵÄĸÇ×" title="»ÊµÛ±³ºó¶àʵÄĸÇ×" height="90" width="120" /><cite>»ÊµÛ±³ºó¶àʵÄĸÇ×</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://sports.163.com/11/0408/18/7150FJDD00051CA1.html">ĪÀ×±í̬ÓûÐøÔ¼Ò¦Ã÷̾ÆäÖÁ¹ØÖØÒª</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://nba.sports.163.com/2010/match/preview/11887.html">7:30ÈÈ»ð-ɽè</a> <a target="_blank" href="httpdisabled://nba.sports.163.com/2010/match/preview/11894.html">10µãºþÈË-¿ªÍØÕß</a></li>
+ <li><a href="httpdisabled://nba.sports.163.com/2010/match/report/11881.html">ÂÞ˹30+8¹«Å£Ê¤Â̾ü¹®¹Ì¶«²¿µÚÒ»</a></li>
+ <li><a href="httpdisabled://sports.163.com/11/0408/08/713SE3OP00051CA1.html">°ÝÄÉÄ·ÅÚºäÈ«¶Ó:ÎÒÃÇ´òµÃºÜÓÞ´À</a></li>
+ <li><a href="httpdisabled://sports.163.com/11/0408/12/71499QKD00051CA1.html">ìøʦÆعâ¿Æ±Èµ±ÃæÌôÐÆÇǵ¤¾ÉÊÂ</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://sports.163.com/11/0408/21/715A9F0600051CA1.html">¸ñÀï·Ò:´ÓδÏë¹ý×Ô¼ºÈç´Ë³É¹¦</a> <a target="_blank" href="httpdisabled://sports.163.com/11/0408/20/7155D72Q00051CA1.html">ÃÀý·íÒ»ÈËÊÇ"°¢ÁªµÚ¶þ"</a></li>
+ <li><a href="httpdisabled://sports.163.com/11/0408/09/713VIFML00051CA1.html">·Ñ¸ù:°¢Ë§ÈôÀëÈÎÂåÀïÊܳå»÷</a> <a target="_blank" href="httpdisabled://sports.163.com/11/0408/15/714L9VOT00051CA1.html">»ð¼ýÖÚ½«±íʾÂúÒâÏÖÓÐÕóÈÝ</a></li>
+ <li><a href="httpdisabled://sports.163.com/11/0408/19/7150Q2U000051CA1.html">ΤµÂΪ¼¾ºóÈü»òÔÝ·ÅÆú¸´³ö</a> <a target="_blank" href="httpdisabled://sports.163.com/11/0408/20/7155VG8I00051CA1.html">µ¤Æ¤¶ûºÀÑÔÈÈ»ðÖзæÎÞ¿ÉÆ¥µÐ</a></li>
+ <li><a href="httpdisabled://sports.163.com/special/nbabusiness/">²ß»®:NBAÇòÐǵÄÉÌÒµµÛ¹úµÞÔìÊ·</a> <a target="_blank" href="httpdisabled://sports.163.com/11/0408/10/7144S0VE00051CA1.html">ƤÅîÍ­ÏñÁÁÏàÁªºÏÖÐÐÄ</a></li>
+ <li><a href="httpdisabled://sports.163.com/11/0408/17/714R2FNM00051CA1.html">ÄÉʲÔÞС²¼Ä˳öÉ«µÃ·ÖÊÖ</a> <a target="_blank" href="httpdisabled://sports.163.com/11/0408/10/7142IAQJ00051CA1.html">¶ÅÀ¼ÌØ:MVPÆÀÑ¡ÎÞÊÓÎҵĴæÔÚ</a></li>
+ <li><a href="httpdisabled://sports.163.com/photoview/00MK0005/66282.html#p=7152GRIO00MK0005">ͼ¼¯:¿¨´÷ɺ±È»ùÄáÐԸзâÃæÕÕ</a> <a target="_blank" href="httpdisabled://v.sports.163.com/video/2011/4/4/2/V70646942.html"><em class=' I_V_'>»ÊµÛÖ÷Ì⶯»­ÕýʽÉÏÓ³</em></a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://sports.163.com/special/nbacolumn/"><em class='cBlack fB'>רÀ¸</em></a> | <a target="_blank" href="httpdisabled://sports.163.com/11/0406/14/70VC6HC500051CA1.html">ÍõÓñ¹ú:Âí´ÌÓÖÍæ¼ÙËÀ°ÑÏ· °Í¿ËÀûÔ¤ÑÔÄÜ·ñ³ÉÕæ?</a></li>
+ <li><a href="httpdisabled://sports.163.com/special/nbastatscool/"><em class='cBlack fB'>Êý¾Ý¿á</em></a> | <a target="_blank" href="httpdisabled://sports.163.com/11/0408/21/715ALJC400051CA1.html">ÂÞ˹Èü¼¾Öú¹¥ÆÆ600 ¾ôʿʤÂÊ28ÄêµÚ¶þµÍ</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://cbachina.163.com/photoview/4E9G0005/66277.html#p=714MR3E24E9G0005"><img src="../img3.cache.netease.com/sports/2011/4/8/20110408164530e0dfd.jpg" alt="ÄÐÀº¼¯Ñµ¶ÓÈ«Ãæ&quot;À©ÕÐ&quot;" title="ÄÐÀº¼¯Ñµ¶ÓÈ«Ãæ&quot;À©ÕÐ&quot;" height="90" width="120" /><cite>ÄÐÀº¼¯Ñµ¶ÓÈ«Ãæ"À©ÕÐ"</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://live.cbachina.163.com/2010/match/report/2010290.html">н®ºáɨ½­ËÕ</a> <a target="_blank" href="httpdisabled://cbachina.163.com/11/0408/22/715D57FI00052UUC.html">×ܾöÈüÔÙÏÖ½®ÔÁÕù°Ô</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://live.cbachina.163.com/2010/match/report/2010291.html"><em class=' I_V_'>¹ã¶«4·ÖÏÕʤºáɨ¶«Ý¸½ú¼¶×ܾöÈü</em></a></li>
+ <li><a href="httpdisabled://cbachina.163.com/11/0408/22/715E3J1K00052UUC.html">¶­å«÷ë×ÔÐŹ㶫Æ߳ɸÅÂʶáÏÂ×ܹھü</a></li>
+ <li><a href="httpdisabled://cbachina.163.com/11/0408/22/715C3R9500052UUC.html">¸ê¶ûÏ£Íû¹ã¶«¶á×ܹھü³ÆÍâÔ®¶¨³É°Ü</a></li>
+ <li><a href="httpdisabled://cbachina.163.com/11/0409/00/715JVM1M00052UUC.html" data-t-h="01">½¯ÐËȨ:¿ªÊ¼×¼±¸´ò¹ã¶«×ÊÁÏ»¹²»¹»</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://cbachina.163.com/11/0408/22/715COHAI00052UUC.html">¹ã¶«¶Óð󼲸´·¢ÈÇÅ­Àî´º½­</a> <a target="_blank" href="httpdisabled://cbachina.163.com/11/0409/00/715ILMMG00052UUC.html">ÓûӮн®ÍâÏßÏÞÖƶűȳɹؼü</a></li>
+ <li><a href="httpdisabled://cbachina.163.com/11/0408/21/715AA83R00052UUC.html">¹þµÂÉ­8¼ÇÈý·Ö¿ñì­41·Ö</a> <a target="_blank" href="httpdisabled://cbachina.163.com/11/0408/23/715F50CF00052UUC.html">¹þµÂÉ­:ûÏëË¢·Ö±ðÄÃÎҺͶűȱÈ</a></li>
+ <li><a href="httpdisabled://cbachina.163.com/11/0408/22/715D3BHS00052UUC.html">ԼʲÏÕÔìÄæתȴ·ÅÆúͶ¾øɱÇò</a> <a target="_blank" href="httpdisabled://cbachina.163.com/11/0408/22/715CM57Q00052UUC.html">¸ê¶ûÔÝͣʧÎóɥʧ·­ÅÌÁ¼»ú</a></li>
+ <li><a href="httpdisabled://cbachina.163.com/photoview/4E9G0005/66281.html#p=7151PV9O4E9G0005">ͼ¼¯-Öì·¼Óê°®×Ó³ÉÈü³¡Ð¡"ÀÍÄ£"</a> <a target="_blank" href="httpdisabled://cbachina.163.com/photoview/4E9G0005/66285.html#p=7155S2NQ4E9G0005">±¦±´Ð¡ÕæÕæ±äÉíÑݼ¼ÅÉ</a></li>
+ <li><a href="httpdisabled://cbachina.163.com/11/0408/10/7143UFAR00052UUC.html">ÄÐÀº37È˳¬´ó¼¯ÑµÃûµ¥Ò¦Ò×ÈëÑ¡</a> <a target="_blank" href="httpdisabled://cbachina.163.com/11/0408/22/715BB62Q00052UUC.html">ËÕȺ:¹«²¼¸úû¹«²¼Ò»Ñù</a></li>
+ <li><a href="httpdisabled://cbachina.163.com/11/0408/22/715DMJKQ00052UUC.html">áÛ·åÈü¹ù°¬Â×È·ÈÏ´ú±í¹ú¼Ê¶Ó³öÕ½</a> <a target="_blank" href="httpdisabled://cbachina.163.com/11/0408/15/714K4FN500052UUC.html">NCAAÇò¶ÓÑûµË»ªµÂÖ´½Ì</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://cbachina.163.com/photoview/4E9G0005/66280.html"><em class='cBlack fB'>ͼ¼¯</em></a> | <a target="_blank" href="httpdisabled://cbachina.163.com/photoview/4E9G0005/66280.html">³¬Ä£³¡Íâ"¼¤¶·"Ô¤ÑݾöÈü</a> <a target="_blank" href="httpdisabled://cbachina.163.com/photoview/4E9G0005/66251.html#p=712I7BHH4E9G0005">н®ÍâÔ®¹ÖÁ¦ËéÀº°å</a></li>
+ <li><a href="httpdisabled://t.163.com/zt/sports/nldmd"><em class='cBlack fB'>΢²©</em></a> | <a target="_blank" href="httpdisabled://t.163.com/zt/sports/nldmd ">ÄÐÀºÅÓ´ó¼¯ÑµÃûµ¥ÒýÈÈÒé</a> <a target="_blank" href="httpdisabled://t.163.com/zt/sports/tzd500">500ÍòÂòÌÆÕý¶«Öµ²»Öµ?</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://sports.163.com/11/0408/10/7143VMGH00051C8V.html"><img src="../img3.cache.netease.com/sports/2011/4/8/20110408234759dabf8.jpg" alt="Î÷¼×Ê®´ó90ºóÌì²Å" title="Î÷¼×Ê®´ó90ºóÌì²Å" height="90" width="120" /><cite>Î÷¼×Ê®´ó90ºóÌì²Å</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://sports.163.com/11/0408/22/715CUOLQ00051CD5.html">¹úÃ׳öÊÛ˹ÄÚµÂÈûÈø¶ûÈ«Á¦ÇóС·¨?</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://sports.163.com/11/0409/00/715KCC1I00051C8V.html" data-t-h="20">¹Ï˧£º°ÍÈø5½«ÊÖÊÆδÌôÐÆ»ÊÂí</a></li>
+ <li><a href="httpdisabled://sports.163.com/11/0408/08/713SLSN700051CD5.html">Ã×À¼Ë«ÐÛÓû·Ö±ðÖØ°õÇó¹ºCÂÞ÷Î÷?</a></li>
+ <li><a href="httpdisabled://sports.163.com/11/0408/08/713R6UHN00051C8V.html">°ÍÈøÓû¹º21ËêÌì²Å ËûÊÇÊÀ½ç±­½ðÑ¥</a></li>
+ <li><a href="httpdisabled://sports.163.com/11/0408/17/714R4JPT00051CCL.html">ÀûÎïÆÖ¹Ù·½Ö¤Êµ½ÜÀ­µÂ±¾Èü¼¾±¨Ïú</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://sports.163.com/11/0408/08/713TO53F00051CCL.html">Ó¢×ã×ÜвÆȲÃÅÐָ֤³Äá?</a> <a target="_blank" href="httpdisabled://sports.163.com/11/0408/19/71521IA800051CCL.html">¸¥¸ñÉ­·ßÅ­ ÅÚºäÓ¢×ã×ܲ»¹«Æ½</a></li>
+ <li><a href="httpdisabled://sports.163.com/11/0408/19/7153IF7S00051CCL.html">PFAÄê¶È×î¼ÑºòÑ¡:ÄÉÄáÈëÐÂÐãÃûµ¥</a> <a target="_blank" href="httpdisabled://sports.163.com/11/0408/16/714NR4BE00051CCL.html">ÂüÁªÎªÄÚά¶û°ì¼ÍÄîÈü</a></li>
+ <li><a href="httpdisabled://sports.163.com/11/0408/09/713V7CHJ00051CD5.html">¹úÃ×»òÓëÇжûÎ÷Õù¶á¹ÏµÏ°ÂÀ­</a> <a target="_blank" href="httpdisabled://sports.163.com/photoview/00CO0005/66290.html#p=715MNCNF00CO0005">ͼ¼¯:¹úÃ×ÀúÊ·´óÀ£°Ü»Ø¹Ë</a></li>
+ <li><a href="httpdisabled://sports.163.com/11/0408/09/7140ONHF00051C8V.html">»ÊÂíPKÂüÁªÕùÓ¢³¬¿Õ°ÔÌúÑü</a> <a target="_blank" href="httpdisabled://sports.163.com/11/0408/09/713UR5OM00051C8V.html">¸¥¸ñÉ­Óû´Ó»ÊÂíÍÚÆë´ïÄÚÖ®×Ó</a></li>
+ <li><a href="httpdisabled://sports.163.com/11/0408/11/7146C9G700051CD5.html">°ÝÈÊ1500Íò+1ÈËÇó¹ºÂÞ±ÈÄá°Â</a> <a target="_blank" href="httpdisabled://sports.163.com/photoview/00CO0005/66261.html#p=713RTJPK00CO0005">²©ÁÐÂåÐÂÅ®ÓÑÕýʽÆعâ(ͼ)</a></li>
+ <li><a href="httpdisabled://sports.163.com/11/0408/09/71415N6400051C97.html">ÀͶûÓÐÍûÐøԼɳ¶û¿ËÖÁ2013Äê</a> <a target="_blank" href="httpdisabled://sports.163.com/11/0408/09/7141DPCC00051C97.html">ÀÕ·òÅúÆÀµÂ¼×ÁªÈüÒý·¢ÖÚÅ­</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><strong><a href="httpdisabled://sports.163.com/world/">Èȵã</a></strong> | <a href="httpdisabled://sports.163.com/11/0408/19/7150SL9M00051C8V.html">ÅÁÀÕĪÐû²¼½ñÏÄÍËÒÛ</a> <a href="httpdisabled://sports.163.com/11/0408/13/714FA53300051C97.html"><em class='I_V_'>Ö£´óÊÀÍ·³¯µØ¾±×µÖØÉË</em></a></li>
+ <li><strong><a href="httpdisabled://sports.163.com/lottery/">²ÊƱ</a></strong> | <a href="httpdisabled://sports.163.com/11/0408/11/7146HD3N00052DT2.html">570ÍòµÃÖ÷¸ÐÑÔ:ûǮ±ðÅöÅ®ÈË</a> <a target="_blank" href="httpdisabled://sports.163.com/11/0408/22/715BUTMV00052DT2.html">×ã²Êר¼Ò36ÆÚ»ã×Ü</a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+<div class="gg gg-h100"><iframe src="../g.163.com/r@site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=column390x100&amp;location=2.html" width="390" height="100" frameborder="0" border="0" marginwidth="0" marginheight="0" scrolling="no" ></iframe>
+</div>
+ </div>
+ <div class="main-col-9">
+ <div id="ent" class="mod wgt-tab">
+ <h2 class="tab-hd tab-u-9 clearfix">
+ <span class="tab-u current"><a href="httpdisabled://ent.163.com/">ÓéÀÖ</a></span>
+ <span class="tab-u"><a href="httpdisabled://ent.163.com/movie/">µçÓ°</a></span>
+ <span class="tab-u"><a href="httpdisabled://ent.163.com/tv/">µçÊÓ</a></span>
+ <span class="tab-u"><a href="httpdisabled://ent.163.com/music/">ÒôÀÖ</a></span>
+ </h2>
+ <div class="bd tab-bd display-control">
+ <div class="tab-con current">
+ <div class="imgText-temp-1 dotline clearfix" style="height:130px;">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://ent.163.com/11/0408/18/714V0T5000031H2L.html"><img src="../img3.cache.netease.com/ent/2011/4/8/20110408183341f6142.jpg" alt="¾Æ¾®·¨×Ó̽·ÃÁ÷À˶ùͯ" title="¾Æ¾®·¨×Ó̽·ÃÁ÷À˶ùͯ" height="90" width="120" /><cite>¾Æ¾®·¨×Ó̽·ÃÁ÷À˶ùͯ</cite></a>
+</div>
+ <!-- some code --> <h3 class="main-title"><a href="httpdisabled://ent.163.com/11/0408/05/713I9FPR00031H2L.html">Ö£ÖлùÅ®ÓÑδ»éÏÈÔÐÃØÃÜ´ý²ú</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://ent.163.com/11/0408/04/713FNOTG00031H2L.html">Ç°ÑëÊÓÃû×ì·½ºê½øÀë»é°¸¿ªÍ¥</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/00/71329KLS00031H2L.html">Íõçóµ¤ÔâÓö³µ»ö¶îÍ··ìÕë</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/08/713SR0UD00031H2L.html">³ÂºÃÏÖÉí±±¾©Ð¡¸¹Â¡ÆðÒÉËÆÓÐÔÐ</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/09/7140UVP700032DGD.html">´«ãÆÄÝÒÑÀë»é½á½»80ºóÄÐÑÝÔ±</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://ent.163.com/special/2011wdyj/">[΢µçÓ°½Ú]µ¼Ñݳ´óÃ÷£ºÎ¢µçÓ°µÄ´´×÷ºÜ×ÔÓÉ</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/06/713KR54M00032DGD.html">¾Æ¾®·¨×ÓÅĽû¶¾Ðû´«Æ¬ ³ÆÑÝÒÕ¹¤×÷ÉÐÎ޼ƻ®(ͼ¼¯)</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/00/7132OPE500031H2L.html">¿ìÅ®Áõϧ¾ý±»ÆØÆ­È¡·ÛË¿Ç®²Æ ¾¯·½ÒÑÁ¢°¸Õì²é(ͼ)</a></li>
+ <li><a href="httpdisabled://ent.163.com/photoview/00AJ0003/43935.html#p=712RO3N100AJ0003">ͼ¼¯:Åí˳·¢»Ó¡¶B+Õì̽¡·´´Òâ ÊÖ»úÅÄÉã΢µçÓ°</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0407/21/712M5VGI00031H2L.html">¡¶ÈâÆÑÍÅ¡·Ö÷½ÇÔ­É´ÑëÀòÒýÍË</a> <a href="httpdisabled://ent.163.com/11/0408/07/713QLNTE00031H2L.html">À¶Ñà³ÎÇå×Ôɱ´«ÎÅ</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/03/713ACTAD00032DGD.html">Å˳¤½­×ÔÚ¼±ÈÕÔ±¾É½ÑóÆø</a> <a href="httpdisabled://ent.163.com/11/0408/01/7135VU5G00032DGD.html">·¶çâç÷:»éÀñ´©Æ½µ×Ь</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://ent.163.com/" class="fB attitude">ÓéÀÖ</a> | <a href="httpdisabled://ent.163.com/special/hybdhz/"><em class='fB'>ÐÐÒµ±¨µÀ</em></a>|<a target="_blank" href="httpdisabled://ent.163.com/special/pfhg1103/">3ÔµçӰƱ·¿Æ£Èí</a></li>
+ <li><a href="httpdisabled://ent.163.com/special/zhuanyejieduhuiz/"><em class='fB'>¼â·å»°Ìâ</em></a>|<a target="_blank" href="httpdisabled://ent.163.com/special/waixingren/ ">ÎÒÃÇÖ»»á³ç°Ý"ÍâÐÇÈË"</a>|<a target="_blank" href="httpdisabled://ent.163.com/special/weiquan/">½âÆÊ"µÁ°æÊÜÒæÕß"</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://ent.163.com/photoview/00B50003/43948.html#p=713PVANJ00B50003"><img src="../img4.cache.netease.com/ent/2011/4/8/2011040809044637924.jpg" alt="Êæä¿ÁõìÇÐÂƬÆغ£±¨" title="Êæä¿ÁõìÇÐÂƬÆغ£±¨" height="90" width="120" /><cite>Êæä¿ÁõìÇÐÂƬÆغ£±¨</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://ent.163.com/special/moviestory003/">[µçÓ°¹ÊÊÂ]¶Åç÷·å±±ÉÏÌÔ½ð¼Ç</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://ent.163.com/11/0407/22/712PQM1F000300B1.html">¡¶ºèÃÅÑç¡·¿ª»úÁõÒà·ÆÊÎÑÝÓݼ§</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/00/7130EP32000300B1.html">³Â´óÃ÷£ºÎ¢µçÓ°µÄ´´×÷ºÜ×ÔÓÉ</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0407/23/712SPQPE000300B1.html">Ò¦³¿·ñÈϲÎÑÝ·ëС¸ÕÐÂƬ</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0407/23/712SNS3P000300B1.html">3D¶¯»­Æ¬¡¶ÀïÔ¼´óðÏÕ¡·Ê×Ó³</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://ent.163.com/photoview/00B50003/43950.html#p=713QJ8A600B50003">¡¶ÍòÓÐÒýÁ¦¡·¶À¼Ò¾çÕÕ(ͼ¼¯)</a> <a target="_blank" href="httpdisabled://ent.163.com/11/0408/05/713I5GEN00032DGD.html">¹ùÌÎÑÝ´²Ï·ÒªÇëʾ</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/08/713SENLN000300B1.html">¡¶¹ØÔƳ¤¡·ÎäÏ·½âÃÜ</a> <a target="_blank" href="httpdisabled://ent.163.com/11/0408/08/713T8JU9000300B1.html">¡¶Õ½¹ú¡·Æض¯×÷ƪ»¨Ðõ</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/05/713I9IHA00032DGD.html">С¶¶«³ÉÎ÷¾Í¡·½«¿ª»ú</a> <a target="_blank" href="httpdisabled://ent.163.com/11/0408/00/71308QBC00032DGD.html">¡¶ÐÁº¥¸ïÃü¡·10ÔµDZ±ÃÀ</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/09/713UNFBJ000300B1.html">¡¶º©¶¹Ìع¤2¡··¢Ô¤¸æ</a> <a target="_blank" href="httpdisabled://ent.163.com/11/0408/03/713AAPJG00032DGD.html">¡¶¹þÀû²¨ÌØ¡·×îÖÕÕÂÊÔƬ</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/09/713UGCK3000300B1.html">¡¶Ëٶȼ¤Çé5¡·5Ô»òÉÏÓ³</a> <a target="_blank" href="httpdisabled://ent.163.com/11/0408/08/713U5GG5000300B1.html">¡¶ÉÙÅ®Ìع¥¶Ó¡·ÔªËضà</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/03/713AB9MR00032DGD.html">¡¶ÔÚÒ»Æð¡·°ì¼ûÃæ»á</a> <a target="_blank" href="httpdisabled://ent.163.com/11/0408/03/713ABDPP00032DGD.html">¡¶¿×Áîѧ¡·ËïÄþ»ñ·¶Î°Á¦Í¦</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://ent.163.com/special/moviestories/"><em class='fB'>µçÓ°¹ÊÊÂ</em></a> | <a target="_blank" href="httpdisabled://ent.163.com/special/moviestories002/">¡¶µ¶¼ûЦ¡·:ÎÚ¶ûÉƵĴ³¹ØÓÎÏ·</a></li>
+ <li><a href="httpdisabled://ent.163.com/special/hybdhz/"><em class='fB'>ÐÐÒµ±¨µÀ</em></a> |<a target="_blank" href="httpdisabled://ent.163.com/special/yinjin/">WTO²Ã¾öÄѺ³Ó°ÊÐ</a> <a target="_blank" href="httpdisabled://ent.163.com/special/hepaipianxinfengxiang/">ÖиۺÏÅÄƬзçÏò</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://ent.163.com/11/0408/00/712VKNLN00032KMI.html#p=713116NF00B70003"><img src="../img4.cache.netease.com/ent/2011/4/8/20110408074407aed87.jpg" alt="ÑîÃÝ:¹«Ë¾°²ÅÅÏàÇ×" title="ÑîÃÝ:¹«Ë¾°²ÅÅÏàÇ×" height="90" width="120" /><cite>ÑîÃÝ:¹«Ë¾°²ÅÅÏàÇ×</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://ent.163.com/11/0408/01/7135PRSL00032KMI.html">¡¶ÎÒµÄÇà´ºÔÚÑÓ°²¡·É±Çà</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://ent.163.com/11/0408/01/7135HBRN00031GVS.html">·´ÌØÐüÒɾ硶¸æÃÜÕß¡·¿ª²¥ </a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/09/713VFT8J00031GVS.html">Ó¢¹úÅĵçÊӰ桶̩̹Äá¿ËºÅ¡· </a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/02/7136UO4A00032KMI.html">³Â¼ü·æоç±äÅä½Ç ¼á³ÆÊÇÒ»Ïß</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/04/713GAV9700031GVS.html">¡¶Ïã¸ñÀïÀ­¡·Íê³ÉºóÆÚÖÆ×÷ </a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://ent.163.com/11/0408/08/713U93U000031GVS.html">¡¶ÀϹ«¿´ÄãµÄ¡·ÍÆÃ÷ÐÇר³¡ ÀÖ¼ÎÓëÓ«Ä»Çé¹ýÕÐ</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/08/713S8H0U00031GVS.html">Ò»Öܸ۾çÊÕÊÓ£ºÉîÒ¹¹í¾ç¡¶ÆߺŲî¹Ý¡·´óÈÈ</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/01/7134DU3A00032DGD.html">¡¶·Ç³Ï¡·±±¾©Ä¼¼Î±ö </a> <a target="_blank" href="httpdisabled://ent.163.com/11/0408/06/713KTRCS00032DGD.html">¡¶ÃÎÏëÐã¡·¸ßƵ¹ã¸æÒýÕùÒé</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/04/713F4HFV00031H2L.html">ÕÔ±¾É½½«ÅÄ"¶«±±ºÚµÀ·çÔÆ"</a> <a target="_blank" href="httpdisabled://ent.163.com/11/0408/06/713N005000032DGD.html">¡¶ÄÜÈË·ëÌì¹ó¡·½«²¥ </a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/05/713I9H1N00032DGD.html">ºþÄÏÎÀÊÓÍƺìÉ«Çà´º¼¾</a> <a target="_blank" href="httpdisabled://ent.163.com/11/0408/05/713I2OKR00032DGD.html">»ÆÖ¾ÖÒ¡¶¼Ò³£²Ë¡·ÑݺÃÄÐÈË</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/03/713ANRVL00032DGD.html">×ÛÒÕ½èÊÆÓ°ÊÓ¾ç³É·ç³±</a> <a target="_blank" href="httpdisabled://ent.163.com/11/0408/05/713I5HS400032DGD.html">¡¶·Ç³Ï¡·¸ðÏþÀڳɹ¦Ç£ÊÖ </a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://ent.163.com/special/jianfenghuathz/"><em class='cBlack fB'>¹Û¾ç</em></a> |<a target="_blank" href="httpdisabled://ent.163.com/special/shbfsgj/">¡¶²»·ÖÊÖ¡·Ì¨´Ê¾È³¡</a> |<a target="_blank" href="httpdisabled://ent.163.com/special/fscq/fscqgj.html">¡¶·çÉù¡·Ç°10¼¯¾«²Ê</a></li>
+ <li><a href="httpdisabled://ent.163.com/special/zhuanyejieduhuiz/"><em class='cBlack fB'>רҵ½â¶Á</em></a> |<a target="_blank" href="httpdisabled://ent.163.com/special/fscqcb/">¡¶·çÉù¡·´©°ï</a> | <a target="_blank" href="httpdisabled://ent.163.com/11/0329/17/70B4E9OE00034KRR.html">¡¶·çÉù¡·Ð̾ßÊ·¿¼</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://ent.163.com/11/0408/07/713PBK2E00031H0O.html"><img src="../img4.cache.netease.com/ent/2011/4/8/201104080804383b8a7.jpg" alt="ÖÁÉÏÀøºÏиèMVÊײ¥" title="ÖÁÉÏÀøºÏиèMVÊײ¥" height="90" width="120" /><cite>ÖÁÉÏÀøºÏиèMVÊײ¥</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://ent.163.com/11/0408/04/713G2HP300031H0O.html">Ã÷Äê¸ñÀ³ÃÀ½±Ï¼õÉÙ31¸ö</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://ent.163.com/11/0408/08/713SH5AN00031H0O.html">"¹â»ÔËêÔÂ"±±¾©Ê×ÑÝÈ·¶¨ÈÕÆÚ</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/07/713QMUBK00031H0O.html">»ªÓïÒôÀÖ´«Ã½´ó½±½ÒÏþÌáÃûÃûµ¥</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/07/713QJU7U00031H0O.html">ºÎÔÏÊ«×ÔÆØÐÔÈ¡Ïò¿ª·Å(ͼ)</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/07/713PORKJ00031H0O.html"><em class='I_M_'>¼Í¼ÑËÉ¡¶½ðÓãµÄÑÛÀá¡·Êײ¥</em></a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://ent.163.com/11/0407/19/712GLFKS00031H0O.html"><em class=' I_V_'>Ïô»ÍÆæ:¡¶Ã»ÄÇô¼òµ¥¡·²»¿É¸´ÖÆ</em></a> <a target="_blank" href="httpdisabled://ent.163.com/11/0407/15/7120Q4G5000334I9.html"><em class=' I_V_'>ÐíáÔÁÄеú</em></a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/02/71394QIE00032DGD.html">ÍõÁ¦ºêÎåÔ¹ãÖÝ¿ª³ª MUSIC MANÑݳª»á×îºóÒ»Õ¾</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/05/713I9H0T00032DGD.html">À¿ªÆôÐÂÒ»ÂÖѲÑÝ ÎåÒ»À´ÉϺ£³ª¡¶´«Ææ¡·</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/01/7135OVCO00032DGD.html">½Ü¿ËÑ·Ò½Éú·ñÈϹýʧɱÈË ³ÆMJÒò²ÆÕþΣ»ú×Ôɱ</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/01/7135OROS00032DGD.html">Áõϧ¾ý·ñÈÏÆ­È¡·ÛË¿Ç®²ÆÍæÏûʧ ¾Ü¾ø»ØÓ¦´«ÑÔ</a></li>
+ <li><a href="httpdisabled://ent.163.com/11/0408/01/7135OROR00032DGD.html">붫Èý¶ÈÎü¶¾¾Ü¾ø²É·Ã ³ªÆ¬¹«Ë¾È¡Ïû·¢²¼»á</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://ent.163.com/special/zhuanyejieduhuiz/"><em class='cBlack fB'>רҵ½â¶Á</em></a> | <a href="httpdisabled://ent.163.com/special/2011kuainvchuangxin/">2011¿ìÅ®Îå´ó´´ÐÂ</a> <a href="httpdisabled://ent.163.com/special/ynbbdlss/">È¥Ô½ÄÏ¿´±«²ªµÏÂ×</a></li>
+ <li><a href="httpdisabled://ent.163.com/special/hybdhz/"><em class='cBlack fB'>ÐÐÒµ±¨µÀ</em></a> | <a href="httpdisabled://ent.163.com/special/yinyuerenweiquan/">άȨսÖÐÒôÀÖ½çÊÇ·ñ±ÈÎÄѧ½çÈõÊÆ</a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+<div class="gg gg-h100"><iframe src="../g.163.com/r@site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=column360x100&amp;location=2.html" width="360" height="100" frameborder="0" border="0" marginwidth="0" marginheight="0" scrolling="no" ></iframe>
+</div>
+ </div>
+ </div>
+ <div class="area-sub">
+ <div id="gy" class="mod">
+ <div class="hd clearfix">
+ <h2 class="mod-title">ÍøÒ×¹«Ë¾ÐÂÎÅ</h2>
+ </div>
+ <div class="bd">
+ <ul class="mod-list sub-list dotline">
+ <li class="title"><a href="httpdisabled://tech.163.com/11/0401/17/70IS705000094JEO.html">ÍøÒ×΢²©·¢²¼°Ë°æÒƶ¯¿Í»§¶Ë</a></li>
+ <li><a href="httpdisabled://tech.163.com/11/0401/11/70I4EANC00094JEO.html">¡¶Ù»Å®ÓĻ꡷4ÔÂ22ÈÕÄÚ²â</a></li>
+ <li><a href="httpdisabled://tech.163.com/11/0401/17/70IP5SQ000094JEO.html">ÍøÒ×ÓÊÏä13ÖÜÄêÇìµäÍêÃÀÂäÄ»</a></li>
+ <li><a href="httpdisabled://tech.163.com/11/0325/17/700QC0T600094JEO.html">ÍøÒ×ÓÊÏä2G¸½¼þÉÏ´«ÌáËÙ3±¶</a></li>
+ <li><a href="httpdisabled://tech.163.com/11/0331/12/70FLRISG00094JEO.html">¶¡Àڼν±ÆóÒµÓÊÓÅÐã¾­ÏúÉÌ</a></li>
+ <li><a href="httpdisabled://tech.163.com/11/0329/10/70AB9A1P00094JEO.html">ÐǼÊÕù°Ô2´ó½¿ª·ÅÃâ·Ñ¹«²â</a></li>
+ </ul>
+ <ul class="mod-list sub-list">
+ <li><a href="httpdisabled://media.163.com/">[´«Ã½]</a> <a target="_blank" href="httpdisabled://media.163.com/11/0408/10/7142T8V800762H91.html">ÐÂÎÅÈÈÒé¾Ü¾ø¼Ù±¨µÀ</a></li>
+ <li><a href="httpdisabled://media.163.com/11/0408/10/71434VLS00762H91.html">Õ½ÕùÏÖ³¡Öйú¼ÇÕß²»È±Ï¯</a></li>
+ <li><a href="httpdisabled://media.163.com/11/0408/10/7142VL1R00762H91.html">΢²©ÀļÓVÖÂÍøÃñÐÅÈζÈϽµ</a></li>
+ <li><a href="httpdisabled://gongyi.163.com/">[¹«Òæ]</a> <a href="httpdisabled://gongyi.163.com/11/0401/18/70ISJFF500933KC8.html">ÓéÀÖÐű¨Óн±µ÷²é</a></li>
+ <li><a href="httpdisabled://gongyi.163.com/special/2011ditanditie/">2011µÍ̼µØÌúÉú»îÓн±µ÷²é</a></li>
+ </ul>
+ </div>
+ </div>
+<div class="gg gg-h180"><iframe src="../g.163.com/r@site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=logo190x180&amp;location=1.html" width="190" height="180" frameborder="0" border="0" marginwidth="0" marginheight="0" scrolling="no" ></iframe></div>
+ </div>
+</div>
+<!-- end -->
+<!-- money & auto -->
+<div class="area">
+ <div class="area-main">
+ <div class="main-col-10">
+ <div id="money" class="mod wgt-tab">
+ <h2 class="tab-hd tab-u-10 clearfix">
+ <span class="tab-u current"><a href="httpdisabled://money.163.com/">²Æ¾­</a></span>
+ <span class="tab-u"><a href="httpdisabled://money.163.com/stock/">¹ÉƱ</a></span>
+ <span class="tab-u"><a href="httpdisabled://biz.163.com/">ÉÌÒµ</a></span>
+ <span class="tab-u"><a href="httpdisabled://money.163.com/licai/">Àí²Æ</a></span>
+ </h2>
+ <div class="bd tab-bd display-control">
+ <div class="tab-con current">
+ <div class="imgText-temp-1 clearfix">
+
+ <div class="mod-img main-img">
+ <a href="httpdisabled://money.163.com/photoview/0HH40025/1431.html"><img src="../img4.cache.netease.com/stock/2011/4/8/20110408183832fdfa0.png" alt="ÍøÒײƾ­Ò»ÖÜͼƬ¾«Ñ¡" title="ÍøÒײƾ­Ò»ÖÜͼƬ¾«Ñ¡" height="90" width="120" /><cite>ÍøÒײƾ­Ò»ÖÜͼƬ¾«Ñ¡</cite></a>
+</div>
+
+ <h3 class="main-title"><a href="httpdisabled://money.163.com/special/oilcompany/">ÈýÓÍÆó¸ß²ãÉú±ä:¸µ³ÉÓñÕƶæÖÐʯ»¯</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://money.163.com/11/0408/10/7143PG4L002526O3.html">ÉϺ£µÏÊ¿ÄὫ½¨È«Çò×î¸ß×î´ó³Ç±¤</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/02/7138637T00253B0H.html">¹úÎñԺȦ¶¨16Ê¡¶½²éµØ·½Â¥Êе÷¿Ø</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/05/713HN73100253B0H.html">¹ú¼ÊÓͼÛ7ÈÕÍ»ÆÆ110ÃÀÔª ÔÙ´´Ð¸ß</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/02/7139D27200253B0H.html">±¦¸Öϵ÷5Ô¸ּÛ</a> <a target="_blank" href="httpdisabled://money.163.com/11/0408/06/713MUP7N002524SO.html">ÒµÄÚ³§ÉÌ»ò¸ú½ø</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://money.163.com/11/0408/06/713KEJ0600253B0H.html">·¢¸Äί£º¹ú¼Ò¶Ô¹ú¼ÊÓͼ۽øÒ»²½ÉÏÕÇÒÑÖƶ¨Ô¤°¸</a> <a target="_blank" href="httpdisabled://money.163.com/special/oilprice0406/">רÌâ</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/09/713VJK5S00253B0H.html">Õã½­ÒÚÍò¸»½ãÎâÓ¢¶þÉóÈÏ×ï·Ç·¨Îü´æ</a> <a target="_blank" href="httpdisabled://money.163.com/11/0408/09/713VJMM600253B0H.html">Í¥Íâ½Ò·¢Òý³öÎÑ°¸</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/04/713ENNS200253B0H.html">ÖØÇì11ÒÚ»ÝÅ©Éç±£½ðÔâ½ØÁô »ù²ã¸É²¿¼¯Ì帯°ÜÏÖÏóÍ»³ö</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0407/23/712T6DV500253B0H.html">Ìì½òÎĽ»ËùÄÚÄ»£ºÉñÃØ80ºó¹É¶«¾ªÏÖÄ»ºó²Ù¿ØȺ</a> <a target="_blank" href="httpdisabled://money.163.com/special/tjartwork/"> רÌâ</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0407/20/712HSFCS00252G50.html">±±Ê¦´ó½ÌÊÚ¶­·ª»ØÓ¦¡°4000Íò¡±Ö®Õù£ºÌ¸Ç®²¢²»ÒâζÃÄË×</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0407/18/712C24F3002534NU.html">±±¾©µØ²úÖнéÐû³ÆÄÜ°ìÄÉË°Ö¤Ã÷±»²é´¦</a> <a target="_blank" href="httpdisabled://money.163.com/11/0407/19/712EP0UR00253B0H.html">¹úÎñԺרÏ²é</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://money.163.com/" class="fB attitude">²Æ¾­</a> | <a href="httpdisabled://money.163.com/focus/"><em class='fB'>ÍøÒ×½â¶Á</em></a> | <a target="_blank" href="httpdisabled://money.163.com/special/focus271/">³ÇÊÐĹµØ²»¸Ã¹ý¶ÈÊг¡»¯</a></li>
+ <li><a href="httpdisabled://money.163.com/news/"><em class='cBlack fB'>ÍøÒ×µÚÒ»Ïß</em></a> | <a target="_blank" href="httpdisabled://money.163.com/special/news75/"><em class='cBlack'>¾©ÏÞ¹ºÁîÄ¥ºÏϸ½Ú ¿ª·¢É̶ڷ¿±¸Õ½Ï°ëÄê</em></a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://money.163.com/11/0408/02/71386IL500253B0H.html"><img src="../img4.cache.netease.com/stock/2011/3/1/201103010846298829b.jpg" alt="ÂÞ½Ü˹:»Æ½ð³å»÷2000$" title="ÂÞ½Ü˹:»Æ½ð³å»÷2000$" height="90" width="120" /><cite>ÂÞ½Ü˹:»Æ½ð³å»÷2000$</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://money.163.com/stock/">ÊÕÆÀ£º»¦Ö¸ËõÁ¿ÕÇ0.74%ËÄÁ¬Ñô</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://money.163.com/11/0408/02/71376E0N00253B0H.html">Å·ÖÞÑëÐÐÈýÄêÀ´Ê׶ȼÓÏ¢</a> <a target="_blank" href="httpdisabled://money.163.com/11/0408/02/7139D6I500253B0H.html">ÀûºÃA¹É</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/05/713HN73100253B0H.html">¹ú¼ÊÓͼÛÔÙ´´Ð¸ß</a> <a target="_blank" href="httpdisabled://money.163.com/11/0408/05/713GMQAC00254ITK.html">ÆÚ½ðÁ¬´´Ð¸ß</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/16/714N86G400254IU4.html">»¦Ö¸ËÄÁ¬Ñô 108ÒÚ×ʽð·äÓµÈ볡</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/02/7139CUD800253B0H.html">ÉϺ£µÏÊ¿Äá½ñÈÕÆÆÍÁ¶¯¹¤</a> <a target="_blank" href="httpdisabled://money.163.com/10/1115/07/6LH00STQ00253EOS.html">Ͷ×ʲßÂÔ</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://money.163.com/11/0408/11/7146IFIG00253B0H.html">°Í·ÆÌؽâÊ͵±ÄêͶÖÐʯÓÍ:ËûÃdzÐŵÀûÈó45%·Öºì </a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/02/71376LJR00253B0H.html">ɳ¸Ö¹É·ÝÍ£ÅƽüÒ»Äê</a> <a target="_blank" href="httpdisabled://money.163.com/11/0408/09/7140B64P00254IU5.html">½ñ»Ö¸´ÉÏÊпªÅÌÕǽü100%</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/20/71559J6E00253B0H.html">½¹Ì¿ÆÚ»õ4ÔÂ15ÈÕÉÏÊн»Ò× Ê×ÈÕÕǵøÍ£°å8%</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/02/71376ABD00253B0H.html">¹úÎñÔº¶½²é16Ê¡µØ²úµ÷¿Ø</a> <a target="_blank" href="httpdisabled://money.163.com/11/0408/02/7138637T00253B0H.html">»ò¾ÀÆ«µØ·½µ÷¿ØÖ¸±ê</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/07/713O2VBR00253B0H.html">Ë®ÄàÒµÒµ¼¨´ó·ùÔö³¤</a> <a target="_blank" href="httpdisabled://money.163.com/11/0408/02/7139CI7D00253B0H.html">ά³Ö¸ß¾°Æø</a> <a target="_blank" href="httpdisabled://money.163.com/11/0408/02/71376S3E00253B0H.html">»ú¹¹Ë³ÊÆÅ×ÊÛ</a></li>
+ <li><a href="httpdisabled://money.163.com/special/2010nianbao/">[Ä걨]</a> <a target="_blank" href="httpdisabled://money.163.com/11/0408/19/7150QKGR00254L67.html">ÖÐÐÅ֤ȯȥÄê¾»Àû113ÒÚÔª</a> <a target="_blank" href="httpdisabled://money.163.com/11/0408/18/71504JH800253B0H.html">±±¾©ÒøÐо»Àû68ÒÚÔª</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://money.163.com/stock/"><em class='cBlack fB'>Êг¡Èȵã</em></a> | <a href="httpdisabled://money.163.com/11/0408/02/71385HGS00253B0H.html">ÖÜÆڹɻ𱬠»ú¹¹·ê¸ß¼õ³Ö</a> <a href="httpdisabled://money.163.com/11/0408/02/7139DI3S00253B0H.html">À¶³ï¹ÉÂÖ¶¯¼ÌÐø</a></li>
+ <li><a href="httpdisabled://money.163.com/blog/"><em class='cBlack fB'>°Ù¼ÒÂÛÊÐ</em></a> | <a target="_blank" href="httpdisabled://money.163.com/11/0408/02/7139CI7C00253B0H.html">Ê®»ú¹¹£ºÓÐЧվÉÏ3000µã ÏòÉÏ¿Õ¼ä´ò¿ª</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://money.163.com/11/0408/09/713VLIUP00253B0H.html"><img src="../img4.cache.netease.com/stock/2011/4/8/201104080929109dd6d.png" alt="ÐìСƽ£ºÃ¤Í¶µ½¶¯Õæ¸ñ" title="ÐìСƽ£ºÃ¤Í¶µ½¶¯Õæ¸ñ" height="90" width="120" /><cite>ÐìСƽ£ºÃ¤Í¶µ½¶¯Õæ¸ñ</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://money.163.com/special/zgfneidou/">Õ湦·ò¼àÊÂÆðË̴߲ï±ê</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://money.163.com/11/0408/07/713OA76Q00253B0H.html">ÎÖ¶ûÂêÕýʽ½ø¾üÖйúÉÌÒµµØ²ú</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/01/7132TNP000253B0H.html">±¼³Û¡°Ë«ÏÞ¡±Áî¾­ÏúÉÌÏÝÁ½ÄÑ</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/01/7132TS2I00253B0H.html">337ÒÚÃÀÔª HTCÊÐÖµÊ׳¬Åµ»ùÑÇ</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/01/7132TJ0500253B0H.html">·ëÂØÒþÉíÄ»ºó רע´óÍòͨ×ʱ¾ÔË×÷</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://money.163.com/11/0408/07/713PJBIV00251LK6.html">Æ滢360¸ßÊÐÓ¯ÂÊÖ®ÀÛ ¿ª·Åƽ̨¶µÊÛÁ÷Á¿</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/08/713R58M500253B0H.html">²¨Òô¹«Ë¾¾ÍÆä737ÐÍ·É»ú³öÏÖÁÑ·ì×ö³ö½âÊÍ</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/07/713P24H800251LJJ.html">ËÄ´¨³¤ºç¾»Àû2.92ÒÚ ÄÑÑÚÏÖ½ðÁ÷Òþ»¼ÖØÖØ</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/09/713VJJMT00253B0H.html">ÖйúÆóÒµÐÅÏ¢°²È«Æð²½£ºÊ×ϯÒþ˽¹ÙºÎʱÂäµØ</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/08/713SB71S00252FPI.html">ÀîÕ×»ù10ÒÚÔö³Öºã»ùµØ²ú ÈϹºÖ¤³É½»Ôö³¤9±¶</a></li>
+ <li><a href="httpdisabled://t.163.com/rank/daren/1293764950255">[΢²©]</a> <a href="httpdisabled://t.163.com/xiabin">¾­¼Ãѧ¼ÒÏıó</a> <a href="httpdisabled://money.163.com/special/zgjrzl2020/">ÐÂÊ顶Öйú½ðÈÚÕ½ÂÔ£º2020¡·</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://money.163.com/interview/"><em class='cBlack fB'>²Æ¾­»á¿ÍÌü</em></a> | <a href="httpdisabled://money.163.com/special/sunzhenyao/">º£»Ô¼¯ÍÅËïÕñÒ«£ºÖйúÍâ°ü´óÓпÉΪ</a></li>
+ <li><a href="httpdisabled://money.163.com/economist/"><em class='cBlack fB'>Òâ¼ûÖйú</em></a> | <a href="httpdisabled://money.163.com/special/zfzhaoxiao/">ÕÔÏþ£ºÖйúȱ·¦ÏÖ´úÍÁµØ²úȨÖƶÈ</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://money.163.com/11/0408/08/713TFEA100253B0H.html"><img src="../img4.cache.netease.com/stock/2011/4/8/20110408121505602ea.jpg" alt="±±¾©½ðÊμ۸ñÈ«ÃæÉϵ÷" title="±±¾©½ðÊμ۸ñÈ«ÃæÉϵ÷" height="90" width="120" /><cite>±±¾©½ðÊμ۸ñÈ«ÃæÉϵ÷</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://money.163.com/11/0408/09/713UQTJ000253B0H.html">½ÒÃØÀí²Æ²úÆ·ÊÕÒæÈçºÎ¡°±»Ëã¼Æ¡±</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://money.163.com/11/0408/05/713JVDEB00253B0H.html">ÉÏÖÜ18¿îÀí²Æ²úÆ·ÅÜÓ®CPI</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/09/7140FBCV00252V0H.html">Ö½²¬½ðͶ×ÊÊܳè ʵÎﲬ½ðÓöÀä</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/09/7141FDSJ00252V0H.html">¶ÌÆÚÒâÍâÏÕÎÞÓÌÔ¥ÆÚ Í˱£ÓÐËðʧ</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/11/7147UDRA00252V0H.html">ÍòÄÜÏÕÅÜÊ䶨´æ ¶à¼Ò±£ÏÕ¹«Ë¾Í£ÊÛ</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://money.163.com/11/0409/00/715JD8ET00253B0H.html" data-t-h="00">½ð¼Û´´Ð¸ßÉϺ£Ïû·ÑÕß×·ÕÇ Ò»ÍíÊÛ³ö°Ù¸ù½ðÌõ</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/04/713GFH8C00253B0H.html">Àí²Æʦ½¨Òé¼ÓÏ¢Ô¤ÆÚÏÂÓÅÏÈ¿¼ÂÇÈý¸öÔ¶¨´æ</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/09/713VHFQ500253B0H.html">»õ»ùÊÕÒæË®ÕÇ´¬¸ß ´æ»îÆÚÒ»Ô²»ÈçÂò»õ»ùÒ»ÐÇÆÚ</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/03/713A9L7900253B0H.html">¹ú¼Ê´ó×ÚÉÌÆ·ÃÍÕÇ ¹Ò¹³Àí²Æ²úÆ·Ô¤ÆÚÊÕÒæ´ï16%</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/10/7143K85C00252V0H.html">ÔÝÍ£¶àÄê·¿´ûÏÕÇÄÈ»»Ø³± ÒøÐÐ×øµØ½Ð¼Û°ÔÆø×ã</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/09/7141MHER00252V0H.html">°×Òø·çÍ·Ô¶¸Ç»Æ½ð Ëijɳ´½ð¿Íתս°×Òø</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/10/7144UQSS00252V0H.html">Ƶ·±×ª´æ²»»®Ëã ת´æ¶à¡°ÀÛ»µ¡±ÒøÐÐϵͳ</a></li>
+ <li><a href="httpdisabled://money.163.com/11/0408/09/7141GAII00253B0H.html">3ÔÂÀí²Æ²úÆ·ÊÕÒæÆÕÕÇ 4ÔÂÓÐÍû½øÒ»²½×߸ß</a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="main-col-9">
+ <div id="auto" class="mod wgt-tab">
+ <h2 class="tab-hd tab-u-9 clearfix">
+ <span class="tab-u current"><a href="httpdisabled://auto.163.com/">Æû³µ</a></span>
+ <span class="tab-u"><a href="httpdisabled://auto.163.com/buy/">гµµ¼¹º</a></span>
+ <span class="tab-u"><a href="httpdisabled://auto.163.com/depreciate/">³µÊÐÐÐÇé</a></span>
+ <span class="tab-u"><a href="httpdisabled://club.auto.163.com/">Æû³µÉçÇø</a></span>
+ </h2>
+ <div class="bd tab-bd display-control">
+ <div class="tab-con current">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://auto.163.com/11/0408/07/713PU5JD00084P03.html"><img src="../img4.cache.netease.com/auto/2011/4/8/20110408091859b1da7.jpg" alt="×î±ãÒ˰µϽ«ÔÚ»ªÁÁÏà" title="×î±ãÒ˰µϽ«ÔÚ»ªÁÁÏà" height="90" width="120" /><cite>×î±ãÒ˰µϽ«ÔÚ»ªÁÁÏà</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://auto.163.com/special/sonata8/">°Ë´úË÷ÄÉËþÉÏÊÐ ÊÛ16.69ÍòÆð</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://auto.163.com/11/0407/16/7124DROB00082H5Q.html">ÊÛ5.99ÍòÔªÆð ±ÈÑǵÏG3RÉÏÊÐ</a></li>
+ <li><a href="httpdisabled://auto.163.com/11/0407/10/711GM44000081G9U.html">ºóÄê¹ú²ú »ª³¿±¦Âí»ì¶¯5ϵÆعâ</a></li>
+ <li><a href="httpdisabled://auto.163.com/11/0408/14/714H566200084MDJ.html">2011µÚ1ÅúÅöײ½á¹û:×ÔÖ÷³¬ºÏ×Ê</a></li>
+ <li><a href="httpdisabled://auto.163.com/11/0408/09/7140OUOI00084P03.html">ºÜСÇÉ ±¼³ÛA¼¶¸ÅÄî³µ¼´½«Ê×·¢</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://auto.163.com/">[гµ]</a> <a target="_blank" href="httpdisabled://auto.163.com/11/0408/09/713V9DFA00081S9K.html">±È;¹Û»¹Ð¡:°ÂµÏQ3Æعâ Å·ÖÞ27.6ÍòÔªÆðÊÛ</a></li>
+ <li><a href="httpdisabled://club.auto.163.com/">[ÈÈÌû]</a> <a target="_blank" href="httpdisabled://club.auto.163.com/bbs/auto_01m0/205676175.html">ÁÉBÅÆ×ӵijµÄãÉ˲»Æð</a> <a target="_blank" href="httpdisabled://club.auto.163.com/bbs/auto_01m0/205675144.html">ÕâÊÇÎÒ¼û¹ý×îNµÄÍ£³µ</a></li>
+ <li><a href="httpdisabled://auto.163.com/">[ÐÂÎÅ]</a> <a target="_blank" href="httpdisabled://auto.163.com/11/0408/07/713PG9S800084JTJ.html">·¢¸Äί³ÆÒѶÔÓͼÛÖƶ¨Ô¤°¸ ²»»áÈÎÓÉÉÏÕÇ</a></li>
+ <li><a href="httpdisabled://auto.163.com/">[гµ]</a> <a target="_blank" href="httpdisabled://auto.163.com/11/0408/07/713O55VI00084IJ9.html">мÑÀÖ1.6LµÇ½Öйú</a> <a target="_blank" href="httpdisabled://auto.163.com/11/0408/07/713O57K400084IJ9.html">2011¿îÐÂÁçÑòÉÏÊÐ</a></li>
+ <li><a href="httpdisabled://auto.163.com/11/0408/07/713QE4QA000816HU.html">[ÊÔ¼Ý]</a> <a target="_blank" href="httpdisabled://auto.163.com/11/0408/07/713QE4QA000816HU.html">²»¸øÖÐʯÓÍÃæ×Ó ÀÍ˹À³Ë¹ÍÆÁãÅŷŵ綯³µ</a></li>
+ <li><a href="httpdisabled://auto.163.com/">[½µ¼Û]</a> <a target="_blank" href="httpdisabled://auto.163.com/11/0407/22/712Q1O0M00083KOO.html">°ÂµÏA5½µ4Íò5</a> <a target="_blank" href="httpdisabled://auto.163.com/11/0407/21/712N8OKG00083KOO.html">Ììô¥½µ2Íò1</a> <a target="_blank" href="httpdisabled://auto.163.com/11/0407/21/712M8DV300083KOU.html">Ú©¸èMDX½µ13Íò</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://club.auto.163.com/"><em class='fB'>ÈÈÌû</em></a> | <a target="_blank" href="httpdisabled://club.auto.163.com/bbs/auto_aaac/205598271.html">±»ÏÆ·­?¾¯³µµ×³¯Ìì</a> <a target="_blank" href="httpdisabled://club.auto.163.com/bbs/auto_sichuan/205661670.html">³¬Å£ÆÕÉ£¸Ä×°³µ</a></li>
+ <li><a href="httpdisabled://auto.163.com/"><em class='fB'>»î¶¯</em></a> | <a target="_blank" href="httpdisabled://auto.163.com/11/0328/09/707M12Q900084K9A.html">Ìîµ÷²éÓ®Åɿ˱Ê</a> <a target="_blank" href="httpdisabled://t.163.com/zt/auto/autogift">¹Ø×¢¹Ù²©ÇÀ˹°Í³³µÄ£</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://auto.163.com/11/0408/07/713QE4QA000816HU.html"><img src="../img4.cache.netease.com/auto/2011/4/8/201104080930543aaa8.jpg" alt="ÊÔ¼ÝÀÍ˹À³Ë¹102EX" title="ÊÔ¼ÝÀÍ˹À³Ë¹102EX" height="90" width="120" /><cite>ÊÔ¼ÝÀÍ˹À³Ë¹102EX</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://auto.163.com/special/shanghai-chezhan/">ÉϺ£³µÕ¹12¿îÈÈÃÅСÐͳµÅ̵ã</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://auto.163.com/special/cyb130/">¹º40-60ÍòÔªµÄÓÐÃæ×ÓÉÌÎñ½Î³µ</a></li>
+ <li><a href="httpdisabled://auto.163.com/11/0408/09/713V9DFA00081S9K.html">°ÂµÏQ3Å·ÖÞÆð²½¼Û27.6ÍòÔªÆð</a></li>
+ <li><a href="httpdisabled://auto.163.com/special/sonata8/">±±¾©ÏÖ´úÐÂË÷ÄÉËþÉè¼Æ½âÎö</a></li>
+ <li><a href="httpdisabled://auto.163.com/11/0407/10/711GM44000081G9U.html">»ª³¿±¦Âí²åµçʽ»ì¶¯5ϵÆعâ</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://auto.163.com/special/shanghai-chezhan/">[³µÕ¹]</a> <a target="_blank" href="httpdisabled://auto.163.com/11/0408/09/7140OUOI00084P03.html">A¼¶¸ÅÄȫÇòÊ×·¢ ÉϺ£³µÕ¹±¼³ÛÕóÈÝÆعâ</a></li>
+ <li><a href="httpdisabled://auto.163.com/special/shanghai-chezhan/">[³µÕ¹]</a> <a target="_blank" href="httpdisabled://auto.163.com/11/0408/09/713V0U3400084P03.html">ÄÉÖǽÝneoraÉϺ£Ê×·¢</a> <a target="_blank" href="httpdisabled://auto.163.com/11/0408/08/713TKAJ400084IKF.html">¹ã·áMPVÒÝÖ³µÕ¹·¢²¼</a></li>
+ <li><a href="httpdisabled://auto.163.com/">[µ¼¹º]</a> <a target="_blank" href="httpdisabled://auto.163.com/11/0408/08/713TD2KH00084IKF.html">°ÚÍÑÒ¡ºÅÏÞÐÐÖ®¿à ±±¾©Èý¿îµç¶¯Æû³µÍƼö</a></li>
+ <li><a href="httpdisabled://auto.163.com/">[µ¼¹º]</a> <a target="_blank" href="httpdisabled://auto.163.com/11/0408/07/713OL5KG00084IK5.html">´ºÅ¯»¨¿ª 9¿îÊʺϼÒ̤ͥÇོÓγµÐÍÍƼö</a></li>
+ <li><a href="httpdisabled://auto.163.com/">[гµ]</a> <a target="_blank" href="httpdisabled://auto.163.com/11/0408/07/713OL49A00084IK5.html">м׿dz泵չÊ×·¢</a> <a target="_blank" href="httpdisabled://auto.163.com/11/0408/07/713OL48600084IK5.html">ÐÂʤ´ï»ò¹ú²úÊÛ20ÍòÆð</a></li>
+ <li><a href="httpdisabled://auto.163.com/">[гµ]</a> <a target="_blank" href="httpdisabled://auto.163.com/11/0407/11/711KQ9B500082H5S.html">±ÈÑǵÏS6½«5ÔÂÉÏÊÐ</a> <a target="_blank" href="httpdisabled://auto.163.com/11/0407/10/711IHA9N00082H5S.html">¹ãÆû¼ª°Â°ÂÐùG5½«ÏÂÏß</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://auto.163.com/test/"><em class='fB'>ÍøÒ×ÊÔ¼Ý</em></a> | <a target="_blank" href="httpdisabled://auto.163.com/special/touareghybrid/">µÍ̼¸ßÄÜ ÊÔ¼ÝÐÂ;Èñ»ìºÏ¶¯Á¦SUV</a></li>
+ <li><a href="httpdisabled://auto.163.com/special/cxshhz/"><em class='fB'>³µÐÍÊ·»°</em></a> | <a target="_blank" href="httpdisabled://auto.163.com/11/0406/23/710A1ST500081GDM.html">ÀúÊ·Õù¶áÕ½ ÆÊÎöÉϺ£´óÖÚÈ«ÐÂÅÁÈøÌØ</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://auto.163.com/11/0407/13/711S5I1E00083QQV.html"><img src="../img4.cache.netease.com/photo/0008/2010-01-30/120x90_5U980MMS294H0008.JPG" alt="Ò»Æû±¼ÌÚB70ÓÅ»Ý1.2Íò" title="Ò»Æû±¼ÌÚB70ÓÅ»Ý1.2Íò" height="90" width="120" /><cite>Ò»Æû±¼ÌÚB70ÓÅ»Ý1.2Íò</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://auto.163.com/11/0407/13/711SL4K900083QQV.html">¶«·çÈÕ²úÆæ¿¥×î¸ßÓŻݴï1.8Íò</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://auto.163.com/11/0407/18/712CFFGD00083KOO.html">ÑÇè¡Ê©ÄÍÔóACS7ÏÖ³µµ½µê</a></li>
+ <li><a href="httpdisabled://auto.163.com/11/0407/21/712M8DV300083KOU.html">2011¿îÚ©¸èMDXȫϵֱ½µ13Íò</a></li>
+ <li><a href="httpdisabled://auto.163.com/11/0407/21/712MIG9A00083KOU.html">2010¿î±¼³ÛA180Ö±½µ7.3ÍòÔª</a></li>
+ <li><a href="httpdisabled://auto.163.com/11/0407/19/712FK6HL00083KOU.html">2011¿î±¦Âí3ϵ¸ßÅäÕûÌå½µ4Íò</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://auto.163.com/11/0407/21/712LSICE00083KOO.html">¸£ÌØÈñ½çÌáÏÖ³µ×î¸ß¼Ó¼Û5Íò</a> | <a target="_blank" href="httpdisabled://auto.163.com/11/0407/19/712GJILM00083KOU.html">´óÖÚÐÂ;°²½µ5000Ôª</a></li>
+ <li><a href="httpdisabled://auto.163.com/11/0407/22/712Q1O0M00083KOO.html">°ÂµÏA5ȫϵ×î¸ß½µ4.5Íò</a> | <a target="_blank" href="httpdisabled://auto.163.com/11/0407/18/712DUJ6P00083KOO.html">ººÀ¼´ï×î¸ßÓÅ»Ý1.8Íò</a></li>
+ <li><a href="httpdisabled://auto.163.com/11/0407/19/712F18UQ00083KOO.html">±¼³ÛE300LÖ±½µ½ü11ÍòÔª</a> | <a target="_blank" href="httpdisabled://auto.163.com/11/0407/13/711RT6AQ00083QQV.html">¸èʫͼ×î¸ßÓÅ»Ý1.5Íò</a></li>
+ <li><a href="httpdisabled://auto.163.com/11/0407/18/712CNQ9E00083KOO.html">´óÖڽݴï×î¸ßÓÅ»Ý4ǧ</a> | <a target="_blank" href="httpdisabled://auto.163.com/11/0407/20/712L3K3A00083KOU.html">Ñ©·ðÀ¼¾°³ÌÕûÌåÓÅ»Ý1.6Íò</a></li>
+ <li><a href="httpdisabled://auto.163.com/11/0407/18/712D9B5B00083KOO.html">·æ·¶ÓÅ»Ý1.1Íò</a> | <a target="_blank" href="httpdisabled://auto.163.com/11/0408/08/713TLQ4Q00084IJT.html">ÈÙÍþ550ÊÀ²©°æMT³µÐÍÓÅ»Ý1Íò</a></li>
+ <li><a href="httpdisabled://auto.163.com/11/0407/11/711L1VMA00083QQV.html">ÐùÒÝ×î¸ß½µ6000Ôª</a> | <a target="_blank" href="httpdisabled://auto.163.com/11/0408/08/713T7QNK00084IJT.html">¾§Èñȫϵ88ÕÛ ×î¸ßÓÅ»ÝÍòÔª</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://auto.163.com/special/wysjhz/"><em class='fB'>ÍøÒ×ÊÔ¼Ý</em></a> | <a target="_blank" href="httpdisabled://auto.163.com/special/peugeot_3008/">¿ç½çÏÈ·æ ÊÔ¼Ý2011¿î±êÖÂ3008(¶àͼ)</a></li>
+ <li><a href="httpdisabled://auto.163.com/special/rcpkhz/"><em class='fB'>ÈȳµPK̨</em></a> | <a target="_blank" href="httpdisabled://auto.163.com/special/x3pkglk/">ºÀ»ª³ÇÊÐSUV¶Ô¾ö ±¦ÂíX3 vs ±¼³ÛGLK</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://auto.163.com/special/show_girl/"><img src="../img3.cache.netease.com/auto/2011/3/30/20110330215354a8c7a.jpg" alt="ÉϺ£³µÕ¹ÃÀÅ®Ö÷²¥ÆÀÑ¡" title="ÉϺ£³µÕ¹ÃÀÅ®Ö÷²¥ÆÀÑ¡" height="90" width="120" /><cite>ÉϺ£³µÕ¹ÃÀÅ®Ö÷²¥ÆÀÑ¡</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://club.auto.163.com/bbs/auto_haiwai/205753679.html">µØÕðÖÐÓÎÀÀÈ«ÈÕ±¾×îÇ¿Âô³µµê</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://auto.163.com/special/yushengkanchetuan/">º¼ÖÝ:Óн±ÊÔ¼Ý10ÍòÔª×ÔÖ÷SUV</a></li>
+ <li><a href="httpdisabled://club.auto.163.com/bbs/qingdao/205377190.html">Çൺ¸£ÈðµÏ³µÓÑÕª²ÝÝ®ÕÙ¼¯ÖÐ</a></li>
+ <li><a href="httpdisabled://club.auto.163.com/bbs/auto_aaac/204446417.html">3Ô¸ÇÂ¥ÌùÆÀÑ¡ °ÙÔªÓÍ¿¨µÈÄãÄÃ</a></li>
+ <li><a href="httpdisabled://club.auto.163.com/bbs/auto_bbtx/203993156.html">"½ÌÄãʹÓñ£ÏÕ" ²ÎÓëÓн± </a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://club.auto.163.com/">[ÓͼÛ]</a> <a target="_blank" href="httpdisabled://club.auto.163.com/bbs/auto_01m0/205427558.html">Ô¹ÓͼÛÕÇ?ÔâÓö¼ÓÁÓÖÊÓ͸ü±¯¾ç</a> <a target="_blank" href="httpdisabled://club.auto.163.com/bbs/auto_01m0/205427558.html">ºÜÅ£µÄ³µÌû</a></li>
+ <li><a href="httpdisabled://club.auto.163.com/">[ÉϺ£³µÕ¹]</a> <a target="_blank" href="httpdisabled://club.auto.163.com/bbs/auto_02z0/205707321.html">ËÄ´óŮħ(Ä£)?Æغ£Âí³µÄ£ÃÀͼ</a></li>
+ <li><a href="httpdisabled://club.auto.163.com/">[³µÓÑ]</a> <a target="_blank" href="httpdisabled://club.auto.163.com/bbs/auto_01lb/205596059.html">ÂÖÌ¥ÉÏÉ« ÈýÃÀÅ®ÆëÉÏÕó°ïæ(¾ø¶ÔÕæÈË°æ)</a></li>
+ <li><a href="httpdisabled://club.auto.163.com/">[×ÔÆØ]</a> <a target="_blank" href="httpdisabled://club.auto.163.com/bbs/qingdao/205375530.html">ÆðÑǸ£ÈðµÏؤ°æ°µÓ£ºì´¦Å®Ìù ¶àͼ·ÅËÍ</a></li>
+ <li><a href="httpdisabled://club.auto.163.com/">[Ìùͼ]</a> <a target="_blank" href="httpdisabled://club.auto.163.com/bbs/auto_sichuan/205661670.html">ÆÕÉ£±©¸Ä³ÉÎ޵иÖÅÚ </a> <a target="_blank" href="httpdisabled://club.auto.163.com/bbs/auto_aaac/205598271.html">¾¯³µÊÇÈçºÎ½Å³¯ÌìµÄ</a></li>
+ <li><a href="httpdisabled://club.auto.163.com/">[½ÖÅÄ]</a> <a target="_blank" href="httpdisabled://club.auto.163.com/bbs/auto_haiwai/205611078.html">LP700ºÚ°×Ë«É·½ü¾àÀë½Ó´¥</a> <a target="_blank" href="httpdisabled://club.auto.163.com/bbs/auto_yueye/205597738.html">С³Ç¸øÁ¦»é³µ¶Ó</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://club.auto.163.com/"><em class='fB'>´ºÓÎÈÕ¼Ç</em></a> <a target="_blank" href="httpdisabled://club.auto.163.com/bbs/auto_aaab/205051595.html">ͬѧ¾Û»á ·¬Ø®Ð¡ÖÞÔç²è¼Ç</a> <a target="_blank" href="httpdisabled://club.auto.163.com/bbs/auto_aaac/201602614.html">·¢ÌûÓ®ÓÍ¿¨</a></li>
+ <li><a href="httpdisabled://club.auto.163.com/bbs/auto_aaai/205364360.html"><em class='fB'>°®ÉãÍÅ</em></a> <a target="_blank" href="httpdisabled://club.auto.163.com/bbs/auto_aaai/205364360.html">4ÔÂ9ÈÕÏàÔ¼ÅÄÉ㡶°×Ñ©¹«Ö÷Óö¼ûÍÜÑÛÍõ×Ó¡· </a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+<div class="area-main">
+ <div class="main-col-10">
+ <iframe src="../g.163.com/r@site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=column390x100&amp;location=3.html" width="390" height="100" frameborder="0" border="0" marginwidth="0" marginheight="0" scrolling="no" ></iframe>
+
+ </div>
+ <div class="main-col-9">
+ <iframe src="../g.163.com/r@site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=column360x100&amp;location=3.html" width="360" height="100" frameborder="0" border="0" marginwidth="0" marginheight="0" scrolling="no" ></iframe>
+
+ </div>
+ </div>
+ </div>
+ <div class="area-sub">
+ <div id="g5n2" class="mod wgt-tab">
+<div class="tab-hd tab-u-5 clearfix">
+ <span class="tab-u current">×ÊѶ</span>
+ <span class="tab-u">»î¶¯</span>
+ <span class="tab-u">ÍƼö</span>
+ <span class="tab-u">¾«Æ·</span>
+ </div>
+ <div class="bd display-control">
+ <div class="tab-con current">
+<ul class="mod-list sub-list">
+<li class="title"><a href="httpdisabledsdisabled://epay.163.com/notice/chongzhi.jsp">ÊÖ»ú¿¨³äÖµÒ²ÄÜÍø¹ºÀ²</a></li>
+<li><a href="httpdisabled://lady.163.com/special/00261ID9/2009DeluxeReport.html">×îÊÜÉÌÎñÈËÊ¿ÖÓ°®Æ·ÅÆ</a></li>
+<li><a href="httpdisabled://survey2.163.com/html/dict_youdao2011q1/paper.html">ÇáµãÊó±êÓ®¾ªÏ²´ó½±</a></li>
+<li><a href="httpdisabled://mail.163.com/html/110127_imap/index.htm">ÊÓƵ½ÌÄãÉèÖÃÓÊÏäIMAP</a></li>
+<li><a href="httpdisabled://pmxj.wan.163.com/">Æ®Ãì¶à·çÔÆÏɽ£ÏÔÎäÁÖ</a></li>
+<li><a href="httpdisabled://money.163.com/2011NAEC/">2011ÍøÒ×¾­¼Ãѧ¼ÒÄê»á</a></li>
+ </ul>
+ <ul class="mod-list sub-list">
+<li><a href="httpdisabled://tech.163.com/11/0322/10/6VO99FLI000915BF.html">ÍøÒ×ÐÂÎÅ¿Í»§¶Ë¡ÖØÉÏÏß</a></li>
+<li><a href="httpdisabled://g.163.com/a?CID=240&Values=53963900&Redirect=http://mhxx.dream.163.com?101222mhxx002">¹úÄÚÊ׿î2Dºá°æÍøÓÎ</a></li>
+<li><a href="httpdisabled://xyq.163.com/xyq?acctsn=MH22-2128-3554-7248">ºÍËûÒ»Æð×ö×îÀËÂþµÄÊÂ</a></li>
+<li><a href="httpdisabled://yuehui.163.com/">ллÍøÒ×ÈÃÎÒÓö¼ûÁËËû</a></li>
+<li><a href="httpdisabled://bafang.163.com/">²é¿´Å®ÓѾßÌåλÖà </a></li>
+<li><a href="httpdisabled://v.163.void/">ÍøÒ×¹«¿ª¿Î ºÃ¿ÎÃâ·ÑÌý</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ </div>
+ <div class="tab-con">
+ </div>
+ <div class="tab-con">
+ </div>
+ </div>
+ </div>
+ <script>
+ NTES.ready( function(){
+ var aChange = new AChange({
+ temp: "/special/00774IHC/11zy2-",
+ content: "#g5n2",
+ num: "4"
+ });
+});
+ </script>
+ <div class="gg gg-h180"><iframe src="../g.163.com/r@site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=logo190x180&amp;location=2.html" width="190" height="180" frameborder="0" border="0" marginwidth="0" marginheight="0" scrolling="no" ></iframe></div>
+ </div>
+</div>
+<!-- end -->
+<!-- tech & house -->
+<div class="area">
+ <div class="area-main">
+ <div class="main-col-10">
+ <div id="tech" class="mod wgt-tab">
+ <h2 class="tab-hd tab-u-10 clearfix">
+ <span class="tab-u current"><a href="httpdisabled://tech.163.com/">¿Æ¼¼</a></span>
+ <span class="tab-u"><a href="httpdisabled://mobile.163.com/">ÊÖ»ú</a></span>
+ <span class="tab-u"><a href="httpdisabled://digi.163.com/">ÊýÂë</a>¡¤<a href="httpdisabled://mobile.163.com/3g/">3G</a></span>
+ <span class="tab-u"><a class="homeAppliances" href="httpdisabled://hea.163.com/">¼Òµç</a></span>
+ </h2>
+ <div class="bd tab-bd display-control">
+ <div class="tab-con current">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://tech.163.com/mobile/11/0408/05/713JK5U300112K88.html"><img src="../img3.cache.netease.com/mobile/2011/4/8/201104080904537def0.jpg" alt="³¬ÃÍ´¿Ò¯ÃǶù»ú´ó½µ¼Û" title="³¬ÃÍ´¿Ò¯ÃǶù»ú´ó½µ¼Û" height="90" width="120" /><cite>³¬ÃÍ´¿Ò¯ÃǶù»ú´ó½µ¼Û</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://tech.163.com/11/0407/22/712Q075F000915BD.html">¹È¸è֤ʵ¿ª·¢Æ½°åChrome OSϵͳ</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://tech.163.com/11/0408/17/714RUP8P000915BE.html">ÁªÍ¨½µµÍiPhone4Ô·ÑÃż÷:×îµÍ66Ôª</a></li>
+ <li><a href="httpdisabled://tech.163.com/11/0408/00/712VNNF5000915BE.html">Öйú½¨³ÉÈ«Çò×î´óIPv6¹Ç¸ÉÍø</a></li>
+ <li><a href="httpdisabled://tech.163.com/11/0408/07/713QDT6Q000915BF.html">¾Å³ÇQ4¾»¿÷4390Íò ³ÖÐø¿÷ËðÆß¼¾¶È</a></li>
+ <li><a href="httpdisabled://tech.163.com/11/0407/17/71292O9G000915BE.html">Á½¸ßÔº£º5000ÌõÕ©Æ­¶ÌÐż´¿É¶¨×ï</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://digi.163.com">Ô¼40%Æ»¹û·ÛÉý¼¶4.3ºó¸ü·Ñµç</a> <a target="_blank" href="httpdisabled://tech.163.com/digi/11/0407/11/711M4HGC001618S7.html">iPod touch5¹¤³Ì»úÔâй¶</a></li>
+ <li><a href="httpdisabled://tech.163.com/digi/11/0408/05/713J8EVC0016192E.html">SNB¼¯ÌåÌøË® Ò»Öܱ¾±¾½µ¼ÛÅÅÐÐ</a> <a target="_blank" href="httpdisabled://tech.163.com/digi/11/0408/00/7132FT3J0016192R.html">´îÔØÐÂi7 ´÷¶ûXPS15ÆÀ²â</a></li>
+ <li><a href="httpdisabled://t.163.com/zt/digi/image">´ºÌì¾ÍÔÚÄãµÄÏà»úÀï ÉãÓ°´ïÈ˽ÌÄãÅÄ´ºÌì</a> <a target="_blank" href="httpdisabled://t.163.com/imagejinfeng">¹Ø×¢½ù·å΢²©</a></li>
+ <li><a href="httpdisabled://tech.163.com/mobile/11/0408/05/713JD13H00112K8C.html">½µ¼Û°ñ:È«ÇòÈÈÏúÊÖ»úTOP10</a> <a target="_blank" href="httpdisabled://tech.163.com/mobile/11/0408/06/713LNSDO0011309K.html">1200ÔªÆð×î¸ßÈËÆøÊÖ»úTOP8</a></li>
+ <li><a href="httpdisabled://tech.163.com/mobile/11/0408/06/713N2LF000112K88.html">¸ß¶ËÆì½¢ÊÖ»ú¼Û¸ñ±©µø»ã×Ü</a> <a target="_blank" href="httpdisabled://tech.163.com/mobile/11/0408/06/713LNSCI0011309K.html">G7ÔÙ½µ:ÈÈÂôAndroidÊÖ»úÅ̵ã</a></li>
+ <li><a href="httpdisabled://tech.163.com/mobile/11/0408/06/713N135S00112K8D.html">Ç¿»ú×ßÊÆ:iPhone4ÏÖ4700Ôª</a> <a target="_blank" href="httpdisabled://tech.163.com/mobile/11/0408/06/713LNSE40011309K.html">Èý´óÍøÂçÐÔ¼Û±È×î¸ßµÄ3GÊÖ»ú</a></li>
+
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://tech.163.com/" class="fB attitude">¿Æ¼¼</a> | <a href="httpdisabled://tech.163.com/special/column/"><em class='fB'>רÀ¸</em></a> | <a target="_blank" href="httpdisabled://tech.163.com/11/0408/09/713UKKTD000949EP.html">¼½ÓÂÇ죺´ÓÓÀÖп´ÖйúÈí¼þÀ§¾Ö</a></li>
+ <li><a href="httpdisabled://tech.163.com/dailysite"><em class='fB'>¿áÕ¾</em></a> | <a target="_blank" href="httpdisabled://tech.163.com/11/0406/23/710A9PED000938EN.html">Ö°³¡É罻ƽ̨</a> | <a target="_blank" href="httpdisabled://t.163.com/zt/pub/tech163"><em class='fB'>΢²©</em></a> | <a target="_blank" href="httpdisabled://t.163.com/2123575232/status/-2250570432325285320#retweet">ÁõÇ¿¶«:ITÐÐÒµµ­¼¾ÌáÇ°</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://tech.163.com/mobile/11/0408/05/713JD13H00112K8C.html"><img src="../img4.cache.netease.com/mobile/2011/4/8/2011040809135520264.jpg" alt="È«ÇòÈÈÏúÊÖ»úTOP10" title="È«ÇòÈÈÏúÊÖ»úTOP10" height="90" width="120" /><cite>È«ÇòÈÈÏúÊÖ»úTOP10</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://mobile.163.com/">N8ÆÆ2600Ôª Íâ¹ÛÐÔÄܶ¼Ã͵ÄÒ¯ÃÇ»ú</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://tech.163.com/mobile/11/0408/06/713LNSDO0011309K.html">×îµÍ½µÖÁ1200Ôª ×î¸ßÈËÆøÊÖ»úTOP8</a></li>
+ <li><a href="httpdisabled://tech.163.com/mobile/11/0408/06/713N2LF000112K88.html">Àï³Ì±®²»µ½2000Ôª ¸ß¶ËÊÖ»ú´ó½µ¼Û</a></li>
+ <li><a href="httpdisabled://tech.163.com/mobile/11/0408/06/713LNSCI0011309K.html">ÈÈÃÅAndroidÊÖ»ú:G7¼Û¸ñÎȶ¨¿ÉÈëÊÖ</a></li>
+ <li><a href="httpdisabled://tech.163.com/mobile/11/0408/06/713N135S00112K8D.html">Ç¿»ú¼Û¸ñ×ßÊÆ:iPhone 4µøÆÆ·¢ÐмÛ</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://tech.163.com/mobile/11/0408/09/7140QMTF001117A5.html">LGÊ׿î´óÆÁË«ºËÖÇÄÜÊÖ»úÊÔÍæ</a> <a target="_blank" href="httpdisabled://tech.163.com/mobile/11/0408/06/713LM42P0011309K.html">TDлúĦÍÐÂÞÀ­MT620ÆÀ²â</a></li>
+ <li><a href="httpdisabled://tech.163.com/mobile/11/0408/11/7147ER3B00112K8E.html">´«ÁªÍ¨ÍÆiPhone 4¹º»úÐÂÕþ×îµÍÿÔÂ66</a> <a target="_blank" href="httpdisabled://tech.163.com/mobile/11/0408/08/713STBVT00112K8E.html">CDMA°æ±ãÒËÉպŹó</a></li>
+ <li><a href="httpdisabled://tech.163.com/mobile/11/0408/06/713MVSB000112K8E.html">Á®¼ÛÖÇÄÜ»ú:ÈýÐÇPrevailÉÍÎö</a> <a target="_blank" href="httpdisabled://tech.163.com/mobile/11/0408/06/713LII220011309K.html">LGÇæÌìϵÁÐÈëÃÅ»ú½üÆÚÉÏÊÐ</a></li>
+ <li><a href="httpdisabled://tech.163.com/mobile/11/0408/06/713LIENF0011309K.html">HTCË«ºËCPUǧÍòÏñËؾµÍ·Ð»ú</a> <a target="_blank" href="httpdisabled://tech.163.com/mobile/11/0408/05/713JI3JU00112K8E.html">Symbian^3Éý¼¶ÎÞʵÖʸĽø</a></li>
+ <li><a href="httpdisabled://tech.163.com/mobile/11/0408/05/713JI3J200112K8E.html">4.3´çÆÁDroid XÉýË«ºË´¦ÀíÆ÷ ĦÍÐÂÞÀ­Droid X2Õæ»úÆعâ</a></li>
+ <li><a href="httpdisabled://tech.163.com/mobile/11/0408/00/7131BH2V00112K95.html">Ë«ºË´¦ÀíÆ÷ÐÔÄÜÔ¶³¬µ¥ºË10±¶ Atrix/i9000ÉúËÀPK´óÕ½</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://tech.163.com/mobile/special/mobile20111/"><em class='cBlack fB'>i´ïÈË |</em></a> <a target="_blank" href="httpdisabled://tech.163.com/mobile/special/mobile20111/">ÊÖ»ú´ïÈËÍƼö£º2011²»µÃ²»ÍæµÄ5¿îÊÖ»úÓÎÏ·</a></li>
+ <li><a href="httpdisabled://club.tech.163.com/"><em class='cBlack fB'>ÂÛ̳ |</em></a> <a target="_blank" href="httpdisabled://club.tech.163.com/bbs/mobile_activ/203210473.html">¿ìÇÀ£¡¸øÍøÒ×ÐÂÎÅ¿Í»§¶ËÆÀ·Ö¾ÍÄÜÄõ½´ó½±£¡</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://digibbs.tech.163.com/bbs/digifuns/205680163.html"><img src="../img4.cache.netease.com/digi/2011/4/8/20110408144717d8da9.jpg" alt="ÐÂÈëÊÖAcer 4750GÑÞÕÕ" title="ÐÂÈëÊÖAcer 4750GÑÞÕÕ" height="90" width="120" /><cite>ÐÂÈëÊÖAcer 4750GÑÞÕÕ</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://tech.163.com/digi/11/0408/06/713ME3UM001624J3.html">Ë÷Äá½ðÉ«NEX-5CÒ¹¾°ÑùÕÅÉÍÎö</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://tech.163.com/digi/11/0408/05/713J8DF60016192R.html">240Hz²»ÉÁ3D±¾ Ë÷ÄáF219Ê×·¢ÆÀ²â</a></li>
+ <li><a href="httpdisabled://tech.163.com/digi/11/0408/05/713J8EUE0016192E.html">¶ÀÏÔ´óÕ½Éý¼¶ мܹ¹ÓÎÏ·±¾ÍƼö</a></li>
+ <li><a href="httpdisabled://tech.163.com/digi/11/0408/02/7138LU3I001624J3.html">¹ã½Ç·À¶¶¿¨Æ¬»ú ¼ÑÄÜA3200 ISÆÀ²â</a></li>
+ <li><a href="httpdisabled://tech.163.com/digi/11/0408/05/713J1R6200163KQR.html">ÓÐÕÇÓнµ 4¿îÈÈÃŵ¥µç×îм۸ñ»ã×Ü</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://tech.163.com/digi/11/0408/06/713MQPDL001618J1.html">À¶±¦µÚ2¿îAPU¼Ü¹¹ÃÔÄã°åÈÕ±¾ÉÏÊÐ</a> <a target="_blank" href="httpdisabled://tech.163.com/digi/11/0408/02/71394GG1001618JK.html">799Ôªµ½999ÔªÏÔ¿¨µ¼¹º</a></li>
+ <li><a href="httpdisabled://tech.163.com/digi/11/0408/08/713RGRV9001618JK.html">ÊÐÊÛǧԪ×óÓÒ¾«Æ·ÏÔʾÆ÷ÍƼö</a> <a target="_blank" href="httpdisabled://tech.163.com/digi/special/lepad/">ÁªÏëÀÖPadÁãÊÛ°æÊÓ¾õÆÀ²â</a></li>
+ <li><a href="httpdisabled://mobile.163.com/3g/">[3G]</a> <a href="httpdisabled://tech.163.com/mobile/11/0408/08/713U2NHC0011309K.html">ͨ»°ÖÊÁ¿ÓÐÌá¸ß Æ»¹ûCƤ¶þ´úÐÂÆ·¸ßÇåͼÆعâ</a></li>
+ <li><a href="httpdisabled://mobile.163.com/3g/">[3G]</a> <a href="httpdisabled://tech.163.com/mobile/11/0408/08/713TLQ2400112K8E.html">Ϊ·â¶ÂÔ½Óü ÏûÏ¢³ÆiOS 4.3.2½«ÔÚÁ½ÖÜÄÚ·¢²¼</a></li>
+ <li><a href="httpdisabled://mobile.163.com/3g/">[3G]</a> <a href="httpdisabled://tech.163.com/mobile/11/0408/06/713LIENF0011309K.html">Ë«ºË´¦Àí1600ÍòÏñËØ HTC NEW EDEN¸ÅÄî»úÆعâ</a></li>
+ <li><a href="httpdisabled://mobile.163.com/3g/">[3G]</a> <a href="httpdisabled://tech.163.com/mobile/11/0407/17/7129GSQ0001164LP.html">Flight Control£º¿ÕÖÐÖ¸»Ó¹Ù ¿¼ÑéÄãµÄ¿ØÖÆÁ¦</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><strong><a href="httpdisabled://digibbs.tech.163.com/">ÂÛ̳</a></strong> | <a href="httpdisabled://digibbs.tech.163.com/bbs/diginews/205444284.html">ÉñÈËÁ¬ÐøÉÏÍø454Сʱ ±£Ä·ÌùÉí¿´»¤</a></li>
+ <li><strong><a href="httpdisabled://digibbs.tech.163.com/">ÂÛ̳</a></strong> | <a href="httpdisabled://digibbs.tech.163.com/bbs/notebook/205336929.html">±Ê¼Ç±¾´ïÈËÈÏÖ¤»úÖ÷»ðÈÈÕÐļÖÐ~~~£¡</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://tech.163.com/digi/11/0408/01/7134II58001618VK.html"><img src="../img1.cache.netease.com/digi/linzj/1102/03/191.jpg" alt="ÃÀµÄ500¿îмҵçÉÏÊÐ" title="ÃÀµÄ500¿îмҵçÉÏÊÐ" height="90" width="120" /><cite>ÃÀµÄ500¿îмҵçÉÏÊÐ</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://tech.163.com/digi/11/0408/01/7133ENRJ001618VK.html">ÃÀµÄÈÕµçÎߺþÉú²ú»ùµØͶ×ʳ¬20ÒÚ</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://tech.163.com/digi/11/0408/00/7132P3SC001618VS.html">¾«ÖÂÖÁ¼« Ë÷Äá24EX520Òº¾§µçÊÓÆÀ²â</a></li>
+ <li><a href="httpdisabled://tech.163.com/digi/11/0408/09/713V39R8001618VK.html">ÏûЭ³ÆÂô³¡ÐèÃ÷Âëʵ¼Û Î¥¹æ½«±»·£</a></li>
+ <li><a href="httpdisabled://tech.163.com/digi/11/0408/08/713U2JFT001628C1.html">¿Õµ÷»ÝÃñ²¹ÌùÈ¡Ïû ½«¼Ó¾ç¢¶Ï¸ñ¾Ö</a></li>
+ <li><a href="httpdisabled://tech.163.com/digi/11/0408/09/713VLFGU001618VK.html">°®ÊË´ï½ÓÅ̲½²½¸ß δ»ñµÃÓÅÖÊ×ʲú</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://tech.163.com/digi/11/0408/08/713U164O001628C1.html">½µ¼Û×îÃÍ32´çµçÊÓÅ̵ã</a> <a href="httpdisabled://tech.163.com/digi/11/0408/06/713N6RJI001628C1.html">2Íò´òÔì35ƽС»§ÐÍÈ«Ì׼ҵ緽°¸</a></li>
+ <li><a href="httpdisabled://tech.163.com/digi/11/0408/00/7132P5VN001628C1.html">ÄÚÈÝȱʧ ÖÇÄܵçÊÓ»á·ñê¼»¨Ò»ÏÖ</a> <a href="httpdisabled://tech.163.com/digi/11/0408/00/7132P5VN001628C1.html">½üÆÚ¸ßÐԼ۱ȵçÊÓÍƼö</a></li>
+ <li><a href="httpdisabled://tech.163.com/digi/11/0408/06/713NF3Q5001628C1.html">Éú»î±¦µä ±ùÏäÈÕ³£Ê¹ÓÃСÇÏÃÅ»ã×Ü</a> <a href="httpdisabled://tech.163.com/digi/11/0408/07/713QIG89001628C1.html">΢²¨Â¯Îó½âÏÖÏó½âÎö</a></li>
+ <li><a href="httpdisabled://t.163.com/zt/digi/image">[΢²©] ´ºÅ¯»¨¿ª,ÉãӰʦ½ÌÄãÅijöÃÀÀöÕÕƬ</a></li>
+ <li><a href="httpdisabled://t.163.com/wangluohao/status/251893127646805523#retweet">[΢²©] ÍõÂ޺ƣºÉ˲»Æð£¡Ï´Ò»úµÄÄÚͲ±ÈÀ¬»øÍ°»¹ÒªÔà</a></li>
+ <li><a href="httpdisabled://t.163.com/yanjun/status/4389364559077117791#retweet">[΢²©] ´ïÈËÑÕ¿¡ÍƼöÓ¢¹úÖªÃûÐÔ¸ÐÅ®ÀÉÐãÂéÀ±Éí²ÄÉÍ</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://digibbs.tech.163.com/list/jiadian.html">ÂÛ̳</a> | <a target="_blank" href="httpdisabled://digibbs.tech.163.com/bbs/baidian/205545452.html">·øÉäÎÞ´¦²»ÔÚ ÄãÈçºÎµÖÓù¼Òµç·øÉä</a></li>
+ <li><a href="httpdisabled://digibbs.tech.163.com/list/jiadian.html">ÂÛ̳</a> |<a target="_blank" href="httpdisabled://digibbs.tech.163.com/bbs/baidian/205552044.html">iPod½ø³ø·¿ ÒôÀÖµç×Ó³ÓÈÃ×ö·¹ºÜ¶¯¸Ð</a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ <div id="t" class="mod wgt-tab">
+ <div class="tab-hd tab-u-10 clearfix">
+ <span class="tab-u current"><a href="httpdisabled://t.163.com/">΢Éú»î</a></span>
+ <span class="tab-u"><a href="httpdisabled://t.163.com/rank/daren">i´ïÈË</a></span>
+ <span class="tab-u"><a href="httpdisabled://t.163.com/rank?f=nav">ÈÈÃÅ</a></span>
+ </div>
+ <div class="bd tab-bd display-control">
+ <div class="tab-con current">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://t.163.com/wenbixia/status/-2133696119988502451#retweet"><img src="../img3.cache.netease.com/life/2011/4/8/20110408175702d86a7.jpg" alt="αÌϼ£ºÕâ¸Ð¾õºÃÌرð" title="αÌϼ£ºÕâ¸Ð¾õºÃÌرð" height="90" width="120" /><cite>αÌϼ£ºÕâ¸Ð¾õºÃÌرð</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://t.163.com/zt/book/chuantongVSwangluo">ÍøÂçÎÄѧÓ봫ͳÎÄѧÊÇ·ñ¶ÔÁ¢£¿</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://t.163.com/duoyu/status/2054209277184657633">¶äÓ棺°¬Çà˵Ðì־ĦÊǸöÉ«ÇéÊ«ÈË </a></li>
+ <li><a href="httpdisabled://t.163.com/lidaokui/status/3028352804215074228"> Àîµ¾¿û£ºÑëÐмÓÏ¢µÄÔ­ÒòÓëºó¹û </a></li>
+ <li><a href="httpdisabled://t.163.com/bqxiong/status/-5397918089764545559#retweet">ÐܱûÆ棺¹þ·ð°ÑÀñÌý豾¿ÆÉú°ì»éÀñ</a></li>
+ <li><a href="httpdisabled://t.163.com/zt/pub/supergirl2011">2011¿ìÀÖÅ®ÉùÍøÒ×΢²©Ö±Í¨Çø</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://t.163.com/zt/2011">´ºÌìÓÐÀñ</a> <a target="_blank" href="httpdisabled://t.163.com/zt/2011">7150ÈËÒÑ»ñ³äÖµ¿¨ ½ñÈÕ´ó½±Î¨Æ·»áÍòÔª´óÀñ°ü</a></li>
+ <li><a href="httpdisabled://t.163.com/ye_zi_feng/status/-3618379367969276810">Ò¶×ӷ磺´ó½ÍøÓÑ£¬ÎÒ¿´²»ÆðÄãÃÇ£¬ÄãÃÇÊ·ÉÏ×îÊÆÀû </a></li>
+ <li><a href="httpdisabled://t.163.com/jixiangsanbao/status/-4568484409001578815">¼ªÏéÈý±¦£º¡°°½Â³¹ÅÑŸèÎè¾ç¡±Àï×îСµÄÑÝÔ±²Å2Ëê8¸öÔ </a></li>
+ <li><a href="httpdisabled://t.163.com/1339279689/status/8695815502864064091#retweet">ÒÁɳ£ºÍõÓÐβµÄ¾ø×÷¡¶»³ÔеÄÅ®¹í¡·ÊÇ¡°ÀäÊãÇ顱µÄµä·¶</a></li>
+ <li><a href="httpdisabled://t.163.com/zt/void64">ÖйúÕì̽ʽ½âÃεÚÒ»È˳Éʵ×ÚÔÚÏßΪÄã½âÃÎ̽ѰÃξ³Ö®ÃÔ</a></li>
+ <li><a href="httpdisabled://t.163.com/luoyonghao/status/9174094553196327643#retweet">ÂÞÓÀºÆ£º»Ø¹Ë×òÒ¹±«²ª¡¤µÏÂ×Ñݳª»á</a> <a target="_blank" href="httpdisabled://t.163.com/luoyonghao/status/-7475778234050622400#retweet">àÞÒ®£¡µÏÂ×£¡Å£±Æ£¡</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://t.163.com/zt/idaren"><em class='fB'>i´ïÈË</em></a> | <a target="_blank" href="httpdisabled://t.163.com/qinhui">¾­¼Ãѧ´ïÈË ÇØêÍ</a><a target="_blank" href="httpdisabled://t.163.com/bashusong"> °ÍÊïËÉ</a> |<a target="_blank" href="httpdisabled://t.163.com/japan_earthquake">ÈÕ±¾µØÕð×îÐÂÏûÏ¢</a></li>
+ <li><a href="httpdisabled://t.163.com/rank/retweets"><em class='fB'>ÍƼö»°Ìâ</em></a> |<a target="_blank" href="httpdisabled://t.163.com/zt/pub/yushaolei">΢ÁÄÕ«</a> |<a target="_blank" href="httpdisabled://t.163.com/zt/pub/spring">ËæÊÖÅÄ´ºÌì</a> |<a target="_blank" href="httpdisabled://t.163.com/zt/pub/sixing">ÊÇ·ñÔÞ³ÉÈ¡ÏûËÀÐÌ</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <ul class="mod-imgList imgList-w80 clearfix">
+ <li><a href="httpdisabled://t.163.com/xiaohan"><img src="../img3.cache.netease.com/life/2011/4/6/201104061402503e782.jpg" alt="img" title="Ïôå«" height="80" width="80" /></a><p><a href="httpdisabled://t.163.com/xiaohan">Ïôå«</a></p></li>
+ <li><a href="httpdisabled://t.163.com/yuzhen0716"><img src="../img3.cache.netease.com/life/2011/4/6/20110406140048c8dea.jpg" alt="img" title="µËí²ÎÄ" height="80" width="80" /></a><p><a href="httpdisabled://t.163.com/yuzhen0716">µËí²ÎÄ</a></p></li>
+ <li><a href="httpdisabled://t.163.com/xiaoshushiping"><img src="../oimagec7.ydstatic.com/image@w=128&amp;h=128&amp;url=http%253A%252F%252F126.fm%252F3cAjJD" alt="img" title="ЦÊñ" height="80" width="80" /></a><p><a href="httpdisabled://t.163.com/xiaoshushiping">ЦÊñ</a></p></li>
+ <li><a href="httpdisabled://t.163.com/qinhui"><img src="../oimagea8.ydstatic.com/image@w=80&amp;h=80&amp;url=http%253A%252F%252F126.fm%252F40hcYl" alt="img" title="ÇØêÍ" height="80" width="80" /></a><p><a href="httpdisabled://t.163.com/qinhui">ÇØêÍ</a></p></li>
+ <li><a href="httpdisabled://t.163.com/maoshoulong"><img src="../img4.cache.netease.com/life/2011/3/7/20110307134125752e1.jpg" alt="img" title="ëÊÙÁú" height="80" width="80" /></a><p><a href="httpdisabled://t.163.com/maoshoulong">ëÊÙÁú</a></p></li>
+ <li><a href="httpdisabled://t.163.com/5461458243"><img src="../oimagea8.ydstatic.com/image@w=80&amp;h=80&amp;url=http%253A%252F%252F126.fm%252F2WEnFW" alt="img" title="ľ×ÓÃÀ" height="80" width="80" /></a><p><a href="httpdisabled://t.163.com/5461458243">ľ×ÓÃÀ</a></p></li>
+ <li><a href="httpdisabled://t.163.com/wuzuolai"><img src="../oimagea4.ydstatic.com/image@w=80&amp;h=80&amp;url=http%253A%252F%252F126.fm%252FPjU3g" alt="img" title="ÎâìñÀ´" height="80" width="80" /></a><p><a href="httpdisabled://t.163.com/wuzuolai">ÎâìñÀ´</a></p></li>
+ <li><a href="httpdisabled://t.163.com/ye_zi_feng"><img src="../oimagea8.ydstatic.com/image@w=80&amp;h=80&amp;url=http%253A%252F%252F126.fm%252F2x2iAO" alt="img" title="Ò¶×Ó·ç" height="80" width="80" /></a><p><a href="httpdisabled://t.163.com/ye_zi_feng">Ò¶×Ó·ç</a></p></li>
+ <li><a href="httpdisabled://t.163.com/lianyue"><img src="../img3.cache.netease.com/life/2011/2/24/20110224214610e49c1.jpg" alt="img" title="Á¬ÔÀ" height="80" width="80" /></a><p><a href="httpdisabled://t.163.com/lianyue">Á¬ÔÀ</a></p></li>
+ <li><a href="httpdisabled://t.163.com/1642658000"><img src="../oimageb3.ydstatic.com/image@w=80&amp;h=80&amp;url=http%253A%252F%252F126.fm%252FTyjFq" alt="img" title="ÕÂÚ±ºÍ" height="80" width="80" /></a><p><a href="httpdisabled://t.163.com/1642658000">ÕÂÚ±ºÍ</a></p></li>
+ <li><a href="httpdisabled://t.163.com/luoyonghao"><img src="../oimagec1.ydstatic.com/image@w=80&amp;h=80&amp;url=http%253A%252F%252F126.fm%252F3SWBUh" alt="img" title="ÂÞÓÀºÆ" height="80" width="80" /></a><p><a href="httpdisabled://t.163.com/luoyonghao">ÂÞÓÀºÆ</a></p></li>
+ <li><a href="httpdisabled://t.163.com/wuxiaobo"><img src="../oimageb2.ydstatic.com/image@w=80&amp;h=80&amp;url=http%253A%252F%252F126.fm%252F46NVMe" alt="img" title="ÎâÏþ²¨" height="80" width="80" /></a><p><a href="httpdisabled://t.163.com/wuxiaobo">ÎâÏþ²¨</a></p></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://t.163.com/zt/2011"><img src="../img3.cache.netease.com/life/2011/4/1/20110401105148c65f3.jpg" alt="ÍøÒ×΢²© ´ºÌìÓÐÀñ" title="ÍøÒ×΢²© ´ºÌìÓÐÀñ" height="90" width="120" /><cite>ÍøÒ×΢²© ´ºÌìÓÐÀñ</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://t.163.com/zt/clientshipping">ÍøÒ×΢²©¿Í»§¶Ë¶à°æÆë·¢</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://t.163.com/Open/status/-2521154402529877591#retweet">Ó¦ÓÃÍƼö£º¿¿Æ×Áµ°®Æ½Ì¨</a></li>
+ <li><a href="httpdisabled://t.163.com/Open/status/402613038294551289#retweet">Ó¦ÓÃÍƼö£ºÇÀŵÑÇ·½ÖÛ´¬Æ±</a></li>
+ <li><a href="httpdisabled://t.163.com/Open/status/-2610850867517385007#retweet">Ó¦ÓÃÍƼö£º×îÁ÷ÐеÄ΢ÓÎÏ·</a></li>
+ <li><a href="httpdisabled://t.163.com/Open/status/-8198518966669524288#retweet">Ó¦ÓÃÍƼö£º²â²â3ÄêºóÄãÊDz»ÊǸ»ÎÌ</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://t.163.com/zt/bbs/man">Ò×΢²©Æ·Åƻ㠵ÚÒ»ÆÚ£ºÐÍÄᦵä</a></li>
+ <li><a href="httpdisabled://t.163.com/mobile/iphone"><em class='fB'>΢²©¿Í»§¶ËÊ¡Á÷Á¿</em></a> <a target="_blank" href="httpdisabled://3g.163.com/links/66">ÏÂÔØ£ºAndroid</a> <a target="_blank" href="httpdisabled://3g.163.com/links/65">S60V5</a> <a target="_blank" href="httpdisabled://3g.163.com/links/63">S60V3</a> <a target="_blank" href="httpdisabled://3g.163.com/links/3020">Java</a></li>
+ <li><a href="httpdisabled://t.163.com/zt/pub/yyhf">º£ÐÄɳѰÕÒ"°Å½¶Ò¶Äк¢"</a></li>
+ <li><a href="httpdisabled://tech.163.com/special/wdk05/">ÎåµÀ¿ÚɳÁú£ºÎ¢²©Ó¦ÓõÄÏÖ×´ÓëÉÌҵģʽ</a></li>
+ <li><a href="httpdisabled://t.163.com/npdp_love/status/-1759136884114661834">Ϊ°®ÉÏÉ«£ºÏ£ÍûСѧÍâǽͿѻ»î¶¯</a></li>
+ <li><a href="httpdisabled://t.163.com/wwfchina/status/1978538898632885672"><em class=' I_V_'>µØÇòһСʱ£ºÍõç󵤳«Òé»·±£</em></a> <a target="_blank" href="httpdisabled://t.163.com/wwfchina/status/5103287786315323769">ÓðȪ¾Ü¾øÒ»´ÎÐԲ;ß</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://t.163.com"><em class='cBlack fB'>ÍƼö</em></a> |<a target="_blank" href="httpdisabled://t.163.com/19394671">Ö°³¡£ºÃÀÀöСϰ¹ß</a> |<a target="_blank" href="httpdisabled://t.163.com/43204654">ÊýÂ룺ÉãÓ°·¢ÉÕÓÑ</a></li>
+ <li><a href="httpdisabled://t.163.com"><em class='fB'>ÍøÕ¾</em></a> |<a target="_blank" href="httpdisabled://t.163.com/youku"> ÓÅ¿áÍø£º»Ø¹Ë°Â˹¿¨¾­µä</a> |<a target="_blank" href="httpdisabled://t.163.com/56wang"> 56Íø£º¹Ø×¢ÈÕ±¾µØÕð</a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="main-col-9">
+ <div id="house" class="mod wgt-tab">
+ <h2 class="tab-hd tab-u-9 clearfix">
+ <span class="tab-u current"><a href="httpdisabled://gz.house.163.com/">¹ãÖÝ·¿²ú</a></span>
+ <span class="tab-u"><a href="httpdisabled://house.163.com">ÒªÎÅ</a></span>
+ <span class="tab-u"><a href="httpdisabled://bbs.gz.house.163.com/">·¿²úÂÛ̳</a></span>
+ <span class="tab-u"><a href="httpdisabled://home.163.com/">¼Ò¾Ó</a></span>
+ </h2>
+ <div class="bd tab-bd display-control">
+ <div class="tab-con current">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://gz.house.163.com/11/0408/09/713UMB1I00873C6D.html"><img src="../img4.cache.netease.com/house/2011/4/8/201104080927161a54f.jpg" alt="²ðǨ»§·Ì°ùÈÎ־ǿ±»¾Ð" title="²ðǨ»§·Ì°ùÈÎ־ǿ±»¾Ð" height="90" width="120" /><cite>²ðǨ»§·Ì°ùÈÎ־ǿ±»¾Ð</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://gz.house.163.com/">"ÎåÒ»"¹ãÖÝÂ¥ÊÐÍÆ»õÔ¼4.5ÍòÌ×</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://gz.house.163.com/11/0408/08/713S0JHO00873C6D.html">¹úÎñÔº¶½²é16Ê¡·ÝÂ¥Êе÷¿ØÕþ²ß</a></li>
+ <li><a href="httpdisabled://gz.house.163.com/11/0408/08/713RK3N500873C6D.html">Ò»¼¾¶È·¿µØ²úÆóÒµÏúÊÛ´ó·ùÏ´ì</a></li>
+ <li><a href="httpdisabled://gz.house.163.com/11/0408/08/713RM5N600873C6D.html">·¿²úÖнé¹æ±ÜÏÞ¹ºÕþ²ß»¨Ñù°Ù³ö</a></li>
+ <li><a href="httpdisabled://gz.house.163.com/11/0407/21/712M3K4S00873C6D.html">Ê®´ó·¿Æó¸ß²ã:µ÷¿ØÊÇÈëÊÐʱ»ú</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://gz.house.163.com/11/0408/08/713R1AKI00873C6D.html">¹ãÖÝÊÐÇøлõÉÏÊдÙ"Á¿¼ÛÆëÕÇ"</a> <a href="httpdisabled://gz.house.163.com/11/0408/07/713OD7DQ00873C6D.html">3Ô¶þÊÖÂ¥¼Û´´Ð¸ß</a></li>
+ <li><a href="httpdisabled://gz.house.163.com/11/0408/07/713P1HI300873C6D.html">¹ãÖÝÂ¥ÊÐתÏòÖÐÐÄÇø¹©Ó¦</a> <a href="httpdisabled://gz.house.163.com/11/0408/07/713PJKVD00873C6D.html">Öнé¼Ó°àÂôĹµØ¿ª¼Û16Íò</a></li>
+ <li><a href="httpdisabled://gz.house.163.com/11/0408/08/713TR26V00873L40.html">DZÁ¦ÇøÓòÖð¸öÊý:°ÂÌåгÇ</a> <a target="_blank" href="httpdisabled://gz.house.163.com/11/0408/08/713TUVHC00873L40.html">°×¶ì̶</a> <a target="_blank" href="httpdisabled://gz.house.163.com/11/0408/08/713U2U1U00873L40.html">°×ÔÆгÇ</a> <a target="_blank" href="httpdisabled://gz.house.163.com/11/0408/08/713U8KVT00873L40.html">º£Öé</a></li>
+ <li><a href="httpdisabled://gz.house.163.com/11/0408/08/713TARIR00873L40.html">ÂåϪÔÙÏÖÐÂÅÌÍÆ»õ³±</a> <a target="_blank" href="httpdisabled://gz.house.163.com/11/0408/08/713TFJL100873L40.html">¹ãÖÝд×ÖÂ¥¿ÕÖÃÂʳ¬Á½³É</a></li>
+ <li><a href="httpdisabled://gz.house.163.com/11/0407/10/711HGQHJ00873L40.html">¶þ´ÎÌáÈ¡¹«»ý½ðÊÖÐø¼ò»¯</a> <a target="_blank" href="httpdisabled://gz.house.163.com/special/xf_xinyejituan/">ÐÅÒµ¼¯Íźñ»ý´ý±¡·¢</a></li>
+ <li>[<a target="_blank" href="httpdisabled://bbs.gz.house.163.com">ÂÛ̳</a>]<a href="httpdisabled://bbs.gz.house.163.com/photoview/2ALD0015/5011.html">Õ¬ÄÐÅ®ÉñÕÅÜ°Óè˽·¿ÕÕ</a> <a target="_blank" href="httpdisabled://bbs.gz.house.163.com/photoview/2ALD0015/5037.html">ÇÅѹ"×îţ¥·¿"15Äê</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://home.163.com/" class="fB">[¼Ò¾Ó]</a> | <a href="httpdisabled://home.163.com/11/0407/19/712FHPOC00104IJT.html">ͬСÇø´óÁ¿TOTOÂíÍ°¿ªÁÑ ³§¼Ò:ÖÊÁ¿ºÏ¸ñ</a></li>
+ <li><a href="httpdisabled://home.163.com/" class="fB">[¼Ò¾Ó]</a> | <a href="httpdisabled://home.163.com/photoview/2OHS0010/1363.html#q=1">ÁÄÕ«·ç¸ñ ÄÐÖ÷ÈËÇ××ÔÉè¼Æ×°ÐÞÃ÷Ç帴¹ÅÎÝ</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://gz.house.163.com/11/0408/09/7140BPSG00873L40.html"><img src="../img3.cache.netease.com/house/2011/4/8/20110408094024dfb90.gif" alt="Öйú¸»ºÀ¿ñɨº£ÍâºÀÕ¬" title="Öйú¸»ºÀ¿ñɨº£ÍâºÀÕ¬" height="90" width="120" /><cite>Öйú¸»ºÀ¿ñɨº£ÍâºÀÕ¬</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://gz.house.163.com/11/0406/11/70V13CVI0087482R.html">ÓÎ×ÊÈÈÇ®Öð²½³·ÀëÒ»Ïß³ÇÊÐ</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://gz.house.163.com/11/0406/16/70VI1I6H00873CN3.html">²»ÏÞ¹º+È뻧¹ãÖÝ:80ÍòÂò´óÈý·¿</a></li>
+ <li><a href="httpdisabled://gz.house.163.com/special/xf_securityroom/">Âò¼ÒÖÃÒµÔú¶ÑÔö³Ç´Ó»¯</a></li>
+ <li><a href="httpdisabled://gz.house.163.com/11/0408/08/713T5S6200873L40.html">¹ãÖÝÊÐÇøлõÉÏÊдÙÁ¿¼ÛÆëÕÇ</a></li>
+ <li><a href="httpdisabled://gz.house.163.com/11/0406/08/70UP8HAN00873L40.html">½ÒÃØÍøÂç·¿ÍеÄÄÇЩʶù</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://gz.house.163.com/11/0408/09/713VBEJ900873CN0.html">½£ÇÅ¿¤ÔÚÊÛ½­É½Â¥Íõ ÓŻݵǼÇÁ½ÍòµÖÁùÍò</a> <a href="httpdisabled://xf.house.163.com/gz/search!xfs.action?district=%B7%AC%D8%AE&plate=&property=&price=&keyword=">·¬Ø®ÈÈÅÌ</a></li>
+ <li><a href="httpdisabled://gz.house.163.com/">[¹ãÖÝ]</a> | <a href="httpdisabled://gz.house.163.com/11/0322/14/6VONRKBA00873CN0.html">½ðÈ󲬹¬ÍÆ60-110©O²úÆ· Ê׸¶Îå³É½ö90Íò/Ì×Æð</a></li>
+ <li><a href="httpdisabled://bj.house.163.com/">[±±¾©]</a> | <a href="httpdisabled://bj.house.163.com/">[ÓÅ»Ý]</a> <a target="_blank" href="httpdisabled://bj.house.163.com/11/0408/07/713QHE3I00073V0K.html">±±ËÄ»·Íû¾©ºÏÉú¡¤÷è÷ëÉç °ì¿¨ÏíÊÜ1ÍòµÖ5Íò</a></li>
+ <li><a href="httpdisabled://sh.house.163.com/">[ÉϺ£]</a> | <a href="httpdisabled://sh.house.163.com/special/00073V4M/shxntfj.html">[ÌÔ·¿]</a> <a target="_blank" href="httpdisabled://sh.house.163.com/special/xntf-blyzy/">ÌÔµÏÊ¿Äáз¿:±£ÀûÓùé×</a> <a target="_blank" href="httpdisabled://sh.house.163.com/special/00074ACD/xntf-bjsxz.html">±Ï¼ÓË÷</a> <a target="_blank" href="httpdisabled://sh.house.163.com/special/00074ACD/xntf-jlgj.html">½ðÁì¹ú¼Ê</a></li>
+ <li><a href="httpdisabled://sz.house.163.com/">[ÉîÛÚ]</a> | <a href="httpdisabled://sz.house.163.com/11/0408/10/7142SVJV00073V29.html">ÿÈÕÊý¾Ý£º4.7з¿³É½»62Ì× ¾ù¼Û19348Ôª</a></li>
+ <li><a href="httpdisabled://bbs.gz.house.163.com/">[ÂÛ̳]</a> <a href="httpdisabled://bbs.gz.house.163.com/bbs/housestory/205466349.html">BFÓÐ10ÍòÂò·¿Ô۾Ͳ»·ÖÊÖ</a> <a target="_blank" href="httpdisabled://bbs.gz.house.163.com/photoview/2ALD0015/5046.html">µ±³èÎïÓöÉϺ¢×Ó</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://home.163.com/" class="fB">[¼Ò¾Ó]</a> | <a href="httpdisabled://home.163.com/special/lovecolor/">¡°Îª°®ÉÏÉ«¡±»î¶¯½áÊø 4ǧ¼þ×÷Æ·ÏÔ°®ÐÄ</a></li>
+ <li><a href="httpdisabled://home.163.com/" class="fB">[¼Ò¾Ó]</a> | <a href="httpdisabled://home.163.com/photoview/2OHS0010/1364.html#q=1">³¬¹ýÔ¤ËãÁË£¡25Íò´òÔì105ƽµÍµ÷ÉÝ»ª¼Ò</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://bbs.gz.house.163.com/photoview/2ALD0015/5011.html"><img src="../img3.cache.netease.com/house/2011/4/7/201104070846149dec5.jpg" alt="Õ¬ÄÐÅ®ÉñÕÅÜ°Óè˽·¿ÕÕ" title="Õ¬ÄÐÅ®ÉñÕÅÜ°Óè˽·¿ÕÕ" height="90" width="120" /><cite>Õ¬ÄÐÅ®ÉñÕÅÜ°Óè˽·¿ÕÕ</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://bbs.gz.house.163.com/">½ãÓÐ8ÍòÊ׸¶È´±»Öн鵱½ÖÁèÈè</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://xiaochongfan.blog.163.com/blog/static/13056609520113723936387/">¸ßѹʹ×ʱ¾Ä¿±êת¶þÈýÏß³ÇÊÐ</a></li>
+ <li><a href="httpdisabled://gaunzhui01.blog.163.com/blog/static/11604175820113862511825/">¶­·ª°ÇÁ˾åŲƸ»Õߵĵ׿ã</a></li>
+ <li><a href="httpdisabled://garydens.blog.163.com/blog/static/9988583320113755236416/">±£ÕÏ·¿ÈëÊÐ »ò¸Ä±äÊг¡¸ñ¾Ö</a></li>
+ <li><a href="httpdisabled://lgs1.blog.163.com/blog/static/276736320113810177203/">¶½²éÄܶÔÕþ¸®Æðµ½¼ì²é×÷ÓÃÂð</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li>[<a target="_blank" href="httpdisabled://bbs.gz.house.163.com">Ã÷ÐÇ</a>] <a href="httpdisabled://bbs.gz.house.163.com/bbs/homegossip/205304885.html">·¶çâç÷Óµ6¶°ºÀÕ¬³ö¼Þ</a> <a target="_blank" href="httpdisabled://bbs.gz.house.163.com/bbs/homegossip/204594863.html">Îé×È1ÒÚÂôºÀ»ªÓÎͧ</a></li>
+ <li>[<a target="_blank" href="httpdisabled://bbs.gz.house.163.com">լŮ</a>] <a href="httpdisabled://bbs.gz.house.163.com/bbs/share/205341065.html">¹ãƯµÄÎÒ3Äê×â·¿5´Î</a> <a target="_blank" href="httpdisabled://bbs.gz.house.163.com/photoview/2ALD0015/5037.html">"×îţ¥"±»Ñ¹ÇÅÏÂ15Äê</a></li>
+ <li>[<a target="_blank" href="httpdisabled://bbs.gz.house.163.com">ºÏ×â</a>] <a href="httpdisabled://bbs.gz.house.163.com/bbs/share/204254000.html">ËýµÄÄÚÒ¹ҹ«¹²Ô¡ÊÒ</a> <a target="_blank" href="httpdisabled://bbs.gz.house.163.com/bbs/share/204466628.html">ͬ×âСÄÐÅ®ºÙßݲ»¹ØÃÅ</a></li>
+ <li>[<a target="_blank" href="httpdisabled://bbs.gz.house.163.com">´µË®</a>] <a href="httpdisabled://bbs.gz.house.163.com/bbs/jinshangu/204635749.html">WCµÄºæÊÖ»úÒ²ÄÃÀ´¶ñ¸ã</a> <a target="_blank" href="httpdisabled://bbs.gz.house.163.com/bbs/housegossip/204633922.html">Öйú·¿²ú±©µøʱ¼ä±í</a></li>
+ <li>[<a href="httpdisabled://bbs.home.163.com/list/sheji.html">»î¶¯</a>]<a href="httpdisabled://bbs.home.163.com/bbs/sheji/205669205.html">µÚËÄÆÚ¡¾Ãâ·Ñ»§ÐÍ·ÖÎö¡¿½áÊø£¬´ðÒɼ¯ÄÉ£¡</a></li>
+ <li>[<a target="_blank" href="httpdisabled://bbs.home.163.com/list/sheji.html">¹¥ÂÔ</a>]<a href="httpdisabled://bbs.home.163.com/bbs/sheji/205684769.html">¾ÈÃü°¡£¬ÎÒÕâÒìÐεĿÍÌüµØ°å¸ÃÔõôÑùÆÌ°¡</a></li>
+ <li>[<a target="_blank" href="httpdisabled://bbs.home.163.com/list/homeshow.html">×°ÐÞÕбê</a>]<a href="httpdisabled://bbs.home.163.com/bbs/homebbs/205693470.html">Ñོ»é·¿×°ÐÞ£¬×°ÐÞ¹«Ë¾Ç뱨¼Û¸øÎÒ</a></li>
+ <li>[<a target="_blank" href="httpdisabled://bbs.home.163.com/list/jjfs.html">·çË®</a>]<a href="httpdisabled://bbs.home.163.com/bbs/jjfs/205670377.html">רҵ·çˮʦÔÚÏßÃâ·ÑΪÄú·ÖÎö»§ÐÍ·çË®</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://home.163.com/special/lovecolor/"><img src="../img4.cache.netease.com/home/2011/4/7/20110407131936bb4ec.png" alt="Ϊ°®ÉÏÉ«»î¶¯Ô²Âú½áÊø" title="Ϊ°®ÉÏÉ«»î¶¯Ô²Âú½áÊø" height="90" width="120" /><cite>Ϊ°®ÉÏÉ«»î¶¯Ô²Âú½áÊø</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://home.163.com/photoview/2OHS0010/1363.html#q=1">ÁÄÕ«·ç ÄÐÖ÷ÈËÉè¼ÆÃ÷Ç帴¹ÅÎÝ</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://home.163.com/photoview/2OHS0010/1362.html#q=1">°×Áì8Íò´òÔì98ƽÃ×¼òÔ¼·ç¸ñ¼Ò</a></li>
+ <li><a href="httpdisabled://home.163.com/photoview/2OHS0010/1364.html#q=1">³¬Ô¤ËãÁË 25Íò×°105ƽÃ×ÉÝ»ª¼Ò</a></li>
+ <li><a href="httpdisabled://home.163.com/11/0407/22/712Q48HU00104IJF.html">3¿î¶ùͯ·¿ÅäÉ«´ø¸ø±¦±´ºÃÐÄÇé</a></li>
+ <li><a href="httpdisabled://home.163.com/11/0407/23/712T8T9G00104IJ9.html">ÑîÃÝ´©Ë¯ÒÂÅÄÐÔ¸ÐÓÕÈ˼ҾÓдÕæ</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li>[<a target="_blank" href="httpdisabled://home.163.com/weiyu/">Èȵã</a>] <a href="httpdisabled://home.163.com/11/0407/19/712FHPOC00104IJT.html">ͬСÇø150Ì×TOTOÂíÍ°¿ªÁÑ ³§¼Ò:·ÇÖÊÁ¿ÎÊÌâ</a></li>
+ <li>[<a target="_blank" href="httpdisabled://home.163.com/jiadian/">¼Òµç</a>] <a href="httpdisabled://home.163.com/11/0407/21/712MU91O00104IJI.html">¿Õµ÷ÕǼ۴«ÑÔ³ÉÕæ ±¾ÔÂÖлòÓ­È«ÃæÕǼÛ</a></li>
+ <li>[<a target="_blank" href="httpdisabled://home.163.com/jiaju/">¼Ò¾ß</a>] <a href="httpdisabled://home.163.com/11/0407/22/712PNBK000104IJJ.html">ÓºÈÝ»ª¹ó Æß¿îÆø¶È·Ç·²Å·Ê½Ì«×Ó´²ÍƼö</a></li>
+ <li>[<a target="_blank" href="httpdisabled://home.163.com/menchuang/">ÃÅ´°</a>] <a href="httpdisabled://home.163.com/11/0407/21/712O3DNS00104IJG.html">Æ­ÄãûÉÌÁ¿ ÏÊΪÈËÖªµÄľÃÅÈý´óÏúÊÛ¹î¼Æ</a></li>
+ <li>[<a target="_blank" href="httpdisabled://home.163.com/weiyu/">ÎÀÔ¡</a>] <a href="httpdisabled://home.163.com/11/0407/23/712S4HF400104IJK.html">´òÆÆ´«Í³ÀíÄî ÓÃÂíÈü¿ËÆÌÌù³öʱÉÐìű³¾°</a></li>
+ <li>[<a target="_blank" href="httpdisabled://home.163.com/jiafang/">¼Ò·Ä</a>] <a href="httpdisabled://home.163.com/11/0407/22/712OS7O700104IJH.html">¸ø¼Ò´©ÉÏÇåдº×° 3¿î¿¿µæÑÝÒï´ºÈÕçÍ·×</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><span><a target="_blank" href="httpdisabled://bbs.home.163.com/bbs/jiance/203933236.html">°ïÄãÉó×°ÐÞ±¨¼Ûµ¥</a></span> | <a href="httpdisabled://bbs.home.163.com/bbs/sheji/201770537.html">Éè¼ÆʦÔÚÏßÃâ·ÑÌṩ»§ÐÍ·½°¸</a></li>
+ <li><span><a target="_blank" href="httpdisabled://home.163.com/special/tujinew/">ͼ¿â</a></span> | <a href="httpdisabled://home.163.com/special/photo/#s=¿ÍÌü&q=1">[¿ÍÌü]</a> <a target="_blank" href="httpdisabled://home.163.com/special/photo/#s=ÎÔÊÒ&q=1">[ÎÔÊÒ]</a> <a target="_blank" href="httpdisabled://home.163.com/special/photo/#s=С»§ÐÍ&q=1">[С»§ÐÍ]</a> <a target="_blank" href="httpdisabled://home.163.com/special/photo/#s=ÌïÔ°&q=1">[ÌïÔ°]</a> <a target="_blank" href="httpdisabled://home.163.com/special/photo/#s=µØÖк£&q=1">[µØÖк£]</a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ <div id="game" class="mod wgt-tab">
+ <h2 class="tab-hd tab-u-9 clearfix">
+ <span class="tab-u current"><a href="httpdisabled://game.163.com/">ÓÎÏ·</a></span>
+ <span class="tab-u"><a href="httpdisabled://game.163.com/photo/">ÃÀͼ</a></span>
+ <span class="tab-u"><a href="httpdisabled://s.163.com/">ÐǼÊ2</a></span>
+ <span class="tab-u"><a href="httpdisabled://w.163.com/">ħÊÞÊÀ½ç</a></span>
+ </h2>
+ <div class="bd tab-bd display-control">
+ <div class="tab-con current">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://game.163.com/"><img src="../img4.cache.netease.com/game/2011/4/8/2011040811265683661.jpg" alt="»­¼ÒÉ­ÆøÂ¥20ÖÜÄê" title="»­¼ÒÉ­ÆøÂ¥20ÖÜÄê" height="90" width="120" /><cite>»­¼ÒÉ­ÆøÂ¥20ÖÜÄê</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://game.163.com/special/news/obama_game.html">°Â°ÍÂíÓëÃÀ¹úÓÎÏ·µÄÉç»áµØλ</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://game.163.com/11/0408/11/7145QSKP00314K8H.html">ÖйúÓÎÏ·Íæ¼ÒµÄÊ®´ó²»Á¼Ï°¹ß</a></li>
+ <li><a href="httpdisabled://game.163.com/11/0408/09/7141LQKA00314K8G.html">75ËêÀÏÄÌÄÌÊÕ·ÏÆ·Çжϻ¥ÁªÍø</a></li>
+ <li><a href="httpdisabled://game.163.com/11/0408/09/713VDQE500314K8G.html">×îÖµµÃ¸¶·ÑÏÂÔصÄ30¿îiPadÓÎÏ·</a></li>
+ <li><a href="httpdisabled://game.163.com/11/0408/09/7140OAIT00314K8G.html">Ë÷ÄáÍƳöGTÈü³µÔ˶¯ÏµÁзþ×°</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://game.163.com/11/0408/10/7142PQP200314K8F.html">ÎÚ³ľÆëÒ»Ó×½ÌÈýÄ꡶´«Ææ¡·¡°Í桱µô14Íò¹«¿î</a></li>
+ <li><a href="httpdisabled://w.163.com/11/0407/15/712281PD00314C3U.html">¹ú·þ·âºÅ´óÁ¦´ò»÷ÍŶӸ±±¾ÆÛÕ©</a> <a target="_blank" href="httpdisabled://w.163.com/11/0407/10/711I56AN00314C3U.html ">ÈýÖÖPVPÍæ¼Ò·ÖÎö</a></li>
+ <li><a href="httpdisabled://game.163.com/11/0407/19/712GRGT500314K8H.html">×°»ú£º1ÄêÒÔºóÒ²²»¹ýʱµÄÅ£X»úÆ÷</a> <a target="_blank" href="httpdisabled://game.163.com/11/0407/17/712A7VM000314K95.html">PS3ÍøÂçÔâ¹¥»÷</a></li>
+ <li><a href="httpdisabled://game.163.com/11/0408/10/7142145000314K8G.html">ÍæÓÎÏ·¼õÉÙ¾«×ÓÖÂÈË¿ÚϽµ</a> <a target="_blank" href="httpdisabled://game.163.com/11/0408/10/7141U05D00314K8G.html">ÐéÄâÈËÎïÌÆÀÏѼ×îÓÐÇ®</a></li>
+ <li><a href="httpdisabled://game.163.com/11/0407/15/71235H5800314K95.html">Ë÷Äá³ÆNGP²»»áÑÓÆÚ</a> <a target="_blank" href="httpdisabled://s.163.com/11/0408/11/71488RKL00314D0E.html">ľ¹ÏÖ®ºóħÊÞÔÙÎÞ¡°Èý¾ÞÍ·¡±</a></li>
+ <li><a href="httpdisabled://ka.game.163.com/">[·¢ºÅ]</a> <a target="_blank" href="httpdisabled://ka.game.163.com/ceshihao/xw/ceshi.html">¡¶ÐþÎä¡·ÌåÑé²âÊÔ¼¤»îÂë</a> <a target="_blank" href="httpdisabled://ka.game.163.com/xinshouka/zt2/neice.html">¡¶Õ÷;2¡·¹ó±ö¿¨</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://bbs.game.163.com/"><em class='fB'>ÂÛ̳</em></a> | <a target="_blank" href="httpdisabled://bbs.game.163.com/thread-167638161-1-2.html">ÈéÉñ°æ·¶±ù±ùÆØ˽ÕÕ</a> | <a target="_blank" href="httpdisabled://bbs.game.163.com/thread-167606978-1-3.html">ÃÀÍÈÃûÄ£ÍÑÒÂPKÔ½ÄÏÃÃ</a></li>
+ <li><a href="httpdisabled://bbs.game.163.com/"><em class='fB'>Èȵã</em></a> | <a target="_blank" href="httpdisabled://bbs.game.163.com/thread-167619841-1-1.html">ÑîÃݶƨ¹ÉдÕæÁÃÈË</a> | <a target="_blank" href="httpdisabled://bbs.game.163.com/thread-167685377-1-2.html">º«×îÃÀ³µÄ£¾ÞÐØÀ´Ï®</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <ul class="mod-imgList dotline imgList-w160-2 clearfix">
+ <li><a href="httpdisabled://game.163.com/photoview/482T0031/19381.html#p=712S98L1482T0031"><img src="../img3.cache.netease.com/game/2011/4/8/20110408100456977e5.jpg" alt="ÃÄÑÛËÖÐØ·çɧÃÀ½¿Äï¹ë·¿×ÔÅÄ" title="ÃÄÑÛËÖÐØ·çɧÃÀ½¿Äï¹ë·¿×ÔÅÄ" height="90" width="160" /></a><p><a href="httpdisabled://game.163.com/photoview/482T0031/19381.html#p=712S98L1482T0031">ÃÄÑÛËÖÐØ·çɧÃÀ½¿Äï¹ë·¿×ÔÅÄ</a></p></li>
+ <li><a href="httpdisabled://game.163.com/photoview/43UD0031/19365.html#p=712DL3BB43UD0031"><img src="../img3.cache.netease.com/game/2011/4/8/20110408091923ca1d8.jpg" alt="¡¶½ÖÍ·°ÔÍõ¡·Q°æ½ÇÉ«¶Ô¾ö" title="¡¶½ÖÍ·°ÔÍõ¡·Q°æ½ÇÉ«¶Ô¾ö" height="90" width="160" /></a><p><a href="httpdisabled://game.163.com/photoview/43UD0031/19365.html#p=712DL3BB43UD0031">¡¶½ÖÍ·°ÔÍõ¡·Q°æ½ÇÉ«¶Ô¾ö</a></p></li>
+ <li><a href="httpdisabled://game.163.com/photoview/482T0031/19375.html#p=712PC9IB482T0031"><img src="../img4.cache.netease.com/game/2011/4/8/201104081009084803f.jpg" alt="ÃÀ¹úÈËÑÛÖÐ×îÃÀµÄÈÕ±¾ÃÀÅ®" title="ÃÀ¹úÈËÑÛÖÐ×îÃÀµÄÈÕ±¾ÃÀÅ®" height="90" width="160" /></a><p><a href="httpdisabled://game.163.com/photoview/482T0031/19375.html#p=712PC9IB482T0031">ÃÀ¹úÈËÑÛÖÐ×îÃÀµÄÈÕ±¾ÃÀÅ®</a></p></li>
+ <li><a href="httpdisabled://game.163.com/photoview/43UD0031/19366.html#p=712DTK3N43UD0031"><img src="../img4.cache.netease.com/game/2011/4/8/201104081007164a116.jpg" alt="ToHeart2СÄÁ°®¼ÑÃÀÉ«ÊÖ°ì" title="ToHeart2СÄÁ°®¼ÑÃÀÉ«ÊÖ°ì" height="90" width="160" /></a><p><a href="httpdisabled://game.163.com/photoview/43UD0031/19366.html#p=712DTK3N43UD0031">ToHeart2СÄÁ°®¼ÑÃÀÉ«ÊÖ°ì</a></p></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://game.163.com/photo/"><em class='fB'>¾«²ÊרÌâ</em></a> | <a target="_blank" href="httpdisabled://game.163.com/special/girls/yxmnchushen.html">½èÓÎÏ·ÉÏλµÄÃÀÅ®</a> | <a target="_blank" href="httpdisabled://game.163.com/special/girls/nvyou.html">ÓëÓÎÏ·ÓÐȾµÄÅ®ÓÅ</a></li>
+ <li><a href="httpdisabled://game.163.com/photo/"><em class='fB'>°ËØÔȤͼ</em></a> | <a target="_blank" href="httpdisabled://game.163.com/photoview/43UD0031/18336.html#p=6VBVC0OC43UD0031">»ÎÑÛ´ó°×Íȼ¯½õ</a> | <a target="_blank" href="httpdisabled://game.163.com/photoview/43UD0031/18327.html#p=6VBGSIPC43UD0031">ÓÎÏ·ÖеÄÇéÉ«³¡¾°</a></li>
+ <li><a href="httpdisabled://game.163.com/photo/"><em class='fB'>ÓÎÏ·ÃÀÅ®</em></a> | <a target="_blank" href="httpdisabled://game.163.com/photoview/482T0031/19021.html#p=70FD1J8F482T0031">¸É¶¶×ß¹âʧ¿Ø</a> | <a target="_blank" href="httpdisabled://game.163.com/photoview/482T0031/18722.html?1301149813#p=70067SO7482T0031">ÖÜΤͮ³ß¶ÈÔÙÉý¼¶</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://s.163.com/"><img src="../img4.cache.netease.com/game/2011/4/5/2011040502293054a8f.jpg" alt="̨Íæ¼Ò×·ÅõÌ©¹ú¶¹»¨ÃÃ" title="̨Íæ¼Ò×·ÅõÌ©¹ú¶¹»¨ÃÃ" height="90" width="120" /><cite>̨Íæ¼Ò×·ÅõÌ©¹ú¶¹»¨ÃÃ</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://s.163.com/">ÐǼÊ2¹«²â½áÊø Ô¿¨¹ºÂòÖ¸ÄÏ</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://s.163.com/11/0408/11/71488RKL00314D0E.html">ľ¹ÏÖ®ºó ħÊÞÔÙÎÞ¡°Èý¾ÞÍ·¡±</a></li>
+ <li><a href="httpdisabled://s.163.com/11/0407/23/712S5EH500314D0E.html">GSLÎåÔÂÈüS¼¶·Ö×鹫²¼</a> <a target="_blank" href="httpdisabled://s.163.com/special/gsl/">GSLרÌâ</a></li>
+ <li><a href="httpdisabled://s.163.com/11/0406/02/70U4TMRO00314D0E.html">À¶Ìû£ºÐǼÊ2ÔÚÏß³äÖµ·½·¨Ö¸ÄÏ</a></li>
+ <li><a href="httpdisabled://s.163.com/11/0406/10/70UV21F200314D0E.html"><ÐǼÊ2>ÕýʽÔËÓª ÌṩÃâ·ÑÊÔÍæ</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://s.163.com/special/00314IL1/sc2zs.html">[¹¥ÂÔ]</a> <a target="_blank" href="httpdisabled://s.163.com/11/0406/15/70VGGIP700314D0E.html">ÐÂÊֱضÁ£ºÎÒ¸ÃÈçºÎ´òÈë¸ü¸ß¼¶±ðÌìÌÝÁªÈü</a></li>
+ <li><a href="httpdisabled://s.163.com/special/sc2_videolist/">[ÊÓƵ]</a> <a target="_blank" href="httpdisabled://s.163.com/11/0408/00/7132L16A00314D09.html"><em class=' I_V_'>ÐǼÊ2½Ìѧ:³æ×åÍâË«¿ª¾Ö</em></a> <a target="_blank" href="httpdisabled://s.163.com/11/0408/10/71448F2T00314D09.html">TvZË«±øӪѹÖÆ</a></li>
+ <li><a href="httpdisabled://s.163.com/special/sc2_maplist/">[µØͼ]</a> <a target="_blank" href="httpdisabled://s.163.com/11/0407/12/711MMP7L00314D0E.html"><°¬¶ûʳÉñ><ÐDZ¦ÃÔÕó><ÇóÉúÎÞ·>Õ½ÍøÉÏÏß</a></li>
+ <li><a href="httpdisabled://s.163.com/special/go4sc2/">[רÌâ]</a> <a target="_blank" href="httpdisabled://s.163.com/11/0408/11/7147LA1200314D09.html"><em class=' I_V_'>Go4SC2ÖйúÇø#21£ºF91¶á¹Ú</em></a> <a target="_blank" href="httpdisabled://s.163.com/11/0408/11/7146GJMP00314D09.html">°ë¾öÈüVOD</a></li>
+ <li><a href="httpdisabled://s.163.com/special/00314IL1/sc2events.html">[ÐÂÎÅ]</a> <a target="_blank" href="httpdisabled://s.163.com/11/0407/13/711R0OIJ00314D0E.html">¹ú·þÍ·ÏñµÛ£ºÇàÍ­×é3000+ʤÀû³É¾ÍºÚ°µÖ®Éù</a></li>
+ <li><a href="httpdisabled://s.163.com/special/sc2_videolist/">[ÊÓƵ]</a> <a target="_blank" href="httpdisabled://s.163.com/11/0407/00/710EU79900314D09.html"><em class=' I_V_'>JY½â˵:Áú¹·°ü³­ÆÆÉñ×å</em></a> <a target="_blank" href="httpdisabled://s.163.com/11/0407/14/711T9M1N00314D09.html">xiaOt³ÉÍõ֮·2</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://s.163.com/special/sc2_special/"><em class='fB'>ÐǼÊרÌâ</em></a> | <a target="_blank" href="httpdisabled://s.163.com/special/sc2newbie/">ÐǼÊ2ÐÂÊÖרÌâ</a> | <a target="_blank" href="httpdisabled://s.163.com/special/single/">ÐǼÊ2µ¥ÈËÕ½ÒÛרÌâ</a></li>
+ <li><a href="httpdisabled://rep.s.163.com/"><em class='fB'>¼ÏñÍƼö</em></a> | <a target="_blank" href="httpdisabled://s.163.com/11/0330/15/70DGET1C00314D06.html">ÐǼÊ2ÿÖܼÏñTOP5</a> | <a target="_blank" href="httpdisabled://rep.s.163.com/Sc2Replay.aspx?ReplayID=4016">¸ÐȾ³æ¿ÕͶÍÀÅ©</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://w.163.com/11/0407/10/711H0GN200314C3U.html"><img src="../img3.cache.netease.com/game/2011/4/8/2011040810253254779.jpg" alt="±©Ñ©¹Ù·½»­ÀȸüÐÂÔ­»­" title="±©Ñ©¹Ù·½»­ÀȸüÐÂÔ­»­" height="90" width="120" /><cite>±©Ñ©¹Ù·½»­ÀȸüÐÂÔ­»­</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://w.163.com/11/0407/15/712281PD00314C3U.html">´ò»÷ÆÛÕ©£º¹ú·þÔÙ·£Êý°ÙÕʺÅ</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://w.163.com/11/0404/10/70PPBQ6200314C3U.html">ESLÖйú¾º¼¼³¡£ºµÚÎå½ì¿ªÊ¼±¨Ãû</a></li>
+ <li><a href="httpdisabled://w.163.com/11/0404/10/70POO85E00314C3U.html">Õ½Õ½µÂ´óÄæת»ñESL-Go4WOW¹Ú¾ü</a></li>
+ <li><a href="httpdisabled://w.163.com/11/0326/10/702L895300314C3U.html">CTM×îиĶ¯£º¶ÜÅƸ½Ä§50ר¾«</a></li>
+ <li><a href="httpdisabled://w.163.com/11/0326/10/702IPQ6900314C3U.html">¸÷Ö°Òµ4T11ÌØЧÊÕÒæÅÅÃû</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://w.163.com/11/0408/11/7145EIFL00314C3U.html">´óÔÖ±ä4.1£º·ÆÀ­Ë¹ÏÖÉñÃضȼٴåׯ</a> <a target="_blank" href="httpdisabled://w.163.com/11/0408/10/7144L1O700314C3U.html">°§º¿¶´Ñ¨½«¼ò»¯</a></li>
+ <li><a href="httpdisabled://w.163.com/11/0408/09/7141696E00314C3U.html">DKµ¥Ë¢¿ñÈËÓûµ¥Ìô°ÂÀ­»ù¶û</a> <a target="_blank" href="httpdisabled://w.163.com/11/0408/11/7146BVU100314C3U.html">Èñ²½ÍƳöħÊÞÖ÷ÌâÇòÒÂ</a></li>
+ <li><a href="httpdisabled://w.163.com/11/0408/11/7147BMJ300314C3U.html">ÓÎÏ·½Ì»áÎÒµÄÊ®¼þÊÂ</a> <a target="_blank" href="httpdisabled://bbs.game.163.com/thread-167684983-1-1.html">ÐÄÀï½Ç¶È·ÖÎöħÊÞΪºÎÁ÷ÐÐ</a></li>
+ <li><a href="httpdisabled://w.163.com/11/0405/17/70T3G1TD00314C3U.html">ħÊÞÄÇЩÄã²»ÖªµÀµÄÊÂ</a> <a target="_blank" href="httpdisabled://bbs.game.163.com/thread-167686243-1-1.html">ÔÙÑ¡Ò»´Î,Ä㻹»áÍæħÊÞÂð£¿</a></li>
+ <li><a href="httpdisabled://w.163.com/11/0406/14/70VAR2QQ00314C3U.html">È«ÇòÊ®¼ÑPVPÍæ¼ÒÅÅÃû¼°ÊÓƵ</a> <a target="_blank" href="httpdisabled://w.163.com/11/0407/10/711I56AN00314C3U.html">ÈýÖÖħÊÞPVPÍæ¼Ò·ÖÎö</a></li>
+ <li><a href="httpdisabled://w.163.com/11/0407/09/711F0UBC00314C3U.html">ħÊÞ½«¿ÉÔ¶³Ì²ÎÓ빫»áÁÄÌì</a> <a target="_blank" href="httpdisabled://w.163.com/11/0407/09/711E9RP700314C3U.html">̨·þÊ׸ö25¼¶¹«»áµ®Éú</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://w.163.com/special/patch-335/">WLK3.3.5</a>Ø­<a target="_blank" href="httpdisabled://w.163.com/special/patch-335/">ICC¸±±¾È«BOSS¹¥ÂÔÏê½â</a> | <a target="_blank" href="httpdisabled://w.163.com/special/talent/#s">Ì츳ģÄâÆ÷</a></li>
+ <li><a href="httpdisabled://w.163.com/special/ctm-cn/"><em class='fB'>´óÔÖ±ä</em></a>Ø­<a target="_blank" href="httpdisabled://w.163.com/10/1207/16/6NAJCKJN00314C3U.html">´óÔÖ±äÁ·¼¶Ö¸ÄÏ</a>Ø­<a target="_blank" href="httpdisabled://w.163.com/special/ctm-cn/">´óÔֱ丱±¾¹¥ÂÔ»ã×Ü</a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="area-sub">
+ <div id="moneySub" class="mod wgt-tab">
+ <h2 class="hd clearfix">
+ <span class="mod-title"><a href="httpdisabled://money.163.com/blog/">²Æ¾­×¨À¸</a></span>
+ </h2>
+ <div class="bd">
+ <ul class="mod-list sub-list dotline">
+ <li class="title"><a href="httpdisabled://gaunzhui01.blog.163.com/blog/static/11604175820113862511825/">³Â±¦´æ£º¶­·ªÈÇÁ˾åŲƸ»Õß</a></li>
+ <li><a href="httpdisabled://heyafu67.blog.163.com/blog/static/1073343422011378356242/">ºÎÑǸ££ººÚ»§²»ÈçÎÞ¹ú¼®ÈË</a></li>
+ <li><a href="httpdisabled://wavow.blog.163.com/blog/static/5322843201137104410534/">¶Ëºê±ó£ºµÍ̼ÊÇ´¿ºöÓÆ</a></li>
+ <li><a href="httpdisabled://xiaochongfan.blog.163.com/blog/static/13056609520113723936387/">·¶Ð¡³å£º×ʱ¾×ªÏò¶þÈýÏß³ÇÊÐ</a></li>
+ <li><a href="httpdisabled://sicmonan.blog.163.com/blog/static/137652998201137112349594/">ÕÅÜÔ骣º±ðÌ«Ö¸Íû»õ±ÒÕþ²ß</a></li>
+ </ul>
+ <ul class="mod-list sub-list">
+ <li><a href="httpdisabled://t.163.com/bashusong/status/-4468598182493823262#retweet">°ÍÊïËÉ£ºÅ·Ã˼ÓÏ¢Ã÷ÏÔÌáÇ°ÁË</a></li>
+ <li><a href="httpdisabled://t.163.com/zuoshixie/status/6936536041798423727#retweet">л×÷Ê«£ºÖйúË°¸³È«Çò×î¸ßÂð</a></li>
+ <li><a href="httpdisabled://t.163.com/dongfan/status/-6533247751173857004">¶­·ª£ºÌ¸ÂÛÇ®²¢²»ÒâζÃÄË×</a></li>
+ <li><a href="httpdisabled://blog.163.com/cctv_ds/blog/static/1751641752011382203889/">¶ÔÊÖ£º°Ù¶ÈÎÄ¿âÇÖȨÁËÂð</a></li>
+ <li><a href="httpdisabled://t.163.com/mahongtaotao/status/6745596757056097518#retweet">ÂíºéÌΣºéëÔᢶϲúÉú±©Àû</a></li>
+ </ul>
+ </div>
+ </div>
+<div class="gg gg-h180"><iframe src="../g.163.com/r@site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=logo190x180&amp;location=3.html" width="190" height="180" frameborder="0" border="0" marginwidth="0" marginheight="0" scrolling="no" ></iframe></div>
+ <div id="nie" class="mod">
+ <div class="hd clearfix">
+ <h2 class="mod-title"><a href="httpdisabled://nie.163.com/">ÍøÒ×ÓÎÏ·</a></h2>
+ <span class="mod-entry"><a href="httpdisabled://xyq.163.com/">ÃλÃ</a> <a href="httpdisabled://xy2.163.com/">´ó»°¢ò</a> <a href="httpdisabled://tx2.163.com/">ÌìÏ·¡</a></span>
+ </div>
+ <div class="bd">
+ <div class="mod-imgText imgText-temp-2 dotline clearfix">
+ <a href="httpdisabled://qn.163.com/"><img class="imgText-img" src="../qn.163.com/images/qnyh20110411.jpg" alt="¡¶Ù»Å®ÓĻ꡷" title="¡¶Ù»Å®ÓĻ꡷" height="70" width="70" /></a>
+ <h3 class="imgText-title"><a href="httpdisabled://qn.163.com/">¡¶Ù»Å®ÓĻ꡷</a></h3>
+ <p class="imgText-digest">¼´Ê±ÖÆÐþ»ÃÍøÓθïÐÂ<span class="cDRed"><a href="httpdisabled://qn.163.com/">[Ïêϸ]</a></span></p>
+ </div>
+ <ul class="mod-list sub-list">
+ <li class="title" ><a href="httpdisabled://nie.163.com/news/2011/4/7/440_234177.html" title="ÌìÏ·¡Áã½ç·¢²¼»áÖÜÈÕ¾ÙÐÐ">ÌìÏ·¡Áã½ç·¢²¼»áÖÜÈÕ¾ÙÐÐ</a></li>
+ <li ><a href="httpdisabled://qn.163.com/news/2011/3/31/8354_233837.html" title="ٻŮÓÄ»ê4ÔÂ22ÈÕ²»Ï޺ſª²â">ٻŮÓÄ»ê4ÔÂ22ÈÕ²»Ï޺ſª²â</a></li>
+ <li ><a href="httpdisabled://csxy.163.com/news/2011/3/30/8533_233741.html" title="´´ÊÀÎ÷ÓÎÇåÃ÷½Ú»î¶¯£º¼ÀÓ¢»ê">´´ÊÀÎ÷ÓÎÇåÃ÷½Ú»î¶¯£º¼ÀÓ¢»ê</a></li>
+ <li ><a href="httpdisabled://dt.163.com/2011/syzf/" title="´óÌƺÀÏÀ×ÊÁÏƬ¡¶Ë­ÓëÕù·æ¡·">´óÌƺÀÏÀ×ÊÁÏƬ¡¶Ë­ÓëÕù·æ¡·</a></li>
+ <li ><a href="httpdisabled://game.163.com/special/xy2/9years.html" title="¡¶´ó»°Î÷Ó΢ò¡·¾ÅÄê³É¹¦ÔËÓª">¡¶´ó»°Î÷Ó΢ò¡·¾ÅÄê³É¹¦ÔËÓª</a></li>
+ <li ><a href="httpdisabled://xy2.163.com/news/2011/3/16/1082_232975.html" title="²Ýľͬ´º--´ó»°2ÇåÃ÷»î¶¯">²Ýľͬ´º--´ó»°2ÇåÃ÷»î¶¯</a></li>
+ </ul>
+ <p class="entry">
+ <span class="cDRed">Íæ¼Ò£º</span><span class="cBlue"><a href="httpdisabled://hi.163.com/user/10037">ÀÏÍ·ÏÉ</a> | <a href="httpdisabled://hi.163.com/user/10015">ÄÝСÄÝ</a></span><br>
+ <span class="cDRed">ÂÛ̳£º</span><span class="cBlue"><a href="httpdisabled://zg.netease.com/">Õ½¹ú·çÔÆ</a> | <a href="httpdisabled://tx2.netease.com/">ÌìÏ·¡</a></span>
+ </p>
+ </div>
+ </div>
+ </div>
+</div>
+<!-- end -->
+<div class="area">
+ <div class="clearfix">
+ <div class="area-sub">
+ </div>
+ <div class="area-main">
+ <div class="main-col-9">
+ <SCRIPT LANGUAGE="JavaScript">var dovimecount=1;var sinamvadflag0688="";function domvview36413(){var mvimg = new Image();mvimg.src = 'mediav.gif';mvimg.onerror = (mvimg.onloaddisabled=(mvimg.onabort=function(){mvimg = null;}));}function view_mvst36413(){if(typeof(mediav_fini36413)!="number" && dovimecount <60){dovimecount++;setTimeout("view_mvst36413()",50);}else if(typeof(mediav_fini36413)!="number" && dovimecount >=60){sinamvadflag0688=1;domvview36413();document.getElementById('mvdiv_36413').innerHTML="<a href='http://click.mediav.com/c?type=2&db=mediav&pub=118_5273_36442&cus=6_221_7917_11779_11779000&url=http://www.masamaso.com/interface.php?id=103387&url=http://www.masamaso.com?from=r_wy_sy04ztlfdx' target='_blank'><img width='360' height='100' border='' alt='' src='../img1.126.net/channel5/360/360100_110318.jpg'></img></a>";}}view_mvst36413();</SCRIPT><div id="mvdiv_36413" name="mvdiv_36413"><SCRIPT LANGUAGE="JavaScript" src="../show.mediav.com/s@type=1&amp;db=mediav&amp;pub=118_2620_36413&amp;cus=0_0_0_0_0&amp;wh=360x100&amp;btype=1&amp;js=1.html"></SCRIPT></div>
+ </div>
+ </div>
+</div>
+<!-- trave & blog -->
+<div class="area">
+ <div class="area-main">
+ <div class="main-col-10">
+ <div id="blog" class="mod wgt-tab">
+ <div class="tab-hd tab-u-10 clearfix">
+ <span class="tab-u current"><a href="httpdisabled://blog.163.com/">²©¿Í</a></span>
+ <span class="tab-u"><a href="httpdisabled://blog.163.com/">²Ý¸ù</a></span>
+ <span class="tab-u"><a href="httpdisabled://blog.163.com/">¶Áͼ</a></span>
+ <span class="tab-u"><a href="httpdisabled://huodong.163.com/">»î¶¯</a></span>
+ </div>
+ <div class="bd tab-bd display-control">
+ <div class="tab-con current">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://blog.163.com/?blog"><img src="../img2.cache.netease.com/cnews/2011/4/8/20110408125931e0a79.jpg" alt="ŦԼºÃ¶àÓ¦ÕÙÅ®Àɱ»É±" title="ŦԼºÃ¶àÓ¦ÕÙÅ®Àɱ»É±" height="90" width="120" /><cite>ŦԼºÃ¶àÓ¦ÕÙÅ®Àɱ»É±</cite></a>
+ </div>
+ <h3 class="main-title"><a href="httpdisabled://zuoshixie.blog.163.com/blog/static/12074505220113885244290/?blog">ÈÃÎÒÃǺúÃÏíÊÜ°ËÔªµÄÆûÓÍ°É</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://chenjieren999.blog.163.com/blog/static/18309409020113811227728/?blog">ºÓ±±ºâË®ÖÐÔºÉæÏÓÔì¼Ù°ï¸»ºÀÐÝÆÞ</a></li>
+ <li><a href="httpdisabled://zhangxiaozhou-blog.blog.163.com/blog/static/128551850201137930521/?blog">±«²ªµÏÂ×ÊǸöÆßÊ®¶àËêµÄÄ°ÉúÄêÇáÈË</a></li>
+ <li><a href="httpdisabled://blog.163.com/xieyong_2011/blog/static/1829532932011377530336/?blog">ÕżÍÖеÄÐÞÑø»¹Ã»ÓÐÍêÈ«µ½¼Ò</a></li>
+ <li><a href="httpdisabled://blog.163.com/jinkaiping@yeah/blog/static/131435277201138920477/?blog">Ó¢¹úС»ïÂí¿Ë°ëÄê½á»é85´Î</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://whbsjwcwh.blog.163.com/blog/static/11268102011386431696/?blog">ÎÒÒ»±²×Ó¶¼×¬²»µ½4000Íò</a> <a target="_blank" href="httpdisabled://zhufangqing1968.blog.163.com/blog/static/15074972120113805221196/">"4000Íò½ÌÊÚ"´´°Ë´óÀøÖ¾¸ñÑÔ</a></li>
+ <li><a href="httpdisabled://zqguanjianbin.blog.163.com/blog/static/1356647582011385411860/?blog">¶í¹úÍƳö¡°ÐԸз´¸¯¹ÒÀú¡±</a> <a target="_blank" href="httpdisabled://lnsy024110.blog.163.com/blog/static/4139491201137111946665/">ʵÅÄÒÉËÆ´øǹµÄ¿àÐÐÉ®</a></li>
+ <li><a href="httpdisabled://gaoweiweiusa.blog.163.com/blog/static/1313159532011351918644/?blog">Öйú±ÈÃÀ¹úÂäºóÕûÕûÒ»°ÙÄꣿ</a> <a target="_blank" href="httpdisabled://blog.163.com/chenwan999@126/blog/static/1305594152011371129432/">Ä«Î÷¸çÈËÔÚÃÀ¹ú¹¤×ʺܵÍ</a></li>
+ <li><a href="httpdisabled://beimeigulang.blog.163.com/blog/static/17988801520113834854421/?blog">ɹɹÎÒµÄÉϴ̵¶²½Ç¹¼¯Èº(ͼ)</a> <a target="_blank" href="httpdisabled://huaqiaoliehurenjia.blog.163.com/blog/static/17935603520113724241721/">·ÖÏíÎÒÕä²ØµÄÐÔ¸ÐÑÌǹ(ͼ)</a></li>
+ <li><a href="httpdisabled://zzdyc.blog.163.com/blog/static/954980712011367181322/?blog">´´ÊÀÎ÷ÓΣº°×¾§¾§µÄÒ»Éú</a> <a target="_blank" href="httpdisabled://blog.163.com/special/0012rt/xyzw.html">дÕ÷ÎÄÓ®½±</a> <a target="_blank" href="httpdisabled://csxy.163.com/fab/">ÌåÑ顶´´ÊÀÎ÷ÓΡ·</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://feifei6338.blog.163.com/blog/static/16943665420113811108126/?caogen"><img src="../img2.cache.netease.com/cnews/2011/4/8/20110408144428d419d.jpg" alt="°³Ö»ÄÜ×øÔÚ×ÔÐгµÀï¿Þ" title="°³Ö»ÄÜ×øÔÚ×ÔÐгµÀï¿Þ" height="90" width="120" /><cite>°³Ö»ÄÜ×øÔÚ×ÔÐгµÀï¿Þ</cite></a>
+ </div>
+ <h3 class="main-title"><a href="httpdisabled://blog.163.com/xi_nan@yeah/blog/static/173764699201137931011/?caogen">Ó¢¹ú»ªÈËÉç»áµÄ»Ò°µÃæÁîÈË·¢Ö¸</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://blog.163.com/chenwan999@126/blog/static/1305594152011371129432/?caogen">ÔÚÃÀ¹ú´ò¹¤µÄÄ«Î÷¸ç¹¤È˹¤×ʺܵÍ</a></li>
+ <li><a href="httpdisabled://missfaye.fashion.blog.163.com/blog/static/10259226020113894838495/?caogen">70Äê´úµÄ¿íéÜñÓÖ»ØÀ´ÁË(ͼ)</a></li>
+ <li><a href="httpdisabled://blog.163.com/xiangqin_good@yeah/blog/static/11811133820113610331622/?caogen">µÂ¹úɽ³Ç£ºÊʺÏÂýÉú»îµÄµØ·½</a></li>
+ <li><a href="httpdisabled://crystal3178.blog.163.com/blog/static/14401067620113895714197/?caogen">¿ª¿Ú˵»°Ö®Ç°ÏȵÍÍ·¿´¿´ÄãµÄÉí·Ý</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://hnayhrh.blog.163.com/blog/static/4213388820113784932395/?caogen">´º¼¾ÒªºÈľ¹ÏÖíÌãÌÀ</a> <a target="_blank" href="httpdisabled://maimaidejianguo.blog.163.com/blog/static/126577749201137103515892/">ºÜ¼òµ¥µÄ»¨±ßÏ㳦ÅûÈø</a></li>
+ <li><a href="httpdisabled://ssmdsszx.blog.163.com/blog/static/166449346201137112632146/?caogen">ÊÇʲôԭÒòµ¼ÖÂ12ÐÇ×ùÀë»é</a> <a target="_blank" href="httpdisabled://okdj103.blog.163.com/blog/static/140239020113761226563/">±¾ÖÜÊ®¶þÐÇ×ùÔ˳̷ÖÎö</a></li>
+ <li><a href="httpdisabled://heiheirage.blog.163.com/blog/static/432978520113724629520/?caogen">ÕâÑùÒ»¸öÓÖÃÈÓÖÇ·µÄÅóÓÑÕæÎÞÄÎ</a> <a target="_blank" href="httpdisabled://dai1209datang.blog.163.com/blog/static/2223546520113695429358/">½²Ò»¸ö¾ÞÀäµÄÀäЦ»°</a></li>
+ <li><a href="httpdisabled://chengyuan68.blog.163.com/blog/static/1361893662011387038390/?caogen">°ÍÎ÷ÔÙСµÄµØ·½¶¼ÓÐÒ¹Éú»î</a> <a target="_blank" href="httpdisabled://ratpetty.blog.163.com/blog/static/6929691120113710417754/">×øС·É»ú±»»ú³¤À­×Ų»ÈöÊÖ</a></li>
+ <li><a href="httpdisabled://ldupaper.blog.163.com/blog/static/1329149852011321224510/?caogen">һλÊÔ¹ÜÓ¤¶ùÂèÂèµÄѪÀá»éÒöÊ·</a> <a target="_blank" href="httpdisabled://blog.163.com/jinkaiping@yeah/blog/static/13143527720113691836307/">Àϸ¾³¤ÊÙÖ»ÒòÒ»Éú²»×ö°®</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <ul class="mod-imgList imgList-w120 clearfix">
+ <li><a href="httpdisabled://picture007.blog.163.com/album/?saitu#m=2&aid=220179338&pid=6825388527"><img src="../img2.cache.netease.com/cnews/2011/4/8/2011040808393075049.jpg" alt="¿´ÃÀ¹úµÄÂäÆÇÆòؤȺ" title="¿´ÃÀ¹úµÄÂäÆÇÆòؤȺ" height="90" width="120" /></a><p><a href="httpdisabled://picture007.blog.163.com/album/?saitu#m=2&aid=220179338&pid=6825388527">¿´ÃÀ¹úµÄÂäÆÇÆòؤȺ</a></p></li>
+ <li><a href="httpdisabled://kenglhh.blog.163.com/album/?saitu#m=2&aid=221347549&pid=6874620546"><img src="../img1.cache.netease.com/cnews/2011/4/8/201104080847137e997.jpg" alt="Сº¢Ó붯ÎïºÏÅÄÐÐΪ" title="Сº¢Ó붯ÎïºÏÅÄÐÐΪ" height="90" width="120" /></a><p><a href="httpdisabled://kenglhh.blog.163.com/album/?saitu#m=2&aid=221347549&pid=6874620546">Сº¢Ó붯ÎïºÏÅÄÐÐΪ</a></p></li>
+ <li><a href="httpdisabled://blog.163.com/xiongy_l/album/?saitu#m=2&aid=221372151&pid=6875512485"><img src="../img1.cache.netease.com/cnews/2011/4/8/2011040808080199ae7.jpg" alt="а¶ñµÄ´´ÒâÉÌÒµ¹ã¸æ" title="а¶ñµÄ´´ÒâÉÌÒµ¹ã¸æ" height="90" width="120" /></a><p><a href="httpdisabled://blog.163.com/xiongy_l/album/?saitu#m=2&aid=221372151&pid=6875512485">а¶ñµÄ´´ÒâÉÌÒµ¹ã¸æ</a></p></li>
+ <li><a href="httpdisabled://picture007.blog.163.com/blog/static/182129446201137115819892/?saitu"><img src="../img2.cache.netease.com/cnews/2011/4/8/201104080828458908d.jpg" alt="ʵÅÄÈ⼦ÍÀÔ×¼Ó¹¤³§" title="ʵÅÄÈ⼦ÍÀÔ×¼Ó¹¤³§" height="90" width="120" /></a><p><a href="httpdisabled://picture007.blog.163.com/blog/static/182129446201137115819892/?saitu">ʵÅÄÈ⼦ÍÀÔ×¼Ó¹¤³§</a></p></li>
+ <li><a href="httpdisabled://image007.blog.163.com/album/?saitu#m=2&aid=221358852&pid=6875473251"><img src="../img1.cache.netease.com/cnews/2011/4/8/2011040811445023471.jpg" alt="´óѧÉúÉÏÑÝÄÚÒÂÅɶÔ" title="´óѧÉúÉÏÑÝÄÚÒÂÅɶÔ" height="90" width="120" /></a><p><a href="httpdisabled://image007.blog.163.com/album/?saitu#m=2&aid=221358852&pid=6875473251">´óѧÉúÉÏÑÝÄÚÒÂÅɶÔ</a></p></li>
+ <li><a href="httpdisabled://blog.163.com/xiongy_l/album/?saitu#m=2&aid=221344170&pid=6874340532"><img src="../img1.cache.netease.com/cnews/2011/4/8/20110408075741e084c.jpg" alt="ͼÊé¹ÝÄÇЩÀ×ÈË˯×Ë" title="ͼÊé¹ÝÄÇЩÀ×ÈË˯×Ë" height="90" width="120" /></a><p><a href="httpdisabled://blog.163.com/xiongy_l/album/?saitu#m=2&aid=221344170&pid=6874340532">ͼÊé¹ÝÄÇЩÀ×ÈË˯×Ë</a></p></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <a href="httpdisabled://yxp.163.com/h/action/scty5.html?sss=fromsyehddt&act=yxpty5_20110331_13?id_2010?huodong"><img src="../imgrc.ph.126.net/F4Oc-9fe_HYFRsSk0SRMmA==/4223532025543403580.jpg" alt="Ó¡ÏñÅÉ" title="Ó¡ÏñÅÉ" class="main-bn-blog" height="90" width="370" /></a>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://blog.163.com/ht/38?id_2020?huodong">¡¾Çé¸ÐÃÅÕï¡¿½â´ðÄãµÄÇé¸ÐÀ§ÈÅ</a></li>
+ <li><a href="httpdisabled://mhxx.dream.163.com?mail=mhxx_20110316_05?id_2021?huodong">ÃλÃÐÞÏÉзþ»ð±¬¿ªÆô</a></li>
+ <li><a href="httpdisabled://yxp.163.com/?sss=fromsyehdwz&act=yxpspring15_20110401_24?id_2022?huodong">¼Ç¼´ºÓκÃʱ¹â£¬ÁìÃâ·Ñ³åӡȯ</a></li>
+ <li><a href="httpdisabled://pmxj.wan.163.com/?02?id_2023?huodong">´©Ô½Ê±¿Õ¸°ÉñÖÝ£¬ÒÀ½£ºÀÒûնħͷ.</a></li>
+ <li><a href="httpdisabled://yxp.163.com/h/139.html?sss=frombkesywz&act=yxp139_20110324_08?id_2024?huodong">È«ÇòͨÓû§ËÙÀ´ÁìÈ¡9999·ÝÃâ·Ñ³åӡȯ£¡</a></li>
+ <li><a href="httpdisabled://fm.163.com/activities/fmzhcd/index.html?act=fmzhcd_20110314_03?id_2025?huodong">ÖÇÄÜ°æÉÁµçÓÊÀ´¸øÄãµÄÖǻ۳äµçÀ²£¬ÌåÑé¸üÓÐitouchËÍ£¡</a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="main-col-9">
+ <div id="hz" class="mod wgt-tab">
+ <h2 class="tab-hd tab-u-9 clearfix">
+ <span class="tab-u current"><a href="httpdisabled://blog.163.com/blogger.html">רÀ¸</a></span>
+ <span class="tab-u"><a href="httpdisabled://photo.163.com/pp/square/">ÉãÓ°</a></span>
+ <span class="tab-u"><a href="httpdisabled://idj.163.com/">ÔÚÏßÒôÀÖ</a></span>
+ </h2>
+ <div class="bd tab-bd display-control">
+ <div class="tab-con current">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://liuyongtw.blog.163.com/blog/static/18040812520113724310499/?zhuanlan"><img src="../img1.cache.netease.com/cnews/2011/4/8/201104080930016f866.jpg" alt="ÁõÜ­£ºÂíÓ¢¾Å¸øÎÒÅõ³¡" title="ÁõÜ­£ºÂíÓ¢¾Å¸øÎÒÅõ³¡" height="90" width="120" /><cite>ÁõÜ­£ºÂíÓ¢¾Å¸øÎÒÅõ³¡</cite></a>
+ </div>
+ <h3 class="main-title"><a href="httpdisabled://blog.163.com/tianye_1993/blog/static/561294622011368336805/?zhuanlan">ݼ³Ç×Ó£º¹«²Þ¸ÃÓÉË­Ìṩ£¿</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://alahaigui.blog.163.com/blog/static/2639938820113885754537/?zhuanlan">ºúÈÙÈÙ£º½ðӹС˵ÃÀÅ®Èý´ó´ú±í</a></li>
+ <li><a href="httpdisabled://cat898-com.blog.163.com/blog/static/12897521620113803224425/?zhuanlan">ÀèÃ÷£º´ÓÁÙÅèµ½ËÀºó¶¼±»ÇÃÖñ¸Ü</a></li>
+ <li><a href="httpdisabled://yetanblog.blog.163.com/blog/static/75669740201138093248/?zhuanlan">Ҷ̴£ºÃñ¼ä½ðÈÚÅÝÄ­½Ó½ü±ÀÀ£</a></li>
+ <li><a href="httpdisabled://sheng.dalin.blog.163.com/blog/static/127099945201138051951/?zhuanlan">Ê¢´óÁÖ£ºÎï¼Û¼à¹ÜË«Öرê×¼²»Í×</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://blog.163.com/xk_shijieguan/blog/static/13340475320113741028743/?zhuanlan">³ÂÑÔ£ºÈÕ±¾´Ä³ÇÏØÓæÃñµÄ¿àÈÕ×ÓÏÖÔڲŸոտªÊ¼</a></li>
+ <li><a href="httpdisabled://zhangjinlai0412.blog.163.com/blog/static/65913120201137101013443/?zhuanlan">ÁùСÁäͯ£ºÉîÉîÃ廳ÌìÖ®½¾×ÓÖܺ£Ó¤ÏÈÉú</a></li>
+ <li><a href="httpdisabled://haoyuehan.blog.163.com/blog/static/70742672011389054501/?zhuanlan">º«ºÆÔ£º±«²ª¡¤µÏÂ×ÈÃÈ˸Ð̾ҡ¹ö¾«Éñ¹ââÍòÕÉ</a></li>
+ <li><a href="httpdisabled://t.163.com/shishusi/status/1136017994388299708?zhuanlan">[»°Ìâ]ʯÊö˼£ºÄÐŮͬÁäÍËÐÝÖ»»á¼Ó¾ç½×²ãÁѺÛ</a></li>
+ <li><a href="httpdisabled://t.163.com/mikesakai/status/8789474217947845724#retweet?zhuanlan">[»°Ìâ]Çű¾Â¡Ôò£ºÈÕ±¾ÒѾ­½øÈëµØÕð»îԾʱÆÚ</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <ul class="mod-imgList imgList-w160 clearfix">
+ <li><a href="httpdisabled://photo.163.com/pp/square/?sheying"><img src="../imgrc.ph.126.net/chRhUK9Mxz9gdCzkEUzn5w==/4226346775310512150.jpg" alt="[ÍƼö] ±ðÕëµÄССÊÀ½ç" title="[ÍƼö] ±ðÕëµÄССÊÀ½ç" height="90" width="160" /></a><p><a href="httpdisabled://photo.163.com/pp/square/?sheying">[ÍƼö] ±ðÕëµÄССÊÀ½ç</a></p></li>
+ <li><a href="httpdisabled://photo.163.com/pp/zhuanti.html?sheying"><img src="../imgrc.ph.126.net/nSvNUs-5vbkySqbYp-lnLw==/4226628250287222807.jpg" alt="[רÌâ] ÅØÏø½ÌÖ÷ÄãÉ˲»Æð" title="[רÌâ] ÅØÏø½ÌÖ÷ÄãÉ˲»Æð" height="90" width="160" /></a><p><a href="httpdisabled://photo.163.com/pp/zhuanti.html?sheying">[רÌâ] ÅØÏø½ÌÖ÷ÄãÉ˲»Æð</a></p></li>
+ <li><a href="httpdisabled://photo.163.com/pp/star/?sheying"><img src="../imgrc.ph.126.net/VfPeCwJ6ufovjjY9ueyUxA==/4224939400426958880.jpg" alt="[ÈËÎï] Éú»î»ýµíËù²úÉúµÄÖ±¾õ" title="[ÈËÎï] Éú»î»ýµíËù²úÉúµÄÖ±¾õ" height="90" width="160" /></a><p><a href="httpdisabled://photo.163.com/pp/star/?sheying">[ÈËÎï] Éú»î»ýµíËù²úÉúµÄ</a></p></li>
+ <li><a href="httpdisabled://photo.163.com/pp/fotoshow/14003/?sheying"><img src="../imgrc.ph.126.net/40YCPhfL6uaLg3xA4ISWew==/4227754150194064440.jpg" alt="[Ó°Ïñ] ׯ±ä ¾µÍ·ÀïµÄʯ¼Òׯ" title="[Ó°Ïñ] ׯ±ä ¾µÍ·ÀïµÄʯ¼Òׯ" height="90" width="160" /></a><p><a href="httpdisabled://photo.163.com/pp/fotoshow/14003/?sheying">[Ó°Ïñ] ׯ±ä ¾µÍ·ÀïµÄʯ¼Ò</a></p></li>
+ </ul>
+ <div class="mod-keyword c-entry clearfix">
+ <h3 class="keyword-hd"><a href="httpdisabled://photo.163.com/upgrade/welcome/">¿ªÍ¨ÉãÓ°¿Õ¼ä£¬¼ÓÈëÍøÒ×ÉãÓ°ÉçÇø <span class="code-en">&raquo;</span></a></h3>
+ </div>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://idj.163.com/idj/album/42444"><img src="../img1.cache.netease.com/cnews/2011/4/8/2011040815090608fd9.jpg" alt="ÐíáÔ:ËÕ¸ñÀ­Ã»Óе×" title="ÐíáÔ:ËÕ¸ñÀ­Ã»Óе×" height="90" width="120" /><cite>ÐíáÔ:ËÕ¸ñÀ­Ã»Óе×</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://idj.163.com/idj/album.jsp?id=42443"><em class='fB'>²¼À¼ÄÝ¡¶Femme Fatale¡·</em></a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://idj.163.com/idj/album/42438">¼ÍÎÄÞ¥ÐÂר¼­¡¶»»ÈÕÏß¡·</a></li>
+ <li><a href="httpdisabled://idj.163.com/idj/album/42435">·¶çâç÷ÐÂר¼­¡¶Love&FanFan¡·</a></li>
+ <li><a href="httpdisabled://idj.163.com/idj/album/42428"><em class='cBlack'>ËïÑà×Ë×îÐÂר¼­¡¶ÊÇʱºò¡·</em></a></li>
+ <li><a href="httpdisabled://idj.163.com/idj/mv_love.jsp"><em class='cBlack'>רÌâ:ÄÇЩ¹ØÓÚ°®ÇéµÄÁ·Ï°Ìâ</em></a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://idj.163.com/idj/earth.jsp"><em class=' I_M_'>[иè] </em></a> <a target="_blank" href="httpdisabled://idj.163.com/idj/listen.jsp?type=music&id=200441">¶¼Ê²Ã´Ê±ºòÁË-ÕÅ»ÝÃÃ</a> <a target="_blank" href="httpdisabled://idj.163.com/idj/listen.jsp?type=music&id=200751"> ÎÒ¶¼¶®µÃ-Å®ÅóÓÑ</a></li>
+ <li><a href="httpdisabled://idj.163.com/idj/mv.jsp"><em class=' I_V_'>[MV]</em></a> <a target="_blank" href="httpdisabled://idj.163.com/idj/mvplay.jsp?vid=6245">ССÂìÒÏ-ÅËçâ°Ø</a> <a target="_blank" href="httpdisabled://idj.163.com/idj/mvplay.jsp?vid=6247"> ÓÞÈ˵Ĺú¶È-ËïÑà×Ë</a></li>
+ <li><a href="httpdisabled://idj.163.com/idj/group.jsp">[ÂÛ̳]</a> <a target="_blank" href="httpdisabled://idj.163.com/idj/group_theme_page.jsp?tid=729&gid=68">¶àÊ×½ðÇú±¬ÔÞ»ìÒômv´óÊײ¥</a></li>
+ <li><a href="httpdisabled://idj.163.com/idj/earth.jsp">[°ñµ¥]</a> |<a target="_blank" href="httpdisabled://idj.163.com/idj/earth_more.jsp?gdid=254">»ªÓï</a> |<a target="_blank" href="httpdisabled://idj.163.com/idj/earth_more.jsp?gdid=256">Å·ÃÀ</a> |<a target="_blank" href="httpdisabled://idj.163.com/idj/earth_more.jsp?gdid=255">ÈÕº«</a> |<a target="_blank" href="httpdisabled://idj.163.com/idj/earth_more.jsp?gdid=318">¶¯Âþ</a> |<a target="_blank" href="httpdisabled://idj.163.com/idj/earth.jsp">¸ü¶à</a></li>
+ <li><a href="httpdisabled://idj.163.com/idj/special_index.jsp">[×ÔÑ¡¼¯]</a> <a target="_blank" href="httpdisabled://idj.163.com/idj/special.jsp?uid=81&tid=2614">˼Éú»î-ÐÂÏÊD</a> <a target="_blank" href="httpdisabled://idj.163.com/idj/special.jsp?uid=232443&tid=8750"> ¸ø×îºÃµÄʱ¹â</a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="area-sub">
+</div>
+<!-- end -->
+<!-- UGC -->
+<div class="area">
+ <div class="area-main">
+ <div class="main-col-10">
+ <div id="market" class="mod wgt-tab">
+ <h2 class="tab-hd tab-u-10 clearfix">
+ <span class="tab-u current"><a href="httpdisabled://travel.163.com/">ÂÃÓÎ</a>¡¤<a href="httpdisabled://edu.163.com/">½ÌÓý</a></span>
+ <span class="tab-u"><a href="httpdisabled://travel.163.com/">¹úÄÚ¡¤³ö¾³</a></span>
+ <span class="tab-u"><a href="httpdisabled://edu.163.com/">¿¼ÊÔ¡¤Áôѧ</a></span>
+ <span class="tab-u"><a href="httpdisabled://baby.163.com/">Ç××Ó</a></span>
+ </h2>
+ <div class="bd tab-bd display-control">
+ <div class="tab-con current">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://daxue.163.com/11/0406/12/70V46VIP00913JC5.html"><img src="../img3.cache.netease.com/edu/2011/4/6/20110406220601277f0.jpg" alt="²»ÎªÈËÖªµÄ&quot;×î&quot;×Ö¸ßУ" title="²»ÎªÈËÖªµÄ&quot;×î&quot;×Ö¸ßУ" height="90" width="120" /><cite>²»ÎªÈËÖªµÄ"×î"×Ö¸ßУ</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://edu.163.com/">ѧÉú40ËêûÓÐ4000Íò ÎÞÑÕ¼ûµ¼Ê¦</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://edu.163.com/liuxue">½ÌÓý²¿¹«²¼×îÐÂÅú×¼¾³Íâ¸ßУÃûµ¥</a></li>
+ <li><a href="httpdisabled://edu.163.com/11/0407/19/712EDPKR00294IJ5.html">̨¸ßУ´ó½ÕÐ2141ÈË Ë½Á¢Ñ§·Ñ2Íò</a></li>
+ <li><a href="httpdisabled://edu.163.com/11/0408/13/714E66Q000294IJK.html">°µ·ÃÄÚÄ»£º¹«¿¼ÅàѵʮÌìѧ·Ñ¹ýÍò</a></li>
+ <li><a href="httpdisabled://edu.163.com/11/0408/10/7144BG1D00294IJH.html">×éͼ£º"±±Æ¯"½ÖÎèÄÐÌÆÏþº½µÄÒ»Ìì</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://travel.163.com/11/0406/11/70V1TCLP00063KE8.html">4Ô¹úÄÚÈÈÃÅÄ¿µÄµØÍƼö</a> <a target="_blank" href="httpdisabled://travel.163.com/photoview/4CHV0006/1465.html#p=708NVJDI4CHV0006">Çã¼Òµ´²úµÄÓ¡¶ÈÉÝ»ª»éÀñ(ͼ)</a></li>
+ <li><a href="httpdisabled://travel.163.com/11/0406/15/70VG176200063JSA.html">±±¾©»¨ÆÚ¸÷´óÉÍ»¨µØÍƼö(ͼ)</a> <a target="_blank" href="httpdisabled://travel.163.com/photoview/4DB90006/1487.html#p=70VS0IQS4DB90006">³©ÓÎÑÇÂíÑ·"¼ÓÀÕ±Èɳ̲"</a></li>
+ <li><a href="httpdisabled://travel.163.com/11/0406/10/70UV76E400063KE8.html">¸æ±ð"ʣŮ"6´óÂÃÓÎÊ¥µØ</a> <a target="_blank" href="httpdisabled://travel.163.com/hellocity/">³É¶¼º¼ÖÝÕù¶áÐÝÏгÇÊÐÈËÆøÍõ</a></li>
+ <li><a href="httpdisabled://travel.163.com/11/0406/11/70V0H2H600063IO0.html">È«Çò29´ó"ÄÐÈËÌìÌÃ"</a> <a target="_blank" href="httpdisabled://travel.163.com/11/0406/19/70VTRF6F00063KE8.html">·ÇÖÞÇÀ»é:¿´¼ûƯÁÁÅ®È˾ÍÇÀ(ͼ)</a></li>
+ <li><a href="httpdisabled://travel.163.com/photoview/17KK0006/1486.html#p=70VT2TKA17KK0006">ÁîÈËÕ¦ÉàµÄÄá²´¶û¿àÐÐÉ®(ͼ)</a> <a target="_blank" href="httpdisabled://travel.163.com/11/0406/16/70VIHAFQ00063KE8.html">̽Ãص¹úµÄºÏ·¨¼ËÔº(ͼ)</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://travel.163.com/photoview/17KK0006/1491.html#p=711HLS7217KK0006"><img src="../img3.cache.netease.com/travel/2011/4/7/2011040719553034b7b.jpg" alt="µØÇòÉÏ×îÏñÍâÐǵĵط½" title="µØÇòÉÏ×îÏñÍâÐǵĵط½" height="90" width="120" /><cite>µØÇòÉÏ×îÏñÍâÐǵĵط½</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://travel.163.com/11/0407/11/711J068700063KE8.html">×îÊÜÍâ¹úÈËϲ»¶µÄÖйú10´ó¾°µã</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://travel.163.com/11/0407/15/7122IB4T00063IO0.html">ÖйúÊ®´ó¡°ÀäÄ®¡±³ÇÊÐÅÅÐаñ(ͼ)</a></li>
+ <li><a href="httpdisabled://travel.163.com/photoview/17KK0006/1489.html#p=711EUAOJ17KK0006">¾ø¶ÔÁîÈ˾ªÑ޵ķۺìɫɳ̲(ͼ)</a></li>
+ <li><a href="httpdisabled://travel.163.com/11/0407/13/711SQ0DJ00063KE8.html">ÁíÀà·þÎñ ÃÀÅ®Õдý´©»¤Ê¿×°ÓÃÕëͲ</a></li>
+ <li><a href="httpdisabled://travel.163.com/11/0407/10/711GEUNS00063KE8.html">Å®ÐÔÎðÈ룡ֻÓÐÄÐÈ˵ÄÏ£À°Ð¡µº(ͼ)</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://travel.163.com/11/0407/12/711NJJHP00063KE8.html">ÎåÒ»³ö¾³ÓμÛÕÇÁ½³É ÀËÂþº£µºÈËÆø×î¸ß</a> <a target="_blank" href="httpdisabled://travel.163.com/11/0407/13/711SF6PT00063KE8.html">´ºÈÕµÇɽȫÍƼö</a></li>
+ <li><a href="httpdisabled://travel.163.com/11/0407/12/711NC3I300063KE8.html">ÊÀ½ç10´óµØÀíÆæ¹ÛÁîÈ˾ªÌ¾</a> <a target="_blank" href="httpdisabled://travel.163.com/11/0407/15/71214JK800063IO0.html">È«Çò×îÃÀÀ뵺³©Óι¥ÂÔ(ͼ)</a></li>
+ <li><a href="httpdisabled://travel.163.com/11/0407/12/711NC28S00063KE8.html">´ºÈÕÃÀʳÓÕ»ó Æ·½­ÄÏС×ʲÍÌü</a> <a target="_blank" href="httpdisabled://travel.163.com/11/0407/13/711SAO9700063KE8.html">ÂíÀ´Î÷ÑÇÒìÓò·çÇéÓι¥ÂÔ</a></li>
+ <li><a href="httpdisabled://travel.163.com/11/0407/10/711HN0GS00063KE8.html">Öйú×îÃÀÎåºþʮɽ</a> <a target="_blank" href="httpdisabled://travel.163.com/11/0407/11/711JQGAM00063KE8.html">ÎåÒ»ÉÍ»¨µØͼ</a> <a target="_blank" href="httpdisabled://travel.163.com/11/0407/14/711TDGE400063KE8.html">ɽÎ÷ƽ˳Ç×Éí¹¥ÂÔ(ͼ)</a></li>
+ <li><a href="httpdisabled://travel.163.com/11/0406/17/70VM1JO800063IBJ.html">´ºÈÕÁµÉϵ¥³µÂÃÐÐ(ͼ)</a> <a target="_blank" href="httpdisabled://travel.163.com/11/0406/11/70V0RMIB00063KE8.html">2011Ó¢Â×Çà´ºÓÎ 29¼þ±Ø×öÖ®ÊÂ</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://daxue.163.com/11/0407/18/712BAVEJ00913JC5.html"><img src="../img3.cache.netease.com/edu/2011/4/9/20110409001451f646c.jpg" alt="´óѧÉúÂ鶹ÈÕ׬Êý°ÙÔª" title="´óѧÉúÂ鶹ÈÕ׬Êý°ÙÔª" height="90" width="120" /><cite>´óѧÉúÂ鶹ÈÕ׬Êý°ÙÔª</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://edu.163.com/11/0408/10/7143IOQU00293L7F.html">ÎÄÃ÷¶½²é¶Ó½ûѧÉú"¹«¹²³¡ËùÇ×ÈÈ"</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://edu.163.com/special/kaoyan2011/">¿¼Ñе÷¼ÁÕýÔÚ½øÐÐ ×¥½ôÍøÉϱ¨Ãû</a></li>
+ <li><a href="httpdisabled://edu.163.com/11/0407/18/712DJDH400294IJ5.html">±±¾©¸ß¿¼¼Ó·Ö·¶Î§ 5Äê¡°ÊÝÉí¡±5´¦</a></li>
+ <li><a href="httpdisabled://daxue.163.com/11/0407/14/711VTQ7600913JC5.html">24¸ö×îÈÝÒ×±»Îó½âµÄ¸ß¿¼×¨Òµ(ͼ)</a></li>
+ <li><a href="httpdisabled://edu.163.com/11/0407/17/7129RGOE00294IPN.html">¼ÓÄôóÑô¹âÃûµ¥ ¹«ÎñÔ±Èë¸ßнÐÐÒµ</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://t.163.com/mylovelydog/status/3130838611543801875#retweet">[΢²©]"³©Ñ§"°ÄÖÞ£º°ÄÖ޵ĴóѧÔÊÐí¡°ÄԲС±´æÔÚ</a></li>
+ <li><a href="httpdisabled://daxue.163.com/11/0408/16/714OLNEO00913KMA.html">[¾ÍÒµ]´óÈýѧÉúѧУʳÌÿª·¹ÆÌ ¿ªÒµÁ½Ìì½øÕË4000Ôª</a></li>
+ <li><a href="httpdisabled://kids.163.com/11/0408/11/7145E9RC002942C4.html">[ÇàÉÙ]µ÷²é£ºÖйú¶ÀÉú×ÓÅ®ÓÐ×ÔÁµÇãÏò Íâ±í¾ö¶¨×Ô×ð</a></li>
+ <li><a href="httpdisabled://edu.163.com/11/0406/17/70VNK51500294IIH.html">[Áôѧ]²»¿´²»ÖªµÀ£ºÖйúÁôѧÉú×î°®Ôú¶ÑÊ®´ó¹ú¼Ò</a></li>
+ <li><a href="httpdisabled://daxue.163.com/photoview/0HQO0091/198.html#p=71499BOF0HQO0091">[×éͼ]ÄêÇáÈ˵Ä"ÁíÀà¼õѹ"·½Ê½£ºÆ¯¸¡¡¢»³¾É¡¢±§±§×å</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://baby.163.com/special/bbc110408/"><img src="../img4.cache.netease.com/lady/2011/4/7/2011040711484089cba.jpg" alt="͵Åij¬ÊÐÀïµÄСÃÈÅ®" title="͵Åij¬ÊÐÀïµÄСÃÈÅ®" height="90" width="120" /><cite>͵Åij¬ÊÐÀïµÄСÃÈÅ®</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://baby.163.com/special/wennibaqu/">ÂèÂ裬ΪʲôÄãºÍ°Ö°ÖÍíÉÏÓйÖÉù</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://baby.163.com/11/0407/14/7120CI3V00262I29.html">¼ÙÈ红×ÓÓöµ½ÐÔÉ˺¦ ÄãÒªÔõô°ì</a></li>
+ <li><a href="httpdisabled://baby.163.com/11/0407/15/71239FNK00262HRQ.html">ÔõÑùÈÃ80ºóС°Éú¡±´úÄÌË®³ä×㣿</a></li>
+ <li><a href="httpdisabled://baby.163.com/11/0407/15/7121GN8400262HRG.html">Äк¢Ö®¼äÖÇÁ¦²î¾à´ó Å®º¢Ïà¶Ôƽ¾ù</a></li>
+ <li><a href="httpdisabled://baby.163.com/11/0406/14/70VCDM6F00262USS.html">´ó³ß¶ÈÐÔ½ÌÓý»æ±¾¡¶ÎÒ´ÓÄÄÀïÀ´¡·</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://bbs.lady.163.com/list/xybb.html">[ÂÛ̳]</a> <a target="_blank" href="httpdisabled://bbs.lady.163.com/bbs/xybb/205510146.html">´ó¼Ò°ïæ¿´¿´ ÕâÇé¿öÊÇÀ´Ô¾­»¹ÊÇÁ÷²ú</a></li>
+ <li><a href="httpdisabled://bbs.lady.163.com/list/hyrj.html">[ÂÛ̳]</a> <a target="_blank" href="httpdisabled://bbs.lady.163.com/bbs/yunzhou18/205253191.html">ÊÖµçͲ̥½Ì ÊÖµçͲ½ÐBB¡°Æ𴲡±Ëû»ØÓ¦</a></li>
+ <li><a href="httpdisabled://bbs.lady.163.com/list/mmbb.html">[ÂÛ̳]</a> <a target="_blank" href="httpdisabled://bbs.lady.163.com/bbs/mmbb/205499080.html">¿´µ½ÎҹصçÄÔ ±¦±¦¾¹À´¾ä£º±¦±¦ºÜÂúÒâ</a></li>
+ <li><a href="httpdisabled://t.163.com/baby163">[΢²©]</a> <a target="_blank" href="httpdisabled://t.163.com/zhile365/status/3418988806356097742#retweet">³£ËµÐ»Ð»µÄº¢×ÓÇéÉ̸ü¸ß ¸üÀÖÓÚÖúÈË</a></li>
+ <li><a href="httpdisabled://baby.163.com/special/bbc11003/"><em class='fB'>3Ô·âÃ汦±¦ÈËÆøͶƱ</em></a> | <a target="_blank" href="httpdisabled://baby.163.com/special/wangchaoge/"><em class='fB'>¹«ÒæÃ÷ÐÇÂèÂ裺Íõ³±¸è</em></a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="main-col-9">
+ <div id="bbs" class="mod wgt-tab">
+ <h2 class="tab-hd tab-u-9 clearfix">
+ <span class="tab-u current"><a href="httpdisabled://bbs.163.com/">ÂÛ̳</a></span>
+ <span class="tab-u"><a href="httpdisabled://bbs.163.com/photo/">ɹͼ</a></span>
+ <span class="mod-entry"><a href="httpdisabled://bbs.163.com/rank/">·çÔÆ°ñ</a></span>
+ </h2>
+ <div class="bd tab-bd display-control">
+ <div class="tab-con current">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://bbs.163.com/"><img src="../img2.cache.netease.com/cnews/2011/4/8/201104080728304dcb2.jpg" alt="ÍøÆØÉîÛÚÇéÉ«Ò¹µêÕÕƬ" title="ÍøÆØÉîÛÚÇéÉ«Ò¹µêÕÕƬ" height="90" width="120" /><cite>ÍøÆØÉîÛÚÇéÉ«Ò¹µêÕÕƬ</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://bbs.news.163.com/photoview/0HMM0015/5041.html">¡¡ÃÀ¹ú´óѧÄÐÅ®¼¤ÇéÄÚÒÂÅɶÔ</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://bbs.news.163.com/bbs/photo/205486243.html">Ò˱öÏØȺÄÐŹ´ò¸¾Å®¼à¿ØÆعâ</a></li>
+ <li><a href="httpdisabled://bbs.news.163.com/bbs/pp/205532782.html">ÆæÎÅ£¡Ô­À´´óÊ÷»¹ÐèÒª´òµõÕë</a></li>
+ <li><a href="httpdisabled://bbs.gz.house.163.com/bbs/housegossip/205313202.html">ÅóÓѸÕ×°ÐÞÍêµÄмҽá¹û±­¾ßÁË</a></li>
+ <li><a href="httpdisabled://bbs.news.163.com/photoview/0HMM0015/5044.html">ó¯òëÂèÂèÉú±¦±¦¹ý³Ì ¶ñÐÄÉ÷Èë</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://bbs.163.com/photo/">[Éç»á]</a> <a target="_blank" href="httpdisabled://bbs.gz.house.163.com/photoview/2ALD0015/5037.html">"×îţ¥·¿"ѹÇÅÏÂ15Äê</a> <a target="_blank" href="httpdisabled://bbs.news.163.com/photoview/0HMM0015/5047.html">¡¡ÍµÅĽÖÍ·´óµ¨ÃÍÅ®</a></li>
+ <li><a href="httpdisabled://club.auto.163.com/">[Æû³µ]</a> <a target="_blank" href="httpdisabled://club.auto.163.com/bbs/auto_bbtx/205555685.html">Å££¡¹«Îñ³µ¾ÍÃâ·ÑÍ£</a> <a target="_blank" href="httpdisabled://club.auto.163.com/bbs/auto_01m0/205427558.html">¡¡ÓͼÛÉÏÕǺó×îNB³µÌù</a></li>
+ <li><a href="httpdisabled://bbs.home.163.com/list/homeshow.html">[¼Ò¾Ó]</a> <a target="_blank" href="httpdisabled://bbs.home.163.com/bbs/sheji/205326250.html">9Íò´òÔìÏç´åʽ±ðÊû</a> <a target="_blank" href="httpdisabled://bbs.home.163.com/bbs/toushu/205484672.html">¡¡¶¬Ìì¸Õ¹ýÂíÍ°¾ÍÁÑÁË</a></li>
+ <li><a href="httpdisabled://bbs.163.com/photo/">[ͼ¿â]</a> <a target="_blank" href="httpdisabled://bbs.news.163.com/photoview/0HMM0015/5031.html">ÈÃÃÀÅ®Õ𾫵ÄÏû»êÒÕÊõ</a> <a target="_blank" href="httpdisabled://bbs.news.163.com/photoview/0HMM0015/5020.html">¡¡Å·ÖÞ¼ËÔºÃâ·Ñ·þÎñ</a></li>
+ <li><a href="httpdisabled://t.163.com/">[΢²©]</a> <a target="_blank" href="httpdisabled://t.163.com/zt/bbs/date">´ïÈ˽ÌÄãÀËÂþÔ¼»á¾øÕÐ</a> <a target="_blank" href="httpdisabled://t.163.com/neigehua/status/-5574834336174310123">¡¡ºº¼é×î¶àµÄµØÓò</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <ul class="mod-imgList imgList-w160-2 clearfix">
+ <li><a href="httpdisabled://bbs.news.163.com/photoview/0HMM0015/5047.html"><img src="../img1.cache.netease.com/cnews/2011/4/8/201104080835397174e.jpg" alt="½Ö±ßżÓö´©×Å´óµ¨ÃÍÅ®" title="½Ö±ßżÓö´©×Å´óµ¨ÃÍÅ®" height="90" width="160" /></a><p><a href="httpdisabled://bbs.news.163.com/photoview/0HMM0015/5047.html">½Ö±ßżÓö´©×Å´óµ¨ÃÍÅ®</a></p></li>
+ <li><a href="httpdisabled://bbs.news.163.com/photoview/0HMM0015/5043.html"><img src="../img2.cache.netease.com/cnews/2011/4/8/20110408082635b6897.jpg" alt="ÍøÆØÉîÛÚÇéÉ«Ò¹µêÕÕƬ" title="ÍøÆØÉîÛÚÇéÉ«Ò¹µêÕÕƬ" height="90" width="160" /></a><p><a href="httpdisabled://bbs.news.163.com/photoview/0HMM0015/5043.html">ÍøÆØÉîÛÚÇéÉ«Ò¹µêÕÕƬ</a></p></li>
+ <li><a href="httpdisabled://bbs.news.163.com/photoview/0HMM0015/5044.html"><img src="../img1.cache.netease.com/cnews/2011/4/8/20110408092834ed61d.jpg" alt="ó¯òëÂèÂèÉú±¦±¦È«¹ý³Ì" title="ó¯òëÂèÂèÉú±¦±¦È«¹ý³Ì" height="90" width="160" /></a><p><a href="httpdisabled://bbs.news.163.com/photoview/0HMM0015/5044.html">ó¯òëÂèÂèÉú±¦±¦È«¹ý³Ì</a></p></li>
+ <li><a href="httpdisabled://bbs.news.163.com/photoview/0HMM0015/5031.html"><img src="../img2.cache.netease.com/cnews/2011/4/8/2011040809433960d68.jpg" alt="µØÌúÀïÕæÊÇÈ˲ű²³ö" title="µØÌúÀïÕæÊÇÈ˲ű²³ö" height="90" width="160" /></a><p><a href="httpdisabled://bbs.news.163.com/photoview/0HMM0015/5031.html">µØÌúÀïÕæÊÇÈ˲ű²³ö</a></p></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="area-sub">
+ <div id="recommend" class="mod">
+ <div class="hd clearfix">
+ <h2 class="mod-title"><a href="httpdisabled://mall.163.com/">ÍøÒ×É̳Ç</a></h2>
+<span class="mod-entry"><a href="httpdisabled://caipiao.163.com/">²ÊƱ</a> | <a href="httpdisabled://tuan.163.com/">ÍŹº</a></span>
+ </div>
+ <div class="bd" id="neteasy_mall">
+
+ <h3 class="neteasy_mall_title">»°·Ñ³äÖµ</h3>
+ <form action="httpdisabled://shop.163.com/mobile/tofill.html" target="_blank" id="neteasy_mall_form" method="post">
+ ÊÖ»úºÅÂ룺<input type="text" class="neteasy_mall_phone" id="mall_phone" maxlength="13" onpaste="return false" autocomplete="off" name="chargeAccount"/><span id="phone_err" style="display:none"></span><br />
+ ¡¡¡¡ÃæÖµ£º<select class="neteasy_mall_value" id="mall_value" name="faceValue">
+ <option value="30">30Ôª</option>
+ <option value="50">50Ôª</option>
+ <option value="100" selected>100Ôª</option>
+ <option value="300">300Ôª</option>
+ <option value="500">500Ôª</option>
+ </select><input type="submit" value="" class="neteasy_mall_submit"/>
+ <br />
+ ¡¡¡¡<span class="mall_price">¼Û¸ñ£º<strong class="cDRed" id="mall_money">98.7-99.5</strong>Ôª</span>
+ </form>
+ <div class="dotline"></div>
+ <objectdisabled classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="httpdisabled://downloaddisabled.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=7,0,19,0" width="170" height="67">
+ <param name="movie" value="httpdisabled://img1.126.net/channel1/rollGame0107.swf">
+ <param name="quality" value="high">
+ <param name="wmode" value="transparent" />
+ <embeddisabled src="../img1.126.net/channel1/rollGame0107.swf" wmode="transparent" quality="high" pluginspage="httpdisabled://www.macromedia.com/go/getflashplayer" type="application/x-shockwave-flash" width="170" height="67"></embed>
+ </object>
+ <a href="httpdisabled://tuan.163.com/?tag=%E7%BE%8E%E9%A3%9F"><img class="mall_img" src="../img3.126.net/techpro/tuangou/20110218/170-80.jpg" width="170" height="80" alt="ÍøÒ×Íų¤" title="ÍøÒ×Íų¤" /></a>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+<!-- end -->
+<!-- lady & bbs -->
+<div class="area">
+ <div class="area-main">
+ <div class="main-col-10">
+<div class="gg gg-h100"><iframe src="../g.163.com/r@site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=column390x100&amp;location=5.html" width="390" height="100" frameborder="0" border="0" marginwidth="0" marginheight="0" scrolling="no" ></iframe>
+</div>
+ <div id="lady" class="mod wgt-tab">
+ <h2 class="tab-hd tab-u-10 clearfix">
+ <span class="tab-u current"><a href="httpdisabled://lady.163.com/">Å®ÈË</a></span>
+ <span class="tab-u"><a href="httpdisabled://fashion.163.com/">ʱÉÐ</a></span>
+ <span class="tab-u"><a href="httpdisabled://lady.163.com/sense/">Çé¸Ð</a></span>
+ <span class="tab-u"><a href="httpdisabled://lady.163.com/beauty/">ÃÀÈÝ</a></span>
+ </h2>
+ <div class="bd tab-bd display-control">
+ <div class="tab-con current">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://lady.163.com/photoview/4CJ80026/10343.html?1302099128#p=70TGMIA34CJ80026"><img src="../img4.cache.netease.com/lady/2011/4/8/20110408224817711dd.jpg" alt="Å®ÐǶ±³×°Æ´Ë­×î÷È»ó" title="Å®ÐǶ±³×°Æ´Ë­×î÷È»ó" height="90" width="120" /><cite>Å®ÐǶ±³×°Æ´Ë­×î÷È»ó</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://lady.163.com/">ÏëµÃµ½ÄÐÈ˵ÄÐÄ ÏÈÕ÷·þËûµÄθ£¿</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://lady.163.com/11/0331/00/70ECI3L600264IJ9.html">´óSÁõ¼ÎÁáʾ·¶ÓÅÑÅÈËÆÞÇÉ×±°ç</a></li>
+ <li><a href="httpdisabled://lady.163.com/11/0328/19/708MHB0T00264IJ2.html">ÁÖÎõÀÙÉÁ»é ÀËÂþ·¢ÐÍÓÅÑÅÓָ߹ó</a></li>
+ <li><a href="httpdisabled://lady.163.com/11/0329/17/70B2TS0B00264IJ2.html">ÑîÃÝÁÁÏàʱÉÐÒ¹ ºìȹ¾í·¢³¬ÇÀ¾µ</a></li>
+ <li><a href="httpdisabled://fashion.163.com/11/0327/18/7063MKU900264J94.html">·¶±ù±ùÊæä¿ Å̵ãÅ®ÐǺì̺³ó̬˲¼ä</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://bbs.lady.163.com/list/lovestory.html">[ÇéÁ÷¸Ð]</a> <a target="_blank" href="httpdisabled://lady.163.com/11/0328/11/707SA0IE00264IIU.html">ºÍÀϹ«AAÖÆ·òÆÞ ¸øËûÁ½Ç§°®Ò»»Ø</a></li>
+ <li><a href="httpdisabled://bbs.lady.163.com/list/danshenmm.html">[µ¥Å®]</a> <a target="_blank" href="httpdisabled://bbs.lady.163.com/bbs/danshenmm/204638575.html">25Ëê¹Ô¹ÔÅ®Õ÷»é~Äã¸ÒÈ¢ÎÒ¸Ò¼Þ£¡</a></li>
+ <li><a href="httpdisabled://bbs.lady.163.com/list/beautify.html">[»¨ÎÑ]</a> <a target="_blank" href="httpdisabled://bbs.lady.163.com/bbs/beautify/205351998.html">´ïÈËСĢ¹½½ÌÄã´òÔìÃÔÈ˵çÑÛ×±</a></li>
+ <li><a href="httpdisabled://bbs.lady.163.com/list/poxi.html">[ÆÅϱ]</a> <a target="_blank" href="httpdisabled://bbs.lady.163.com/bbs/poxi/205219505.html">ÀϹ«ÀäÂäÎÒ ËµÅÂÆÅÆÅÐÄÀïÄÑÊÜ </a></li>
+ <li><a href="httpdisabled://bbs.lady.163.com/list/fashion.html">[ʱÉÐ]</a> <a target="_blank" href="httpdisabled://bbs.lady.163.com/bbs/fashion/205114172.html">ÿÈÕÒ»´îÅä Ñý¾«Ð¡½ã±¾ÃüÄêÃÀÀö´î </a></li>
+ <li><a href="httpdisabled://bbs.lady.163.com/list/lovestory.html">[ÇéÁ÷¸Ð]</a> <a target="_blank" href="httpdisabled://bbs.lady.163.com/bbs/danshenmm/204638575.html">ÄÐÈËÀ뿪ÐÔ°®¾Í»î²»ÏÂÈ¥Âð£¿</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://lady.163.com"><em class='fB'>´óƬÐ㳡</em></a> |<a target="_blank" href="httpdisabled://lady.163.com/photoview/4CJ80026/10239.html?1301902571">·¶±ù±ùµÇ·¨¹úÔÓÖ¾·âÃæ ÃÀÈ˵ãðë±ðÑùåüÃÄ</a></li>
+ <li><a href="httpdisabled://lady.163.com/astro/"><em class='fB'>ÍøÒ×ÐÇ×ù</em></a> |<a target="_blank" href="httpdisabled://lady.163.com/special/sense/siyueyunshi.html">2011ËÄÔÂÐÇÔ˴󼯺Ï</a><a target="_blank" href="httpdisabled://lady.163.com/11/0406/00/70TR7PTK00264III.html">Å®Î×µêÐÇ×ù±¾ÖÜÔËÊÆ</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://fashion.163.com/photoview/43AJ0026/10398.html#p=711THJPG43AJ0026"><img src="../img4.cache.netease.com/lady/2011/4/8/20110408014720d3fc0.jpg" alt="͸ÊÓïοձ¡É´ÊÓ¾õÊ¢Ñç" title="͸ÊÓïοձ¡É´ÊÓ¾õÊ¢Ñç" height="90" width="120" /><cite>͸ÊÓïοձ¡É´ÊÓ¾õÊ¢Ñç</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://fashion.163.com/">¿­ÌØĪ˹ÓëÕæÉß ÃÀŮҰÊÞ´´áÛ·å</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://fashion.163.com/photoview/4GJG0026/10400.html#p=711UCHGK4GJG0026">Ó¡»¨É«²Ê´ó±¬Õ¨ ½ÖÅij±ÈËÑÝÒﴺװ</a></li>
+ <li><a href="httpdisabled://fashion.163.com/11/0407/14/711VR9L500264K9M.html">СS¿ñ°® ¸ÒºÍºìµ×Ь½Ð°åµÄŮЬ</a></li>
+ <li><a href="httpdisabled://fashion.163.com/photoview/43AJ0026/10405.html#p=71259T7P43AJ0026">ÓÐÁõÏè ÌåÓýÃ÷ÐÇЯ¼Ò¾ìÂãÉíÅÄƬ</a></li>
+ <li><a href="httpdisabled://fashion.163.com/11/0407/15/7122USDA00264J94.html">BalmainÉè¼ÆʦƤ°£¶û¡¤°Í¶ûÂüÀëÈÎ</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://fashion.163.com/">[´óƬ]</a> <a target="_blank" href="httpdisabled://fashion.163.com/photoview/3QV10026/10411.html#p=712R1EA13QV10026">ÃÔÀëÊæä¿VS¸öÐÔËïÙ³ </a> <a target="_blank" href="httpdisabled://fashion.163.com/photoview/43AJ0026/10396.html#p=711T9O3A43AJ0026">´¿ÕæÓÕ»ó£¡ÄÑ¿¹¾Ü·ÛÄÛ÷ÈÁ¦</a></li>
+ <li><a href="httpdisabled://fashion.163.com/attractive/">[´îÅä]</a> <a target="_blank" href="httpdisabled://fashion.163.com/11/0407/17/7129IQEJ002626K1.html">ÈÕϵËØÑÅͨÇÚ×° ÈËÆøUPÃؾ÷</a> <a target="_blank" href="httpdisabled://fashion.163.com/11/0407/13/711SKLGF00264KF8.html">´º¼¾³öÓθöÐÔ´òµ×¿ã </a></li>
+ <li><a href="httpdisabled://fashion.163.com/">[ʱ÷Ö]</a> <a target="_blank" href="httpdisabled://fashion.163.com/photoview/43AJ0026/10409.html#p=7128GQKO43AJ0026">ÕâЩ´ºÏĸ߸úЬÃÀµÃ²»Ïñ»°</a> <a target="_blank" href="httpdisabled://fashion.163.com/11/0407/17/7128012O00264J93.html">¸ßµ÷¸´¹Å·þ×°ÓÐÒÕÊõ¸Ð</a></li>
+ <li><a href="httpdisabled://fashion.163.com">[´ó³ß¶È]</a> <a target="_blank" href="httpdisabled://fashion.163.com/photoview/43AJ0026/10386.html#p=711II84E43AJ0026">ÖÚÅ®ÐǸ°ÈÕÅļ«ÏÞдÕæ </a> <a target="_blank" href="httpdisabled://fashion.163.com/photoview/43AJ0026/10390.html#p=711LMLAU43AJ0026">Ä£ÌØÑÝÒïºì´½+ÐÔ¸ÐÄÚÒÂ</a></li>
+ <li><a href="httpdisabled://koreastyle.163.com/">[º«¹úÕ¾]</a> <a target="_blank" href="httpdisabled://fashion.163.com/photoview/4GJF0026/10406.html#p=7127JKFL4GJF0026">ÒüÏàîçȫзþװдÕæ</a> <a target="_blank" href="httpdisabled://fashion.163.com/photoview/4GJF0026/10407.html#p=7127NDG94GJF0026">ÒüʤÑÅÆ×д´ºÈÕ×àÃùÇú</a></li>
+ <li><a href="httpdisabled://fashion.163.com/">[΢²©¿Ø]</a> <a target="_blank" href="httpdisabled://t.163.com/liujianan/status/-9002711272039920505#retweet ">ÄÐÈË×°ÉãӰʦ´òÔìÐÔ¸Ð×°Å®ÀÉ</a> <a target="_blank" href="httpdisabled://t.163.com/lidongtian/status/-6067023179144458606#retweet">ÀÌï×óÓµÓÒ±§</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://fashion.163.com/special/2011chicyg/"><em class='fB'>ÖØ°õ²ß»®</em></a> | <a target="_blank" href="httpdisabled://fashion.163.com/special/2011chicyg/">ÍøÒ×ʱÉÐÆô¶¯¡°Öйú´´Ô족´óÐÍÆ·Åƻ</a></li>
+ <li><a href="httpdisabled://fashion.163.com/special/2011chinafashionweek/"><em class='fB'>ʱÉÐÏÈ·æ</em></a> | <a target="_blank" href="httpdisabled://fashion.163.com/special/shuqi1/">Êæä¿£º±»½ÐʣŮ²»ÔÚºõ£¬ºÃ¹ýÓ¹Ë×ÈËÉú</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://lady.163.com/photoview/43A90026/10329.html#p=70L44JAT43A90026"><img src="../img3.cache.netease.com/lady/2011/4/8/20110408082553b8653.jpg" alt="Ã÷ÐÇÏÖÉí½ÌÄãʧÁµÁÆ·¨" title="Ã÷ÐÇÏÖÉí½ÌÄãʧÁµÁÆ·¨" height="90" width="120" /><cite>Ã÷ÐÇÏÖÉí½ÌÄãʧÁµÁÆ·¨</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://lady.163.com/11/0402/19/70LJBHNH002649P6.html">±ðÌ«¿´ÖسÐŵ ÓÐʱËüÖ»ÊÇÒ»ÖÖµ÷Çé</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://lady.163.com/11/0331/12/70FMRUVM00264IIU.html">Ëû´øÎÒÈ¥¼û³õÁµÇéÈË ÎÒ×ÔÐŲ»ÆðÀ´</a></li>
+ <li><a href="httpdisabled://lady.163.com/11/0331/12/70FMDBQ600264IIU.html">°®ÉϺÃÓѵÄÇ°ÄÐÓÑ Ëý˵ËûÃÇû·ÖÊÖ</a></li>
+ <li><a href="httpdisabled://lady.163.com/11/0331/12/70FLLJKK00264IIU.html">ÅãËûÐÁ¿àƯ²´¶àÄê ËûÈ´ÀÏÏò×ÅËûÂè</a></li>
+ <li><a href="httpdisabled://lady.163.com/11/0331/11/70FKMTV000264IIU.html">ÀϹ«ËµËûÓбÜÔÐËùÒÔСÈý²»»á»³º¢×Ó</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://lady.163.com/sense/">[¶à½ÇÁµ]</a> <a target="_blank" href="httpdisabled://lady.163.com/11/0406/16/70VIVBLK00264IIU.html">×÷ΪÇéÈË ÎÒÈ´ÓëËûµÄ˾»ú·¢ÉúÁ˹Øϵ</a></li>
+ <li><a href="httpdisabled://lady.163.com/sense/">[ÌðÃÛ]</a> <a target="_blank" href="httpdisabled://lady.163.com/11/0406/10/70UVV1JV00264IIU.html">¼´Ê¹Ã»ÓÐÔÚ×îÃÀÀöµÄÄ껪Óö¼ûÄã Ò²²»¾õµÃÒź¶</a></li>
+ <li><a href="httpdisabled://lady.163.com/sense/">[ÌÖÂÛ]</a> <a target="_blank" href="httpdisabled://lady.163.com/11/0406/10/70UVHFF900264IIU.html">Èç¹ûûÓÐÇ® ÎÒÃǵİ®Çéµ½µ×Äܹ»×߶àÔ¶£¿</a></li>
+ <li><a href="httpdisabled://lady.163.com/sense/">[ÇóÖú]</a> <a target="_blank" href="httpdisabled://lady.163.com/11/0331/12/70FM5AQR00264IIU.html">¾Ý˵Õæ°®ÄãµÄ²»»áºÍÄã¸ãêÓÃÁ ÄÇÎÒ¸ÃÀ뿪Âð</a></li>
+ <li><a href="httpdisabled://lady.163.com/sense/">[ÉËÐÄ]</a> <a target="_blank" href="httpdisabled://lady.163.com/11/0331/11/70FKVBUP00264IIU.html">ÒòΪ²»ÄÜÉúº¢×Ó×îÖÕ±»ËûÅ×Æú »¹²»¸ø·ÖÊÖ·Ñ</a></li>
+ <li><a href="httpdisabled://lady.163.com/astro/">[ÐÇ×ù] </a><a target="_blank" href="httpdisabled://lady.163.com/special/sense/siyueyunshi.html">ËÄÔÂÔËÊƴ󼯺Ï</a> <a target="_blank" href="httpdisabled://lady.163.com/11/0406/18/70VP9DAB00264III.html">AlexÐÇ×ùÒ»ÖÜÔËÊÆ4.6-4.12</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://lady.163.com/sense/"><em class='fB'>ʱÆÀ×÷ÕßȦ</em></a> |<a target="_blank" href="httpdisabled://t.163.com/xiazm/status/204208825049025523#retweet">40ËêÄãÄÜ׬µ½4ǧÍòÂð</a> <a target="_blank" href="httpdisabled://t.163.com/cbyxw/status/2221663630884260824#retweet">ÓͼÛÉϵ÷³Ô²»Ïû</a></li>
+ <li><a href="httpdisabled://lady.163.com/sense/"><em class='fB'>Çé¸Ðר¼ÒÍÅ</em></a> |<a target="_blank" href="httpdisabled://t.163.com/woshiyushunshun/status/5810766844942876669">¿´ÖØÄÐÈ˳ö¹ì¾ÍÏñÄÐÈË¿´ÖØ´¦Å®Ò»Ñù»¬»ü</a> <a target="_blank" href="httpdisabled://t.163.com/zhaogeyu/status/1145370794134928600">Áµ°®°Ë´ó½û¼ÉÊØÔò</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://lady.163.com/photoview/4CJ80026/10401.html#p=711UV1DK4CJ80026"><img src="../img3.cache.netease.com/lady/2011/4/7/20110407235235eb565.jpg" alt="·ëÉÜ·åÑîÃÝÐԸгö¾µ" title="·ëÉÜ·åÑîÃÝÐԸгö¾µ" height="90" width="120" /><cite>·ëÉÜ·åÑîÃÝÐԸгö¾µ</cite></a>
+</div>
+ <h3 class="main-title"><a href="httpdisabled://lady.163.com/11/0407/21/712MUKQB00264IJ9.html">ٻŮÓÄ»êPK ÁõÒà·Æ¸üÓÐÓù½ã·¶</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://lady.163.com/photoview/4CJ80026/10402.html#p=712014OG4CJ80026">ºÃÀ³ÎëÅ®ÐDZä×±Ðã Ч¹û¿°±ÈPS</a></li>
+ <li><a href="httpdisabled://lady.163.com/photoview/4CJ80026/10392.html#p=711S5P4U4CJ80026">ºì±éÑÇÖÞ ÓÀ²»¹ýÆø52¿î±¬ÃÀ·¢ÐÍ</a></li>
+ <li><a href="httpdisabled://lady.163.com/11/0407/13/711QCROP00261IDC.html">ºË·øÉäÏ ÈÕϵ·çױƷPKÅ·ÃÀ·¶¶ù</a></li>
+ <li><a href="httpdisabled://lady.163.com/11/0407/14/711TA9SO00264IJ3.html">Å®ÐÇÃÀ±³ÃîÕÐ ½ÌÄãתÉíÒ²ÄÜÓÕ»ó</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://lady.163.com/beauty/">[Ã÷ÐÇ]</a> <a target="_blank" href="httpdisabled://lady.163.com/11/0407/14/711UD5G300261IDD.html">ÍÑÀëʣٵÄÅ®ÐÇÔìÐÍ</a> <a target="_blank" href="httpdisabled://lady.163.com/photoview/4CJ80026/10408.html#p=7127TLEP4CJ80026">ÌÀΨ0ºÅÉí²Ä³Æ°ÔʱÉÐȦ</a></li>
+ <li><a href="httpdisabled://lady.163.com/beauty/">[¼¼ÇÉ]</a> <a target="_blank" href="httpdisabled://lady.163.com/11/0407/15/7123O09R00261IDD.html">¾ëÈݲ»½ø°ì¹«ÊÒ 5Ãë»»ÑÕÃîÕÐ</a> <a target="_blank" href="httpdisabled://lady.163.com/11/0407/14/711VU77U00261IDD.html">ѧ»¯À滨װÄÛ×±</a></li>
+ <li><a href="httpdisabled://lady.163.com/beauty/">[´óƬ]</a> <a target="_blank" href="httpdisabled://lady.163.com/photoview/4CJ80026/10404.html#p=7122Q5VQ4CJ80026">×¢ÒâÕâ¸ö´ºÌìÒª×ö·ÛºìÅ®ÀÉ</a> <a target="_blank" href="httpdisabled://lady.163.com/photoview/4CJ80026/10383.html#p=710A2AHE4CJ80026">À­¶¡»ÊºóÌÒÉ«÷ÈÓ°</a></li>
+ <li><a href="httpdisabled://lady.163.com/beauty/">[»¤·ô]</a> <a target="_blank" href="httpdisabled://lady.163.com/11/0407/14/71205P3T00261IDC.html">¾¯Ì軯ױƷµÄɽկ»õ</a> <a target="_blank" href="httpdisabled://lady.163.com/11/0407/15/7120OGQN00261IDC.html">»¤·ôÆ·ÓöÔÁ¿Ð§¹û·­±¶</a></li>
+ <li><a href="httpdisabled://lady.163.com/beauty/">[ÃÀ·¢]</a> <a target="_blank" href="httpdisabled://lady.163.com/11/0407/16/7124J76Q00261PDG.html">DIYȾ·¢¼¼Êõ±ÈÃÀ·¢Ê¦×¨Òµ</a> <a target="_blank" href="httpdisabled://lady.163.com/11/0407/12/711PFBDR00261PDG.html">»¨¶ä±à·¢ÀËÂþÉý¼¶</a></li>
+ <li><a href="httpdisabled://lady.163.com/beauty/">[͵ʦ]</a> <a target="_blank" href="httpdisabled://lady.163.com/11/0407/14/711VOECH00264IJ3.html">ֽƬÈËÅ®ÐǵÄÊÝÉíÐļÆ</a> <a target="_blank" href="httpdisabled://lady.163.com/11/0407/12/711ON0LV00261IDD.html">ÃÀ¼×ÕÀ·Å½ñ´ºµ±ºìÉ«²Ê</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://lady.163.com/special/2010meirongdajiangbanjiang/"><em class='fB'>ÃÀÈÝ´ó½±</em></a> |<a target="_blank" href="httpdisabled://t.163.com/jinyuxi">½ùÓðÎ÷³öϯÉϺ£µÏÊ¿ÄῪ¹¤Àñ</a> <a target="_blank" href="httpdisabled://t.163.com/fenshouzhuang/status/-711319073236628">·ÖÊÖÃÃдÕæ</a></li>
+ <li><a href="httpdisabled://t.163.com/zt/lady/weimeiren"><em class='fB'>΢ÃÀÈË</em></a> |<a target="_blank" href="httpdisabled://t.163.com/rogerxiaoxin/status/-2251987904942340332">×¢Ò⣺ë¿×Ô½³¶Ô½´ó</a> <a target="_blank" href="httpdisabled://t.163.com/2951350383">С½ÌÖ÷Ö§ÕУº¹ûËá»»·ô</a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="main-col-9">
+<div class="gg gg-h100"><iframe src="../g.163.com/r@site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=column360x100&amp;location=5.html" width="360" height="100" frameborder="0" border="0" marginwidth="0" marginheight="0" scrolling="no" ></iframe>
+</div>
+ <div id="book" class="mod wgt-tab">
+ <h2 class="tab-hd tab-u-9 clearfix">
+ <span class="tab-u current"><a href="httpdisabled://book.163.com/">¶ÁÊé</a></span>
+ <span class="tab-u"><a href="httpdisabled://data.book.163.com/">ͼÎÄ</a></span>
+ <span class="mod-entry"><a href="httpdisabled://book.163.com/rank/">ͼÊéÅÅÐаñ</a></span>
+ </h2>
+ <div class="bd tab-bd display-control">
+ <div class="tab-con current">
+ <div class="imgText-temp-1 dotline clearfix">
+ <div class="mod-img main-img">
+ <a href="httpdisabled://data.book.163.com/book/section/0000FaLV/0000FaLV70.html?wangshou1"><img src="../img4.cache.netease.com/book/2011/4/8/20110408102221db369.jpg" alt="ÈÕ±¾ºÜºó»Ú·¢¶¯77ʱä" title="ÈÕ±¾ºÜºó»Ú·¢¶¯77ʱä" height="90" width="120" /><cite>ÈÕ±¾ºÜºó»Ú·¢¶¯77ʱä</cite></a>
+ </div>
+ <h3 class="main-title"><a href="httpdisabled://book.163.com/?wangshou1">»¹Ô­±»Ñýħ»¯µÄ¹úÃñµ³½«Áì</a></h3>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://data.book.163.com/book/section/000BEaSf/000BEaSf1.html?wangshou1">¶«±±ºÚÉç»áÀϴ󣺶çÊÖÖ¸Á¢Íþ</a></li>
+ <li><a href="httpdisabled://data.book.163.com/book/home/009200010013/0000LWGF.html?wangshou1">¹«°²¾Ö³¤¾¹ÊÇÄ»ºóºÚ°ïÀÏ´ó</a></li>
+ <li><a href="httpdisabled://data.book.163.com/book/section/0000FUKb/0000FUKb8.html?wangshou1">Õâ¸öÄÐÓÑÓеãÀ䣬Ëû˵kissÔà</a></li>
+ <li><a href="httpdisabled://data.book.163.com/book/section/0000FFVL/0000FFVL39.html?wangshou1">×îÔçÔ¤ÑÔëÔ󶫳ÉΪÁìÐäµÄÈË</a></li>
+ </ul>
+ </div>
+ <ul class="mod-list main-list">
+ <li><a href="httpdisabled://data.book.163.com/index.html?wangshou1">[ÀúÊ·]</a> <a target="_blank" href="httpdisabled://data.book.163.com/book/section/0000FdFQ/0000FdFQ5.html">µËСƽÈçºÎÕÆȨ</a> <a target="_blank" href="httpdisabled://data.book.163.com/book/section/0000FEVC/0000FEVC74.html">ÃÀÏòÈÕ±¾Í¶ÁËÈý¿ÅÔ­×Óµ¯</a></li>
+ <li><a href="httpdisabled://data.book.163.com/list/3_009200010013.html?wangshou1">[¹Ù³¡]</a> <a target="_blank" href="httpdisabled://data.book.163.com/book/home/009200010013/0000fKFb.html">Á½¸öÅ®¿Æ³¤µÄ±ðÑùÈËÉú</a> <a target="_blank" href="httpdisabled://data.book.163.com/book/home/009200010013/0000fJfP.html">ÎÂÈáÏÝÚå±³ºóµÄ¹Ù³¡</a></li>
+ <li><a href="httpdisabled://data.book.163.com/list/2_00920001.html?wangshou1">[¶¼ÊÐ]</a> <a target="_blank" href="httpdisabled://data.book.163.com/book/home/009200010019/0000UHKZ.html">ɱÈË°¸Òý³öêÓÃÁÇé¸Ð</a> <a target="_blank" href="httpdisabled://data.book.163.com/book/home/009200010019/0000ZZMH.html">Äа×Áì°®ÉÏÒ¹×Ü»áС½ã</a></li>
+ <li><a href="httpdisabled://data.book.163.com/list/2_00920001.html?wangshou1">[Ô­´´]</a> <a target="_blank" href="httpdisabled://data.book.163.com/book/home/009200010011/0000JMdT.html">±»Àä¿áÕÉ·ò"Çô½û"ÆßÄê</a> <a target="_blank" href="httpdisabled://data.book.163.com/book/home/009200010010/0000XLDL.html">¹Å´úÅ®¼äµýÊÖÍó¸ß³¬</a></li>
+ <li><a href="httpdisabled://data.book.163.com/list/2_00920019.html?wangshou1">[Éç¿Æ]</a> <a target="_blank" href="httpdisabled://data.book.163.com/book/section/000BEaVb/000BEaVb17.html">Ê׸ö³ÐÈϱ»ÖйúÎüÒýµÄ×Üͳ</a> <a target="_blank" href="httpdisabled://data.book.163.com/book/home/009200190001/000BEdSA.html">Å®ÈËÄãÀÏÁËÕ¦°ì</a></li>
+ <li><a href="httpdisabled://data.book.163.com/list/2_00920009.html?wangshou1">[ͼÎÄ]</a> <a target="_blank" href="httpdisabled://data.book.163.com/book/section/000BELIL/000BELIL24.html?wangshou1">ÁÖ»ÕÒòÓëÁºË¼³É</a> <a target="_blank" href="httpdisabled://data.book.163.com/book/section/0000UGTM/0000UGTM2.html?wangshou1">ëÔó¶«×îºó7Äê</a> <a target="_blank" href="httpdisabled://data.book.163.com/book/section/0000UHHF/0000UHHF0.html?wangshou1">Íõ¹âÃÀÏà²á</a></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://book.163.com/rank/?wangshou1"><em class='fB'>ÈÈÊé</em></a> | <a target="_blank" href="httpdisabled://data.book.163.com/book/home/009200010014/000BEKcO.html?wangshou1">°Ë·¾üѪսÈÕ¿Ü</a> | <a target="_blank" href="httpdisabled://data.book.163.com/book/home/009200010013/000BEKYJ.html?wangshou1">¹Ù³¡Ç±¹æÔò</a> | <a target="_blank" href="httpdisabled://data.book.163.com/book/home/009200010019/000BELMA.html?wangshou1">ºì³¾µßµ¹</a></li>
+ <li><a href="httpdisabled://book.163.com/rank/?wangshou1"><em class='fB'>È«±¾</em></a> | <a target="_blank" href="httpdisabled://data.book.163.com/book/home/009200010013/000BCFHb.html">ÉóÅÐÔÚ¼´</a> | <a target="_blank" href="httpdisabled://data.book.163.com/book/home/009200010011/000BELMS.html">ÎÒÊÇ»µÅ®Éú</a> | <a target="_blank" href="httpdisabled://data.book.163.com/book/home/009200010013/000BEKaV.html">¸±Ê¡³¤²»°®Å®É«</a></li>
+ </ul>
+ </div>
+ <div class="tab-con">
+ <ul class="mod-imgList imgList-w160-2 dotline clearfix">
+ <li><a href="httpdisabled://data.book.163.com/book/section/000BDIGW/000BDIGW5.html"><img src="../img3.cache.netease.com/book/2011/4/8/20110408110145beb70.jpg" alt="ÔÚÌì°²ÃÅѧϰµÄÇàÄê" title="ÔÚÌì°²ÃÅѧϰµÄÇàÄê" height="90" width="160" /></a><p><a href="httpdisabled://data.book.163.com/book/section/000BDIGW/000BDIGW5.html">ÔÚÌì°²ÃÅѧϰµÄÇàÄê</a></p></li>
+ <li><a href="httpdisabled://data.book.163.com/book/section/000BDBXU/000BDBXU10.html"><img src="../img3.cache.netease.com/book/2011/4/8/20110408105903d5d53.jpg" alt="ÕÅÒÕı¹®ÀþÀÏÕÕƬÆعâ" title="ÕÅÒÕı¹®ÀþÀÏÕÕƬÆعâ" height="90" width="160" /></a><p><a href="httpdisabled://data.book.163.com/book/section/000BDBXU/000BDBXU10.html">ÕÅÒÕı¹®ÀþÀÏÕÕƬÆعâ</a></p></li>
+ <li><a href="httpdisabled://data.book.163.com/book/section/000BELIL/000BELIL34.html"><img src="../img3.cache.netease.com/book/2011/4/7/20110407103153df111.jpg" alt="Ãû¼ËÈü½ð»¨½á»éÕÕ" title="Ãû¼ËÈü½ð»¨½á»éÕÕ" height="90" width="160" /></a><p><a href="httpdisabled://data.book.163.com/book/section/000BELIL/000BELIL34.html">Ãû¼ËÈü½ð»¨½á»éÕÕ</a></p></li>
+ <li><a href="httpdisabled://data.book.163.com/book/section/000BELBS/000BELBS30.html"><img src="../img3.cache.netease.com/book/2011/4/7/201104071025387042e.jpg" alt="º«¹úÃ÷ÐÇʱÉбضÁ±¦µä" title="º«¹úÃ÷ÐÇʱÉбضÁ±¦µä" height="90" width="160" /></a><p><a href="httpdisabled://data.book.163.com/book/section/000BELBS/000BELBS30.html">º«¹úÃ÷ÐÇʱÉбضÁ±¦µä</a></p></li>
+ </ul>
+ <ul class="mod-list specialTopic-list">
+ <li><a href="httpdisabled://book.163.com/?wangshou2?wangshou1"><em class='fB'>΢²©ÎÄѧ</em></a> | <a target="_blank" href="httpdisabled://t.163.com/zt/book/qingming?wangshou2">ÇåÃ÷½Ú£¬ÓвŵÄÄã¿ìÀ´Ðøд#¶Ï»êÌå</a></li>
+ <li><a href="httpdisabled://book.163.com/?wangshou1"><em class='fB'>΢²©Êéµ¥</em></a> | <a target="_blank" href="httpdisabled://t.163.com/zt/book/weishudan03">ÇåÃ÷½ÚÈÃÎÒÃÇÒ»ÆðÀ´¶Á¶Á¡°¸¼¸æÌ塱</a></li>
+ <li><a href="httpdisabled://book.163.com/?wangshou1"><em class='fB'>×÷¼Ò¹Ûµã</em></a> | <a target="_blank" href="httpdisabled://t.163.com/haoyuehan/status/-7869073952390510568">º«ºÆÔÂ:°²ÌïÊǸöÇéÉ̲»¸ßµÄÓ×ÖÉÇàÄê</a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="area-sub">
+<div class="gg gg-h180"><iframe src="../g.163.com/r@site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=logo190x180&amp;location=4.html" width="190" height="180" frameborder="0" border="0" marginwidth="0" marginheight="0" scrolling="no" ></iframe></div>
+ <div class="gg gg-h300"><iframe src="../g.163.com/r@site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=logo190x300&amp;location=2.html" width="190" height="300" frameborder="0" border="0" marginwidth="0" marginheight="0" scrolling="no" ></iframe></div>
+ </div>
+</div>
+<!-- end -->
+<div class="area">
+<div class="area-main">
+ <div class="main-col-10">
+ <iframe src="../g.163.com/r@site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=column390x100&amp;location=6.html" width="390" height="100" frameborder="0" border="0" marginwidth="0" marginheight="0" scrolling="no" ></iframe>
+
+ </div>
+ <div class="main-col-9">
+ <iframe src="../g.163.com/r@site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=column360x100&amp;location=6.html" width="360" height="100" frameborder="0" border="0" marginwidth="0" marginheight="0" scrolling="no" ></iframe>
+
+ </div>
+ </div>
+ <div class="area-sub">
+ <div id="trends" class="mod trends-mod">
+ <div class="hd clearfix">
+ <h2 class="mod-title"><a href="httpdisabled://gb.corp.163.com/gbnews/General.html">ÍøÒ׶¯Ì¬</a></h2>
+ <span class="mod-entry">NTES:52.93 -0.13%
+</span>
+ </div>
+ <div class="bd">
+ <ul class="mod-list trends-list cBlue">
+ <li><a href="httpdisabled://corp.163.com/11/0318/11/6VE3M14G00832V3T.html">¡¶ÐǼÊÕù°Ô2¡·3ÔÂ29ÈÕÃâ·Ñ¹«²â</a></li>
+ <li><a href="httpdisabled://corp.163.com/11/0304/11/6UA4807T00832V3T.html">å­¹«Òæ»ù½ð»á³ÉÁ¢ ÍøÒ×¾èǧÍò</a></li>
+ <li><a href="httpdisabled://corp.163.com/11/0224/07/6TL1O5ML00832V3T.html">ÍøÒ×¹«²¼Ëļ¾¶ÈδÉó¼Æ²ÆÎñÒµ¼¨</a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+</div>
+</div>
+<div class="footer">
+ <div class="aboutNetease">
+ <ul>
+ <li><a href="httpdisabled://corp.163.com/eng/about/overview.html">About NetEase</a>¡¡-¡¡<a href="httpdisabled://gb.corp.163.com/gb/about/overview.html">¹«Ë¾¼ò½é</a>¡¡-¡¡<a href="httpdisabled://gb.corp.163.com/gb/contactus.html">ÁªÏµ·½·¨</a>¡¡-¡¡<a href="httpdisabled://hr.163.com/">ÕÐƸÐÅÏ¢</a>¡¡-¡¡<a href="httpdisabled://help.163.com/">¿Í»§·þÎñ</a>¡¡-¡¡<a href="httpdisabled://gb.corp.163.com/gb/legal.html">Ïà¹Ø·¨ÂÉ</a>¡¡-¡¡<a href="httpdisabled://emarketing.163.com/">ÍøÂçÓªÏú</a>¡¡-¡¡<a href="httpdisabled://sitemap.163.com/">ÍøÕ¾µØͼ</a>¡¡-¡¡<a href="httpdisabled://survey2.163.com/html/userexperience/cover.html">Óû§ÌåÑéÌáÉý¼Æ»®</a></li>
+ </ul>
+ </div>
+<!-- Ò³½Å -->
+ <div class="foot">
+ <div class="copyRight">ÍøÒ×¹«Ë¾°æȨËùÓС¡&copy;1997-2011<br />
+ <a href="httpdisabled://img3.126.net/163homepage/license_090531.jpg">ICPÖ¤ÔÁB2-20090191</a> <a href="httpdisabled://img3.126.net/163homepage/zzxk09.jpg">ÔöÖµµçÐÅÒµÎñ¾­ÓªÐí¿ÉÖ¤B2-20090058</a> <a href="httpdisabled://img3.126.net/163homepage/cert.jpg">»¥ÁªÍø³ö°æÐí¿ÉÖ¤ÔÁ002ºÅ</a> <a href="httpdisabled://img1.cache.netease.com/cnews/163/img6/xuke.jpg">»¥ÁªÍøÐÂÎÅÐÅÏ¢·þÎñÐí¿ÉÖ¤</a> <a href="httpdisabled://www.gdca.gov.cn/">¹ã¶«Ê¡Í¨ÐŹÜÀí¾Ö</a> <a href="httpdisabled://cimg.163.com/home/2005/8/16/20050816101415d41d8.jpg">¹ú¼ÊÁªÍø±¸°¸</a> <a href="httpdisabled://www.bjjubao.org/index.htm">±±¾©»¥ÁªÍøÎ¥·¨²»Á¼ÐÅÏ¢¾Ù±¨</a><br>
+ <a href="httpdisabled://net.china.cn/chinese/index.htm">Î¥·¨²»Á¼ÐÅÏ¢¾Ù±¨ÖÐÐÄ</a>¡¡<a href="httpdisabled://post.news.163.com/msg/jubao.jsp">²»Á¼ÐÅÏ¢¾Ù±¨ÐÅÏä</a>¡¡<a href="httpdisabled://post.news.163.com/msg/zhubian.jsp">ÎÄÃ÷°ìÍø¾Ù±¨µç»°</a>¡¡<a href="httpdisabled://www.netbj.org.cn/">±±¾©ÍøÂçÐÐҵЭ»á</a>¡¡<a href="httpdisabled://img1.126.net/channel1/html/stxkz.JPG">ÊÓÌý½ÚÄ¿ÖÆ×÷Ðí¿ÉÖ¤</a>¡¡<a href="httpdisabled://cimg20.163.com/sports/2008/5/16/20080516153237ce90f.gif">´«²¥Ðí¿ÉÖ¤</a>¡¡<a href="httpdisabled://img4.cache.netease.com/cnews/img10/20101013.jpg">ÎÄÍøÎÄ[2008]080ºÅ</a>¡¡<a href="httpdisabled://img1.126.net/channel1/html/ylzs_0422.pdf">»¥ÁªÍøÒ©Æ·ÐÅÏ¢·þÎñ×ʸñÖ¤Êé</a><br>
+ ¾©¹«Íø°²±¸110000000013ºÅ ±±¾©ÍøͨÌṩÍøÂç´ø¿í <br /> <a href="httpdisabled://www.hd315.gov.cn/beian/view.asp?bianhao=0102000102300012"><img src="../img3.126.net/163homepage/biaoshi.gif" alt=""></a> <a href="httpdisabled://www.itrust.org.cn/yz/pjwx.asp?wm=2012043533"><img alt="" src="../img1.cache.netease.com/cnews/netease/wzdzbs.gif"></a> <a href="httpdisabled://www.bj.cyberpolice.cn/index.htm"><img alt="" src="../img3.126.net/163homepage/bj110.gif"></a> </div>
+ </div>
+</div>
+</div>
+<script type="text/javascript">
+//<![CDATA[
+//NTES Login Start
+(function () {
+ var body = NTES(document.body), doc = NTES(document.documentElement);
+ var TIMER = null;
+ var P_INFO = NTES.cookie.get("P_INFO");
+ var S_INFO = NTES.cookie.get("S_INFO");
+ var SELECT_VALUE = NTES.cookie.get("selectValue");
+ var CLOSE_NUM = NTES.cookie.get("closeNum");
+ SELECT_VALUE = SELECT_VALUE == "" ? -1 : SELECT_VALUE;
+ CLOSE_NUM = CLOSE_NUM == "" ? 0 : parseInt(CLOSE_NUM);
+ NTES.element.datasetFix = function(name, value) {
+ var t = this;
+ if (value !== undefined) {
+ if (t.dataset){
+ t.dataset[name] = value;
+ }else{
+ t.setAttribute("data-" + name, value);
+ };
+ t.className = t.className;
+ }else{
+ if (t.dataset){
+ return t.dataset[name];
+ }else{
+ return t.getAttribute("data-" + name);
+ }
+ }
+ };
+
+
+ NTES.element.showed = function() {
+ if (NTES.style.getCurrentStyle(this, "display") == "none"){
+ return false
+ }else{
+ return true
+ }
+ };
+
+ NTES.element.show = function() {
+ this.addCss("display:block");
+ };
+
+ NTES.element.hide = function(delay) {
+ var t = this;
+ TIMER = window.setTimeout(function(){
+ t.addCss("display:none");
+ }, delay);
+ };
+
+ NTES.element.toggle = function() {
+ var t = this;
+ if ( !(this.showed()) ){
+ t.show();
+ }else{
+ t.hide();
+ }
+ };
+
+ //NTES Auto Complete
+ var NTESAutoComplete = function( inputElem, nextElem ) {
+ var t = this;
+ t._inputElem = inputElem;
+ t._nextElem = nextElem;
+ t._idName = "login_auto_list";
+ t._className = "login-auto-list";
+ t._domains = ["163.com", "126.com", "vip.126.com", "yeah.net", "188.com", "vip.163.com", "gmail.com", "qq.com", "sina.com", "hotmail.com"];
+ t._account = "";
+ t._domain = "163.com";
+ t.mailType = 0;
+ t.buildList();
+ t._autoList = NTES("#" + t._idName);
+ t._autoListItem = t._autoList.NTES("td");
+
+ t._autoListItem.addEvent("mouseover", function() {
+ t._autoListItem.removeCss("hover");
+ $(this).addCss("hover");
+ })
+ .addEvent("mouseout", function() {
+ $(this).removeCss("hover");
+ });
+
+ t._inputElem.addEvent("focus", function() {
+ if (this.value.trim() != "") t._autoList.show();
+ })
+ .addEvent("blur", function() {
+ if (this.value.trim() != ""){
+ t.selectCurrent();
+ t._autoList.hide();
+ t._nextElem.focus();
+ }
+ })
+ .addEvent("keydown", function(e) {
+ switch(e.keyCode) {
+ case 38: // up
+ e.preventDefault();
+ t.moveSelect(-1);
+ break;
+ case 40: // down
+ e.preventDefault();
+ t.moveSelect(1);
+ break;
+ case 9: // tab
+ if (t._autoList.showed()){
+ t._autoListItem.removeCss("hover");
+ };
+ e.preventDefault();
+ this.blur();
+ break;
+ case 13: // return
+ case 108: // num return
+ if( t.selectCurrent() ){
+ e.preventDefault();
+ this.blur();
+ };
+ break;
+ default:
+ break;
+ }
+ t.refreshAutoList();
+ })
+ .addEvent("keyup", function(e) {
+ t.refreshAutoList();
+ });
+ };
+ NTESAutoComplete.prototype = {
+ insertAfter : function(newElem, targetElem) {
+ var parentElem = targetElem.parentNode;
+ if(parentElem.lastChild == targetElem)
+ {
+ parentElem.appendChild(newElem);
+ }else{
+ parentElem.insertBefore(newElem, targetElem);
+ }
+ },
+ isNTESDomain : function(value){
+ var t = this;
+ var pos = value.indexOf("@");
+ var domain = value.substr(pos + 1, value.length);
+ for (var i = 0 ; i < t._domains.length; i++){
+ if (t._domains[i] == domain) {
+ return true;
+ }
+ };
+ return false;
+ },
+ selectCurrent : function() {
+ var t = this;
+ var hoverItem = -1;
+ for (var i = 0 ; i < t._autoListItem.length; i++){
+ if ( $(t._autoListItem[i]).hasClass("hover") ){
+ hoverItem = i;
+ break;
+ }
+ };
+ if (hoverItem == -1) {
+ if (t._inputElem.value.indexOf("@") == -1) {
+ t.mailType = t.mailType == 0 ? t.mailType = 1 : t.mailType;
+ t.setInputValue(t.mailType);
+ }else if( !(t.isNTESDomain(t._inputElem.value)) ){
+ t.mailType = 0;
+ }
+ };
+ if (hoverItem > -1) {
+ t.mailType = hoverItem + 1;
+ t.setInputValue(t.mailType);
+ return true;
+ } else {
+ return false;
+ };
+ },
+ moveSelect : function (step){
+ var t = this;
+ if (t._inputElem.value.trim() == "") return;
+ t.mailType += step;
+ if (t.mailType <= 0) {
+ t.mailType = t._domains.length;
+
+ } else if (t.mailType > t._domains.length) {
+ t.mailType = 1;
+ }
+
+ t._autoListItem.removeCss("hover");
+ $(t._autoList.$("td")[t.mailType - 1]).addCss("hover");
+ t.selectCurrent();
+
+ },
+ splitValue : function() {
+ var t = this;
+ var value = t._inputElem.value;
+ var pos = value.indexOf("@");
+ var account, domain;
+ if (value.trim() != "") {
+ if (pos == -1){
+ account = value;
+ domain = t._domain;
+ }else{
+ account = value.substr(0, pos);
+ domain = value.substr(pos + 1, value.length);
+ }
+ }else{
+ account = "";
+ domain = t._domain;
+ };
+ t.mailType = 0;
+ for (var i = 0 ; i < t._domains.length ; i++ ){
+ if ( t._domains[i] == domain ) t.mailType = i + 1;
+ };
+ t._account = account;
+ t._domain = domain;
+ },
+ refreshAutoList : function(silent) {
+ var t = this;
+ t.splitValue();
+ for (var i = 0 ; i < t._autoListItem.length; i ++){
+ t._autoListItem[i].firstChild.nodeValue = t._account + "@" + t._domains[i];
+ };
+ if (t._inputElem.value.trim() == "") {
+ t._autoList.hide();
+ t.mailType = 0;
+ }else if ( silent == undefined ){
+ t._autoList.show();
+ };
+ },
+ setInputValue : function(n) {
+ if ( n == 0 ) return;
+ var t = this;
+ t.mailType = n;
+ t._inputElem.value = t._autoListItem[n - 1].firstChild.nodeValue;
+ t.splitValue();
+ },
+ buildList : function() {
+ var t = this;
+ var oTable = document.createElement("table");
+ var oTbody = document.createElement("tbody");
+ var oThead = document.createElement("thead");
+ var oTr = document.createElement("tr");
+ var oTd = document.createElement("td");
+ var oTh = document.createElement("th");
+ t.splitValue();
+ oThead = document.createElement("thead");
+ oTr = document.createElement("tr");
+ oTh = document.createElement("th");
+ oTh.innerHTML = "ÇëÑ¡Ôñ»ò¼ÌÐøÊäÈë...";
+ oTr.appendChild(oTh);
+ oThead.appendChild(oTr);
+ oTable.appendChild(oThead);
+ for (var i = 0; i < t._domains.length; i++){
+ oTr = document.createElement("tr");
+ oTd = document.createElement("td");
+ oTd.innerHTML = t._account ? t._account + "@" + t._domains[i] : t._domains[i];
+ oTr.appendChild(oTd);
+ oTbody.appendChild(oTr);
+ }
+ oTable.appendChild(oTbody);
+ oTable.id = t._idName;
+ oTable.className = t._className;
+ oTable.style.display = "none";
+ t.insertAfter(oTable, t._inputElem);
+ }
+ };
+
+ //NTES Login
+ var NTESLogin = function( usernameElem, passwordElem ) {
+ if (!arguments.length) { return; }
+ var t = this;
+ t.constructor = arguments.callee;
+ t._login = NTES("#NTES_Login");
+ t.addTips(t._login);
+ t._loginForm = NTES("#login_form");
+ t._username = usernameElem;
+ t._password = passwordElem;
+ t._ursname = t._loginForm.ursname;
+ t._submit = NTES($("#login_form input[type=submit]")[0]);
+ t._selectInput = NTES("#login_form [name=selected]")[0];
+ t._selected = SELECT_VALUE;
+ t._selectArea = NTES("#login_select_area");
+ t._select = NTES("#login_selected");
+ t._selectDefault = t._select.firstChild.nodeValue;
+ t._selectMain = NTES("#login_select_main");
+ t._selectList = NTES("li", t._selectMain);
+ t._loginTipsUsername = NTES("#login_tips_username");
+ t._loginTipsPassword = NTES("#login_tips_password");
+ t._loginTipsMobile = NTES("#login_tips_mobile");
+ t._loginBefore = NTES("#login_before");
+ t._loginAfter = NTES("#login_after");
+ t._tipsMobileEnable = (CLOSE_NUM < 3) ? true : false;
+ t._product = NTES("#product");
+ t._autoComplete = new NTESAutoComplete(t._username, t._password);
+ t._selectFilter = [
+ ["others", [0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0]],
+ ["163.com", [0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0]],
+ ["126.com", [0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0]],
+ ["vip.126.com", [0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0]],
+ ["yeah.net", [0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0]],
+ ["188.com", [1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1]],
+ ["vip.163.com", [0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0]],
+ ["gmail.com", [0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0]],
+ ["qq.com", [0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0]],
+ ["hotmail.com", [0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0]]
+ ];
+ t._formRequestArray = [
+ "httpdisabledsdisabled://reg.163.com/logins.jsp",
+ "httpdisabledsdisabled://reg.163.com/logins.jsp?url=http://entry.mail.163.com/coremail/fcg/ntesdoor2?lightweight=1&",
+ "httpdisabledsdisabled://reg.163.com/logins.jsp?type=1&url=http://entry.mail.126.com/cgi/ntesdoor?hid=10010102&lightweight=1&verifycookie=1&language=0&style=-1",
+ "httpdisabledsdisabled://reg.163.com/logins.jsp?type=1&url=http://reg.vip.126.com/enterMail.m",
+ "httpdisabledsdisabled://reg.163.com/logins.jsp?type=1&url=http://entry.yeah.net/cgi/ntesdoor?lightweight=1&verifycookie=1&style=-1",
+ "httpdisabledsdisabled://reg.163.com/logins.jsp?url=http://reg.mail.188.com/servlet/login",
+ "httpdisabledsdisabled://reg.163.com/logins.jsp?type=1&url=http://reg.vip.163.com/enterMail.m?language=-1&style=-1&enterVip=true",
+ "httpdisabledsdisabled://reg.163.com/logins.jsp?url=http://blog.163.com/passportIn.do?entry=163",
+ "httpdisabledsdisabled://reg.163.com/logins.jsp?url=http://photo.163.com/",
+ "httpdisabledsdisabled://reg.163.com/logins.jsp?url=http://yuehui.163.com/?keyfrom=163home",
+ "httpdisabledsdisabled://reg.163.com/logins.jsp?url=http://bbs.163.com",
+ "httpdisabledsdisabled://reg.163.com/logins.jsp?url=http://t.163.com"
+ ];
+ t._username.addEvent("keydown", function(e) {
+ t._loginTipsMobile.hide();
+ })
+ .addEvent("keyup", function(e) {
+ if ( t._selectFilter[t._autoComplete.mailType][1][t.getSelect()] == 1 ){
+ t.setSelect("default");
+ t._selectList.removeCss("hover");
+ }
+ })
+ .addEvent("blur", function() {
+ t._loginTipsUsername.hide();
+ t._username.datasetFix("state", "");
+ if ( t._selectFilter[t._autoComplete.mailType][1][t.getSelect()] == 1 ){
+ t.setSelect("default");
+ t._selectList.removeCss("hover");
+ }
+ t.showTipsMobile();
+ }).addEvent("mouseover", function() {
+ if ( this.value.trim() != ""){
+ t._loginTipsMobile.hide();
+ t.showTipsUsername();
+ }else{
+ t.showTipsMobile();
+ }
+ }).addEvent("mouseout", function() {
+ t._loginTipsUsername.hide();
+ if ( !(t._autoComplete._autoList.showed()) ){
+ t.showTipsMobile();
+ };
+ });
+ t._password.addEvent("blur", function() {
+ t._loginTipsPassword.hide();
+ t._password.datasetFix("state", "");
+ })
+ t._select.addEvent("click", function(e) {
+ e.preventDefault();
+ t.setSelectList();
+ t._selectMain.toggle();
+ })
+ .addEvent("mouseover", function() {
+ if ( t._selectMain.showed() ){
+ TIMER && clearTimeout( TIMER );
+ t._selectMain.show();
+ }})
+ .addEvent("mouseout", function() {
+ t._selectMain.hide(300);
+ });
+ t._selectMain.addEvent("mouseover", function() {
+ TIMER && clearTimeout( TIMER );
+ this.show();
+ })
+ .addEvent("mouseout", function() {
+ this.hide(300);
+ });
+ t._selectList.addEvent("mouseover", function() {
+ t._selectList.removeCss("hover");
+ if( !($(this).hasClass("disable")) ){
+ $(this).addCss("hover");
+ }
+ }).addEvent("click", function() {
+ if( !($(this).hasClass("disable")) ){
+ t.setSelect(t.getSelect());
+ t._selectMain.toggle();
+ }
+ });
+ t._loginForm.addEvent("submit", function(e) {
+ e.preventDefault();
+ t.doSubmit();
+ });
+ t._loginTipsMobile.NTES("a.login-tips-close").addEvent("click", function(e) {
+ e.preventDefault();
+ t._tipsMobileEnable = false;
+ t._loginTipsMobile.hide();
+ NTES.cookie.set("closeNum", CLOSE_NUM + 1 , 30 * 24 * 60);
+ });
+
+ t.init();
+ };
+ NTESLogin.prototype = {
+ init : function() {
+ var t = this;
+ var pInfo = P_INFO;
+ var sInfo = S_INFO;
+ pInfo = pInfo.substr(0, pInfo.indexOf("|"));
+ var account = pInfo.substr(0, pInfo.indexOf("@"));
+ /@([^*]+)/.test(pInfo);
+ var domain = RegExp.$1;
+ if ( t._selected > 0 && t._selected < t._selectFilter.length ){
+ for ( var i = 0; i < t._selectFilter.length; i++ ){
+ if ( t._selectFilter[i][0] == domain ){
+ if ( i > 0 && i <= t._selectFilter.length ) t._autoComplete.mailType = i;
+ t._selected = i;
+ break;
+ }
+ }
+ }
+ var mobile = P_INFO.split("|")[6] || "";
+ if (pInfo && t._username.value == "") {
+ t._username.value = pInfo;
+ t._autoComplete.refreshAutoList("silent");
+ t._autoComplete._autoList.hide();
+ }
+ if (pInfo && sInfo) {
+ t._loginForm.parentNode.removeChild(t._loginForm);
+ t._loginTipsMobile.parentNode.removeChild(t._loginTipsMobile);
+ t._loginTipsUsername.parentNode.removeChild(t._loginTipsUsername);
+ t._loginTipsPassword.parentNode.removeChild(t._loginTipsPassword);
+ t._loginBefore.parentNode.removeChild(t._loginBefore);
+ t._loginAfter.show();
+ var loginAfterUsername = $("#login_after_username");
+ var loginAfterSelect = $("#login_after_select");
+ var loginAfterLogout = $("#login_after_logout");
+ var links = loginAfterSelect.$("a");
+ var linksMail = loginAfterSelect.$("a.select-mail-link");
+ if(pInfo.split("@")[0].length <= 12){
+ loginAfterUsername.firstChild.nodeValue =loginAfterUsername.firstChild.nodeValue.replace(/pInfo/, pInfo);
+ }
+ else {
+ loginAfterUsername.firstChild.nodeValue =loginAfterUsername.firstChild.nodeValue.replace(/pInfo/, pInfo.substr(0,12)+'...');
+ }
+ links[0].href =links[0].href.replace(/pInfo/, pInfo);
+ links[8].href =links[8].href.replace(/pInfo/, pInfo);
+ loginAfterLogout.href =loginAfterLogout.href.replace(/accountName/, account);
+
+ if ( t._autoComplete.mailType == 0){
+ if ( domain == "popo.163.com" || domain == "gmail.com" || domain == "qq.com" || domain == "hotmail.com"){
+ for (var i = 0 ; i < links.length; i++){
+ if ( !($(links[i]).hasClass("popo-link")) ) {
+ links[i].parentNode.removeChild(links[i]);
+ }
+ }
+ }else{
+ for (var i = 0 ; i < links.length; i++){
+ if ( !($(links[i]).hasClass("others-link")) ) {
+ links[i].parentNode.removeChild(links[i]);
+ }
+ }
+ }
+ }else{
+ if( domain == "gmail.com" || domain == "qq.com" || domain == "hotmail.com"){
+ for (var i = 0 ; i < links.length; i++){
+ if ( !($(links[i]).hasClass("popo-link")) ) {
+ links[i].parentNode.removeChild(links[i]);
+ }
+ }
+ }else{
+ $(linksMail[t._autoComplete.mailType - 1]).addCss("user-link");
+ for (var i = 0 ; i < linksMail.length; i++){
+ if ( !($(linksMail[i]).hasClass("user-link")) ) {
+ linksMail[i].parentNode.removeChild(linksMail[i]);
+ }
+ }
+ }
+ };
+ loginAfterUsername.addEvent("click", function(e) {
+ e.preventDefault();
+ loginAfterSelect.toggle();
+ })
+ .addEvent("mouseover", function() {
+ if ( loginAfterSelect.showed() ){
+ TIMER && clearTimeout( TIMER );
+ loginAfterSelect.show();
+ }
+ })
+ .addEvent("mouseout", function() {
+ loginAfterSelect.hide(300);
+ })
+ loginAfterSelect.addEvent("mouseover", function() {
+ TIMER && clearTimeout( TIMER );
+ this.show();
+ })
+ .addEvent("mouseout", function() {
+ this.hide(300);
+ });
+ var pHead = NTES(t._product.$("h2")[0]);
+ var pBody = t._product.NTES(".bd");
+ var myList = pBody.NTES(" > div.tab-con > ul.product-list > li > span");
+ myList.each(function() {
+ var link = NTES(this).NTES("a")[0];
+ link.href = link.href.replace(/pInfo/, pInfo);
+ });
+ if ( t._autoComplete.mailType > 0 && domain != "qq.com" && domain != "gmail.com" && domain != "hotmail.com"){
+ var myMailLink = loginAfterSelect.$("a.user-link")[0].cloneNode(true);
+ var oldMailLink = NTES(myList[0]).$("a")[0];
+ myMailLink.className = oldMailLink.className;
+ oldMailLink.parentNode.removeChild(oldMailLink);
+ myMailLink.innerHTML = t._selectList[t._autoComplete.mailType].innerHTML;
+ myList[0].appendChild(myMailLink);
+ }
+ t._product.addCss("wgt-tab");
+ pHead.removeCss("hd");
+ pHead.addCss("tab-hd product-tab");
+ pHead.NTES(" > span").removeCss("mod-title");
+ pHead.NTES(" > span").addCss("tab-u");
+ pHead.NTES(" > span.tab-u").removeCss("current");
+ pBody.NTES(" > div.tab-con").removeCss("current");
+ NTES(pBody.NTES(" > div.tab-con")[1]).addCss("current");
+ var oSpan = document.createElement("span");
+ oSpan.innerHTML = "ÎÒµÄÍøÒ×";
+ oSpan.className = "tab-u current";
+ pHead.appendChild(oSpan);
+ } else {
+ if( mobile && mobile != "&0" ){
+ var mobileNum = mobile.substr(0,3) + "*****" + mobile.substr(3,3);
+ var closeBtn = t._loginTipsMobile.$("a")[1];
+ while (t._loginTipsMobile.firstChild) {
+ t._loginTipsMobile.removeChild(t._loginTipsMobile.firstChild);
+ }
+ var oSpan = document.createElement("span");
+ oSpan.innerHTML = "ÓÃÄãµÄÊÖ»úºÅÂë <span style=\"color:green\">" + mobileNum + "</span>" + " ¿ÉÒԵǼ ";
+ t._loginTipsMobile.appendChild(oSpan);
+ t._loginTipsMobile.appendChild(closeBtn);
+ };
+
+ if (t._selected == -1) t.setSelect("default", true);
+ else{
+ t.setSelect( t._selected, true );
+ $(t._selectList[t._selected]).addCss("hover");
+ }
+ if ( t._selected > 0 && t._selected < t._selectFilter.length ) t._autoComplete.mailType = t._selected;
+
+ t._username.datasetFix("state", "");
+ t._password.datasetFix("state", "");
+ t._submit.datasetFix("state", "");
+ t.showTipsMobile();
+ }
+
+ },
+ setCursorPosition : function(ctrl, pos){
+ if(ctrl.setSelectionRange)
+ {
+ ctrl.focus();
+ ctrl.setSelectionRange(pos,pos);
+ }
+ else if (ctrl.createTextRange) {
+ var range = ctrl.createTextRange();
+ range.collapse(true);
+ range.moveEnd('character', pos);
+ range.moveStart('character', pos);
+ range.select();
+ }
+ },
+ setSelectList : function() {
+ var t = this;
+ t._selectList.removeCss("disable");
+ if (t._username.value.trim() != ""){
+ for (var i = 0 ; i < t._selectList.length ; i++){
+ if (t._selectFilter[t._autoComplete.mailType][1][i] == 1){
+ $(t._selectList[i]).addCss("disable");
+ }
+ }
+ }
+ },
+ getSelect : function() {
+ var t = this;
+ var hoverItem = 0;
+ for (var i = 0 ; i < t._selectList.length; i++){
+ if ($(t._selectList[i]).hasClass("hover")){
+ hoverItem = i;
+ break;
+ }
+ };
+ return hoverItem;
+ },
+ setSelect : function(n, init) {
+ var t = this;
+ if ( n == "default"){
+ t._select.firstChild.nodeValue = t._selectDefault;
+ t._selected = -1;
+ return;
+ }
+ t._select.firstChild.nodeValue = t._selectList[n].firstChild.nodeValue;
+ t._selected = n;
+ if ( !init && t._username.value.trim() == "" && n > 0 && n < t._selectFilter.length){
+ t._username.value = "@" + t._selectFilter[n][0];
+ t._autoComplete.refreshAutoList();
+ t._autoComplete.mailType = n;
+ t.setCursorPosition(t._username, 0);
+ }
+ },
+ showTipsUsername : function() {
+ var t = this;
+ t._loginTipsUsername.innerHTML = t._username.value;
+ t._loginTipsUsername.show();
+ },
+ showTipsMobile : function() {
+ var t = this;
+ if ( t._tipsMobileEnable ){
+ t._loginTipsMobile.show();
+ };
+ },
+ checkInput : function(){
+ var t = this;
+ if( t._username.value.trim() === "" ) {
+ t._username.datasetFix("state", "error");
+ t._username.focus();
+ t._loginTipsUsername.innerHTML = "ÇëÊäÈëÕ˺Å";
+ t._loginTipsMobile.hide();
+ t._loginTipsUsername.show();
+ return false;
+ };
+ if( t._password.value === "" ) {
+ t._password.datasetFix("state", "error");
+ t._password.focus();
+ t._loginTipsPassword.innerHTML = "ÇëÊäÈëÃÜÂë";
+ t._loginTipsMobile.hide();
+ t._loginTipsPassword.show();
+ return false;
+ };
+ return true;
+ },
+ doSubmit : function(e) {
+ var t = this;
+ if( t.checkInput() ) {
+ t._loginForm.target = "_blank";
+ NTES.cookie.set("selectValue", t._selected, 30 * 24 * 60);
+ switch (parseInt(t._selected)) {
+ case -1:
+ t._username.name = "username";
+ t._password.name = "password";
+ t._loginForm.target = "_self";
+ t._loginForm.action = t._formRequestArray[0];
+ break;
+ case 0:
+ case 1:
+ t._username.name = "username";
+ t._password.name = "password";
+ t._loginForm.action = t._formRequestArray[t._selected];
+ break;
+ case 4:
+ t._username.name = "username";
+ t._password.name = "password";
+ t._loginForm.action = t._formRequestArray[t._selected];
+ break;
+ default:
+ t._username.name = "username";
+ t._password.name = "password";
+ t._ursname.name = "username";
+ t._ursname.value = t._username.value;
+ t._loginForm.action = t._formRequestArray[t._selected];
+ break;
+ };
+ t._loginForm.submit();
+ };
+ },
+ addTips : function(elem) {
+ var oSpan = document.createElement("span");
+ oSpan.className = "login-tips login-tips-username";
+ oSpan.id = "login_tips_username";
+ elem.appendChild(oSpan);
+ oSpan = document.createElement("span");
+ oSpan.className = "login-tips login-tips-password";
+ oSpan.id = "login_tips_password";
+ elem.appendChild(oSpan);
+ oSpan = document.createElement("span");
+ oSpan.className = "login-tips";
+ oSpan.id = "login_tips_mobile";
+ oSpan.innerHTML = "ÊÖ»úºÅÂëÒ²¿ÉÒԵǼ <a class=\"cBlue\" href="httpdisabled://www.163.com/%5C%22http://e.mail.163.com/mobilemail/home.do?from=163home%5C%22">Ãâ·Ñ°ó¶¨</a> <a class=\"login-tips-close\" href="httpdisabled://www.163.com/%5C%22#\"">¡Á</a>";
+ elem.appendChild(oSpan);
+ }
+
+ }
+
+ var NTESLoginObj = window.NTESLoginObj = new NTESLogin(NTES("#login_username"), NTES("#login_password"));
+})();
+//NTES Login End
+//widget tab
+(function () {
+ var slides = $(".wgt-tab"), slides_gg = $(".wgt-tab-gg"), i, wrapper;
+ //alert (slides);
+ for (i = slides.length - 1; i >= 0; i--) {
+ wrapper = $(slides[i]);
+ new NTES.ui.Slide(wrapper.$(".tab-u"), wrapper.$(".tab-con"), "current","mouseover",null, 150);
+ }
+ for (i = slides_gg.length - 1; i >= 0; i--) {
+ wrapper = $(slides_gg[i]);
+ new NTES.ui.Slide(wrapper.$(".tab-u"), wrapper.$(".tab-con"), "current","mouseover",null, 150);
+ }
+})();
+// ad for random
+(function () {
+ var AChange = function (obj) {
+ var t = this;
+ t.obj = obj ? obj : {};
+ var num = t.obj["num"] == "" ? 1 : Math.floor(Math.random()*t.obj["num"]) + 1;
+ var url = t.obj["temp"] + num + ".html";
+ var content = $(t.obj["content"]);
+ NTES.ajax.send(url, "GET", null, {
+ onSuccess: function(xhr){
+ content.innerHTML = xhr.responseText;
+ new NTES.ui.Slide(content.$("span.tab-u"), content.$("div.tab-con"), "current", "mouseover", 5000);
+ }
+ });
+ }
+ window.AChange = AChange;
+})();
+function isYdDefault(s){return false}
+function ydInputFocus(e){return false}
+function ydInputBlur(e){}
+function getSearchUrl(inputId, product, keyfrom) {
+ var url = "httpdisabled://" + product + ".youdao.com/";
+ if (window.RegExp && window.encodeURIComponent) {
+ var input = document.getElementById(inputId);
+ var query = input.value;
+ if (query != "") {
+ query = query.replace(/(^link:)|(^inlink:)|(^related:)/,"");
+ url = url + "search?q=" + encodeURIComponent(query) + "&keyfrom=" + keyfrom;
+ } else {
+ url = url + "?keyfrom=" + keyfrom;
+ }
+ }
+ return url;
+}
+function changeProduct(p) {
+ var url = getSearchUrl("query", p, "163.index");
+ void(url, "_blank");
+}
+//]]>
+</script>
+<script type="text/javascript" src="../img3.126.net/yodaoimages/pack.r091221/scripts/autocomplete.163.165290.js" charset="utf-8"></script>
+<script type="text/javascript">
+//<![CDATA[
+if(typeof(SEvent) != "undefined") {
+ var aa;
+ if (SEvent.observe) {
+ SEvent.observe(window, "loaddisabled", function() {
+ aa = new AutoComplete("query", "ydQuery", "aa", true, false, "", true, "httpdisabled://www.youdao.com");
+ aa.setSearchServer("httpdisabled://www.youdao.com/search?");
+ aa.setLogServer("httpdisabled://www.youdao.com/");
+ aa.setKeyFrom("163.index");
+ aa.showContent = function(){
+ var inputbox = document.getElementById("query");
+ var position = SP.cumOffset(inputbox);
+ this.sdiv.style.top = position[1] + inputbox.offsetHeight + "px";
+ if (this.bName == "IE" && this.bVer == 8) {
+ this.sdiv.style.left = (position[0] - 1) + "px";
+ } else {
+ this.sdiv.style.left = (position[0] + 1) + "px";
+ }
+ this.sdiv.style.cursor = "default";
+ this.sdiv.style.width = (inputbox.offsetWidth + 1) + "px";
+ SElement.show(this.sdiv);
+ this.vis = true;
+ this.curNodeIdx = -1;
+ }
+ aa.start();
+ });
+ }
+}
+//yodao keyword weather
+(function () {
+ var ydConfig = {
+ ipQuery: "httpdisabled://ip.ws.126.net/ipquery",
+ dataCity: "httpdisabled://www.163.com/special/0077450P/citycode.html",
+ dataUrl: "httpdisabled://www.163.com/inc/163new/youdao/tq_id_",
+ cityCode: "",
+ cityName: "",
+ defaultCity: "±±¾©",
+ defaultCode: "54511",
+ keywordUrl: "httpdisabled://www.163.com/inc/163new/youdao/hw.html"
+ };
+ var ydService = {
+ getWeather: function(url) {
+ var url = ydConfig.dataUrl + url + ".html";
+ NTES.ajax.importJs(url, function(){
+ if (typeof cityWeather == "undefined") {return false};
+ var tpl1 = '<span id="setChange" class="weather-location"><#=cityName#></span><span class="weather-temperature"><a href="httpdisabled://www.youdao.com/search?keyfrom=163.index.weather&q=<#=cityName#>ÌìÆø" title="<#=weatherInfo#> <#=temperature#>"><#=temperature#></a></span>'
+ var ele = $("#weatherIcon");
+ if (typeof ele.style.maxWidth == "undefined") {
+ var tpl2 = '<a title="<#=weatherInfo#> <#=temperature#>" href="httpdisabled://www.youdao.com/search?keyfrom=163.index.weather&q=<#=cityName#>ÌìÆø"><#=weatherInfo#></a>';
+ }
+ else {
+ var tpl2 = '<a title="<#=weatherInfo#> <#=temperature#>" href="httpdisabled://www.youdao.com/search?keyfrom=163.index.weather&q=<#=cityName#>ÌìÆø"><img src="httpdisabled://img3.126.net/yodaoimages/icons/weather2/<#=weatherImg1#>.png" width="65" height="40" alt="<#=weatherInfo#> <#=temperature#>" /></a>';
+ }
+ $("#weather").innerHTML = NTES.util.parseTpl(tpl1, cityWeather);
+ $("#weatherIcon").innerHTML = NTES.util.parseTpl(tpl2, cityWeather);
+ $("#wgt_weather").style.visibility = "visible";
+ $("#setChange").addEvent("click", function(e){
+ $("#ydAreas").style.display = $("#ydAreas").style.display == "block" ? "none" : "block";
+ e.preventDefault();
+ e.cancelBubble = true;
+ if($("#ydAreas").style.display == "block"){
+ document.onclick = function(){
+ $("#ydAreas").style.display = "none";
+ $("#categoryMore").style.display = "none";
+ }
+ }
+ $("#ydAreas").addEvent("click",function(e){
+ e.cancelBubble = true;
+ });
+ });
+ }, "gbk");
+ if($("#selectProvince option").length == "1"){
+ NTES.ajax.importJs(ydConfig.dataCity, function(){
+ var sel = $("#selectProvince");
+ var opt="";
+ var opt_txt = "";
+ sel.innerHTML = "<option>ÇëÑ¡Ôñ</option>";
+ var len = cityLibrary.length;
+ for (var i = 0; i < len; i++) {
+ opt = document.createElement("option");
+ opt_txt = document.createTextNode(cityLibrary[i][0]);
+ opt.appendChild(opt_txt);
+ opt.setAttribute("value",cityLibrary[i][0]);
+ sel.appendChild(opt);
+ }
+ sel.disabled = false;
+ });
+ }
+ $("#selectProvince").addEvent("change",function(){
+ var name = this.options[this.options.selectedIndex].value;
+ var len = cityLibrary.length;
+ var i = -1;
+ while (++i < len) {
+ if (cityLibrary[i][0] == name) {
+ var sel = $("#selectCity");
+ opt_txt = "";
+ sel.innerHTML = "<option>ÇëÑ¡Ôñ</option>";
+ var obj = cityLibrary[i][1];
+ var tlen = obj.length;
+ var result = [];
+ for (var j = 0; j < tlen; j++) {
+ opt = document.createElement("option");
+ opt_txt = document.createTextNode(obj[j][1]);
+ opt.appendChild(opt_txt);
+ opt.setAttribute("value",obj[j][0]);
+ sel.appendChild(opt);
+ }
+ sel.disabled = false;
+ }
+ }
+ });
+ },
+ init: function(){
+ ydConfig.cityCode = NTES.cookie.get("theCity");
+ if ("" == ydConfig.cityCode) {
+ NTES.ajax.importJs(ydConfig.ipQuery, function(){
+ ydConfig.cityName = "" !== lc ? lc.replace(/[Ê¡ÊÐ]$/, "") : lo.replace(/[Ê¡ÊÐ]$/, "");
+ if (!ydConfig.cityName) {
+ ydConfig.cityCode = ydConfig.defaultCode;
+ NTES.cookie.set("theCity", ydConfig.cityCode, "30d");
+ ydService.getWeather(ydConfig.cityCode);
+ }
+ else {
+ NTES.ajax.importJs(ydConfig.dataCity, function(){
+ var len = cityLibrary.length;
+ for (var i = 0; i < len; i++) {
+ if (cityLibrary[i][0] == ydConfig.cityName) {
+ ydConfig.cityCode = cityLibrary[i][1][0][0];
+ }
+ else {
+ var len1 = cityLibrary[i][1].length;
+ var j = -1;
+ while (++j < len1) {
+ if (cityLibrary[i][1][j][1] == ydConfig.cityName) {
+ ydConfig.cityCode = cityLibrary[i][1][j][0];
+ }
+ }
+ }
+ }
+ NTES.cookie.set("theCity", ydConfig.cityCode, "30d");
+ ydService.getWeather(ydConfig.cityCode);
+ }, "gb2312");
+ }
+ }, "gb2312");
+ }
+ else {
+ ydService.getWeather(ydConfig.cityCode);
+ }
+ $("#yodaoMore").addEvent("click",function(e){
+ $("#categoryMore").style.display = $("#categoryMore").style.display == "block" ? "none" : "block";
+ e.cancelBubble = true;
+ if($("#categoryMore").style.display == "block"){
+ document.onclick = function(){
+ $("#categoryMore").style.display = "none";
+ $("#ydAreas").style.display = "none";
+ }
+ }
+ });
+ $("#yodaoMore").addEvent("click",function(e){
+ e.cancelBubble = true;
+ });
+ $("#closeWeather").addEvent("click", function(e){
+ $("#ydAreas").style.display = "none";
+ e.preventDefault();
+ });
+ $("#ydaSubmit").addEvent("click", function(e){
+ var province = $("#selectProvince").value;
+ var city = $("#selectCity").value;
+ if ( city == "") {
+ alert("ÇëÑ¡ÔñÏàÓ¦µÄ³ÇÊÐ");
+ }
+ else {
+ NTES.cookie.set("theCity", city, "30d");
+ ydService.getWeather(city);
+ $("#ydAreas").style.display = "none";
+ }
+ e.preventDefault();
+ });
+ //end
+ //keywords
+ NTES.ajax.importJs(ydConfig.keywordUrl, function(){
+ var len = keywords.length;
+ var tpl = '<a href="httpdisabled://www.163.com/%3C#=url#>"><#=text#></a>';
+ var keyword1 = [], keyword2 = [], keyword3 = [];
+ for (var i = 0; i < len; i++) {
+ if (i < 3) {
+ keyword1.push(NTES.util.parseTpl(tpl, keywords[i]));
+ }
+ else if (i < 6){
+ keyword2.push(NTES.util.parseTpl(tpl, keywords[i]))
+ }
+ else if (i < 9){
+ keyword3.push(NTES.util.parseTpl(tpl, keywords[i]))
+ }
+ }
+ $("#ydHotKeys").innerHTML = keyword1.join(" ");
+ $("#ydHotKeys").style.visibility = "visible";
+ result = [];
+ var flag = 1;
+ setInterval(function(){
+ if (flag == 1) {
+ $("#ydHotKeys").innerHTML = keyword1.join(" ");
+ flag = 2;
+ }
+ else if (flag == 2) {
+ $("#ydHotKeys").innerHTML = keyword2.join(" ");
+ flag = 3;
+ }
+ else if (flag == 3){
+ $("#ydHotKeys").innerHTML = keyword3.join(" ");
+ flag = 1;
+ }
+ }, 8000);
+ }, "gbk");
+ }
+ };ydService.init();
+ })();
+//house ip check
+(function(){
+ var HouseConfig = {
+ ip: "httpdisabled://ip.ws.126.net/ipquery",
+ textHouse: "·¿²ú",
+ textBuy: "Âò·¿",
+ city: [{
+ name: "¹ãÖÝ",
+ url1: "httpdisabled://gz.house.163.com/",
+ url2: "httpdisabled://xf.house.163.com/gz/",
+ src1: "/special/00774IHD/house_gz_01.html",
+ src2: "/special/00774IHD/house_gz_02.html"
+ }, {
+ name: "·ðɽ",
+ url1: "httpdisabled://fs.house.163.com/",
+ url2: "httpdisabled://house.163.com/",
+ src1: "/special/00774IHD/house_fs_01.html",
+ src2: "/special/00774IHD/house_gz_02.html"
+ }, {
+ name: "Ö麣",
+ url1: "httpdisabled://zh.house.163.com/",
+ url2: "httpdisabled://house.163.com/",
+ src1: "/special/00774IHD/house_zh_01.html",
+ src2: "/special/00774IHD/house_gz_02.html"
+ }, {
+ name: "°ÄÃÅ",
+ url1: "httpdisabled://zh.house.163.com/",
+ url2: "httpdisabled://house.163.com/",
+ src1: "/special/00774IHD/house_zh_01.html",
+ src2: "/special/00774IHD/house_gz_02.html"
+ }, {
+ name: "ÉîÛÚ",
+ url1: "httpdisabled://sz.house.163.com/",
+ url2: "httpdisabled://xf.house.163.com/sz/",
+ src1: "/special/00774IHD/house_sz_01.html",
+ src2: "/special/00774IHD/house_sz_02.html"
+ }, {
+ name: "ÕØÇì",
+ url1: "httpdisabled://zq.house.163.com/",
+ url2: "httpdisabled://house.163.com/",
+ src1: "/special/00774IHD/house_zq_01.html",
+ src2: "/special/00774IHD/house_gz_02.html"
+ }, {
+ name: "ºÏ·Ê",
+ url1: "httpdisabled://hf.house.163.com/",
+ url2: "httpdisabled://xf.house.163.com/hf/search/0-0-0-0-0-0-0-0-1.html",
+ src1: "/special/00774IHD/house_hf_01.html",
+ src2: "/special/00774IHD/house_hf_02.html"
+ }, {
+ name: "±±¾©",
+ url1: "httpdisabled://bj.house.163.com/",
+ url2: "httpdisabled://xf.house.163.com/bj/",
+ src1: "/special/00774IHD/house_bj_01.html",
+ src2: "/special/00774IHD/house_bj_02.html"
+ }, {
+ name: "º£¿Ú",
+ url1: "httpdisabled://hn.house.163.com/",
+ url2: "httpdisabled://house.163.com/",
+ src1: "/special/00774IHD/house_hn_01.html",
+ src2: "/special/00774IHD/house_gz_02.html"
+ }, {
+ name: "ÈýÑÇ",
+ url1: "httpdisabled://hn.house.163.com/",
+ url2: "httpdisabled://house.163.com/",
+ src1: "/special/00774IHD/house_hn_01.html",
+ src2: "/special/00774IHD/house_gz_02.html"
+ }, {
+ name: "Ö£ÖÝ",
+ url1: "httpdisabled://zz.house.163.com/",
+ url2: "httpdisabled://xf.house.163.com/zz/search/0-0-0-0-0-0-0-0-1.html",
+ src1: "/special/00774IHD/house_zz_01.html",
+ src2: "/special/00774IHD/house_zz_02.html"
+ }, {
+ name: "Î人",
+ url1: "httpdisabled://wh.house.163.com/",
+ url2: "httpdisabled://xf.house.163.com/wh/search/0-0-0-0-0-0-0-0-1.html",
+ src1: "/special/00774IHD/house_wh_01.html",
+ src2: "/special/00774IHD/house_wh_02.html"
+ }, {
+ name: "Î÷°²",
+ url1: "httpdisabled://xa.house.163.com/",
+ url2: "httpdisabled://xf.house.163.com/xa/search/0-0-0-0-0-0-0-0-1.html",
+ src1: "/special/00774IHD/house_xa_01.html",
+ src2: "/special/00774IHD/house_xa_02.html"
+ }, {
+ name: "ÉϺ£",
+ url1: "httpdisabled://sh.house.163.com/",
+ url2: "httpdisabled://xf.house.163.com/sh/",
+ src1: "/special/00774IHD/house_sh_01.html",
+ src2: "/special/00774IHD/house_sh_02.html"
+ }, {
+ name: "Ìì½ò",
+ url1: "httpdisabled://tj.house.163.com/",
+ url2: "httpdisabled://xf.house.163.com/tj/search/0-0-0-0-0-0-0-0-1.html",
+ src1: "/special/00774IHD/house_tj_01.html",
+ src2: "/special/00774IHD/house_tj_02.html"
+ }, {
+ name: "ÖØÇì",
+ url1: "httpdisabled://cq.house.163.com/",
+ url2: "httpdisabled://xf.house.163.com/cq/search/0-0-0-0-0-0-0-0-1.html",
+ src1: "/special/00774IHD/house_cq_01.html",
+ src2: "/special/00774IHD/house_cq_02.html"
+ }, {
+ name: "º¼ÖÝ",
+ url1: "httpdisabled://hz.house.163.com/",
+ url2: "httpdisabled://xf.house.163.com/hz/search/0-0-0-0-0-0-0-0-1.html",
+ src1: "/special/00774IHD/house_hz_01.html",
+ src2: "/special/00774IHD/house_hz_02.html"
+ }, {
+ name: "ÉòÑô",
+ url1: "httpdisabled://sy.house.163.com/",
+ url2: "httpdisabled://xf.house.163.com/sy/search/0-0-0-0-0-0-0-0-1.html",
+ src1: "/special/00774IHD/house_sy_01.html",
+ src2: "/special/00774IHD/house_sy_02.html"
+ }, {
+ name: "´óÁ¬",
+ url1: "httpdisabled://dl.house.163.com/",
+ url2: "httpdisabled://dl.house.163.com/more/index4-1.shtml",
+ src1: "/special/00774IHD/house_dl_01.html",
+ src2: "/special/00774IHD/house_dl_02.html"
+ }, {
+ name: "ÄϾ©",
+ url1: "httpdisabled://nj.house.163.com/",
+ url2: "httpdisabled://xf.house.163.com/nj/search/0-0-0-0-0-0-0-0-1.html",
+ src1: "/special/00774IHD/house_nj_01.html",
+ src2: "/special/00774IHD/house_nj_02.html"
+ }, {
+ name: "ËÕÖÝ",
+ url1: "httpdisabled://suzhou.house.163.com/",
+ url2: "httpdisabled://xf.house.163.com/suzhou/search/0-0-0-0-0-0-0-0-1.html",
+ src1: "/special/00774IHD/house_suzhou_01.html",
+ src2: "/special/00774IHD/house_suzhou_02.html"
+ }],
+ defaultCity: {//ĬÈÏΪ¹ãÖÝ
+ name: "¹ãÖÝ",
+ url1: "httpdisabled://gz.house.163.com/",
+ url2: "httpdisabled://xf.house.163.com/gz/",
+ src1: "/special/00774IHD/house_gz_01.html",
+ src2: "/special/00774IHD/house_gz_02.html"
+ }
+ }
+ NTES.ajax.importJs(HouseConfig.ip, function(){
+
+ var len = HouseConfig.city.length;
+ var locName = "", buyName = HouseConfig.textBuy, locNameUrl = "", buyNameUrl = "", loaddisabledCon1 = "", loaddisabledCon2 = "";
+ var i = -1;
+ while (++i < len) {
+ if (localAddress.city.indexOf(HouseConfig.city[i].name) != -1 || localAddress.province.indexOf(HouseConfig.city[i].name) != -1) {
+ locName = HouseConfig.city[i].name + HouseConfig.textHouse;
+ locNameUrl = HouseConfig.city[i].url1;
+ buyNameUrl = HouseConfig.city[i].url2;
+ loaddisabledCon1 = HouseConfig.city[i].src1;
+ loaddisabledCon2 = HouseConfig.city[i].src2;
+ }
+ }
+ if (locName == "" && locNameUrl == "") {
+ locName = HouseConfig.defaultCity.name + HouseConfig.textHouse;
+ locNameUrl = HouseConfig.defaultCity.url1;
+ buyNameUrl = HouseConfig.defaultCity.url2;
+ loaddisabledCon1 = HouseConfig.defaultCity.src1;
+ loaddisabledCon2 = HouseConfig.defaultCity.src2;
+ }
+ var tab_1 = NTES("#house .tab-u a")[0];
+ var tab_2 = NTES("#house .tab-u a")[1];
+ tab_1.innerHTML = locName;
+ tab_1.href = locNameUrl;
+ tab_2.innerHTML = buyName;
+ tab_2.href = buyNameUrl;
+
+ NTES.ajax.send(loaddisabledCon1, "GET", null, {
+ onSuccess: function(xhr){
+ $("#house div.tab-con")[0].innerHTML = xhr.responseText;
+ }
+ });
+ NTES.ajax.send(loaddisabledCon2, "GET", null, {
+ onSuccess: function(xhr){
+ $("#house div.tab-con")[1].innerHTML = xhr.responseText;
+ }
+ });
+
+ });
+})();
+//end
+//shop.163.com
+var Mall = {};
+Mall.gId = function(id){return document.getElementById(id)};
+Mall.checkPhone = function(num){
+ if(/^13\d{9}$/.test(num)||(/^15[0-35-9]\d{8}$/.test(num))||(/^18\d{9}$/.test(num))){return true};return false;
+} ;
+Mall.init = function(){
+ this.issubmit = true;
+ var rep = /[^\d]/;
+ Mall.gId("mall_phone").onkeyup = function(){this.value = this.value.replace(rep,"");};
+ Mall.gId("mall_phone").onblur = function(){
+ if(!Mall.checkPhone(this.value)){
+ this.style.borderColor = "#DC3A3B";Mall.issubmit = false;Mall.gId("phone_err").style.display = "";
+ }
+ else{this.style.borderColor = "#86A2BD";Mall.issubmit = true;Mall.gId("phone_err").style.display = "none";}
+ };
+ var arr = ["29.55-30.15","49.35-49.9","98.7-99.7","296.1-300.3","493.5-498"];
+ Mall.gId("mall_value").onchange = function(){
+ Mall.gId("mall_money").innerHTML = arr[this.selectedIndex];
+ };
+
+ Mall.gId("neteasy_mall_form").onsubmit = function(){
+ Mall.gId("mall_phone").onblur();
+ if(!Mall.issubmit){return false;}
+ };
+};
+Mall.init();
+//update news
+(function(){
+ var UpdateNews = function(option) {
+ var t = this;
+ t.option = option ? option : {};
+ var nowTime = "2011-04-09 05:51:01"
+ t.nowTime = nowTime.split(" ")[1].substring(0,2);
+ if (NTES.cookie.get(t.option["cookieName"]) == "") {
+ var cookieStr = [];
+ cookieStr.push("00");
+ }
+ else{
+ var cookieStr = NTES.cookie.get(t.option["cookieName"]).split("|");
+ cookieStr.shift();
+ }
+ cookieStr.push(t.nowTime);
+ NTES.cookie.set(t.option["cookieName"], cookieStr.join("|"), "1d");
+ if (cookieStr[0] > cookieStr[1]) {
+ cookieStr[0] = 0;
+ }
+ else if (cookieStr[0] == cookieStr[1]) {
+ NTES(t.option["infoSelect"]).attr("innerHTML",t.option["infotxt1"]);
+ NTES(t.option["btnSelect"]).attr("innerHTML",t.option["btntxt1"]);
+ return false;
+ }
+ var aElements = NTES("a[data-t-h]"), newElements = [];
+ for (var i = 0; i < aElements.length; i++) {
+ var tmp = aElements[i].getAttribute("data-t-h");
+ if( cookieStr[0] <= tmp && tmp <= cookieStr[1])
+ newElements.push(aElements[i]);
+ }
+ t.closeUpdate(cookieStr, newElements);
+ if(newElements.length != "0"){
+ NTES(t.option["btnSelect"]).style.visibility="visible";
+ NTES(t.option["btnSelect"]).addEvent("click", function() {
+ if (t.option["btntxt1"] == this.innerHTML) {
+ t.showUpdate(cookieStr, newElements);
+ }
+ else {
+ t.closeUpdate(cookieStr, newElements);
+ }
+ });
+ }
+ };
+ UpdateNews.prototype = {
+ showUpdate: function(cookieStr, newElements) {
+ if(newElements.length != "0"){
+ var t = this;
+ NTES.each(newElements, function(){NTES.style.addCss(this,"new")});
+ NTES(t.option["infoSelect"]).attr("innerHTML",t.option["infotxt2"]);
+ NTES(t.option["btnSelect"]).attr("innerHTML",t.option["btntxt2"]);
+ NTES(t.option["btnSelect"]).style.visibility = "visible";
+ }
+ },
+ closeUpdate: function(cookieStr, newElements) {
+ var t = this;
+ var str;
+ if(newElements.length == "0"){return false;};
+ if(cookieStr[0] == "00" ){
+ str = t.option["infotxt3"];
+ }
+ else{
+ str = t.option["infotxt4"];
+ }
+ str = str.replace("NEWNUM",newElements.length);
+ NTES.each(newElements, function(){NTES.style.removeCss(this,"new")});
+ NTES(t.option["infoSelect"]).attr('innerHTML', str);
+ NTES(t.option["btnSelect"]).attr("innerHTML",t.option["btntxt1"]);
+ }
+ }
+ window.UpdateNews = UpdateNews;
+})();
+NTES.ready( function(){
+ var updateNews = new UpdateNews({
+ cookieName: "updateRange",
+ infoSelect: "#updateInfo",
+ btnSelect: "#updateBtn",
+ infotxt1: '´ÓÄúÉϴηÃÎʵ½ÏÖÔÚ¸üР<span class="fB cDRed">0</span> Ìõ×ÊѶ',
+ infotxt2: "ÓÐÏ»®ÏßÌáʾΪ×îÐÂ×ÊѶ",
+ infotxt3: '´Ó0µãµ½ÏÖÔÚ¸üР<span class="fB cDRed">NEWNUM</span> Ìõ×ÊѶ',
+ infotxt4: '´ÓÄúÉϴηÃÎʵ½ÏÖÔÚ¸üР<span class="fB cDRed">NEWNUM</span> Ìõ×ÊѶ',
+ btntxt1: "ÏÔʾ",
+ btntxt2: "¹Ø±Õ"
+ });
+});
+//]]>
+</script>
+<div id="ssid1"></div>
+<!-- È«ÆÁÊÕËõ begin -->
+<!-- <script type="text/javascript" src="httpdisabled://popme.163.com/js/nadScreenFloat2011_1.js"></script>
+<script type="text/javascript">//<![CDATA[
+function shownad(){
+ new nadScreenFloat("httpdisabled://img1.126.net/channel5/009396/audi750550_110401.jpg",{
+ type : "image",
+ href : "httpdisabled://g.163.com/a?CID=6678&Values=1009526041&Redirect=http://as.kejet.com/afaclick?u/MDE3MDU1NjUyQUU3MkIy/o/MENGOURGRDlCRUNEN0E3/m/MzlGQTEwMTAzNDVBNDQ1?http://www.audi.cn/cn/brand/zh/financial_products/financial/finance_A3.html",
+ playFunc : function(){
+ //document.getElementById("nad2234Home").parentNode.style.visibility= "visible";
+ }
+ });
+}
+var timeOut = setTimeout("shownad()",1);
+</script> -->
+<!-- È«ÆÁÊÕËõ end -->
+<!-- 2010Ê×Ò³ÂÖÌæ¶ÔÁª -->
+<script>
+ var sydl=0;
+ var coupletTop = 120;
+ var rdmdl=Math.floor(Math.random()*3)+1;
+if(rdmdl==1)
+{
+ var coupletLeftUrl = 'http://img1.126.net/channel4/008972/mai20300la_0325.swf';
+ var coupletRightUrl = 'http://img1.126.net/channel4/008972/mai20300ra_0325.swf';
+ var coupletLeftUrlb = 'http://img1.126.net/channel5/008981/mai110300l_110407.swf';
+ var coupletRightUrlb = 'http://img1.126.net/channel5/008981/mai110300r_110407.swf';
+ var sydl=1;
+}
+if(rdmdl==2)
+{
+ var coupletLeftUrl = 'http://img1.126.net/channel5/008981/mbb20300l_110331.swf';
+ var coupletRightUrl = 'http://img1.126.net/channel5/008981/mbb20300r_110331.swf';
+ var coupletLeftUrlb = 'http://img1.126.net/channel5/008981/mbb110300l_110331.swf';
+ var coupletRightUrlb = 'http://img1.126.net/channel5/008981/mbb110300r_110331.swf';
+ var sydl=1;
+}
+if(rdmdl==3)
+{
+ var coupletLeftUrl = 'http://img1.126.net/channel5/008981/bl20300l_110331.swf';
+ var coupletRightUrl = 'http://img1.126.net/channel5/008981/bl20300r_110331.swf';
+ var coupletLeftUrlb = 'http://img1.126.net/channel5/008981/bl110300l_110331.swf';
+ var coupletRightUrlb = 'http://img1.126.net/channel5/008981/bl110300r_110331.swf';
+ var sydl=1;
+}
+if(sydl==1)
+{
+void('<scr'+'ipt type="text/javascript" src="httpdisabled://img1.126.net/channel7/js/duilian_2011n.js"></scr'+'ipt>');
+}
+</script>
+<!-- 2010Ê×Ò³ÂÖÌæ¶ÔÁª -->
+<SCRIPT LANGUAGE="JavaScript1.1" SRC="../pro.163.com/js.ng/site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=flash&amp;location=1"></SCRIPT>
+<SCRIPT LANGUAGE="JavaScript1.1" SRC="../g.163.com/jr@site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=popup&amp;location=1"></SCRIPT>
+<img src='../adgeo.163.com/ad_cookies' width="0" height="0">
+<!-- ¸¡²ã -->
+<!-- µ×²¿µ¯´° -->
+<SCRIPT LANGUAGE="JavaScript1.1" SRC="../g.163.com/jr@site=netease&amp;affiliate=homepage&amp;cat=homepage&amp;type=adend&amp;location=1"></SCRIPT>
+<!-- START WRating v1.0 -->
+<script type="text/javascript" src="../163.wrating.com/a1.js">
+</script>
+<script type="text/javascript">
+var vjAcc="860010-0503010000";
+var wrUrl="httpdisabled://163.wrating.com/";
+vjTrack("");
+</script>
+<noscript><img src="../163.wrating.com/a.gif@a=&amp;c=860010-0503010000" width="1" height="1"/></noscript>
+<!-- END WRating v1.0 -->
+<!-- START NetEase Devilfish 2006 -->
+<script src="../analytics.163.com/ntes.js" type="text/javascript"></script>
+<script type="text/javascript">
+_ntes_nacc = "www";
+neteaseTracker();
+neteaseClickStat();
+</script>
+<!-- END NetEase Devilfish 2006 -->
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/www.163.com/mediav.gif b/mobile/android/tests/browser/chrome/tp5/163.com/www.163.com/mediav.gif
new file mode 100755
index 0000000000..0f63363515
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/www.163.com/mediav.gif
@@ -0,0 +1,8 @@
+<html><head><meta http-equiv="cache-control" content="no-cache"></head><body>
+<SCRIPT LANGUAGE="Javascript">
+mvas_43392=36442;mv_acquire=1;mv_bid=43392;
+mvcu_43392='http://show.mediav.com/c?type=2&db=mediav&impid=FW9XAHo9veWj&pub=118_5273_36442&cus=189_1757_19945_43392_0&ref=&url=http://a1410.oadz.com/link/C/1410/522442/.-WeASW6LJ46VaLGFhf4K8Gv7lo_/p021/185/http://www.yksuit.com/?aid=483719';
+</SCRIPT>
+
+<SCRIPT>eval('mediav_fini'+mvas_43392+'=1');var mediav_fini2010010688=1;</SCRIPT>
+<iframe style="display:none" src="http://material.mediav.com/ckmap.htm#prefix=http://audit.wrating.com/a.gif&a=1&c=860010-3000005801&mvck=0O7psfuGKe_9FwVoR1AeWU=="></iframe> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/163.com/zjs.ipinyou.com/2011032517331513260_2342_190180.js b/mobile/android/tests/browser/chrome/tp5/163.com/zjs.ipinyou.com/2011032517331513260_2342_190180.js
new file mode 100755
index 0000000000..7526f6978c
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/163.com/zjs.ipinyou.com/2011032517331513260_2342_190180.js
@@ -0,0 +1 @@
+window.onerror=function(){return true};var toprefer="m_ZJSTAT";var parentlocation="";var parentrefer="m_ZJSTAT";var selflocation=window.location;var selfrefer=document.referrer;var realrefer="";var reallocation="";var hourvisitnum=1;var realvisitnum=1;var nowdate=new Date();var clientcolor="";if (navigator.appName=="Netscape"){clientcolor=screen.pixelDepth;}else {clientcolor=screen.colorDepth;}hourvisitnum=document.cookie.match(new RegExp("(^| )m_ZJSTAT_PAGES=([^;]*)(;|$)"));hourvisitnum=(hourvisitnum==null)?1: (parseInt(unescape((hourvisitnum)[2]))+1);var currentdate =new Date();currentdate.setTime(currentdate.getTime()+60*60*1000);document.cookie="m_ZJSTAT_PAGES="+hourvisitnum+ ";path=/;expires="+currentdate.toGMTString();realvisitnum=document.cookie.match(new RegExp("(^| )m_ZJSTAT_TIMES=([^;]*)(;|$)"));if(realvisitnum==null){realvisitnum=1;}else{ realvisitnum=parseInt(unescape((realvisitnum)[2])); realvisitnum=(hourvisitnum==1)?(realvisitnum+1):(realvisitnum);}currentdate.setTime(currentdate.getTime()+365*24*60*60*1000);document.cookie="m_ZJSTAT_TIMES="+realvisitnum+";path=/;expires="+currentdate.toGMTString();realrefer=selfrefer;if(parentrefer!=="m_ZJSTAT"){realrefer=parentrefer;}if(toprefer!=="m_ZJSTAT"){realrefer=toprefer;} reallocation=parentlocation;try{lainframe}catch(e){reallocation=selflocation;}void('<iframe target="_blank" src="httpdisabled://www.ipinyou.com/collect.jsp?mediaId=13472&stationId=13260&adPlaceId=2342&collectCode=2342&collectCodeType=2&hourVisitNum='+hourvisitnum+'&realVisitNum='+realvisitnum+'&zone='+(0-nowdate.getTimezoneOffset()/60)+'&screenColor='+clientcolor+'&screen='+screen.width+','+screen.height+'&referUrl='+escape(realrefer)+'&url='+escape(reallocation)+'" width="190" height="180" frameborder="0" scrolling="no" marginwidth="0" marginheight="0"></iframe>'); \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/README b/mobile/android/tests/browser/chrome/tp5/README
new file mode 100644
index 0000000000..c733fb4c03
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/README
@@ -0,0 +1 @@
+This directory contains pages and other resources downloaded from the web for the purpose of testing against pages from the real world. Pages are copied from the Talos tp5 data -- see https://wiki.mozilla.org/Buildbot/Talos/Tests#tp5. These files are not made available under an open source license.
diff --git a/mobile/android/tests/browser/chrome/tp5/baidu.com/c.baidu.com/c.gif@t=0&q=mozilla&p=0&pn=1.html b/mobile/android/tests/browser/chrome/tp5/baidu.com/c.baidu.com/c.gif@t=0&q=mozilla&p=0&pn=1.html
new file mode 100755
index 0000000000..e69de29bb2
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/baidu.com/c.baidu.com/c.gif@t=0&q=mozilla&p=0&pn=1.html
diff --git a/mobile/android/tests/browser/chrome/tp5/baidu.com/open.baidu.com/stat/image/Icon_Aladdin.gif b/mobile/android/tests/browser/chrome/tp5/baidu.com/open.baidu.com/stat/image/Icon_Aladdin.gif
new file mode 100755
index 0000000000..6e1d27f897
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/baidu.com/open.baidu.com/stat/image/Icon_Aladdin.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/aladdin/img/table/bg.gif b/mobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/aladdin/img/table/bg.gif
new file mode 100755
index 0000000000..dd7f760484
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/aladdin/img/table/bg.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/img/arr.gif b/mobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/img/arr.gif
new file mode 100755
index 0000000000..c466d496b8
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/img/arr.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/img/baidu_jgylogo1.gif b/mobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/img/baidu_jgylogo1.gif
new file mode 100755
index 0000000000..e1d5d3714e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/img/baidu_jgylogo1.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/img/i2.png b/mobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/img/i2.png
new file mode 100755
index 0000000000..85b8e9683c
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/img/i2.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/js/bdsug.js@v=1.0.3.0 b/mobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/js/bdsug.js@v=1.0.3.0
new file mode 100755
index 0000000000..dab5c2a400
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/js/bdsug.js@v=1.0.3.0
@@ -0,0 +1 @@
+(function(){var M=navigator.userAgent.indexOf("MSIE")!=-1&&!window.opera;var V=(document.compatMode=="BackCompat");function I(C){return document.getElementById(C)}function K(C){return document.createElement(C)}function S(C){return String(C).replace(new RegExp("(^[\\s\\t\\xa0\\u3000]+)|([\\u3000\\xa0\\s\\t]+\x24)","g"),"")}function U(C){return String(C).replace(new RegExp("[\\s\\t\\xa0\\u3000]","g"),"")}function P(G,X,C){if(M){G.attachEvent("on"+X,(function(Y){return function(){C.call(Y)}})(G))}else{G.addEventListener(X,C,false)}}function N(C){if(M){C.returnValue=false}else{C.preventDefault()}}function R(X){if(M){var G=document.createStyleSheet();G.cssText=X}else{var C=document.createElement("style");C.type="text/css";C.appendChild(document.createTextNode(X));document.getElementsByTagName("HEAD")[0].appendChild(C)}}function H(G){var X=document.forms[0];for(var Y in G){if(G[Y]==undefined){if(I("bdsug_ipt_"+Y)){X.removeChild(I("bdsug_ipt_"+Y))}}else{if(!O(Y)){X.appendChild(C(Y,G[Y]))}else{O(Y).value=G[Y]}}}function C(Z,b){var a=K("INPUT");a.type="hidden";a.name=Z;a.id="bdsug_ipt_"+Z;a.value=b;return a}}function O(Y){var X=document.forms[0];var G=false;var C=X.getElementsByTagName("INPUT");for(var Z=0;Z<C.length;Z++){if(Y==C[Z].getAttribute("name")){G=C[Z];return G}else{G=false}}}function L(G){var X=document.forms[0];for(var C in G){if(C=="f"){if(O("f")){if(O("f").id=="bdsug_ipt_f"){X.removeChild(I("bdsug_ipt_f"))}else{O("f").value="8"}}}else{if(I("bdsug_ipt_"+C)){X.removeChild(I("bdsug_ipt_"+C))}}}}var A=0;if(typeof window.bdsug!="object"||window.bdsug==null){window.bdsug={}}bdsug.sug={};bdsug.sugkeywatcher={};var J=(function(){function C(b){var Z=this.__MSG_QS__;if(!Z[b]){Z[b]=[]}for(var a=1,X=arguments.length,Y;a<X;a++){Z[b].push(arguments[a])}}function G(Y){var Z=this.__MSG_QS__[Y.type];if(Z==null){return }for(var a=0,X=Z.length;a<X;a++){Z[a].rm(Y)}}return{ini:function(X){X.__MSG_QS__={};X.on=C;X.dm=G;return X}}})();var F=(function(){var X=I("kw");var f;var i=0;var C=0;var d="";var Y="";var c;var k=false;var a=true;var h;function Z(){if(a){A=new Date().getTime();F.dm({type:"start"});a=false}}function e(o){if(a){A=new Date().getTime();F.dm({type:"start"});a=false}o=o||window.event;if(o.keyCode==9||o.keyCode==27){F.dm({type:"hide_div"})}if(o.keyCode==13){N(o);F.dm({type:"key_enter"})}if(o.keyCode==86&&o.ctrlKey){H({n:2})}if(f.style.display!="none"){if(o.keyCode==38){N(o);F.dm({type:"key_up"})}if(o.keyCode==40){F.dm({type:"key_down"})}}else{if(o.keyCode==38||o.keyCode==40){F.dm({type:"need_data",wd:X.value})}}}function l(){var o=X.value;if(o==d&&o!=""&&o!=Y&&o!=c){if(C==0){C=setTimeout(function(){F.dm({type:"need_data",wd:o})},100)}}else{clearTimeout(C);C=0;d=o;if(o==""){F.dm({type:"hide_div"})}if(Y!=X.value){Y=""}}}function m(){i=setInterval(l,10)}function g(){clearInterval(i)}function j(){if(k){window.event.cancelBubble=true;window.event.returnValue=false;k=false}}function b(o){X.blur();X.setAttribute("autocomplete",o);X.focus()}function G(o){var o=o||window.event;if(o.keyCode==13){N(o)}}X.setAttribute("autocomplete","off");var n=false;bdsug.sugkeywatcher.on=function(){if(!n){if(M){X.attachEvent("onkeydown",e)}else{X.addEventListener("keydown",e,false)}n=true}};bdsug.sugkeywatcher.off=function(){if(n){if(M){X.detachEvent("onkeydown",e)}else{X.removeEventListener("keydown",e,false)}n=false}};bdsug.sugkeywatcher.on();P(X,"mousedown",Z);P(X,"beforedeactivate",j);if(window.opera){P(X,"keypress",G)}return J.ini({rm:function(o){switch(o.type){case"div_ready":f=o.sdiv;Y=X.value;m();break;case"clk_submit":g();X.blur();X.value=o.wd;break;case"ent_submit":g();X.blur();break;case"key_select":c=o.selected;break;case"close":g();b("on");break;case"mousedown_tr":if(navigator.userAgent.toLowerCase().indexOf("webkit")!=-1){g();setTimeout(m,2000)}k=true;break}}})})();var W=(function(){var h;var a=I("kw");var l;var d=-1;var C;var m;var o;function n(){var r=l.rows;for(var q=0;q<r.length;q++){r[q].className="ml"}}function e(){if(typeof (l)!="undefined"&&l!=null&&h.style.display!="none"){var r=l.rows;for(var q=0;q<r.length;q++){if(r[q].className=="mo"){return[q,r[q].cells[0].innerHTML]}}}return[-1,""]}function i(){if(M){o.style.display="none"}h.style.display="none"}function G(){n();this.className="mo"}function b(q){W.dm({type:"mousedown_tr"});if(!M){q.stopPropagation();q.preventDefault();return false}}function c(q){var r=q;return function(){var s=C[r];i();W.dm({type:"clk_submit",oq:I("kw").value,wd:s,rsp:r})}}function f(q){q=q||window.event;N(q);W.dm({type:"close"});i();(new Image()).src="httpdisabled://sclick.baidu.com/w.gif?fm=suggestion&title=%B9%D8%B1%D5&t="+new Date().getTime()}function X(){var q=[a.offsetWidth,a.offsetHeight];h.style.width=((M&&V)?q[0]:q[0]-2)+"px";h.style.top=((M&&V)?q[1]:q[1]-1)+"px";h.style.display="block";if(M){o.style.top=((M&&V)?q[1]:q[1]-1)+"px";o.style.width=((M&&V)?q[0]:q[0]-2)+"px"}}function Y(r,q){if(r&&q){var s=S(r);if(q.indexOf(s)==0){q=p(q,s)}else{if(q.indexOf(U(r))==0){s=U(r);q=p(q,s)}else{}}}q=q.replace("&","&amp;");return q}function p(q,s){var t="<span>"+s+"</span>";var u=s.length;var r="<b>"+q.substring(u)+"</b>";return(t+r)}function j(){l=K("TABLE");l.id="st";l.cellSpacing=0;l.cellPadding=2;var s=K("tbody");l.appendChild(s);for(var t=0,u=C.length;t<u;t++){var r=s.insertRow(-1);P(r,"mouseover",G);P(r,"mouseout",n);P(r,"mousedown",b);P(r,"click",c(t));var q=r.insertCell(-1);q.innerHTML=Y(m,C[t])}h.innerHTML="";h.appendChild(l);X();if(M){o.style.display="block";o.style.left=0+"px";o.style.top=a.offsetHeight+"px";o.style.width=a.offsetWidth+"px";o.style.height=h.offsetHeight-10+"px"}}function Z(){d=e()[0];if(d==-1){W.dm({type:"submit"})}else{W.dm({type:"ent_submit",oq:m,wd:e()[1],rsp:d})}}function k(){d=e()[0];n();if(d==0){W.dm({type:"key_select",selected:""});I("kw").value=m;d--;L({oq:m,sug:C[d],n:1,rsp:d,f:3,rsv_sug:rsv_sug})}else{if(d==-1){d=C.length}d--;var q=l.rows[d];q.className="mo";W.dm({type:"key_select",selected:C[d]});I("kw").value=C[d];H({oq:m,sug:C[d],n:1,rsp:d,f:3,rsv_sug:rsv_sug})}}function g(){d=e()[0];n();if(d==C.length-1){W.dm({type:"key_select",selected:""});I("kw").value=m;d=-1;L({oq:m,sug:C[d],n:1,rsp:d,f:3,rsv_sug:rsv_sug})}else{d++;var q=l.rows[d];q.className="mo";W.dm({type:"key_select",selected:C[d]});I("kw").value=C[d];H({oq:m,sug:C[d],n:1,rsp:d,f:3,rsv_sug:rsv_sug})}}return J.ini({rm:function(q){switch(q.type){case"div_ready":h=q.sdiv;o=q.frm;break;case"give_data":m=q.data.q;C=q.data.s;rsv_sug=q.data.t;if(C.length!=0){j()}else{i()}break;case"key_enter":Z();break;case"key_up":k();break;case"key_down":g();break;case"hide_div":i();break;case"mousedown_other":i();break;case"window_blur":i();break;case"need_resize":X();break}}})})();var T=(function(){var C=document.forms[0];function G(){if(I("bdsug_ipt_sug")){if(I("bdsug_ipt_sug").value==S(I("kw").value)){L({n:1,sug:1})}else{L({f:1})}}}P(C,"submit",G);function X(){G();C.submit()}function Y(Z){H(Z);L({sug:1,n:1});C.submit()}return J.ini({rm:function(Z){switch(Z.type){case"clk_submit":case"ent_submit":Y({oq:Z.oq,rsp:Z.rsp,f:3,sugT:(new Date().getTime()-A),rsv_sug:rsv_sug});break;case"submit":X();break}}})})();var B=(function(){var G={};function X(C){if(typeof G[C]=="undefined"){B.dm({type:"request_data",wd:C})}else{B.dm({type:"give_data",data:G[C]})}}function Y(C){G[C.q]=C;B.dm({type:"give_data",data:G[C.q]})}return J.ini({rm:function(C){switch(C.type){case"response_data":Y(C.data);break;case"need_data":X(C.wd);break}}})})();var Q=(function(){var C;var X;function G(Y){Q.dm({type:"need_cookie"});if(C){document.body.removeChild(C)}C=K("SCRIPT");C.src="httpdisabled://suggestion.baidu.com/su?wd="+encodeURIComponent(Y)+"&p="+X+"&cb=window.bdsug.sug&t="+(new Date()).getTime();C.charset="gb2312";document.body.appendChild(C)}return J.ini({rm:function(Y){switch(Y.type){case"request_data":G(Y.wd);break;case"give_cookie":var Z=Y.sug;if(Z>0){Z=3}X=Z;break}}})})();bdsug.sug=function(C){bdsug.dm({type:"response_data",data:C})};bdsug.initSug=function(){bdsug.dm({type:"init"})};J.ini(bdsug);var E=(function(){function C(){if(navigator.cookieEnabled){document.cookie="su=0; domain=www.baidu.com"}}function G(){var X=(navigator.cookieEnabled&&/sug=(\d)/.test(document.cookie)?RegExp.$1:3);E.dm({type:"give_cookie",sug:X})}return J.ini({rm:function(X){switch(X.type){case"close":C();break;case"need_cookie":G();break}}})})();var D=(function(){var Z=I("kw");var C;var c=document.forms[0];var Y;function a(){if(C.offsetWidth!=0&&Z.offsetWidth!=C.offsetWidth){D.dm({type:"need_resize"})}}function d(){C=K("DIV");C.id="sd_"+new Date().getTime();C.style.display="none";c.appendChild(C);if(M){Y=K("IFRAME");Y.style.display="none";Y.style.position="absolute";C.parentNode.insertBefore(Y,C)}}function b(e){e=e||window.event;var f=e.target||e.srcElement;if(f==Z){return }while(f=f.parentNode){if(f==C){return }}D.dm({type:"mousedown_other"})}function X(){D.dm({type:"window_blur"})}function G(){var f="#"+C.id;var e=[];D.dm({type:"div_ready",sdiv:C,frm:Y});setInterval(a,100);P(document,"mousedown",b);P(window,"blur",X);e.push(f+"{border:1px solid #817F82;position:absolute;top:28px;left:0}");e.push(f+" table{width:100%;background:#fff;cursor:default}");e.push(f+" td{font:14px verdana;line-height:20px;text-indent:6px}");e.push(f+" td b{color:#333}");e.push(f+" .mo{background-color:#E2EAFF}");e.push(f+" .ml{background-color:#fff}");R(e.join(""))}bdsug.sug.initial=G;return J.ini({rm:function(e){switch(e.type){case"start":G();break;case"init":d();break}}})})();F.on("need_data",B);F.on("close_div",W);F.on("key_enter",W);F.on("key_up",W);F.on("key_down",W);F.on("hide_div",W);F.on("start",D);B.on("request_data",Q);B.on("give_data",W);bdsug.on("response_data",B);bdsug.on("init",D);W.on("clk_submit",F,T);W.on("ent_submit",F,T);W.on("submit",T);W.on("key_select",F);W.on("close",F,E);W.on("mousedown_tr",F);D.on("mousedown_other",W);D.on("need_resize",W);D.on("div_ready",F,W);D.on("window_blur",W);Q.on("need_cookie",E);E.on("give_cookie",Q);window.bdsug.initSug()})(); \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/s@wd=mozilla.html b/mobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/s@wd=mozilla.html
new file mode 100755
index 0000000000..1c0734368f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/s@wd=mozilla.html
@@ -0,0 +1,123 @@
+<!DOCTYPE html><!--STATUS OK--><html><head>
+<meta http-equiv="X-UA-Compatible" content="IE=7">
+<meta http-equiv="content-type" content="text/html;charset=gb2312">
+<title>°Ù¶ÈËÑË÷_mozilla </title>
+<style>body{color:#000;background:#fff;padding:7px 0 0;margin:0;position:relative}body,th,td,.p1,.p2{font-family:arial}p,form,ul,li,h3{margin:0;padding:0;list-style:none}input{padding-top:0;padding-bottom:0;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}table,img{border:0}td{font-size:9pt;line-height:18px}em{font-style:normal;color:#cc0000}a em{text-decoration:underline}.m,a.m{color:#666}a.m:visited{color:#606}.g,a.g{color:#008000}.c{color:#77c}.f14{font-size:14px}.f10{font-size:10.5pt}.f16{font-size:16px}#u,#head,#tool,#search,#foot{font-size:12px}.p1{line-height:120%;margin-left:-12pt}.p2{width:100%;line-height:120%;margin-left:-12pt}#out{_margin-left:880px;_zoom:1}#in{_position:relative;_float:left;_margin-left:-880px}#wrapper{min-width:880px;_zoom:1}#u{white-space:nowrap;position:absolute;right:10px;top:6px;_top:0;z-index:210}#u_m{color:#00c;cursor:pointer}#u_ms{text-decoration:underline}#u_m_tip{position:absolute;right:40px;top:24px;_top:26px;z-index:210;border:1px solid #9a99ff;display:none;background:#fff;overflow:hidden;width:100px}#u_m_tip a{display:block;line-height:22px;color:#0001cf;padding:0 10px;font-size:12px;text-decoration:none;border-bottom:1px solid #e6e6e6;width:100%}#u_m_tip a:hover{background:#d9e1f6}#u_m_tip a.last{border-bottom:0}#head{padding-left:15px}.fm{clear:both;position:relative;z-index:9}.nv{height:45px;position:relative;z-index:200}.nv .logo{float:left;margin-right:20px}.nv .tab{float:left;padding:20px 0 0;line-height:18px}.nv a,.nv b,.btn,#page,#more{font-size:14px}.nv a{color:#0000cc}.i{width:536px;*width:519px;height:32px;*height:20px;padding:3px 7px;padding-top:7px\9;font:16px arial;background:url(img/i2.png) no-repeat;border:1px solid #b6b6b6;border-color:#7b7b7b #b6b6b6 #b6b6b6 #7b7b7b;vertical-align:top;margin-right:5px}.btn{width:95px;height:32px;padding:0;padding-top:2px\9;border:0;background:#ddd url(img/i2.png) 0 -35px;cursor:pointer}.btn_h{background-position:-100px -35px}.btn_wr{width:97px;height:34px;display:inline-block;background:url(img/i2.png) no-repeat -202px bottom;_padding-top:1px;*position:relative}.seth{margin-left:22px;display:none;display:inline\9}.seth a{color:#00c}#tb_mr{color:#00c;cursor:pointer;position:relative;z-index:200}#tb_mr b{font-weight:normal;text-decoration:underline}#tb_mr small{font-size:11px}#more{width:58px;height:100px;border:1px solid #9A99FF;background:#fff;position:absolute;z-index:200;left:452px;top:45px;*top:46px;overflow:hidden;display:none;outline:none}#more a{width:53px;height:25%;line-height:24px;display:block;padding:0 0 0 7px;color:#0001CF;text-decoration:none}#more a span{font-family:"ËÎÌå"}#more a:hover{background:#D9E1F6}#more div{height:1px;overflow:hidden;background:#ccf;margin:0 3px}#page{padding:0 0 0 18px;white-space:nowrap}#page{word-spacing:4px}#page .n{font-size:16px}#rs{width:100%;background:#eff2fa;padding:8px 0;margin:20px 0 0}#rs td{width:5%}#rs th{font-size:14px;font-weight:normal;line-height:19px;white-space:nowrap;text-align:left;vertical-align:top}#rs .tt{font-weight:bold;padding:0 10px 0 23px}.to{font-size:16px;line-height:24px;padding:0 0 0 58px;margin:20px 0 0}#search{padding:35px 0 16px 18px}#search .btn_wr{vertical-align:middle}#foot{height:20px;line-height:20px;color:#77c;background:#e6e6e6;text-align:center}#foot span{color:#666}.f{line-height:115%;*line-height:120%;font-size:100%;width:33.7em;padding-left:15px;word-break:break-all;word-wrap:break-word}.h{margin-left:8px;width:100%}.r{word-break:break-all;cursor:hand;width:238px}.t{font-weight:normal;font-size:medium}.pl{padding-left:3px;height:8px;padding-right:2px;font-size:14px}.mo,a.mo:link,a.mo:visited{color:#666;font-size:100%;line-height:10px}.htb{margin-bottom:5px}.jc a{color:#cc0000}a font[size="3"] font, font[size="3"] a font{text-decoration:underline}div.blog,div.bbs{color:#707070;padding-top:3px}.result{width:34em;table-layout:fixed}.nums{font-size:12px;color:#999}.tools{width:220px;position:absolute;top:10px}#mHolder{width:62px;position:relative;top:-18px;margin-left:9px;margin-right:-12px;display:none}#mCon{position:absolute;right:7px;top:3px;*top:6px;cursor:pointer;padding:0 18px 0 0;line-height:normal;background:url(img/arr.gif) no-repeat right center}#mCon span{color:#00c;cursor:default;display:block;padding-top:3px}#mCon .hw{text-decoration:underline;cursor:pointer}#mMenu{width:56px;border:1px solid #9a99ff;position:absolute;right:7px;top:28px;display:none;background:#fff}#mMenu a{width:100%;height:100%;color:#00c;display:block;line-height:22px;text-indent:6px;text-decoration:none}#mMenu a:hover{background:#d9e1f6}#mMenu .ln{height:1px;background:#ccf;overflow:hidden;margin:2px;font-size:1px;line-height:1px}.op_LAMP{background:url("..void.baidu.com/stat/image/Icon_Aladdin.gif") no-repeat 0 2px;color:#77C;display:inline-block;font-size:13px;height:12px;*height:14px;width:16px;text-decoration:none;zoom:1;}
+.EC_mr15{margin-left:15px}.pd15{padding-left:15px}.favurl{background-repeat:no-repeat;background-position:0 1px;padding-left:20px;}</style>
+
+<script>var name,location,navigate,bdQid="e0156d2700181a32",al_arr=[];var selfOpen = void;eval("void = selfOpen;");function G(id){return document.getElementById(id);}function h(obj){obj.style.behavior='url(#default#homepage)';obj.setHomePage('http://www.baidu.com');var img=window["BD_PS_C"+(new Date()).getTime()]=new Image();img.src="httpdisabled://sclick.baidu.com/w.gif?fm=hp&tn=baidu&t="+new Date().getTime();}function al_c(A){while(A.tagName!="TABLE"){A=A.parentNode;}return A.getAttribute("id");}function al_c2(n,c){while(c--){while((n=n.parentNode).tagName!="TABLE");};return n.getAttribute("id");}function c(q){var p = window.document.location.href, sQ = '', sV = '', mu='', img = window["BD_PS_C" + (new Date()).getTime()] = new Image();for (v in q) {switch (v) {case "title":sV = encodeURIComponent(q[v].replace(/<[^<>]+>/g, ""));break;case "url":sV = escape(q[v]);break;default:sV = q[v];}sQ += v + "=" + sV + "&";}try{if (("p2" in q)&&G(q["p1"]).getAttribute("mu") && q["fm"]!="pl") {mu= "mu=" + escape(G(q["p1"]).getAttribute("mu"));}}catch(e){};img.src = "httpdisabled://sclick.baidu.com/w.gif?q=mozilla&" + sQ + mu + "&cid=0&qid=e0156d2700181a32&t="+new Date().getTime()+"&path="+p;return true;}window["bdUser"]=null;window["login_success"]=[];</script>
+</head>
+<body link="#0000cc">
+<div id="out"><div id="in"><div id="wrapper">
+<p id="u"><a href="httpdisabled://www.baidu.com/gaoji/preferences.html" onmousedown="return user_c({'fm':'set','tab':'setting','url':this.href})">ËÑË÷ÉèÖÃ</a>&nbsp;|&nbsp;<a id="lb" href="httpdisabled://passport.baidu.com/?login&tpl=mn" onclick="return false;" onmousedown="return user_c({'fm':'set','tab':'login','url':this.href})">µÇ¼</a></p>
+<div id="head"><div class="nv"><a href="httpdisabled://www.baidu.com/" class="logo"><img src="img/baidu_jgylogo1.gif" width="117" height="38" border="0" alt="µ½°Ù¶ÈÊ×Ò³"></a><div class="tab"><a href="httpdisabled://news.baidu.com/ns?cl=2&rn=20&tn=news&word=mozilla" onmousedown="return c({'fm':'tab','tab':'news'})">ÐÂÎÅ</a>¡¡<b>ÍøÒ³</b>¡¡<a href="httpdisabled://tieba.baidu.com/f?kw=mozilla&fr=wwwt" onmousedown="return c({'fm':'tab','tab':'tieba'})">Ìù°É</a>¡¡<a href="httpdisabled://zhidao.baidu.com/q?ct=17&pn=0&tn=ikaslist&rn=10&word=mozilla&fr=wwwt" onmousedown="return c({'fm':'tab','tab':'zhidao'})">ÖªµÀ</a>¡¡<a href="httpdisabled://mp3.baidu.com/m?tn=baidump3&ct=134217728&lm=-1&word=mozilla" onmousedown="return c({'fm':'tab','tab':'mp3'})">MP3</a>¡¡<a href="httpdisabled://image.baidu.com/i?tn=baiduimage&ct=201326592&lm=-1&cl=2&word=mozilla" onmousedown="return c({'fm':'tab','tab':'pic'})">ͼƬ</a>¡¡<a href="httpdisabled://video.baidu.com/v?ct=301989888&rn=20&pn=0&db=0&s=25&word=mozilla" onmousedown="return c({'fm':'tab','tab':'video'})">ÊÓƵ</a>¡¡<a href="httpdisabled://map.baidu.com/m?word=mozilla&fr=ps01000" onmousedown="return c({'fm':'tab','tab':'map'})">µØͼ</a>¡¡<span id="tb_mr" onmousedown="return c({'fm':'tab','tab':'tbmore'});"><b>¸ü¶à</b><small>¨‹</small></span></div><div id="more"><a href="httpdisabled://baike.baidu.com/searchword/?word=mozilla&pic=1" onmousedown="return c({'fm':'tab','tab':'baike'})">°Ù¿Æ</a><a href="httpdisabled://wenku.baidu.com/search?word=mozilla&lm=0&od=0" onmousedown="return c({'fm':'tab','tab':'wenku'})">ÎÄ¿â</a><a href="httpdisabled://dict.baidu.com/s?wd=mozilla" onmousedown="return c({'fm':'tab','tab':'dict'})">´Êµä</a><div></div><a href="httpdisabled://www.baidu.com/more/" onmousedown="return c({'fm':'tab','tab':'more'})">¸ü¶à<span>&gt;&gt;</span></a></div></div><form name="f" action="httpdisabled://www.baidu.com/s" class="fm"><input type="hidden" name="bs" value="mozilla"><input type="hidden" name="f" value="8"><input name="wd" id="kw" class="i" value="mozilla" maxlength="100"><span class="btn_wr"><input type="submit" id="su" value="°Ù¶ÈÒ»ÏÂ" class="btn" onmousedown="this.className='btn btn_h'" onmouseout="this.className='btn'"></span><span class="tools"><span id="mHolder"><div id="mCon"><span>ÊäÈë·¨</span></div><ul id="mMenu"><li><a href="s@wd=mozilla.html#" name="ime_hw">ÊÖд</a></li><li><a href="s@wd=mozilla.html#" name="ime_py">Æ´Òô</a></li><li class="ln"></li><li><a href="s@wd=mozilla.html#" name="ime_cl">¹Ø±Õ</a></li></ul></span><span class="seth"><a href="s@wd=mozilla.html#" onClick="h(this)">°Ñ°Ù¶ÈÉèΪÖ÷Ò³</a></span>
+</span></form></div><br>
+
+<table width="30%" border="0" cellpadding="0" cellspacing="0" align="right"><tr>
+<td align="left" style="padding-right:10px">
+<div style="border-left:1px solid #e1e1e1;padding-left:10px;word-break:break-all;word-wrap:break-word;">
+
+
+
+
+
+<style type="text/css">
+.r.ec_bdtg{ width:238px;}
+.ec_bdtg .fsblock{padding:0;word-break:normal;font-family:arial}
+.ec_bdtg .fsblock a{text-decoration:none;}
+.ec_bdtg .title a{ text-decoration:underline; margin:0; padding:0; cursor:pointer;}
+</style>
+<div class="r ec_bdtg">
+<div class="fsblock">
+
+ <div class="title"><a href="httpdisabled://www.baidu.com/adrc.php?t=000a00c00f7Ul0D0SOY00FTFK60U6GqP0000000000000000wp6s7s.THdVULGGUAk90A3qmh7GuZR0T1d-njDdPhfzP10snH6kmHnz0ZRq0ADquZCkIAc1TZKBwL9Cih4PrNGRfg9rN7GoHyGWIYdDwHwPNYNlHy-pI7bzUAVfN77lHdwWnDqRmvduNjK3yZGKUywZnj-2U-uYR7PpIhPVp1-PRdGlu7I2IhPVp1-2UbF4mNfsnfK-5y9YIZ0lQzq-QhF9pywdQhPEUitOThNhugcqnH0z0APzm1Y1nWTdn6" target="_blank"><font size="3" style="_font-size:8pt;">&#9654</font><font size="3">À´°Ù¶ÈÍƹãÄúµÄ²úÆ·</font></a></div>
+ <a href="httpdisabled://www.baidu.com/adrc.php?t=000a00c00f7Ul0D0SOY00FTFK60U6GqP0000000000000000wp6s7s.THdVULGGUAk90A3qmh7GuZR0T1d-njDdPhfzP10snH6kmHnz0ZRq0ADquZCkIAc1TZKBwL9Cih4PrNGRfg9rN7GoHyGWIYdDwHwPNYNlHy-pI7bzUAVfN77lHdwWnDqRmvduNjK3yZGKUywZnj-2U-uYR7PpIhPVp1-PRdGlu7I2IhPVp1-2UbF4mNfsnfK-5y9YIZ0lQzq-QhF9pywdQhPEUitOThNhugcqnH0z0APzm1Y1nWTdn6" target="_blank"><font color="#000" size="-1">×ÉѯÈÈÏߣº400-800-8888</font><br/>
+ <font color="#008000" size="-1">e.baidu.com</font></a>
+
+</div>
+</div>
+<br />
+
+
+
+</div>
+<br>
+</td></tr></table>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<table cellpadding="0" cellspacing="0" class="result" id="1" mu="httpdisabled://baike.baidu.com/view/393243.htm"><tr><td class="f"><h3 class="t"><a target="_blank" href="httpdisabled://baike.baidu.com/view/393243.htm" onmousedown="return c({'fm':'albk','title':this.innerHTML,'url':this.href,'p1':al_c(this)});"><em>mozilla</em>_°Ù¶È°Ù¿Æ</a></h3><font size="-1"><em>Mozilla</em>»ù½ð»á¼ò³Æ<em>Mozilla</em>(ËõдMF»òMoFo£¬ÈçͼΪ<em>Mozilla</em> »ù½ð»áµÄ×¢²á±êʶ)£¬ÊÇΪ֧³ÖºÍÁìµ¼¿ªÔ´µÄ<em>Mozilla</em>ÏîÄ¿¶øÉèÁ¢µÄÒ»¸ö·ÇÓªÀû×éÖ¯¡£¸Ã×éÖ¯Öƶ¨...<font color="#666666">¹²19´Î±à¼­</font><br/><a target="_blank" href="httpdisabled://baike.baidu.com/view/393243.htm#1" onmousedown="return c({'fm':'albk','title':this.innerHTML,'url':this.href,'p1':al_c(this),'p2':'1'});">·¢Õ¹¼òÊ·</a> - <a target="_blank" href="httpdisabled://baike.baidu.com/view/393243.htm#2" onmousedown="return c({'fm':'albk','title':this.innerHTML,'url':this.href,'p1':al_c(this),'p2':'2'});">Ãû×ÖÀ´Àú</a> - <a target="_blank" href="httpdisabled://baike.baidu.com/view/393243.htm#3" onmousedown="return c({'fm':'albk','title':this.innerHTML,'url':this.href,'p1':al_c(this),'p2':'3'});">ͼ±êÀ´Ô´</a> - <a target="_blank" href="httpdisabled://baike.baidu.com/view/393243.htm#4" onmousedown="return c({'fm':'albk','title':this.innerHTML,'url':this.href,'p1':al_c(this),'p2':'4'});">³õÆڳɾÍ</a> <br/><font color="#008000">baike.baidu.com/view/393243.htm 2010-12-26</font></font></td></tr></table><br>
+<table cellpadding="0" cellspacing="0" class="result" id="2"><tr><td class=f><h3 class="t"><a onmousedown="return c({'fm':'as','F':'779317EA','F1':'9D73F1E4','F2':'4CA63E6B','F3':'54E5243F','T':'1302299163','title':this.innerHTML,'url':this.href,'p1':2,'y':'FB6977FE'})" href="httpdisabled://www.mozilla.org/products/firefox/" target="_blank">ıÖÇÍøÂ磬»ðºüä¯ÀÀÆ÷ÖйúΨһ¹Ù·½ÍøÕ¾ | <em>Mozilla</em>, Firefox, and ...</a></h3><font size=-1> [10-13] <em>Mozilla</em>¶­Ê³¤Ã×Çжû³ÆδÀ´ä¯ÀÀÆ÷»áÊÇÕûºÏƽ̨ [09-26] ÊÀ½çÈí¼þ×ÔÓÉÈÕ »ðºüÉçÇøÔÙÏÆ¿ªÔ´Èȳ± [09-08] »ðºü¹È¸èÁªÊÖ·¢²¼¡°W3Help¡±ÍøÕ¾ ÖÂÁ¦´Ù½øW3C...<br><span class="g">www.<b>mozilla</b>.org/products/firefox/ 2010-12-18 </span> - <a href="httpdisabled://cache.baidu.com/c?m=9f65cb4a8c8507ed4fece763105392230e54f73c619f8b4b21878448e4391b145a24a3e67165414292d8283c41f81d01a0ed22376a4376b8c495c01183e5c1&p=8b2a931d84d805ff57ed972c1349&user=baidu&fm=sc&query=mozilla&qid=e0156d2700181a32&p1=2" target="_blank" class="m">°Ù¶È¿ìÕÕ</a><br></font></td></tr></table><br>
+<table cellpadding="0" cellspacing="0" id="3" mu="httpdisabled://soft.baidu.com/softwaresearch/s?tn=software&rn=10&wd=mozilla"><style>.op_table01_content table{margin-top:4px;}.op_table01_content th{text-align:left;white-space:nowrap;background:url("aladdin/img/table/bg.gif") repeat-x 0 -37px;font-weight:normal;height:26px;line-height:26px;font-size:13px;}.op_table02_content td a{text-decoration:none;}.op_table01_content td{white-space:nowrap;height:33px;font-size:14px;border-bottom:#eee 1px solid;}.op_software table{width:98%;}.op_software td{padding-right:14px;}</style><script>function jI(D){var C=D;var B=0;while(C=C.parentNode){B=parseInt(C.getAttribute("id"));if(B>0){break}}var A=C.getElementsByTagName("a");for(var B=0;B<A.length;B++){if(D==A[B]){return B}}return A.length-1}function _aMC(C){var B=C,A=-1;while(B=B.parentNode){A=parseInt(B.getAttribute("id"));if(A>0){return A}}};</script><tr><td class=f> <a onmousedown="return c({'fm':'alop','title':this.innerHTML,'url':this.href,'p1':_aMC(this)})" href="httpdisabled://soft.baidu.com/softwaresearch/s?tn=software&rn=10&wd=mozilla" target="_blank"><font size="3"><em>mozilla</em>_Ïà¹ØÏÂÔØÐÅÏ¢123Ìõ_°Ù¶ÈÈí¼þËÑË÷</font></a><br> <div class="op_table01_content op_software"> <table cellspacing="0" class="op_software_tb"><tr><th style="border-left:0;">Èí¼þÃû³Æ</th><th class="op_software_size">Èí¼þ´óС</th><th class="op_software_src">À´Ô´</th></tr> <tr><td> <a target="_blank" onmousedown="return c({'fm':'alop','title':this.innerHTML,'url':this.href,'p1':_aMC(this),'p2':jI(this)})" href="httpdisabled://www.newhua.com/soft/3600.htm" ><em>Mozilla</em> Firefox 4.0 ¼òÌå°æ</a></td><td class="op_software_size"> 11.85 M </td><td class="op_software_src"> »ª¾üÈí¼þÔ° </td></tr> <tr><td> <a target="_blank" onmousedown="return c({'fm':'alop','title':this.innerHTML,'url':this.href,'p1':_aMC(this),'p2':jI(this)})" href="httpdisabled://www.skycn.com/soft/16613.html" ><em>Mozilla</em> Thunderbird 2.0.0.23 ¼òÌåÖÐÎÄ°æ</a></td><td class="op_software_size"> 6.34 M </td><td class="op_software_src"> Ìì¿ÕÈí¼þÕ¾ </td></tr> <tr><td> <a target="_blank" onmousedown="return c({'fm':'alop','title':this.innerHTML,'url':this.href,'p1':_aMC(this),'p2':jI(this)})" href="httpdisabled://dl.pconline.com.cn/html_2/1/104/id=49635&pn=0.html" ><em>Mozilla</em> Firefox 4 ¼òÌåÖÐÎÄÕýʽ°æ</a></td><td class="op_software_size"> 11.85 M </td><td class="op_software_src"> ̫ƽÑóÏÂÔØ </td></tr> <tr><td> <a target="_blank" onmousedown="return c({'fm':'alop','title':this.innerHTML,'url':this.href,'p1':_aMC(this),'p2':jI(this)})" href="httpdisabled://www.duote.com/soft/9276.html" ><em>Mozilla</em> Firefox Plus(FoxPlus) V3.0.2.1 ¼ò...</a></td><td class="op_software_size"> 9.96 M </td><td class="op_software_src"> ¶àÌØÈí¼þÕ¾ </td></tr> <tr><td> <a target="_blank" onmousedown="return c({'fm':'alop','title':this.innerHTML,'url':this.href,'p1':_aMC(this),'p2':jI(this)})" href="httpdisabled://www.crsky.com/soft/9196.html" ><em>Mozilla</em> SeaMonkey v2.0.13 for Windows</a></td><td class="op_software_size"> 10.15 M </td><td class="op_software_src"> ·Ç·²Èí¼þÕ¾ </td></tr> </table> <div style="padding:4px 0 2px;"><a onmousedown="return c({'fm':'alop','title':this.innerHTML,'url':this.href,'p1':_aMC(this),'p2':jI(this)})" style="color:#7777CC;font-size:12px;" href="httpdisabled://soft.baidu.com/softwaresearch/s?tn=software&rn=10&wd=mozilla" target="_blank">²é¿´È«²¿123Ìõ½á¹û<span style="font-family:simsun">&gt;&gt;</span></a></div><font size=-1 color=#008000>soft.baidu.com/softwaresearch/s?tn=software&r... 2011-4-9</font></div> </div></td></tr></table><br><table cellpadding="0" cellspacing="0" class="result" id="4"><tr><td class=f><h3 class="t"><a onmousedown="return c({'fm':'as','F':'771317EA','F1':'9D73F4E4','F2':'4CA63E6B','F3':'54E5243F','T':'1302299163','title':this.innerHTML,'url':this.href,'p1':4,'y':'F2E77FDE'})" href="httpdisabled://www.mozilla.com/firefox/" target="_blank">ıÖÇÍøÂ磬»ðºüä¯ÀÀÆ÷ÖйúΨһ¹Ù·½ÍøÕ¾ | <em>Mozilla</em>, Firefox, and ...</a></h3><font size=-1> [10-13] <em>Mozilla</em>¶­Ê³¤Ã×Çжû³ÆδÀ´ä¯ÀÀÆ÷»áÊÇÕûºÏƽ̨ [09-26] ÊÀ½çÈí¼þ... [01-22] Firefox»ðºüä¯ÀÀÆ÷3.6Õýʽ·¢²¼£¬ÊÓƵ½éÉÜÐÂÌØÐÔ£¡ [01-06] »ðºü...<br><span class="g">www.<b>mozilla</b>.com/firefox/ 2010-12-26 </span> - <a href="httpdisabled://cache.baidu.com/c?m=9d78d513d99c1ce703b3ca2d19519738160ec6257ec0d16662c9d60dd6735b36183babe0797c4313d3b22d3a5eb21d07aaa7622f7d1e&p=9e759a41d6b119b406f3c7710b5f&user=baidu&fm=sc&query=mozilla&qid=e0156d2700181a32&p1=4" target="_blank" class="m">°Ù¶È¿ìÕÕ</a><br></font></td></tr></table><br>
+<table cellpadding="0" cellspacing="0" class="result" id="5"><tr><td class=f><h3 class="t"><a onmousedown="return c({'fm':'as','F':'779317EA','F1':'9D33F1E4','F2':'4CA67D6B','F3':'54E5243F','T':'1302299163','title':this.innerHTML,'url':this.href,'p1':5,'y':'9E0BBBDE'})" href="httpdisabled://www.mozilla.com/" target="_blank">ıÖÇÍøÂç »ðºüä¯ÀÀÆ÷ÖйúΨһ¹Ù·½ÍøÕ¾ | <em>Mozilla</em>, Firefox, and C...</a></h3><font size=-1> »ðºüÉçÇøÊÇıÖÇÍøÂ繫˾´î½¨µÄΪÓû§·þÎñµÄÉçÇø¡£Ä±ÖÇÍøÂç²»ÊÇ´«Í³ÒâÒåÉϵÄÈí¼þ¹«Ë¾£¬ËûÊÇÖÂÁ¦ÓÚ¹¹½¨×ÔÓÉ¡¢¿ª·Å»¥ÁªÍø¼°²úÆ·µÄÈ«ÇòÉçÇø£¬ÎªÄú´òÔìȫеÄÍøÂçä¯ÀÀÆ÷¡ª...<br><span class="g">www.<b>mozilla</b>.com/ 2011-4-6 </span> - <a href="httpdisabled://cache.baidu.com/c?m=9d78d513d99c1ce703b3ca2d19519738160ec6257ec0d16662c9d60dd6735b36183babe0797c4313d3b22d3a5eb2&p=9a63c00485cc41ec08e2966053&user=baidu&fm=sc&query=mozilla&qid=e0156d2700181a32&p1=5" target="_blank" class="m">°Ù¶È¿ìÕÕ</a><br></font></td></tr></table><br>
+<table cellpadding="0" cellspacing="0" class="result" id="6"><tr><td class=f><h3 class="t"><a onmousedown="return c({'fm':'as','F':'779717EA','F1':'9D73F1E4','F2':'4CA67D6B','F3':'54E5243F','T':'1302299163','title':this.innerHTML,'url':this.href,'p1':6,'y':'4C376A76'})" href="httpdisabled://www.mozilla.org.cn/" target="_blank">ıÖÇÍøÂç »ðºüä¯ÀÀÆ÷ÖйúΨһ¹Ù·½ÍøÕ¾ | <em>Mozilla</em>, Firefox, and C...</a></h3><font size=-1> »ðºüÉçÇøÊÇıÖÇÍøÂ繫˾´î½¨µÄΪÓû§·þÎñµÄÉçÇø¡£Ä±ÖÇÍøÂç²»ÊÇ´«Í³ÒâÒåÉϵÄÈí¼þ¹«Ë¾£¬ËûÊÇÖÂÁ¦ÓÚ¹¹½¨×ÔÓÉ¡¢¿ª·Å»¥ÁªÍø¼°²úÆ·µÄÈ«ÇòÉçÇø£¬ÎªÄú´òÔìȫеÄÍøÂçä¯ÀÀÆ÷¡ª...<br><span class="g">www.<b>mozilla</b>.org.cn/ 2011-4-7 </span> - <a href="httpdisabled://cache.baidu.com/c?m=9d78d513d99c1ce703b3ca2d19519738160ec6257ec0d16662c9d60dd6735b36183babe0797c4313d3b2212754b8492bbbac2b&p=8b2a950ec58011a05eead3371347&user=baidu&fm=sc&query=mozilla&qid=e0156d2700181a32&p1=6" target="_blank" class="m">°Ù¶È¿ìÕÕ</a><br></font></td></tr></table><br>
+<table cellpadding="0" cellspacing="0" class="result" id="7"><tr><td class=f><h3 class="t"><a onmousedown="return c({'fm':'as','F':'779717EA','F1':'9D73F1E4','F2':'4CA63E6B','F3':'54E5243F','T':'1302299163','title':this.innerHTML,'url':this.href,'p1':7,'y':'39D7CAFF'})" href="httpdisabled://www.mozilla.org/firefox/" target="_blank">ıÖÇÍøÂç »ðºüä¯ÀÀÆ÷ÖйúΨһ¹Ù·½ÍøÕ¾ | <em>Mozilla</em>, Firefox, and C...</a></h3><font size=-1> ¹ØÓÚ<em>Mozilla</em> ·¨ÂÉÉùÃ÷ »ðºü²©¿ÍFirefox screenshot »ðºüä¯ÀÀÆ÷ Firefox »ðºüfirefox Ãâ·ÑÏÂÔØ 4.0 ¼òÌåÖÐÎÄ Windows &gt;&gt;ÆäËüϵͳ/ÓïÑÔ°æ±¾ÏÂÔØ &gt;&gt;ÌÚѶÏÂÔØ &gt;&gt;ÐÂÀË...<br><span class="g">www.<b>mozilla</b>.org/firefox/ 2011-3-24 </span> - <a href="httpdisabled://cache.baidu.com/c?m=9f65cb4a8c8507ed4fece763105392230e54f73c619f8b4b21878448e4391b145a32b8fb70764d4eced1393a41f94603b7b86d2c6950&p=c9769a4286cc4ab11ca7c66856&user=baidu&fm=sc&query=mozilla&qid=e0156d2700181a32&p1=7" target="_blank" class="m">°Ù¶È¿ìÕÕ</a><br></font></td></tr></table><br>
+<table cellpadding="0" cellspacing="0" class="result" id="8"><tr><td class=f><h3 class="t"><a onmousedown="return c({'fm':'as','F':'779717EA','F1':'9D43F1E4','F2':'4CA65EEA','F3':'54E5243F','T':'1302299163','title':this.innerHTML,'url':this.href,'p1':8,'y':'8B8DFDEE'})" href="httpdisabled://addons.mozilla.org/" target="_blank">ÄãÒ¡¹ö¸ÐлÄúʹÓÃfirefoxä¯ÀÀÆ÷³¢ÊÔ¸½¼Ó×é¼þ</a></h3><font size=-1> <span class="g">addons.<b>mozilla</b>.org/ 2011-4-6 </span> - <a href="httpdisabled://cache.baidu.com/c?m=9d78d513d99c1ce703b3ca2d19519738160ec6257ec0d16662c9c01ec5390700506694e47a6a4b5a8d966b6776f20909f7&p=8f72c64ad3871cfa08e297744e42&user=baidu&fm=sc&query=mozilla&qid=e0156d2700181a32&p1=8" target="_blank" class="m">°Ù¶È¿ìÕÕ</a><br></font></td></tr></table><br>
+<table cellpadding="0" cellspacing="0" id="9" mu="httpdisabled://news.baidu.com/ns?cl=2&rn=20&tn=news&word=mozilla&ct=1&fr=ala0"><tr><td class="f" style="padding-bottom:3px;"><font size="3"><a href="httpdisabled://news.baidu.com/ns?cl=2&rn=20&tn=news&word=mozilla&ct=1&fr=ala0" target="_blank" onmousedown="return c({'fm':'alns','title':this.innerHTML,'url':this.href,'p1':al_c(this)});"><em>mozilla</em>µÄÏà¹ØÐÂÎÅ</a></font></td></tr><tr><td class="f"><p style="margin:0;padding:0;margin-left:1em;"><font size="-1"><a href="httpdisabled://tech.hexun.com/2011-04-08/128582384.html " target="_blank" onmousedown="return c({'fm':'alns','title':this.innerHTML,'url':this.href,'p1':al_c(this),'p2':1});"><em>mozilla</em>·ÏßͼÆعâ!»ðºü5/6·¼×ÙÕ§ÏÖ</a> <font color="#008000">ºÍѶÍø</font>&nbsp;<font color="#666666">13Сʱǰ</font><br><em>Mozilla</em>¹«Ë¾ÈÕÇ°½øÒ»²½·Å³öÁËÆäFirefox»ðºüϵÁÐä¯ÀÀÆ÷²úÆ·µÄ¿ª·¢Ä£ÐÍ¡£´ÓÒѾ­Ð¹Â¶µÄ»ðºü²úƷ·ÏßͼÀ´¿´,½ô¸úÔÚ»ðºü4ä¯ÀÀÆ÷Ö®ºóµÄ»ðºü5Ô¤¼Æ½«ÓÚ201...</font><br><font size="-1"><a href="httpdisabled://www.cnbeta.com/articles/139492.htm " target="_blank" onmousedown="return c({'fm':'alns','title':this.innerHTML,'url':this.href,'p1':al_c(this),'p2':2});"><em>mozilla</em>:firefox 5½«ÔÚ6ÔÂ21ÈÕµ½À´ °æ±¾6ÔÚ8ÔÂ18ÈÕ</a> <font color="#008000">cnBeta</font>&nbsp;<font color="#666666">21Сʱǰ</font></font><br><font size="-1"><a href="httpdisabled://tech.hexun.com/2011-04-07/128550959.html " target="_blank" onmousedown="return c({'fm':'alns','title':this.innerHTML,'url':this.href,'p1':al_c(this),'p2':3});">²»ÏëÃðÍö!<em>mozilla</em>³ÜÈèǽ¼à¶½»ðºü²å¼þ</a> <font color="#008000">ºÍѶÍø</font>&nbsp;<font color="#666666">1ÌìÇ°</font></font><br></p></td></tr>
+</table><br><table cellpadding="0" cellspacing="0" class="result" id="10"><tr><td class=f><h3 class="t"><a onmousedown="return c({'fm':'as','F':'779717EA','F1':'9D73F3E4','F2':'4CA6BE6B','F3':'54E5243F','T':'1302299163','title':this.innerHTML,'url':this.href,'p1':10,'y':'DFFFFFD7'})" href="httpdisabled://www.newhua.com/soft/1349.htm" target="_blank"><em>Mozilla</em> Firefox 4.0 Beta 12 ¼òÌå°æ ÏÂÔØ - »ª¾üÈí¼þÔ° - ÍøÂ繤...</a></h3><font size=-1> <em>Mozilla</em> Firefox 4.0 Beta 12 ¼òÌå°æ [ÏÂÔصØÖ·] Èí¼þÀà±ð£º¹úÍâÈí¼þ/Ö÷Ò³ä¯ÀÀ Èí¼þÊÚȨ£ºÃâ·Ñ°æ ÔËÐл·¾³£ºWinxp/vista/win7/2000/2003 ¸üÐÂʱ¼ä£º2011-2-26...<br><span class="g">www.newhua.com/soft/1349.htm 2011-2-26 </span> - <a href="httpdisabled://cache.baidu.com/c?m=9d78d513d99c1ce703b3ca2d19519738160ec6257ec0d16662c9d60dd6735b361b31a6e160710704a49421381cee1408aced3573310837b7ec92ce15&p=aa7dc64ad0af06b105bd9b7842&user=baidu&fm=sc&query=mozilla&qid=e0156d2700181a32&p1=10" target="_blank" class="m">°Ù¶È¿ìÕÕ</a><br></font></td></tr></table><br>
+
+
+
+
+
+
+
+
+
+
+<script></script>
+<br clear=all>
+
+<p id="page"><span>1</span> <a href="httpdisabled://www.baidu.com/s?wd=mozilla&amp;pn=10&amp;usm=2">[2]</a> <a href="httpdisabled://www.baidu.com/s?wd=mozilla&amp;pn=20&amp;usm=2">[3]</a> <a href="httpdisabled://www.baidu.com/s?wd=mozilla&amp;pn=30&amp;usm=2">[4]</a> <a href="httpdisabled://www.baidu.com/s?wd=mozilla&amp;pn=40&amp;usm=2">[5]</a> <a href="httpdisabled://www.baidu.com/s?wd=mozilla&amp;pn=50&amp;usm=2">[6]</a> <a href="httpdisabled://www.baidu.com/s?wd=mozilla&amp;pn=60&amp;usm=2">[7]</a> <a href="httpdisabled://www.baidu.com/s?wd=mozilla&amp;pn=70&amp;usm=2">[8]</a> <a href="httpdisabled://www.baidu.com/s?wd=mozilla&amp;pn=80&amp;usm=2">[9]</a> <a href="httpdisabled://www.baidu.com/s?wd=mozilla&amp;pn=90&amp;usm=2">[10]</a> <a href="httpdisabled://www.baidu.com/s?wd=mozilla&amp;pn=10&amp;usm=2" class="n">ÏÂÒ»Ò³</a> <span class="nums" style="margin-left:120px">ÕÒµ½Ïà¹Ø½á¹ûÔ¼11,900,000¸ö</span></p>
+
+
+
+
+
+
+
+
+<div id="rs"><table cellpadding="0"><tr><th rowspan="2" class="tt">Ïà¹ØËÑË÷</th><th><a href="httpdisabled://www.baidu.com/s?wd=mozilla%20firefox&amp;rsp=0&amp;oq=mozilla&amp;f=1&amp;rsv_ers=xdt0">mozilla firefox</a>
+</th><td></td><th><a href="httpdisabled://www.baidu.com/s?wd=mozilla%204.0&amp;rsp=1&amp;oq=mozilla&amp;f=1&amp;rsv_ers=xdt0">mozilla 4.0</a>
+</th><td></td><th><a href="httpdisabled://www.baidu.com/s?wd=mozilla%20sunbird&amp;rsp=2&amp;oq=mozilla&amp;f=1&amp;rsv_ers=xdt0">mozilla sunbird</a>
+</th><td></td><th><a href="httpdisabled://www.baidu.com/s?wd=mozilla%BB%F9%BD%F0%BB%E1&amp;rsp=3&amp;oq=mozilla&amp;f=1&amp;rsv_ers=xdt0">mozilla»ù½ð»á</a>
+</th><td></td><th><a href="httpdisabled://www.baidu.com/s?wd=mozilla%D0%FB%D1%D4&amp;rsp=4&amp;oq=mozilla&amp;f=1&amp;rsv_ers=xdt0">mozillaÐûÑÔ</a>
+</th></tr><tr><th><a href="httpdisabled://www.baidu.com/s?wd=mozilla.org&amp;rsp=5&amp;oq=mozilla&amp;f=1&amp;rsv_ers=xdt0">mozilla.org</a>
+</th><td></td><th><a href="httpdisabled://www.baidu.com/s?wd=mozilla%20%C2%DB%CC%B3&amp;rsp=6&amp;oq=mozilla&amp;f=1&amp;rsv_ers=xdt0">mozilla ÂÛ̳</a>
+</th><td></td><th><a href="httpdisabled://www.baidu.com/s?wd=mozilla%CA%C7%CA%B2%C3%B4&amp;rsp=7&amp;oq=mozilla&amp;f=1&amp;rsv_ers=xdt0">mozillaÊÇʲô</a>
+</th><td></td><th><a href="httpdisabled://www.baidu.com/s?wd=mozilla.com&amp;rsp=8&amp;oq=mozilla&amp;f=1&amp;rsv_ers=xdt0">mozilla.com</a>
+</th><td></td><th><a href="httpdisabled://www.baidu.com/s?wd=mozilla%20%B9%D9%CD%F8&amp;rsp=9&amp;oq=mozilla&amp;f=1&amp;rsv_ers=xdt0">mozilla ¹ÙÍø</a>
+</th></tr></table></div>
+
+<div id="search"><form name="f2" action="httpdisabled://www.baidu.com/s" ><input type="hidden" name="bs" value="mozilla"><input type="hidden" name="f" value="8"><input name="wd" class="i" value="mozilla" maxlength="100"><span class="btn_wr"><input type="submit" value="°Ù¶ÈÒ»ÏÂ" class="btn" onmouseout="this.className='btn'" onmousedown="this.className='btn btn_h'"></span>&nbsp;&nbsp;&nbsp;<a href="httpdisabled://www.baidu.com/s?wd=mozilla&amp;tn=baidufir" onmousedown="return c({'almid':'fir','stl':'link'})">½á¹ûÖÐÕÒ</a>&nbsp;&nbsp;&nbsp;<a href="httpdisabled://www.baidu.com/search/jiqiao.html" target="_blank" onmousedown="return c({'fm':'behb','tab':'help','url':this.href,'title':this.innerHTML})">°ïÖú</a>&nbsp;&nbsp;&nbsp;<a href="httpdisabled://www.baidu.com/search/jubao.html" target="_blank" onmousedown="return c({'fm':'behb','tab':'jubao','url':this.href,'title':this.innerHTML})">¾Ù±¨</a>&nbsp;&nbsp;&nbsp;<a href="httpdisabled://www.baidu.com/gaoji/advanced.html" onclick='location.href=this.href+"?q="+encodeURIComponent(document.f.kw.value);return false;' onmousedown="return c({'fm':'behb','tab':'gaoji','url':this.href,'title':this.innerHTML})">¸ß¼¶ËÑË÷</a></form></div>
+
+
+<div id="foot">&copy;2011 Baidu <span>´ËÄÚÈÝϵ°Ù¶È¸ù¾ÝÄúµÄÖ¸Áî×Ô¶¯ËÑË÷µÄ½á¹û£¬²»´ú±í°Ù¶ÈÔ޳ɱ»ËÑË÷ÍøÕ¾µÄÄÚÈÝ»òÁ¢³¡</span></div>
+</div></div></div>
+<img src="../c.baidu.com/c.gif@t=0&amp;q=mozilla&amp;p=0&amp;pn=1.html" style="display:none">
+</body>
+
+<script>for(ai in al_arr){al_arr[ai]()};c({'fm':'se','T':'1302299163','y':'FFDFDFEF'});if(navigator.cookieEnabled && !/sug?=0/.test(document.cookie)){void('<script src="js/bdsug.js@v=1.0.3.0"><\/script>')};</script>
+<script>window.onunloaddisabled=function(){};window.onloaddisabled=function(){document.forms[0].reset();document.forms[document.forms.length-1].reset()};function addEV(C,B,A){if(window.attachEvent){C.attachEvent("on"+B,A)}else{if(window.addEventListener){C.addEventListener(B,A,false)}}}addEV(document,"click",function(E){var E=E||window.event;var A=E.target||E.srcElement;var D=window.event?E.button:E.which;var C=G("tb_mr"),B=G("more");while(A&&A!=document.body&&A.tagName.toLowerCase()!="html"){if(A==C){break}A=A.parentNode}if(A!=C){B.style.display="none"}else{if(D<2){B.style.display=B.style.display=="block"?"none":"block"}}});var bdimeHW={};var imeTar="kw";var ime_t1=(new Date()).getTime();(function(){var d=navigator,Y=document,j=window,e=d.userAgent.indexOf("MSIE")!=-1;var c=G("mCon"),f=G("mMenu");var q=["ÊäÈë·¨","ÊÖд","Æ´Òô"],r=["cl","hw","py"],m=["","httpdisabled://www.baidu.com/hw/hwInput_1.1.js","httpdisabled://www.baidu.com/olime/bdime.js"],o=[0,0,0];var l=d.cookieEnabled;if(l&&/\bbdime=(\d)/.test(Y.cookie)){W(r[RegExp["\x241"]],false)}var n=f.getElementsByTagName("a");for(var b=0;b<n.length;b++){n[b].onclick=Z}if(e){var g=[];var h=c.getElementsByTagName("*");for(var b=0;b<h.length;b++){g.push(h[b])}g.push(c);var h=f.getElementsByTagName("*");for(var b=0;b<h.length;b++){g.push(h[b])}g.push(f);for(var b=0;b<g.length;b++){g[b].setAttribute("unselectable","on")}}else{try{var i=k.value.length;k.selectionStart=i;k.selectionEnd=i;bdimeHW.hasF=1}catch(a){}}function Z(){ime_t1=(new Date()).getTime();var A=this.name.split("_")[1];if(j.bdime){bdime.control.closeIme()}W(A,true);return false}function W(B,A){var C=0;if(B==r[1]){C=1;G("mHolder").style.display="inline-block";c.innerHTML='<span id="imeS" class="hw">'+q[1]+"</span>";if(e){G("imeS").setAttribute("unselectable","on")}function D(){if(!o[1]){if(Y.selection&&Y.activeElement.id&&Y.activeElement.id=="kw"){bdimeHW.hasF=1}bdimeHW.input=imeTar;bdimeHW.submit="su";X(m[1]);setTimeout(function(){if(bdsug){bdsug.sug.initial()}},1000);o[1]=1}else{bdimeHW.reloaddisabled(A)}}if(A){D()}else{addEV(G("imeS"),"click",D)}}else{if(B==r[2]){C=2;G("mHolder").style.display="inline-block";c.innerHTML="<span>"+q[2]+"</span>";if(!o[2]){X(m[2]);o[2]=1}else{if(j.bdime){bdime.voidIme()}}}else{c.innerHTML="<span>"+q[0]+"</span>"}}if(A&&l){var E=new Date();E.setTime(E.getTime()+365*24*3600*1000);Y.cookie="bdime="+C+";domain=baidu.com;path=/;expires="+E.toGMTString()}}function X(A){if(A){var B=Y.createElement("script");B.src=A;Y.getElementsByTagName("head")[0].appendChild(B)}}function p(B){var B=B||window.event;var A=B.target||B.srcElement;f.style.display=A.id=="mCon"&&f.style.display!="block"?"block":"none"}addEV(Y,"click",p)})();</script>
+<script src="user/js/u.js"></script>
+
+
+</html><!--afa2eb9a8e2636eb--> \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/user/js/u.js b/mobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/user/js/u.js
new file mode 100755
index 0000000000..8c9ae60860
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/baidu.com/www.baidu.com/user/js/u.js
@@ -0,0 +1 @@
+function user_c(F){var G=encodeURIComponent(window.document.location.href),E="",D="",A="",B="",C=window["BD_PS_C"+(new Date()).getTime()]=new Image();for(v in F){switch(v){case"title":A=encodeURIComponent(F[v].replace(/<[^<>]+>/g,""));break;case"url":A=encodeURIComponent(F[v]);break;default:A=F[v]}E+=v+"="+A+"&"}B="&mu="+G;C.src="httpdisabled://nsclick.baidu.com/v.gif?pid=201&pj=psuser&"+E+"path="+G+"&wd="+D+"&t="+new Date().getTime();return true}var const_callback_list="login_success";var bdu=(function(){var I="passport.baidu.com";var D=navigator.userAgent.indexOf("MSIE")!=-1&&!window.opera;if(!window[const_callback_list]){window[const_callback_list]=[]}function O(C){return document.getElementById(C)}function A(Q,C,P){var G=document.createElement(Q);if(C){G.id=C}if(P){G.className=P}return G}function H(P,G,C,Q){if(D){P.attachEvent("on"+G,C)}else{P.addEventListener(G,C,!Q)}}function M(P,G,C){if(D){P.detachEvent("on"+G,C)}else{P.removeEventListener(G,C,true)}}function N(C){C=C||window.event;C.stopPropagation?C.stopPropagation():(C.cancelBubble=true)}function B(P,Q,C){if(C!=undefined){P.style[Q]=C}else{if(P.style[Q]){return P.style[Q]}else{if(P.currentStyle){return P.currentStyle[Q]}else{if(document.defaultView&&document.defaultView.getComputedStyle){Q=Q.replace(/([A-Z])/g,"-\u00241").toLowerCase();var G=document.defaultView.getComputedStyle(P,"");return G&&G.getPropertyValue(Q)||""}}}}}function F(G){if(D){var P=document.createStyleSheet();P.cssText=G}else{var C=document.createElement("style");C.type="text/css";C.appendChild(document.createTextNode(G));document.getElementsByTagName("HEAD")[0].appendChild(C)}}(function(){try{var G=[".bd_dialog{border:2px solid #a8b9eb;background:#dae4ff;color:#333;overflow:hidden}",".bd_dialog_handle{width:100%;height:30px;overflow:hidden;background:url(/user/img/bg1.gif) repeat-x;cursor:move;-moz-user-select:none}",".bd_dialog_title{line-height:24px;font-size:14px;font-weight:bold;float:left;overflow:hidden;margin:3px 10px}",".bd_dialog_close{width:19px;height:19px;float:right;background:url(/user/img/close.gif);overflow:hidden;margin:6px;cursor:pointer}",".bd_dialog_main{width:auto;height:auto;margin:5px;border:1px solid #c3cff2;overflow:hidden;background:#FFF}",".bd_tab{margin:20px 20px 10px 20px}",".bd_tab_btns{overflow:hidden;height:28px;padding-left:10px;background:url(/user/img/p.gif) repeat-x bottom;text-align:center}",".bd_tab_conts{margin-top:15px}",".bd_tab_btn{overflow:hidden;float:left;cursor:pointer;font-size:14px;font-weight:bold;width:76px;margin-left:6px;line-height:28px;background:url(/user/img/bb.gif);color:#00c;text-decoration:underline}",".bd_tab_btn_s{overflow:hidden;float:left;cursor:pointer;font-size:14px;font-weight:bold;width:76px;margin-left:6px;line-height:28px;background:url(/user/img/bs.gif)}",".bd_tab_cont{display:none;height:auto}",".bd_tab_cont_s{display:block}","#login_div{font-size:14px}","#login_div u{text-decoration:none;font-size:13px;color:#f00;display:inline-block;height:auto}","#login_msg_div{overflow:hidden;height:18px}","#login_cap{margin-left:20px}","#login_msg_div{margin-left:20px}","#login_tb_div{margin-left:20px}","#login_tb td{padding:4px 4px 4px 0;vertical-align:middle;font-size:14px;line-height:normal}","#login_tb td.lb{width:60px}","#login_tb input.lt{padding:1px;line-height:18px;height:24px;*height:18px;width:170px}","#login_tb input.v{padding:1px;line-height:18px;height:24px;*height:18px}","#login_tb label{font-size:13px}","#login_tb .c9{font-size:13px;color:#999}","#login_check{line-height:130%}","#login_v_tr{display:none}","#login_v_img{border:1px solid #000}","#login_tb a.la{font-size:13px;*margin-bottom:4px;color:#03c;display:inline-block;line-height:30px;vertical-align:middle}","#login_submit{width:78px;height:28px;font-size:14px}","#login_line{background:#d9e1f7;overflow:hidden;height:1px;width:auto;margin:20px 0}","#login_sug{width:100%;line-height:40px;font-weight:bold;text-align:center}","#login_u_msg{width:40px}","#login_p_msg{width:40px}"];F(G.join(""))}catch(C){}})();function L(){var R=O("u");var G=O("lb");var C=[{text:"ËÑË÷ÉèÖÃ",url:"httpdisabled://www.baidu.com/gaoji/preferences.html",tab:"setting"},{text:"ÎÒµÄÓ¦ÓÃÖÐÐÄ",url:"httpdisabled://app.baidu.com/store/mine",tab:"myapp"}];var Q=0;if(R&&G){H(G,"click",function(){switch(Q){case 0:break;case 1:bdlogin.box.show();break}})}function P(){Q=1;T()}function S(U,V){R.innerHTML="<a target=_blank href=http://passport.baidu.com onmousedown=\"return user_c({'fm':'set','tab':'username','url':this.href})\"><b>"+U+"</b></a>&nbsp;|&nbsp;<span id=\"u_m\" onmousedown=\"return user_c({'fm':'set','tab':'more','url':this.href})\"><span id=\"u_ms\">ÉèÖÃ</span><small>¨‹</small></span>&nbsp;|&nbsp;<a href=http://passport.baidu.com/?logout&tpl=mn onmousedown=\"return user_c({'fm':'set','tab':'logout','url':this.href})\">Í˳ö</a>";setTimeout(function(){T()},10)}function T(){var X=O("u_m");if(X){var Y=A("div","u_m_tip");var W=[];for(var V=0,U=C.length;V<U;V++){var Z=C[V];W.push("<a "+((V==U-1)?"class='last'":"")+" href='"+Z.url+"' target='_self' onmousedown=\"return user_c({'fm':'set','tab':'"+Z.tab+"','url':this.href})\">"+Z.text+"</a>")}Y.innerHTML=W.join("");document.body.appendChild(Y);H(Y,"click",N,true);H(X,"click",function(a){Y.style.display="block";N(a)},true);H(document,"click",function(){Y.style.display="none"},true)}}return{login:S,notifyReady:P}}function E(P){var U=A("div");var V=[];var R=[];var C=0;function W(){Q()}function Q(){var d=P.cssPrefix;var f=P.tabs;var a=P.selectIndex;C=a;U.innerHTML="";U.className=d;V=[];R=[];var e=A("div");e.className=d+"_btns";var Y=A("div");Y.className=d+"_conts";for(var Z=0,b=f.length;Z<b;Z++){var g=f[Z];var X=A("span");var h=A("div");if(Z==a){X.loaddisabled=true;if(g.loaddisabled){g.loaddisabled.call(window)}X.className=d+"_btn_s";h.className=d+"_cont_s"}else{X.loaddisabled=false;X.className=d+"_btn";h.className=d+"_cont"}X.innerHTML=g.label;h.appendChild(g.domNode);H(X,"click",(function(c){return function(){T(c)}})(Z));V.push(X);R.push(h);e.appendChild(X);Y.appendChild(h)}U.appendChild(e);U.appendChild(Y)}function S(X,Y){for(i=0,len=V.length;i<len;i++){V[i].className=Y+"_btn";R[i].className=Y+"_cont"}V[X].className=Y+"_btn_s";R[X].className=Y+"_cont_s"}function T(Y){var a=V[Y];var Z=P.tabs[Y].loaddisabled;var X=P.tabs[Y].click;C=Y;if(!a.loaddisabled){a.loaddisabled=true;if(Z){Z.call(window)}}S(Y,P.cssPrefix);if(X){X.call(window)}}function G(){return C}W();return{getIndex:G,show:T,domNode:U}}function K(G){var r=window,z=document.body,x=document.documentElement;var V=A("span");var p=G.width||395,w=G.height||400;var c,a,e,j,f=8;var P=null;function s(){C()}function g(){z.appendChild(V);if(G.loaddisabled){G.loaddisabled.call(window)}T(true)}function q(b){try{z.removeChild(V)}catch(d){}if(!b&&P){try{P.call(window,0)}catch(d){}P=null}}function T(b){if(V.sbIE6){V.sbIE6.redraw()}if(b&&V.msk){V.msk.redraw()}if(V.dlg){V.dlg.redraw()}if(V.shd){V.shd.redraw()}}function X(b,d){p=b;w=d;if(V.dlg){V.dlg.resize(b,d)}if(V.shd){V.shd.resize(b,d)}}function C(){V.innerHTML="";if(D){V.sbIE6=o();V.appendChild(V.sbIE6)}if(G.mask){V.msk=W();V.appendChild(V.msk)}V.dlg=R("µÇ¼");V.appendChild(V.dlg);if(G.shadow){V.shd=t();V.appendChild(V.shd)}H(r,"resize",function(){T(true)})}function U(d){d=r.event||d;var b=AA(d.clientX-c,0,e);var AB=AA(d.clientY-a,0,j);B(V.dlg,"left",b+"px");B(V.dlg,"top",AB+"px");if(V.shd){B(V.shd,"left",b+f+"px");B(V.shd,"top",AB+f+"px")}}function u(){M(z,"mousemove",U);M(z,"mouseup",u);if(V.dlg.releaseCapture){V.dlg.releaseCapture()}if(r.releaseEvents){r.releaseEvents(Event.MOUSEMOVE|Event.MOUSEUP)}}function S(){if(G.ready){G.ready.call(window)}}function W(){var b=Q();var d=n("#333",40,b);d.redraw=function(){var AB=Q(true);l(d,AB);setTimeout(function(){var AC=Q();l(d,AC)},0)};return d}function o(){var b=Q();var d=A("iframe");B(d,"position","absolute");B(d,"top",b.y+"px");B(d,"left",b.x+"px");B(d,"zIndex",10000);B(d,"opacity",1/100);B(d,"filter","alpha(opacity=1)");B(d,"width",b.w+"px");B(d,"height",b.h+"px");d.redraw=function(){var AB=Q(true);B(d,"width",AB.w+"px");B(d,"height",AB.h+"px");setTimeout(function(){var AC=Q();B(d,"width",AC.w+"px");B(d,"height",AC.h+"px")},0)};return d}function Q(d){var b=d?k():Y();b.x=0;b.y=0;b.z=10001;return b}function t(){var b=n("#333",20,Z());b.redraw=function(){var d=Z();l(b,d)};b.resize=y;return b}function R(AF){var d=n("",100,Z(true));d.className="bd_dialog";var AC=A("div");AC.className="bd_dialog_handle";var AD=A("span");AD.className="bd_dialog_title";AD.innerHTML=AF;AC.appendChild(AD);var AB=A("span");AB.className="bd_dialog_close";H(AB,"click",function(){user_c({fm:"set",tab:"close",url:"httpdisabled://passport.baidu.com/"});q()});AC.appendChild(AB);var AE=A("span");B(AE,"clear","both");AC.appendChild(AE);if(G.drag){H(AC,"mousedown",function(AG){AG=r.event||AG;var AH=Y();c=AG.clientX-d.offsetLeft;a=AG.clientY-d.offsetTop;e=AH.w-d.clientWidth-f;j=AH.h-d.clientHeight-f;H(z,"mousemove",U);H(z,"mouseup",u);H(r,"scroll",u);if(d.setCapture){d.setCapture()}else{H(r,"mouseup",u)}if(r.captureEvents){r.captureEvents(Event.MOUSEMOVE|Event.MOUSEUP)}})}var b=A("div");b.className="bd_dialog_main";if(G.domNode){b.appendChild(G.domNode)}d.caption=function(AG){if(AG){AD.innerHTML=AG}return AD.innerHTML};d.clear=function(){b.innerHTML=""};d.redraw=function(){var AG=Z(true);l(d,AG)};d.resize=y;d.appendChild(AC);d.appendChild(b);return d}function n(d,AB,b){var AC=A("div");B(AC,"position","absolute");B(AC,"top",b.y+"px");B(AC,"left",b.x+"px");B(AC,"zIndex",b.z);B(AC,"backgroundColor",d);B(AC,"opacity",AB/100);B(AC,"filter","alpha(opacity="+AB+")");B(AC,"width",b.w+"px");B(AC,"height",b.h+"px");return AC}function y(d,AC){var AE=Y();maxX=AE.w-d-f;maxY=AE.h-AC-f;var AB=d-parseInt(B(this,"width"));var AD=AC-parseInt(B(this,"height"));var b=AA(parseInt(B(this,"left"))-AB/2,0,maxX);var AF=AA(parseInt(B(this,"top"))-AD/2,0,maxY);B(this,"top",AF+"px");B(this,"left",b+"px");B(this,"width",d+"px");B(this,"height",AC+"px")}function l(d,b){B(d,"top",b.y+"px");B(d,"left",b.x+"px");B(d,"width",b.w+"px");B(d,"height",b.h+"px")}function Z(AB){var b=k();var d=AB?0:f;return{z:AB?10005:10002,x:AA((b.w-p)/2,0)+d+b.l,y:AA((b.h-w)/2,0)+d+b.t,w:p,h:w}}function AA(d,AB,b){if(b){d=d>b?b:d}return d>=AB?d:AB}function Y(d){var AB=Math.max(z.scrollHeight,x.scrollHeight);var b=Math.max(z.scrollWidth,x.scrollWidth);if(x&&x.clientWidth){AB=Math.max(x.clientHeight,AB);b=Math.max(x.clientWidth,b)}else{AB=Math.max(z.clientHeight,AB);b=Math.max(z.clientWidth,b)}return{h:AB,w:b}}function k(){var b,d;if(x&&x.clientWidth){b=x.clientWidth;d=x.clientHeight}else{b=z.clientWidth;d=z.clientHeight}return{w:b,h:d,t:Math.max(z.scrollTop,x.scrollTop),l:Math.max(z.scrollLeft,x.scrollLeft)}}function m(b){if(V.dlg){V.dlg.caption(b)}}function h(b){P=b}s();return{resize:X,setCloseCallback:h,caption:m,ready:S,show:g,close:q,domNode:V}}function J(Q){var AG="httpdisabledsdisabled://"+I+"/";var n=Q.prefix||"login";var AI=Q.callbackName||"baiduLoginReply";var a=Q.jump||"";var Y=Q.showVerifyCode;var k=Q.hideVerifyCode;var R=Q.success;var g=null;var d=false;var X=false;var AB=A("div",n+"_div");var p,j,w,h,b,AD,q,AH,T,r,m,U,z,y,o;var G={0:"µÇ¼³É¹¦",1:"Óû§Ãû¸ñʽ´íÎó",2:"Óû§²»´æÔÚ",3:"",4:"µÇ¼ÃÜÂë´íÎó",5:"½ñÈյǼ´ÎÊý¹ý¶à¡£",6:"ÑéÖ¤Â벻ƥÅä",7:"µÇ¼ʱ·¢Éúδ֪´íÎó£¬ÇëÖØеǼ¡£",8:"µÇ¼ʱ·¢Éúδ֪´íÎó£¬ÇëÖØеǼ¡£",16:"¶Ô²»Æð£¬ÄúÏÖÔÚÎÞ·¨µÇ¼¡£",20:"´ËÕʺÅÒѵǼÈËÊý¹ý¶à¡£",256:"",257:"ÇëÊäÈëÑéÖ¤Âë","default":"µÇ¼ʱ·¢Éúδ֪´íÎó£¬ÇëÖØеǼ¡£"};function P(){AB.innerHTML=Z()}function Z(){var AJ="<div id="+n+"_cap>"+Q.caption+"</div><div id="+n+"_msg_div><u id="+n+"_msg></u></div><div id="+n+"_tb_div><form method=post id="+n+"_fm action="+AG+"api/?login target=login_Hide_Frame><table id="+n+"_tb cellpadding=0 cellspacing=0><tr><td class=lb>Óû§Ãû£º<td><input class=lt type=text id="+n+"_u><td><u id="+n+"_u_msg></u><tr><td>ÃÜ¡¡Â룺<td><input class=lt type=password id="+n+"_p><td><u id="+n+"_p_msg></u><tr id="+n+"_v_tr><td >ÑéÖ¤Â룺<td colspan=2 valign=middle><input id="+n+"_v class=v type=text size=4> <img align=middle id="+n+"_v_img src='about:blank'> <a id="+n+"_v_re class=la href=# >¿´²»Ç壿</a><tr><td><td id="+n+"_check><input style='margin-left:-1px;*margin-left:-3px' type=checkbox id="+n+"_mem value='on' checked> <label for="+n+"_mem>¼ÇסÎҵĵǼ״̬</label><br><span class=c9>ÔÚ¹«ÓõçÄÔÉÏÇëÎð¹´Ñ¡</span><td><tr><td><td colspan=2><input type=submit id="+n+"_submit value=µÇ¼> <a id="+n+"_getpass class=la href=# target=_blank>Íü¼ÇÃÜÂë</a><input type=hidden id="+n+"_token><input type=hidden id="+n+"_tpl><input type=hidden id="+n+"_time><input type=hidden name=callback value="+AI+"><input type=hidden name=staticpage value="+a+"></table></form></div><div id="+n+"_line></div><div id="+n+"_sug>"+Q.footer+"</div><iframe style='display:none' src='about:blank' id=login_Hide_Frame name=login_Hide_Frame></iframe>";return AJ}function x(){P()}function f(){p=O(n+"_msg");j=O(n+"_u_msg");w=O(n+"_p_msg");h=O(n+"_u");AD=O(n+"_p");q=O(n+"_v");T=O(n+"_v_img");AH=O(n+"_v_tr");loginValiRe=O(n+"_v_re");b=O(n+"_mem");r=O(n+"_getpass");m=O(n+"_submit");U=O(n+"_fm");z=O(n+"_token");y=O(n+"_tpl");o=O(n+"_time");H(U,"submit",(function(){return function(AJ){if(!AC()){AE(AJ);return false}else{m.disabled=true;return true}}})());H(loginValiRe,"click",function(AJ){s();AE(AJ);return false});H(h,"blur",function(){var AJ=A("script");AJ.src=AG+"?logcheck&username="+encodeURIComponent(h.value)+"&callback=bdlogin.login.ucheck&tpl=mn&t="+(new Date()).getTime();document.body.appendChild(AJ);return true})}function u(AJ){if(!X){switch(AJ){case"0":W(false);d=false;break;case"1":W(true);d=true;break}}}function AF(){var AK=(new Date()).getTime();var AJ=O("bd_login_script");if(AJ){document.body.removeChild(AJ)}var AL=A("script","bd_login_script");if(h){AL.src=AG+"?apilogin&callback=bdlogin.login.ready&tpl=mn&username="+h.value+"&tt="+AK}else{AL.src=AG+"?apilogin&callback=bdlogin.login.ready&tpl=mn&tt="+AK}document.body.appendChild(AL)}function V(AJ){f();if("0"==AJ.error_no){r.href=AJ.more_ext.ext1_url;r.innerHTML=AJ.more_ext.ext1_name;l(AJ.param_in,AJ.param_out);window[AI]=e}}function AE(AJ){if(AJ&&AJ.preventDefault){AJ.preventDefault()}else{window.event.returnValue=false}}function AC(){if(""==C(h.value)){S(null,"ÇëÊäÈëÓû§Ãû",null,null,true);return false}if(""==C(AD.value)){S(null,null,"ÇëÊäÈëÃÜÂë",null,true);return false}if(d&&""==C(q.value)){S("ÇëÊäÈëÑéÖ¤Âë",null,null,null,true);return false}return true}function l(AJ,AK){h.name=AJ.param1_name;h.value=AJ.param1_value;AD.name=AJ.param2_name;AD.value=AJ.param2_value;b.name=AJ.param5_name;q.name=AJ.param4_name;q.value="";s();if("1"==AJ.param4_value){W(true);d=true;X=true}else{W(false);d=false;X=false}z.name=AK.param1_name;z.value=AK.param1_contex;y.name=AK.param2_name;y.value=AK.param2_contex;o.name=AK.param3_name;o.value=AK.param3_contex;if(""==h.value){h.focus()}else{AD.focus()}}function s(){var AJ="httpdisabledsdisabled://"+I+"/?verifypic&t="+(new Date()).getTime();T.src=AJ}function W(AJ){if(D){AH.style.display=AJ?"block":"none"}else{AH.style.display=AJ?"table-row":"none"}if(AJ&&Y){Y.call(window)}if(!AJ&&k){k.call(window)}}function e(AK,AL){m.disabled=false;switch(AK){case"0":var AJ=decodeURIComponent(AL.un);c(1,AJ);break;case"1":S(null,G[AK]);break;case"2":S(null,G[AK]);break;case"4":S(null,null,G[AK]);break;case"5":S(G[AK]);break;case"6":S(G[AK]);break;case"7":S(G[AK]);break;case"8":S(G[AK]);break;case"16":S(G[AK]);break;case"20":S(G[AK]);break;case"257":S(G[AK]);break;default:S()}}function c(AK,AJ){if(R){R.call(window,AK,AJ)}if(g){g.call(window,AK,AJ);g=null}}function S(AM,AN,AK,AL,AJ){if(AN||AK){B(h,"width","128px");B(AD,"width","128px");B(j,"width","100px");B(w,"width","100px")}else{B(h,"width","170px");B(AD,"width","170px");B(j,"width","40px");B(w,"width","40px")}p.innerHTML=AM?AM:"";j.innerHTML=AN?AN:"";w.innerHTML=AK?AK:"";if(AN){h.focus()}if(AK){AD.focus()}if(d&&AL){q.focus()}if(!AJ){AF()}}function AA(){return d||X}function t(AJ){g=AJ}function C(AJ){AJ=AJ.replace(/(\u3000+)|(\u3000+)/g,"");AJ=AJ.replace(/( +)|( +)/g,"");return AJ}x();return{domNode:AB,verify:AA,loaddisabled:AF,ready:V,ucheck:u,setCallback:t,success:c}}return{Bar:L,Tab:E,Dialog:K,Login:J}})();var bdlogin=[];bdlogin.bar=bdu.Bar();var lParams={prefix:"login",callbackName:"bdLoginReply",jump:"httpdisabled://www.baidu.com/user/j.html",caption:"°Ù¶È×¢²áÓû§ÇëÖ±½ÓµÇ¼",footer:"ûÓаٶÈÕʺţ¿<a href='javascript:showRegTab()'>Á¢¼´×¢²á°Ù¶ÈÕʺÅ</a>",success:function(E,B){if(E==1){user_c({fm:"set",tab:"loginOK",url:"httpdisabled://passport.baidu.com/login"})}else{if(E==2){user_c({fm:"set",tab:"regOK",url:"httpdisabled://passport.baidu.com/reg"})}}bdlogin.box.close(true);for(var C=0,A=window[const_callback_list].length;C<A;C++){var D=window[const_callback_list][C];try{D.call(window,E,B,null)}catch(F){}}window.bdUser=B},showVerifyCode:function(){if(bdlogin.tab.getIndex()==0){bdlogin.box.resize(395,436)}},hideVerifyCode:function(){if(bdlogin.tab.getIndex()==0){bdlogin.box.resize(395,386)}}};bdlogin.login=bdu.Login(lParams);window[const_callback_list].push(function(C,A,B){bdlogin.bar.login(A)});window.regSuccess=function(A){bdlogin.login.success(2,A)};window.showRegTab=function(){if(bdlogin.tab){bdlogin.tab.show(1)}};var regDiv=document.createElement("DIV");var tabParams={tabs:[{label:"µÇ¼",domNode:bdlogin.login.domNode,click:function(){setTimeout(function(){if(bdlogin.login.verify()){bdlogin.box.resize(395,436)}else{bdlogin.box.resize(395,386)}bdlogin.box.caption("µÇ¼")},30)},loaddisabled:function(){}},{label:"×¢²á",domNode:regDiv,click:function(){bdlogin.box.resize(570,510);bdlogin.box.caption("×¢²á")},loaddisabled:function(){regDiv.innerHTML="<iframe src=/user/reg.html frameborder=0 scrolling=no width=515 height=390 style='border:0;margin:0;padding:0'></iframe>"}}],selectIndex:0,cssPrefix:"bd_tab"};bdlogin.tab=bdu.Tab(tabParams);var dParams={domNode:bdlogin.tab.domNode,width:395,height:400,mask:true,shadow:true,drag:true,ready:function(){bdlogin.bar.notifyReady()},loaddisabled:function(){bdlogin.tab.show(0);bdlogin.login.loaddisabled()}};bdlogin.box=bdu.Dialog(dParams);bdlogin.login.setCallback(function(){});bdlogin.box.ready();window._invoke_login=function(B){if(B){try{bdlogin.login.setCallback(B);bdlogin.box.setCloseCallback(B)}catch(A){return false}}bdlogin.box.show();return true}; \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/b.scorecardresearch.com/b2@c1=2&c2=6035051&c3=&c4=www.bbc.co.uk%2Fnews%2F&c5=&c6=&c15=&cv=1.3&cj=1.html b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/b.scorecardresearch.com/b2@c1=2&c2=6035051&c3=&c4=www.bbc.co.uk%2Fnews%2F&c5=&c6=&c15=&cv=1.3&cj=1.html
new file mode 100755
index 0000000000..e69de29bb2
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/b.scorecardresearch.com/b2@c1=2&c2=6035051&c3=&c4=www.bbc.co.uk%2Fnews%2F&c5=&c6=&c15=&cv=1.3&cj=1.html
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/bbc.112.2o7.net/b/ss/bbcwglobalprod/1/H.21--NS/0@AQB=1&pccr=true&AQE=1 b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/bbc.112.2o7.net/b/ss/bbcwglobalprod/1/H.21--NS/0@AQB=1&pccr=true&AQE=1
new file mode 100755
index 0000000000..f4a2493a1f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/bbc.112.2o7.net/b/ss/bbcwglobalprod/1/H.21--NS/0@AQB=1&pccr=true&AQE=1
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/edge.quantserve.com/quant.js b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/edge.quantserve.com/quant.js
new file mode 100755
index 0000000000..2e74eb3e1e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/edge.quantserve.com/quant.js
@@ -0,0 +1,28 @@
+if(!__qc){var __qc={qcdst:function(){if(__qc.qctzoff(0)!=__qc.qctzoff(6))return 1;return 0;},qctzoff:function(m){var d1=new Date(2000,m,1,0,0,0,0);var t=d1.toGMTString();var d3=new Date(t.substring(0,t.lastIndexOf(" ")-1));return d1-d3;},qceuc:function(s){if(typeof(encodeURIComponent)=='function'){return encodeURIComponent(s);}
+else{return escape(s);}},qcrnd:function(){return Math.round(Math.random()*2147483647);},qcgc:function(n){var v='';var c=document.cookie;if(!c)return v;var i=c.indexOf(n+"=");var len=i+n.length+1;if(i>-1){var end=c.indexOf(";",len);if(end<0)end=c.length;v=c.substring(len,end);}
+return v;},qcdomain:function(){var d=document.domain;if(d.substring(0,4)=="www.")d=d.substring(4,d.length);var a=d.split(".");var len=a.length;if(len<3)return d;var e=a[len-1];if(e.length<3)return d;d=a[len-2]+"."+a[len-1];return d;},qhash2:function(h,s){for(var i=0;i<s.length;i++){h^=s.charCodeAt(i);h+=(h<<1)+(h<<4)+(h<<7)+(h<<8)+(h<<24);}
+return h;},qhash:function(s){var h1=0x811c9dc5,h2=0xc9dc5118;var hash1=__qc.qhash2(h1,s);var hash2=__qc.qhash2(h2,s);return(Math.round(Math.abs(hash1*hash2)/65536)).toString(16);},sd:["4dcfa7079941","127fdf7967f31","588ab9292a3f","32f92b0727e5","22f9aa38dfd3","a4abfe8f3e04","18b66bc1325c","958e70ea2f28","bdbf0cb4bbb","65118a0d557","40a1d9db1864","18ae3d985046","3b26460f55d"],qcsc:function(){var s="";var d=__qc.qcdomain();if(__qc.qad==1)return";fpan=u;fpa=";var qh=__qc.qhash(d);for(var i=0;i<__qc.sd.length;i++){if(__qc.sd[i]==qh)return";fpan=u;fpa=";}
+var u=document;var a=__qc.qcgc("__qca");if(a.length>0){s+=";fpan=0;fpa="+a;}
+else{var da=new Date();a='P0-'+__qc.qcrnd()+'-'+da.getTime();u.cookie="__qca="+a+"; expires=Sun, 18 Jan 2038 00:00:00 GMT; path=/; domain="+d;a=__qc.qcgc("__qca");if(a.length>0){s+=";fpan=1;fpa="+a;}
+else{s+=";fpan=u;fpa=";}}
+return s;},qcdc:function(n){document.cookie=n+"=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/; domain="+__qc.qcdomain();},qpxloaddisabled:function(img){if(img&&typeof(img.width)=="number"&&img.width==3){__qc.qcdc("__qca");}},qcp:function(p,myqo){var s='',a=null;var media='webpage',event='loaddisabled';if(myqo!=null){for(var k in myqo){if(typeof(k)!='string'){continue;}
+if(typeof(myqo[k])!='string'){continue;}
+if(k=='qacct'){a=myqo[k];continue;}
+s+=';'+k+p+'='+__qc.qceuc(myqo[k]);if(k=='media'){media=myqo[k];}
+if(k=='event'){event=myqo[k];}}}
+if(typeof a!="string"){if((typeof _qacct=="undefined")||(_qacct.length==0))return'';a=_qacct;}
+if(media=='webpage'&&event=='loaddisabled'){for(var i=0;i<__qc.qpixelsent.length;i++){if(__qc.qpixelsent[i]==a)return'';}
+__qc.qpixelsent.push(a);}
+if(media=='ad'){__qc.qad=1;}
+s=';a'+p+'='+a+s;return s;},qcesc:function(s){return s.replace(/\./g,'%2E').replace(/,/g,'%2C');},qcd:function(o){return(typeof(o)!="undefined"&&o!=null);},qcogl:function(){var m=document.getElementsByTagName('meta');var o='';for(var i=0;i<m.length;i++){if(o.length>=1000)return o;if(__qc.qcd(m[i])&&__qc.qcd(m[i].attributes)&&__qc.qcd(m[i].attributes.property)&&__qc.qcd(m[i].attributes.property.value)&&__qc.qcd(m[i].content)){var p=m[i].attributes.property.value;var c=m[i].content;if(p.length>3&&p.substring(0,3)=='og:'){if(o.length>0)o+=',';var l=(c.length>80)?80:c.length;o+=__qc.qcesc(p.substring(3,p.length))+'.'+__qc.qcesc(c.substring(0,l));}}}
+return __qc.qceuc(o);},firepixel:function(qoptions){var e=(typeof(encodeURIComponent)=='function')?"n":"s";var r=__qc.qcrnd();var sr='',qo='',qm='',url='',ref='',je='u',ns='1';var qocount=0;__qc.qad=0;if(typeof __qc.qpixelsent=="undefined"){__qc.qpixelsent=new Array();}
+if(typeof qoptions!="undefined"&&qoptions!=null){__qc.qopts=qoptions;for(var k in __qc.qopts){if(typeof(__qc.qopts[k])=='string'){qo=__qc.qcp("",__qc.qopts);break;}else if(typeof(__qc.qopts[k])=='object'&&__qc.qopts[k]!=null){++qocount;qo+=__qc.qcp("."+qocount,__qc.qopts[k]);}}}else if(typeof _qacct=="string"){qo=__qc.qcp("",null);}
+if(qo.length==0)return;var ce=(navigator.cookieEnabled)?"1":"0";if(typeof navigator.javaEnabled!='undefined')je=(navigator.javaEnabled())?"1":"0";if(typeof _qmeta!="undefined"&&_qmeta!=null){qm=';m='+__qc.qceuc(_qmeta);_qmeta=null;}
+if(self.screen){sr=screen.width+"x"+screen.height+"x"+screen.colorDepth;}
+var d=new Date();var dst=__qc.qcdst();var qs='http';if(window.location.protocol=='https:'){qs+='s';}
+qs="../../pixel.quantserve.com";var fp=__qc.qcsc();if(window.location&&window.location.href)url=__qc.qceuc(window.location.href);if(window.document&&window.document.referrer)ref=__qc.qceuc(window.document.referrer);if(self==top)ns='0';var ogl=__qc.qcogl();var img=new Image();img.alt="";img.src=qs+'/pixel/r.html';img.onloaddisabled=function(){__qc.qpxloaddisabled(img);}},quantserve:function(){if(typeof _qevents=='undefined'){_qevents=[];}
+if(typeof _qoptions!="undefined"&&_qoptions!=null){__qc.firepixel(_qoptions);_qoptions=null;}else if(!_qevents.length&&typeof _qacct!="undefined"){__qc.firepixel(null);}
+if(!__qc.evts){for(var k in _qevents){__qc.firepixel(_qevents[k]);}
+_qevents={push:function(){var a=arguments;for(var i=0;i<a.length;i++){__qc.firepixel(a[i]);}}};__qc.evts=1;}}};}
+function quantserve(){__qc.quantserve();}
+quantserve();
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/js.revsci.net/gateway/gw.js@csid=J08781 b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/js.revsci.net/gateway/gw.js@csid=J08781
new file mode 100755
index 0000000000..a82317b0b6
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/js.revsci.net/gateway/gw.js@csid=J08781
@@ -0,0 +1,4 @@
+//Vermont-12.4.0-1133
+var rsi_now= new Date();
+var rsi_csid= 'J08781';if(typeof(csids)=="undefined"){var csids=[rsi_csid];}else{csids.push(rsi_csid);};function rsiClient(Da){this._rsiaa=Da;this._rsiba=1;this._rsica=1;this._rsida=0;this._rsiea=0;this._rsifa=0;this._rsiga="1003161";this._rsiha="pix04.revsci.net";this._rsiia="js";this._rsija="b";this._rsika="3";this._rsila=3;this._rsima=1;this._rsina=new Array();this._rsioa=0;this._rsipa=null;this._rsiqa=null;this._rsira=null;this._rsisa=null;this._rsita=null;this._rsiua=null;this._rsiva=0;this.DM_cat=function(Ea){this._rsipa=Ea;};this.DM_name=function(Fa){this._rsiqa=Fa;};this.DM_keywords=function(st){this._rsira=st;};this.DM_event=function(Ga){this._rsisa=Ga;};this.DM_addToLoc=function(n,v){this._rsita=_rsiwa(this._rsita,n,v);};this.DM_addEncToLoc=function(n,v){this.DM_addToLoc(_rsixa(n),_rsixa(v));};this.DM_setLoc=function(u){this._rsita=u;};this.rsi_c=function(Da){this._rsiaa=Da;};this.rsi_ral=function(Ha){this._rsiba=Ha;};this.rsi_riu=function(Ia){this._rsica=Ia;};this.rsi_tiu=function(Ja){this._rsida=Ja;};this.rsi_m=function(Ka){this._rsiea=Ka;};this.rsi_dw=function(La){this._rsifa=La;};this.rsi_s=function(Ma){this._rsiha=Ma;};this.rsi_t=function(Na){this._rsiia=Na;};this.rsi_en=function(Oa){this._rsija=Oa;};this.rsi_cn=function(Pa){this._rsika=Pa;};this.rsi_us=function(Qa){this._rsila=Qa;};this.rsi_ra=function(ra){this._rsima=ra;};this.DM_tag=function(){var Ra;if(this._rsioa==0||this._rsiea==1){if(typeof(DM_prepClient)=="function"){DM_prepClient(this._rsiaa,this);}var Sa=this._rsiya();if(this._rsiia=="gif"){Ra=new Image(2,3);Ra.src=Sa;this._rsina[this._rsina.length]=Ra;}else if(this._rsiia=="js"){if(this._rsifa==1){void("<script language=\"JavaScript\" type=\"text/javascript\" src=\""+Sa+"\"><"+"/script>");}else{var Ta=document.createElement("script");Ta.language="JavaScript";Ta.type="text/javascript";Ta.src=Sa;if(document.body==null){document.getElementsByTagName("head")[0].appendChild(Ta);}else{document.body.insertBefore(Ta,document.body.firstChild);}Ra=Ta;}}this._rsioa=1;}this.rsi_r();return Ra;};this._rsiya=function(){var Ua="";this.DM_addEncToLoc("_rsiL",this._rsiva);Ua="DM_LOC="+_rsixa(this._rsita);if(this._rsipa){Ua+="&DM_CAT="+_rsixa(this._rsipa);}if(this._rsisa){Ua+="&DM_EVT="+_rsixa(this._rsisa);}if(this._rsira){Ua+="&DM_KYW="+_rsixa(this._rsira);}if(this._rsica==1&&this._rsiua){Ua+="&DM_REF="+_rsixa(this._rsiua);}if(this._rsida==1){Ua+="&DM_TIT="+_rsixa(document.title);}if(this._rsiqa){Ua+="&DM_NAM="+_rsixa(this._rsiqa);}Ua+="&DM_EOM=1";var Va="httpdisabled"+(location.protocol=="httpdisabledsdisabled:"?"s":"")+"://";var Wa="/"+this._rsiaa+"/"+this._rsija+this._rsika+"/0/"+this._rsila+"/"+this._rsiga+"/";var Xa=Math.floor(Math.random()*1000000000)+"."+this._rsiia;var Ya=Va+this._rsiha+Wa+Xa+"?D="+_rsixa(Ua)+"&C="+_rsixa(csids);var Za=Ya.length;if(Za>=2000){if(Ya.charAt(1998)=='%'){Ya=Ya.substr(0,1998);}else if(Ya.charAt(1999)=='%'){Ya=Ya.substr(0,1999);}else{Ya=Ya.substr(0,2000);}if(Ya.charAt(Ya.length-3)=='%'&&Ya.charAt(Ya.length-2)=='2'&&Ya.charAt(Ya.length-1)=='5'){Ya=Ya.substr(0,Ya.length-3);}}return Ya;};this.rsi_r=function(){var $a;var ab;var bb=0;var cb=0;if(this._rsiba==1){var db=window;while(true){try{$a=db.document.location;ab=db.document.referrer;bb=cb;}catch(notAllowed){}if(db==window.top||db==db.parent){break;}db=db.parent;cb++;}}else{$a=window.document.location;ab=window.document.referrer;}this._rsiva=cb-bb;this._rsiua=this._rsima?_rsiza(ab.toString()):ab.toString();if(this._rsiva==0){this._rsita=(this._rsima)?_rsiza($a.href):$a.href;}else{this._rsita=this._rsiua;}this._rsipa=null;this._rsiqa=null;this._rsira=null;this._rsisa=null;};this.rsi_r();}var _rsixa;if(typeof(encodeURIComponent)=="function"){_rsixa=encodeURIComponent;}else{var _rsiAa=new RegExp("[\x00-\x20]|[\x22-\x26]|[\x2B-\x2C]|\x2F|[\x3A-\x40]|[\x5B-\x5E]|\x60|[\x7B-\x7D]|[\x7F-\uFFFF]","g");_rsixa=function(v){return v.toString().replace(_rsiAa,_rsiBa);}}function _rsiwa(u,n,v){return u+(u.indexOf("?")==-1?"?":"&")+n+"="+v;}function _rsiza(u){var i=u.indexOf('#');return(i>=0)?u.substr(0,i):u;}function _rsiCa(i){var eb=i.toString(16).toUpperCase();return eb.length<2?"0"+eb:eb;}function _rsiBa(c){var i=c.charCodeAt(0);if(isNaN(i))return "";if(i<128)return "%"+_rsiCa(i);if(i<2048)return "%"+_rsiCa(0xC0+(i>>6))+"%"+_rsiCa(0x80+(i&0x3F));if(i<65536)return "%"+_rsiCa(0xE0+(i>>12))+"%"+_rsiCa(0x80+(i>>6&0x3F))+"%"+_rsiCa(0x80+(i&0x3F));return "%"+_rsiCa(0xF0+(i>>18))+"%"+_rsiCa(0x80+(i>>12&0x3F))+"%"+_rsiCa(0x80+(i>>6&0x3F))+"%"+_rsiCa(0x80+(i&0x3F));}window[rsi_csid]=new rsiClient(rsi_csid);
+function DM_cat(aa){window[rsi_csid].DM_cat(aa);}function DM_name(ba){window[rsi_csid].DM_name(ba);}function DM_keywords(kw){window[rsi_csid].DM_keywords(kw);}function DM_event(ca){window[rsi_csid].DM_event(ca);}function DM_addToLoc(n,v){window[rsi_csid].DM_addToLoc(n,v);}function DM_addEncToLoc(n,v){window[rsi_csid].DM_addEncToLoc(n,v);}function DM_setLoc(u){window[rsi_csid].DM_setLoc(u);}function DM_tag(){window[rsi_csid].DM_tag();}
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbc.co.uk/js/app/av/emp/1_1_3_0_0_426652_426614_1/config.sjson@edition=us&site=news&section=%2FFrontpage b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbc.co.uk/js/app/av/emp/1_1_3_0_0_426652_426614_1/config.sjson@edition=us&site=news&section=%2FFrontpage
new file mode 100755
index 0000000000..2794be9253
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbc.co.uk/js/app/av/emp/1_1_3_0_0_426652_426614_1/config.sjson@edition=us&site=news&section=%2FFrontpage
@@ -0,0 +1,195 @@
+/**
+ * namespace Holds at least 1 config JSON object
+ */
+bbc.fmtj.page.empConfig = {};
+
+/**
+ * The base configuration applied to all
+ * EMPs embedded on Journalism sites.
+ *
+ */
+bbc.fmtj.page.empConfig.base = {
+
+ /**
+ * Related to the EMP such as size,
+ * location etc.
+ *
+ */
+ player : {
+ /** the height of the transport controls in the EMP */
+ toolbarPadding: 0,
+ uxHighlightColour: '0xff0000',
+
+ /**
+ * The footer that appears when the EMP voided in a
+ * popout window.
+ */
+ popoutFooterHeight : {
+ /** height of the audio footer */
+ audio: 32,
+ /** height of the video footer */
+ video: 77
+ },
+
+ /* Used to feed the embeddedMedia.Player required version */
+ requiredVersion: "9.0.115"
+
+ },
+
+ /** Supported audio player sizes. The size keys must match those output by CPS */
+ audio : {
+ /** the 'small' size */
+ small : { width: 226, height: 115 },
+ /** the larger size */
+ full : { width: 512, height: 115 }
+ },
+
+ /**
+ * Supported video player sizes
+ * TODO: Are these used
+ */
+ video : {
+ /* should be named sizes? */
+ /* these may not be used...*/
+ standard : { width: 448, height: 252 },
+ popout : { width: 512, height: 323 }
+ },
+
+ /**
+ * Message text for users that do not have the
+ * correct version of Flash to embed the EMP
+ */
+ noFlashMessage : {
+ heading : "Cannot play media.",
+ bodyText : "You do not have the correct version of the flash player. ",
+ linkText : "Downloaddisabled the correct version",
+ linkUrl : "httpdisabled://www.adobe.com/shockwave/downloaddisabled/downloaddisabled.cgi?P1_Prod_Version=ShockwaveFlash"
+ },
+
+ /**
+ * The templates to be used with the noFlashMessage.
+ */
+ noFlashTemplate : {
+ audio: '<div class="audioImage"></div><div class="warning"><p><strong>{heading}</strong>{bodyText} <a href="{linkUrl}">{linkText}</a></p></div>',
+ video: '<img class="holding" src="{holding}" width="{width}" height="{height}"><div class="warning"><p><strong>{heading}</strong>{bodyText} <a href="{linkUrl}">{linkText}</a></p></div>'
+ },
+
+ /**
+ * The URL of the config XML file for configuring the EMP
+ * see http://newsimg.bbc.co.uk/player/emp/1_1_3_0_0_426652_426614_1/
+ */
+ configUrl: "httpdisabled://news.bbc.co.uk/player/emp/1_1_3_0_0_426652_426614_1/config/default.xml",
+
+ /**
+ The settings to be converted into keys such as config_settings_KEY = VALUE
+ and passed to emp.set(key, value)
+
+ example settings: {
+ language: "default"
+ }
+
+ =>
+
+ emp.set( "config_settings_language", "default" );
+
+ see http://newsimg.bbc.co.uk/player/emp/1_1_3_0_0_426652_426614_1/docs/guides/configurationSettings.html
+ */
+ settings: {
+ /** Language to use for EMP interface */
+ language: "default",
+ skin:"silver"
+ },
+
+ /**
+ Same behaviour as settings object above but used to configure
+ any EMP plugins:
+
+ examples plugins: {
+ fmtjLiveStats: {
+ pageType: "eav7"
+ }
+ }
+
+ =>
+
+ emp.set( "config_plugins_fmtjLivestats_pageType", eav7" )
+
+ see http://newsimg.bbc.co.uk/player/emp/1_1_3_0_0_426652_426614_1/docs/guides/plugins.html
+
+ TODO what do we do about livestats tracking for developer usage??
+ */
+ plugins: {
+ /** Livestats plugin parameters are supplied by CPS */
+ fmtjLiveStats: {}
+ },
+
+ // TODO: Add quova geoip logic here to stop appearing when inside the uk
+
+ /**
+ * Options for configuring adverts
+ * when EMP is viewed internationally.
+ */
+ ads: {
+ /**
+ * Configuration for companion ad format
+ */
+ companion: {
+ /** Dimensions of the companion */
+ size: "300x60",
+
+ /** Type of companion */
+ type: "adi",
+
+ /**
+ * Prefix for the id of the companion banner if automatically created
+ * by this embed code (for example if embedded by a developer)
+ */
+ idPrefix: "bbccom_companion_",
+
+ /**
+ * Template for the companion banner dom id
+ */
+ template: '<div class="bbccom_visibility_hidden" id="{id}"><div class="bbccom_companion_text">Advertisement</div>'
+
+ /**
+ * name suppress {boolean}
+ * When true and a developer is embedding then no companion will
+ * be created. Make sure that you also stop the EMP showing a
+ * pre-roll advert by calling:
+ * emp.set( "config_settings_suppressItemKind", "advert" );
+ */
+ }
+ }
+};
+
+
+
+ /*
+ * Begin panorama config
+ */
+
+
+ /*
+ * Begin welsh config
+ */
+
+
+ /*
+ * Begin F1 config
+ */
+
+
+ /*
+ * Begin weather config
+ */
+
+
+ /*
+ * Register EMP
+ */
+ bbc.fmtj.queue.register({namespace:"bbc.fmtj.av.emp",method:"loaddisabled",scripts: {foot: [ "/js/app/av/emp/1_1_3_0_0_426652_426614_1/emp.js" ]}});
+
+/*
+* DemocracyLive and Childrens do not have a site_to_serve variable
+* So there is currently no way to configure them.
+*/
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/css/screen/shared/19_58/3pt_ads.css b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/css/screen/shared/19_58/3pt_ads.css
new file mode 100755
index 0000000000..e5de0645a6
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/css/screen/shared/19_58/3pt_ads.css
@@ -0,0 +1 @@
+body#travel .bbccom_wallpaper #blq-container-inner{overflow:visible;}#bbccom_slot_wallpaper_left{height:600px;left:-310px;position:absolute;top:-40px;width:300px;}#bbccom_slot_wallpaper_right{height:600px;position:absolute;right:-310px;top:-40px;width:300px;}#blq-pre-mast{z-index:9999;}.bbccom_display_none{display:none!important;}.bbccom_visibility_hidden{height:60px;visibility:hidden;}.bbccom_text,.bbccom_text a,.bbccom_text a:hover,.bbccom_text a:active,.bbccom_text a:visited,.lhs .bbccom_text a:link,.lhs .bbccom_text a:visited,.lhs .bbccom_text a:active,.lhs .bbccom_text a:hover,.lhs #bbccom_button .bbccom_text a,.lhs #bbccom_button .bbccom_text a:hover,#bbccom_sponsor_section_text,.bbccom_sponsor_text,.photo_gallery #bbccom_sponsor_section_text,#blq-main .bbccom_companion_text,#blq-main .bbccom_companion_text a,#bbccom_mpu a,#bbccom_mpu_high a,#bbccom_leaderboard a,#galothercontentbox .bbccom_text a:link,#navigation .bbccom_text a:link,#secondary-content .bbccom_text a:link,#secondary-content .bbccom_text a:visited,#secondary-content .bbccom_text a:active,#secondary-content .bbccom_text a:hover,#secondary-content .bbccom_text a:focus,#travel #bbccom_sponsor_section_text,#bbccom_sponsor_section_news,#blq-main .bbccom_adsense h3 a{color:#505050;font:bold 10px/23px helvetica,arial,sans-serif!important;letter-spacing:0!important;text-transform:uppercase;background-color:transparent;}#travel .bbccom_adsense h4 a,#travel .bbccom_adsense h4 p a{color:#000;font:bold 13px/13px arial,helvetica,sans-serif!important;text-transform:none;}#travel #blq-main .bbccom_adsense p{color:#505050;}#travel #blq-main .bbccom_adsense p a{color:#78b200;text-transform:none;}#bbccom_sponsor_section_text,#bbccom_module_special-reports .bbccom_text,#bbccom_leaderboard_container .bbccom_text a,#bbccom_module_programme-breakout .bbccom_text,#bbccom_module_hyper-promotional-content .bbccom_text,.news #header-wrapper #bbccom_sponsor_section .bbccom_text{color:#fff;text-decoration:none;}.bbccom_text{text-align:right;}.bbccom_text,#bbccom_module_range-most-popular_text{text-align:right;}.bbccom_sponsor .bbccom_text{padding-right:3px;display:inline;}.adimg{border-style:none;}.news .bbccom-advert{margin-top:-1px;}.bbccom_slot_mpu .more-galleries li.last-child{display:none;}.news .partner_buttons_2cols{padding:10px 23px 0;width:288px;}.news #bbccom_partner_button1{width:334px;}.news .bbccom_adsense,.blogs .bbccom_adsense{background-color:#fff;border:1px solid #999;}.news #blq-main .bbccom_adsense h3 a,.news #blq-main .bbccom_adsense p,.blogs #blq-main .bbccom_adsense h3 a,.blogs #blq-main .bbccom_adsense p{color:#505050;}.news #blq-main .bbccom_adsense a,.blogs #blq-main .bbccom_adsense a{color:#174f82;}#travel #bbccom_leaderboard,#travel #bbccom_mpu,#travel #bbccom_button,#travel #bbccom_promo_feature_1{background-color:#e8e8e8;}#travel .gallery #bbccom_leaderboard,#travel .gallery #bbccom_mpu,#travel .gallery #bbccom_button,#travel .gallery #bbccom_promo_feature_1{background-color:#212121;}#travel .gallery .bbccom_text a{color:#878787;}#travel #bbccom_leaderboard{line-height:1;padding:0 8px 8px;}#travel #bbccom_mpu{margin-bottom:20px;padding:0 8px;text-align:center;}.bbccom_slot_mpu336 #promo-content #bbccom_mpu{padding:0;}#travel.bbccom_slot_mpu336 #bbccom_mpu{width:336px;}.bbccom_slot_xxl .feature #promo-content div{margin:0 auto;}#travel #bbccom_button{line-height:1;padding:0 8px 8px;}#travel #bbccom_button a{display:inline-block;line-height:1;}#travel #bbccom_sponsor_section{bottom:22px;top:auto;right:0;}#travel #promo-content .bbccom_text{padding-right:8px;}#travel .promo_feature{line-height:1;margin-bottom:24px;width:336px;}#travel #blq-main .bbccom_adsense{background-color:#e8e8e8;}#travel #blq-main .bbccom_adsense h3{border-bottom:1px solid #ABABAB;margin:0 8px;padding:0;text-align:right;}#travel #bbccom_adsense_mpu{margin-top:0;}#bbccom_leaderboard{width:728px;padding:0 2px;margin:0 auto;}#bbccom_leaderboard_container{background-color:#484848;}#bbccom_leaderboard_container #bbccom_leaderboard{padding-bottom:8px;}.bbccom_slot_leaderboard970 #bbccom_leaderboard,.bbccom_slot_leaderboard97066 #bbccom_leaderboard,.bbccom_slot_leaderboard97090 #bbccom_leaderboard{height:auto;padding:0 3px;width:auto;}.bbccom_slot_leaderboard970 #blq-main #bbccom_leaderboard,.bbccom_slot_leaderboard97066 #blq-main #bbccom_leaderboard,.bbccom_slot_leaderboard97090 #blq-main #bbccom_leaderboard{padding:0 3px;}#bbccom_button .bbccom_text{text-align:right;}#bbccom_button{border:0 none;float:left;padding:0 0 8px 20px;width:120px;}.lhs #bbccom_button a:link,.lhs #bbccom_button a:visited,.lhs #bbccom_button a:active{padding:0;}#blq-main .companion_v4{height:60px;padding-top:10px;width:auto;}#blq-main .bbccom_companion{height:84px;margin:auto;overflow:hidden;width:300px;}#blq-main .bbccom_companion div{float:right;height:auto;padding:0;}#blq-main .companion_v4 div.bbccom_companion_text{width:auto;}#story-body .companion_v4{padding-bottom:10px;}.videoInStoryA .bbccom_visibility_show,.videoInStoryB .bbccom_visibility_show{height:auto;margin-top:8px;}#blq-main .companion_parent{background-color:#D5D5D5;height:auto;margin-top:-3px;padding-bottom:10px;text-align:center;}#blq-main .companion_parent .bbccom_companion_text{display:block;float:none;height:auto;margin:0 auto;padding:0;text-align:right;width:300px;}#blq-main .companion_parent .bbccom_companion_text a{background-image:none;float:none;padding:0;}#blq-main .companion_parent div{height:60px;}#blq-main .companion_parent div.bbccom_companion_text{height:24px;}#bbccom_sponsor_section{background:none;height:31px;position:absolute;right:85px;text-align:right;top:27px;width:auto;}.sportbanner #bbccom_sponsor_section{padding:0 28px 0 0;right:-11px;top:-2px;}.sportbanner #bbccom_sponsor_section_text{float:left;margin-top:15px;}.wodban #bbccom_sponsor_section{padding:0 13px 0 0;right:0;top:-3px;}#bbccom_sponsor_section div{background:none;display:inline-block;float:none;margin:0;padding:0;}#bbccom_sponsor_section_text{width:130px;}#bbccom_sponsor_section_image,#bbccom_sponsor_section_image a{width:96px;}#bbccom_sponsor_section_image{padding-left:10px;}.banner #bbccom_sponsor_section div{height:auto;}.news #header-wrapper #bbccom_sponsor_section{left:auto;right:0;top:15px;width:205px;}.news #header-wrapper #bbccom_sponsor_section .bbccom_text{float:left;line-height:38px!important;}.bbccom_sponsor{height:31px;}#bbccom_sponsor_section_news{float:left;margin:0 0 0 10px;}#bbccom_sponsor_section_news p{float:left;margin:20px 5px 0 0;}#bbccom_sponsor_section_news img{display:block;}.news .photo-gallery #bbccom_sponsor_section{left:auto;right:0;top:42px;}.news .photo-gallery #bbccom_sponsor_section .bbccom_text{float:left;padding-top:3px;}.news .photo-gallery #bbccom_sponsor_section a{margin-left:3px;}#socialBookMarks iframe{border:none;}#bbccom_module_page-bookmark-links{height:31px;position:absolute;right:0;text-align:right;top:-10px;width:230px;}.story-related{margin:0 0 24px!important;}#bbccom_module_hyper-promotional-content{background:#D1700E;border-top:1px solid #fff;padding:8px;text-align:right;margin-top:0;}.bbccom_slot_module_special-reports .special-reports .top-report{padding-bottom:4em;}.bbccom_slot_module_special-reports .more-special-reports{display:none;}#bbccom_module_special-reports{text-align:right;min-height:1px;padding:8px 8px 5px 0;position:absolute;bottom:0;right:0;width:100%;z-index:1;}#bbccom_storyprintsponsorship{overflow:auto;border:1px solid #eee;float:right;margin:9px 0 29px 0;width:251px;padding:4px 0;}#bbccom_storyprintsponsorship p{float:left;font-size:.8em;padding:6px 0 6px 6px;text-transform:uppercase;margin-right:10px;}#bbccom_storyprintsponsorship div{height:31px;}#bbccom_storyprintsponsorship a{background-image:none;margin:0;padding:0;}#bbccom_storyprintsponsorship .bbccom_text{display:none;}#bbccom_module_most-popular,#bbccom_module_programme-breakout,#bbccom_module_range-most-popular,#bbccom_module_most-popular-category{clear:both;height:31px;overflow:hidden;padding-top:8px;position:relative;text-align:right;}.bbccom_slot_xxl #bbccom_module_programme-breakout{float:right;padding-bottom:8px;width:336px;}.bbccom_slot_xxl li.no-image #bbccom_module_programme-breakout{right:auto;bottom:auto;position:relative;width:auto;}#bbccom_module_market-data-include{background:#ededed;border-top:1px solid #fff;margin:-16px 0 16px 0;padding:8px 8px 8px 0;text-align:right;}#main-content .story-actions{float:left;width:468px;}#bbccom_mpu,#bbccom_mpu_high{border:0;margin:0 auto;width:300px;}.news #bbccom_mpu{margin:-8px auto 13px;}.news .media-asset #bbccom_mpu{margin-top:0;}.news #bbccom_mpu_high{margin:-6px auto 13px;}.news #bbccom_mpu img,.news #bbccom_mpu_high img{margin-bottom:-6px;}.wgreylinebottom #bbccom_mpu{margin:-14px auto 11px;}td #bbccom_mpu{margin-bottom:16px;}.blq_hp #bbccom_mpu{margin:0;}#galothercontentbox #bbccom_mpu{margin-top:-12px;}.bbccom_slot_xxl #bbccom_mpu,.bbccom_slot_xxl #bbccom_mpu_high{width:468px;margin-left:15px;}.bbccom_slot_mpu336 #bbccom_mpu,.bbccom_slot_mpu336 #bbccom_mpu_high{width:auto;}.bbccom_slot_mpu_skyscraper #bbccom_mpu,.bbccom_slot_mpu_skyscraper #bbccom_mpu_high{height:644px;width:160px;}.mpu_v4{margin:0 0 20px 3px;}#bbccom_adsense_mpu,#secondary-content #bbccom_adsense_mpu{margin-top:10px;}#secondary-content #bbccom_mpu,#secondary-content #bbccom_mpu div,#secondary-content #bbccom_mpu_high,#secondary-content #bbccom_mpu_high div,#secondary-content #bbccom_adsense_mpu,#secondary-content #bbccom_adsense_mpu div{border-bottom:none;}#secondary-content #bbccom_mpu,#secondary-content #bbccom_mpu div,#secondary-content #bbccom_mpu_high,#secondary-content #bbccom_mpu_high div{border-bottom:none;padding-bottom:0;width:300px;}#bbccom_skyscraper{position:absolute;border:0;width:160px;height:600px;text-align:center;}.skyscraper_v4{top:156px;left:1000px;}.skyscraper_v3_5{top:104px;left:815px;}.skyscraper_v3{top:256px;left:770px;}#bbccom_bottom{height:71px;width:470px;}.bottom_v3,.bottom_v3_5{margin:0 0 0 215px;padding:8px 0;}#bbccom_adsense_mpu,#bbccom_adsense_middle{display:inline-block;font-family:arial,sans-serif;}.bbccom_adsense{background-color:#EDF3F3;}#bbccom_adsense_mpu{margin-bottom:10px;}#bbccom_adsense_middle{margin-bottom:20px;width:466px;}#main-content #bbccom_adsense_middle{margin-bottom:20px;margin-top:22px;}.bbccom_adsense h3,.bbccom_adsense h4{font-size:14px;}#blq-main .bbccom_adsense h3{padding:6px;}#blq-main .bbccom_adsense li{background-image:none;padding:6px;}#blq-main .bbccom_adsense h3 a{background-image:none;padding:0;color:#555;font-weight:bold;}#blq-main .bbccom_adsense a{border-bottom:none;color:#006;}#blq-main .bbccom_adsense p{color:#000;font-size:13px;margin:0;padding:0;}#blq-main .bbccom_adsense p a{font-weight:normal;}#blq-main .adsense_mpu_weather_top{margin-top:0;}.adsense_mpu_weather{background-color:#fff;width:296px;}.layout-block-a .bbccom_adsense{width:462px;}.layout-block-a #bbccom_adsense_mpu{margin-top:0;}#secondary-content .adsense_mpu li{background-image:none;}#bbccom_partner_button1{border:1px solid #EAEAEA;float:left;margin:8px 0 16px 0;padding-top:10px;position:relative;}#bbccom_partner_button1 a{display:inline-block;height:30px;margin-left:31px;padding-bottom:10px;width:120px;}#bbccom_partner_button1 .partner_buttons_4cols a{margin-left:27px;}#bbccom_partner_button1 .bbccom_text{position:absolute;right:0;top:-26px;}.bbccom_slot_xxl #bbccom_partner_button1{width:494px;}.sportbanner{position:relative;}#bbccom_bottom_adlabel{color:#bbb;}#bbccom_leaderboard table,#bbccom_leaderboard tr,#bbccom_leaderboard td,#bbccom_leaderboard th{margin:0;padding:0;}.bbccom_slot_sponsor_section div.cg-2010-section{background:url("../../../../sol/shared/img/v4/commonwealth_games_2010/cg_bbccom_banner_sprite2.png") no-repeat scroll 160px 0 transparent;}.cg-2010-section .sportbanner #bbccom_sponsor_section{padding:0 16px 0 0;right:0;}.cg-2010-section #bbccom_sponsor_section_text{color:#000;float:left;margin-top:15px;}.bbccom_slot_sponsor_section a#rss-alternative{margin-right:340px;}.bbccom_slot_sponsor_section #fixturesLink{background-image:none;float:left;line-height:1.3em;margin-left:10px;}.bbccom_slot_sponsor_section .world-cup-2010-section .athleticsbg{margin-top:65px;}.world-cup-2010-section #bbccom_mpu{margin-bottom:20px;}.bbccom_slot_sponsor_section .world-cup-2010-section .contentwrapper,.bbccom_slot_sponsor_section .world-cup-2010-section .contentwrapperwide{padding:32px 0 0 0;}.world-cup-2010-section #bbccom_sponsor_section{color:#505050;left:auto;line-height:55px;right:14px;top:118px;width:230px;}.world-cup-2010-section #bbccom_sponsor_section div{float:left;line-height:53px;}.world-cup-2010-section #bbccom_sponsor_section a{left:0;position:relative;top:0;width:100px;}.bbccom_slot_mpu div.f1-winter-olympics-2010 #bbccom_mpu{padding-bottom:29px;}div.f1-winter-olympics-2010 #bbccom_sponsor_section{left:531px;top:21px;width:260px;}div.f1-winter-olympics-2010 #bbccom_sponsor_section div{float:left;}div.f1-winter-olympics-2010 #bbccom_sponsor_section_text{padding-top:6px;}div.f1-winter-olympics-2010 #bbccom_sponsor_section_image{position:relative;width:100px;}div.f1-winter-olympics-2010 .sportbanner div #bbccom_sponsor_section_image a{left:0;top:0;width:100px;}.embedvideo .latestinfo p{margin-top:388px;}.i #bbccom_leaderboard_adlabel{color:#fff;}#ad,#ad .container{background:transparent;}div.contentBlock a.headerSponsor{display:block;font-size:.667em;font-weight:normal;height:30px;padding:5px 5px 5px 40px;background-color:#EFEDED;}div.contentBlock a.headerSponsor img{float:left;}div.contentBlock a.headerSponsor span{float:left;line-height:26px;margin-right:5px;}.moduleAdvertContent{height:31px;padding:6px 10px;cursor:default;}.moduleAdvertContent a,.moduleAdvertContent a img{float:left;}div.bbccom_module_adlabel{color:#666;float:left;font-size:.8em;margin:6px 10px 0 0;width:180px;}#bbccom_halfbanner{border-top:1px solid #DBDBDB;text-align:center;}div.bbccom_halfbanner_adlabel{color:#666;font-size:.8em;margin:6px 10px 0 0;text-align:center;width:300px;}.bbccom_halfbanner_image{padding:5px 0 10px 0;}#international .colB{min-height:250px;height:auto;padding-top:0;}.blq-js #international.bbccom_slot_mpu .colB{height:auto;padding-top:7px;}.blq-js #international.bbcdotcomAdvertsResetMpu .colB{padding-top:0;height:250px;}.blq-js .bbccom_slot_mpu .colE{margin-top:0!important;}.blq-js #international.bbcdotcomAdvertsResetMpu .colE{margin-top:-250px;}.advert{margin-left:2px;}.news .advert{margin:0 0 -2px 2px;}.front-page .advert{margin:0 0 3px 2px;}.bbcdotcomAdvertsResetMpu #bbccom_mpu{display:none;}.skylightTheme #bbccom_leaderboard_container{background-color:#1778B3;}.doveTheme #bbccom_leaderboard_container{background-color:#5B688F;}.tealTheme #bbccom_leaderboard_container{background-color:#2383A3;}.aquaTheme #bbccom_leaderboard_container{background-color:#158979;}.greenTheme #bbccom_leaderboard_container{background-color:#5D891B;}.violetTheme #bbccom_leaderboard_container{background-color:#6A5789;}.purpleTheme #bbccom_leaderboard_container{background-color:#823892;}.pinkTheme #bbccom_leaderboard_container{background-color:#9D1767;}.oliveTheme #bbccom_leaderboard_container{background-color:#7C7854;}.suedeTheme #bbccom_leaderboard_container{background-color:#695C4A;}.redTheme #bbccom_leaderboard_container{background-color:#9E2C1D;}.orangeTheme #bbccom_leaderboard_container{background-color:#C55F16;}.blackTheme #bbccom_leaderboard_container{background-color:#3F3F3F;}.module h2 a.headerSponsor{position:absolute;right:30px;top:0;width:auto;}.module h2 a.headerSponsor{display:block;font-size:.667em;font-weight:normal;height:30px;margin:5px 0 0;}.module h2 a.headerSponsor span{float:left;line-height:26px;margin-right:5px;}.module h2 a.headerSponsor img{float:left;}.blogs #bbccom_mpu,.blogs #bbccom_partner_button1,.blogs #bbccom_adsense_mpu{float:left;}.blogs #bbccom_partner_button1{margin:14px 0;}.blogs #bbccom_mpu{margin-bottom:14px;}#bbccom_leaderboard_adlabel.bbccom_text a,.blogs #content #bbccom_mpu_adlabel a{color:#666;}.blogs .adsense_mpu{font-size:1em;}.blogs #content .adsensetitle a:link{color:#555;}.blogs #content .adsenselabel a:link{font-size:1.1em;}.blogs #content .adsenselabel a:link,.blogs #content .adsenseurl a:link{color:#006;}.blogs #content .adsensead span{line-height:1.4em;}.blogs #bbccom_sponsor_section{width:auto;top:20px;}.blogs #bbccom_sponsor_section_text{width:auto;font-size:.8em;line-height:31px;}.blogs #bbccom_sponsor_section_image{width:auto;}.blogs #bbccom_sponsor_section_text,.blogs #bbccom_sponsor_section_image{float:left;margin-left:10px;}#blq-weather-header #bbccom_sponsor_section{top:-2px;right:14px;display:block;}#blq-weather-header #bbccom_sponsor_section div{display:block;}#blq-weather-header #bbccom_sponsor_section_text{text-align:right;}#blq-weather-header #bbccom_sponsor_section_image{width:auto;}.bbccom_slot_interstitial #blq-acc,.bbccom_slot_interstitial #blq-mast{display:none!important;}.bbccom_slot_interstitial{overflow:hidden;}.bbccom_slot_interstitial_300x600 #bbccom_int{width:300px;}#bbccom_int_container{width:100%;text-align:center;height:5000px;left:0;position:absolute;top:-40px;z-index:10000;}#bbccom_int_outer{top:0;z-index:9999;background:url('../../../../../www.bbc.co.uk/bbc.com/images/interstitial/header.gif') #ddd no-repeat;height:100%!important;height:4000px;}#bbccom_int_inner{width:976px;}#bbccom_int_head{text-align:right;padding:70px 10px 0 0;}#bbccom_int_link{font-family:arial,sans-serif;color:black;font-size:.8em;font-weight:bold;text-decoration:none;cursor:pointer;background:url('../../../../../www.bbc.co.uk/bbc.com/images/interstitial/arrow.gif') transparent right center no-repeat;padding-right:14px;}#bbccom_int{margin:0 auto;padding-top:20px;width:640px;}#travel #bbccom_int_outer{background:url('../../../../../www.bbc.co.uk/bbc.com/images/interstitial/header_travel.gif') #ddd no-repeat;}.blq_hp .pulse-pop{top:103px;}.bbccom_slot_leaderboard .pulse-pop{top:232px;}.bbccom_slot_leaderboard97066 .pulse-pop{top:200px;}.bbccom_slot_leaderboard97090 .pulse-pop{top:224px;}.news #blq-pre-mast .pulse-pop{left:700px;top:300px;}.centerbody #blq-pre-mast .pulse-pop{left:700px;top:300px;}.blq-js #international .colB{padding-top:0;height:280px;}.blq-js #international .colE{margin-top:-280px;}#blq-main .front-page .bbccom_companion{display:none;} \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/img/1_0_1/cream/hi/news/news-blocks.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/img/1_0_1/cream/hi/news/news-blocks.gif
new file mode 100755
index 0000000000..33d693a4a5
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/img/1_0_1/cream/hi/news/news-blocks.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/app/bbccom/19_52/s_code.js b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/app/bbccom/19_52/s_code.js
new file mode 100755
index 0000000000..ea521da8d8
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/app/bbccom/19_52/s_code.js
@@ -0,0 +1,1091 @@
+/* SiteCatalyst code version: H.22.1.
+Copyright 1996-2010 Adobe, Inc. All Rights Reserved
+More info available at http://www.omniture.com */
+
+/*
+Version: 1.7.3
+ */
+//var s_account = "bbcwglobaldev"
+var s_account = "bbcwglobalprod"
+var s=s_gi(s_account)
+/************************** CONFIG SECTION **************************/
+/* You may add or alter any code config here. */
+s.charSet="ISO-8859-1"
+/* Conversion Config */
+s.currencyCode="USD"
+/* Link Tracking Config */
+s.trackDownloaddisabledLinks=true
+s.trackExternalLinks=true
+s.trackInlineStats=true
+s.linkDownloaddisabledFileTypes="exe,zip,wav,mp3,mov,mpg,avi,wmv,doc,pdf,xls"
+s.linkInternalFilters="javascript:,bbc.com,bbc.co.uk"
+s.linkLeaveQueryString=false
+s.linkTrackVars="None"
+s.linkTrackEvents="None"
+
+/* Time Parting Config */
+s.dstStart = "3/28/2010";
+s.dstEnd = "10/31/2010";
+s.currentYear = Set_Year();
+
+/* Channel Manager Configuration */
+s._extraSearchEngines=""
+s._channelDomain = "Facebook|facebook.com>Twitter|twitter.com>YouTube|youtube.com>LinkedIn|linkedin.com>MySpace|myspace.com>Other Social Media|digg.com,flickr.com,stumbleupon.com,del.icio.us,reddit.com,metacafe.com,technorati.com"
+s._channelParameter = ""
+s._channelPattern = ""
+
+/* Hierarchy Section Rules */
+var thisURL = document.URL;
+var h1 = "";
+var h2 = "";
+var h3 = "";
+var h4 = "";
+var br = "<br>";
+var url = "";
+var hArray;
+var hLength;
+var newUrl = "";
+var contentIdMatch;
+//string sequence starting with a - or _, followed by 7 - 8 digits, followed by a non digit or EOL
+var reStandardContentId = new RegExp("([0-9]{7,8})([^0-9]|$)");
+var reNewsContentId = new RegExp("(-)([0-9]{7,8})([^0-9]|$)");
+
+s.usePlugins = true
+function s_doPlugins(s) {
+ /* Add calls to plugins here */
+
+ /* Custom Page Views event */
+ s.events = s.apl(s.events, "event2", ",", 1);
+
+
+ /* Hierarchy Section Rules */
+
+ evaluateUrl(thisURL);
+
+ s.channel = s.prop6;
+
+ /* Meta tag capture */
+ if (document.getElementsByName) {
+
+ //Page Name variable
+ s.pageName = document.title.replace(/ - /g, ' | ').toLowerCase();
+ s.eVar2 = s.pageName;
+
+ s.hier1 = document.title.replace(/ - /g, '|');
+ if ("undefined" != typeof BBC) {
+ s.hier2 = BBC.adverts.getSectionPath();
+ } else {
+ sectionPath = window.location.pathname.replace('2/hi', 'news'); // Flip news
+ sectionPath = sectionPath.replace('sport2/hi', 'sport'); // Flip sport
+ sectionPath = sectionPath.replace(/\/+[0-9]*.([a-z])*$/g, ''); // Remove / from the end of the link
+ s.hier2 = sectionPath.substring(1);
+ }
+
+
+ //Site Section for channel now set above
+// var metaArray = document.getElementsByName('CPS_SITE_NAME');
+// for (var i = 0; i < metaArray.length; i++) {
+// s.channel = metaArray[i].content;
+// }
+
+
+ //prop3
+ var metaArray = document.getElementsByName('Headline');
+ for (var i = 0; i < metaArray.length; i++) {
+ s.prop3 = metaArray[i].content;
+ s.eVar3 = s.prop3;
+ }
+
+ //prop4
+ var metaArray = document.getElementsByName('CPS_ID');
+ for (var i = 0; i < metaArray.length; i++) {
+ s.prop4 = metaArray[i].content;
+ s.eVar4 = s.prop4;
+ }
+
+ //prop5
+ var metaArray = document.getElementsByName('contentFlavor');
+ for (var i = 0; i < metaArray.length; i++) {
+ s.prop5 = metaArray[i].content.toUpperCase();
+ s.eVar5 = s.prop5;
+ }
+
+
+ //prop10
+ var metaArray = document.getElementsByName('OriginalPublicationDate');
+ for (var i = 0; i < metaArray.length; i++) {
+ s.prop10 = metaArray[i].content;
+ s.eVar10 = s.prop10;
+ }
+
+ //prop14
+ //var metaArray = document.getElementsByName('CPS_AUDIENCE');
+ //for (var i = 0; i < metaArray.length; i++) {
+ // s.prop14 = metaArray[i].content;
+ // s.eVar14 = s.prop14;
+ //}
+
+ //prop15
+ var metaArray = document.getElementsByName('IFS_URL');
+ for (var i = 0; i < metaArray.length; i++) {
+ s.prop15 = metaArray[i].content;
+ s.eVar15 = s.prop15;
+ }
+ }
+
+ if ("undefined" != typeof bbc &&
+ "undefined" != typeof bbc.fmtj &&
+ "undefined" != typeof bbc.fmtj.page ) {
+ s.prop32 = bbc.fmtj.page.adKeyword;
+ s.eVar32 = s.prop32;
+ }
+
+ if ("undefined" != typeof bbcdotcom &&
+ "undefined" != typeof bbcdotcom.stats ) {
+
+ if ('yes' == bbcdotcom.stats.adEnabled) {
+ s.prop57 = "yes";
+ s.eVar57 = s.prop57;
+ } else {
+ s.prop57 = "no";
+ s.eVar57 = s.prop57;
+ }
+ // TODO: Remove the if statement and set prop14 once a fix for
+ // https://jira.dev.bbc.co.uk/browse/BBCCOM-490 is live.
+ if('homepage' != h1) {
+ s.prop14 = bbcdotcom.stats.audience;
+ s.eVar14 = s.prop14;
+ }
+ s.prop31 = bbcdotcom.stats.contentType;
+ s.eVar31 = s.prop31;
+ }
+
+ //s.prop31 = "standard - html"
+ //s.eVar31 = "standard - html"
+ //s.prop31 = "Normal Web"
+
+ /* Visitor Information */
+ //Visitor Number
+ s.prop12 = s.getVisitNum();
+
+ //Days Since Last Visit
+ s.prop13 = s.getDaysSinceLastVisit();
+ s.eVar13 = s.prop13;
+
+ //Page Refresh Variable
+ s.prop17 = s.trackRefresh(s.pageName, 'tr_pr1');
+
+ //Track New and Repeat Visitors
+ s.prop18 = s.getNewRepeat();
+ if (s.pageName && s.prop18 == 'New') s.prop19 = s.pageName;
+ if (s.pageName && s.prop18 == 'Repeat') s.prop20 = s.pageName;
+
+ //Set Time Parting Variables
+ s_hour = s.getTimeParting('h', '0');
+ s_day = s.getTimeParting('d', '0');
+ s_timepart = s_day + "|" + s_hour;
+ s.prop11 = s_timepart.toLowerCase();
+ if (s.visEvent) s.eVar11 = s.prop11;
+
+ /* Campaign Config */
+ //The Campaign variable
+ //s.campaign = s.getQueryParam('cmpid');
+ //s.eVar1 = s.crossVisitParticipation(s.campaign, 's_cpm', '90', '5', '>', 'purchase');
+
+ s.campaign = s.getQueryParam('ocid');
+ s.campaign = s.getValOnce(s.campaign,'s_campaign',0);
+
+ /* Plugin: channelManager v2.2 */
+ s.channelManager('cmp,cmpid,cid,rss,ocid,OCID', ':', 's_cm', '0');
+
+ if (s._channel == "Natural Search") {
+ s._channel = "Organic";
+ s._campaign = s._partner + "-" + s._channel + "-" + s._keywords.toLowerCase();
+ }
+ if (s._channel == "Referrers") {
+ s._channel = "Other Referrers";
+ s._campaign = s._channel + "-" + s._referringDomain;
+ }
+
+ s.eVar43 = s._referrer
+ s.eVar44 = s._referringDomain
+ s.eVar45 = s._keywords
+ s.eVar46 = s._partner
+ s.eVar47 = s._channel
+
+ //Referrer - Search Term Stacking
+ s.eVar48 = s.crossVisitParticipation(s._keywords, 's_ev48', '30', '5', '>', 'event2', 1);
+
+ //Referrer - Channel Stacking
+ s.eVar49 = s.crossVisitParticipation(s._channel, 's_ev49', '30', '5', '>');
+
+
+}
+s.doPlugins = s_doPlugins
+
+function Set_Year() {
+ var now = new Date();
+ var year = now.getYear();
+ if (year < 1900) {
+ year = year + 1900;
+ }
+ return year;
+}
+
+/*********************** PLUGINS SECTION ****************************/
+
+function evaluateUrl(siteUrl) {
+
+ h1 = "";
+ h2 = "";
+ h3 = "";
+ h4 = "";
+ url = "";
+ hArray = null;
+ hLength = null;
+ newUrl = "";
+ contentIdMatch = null;
+
+ url = siteUrl;
+
+ url = url.toLowerCase();
+ url = url.replace(/^\s*(.*?)\s*$/,"$1");
+
+ //if last character is / then remove
+ url = url.replace(/\/$/g,'');
+
+ //remove protocol in url
+ newUrl = url.replace(/^(http|https):\/\//g,"");
+
+ //remove uk- in url if there is another section
+ //i.e. http://www.bbc.co.uk/news/uk-politics-11754656
+ if(3 <= newUrl.split('-').length) {
+ newUrl = newUrl.replace(/uk-/,"");
+ }
+
+ //replace a number document with article, mainly for v3 stories
+ newUrl = newUrl.replace(/\/[0-9]{7}.stm/,"/articles");
+
+ // strip default.stm
+ newUrl = newUrl.replace(/\/default.stm/g,'');
+
+ //split the url
+ hArray = newUrl.split("\/");
+ hLength = hArray.length;
+
+ if (hLength >= 2) {
+
+ siteSection = hArray[1];
+
+ switch(siteSection){
+ case 'news': news(); break;
+ case 'sport': sport(); break;
+ case 'sport2': sport(); break;
+ case 'weather': weather(); break;
+ case 'travel': travel(); break;
+ case 'blogs': blogs(); break;
+ case 'radio': radio(); break;
+ //default:void("no siteSection found" + br);break;
+ }
+ }
+
+ //Handle home page
+ if (hLength == 1) {
+ h1 = 'homepage';
+ }
+
+ if ("undefined" != typeof h1 && '' != h1) {
+ s.prop6 = h1;
+ s.eVar6 = s.prop6;
+ s.channel = s.prop6;
+ }
+ if ("undefined" != typeof h2 && '' != h2) {
+ s.prop7 = h2;
+ s.eVar7 = s.prop7;
+ }
+ if ("undefined" != typeof h3 && '' != h3) {
+ s.prop8 = h3;
+ s.eVar8 = s.prop8;
+ }
+ if ("undefined" != typeof h4 && '' != h4) {
+ s.prop9 = h4;
+ s.eVar9 = s.prop9;
+ }
+
+ //***
+ //END of Main section
+ //***
+}
+
+function news (){
+
+ h1 = hArray[1];
+
+ contentIdMatch = reNewsContentId.test(newUrl)
+ if (!contentIdMatch) {
+ if (hLength >= 3) {
+ h2 = h1 + ">" + hArray[2];
+ if (hLength >= 4) {
+ h3 = h2 + ">" + hArray[3];
+ }
+ }
+ }
+ else {
+ if (hLength >= 2) {
+
+ //change all \d{7} and \d{8} to 'articles'
+ var lastFwdSlash = newUrl.lastIndexOf("/");
+ var lastValue = newUrl.substr(lastFwdSlash + 1);
+
+ lastValue = lastValue.replace(/([0-9]{8})/,"articles");
+ lastValue = lastValue.replace(/([0-9]{7})/,"articles");
+
+ var firstDash = lastValue.indexOf("-");
+
+ if (firstDash > 0){
+
+ h2 = h1 + ">" + lastValue.substring(0,firstDash);
+ h3 = h2 + ">" + lastValue.substr(firstDash + 1);
+
+ } else {
+
+ h2 = h1 + ">" + lastValue;
+
+ }
+
+ }
+
+ }
+}
+
+function sport() {
+
+ var newUrlSport = "";
+
+ //Keep the original value (eg Sport2),
+ h1 = hArray[1].replace(/sport2/g,'sport');
+
+ contentIdMatch = reStandardContentId.test(newUrl);
+
+ if (!contentIdMatch) {
+
+ newUrl = newUrl.replace(/sport2/g,'sport');
+ newUrlSport = newUrl.replace(/\/hi\//g,'\/');
+ hArray = newUrlSport.split("\/");
+ hLength = hArray.length;
+
+ if (hLength >= 3) {
+
+ //Use the new value of h1 eg sport not sport2
+ h2 = hArray[1] + ">" + hArray[2];
+ if (hLength >= 4) {
+ h3 = h2 + ">" + hArray[3];
+
+ newUrlSport = newUrlSport.replace(/\/m\//g,'\/');
+ hArray = newUrlSport.split("\/");
+ hLength = hArray.length;
+
+ if (hLength >= 5) {
+ h4 = h3 + ">" + hArray[4];
+ }
+ }
+ }
+
+ } else {
+
+ var posHi = hArray.indexOf('hi');
+
+ if (posHi > 0 && (hLength >= (posHi + 1) )) {
+ h2 = h1 + ">" + hArray[posHi + 1];
+ }
+ if (posHi > 0 && (hLength >= (posHi + 2) )) {
+ h3 = h2 + ">" + hArray[posHi + 2];
+ }
+ var posM = hArray.indexOf('m');
+ if (posM > 0 && (hLength >= (posM + 1) )) {
+ h4 = h3 + ">" + hArray[posM + 1];
+ }
+ }
+}
+
+function weather() {
+
+ h1 = hArray[1];
+
+ newUrl = newUrl.replace(/\/hi\//g,'\/');
+ hArray = newUrl.split("\/");
+ hLength = hArray.length;
+
+ if (hLength >= 3) {
+ h2 = h1 + ">" + hArray[2];
+ contentIdMatch = reStandardContentId.test(newUrl);
+ if (!contentIdMatch) {
+ if (hLength >= 4) {
+ h3 = h2 + ">" + hArray[3];
+ }
+ } else {
+ h3 = h2 + ">" + 'articles';
+ }
+ }
+}
+
+function travel () {
+ h1 = hArray[1];
+ if (hLength >= 3) {
+ h2 = h1 + ">" + hArray[2];
+ contentIdMatch = reStandardContentId.test(newUrl);
+ if (!contentIdMatch) {
+ if (hLength >= 4) {
+ h3 = h2 + ">" + hArray[3];
+ }
+ }
+ else {
+ h3 = h2 + ">" + 'articles';
+ }
+ }
+}
+
+function blogs() {
+
+ newUrl = newUrl.replace(/\.shtml/g,'\/');
+ hArray = newUrl.split("\/");
+ hLength = hArray.length;
+
+ h1 = hArray[1];
+
+ if (hLength >= 3) {
+ h2 = h1 + ">" + hArray[2];
+ if (hLength >= 4) {
+ h3 = h2 + ">" + hArray[3];
+ if (hLength >= 5) {
+ h4 = h3 + ">" + hArray[4];
+ }
+ }
+ }
+}
+
+function radio() {
+ h1 = hArray[1];
+}
+
+
+/*********************************************************************
+* Function p_fo(x,y): Ensures the plugin code is fired only on the
+* first call of do_plugins
+* Returns:
+* - 1 if first instance on firing
+* - 0 if not first instance on firing
+*********************************************************************/
+s.p_fo = new Function("n", ""
++ "var s=this;if(!s.__fo){s.__fo=new Object;}if(!s.__fo[n]){s.__fo[n]="
++ "new Object;return 1;}else {return 0;}");
+
+/*
+* Plugin: getValOnce 0.2 - get a value once per session or number of days
+*/
+s.getValOnce = new Function("v", "c", "e", ""
++ "var s=this,k=s.c_r(c),a=new Date;e=e?e:0;if(v){a.setTime(a.getTime("
++ ")+e*86400000);s.c_w(c,v,e?a:0);}return v==k?'':v");
+
+/*
+* Plugin Utility: apl v1.1
+*/
+s.apl = new Function("l", "v", "d", "u", ""
++ "var s=this,m=0;if(!l)l='';if(u){var i,n,a=s.split(l,d);for(i=0;i<a."
++ "length;i++){n=a[i];m=m||(u==1?(n==v):(n.toLowerCase()==v.toLowerCas"
++ "e()));}}if(!m)l=l?l+d+v:v;return l");
+
+/*
+* Utility Function: split v1.5 - split a string (JS 1.0 compatible)
+*/
+s.split = new Function("l", "d", ""
++ "var i,x=0,a=new Array;while(l){i=l.indexOf(d);i=i>-1?i:l.length;a[x"
++ "++]=l.substring(0,i);l=l.substring(i+d.length);}return a");
+
+/* Utility Function: p_c */
+s.p_c = new Function("v", "c", ""
++ "var x=v.indexOf('=');return c.toLowerCase()==v.substring(0,x<0?v.le"
++ "ngth:x).toLowerCase()?v:0");
+
+/*
+* s.join: 1.0 - s.join(v,p)
+*
+* v - Array (may also be array of array)
+* p - formatting parameters (front, back, delim, wrap)
+*
+*/
+s.join = new Function("v", "p", ""
++ "var s = this;var f,b,d,w;if(p){f=p.front?p.front:'';b=p.back?p.back"
++ ":'';d=p.delim?p.delim:'';w=p.wrap?p.wrap:'';}var str='';for(var x=0"
++ ";x<v.length;x++){if(typeof(v[x])=='object' )str+=s.join( v[x],p);el"
++ "se str+=w+v[x]+w;if(x<v.length-1)str+=d;}return f+str+b;");
+
+/*
+* Plugin Utility: Replace v1.0
+*/
+s.repl = new Function("x", "o", "n", ""
++ "var i=x.indexOf(o),l=n.length;while(x&&i>=0){x=x.substring(0,i)+n+x."
++ "substring(i+o.length);i=x.indexOf(o,i+l)}return x");
+
+/*
+* Plugin - trackRefresh v1.1 (requires split utility function)
+*/
+s.trackRefresh = new Function("v", "c", ""
++ "var s=this,a,t=new Date,x;t.setTime(t.getTime()+1800000);if(!s.c_r("
++ "c)){s.c_w(c,v,t);return v}else{x=unescape(s.c_r(c));if(x==v){x+='~["
++ "1]';s.c_w(c,x,0);return x}else{a=s.split(x,'~[');if(a[0]==v){i=pars"
++ "eInt(a[1])+1;x=a[0]+'~['+i+']';s.c_w(c,x,0);return x}else{s.c_w(c,v"
++ ",0);return v}}}");
+
+/*
+* Plugin: Visit Number By Month 2.0 - Return the user visit number
+*/
+s.getVisitNum = new Function(""
++ "var s=this,e=new Date(),cval,cvisit,ct=e.getTime(),c='s_vnum',c2='s"
++ "_invisit';e.setTime(ct+30*24*60*60*1000);cval=s.c_r(c);if(cval){var"
++ " i=cval.indexOf('&vn='),str=cval.substring(i+4,cval.length),k;}cvis"
++ "it=s.c_r(c2);if(cvisit){if(str){e.setTime(ct+30*60*1000);s.c_w(c2,'"
++ "true',e);return str;}else return 'unknown visit number';}else{if(st"
++ "r){str++;k=cval.substring(0,i);e.setTime(k);s.c_w(c,k+'&vn='+str,e)"
++ ";e.setTime(ct+30*60*1000);s.c_w(c2,'true',e);return str;}else{s.c_w"
++ "(c,ct+30*24*60*60*1000+'&vn=1',e);e.setTime(ct+30*60*1000);s.c_w(c2"
++ ",'true',e);return 1;}}"
+);
+
+/*
+* Plugin: getTimeToComplete 0.4 - return the time from start to stop
+*/
+s.getTimeToComplete = new Function("v", "cn", "e", ""
++ "var s=this,d=new Date,x=d,k;if(!s.ttcr){e=e?e:0;if(v=='start'||v=='"
++ "stop')s.ttcr=1;x.setTime(x.getTime()+e*86400000);if(v=='start'){s.c"
++ "_w(cn,d.getTime(),e?x:0);return '';}if(v=='stop'){k=s.c_r(cn);if(!s"
++ ".c_w(cn,'',d)||!k)return '';v=(d.getTime()-k)/1000;var td=86400,th="
++ "3600,tm=60,r=5,u,un;if(v>td){u=td;un='days';}else if(v>th){u=th;un="
++ "'hours';}else if(v>tm){r=2;u=tm;un='minutes';}else{r=.2;u=1;un='sec"
++ "onds';}v=v*r/u;return (Math.round(v)/r)+' '+un;}}return '';");
+
+/*
+* Plugin: Days since last Visit 1.1.H - capture time from last visit
+*/
+s.getDaysSinceLastVisit = new Function("c", ""
++ "var s=this,e=new Date(),es=new Date(),cval,cval_s,cval_ss,ct=e.getT"
++ "ime(),day=24*60*60*1000,f1,f2,f3,f4,f5;e.setTime(ct+3*365*day);es.s"
++ "etTime(ct+30*60*1000);f0='Cookies Not Supported';f1='First Visit';f"
++ "2='More than 30 days';f3='More than 7 days';f4='Less than 7 days';f"
++ "5='Less than 1 day';cval=s.c_r(c);if(cval.length==0){s.c_w(c,ct,e);"
++ "s.c_w(c+'_s',f1,es);}else{var d=ct-cval;if(d>30*60*1000){if(d>30*da"
++ "y){s.c_w(c,ct,e);s.c_w(c+'_s',f2,es);}else if(d<30*day+1 && d>7*day"
++ "){s.c_w(c,ct,e);s.c_w(c+'_s',f3,es);}else if(d<7*day+1 && d>day){s."
++ "c_w(c,ct,e);s.c_w(c+'_s',f4,es);}else if(d<day+1){s.c_w(c,ct,e);s.c"
++ "_w(c+'_s',f5,es);}}else{s.c_w(c,ct,e);cval_ss=s.c_r(c+'_s');s.c_w(c"
++ "+'_s',cval_ss,es);}}cval_s=s.c_r(c+'_s');if(cval_s.length==0) retur"
++ "n f0;else if(cval_s!=f1&&cval_s!=f2&&cval_s!=f3&&cval_s!=f4&&cval_s"
++ "!=f5) return '';else return cval_s;");
+
+/*
+* Plugin: getNewRepeat 1.0 - Return whether user is new or repeat
+*/
+s.getNewRepeat = new Function(""
++ "var s=this,e=new Date(),cval,ct=e.getTime(),y=e.getYear();e.setTime"
++ "(ct+30*24*60*60*1000);cval=s.c_r('s_nr');if(cval.length==0){s.c_w("
++ "'s_nr',ct,e);return 'New';}if(cval.length!=0&&ct-cval<30*60*1000){s"
++ ".c_w('s_nr',ct,e);return 'New';}if(cval<1123916400001){e.setTime(cv"
++ "al+30*24*60*60*1000);s.c_w('s_nr',ct,e);return 'Repeat';}else retur"
++ "n 'Repeat';");
+
+/*
+* Plugin: getTimeParting 2.0 - Set timeparting values based on time zone
+*/
+s.getTimeParting = new Function("t", "z", ""
++ "var s=this,cy;dc=new Date('1/1/2000');"
++ "if(dc.getDay()!=6||dc.getMonth()!=0){return'Data Not Available'}"
++ "else{;z=parseFloat(z);var dsts=new Date(s.dstStart);"
++ "var dste=new Date(s.dstEnd);fl=dste;cd=new Date();if(cd>dsts&&cd<fl)"
++ "{z=z+1}else{z=z};utc=cd.getTime()+(cd.getTimezoneOffset()*60000);"
++ "tz=new Date(utc + (3600000*z));thisy=tz.getFullYear();"
++ "var days=['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday',"
++ "'Saturday'];if(thisy!=s.currentYear){return'Data Not Available'}else{;"
++ "thish=tz.getHours();thismin=tz.getMinutes();thisd=tz.getDay();"
++ "var dow=days[thisd];var ap='AM';var dt='Weekday';var mint='00';"
++ "if(thismin>30){mint='30'}if(thish>=12){ap='PM';thish=thish-12};"
++ "if (thish==0){thish=12};if(thisd==6||thisd==0){dt='Weekend'};"
++ "var timestring=thish+':'+mint+ap;if(t=='h'){return timestring}"
++ "if(t=='d'){return dow};if(t=='w'){return dt}}};");
+
+/*
+* Plugin: getQueryParam 2.3
+*/
+s.getQueryParam = new Function("p", "d", "u", ""
++ "var s=this,v='',i,t;d=d?d:'';u=u?u:(s.pageURL?s.pageURL:s.wd.locati"
++ "on);if(u=='f')u=s.gtfs().location;while(p){i=p.indexOf(',');i=i<0?p"
++ ".length:i;t=s.p_gpv(p.substring(0,i),u+'');if(t){t=t.indexOf('#')>-"
++ "1?t.substring(0,t.indexOf('#')):t;}if(t)v+=v?d+t:t;p=p.substring(i="
++ "=p.length?i:i+1)}return v");
+s.p_gpv = new Function("k", "u", ""
++ "var s=this,v='',i=u.indexOf('?'),q;if(k&&i>-1){q=u.substring(i+1);v"
++ "=s.pt(q,'&','p_gvf',k)}return v");
+s.p_gvf = new Function("t", "k", ""
++ "if(t){var s=this,i=t.indexOf('='),p=i<0?t:t.substring(0,i),v=i<0?'T"
++ "rue':t.substring(i+1);if(p.toLowerCase()==k.toLowerCase())return s."
++ "epa(v)}return ''");
+
+/*
+* channelManager v2.2 - Tracking External Traffic
+*/
+s.channelManager = new Function("a", "b", "c", "V", ""
++ "var s=this,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,t,u,v,w,x,y,z,A,B,C,D,E,F,"
++ "G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,W,X,Y;g=s.referrer?s.referrer:documen"
++ "t.referrer;g=g.toLowerCase();if(!g){h='1'}i=g.indexOf('?')>-1?g.ind"
++ "exOf('?'):g.length;j=g.substring(0,i);k=s.linkInternalFilters.toLow"
++ "erCase();k=s.split(k,',');l=k.length;for(m=0;m<l;m++){n=j.indexOf(k"
++ "[m])==-1?'':g;if(n)o=n}if(!o&&!h){p=g;q=g.indexOf('//')>-1?g.indexO"
++ "f('//')+2:0;r=g.indexOf('/',q)>-1?g.indexOf('/',q):i;t=g.substring("
++ "q,r);t=t.toLowerCase();u=t;P='Referrers';v=s.seList+'>'+s._extraSea"
++ "rchEngines;if(V=='1'){j=s.repl(j,'oogle','%');j=s.repl(j,'ahoo','^'"
++ ");g=s.repl(g,'as_q','*');}A=s.split(v,'>');B=A.length;for(C=0;C<B;C"
++ "++){D=A[C];D=s.split(D,'|');E=s.split(D[0],',');F=E.length;for(G=0;"
++ "G<F;G++){H=j.indexOf(E[G]);if(H>-1){I=s.split(D[1],',');J=I.length;"
++ "for(K=0;K<J;K++){L=s.getQueryParam(I[K],'',g);if(L){L=L.toLowerCase"
++ "();M=L;if(D[2]){u=D[2];N=D[2]}else{N=t}if(V=='1'){N=s.repl(N,'#',' "
++ "- ');g=s.repl(g,'*','as_q');N=s.repl(N,'^','ahoo');N=s.repl(N,'%','"
++ "oogle');}}}}}}}O=s.getQueryParam(a,b);if(O){u=O;if(M){P='Paid Searc"
++ "h'}else{P='Paid Non-Search';}}if(!O&&M){u=N;P='Natural Search'}f=s."
++ "_channelDomain;if(f){k=s.split(f,'>');l=k.length;for(m=0;m<l;m++){Q"
++ "=s.split(k[m],'|');R=s.split(Q[1],',');S=R.length;for(T=0;T<S;T++){"
++ "W=j.indexOf(R[T]);if(W>-1)P=Q[0]}}}d=s._channelParameter;if(d){k=s."
++ "split(d,'>');l=k.length;for(m=0;m<l;m++){Q=s.split(k[m],'|');R=s.sp"
++ "lit(Q[1],',');S=R.length;for(T=0;T<S;T++){U=s.getQueryParam(R[T]);i"
++ "f(U)P=Q[0]}}}e=s._channelPattern;if(e){k=s.split(e,'>');l=k.length;"
++ "for(m=0;m<l;m++){Q=s.split(k[m],'|');R=s.split(Q[1],',');S=R.length"
++ ";for(T=0;T<S;T++){X=O.indexOf(R[T]);if(X==0)P=Q[0]}}}if(h=='1'&&!O)"
++ "{u=P=t=p='Direct Load'}T=M+u+t;U=c?'c':'c_m';if(c!='0'){T=s.getValO"
++ "nce(T,U,0);}if(T)M=M?M:'n/a';s._referrer=T&&p?p:'';s._referringDoma"
++ "in=T&&t?t:'';s._partner=T&&N?N:'';s._campaignID=T&&O?O:'';s._campai"
++ "gn=T&&u?u:'';s._keywords=T&&M?M:'';s._channel=T&&P?P:'';");
+/* Top 130 - Grouped
+s.seList="altavista.co,altavista.de|q,r|AltaVista>.aol.,suche.aolsvc"
++".de|q,query|AOL>ask.jp,ask.co|q,ask|Ask>www.baidu.com|wd|Baidu>daum"
++".net,search.daum.net|q|Daum>google.,googlesyndication.com|q,as_q|Go"
++"ogle>icqit.com|q|icq>bing.com|q|Microsoft Bing>myway.com|searchfor|"
++"MyWay.com>naver.com,search.naver.com|query|Naver>netscape.com|query"
++",search|Netscape Search>reference.com|q|Reference.com>seznam|w|Sezn"
++"am.cz>abcsok.no|q|Startsiden>tiscali.it,www.tiscali.co.uk|key,query"
++"|Tiscali>virgilio.it|qs|Virgilio>yahoo.com,yahoo.co.jp|p,va|Yahoo!>"
++"yandex|text|Yandex.ru>search.cnn.com|query|CNN Web Search>search.ea"
++"rthlink.net|q|Earthlink Search>search.comcast.net|q|Comcast Search>"
++"search.rr.com|qs|RoadRunner Search>optimum.net|q|Optimum Search";*/
+/* Top 130 */
+s.seList = "altavista.co|q,r|AltaVista>aol.co.uk,search.aol.co.uk|query"
++ "|AOL - United Kingdom>search.aol.com,search.aol.ca|query,q|AOL.com "
++ "Search>ask.com,ask.co.uk|ask,q|Ask Jeeves>www.baidu.com|wd|Baidu>da"
++ "um.net,search.daum.net|q|Daum>google.co,googlesyndication.com|q,as_"
++ "q|Google>google.com.ar|q,as_q|Google - Argentina>google.com.au|q,as"
++ "_q|Google - Australia>google.at|q,as_q|Google - Austria>google.com."
++ "bh|q,as_q|Google - Bahrain>google.com.bd|q,as_q|Google - Bangladesh"
++ ">google.be|q,as_q|Google - Belgium>google.com.bo|q,as_q|Google - Bo"
++ "livia>google.ba|q,as_q|Google - Bosnia-Hercegovina>google.com.br|q,"
++ "as_q|Google - Brasil>google.bg|q,as_q|Google - Bulgaria>google.ca|q"
++ ",as_q|Google - Canada>google.cl|q,as_q|Google - Chile>google.cn|q,a"
++ "s_q|Google - China>google.com.co|q,as_q|Google - Colombia>google.co"
++ ".cr|q,as_q|Google - Costa Rica>google.hr|q,as_q|Google - Croatia>go"
++ "ogle.cz|q,as_q|Google - Czech Republic>google.dk|q,as_q|Google - De"
++ "nmark>google.com.do|q,as_q|Google - Dominican Republic>google.com.e"
++ "c|q,as_q|Google - Ecuador>google.com.eg|q,as_q|Google - Egypt>googl"
++ "e.com.sv|q,as_q|Google - El Salvador>google.ee|q,as_q|Google - Esto"
++ "nia>google.fi|q,as_q|Google - Finland>google.fr|q,as_q|Google - Fra"
++ "nce>google.de|q,as_q|Google - Germany>google.gr|q,as_q|Google - Gre"
++ "ece>google.com.gt|q,as_q|Google - Guatemala>google.hn|q,as_q|Google"
++ " - Honduras>google.com.hk|q,as_q|Google - Hong Kong>google.hu|q,as_"
++ "q|Google - Hungary>google.co.in|q,as_q|Google - India>google.co.id|"
++ "q,as_q|Google - Indonesia>google.ie|q,as_q|Google - Ireland>google."
++ "is|q,as_q|Google - Island>google.co.il|q,as_q|Google - Israel>googl"
++ "e.it|q,as_q|Google - Italy>google.com.jm|q,as_q|Google - Jamaica>go"
++ "ogle.co.jp|q,as_q|Google - Japan>google.jo|q,as_q|Google - Jordan>g"
++ "oogle.co.ke|q,as_q|Google - Kenya>google.co.kr|q,as_q|Google - Kore"
++ "a>google.lv|q,as_q|Google - Latvia>google.lt|q,as_q|Google - Lithua"
++ "nia>google.com.my|q,as_q|Google - Malaysia>google.com.mt|q,as_q|Goo"
++ "gle - Malta>google.mu|q,as_q|Google - Mauritius>google.com.mx|q,as_"
++ "q|Google - Mexico>google.co.ma|q,as_q|Google - Morocco>google.nl|q,"
++ "as_q|Google - Netherlands>google.co.nz|q,as_q|Google - New Zealand>"
++ "google.com.ni|q,as_q|Google - Nicaragua>google.com.ng|q,as_q|Google"
++ " - Nigeria>google.no|q,as_q|Google - Norway>google.com.pk|q,as_q|Go"
++ "ogle - Pakistan>google.com.py|q,as_q|Google - Paraguay>google.com.p"
++ "e|q,as_q|Google - Peru>google.com.ph|q,as_q|Google - Philippines>go"
++ "ogle.pl|q,as_q|Google - Poland>google.pt|q,as_q|Google - Portugal>g"
++ "oogle.com.pr|q,as_q|Google - Puerto Rico>google.com.qa|q,as_q|Googl"
++ "e - Qatar>google.ro|q,as_q|Google - Romania>google.ru|q,as_q|Google"
++ " - Russia>google.st|q,as_q|Google - Sao Tome and Principe>google.co"
++ "m.sa|q,as_q|Google - Saudi Arabia>google.com.sg|q,as_q|Google - Sin"
++ "gapore>google.sk|q,as_q|Google - Slovakia>google.si|q,as_q|Google -"
++ " Slovenia>google.co.za|q,as_q|Google - South Africa>google.es|q,as_"
++ "q|Google - Spain>google.lk|q,as_q|Google - Sri Lanka>google.se|q,as"
++ "_q|Google - Sweden>google.ch|q,as_q|Google - Switzerland>google.com"
++ ".tw|q,as_q|Google - Taiwan>google.co.th|q,as_q|Google - Thailand>go"
++ "ogle.bs|q,as_q|Google - The Bahamas>google.tt|q,as_q|Google - Trini"
++ "dad and Tobago>google.com.tr|q,as_q|Google - Turkey>google.com.ua|q"
++ ",as_q|Google - Ukraine>google.ae|q,as_q|Google - United Arab Emirat"
++ "es>google.co.uk|q,as_q|Google - United Kingdom>google.com.uy|q,as_q"
++ "|Google - Uruguay>google.co.ve|q,as_q|Google - Venezuela>google.com"
++ ".vn|q,as_q|Google - Viet Nam>google.co.vi|q,as_q|Google - Virgin Is"
++ "lands>icqit.com|q|icq>bing.com|q|Microsoft Bing>myway.com|searchfor"
++ "|MyWay.com>naver.com,search.naver.com|query|Naver>netscape.com|quer"
++ "y,search|Netscape Search>reference.com|q|Reference.com>seznam|w|Sez"
++ "nam.cz>abcsok.no|q|Startsiden>tiscali.it|key|Tiscali>virgilio.it|qs"
++ "|Virgilio>yahoo.com,search.yahoo.com|p|Yahoo!>ar.yahoo.com,ar.searc"
++ "h.yahoo.com|p|Yahoo! - Argentina>au.yahoo.com,au.search.yahoo.com|p"
++ "|Yahoo! - Australia>ca.yahoo.com,ca.search.yahoo.com|p|Yahoo! - Can"
++ "ada>fr.yahoo.com,fr.search.yahoo.com|p|Yahoo! - France>de.yahoo.com"
++ ",de.search.yahoo.com|p|Yahoo! - Germany>hk.yahoo.com,hk.search.yaho"
++ "o.com|p|Yahoo! - Hong Kong>in.yahoo.com,in.search.yahoo.com|p|Yahoo"
++ "! - India>yahoo.co.jp,search.yahoo.co.jp|p,va|Yahoo! - Japan>kr.yah"
++ "oo.com,kr.search.yahoo.com|p|Yahoo! - Korea>mx.yahoo.com,mx.search."
++ "yahoo.com|p|Yahoo! - Mexico>ph.yahoo.com,ph.search.yahoo.com|p|Yaho"
++ "o! - Philippines>sg.yahoo.com,sg.search.yahoo.com|p|Yahoo! - Singap"
++ "ore>es.yahoo.com,es.search.yahoo.com|p|Yahoo! - Spain>telemundo.yah"
++ "oo.com,espanol.search.yahoo.com|p|Yahoo! - Spanish (US : Telemundo)"
++ ">tw.yahoo.com,tw.search.yahoo.com|p|Yahoo! - Taiwan>uk.yahoo.com,uk"
++ ".search.yahoo.com|p|Yahoo! - UK and Ireland>yandex|text|Yandex.ru>s"
++ "earch.cnn.com|query|CNN Web Search>search.earthlink.net|q|Earthlink"
++ " Search>search.comcast.net|q|Comcast Search>search.rr.com|qs|RoadRu"
++ "nner Search>optimum.net|q|Optimum Search";
+
+/*
+* Plug-in: crossVisitParticipation v1.6 - stacks values from
+* specified variable in cookie and returns value
+*/
+s.crossVisitParticipation = new Function("v", "cn", "ex", "ct", "dl", "ev", "dv", ""
++ "var s=this,ce;if(typeof(dv)==='undefined')dv=0;if(s.events&&ev){var"
++ " ay=s.split(ev,',');var ea=s.split(s.events,',');for(var u=0;u<ay.l"
++ "ength;u++){for(var x=0;x<ea.length;x++){if(ay[u]==ea[x]){ce=1;}}}}i"
++ "f(!v||v==''){if(ce){s.c_w(cn,'');return'';}else return'';}v=escape("
++ "v);var arry=new Array(),a=new Array(),c=s.c_r(cn),g=0,h=new Array()"
++ ";if(c&&c!='')arry=eval(c);var e=new Date();e.setFullYear(e.getFullY"
++ "ear()+5);if(dv==0&&arry.length>0&&arry[arry.length-1][0]==v)arry[ar"
++ "ry.length-1]=[v,new Date().getTime()];else arry[arry.length]=[v,new"
++ " Date().getTime()];var start=arry.length-ct<0?0:arry.length-ct;var "
++ "td=new Date();for(var x=start;x<arry.length;x++){var diff=Math.roun"
++ "d((td.getTime()-arry[x][1])/86400000);if(diff<ex){h[g]=unescape(arr"
++ "y[x][0]);a[g]=[arry[x][0],arry[x][1]];g++;}}var data=s.join(a,{deli"
++ "m:',',front:'[',back:']',wrap:\"'\"});s.c_w(cn,data,e);var r=s.join"
++ "(h,{delim:dl});if(ce)s.c_w(cn,'');return r;");
+
+
+/* Configure Modules and Plugins */
+
+s.loaddisabledModule("Media")
+s.Media.autoTrack = false
+s.Media.playerName = "EMP"
+s.Media.trackVars = "eVar21,events,eVar22,eVar23,eVar24";
+
+s.Media.trackWhilePlaying=true;
+s.Media.trackMilestones="25,50,75";
+
+// Set up variables to fake ad request
+s.playUndefinedMovie = 0;
+s.stopUndefinedMovie = 0;
+
+// Used to make sure each videos events are only tracked once
+s.eventsTracked = {};
+s.liveStreamTracked = false;
+
+s.Media.monitor = function (s, obj) {//Use this code with either JavaScript or Flash.
+ // eVar1 = Media Name
+ // event3 = Movie Starts
+ // event4 = Ad Plays
+ // event5 = Ad Stops
+ // event6 = Content Plays
+ // event7 = Content Stops
+ // event8 = Movie Ends
+
+ if (obj.mediaEvent == "adPlay" && !s.eventsTracked[obj.mediaName].event4) { //Executes when the video voids.
+ s.Media.trackVars = "eVar21,events,eVar22,eVar23,eVar24";
+ s.Media.trackEvents = "event4";
+ s.events="event4";
+ s.Media.track(obj.mediaName);
+ s.eventsTracked[obj.mediaName].event4 = true;
+ } else if (obj.mediaEvent == "adStop" && !s.eventsTracked[obj.mediaName].event5) { //Executes when the video ad stops.
+ s.Media.trackVars = "eVar21,events,eVar22,eVar23,eVar24";
+ s.Media.trackEvents = "event5";
+ s.events="event5";
+ s.Media.track(obj.mediaName);
+ s.eventsTracked[obj.mediaName].event5 = true;
+ } else if (obj.mediaEvent == "contentPlay" && !s.eventsTracked[obj.mediaName].event6) { //Executes when the voids.
+ s.Media.trackVars = "eVar21,events,eVar22,eVar23,eVar24";
+ s.Media.trackEvents = "event6";
+ s.events="event6";
+ s.Media.track(obj.mediaName);
+ s.eventsTracked[obj.mediaName].event6 = true;
+ } else if (obj.mediaEvent == "contentStop" && !s.eventsTracked[obj.mediaName].event7) { //Executes when the video stops.
+ s.Media.trackVars = "eVar21,events,eVar22,eVar23,eVar24";
+ s.Media.trackEvents = "event7";
+ s.events="event7";
+ s.Media.track(obj.mediaName);
+ s.eventsTracked[obj.mediaName].event7 = true;
+ } else if (obj.mediaEvent == "movieEnd" && !s.eventsTracked[obj.mediaName].event8) { //Executes when the playlist ends.
+ s.Media.trackVars = "eVar21,events,eVar22,eVar23,eVar24";
+ s.Media.trackEvents = "event8";
+ s.events="event8";
+ s.Media.track(obj.mediaName);
+ s.eventsTracked[obj.mediaName].event8 = true;
+ }
+};
+
+function setProperties (obj){
+ s.prop21 = obj.mediaName;
+ s.prop22 = obj.mediaType;
+ s.prop23 = obj.mediaId;
+ s.prop24 = obj.adId;
+ s.eVar21 = s.prop21;
+ s.eVar22= s.prop22;
+ s.eVar23= s.prop23;
+ s.eVar24= s.prop24;
+}
+
+/*
+ * Faking the ad if three undefined movieTypes played with this method
+ */
+function playStopAd(obj) {
+ // Faking the adPlay for EMP 10.17
+ // monitor is used to fire events
+ obj.mediaEvent = 'adPlay';
+ s.Media.monitor(s, obj);
+ // Faking the adStop for EMP 10.17
+ // monitor is used to fire events
+ obj.mediaEvent = 'adStop';
+ s.Media.monitor(s, obj);
+}
+
+function startMovie(obj) {
+
+ setProperties(obj);
+
+ // This videos tracked obj
+ if ('undefined' == typeof(s.eventsTracked[obj.mediaName])) {
+ s.eventsTracked[obj.mediaName] = {
+ event3: false,
+ event4: false,
+ event5: false,
+ event6: false,
+ event7: false,
+ event8: false
+ };
+ }
+
+ if (!s.eventsTracked[obj.mediaName].event3) { //Executes when the playlist starts.
+ s.Media.trackVars = "eVar21,events,eVar22,eVar23,eVar24";
+ s.Media.trackEvents = "event3";
+ s.events="event3";
+ s.void(obj.mediaName, obj.mediaLength, obj.mediaPlayerName);
+ s.eventsTracked[obj.mediaName].event3 = true;
+ }
+
+
+ //s.Media.monitor(s, obj);
+ // Removing this for the moment as there is always at least a one second ad
+ /*
+ // 1st playUndefinedMovie - We called play but passed to AS3
+ // 2nd playUndefinedMovie - We called play on ident
+ // 1st stopUndefinedMovie - We called stop on ident
+ if(2 == s.playUndefinedMovie && 1 == s.stopUndefinedMovie) {
+ playStopAd(obj);
+ }
+ */
+
+}
+
+function playMovie(obj) {
+ // Not used for live streams
+ if('programme' == obj.mediaType && -1 !== obj.mediaLength) {
+ obj.mediaEvent = 'contentPlay';
+ setProperties(obj);
+ s.Media.play(obj.mediaName, obj.mediaOffset);
+ s.Media.monitor(s, obj);
+ }
+}
+
+function stopMovie(obj) {
+ if(undefined == obj.mediaType || 'undefined' == obj.mediaType || '' == obj.mediaType) {
+ s.stopUndefinedMovie += 1;
+ } else if('programme' == obj.mediaType) {
+ obj.mediaEvent = 'contentStop';
+ setProperties(obj);
+ s.Media.stop(obj.mediaName, obj.mediaOffset);
+ s.Media.monitor(s, obj);
+ }
+}
+
+function endMovie(obj) {
+ obj.mediaEvent = 'movieEnd';
+ setProperties(obj);
+ s.Media.monitor(s, obj);
+ s.Media.close(obj.mediaName);
+}
+
+/* WARNING: Changing any of the below variables will cause drastic
+changes to how your visitor data is collected. Changes should only be
+made when instructed to do so by your account manager.*/
+s.trackingServer="bbc.112.2o7.net"
+
+/****************************** MODULES *****************************/
+/* Module: Media */
+s.m_Media_c="var m=s.m_i('Media');m.cn=function(n){var m=this;return m.s.rep(m.s.rep(m.s.rep(n,\"\\n\",''),\"\\r\",''),'--**--','')};void=function(n,l,p,b){var m=this,i=new Object,tm=new Date,a='',"
++"x;n=m.cn(n);l=parseInt(l);if(!l)l=1;if(n&&p){if(!m.l)m.l=new Object;if(m.l[n])m.close(n);if(b&&b.id)a=b.id;for (x in m.l)if(m.l[x]&&m.l[x].a==a)m.close(m.l[x].n);i.n=n;i.l=l;i.p=m.cn(p);i.a=a;i.t=0"
++";i.ts=0;i.s=Math.floor(tm.getTime()/1000);i.lx=0;i.lt=i.s;i.lo=0;i.e='';i.to=-1;m.l[n]=i}};m.close=function(n){this.e(n,0,-1)};m.play=function(n,o){var m=this,i;i=m.e(n,1,o);i.m=new Function('var m"
++"=s_c_il['+m._in+'],i;if(m.l){i=m.l[\"'+m.s.rep(i.n,'\"','\\\\\"')+'\"];if(i){if(i.lx==1)m.e(i.n,3,-1);i.mt=setTimeout(i.m,5000)}}');i.m()};m.stop=function(n,o){this.e(n,2,o)};m.track=function(n){va"
++"r m=this;if (m.trackWhilePlaying) {m.e(n,4,-1)}};m.e=function(n,x,o){var m=this,i,tm=new Date,ts=Math.floor(tm.getTime()/1000),ti=m.trackSeconds,tp=m.trackMilestones,z=new Array,j,d='--**--',t=1,b,"
++"v=m.trackVars,e=m.trackEvents,pe='media',pev3,w=new Object,vo=new Object;n=m.cn(n);i=n&&m.l&&m.l[n]?m.l[n]:0;if(i){w.name=n;w.length=i.l;w.playerName=i.p;if(i.to<0)w.event=\"OPEN\";else w.event=(x="
++"=1?\"PLAY\":(x==2?\"STOP\":(x==3?\"MONITOR\":\"CLOSE\")));voidTime=new Date();voidTime.setTime(i.s*1000);if(x>2||(x!=i.lx&&(x!=2||i.lx==1))) {b=\"Media.\"+name;pev3 = m.s.ape(i.n)+d+i.l+d+m.s.a"
++"pe(i.p)+d;if(x){if(o<0&&i.lt>0){o=(ts-i.lt)+i.lo;o=o<i.l?o:i.l-1}o=Math.floor(o);if(x>=2&&i.lo<o){i.t+=o-i.lo;i.ts+=o-i.lo;}if(x<=2){i.e+=(x==1?'S':'E')+o;i.lx=x;}else if(i.lx!=1)m.e(n,1,o);i.lt=ts"
++";i.lo=o;pev3+=i.t+d+i.s+d+(m.trackWhilePlaying&&i.to>=0?'L'+i.to:'')+i.e+(x!=2?(m.trackWhilePlaying?'L':'E')+o:'');if(m.trackWhilePlaying){b=0;pe='m_o';if(x!=4){w.offset=o;w.percent=((w.offset+1)/w"
++".length)*100;w.percent=w.percent>100?100:Math.floor(w.percent);w.timePlayed=i.t;if(m.monitor)m.monitor(m.s,w)}if(i.to<0)pe='m_s';else if(x==4)pe='m_i';else{t=0;v=e='None';ti=ti?parseInt(ti):0;z=tp?"
++"m.s.sp(tp,','):0;if(ti&&i.ts>=ti)t=1;else if(z){if(o<i.to)i.to=o;else{for(j=0;j<z.length;j++){ti=z[j]?parseInt(z[j]):0;if(ti&&((i.to+1)/i.l<ti/100)&&((o+1)/i.l>=ti/100)){t=1;j=z.length}}}}}}}else{m"
++".e(n,2,-1);if(m.trackWhilePlaying){w.offset=i.lo;w.percent=((w.offset+1)/w.length)*100;w.percent=w.percent>100?100:Math.floor(w.percent);w.timePlayed=i.t;if(m.monitor)m.monitor(m.s,w)}m.l[n]=0;if(i"
++".e){pev3+=i.t+d+i.s+d+(m.trackWhilePlaying&&i.to>=0?'L'+i.to:'')+i.e;if(m.trackWhilePlaying){v=e='None';pe='m_o'}else{t=0;m.s.fbr(b)}}else t=0;b=0}if(t){vo.linkTrackVars=v;vo.linkTrackEvents=e;vo.p"
++"e=pe;vo.pev3=pev3;m.s.t(vo,b);if(m.trackWhilePlaying){i.ts=0;i.to=o;i.e=''}}}}return i};m.ae=function(n,l,p,x,o,b){if(n&&p){var m=this;if(!m.l||!m.l[n])void(n,l,p,b);m.e(n,x,o)}};m.a=function(o,t"
++"){var m=this,i=o.id?o.id:o.name,n=o.name,p=0,v,c,c1,c2,xc=m.s.h,x,e,f1,f2='s_media_'+m._in+'_oc',f3='s_media_'+m._in+'_t',f4='s_media_'+m._in+'_s',f5='s_media_'+m._in+'_l',f6='s_media_'+m._in+'_m',"
++"f7='s_media_'+m._in+'_c',tcf,w;if(!i){if(!m.c)m.c=0;i='s_media_'+m._in+'_'+m.c;m.c++}if(!o.id)o.id=i;if(!o.name)o.name=n=i;if(!m.ol)m.ol=new Object;if(m.ol[i])return;m.ol[i]=o;if(!xc)xc=m.s.b;tcf=n"
++"ew Function('o','var e,p=0;try{if(o.versionInfo&&o.currentMedia&&o.controls)p=1}catch(e){p=0}return p');p=tcf(o);if(!p){tcf=new Function('o','var e,p=0,t;try{t=o.GetQuickTimeVersion();if(t)p=2}catc"
++"h(e){p=0}return p');p=tcf(o);if(!p){tcf=new Function('o','var e,p=0,t;try{t=o.GetVersionInfo();if(t)p=3}catch(e){p=0}return p');p=tcf(o)}}v=\"var m=s_c_il[\"+m._in+\"],o=m.ol['\"+i+\"']\";if(p==1){"
++"p='Windows Media Player '+o.versionInfo;c1=v+',n,p,l,x=-1,cm,c,mn;if(o){cm=o.currentMedia;c=o.controls;if(cm&&c){mn=cm.name?cm.name:c.URL;l=cm.duration;p=c.currentPosition;n=o.playState;if(n){if(n="
++"=8)x=0;if(n==3)x=1;if(n==1||n==2||n==4||n==5||n==6)x=2;}';c2='if(x>=0)m.ae(mn,l,\"'+p+'\",x,x!=2?p:-1,o)}}';c=c1+c2;if(m.s.isie&&xc){x=m.s.d.createElement('script');x.language='jscript';x.type='tex"
++"t/javascript';x.htmlFor=i;x.event='PlayStateChange(NewState)';x.defer=true;x.text=c;xc.appendChild(x);o[f6]=new Function(c1+'if(n==3){x=3;'+c2+'}setTimeout(o.'+f6+',5000)');o[f6]()}}if(p==2){p='Qui"
++"ckTime Player '+(o.GetIsQuickTimeRegistered()?'Pro ':'')+o.GetQuickTimeVersion();f1=f2;c=v+',n,x,t,l,p,p2,mn;if(o){mn=o.GetMovieName()?o.GetMovieName():o.GetURL();n=o.GetRate();t=o.GetTimeScale();l"
++"=o.GetDuration()/t;p=o.GetTime()/t;p2=o.'+f5+';if(n!=o.'+f4+'||p<p2||p-p2>5){x=2;if(n!=0)x=1;else if(p>=l)x=0;if(p<p2||p-p2>5)m.ae(mn,l,\"'+p+'\",2,p2,o);m.ae(mn,l,\"'+p+'\",x,x!=2?p:-1,o)}if(n>0&&"
++"o.'+f7+'>=10){m.ae(mn,l,\"'+p+'\",3,p,o);o.'+f7+'=0}o.'+f7+'++;o.'+f4+'=n;o.'+f5+'=p;setTimeout(\"'+v+';o.'+f2+'(0,0)\",500)}';o[f1]=new Function('a','b',c);o[f4]=-1;o[f7]=0;o[f1](0,0)}if(p==3){p='"
++"RealPlayer '+o.GetVersionInfo();f1=n+'_OnPlayStateChange';c1=v+',n,x=-1,l,p,mn;if(o){mn=o.GetTitle()?o.GetTitle():o.GetSource();n=o.GetPlayState();l=o.GetLength()/1000;p=o.GetPosition()/1000;if(n!="
++"o.'+f4+'){if(n==3)x=1;if(n==0||n==2||n==4||n==5)x=2;if(n==0&&(p>=l||p==0))x=0;if(x>=0)m.ae(mn,l,\"'+p+'\",x,x!=2?p:-1,o)}if(n==3&&(o.'+f7+'>=10||!o.'+f3+')){m.ae(mn,l,\"'+p+'\",3,p,o);o.'+f7+'=0}o."
++"'+f7+'++;o.'+f4+'=n;';c2='if(o.'+f2+')o.'+f2+'(o,n)}';if(m.s.wd[f1])o[f2]=m.s.wd[f1];m.s.wd[f1]=new Function('a','b',c1+c2);o[f1]=new Function('a','b',c1+'setTimeout(\"'+v+';o.'+f1+'(0,0)\",o.'+f3+"
++"'?500:5000);'+c2);o[f4]=-1;if(m.s.isie)o[f3]=1;o[f7]=0;o[f1](0,0)}};m.as=new Function('e','var m=s_c_il['+m._in+'],l,n;if(m.autoTrack&&m.s.d.getElementsByTagName){l=m.s.d.getElementsByTagName(m.s.i"
++"sie?\"OBJECT\":\"EMBED\");if(l)for(n=0;n<l.length;n++)m.a(l[n]);}');if(s.wd.attachEvent)s.wd.attachEvent('onloaddisabled',m.as);else if(s.wd.addEventListener)s.wd.addEventListener('loaddisabled',m.as,false)";
+s.m_i("Media");
+
+
+/************* DO NOT ALTER ANYTHING BELOW THIS LINE ! **************/
+var s_code='',s_objectID;function s_gi(un,pg,ss){var c="s._c='s_c';s.wd=window;if(!s.wd.s_c_in){s.wd.s_c_il=new Array;s.wd.s_c_in=0;}s._il=s.wd.s_c_il;s._in=s.wd.s_c_in;s._il[s._in]=s;s.wd.s_c_in++;s"
++".an=s_an;s.cls=function(x,c){var i,y='';if(!c)c=this.an;for(i=0;i<x.length;i++){n=x.substring(i,i+1);if(c.indexOf(n)>=0)y+=n}return y};s.fl=function(x,l){return x?(''+x).substring(0,l):x};s.co=func"
++"tion(o){if(!o)return o;var n=new Object,x;for(x in o)if(x.indexOf('select')<0&&x.indexOf('filter')<0)n[x]=o[x];return n};s.num=function(x){x=''+x;for(var p=0;p<x.length;p++)if(('0123456789').indexO"
++"f(x.substring(p,p+1))<0)return 0;return 1};s.rep=s_rep;s.sp=s_sp;s.jn=s_jn;s.ape=function(x){var s=this,h='0123456789ABCDEF',i,c=s.charSet,n,l,e,y='';c=c?c.toUpperCase():'';if(x){x=''+x;if(s.em==3)"
++"return encodeURIComponent(x);else if(c=='AUTO'&&('').charCodeAt){for(i=0;i<x.length;i++){c=x.substring(i,i+1);n=x.charCodeAt(i);if(n>127){l=0;e='';while(n||l<4){e=h.substring(n%16,n%16+1)+e;n=(n-n%"
++"16)/16;l++}y+='%u'+e}else if(c=='+')y+='%2B';else y+=escape(c)}return y}else{x=s.rep(escape(''+x),'+','%2B');if(c&&s.em==1&&x.indexOf('%u')<0&&x.indexOf('%U')<0){i=x.indexOf('%');while(i>=0){i++;if"
++"(h.substring(8).indexOf(x.substring(i,i+1).toUpperCase())>=0)return x.substring(0,i)+'u00'+x.substring(i);i=x.indexOf('%',i)}}}}return x};s.epa=function(x){var s=this;if(x){x=''+x;return s.em==3?de"
++"codeURIComponent(x):unescape(s.rep(x,'+',' '))}return x};s.pt=function(x,d,f,a){var s=this,t=x,z=0,y,r;while(t){y=t.indexOf(d);y=y<0?t.length:y;t=t.substring(0,y);r=s[f](t,a);if(r)return r;z+=y+d.l"
++"ength;t=x.substring(z,x.length);t=z<x.length?t:''}return ''};s.isf=function(t,a){var c=a.indexOf(':');if(c>=0)a=a.substring(0,c);if(t.substring(0,2)=='s_')t=t.substring(2);return (t!=''&&t==a)};s.f"
++"sf=function(t,a){var s=this;if(s.pt(a,',','isf',t))s.fsg+=(s.fsg!=''?',':'')+t;return 0};s.fs=function(x,f){var s=this;s.fsg='';s.pt(x,',','fsf',f);return s.fsg};s.si=function(){var s=this,i,k,v,c="
++"s_gi+'var s=s_gi(\"'+s.oun+'\");s.sa(\"'+s.un+'\");';for(i=0;i<s.va_g.length;i++){k=s.va_g[i];v=s[k];if(v!=undefined){if(typeof(v)=='string')c+='s.'+k+'=\"'+s_fe(v)+'\";';else c+='s.'+k+'='+v+';'}}"
++"c+=\"s.lnk=s.eo=s.linkName=s.linkType=s.wd.s_objectID=s.ppu=s.pe=s.pev1=s.pev2=s.pev3='';\";return c};s.c_d='';s.c_gdf=function(t,a){var s=this;if(!s.num(t))return 1;return 0};s.c_gd=function(){var"
++" s=this,d=s.wd.location.hostname,n=s.fpCookieDomainPeriods,p;if(!n)n=s.cookieDomainPeriods;if(d&&!s.c_d){n=n?parseInt(n):2;n=n>2?n:2;p=d.lastIndexOf('.');if(p>=0){while(p>=0&&n>1){p=d.lastIndexOf('"
++".',p-1);n--}s.c_d=p>0&&s.pt(d,'.','c_gdf',0)?d.substring(p):d}}return s.c_d};s.c_r=function(k){var s=this;k=s.ape(k);var c=' '+s.d.cookie,i=c.indexOf(' '+k+'='),e=i<0?i:c.indexOf(';',i),v=i<0?'':s."
++"epa(c.substring(i+2+k.length,e<0?c.length:e));return v!='[[B]]'?v:''};s.c_w=function(k,v,e){var s=this,d=s.c_gd(),l=s.cookieLifetime,t;v=''+v;l=l?(''+l).toUpperCase():'';if(e&&l!='SESSION'&&l!='NON"
++"E'){t=(v!=''?parseInt(l?l:0):-60);if(t){e=new Date;e.setTime(e.getTime()+(t*1000))}}if(k&&l!='NONE'){s.d.cookie=k+'='+s.ape(v!=''?v:'[[B]]')+'; path=/;'+(e&&l!='SESSION'?' expires='+e.toGMTString()"
++"+';':'')+(d?' domain='+d+';':'');return s.c_r(k)==v}return 0};s.eh=function(o,e,r,f){var s=this,b='s_'+e+'_'+s._in,n=-1,l,i,x;if(!s.ehl)s.ehl=new Array;l=s.ehl;for(i=0;i<l.length&&n<0;i++){if(l[i]."
++"o==o&&l[i].e==e)n=i}if(n<0){n=i;l[n]=new Object}x=l[n];x.o=o;x.e=e;f=r?x.b:f;if(r||f){x.b=r?0:o[e];x.o[e]=f}if(x.b){x.o[b]=x.b;return b}return 0};s.cet=function(f,a,t,o,b){var s=this,r,tcf;if(s.apv"
++">=5&&(!s.isopera||s.apv>=7)){tcf=new Function('s','f','a','t','var e,r;try{r=s[f](a)}catch(e){r=s[t](e)}return r');r=tcf(s,f,a,t)}else{if(s.ismac&&s.u.indexOf('MSIE 4')>=0)r=s[b](a);else{s.eh(s.wd,"
++"'onerror',0,o);r=s[f](a);s.eh(s.wd,'onerror',1)}}return r};s.gtfset=function(e){var s=this;return s.tfs};s.gtfsoe=new Function('e','var s=s_c_il['+s._in+'],c;s.eh(window,\"onerror\",1);s.etfs=1;c=s"
++".t();if(c)s.void(c);s.etfs=0;return true');s.gtfsfb=function(a){return window};s.gtfsf=function(w){var s=this,p=w.parent,l=w.location;s.tfs=w;if(p&&p.location!=l&&p.location.host==l.host){s.tfs="
++"p;return s.gtfsf(s.tfs)}return s.tfs};s.gtfs=function(){var s=this;if(!s.tfs){s.tfs=s.wd;if(!s.etfs)s.tfs=s.cet('gtfsf',s.tfs,'gtfset',s.gtfsoe,'gtfsfb')}return s.tfs};s.mrq=function(u){var s=this,"
++"l=s.rl[u],n,r;s.rl[u]=0;if(l)for(n=0;n<l.length;n++){r=l[n];s.mr(0,0,r.r,0,r.t,r.u)}};s.br=function(id,rs){var s=this;if(s.disableBufferedRequests||!s.c_w('s_br',rs))s.brl=rs};s.flushBufferedReques"
++"ts=function(){this.fbr(0)};s.fbr=function(id){var s=this,br=s.c_r('s_br');if(!br)br=s.brl;if(br){if(!s.disableBufferedRequests)s.c_w('s_br','');s.mr(0,0,br)}s.brl=0};s.mr=function(sess,q,rs,id,ta,u"
++"){var s=this,dc=s.dc,t1=s.trackingServer,t2=s.trackingServerSecure,tb=s.trackingServerBase,p='.sc',ns=s.visitorNamespace,un=s.cls(u?u:(ns?ns:s.fun)),r=new Object,l,imn='s_i_'+(un),im,b,e;if(!rs){if"
++"(t1){if(t2&&s.ssl)t1=t2}else{if(!tb)tb='2o7.net';if(dc)dc=(''+dc).toLowerCase();else dc='d1';if(tb=='2o7.net'){if(dc=='d1')dc='112';else if(dc=='d2')dc='122';p=''}t1=un+'.'+dc+'.'+p+tb}rs='http'+(s"
++".ssl?'s':'')+'://'+t1+'/b/ss/'+s.un+'/'+(s.mobile?'5.1':'1')+'/H.22.1/'+sess+'?AQB=1&ndh=1'+(q?q:'')+'&AQE=1';if(s.isie&&!s.ismac)rs=s.fl(rs,2047);if(id){s.br(id,rs);return}}if(s.d.images&&s.apv>=3"
++"&&(!s.isopera||s.apv>=7)&&(s.ns6<0||s.apv>=6.1)){if(!s.rc)s.rc=new Object;if(!s.rc[un]){s.rc[un]=1;if(!s.rl)s.rl=new Object;s.rl[un]=new Array;setTimeout('if(window.s_c_il)window.s_c_il['+s._in+']."
++"mrq(\"'+un+'\")',750)}else{l=s.rl[un];if(l){r.t=ta;r.u=un;r.r=rs;l[l.length]=r;return ''}imn+='_'+s.rc[un];s.rc[un]++}im=s.wd[imn];if(!im)im=s.wd[imn]=new Image;im.s_l=0;im.onloaddisabled=new Function('e',"
++"'this.s_l=1;var wd=window,s;if(wd.s_c_il){s=wd.s_c_il['+s._in+'];s.mrq(\"'+un+'\");s.nrs--;if(!s.nrs)s.m_m(\"rr\")}');if(!s.nrs){s.nrs=1;s.m_m('rs')}else s.nrs++;im.src=rs;if((!ta||ta=='_self'||ta="
++"='_top'||(s.wd.name&&ta==s.wd.name))&&rs.indexOf('&pe=')>=0){b=e=new Date;while(!im.s_l&&e.getTime()-b.getTime()<500)e=new Date}return ''}return '<im'+'g sr'+'c=\"'+rs+'\" width=1 height=1 border=0"
++" alt=\"\">'};s.gg=function(v){var s=this;if(!s.wd['s_'+v])s.wd['s_'+v]='';return s.wd['s_'+v]};s.glf=function(t,a){if(t.substring(0,2)=='s_')t=t.substring(2);var s=this,v=s.gg(t);if(v)s[t]=v};s.gl="
++"function(v){var s=this;if(s.pg)s.pt(v,',','glf',0)};s.rf=function(x){var s=this,y,i,j,h,l,a,b='',c='',t;if(x){y=''+x;i=y.indexOf('?');if(i>0){a=y.substring(i+1);y=y.substring(0,i);h=y.toLowerCase()"
++";i=0;if(h.substring(0,7)=='http://')i+=7;else if(h.substring(0,8)=='https://')i+=8;h=h.substring(i);i=h.indexOf(\"/\");if(i>0){h=h.substring(0,i);if(h.indexOf('google')>=0){a=s.sp(a,'&');if(a.lengt"
++"h>1){l=',q,ie,start,search_key,word,kw,cd,';for(j=0;j<a.length;j++){t=a[j];i=t.indexOf('=');if(i>0&&l.indexOf(','+t.substring(0,i)+',')>=0)b+=(b?'&':'')+t;else c+=(c?'&':'')+t}if(b&&c){y+='?'+b+'&'"
++"+c;if(''+x!=y)x=y}}}}}}return x};s.hav=function(){var s=this,qs='',fv=s.linkTrackVars,fe=s.linkTrackEvents,mn,i;if(s.pe){mn=s.pe.substring(0,1).toUpperCase()+s.pe.substring(1);if(s[mn]){fv=s[mn].tr"
++"ackVars;fe=s[mn].trackEvents}}fv=fv?fv+','+s.vl_l+','+s.vl_l2:'';for(i=0;i<s.va_t.length;i++){var k=s.va_t[i],v=s[k],b=k.substring(0,4),x=k.substring(4),n=parseInt(x),q=k;if(v&&k!='linkName'&&k!='l"
++"inkType'){if(s.pe||s.lnk||s.eo){if(fv&&(','+fv+',').indexOf(','+k+',')<0)v='';if(k=='events'&&fe)v=s.fs(v,fe)}if(v){if(k=='dynamicVariablePrefix')q='D';else if(k=='visitorID')q='vid';else if(k=='pa"
++"geURL'){q='g';v=s.fl(v,255)}else if(k=='referrer'){q='r';v=s.fl(s.rf(v),255)}else if(k=='vmk'||k=='visitorMigrationKey')q='vmt';else if(k=='visitorMigrationServer'){q='vmf';if(s.ssl&&s.visitorMigra"
++"tionServerSecure)v=''}else if(k=='visitorMigrationServerSecure'){q='vmf';if(!s.ssl&&s.visitorMigrationServer)v=''}else if(k=='charSet'){q='ce';if(v.toUpperCase()=='AUTO')v='ISO8859-1';else if(s.em="
++"=2||s.em==3)v='UTF-8'}else if(k=='visitorNamespace')q='ns';else if(k=='cookieDomainPeriods')q='cdp';else if(k=='cookieLifetime')q='cl';else if(k=='variableProvider')q='vvp';else if(k=='currencyCode"
++"')q='cc';else if(k=='channel')q='ch';else if(k=='transactionID')q='xact';else if(k=='campaign')q='v0';else if(k=='resolution')q='s';else if(k=='colorDepth')q='c';else if(k=='javascriptVersion')q='j"
++"';else if(k=='javaEnabled')q='v';else if(k=='cookiesEnabled')q='k';else if(k=='browserWidth')q='bw';else if(k=='browserHeight')q='bh';else if(k=='connectionType')q='ct';else if(k=='homepage')q='hp'"
++";else if(k=='plugins')q='p';else if(s.num(x)){if(b=='prop')q='c'+n;else if(b=='eVar')q='v'+n;else if(b=='list')q='l'+n;else if(b=='hier'){q='h'+n;v=s.fl(v,255)}}if(v)qs+='&'+q+'='+(k.substring(0,3)"
++"!='pev'?s.ape(v):v)}}}return qs};s.ltdf=function(t,h){t=t?t.toLowerCase():'';h=h?h.toLowerCase():'';var qi=h.indexOf('?');h=qi>=0?h.substring(0,qi):h;if(t&&h.substring(h.length-(t.length+1))=='.'+t"
++")return 1;return 0};s.ltef=function(t,h){t=t?t.toLowerCase():'';h=h?h.toLowerCase():'';if(t&&h.indexOf(t)>=0)return 1;return 0};s.lt=function(h){var s=this,lft=s.linkDownloaddisabledFileTypes,lef=s.linkExt"
++"ernalFilters,lif=s.linkInternalFilters;lif=lif?lif:s.wd.location.hostname;h=h.toLowerCase();if(s.trackDownloaddisabledLinks&&lft&&s.pt(lft,',','ltdf',h))return 'd';if(s.trackExternalLinks&&h.substring(0,1)"
++"!='#'&&(lef||lif)&&(!lef||s.pt(lef,',','ltef',h))&&(!lif||!s.pt(lif,',','ltef',h)))return 'e';return ''};s.lc=new Function('e','var s=s_c_il['+s._in+'],b=s.eh(this,\"onclick\");s.lnk=s.co(this);s.t"
++"();s.lnk=0;if(b)return this[b](e);return true');s.bc=new Function('e','var s=s_c_il['+s._in+'],f,tcf;if(s.d&&s.d.all&&s.d.all.cppXYctnr)return;s.eo=e.srcElement?e.srcElement:e.target;tcf=new Functi"
++"on(\"s\",\"var e;try{if(s.eo&&(s.eo.tagName||s.eo.parentElement||s.eo.parentNode))s.t()}catch(e){}\");tcf(s);s.eo=0');s.oh=function(o){var s=this,l=s.wd.location,h=o.href?o.href:'',i,j,k,p;i=h.inde"
++"xOf(':');j=h.indexOf('?');k=h.indexOf('/');if(h&&(i<0||(j>=0&&i>j)||(k>=0&&i>k))){p=o.protocol&&o.protocol.length>1?o.protocol:(l.protocol?l.protocol:'');i=l.pathname.lastIndexOf('/');h=(p?p+'//':'"
++"')+(o.host?o.host:(l.host?l.host:''))+(h.substring(0,1)!='/'?l.pathname.substring(0,i<0?0:i)+'/':'')+h}return h};s.ot=function(o){var t=o.tagName;t=t&&t.toUpperCase?t.toUpperCase():'';if(t=='SHAPE'"
++")t='';if(t){if((t=='INPUT'||t=='BUTTON')&&o.type&&o.type.toUpperCase)t=o.type.toUpperCase();else if(!t&&o.href)t='A';}return t};s.oid=function(o){var s=this,t=s.ot(o),p,c,n='',x=0;if(t&&!o.s_oid){p"
++"=o.protocol;c=o.onclick;if(o.href&&(t=='A'||t=='AREA')&&(!c||!p||p.toLowerCase().indexOf('javascript')<0))n=s.oh(o);else if(c){n=s.rep(s.rep(s.rep(s.rep(''+c,\"\\r\",''),\"\\n\",''),\"\\t\",''),' '"
++",'');x=2}else if(t=='INPUT'||t=='SUBMIT'){if(o.value)n=o.value;else if(o.innerText)n=o.innerText;else if(o.textContent)n=o.textContent;x=3}else if(o.src&&t=='IMAGE')n=o.src;if(n){o.s_oid=s.fl(n,100"
++");o.s_oidt=x}}return o.s_oid};s.rqf=function(t,un){var s=this,e=t.indexOf('='),u=e>=0?t.substring(0,e):'',q=e>=0?s.epa(t.substring(e+1)):'';if(u&&q&&(','+u+',').indexOf(','+un+',')>=0){if(u!=s.un&&"
++"s.un.indexOf(',')>=0)q='&u='+u+q+'&u=0';return q}return ''};s.rq=function(un){if(!un)un=this.un;var s=this,c=un.indexOf(','),v=s.c_r('s_sq'),q='';if(c<0)return s.pt(v,'&','rqf',un);return s.pt(un,'"
++",','rq',0)};s.sqp=function(t,a){var s=this,e=t.indexOf('='),q=e<0?'':s.epa(t.substring(e+1));s.sqq[q]='';if(e>=0)s.pt(t.substring(0,e),',','sqs',q);return 0};s.sqs=function(un,q){var s=this;s.squ[u"
++"n]=q;return 0};s.sq=function(q){var s=this,k='s_sq',v=s.c_r(k),x,c=0;s.sqq=new Object;s.squ=new Object;s.sqq[q]='';s.pt(v,'&','sqp',0);s.pt(s.un,',','sqs',q);v='';for(x in s.squ)if(x&&(!Object||!Ob"
++"ject.prototype||!Object.prototype[x]))s.sqq[s.squ[x]]+=(s.sqq[s.squ[x]]?',':'')+x;for(x in s.sqq)if(x&&(!Object||!Object.prototype||!Object.prototype[x])&&s.sqq[x]&&(x==q||c<2)){v+=(v?'&':'')+s.sqq"
++"[x]+'='+s.ape(x);c++}return s.c_w(k,v,0)};s.wdl=new Function('e','var s=s_c_il['+s._in+'],r=true,b=s.eh(s.wd,\"onloaddisabled\"),i,o,oc;if(b)r=this[b](e);for(i=0;i<s.d.links.length;i++){o=s.d.links[i];oc=o"
++".onclick?\"\"+o.onclick:\"\";if((oc.indexOf(\"s_gs(\")<0||oc.indexOf(\".s_oc(\")>=0)&&oc.indexOf(\".tl(\")<0)s.eh(o,\"onclick\",0,s.lc);}return r');s.wds=function(){var s=this;if(s.apv>3&&(!s.isie|"
++"|!s.ismac||s.apv>=5)){if(s.b&&s.b.attachEvent)s.b.attachEvent('onclick',s.bc);else if(s.b&&s.b.addEventListener)s.b.addEventListener('click',s.bc,false);else s.eh(s.wd,'onloaddisabled',0,s.wdl)}};s.vs=func"
++"tion(x){var s=this,v=s.visitorSampling,g=s.visitorSamplingGroup,k='s_vsn_'+s.un+(g?'_'+g:''),n=s.c_r(k),e=new Date,y=e.getYear();e.setYear(y+10+(y<1900?1900:0));if(v){v*=100;if(!n){if(!s.c_w(k,x,e)"
++")return 0;n=x}if(n%10000>v)return 0}return 1};s.dyasmf=function(t,m){if(t&&m&&m.indexOf(t)>=0)return 1;return 0};s.dyasf=function(t,m){var s=this,i=t?t.indexOf('='):-1,n,x;if(i>=0&&m){var n=t.subst"
++"ring(0,i),x=t.substring(i+1);if(s.pt(x,',','dyasmf',m))return n}return 0};s.uns=function(){var s=this,x=s.dynamicAccountSelection,l=s.dynamicAccountList,m=s.dynamicAccountMatch,n,i;s.un=s.un.toLowe"
++"rCase();if(x&&l){if(!m)m=s.wd.location.host;if(!m.toLowerCase)m=''+m;l=l.toLowerCase();m=m.toLowerCase();n=s.pt(l,';','dyasf',m);if(n)s.un=n}i=s.un.indexOf(',');s.fun=i<0?s.un:s.un.substring(0,i)};"
++"s.sa=function(un){var s=this;s.un=un;if(!s.oun)s.oun=un;else if((','+s.oun+',').indexOf(','+un+',')<0)s.oun+=','+un;s.uns()};s.m_i=function(n,a){var s=this,m,f=n.substring(0,1),r,l,i;if(!s.m_l)s.m_"
++"l=new Object;if(!s.m_nl)s.m_nl=new Array;m=s.m_l[n];if(!a&&m&&m._e&&!m._i)s.m_a(n);if(!m){m=new Object,m._c='s_m';m._in=s.wd.s_c_in;m._il=s._il;m._il[m._in]=m;s.wd.s_c_in++;m.s=s;m._n=n;m._l=new Ar"
++"ray('_c','_in','_il','_i','_e','_d','_dl','s','n','_r','_g','_g1','_t','_t1','_x','_x1','_rs','_rr','_l');s.m_l[n]=m;s.m_nl[s.m_nl.length]=n}else if(m._r&&!m._m){r=m._r;r._m=m;l=m._l;for(i=0;i<l.le"
++"ngth;i++)if(m[l[i]])r[l[i]]=m[l[i]];r._il[r._in]=r;m=s.m_l[n]=r}if(f==f.toUpperCase())s[n]=m;return m};s.m_a=new Function('n','g','e','if(!g)g=\"m_\"+n;var s=s_c_il['+s._in+'],c=s[g+\"_c\"],m,x,f=0"
++";if(!c)c=s.wd[\"s_\"+g+\"_c\"];if(c&&s_d)s[g]=new Function(\"s\",s_ft(s_d(c)));x=s[g];if(!x)x=s.wd[\\'s_\\'+g];if(!x)x=s.wd[g];m=s.m_i(n,1);if(x&&(!m._i||g!=\"m_\"+n)){m._i=f=1;if((\"\"+x).indexOf("
++"\"function\")>=0)x(s);else s.m_m(\"x\",n,x,e)}m=s.m_i(n,1);if(m._dl)m._dl=m._d=0;s.dlt();return f');s.m_m=function(t,n,d,e){t='_'+t;var s=this,i,x,m,f='_'+t,r=0,u;if(s.m_l&&s.m_nl)for(i=0;i<s.m_nl."
++"length;i++){x=s.m_nl[i];if(!n||x==n){m=s.m_i(x);u=m[t];if(u){if((''+u).indexOf('function')>=0){if(d&&e)u=m[t](d,e);else if(d)u=m[t](d);else u=m[t]()}}if(u)r=1;u=m[t+1];if(u&&!m[f]){if((''+u).indexO"
++"f('function')>=0){if(d&&e)u=m[t+1](d,e);else if(d)u=m[t+1](d);else u=m[t+1]()}}m[f]=1;if(u)r=1}}return r};s.m_ll=function(){var s=this,g=s.m_dl,i,o;if(g)for(i=0;i<g.length;i++){o=g[i];if(o)s.loaddisabledMo"
++"dule(o.n,o.u,o.d,o.l,o.e,1);g[i]=0}};s.loaddisabledModule=function(n,u,d,l,e,ln){var s=this,m=0,i,g,o=0,f1,f2,c=s.h?s.h:s.b,b,tcf;if(n){i=n.indexOf(':');if(i>=0){g=n.substring(i+1);n=n.substring(0,i)}else "
++"g=\"m_\"+n;m=s.m_i(n)}if((l||(n&&!s.m_a(n,g)))&&u&&s.d&&c&&s.d.createElement){if(d){m._d=1;m._dl=1}if(ln){if(s.ssl)u=s.rep(u,'http:','https:');i='s_s:'+s._in+':'+n+':'+g;b='var s=s_c_il['+s._in+'],"
++"o=s.d.getElementById(\"'+i+'\");if(s&&o){if(!o.l&&s.wd.'+g+'){o.l=1;if(o.i)clearTimeout(o.i);o.i=0;s.m_a(\"'+n+'\",\"'+g+'\"'+(e?',\"'+e+'\"':'')+')}';f2=b+'o.c++;if(!s.maxDelay)s.maxDelay=250;if(!"
++"o.l&&o.c<(s.maxDelay*2)/100)o.i=setTimeout(o.f2,100)}';f1=new Function('e',b+'}');tcf=new Function('s','c','i','u','f1','f2','var e,o=0;try{o=s.d.createElement(\"script\");if(o){o.type=\"text/javas"
++"cript\";'+(n?'o.id=i;o.defer=true;o.onloaddisabled=o.onreadystatechange=f1;o.f2=f2;o.l=0;':'')+'o.src=u;c.appendChild(o);'+(n?'o.c=0;o.i=setTimeout(f2,100)':'')+'}}catch(e){o=0}return o');o=tcf(s,c,i,u,f1,"
++"f2)}else{o=new Object;o.n=n+':'+g;o.u=u;o.d=d;o.l=l;o.e=e;g=s.m_dl;if(!g)g=s.m_dl=new Array;i=0;while(i<g.length&&g[i])i++;g[i]=o}}else if(n){m=s.m_i(n);m._e=1}return m};s.vo1=function(t,a){if(a[t]"
++"||a['!'+t])this[t]=a[t]};s.vo2=function(t,a){if(!a[t]){a[t]=this[t];if(!a[t])a['!'+t]=1}};s.dlt=new Function('var s=s_c_il['+s._in+'],d=new Date,i,vo,f=0;if(s.dll)for(i=0;i<s.dll.length;i++){vo=s.d"
++"ll[i];if(vo){if(!s.m_m(\"d\")||d.getTime()-vo._t>=s.maxDelay){s.dll[i]=0;s.t(vo)}else f=1}}if(s.dli)clearTimeout(s.dli);s.dli=0;if(f){if(!s.dli)s.dli=setTimeout(s.dlt,s.maxDelay)}else s.dll=0');s.d"
++"l=function(vo){var s=this,d=new Date;if(!vo)vo=new Object;s.pt(s.vl_g,',','vo2',vo);vo._t=d.getTime();if(!s.dll)s.dll=new Array;s.dll[s.dll.length]=vo;if(!s.maxDelay)s.maxDelay=250;s.dlt()};s.t=fun"
++"ction(vo,id){var s=this,trk=1,tm=new Date,sed=Math&&Math.random?Math.floor(Math.random()*10000000000000):tm.getTime(),sess='s'+Math.floor(tm.getTime()/10800000)%10+sed,y=tm.getYear(),vt=tm.getDate("
++")+'/'+tm.getMonth()+'/'+(y<1900?y+1900:y)+' '+tm.getHours()+':'+tm.getMinutes()+':'+tm.getSeconds()+' '+tm.getDay()+' '+tm.getTimezoneOffset(),tcf,tfs=s.gtfs(),ta=-1,q='',qs='',code='',vb=new Objec"
++"t;s.gl(s.vl_g);s.uns();s.m_ll();if(!s.td){var tl=tfs.location,a,o,i,x='',c='',v='',p='',bw='',bh='',j='1.0',k=s.c_w('s_cc','true',0)?'Y':'N',hp='',ct='',pn=0,ps;if(String&&String.prototype){j='1.1'"
++";if(j.match){j='1.2';if(tm.setUTCDate){j='1.3';if(s.isie&&s.ismac&&s.apv>=5)j='1.4';if(pn.toPrecision){j='1.5';a=new Array;if(a.forEach){j='1.6';i=0;o=new Object;tcf=new Function('o','var e,i=0;try"
++"{i=new Iterator(o)}catch(e){}return i');i=tcf(o);if(i&&i.next)j='1.7'}}}}}if(s.apv>=4)x=screen.width+'x'+screen.height;if(s.isns||s.isopera){if(s.apv>=3){v=s.n.javaEnabled()?'Y':'N';if(s.apv>=4){c="
++"screen.pixelDepth;bw=s.wd.innerWidth;bh=s.wd.innerHeight}}s.pl=s.n.plugins}else if(s.isie){if(s.apv>=4){v=s.n.javaEnabled()?'Y':'N';c=screen.colorDepth;if(s.apv>=5){bw=s.d.documentElement.offsetWid"
++"th;bh=s.d.documentElement.offsetHeight;if(!s.ismac&&s.b){tcf=new Function('s','tl','var e,hp=0;try{s.b.addBehavior(\"#default#homePage\");hp=s.b.isHomePage(tl)?\"Y\":\"N\"}catch(e){}return hp');hp="
++"tcf(s,tl);tcf=new Function('s','var e,ct=0;try{s.b.addBehavior(\"#default#clientCaps\");ct=s.b.connectionType}catch(e){}return ct');ct=tcf(s)}}}else r=''}if(s.pl)while(pn<s.pl.length&&pn<30){ps=s.f"
++"l(s.pl[pn].name,100)+';';if(p.indexOf(ps)<0)p+=ps;pn++}s.resolution=x;s.colorDepth=c;s.javascriptVersion=j;s.javaEnabled=v;s.cookiesEnabled=k;s.browserWidth=bw;s.browserHeight=bh;s.connectionType=c"
++"t;s.homepage=hp;s.plugins=p;s.td=1}if(vo){s.pt(s.vl_g,',','vo2',vb);s.pt(s.vl_g,',','vo1',vo)}if((vo&&vo._t)||!s.m_m('d')){if(s.usePlugins)s.doPlugins(s);var l=s.wd.location,r=tfs.document.referrer"
++";if(!s.pageURL)s.pageURL=l.href?l.href:l;if(!s.referrer&&!s._1_referrer){s.referrer=r;s._1_referrer=1}s.m_m('g');if(s.lnk||s.eo){var o=s.eo?s.eo:s.lnk;if(!o)return '';var p=s.pageName,w=1,t=s.ot(o)"
++",n=s.oid(o),x=o.s_oidt,h,l,i,oc;if(s.eo&&o==s.eo){while(o&&!n&&t!='BODY'){o=o.parentElement?o.parentElement:o.parentNode;if(!o)return '';t=s.ot(o);n=s.oid(o);x=o.s_oidt}oc=o.onclick?''+o.onclick:''"
++";if((oc.indexOf(\"s_gs(\")>=0&&oc.indexOf(\".s_oc(\")<0)||oc.indexOf(\".tl(\")>=0)return ''}if(n)ta=o.target;h=s.oh(o);i=h.indexOf('?');h=s.linkLeaveQueryString||i<0?h:h.substring(0,i);l=s.linkName"
++";t=s.linkType?s.linkType.toLowerCase():s.lt(h);if(t&&(h||l))q+='&pe=lnk_'+(t=='d'||t=='e'?s.ape(t):'o')+(h?'&pev1='+s.ape(h):'')+(l?'&pev2='+s.ape(l):'');else trk=0;if(s.trackInlineStats){if(!p){p="
++"s.pageURL;w=0}t=s.ot(o);i=o.sourceIndex;if(s.gg('objectID')){n=s.gg('objectID');x=1;i=1}if(p&&n&&t)qs='&pid='+s.ape(s.fl(p,255))+(w?'&pidt='+w:'')+'&oid='+s.ape(s.fl(n,100))+(x?'&oidt='+x:'')+'&ot="
++"'+s.ape(t)+(i?'&oi='+i:'')}}if(!trk&&!qs)return '';s.sampled=s.vs(sed);if(trk){if(s.sampled)code=s.mr(sess,(vt?'&t='+s.ape(vt):'')+s.hav()+q+(qs?qs:s.rq()),0,id,ta);qs='';s.m_m('t');if(s.p_r)s.p_r("
++");s.referrer=''}s.sq(qs);}else{s.dl(vo);}if(vo)s.pt(s.vl_g,',','vo1',vb);s.lnk=s.eo=s.linkName=s.linkType=s.wd.s_objectID=s.ppu=s.pe=s.pev1=s.pev2=s.pev3='';if(s.pg)s.wd.s_lnk=s.wd.s_eo=s.wd.s_link"
++"Name=s.wd.s_linkType='';if(!id&&!s.tc){s.tc=1;s.flushBufferedRequests()}return code};s.tl=function(o,t,n,vo){var s=this;s.lnk=s.co(o);s.linkType=t;s.linkName=n;s.t(vo)};if(pg){s.wd.s_co=function(o)"
++"{var s=s_gi(\"_\",1,1);return s.co(o)};s.wd.s_gs=function(un){var s=s_gi(un,1,1);return s.t()};s.wd.s_dc=function(un){var s=s_gi(un,1);return s.t()}}s.ssl=(s.wd.location.protocol.toLowerCase().inde"
++"xOf('https')>=0);s.d=document;s.b=s.d.body;if(s.d.getElementsByTagName){s.h=s.d.getElementsByTagName('HEAD');if(s.h)s.h=s.h[0]}s.n=navigator;s.u=s.n.userAgent;s.ns6=s.u.indexOf('Netscape6/');var ap"
++"n=s.n.appName,v=s.n.appVersion,ie=v.indexOf('MSIE '),o=s.u.indexOf('Opera '),i;if(v.indexOf('Opera')>=0||o>0)apn='Opera';s.isie=(apn=='Microsoft Internet Explorer');s.isns=(apn=='Netscape');s.isope"
++"ra=(apn=='Opera');s.ismac=(s.u.indexOf('Mac')>=0);if(o>0)s.apv=parseFloat(s.u.substring(o+6));else if(ie>0){s.apv=parseInt(i=v.substring(ie+5));if(s.apv>3)s.apv=parseFloat(i)}else if(s.ns6>0)s.apv="
++"parseFloat(s.u.substring(s.ns6+10));else s.apv=parseFloat(v);s.em=0;if(s.em.toPrecision)s.em=3;else if(String.fromCharCode){i=escape(String.fromCharCode(256)).toUpperCase();s.em=(i=='%C4%80'?2:(i=="
++"'%U0100'?1:0))}s.sa(un);s.vl_l='dynamicVariablePrefix,visitorID,vmk,visitorMigrationKey,visitorMigrationServer,visitorMigrationServerSecure,ppu,charSet,visitorNamespace,cookieDomainPeriods,cookieLi"
++"fetime,pageName,pageURL,referrer,currencyCode';s.va_l=s.sp(s.vl_l,',');s.vl_t=s.vl_l+',variableProvider,channel,server,pageType,transactionID,purchaseID,campaign,state,zip,events,products,linkName,"
++"linkType';for(var n=1;n<76;n++)s.vl_t+=',prop'+n+',eVar'+n+',hier'+n+',list'+n;s.vl_l2=',tnt,pe,pev1,pev2,pev3,resolution,colorDepth,javascriptVersion,javaEnabled,cookiesEnabled,browserWidth,browse"
++"rHeight,connectionType,homepage,plugins';s.vl_t+=s.vl_l2;s.va_t=s.sp(s.vl_t,',');s.vl_g=s.vl_t+',trackingServer,trackingServerSecure,trackingServerBase,fpCookieDomainPeriods,disableBufferedRequests"
++",mobile,visitorSampling,visitorSamplingGroup,dynamicAccountSelection,dynamicAccountList,dynamicAccountMatch,trackDownloaddisabledLinks,trackExternalLinks,trackInlineStats,linkLeaveQueryString,linkDownloaddisabledF"
++"ileTypes,linkExternalFilters,linkInternalFilters,linkTrackVars,linkTrackEvents,linkNames,lnk,eo,_1_referrer';s.va_g=s.sp(s.vl_g,',');s.pg=pg;s.gl(s.vl_g);if(!ss)s.wds()",
+w=window,l=w.s_c_il,n=navigator,u=n.userAgent,v=n.appVersion,e=v.indexOf('MSIE '),m=u.indexOf('Netscape6/'),a,i,s;if(un){un=un.toLowerCase();if(l)for(i=0;i<l.length;i++){s=l[i];if(!s._c||s._c=='s_c'){if(s.oun==un)return s;else if(s.fs&&s.sa&&s.fs(s.oun,un)){s.sa(un);return s}}}}w.s_an='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+w.s_sp=new Function("x","d","var a=new Array,i=0,j;if(x){if(x.split)a=x.split(d);else if(!d)for(i=0;i<x.length;i++)a[a.length]=x.substring(i,i+1);else while(i>=0){j=x.indexOf(d,i);a[a.length]=x.subst"
++"ring(i,j<0?x.length:j);i=j;if(i>=0)i+=d.length}}return a");
+w.s_jn=new Function("a","d","var x='',i,j=a.length;if(a&&j>0){x=a[0];if(j>1){if(a.join)x=a.join(d);else for(i=1;i<j;i++)x+=d+a[i]}}return x");
+w.s_rep=new Function("x","o","n","return s_jn(s_sp(x,o),n)");
+w.s_d=new Function("x","var t='`^@$#',l=s_an,l2=new Object,x2,d,b=0,k,i=x.lastIndexOf('~~'),j,v,w;if(i>0){d=x.substring(0,i);x=x.substring(i+2);l=s_sp(l,'');for(i=0;i<62;i++)l2[l[i]]=i;t=s_sp(t,'');d"
++"=s_sp(d,'~');i=0;while(i<5){v=0;if(x.indexOf(t[i])>=0) {x2=s_sp(x,t[i]);for(j=1;j<x2.length;j++){k=x2[j].substring(0,1);w=t[i]+k;if(k!=' '){v=1;w=d[b+l2[k]]}x2[j]=w+x2[j].substring(1)}}if(v)x=s_jn("
++"x2,'');else{w=t[i]+' ';if(x.indexOf(w)>=0)x=s_rep(x,w,t[i]);i++;b+=62}}}return x");
+w.s_fe=new Function("c","return s_rep(s_rep(s_rep(c,'\\\\','\\\\\\\\'),'\"','\\\\\"'),\"\\n\",\"\\\\n\")");
+w.s_fa=new Function("f","var s=f.indexOf('(')+1,e=f.indexOf(')'),a='',c;while(s>=0&&s<e){c=f.substring(s,s+1);if(c==',')a+='\",\"';else if((\"\\n\\r\\t \").indexOf(c)<0)a+=c;s++}return a?'\"'+a+'\"':"
++"a");
+w.s_ft=new Function("c","c+='';var s,e,o,a,d,q,f,h,x;s=c.indexOf('=function(');while(s>=0){s++;d=1;q='';x=0;f=c.substring(s);a=s_fa(f);e=o=c.indexOf('{',s);e++;while(d>0){h=c.substring(e,e+1);if(q){i"
++"f(h==q&&!x)q='';if(h=='\\\\')x=x?0:1;else x=0}else{if(h=='\"'||h==\"'\")q=h;if(h=='{')d++;if(h=='}')d--}if(d>0)e++}c=c.substring(0,s)+'new Function('+(a?a+',':'')+'\"'+s_fe(c.substring(o+1,e))+'\")"
++"'+c.substring(e+1);s=c.indexOf('=function(')}return c;");
+c=s_d(c);if(e>0){a=parseInt(i=v.substring(e+5));if(a>3)a=parseFloat(i)}else if(m>0)a=parseFloat(u.substring(m+10));else a=parseFloat(v);if(a>=5&&v.indexOf('Opera')<0&&u.indexOf('Opera')<0){w.s_c=new Function("un","pg","ss","var s=this;"+c);return new s_c(un,pg,ss)}else s=new Function("un","pg","ss","var s=new Object;"+s_ft(c)+";return s");return s(un,pg,ss)} \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/app/bbccom/19_61/bbccom.js b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/app/bbccom/19_61/bbccom.js
new file mode 100755
index 0000000000..6c702097a8
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/app/bbccom/19_61/bbccom.js
@@ -0,0 +1 @@
+gloaddisableder.loaddisabled(["glow","1","glow.dom","glow.anim","glow.events","glow.embed"],{onLoad:function(A){BBC.adverts=function(){var AM="undefined",s="keyValues",AY="slots",u="ads",P=0,l="",p="/",K=";",Ak="=",Ae="bbccom_display_none",Ac='<script type="text/javascript" src="',AA='"><\/script>',J="/",AB=".js",U="location",x="domain",Aj=false,Y="zoneVersion",V="zoneOverride",AX="zoneReferrer",AE="yes",Ag="no",z="bbccom_",Q="&ord=",T="httpdisabled://ad.doubleclick.net/adj/",D="httpdisabled://ad.doubleclick.net/ad/",C="httpdisabled://ad.doubleclick.net/jump/",r="httpdisabled://ad.doubleclick.net/adi/",h="httpdisabled://ad.doubleclick.net/adx/",AC="httpdisabled://ad.doubleclick.net/pfadx/bbccom.live.site.news/;tile=6;sz=512x288;dcgzip=0",AT="httpdisabled://ad.doubleclick.net/pfadx/",Ad=";slot=",B=";sz=",AG=";tile=",Ah=";dcopt=ist",k=";ord=",i="?",X="bbccom_slot_",AW=undefined,o="disable-wide-advert",AR={newsonline:"/2/hi",bbc_news:"/2/hi",refresh:"/news"},F={news:{old:"/2/hi",refresh:"/news"},sport:{old:"/sport2/hi",refresh:"/sport"},real_cities:{old:"/2/hi",refresh:"/news"}},AP='<div class="bbccom_text"><a href="httpdisabled://faq.external.bbc.co.uk/questions/bbc_online/adverts_general">Advertisement</a></div>',Aa={leaderboard:{size:"728x90,970x66,970x90"},skyscraper:{size:"120x600,160x600"},bottom:{size:"468x60"},mpu:{photo_gallery:"300x250",size:"300x250,300x600",medium_size:"160x600,300x250,300x600,336x700,336x850,336x280",wide_size:"160x600,300x250,300x600,336x700,336x850,336x280,468x648"},button:{size:"120x240"},wallpaper:{size:"1x1"},video:{size:"1x1"},companion:{size:"512x288"},storyprintsponsorship:{size:"88x31"},halfbanner:{size:"234x60"},printableversionsponsorship:{size:"120x60,215x60"},sponsor_1:{size:"88x31"},sponsor_2:{size:"88x31"},sponsor_3:{size:"88x31"},sponsor_4:{size:"88x31"},sponsor_section:{size:"88x31"},sponsor_section_news:{size:"88x31"},partner_button1:{size:"120x30"},partner_button2:{size:"120x30"},partner_button3:{size:"120x30"},partner_button4:{size:"120x30"},partner_button5:{size:"120x30"},partner_button6:{size:"120x30"},partner_button7:{size:"120x30"},partner_button8:{size:"120x30"},adsense_middle:{size:""},adsense_mpu:{size:""},promo_feature:{size:"336x224"},sponsor:{size:"88x31"},module:{size:"88x31"},"module_page-bookmark-links-top":{size:"205x31"},rectangle300x100:{size:"300x100"},not_found:{size:""}};var a,t=l,AJ={},AL={},v=14,E=false,I=false,Al=undefined,AH="4",m=["_v4","_v3_5","_v3"],AK=false,S="",AV="",e="",f="",w="",c=[],AF=[{key:"airline",rules:[{match:[/air|plane|flight|jet|aviation/g,/ash|bomb|crash|dead|detonat|disaster|disrupt|fire|injur|kill|package|passenger|crew|score|strand|strike|volcan|wreck/g],value:"!e"}]}],AO={mpu:{def:"mpu",720:"",160:"mpu_skyscraper",468:"xxl",336:"mpu336"},leaderboard:{def:"leaderboard",300:"",970:"leaderboard970"}};while(v--){t+=(Math.floor(Math.random()*10))}var Af=function(An){var Ao=[];var Am=0;if(AL.keyValues){for(var Ap in AL.keyValues){Ao[Am]=K;Ao[Am+1]=Ap;Ao[Am+2]=Ak;Ao[Am+3]=AL.keyValues[Ap];Am+=4}}if(AL.domValues){for(var As in AL.domValues){var Aq=A.dom.get(AL.domValues[As]);if("undefined"!==typeof (Aq[0])){Ao[Am]=K;Ao[Am+1]=As;Ao[Am+2]=Ak;Ao[Am+3]=escape(Aq[0].innerHTML.split(" ").join("_"));Am+=4}}}if(An){for(var Ar in An){Ao[Am]=K;Ao[Am+1]=Ar;Ao[Am+2]=Ak;Ao[Am+3]=An[Ar];Am+=4}}if(O()&&"undefined"!==bbc.fmtj.page.assetType){Ao[Am]=K;Ao[Am+1]="asset_type";Ao[Am+2]=Ak;Ao[Am+3]=bbc.fmtj.page.assetType;Am+=4}return Ao.join(l)};var q=function(){P++;return P};var d=function(At,An,Aq,Ao){if(typeof (Ao)=="undefined"||AJ[Ao]!==Ag){var As=q();var Ar=g(At);if(Ar==""){return""}if(Aq=="standardUri"){return[D,AL.site,p,AL.zone,Ad,At,B,Ar,Af(An),L(),N(),f,AG,As,k,t,i].join(l)}if(Aq=="iframe"){if(Ar.indexOf(",")!==-1){Ar=Ar.slice(0,Ar.indexOf(","))}var Ap=Ar.slice(0,Ar.indexOf("x"));var Am=Ar.slice(Ar.indexOf("x")+1);return['<iframe width="',Ap,'" height="',Am,'" frameborder="0" scrolling="no" src="',r,AL.site,p,AL.zone,Ad,At,B,Ar,Af(An),L(),N(),f,AG,As,k,t,i,'"></iframe>'].join(l)}if(At=="companion"){return[AT,AL.site,p,AL.zone,Ad,At,B,Ar,Af(An),L(),N(),f,AG,1].join(l)}if(At=="wallpaper"){}return[Ac,T,AL.site,p,AL.zone,Ad,At,B,Ar,Af(An),L(),N(),f,AG,As,W(At),k,t,i,AA].join(l)}else{return"<!-- bbccom: dependent slot closed -->"}};var W=function(Am){return Am=="leaderboard"?Ah:""};var AN=function(An,Am){AJ[An]=Ag;A.dom.get("#"+z+An).addClass(Ae);if(Am&&An=="mpu"){An="mpu_high";AN(An)}};var y=function(){var An=AJ[V]===true?AJ[Y]:J+AJ[Y]+AB;var Am=[Ac,An,AA].join(l);void(Am)};var g=function(An){if(!AL.slotSize||(typeof (AL.slotSize[An])=="undefined")){var Am=Z(An);if(E&&typeof (Am[bbc.fmtj.page.assetType])!="undefined"){return Am[bbc.fmtj.page.assetType]}else{if((I||E)&&Ai().hasClass(o)&&typeof (Am.medium_size)!="undefined"){return Am.medium_size}else{if((I||E)&&typeof (Am.wide_size)!="undefined"){return Am.wide_size}}}return Am.size}else{return AL.slotSize[An]}};var AZ=function(){if(O()&&"cream"==bbc.fmtj.page.siteVersion){E=true}};var j=function(){if(document.getElementsByName("CPS_ASSET_TYPE").length!=0){return true}return false};var O=function(){var Am=G(["CPS_ASSET_TYPE"]);if("undefined"!=typeof (bbc)&&"undefined"!=typeof (bbc.fmtj)&&"undefined"!=typeof (bbc.fmtj.page)&&"(none)"!=bbc.fmtj.page.sectionPath&&null!=bbc.fmtj.page.sectionPath){return true}return false};var R=function(){var Am=G(["CPS_ASSET_TYPE"]);return Am.CPS_ASSET_TYPE=="fix"?true:false};var AU=function(){var Ao=[];var An=window.location.pathname.replace(/^\/*/,"").replace(/\/*$/,"").split("/");for(var Am in An){if(An[Am].search(".stm")==-1){Ao.push(An[Am])}}return Ao};var AD=function(){var Am=G(["CPS_SECTION_PATH"]);return Am.CPS_SECTION_PATH.replace(/^\/*/,"").replace(/\/*$/,"").split("/")};var b=function(){if(S.length==0){if(O()&&!R()){var Ao=("index"!==bbc.fmtj.page.assetType&&"-"!==bbc.fmtj.page.storyId&&null!==bbc.fmtj.page.storyId)?bbc.fmtj.page.storyId:"default.stm";var An=("/"!==bbc.fmtj.page.sectionPath)?bbc.fmtj.page.sectionPath.toLowerCase().replace(/ /g,"_"):"";var Am="/"+bbc.fmtj.page.siteToServe;for(siteToServe in F){if(siteToServe==bbc.fmtj.page.siteToServe){Am=F[bbc.fmtj.page.siteToServe].old;continue}}S=Am+An+"/"+Ao}else{if(j()){var Ap=G(["CPS_ID","CPS_SITE_NAME","CPS_SECTION_PATH","CPS_ASSET_TYPE"]);var Am=("undefined"!=AR[Ap.CPS_SITE_NAME])?AR[Ap.CPS_SITE_NAME]:"";var Ao=("IDX"!==Ap.CPS_ASSET_TYPE)?Ap.CPS_ID:"default.stm";var An=Ap.CPS_SECTION_PATH.replace("frontpage","");S=(""!==An&&"/"!==An)?Am+"/"+An+"/"+Ao:Am}else{S=AJ[U]}}}return S};var G=function(Am){var An={};for(key in Am){if(document.getElementsByName(Am[key]).length!=0&&document.getElementsByName(Am[key])[0].getAttribute("content")!==null){An[Am[key]]=document.getElementsByName(Am[key])[0].getAttribute("content").toLowerCase().replace(/ /g,"_")}}return An};var H=function(){var Am=G(["ad_keyword","Slug"]);if(Am.ad_keyword){e=K+"keyword="+Am.ad_keyword}else{if(Am.Slug){e=K+"keyword="+Am.Slug}}};var N=function(){return e};var M=function(){for(var Am in AF){for(var Ar in AF[Am].rules){var Ap=-1;var Aq=AF[Am].rules[Ar].match.length-1;for(var An=0;An<=Aq;An++){if(AF[Am].rules[Ar].match[An].test(AV)){Ap++}else{return }}if(Ap==Aq){if("!e"==AF[Am].rules[Ar].value){c[AF[Am].key]="!e="+AF[Am].key}else{c[AF[Am].key]=AF[Am].key+"=yes"}}}}for(var Ao in c){w+=";"+c[Ao]}};var AS=function(){var Am=/[?|&]zone=preview&uid=([0-9a-fxA-FX]{26})/.test(window.location.search);if(Am){f=";uid="+RegExp.$1;if("3pt_zone_file"==AJ[Y]){AJ[Y]="preview"}else{AJ[Y]+="_preview"}A.ready(function(){var An=A.dom.get("a").filter(function(Ao){return(this.href&&this.href.indexOf("#")!=1&&this.href.indexOf("bbc.co")!=-1)});A.events.addListener(An,"click",n)})}};var n=function(Ao){Ao.stopPropagation();var Am=Ao.attachedTo.href;var An="zone=preview&"+f.split(";")[1];if(Am.indexOf("?")==-1){Am+="?"+An}else{if(Am.indexOf("#")!=-1){Am=Am.substring(0,Am.indexOf("#"))+"&"+An+Am.substring(Am.indexOf("#"))}else{Am+="&"+An}}window.location=Am;return false};var L=function(){return w};var AQ=function(Am){for(var An in Am){AJ[An]=Am[An]}};var Ai=function(){if(AW==undefined){AW=A.dom.get("body")}return AW};var Z=function(Am){if(typeof (Aa[Am])!="undefined"){return Aa[Am]}else{if(Am.indexOf("_")!==-1){return Aa[Am.slice(0,Am.lastIndexOf("_"))]}else{AJ[Am]=Ag;return Aa.not_found}}};var Ab=function(Am){BBC.adverts.addBodyClass("slot_interstitial");BBC.adverts.addBodyClass("slot_interstitial_"+Am);document.getElementById("bbccom_int_container").className="";A.ready(function(){A.events.addListener("#bbccom_int_link","click",BBC.adverts.closeInterstitial);setTimeout(AI,7000)})};var AI=function(){A.dom.get("body").removeClass("bbccom_slot_interstitial");document.getElementById("bbccom_int_container").className="bbccom_display_none"};return{init:function(An){AQ(An);var Am=G(["Headline","Description"]);AV=Am.Headline+" "+Am.Description;M();AS();H();AZ();y()},setAutoAdRefresh:function(){var Am=A.dom.get("#bbccom_mpu");if(0<Am.length){}Am=A.dom.get("#bbccom_leaderboard");if(0<Am.length){}setTimeout("BBC.adverts.setAutoAdRefresh()",10000)},setGvl3:function(Am){I=Am},setAdsBlocked:function(Am){Aj=Am},getConfig:function(Am){return AJ[Am]},getZoneData:function(){return AL},getAdvertTag:function(Ap,Am,Ao,An){return d(Ap,Am,Ao,An)},getMetaData:function(Am){var Aq,Ar,Ap={};for(var Ao=0;Ao<Am.length;Ao++){if((Aq=window[z+Am[Ao]])){for(var An in Aq){Ar=escape(Aq[An].replace(/\s+/g,""));if(Ar.length>0&&Ar.length<=64){Ap[Am[Ao]+"_"+An]=Ar}}}}return Ap},getSectionPath:function(){S=S.replace(F.news.old,F.news.refresh);S=S.replace(F.sport.old,F.sport.refresh);return S.substring(1).replace(/\/[0-9]*$/,"")},setZone:function(Am){var Ao=b();var An=AJ[x];var Ap=AJ[AX];var Aq={keyValues:{},slots:{}};var Ar=function(Av,Ax){for(var At in Ax.data){if(At===s){for(var Au in Ax.data.keyValues){Aq.keyValues[Au]=Ax.data.keyValues[Au]}}else{if(At==AY){for(var Aw in Ax.data.slots){Aq.slots[Aw]=Ax.data.slots[Aw]}}else{Aq[At]=Ax.data[At]}}}if(Ax.zones){var As=Ax.zones.length;while(As--){if(Ao.indexOf(Av+Ax.zones[As].uri)!==-1){return Ar(Av+Ax.zones[As].uri,Ax.zones[As])}}}return Aq};if(An.indexOf(".external.")!==-1){Ao="/"+An+Ao}else{if(An.indexOf("bbcearth.com")!==-1){Ao="/"+An+Ao}else{if((Ao=="/")||Ao.indexOf("/wwhomepage/")!==-1||Ao.indexOf("/wwhomepageus/")!==-1||Ao.indexOf("/wwhomepageinternational/")!==-1||Ao.indexOf("/internationalhomepage/")!==-1){Ao="/home/"}}}AL=Am.process(Ar(l,Am.zones),An,Ao,Ap)}void:function(Ao,An,Am){if(Aj===false&&AL.ads){if((!AL.slots||(AL.slots[Ao]!=false))&&AJ[Ao]!==Ag){AJ[Ao]=AE;if(typeof (Am)=="object"){Am.is_module="true"}Ai().addClass(X+Ao);if(An||typeof (An)=="undefined"){void(AP+d(Ao,Am))}else{void(d(Ao,Am))}}else{AN(Ao)}}else{AN(Ao)}},checkWrite:function(Am){if(Aj===false&&AL.ads){if((!AL.slots||(AL.slots[Am]!=false))&&AJ[Am]!==Ag){AJ[Am]=AE;return true}else{AN(Am)}return false}else{AN(Am)}return false}voidAttr:function(An,Am){if(Aj===false&&AL.ads){if(!AL.styles||(typeof (AL.styles[Am])=="undefined")){return false}return AL.styles[Am][An]}return false},hasStyles:function(){if(!AL.styles||(typeof (AL.styles)=="undefined")){return false}return true},show:function(Aq,Ao,An){if(a!==undefined){a();a=undefined}if(AJ[Aq]===AE){if(AK&&Al===AH){An=Aq+m[0]}else{if(AK&&Al!==AH){An=Aq+m[1]}else{An=Aq+m[2]}}var Ap;if(Ao==undefined){Ap=z+Aq}else{Ap=Ao}if(document.getElementById(Ap)!==null&&"undefined"!=document.getElementById(Ap)){if("bbccom_visibility_show"!=document.getElementById(Ap).className){document.getElementById(Ap).className=document.getElementById(Ap).className.replace(Ae,"")+" "+An}else{document.getElementById(Ap).className=document.getElementById(Ap).className+" "+An}}var Am=Aq.match(/^module_([a-z]+)$/);if(Am&&document.getElementById(z+Ap)!=null){document.getElementById(z+Ap).className="bbccom_module"}return true}return false},close:function(An){AJ[An]=Ag;Ai().removeClass(X+An);var Am;switch(An){case"leaderboard":Am="bbcdotcomAdvertsResetTop";break;case"bottom":Am="bbcdotcomAdvertsResetBottom";break;case"mpu":Am="bbcdotcomAdvertsResetMpu";break;default:Am=""}Ai().addClass(Am)},addBodyClass:function(Am){Ai().addClass(z+Am)},moveAd:function(An,Am){a=function(){if(A.dom.get("#"+z+Am).length>0){var Ap=A.dom.get("#"+z+An);var Ao=A.dom.get("#"+z+Am);Ap.get("script").remove();Ao.removeClass(Ae);Ao.html(Ap.html());A.dom.get("#"+z+An).remove()}}},setPageVersion:function(Am){AK=true;if(Am==="4"){Al=Am}},getPageVersion:function(){return Al},empCompanion:function(){var Am=d("companion");return Am},empCompanionResponse:function(Aq,Ao){if(Ao==undefined){Ao="bbccom_companion"}var Ar="companion";AJ[Ar]=AE;var Am=document.createElement("div");Am.setAttribute("class","comp_banner_holder");var An=document.createElement("iframe");An.setAttribute("width","300");An.setAttribute("scrolling","no");An.setAttribute("frameBorder","no");An.setAttribute("src",Aq);var Ap=document.getElementById(Ao);Ap.className="bbccom_companion bbccom_visibility_show";Am.appendChild(An);Ap.appendChild(Am)},createElement:function(Ao,An){Ao=document.createElement(Ao);for(var Am in An){Ao.setAttribute(Am,An[Am])}return Ao},empSlideCompanionResponse:function(Ar,Ap){if(Ap==undefined){Ap="bbccom_companion"}var Am=this.createElement("div",{"class":"comp_banner_holder"});var Ao={width:300,height:60,scrolling:"no",frameBorder:"no",src:Ar};var An=this.createElement("iframe",Ao);Am.appendChild(An);var Aq=document.getElementById(Ap);Aq.className="companion_parent bbccom_visibility_show";var As=A.anim.css(Aq,0.5,{height:{from:0,to:84}},{tween:A.tweens.easeOut()});A.events.addListener(As,"complete",function(At){Aq.appendChild(Am)});As.start()},adTextWrapper:function(){var Am=document.createElement("div");Am.className="bbccom_text";Am.innerHTML="Advertisement";return Am},removeCompanionBodyClasses:function(Ap,Ao){for(var Am in AO[Ap]){var An=X+AO[Ap][Am];if(Ao==0||(Am!=Ao&&Am!="def")){if(Ai().hasClass(An)){Ai().removeClass(An)}}}},replaceAd:function(At,Am){var Aw=Am.indexOf("sz");var Ar=Am.slice(Aw);var Ao=Aw+Ar.indexOf(";");var As=Am.slice(Aw,Ao);var Ax=As.slice(3);var Ap="bbccom_"+At;var Aq=document.getElementById(Ap);var An=Ax.slice(0,Ax.indexOf("x"));var Av=Ax.slice(Ax.indexOf("x")+1);var Au=this.tryReplaceAd(At,Am,Aq,An,Av);if(!Au&&At=="mpu"){var Ap="bbccom_"+At+"_high";var Aq=document.getElementById(Ap);this.tryReplaceAd(At,Am,Aq,An,Av)}},tryReplaceAd:function(As,Ar,Ap,Aq,Am){if(Ap!==null){if((Aq==0)&&(Am==0)){AN(As,true);this.removeCompanionBodyClasses(As,Aq)}else{this.removeCompanionBodyClasses(As,Aq);if(AO[As][Aq]!=""&&!Ai().hasClass(X+AO[As][Aq])){Ai().addClass(X+AO[As][Aq])}var Ao={width:Aq,height:Am,scrolling:"no",frameBorder:"no",src:Ar};var An=this.createElement("iframe",Ao);Ap.innerHTML="";Ap.appendChild(this.adTextWrapper());Ap.appendChild(An)}return true}else{return false}},setScriptRoot:function(Am){J=Am},setVideoAds:function(Am){var An=this.createElement("video",{controls:"controls",src:Am});var Ao=document.getElementById("bbccom_video");Ao.appendChild(An)},getNewsGvl3:function(){return E},getScriptRoot:function(){return J},getPredicates:function(){return L()},getSectionUrl:function(){return S},getConfig:function(){return AJ},loaddisabledInterstitial:Ab,closeInterstitial:AI}}()}});var bbcdotcom={av:{}};bbcdotcom.av.emp={hasPlayers:function(){return(typeof (embeddedMedia)=="object"&&typeof (embeddedMedia.playerInstances)=="object")},getPlayers:function(){return embeddedMedia.playerInstances},configureAll:function(){var B=bbcdotcom.av.emp;if(B.hasPlayers()){var A=B.getPlayers();for(instance in A){B.adverts.configure(A[instance]);B.events.configure(A[instance])}}}};bbcdotcom.av.emp.adverts={configure:function(A){var B=bbcdotcom.av.emp.adverts.companion.getCompanionId(A.domId);A.set("preroll",BBC.adverts.empCompanion());A.set("companionSize","300x60");A.set("companionType","adi");A.set("companionId","bbccom_companion_"+B)}};bbcdotcom.av.emp.events={register:{onPlaybackProgress:function(A){if(this.evLock){this.evLock=false;this.call("getItem",[this.domId],"getItemKind");this.metadata.mediaLength=A.duration;this.metadata.mediaId=this.attrs.id;this.metadata.adId=null;bbcdotcom.av.emp.analytics.callback("mediaStarted",this.metadata);this.metadata.mediaOffset=0;bbcdotcom.av.emp.analytics.callback("mediaPlaying",this.metadata)}else{this.metadata.mediaOffset=A.progress}},onPlaylistStarted:function(A){this.metadata.mediaName=A.title;this.metadata.mediaPlayerName=A.version},onPlaylistCompleted:function(A){bbcdotcom.av.emp.analytics.callback("playlistCompleted",this.metadata)},onMediaCompleted:function(A){this.evLock=true;bbcdotcom.av.emp.analytics.callback("mediaCompleted",this.metadata)},cueItem:function(A){this.call("getItem",[this.domId],"getItemKind")}},configure:function(A){A.onMediaPlayerInitialised=function(){for(event in bbcdotcom.av.emp.events.register){A.evLock=true;A.register(event);A[event]=bbcdotcom.av.emp.events.register[event];A.getItemKind=function(B){this.metadata.mediaType=B.item.kind};A.metadata={}}}}};bbcdotcom.av.emp.analytics={callback:function(A,B){switch(A){case"mediaStarted":startMovie(B);break;case"mediaPlaying":playMovie(B);break;case"mediaCompleted":stopMovie(B);break;case"playlistCompleted":endMovie(B);break}}};bbcdotcom.av.emp.adverts.companion={getCompanionId:function(C){var B=C.split("-"),A="";if(B.length>=2){A=B[1]}else{A=false}return A}}; \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/common/3_2/bbc_fmtj_common.js b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/common/3_2/bbc_fmtj_common.js
new file mode 100755
index 0000000000..3d5b340980
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/common/3_2/bbc_fmtj_common.js
@@ -0,0 +1 @@
+bbc.fmtj.utils.createObject("bbc.fmtj.common");bbc.fmtj.common.isReady=false;bbc.fmtj.common.version="3_2";gloaddisableder.loaddisabled(["glow","1","glow.dom","glow.events"],{async:true,onLoad:function(d){var b=d,a=d.dom,c=d.events;bbc.fmtj.common.bookmarks=(function(){var h="bbc.fmtj.common.bookmarks.display.DISPLAY_POPUP";var m="bbc.fmtj.common.bookmarks.display.DISPLAY_IFRAME";return{createBookmarks:f,display:{DISPLAY_POPUP:h,DISPLAY_IFRAME:m}};function f(p){if(p.container===undefined){p.container=".bookmark-list";}if(p.headline===undefined){p.headline=bbc.fmtj.page.headline;}if(p.site===undefined){p.site=bbc.fmtj.page.site;}if(p.storyId===undefined){p.storyId=bbc.fmtj.page.storyId;}if(p.sectionId===undefined){p.sectionId=bbc.fmtj.page.sectionId;}if(p.url===undefined){p.url=bbc.fmtj.page.url;}if(p.edition===undefined){p.edition=bbc.fmtj.page.edition;}if(p.display===undefined){p.display=h;}l(p);}function l(q){this.o=q;var p=a.get(q.container+" li a");c.addListener(p,"click",n,this);}function n(p){o.service=a.get(p.attachedTo).text();j(o);g(o);return false;}function j(p){bbc.fmtj.common.liveStats.createWebBug({referrer:document.location,pageType:"soc_"+p.service.toLowerCase(),sectionId:p.sectionId});return false;}function g(p){switch(p.display){case h:i(p);break;case m:e(p);break;default:break;}return false;}function i(q){var p=k(q.service,q.url,q.headline);bbc.fmtj.common.window.createPopup({url:p,resizable:1,scrollbars:1,width:750});return false;}function e(r){var q=k(r.service,r.url,r.headline);var p='<div id="bookmarkLightbox" class="bookmark-panel"><h2 class="hd">'+r.service+'</h2><div><iframe src="'+q+'">Your browser does not support frames.</iframe></div></div>';gloaddisableder.loaddisabled(["glow","1","glow.widgets.Panel"],{async:true,onLoad:function(t){var s=new t.widgets.Panel(t.dom.create(p),{width:750,theme:"dark",modal:true,anim:"fade"});s.show();}});}function k(p,q,s){var r="";switch(p.toLowerCase()){case"delicious":r="httpdisabled://del.icio.us/post?v=4&noui&jump=close&url="+q+"&title="+s;break;case"digg":r="httpdisabled://digg.com/remote-submit?phase=2&url="+q+"&title="+s;break;case"reddit":r="httpdisabled://reddit.com/submit?url="+q+"&title="+s;break;case"facebook":r="httpdisabled://www.facebook.com/sharer.php?u="+q+"&t="+s;break;case"stumbleupon":r="httpdisabled://www.stumbleupon.com/submit?url="+q+"&title="+s;break;default:break;}return r;}})();bbc.fmtj.common.cookies=(function(){var h={};return{cookie:k,loaddisabled:g,store:j,remove:f,create:e,read:i,erase:l};function e(p,s,m,u,r,t){var n=[];if(m!==null&&m!==undefined){var q=new Date(new Date(new Date()).getTime()+m*3600000).toUTCString();n.push("; expires="+q);}if(u!==null&&u!==undefined){n.push("; path="+u);}if(r!==null&&r!==undefined){n.push("; domain="+r);}if(t!==null&&t!==undefined){n.push("; secure="+t);}document.cookie=p+"="+s+n.join("");}function i(n){var q=n+"=";var m=document.cookie.split(";");for(var p=0;p<m.length;p++){var r=m[p];while(r.charAt(0)==" "){r=r.substring(1,r.length);}if(r.indexOf(q)==0){return r.substring(q.length,r.length);}}return null;}function l(m,q,n,p){e(m,"",-1,q,n,p);}function k(m){this._content=m.content;this._document=document;this._name=m.name;this._expiration=null;this._hours;this._path=null;this._domain=null;this._secure=false;if(m.hours!==undefined){this._expiration=new Date(new Date()).getTime()+m.hours*3600000;}if(m.path!==undefined){this._path=m.path;}if(m.domain!==undefined){this._domain=m.domain;}if(m.secure!==undefined){this._secure=m.secure;}return{hours:this._hours,path:this._path,name:this._name,domain:this._domain,secure:this._secure,content:this._content};}function g(m){}function j(p){var q=new String();var n=q.stringify(this._content);var m=this._name+"="+n;if(this._expiration!==undefined){this._expiration=new Date(this._expiration);m+="; expires="+this._expiration.toUTCString();}if(this._path!==undefined){m+="; path="+this._path;}if(this._domain!==undefined){m+="; domain="+this._domain;}if(this._secure!==undefined){m+="; secure";}this._document.cookie=m;}function f(m){}h.Cookie.prototype.store=function(){var p=new String();var n=p.stringify(this._content);var m=this._name+"="+n;if(this._expiration!==undefined){this._expiration=new Date(this._expiration);m+="; expires="+this._expiration.toUTCString();}if(this._path!==undefined){m+="; path="+this._path;}if(this._domain!==undefined){m+="; domain="+this._domain;}if(this._secure!==undefined){m+="; secure";}this._document.cookie=m;};h.Cookie.prototype.loaddisabled=function(t){if(t!==undefined){this._name=t;}var p=this._document.cookie;if(p==""){return false;}var n=p.indexOf(this._name+"=");if(n==-1){return false;}n+=this._name.length+1;var q=p.indexOf(";",n);if(q==-1){q=p.length;}var u=unescape(p.substring(n,q));var v=u.split("&");var s=false;for(var r=0;r<v.length;r++){if(v[r].search(/:/i)!=-1){v[r]=v[r].split(":");s=true;}else{this.content=v;}}if(s){this.content={};for(var r=0;r<v.length;r++){var m=unescape(v[r][1]);if(m=="false"){m=false;}else{if(m=="true"){m=true;}else{if(m.indexOf("[")!=-1){m=m.substring(1,m.length-1);if(m){m=m.split(",");}else{m=new Array();}}}}this.content[v[r][0]]=m;}}return true;};h.Cookie.prototype.remove=function(){var m=this._name+"=";if(this._path!==undefined){m+="; path="+this._path;}if(this._domain!==undefined){m+="; domain="+this._domain;}m+="; expires=Fri, 02-Jan-1970 00:00:00 GMT";this._document.cookie=m;};})();String.prototype.stringify=function(f){var e=":";if(arguments[1]){if(arguments[1]=="="){e="=";}}this._obj=f;if(typeof this._obj=="object"){if(this._obj!="[object Object]"){this._isArray=true;}this._str="";for(var g in this._obj){if(this._isArray){property="";}else{property=g+e;}if(typeof this._obj[g]!="function"){this._str=String(this._str)+property+this._obj[g]+"&";}}this._isArray=false;this._str=escape(this._str.substring(0,(this._str.length-1)));return(String(this._str));}if(typeof this._obj=="string"){this._str=escape(this._obj);return(String(this._str));}};bbc.fmtj.common.liveStats=(function(){return{initialise:g,createWebBug:h};function h(j){if(j.baseUrl===undefined){(j.baseUrl="httpdisabled://stats.bbc.co.uk/o.gif?");}if(j.siteName===undefined){j.siteName=bbc.fmtj.page.siteToServe;}if(j.pageType===undefined){(j.pageType="-");}if(j.storyId===undefined){j.storyId=bbc.fmtj.page.storyId;}if(j.sectionId===undefined){j.sectionId=bbc.fmtj.page.sectionId;}if(j.edition===undefined){j.edition=bbc.fmtj.page.editionToServe;}if(j.url===undefined){j.url=bbc.fmtj.page.uri;}if(j.referrer===undefined){j.referrer=bbc.fmtj.page.referrer;}if(j.queryString===undefined){j.queryString="-";}if(j.randomNumber===undefined){j.randomNumber=f();}var k=[j.baseUrl,"s",j.siteName,"t",j.pageType,"i",j.storyId,"p",j.sectionId,"a",j.edition,"u",j.url,"r",j.referrer,"q",j.queryString,"Z",j.randomNumber,""];b.onDomReady(function(){var l=new Image(1,1);l.src=k.join("~RS~");});return true;}function g(){b.ready(function(){c.addListener(document,"click",i);});}function i(n){var l=a.get(n.source);while(l.length>0&&l[0].attributes!==null&&!l.hasAttr("href")){l=a.get(l.parent());if(l.is("html, head, body")){return;}}var m=[];var j=true;m.push(e(l));var k=l.ancestors();k.filter(function(){return !(a.get(this).is("html, head, body"));}).each(function(){if(!j){return null;}var p=a.get(this);var q=e(p);m.push(q);if(isNaN(Number(q))){j=false;}});if(m.length>1||(m.length===1&&m[0]!="0")){document.cookie="BBCLiveStatsClick="+m.reverse().join("|")+";domain=bbc.co.uk;path=/;expires="+new Date(new Date().getTime()+8000).toGMTString()+";";}}function e(j){if(j.length>0&&j[0].attributes!==null&&j.hasAttr("id")){return(j.attr("id"));}var l=0;var k=j.prev();while(k.length>0){k=k.prev();l++;}return l;}function f(){var j=parseInt((Math.random()*10000)/100);if(j==100||j<10){j=99;}return j;}})();bbc.fmtj.common.removeNoScript=function(){a.get(".noscript").remove();};bbc.fmtj.common.tabs=(function(){var j=[];var i=0;return{createTabs:h};function h(m){var l=a.get(".tabbed");l.each(function(n){g(a.get(l[n]));});}function g(q){var l="bbc_fmtj_common_tabs_unique_index_"+f();q.addClass(l);var p=q.get(".tab");var m=q.get(".panel");var n=new a.NodeList();var r=new a.NodeList();p.each(function(u){var t=a.get(p.item(u));var v=true;var s=false;do{if(t.hasClass("tabbed")&&!t.hasClass(l)&&!v){s=true;}t=a.get(t.parent());v=false;}while(!t.hasClass(l)&&!s);if(!s){n.push(a.get(p.item(u)));}});m.each(function(u){var t=a.get(m.item(u));var v=true;var s=false;do{if(t.hasClass("tabbed")&&!t.hasClass(l)&&!v){s=true;}t=a.get(t.parent());v=false;}while(!t.hasClass(l)&&!s);if(!s){r.push(a.get(m.item(u)));}});q.removeClass(l);if(n.length!=r.length){return false;}n.each(function(s){k(n,r,s,q);});return true;}function k(n,m,l,p){c.addListener(n[l],"click",function(q){e(n,m,l);q.preventDefault();q.stopPropagation();},this);c.addListener(a.get(n[l]).get("a"),"focus",function(q){e(n,m,l);q.preventDefault();q.stopPropagation();},this);}function e(n,m,l){a.get(n,m).removeClass(void");a.get(n[l],m[l]).addClass(void");}function f(){return i++;}})();bbc.fmtj.common.window=(function(){var h=[];var g=-1;return{createPopup:j,href:voider:k,resizeTo:f};function f(l){if(l.height===undefined){l.height=window.height;}if(l.width===undefined){l.width=window.width;}window.resizeTo(l.width,l.height);}function e(l){if(l.url!==undefined){window.location.href=l.url;}return;}function k(l){if(l.url!==undefined){void(l.url);}return;}function j(n){if(n.url===undefined){n.url="";}if(n.toolbar===undefined){n.toolbar=0;}if(n.scrollbars===undefined){n.scrollbars=0;}if(n.location===undefined){n.location=0;}if(n.status===undefined){n.status=0;}if(n.menubar===undefined){n.menubar=0;}if(n.resizable===undefined){n.resizable=0;}if(n.top===undefined){n.top=100;}if(n.left===undefined){n.left=100;}if(n.width===undefined){n.width=671;}if(n.height===undefined){n.height=373;}if(n.name===undefined){n.name="win"+new Date().getTime();}if(n.parameters===undefined){n.parameters=b.lang.interpolate("toolbar={toolbar},scrollbars={scrollbars},location={location},status={status},menubar={menubar},resizable={resizable},width={width},height={height},top={top},left={left};",n);}var l=void(n.url,n.name,n.parameters);var m={id:++g,window:l,config:n};h.push(m);return m;}function i(l){}})();bbc.fmtj.common.isReady=true;}}); \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/config/apps/4_5/bbc_fmtj_config.js b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/config/apps/4_5/bbc_fmtj_config.js
new file mode 100755
index 0000000000..ecdd98e534
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/config/apps/4_5/bbc_fmtj_config.js
@@ -0,0 +1 @@
+bbc.fmtj.queue.register({namespace:"bbc.fmtj.apps.carousel",method:"createCarousel",script:"httpdisabled://news.bbcimg.co.uk/js/app/picture_gallery/2_1/carousel/carousel.js"});bbc.fmtj.queue.register({namespace:"bbc.fmtj.apps.personalisationPanel",method:"initialise",script:"httpdisabled://news.bbcimg.co.uk/js/app/personalisation_panel/1_9/personalisation_panel.js"});bbc.fmtj.queue.register({namespace:"bbc.fmtj.apps.pictureGallery",method:"createGallery",scripts:{foot:["httpdisabled://news.bbcimg.co.uk/js/app/picture_gallery/2_1/carousel/carousel.js","httpdisabled://news.bbcimg.co.uk/js/app/picture_gallery/2_1/slideshow/slideshow.js","httpdisabled://news.bbcimg.co.uk/js/app/picture_gallery/2_1/picture_gallery/picture_gallery.js"]}});bbc.fmtj.queue.register({namespace:"bbc.fmtj.apps.site_wide_alert",method:"checkAlert",scripts:{inline:["httpdisabled://news.bbcimg.co.uk/js/app/site_wide_alert/v1_2/site_wide_alert.js"]}});bbc.fmtj.queue.register({namespace:"bbc.fmtj.apps.slideshow",method:"createSlideShow",script:"httpdisabled://news.bbcimg.co.uk/js/app/picture_gallery/2_1/slideshow/slideshow.js"});bbc.fmtj.queue.register({namespace:"bbc.fmtj.apps.ticker",method:"initialise",script:"httpdisabled://news.bbcimg.co.uk/js/app/ticker/2_1/ticker.js"});bbc.fmtj.queue.register({namespace:"bbc.fmtj.net.json.model",method:"addModule",script:"httpdisabled://news.bbcimg.co.uk/js/net/json/jsonloaddisableder/2_12/jsonloaddisableder.js"}); \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/core/3_2/bbc_fmtj.js b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/core/3_2/bbc_fmtj.js
new file mode 100755
index 0000000000..6678eb0172
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/core/3_2/bbc_fmtj.js
@@ -0,0 +1 @@
+var $render;var $useMap;var $loaddisabledView;gloaddisableder.loaddisabled(["glow","1","glow.dom"],{onLoad:function(b){function a(f){var d=f.split("."),e=0,c=d.length,g=window;for(;e<c;e++){if(g[d[e]]===undefined){g[d[e]]={};}g=g[d[e]];}return g;}a("bbc.fmtj");bbc.fmtj.utils=(function(){return{createObject:a,isArray:c,prefixUrl:d};function c(e){if(e===undefined){return false;}return e.push!==undefined;}function d(e,f){if(e.toLowerCase().indexOf("httpdisabled")!=0){var g=document.domain.toLowerCase();if(g==="news.bbc.co.uk"||g==="bbc.co.uk"){if(f===undefined){f="httpdisabled://news.bbcimg.co.uk";}e=f+e;}}return e;}})();bbc.fmtj.queue=(function(){var l={},e={},f,d;return{callback:o,register:n};function k(p,r,q){var s=m(window,r);s.apply(p,q);}function c(p,r,q){if(typeof l[r]!=="object"||l[r]===null){l[r]=new Array();}l[r][l[r].length]={fContext:p,fName:r,fArgs:q};}function o(q){if(typeof l[q]==="object"&&l[q]!==null){for(var p=0;p<l[q].length;p++){k(l[q][p].fContext,l[q][p].fName,l[q][p].fArgs);}l[q]=null;}}function g(p){if(e[p]!==undefined){return;}e[p]={loaddisableding:true};if(!b.isReady){void('<script type="text/javascript" src="'+p+'"><\/script>');}else{j(p);}}function h(q){if(e[q]!==undefined){return;}e[q]={loaddisableding:true};if(f===undefined){f=document.getElementsByTagName("head")[0];}var p=document.createElement("script");p.type="text/javascript";p.src=q;f.appendChild(p);}function j(p){if(e[p]!==undefined){return;}e[p]={loaddisableding:true};b.onDomReady(function(){if(d===undefined){d=document.getElementsByTagName("body")[0];}var q=document.createElement("script");q.type="text/javascript";q.src=p;d.appendChild(q);});}function m(s,A){var p=true;var u=A.split(".");var w=s;for(i=0;i<u.length;i++){var v=u[i].indexOf("[");if(v>0){var t=/\[([0-9]*)\]/;var r=u[i].substr(0,v);var q=t.exec(u[i])[1];w=w[r];w=w[q];}else{w=w[u[i]];}p=(typeof w!=="undefined");if(!p){return null;}}return w;}function n(s){if(s.namespace===undefined){alert("No namespace specified to register script.");return;}if(s.method===undefined){alert("No method specified to register script.");return;}if(s.script===undefined&&s.scripts===undefined){alert("No sources specified to register script.");return;}var q=bbc.fmtj.utils.createObject(s.namespace);var r;var t;if(bbc.fmtj.utils.isArray(s.method)){r=s.method;}else{r=new Array(s.method);}for(var p=0;p<r.length;p++){t=r[p];q[t]=function(){c(this,s.namespace+"."+t,arguments);if(s.script!==undefined){j(s.script);}if(s.scripts!==undefined){if(s.scripts.head!==undefined){for(var u=0;u<s.scripts.head.length;u++){h(s.scripts.head[u]);}}if(s.scripts.inline!==undefined){for(var u=0;u<s.scripts.inline.length;u++){g(s.scripts.inline[u]);}}if(s.scripts.foot!==undefined){for(var u=0;u<s.scripts.foot.length;u++){j(s.scripts.foot[u]);}}}};}q.isReady=false;}})();bbc.fmtj.components=(function(){var c=[];return{registerNamespace:g,render:f,useMap:d,loaddisabledView:e};function g(h){c.splice(0,0,h);}function f(l){if(window.gloaddisableder===undefined){return false;}var n;for(var m=0;m<c.length;m++){if(c[m][l]!==undefined){n=c[m][l];break;}}if(n!==undefined){var k=[];for(var h=1;h<arguments.length;h++){k.push(arguments[h]);}n.apply(this,k);}}function d(h){if(window.gloaddisableder===undefined){return false;}gloaddisableder.use("bbc.fmtj.view",h);gloaddisableder.map.setProperties("bbc.fmtj.view",{$versionPath:"",$path:"{$base}{$versionPath}/"});}function e(h,k){if(window.gloaddisableder===undefined){return false;}var j=k;j.splice(0,0,h);j.splice(0,0,"bbc.fmtj.view");gloaddisableder.loaddisabled(j);}})();$render=bbc.fmtj.components.render;$useMap=bbc.fmtj.components.useMap;$loaddisabledView=bbc.fmtj.components.loaddisabledView;}}); \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/locationservices/locator/v4_0/locator.js b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/locationservices/locator/v4_0/locator.js
new file mode 100755
index 0000000000..a46fb0510a
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/js/locationservices/locator/v4_0/locator.js
@@ -0,0 +1 @@
+(function(){if(typeof window.locator=="object"){return;}var n,w,L,k,d,A={},F="LocatorMessages";gloaddisableder.loaddisabled(["glow","1","glow.dom","glow.events","glow.net","glow.i18n"],{onLoad:function(P){n=P;w=n.events;L=n.net;k=n.lang;d=n.i18n;n.ready(function(){A.cookie=v();});d.addLocaleModule(F,"en",{help:{title:"My Location Help",body:"The My Location feature takes a location of your choice and uses it to display relevant information on the BBC News and Weather websites. You only need to set it in one place and the information is automatically shared."}});}});var y="MYLOC",h=".bbc.co.uk",u="365",O=4,e="@",q="|",g="~";var b=false;var K=4;var p=true,M="cachebuster=cb{random}",t="locator";var N={A:"WCW",B:"WID",C:"LOCATOR"};var J="httpdisabled://news.bbc.co.uk",H="/weather/util/search/WeatherSuggestJSON",m="/weather/util/search/WeatherSearch",o="/weather/forecast/{loc}/Location";var B=J+H+".{format}?region={region}&search={search}&jsoncallback={callback}",s=J+m+".{format}?region={region}&search={search}&jsoncallback={callback}",f=J+m+".{format}?region={region}&lat={lat}&lon={lon}&radius={radius}&jsoncallback={callback}",D=J+o+".{format}?jsoncallback={callback}",c={format:"json",region:"world",search:""};var j={};var z=function(P,V,T){if(!V){return;}var T=T||{};if(j[P]){j[P].abort();j[P]=null;}if(t=="locator"){var R="c"+locator._callbackManagement.nextId++;P=k.interpolate(P,{callback:"locator._callbackManagement."+R});locator._callbackManagement[R]=function(W){j[P].destroy();j[P]=null;V(W);};}if(typeof T.useCache!=null&&!T.useCache){var Q=[new Date().getTime(),parseInt(Math.random()*100000)].join(""),U=(P.indexOf("?")>0?"&":"?");P+=U+k.interpolate(M,{random:Q});}var S=L.loaddisabledScript(P,{onLoad:function(W){j[P]=null;V(W);},onError:function(){j[P]=null;},useCache:true});if(t=="locator"){}j[P]=S;};var r=function(P){return N[P];};var G=function(Q){for(var P in N){if(N[P]==Q){return P;}}return null;};var I=function(Q,R){for(var S=0,P=Q.length;S<P;S++){if(Q[S]&&Q[S].name==R){return S;}}return null;};var i=function(P,Q){var R=I(P,Q);if(R!=null){return P[R];}else{return null;}};var E=function(P,R){var Q=i(P,R);if(!Q&&l(R)){Q={name:R,value:""};P.push(Q);}return Q;};var l=function(P){return !!G(P);};var a=function(P,Q){var R=I(P,Q);if(R!=null){delete P[R];}};var C=function(R,P){var P=P||{},Y=[],V="",Q;if(!A.shouldWriteToCookie()){return;}for(var S=0,T=R.dataStores.length;S<T;S++){var X=R.dataStores[S];if(X!=null&&X.name!=null&&X.value!=null){var W=G(X.name),Z=encodeURI(X.value),U=W+Z;Y.push(U);}}V=Y.join(q);Q=[(R.version?R.version:O),V].join(e);A.util.Cookie.create(y,Q,u,h);};var v=function(){var P=A.util.Cookie.read(y),Q={dataStores:[]},ac,Y,R,V,U;if(typeof P==="string"){P=unescape(P);ac=P.split(e);try{Y=parseInt(ac[0])?parseInt(ac[0]):null;V=ac[1];if(Y){Q.version=Y;}else{x();}if(V){U=V.split(q);for(var W=0,X=U.length;W<X;W++){var ab=U[W],aa=ab.charAt(0),Z=r(aa),S=decodeURI(ab.slice(1));Q.dataStores.push({name:Z,value:S});}}else{x();}}catch(T){x();}}return Q;};var x=function(){A.util.Cookie.destroy(y,h);};A={locales:{msg:F},EVENTS:{locationChanged:"locationChanged",autoSuggestSelected:"autoSuggestSelected",userShownLocationChange:"userShownLocationChange",userConfirmedLocationChange:"userConfirmedLocationChange",userCancelledLocationChange:"userCancelledLocationChange",userShownClearLocation:"userShownClearLocation",userConfirmedClearLocation:"userConfirmedClearLocation",userCancelledClearLocation:"userCancelledClearLocation"},cookie:{version:null,dataStores:[]},migrate:function(Q){var P=this;this.cookie=v();if(locator.getSharedLocationId()&&(P.cookie.version<O)){if(locator.debug){console.log("migrate()");}locator.fetchFromDataSet(locator.getSharedLocationId,function(R){if(locator.debug){console.log("migrate info: ",R,R.id,R.name,R.isNearest);}if(R&&R.id&&R.name){locator.getInfoFor(R.id,R.name,R.isNearest,function(T){P.cookie.version=O;var S=[];if(T.where_i_live){S.push(T.where_i_live);}if(T.where_i_live_alt){S.push(T.where_i_live_alt);}locator._setDataSet("WID",S.join("~"));if(locator.debug){console.log("migrate: success (migrated)");}if(typeof Q==="function"){Q();}});}else{if(locator.debug){console.log("migrate: no location data for migration in cookie (error)");}if(typeof Q==="function"){Q();}}},"wcw");}else{if(locator.debug){console.log("migrate: no shared location or cookie at latest version ");}if(typeof Q==="function"){Q();}}},on:function(P,Q){return w.addListener(this,P,Q);},getSharedLocationId:function(){this.cookie=v();var P=i(this.cookie.dataStores,"WCW");if(!this.COOKIE_EXTENDED&&P){C(this.cookie);this.COOKIE_EXTENDED=true;}if(P){return P.value;}return null;},getCookieVersion:function(){return this.cookie.version;},isIdValid:function(R){var Q=true;if(Q&&R=="undefined"){Q=false;}if(Q&&(R==undefined||R==null)){Q=false;}if(Q&&R.split){var P=R.split(g);Q=(P.length==K)?true:false;}return Q;},setSharedLocationId:function(S,Q){var Q=Q||{};if(S instanceof locator.Location){S=S.toString();}if(!this.isIdValid(S)||!this.shouldWriteToCookie()){return;}var R=E(this.cookie.dataStores,"WCW");R.value=S;var P=i(this.cookie.dataStores,"WID");if(P){a(this.cookie.dataStores,"WID");}this.cookie.version=O;C(this.cookie);if(Q._beforeEvent){Q._beforeEvent();}w.fire(this,this.EVENTS.locationChanged,{locationId:S});},clearSharedLocation:function(){if(!this.shouldWriteToCookie()){return;}A.util.Cookie.destroy(y,h);w.fire(this,this.EVENTS.locationChanged,{locationId:null});},hasSharedLocationIdChanged:function(P){return !!getSavedLocation()==P;},shouldWriteToCookie:function(){var Q=(this.cookie.version?this.cookie.version:O),P=O;return !!(Q<=P);},getMessageForId:function(P){var Q=d.getLocaleModule(locator.locales.msg);return Q[P]?Q[P]:null;},searchByPlaceName:function(S,X,P){var P=P||{},V=P.searchType||"full",W=P.startIndex||null,U=P.type||null,R=k.apply(c,P),Q,T;R.search=S;if(V=="suggest"){Q=k.interpolate(B,R);}else{Q=k.interpolate(s,R);}if(W){Q+="&startIndex="+W;}if(U){Q+="&type="+U;}z(Q,X,{useCache:p});},searchByCoordinatesWithRadius:function(U,T,P,V,S){var R=n.lang.apply(c,{lon:U,lat:T,radius:P}),Q=k.interpolate(f,R);if(!V){return;}z(Q,V,{useCache:p});},searchByPostcode:function(Q,T,S){var R=n.lang.apply(c,{search:encodeURIComponent(Q)}),P=k.interpolate(s,R);if(!T){return;}z(P,function(V){var U=null;if(V.results&&V.results[0]){U=V.results[0];}T(U);},{useCache:p});},getInfoFor:function(S,R,U,T){var Q=n.lang.apply(c,{loc:S,area:encodeURIComponent(R)}),P=k.interpolate(D,Q);if(U==true){P+="&area="+Q.area;}if(!T){return;}z(P,function(W){var V=null;if(W.location){T(W.location);}},{useCache:p});},fetchFromDataSet:function(U,W,S){if(!U){return null;}if(S=="wcw"){var V=i(this.cookie.dataStores,"WCW");var R=(V&&V.value)?V.value.split(g):[null,null,null,null];W({id:R[0],name:R[1],isNearest:R[2],nationId:R[3]});}else{if(S=="wid"){var Q=i(this.cookie.dataStores,"WID"),T={id:null,alt:null};if(Q&&Q.value){var P=(Q&&Q.value)?Q.value.split(g):[null,null];T={id:(P[0]?P[0]:null),alt:(P[1]?P[1]:null)};}W(T);}else{throw Error("Data Set not supported");}}},_setDataSet:function(Q,R){if(!this.shouldWriteToCookie()){return;}var P=E(this.cookie.dataStores,Q);P.value=R;C(this.cookie);},_setLegacyCookieVersion:function(P){O=P;},_callbackManagement:{nextId:0}};A.Location=(function(){var P=function(T,Q,S,R){this.id=T;this.name=Q;if(S==1){this.isNearest=true;}else{this.isNearest=false;}this.nationId=R;};P.DELIM=g;P.parse=function(R){var S=R.split(P.DELIM),V=S[0],Q=S[1],U=S[2],T=S[3];return new P(V,Q,U,T);};P.prototype={toString:function(){var Q=[this.id,this.name,(this.isNearest?"1":"0"),this.nationId];return Q.join(P.DELIM);}};return P;})();if(!window.locator){window.locator=A;}})();(function(){var a={create:function(d,g,h,f){var e="";if(h){var c=new Date();c.setTime(c.getTime()+(h*24*60*60*1000));var b="; expires="+c.toGMTString();}else{var b="";}e=d+"="+g+b+"; path=/";if(f){e+="; domain="+f;}document.cookie=e;},read:function(d){var f=d+"=";var b=document.cookie.split(";");for(var e=0;e<b.length;e++){var g=b[e];while(g.charAt(0)==" "){g=g.substring(1,g.length);}if(g.indexOf(f)==0){return g.substring(f.length,g.length);}}return null;},destroy:function(b,c){a.create(b,"",-1,c);}};if(typeof window.locator=="object"&&typeof window.locator.util!="object"){window.locator.util={Cookie:a,bind:function(b,c){return function(){c.apply(b,arguments);};}};}})();(function(){var a;gloaddisableder.loaddisabled(["glow","1","glow.dom","glow.events","glow.widgets.AutoSuggest"],{onLoad:function(b){var d={},c="postcode";a=b;d.AutoSuggest=function(e,f){var f=f||{};this.minimumThreshold=3;this.removeDuplicates=f.removeDuplicates!=undefined?f.removeDuplicates:true;this.formatItem=f.formatItem||this.formatItem;this.searchRegion=f.searchRegion;this.showSearchMessage=f.showSearchMessage!=undefined?f.showSearchMessage:false;this.autoSuggest=new a.widgets.AutoSuggest(e,function(){return[];},a.lang.apply(f,{onInputChange:locator.util.bind(this,this.inputChange),onItemSelect:locator.util.bind(this,this.itemSelect),formatItem:this.formatItem,width:f.width,activeOnShow:this.autoSelectFirstItemInList,className:f.className}));this.locale=a.i18n.getLocaleModule(locator.locales.ui);this.searchingMessage=this.locale.autoSuggest.searching;this.noResultsMessage=this.locale.autoSuggest.noResults;this.inputElement=this.autoSuggest.inputElement;};d.AutoSuggest.prototype={autoSelectFirstItemInList:false,itemElementStart:"<span>",itemElementEnd:"</span>",searchingClass:"searching",searchingMessage:"",noResultsMessage:"",inputElement:null,show:function(){this.autoSuggest.show();},hide:function(){this.autoSuggest.hide();},displaySearchingMessage:function(){this.autoSuggest.inputElement.addClass(this.searchingClass);if(this.showSearchMessage){this.displayMessage(this.searchingMessage);}},displayNoResultsMessage:function(){this.displayMessage('<span class="no-results">'+this.noResultsMessage+"</span>");},displayMessage:function(f){this.autoSuggest.setData([{name:f}]);this.autoSuggest.find(f);var e=this;window.setTimeout(function(){e.autoSuggest.show();},10);},formatItem:function(e){return e.name+(e.context?", "+e.context:"");},inputChange:function(){if(this.autoSuggest.val().length<this.minimumThreshold){return;}this.displaySearchingMessage();locator.searchByPlaceName(this.autoSuggest.val(),locator.util.bind(this,this.updateSearchResults),{searchType:"suggest",region:this.searchRegion});},itemSelect:function(e){var f=e.selectedItem.name;if(f.match(this.searchingMessage)||f.match(this.noResultsMessage)){}else{a.events.fire(locator,locator.EVENTS.autoSuggestSelected,{name:f,item:e.selectedItem});if(!e.defaultPrevented()){}}e.preventDefault();return false;},updateSearchResults:function(o){var k={},p=[],m=o[1],j=o[2],n=o[3],e=o[4],r=o[5],q=0,g,f;for(var h=0,l=m.length;h<l;h++){g=m[h];f=j[h];forecastLoc=n[h];hasForecastPage=e[h]==="1"?true:false;isForecastLoc=r[h]==="true"?true:false;comparison=g+f;if(!k[comparison]){if(this.removeDuplicates){k[comparison]=true;}if(f!=c){p.push({name:g,context:f,forecastLoc:forecastLoc,hasForecastPage:hasForecastPage,isForecastLoc:isForecastLoc});}else{q++;}}}this.autoSuggest.inputElement.removeClass(this.searchingClass);if(p.length==0&&q==0){this.displayNoResultsMessage();}else{if(p.length>0){this.autoSuggest.setData(p);this.autoSuggest.find();}}}};if(!window.locator){window.locator={};}if(!window.locator.ui){window.locator.ui={};}window.locator.ui=d;}});})();(function(){var b,a="postcode";gloaddisableder.loaddisabled(["glow","1","glow.dom","glow.events","glow.widgets.AutoSuggest","glow.i18n"],{onLoad:function(e){b=e;locator.locales.ui="LocatorUI";b.i18n.addLocaleModule(locator.locales.ui,"en",{changeLocationButton:"Change My Location",confirmSave:{confirm:"Confirm",cancel:"Cancel",back:"Back",titleSet:'You have chosen to set My Location to: <span class="locator-loc">{location}</span>',bodySet:'<p>My Location is also shared with the <a href="/weather">BBC Weather</a> site.</p><p>Press <span class="locator-action">Confirm</span> to set your Location, or <span class="locator-action">Cancel</span> to leave it unset.</p>',titleChange:'You have chosen to change My Location from: <span class="locator-loc-old">{locationOld}</span> to: <span class="locator-loc">{location}</span>',bodyChange:'<p>My Location is also shared with the <a href="/weather">BBC Weather</a> site.<p>Press <span class="locator-action">Confirm</span> if you are happy to make this change, or press <span class="locator-action">Cancel</span> to leave My Location as it is.</p>'},confirmClear:{confirm:"Confirm",cancel:"Cancel",title:"You have chosen to clear My Location.",body:'<p>Changing your setting here will also clear your location on the <a href="/weather">BBC Weather</a> site.</p><p>Please press <span class="locator-action">Confirm</span> to clear My Location, or press <span class="locator-action">Cancel</span> to leave it set to:</p><p><span class="locator-loc">{location}</span></p>'},results:{title:"Search Results for '{searchTerm}'"},pagination:{next:"Next",previous:"Prev"},autoSuggest:{searching:"Searching",noResults:"No results found, please try again"}});var c=[];window.locator.ui.Search=function(f,g){this.container=b.dom.get(f);this.searchTerm="";this.searchRegion=g.searchRegion;this.locale=b.i18n.getLocaleModule(locator.locales.ui);};window.locator.ui.Search.prototype={previousContents:[],search:function(g,h){var h=h||{};this.searchTerm=g;this.searchType=h.searchType||null;if(locator.debug){console.log("Search for %o with opts %o",g,h);}this.resetState();var f=this;locator.searchByPlaceName(g,function(j){if(!j){return;}if(j.results.length==0){b.events.fire(f,"noResults");}else{if(j.results.length==1){var i=j.results[0];if(i.has_forecast==0&&(i.type=="County"||i.type=="soundex_County")){if(locator.debug){console.log("COUNTY - only one result, searching again");}f.search(i.site_name,{searchType:"county_state"});}else{var k=k=new locator.Location(i.loc,i.site_name,(i.is_fsssi=="true"?false:true),(i.nation_id?i.nation_id:0));if(i.container){k.container=i.container;}if(locator.debug){console.log("Location object from search: ",k);}f.confirmLocationChange(k,function(l){if(locator.debug){console.log("Success!",l);}locator.setSharedLocationId(l.location.toString(),{_beforeEvent:function(){var m=[];if(i.where_i_live){m.push(i.where_i_live);}if(i.where_i_live_alt){m.push(i.where_i_live_alt);}locator._setDataSet("WID",m.join("~"));}});},function(l){if(locator.debug){console.log("User declined, go back.");}},{disambiguated:false});}}else{f.disambiguate(j);}}},{region:this.searchRegion,type:this.searchType});},showLocationChangeDialogue:function(m,q,i,f){var g=locator.getSharedLocationId()==null?false:true;var h=m.name+(m.container&&m.container!==a?", "+m.container:"");var s=this;var p=g?this.locale.confirmSave.titleChange:this.locale.confirmSave.titleSet;p=b.lang.interpolate(p,{location:h,locationOld:f.locationOld});var l=g?this.locale.confirmSave.bodyChange:this.locale.confirmSave.bodySet;var r=f.disambiguated?this.locale.confirmSave.back:this.locale.confirmSave.cancel;var o=b.lang.apply(this.locale,{title:p,body:l,cancel:r});var k=b.dom.create('<div class="locator-confirm"><p class="locator-panel-header"><strong>{title}</strong></p><div>{body}</div></div>',{interpolate:o});var n=b.dom.create('<button type="submit">{confirmSave.confirm}</button>',{interpolate:o});var j=b.dom.create('<button type="submit">{cancel}</button>',{interpolate:o});k.append(j).append(n);this.previousContents.push(this.container.children());if(locator.debug){console.log("Pushed previous contents onto stack",this.previousContents.length);}b.events.fire(locator,locator.EVENTS.userShownLocationChange,{location:m});if(g){locator.transition(this.container,k.children(),"fadeIn");}else{locator.transition(this.container,k.children(),"slideDown");}b.events.addListener(n,"click",function(t){t.location=m;b.events.fire(locator,locator.EVENTS.userConfirmedLocationChange,t);s.previousContents.pop();q(t);if(!t.defaultPrevented()){s.container.empty();}s.container.removeClass("locator-msg-confirm");});b.events.addListener(j,"click",function(t){t.location=m;b.events.fire(locator,locator.EVENTS.userCancelledLocationChange,t);var u=s.previousContents.pop();i(t);if(!t.defaultPrevented()){locator.transition(s.container,u,"fadeIn");}s.container.removeClass("locator-msg-confirm");if(f.disambiguated){s.container.addClass("locator-msg-disambiguate");}});},confirmLocationChange:function(h,l,g,k){this.container.addClass("locator-msg-confirm");var j=locator.getSharedLocationId()==null?false:true;var i=h.name+(h.container&&h.container!==a?", "+h.container:"");var f=this;if(j){locator.fetchFromDataSet(locator.getSharedLocationId(),function(m){k.locationOld=(m&&m.name)?m.name:"Not Set";f.showLocationChangeDialogue(h,l,g,k);},"wcw");}else{this.showLocationChangeDialogue(h,l,g,k);}},resetState:function(){this.container.empty();this.container.removeClass("locator-msg-disambiguate");this.container.removeClass("locator-msg-confirm");this.previousContents=[];},invokeLocationChangeFromSingleResult:function(f){if(locator.debug){console.log("invokeLocationChangeFromSingleResult: ",f);}var g=new locator.Location(f.loc,(f.area_name?f.area_name:f.name),(f.area_name?true:false),(f.nation_id?f.nation_id:0));if(f.county){g.container=f.county;}this.resetState();this.confirmLocationChange(g,function(h){if(locator.debug){console.log("Success!",h);}locator.setSharedLocationId(h.location.toString(),{_beforeEvent:function(){var i=[];if(f.where_i_live){i.push(f.where_i_live);}if(f.where_i_live_alt){i.push(f.where_i_live_alt);}if(i.length>0){locator._setDataSet("WID",i.join("~"));}}});},function(h){if(locator.debug){console.log("User declined, go back.");}},{disambiguated:false});},disambiguate:function(l){if(l.results&&l.results.length>0){this.container.addClass("locator-msg-disambiguate");var j=b.lang.interpolate(this.locale.results.title,{searchTerm:l.searchTerms});var f=l.results;var i='<div><p class="locator-panel-header"><strong>{resultsTitle}</strong></p><div class="locator-results"></div></div>';var g=b.dom.create(i,{interpolate:{resultsTitle:j}});var h=this.createResultsList(f);g.get(".locator-results").append(h);this.content=g;if(l.itemsPerPage<=l.totalResults){var k=new locator.ui.Paginator(this,this.content,l.itemsPerPage,l.totalResults);}locator.transition(this.container,this.content.children(),"slideDown");this.attachResultEventListeners();}},attachResultEventListeners:function(){var f=this;b.events.addListener(this.container.get(".locator-results a"),"click",function(g){var i=b.dom.get(g.source);if(i.data("type")&&i.data("type")=="county_state"&&i.data("searchTerm")){if(locator.debug){console.log("COUNTY - result click, searching again");}f.search(i.data("searchTerm"),{searchType:"county_state"});}else{f.container.removeClass("locator-msg-disambiguate");var h=i.data("loc");if(locator.debug){console.log("location selected",h);}f.selectLocation(h);}});},fetchResultSetAndUpdateResultsList:function(i,g,f){if(locator.debug){console.log("Fetch new result set for %o start %o, end %o for el %o",this.searchTerm,i,g);}var h=this;locator.searchByPlaceName(this.searchTerm,function(k){if(k.results){var j=h.createResultsList(k.results);j.attr("start",i+1);h.container.get(".locator-results").empty().append(j);h.attachResultEventListeners();f?f():null;}},{startIndex:i,region:this.searchRegion,type:this.searchType});},createResultsList:function(l){var j=b.dom.create("<ol></ol>"),m,n,h;for(var k=0,g=l.length;k<g;k++){h=l[k];m=b.dom.create('<li><a href="javascript:;">'+h.site_name+(h.type!=a&&h.container?", "+h.container:"")+"</a></li>");anchor=m.get("a");n=new locator.Location(h.loc,h.site_name,(h.is_fsssi=="true"?false:true),(h.nation_id?h.nation_id:0));if(h.container){n.container=h.container;}var f=[];if(h.where_i_live){f.push(h.where_i_live);}if(h.where_i_live_alt){f.push(h.where_i_live_alt);}n._wids=f;anchor.data("loc",n);if(h.has_forecast==0&&(h.type=="County"||h.type=="soundex_County")){if(locator.debug){console.log("COUNTY - attach county_state type to anchor.data");}anchor.data("searchTerm",h.site_name);anchor.data("type","county_state");}j.append(m);}return j;},selectLocation:function(f){if(locator.debug){console.log("selectLocation ",f);}this.confirmLocationChange(f,function(g){var h=g.location;if(locator.debug){console.log("Confirm change: ",h.id,h.name);}locator.setSharedLocationId(h.toString(),{_beforeEvent:function(){if(h._wids&&h._wids.length){locator._setDataSet("WID",h._wids.join("~"));}}});},function(g){var h=g.location;if(locator.debug){console.log("Request failed, should go back?");}},{disambiguated:true});},on:function(f,g){b.events.addListener(this,f,g);}};window.locator.transition=function(h,j,g,i){var i=i||{},k=i.duration||0.3,f=i.tween||b.tweens.easeOut();if(locator.debug){console.log("transition: ",h,j,g,k,f);}h.empty();switch(g){case"slideDown":h.css("height","0px").css("overflow","hidden");break;case"slideUp":h.css("overflow","hidden");break;case"fadeIn":h.css("opacity",0);break;case"fadeOut":break;}h.append(j);b.anim[g](h,k,{tween:f});};window.locator.initAutoSuggest=function(q){var q=q||{},h=q.inputSelector||".locator-auto-suggest",g=b.dom.get(h),z=q.submitSelector||".locator-search",t=b.dom.get(z),u=q.formSelector||".locator-form",n=b.dom.get(u),m=q.msgSelector||".locator-msg",r=b.dom.get(m),p=q.parentSelector,l=p?b.dom.get(p):null,k=q.defaultSelectedEvent!=undefined?q.defaultSelectedEvent:true,x,o;function A(i){var B=i.locationId;if(B){if(l&&!l.hasClass("locator-location-set")){l.addClass("locator-location-set");}}else{if(l&&l.hasClass("locator-location-set")){l.removeClass("locator-location-set");}}}locator.on("locationChanged",A);A({locationId:locator.getSharedLocationId()});o=new locator.ui.Search(r,q);var w=0,y=c.length,v=g,s;for(;w<y;w++){s=c[w];if(s.eq(v)){return;}}if(g.length===0||t.length===0||n.length===0){return;}x=new locator.ui.AutoSuggest(g,q);c.push(x.inputElement);var f=b.events.addListener(x.inputElement,"click",function(i){b.dom.get(this).val("");b.events.removeListener(f);});if(k){locator.on("autoSuggestSelected",function(i){x.hide();var B=i.item,D=(B.hasForecastPage&&B.forecastLoc&&B.context!==""),C=B.name;if(D){locator.getInfoFor(B.forecastLoc,B.name,!B.isForecastLoc,function(E){o.invokeLocationChangeFromSingleResult(E);});}else{if(B.context===""){o.search(C);}else{o.search(C,{searchType:"county_state"});}}});}var j=g.val();o.on("noResults",function(i){x.displayNoResultsMessage();});if(k){b.events.addListener(n,"submit",function(i){var B=g.val();i.preventDefault();if(j!==B){o.search(B);}return false;});}return x;};var d;locator.ui.Paginator=function(i,g,h,f){this.parent=i;this.locale=b.i18n.getLocaleModule(locator.locales.ui);this.startIndex=0;this.endIndex=h;this.currentPage=1;this.itemsPerPage=h;this.totalItems=parseInt(f);this.totalPages=Math.ceil(this.totalItems/this.endIndex);this.maxPagesToShow=5;this.maxPagesToShowHigher=10;this.numResultsPageToTriggerHigherPagination=10;this.pageMoreChar="&hellip;";this.pageJoinChar="|";this.container=b.dom.get(g);var j='<div class="locator-controls"><a href="#locator-previous" class="locator-control-prev disabled"><span class="locator-pagination-prev"></span>{pagination.previous}</a><div class="locator-pages"></div><a href="#locator-next" class="locator-control-next disabled">{pagination.next}<span class="locator-pagination-next"></span></a></div>';this.controls=b.dom.create(j,{interpolate:this.locale});this.container.append(this.controls);this.updatePrevNextLinks();this.attachEventListeners();if(locator.debug){console.log("Paging init: perPage %o, total %o, pages %o",this.itemsPerPage,this.totalItems,this.totalItems/this.endIndex);}};locator.ui.Paginator.prototype={createPageListHtml:function(){var j="<li>"+this.pageMoreChar+"</li>",q=this.maxPagesToShow,g,o,m,l,n,p;if(this.totalPages>=this.numResultsPageToTriggerHigherPagination){q=this.maxPagesToShowHigher;}g=Math.ceil(q/2);if(this.currentPage<=g||this.totalPages<=q){o=1;l=q;}else{o=this.currentPage-g+1;l=this.currentPage-g+q;}if(l>this.totalPages){l=this.totalPages;}if(locator.debug){console.log("Generating page list: currentPage: %o, currentPageOffset: %o, totalPages: %o, pageStart: %o, pageEnd: %o",this.currentPage,g,this.totalPages,o,l);}n=(o>1)?true:false;p=(l<this.totalPages)?true:false;var f="<ol>";if(n){f+=j;}for(var h=o,k=l;h<=k;h++){f+="<li"+(h==1?' class="first"':"")+'><a href="#locator-page'+h+'"'+(h==this.currentPage?' class="selected" ':"")+">"+h+"</a></li>";}if(p){f+=j;}f+="</ol>";return f;},attachEventListeners:function(){var f=this;b.events.addListener(this.controls.get(".locator-control-prev"),"click",function(g){if(locator.debug){console.log("click prev");}var h=b.dom.get(this);if(f.startIndex!=0){if(locator.debug){console.log("prev is ok");}f.parent.fetchResultSetAndUpdateResultsList(f.startIndex-f.itemsPerPage,f.endIndex-f.itemsPerPage,function(){f.startIndex-=f.itemsPerPage;f.endIndex-=f.itemsPerPage;f.currentPage--;f.updatePrevNextLinks();});}g.preventDefault();});b.events.addListener(this.controls.get(".locator-control-next"),"click",function(g){if(locator.debug){console.log("click next");}if(f.currentPage<f.totalPages){if(locator.debug){console.log("proceed to next");}f.parent.fetchResultSetAndUpdateResultsList(f.startIndex+f.itemsPerPage,f.endIndex+f.itemsPerPage,function(){f.startIndex+=f.itemsPerPage;f.endIndex+=f.itemsPerPage;f.currentPage++;f.updatePrevNextLinks();});}g.preventDefault();});b.events.addListener(this.controls.get(".locator-pages"),"click",function(h){var i=parseInt(b.dom.get(h.source).text());if(locator.debug){console.log("clicked page %o (type: %o)",i,typeof i);}if(typeof i=="number"&&!window.isNaN(i)&&f.currentPage!=i){var j=f.itemsPerPage*(i-1),g=(f.itemsPerPage*i)+f.itemsPerPage;f.parent.fetchResultSetAndUpdateResultsList(j,g,function(){f.startIndex=j;f.endIndex=g;f.currentPage=i;f.updatePrevNextLinks();});}h.preventDefault();});},updatePrevNextLinks:function(){var f=this.currentPage;if(this.currentPage>1){this.controls.get(".locator-control-prev").removeClass("disabled");}else{this.controls.get(".locator-control-prev").addClass("disabled");}if(this.currentPage<this.totalPages){this.controls.get(".locator-control-next").removeClass("disabled");}else{this.controls.get(".locator-control-next").addClass("disabled");}this.controls.get(".locator-pages").empty().append(this.createPageListHtml());},next:function(){if(locator.debug){console.log("next");}},previous:function(){if(locator.debug){console.log("prev");}}};window.locator.initSavedLocationDisplay=function(f){var f=f||".locator-saved-location",h=b.dom.get(f);if(h.length==0){return;}function g(i){var j=i.locationId;if(j){locDetails=locator.fetchFromDataSet(j,function(l){var k=(l&&l.name)?l.name:"Not Set";h.text(k);},"wcw");}else{h.text("Not set");}}locator.on("locationChanged",g);g({locationId:locator.getSharedLocationId()});};window.locator.clearLocation=function(k){var k=k||".locator-msg",f=b.dom.get(k);if(f.length==0){return;}var i;locator.fetchFromDataSet(locator.getSharedLocationId(),function(o){i=(o&&o.name)?o.name:"Not Set";},"wcw");var m=b.i18n.getLocaleModule(locator.locales.ui);var n=b.lang.interpolate(m.confirmClear.body,{location:i});m=b.lang.apply(m,{templWithLoc:n});var h=b.dom.create('<div class="locator-confirm"><p class="locator-panel-header"><strong>{confirmClear.title}</strong></p><div>{templWithLoc}</div></div>',{interpolate:m});var l=b.dom.create('<button type="submit">{confirmClear.confirm}</button>',{interpolate:m});var g=b.dom.create('<button type="submit">{confirmClear.cancel}</button>',{interpolate:m});h.append(g).append(l);var j=f.children();b.events.fire(locator,locator.EVENTS.userShownClearLocation);locator.transition(f,h.children(),"slideDown");b.events.addListener(l,"click",function(o){if(locator.debug){console.log("Confirm clear location");}b.events.fire(locator,locator.EVENTS.userConfirmedLocationChange,o);locator.clearSharedLocation();if(!o.defaultPrevented()){f.empty();}});b.events.addListener(g,"click",function(o){if(locator.debug){console.log("Cancelled clear location");}b.events.fire(locator,locator.EVENTS.userCancelledClearLocation,o);if(!o.defaultPrevented()){f.empty();f.append(j);}});};}});})(); \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/50112000/jpg/_50112416_010706746-1.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/50112000/jpg/_50112416_010706746-1.jpg
new file mode 100755
index 0000000000..f7059ba5e2
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/50112000/jpg/_50112416_010706746-1.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/50906000/jpg/_50906324_006353309-2.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/50906000/jpg/_50906324_006353309-2.jpg
new file mode 100755
index 0000000000..64feb645d6
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/50906000/jpg/_50906324_006353309-2.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/51990000/jpg/_51990536_011672235-1.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/51990000/jpg/_51990536_011672235-1.jpg
new file mode 100755
index 0000000000..a1a70e47c5
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/51990000/jpg/_51990536_011672235-1.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52015000/jpg/_52015349_flag_reuters_144.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52015000/jpg/_52015349_flag_reuters_144.jpg
new file mode 100755
index 0000000000..5a4453977f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52015000/jpg/_52015349_flag_reuters_144.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52054000/jpg/_52054442_mj.144.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52054000/jpg/_52054442_mj.144.jpg
new file mode 100755
index 0000000000..7e996c00b7
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52054000/jpg/_52054442_mj.144.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52057000/jpg/_52057539_arniecomp.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52057000/jpg/_52057539_arniecomp.jpg
new file mode 100755
index 0000000000..8acb511e6d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52057000/jpg/_52057539_arniecomp.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52058000/jpg/_52058296_holdring_thinks.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52058000/jpg/_52058296_holdring_thinks.jpg
new file mode 100755
index 0000000000..3f5edd74c5
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52058000/jpg/_52058296_holdring_thinks.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52058000/jpg/_52058744_jex_1012144_de27-1.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52058000/jpg/_52058744_jex_1012144_de27-1.jpg
new file mode 100755
index 0000000000..32d8f37538
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52058000/jpg/_52058744_jex_1012144_de27-1.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52063000/jpg/_52063276_52063272.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52063000/jpg/_52063276_52063272.jpg
new file mode 100755
index 0000000000..2c82074c9d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52063000/jpg/_52063276_52063272.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52064000/jpg/_52064940_94471941.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52064000/jpg/_52064940_94471941.jpg
new file mode 100755
index 0000000000..ef8036a316
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52064000/jpg/_52064940_94471941.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52065000/jpg/_52065323_aionscreenshot,ncsoft.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52065000/jpg/_52065323_aionscreenshot,ncsoft.jpg
new file mode 100755
index 0000000000..6cfff0b94c
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52065000/jpg/_52065323_aionscreenshot,ncsoft.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52068000/jpg/_52068942_jex_1012675_de09-1.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52068000/jpg/_52068942_jex_1012675_de09-1.jpg
new file mode 100755
index 0000000000..4c252cd4dc
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52068000/jpg/_52068942_jex_1012675_de09-1.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52069000/jpg/_52069270_011711396-1.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52069000/jpg/_52069270_011711396-1.jpg
new file mode 100755
index 0000000000..02539eb557
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52069000/jpg/_52069270_011711396-1.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52072000/jpg/_52072075_52072074.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52072000/jpg/_52072075_52072074.jpg
new file mode 100755
index 0000000000..86a564bb3d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52072000/jpg/_52072075_52072074.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52072000/jpg/_52072121_-3.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52072000/jpg/_52072121_-3.jpg
new file mode 100755
index 0000000000..48025005ed
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52072000/jpg/_52072121_-3.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52072000/jpg/_52072276_jex_1012855_de27-1.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52072000/jpg/_52072276_jex_1012855_de27-1.jpg
new file mode 100755
index 0000000000..fc8ffe683d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52072000/jpg/_52072276_jex_1012855_de27-1.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52073000/jpg/_52073406_008253948-1.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52073000/jpg/_52073406_008253948-1.jpg
new file mode 100755
index 0000000000..38ed1ae843
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52073000/jpg/_52073406_008253948-1.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52073000/jpg/_52073764_011717136-1.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52073000/jpg/_52073764_011717136-1.jpg
new file mode 100755
index 0000000000..7a307528cf
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52073000/jpg/_52073764_011717136-1.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52074000/jpg/_52074033_jex_1013006_de27.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52074000/jpg/_52074033_jex_1013006_de27.jpg
new file mode 100755
index 0000000000..51ef1d269e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52074000/jpg/_52074033_jex_1013006_de27.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52075000/jpg/_52075786_stewart_getty304.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52075000/jpg/_52075786_stewart_getty304.jpg
new file mode 100755
index 0000000000..826b1ed59a
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52075000/jpg/_52075786_stewart_getty304.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52076000/jpg/_52076863_jex_1013152_de27-1.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52076000/jpg/_52076863_jex_1013152_de27-1.jpg
new file mode 100755
index 0000000000..15a233edfc
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52076000/jpg/_52076863_jex_1013152_de27-1.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52077000/jpg/_52077604_jex_1013246_de27-1.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52077000/jpg/_52077604_jex_1013246_de27-1.jpg
new file mode 100755
index 0000000000..801aa87222
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52077000/jpg/_52077604_jex_1013246_de27-1.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52077000/jpg/_52077792_52077791.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52077000/jpg/_52077792_52077791.jpg
new file mode 100755
index 0000000000..a2101f2f8f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52077000/jpg/_52077792_52077791.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52077000/jpg/_52077993_ivory_coast.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52077000/jpg/_52077993_ivory_coast.jpg
new file mode 100755
index 0000000000..1dc9a0fe2e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52077000/jpg/_52077993_ivory_coast.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52078000/jpg/_52078134_astuteshoot.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52078000/jpg/_52078134_astuteshoot.jpg
new file mode 100755
index 0000000000..b8dcf1daf2
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52078000/jpg/_52078134_astuteshoot.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52078000/jpg/_52078945_jex_1013338_de27-1.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52078000/jpg/_52078945_jex_1013338_de27-1.jpg
new file mode 100755
index 0000000000..7c41e2dde8
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52078000/jpg/_52078945_jex_1013338_de27-1.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52079000/jpg/_52079170_jex_1013354_de30-1.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52079000/jpg/_52079170_jex_1013354_de30-1.jpg
new file mode 100755
index 0000000000..cef7cd1108
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/media/images/52079000/jpg/_52079170_jex_1013354_de30-1.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/sol/shared/img/v4/commonwealth_games_2010/cg_bbccom_banner_sprite2.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/sol/shared/img/v4/commonwealth_games_2010/cg_bbccom_banner_sprite2.png
new file mode 100755
index 0000000000..8edc15904e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/sol/shared/img/v4/commonwealth_games_2010/cg_bbccom_banner_sprite2.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/components/components.css b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/components/components.css
new file mode 100755
index 0000000000..f55eada81a
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/components/components.css
@@ -0,0 +1 @@
+.feature-promotion-accordion-holder{position:relative;z-index:100;}.feature-promotion-accordion-holder .heading-24{font-size:1.846em;font-weight:bold;letter-spacing:-1px;line-height:24px;text-rendering:optimizelegibility;padding:2px 0 9px;}.feature-promotion-accordion{position:relative;clear:both;overflow:hidden;width:624px;height:171px;margin:0 0 16px;}.feature-promotion-accordion ul{margin:0;padding:0;width:1000%;height:100%;}.feature-promotion-accordion ul li{position:relative;overflow:hidden;float:left;display:inline;clear:none;margin:0 0 0 1px;padding:0;width:304px;list-style:none;background-color:#808080;background-repeat:no-repeat;height:100%;}.feature-promotion-accordion li.first{margin-left:0;}.feature-promotion-accordion li.accordion-closed{width:159px;}.feature-promotion-accordion li .accordion-link,.feature-promotion-accordion li .accordion-link *{display:block;cursor:pointer;text-decoration:none;}.feature-promotion-accordion li .accordion-hover-layer{display:none;}.feature-promotion-accordion li.accordion-closed .accordion-hover-layer{opacity:.3;display:block;position:absolute;left:0;top:0;height:100%;width:100%;z-index:10;background-color:#fff;-webkit-transition:opacity 1s ease-out;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(opacity=30)";filter:progid:DXImageTransform.Microsoft.Alpha(opacity=30);}.feature-promotion-accordion li.accordion-hover .accordion-hover-layer{opacity:.1;-webkit-transition:opacity .2s ease-in;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(opacity=20)";filter:progid:DXImageTransform.Microsoft.Alpha(opacity=20);}.feature-promotion-accordion li .accordion-overlay{z-index:20;display:block;}.feature-promotion-accordion li .accordion-overlay-content{display:block;}.feature-promotion-accordion li .accordion-overlay{background:transparent url(../img/accordian_overlay.png) bottom left repeat;display:block;position:absolute;bottom:0;height:68px;left:0;width:100%;overflow:hidden;}.ie .feature-promotion-accordion li .accordion-overlay{bottom:-1px;}.feature-promotion-accordion li a.accordion-link:active .accordion-overlay{background:#D1700E;}.blq-js .feature-promotion-accordion li.accordion-closed a.accordion-link:active .accordion-overlay,.blq-js .feature-promotion-accordion li.accordion-closed .accordion-overlay{background:transparent url(../img/accordian_overlay.png) -1952px bottom repeat;}.blq-js .feature-promotion-accordion li.accordion-right a.accordion-link:active .accordion-overlay,.blq-js .feature-promotion-accordion li.accordion-right .accordion-overlay{background:transparent url(../img/accordian_overlay.png) -976px bottom repeat;}.feature-promotion-accordion li .accordion-link,.feature-promotion-accordion li .accordion-link strong,.feature-promotion-accordion li .accordion-link p.description{color:#fff;}.feature-promotion-accordion li strong.title{display:block;margin:0;padding:8px 8px 0;}.feature-promotion-accordion li a.accordion-link:hover strong.title,.feature-promotion-accordion li a.accordion-link:focus strong.title{text-decoration:underline;}.blq-js .feature-promotion-accordion li.accordion-closed a.accordion-link:hover strong.title,.blq-js .feature-promotion-accordion li.accordion-closed a.accordion-link:focus strong.title{text-decoration:none;}.feature-promotion-accordion li p.description{margin:0;padding:0 8px 8px;font-weight:normal;}.feature-promotion-accordion li.accordion-closed p.description{display:none;}.full-height-accordion{height:287px;}.full-height-accordion ul li{width:448px;}.full-height-accordion li.accordion-closed{width:87px;}.also-in-news{position:relative;overflow:visible;width:624px;margin:0 0 12px;padding-top:0;border-top:1px solid #ddd;}.also-in-news h2{position:relative;padding:7px 0 8px 0;border-bottom:1px solid #ddd;}.also-in-news ul{position:relative;overflow:hidden;width:304px;margin:0 0 4px;padding-top:7px;padding-left:320px;}.also-in-news li{padding-bottom:8px;}.also-in-news .column-1{position:relative;float:left;clear:left;display:inline;overflow:visible;margin-left:-320px;width:304px;}.also-in-news .column-2{clear:none;position:relative;width:304px;}.also-in-news .column-2{float:right;}.also-in-news .small-image{width:176px;padding-left:128px;padding-bottom:10px;}.also-in-news .small-image img{float:left;display:inline;margin-top:3px;margin-left:-128px;width:112px;height:63px;}.also-in-news .gvl3-icon{position:absolute;top:0;left:0;}.also-in-news .gvl3-icon-wrapper{position:absolute;top:3px;left:0;}.also-in-news .gvl3-icon-wrapper .gvl3-icon{position:relative;top:auto;left:auto;}.also-in-news a.from-external-source{display:block;margin-left:0;}.container-av-best .av-live-streams{position:relative;margin-top:-20px;margin-bottom:16px;padding-top:5px;}.av-live-streams{background:#ededed;width:320px;padding:0 8px;}.av-live-streams li{position:relative;clear:both;display:block;overflow:hidden;padding:7px 0 8px;border-top:solid 1px #ddd;}.av-live-streams li.latest-summary{border-top:none;}.av-live-streams .av-live-streams-include{position:relative;clear:both;display:block;padding:0;}.av-live-streams li.first-child{border-top:none;}.av-live-streams .gvl3-icon{position:absolute;top:8px;left:0;}.container-av-best .av-live-streams li.latest-summary{border-top:0;padding-top:7px;}.av-live-streams li.latest-summary .gvl3-icon{top:0;}.av-live-streams .news-channel-promotion li{clear:both;}.av-live-streams li.latest-summary h3,.av-live-streams li.latest-summary a{float:left;display:inline;position:relative;}.container-av-best .av-live-streams li.latest-summary h3{padding-right:10px;padding-bottom:0;}.av-live-streams li.latest-summary a{padding-left:20px;padding-right:8px;}.container-av-best .av-live-streams .news-channel-promotion li.has-icon-boxedlive{padding-bottom:8px;}.av-stories-best{position:relative;overflow:hidden;width:336px;margin:0 0 16px;background:#eee;z-index:5;}.av-stories-best .av-best-header{position:relative;padding:8px;}.av-stories-best .list-wrapper{position:relative;clear:both;overflow:scroll;overflow-y:hidden;width:100%;}.blq-js .av-stories-best .list-wrapper{overflow:visible;}.av-stories-best .carousel,.av-stories-best .av-best-items{position:relative;clear:both;overflow:hidden;width:400%;height:124px;background:#ccc;-webkit-user-select:text;}.av-stories-best .carousel li,.av-stories-best .av-best-items li{position:relative;clear:none;display:inline;float:left;overflow:hidden;width:128px;border-right:1px solid #ededed;height:116px;padding:0 7px 8px 8px;background:#D1700E;}.av-stories-best .carousel li.carousel-added{display:none;}.av-stories-best .carousel li *,.av-stories-best .av-best-items li *{color:white;font-weight:normal;}.av-stories-best .carousel li img,.av-stories-best .av-best-items li img{position:relative;display:block;width:144px;height:81px;margin:0 -8px;padding-bottom:4px;}.av-stories-best .carousel li .gvl3-icon-wrapper,.av-stories-best .av-best-items li .gvl3-icon-wrapper{position:absolute;top:0;left:0;opacity:1;z-index:10;-webkit-transition:opacity .2s ease-in;}.av-stories-best .single-item,.av-stories-best .two-items{height:auto;width:100%;}.av-stories-best .single-item li,.av-stories-best .two-items li{clear:both;height:auto;width:320px;padding-top:8px;padding-right:8px;border-right:none;}.av-stories-best .two-items li+li{border-top:1px solid white;}.av-stories-best .single-item li img,.av-stories-best .two-items li img{float:left;display:inline;margin:-8px 8px -8px -8px;padding-bottom:0;}.blq-js .av-best-carousel{display:block;}.blq-js .av-stories-best .carousel-window{background:#888;padding:0 24px;width:288px!important;}.blq-js .av-stories-best .not-visible .gvl3-icon-wrapper{opacity:0;-webkit-transition:opacity .2s ease-in;}.blq-js .ie .av-stories-best .not-visible .gvl3-icon-wrapper,.blq-js .ie7 .av-stories-best .not-visible .gvl3-icon-wrapper{display:none;}.blq-js .av-stories-best .gvl3-carousel{position:relative;margin:0;overflow:visible;clear:both;height:124px;}.blq-js .av-stories-best .pageNav{position:absolute;right:0;top:-1px;height:1px!important;width:100%!important;overflow:visible;text-align:right;}.blq-js .av-stories-best .pageNav li{position:relative;cursor:pointer;background:none;height:8px;width:8px;margin-right:3px;}.blq-js .av-stories-best .pageNav .carousel-prev-disabled,.blq-js .av-stories-best .pageNav .carousel-prev-disabled a,.blq-js .av-stories-best .pageNav .carousel-next-disabled,.blq-js .av-stories-best .pageNav .carousel-next-disabled a{cursor:default;}.blq-js .av-stories-best .pageNav .dot{float:none;display:inline;text-align:right;}.blq-js .av-stories-best .dotLabel{position:relative;top:-23px;left:-5px;display:-moz-inline-stack;display:inline-block;text-align:left;width:8px!important;height:8px!important;background:#505050;}.blq-js .ie .av-stories-best .dotLabel,.blq-js .ie7 .av-stories-best .dotLabel{display:inline;}.blq-js .av-stories-best .dotActive .dotLabel{background:#D1700E;}.blq-js .av-stories-best #leftarrow{position:absolute;margin:0;padding:0;left:0;top:1px;width:23px;height:124px;z-index:10;border-right:1px solid #ededed;background:transparent url(../../../../../1_4_9/cream/hi/shared/img/carousel-prev-next-3.png) no-repeat!important;background-position:0 center!important;-webkit-animation-name:carousel-arrow;-webkit-animation-duration:.6s;-webkit-animation-iteration-count:1;-webkit-animation-timing-function:ease-in;}.blq-js .av-stories-best #leftarrow.carousel-prev-disabled{background-position:-24px center!important;}.blq-js .av-stories-best #rightarrow{position:absolute;margin:0;padding:0;right:0;top:1px;width:24px;height:124px;z-index:10;border-left:1px solid #ededed;background:transparent url(../../../../../1_4_9/cream/hi/shared/img/carousel-prev-next-3.png) no-repeat!important;background-position:-72px center!important;-webkit-animation-name:carousel-arrow;-webkit-animation-duration:.6s;-webkit-animation-iteration-count:1;-webkit-animation-timing-function:ease-in;}.blq-js .av-stories-best #rightarrow.carousel-next-disabled{background-position:-48px center!important;}.blq-js .av-stories-best #leftarrow .dotLabel,.blq-js .av-stories-best #rightarrow .dotLabel{display:none;}@-webkit-keyframes carousel-arrow{from{opacity:0;}to{opacity:1;}}.av-stories-now{position:relative;overflow:hidden;width:624px;margin:0 0 16px;z-index:5;}.av-stories-now .av-now-header{position:relative;margin-top:-1px;padding:0 0 9px;font-size:1.846em;font-weight:bold;letter-spacing:-1px;line-height:24px;text-rendering:optimizelegibility;}.av-stories-now .list-wrapper{position:relative;clear:both;overflow:scroll;overflow-y:hidden;width:100%;}.blq-js .av-stories-now .list-wrapper{overflow:visible;}.av-stories-now .carousel,.av-stories-now .av-best-items{position:relative;clear:both;overflow:hidden;width:400%;height:124px;background:#ccc;-webkit-user-select:text;}.av-stories-now .carousel li,.av-stories-now .av-best-items li{position:relative;clear:none;display:inline;float:left;overflow:hidden;width:128px;border-right:1px solid white;height:116px;padding:0 7px 8px 8px;background:#D1700E;}.av-stories-now .carousel li.carousel-added{display:none;}.ie .av-stories-now .carousel li,.ie .av-stories-now .av-best-items li{height:116px;}.av-stories-now .carousel li *,.av-stories-now .av-best-items li *{color:white;font-weight:normal;}.av-stories-now .carousel li img,.av-stories-now .av-best-items li img{position:relative;display:block;width:144px;height:81px;margin:0 -8px;padding-bottom:4px;}.av-stories-now .carousel li .gvl3-icon-wrapper,.av-stories-now .av-best-items li .gvl3-icon-wrapper{position:absolute;top:0;left:0;opacity:1;z-index:10;-webkit-transition:opacity .2s ease-in;}.blq-js .av-best-carousel{display:block;}.blq-js .av-stories-now .carousel-window{background:#888;padding:0 24px;width:576px!important;}.blq-js .av-stories-now .not-visible .gvl3-icon-wrapper{opacity:0;-webkit-transition:opacity .2s ease-in;}.blq-js .ie .av-stories-now .not-visible .gvl3-icon-wrapper,.blq-js .ie7 .av-stories-now .not-visible .gvl3-icon-wrapper{display:none;}.blq-js .av-stories-now .gvl3-carousel{position:relative;margin:0;overflow:visible;clear:both;height:124px;}.blq-js .av-stories-now .pageNav{position:absolute;right:-8px;top:-1px;height:1px!important;width:100%!important;overflow:visible;text-align:right;}.blq-js .av-stories-now .pageNav li{position:relative;cursor:pointer;background:none;height:8px;width:8px;margin-right:3px;}.blq-js .av-stories-now .pageNav .carousel-prev-disabled,.blq-js .av-stories-now .pageNav .carousel-prev-disabled a,.blq-js .av-stories-now .pageNav .carousel-next-disabled,.blq-js .av-stories-now .pageNav .carousel-next-disabled a{cursor:default;}.blq-js .av-stories-now .pageNav .dot{float:none;display:inline;text-align:right;}.blq-js .av-stories-now .dotLabel{position:relative;top:-23px;left:-5px;display:-moz-inline-stack;display:inline-block;text-align:left;width:8px!important;height:8px!important;background:#505050;}.blq-js .ie .av-stories-now .dotLabel,.blq-js .ie7 .av-stories-now .dotLabel{display:inline;}.blq-js .av-stories-now .dotActive .dotLabel{background:#D1700E;}.blq-js .av-stories-now #leftarrow{position:absolute;margin:0;padding:0;left:-8px;top:1px;width:23px;height:124px;z-index:10;border-right:1px solid white;background:transparent url(../../../../../1_4_9/cream/hi/shared/img/carousel-prev-next-3.png) no-repeat!important;background-position:0 center!important;-webkit-animation-name:carousel-arrow;-webkit-animation-duration:.6s;-webkit-animation-iteration-count:1;-webkit-animation-timing-function:ease-in;}.blq-js .av-stories-now #leftarrow.carousel-prev-disabled{background-position:-24px center!important;}.blq-js .av-stories-now #rightarrow{position:absolute;margin:0;padding:0;right:8px;top:1px;width:24px;height:124px;z-index:10;border-left:1px solid white;background:transparent url(../../../../../1_4_9/cream/hi/shared/img/carousel-prev-next-3.png) no-repeat!important;background-position:-72px center!important;-webkit-animation-name:carousel-arrow;-webkit-animation-duration:.6s;-webkit-animation-iteration-count:1;-webkit-animation-timing-function:ease-in;}.blq-js .av-stories-now #rightarrow.carousel-next-disabled{background-position:-48px center!important;}.blq-js .av-stories-now #leftarrow .dotLabel,.blq-js .av-stories-now #rightarrow .dotLabel{display:none;}@-webkit-keyframes carousel-arrow{from{opacity:0;}to{opacity:1;}}.av-stories-source{position:relative;overflow:hidden;width:336px;margin:64px 0 16px;background:#eee;z-index:5;}.av-stories-source .av-best-header{position:relative;padding:8px;}.av-stories-source .list-wrapper{position:relative;clear:both;overflow:scroll;overflow-y:hidden;width:100%;}.blq-js .av-stories-source .list-wrapper{overflow:visible;}.av-stories-source .carousel,.av-stories-source .av-best-items{position:relative;clear:both;overflow:hidden;width:400%;height:124px;background:#ccc;-webkit-user-select:text;}.av-stories-source .carousel li,.av-stories-source .av-best-items li{position:relative;clear:none;display:inline;float:left;overflow:hidden;width:128px;border-right:1px solid #ededed;height:116px;padding:0 7px 8px 8px;background:#D1700E;}.av-stories-source .carousel li.carousel-added{display:none;}.av-stories-source .carousel li *,.av-stories-source .av-best-items li *{color:white;font-weight:normal;}.av-stories-source .carousel li img,.av-stories-source .av-best-items li img{position:relative;display:block;width:144px;height:81px;margin:0 -8px;padding-bottom:4px;}.av-stories-source .carousel li .gvl3-icon-wrapper,.av-stories-source .av-best-items li .gvl3-icon-wrapper{position:absolute;top:0;left:0;opacity:1;z-index:10;-webkit-transition:opacity .2s ease-in;}.av-stories-source .single-item,.av-stories-source .two-items{height:auto;width:100%;}.av-stories-source .single-item li,.av-stories-source .two-items li{clear:both;height:auto;width:320px;padding-top:8px;padding-right:8px;border-right:none;}.av-stories-source .two-items li+li{border-top:1px solid white;}.av-stories-source .single-item li img,.av-stories-source .two-items li img{float:left;display:inline;margin:-8px 8px -8px -8px;padding-bottom:0;}.blq-js .av-best-carousel{display:block;}.blq-js .av-stories-source .carousel-window{background:#888;padding:0 24px;width:288px!important;}.blq-js .av-stories-source .not-visible .gvl3-icon-wrapper{opacity:0;-webkit-transition:opacity .2s ease-in;}.blq-js .ie .av-stories-source .not-visible .gvl3-icon-wrapper,.blq-js .ie7 .av-stories-source .not-visible .gvl3-icon-wrapper{display:none;}.blq-js .av-stories-source .gvl3-carousel{position:relative;margin:0;overflow:visible;clear:both;height:124px;}.blq-js .av-stories-source .pageNav{position:absolute;right:0;top:-1px;height:1px!important;width:100%!important;overflow:visible;text-align:right;}.blq-js .av-stories-source .pageNav li{position:relative;cursor:pointer;background:none;height:8px;width:8px;margin-right:3px;}.blq-js .av-stories-source .pageNav .carousel-prev-disabled,.blq-js .av-stories-source .pageNav .carousel-prev-disabled a,.blq-js .av-stories-source .pageNav .carousel-next-disabled,.blq-js .av-stories-source .pageNav .carousel-next-disabled a{cursor:default;}.blq-js .av-stories-source .pageNav .dot{float:none;display:inline;text-align:right;}.blq-js .av-stories-source .dotLabel{position:relative;top:-23px;left:-5px;display:-moz-inline-stack;display:inline-block;text-align:left;width:8px!important;height:8px!important;background:#505050;}.blq-js .ie .av-stories-source .dotLabel,.blq-js .ie7 .av-stories-source .dotLabel{display:inline;}.blq-js .av-stories-source .dotActive .dotLabel{background:#D1700E;}.blq-js .av-stories-source #leftarrow{position:absolute;margin:0;padding:0;left:0;top:1px;width:23px;height:124px;z-index:10;border-right:1px solid #ededed;background:transparent url(../../../../../1_4_9/cream/hi/shared/img/carousel-prev-next-3.png) no-repeat!important;background-position:0 center!important;-webkit-animation-name:carousel-arrow;-webkit-animation-duration:.6s;-webkit-animation-iteration-count:1;-webkit-animation-timing-function:ease-in;}.blq-js .av-stories-source #leftarrow.carousel-prev-disabled{background-position:-24px center!important;}.blq-js .av-stories-source #rightarrow{position:absolute;margin:0;padding:0;right:0;top:1px;width:24px;height:124px;z-index:10;border-left:1px solid #ededed;background:transparent url(../../../../../1_4_9/cream/hi/shared/img/carousel-prev-next-3.png) no-repeat!important;background-position:-72px center!important;-webkit-animation-name:carousel-arrow;-webkit-animation-duration:.6s;-webkit-animation-iteration-count:1;-webkit-animation-timing-function:ease-in;}.blq-js .av-stories-source #rightarrow.carousel-next-disabled{background-position:-48px center!important;}.blq-js .av-stories-source #leftarrow .dotLabel,.blq-js .av-stories-source #rightarrow .dotLabel{display:none;}@-webkit-keyframes carousel-arrow{from{opacity:0;}to{opacity:1;}}.big-picture-teaser{display:block;overflow:hidden;float:none;clear:both;background-color:#ededed;position:relative;padding-bottom:8px;margin-bottom:16px;}.big-picture-teaser .heading-24{font-size:1.846em;font-weight:bold;letter-spacing:-1px;line-height:24px;text-rendering:optimizelegibility;padding:8px 0 10px 8px;}.big-picture-teaser .teaser-link{display:block;padding-left:32px;position:relative;overflow:hidden;}.big-picture-teaser .teaser-link img{margin-bottom:6px;margin-left:-32px;float:none;position:relative;display:block;}.big-picture-teaser .teaser-link .gvl3-icon{position:absolute;bottom:-5px;left:8px;float:left;}.image-light-box .tr,.image-light-box .tl,.image-light-box .tb,.image-light-box .tc .bars,.image-light-box .br,.image-light-box .bl,.image-light-box .bb{display:none;}.image-light-box .panel-hd,.image-light-box .panel-bd,.image-light-box .panel-ft{padding:0!important;margin:0!important;background:#fff!important;color:#000!important;}.blq-js .image-light-box .panel-hd,.blq-js .image-light-box .panel-bd,.blq-js .image-light-box .panel-ft{background-color:#ededed!important;}.blq-js .image-light-box .panel-hd .hd{padding:10px 8px;}.blq-js .ie7 .image-light-box .panel-hd .hd{font-size:2.4em;padding:14px 8px 14px 8px;}.blq-js .ie7 .image-light-box .panel-hd{display:block;}.blq-js .image-light-box .c a.panel-close{background-image:url(../../../../../1_4_9/cream/hi/shared/img/gvl3-icons-0-2.png)!important;background-position:-510px 0!important;background-repeat:none;overflow:hidden;width:15px;height:16px;margin:14px 10px 0 0!important;}.blq-js .ie .image-light-box .c a.panel-close{background-image:url(../../../../../1_4_9/cream/hi/shared/img/gvl3-icons-0-2.gif)!important;right:10px;}.blq-js .image-light-box .panel-ft .ft{font-size:1.3em;padding:5px 8px 14px;width:auto!important;}.ie .big-picture-teaser,.ie .big-picture-teaser .teaser-link,.ie7 .big-picture-teaser,.ie7 .big-picture-teaser .teaser-link{height:1%;}.blq-js .ie .glow173-panel .c,.blq-js .ie .image-light-box .panel-ft{width:100%;}.share-help{position:relative;width:230px;float:right;padding:0;}.share-help ul{padding:0 0 16px 0;margin:0;position:relative;}.share-help ul li{background:none;float:left;margin:0 0 0 5px;padding:0 1px;}.share-help ul li.facebook-context{overflow:visible;height:1px;position:relative;}.share-help ul li a{display:block;padding:0 0 0 16px;background-image:url(../../../../../1_4_9/cream/hi/shared/img/story_sprite.gif);background-repeat:no-repeat;text-indent:-6000px;text-decoration:none;float:left;}body.ie li.twitter a,body.ie li.facebook-popup a,.body.ie li.share a,body.ie li.email a,body.ie li.print a{width:1px;}.share-help ul .twitter a{background-position:-300px 0;}.share-help ul .share a{background-position:-460px 0;}.share-help ul .email a{background-position:-620px 0;}.share-help ul .print a{background-position:-773px 0;}.share-help iframe{top:-2px;left:0;overflow:hidden;width:80px;height:24px;font-family:Helvetica;}.blq-js .share-help ul{padding:0 0 11px 0;}.blq-js li.delicious,.blq-js li.digg,.blq-js li.reddit,.blq-js li.stumbleupon,.blq-js li.mixx,.blq-js li.google{display:none;}.share-help ul li.delicious a{background-position:-3650px 0;}.share-help ul li.digg a{background-position:-2200px 0;}.share-help ul li.facebook a{background-position:0 0;}.share-help ul li.reddit a{background-position:-2650px 0;}.share-help ul li.stumbleupon a{background-position:-2400px 0;}.share-help ul li.twitter a{background-position:-300px 0;}.share-help ul li.mixx a{background-position:-2900px 0;}.share-help ul li.google a{background-position:-3150px 0;}.story .share-help{position:relative;width:240px;float:right;margin:8px -160px -2px 0;padding:0;z-index:1;}#main-content .layout-block-a.expanded .share-help,.story-wide .share-help,.storybody-halfwide-include .share-help{margin-right:0;}.ie .story .story-body .share-help{width:245px!important;}.bbccom_slot_xxl .story .share-help{margin:8px 0 0 0;float:right;width:120px;}.story .share-help h3{position:absolute;left:-5000px;top:-5000px;}.story .share-help ul{float:right;}.story .share-help ul li{background:none;}.story .share-body-bottom .share-help{position:relative;clear:both;float:left;margin:0 0 16px;width:100%;}.story .share-body-bottom .share-help h3{position:relative;top:0;left:0;padding:0 0 8px 0;margin:0 0 16px 0;border-bottom:1px solid #ccc;}.story .share-body-bottom .share-help ul{float:left;height:20px;}.blq-js .story .share-body-bottom .share-help ul{width:102px!important;}.blq-js .story .share-body-bottom .share-help ul li{position:absolute;margin:0;}.blq-js .story .share-body-bottom .share-help ul li.email{top:0;left:0;}.blq-js .story .share-body-bottom .share-help ul li.print{position:absolute;top:0;left:23px;}.blq-js .story .share-body-bottom .share-help ul li.facebook-popup{position:absolute;top:0;left:92px;}.blq-js .media-asset .share-help ul li.facebook-popup,.blq-js .photo-gallery .share-help ul li.facebook-popup{position:absolute;top:0;left:67px;}.blq-js .story .share-body-bottom .share-help ul li.twitter{position:absolute;top:0;left:69px;}.blq-js .story .share-body-bottom .share-help ul li.share{position:absolute;top:0;left:46px;}.blq-js .story .share-body-bottom .share-help ul li.facebook{position:absolute;top:0;left:92px;width:160px;}.story .share-body-bottom .share-help ul li.facebook-context{position:absolute;left:94px;}.story .share-body-bottom .share-help iframe{font-family:Helvetica;left:0;overflow:hidden;}.media-asset .share-help,.photo-gallery .share-help{clear:both;position:relative;float:left!important;width:100%!important;padding:8px 0 0 0;height:76px;margin:0 0 16px 0;}.media-asset .share-help h3,.photo-gallery .share-help h3{padding:0 0 8px 0;margin:0 0 14px 0;border-bottom:1px solid #ccc;}.blq-js .media-asset .share-help ul,.blq-js .photo-gallery .share-help ul{width:102px!important;}.media-asset .share-help ul,.photo-gallery .share-help ul{float:left;}.blq-js .media-asset .share-help ul li,.blq-js .photo-gallery .share-help ul li{position:absolute;}.media-asset .share-help ul li.email,.photo-gallery .share-help ul li.email{top:0;left:-2px;}.media-asset .share-help ul li.print,.photo-gallery .share-help ul li.print{position:absolute;top:0;left:21px;display:none;}.blq-js .media-asset .share-help ul li.twitter,.blq-js .photo-gallery .share-help ul li.twitter{position:absolute;top:0;left:44px;}.media-asset .share-help ul li.share,.photo-gallery .share-help ul li.share{position:absolute;top:0;left:21px;}.media-asset .share-help ul li.facebook-context,.photo-gallery .share-help ul li.facebook-context{left:68px;}.media-asset .share-help iframe,.photo-gallery .share-help iframe{font-family:Helvetica;left:0!important;overflow:hidden;}.container-travel-best{background-color:#EDEDED;padding:0 8px;clear:both;position:relative;margin-bottom:16px;overflow:auto;}.ie .container-travel-best{height:1%;}.container-travel-best .heading-24{font-size:24px;font-weight:bold;letter-spacing:-1px;margin:0;padding:8px 0 8px 0;text-rendering:optimizelegibility;line-height:24px;}.container-travel-best h2 a{line-height:24px;}.container-travel-best .useful-links{border-top:none;margin:0;}.container-travel-best .useful-links .useful-links-header{border-bottom:medium none;display:block;font-size:15px;font-weight:bold;letter-spacing:normal;line-height:16px;padding:8px 0 0 0;position:relative;text-rendering:optimizelegibility;}.container-travel-best .useful-links ul{overflow:hidden;padding:0;position:relative;width:304px;}.container-travel-best .useful-links .column-1,.container-travel-best .useful-links .column-2{clear:both;display:block;float:none;margin-left:0;overflow:visible;position:relative;width:100%;padding-bottom:0;}.container-travel-best .useful-links ul a{padding-right:0;font-weight:normal;}.correspondent-byline{position:relative;display:block;clear:both;overflow:hidden;width:624px;top:-5px;margin:0 -160px 12px 0;background:#333;}.bbccom_slot_xxl .correspondent-byline{margin-right:0;width:464px;}.correspondent-byline-inner{position:relative;overflow:visible;border-top:16px solid white;}.correspondent-byline *{color:#ccc;}.correspondent-byline .name,.correspondent-byline a{font-weight:bold;color:white;}.correspondent-byline .byline-lead-in{position:absolute;left:-5000%;}.correspondent-byline .correspondent-portrait{position:relative;float:right;display:inline;z-index:10;}.correspondent-byline .correspondent-portrait img{position:relative;display:block;height:104px;width:144px;margin-top:-16px;}.correspondent-byline .name{float:left;display:inline;font-size:24px;line-height:28px;padding:10px 8px 2px;letter-spacing:-1px;}.correspondent-byline .bbc-role{display:block;clear:left;position:relative;top:1px;font-size:16px;line-height:20px;padding:0 8px;color:#D2700E;}.correspondent-byline ul.social-links{margin:-2px 0 0;position:relative;overflow:hidden;padding:0;}.correspondent-byline ul.social-links li{position:relative;float:left;display:inline;margin:5px 0 0;padding:0 8px;border-left:1px solid #505050;background:none;line-height:16px;}.correspondent-byline ul.social-links li:first-child{border:none;}.correspondent-byline ul.social-links li a{font-size:13px;}.story-feature .correspondent-byline{margin:0 0 11px;top:0;padding:0 0 3px 118px;width:auto;background:transparent;border-bottom:1px solid #D8D8D8;}.story-feature .correspondent-byline .byline-picture{position:relative;float:left;display:inline;margin:0 0 0 -118px;}.story-feature .correspondent-byline .byline-picture img{margin:0!important;width:112px;height:63px;}.story-feature .correspondent-byline .byline-heading{position:relative;color:#505050;font-weight:bold;font-size:24px;letter-spacing:-1px;top:1px;line-height:28px;padding:0 8px 0 0;margin:0;}.story-feature .correspondent-byline .byline-name{display:block;color:#505050;font-size:16px;line-height:20px;margin-bottom:1px;}.story-feature .correspondent-byline .byline-title{position:relative;top:-1px;display:block;padding:0 8px 0 0;color:#D2700E;font-size:16px;line-height:20px;}.correspondent-facts{position:relative;display:block;clear:both;margin:0 0 16px;background:#ededed;}.correspondent-facts .correspondent-facts-header{font-size:24px;font-weight:bold;letter-spacing:-1px;line-height:24px;text-rendering:optimizelegibility;padding:10px 8px 14px;}.correspondent-facts p{padding:0 8px 8px;font-size:15px;line-height:20px;}.correspondent-facts p *{font-size:inherit;line-height:inherit;}.correspondent-facts h3{padding:4px 0 0;font-size:16px;color:#80989a;}.correspondents-more{position:relative;display:block;clear:both;margin:0 0 16px;background:#ededed;}.correspondents-more .correspondents-more-header{font-size:24px;font-weight:bold;letter-spacing:-1px;line-height:24px;text-rendering:optimizelegibility;padding:10px 8px;}.correspondents-more ul{padding:0 8px;}.correspondents-more ul li{position:relative;overflow:hidden;padding:5px 0 9px 120px;}.correspondents-more ul li .name{display:block;font-size:16px;color:inherit;}.correspondents-more ul li .bbc-role{padding-top:2px;display:block;font-weight:normal;color:#D2700E;}.correspondents-more ul li img{position:relative;float:left;display:inline;margin:2px 0 0 -120px;width:112px;height:63px;}.correspondents-more hr{display:none;}.ie .correspondents-more hr{display:block;}.correspondent-promo{position:relative;clear:both;display:block;overflow:hidden;padding-left:160px;}.index #now .correspondent-promo{width:464px;margin:0 0 16px 0;}.index #best .correspondent-promo{width:176px;margin:0 0 16px 0;}.story .layout-block-a .correspondent-promo{width:304px;margin:2px 0 16px 0;}.correspondent-promo-inner{position:relative;overflow:visible;padding-top:24px;}.correspondent-promo-inner a:after{content:"\00a0";float:left;position:relative;margin-left:-5000%;}.correspondent-promo .name{font-weight:bold;}.correspondent-promo .promo-lead-in{position:absolute;left:-5000%;}.correspondent-promo .correspondent-portrait{position:relative;margin-top:-24px;margin-left:-160px;float:left;display:inline;overflow:hidden;}.correspondent-promo .correspondent-portrait img{position:relative;display:block;margin-bottom:-1px;width:144px;height:104px;}.correspondent-promo .name{display:block;color:inherit;font-size:24px;line-height:28px;padding:2px 8px 0 0;letter-spacing:-1px;}.correspondent-promo .bbc-role{display:block;position:relative;top:-1px;font-size:16px;line-height:20px;padding:0 8px 0 0;color:#D2700E;}.correspondent-promo ul.social-links{margin:0;overflow:hidden;padding:2px 0 8px;}.correspondent-promo ul.social-links li{position:relative;float:left;display:inline;margin:0;line-height:16px;padding:0 8px;border-left:1px solid #ccc;background:none;}.correspondent-promo ul.social-links li:first-child{border:none;padding-left:0;}.index #best .correspondent-promo ul.social-links li{clear:both;padding-left:0;border:none;}.correspondent-promo ul.social-links li a{font-size:13px;}.correspondent-promo .correspondent-promo-item{position:relative;clear:both;margin:0 0 0 -160px;background:#ededed;}.correspondent-promo .correspondent-promo-item .article{padding:8px;}.correspondent-promo .correspondent-promo-item .article+.article{border-top:8px solid white;}.correspondent-promo .correspondent-promo-item .blog h2{padding:2px 0 6px;font-size:16px;}.correspondent-promo .correspondent-promo-item .blog .article-date{color:#888;}.correspondent-promo .correspondent-promo-item .blog p{padding:0 0 4px;}.correspondent-promo .correspondent-promo-item .twitter h2{padding:1px 0 3px;font-size:16px;line-height:20px;font-weight:normal;}.correspondent-promo .correspondent-promo-item .twitter p{padding:0 0 4px;font-size:18px;line-height:20px;}.container-country-profile{clear:both;position:relative;overflow:hidden;width:608px;margin:0 0 16px;padding:8px;background:#EDEDED;}.container-country-profile .container-country-profile-header{font-size:24px;font-size:1.846em;font-weight:bold;letter-spacing:-0.05em;line-height:24px;text-indent:-2px;padding:0;}.container-country-profile .tab{padding:8px 16px 9px 0;position:relative;background:transparent;z-index:100;font-size:1.231em;font-weight:bold;line-height:23px;}.container-country-profile .panel{position:relative;overflow:hidden;background:#fff;width:608px;}.container-country-profile .panel h3.more{position:absolute;left:-10000px;top:auto;width:1px;height:1px;overflow:hidden;}.container-country-profile .panel ul.column{width:152px;float:left;padding:4px 0 10px;}.container-country-profile .panel li{display:block;padding:8px 8px 2px 8px;}.ie .container-country-profile .panel li a{padding-bottom:4px;height:1%;}.container-country-profile .panel li.first-child{border-top:none;}.blq-js .container-country-profile-tabbed .container-country-profile-header{padding-bottom:8px;}.blq-js .container-country-profile-tabbed .tab{float:left;display:inline;top:0;padding-left:8px;cursor:pointer;}.blq-js .container-country-profile-tabbed void{background:#fff;}.blq-js .container-country-profile-tabbed h3.tab a{color:#505050;}.blq-js .container-country-profile-tabbed void a{color:#D1700E;}.blq-js .container-country-profile-tabbed .panel{position:absolute;float:right;display:inline;clear:right;right:-500%;margin-right:0;margin-top:40px;opacity:0;}.blq-js .ie .container-country-profile-tabbed .panel,.blq-js .ie7 .container-country-profile-tabbed .panel{clear:none;}.blq-js .container-country-profile-tabbed void{position:relative;margin-right:-608px;right:608px;opacity:1;-webkit-transition:opacity .2s ease-in;}.blq-js .ie .container-country-profile-tabbed void,.blq-js .ie7 .container-country-profile-tabbed void{margin-top:32px;}.container-digest-grid{position:relative;display:block;clear:both;overflow:hidden;margin:0;min-height:16px;padding-top:4px;}.container-digest-grid .heading-24{font-size:1.846em;font-weight:bold;letter-spacing:-1px;line-height:24px;text-rendering:optimizelegibility;padding:2px 0 9px;}.digest-grid{position:relative;display:block;clear:both;overflow:hidden;margin:0 0 20px;min-height:16px;border-top:1px solid #DDD;padding-top:4px;}.digest-grid .digest-unit{position:relative;float:left;display:inline;width:144px;margin-left:16px;}.digest-grid .digest-unit .heading-16{display:block;font-size:1.231em;font-weight:bold;line-height:20px;padding:4px 0 8px;position:relative;text-rendering:optimizelegibility;}.digest-grid .digest-unit .heading-16 a{line-height:20px;}.digest-grid .first-child{margin-left:0;}.digest-grid .digest-unit img{display:block;margin-bottom:6px;}.digest-grid li{position:relative;padding-top:8px;}.digest-grid li.standard-no-image{padding-top:0;}.digest-grid .stacked-144{position:relative;padding-top:0;}.digest-grid .standard-no-image a:hover .headline,.digest-grid .standard-no-image a:focus .headline,.digest-grid .stacked-144 a:hover,.digest-grid .stacked-144 a:focus{text-decoration:none;}.digest-grid .standard-no-image a:hover .headline,.digest-grid .standard-no-image a:focus .headline,.digest-grid .stacked-144 a:hover .headline,.digest-grid .stacked-144 a:focus .headline{text-decoration:underline;}.digest-grid .standard-no-image a .headline,.digest-grid .stacked-144 a .headline{color:#1F4F82;}.digest-grid .standard-no-image a:visited .headline,.digest-grid .stacked-144 a:visited .headline{color:#4A7194;}.digest-grid .standard-no-image a:active .headline,.digest-grid .stacked-144 a:active .headline{color:#D1700E;}.digest-grid .standard-no-image{position:relative;}.digest-grid .standard-no-image .gvl3-icon,.digest-grid .gvl3-icon-wrapper{position:absolute;top:0;left:0;}.digest-grid .gvl3-icon-wrapper .gvl3-icon{position:relative;top:0;}.digest-grid .gvl3-icon{position:absolute;top:7px;left:0;}.digest-grid .gvl3-icon-live{top:8px;}.digest-grid-text-only{position:relative;display:block;clear:both;overflow:hidden;margin:0 0 20px;min-height:16px;border-top:1px solid #DDD;padding-top:4px;}.digest-grid-text-only+.digest-grid-text-only{margin-top:-16px;border-top:none;padding-top:5px;}.digest-grid-text-only .digest-unit{position:relative;float:left;display:inline;width:144px;margin-left:16px;}.digest-grid-text-only .digest-unit h3{display:block;padding:7px 0 0;position:relative;}.digest-grid-text-only .digest-unit .heading-11{font-size:.846em;font-weight:normal;line-height:16px;text-transform:uppercase;text-rendering:optimizelegibility;color:#505050;}.digest-grid-text-only .heading-11:active{color:#D1700E;}.digest-grid-text-only .first-child{margin-left:0;}.digest-grid-text-only .standard-no-image{position:relative;}.digest-grid-text-only .gvl3-icon{position:absolute;top:-1px;left:0;}.digest-grid-text-only .gvl3-icon-live{top:0;}.container-digest-grid .include-wrapper{position:relative;overflow:hidden;clear:both;margin:0 0 8px 0;width:100%;}.container-digest-grid .include-wrapper .simple-promo h3,.container-digest-grid .include-wrapper .simple-promo p{display:inline;}.container-digest-grid .include-wrapper .geo-digest-region{width:304px;padding-top:0;margin-bottom:0;}.container-digest-grid #personalisation{width:304px;padding-top:4px;border-bottom:none;}.container-digest-grid #personalisation .locator-forms{width:608px;}.container-digest-grid #personalisation .options{top:8px;}.include-wrapper+.digest-wrapper{border-top:none;}.container-digest-grid .geo-digest-region #personalisation .column-1{width:296px;}.container-digest-grid .geo-digest-region #personalisation .column-2{width:296px;margin-left:6px;}.container-digest-grid .container-block-grid{display:block;overflow:auto;clear:both;position:relative;}.digest-grid-list-column{position:relative;float:left;display:inline;width:304px;padding-bottom:9px;}.digest-grid-list-column+.digest-grid-list-column{padding-left:16px;}.digest-grid-list-column .digest-unit{position:relative;padding-top:5px;padding-bottom:16px;}.digest-grid-list-column .heading-16{font-size:1.231em;font-weight:bold;line-height:20px;text-rendering:optimizelegibility;}.digest-grid-list-column .standard-no-image{border-top:1px solid #ddd;padding-top:6px;position:relative;}.digest-grid-list-column .standard-no-image .gvl3-icon{position:absolute;top:5px;left:0;}.digest-grid .digest-unit .heading-16 .or-text{left:-9000px;position:absolute;}.digest-grid .digest-unit .heading-16 .vertical-line{border-left:2px solid #505050;font-size:.76em;margin-left:5px;margin-right:5px;}.ie .container-digest-grid .digest-grid-text-only,.ie .container-digest-grid .digest-grid .standard-no-image,.ie .container-digest-grid .digest-grid-text-only .standard-no-image,.ie7 .container-digest-grid .digest-grid-text-only,.ie7 .container-digest-grid .digest-grid .standard-no-image,.ie7 .container-digest-grid .digest-grid-text-only .standard-no-image{height:1%;}.ie .container-digest-grid .digest-grid,.ie .container-digest-grid .digest-grid .digest-unit,.ie7 .container-digest-grid .digest-grid,.ie7 .container-digest-grid .digest-grid .digest-unit{height:1%;}.ie .container-digest-grid .digest-grid .digest-unit ul,.ie .container-digest-grid .digest-grid .digest-unit li,.ie7 .container-digest-grid .digest-grid .digest-unit ul,.ie7 .container-digest-grid .digest-grid .digest-unit li{height:1%;}.ie .digest-grid-list-column .standard-no-image{height:1%;}.container-single-section-digest{position:relative;clear:both;}.container-single-section-digest .container-single-section-digest-heading{font-size:1.846em;font-weight:bold;letter-spacing:-1px;line-height:24px;text-rendering:optimizelegibility;padding:2px 0 9px;}.container-single-section-digest .digest-grid-text-only{border-top:none;}#ent-widget{padding:0;margin:0;height:303px;width:606px;background-color:#EDEDED;color:#505050;font-family:Verdana,Arial,sans-serif;font-size:13px;}#ent-widget ul{margin:0;padding:0;padding-left:9px;list-style-type:none;}#ent-widget li{margin:0;padding:0;list-style-type:none;}#ent-widget h2,#ent-widget h3,#ent-widget h4{padding:0;margin:0;font-weight:bold;}#ent-widget h2{padding-top:9px;padding-left:9px;font-size:21px;color:#505050;margin-bottom:10px;}#ent-widget h3{font-size:13px;color:#505050;}#ent-widget p{padding:0;margin:0;margin-bottom:10px;}#ent-widget a{color:#194E80;text-decoration:none;}#ent-widget a:hover{text-decoration:underline;}#ent-widget .column-1{float:left;clear:left;overflow:visible;position:relative;width:300px;}#ent-widget .column-2{float:left;margin-left:10px;clear:none;position:relative;width:280px;}#ent-widget .column-1 h3{font-size:15px;font-weight:bold;}#ent-widget .column-1 a{font-size:17px;font-weight:bold;}#ent-widget .column-2 p{font-weight:bold;}.container-expert-views{width:624px;padding-top:0;}.container-expert-views h2{margin-top:0;border-top:1px solid #DDD;}.digest-wrapper-header{margin-top:0;padding-top:3px;}.container-expert-views{margin-top:0;overflow:hidden;}.container-expert-views .digest{float:left;display:inline;width:304px;margin-left:16px;}.container-expert-views .new-row{margin-left:0;clear:both;}.container-expert-views .digest div.first-child{padding-left:0;}.container-expert-views .digest ul{width:304px;padding-left:0;margin-bottom:7px;}.container-expert-views .digest .medium-image{width:144px;padding-bottom:14px;margin-top:4px;margin-left:0;}.container-expert-views .digest .medium-image .digest-story-header{padding-bottom:4px;}.container-expert-views .digest .medium-image img{margin-bottom:-3px;top:3px;}.container-expert-views .digest .medium-image .gvl3-icon-wrapper{top:3px;}.expert-odd .first-child .first-child .gvl3-icon-wrapper{top:3px;}.container-expert-views .digest .no-image{width:304px;}.container-expert-views .digest .no-image .digest-story-header{padding-bottom:4px;}.container-expert-views .digest .column-1{width:304px;margin-left:0;}.container-expert-views .digest .column-2{position:relative;float:left;clear:none;display:inline;overflow:hidden;width:304px;margin-left:0;}.expert-odd div.first-child{width:624px;padding-left:0;margin-left:0;}.expert-odd .digest{margin-bottom:0;}div.expert-odd .digest ul{margin-bottom:1px;}.expert-odd .digest li{clear:both;}.expert-odd div.first-child ul{position:relative;overflow:auto;width:304px;margin:1px 0 9px;padding-left:320px;}.expert-odd div.first-child li{padding-bottom:8px;}.expert-odd div.first-child .medium-image{position:relative;clear:both;overflow:hidden;width:464px;margin-left:-320px;margin-top:4px;padding-bottom:15px;padding-left:160px;}.expert-odd div.first-child .no-image{padding-left:-320px;}.expert-odd div.first-child .medium-image .digest-story-header{position:relative;padding-bottom:4px;}.expert-odd div.first-child .medium-image img{position:relative;float:left;display:inline;margin-left:-160px;width:144px;height:81px;top:3px;}.expert-odd div.first-child .column-1{position:relative;float:left;clear:left;display:inline;overflow:hidden;width:304px;margin-left:-320px;}.expert-odd div.first-child .column-2{clear:none;position:relative;width:304px;}.container-expert-views .digest-wrapper-header{margin-bottom:-1px;}.digest-wrapper-header a{line-height:24px;}.container-expert-views .digest .column-1,.container-expert-views .digest .column-2{display:block;float:none;}.expert-odd .digest .no-image{width:304px;padding-right:0;margin-left:0;padding-bottom:6px;}.expert-odd .digest .medium-image{padding-bottom:14px;}.expert-even div.digest .no-image{padding-bottom:6px;margin-top:4px;}.expert-odd div.first-child .no-image{margin-top:4px;padding-right:0;width:342px;margin-left:-320px;}.container-expert-views .digest ul{margin-bottom:16px;}.ie .container-expert-views .medium-image,.ie7 .container-expert-views .medium-image{padding-bottom:0!important;}.ie .container-expert-views .digest .column-1,.ie .container-expert-views .digest .column-2{position:relative;float:left!important;}.ie7 .container-expert-views .no-image p{margin-bottom:-4px;}.ie .container-expert-views .no-image,.ie7 .container-expert-views .no-image{padding-bottom:0;}.ie .container-expert-views div.digest ul,.ie7 .container-expert-views div.digest ul{padding-bottom:0;margin-bottom:0;}.ie .container-expert-views.expert-even .digest{height:1%!important;}div.expert-even div.digest ul{margin-bottom:1px;}div.expert-even hr.breaker{margin-bottom:8px;}.ie div.expert-even hr.breaker,.ie7 div.expert-even hr.breaker{margin-bottom:0;}.feature-digest{position:relative;overflow:visible;width:624px;margin:0;padding-top:3px;}.feature-digest .feature-digest-header{position:relative;margin-bottom:3px;padding-top:9px;padding-bottom:11px;line-height:16px;}.feature-digest ul{position:relative;overflow:hidden;}.feature-digest li.new-row,.feature-digest li.first-child{margin-left:0;}.feature-digest li{position:relative;width:144px;margin-left:16px;border-top:1px solid #D8D8D8;padding:8px 0 16px 0;}.feature-digest li.new-row{clear:both;}.feature-digest .medium-image img{position:relative;float:left;width:144px;height:81px;margin-bottom:8px;}.feature-digest .medium-image .gvl3-icon-wrapper{top:8px;left:0;}.feature-digest li h3{padding-bottom:4px;}.feature-digest .feature-stories li{display:-moz-inline-box;-moz-box-orient:vertical;display:inline-block;vertical-align:top;word-wrap:break-word;}* html .feature-digest .feature-stories li{display:inline;}*+html .feature-digest .feature-stories li{display:inline;}.feature-digest .feature-stories li>*{display:table;table-layout:fixed;overflow:hidden;}* html .feature-digest .story-list li{width:144px;}.feature-digest .story-list li>*{width:144px;}.feature-digest .story-list{width:624px;}.feature-digest .no-image .gvl3-icon{position:absolute;top:8px;left:0;}.generic-tiled-digest{position:relative;overflow:visible;width:624px;margin:0;padding-top:3px;}.generic-tiled-digest .generic-tiled-digest-header{position:relative;margin-bottom:3px;padding-top:9px;padding-bottom:11px;line-height:16px;}.generic-tiled-digest ul{position:relative;overflow:hidden;}.generic-tiled-digest li.new-row,.generic-tiled-digest li.first-child{margin-left:0;}.generic-tiled-digest li{position:relative;width:144px;margin-left:16px;border-top:1px solid #D8D8D8;padding:8px 0 16px 0;}.generic-tiled-digest li.new-row{clear:both;}.generic-tiled-digest .medium-image img{position:relative;float:left;width:144px;height:81px;margin-bottom:8px;}.generic-tiled-digest .medium-image .gvl3-icon-wrapper{top:8px;left:0;}.generic-tiled-digest li h3{padding-bottom:4px;}.generic-tiled-digest .digest-stories li{display:-moz-inline-box;-moz-box-orient:vertical;display:inline-block;vertical-align:top;word-wrap:break-word;}* html .generic-tiled-digest .digest-stories li{display:inline;}*+html .generic-tiled-digest .digest-stories li{display:inline;}.generic-tiled-digest .digest-stories li>*{display:table;table-layout:fixed;overflow:hidden;}* html .generic-tiled-digest .story-list li{width:144px;}.generic-tiled-digest .story-list li>*{width:144px;}.generic-tiled-digest .story-list{width:624px;}.generic-tiled-digest .no-image .gvl3-icon{position:absolute;top:8px;left:0;}.featured-site-top-stories{position:relative;clear:both;overflow:visible;width:624px;margin:0 0 13px;border-top:1px solid #ddd;}.featured-site-top-stories+script+.featured-site-include{margin-top:-17px;}.ie7 .featured-site-top-stories+.featured-site-include{margin-top:-17px;}.featured-site-top-stories h2{position:relative;padding:7px 0 8px;margin-bottom:7px;border-bottom:1px solid #ddd;}.ie7 .featured-site-top-stories h2,.ie .featured-site-top-stories h2{position:relative;padding-top:7px;}.featured-site-top-stories ul{position:relative;overflow:auto;width:304px;margin:10px 0 4px;padding-left:320px;}.featured-site-top-stories li{padding-bottom:7px;padding-top:1px;}.featured-site-top-stories .with-summary{position:relative;clear:both;overflow:hidden;width:512px;margin:-3px 0 10px -320px;padding-right:112px;padding-bottom:14px;border-bottom:1px solid #ddd;}.featured-site-top-stories .with-summary h3{position:relative;margin-top:2px;margin-bottom:4px;}.featured-site-top-stories .medium-image{position:relative;clear:both;overflow:hidden;width:464px;margin:-1px 0 10px -320px;padding-bottom:9px;padding-left:160px;border-bottom:1px solid #ddd;}.featured-site-top-stories .medium-image h3{position:relative;margin-bottom:4px;}.featured-site-top-stories .medium-image img{float:left;display:inline;margin-left:-160px;width:144px;height:81px;}.featured-site-top-stories .large-image{position:relative;clear:both;overflow:hidden;width:304px;margin:-3px 0 10px -320px;padding-bottom:7px;padding-left:320px;border-bottom:1px solid #ddd;}.featured-site-top-stories .large-image h3{position:relative;margin-bottom:4px;padding-top:2px;}.featured-site-top-stories .large-image img{float:left;display:inline;margin-left:-320px;width:304px;height:171px;}.featured-site-top-stories .classic-image{position:relative;clear:both;overflow:hidden;width:382px;margin:-1px 0 10px -320px;padding-bottom:8px;padding-left:242px;border-bottom:1px solid #ddd;}.featured-site-top-stories .classic-image h3{position:relative;margin-bottom:8px;}.featured-site-top-stories .classic-image img{float:left;display:inline;margin-left:-242px;width:226px;height:170px;}.featured-site-top-stories .column-1{position:relative;float:left;clear:left;display:inline;overflow:hidden;width:304px;margin-left:-320px;}.featured-site-top-stories .column-2{clear:none;position:relative;width:304px;}.ie .featured-site-top-stories .column-2{float:right;}.featured-site-top-stories .see-also{width:100%;margin-top:8px;margin-bottom:2px;padding:0;overflow:hidden;}.featured-site-top-stories .medium-image .see-also{margin-bottom:0;}.featured-site-top-stories .see-also li{padding-top:0;padding-bottom:8px;position:relative;float:left;display:inline;width:100%;}.featured-site-top-stories .see-also .story{font-size:1em;}.featured-site-top-stories .see-also .play-audio{text-indent:20px;}.featured-site-top-stories .see-also .play-video{text-indent:16px;}.featured-site-top-stories .gvl3-icon{position:absolute;top:0;left:0;}.featured-site-top-stories .with-summary gvl3-icon{top:-2px;}.featured-site-top-stories .gvl3-icon-wrapper{position:absolute;top:0;left:-320px;}.featured-site-top-stories .medium-image .gvl3-icon-wrapper{left:-160px;}.featured-site-top-stories .classic-image .gvl3-icon-wrapper{left:-242px;}.featured-site-top-stories .large-image .gvl3-icon-wrapper{top:3px;}.ie7 .featured-site-top-stories .gvl3-icon-wrapper,.ie .featured-site-top-stories .gvl3-icon-wrapper{top:4px;}.featured-site-top-stories .gvl3-icon-wrapper .gvl3-icon{position:relative;top:auto;left:auto;}.ie7 .featured-site-top-stories li.first-child hr,.ie .featured-site-top-stories li.first-child hr{visibility:hidden;}.container-featured-other-site{position:relative;clear:both;}.container-featured-other-site-heading{border-bottom:1px solid #DDD;border-top:1px solid #DDD;margin-bottom:-1px;padding:7px 0 8px;position:relative;}.feature-generic,.container-features-and-analysis{position:relative;overflow:visible;width:336px;padding-bottom:8px;margin:0 0 16px;background:#ededed;}.ie .feature-generic,.ie .container-features-and-analysis{padding-bottom:2px;}.bbccom_slot_xxl .feature-generic,.bbccom_slot_xxl .container-features-and-analysis{width:496px;}.feature-generic .features-header,.container-features-and-analysis .features-header{padding:8px;}.feature-generic ul,.container-features-and-analysis ul{position:relative;overflow:hidden;padding:0 8px 0;clear:both;}.feature-generic li,.container-features-and-analysis li{position:relative;display:block;clear:both;overflow:hidden;zoom:1;}.feature-generic li .gvl3-icon,.container-features-and-analysis li .gvl3-icon{position:absolute;top:9px;left:0;}.feature-generic li.large-image .gvl3-icon-wrapper,.container-features-and-analysis li.large-image .gvl3-icon-wrapper{position:absolute;top:0;left:0;}.feature-generic li.medium-image .gvl3-icon-wrapper,.container-features-and-analysis li.medium-image .gvl3-icon-wrapper{position:absolute;top:8px;left:-120px;}.ie .feature-generic li.medium-image .gvl3-icon-wrapper,.ie .container-features-and-analysis li.medium-image .gvl3-icon-wrapper,.ie7 .feature-generic li.medium-image .gvl3-icon-wrapper,.ie7 .container-features-and-analysis li.medium-image .gvl3-icon-wrapper{top:10px;}.feature-generic li .gvl3-icon-wrapper .gvl3-icon,.container-features-and-analysis li .gvl3-icon-wrapper .gvl3-icon{position:relative;top:0;left:0;}.feature-generic li.no-image .gvl3-icon,.container-features-and-analysis li.no-image .gvl3-icon{top:6px;}.feature-generic li.large-image,.container-features-and-analysis li.large-image{width:320px;margin:0 -8px 0;padding:8px 8px 12px;border-bottom:1px solid #fff;background:#505050;}.ie .feature-generic li.large-image,.ie .container-features-and-analysis li.large-image{display:inline;}.feature-generic li.large-image .feature-header,.container-features-and-analysis li.large-image .feature-header{margin-bottom:3px;overflow:visible;}.feature-generic li.large-image img,.container-features-and-analysis li.large-image img{position:relative;display:block;margin:-8px -8px 8px;width:336px;height:auto;border-bottom:1px solid #fff;}.feature-generic li.large-image p,.container-features-and-analysis li.large-image p{padding-top:2px;}.feature-generic li.large-image *,.container-features-and-analysis li.large-image *{color:#fff;}.feature-generic li.medium-image,.container-features-and-analysis li.medium-image{padding:0 0 0 120px;position:relative;margin-bottom:-2px;}.feature-generic li.medium-image:first-child,.container-features-and-analysis li.medium-image:first-child{margin-top:-8px;}.ie7 .feature-generic li.medium-image,.ie7 .container-features-and-analysis li.medium-image,.ie .feature-generic li.medium-image,.ie .container-features-and-analysis li.medium-image{margin-bottom:-14px;}.feature-generic li.medium-image .feature-header,.container-features-and-analysis li.medium-image .feature-header{position:relative;padding-top:7px;margin-bottom:-1px;}.feature-generic li.medium-image img,.container-features-and-analysis li.medium-image img{position:relative;float:left;display:inline;margin-top:1px;margin-left:-120px;width:112px;height:63px;}.feature-generic li.medium-image p,.container-features-and-analysis li.medium-image p{padding-bottom:4px;padding-top:6px;}.feature-generic li.no-image,.container-features-and-analysis li.no-image{padding:7px 0 4px;}.feature-generic li.no-image .feature-header,.container-features-and-analysis li.no-image .feature-header{margin-bottom:-1px;}.feature-generic li.no-image p,.container-features-and-analysis li.no-image p{padding:6px 0 0;}.feature-generic li.first-child,.container-features-and-analysis li.first-child{background:#d2700f;}.feature-generic li.first-child *,.container-features-and-analysis li.first-child *,.container-features-and-analysis li.first-child a:visited{color:#fff;}.feature-generic li.large-image a.from-external-source:visited,.container-features-and-analysis li.large-image a.from-external-source:visited{color:#fff;}.feature-generic .solo-other-label a.from-external-source,.container-features-and-analysis .solo-other-label a.from-external-source{margin:6px 0 0 0;display:block;position:relative;}.digest{position:relative;overflow:visible;width:624px;margin:0;padding-top:3px;border-top:1px solid #ddd;}.digest .digest-header{position:relative;margin-bottom:3px;padding-top:9px;padding-bottom:11px;line-height:16px;border-bottom:1px solid #ddd;}.ie .digest-multiple .first-child .digest-header,.ie .digest .digest-header{height:1%;}.digest ul{position:relative;overflow:hidden;width:304px;margin:1px 0 0;padding-left:320px;}.digest li{padding-bottom:8px;}.digest-world-service .digest li.first-child{padding-top:8px;}.digest .gvl3-icon{position:absolute;top:-1px;left:0;}.digest .no-image{position:relative;clear:both;overflow:hidden;width:464px;margin-left:-320px;padding-bottom:14px;margin-top:8px;padding-right:160px;}.digest .medium-image{position:relative;clear:both;overflow:hidden;width:464px;margin-left:-320px;padding-bottom:15px;padding-left:160px;margin-top:4px;}.digest .medium-image .has-icon-listen{text-indent:0;}.digest .medium-image .has-icon-watch{text-indent:0;}.digest .medium-image .gvl3-icon-wrapper{position:absolute;left:-160px;top:3px;}.digest .medium-image .gvl3-icon{position:relative;top:auto;left:auto;}.digest .no-image{padding-left:0;}.digest .medium-image .digest-story-header{position:relative;padding-bottom:4px;}.digest .medium-image img{position:relative;float:left;display:inline;margin-left:-160px;margin-bottom:-3px;width:144px;height:81px;top:3px;}.digest .column-1{position:relative;float:left;clear:left;display:inline;overflow:hidden;width:304px;margin-left:-320px;}.digest .column-2{clear:none;position:relative;width:304px;}.ie .digest .column-2{float:right;}.digest h3.rtl,.digest .rtl *{direction:rtl;text-align:right;}.ie .digest-double .rtl .medium-image a,.ie .expert-even .rtl .medium-image a{margin-left:0;position:relative;}.ie7 .rtl li a{position:relative;right:-15px;}.ie .digest-double .rtl .medium-image a .link-text,.ie7 .digest-double .rtl .medium-image a .link-text,.ie .expert-even .rtl .medium-image a .link-text,.ie7 .expert-even .rtl .medium-image a .link-text{display:block;width:100%;cursor:pointer;color:inherit;}.ie7 .digest-double .rtl .medium-image img,.ie7 .expert-even .rtl .medium-image img{left:-16px;}.ie .digest-double .rtl .medium-image img,.ie .expert-even .rtl .medium-image img{position:absolute;right:160px;float:none;}.ie .digest-multiple .rtl .medium-image a,.ie7 .digest-multiple .rtl .medium-image a{height:1%;}.digest .rtl .gvl3-icon{left:auto;right:-8px;}.ie .digest-double .rtl .gvl3-icon-wrapper,.ie .expert-even .rtl .gvl3-icon-wrapper{left:-304px;}.digest .rtl .gvl3-icon-wrapper .gvl3-icon{right:auto;}.container-expert-views{border-top:none;position:relative;clear:both;margin-bottom:11px;}.container-category-digests,.container-section-digests{position:relative;clear:both;overflow:hidden;margin-bottom:8px;}.digest-wrapper-header{position:relative;padding-bottom:8px;padding-top:7px;margin-top:-1px;margin-bottom:-1px;border-bottom:1px solid #ddd;}.container-expert-views .digest{margin-top:0;}.digest-double .digest,.expert-even .digest{float:left;display:inline;width:304px;margin-left:16px;}.digest-double div.first-child,.expert-even div.first-child{margin-left:0;}.digest-double .digest ul,.expert-even .digest ul{width:304px;padding-left:0;margin-bottom:4px;}.digest-double .no-image,.expert-even .no-image{position:relative;clear:both;overflow:hidden;width:100%;margin-left:0;padding-bottom:14px;margin-top:0;padding-right:0;}.digest-double .medium-image,.expert-even .medium-image{width:144px;padding-bottom:14px;margin-left:0;}.ie .digest-double .digest,.ie .expert-even .digest{height:226px;}.ie .digest-world-service .digest{height:0;}.digest-double .no-image,.expert-even .no-image{width:304px;}.digest-double .column-1,.expert-even .column-1{width:304px;margin-left:0;}.digest-double .column-2,.expert-even .column-2{position:relative;float:left;clear:none;display:inline;overflow:hidden;width:304px;margin-left:0;}.digest-multiple{margin-bottom:7px;}.ie .digest-multiple{height:1%;}.digest-multiple .digest{float:left;display:inline;width:144px;margin-left:16px;}.container-category-digests .digest{margin-bottom:4px;}.ie .digest-multiple .digest{height:208px;}.digest-multiple div.first-child{margin-left:0;}.digest-multiple .digest ul{width:144px;padding-left:0;margin-bottom:5px;}.digest-multiple .no-image{position:relative;clear:both;overflow:hidden;width:100%;margin-left:0;padding-bottom:14px;margin-top:8px;padding-right:0;}.digest-multiple .medium-image{width:144px;padding-left:0;padding-bottom:6px;margin-left:0;margin-top:5px;}.ie .digest-multiple .medium-image hr,.ie7 .digest-multiple .medium-image hr,.ie .digest-multiple .no-image hr,.ie7 .digest-multiple .no-image hr{display:none;}.digest-multiple .medium-image a,.digest-multiple .no-image a{display:block;}.digest-multiple .medium-image img{float:none;display:block;margin-left:0;padding-bottom:13px;}.digest-multiple .medium-image .gvl3-icon-wrapper{position:absolute;left:0;top:3px;}.digest-multiple .column-1{width:144px;margin-left:0;}.digest-multiple .column-2,.ie .digest-multiple .column-2{position:relative;float:left;clear:both;display:inline;overflow:hidden;width:144px;margin-left:0;}.digest-multiple .new-row,.container-section-digest .new-row,.container-expert-views .new-row{clear:both;margin-left:0;}.text-first .digest li.medium-image h4{padding-top:3px;padding-bottom:2px;margin-left:-160px;}.text-first .digest li.medium-image h4 a{line-height:1;}.text-first .digest li.medium-image img{margin-top:18px;left:160px;}.text-first .digest .medium-image .gvl3-icon-wrapper{left:0;top:24px;}.text-first .digest .no-image h4{padding-top:4px;}.digest-single .digest .no-image p,.digest-double .digest li.no-image p,.digest-multiple .digest li.no-image p{padding-top:4px;}.digest-double .no-image{padding-top:4px;padding-bottom:8px;}.digest-multiple .digest li.no-image{padding-bottom:8px;}.ie .digest-single .digest .medium-image,.ie7 .digest-single .digest .medium-image{padding-bottom:8px;}.ie .digest-single .digest .no-image,.ie .digest-double .digest .no-image,.ie7 .digest-single .digest .no-image,.ie7 .digest-double .digest .no-image{padding-bottom:0;}.ie .digest-double .digest .medium-image h4 img,.ie7 .digest-double .digest .medium-image h4 img{margin-top:0;}.geo-digest-vertical{position:relative;overflow:hidden;margin:0 0 16px;padding:0 8px;width:632px;border:8px solid #262835;border-width:8px 0;background:#262835;text-shadow:0 0 1px rgba(0,0,0,0.1);}.ie .geo-digest{float:left;}.geo-digest-vertical-header{padding-top:4px;}.geo-digest-vertical .geo-digest-region{margin-bottom:0;}.geo-digest-vertical .geo-digest-section ul{width:288px;}.geo-digest-vertical .tab{position:relative;padding:17px 16px 7px 0;}.geo-digest-vertical .tab a,.geo-digest-vertical-header{color:#f0f0f0;}.blq-js .geo-digest-vertical{padding-right:304px;width:312px;background:#262835 url(../img/geo-digest-vertical-panel.gif) 312px top repeat-y;}.blq-js .geo-digest-vertical .geo-digest-vertical-header{position:relative;padding-bottom:12px;}.blq-js .geo-digest-vertical .tab{position:relative;float:left;display:inline;clear:left;width:280px;margin-left:-4px;border-top:1px solid #3c3e51;padding:8px 16px 7px 4px;cursor:pointer;}.blq-js .geo-digest-vertical .tab a{color:#a9a9a9;}.blq-js .geo-digest-vertical void{background:#3c3e51;width:288px;border-bottom:1px solid #3c3e51;margin-bottom:-1px;}.blq-js .geo-digest-vertical void a{color:#f0f0f0;}.blq-js .geo-digest-vertical .geo-digest-region{padding-left:8px;float:right;display:inline;clear:right;margin-right:-296px;}.blq-js .geo-digest-vertical .geo-digest-section{padding-top:1px;}.blq-js .geo-digest-vertical .panel{opacity:0;position:absolute;right:-5000%;}.blq-js .geo-digest-vertical void{opacity:1;left:312px;top:0;-webkit-transition:opacity .2s ease-in;}.blq-js .geo-digest-vertical .geo-digest-region .column-1{clear:none;display:block;float:none;margin-left:0;}#england-map{position:relative;margin:0 63px 15px 57px;height:241px;width:192px;background-image:url(../img/england-map.png);background-repeat:none;overflow:hidden;}#england-map ul li{list-style:none;width:1px;height:1px;position:absolute;overflow:visible;left:-1px;top:-1px;}#england-map ul li a,#england-map ul li a span{position:absolute;display:block;text-indent:-50000px;overflow:visible;line-height:1px;padding:0;font-size:1px;}#england-map ul li a{width:1px;height:1px;overflow:visible;left:0;top:0;}#england-map ul li a span{background-image:url(../img/england-map.png);background-repeat:none;filter:alpha(opacity=0);opacity:0;-moz-opacity:0;-webkit-transition:opacity 1s ease-out;cursor:pointer;}.blq-js #england-map ul li a.selected span,#england-map ul li a:hover span,#england-map ul li a:active span,#england-map ul li a:focus span{filter:alpha(opacity=100);opacity:1;-moz-opacity:1;-webkit-transition:opacity .2s ease-in;}.blq-js #england-map ul li a.selected,#england-map ul li a:active,#england-map ul li a:focus{outline:0;}#england-map-region-1 a span.filler-1{background-position:1px 242px;top:0;left:0;height:53px;width:85px;}#england-map-region-1 a span.filler-2{background-position:1px 189px;top:53px;left:0;height:22px;width:30px;}#england-map-region-1 a span.filler-3{background-position:145px 189px;top:53px;left:48px;height:22px;width:37px;}#england-map-region-1 a span.filler-4{background-position:1px 167px;top:75px;left:0;height:35px;width:95px;}#england-map-region-2 a span.filler-1{background-position:107px 242px;top:0;left:86px;height:56px;width:107px;}#england-map-region-3 a span.filler-1{background-position:107px 185px;top:57px;left:86px;height:17px;width:107px;}#england-map-region-3 a span.filler-2{background-position:97px 168px;top:74px;left:96px;height:24px;width:97px;}#england-map-region-3 a span.filler-3{background-position:70px 144px;top:98px;left:123px;height:12px;width:70px;}#england-map-region-3 a span.filler-4{background-position:70px 132px;top:110px;left:123px;height:13px;width:22px;}#england-map-region-4 a span.filler-1{background-position:1px 131px;top:111px;left:0;height:39px;width:105px;}#england-map-region-5 a span.filler-1{background-position:97px 155px;top:99px;left:96px;height:11px;width:26px;}#england-map-region-5 a span.filler-2{background-position:87px 132px;top:110px;left:106px;height:14px;width:16px;}#england-map-region-5 a span.filler-3{background-position:87px 118px;top:124px;left:106px;height:26px;width:22px;}#england-map-region-6 a span.filler-1{background-position:1px 91px;top:151px;left:0;height:38px;width:99px;}#england-map-region-6 a span.filler-2{background-position:1px 53px;top:189px;left:0;height:53px;width:69px;}#england-map-region-7 a span.filler-1{background-position:47px 131px;top:111px;left:146px;height:13px;width:47px;}#england-map-region-7 a span.filler-2{background-position:64px 118px;top:124px;left:129px;height:27px;width:64px;}#england-map-region-7 a span.filler-3{background-position:74px 91px;top:151px;left:119px;height:13px;width:74px;}#england-map-region-8 a span.filler-1{background-position:93px 91px;top:151px;left:100px;height:39px;width:18px;}#england-map-region-8 a span.filler-2{background-position:123px 52px;top:190px;left:70px;height:24px;width:48px;}#england-map-region-8 a span.filler-3{background-position:123px 28px;top:214px;left:70px;height:28px;width:6px;}#england-map-region-8 a span.filler-4{background-position:82px 28px;top:214px;left:111px;height:28px;width:7px;}#england-map-region-9 a span.filler-1{background-position:74px 77px;top:165px;left:119px;height:77px;width:74px;}#england-map-region-10 a span.filler-1{background-position:162px 188px;top:54px;left:31px;height:20px;width:16px;}#england-map-region-10 a span.filler-2{background-position:116px 27px;top:215px;left:77px;height:26px;width:33px;}.geo-digest-solo-header{padding:8px 0;}.geo-digest-section{position:relative;overflow:visible;margin:0 0 5px;}.ie .geo-digest-section{height:1%;}.geo-digest-section-header{padding-top:11px;padding-bottom:5px;border-bottom:1px solid #ddd;}.geo-digest-section ul{padding-top:7px;padding-bottom:7px;overflow:hidden;width:288px;}.geo-digest-section li{margin-top:8px;width:100%;position:relative;float:left;display:inline;clear:left;}.geo-digest-section .gvl3-icon{position:absolute;top:-1px;left:0;}.geo-digest-region{position:relative;overflow:hidden;margin:0 0 8px;width:288px;padding:8px 8px 0 312px;background:#3c3e51;}.geo-digest-region-header,#personalisation h3{position:relative;padding-top:4px;padding-bottom:12px;margin-left:-304px;width:592px;}.geo-digest-region .column-1{position:relative;float:left;display:inline;clear:left;width:288px;margin-left:-304px;}.geo-digest-region .column-2{width:288px;}.ie7 .geo-digest-region .column-2,.ie .geo-digest-region .column-2{float:right;clear:right;}.geo-digest-region .geo-digest-section{margin-top:0;}.geo-digest-region-header,#personalisation h3,.geo-digest-region .geo-digest-section-header,.geo-digest-section-header a.story{color:#f0f0f0;}.geo-digest-region .geo-digest-section-header{border-bottom-color:#262835;}.geo-digest-region .geo-digest-section ul{padding-bottom:8px;margin-bottom:-1px;margin-top:-8px;}.geo-digest-region a{color:#ACC2D4;text-shadow:0 0 1px #3c3e51;}.geo-digest-region a.from-external-source{color:#F0F0F0;display:block;margin-left:0;text-indent:0;}#world-map{margin:16px 24px 17px 16px;}#geo-world-news-digest .geo-digest-region-header{top:-69px;margin-bottom:4px;position:absolute;left:-50000px;}.container-geographic-regions-generic{position:relative;clear:both;overflow:hidden;width:608px;margin:0 0 16px;padding:8px 8px 0;background:#262835;text-shadow:0 0 1px rgba(0,0,0,0.1);}.ie .container-geographic-regions-generic{float:left;}.container-geographic-regions-generic .tab{padding:11px 16px 12px 8px;position:relative;background:transparent;z-index:100;}.container-geographic-regions-generic .tab a{color:#f0f0f0;}.container-geographic-regions-generic .panel{position:relative;}.container-geographic-regions-generic .uk-news-heading,.container-geographic-regions-generic .world-news-heading{left:-50000px;margin-bottom:4px;position:absolute;top:-69px;}.container-geographic-regions-generic .around-uk-digest-header{color:#f0f0f0;padding-bottom:12px;margin-left:-304px;padding-top:4px;position:relative;width:592px;}.container-geographic-regions-generic .geo-digest-region .vertical-line{border-left:2px solid #f0f0f0;margin-left:5px;margin-right:5px;font-size:.76em;}.container-geographic-regions-generic .geo-digest-region .or-text{position:absolute;left:-9000px;}.ie #geo-uk-news-digest{height:1%;}.blq-js .container-geographic-regions-generic .tab{float:left;display:inline;top:0;cursor:pointer;}.blq-js .container-geographic-regions-generic void{background:#3c3e51;}.blq-js .container-geographic-regions-generic .panel{position:absolute;float:right;display:inline;clear:right;right:-500%;margin-right:0;margin-top:40px;opacity:0;}.blq-js .ie .container-geographic-regions-generic .panel,.blq-js .ie7 .container-geographic-regions-generic .panel,.blq-js .ie8 .container-geographic-regions-generic .panel{margin-top:39px;clear:none;}.blq-js .ie .container-geographic-regions-generic .panel{height:1%;}.blq-js .container-geographic-regions-generic void{position:relative;margin-right:-608px;right:608px;opacity:1;-webkit-transition:opacity .2s ease-in;}.extra-padding .column-top{margin-top:92px;}#geo-wales-news-digest .column-2{clear:right;}#wales-map{float:right;width:138px;height:138px;background:transparent url(../img/wales-map.png) no-repeat top left;margin:-7px 80px -15px 0;}#geo-scotland-news-digest .column-2{clear:right;}#scotland-map{float:right;width:251px;height:220px;background:transparent url(../img/scotland-map.png@v.2) no-repeat top left;margin:8px 0 4px;}#world-map{position:relative;height:151px;width:240px;background-image:url('../img/world-map.png');background-repeat:none;overflow:hidden;}#world-map ul li{list-style:none;width:1px;height:1px;position:absolute;overflow:visible;left:-1px;top:-1px;}#world-map ul li a,#world-map ul li a span{position:absolute;display:block;text-indent:-50000px;overflow:visible;line-height:1px;padding:0;font-size:1px;}#world-map ul li a{width:1px;height:1px;overflow:visible;left:0;top:0;}#world-map ul li a span{background-image:url('../img/world-map.png');background-repeat:none;filter:alpha(opacity=0);opacity:0;-moz-opacity:0;-webkit-transition:opacity 1s ease-out;cursor:pointer;}#world-map ul li a:hover span,#world-map ul li a:active span,#world-map ul li a:focus span{filter:alpha(opacity=100);opacity:1;-moz-opacity:1;-webkit-transition:opacity .2s ease-in;}#world-map ul li a:active,#world-map ul li a:focus{outline:0;}#world-map-us-canada a span.filler-1{background-position:1px 152px;top:0;left:0;height:20px;width:77px;}#world-map-us-canada a span.filler-2{background-position:1px 132px;top:20px;left:0;height:18px;width:71px;}#world-map-us-canada a span.filler-3{background-position:1px 114px;top:38px;left:0;height:25px;width:81px;}#world-map-us-canada a span.filler-4{background-position:1px 89px;top:63px;left:0;height:28px;width:109px;}#world-map-latin-america a span.filler-1{background-position:1px 60px;top:92px;left:0;height:60px;width:109px;}#world-map-africa a span.filler-1{background-position:131px 69px;top:83px;left:110px;height:17px;width:37px;}#world-map-africa a span.filler-2{background-position:131px 52px;top:100px;left:110px;height:52px;width:57px;}#world-map-europe a span.filler-1{background-position:163px 152px;top:0;left:78px;height:21px;width:122px;}#world-map-europe a span.filler-2{background-position:169px 131px;top:21px;left:72px;height:16px;width:128px;}#world-map-europe a span.filler-3{background-position:159px 115px;top:37px;left:82px;height:25px;width:118px;}#world-map-europe a span.filler-4{background-position:131px 90px;top:62px;left:110px;height:6px;width:90px;}#world-map-europe a span.filler-5{background-position:131px 84px;top:68px;left:110px;height:14px;width:57px;}#world-map-middle-east a span.filler-1{background-position:93px 69px;top:83px;left:148px;height:16px;width:19px;}#world-map-south-asia a span.filler-1{background-position:73px 70px;top:82px;left:168px;height:70px;width:17px;}#world-map-asia-pacific a span.filler-1{background-position:40px 152px;top:0;left:201px;height:69px;width:40px;}#world-map-asia-pacific a span.filler-2{background-position:74px 83px;top:69px;left:167px;height:13px;width:74px;}#world-map-asia-pacific a span.filler-3{background-position:55px 70px;top:82px;left:186px;height:71px;width:55px;}#scotland-map-hover{float:right;width:251px;height:220px;background:transparent url(../img/scotland-map-hover.png) no-repeat top left;margin:8px 0 4px;position:relative;}#scotland-map-hover ul li{list-style:none;width:1px;height:1px;position:absolute;overflow:visible;left:-1px;top:-1px;}#scotland-map-hover ul li a,#scotland-map-hover ul li a span{position:absolute;display:block;text-indent:-50000px;overflow:visible;line-height:1px;padding:0;font-size:1px;}#scotland-map-hover ul li a{width:1px;height:1px;overflow:visible;left:0;top:0;}#scotland-map-hover ul li a span{background-image:url(../img/scotland-map-hover.png);background-repeat:none;filter:alpha(opacity=0);opacity:0;-moz-opacity:0;-webkit-transition:opacity 1s ease-out;cursor:pointer;}#scotland-map-hover ul li a:hover span,#world-map-hover ul li a:active span,#world-map-hover ul li a:focus span{filter:alpha(opacity=100);opacity:1;-moz-opacity:1;-webkit-transition:opacity .2s ease-in;}#scotland-map-hover ul li a:active,#world-map-hover ul li a:focus{outline:0;}#scotland-news-map-edinburgh-east-fife a span.filler-1{background-position:-115px 89px;top:132px;left:116px;height:21px;width:81px;}#scotland-news-map-edinburgh-east-fife a span.filler-2{background-position:-105px 77px;top:144px;left:106px;height:18px;width:10px;}#scotland-news-map-edinburgh-east-fife a span.filler-3{background-position:-115px 68px;top:153px;left:116px;height:9px;width:16px;}#scotland-news-map-glasgow-west a span.filler-1{background-position:1px 103px;top:118px;left:0;height:15px;width:60px;}#scotland-news-map-glasgow-west a span.filler-2{background-position:1px 88px;top:133px;left:0;height:16px;width:81px;}#scotland-news-map-glasgow-west a span.filler-3{background-position:1px 72px;top:149px;left:0;height:7px;width:97px;}#scotland-news-map-glasgow-west a span.filler-4{background-position:1px 65px;top:156px;left:0;height:20px;width:105px;}#scotland-news-map-glasgow-west a span.filler-5{background-position:1px 45px;top:176px;left:0;height:12px;width:93px;}#scotland-news-map-glasgow-west a span.filler-6{background-position:1px 33px;top:188px;left:0;height:5px;width:79px;}#scotland-news-map-glasgow-west a span.filler-7{background-position:1px 28px;top:193px;left:0;height:15px;width:65px;}#scotland-news-map-highlands-islands a span.filler-1{background-position:1px 212px;top:9px;left:0;height:27px;width:138px;}#scotland-news-map-highlands-islands a span.filler-2{background-position:1px 185px;top:36px;left:0;height:32px;width:120px;}#scotland-news-map-highlands-islands a span.filler-3{background-position:1px 153px;top:68px;left:0;height:43px;width:105px;}#scotland-news-map-highlands-islands a span.filler-4{background-position:1px 110px;top:111px;left:0;height:6px;width:81px;}#scotland-news-map-highlands-islands a span.filler-5{background-position:-60px 104px;top:117px;left:61px;height:15px;width:20px;}#scotland-news-map-east-orkney-shetland a span.filler-1{background-position:-138px 212px;top:9px;left:139px;height:107px;width:78px;}#scotland-news-map-east-orkney-shetland a span.filler-2{background-position:-105px 152px;top:69px;left:106px;height:42px;width:33px;}#scotland-news-map-east-orkney-shetland a span.filler-3{background-position:-120px 184px;top:37px;left:121px;height:32px;width:18px;}#scotland-news-map-east-orkney-shetland a span.filler-4{background-position:-126px 110px;top:111px;left:127px;height:5px;width:12px;}#scotland-news-map-south-scotland a span.filler-1{background-position:-132px 67px;top:154px;left:133px;height:59px;width:64px;}#scotland-news-map-south-scotland a span.filler-2{background-position:-105px 58px;top:163px;left:106px;height:50px;width:27px;}#scotland-news-map-south-scotland a span.filler-3{background-position:-93px 44px;top:177px;left:94px;height:36px;width:12px;}#scotland-news-map-south-scotland a span.filler-4{background-position:-79px 32px;top:189px;left:80px;height:24px;width:14px;}#scotland-news-map-south-scotland a span.filler-5{background-position:-65px 27px;top:194px;left:66px;height:19px;width:14px;}#scotland-news-map-tayside-central a span.filler-1{background-position:-81px 109px;top:112px;left:82px;height:36px;width:15px;}#scotland-news-map-tayside-central a span.filler-2{background-position:-96px 109px;top:112px;left:97px;height:43px;width:8px;}#scotland-news-map-tayside-central a span.filler-3{background-position:-104px 109px;top:112px;left:105px;height:19px;width:21px;}#scotland-news-map-tayside-central a span.filler-4{background-position:-104px 90px;top:131px;left:105px;height:12px;width:10px;}#scotland-news-map-tayside-central a span.filler-5{background-position:-125px 104px;top:117px;left:126px;height:14px;width:71px;}#wales-map-hover{float:right;width:138px;height:138px;background:transparent url(../img/wales-map-hover.png) no-repeat top left;margin:-7px 80px -15px 0;position:relative;}#wales-map-hover ul li{list-style:none;width:1px;height:1px;position:absolute;overflow:visible;left:-1px;top:-1px;}#wales-map-hover ul li a,#wales-map-hover ul li a span{position:absolute;display:block;text-indent:-50000px;overflow:visible;line-height:1px;padding:0;font-size:1px;}#wales-map-hover ul li a{width:1px;height:1px;overflow:visible;left:0;top:0;}#wales-map-hover ul li a span{background-image:url(../img/wales-map-hover.png);background-repeat:none;filter:alpha(opacity=0);opacity:0;-moz-opacity:0;-webkit-transition:opacity 1s ease-out;cursor:pointer;}#wales-map-hover ul li a:hover span,#world-map-hover ul li a:active span,#world-map-hover ul li a:focus span{filter:alpha(opacity=100);opacity:1;-moz-opacity:1;-webkit-transition:opacity .2s ease-in;}#wales-map-hover ul li a:active,#world-map-hover ul li a:focus{outline:0;}#wales-news-map-north-west a span.filler-1{background-position:-14px 123px;top:16px;left:15px;height:39px;width:65px;}#wales-news-map-north-west a span.filler-2{background-position:-14px 85px;top:55px;left:15px;height:5px;width:58px;}#wales-news-map-mid a span.filler-1{background-position:-15px 78px;top:61px;left:16px;height:21px;width:65px;}#wales-news-map-mid a span.filler-2{background-position:-80px 94px;top:45px;left:81px;height:37px;width:45px;}#wales-news-map-mid a span.filler-3{background-position:-80px 94px;top:56px;left:74px;height:5px;width:7px;}#wales-news-map-mid a span.filler-4{background-position:-15px 57px;top:82px;left:16px;height:6px;width:47px;}#wales-news-map-mid a span.filler-5{background-position:-79px 57px;top:82px;left:80px;height:8px;width:45px;}#wales-news-map-mid a span.filler-6{background-position:-79px 49px;top:90px;left:80px;height:7px;width:18px;}#wales-news-map-mid a span.filler-7{background-position:-79px 42px;top:97px;left:80px;height:4px;width:5px;}#wales-news-map-south-west a span.filler-1{background-position:-62px 56px;top:83px;left:63px;height:25px;width:17px;}#wales-news-map-south-west a span.filler-2{background-position:-79px 37px;top:102px;left:80px;height:5px;width:4px;}#wales-news-map-south-west a span.filler-3{background-position:-15px 50px;top:89px;left:16px;height:34px;width:47px;}#wales-news-map-south-west a span.filler-4{background-position:-62px 31px;top:108px;left:63px;height:16px;width:14px;}#wales-news-map-north-east a span.filler-1{background-position:-80px 123px;top:16px;left:81px;height:28px;width:45px;}#wales-news-map-south-east a span.filler-1{background-position:-84px 41px;top:98px;left:85px;height:27px;width:40px;}#wales-news-map-south-east a span.filler-2{background-position:-76px 31px;top:108px;left:77px;height:18px;width:8px;}#wales-news-map-south-east a span.filler-3{background-position:-98px 49px;top:90px;left:99px;height:8px;width:26px;}.container-guides{position:relative;clear:both;overflow:hidden;}.ie .container-guides{height:1%;overflow:auto;}.ie7 .container-guides{height:1%;overflow:auto;padding-bottom:16px;}.guide-content{float:left;display:inline;position:relative;padding-top:0;margin-bottom:16px;}.full-width-guides .guide-content{background-color:#505050;width:624px;display:block;padding-top:4px;}.full-width-guides .wide-content-group h3 a{padding-left:16px;}.ie .full-width-guides .guide-content .guide{margin-top:4px;}.ie .full-width-guides .guide-content{display:block;padding-top:0;}.guides-stories-header{padding:8px 0;}.guide-content .guide{background-color:#505050;padding:11px 0 4px;}.guide-content .guide h3 img{display:inline;float:left;margin-top:-11px;position:relative;z-index:2;}.guide-content .guide h3 a{color:#fff;margin:3px 8px 0 0;}.guide-content .guide p{color:#fff;margin:1px 8px 8px 16px;}.guide-content .wide-content-group{margin-left:304px;width:320px;margin-top:-4px;}.guide-content .wide-content-group img{border-right:1px solid #FFF;margin-left:-304px;}.guide-content .stacked-content-group{width:304px;}.container-guides .first-group .stacked-content-group{margin-right:16px;display:block;}.guide-content .stacked-content-group h3{padding-left:8px;}.guide-content .stacked-content-group h3 img{border-bottom:1px solid #FFF;margin-bottom:8px;margin-left:-8px;float:none;margin-top:-14px;}.guide-content .stacked-content-group .guide p{margin-left:8px;}.guide-content .stacked-content-group .guide,.guide-content .stacked-content-group .guide h3,.guide-content .stacked-content-group .guide h3 a,.guide-content .stacked-content-group .guide h3 img{display:block;}.guide-content .stacked-content-group .guide h3 a{padding-right:0;margin-right:0;}.ie .guide-content .stacked-content-group .guide{margin-top:4px;}.stacked-overlay-guides .guide{background:none;padding:11px 0 0 0;}.stacked-overlay-guides .guide .overlay{position:absolute;width:288px;bottom:1px;left:0;z-index:10;cursor:pointer;padding:9px 8px 7px;background:transparent url(../../../../../1_4_9/cream/hi/shared/img/transparencies/rgba-0-0-0-07.png) repeat;}.ie .stacked-overlay-guides .guide .overlay{background:black;}.stacked-overlay-guides .guide a:active .overlay{background:#D2700F;}.stacked-overlay-guides .guide h3 a.story .summary{font-weight:normal;}.stacked-overlay-guides .guide h3 a.story:hover,.stacked-overlay-guides .guide h3 a.story:focus{text-decoration:none;}.stacked-overlay-guides .guide h3 a.story:hover .headline,.stacked-overlay-guides .guide h3 a.story:focus .headline{text-decoration:underline;}.stacked-overlay-guides .guide h3 a.story:hover .summary,.stacked-overlay-guides .guide h3 a.story:focus .summary{text-decoration:none;}.stacked-overlay-guides .guide h3 .overlay strong,.stacked-overlay-guides .guide h3 a.story .summary{color:#fff;display:block;}.stacked-overlay-guides .guide h3 a .overlay strong{font-weight:bold;padding-bottom:2px;}.guide-content .stacked-overlay-guides h3 img{margin-bottom:0;}.ie .guide-content .stacked-overlay-guides{margin-bottom:0;margin-top:-4px;}.ie .guide-content .stacked-overlay-guides .overlay{bottom:0;}.container-guides .wide-content-group .gvl3-icon-wrapper,.container-guides .stacked-overlay-guides .gvl3-icon-wrapper{left:0;top:0;z-index:10;}.hyper-foldout{position:relative;overflow:hidden;clear:both;width:464px;margin:0 0 15px;margin-top:-1px;}.hyper-foldout .hyper-foldout-header{position:relative;display:block;padding:8px 8px;background:#505050;color:#fff;text-shadow:0 0 1px rgba(0,0,0,0.3);font-weight:bold;}.blq-js .hyper-foldout .hyper-foldout-header{padding:0;}.blq-js .hyper-foldout .hyper-foldout-header a{position:relative;display:block;width:448px;padding:8px;background:#505050;color:#fff;text-shadow:0 0 1px rgba(0,0,0,0.3);font-weight:bold;}.blq-js .hyper-foldout .hyper-foldout-header{cursor:pointer;}.blq-js .hyper-foldout .hyper-foldout-header a:hover,.blq-js .hyper-foldout .hyper-foldout-header a:focus{background:#323232;}.blq-js void .hyper-foldout-header a:link,.blq-js void .hyper-foldout-header a:hover,.blq-js void .hyper-foldout-header a:focus,.blq-js .hyper-foldout .hyper-foldout-header a:active{background:#D1700E;}.hyper-foldout .hyper-foldout-header .hyper-foldout-arrow{position:absolute;top:8px;right:8px;width:16px;height:16px;overflow:hidden;text-indent:-50000%;background:url(../../../../../1_4_9/cream/hi/shared/img/foldout-arrow.gif) left 0 no-repeat;}.blq-js .hyper-foldout-header a .hyper-foldout-arrow{background-position:left 0;}.blq-js .hyper-foldout-header a:hover .hyper-foldout-arrow,.blq-js .hyper-foldout-header a:focus .hyper-foldout-arrow{background-position:center 0;}.blq-js .hyper-foldout-header a:active .hyper-foldout-arrow{background-position:right 0;}.blq-js void .hyper-foldout-header a .hyper-foldout-arrow,.blq-js void .hyper-foldout-header a:hover .hyper-foldout-arrow,.blq-js void .hyper-foldout-header a:focus .hyper-foldout-arrow,.blq-js void .hyper-foldout-header a:active .hyper-foldout-arrow{background-position:right -160px;}.hyper-foldout .hyper-foldout-panel{position:relative;float:left;display:inline;width:464px;clear:both;margin-top:1px;overflow:hidden;background:#f0f0f0;}.blq-js .hyper-foldout .hyper-foldout-panel{height:0;}.hyper-foldout .hyper-foldout-panel ul{position:relative;overflow:hidden;clear:both;float:left;display:inline;padding:11px 8px 5px 240px;}.hyper-foldout .hyper-foldout-panel li{position:relative;width:224px;padding-bottom:8px;}.hyper-foldout .hyper-foldout-panel .column-1{float:left;display:inline;clear:left;margin-left:-232px;}.ie .hyper-foldout .hyper-foldout-panel .column-2,.ie7 .hyper-foldout .hyper-foldout-panel .column-2{float:right;}.hyper-promotional-content{position:relative;display:block;overflow:hidden;clear:both;width:336px;background:#ededed;margin-bottom:16px;}.hyper-promotional-content h2{font-size:1.846em;font-weight:bold;letter-spacing:-0.05em;line-height:24px;padding:8px;position:relative;text-indent:-1px;}.hyper-promotional-content li h3{float:left;display:inline;clear:both;width:100%;font-weight:bold;padding:4px 0 0 0;overflow:visible;}.hyper-promotional-content ul{background:#d1700e;list-style-type:none;padding:0 8px 7px;}.hyper-promotional-content ul li{clear:both;position:relative;display:block;overflow:hidden;padding-bottom:11px;}.ie .hyper-promotional-content ul li{height:1%;}.hyper-promotional-content ul li.first-child{padding-top:8px;}.hyper-promotional-content ul li p{color:#fff;position:relative;clear:both;padding:4px 0;}.hyper-promotional-content ul li a,.hyper-promotional-content ul li a:visited,.hyper-promotional-content ul li a:active{color:#fff;}.hyper-promotional-content ul li.large-image{overflow:visible;padding-top:0;}.hyper-promotional-content li.large-image img{border-bottom:solid 1px #fff;display:block;height:189px;margin:-4px 0 6px -8px;position:relative;width:336px;}.hyper-promotional-content ul li.medium-image{padding:8px 0 0 152px;}.hyper-promotional-content li.medium-image h3{float:none;display:inline;overflow:auto;}.hyper-promotional-content li.medium-image img{display:inline;float:left;position:relative;top:3px;margin-left:-152px;height:81px;width:144px;}.hyper-promotional-content ul li.medium-image p{clear:none;display:block;}.hyper-promotional-content .large-image .gvl3-icon-wrapper{left:-8px;position:absolute;top:0;}.hyper-promotional-content .medium-image .gvl3-icon-wrapper{left:0;position:absolute;top:11px;}.hyper-promotional-content .gvl3-icon{left:0;position:absolute;top:3px;}.hyper-promotional-content .first-child .gvl3-icon{top:11px;}.hyper-promotional-content .gvl3-icon-wrapper .gvl3-icon{left:auto;position:relative;top:auto;}.bbccom_slot_xxl #bbccom_module_hyper-promotional-content{clear:both;}.bbccom_slot_xxl .hyper-promotional-content{width:496px;}.bbccom_slot_xxl .hyper-promotional-content ul{overflow:hidden;width:480px;padding-bottom:0;}.bbccom_slot_xxl .hyper-promotional-content li{width:224px;padding-right:16px;clear:none;float:left;display:inline;margin-top:8px;}.bbccom_slot_xxl .hyper-promotional-content ul li.first-child{width:100%;padding-right:0;margin-top:0;}.bbccom_slot_xxl .hyper-promotional-content ul li.large-image{float:none;display:block;margin-top:0;padding:8px 0 0 336px;width:144px;height:180px;}.bbccom_slot_xxl .hyper-promotional-content li.large-image h3{float:none;display:block;clear:both;}.bbccom_slot_xxl .hyper-promotional-content li.large-image img{border-bottom:none;position:absolute;top:0;left:0;margin-top:0;margin-bottom:0;}.bbccom_slot_xxl .hyper-promotional-content li.large-image p{clear:none;}.bbccom_slot_xxl .hyper-promotional-content ul li.medium-image{width:auto;float:none;display:block;clear:both;margin-top:0;padding-bottom:8px;}.bbccom_slot_xxl .hyper-promotional-content li.medium-image img{top:1px;}.bbccom_slot_xxl .hyper-promotional-content li.medium-image .gvl3-icon-wrapper{top:9px;}.hyper-container-title{position:relative;overflow:visible;width:464px;margin:0 0 4px;padding-top:7px;border-top:1px solid #ddd;}.hyper-container-title .hyper-container-title-header{position:relative;margin-top:1px;margin-bottom:10px;padding-bottom:1px;}.hyper-container-title .hyper-container-title-header a.special-report{position:relative;display:block;margin-top:-9px;margin-bottom:-12px;padding:9px 8px 12px;z-index:1;color:#fff;background:#d60000;}.hyper-container-title .hyper-depth-header{position:relative;margin:-7px 0 0;padding:11px 0 12px;overflow:hidden;}.ie .hyper-container-title .hyper-depth-header{height:1%;}.hyper-container-title .hyper-depth-header-branded{border-bottom:3px solid #ddd;z-index:0;}.hyper-container-title .hyper-depth-header-branded a{position:relative;display:block;margin-top:-11px;margin-bottom:-13px;padding:11px 0 13px;z-index:1;}.hyper-container-title .hyper-depth-header-branded img{position:absolute;bottom:0;right:0;z-index:-10;}.hyper-container-title .hyper-depth-header a.special-report{position:relative;display:block;margin-top:-11px;margin-bottom:-13px;padding:11px 8px 13px;z-index:1;color:#fff;background:#CD1211;}.hyper-related-assets{position:relative;overflow:visible;width:464px;margin:0 0 2px;padding-top:11px;border-top:1px solid #ddd;}.hyper-related-assets .hyper-depth-stories-header{position:relative;margin-top:3px;margin-bottom:12px;}.hyper-related-assets ul{position:relative;overflow:hidden;width:224px;margin:-3px 0 4px;padding-left:240px;}.hyper-related-assets li{position:relative;padding-bottom:8px;}.hyper-related-assets .gvl3-icon{position:absolute;top:-1px;left:0;}.hyper-related-assets .gvl3-icon-wrapper{position:absolute;top:0;}.hyper-related-assets .gvl3-icon-wrapper .gvl3-icon{position:relative;top:auto;left:auto;}.hyper-related-assets .with-summary{position:relative;clear:both;overflow:hidden;width:352px;margin-top:0;margin-left:-240px;padding-right:112px;margin-bottom:8px;}.hyper-related-assets .with-summary .hyper-story-header{position:relative;float:left;display:inline;width:100%;margin-bottom:0;}.hyper-related-assets .medium-image{position:relative;clear:both;overflow:hidden;width:304px;margin-top:-2px;margin-left:-240px;padding-bottom:7px;padding-left:160px;}.hyper-related-assets .medium-image .hyper-story-header{position:relative;margin-bottom:0;}.hyper-related-assets .medium-image img{position:relative;float:left;display:inline;margin-left:-160px;width:144px;height:81px;top:2px;}.hyper-related-assets .medium-image .gvl3-icon-wrapper{top:2px;left:-160px;}.hyper-related-assets .column-1{position:relative;float:left;clear:left;display:inline;overflow:hidden;width:224px;margin-left:-240px;}.hyper-related-assets .column-2{clear:none;position:relative;width:224px;}.ie .hyper-related-assets .column-2{float:right;}.top-section-stories{width:464px;background-color:#ededed;overflow:auto;clear:both;}.top-section-stories h3{position:relative;}.ie .top-section-stories .column-1 h3,.ie .top-section-stories .column-2 h3{float:left;}.top-section-stories h3 .gvl3-icon-wrapper{position:absolute;top:1px;left:-152px;}.top-section-stories .column-1 h3 .gvl3-icon,.top-section-stories .column-2 h3 .gvl3-icon{position:absolute;top:0;left:0;}.top-section-stories ul{margin:0 8px 0 8px;}.top-section-stories .top-section-stories-header{padding:8px;}.top-section-stories li{padding-bottom:8px;position:relative;}.top-section-stories .medium-image{clear:both;margin-top:-1px;overflow:hidden;padding-bottom:12px;padding-left:152px;position:relative;}.ie .top-section-stories .medium-image{margin-bottom:8px;}.top-section-stories .medium-image img{display:inline;float:left;top:1px;height:81px;margin-left:-152px;position:relative;width:144px;}.top-section-stories .medium-image h3{margin-bottom:1px;position:relative;}.top-section-stories .column-1{clear:left;display:inline;float:left;margin-left:0;overflow:hidden;position:relative;width:216px;padding-right:16px;}.top-section-stories .column-2{clear:none;position:relative;float:left;width:216px;}.container-local-weather-and-travel{width:464px;background-color:#ededed;overflow:auto;padding:0;}.container-local-weather-and-travel .travel .useful-links{width:224px;margin-bottom:0;padding-bottom:0;margin-left:0;}.container-local-weather-and-travel .travel,.container-local-weather-and-travel .weather{border-top:1px solid #DDD;}.container-local-weather-and-travel .weather .data-feed-now h3{border-bottom:1px solid #DDD;padding:8px 0 7px;}.container-local-weather-and-travel .data-feed-now h3#weather_sitelabel{border-bottom:none;padding-bottom:0;}.container-local-weather-and-travel .travel{padding:0;margin:0 8px 0 8px;width:216px;float:left;display:inline;}.container-local-weather-and-travel .travel .useful-links{width:216px;}.container-local-weather-and-travel .travel .useful-links ul{width:216px;padding:7px 0 0 0;}.container-local-weather-and-travel .travel .useful-links li.column-1,.container-local-weather-and-travel .travel .useful-links li.column-2{margin-left:0;float:left;padding-left:0;}.container-local-weather-and-travel .travel .useful-links li a{padding-right:0;}.container-local-weather-and-travel .weather .data-feed-now h2,.container-local-weather-and-travel .travel .useful-links .useful-links-header{padding:8px 0 7px;border-bottom:1px solid #DDD;}.container-local-weather-and-travel .weather{width:216px;float:left;margin-left:8px;}.container-local-weather-and-travel .weather img{float:left;margin-right:8px;}.container-local-weather-and-travel .weather .stripes div{margin:8px 0 8px 0;}.container-local-weather-and-travel .weather ul li.wind{display:none;visibility:hidden;}.container-local-weather-and-travel .data-feed-now .next3daysweather{padding-top:1px;}.container-local-weather-and-travel .data-feed-now .next3daysweather h4{float:right;width:151px;font-weight:normal;}.ie .container-local-weather-and-travel .data-feed-now .next3daysweather ul{margin-top:-8px;}.hyperpuff .useful-links{border-top:none;background-color:#ededed;width:464px padding-bottom:4px;}.hyperpuff .useful-links ul{width:224px;padding:4px 0 0 240px;}.hyperpuff .useful-links .column-1{width:216px;margin-left:-240px;padding-left:8px;}.hyperpuff .useful-links .column-2{width:216px;}.hyperpuff .useful-links .column-top h3{padding-top:11px;border-top:1px solid #DDD;}.hyperpuff .useful-links li a{padding-right:0;}.hyperpuff .useful-links li .gvl3-icon{top:11px;}.blq-js .hypertabs{position:relative;overflow:hidden;height:41px;}.blq-js .hypertabs .hypertab-container{overflow:hidden;position:relative;background:#eee;}.blq-js .hypertabs a.hypertab-nav{z-index:1000;position:absolute;top:0;height:100%;}.blq-js .hypertabs ul{margin:0;position:relative;border-top:8px solid #eee;border-left:8px solid #eee;border-right:8px solid #eee;background:#eee;height:33px!important;padding:0;}.hypertabs ul li,.story-body .hypertabs ul li,.story-wide .hypertabs ul li{margin:0 0 8px;padding:0;background:none;}.hypertabs ul li.selected{background-colour:#eee;}.hypertabs ul li.selected a{color:#D1700E;}.blq-js .hypertabs ul li{background-image:none;display:inline;float:left;font-weight:bold;margin:5px 0 5px 0;text-align:center;border-right:1px solid #666;padding:0;}.blq-js .hypertabs li.selected{margin:0 0 0 -1px;padding:5px 0 0 0;height:33px!important;background:#fff;color:#D1700E;border:none;}.blq-js .hypertabs ul li.last-child{border-right:none;}.blq-js .hypertabs ul li a{display:block;padding:0 10px;font-weight:bold;white-space:nowrap;}.blq-js .hypertabs ul li.selected a{color:#D1700E;}.blq-js .hypertabs a.hypertab-prev-disabled{background-position:0 -522px;cursor:default;}.blq-js .hypertabs a.hypertab-next-disabled{background-position:0 -579px;cursor:default;}.blq-js .hypertabs .hypertab-prev{text-indent:-5000px;background-image:url(../../../../../1_4_9/cream/hi/shared/img/story_sprite.png);background-repeat:no-repeat;background-position:0 -7px;width:23px;}.blq-js .hypertabs .hypertab-next{text-indent:-5000px;background-image:url(../../../../../1_4_9/cream/hi/shared/img/story_sprite.png);background-repeat:no-repeat;background-position:0 -64px;width:23px;}.languages{position:relative;clear:both;overflow:hidden;padding:0 8px;background-color:#eee;margin:0 0 24px;}.languages h3{position:relative;display:block;padding:8px 0 11px;}.ie .languages h3 a,.ie7 .languages h3 a{line-height:24px;}.languages h4{position:relative;clear:both;display:block;}.languages h5{position:absolute;left:-50000%;}.languages ul{position:relative;overflow:hidden;float:left;display:inline;width:25%;padding:9px 0 4px;}.languages ul li{float:left;display:inline;width:136px;padding:0 16px 8px 0;}.ie .languages ul li,.ie7 .languages ul li{width:120px;}.languages ul li span.lang-with-image{position:absolute;margin-left:-50000%;float:left;display:inline;padding-right:5px;color:#505050;font-weight:normal;cursor:pointer;z-index:10;}.languages ul li a:hover,.languages ul li a:focus{text-decoration:none;}.languages ul li .lang-sprite{position:relative;cursor:pointer;margin:-4px 0;float:left;display:inline;height:24px;width:30px;background:red;overflow:hidden;text-indent:-5000px;background:transparent url(../img/languages-sprite.gif) no-repeat;}.languages ul li span.lang-albanian{background-position:-16px 0;width:28px;}.languages ul li a:hover span.lang-albanian,.languages ul li a:focus span.lang-albanian{background-position:-16px -24px;}.languages ul li span.lang-arabic{background-position:-59px 0;width:27px;}.languages ul li a:hover span.lang-arabic,.languages ul li a:focus span.lang-arabic{background-position:-59px -24px;}.languages ul li span.lang-azeri{background-position:-101px 0;width:66px;}.languages ul li a:hover span.lang-azeri,.languages ul li a:focus span.lang-azeri{background-position:-101px -24px;}.languages ul li span.lang-bangla{background-position:-182px 0;width:27px;}.languages ul li a:hover span.lang-bangla,.languages ul li a:focus span.lang-bangla{background-position:-182px -24px;}.languages ul li span.lang-bermese{background-position:-224px 0;width:88px;}.languages ul li a:hover span.lang-bermese,.languages ul li a:focus span.lang-bermese{background-position:-224px -24px;}.languages ul li span.lang-carribbean{background-position:-327px 0;width:53px;}.languages ul li a:hover span.lang-carribbean,.languages ul li a:focus span.lang-carribbean{background-position:-327px -24px;}.languages ul li span.lang-chinese{background-position:-395px 0;width:27px;}.languages ul li a:hover span.lang-chinese,.languages ul li a:focus span.lang-chinese{background-position:-395px -24px;}.languages ul li span.lang-french{background-position:-437px 0;width:46px;}.languages ul li a:hover span.lang-french,.languages ul li a:focus span.lang-french{background-position:-437px -24px;}.languages ul li span.lang-hausa{background-position:-498px 0;width:32px;}.languages ul li a:hover span.lang-hausa,.languages ul li a:focus span.lang-hausa{background-position:-498px -24px;}.languages ul li span.lang-hindi{background-position:-545px 0;width:23px;}.languages ul li a:hover span.lang-hindi,.languages ul li a:focus span.lang-hindi{background-position:-545px -24px;}.languages ul li span.lang-indonesian{background-position:-583px 0;width:53px;}.languages ul li a:hover span.lang-indonesian,.languages ul li a:focus span.lang-indonesian{background-position:-583px -24px;}.languages ul li span.lang-kinyarwanda{background-position:-651px 0;width:72px;}.languages ul li a:hover span.lang-kinyarwanda,.languages ul li a:focus span.lang-kinyarwanda{background-position:-651px -24px;}.languages ul li span.lang-kirundi{background-position:-738px 0;width:44px;}.languages ul li a:hover span.lang-kirundi,.languages ul li a:focus span.lang-kirundi{background-position:-738px -24px;}.languages ul li span.lang-kyrgyz{background-position:-797px 0;width:37px;}.languages ul li a:hover span.lang-kyrgyz,.languages ul li a:focus span.lang-kyrgyz{background-position:-797px -24px;}.languages ul li span.lang-macedonian{background-position:-849px 0;width:69px;}.languages ul li a:hover span.lang-macedonian,.languages ul li a:focus span.lang-macedonian{background-position:-849px -24px;}.languages ul li span.lang-nepali{background-position:-933px 0;width:32px;}.languages ul li a:hover span.lang-nepali,.languages ul li a:focus span.lang-nepali{background-position:-933px -24px;}.languages ul li span.lang-pashto{background-position:-980px 0;width:29px;}.languages ul li a:hover span.lang-pashto,.languages ul li a:focus span.lang-pashto{background-position:-980px -24px;}.languages ul li span.lang-persian{background-position:-1024px 0;width:28px;}.languages ul li a:hover span.lang-persian,.languages ul li a:focus span.lang-persian{background-position:-1024px -24px;}.languages ul li span.lang-brasil{background-position:-1067px 0;width:30px;}.languages ul li a:hover span.lang-brasil,.languages ul li a:focus span.lang-brasil{background-position:-1067px -24px;}.languages ul li span.lang-portuguese{background-position:-1112px 0;width:56px;}.languages ul li a:hover span.lang-portuguese,.languages ul li a:focus span.lang-portuguese{background-position:-1112px -24px;}.languages ul li span.lang-russian{background-position:-1183px 0;width:88px;}.languages ul li a:hover span.lang-russian,.languages ul li a:focus span.lang-russian{background-position:-1183px -24px;}.languages ul li span.lang-serbian{background-position:-1287px 0;width:29px;}.languages ul li a:hover span.lang-serbian,.languages ul li a:focus span.lang-serbian{background-position:-1287px -24px;}.languages ul li span.lang-sinhala{background-position:-1331px 0;width:41px;}.languages ul li a:hover span.lang-sinhala,.languages ul li a:focus span.lang-sinhala{background-position:-1331px -24px;}.languages ul li span.lang-somali{background-position:-1387px 0;width:35px;}.languages ul li a:hover span.lang-somali,.languages ul li a:focus span.lang-somali{background-position:-1387px -24px;}.languages ul li span.lang-mundo{background-position:-1437px 0;width:38px;}.languages ul li a:hover span.lang-mundo,.languages ul li a:focus span.lang-mundo{background-position:-1437px -24px;}.languages ul li span.lang-swahili{background-position:-1490px 0;width:38px;}.languages ul li a:hover span.lang-swahili,.languages ul li a:focus span.lang-swahili{background-position:-1490px -24px;}.languages ul li span.lang-tamil{background-position:-1543px 0;width:28px;}.languages ul li a:hover span.lang-tamil,.languages ul li a:focus span.lang-tamil{background-position:-1543px -24px;}.languages ul li span.lang-turkish{background-position:-1586px 0;width:38px;}.languages ul li a:hover span.lang-turkish,.languages ul li a:focus span.lang-turkish{background-position:-1586px -24px;}.languages ul li span.lang-ukrainian{background-position:-1639px 0;width:60px;}.languages ul li a:hover span.lang-ukrainian,.languages ul li a:focus span.lang-ukrainian{background-position:-1639px -24px;}.languages ul li span.lang-urdu{background-position:-1715px 0;width:23px;}.languages ul li a:hover span.lang-urdu,.languages ul li a:focus span.lang-urdu{background-position:-1715px -24px;}.languages ul li span.lang-uzbek{background-position:-1753px 0;width:35px;}.languages ul li a:hover span.lang-uzbek,.languages ul li a:focus span.lang-uzbek{background-position:-1753px -24px;}.languages ul li span.lang-vietnamese{background-position:-1803px 0;width:55px;}.languages ul li a:hover span.lang-vietnamese,.languages ul li a:focus span.lang-vietnamese{background-position:-1803px -24px;}.languages .languages-footer{position:relative;clear:both;border-top:1px solid #d8d8d8;width:100%;padding:11px 0 12px;}.ie .languages .languages-footer{height:1%;}.lead-feature-now{position:relative;clear:both;overflow:hidden;width:624px;padding:8px 0 0;margin-bottom:17px;}.lead-feature-now a:hover,.lead-feature-now a:focus{text-decoration:none;}.lead-feature-now .overlay{position:absolute;width:272px;bottom:0;left:0;z-index:10;cursor:pointer;padding:7px 16px 11px;background:transparent url(../../../../../1_4_9/cream/hi/shared/img/transparencies/rgba-0-0-0-07.png) repeat;}.ie .lead-feature-now .overlay{bottom:-1px;background:black;}.lead-feature-now a:active .overlay{background:#D2700F;}.lead-feature-now .headline{display:block;color:white;position:relative;margin-bottom:7px;}.lead-feature-now a:hover .headline,.lead-feature-now a:focus .headline{text-decoration:underline;}.lead-feature-now .summary{display:block;color:white;font-weight:normal;}.lead-feature-now .story img{display:block;position:relative;}.lead-feature-now .gvl3-icon-wrapper{top:8px;left:0;}.live-event-promo{position:relative;clear:both;overflow:hidden;margin:8px 0 16px;}.live-event-promo h2{position:absolute;left:-5000%;}.live-event-promo .live-event-promo-action{display:block;overflow:hidden;position:relative;z-index:10;padding-bottom:400px;margin:0 0 -400px;}.live-event-promo a.live-event-promo-action:hover,.live-event-promo a.live-event-promo-action:focus{text-decoration:none;}.live-event-promo .live-event-promo-title{float:left;display:inline;margin-right:8px;padding:0 8px 0 66px;line-height:32px;color:white;font-weight:bold;font-size:20px;background:#D60000 url(../../../../../1_4_9/cream/hi/shared/img/live-icon-32.gif) no-repeat 2px center;}.live-event-promo a.live-event-promo-action:hover .live-event-promo-title,.live-event-promo a.live-event-promo-action:focus .live-event-promo-title{text-decoration:underline;}.live-event-promo .live-event-promo-video{display:block;padding:7px 8px 1px;line-height:24px;color:white;font-weight:bold;font-size:13px;background:#333;}.live-event-promo .live-event-promo-video strong{position:relative;top:4px;float:left;display:inline;padding-right:4px;color:white;text-transform:uppercase;}.live-event-promo .live-event-promo-video .gvl3-icon{position:relative;top:3px;float:left;display:inline;}.live-event-promo ol{background:#505050;}.live-event-promo ol *{color:white;}.live-event-promo ol li{padding:4px 8px;}.live-event-promo ol li img,.live-event-promo ol li .icon-front,.live-event-promo ol li .icon-back{display:none;}.live-event-promo li .item-info{font-weight:bold;}.story-body .live-event-promo{clear:both;display:inline;float:right;margin:0 -160px 16px 16px;position:relative;width:304px;}.bbccom_slot_xxl .story-body .live-event-promo{margin-right:0;}.story-body .live-event-promo .live-event-promo-action{position:absolute;height:100%;width:100%;top:0;left:0;padding-bottom:0;margin-bottom:0;}.story-body .live-event-promo .live-event-promo-action,.story-body .live-event-promo .live-event-promo-action *{cursor:pointer;}.story-body .live-event-promo .live-event-promo-title{float:none;display:block;margin-right:0;}.story-body .live-event-promo .live-event-promo-video{position:absolute;width:100%;bottom:0;left:0;padding-top:3px;}.story-body .live-event-promo ol{position:relative;padding:32px 0 28px;margin:0;}.story-body .live-event-promo ol *{font-size:13px;line-height:16px;}.story-body .live-event-promo ol li{padding:8px;margin:0;}.story-body .live-event-promo ol li p{margin:0;}.livestats{clear:both;position:relative;overflow:hidden;width:320px;margin:0 0 16px;padding:8px 8px 8px;background:#ededed;}.bbccom_slot_xxl .livestats{width:480px;}.livestats .livestats-header{font-size:24px;font-size:1.846em;font-weight:bold;letter-spacing:-0.05em;line-height:24px;text-indent:-1px;padding:0;}.livestats .tab{padding:9px 16px 10px 0;position:relative;background:transparent;z-index:100;font-size:1.231em;font-weight:bold;line-height:20px;}.livestats .panel{position:relative;overflow:hidden;width:320px;background:#fff;}.bbccom_slot_xxl .livestats .panel{width:480px;}.livestats .panel li{clear:both;display:block;overflow:hidden;border-top:1px solid #eee;position:relative;zoom:1;}.livestats .panel li a{position:relative;overflow:visible;float:left;display:inline;width:283px;padding:11px 29px 12px 8px;}.bbccom_slot_xxl .livestats .panel li a{width:442px;}.ie7 .livestats .panel li a,.ie .livestats .panel li a{padding-bottom:8px;height:1%;}.livestats .panel li.first-child{border-top:none;}.livestats .panel li.first-child a{padding-top:12px;}.livestats .has-icon-listen{text-indent:25px;}.livestats .has-icon-watch{text-indent:20px;}.livestats .gvl3-icon{position:absolute;top:11px;left:9px;}.livestats .livestats-icon{position:absolute;right:7px;top:50%;margin-top:-18px;height:36px;width:14px;cursor:pointer;overflow:hidden;text-indent:-50000px;background:#a9a9a9 url(../img/livestats-sprite-ko.png) repeat;-webkit-transition:all 1s ease-out;}.ie .livestats .livestats-icon{background-color:transparent;background-image:url(../img/livestats-sprite.gif);}.livestats a:hover .livestats-icon,.livestats a:focus .livestats-icon{background-color:#d1700e;-webkit-transition:.2s ease-in;}.livestats .livestats-monday{background-position:1px -45px;right:3px;}.ie .livestats a:hover .livestats-monday,.ie .livestats a:focus .livestats-monday{background-position:-335px -45px;}.livestats .livestats-tuesday{background-position:-31px -46px;right:3px;}.ie .livestats a:hover .livestats-tuesday,.ie .livestats a:focus .livestats-tuesday{background-position:-367px -46px;}.livestats .livestats-wednesday{background-position:-63px -45px;right:3px;}.ie .livestats a:hover .livestats-wednesday,.ie .livestats a:focus .livestats-wednesday{background-position:-399px -45px;}.livestats .livestats-thursday{background-position:-95px -45px;right:3px;}.ie .livestats a:hover .livestats-thursday,.ie .livestats a:focus .livestats-thursday{background-position:-431px -45px;}.livestats .livestats-friday{background-position:-127px -45px;right:3px;}.ie .livestats a:hover .livestats-friday,.ie .livestats a:focus .livestats-friday{background-position:-463px -45px;}.livestats .livestats-saturday{background-position:-159px -45px;right:3px;}.ie .livestats a:hover .livestats-saturday,.ie .livestats a:focus .livestats-saturday{background-position:-495px -45px;}.livestats .livestats-sunday{background-position:-191px -46px;right:3px;}.ie .livestats a:hover .livestats-sunday,.ie .livestats a:focus .livestats-sunday{background-position:-527px -46px;}.livestats .livestats-1{background-position:1px -5px;}.ie .livestats a:hover .livestats-1,.ie .livestats a:focus .livestats-1{background-position:-335px -5px;}.livestats .livestats-2{background-position:-32px -5px;}.ie .livestats a:hover .livestats-2,.ie .livestats a:focus .livestats-2{background-position:-368px -5px;}.livestats .livestats-3{background-position:-64px -5px;}.ie .livestats a:hover .livestats-3,.ie .livestats a:focus .livestats-3{background-position:-400px -5px;}.livestats .livestats-4{background-position:-96px -5px;}.ie .livestats a:hover .livestats-4,.ie .livestats a:focus .livestats-4{background-position:-432px -5px;}.livestats .livestats-5{background-position:-127px -5px;}.ie .livestats a:hover .livestats-5,.ie .livestats a:focus .livestats-5{background-position:-463px -5px;}.livestats .livestats-6{background-position:-160px -5px;}.ie .livestats a:hover .livestats-6,.ie .livestats a:focus .livestats-6{background-position:-496px -5px;}.livestats .livestats-7{background-position:-192px -5px;}.ie .livestats a:hover .livestats-7,.ie .livestats a:focus .livestats-7{background-position:-528px -5px;}.livestats .livestats-8{background-position:-224px -5px;}.ie .livestats a:hover .livestats-8,.ie .livestats a:focus .livestats-8{background-position:-560px -5px;}.livestats .livestats-9{background-position:-256px -5px;}.ie .livestats a:hover .livestats-9,.ie .livestats a:focus .livestats-9{background-position:-592px -5px;}.livestats .livestats-10{width:26px;background-position:-287px -5px;}.ie .livestats a:hover .livestats-10,.ie .livestats a:focus .livestats-10{background-position:-623px -5px;}.blq-js .livestats-tabbed .livestats-header{padding-bottom:8px;}.blq-js .livestats-tabbed .tab{float:left;display:inline;top:0;padding:7px 16px 6px 8px;cursor:pointer;}.blq-js .ie .livestats-tabbed .tab,.blq-js .ie7 .livestats-tabbed .tab{padding-top:9px;padding-bottom:8px;}.blq-js .livestats-tabbed void{background:#fff;}.blq-js .livestats-tabbed void a{color:#d2700f;}.blq-js .livestats-tabbed .panel{position:absolute;float:right;display:inline;clear:right;right:-500%;margin-right:0;margin-top:32px;opacity:0;}.blq-js .ie .livestats-tabbed .panel,.blq-js .ie7 .livestats-tabbed .panel{clear:none;}.blq-js .livestats-tabbed void{position:relative;margin-right:-320px;right:320px;opacity:1;-webkit-transition:opacity .2s ease-in;}.blq-js .bbccom_slot_xxl .livestats-tabbed void{margin-right:-480px;right:480px;}.blq-js .ie .livestats-tabbed void,.blq-js .ie7 .livestats-tabbed void{margin-top:32px;}.livestats .has-icon-watch .gvl3-icon-watch{width:15px;}.livestats .has-icon-watch .gvl3-icon-watch{background-position:-1301px -0px;}.livestats .has-icon-watch a:active .gvl3-icon-watch{background-position:-1301px -16px;}.container-promo-best{clear:both;position:relative;}.container-local{clear:both;overflow:hidden;margin:0 0 16px 0;padding:0 8px 0 8px!important;background:#ededed;}.local-av{overflow:visible;}.local-av-header{font-size:24px;font-weight:bold;letter-spacing:-1px;line-height:24px;margin:8px 0 8px 0;text-rendering:optimizelegibility;}.local-av-header a{line-height:24px;}.ie .local-av .local-av-header{padding-top:8px;}.local-av h3{font-size:13px;font-weight:normal;line-height:16px;text-transform:uppercase;text-rendering:optimizelegibility;}.local-av .program-time{font-weight:bold;}.local-av .local-radio-now .program-time{padding:2px 0 0 0;display:inline-block;width:90px;}.container-local .feature-item{position:relative;display:block;clear:both;margin:8px -8px 7px;}.container-local .feature-item *{cursor:pointer;}.container-local .feature-item .overlay{position:absolute;width:320px;bottom:0;left:0;z-index:10;cursor:pointer;padding:8px 8px 7px;background:transparent url(../../../../../1_4_9/cream/hi/shared/img/transparencies/rgba-0-0-0-07.png) repeat;}.ie .container-local .feature-item .overlay{background:black;}.container-local a.feature-item:active .overlay{background:#D2700F;}.container-local a.feature-item .summary{font-weight:normal;}.container-local a.feature-item:hover,.container-local a.feature-item:focus,.container-local a.feature-item:hover .summary,.container-local a.feature-item:focus .summary{text-decoration:none;}.container-local a.feature-item:hover .headline,.container-local a.feature-item:focus .headline{text-decoration:underline;}.container-local .feature-item .overlay strong,.container-local .feature-item .overlay .summary{color:#fff;display:block;}.container-local a.feature-item .overlay strong{position:relative;float:left;display:inline;clear:both;font-weight:bold;padding-bottom:2px;}.container-local a.feature-item .overlay strong.has-icon-podcast{padding-left:15px;}.container-local a.feature-item .overlay .gvl3-icon{position:absolute;top:0;left:0;}.container-local .feature-item img{display:block;}.local-radio-now{clear:both;padding-top:7px;border-top:1px solid #ddd;}.local-radio-now h3{position:absolute;left:-5000px;}.local-radio-now .local-now{position:relative;overflow:visible;padding-bottom:10px;padding-right:4px;font-size:1.231em;font-weight:bold;line-height:16px;text-rendering:optimizelegibility;}.local-av .local-radio-now .gvl3-icon{top:2px;}.local-radio-next{margin-bottom:4px;}.local-radio-next h3{display:inline;padding-right:3px;}.local-av .local-radio-next .program-time{padding:2px 0 0 0;display:inline-block;width:90px;}.local-radio-services{position:relative;clear:both;overflow:hidden;padding-top:2px;padding-bottom:8px;}.local-radio-services h3{position:absolute;left:-5000px;}.local-radio-services li{padding-left:9px;margin-left:7px;float:left;display:inline;background:url(../img/nav-divider.png) center left no-repeat;}.local-radio-services li:first-child{padding-left:0;margin-left:0;background:none;}.ie .local-radio-services{height:1%;}.local-av-regional-variations{padding:8px 0;border-top:1px solid #ddd;}.local-av-alt-language{clear:both;padding-top:0;border-top:1px solid #ddd;}.local-av-alt-language .local-now{position:relative;float:left;display:inline;overflow:visible;}.local-av-alt-language .local-now .gvl3-icon{position:absolute;top:0;left:0;}.local-av-tv{clear:both;margin-top:1px;padding-bottom:8px;border-top:1px solid #ddd;width:100%;}.local-av-tv-only{border-top:none;}.local-av-tv .local-av-header{margin-bottom:2px;}.local-tv-now h3{position:absolute;float:left;left:-5000px;}.local-tv-now .local-now{position:relative;float:left;display:inline;clear:both;overflow:visible;padding-left:24px;margin-top:4px;padding-top:1px;}.local-radio-podcast{clear:both;overflow:hidden;padding-bottom:8px;border-top:1px solid #ddd;padding-top:8px;}.local-radio-podcast .local-podcast{position:relative;float:left;display:inline;overflow:visible;padding-left:0;padding-left:18px;}.local-av .local-radio-podcast .gvl3-icon{position:absolute;top:1px;left:0;}.local-av .local-tv-now .gvl3-icon{position:absolute;top:1px;left:0;}.local-av .gvl3-icon{position:relative;top:0;left:0;float:left;}.local-av-alt-language .gvl3-icon-boxedlive{top:1px;}.local-av .has-icon-boxedwatch,.local-av .has-icon-live,.local-av .has-icon-boxedlive{text-indent:0;}.local-av-times{clear:left;display:block;padding-top:1px;}.local-weather .next3daysweather .stripes{background-color:#fff;float:left;padding:0;}.local-weather .next3daysweather .stripes li{font-size:13px;}.local-weather .next3daysweather .stripes div.time{width:84px;float:left;margin-right:20px;}.local-weather .next3daysweather .stripes img{margin:0 0 4px 0;}.local-weather .next3daysweather a{color:#369;}.local-weather .next3daysweather a:hover{color:#0D3059;}.local-weather .next3daysweather .stripes h3{padding:0 0 4px;font-weight:normal;}.local-weather .next3daysweather p{width:285px;margin:7px 0 10px 0;font-size:1.2em;}.local-weather .next3daysweather p a{font-size:.9em;}.local-weather .next3daysweather .stripes .c3{margin-right:10px!important;}.local-weather .next3daysweather .clear{clear:both;}.local-weather{background-color:#EDEDED;clear:both;float:left;padding:8px;position:relative;width:320px;}.ie .local-weather,.ie7 .local-weather{overflow:hidden;margin-bottom:0;}.weather-dropdown ul{position:relative;padding-top:24px;overflow:hidden;background-color:#FFF;height:0;margin-bottom:8px;padding-bottom:8px;}.weather-dropdown ul li{cursor:pointer;margin:8px 8px 0;}.ie .weather-dropdown ul,.ie7 .weather-dropdown ul{padding-top:32px;padding-bottom:0;}.weather-dropdown ul li a{display:block;}.weather-dropdown ul li.selected{top:0;position:absolute;font-weight:bold;width:100%;left:0;}.weather-dropdown ul li.selected a{color:black;}.local-weather .tabbed void{background-color:#FFF;}.local-weather .tabbed div.panel{display:none;clear:both;background-color:#FFF;padding:8px;}.local-weather .tabbed void{display:block;}.local-weather .stripes .wind{display:none;}.local-weather .tabbed div.weather-video{padding-top:16px;}.ie7 .local-weather .tabbed ul{overflow:auto;}.ie .local-weather .tabbed ul{overflow:hidden;height:1%;}.local-weather h2{padding-top:8px;padding-bottom:8px;}#local-weather-title{margin-top:8px;line-height:24px;font-size:24px;}.local-weather-loc-id li{font-size:16px;font-weight:bold;line-height:18px;}.local-weather .tabbed li.tab{cursor:pointer;display:inline;float:left;margin-right:8px;padding:9px 16px 10px 8px;}.ie .local-weather .tabbed li.tab{overflow:auto;height:1%;margin-bottom:0;}.local-weather li.tab{font-size:16px;font-weight:bold;line-height:18px;}.local-weather-links{padding:8px 0 8px 0;}.local-weather .weathervideo{border:1px solid #ccc;border-top:0;padding:1px 10px 10px;}.o .local-weather .weathervideo{border:none;}.local-weather .weathervideo div{min-height:181px;}.local-weather .emp{margin:0 auto;display:block;margin:16px auto 0;width:256px;margin-bottom:8px;overflow:auto;position:relative;}.ie .local-weather .emp,.ie .local-weather .tabbed div.weather-video{height:1%;}.weather-dropdown ul li.selected{background:url("../img/arrow_foldout.png") no-repeat scroll 294px 1px transparent;}.ie .weather-dropdown ul li.selected{background:url("../img/arrow_foldout.gif") no-repeat scroll 294px 1px transparent;}.weather-dropdown void li.selected{background-position:-93px 2px;}.ie .local-weather .tabbed div.panel{width:304px;}.ie .local-weather .next3daysweather .stripes .c3{margin-right:5px!important;}.ie .weather-dropdown{position:relative;}.ie .weather-dropdown ul{position:static;}.ie .weather-dropdown ul li{height:1%;}.local-travel-box{background-color:#EDEDED;margin:0;padding:0 0 8px 0;}.local-travel-box h3{font-weight:bold;font-size:15px;padding-top:10px;line-height:16px;}.local-travel-box p a{margin-left:8px;font-size:12px;font-weight:normal;}.local-travel-incident{background-color:#FFF;margin:0;padding-bottom:8px;padding-right:8px;}.ie .local-travel-incident{height:1%;}.local-travel-box .local-travel-incident h3{color:#D07128;font-size:15px;line-height:26px;padding-top:0;margin-left:8px;}.local-travel-incident p{color:#505050;font-size:15px;font-weight:bold;margin-left:40px;}.local-travel-incident p a{color:#1f4f82;font-size:12px;line-height:14px;margin-left:0;font-weight:normal;}.road-incident{background:url("../img/roadicon.gif") 8px 27px no-repeat #fff;width:24px height:24px;}.local-traffic-links{font-weight:normal;font-size:13px;line-height:16px;display:block;}.local-journey-planner{font-weight:bold;padding-top:8px;}#travel-key-info{margin:-10px 0 12px 0;}.market-data{position:relative;overflow:hidden;clear:both;background-color:#ededed;padding:0 8px;margin:0 0 16px;width:320px;}.market-data h2{position:relative;display:inline;float:left;padding:8px 0 8px;z-index:10;}.market-data h2 a{line-height:24px;}.market-data .mkt-last-updated{position:relative;padding:16px 0 0;text-align:right;color:#505050;}.market-data .mkt-table{position:relative;clear:both;width:100%;}.market-data .mkt-table .table-headers{display:none;}.market-data .mkt-table td{background-color:#fff;color:#505050;padding:7px 8px 8px;border-bottom:1px solid #ededed;text-align:right;width:1%;}.market-data .mkt-table td.mkt-index{width:96%;text-align:left;}.market-data .mkt-table td.mkt-percent{font-weight:bold;}.market-data .mkt-table .mkt-trend-image{display:block;width:9px;height:7px;}.market-data .mkt-table .mkt-trend{text-align:left;text-indent:-9999px;}.market-data .mkt-table td{white-space:nowrap;vertical-align:middle;}.market-data .mkt-table .mkt-up .mkt-trend-image{background:transparent url(../../../../../1_4_9/cream/hi/shared/img/market-data-up.png) 0 0 no-repeat;}.market-data .mkt-table .mkt-down .mkt-trend-image{background:transparent url(../../../../../1_4_9/cream/hi/shared/img/market-data-down.png) 0 0 no-repeat;}.ie .market-data .mkt-table .mkt-up .mkt-trend-image{background:transparent url(../../../../../1_4_9/cream/hi/shared/img/market-data-up.gif) 0 0 no-repeat;}.ie .market-data .mkt-table .mkt-down .mkt-trend-image{background:transparent url(../../../../../1_4_9/cream/hi/shared/img/market-data-down.gif) 0 0 no-repeat;}.market-data .mkt-table .mkt-nochange .mkt-trend-image,.ie .market-data .mkt-table .mkt-nochange .mkt-trend-image{background:none;}.market-data .mkt-footer{padding:11px 0 12px;position:relative;clear:both;overflow:visible;width:100%;}.market-data .mkt-footer p{position:relative;overflow:hidden;display:inline;}.market-data .mkt-footer .mkt-ticker{float:none;display:inline;clear:both;}.market-data .mkt-footer .mkt-data-delayed{float:right;display:inline;text-align:right;color:#505050;padding-top:2px;}.ie .market-data .mkt-footer .mkt-data-delayed,.ie7 .market-data .mkt-footer .mkt-data-delayed{margin-top:-16px;}.market-data .mkt-footer .gvl3-icon{float:left;display:inline;}#marketdata_v4 .livestats{background:none repeat scroll 0 0 #EDEDED;clear:both;margin:0;overflow:hidden;padding:0;position:relative;width:613px;border-top:8px solid #EDEDED;border-left:8px solid #EDEDED;border-right:8px solid #EDEDED;}#marketdata_v4 .livestats-tabbed void{background:none repeat scroll 0 0 #FFF;}#marketdata_v4 .livestats-tabbed .tab{cursor:pointer;font-size:1em;display:inline;float:left;padding:0 3px 7px 3px;top:0;margin:0;border-right:5px solid #EDEDED;}#marketdata_v4 .livestats-tabbed .tablast{border-right:0;}#marketdata_v4 .livestats-tabbed void a{color:#D2700F;}#marketdata_v4 .border-bottom td{border-bottom:1px solid #DDD;}#marketdata_v4 .border-bottom-dark{border-bottom:1px solid #999;}#marketdata_v4 .statsclick{font-weight:normal;}#marketdata_v4 .statsclick b{color:#1F4F82;}#marketdata_v4 .instructions{padding:10px 0 5px 0;}#marketdata_v4 .statslo{color:#C00;}#marketdata_v4 .statshi{color:#3C8100;}.marketdata_v4_container-now{margin:0 11px 16px 0;width:629px;}#main-content .layout-block-c{border-top:8px solid #eee;}.playlist{clear:both;position:relative;float:left;display:inline;margin:0;padding:0 8px 12px 8px;width:960px;background:#eee;}.iplayer{border-right:none;border-left:none;padding:16px 8px 10px 8px;background:#000;}.iplayer h2,.iplayer li a{color:#ec43a0;}.iplayer h2,.iplayer li span.date{color:#fff;font-weight:normal;}.playlist h2{display:inline;float:left;margin:0 8px 0 0;padding:0;position:relative;width:144px;}.playlist ul{position:relative;overflow:hidden;float:left;display:inline;}.playlist li{position:relative;display:inline;float:left;margin:0 0 0 16px;width:144px;padding:0;}.ie .playlist li{margin:0 0 0 14px;}.playlist img{left:0;position:absolute;top:0;}.playlist li a{position:relative;display:block;padding:84px 0 0 0;margin:0;width:146px;}.playlist li h3 a .gvl3-icon-wrapper{position:absolute;top:0;left:0;z-index:10;}.hyper-container-title .playlist li h3{margin:0;padding:0;}.more-galleries{clear:both;position:relative;float:left;display:inline;background:#eee;margin:0;padding:16px 0 0 0;width:100%;margin:0 0 16px 0;}.more-galleries h2{display:inline;float:left;margin:3px 0 0 16px;padding:0;position:relative;width:128px;}.more-galleries ul{float:right;position:relative;overflow:hidden;width:816px;}.more-galleries li{position:relative;display:inline;float:left;margin:0 0 0 14px;width:146px;padding:0;height:160px;overflow:hidden;}.more-galleries img{left:0;position:absolute;top:0;}.more-galleries li a{position:relative;display:block;padding:83px 0 0 0;margin:0;width:126px;}.more-galleries li h3 a span{position:absolute;top:0;left:0;display:block;text-indent:-5000px;background-image:url(../img/story_sprite.gif);background-repeat:no-repeat;background-position:-4200px 0;height:32px;width:32px;z-index:10;}.most-watched-list{padding:0;margin-left:16px;margin-bottom:0;}.most-watched-list h2{padding:8px;}.most-watched-list .most-pop-carousel{margin:0;width:320px!important;}.most-watched-list .most-pop-carousel .carousel-light{position:relative;padding-bottom:4px;}.most-watched-list .most-pop-carousel .carousel-light .carousel-window{height:652px!important;background:#ededed;}.bbccom_companion .most-watched-list .most-pop-carousel .carousel-light .carousel-window{height:722px!important;}.bbccom_slot_mpu .most-watched-list .most-pop-carousel .carousel-light .carousel-window{height:216px!important;}.most-watched-list .most-pop-carousel .carousel-nav{position:absolute;width:320px!important;height:24px!important;overflow:hidden;z-index:50;background:transparent url(../../../../../1_4_9/cream/hi/shared/img/most_watched.png) 0 0 no-repeat;}.most-watched-list .most-pop-carousel .carousel-nav span{display:none;}.most-watched-list .most-pop-carousel .carousel-nav .carousel-label{display:block;position:absolute;width:1px;overflow:hidden;left:-50000%;}.most-watched-list .most-pop-carousel .carousel-prev{top:0;background-position:0 0;}.most-watched-list .most-pop-carousel .carousel-prev-disabled{background-position:0 -24px;display:none;}.most-watched-list .most-pop-carousel .carousel-next{bottom:0;background-position:0 -72px;}.most-watched-list .most-pop-carousel .carousel-next-disabled{background-position:0 -48px;display:none;}.most-watched-list ol{padding-top:8px;}.most-watched-list li{position:relative;clear:both;overflow:hidden;height:72px;width:152px;padding:0 40px 0 128px;top:-4px;}.most-watched-list li img{position:absolute;top:3px;left:8px;width:112px;height:63px;}.most-watched-list li .gvl3-icon-wrapper{position:absolute;top:3px;left:8px;z-index:30;}.most-watched-list li .gvl3-icon-wrapper .gvl3-icon{position:relative;top:0;left:0;}.most-watched-list li .livestats-icon{top:-11px;margin-top:0;background-image:url(../img/sprite_most_watched_ko.png);}.ie .most-watched-list li .livestats-icon{background-color:transparent;background-image:url(../img/sprite_most_watched.gif);}.most-watched-list li.ol1 .livestats-icon{background-position:1px 0;}.ie .most-watched-list li.ol1 a:hover .livestats-icon,.ie .most-watched-list li.ol1 a:focus .livestats-icon{background-position:-560px -0px;}.most-watched-list li.ol2 .livestats-icon{background-position:-32px 0;}.ie .most-watched-list li.ol2 a:hover .livestats-icon,.ie .most-watched-list li.ol2 a:focus .livestats-icon{background-position:-593px -0px;}.most-watched-list li.ol3 .livestats-icon{background-position:-64px 0;}.ie .most-watched-list li.ol3 a:hover .livestats-icon,.ie .most-watched-list li.ol3 a:focus .livestats-icon{background-position:-625px -0px;}.most-watched-list li.ol4 .livestats-icon{background-position:-95px 0;}.ie .most-watched-list li.ol4 a:hover .livestats-icon,.ie .most-watched-list li.ol4 a:focus .livestats-icon{background-position:-656px -0px;}.most-watched-list li.ol5 .livestats-icon{background-position:-127px 0;}.ie .most-watched-list li.ol5 a:hover .livestats-icon,.ie .most-watched-list li.ol5 a:focus .livestats-icon{background-position:-688px -0px;}.most-watched-list li.ol6 .livestats-icon{background-position:-159px 0;}.ie .most-watched-list li.ol6 a:hover .livestats-icon,.ie .most-watched-list li.ol6 a:focus .livestats-icon{background-position:-720px -0px;}.most-watched-list li.ol7 .livestats-icon{background-position:-192px 0;}.ie .most-watched-list li.ol7 a:hover .livestats-icon,.ie .most-watched-list li.ol7 a:focus .livestats-icon{background-position:-753px -0px;}.most-watched-list li.ol8 .livestats-icon{background-position:-223px 0;}.ie .most-watched-list li.ol8 a:hover .livestats-icon,.ie .most-watched-list li.ol8 a:focus .livestats-icon{background-position:-784px -0px;}.most-watched-list li.ol9 .livestats-icon{background-position:-254px 0;}.ie .most-watched-list li.ol9 a:hover .livestats-icon,.ie .most-watched-list li.ol9 a:focus .livestats-icon{background-position:-815px -0px;}.most-watched-list li.ol10 .livestats-icon{width:26px;background-position:-287px 0;}.ie .most-watched-list li.ol10 a:hover .livestats-icon,.ie .most-watched-list li.ol10 a:focus .livestats-icon{background-position:-848px -0px;}.most-watched-list li.ol11 .livestats-icon{width:26px;background-position:-331px 0;}.ie .most-watched-list li.ol11 a:hover .livestats-icon,.ie .most-watched-list li.ol11 a:focus .livestats-icon{background-position:-892px -0px;}.most-watched-list li.ol12 .livestats-icon{width:26px;background-position:-376px 0;}.ie .most-watched-list li.ol12 a:hover .livestats-icon,.ie .most-watched-list li.ol12 a:focus .livestats-icon{background-position:-937px -0px;}.most-watched-list li.ol13 .livestats-icon{width:26px;background-position:-422px 0;}.ie .most-watched-list li.ol13 a:hover .livestats-icon,.ie .most-watched-list li.ol13 a:focus .livestats-icon{background-position:-983px -0px;}.most-watched-list li.ol14 .livestats-icon{width:26px;background-position:-465px 0;}.ie .most-watched-list li.ol14 a:hover .livestats-icon,.ie .most-watched-list li.ol14 a:focus .livestats-icon{background-position:-1026px -0px;}.most-watched-list li.ol15 .livestats-icon{width:26px;background-position:-511px 0;}.ie .most-watched-list li.ol15 a:hover .livestats-icon,.ie .most-watched-list li.ol15 a:focus .livestats-icon{background-position:-1072px -0px;}.newstracker-list{clear:both;overflow:hidden;padding-bottom:7px;}.newstracker-list h3{border-bottom:1px solid #CCC;border-top:1px solid #CCC;clear:both;margin:0 0 8px;padding:8px 0 7px;width:100%;}.newstracker-list ul li{padding:3px 0 13px 0;width:223px;clear:both;float:left;display:inline;position:relative;background:none;}.newstracker-list ul li.even{float:right;display:inline;clear:none;}.newstracker-list span{font-weight:normal;display:block;padding:0;}.newstracker-list a strong{color:#1F527B;display:block;}.newstracker-list a{font-weight:bold;display:block;padding:0;background-repeat:no-repeat;}.newstracker-list p{clear:both;margin:0 0 16px 0;}.newstracker-list .di{clear:both;padding:11px 0 13px 0;}.other-site-content{float:left;display:inline;position:relative;padding-top:16px;margin-bottom:16px;}.ie .other-site-content,.ie7 .other-site-content{margin-top:4px;}.full-width-other-site .other-site-content{background-color:#EDEDED;width:624px;padding:0;}.other-site-content-header{padding:8px 0;}.other-site-content .wide-content-group .first-other-promo{background-color:#505050;padding:16px 0 4px;padding-bottom:20px;min-height:60px;}.ie7 .other-site-content .wide-content-group .first-other-promo{min-height:auto;}.other-site-content .wide-content-group .first-other-promo h3 img{display:inline;float:left;margin-top:-16px;position:relative;margin-right:16px;border-right:1px solid #fff;}.other-site-content .wide-content-group .first-other-promo h3 a{color:#fff;}.other-site-content .wide-content-group .first-other-promo p{color:#fff;margin:4px 8px 8px 0;}.other-site-content .wide-content-group .other-promo{padding:0 8px 8px 0;background-color:#EDEDED;}.other-site-content .wide-content-group .second-promo{border-top:1px solid #fff;padding-top:9px;}.other-site-content .wide-content-group{padding:0;overflow:auto;height:100%;}.other-site-content .single-story{background-color:#505050;}.other-site-content .stacked-content-group .first-other-promo{background-color:#505050;padding:0 0 4px;}.other-site-content .stacked-content-group .first-other-promo h3 img{display:block;position:relative;z-index:2;}.other-site-content .stacked-content-group .first-other-promo h3 a{color:#fff;margin:0;padding-left:0;}.other-site-content .stacked-content-group .first-other-promo p{color:#fff;margin:4px 8px 8px 16px;}.other-site-content .stacked-content-group li.other-promo{padding:0 8px 8px 16px;}.other-site-content .stacked-content-group li.second-promo{border-top:1px solid #fff;padding-top:8px;}.other-site-content .stacked-content-group{width:304px;}.container-other-site-promotion .first-group .stacked-content-group{margin-right:16px;}.other-site-content .stacked-content-group h3{padding-left:8px;margin-left:0;}.other-site-content .stacked-content-group h3 img{border-bottom:1px solid #FFF;margin-bottom:8px;margin-left:-8px;padding-left:0;margin-top:-17px;}.other-site-content .stacked-content-group .first-other-promo p{margin-left:8px;}.other-site-content .stacked-content-group li.other-promo{background-color:#F0F0F0;padding-left:8px;}.other-site-content .stacked-overlay-other-site-promotion .first-other-promo{background:none;padding:1px 0 0 0;position:relative;}.stacked-overlay-other-site-promotion .first-other-promo .overlay{position:absolute;width:288px;bottom:0;left:0;z-index:10;cursor:pointer;padding:9px 8px 7px;background:transparent url(../../../../../1_4_9/cream/hi/shared/img/transparencies/rgba-0-0-0-07.png) repeat;}.ie .stacked-overlay-other-site-promotion .first-other-promo .overlay{background:black;}.stacked-overlay-other-site-promotion .first-other-promo a:active .overlay{background:#D2700F;}.stacked-overlay-other-site-promotion .first-other-promo h3 a.story .summary{font-weight:normal;}.stacked-overlay-other-site-promotion .first-other-promo h3 .overlay strong,.stacked-overlay-other-site-promotion .first-other-promo h3 a.story .summary{color:#fff;display:block;}.stacked-overlay-other-site-promotion .first-other-promo a:hover,.stacked-overlay-other-site-promotion .first-other-promo a:focus{text-decoration:none;}.stacked-overlay-other-site-promotion .first-other-promo a:hover .headline,.stacked-overlay-other-site-promotion .first-other-promo a:focus .headline{text-decoration:underline;}.stacked-overlay-other-site-promotion .first-other-promo a:hover .summary,.stacked-overlay-other-site-promotion .first-other-promo a:focus .summary{text-decoration:none;}.stacked-overlay-other-site-promotion .first-other-promo h3 a .overlay strong{font-weight:bold;padding-bottom:2px;}.other-site-content .stacked-overlay-other-site-promotion .first-other-promo h3 img{margin-bottom:0;border-bottom:none;}.other-site-content .stacked-overlay-other-site-promotion li.second-promo{border-top:none;}.ie .other-site-content .stacked-overlay-other-site-promotion{margin-bottom:0;}.ie .other-site-content .stacked-overlay-other-site-promotion .overlay{bottom:-1px;}.ie7 .other-site-content .stacked-overlay-other-site-promotion .second-promo,.ie .other-site-content .stacked-overlay-other-site-promotion .second-promo{margin-top:-3px;}.ie7 .stacked-overlay-other-site-promotion li.first-other-promo,.ie .stacked-overlay-other-site-promotion li.first-other-promo{height:1%;}.other-top-stories{position:relative;overflow:auto;width:624px;margin:0 0 15px 0;padding-top:0;border-top:1px solid #ddd;clear:both;}.other-top-stories .other-top-stories-header{position:relative;margin-bottom:0;padding:7px 0 8px;border-bottom:1px solid #ddd;}.other-top-stories-stories{position:relative;overflow:hidden;padding-left:320px;width:304px;margin:4px 0 4px;}.other-top-stories-stories li{position:relative;padding-top:7px;margin-bottom:1px;}.other-top-stories .gvl3-icon{position:absolute;top:6px;left:0;}.other-top-stories .with-summary .gvl3-icon{top:6px;}.other-top-stories-stories .column-1{position:relative;float:left;clear:left;display:inline;overflow:hidden;width:304px;margin-left:-320px;}.other-top-stories-stories .column-2{clear:none;position:relative;width:304px;}.ie .other-top-stories-stories .column-2{float:right;clear:right;}.other-top-stories-stories .with-summary{margin-top:-1px;padding-bottom:6px;padding-top:6px;margin-bottom:7px;border-top:1px solid #ddd;}.other-top-stories-stories p{position:relative;}.other-top-stories-stories .with-summary p{margin-top:4px;}.other-top-stories-stories h2 a.from-external-source,.other-top-stories-stories h3 a.from-external-source{display:block;text-indent:0;width:250px;margin-left:0;}.container-hyper-topic-cluster .hyper-container-title{position:relative;border-top:none;margin:0;padding-top:0;width:624px;}.container-hyper-topic-cluster .hyper-container-title .hyper-depth-header{border-top:1px solid #DDD;margin-bottom:0;padding:6px 0 8px;position:relative;}.ie .other-top-stories .other-top-stories-header a,.ie .container-hyper-topic-cluster .hyper-container-title .hyper-depth-header a,.ie7 .container-hyper-topic-cluster .hyper-container-title .hyper-depth-header a{line-height:24px;}#personalisation{position:relative;float:none;clear:left;overflow:hidden;width:288px;padding:0 8px 3px 312px;margin-left:-312px;margin-right:-8px;margin-bottom:7px;border-bottom:1px solid #262835;}.ie #personalisation{height:1%;}#personalisation.location-set .locator-forms,#personalisation .locator-msg-disambiguate{background:#262835;}#personalisation .locator-forms{margin-left:-304px;width:592px;position:relative;overflow:visible;}.ie #personalisation .locator-forms{height:1%;}#personalisation.location-set .locator-forms{margin:0 -8px 0 -312px;padding:8px 8px 1px;overflow:hidden;}#personalisation .geo-digest-region-header{margin-bottom:0;}#personalisation.change-my-location .geo-digest-region-header{margin-bottom:12px;}#personalisation .locator-form,#personalisation .clear-form{width:592px;display:block;float:none;margin:0;}.blq-js #personalisation-panel-location-form label{position:absolute;margin-top:4px;left:0;display:block;text-transform:none;font-size:.923em;line-height:16px;padding-left:8px;z-index:10;}.blq-js #personalisation-panel-location-form label.input-has-focus{opacity:.3;filter:alpha(opacity=30);}.blq-js #personalisation-panel-location-form label.input-has-content{opacity:0;filter:alpha(opacity=0);}#personalisation .locator-auto-suggest{position:relative;float:left;display:inline;width:245px;padding:0 7px;height:24px;line-height:24px;border:none;font-size:.923em;border-right:1px solid #eee;margin-bottom:8px;}#personalisation .locator-auto-suggest:focus{outline:none;}#personalisation .locator-search{color:black;cursor:pointer;float:left;display:inline;line-height:1.8;margin:0;width:29px;text-indent:-2000em;padding:0;height:24px;border:none;background:white url(../../../../../../../static.bbc.co.uk/frameworks/barlesque/1.3.2/newnav/img/search_icon.png) no-repeat center center;}#personalisation .options{top:0;right:0;padding-right:31px;position:absolute;height:23px;}#personalisation .options li{float:left;display:inline;}#personalisation .options a{position:relative;float:left;display:inline;padding:3px 6px;margin-right:8px;color:#f0f0f0;border:1px solid #f0f0f0;}#personalisation .options a:hover,#personalisation .options a:focus{text-decoration:none;}#personalisation .close-my-location{top:12px;right:12px;cursor:pointer;display:block;font-size:1px;height:16px;width:16px;line-height:1px;overflow:hidden;text-indent:-5000px;z-index:10;margin:0;padding:0;position:absolute;color:#F0F0F0;background:url(../../../../../1_4_9/cream/hi/shared/img/gvl3-icons-0-2.png) no-repeat scroll -510px -32px;}.ie #personalisation .close-my-location{background-image:url(../../../../../1_4_9/cream/hi/shared/img/gvl3-icons-0-2.gif);}#personalisation .help-button,#personalisation .options .help-button{position:absolute;top:0;right:8px;margin:0;padding:0;width:22px;height:22px;background:url(../img/personalisation-help-icon.gif) no-repeat center 4px;}#personalisation .locator-msg{clear:both;padding-top:7px;}#personalisation .locator-msg{clear:both;padding:7px 8px 4px 8px;margin-left:-8px;margin-bottom:-3px;margin-right:-8px;}#personalisation.location-not-set .locator-msg{margin-left:0;margin-right:0;}#personalisation .locator-msg ol li a{color:#ACC2D4;font-weight:bold;}#personalisation .locator-msg *{color:#f0f0f0;font-weight:normal;}#personalisation .locator-msg ol li{margin:4px 0;}#personalisation .locator-msg p.locator-panel-header{font-size:1.4em;font-size:1.231em;font-weight:bold;letter-spacing:-0.05em;line-height:24px;margin-bottom:11px;padding-top:0;padding-bottom:0;position:relative;text-indent:-1px;margin-top:0;}#personalisation .locator-msg-disambiguate p.locator-panel-header{margin-top:0;margin-bottom:5px;padding-top:0;border-bottom:solid 1px #4e515a;padding-bottom:1px;}#personalisation .locator-msg-disambiguate p.locator-panel-header strong{position:relative;display:block;padding-top:4px;padding-bottom:4px;}#personalisation .locator-msg p{margin:8px 0;}#personalisation .locator-msg p a{font-weight:bold;color:#ACC2D4;}#personalisation .locator-msg .locator-loc{position:relative;padding-top:5px;padding-bottom:4px;display:block;width:592px;font-size:1.5em;font-weight:bold;letter-spacing:-0.05em;line-height:24px;text-indent:-1px;border-bottom:solid 1px #262835;}#personalisation.location-set .locator-msg .locator-loc{border-bottom:solid 1px #3c3e51;}#personalisation .locator-msg-disambiguate .locator-loc{border-bottom:none;padding-bottom:5px;}#personalisation .locator-controls{margin-top:8px;padding-bottom:8px;}#personalisation .locator-controls:after{content:".";display:block;height:0;clear:both;visibility:hidden;}#personalisation .locator-controls .locator-control-prev,#personalisation .locator-controls .locator-control-next{position:relative;padding-left:20px;display:block;font-weight:bold;color:#ACC2D4;}#personalisation .locator-controls .locator-control-prev{margin-top:4px;}#personalisation .locator-controls .locator-control-next{padding-right:20px;padding-left:5px;margin-top:4px;}#personalisation .locator-controls .disabled,#personalisation .locator-controls .locator-pages .selected{color:#d07128;text-decoration:none;cursor:default;}#personalisation .locator-controls .locator-pages a{border-left:solid 1px #ACC2D4;padding:0 5px;}#personalisation .locator-controls .locator-pages .first a{border-left:none;padding-left:0;}#personalisation .locator-results{padding-top:2px;padding-bottom:12px;}#personalisation span.locator-pagination-prev,#personalisation span.locator-pagination-next{overflow:hidden;position:absolute;height:15px;width:13px;left:0;background:url(../../../../../1_4_9/cream/hi/shared/img/GVL3-icons-test.png) no-repeat -200px -32px;}#personalisation span.locator-pagination-next{left:auto;right:0;background-position:-226px -32px;}#personalisation .disabled span.locator-pagination-prev{background-position:-200px -16px;}#personalisation .disabled span.locator-pagination-next{background-position:-226px -16px;}#personalisation .locator-controls .locator-control-prev,#personalisation .locator-controls .locator-pages,#personalisation .locator-controls .locator-pages ol,#personalisation .locator-controls .locator-pages li{float:left;display:inline;}#personalisation .locator-controls a{float:left;padding-right:8px;}#personalisation .locator-controls li{list-style-type:none;}#personalisation .locator-confirm p{position:relative;padding-top:3px;padding-bottom:5px;display:block;width:592px;}#personalisation .locator-confirm a{font-weight:bold;color:#ACC2D4;}#personalisation .locator-confirm .locator-action,#personalisation .locator-msg .locator-action{font-weight:bold;}#personalisation button{padding:4px 8px;background:transparent;border:1px solid #fff;margin-right:8px;margin-top:4px;margin-bottom:11px;cursor:pointer;}#personalisation .clear-form button{background:transparent;border:none;margin:3px 8px 12px 0;cursor:pointer;padding:0;font-size:1.1em;font-weight:bold;color:#ACC2D4;display:inline;}.ie7 #personalisation .clear-form button,.ie #personalisation .clear-form button{text-align:left;}#personalisation .clear-form button:hover,#personalisation .clear-form button:focus{text-decoration:underline;}#personalisation .geo-digest-section{margin-top:6px;padding:0 0 4px;}#personalisation .geo-digest-section ol{padding-top:8px;overflow:hidden;}#personalisation .geo-digest-section ol.news-stories{padding-top:11px;}.ie7 #personalisation .geo-digest-section li,.ie #personalisation .geo-digest-section li{padding-bottom:0;margin-top:4px;}#personalisation .column-1 ol{padding-top:4px 0;}#personalisation .weather-forecast *{color:#f0f0f0;}#personalisation .weather-forecast p a{color:#ACC2D4;}#personalisation .weather-forecast h4,#personalisation .news-stories h4{font-size:1.231em;font-weight:bold;line-height:20px;padding-bottom:3px;border-bottom:1px solid #262835;}#personalisation .news-stories h4 a{color:#fff;}#personalisation .weather-forecast .weather-days li{position:relative;float:left;display:inline;width:84px;clear:none;padding-top:3px;padding-right:8px;}#personalisation .weather-forecast h5{padding-bottom:12px;}#personalisation .weather-forecast .weather-type{position:relative;margin-top:-1px;display:block;font-weight:bold;}#personalisation .weather-forecast .max-temperature,#personalisation .weather-forecast .min-temperature{display:block;}#personalisation .weather-forecast p{clear:both;}#personalisation.location-not-set .geo-digest-section{display:none;}#personalisation .locator-auto-suggest-overlay li.odd,#personalisation .locator-auto-suggest-overlay li.even{background:#fff;color:#174f82;font-size:1.2em;font-weight:bold;padding:4px 8px;border-bottom:solid 1px #ccc;}#personalisation .locator-auto-suggest-overlay li.active{background:#d1700e;color:#fff;border-bottom:solid 1px #d1700e;}.ie #personalisation .weather-days,.ie7 #personalisation .weather-days{clear:both;margin-bottom:8px;}.ie7 #personalisation .news-stories li{padding-bottom:8px;}#personalisation.location-not-set .locator-msg-confirm,#personalisation.location-not-set .locator-msg-disambiguate{margin-left:-8px;margin-right:-8px;margin-bottom:-4px;position:relative;}.ie #personalisation.location-not-set .locator-msg-confirm,.ie #personalisation.location-not-set .locator-msg-disambiguate{height:1%;width:592px;padding-bottom:16px;}#personalisation .locator-msg-confirm{margin-bottom:-1px;}.panel-hd .hd{font-size:24px;font-weight:bold;letter-spacing:-0.05em;line-height:24px;text-indent:-1px;}.panel-bd .bd{font-size:13px;line-height:16px;font-weight:bold;}.panel-bd .bd p{padding:8px 0;}#personalisation-panel-auto-suggest{width:304px!important;}#personalisation-panel-auto-suggest .autosuggest-light li{color:#1F4F82;font-weight:bold;}#personalisation-panel-auto-suggest .autosuggest-light li.odd{background:#fff;}#personalisation .location-panel,#personalisation .we-remembered-panel{position:relative;margin-left:-312px;margin-right:-8px;width:592px;padding:0 8px;background:#f0f0f0;}#personalisation .location-panel h4,#personalisation .we-remembered-panel h4{padding-top:12px;margin-bottom:7px;padding-bottom:12px;border-bottom:1px solid #dcdcdc;}#personalisation .we-remembered-panel h4 a{position:absolute;top:8px;right:11px;height:20px;width:20px;overflow:hidden;text-indent:-50000%;background:transparent url(../../../../../1_4_9/cream/hi/shared/img/gvl3-icons-0-2.png) no-repeat;background-position:-507px 4px;}.ie #personalisation .we-remembered-panel h4 a{background-image:url(../../../../../1_4_9/cream/hi/shared/img/gvl3-icons-0-2.gif);}#personalisation .location-panel p{padding-top:4px;padding-bottom:4px;}#personalisation .we-remembered-panel p{padding-top:4px;padding-bottom:11px;}#personalisation .location-panel ul{padding-top:8px;padding-bottom:7px;}#personalisation .location-panel li{font-weight:bold;margin:1px 0 4px;cursor:pointer;font-size:1.231em;font-weight:bold;line-height:20px;}#personalisation-panel-auto-suggest{background:#323232;background:rgba(50,50,50,0.7);width:456px!important;padding:8px;margin-left:-8px;}#personalisation-panel-auto-suggest ul{border:none;}#personalisation-panel-auto-suggest ul li{width:440px;display:block;font-size:13px;font-weight:bold;padding:4px 8px;line-height:16px;}#personalisation-panel-auto-suggest ul li:hover,#personalisation-panel-auto-suggest ul li.active{background:#D2700F!important;color:white!important;text-decoration:underline;}.geo-digest-region-2{position:relative;clear:both;display:none;margin:0 0 16px;}.blq-js .geo-digest-region-2{display:block;}.geo-digest-region-2 #personalisation{padding:0 0 0 304px;margin:0;width:320px;}.geo-digest-region-2 .personalisation-wrapper{position:relative;margin-left:-304px;padding:4px 0 0 312px;background:#323232;-webkit-font-smoothing:antialiased;}.geo-digest-region-2 .geo-digest-region-header,.geo-digest-region-2 #personalisation h3{padding-bottom:8px;}.container-digest-grid .geo-digest-region-2 #personalisation.location-not-set .options,.container-single-section-digest .geo-digest-region-2 #personalisation.location-not-set .options{top:40px;}.geo-digest-region-2 #personalisation .locator-auto-suggest{width:412px;}.geo-digest-region-2 #personalisation .locator-msg .locator-loc,.geo-digest-region-2 #personalisation.location-set .locator-msg .locator-loc{border-bottom:none;padding-bottom:1px;}.geo-digest-region-2 #personalisation .locator-msg-disambiguate p.locator-panel-header{border-bottom:none;margin-bottom:2px;}.geo-digest-region-2 #personalisation .locator-results{padding-bottom:4px;}.geo-digest-region-2 #personalisation .locator-msg button{background:white;color:#323232;font-weight:bold;}.geo-digest-region-2 #personalisation .clear-form button{margin-bottom:8px;}.geo-digest-region-2 #personalisation.location-set .locator-forms,.geo-digest-region-2 #personalisation .locator-msg-disambiguate{background:none;}.geo-digest-region-2 #personalisation .locator-forms{width:608px!important;}.geo-digest-region-2 #personalisation .locator-form{position:relative;}.geo-digest-region-2 #personalisation .locator-auto-suggest{margin-bottom:0;}.geo-digest-region-2 #personalisation .geo-digest-section{margin-top:16px;}.geo-digest-region-2 #personalisation div.news-stories{position:relative;float:left;display:inline;width:304px;margin-left:-304px;background:white;}.geo-digest-region-2 #personalisation .options a{opacity:1;}.geo-digest-region-2 #personalisation .news-stories *,.geo-digest-region-2 #personalisation .weather-forecast *{color:#505050;text-shadow:none;}.geo-digest-region-2 #personalisation .news-stories a,.geo-digest-region-2 #personalisation .weather-forecast a{color:#1F4F82;}.geo-digest-region-2 #personalisation .news-stories a:visited,.geo-digest-region-2 #personalisation .weather-forecast a:visited{color:#4A7194;}.geo-digest-region-2 #personalisation .news-stories a:active,.geo-digest-region-2 #personalisation .weather-forecast a:active{color:#D1700E;}.geo-digest-region-2 #personalisation .news-stories h4{border-color:#ddd;padding:7px 0;border-top:1px solid #ddd;}.geo-digest-region-2 #personalisation .geo-digest-section ol.news-stories{padding-top:4px;}.geo-digest-region-2 #personalisation div.weather-forecast{position:relative;width:304px;margin-left:16px;background:#ededed;}.ie .geo-digest-region-2 #personalisation div.weather-forecast{float:right;display:inline;}.geo-digest-region-2 #personalisation .weather-forecast h4{padding:8px;border:none;}.geo-digest-region-2 #personalisation .geo-digest-section ol.weather-days{padding:1px 0 8px 8px;background:white;margin:0 8px;overflow:hidden;position:relative;display:block;}.ie .geo-digest-region-2 #personalisation .geo-digest-section ol.weather-days{height:1%;}.geo-digest-region-2 #personalisation .weather-forecast p{padding:8px 8px 4px;}.geo-digest-region-2 #personalisation .weather-forecast li p{padding:0;}.geo-digest-region-2 #personalisation.location-set .personalisation-wrapper{background:transparent;}.geo-digest-region-2 #personalisation.location-set .personalisation-wrapper h3{font-size:16px;letter-spacing:0;margin-bottom:-8px;margin-left:-312px;color:#505050;}.geo-digest-region-2 #personalisation.change-my-location .personalisation-wrapper h3{margin-bottom:0;}.geo-digest-region-2 #personalisation.location-set .options{right:-8px;}.geo-digest-region-2 #personalisation.location-set .options a{border-color:#ccc;color:#505050;}.geo-digest-region-2 #personalisation.location-set .options a.help-button{background:url(http://news.bbcimg.co.uk/view/@css_hi_news@/cream/hi/news/img/personalisation-help-icon-2.gif) no-repeat center center;}.geo-digest-region-2 #personalisation.location-set .locator-forms{background:#323232;}.geo-digest-region-2 #personalisation.location-set .locator-forms *{-webkit-font-smoothing:antialiased;}.geo-digest-region-2 #personalisation .location-panel,.geo-digest-region-2 #personalisation .we-remembered-panel{width:608px;margin-top:16px;background:#ccc;}.geo-digest-region-2 #personalisation .location-panel h4,.geo-digest-region-2 #personalisation .we-remembered-panel h4{border-color:white;}.geo-digest-region-2 #personalisation.location-not-set .geo-digest-section{display:none;}.ie .geo-digest-region-2 #personalisation button,.ie7 .geo-digest-region-2 #personalisation button{padding:2px 4px;}.geo-digest-region-2 #personalisation .personalisation-wrapper a,.geo-digest-region-2 #personalisation .personalisation-wrapper .clear-form button{color:#A9C0D3;}.geo-digest-region-2 #personalisation .locator-msg-confirm .locator-loc-old{font-weight:bold;}.geo-digest-region-2 #personalisation{border-bottom:none;}.ie .geo-digest-region-2 #personalisation.location-not-set .news-stories,.ie .geo-digest-region-2 #personalisation.location-not-set .weather-forecast{display:none;}.podcasts-range-module{clear:both;position:relative;overflow:hidden;width:336px;margin:16px 0 5px;padding-top:4px;padding-bottom:4px;background:#eee;}.podcasts-range-module .podcasts-range-module-header{padding:4px 8px 8px;}.podcasts-range-module ul{overflow:visible;padding:8px 8px 12px;}.podcasts-range-module li{position:relative;float:left;display:inline;width:100%;padding-bottom:8px;}.podcasts-range-module .gvl3-icon{position:absolute;top:0;left:0;}.podcasts-range-module .medium-image{width:238px;padding-left:78px;padding-bottom:16px;margin-bottom:-1px;}.podcasts-range-module .medium-image img{position:relative;float:left;display:inline;width:70px;height:70px;margin-left:-78px;}.podcasts-range-module .medium-image .gvl3-icon-wrapper{position:absolute;top:0;left:0;}.podcasts-range-module .medium-image .gvl3-icon{position:relative;top:auto;left:auto;}.container-programme-promotion{position:relative;overflow:visible;width:336px;margin:0 0 16px;background:#000;clear:both;}.bbccom_slot_xxl .container-programme-promotion{width:496px;}.container-programme-promotion .programmes-header{font-size:24px;font-size:1.846em;font-weight:bold;letter-spacing:-0.05em;line-height:24px;text-indent:-2px;color:#fff;display:block;padding:8px;}.container-programme-promotion a.iplayer-branding{right:8px;color:#fff;position:absolute;overflow:hidden;text-indent:-50000px;top:12px;width:122px;height:22px;background:url(../img/programmes-iplayer-brand.png) no-repeat top left;}.container-programme-promotion .programmes-section-header,.container-programme-promotion .data-feed-best h3{font-size:16px;font-size:1.231em;font-weight:bold;line-height:20px;padding:9px 8px 0;margin-bottom:-5px;}.container-programme-promotion .programmes-header a,.container-programme-promotion .programmes-section-header a,.container-programme-promotion .data-feed-best h3 a{color:#A9C0D3;}.container-programme-promotion .programmes-header a{display:block;}.container-programme-promotion ul{position:relative;overflow:hidden;padding:0 8px 0;clear:both;}.container-programme-promotion ul.programme-breakout{padding-top:0;padding-bottom:0;}.container-programme-promotion li{position:relative;display:block;clear:both;overflow:hidden;padding-bottom:1px;zoom:1;}.container-programme-promotion li a{color:#A9C0D3;}.container-programme-promotion li p{color:#eee;}.container-programme-promotion li.large-image{width:320px;margin:0 -8px;padding:8px 8px 8px;border-bottom:none;background:#505050;}.bbccom_slot_xxl .container-programme-promotion li.large-image{padding:8px 8px 0 344px;width:144px;border-bottom:1px solid #fff;}.ie .container-programme-promotion li.large-image,.ie7 .container-programme-promotion li.large-image{display:inline;}.container-programme-promotion li.large-image .programme-header{margin-bottom:5px;overflow:visible;font-size:16px;font-size:1.231em;font-weight:bold;}.container-programme-promotion li.large-image .story img{position:relative;display:block;margin:-8px -8px 9px;width:336px;height:189px;border-bottom:1px solid #fff;}.bbccom_slot_xxl .container-programme-promotion li.large-image .story img{float:left;display:inline;margin:-8px 0 0 -344px;border-bottom:none;}.container-programme-promotion li.large-image *{color:#fff;}.container-programme-promotion li.medium-image{padding:0 0 0 120px;list-style:none outside;}.container-programme-promotion li.medium-image .programme-header{position:relative;padding-top:5px;}.container-programme-promotion li.medium-image img{position:relative;float:left;display:inline;margin-top:3px;margin-left:-120px;width:112px;height:63px;}.container-programme-promotion li.medium-image p{padding-bottom:1px;}.container-programme-promotion li.no-image{padding:12px 0 2px;list-style:none outside;}.container-programme-promotion li.first-child{background:#d2700f;}.container-programme-promotion li.first-child *{color:#fff;}.container-programme-promotion li.first-item{padding-top:4px;}.container-programme-promotion li .gvl3-icon{position:absolute;top:11px;left:0;}.container-programme-promotion li.large-image .gvl3-icon-wrapper{position:absolute;top:0;left:0;}.container-programme-promotion li.medium-image .gvl3-icon-wrapper{position:absolute;top:8px;left:-120px;}.container-programme-promotion li .gvl3-icon-wrapper .gvl3-icon{position:relative;top:0;left:0;}.container-programme-promotion .data-feed-best{margin:0 8px;padding:0;}.container-programme-promotion .data-feed-best h3{padding:9px 0 8px;margin-bottom:-6px;}.container-programme-promotion .data-feed-best ul{padding:0 0 8px;}.container-programme-promotion .programme-breakout li.no-image{border-bottom:medium none;margin:0 -8px;padding:8px 8px 7px;width:320px;}.bbccom_slot_xxl .container-programme-promotion .programme-breakout li.no-image{width:480px;}.programme-breakout li.no-image .programme-header{font-size:1.231em;font-weight:bold;line-height:20px;margin-bottom:1px;position:relative;}.programme-breakout li.no-image .programme-header a{position:relative;}.programme-breakout li.no-image .gvl3-icon{left:-20px;top:0;position:absolute;}.programme-breakout li.no-image .gvl3-icon-invert-boxedlive{left:-32px;}.ie .programme-breakout li.no-image .gvl3-icon-invert-boxedlive{left:0;}.ie .container-programme-promotion hr,.ie7 .container-programme-promotion hr{margin-bottom:-10px;display:block;}.ie .programme-breakout .programme-header a.story{color:#fff;}.best-quote-box{position:relative;clear:both;overflow:hidden;margin:0 0 16px;padding:43px 8px 7px;background:#ededed url(../../../../../1_4_9/cream/hi/shared/img/index-quote.png) left 4px no-repeat;}.best-quote-box .strapline{display:block;padding-top:0;font-size:1.846em;font-weight:bold;letter-spacing:-1px;line-height:24px;}.best-quote-box blockquote{display:block;overflow:visible;padding:6px 16px 4px 0;font-size:1.231em;font-weight:bold;line-height:20px;}.best-quote-box blockquote img{position:relative;margin:3px -24px 11px -8px;display:block;}.best-quote-box blockquote *{line-height:20px;}.best-quote-box blockquote.solo-block{padding-top:34px;}.best-quote-box .quote_credit{display:block;padding:3px 0 0;}.best-quote-box .related-links{border-top:1px solid #d8d8d8;position:relative;margin-top:9px;padding:8px 0 4px;}.best-quote-box .related-links li{padding:3px 0 1px;position:relative;}.best-quote-box .related-links li .gvl3-icon{position:absolute;left:0;top:2px;}.ie .best-quote-box .related-links li .gvl3-icon{left:-20px;}.ie .best-quote-box .related-links li.has-icon-boxedlive .gvl3-icon{left:0;}.secondary-top-story{position:relative;overflow:visible;width:624px;margin:0;padding-top:8px;padding-bottom:8px;border-top:1px solid #ddd;}.secondary-top-story .secondary-story-header{padding-top:2px;margin-bottom:4px;}.secondary-top-story .with-summary{position:relative;clear:both;overflow:hidden;width:512px;padding-right:112px;padding-bottom:12px;}.secondary-top-story .medium-image{position:relative;clear:both;overflow:hidden;width:464px;padding-bottom:13px;padding-left:160px;}.secondary-top-story .medium-image img{position:relative;float:left;display:inline;top:3px;margin-top:-1px;margin-left:-160px;width:144px;height:81px;}.secondary-top-story .large-image{position:relative;clear:both;overflow:hidden;width:304px;padding-left:320px;padding-bottom:11px;}.secondary-top-story .large-image img{position:relative;float:left;display:inline;top:3px;margin-top:-1px;margin-left:-320px;width:304px;height:171px;}.ie .secondary-top-story .large-image img,.ie7 .secondary-top-story .large-image img{padding-bottom:8px;}.secondary-top-story .classic-image{position:relative;clear:both;overflow:hidden;width:384px;padding-bottom:11px;padding-left:240px;}.secondary-top-story .classic-image img{position:relative;float:left;display:inline;top:3px;margin-left:-240px;width:226px;height:170px;}.secondary-top-story p{position:relative;margin-top:3px;}.secondary-top-story li{position:relative;}.secondary-top-story .gvl3-icon{position:absolute;top:0;left:0;}.secondary-top-story .with-summary .secondary-story-header .gvl3-icon{top:2px;}.secondary-top-story .gvl3-icon-wrapper{position:absolute;top:4px;left:0;}.secondary-top-story .classic-image .gvl3-icon-wrapper{top:5px;}.ie7 .secondary-top-story .gvl3-icon-wrapper,.ie .secondary-top-story .gvl3-icon-wrapper{top:4px;}.ie7 .secondary-top-story .classic-image .gvl3-icon-wrapper,.ie .secondary-top-story .classic-image .gvl3-icon-wrapper{top:5px;}.secondary-top-story .gvl3-icon-wrapper .gvl3-icon{position:relative;top:auto;left:auto;}.secondary-top-story .see-also{clear:none;display:inline;float:left;position:relative;width:100%;margin:4px 0 -8px 0;padding:0;}.ie .secondary-top-story .see-also{width:90%;}.secondary-top-story .with-summary .see-also{margin-bottom:-8px;width:304px;padding-left:320px;}.secondary-top-story .with-summary .see-also .column-1{position:relative;float:left;clear:left;display:inline;overflow:hidden;width:304px;margin-left:-320px;}.secondary-top-story .with-summary .see-also .column-2{width:304px;float:none;clear:none;display:block;}.ie .secondary-top-story .with-summary .see-also .column-2,.ie7 .secondary-top-story .with-summary .see-also .column-2{float:right;}.secondary-top-story .medium-image .see-also{float:right;clear:none;width:224px;padding-left:240px;margin-bottom:-9px;}.secondary-top-story .medium-image .see-also li{float:none;clear:none;display:block;}.secondary-top-story .medium-image .see-also .column-1{position:relative;float:left;clear:left;display:inline;overflow:hidden;width:224px;margin-left:-240px;}.secondary-top-story .medium-image .see-also .column-2{width:224px;}.ie .secondary-top-story .medium-image .see-also .column-2,.ie7 .secondary-top-story .medium-image .see-also .column-2{float:right;}.secondary-top-story .see-also li{clear:left;display:inline;float:left;padding-bottom:8px;width:100%;}.secondary-top-story .see-also .play-audio{text-indent:20px;}.secondary-top-story .see-also .play-video{text-indent:16px;}.secondary-top-story .see-also .play-audio-icon{width:14px;left:-20px;background:url(../../../../../1_4_9/cream/hi/shared/img/icons/listen-charcoal.png) no-repeat center left;}.secondary-top-story .see-also .play-video-icon{width:12px;left:-16px;background:url(../../../../../1_4_9/cream/hi/shared/img/icons/play-charcoal.png) no-repeat center left;}.secondary-top-story .see-also .play-audio-icon,.secondary-top-story .see-also .play-video-icon{top:1px;height:14px;}body #blq-container #blq-container-inner .secondary-top-story .see-also a.is-index{font-weight:bold;}.blq-js .bbc-st{display:none;}#blq-main .bbc-st{float:right;position:relative;top:-3px;}#page-bookmark-links-head .bbc-st{margin-right:-7px;}#page-bookmark-links-foot .bbc-st{float:left;}.bbc-st p{display:inline;}#blq-main ul.bbc-st-buttons{width:auto!important;}#blq-main ul.bbc-st-buttons li{position:relative;}.share-form{width:320px;padding-top:8px;}.ie .share-form{width:290px;}.share-form ul.networks{position:relative;overflow:hidden;padding:16px 0 16px 0;}.share-form ul.networks li{width:125px;float:left;padding:0 0 8px 24px;background-image:url(../../../../../1_4_9/cream/hi/shared/img/story_sprite.gif);background-repeat:no-repeat;}body.ie .share-form ul.networks{width:280px;}body.ie .share-form ul.networks li{width:100px;padding:0 0 8px 25px;}.share-form a.share-help-link{font-size:13px;}.share-form ul.networks li.delicious{background-position:-3650px 0;margin:0 16px 0 0;}.share-form ul.networks li.digg{background-position:-2200px 0;}.share-form ul.networks li.facebook{background-position:0 0;margin:0 16px 0 0;}.share-form ul.networks li.reddit{background-position:-2650px 0;}.share-form ul.networks li.stumbleupon{background-position:-2400px 0;margin:0 16px 0 0;}.share-form ul.networks li.twitter{background-position:-300px 0;}.share-form ul.networks li.mixx{background-position:-2900px 0;margin:0 16px 0 0;}.share-form ul.networks li.google{background-position:-3150px 0;}.share-help ul li.delicious a{background-position:-3650px 0;}.share-help ul li.digg a{background-position:-2200px 0;}.share-help ul li.facebook a{background-position:0 0;}.share-help ul li.reddit a{background-position:-2650px 0;}.share-help ul li.stumbleupon a{background-position:-2400px 0;}.share-help ul li.twitter a{background-position:-300px 0;}.share-help ul li.mixx a{background-position:-2900px 0;}.share-help ul li.google a{background-position:-3150px 0;}.blq-js #socialBookMarks li.delicious,.blq-js #socialBookMarks li.digg,.blq-js #socialBookMarks li.facebook,.blq-js #socialBookMarks li.reddit,.blq-js #socialBookMarks li.stumbleupon,.blq-js #socialBookMarks li.mixx,.blq-js #socialBookMarks li.google{display:block;}.panel-light .c{background:#FFF!important;}.glow173-panel .panel-close,.glow173-panel .panel-light .panel-close{background-image:url(../../../../../1_4_9/cream/hi/shared/img/story_sprite.gif);background-repeat:no-repeat;background-position:-3500px 0;}.panel-light .c .panel-hd{margin:0 15px 0 10px!important;padding-left:0!important;}.glowNoMask .panel-light .bb{border-color:#fff!important;}#share-links-panel-top .br,#share-links-panel-bottom .br{background:url(../../../../../1_4_9/cream/hi/shared/img/cbr.png);}.ie7 #share-links-panel-top .br,.ie7 #share-links-panel-bottom .br,.ie #share-links-panel-top .br,.ie #share-links-panel-bottom .br{background:none;filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/view/1_4_9/cream/hi/shared/img/cbr.png',sizingMethod='crop');}#share-links-panel-top .bl,#share-links-panel-bottom .bl{background:url(../../../../../1_4_9/cream/hi/shared/img/cbl.png);}.ie7 #share-links-panel-top .bl,.ie7 #share-links-panel-bottom .bl,.ie #share-links-panel-top .bl,.ie #share-links-panel-bottom .bl{background:none;filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/view/1_4_9/cream/hi/shared/img/cbl.png',sizingMethod='crop');}.simple-guide-list{margin:0 0 0 16px;}.simple-guide-list ul li{padding:0 0 8px 0;position:relative;float:none;display:block;overflow:auto;}.simple-guide-list-heading{font-size:1.231em;border-top:1px solid #ccc;border-bottom:1px solid #ccc;padding:8px 0;margin:0 0 16px 0;}.simple-guide-list li.medium-image{clear:both;overflow:hidden;padding-bottom:13px;padding-left:160px;position:relative;width:144px;}.simple-guide-list li.medium-image img{display:inline;float:left;height:81px;margin-left:-160px;margin-top:-1px;position:relative;top:3px;width:144px;}.simple-guide-list h3{position:relative;}.simple-guide-list li.medium-image .gvl3-icon{position:absolute;top:2px;left:0;}.simple-guide-list .gvl3-icon{left:0;position:absolute;top:-1px;}.simple-guide-list li.medium-image .gvl3-icon-wrapper{position:absolute;left:0;top:2px;}.simple-guide-list li .gvl3-icon-wrapper .gvl3-icon{left:auto;position:relative;top:auto;}.ie .simple-guide-list .has-icon-boxedlisten .gvl3-icon,.ie .simple-guide-list .has-icon-boxedwatch .gvl3-icon{margin:0 0 0 -20px;}.ie .simple-guide-list .has-icon-boxedlive a{padding:0 0 0 35px;}.simple-story-list ul li{padding:0 0 8px 0;position:relative;}.simple-story-list .gvl3-icon{left:0;position:absolute;top:-1px;}.ie .simple-story-list .has-icon-boxedlisten .gvl3-icon,.ie .simple-story-list .has-icon-boxedwatch .gvl3-icon{margin:0 0 0 -20px;}.ie .simple-story-list .has-icon-boxedlive a{padding:0 0 0 35px;}.single-split-column-layout{float:left;width:304px;}#site-wide-alert,#site-wide-alert p,#site-wide-alert h2 span,#site-wide-alert p.close_alert{background:none;color:#fff;padding:0;margin:0;}#site-wide-alert *{color:white;}#site-wide-alert h2{padding:0 30px 3px 8px;width:auto;}#site-wide-alert p a{color:#fff;font-weight:400;}#site-wide-alert{clear:both;overflow:hidden;background:#ce1211;margin:8px 0 4px;width:auto;padding:7px 0 10px;position:relative;}#site-wide-alert p{padding:0 8px 0;}#site-wide-alert p.close_alert{background:transparent url(../../../../../1_4_9/cream/hi/shared/img/gvl3-icons-0-2.png) -488px -33px no-repeat;display:block;width:14px;height:14px;position:absolute;right:8px;top:8px;}.ie #site-wide-alert p.close_alert{background:transparent url(../../../../../1_4_9/cream/hi/shared/img/gvl3-icons-0-2.gif) -511px -33px no-repeat;}#site-wide-alert p.close_alert a{display:block;width:11px;height:11px;text-indent:-9999px;padding:0;}.social-link-digests{background:#ededed;clear:both;margin:0 0 16px;overflow:hidden;padding-bottom:12px;position:relative;width:336px;}.social-link-digest .social-link-header{display:block;font-size:1.231em;font-weight:bold;line-height:20px;padding:8px 8px 0;position:relative;text-rendering:optimizelegibility;}.social-link-digest ul{padding:0 8px;}.social-link-digest p{padding:0 0 8px;}.social-link-digest li a.story{font-weight:normal;}#special-events-promotion-now-include h2{padding-bottom:8px;width:auto;}.special-reports-component{position:relative;overflow:hidden;margin-bottom:21px;width:624px;}.special-reports-wrapper{background-color:#D1700E;float:left;display:inline;position:relative;padding-top:11px;padding-left:320px;width:304px;}.special-reports-header{padding:8px 0;}.special-reports-component .top-report{padding-right:8px;padding-bottom:12px;}.special-reports-component .top-report h3 img{display:inline;float:left;margin-top:-11px;position:relative;z-index:2;border-right:1px solid #FFF;margin-left:-320px;}.special-reports-component .top-report h3 a{color:#fff;padding:3px 8px 0 0;}.special-reports-component .top-report p{color:#fff;margin:4px 8px 8px 0;}.special-reports-component .more-special-reports{background-color:#EDEDED;kbackground-color:pink;position:absolute;bottom:0;width:320px;border-top:1px solid #FFF;right:0;}.special-reports-component .more-special-reports h3{margin:11px 16px 0 16px;}.special-reports-component .more-special-reports ul{margin:4px 16px 11px 16px;}.special-reports-component .more-special-reports ul li{padding:4px 0;}.ie .special-reports-component .more-special-reports{bottom:-1px;}.container-archived-content{clear:both;margin-bottom:16px;position:relative;}.container-archived-content-heading{padding:8px 0 4px 0;border-top:1px solid #DDD;margin-top:0;margin-bottom:8px;}.container-archived-content-heading span{border-bottom:1px solid #DDD;padding-bottom:15px;display:block;width:464px;}.find-a-representative{position:relative;display:block;width:100%;clear:both;overflow:hidden;margin:0 0 22px;}.find-a-representative h2{font-size:1.231em;font-weight:bold;line-height:16px;padding-bottom:12px;padding-top:12px;position:relative;}.blq-js .find-a-representative h2{padding-bottom:8px;}.find-a-representative legend,.find-a-representative legend span{position:absolute;left:-5000%;}.blq-js .find-a-representative label{position:absolute;margin-top:5px;left:0;width:580px;display:block;text-transform:none;font-size:.923em;line-height:16px;padding:5px 8px 6px;}.blq-js .find-a-representative label.input-has-focus{color:#ccc;}.blq-js .find-a-representative label.input-has-content{color:white;display:none;}.find-a-representative .input{position:relative;clear:both;display:inline;float:left;margin-top:5px;height:16px;width:580px;line-height:24px;padding:4px 7px 4px;border:1px solid #ddd;border-right:none;font-size:.923em;text-transform:none;}.ie7 .find-a-representative .input,.ie .find-a-representative .input{padding:0 7px;height:24px;}.blq-js .find-a-representative .input{background-color:transparent;}.find-a-representative .input:focus{outline:0;}.find-a-representative .submit{position:relative;display:inline;float:left;margin-top:5px;height:26px;width:29px;line-height:24px;background:#fff url(../../../../../1_4_9/cream/hi/shared/img/search.png) no-repeat center center;border:1px solid #ddd;padding:0;text-indent:-2000em;overflow:hidden;cursor:pointer;}.find-a-representative .glow-errorMsg{display:block!important;position:absolute;text-indent:-5000%;overflow:hidden;top:0;right:0;width:100%;height:100%;background:rgba(255,255,0,0.1);}.ie8 .find-a-representative .glow-errorMsg,.ie7 .find-a-representative .glow-errorMsg{background:yellow;filter:alpha(opacity=10);}.ie .find-a-representative .glow-errorMsg{background:yellow;filter:alpha(opacity=10);padding:5px 8px 6px;margin:0 0 -11px -16px;}.find-a-representative .glow-errorSummary{display:none!important;}.school-tables-finder{position:relative;clear:both;overflow:hidden;margin:0 0 16px;padding:8px;background:#ededed;width:608px;}.ie .school-tables-finder{height:1%;}.school-tables-finder .strapline{position:relative;margin-top:-2px;padding-bottom:1px;}.school-tables-finder .strapline a{line-height:24px;}.school-tables-finder .description{padding:9px 0 3px;line-height:20px;}.school-tables-finder fieldset{position:relative;margin-bottom:16px;background:white;}.school-tables-finder label{display:block;width:296px;padding:6px 0;white-space:nowrap;font-weight:bold;background:#ededed;overflow:hidden;}.blq-js .school-tables-finder label{position:absolute;top:0;left:0;width:250px;padding:6px 8px;background:transparent;font-weight:normal;}.school-tables-finder label.input-has-focus{color:#ccc;}.school-tables-finder label.input-has-content{color:white;display:none;}.school-tables-finder #school-tables-finder-select label{border-bottom:1px solid #ededed;padding-bottom:5px;}.blq-js .school-tables-finder #school-tables-finder-select label{border-bottom:none;padding-bottom:6px;padding-right:38px;width:210px;cursor:pointer;background:url(../../../../../1_4_9/cream/hi/shared/img/select-arrow.png) no-repeat center right;}.school-tables-finder #school-tables-finder-search{position:relative;float:left;display:inline;width:296px;padding:8px 0 4px;}.school-tables-finder #school-tables-finder-search .text{position:relative;float:left;display:inline;font-size:13px;height:16px;width:252px;line-height:16px;padding:6px 7px 6px;border:none;border-top:1px solid #ededed;background:transparent;}.school-tables-finder #school-tables-finder-search .submit{float:right;display:inline;height:28px;margin:0;width:30px;line-height:16px;padding:0;text-indent:-2000%;overflow:hidden;border:none;cursor:pointer;border-left:1px solid #ededed;border-top:1px solid #ededed;background:white url(../../../../../1_4_9/cream/hi/shared/img/search.png) no-repeat 50% 50%;}.blq-js .school-tables-finder #school-tables-finder-search .text,.blq-js .school-tables-finder #school-tables-finder-search .submit{border-top:none;}.school-tables-finder #school-tables-finder-select{position:relative;float:right;display:inline;width:296px;padding:8px 0 4px;}.school-tables-finder #school-tables-finder-select select{position:absolute;top:30px;left:4px;width:240px;-moz-border-radius:8px;-webkit-border-radius:8px;border-radius:8px;-moz-box-shadow:0 0 10px rgba(0,0,0,0.3);-webkit-box-shadow:0 0 10px rgba(0,0,0,0.3);box-shadow:0 0 10px rgba(0,0,0,0.3);font-size:13px;line-height:16px;font-family:Arial;color:#505050;}.blq-js .school-tables-finder #school-tables-finder-select select{position:absolute;padding:8px;top:-56px;left:0;width:264px;border:none;}.school-tables-finder #school-tables-finder-select option{display:block;font-size:13px;font-family:Arial;color:#505050;}.school-tables-finder #school-tables-finder-select .submit{float:right;display:inline;text-align:center;margin:0;height:28px;padding:6px;line-height:16px;overflow:hidden;border:none;cursor:pointer;border-left:1px solid #ededed;background:#505050;color:white;font-weight:bold;text-transform:uppercase;font-size:13px;}.ie .school-tables-finder #school-tables-finder-select .submit,.ie7 .school-tables-finder #school-tables-finder-select .submit{padding:6px 3px;}.blq-js .school-tables-finder #school-tables-finder-select .submit{border-left-width:8px;}.school-tables-finder input:focus,.school-tables-finder select:focus{outline:none;}.school-tables-finder .glow-errorMsg{display:block!important;position:absolute;text-indent:-5000%;overflow:hidden;top:0;width:100%;height:100%;background:rgba(255,255,0,0.1);}.ie8 .school-tables-finder .glow-errorMsg,.ie7 .school-tables-finder .glow-errorMsg{background:yellow;filter:alpha(opacity=10);}.ie .school-tables-finder .glow-errorMsg{background:yellow;filter:alpha(opacity=10);padding:6px 8px;margin:0 0 -12px -16px;}.school-tables-finder #school-tables-finder-search .glow-errorMsg{right:0;}.school-tables-finder #school-tables-finder-select .glow-errorMsg{right:31px;}.school-tables-finder .glow-errorSummary{display:none!important;}.nhs-ratings-finder{position:relative;clear:both;overflow:hidden;margin:0 0 16px;padding:8px;width:608px;background:#ededed;}.ie .nhs-ratings-finder{height:1%;}.nhs-ratings-finder .strapline{position:relative;margin-top:-1px;padding-bottom:1px;}.nhs-ratings-finder .strapline a{line-height:24px;}.nhs-ratings-finder .description{padding:9px 0 3px;line-height:20px;}.nhs-ratings-finder fieldset{position:relative;margin-bottom:8px;background:white;}.nhs-ratings-finder label{display:block;width:608px;padding:6px 0;white-space:nowrap;font-weight:bold;background:#ededed;overflow:hidden;}.blq-js .nhs-ratings-finder label{position:absolute;top:0;left:0;width:562px;padding:6px 8px;background:transparent;font-weight:normal;}.nhs-ratings-finder label.input-has-focus{color:#ccc;}.nhs-ratings-finder label.input-has-content{color:white;display:none;}.nhs-ratings-finder #nhs-ratings-finder-search{position:relative;float:left;display:inline;width:608px;padding:8px 0 0;}.nhs-ratings-finder #nhs-ratings-finder-search .text{position:relative;float:left;display:inline;font-size:13px;height:16px;width:563px;line-height:16px;padding:6px 7px 6px;border:none;border-top:1px solid #ededed;background:transparent;}.nhs-ratings-finder #nhs-ratings-finder-search .submit{float:right;display:inline;height:28px;width:30px;margin:0;line-height:16px;padding:0;text-indent:-2000%;overflow:hidden;border:none;cursor:pointer;border-left:1px solid #ededed;border-top:1px solid #ededed;background:white url(../../../../../1_4_9/cream/hi/shared/img/search.png) no-repeat 50% 50%;}.blq-js .nhs-ratings-finder #nhs-ratings-finder-search .text,.blq-js .nhs-ratings-finder #nhs-ratings-finder-search .submit{border-top:none;}.nhs-ratings-finder input:focus,.nhs-ratings-finder select:focus{outline:none;}.nhs-ratings-finder .warning{font-size:11px;text-transform:uppercase;}.nhs-ratings-finder .glow-errorMsg{display:block!important;position:absolute;text-indent:-5000%;overflow:hidden;top:0;right:0;width:100%;height:100%;background:rgba(255,255,0,0.1);}.ie8 .nhs-ratings-finder .glow-errorMsg,.ie7 .nhs-ratings-finder .glow-errorMsg{background:yellow;filter:alpha(opacity=10);}.ie .nhs-ratings-finder .glow-errorMsg{background:yellow;filter:alpha(opacity=10);padding:6px 8px;margin:0 0 -12px -16px;}.nhs-ratings-finder .glow-errorSummary{display:none!important;}.special-event-promotion-best{margin-bottom:16px;}.se-promo-now-inc{margin-bottom:16px;position:relative;overflow:hidden;width:624px;}.se-promo-now-inc .se-promo-now-inc-header{line-height:34px;padding-bottom:8px;}.se-promo-best-inc{background:none repeat scroll 0 0 #EDEDED;margin:0 0 16px;overflow:visible;padding-bottom:8px;position:relative;width:336px;}.se-promo-best-inc .se-promo-best-inc-header{padding:8px;}.se-promo-best-inc ul{margin-left:8px;}.se-promo-best-inc ul li{margin-bottom:16px;padding-right:8px;padding-left:16px;background-image:url(../../../../../1_4_9/cream/hi/shared/img/story_sprite.gif);background-position:-1200px 5px;background-repeat:no-repeat;width:304px;}.se-promo-best-inc ul li h3 a,.se-promo-best-inc ul li h3{font-weight:normal;}.have-your-say-inc{background:none repeat scroll 0 0 #EDEDED;margin:0 0 16px;overflow:visible;padding-bottom:2px;position:relative;width:336px;}.have-your-say-inc .have-your-say-inc-header{padding:8px;}.have-your-say-inc ul{margin-left:8px;}.have-your-say-inc ul li{margin-bottom:8px;padding-right:8px;padding-left:16px;background-image:url(../../../../../1_4_9/cream/hi/shared/img/story_sprite.gif);background-position:-1200px 5px;background-repeat:no-repeat;width:304px;}.have-your-say-inc ul.domestic-inc li.with-contact-numbers{background-position:-1200px 12px;}.have-your-say-inc ul.international-inc li.with-contact-numbers{background-position:-1200px 5px;}.have-your-say-inc ul.international-inc li.with-contact-numbers .contact-number{padding-top:1px;}.have-your-say-inc ul.international-inc li.with-contact-numbers .core-text,.have-your-say-inc ul.international-inc li.with-contact-numbers .contact-number{display:block;}.have-your-say-inc ul li h3{font-weight:normal;}.have-your-say-inc .contact-method{font-weight:bold;}.have-your-say-inc .contact-number{color:#D1700E;}.have-your-say-inc ul li .core-text{font-weight:normal!important;}.external-linkbox{position:relative;overflow:visible;width:336px;padding-bottom:8px;margin:0 0 16px;background:#ededed;}.bbccom_slot_xxl .external-linkbox{width:496px;}.external-linkbox .strapline{padding:8px 8px 0;}.external-linkbox ul{position:relative;overflow:hidden;padding:0 8px 0;clear:both;}.external-linkbox li{position:relative;display:block;clear:both;overflow:hidden;}.external-linkbox li.medium-image{padding:0 0 0 120px;position:relative;margin-bottom:-2px;}.ie7 .external-linkbox li.medium-image,.ie .external-linkbox li.medium-image{margin-bottom:-14px;}.external-linkbox li.medium-image .item-header{position:relative;padding-top:7px;margin-bottom:-1px;}.external-linkbox li.medium-image img{position:relative;float:left;display:inline;margin-top:1px;margin-left:-120px;width:112px;height:63px;}.external-linkbox li.medium-image p{padding-bottom:4px;padding-top:6px;}#splash{position:relative;overflow:hidden;width:448px;padding-left:528px;min-height:100px;padding-bottom:12px;margin-top:9px;min-height:290px;}.ie #splash{overflow:visible;height:290px;}#splash .splash-header{position:relative;padding-bottom:7px;}#splash .splash-header img{position:relative;float:left;display:inline;top:5px;margin-left:-528px;width:512px;height:288px;}#splash #splashSlideShow{position:relative;float:left;display:inline;top:5px;margin-left:-528px;width:512px;min-height:288px;}#splash .splash-emp{position:relative;float:left;display:inline;top:5px;margin-left:-528px;width:512px;min-height:288px;padding-bottom:1px;}#splash .splash-emp .caption{position:absolute;left:-5000%;overflow:hidden;}#splash .splash-emp .warning{position:relative;}#splash .splash-emp .warning .holding{width:100%;height:auto;}#splash .splash-emp .warning p{position:absolute;top:0;left:0;width:496px;padding:4px 8px;background:black;background:rgba(0,0,0,0.7);color:white;font-size:16px;font-weight:bold;}#splash .see-also{position:relative;float:left;display:inline;clear:none;overflow:hidden;padding-left:232px;width:216px;margin-top:12px;margin-bottom:-5px;}#splash .see-also li{position:relative;padding-top:4px;width:216px;}#splash .see-also li a{font-weight:normal;}#splash .column-1{position:relative;float:left;clear:left;display:inline;overflow:hidden;margin-left:-232px;}#splash .column-2{clear:none;position:relative;}.ie #splash .column-2,.ie7 #splash .column-2{float:right;}.ie7 #splash .see-also,.ie #splash .see-also{width:215px;}.ie7 #splash .see-also li.column-2,.ie #splash .see-also li.column-2{width:215px;}#splash .see-also .first-child-live{margin-top:-1px;float:none;display:block;width:438px;}body #blq-container #blq-container-inner #splash .see-also .first-child-live a.story{font-weight:bold;}body #blq-container #blq-container-inner #splash .see-also a.is-index{font-weight:bold;}.ie #splash .see-also .first-child-live,.ie7 #splash .see-also .first-child-live{left:-16px;}#splash .gvl3-icon{position:absolute;top:3px;left:0;}.ie7 #splash .gvl3-icon,.ie #splash .gvl3-icon{top:5px;}#splash .splash-header .gvl3-icon{top:3px;}.ie7 #splash .splash-header .gvl3-icon{top:7px;}.ie #splash .splash-header .gvl3-icon{top:7px;}#splash .first-child-live .gvl3-icon{top:4px;}.ie #splash .first-child-live .gvl3-icon{top:3px;}.ie #splash .splash-header .gvl3-icon-boxedlisten,.ie #splash .splash-header .gvl3-icon-boxedwatch{left:-21px;}#splash .has-icon-live .gvl3-icon{top:4px;}.ie7 #splash .has-icon-live .gvl3-icon,.ie #splash .has-icon-live .gvl3-icon{top:6px;}#splash .gvl3-icon-wrapper{position:absolute;top:5px;left:-528px;}.ie7 #splash .gvl3-icon-wrapper{top:10px;}.ie #splash .gvl3-icon-wrapper{top:10px;left:-528px;}#splash .gvl3-icon-wrapper .gvl3-icon{position:relative;top:auto;left:auto;}.ie7 #splash .gvl3-icon-wrapper .gvl3-icon,.ie #splash .gvl3-icon-wrapper .gvl3-icon{top:auto;}.ie7 .splash-top-story h2.has-icon-boxedwatch,.ie .splash-top-story h2.has-icon-boxedwatch{border:1px solid #fff;}.ie7 #splash .splash-header,.ie #splash .splash-header{border:1px solid #fff;}.splash-top-story .splash-emp object{display:block;}#splash .splash-emp .warning img{width:512px;height:288px;}.split-column-now-layout{clear:both;margin:0 0 16px 0;}.split-column-now-layout .secondary-top-story{clear:both;border:none;}.split-column-now-layout .split-column-now-heading,.split-column-now-layout .secondary-top-story-heading{clear:both;font-size:1.846em;font-weight:bold;letter-spacing:-1px;line-height:24px;border-top:1px solid #ccc;border-bottom:1px solid #ccc;padding:8px 0;}.secondary-top-story .secondary-top-story-heading{clear:both;font-size:1.846em;font-weight:bold;letter-spacing:-1px;line-height:24px;border-bottom:1px solid #ccc;padding:3px 0 8px 0;margin:0 0 16px 0;}#ticker{z-index:10;}.blq-js .ie #ticker{height:8px;}.has-ticker #ticker{min-height:41px;}.blq-js .ie .has-ticker #ticker{height:41px;}#ticker-container{position:relative;display:block;overflow:hidden;clear:both;padding-bottom:4px;min-height:41px;}.ie #ticker-container{height:41px;}#ticker{position:relative;display:block;overflow:hidden;clear:both;}.ie7 #ticker{filter:none;}#tickerHolder{position:relative;display:block;overflow:hidden;clear:both;}.ticker_container{color:#1F527B;background-color:#fff;position:relative;display:block;overflow:visible;clear:both;margin-top:4px;padding-bottom:4px;border-bottom:1px solid #ddd;}.ie .ticker_container{height:1%;}.renderer_output{position:relative;display:block;overflow:hidden;clear:both;width:100%;height:32px;}.ticker_container div.title_container,.ticker_container p.ticker_content{position:relative;z-index:1;}.ticker_container h4.ticker_title,.ticker_container p.ticker_content,.ticker_container li{float:left;}.ticker_container p.ticker_content{margin-left:8px;}.ticker_container ul.ticker_controls{float:right;margin-right:-8px;margin-top:-16px;padding-right:8px;}.ticker_container p.ticker_content{white-space:nowrap;width:auto;background:#fff;}.ticker_container p.ticker_content a{font-weight:normal;}.ticker_warning .ticker_container p.ticker_content a{font-weight:bold;}.ticker_container a.media_type{float:left;background:url(../../../../../1_4_9/cream/hi/shared/img/gvl3-icons-0-2.png) no-repeat scroll 0 0 transparent;cursor:pointer;display:block;font-size:1px;height:15px;line-height:1px;margin:0;overflow:hidden;position:relative;text-indent:-5000px;z-index:10;}.ticker_container a.ticker_audio{background-position:-907px 0;width:16px;}.ticker_container a.ticker_video{background-position:-1301px 0;width:16px;}.ticker_container a.ticker_live{background-position:-973px 0;width:27px;}.ticker_container .ticker_controls span{background:url(../../../../../1_4_9/cream/hi/shared/img/gvl3-icons-0-2.png) no-repeat scroll 0 0 transparent;cursor:pointer;display:block;font-size:1px;height:15px;line-height:1px;margin:0 8px 5px 0;overflow:hidden;position:relative;text-indent:-5000px;z-index:10;opacity:.7;}.ie .ticker_container .ticker_controls span{filter:alpha(opacity=70);}.ticker_container .ticker_controls a:hover span,.ticker_container .ticker_controls a:focus span{opacity:1;}.ie .ticker_container .ticker_controls a:hover span,.ie .ticker_container .ticker_controls a:focus span{filter:alpha(opacity=100);}.ticker_container .ticker_controls .started span{background-position:-27px 0;width:12px;}.ticker_container .ticker_controls .stopped span{background-position:-5px 0;width:11px;}.ticker_container .ticker_controls .prev span{background-position:-224px 0;width:10px;}.ticker_container .ticker_controls .next span{background-position:-247px 0;width:10px;}.ticker_container .ticker_controls .started a:active span{background-position:-27px -16px;}.ticker_container .ticker_controls .stopped a:active span{background-position:-5px -16px;}.ticker_container .ticker_controls .prev a:active span{background-position:-224px -16px;}.ticker_container .ticker_controls .next a:active span{background-position:-247px -16px;}.title_container,.ticker_title{float:left;display:inline;overflow:visible;}.title_container,.ticker_content{padding:8px 0 8px;}.ticker_warning div.title_container{padding:8px 0 8px 8px;}.ticker_warning p.ticker_content{white-space:nowrap;color:#fff;background:transparent;}.ticker_warning .bg_bar{background:#d60000;left:0;position:absolute;width:100%;height:29px;}.ticker_warning .in_sequence{display:none;}.ticker_warning .ticker_container .ticker_controls span{opacity:1;}.ie .ticker_warning .ticker_container .ticker_controls span{filter:alpha(opacity=100);}.ticker_warning .ticker_container h4.ticker_title{text-transform:uppercase;font-weight:bold;color:#fff;font-size:1.1em;}.ticker_warning .ticker_container p.ticker_content a.ticker_content_anchor{color:#fff;font-size:1.1em;}.ticker_warning .ticker_container .ticker_controls .started span,.ticker_warning .ticker_container .ticker_controls .started a:active span{background-position:-27px -32px;}.ticker_warning .ticker_container .ticker_controls .stopped span,.ticker_warning .ticker_container .ticker_controls .stopped a:active span{background-position:-5px -32px;}.ticker_warning .ticker_container .ticker_controls .prev span,.ticker_warning .ticker_container .ticker_controls .prev a:active span{background-position:-224px -32px;}.ticker_warning .ticker_container .ticker_controls .next span,.ticker_warning .ticker_container .ticker_controls .next a:active span{background-position:-247px -32px;}.ticker_warning .ticker_container a.ticker_audio{background-position:-907px -32px;}.ticker_warning .ticker_container a.ticker_video{background-position:-1301px -32px;}.ticker_warning .ticker_container a.ticker_live{background-position:-973px -32px;}div.ticker_container h4.ticker_title{font-size:1em;font-weight:bold;text-transform:capitalize;padding-right:8px;background:#fff;}.ticker_warning div.ticker_container h4.ticker_title{font-size:1em;font-weight:bold;text-transform:uppercase;background:transparent;}.ticker_container p.ticker_content{position:relative;margin-left:0;}.ticker_container p.has-icon-ticker_audio{padding-left:21px;}.ticker_container p.has-icon-ticker_video{padding-left:21px;}.ticker_container p.has-icon-ticker_live{padding-left:35px;}.ticker_container .media_type{display:block;background:url(../../../../../1_4_9/cream/hi/shared/img/gvl3-icons-0-2.png) no-repeat scroll 0 0 transparent;cursor:pointer;font-size:1px;height:15px;line-height:1px;margin:0 8px 0 0;overflow:hidden;position:absolute;top:7px;left:0;text-indent:-5000px;z-index:10;opacity:.7;}.ie .ticker_container .media_type,.ie .ticker_container .ticker_controls span{background-image:url(../../../../../1_4_9/cream/hi/shared/img/gvl3-icons-0-2.gif);filter:alpha(opacity=70);}.ticker_container .ticker_audio{background-position:-1323px 0;width:16px;}.ticker_container a:hover .ticker_audio,.ticker_container a:focus .ticker_audio{opacity:1;}.ie .ticker_container a:hover .ticker_audio,.ie .ticker_container a:focus .ticker_audio{filter:alpha(opacity=100);}.ticker_container a:active .ticker_audio{background-position:-1323px -16px;}.ticker_warning .ticker_container .ticker_audio,.ticker_warning .ticker_container a:active .ticker_audio{background-position:-1323px -32px;opacity:1;filter:alpha(opacity=100);}.ticker_container .ticker_video{background-position:-1301px 0;width:16px;}.ticker_container a:hover .ticker_video,.ticker_container a:focus .ticker_video{opacity:1;background-position:-1301px -16px;}.ie .ticker_container a:hover .ticker_video,.ie .ticker_container a:focus .ticker_video{filter:alpha(opacity=100);}.ticker_container a:active .ticker_video{background-position:-1301px -16px;}.ticker_warning .ticker_container .ticker_video,.ticker_warning .ticker_container a:active .ticker_video{background-position:-1301px -32px;opacity:1;filter:alpha(opacity=100);}.ticker_container .ticker_live,.ticker_container a:hover .ticker_live,.ticker_container a:focus .ticker_live,.ticker_container a:active .ticker_live{background-position:-973px 0;width:27px;opacity:1;margin-top:1px;}.ie .ticker_container .ticker_live{filter:alpha(opacity=100);}.ticker_warning .ticker_container .ticker_live,.ticker_warning .ticker_container a:hover .ticker_live,.ticker_warning .ticker_container a:focus .ticker_live,.ticker_warning .ticker_container a:active .ticker_live{background:url(../../../../../1_4_9/cream/hi/shared/img/gvl3-live-icon-inverted.gif) no-repeat scroll 0 1px transparent;opacity:1;filter:alpha(opacity=100);}.ticker_container ul.ticker_controls{position:absolute;margin-top:0;top:8px;right:0;}.ticker_warning .ticker_container p.ticker_content a.ticker_content_anchor{font-size:1em;text-shadow:0 0 1px solid rgba(0,0,0,0.5);}#tickerHolder .spacer{position:absolute;}.ticker_warning .bg_bar{height:100%;margin-bottom:-2px;}.static-ticker h2{padding:4px 0;font-size:1.3em;}.static-ticker li{padding:4px 0;border-top:1px solid #E6E6E6;text-transform:uppercase;}.static-ticker li a{text-transform:none;}.static-ticker{clear:both;position:relative;border-bottom:1px solid #E6E6E6;font-size:1em;}.ticker ul.tickerItem .tickerEntry{margin:8px 0;}.ticker ul.tickerItem .tickerEntry .tickerPrompt{font-weight:bold;margin-right:4px;}.ticker ul.tickerItem .tickerEntry .tickerHeadline a.story{position:relative;}.ticker ul.tickerItem .tickerEntry .tickerHeadline .has-icon-boxedlisten a.story,.ticker ul.tickerItem .tickerEntry .tickerHeadline .has-icon-boxedwatch a.story{padding-left:23px;}.ticker ul.tickerItem .tickerEntry .tickerHeadline .has-icon-boxedlive a.story{padding-left:33px;}.ticker ul.tickerItem .tickerEntry .tickerHeadline .gvl3-icon{position:absolute;left:0;top:0;}.ticker .hidden{position:absolute;left:-9999px;}.top-stories-range-module{clear:both;position:relative;overflow:hidden;width:336px;margin:16px 0 16px;padding-top:4px;padding-bottom:4px;background:#ededed;}.top-stories-range-module a:hover .new-story-icon,.top-stories-range-module a:focus .new-story-icon{background:#ededed;}.bbccom_slot_xxl #main-content .top-stories-range-module{width:100%;}.top-stories-range-module .top-stories-range-module-header{padding:4px 8px 7px;}.top-stories-range-module ul{overflow:visible;padding:4px 8px 12px;}.top-stories-range-module li{position:relative;float:left;display:inline;width:100%;padding-bottom:8px;}.top-stories-range-module .gvl3-icon{position:absolute;top:0;left:0;}.top-stories-range-module .medium-image{width:144px;padding-left:151px;padding-bottom:14px;margin-bottom:-1px;margin-top:-4px;}.bbccom_slot_xxl .top-stories-range-module .medium-image{width:320px;}.top-stories-range-module .medium-image img{position:relative;float:left;display:inline;width:144px;height:81px;margin-left:-151px;margin-top:3px;margin-bottom:0;}.top-stories-range-module .medium-image .gvl3-icon-wrapper{position:absolute;top:3px;left:0;}.top-stories-range-module .medium-image .gvl3-icon{position:relative;top:auto;left:auto;}#top-story{position:relative;overflow:hidden;width:624px;margin:0;padding-top:3px;padding-bottom:5px;border-top:1px solid #ddd;}#top-story .top-story-header{width:624px;padding-bottom:8px;width:auto;}#top-story .top-story-header a{line-height:1;}#top-story.medium-image{width:464px;padding-left:160px;padding-bottom:9px;}#top-story.medium-image .top-story-header{position:relative;margin-left:-160px;}#top-story.medium-image .top-story-header img{position:relative;margin-bottom:5px;margin-left:-160px;left:160px;width:144px;height:81px;}.ie #top-story.large-image .top-story-header img{margin-top:7px;top:-7px;}#top-story.large-image{width:304px;padding-left:320px;margin-bottom:8px;position:relative;zoom:1;}#top-story.large-image .top-story-header{position:relative;margin-left:-320px;}#top-story.large-image .top-story-header img{position:relative;margin-bottom:0;margin-left:-320px;left:320px;width:304px;height:171px;zoom:1;}.ie #top-story.large-image .top-story-header{padding-left:320px;}#top-story.classic-image{width:382px;padding-left:242px;padding-bottom:9px;}#top-story.classic-image .top-story-header{position:relative;margin-left:-242px;}#top-story.classic-image .top-story-header img{position:relative;margin-bottom:4px;margin-left:-242px;left:242px;width:226px;}.ie #top-story.classic-image .top-story-header,.ie #top-story.classic-image p,.ie #top-story.classic-image .see-also{padding-left:242px;}#top-story.medium-image .top-story-header img,#top-story.large-image .top-story-header img,#top-story.classic-image .top-story-header img{float:left;display:inline;margin-top:42px;}.ie #top-story.medium-image .top-story-header img,.ie #top-story.large-image .top-story-header img,.ie #top-story.classic-image .top-story-header img,.ie7 #top-story.medium-image .top-story-header img,.ie7 #top-story.large-image .top-story-header img,.ie7 #top-story.classic-image .top-story-header img{margin-top:3px;}.firefox-older-than-3-5 #top-story.medium-image .top-story-header img,.firefox-older-than-3-5 #top-story.large-image .top-story-header img,.firefox-older-than-3-5 #top-story.classic-image .top-story-header img{margin-top:8px;}#top-story.no-image{padding-bottom:18px;}#top-story.no-image .top-story-header{width:624px;position:relative;overflow:hidden;}#top-story.no-image p{max-width:624px;}#top-story.special-emp{width:224px;padding-left:400px;}#top-story.special-emp .top-story-header{width:624px;position:relative;overflow:hidden;margin-left:-400px;}#top-story.special-emp .top-story-emp{position:relative;float:left;display:inline;margin-top:2px;margin-bottom:3px;margin-left:-400px;width:384px;}.ie #top-story.special-emp .top-story-emp{margin-left:0;}.ie #top-story.special-emp .top-story-header{padding-left:400px;}#top-story.special-emp .caption{position:absolute;left:-5000%;overflow:hidden;}#top-story.slideshow{width:224px;padding-left:400px;}#top-story.special-emp p,#top-story.special-emp .see-also,#top-story.slideshow p,#top-story.slideshow .see-also{width:224px;max-width:224px!important;float:right;clear:right;display:block;}#top-story.slideshow .top-story-header{width:624px;position:relative;overflow:hidden;margin-left:-400px;}#top-story.slideshow #topStorySlideShow{position:relative;float:left;display:inline;overflow:hidden;margin-top:2px;margin-bottom:10px;margin-left:-400px;width:384px;height:216px;background:#ccc;}.ie #top-story.slideshow #topStorySlideShow{margin-left:0;}.ie #top-story.slideshow .top-story-header{padding-left:400px;}#top-story .gvl3-icon{position:absolute;top:3px;left:0;}#top-story .top-story-header .gvl3-icon{top:3px;}#top-story .top-story-header .gvl3-icon-live{top:4px;}.ie #top-story.special-emp .top-story-header .gvl3-icon,.ie #top-story.slideshow .top-story-header .gvl3-icon{left:400px;}#top-story .gvl3-icon-wrapper .gvl3-icon{position:relative;top:auto;left:auto;}#top-story .gvl3-icon-wrapper{position:absolute;bottom:-31px;left:0;}.ie7 #top-story .gvl3-icon-wrapper{bottom:-32px;}.ie #top-story.small-image .gvl3-icon-wrapper{bottom:84px;}.ie #top-story.medium-image .gvl3-icon-wrapper{bottom:56px;}.ie #top-story.large-image .gvl3-icon-wrapper{bottom:149px;}.ie #top-story.classic-image .gvl3-icon-wrapper{bottom:149px;}#top-story p{max-width:464px;}.ie7 #top-story.large-image p{float:left;}#top-story .see-also{float:left;display:inline;width:100%;margin-bottom:-1px;position:relative;zoom:1;}.ie #top-story .see-also{float:right;}#top-story.special-emp .see-also,#top-story.slideshow .see-also{float:right;}#top-story.medium-image .see-also{float:right;clear:none;width:224px;padding-left:240px;margin-bottom:0;padding-bottom:11px;}#top-story.medium-image .see-also li{float:none;clear:none;display:block;margin-bottom:4px;}#top-story.medium-image .see-also .column-1{position:relative;float:left;clear:left;display:inline;overflow:hidden;width:224px;margin-left:-240px;}#top-story.medium-image .see-also .column-2{width:224px;}.ie #top-story.medium-image .see-also .column-2,.ie7 #top-story.medium-image .see-also .column-2{float:right;}#top-story .see-also li{position:relative;float:left;display:inline;clear:both;padding-top:4px;}#top-story.special-emp .see-also li,#top-story.slideshow .see-also li{width:224px;float:right;}#top-story.large-image .see-also li,#top-story.large-image p{width:304px;float:right;display:inline;}#top-story .see-also li a{font-weight:normal;}body #blq-container #blq-container-inner #top-story .see-also a.is-index{font-weight:bold;}#top-story.no-image .see-also{padding-left:320px;}#top-story.no-image .see-also .column-1{position:relative;float:left;clear:left;display:inline;overflow:hidden;margin-left:-320px;width:304px;}#top-story.no-image .see-also .column-2{clear:none;float:none;display:block;position:relative;width:304px;}#top-story.small-image .see-also{width:232px;padding-left:232px;float:right;display:inline;clear:none;}#top-story.small-image .see-also .column-1{position:relative;float:left;clear:left;display:inline;overflow:hidden;margin-left:-232px;width:216px;}#top-story.small-image .see-also .column-2{clear:none;float:none;display:block;position:relative;width:216px;}.ie #top-story.no-image .see-also .column-2,.ie #top-story.small-image .see-also .column-2{float:right;}.ie #top-story hr,.ie7 #top-story hr{margin:-9px 0 -4px;display:block;}.ie8 #top-story .top-story-emp{margin-top:-12px;}#top-story .top-story-emp object{display:block;}.ie #top-story.medium-image .top-story-header img,.ie7 #top-story.medium-image .top-story-header img{padding-bottom:14px;}.ie8 #top-story.special-emp .top-story-emp{margin-top:2px;}.container-hyper-topic-cluster,.container-hyper-topic-cluster .hyperpuff{position:relative;display:block;overflow:auto;clear:both;width:624px;}.topic-cluster{position:relative;overflow:auto;width:624px;margin:0 0 16px;padding-top:0;border-top:1px solid #ddd;clear:both;}.topic-cluster .topic-cluster-header{position:relative;margin-bottom:0;padding:7px 0 8px;border-bottom:1px solid #ddd;}.topic-cluster-stories{position:relative;overflow:hidden;padding-left:320px;width:304px;margin:-1px 0 12px;}.topic-cluster .topic-cluster-stories{margin-bottom:0;}.topic-cluster-stories li{position:relative;padding-top:8px;}.topic-cluster .gvl3-icon{position:absolute;top:8px;left:0;}.topic-cluster .with-summary .gvl3-icon{top:9px;}.topic-cluster-stories .column-1{position:relative;float:left;clear:left;display:inline;overflow:hidden;width:304px;margin-left:-320px;}.topic-cluster-stories .column-2{clear:none;position:relative;width:304px;}.ie .topic-cluster-stories .column-2{float:right;clear:right;}.topic-cluster-stories .with-summary{padding-bottom:6px;padding-top:10px;margin-bottom:5px;border-top:1px solid #ddd;}.topic-cluster-stories p{position:relative;}.topic-cluster-stories .with-summary p{margin-top:4px;}.topic-cluster-stories .first-child p{margin-bottom:1px;}.topic-cluster-stories h2 a.from-external-source,.topic-cluster-stories h3 a.from-external-source{display:block;text-indent:0;margin-left:0;}.container-hyper-topic-cluster .hyper-container-title{position:relative;border-top:none;margin:0;padding-top:0;width:624px;}.container-hyper-topic-cluster .hyper-container-title .hyper-depth-header{border-top:1px solid #DDD;margin-top:0;margin-bottom:0;padding:7px 0 8px;position:relative;}.ie .topic-cluster .topic-cluster-header a,.ie .container-hyper-topic-cluster .hyper-container-title .hyper-depth-header a,.ie7 .container-hyper-topic-cluster .hyper-container-title .hyper-depth-header a{line-height:24px;}.traffic-travel{background-color:#ededed;padding:0 8px 8px;margin:0 0 1px;}.traffic-travel h2{font-size:1.846em;line-height:1.846em;letter-spacing:-0.04em;color:#1F527B;}.traffic-travel h3{color:#666;font-size:1.2em;line-height:1.2em;letter-spacing:-0.04em;padding-bottom:12px;}.traffic-travel .incident-box{padding:6px 8px 6px 40px;background:#fff url(../../../../../1_4_9/cream/hi/shared/img/traffic_icon.gif) 8px 8px no-repeat;}.traffic-travel .incident-box p{color:#666;}.useful-links{position:relative;clear:both;overflow:hidden;width:100%;margin:0 0 12px;border-top:1px solid #ddd;}.useful-links .useful-links-header{position:relative;padding:7px 0 8px 0;border-bottom:1px solid #ddd;}.useful-links ul{position:relative;overflow:hidden;width:304px;padding:11px 0 0 320px;}.useful-links li{padding-bottom:8px;position:relative;}.useful-links .gvl3-icon{position:absolute;top:0;left:0;}.story .useful-links .gvl3-icon{left:8px;top:8px;}.useful-links .gvl3-icon-wrapper{position:absolute;top:0;}.useful-links .gvl3-icon-wrapper .gvl3-icon{position:relative;top:auto;left:auto;}.useful-links .column-1{position:relative;float:left;clear:left;display:inline;overflow:hidden;width:304px;margin-left:-320px;}.useful-links .column-2{clear:none;position:relative;width:304px;}.ie .useful-links .column-2{float:right;}.useful-links a{padding-right:16px;}.double-useful-links{clear:both;position:relative;width:100%;overflow:auto;display:block;}.double-useful-links .useful-links{float:left;clear:none;width:304px;}.double-useful-links .useful-links-2{margin-left:16px;}.double-useful-links .useful-links ul{padding-left:0;}.double-useful-links .useful-links li{margin-left:0;float:none;display:block;}.weather-3day{position:relative;overflow:hidden;clear:both;background-color:#ededed;padding:0 8px;margin:0 0 16px;width:320px;}.weather-3day h2{position:relative;display:inline;float:left;padding:8px 0 7px;}.weather-3day h2 a{line-height:24px;}.weather-3day h3{clear:left;color:#505050;margin:0 0 14px;}.weather-3day h4{font-weight:normal;}.weather-3day li.first{background:#fff;display:block;margin:0 0 6px;padding:4px 0 4px 5px;}.ie .weather-3day li.first{display:inline-block;}.weather-3day li.help{position:absolute;right:0;top:7px;}.weather-3day .next3daysweather{clear:both;border-bottom:medium none;margin:0;width:100%;}.weather-3day .next3daysweather .stripes{float:left;display:inline;background:#fff;margin:0;width:320px;}.weather-3day .next3daysweather .stripes li,.weather-3day .next3daysweather .stripes li strong{font-weight:400;color:#505050;}.weather-3day .next3daysweather .stripes li.wind{display:none;}.weather-3day .next3daysweather .stripes li strong{font-weight:800;}.weather-3day .next3daysweather .stripes div.time{float:left;padding:4px 8px 12px;border-left:2px solid #ededed;width:106px;min-height:152px;}.ie .weather-3day .next3daysweather .stripes div.time{height:152px;}.weather-3day .next3daysweather .stripes div.c1{border-left:none;width:87px;}.weather-3day .next3daysweather .stripes div.c2{width:94px;}.weather-3day .next3daysweather .stripes div.c3{width:87px;}.weather-3day .next3daysweather .stripes img{margin:4px 0 7px;}.weather-3day .next3daysweather a{color:#369;}.weather-3day .next3daysweather a:hover{color:#0D3059;}.weather-3day .next3daysweather .stripes h3{font-weight:400;margin:0 0 3px;}.weather-3day .next3daysweather .clear{clear:both;}.weather-3day .weather-5day-forecast{padding:8px 0;}.weather-4items{position:relative;overflow:hidden;clear:both;background-color:#ededed;padding:0 8px 8px 8px;margin:0 0 16px;width:608px;}.weather-4items h2{position:relative;display:inline;float:left;padding:8px 0 7px;}.weather-4items h2 a{line-height:24px;}.weather-4items h3{clear:left;color:#505050;margin:0 0 14px;}.weather-4items h4{font-weight:normal;}.weather-4items li.first{background:#fff;display:block;margin:0 0 6px;padding:4px 0 4px 5px;}.ie .weather-4items li.first{display:inline-block;}.weather-4items li.help{position:absolute;right:0;top:7px;}.weather-4items .fruitmachine{clear:both;border-bottom:medium none;margin:0;width:100%;}.weather-4items .fruitmachine .stripes{float:left;display:inline;background:#fff;margin:0;width:608px;}.ie .weather-4items .fruitmachine .stripes{margin-bottom:-6px;}.weather-4items .fruitmachine .stripes li,.weather-4items .fruitmachine .stripes li strong{font-weight:400;color:#505050;}.weather-4items .fruitmachine .stripes li.wind{display:none;}.weather-4items .fruitmachine .stripes li strong{font-weight:800;}.weather-4items .fruitmachine .stripes div.time{float:left;padding:4px 8px 12px;border-left:2px solid #ededed;width:106px;padding-bottom:200px;margin-bottom:-184px;}.ie .weather-4items .fruitmachine .stripes div.time{height:152px;}.weather-4items .fruitmachine .stripes div.c1{border-left:none;}.weather-4items .fruitmachine .stripes div.c1,.weather-4items .fruitmachine .stripes div.c4{padding-right:8px;width:133px;}.weather-4items .fruitmachine .stripes div.c2,.weather-4items .fruitmachine .stripes div.c3{padding-right:8px;width:134px;}.weather-4items .fruitmachine .stripes div.time ul{margin-right:51px;}.weather-4items .fruitmachine .stripes img{margin:4px 0 7px;}.weather-4items .fruitmachine a{color:#369;}.weather-4items .fruitmachine a:hover{color:#0D3059;}.weather-4items .fruitmachine .stripes h3{font-weight:400;margin:0 0 3px;}.weather-4items .fruitmachine .clear{clear:both;}.weather-4items .weather-5day-forecast{padding:8px 0;}.weather-4items h4{font-weight:bold;margin:8px 0 9px;}.weather-4items p.weather-home{position:absolute;right:8px;top:15px;}#www-bbcarabic-com h4.digest-story-header a{font-family:"Arabic Transparent","Simplified Arabic",arial,verdana,sans-serif;}.ie7 .digest-world-service #www-bbcarabic-com .rtl li a{right:0;}.ie7 #www-bbcarabic-com.digest .digest-header{height:100%;} \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/accordian_overlay.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/accordian_overlay.png
new file mode 100755
index 0000000000..72f7a23cb6
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/accordian_overlay.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/arrow_foldout.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/arrow_foldout.gif
new file mode 100755
index 0000000000..1e2d14325a
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/arrow_foldout.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/arrow_foldout.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/arrow_foldout.png
new file mode 100755
index 0000000000..fd1304a9d7
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/arrow_foldout.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/england-map.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/england-map.png
new file mode 100755
index 0000000000..005e4216ed
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/england-map.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/geo-digest-vertical-panel.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/geo-digest-vertical-panel.gif
new file mode 100755
index 0000000000..8114fe9cad
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/geo-digest-vertical-panel.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/languages-sprite.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/languages-sprite.gif
new file mode 100755
index 0000000000..d4ac275cb3
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/languages-sprite.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/livestats-sprite-ko.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/livestats-sprite-ko.png
new file mode 100755
index 0000000000..7c765379b6
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/livestats-sprite-ko.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/livestats-sprite.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/livestats-sprite.gif
new file mode 100755
index 0000000000..b22c386aab
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/livestats-sprite.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/nav-divider.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/nav-divider.png
new file mode 100755
index 0000000000..ce9c3f9dbf
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/nav-divider.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/news_masthead.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/news_masthead.gif
new file mode 100755
index 0000000000..3677465aab
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/news_masthead.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/personalisation-help-icon.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/personalisation-help-icon.gif
new file mode 100755
index 0000000000..c2c8a64226
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/personalisation-help-icon.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/programmes-iplayer-brand.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/programmes-iplayer-brand.png
new file mode 100755
index 0000000000..22eb3c1656
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/programmes-iplayer-brand.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/red-masthead.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/red-masthead.png
new file mode 100755
index 0000000000..54522d61e0
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/red-masthead.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/roadicon.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/roadicon.gif
new file mode 100755
index 0000000000..b0b198a2ad
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/roadicon.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/scotland-map-hover.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/scotland-map-hover.png
new file mode 100755
index 0000000000..e17928dbae
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/scotland-map-hover.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/scotland-map.png@v.2 b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/scotland-map.png@v.2
new file mode 100755
index 0000000000..4346143bf0
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/scotland-map.png@v.2
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/sprite_most_watched.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/sprite_most_watched.gif
new file mode 100755
index 0000000000..714c92fedd
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/sprite_most_watched.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/sprite_most_watched_ko.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/sprite_most_watched_ko.png
new file mode 100755
index 0000000000..7f5a87de80
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/sprite_most_watched_ko.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/story_sprite.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/story_sprite.gif
new file mode 100755
index 0000000000..7292332f3a
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/story_sprite.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/subnav-divider.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/subnav-divider.png
new file mode 100755
index 0000000000..111499fac5
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/subnav-divider.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/wales-map-hover.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/wales-map-hover.png
new file mode 100755
index 0000000000..481825a9e5
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/wales-map-hover.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/wales-map.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/wales-map.png
new file mode 100755
index 0000000000..1c53b2d8c2
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/wales-map.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/world-map.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/world-map.png
new file mode 100755
index 0000000000..4b6488f626
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/img/world-map.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/skin.css b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/skin.css
new file mode 100755
index 0000000000..0bc2f152e3
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_11/cream/hi/news/skin.css
@@ -0,0 +1 @@
+#blq-container-inner{background:transparent;}#blq-main{background:transparent;}#blq-main #main-content{position:relative;}#blq-main h1.banner{height:50px;font-size:1.8em;line-height:1.6em;padding:10px 0 0 112px;font-family:verdana;font-weight:normal;color:#999;}#blq-main h1.banner span{position:absolute;top:-5000px;left:-5000px;}body{background:#fff url(img/red-masthead.png) repeat-x top center;}body #blq-container{background:url(img/red-masthead.png) repeat-x scroll center top;}#header a{background:transparent url(img/news_masthead.gif) no-repeat top left;}#nav a{background:url(img/nav-divider.png) no-repeat center left;}#sub-nav a{background:url(img/subnav-divider.png) no-repeat center left;}.emp{position:relative;}.birmingham-and-black-country #header .section-title{font-size:26px;top:4px;}.south-yorkshire #header .section-title{font-size:28px;}.coventry-and-warwickshire #header .section-title{font-size:28px;}.edinburgh-east-and-fife #header .section-title{font-size:24px;top:5px;}.glasgow-and-west #header .section-title{font-size:28px;}.north-east-orkney-and-shetland #header .section-title{font-size:23px;top:5px;}.tayside-and-central #header .section-title{font-size:24px;top:5px;} \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/global.css b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/global.css
new file mode 100755
index 0000000000..6e81a8f021
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/global.css
@@ -0,0 +1 @@
+ #blq-pre-mast{z-index:12;}#blq-pre-mast .pulse-pop{position:absolute;left:700px;}#blq-container.blq-gvl-3{background:transparent;}#blq-container-inner{overflow:hidden;}#blq-container.blq-gvl-3 .blq-foot-text-dark a:hover,#blq-container.blq-gvl-3 .blq-foot-text-dark a:focus{color:#4c4c4c;text-decoration:underline;}abbr{border:none;}img{-webkit-user-select:none;}#blq-foot{border-top:none;}#blq-main{position:relative;clear:both;min-width:976px;background:#fff;}#header-wrapper{position:relative;clear:both;overflow:hidden;width:100%;background:transparent;}#header{position:relative;clear:both;overflow:hidden;width:976px;margin:0 auto;padding:14px 0 16px;}#header a{position:relative;float:left;display:inline;margin:0 0 0 10px;width:124px;height:34px;text-indent:-5000px;overflow:hidden;}#header .section-title{position:relative;float:left;display:inline;margin-left:20px;margin-bottom:-7px;top:3px;}#header .section-updated{position:relative;float:left;display:inline;top:21px;margin-left:20px;}#header .section-updated .date,#header .section-updated .time-text,#header .section-updated .time{font-size:.923em;}#header .section-updated .date{color:#fff;text-shadow:0 0 1px rgba(0,0,0,0.1);}#header .section-updated .time-text,#header .section-updated .time{font-weight:normal;color:#fff;text-shadow:0 0 1px rgba(0,0,0,0.1);}#header .section-updated .time-text{padding-left:4px;}a#rss-alternative{position:absolute;right:0;top:35px;margin-right:25px;overflow:visible;color:white;}a#rss-alternative .gvl3-icon{position:absolute;top:0;right:0;margin-right:-17px;}h2.nav-title{position:absolute;left:-50000px;}#nav{clear:both;position:relative;overflow:auto;width:976px;margin:0 auto;background:#3E0C0D;}#nav li{position:relative;float:left;display:inline;list-style:none;}#nav a{display:block;padding:4px 6px 4px 6px;font-size:.923em;}#nav .first-child a{background:none;}#nav .selected{margin-right:-1px;z-index:1;}#nav .selected a{background:#ededed;padding-right:7px;color:#505050!important;}#sub-nav{position:relative;clear:both;overflow:auto;width:976px;margin:0 auto;background:#ededed;}#sub-nav li{position:relative;float:left;display:inline;list-style:none;}#sub-nav a{position:relative;display:block;padding:4px 8px 4px 8px;font-size:.923em;color:#505050;}#sub-nav .first-child a{background:none;padding:4px 8px 4px 6px;}#sub-nav li.selected{margin-right:-1px;z-index:1;}#sub-nav .selected a{padding-right:7px;background:#fff;color:#D2700F;}#content-wrapper{position:relative;clear:both;margin:0 auto;width:976px;overflow:hidden;background:#fff;}.ie #content-wrapper{height:100%;overflow:visible;}.ie7 #content-wrapper{height:100%;}.slideshow .nav{overflow:hidden;text-indent:-5000px;position:absolute;left:0;margin-top:60px;width:100%;height:60px;z-index:50;background:transparent url(img/transparencies/rgba-0-0-0-07.png) repeat;}.ie .slideshow .nav{background:black;filter:alpha(opacity=70);}.slideshow .nav .controls{position:relative;margin:9px auto 3px;width:64px;}.slideshow .nav .next{position:absolute;top:0;right:0;height:16px;width:10px;overflow:hidden;text-indent:-50000px;background:transparent url(img/gvl3-icons-0-2.png) no-repeat;background-position:-247px -32px;cursor:pointer;}.slideshow .nav .previous{position:absolute;top:0;left:0;height:16px;width:10px;overflow:hidden;text-indent:-50000px;background:transparent url(img/gvl3-icons-0-2.png) no-repeat;background-position:-224px -32px;cursor:pointer;}.slideshow .nav .playpause_button{position:absolute;display:block;height:16px;top:0;left:26px;background:transparent url(img/gvl3-icons-0-2.png) no-repeat;font-size:1px;text-indent:-5000px;overflow:hidden;cursor:pointer;}.ie .slideshow .nav .next,.ie .slideshow .nav .previous,.ie .slideshow .nav .playpause_button{background-image:url(img/gvl3-icons-0-2.gif);}.slideshow .nav .pause{width:12px;background-position:-27px -32px;}.slideshow .nav .play{width:12px;left:27px;background-position:-5px -32px;}.slideshow .imageposition{position:relative;text-align:center;width:100%;padding:7px 0 6px;text-indent:0;color:#f0f0f0;text-shadow:0 0 1px rgba(0,0,0,0.5);}.from-external-source{position:relative;white-space:nowrap;cursor:pointer;color:#888;font-weight:bold;margin:0;font-family:Arial;font-size:11px;text-transform:uppercase;line-height:16px;width:1%;white-space:nowrap;}.new-story-icon{padding-left:4px;padding-bottom:5px;font-size:11px;text-transform:uppercase;line-height:16px;letter-spacing:0;font-weight:bold;color:#900;}a:hover .new-story-icon,a:focus .new-story-icon{text-decoration:none;background:#fff;}label{-webkit-transition:color .2s ease-in;}label.input-has-content{-webkit-transition:color .1s ease-in;}#related-services{position:relative;clear:both;overflow:hidden;width:976px;margin:0 auto;padding:0 0 12px;}#related-services h2{padding:4px 0 8px;}#news-services{position:relative;float:left;display:inline;width:640px;}#news-services li{position:relative;float:left;display:inline;width:128px;padding:48px 0 0;}#news-services li .services-icon{position:absolute;left:0;overflow:hidden;text-indent:-5000px;cursor:pointer;}#news-services li#service-feeds .services-icon{top:12px;width:32px;height:32px;background:url(img/services-rss.gif) no-repeat top left;}#news-services li#service-mobile .services-icon{top:5px;width:24px;height:39px;background:url(img/services-mobile.gif) no-repeat top left;}#news-services li#service-podcasts .services-icon{top:8px;width:24px;height:36px;background:url(img/services-podcast.gif) no-repeat top left;}#news-services li#service-alerts .services-icon{top:16px;width:32px;height:27px;background:url(img/services-alert.gif) no-repeat top left;}#news-services li#service-email-news .services-icon{top:20px;width:32px;height:23px;background:url(img/services-mail.gif) no-repeat top left;}#news-related-sites{position:relative;float:left;display:inline;width:160px;padding-left:172px;}#news-related-sites h2{position:relative;margin-left:-172px;}#news-related-sites .column-1{position:relative;float:left;display:inline;clear:left;margin-left:-172px;width:332px;}#cps-info{display:none;}.livestats-web-bug{position:absolute;left:-5000%;width:1px;height:1px;}hr{position:relative;background:transparent;height:1px;overflow:hidden;margin-top:-1px;display:block;clear:both;border-color:transparent;visibility:hidden;line-height:1px;}.has-icon-watch{text-indent:15px;}.has-icon-listen{text-indent:21px;}.has-icon-boxedlive,.has-icon-live{text-indent:32px;}.has-icon-boxedwatch{text-indent:21px;}.has-icon-boxedlisten{text-indent:21px;}.ie .has-icon-boxedlive{text-indent:0;}.ie .has-icon-boxedlive a{padding-left:32px;}.gvl3-icon{position:relative;z-index:10;height:15px;background:transparent url(img/gvl3-icons-0-2.png) no-repeat;display:block;margin:0 8px 5px 0;font-size:1px;line-height:1px;text-indent:-5000px;overflow:hidden;cursor:pointer;opacity:.85;-webkit-user-select:none;}.ie7 .gvl3-icon{filter:alpha(opacity=85);}.ie .gvl3-icon{background-image:url(img/gvl3-icons-0-2.gif);filter:alpha(opacity=85);}a:hover .gvl3-icon,a:focus .gvl3-icon,.gvl3-icon-wrapper .gvl3-icon{opacity:1;}.ie a:hover .gvl3-icon,.ie a:focus .gvl3-icon,.ie .gvl3-icon-wrapper .gvl3-icon,.ie7 a:hover .gvl3-icon,.ie7 a:focus .gvl3-icon,.ie7 .gvl3-icon-wrapper .gvl3-icon{filter:alpha(opacity=100);}.gvl3-icon-wrapper{font-size:1px;line-height:1px;position:absolute;background:transparent url(img/transparencies/rgba-0-0-0-07.png) repeat;padding:8px 10px;cursor:pointer;}.ie .gvl3-icon-wrapper{background:black;filter:alpha(opacity=60);}a:hover .gvl3-icon-wrapper,a:focus .gvl3-icon-wrapper{background:black;filter:alpha(opacity=100);}a:active .gvl3-icon-wrapper{background:#d2700f;filter:alpha(opacity=100);}.gvl3-icon-wrapper .gvl3-icon{margin:0;}.gvl3-icon-wrapper .gvl3-icon-invert-listen{margin:0 -3px 0 -2px;}.gvl3-icon-watch{background-position:-5px 0;width:11px;}a:active .gvl3-icon-watch{background-position:-5px -16px;}.gvl3-icon-pause{background-position:-27px 0;width:12px;}a:active .gvl3-icon-pause{background-position:-27px -16px;}.gvl3-icon-rewind{background-position:-49px 0;width:12px;}a:active .gvl3-icon-rewind{background-position:-49px -16px;}.gvl3-icon-expand{background-position:-71px 0;width:13px;}a:active .gvl3-icon-expand{background-position:-71px -16px;}.gvl3-icon-popout{background-position:-93px 0;width:17px;}a:active .gvl3-icon-popout{background-position:-93px -16px;}.gvl3-icon-share{background-position:-115px 0;width:13px;}a:active .gvl3-icon-share{background-position:-115px -16px;}.gvl3-icon-volume{background-position:-137px 0;width:27px;}a:active .gvl3-icon-volume{background-position:-137px -16px;}.gvl3-icon-highdefinition{background-position:-181px 0;width:25px;}a:active .gvl3-icon-highdefinition{background-position:-181px -16px;}.gvl3-icon-previous{background-position:-224px 0;width:10px;}a:active .gvl3-icon-previous{background-position:-224px -16px;}.gvl3-icon-next{background-position:-247px 0;width:10px;}a:active .gvl3-icon-next{background-position:-247px -16px;}.gvl3-icon-zoomout{background-position:-269px 0;width:13px;}a:active .gvl3-icon-zoomout{background-position:-269px -16px;}.gvl3-icon-zoomin{background-position:-291px 0;width:13px;}a:active .gvl3-icon-zoomin{background-position:-291px -16px;}.gvl3-icon-pinpoint{background-position:-313px 0;width:9px;}a:active .gvl3-icon-pinpoint{background-position:-313px -16px;}.gvl3-icon-pinpoint-content{background-position:-335px 0;width:9px;}a:active .gvl3-icon-pinpoint-content{background-position:-336px -16px;}.gvl3-icon-reset{background-position:-357px 0;width:17px;}a:active .gvl3-icon-reset{background-position:-357px -16px;}.gvl3-icon-refresh{background-position:-379px 0;width:14px;}a:active .gvl3-icon-refresh{background-position:-379px -16px;}.gvl3-icon-lock{background-position:-401px 0;width:12px;}a:active .gvl3-icon-lock{background-position:-401px -16px;}.gvl3-icon-unlock{background-position:-423px 0;width:15px;}a:active .gvl3-icon-unlock{background-position:-423px -16px;}.gvl3-icon-search{background-position:-445px 0;width:13px;}a:active .gvl3-icon-search{background-position:-445px -16px;}.gvl3-icon-gridview{background-position:-467px 0;width:14px;}a:active .gvl3-icon-gridview{background-position:-467px -16px;}.gvl3-icon-listview{background-position:-489px 0;width:14px;}a:active .gvl3-icon-listview{background-position:-489px -16px;}.gvl3-icon-close{background-position:-511px 0;width:13px;}a:active .gvl3-icon-close{background-position:-511px -16px;}.gvl3-icon-yes{background-position:-533px 0;width:15px;}a:active .gvl3-icon-yes{background-position:-533px -16px;}.gvl3-icon-rate{background-position:-555px 0;width:15px;}a:active .gvl3-icon-rate{background-position:-555px -16px;}.gvl3-icon-top{background-position:-577px 0;width:14px;}a:active .gvl3-icon-top{background-position:-577px -16px;}.gvl3-icon-home{background-position:-599px 0;width:17px;}a:active .gvl3-icon-home{background-position:-599px -16px;}.gvl3-icon-print{background-position:-621px 0;width:15px;}a:active .gvl3-icon-print{background-position:-621px -16px;}.gvl3-icon-email{background-position:-643px 0;width:15px;}a:active .gvl3-icon-email{background-position:-643px -16px;}.gvl3-icon-help{background-position:-665px 0;width:13px;}a:active .gvl3-icon-help{background-position:-665px -16px;}.gvl3-icon-information{background-position:-687px 0;width:14px;}a:active .gvl3-icon-information{background-position:-687px -16px;}.gvl3-icon-alert{background-position:-709px 0;width:15px;}a:active .gvl3-icon-alert{background-position:-709px -16px;}.gvl3-icon-add{background-position:-731px 0;width:15px;}a:active .gvl3-icon-add{background-position:-731px -16px;}.gvl3-icon-favourite{background-position:-753px 0;width:13px;}a:active .gvl3-icon-favourite{background-position:-753px -16px;}.gvl3-icon-edit{background-position:-775px 0;width:13px;}a:active .gvl3-icon-edit{background-position:-775px -16px;}.gvl3-icon-rss{background-position:-797px 0;width:13px;}a:active .gvl3-icon-rss{background-position:-797px -16px;}.gvl3-icon-duration{background-position:-819px 0;width:15px;}a:active .gvl3-icon-duration{background-position:-819px -16px;}.gvl3-icon-clock{background-position:-841px 0;width:13px;}a:active .gvl3-icon-clock{background-position:-841px -16px;}.gvl3-icon-comment{background-position:-863px 0;width:13px;}a:active .gvl3-icon-comment{background-position:-863px -16px;}.gvl3-icon-guidance{background-position:-885px 0;width:14px;}a:active .gvl3-icon-guidance{background-position:-885px -16px;}.gvl3-icon-listen{background-position:-907px 0;width:16px;}a:active .gvl3-icon-listen{background-position:-907px -16px;}.gvl3-icon-viewimage{background-position:-929px 0;width:16px;}a:active .gvl3-icon-viewimage{background-position:-929px -16px;}.gvl3-icon-downloaddisabled{background-position:-951px 0;width:12px;}a:active .gvl3-icon-downloaddisabled{background-position:-951px -16px;}.gvl3-icon-live,.gvl3-icon-boxedlive{background-position:-973px 0;width:27px;opacity:1;filter:alpha(opacity=100);}a:hover .gvl3-icon-live,a:focus .gvl3-icon-live,a:active .gvl3-icon-boxedlive{background-position:-973px -16px;}.ie .gvl3-icon-live,.ie .gvl3-icon-boxedlive,.ie7 .gvl3-icon-live,.ie7 .gvl3-icon-boxedlive{filter:alpha(opacity=100);}.gvl3-icon-invert-boxedlive{background-position:-973px -31px;width:27px;opacity:1;filter:alpha(opacity=100);}a:active .gvl3-icon-invert-boxedlive{background-position:-973px -31px;}.ie .gvl3-icon-invert-boxedlive,.ie7 .gvl3-icon-invert-boxedlive{filter:alpha(opacity=100);}.gvl3-icon-mobile{background-position:-1017px 0;width:8px;}a:active .gvl3-icon-mobile{background-position:-1017px -16px;}.gvl3-icon-digitaltv{background-position:-1039px 0;width:18px;}a:active .gvl3-icon-digitaltv{background-position:-1039px -16px;}.gvl3-icon-dabradio{background-position:-1061px 0;width:22px;}a:active .gvl3-icon-dabradio{background-position:-1061px -16px;}.gvl3-icon-easytoread{background-position:-1083px 0;width:21px;}a:active .gvl3-icon-easytoread{background-position:-1083px -16px;}.gvl3-icon-pc{background-position:-1105px 0;width:17px;}a:active .gvl3-icon-pc{background-position:-1105px -16px;}.gvl3-icon-podcast{background-position:-1127px 0;width:10px;}a:active .gvl3-icon-podcast{background-position:-1127px -16px;}.gvl3-icon-newsletter{background-position:-1149px 0;width:19px;}a:active .gvl3-icon-newsletter{background-position:-1149px -16px;}.gvl3-icon-accessibility{background-position:-1171px 0;width:12px;}a:active .gvl3-icon-accessibility{background-position:-1171px -16px;}.gvl3-icon-adhd{background-position:-1191px 0;width:13px;}a:active .gvl3-icon-adhd{background-position:-1191px -16px;}.gvl3-icon-aspergers{background-position:-1213px 0;width:13px;}a:active .gvl3-icon-aspergers{background-position:-1213px -16px;}.gvl3-icon-easiertoread{background-position:-1235px 0;width:19px;}a:active .gvl3-icon-easiertoread{background-position:-1235px -16px;}.gvl3-icon-dyslexia{background-position:-1257px 0;width:22px;}a:active .gvl3-icon-dyslexia{background-position:-1257px -16px;}.gvl3-icon-boxedwatch{background-position:-1301px 0;width:16px;}a:active .gvl3-icon-boxedwatch{background-position:-1301px -16px;}.gvl3-icon-boxedlisten{background-position:-1323px 0;width:16px;}a:active .gvl3-icon-boxedlisten{background-position:-1323px -16px;}.gvl3-icon-invert-watch{background-position:-5px -32px;width:11px;}.gvl3-icon-invert-pause{background-position:-27px -32px;width:12px;}.gvl3-icon-invert-rewind{background-position:-49px -32px;width:12px;}.gvl3-icon-invert-expand{background-position:-71px -32px;width:13px;}.gvl3-icon-invert-popout{background-position:-93px -32px;width:17px;}.gvl3-icon-invert-share{background-position:-115px -32px;width:13px;}.gvl3-icon-invert-volume{background-position:-137px -32px;width:27px;}.gvl3-icon-invert-highdefinition{background-position:-181px -32px;width:25px;}.gvl3-icon-invert-previous{background-position:-224px -32px;width:10px;}.gvl3-icon-invert-next{background-position:-247px -32px;width:10px;}.gvl3-icon-invert-zoomout{background-position:-269px -32px;width:13px;}.gvl3-icon-invert-zoomin{background-position:-291px -32px;width:13px;}.gvl3-icon-invert-pinpoint{background-position:-313px -32px;width:10px;}.gvl3-icon-invert-pinpoint-content{background-position:-335px -32px;width:10px;}.gvl3-icon-invert-reset{background-position:-357px -32px;width:17px;}.gvl3-icon-invert-refresh{background-position:-379px -32px;width:14px;}.gvl3-icon-invert-lock{background-position:-401px -32px;width:12px;}.gvl3-icon-invert-unlock{background-position:-423px -32px;width:15px;}.gvl3-icon-invert-search{background-position:-445px -32px;width:13px;}.gvl3-icon-invert-gridview{background-position:-467px -32px;width:14px;}.gvl3-icon-invert-listview{background-position:-489px -32px;width:14px;}.gvl3-icon-invert-close{background-position:-511px -32px;width:13px;}.gvl3-icon-invert-yes{background-position:-533px -32px;width:15px;}.gvl3-icon-invert-rate{background-position:-555px -32px;width:15px;}.gvl3-icon-invert-top{background-position:-577px -32px;width:14px;}.gvl3-icon-invert-home{background-position:-599px -32px;width:17px;}.gvl3-icon-invert-print{background-position:-621px -32px;width:15px;}.gvl3-icon-invert-email{background-position:-643px -32px;width:15px;}.gvl3-icon-invert-help{background-position:-665px -32px;width:13px;}.gvl3-icon-invert-information{background-position:-687px -32px;width:14px;}.gvl3-icon-invert-alert{background-position:-709px -32px;width:15px;}.gvl3-icon-invert-add{background-position:-731px -32px;width:15px;}.gvl3-icon-invert-favourite{background-position:-753px -32px;width:13px;}.gvl3-icon-invert-edit{background-position:-775px -32px;width:13px;}.gvl3-icon-invert-rss{background-position:-797px -32px;width:13px;}.gvl3-icon-invert-duration{background-position:-819px -32px;width:15px;}.gvl3-icon-invert-clock{background-position:-841px -32px;width:13px;}.gvl3-icon-invert-comment{background-position:-863px -32px;width:13px;}.gvl3-icon-invert-guidance{background-position:-885px -32px;width:14px;}.gvl3-icon-invert-listen{background-position:-907px -32px;width:16px;}.gvl3-icon-invert-viewimage{background-position:-929px -32px;width:16px;}.gvl3-icon-invert-downloaddisabled{background-position:-951px -32px;width:12px;}.gvl3-icon-invert-live{background-position:-973px -32px;width:27px;opacity:1;filter:alpha(opacity=100);}.gvl3-icon-invert-mobile{background-position:-1017px -32px;width:8px;}.gvl3-icon-invert-digitaltv{background-position:-1039px -32px;width:18px;}.gvl3-icon-invert-dabradio{background-position:-1061px -32px;width:22px;}.gvl3-icon-invert-easytoread{background-position:-1083px -32px;width:21px;}.gvl3-icon-invert-pc{background-position:-1105px -32px;width:17px;}.gvl3-icon-invert-podcast{background-position:-1127px -32px;width:10px;}.gvl3-icon-invert-newsletter{background-position:-1149px -32px;width:19px;}.gvl3-icon-invert-accessibility{background-position:-1171px -32px;width:12px;}.gvl3-icon-invert-adhd{background-position:-1191px -32px;width:13px;}.gvl3-icon-invert-aspergers{background-position:-1213px -32px;width:14px;}.gvl3-icon-invert-easiertoread{background-position:-1235px -32px;width:19px;}.gvl3-icon-invert-dyslexia{background-position:-1257px -32px;width:22px;}.gvl3-icon-invert-boxedwatch{background-position:-1301px -32px;width:16px;}.gvl3-icon-invert-boxedlisten{background-position:-1323px -32px;width:16px;}.story-date-invisible,.invisible{position:absolute;top:-5000px;left:-5000px;}.debug body{background:white url(img/gvl3-grid-2.png) repeat-y center 1px;}.debug #blq-container{opacity:.6;} \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/GVL3-icons-test.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/GVL3-icons-test.png
new file mode 100755
index 0000000000..a8f1369446
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/GVL3-icons-test.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/carousel-prev-next-3.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/carousel-prev-next-3.png
new file mode 100755
index 0000000000..d4242618d5
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/carousel-prev-next-3.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/cbl.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/cbl.png
new file mode 100755
index 0000000000..bd6fb3ecd9
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/cbl.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/cbr.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/cbr.png
new file mode 100755
index 0000000000..571cfef549
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/cbr.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/foldout-arrow.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/foldout-arrow.gif
new file mode 100755
index 0000000000..2cf9da05ed
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/foldout-arrow.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/gvl3-grid-2.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/gvl3-grid-2.png
new file mode 100755
index 0000000000..4d9f167976
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/gvl3-grid-2.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/gvl3-icons-0-2.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/gvl3-icons-0-2.gif
new file mode 100755
index 0000000000..88ee92591b
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/gvl3-icons-0-2.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/gvl3-icons-0-2.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/gvl3-icons-0-2.png
new file mode 100755
index 0000000000..8026479418
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/gvl3-icons-0-2.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/gvl3-live-icon-inverted.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/gvl3-live-icon-inverted.gif
new file mode 100755
index 0000000000..693c63ea3a
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/gvl3-live-icon-inverted.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/icons/listen-charcoal.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/icons/listen-charcoal.png
new file mode 100755
index 0000000000..d16c6c56da
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/icons/listen-charcoal.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/icons/play-charcoal.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/icons/play-charcoal.png
new file mode 100755
index 0000000000..a863f55720
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/icons/play-charcoal.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/index-quote.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/index-quote.png
new file mode 100755
index 0000000000..d355724368
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/index-quote.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/live-icon-32.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/live-icon-32.gif
new file mode 100755
index 0000000000..a4725534c2
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/live-icon-32.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/market-data-down.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/market-data-down.gif
new file mode 100755
index 0000000000..4d89554ea1
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/market-data-down.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/market-data-down.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/market-data-down.png
new file mode 100755
index 0000000000..1aa1872d20
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/market-data-down.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/market-data-up.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/market-data-up.gif
new file mode 100755
index 0000000000..633a2cc950
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/market-data-up.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/market-data-up.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/market-data-up.png
new file mode 100755
index 0000000000..59b46df378
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/market-data-up.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/most_watched.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/most_watched.png
new file mode 100755
index 0000000000..8631f3294a
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/most_watched.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/search.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/search.png
new file mode 100755
index 0000000000..be856addfc
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/search.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/select-arrow.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/select-arrow.png
new file mode 100755
index 0000000000..d55e57a426
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/select-arrow.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/services-alert.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/services-alert.gif
new file mode 100755
index 0000000000..4f40800165
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/services-alert.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/services-mail.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/services-mail.gif
new file mode 100755
index 0000000000..1fc9f1ca12
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/services-mail.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/services-mobile.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/services-mobile.gif
new file mode 100755
index 0000000000..babd18a8b8
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/services-mobile.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/services-podcast.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/services-podcast.gif
new file mode 100755
index 0000000000..3fe3de2066
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/services-podcast.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/services-rss.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/services-rss.gif
new file mode 100755
index 0000000000..5036e5682a
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/services-rss.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/story_sprite.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/story_sprite.gif
new file mode 100755
index 0000000000..f236f25037
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/story_sprite.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/story_sprite.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/story_sprite.png
new file mode 100755
index 0000000000..bbc298f402
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/story_sprite.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/traffic_icon.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/traffic_icon.gif
new file mode 100755
index 0000000000..5fc5235d96
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/traffic_icon.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/transparencies/rgba-0-0-0-07.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/transparencies/rgba-0-0-0-07.png
new file mode 100755
index 0000000000..2ad571dc98
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/img/transparencies/rgba-0-0-0-07.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/layout/index.css b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/layout/index.css
new file mode 100755
index 0000000000..bde1b15386
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/layout/index.css
@@ -0,0 +1 @@
+.container-now,.container-best,.container{position:relative;display:block;overflow:hidden;margin:0;}#full-width{position:relative;clear:both;overflow:hidden;margin:0 0 -1px;padding-bottom:1px;min-height:8px;width:976px;z-index:10;background:#fff;}.container-now{position:relative;float:left;display:inline;clear:both;margin:0 16px 16px 0;width:624px;}.container-best{position:relative;float:right;display:inline;overflow:visible;width:336px;z-index:11;}.container-best{margin-top:-16px;}.front-page .container-best{margin-top:8px;}.index-date{position:relative;display:block;clear:both;padding:12px 0 4px;}.index-date .date{font-weight:bold;} \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/mobile.css b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/mobile.css
new file mode 100755
index 0000000000..8307c1ccd0
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/mobile.css
@@ -0,0 +1 @@
+body{-webkit-text-size-adjust:none;} \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/print.css b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/print.css
new file mode 100755
index 0000000000..ca24e216cc
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/print.css
@@ -0,0 +1 @@
+.embedded-hyper,.story-feature,.story-related,.more-stories,.caption,.videoInStoryA,.videoInStoryB,.videoInStoryC,.more-stories,.top-section-stories,.hypertabs,.container-local-weather-and-travel,.useful-links,#site-wide-alert,.marketdata-widget a{display:none!important;}#blq-mast,#blq-nav,#blq-footlinks,#blq-obit,#blq-acc,#debug{display:none!important;}#blq-foot{background:transparent;width:624px!important;}#blq-copy{margin-left:0!important;}body{background:white!important;}body *{color:black!important;}#blq-container{background:none!important;}#blq-container-inner{width:624px!important;padding:1.64cm 3cm!important;}#main-content{padding-bottom:47px;}a,a *{color:blue!important;text-decoration:underline;}hr{background:white!important;border:white!important;}#header a{display:block;}#header img{padding-bottom:12px;display:block;}#header{padding-bottom:9px!important;}.section-title{display:block;font-size:35px!important;padding-top:6px;padding-bottom:16px;font-family:"Gill Sans";text-transform:uppercase;font-weight:normal!important;border-bottom:3px solid black;text-indent:-2px!important;}#nav,#sub-nav,.gvl3-icon,#best,#related-services,#page-bookmark-links-head,#page-bookmark-links-foot,.story-body a.hidden,.bbccom_display_none,.layout-block-b{display:none!important;}.story-date{display:block;padding-bottom:31px;}.story-date .date{font-weight:bold;}.story-header{padding-top:.25cm;padding-bottom:52px;}.story-body p,.story-related li{font-size:13pt;line-height:24px;padding-bottom:24px;}.story-body strong,.story-body .introduction{font-weight:bold;}.story-related{padding-top:23px;}.story-related .timestamp{text-transform:none!important;font-size:13px!important;line-height:24px!important;font-weight:bold;}.story-related a{display:block;font-size:13pt;line-height:18px;}.story-related h2,.story-related h3,.story-related li{padding-bottom:12px;}.story-body h2,.story-related h2,.cross-head{display:block;font-size:13pt;line-height:24px;font-weight:bold;}#content-wrapper a.story:after{content:" ["attr(href)"]";font-size:13px;display:block;width:100%;}.caption{clear:both;margin:12px 0 25px;padding:6px 0 4px;}.story-body img{display:block;padding-bottom:4px;}.story-body form{display:none;}.story-feature,.videoInStoryB{clear:both;position:relative;overflow:hidden;margin:-6px 0 13px;padding:5px 12px 12px;border:1px dotted black;}.story-feature h2{padding-top:0;padding-bottom:7px;}.story-feature h2.quote{font-size:48px;position:absolute;top:9px;width:50px;height:50px;line-height:48px;}.story-feature blockquote p{text-indent:30px;padding-top:11px;padding-bottom:11px;}.story-feature blockquote{display:inline;}.story-feature .quote_credit{font-weight:bold;}.story-feature .quote_credit_title{display:block;}.story-feature h2.quote span,.story-feature .endquote{display:none;}.byline{display:block;overflow:auto;position:relative;margin-bottom:26px;padding-top:11px;border-top:1px dotted black;}.byline .byline-picture img{display:none;}.byline-name{display:block;font-weight:bold;}.next-container,.previous-container,.next-container a.next,.previous-container a.previous{display:none;}.more-stories,.story-related{position:relative;clear:both;padding-top:0!important;padding-bottom:12px!important;border-top:3px solid black;}.more-stories h2,.more-stories .first,.more-stories li{padding-top:12px;}.more-stories img,.rss{display:none;}.newstracker-list ul li{clear:both!important;height:auto!important;padding-bottom:12px!important;}.newstracker-list ul li.even{clear:none!important;}#cps-info{display:block!important;}.bbccom-advert{display:none!important;}#print-advert{position:absolute;right:0;top:0;}.debug body{background:white url(img/gvl3-grid-2.png) repeat-y top center!important;}.debug #blq-container{opacity:.6;}#blq-container-inner{width:100%!important;padding-left:0!important;padding-right:0!important;}#blq-container-outer{border:2px solid transparent!important;margin:0!important;padding:0!important;} \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/type.css b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/type.css
new file mode 100755
index 0000000000..34a648093f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/type.css
@@ -0,0 +1,300 @@
+
+/* Default type */
+body #blq-container #blq-main {
+ font-size: 67.5%;
+ font-size: 1.3em;
+ -webkit-font-smoothing: antialiased;
+}
+
+* {
+ color: #505050;
+ font-family: Arial, Helmet, Freesans, sans-serif;
+ line-height: 16px;
+}
+
+/* Knockout */
+.knockout {
+ color: #fff;
+ text-shadow: 0 0 1px rgba(0,0,0,0.2);
+}
+
+/* Highlight (orange) */
+.highlight {
+ color: #D1700E;
+}
+
+#header .section-title {
+ color: #fff;
+ text-shadow: 0 0 1px rgba(0,0,0,0.1);
+ font-family: "Gill Sans MT", "Gill Sans", Arial, Helmet, Freesans, sans-serif;
+ font-weight: normal;
+ text-transform: uppercase;
+ font-size: 30px;
+ letter-spacing: 0;
+ line-height: 40px;
+ text-rendering: optimizeLegibility;
+}
+
+/* 48px */
+.special-48 {
+ font-size: 3.692em; /* 13px < 48px */
+ font-weight: bold;
+ letter-spacing: -2px;
+ line-height: 48px;
+ text-rendering: optimizeLegibility;
+}
+
+/* 36px */
+.special-36,
+#splash .splash-header .story {
+ font-size: 2.769em; /* 13px < 36px */
+ font-weight: bold;
+ letter-spacing: -1px;
+ line-height: 36px;
+ text-rendering: optimizeLegibility;
+}
+
+/* 32px */
+.special-32,
+.se-promo-now-inc-header,
+#main-content h1.story-header,
+#top-story .top-story-header,.story-wide h1,
+.lead-feature-now .headline {
+ font-size: 2.461em; /* 13px < 32px */
+ font-weight: bold;
+ letter-spacing: -1px;
+ line-height: 34px;
+ text-rendering: optimizeLegibility;
+}
+
+/* 24px */
+.special-24,
+.se-promo-best-inc-header,
+.top-index-stories .top-index-stories-header,
+.top-section-stories .top-section-stories-header,
+.useful-links-header,
+.also-in-news h2,
+.av-stories-best .av-best-header,
+.feature-generic .features-header, .container-features-and-analysis .features-header,
+.featured-site-top-stories h2,
+.digest-wrapper-header,
+.geo-digest-solo-header,
+.geo-digest-region-header,
+.geo-digest-vertical-header,
+.languages h3,
+.other-top-stories .other-top-stories-header,
+.podcasts-range-module .podcasts-range-module-header,
+#site-wide-alert h2,
+.special-reports-header,
+.top-stories-range-module .top-stories-range-module-header,
+.topic-cluster .topic-cluster-header,
+.market-data h2,
+.weather-3day h2,
+.other-site-content-header,
+.guides-stories-header,
+.container-hyper-topic-cluster .hyper-depth-header,
+#personalisation h3,
+#related-services h2,.media_asset .most-pop h2,.byline h2,.alert h2,tr.heading th h2,.share-help h2,.share-help h3,.story-related h2,.most-watched-list h2,.useful-links h2,.more-stories h2,
+#personalisation .location-panel h4,
+#personalisation .we-remembered-panel h4,
+.have-your-say-inc-header,
+.have-your-say-inc .contact-number,
+.container-archived-content-heading,
+.container-featured-other-site-heading,
+.feature-digest-header,
+.generic-tiled-digest-header,
+.weather-4items h2,.marketdata-widget h2 {
+ font-size: 1.846em; /* 13px < 24px */
+ font-weight: bold;
+ letter-spacing: -1px;
+ line-height: 24px;
+ text-rendering: optimizeLegibility;
+}
+
+/* 20px */
+.special-20,
+.container-geographic-regions-generic .around-uk-digest-header,
+#personalisation .locator-msg-disambiguate p.locator-panel-header strong {
+ font-size: 1.538em; /* 13px < 20px */
+ font-weight: bold;
+ letter-spacing: -1px;
+ line-height: 24px;
+ text-rendering: optimizeLegibility;
+}
+
+
+/* 16px */
+.special-16,
+.top-section-stories .first-child h3,
+.container-local-weather-and-travel .weather .data-feed-now h3,
+.container-local-weather-and-travel .travel .useful-links .useful-links-header,
+.feature-generic .feature-header, .container-features-and-analysis .feature-header,
+.featured-site-top-stories .with-summary .story,
+.featured-site-top-stories .medium-image .story,
+.featured-site-top-stories .large-image .story,
+.featured-site-top-stories .classic-image .story,
+.digest .digest-header,
+.geo-digest-section-header,
+.geo-digest-vertical .tab a,
+.container-geographic-regions-generic .tab a,
+.container-geographic-news-digests .tab a,
+.hyper-container-title .hyper-container-title-header,
+.hyper-related-assets .hyper-depth-stories-header,
+.hyper-foldout .hyper-foldout-header,
+.languages h4,
+.other-top-stories-stories .with-summary a.story,
+.secondary-top-story .with-summary .story,
+.secondary-top-story .medium-image .story,
+.secondary-top-story .large-image .story,
+.secondary-top-story .classic-image .story,
+.other-site-content .first-other-promo h3 a.story,
+.guide-content .guide h3 a.story,
+#site-wide-alert p,
+.special-reports-component h3 a,
+#splash .see-also .first-child-live a.story,
+.topic-cluster-stories .with-summary a.story,
+.media-asset .read-full h2, .media-asset .read-more h2, .media-asset .playlist h2, .features h2, .top-stories h2, .most-pop-story h2, h2.more-stories-head,.tabs li,.alert h3,.story-body .story-feature h2,.story-body .story-feature blockquote p,.see-also h3, #main-content .story-related h3,.features h3, .byline .byline_name,.story-body tr.subheading th, .story-body tr.subheading td h3,
+.story-body .cross-head,
+.weather-3day h3#weather_sitelabel,.more-galleries h2,.share-form ul.networks li a,.story-body .embedded-hyper h2,.hyper-promotional-content ul li.large-image h3, .hyper-promotional-content ul li.medium-image h3, .hyper-promotional-content ul li.first-child h3,
+.story-body tr.colheading th,
+.have-your-say-inc ul li h3 a,
+.have-your-say-inc ul li h3 .core-text,
+.byline .byline-name,
+.guide-content .stacked-overlay-guides .guide h3 a .overlay strong,
+.other-site-content .stacked-overlay-other-site-promotion .first-other-promo h3 a .overlay strong,
+.weather-4items h3#weather_sitelabel,
+.weather-4items h4,.dna-comment-count-personal a {
+ font-size: 1.231em; /* 13px < 16px */
+ font-weight: bold;
+ line-height: 16px;
+ text-rendering: optimizeLegibility;
+}
+
+/* 13px Normal */
+.container-local-weather-and-travel .data-feed-now h3#weather_sitelabel,
+.secondary-top-story .see-also .story,
+.see-also ul li a, #main-content .internet-links a, .features p,
+#related-services,
+.weather-3day .next3daysweather .stripes li,.weather-3day .next3daysweather .stripes li strong,.weather-3day .next3daysweather .stripes h3,.story-body #heading-2,
+.story-body table p,
+.story-body div p.caption,
+.story-body .story-feature p,
+.story-body .story-feature li,
+.guide-content .stacked-overlay-guides .guide h3 a.story,
+.other-site-content .stacked-overlay-other-site-promotion .first-other-promo h3 a.story,
+.weather-4items .next3daysweather .stripes li,
+.weather-4items .next3daysweather .stripes li strong,
+.weather-4items .next3daysweather .stripes h4 {
+ font-size: 1em; /* 13px */
+ line-height: 16px;
+}
+
+/* 14px Storybody */
+.story-body p,
+.story-body li,
+.story-wide li,
+.media-asset .emp-decription p,
+.photo-gallery .hypertabs li {
+ font-size: 1.077em; /* 13px < 14px */
+ line-height: 18px;
+ text-rendering: auto;
+}
+
+/* Storybody Color */
+.story-body p,
+.story-body p strong,
+.story-body p em,
+.media_asset .emp-decription p,
+.photo-gallery .hypertabs li {
+ color: #333;
+}
+
+
+/* 11px */
+.special-11,
+.special-reports-component #bbccom_module_specialreport_text,
+.market-data .mkt-last-updated,
+.market-data .mkt-footer .mkt-data-delayed,
+#pictureGallery .controls a,
+.media_asset p.published, .media_asset .warning p, #main-content .internet-links span, .top-stories li span,.see-also span.timestamp,.see-also span.section,.marketdata-widget span,
+.story-body table tfoot p,.dna-comment-count-simple a {
+ font-family: Arial;
+ font-size: 0.846em; /* 13px > 11px */
+ text-transform: uppercase;
+ line-height: 16px;
+ text-rendering: optimizeLegibility;
+}
+
+
+
+/* Bold */
+strong {
+ font-weight: bold;
+}
+
+/* Italic */
+em {
+ font-style: italic;
+}
+
+/* Default anchor */
+a {
+ color: #1F4F82;
+ text-decoration: none;
+ font-weight: bold;
+}
+
+/* This is a super strong overide - applies to all see alsos */
+body #blq-container #blq-container-inner .see-also a.story {
+ font-weight: normal;
+}
+
+a:visited {
+ color: #4A7194;
+}
+
+/* Knockout anchor */
+.knockout a,
+#nav * {
+ color: #fff;
+ text-shadow: 0 0 1px rgba(0,0,0,0.2);
+}
+
+/* Blue knockout anchor */
+.knockout a {
+ color: #a9c0d3;
+}
+
+a.from-external-source,
+a.from-external-source:visited {
+ color: #888888;
+}
+
+/* Default anchor hover/focus */
+a:hover, a:focus {
+/* color: #123E60; */
+ text-decoration: underline;
+ outline: none;
+}
+
+/* Knockout anchor hover/focus */
+.knockout a:hover, .knockout a:focus {
+ color: #fff;
+}
+
+/* Default anchor active */
+a:active {
+ color: #D1700E;
+}
+
+/* Knockout anchor active */
+.knockout a:active {
+ color: #fff;
+}
+
+#blq-container #blq-container-inner img {
+ font-size: 13px;
+ letter-spacing: 0;
+ font-weight: normal;
+ font-style: italic;
+}
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/node1.bbcimg.co.uk/glow/gloader.0.1.4.js b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/node1.bbcimg.co.uk/glow/gloader.0.1.4.js
new file mode 100755
index 0000000000..4ed91d3bba
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/node1.bbcimg.co.uk/glow/gloader.0.1.4.js
@@ -0,0 +1,18 @@
+/*
+ Copyright 2009 British Broadcasting Corporation
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+(function(){if(window.gloaddisableder){return;}window.gloaddisableder={_requests:[],_modules:{},_expects:{},_extras:{},_errors:[],util:{getGloaddisablederFile:function(filepath){if(filepath&&/(^|^.*[\/\\])(gloaddisableder(\.[^\/\\]+)?\.js)(\?|$)/i.test(filepath)){var dir=RegExp.$1;if(dir){dir=dir.replace("cached/","");}return{dir:dir,name:RegExp.$2};}return undefined;}},map:{js:{},css:{},parse:function(libraryName,libraryVersions){var lib={name:libraryName,versions:[]};var scope={};var scopeProps=["$name","$version","$base"];scopeProps.has=function(what){for(var i=0;i<this.length;i++){if(what==this[i]){return true;}}return false;};if(gloaddisableder.mapProps&&gloaddisableder.mapProps[lib.name]){for(var p in gloaddisableder.mapProps[lib.name]){if(!scopeProps.has(p)){scopeProps.push(p);}}}for(var p=0;p<scopeProps.length;p++){scope[scopeProps[p]]=undefined;}scope.$name=lib.name;if(gloaddisableder.mapProps&&gloaddisableder.mapProps[lib.name]){for(var p in gloaddisableder.mapProps[lib.name]){scope[p]=gloaddisableder.mapProps[lib.name][p];}}for(var i=0;i<libraryVersions.length;i++){var version=libraryVersions[i];for(var v in version){if(version[v]===null){delete version[v];delete scope[v];}else{if(v.indexOf("$")===0){scope[v]=version[v];}else{if(typeof version[v]=="string"){version[v]=[version[v]];scope[v]=version[v].slice(0);}else{if(typeof version[v].push!="undefined"){scope[v]=version[v].slice(0);}else{throw new Error("invalid type: "+typeof version[v]);}}}}}for(var s in scope){if(typeof version[s]=="undefined"){if(s.indexOf("$")===0){version[s]=scope[s];}else{version[s]=scope[s].slice(0);}}}for(var p=0;p<scopeProps.length;p++){var prop=scopeProps[p];for(var vp=0;vp<p;vp++){var vprop=scopeProps[vp];if(typeof version[vprop]=="undefined"){version[prop]=scope[prop];}var patt=new RegExp("\\{\\"+vprop+"\\}","g");version[prop]=version[prop].replace(patt,version[vprop]);}}for(vi in version){if(vi.indexOf("$")===0){continue;}for(var vii=0;vii<version[vi].length;vii++){for(var p=0;p<scopeProps.length;p++){var prop=scopeProps[p];var patt=new RegExp("\\{\\"+prop+"\\}","g");version[vi][vii]=""+version[vi][vii].replace(patt,""+version[prop]);}}}lib.versions.push(version);}return lib;},add:function(){var args=[];for(var i=1;i<arguments.length;i++){args.push(arguments[i]);}var lib=gloaddisableder.map.parse(arguments[0],args);for(var i=0;i<lib.versions.length;i++){var version=lib.versions[i];for(var p in version){if(p.charAt(0)=="$"){continue;}var modId=version.$name+"/"+version.$version+"/"+p;gloaddisableder.map.js[modId]=version[p][0];gloaddisableder.map.css[modId]=version[p][1];}}},include:function(src){if(gloaddisableder.map._include[src]){return false;}else{void('<script type="text/javascript" src="'+src+'"><\/script>\n');gloaddisableder.map._include[src]=true;return true;}},_include:{},latest:function(libName,v){if(gloaddisableder.map.$latest[libName+"/"+v]){return gloaddisableder.map.$latest[libName+"/"+v];}var result=v;var parts=v.split(".");if(parts.length<3){if(parts[0]==parseInt(parts[0])&&(typeof parts[1]=="undefined"||parts[1]==parseInt(parts[1]))){var invalid=new RegExp("[a-zA-Z-]"),latest=[parts[0],null,null],mod;for(mod in gloaddisableder.map.js){var modParts=mod.split("/");if(modParts[0]==libName&&modParts[2]==libName&&!invalid.test(modParts[1])){var modVParts=modParts[1].split(".");if(modVParts[0]==parts[0]){if((typeof parts[1]=="undefined"&&(latest[1]<=modVParts[1]||(latest[1]==modVParts[1]&&latest[2]<=modVParts[2])))||(typeof parts[1]!="undefined"&&parts[1]==modVParts[1]&&latest[2]<=modVParts[2])){latest[1]=modVParts[1];latest[2]=modVParts[2];}}}}if(latest[2]!=null){result=latest.join(".");}}}gloaddisableder.map.$latest[libName+"/"+v]=result;return result;},$latest:{}},settings:{ns:"bbc.glow.gloaddisableder",get:function(name){var n=" "+gloaddisableder.settings.ns+"."+name+"=";var cookies=document.cookie.split(";");for(var i=0;i<cookies.length;i++){if((" "+cookies[i]).indexOf(n)>-1){return unescape(cookies[i].split("=")[1]);}}},set:function(name,value,path){var n=gloaddisableder.settings.ns+"."+name;document.cookie=n+"="+escape(value)+"; path="+((path)?path:"/")+";";},clear:function(name,path){var d=new Date();d.setTime(d.getTime()-1);var n=gloaddisableder.settings.ns+"."+name;document.cookie=n+"=; path="+((path)?path:"/")+"; expires=Thu, 01-Jan-70 00:00:01 GMT;";}},loaddisabledOverride:function(version){var current=gloaddisableder.settings.get("override");version=version||prompt("Enter version",current?current:"");if(version===""){gloaddisableder.settings.clear("override");}else{if(version!==null){gloaddisableder.settings.set("override",version);}}location.reloaddisabled();},loaddisabledDebug:function(){gloaddisableder.settings.set("debug","1");location.reloaddisabled();},unloaddisabledDebug:function(){gloaddisableder.settings.clear("debug");location.reloaddisabled();},expect:function(srcFile){srcFile=""+srcFile;var modsInFile=[];var modId;for(modId in gloaddisableder.map.js){if(gloaddisableder.map.js[modId]==srcFile){modsInFile.push(modId);gloaddisableder._expects[modId]=(gloaddisableder._expects[modId]||0)+1;}}},loaddisabled:function(){var r={};if(typeof arguments[arguments.length-1].length=="undefined"){r=arguments[arguments.length-1];arguments.length--;}var newRequest=new gloaddisableder.Request(r);gloaddisableder._requests.push(newRequest);var mods=[];var override=gloaddisableder.settings.get("override");for(var i=0;i<arguments.length;i++){if(override&&arguments[i][0]=="glow"){if(typeof console!="undefined"&&console.log){console.log("Overriding version '"+arguments[i][1]+"' of glow to version '"+override+"'");}arguments[i][1]=override;}mods.push(arguments[i]);}var ids=gloaddisableder.toIds(mods);newRequest.args=[];for(var i=0;i<ids.length;i++){newRequest.include(ids[i]);if(ids[i].match(/\/[^.]+$/)){newRequest.args.push(ids[i]);}}var waitCount=newRequest.waits.length;for(var i=0;i<newRequest.waits.length;i++){if(gloaddisableder._modules[newRequest.waits[i]]&&gloaddisableder._modules[newRequest.waits[i]].status==gloaddisableder.Module.IMPLEMENTED){waitCount--;}}if(waitCount>0){newRequest.status=gloaddisableder.Request.WAITING;gloaddisableder.request(ids,newRequest.async);gloaddisableder.resolve();}else{newRequest.complete();}},request:function(mIds,async){for(var i=0;i<mIds.length;i++){var m=mIds[i];if(gloaddisableder._extras[m]){gloaddisableder._modules[m]=new gloaddisableder.Module(m);var extra=gloaddisableder._extras[m];delete gloaddisableder._extras[m];gloaddisableder.provide(extra);}else{if(!gloaddisableder._modules[m]){gloaddisableder._modules[m]=new gloaddisableder.Module(m);gloaddisableder._modules[m].status=gloaddisableder.Module.REQUESTED;}}if(gloaddisableder._modules[m].status<gloaddisableder.Module.IMPLEMENTED||gloaddisableder._modules[m].css){gloaddisableder._modules[m].css=null;gloaddisableder.fetch(gloaddisableder._modules[m],async);}}},fetch:function(m,async){var cssSrc=gloaddisableder.map.css[m.id],jsSrc=gloaddisableder.map.js[m.id];gloaddisableder._modules[m.id].async=async;var force=(!async&&gloaddisableder._fetched[jsSrc]&&gloaddisableder._fetched[jsSrc].async&&gloaddisableder._modules[m.id].status<gloaddisableder.Module.IMPLEMENTED);if(cssSrc&&(force||!gloaddisableder._fetched[cssSrc])){gloaddisableder._fetched[cssSrc]={};if(document){var headElement;if(headElement=document.getElementsByTagName("head")[0]){var link;if(link=document.createElement("link")){link.href=cssSrc;link.rel="stylesheet";link.type="text/css";link.className="gloaddisableded async";headElement.appendChild(link);}}else{void('<link rel="stylesheet" type="text/css" href="'+cssSrc+'" class="gloaddisableded sync">');}}}if(!jsSrc){var msg="The gloaddisableder map is missing a JavaScript filepath for the module: "+m.id;var maps=[];for(var included_map in gloaddisableder.map._include){maps.push(included_map);}msg+=".\rMaps included are: "+maps.join(", ")+".";gloaddisableder._errors.push(msg);throw new Error(msg);}if(jsSrc){if(force||!gloaddisableder._fetched[jsSrc]){gloaddisableder._fetched[jsSrc]={};gloaddisableder.expect(jsSrc);if(async){gloaddisableder._fetched[jsSrc].async=true;var headElement=document.getElementsByTagName("head")[0];var scriptElement=document.createElement("script");scriptElement.type="text/javascript";scriptElement.src=jsSrc;scriptElement.className="gloaddisableded async";headElement.appendChild(scriptElement);}else{gloaddisableder._fetched[jsSrc].sync=true;void('<script type="text/javascript" src="'+jsSrc+'" class="gloaddisableded sync"><\/script>\n');}}else{}if(gloaddisableder._modules[m.id].status<gloaddisableder.Module.FETCHED){gloaddisableder._modules[m.id].status=gloaddisableder.Module.FETCHED;}}},_fetched:{},provide:function(m){m.id=m.library[0]+"/"+m.library[1]+"/"+m.name;if(!gloaddisableder._modules[m.id]){gloaddisableder._extras[m.id]=m;return;}if(gloaddisableder._modules[m.id].status>=gloaddisableder.Module.PROVIDED){return;}gloaddisableder._modules[m.id].status=gloaddisableder.Module.PROVIDED;gloaddisableder._modules[m.id].builder=m.builder;gloaddisableder._modules[m.id].builder.args=[];var d=gloaddisableder._modules[m.id].depends=(m.depends)?gloaddisableder.toIds(m.depends):[];if(d.length>0){var includes=[];for(var i=0;i<d.length;i++){var requests=gloaddisableder.getRequests(m);var include={async:true,ids:[]};for(var j=0;j<requests.length;j++){requests[j].include(d[i]);include.ids.push(d[i]);if(requests[j].async===false){include.async=false;}}includes.push(include);if(d[i].match(/\/[^.]+$/)){gloaddisableder._modules[m.id].builder.args.push(d[i]);}}for(var i=0;i<includes.length;i++){if(includes[i].ids.length){gloaddisableder.request(includes[i].ids,includes[i].async);}}}else{gloaddisableder.implement(m);}gloaddisableder.resolve();},_greet:function(mId){if(gloaddisableder._expects[mId]>0){gloaddisableder._expects[mId]--;}else{var msg="Unexpected module provided to gloaddisableder: "+mId;gloaddisableder._errors.push(msg);throw (msg);}},module:function(modDef){var modId=modDef.library[0]+"/"+modDef.library[1]+"/"+modDef.name;gloaddisableder._greet(modId);if(!modDef.depends){modDef.depends=[];}modDef.depends.unshift(modDef.library);gloaddisableder.provide(modDef);},library:function(modDef){var modId=modDef.name+"/"+modDef.version+"/"+modDef.name;gloaddisableder._greet(modId);if(!modDef.depends){modDef.depends=[];}modDef.library=[modDef.name,modDef.version];gloaddisableder.provide(modDef);},implement:function(m){if(gloaddisableder._modules[m.id].status!=gloaddisableder.Module.PROVIDED){return;}for(var i=0;i<gloaddisableder._modules[m.id].builder.args.length;i++){var argName=gloaddisableder._modules[m.id].builder.args[i];gloaddisableder._modules[m.id].builder.args[i]=gloaddisableder._modules[m.builder.args[i]].implementation;gloaddisableder._modules[m.id].builder.args[i].name=argName;}gloaddisableder._modules[m.id].implementation=gloaddisableder._modules[m.id].builder.apply(null,gloaddisableder._modules[m.id].builder.args);gloaddisableder._modules[m.id].status=gloaddisableder.Module.IMPLEMENTED;for(var i=0;i<gloaddisableder._requests.length;i++){gloaddisableder._requests[i].release(m.id);}},resolve:function(){MODULES:for(var m in gloaddisableder._modules){var module=gloaddisableder._modules[m];if(module.status==gloaddisableder.Module.PROVIDED){for(var j=0;j<module.depends.length;j++){var dModule=gloaddisableder._modules[module.depends[j]];if(!dModule||dModule.status!=gloaddisableder.Module.IMPLEMENTED){continue MODULES;}}gloaddisableder.implement(module);gloaddisableder.resolve();}}},getRequests:function(m){var requests=[];REQUESTS:for(var i=0;i<gloaddisableder._requests.length;i++){var request=gloaddisableder._requests[i];for(var j=0;j<request.waits.length;j++){if(request.waits[j]==m.id){requests.push(request);break REQUESTS;}}}return requests;},toIds:function(lib){var result=[];for(var i=0;i<lib.length;i++){var mods=lib[i];var libName=mods.shift();var libVersion=mods.shift();var libId=libName+"/"+gloaddisableder.map.latest(libName,libVersion);result.push(libId+"/"+libName);for(var j=0;j<mods.length;j++){result.push(libId+"/"+mods[j]);}}return result;}};gloaddisableder.Request=function(r){this.waits=[];this.status=gloaddisableder.Request.INITIAL;if(r.onLoad){r.onloaddisabled=r.onLoad;}if(r.onTimeout){r.ontimeout=r.onTimeout;}if(!r.async&&!r.onloaddisabled){this.setGlobal=true;}this.async=(typeof r.async!="undefined")?r.async:false;this.onloaddisabled=r.onloaddisabled;if(r.ontimeout){if(typeof r.timeout=="undefined"){r.timeout=20000;}this.timeoutRef=setTimeout(r.ontimeout,r.timeout);}};gloaddisableder.Request.INITIAL=-1;gloaddisableder.Request.WAITING=0;gloaddisableder.Request.COMPLETED=1;gloaddisableder.Request.prototype.include=function(mId){for(var i=0;i<this.waits.length;i++){if(this.waits[i]==mId){return;}}this.waits.push(mId);};gloaddisableder.Request.prototype.release=function(mId){var implementCount=0;for(var i=0;i<this.waits.length;i++){var wModule=gloaddisableder._modules[this.waits[i]];if(wModule&&wModule.status==gloaddisableder.Module.IMPLEMENTED){implementCount++;}}if(implementCount==this.waits.length){this.complete();}};gloaddisableder.Request.prototype.complete=function(){if(this.setGlobal){for(var i=0;i<this.waits.length;i++){var gModule=gloaddisableder._modules[this.waits[i]];window[gModule.name]=gloaddisableder._modules[this.waits[i]].implementation;}}if(this.status==gloaddisableder.Request.COMPLETED){return;}this.status=gloaddisableder.Request.COMPLETED;if(this.timeoutRef){clearTimeout(this.timeoutRef);}for(var i=0;i<this.args.length;i++){this.args[i]=gloaddisableder._modules[this.args[i]].implementation;}if(this.onloaddisabled){this.onloaddisabled.apply(null,this.args);}};gloaddisableder.Module=function(mId){this.id=mId;this.name=mId.split("/").pop();this.status=gloaddisableder.Module.INITIAL;this.css=gloaddisableder.map.css[mId];};gloaddisableder.Module.INITIAL=-1;gloaddisableder.Module.REQUESTED=0;gloaddisableder.Module.FETCHED=1;gloaddisableder.Module.PROVIDED=2;gloaddisableder.Module.IMPLEMENTED=3;gloaddisableder.isReady=false;(function(){var d=document;if(
+/*@cc_on!@*/
+false){if(typeof window.frameElement!="undefined"){d.attachEvent("onreadystatechange",function(){if(d.readyState=="complete"){d.detachEvent("onreadystatechange",arguments.callee);gloaddisableder.isReady=true;}});}else{(function(){try{d.documentElement.doScroll("left");}catch(e){setTimeout(arguments.callee,50);return;}gloaddisableder.isReady=true;})();}}else{if(typeof d.readyState!="undefined"){var f=function(){/loaddisableded|complete/.test(d.readyState)?gloaddisableder.isReady=true:setTimeout(f,10);};f();}else{var callback=function(){if(arguments.callee.fired){return;}arguments.callee.fired=true;if(gloaddisableder){gloaddisableder.isReady=true;}};if(d.addEventListener){d.addEventListener("DOMContentLoaded",callback,false);}var oldOnloaddisabled=window.onloaddisabled;window.onloaddisabled=function(){if(oldOnloaddisabled){oldOnloaddisabled();}callback();};}}})();gloaddisableder.map.setProperties=function(libraryName,props){if(typeof gloaddisableder.mapProps=="undefined"){gloaddisableder.mapProps={};}if(typeof gloaddisableder.mapProps[libraryName]=="undefined"){gloaddisableder.mapProps[libraryName]={};}for(var p in props){gloaddisableder.mapProps[libraryName][p]=props[p];}};gloaddisableder.use=function(name,opts){name=(name||"glow");opts=(opts||{});var properties={};for(var opts_name in opts){var property_name=(opts_name.indexOf("$")==0)?opts_name:"$"+opts_name;properties[property_name]=opts[opts_name];}properties.$debug=(properties.$debug||"");properties.$base=(properties.$base||gloaddisableder._baseDir+name+"/{$version}/");properties.$map=(properties.$map||gloaddisableder._baseDir+name+"/map.js");gloaddisableder.map.setProperties(name,properties);gloaddisableder.map.include(properties.$map);};})();(function(){var scripts=document.getElementsByTagName("script");for(var i=scripts.length-1;i>=0;i--){var src=scripts[i].getAttribute("src");var filespec=gloaddisableder.util.getGloaddisablederFile(src);if(typeof filespec!="undefined"){gloaddisableder._baseDir=filespec.dir;var gloaddisablederScript=scripts[i].innerHTML;if(/\S/.test(gloaddisablederScript)){eval(gloaddisablederScript);}var mapped=false;for(var p in gloaddisableder.map._include){mapped=true;continue;}if(!mapped){gloaddisableder.use();}break;}}})(); \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/pixel.quantserve.com/pixel/p-ccrmZLtMqYB8w.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/pixel.quantserve.com/pixel/p-ccrmZLtMqYB8w.gif
new file mode 100755
index 0000000000..a7ab9529e6
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/pixel.quantserve.com/pixel/p-ccrmZLtMqYB8w.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/pixel.quantserve.com/pixel/r.html b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/pixel.quantserve.com/pixel/r.html
new file mode 100755
index 0000000000..e69de29bb2
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/pixel.quantserve.com/pixel/r.html
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/sa.bbc.co.uk/bbc/bbc/s.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/sa.bbc.co.uk/bbc/bbc/s.gif
new file mode 100755
index 0000000000..35d42e808f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/sa.bbc.co.uk/bbc/bbc/s.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/sa.bbc.co.uk/bbc/bbc/s.html b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/sa.bbc.co.uk/bbc/bbc/s.html
new file mode 100755
index 0000000000..35d42e808f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/sa.bbc.co.uk/bbc/bbc/s.html
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.3.2/newnav/img/search_icon.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.3.2/newnav/img/search_icon.png
new file mode 100755
index 0000000000..ede38e21e1
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.3.2/newnav/img/search_icon.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/autosuggest_loader.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/autosuggest_loader.gif
new file mode 100755
index 0000000000..d568dc0d3f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/autosuggest_loader.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/blocks/dark.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/blocks/dark.png
new file mode 100755
index 0000000000..a03e8f4863
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/blocks/dark.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/blocks/light.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/blocks/light.png
new file mode 100755
index 0000000000..ca04a45b45
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/blocks/light.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/main_sprite.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/main_sprite.png
new file mode 100755
index 0000000000..93fb6c4123
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/main_sprite.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/mast_bg.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/mast_bg.png
new file mode 100755
index 0000000000..78139475b5
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/mast_bg.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/mast_colours.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/mast_colours.png
new file mode 100755
index 0000000000..3a16de88df
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/mast_colours.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/more_arrow.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/more_arrow.png
new file mode 100755
index 0000000000..1cdd516e9c
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/more_arrow.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/mothball/bg.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/mothball/bg.jpg
new file mode 100755
index 0000000000..130aac8e1f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/mothball/bg.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/mothball/i.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/mothball/i.gif
new file mode 100755
index 0000000000..1fcdabcb6b
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/mothball/i.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/nav_divider.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/nav_divider.png
new file mode 100755
index 0000000000..006e134f20
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/nav_divider.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/panel.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/panel.png
new file mode 100755
index 0000000000..695c8da799
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/panel.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/search_icon.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/search_icon.png
new file mode 100755
index 0000000000..ede38e21e1
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/search_icon.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/suggest_sprite.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/suggest_sprite.png
new file mode 100755
index 0000000000..8f948f208e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/suggest_sprite.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/suggest_sprite_rtl.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/suggest_sprite_rtl.png
new file mode 100755
index 0000000000..2129e0b078
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/suggest_sprite_rtl.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/tooltip.png b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/tooltip.png
new file mode 100755
index 0000000000..a030cc240f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/tooltip.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/script/barlesque.js b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/script/barlesque.js
new file mode 100755
index 0000000000..0229aea4f3
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/script/barlesque.js
@@ -0,0 +1 @@
+(function(){var H,F,R,N=null,A={},P={searchSuggestion:"Search the BBC"},G={updateNoImagesState:function(){var S=F("#blq-search-btn")[0];if(S.currentStyle){var T=S.currentStyle.backgroundImage}else{if(window.getComputedStyle){var T=document.defaultView.getComputedStyle(S,null).getPropertyValue("background-image")}}if(T.indexOf("search_icon.png")==-1){return false}else{F("#blq-mast-home").removeClass("blq-no-images");return true}},searchTheBBCSearchHint:function(){var S=F("#blq-search");R(S,"focus",function(){if(S.val()==P.searchSuggestion){S.val("")}});R(S,"blur",function(){if(!S.val()){S.val(P.searchSuggestion)}});R(S.parent().parent(),"submit",function(T){if(S.val()==P.searchSuggestion){S.val("")}});if(S.val()==""&&!F(document.activeElement).eq(S)){S.val(P.searchSuggestion)}},addMorePanel:function(){var T=F("#blq-nav-links"),V=F("#blq-nav-main");T.css({visibility:"hidden",display:"block"});var S=F("body"),U=new H.widgets.Panel(F("#blq-nav-links"),{modal:false,hideWindowedFlash:false,width:T.width(),height:T.height(),template:'<div><div class="panel-hd"></div><div class="panel-bd"></div><div class="panel-ft"></div></div>',autoPosition:false,id:"blq-morepanel"}),X,W=function(){U.hide();V.removeClass("blq-morepanel-shown");unbind(X);return false},Z=function(){if(U.isShown){return false}U.show();V.addClass("blq-morepanel-shown");setTimeout(function(){X=R(document,"click",Y)},0);return false};T.css({visibility:""});U.container.addClass("blq-overlay").addClass("blq-gvl-3").addClass("blq-rst").appendTo("#blq-container-inner");R("#blq-nav-m a","click",Z);function Y(a){if(a.source!=U.container[0]&&!F(a.source).isWithin(U.container)){W()}}},mapPublicApi:function(){if(!window.blqOnDomReady){window.blqOnDomReady=H.ready}},addAutosuggest:function(){var S=this;if(!blq.suggest){blq.suggest=function(T){if(S.suggestion&&S.suggestion._pendingRequest){clearTimeout(S.suggestion._pendingRequest._timeout)}S.suggestion._pendingRequest=null;S.suggestion.setData(T[1]||[]);S.suggestion.find()}}H.ready(function(){var T=document.getElementById("blq-search");if(!T){return false}T.setAttribute("autocomplete","off");T.onfocus=function(){if(document.getElementById("blq-autosuggest")){return false}gloaddisableder.loaddisabled(["glow","1","glow.dom","glow.net","glow.widgets.AutoSuggest"],{async:true,onLoad:function(U){U.ready(function(){var X=U.dom.get;var W=X("#blq-mast input").filter(function(Y){return X(this).attr("name")=="scope"});W=(W.length)?W.attr("value"):"all";S.noData=false;var V=U.dom.get("#blq-container-inner").hasClass("blq-rtl");S.suggestion=new U.widgets.AutoSuggest("#blq-search",[],{index:"title",maxListLength:blq.suggest_short?3:6,activeOnShow:false,useCache:true,formatItem:function(Z){var Y=Z.title;if(Y.length>28){Y=Z.title.substring(0,25)+"...";Y+='<span class="blq-hide">'+Z.title.substring(25,Z.title.length)+"</span>"}return Y},isMatch:function(){return this.data.length?true:false},onInputChange:function(a){a.preventDefault();if(this.noData){return false}U.dom.get("#suggid").remove();if(this._pendingRequest){this._pendingRequest.abort()}var Z=this;var Y=U.data.encodeUrl({q:a.value,scope:W,format:"blq-1",callback:"blq.suggest"});this._pendingRequest=U.net.loaddisabledScript(this.searchHost+"/suggest?"+Y,{useCache:true,charset:"utf-8",onError:function(){Z.inputElement.attr("autocomplete","on");Z.noData=true},timeout:5});U.dom.get("#blq-search-btn").addClass("loaddisableding")},onDataError:function(Y){this.inputElement.attr("autocomplete","on")},onItemSelect:function(a){this.setValue(a.selectedItem.title);var Y=U.dom.get("#suggid");if(!Y.length){Y=U.dom.create('<input type="hidden" name="suggid" id="suggid" />');U.dom.get("#blq-mast form").prepend(Y)}Y.val(a.selectedItem.id);var Z=U.dom.get("#blq-mast form");Z[0].submit()}});S.suggestion.searchHost=S.searchHost||"httpdisabled://search.bbc.co.uk";S.suggestion.overlay.container.attr("id","blq-autosuggest");S.suggestion.overlay.container.addClass("blq-rst");if(V){S.suggestion.overlay.container.addClass("blq-rtl")}S.suggestion.overlay.opts.hideWindowedFlash=false;U.events.addListener(S.suggestion,"show",function(){U.dom.get("#blq-search-btn").removeClass("loaddisableding")})})}});T.onfocus=function(){}}})},setARIAValues:function(){F("#blq-acc").attr("role","navigation");F("#blq-search").attr("role","search");F("#blq-local-nav").attr("role","navigation");F("#blq-content").attr("role","main");F("#blq-nav-main").attr("role","navigation");F("#blq-nav").attr("role","navigation");F("#blq-foot").attr("role","contentinfo")},defaultGoTracking:function(){E("blq-acc",{go:"{id}3/{dir}"});E("blq-mast",{go:"{id}3/{dir}"});E("blq-nav",{go:"{id}3/{dir}"});E("blq-disclaim",{go:"{id}3/{dir}"});E("blq-sitelinks",{go:"{id}3/{count}/{dir}"});E("blq-bbclinks",{go:"{id}3/{dir}"});E("blq-nav-links",{go:"{id}3/{dir}"});E("blq-main",{external:true});if(blq.externalGoTrackingConfig){for(var U in blq.externalGoTrackingConfig){var T=blq.externalGoTrackingConfig[U];var S=H.dom.get(U);if(S.length){E(S,{go:T,external:true})}}}}};function C(){return(document.getElementById&&document.getElementsByTagName)}function E(V,f){var V=document.getElementById(V)||V,f=f||{},W=V.nodeName=="A"?[V]:V.length?V:V.getElementsByTagName("a"),k=f.external||false,h=f.path||window.location.toString().split("bbc.co.uk/")[1],d=V.id?V.id.replace(/-/g,"/"):"_auto",b=f.go||"{path}/int/{id}/{dir}",m=f.currentServer||window.location.href.split("//")[1].split("/")[0];for(var n=0;n<W.length;n++){var X=false,o=false;if((typeof W[n]!="object")||(!W[n].href)||W[n].className.indexOf("blq-nogo")!=-1||W[n].href.indexOf("/go/")!=-1||W[n].href.charAt(0)=="#"||W[n].href.charAt(window.location.toString().split("#")[0].length)=="#"||W[n].href.indexOf("mailto:")==0||W[n].href.indexOf("javascript:")==0||W[n].href.indexOf("itpc:")==0||W[n].href.indexOf("zune:")==0||W[n].href.indexOf("zcast:")==0){continue}if(W[n].href.indexOf("bbc.co.uk")!=-1&&W[n].href.indexOf("#")!=-1){if(W[n].href.indexOf(h+"#")!=-1){continue}}var c=m.replace(/\./g,"\\."),j=W[n].href.split("?")[0],l=new RegExp("^[A-Za-z]+:\\/\\/(?:[^.]+\\.)*(?:bbc\\.co\\.uk|doubleclick\\.net|"+c+")");if(!l.test(j)){if(f.go){var g=j.split("/")[2],X=true,T="/"+b.replace("{dir}",g).replace("{count}",n+1).replace("{path}",h).replace("{id}",d).replace("#","_")}else{var Y=location.pathname,X=true,T=Y+(Y.charAt(Y.length-1)=="/"?"":"/")+"ext/_auto"}}else{if(k){continue}else{var e=j.split("/"),T=b,S=e.length<5?e[e.length-1]||e[e.length-2]:e[e.length-2]||e[e.length-1];T=T.replace("{dir}",S);T=T.replace("{count}",n+1);T=T.replace("{path}",h);T=T.replace("{id}",d);T=T.replace("#","_");T="/"+T}}var Z=W[n].onclick,a,U=false;if(j.indexOf("www.bbc.co.uk")!=-1){a="httpdisabled://www.bbc.co.uk/go"+T+"/-/"+W[n].href.substring(W[n].href.indexOf("bbc.co.uk/")+10,W[n].href.length)}else{if(W[n].href.indexOf("www.bbc.com")!=-1){a="httpdisabled://www.bbc.com/go"+T+"/-/"+W[n].href.substring(W[n].href.indexOf("httpdisabled://www.bbc.com/"),W[n].href.length)}else{if(X||W[n].href.indexOf(m)==-1){a="/go"+T+"/-/"+W[n].href}else{a="/go"+T+"/-/"+W[n].href.substring(W[n].href.indexOf(m)+(m.length+1),W[n].href.length)}}}if(!W[n].blqGoTrackingHref){W[n].onclick=(function(){var i=Z;return function(){this.href=this.blqGoTrackingHref;if(typeof i=="function"){i()}}})()}W[n].blqGoTrackingHref=a}}function B(S){if(I[S]){delete I[S]}if(S=="addGoTrack"||S=="defaultGoTracking"){I.addGoTrack=function(){};I.addGoTrack.isStub=true}}function Q(){return N}function J(S){N=S}function K(S){return A[S]}function M(S,T){A[S]=T}function O(S,T){P[S]=T}function D(U,S){if(U.createTextRange){var T=U.createTextRange();T.moveStart("character",S);T.moveEnd("character",S-U.value.length);T.select()}else{if(U.selectionStart){U.focus();U.setSelectionRange(S,S)}else{U.focus()}}}function L(T){if(C()){H=T,F=T.dom.get,R=T.events.addListener;unbind=T.events.removeListener;for(var S in I){I[S]()}}}var I=(function(){return G})();window.blq={addGoTrack:E,disableFeature:B,environment:Q,setEnvironment:J,setLabel:O,flagpole:K,setFlagpole:M,availableFeatures:I};if(window.gloaddisableder){document.documentElement.className+=" blq-js";gloaddisableder.loaddisabled(["glow","1","glow.dom","glow.widgets.Panel"],{async:true,onLoad:function(S){S.ready(function(){L(S)})}})}})();var demi=(function(){var C=false,B=[],A=null;return{_reset:function(){C=false;B=[];A=null},_loaddisableded:function(){while(B.length>0){demi.getDevice(B.shift(),blq.environment())}},_addScriptTag:function(F){var D=document.getElementsByTagName("head")[0];var E=document.createElement("script");E.type="text/javascript";E.src=F;D.insertBefore(E,D.firstChild)},_setSource:function(D){A=D},getDevice:function(D){B.push(D);if(!C){C=true;demi._addScriptTag(A)}}}})(); \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/style/main.css b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/style/main.css
new file mode 100755
index 0000000000..3faacf7dc5
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/style/main.css
@@ -0,0 +1 @@
+body{font-size:62.5%;font-family:verdana,helvetica,arial,sans-serif;line-height:1;}body,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,legend,input,p,blockquote,th,td,hr{margin:0;padding:0;}h1,h2,h3,h4,h5,h6{font-size:100%;}table{border-collapse:collapse;border-spacing:0;}caption{text-align:left;font-weight:normal;}th{text-align:left;}cite,address{font-style:normal;}ol,ul{list-style:none;}sub,sup{line-height:2;}img{border:none;}pre,code{font-size:1.2em;}fieldset{border:0;}q:before,q:after{content:'';}.blq-rst{font-family:verdana,helvetica,arial,sans-serif;}.blq-rst dl,.blq-rst dt,.blq-rst dd,.blq-rst ul,.blq-rst ol,.blq-rst li,.blq-rst h1,.blq-rst h2,.blq-rst h3,.blq-rst h4,.blq-rst h5,.blq-rst h6,.blq-rst pre,.blq-rst form,.blq-rst fieldset,.blq-rst caption,.blq-rst p,.blq-rst blockquote,.blq-rst th,.blq-rst td,.blq-rst hr{margin:0;padding:0;line-height:1;font-size:100%;background-color:transparent;}.blq-rst *,.blq-rst input,.blq-rst a:link,.blq-rst a:visited{margin:0;padding:0;line-height:1;font-size:100%;font-family:verdana,helvetica,arial,sans-serif;text-decoration:none;font-weight:normal;text-transform:none;}.blq-rst table{border-collapse:collapse;border-spacing:0;}.blq-rst caption,.blq-rst legend{text-align:left;font-weight:normal;}.blq-rst th{text-align:left;}.blq-rst cite,.blq-rst address{font-style:normal;}.blq-rst ol,.blq-rst ul{list-style:none;}.blq-rst sub,.blq-rst sup{line-height:2;}.blq-rst img{border:none;}.blq-rst input,.blq-rst pre,.blq-rst code{font-size:1.1em;}.blq-rst fieldset{border:0;}.blq-rst q:before,.blq-rst q:after{content:'';}.blq-rst h1,.blq-rst h2,.blq-rst h3,.blq-rst h4,.blq-rst h5,.blq-rst h6,.blq-rst th,.blq-rst strong{font-weight:bold;}.blq-rst dt{font-weight:normal;}body{background:#fff;}.blq-hide{position:absolute;left:-2500px;width:1px;overflow:hidden;}.blq-clearfix:after{content:".";display:block;height:0;clear:both;visibility:hidden;}#blq-container{position:relative;padding-bottom:10px;}#blq-pre-mast,#blq-container-inner{width:974px;margin:0 auto;}#blq-pre-mast{z-index:1;}#blq-container-inner{background-color:#fff;position:relative;padding-top:70px;}#blq-pre-mast,#blq-acc,#blq-mast,#blq-main,#blq-foot,#blq-nav{font-size:1.2em;line-height:1.3;font-family:verdana,helvetica,arial,sans-serif;color:#fff;}#blq-pre-mast,#blq-acc,#blq-mast,#blq-main,#blq-foot{position:relative;}#blq-mast,#blq-foot,#blq-nav{direction:ltr;}#blq-main{line-height:1;color:#000;background-color:#fff;}#blq-mast p,#blq-foot p{margin:0;padding-bottom:0;}#blq-acc ul,#blq-foot ul,#blq-foot li{list-style:none;margin:0;line-height:1.3;}#blq-acc a,#blq-mast a,#blq-foot a{text-decoration:none;font-weight:normal;}#blq-acc a:hover,#blq-mast a:hover{color:#fff;}#bbccom_bottom{width:468px;margin:14px 0 0 14px;padding:0;}#blq-acc{position:absolute;top:0;left:0;width:974px;height:69px;border-bottom:1px solid #ccc;z-index:5;}#blq-mast-home.blq-no-images{background-color:#000;}#blq-mast-home{position:absolute;top:19px;width:974px;height:50px;background:transparent url(../img/mast_bg.png) top repeat-x;}#blq-mast-home a{display:block;color:#ccc;font-size:.95em;height:24px;width:84px;padding:0;margin:8px 0 0 8px;}#blq-mast-home span.blq-home,#blq-mast-home .blq-span{position:relative;bottom:11px;left:18px;}#blq-mast-home span.blq-home{text-indent:-2000em;display:block;height:0;}#blq-mast-home a:hover,#blq-nav-main a:hover{background:left -144px url(../img/main_sprite.png) repeat-x;}#blq-mast-home a:hover{background-position:-2px -144px;}#blq-blocks{border:none;}#blq-acc-links{height:19px;width:974px;font-size:.9em;background-color:#fff;}#blq-acc li{float:left;overflow:visible;}#blq-acc-links a{line-height:1.3;color:#000;}#blq-acc-links a:hover{color:#000;text-decoration:underline;}#blq-acc li.blq-hide a:focus,#blq-acc li.blq-hide a:active{position:absolute;top:70px;left:2500px;width:966px;opacity:.9999;font-weight:bold;padding:2px;background:#ff9;border:2px solid #000;z-index:999;}#blq-acc-txt{padding-right:12px;}#blq-acc-help,#blq-acc-mobile{padding:0 12px;background:-186px -112px url(../img/main_sprite.png) no-repeat;}#blq-acc-txt,#blq-acc-help,#blq-acc-mobile{position:relative;top:3px;left:14px;}.blq-toolbar-dark #blq-acc-links,.blq-toolbar-transp #blq-acc-links{background-color:#212121;}.blq-toolbar-dark #blq-acc-links a,.blq-toolbar-transp #blq-acc-links a{color:#fff;}.blq-toolbar-transp #blq-acc-links{opacity:.7;}#blq-mast{z-index:10;position:absolute;top:0;right:9px;left:9px;height:40px;background:#646464;background:rgba(0,0,0,0.7);width:884px;padding-left:92px;}#blq-mast p input{border:0 none;}#blq-mast p input:focus{outline:none;}#blq-mast #blq-search{width:137px;margin:0 -66px 0 0;position:absolute;left:4px;top:4px;height:20px;padding:2px 3px 0 10px;background:url(../img/main_sprite.png) -66px -194px no-repeat #fff;color:#000;line-height:1.2;-webkit-border-radius:0;-webkit-appearance:none;}#blq-mast #blq-search-btn{width:66px;margin:0;padding:0;position:absolute;right:4px;top:4px;color:#000;cursor:pointer;padding-bottom:2px;background:url(../img/main_sprite.png) 0 -194px no-repeat #efefef;height:22px;line-height:1.8;-webkit-border-radius:0;-webkit-appearance:none;}#blq-autosuggest{overflow:visible;margin-top:0;padding:0;margin-left:0;background:transparent none no-repeat 0 0;}#blq-autosuggest.blq-rtl{margin-left:-29px;}#blq-autosuggest ul{border:none;background:#dcdcdc none no-repeat 0 bottom;width:215px;padding:0;}#blq-autosuggest li{padding:7px 10px 7px 8px;font-size:1.2em;border-top:none;color:#333;background:transparent;}#blq-autosuggest li.active{background-color:#575757;color:#fff;}#blq-mast form p{position:absolute;height:22px;border:none;bottom:auto;padding:0;right:0;top:0;width:215px;}#blq-mast form.active p{background:transparent url(../img/suggest_sprite.png) no-repeat 0 0;}#blq-mast form.active #blq-search,#blq-mast form.active #blq-search-btn{background:none;}.blq-gvl-3 #blq-mast #blq-search-btn{width:66px;margin:0;padding:0;position:absolute;right:4px;top:4px;color:#000;cursor:pointer;padding-bottom:2px;background:url(../img/main_sprite.png) 0 -194px no-repeat #fff;height:22px;line-height:1.8;}#blq-nav-main li{display:inline;}#blq-nav-main a{display:block;float:left;height:30px;width:78px;padding-top:20px;text-align:center;color:#ccc;background:left -94px url(../img/main_sprite.png) no-repeat;font-size:.95em;}#blq-nav-w a{width:90px;}#blq-nav-i a,#blq-nav-tr a{width:84px;}#blq-nav-t a{width:56px;}#blq-nav-m a{width:82px;}.blq-lang-cy-GB #blq-nav-i a{padding-top:14px;height:36px;}.blq-lang-cy-GB #blq-mast .blq-tooltip,.blq-lang-cy-GB #blq-mast-home .blq-tooltip{display:none;}.blq-lang-gd-GB #blq-nav-n a{width:104px;}.blq-lang-gd-GB #blq-nav-s a{width:68px;}.blq-lang-gd-GB #blq-nav-w a{width:74px;}.blq-lang-ga-GB #blq-nav-w a{width:75px;}.blq-lang-ga-GB #blq-nav-t a{width:76px;}.blq-lang-ga-GB #blq-nav-i a{width:79px;}.blq-cbeebies #blq-nav-i a{padding-top:14px;height:36px;}.blq-cbbc #blq-nav-n a{width:91px;}.blq-cbbc #blq-nav-s a{width:100px;}.blq-cbbc #blq-nav-w a{width:79px;}.blq-cbbc #blq-nav-i a{width:68px;padding-top:14px;height:36px;}.blq-cbbc #blq-nav-t a{width:51px;padding:14px 5px 0 5px;height:36px;}.blq-cbbc #blq-nav-r a{width:64px;}.blq-cbbc #blq-nav-m a{width:82px;}#blq-nav-m a:hover{background-position:left -240px;}.blq-js .blq-int-nav #blq-nav-links{left:80px;}.blq-int-nav #blq-nav-m a{margin-left:370px;padding-left:10px;width:162px;}#blq-nav{clear:both;font-size:1.1em;line-height:1.3;border-top:1px solid #ccc;background-color:#f9f9f9;}#blq-nav h2{margin:7px 0 7px 14px;font-size:1.3em;}#blq-nav a{text-decoration:none;font-weight:normal;}.blq-js #blq-nav h2{position:absolute;left:-2500px;width:1px;}.blq-js #blq-nav{border:none;}#blq-nav-links{width:486px;border-right:1px solid #ccc;padding-bottom:10px;}.blq-js #blq-nav-links{display:none;width:505px;position:absolute;top:0;left:160px;z-index:999;border:none;}.blq-js #blq-nav-links-inner{padding-top:8px;background-image:url(../img/panel.png);width:100%;}.blq-js #blq-nav-links-inner a{position:relative;}.blq-js #blq-nav-links-inner:after{content:".";display:block;height:0;clear:both;visibility:hidden;}#blq-nav .blq-no-images{border:1px solid #ccc;background-color:#efefef;}#blq-pop a,#blq-az a{position:relative;display:block;color:#000;background:url(../img/main_sprite.png);}#blq-az a{float:right;padding:3px 0 5px 5px;width:146px;margin-top:2px;}#blq-pop a{float:left;width:135px;padding:5px 0 7px 15px;margin-left:10px;}#blq-az a:hover{color:#fff;}#blq-pop{float:left;}#blq-pop li,#blq-az{display:inline;}.blq-js #blq-az a{margin-right:26px;}.blq-js #blq-pop a{padding-bottom:5px;}.blq-js #blq-pop{background:none;}.blq-nav-sub{float:left;width:160px;}.blq-first{clear:both;}.blq-nav-sub li{display:inline;}.blq-nav-sub a,.blq-nav-sub a:link,.blq-nav-sub a:visited{position:relative;display:block;padding:3px 0 4px 5px;margin-left:8px;color:#545454;font-weight:normal;}.blq-nav-sub a:hover{color:#fff;}.blq-js .blq-nav-sub{border-top:none;}.blq-js #blq-nav-links-inner .blq-last{width:159px;}#blq-eng{height:22px;padding:6px 0 0 13px;color:#000;}.blq-js #blq-nav-foot{clear:both;width:100%;height:29px;padding-top:3px;background:url(../img/panel.png) bottom;}#blq-obit{display:none;visibility:hidden;}#blq-mothball{background:url(../img/mothball/bg.jpg) 0 0 repeat-x #fbfbfb;}#blq-mothball a{display:block;height:56px;background:url(../img/mothball/i.gif) 230px 10px no-repeat;color:#666;font-size:2em;text-decoration:none;padding-left:300px;padding-top:17px;}#blq-mothball-sub{display:block;font-size:.5em;padding:10px 0 0 70px;font-weight:bold;}#blq-mothball-sub span{color:#1A75BB;margin-left:5px;}a:hover #blq-mothball-sub span{text-decoration:underline;}.blq-rtl #blq-mast #blq-search{background:url(../img/main_sprite.png) 148px -217px no-repeat #fff;position:absolute;left:70px;top:4px;text-align:right;margin:0;padding:2px 8px 0 5px;}.blq-rtl #blq-mast #blq-search-btn{background:url(../img/main_sprite.png) -2px -217px no-repeat #efefef;position:absolute;left:4px;top:4px;}.blq-rtl #blq-mast-home span.blq-home{text-indent:2000em;bottom:5em;}.blq-rtl #blq-acc{left:auto;right:9px;}.blq-rtl #blq-mast{direction:rtl;}.blq-rtl #blq-nav-main{right:auto;left:227px;background-position:94% 19px;}.blq-rtl #blq-morepanel{right:auto;left:280px;}#blq-container.blq-int-nav #blq-container-inner.blq-rtl #blq-morepanel{left:18px;}.blq-int-nav #blq-nav-m a{margin-left:0;}.blq-rtl #blq-eng{padding:6px 13px 0 0;}.blq-rtl #blq-foot{direction:rtl;}.blq-gvl-3 .blq-rtl #blq-foot #blq-logo{float:right;}.blq-gvl-3 .blq-rtl #blq-foot #blq-disclaim{float:right;clear:right;}.blq-gvl-3 .blq-rtl #blq-footlinks{float:left;}.blq-gvl-3 .blq-rtl #blq-bbclinks{text-align:right;}.blq-rtl #blq-mast-home a{margin:8px 8px 0 0;}.blq-rtl #blq-mast{padding-left:0;padding-right:92px;}.blq-rtl #blq-mast form p{right:auto;left:16px;}.blq-rtl #blq-mast-home a{margin:8px 8px 0 0;}.blq-rtl #blq-mast{padding-left:0;padding-right:92px;}.blq-rtl #blq-mast form p{right:auto;left:16px;}.blq-rtl #blq-acc .blq-hide{direction:rtl;}.blq-rtl #blq-mast form.active p{background:transparent url(../img/suggest_sprite_rtl.png) no-repeat -224px 0;}.blq-tooltip{position:absolute;margin-top:-2.9em;margin-left:-2500px;height:32px;width:30em;z-index:999;text-decoration:none;font-weight:normal;line-height:1.1;}a.blq-tooltipped:hover .blq-tooltip,.blq-tooltipped:focus .blq-tooltip{margin-left:-50px;}a.blq-tooltipped:hover .blq-tipunder,.blq-tooltipped:focus .blq-tipunder{margin-top:1.8em;}a.blq-tooltipped:hover .blq-tipright,.blq-tooltipped:focus .blq-tipright{margin-left:auto;margin-left:-30em;}.blq-tooltip-l{padding:6px 0 12px 13px;color:#000;background:url(../img/tooltip.png) 0 -36px no-repeat;float:left;}.blq-tooltip-r{padding:6px 3px 12px 0;background:url(../img/tooltip.png) right -36px no-repeat;float:left;}.blq-tipunder .blq-tooltip-l{padding:10px 0 14px 13px;background-position:0 0;}.blq-tipunder .blq-tooltip-r{padding:10px 3px 14px 0;background-position:right 0;}.blq-tipright .blq-tooltip-l{padding:5px 8px 14px 0;background-position:right -72px;float:right;}.blq-tipright .blq-tooltip-r{padding:5px 0 14px 11px;background-position:0 -72px;float:right;}.blq-blue #blq-pop a:hover{color:#242B6C;}.blq-blue #blq-pop a,.blq-blue .blq-nav-sub a:hover,.blq-blue #blq-az a{background-color:#4264A7;}.blq-sky #blq-pop a:hover{color:#023F6D;}.blq-sky #blq-pop a,.blq-sky .blq-nav-sub a:hover,.blq-sky #blq-az a{background-color:#057CB5;}.blq-teal #blq-pop a:hover{color:#013E60;}.blq-teal #blq-pop a,.blq-teal .blq-nav-sub a:hover,.blq-teal #blq-az a{background-color:#2591AB;}.blq-lime #blq-pop a:hover{color:#678C00;}.blq-lime #blq-pop a,.blq-lime .blq-nav-sub a:hover,.blq-lime #blq-az a{background-color:#9CBF00;}.blq-green #blq-pop a:hover{color:#1E5900;}.blq-green #blq-pop a,.blq-green .blq-nav-sub a:hover,.blq-green #blq-az a{background-color:#329600;}.blq-aqua #blq-pop a:hover{color:#00574C;}.blq-aqua #blq-pop a,.blq-aqua .blq-nav-sub a:hover,.blq-aqua #blq-az a{background-color:#008A79;}.blq-khaki #blq-pop a:hover{color:#4C482C;}.blq-khaki #blq-pop a,.blq-khaki .blq-nav-sub a:hover,.blq-khaki #blq-az a{background-color:#8A8459;}.blq-magenta #blq-pop a:hover{color:#800143;}.blq-magenta #blq-pop a,.blq-magenta .blq-nav-sub a:hover,.blq-magenta #blq-az a{background-color:#FF0286;}.blq-rose #blq-pop a:hover{color:#6D1448;}.blq-rose #blq-pop a,.blq-rose .blq-nav-sub a:hover,.blq-rose #blq-az a{background-color:#B11F7B;}.blq-purple #blq-pop a:hover{color:#5A266A;}.blq-purple #blq-pop a,.blq-purple .blq-nav-sub a:hover,.blq-purple #blq-az a{background-color:#884593;}.blq-red #blq-pop a:hover{color:#A00000;}.blq-red #blq-pop a,.blq-red .blq-nav-sub a:hover,.blq-red #blq-az a{background-color:#E50000;}.blq-orange #blq-pop a:hover{color:#A64100;}.blq-orange #blq-pop a,.blq-orange .blq-nav-sub a:hover,.blq-orange #blq-az a{background-color:#FF9A1F;}#blq-nav-links #blq-pop a{background-position:-12px -69px;background-repeat:no-repeat;}#blq-nav-links #blq-az a{background-position:-22px -5px;background-repeat:no-repeat;}#blq-nav-links #blq-az a:hover{background-position:-22px -34px;}#blq-foot{clear:both;background-color:#646464;border-top:1px solid #ccc;}#blq-foot p,#blq-foot li,#blq-foot a{font-size:.95em;line-height:1.4;color:#fff;}#blq-foot a{color:#fff;text-decoration:none;}#blq-foot a:hover{color:#d9d9d9;}#blq-footlinks{float:right;width:550px;margin:10px 14px 0 0;}#blq-sitelinks,#blq-bbclinks{text-align:right;background-color:#646464;}#blq-sitelinks{float:left;width:230px;}#blq-bbclinks{float:right;width:320px;}#blq-bbclinks li{float:right;width:160px;}#blq-foot #blq-copy{font-size:1.4em;padding-top:8px;margin-left:13px;width:10em;}#blq-copy img{position:relative;top:5px;}#blq-foot #blq-disclaim{padding:9px 0 12px 0;margin-left:14px;width:19em;}#blq-container .blq-foot-white,.blq-foot-white #blq-sitelinks,.blq-foot-white #blq-bbclinks{background-color:#fff;color:#000;}#blq-container .blq-foot-white a,#blq-container .blq-foot-white p{color:#000;}#blq-container .blq-foot-white a:hover{color:#666;}#blq-container .blq-foot-black,.blq-foot-black #blq-sitelinks,.blq-foot-black #blq-bbclinks{background-color:#000;}#blq-container .blq-foot-black a:hover{color:#b2b2b2;}.skylightTheme #blq-mast-home{background:0 200px #1778B3;}.skylightTheme #blq-acc{border-bottom-color:#45AAE6;}.doveTheme #blq-mast-home{background:0 550px #5B688F;}.doveTheme #blq-acc{border-bottom-color:#7C8EC2;}.tealTheme #blq-mast-home{background:0 100px #2383A3;}.tealTheme #blq-acc{border-bottom-color:#53B5D6;}.aquaTheme #blq-mast-home{background:0 600px #158979;}.aquaTheme #blq-acc{border-bottom-color:#3CBCAB;}.greenTheme #blq-mast-home{background:0 500px #5D891B;}.greenTheme #blq-acc{border-bottom-color:#80BC25;}.violetTheme #blq-mast-home{background:0 50px #6A5789;}.violetTheme #blq-acc{border-bottom-color:#A496BC;}.purpleTheme #blq-mast-home{background:0 300px #823892;}.purpleTheme #blq-acc{border-bottom-color:#B56CC5;}.pinkTheme #blq-mast-home{background:0 350px #9D1767;}.pinkTheme #blq-acc{border-bottom-color:#D04283;}.oliveTheme #blq-mast-home{background:0 450px #7C7854;}.oliveTheme #blq-acc{border-bottom-color:#AFAC92;}.suedeTheme #blq-mast-home{background:0 150px #695C4A;}.suedeTheme #blq-acc{border-bottom-color:#9C896E;}.redTheme #blq-mast-home{background:0 250px #9E2C1D;}.redTheme #blq-acc{border-bottom-color:#D15A4A;}.orangeTheme #blq-mast-home{background:0 400px #C55F16;}.orangeTheme #blq-acc{border-bottom-color:#DC7D2A;}.blackTheme #blq-acc{border-bottom-color:#505153;}.blq_hp #blq-mast-home{background-image:url(../img/mast_colours.png);}.blq_hp #blq-mast-home a,.blq_hp #blq-nav-main a{color:#fff;}.blq_hp #blq-pop .blq-last{display:none;}.bbcdotcomAdvertsResetBottom .blq-dotcom #blq-footlinks{width:550px;}.bbcdotcomAdvertsResetBottom .blq-dotcom #blq-sitelinks{width:230px;}.blq-dotcom #blq-footlinks{width:470px;}.blq-dotcom #blq-sitelinks{width:150px;}@media print{#blq-obit,#blq-mast,#blq-mast p,#blq-nav-main,#blq-nav,#blq-acc-links{display:none;}#blq-acc{border-bottom:1px solid #000;}#blq-foot{border-top:1px solid #000;}}#blq-container.blq-gvl-3{padding:0;}.blq-gvl-3 .blq-rst *,.blq-gvl-3 .blq-rst input,.blq-gvl-3 .blq-rst a:link,.blq-gvl-3 .blq-rst a:visited{font-family:arial,helvetica,sans-serif;}.blq-gvl-3 #blq-pre-mast,.blq-gvl-3 #blq-acc,.blq-gvl-3 #blq-mast,.blq-gvl-3 #blq-main,.blq-gvl-3 #blq-foot,.blq-gvl-3 #blq-nav{font-size:1.3em;line-height:1.6em;font-family:arial,sans-serif;}.blq-gvl-3 #blq-mast.blq-mast-light{background:rgba(0,0,0,0.4);}.blq-gvl-3 #blq-pre-mast,.blq-gvl-3 #blq-container-inner{width:976px;}.blq-gvl-3 #blq-container-inner{padding:40px 9px 0 9px;background:transparent;}.blq-gvl-3 #blq-acc{height:40px;border:none;position:auto;z-index:11;width:92px;left:9px;}.blq-gvl-3 #blq-acc-links{font-size:1.0em;background:none;margin-left:87px;font-size:.923em;position:absolute;z-index:12;width:250px;}.blq-gvl-3 #blq-acc-links li{padding:13px 7px 0;}.blq-gvl-3 #blq-acc-links a{color:#fff;line-height:14px;}.blq-gvl-3 #blq-acc-txt,.blq-gvl-3 #blq-acc-help,.blq-gvl-3 #blq-acc-mobile{top:0;}.blq-gvl-3 #blq-acc-mobile{background:none;line-height:14px;}.blq-gvl-3 #blq-acc-links li{padding:14px 7px 0;}.blq-gvl-3 #blq-acc-links a{color:#fff;line-height:100%;}.blq-gvl-3 #blq-acc-txt,.blq-gvl-3 #blq-acc-help,.blq-gvl-3 #blq-acc-mobile{top:0;}.blq-gvl-3 #blq-acc-mobile{background:none;position:static;float:left;padding:0 0 0 8px;}.blq-gvl-3 #blq-acc-mobile a{color:#fff;padding:14px 8px;display:block;font-size:.923em;}.blq-gvl-3 #blq-acc-txt,.blq-gvl-3 #blq-acc-help{display:none;}.blq-gvl-3 #blq-acc-txt a,.blq-gvl-3 #blq-acc-help a{color:#fff;line-height:1;}.blq-gvl-3 #blq-acc-help{display:none;}.blq-gvl-3 #blq-mast #blq-search{width:175px;padding:4px 4px 5px 7px;height:15px;font-size:.923em;color:#4c4c4c;background-image:none;left:auto;top:8px;right:37px;margin:0;}.blq-gvl-3 #blq-mast #blq-search.focused{color:#bbb;}.blq-gvl-3 #blq-mast #blq-search-btn,.blq-gvl-3 #blq-mast form.active #blq-search-btn{height:24px;width:29px;overflow:hidden;padding:0;border:none;background:#fff url('../img/search_icon.png') no-repeat center center;text-indent:-2000em;left:auto;top:8px;right:8px;}.blq-gvl-3 #blq-mast #blq-search-btn.loaddisableding{background-image:url('../img/autosuggest_loaddisableder.gif');}.blq-gvl-3 #blq-mast form.active #blq-search{background:#fff;}.blq-gvl-3 #blq-mast form.active p{background:#dcdcdc none no-repeat 0 0;color:#fff;}.blq-rtl #blq-mast #blq-search-btn{right:194px;}.blq-rtl #blq-acc-mobile{float:right;padding:0 8px 0 0;}.blq-rtl #blq-acc-mobile a{font-family:Nassim,arial,helvetica,sans-serif;padding-top:12px;padding-bottom:0;font-size:1.154em;}.blq-rtl #blq-mast #blq-search{right:8px;font-size:1.154em;font-weight:bold;font-family:Nassim,arial,helvetica,sans-serif;color:#505050;}.blq-gvl-3 #blq-mast-home{background:transparent;}.blq-gvl-3 #blq-mast-home{top:0;width:auto;height:40px;position:absolute;z-index:5;}.blq-gvl-3 #blq-mast-home a:hover{background:none;}.blq-gvl-3 #blq-mast-home img{height:24px;}.blq-gvl-3 #blq-mast-home .blq-span{bottom:16px;left:534px;position:absolute;display:none;}#blq-nav-main{position:absolute;right:227px;top:0;}.blq-gvl-3 #blq-nav-main a{height:26px;width:auto;padding:14px 7px 0 7px;font-size:.923em;color:#fff;background:url(../img/nav_divider.png) no-repeat right 13px;}.blq-gvl-3 #blq-nav-main #blq-nav-h a{background:none;}.blq-gvl-3 #blq-acc-links a:hover,.blq-gvl-3 #blq-nav-main a:hover,.blq-gvl-3 #blq-acc-mobile a:hover{color:#fff;text-decoration:underline;}.blq-gvl-3 #blq-nav-main #blq-nav-m a{background:none;margin-right:4px;padding-right:24px;background:url(../img/more_arrow.png) no-repeat scroll right 19px;}#blq-container-inner.blq-rtl #blq-nav-m a{background:none;margin-left:4px;padding-left:24px;padding-right:7px;margin-right:0;background:url(../img/more_arrow.png) no-repeat scroll left 19px;}#blq-container-inner.blq-rtl #blq-nav-main.blq-not-uk #blq-nav-m a{font-family:Nassim,arial,helvetica,sans-serif;font-size:1.154em;padding-top:12px;font-weight:normal;}#blq-container-inner.blq-rtl #blq-nav-main.blq-morepanel-shown #blq-nav-m a{background:url("../img/more_arrow.png") no-repeat scroll left -12px #DCDCDC;}.blq-js .blq-overlay{position:absolute;display:none;overflow:visible;}.blq-js .blq-morepanel-shown .overlay{display:block;}.blq-gvl-3 #blq-nav-links{display:block;width:336px;height:120px;font-size:1em;right:9px;border:none;padding:0;}#blq-container.blq-int-nav #blq-container-inner.blq-rtl #blq-nav-links{width:384px;}.blq-gvl-3 .blq-nav-sub{width:auto;}.blq-rtl .blq-nav-sub{text-align:left;}.blq-js .blq-gvl-3 #blq-nav-links-inner .blq-last{width:104px;}.blq-gvl-3 .blq-nav-sub a,.blq-gvl-3 .blq-nav-sub a:link,.blq-gvl-3 .blq-nav-sub a:visited,.blq-gvl-3 #blq-pop li a,.blq-gvl-3 #blq-az a{font-size:1.2em;margin:0;padding:4px 0 4px 8px;line-height:16px;width:104px;color:#333;}.blq-gvl-3 #blq-pop{clear:both;}.blq-gvl-3 #blq-pop li a{color:#000;}.blq-gvl-3 #blq-pop li a,.blq-gvl-3 #blq-nav-links #blq-az a{background:transparent;}#blq-nav-main.blq-morepanel-shown #blq-nav-m a,#blq-nav-main.blq-morepanel-shown #blq-nav-m a:hover{color:#333;margin-top:8px;padding-top:6px;background:#dcdcdc url(../img/more_arrow.png) no-repeat right -12px;}.blq-js .blq-gvl-3 #blq-nav-links{position:absolute;display:none;left:auto;background:#e9e9e9;background:#dcdcdc;}.blq-js .blq-overlay #blq-nav-links{display:block;}.blq-js .blq-gvl-3 #blq-nav-links-inner,.blq-js .blq-gvl-3 #blq-nav-foot{padding:0;background:transparent;height:auto;}.blq-js .blq-gvl-3 #blq-nav #blq-nav-foot a{font-weight:bold;}.blq-js .blq-gvl-3 #blq-nav-foot h3{border-top:#c1c1c1 solid 1px;left:8px;height:1px;width:320px;}.blq-gvl-3 #blq-pop a:hover,.blq-gvl-3 .blq-nav-sub a:hover,.blq-gvl-3 #blq-nav-links #blq-az a:hover{color:#fff;background-color:#333;}.blq-gvl-3 #blq-az a{float:left;}.blq-rtl #blq-az a{float:none;}.blq-gvl-3 #blq-acc li.blq-hide a:focus,.blq-gvl-3 #blq-acc li.blq-hide a:active{position:absolute;top:40px;left:3036px;width:336px;opacity:.9999;font-weight:normal;padding:8px;color:#333;background:#dcdcdc;border:none;}.blq-gvl-3 #blq-main{background:transparent;}#blq-morepanel{top:40px;right:0;}.blq-gvl-3 #blq-foot{clear:both;border:none;backround:none;}.blq-gvl-3 #blq-foot p,.blq-gvl-3 #blq-foot li,.blq-gvl-3 #blq-foot a{font-size:1em;line-height:normal;}.blq-gvl-3 #blq-footlinks{margin:0;}.blq-gvl-3 #blq-bbclinks{background-color:transparent;}.blq-gvl-3 #blq-foot #blq-copy{font-size:1em;padding:0;margin:0;width:auto;}.blq-gvl-3 #blq-foot #blq-disclaim{padding:0;margin:0;width:auto;line-height:normal;}.blq-gvl-3 #bbccom_bottom{margin:0;padding:0;}.blq-gvl-3 #blq-foot{font-size:1.2em;padding:16px 16px 13px 16px;width:944px;}.blq-gvl-3 #blq-foot p,.blq-gvl-3 #blq-foot li{color:#fff;}.blq-gvl-3 #blq-foot a:link,.blq-gvl-3 #blq-foot a:visited,.blq-gvl-3 #blq-foot a:hover,.blq-gvl-3 #blq-foot a:active,.blq-gvl-3 #blq-foot a{text-decoration:none;text-shadow:none;color:#fff;}.blq-gvl-3 #blq-foot a:hover{text-decoration:underline;}.blq-gvl-3 #blq-footlinks{float:right;width:320px;margin:-4px 0 0 0;}.blq-gvl-3 #blq-bbclinks{text-align:left;}.blq-gvl-3 #blq-bbclinks{float:right;width:320px;}.blq-gvl-3 #blq-bbclinks li{float:right;line-height:16px;padding-right:16px;width:144px;}.blq-gvl-3 #blq-foot #blq-copy{font-weight:bold;color:#fff;}.blq-gvl-3 #blq-foot #blq-logo{float:left;height:24px;width:84px;margin-bottom:23px;}.blq-gvl-3 #blq-foot #blq-disclaim{line-height:16px;clear:left;float:left;width:307px;}#blq-container.blq-gvl-3 .blq-foot-transparent{clear:both;font-size:1.2em;padding-left:0;width:960px;background-color:transparent;}#blq-container.blq-gvl-3 .blq-foot-text-dark{border-top:1px solid #4c4c4c;}#blq-container.blq-gvl-3 .blq-foot-text-dark a,#blq-container.blq-gvl-3 .blq-foot-text-dark p,#blq-container.blq-gvl-3 .blq-foot-text-dark #blq-copy{color:#4c4c4c;}#blq-container.blq-gvl-3 .blq-foot-text-light{border-top:1px solid #fff;}#blq-container.blq-gvl-3 .blq-foot-text-light a,#blq-container.blq-gvl-3 .blq-foot-text-light p{color:#fff;}#blq-container.blq-gvl-3 .blq-foot-opaque{background:none repeat scroll 0 0 rgba(0,0,0,0.7);}#blq-container.blq-gvl-3 .blq-foot-black{background-color:black;}.blq-gvl-3 #blq-foot #blq-promo{margin-top:-16px;margin-left:-16px;margin-bottom:16px;}.blq-gvl-3 #blq-foot.blq-foot-transparent #blq-promo{margin-top:0;margin-left:0;margin-bottom:16px;}.blq-gvl-3 #bbccom_bottom{float:left;margin-bottom:8px;margin-top:0;margin-left:16px;width:468px;height:60px;}.blq-gvl-3 #blq-container-inner{padding-top:0;}.blq-gvl-3 #blq-main{padding-top:40px;} \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/requirejs/0.6.4/sharedmodules/require.js b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/requirejs/0.6.4/sharedmodules/require.js
new file mode 100755
index 0000000000..95e91e6976
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/frameworks/requirejs/0.6.4/sharedmodules/require.js
@@ -0,0 +1 @@
+var require,define;(function(){var version="0.24.0",commentRegExp=/(\/\*([\s\S]*?)\*\/|\/\/(.*)$)/mg,cjsRequireRegExp=/require\(["']([^'"\s]+)["']\)/g,currDirRegExp=/^\.\//,jsSuffixRegExp=/\.js$/,ostring=Object.prototype.toString,ap=Array.prototype,aps=ap.slice,apsp=ap.splice,isBrowser=!!(typeof window!=="undefined"&&navigator&&document),isWebWorker=!isBrowser&&typeof importScripts!=="undefined",readyRegExp=isBrowser&&navigator.platform==="PLAYSTATION 3"?/^complete$/:/^(complete|loaddisableded)$/,defContextName="_",isOpera=typeof opera!=="undefined"&&opera.toString()==="[object Opera]",reqWaitIdPrefix="_r@@",empty={},contexts={},globalDefQueue=[],interactiveScript=null,isDone=false,useInteractive=false,req,cfg={},currentlyAddingScript,s,head,baseElement,scripts,script,src,subPath,mainScript,dataMain,i,scrollIntervalId,setReadyState,ctx;function isFunction(it){return ostring.call(it)==="[object Function]"}function isArray(it){return ostring.call(it)==="[object Array]"}function mixin(target,source,force){for(var prop in source){if(!(prop in empty)&&(!(prop in target)||force)){target[prop]=source[prop]}}return req}function configurePackageDir(pkgs,currentPackages,dir){var i,location,pkgObj;for(i=0;(pkgObj=currentPackages[i]);i++){pkgObj=typeof pkgObj==="string"?{name:pkgObj}:pkgObj;location=pkgObj.location;if(dir&&(!location||(location.indexOf("/")!==0&&location.indexOf(":")===-1))){location=dir+"/"+(location||pkgObj.name)}pkgs[pkgObj.name]={name:pkgObj.name,location:location||pkgObj.name,lib:pkgObj.lib||"lib",main:(pkgObj.main||"lib/main").replace(currDirRegExp,"").replace(jsSuffixRegExp,"")}}}if(typeof require!=="undefined"){if(isFunction(require)){return }else{cfg=require}}function newContext(contextName){var context,resume,config={waitSeconds:7,baseUrl:s.baseUrl||"./",paths:{},pkgs:{}},defQueue=[],specified={require:true,exports:true,module:true},urlMap={},defined={},loaddisableded={},waiting={},waitAry=[],waitIdCounter=0,managerCallbacks={},plugins={},pluginsQueue={},resumeDepth=0,normalizedWaiting={};function trimDots(ary){var i,part;for(i=0;(part=ary[i]);i++){if(part==="."){ary.splice(i,1);i-=1}else{if(part===".."){if(i===1&&(ary[2]===".."||ary[0]==="..")){break}else{if(i>0){ary.splice(i-1,2);i-=2}}}}}}function normalize(name,baseName){var pkgName,pkgConfig;if(name.charAt(0)==="."){if(baseName){if(config.pkgs[baseName]){baseName=[baseName]}else{baseName=baseName.split("/");baseName=baseName.slice(0,baseName.length-1)}name=baseName.concat(name.split("/"));trimDots(name);pkgConfig=config.pkgs[(pkgName=name[0])];name=name.join("/");if(pkgConfig&&name===pkgName+"/"+pkgConfig.main){name=pkgName}}}return name}function makeModuleMap(name,parentModuleMap){var index=name?name.indexOf("!"):-1,prefix=null,parentName=parentModuleMap?parentModuleMap.name:null,originalName=name,normalizedName,url,pluginModule;if(index!==-1){prefix=name.substring(0,index);name=name.substring(index+1,name.length)}if(prefix){prefix=normalize(prefix,parentName)}if(name){if(prefix){pluginModule=defined[prefix];if(pluginModule){if(pluginModule.normalize){normalizedName=pluginModule.normalize(name,function(name){return normalize(name,parentName)})}else{normalizedName=normalize(name,parentName)}}else{normalizedName="__$p"+parentName+"@"+name}}else{normalizedName=normalize(name,parentName)}url=urlMap[normalizedName];if(!url){if(req.toModuleUrl){url=req.toModuleUrl(context,name,parentModuleMap)}else{url=context.nameToUrl(name,null,parentModuleMap)}urlMap[normalizedName]=url}}return{prefix:prefix,name:normalizedName,parentMap:parentModuleMap,url:url,originalName:originalName,fullName:prefix?prefix+"!"+normalizedName:normalizedName}}function isPriorityDone(){var priorityDone=true,priorityWait=config.priorityWait,priorityName,i;if(priorityWait){for(i=0;(priorityName=priorityWait[i]);i++){if(!loaddisableded[priorityName]){priorityDone=false;break}}if(priorityDone){delete config.priorityWait}}return priorityDone}function makeSetExports(moduleObj){return function(exports){moduleObj.exports=exports}}function makeContextModuleFunc(func,relModuleMap,enableBuildCallback){return function(){var args=[].concat(aps.call(arguments,0)),lastArg;if(enableBuildCallback&&isFunction((lastArg=args[args.length-1]))){lastArg.__requireJsBuild=true}args.push(relModuleMap);return func.apply(null,args)}}function makeRequire(relModuleMap,enableBuildCallback){var modRequire=makeContextModuleFunc(context.require,relModuleMap,enableBuildCallback);mixin(modRequire,{nameToUrl:makeContextModuleFunc(context.nameToUrl,relModuleMap),toUrl:makeContextModuleFunc(context.toUrl,relModuleMap),isDefined:makeContextModuleFunc(context.isDefined,relModuleMap),ready:req.ready,isBrowser:req.isBrowser});if(req.paths){modRequire.paths=req.paths}return modRequire}function updateNormalizedNames(pluginName){var oldFullName,oldModuleMap,moduleMap,fullName,callbacks,i,j,k,depArray,existingCallbacks,maps=normalizedWaiting[pluginName];if(maps){for(i=0;(oldModuleMap=maps[i]);i++){oldFullName=oldModuleMap.fullName;moduleMap=makeModuleMap(oldModuleMap.originalName,oldModuleMap.parentMap);fullName=moduleMap.fullName;callbacks=managerCallbacks[oldFullName]||[];existingCallbacks=managerCallbacks[fullName];if(fullName!==oldFullName){if(oldFullName in specified){delete specified[oldFullName];specified[fullName]=true}if(existingCallbacks){managerCallbacks[fullName]=existingCallbacks.concat(callbacks)}else{managerCallbacks[fullName]=callbacks}delete managerCallbacks[oldFullName];for(j=0;j<callbacks.length;j++){depArray=callbacks[j].depArray;for(k=0;k<depArray.length;k++){if(depArray[k]===oldFullName){depArray[k]=fullName}}}}}}delete normalizedWaiting[pluginName]}function queueDependency(dep){var prefix=dep.prefix,fullName=dep.fullName;if(specified[fullName]||fullName in defined){return }if(prefix&&!plugins[prefix]){plugins[prefix]=undefined;(normalizedWaiting[prefix]||(normalizedWaiting[prefix]=[])).push(dep);(managerCallbacks[prefix]||(managerCallbacks[prefix]=[])).push({onDep:function(name,value){if(name===prefix){updateNormalizedNames(prefix)}}});queueDependency(makeModuleMap(prefix))}context.paused.push(dep)}function execManager(manager){var i,ret,waitingCallbacks,cb=manager.callback,fullName=manager.fullName,args=[],ary=manager.depArray;if(cb&&isFunction(cb)){if(ary){for(i=0;i<ary.length;i++){args.push(manager.deps[ary[i]])}}ret=req.execCb(fullName,manager.callback,args);if(fullName){if(manager.usingExports&&ret===undefined&&(!manager.cjsModule||!("exports" in manager.cjsModule))){ret=defined[fullName]}else{if(manager.cjsModule&&"exports" in manager.cjsModule){ret=defined[fullName]=manager.cjsModule.exports}else{if(fullName in defined&&!manager.usingExports){return req.onError(new Error(fullName+" has already been defined"))}defined[fullName]=ret}}}}else{if(fullName){ret=defined[fullName]=cb}}if(fullName){waitingCallbacks=managerCallbacks[fullName];if(waitingCallbacks){for(i=0;i<waitingCallbacks.length;i++){waitingCallbacks[i].onDep(fullName,ret)}delete managerCallbacks[fullName]}}if(waiting[manager.waitId]){delete waiting[manager.waitId];manager.isDone=true;context.waitCount-=1;if(context.waitCount===0){waitAry=[]}}return undefined}function main(inName,depArray,callback,relModuleMap){var moduleMap=makeModuleMap(inName,relModuleMap),name=moduleMap.name,fullName=moduleMap.fullName,uniques={},manager={waitId:name||reqWaitIdPrefix+(waitIdCounter++),depCount:0,depMax:0,prefix:moduleMap.prefix,name:name,fullName:fullName,deps:{},depArray:depArray,callback:callback,onDep:function(depName,value){if(!(depName in manager.deps)){manager.deps[depName]=value;manager.depCount+=1;if(manager.depCount===manager.depMax){execManager(manager)}}}},i,depArg,depName,cjsMod;if(fullName){if(fullName in defined||loaddisableded[fullName]===true){return }specified[fullName]=true;loaddisableded[fullName]=true;context.jQueryDef=(fullName==="jquery")}for(i=0;i<depArray.length;i++){depArg=depArray[i];if(depArg){depArg=makeModuleMap(depArg,(name?moduleMap:relModuleMap));depName=depArg.fullName;depArray[i]=depName;if(depName==="require"){manager.deps[depName]=makeRequire(moduleMap)}else{if(depName==="exports"){manager.deps[depName]=defined[fullName]={};manager.usingExports=true}else{if(depName==="module"){manager.cjsModule=cjsMod=manager.deps[depName]={id:name,uri:name?context.nameToUrl(name,null,relModuleMap):undefined};cjsMod.setExports=makeSetExports(cjsMod)}else{if(depName in defined&&!(depName in waiting)){manager.deps[depName]=defined[depName]}else{if(!uniques[depName]){manager.depMax+=1;queueDependency(depArg);(managerCallbacks[depName]||(managerCallbacks[depName]=[])).push(manager);uniques[depName]=true}}}}}}}if(manager.depCount===manager.depMax){execManager(manager)}else{waiting[manager.waitId]=manager;waitAry.push(manager);context.waitCount+=1}}function callDefMain(args){main.apply(null,args);loaddisableded[args[0]]=true}function jQueryCheck(jqCandidate){if(!context.jQuery){var $=jqCandidate||(typeof jQuery!=="undefined"?jQuery:null);if($&&"readyWait" in $){context.jQuery=$;callDefMain(["jquery",[],function(){return jQuery}]);if(context.scriptCount){$.readyWait+=1;context.jQueryIncremented=true}}}}function forceExec(manager,traced){if(manager.isDone){return undefined}var fullName=manager.fullName,depArray=manager.depArray,depName,i;if(fullName){if(traced[fullName]){return defined[fullName]}traced[fullName]=true}for(i=0;i<depArray.length;i++){depName=depArray[i];if(depName){if(!manager.deps[depName]&&waiting[depName]){manager.onDep(depName,forceExec(waiting[depName],traced))}}}return fullName?defined[fullName]:undefined}function checkLoaded(){var waitInterval=config.waitSeconds*1000,expired=waitInterval&&(context.startTime+waitInterval)<new Date().getTime(),noLoads="",hasLoadedProp=false,stillLoading=false,prop,err,manager;if(context.pausedCount>0){return undefined}if(config.priorityWait){if(isPriorityDone()){resume()}else{return undefined}}for(prop in loaddisableded){if(!(prop in empty)){hasLoadedProp=true;if(!loaddisableded[prop]){if(expired){noLoads+=prop+" "}else{stillLoading=true;break}}}}if(!hasLoadedProp&&!context.waitCount){return undefined}if(expired&&noLoads){err=new Error("require.js loaddisabled timeout for modules: "+noLoads);err.requireType="timeout";err.requireModules=noLoads;return req.onError(err)}if(stillLoading||context.scriptCount){if(isBrowser||isWebWorker){setTimeout(checkLoaded,50)}return undefined}if(context.waitCount){for(i=0;(manager=waitAry[i]);i++){forceExec(manager,{})}checkLoaded();return undefined}req.checkReadyState();return undefined}function callPlugin(pluginName,dep){var name=dep.name,fullName=dep.fullName,loaddisabled;if(fullName in defined||fullName in loaddisableded){return }if(!plugins[pluginName]){plugins[pluginName]=defined[pluginName]}if(!loaddisableded[fullName]){loaddisableded[fullName]=false}loaddisabled=function(ret){if(require.onPluginLoad){require.onPluginLoad(context,pluginName,name,ret)}execManager({prefix:dep.prefix,name:dep.name,fullName:dep.fullName,callback:function(){return ret}});loaddisableded[fullName]=true};loaddisabled.fromText=function(moduleName,text){var hasInteractive=useInteractive;context.loaddisableded[moduleName]=false;context.scriptCount+=1;if(hasInteractive){useInteractive=false}eval(text);if(hasInteractive){useInteractive=true}context.completeLoad(moduleName)};plugins[pluginName].loaddisabled(name,makeRequire(dep.parentMap,true),loaddisabled,config)}function loaddisabledPaused(dep){if(dep.prefix&&dep.name.indexOf("__$p")===0&&defined[dep.prefix]){dep=makeModuleMap(dep.originalName,dep.parentMap)}var pluginName=dep.prefix,fullName=dep.fullName;if(specified[fullName]||loaddisableded[fullName]){return }else{specified[fullName]=true}if(pluginName){if(defined[pluginName]){callPlugin(pluginName,dep)}else{if(!pluginsQueue[pluginName]){pluginsQueue[pluginName]=[];(managerCallbacks[pluginName]||(managerCallbacks[pluginName]=[])).push({onDep:function(name,value){if(name===pluginName){var i,oldModuleMap,ary=pluginsQueue[pluginName];for(i=0;i<ary.length;i++){oldModuleMap=ary[i];callPlugin(pluginName,makeModuleMap(oldModuleMap.originalName,oldModuleMap.parentMap))}delete pluginsQueue[pluginName]}}})}pluginsQueue[pluginName].push(dep)}}else{req.loaddisabled(context,fullName,dep.url)}}resume=function(){var args,i,p;resumeDepth+=1;if(context.scriptCount<=0){context.scriptCount=0}while(defQueue.length){args=defQueue.shift();if(args[0]===null){return req.onError(new Error("Mismatched anonymous require.def modules"))}else{callDefMain(args)}}if(!config.priorityWait||isPriorityDone()){while(context.paused.length){p=context.paused;context.pausedCount+=p.length;context.paused=[];for(i=0;(args=p[i]);i++){loaddisabledPaused(args)}context.startTime=(new Date()).getTime();context.pausedCount-=p.length}}if(resumeDepth===1){checkLoaded()}resumeDepth-=1;return undefined};context={contextName:contextName,config:config,defQueue:defQueue,waiting:waiting,waitCount:0,specified:specified,loaddisableded:loaddisableded,urlMap:urlMap,scriptCount:0,urlFetched:{},defined:defined,paused:[],pausedCount:0,plugins:plugins,managerCallbacks:managerCallbacks,makeModuleMap:makeModuleMap,normalize:normalize,configure:function(cfg){var paths,prop,packages,pkgs,packagePaths,requireWait;if(cfg.baseUrl){if(cfg.baseUrl.charAt(cfg.baseUrl.length-1)!=="/"){cfg.baseUrl+="/"}}paths=config.paths;packages=config.packages;pkgs=config.pkgs;mixin(config,cfg,true);if(cfg.paths){for(prop in cfg.paths){if(!(prop in empty)){paths[prop]=cfg.paths[prop]}}config.paths=paths}packagePaths=cfg.packagePaths;if(packagePaths||cfg.packages){if(packagePaths){for(prop in packagePaths){if(!(prop in empty)){configurePackageDir(pkgs,packagePaths[prop],prop)}}}if(cfg.packages){configurePackageDir(pkgs,cfg.packages)}config.pkgs=pkgs}if(cfg.priority){requireWait=context.requireWait;context.requireWait=false;context.takeGlobalQueue();resume();context.require(cfg.priority);resume();context.requireWait=requireWait;config.priorityWait=cfg.priority}if(cfg.deps||cfg.callback){context.require(cfg.deps||[],cfg.callback)}if(cfg.ready){req.ready(cfg.ready)}},isDefined:function(moduleName,relModuleMap){return makeModuleMap(moduleName,relModuleMap).fullName in defined},require:function(deps,callback,relModuleMap){var moduleName,ret,moduleMap;if(typeof deps==="string"){if(req.get){return req.get(context,deps,callback)}moduleName=deps;relModuleMap=callback;moduleMap=makeModuleMap(moduleName,relModuleMap);ret=defined[moduleMap.fullName];if(ret===undefined){return req.onError(new Error("require: module name '"+moduleMap.fullName+"' has not been loaddisableded yet for context: "+contextName))}return ret}main(null,deps,callback,relModuleMap);if(!context.requireWait){while(!context.scriptCount&&context.paused.length){resume()}}return undefined},takeGlobalQueue:function(){if(globalDefQueue.length){apsp.apply(context.defQueue,[context.defQueue.length-1,0].concat(globalDefQueue));globalDefQueue=[]}},completeLoad:function(moduleName){var args;context.takeGlobalQueue();while(defQueue.length){args=defQueue.shift();if(args[0]===null){args[0]=moduleName;break}else{if(args[0]===moduleName){break}else{callDefMain(args);args=null}}}if(args){callDefMain(args)}else{callDefMain([moduleName,[],moduleName==="jquery"&&typeof jQuery!=="undefined"?function(){return jQuery}:null])}loaddisableded[moduleName]=true;jQueryCheck();if(req.isAsync){context.scriptCount-=1}resume();if(!req.isAsync){context.scriptCount-=1}},toUrl:function(moduleNamePlusExt,relModuleMap){var index=moduleNamePlusExt.lastIndexOf("."),ext=null;if(index!==-1){ext=moduleNamePlusExt.substring(index,moduleNamePlusExt.length);moduleNamePlusExt=moduleNamePlusExt.substring(0,index)}return context.nameToUrl(moduleNamePlusExt,ext,relModuleMap)},nameToUrl:function(moduleName,ext,relModuleMap){var paths,pkgs,pkg,pkgPath,syms,i,parentModule,url,config=context.config;if(moduleName.indexOf("./")===0||moduleName.indexOf("../")===0){syms=relModuleMap&&relModuleMap.url?relModuleMap.url.split("/"):[];if(syms.length){syms.pop()}syms=syms.concat(moduleName.split("/"));trimDots(syms);url=syms.join("/")+(ext?ext:(req.jsExtRegExp.test(moduleName)?"":".js"))}else{moduleName=normalize(moduleName,relModuleMap);if(req.jsExtRegExp.test(moduleName)){url=moduleName+(ext?ext:"")}else{paths=config.paths;pkgs=config.pkgs;syms=moduleName.split("/");for(i=syms.length;i>0;i--){parentModule=syms.slice(0,i).join("/");if(paths[parentModule]){syms.splice(0,i,paths[parentModule]);break}else{if((pkg=pkgs[parentModule])){if(moduleName===pkg.name){pkgPath=pkg.location+"/"+pkg.main}else{pkgPath=pkg.location+"/"+pkg.lib}syms.splice(0,i,pkgPath);break}}}url=syms.join("/")+(ext||".js");url=(url.charAt(0)==="/"||url.match(/^\w+:/)?"":config.baseUrl)+url}}return config.urlArgs?url+((url.indexOf("?")===-1?"?":"&")+config.urlArgs):url}};context.jQueryCheck=jQueryCheck;context.resume=resume;return context}req=require=function(deps,callback){var contextName=defContextName,context,config;if(!isArray(deps)&&typeof deps!=="string"){config=deps;if(isArray(callback)){deps=callback;callback=arguments[2]}else{deps=[]}}if(config&&config.context){contextName=config.context}context=contexts[contextName]||(contexts[contextName]=newContext(contextName));if(config){context.configure(config)}return context.require(deps,callback)};req.version=version;req.isArray=isArray;req.isFunction=isFunction;req.mixin=mixin;req.jsExtRegExp=/^\/|:|\?|\.js$/;s=req.s={contexts:contexts,skipAsync:{},isPageLoaded:!isBrowser,readyCalls:[]};req.isAsync=req.isBrowser=isBrowser;if(isBrowser){head=s.head=document.getElementsByTagName("head")[0];baseElement=document.getElementsByTagName("base")[0];if(baseElement){head=s.head=baseElement.parentNode}}req.onError=function(err){throw err};req.loaddisabled=function(context,moduleName,url){var contextName=context.contextName,urlFetched=context.urlFetched,loaddisableded=context.loaddisableded;isDone=false;if(!loaddisableded[moduleName]){loaddisableded[moduleName]=false}if(!urlFetched[url]){context.scriptCount+=1;req.attach(url,contextName,moduleName);urlFetched[url]=true;if(context.jQuery&&!context.jQueryIncremented){context.jQuery.readyWait+=1;context.jQueryIncremented=true}}};function getInteractiveScript(){var scripts,i,script;if(interactiveScript&&interactiveScript.readyState==="interactive"){return interactiveScript}scripts=document.getElementsByTagName("script");for(i=scripts.length-1;i>-1&&(script=scripts[i]);i--){if(script.readyState==="interactive"){return(interactiveScript=script)}}return null}define=req.def=function(name,deps,callback){var node,context;if(typeof name!=="string"){callback=deps;deps=name;name=null}if(!req.isArray(deps)){callback=deps;deps=[]}if(!name&&!deps.length&&req.isFunction(callback)){if(callback.length){callback.toString().replace(commentRegExp,"").replace(cjsRequireRegExp,function(match,dep){deps.push(dep)});deps=["require","exports","module"].concat(deps)}}if(useInteractive){node=currentlyAddingScript||getInteractiveScript();if(!node){return req.onError(new Error("ERROR: No matching script interactive for "+callback))}if(!name){name=node.getAttribute("data-requiremodule")}context=contexts[node.getAttribute("data-requirecontext")]}(context?context.defQueue:globalDefQueue).push([name,deps,callback]);return undefined};define.amd={multiversion:true,plugins:true};req.execCb=function(name,callback,args){return callback.apply(null,args)};req.onScriptLoad=function(evt){var node=evt.currentTarget||evt.srcElement,contextName,moduleName,context;if(evt.type==="loaddisabled"||readyRegExp.test(node.readyState)){interactiveScript=null;contextName=node.getAttribute("data-requirecontext");moduleName=node.getAttribute("data-requiremodule");context=contexts[contextName];contexts[contextName].completeLoad(moduleName);if(node.detachEvent&&!isOpera){node.detachEvent("onreadystatechange",req.onScriptLoad)}else{node.removeEventListener("loaddisabled",req.onScriptLoad,false)}}};req.attach=function(url,contextName,moduleName,callback,type){var node,loaddisableded,context;if(isBrowser){callback=callback||req.onScriptLoad;node=document.createElement("script");node.type=type||"text/javascript";node.charset="utf-8";node.async=!s.skipAsync[url];node.setAttribute("data-requirecontext",contextName);node.setAttribute("data-requiremodule",moduleName);if(node.attachEvent&&!isOpera){useInteractive=true;node.attachEvent("onreadystatechange",callback)}else{node.addEventListener("loaddisabled",callback,false)}node.src=url;currentlyAddingScript=node;if(baseElement){head.insertBefore(node,baseElement)}else{head.appendChild(node)}currentlyAddingScript=null;return node}else{if(isWebWorker){context=contexts[contextName];loaddisableded=context.loaddisableded;loaddisableded[moduleName]=false;importScripts(url);context.completeLoad(moduleName)}}return null};if(isBrowser){scripts=document.getElementsByTagName("script");for(i=scripts.length-1;i>-1&&(script=scripts[i]);i--){if(!head){head=script.parentNode}if((dataMain=script.getAttribute("data-main"))){if(!cfg.baseUrl){src=dataMain.split("/");mainScript=src.pop();subPath=src.length?src.join("/")+"/":"./";cfg.baseUrl=subPath;dataMain=mainScript.replace(jsSuffixRegExp,"")}cfg.deps=cfg.deps?cfg.deps.concat(dataMain):[dataMain];break}}}s.baseUrl=cfg.baseUrl;req.pageLoaded=function(){if(!s.isPageLoaded){s.isPageLoaded=true;if(scrollIntervalId){clearInterval(scrollIntervalId)}if(setReadyState){document.readyState="complete"}req.callReady()}};req.checkReadyState=function(){var contexts=s.contexts,prop;for(prop in contexts){if(!(prop in empty)){if(contexts[prop].waitCount){return }}}s.isDone=true;req.callReady()};req.callReady=function(){var callbacks=s.readyCalls,i,callback,contexts,context,prop;if(s.isPageLoaded&&s.isDone){if(callbacks.length){s.readyCalls=[];for(i=0;(callback=callbacks[i]);i++){callback()}}contexts=s.contexts;for(prop in contexts){if(!(prop in empty)){context=contexts[prop];if(context.jQueryIncremented){context.jQuery.ready(true);context.jQueryIncremented=false}}}}};req.ready=function(callback){if(s.isPageLoaded&&s.isDone){callback()}else{s.readyCalls.push(callback)}return req};if(isBrowser){if(document.addEventListener){document.addEventListener("DOMContentLoaded",req.pageLoaded,false);window.addEventListener("loaddisabled",req.pageLoaded,false);if(!document.readyState){setReadyState=true;document.readyState="loaddisableding"}}else{if(window.attachEvent){window.attachEvent("onloaddisabled",req.pageLoaded);if(self===self.top){scrollIntervalId=setInterval(function(){try{if(document.body){document.documentElement.doScroll("left");req.pageLoaded()}}catch(e){}},30)}}}if(document.readyState==="complete"){req.pageLoaded()}}req(cfg);if(req.isAsync&&typeof setTimeout!=="undefined"){ctx=s.contexts[(cfg.context||defContextName)];ctx.requireWait=true;setTimeout(function(){ctx.requireWait=false;ctx.takeGlobalQueue();ctx.jQueryCheck();if(!ctx.scriptCount){ctx.resume()}req.checkReadyState()},0)}}());(function(){var A=12345,B=[],H,E={};if(navigator.userAgent.toLowerCase().indexOf("msie")>-1){var F=document.getElementById;document.getElementById=function(L){var K=F(L);if(K){if(K.attributes.id.value===L){return K}else{for(var J=1,I=document.all[L].length;J<I;J++){if(document.all[L][J].attributes.id.value===L){return document.all[L][J]}}}}return null}}function C(I){var J;if(I){if(window.getComputedStyle){J=document.defaultView.getComputedStyle(I,null).getPropertyValue("z-index")}else{if(I.currentStyle){J=I.currentStyle.zIndex}}J=+J;return J}}function G(){var K,J;if(!document.body){return }for(var I=0;I<B.length;I++){K=B[I],J=document.getElementById(K[0]);if(!J){J=D(K[0])}if(C(J)===A){K[1]();B.splice(I--,1);if(B.length===0){clearInterval(H)}}}}function D(J){var I=document.createElement("div");I.id=J;I.className="addedByCssp";I.setAttribute("style","position: absolute; width: 1px; height: 1px; top: -1px; left: -1px;");document.body.insertBefore(I,document.body.firstChild);return I}define("cssp",{loaddisabled:function(I,P,R,K){var O=I.lastIndexOf("?"),L=(O>0?I.substring(0,O):I),N=(O>0?I.substring(O+1,I.length):"cssp-"+I.replace(/[^a-z0-9_]/gi,"-")),J="cssp!"+L,Q=document.getElementsByTagName("head")[0],M=document.createElement("link");L=K.paths["cssp!"+L]?K.paths["cssp!"+L]:L;if(!L){throw ("CSS URL is required.")}L=P.toUrl(L);if(E[L]){R(E[L]);return }E[L]=M;B.push([N,function(){var S=document.getElementById(N);if(S.className==="addedByCssp"){document.body.removeChild(S)}R(M)}]);if(B.length===1){H=setInterval(G,250)}M.type="text/css";M.rel="stylesheet";M.href=L;Q.appendChild(M)}})}());require.addPaths=function(){var G={},F,C;for(var E=0,B=arguments.length;E<B;E+=2){F=arguments[E];C=arguments[E+1];for(var D=0,A=C.length;D<A;D++){G[C[D]]=F}}this({paths:G})}; \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/wwtravel/img/ic/304-170/1300928948164652012_1.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/wwtravel/img/ic/304-170/1300928948164652012_1.jpg
new file mode 100755
index 0000000000..5f5a1c71f6
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/wwtravel/img/ic/304-170/1300928948164652012_1.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/wwtravel/img/ic/304-170/130203147123329681316_1.jpg b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/wwtravel/img/ic/304-170/130203147123329681316_1.jpg
new file mode 100755
index 0000000000..8379b97b57
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/static.bbc.co.uk/wwtravel/img/ic/304-170/130203147123329681316_1.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/www.bbc.co.uk/bbc.com/images/interstitial/arrow.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/www.bbc.co.uk/bbc.com/images/interstitial/arrow.gif
new file mode 100755
index 0000000000..66b9d7793e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/www.bbc.co.uk/bbc.com/images/interstitial/arrow.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/www.bbc.co.uk/bbc.com/images/interstitial/header.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/www.bbc.co.uk/bbc.com/images/interstitial/header.gif
new file mode 100755
index 0000000000..ac9530af2c
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/www.bbc.co.uk/bbc.com/images/interstitial/header.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/www.bbc.co.uk/bbc.com/images/interstitial/header_travel.gif b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/www.bbc.co.uk/bbc.com/images/interstitial/header_travel.gif
new file mode 100755
index 0000000000..ac9530af2c
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/www.bbc.co.uk/bbc.com/images/interstitial/header_travel.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/www.bbc.co.uk/news/index.html b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/www.bbc.co.uk/news/index.html
new file mode 100755
index 0000000000..c9c623ed81
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/bbc.co.uk/www.bbc.co.uk/news/index.html
@@ -0,0 +1,2982 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" "httpdisabled://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd">
+
+
+<html xmlns="httpdisabled://www.w3.org/1999/xhtml" xmlns:og="httpdisabled:/voidgraphprotocol.org/schema/" xml:lang="en-GB">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <head profile="httpdisabled://dublincore.org/documents/dcq-html/">
+ <meta http-equiv="X-UA-Compatible" content="IE=8" />
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <title>BBC News - Home</title>
+ <meta name="Description" content="Visit BBC News for up-to-the-minute news, breaking news, video, audio and feature stories. BBC News provides trusted World and UK news as well as local and regional perspectives. Also entertainment, business, science, technology and health news."/>
+ <meta name="OriginalPublicationDate" content="2011/04/08 21:54:12"/>
+ <meta name="UKFS_URL" content="/news/"/>
+ <meta name="Headline" content="INDEX "/>
+ <meta name="IFS_URL" content="/news/"/>
+ <meta name="Section" content="Home"/>
+ <meta name="contentFlavor" content="INDEX"/>
+ <meta name="CPS_ID" content="10263779" />
+ <meta name="CPS_SITE_NAME" content="BBC News" />
+ <meta name="CPS_SECTION_PATH" content="Front page" />
+ <meta name="CPS_ASSET_TYPE" content="IDX" />
+ <meta name="CPS_PLATFORM" content="HighWeb" />
+ <meta name="CPS_AUDIENCE" content="US" />
+
+
+ <meta name="bbcsearch_noindex" content="atom"/>
+ <link rel="canonical" href="index.html" />
+ <link href="httpdisabled://feeds.bbci.co.uk/news/rss.xml" rel="alternate" type="application/rss+xml" title="BBC News - Home" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<!-- THIS FILE CONFIGURES NEWS V6 -->
+
+
+
+
+
+
+
+
+
+
+
+
+ <!-- hi/news/head_first.inc -->
+
+<!-- PULSE_ENABLED:yes -->
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <meta http-equiv="X-UA-Compatible" content="IE=8" /> <link rel="schema.dcterms" href="httpdisabled://purl.org/dc/terms/" /> <link rel="index" href="httpdisabled://www.bbc.co.uk/a-z/" title="A to Z" /> <link rel="help" href="httpdisabled://www.bbc.co.uk/help/" title="BBC Help" /> <link rel="copyright" href="httpdisabled://www.bbc.co.uk/terms/" title="Terms of Use" /> <link rel="icon" href="httpdisabled://www.bbc.co.uk/favicon.ico" type="image/x-icon" /> <meta name="viewport" content="width = 996" /> <link rel="stylesheet" type="text/css" href="../../static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/style/main.css" /> <script type="text/javascript" src="../../node1.bbcimg.co.uk/glow/gloader.0.1.4.js"> </script> <script type="text/javascript"> gloaddisableder.use("glow", {map: "httpdisabled://node1.bbcimg.co.uk/glow/glow/map.1.7.3.js"}); </script> <script type="text/javascript" src="../../static.bbc.co.uk/frameworks/requirejs/0.6.4/sharedmodules/require.js"></script> <script type="text/javascript"> bbcRequireMap = {"jquery-1":"httpdisabled://static.bbc.co.uk/frameworks/jquery/0.1.4/sharedmodules/jquery-1.5.1","jquery-1.4":"httpdisabled://static.bbc.co.uk/frameworks/jquery/0.1.4/sharedmodules/jquery-1.4","swfobject-2":"httpdisabled://static.bbc.co.uk/frameworks/swfobject/0.1.3/sharedmodules/swfobject-2","demi-1":"httpdisabled://static.bbc.co.uk/frameworks/demi/0.6.12/sharedmodules/demi-1","gelui-1":"httpdisabled://static.bbc.co.uk/frameworks/gelui/0.6.5/sharedmodules/gelui-1","cssp!gelui-1/overlay":"httpdisabled://static.bbc.co.uk/frameworks/gelui/0.6.5/sharedmodules/gelui-1/overlay.css","istats-1":"httpdisabled://static.bbc.co.uk/frameworks/nedstat/0.1.28/sharedmodules/istats-1"}; require({ baseUrl: 'http://static.bbc.co.uk/', paths: bbcRequireMap, waitSeconds: 30 }); </script> <script type="text/javascript" src="../../static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/script/barlesque.js"></script>
+ <!--[if (IE 6)|(IE 7)|(IE 8)]> <style type="text/css"> .blq-gvl-3 #blq-mast, body #blq-container.blq-gvl-3 .blq-foot-opaque { background: transparent; -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#B2000000,endColorstr=#B2000000)"; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#B2000000,endColorstr=#B2000000); } .blq-gvl-3 #blq-mast.blq-mast-light { -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#66000000,endColorstr=#66000000)"; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#66000000,endColorstr=#66000000); } html body #blq-container #blq-nav {background: transparent;} .blq-gvl-3 #blq-mast #blq-search { padding: 5px 4px 4px 7px; } .blq-gvl-3 #blq-nav-main a { background-position: right 12px; } .blq-gvl-3 #blq-nav-main { background-position: 97% 18px; } .blq-morepanel-shown #blq-nav-m a { background-position: 83% -17px} </style> <![endif]--> <!--[if IE 6]> <style type="text/css"> .blq-clearfix {height:1%;} .blq-gvl-3 #blq-mast-home .blq-home {display:none;} .blq-gvl-3 #blq-autosuggest { margin-left:-7px; padding-bottom:8px} .blq-gvl-3 #blq-nav-main { background-position: 96% 17px; } .blq-gvl-3 #blq-mast-home img {visibility: hidden;} .blq-gvl-3 #blq-mast-home a {filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod='crop', src='http://static.bbc.co.uk/frameworks/barlesque/1.8.15//desktop/3/img/blocks/light.png');} .blq-gvl-3 #blq-mast-home a {cursor:pointer} .blq-gvl-3 #blq-mast-home span.blq-home {height:32px; width:107px;} .blq-footer-image-light, .blq-footer-image-dark {width: 68px;height: 21px;display: block;} .blq-footer-image-dark img, .blq-footer-image-light img { visibility: hidden; } .blq-footer-image-light {filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod='scale', src='http://static.bbc.co.uk/frameworks/barlesque/1.8.15//desktop/3/img/blocks/light.png');} .blq-footer-image-dark {filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod='scale', src='http://static.bbc.co.uk/frameworks/barlesque/1.8.15//desktop/3/img/blocks/dark.png');} </style> <script type="text/javascript"> try { document.execCommand("BackgroundImageCache",false,true); } catch(e) {} </script> <![endif]--> <!--[if lt IE 6]> <style> html body #blq-container #blq-foot { background: #4c4c4c; } html body #blq-container #blq-foot a, html body #blq-container #blq-foot p, html body #blq-container #blq-foot li { color: white; } </style> <![endif]--> <script type="text/javascript"> blq.setEnvironment('live'); if (blq.setFlagpole) { blq.setFlagpole('barlesque/nedstat', 'ON'); } if (blq.setLabel) blq.setLabel('searchSuggestion', 'Search'); </script> <!-- BBCDOTCOM ipIsAdvertiseCombined: true journalismVariant: true adsEnabled: false flagpole: not set -->
+ <script type="text/javascript">
+ if(typeof(bbcdotcom) == "undefined") bbcdotcom = {};
+ </script>
+
+
+
+
+ <!-- shared/head -->
+<meta http-equiv="imagetoolbar" content="no" />
+<!--[if !(lt IE 6)]>
+ <link rel="stylesheet" type="text/css" href="httpdisabled://news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/type.css" />
+
+
+ <link rel="stylesheet" type="text/css" media="screen" href="httpdisabled://news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/global.css" />
+
+
+ <link rel="stylesheet" type="text/css" media="print" href="httpdisabled://news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/print.css" />
+
+ <link rel="stylesheet" type="text/css" media="screen and (max-device-width: 976px)" href="httpdisabled://news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/mobile.css" />
+
+
+<![endif]-->
+<!--[if !IE]>-->
+ <link rel="stylesheet" type="text/css" href="../../news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/type.css" />
+
+
+ <link rel="stylesheet" type="text/css" media="screen" href="../../news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/global.css" />
+
+
+ <link rel="stylesheet" type="text/css" media="print" href="../../news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/print.css" />
+
+ <link rel="stylesheet" type="text/css" media="screen and (max-device-width: 976px)" href="../../news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/mobile.css" />
+
+
+<!--<![endif]-->
+<script type="text/javascript">
+/*<![CDATA[*/
+gloaddisableder.loaddisabled(["glow","1","glow.dom"],{onLoad:function(glow){glow.dom.get("html").addClass("blq-js")}});
+gloaddisableder.loaddisabled(["glow","1","glow.dom"],{onLoad:function(glow){glow.ready(function(){if (glow.env.gecko){var gv = glow.env.version.split(".");for (var i=gv.length;i<4;i++){gv[i]=0;}if((gv[0]==1 && gv[1]==9 && gv[2]==0)||(gv[0]==1 && gv[1]<9)||(gv[0]<1)){glow.dom.get("body").addClass("firefox-older-than-3-5");}}});}});
+
+window.disableFacebookSDK=true;
+if (window.location.pathname.indexOf('+')>=0){window.disableFacebookSDK=true;}
+
+/*]]>*/
+</script>
+<script type="text/javascript" src="../../news.bbcimg.co.uk/js/locationservices/locator/v4_0/locator.js"></script>
+
+<script type="text/javascript" src="../../news.bbcimg.co.uk/js/core/3_2/bbc_fmtj.js"></script>
+
+<script type="text/javascript">
+<!--
+ bbc.fmtj.page = {
+ serverTime: 1302299800000,
+ editionToServe: 'us',
+ queryString: null,
+ referrer: null,
+ section: 'front-page',
+ sectionPath: '/Front page',
+ siteName: 'BBC News',
+ siteToServe: 'news',
+ siteVersion: 'cream',
+ storyId: '10263779',
+ assetType: 'index',
+ uri: '/news/',
+ country: 'us',
+ masthead: false,
+ adKeyword: null,
+ templateVersion: 'v1_0'
+ }
+-->
+</script>
+<script type="text/javascript" src="../../news.bbcimg.co.uk/js/common/3_2/bbc_fmtj_common.js"></script>
+
+
+<script type="text/javascript">$useMap({map:"httpdisabled://news.bbcimg.co.uk/js/map/map_0_0_17.js"});</script>
+<script type="text/javascript">$loaddisabledView("0.0",["bbc.fmtj.view"]);</script>
+<script type="text/javascript">$render("livestats-heatmap");</script>
+
+
+<script type="text/javascript" src="../../news.bbcimg.co.uk/js/config/apps/4_5/bbc_fmtj_config.js"></script>
+
+
+<script type="text/javascript" src="../../news.bbc.co.uk/js/app/av/emp/1_1_3_0_0_426652_426614_1/config.sjson@edition=us&amp;site=news&amp;section=%252FFront&#32;page"></script>
+
+<!-- Check for advertising testing -->
+
+<meta name="viewport" content="width = 996" />
+
+
+
+ <!-- shared/head_index -->
+<!-- THESE STYLESHEETS VARY ACCORDING TO PAGE CONTENT -->
+
+<link rel="stylesheet" type="text/css" media="screen" href="../../news.bbcimg.co.uk/view/1_4_9/cream/hi/shared/layout/index.css" />
+
+
+<!-- js index view -->
+<script type="text/javascript">$loaddisabledView("0.0",["bbc.fmtj.view.news.index"]);</script>
+ <!-- #CREAM hi news US head.inc -->
+ <!-- is suitable for ads adding isadvertise ... -->
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<link href="../../news.bbcimg.co.uk/css/screen/shared/19_58/3pt_ads.css" rel="stylesheet" type="text/css" />
+
+<script type="text/javascript">
+/*<![CDATA[*/
+ function syncRoadBlock(src,companionId) {
+ var compidarg;
+ if (arguments.length == 2 && typeof companionId == 'string') {
+ compidarg = companionId;
+ }
+ BBC.adverts.empCompanionResponse(src, compidarg);
+ };
+ pt=new Object();
+ pt.aenab = "yes";
+/*]]>*/
+</script>
+
+<script type="text/javascript">
+ if(typeof BBC === 'undefined') var BBC = {};
+ BBC.adverts = {setZone: function(){}, configure: function(){},void: function(){}, show: function(){}};
+ if(typeof(bbcdotcom) == "undefined") bbcdotcom = {};
+</script>
+
+<script type="text/javascript" src="../../news.bbcimg.co.uk/js/app/bbccom/19_61/bbccom.js"></script>
+
+<script type="text/javascript"><!--
+
+ (function(){
+
+ var fiddleredition = '(none)';
+ var url = '/news/';
+ switch('/news/') {
+ case "/":
+ case "/default.stm":
+ url = (fiddleredition === "domestic") ? "/1/hi/default.stm" : "/2/hi/default.stm";
+ break;
+ case "/sport":
+ case "/sport/":
+ case "/sport/default.stm":
+ url = (fiddleredition === "domestic") ? "/sport1/hi/default.stm" : "/sport2/hi/default.stm";
+ break;
+ };
+
+ var zone = "3pt_zone_file",
+ zoneOverride = false;
+ if(/[?|&]zone=((?!preview)\w*\/*\w+)(&|$)/.test(window.location.search)) {
+ zone = RegExp.$1;
+ };
+
+ if(/[?|&]zone=(http:\/\/.+(\.bbc\.co\.uk\/){1}.*(bbccom){1}.*\.js)(&|$)/.test(window.location.search)) {
+ if (RegExp.$1.indexOf("/../") === -1) {
+ zone = RegExp.$1;
+ zoneOverride = true;
+ };
+ };
+
+ BBC.adverts.setScriptRoot("httpdisabled://news.bbcimg.co.uk/js/app/bbccom/19_61/");
+
+ BBC.adverts.init({
+ domain: "www.bbc.co.uk",
+ location: url,
+ zoneVersion: zone,
+ zoneOverride: zoneOverride,
+ zoneReferrer: document.referrer
+ });
+
+ })();
+
+ if(BBC.adverts.getNewsGvl3()) {
+ void('<script language="JavaScript" src="httpdisabled://news.bbcimg.co.uk/js/app/bbccom/19_47/advert.js"><\/script>');
+ }
+
+--></script>
+
+
+<script type="text/javascript">
+ if(BBC.adverts){
+ BBC.adverts.setPageVersion('(none)');
+ }
+</script>
+
+
+
+
+
+
+
+ <!-- hi/news/head_last.inc -->
+
+<link rel="stylesheet" type="text/css" href="../../news.bbcimg.co.uk/view/1_4_11/cream/hi/news/components/components.css" />
+<link rel="stylesheet" type="text/css" media="screen" href="../../news.bbcimg.co.uk/view/1_4_11/cream/hi/news/skin.css" />
+
+
+<link rel="apple-touch-icon" href="httpdisabled://news.bbcimg.co.uk/img/1_0_1/cream/hi/news/iphone.png"/>
+<script type="text/javascript">
+blq.setLabel('searchSuggestion', 'Search BBC News');
+blq.externalGoTrackingConfig = {
+ ".story-body a":"{path}/ext/story-body/{dir}",
+ ".story-related .related-links a":"{path}/ext/related-links/{dir}",
+ ".story-related .newstracker-list a":"{path}/ext/newstracker/{dir}"
+}
+</script>
+
+ </head>
+
+
+ <!--[if lte IE 6]><body class="news ie disable-wide-advert"><![endif]-->
+ <!--[if IE 7]><body class="news ie7 disable-wide-advert"><![endif]-->
+ <!--[if IE 8]><body class="news ie8 disable-wide-advert"><![endif]-->
+ <!--[if !IE]>--><body class="news disable-wide-advert"><!--<![endif]-->
+ <div class="livestats-web-bug"><img alt="" id="livestats" src="../../stats.bbc.co.uk/o.gif@~RS~s~RS~News~RS~t~RS~HighWeb_Index~RS~i~RS~0~RS~p~RS~99854~RS~a~RS~US~RS~u~RS~%252Fnews%252F~RS~r~RS~(none)~RS~q~RS~~RS~z~RS~40~RS~"/></div>
+
+
+
+
+ <!-- NEDSTAT -->
+
+
+
+
+
+
+
+
+
+
+<!-- NEDSTAT -->
+<!-- Begin iStats 20100118 (UX-CMC 1.1009.3) -->
+<script type="text/javascript">
+// <![CDATA[
+function sitestat(n){var j=document,f=j.location,b="";if(j.cookie.indexOf("st_ux=")!=-1){var k=j.cookie.split(";");var e="st_ux",h=document.domain,a="/";if(typeof ns_!="undefined"&&typeof ns_.ux!="undefined"){e=ns_.ux.cName||e;h=ns_.ux.cDomain||h;a=ns_.ux.cPath||a}for(var g=0,f=k.length;g<f;g++){var m=k[g].indexOf("st_ux=");if(m!=-1){b="&"+unescape(k[g].substring(m+6))}}document.cookie=e+"=; expires="+new Date(new Date().getTime()-60).toGMTString()+"; path="+a+"; domain="+h}ns_pixelUrl=n;n=ns_pixelUrl+"&ns__t="+(new Date().getTime())+"&ns_c="+((j.characterSet)?j.characterSet:j.defaultCharset)+"&ns_ti="+escape(j.title)+b+"&ns_jspageurl="+escape(f&&f.href?f.href:j.URL)+"&ns_referrer="+escape(j.referrer);if(n.length>2000&&n.lastIndexOf("&")){n=n.substring(0,n.lastIndexOf("&")+1)+"ns_cut="+n.substring(n.lastIndexOf("&")+1,n.lastIndexOf("=")).substring(0,40)}(j.images)?new Image().src=n:void('<p><i'+'mg src="'+n+'" height="1" width="1" alt="" /></p>')};
+// ]]>
+</script>
+<noscript><p><img src="../../sa.bbc.co.uk/bbc/bbc/s.gif" height="1" width="1" alt="" /></p></noscript>
+<!-- End iStats (UX-CMC) -->
+<!-- END NEDSTAT -->
+ <div id="blq-global" class="blq-gvl-3"> <div id="blq-pre-mast" xml:lang="en-GB"> <!-- Pre mast --> </div> </div> <div id="blq-container-outer"> <div id="blq-container" class="blq-lang-en-GB blq-dotcom blq-gvl-3"> <div id="blq-container-inner" xml:lang="en-GB"> <div id="blq-acc" class="blq-rst"> <p id="blq-mast-home" class="blq-no-images"><a href="httpdisabled://www.bbc.co.uk/" hreflang="en-GB"> <span class="blq-home">British Broadcasting Corporation</span><img src="../../static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/blocks/light.png" alt="BBC" id="blq-blocks" width="84" height="24" /><span class="blq-span">Home</span></a></p> <p class="blq-hide"><strong><a id="page-top">Accessibility links</a></strong></p> <ul id="blq-acc-links"> <li class="blq-hide"><a href="index.html#main-content">Skip to content</a></li> <li class="blq-hide"><a href="index.html#blq-local-nav">Skip to local navigation</a></li> <li class="blq-hide"><a href="index.html#blq-nav-main">Skip to bbc.co.uk navigation</a></li> <li class="blq-hide"><a href="index.html#blq-search">Skip to bbc.co.uk search</a></li> <li id="blq-acc-help"><a href="httpdisabled://www.bbc.co.uk/help/">Help</a></li> <li class="blq-hide"><a href="httpdisabled://www.bbc.co.uk/accessibility/">Accessibility Help</a></li> </ul> </div> <div id="blq-main" class="blq-clearfix">
+
+
+ <div class="front-page has-ticker">
+ <div id="header-wrapper">
+
+ <h1 id="header">
+ <a rel="index" href="index.html"><img alt="BBC News" src="../../news.bbcimg.co.uk/img/1_0_1/cream/hi/news/news-blocks.gif" /></a>
+ <span class="section-updated">
+ <span class="date">8 April 2011</span>
+<span class="time-text">Last updated at </span><span class="time">17:54 ET</span>
+ </span>
+ </h1>
+
+
+ <div class="bbccom_advert_placeholder">
+ <script type="text/javascript">$render("advert","advert-sponsor-section");</script>
+ </div>
+ <script type="text/javascript">$render("advert-post-script-loaddisabled");</script>
+
+ <a href="httpdisabled://feeds.bbci.co.uk/news/rss.xml" id="rss-alternative">
+ RSS<span class="gvl3-icon gvl3-icon-rss"> feed</span>
+ </a>
+
+
+ <div id="blq-local-nav">
+ <ul id="nav" class="nav">
+
+
+ <li class="first-child selected"><a href="index.html">Home</a></li>
+
+
+ <li><a href="httpdisabled://www.bbc.co.uk/news/world/us_and_canada/">US &amp; Canada</a></li>
+
+
+ <li><a href="httpdisabled://www.bbc.co.uk/news/world/latin_america/">Latin America</a></li>
+
+
+ <li><a href="httpdisabled://www.bbc.co.uk/news/uk/">UK</a></li>
+
+
+ <li><a href="httpdisabled://www.bbc.co.uk/news/world/africa/">Africa</a></li>
+
+
+ <li><a href="httpdisabled://www.bbc.co.uk/news/world/asia_pacific/">Asia-Pac</a></li>
+
+
+ <li><a href="httpdisabled://www.bbc.co.uk/news/world/europe/">Europe</a></li>
+
+
+ <li><a href="httpdisabled://www.bbc.co.uk/news/world/middle_east/">Mid-East</a></li>
+
+
+ <li><a href="httpdisabled://www.bbc.co.uk/news/world/south_asia/">South Asia</a></li>
+
+
+ <li><a href="httpdisabled://www.bbc.co.uk/news/business/">Business</a></li>
+
+
+ <li><a href="httpdisabled://www.bbc.co.uk/news/health/">Health</a></li>
+
+
+ <li><a href="httpdisabled://www.bbc.co.uk/news/science_and_environment/">Sci/Environment</a></li>
+
+
+ <li><a href="httpdisabled://www.bbc.co.uk/news/technology/">Tech</a></li>
+
+
+ <li><a href="httpdisabled://www.bbc.co.uk/news/entertainment_and_arts/">Entertainment</a></li>
+
+
+ <li><a href="httpdisabled://www.bbc.co.uk/news/10462520">Video</a></li>
+ </ul>
+
+ <ul id="sub-nav" class="nav">
+
+ <li class="first-child "><a href="httpdisabled://www.bbc.co.uk/news/in_pictures/">In Pictures</a></li>
+
+ <li><a href="httpdisabled://www.bbc.co.uk/news/also_in_the_news/">Also in the News</a></li>
+
+ <li><a href="httpdisabled://www.bbc.co.uk/blogs/theeditors/">Editors' Blog</a></li>
+
+ <li><a href="httpdisabled://www.bbc.co.uk/news/have_your_say/">Have Your Say</a></li>
+
+ <li><a href="httpdisabled://www.bbc.co.uk/news/world_radio_and_tv/">World Radio and TV</a></li>
+
+ <li><a href="httpdisabled://www.bbc.co.uk/news/special_reports/">Special Reports</a></li>
+
+ <li><a href="httpdisabled://www.bbc.co.uk/news/uk-11767495">UK Royal Wedding</a></li>
+ </ul>
+ </div>
+
+
+ </div>
+ <!-- START CPS_SITE CLASS: us -->
+ <div id="content-wrapper" class="us">
+
+ <div class="advert">
+
+ <div class="bbccom_advert_placeholder">
+ <script type="text/javascript">$render("advert","advert-leaderboard");</script>
+ </div>
+ <script type="text/javascript">$render("advert-post-script-loaddisabled");</script>
+
+ </div>
+
+ <!-- START CPS_SITE CLASS: index -->
+ <div id="main-content" class="index blq-clearfix">
+
+
+
+<div id="full-width" class="container-full-width">
+ <div id="ticker" class="include-only ticker">
+
+<div id="tickerHolder"></div>
+
+<noscript>
+ <div class="ticker">
+ <h2 class="hidden"> Latest Stories </h2>
+ <ul class="tickerItem">
+ <li class="tickerEntry">
+ <span class="tickerPrompt">LATEST</span>
+ <span class="tickerHeadline">
+
+
+
+
+
+
+
+
+
+
+
+
+
+<span>
+ <a class="story" href="httpdisabled://www.bbc.co.uk/news/world-latin-america-13021002">Veteran Cuban anti-communist militant Luis Posada Carriles is acquitted of lying to US immigration officials</a>
+ </span>
+
+ </span>
+ </li>
+ <li class="tickerEntry">
+ <span class="tickerPrompt">LATEST</span>
+ <span class="tickerHeadline">
+ Forces loyal to Ivory Coast's encumbent President Gbagbo expand the area they hold in Abidjan, UN says
+ </span>
+ </li>
+ <li class="tickerEntry">
+ <span class="tickerPrompt">LATEST</span>
+ <span class="tickerHeadline">
+
+
+
+
+
+
+
+
+
+
+
+
+
+<span>
+ <a class="story" href="httpdisabled://www.bbc.co.uk/news/world-africa-13021093">At least six people killed in explosion at local election office in Nigeria on eve of vote</a>
+ </span>
+
+ </span>
+ </li>
+ <li class="tickerEntry">
+ <span class="tickerPrompt">LATEST</span>
+ <span class="tickerHeadline">
+
+
+
+
+
+
+
+
+
+
+
+
+
+<span>
+ <a class="story" href="httpdisabled://www.bbc.co.uk/news/world-africa-13010942">Kenya&#039;s Deputy Prime Minister Uhuru Kenyatta appears at the International Criminal Court in The Hague</a>
+ </span>
+
+ </span>
+ </li>
+ <li class="tickerEntry">
+ <span class="tickerPrompt">LATEST</span>
+ <span class="tickerHeadline">
+
+
+
+
+
+
+
+
+
+
+
+
+
+<span>
+ <a class="story" href="httpdisabled://www.bbc.co.uk/news/world-middle-east-13017502">At least three people killed during protests in Yemen, doctors say</a>
+ </span>
+
+ </span>
+ </li>
+ </ul>
+ </div>
+</noscript>
+
+<script type="text/javascript">$render("ticker","tickerHolder",{"updatePeriod":30,"dataSource":"/news/10284448/ticker.sjson"});</script>
+
+
+</div>
+<script type="text/javascript">$render("ticker","ticker");</script>
+
+</div>
+<script type="text/javascript">$render("container-full-width","full-width");</script>
+
+<div id="now" class="container-now">
+
+<div id="container-top-stories-with-splash" class="container-top-stories">
+
+
+
+
+
+ <div id="top-story" class="large-image">
+
+
+ <h2 class="top-story-header ">
+ <a class="story" rel="published-1302264742438" href="httpdisabled://www.bbc.co.uk/news/world-us-canada-13015909">Blame game as US shutdown looms<img src="../../news.bbcimg.co.uk/media/images/52072000/jpg/_52072075_52072074.jpg" alt="John Boehner" /></a>
+ </h2>
+
+
+ <p>Republicans and Democrats offer starkly different reasons for an impasse in talks on US spending cuts as the clock ticks down towards a government shutdown. </p>
+ <ul class="see-also">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<li class="has-icon-boxedlive first-child column-1">
+ <a class="story is-live" rel="published-1301125209864" href="httpdisabled://www.bbc.co.uk/news/world-middle-east-12776418">Battle over US budget<span class="gvl3-icon gvl3-icon-boxedlive"> Live</span></a>
+ </li>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<li class=" column-1">
+ <a class="story" href="httpdisabled://www.bbc.co.uk/blogs/thereporters/markmardell/2011/04/beyond_the_brink.html">Mardell: Beyond the brink</a>
+ </li>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<li class=" column-1">
+ <a class="story" rel="published-1301605252225" href="httpdisabled://www.bbc.co.uk/news/world-us-canada-12571718">What is a &#039;government shutdown&#039;?</a>
+ </li>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<li class="has-icon-boxedwatch column-2">
+ <a class="story" rel="published-1302287373647" href="httpdisabled://www.bbc.co.uk/news/world-us-canada-13021011">Boehner: battle over spending<span class="gvl3-icon gvl3-icon-boxedwatch"> Watch</span></a>
+ </li>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<li class="has-icon-boxedwatch column-2">
+ <a class="story" rel="published-1302293683039" href="httpdisabled://www.bbc.co.uk/news/world-us-canada-13020801">Federal workers stage protest<span class="gvl3-icon gvl3-icon-boxedwatch"> Watch</span></a>
+ </li>
+ </ul>
+ <hr />
+ </div>
+ <script type="text/javascript">$render("top-story","top-story");</script>
+
+
+
+
+
+
+
+ <div id="second-story" class="secondary-top-story">
+
+
+ <div class="medium-image">
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h2 class=" secondary-story-header">
+ <a class="story" rel="published-1302267266893" href="httpdisabled://www.bbc.co.uk/news/world-13016843"><img src="../../news.bbcimg.co.uk/media/images/52074000/jpg/_52074033_jex_1013006_de27.jpg" alt="Protest in Deraa filmed on mobile phone" />Syrian city hit by deadly clashes</a>
+ </h2>
+
+ <p>At least 23 demonstrators are reported killed in anti-government rallies in the Syrian city of Deraa, as renewed protests spread across the country. </p>
+
+ <ul class="see-also">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<li class="has-icon-boxedwatch first-child column-1">
+ <a class="story" rel="published-1302271714591" href="httpdisabled://www.bbc.co.uk/news/world-middle-east-13016690">&#039;Protesters shot&#039; in Syria rally<span class="gvl3-icon gvl3-icon-boxedwatch"> Watch</span></a>
+
+ </li>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<li class=" column-2">
+ <a class="story" rel="published-1301097764284" href="httpdisabled://www.bbc.co.uk/news/world-middle-east-12868719">How secure is Bashar?</a>
+
+ </li>
+ </ul>
+ </div>
+ </div>
+<script type="text/javascript">$render("secondary-top-story","second-story");</script>
+
+
+
+
+
+
+
+
+ <div id="third-story" class="secondary-top-story">
+
+
+ <div class="medium-image">
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h2 class=" secondary-story-header">
+ <a class="story" rel="published-1302265226937" href="httpdisabled://www.bbc.co.uk/news/uk-england-hampshire-13014640"><img src="../../news.bbcimg.co.uk/media/images/52078000/jpg/_52078134_astuteshoot.jpg" alt="Police boarding HMS Astute" />Shooting attack on UK nuclear sub</a>
+ </h2>
+
+ <p>One person is killed and another is in a life-threatening condition after a shooting on board a British nuclear submarine. </p>
+
+ <ul class="see-also">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<li class="has-icon-boxedwatch first-child column-1">
+ <a class="story" rel="published-1302291295018" href="httpdisabled://www.bbc.co.uk/news/uk-13020498">&#039;Sub security not breached&#039;<span class="gvl3-icon gvl3-icon-boxedwatch"> Watch</span></a>
+
+ </li>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<li class="has-icon-boxedwatch column-2">
+ <a class="story" rel="published-1302274921068" href="httpdisabled://www.bbc.co.uk/news/uk-13016791">Aerial footage of scene<span class="gvl3-icon gvl3-icon-boxedwatch"> Watch</span></a>
+
+ </li>
+ </ul>
+ </div>
+ </div>
+<script type="text/javascript">$render("secondary-top-story","third-story");</script>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<div id="other-top-stories" class="other-top-stories">
+
+ <ul class="other-top-stories-stories">
+
+
+
+
+
+ <li class="column-1 with-summary first-child">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h2>
+ <a class="story" rel="published-1302263788014" href="httpdisabled://www.bbc.co.uk/news/world-middle-east-13014046">Israel strikes &#039;kill nine Gazans&#039;</a>
+ </h2>
+
+ <p>At least nine Palestinians are killed by Israeli air strikes in Gaza, doctors say, amid fresh cross-border exchanges. </p>
+ </li>
+
+
+
+
+ <li class="column-1 with-summary ">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h2>
+ <a class="story" rel="published-1302264028934" href="httpdisabled://www.bbc.co.uk/news/science-environment-13011073">New York &#039;at risk&#039; as seas rise </a>
+ </h2>
+
+ <p>New York is set to be a major loser as a result of future sea level rise, according to a new forecast model. </p>
+ </li>
+
+
+
+
+ <li class="column-2 ">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h2>
+ <a class="story" rel="published-1302251341550" href="httpdisabled://www.bbc.co.uk/news/world-africa-13010170">Nato &#039;regrets&#039; Libya loss of life</a>
+
+ </h2>
+
+ </li>
+
+
+
+
+ <li class="column-2 ">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h2>
+ <a class="story" rel="published-1302255270616" href="httpdisabled://www.bbc.co.uk/news/world-africa-13013082">More bodies found in Ivory Coast</a>
+
+ </h2>
+
+ </li>
+
+
+
+
+ <li class="column-2 ">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h2>
+ <a class="story" rel="published-1302219866233" href="httpdisabled://www.bbc.co.uk/news/health-12999000">Cancer &#039;fuelled by extra drinks&#039;</a>
+
+ </h2>
+
+ </li>
+
+
+
+
+ <li class="column-2 ">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h2>
+ <a class="story" rel="published-1302287088358" href="httpdisabled://www.bbc.co.uk/news/13017141">US criticises China rights record</a>
+
+ </h2>
+
+ </li>
+
+
+
+
+ <li class="column-2 ">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h2>
+ <a class="story" rel="published-1302243563772" href="httpdisabled://www.bbc.co.uk/news/business-13009669">Mid-May target for Portugal aid</a>
+
+ </h2>
+
+ </li>
+
+
+
+
+ <li class="column-2 ">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h2>
+ <a class="story" rel="published-1302271310277" href="httpdisabled://www.bbc.co.uk/news/uk-13014161">Murdoch paper to admit hacking liability</a>
+
+ </h2>
+
+ </li>
+
+
+
+
+ <li class="column-2 ">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h2>
+ <a class="story" rel="published-1302230850633" href="httpdisabled://www.bbc.co.uk/news/business-12998807">Oil prices highest for 32 months</a>
+
+ </h2>
+
+ </li>
+ </ul>
+</div>
+<script type="text/javascript">$render("other-top-stories","other-top-stories");</script>
+
+</div>
+<script type="text/javascript">$render("container-top-stories","container-top-stories-with-splash");</script>
+
+<div id="compact-more-from-bbc-news" class="container-digest-grid">
+
+ <div class="digest-wrapper digest-grid">
+
+
+ <div class="digest-unit first-child">
+ <h2 class="heading-16"><a href="httpdisabled://www.bbc.co.uk/news/business/">Business</a></h2>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<div class=" stacked-144">
+ <a class="headline-anchor" rel="published-1302296354846" href="httpdisabled://www.bbc.co.uk/news/business-13013659"><img src="../../news.bbcimg.co.uk/media/images/50906000/jpg/_50906324_006353309-2.jpg" alt="Canary Wharf" /><span class="headline heading-13">Banks report to back &#039;firewalls&#039;</span></a>
+ <br class="ie-clear" />
+</div>
+
+ </div>
+
+
+
+
+ <div class="digest-unit ">
+ <h2 class="heading-16"><a href="httpdisabled://www.bbc.co.uk/news/technology/">Technology</a></h2>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<div class=" stacked-144">
+ <a class="headline-anchor" rel="published-1302261593601" href="httpdisabled://www.bbc.co.uk/news/technology-13012041"><img src="../../news.bbcimg.co.uk/media/images/52065000/jpg/_52065323_aionscreenshot,ncsoft.jpg" alt="Aion screenshot, NCSoft" /><span class="headline heading-13">Virtual sales aid poorer nations</span></a>
+ <br class="ie-clear" />
+</div>
+
+ </div>
+
+
+
+
+ <div class="digest-unit ">
+ <h2 class="heading-16"><a href="httpdisabled://www.bbc.co.uk/news/entertainment_and_arts/">Entertainment/Arts</a></h2>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<div class=" stacked-144">
+ <a class="headline-anchor" rel="published-1302258278265" href="httpdisabled://www.bbc.co.uk/news/entertainment-arts-13010131"><img src="../../news.bbcimg.co.uk/media/images/52075000/jpg/_52075786_stewart_getty304.jpg" alt="Sir Patrick Stewart " /><span class="headline heading-13">Actors protest over art funding</span></a>
+ <br class="ie-clear" />
+</div>
+
+ </div>
+
+
+
+
+ <div class="digest-unit ">
+ <h2 class="heading-16"><a href="httpdisabled://www.bbc.co.uk/news/science_and_environment/">Science/Env</a></h2>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<div class=" stacked-144">
+ <a class="headline-anchor" rel="published-1302282838772" href="httpdisabled://www.bbc.co.uk/news/science-environment-12990213"><img src="../../news.bbcimg.co.uk/media/images/52064000/jpg/_52064940_94471941.jpg" alt="VU meter (Thinkstock)" /><span class="headline heading-13">Strangely silent star system seen</span></a>
+ <br class="ie-clear" />
+</div>
+
+ </div>
+
+
+ </div>
+ <div class="digest-wrapper digest-grid">
+
+
+ <div class="digest-unit first-child">
+ <h2 class="heading-16"><a href="httpdisabled://www.bbc.co.uk/news/health/">Health</a></h2>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<div class=" stacked-144">
+ <a class="headline-anchor" rel="published-1302284644603" href="httpdisabled://www.bbc.co.uk/news/uk-england-beds-bucks-herts-13020208"><img src="../../news.bbcimg.co.uk/media/images/52077000/jpg/_52077792_52077791.jpg" alt="Some of the fake drugs seized" /><span class="headline heading-13">Man jailed over £4.7m fake drugs</span></a>
+ <br class="ie-clear" />
+</div>
+
+ </div>
+
+
+
+
+ <div class="digest-unit ">
+ <h2 class="heading-16"><a href="httpdisabled://www.bbc.co.uk/news/have_your_say/">Have Your Say</a></h2>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<div class=" stacked-144">
+ <a class="headline-anchor" rel="published-1301913456280" href="httpdisabled://www.bbc.co.uk/news/world-africa-12957580"><img src="../../news.bbcimg.co.uk/media/images/51990000/jpg/_51990536_011672235-1.jpg" alt="Forces loyal to Alassane Ouattara about advance on Abidjan on 1 April 2011" /><span class="headline heading-13">Ivory Coast eyewitness: Panic in Abidjan</span></a>
+ <br class="ie-clear" />
+</div>
+
+ </div>
+
+
+
+
+ <div class="digest-unit ">
+ <h2 class="heading-16"><a href="httpdisabled://www.bbc.co.uk/news/magazine/">Magazine</a></h2>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<div class=" stacked-144">
+ <a class="headline-anchor" rel="published-1302193686143" href="httpdisabled://www.bbc.co.uk/news/magazine-12998204"><img src="../../news.bbcimg.co.uk/media/images/52054000/jpg/_52054442_mj.144.jpg" alt="Michael Jackson" /><span class="headline heading-13">Quiz of the week&#039;s news</span></a>
+ <br class="ie-clear" />
+</div>
+
+ </div>
+
+
+
+
+ <div class="digest-unit ">
+ <h2 class="heading-16"><a href="httpdisabled://www.bbc.co.uk/news/also_in_the_news/">Also In The News</a></h2>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<div class=" stacked-144">
+ <a class="headline-anchor" rel="published-1302284283617" href="httpdisabled://www.bbc.co.uk/news/world-south-asia-13015768"><img src="../../news.bbcimg.co.uk/media/images/52073000/jpg/_52073406_008253948-1.jpg" alt="Sesame Street characters pose under a &quot;123 Sesame Street&quot; sign, November 2009 in New York City" /><span class="headline heading-13">Sesame Street heads to Pakistan</span></a>
+ <br class="ie-clear" />
+</div>
+
+ </div>
+
+
+ </div>
+ </div>
+<script type="text/javascript">$render("container-compact-section-digests","compact-more-from-bbc-news");</script>
+
+
+
+<div id="featured-other-site---bbc-sport" class="container-featured-other-site">
+ <h2 class="container-featured-other-site-heading"><a href="httpdisabled://www.bbc.co.uk/sport2/hi/default.stm">Sport</a></h2>
+
+<div id="featured-site-top-stories---bbc-sport" class="featured-site-top-stories">
+
+
+ <ul>
+
+
+
+
+ <li class=" medium-image first-child">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h3>
+ <a class="story" rel="published-1302262215000" href="httpdisabled://www.bbc.co.uk/sport2/hi/golf/9451236.stm"><img src="../../news.bbcimg.co.uk/media/images/52015000/jpg/_52015349_flag_reuters_144.jpg" alt="Masters flag" />Live - 2011 Masters day two</a>
+ </h3>
+
+ <p>Rory McIlroy leads the Masters by two shots over Australian Jason Day with the second round in full swing at Augusta National.</p>
+
+
+
+
+ <hr />
+ </li>
+
+
+
+
+
+
+ <li class="column-1 ">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h3>
+ <a class="story" rel="published-1302289407000" href="httpdisabled://www.bbc.co.uk/sport2/hi/tennis/9452020.stm">Djokovic ruled out of Monte Carlo</a>
+ </h3>
+
+
+
+
+
+ </li>
+
+
+
+
+
+
+ <li class="column-2 ">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h3>
+ <a class="story" rel="published-1302260853390" href="httpdisabled://www.bbc.co.uk/sport2/hi/football/13013741.stm">Rooney ref pressurised - Ferguson</a>
+ </h3>
+
+
+
+
+
+ </li>
+
+
+
+
+
+
+ <li class="column-1 ">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h3>
+ <a class="story" rel="published-1302190310000" href="httpdisabled://www.bbc.co.uk/sport2/hi/rugby_league/9447492.stm">Wigan 28-47 Catalan Dragons</a>
+ </h3>
+
+
+
+
+
+ </li>
+
+
+
+
+
+
+ <li class="column-2 ">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h3>
+ <a class="story" rel="published-1302169822000" href="httpdisabled://www.bbc.co.uk/sport2/hi/rugby_league/9447490.stm">Huddersfield 29-10 Warrington</a>
+ </h3>
+
+
+
+
+
+ </li>
+
+ </ul>
+</div>
+<script type="text/javascript">$render("featured-site-top-stories","featured-site-top-stories---bbc-sport");</script>
+
+</div>
+<script type="text/javascript">$render("container-featured-other-site","featured-other-site---bbc-sport");</script>
+ <div id="geographic-news-digests-no-tabs" class="container-digest-grid">
+
+<div id="compact-geographic-section-digests" class="container-block-grid">
+ <h2 class="heading-24">World News</h2>
+
+
+ <div class="digest-wrapper digest-grid-list-column">
+
+ <div class="digest-unit first-child">
+ <h3 class="heading-16"><a href="httpdisabled://www.bbc.co.uk/news/world/us_and_canada/">US &amp; Canada</a></h3>
+
+
+
+
+
+
+
+
+
+
+
+
+
+<div class=" standard-no-image">
+ <a class="headline-anchor" rel="published-1302257749429" href="httpdisabled://www.bbc.co.uk/news/technology-13010760">US developing activist technology</span></a>
+ <br class="ie-clear" />
+</div>
+
+ </div>
+
+
+ <div class="digest-unit ">
+ <h3 class="heading-16"><a href="httpdisabled://www.bbc.co.uk/news/world/latin_america/">Latin America</a></h3>
+
+
+
+
+
+
+
+
+
+
+
+
+
+<div class=" standard-no-image">
+ <a class="headline-anchor" rel="published-1302291982657" href="httpdisabled://www.bbc.co.uk/news/world-latin-america-13020999">Brazil mourns murdered Rio pupils</span></a>
+ <br class="ie-clear" />
+</div>
+
+ </div>
+
+
+ <div class="digest-unit ">
+ <h3 class="heading-16"><a href="httpdisabled://www.bbc.co.uk/news/world/africa/">Africa</a></h3>
+
+
+
+
+
+
+
+
+
+
+
+
+
+<div class=" standard-no-image">
+ <a class="headline-anchor" rel="published-1302289064895" href="httpdisabled://www.bbc.co.uk/news/world-africa-13021093">Bomb hits Nigeria election office</span></a>
+ <br class="ie-clear" />
+</div>
+
+ </div>
+
+
+ <div class="digest-unit ">
+ <h3 class="heading-16"><a href="httpdisabled://www.bbc.co.uk/news/world/asia_pacific/">Asia-Pacific</a></h3>
+
+
+
+
+
+
+
+
+
+
+
+
+
+<div class=" standard-no-image">
+ <a class="headline-anchor" rel="published-1302187507701" href="httpdisabled://www.bbc.co.uk/news/world-asia-pacific-13005110">Three dead after Japan aftershock</span></a>
+ <br class="ie-clear" />
+</div>
+
+ </div>
+
+ </div>
+ <div class="digest-wrapper digest-grid-list-column">
+
+ <div class="digest-unit first-child">
+ <h3 class="heading-16"><a href="httpdisabled://www.bbc.co.uk/news/world/europe/">Europe</a></h3>
+
+
+
+
+
+
+
+
+
+
+
+
+
+<div class=" standard-no-image">
+ <a class="headline-anchor" rel="published-1302257160809" href="httpdisabled://www.bbc.co.uk/news/world-europe-13011540">Medvedev denounces cyber-attack</span></a>
+ <br class="ie-clear" />
+</div>
+
+ </div>
+
+
+ <div class="digest-unit ">
+ <h3 class="heading-16"><a href="httpdisabled://www.bbc.co.uk/news/world/middle_east/">Middle East</a></h3>
+
+
+
+
+
+
+
+
+
+
+
+
+
+<div class=" standard-no-image">
+ <a class="headline-anchor" rel="published-1302275084283" href="httpdisabled://www.bbc.co.uk/news/world-middle-east-13017502">Yemeni forces fire on protesters</span></a>
+ <br class="ie-clear" />
+</div>
+
+ </div>
+
+
+ <div class="digest-unit ">
+ <h3 class="heading-16"><a href="httpdisabled://www.bbc.co.uk/news/world/south_asia/">South Asia</a></h3>
+
+
+
+
+
+
+
+
+
+
+
+
+
+<div class=" standard-no-image">
+ <a class="headline-anchor" rel="published-1302237618867" href="httpdisabled://www.bbc.co.uk/news/world-south-asia-13009198">India anti-graft protests urged</span></a>
+ <br class="ie-clear" />
+</div>
+
+ </div>
+
+ </div>
+ </div>
+
+
+
+</div>
+<script type="text/javascript">$render("container-digest-grid","geographic-news-digests-no-tabs");</script>
+
+
+<div id="digest-from-a-single-section---container" class="container-single-section-digest">
+ <h2 class="container-single-section-digest-heading"><a href="httpdisabled://www.bbc.co.uk/news/uk/">UK News</a></h2>
+
+
+ <div id="geo-uk-digest" class="geo-digest-region-2">
+<script type="text/javascript">$render("personalisation-panel","geo-uk-digest",{panelId:"personalisation"});</script>
+</div>
+
+
+
+ <div id="single-section-digest" class="container-digest-grid">
+ <ul class="digest-wrapper digest-grid-text-only">
+
+
+
+
+
+
+
+
+
+
+
+
+
+<li class=" digest-unit standard-no-image first-child">
+ <a class="headline-anchor" rel="published-1302263207369" href="httpdisabled://www.bbc.co.uk/news/uk-politics-13013250">Osborne downplays bailout impact</span></a>
+ <br class="ie-clear" />
+</li>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<li class=" digest-unit standard-no-image ">
+ <a class="headline-anchor" rel="published-1302290270060" href="httpdisabled://www.bbc.co.uk/news/business-13020668">Pinewood studios gets £87.8m bid</span></a>
+ <br class="ie-clear" />
+</li>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<li class=" digest-unit standard-no-image ">
+ <a class="headline-anchor" rel="published-1302281869114" href="httpdisabled://www.bbc.co.uk/news/uk-northern-ireland-13020381">Third man arrested over NI murder</span></a>
+ <br class="ie-clear" />
+</li>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<li class=" digest-unit standard-no-image ">
+ <a class="headline-anchor" rel="published-1302270359690" href="httpdisabled://www.bbc.co.uk/news/uk-england-13015824">Court rejects deportation appeal</span></a>
+ <br class="ie-clear" />
+</li>
+
+
+ </ul>
+</div>
+
+</div>
+<script type="text/javascript">$render("container-single-section-digest","digest-from-a-single-section---container");</script>
+
+<div id="special-reports" class="special-reports-component">
+
+ <h2 class="special-reports-header">
+ Special Reports
+ </h2>
+
+
+ <div class="special-reports-wrapper">
+
+
+
+
+ <div class="top-report">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h3>
+ <a class="story" rel="published-1289925174387" href="httpdisabled://www.bbc.co.uk/news/uk-11767495"><img src="../../news.bbcimg.co.uk/media/images/50112000/jpg/_50112416_010706746-1.jpg" alt="Kate Middleton&#039;s engagement ring" />Britain&#039;s Royal Wedding</a>
+ </h3>
+ <p>News and features on the royal engagement</p>
+
+
+ <div class="bbccom_advert_placeholder">
+ <script type="text/javascript">$render("advert","advert-sponsor-module","special-reports","britains-royal-wedding");</script>
+ </div>
+ <script type="text/javascript">$render("advert-post-script-loaddisabled");</script>
+
+ </div>
+
+ <div class="more-special-reports">
+ <h3 class="more">More Special Reports:</h3>
+ <ul>
+ <li>
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h4>
+ <a class="story" rel="published-1300297841012" href="httpdisabled://www.bbc.co.uk/news/world-asia-pacific-12711226">Japan earthquake</a>
+ </h4>
+ </li>
+
+
+ <li>
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h4>
+ <a class="story" rel="published-1297967084658" href="httpdisabled://www.bbc.co.uk/news/world-middle-east-12480844">Libya crisis</a>
+ </h4>
+ </li>
+
+ </ul>
+ </div>
+ </div>
+
+</div>
+<script type="text/javascript">$render("special-reports","special-reports");</script>
+
+
+
+<div id="featured-other-site---democracy-live" class="container-featured-other-site">
+ <div id="featured-site-include---democracy-live" class="include-only featured-site-include">
+
+
+
+<div id="from-bbc-travel" class="container-other-site-promotion ">
+ <h2 class="other-site-content-header"><a href="httpdisabled://www.bbc.com/travel/">From BBC Travel</a></h2>
+ <div id="other-site-content-1" class="other-site-content first-group">
+ <ul class="stacked-content-group stacked-overlay-other-site-promotion ">
+ <li class="first-other-promo">
+ <h3>
+ <a class="story" href="httpdisabled://www.bbc.com/travel/feature/20110407-amsterdam-on-wheels">
+ <span class="overlay">
+ <strong class="headline">Amsterdam on wheels</strong>
+ <span class="summary">Make like a local in the bike capital of Europe</span>
+ </span>
+ <img alt="Peddling through Amsterdam" src="../../static.bbc.co.uk/wwtravel/img/ic/304-170/130203147123329681316_1.jpg" />
+ </a>
+ </h3>
+ </li>
+ </ul>
+ </div>
+ <div id="other-site-content-2" class="other-site-content ">
+ <ul class="stacked-content-group stacked-overlay-other-site-promotion ">
+ <li class="first-other-promo">
+ <h3>
+ <a class="story" href="httpdisabled://www.bbc.com/travel/feature/20110324-40-free-attractions-in-new-york-city">
+ <span class="overlay">
+ <strong class="headline">Free attractions in New York</strong>
+ <span class="summary">Forty things to do without handing over a cent</span>
+ </span>
+ <img alt="City Hall, New York" src="../../static.bbc.co.uk/wwtravel/img/ic/304-170/1300928948164652012_1.jpg" />
+ </a>
+ </h3>
+ </li>
+ </ul>
+ </div>
+</div>
+<script type="text/javascript">$render("container-other-site-promotion","from-bbc-travel");</script>
+</div>
+<script type="text/javascript">$render("featured-site-include---democracy-live","featured-site-include---democracy-live");</script>
+</div>
+<script type="text/javascript">$render("container-featured-other-site","featured-other-site---democracy-live");</script>
+
+<div class="languages">
+ <h3><a href="httpdisabled://www.bbc.co.uk/worldservice/">BBC World Service</a></h3>
+ <h4>News and analysis in 32 languages</h4>
+ <ul>
+ <li><a href="httpdisabled://www.bbc.co.uk/arabic"><span class="lang-with-image">Arabic</span> <span class="lang-sprite lang-arabic">عربي</span></a></li>
+ <li><a href="httpdisabled://www.bbc.co.uk/chinese"><span class="lang-with-image">Chinese</span> <span class="lang-sprite lang-chinese">&#20013;&#25991;</span></a></li>
+ </ul>
+ <h5>Languages continued (2 of 4)</h5>
+ <ul>
+ <li><a href="httpdisabled://www.bbcpersian.com/"><span class="lang-with-image">Persian</span> <span class="lang-sprite lang-persian">&#x0641;&#x0627;&#x0631;&#x0633;&#x06cc;</span></a></li>
+ <li><a href="httpdisabled://www.bbc.co.uk/portuguese"><span class="lang-with-image">Portuguese</span> <span class="lang-sprite lang-brasil">Brasil</span></a></li>
+ </ul>
+ <h5>Languages continued (3 of 4)</h5>
+ <ul>
+ <li><a href="httpdisabled://www.bbc.co.uk/russian"><span class="lang-with-image">Russian</span> <span class="lang-sprite lang-russian">&#1056;&#1091;&#1089;&#1089;&#1082;&#1080;&#1081;</span></a></li>
+ <li><a href="httpdisabled://www.bbcmundo.com/"><span class="lang-with-image">Spanish</span> <span class="lang-sprite lang-mundo">Mundo</span></a></li>
+ </ul>
+ <h5>Languages continued (4 of 4)</h5>
+ <ul>
+ <li><a href="httpdisabled://www.bbcurdu.com/"><span class="lang-with-image">Urdu</span> <span class="lang-sprite lang-urdu">&#x0627;&#x0631;&#x062f;&#x0648;</span></a></li>
+ <li><a href="httpdisabled://www.bbc.co.uk/vietnamese"><span class="lang-with-image">Vietnamese</span> <span class="lang-sprite lang-vietnamese">Ti&#x1ebf;ng Vi&#x1ec7;t</span></a></li>
+ </ul>
+ <div class="languages-footer">
+ <a href="httpdisabled://www.bbc.co.uk/worldservice/languages/">More languages</a>
+ </div>
+</div>
+
+
+</div>
+<script type="text/javascript">$render("container-now","now");</script>
+
+<div id="best" class="container-best">
+
+<div id="promo-best" class="container-promo-best">
+
+<div class="bbccom_advert_placeholder">
+ <script type="text/javascript">$render("advert","advert-mpu-high");</script>
+</div>
+<script type="text/javascript">$render("advert-post-script-loaddisabled");</script>
+
+
+<div id="av-best" class="container-av-best">
+
+
+ <div id="av-stories-best" class="av-stories-best">
+
+ <h2 class="av-best-header">Watch/Listen</h2>
+
+ <div class="list-wrapper">
+
+
+ <ul class="av-best-carousel carousel ">
+
+
+
+
+
+
+
+
+
+
+
+
+
+<li class=" first-child">
+ <a class="story" rel="published-1302294075339" href="httpdisabled://www.bbc.co.uk/news/world-africa-13020909"><img src="../../news.bbcimg.co.uk/media/images/52078000/jpg/_52078945_jex_1013338_de27-1.jpg" alt="Election posters" />Doubt over fair Nigerian election<span class="gvl3-icon-wrapper"><span class="gvl3-icon gvl3-icon-invert-watch"> Watch</span></span></a>
+ </li>
+
+
+
+
+
+
+
+
+
+
+
+
+
+<li>
+ <a class="story" rel="published-1302263035778" href="httpdisabled://www.bbc.co.uk/news/technology-13012083"><img src="../../news.bbcimg.co.uk/media/images/52068000/jpg/_52068942_jex_1012675_de09-1.jpg" alt="Dutch &#039;super bus&#039;" />Electric &#039;super bus&#039; reaches 250km/h<span class="gvl3-icon-wrapper"><span class="gvl3-icon gvl3-icon-invert-watch"> Watch</span></span></a>
+ </li>
+
+
+
+
+
+
+
+
+
+
+
+
+
+<li>
+ <a class="story" rel="published-1302284070065" href="httpdisabled://www.bbc.co.uk/news/world-south-asia-13020443"><img src="../../news.bbcimg.co.uk/media/images/52077000/jpg/_52077604_jex_1013246_de27-1.jpg" alt="Security forces" />Cleric killed by bomb in Kashmir<span class="gvl3-icon-wrapper"><span class="gvl3-icon gvl3-icon-invert-watch"> Watch</span></span></a>
+ </li>
+
+
+
+
+
+
+
+
+
+
+
+
+
+<li>
+ <a class="story" rel="published-1302291873011" href="httpdisabled://www.bbc.co.uk/news/world-middle-east-13021417"><img src="../../news.bbcimg.co.uk/media/images/52079000/jpg/_52079170_jex_1013354_de30-1.jpg" alt="Amateur video appearing to show protesters in Banias" />Deadly protests sweep across Syria<span class="gvl3-icon-wrapper"><span class="gvl3-icon gvl3-icon-invert-watch"> Watch</span></span></a>
+ </li>
+
+
+
+
+
+
+
+
+
+
+
+
+
+<li>
+ <a class="story" rel="published-1302268171566" href="httpdisabled://www.bbc.co.uk/news/world-africa-13012968"><img src="../../news.bbcimg.co.uk/media/images/52072000/jpg/_52072276_jex_1012855_de27-1.jpg" alt="Plumes of smoke after the Nato attack on rebel forces" />Confusion over Nato Libya strike<span class="gvl3-icon-wrapper"><span class="gvl3-icon gvl3-icon-invert-watch"> Watch</span></span></a>
+ </li>
+
+
+
+
+
+
+
+
+
+
+
+
+
+<li>
+ <a class="story" rel="published-1302286920993" href="httpdisabled://www.bbc.co.uk/news/world-africa-13020575"><img src="../../news.bbcimg.co.uk/media/images/52077000/jpg/_52077993_ivory_coast.jpg" alt="Armed pro-Ouattara forces" />Divided loyalties in Ivory Coast <span class="gvl3-icon-wrapper"><span class="gvl3-icon gvl3-icon-invert-watch"> Watch</span></span></a>
+ </li>
+
+
+
+
+
+
+
+
+
+
+
+
+
+<li>
+ <a class="story" rel="published-1302279220278" href="httpdisabled://www.bbc.co.uk/news/business-13018318"><img src="../../news.bbcimg.co.uk/media/images/52076000/jpg/_52076863_jex_1013152_de27-1.jpg" alt="Toyota car in production" />Toyota production returns to Japan<span class="gvl3-icon-wrapper"><span class="gvl3-icon gvl3-icon-invert-watch"> Watch</span></span></a>
+ </li>
+
+
+
+
+
+
+
+
+
+
+
+
+
+<li>
+ <a class="story" rel="published-1302215956187" href="httpdisabled://www.bbc.co.uk/newsbeat/13005125"><img src="../../news.bbcimg.co.uk/media/images/52058000/jpg/_52058744_jex_1012144_de27-1.jpg" alt="Pillow fight" />Giant pillow fight - it&#039;s Odd Box<span class="gvl3-icon-wrapper"><span class="gvl3-icon gvl3-icon-invert-watch"> Watch</span></span></a>
+ </li>
+ </ul>
+ </div>
+
+ </div>
+
+ <script type="text/javascript">$render("av-stories-best","av-stories-best");</script>
+
+
+
+<div id="av-live-streams" class="av-live-streams">
+
+
+
+
+ <div class="av-live-streams-include">
+ <ul id="news-channel-promotion" class="news-channel-promotion">
+ <li class="latest-summary"><h3>Latest summary:</h3> <a href="httpdisabled://www.bbc.co.uk/news/10462520" class="story watch">Watch <span class="gvl3-icon gvl3-icon-boxedwatch"> latest news summary</span></a> <a href="httpdisabled://www.bbc.co.uk/worldservice/includes/1024/screen/audio_console.shtml?stream=news_bullettin" class="story listen">Listen <span class="gvl3-icon gvl3-icon-boxedlisten"> latest news summary</span></a></li>
+ <li class="has-icon-boxedlive ">
+ <a href="httpdisabled://www.bbc.co.uk/iplayer/console/bbc_world_service" class="story is-live" onclick="javascript: void void('http://www.bbc.co.uk/iplayer/console/bbc_world_service', 'BBC', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=0,width=512,height=270,left=0,top=0'); return false;" >BBC World Service <span class="gvl3-icon gvl3-icon-boxedlive">Live</span></a>
+ </li>
+</ul>
+<script type="text/javascript">$render("news-channel-promotion","news-channel-promotion");</script>
+
+ </div>
+ </div>
+<script type="text/javascript">$render("av-live-streams","av-live-streams");</script>
+
+</div>
+<script type="text/javascript">$render("container-av-best","av-best");</script>
+
+<div class="bbccom_advert_placeholder">
+ <script type="text/javascript">$render("advert","advert-mpu-low");</script>
+</div>
+<script type="text/javascript">$render("advert-post-script-loaddisabled");</script>
+
+</div>
+<script type="text/javascript">$render("container-promo-best","promo-best");</script>
+
+<div id="features-and-analysis" class="container-features-and-analysis">
+ <h2 class="features-header">Features &amp; Analysis</h2>
+
+ <ul>
+
+
+
+
+
+
+ <!-- Non specific version -->
+
+
+
+ <li class="medium-image">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h3 class=" feature-header">
+ <a class="story" rel="published-1302264356466" href="httpdisabled://www.bbc.co.uk/news/world-middle-east-13013322"><img src="../../news.bbcimg.co.uk/media/images/52069000/jpg/_52069270_011711396-1.jpg" alt="An Iron Dome battery outside Ashkelon (4 April 2011)" />&#039;Iron Dome&#039;</a>
+ </h3>
+
+ <p>Israel&#039;s missile defence system may be a game changer </p>
+
+ <hr />
+ </li>
+
+
+ <li class="medium-image">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h3 class=" feature-header">
+ <a class="story" rel="published-1302268714570" href="httpdisabled://www.bbc.co.uk/news/world-asia-pacific-13010757"><img src="../../news.bbcimg.co.uk/media/images/52073000/jpg/_52073764_011717136-1.jpg" alt="A Humboldt penguin swims at a zoo in Lusisenpark, Mannheim, Germany, on 8/4/11" />Day in pictures</a>
+ </h3>
+
+ <p>Striking images from around the world </p>
+
+ <hr />
+ </li>
+
+
+ <li class="medium-image">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h3 class=" feature-header">
+ <a class="story" rel="published-1302230709033" href="httpdisabled://www.bbc.co.uk/news/magazine-12986535"><img src="../../news.bbcimg.co.uk/media/images/52058000/jpg/_52058296_holdring_thinks.jpg" alt="Man holding wedding ring" />Ring thing</a>
+ </h3>
+
+ <p>When did most men start wearing wedding bands? </p>
+
+ <hr />
+ </li>
+
+
+ <li class="medium-image">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h3 class=" feature-header">
+ <a class="story" rel="published-1302217882578" href="httpdisabled://www.bbc.co.uk/news/world-12992548"><img src="../../news.bbcimg.co.uk/media/images/52057000/jpg/_52057539_arniecomp.jpg" alt="Arnie" />It&#039;s quiz time!</a>
+ </h3>
+
+ <p>Arnie said he&#039;d be back - but what as? </p>
+
+ <hr />
+ </li>
+
+
+
+
+
+
+
+
+
+
+
+
+ <!-- Non specific version -->
+
+
+
+ <li class="no-image">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h3 class=" feature-header">
+ <a class="story" rel="published-1302254723664" href="httpdisabled://www.bbc.co.uk/news/magazine-12893416">LOL&#039;s triumph</a>
+ </h3>
+
+ <p>How did a little web acronym spread so fast? </p>
+
+ </li>
+
+
+ <li class="no-image">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h3 class=" feature-header">
+ <a class="story" rel="published-1302218200000" href="httpdisabled://www.bbc.co.uk/2/hi/programmes/from_our_own_correspondent/9450807.stm">Driving on ice</a>
+ </h3>
+
+ <p>No seatbelts allowed on Europe&#039;s longest road over frozen sea <a class="from-external-source" href="httpdisabled://www.bbc.co.uk/1/hi/programmes/from_our_own_correspondent/default.stm">From our own correspondent</a>
+ </p>
+
+ </li>
+
+
+ <li class="no-image">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h3 class=" feature-header">
+ <a class="story" rel="published-1302217339004" href="httpdisabled://www.bbc.co.uk/news/world-south-asia-13001640">Unloved US</a>
+ </h3>
+
+ <p>Rage against America breaks out in Afghanistan and Pakistan </p>
+
+ </li>
+
+
+
+
+
+
+ </ul>
+</div>
+<script type="text/javascript">$render("container-features-and-analysis","features-and-analysis");</script>
+
+ <div id="special-event-promotion-best-promo-module-hyper" class="include-only special-event-promotion-best">
+
+
+
+
+
+ <div class="hyperpuff">
+ <div id="promotional-content" class="hyper-promotional-content">
+
+ <h2>Elsewhere on BBC News</h2>
+
+ <ul>
+
+
+
+ <li class="medium-image first-child">
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h3>
+ <a class="story" rel="published-1302259690526" href="httpdisabled://www.bbc.co.uk/news/business-13000883"><img src="../../news.bbcimg.co.uk/media/images/52063000/jpg/_52063276_52063272.jpg" alt="Apps" />An app for that</a>
+ </h3>
+ <p>In a competitive, developing marketplace will apps remain a feature of business?</p>
+ </li>
+ </ul>
+
+ <div class="bbccom_advert_placeholder">
+ <script type="text/javascript">$render("advert","advert-sponsor-module","hyper-promotional-content","an-app-for-that");</script>
+ </div>
+ <script type="text/javascript">$render("advert-post-script-loaddisabled");</script>
+
+</div>
+<script type="text/javascript">$render("hyper-promotional-content","promotional-content");</script>
+
+
+ </div>
+
+</div>
+<script type="text/javascript">$render("special-event-promotion-best-promo-module-hyper","special-event-promotion-best-promo-module-hyper");</script>
+ <div id="market-data-include" class="include-only market-data-include">
+ <div class="market-data">
+<h2><a href="httpdisabled://www.bbc.co.uk/news/business/market_data/overview/default.stm">Market Data</a></h2>
+<p class="mkt-last-updated">Last Updated at 17:56 ET</p>
+
+<table class="mkt-table">
+ <tbody>
+ <tr class="table-headers">
+ <th id="mkt-index">Market index</th>
+ <th id="mkt-current">Current value</th>
+ <th id="mkt-trend">Trend</th>
+ <th id="mkt-var">Variation</th>
+ <th id="mkt-percent">% variation</th>
+ </tr>
+
+ <tr class="mkt-down">
+ <td headers="mkt-index" class="mkt-index"><a href="httpdisabled://www.bbc.co.uk/news/business/market_data/stockmarket/2/default.stm">Dow Jones</a></td>
+ <td headers="mkt-current">12380.05</td>
+ <td headers="mkt-trend" class="mkt-trend"><span class="mkt-trend-image">Down</span></td>
+ <td headers="mkt-var">-29.44</td>
+ <td headers="mkt-percent" class="mkt-percent">-0.24%</td>
+ </tr>
+
+ <tr class="mkt-down">
+ <td headers="mkt-index" class="mkt-index"><a href="httpdisabled://www.bbc.co.uk/news/business/market_data/stockmarket/12122/default.stm">Nasdaq</a></td>
+ <td headers="mkt-current">2780.41</td>
+ <td headers="mkt-trend" class="mkt-trend"><span class="mkt-trend-image">Down</span></td>
+ <td headers="mkt-var">-15.72</td>
+ <td headers="mkt-percent" class="mkt-percent">-0.56%</td>
+ </tr>
+
+ <tr class="mkt-down">
+ <td headers="mkt-index" class="mkt-index"><a href="httpdisabled://www.bbc.co.uk/news/business/market_data/stockmarket/11826/default.stm">S&amp;P 500</a></td>
+ <td headers="mkt-current">1328.17</td>
+ <td headers="mkt-trend" class="mkt-trend"><span class="mkt-trend-image">Down</span></td>
+ <td headers="mkt-var">-5.34</td>
+ <td headers="mkt-percent" class="mkt-percent">-0.40%</td>
+ </tr>
+
+ <tr class="mkt-up">
+ <td headers="mkt-index" class="mkt-index"><a href="httpdisabled://www.bbc.co.uk/news/business/market_data/stockmarket/3/default.stm">FTSE 100</a></td>
+ <td headers="mkt-current">6055.75</td>
+ <td headers="mkt-trend" class="mkt-trend"><span class="mkt-trend-image">Up</span></td>
+ <td headers="mkt-var">48.38</td>
+ <td headers="mkt-percent" class="mkt-percent">0.81%</td>
+ </tr>
+
+ <tr class="mkt-up">
+ <td headers="mkt-index" class="mkt-index"><a href="httpdisabled://www.bbc.co.uk/news/business/market_data/stockmarket/18/default.stm">Dax</a></td>
+ <td headers="mkt-current">7217.02</td>
+ <td headers="mkt-trend" class="mkt-trend"><span class="mkt-trend-image">Up</span></td>
+ <td headers="mkt-var">38.24</td>
+ <td headers="mkt-percent" class="mkt-percent">0.53%</td>
+ </tr>
+
+ <tr class="mkt-up">
+ <td headers="mkt-index" class="mkt-index"><a href="httpdisabled://www.bbc.co.uk/news/business/market_data/stockmarket/29954/default.stm">BBC Global 30</a></td>
+ <td headers="mkt-current">5727.44</td>
+ <td headers="mkt-trend" class="mkt-trend"><span class="mkt-trend-image">Up</span></td>
+ <td headers="mkt-var">2.06</td>
+ <td headers="mkt-percent" class="mkt-percent">0.02%</td>
+ </tr>
+
+ </tbody>
+</table>
+
+<div class="mkt-footer">
+
+<p><a href="httpdisabled://www.bbc.co.uk/news/business/market_data/ticker/markets/default.stm" class="mkt-ticker">Marketwatch ticker</a></p>
+
+<p class="mkt-data-delayed">Data delayed by 15 mins</p>
+</div>
+</div>
+
+
+ <div class="bbccom_advert_placeholder">
+ <script type="text/javascript">$render("advert","advert-sponsor-module","market-data-include","market-data-include");</script>
+ </div>
+ <script type="text/javascript">$render("advert-post-script-loaddisabled");</script>
+
+</div>
+<script type="text/javascript">$render("market-data-include","market-data-include");</script>
+
+<div id="most-popular-promotion" class="container-most-popular-promotion">
+
+<div class="bbccom_advert_placeholder">
+ <script type="text/javascript">$render("advert","advert-partner-button");</script>
+</div>
+<script type="text/javascript">$render("advert-post-script-loaddisabled");</script>
+
+
+<div id="most-popular" class="livestats livestats-tabbed tabbed most-popular">
+
+ <h2 class="livestats-header">Most Popular</h2>
+
+
+ <h3 class="void"><a href="index.html#">Shared</a></h3>
+
+ <div class="void">
+ <ol>
+ <li
+ class="first-child ol1">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/health-12999000"
+ class="story">
+ <span
+ class="livestats-icon livestats-1">1: </span>Cancer 'fuelled by extra drinks'</a>
+</li>
+<li
+ class="ol2">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/science-environment-13011073"
+ class="story">
+ <span
+ class="livestats-icon livestats-2">2: </span>New York 'at risk' as seas rise </a>
+</li>
+<li
+ class="ol3">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/science-environment-13009718"
+ class="story">
+ <span
+ class="livestats-icon livestats-3">3: </span>Stars' structure revealed by 'music'</a>
+</li>
+<li
+ class="ol4">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/uk-england-hampshire-13014640"
+ class="story">
+ <span
+ class="livestats-icon livestats-4">4: </span>Nuclear submarine man shot dead</a>
+</li>
+<li
+ class="ol5">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/magazine-12893416"
+ class="story">
+ <span
+ class="livestats-icon livestats-5">5: </span>Why did LOL infiltrate the language?</a>
+</li>
+ </ol>
+ </div>
+
+ <h3 class="tab "><a href="index.html#">Read</a></h3>
+
+ <div class="panel ">
+ <ol>
+ <li
+ class="first-child ol1">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/uk-england-hampshire-13014640"
+ class="story">
+ <span
+ class="livestats-icon livestats-1">1: </span>Nuclear submarine man shot dead</a>
+</li>
+<li
+ class="ol2">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/world-us-canada-13015909"
+ class="story">
+ <span
+ class="livestats-icon livestats-2">2: </span>Blame game as US shutdown looms</a>
+</li>
+<li
+ class="ol3">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/science-environment-13011073"
+ class="story">
+ <span
+ class="livestats-icon livestats-3">3: </span>New York 'at risk' as seas rise </a>
+</li>
+<li
+ class="ol4">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/health-12999000"
+ class="story">
+ <span
+ class="livestats-icon livestats-4">4: </span>Cancer 'fuelled by extra drinks'</a>
+</li>
+<li
+ class="ol5">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/world-asia-pacific-13010757"
+ class="story">
+ <span
+ class="livestats-icon livestats-5">5: </span>Day in pictures</a>
+</li>
+<li
+ class="ol6">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/world-us-canada-12571718"
+ class="story">
+ <span
+ class="livestats-icon livestats-6">6: </span>What does 'government shutdown' mean?</a>
+</li>
+<li
+ class="ol7">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/science-environment-12990213"
+ class="story">
+ <span
+ class="livestats-icon livestats-7">7: </span>Strangely silent star system seen</a>
+</li>
+<li
+ class="ol8">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/world-middle-east-13013322"
+ class="story">
+ <span
+ class="livestats-icon livestats-8">8: </span>Israeli new missile defence in action</a>
+</li>
+<li
+ class="ol9">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/world-13016843"
+ class="story">
+ <span
+ class="livestats-icon livestats-9">9: </span>Syrian city hit by deadly clashes</a>
+</li>
+<li
+ class="ol10">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/magazine-12986535"
+ class="story">
+ <span
+ class="livestats-icon livestats-10">10: </span>Without this ring, I thee wed</a>
+</li>
+ </ol>
+ </div>
+
+ <h3 class="tab "><a href="index.html#">Video/Audio</a></h3>
+
+ <div class="panel ">
+ <ol>
+ <li
+ class="first-child has-icon-watch ol1">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/technology-13012083"
+ class="story">
+ <span
+ class="livestats-icon livestats-1">1: </span>Electric 'super bus' reaches 250km/h<span
+ class="gvl3-icon gvl3-icon-watch"> Watch</span></a>
+</li>
+<li
+ class="has-icon-watch ol2">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/uk-13020498"
+ class="story">
+ <span
+ class="livestats-icon livestats-2">2: </span>'Submarine security was not breached'<span
+ class="gvl3-icon gvl3-icon-watch"> Watch</span></a>
+</li>
+<li
+ class="has-icon-watch ol3">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/world-us-canada-12994270"
+ class="story">
+ <span
+ class="livestats-icon livestats-3">3: </span>US government shutdown - what next?<span
+ class="gvl3-icon gvl3-icon-watch"> Watch</span></a>
+</li>
+<li
+ class="has-icon-watch ol4">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/health-13010512"
+ class="story">
+ <span
+ class="livestats-icon livestats-4">4: </span>Alcohol can increase cancer risk<span
+ class="gvl3-icon gvl3-icon-watch"> Watch</span></a>
+</li>
+<li
+ class="has-icon-watch ol5">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/world-middle-east-13016690"
+ class="story">
+ <span
+ class="livestats-icon livestats-5">5: </span>'Protesters shot' in Syria rally<span
+ class="gvl3-icon gvl3-icon-watch"> Watch</span></a>
+</li>
+<li
+ class="has-icon-watch ol6">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/video_and_audio/"
+ class="story">
+ <span
+ class="livestats-icon livestats-6">6: </span>One-minute World News<span
+ class="gvl3-icon gvl3-icon-watch"> Watch</span></a>
+</li>
+<li
+ class="has-icon-watch ol7">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/world-us-canada-13020801"
+ class="story">
+ <span
+ class="livestats-icon livestats-7">7: </span>Protest over US budget stalemate<span
+ class="gvl3-icon gvl3-icon-watch"> Watch</span></a>
+</li>
+<li
+ class="has-icon-watch ol8">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/science-environment-13009718"
+ class="story">
+ <span
+ class="livestats-icon livestats-8">8: </span>Stars' structure revealed by 'music'<span
+ class="gvl3-icon gvl3-icon-watch"> Watch</span></a>
+</li>
+<li
+ class="has-icon-watch ol9">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/uk-13020502"
+ class="story">
+ <span
+ class="livestats-icon livestats-9">9: </span>'I wrestled the gunman to the ground'<span
+ class="gvl3-icon gvl3-icon-watch"> Watch</span></a>
+</li>
+<li
+ class="has-icon-watch ol10">
+ <a
+ href="httpdisabled://www.bbc.co.uk/news/world-us-canada-13021011"
+ class="story">
+ <span
+ class="livestats-icon livestats-10">10: </span>Boehner's battle over spending<span
+ class="gvl3-icon gvl3-icon-watch"> Watch</span></a>
+</li>
+ </ol>
+ </div>
+
+ <div class="bbccom_advert_placeholder">
+ <script type="text/javascript">$render("advert","advert-sponsor-module","most-popular","most-popular");</script>
+ </div>
+ <script type="text/javascript">$render("advert-post-script-loaddisabled");</script>
+
+</div>
+
+<script type="text/javascript">$render("most-popular","most-popular");</script>
+
+</div>
+<script type="text/javascript">$render("container-most-popular-promotion","most-popular-promotion");</script>
+ <div id="programmes-promotion" class="include-only programmes-promotion">
+
+
+
+
+
+ <div class="hyperpuff">
+
+
+
+
+<div id="container-programme-promotion" class="container-programme-promotion">
+ <h2 class="programmes-header">Programmes</h2>
+
+
+
+ <ul class="programmes-standard">
+
+
+
+ <li class="medium-image first-item">
+
+
+
+
+
+
+
+
+
+
+
+
+
+<h3 class=" programme-header">
+ <a class="story" rel="published-1302263131000" href="httpdisabled://www.bbc.co.uk/2/hi/programmes/click_online/9451425.stm"><img src="../../news.bbcimg.co.uk/media/images/52072000/jpg/_52072121_-3.jpg" alt="A scientist monitors radiation" />Click<span class="gvl3-icon-wrapper"><span class="gvl3-icon gvl3-icon-invert-listen"> Listen</span></span></a>
+ </h3>
+ <p>How radiation level readings from Japan are being crowd-sourced</p>
+ <hr />
+ </li>
+</ul>
+
+ <div id="data-feed-best" class="include-only data-feed-best">
+ <h3><a href="httpdisabled://www.bbc.co.uk/worldservice/programmes/">BBC World Service</a></h3>
+ <ul><li class="no-image"><h4 class="has-icon-listen programme-header"><a class="story" href="httpdisabled://www.bbc.co.uk/iplayer/episode/b00zxn8x/From_Our_Own_Correspondent_02_04_2011/">From Our Own Correspondent<span xmlns:ion="httpdisabled://bbc.co.uk/2008/iplayer/ion" class="gvl3-icon gvl3-icon-invert-boxedlisten"> Listen</span></a></h4><p>Kate Adie hosts stories from reporters around the world.</p></li><li class="no-image"><h4 class="has-icon-listen programme-header"><a class="story" href="httpdisabled://www.bbc.co.uk/iplayer/episode/p00fvlfm/Newshour_07_04_2011_(2000_GMT)/">Newshour<span xmlns:ion="httpdisabled://bbc.co.uk/2008/iplayer/ion" class="gvl3-icon gvl3-icon-invert-boxedlisten"> Listen</span></a></h4><p>Who bombed Libyan rebels? Brazil school shooting; George Washington's belongings auctioned</p></li></ul>
+</div>
+<script type="text/javascript">$render("data-feed-best","data-feed-best");</script>
+ </div>
+<script type="text/javascript">$render("container-programmes-promotion","container-programme-promotion");</script>
+
+ </div>
+
+</div>
+<script type="text/javascript">$render("programmes-promotion","programmes-promotion");</script>
+
+<div class="bbccom_advert_placeholder">
+ <script type="text/javascript">$render("advert","advert-google-adsense");</script>
+</div>
+<script type="text/javascript">$render("advert-post-script-loaddisabled");</script>
+
+</div>
+<script type="text/javascript">$render("container-best","best");</script>
+
+ <!-- END #MAIN-CONTENT & CPS_ASSET_TYPE CLASS: index -->
+ </div>
+<!-- END CPS_AUDIENCE CLASS: us -->
+
+</div>
+<div id="related-services" class="footer">
+ <div id="news-services">
+ <h2>Services</h2>
+ <ul>
+ <li id="service-feeds"><a href="httpdisabled://www.bbc.co.uk/news/10628494"><span class="gvl3-feeds-icon-large services-icon">&nbsp;</span>News feeds</a></li>
+ <li id="service-mobile" class="first-child"><a href="httpdisabled://www.bbc.co.uk/news/help-10801499"><span class="gvl3-mobile-icon-large services-icon">&nbsp;</span>Mobile</a></li>
+ <li id="service-podcasts"><a href="httpdisabled://www.bbc.co.uk/podcasts/"><span class="gvl3-podcast-icon-large services-icon">&nbsp;</span>Podcasts</a></li>
+ <li id="service-alerts"><a href="httpdisabled://www.bbc.co.uk/news/10628323"><span class="gvl3-alerts-icon-large services-icon">&nbsp;</span>Alerts</a></li>
+ <li id="service-email-news"><a href="httpdisabled://newsvote.bbc.co.uk/email"><span class="gvl3-email-icon-large services-icon">&nbsp;</span>E-mail news</a></li>
+ </ul>
+ </div>
+ <div id="news-related-sites">
+ <h2>About BBC News</h2>
+ <ul>
+ <li class="column-1"><a href="httpdisabled://www.bbc.co.uk/blogs/theeditors/">Editors' blog</a></li>
+ <li class="column-1"><a href="httpdisabled://www.bbc.co.uk/journalism/">BBC College of Journalism</a></li>
+ <li class="column-1"><a href="httpdisabled://www.bbc.co.uk/news/10621655">News sources</a></li>
+ <li class="column-1"><a href="httpdisabled://www.bbc.co.uk/worldservice/trust/">World Service Trust</a></li>
+ </ul>
+ </div>
+</div>
+</div><!-- close front-page -->
+
+
+
+
+
+
+ </div> <div id="blq-mast" class="blq-rst blq-mast-light blq-new-nav" xml:lang="en-GB"> <div id="blq-acc-mobile"><a href="httpdisabled://www.bbc.co.uk/news/mobile/">Mobile</a></div> <form method="get" action="httpdisabled://search.bbc.co.uk/search" accept-charset="utf-8"> <p> <input type="hidden" name="go" value="toolbar" /> <input type="hidden" value="httpdisabled://www.bbc.co.uk/news/" name="uri" /> <input type="hidden" name="scope" value="news" /> <label for="blq-search" class="blq-hide">Search term:</label> <input id="blq-search" type="text" name="q" value="" maxlength="128" /> <input id="blq-search-btn" type="submit" value="Search" /> </p> </form> <h2 class="blq-hide">bbc.co.uk navigation</h2> <ul id="blq-nav-main" class="blq-not-uk"> <li id="blq-nav-n"><a href="index.html" hreflang="en-GB">News</a></li> <li id="blq-nav-s"><a href="httpdisabled://news.bbc.co.uk/sport/" hreflang="en-GB">Sport</a></li> <li id="blq-nav-w"><a href="httpdisabled://news.bbc.co.uk/weather/" hreflang="en-GB">Weather</a></li> <li id="blq-nav-tr"> <a href="httpdisabled://www.bbc.com/travel/" hreflang="en-GB">Travel</a> </li> <li id="blq-nav-t"><a href="httpdisabled://www.bbc.co.uk/tv/" hreflang="en-GB">TV</a></li> <li id="blq-nav-r"><a href="httpdisabled://www.bbc.co.uk/radio/" hreflang="en-GB">Radio</a></li> <li id="blq-nav-m"><a href="index.html#blq-nav">More</a></li> </ul> </div> <div id="blq-nav" class="blq-orange blq-rst"> <div id="blq-nav-links" class="blq-clearfix" xml:lang="en-GB"> <div id="blq-nav-links-inner"> <ul class="blq-nav-sub blq-first"> <li><a href="httpdisabled://www.bbc.co.uk/cbbc/">CBBC</a></li> <li><a href="httpdisabled://www.bbc.co.uk/cbeebies/">CBeebies</a></li> <li><a href="httpdisabled://www.bbc.co.uk/comedy/">Comedy</a></li> <li><a href="httpdisabled://www.bbc.co.uk/food/">Food</a></li> <li><a href="httpdisabled://www.bbc.co.uk/health/">Health</a></li> </ul> <ul class="blq-nav-sub"> <li><a href="httpdisabled://www.bbc.co.uk/history/">History</a></li> <li><a href="httpdisabled://www.bbc.co.uk/learning/">Learning</a></li> <li><a href="httpdisabled://www.bbc.co.uk/music/">Music</a></li> <li><a href="httpdisabled://www.bbc.co.uk/science/">Science</a></li> <li><a href="httpdisabled://www.bbc.co.uk/nature/">Nature</a></li> </ul> <ul class="blq-nav-sub blq-last"> <li><a href="httpdisabled://www.bbc.co.uk/local/">Local</a></li> <li><a href="httpdisabled://www.bbc.co.uk/northernireland/">Northern Ireland</a></li> <li><a href="httpdisabled://www.bbc.co.uk/scotland/">Scotland</a></li> <li><a href="httpdisabled://www.bbc.co.uk/wales/">Wales</a></li> <li id="blq-az"><a href="httpdisabled://www.bbc.co.uk/a-z/">Full A-Z<span class="blq-hide"> of BBC sites</span></a></li> </ul> </div> </div> </div> <div id="blq-foot" xml:lang="en-GB" class="blq-rst blq-clearfix blq-foot-transparent blq-foot-text-dark"> <div id="blq-footlinks"> <h2 class="blq-hide">BBC links</h2> <ul id="blq-bbclinks"> <li> <a href="httpdisabled://www.bbc.co.uk/aboutthebbc/">About the BBC</a> </li> <li> <a href="httpdisabled://www.bbc.co.uk/help/">BBC Help</a> </li> <li> <a href="httpdisabled://news.bbc.co.uk/newswatch/ifs/hi/feedback/default.stm">Contact Us</a> </li> <li> <a href="httpdisabled://www.bbc.co.uk/accessibility/">Accessibility Help</a> </li> <li> <a href="httpdisabled://www.bbc.co.uk/terms/">Terms of Use</a> </li> <li> <a href="httpdisabled://www.bbc.co.uk/jobs/">Jobs</a> </li> <li> <a href="httpdisabled://www.bbc.co.uk/privacy/">Privacy &amp; Cookies</a> </li> <li> <a href="httpdisabled://www.bbc.co.uk/bbc.com/furtherinformation/">Advertise With Us</a> </li> </ul> </div> <p id="blq-logo" class="blq-footer-image-dark"><img src="../../static.bbc.co.uk/frameworks/barlesque/1.8.15/desktop/3/img/blocks/dark.png" width="84" height="24" alt="BBC" /></p> <p id="blq-disclaim"><span id="blq-copy">BBC &copy; 2011</span> <a href="httpdisabled://www.bbc.co.uk/help/web/links/">The BBC is not responsible for the content of external sites. Read more.</a></p> <div id="blq-obit"><p><strong>This page is best viewed in an up-to-date web browser with style sheets (CSS) enabled. While you will be able to view the content of this page in your current browser, you will not be able to get the full visual experience. Please consider upgrading your browser software or enabling style sheets (CSS) if you are able to do so.</strong></p></div> </div> </div> <div id="bbccomWebBug" class="bbccomWebBug"></div>
+<script type="text/javascript">
+bbcdotcom.stats = {
+ "adEnabled" : "yes",
+ "contentType" : "HTML",
+ "audience" : "us"
+};
+</script>
+
+<script type="text/javascript" src="../../js.revsci.net/gateway/gw.js@csid=J08781"></script>
+<script type="text/javascript">
+ DM_tag();
+</script>
+<!-- Start Quantcast tag -->
+<script type="text/javascript">
+ _qoptions={
+ qacct:"p-ccrmZLtMqYB8w"
+ };
+</script>
+<script type="text/javascript" src="../../edge.quantserve.com/quant.js"></script>
+<noscript>
+ <div>
+ <img src="../../pixel.quantserve.com/pixel/p-ccrmZLtMqYB8w.gif" style="display: none;" height="1" width="1" alt="Quantcast"/>
+ </div>
+</noscript>
+<!-- End Quantcast tag -->
+
+<!-- SiteCatalyst code version: H.21.
+Copyright 1996-2010 Adobe, Inc. All Rights Reserved
+More info available at http://www.omniture.com -->
+<script type="text/javascript" src="../../news.bbcimg.co.uk/js/app/bbccom/19_52/s_code.js"></script>
+<script type="text/javascript"><!--
+/* You may give each page an identifying name, server, and channel on
+the next lines. */
+
+/************* DO NOT ALTER ANYTHING BELOW THIS LINE ! **************/
+var s_code=s.t();if(s_code)void(s_code)//--></script>
+<script type="text/javascript"><!--
+if(navigator.appVersion.indexOf('MSIE')>=0)void(unescape('%3C')+'\!-'+'-')
+//--></script><noscript><div><a href="httpdisabled://www.omniture.com" title="Web Analytics"><img
+src="../../bbc.112.2o7.net/b/ss/bbcwglobalprod/1/H.21--NS/0@AQB=1&amp;pccr=true&amp;AQE=1"
+height="1" width="1" alt="" /></a></div></noscript><!--/DO NOT REMOVE/-->
+<!-- End SiteCatalyst code version: H.21. -->
+
+
+
+<!-- Begin comScore Tag -->
+<script type="text/javascript">
+ void(unescape("%3Cscript src='" + (document.location.protocol == "httpdisabledsdisabled:" ? "httpdisabledsdisabled://sb" : "httpdisabled://b") + ".scorecardresearch.com/beacon.js' %3E%3C/script%3E"));</script>
+<script type="text/javascript">
+ COMSCORE.beacon({
+ c1:2,
+ c2:"6035051",
+ c3:"",
+ c4:"www.bbc.co.uk/news/",
+ c5:"",
+ c6:"",
+ c15:""
+ });
+</script>
+<noscript>
+ <div>
+ <img src="../../b.scorecardresearch.com/b2@c1=2&amp;c2=6035051&amp;c3=&amp;c4=www.bbc.co.uk%252Fnews%252F&amp;c5=&amp;c6=&amp;c15=&amp;cv=1.3&amp;cj=1.html" style="display:none" width="0" height="0" alt="" />
+ </div>
+</noscript>
+<!-- End comScore Tag -->
+
+
+
+
+
+
+
+
+
+
+ </div> </div>
+
+
+<!-- shared/foot -->
+<script type="text/javascript">
+ bbc.fmtj.common.removeNoScript({});
+ bbc.fmtj.common.tabs.createTabs({});
+</script>
+<!-- hi/news/foot.inc -->
+<!-- shared/foot_index -->
+<!-- #CREAM hi news international foot.inc -->
+
+
+</body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/blst.msn.com/as/wea3/i/en-us/law/30.gif b/mobile/android/tests/browser/chrome/tp5/msn.com/blst.msn.com/as/wea3/i/en-us/law/30.gif
new file mode 100755
index 0000000000..992699f4db
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/blst.msn.com/as/wea3/i/en-us/law/30.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/23/6B8E88315584A40B04E32D89551E.jpg b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/23/6B8E88315584A40B04E32D89551E.jpg
new file mode 100755
index 0000000000..e373533235
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/23/6B8E88315584A40B04E32D89551E.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/2F/9EFAECEC174B21FB83D10C82522D2.jpg b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/2F/9EFAECEC174B21FB83D10C82522D2.jpg
new file mode 100755
index 0000000000..a2dd412987
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/2F/9EFAECEC174B21FB83D10C82522D2.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/38/FAF3346E94CF4579ECAB641703868.jpg b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/38/FAF3346E94CF4579ECAB641703868.jpg
new file mode 100755
index 0000000000..4b3ccca52e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/38/FAF3346E94CF4579ECAB641703868.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/5B/CC662FC6233C7449D9C7F9796801D.jpg b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/5B/CC662FC6233C7449D9C7F9796801D.jpg
new file mode 100755
index 0000000000..53cf4bdbb9
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/5B/CC662FC6233C7449D9C7F9796801D.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/76/CAF5FAB7F245F96327F2B4C806D.jpg b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/76/CAF5FAB7F245F96327F2B4C806D.jpg
new file mode 100755
index 0000000000..c9aa55971c
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/76/CAF5FAB7F245F96327F2B4C806D.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/80/82E2A652E4A790B140675E74293AD6.jpg b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/80/82E2A652E4A790B140675E74293AD6.jpg
new file mode 100755
index 0000000000..75bd585195
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/80/82E2A652E4A790B140675E74293AD6.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/B7/EB75D45B8948F72EE451223E95A96.gif b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/B7/EB75D45B8948F72EE451223E95A96.gif
new file mode 100755
index 0000000000..d316f8451d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/B7/EB75D45B8948F72EE451223E95A96.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/CE/19F603C3122D48B6554BBD495195.jpg b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/CE/19F603C3122D48B6554BBD495195.jpg
new file mode 100755
index 0000000000..d65e93190c
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/CE/19F603C3122D48B6554BBD495195.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/CF/59B3CB34EF11B221719175143187.jpg b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/CF/59B3CB34EF11B221719175143187.jpg
new file mode 100755
index 0000000000..dd51c1c30f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/CF/59B3CB34EF11B221719175143187.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/D8/41FF8CA0A47CC8208E684FA1BE6D6.jpg b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/D8/41FF8CA0A47CC8208E684FA1BE6D6.jpg
new file mode 100755
index 0000000000..8f8798743d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/D8/41FF8CA0A47CC8208E684FA1BE6D6.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/E2/37BA92E210D341BFDBF4126422A3D2.gif b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/E2/37BA92E210D341BFDBF4126422A3D2.gif
new file mode 100755
index 0000000000..3abac737ed
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/E2/37BA92E210D341BFDBF4126422A3D2.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/EA/9BECE90994978BFAE6F38561515E8.jpg b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/EA/9BECE90994978BFAE6F38561515E8.jpg
new file mode 100755
index 0000000000..b499cbd701
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/EA/9BECE90994978BFAE6F38561515E8.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/FF/6B3EB94D554DA0488C66DC31482D48.jpg b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/FF/6B3EB94D554DA0488C66DC31482D48.jpg
new file mode 100755
index 0000000000..0505af37b2
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stb.s-msn.com/i/FF/6B3EB94D554DA0488C66DC31482D48.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/gbl/lg/csl/favicon.ico b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/gbl/lg/csl/favicon.ico
new file mode 100755
index 0000000000..a7e042d653
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/gbl/lg/csl/favicon.ico
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/css/1d/b0ebeba5ed4ca3c158e6d6059f5074.css b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/css/1d/b0ebeba5ed4ca3c158e6d6059f5074.css
new file mode 100755
index 0000000000..86da9d1467
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/css/1d/b0ebeba5ed4ca3c158e6d6059f5074.css
@@ -0,0 +1 @@
+#wrapper .w12,#wrapper.w12{min-width:972px;width:81em}.pa{margin:0 auto;padding:1em .5em}.pa #content,.pa #area1,.pa #area2,.pa #area3{float:left}.pa #subfoot{clear:both}.pa #area2,.pa #area3{margin-left:1em}.pa #page:after{clear:both;content:".";display:block;height:0;visibility:hidden}#wrapper{padding:1em 0;text-align:left;margin:0 auto}#wrapper .w1{min-width:70px;width:5.833em}#wrapper .w2{min-width:152px;width:12.667em}#wrapper .w3{min-width:234px;width:19.5em}#wrapper .w3 .w50{min-width:111px;width:9.25em}#wrapper .w4{min-width:316px;width:26.333em}#wrapper .w4 .w33{min-width:97px;width:8.083em}#wrapper .w4 .ce3.w33{min-width:98px;width:8.167em}#wrapper .w5{min-width:398px;width:33.167em}#wrapper .w5 .w33{min-width:124px;width:10.333em}#wrapper .w5 .ce2.w33,#wrapper .w5 .ce3.w33{min-width:125px;width:10.417em}#wrapper .w5 .w50{min-width:193px;width:16.083em}#wrapper .w6{min-width:480px;width:40em}#wrapper .w7{min-width:562px;width:46.833em}#wrapper .w7 .w33{min-width:179px;width:14.917em}#wrapper .w7 .ce3.w33{min-width:180px;width:15em}#wrapper .w7 .w50{min-width:275px;width:22.917em}#wrapper .w8{min-width:644px;width:53.667em}#wrapper .w8 .w33{min-width:206px;width:17.167em}#wrapper .w8 .ce1.w33,#wrapper .w8 .ce3.w33{min-width:206px;width:17.167em}#wrapper .w9{min-width:726px;width:60.5em}#wrapper .w9 .w50{min-width:357px;width:29.75em}#wrapper .w10{min-width:808px;width:67.333em}#wrapper .w10 .w33{min-width:261px;width:21.75em}#wrapper .w10 .ce3.w33{min-width:262px;width:21.833em}#wrapper .w11{min-width:890px;width:74.167em}#wrapper .w11 .w33{min-width:288px;width:24em}#wrapper .w11 .ce2.w33,#wrapper .w11 .ce3.w33{min-width:289px;width:24.083em}#wrapper .w11 .w50{min-width:439px;width:36.583em}#wrapper .w12{min-width:972px;width:81em}#head{min-width:972px;background:transparent}#page{min-width:972px;background:#fff}#nav{min-width:972px;background:transparent}#content{background:transparent}#foot{min-width:972px;background:transparent}#wrapper .wings{background-color:#009ad9;height:1.667em;min-width:81em;width:100%}a.more,div.br *,.cotb *,.coss *{font-family:arial,sans-serif}@media print{form,object{display:none}}a,a:link,a:visited{color:#333;text-decoration:none}a:hover,a:hover span{color:#000;text-decoration:underline}a img{border:none}input,select,textarea{font-size:15px;line-height:normal}big,div.h2,div.h3,h1,h2,h3,h4,h5,h6,small{font-family:arial,sans-serif;font-size:100%;margin:0;padding:0}.cf:after,ul.cf li:after,.ro:after{clear:both;content:".";display:block;height:0;visibility:hidden}.none{display:none}#wrapper .grsep{border-bottom:solid 1px #e1e1e1}#wrapper .headerbar2,#wrapper .breaknews1,#wrapper .ad1,#wrapper .alert1,#wrapper .hotmail1,#wrapper .spotlight1,#wrapper .msnfoot1,#wrapper #area2 .linkedimg1{margin:.667em}#wrapper .money1,#wrapper .sponad1{margin:0 0 0 .667em}#wrapper .shopping1 ul.linklist22 li.last{border-bottom:solid 1px #e1e1e1}#wrapper .local1 .simple8 div.loclist ul li{clear:both;float:none}body{background:transparent url(../../i/1a/57011fe37f98be0ee74ce87a62ba9b.png) no-repeat top center;color:#333;font-family:arial,sans-serif;font-size:75%;line-height:1.33em;margin:0;padding:0;text-align:center}#wrapper #hotmail{margin:.457em .667em 2.33em}#wrapper #content #stgsearch{margin-top:-.21em}#wrapper #content #gendermodule{margin-top:1.418em}.exphd .wlcard1 ul li.tolatino{float:right;border-left:none;border-right:1px solid #999;padding-right:5px}.localshopping h3.cf{border-bottom:1px solid #d7d7d7;padding-bottom:.8333em}.dating1 .complex1 fieldset.last input{padding-top:.167em;padding-bottom:.167em}.dating1 .complex1 fieldset.last input.button{background-color:#009ad9}.dating1 .complex1 fieldset.last input.button:hover{background-color:#33aee1;cursor:pointer}.dating1 .complex1 fieldset.last label,.dating1 .complex1 fieldset.last select{margin-top:.167em}.dating1 .br2 .complex1 label{color:#333}.dating1 .linkedimglinklist8 span{color:#666}.dating1 #cff1 select{height:21px;width:108px}.dating1 #agemin,.dating1 #agemax{height:21px;width:46px}#wrapper #area2 .dating1 .linkedimg1{margin-left:0;margin-right:0}.dating1 .complex1 fieldset.last #txtlocation{margin-right:.667em;width:4.333em}.dating1 .complex1 fieldset.last .button{width:8.333em}.w12 .generic1 div.br{margin:0}.w12 .generic1 .linkedimg1 img{vertical-align:top}.dating1 .br3 img{vertical-align:bottom}.dating1 .complex1 fieldset.last label{margin-right:.667em}#wrapper #tg{background:transparent url(../../i/62/b5797d19976f0955d6d5d5c87ec996.jpg) no-repeat top center}#stk_head .single2{margin:.6em 0 0 .4em}#stk_data .simple8 input.text{color:#666;margin:0 .167em 0 .083em}#stk_data .simple8 div div{padding:0 .083em .083em}#stk_data .simple8 .image{vertical-align:bottom}#wrapper .ce .sponad1{margin:0}.sponad1 .richtext p{margin:.583em 0 0}.sponad1 .m16{margin:.667em 0}#stk_data .linklist16{margin-top:-.5em}#stk_data .linklist16 li{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2236px;padding:.333em 0 .333em 1.583em}.headerbar_us .websearch2 .opt,.headerbar_us .websearch2 span{color:#666}.dating1 .linkedimglinklist8,.dating1 .br2,.dating1 .br3{margin-left:.333em}.dating1 .h3{background-color:#fff;padding-bottom:.5em}#nav .pa{padding-top:0}.headerbar_us .pgopt1{float:none}#page .menunavbar1{margin:0 auto .667em}#page .menunavbar1 .ntier1{background-color:#009ad9}.scp1 .headline .first .richtext{margin-top:1.167em}div.hlcpm1 .hcpep3 li{display:inline-block;float:none;vertical-align:top}.hlcpm1 .hcpep3 li,.hlcpm1 .hcpep3 li.media{line-height:1.5em;padding-top:.25em}.scp1 .npane span a,.scp1 .linkedimg span a{display:block}#wrapper .exphd{margin:0 0 22px}.exphd .wlcard1 ul li.first{border-left:0;line-height:20px}.exphd .wlcard1 ul li{border-left:solid 1px #999;display:inline;float:left;line-height:20px;margin-left:5px;padding-left:5px}.expblu .exphd .wlcard1 ul li,.expblu .exphd .wlcard1 ul li.myhp a{border-color:#fff}#wrapper .exphd .wlcard1 a{color:#999}.exphd .wlcard1 ul li.last{border-left:none;float:right}.exphd .wlcard1 ul{width:93%}.exphd .wlcard1 ul li.myhp a{padding-right:5px;border-right:solid 1px #999;font-weight:bold}.exphd .wlcard1 div{font-size:100%;float:left;margin-left:5px;padding-top:1px;line-height:16px}#wrapper #head .exphd .br4{clear:none;margin:0;padding-left:.417em;padding-top:1.25em}#wrapper #head .exphd .br2{display:block;float:left;min-width:16.7em;padding-top:0;width:200px}#wrapper .exphd .br1{width:100%;display:block;padding-bottom:.3em}#wrapper #head .exphd .br3{min-width:600px;padding-top:.9em;width:50em}.exphd .websearch2 .bi{padding:0 0 0 5px}#wrapper .exphd .websearch2 input.text{margin-top:7px;width:396px}.expfoot{min-width:600px;padding-top:.9em;width:50em}.expfoot .websearch2 .bi{padding:0 0 0 5px}#wrapper .expfoot .websearch2 input.text{margin-top:7px;width:396px}body.expht{background:none}.expht #wrapper{padding-top:0}.expht .headerbg,#wrapper #tg{background:transparent url(../../i/94/8b0fe9bcd1399077fdc9374e5f314d_1.png) no-repeat 0 0}.expht .exphd{padding-top:1em}.expht #head .ro .ce{position:relative;z-index:51}.expht .exphd .imgloaddisabledla{position:absolute;z-index:-1;top:0;left:0}.expht .exphd .br5{margin:5px 0 0 21.2%}.expht .exphd .br5 .richtext p{background:transparent url(../../i/11/999518480e3c07301320f84f4bd855.png) no-repeat scroll 0 0;padding-left:20px;margin:0}.thumb_h ul{padding-left:0;width:100%;height:6.2em;margin:0}.thumb_h ul li{background-color:#f1f1f1;margin:3px 2px 0 0;list-style:none;width:23%;float:left;padding:.44em;min-height:60px}.thumb_h ul .last{margin-right:0}.thumb_h a p{margin:0;height:5em}.thumb_h a img{float:left;margin-right:10px}.thumb_h ul li.selected{border:1px solid #009ad9;border-top-width:3px;margin-top:0;height:4.9em;background-color:#fff}.tfh .scp1 .npane li img{padding-bottom:8px}.tfh .scp1 .npane ul li{margin:3px 9px 0 0}.tfh .scp1 .npane ul .last{margin-right:0}.tfh .scp1 .npane ul a{font-size:1.2em;font-weight:bold;padding-bottom:1px}.tfh .scp1 .headline ul{padding-left:1.6em}.tfh .scp1 .npane,.tfh .scp1 .linkedimg{text-align:center}.tfh .co1b1{min-height:191px}.thumb_h ul li a{height:5em;display:block;color:#666}.thumb_h ul li.selected a{color:#333}.thumb_h ul li.selected a:hover span,.thumb_h ul li.selected a:hover{text-decoration:none}.thumb_v{float:left;margin-right:.75em}.thumb_v ul{padding:0;margin:0;width:100%}.thumb_v ul li{list-style:none;height:5em;min-height:60px;margin:0 0 1px 2px;background:#f1f1f1;width:17.9em;border:1px solid #f1f1f1}.thumb_v ul li a{height:5em;display:block;color:#666}.thumb_v ul li a span{display:block;padding-top:1.1em}.thumb_v ul li.selected a:hover span,.thumb_v ul li.selected a:hover{text-decoration:none}.thumb_v ul li.selected a{color:#333}.thumb_v ul .selected{border:1px solid #009ad9;border-left-width:3px;margin-left:0;background-color:#fff}.thumb_v ul li img{float:left;margin-right:.75em}.tfv .scp1 .npane li img{padding-bottom:8px}.tfv .scp1 .npane ul li{margin-right:10px}.tfv .scp1 .npane ul .last{margin-right:0}.tfv .scp1 .npane ul a{font-size:1.2em;font-weight:bold;text-align:left}.tfv .scp1 .headline ul{padding-left:20.6em}.tfv .scp1 .linkedimg .richtext{margin-bottom:3px}.tfv .scp1 .npane.n3{text-align:left}.tfv .scp1 .headline li{padding-top:0}.tfv .scp1 .linkedimg{margin-left:19em}#wrapper #content .cogr{margin:.6em 0 1.2em .4em}.tfv .headline div a,.tfh .headline div a{float:left}.tfv .headline div p a,.tfh .headline div p a{float:none}#infopane_hc .tfh div.co,#infopane_vc .tfv div.co{display:none}#infopane_hc .cof div.co,#infopane_vc .cof div.co{display:block}#wrapper #head .expsh{margin-bottom:17px}#wrapper #head .ro .expsh .br4{display:none;margin:0 auto;min-width:582px;padding:0 0 0 1.5em;width:50.4em}#wrapper #head .expsh .br3{margin-bottom:3px}#wrapper .expsh .br4 .richtext p{float:left;margin-bottom:0;margin-right:2px}#wrapper .expsh .br4 .prefix{float:left;margin-right:3px}#wrapper .expsh .br4 .resultlist{margin:0;padding:0}#wrapper .expsh .br4 .resultlist li{float:left;list-style:none}#wrapper .expsh .br4 .resultlist li.last{float:right}#wrapper .expsh .br4 .resultlist li.last a{color:#666}#wrapper .expsh .br4 .resultlist li a,#wrapper .headerbar2 .br5 .richtext p a{text-decoration:none}#wrapper .expsh .br4 .resultlist li a:hover,#wrapper .headerbar2 .br5 .richtext p a:hover{color:#000;text-decoration:underline}#wrapper .exphd .websearch2 .opt{position:absolute;margin-left:0}#wrapper .exphd .websearch2 .opt a{margin:0 3px}#wrapper .exphd .websearch2 .opt a.first{margin:0}#mq1 #msd{float:left;margin:-6.215em 0 0 17.8em}#mq1 br{display:none}#mq1 .br4{float:none;margin:-4.75em 0 0 35.2em}#mq1 .br3 .simple8 input.text{color:#666;width:12.55em;margin-left:6px}#mq1 .br3 .co3b1 .br2{float:left;margin-top:1.65em}#mq1 .br3 .co3b1 .br3{clear:none;float:left;margin-top:1.3em}#mq1 .br1 table{width:15.3em}#mq1 .br1 table td{line-height:1.16em;width:4.4em}#mq1 .br1 td.siidx{font-weight:bold;padding:0 0 .25em;width:auto}#mq1 .br1 td.silast{width:auto}#mq2 .br4{margin-left:.67em;margin-top:-6.92em}#mq2 .indices1 table{width:25.08em}#mq2 .indices1 td{margin:0;padding:0;width:6.17em}#mq2 .indices1 .siidx{font-weight:bold;width:6.58em}#mq2 .simple8 input.text{color:#666;margin-left:6px;width:22.42em}#mq2 .br3 .co3b1 .br2{float:left;margin-top:1.42em}#mq2 .br3 .co3b1 .br3{clear:none;margin-top:.58em}#mq1 .h2,#mq2 .h2{margin:0 0 .65em;width:98.8%}#mq1 .linklist16 li,#mq2 .linklist16 li{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2236px;display:list-item;line-height:1.98em;margin:0;padding:.083em 0 .083em 19px}#mq1 .simple8 div div,#mq2 .simple8 div div{border-color:#ccc;height:24px;padding:0}#mq1 .br2 .richtext,#mq2 .br2 .richtext{color:#707070;font-size:83%}#mq1 .br3 .co3b1 .br2 .richtext,#mq2 .br3 .co3b1 .br2 .richtext{color:#333;font-size:100%;padding-right:.4em}#mq1 .br3 .co3b1 .br2 .richtext{font-size:79%}#mq1 .simple8 input.image,#mq2 .simple8 input.image{margin-right:1px}#mq1 .linklist16 li span.media,#mq2 .linklist16 li span.media{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2351px;display:block;margin-left:-19px}#mq1 .linklist16 li span.media a,#mq2 .linklist16 li span.media a{display:block;margin-left:19px}.bottomline{border-bottom:solid 1px #bcbcbc}#wrapper div.scrollhead{background-color:#fff;min-width:972px;position:fixed;top:0;width:81em;z-index:60}.scrollheadheight{padding-top:14em}.scrollhead #nav .ro div.ce{position:static}.msnvideooverlayplayer{z-index:16777271}#heroplayer1internalgallerydiv_content object{visibility:visible!important}#popsrchnew .coa2 div.hr{margin:.7em 0}#popsrchnew .alist1 div.br2 h3,#popsrchnew .alist1 div.br3 h3,#popsrchnew .alist1 div.br4 h3{margin-bottom:0}#popsrchnew .hlcp2 .sec img.landscape{margin-bottom:.35em}#head .shortersrch #q{width:316px}.shortersrch .search{margin-top:1.4em}#wrapper .minihead{background-color:#009ad9;left:0;padding:.6em 0 1em;position:fixed;top:0;width:100%;z-index:60}#wrapper .minihead #tg{background:none}.minihead .headerbar_us{height:auto;min-height:0;width:81em;min-width:972px;margin:0 auto}.minihead .headerbar_us .br1,.minihead .headerbar_us .br2,.minihead .headerbar_us .scopes,.minihead .headerbar_us .br4 img,.minihead .headerbar_us .opt,.minihead .condbanner1{display:none}.minihead .headerbar_us .br4 a{background:transparent url(../../i/61/def0ebad64d00fda0702cb7b8179ea.png) no-repeat scroll 0 -132px;display:block;height:34px;width:77px}.minihead .headerbar_us .br3{height:0}.minihead .headerbar_us .br3 a{color:#fff;display:block;font-weight:normal;margin-top:10px}.minihead .headerbar_us .br3 a:link{font-weight:normal}.minihead .headerbar_us .br4{min-width:179px;padding-top:0;width:179px}.miniheadpageheight{padding-top:9.8em}#wrapper .minihead .w12{width:100%}#wrapper .minihead .headerbar_us .br5{padding-top:0;width:auto}#headl .headerbar_us .br4{padding-top:0;margin-bottom:.5em}#headl .headerbar_us .br5{margin:0}.lhemhp,.lhe{background-color:#009ad9;height:5.6em;left:0;position:fixed;top:0;width:100%;z-index:60}.lhe{height:4.5em}#wrapper .lhemhp #tg,#wrapper .lhe #tg{background:none}.lhemhp .headerbar_us,.lhe .headerbar_us{height:auto;margin:0 auto;min-height:0;min-width:972px;width:81em}.lhemhp .headerbar_us .br1,.lhemhp .headerbar_us .br2,.lhemhp .headerbar_us .br3,.lhemhp .headerbar_us .scopes,.lhemhp .headerbar_us .br4 img,.lhemhp .headerbar_us .opt,.lhemhp #nav,.lhe .headerbar_us .br1,.lhe .headerbar_us .br2,.lhe .headerbar_us .br3,.lhe .headerbar_us .scopes,.lhe .headerbar_us .br4 img,.lhe .headerbar_us .opt,.lhe #nav,.scrollhead .expandbuttoncontainer,.scrollhead .msnheadlogo,.scrollhead .mkhmhead a,.scrollhead #nav .ro,.lhe .condbanner1{display:none}.lhemhp #nav .ntier1,.lhe #nav .ntier1{background-color:transparent;display:none}.lhemhp .headerbar_us .br4,.lhe .headerbar_us .br4{margin-top:.5em;min-width:0;width:12em}.lhemhp .headerbar_us .br4{margin:1.2em 0 1.7em 0;padding-top:0}#wrapper .lhemhp .w12,#wrapper .lhe .w12{width:100%}.lhemhp .headerbar_us .br5{padding-top:0;margin-top:1.23em}.lhemhp .headerbar_us a.expandnavigation,.lhe .headerbar_us a.expandnavigation{border:solid 1px #fff;color:#fff;display:block;float:left;padding:6px 17px;margin-top:7px}.lhemhp .mhexpandhead{min-height:8px;margin-top:-.2em;padding-top:.8em}.lhemhp .headerbar_us .mhexpandhead a{color:#fff;display:block;font-weight:normal}.lhemhp .msnheadlogo,.lhemhp .headerbar_us .br4 a,.lhe .msnheadlogo,.lhe .headerbar_us .br4 a{background:transparent url(../../i/61/def0ebad64d00fda0702cb7b8179ea.png) no-repeat scroll 0 -132px;display:block;height:34px;width:77px}.lhe .headerbar_us .br5{margin-top:.6em}.expandbuttoncontainer{float:right;margin-right:.5em}.lhe .expandbuttoncontainer{margin-top:.5em}#subfoot .bingpulse1{margin-bottom:2.917em;overflow:hidden;padding-top:.833em}.bingpulse1 .br{display:inline;float:left;margin-left:10px;min-width:237px;width:19.75em}.bingpulse1 .br2,.bingpulse1 .br3{min-width:236px;width:19.667em}.bingpulse1 .br2,.bingpulse1 .br3,.bingpulse1 .br4{margin-left:0}.bingpulse1 .linklist1 li{background:url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2241px;color:#333;line-height:1.333em;margin-bottom:0;padding:0 0 .333em 12px}.bingpulse1 .linklist1 li.first{background:none;font-weight:bold;font-size:130%;margin-bottom:.416em;margin-left:-13px;padding-top:0}#subfoot .bingpulse1 .h2{margin-bottom:.5em;min-width:956px;width:79.667em}.srchhs{width:610px}.srchhs span{color:#333;float:left;font-weight:bold;margin-bottom:1.08em;padding-top:.21em}.srchhs ul{list-style:none outside none;margin:0;padding:0}.srchhs li{color:#333;float:left;margin:.23em 0 0 5px}.srchhs ul .last{float:right}.minihead .websearch2 .srchhs{margin-top:0;padding-top:4px}.minihead .websearch2 .srchhs span{margin:0}.minihead .headerbar_us .srchhs a,.minihead .websearch2 .srchhs span,.minihead .headerbar_us .srchhs li,.minihead .headerbar_us .srchhs a:hover{color:#fff}.srchhs.husearchbox,.minihead .srchhs.hlockedheader{display:none}.minihead div.srchhs{display:block}#head .headerbar_us{margin-top:0}.coa2.coc1 .h2,.cogr ul.cotb.coc1 li.tabsel span,.cogr ul.cotb.coc1{border-color:#009ad9}.coa2.coc1 div.hr,.coa3.coc1 div.hr,.coa3.coc1 .h2,.coa3.coc1 .h3{border-color:#ccebf7}.coa2.coc2 .h2,.cogr ul.cotb.coc2 li.tabsel span,.cogr ul.cotb.coc2{border-color:#89c655}.coa2.coc2 div.hr,.coa3.coc2 div.hr,.coa3.coc2 .h2,.coa3.coc2 .h3{border-color:#e2f1d4}.coa2.coc3 .h2,.cogr ul.cotb.coc3 li.tabsel span,.cogr ul.cotb.coc3{border-color:#bdbdbd}.coa2.coc3 div.hr,.coa3.coc3 div.hr,.coa3.coc3 .h2,.coa3.coc3 .h3{border-color:#e3e3e3}.m1{margin:.667em 0 0 0}.m2{margin:0 .5em 0 0}.m3{margin:0 0 .667em 0}.m4{margin:0 0 0 .5em}.m5{margin:.667em 0 .667em 0}.m6{margin-bottom:0;margin-top:0}.m7{margin:0 .5em 0 .5em}.m8{margin-left:0;margin-right:0}.m9{margin:.667em 0 0 .5em}.m10{margin:.667em .5em 0 0}.m11{margin:0 0 .667em .5em}.m12{margin:0 .5em .667em 0}.m13{margin:0 .5em .667em .5em}.m14{margin:.667em 0 .667em .5em}.m15{margin:.667em .5em 0 .5em}.m16{margin:.667em .5em .667em 0}.m17{margin:.667em .5em .667em .5em}.coa2.ruled{border-bottom:solid 1px #e1e1e1}.coa2 .h3{color:#333;font-weight:bold;line-height:1.43em;margin:0 0 .833em 0}.coa2 .h2 a,.coa2 .h2 a:link,.coa2 .h2 a:visited{color:#333}.coa2 .h2 a:hover,.coa2 .h2 a:active{color:#000}.coa2 .h3 a,.coa2 .h3 a:link,.coa2 .h3 a:visited{color:#333}.coa2 .h3 a:hover,.coa2 .h3 a:active{color:#000}.coa2 .h2 span.icon,.cogr ul.cotb li span.icon,.cogr ul.cotb li.tabsel span.icon{padding-left:.333em}.coa2 .h2 a span,.coa2 .h2 span span{padding:0}.coa2 .h2 a,.coa2 .h2 span{display:block;float:left;padding:0 0 .333em 0}#wrapper .coa2 .attr{border:0;bottom:-3px;float:right;position:relative}#wrapper .coa2 .attr,#wrapper .coa2 a.attr:link,#wrapper .coa2 a.attr:visited,#wrapper .coa2 a.more,#wrapper .coa2 a.more:link,#wrapper .coa2 a.more:visited{color:#666;font-size:92%;font-weight:normal}#wrapper .coa2 a.attr:hover,#wrapper .coa2 a.attr:active,#wrapper .coa2 a.more:hover,#wrapper .coa2 a.more:active{color:#333}.coa2 div.hr{border-top:solid 2px;margin:.833em 0}.cogr ul.cotb,.coa2 .h2{clear:both;font-weight:bold;line-height:normal;list-style-type:none;margin:0 0 .833em 0;padding:0;width:100%}.coa2 .h2{border-bottom:solid 3px;color:#333;font-weight:bold;line-height:normal}.cogr ul.cotb{border-bottom:solid 1px;background-color:#f1f1f1}.cogr ul.cotb li{float:left;list-style-type:none}.cogr ul.cotb li a,.cogr ul.cotb li span{display:block;float:left;font-size:100%;line-height:normal}.cogr ul.cotb li span span,.cogr ul.cotb li.tabsel span span,.cogr ul.cotb li a span{border:0;bottom:0;padding:0;margin:0;top:0}.cogr ul.cotb li span span span,.cogr ul.cotb li.tabsel span span span,.cogr ul.cotb li a span span{padding-left:.333em}.cogr ul.cotb li a{background-color:#f1f1f1;border-width:0 1px;border-color:#fff;border-style:solid;padding:.5em 1.333em .25em 1.333em;text-decoration:none}.cogr ul.cotb li a,.cogr ul.cotb li a:link,.cogr ul.cotb li a:visited{color:#666}.cogr .js ul.cotb li a:hover{color:#666;text-decoration:none}.cogr ul.cotb li a:hover,.cogr ul.cotb li a:active{color:#333;text-decoration:none}.cogr .js ul.cotb li a.hover{color:#333;text-decoration:none}.cogr ul.cotb li.tabsel span{background-color:#fff;border-style:solid;border-width:4px 1px 0 1px;bottom:-1px;color:#333;cursor:default;margin-left:-1px;margin-top:-4px;padding:.417em 1.333em .417em 1.333em;position:relative}.coa2 .h2 a span,.coa2 .h3 a span,.cogr ul.cotb li span{cursor:pointer}.cogr ul.cotb li.first a{border-left:0}.cogr ul.cotb li.last a{border-right:0}.cogr ul.cotb li a.hover,.cogr ul.cotb li a:hover{text-decoration:none}.coa2 .h2 img,.cogr ul.cotb li img{display:block;float:left;margin-bottom:.083em}.coa3.ruled{border-bottom:solid 1px #e1e1e1}.coa3 .h2,.coa3 .h3{border-style:solid;line-height:normal}.coa3 .h2{border-width:0 0 2px 0;font-weight:bold;font-size:117%}.coa3 .h3{border-width:0 0 1px 0;font-weight:normal}.coa3 .h2 a,.coa3 .h3 a,.coa3 .h2 span,.coa3 .h3 span{display:block;float:left}.coa3 .h2 span span,.coa3 .h2 a span,.coa3 .h3 span span,.coa3 .h3 a span{padding:0}.coa3 .h2 a,.coa3 .h2 span{padding:.47em 0}.coa3 .h3 a,.coa3 .h3 span{padding:.8em 0}#wrapper .coa3 .attr,#wrapper .coa3 a.attr:link,#wrapper .coa3 a.attr:visited{color:#666;font-size:86%;font-weight:normal;float:right}#wrapper .coa3 a.attr:hover,#wrapper .coa3 a.attr:active{color:#333}.coa3 a.more{display:block;float:none;padding:.58em 0 0 0}.coa3 div.hr{border-top:solid 2px;margin:.833em 0}.coa3 .h2 a span,.coa3 .h3 a span{cursor:pointer}.coa3 .h2 a,.coa3 .h2 a:link,.coa3 .h2 a:visited,.coa3 .h2 span{color:#333}.coa3 .h3 a,.coa3 .h3 a:link,.coa3 .h3 a:visited,.coa3 .h3 span{color:#333}.coa3 .more,.coa3 a.more,.coa3 a.more:link,.coa3 a.more:visited{color:#333}.coa3 .h2 a:hover,.coa3 .h2 a:active{color:#000}.coa3 .h3 a:hover,.coa3 .h3 a:active{color:#000}.coa3 a.more:hover,.coa3 a.more:active{color:#000}.coa4.ruled{border-bottom:solid 1px #e1e1e1}.coa4 .h2,.coa4 .h3{line-height:normal}.coa4 .h2 a,.coa4 .h3 a,.coa4 .h2 span,.coa4 .h3 span{display:block;float:left;padding:.5em .86em .286em .86em}.coa4 .h2 a,.coa4 .h2 span{font-weight:normal}.coa4 .h3 a,.coa4 .h3 span{padding:0;font-weight:normal}#wrapper .coa4 .attr{font-size:92%;font-weight:normal;float:right}.coa4 a.more{display:block;float:none}.coa4 div.hr{border-top:solid 2px;margin:.833em 0}.coa4 .h2 a span,.coa4 .h3 a span{cursor:pointer}#wrapper .headerbar2 .br2{clear:none;padding-left:.417em}#wrapper .headerbar2 .br2,#wrapper .headerbar2 .br3{padding-top:1.25em;margin:0}.headerbar2 .br3 .wlcard1{margin-left:1.833em}#wrapper .headerbar2 .br4{clear:both;float:none}.headerbar2 .websearch2 input.text{width:350px}.co4b11 .b3{display:none}.co4b11 .br{display:block;float:left}.co4b11 .br2{clear:left}.co4b11 .more{clear:both}.co4b11 .br .more{clear:none}#sw_as{display:none;position:relative;z-index:100}input.text{-webkit-appearance:none;-webkit-border-radius:0}.websearch2 h2,.websearch2 label.hide{display:none}.websearch2 form{margin:0}.websearch2 input.image{border:0;cursor:pointer;display:block;float:left;margin:0;margin-left:3px;padding:0;text-align:right}.websearch2 input.text,.websearch2 select.dd{border:0;color:#333;display:block;float:left;margin:0;margin-top:7px;outline:none;padding:0;text-align:left;width:429px}.websearch2 input.txt1{border:solid 1px #c0c0c0;padding:5px 0 3px 3px;margin:1px 1px 0 -3px}.websearch2 input.txt2{border:solid 1px #c0c0c0;padding:5px 0 3px 3px;margin:1px 0 0 1px}.websearch2 select.dd{border:solid 1px #c0c0c0;border-top:solid 1px #a0a0a0;margin:1px 0 1px 1px;padding:3px 3px 3px 0}.websearch2 .opt,.websearch2 .scopes{font-family:verdana,sans-serif;font-size:100%}.websearch2 a,.websearch2 a:link,.websearch2 a:visited,.websearch2 a:hover,.websearch2 a:active,.websearch2 label,.websearch2 span,.websearch2 a:hover span{color:#fff}.websearch2 a:hover span{text-decoration:underline}.websearch2 span.bi{background-color:#fff;border:1px solid #2e6ba5;display:block;float:left;padding:0 0 0 1px}.websearch2 span.bo{border:2px solid #c7d9e9;clear:both;display:block;float:left}.websearch2 .opt{clear:both;color:#fff;margin-top:5px}.websearch2 .opt a,.websearch2 .opt label{margin:0 2px}.websearch2 .opt label{margin-left:5px;margin-right:11px}.websearch2 .opt a.first{margin:0}.websearch2 .opt input{margin:0 0 4px 0;padding:0;vertical-align:middle}.websearch2 .opt .delimited{display:inline}.websearch2 .opthide input,.websearch2 .opthide label{display:none}.websearch2 .scopes a{display:block;float:left;padding:2px 6px 4px 6px}.websearch2 .scopes a.selected,.websearch2 .scopes a.selected:hover{background:transparent url(../../i/07/617475cf39bf6f5c0bd6ecb985335c.gif) no-repeat 53% bottom;margin-bottom:0;padding-bottom:6px}.websearch2 .scopes a.selected,.websearch2 .scopes a.selected:hover,.websearch2 .scopes a.selected span,.websearch2 .scopes a.selected:hover span{color:#faae32;cursor:default;font-weight:bold;position:relative;text-decoration:none}.websearch2 .scopes span{display:inline;float:left;margin-top:1px}.websearch2 .scopes a span{display:inline-block;float:none;font-size:100%;margin-top:0;cursor:pointer}.websearch2 .scopes a span.icon1{background:transparent url(../../i/50/f63ed0301e8b02a8a42d8590a46291.gif) no-repeat right center;padding-right:30px}.websearch2 .scopes a span.icon2{background:transparent url(../../i/50/f63ed0301e8b02a8a42d8590a46291.gif) no-repeat right center;padding-right:30px}.websearch2 .scopes a span.icon3{background:transparent url(../../i/50/f63ed0301e8b02a8a42d8590a46291.gif) no-repeat right center;padding-right:30px}.websearch2 .scopes a span.icon4{background:transparent url(../../i/50/f63ed0301e8b02a8a42d8590a46291.gif) no-repeat right center;padding-right:30px}.websearch2 .scopes a span.icon5{background:transparent url(../../i/50/f63ed0301e8b02a8a42d8590a46291.gif) no-repeat right center;padding-right:30px}.wlcard1 div{font-size:117%;line-height:normal;text-align:right}.wlcard1 ul{float:left;list-style-type:none;margin:0;padding:0}.wlcard1 ul li{padding:0}.wlcard1 ul li.first{padding:0 0 .417em 0}.wlcard1 a,.wlcard1 a:link,.wlcard1 a:visited{color:#666}.wlcard1 a:hover,.wlcard1 a:active{color:#333}.wlcard1 ul li a span{color:#74a0c9}.linkedimglinklist8{list-style-type:none;margin:0;padding:0}.linkedimglinklist8 a{float:left}.linkedimglinklist8 a:hover{text-decoration:none}.linkedimglinklist8 img{border:none;margin-bottom:.4em}.linkedimglinklist8 li{float:left;margin:0 auto;padding:0 .5em;text-align:center}.linkedimglinklist8 a span{display:block}.richtext a,.richtext a:link,.richtext a:visited{text-decoration:underline}.richtext cite,.richtext dfn{font-style:normal}.richtext h4{margin:0 0 3px 0}.richtext p{margin:0 0 1em 0}.richtext code,.richtext samp,.richtext kbd{font-family:"courier new",courier,monospace;vertical-align:baseline}.ro{clear:left}.ro .ce{float:left;margin-left:.5em;margin-right:.5em;min-width:70px;width:5.833em}#wrapper .ro .ce1{margin-left:0}#wrapper .ro .cel{margin-right:0}.ro.m1,.ro .ce.m1{margin-top:1em}.ro.m3,.ro .ce.m3{margin-bottom:1em}.ro.m5,.ro .ce.m5{margin-bottom:1em;margin-top:1em}#wrapper .cogr .co{margin:0}#wrapper .cogr{margin:.667em}#wrapper .llmsg{text-align:center;padding-top:5em}#wrapper #page div.cotc{margin-bottom:18em}#wrapper div.tab h2{float:left}#wrapper div.tab div.tabchild{display:none;left:0;position:absolute;padding-top:.833em;top:2.167em}#wrapper div.cof div.tabchild{display:block}#wrapper div.cotc.cotch:hover div.cof div.tabchild{display:none}#wrapper div.cotc.cotch div.tab:hover div.tabchild{display:block}#wrapper div.cotc{background-color:#f1f1f1;border-bottom:1px solid;border-color:#009ad9;border-top:1px solid #f1f1f1;padding:.417em 0;position:relative}#wrapper div.cotc div.tab h2 span,#wrapper div.cotc.cotch:hover div.cof h2 span{background-color:#f1f1f1;border-color:#fff;border-style:solid;border-width:0 1px;bottom:-1px;padding:.5em 1.333em .25em;position:relative}#wrapper div.cotc div.cof h2 span,#wrapper div.cotc.cotch div.tab:hover h2 span{background-color:#fff;border-color:#009ad9;border-style:solid;border-width:4px 1px 0;bottom:-1px;color:#333;cursor:default;padding:.45em 1.333em;position:relative}#wrapper div.cotc div.tab h2 span span,#wrapper div.cotc.cotch:hover div.cof h2 span span,#wrapper div.cotc div.tab.cof h2 span span,#wrapper div.cotc.cotch div.tab:hover h2 span span{border:0 none;bottom:0;margin:0;margin-right:1px;padding:0;top:0}#wrapper #content .cossf{margin-bottom:3.33em}#wrapper #content .cossf .coss{margin-bottom:-25px}.cogr .coss ul{list-style-type:none;margin:0;padding:1px 0 0 0}.cogr .coss ul li{float:left;list-style-type:none}.cogr .coss ul li a{border:solid 1px #fff;display:block;height:19px;padding:0;text-decoration:none;width:18px}.cogr .coss ul li a.prev{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -730px}.cogr .coss ul li a.prev:hover{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -849px}.cogr .coss ul li a.next{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -968px}.cogr .coss ul li a.next:hover{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -1087px}.cogr .coss ul li a.pause{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -1206px;border-width:1px 0 1px 0;width:17px}.cogr .coss ul li a.pause:hover{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -1325px}.cogr .coss ul li a.play{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -1444px;border-width:1px 0 1px 0;width:17px}.cogr .coss ul li a.play:hover{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -1563px}.cogr .coss ul li a span{display:none;left:-10000px;position:relative}.cogr .coss ul li.last{color:#666;line-height:normal;padding:2px 0 4px;padding-left:12px}.ssa .as{background-color:#fff;left:54.5em;position:absolute;top:0;width:100%;z-index:1}.ssa .cof{left:0;z-index:2}.ssa{position:relative;overflow:hidden}.ssa .asn{left:0;position:absolute;z-index:3}.ssa .act{z-index:2}.ssa .nact{z-index:1}.date1{line-height:1.25em;margin:1.333em .667em .667em .667em}.date1 a,.date1 a:link,.date1 a:visited{color:#666}.date1 a:hover,.date1 a:active{color:#333}.blowoutmod1 .blowout1 h3{margin:0}.blowout1{border-bottom:solid 1px #ccc;padding:0 2em 1.333em 2em}.blowout1 div div{text-align:center}.blowout1 div div div{padding:.583em 0 0 0}.blowout1 h2{font-size:133%;line-height:1.13em;margin:0 0 .188em 0}.blowout1 h3{font-size:267%;font-weight:bold;line-height:1.09em}.blowout1 h3 a,.blowout1 h3 a:link,.blowout1 h3 a:visited{color:#333}.blowout1 h3 a:hover,.blowout1 h3 a:active{color:#000}.blowout1 p{color:#ccc;line-height:1.5em;margin:0;padding:0}.blowout1 ul{list-style-type:none;margin:.833em 0;padding:0}.blowout1 ul li{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -3683px;line-height:1.5em;padding:0 0 0 .75em}.breaknews1{font-size:133%;line-height:normal;padding:.625em .75em;text-align:center;margin:0 0 .667em}.breaknews1,.breaknews1 .richtext a,.breaknews1 .richtext a:link,.breaknews1 .richtext a:visited,.breaknews1 .richtext h4,.breaknews1 .richtext p,.breaknews1 .richtext span.custom{background-color:#ba1010;color:#fff;font-weight:bold}.breaknews1 .richtext h4{display:inline;margin:0 1em 0 0}.breaknews1 .richtext p{display:inline;margin:0}.navbar2 .br1 .menubar2{margin:0 0 0 8px}.navbar2 .br2 .menubar2{margin:0 0 0 9px}.navbar2 .br2 .menubar2 li.first{min-width:202px;width:16.833em}.navbar2 .br2 .menubar2 li.last{min-width:102px;width:8.5em}#wrapper .navbar2 .br2{min-width:316px;width:auto}.navbar2 .br2 .menubar2 li li.first,.navbar2 .br2 .menubar2 li li.last{min-width:142px;width:11.83em}.navbar2 .br2 .menubar2 li.fluid:hover ul,.navbar2 .br2 .menubar2 ul.js #hover.fluid ul{min-width:302px;width:25.167em}.navbar2 .br2 .menubar2 li.fluid ul li{min-width:0}.navbar2 .br2 .menubar2 li.fluid ul.mod2 li{width:50%}.navbar2 .br2 .menubar2 li.fluid ul.mod3 li{width:33.3%}.navbar2 .br2 .menubar2 li.fluid ul.mod4 li{width:25%}.navbar2 .br2 .menubar2 li.fluid ul.mod5 li{width:20%}.navbar2 .br2 .menubar2 li.fluid li a,.navbar2 .br2 .menubar2 li.fluid li a:link,.navbar2 .br2 .menubar2 li.fluid li a:hover,.navbar2 .br2 .menubar2 li.fluid li a:active,.navbar2 .br2 .menubar2 li.fluid li a:visited{font-size:100%;padding:.5em .25em .5em .571em}.co2b1 .br{float:left}.co2b1 .more{clear:both}.co2b1 .br .more{clear:none}.menubar2 ul,.menubar2 li{display:block;float:left;list-style:none;position:relative}.menubar2 ul{margin:0;padding:0;z-index:50}.menubar2 li{background-color:transparent;margin-right:1px;text-align:center}.menubar2 .snap li{min-width:102px;width:8.5em}.menubar2 .snap li.last{min-width:105px;width:8.75em}.menubar2 ul li.fluid:hover ul,.menubar2 ul.js #hover.fluid ul{left:auto;right:0;min-width:616px;width:51.333em}.menubar2 li a{outline:none}.menubar2 a:link,.menubar2 a:visited,.menubar2 a:active,.menubar2 a:hover,.menubar2 span{border-bottom:solid 4px;display:block;font-size:117%;font-weight:bold;line-height:normal;padding:.5em .571em}.menubar2 a:active,.menubar2 a:hover,.menubar2 a:hover span,.menubar2 span{text-decoration:none}.menubar2 span{cursor:default}.menubar2 li ul,.menubar2 ul.js li:hover ul,.menubar2 li ul li,.menubar2 .snap ul li.last{min-width:142px;width:11.833em}.menubar2 li ul,.menubar2 ul.js li:hover ul,.menubar2 ul.js li.last:hover ul{left:-999em;position:absolute;z-index:50}.menubar2 li ul{background-color:#fff;border:solid 1px}.menubar2 li li,.menubar2 .snap li li{margin:0;min-width:142px;position:static;text-align:left;width:11.833em}.menubar2 ul li:hover ul,.menubar2 ul.js #hover ul{left:0}.menubar2 li li:hover,.menubar2 ul.js li #hover,.menubar2 ul.js li li.focus{background-color:#e5e5e5}.menubar2 li li a:link,.menubar2 li li a:visited,.menubar2 li li a:active,.menubar2 li li a:hover{border:solid 1px #fff;font-weight:normal}.menubar2 li li span{font-weight:normal}.menubar2 .coc1 li a,.menubar2 .coc1 li span,.menubar2 .coc2 li a,.menubar2 .coc2 li span,.menubar2 .coc3 li a,.menubar2 .coc3 li span,.menubar2 .coc4 li a,.menubar2 .coc4 li span,.menubar2 .coc5 li a,.menubar2 .coc5 li span,.menubar2 .coc6 li a,.menubar2 .coc6 li span,.menubar2 .coc7 li a,.menubar2 .coc7 li span,.menubar2 .coc8 li a,.menubar2 .coc8 li span{color:#525151}.menubar2 .coc1 a,.menubar2 .coc1 span{border-color:#f57325;color:#f57325}.menubar2 .coc2 a,.menubar2 .coc2 span{border-color:#e44097;color:#e44097}.menubar2 .coc3 a,.menubar2 .coc3 span{border-color:#6a439c;color:#6a439c}.menubar2 .coc4 a,.menubar2 .coc4 span{border-color:#0253a2;color:#0253a2}.menubar2 .coc5 a,.menubar2 .coc5 span{border-color:#0191ce;color:#0191ce}.menubar2 .coc6 a,.menubar2 .coc6 span{border-color:#00aeff;color:#0296db}.menubar2 .coc7 a,.menubar2 .coc7 span{border-color:#58de81;color:#43ac63}.menubar2 .coc8 a,.menubar2 .coc8 span{border-color:#89c655;color:#73a846}.menubar2 .coc1 ul{border-color:#fde5d6}.menubar2 .coc2 ul{border-color:#ecd7e2}.menubar2 .coc3 ul{border-color:#e3dcec}.menubar2 .coc4 ul{border-color:#cfdfed}.menubar2 .coc5 ul{border-color:#cfeaf6}.menubar2 .coc6 ul{border-color:#cff0ff}.menubar2 .coc7 ul{border-color:#d5f7df}.menubar2 .coc8 ul{border-color:#cff4ec}.menubar2 ul ul span.custom{color:#333}.menubar2 li.last:hover ul,.menubar2 ul.js #hover.last ul{left:auto;right:0}.menubar2 .snap a:link,.menubar2 .snap a:visited,.menubar2 .snap a:active,.menubar2 .snap a:hover{padding:.5em 0}.menubar2 a span{padding:0;border:0;cursor:pointer;font-size:100%}.menubar2 .snap li li a:link,.menubar2 .snap li li a:visited,.menubar2 .snap li li a:active,.menubar2 .snap li li a:hover{padding:.5em .571em}.menubar2 li.fluid li a:link,.menubar2 li.fluid li a:visited,.menubar2 li.fluid li a:active,.menubar2 li.fluid li a:hover{border-top:none;border-left:none}.menubar2 li.fluid ul{padding-top:1px;padding-left:1px}.menubar2 li ul.mod2 li,.menubar2 .snap li ul.mod2 li.last{min-width:0;width:50%}.menubar2 li ul.mod3 li,.menubar2 .snap li ul.mod3 li.last{min-width:0;width:33.3%}.menubar2 li ul.mod4 li,.menubar2 .snap li ul.mod4 li.last{min-width:0;width:25%}.menubar2 li ul.mod5 li,.menubar2 .snap li ul.mod5 li.last{min-width:0;width:20%}.menubar2 li li.new a{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat right -232px}.menubar2 li li.beta a{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat right -344px}.menubar2 li li.new a,.menubar2 .snap li li.new a:link,.menubar2 .snap li li.new a:visited,.menubar2 .snap li li.new a:hover,.menubar2 .snap li li.new a:active{padding-right:33px}.menubar2 li li.beta a,.menubar2 .snap li li.beta a:link,.menubar2 .snap li li.beta a:visited,.menubar2 .snap li li.beta a:hover,.menubar2 .snap li li.beta a:active{padding-right:33px}.searchbar1{text-align:left}#wrapper .searchbar1 .br{margin:0;width:628px}.searchbar2{clear:both;text-align:center}#wrapper .searchbar2 .br{margin:0 auto;width:550px}.msnfoot1{border-top:solid 1px #cdcdcd;font-size:92%;line-height:normal;margin:1.083em .667em .667em .667em;padding:.5em 0 0 0}.msnfoot1 a,.msnfoot1 a:link,.msnfoot1 a:visited{color:#999}.msnfoot1 a:hover,.msnfoot1 a:active{color:#333}.msnfoot1 .primary li{float:left;display:block;padding:0 .833em}.msnfoot1 .primary li.first{padding-left:0}.msnfoot1 .secondary{text-align:right}.msnfoot1 .secondary a{white-space:pre}.msnfoot1 .secondary li{float:right;display:block;padding:0 .833em}.msnfoot1 .secondary li.first{padding-right:0}.msnfoot1 ul{list-style-type:none;margin:0;padding:0}.msnfoot1 .copyright{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat right 0;clear:right;color:#999;float:right;margin:1.5em 0 0 0}.msnfoot1 .copyright span{border-right:solid 1px #999;display:block;line-height:20px;margin-right:133px;padding-right:10px}.ad1 .adfb a,.ad1 .adfb a:link,.ad1 .adfb a:visited{color:#666;font-size:83%;line-height:1.5em}.ad1 .adfb a:hover,.ad1 .adfb a:active{color:#333}.adfb.left span,.adfb.left a.adch{float:left}.adfb.left a,.adfb.right a.adch{float:right}.adfb{color:#666;text-align:center;width:100%}.alert1 a,.alert1 a:link,.alert1 a:visited{color:#333}.alert1 a:hover,.alert1 a:active{color:#000}.linkedimglink1{display:block;margin:0;padding:0}.linkedimglink1 a{clear:right;display:block;float:left}.linkedimglink1 a span{cursor:pointer;float:left;padding-top:8px}.linkedimglink1 img{border:none;float:left;margin-right:6px}.linkedimglink2{margin:0;padding:0}.linkedimglink2 a{display:block}.linkedimglink2 a span{cursor:pointer;float:right;text-align:right;padding-top:8px}.linkedimglink2 img{border:none;margin-left:6px;float:right}.hlcp2 .pri .piped,.hlcp2 .pri a,.hlcp2 .pri a:link,.hlcp2 .pri a:visited{color:#333;font-size:150%;line-height:1.22em}.hlcp2 .pri a:hover,.hlcp2 .pri a:active{color:#000}.hlcp2 .sec .piped,.hlcp2 .sec a,.hlcp2 .sec a:link,.hlcp2 .sec a:visited{color:#333;font-size:117%;line-height:1.07em}.hlcp2 .sec a:hover,.hlcp2 .sec a:active{color:#000}#wrapper .hlcp2 p a,#wrapper .hlcp2 p a:link,#wrapper .hlcp2 p a:visited,#wrapper .hlcp2 p a:hover,#wrapper .hlcp2 p a:active{font-size:100%;line-height:100%;text-decoration:underline}.hlcp2 .pri .piped a,.hlcp2 .pri .piped a:link,.hlcp2 .pri .piped a:hover,.hlcp2 .pri .piped a:visited,.hlcp2 .pri .piped a:active,.hlcp2 .sec .piped a,.hlcp2 .sec .piped a:link,.hlcp2 .sec .piped a:hover,.hlcp2 .sec .piped a:visited,.hlcp2 .sec .piped a:active{font-size:100%;line-height:100%}.hlcp2 .pri div{margin:.833em 0 0 0}.hlcp2 .cf{margin:0 0 .583em}.hlcp2 .pri div div,.hlcp2 .pri .first{margin:0}.hlcp2 img{border:0;display:block}.hlcp2 .pri img.landscape{margin:0 0 .333em 0}.hlcp2 .sec img.landscape{margin:0 .833em .833em 0}.hlcp2 ul.right .sec img{float:right}.hlcp2 ul.left .sec img{float:left}.hlcp2 .richtext p{margin:0}.hlcp2 .pri .richtext{margin:.333em 0 0 0}.hlcp2 .sec .richtext{margin:.083em 0 0 0}.hlcp2 ul{list-style-type:none;margin:0;padding:0}.hlcp2 li.sec{display:inline-block;padding:0 0 1.167em 0}.hlcp2 li.sec.last{padding:0 0 1.167em}.hlcp2 li.ter,.hlcp2 li.media{border-top:solid 1px #e1e1e1;display:list-item;line-height:1.25em;margin:0;padding:.583em 0}.hlcp2 li.media{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2347px;padding-left:19px}.hlcp2 span.media{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2355px}.hlcp2 .pri span.media{background-image:none}.hlcp2 .pri span.piped span.media{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2351px}.hlcp2 .pri span.media a{margin-left:0}.hlcp2 span.media a,.hlcp2 .pri span.piped span.media a{margin-left:19px}.hlcp2 .date{color:#999;clear:both;float:left;padding:.938em 0 1.667em 0}.hlcp2 span.icon{display:inline-block;font-size:75%;margin-left:.25em;text-decoration:none}.hlcp2 .pri .piped span.icon{font-size:50%}.hlcp2 span.new{background:transparent url(../../i/77/b23a82d78a0605243aad8f44e8c079.gif) no-repeat right center;padding-right:12px;padding-top:.417em}.hlcp2 span.fresh1{background:transparent url(../../i/d7/fb6441a4c45cb3a3b2f592d914a3cd.gif) no-repeat right center;padding-right:31px;padding-top:.417em}.hlcp2 span.fresh2{background:transparent url(../../i/77/b23a82d78a0605243aad8f44e8c079.gif) no-repeat right center;padding-right:12px;padding-top:.417em}.hlcp2 span.fresh3{background:transparent url(../../i/77/b23a82d78a0605243aad8f44e8c079.gif) no-repeat right center;padding-right:12px;padding-top:.417em}.hlcp2 span.photo{background:transparent url(../../i/b9/ab98403e7de9ce52839e5de99d27e5.gif) no-repeat right center;padding-right:12px;padding-top:.417em}.hlcp2 span.dest1{background:transparent url(../../i/b9/ab98403e7de9ce52839e5de99d27e5.gif) no-repeat right center;padding-right:10px;padding-top:.417em}.hlcp2 span.dest2{background:transparent url(../../i/b9/ab98403e7de9ce52839e5de99d27e5.gif) no-repeat right center;padding-right:10px;padding-top:.417em}.hlcp2 span.dest3{background:transparent url(../../i/b9/ab98403e7de9ce52839e5de99d27e5.gif) no-repeat right center;padding-right:10px;padding-top:.417em}.hlcp2 .pri span.new{padding-top:.667em}.hlcp2 .pri span.fresh1{padding-top:.667em}.hlcp2 .pri span.fresh2{padding-top:.667em}.hlcp2 .pri span.fresh3{padding-top:.667em}.hlcp2 .pri span.photo{padding-top:.667em}.hlcp2 .pri span.dest1{padding-top:.667em}.hlcp2 .pri span.dest2{padding-top:.667em}.hlcp2 .pri span.dest3{padding-top:.667em}.menunavbar1{height:3.5em;margin-top:-1.667em;position:relative;width:100%}.menunavbar1 ul,.menunavbar1 li{display:block;float:left;list-style:none}.menunavbar1 ul{margin:0;padding:0;width:100%}.menunavbar1 li{text-align:center}.menunavbar1 ul li a{display:block;outline:none}.menunavbar1 a:link,.menunavbar1 a:visited,.menunavbar1 a:active{color:#fff;display:block;font-size:100%;font-weight:bold;line-height:normal;padding:.25em .8em;text-decoration:none}.menunavbar1 .ntier2 li{height:1.8em}.menunavbar1 ul .ntier2 li a{padding:0 .5em;color:#666;display:block}.menunavbar1 .ntier1 .ntier2{background-color:#fff;padding:0 1.5em 1.083em 0;padding-bottom:.3em;padding-top:.25em;width:79.5em}.menunavbar1 li ul,.menunavbar1 ul li:hover ul,.menunavbar1 ul li.last:hover ul{left:-999em;position:absolute}.menunavbar1 li li a:link,.menunavbar1 li li a:visited,.menunavbar1 li li a:hover,.menunavbar1 li li a:active{font-size:145%;font-weight:normal}.menunavbar1 ul.js li .showsm{display:block;z-index:10}.menunavbar1 ul.js li .hidesm{display:none;z-index:0}.menunavbar1 ul.js ul.notier li a{display:none}.menunavbar1 .ntier1 li:hover .ntier2,.menunavbar1 ul.js li.hover .ntier2,.menunavbar1 .ntier1 li.selected .ntier2{left:0;z-index:0}.menunavbar1 .js li:hover .ntier2{left:-999em}.menunavbar1 .ntier1 li:hover .ntier2{z-index:10}.menunavbar1 .ntier1 li:hover a,.menunavbar1 ul.js li.hover a,.menunavbar1 ul.ntier1 li.selected a{background-color:#fff;color:#666;outline:0}.menunavbar1 .js li:hover a{background-color:transparent;color:#fff}.menunavbar1 .js li:hover .ntier2 li a{background-color:#fff;color:#666}.menunavbar1 .ntier1 li .ntier2 li:hover a,.menunavbar1 ul.js li.hover .ntier2 li.focus a,.menunavbar1 .ntier1 li.selected .ntier2 li.highlighted a,.menunavbar1 .ntier1 li.selected .ntier2 li.focus a{color:#000;outline:0}.menunavbar1 .js li .ntier2 li:hover a{color:#666}.co4b5 .b3{display:none}.headlinelist2 div{float:left}.headlinelist2 ul{margin:0;padding:0}.headlinelist2 ul li{display:list-item;list-style-type:none;margin:0;padding:.417em 0}.headlinelist2 ul li.first{border-top:none;padding-top:0}.headlinelist2 ul li a{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2241px;display:block;line-height:1.25em;padding:0;padding-left:18px}.headlinelist2 span.media a{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2355px}.headlinelist3 div{float:left}.headlinelist3 ul{margin:0;padding:0}.headlinelist3 ul li{display:list-item;list-style-type:none;margin:0;padding:.417em 0}.headlinelist3 ul li.first{border-top:none;padding-top:0}.headlinelist3 ul li.first a{background-image:none;font-size:117%;font-weight:bold;padding-left:0}.headlinelist3 ul li a{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2241px;display:block;line-height:1.25em;padding:0;padding-left:18px}.headlinelist3 span.media a{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2355px}.imglinkabs1 img{float:left;margin-bottom:3px;margin-right:10px}.imglinkabs1 .media{display:inline}.imglinkabs1 p{margin:.2em 0 0 0}.imglinkabslist1{list-style-type:none;margin:0;padding:0}.imglinkabslist1 img{float:left;margin:0;margin-bottom:3px;margin-right:6px}.imglinkabslist1 li{margin:0 0 .9em 0}.imglinkabslist1 li.last{margin:0}.imglinkabslist1 .media{display:inline}.imglinkabslist1 div.richtext{margin:.2em 0 0 0}.imglinkabslist1 div.richtext p{margin:0}.imglinkabslist2{list-style-type:none;margin:0;padding:0}.imglinkabslist2 img{float:right;margin:0;margin-bottom:3px;margin-left:6px}.imglinkabslist2 li{margin:0 0 .9em 0}.imglinkabslist2 li.last{margin:0}.imglinkabslist2 div.richtext{margin:.2em 0 0 0}.imglinkabslist2 div.richtext p{margin:0}.imglinkabslist3{list-style-type:none;margin:0;padding:0}.imglinkabslist3 img{float:left;margin:0;margin-bottom:3px;margin-right:6px}.imglinkabslist3 li{margin:0 0 .9em 0}.imglinkabslist3 li.last{margin:0}.imglinkabslist3 .media{display:inline}.imglinkabslist3 div.richtext{border-top:solid 1px #ace;margin:.2em 0 0 0;padding:.3em 0 0 0}.imglinkabslist3 div.richtext p{margin:0}.imglinkabslist4{list-style-type:none;margin:0;padding:0}.imglinkabslist4 img{float:right;margin:0;margin-bottom:3px;margin-left:6px}.imglinkabslist4 li{margin:0 0 .9em 0}.imglinkabslist4 li.last{margin:0}.imglinkabslist4 div.richtext{border-top:solid 1px #ace;margin:.2em 0 0 0;padding:.3em 0 0 0}.imglinkabslist4 div.richtext p{margin:0}.imglinkabslist5{list-style-type:none;margin:0;padding:0}.imglinkabslist5 a,.imglinkabslist5 img{display:block;margin:0 0 3px 0}.imglinkabslist5 li{margin:0 0 .9em 0}.imglinkabslist5 li.last{margin:0}.imglinkabslist5 a{display:inline}.imglinkabslist5 div.richtext p{margin:0}.imglinkabslist6{list-style-type:none;margin:0;padding:0}.imglinkabslist6 a,.imglinkabslist6 img{display:block;margin:0 0 3px 0}.imglinkabslist6 li{margin:0 0 .9em 0}.imglinkabslist6 li.last{margin:0}.imglinkabslist6 div.richtext{border-top:solid 1px #ace;margin:0;padding:.3em 0 0 0}.imglinkabslist6 div.richtext a{display:inline}.imglinkabslist6 div.richtext p{margin:0}.imglinkabslist7{list-style-type:none;margin:0;padding:0}.imglinkabslist7 img{float:left;margin:0;margin-bottom:3px;margin-right:6px}.imglinkabslist7 li{display:block;float:left;margin:0;margin-bottom:.9em;margin-right:3%;width:47%}.imglinkabslist7 .media{display:inline}.imglinkabslist7 div.richtext{margin:.2em 0 0 0}.imglinkabslist7 div.richtext p{margin:0}.imglinkabslist8{list-style-type:none;margin:0;padding:0}.imglinkabslist8 img{float:right;margin:0;margin-bottom:3px;margin-left:6px}.imglinkabslist8 li{display:block;float:left;margin:0;margin-bottom:.9em;margin-right:3%;width:47%}.imglinkabslist8 div.richtext{margin:.2em 0 0 0}.imglinkabslist8 div.richtext p{margin:0}.imglinkabslist9{list-style-type:none;margin:0;padding:0}.imglinkabslist9 img{float:left;margin:0;margin-bottom:3px;margin-right:6px}.imglinkabslist9 li{display:block;float:left;margin:0;margin-bottom:.9em;margin-right:3%;width:47%}.imglinkabslist9 .media{display:inline}.imglinkabslist9 div.richtext{border-top:solid 1px #ace;margin:.2em 0 0 0;padding:.3em 0 0 0}.imglinkabslist9 div.richtext p{margin:0}.imglinkabslist10{list-style-type:none;margin:0;padding:0}.imglinkabslist10 img{float:right;margin:0;margin-bottom:3px;margin-left:6px}.imglinkabslist10 li{display:block;float:left;margin:0;margin-bottom:.9em;margin-right:3%;width:47%}.imglinkabslist10 div.richtext{border-top:solid 1px #ace;margin:.2em 0 0 0;padding:.3em 0 0 0}.imglinkabslist10 div.richtext p{margin:0}.imglinkabslist11{list-style-type:none;margin:0;padding:0}.imglinkabslist11 a,.imglinkabslist11 img{display:block;margin:0 0 3px 0}.imglinkabslist11 li{display:block;float:left;margin:0;margin-bottom:.9em;margin-right:3%;width:47%}.imglinkabslist11 div.richtext a{display:inline}.imglinkabslist11 div.richtext p{margin:0}.imglinkabslist12{list-style-type:none;margin:0;padding:0}.imglinkabslist12 a,.imglinkabslist12 img{display:block;margin:0 0 3px 0}.imglinkabslist12 li{display:block;float:left;margin:0;margin-bottom:.9em;margin-right:3%;width:47%}.imglinkabslist12 div.richtext{border-top:solid 1px #ace;margin:0;padding:.3em 0 0 0}.imglineabslist12 div.richtext a{display:inline}.imglinkabslist12 div.richtext p{margin:0}.imglinkabslist13{list-style-type:none;margin:0;padding:0}.imglinkabslist13 img{float:left;margin:0;margin-bottom:3px;margin-right:6px}.imglinkabslist13 li{display:block;float:left;margin:0;margin-bottom:.9em;margin-right:3%;min-width:90px;width:28%}.imglinkabslist13 .media{display:inline}.imglinkabslist13 div.richtext{margin:.2em 0 0 0}.imglinkabslist13 div.richtext p{margin:0}.imglinkabslist14{list-style-type:none;margin:0;padding:0}.imglinkabslist14 img{float:right;margin:0;margin-bottom:3px;margin-left:6px}.imglinkabslist14 li{display:block;float:left;margin:0;margin-bottom:.9em;margin-right:3%;min-width:90px;width:28%}.imglinkabslist14 div.richtext{margin:.2em 0 0 0}.imglinkabslist14 div.richtext p{margin:0}.imglinkabslist15{list-style-type:none;margin:0;padding:0}.imglinkabslist15 img{float:left;margin:0;margin-bottom:3px;margin-right:6px}.imglinkabslist15 li{display:block;float:left;margin:0;margin-bottom:.9em;margin-right:3%;min-width:90px;width:28%}.imglinkabslist15 .media{display:inline}.imglinkabslist15 div.richtext{border-top:solid 1px #ace;margin:.2em 0 0 0;padding:.3em 0 0 0}.imglinkabslist15 div.richtext p{margin:0}.imglinkabslist16{list-style-type:none;margin:0;padding:0}.imglinkabslist16 img{float:right;margin:0;margin-bottom:3px;margin-left:6px}.imglinkabslist16 li{display:block;float:left;margin:0;margin-bottom:.9em;margin-right:3%;min-width:90px;width:28%}.imglinkabslist16 div.richtext{border-top:solid 1px #ace;margin:.2em 0 0 0;padding:.3em 0 0 0}.imglinkabslist16 div.richtext p{margin:0}.imglinkabslist17{list-style-type:none;margin:0;padding:0}.imglinkabslist17 a,.imglinkabslist17 img{display:block;margin:0 0 3px 0}.imglinkabslist17 li{display:block;float:left;margin:0;margin-bottom:.9em;margin-right:3%;min-width:90px;width:28%}.imglinkabslist17 div.richtext a{display:inline}.imglinkabslist17 div.richtext p{margin:0}.imglinkabslist18{list-style-type:none;margin:0;padding:0}.imglinkabslist18 img{display:block;margin:0 0 3px 0}.imglinkabslist18 li{display:block;float:left;margin:0;margin-bottom:.9em;margin-right:3%;min-width:90px;width:28%}.imglinkabslist18 div.richtext{border-top:solid 1px #ace;margin:0;padding:.3em 0 0 0}.imglinkabslist18 div.richtext p{margin:0}.imglinkabslist19{list-style-type:none;margin:0;padding:0}.imglinkabslist19 img{float:left;margin:0;margin-bottom:3px;margin-right:6px}.imglinkabslist19 li{display:block;float:left;margin:0;margin-bottom:.9em;margin-right:3%;min-width:90px;width:21%}.imglinkabslist19 .media{display:inline}.imglinkabslist19 div.richtext{margin:.2em 0 0 0}.imglinkabslist19 div.richtext p{margin:0}.imglinkabslist20{list-style-type:none;margin:0;padding:0}.imglinkabslist20 img{float:right;margin:0;margin-bottom:3px;margin-left:6px}.imglinkabslist20 li{display:block;float:left;margin:0;margin-bottom:.9em;margin-right:3%;min-width:90px;width:21%}.imglinkabslist20 div.richtext{margin:.2em 0 0 0}.imglinkabslist20 div.richtext p{margin:0}.imglinkabslist21{list-style-type:none;margin:0;padding:0}.imglinkabslist21 img{float:left;margin:0;margin-bottom:3px;margin-right:6px}.imglinkabslist21 li{display:block;float:left;margin:0;margin-bottom:.9em;margin-right:3%;min-width:90px;width:21%}.imglinkabslist21 .media{display:inline}.imglinkabslist21 div.richtext{border-top:solid 1px #ace;margin:.2em 0 0 0;padding:.3em 0 0 0}.imglinkabslist21 div.richtext p{margin:0}.imglinkabslist22{list-style-type:none;margin:0;padding:0}.imglinkabslist22 img{float:right;margin:0 0 3px 0;margin-left:6px}.imglinkabslist22 li{display:block;float:left;margin:0;margin-bottom:.9em;margin-right:3%;min-width:90px;width:21%}.imglinkabslist22 div.richtext{border-top:solid 1px #ace;margin:.2em 0 0 0;padding:.3em 0 0 0}.imglinkabslist22 div.richtext p{margin:0}.imglinkabslist23{list-style-type:none;margin:0;padding:0}.imglinkabslist23 a,.imglinkabslist23 img{display:block;margin:0 0 3px 0}.imglinkabslist23 li{display:block;float:left;margin:0;margin-bottom:.9em;margin-right:3%;min-width:90px;width:21%}.imglinkabslist23 div.richtext a{display:inline}.imglinkabslist23 div.richtext p{margin:0}.imglinkabslist24{list-style-type:none;margin:0;padding:0}.imglinkabslist24 a,.imglinkabslist24 img{display:block;margin:0 0 3px 0}.imglinkabslist24 li{display:block;float:left;margin:0;margin-bottom:.9em;margin-right:3%;width:21%}.imglinkabslist24 div.richtext{border-top:solid 1px #ace;margin:0;padding:.3em 0 0 0}.imglinkabslist24 div.richtext a{display:inline}.imglinkabslist24 div.richtext p{margin:0}.linkedimglinklist13{list-style-type:none;margin:0;padding:0}.linkedimglinklist13 li{display:block;float:left;margin-bottom:.9em;margin-right:3%;position:relative;width:47%}.linkedimglinklist13 a{display:block}.linkedimglinklist13 a:after{content:".";clear:both;display:block;height:0;visibility:hidden}.linkedimglinklist13 img{border:none;float:left;margin-bottom:.4em;margin-right:.4em}.linkedimglinklist13 a span{cursor:pointer;padding-top:6px}.linklist9{list-style-type:none;margin:0;padding:0}.linklist9 a{white-space:pre}.linklist9 li{background:transparent url(../../i/f8/614595fba50d96389708a4135776e4.gif) repeat-y 100% 0;float:left;margin:0 0 2px 0;margin-right:.7em;padding:0;padding-right:.7em}.linklist9 li.last{background-image:none;margin:0;padding:0}.linklist16{list-style-type:none;margin:0;padding:0}.linklist16 li{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -3681px;margin:0;padding:.25em 0 .2em 9px}.linkimgabs1 a{display:block;margin:0 0 .4em 0}.linkimgabs1 br{display:none}.linkimgabs1 div.richtext br{display:inline}.linkimgabs1 img{display:block;float:left;margin-bottom:3px;margin-right:6px}.linkimgabs1 div.richtext a{display:inline}.conban1{position:relative}.conban1 .close{left:980px;top:0}.headerbar_us{height:9.833em;min-height:118px}.headerbar_us .pageoptions1 .welcome{text-align:right}.headerbar_us a,.headerbar_us a:link,.headerbar_us a:visited,.headerbar_us a:hover,.headerbar_us a:active,.headerbar_us label,.headerbar_us span,.headerbar_us a:hover span,.headerbar_us .br2 .welcome{color:#666}.headerbar_us .br1{float:left;width:54em}.headerbar_us .br1 .linklist1 li{float:left;padding:.25em 1.5em .2em 0}.headerbar_us .br2{float:right}.headerbar_us .br3{float:right;margin-right:.5em}.headerbar_us .br4{clear:both;float:left;min-width:208px;padding-top:.5em;width:17.33em}.headerbar_us .br5{float:left}.headerbar_us .br3 a,.headerbar_us .br3 a:link,.headerbar_us .br2 .welcome{font-weight:bold}.headerbar_us .m2{margin-top:.25em}.rstkq1 .rstockq1 h4{float:left;margin-right:.333em}.rstkq1 a,.rstkq1 a:link,.rstkq1 a:visited{color:#666}.co3b5 .br{float:left}.co3b5 .br2{clear:right;float:right}.co3b5 .br3,.co3b5 .more{clear:left}.co3b5 .br .more{clear:none}.co3b6 .br{clear:right;float:right}.co3b6 .br1{float:left}.co3b6 .more{clear:both;float:none}.co3b6 .br .more{clear:none}.co4b1 .br{float:left}.co4b1 .br2{clear:right}.co4b1 .br3{clear:left}.co4b1 .more{clear:both}.co4b1 .br .more{clear:none}.co4b1 .b3{display:block}.co4b2 .b3{display:none}.co4b2 .br{float:left}.co4b2 .more{clear:both}.co4b2 .br .more{clear:none}.co4b8 .b3{display:none}.co4b8 .br{float:left}.co4b8 .br3,.co4b8 .br4{clear:both;display:block;float:none}.co4b8 .more{clear:both}.co5b9 .b3,.co5b9 .b4{display:none}.co6b1 .b3,.co6b1 .b5{display:none}.co6b1 .b4{display:block}.co6b1 .br{float:left}.co6b1 .br4{clear:left}.co6b1 .more{clear:left}.co6b1 .br .more{clear:none}.hlcp1 .pri .piped,.hlcp1 .pri a,.hlcp1 .pri a:link,.hlcp1 .pri a:visited{color:#333;font-size:150%;line-height:1.22em}.hlcp1 .pri a:hover,.hlcp1 .pri a:active{color:#000}.hlcp1 .sec .piped,.hlcp1 .sec a,.hlcp1 .sec a:link,.hlcp1 .sec a:visited{color:#333;font-size:117%;line-height:1.07em}.hlcp1 .sec a:hover,.hlcp1 .sec a:active{color:#000}#wrapper .hlcp1 p a,#wrapper .hlcp1 p a:link,#wrapper .hlcp1 p a:visited,#wrapper .hlcp1 p a:hover,#wrapper .hlcp1 p a:active{font-size:100%;line-height:100%;text-decoration:underline}.hlcp1 .pri .piped a,.hlcp1 .pri .piped a:link,.hlcp1 .pri .piped a:hover,.hlcp1 .pri .piped a:visited,.hlcp1 .pri .piped a:active,.hlcp1 .sec .piped a,.hlcp1 .sec .piped a:link,.hlcp1 .sec .piped a:hover,.hlcp1 .sec .piped a:visited,.hlcp1 .sec .piped a:active{font-size:100%;line-height:100%}.hlcp1 .pri div{margin:.833em 0 0 0}.hlcp1 .cf{margin:0 0 .583em}.hlcp1 .pri div div,.hlcp1 .pri .first{margin:0}.hlcp1 img{border:0;display:block}.hlcp1 .pri img.landscape{margin:0 0 .333em 0}.hlcp1 .sec img.landscape{margin:0 .833em .833em 0}.hlcp1 ul.right .sec img{float:right}.hlcp1 ul.left .sec img{float:left}.hlcp1 .richtext p{margin:0}.hlcp1 .pri .richtext{margin:.333em 0 0 0}.hlcp1 .sec .richtext{margin:.083em 0 0 0}.hlcp1 ul{list-style-type:none;margin:0;padding:0}.hlcp1 li.sec{display:inline-block;padding:0 0 1.167em 0}.hlcp1 li.sec.last{padding:0 0 1.167em}.hlcp1 li.ter,.hlcp1 li.media{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2236px;display:list-item;line-height:1.333em;margin:0;padding:.333em 0 .333em 1.583em}.hlcp1 li.media{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2350px}.hlcp1 span.media{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2355px}.hlcp1 .pri span.media{background-image:none}.hlcp1 .pri span.piped span.media{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2351px}.hlcp1 .pri span.media a{margin-left:0}.hlcp1 span.media a,.hlcp1 .pri span.piped span.media a{margin-left:19px}.hlcp1 .date{color:#999;clear:both;float:left;padding:.938em 0 1.667em 0}.hlcp1 span.icon{display:inline-block;font-size:75%;margin-left:.25em;text-decoration:none}.hlcp1 .pri .piped span.icon{font-size:50%}.hlcp1 span.new{background:transparent url(../../i/77/b23a82d78a0605243aad8f44e8c079.gif) no-repeat right center;padding-right:12px;padding-top:1px}.hlcp1 span.fresh1{background:transparent url(../../i/77/b23a82d78a0605243aad8f44e8c079.gif) no-repeat right center;padding-right:12px;padding-top:1px}.hlcp1 span.fresh2{background:transparent url(../../i/77/b23a82d78a0605243aad8f44e8c079.gif) no-repeat right center;padding-right:12px;padding-top:1px}.hlcp1 span.fresh3{background:transparent url(../../i/77/b23a82d78a0605243aad8f44e8c079.gif) no-repeat right center;padding-right:12px;padding-top:1px}.hlcp1 span.photo{background:transparent url(../../i/b9/ab98403e7de9ce52839e5de99d27e5.gif) no-repeat right center;padding-right:10px;padding-top:.25em}.hlcp1 span.dest1{background:transparent url(../../i/b9/ab98403e7de9ce52839e5de99d27e5.gif) no-repeat right center;padding-right:12px;padding-top:.25em}.hlcp1 span.dest2{background:transparent url(../../i/b9/ab98403e7de9ce52839e5de99d27e5.gif) no-repeat right center;padding-right:12px;padding-top:.25em}.hlcp1 span.dest3{background:transparent url(../../i/b9/ab98403e7de9ce52839e5de99d27e5.gif) no-repeat right center;padding-right:12px;padding-top:.25em}.hlcp1 .pri span.new{padding-top:.667em}.hlcp1 .pri span.fresh1{padding-top:.667em}.hlcp1 .pri span.fresh2{padding-top:.667em}.hlcp1 .pri span.fresh3{padding-top:.667em}.hlcp1 .pri span.photo{padding-top:.667em}.hlcp1 .pri span.dest1{padding-top:.667em}.hlcp1 .pri span.dest2{padding-top:.667em}.hlcp1 .pri span.dest3{padding-top:.667em}.imgmap1 img{border:none}.linklist1{list-style-type:none;margin:0;padding:0}.linklist1 li{margin:0;padding:.25em 0 .2em 0}.linklist2{list-style-type:square;margin:0;margin-left:18px;padding:0}.linklist2 li{margin:0;padding:.25em 0 .2em 0}.linklist13{list-style-type:none;margin:0;padding:0}.linklist13 li{display:block;float:left;margin:0;margin-right:1%;padding:.25em 0 .2em 0;width:29%}.linklist14{list-style-type:none;margin:0;padding:0}.linklist14 li{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -3681px;display:block;float:left;margin:0 1% 0 0;padding:.25em 0 .2em 9px;width:44%}.linklist15{list-style-type:none;margin:0;padding:0}.linklist15 li{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -3681px;display:block;float:left;margin:0 1% 0 0;padding:.25em 0 .2em 9px;width:27%}.condbanner1 a.close{display:inline;padding:0 32px 32px 0;position:absolute;margin:0 0 0 -32px}.condbanner1 a.white{background:0 -466px transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat}.condbanner1 a.black{background:0 -598px transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat}.pgopt1{float:right;clear:both}.pgopt1 .opt,.pgopt1 .signin{display:inline;float:left;font-size:83.33%;line-height:1.6em;height:1.6em;min-height:16px}.pgopt1 li a,.pgopt1 li span{border-bottom:solid 2px #fff}.pgopt1 li li a,.pgopt1 li li span{border-bottom:none}.pgopt1 .opt ul{font-size:120%;line-height:1.333em;top:1.333em}.pgopt1 .user ul{top:1.417em}.pgopt1 .user div a,.pgopt1 .user div span{font-size:100%;font-weight:bold}.pgopt1 .user div ul a{font-weight:normal}.pgopt1 ul,.pgopt1 li{display:block;margin:0;padding:0;text-align:right;list-style:none}.pgopt1 li li{background-color:#fff;display:block;width:100%;text-align:left}.pgopt1 li a,.pgopt1 .js li:hover a{border-color:#fff}.pgopt1 li a span,.pgopt1 li a:link,.pgopt1 li a:visited,.pgopt1 li a:active,.pgopt1 li a:visited,.pgopt1 .js li:hover a:link,.pgopt1 .js li:hover a:visited,.pgopt1 .js li:hover a:hover,.pgopt1 .js li:hover a:active{text-decoration:none}.pgopt1 .pipe{border-left:solid 1px #999;margin-left:.417em;padding-left:.417em}.pgopt1 li.signin:hover a,.pgopt1 .js li#hover.signin a:hover{text-decoration:underline}.pgopt1 li.signin:hover a,.pgopt1 li.signin a:hover,.pgopt1 .js li#hover.signin a{border-bottom-color:#fff}.pgopt1 li:hover a,.pgopt1 li a:hover,.pgopt1 .js #hover a{border-bottom-color:#666}.pgopt1 ul li:hover ul,.pgopt1 ul.js #hover ul{left:auto;right:0;z-index:110}.pgopt1 li li:hover,.pgopt1 .js li li#hover,.pgopt1 li li.focus{background-color:#f1f1f1}.pgopt1 li:hover li a,.pgopt1 li#hover li a{border:none}.pgopt1 .user div,.pgopt1 .opt div{position:relative}.pgopt1 li ul,.pgopt1 ul.js li:hover ul{left:-999em;right:auto;z-index:auto}.pgopt1 .user ul,.pgopt1 .opt ul{background-color:#fff;border:solid 1px #666;position:absolute}.pgopt1 li li a,.pgopt1 ul.js li li a{display:block;float:none;padding:.417em 1.417em;min-width:132px;white-space:nowrap}.pgopt1 li li a.checked{background:transparent url(../../i/ff/290e7f0b12fa8a201581c74c1ae75a.gif) no-repeat .417em center}.pgopt1 li li.separator#hover,.pgopt1 li li.separator:hover{background-color:transparent}.pgopt1 li li.separator{font-size:10%;height:1px;line-height:1px;min-height:1px;position:absolute;z-index:100}.pgopt1 li li.separator div{border:none;border-top:1px solid #666;height:1px;margin:0 17px}.rstockq1 a,.rstockq1 h4,.rstockq1 span{margin:0;padding:0}.esbm,.esb{display:none}.esh .esbm{display:block}.esh .esb{background:#b2bdc4;display:block;font-size:12px}.esh #head .ro .ce{position:inherit}.esh #srchfrm{position:relative;z-index:110}.esh .scopes a{color:#fff}.esh #srchfrm .opt{display:none}.headerbar_us .esbc span,.headerbar_us .esbc a:hover span{color:#fff}.esbm{background:#333;height:100%;position:absolute;width:100%;z-index:110}.esbtn{background-image:url("../../i/61/379589e51e05637f600f129f305b52.png");cursor:pointer;height:23px;position:absolute;right:-23px;top:0;width:23px}.esbc{background-color:#b2bdc4;position:relative}.esbc ul,.esbc ul{list-style:none outside none;margin:0;padding:0}.esbc .cbg{line-height:0}.esbc .mbg{background:none repeat scroll 0 0 #000;height:100%;position:absolute;width:100%;z-index:1}.esbc .mlogo{background:0 0 url(../../i/61/def0ebad64d00fda0702cb7b8179ea.png) no-repeat;float:left;height:32px;margin:14px 0 2px 14px;width:90px}.esbc .cmenu{height:100%;position:absolute;top:0;width:113px}.menus{top:48px;position:absolute;z-index:10}.esh .menus li a{color:#fff}.menus li a{display:block;padding:10px 14px}.menus li .sc_pc{background:#333;border:1px solid #777;display:block;left:105px;padding:.5em;position:absolute;margin-top:-45px;width:22em;white-space:nowrap}.menus li.col3 .sc_pc{margin-top:-55px}.menus li.col4 .sc_pc{margin-top:-65px}.menus li a span{background-color:#fff;display:block;height:26px;left:0;margin-top:-6px;position:absolute;width:100%}.menus .sc_pc a{padding:2px 5px}.menus .sc_pc h3{color:#999;padding:5px}.menus li .sc_pca{color:#fff;padding-top:10px}.menus li .sc_pca a{display:inline;padding:5px}.sl_sh{font-size:13px}.sh_hto{background:none repeat scroll 0 0 #000;height:37px;padding:1px;width:37px}.sh_hs p{color:#fff;margin:0 0 5px}.sh_hto div{border:1px solid #fff;float:left;height:35px;width:35px}.sh_hst{position:absolute;z-index:5}a.sh_hs{cursor:pointer;display:block;line-height:1.4em;padding:5px 5px 5px 10px;position:absolute;width:219px;z-index:15}a.sh_hs:hover{text-decoration:none}.sh_ho{background:none repeat scroll 0 0 #000;left:0;padding:1px;position:absolute;top:0;width:100%;z-index:-1}.sh_ho div{border:1px solid #fff}.sh_hq{text-decoration:underline}.sh_hs span.sh_hi,a.sh_hs:hover span.sh_hi{color:#ffa500;text-decoration:none}.sh_rdiv{bottom:10px;font-size:11px;position:absolute;right:5px}.sh_rdiv a{cursor:default;display:block;float:left;margin:0 3px;outline:medium none;position:relative;text-decoration:none}#head .sh_rdiv a:hover span{display:block;text-decoration:none}.sc_light{border:1px solid;cursor:pointer;display:block;text-indent:3px;height:18px;width:17px}.dis .sc_light{cursor:default}#head .sc_msg{color:#000}.sc_msg{background:none repeat scroll 0 0 #fff;border:1px solid #555;bottom:2em;display:none;padding:.2em .5em;position:absolute;right:.18em;white-space:nowrap}.sh_rdiv .copy{font-size:14px}.sh_rdiv .sh_igc{margin:1px 2px 0 5px}.sh_igc .sc_msg{bottom:1.82em;white-space:normal;width:300px}.esbf{background:none repeat scroll 0 0 #d0d9dd;padding:14px 4px 10px}.esbf h3{color:#737373;font-size:16px;font-weight:200;margin-left:6px}.esbf ul{margin:0;padding:9px 0 0;list-style:none outside none}.esbf li{float:left;font-size:14px;font-weight:700}.esb .esbf li a{color:#000}.esbf a,.esbf a:hover{line-height:1.2em;font-weight:200;margin:0 6px}.es2 .menus li a{padding:7px 14px}.es2 .menus .sc_pc a{padding:2px 5px}.esh .es2 #sa_drw li{padding:7px}.dating1{background-color:#f7f7f7;padding:0;margin:.667em 0 .667em .333em}#wrapper .dating1 .h2{border-bottom:none;margin:0 0 .83em;background-color:#fff;padding-bottom:.4em}.dating1 br{display:none}#wrapper .dating1 .br2{padding-top:0}.dating1 .linkedimglinklist8 img{margin-bottom:0}.dating1 .linkedimglinklist8 li{padding:0 .333em 0 0;margin:0;text-align:left}.dating1 .linkedimglinklist8 li.last{padding:0}.w4 .dating1 .linkedimglinklist8 li{width:6.32em}.w4 .dating1 .linkedimglinklist8 li.last{width:5em}.dating1 .linkedimglinklist8 a span{font-size:92%;line-height:1em}.dating1 .complex1 .dddiv1,.dating1 .complex1 .dddiv2{float:left}.dating1 .complex1 .dddiv1 select,.dating1 .complex1 .dddiv2 select,.dating1 .complex1 .dddiv3,.dating1 .complex1 .dddiv5{clear:left}.dating1 .complex1 .dddiv2{margin-left:1.5em}.dating1 .complex1 .dddiv3,.dating1 .complex1 .dddiv5{padding-top:.583em}.dating1 .complex1 .dddiv4{margin-top:0}.dating1 .complex1 .dddiv1 label,.dating1 .complex1 .dddiv2 label{margin-bottom:.25em}.dating1 .complex1 .dddiv3 label,.dating1 .complex1 .dddiv4 label,.dating1 .complex1 .dddiv5 label{margin-top:.167em}.dating1 .complex1 label,.dating1 .complex1 select{color:#666;margin-right:1.25em;margin-bottom:0}.dating1 .complex1 label{margin-top:0}.dating1 .complex1 .button{background-color:#36b701;border:solid 1px #92b0dd;color:#fff;font-weight:bold}#wrapper .dating1 .br4{padding-top:1.25em}.dating1 fieldset #mygender{clear:left}.dating1 fieldset #theirgenderlbl{float:none}.dating1 fieldset#cff1 label{margin-right:7.9em;margin-top:0}.dating1 fieldset#cff1 select{margin-right:1.667em}.dating1 fieldset{padding:.5em 0 0}.dating1 fieldset.last{padding-bottom:.5em}.dating1 fieldset.last select{width:auto}.dating2{background-color:#fff0fb;padding:0}#wrapper .dating2 .h2{border-bottom:0;margin:0 0 0 .641em}.dating2 br{display:none}.dating2 .br1,.dating2 .br2{padding:0 0 0 .75em}#wrapper .dating2 .br2{padding-top:0}.dating2 .linkedimglinklist8 li{padding:0 1.222em 0 0;margin:0;text-align:left}.dating2 .linkedimglinklist8 li.last{padding:0}.w4 .dating2 .linkedimglinklist8 li{width:5em}.w4 .dating2 .linkedimglinklist8 li.last{width:5em}.dating2 .linkedimglinklist8 a span{font-size:92%;line-height:1em}.dating2 .complex1 .dddiv1,.dating2 .complex1 .dddiv2{float:left}.dating2 .complex1 .dddiv1 select,.dating2 .complex1 .dddiv2 select,.dating2 .complex1 .dddiv3,.dating2 .complex1 .dddiv5{clear:left}.dating2 .complex1 .dddiv2{margin-left:1.5em}.dating2 .complex1 .dddiv3,.dating2 .complex1 .dddiv5{padding-top:.583em}.dating2 .complex1 .dddiv4{margin-top:0}.dating2 .complex1 .dddiv1 label,.dating2 .complex1 .dddiv2 label{margin-bottom:.25em}.dating2 .complex1 .dddiv3 label,.dating2 .complex1 .dddiv4 label,.dating2 .complex1 .dddiv5 label{margin-top:.167em}.dating2 .complex1 label,.dating2 .complex1 select{color:#666;margin-right:.667em;margin-bottom:0}.dating2 .complex1 .button{background-color:#36b701;border:solid 1px #92b0dd;color:#fff;font-weight:bold}#wrapper .dating2 .br4{padding-top:1.25em}.dating2 fieldset #mygender{clear:left}.dating2 fieldset #theirgenderlbl{float:none}.dating2 fieldset#cff1 label{margin-right:5.2em}.dating2 fieldset#cff1 select{margin-right:2.2em}.dating2 fieldset{padding:.5em 0 0}.dating2 fieldset.last{padding-bottom:.5em}.complex1,.complex1 p{margin:0;padding:0}.complex1 fieldset{border:none;clear:both}.complex1 fieldset.last div{margin-top:0}.complex1 cite{display:block;font-style:normal}.complex1 label,.complex1 select,.complex1 input,.complex1 textarea{float:left;margin-right:.2em}.complex1 cite,.complex1 div{margin-top:.4em}.complex1 select{font-size:100%}.complex1 input{font-size:100%;line-height:1.25em}.complex1 input.alt{float:right}.linkedimg1 a:hover{text-decoration:none}.dhppromo1 .adfb{color:#666;text-align:center;width:100%}.co5b18 .b3,.co5b18 .b4{display:none}.co5b18 .br{float:left}.co5b18 .br1,.co5b18 .br5{clear:both;display:block;float:none}.co5b18 .more{clear:both;float:none}#wrapper .hotmail1{margin:.667em .667em 1.667em .667em}#wrapper .hotmail1 .h2{margin:0}.hminbox1 .expands{display:none}.hminbox1 p{line-height:1.333em;margin:.833em 0}.hminbox1 table{border-collapse:collapse;border-bottom:solid 2px #ccebf7;width:100%}.hminbox1 caption,.hminbox1 thead{display:none}.hminbox1 td{border-bottom:solid 1px #e1e1e1;padding:6px 0}.hminbox1 td.rec{text-align:right}.hminbox1 td.msg{padding-left:27px}.hminbox1 tr.unread td.msg{font-weight:bold;background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2461px}.hminbox1 tr.unread td.rec span.date{font-weight:bold}.hminbox1 tr.read td.msg{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2573px}.hminbox1 tr.replied td.msg{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -1674px}.hminbox1 tr.forwarded td.msg{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -1785px}.hminbox1 tr.attached td.msg{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -1897px}.hminbox1 tr.msn td.msg{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2009px}.hminbox1 tr.courier td.msg{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2121px}.hminbox1 tr.prilow td.msg{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 5px -3461px}.hminbox1 tr.prihigh td.msg{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 6px -3571px}.hminbox1 td.msg span,.hminbox1 td.rec span.time{display:block;font-weight:normal;margin-top:.5em}.hminbox1 td.rec span.date{color:#333}.hminbox1 td.rec span.time{color:#999;line-height:1.5em;white-space:nowrap}.hminbox1 ul.greet{border-bottom:solid 2px #ccebf7;margin:0;padding:.417em 0}.hminbox1 p.teaser{border-top:solid 1px #e1e1e1;margin-top:0;padding:.583em 0 0 0}.hminbox1 div.logo{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -116px;float:none;height:30px;width:90px}.hminbox1 ul.actions{float:right;margin:.54em 0;padding:0}.hminbox1 div.hr{margin-top:0}.hminbox1 li{border-right:solid 1px #e1e1e1;display:block;float:left;margin:0;padding:0 1.083em}.hminbox1 li.last{border-right:none;padding-right:0}.hminbox1 li.first{padding-left:0}.hminbox1 ul.greet li{border-right:none;padding:0;width:50%}.hminbox1 ul.greet li.last{text-align:right}.hminbox1 ul.actions li a.hide{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat right -3032px;padding-right:15px}.hminbox1 ul.actions li a.hide:hover{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat right -3141px}.hminbox1 ul.actions li a.show{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat right -3250px;padding-right:15px}.hminbox1 ul.actions li a.show:hover{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat right -3359px}.hminbox1 ul.greet li.first a,.hminbox1 ul.greet li.first a:link,.hminbox1 ul.greet li.first a:visited{color:#333;font-size:117%;line-height:1.43em}.hminbox1 ul.greet li.first a:visited,.hminbox1 ul.greet li.first a:active{color:#000}.hminbox1 td.msg a,.hminbox1 td.msg a:link,.hminbox1 td.msg a:visited{color:#333}.hminbox1 td.msg a:hover,.hminbox1 td.msg a:active{color:#000;cursor:pointer}.hminbox1 ul.greet li.first a span{font-size:86%;line-height:1.333em;padding:.25em}.hminbox1 p.error a,.hminbox1 p.error a:link,.hminbox1 p.error a:visited{color:#333}.hminbox1 p.error a:hover,.hminbox1 p.error a:active{color:#000}.hminbox1 ul.greet li a span,.hminbox1 ul.greet li a:link span,.hminbox1 ul.greet li a:visited span,.hminbox1 td.msg a span,.hminbox1 td.msg a:link span,.hminbox1 td.msg a:visited span{color:#333}.hminbox1 ul.greet li a:hover span,.hminbox1 ul.greet li a:active span,.hminbox1 td.msg a:hover span,.hminbox1 td.msg a:active span{color:#000}.hminbox1 ul a,.hminbox1 ul a:link,.hminbox1 ul a:visited{color:#666}.hminbox1 ul a:hover,.hminbox1 ul a:active{color:#333}.ht1{margin:0;padding:0;text-align:left}.ht1 .sub{color:#666;line-height:2.333em}.ht1 .sub a:link{color:#666}.ht1 .topicitem{width:26.333em;min-width:316px;border-top:solid 1px #e1e1e1;padding:0}.ht1 .atopicitem{width:24.833em;min-width:298px}.ht1 .first{display:inline-block;border-top:none;margin-bottom:.833em;padding:0;vertical-align:top}.ht1 .num,.ht1 .first .topic{color:#666;font-size:116.7%;font-weight:bold;line-height:1.333em}.ht1 .first p{color:#999;font-weight:normal;line-height:1.333em;margin:0;padding:0}.ht1 .down{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2685px}.ht1 .up{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2799px}.ht1 .side{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2913px}.ht1 .downflush{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2690px}.ht1 .upflush{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2804px}.ht1 .sideflush{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2918px}.ht1 .first .content{margin-right:136px;padding-right:.833em}.ht1 .media{float:right}.ht1 .media img{display:block}.ht1 .arrow{padding-left:1.5em}.ht1 .rank{float:left;text-align:right;width:1.25em;display:inline}.ht1 .first .rank{width:1.071em}.ht1 .content{padding-left:1.667em}#marchmadness .br3 img,#marchmadness .br2{display:none}#marchmadness .br3 noscript p{padding:5em 10em 0}#marchmadness .hlcp1 .pri a,#marchmadness .hlcp1 .pri a:link,#marchmadness .hlcp1 .pri a:visited{font-size:117%}#marchmadness .m2{margin-right:1em}.co3b1 .br{float:left}.co3b1 .more{clear:left}.co3b1 .br .more{clear:none}.co3b2 .br{float:left}.co3b2 .br1{clear:both;display:block;float:none}.co3b2 .more{clear:left}.co3b2 .br .more{clear:none}.co3b3 .br{float:left}.co3b3 .br3{clear:both;display:block;float:none}.co3b3 .more{clear:both}.srchh1 div.loaddisableding{font-weight:bold;margin:.66em 0;padding:0 0 .33em 0;text-align:center}.srchh1 div.logo{background:transparent url(../../i/61/def0ebad64d00fda0702cb7b8179ea.png) no-repeat 0 -263px;height:19px;width:37px}.srchh1 div.shupsell{margin-bottom:.33em;padding-bottom:0}.srchh1 div.results,.srchh1 div.shupsell,.srchh1 div.loaddisableding{border-bottom:solid 2px #ccebf7}.srchh1 div.results{margin-bottom:.33em;padding:0}.srchh1 ul.resultlist{list-style-type:none;margin:0;padding:0}.srchh1 ul.resultlist li{border-bottom:solid 1px #e1e1e1;display:list-item;line-height:1.25em;padding:.58em 0}.srchh1 ul.resultlist li.first{padding-top:0}.srchh1 ul.logobar{float:right;margin:.1em 0;padding:0}.srchh1 ul.logobar li{float:left;list-style-type:none;margin:0;padding:0 1.08em}.srchh1 ul.logobar li.first{border-right:solid 1px #e1e1e1;padding-left:0}.srchh1 ul.logobar li.last{border-right-style:none;padding-right:0}.srchh1 ul.logobar li a.hide{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat right -3032px;padding-right:15px}.srchh1 ul.logobar li a.hide:hover{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat right -3141px}.srchh1 ul.logobar li a.show{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat right -3250px;padding-right:15px}.srchh1 ul.logobar li a.show:hover{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat right -3359px}.srchh1 ul a,.srchh1 ul a:link,.srchh1 ul a:visited{color:#666}.srchh1 ul a:hover,.srchh1 ul a:active{color:#333}.eula1 h2.h2 span,.eula1 div.h2 span{display:none}.eula1{display:none;left:0;height:100%;text-align:center;top:0;position:absolute;width:100%;z-index:110}.eula1 h2.h2,.eula1 div.h2{background:transparent url(../../i/09/4ebdf19a1ce03cce12e11926256422.gif) repeat 0 0;height:100%;left:0;position:absolute;text-align:center;width:100%}#wrapper .eula1 .br1{background:#f5f9fb;border:solid 2px #9c9c9c;margin:0 auto;position:relative;top:180px;width:37.2em;z-index:111}.eula1 .richtext{padding:6px 10px;text-align:left}.eula1 .custom2{display:block;font-weight:bold;margin:6px auto;text-align:center}.eula1 .custom2 a{background-color:#eb7c00;border:solid 1px #ffa615;color:#fff;padding:2px 7px}.actfeed1 .ac-head{margin-top:-5px;border-bottom:2px solid #ccebf7;width:100%}.actfeed1 .ac-greet{float:left}.actfeed1 .ac-greettext{font-size:117%;padding:4px 0 7px 0}.actfeed1 .ac-upsell,.actfeed1 .ac-errortext{float:left;padding:5px 0 6px 0}.actfeed1 .ac-errortext{padding-top:5px}.actfeed1 .ac-signinlink,.actfeed1 .ac-signout{float:right;margin-top:5px}.actfeed1 .ac-signinlink.fbsignin{margin-top:1px}.actfeed1 .ac-signinlink.fbsignin a span{background:url(../../i/16/9798fea395258497f598bba500bf83.png) repeat 0% -263px #5f78ab;border-bottom:1px solid #1a356e;border-top:1px solid #879ac0;color:#fff;display:block;font-family:"lucida grande",tahoma,verdana,arial,sans-serif;font-weight:bold;margin:1px 1px 0 21px;padding:2px 6px 3px;text-decoration:none}.actfeed1 .ac-signinlink a{padding:3px 0 5px 23px}.actfeed1 .ac-signinlink.fbsignin a{margin:0;padding:0;background:url(../../i/16/9798fea395258497f598bba500bf83.png) no-repeat 0% -229px #29447e;cursor:pointer;display:inline-block;outline:medium none;text-decoration:none;font-size:92%;line-height:14px}.actfeed1 .wlsignin{background:transparent url(../../i/16/9798fea395258497f598bba500bf83.png) no-repeat 0% -149px}.actfeed1 .twsignin{background:transparent url(../../i/16/9798fea395258497f598bba500bf83.png) no-repeat 0% -120px}.actfeed1 .ac-list,.actfeed1 .ac-item{list-style-type:none;margin:0;padding:0}.actfeed1 .ac-item{padding:10px 0 0 0;border-bottom:1px solid #e1e1e1}.actfeed1 .ac-itemauthorpicdiv{width:26px;float:left}.actfeed1 .ac-itemauthorpic{width:26px;height:26px}.actfeed1 .ac-itemmain{margin:-3px 0 0 0;float:right;width:88%;padding:0;overflow:hidden}.actfeed1 .ac-itemauthorname{display:inline;font-weight:bold}.actfeed1 .ac-itemfoot{padding-left:0;padding-bottom:8px;margin-left:0}.actfeed1 .ac-itemfoot li.first{background:none;padding-left:0}.actfeed1 .ac-itemfoot li{list-style-type:disc;list-style:none;background:transparent url(../../i/16/9798fea395258497f598bba500bf83.png) no-repeat -1.4em -170px;padding:0 8px 0 11px;margin-top:3px;float:left}.actfeed1 .ac-noitems a:link,.actfeed1 .ac-noitems a:visited,.actfeed1 .ac-list a:link,.actfeed1 .ac-list a:visited,.actfeed1 .ac-errortext a:link,.actfeed1 .ac-errortext a:visited,.actfeed1 .ac-foot .ac-footlefthide a:link,.actfeed1 .ac-foot .ac-footlefthide a:visited{color:#000}.actfeed1 .ac-noitems a:hover,.actfeed1 .ac-noitems a:active,.actfeed1 .ac-list a:hover,.actfeed1 .ac-list a:active,.actfeed1 .ac-errortext a:hover,.actfeed1 .ac-errortext a:active,.actfeed1 .ac-foot .ac-footlefthide a:hover,.actfeed1 .ac-foot .ac-footlefthide a:active{color:#000}.actfeed1 .ac-statustext.ac-statustextcurrent,.actfeed1 .ac-commentinput.ac-statustextcurrent,.actfeed1 .ac-signout a:link,.actfeed1 .ac-signout a:visited,.actfeed1 .ac-foot a:link,.actfeed1 .ac-foot a:visited,.actfeed1 .ac-commenttime,.actfeed1 .ac-liketext,.actfeed1 .ac-liketext a:link,.actfeed1 .ac-liketext a:visited,.actfeed1 .ac-itemfoot a:link,.actfeed1 .ac-itemfoot a:visited,.actfeed1 .ac-allcomments a:link,.actfeed1 .ac-allcomments a:visited{color:#666}.actfeed1 .ac-statustext,.actfeed1 .ac-commentinput,.actfeed1 .ac-signout a:hover,.actfeed1 .ac-signout a:active,.actfeed1 .ac-foot a:hover,.actfeed1 .ac-foot a:active,.actfeed1 .ac-itemfoot a:hover,.actfeed1 .ac-itemfoot a:active,.actfeed1 .ac-liketext a:hover,.actfeed1 .ac-liketext a:active,.actfeed1 .ac-allcomments a:hover,.actfeed1 .ac-allcomments a:active{color:#333}.actfeed1 .ac-itemtext{display:inline;margin:0;padding:0}.actfeed1 .ac-itembody{padding-top:5px}.actfeed1.facebook .ac-itembasic{overflow:hidden;line-height:1.34em;max-height:13.4em}.actfeed1.facebook .ac-itembodymain{max-height:9.3em;line-height:1.34em;overflow:hidden}.actfeed1 .ac-itembodypicdiv{float:left;margin-right:9px}.actfeed1 .ac-noitems{padding:10px 0 10px 0;border-bottom:1px solid #e1e1e1}.actfeed1 .ac-status{margin:0;padding:11px 0 12px 0;border-bottom:2px solid #ccebf7;height:26px}.actfeed1 .ac-statusform{height:26px}.actfeed1 .ac-status .ac-statusmsgs{margin-top:6px}.actfeed1 .ac-statustext,.actfeed1 .ac-commentinput{padding:5px}.actfeed1 .ac-statustext{width:65.3%;float:left}.actfeed1 .ac-commentinput{width:92%;float:right;margin-top:2px}.actfeed1 .ac-commentsubmit{float:right;margin:7px 0 3px 0}.actfeed1 .ac-poststatus{float:right;margin-top:3px;padding:1px 0 0 0;width:27%}.actfeed1 input{font-size:100%;line-height:1.25em}.actfeed1 .ac-foot{padding:6px 0 7px 0;border-bottom:1px solid #e1e1e1}.actfeed1 .ac-footleftshow,.actfeed1 .ac-footlefthide{float:left}.actfeed1 .ac-footright{float:right}.actfeed1 .ac-hidelink{padding-right:15px;background:transparent url(../../i/16/9798fea395258497f598bba500bf83.png) no-repeat 100% 3px}.actfeed1 a:hover.ac-hidelink{background:transparent url(../../i/16/9798fea395258497f598bba500bf83.png) no-repeat 100% -16px}.actfeed1 .ac-showlink{padding-right:15px;background:transparent url(../../i/16/9798fea395258497f598bba500bf83.png) no-repeat 100% -79px}.actfeed1 a:hover.ac-showlink{background:transparent url(../../i/16/9798fea395258497f598bba500bf83.png) no-repeat 100% -98px}.actfeed1 .ac-refreshpic{background:transparent url(../../i/16/9798fea395258497f598bba500bf83.png) no-repeat 100% -36px;padding-right:17px}.actfeed1 a:hover.ac-refreshpic{background:transparent url(../../i/16/9798fea395258497f598bba500bf83.png) no-repeat 100% -58px}.actfeed1 .ac-updatestatus{width:283px;height:91px;text-align:center;vertical-align:middle;padding-top:192px}.actfeed1 .ac-loaddisabled{background:transparent url(../../i/fb/f017d9e8cc630c5e02659b6eaf35fa.gif) no-repeat 0 0}.actfeed1 .ac-comments .ac-commentsform{display:inline}.actfeed1 .ac-comments{list-style-type:none;padding:0;margin:0}.actfeed1 .ac-comments li{list-style:none;padding:10px 0;border-top:1px solid #e1e1e1}.actfeed1 .ac-comments .ac-liketext,.actfeed1 .ac-comments .ac-allcomments{padding:7px 0}.actfeed1 .ac-comments .ac-liketext span,.actfeed1 .ac-comments .ac-allcomments a{padding:0 0 0 30px}.actfeed1 .ac-liketext span{background:transparent url(../../i/16/9798fea395258497f598bba500bf83.png) no-repeat 0% -189px}.actfeed1 .ac-allcomments a{background:transparent url(../../i/16/9798fea395258497f598bba500bf83.png) no-repeat 0% -208px}.actfeed1 .ac-comment .ac-itemmain{width:86.5%}.actfeed1 .ac-selfcomment{padding:10px 0 0 0}.actfeed ac-commentinput{width:78%}#wrapper #content .generic1{margin:0 0 1em}.generic1 div.br{margin:0 0 .833em}.generic1 .link{text-align:left}.generic1 .link a{color:#333;font-family:arial;font-size:1.499em;font-weight:normal}.generic1 .richtext p{color:#333;font-family:arial;font-size:.996em;font-weight:normal;line-height:1.333em;text-align:left}.co6b7 .b3,.co6b7 .b4,.co6b7 .b5{display:none}.simple1,.simple1 p{margin:0;padding:0}.simple1 cite{font-style:normal}.simple1 cite,.simple1 label{display:block}.simple1 cite,.simple1 div{margin:.5em 0 0 0}.simple1 input{font-size:100%;line-height:1.25em;outline:none}.simple1 input.button{margin:0;margin-left:.2em}.simple2,.simple2 p{margin:0;padding:0}.simple2 cite{font-style:normal;margin:.5em 0 0;margin-left:-.9em}.simple2 cite,.simple2 label{display:block}.simple2 div{margin:.5em 0 0;margin-left:.9em}.simple2 input{font-size:100%;line-height:1.25em;outline:none}.simple2 input.button{margin:0;margin-left:.2em}.simple3,.simple3 p{margin:0;padding:0}.simple3 cite{font-style:normal;margin:.5em 0 0;margin-left:-.9em}.simple3 cite,.simple3 input,.simple3 label{display:block}.simple3 div{margin:.5em 0 0;margin-left:.9em}.simple3 input{font-size:100%;line-height:1.25em;outline:none}.simple3 input.button{margin:.2em 0 0 0}.simple4,.simple4 p{margin:0;padding:0}.simple4 cite{font-style:normal;margin:0;margin-left:.6em}.simple4 div{margin:.5em 0}.simple4 input{font-size:100%;line-height:1.25em;outline:none}.simple4 input.button{margin:0;margin-left:.2em}.simple5,.simple5 p{margin:0;padding:0}.simple5 cite{display:block;font-style:normal}.simple5 cite,.simple5 div{margin:.5em 0 0 0}.simple5 input{font-size:100%;line-height:1.25em;outline:none}.simple5 input.button{margin:0;margin-left:.2em}.simple6,.simple6 p{margin:0;padding:0}.simple6 cite{display:block;font-style:normal}.simple6 cite,.simple6 div{margin:.5em 0 0 0}.simple6 input{font-size:100%;line-height:1.25em;outline:none}.simple6 input.image{margin:0;margin-left:.2em;vertical-align:bottom}.simple7,.simple7 p{margin:0;padding:0}.simple7 cite{font-style:normal}.simple7 cite,.simple7 input,.simple7 label{clear:left;display:block}.simple7 cite,.simple7 div{margin:.5em 0 0 0}.simple7 input{font-size:100%;line-height:1.25em;outline:none}.simple7 input.button{margin:.2em 0 0 0}.simple8{margin:0;padding:0;position:relative}.simple8 cite,.simple8 p{display:none}.simple8 div{clear:left}.simple8 div div{border:solid 1px #bcbcbc;clear:none;float:left;padding:3px 3px 0 3px}.simple8 input.image{margin:1px 0 0 0}.simple8 input.text{border-width:0;font-size:100%;line-height:1.25em;outline:none;padding:4px 3px 0 0;vertical-align:top}.simple8 label{color:#666;display:block}.hlcp1 .pri .last{float:left}.hlcpm1 .hlcp1 .pri .piped,.hlcpm1 .hlcp1 .pri a,.hlcpm1 .hlcp1 .pri a:link,.hlcpm1 .hlcp1 .pri a:visited,.hlcpm1 .hlcp2 .pri .piped,.hlcpm1 .hlcp2 .pri a,.hlcpm1 .hlcp2 .pri a:link,.hlcpm1 .hlcp2 .pri a:visited{font-size:150%}.hcpep1 ul{list-style-type:none;margin:0;padding:0}.hcpep1 li,.hcpep1 li.media{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2236px;display:block;line-height:1.25em;margin:0;padding:.5em 0 .5em 19px}.hcpep1 li.media{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2350px}.hcpep1 span.piped span.media{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2354px;padding:.083em 0 .083em 19px}.hcpep1 .date{color:#999;clear:both;padding:.333em 0 1.667em 0}.hcpep1 span.icon{display:inline-block;font-size:75%;text-decoration:none}.hcpep1 span.new{background:transparent url(../../i/77/b23a82d78a0605243aad8f44e8c079.gif) no-repeat right center;padding-right:14px;padding-top:1px}.hcpep1 span.fresh1{background:transparent url(../../i/d7/fb6441a4c45cb3a3b2f592d914a3cd.gif) no-repeat right center;padding-right:33px;padding-top:1px}.hcpep1 span.fresh2{background:transparent url(../../i/77/b23a82d78a0605243aad8f44e8c079.gif) no-repeat right center;padding-right:14px;padding-top:1px}.hcpep1 span.fresh3{background:transparent url(../../i/77/b23a82d78a0605243aad8f44e8c079.gif) no-repeat right center;padding-right:14px;padding-top:1px}.hcpep1 span.photo{background:transparent url(../../i/b9/ab98403e7de9ce52839e5de99d27e5.gif) no-repeat right center;padding-right:11px;padding-top:1px}.hcpep1 span.dest1{background:transparent url(../../i/b9/ab98403e7de9ce52839e5de99d27e5.gif) no-repeat right center;padding-right:11px;padding-top:3px}.hcpep1 span.dest2{background:transparent url(../../i/b9/ab98403e7de9ce52839e5de99d27e5.gif) no-repeat right center;padding-right:11px;padding-top:3px}.hcpep1 span.dest3{background:transparent url(../../i/b9/ab98403e7de9ce52839e5de99d27e5.gif) no-repeat right center;padding-right:11px;padding-top:3px}.hcpep2 ul{list-style-type:none;margin:0;padding:0}.hcpep2 li,.hcpep2 li.media{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2236px;display:block;float:left;line-height:1.25em;margin:0 7px 0 8px;padding:.5em 0 .5em 19px;width:42%}.hcpep2 li.media{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2350px}.hcpep2 span.piped span.media{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2354px;padding:.083em 0 .083em 19px}.hcpep2 .date{color:#999;clear:both;padding:.333em 0 1.667em 0}.hcpep2 span.icon{display:inline-block;font-size:75%;text-decoration:none}.hcpep2 span.new{background:transparent url(../../i/77/b23a82d78a0605243aad8f44e8c079.gif) no-repeat right center;padding-right:14px;padding-top:1px}.hcpep2 span.fresh1{background:transparent url(../../i/d7/fb6441a4c45cb3a3b2f592d914a3cd.gif) no-repeat right center;padding-right:33px;padding-top:1px}.hcpep2 span.fresh2{background:transparent url(../../i/77/b23a82d78a0605243aad8f44e8c079.gif) no-repeat right center;padding-right:14px;padding-top:1px}.hcpep2 span.fresh3{background:transparent url(../../i/77/b23a82d78a0605243aad8f44e8c079.gif) no-repeat right center;padding-right:14px;padding-top:1px}.hcpep2 span.photo{background:transparent url(../../i/b9/ab98403e7de9ce52839e5de99d27e5.gif) no-repeat right center;padding-right:11px;padding-top:1px}.hcpep2 span.dest1{background:transparent url(../../i/b9/ab98403e7de9ce52839e5de99d27e5.gif) no-repeat right center;padding-right:11px;padding-top:3px}.hcpep2 span.dest2{background:transparent url(../../i/b9/ab98403e7de9ce52839e5de99d27e5.gif) no-repeat right center;padding-right:11px;padding-top:3px}.hcpep2 span.dest3{background:transparent url(../../i/b9/ab98403e7de9ce52839e5de99d27e5.gif) no-repeat right center;padding-right:11px;padding-top:3px}.hcpep3 ul{list-style-type:none;margin:0;padding:0}.hcpep3 li,.hcpep3 li.media{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2236px;display:block;float:left;line-height:1.25em;margin:0 7px 0 8px;padding:.5em 0 .5em 19px;width:27%}.hcpep3 li.media{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2350px}.hcpep3 span.piped span.media{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2354px;padding:.083em 0 .083em 19px}.hcpep3 .date{color:#999;clear:both;padding:.333em 0 1.667em 0}.hcpep3 span.icon{display:inline-block;font-size:75%;text-decoration:none}.hcpep3 span.new{background:transparent url(../../i/77/b23a82d78a0605243aad8f44e8c079.gif) no-repeat right center;padding-right:14px;padding-top:1px}.hcpep3 span.fresh1{background:transparent url(../../i/d7/fb6441a4c45cb3a3b2f592d914a3cd.gif) no-repeat right center;padding-right:33px;padding-top:1px}.hcpep3 span.fresh2{background:transparent url(../../i/77/b23a82d78a0605243aad8f44e8c079.gif) no-repeat right center;padding-right:14px;padding-top:1px}.hcpep3 span.fresh3{background:transparent url(../../i/77/b23a82d78a0605243aad8f44e8c079.gif) no-repeat right center;padding-right:14px;padding-top:1px}.hcpep3 span.photo{background:transparent url(../../i/b9/ab98403e7de9ce52839e5de99d27e5.gif) no-repeat right center;padding-right:11px;padding-top:1px}.hcpep3 span.dest1{background:transparent url(../../i/b9/ab98403e7de9ce52839e5de99d27e5.gif) no-repeat right center;padding-right:11px;padding-top:3px}.hcpep3 span.dest2{background:transparent url(../../i/b9/ab98403e7de9ce52839e5de99d27e5.gif) no-repeat right center;padding-right:11px;padding-top:3px}.hcpep3 span.dest3{background:transparent url(../../i/b9/ab98403e7de9ce52839e5de99d27e5.gif) no-repeat right center;padding-right:11px;padding-top:3px}.local1 .br5 h3{font-size:100%;font-weight:normal;line-height:1.67em}.local1 .simple8{margin-top:8px}.local1 input{width:237px}.local1 .simple8 div div{float:none;width:262px}.local1 .simple8 div.loclist{background-color:#fff;border:1px solid #bcbcbc;clear:left;margin:10px 0 0 0;padding:0;position:absolute;width:268px}.local1 .simple8 div.loclist a.cancel{border:solid 1px #bcbcbc;float:right;padding:0 3px;margin:2px 2px 0 0}.local1 .simple8 div.loclist.down{top:45px}.local1 .simple8 div.loclist.up{bottom:28px}.local1 .simple8 div.loclist ul{list-style-type:none;margin:0;padding:0}.local1 .simple8 div.loclist ul li a{display:block;padding:5px 10px}.local1 .simple8 div.loclist ul li a:hover,.local1 .simple8 div.loclist ul li a:active{color:#333;background-color:#e5e5e5}.local1 .simple8 div.loclist ul li p{display:block;font-weight:normal;margin:0;padding:5px 10px}.local1 .simple8 .image{cursor:pointer}.weather1{line-height:1.25em}.weather1 .selected a,.weather1 .selected a:link,.weather1 .selected a:visited,.weather1 h4 a,.weather1 h4 a:link,.weather1 h4 a:visited,.weather1 .temp,.weather1 .conditions,.weather1 ul.degreetype li a:hover,.weather1 ul.forecasts a:hover,.weather1 div.today a:hover,.weather1 div.today a:hover span{color:#333}.weather1 .selected a:hover,.weather1 .selected a:active,.weather1 h4 a:hover,.weather1 h4 a:active,.weather1 ul.forecasts a:hover,.weather1 div.conditions a:hover,.weather1 .today li a:hover,.weather1 ul.degreetype li a:hover{color:#333}.weather1 h4 a:hover,.weather1 div.temp a:hover{color:#000}.weather1 .h3{line-height:1.4em;margin-bottom:.31em}.weather1 .h3 a{color:#666;float:left;font-size:117%;font-weight:bold}.weather1 .h3 span a.mapit{color:#333;font-size:83%;font-weight:normal;text-align:right;width:100%}.weather1 .h3 span{float:right}.weather1 div.data{float:left;padding-left:1.35em}.weather1 div.conditions{padding-bottom:1.75em}.weather1 div.msgbox{padding-top:.833em}.weather1 div.temp{font-size:150%;font-weight:bold;line-height:normal;padding:.2em 0 .35em 0}.weather1 div.today{height:1em;padding-bottom:.667em;width:100%}.weather1 div.today a,.weather1 div.today a:link,.weather1 div.today a:visited,.weather1 ul.degreetype li,.weather1 ul.degreetype li a{color:#666}.weather1 div.today ul,.weather1 ul{list-style-type:none;margin:0;padding:0}.weather1 div.weaheading{padding-bottom:.63em}.weather1 h4,.weather1 img{float:left}.weather1 div.weahr{border-top:solid 1px;color:#e1e1e1;margin-bottom:.7em}.weather1 ul.degreetype{float:right;text-align:right;width:20%}.weather1 ul.degreetype li{display:inline;padding:0}.weather1 ul.degreetype li.celsius{padding:0 0 0 1.417em}.weather1 ul.degreetype li.selected{font-weight:bold;color:#333}.weather1 ul.forecasts a,.weather1 ul.forecasts a:link,.weather1 ul.forecasts a:visited,.weather1 ul.degreetype li.selected a{color:#333}.weather1 div.conditions a{color:#666}.weather1 ul.forecasts li,.weather1 div.today ul li{border-right:solid 1px #333;float:left;font-size:100%;padding:0 .917em;text-align:right}.weather1 ul.forecasts li.first,.weather1 div.today ul li.first{padding-left:0}.weather1 ul.forecasts li.last,.weather1 div.today ul li.last{border:none}.weather1 h3.h3{font-size:117%}.weather1 h3.h3 a{font-size:100%}.weather1 .weaheading h4{font-size:100%}.weather1 div.temp a{font-weight:bold;line-height:1em}.weather1 ul.forecasts a:hover,.weather1 div.conditions a:hover,.weather1 .today li a:hover,.weather1 ul.degreetype li a:hover,.weather1 .h3 span a:hover{color:#333}.weather1 ul.forecasts a:hover,.weather1 ul.degreetype li.selected a:hover,.weather1 .h3 span a:hover,.weather1 h3.h3 span a:hover,.weather1 h4 a:hover{color:#000}.weather2 .location{border-right:1px solid #333;float:left;margin-top:.417em;padding:0 .333em}.weather2 .forecast{float:left}.weather2 .forecast .weatherimage{float:left;margin:.25em .25em 0 .2em}.weather2 .forecast .minidata{float:left;margin-top:.417em}.weather2 .forecast .minidata .weaheading{float:left}.weather2{float:right;margin:.66em}.weather2 .weaheading ul.degreetype li{display:inline;padding:.083em .25em .083em .083em}.weather2 .h3{line-height:1.4em;margin-bottom:.31em}.weather2 .h3 a{color:#333;float:left;font-size:100%;font-weight:bold}.weather2 ul.degreetype li.selected{color:#333;font-weight:bold}.weather2 ul.forecasts a,.weather2 ul.forecasts a:link,.weather2 ul.forecasts a:visited,.weather2 ul.degreetype li.selected a{color:#333}.weather2 div.today{display:inline;float:left;height:1em;padding-bottom:.667em}.weather2 div.today ul,.weather2 ul{display:inline;list-style-type:none;margin-left:.417em;padding:0}.weather2 div.today ul li{float:left;font-size:100%;padding:0 .917em;text-align:right}.weather2 ul.degreetype li.celsius{padding:0}.hideminiweather{visibility:hidden}.weather2 .degreetype li a,.weather2 .degreetype li a:link{color:#666}.weather2 .degreetype li.selected a,.weather2 .degreetype li.selected a:link{color:#333}.locnews1 ul{list-style-type:none;margin:0;padding:0}.locnews1 ul li{border-bottom:solid 1px #e1e1e1;line-height:1.25em;padding:.583em 0}.locnews1 ul.msg li{border-top:0;border-bottom:0}.locnews1 ul li.first{border-top:solid 1px #e1e1e1;margin:.583em 0 0 0}.locnews1 p.msg{line-height:1.33em;margin:0 0 5.417em 0}.locnews1 .seemore{font-style:italic;line-height:1.25em;padding:.583em 0 0 0;text-align:right}.locnews1 .hideseemore{display:none}.locnews1 ul li.last{border-bottom:none}.locevents1 ul{list-style-type:none;margin:0;padding:0}.locevents1 ul li{border-bottom:solid 1px #e1e1e1;line-height:1.25em;padding:.583em 0}.locevents1 ul li.first{border-top:solid 1px #e1e1e1;margin:.583em 0 0 0}.locevents1 p.msg{line-height:1.33em;margin:0 0 5.417em 0}.locevents1 .seemore{font-style:italic;line-height:1.25em;padding:.583em 0 0 0;text-align:right}.locevents1 .hideseemore{display:none}.locevents1 ul li.last{border-bottom:none}.locsports1 ul{list-style-type:none;margin:0;padding:0}.locsports1 ul li{border-bottom:solid 1px #e1e1e1;line-height:1.25em;padding:.583em 0}.locsports1 ul.msg li{border-top:0;border-bottom:0}.locsports1 ul li.first{border-top:solid 1px #e1e1e1;margin:.583em 0 0 0}.locsports1 p.msg{line-height:1.33em;margin:0 0 5.417em 0}.locsports1 .seemore{font-style:italic;line-height:1.25em;padding:.583em 0 0 0;text-align:right}.locsports1 .hideseemore{display:none}.locsports1 ul li.last{border-bottom:none}.lmlsf1 .findmore{border-bottom:solid 1px #e1e1e1;height:1%;line-height:1.43em;padding:0 0 .833em;overflow:hidden}.lmlsf1 .findmore div{float:left}.lmlsf1 .findmore ul li.first{padding:0 .8em 0 .583em}.lmlsf1 .findmore strong{font-size:100%}.lmlsf1 .simpleform p{display:block;font-size:100%;font-weight:bold;line-height:1.43em;margin:.833em 0}.linklist22{list-style-type:none;margin:0;padding:0}.linklist22 li{border-top:solid 1px #e1e1e1;line-height:1.25em;padding:.583em 0}#wrapper .money1 .br2{margin:0}#wrapper .money1 .br1{float:none}.money1 .indices1 table{border-collapse:collapse;border-spacing:0;width:91%}.money1 .simple8 input.text{width:20.583em}.money1 .siidx{font-weight:bold}.indices1 a,.indices1 a:link,.indices1 a:visited{color:#333}.indices1 a:hover,.indices1 a:active{color:#000}.indices1 caption,.indices1 thead{display:none}.indices1 td{color:#333;padding:0 0 0 .833em;line-height:1.25em;text-align:right}.indices1 td.neg{color:#c30505}.indices1 td.pos{color:#090}.indices1 td.siidx{padding:0 1em 0 0;text-align:left}.indices1 .sitime{text-align:left;color:#999;padding-bottom:.333em}.co4b3 .b3{display:none}.co4b3 .br{float:left}.co4b3 .br1{clear:both;display:block;float:none}.co4b3 .more{clear:both}.co4b3 .br .more{clear:none}.co4b4 .b3{display:none}.co4b4 .br{float:left}.co4b4 .br4{clear:both;display:block;float:none}.co4b4 .more{clear:both}.co4b6 .b3{display:none}.co4b6 .br{clear:right}.co4b6 .br1{float:left}.co4b6 .br2,.co4b6 .br3,.co4b6 .br4{float:right}.co4b6 .more{clear:both;float:none}.co4b6 .br .more{clear:none}.co4b7 .b3{display:none}.co4b7 .br1{float:right}.co4b7 .more{clear:left}.co4b7 .br .more{clear:none}.co4b9 .b3{display:none}.co4b9 .br{float:left}.co4b9 .br1,.co4b9 .br2{clear:both;display:block;float:none}.co4b9 .more{clear:both}.co4b9 .br .more{clear:none}.co4b10 .b3{display:none}.co4b10 .br{float:left}.co4b10 .br1,.co4b10 .br4{clear:both;display:block;float:none}.co4b10 .more{clear:both}.co4b10 .br .more{clear:none}div.hlcp3 li.ter{background:transparent url(../../i/0c/c57bc2a7d38843d7c4aa8028fc9f82.gif) no-repeat scroll 0 center;padding:.167em .6em;white-space:nowrap}div.hlcp3 li.ter .piped a{background:transparent url(../../i/0c/c57bc2a7d38843d7c4aa8028fc9f82.gif) no-repeat scroll 0 center;margin-left:-.2em;padding-left:.7em}div.hlcp3 li.ter span{color:#fff}.headlinelist1 div{float:left}.headlinelist1 ul{display:block;margin:0;padding:0}.headlinelist1 ul li{border-top:solid 1px #e1e1e1;display:list-item;line-height:1.25em;list-style-type:none;margin:0;padding:.583em 0}.headlinelist1 ul li.first{border-top:none;padding-top:0}.headlinelist1 span.media{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2355px;display:inline-block}.headlinelist1 span.media a{margin-left:19px}.adserved1 ul{margin:0;padding:0 0 0 .833em}.adserved1 ul li{border-top:solid 1px #e1e1e1;display:list-item;line-height:1.25em;list-style-type:none;margin:0;padding:.583em 0}.adserved1 ul li.first{border-top:none;padding-top:0}.slupsell1 .appbar .detail{position:absolute;height:326px;width:520px}.slupsell1 .appbar .detail .hidden{left:-9000px;top:0;position:absolute}.single2 .linklist22 li.first{border-top:none;padding-top:0}.single2 .orderedlist1{margin:0 0 0 1.5em}.single2 .imglinkabslist23 li a{font-size:150%}.w4 .single2 .imglinkabslist17 li,.w4 .single2 .linkedimgabslist7 li,.w4 .single2 .linkedimglinklist14 li,.w4 .single2 .linkedimglinklist17 li{margin:0 1.75em 0 0;min-width:86px;padding:0;width:7.167em}.w8 .single2 .imglinkabslist17 li,.w8 .single2 .linkedimgabslist7 li,.w8 .single2 .linkedimglinklist14 li,.w8 .single2 .linkedimglinklist17 li{margin:0 2.16em 0 0;min-width:192px;padding:0;width:16em}.w4 .single2 .linklist15 li{margin:0 1.75em 0 0;min-width:77px;padding:.25em 0 .25em .75em;width:7.167em}.w8 .single2 .linklist15 li{margin:0 1.333em 0 0;min-width:183px;padding:.25em 0 .5em .75em;width:15.25em}.w4 .single2 .imglinkabslist23 li,.w4 .single2 .linkedimglinklist18 li{margin:0 1.667em 0 0;min-width:60px;padding:0;width:5em}.w8 .single2 .imglinkabslist23 li,.w8 .single2 .linkedimglinklist18 li{margin:0 2.25em 0 0;min-width:136px;padding:0;width:11.333em}.w4 .single2 .imglinkabslist17 li.last,.w4 .single2 .linkedimgabslist7 li.last,.w4 .single2 .linkedimglinklist14 li.last,.w4 .single2 .linkedimglinklist17 li.last,.w4 .single2 .linklist15 li.last,.w8 .single2 .imglinkabslist17 li.last,.w8 .single2 .linkedimgabslist7 li.last,.w8 .single2 .linkedimglinklist14 li.last,.w8 .single2 .linkedimglinklist17 li.last,.w8 .single2 .linklist15 li.last,.w4 .single2 .imglinkabslist23 li.last,.w4 .single2 .linkedimglinklist18 li.last,.w8 .single2 .imglinkabslist23 li.last,.w8 .single2 .linkedimglinklist18 li.last{margin:0}.single2 .linkedimglinklist16 a{text-align:left}.w4 .single2 .linkedimglinklist16 li{margin-right:.833em;width:12.083em}.single2 .linkedimglinklist16 li.last{margin-right:0}.complex2,.complex2 p{margin:0;padding:0}.complex2 fieldset{border:none;clear:both}.complex2 fieldset.last div{display:block}.complex2 cite,.complex2 label{display:block;font-style:normal}.complex2 cite,.complex2 div{margin-top:.4em}.complex2 select{font-size:100%}.complex2 input{font-size:100%;line-height:1.25em}.complex2 label,.complex2 select{margin-bottom:.2em}.complex2 input.alt{float:right}.linkedimgabslist7{list-style-type:none;margin:0;padding:0}.linkedimgabslist7 img{border:solid 1px #333;float:left;margin-bottom:3px;margin-right:6px}.linkedimgabslist7 li{display:block;float:left;margin-bottom:.9em;margin-right:1%;width:29%}.linkedimglinklist1{list-style-type:none;margin:0;padding:0}.linkedimglinklist1 a{display:block}.linkedimglinklist1 a:after{content:".";clear:both;display:block;height:0;visibility:hidden}.linkedimglinklist1 img{border:none;float:left;margin-bottom:.4em;margin-right:.4em}.linkedimglinklist1 li{margin-bottom:.9em}.linkedimglinklist1 li.last{margin-bottom:0}.linkedimglinklist1 a span{cursor:pointer;float:left;padding-top:6px}.linkedimglinklist1 a span span{padding-top:0}.linkedimglinklist14{list-style-type:none;margin:0;padding:0}.linkedimglinklist14 a{display:block}.linkedimglinklist14 a:after{content:".";clear:both;display:block;height:0;visibility:hidden}.linkedimglinklist14 img{border:none;float:left;margin-bottom:.4em;margin-right:.4em}.linkedimglinklist14 li{display:block;float:left;margin-bottom:.9em;margin-right:3%;position:relative;width:29%}.linkedimglinklist14 a span{cursor:pointer;float:left;padding-top:6px}.linkedimglinklist16{list-style-type:none;margin:0;padding:0}.linkedimglinklist16 a,.linkedimglinklist16 img{display:block;margin:0 auto;text-align:center}.linkedimglinklist16 img{border:none;margin-bottom:.4em}.linkedimglinklist16 li{display:block;float:left;margin-bottom:.9em;margin-right:3%;position:relative;width:47%}.linkedimglinklist17{list-style-type:none;margin:0;padding:0}.linkedimglinklist17 a,.linkedimglinklist17 img{display:block;margin:0 auto;text-align:center}.linkedimglinklist17 img{border:none;margin-bottom:.4em}.linkedimglinklist17 li{display:block;float:left;margin-bottom:.9em;margin-right:3%;position:relative;width:29%}.linkedimglinklist18{list-style-type:none;margin:0;padding:0}.linkedimglinklist18 a,.linkedimglinklist18 img{display:block;margin:0 auto;text-align:center}.linkedimglinklist18 img{border:none;margin-bottom:.4em}.linkedimglinklist18 li{display:block;float:left;margin-bottom:.9em;margin-right:3%;position:relative;width:21%}.linklist8{list-style-type:none;margin:0;padding:0}.linklist8 a{white-space:pre}.linklist8 li{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat right -3788px;float:left;margin:0;margin-right:.7em;padding:0;padding-right:.8em}.linklist8 li.last{background-image:none;margin:0;padding:0}.orderedlist1{list-style-type:decimal;margin:0;margin-left:3em;padding:0}.orderedlist1 li{margin:0;padding:.25em 0 .2em 0}.scp1 p{line-height:1.5em;margin:0;padding:0}.scp1 p a,.scp1 p a:link,.scp1 p a:visited,.scp1 p a:hover,.scp1 p a:active{text-decoration:underline}.scp1 ul,.scp1 li{line-height:1.25em;list-style:none;margin:0;padding:0}.scp1 img,.scp1 .headline li.first{border:0}.scp1 img,.scp1 object{display:block;float:left}.scp1 .npane img{margin-bottom:1em}.scp1 span a,.scp1 span .media a{padding-bottom:.13em}.scp1 li span a{padding-bottom:.47em}.scp1 .media a,.scp1 .piped .media a{background:transparent url(../../i/c6/7980776cb684844c20339b839ac35e.gif) no-repeat 0 -2355px;font-size:100%;line-height:1.25em;padding-left:19px;padding-bottom:.13em}.scp1 .piped a{background:none;font-size:100%;line-height:1.25em;padding-bottom:.13em}.scp1 span .media a{padding-left:0}.scp1 span a,.scp1 span .media a,.scp1 .npane.n2 span.hlnotlinked,.scp1 .npane.n3 span.hlnotlinked{background:none;display:inline;font-size:250%;line-height:1.13em}.scp1 .npane.n3,.scp1 .npane.n3 span.hlnotlinked{margin:0 auto}.scp1 .npane.n3,.scp1 .npane.n2{text-align:center}.scp1 .linkedimg{text-align:left}.scp1 .npane.n2 li{padding:0 1em}.scp1 .npane.n2 .first{padding-left:0}.scp1 .npane.n2 .last{padding-right:0}.scp1 .npane li{margin:0 auto;text-align:center}.scp1 .headline ul{padding-left:2.08em}.scp1 .npane li,.scp1 .headline img,.scp1 .headline object,.scp1 .npane{float:left}.scp1 .richtext,.scp1 .headline{text-align:left}.scp1 .headline ul,.scp1 div{float:none}.scp1 .headline li{border-top:solid 1px #e1e1e1;padding:.42em 0}.scp1 .headline li.last{border-bottom:solid 1px #e1e1e1}.scp1 .npane a{clear:left}.scp1 .npane li a{display:block} \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/07/617475cf39bf6f5c0bd6ecb985335c.gif b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/07/617475cf39bf6f5c0bd6ecb985335c.gif
new file mode 100755
index 0000000000..a7d9c2e798
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/07/617475cf39bf6f5c0bd6ecb985335c.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/09/4ebdf19a1ce03cce12e11926256422.gif b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/09/4ebdf19a1ce03cce12e11926256422.gif
new file mode 100755
index 0000000000..4ff69a3c0b
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/09/4ebdf19a1ce03cce12e11926256422.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/0c/c57bc2a7d38843d7c4aa8028fc9f82.gif b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/0c/c57bc2a7d38843d7c4aa8028fc9f82.gif
new file mode 100755
index 0000000000..7b5405b04d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/0c/c57bc2a7d38843d7c4aa8028fc9f82.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/11/999518480e3c07301320f84f4bd855.png b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/11/999518480e3c07301320f84f4bd855.png
new file mode 100755
index 0000000000..782d2e9bf1
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/11/999518480e3c07301320f84f4bd855.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/16/9798fea395258497f598bba500bf83.png b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/16/9798fea395258497f598bba500bf83.png
new file mode 100755
index 0000000000..7a339f83fe
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/16/9798fea395258497f598bba500bf83.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/1a/57011fe37f98be0ee74ce87a62ba9b.png b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/1a/57011fe37f98be0ee74ce87a62ba9b.png
new file mode 100755
index 0000000000..f3fe32a3db
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/1a/57011fe37f98be0ee74ce87a62ba9b.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/50/f63ed0301e8b02a8a42d8590a46291.gif b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/50/f63ed0301e8b02a8a42d8590a46291.gif
new file mode 100755
index 0000000000..c8bf622d2c
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/50/f63ed0301e8b02a8a42d8590a46291.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/61/379589e51e05637f600f129f305b52.png b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/61/379589e51e05637f600f129f305b52.png
new file mode 100755
index 0000000000..077b4a4b0e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/61/379589e51e05637f600f129f305b52.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/61/def0ebad64d00fda0702cb7b8179ea.png b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/61/def0ebad64d00fda0702cb7b8179ea.png
new file mode 100755
index 0000000000..d8cc603468
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/61/def0ebad64d00fda0702cb7b8179ea.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/62/b5797d19976f0955d6d5d5c87ec996.jpg b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/62/b5797d19976f0955d6d5d5c87ec996.jpg
new file mode 100755
index 0000000000..79caddac41
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/62/b5797d19976f0955d6d5d5c87ec996.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/77/b23a82d78a0605243aad8f44e8c079.gif b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/77/b23a82d78a0605243aad8f44e8c079.gif
new file mode 100755
index 0000000000..1c10fc6878
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/77/b23a82d78a0605243aad8f44e8c079.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/94/8b0fe9bcd1399077fdc9374e5f314d_1.png b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/94/8b0fe9bcd1399077fdc9374e5f314d_1.png
new file mode 100755
index 0000000000..3f11704f6c
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/94/8b0fe9bcd1399077fdc9374e5f314d_1.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/b9/ab98403e7de9ce52839e5de99d27e5.gif b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/b9/ab98403e7de9ce52839e5de99d27e5.gif
new file mode 100755
index 0000000000..b8a3f451e4
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/b9/ab98403e7de9ce52839e5de99d27e5.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/c6/7980776cb684844c20339b839ac35e.gif b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/c6/7980776cb684844c20339b839ac35e.gif
new file mode 100755
index 0000000000..d30d7a9425
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/c6/7980776cb684844c20339b839ac35e.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/d7/fb6441a4c45cb3a3b2f592d914a3cd.gif b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/d7/fb6441a4c45cb3a3b2f592d914a3cd.gif
new file mode 100755
index 0000000000..888a2f90b9
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/d7/fb6441a4c45cb3a3b2f592d914a3cd.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/f8/614595fba50d96389708a4135776e4.gif b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/f8/614595fba50d96389708a4135776e4.gif
new file mode 100755
index 0000000000..8af01434a2
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/f8/614595fba50d96389708a4135776e4.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/fb/f017d9e8cc630c5e02659b6eaf35fa.gif b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/fb/f017d9e8cc630c5e02659b6eaf35fa.gif
new file mode 100755
index 0000000000..e26786b415
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/fb/f017d9e8cc630c5e02659b6eaf35fa.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/ff/290e7f0b12fa8a201581c74c1ae75a.gif b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/ff/290e7f0b12fa8a201581c74c1ae75a.gif
new file mode 100755
index 0000000000..cc3801c22e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/ff/290e7f0b12fa8a201581c74c1ae75a.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/icons/BING_websearch_2.jpg b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/icons/BING_websearch_2.jpg
new file mode 100755
index 0000000000..4027d2428d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/icons/BING_websearch_2.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/icons/adchoices_gif.gif b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/icons/adchoices_gif.gif
new file mode 100755
index 0000000000..a959edf65c
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stc.s-msn.com/br/sc/i/icons/adchoices_gif.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/col.stj.s-msn.com/br/sc/js/jquery/jquery-1.4.2.min.js b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stj.s-msn.com/br/sc/js/jquery/jquery-1.4.2.min.js
new file mode 100755
index 0000000000..100db1a9a9
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/col.stj.s-msn.com/br/sc/js/jquery/jquery-1.4.2.min.js
@@ -0,0 +1,154 @@
+/*!
+ * jQuery JavaScript Library v1.4.2
+ * http://jquery.com/
+ *
+ * Copyright 2010, John Resig
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * Includes Sizzle.js
+ * http://sizzlejs.com/
+ * Copyright 2010, The Dojo Foundation
+ * Released under the MIT, BSD, and GPL Licenses.
+ *
+ * Date: Sat Feb 13 22:33:48 2010 -0500
+ */
+(function(A,w){function ma(){if(!c.isReady){try{s.documentElement.doScroll("left")}catch(a){setTimeout(ma,1);return}c.ready()}}function Qa(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)}function X(a,b,d,f,e,j){var i=a.length;if(typeof b==="object"){for(var o in b)X(a,o,b[o],f,e,d);return a}if(d!==w){f=!j&&f&&c.isFunction(d);for(o=0;o<i;o++)e(a[o],b,f?d.call(a[o],o,e(a[o],b)):d,j);return a}return i?
+e(a[0],b):w}function J(){return(new Date).getTime()}function Y(){return false}function Z(){return true}function na(a,b,d){d[0].type=a;return c.event.handle.apply(b,d)}function oa(a){var b,d=[],f=[],e=arguments,j,i,o,k,n,r;i=c.data(this,"events");if(!(a.liveFired===this||!i||!i.live||a.button&&a.type==="click")){a.liveFired=this;var u=i.live.slice(0);for(k=0;k<u.length;k++){i=u[k];i.origType.replace(O,"")===a.type?f.push(i.selector):u.splice(k--,1)}j=c(a.target).closest(f,a.currentTarget);n=0;for(r=
+j.length;n<r;n++)for(k=0;k<u.length;k++){i=u[k];if(j[n].selector===i.selector){o=j[n].elem;f=null;if(i.preType==="mouseenter"||i.preType==="mouseleave")f=c(a.relatedTarget).closest(i.selector)[0];if(!f||f!==o)d.push({elem:o,handleObj:i})}}n=0;for(r=d.length;n<r;n++){j=d[n];a.currentTarget=j.elem;a.data=j.handleObj.data;a.handleObj=j.handleObj;if(j.handleObj.origHandler.apply(j.elem,e)===false){b=false;break}}return b}}function pa(a,b){return"live."+(a&&a!=="*"?a+".":"")+b.replace(/\./g,"`").replace(/ /g,
+"&")}function qa(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function ra(a,b){var d=0;b.each(function(){if(this.nodeName===(a[d]&&a[d].nodeName)){var f=c.data(a[d++]),e=c.data(this,f);if(f=f&&f.events){delete e.handle;e.events={};for(var j in f)for(var i in f[j])c.event.add(this,j,f[j][i],f[j][i].data)}}})}function sa(a,b,d){var f,e,j;b=b&&b[0]?b[0].ownerDocument||b[0]:s;if(a.length===1&&typeof a[0]==="string"&&a[0].length<512&&b===s&&!ta.test(a[0])&&(c.support.checkClone||!ua.test(a[0]))){e=
+true;if(j=c.fragments[a[0]])if(j!==1)f=j}if(!f){f=b.createDocumentFragment();c.clean(a,b,f,d)}if(e)c.fragments[a[0]]=j?f:1;return{fragment:f,cacheable:e}}function K(a,b){var d={};c.each(va.concat.apply([],va.slice(0,b)),function(){d[this]=a});return d}function wa(a){return"scrollTo"in a&&a.document?a:a.nodeType===9?a.defaultView||a.parentWindow:false}var c=function(a,b){return new c.fn.init(a,b)},Ra=A.jQuery,Sa=A.$,s=A.document,T,Ta=/^[^<]*(<[\w\W]+>)[^>]*$|^#([\w-]+)$/,Ua=/^.[^:#\[\.,]*$/,Va=/\S/,
+Wa=/^(\s|\u00A0)+|(\s|\u00A0)+$/g,Xa=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,P=navigator.userAgent,xa=false,Q=[],L,$=Object.prototype.toString,aa=Object.prototype.hasOwnProperty,ba=Array.prototype.push,R=Array.prototype.slice,ya=Array.prototype.indexOf;c.fn=c.prototype={init:function(a,b){var d,f;if(!a)return this;if(a.nodeType){this.context=this[0]=a;this.length=1;return this}if(a==="body"&&!b){this.context=s;this[0]=s.body;this.selector="body";this.length=1;return this}if(typeof a==="string")if((d=Ta.exec(a))&&
+(d[1]||!b))if(d[1]){f=b?b.ownerDocument||b:s;if(a=Xa.exec(a))if(c.isPlainObject(b)){a=[s.createElement(a[1])];c.fn.attr.call(a,b,true)}else a=[f.createElement(a[1])];else{a=sa([d[1]],[f]);a=(a.cacheable?a.fragment.cloneNode(true):a.fragment).childNodes}return c.merge(this,a)}else{if(b=s.getElementById(d[2])){if(b.id!==d[2])return T.find(a);this.length=1;this[0]=b}this.context=s;this.selector=a;return this}else if(!b&&/^\w+$/.test(a)){this.selector=a;this.context=s;a=s.getElementsByTagName(a);return c.merge(this,
+a)}else return!b||b.jquery?(b||T).find(a):c(b).find(a);else if(c.isFunction(a))return T.ready(a);if(a.selector!==w){this.selector=a.selector;this.context=a.context}return c.makeArray(a,this)},selector:"",jquery:"1.4.2",length:0,size:function(){return this.length},toArray:function(){return R.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this.slice(a)[0]:this[a]},pushStack:function(a,b,d){var f=c();c.isArray(a)?ba.apply(f,a):c.merge(f,a);f.prevObject=this;f.context=this.context;if(b===
+"find")f.selector=this.selector+(this.selector?" ":"")+d;else if(b)f.selector=this.selector+"."+b+"("+d+")";return f},each:function(a,b){return c.each(this,a,b)},ready:function(a){c.bindReady();if(c.isReady)a.call(s,c);else Q&&Q.push(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(R.apply(this,arguments),"slice",R.call(arguments).join(","))},map:function(a){return this.pushStack(c.map(this,
+function(b,d){return a.call(b,d,b)}))},end:function(){return this.prevObject||c(null)},push:ba,sort:[].sort,splice:[].splice};c.fn.init.prototype=c.fn;c.extend=c.fn.extend=function(){var a=arguments[0]||{},b=1,d=arguments.length,f=false,e,j,i,o;if(typeof a==="boolean"){f=a;a=arguments[1]||{};b=2}if(typeof a!=="object"&&!c.isFunction(a))a={};if(d===b){a=this;--b}for(;b<d;b++)if((e=arguments[b])!=null)for(j in e){i=a[j];o=e[j];if(a!==o)if(f&&o&&(c.isPlainObject(o)||c.isArray(o))){i=i&&(c.isPlainObject(i)||
+c.isArray(i))?i:c.isArray(o)?[]:{};a[j]=c.extend(f,i,o)}else if(o!==w)a[j]=o}return a};c.extend({noConflict:function(a){A.$=Sa;if(a)A.jQuery=Ra;return c},isReady:false,ready:function(){if(!c.isReady){if(!s.body)return setTimeout(c.ready,13);c.isReady=true;if(Q){for(var a,b=0;a=Q[b++];)a.call(s,c);Q=null}c.fn.triggerHandler&&c(s).triggerHandler("ready")}},bindReady:function(){if(!xa){xa=true;if(s.readyState==="complete")return c.ready();if(s.addEventListener){s.addEventListener("DOMContentLoaded",
+L,false);A.addEventListener("loaddisabled",c.ready,false)}else if(s.attachEvent){s.attachEvent("onreadystatechange",L);A.attachEvent("onloaddisabled",c.ready);var a=false;try{a=A.frameElement==null}catch(b){}s.documentElement.doScroll&&a&&ma()}}},isFunction:function(a){return $.call(a)==="[object Function]"},isArray:function(a){return $.call(a)==="[object Array]"},isPlainObject:function(a){if(!a||$.call(a)!=="[object Object]"||a.nodeType||a.setInterval)return false;if(a.constructor&&!aa.call(a,"constructor")&&!aa.call(a.constructor.prototype,
+"isPrototypeOf"))return false;var b;for(b in a);return b===w||aa.call(a,b)},isEmptyObject:function(a){for(var b in a)return false;return true},error:function(a){throw a;},parseJSON:function(a){if(typeof a!=="string"||!a)return null;a=c.trim(a);if(/^[\],:{}\s]*$/.test(a.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,"")))return A.JSON&&A.JSON.parse?A.JSON.parse(a):(new Function("return "+
+a))();else c.error("Invalid JSON: "+a)},noop:function(){},globalEval:function(a){if(a&&Va.test(a)){var b=s.getElementsByTagName("head")[0]||s.documentElement,d=s.createElement("script");d.type="text/javascript";if(c.support.scriptEval)d.appendChild(s.createTextNode(a));else d.text=a;b.insertBefore(d,b.firstChild);b.removeChild(d)}},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,b,d){var f,e=0,j=a.length,i=j===w||c.isFunction(a);if(d)if(i)for(f in a){if(b.apply(a[f],
+d)===false)break}else for(;e<j;){if(b.apply(a[e++],d)===false)break}else if(i)for(f in a){if(b.call(a[f],f,a[f])===false)break}else for(d=a[0];e<j&&b.call(d,e,d)!==false;d=a[++e]);return a},trim:function(a){return(a||"").replace(Wa,"")},makeArray:function(a,b){b=b||[];if(a!=null)a.length==null||typeof a==="string"||c.isFunction(a)||typeof a!=="function"&&a.setInterval?ba.call(b,a):c.merge(b,a);return b},inArray:function(a,b){if(b.indexOf)return b.indexOf(a);for(var d=0,f=b.length;d<f;d++)if(b[d]===
+a)return d;return-1},merge:function(a,b){var d=a.length,f=0;if(typeof b.length==="number")for(var e=b.length;f<e;f++)a[d++]=b[f];else for(;b[f]!==w;)a[d++]=b[f++];a.length=d;return a},grep:function(a,b,d){for(var f=[],e=0,j=a.length;e<j;e++)!d!==!b(a[e],e)&&f.push(a[e]);return f},map:function(a,b,d){for(var f=[],e,j=0,i=a.length;j<i;j++){e=b(a[j],j,d);if(e!=null)f[f.length]=e}return f.concat.apply([],f)},guid:1,proxy:function(a,b,d){if(arguments.length===2)if(typeof b==="string"){d=a;a=d[b];b=w}else if(b&&
+!c.isFunction(b)){d=b;b=w}if(!b&&a)b=function(){return a.apply(d||this,arguments)};if(a)b.guid=a.guid=a.guid||b.guid||c.guid++;return b},uaMatch:function(a){a=a.toLowerCase();a=/(webkit)[ \/]([\w.]+)/.exec(a)||/(opera)(?:.*version)?[ \/]([\w.]+)/.exec(a)||/(msie) ([\w.]+)/.exec(a)||!/compatible/.test(a)&&/(mozilla)(?:.*? rv:([\w.]+))?/.exec(a)||[];return{browser:a[1]||"",version:a[2]||"0"}},browser:{}});P=c.uaMatch(P);if(P.browser){c.browser[P.browser]=true;c.browser.version=P.version}if(c.browser.webkit)c.browser.safari=
+true;if(ya)c.inArray=function(a,b){return ya.call(b,a)};T=c(s);if(s.addEventListener)L=function(){s.removeEventListener("DOMContentLoaded",L,false);c.ready()};else if(s.attachEvent)L=function(){if(s.readyState==="complete"){s.detachEvent("onreadystatechange",L);c.ready()}};(function(){c.support={};var a=s.documentElement,b=s.createElement("script"),d=s.createElement("div"),f="script"+J();d.style.display="none";d.innerHTML=" <link/><table></table><a href='/a' style='color:red;float:left;opacity:.55;'>a</a><input type='checkbox'/>";
+var e=d.getElementsByTagName("*"),j=d.getElementsByTagName("a")[0];if(!(!e||!e.length||!j)){c.support={leadingWhitespace:d.firstChild.nodeType===3,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/red/.test(j.getAttribute("style")),hrefNormalized:j.getAttribute("href")==="/a",opacity:/^0.55$/.test(j.style.opacity),cssFloat:!!j.style.cssFloat,checkOn:d.getElementsByTagName("input")[0].value==="on",optSelected:s.createElement("select").appendChild(s.createElement("option")).selected,
+parentNode:d.removeChild(d.appendChild(s.createElement("div"))).parentNode===null,deleteExpando:true,checkClone:false,scriptEval:false,noCloneEvent:true,boxModel:null};b.type="text/javascript";try{b.appendChild(s.createTextNode("window."+f+"=1;"))}catch(i){}a.insertBefore(b,a.firstChild);if(A[f]){c.support.scriptEval=true;delete A[f]}try{delete b.test}catch(o){c.support.deleteExpando=false}a.removeChild(b);if(d.attachEvent&&d.fireEvent){d.attachEvent("onclick",function k(){c.support.noCloneEvent=
+false;d.detachEvent("onclick",k)});d.cloneNode(true).fireEvent("onclick")}d=s.createElement("div");d.innerHTML="<input type='radio' name='radiotest' checked='checked'/>";a=s.createDocumentFragment();a.appendChild(d.firstChild);c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var k=s.createElement("div");k.style.width=k.style.paddingLeft="1px";s.body.appendChild(k);c.boxModel=c.support.boxModel=k.offsetWidth===2;s.body.removeChild(k).style.display="none"});a=function(k){var n=
+s.createElement("div");k="on"+k;var r=k in n;if(!r){n.setAttribute(k,"return;");r=typeof n[k]==="function"}return r};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=e=j=null}})();c.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var G="jQuery"+J(),Ya=0,za={};c.extend({cache:{},expando:G,noData:{embed:true,object:true,
+applet:true},data:function(a,b,d){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var f=a[G],e=c.cache;if(!f&&typeof b==="string"&&d===w)return null;f||(f=++Ya);if(typeof b==="object"){a[G]=f;e[f]=c.extend(true,{},b)}else if(!e[f]){a[G]=f;e[f]={}}a=e[f];if(d!==w)a[b]=d;return typeof b==="string"?a[b]:a}},removeData:function(a,b){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var d=a[G],f=c.cache,e=f[d];if(b){if(e){delete e[b];c.isEmptyObject(e)&&c.removeData(a)}}else{if(c.support.deleteExpando)delete a[c.expando];
+else a.removeAttribute&&a.removeAttribute(c.expando);delete f[d]}}}});c.fn.extend({data:function(a,b){if(typeof a==="undefined"&&this.length)return c.data(this[0]);else if(typeof a==="object")return this.each(function(){c.data(this,a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===w){var f=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(f===w&&this.length)f=c.data(this[0],a);return f===w&&d[1]?this.data(d[0]):f}else return this.trigger("setData"+d[1]+"!",[d[0],b]).each(function(){c.data(this,
+a,b)})},removeData:function(a){return this.each(function(){c.removeData(this,a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var f=c.data(a,b);if(!d)return f||[];if(!f||c.isArray(d))f=c.data(a,b,c.makeArray(d));else f.push(d);return f}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),f=d.shift();if(f==="inprogress")f=d.shift();if(f){b==="fx"&&d.unshift("inprogress");f.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b===
+w)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this,a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this,a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]||a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var Aa=/[\n\t]/g,ca=/\s+/,Za=/\r/g,$a=/href|src|style/,ab=/(button|input)/i,bb=/(button|input|object|select|textarea)/i,
+cb=/^(a|area)$/i,Ba=/radio|checkbox/;c.fn.extend({attr:function(a,b){return X(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this,a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(n){var r=c(this);r.addClass(a.call(this,n,r.attr("class")))});if(a&&typeof a==="string")for(var b=(a||"").split(ca),d=0,f=this.length;d<f;d++){var e=this[d];if(e.nodeType===1)if(e.className){for(var j=" "+e.className+" ",
+i=e.className,o=0,k=b.length;o<k;o++)if(j.indexOf(" "+b[o]+" ")<0)i+=" "+b[o];e.className=c.trim(i)}else e.className=a}return this},removeClass:function(a){if(c.isFunction(a))return this.each(function(k){var n=c(this);n.removeClass(a.call(this,k,n.attr("class")))});if(a&&typeof a==="string"||a===w)for(var b=(a||"").split(ca),d=0,f=this.length;d<f;d++){var e=this[d];if(e.nodeType===1&&e.className)if(a){for(var j=(" "+e.className+" ").replace(Aa," "),i=0,o=b.length;i<o;i++)j=j.replace(" "+b[i]+" ",
+" ");e.className=c.trim(j)}else e.className=""}return this},toggleClass:function(a,b){var d=typeof a,f=typeof b==="boolean";if(c.isFunction(a))return this.each(function(e){var j=c(this);j.toggleClass(a.call(this,e,j.attr("class"),b),b)});return this.each(function(){if(d==="string")for(var e,j=0,i=c(this),o=b,k=a.split(ca);e=k[j++];){o=f?o:!i.hasClass(e);i[o?"addClass":"removeClass"](e)}else if(d==="undefined"||d==="boolean"){this.className&&c.data(this,"__className__",this.className);this.className=
+this.className||a===false?"":c.data(this,"__className__")||""}})},hasClass:function(a){a=" "+a+" ";for(var b=0,d=this.length;b<d;b++)if((" "+this[b].className+" ").replace(Aa," ").indexOf(a)>-1)return true;return false},val:function(a){if(a===w){var b=this[0];if(b){if(c.nodeName(b,"option"))return(b.attributes.value||{}).specified?b.value:b.text;if(c.nodeName(b,"select")){var d=b.selectedIndex,f=[],e=b.options;b=b.type==="select-one";if(d<0)return null;var j=b?d:0;for(d=b?d+1:e.length;j<d;j++){var i=
+e[j];if(i.selected){a=c(i).val();if(b)return a;f.push(a)}}return f}if(Ba.test(b.type)&&!c.support.checkOn)return b.getAttribute("value")===null?"on":b.value;return(b.value||"").replace(Za,"")}return w}var o=c.isFunction(a);return this.each(function(k){var n=c(this),r=a;if(this.nodeType===1){if(o)r=a.call(this,k,n.val());if(typeof r==="number")r+="";if(c.isArray(r)&&Ba.test(this.type))this.checked=c.inArray(n.val(),r)>=0;else if(c.nodeName(this,"select")){var u=c.makeArray(r);c("option",this).each(function(){this.selected=
+c.inArray(c(this).val(),u)>=0});if(!u.length)this.selectedIndex=-1}else this.value=r}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,f){if(!a||a.nodeType===3||a.nodeType===8)return w;if(f&&b in c.attrFn)return c(a)[b](d);f=a.nodeType!==1||!c.isXMLDoc(a);var e=d!==w;b=f&&c.props[b]||b;if(a.nodeType===1){var j=$a.test(b);if(b in a&&f&&!j){if(e){b==="type"&&ab.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed");
+a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:bb.test(a.nodeName)||cb.test(a.nodeName)&&a.href?0:w;return a[b]}if(!c.support.style&&f&&b==="style"){if(e)a.style.cssText=""+d;return a.style.cssText}e&&a.setAttribute(b,""+d);a=!c.support.hrefNormalized&&f&&j?a.getAttribute(b,2):a.getAttribute(b);return a===null?w:a}return c.style(a,b,d)}});var O=/\.(.*)$/,db=function(a){return a.replace(/[^\w\s\.\|`]/g,
+function(b){return"\\"+b})};c.event={add:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){if(a.setInterval&&a!==A&&!a.frameElement)a=A;var e,j;if(d.handler){e=d;d=e.handler}if(!d.guid)d.guid=c.guid++;if(j=c.data(a)){var i=j.events=j.events||{},o=j.handle;if(!o)j.handle=o=function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(o.elem,arguments):w};o.elem=a;b=b.split(" ");for(var k,n=0,r;k=b[n++];){j=e?c.extend({},e):{handler:d,data:f};if(k.indexOf(".")>-1){r=k.split(".");
+k=r.shift();j.namespace=r.slice(0).sort().join(".")}else{r=[];j.namespace=""}j.type=k;j.guid=d.guid;var u=i[k],z=c.event.special[k]||{};if(!u){u=i[k]=[];if(!z.setup||z.setup.call(a,f,r,o)===false)if(a.addEventListener)a.addEventListener(k,o,false);else a.attachEvent&&a.attachEvent("on"+k,o)}if(z.add){z.add.call(a,j);if(!j.handler.guid)j.handler.guid=d.guid}u.push(j);c.event.global[k]=true}a=null}}},global:{},remove:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){var e,j=0,i,o,k,n,r,u,z=c.data(a),
+C=z&&z.events;if(z&&C){if(b&&b.type){d=b.handler;b=b.type}if(!b||typeof b==="string"&&b.charAt(0)==="."){b=b||"";for(e in C)c.event.remove(a,e+b)}else{for(b=b.split(" ");e=b[j++];){n=e;i=e.indexOf(".")<0;o=[];if(!i){o=e.split(".");e=o.shift();k=new RegExp("(^|\\.)"+c.map(o.slice(0).sort(),db).join("\\.(?:.*\\.)?")+"(\\.|$)")}if(r=C[e])if(d){n=c.event.special[e]||{};for(B=f||0;B<r.length;B++){u=r[B];if(d.guid===u.guid){if(i||k.test(u.namespace)){f==null&&r.splice(B--,1);n.remove&&n.remove.call(a,u)}if(f!=
+null)break}}if(r.length===0||f!=null&&r.length===1){if(!n.teardown||n.teardown.call(a,o)===false)Ca(a,e,z.handle);delete C[e]}}else for(var B=0;B<r.length;B++){u=r[B];if(i||k.test(u.namespace)){c.event.remove(a,n,u.handler,B);r.splice(B--,1)}}}if(c.isEmptyObject(C)){if(b=z.handle)b.elem=null;delete z.events;delete z.handle;c.isEmptyObject(z)&&c.removeData(a)}}}}},trigger:function(a,b,d,f){var e=a.type||a;if(!f){a=typeof a==="object"?a[G]?a:c.extend(c.Event(e),a):c.Event(e);if(e.indexOf("!")>=0){a.type=
+e=e.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();c.event.global[e]&&c.each(c.cache,function(){this.events&&this.events[e]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType===8)return w;a.result=w;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(f=c.data(d,"handle"))&&f.apply(d,b);f=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+e]&&d["on"+e].apply(d,b)===false)a.result=false}catch(j){}if(!a.isPropagationStopped()&&
+f)c.event.trigger(a,b,f,true);else if(!a.isDefaultPrevented()){f=a.target;var i,o=c.nodeName(f,"a")&&e==="click",k=c.event.special[e]||{};if((!k._default||k._default.call(d,a)===false)&&!o&&!(f&&f.nodeName&&c.noData[f.nodeName.toLowerCase()])){try{if(f[e]){if(i=f["on"+e])f["on"+e]=null;c.event.triggered=true;f[e]()}}catch(n){}if(i)f["on"+e]=i;c.event.triggered=false}}},handle:function(a){var b,d,f,e;a=arguments[0]=c.event.fix(a||A.event);a.currentTarget=this;b=a.type.indexOf(".")<0&&!a.exclusive;
+if(!b){d=a.type.split(".");a.type=d.shift();f=new RegExp("(^|\\.)"+d.slice(0).sort().join("\\.(?:.*\\.)?")+"(\\.|$)")}e=c.data(this,"events");d=e[a.type];if(e&&d){d=d.slice(0);e=0;for(var j=d.length;e<j;e++){var i=d[e];if(b||f.test(i.namespace)){a.handler=i.handler;a.data=i.data;a.handleObj=i;i=i.handler.apply(this,arguments);if(i!==w){a.result=i;if(i===false){a.preventDefault();a.stopPropagation()}}if(a.isImmediatePropagationStopped())break}}}return a.result},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),
+fix:function(a){if(a[G])return a;var b=a;a=c.Event(b);for(var d=this.props.length,f;d;){f=this.props[--d];a[f]=b[f]}if(!a.target)a.target=a.srcElement||s;if(a.target.nodeType===3)a.target=a.target.parentNode;if(!a.relatedTarget&&a.fromElement)a.relatedTarget=a.fromElement===a.target?a.toElement:a.fromElement;if(a.pageX==null&&a.clientX!=null){b=s.documentElement;d=s.body;a.pageX=a.clientX+(b&&b.scrollLeft||d&&d.scrollLeft||0)-(b&&b.clientLeft||d&&d.clientLeft||0);a.pageY=a.clientY+(b&&b.scrollTop||
+d&&d.scrollTop||0)-(b&&b.clientTop||d&&d.clientTop||0)}if(!a.which&&(a.charCode||a.charCode===0?a.charCode:a.keyCode))a.which=a.charCode||a.keyCode;if(!a.metaKey&&a.ctrlKey)a.metaKey=a.ctrlKey;if(!a.which&&a.button!==w)a.which=a.button&1?1:a.button&2?3:a.button&4?2:0;return a},guid:1E8,proxy:c.proxy,special:{ready:{setup:c.bindReady,teardown:c.noop},live:{add:function(a){c.event.add(this,a.origType,c.extend({},a,{handler:oa}))},remove:function(a){var b=true,d=a.origType.replace(O,"");c.each(c.data(this,
+"events").live||[],function(){if(d===this.origType.replace(O,""))return b=false});b&&c.event.remove(this,a.origType,oa)}},beforeunloaddisabled:{setup:function(a,b,d){if(this.setInterval)this.onbeforeunloaddisabled=d;return false},teardown:function(a,b){if(this.onbeforeunloaddisabled===b)this.onbeforeunloaddisabled=null}}}};var Ca=s.removeEventListener?function(a,b,d){a.removeEventListener(b,d,false)}:function(a,b,d){a.detachEvent("on"+b,d)};c.Event=function(a){if(!this.preventDefault)return new c.Event(a);if(a&&a.type){this.originalEvent=
+a;this.type=a.type}else this.type=a;this.timeStamp=J();this[G]=true};c.Event.prototype={preventDefault:function(){this.isDefaultPrevented=Z;var a=this.originalEvent;if(a){a.preventDefault&&a.preventDefault();a.returnValue=false}},stopPropagation:function(){this.isPropagationStopped=Z;var a=this.originalEvent;if(a){a.stopPropagation&&a.stopPropagation();a.cancelBubble=true}},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=Z;this.stopPropagation()},isDefaultPrevented:Y,isPropagationStopped:Y,
+isImmediatePropagationStopped:Y};var Da=function(a){var b=a.relatedTarget;try{for(;b&&b!==this;)b=b.parentNode;if(b!==this){a.type=a.data;c.event.handle.apply(this,arguments)}}catch(d){}},Ea=function(a){a.type=a.data;c.event.handle.apply(this,arguments)};c.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){c.event.special[a]={setup:function(d){c.event.add(this,b,d&&d.selector?Ea:Da,a)},teardown:function(d){c.event.remove(this,b,d&&d.selector?Ea:Da)}}});if(!c.support.submitBubbles)c.event.special.submit=
+{setup:function(){if(this.nodeName.toLowerCase()!=="form"){c.event.add(this,"click.specialSubmit",function(a){var b=a.target,d=b.type;if((d==="submit"||d==="image")&&c(b).closest("form").length)return na("submit",this,arguments)});c.event.add(this,"keypress.specialSubmit",function(a){var b=a.target,d=b.type;if((d==="text"||d==="password")&&c(b).closest("form").length&&a.keyCode===13)return na("submit",this,arguments)})}else return false},teardown:function(){c.event.remove(this,".specialSubmit")}};
+if(!c.support.changeBubbles){var da=/textarea|input|select/i,ea,Fa=function(a){var b=a.type,d=a.value;if(b==="radio"||b==="checkbox")d=a.checked;else if(b==="select-multiple")d=a.selectedIndex>-1?c.map(a.options,function(f){return f.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d},fa=function(a,b){var d=a.target,f,e;if(!(!da.test(d.nodeName)||d.readOnly)){f=c.data(d,"_change_data");e=Fa(d);if(a.type!=="focusout"||d.type!=="radio")c.data(d,"_change_data",
+e);if(!(f===w||e===f))if(f!=null||e){a.type="change";return c.event.trigger(a,b,d)}}};c.event.special.change={filters:{focusout:fa,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return fa.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return fa.call(this,a)},beforeactivate:function(a){a=a.target;c.data(a,
+"_change_data",Fa(a))}},setup:function(){if(this.type==="file")return false;for(var a in ea)c.event.add(this,a+".specialChange",ea[a]);return da.test(this.nodeName)},teardown:function(){c.event.remove(this,".specialChange");return da.test(this.nodeName)}};ea=c.event.special.change.filters}s.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(f){f=c.event.fix(f);f.type=b;return c.event.handle.call(this,f)}c.event.special[b]={setup:function(){this.addEventListener(a,
+d,true)},teardown:function(){this.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,f,e){if(typeof d==="object"){for(var j in d)this[b](j,f,d[j],e);return this}if(c.isFunction(f)){e=f;f=w}var i=b==="one"?c.proxy(e,function(k){c(this).unbind(k,i);return e.apply(this,arguments)}):e;if(d==="unloaddisabled"&&b!=="one")this.one(d,f,e);else{j=0;for(var o=this.length;j<o;j++)c.event.add(this[j],d,i,f)}return this}});c.fn.extend({unbind:function(a,b){if(typeof a==="object"&&
+!a.preventDefault)for(var d in a)this.unbind(d,a[d]);else{d=0;for(var f=this.length;d<f;d++)c.event.remove(this[d],a,b)}return this},delegate:function(a,b,d,f){return this.live(b,d,f,a)},undelegate:function(a,b,d){return arguments.length===0?this.unbind("live"):this.die(b,null,d,a)},trigger:function(a,b){return this.each(function(){c.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0]){a=c.Event(a);a.preventDefault();a.stopPropagation();c.event.trigger(a,b,this[0]);return a.result}},
+toggle:function(a){for(var b=arguments,d=1;d<b.length;)c.proxy(a,b[d++]);return this.click(c.proxy(a,function(f){var e=(c.data(this,"lastToggle"+a.guid)||0)%d;c.data(this,"lastToggle"+a.guid,e+1);f.preventDefault();return b[e].apply(this,arguments)||false}))},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}});var Ga={focus:"focusin",blur:"focusout",mouseenter:"mouseover",mouseleave:"mouseout"};c.each(["live","die"],function(a,b){c.fn[b]=function(d,f,e,j){var i,o=0,k,n,r=j||this.selector,
+u=j?this:c(this.context);if(c.isFunction(f)){e=f;f=w}for(d=(d||"").split(" ");(i=d[o++])!=null;){j=O.exec(i);k="";if(j){k=j[0];i=i.replace(O,"")}if(i==="hover")d.push("mouseenter"+k,"mouseleave"+k);else{n=i;if(i==="focus"||i==="blur"){d.push(Ga[i]+k);i+=k}else i=(Ga[i]||i)+k;b==="live"?u.each(function(){c.event.add(this,pa(i,r),{data:f,selector:r,handler:e,origType:i,origHandler:e,preType:n})}):u.unbind(pa(i,r),e)}}return this}});c.each("blur focus focusin focusout loaddisabled resize scroll unloaddisabled click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error".split(" "),
+function(a,b){c.fn[b]=function(d){return d?this.bind(b,d):this.trigger(b)};if(c.attrFn)c.attrFn[b]=true});A.attachEvent&&!A.addEventListener&&A.attachEvent("onunloaddisabled",function(){for(var a in c.cache)if(c.cache[a].handle)try{c.event.remove(c.cache[a].handle.elem)}catch(b){}});(function(){function a(g){for(var h="",l,m=0;g[m];m++){l=g[m];if(l.nodeType===3||l.nodeType===4)h+=l.nodeValue;else if(l.nodeType!==8)h+=a(l.childNodes)}return h}function b(g,h,l,m,q,p){q=0;for(var v=m.length;q<v;q++){var t=m[q];
+if(t){t=t[g];for(var y=false;t;){if(t.sizcache===l){y=m[t.sizset];break}if(t.nodeType===1&&!p){t.sizcache=l;t.sizset=q}if(t.nodeName.toLowerCase()===h){y=t;break}t=t[g]}m[q]=y}}}function d(g,h,l,m,q,p){q=0;for(var v=m.length;q<v;q++){var t=m[q];if(t){t=t[g];for(var y=false;t;){if(t.sizcache===l){y=m[t.sizset];break}if(t.nodeType===1){if(!p){t.sizcache=l;t.sizset=q}if(typeof h!=="string"){if(t===h){y=true;break}}else if(k.filter(h,[t]).length>0){y=t;break}}t=t[g]}m[q]=y}}}var f=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
+e=0,j=Object.prototype.toString,i=false,o=true;[0,0].sort(function(){o=false;return 0});var k=function(g,h,l,m){l=l||[];var q=h=h||s;if(h.nodeType!==1&&h.nodeType!==9)return[];if(!g||typeof g!=="string")return l;for(var p=[],v,t,y,S,H=true,M=x(h),I=g;(f.exec(""),v=f.exec(I))!==null;){I=v[3];p.push(v[1]);if(v[2]){S=v[3];break}}if(p.length>1&&r.exec(g))if(p.length===2&&n.relative[p[0]])t=ga(p[0]+p[1],h);else for(t=n.relative[p[0]]?[h]:k(p.shift(),h);p.length;){g=p.shift();if(n.relative[g])g+=p.shift();
+t=ga(g,t)}else{if(!m&&p.length>1&&h.nodeType===9&&!M&&n.match.ID.test(p[0])&&!n.match.ID.test(p[p.length-1])){v=k.find(p.shift(),h,M);h=v.expr?k.filter(v.expr,v.set)[0]:v.set[0]}if(h){v=m?{expr:p.pop(),set:z(m)}:k.find(p.pop(),p.length===1&&(p[0]==="~"||p[0]==="+")&&h.parentNode?h.parentNode:h,M);t=v.expr?k.filter(v.expr,v.set):v.set;if(p.length>0)y=z(t);else H=false;for(;p.length;){var D=p.pop();v=D;if(n.relative[D])v=p.pop();else D="";if(v==null)v=h;n.relative[D](y,v,M)}}else y=[]}y||(y=t);y||k.error(D||
+g);if(j.call(y)==="[object Array]")if(H)if(h&&h.nodeType===1)for(g=0;y[g]!=null;g++){if(y[g]&&(y[g]===true||y[g].nodeType===1&&E(h,y[g])))l.push(t[g])}else for(g=0;y[g]!=null;g++)y[g]&&y[g].nodeType===1&&l.push(t[g]);else l.push.apply(l,y);else z(y,l);if(S){k(S,q,l,m);k.uniqueSort(l)}return l};k.uniqueSort=function(g){if(B){i=o;g.sort(B);if(i)for(var h=1;h<g.length;h++)g[h]===g[h-1]&&g.splice(h--,1)}return g};k.matches=function(g,h){return k(g,null,null,h)};k.find=function(g,h,l){var m,q;if(!g)return[];
+for(var p=0,v=n.order.length;p<v;p++){var t=n.order[p];if(q=n.leftMatch[t].exec(g)){var y=q[1];q.splice(1,1);if(y.substr(y.length-1)!=="\\"){q[1]=(q[1]||"").replace(/\\/g,"");m=n.find[t](q,h,l);if(m!=null){g=g.replace(n.match[t],"");break}}}}m||(m=h.getElementsByTagName("*"));return{set:m,expr:g}};k.filter=function(g,h,l,m){for(var q=g,p=[],v=h,t,y,S=h&&h[0]&&x(h[0]);g&&h.length;){for(var H in n.filter)if((t=n.leftMatch[H].exec(g))!=null&&t[2]){var M=n.filter[H],I,D;D=t[1];y=false;t.splice(1,1);if(D.substr(D.length-
+1)!=="\\"){if(v===p)p=[];if(n.preFilter[H])if(t=n.preFilter[H](t,v,l,p,m,S)){if(t===true)continue}else y=I=true;if(t)for(var U=0;(D=v[U])!=null;U++)if(D){I=M(D,t,U,v);var Ha=m^!!I;if(l&&I!=null)if(Ha)y=true;else v[U]=false;else if(Ha){p.push(D);y=true}}if(I!==w){l||(v=p);g=g.replace(n.match[H],"");if(!y)return[];break}}}if(g===q)if(y==null)k.error(g);else break;q=g}return v};k.error=function(g){throw"Syntax error, unrecognized expression: "+g;};var n=k.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF-]|\\.)+)/,
+CLASS:/\.((?:[\w\u00c0-\uFFFF-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(g){return g.getAttribute("href")}},
+relative:{"+":function(g,h){var l=typeof h==="string",m=l&&!/\W/.test(h);l=l&&!m;if(m)h=h.toLowerCase();m=0;for(var q=g.length,p;m<q;m++)if(p=g[m]){for(;(p=p.previousSibling)&&p.nodeType!==1;);g[m]=l||p&&p.nodeName.toLowerCase()===h?p||false:p===h}l&&k.filter(h,g,true)},">":function(g,h){var l=typeof h==="string";if(l&&!/\W/.test(h)){h=h.toLowerCase();for(var m=0,q=g.length;m<q;m++){var p=g[m];if(p){l=p.parentNode;g[m]=l.nodeName.toLowerCase()===h?l:false}}}else{m=0;for(q=g.length;m<q;m++)if(p=g[m])g[m]=
+l?p.parentNode:p.parentNode===h;l&&k.filter(h,g,true)}},"":function(g,h,l){var m=e++,q=d;if(typeof h==="string"&&!/\W/.test(h)){var p=h=h.toLowerCase();q=b}q("parentNode",h,m,g,p,l)},"~":function(g,h,l){var m=e++,q=d;if(typeof h==="string"&&!/\W/.test(h)){var p=h=h.toLowerCase();q=b}q("previousSibling",h,m,g,p,l)}},find:{ID:function(g,h,l){if(typeof h.getElementById!=="undefined"&&!l)return(g=h.getElementById(g[1]))?[g]:[]},NAME:function(g,h){if(typeof h.getElementsByName!=="undefined"){var l=[];
+h=h.getElementsByName(g[1]);for(var m=0,q=h.length;m<q;m++)h[m].getAttribute("name")===g[1]&&l.push(h[m]);return l.length===0?null:l}},TAG:function(g,h){return h.getElementsByTagName(g[1])}},preFilter:{CLASS:function(g,h,l,m,q,p){g=" "+g[1].replace(/\\/g,"")+" ";if(p)return g;p=0;for(var v;(v=h[p])!=null;p++)if(v)if(q^(v.className&&(" "+v.className+" ").replace(/[\t\n]/g," ").indexOf(g)>=0))l||m.push(v);else if(l)h[p]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()},
+CHILD:function(g){if(g[1]==="nth"){var h=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=h[1]+(h[2]||1)-0;g[3]=h[3]-0}g[0]=e++;return g},ATTR:function(g,h,l,m,q,p){h=g[1].replace(/\\/g,"");if(!p&&n.attrMap[h])g[1]=n.attrMap[h];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,h,l,m,q){if(g[1]==="not")if((f.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=k(g[3],null,null,h);else{g=k.filter(g[3],h,l,true^q);l||m.push.apply(m,
+g);return false}else if(n.match.POS.test(g[0])||n.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled===true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,h,l){return!!k(l[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)},
+text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"===g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}},
+setFilters:{first:function(g,h){return h===0},last:function(g,h,l,m){return h===m.length-1},even:function(g,h){return h%2===0},odd:function(g,h){return h%2===1},lt:function(g,h,l){return h<l[3]-0},gt:function(g,h,l){return h>l[3]-0},nth:function(g,h,l){return l[3]-0===h},eq:function(g,h,l){return l[3]-0===h}},filter:{PSEUDO:function(g,h,l,m){var q=h[1],p=n.filters[q];if(p)return p(g,l,h,m);else if(q==="contains")return(g.textContent||g.innerText||a([g])||"").indexOf(h[3])>=0;else if(q==="not"){h=
+h[3];l=0;for(m=h.length;l<m;l++)if(h[l]===g)return false;return true}else k.error("Syntax error, unrecognized expression: "+q)},CHILD:function(g,h){var l=h[1],m=g;switch(l){case "only":case "first":for(;m=m.previousSibling;)if(m.nodeType===1)return false;if(l==="first")return true;m=g;case "last":for(;m=m.nextSibling;)if(m.nodeType===1)return false;return true;case "nth":l=h[2];var q=h[3];if(l===1&&q===0)return true;h=h[0];var p=g.parentNode;if(p&&(p.sizcache!==h||!g.nodeIndex)){var v=0;for(m=p.firstChild;m;m=
+m.nextSibling)if(m.nodeType===1)m.nodeIndex=++v;p.sizcache=h}g=g.nodeIndex-q;return l===0?g===0:g%l===0&&g/l>=0}},ID:function(g,h){return g.nodeType===1&&g.getAttribute("id")===h},TAG:function(g,h){return h==="*"&&g.nodeType===1||g.nodeName.toLowerCase()===h},CLASS:function(g,h){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(h)>-1},ATTR:function(g,h){var l=h[1];g=n.attrHandle[l]?n.attrHandle[l](g):g[l]!=null?g[l]:g.getAttribute(l);l=g+"";var m=h[2];h=h[4];return g==null?m==="!=":m===
+"="?l===h:m==="*="?l.indexOf(h)>=0:m==="~="?(" "+l+" ").indexOf(h)>=0:!h?l&&g!==false:m==="!="?l!==h:m==="^="?l.indexOf(h)===0:m==="$="?l.substr(l.length-h.length)===h:m==="|="?l===h||l.substr(0,h.length+1)===h+"-":false},POS:function(g,h,l,m){var q=n.setFilters[h[2]];if(q)return q(g,l,h,m)}}},r=n.match.POS;for(var u in n.match){n.match[u]=new RegExp(n.match[u].source+/(?![^\[]*\])(?![^\(]*\))/.source);n.leftMatch[u]=new RegExp(/(^(?:.|\r|\n)*?)/.source+n.match[u].source.replace(/\\(\d+)/g,function(g,
+h){return"\\"+(h-0+1)}))}var z=function(g,h){g=Array.prototype.slice.call(g,0);if(h){h.push.apply(h,g);return h}return g};try{Array.prototype.slice.call(s.documentElement.childNodes,0)}catch(C){z=function(g,h){h=h||[];if(j.call(g)==="[object Array]")Array.prototype.push.apply(h,g);else if(typeof g.length==="number")for(var l=0,m=g.length;l<m;l++)h.push(g[l]);else for(l=0;g[l];l++)h.push(g[l]);return h}}var B;if(s.documentElement.compareDocumentPosition)B=function(g,h){if(!g.compareDocumentPosition||
+!h.compareDocumentPosition){if(g==h)i=true;return g.compareDocumentPosition?-1:1}g=g.compareDocumentPosition(h)&4?-1:g===h?0:1;if(g===0)i=true;return g};else if("sourceIndex"in s.documentElement)B=function(g,h){if(!g.sourceIndex||!h.sourceIndex){if(g==h)i=true;return g.sourceIndex?-1:1}g=g.sourceIndex-h.sourceIndex;if(g===0)i=true;return g};else if(s.createRange)B=function(g,h){if(!g.ownerDocument||!h.ownerDocument){if(g==h)i=true;return g.ownerDocument?-1:1}var l=g.ownerDocument.createRange(),m=
+h.ownerDocument.createRange();l.setStart(g,0);l.setEnd(g,0);m.setStart(h,0);m.setEnd(h,0);g=l.compareBoundaryPoints(Range.START_TO_END,m);if(g===0)i=true;return g};(function(){var g=s.createElement("div"),h="script"+(new Date).getTime();g.innerHTML="<a name='"+h+"'/>";var l=s.documentElement;l.insertBefore(g,l.firstChild);if(s.getElementById(h)){n.find.ID=function(m,q,p){if(typeof q.getElementById!=="undefined"&&!p)return(q=q.getElementById(m[1]))?q.id===m[1]||typeof q.getAttributeNode!=="undefined"&&
+q.getAttributeNode("id").nodeValue===m[1]?[q]:w:[]};n.filter.ID=function(m,q){var p=typeof m.getAttributeNode!=="undefined"&&m.getAttributeNode("id");return m.nodeType===1&&p&&p.nodeValue===q}}l.removeChild(g);l=g=null})();(function(){var g=s.createElement("div");g.appendChild(s.createComment(""));if(g.getElementsByTagName("*").length>0)n.find.TAG=function(h,l){l=l.getElementsByTagName(h[1]);if(h[1]==="*"){h=[];for(var m=0;l[m];m++)l[m].nodeType===1&&h.push(l[m]);l=h}return l};g.innerHTML="<a href='#'></a>";
+if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")n.attrHandle.href=function(h){return h.getAttribute("href",2)};g=null})();s.querySelectorAll&&function(){var g=k,h=s.createElement("div");h.innerHTML="<p class='TEST'></p>";if(!(h.querySelectorAll&&h.querySelectorAll(".TEST").length===0)){k=function(m,q,p,v){q=q||s;if(!v&&q.nodeType===9&&!x(q))try{return z(q.querySelectorAll(m),p)}catch(t){}return g(m,q,p,v)};for(var l in g)k[l]=g[l];h=null}}();
+(function(){var g=s.createElement("div");g.innerHTML="<div class='test e'></div><div class='test'></div>";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length===0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){n.order.splice(1,0,"CLASS");n.find.CLASS=function(h,l,m){if(typeof l.getElementsByClassName!=="undefined"&&!m)return l.getElementsByClassName(h[1])};g=null}}})();var E=s.compareDocumentPosition?function(g,h){return!!(g.compareDocumentPosition(h)&16)}:
+function(g,h){return g!==h&&(g.contains?g.contains(h):true)},x=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false},ga=function(g,h){var l=[],m="",q;for(h=h.nodeType?[h]:h;q=n.match.PSEUDO.exec(g);){m+=q[0];g=g.replace(n.match.PSEUDO,"")}g=n.relative[g]?g+"*":g;q=0;for(var p=h.length;q<p;q++)k(g,h[q],l);return k.filter(m,l)};c.find=k;c.expr=k.selectors;c.expr[":"]=c.expr.filters;c.unique=k.uniqueSort;c.text=a;c.isXMLDoc=x;c.contains=E})();var eb=/Until$/,fb=/^(?:parents|prevUntil|prevAll)/,
+gb=/,/;R=Array.prototype.slice;var Ia=function(a,b,d){if(c.isFunction(b))return c.grep(a,function(e,j){return!!b.call(e,j,e)===d});else if(b.nodeType)return c.grep(a,function(e){return e===b===d});else if(typeof b==="string"){var f=c.grep(a,function(e){return e.nodeType===1});if(Ua.test(b))return c.filter(b,f,!d);else b=c.filter(b,f)}return c.grep(a,function(e){return c.inArray(e,b)>=0===d})};c.fn.extend({find:function(a){for(var b=this.pushStack("","find",a),d=0,f=0,e=this.length;f<e;f++){d=b.length;
+c.find(a,this[f],b);if(f>0)for(var j=d;j<b.length;j++)for(var i=0;i<d;i++)if(b[i]===b[j]){b.splice(j--,1);break}}return b},has:function(a){var b=c(a);return this.filter(function(){for(var d=0,f=b.length;d<f;d++)if(c.contains(this,b[d]))return true})},not:function(a){return this.pushStack(Ia(this,a,false),"not",a)},filter:function(a){return this.pushStack(Ia(this,a,true),"filter",a)},is:function(a){return!!a&&c.filter(a,this).length>0},closest:function(a,b){if(c.isArray(a)){var d=[],f=this[0],e,j=
+{},i;if(f&&a.length){e=0;for(var o=a.length;e<o;e++){i=a[e];j[i]||(j[i]=c.expr.match.POS.test(i)?c(i,b||this.context):i)}for(;f&&f.ownerDocument&&f!==b;){for(i in j){e=j[i];if(e.jquery?e.index(f)>-1:c(f).is(e)){d.push({selector:i,elem:f});delete j[i]}}f=f.parentNode}}return d}var k=c.expr.match.POS.test(a)?c(a,b||this.context):null;return this.map(function(n,r){for(;r&&r.ownerDocument&&r!==b;){if(k?k.index(r)>-1:c(r).is(a))return r;r=r.parentNode}return null})},index:function(a){if(!a||typeof a===
+"string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){a=typeof a==="string"?c(a,b||this.context):c.makeArray(a);b=c.merge(this.get(),a);return this.pushStack(qa(a[0])||qa(b[0])?b:c.unique(b))},andSelf:function(){return this.add(this.prevObject)}});c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode",
+d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling",d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")?
+a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,f){var e=c.map(this,b,d);eb.test(a)||(f=d);if(f&&typeof f==="string")e=c.filter(f,e);e=this.length>1?c.unique(e):e;if((this.length>1||gb.test(f))&&fb.test(a))e=e.reverse();return this.pushStack(e,a,R.call(arguments).join(","))}});c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return c.find.matches(a,b)},dir:function(a,b,d){var f=[];for(a=a[b];a&&a.nodeType!==9&&(d===w||a.nodeType!==1||!c(a).is(d));){a.nodeType===
+1&&f.push(a);a=a[b]}return f},nth:function(a,b,d){b=b||1;for(var f=0;a;a=a[d])if(a.nodeType===1&&++f===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==b&&d.push(a);return d}});var Ja=/ jQuery\d+="(?:\d+|null)"/g,V=/^\s+/,Ka=/(<([\w:]+)[^>]*?)\/>/g,hb=/^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,La=/<([\w:]+)/,ib=/<tbody/i,jb=/<|&#?\w+;/,ta=/<script|<objectdisabled|<embeddisabled|<option|<style/i,ua=/checked\s*(?:[^=]|=\s*.checked.)/i,Ma=function(a,b,d){return hb.test(d)?
+a:b+"></"+d+">"},F={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]};F.optgroup=F.option;F.tbody=F.tfoot=F.colgroup=F.caption=F.thead;F.th=F.td;if(!c.support.htmlSerialize)F._default=[1,"div<div>","</div>"];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d=
+c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==w)return this.empty().append((this[0]&&this[0].ownerDocument||s).createTextNode(a));return c.text(this)},wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this,d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this},
+wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length?d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})},
+prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a=c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,
+this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},remove:function(a,b){for(var d=0,f;(f=this[d])!=null;d++)if(!a||c.filter(a,[f]).length){if(!b&&f.nodeType===1){c.cleanData(f.getElementsByTagName("*"));c.cleanData([f])}f.parentNode&&f.parentNode.removeChild(f)}return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++)for(b.nodeType===1&&c.cleanData(b.getElementsByTagName("*"));b.firstChild;)b.removeChild(b.firstChild);
+return this},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&!c.isXMLDoc(this)){var d=this.outerHTML,f=this.ownerDocument;if(!d){d=f.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(Ja,"").replace(/=([^="'>\s]+\/)>/g,'="$1">').replace(V,"")],f)[0]}else return this.cloneNode(true)});if(a===true){ra(this,b);ra(this.find("*"),b.find("*"))}return b},html:function(a){if(a===w)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Ja,
+""):null;else if(typeof a==="string"&&!ta.test(a)&&(c.support.leadingWhitespace||!V.test(a))&&!F[(La.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Ka,Ma);try{for(var b=0,d=this.length;b<d;b++)if(this[b].nodeType===1){c.cleanData(this[b].getElementsByTagName("*"));this[b].innerHTML=a}}catch(f){this.empty().append(a)}}else c.isFunction(a)?this.each(function(e){var j=c(this),i=j.html();j.empty().append(function(){return a.call(this,e,i)})}):this.empty().append(a);return this},replaceWith:function(a){if(this[0]&&
+this[0].parentNode){if(c.isFunction(a))return this.each(function(b){var d=c(this),f=d.html();d.replaceWith(a.call(this,b,f))});if(typeof a!=="string")a=c(a).detach();return this.each(function(){var b=this.nextSibling,d=this.parentNode;c(this).remove();b?c(b).before(a):c(d).append(a)})}else return this.pushStack(c(c.isFunction(a)?a():a),"replaceWith",a)},detach:function(a){return this.remove(a,true)},domManip:function(a,b,d){function f(u){return c.nodeName(u,"table")?u.getElementsByTagName("tbody")[0]||
+u.appendChild(u.ownerDocument.createElement("tbody")):u}var e,j,i=a[0],o=[],k;if(!c.support.checkClone&&arguments.length===3&&typeof i==="string"&&ua.test(i))return this.each(function(){c(this).domManip(a,b,d,true)});if(c.isFunction(i))return this.each(function(u){var z=c(this);a[0]=i.call(this,u,b?z.html():w);z.domManip(a,b,d)});if(this[0]){e=i&&i.parentNode;e=c.support.parentNode&&e&&e.nodeType===11&&e.childNodes.length===this.length?{fragment:e}:sa(a,this,o);k=e.fragment;if(j=k.childNodes.length===
+1?(k=k.firstChild):k.firstChild){b=b&&c.nodeName(j,"tr");for(var n=0,r=this.length;n<r;n++)d.call(b?f(this[n],j):this[n],n>0||e.cacheable||this.length>1?k.cloneNode(true):k)}o.length&&c.each(o,Qa)}return this}});c.fragments={};c.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){c.fn[a]=function(d){var f=[];d=c(d);var e=this.length===1&&this[0].parentNode;if(e&&e.nodeType===11&&e.childNodes.length===1&&d.length===1){d[b](this[0]);
+return this}else{e=0;for(var j=d.length;e<j;e++){var i=(e>0?this.clone(true):this).get();c.fn[b].apply(c(d[e]),i);f=f.concat(i)}return this.pushStack(f,a,d.selector)}}});c.extend({clean:function(a,b,d,f){b=b||s;if(typeof b.createElement==="undefined")b=b.ownerDocument||b[0]&&b[0].ownerDocument||s;for(var e=[],j=0,i;(i=a[j])!=null;j++){if(typeof i==="number")i+="";if(i){if(typeof i==="string"&&!jb.test(i))i=b.createTextNode(i);else if(typeof i==="string"){i=i.replace(Ka,Ma);var o=(La.exec(i)||["",
+""])[1].toLowerCase(),k=F[o]||F._default,n=k[0],r=b.createElement("div");for(r.innerHTML=k[1]+i+k[2];n--;)r=r.lastChild;if(!c.support.tbody){n=ib.test(i);o=o==="table"&&!n?r.firstChild&&r.firstChild.childNodes:k[1]==="<table>"&&!n?r.childNodes:[];for(k=o.length-1;k>=0;--k)c.nodeName(o[k],"tbody")&&!o[k].childNodes.length&&o[k].parentNode.removeChild(o[k])}!c.support.leadingWhitespace&&V.test(i)&&r.insertBefore(b.createTextNode(V.exec(i)[0]),r.firstChild);i=r.childNodes}if(i.nodeType)e.push(i);else e=
+c.merge(e,i)}}if(d)for(j=0;e[j];j++)if(f&&c.nodeName(e[j],"script")&&(!e[j].type||e[j].type.toLowerCase()==="text/javascript"))f.push(e[j].parentNode?e[j].parentNode.removeChild(e[j]):e[j]);else{e[j].nodeType===1&&e.splice.apply(e,[j+1,0].concat(c.makeArray(e[j].getElementsByTagName("script"))));d.appendChild(e[j])}return e},cleanData:function(a){for(var b,d,f=c.cache,e=c.event.special,j=c.support.deleteExpando,i=0,o;(o=a[i])!=null;i++)if(d=o[c.expando]){b=f[d];if(b.events)for(var k in b.events)e[k]?
+c.event.remove(o,k):Ca(o,k,b.handle);if(j)delete o[c.expando];else o.removeAttribute&&o.removeAttribute(c.expando);delete f[d]}}});var kb=/z-?index|font-?weight|opacity|zoom|line-?height/i,Na=/alpha\([^)]*\)/,Oa=/opacity=([^)]*)/,ha=/float/i,ia=/-([a-z])/ig,lb=/([A-Z])/g,mb=/^-?\d+(?:px)?$/i,nb=/^-?\d/,ob={position:"absolute",visibility:"hidden",display:"block"},pb=["Left","Right"],qb=["Top","Bottom"],rb=s.defaultView&&s.defaultView.getComputedStyle,Pa=c.support.cssFloat?"cssFloat":"styleFloat",ja=
+function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){return X(this,a,b,true,function(d,f,e){if(e===w)return c.curCSS(d,f);if(typeof e==="number"&&!kb.test(f))e+="px";c.style(d,f,e)})};c.extend({style:function(a,b,d){if(!a||a.nodeType===3||a.nodeType===8)return w;if((b==="width"||b==="height")&&parseFloat(d)<0)d=w;var f=a.style||a,e=d!==w;if(!c.support.opacity&&b==="opacity"){if(e){f.zoom=1;b=parseInt(d,10)+""==="NaN"?"":"alpha(opacity="+d*100+")";a=f.filter||c.curCSS(a,"filter")||"";f.filter=
+Na.test(a)?a.replace(Na,b):b}return f.filter&&f.filter.indexOf("opacity=")>=0?parseFloat(Oa.exec(f.filter)[1])/100+"":""}if(ha.test(b))b=Pa;b=b.replace(ia,ja);if(e)f[b]=d;return f[b]},css:function(a,b,d,f){if(b==="width"||b==="height"){var e,j=b==="width"?pb:qb;function i(){e=b==="width"?a.offsetWidth:a.offsetHeight;f!=="border"&&c.each(j,function(){f||(e-=parseFloat(c.curCSS(a,"padding"+this,true))||0);if(f==="margin")e+=parseFloat(c.curCSS(a,"margin"+this,true))||0;else e-=parseFloat(c.curCSS(a,
+"border"+this+"Width",true))||0})}a.offsetWidth!==0?i():c.swap(a,ob,i);return Math.max(0,Math.round(e))}return c.curCSS(a,b,d)},curCSS:function(a,b,d){var f,e=a.style;if(!c.support.opacity&&b==="opacity"&&a.currentStyle){f=Oa.test(a.currentStyle.filter||"")?parseFloat(RegExp.$1)/100+"":"";return f===""?"1":f}if(ha.test(b))b=Pa;if(!d&&e&&e[b])f=e[b];else if(rb){if(ha.test(b))b="float";b=b.replace(lb,"-$1").toLowerCase();e=a.ownerDocument.defaultView;if(!e)return null;if(a=e.getComputedStyle(a,null))f=
+a.getPropertyValue(b);if(b==="opacity"&&f==="")f="1"}else if(a.currentStyle){d=b.replace(ia,ja);f=a.currentStyle[b]||a.currentStyle[d];if(!mb.test(f)&&nb.test(f)){b=e.left;var j=a.runtimeStyle.left;a.runtimeStyle.left=a.currentStyle.left;e.left=d==="fontSize"?"1em":f||0;f=e.pixelLeft+"px";e.left=b;a.runtimeStyle.left=j}}return f},swap:function(a,b,d){var f={};for(var e in b){f[e]=a.style[e];a.style[e]=b[e]}d.call(a);for(e in b)a.style[e]=f[e]}});if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b=
+a.offsetWidth,d=a.offsetHeight,f=a.nodeName.toLowerCase()==="tr";return b===0&&d===0&&!f?true:b>0&&d>0&&!f?false:c.curCSS(a,"display")==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var sb=J(),tb=/<script(.|\s)*?\/script>/gi,ub=/select|textarea/i,vb=/color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i,N=/=\?(&|$)/,ka=/\?/,wb=/(\?|&)_=.*?(&|$)/,xb=/^(\w+:)?\/\/([^\/?#]+)/,yb=/%20/g,zb=c.fn.loaddisabled;c.fn.extend({loaddisabled:function(a,b,d){if(typeof a!==
+"string")return zb.call(this,a);else if(!this.length)return this;var f=a.indexOf(" ");if(f>=0){var e=a.slice(f,a.length);a=a.slice(0,f)}f="GET";if(b)if(c.isFunction(b)){d=b;b=null}else if(typeof b==="object"){b=c.param(b,c.ajaxSettings.traditional);f="POST"}var j=this;c.ajax({url:a,type:f,dataType:"html",data:b,complete:function(i,o){if(o==="success"||o==="notmodified")j.html(e?c("<div />").append(i.responseText.replace(tb,"")).find(e):i.responseText);d&&j.each(d,[i.responseText,o,i])}});return this},
+serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||ub.test(this.nodeName)||vb.test(this.type))}).map(function(a,b){a=c(this).val();return a==null?null:c.isArray(a)?c.map(a,function(d){return{name:b.name,value:d}}):{name:b.name,value:a}}).get()}});c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),
+function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:f})},getScript:function(a,b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:f})},ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href,
+global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:A.XMLHttpRequest&&(A.location.protocol!=="file:"||!A.ActiveXObject)?function(){return new A.XMLHttpRequest}:function(){try{return new A.ActiveXObject("Microsoft.XMLHTTP")}catch(a){}},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},etag:{},ajax:function(a){function b(){e.success&&
+e.success.call(k,o,i,x);e.global&&f("ajaxSuccess",[x,e])}function d(){e.complete&&e.complete.call(k,x,i);e.global&&f("ajaxComplete",[x,e]);e.global&&!--c.active&&c.event.trigger("ajaxStop")}function f(q,p){(e.context?c(e.context):c.event).trigger(q,p)}var e=c.extend(true,{},c.ajaxSettings,a),j,i,o,k=a&&a.context||e,n=e.type.toUpperCase();if(e.data&&e.processData&&typeof e.data!=="string")e.data=c.param(e.data,e.traditional);if(e.dataType==="jsonp"){if(n==="GET")N.test(e.url)||(e.url+=(ka.test(e.url)?
+"&":"?")+(e.jsonp||"callback")+"=?");else if(!e.data||!N.test(e.data))e.data=(e.data?e.data+"&":"")+(e.jsonp||"callback")+"=?";e.dataType="json"}if(e.dataType==="json"&&(e.data&&N.test(e.data)||N.test(e.url))){j=e.jsonpCallback||"jsonp"+sb++;if(e.data)e.data=(e.data+"").replace(N,"="+j+"$1");e.url=e.url.replace(N,"="+j+"$1");e.dataType="script";A[j]=A[j]||function(q){o=q;b();d();A[j]=w;try{delete A[j]}catch(p){}z&&z.removeChild(C)}}if(e.dataType==="script"&&e.cache===null)e.cache=false;if(e.cache===
+false&&n==="GET"){var r=J(),u=e.url.replace(wb,"$1_="+r+"$2");e.url=u+(u===e.url?(ka.test(e.url)?"&":"?")+"_="+r:"")}if(e.data&&n==="GET")e.url+=(ka.test(e.url)?"&":"?")+e.data;e.global&&!c.active++&&c.event.trigger("ajaxStart");r=(r=xb.exec(e.url))&&(r[1]&&r[1]!==location.protocol||r[2]!==location.host);if(e.dataType==="script"&&n==="GET"&&r){var z=s.getElementsByTagName("head")[0]||s.documentElement,C=s.createElement("script");C.src=e.url;if(e.scriptCharset)C.charset=e.scriptCharset;if(!j){var B=
+false;C.onloaddisabled=C.onreadystatechange=function(){if(!B&&(!this.readyState||this.readyState==="loaddisableded"||this.readyState==="complete")){B=true;b();d();C.onloaddisabled=C.onreadystatechange=null;z&&C.parentNode&&z.removeChild(C)}}}z.insertBefore(C,z.firstChild);return w}var E=false,x=e.xhr();if(x){e.username?void(n,e.url,e.async,e.username,e.password):void(n,e.url,e.async);try{if(e.data||a&&a.contentType)x.setRequestHeader("Content-Type",e.contentType);if(e.ifModified){c.lastModified[e.url]&&x.setRequestHeader("If-Modified-Since",
+c.lastModified[e.url]);c.etag[e.url]&&x.setRequestHeader("If-None-Match",c.etag[e.url])}r||x.setRequestHeader("X-Requested-With","XMLHttpRequest");x.setRequestHeader("Accept",e.dataType&&e.accepts[e.dataType]?e.accepts[e.dataType]+", */*":e.accepts._default)}catch(ga){}if(e.beforeSend&&e.beforeSend.call(k,x,e)===false){e.global&&!--c.active&&c.event.trigger("ajaxStop");x.abort();return false}e.global&&f("ajaxSend",[x,e]);var g=x.onreadystatechange=function(q){if(!x||x.readyState===0||q==="abort"){E||
+d();E=true;if(x)x.onreadystatechange=c.noop}else if(!E&&x&&(x.readyState===4||q==="timeout")){E=true;x.onreadystatechange=c.noop;i=q==="timeout"?"timeout":!c.httpSuccess(x)?"error":e.ifModified&&c.httpNotModified(x,e.url)?"notmodified":"success";var p;if(i==="success")try{o=c.httpData(x,e.dataType,e)}catch(v){i="parsererror";p=v}if(i==="success"||i==="notmodified")j||b();else c.handleError(e,x,i,p);d();q==="timeout"&&x.abort();if(e.async)x=null}};try{var h=x.abort;x.abort=function(){x&&h.call&&h.call(x);
+g("abort")}}catch(l){}e.async&&e.timeout>0&&setTimeout(function(){x&&!E&&g("timeout")},e.timeout);try{x.send(n==="POST"||n==="PUT"||n==="DELETE"?e.data:null)}catch(m){c.handleError(e,x,null,m);d()}e.async||g();return x}},handleError:function(a,b,d,f){if(a.error)a.error.call(a.context||a,b,d,f);if(a.global)(a.context?c(a.context):c.event).trigger("ajaxError",[b,a,f])},active:0,httpSuccess:function(a){try{return!a.status&&location.protocol==="file:"||a.status>=200&&a.status<300||a.status===304||a.status===
+1223||a.status===0}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"),f=a.getResponseHeader("Etag");if(d)c.lastModified[b]=d;if(f)c.etag[b]=f;return a.status===304||a.status===0},httpData:function(a,b,d){var f=a.getResponseHeader("content-type")||"",e=b==="xml"||!b&&f.indexOf("xml")>=0;a=e?a.responseXML:a.responseText;e&&a.documentElement.nodeName==="parsererror"&&c.error("parsererror");if(d&&d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b===
+"json"||!b&&f.indexOf("json")>=0)a=c.parseJSON(a);else if(b==="script"||!b&&f.indexOf("javascript")>=0)c.globalEval(a);return a},param:function(a,b){function d(i,o){if(c.isArray(o))c.each(o,function(k,n){b||/\[\]$/.test(i)?f(i,n):d(i+"["+(typeof n==="object"||c.isArray(n)?k:"")+"]",n)});else!b&&o!=null&&typeof o==="object"?c.each(o,function(k,n){d(i+"["+k+"]",n)}):f(i,o)}function f(i,o){o=c.isFunction(o)?o():o;e[e.length]=encodeURIComponent(i)+"="+encodeURIComponent(o)}var e=[];if(b===w)b=c.ajaxSettings.traditional;
+if(c.isArray(a)||a.jquery)c.each(a,function(){f(this.name,this.value)});else for(var j in a)d(j,a[j]);return e.join("&").replace(yb,"+")}});var la={},Ab=/toggle|show|hide/,Bb=/^([+-]=)?([\d+-.]+)(.*)$/,W,va=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b){if(a||a===0)return this.animate(K("show",3),a,b);else{a=0;for(b=this.length;a<b;a++){var d=c.data(this[a],"olddisplay");
+this[a].style.display=d||"";if(c.css(this[a],"display")==="none"){d=this[a].nodeName;var f;if(la[d])f=la[d];else{var e=c("<"+d+" />").appendTo("body");f=e.css("display");if(f==="none")f="block";e.remove();la[d]=f}c.data(this[a],"olddisplay",f)}}a=0;for(b=this.length;a<b;a++)this[a].style.display=c.data(this[a],"olddisplay")||"";return this}},hide:function(a,b){if(a||a===0)return this.animate(K("hide",3),a,b);else{a=0;for(b=this.length;a<b;a++){var d=c.data(this[a],"olddisplay");!d&&d!=="none"&&c.data(this[a],
+"olddisplay",c.css(this[a],"display"))}a=0;for(b=this.length;a<b;a++)this[a].style.display="none";return this}},_toggle:c.fn.toggle,toggle:function(a,b){var d=typeof a==="boolean";if(c.isFunction(a)&&c.isFunction(b))this._toggle.apply(this,arguments);else a==null||d?this.each(function(){var f=d?a:c(this).is(":hidden");c(this)[f?"show":"hide"]()}):this.animate(K("toggle",3),a,b);return this},fadeTo:function(a,b,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,d)},
+animate:function(a,b,d,f){var e=c.speed(b,d,f);if(c.isEmptyObject(a))return this.each(e.complete);return this[e.queue===false?"each":"queue"](function(){var j=c.extend({},e),i,o=this.nodeType===1&&c(this).is(":hidden"),k=this;for(i in a){var n=i.replace(ia,ja);if(i!==n){a[n]=a[i];delete a[i];i=n}if(a[i]==="hide"&&o||a[i]==="show"&&!o)return j.complete.call(this);if((i==="height"||i==="width")&&this.style){j.display=c.css(this,"display");j.overflow=this.style.overflow}if(c.isArray(a[i])){(j.specialEasing=
+j.specialEasing||{})[i]=a[i][1];a[i]=a[i][0]}}if(j.overflow!=null)this.style.overflow="hidden";j.curAnim=c.extend({},a);c.each(a,function(r,u){var z=new c.fx(k,j,r);if(Ab.test(u))z[u==="toggle"?o?"show":"hide":u](a);else{var C=Bb.exec(u),B=z.cur(true)||0;if(C){u=parseFloat(C[2]);var E=C[3]||"px";if(E!=="px"){k.style[r]=(u||1)+E;B=(u||1)/z.cur(true)*B;k.style[r]=B+E}if(C[1])u=(C[1]==="-="?-1:1)*u+B;z.custom(B,u,E)}else z.custom(B,u,"")}});return true})},stop:function(a,b){var d=c.timers;a&&this.queue([]);
+this.each(function(){for(var f=d.length-1;f>=0;f--)if(d[f].elem===this){b&&d[f](true);d.splice(f,1)}});b||this.dequeue();return this}});c.each({slideDown:K("show",1),slideUp:K("hide",1),slideToggle:K("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(a,b){c.fn[a]=function(d,f){return this.animate(b,d,f)}});c.extend({speed:function(a,b,d){var f=a&&typeof a==="object"?a:{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};f.duration=c.fx.off?0:typeof f.duration===
+"number"?f.duration:c.fx.speeds[f.duration]||c.fx.speeds._default;f.old=f.complete;f.complete=function(){f.queue!==false&&c(this).dequeue();c.isFunction(f.old)&&f.old.call(this)};return f},easing:{linear:function(a,b,d,f){return d+f*a},swing:function(a,b,d,f){return(-Math.cos(a*Math.PI)/2+0.5)*f+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]||
+c.fx.step._default)(this);if((this.prop==="height"||this.prop==="width")&&this.elem.style)this.elem.style.display="block"},cur:function(a){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];return(a=parseFloat(c.css(this.elem,this.prop,a)))&&a>-10000?a:parseFloat(c.curCSS(this.elem,this.prop))||0},custom:function(a,b,d){function f(j){return e.step(j)}this.startTime=J();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start;
+this.pos=this.state=0;var e=this;f.elem=this.elem;if(f()&&c.timers.push(f)&&!W)W=setInterval(c.fx.tick,13)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(a){var b=J(),d=true;if(a||b>=this.options.duration+this.startTime){this.now=
+this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var f in this.options.curAnim)if(this.options.curAnim[f]!==true)d=false;if(d){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;a=c.data(this.elem,"olddisplay");this.elem.style.display=a?a:this.options.display;if(c.css(this.elem,"display")==="none")this.elem.style.display="block"}this.options.hide&&c(this.elem).hide();if(this.options.hide||this.options.show)for(var e in this.options.curAnim)c.style(this.elem,
+e,this.options.orig[e]);this.options.complete.call(this.elem)}return false}else{e=b-this.startTime;this.state=e/this.options.duration;a=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||a](this.state,e,0,1,this.options.duration);this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a=c.timers,b=0;b<a.length;b++)a[b]()||a.splice(b--,1);a.length||
+c.fx.stop()},stop:function(){clearInterval(W);W=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){c.style(a.elem,"opacity",a.now)},_default:function(a){if(a.elem.style&&a.elem.style[a.prop]!=null)a.elem.style[a.prop]=(a.prop==="width"||a.prop==="height"?Math.max(0,a.now):a.now)+a.unit;else a.elem[a.prop]=a.now}}});if(c.expr&&c.expr.filters)c.expr.filters.animated=function(a){return c.grep(c.timers,function(b){return a===b.elem}).length};c.fn.offset="getBoundingClientRect"in s.documentElement?
+function(a){var b=this[0];if(a)return this.each(function(e){c.offset.setOffset(this,a,e)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return c.offset.bodyOffset(b);var d=b.getBoundingClientRect(),f=b.ownerDocument;b=f.body;f=f.documentElement;return{top:d.top+(self.pageYOffset||c.support.boxModel&&f.scrollTop||b.scrollTop)-(f.clientTop||b.clientTop||0),left:d.left+(self.pageXOffset||c.support.boxModel&&f.scrollLeft||b.scrollLeft)-(f.clientLeft||b.clientLeft||0)}}:function(a){var b=
+this[0];if(a)return this.each(function(r){c.offset.setOffset(this,a,r)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return c.offset.bodyOffset(b);c.offset.initialize();var d=b.offsetParent,f=b,e=b.ownerDocument,j,i=e.documentElement,o=e.body;f=(e=e.defaultView)?e.getComputedStyle(b,null):b.currentStyle;for(var k=b.offsetTop,n=b.offsetLeft;(b=b.parentNode)&&b!==o&&b!==i;){if(c.offset.supportsFixedPosition&&f.position==="fixed")break;j=e?e.getComputedStyle(b,null):b.currentStyle;
+k-=b.scrollTop;n-=b.scrollLeft;if(b===d){k+=b.offsetTop;n+=b.offsetLeft;if(c.offset.doesNotAddBorder&&!(c.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(b.nodeName))){k+=parseFloat(j.borderTopWidth)||0;n+=parseFloat(j.borderLeftWidth)||0}f=d;d=b.offsetParent}if(c.offset.subtractsBorderForOverflowNotVisible&&j.overflow!=="visible"){k+=parseFloat(j.borderTopWidth)||0;n+=parseFloat(j.borderLeftWidth)||0}f=j}if(f.position==="relative"||f.position==="static"){k+=o.offsetTop;n+=o.offsetLeft}if(c.offset.supportsFixedPosition&&
+f.position==="fixed"){k+=Math.max(i.scrollTop,o.scrollTop);n+=Math.max(i.scrollLeft,o.scrollLeft)}return{top:k,left:n}};c.offset={initialize:function(){var a=s.body,b=s.createElement("div"),d,f,e,j=parseFloat(c.curCSS(a,"marginTop",true))||0;c.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"});b.innerHTML="<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>";
+a.insertBefore(b,a.firstChild);d=b.firstChild;f=d.firstChild;e=d.nextSibling.firstChild.firstChild;this.doesNotAddBorder=f.offsetTop!==5;this.doesAddBorderForTableAndCells=e.offsetTop===5;f.style.position="fixed";f.style.top="20px";this.supportsFixedPosition=f.offsetTop===20||f.offsetTop===15;f.style.position=f.style.top="";d.style.overflow="hidden";d.style.position="relative";this.subtractsBorderForOverflowNotVisible=f.offsetTop===-5;this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==j;a.removeChild(b);
+c.offset.initialize=c.noop},bodyOffset:function(a){var b=a.offsetTop,d=a.offsetLeft;c.offset.initialize();if(c.offset.doesNotIncludeMarginInBodyOffset){b+=parseFloat(c.curCSS(a,"marginTop",true))||0;d+=parseFloat(c.curCSS(a,"marginLeft",true))||0}return{top:b,left:d}},setOffset:function(a,b,d){if(/static/.test(c.curCSS(a,"position")))a.style.position="relative";var f=c(a),e=f.offset(),j=parseInt(c.curCSS(a,"top",true),10)||0,i=parseInt(c.curCSS(a,"left",true),10)||0;if(c.isFunction(b))b=b.call(a,
+d,e);d={top:b.top-e.top+j,left:b.left-e.left+i};"using"in b?b.using.call(a,d):f.css(d)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),f=/^body|html$/i.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.curCSS(a,"marginTop",true))||0;d.left-=parseFloat(c.curCSS(a,"marginLeft",true))||0;f.top+=parseFloat(c.curCSS(b[0],"borderTopWidth",true))||0;f.left+=parseFloat(c.curCSS(b[0],"borderLeftWidth",true))||0;return{top:d.top-
+f.top,left:d.left-f.left}},offsetParent:function(){return this.map(function(){for(var a=this.offsetParent||s.body;a&&!/^body|html$/i.test(a.nodeName)&&c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(f){var e=this[0],j;if(!e)return null;if(f!==w)return this.each(function(){if(j=wa(this))j.scrollTo(!a?f:c(j).scrollLeft(),a?f:c(j).scrollTop());else this[d]=f});else return(j=wa(e))?"pageXOffset"in j?j[a?"pageYOffset":
+"pageXOffset"]:c.support.boxModel&&j.document.documentElement[d]||j.document.body[d]:e[d]}});c.each(["Height","Width"],function(a,b){var d=b.toLowerCase();c.fn["inner"+b]=function(){return this[0]?c.css(this[0],d,false,"padding"):null};c.fn["outer"+b]=function(f){return this[0]?c.css(this[0],d,false,f?"margin":"border"):null};c.fn[d]=function(f){var e=this[0];if(!e)return f==null?null:this;if(c.isFunction(f))return this.each(function(j){var i=c(this);i[d](f.call(this,j,i[d]()))});return"scrollTo"in
+e&&e.document?e.document.compatMode==="CSS1Compat"&&e.document.documentElement["client"+b]||e.document.body["client"+b]:e.nodeType===9?Math.max(e.documentElement["client"+b],e.body["scroll"+b],e.documentElement["scroll"+b],e.body["offset"+b],e.documentElement["offset"+b]):f===w?c.css(e,d):this.css(d,typeof f==="string"?f:f+"px")}});A.jQuery=A.$=c})(window);
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/static.foxsports.com/content/fscom/img/2011/04/07/040711-Golf-Tiger-Woods-1120pm-PI_20110407142414593_116_175.JPG b/mobile/android/tests/browser/chrome/tp5/msn.com/static.foxsports.com/content/fscom/img/2011/04/07/040711-Golf-Tiger-Woods-1120pm-PI_20110407142414593_116_175.JPG
new file mode 100755
index 0000000000..53e23a3c2e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/static.foxsports.com/content/fscom/img/2011/04/07/040711-Golf-Tiger-Woods-1120pm-PI_20110407142414593_116_175.JPG
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/udc.msn.com/c.gif b/mobile/android/tests/browser/chrome/tp5/msn.com/udc.msn.com/c.gif
new file mode 100755
index 0000000000..9935f82104
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/udc.msn.com/c.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/www.bing.com/partner/primedns.gif b/mobile/android/tests/browser/chrome/tp5/msn.com/www.bing.com/partner/primedns.gif
new file mode 100755
index 0000000000..35d42e808f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/www.bing.com/partner/primedns.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/msn.com/www.msn.com/index.html b/mobile/android/tests/browser/chrome/tp5/msn.com/www.msn.com/index.html
new file mode 100755
index 0000000000..9c180e1d8a
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/msn.com/www.msn.com/index.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "httpdisabled://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xml:lang="en-us" lang="en-us" dir="ltr" xmlns="httpdisabled://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><meta name="msapplication-task" content="name=News;action-uri=http://www.msnbc.msn.com/?OCID=MSNIE9Jumplist;icon-uri=http://www.msnbc.msn.com/favicon.ico" /><meta name="msapplication-task" content="name=Entertainment;action-uri=http://entertainment.msn.com/?OCID=MSNIE9Jumplist;icon-uri=http://col.stc.s-msn.com/br/gbl/lg/csl/favicon.ico" /><meta name="msapplication-task" content="name=Sports;action-uri=http://msn.foxsports.com/?OCID=MSNIE9Jumplist;icon-uri=http://msn.foxsports.com/favicon.ico" /><meta name="msapplication-task" content="name=Money;action-uri=http://moneycentral.msn.com/?OCID=MSNIE9Jumplist;icon-uri=http://col.stc.s-msn.com/br/gbl/lg/csl/favicon.ico" /><meta name="msapplication-task" content="name=Lifestyle;action-uri=http://lifestyle.msn.com/?OCID=MSNIE9Jumplist;icon-uri=http://col.stc.s-msn.com/br/gbl/lg/csl/favicon.ico" /><link rel="SHORTCUT ICON" href="../col.stc.s-msn.com/br/gbl/lg/csl/favicon.ico" type="image/x-icon" /><meta name="description" content="MSN is Microsoft's portal, offering MSNBC News, sports, MSN Money, games, videos, entertainment &amp; celebrity gossip, weather, shopping and more great content, as well as Windows Live services such as Hotmail and Messenger." /><meta http-equiv="pics-label" content="(pics-1.1 &quot;http://www.icra.org/ratingsv02.html&quot; comment &quot;Single file v2.0&quot; l gen true for &quot;http://www.msn.com&quot; r (nz 1 vz 1 lz 1 oz 1 cz 1) &quot;http://www.rsac.org/ratingsv01.html&quot; l gen true for &quot;http://www.msn.com&quot; r (n 0 s 0 v 0 l 0)" /><link rel="canonical" href="index.html" /><title>MSN.com</title><!--[if IE 6]><![endif]--><link rel="stylesheet" type="text/css" href="../col.stc.s-msn.com/br/sc/css/1d/b0ebeba5ed4ca3c158e6d6059f5074.css" media="all" /><!--[if lt IE 8]><link rel="stylesheet" type="text/css" href="httpdisabled://col.stc.s-msn.com/br/sc/css/74/47e7e4c3ce83c85fb512d4ea7ba2a0.css" media="all" /><![endif]--><!--[if IE 6]><link rel="stylesheet" type="text/css" href="httpdisabled://col.stc.s-msn.com/br/sc/css/be/ce1e39d847e4e191073d19728a5fed.css" media="all" /><![endif]--><script type="text/javascript" src="../col.stj.s-msn.com/br/sc/js/jquery/jquery-1.4.2.min.js"></script><script type="text/javascript">/*<![CDATA[*/(function($){$.extend({dapUrl:"httpdisabled://ads1.msn.com/library/dapmsn.js",signedIn:"False",jsUrl:"httpdisabled://col.stj.s-msn.com/br/sc/js/78/df148a5c898e51aa6820b1ecee461c.js",cookie:document.cookie,dapDelay:["MSNHP4"]});})(jQuery);(function(b){function a(b,d,c){return typeof b=="number"&&(a(d)?b>=d:true)&&(a(c)?b<=c:true)}function d(c,b){return typeof c=="string"&&(a(b)?c.length>=b:true)}var c=b.isArray;b.extend({isNumber:a,isString:d,isObject:function(a){return typeof a=="object"&&a!==null},isDefined:function(a){return typeof a!="undefined"},isArray:function(d,b){return c(d)&&(a(b)?d.length>=b:true)}})})(jQuery);(function(a){var g={timeout:50},h={},d=[],e=[],i,f=a.isString,l=a.isFunction,j=window;function n(n,e,o){var q;if(f(n,1)&&(q=this[n])){if(l(e))c(e,this);else if(l(q))if(a.isArray(e))c(q,this,e);else!a.isDefined(e)&&c(q,this)}else if(f(o)){var p=h[o];if(p)p.push(new b(n,e,this));else{h[o]=[new b(n,e,this)];a.ajax({url:o,dataType:"script",cache:1,success:function(){p=h[o];for(var a,b=0;a=p[b];++b)m(a)}})}}else if(f(n,1)){d.push(new b(n,e,this));if(!i)i=j.setTimeout(k,g.timeout)}}function c(f,d,c){if(d.selector&&d.size()==0){e.push(new b(f,c,d));return}if(d.selector&&a.isArray(c)&&(c[0].asyncp||c[1]&&c[1][0]&&c[1][0].asyncp)){d=d.filter(o);e.push(new b(f,c,d.end()))}if(c)f.apply(d,c);else f.apply(d)}function o(){var b=a(this);if(b.data("asyncfilter"))return 0;b.data("asyncfilter",1);return 1}function p(){var g=e;e=[];for(var b,d,f=0;b=g[f];++f){d=a(b.callee.selector,b.callee.context);c(b.func,d,b.action)}}function k(){var c=d;d=[];for(var a,b=0;a=c[b];++b)m(a);i=d.length==0?0:j.setTimeout(k,g.timeout)}function m(a){n.call(a.callee,a.func,a.action)}function b(c,a,b){this.func=c;this.action=a;this.callee=b}a.async=a.fn.async=j.async=n;a.async.defaults=g;a.async.delayed=p})(jQuery);(function($){$.async(0,0,$.jsUrl);})(jQuery);void("<style type='text/css'>.srchh1 .shupsell{display:none}</style>");void("<style type='text/css'>.cogr .co{display:none}.cogr .cof .co{display:block}</style>");;(function(b){var a={bannerCookieName:"hppr"};function c(i,g){var e=b.extend(true,{},a,g),d,h=e.bannerCookieName.getCookie(),c=RegExp("(^|,)"+i+":([^,:]+):?([^,]*)","gi").exec(h),f=""+Math.round(+new Date/1e6);if(c&&(!c[3]||f<c[3]))d=c[2];return d}b.extend(true,{condition:{getCookie:c,defaults:a}})})(jQuery);(function(){String.prototype.format=function(){for(var b=this,a=0;a<arguments.length;++a)b=b.replace(new RegExp("\\{"+a+"\\}","g"),arguments[a]);return b};String.prototype.findKey=function(g,b,a){b=b||"|";a=a||":";var f=null,c=this.split(b);if(c)for(var d=0;d<c.length;d++){var e=c[d].split(a);if(e[0]==g){f=e[1];break}}return f}})();(function(a){var b={silverlightVersions:["5.0","4.0","3.0","2.0"],silverlightMimeType:"application/x-silverlight-2"};function c(l){var h=a.extend(true,{},b,l),j=window,c;if(!a.isArray(h.silverlightVersions,1))return 0;var d=0;try{var m=j.navigator,f=m.plugins;if(f&&f.length){c=f["Silverlight Plug-In"];if(c)d=/^\d+\.\d+/.exec(c.description)[0];c=0}else if(j.ActiveXObject){var k=new ActiveXObject("AgControl.AgControl");if(k){d=1;var e=a("<objectdisabled/>")[0];e.codeType=h.silverlightMimeType;if(typeof e.IsVersionSupported!="undefined")for(var g,i=0;g=h.silverlightVersions[i];++i)if(e.IsVersionSupported(g)){d=g;break}e=0}}}catch(n){}return d}a.silverlight=c;a.silverlight.version=c();a.silverlight.defaults=b})(jQuery);(function(){String.prototype.getCookie=function(){var b=new RegExp("\\b"+this+"\\s*=\\s*([^;]*)","i"),a=b.exec(document.cookie);return a&&a.length>1?a[1]:""}})();(function(){String.prototype.setCookie=function(g,c,d,e,f){var a=[this,"=",g];if(c){var b=new Date;b.setTime(b.getTime()+c*8.64e7);a.push(";expires=");a.push(b.toUTCString())}if(d){a.push(";domain=");a.push(d)}if(e){a.push(";path=");a.push(e)}f&&a.push(";secure");document.cookie=a.join("")};String.prototype.delCookie=function(){document.cookie=this+"=; expires=Fri, 31 Dec 1999 23:59:59 GMT;"}})();(function(a){a.fireAndForget=function(b){if(b){var a=new Image;a.onloaddisabled=a.onerror=function(){a.onloaddisabled=a.onerror=null};a.src=b.replace(/&amp;/gi,"&")}}})(jQuery);(function(a){var e=a.dapUrl,d=a.isArray(a.dapDelay),c=[],f=/PG=([^&]*)&/;function b(m,s,p,i,h){h=a.extend(true,{},b.defaults,h);var q=!!h.acb,o=!!h.imm,n;if(d){var r=f.exec(m);n=r&&RegExp.$1}var g=a("#"+i).parents("div.co");if(g.length){var k=g.parents("div.cogr.cotb");if(k.length)l("OnTabFocus");else{k=g.parents("div.cogr.coss");if(k.length)l("OnSlideFocus");else j()}}else j();function l(a){g.bind(a,function(){g.unbind(a,arguments.callee);j()})}function j(){o?b():a(b);function b(){var b=window;b.async("dapMgr",function(){var e=function(){b.dapMgr.enableACB(i,q);b.dapMgr.renderAd(i,m,s,p)};if(d&&a.inArray(n,a.dapDelay)!=-1)c.push(e);else e()},e)}}}b.defaults={acb:0,imm:1};b.run=function(){d=0;for(var a=0;a<c.length;a++)c[a]()};a.dap=b;e&&a.async(0,0,e)})(jQuery);(function(a){var e=document,d=window,f=d.location,g={evtType:"click",spinTimeout:150,trackInfoOpts:{notrack:"notrack",cmSeparator:">",defaultModule:"body",defaultFormHeadline:"[form submit]",piitxt:"piitxt",piiurl:"piiurl",wrapperId:"wrapper",defaultConnectionType:"LAN",smpCookie:"Sample",smpExp:182,MUIDCookie:"MUID",event:{},sitePage:{},userStatic:{}}};function b(i){var d=a.extend(true,{},g,i);b.recipients=[];b.trackInfo=new c(d.trackInfoOpts);b.register=function(){b.recipients=b.recipients.concat(Array.prototype.slice.call(arguments));return b};b.trackEvent=function(h,f,a,d,g,i,c){b.trackInfo.event=h;b.trackInfo.createReport(f,a,d,g,i,c)&&e("getEventTrackingUrl",true)};b.trackPage=function(){e("getPageViewTrackingUrl",false)};a.fn.trackForms=function(){return this.each(function(){var b=a(this);b=!b.is("form")?a("form",b):b;b.bind("submit",f)})};function e(c,f){b.trackInfo.incrementEventNumber();for(var h in b.recipients){var e=b.recipients[h];a.isFunction(e[c])&&a.fireAndForget(e[c](b.trackInfo))}if(f&&!b.trackInfo.client.isIE()){var g=d.spinTimeout+new Date;while(g>+new Date);}}function h(c){if(c&&c.target&&c.button!=2){var e=a(c.target),d=e.filter("*[href]:first");if(!d.length)d=e.closest("*[href]");d.length&&b.trackEvent(c,d[0])}}function f(a){b.trackEvent(a)}a(document).bind(d.evtType,h).bind("impr",b.trackEvent);a(window).bind("loaddisabled unloaddisabled scroll",b.trackEvent);a(function(){a("body").trackForms()});return b}a.track=b;function c(s){var n=screen,g=c.prototype,b=a.extend(true,{},s);g.sitePage=b.sitePage;g.userStatic=b.userStatic;var i,j,m,h=-1,l,k;g.client=a.extend({screenResolution:function(){return n.width+"x"+n.height},clientId:function(){if(!k){var a=b.MUIDCookie.getCookie();k=a?a:b.userStatic.requestId}return k},colorDepth:n.colorDepth,cookieSupport:function(){return e.cookie?"Y":"N"},height:function(){!i&&p();return i},width:function(){!j&&p();return j},isIE:function(){if(!a.isDefined(m))m=a.isDefined(d.ActiveXObject);return m},connectionType:function(){return b.defaultConnectionType},pageUrl:f.href,referrer:e.referrer,sample:function(){if(h==-1){var d=b.smpCookie.getCookie();h=parseInt(d);h=!isNaN(h)?h%100:Math.floor(Math.random()*100);var a=location.hostname.match(/([^.]+\.[^.]*)$/),c=a?a[0]:"";b.smpCookie.setCookie(h,b.smpExp,c)}return h},timezone:function(){if(!l){var b=new Date,a=new Date;a.setMonth(b.getMonth()+6);var c=Math.round(b.getTimezoneOffset()/60)*-1,d=Math.round(a.getTimezoneOffset()/60)*-1;l=c<d?c:d}return l}},g.client);g.createReport=function(d,j,f,q,t,p){var c,g=this;if(!d&&g.event&&g.event.target)d=g.event.target;if(d&&!a(d).attr(b.notrack)){var e=a(d);c={destinationUrl:j,campaignId:"",contentElement:t,contentModule:q,headline:f,sourceIndex:d.sourceIndex?d.sourceIndex:"",nodeName:d.nodeName};if(!j){var s=d.href||d.action;c.destinationUrl=e.attr(b.piiurl)||s||""}if(!f){f=e.attr(b.piitxt);if(!f)if(e.filter("form").length)f=b.defaultFormHeadline;else try{f=e.text()||e.attr("alt")||a("[alt]",e).attr("alt")}catch(u){f=""}c.headline=f}c.campaignId=p||r(c.destinationUrl);var h=e.parents("[id]");if(!c.contentModule){for(var k=[],l,m=0;l=h[m];++m){var n=l.id;if(n==b.wrapperId)break;k.splice(0,0,n)}c.contentModule=k.join(b.cmSeparator);if(!c.contentModule)c.contentModule=b.defaultModule}if(!c.contentElement){var i=0;if(e.attr("id"))i=1;else if(h.length)i=o(h[0],d,-1);c.contentElement=i}}g.report=c;return c};g.report={};g.incrementEventNumber=function(){this.userDynamic.eventNumber++};g.isSampled=function(a){return!(g.client.sample()>a)};g.generateUrl=function(i,j,h,k,b){var f="",d=a.extend(true,{},k,j);b=a.extend(true,{},h,b);if(b)for(var c in b)if(this[c]){var g=q(b[c],this[c]);d=a.extend(true,{},g,d)}var e=a.param(d);if(e.length>0)f=i+e;return f};function q(c,d){var e=[];if(c&&d)for(var f in c){var g=c[f],b=d[g];if(b)e[f]=a.isFunction(b)?b():b}return e}function o(h,f,c){if(!c)c=-1;for(var i=a(h).children(),d,g=0;c<0&&(d=i[g]);++g){if(d==f)return-c;var e=a(d);if(!e.attr("id")){if(e.attr("href")&&!e.attr(b.notrack))--c;c=o(d,f,c)}}return c}function r(b){var a=/\bGT1=(\d+)/i.exec(b);return a?a[1]:""}function p(){if(a.isNumber(d.innerWidth)){j=d.innerWidth;i=d.innerHeight}else{var b=e.documentElement;if(b&&b.clientWidth){j=b.clientWidth;i=b.clientHeight}else if(b.offsetWidth){j=b.offsetWidth;i=b.offsetHeight}}}}c.prototype.client={};c.prototype.userDynamic={isHomePage:function(){var b=e.documentElement,c=0;if(a.isDefined(b.addBehavior)&&b.addBehavior("#default#homePage"))try{c=b.isHomePage(f.href)?"Y":"N"}catch(d){}return c},anid:function(){return"ANON".getCookie()},timeStamp:function(){return+new Date},eventNumber:0};b.trackInfo=c})(jQuery);(function(c){var a={itemSeparator:",",keyValueSeparator:":",trueValue:"t",falseValue:"f",socialTrackList:[{uid:"facebook_userid",trk:"fb"},{uid:"twitter_userid",trk:"tw"}]},e=c.track,b,d;if(e&&(b=e.trackInfo)&&(d=b.prototype.userDynamic))d.settings=function(){var d=[];for(var i in a.socialTrackList){var f=a.socialTrackList[i],g=f.trk,h=f.uid;g&&h&&d.push(g+a.keyValueSeparator+(c.isString(h.getCookie(),1)?a.trueValue:a.falseValue))}var e=b.prototype.userStatic;e&&e.settings&&d.push(e.settings);return d.join(a.itemSeparator)};c.fn.pageSettingsDefaults=a})(jQuery);(function(b){var d=b.track,c,a;if(d&&(c=d.trackInfo)&&(a=c.prototype.userDynamic)){a.isSHPresent=function(){var c=b.fn.searchHistory,a=c?c.trackingData:0;return a&&a.isSHPresent};a.countOfSHFromBing=function(){var c=b.fn.searchHistory,a=c?c.trackingData:0;return a&&a.countOfSHFromBing};a.countOfSHDisplayed=function(){var c=b.fn.searchHistory,a=c?c.trackingData:0;return a&&a.countOfSHDisplayed}}})(jQuery);(function(b){var a={cookieName:"stvs",crossSessionCookieName:"stvx",itemSeparator:",",keyvalueSeparator:":"};b.fn.stickyTabDefaults=a;if(b.track&&b.track.trackInfo&&b.track.trackInfo.prototype.userDynamic)b.track.trackInfo.prototype.userDynamic.defaultSlotTrees=function(){var f=a.cookieName.getCookie()||"",e=a.crossSessionCookieName.getCookie()||"",d=[];e&&d.push(e);if(f){d.length&&d.push(a.itemSeparator);d.push(f)}var g=b.track.trackInfo.userStatic.defaultSlotTrees||"";return c(g,d.join(""))};function c(j,i){var d=i.split(a.itemSeparator),e=j.split(a.itemSeparator),f=[];for(var h in e){var b=e[h].split(a.keyvalueSeparator);for(var g in d){var c=d[g].split(a.keyvalueSeparator);if(c[0]==b[0]){b[1]=c[1];break}}f.push(b.join(a.keyvalueSeparator))}return f.join(a.itemSeparator)}})(jQuery);(function(a){if(a.track&&a.track.trackInfo&&a.track.trackInfo.prototype.userDynamic)a.track.trackInfo.prototype.userDynamic.expCookie=function(){return"expac".getCookie()}})(jQuery);(function(a){if(a.track&&a.track.trackInfo){var c=a.track.trackInfo.prototype.client,b=-1;if(c){function d(){if(b==-1)b=a.silverlight&&a.silverlight.version?a.silverlight.version:"";return b}c.silverlightVersion=d;c.silverlightEnabled=function(){return Number(d()>0)}}}})(jQuery);(function(b){if(b.track&&b.track.trackInfo){var a=b.track.trackInfo.prototype,f=a.client;if(f){var d,c,e;function g(){return a.userStatic&&a.userStatic.userGroup}f=b.extend(f,{flightKey:function(){if(!d){var a=g();d=a&&a.substring(0,a.indexOf(":"))||"default"}return d},groupAssignment:function(){if(!c){var a=g();c=a&&parseInt(a.substring(a.indexOf(":")+1))?"S":"P"}return c},optKey:function(){if(!e)e=a.userStatic.optKey||"default";return e}})}}})(jQuery);(function(a){var b={base:"",samplingRate:100,eventAlias:{submit:"click",mouseenter:"click",mouseleave:"click"}};a.track.genericTracking=function(e){var d=this,c=d.opts=a.extend(true,{},b,e);d.getEventTrackingUrl=function(f,d){var g="";if(f.isSampled(c.samplingRate)){d=d?d:f.event.type;var b=c[d];if(!a.isDefined(b)&&a.isDefined(c.eventAlias[d]))b=c[c.eventAlias[d]];if(a.isDefined(b)){var e=b.condition;if(!a.isDefined(e)||a.isNumber(e)&&e||a.isFunction(a[e])&&a[e].call()){var h=c.base+(b.url?b.url:"");g=f.generateUrl(h,c.common,c.commonMap,b.param,b.paramMap)}}}return g};d.getPageViewTrackingUrl=function(a){return d.getEventTrackingUrl(a,"impr")}}})(jQuery);(function(b){var a=new Date,d={base:"",linkTrack:1,samplingRate:100,common:{v:"Y",j:"1.3"},commonMap:{client:{c:"colorDepth"}},page:{v1:a.getMonth()+1+"/"+a.getFullYear(),v2:a.getMonth()+1+"/"+a.getDate()+"/"+a.getFullYear(),t:c()},pageMap:{sitePage:{c3:"pageVersion"}},link:{t:c(),ndh:1,pidt:1,pe:"lnk_o"},linkMap:{sitePage:{c38:"pageVersion"}},eventList:["click","mouseenter","mouseleave","submit"]};b.track.omniTracking=function(e){var c=this,a=c.opts=b.extend(true,{},d,e);c.getEventTrackingUrl=function(c){var e="";if(c.isSampled(a.samplingRate)){var d=c.event?c.event.type:"";if(a.linkTrack&&b.inArray(d,a.eventList)!=-1){var g=b.extend(true,{},a.link,{c11:d=="mouseenter"||d=="mouseleave"?"hover":d,events:"events4"}),f=c.generateUrl("",a.common,a.commonMap,g,a.linkMap);e=a.base.format(c.userDynamic.timeStamp(),f)}}return e};c.getPageViewTrackingUrl=function(b){var c="";if(b.isSampled(a.samplingRate)){var d=b.generateUrl("",a.common,a.commonMap,a.page,a.pageMap);c=a.base.format(b.userDynamic.timeStamp(),d)}return c}};function c(){var b=[a.getDate(),"/",a.getMonth(),"/",a.getFullYear()," ",a.getHours(),":",a.getMinutes(),":",a.getSeconds()," ",a.getDay()," ",a.getTimezoneOffset()];return b.join("")}b.track.omniTracking.defaults=d})(jQuery);$.track({trackInfoOpts:{sitePage:{lang:"en-us",siteGroupId:"MSFT",pageName:"US HPMSFT3W",pageVersion:"V14",omniPageName:"US HPMSFT3W:MSFT",domainId:"340",propertyId:"7317",propertySpecific:"95101",sourceUrl:"httpdisabled://www.msn.com/defaultwpe3w.aspx",pageId:"6713487"},userStatic:{signedIn:"False",birthdate:"",gender:"",userGroup:"W:default",optKey:"",beginRequestTicks:"634378960591716294",requestId:"e09445ff071f40729f50d96932503769",defaultSlotTrees:"infopane_hops:na,maintg:countdown,sectabs_hops:sports,localtg:local,stgsearch:popsrchnew,socialtg:facebook,gendermodule:forher",expContext:"msnhp_us_master_v2:C",topsKey:"",topsUserGroup:"C:default"}},spinTimeout:150}).register(new $.track.genericTracking({base:"httpdisabled://amer.rel.msn.com/default.aspx?",linkTrack:1,common:{parsergroup:'hops'},commonMap:{client:{fk:'flightKey',gp:'groupAssignment',optkey:'optKey'},sitePage:{di:'domainId',pi:'propertyId',ps:'propertySpecific',pageid: 'pageId',mk:'lang'},userStatic:{tfk:'topsUserGroup',utk:'topsKey'}},impr:{paramMap:{client:{rf:'referrer'},sitePage:{tp:'sourceUrl'},userDynamic:{tv:'defaultSlotTrees'}}},click:{paramMap:{sitePage:{su:'sourceUrl',pn:'pageName'},report:{ce:'contentElement',cm:'contentModule',hl:'headline', gt1:'campaignId',du:'destinationUrl'}, userDynamic:{ctx:'currentTab'}}}}),new $.track.genericTracking({base:"httpdisabled://exp.www.msn.com/ro.aspx?",linkTrack:1,commonMap:{event:{evt:'type'},sitePage:{di:'domainId',pi:'propertyId',ps:'propertySpecific'},userStatic:{rid:'requestId'}},impr:{param:{evt:'impr',obs:'msnhp_us_pv'},paramMap:{client:{rf:'referrer',slv:'silverlightVersion',tp:'pageUrl'},sitePage:{pn:'pageName',ch:'siteGroupId'}}},click:{param:{obs:'msnhp_us_click'},paramMap:{client:{su:'pageUrl',gp:'groupAssignment'},sitePage:{pn:'pageName',ch:'siteGroupId'},report:{ce:'contentElement',cm:'contentModule',hl:'headline', gt1:'campaignId',du:'destinationUrl'},userStatic:{pp:'signedIn'}}},br:{paramMap:{ event:{ evt:'type'}, report: {ce:'contentElement', hl:'headline', cm:'contentModule'} }}}),new $.track.genericTracking({base:"httpdisabled://g.msn.com/_0USHP/32?",linkTrack:1,click:{paramMap:{client:{fk:'flightKey'},sitePage:{di:'domainId',pi:'propertyId',ps:'propertySpecific',su:'sourceUrl'},report:{ce:'contentElement',cm:'contentModule',hl:'headline', gt1:'campaignId',du:'destinationUrl'},userStatic:{rid:'requestId'}}}}),new $.track.genericTracking({base:"httpdisabled://udc.msn.com/c.gif?",linkTrack:1,samplingRate:99,commonMap:{event:{evt:'type'},userStatic:{rid:'requestId',exa:'expContext'},userDynamic:{cts:'timeStamp', expac:'expCookie'},client:{fk:'flightKey',gp:'groupAssignment',optkey:'optKey',clid:'clientId'}, sitePage:{di:'domainId',pi:'propertyId',ps:'propertySpecific',pn:'pageName'}},impr:{param:{evt:'impr',js:'1'},paramMap:{client:{rf:'referrer',cu:'pageUrl',sl:'silverlightEnabled',slv:'silverlightVersion',bh:'height',bw:'width',cu:'pageUrl',scr:'screenResolution',sd:'colorDepth'},sitePage:{br:'siteGroupId',mk:'lang',pid:'pageId',mv:'pageVersion',su:'sourceUrl'},userStatic:{pp:'signedIn',bd:'birthdate',gnd:'gender'},userDynamic:{'dv.SNLogin':'settings','dv.GrpFrMod':'defaultSlotTrees',hp:'isHomePage'}}},click:{paramMap:{report:{ce:'contentElement',cm:'contentModule',hl:'headline',du:'destinationUrl'}}},unloaddisabled:{/**/}, mhreset:{}, br: { paramMap:{ event:{ evt:'type'}, report: {ce:'contentElement', hl:'headline', cm:'contentModule'} }}}),new $.track.genericTracking({base:"httpdisabled://view.atdmt.com/action/MSN_Homepage_Remessaging_111808/nc?",linkTrack:0,impr:{param:{a:'1'}}}),new $.track.genericTracking({base:"httpdisabled://b.scorecardresearch.com/b?",linkTrack:0,impr:{param:{c1:'2',c2:'3000001'},paramMap:{client:{c7:'pageUrl',c9:'referrer'},userDynamic:{rn:'timeStamp'}}}}),new $.track.genericTracking({base:"httpdisabled://exp.www.msn.com/msn/msnhp_us_lpv?",linkTrack:0,commonMap:{sitePage:{di:'domainId',pi:'propertyId',ps:'propertySpecific'},userStatic:{rid:'expRequestId'},client:{tp:'pageUrl',rf:'referrer',slv:'silverlightVersion'},userDynamic:{shp:'isSHPresent',csh:'countOfSHFromBing',cshd:'countOfSHDisplayed'}},SearchHistoryComplete:1}),new $.track.omniTracking({base:"httpdisabled://msnportal.112.2o7.net/b/ss/msnportalhome/1/H.7-pdv-2/{0}?[AQB]&{1}&[AQE]",linkTrack:1,samplingRate:9,common:{ns:"msnportalhome"},commonMap:{client:{bh:'height',bw:'width',g:'pageUrl',s:'screenResolution',k:'cookieSupport'},sitePage:{pageName:'pageName'},userDynamic:{hp:'isHomePage'}},page:{server:"Msn.com",cc:"USD",c1:"Portal"},pageMap:{client:{c29:'pageUrl',c42:'silverlightVersion',ct:'connectionType',r:'referrer'},sitePage:{c2:'lang',ch:'siteGroupId'},userStatic:{c22:'signedIn',c25:'birthdate',c26:'gender'},userDynamic:{c19:'settings',c7:'defaultSlotTrees',c23:'anid'}},link:{events:"events4"},linkMap:{report:{c12:'destinationUrl',c13:'contentModule',c15:'contentElement',c16:'headline',c18:'campaignId',oi:'sourceIndex',oid:'destinationUrl',ot:'nodeName',pev1:'destinationUrl',pev2:'headline',v11:'headline',v12:'destinationUrl'},sitePage:{pid:'pageName',c17:'omniPageName'}}}));window.async("wlAnalytics",function(){wlAnalytics.DomainIndicator="340";wlAnalytics.PropertyIndicator="7317";wlAnalytics.PropertySpecific="95101";wlAnalytics.TargetPage="httpdisabled://www.msn.com/defaultwpe3w.aspx";wlAnalytics.Taxonomy.Add("RequestId", "rid", ListNodeType.Attribute);wlAnalytics.RequestId="e09445ff071f40729f50d96932503769";wlAnalytics.Taxonomy.Add("UDCSampled", "udc", ListNodeType.Attribute);wlAnalytics.UDCSampled=true&& $.track.trackInfo.isSampled('99' ? Number('99') : 100);wlAnalytics.TrackPage();},"httpdisabled://analytics.live.com/Analytics/wlanalytics.js");//]]></script></head><body class="expht"><div class="none"><script type="text/javascript">/*<![CDATA[*/$.async("track",function(){$.track.trackPage()});//]]></script><noscript ><div><img src="../udc.msn.com/c.gif@js=0&amp;evt=impr&amp;di=340&amp;pi=7317&amp;ps=95101&amp;su=http%253A%252F%252Fwww.msn.com%252Fdefaultwpe3w.aspx&amp;pageId=6713487&amp;mk=en-us&amp;pn=US&#32;HPMSFT3W&amp;br=MSFT&amp;mv=V14&amp;pp=False&amp;rid=e09445ff071f40729f50d96932503769" alt="image beacon" /></div></noscript><img width="1" height="1" alt="" src="../www.bing.com/partner/primedns.gif" /></div><div id="wrapper" class="pa w12"><div id="head"><div class="ro"><div class="ce ce1 cel w12"><div id="tg" class="co5b9 co coa2 coc1 headerbar_us"><div class="br br1 m9"><ul class="linklist1"><li class="first"><a href="httpdisabled://login.live.com/login.srf?wa=wsignin1.0&amp;rpsnv=11&amp;ct=1253879194&amp;rver=6.0.5285.0&amp;wp=MBI&amp;wreply=http:%2F%2Fmail.live.com%2Fdefault.aspx&amp;lc=1033&amp;id=64855&amp;mkt=en-us" id="to_inbox">Hotmail</a></li><li><a href="httpdisabled://downloaddisabled.live.com/?sku=messenger">Messenger</a></li><li><a href="httpdisabled://my.msn.com/">My MSN</a></li><li class="last"><a href="httpdisabled://www.bing.com/?FORM=MSNH14">Bing</a></li></ul></div><div class="br br2 m10"><div class="pgopt1"><ul><li class="user"><span>Welcome to MSN</span></li><li class="opt"><div><a href="index.html#">Page Options</a><ul style="width:16.7em"><li class="first"><a href="index.html#" id="asugoff">Turn off autosuggest</a></li><li><a href="httpdisabled://help.live.com/help.aspx?project=wl_searchv1&amp;querytype=keyword&amp;query=sihggus&amp;mkt=en-US">What is autosuggest?</a></li><li><a href="httpdisabled://onlinehelp.microsoft.com/en-us/msn/ff808788.aspx" id="fontsize" class=voidnew">Make text larger</a></li><li class="last"><a href="httpdisabled://ie9.discoverbing.com/index_nie9.html" id="addtostart">Install IE9</a></li></ul></div></li><li class="pipe signin"><a href="httpdisabledsdisabled://login.live.com/login.srf?wa=wsignin1.0&amp;rpsnv=11&amp;ct=1302299259&amp;rver=5.5.4177.0&amp;wp=MBI&amp;wreply=http:%2F%2Fwww.msn.com%2F&amp;lc=1033&amp;id=1184" class="dMSNME_1">Sign in</a></li></ul></div></div><br class="b3" /><div class="br br3 m1"><div class="link"><a href="index.html#" id="mkhm"></a></div></div><br class="b4" /><div class="br br4 m4"><a href="index.html"><img src="../col.stb.s-msn.com/i/B7/EB75D45B8948F72EE451223E95A96.gif" width="147" height="51" alt="MSN Logo" /></a></div><div class="br br5 brl w8"><div class="websearch2"><h2>Bing Search</h2><form action="httpdisabled://www.bing.com/search" method="get" id="srchfrm"><div class="scopes cf"><a href="httpdisabled://www.bing.com/results.aspx?q=" class="first selected">Web</a><span> | </span><a href="httpdisabled://www.bing.com/results.aspx?scope=msn&amp;q=">MSN</a><span> | </span><a href="httpdisabled://www.bing.com/images/results.aspx?q=">Images</a><span> | </span><a href="httpdisabled://www.bing.com/videos/results.aspx?q=">Video</a><span> | </span><a href="httpdisabled://www.bing.com/news/results.aspx?q=">News</a><span> | </span><a href="httpdisabled://www.bing.com/maps/?q=" class="last">Maps</a></div><div class="search cf"><span class="bo"><span class="bi"><label class="hide" for="q">Search:</label><input id="q" type="text" class="text" name="q" size="69" maxlength="250" accesskey="S" /><input type="image" class="image" value="Search" alt="Search" title="Search" style="height:33px;width:173px;" src="../col.stc.s-msn.com/br/sc/i/icons/BING_websearch_2.jpg" /><input type="hidden" name="form" value="MSNH14" /></span></span></div><div class="opt"></div></form></div></div></div></div></div></div><div id="page"><div id="nav"><div class="co1b1 co coa1 coc1 m3 menunavbar2"><div class="br br1"><div class="menunavbar1 cf"><ul class="ntier1"><li class="first"><a href="httpdisabled://www.msnbc.msn.com/">NEWS</a><ul class="ntier2"><li class="first"><a href="httpdisabled://www.msnbc.msn.com/id/3032525/ns/us_news">us news</a></li><li><a href="httpdisabled://www.msnbc.msn.com/id/3032507/ns/world_news">world news</a></li><li><a href="httpdisabled://www.msnbc.msn.com/id/3032553/ns/politics">politics</a></li><li><a href="httpdisabled://today.msnbc.msn.com/">'today'</a></li><li><a href="httpdisabled://www.msnbc.msn.com/id/3032118/ns/technology_and_science">tech &amp; science</a></li><li><a href="httpdisabled://www.bltwy.com">bltwy</a></li><li><a href="httpdisabled://www.msnbc.msn.com/id/3032076/ns/health">health news</a></li><li class="last"><a href="httpdisabled://www.msnbc.msn.com/id/3032619/ns/nightly_news/">'nbc nightly news'</a></li></ul></li><li class="selected"><a href="httpdisabled://entertainment.msn.com/">ENTERTAINMENT</a><ul class="ntier2"><li class="first"><a href="httpdisabled://wonderwall.msn.com/">celebrities</a></li><li><a href="httpdisabled://zone.msn.com/en-us/home">games</a></li><li><a href="httpdisabled://movies.msn.com/ ">movies</a></li><li><a href="httpdisabled://music.msn.com/">music</a></li><li><a href="httpdisabled://tv.msn.com/">tv</a></li><li><a href="httpdisabled://thebubble.msn.com/">comedy</a></li><li><a href="httpdisabled://entertainment.msn.com/video/?from=en-us_msnhp">video</a></li><li><a href="httpdisabled://movies.msn.com/new-on-dvd/movies/">new on dvd</a></li><li><a href="httpdisabled://social.entertainment.msn.com/bloglist.aspx ">blogs</a></li><li class="last"><a href="httpdisabled://entertainment.msn.com/news/?ipp=15">entertainment news</a></li></ul></li><li><a href="httpdisabled://msn.foxsports.com/">SPORTS</a><ul class="ntier2"><li class="first"><a href="httpdisabled://msn.foxsports.com/nfl">nfl</a></li><li><a href="httpdisabled://msn.foxsports.com/mlb">mlb</a></li><li><a href="httpdisabled://msn.foxsports.com/nba">nba</a></li><li><a href="httpdisabled://msn.foxsports.com/nhl">nhl</a></li><li><a href="httpdisabled://msn.foxsports.com/collegebasketball">ncaa basketball</a></li><li><a href="httpdisabled://msn.foxsports.com/nascar">nascar</a></li><li><a href="httpdisabled://msn.foxsports.com/foxsoccer">soccer</a></li><li><a href="httpdisabled://msn.foxsports.com/tennis">tennis</a></li><li><a href="httpdisabled://msn.foxsports.com/golf">golf</a></li><li><a href="httpdisabled://msn.foxsports.com/fantasy">fantasy games</a></li><li class="last"><a href="httpdisabled://msn.foxsports.com/video?from=en-us_msnhp">video</a></li></ul></li><li><a href="httpdisabled://money.msn.com">MONEY</a><ul class="ntier2"><li class="first"><a href="httpdisabled://www.msnbc.msn.com/id/3032072/ns/business">business news</a></li><li><a href="httpdisabled://money.msn.com/investing/">investing</a></li><li><a href="httpdisabled://money.msn.com/stocks/">quotes</a></li><li><a href="httpdisabled://money.msn.com/personal-finance/">personal finance</a></li><li><a href="httpdisabled://money.bundle.com/mymoney">my money</a></li><li><a href="httpdisabled://money.msn.com/taxes">taxes</a></li><li><a href="httpdisabled://realestate.msn.com/">real estate</a></li><li class="last"><a href="httpdisabled://msn.careerbuilder.com/msn/default.aspx?SiteId=cbmsn_home">careers</a></li></ul></li><li><a href="httpdisabled://lifestyle.msn.com/">LIFESTYLE</a><ul class="ntier2"><li class="first"><a href="httpdisabled://lifestyle.msn.com/your-look/">beauty &amp; fashion</a></li><li><a href="httpdisabled://www.delish.com/">cooking</a></li><li><a href="httpdisabled://glo.msn.com/">glo</a></li><li><a href="httpdisabled://health.msn.com/">health</a></li><li><a href="httpdisabled://fitbie.msn.com">fitbie</a></li><li><a href="httpdisabled://glo.msn.com/horoscopes">horoscopes</a></li><li><a href="httpdisabled://lifestyle.msn.com/your-life/family-parenting/">moms</a></li><li><a href="httpdisabled://lifestyle.msn.com/relationships/">love &amp; relationships</a></li><li class="last"><a href="httpdisabled://dating.msn.com/index.aspx?TrackingID=516163&amp;BannerID=670269">dating</a></li></ul></li><li><a href="httpdisabled://local.msn.com/">LOCAL</a><ul class="ntier2"><li class="first"><a href="httpdisabled://local.msn.com/news.aspx">news</a></li><li><a href="httpdisabled://local.msn.com/weather.aspx">weather</a></li><li><a href="httpdisabled://local.msn.com/sports.aspx">sports</a></li><li><a href="httpdisabled://local.msn.com/movies-events.aspx">movies &amp; events</a></li><li><a href="httpdisabled://local.msn.com/restaurants.aspx">restaurants</a></li><li><a href="httpdisabled://deals.msn.com">local deals</a></li><li><a href="httpdisabled://www.bing.com/local/ypdefault.aspx?cobrand=1">local directory</a></li><li class="last"><a href="httpdisabled://msn.whitepages.com/">white pages</a></li></ul></li><li><a href="httpdisabled://home.autos.msn.com">AUTOS</a><ul class="ntier2"><li class="first"><a href="httpdisabled://editorial.autos.msn.com/new-cars/default.aspx">new cars</a></li><li><a href="httpdisabled://editorial.autos.msn.com/used-cars/default.aspx">used cars</a></li><li><a href="httpdisabled://autos.msn.com/research/compare/AddCompare.aspx">compare vehicles</a></li><li><a href="httpdisabled://editorial.autos.msn.com/articles/default.aspx">reviews &amp; articles</a></li><li><a href="httpdisabled://editorial.autos.msn.com/media/default.aspx">pictures</a></li><li><a href="httpdisabled://editorial.autos.msn.com/media/video/default.aspx">videos</a></li><li class="last"><a href="httpdisabled://editorial.autos.msn.com/blogs/autosblog.aspx">blogs</a></li></ul></li><li><a href="httpdisabled://msn.foxsports.com/golf">MASTERS</a><ul class="ntier2"><li class="first"><a href="httpdisabled://msn.foxsports.com/golf ">news</a></li><li><a href="httpdisabled://msn.foxsports.com/golf/leaderboard">leaderboard</a></li><li><a href="httpdisabled://msn.foxsports.com/golf/photo-gallery">photos</a></li><li class="last"><a href="httpdisabled://msn.foxsports.com/video/Golf?from=en-us_msnhp">video analysis</a></li></ul></li><li class="last"><a href="httpdisabled://specials.msn.com/alphabet.aspx">MORE</a><ul class="ntier2"><li class="first"><a href="httpdisabled://www.bing.com/travel/?cid=msn_nav_lifestyle&amp;FORM=MSNNAV">travel</a></li><li><a href="httpdisabled://latino.msn.com/">latino</a></li><li><a href="httpdisabled://www.bing.com/maps/default.aspx?FORM=MSNNAV">maps</a></li><li><a href="httpdisabled://www.discovermsn.com">mobile</a></li><li><a href="httpdisabled://my.msn.com/">my msn</a></li><li><a href="httpdisabled://www.bing.com/videos/browse?from=en-us_msnhp">video</a></li><li><a href="httpdisabled://games.msn.com">games preview</a></li><li><a href="httpdisabled://www.bing.com/shopping?FORM=SHOPH2">shopping</a></li><li><a href="httpdisabled://insidemsn.wordpress.com/">corrections</a></li><li><a href="httpdisabledsdisabled://secure.opinionlab.com/ccc01/o.asp?ID=WpkpVtTB ">feedback</a></li><li class="last"><a href="httpdisabled://specials.msn.com/alphabet.aspx">full msn index</a></li></ul></li></ul></div></div></div></div><div id="content"><div id="subhead"></div><div id="area1" class="re w8"><div class="ro"><div class="ce ce1 w3"><div id="pagedate" class="co1b1 co coa1 coc1 date1"><div class="br br1"><div class="link"><a href="httpdisabled://www.bing.com/search?q=April+8&amp;mkt=en-us&amp;FORM=MSNHPT">Friday, April 8, 2011</a></div></div></div></div><div class="ce ce2 cel w5"><div id="miniweather" class="co1b1 co coa1 coc1 m3 "><div class="br br1"><div class="weather2" ><div class="location" style="padding-right:5px;"><h3 class="h3"><a href="httpdisabled://local.msn.com/weather.aspx?q=Portland-OR&amp;zip=97205">Portland, OR</a></h3></div><div class="forecast"><div class="weatherimage"><a href="httpdisabled://local.msn.com/weather.aspx?q=Portland-OR&amp;zip=97205"><img src="../blst.msn.com/as/wea3/i/en-us/law/30.gif" height="20" width="25" alt="Partly Cloudy" title="Partly Cloudy" /></a></div><div class="minidata"><div class="today"><a href="httpdisabled://local.msn.com/weather.aspx?q=Portland-OR&amp;zip=97205"><span class="high">59°</span>/
+ <span class="low">39°</span></a></div><div class="weaheading" style="padding-right:5px;"><ul class="degreetype"><li class="fahrenheit selected"><a title="Fahrenheit" href="index.html#">°F</a></li><li class="celsius"><a title="Celsius" href="index.html#">°C</a></li></ul></div></div><div class="extended" style="border-left:1px solid #333333;float:left;padding-bottom:1px;margin-top:0.417em"><ul class="extendperiod" style="list-style-type:none;margin-left:0.417em;padding:0;display:inline;"><li style="display:inline;padding:0.083em 0.25em 0.083em 0.083em;color:#333;"><a href="httpdisabled://local.msn.com/weather.aspx?q=Portland-OR&amp;zip=97205#fivedayforecast"><span class="five">5-day</span></a></li><li style="display:inline;padding:0.083em 0.25em 0.083em 0.083em;color:#333;"><a href="httpdisabled://local.msn.com/ten-day.aspx?q=Portland-OR&amp;zip=97205"><span class="ten">10-day</span></a></li></ul></div></div></div></div></div></div></div><div class="ro"><div class="ce ce1 cel w8"><div class="cogr coss coj" id="infopane_hops"><div id="slide1" class="cof"><div id="headlinepane1" class="co1b1 co coa2 coc1"><div class="br br1"><div class="scp1 cf"><div class="npane n3"><span><a href="httpdisabled://firstread.msnbc.msn.com/_news/2011/04/08/6433568-gop-dem-huddles-fail-to-yield-progress-on-budget-deal/?GT1=43001">Latest: Budget Talks, Sudden Departure, More</a></span><ul><li style="width:206px;" class="first"><a href="httpdisabled://firstread.msnbc.msn.com/_news/2011/04/08/6433568-gop-dem-huddles-fail-to-yield-progress-on-budget-deal/?GT1=43001"><img src="../col.stb.s-msn.com/i/CE/19F603C3122D48B6554BBD495195.jpg" title="Image: (From left) Senate Majority Leader Harry Reid &amp; House Speaker John Boehner (Photos © J. Scott Applewhite/AP)" width="206" height="144" alt="Image: (From left) Senate Majority Leader Harry Reid &amp; House Speaker John Boehner (Photos © J. Scott Applewhite/AP)" /></a><a href="httpdisabled://firstread.msnbc.msn.com/_news/2011/04/08/6433568-gop-dem-huddles-fail-to-yield-progress-on-budget-deal/?GT1=43001">Tempers flare, clock ticks</a></li><li style="width:206px;"><a href="httpdisabled://msn.foxsports.com/mlb/story/Slugger-Manny-Ramirez-retires-amid-mlb-drug-policy-issues-040811/?GT1=39002"><img src="../col.stb.s-msn.com/i/23/6B8E88315584A40B04E32D89551E.jpg" title="Image: Manny Ramirez of the Tampa Bay Rays (© Tom DiPace/Sports Illustrated/Getty Images)" width="206" height="144" alt="Image: Manny Ramirez of the Tampa Bay Rays (© Tom DiPace/Sports Illustrated/Getty Images)" /></a><a href="httpdisabled://msn.foxsports.com/mlb/story/Slugger-Manny-Ramirez-retires-amid-mlb-drug-policy-issues-040811/?GT1=39002">Manny Ramirez quits MLB</a></li><li style="width:206px;" class="last"><a href="httpdisabled://wonderwall.msn.com/music/naomi-wynonna-judd-we-were-sexually-abused-too-1613238.story?GT1=28135"><img src="../col.stb.s-msn.com/i/D8/41FF8CA0A47CC8208E684FA1BE6D6.jpg" title="Image: (From left) Wynonna &amp; Naomi Judd (© Frazer Harrison/Getty Images for ACM)" width="206" height="144" alt="Image: (From left) Wynonna &amp; Naomi Judd (© Frazer Harrison/Getty Images for ACM)" /></a><a href="httpdisabled://wonderwall.msn.com/music/naomi-wynonna-judd-we-were-sexually-abused-too-1613238.story?GT1=28135">More abuse reports for Judds</a></li></ul></div></div></div></div></div><div id="slide2"><div id="headlinepane2" class="co1b1 co coa2 coc1"> </div></div><div id="slide3"><div id="headlinepane3" class="co1b1 co coa2 coc1"> </div></div><div id="slide4"><div id="headlinepane4" class="co1b1 co coa2 coc1"> </div></div><div id="slide5"><div id="headlinepane5" class="co1b1 co coa2 coc1"> </div></div></div></div></div><div class="ro"><div class="ce ce1 cel w8"><div id="todays_picks" class="co1b1 co coa2 coc1 m3 hlcpm1"><h2 class="h2 cf"><span>EDITORS' PICKS</span></h2><div class="br br1"><div class="hcpep3 cf"><ul><li><a href="httpdisabled://www.msnbc.msn.com/id/42497862/ns/weather/?GT1=43001">Deadly pileup after sandstorm</a></li><li><a href="httpdisabled://www.msnbc.msn.com/id/42492782/ns/world_news-europe/?GT1=43001">People shot on British sub</a></li><li><a href="httpdisabled://msn.foxsports.com/mlb/story/109-year-old-woman-throws-out-first-pitch-at-minor-league-baseball-game-040811?GT1=39002">Woman, 109, throws 1st pitch</a></li><li><a href="httpdisabled://www.delish.com/food/recalls-reviews/pizza-topped-with-face-of-jesus-auctioned-on-ebay?GT1=47001">'Jesus' pizza sells at auction</a></li><li><a href="httpdisabled://msn.foxsports.com/foxsoccer/latinamerica/story/Brazilian-soccer-star-Neymar-is-ejected-for-celebrating-with-face-mask-040711/?gt1=39002">Player ejected for celebration</a></li><li><a href="httpdisabled://msn.foxsports.com/mlb/story/Barry-Bonds-perjury-trial-jury-begins-deliberations-040811/?GT1=39002">Bonds jury deliberating</a></li><li><a href="httpdisabled://money.msn.com/home-loans/article.aspx?post=06a7a70e-1e8e-4b24-8331-48aa100c6075&amp;gt1=33032">How to 'steal' a house</a></li><li><a href="httpdisabled://wonderwall.msn.com/movies/celebs-gone-royal-12022.gallery?GT1=28135">Which celebs are also royals?</a></li><li><a href="httpdisabled://realestate.msn.com/article.aspx?cp-documentid=28280145&amp;GT1=35006">What all tenants should know</a></li></ul></div></div></div></div></div><div class="ro"><div class="ce ce1 cel w8"><div class="cogr cotb cogrst coj" id="maintg"><div id="latest_hops"><div class="co1b1 co coa2 coc1 m3 hlcpm1"><h2 class="h2 cf"><span>MUST-SEE</span></h2></div></div><div id="news"><div class="co1b1 co coa2 coc1 hlcpm1"><h2 class="h2 cf"><span>NEWS</span></h2></div></div><div id="video"><div class="co1b1 co coa2 coc1 m3 hlcp1"><h2 class="h2 cf"><span>VIDEO</span></h2></div></div><div id="health"><div class="co1b1 co coa2 coc1 m3 hlcp1"><h2 class="h2 cf"><span>HEALTH</span></h2></div></div><div id="countdown" class="cof"><div class="co3b6 cf co coa2 coc1 m3 en-us1"><h2 class="h2 cf"><span>GOVERNMENT SHUTDOWN</span></h2><div class="br br1 w4"><div style="clear:both;" class="hlcp1 cf"><div class="pri" style="float:left;width:303px"><div class="first"><div><a href="httpdisabled://money.msn.com/taxes/latest.aspx?post=f980ad10-50ca-418d-8925-997d2fc859a7&amp;gt1=33005"><img class="landscape" src="../col.stb.s-msn.com/i/76/CAF5FAB7F245F96327F2B4C806D.jpg" title="Image: U.S. Capitol (© Jim Young/Reuters)" width="303" height="117" alt="Image: U.S. Capitol (© Jim Young/Reuters)" /></a></div><div><a href="httpdisabled://money.msn.com/taxes/latest.aspx?post=f980ad10-50ca-418d-8925-997d2fc859a7&amp;gt1=33005">Could shutdown derail economy?</a><div class="richtext"><p>Some analysts fear that a government closure could <a href="httpdisabled://money.msn.com/taxes/latest.aspx?post=f980ad10-50ca-418d-8925-997d2fc859a7&amp;gt1=33005">dampen consumer confidence &amp; spending</a>.</p></div></div></div></div></div></div><div class="br br2"><div class="flashlinkedimg" id="f_countdown"><a href="httpdisabled://www.msnbc.msn.com/id/42350517/ns/politics/?GT1=43001"><img src="../col.stb.s-msn.com/i/5B/CC662FC6233C7449D9C7F9796801D.jpg" width="311" height="72" alt="Image: The White House (© Murat Taner/Getty Images)" /></a><script type="text/javascript">jQuery("#f_countdown").async("createFlash",[{version:9,attr:{id:'f_countdownObj',width:311,height:76},param:{movie:'http://col.stb.s-msn.com/i/D3/3894AD869183D2FA8BA6BCCEB519C.swf'}}]);</script></div></div><div class="br br3 brl w4"><ul class="linklist16"><li class="first"><a href="httpdisabled://firstread.msnbc.msn.com/_news/2011/04/08/6433138-boehner-joins-lawmakers-pledging-to-return-pay-in-event-of-shutdown-?GT1=43001">Some lawmakers volunteer to forgo pay</a></li><li><a href="httpdisabled://www.bing.com/travel/content/search?q=National+Parks%2c+Museums+Face+Looming+Shutdown&amp;cid=msn1183652&amp;form=TRVCON&amp;gt1=41000">Parks, museums around US could shutter</a></li><li><a href="httpdisabledsdisabled://www.facebook.com/#!/msn">Sound off on the shutdown on Facebook</a></li><li><a href="httpdisabled://msnbc.newsvine.com/_question/2011/04/08/6431363-do-you-think-the-us-government-will-shut-down-at-midnight?gt1=43001">Vote: Do you think government will close?</a></li><li class="last"><a href="httpdisabled://specials.msn.com/A-List/Money/Government-shutdown.aspx?cp-documentid=28275815&amp;imageindex=1">Search: See what's at stake if a deal isn't reached</a></li></ul></div></div></div></div></div></div><div class="ro" id="stk_heading"><div class="ce ce1 cel w8 m3"><div id="stk_head" class="co1b1 co coa2 coc1 single2"><h2 class="h2 cf"><span>MARKET UPDATE</span></h2><div id="stk_off" class="br br1"></div></div></div></div><div class="ro" id="stk_data"><div class="ce ce1 w33"><div id="stk" class="co2b1 cf co coa2 coc1 money1"><div class="br br1 m3"><div class="indices1"><div class="sitime">Updated: 04/08/2011 04:40 ET</div><table summary="Market update"><caption>Market update</caption><thead><tr><th abbr="Symbol" id="siindex">Symbol</th><th abbr="Last" id="silast">Last</th><th abbr="Change" id="sichange">Change</th></tr></thead><tbody><tr class="first"><td class="siidx" headers="siindex"><a href="httpdisabled://investing.money.msn.com/investments/stock-price?Symbol=$INDU">DOW</a></td><td class="silast" headers="silast">12,380.05</td><td headers="sichange" class="neg">-29.44</td></tr><tr><td class="siidx" headers="siindex"><a href="httpdisabled://investing.money.msn.com/investments/stock-price?Symbol=$COMPX">NASDAQ</a></td><td class="silast" headers="silast">2,780.42</td><td headers="sichange" class="neg">-15.72</td></tr><tr class="last"><td class="siidx" headers="siindex"><a href="httpdisabled://investing.money.msn.com/investments/stock-price?Symbol=$INX">S&amp;P</a></td><td class="silast" headers="silast">1,328.17</td><td headers="sichange" class="neg">-5.34</td></tr></tbody></table></div></div><div class="br br2 brl m3"></div></div></div><div class="ce ce2 w33 m3" id="stk_form"><div class="co1b1 co coa1 coc1 m3 generic1"><div class="br br1"><form action="httpdisabled://moneycentral.msn.com/detail/stock_quote" method="get" class="simple8 cf"><p>legend</p><div><label for="idlblsearch">Get a stock quote</label><div><input id="idlblsearch" type="text" class="text" name="symbol" size="25" maxlength="255" /><input type="image" class="image" alt="" title="" value="" style="height:22px;width:22px;" src="../col.stb.s-msn.com/i/E2/37BA92E210D341BFDBF4126422A3D2.gif" /></div></div></form></div></div><div class="co2b1 cf co coa2 coc1 sponad1"><div class="br br1"><div class="richtext"><p>Brought to you by:</p></div></div><div class="br br2 brl"><div class="advertisement"><div id="SponsorAd"><script type="text/javascript">$.dap("&amp;PG=MSNHQ2&amp;AP=1402",73,14,"SponsorAd");</script></div></div></div></div></div><div class="ce ce3 cel w33"><div id="stk_linklist" class="co1b1 co coa2 coc1 single2"><div class="br br1"><ul class="linklist16"><li class="first"><a href="httpdisabled://money.msn.com/market-news/post.aspx?post=46377f6d-466f-47c3-9c5a-0355cfb08b9a">Dow falls 29; oil tops $112</a></li><li><a href="httpdisabled://money.msn.com/how-to-invest/article.aspx?post=a86ac614-4428-403e-8e2e-4899c3f3638a">How Clorox is cleaning up</a></li><li class="last"><a href="httpdisabled://money.msn.com/market-news/default.aspx?feat=0ba6fe64-a5a5-4091-af1d-f710464b7fa5&amp;_nwpt=1">Gold hits another record</a></li></ul></div></div></div></div><div class="ro"><div class="ce ce1 cel w8"><div class="cogr cotb cogrst coj" id="sectabs_hops"><div id="entertainment"><div class="co1b1 co coa2 coc1 m3 hlcpm1"><h2 class="h2 cf"><span>CELEBS &amp; GOSSIP</span></h2></div></div><div id="sports" class="cof"><div class="co1b1 co coa2 coc1 hlcp1"><h2 class="h2 cf"><span>SPORTS</span></h2><div class="br br1"><div style="clear:both;" class="hlcp1 cf"><div class="pri" style="float:left;width:303px"><div class="first" style="clear:right;"><div style="float:right;width:116px"><a href="httpdisabled://msn.foxsports.com/golf/story/Masters-Round-2-live-blog-040811"><img class="portrait" src="../static.foxsports.com/content/fscom/img/2011/04/07/040711-Golf-Tiger-Woods-1120pm-PI_20110407142414593_116_175.JPG" width="116" height="175" alt="Image: Tiger Woods (© Jamie Squire/Getty Images)" /></a></div><div style="margin-right:116px; padding-right: 10px;"><a href="httpdisabled://msn.foxsports.com/golf/story/Masters-Round-2-live-blog-040811">In progress: Tiger back for Round 2</a><div class="richtext"><p>voiding the Masters with a 71, Tiger Woods needs to hustle to make up ground and catch the tournament leaders. Plus: <a href="httpdisabled://msn.foxsports.com/golf/leaderboard">Round 2 leaderboard</a></p></div></div></div></div><ul style="margin-left: 303px; padding-left: 25px;"><li class="ter"><a href="httpdisabled://msn.foxsports.com/nba/story/Getting-to-know-the-real-Maverick-Carter-LeBron-James-business-manager-childhood-friend-040711">Whitlock: Meet the man behind LeBron James</a></li><li class="ter"><a href="httpdisabled://msn.foxsports.com/cbk/story/St-Johns-Red-Storm-coach-Steve-Lavin-has-prostate-cancer-040811">Successful Big East basketball coach has cancer</a></li><li class="ter"><a href="httpdisabled://msn.foxsports.com/mlb/story/Boston-Red-Sox-defeat-New-York-Yankees-040811">Red Sox earn first victory at Yankees' expense</a></li><li class="ter"><a href="httpdisabled://msn.foxsports.com/nba/story/new-jersy-nets-fined-50000-for-jay-zs-kentucky-locker-room-visit-040911">Nets fined for rap star's Kentucky locker room visit</a></li><li class="ter"><a href="httpdisabled://msn.foxsports.com/nascar/story/NASCAR-Denny-Hamlin-to-drive-kid-created-March-of-Dimes-paint-scheme">NASCAR heavyweight to use children's design</a></li><li class="ter"><a href="httpdisabled://msn.foxsports.com/nba/story/Former-NBA-MVP-Allen-Iverson-lets-loose-against-Atlanta-police-040811">Former NBA MVP gets snippy with Atlanta police</a></li><li class="ter"><a href="httpdisabled://rutgers.scout.com/2/1062634.html">Video: Rutgers football team gets surprise visitor</a></li></ul></div></div></div></div><div id="realestate"><div class="co1b1 co coa2 coc1 m3 hlcpm1"><h2 class="h2 cf"><span>REAL ESTATE</span></h2></div></div><div id="weirdnews"><div class="co1b1 co coa2 coc1 m3 hlcp1"><h2 class="h2 cf"><span>WEIRD NEWS</span></h2></div></div></div></div></div><div class="ro"><div class="ce ce1 w4"><div class="cogr cotb cogrst" id="localtg"><div id="local" class="cof"><div class="co6b7 co coa2 coc1 m5 local1"><h2 class="h2 cf"><span>LOCAL</span></h2><div class="br br1 brl m3"><div class="weather1" ><h3 class="h3 cf"><a href="httpdisabled://local.msn.com/weather.aspx?q=Portland-OR&amp;zip=97205">Portland, OR</a><span><a class="voidnew" href="httpdisabled://www.bing.com/maps/?where1=Portland, OR&amp;FORM=MSNHPM">Get Directions</a></span></h3><div class="weahr"> </div><div class="weaheading cf"><h4><a href="httpdisabled://local.msn.com/weather.aspx?q=Portland-OR&amp;zip=97205">Local Weather</a></h4><ul class="degreetype"><li class="fahrenheit selected"><a title="Fahrenheit" href="index.html#">°F
+ </a></li><li class="celsius"><a title="Celsius" href="index.html#">°C
+ </a></li></ul></div><div class="forecast cf"><a href="httpdisabled://local.msn.com/weather.aspx?q=Portland-OR&amp;zip=97205"><img src="../blst.msn.com/as/wea3/i/en-us/law/30.gif" height="45" width="55" alt="Partly Cloudy" title="Partly Cloudy" /></a><div class="data"><div class="today"><ul><li class="first"><a href="httpdisabled://local.msn.com/weather.aspx?q=Portland-OR&amp;zip=97205">Friday</a></li><li class="last"><a href="httpdisabled://local.msn.com/weather.aspx?q=Portland-OR&amp;zip=97205"><span class="high">High 59°</span>/
+ <span class="low">Low 39°</span></a></li></ul></div><div class="temp"><a href="httpdisabled://local.msn.com/weather.aspx?q=Portland-OR&amp;zip=97205">55°</a></div><div class="conditions"><a href="httpdisabled://local.msn.com/weather.aspx?q=Portland-OR&amp;zip=97205">Partly Cloudy</a></div><ul class="forecasts"><li class="first"><a href="httpdisabled://local.msn.com/hourly.aspx?q=Portland-OR&amp;zip=97205">Hourly</a></li><li><a href="httpdisabled://local.msn.com/weather.aspx?q=Portland-OR&amp;zip=97205#fivedayforecast">5-day</a></li><li class="last"><a href="httpdisabled://local.msn.com/ten-day.aspx?q=Portland-OR&amp;zip=97205">10-day</a></li></ul></div></div></div></div><div class="hr"></div><div class="br br2 m3"><div class="locnews1"><h3 class="h3 cf"><a href="httpdisabled://local.msn.com/news.aspx?zip=97220&amp;q=97220">Local News</a></h3><div><ul>
+ <li class="first"><a href="httpdisabled://www.msnbc.msn.com/id/42500254/ns/local_news-portland_or/">OR lawmakers look to expand bottle bill</a></li>
+ <li><a href="httpdisabled://www.msnbc.msn.com/id/42499610/ns/local_news-portland_or/">Wash. cop awarded for sword attack rescue</a></li>
+ <li class="last"><a href="httpdisabled://www.msnbc.msn.com/id/42482989/ns/local_news-portland_or/">Suspect indicted in Vaughn death for murder with firearm</a></li>
+</ul></div><div class="hideseemore"><a href="httpdisabled://local.msn.com/news.aspx?zip=97220&amp;q=97220">Find more local news</a></div></div></div><div class="hr"></div><br class="b3" /><div class="br br3 m3"><div class="locsports1"><h3 class="h3 cf"><a href="httpdisabled://local.msn.com/sports.aspx?zip=97220&amp;q=97220">Local Sports</a></h3><div><ul>
+ <li class="first"><a href="httpdisabled://msn.foxsports.com/mlb/story/Seattle-prepares-for-emotional-voider-47907243">Seattle prepares for emotional voider</a></li>
+ <li><a href="httpdisabled://msn.foxsports.com/nba/story/gerald-wallace-leads-portland-trail-blazers-past-utah-jazz-040711-">Wallace, Batum pace Trail Blazers</a></li>
+ <li class="last"><a href="httpdisabled://msn.foxsports.com/nba/story/LakersTrail-Blazers-Preview-87822291">Lakers-Trail Blazers Preview</a></li>
+</ul></div><div class="hideseemore"><a href="httpdisabled://local.msn.com/sports.aspx?zip=97220&amp;q=97220">Find more local sports</a></div></div></div><div class="hr"></div><br class="b4" /><div class="br br4 m3"><div class="locevents1"><h3 class="h3 cf"><a href="httpdisabled://local.msn.com/movies-events.aspx?zip=97220&amp;q=97220">Local Events</a></h3><div><ul><li class="first"><a href="httpdisabled://www.bing.com/events/search?q=events near 97220&amp;p1=[Events+source=&quot;vertical&quot;+qzeventid=&quot;z172934165&quot;]&amp;form=MSNLAP">A Rocket To The Moon</a></li><li><a href="httpdisabled://www.bing.com/events/search?q=events near 97220&amp;p1=[Events+source=&quot;vertical&quot;+qzeventid=&quot;z173806065&quot;]&amp;form=MSNLAP">Shuffleboard Tournament</a></li><li class="last"><a href="httpdisabled://www.bing.com/events/search?q=events near 97220&amp;p1=[Events+source=&quot;vertical&quot;+qzeventid=&quot;z173818465&quot;]&amp;form=MSNLAP">Redneck Summer Games</a></li></ul></div><div class="hideseemore"><a href="httpdisabled://local.msn.com/movies-events.aspx?zip=97220&amp;q=97220">Find more local events</a></div></div></div><div class="hr"></div><br class="b5" /><div class="br br5 m3"><div class="lmlsf1 cf"><div class="findmore"><div><strong>Find more local:</strong></div><div><ul class="linklist9 cf"><li class="first"><a href="httpdisabled://local.msn.com/news.aspx?zip=97220&amp;q=97220">News</a></li><li><a href="httpdisabled://local.msn.com/sports.aspx?zip=97220&amp;q=97220">Sports</a></li><li class="last"><a href="httpdisabled://local.msn.com/movies-events.aspx?zip=97220&amp;q=97220">Events</a></li></ul></div></div><div class="simpleform"><form action="httpdisabled://local.msn.com/news.aspx" method="get" class="simple8 cf" id="newsid"><p>Change your location</p><div><label for="txtZipCode"><strong>Enter US ZIP or city</strong></label><div><input id="txtZipCode" type="text" class="text" name="zip" size="30" maxlength="30" /><input type="image" class="image" alt="" title="" value="" style="height:22px;width:22px;" src="../col.stb.s-msn.com/i/E2/37BA92E210D341BFDBF4126422A3D2.gif" /></div></div></form></div></div></div><div class="hr"></div><div class="br br6 brl m3"><h3 class="h3 cf"><a href="httpdisabled://deals.msn.com/">Local Shopping</a></h3><div style="clear:both;" class="hlcp1 cf"><ul><li class="sec last"><div style="float:left;width:128px"><a href="httpdisabled://deals.msn.com/"><img class="landscape" src="../col.stb.s-msn.com/i/EA/9BECE90994978BFAE6F38561515E8.jpg" title="Image: Women window-shopping (© Nisian Hughes/Getty Images)" width="128" height="73" alt="Image: Man &amp; woman with hat (© MM Productions/Getty Images)" /></a></div><div style="margin-left:128px; padding-left: 10px"><a href="httpdisabled://deals.msn.com/"><strong>Top off your look</strong></a><div class="richtext"><p>Save on accessories, home décor, cameras &amp; more to get a jump on spring.</p></div></div></li></ul></div></div></div></div><div id="movies"><div class="co2b2 co coa2 coc1 movies1"><h2 class="h2 cf"><span>MOVIES</span></h2></div></div><div id="jobs"><div class="co2b2 co coa2 coc1 jobs1"><h2 class="h2 cf"><span>JOBS</span></h2></div></div><div id="maps"><div class="co2b2 co coa2 coc1 movies1"><h2 class="h2 cf"><span>TRAVEL</span></h2></div></div></div></div><div class="ce ce2 cel w4"><div class="co1b1 co coa2 coc1 ad1"><div class="br br1"><div class="advertisement" style="width:300px"><div id="Ad300x60"><script type="text/javascript">$.dap("&amp;PG=MSNIF1&amp;AP=1455",300,60,"Ad300x60");</script></div><div class="adfb cf right"><a href="httpdisabled://g.msn.com/AIPRIV/en-us" class="adch"><img src="../col.stc.s-msn.com/br/sc/i/icons/adchoices_gif.gif" alt="Ad Choice" title="Ad Choice" height="12" width="68" /></a></div></div></div></div><div class="cogr"><div id="shpcob15" class="cof"><div id="shopping" class="co4b5 co coa2 coc1 shopping1"><h2 class="h2 cf"><span>SHOPPING</span></h2><div class="br br1 m3"><form action="httpdisabled://www.bing.com/shopping/search" method="get" class="simple8 cf"><p>legend</p><div><label for="idQuickSearch">Shop fitness equipment</label><div><input id="idQuickSearch" type="text" class="text" name="q" size="30" maxlength="30" /><input type="hidden" name="form" value="MSNSSB" /><input type="image" class="image" alt="" title="" value="" style="height:22px;width:22px;" src="../col.stb.s-msn.com/i/E2/37BA92E210D341BFDBF4126422A3D2.gif" /></div></div></form></div><div class="br br2"><ul class="linklist22"><li class="first"><a href="httpdisabled://www.bing.com/shopping/spring-shoe-trends/r/169?FORM=SHOPH1&amp;crea=040811shoes">What are the hottest shoe trends for spring?</a></li><li><a href="httpdisabled://www.bing.com/shopping/digital-cameras/search?q=digita%20camera&amp;p1=%5bCommerceService%20scenario%3d%22f%22%20a%3d%22ra%22%20r%3d%22pricelow%7c60%2cpricehigh%7c150%2cleafcategoryid%7c4362%2cbrandid%7c1213%22%5d&amp;FORM=SHOPH1&amp;crea=040811cameras">Canon digital cameras under $150</a></li><li class="last"><a href="httpdisabled://www.bing.com/shopping/sheets/c/4934?q=cotton+bedding&amp;FORM=SHOPH1&amp;crea=040811sheets">Crisp, cool cotton sheets for spring</a></li></ul></div><br class="b3" /><div class="br br3 m1"><ul class="linklist9 cf"><li class="first"><a href="httpdisabled://www.bing.com/shopping?form=SHOPH1&amp;crea=shoppingpipe">Shop now</a></li><li class="last"><a href="httpdisabled://www.bing.com/shopping/sunless-tanners/c/4346?q=sunless+tanners&amp;s=ra&amp;FORM=SHOPH1&amp;crea=040811tanners">Top-rated sunless tanners</a></li></ul></div><div class="hr"></div><div id="ad" class="br br4 brl"><h3 class="h3 cf">ADVERTISEMENTS</h3><div class="headlinelist1 cf"><div style="width: 70px;"><a href="httpdisabledsdisabled://www.lendgo.com/lam/?tg_ref=ms_hp&amp;camp_id=tl1&amp;rate=2.6&amp;keyword=shoc2&amp;lt=31&amp;rt=v" class=voidnew"><img src="../col.stb.s-msn.com/i/CF/59B3CB34EF11B221719175143187.jpg" title="Click here!" width="70" height="70" alt="Click here!" /></a></div><ul style="margin-left: 80px;"><li class="first"><a href="httpdisabledsdisabled://www.lendgo.com/lam/?tg_ref=ms_hp&amp;camp_id=tl1&amp;rate=2.6&amp;keyword=shoc2&amp;lt=31&amp;rt=v" class=voidnew"><strong>2.6% Mortgage: $160K loan $659mo</strong></a></li><li><a href="httpdisabled://ib.adnxs.com/seg?add=111553,108836,108837&amp;redir=http%3A%2F%2Ftracking.servedbyy.com%2Faff_c%3Foffer_id%3D237%26aff_id%3D1%26source%3DMSNHPTL%26url_id%3D484" class=voidnew">Half Off highly rated wines</a></li><li><a href="httpdisabled://deals.msn.com/default.aspx?m=4LSWSD" class=voidnew">4 Local stores – Shocking deals</a></li><li class="last"><a href="httpdisabled://clk.atdmt.com/AST/go/307318345/direct/01/" class=voidnew">L.L.Bean Free Shipping</a></li></ul></div></div><div class="hr"></div></div></div></div><div id="spotlight" class="co1b1 co coa2 coc1 spotlight1"><div class="br br1"><h3 class="h3 cf"><a href="httpdisabled://specials.msn.com/alphabet.aspx">MSN SPOTLIGHT</a></h3><div class="headlinelist1 cf"><div style="width: 75px;"><a href="httpdisabled://businessonmain.msn.com/browseresources/slideshows/smallbusinesstrends.aspx?cp-documentid=26870779&amp;source=msneditorial&amp;gt1=25049"><img src="../col.stb.s-msn.com/i/80/82E2A652E4A790B140675E74293AD6.jpg" title="Image: Jennifer Lopez (© Charles Sykes/AP)" width="75" height="128" alt="Image: Jennifer Lopez (© Charles Sykes/AP)" /></a></div><ul style="margin-left: 85px;"><li class="first"><a href="httpdisabled://businessonmain.msn.com/browseresources/slideshows/smallbusinesstrends.aspx?cp-documentid=26870779&amp;source=msneditorial&amp;gt1=25049">J.Lo finds a new way to be hot </a></li><li><span class="media"><a href="httpdisabled://businessonmain.msn.com/videos/newsonmain.aspx?cp-documentid=27678565&amp;source=msneditorial&amp;from=en-us_msnhp&amp;gt1=25049">Meet a real-life Willy Wonka </a></span></li><li><a href="httpdisabled://businessonmain.msn.com/browseresources/articles/firststeps.aspx?cp-documentid=28039284&amp;source=msneditorial&amp;gt1=25049">How to start a biz with 50 bucks </a></li><li><span class="media"><a href="httpdisabled://businessonmain.msn.com/videos/newsonmain.aspx?cp-documentid=27844438&amp;source=msneditorial&amp;gt1=25049">2 indie theaters take on big boys </a></span></li><li class="last"><a href="httpdisabled://businessonmain.msn.com/browseresources/articles/innovationstories.aspx?cp-documentid=28039658&amp;source=msneditorial&amp;gt1=25049">They throw zombie parties for a living </a></li></ul></div></div></div></div></div></div><div id="area2" class="re w4"><div class="ro"><div class="ce ce1 cel w4"><div class="co1b1 co coa2 coc1 ad1"><div class="br br1"><div class="advertisement" style="width:300px"><div id="idShowcaseAd" style="min-height:250px"><script type="text/javascript">$.dap("&amp;PG=MSNREC&amp;AP=1089",300,250,"idShowcaseAd");</script></div><div class="adfb cf left"><a href="httpdisabled://g.msn.com/AIPRIV/en-us" class="adch"><img src="../col.stc.s-msn.com/br/sc/i/icons/adchoices_gif.gif" alt="Ad Choice" title="Ad Choice" height="12" width="68" /></a><a href="httpdisabled://ccc01.opinionlab.com/o.asp?id=swHtlTXj">Ad Feedback</a></div></div></div></div></div></div><div class="ro"><div class="ce ce1 cel w4"><div class="co1b1 co coa1 coc1 alert1"><div class="br br1"><div class="link"><a href="httpdisabled://ie9.discoverbing.com/?form=MFEHPG&amp;publ=MSN&amp;crea=STND_MFEHPG_MSNinprod_alert_br0304_1x1" class=voidnew"><strong>Upgrade to the faster Internet Explorer 9 – FREE </strong></a></div></div></div></div></div><div class="ro"><div class="ce ce1 cel w4"><div class="cogr coj"></div></div></div><div class="ro"><div class="ce ce1 cel w4"><div class="cogr cotb" id="stgsearch"><div id="popsrchnew" class="cof"><div class="co4b5 co coa2 coc1 alist1"><h2 class="h2 cf"><span>POPULAR SEARCHES</span></h2><div id="pop1_hops" class="br br1"><div style="clear:both;" class="hlcp2 cf"><ul><li class="sec last"><div style="float:left;width:128px"><a href="httpdisabled://specials.msn.com/A-List/Lifestyle/Weirdest-mug-shots.aspx?cp-documentid=27403042 "><img class="landscape" src="../col.stb.s-msn.com/i/FF/6B3EB94D554DA0488C66DC31482D48.jpg" title="Image: Booking photo of Michelle Allen (Courtesy of Middletown Police Dept.)" width="128" height="73" alt="Image: Booking photo of Michelle Allen (Courtesy of Middletown Police Dept.)" /></a></div><div style="margin-left:128px; padding-left: 10px"><a href="httpdisabled://specials.msn.com/A-List/Lifestyle/Weirdest-mug-shots.aspx?cp-documentid=27403042 "><strong>Weirdest mug shots</strong></a><div class="richtext"><p>While their acts got them in trouble, some suspects' <a href="httpdisabled://specials.msn.com/A-List/Lifestyle/Weirdest-mug-shots.aspx?cp-documentid=27403042 ">arrest photos</a> were quite odd.</p></div></div></li></ul></div></div><div class="hr"></div><div id="pop2_hops" class="br br2"><h3 class="h3 cf">On Bing</h3><div style="clear:both;" class="hlcp1 hlcp3 cf"><ul><li class="ter"><a href="httpdisabled://www.bing.com/search?q=Peyton+Manning+twins&amp;form=msnhpm">Peyton Manning</a><span class="piped"> | <a href="httpdisabled://www.bing.com/search?q=christie+brinkley&amp;form=msnwis">Christie Brinkley</a> | <a href="httpdisabled://www.bing.com/search?q=sarah+palin+shutdown&amp;form=msnhpm">Sarah Palin</a></span></li><li class="ter"><a href="httpdisabled://www.bing.com/search?q=tiger+woods+video+game&amp;form=msnhpm">Tiger Woods</a><span class="piped"> | <a href="httpdisabled://www.bing.com/search?q=kelly+clarkson+ellen&amp;form=msnhpm">Kelly Clarkson</a> | <a href="httpdisabled://www.bing.com/search?q=hanna+movie&amp;form=msnhpm">'Hanna'</a></span></li><li class="ter"><a href="httpdisabled://www.bing.com/search?q=carrie+underwood&amp;form=msnwis">Carrie Underwood</a><span class="piped"> | <a href="httpdisabled://www.bing.com/news/search?q=greta+gerwig&amp;FORM=msnhpm">Greta Gerwig</a> | <a href="httpdisabled://www.bing.com/search?q=gabrielle+giffords+shuttle&amp;form=msnhpm">Gabrielle Giffords</a></span></li><li class="ter"><a href="httpdisabled://www.bing.com/search?q=blue+bird+school+buses+fire+risk&amp;form=msnhpm">School bus recall</a><span class="piped"> | <a href="httpdisabled://www.bing.com/search?q=john+boehner&amp;form=msnwis">John Boehner</a> | <a href="httpdisabled://www.bing.com/search?q=patty+duke+social+security&amp;FORM=msnhpm">Patty Duke</a></span></li><li class="ter"><a href="httpdisabled://www.bing.com/news/search?q=planned+parenthood&amp;FORM=msnhpm">Planned Parenthood</a><span class="piped"> | <a href="httpdisabled://www.bing.com/search?q=johnson+%26+johnson+settles&amp;form=msnhpm">Johnson &amp; Johnson</a> | <a href="httpdisabled://www.bing.com/search?q=military+pay&amp;form=msnhpm">Military pay</a></span></li></ul></div></div><div class="hr"></div><br class="b3" /><div id="pop3_hops" class="br br3"><h3 class="h3 cf">Popular Pages</h3><div style="clear:both;" class="hlcp1 hlcp3 cf"><ul><li class="ter"><a href="httpdisabled://specials.msn.com/A-List/Movies/Natalie-Portman-on-Black-Swan-controversy.aspx?cp-documentid=28301675">Portman on 'Swan' dispute</a><span class="piped"> | <a href="httpdisabled://specials.msn.com/A-List/TV/Bill-Cosby-slams-Trump.aspx?cp-documentid=28292430">Bill Cosby slams Trump</a></span></li><li class="ter"><a href="httpdisabled://specials.msn.com/A-List/Lifestyle/Commodore-64-is-back.aspx?cp-documentid=28291495">A new Commodore 64?</a><span class="piped"> | <a href="httpdisabled://specials.msn.com/A-List/sports/Big-Ben-to-wed.aspx?cp-documentid=28299660">'Big Ben' confirms wedding</a></span></li><li class="ter"><a href="httpdisabled://specials.msn.com/A-List/Entertainment/Iggy-Pop-stuns-Idol-audience.aspx?cp-documentid=28302289">Iggy Pop performs on 'Idol'</a><span class="piped"> | <a href="httpdisabled://specials.msn.com/A-List/Lifestyle/Surprise-plea-in-Dugard-case.aspx?cp-documentid=28292186">Plea in Dugard case</a></span></li><li class="ter"><a href="httpdisabled://specials.msn.com/A-List/Movies/Hailee-Steinfeld-in-Romeo-and-Juliet.aspx?cp-documentid=28299589">Hailee Steinfeld in talks for new role</a><span class="piped"> | <a href="httpdisabled://specials.msn.com/A-List/TV/A-Glee-spin-off.aspx?cp-documentid=28301295">'Glee' spin-off?</a></span></li></ul></div></div><div class="hr"></div><div id="pop4_hops" class="br br4 brl"><h3 class="h3 cf">In Case You Missed It</h3><div style="clear:both;" class="hlcp1 hlcp3 cf"><ul><li class="ter"><a href="httpdisabled://specials.msn.com/A-List/TV/Eva-Longoria-has-on-air-wardrobe-malfunction.aspx?cp-documentid=28287950">Longoria's shirt void </a><span class="piped"> | <a href="httpdisabled://specials.msn.com/A-List/sports/LeBron-James-mom-arrested.aspx?cp-documentid=28287065">LeBron's mom arrested</a></span></li><li class="ter"><a href="httpdisabled://specials.msn.com/A-List/Lifestyle/Mariska-Hargitay-adopts.aspx?cp-documentid=28291248">Mariska Hargitay adopts baby</a><span class="piped"> | <a href="httpdisabled://specials.msn.com/A-List/Health/The-17-day-diet-goes-viral.aspx?cp-documentid=28289344">17 Day Diet goes viral</a></span></li></ul></div></div><div class="hr"></div></div></div></div></div></div><div class="ro" id="apps"><div class="ce ce1 cel w4"><div id="hotmail" class="co1b1 co coa2 coc1 hotmail1"><h2 class="h2 cf"><span>HOTMAIL</span></h2><div id="htup" class="br br1"><div class="hminbox1" ><noscript><p>This module requires scripting to be enabled in your browser.</p></noscript><div class="actions"><a href="httpdisabledsdisabled://login.live.com/login.srf?wa=wsignin1.0&amp;rpsnv=11&amp;ct=1302299259&amp;rver=5.5.4177.0&amp;wp=MBI&amp;wreply=http:%2F%2Fwww.msn.com%2F&amp;lc=1033&amp;id=1184" class="dMSNME_1">Sign in</a></div></div></div></div><div class="cogr cotb cogrst" id="socialtg"><div id="windowslive"><div class="co1b1 co coa2 coc1 activitiesmodule1"><h2 class="h2 cf"><span>MESSENGER</span></h2><div class="br br1"><div class="actfeed1 windowslive"><noscript><p>This module needs JavaScript to be enabled on your browser</p></noscript><div class="ac-head ac-error cf none"><div class="ac-errortext">Unable to show activities. <a href="index.html#" class="ac-refreshlink">Please try again.</a></div><div class="ac-signout"><a class="ac-link dMSNME_1" href="httpdisabledsdisabled://login.live.com/login.srf?wa=wsignin1.0&amp;rpsnv=11&amp;ct=1302299259&amp;rver=5.5.4177.0&amp;wp=MBI&amp;wreply=http:%2F%2Fwww.msn.com%2F&amp;lc=1033&amp;id=1184" rel="False">Sign out</a></div></div><div class="ac-head ac-signin none cf"><div class="ac-upsell">Sign in to see your social updates.</div><div class="ac-signinlink"><a class="ac-link wlsignin dMSNME_1" href="httpdisabledsdisabled://login.live.com/login.srf?wa=wsignin1.0&amp;rpsnv=11&amp;ct=1302299259&amp;rver=5.5.4177.0&amp;wp=MBI&amp;wreply=http:%2F%2Fwww.msn.com%2F&amp;lc=1033&amp;id=1184" rel="False">Sign in</a></div></div></div><div class="ac-loaddisabled none ac-updatestatus">Please wait.<br />This may take a few seconds</div><div class="ac-error none ac-head ac-errortext">Unable to show activities. Please try again later.</div></div></div></div><div id="facebook" class="cof"><div class="co1b1 co coa2 coc1 facebook1"><h2 class="h2 cf"><span>FACEBOOK</span></h2><div class="br br1"><div class="actfeed1 facebook"><noscript><p>This module needs JavaScript to be enabled on your browser</p></noscript><div class="ac-head ac-error cf none"><div class="ac-errortext">Unable to show activities. <a href="index.html#" class="ac-refreshlink">Please try again.</a></div><div class="ac-signout"><a class="ac-link ac-signinoutpopup" href="httpdisabled://www.msn.com/scp/AuthServiceFacebookLogOff.aspx?redirectTo=0&amp;mkt=en-us&amp;format=Homepage">Logout</a></div></div><div class="ac-head ac-signin ac-upsellfb none cf"><div class="ac-upsell">Login to see your News Feed.</div><div class="ac-signinlink fbsignin"><a class="ac-link ac-signinoutpopup" rel="facebook_480_440" href="httpdisabled://www.msn.com/scp/AuthServiceFacebook.aspx?redirectTo=0&amp;mkt=en-us&amp;format=Homepage"><span>Login</span></a></div></div></div><div class="ac-loaddisabled none ac-updatestatus">Please wait.<br />This may take a few seconds</div><div class="ac-error none ac-head ac-errortext">Unable to show activities. Please try again later.</div></div></div></div><div id="twitter"><div class="co1b1 co coa2 coc1 twitter1"><h2 class="h2 cf"><span>TWITTER</span></h2><div class="br br1"><div class="actfeed1 twitter"><noscript><p>This module needs JavaScript to be enabled on your browser</p></noscript><div class="ac-head ac-error cf none"><div class="ac-errortext">Unable to show activities. <a href="index.html#" class="ac-refreshlink">Please try again.</a></div><div class="ac-signout"><a class="ac-link ac-signinoutajax" href="httpdisabled://www.msn.com/scp/AuthServiceTwitterLogOff.aspx?redirectTo=0&amp;mkt=en-us&amp;format=Homepage">Sign out</a></div></div><div class="ac-head ac-signin none cf"><div class="ac-upsell">Want to see your Tweets?</div><div class="ac-signinlink"><a class="ac-link ac-signinoutpopup twsignin" rel="twitter_800_390" href="httpdisabled://www.msn.com/scp/AuthServiceTwitter.aspx?redirectTo=0&amp;mkt=en-us&amp;format=Homepage">Sign in</a></div></div></div><div class="ac-loaddisabled none ac-updatestatus">Please wait.<br />This may take a few seconds</div><div class="ac-error none ac-head ac-errortext">Unable to show activities. Please try again later.</div></div></div></div></div></div></div><div class="ro"><div class="ce ce1 cel w4"><div class="cogr cotb cogrsx coj" id="gendermodule"><div id="forher" class="cof"><div class="co1b1 co coa2 coc1 m3 hlcpm1"><h2 class="h2 cf"><span>FOR HER</span></h2><div class="br br1"><div style="clear:both;" class="hlcp1 cf"><ul><li class="sec last"><div style="float:right;width:128px"><a href="httpdisabled://bodyodd.msnbc.msn.com/_news/2011/04/05/6413263-pop-songs-reflect-our-me-me-me-attitudes-study-says?gt1=43001"><img class="landscape" src="../col.stb.s-msn.com/i/38/FAF3346E94CF4579ECAB641703868.jpg" title="Image: Rihanna &amp; Kanye West (© Lucy Nicholson/Reuters)" width="128" height="73" alt="Image: Rihanna &amp; Kanye West (© Lucy Nicholson/Reuters)" /></a></div><div style="margin-right:128px; padding-right: 10px"><a href="httpdisabled://bodyodd.msnbc.msn.com/_news/2011/04/05/6413263-pop-songs-reflect-our-me-me-me-attitudes-study-says?gt1=43001"><strong>Study: Pop songs all about 'me me me'</strong></a><div class="richtext"><p>Music seems a lot more narcissistic than days gone by. <a href="httpdisabled://bodyodd.msnbc.msn.com/_news/2011/04/05/6413263-pop-songs-reflect-our-me-me-me-attitudes-study-says?gt1=43001">Do you agree</a>?</p></div></div></li><li class="ter"><a href="httpdisabled://www.theroot.com/views/good-sports-encouraging-black-female-athletes?auto=true&amp;gt1=38002">Black female athletes are good role models for girls</a></li><li class="ter media"><a href="httpdisabled://www.msnbc.msn.com/id/21134540/vp/42485020#42485020?from=en-us_msnhp&amp;gt1=43001">Woman accused of shutting down country’s Internet</a></li><li class="ter"><a href="httpdisabled://www.bing.com/search?q=Recommended+Self-help+Books&amp;form=msnspe&amp;gt1=36010">Bing: Recommended self-help books</a></li><li class="ter"><a href="httpdisabled://www.bing.com/search?q=Family+Camping+Checklist&amp;form=msnspe&amp;gt1=36010">Search: Family camping checklist</a></li><li class="ter"><a href="httpdisabled://www.bing.com/browse?g=cat_breeds&amp;form=msnspe&amp;gt1=36010#toc=3">Find: Gallery of easy-going cat breeds</a></li></ul></div></div></div></div><div id="forhim"><div class="co1b1 co coa2 coc1 m3 hlcpm1"><h2 class="h2 cf"><span>FOR HIM</span></h2><div class="br br1"><div style="clear:both;" class="hlcp1 cf"><ul><li class="sec last"><div style="float:right;width:128px"><a href="httpdisabled://money.msn.com/auto-insurance/article.aspx?post=91a93e53-2c02-426a-905a-3c2a175abc11&amp;GT1=33033"><img class="landscape" src="../col.stb.s-msn.com/i/2F/9EFAECEC174B21FB83D10C82522D2.jpg" title="Image: Couple in car (© Caroline von Tuempling/Getty Images)" width="128" height="73" alt="Image: Couple in car (© Caroline von Tuempling/Getty Images)" /></a></div><div style="margin-right:128px; padding-right: 10px"><a href="httpdisabled://money.msn.com/auto-insurance/article.aspx?post=91a93e53-2c02-426a-905a-3c2a175abc11&amp;GT1=33033"><strong>Date a safe driver, save on insurance</strong></a><div class="richtext"><p>Relationship benefits: Love, companionship &amp; ... extra cash? Find out <a href="httpdisabled://money.msn.com/auto-insurance/article.aspx?post=91a93e53-2c02-426a-905a-3c2a175abc11&amp;GT1=33033">how it works</a>.</p></div></div></li><li class="ter"><a href="httpdisabled://digitallife.today.com/_news/2011/04/06/6421524-10-ridiculous-products-to-outsmart-thieves/?gt1=43001">10 ridiculous products to outsmart thieves</a></li><li class="ter"><a href="httpdisabled://realestate.msn.com/blogs/listedblogpost.aspx?post=ed79c32b-2ecc-4cb7-94c9-bfca06f7727c&amp;GT1=35010">Make some cash, turn your home into a giant ad</a></li><li class="ter"><a href="httpdisabled://www.bing.com/browse?g=hdtvs&amp;form=msnspe&amp;gt1=36010#toc=5">Bing: Gallery of top 50- to 52-inch HDTVs</a></li><li class="ter"><a href="httpdisabled://www.bing.com/search?q=Unusual+Sports&amp;form=msnspe&amp;gt1=36010">Find: Most unusual sports around the world</a></li><li class="ter"><a href="httpdisabled://www.bing.com/search?q=best+chefs+in+america&amp;go=&amp;form=msnspe&amp;gt1=36010">Search: Best chefs in America</a></li></ul></div></div></div></div></div></div></div><div class="ro"><div class="ce ce1 cel w4"><div class="cogr coj"></div></div></div><div class="ro"><div class="ce ce1 cel w4"><div class="co1b1 co coa1 coc1 m3 ad1"><div class="br br1"><div class="advertisement" style="width:300px"><div id="publicis_ad"><script type="text/javascript">$.dap("&amp;PG=MSNHP4&amp;AP=1455",300,120,"publicis_ad");</script></div><div class="adfb cf right"><a href="httpdisabled://g.msn.com/AIPRIV/en-us" class="adch"><img src="../col.stc.s-msn.com/br/sc/i/icons/adchoices_gif.gif" alt="Ad Choice" title="Ad Choice" height="12" width="68" /></a></div></div></div></div></div></div><div class="ro"><div class="ce ce1 cel w4"><div class="co1b1 co coa1 coc1 m3 ad1"><div class="br br1"><div class="advertisement"><div id="dap_survey"><script type="text/javascript">$.dap("&amp;PG=MSNSUR&amp;AP=1089",1,1,"dap_survey");</script></div></div></div></div></div></div></div><div id="area3" class="none"></div><div id="subfoot"><div class="co1b1 co coa1 coc1 m3 searchbar2"><div class="br br1 expfoot expsrch"><div class="websearch2"><h2>Bing Search</h2><form action="httpdisabled://www.bing.com/search" method="get" id="footersrchfrm"><div class="search cf"><span class="bo"><span class="bi"><label class="hide" for="qf">Search:</label><input id="qf" type="text" class="text" name="q" size="69" maxlength="250" accesskey="S" /><input type="image" class="image" value="Search" alt="Search" title="Search" style="height:33px;width:173px;" src="../col.stc.s-msn.com/br/sc/i/icons/BING_websearch_2.jpg" /><input type="hidden" name="form" value="MSNH14" /></span></span></div><div class="opt"></div></form></div></div></div></div></div></div><div id="foot"><div class="co1b1 co coa1 coc1 m3 "><div class="br br1"><div class="msnfoot1 cf"><ul class="primary"><li class="first"><a href="httpdisabled://go.microsoft.com/fwlink/?LinkId=74170">MSN Privacy</a></li><li><a href="httpdisabled://g.msn.com/0TO_/enus">Legal</a></li><li><a href="httpdisabled://advertising.microsoft.com/home/home">Advertise</a></li><li class="last"><a href="httpdisabled://www.msn.com/worldwide.aspx">MSN Worldwide</a></li></ul><ul class="secondary"><li class="first"><a href="httpdisabled://rss.msn.com/default.aspx">RSS</a></li><li><a href="httpdisabledsdisabled://careers.microsoft.com/">Jobs</a></li><li><a href="httpdisabled://moneycentral.msn.com/inc/Attributions.asp">Data Providers</a></li><li><a class=voidnew" href="httpdisabledsdisabled://secure.opinionlab.com/ccc01/o.asp?ID=WpkpVtTB">Feedback</a></li><li><a href="httpdisabled://onlinehelp.microsoft.com/en-us/msn/thebasics.aspx">Help</a></li><li class="last"><a href="httpdisabled://g.msn.com/AIPRIV/en-us">About our ads</a></li></ul><div class="copyright"><span>© 2011 Microsoft</span></div></div></div></div></div></div><script type="text/javascript">/*<![CDATA[*/(function(a){var d="body",b="scrollBind";a(window).unbind("scroll",a.track.trackEvent);function e(){a.async("asyncCanary",function(){var d="#to_inbox";c.sm("start");a("#head").scrollHead({withNav:false,scrollHeadFormCode:"MSNL14",animate:1,sSpeed:400,bannerClassName:"generic1"});a("#q").uberFocus();c.sm("sb_uf");a("#infopane_hops a, #todays_picks a").heroPlayer({widgetParams:{flashvars:{configName:"divoverlayplayer",configCsid:"msnvideo"}}});a("div.menunavbar1").menuNavBar({animate:1,fromOpacity:.3});try{a("#addtostart, .addtostart a").async("addToStartMenu",[{LinkText:"Add MSN to Start menu"}])}catch(g){}a("#hp_spotlight_notebook,#hp_spotlight_desktop,#hp_spotlight,#cp_spotlight_desktop,#cp_spotlight_notebook").async("contentRotation",[{dataUrl:"/ajax/hpcStub.aspx",innerSel:".imglinkabslist1"}]);a.cobrandmoduletracking();a("div.cogr.coss").slideshow({delay:7e3,hpad:6,animate:1});a("div.cogr.cotb").scrollBind("tabGroup",[{hover:{delay:300},animate:1}]);a("div.headerbar_us a#mkhm").setHomepage({txt:"Make MSN my homepage"});a("#cobrandeula").async("cobrandeula");a("div.menubar2").menuBar({delay:void:500,close:50}});a("#srchfrm").scrollBind("bindDualSearch",[{scope2:{altImageSrc:"icons/BING_MSN_search_2.jpg",altImageWidth:175,altImageHeight:33},scope3:{altImageSrc:"icons/BING_image_search_2.jpg",altImageWidth:185,altImageHeight:33},scope4:{altImageSrc:"icons/BING_video_search_2.jpg",altImageWidth:182,altImageHeight:33},scope5:{altImageSrc:"icons/BING_news_search_2.jpg",altImageWidth:181,altImageHeight:33},scope6:{altImageSrc:"icons/BING_map_search_2.jpg",altImageWidth:171,altImageHeight:33}}]);a(".websearch2 form").scrollBind("bindSearch");var e="www";switch(a.track.trackInfo.sitePage.siteGroupId){case "DELL":e="dell";break;case "QWEST":e="qwest";break;case "VERIZON":e="verizon";break;case "MSNMEMBER":e="msnmember";break;case "ASUS":case "LENOVO":case "ACER":case "COMPAQ-DESKTOP":case "COMPAQ-NOTEBOOK":case "GATEWAY":case "EMACHINES":case "PACKARDBELL":case "SAMSUNG":case "TOSHIBA":case "MSI":case "MEDION":case "SONY":case "HP-COMM":case "HP-NOTEBOOK":case "HP-DESKTOP":e="us"}a("#stk_heading div").css("margin-bottom","0");a("#marchmadness .br3").async(b,["MarchMadness",[{params:{source:"httpdisabled://col.stc.s-msn.com/br/sc/xap/03182011_1/MarchMadness.xap",initparams:{targetname:"_blank"}}}]]);a.extend(true,{hotmail:{defaults:{proxyurl:"httpdisabled://hotmailproxy.{0}.msn.com/pm/v1.0/getheaders.aspx".format(e)}}});a("div.hminbox1").scrollBind("previewInbox",[{tsrttextOn:0,signinMsg:"Windows Live Hotmail: E-mail made simple. Fight spam with Microsoft SmartScreen technology."}]);a("div.headerbar_us").scrollBind("previewlivescorecard",[{hotmail:d,hmtemplate:"({0})"}]);a.autoSuggest({resources:{js:"httpdisabled://www.bing.com/s/as/863238/en.js"},inputId:"q",delayBind:0,sharedCk:{cn:"_SS",ru:"httpdisabled://www.msn.com/sck.aspx",delay:2},config:{nw:"true",u:" http://api.bing.com/qsonhs.aspx?form=MSN005",mkt:"en-US",tPN:"Popular Now",eHS:1,ePN:1,f:"srchfrm",lh:"httpdisabled://onlinehelp.microsoft.com/en-us/bing/ff808490.aspx",lmh:"httpdisabled://www.bing.com/profile/history"}});c.sm("sb_as");setTimeout(function(){a("#q").trigger("focus");c.sm("sb_f")},0);a(d).scrollBind(voidPopup",[{features:"width=1224,height=768,menubar=1,scrollbars=1,resizable=1,top=0px,left=1,location=1,toolbar=yes,directories=yes"}]);a("#idlblsearch,#txtZipCode, #sc-TWITTERstatus,#idQuickSearch,#shloc,#s_rawwords,#where1").async(b,["bindHinting",[{asyncp:1}]]);a("#srchfrm, #footersrchfrm")voidNew();a(window).loaddisabled(a.dap.run);setTimeout(f,1e3);c.sm("end")},a.jsUrl)}function f(){var f="httpdisabled://local.msn.com/sports.aspx?zip={0}&q={0}",e="httpdisabled://local.msn.com/movies-events.aspx?zip={0}&q={0}",c="ajax/spseventsstub.aspx",d="socialActivityList";a("div.pgopt1").async(b,["pageOptions",[{delay:void:100,close:100}}]]);a("div.pgopt1 #locsrch").async(b,["srchfrm"]);a("div.facebook").async(b,[d,[{serviceName:"facebook",tabSet:"wft"}]]);a("div.twitter").async(b,[d,[{serviceName:"twitter",tabSet:"wft"}]]);a("div.windowslive").async(b,[d,[{serviceName:"windowslive",tabSet:"wft",PassportReturnUrl:"httpdisabled://www.msn.com"}]]);a("#newsid").async(b,["localmodule",[{disambiguationBaseUrl:"ajax/wealocdisambiguatorstub.aspx?weasearchstr="}]]);a("div.weather1").async("weatherForecast",[{weaBaseUrl:"ajax/weadatastub2.aspx?wealocations={0}&weadegreetype={1}",spsUrl:c}]);a("div.locnews1").async(b,["localnews",[{feedUrl:"ajax/localnews.aspx?NewsProviderId=",providerUrl:"ajax/lssproxy.aspx?keyword=",localNewsUrl:" http://local.msn.com/news.aspx?zip={0}&q={0}",spsUrl:c}]]);a("div.locevents1").async(b,["localevents",[{eventsStubURL:"ajax/loceventsstub.aspx?zipcode=",evenstURL:e,spsUrl:c}]]);a("div.locsports1").async(b,["localsports",[{sportsStubURL:"ajax/localsports.aspx?SportsArea=",sportsURL:f,spsUrl:c,ldsStubURL:"ajax/localdata.aspx?zip="}]]);a("div.lmlsf1").async(b,["localMoreLinksSimpleForm",[{link1Text:"News",link2Text:"Sports",link3Text:"Events",link1Url:"httpdisabled://local.msn.com/news.aspx?zip={0}&q={0}",link2Url:f,link3Url:e}]]);a("voidnew").async(b,[voidNew",[{asyncp:1}]]);a("#foot .secondary a").eq(4)voidNew();a("#bing_dsp a")voidNew();a("#dhp1").dhpPromo();a(".exphd .wlcard1 ul li.last").before("<li class='tolatino'><a href='http://latino.msn.com'>Latino</a></li>");a("#srchfrm, #footersrchfrm, .wlcard1 li:eq(2) a").scrollBind("appendQueryParam",[{pc:/^[A-Za-z0-9]{1,10}$/g}])}var c={sm:function(){}};if(location.href.match(/[?&]ll=0(&|$)/i))c.sm=function(b){a.fireAndForget("httpdisabled://col.stj.s-msn.com/br/sc/i/f8/614595fba50d96389708a4135776e4.gif?loc="+b+"&ts="+(new Date).getTime())};c.sm("ab1");a(window).loaddisabled(function(){c.sm("onloaddisabled");setTimeout(function(){c.sm("ab2")},2e3)});a("#makebing").click(function(a){try{window.external.AddSearchProvider("httpdisabled://www.bing.com/s/osd3.xml");a.preventDefault()}catch(b){}});a(d).bind("LLError",function(c,e){a(d).unbind("LLError");var b="LL Error ";switch(e){case 1:b+="Ajax";break;case 2:b+="Parse";break;case 3:b+="Timeout";break;case 4:b+="Content";break;default:b+="Undefined"}a.track.trackEvent(c,0,0,b)});a("#q").attr("autocomplete","off");a(function(){c.sm("dom_r");e()});c.sm("cbc_end");var g="mh".getCookie();if(g=="ASUS"&&/^[^?#]*www\.msn\.com[/?#]/.test(location)){"mh".setCookie("MSFT",30,".msn.com","/");a(d).bind("mhreset",a.track.trackEvent).trigger("mhreset")}})(jQuery)//]]></script><script type="text/javascript">/*<![CDATA[*/(function(a){var c="domcomp",b="cbcomp";if(a.track&&a.track.trackEvent){a(document).bind(c,a.track.trackEvent).trigger(c);a(function(){a.async("asyncCanary",function(){a(document).bind(b,a.track.trackEvent).trigger(b)})})}})(jQuery);//]]></script></body></html>
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/a/1302214109/images/loader.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/a/1302214109/images/loader.gif
new file mode 100755
index 0000000000..31220d2672
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/a/1302214109/images/loader.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/a/1302214109/images/twitter_logo_header.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/a/1302214109/images/twitter_logo_header.png
new file mode 100755
index 0000000000..f681f040ef
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/a/1302214109/images/twitter_logo_header.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/a/1302214109/javascripts/lib/jquery.tipsy.min.js@1302114648 b/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/a/1302214109/javascripts/lib/jquery.tipsy.min.js@1302114648
new file mode 100755
index 0000000000..2ff318143b
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/a/1302214109/javascripts/lib/jquery.tipsy.min.js@1302114648
@@ -0,0 +1,3 @@
+//Licensed under The MIT License
+//Copyright (c) 2008 Jason Frame (jason@onehackoranother.com)
+(function($){$.fn.tipsy=function(g){g=$.extend({fade:false,gravity:'n'},g||{});if(!g['offsetTop']){g['offsetTop']=0}if(!g['offsetLeft']){g['offsetLeft']=0}if(!g['header']){g['header']=''}if(!g['footer']){g['footer']=''}if(!g['hideTimeout']){g['hideTimeout']=100}if(!g['showTimeout']){g['hideTimeout']=0}if(!g['additionalCSSClass']){g['additionalCSSClass']=''}var h=false;var i=null,cancelHide=false;this.hover(function(){var a=$(this).text();var b=g['header'].replace('%{link}',a);var c=g['footer'].replace('%{link}',a);$.data(this,'cancel.tipsy',true);var d=$.data(this,'active.tipsy');if(!d){$('.tipsy').hide();d=$('<div class="tipsy '+g['additionalCSSClass']+'"><div class="tipsy-inner">'+b+$(this).attr('title')+c+'</div></div>');d.css({position:'absolute',zIndex:100000});$(this).attr('title','');$.data(this,'active.tipsy',d)}else if($(this).attr('title')!=''){d.find('.tipsy-inner').html($(this).attr('title'));$(this).attr('title','')}var e=$.extend({},$(this).offset(),{width:this.offsetWidth,height:this.offsetHeight});e.top=e.top+g['offsetTop'];e.left=e.left+g['offsetLeft'];$('.tipsy').hide();d.remove().css({top:0,left:0,visibility:'hidden',display:'block'}).appendTo(document.body);var f=d[0].offsetWidth,actualHeight=d[0].offsetHeight;switch(g.gravity.charAt(0)){case'n':d.css({top:e.top+e.height,left:e.left+e.width/2-f/2}).addClass('tipsy-north');break;case'l':d.css({top:e.top+e.height,left:e.left+e.width/2-18}).addClass('tipsy-north');break;case's':d.css({top:e.top-actualHeight,left:e.left+e.width/2-f/2}).addClass('tipsy-south');break;case'e':d.css({top:e.top+e.height/2-actualHeight/2,left:e.left-f}).addClass('tipsy-east');break;case'w':d.css({top:e.top+e.height/2-actualHeight/2,left:e.left+e.width}).addClass('tipsy-west');break}function show(){if(g.fade){d.css({opacity:0,display:'block',visibility:'visible'}).animate({opacity:1})}else{d.css({visibility:'visible'})}}if(g['showTimeout']){h=setTimeout(show,g['showTimeout'])}else{show()}},function(){clearTimeout(h);$.data(this,'cancel.tipsy',false);var b=this;setTimeout(function(){if($.data(this,'cancel.tipsy'))return;var a=$.data(b,'active.tipsy');if(g.fade){a.stop().fadeOut(function(){$(this).remove()})}else{a.remove()}},g['hideTimeout'])})}})(jQuery);
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/a/1302214109/javascripts/lib/mustache.js@1302114648 b/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/a/1302214109/javascripts/lib/mustache.js@1302114648
new file mode 100755
index 0000000000..7072caa30e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/a/1302214109/javascripts/lib/mustache.js@1302114648
@@ -0,0 +1,403 @@
+/*
+ mustache.js — Logic-less templates in JavaScript
+
+ See http://mustache.github.com/ for more info.
+*/
+
+var Mustache = function() {
+ var Renderer = function() {};
+
+ Renderer.prototype = {
+ otag: "{{",
+ ctag: "}}",
+ pragmas: {},
+ buffer: [],
+ pragmas_implemented: {
+ "IMPLICIT-ITERATOR": true,
+ "TRANSLATION-HINT": true
+ },
+ context: {},
+
+ render: function(template, context, partials, in_recursion) {
+ // reset buffer & set context
+ if(!in_recursion) {
+ this.context = context;
+ this.buffer = []; // TODO: make this non-lazy
+ }
+
+ // fail fast
+ if(!this.includes("", template)) {
+ if(in_recursion) {
+ return template;
+ } else {
+ this.send(template);
+ return;
+ }
+ }
+
+ // Branching or moving down the partial stack, save any translation mode info.
+ if (this.pragmas['TRANSLATION-HINT']) {
+ context['_TRANSLATION-HINT_mode'] = this.pragmas['TRANSLATION-HINT'].mode;
+ }
+
+ // get the pragmas together
+ template = this.render_pragmas(template);
+
+ // handle all translations
+ template = this.render_i18n(template, context, partials);
+
+ // render the template
+ var html = this.render_section(template, context, partials);
+
+ // render_section did not find any sections, we still need to render the tags
+ if (html === false) {
+ html = this.render_tags(template, context, partials, in_recursion);
+ }
+
+ if (in_recursion) {
+ return html;
+ } else {
+ this.sendLines(html);
+ }
+ },
+
+ /*
+ Sends parsed lines
+ */
+ send: function(line) {
+ if(line != "") {
+ this.buffer.push(line);
+ }
+ },
+
+ sendLines: function(text) {
+ if (text) {
+ var lines = text.split("\n");
+ for (var i = 0; i < lines.length; i++) {
+ this.send(lines[i]);
+ }
+ }
+ },
+
+ /*
+ Looks for %PRAGMAS
+ */
+ render_pragmas: function(template) {
+ // no pragmas
+ if(!this.includes("%", template)) {
+ return template;
+ }
+
+ var that = this;
+ var regex = new RegExp(this.otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" +
+ this.ctag);
+ return template.replace(regex, function(match, pragma, options) {
+ if(!that.pragmas_implemented[pragma]) {
+ throw({message:
+ "This implementation of mustache doesn't understand the '" +
+ pragma + "' pragma"});
+ }
+ that.pragmas[pragma] = {};
+ if(options) {
+ var opts = options.split("=");
+ that.pragmas[pragma][opts[0]] = opts[1];
+ }
+ return "";
+ // ignore unknown pragmas silently
+ });
+ },
+
+ /*
+ Tries to find a partial in the curent scope and render it
+ */
+ render_partial: function(name, context, partials) {
+ name = this.trim(name);
+ if(!partials || partials[name] === undefined) {
+ throw({message: "unknown_partial '" + name + "'"});
+ }
+ if(typeof(context[name]) != "object") {
+ return this.render(partials[name], context, partials, true);
+ }
+ return this.render(partials[name], context[name], partials, true);
+ },
+
+ render_i18n: function(html, context, partials) {
+ if (html.indexOf(this.otag + "_i") == -1) {
+ return html;
+ }
+ var that = this;
+ var regex = new RegExp(this.otag + "\\_i" + this.ctag +
+ "\\s*([\\s\\S]+?)" + this.otag + "\\/i" + this.ctag, "mg");
+
+ // for each {{_i}}{{/i}} section do...
+ return html.replace(regex, function(match, content) {
+ var translationMode;
+
+ if (that.pragmas && that.pragmas["TRANSLATION-HINT"] && that.pragmas["TRANSLATION-HINT"].mode) {
+ translationMode = that.pragmas["TRANSLATION-HINT"].mode;
+ } else if (context['_TRANSLATION-HINT_mode']) {
+ translationMode = context['_TRANSLATION-HINT_mode'];
+ }
+
+ var params = content;
+
+ if (translationMode) {
+ params = {
+ text: content,
+ mode: translationMode
+ };
+ }
+
+ return _(params);
+ });
+ },
+
+ /*
+ Renders inverted (^) and normal (#) sections
+ */
+ render_section: function(template, context, partials) {
+ if(!this.includes("#", template) && !this.includes("^", template)) {
+ // did not render anything, there were no sections
+ return false;
+ }
+
+ var that = this;
+
+ // This regex matches _the first_ section ({{#foo}}{{/foo}}), and captures the remainder
+ var regex = new RegExp(
+ "^([\\s\\S]*?)" + // all the crap at the beginning that is not {{*}} ($1)
+
+ this.otag + // {{
+ "(\\^|\\#)\\s*(.+)\\s*" + // #foo (# == $2, foo == $3)
+ this.ctag + // }}
+
+ "\n*([\\s\\S]*?)" + // between the tag ($2). leading newlines are dropped
+
+ this.otag + // {{
+ "\\/\\s*\\3\\s*" + // /foo (backreference to voiding tag).
+ this.ctag + // }}
+
+ "\\s*([\\s\\S]*)$", // everything else in the string ($4). leading whitespace is dropped.
+
+ "g");
+
+ // for each {{#foo}}{{/foo}} section do...
+ return template.replace(regex, function(match, before, type, name, content, after) {
+ // before contains only tags, no sections
+ var renderedBefore = before ? that.render_tags(before, context, partials, true) : "",
+
+ // after may contain both sections and tags, so use full rendering function
+ renderedAfter = after ? that.render(after, context, partials, true) : "";
+
+ var value = that.find(name, context);
+ if(type == "^") { // inverted section
+ if(!value || that.is_array(value) && value.length === 0) {
+ // false or empty list, render it
+ return renderedBefore + that.render(content, context, partials, true) + renderedAfter;
+ } else {
+ return renderedBefore + "" + renderedAfter;
+ }
+ } else if(type == "#") { // normal section
+ if(that.is_array(value)) { // Enumerable, Let's loop!
+ return renderedBefore + that.map(value, function(row) {
+ return that.render(content, that.create_context(row), partials, true);
+ }).join("") + renderedAfter;
+ } else if(that.is_object(value)) { // Object, Use it as subcontext!
+ return renderedBefore + that.render(content, that.create_context(value),
+ partials, true) + renderedAfter;
+ } else if(typeof value === "function") {
+ // higher order section
+ return renderedBefore + value.call(context, content, function(text) {
+ return that.render(text, context, partials, true);
+ }) + renderedAfter;
+ } else if(value) { // boolean section
+ return renderedBefore + that.render(content, context, partials, true) + renderedAfter;
+ } else {
+ return renderedBefore + "" + renderedAfter;
+ }
+ }
+ });
+ },
+
+ /*
+ Replace {{foo}} and friends with values from our view
+ */
+ render_tags: function(template, context, partials, in_recursion) {
+ // tit for tat
+ var that = this;
+
+ var new_regex = function() {
+ return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" +
+ that.ctag + "+", "g");
+ };
+
+ var regex = new_regex();
+ var tag_replace_callback = function(match, operator, name) {
+ switch(operator) {
+ case "!": // ignore comments
+ return "";
+ case "=": // set new delimiters, rebuild the replace regexp
+ that.set_delimiters(name);
+ regex = new_regex();
+ return "";
+ case ">": // render partial
+ return that.render_partial(name, context, partials);
+ case "{": // the triple mustache is unescaped
+ return that.find(name, context);
+ default: // escape the value
+ return that.escape(that.find(name, context));
+ }
+ };
+ var lines = template.split("\n");
+ for(var i = 0; i < lines.length; i++) {
+ lines[i] = lines[i].replace(regex, tag_replace_callback, this);
+ if(!in_recursion) {
+ this.send(lines[i]);
+ }
+ }
+
+ if(in_recursion) {
+ return lines.join("\n");
+ }
+ },
+
+ set_delimiters: function(delimiters) {
+ var dels = delimiters.split(" ");
+ this.otag = this.escape_regex(dels[0]);
+ this.ctag = this.escape_regex(dels[1]);
+ },
+
+ escape_regex: function(text) {
+ // thank you Simon Willison
+ if(!arguments.callee.sRE) {
+ var specials = [
+ '/', '.', '*', '+', '?', '|',
+ '(', ')', '[', ']', '{', '}', '\\'
+ ];
+ arguments.callee.sRE = new RegExp(
+ '(\\' + specials.join('|\\') + ')', 'g'
+ );
+ }
+ return text.replace(arguments.callee.sRE, '\\$1');
+ },
+
+ /*
+ find `name` in current `context`. That is find me a value
+ from the view object
+ */
+ find: function(name, context) {
+ name = this.trim(name);
+
+ // Checks whether a value is thruthy or false or 0
+ function is_kinda_truthy(bool) {
+ return bool === false || bool === 0 || bool;
+ }
+
+ var value;
+ if(is_kinda_truthy(context[name])) {
+ value = context[name];
+ } else if(is_kinda_truthy(this.context[name])) {
+ value = this.context[name];
+ }
+
+ if(typeof value === "function") {
+ return value.apply(context);
+ }
+ if(value !== undefined) {
+ return value;
+ }
+ // silently ignore unkown variables
+ return "";
+ },
+
+ // Utility methods
+
+ /* includes tag */
+ includes: function(needle, haystack) {
+ return haystack.indexOf(this.otag + needle) != -1;
+ },
+
+ /*
+ Does away with nasty characters
+ */
+ escape: function(s) {
+ s = String(s === null ? "" : s);
+ return s.replace(/&(?!\w+;)|["'<>\\]/g, function(s) {
+ switch(s) {
+ case "&": return "&amp;";
+ case "\\": return "\\\\";
+ case '"': return '&quot;';
+ case "'": return '&#39;';
+ case "<": return "&lt;";
+ case ">": return "&gt;";
+ default: return s;
+ }
+ });
+ },
+
+ // by @langalex, support for arrays of strings
+ create_context: function(_context) {
+ if(this.is_object(_context)) {
+ return _context;
+ } else {
+ var iterator = ".";
+ if(this.pragmas["IMPLICIT-ITERATOR"]) {
+ iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator;
+ }
+ var ctx = {};
+ ctx[iterator] = _context;
+ return ctx;
+ }
+ },
+
+ is_object: function(a) {
+ return a && typeof a == "object";
+ },
+
+ is_array: function(a) {
+ return Object.prototype.toString.call(a) === '[object Array]';
+ },
+
+ /*
+ Gets rid of leading and trailing whitespace
+ */
+ trim: function(s) {
+ return s.replace(/^\s*|\s*$/g, "");
+ },
+
+ /*
+ Why, why, why? Because IE. Cry, cry cry.
+ */
+ map: function(array, fn) {
+ if (typeof array.map == "function") {
+ return array.map(fn);
+ } else {
+ var r = [];
+ var l = array.length;
+ for(var i = 0; i < l; i++) {
+ r.push(fn(array[i]));
+ }
+ return r;
+ }
+ }
+ };
+
+ return({
+ name: "mustache.js",
+ version: "0.3.1-dev-twitter",
+
+ /*
+ Turns a template and view into HTML
+ */
+ to_html: function(template, view, partials, send_fun) {
+ var renderer = new Renderer();
+ if(send_fun) {
+ renderer.send = send_fun;
+ }
+ renderer.render(template, view || {}, partials);
+ if(!send_fun) {
+ return renderer.buffer.join("\n");
+ }
+ }
+ });
+}();
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/1129087853/151aec2f-1534-4f61-9f3e-1e787cb51a8b_mini.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/1129087853/151aec2f-1534-4f61-9f3e-1e787cb51a8b_mini.png
new file mode 100755
index 0000000000..daa825d60d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/1129087853/151aec2f-1534-4f61-9f3e-1e787cb51a8b_mini.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/1139176116/5c42a320-1e91-4d89-a034-0f140d2f23ba_mini.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/1139176116/5c42a320-1e91-4d89-a034-0f140d2f23ba_mini.png
new file mode 100755
index 0000000000..f336453d3d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/1139176116/5c42a320-1e91-4d89-a034-0f140d2f23ba_mini.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/1277610502/Untitled-9_mini.jpg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/1277610502/Untitled-9_mini.jpg
new file mode 100755
index 0000000000..586f3a2ac5
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/1277610502/Untitled-9_mini.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/316019228/326994260_1117936370_0_mini.jpeg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/316019228/326994260_1117936370_0_mini.jpeg
new file mode 100755
index 0000000000..0d324633e0
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/316019228/326994260_1117936370_0_mini.jpeg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/81990615/nightexterior-1_mini.jpg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/81990615/nightexterior-1_mini.jpg
new file mode 100755
index 0000000000..808e145079
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/81990615/nightexterior-1_mini.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/959692632/13659_1215732676789_1332990286_30703899_6344768_n_mini.jpg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/959692632/13659_1215732676789_1332990286_30703899_6344768_n_mini.jpg
new file mode 100755
index 0000000000..dc33e0af27
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/profile_images/959692632/13659_1215732676789_1332990286_30703899_6344768_n_mini.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/sticky/default_profile_images/default_profile_4_mini.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/sticky/default_profile_images/default_profile_4_mini.png
new file mode 100755
index 0000000000..f38de25787
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a0.twimg.com/sticky/default_profile_images/default_profile_4_mini.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/favicon.ico b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/favicon.ico
new file mode 100755
index 0000000000..00450d4fe3
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/favicon.ico
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/icon_lock.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/icon_lock.gif
new file mode 100755
index 0000000000..53e6641408
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/icon_lock.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/reject_small.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/reject_small.gif
new file mode 100755
index 0000000000..d346a0da48
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/reject_small.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/spinner.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/spinner.gif
new file mode 100755
index 0000000000..6e5bace6e6
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/spinner.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/sprite-icons.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/sprite-icons.png
new file mode 100755
index 0000000000..a93cede946
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/sprite-icons.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/toggle_down_dark.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/toggle_down_dark.png
new file mode 100755
index 0000000000..f3fd0f4b19
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/images/toggle_down_dark.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/javascripts/dismissable.js@1302114648 b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/javascripts/dismissable.js@1302114648
new file mode 100755
index 0000000000..605865ff9a
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/javascripts/dismissable.js@1302114648
@@ -0,0 +1 @@
+(function(A){A.fn.dismissable=function(B){var D=A(this);var C={authenticity_token:twttr.form_authenticity_token,_method:"put"};C["user["+B.userAttribute+"]"]="1";D.find("a.dismiss").click(function(){D.hide();A.ajax({type:"POST",url:B.userUrl,data:C});return false});return this}})(jQuery); \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/stylesheets/following.css@1302114648.css b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/stylesheets/following.css@1302114648.css
new file mode 100755
index 0000000000..49f2f3f508
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/a/1302214109/stylesheets/following.css@1302114648.css
@@ -0,0 +1 @@
+body #content h2,body #content h2{font:18px 'Helvetica',sans-serif;font-weight:bold;color:#000;margin-left:10px;}.profile-user,body#profile div.section,body#profile_favorites div.section{padding:0 10px;}.profile-user h2.thumb{clear:both;float:none;padding:10px 0;line-height:1.25em;}.profile-user h2 img{float:left;}.profile-user h2 div{margin-left:65px;}#follow h2 img{margin-right:5px;}#follow h2 small{font:11px 'Lucida Grande',Arial,sans-serif;font-weight:normal;}.protected-profile-controls .profile-controls{height:23px;}#friend_requests h2.heading form{float:right;}body#friend_requests.ie7 h2.heading form{margin-top:-15px;padding-right:8px;}.denied-follow-request{float:right;padding-left:30px;background:transparent url(../images/reject_small.gif) no-repeat 0 center;margin:5px -100px 0 0;}.subpage #content ul.ctrlbar{padding:8px 10px;background-color:#f6f6f6;clear:both;float:none;}.wrapper{padding:15px;}#content div.section{padding:0;}#content div.section ul li{padding:0;}#content div.section ul.ctrlbar li{margin-right:4px;position:static;}.ctrlbar li{display:inline-block;margin-right:4px;position:relative;}.ctrlbar a{display:inline-block;vertical-align:middle;outline:none;padding:3px 4px;border:1px solid transparent;}.ctrlbar a i{display:block;overflow:hidden;width:13px;height:13px;background-image:url(../images/sprite-icons.png);background-repeat:no-repeat;}.ctrlbar a.expanded i{background-position:-1px -81px;}.follow-expanded .ctrlbar a.expanded,.follow-compact .ctrlbar a.compact{background-color:#fff;border-color:#ccc;}.ctrlbar a.compact i{background-position:-17px -81px;}#follow_grid table{margin-top:10px;width:100%;border-collapse:collapse;}#follow_grid tr{font:12px 'Lucida Grande',Arial,sans-serif;color:#333;border-bottom:1px solid #eee;}#follow_grid tr .is-blocked{position:static;}#follow_grid tr.even td{background-color:transparent;}#follow_grid tr:hover td{background-color:#f6f6f6;}#follow_grid th{font:11px 'Lucida Grande',Arial,sans-serif;color:#999;}#follow_grid th,#follow_grid td{padding:10px;vertical-align:top;}#follow_grid th{padding-bottom:6px;}#follow_grid th.actions-header,#follow_grid th.settings-header{text-align:right;}#follow_grid td.thumb{padding-right:0;}.user i{display:inline-block;width:13px;height:13px;background-image:url(../images/sprite-icons.png);background-repeat:no-repeat;overflow:hidden;outline:none;}#follow_grid .thumb{height:50px;width:50px;}#follow_grid .thumb img{width:50px;height:50px;}.user .user-detail{font:11px 'Lucida Grande',Arial,sans-serif;line-height:16px;width:225px;}#follow_grid td.user-detail{padding-right:0;}#follow_grid td.thumb{width:10%;}.protected .screenname{padding-right:12px;background:transparent url(../images/icon_lock.gif) no-repeat 100% 30%;}.verified-icon{vertical-align:top;padding:2px;}.user .user-detail address{position:relative;}.user .screenname{font:15px 'Helvetica';font-weight:bold;}.user .fullname,.user .location,.user .user-body{color:#666;}.user .user-body{display:block;overflow:hidden;width:265px;color:#666;}.user .user-detail strong{color:#333;}.user .user-body em{font-style:normal;}.user .currently em{white-space:nowrap;}.blocked-user,.blocked-user:hover,.blocked-user .screenname,.blocked-user .user-body,.blocked-user .user-body strong,.blocked-user .user-body:hover{background-color:#f2f2f2;color:#666;width:100%;}.user .is-following,.user .is-blocked,.user .is-pending{display:none;padding-left:.5em;}#container .following .is-following,#container .blocking .is-blocked,#container .pending .is-pending{display:inline-block;}.blocking .is-pending{display:none;}#follow_grid .is-following,#follow_grid .is-blocked,#follow_grid .is-pending{position:absolute;top:0;right:0;}.profile-user .is-following,.profile-user .is-blocked,.profile-user .is-pending{padding-left:0;}.user .is-following i,.user .is-blocked i,.user .is-pending i{height:9px;width:10px;margin-right:5px;}.profile-user .is-following i,.profile-user .is-blocked i,.profile-user .is-pending i{height:13px;width:15px;position:relative;top:1px;}.user .is-following i{background-position:-160px -16px;}.user .is-blocked i{background-position:-224px -16px;}.user .is-pending i{background-position:-192px -16px;}.profile-user .user .is-following i{background-position:-144px -16px;}.profile-user .user .is-blocked i{background-position:-208px -16px;}.profile-user .user .is-pending i{background-position:-176px -16px;}#follow_grid .user:hover .fullname,#follow_grid .user:hover .location,#follow_grid .user:hover .user-body{color:#333;}#follow_grid .user-actions-outer,#follow_grid.follow-compact .user-actions-outer{width:90px;padding-right:10px;}.current-user-following-page .user-actions-outer{width:40px;}#follow_grid .user-settings{width:80px;float:right;}#follow_grid .blocked-user:hover .user-body{color:#666;}.profile-user ul.user-settings{float:left;margin-left:10px;}#follow_grid .user-actions-outer,#follow_grid .user-settings-outer{text-align:right;}ul.user-settings>li{display:none;position:static;}.profile-user ul.user-settings>li{margin:0;}#follow_grid .following ul.user-settings>li,.profile-user .following ul.user-settings>li{display:inline-block;}ul.user-actions>li{display:inline-block;}ul.user-settings>li>a,ul.user-actions>li>a{display:inline-block;width:16px;height:16px;background-repeat:no-repeat;overflow:hidden;cursor:pointer;background-image:url(../images/sprite-icons.png);text-decoration:none;margin-right:3px;outline:none;position:relative;}ul.user-settings>li>a{margin-top:4px;}.profile-user ul.user-settings>li a{margin-right:0;}.user-settings li.sms-setting a.on{background-position:-64px -48px;}.is-blocked .learn-more{font-size:11px;margin-left:3px;}#follow_grid .user:hover .user-settings li.sms-setting a.on,.profile-user .user-settings li.sms-setting a.on{background-position:-48px -48px;}.user-settings li.sms-setting a.off{background-position:-80px -48px;}#follow_grid .user:hover .user-settings li.sms-setting a.off,.profile-user .user-settings li.sms-setting a.off{background-position:-160px -48px;}.user-settings li.replies-setting a.on{background-position:-16px -48px;}#follow_grid .user:hover .user-settings li.replies-setting a.on,.profile-user .user-settings li.replies-setting a.on{background-position:0 -48px;}.user-settings li.replies-setting a.off{background-position:-32px -48px;}#follow_grid .user:hover .user-settings li.replies-setting a.off,.profile-user .user-settings li.replies-setting a.off{background-position:-144px -48px;}.user-settings li.shares-setting a.on{background-position:-112px -48px;}.user:hover .user-settings li.shares-setting a.on,.profile-user .user-settings li.shares-setting a.on{background-position:-96px -48px;}.user-settings li.shares-setting a.off{background-position:-128px -48px;}.user:hover .user-settings li.shares-setting a.off,.profile-user .user-settings li.shares-setting a.off{background-position:-176px -48px;}.user .user-actions i{display:block;width:15px;}#follow_grid .user-actions .follow-action button{width:29px;}.profile-user .user-actions .follow-action button i{float:left;margin:0 5px 0 0;}.user-actions button{height:25px;}.current-user-following-page .user-actions .follow-action button{display:inline-block;}#follow_grid .pending .user-actions .follow-action button,.profile-user .pending .user-actions .follow-action button,#follow_grid .following .user-actions .follow-action button,.profile-user .following .user-actions .follow-action button,#follow_grid .blocking .user-actions .follow-action button,.profile-user .blocking .user-actions .follow-action button,#follow_grid .current-user-following-page .following .user-actions .follow-action button{display:none;}.user-actions .follow-action button i{display:block;background-position:-160px -32px;margin:0 4px;}#follow_grid .user:hover .user-actions .follow-action button i,.profile-user .user .user-actions .follow-action button i{background-position:-176px -32px;}.user-actions .action-menu{vertical-align:top;}.profile-controls .followed-by{margin-top:4px;text-align:left;font-size:11px;}.profile-controls .followed-by hr{color:#F6F6F6;background:#F6F6F6;border:0 solid #F6F6F6;border-top:1px solid #eee;border-bottom:1px solid #fff;height:0;margin:0 0 5px 0;display:block;}.profile-controls .followed-by label{color:#666;}.user-actions .action-menu button{width:36px;}.user-actions .action-menu button i{display:block;background-position:0 -64px;width:22px;margin:1px 7px;}#follow_grid .user:hover .user-actions .action-menu button i,.profile-user .user .user-actions .action-menu button i,.user-actions .action-menu button.clicked i{background-position:-32px -64px;}#follow_grid .user .user-actions .accept-action button,#follow_grid .user .user-actions .deny-action button{color:#aaa;margin-right:3px;}#follow_grid .user:hover .user-actions .accept-action button,#follow_grid .user:hover .user-actions .deny-action button{color:#333;}#friend_requests td.thumb{width:5px!important;padding-right:0;}#friend_requests td.user-detail{width:500px;}#friend_requests td.user-actions-outer{width:200px!important;}#friend_requests .user .user-body{width:400px;}body#friend_requests.ie7 #follow_grid table tr td{border-bottom:1px solid #eee!important;}body#friend_requests.ie7 td.user-actions-outer ul{width:190px;display:inline;}body#friend_requests.ie7 td.user-actions-outer ul li{float:left!important;}body#friend_requests.ie8 #follow_grid th.name-header{text-align:left!important;}body#friend_requests #follow_grid.empty{height:300px;}#follow_requests_all{color:#ccc;}.user-actions .menu button.clicked{background-image:none;}.user-actions .menu ul{display:none;position:absolute;width:200px;margin-top:-1px;padding:4px 0;text-align:left;border:1px solid #666;background-color:#fff;z-index:9999;}.user-actions .menu ul li a,.user-actions .menu ul li label,.user-actions .menu ul li input[type="checkbox"]{display:inline-block;font:11px 'Lucida Grande',Arial,sans-serif;color:#666;position:relative;padding:4px 5px;vertical-align:top;}.user-actions .menu ul li .loaddisableding-spinner{display:inline-block;position:relative;top:4px;left:1px;margin-left:4px;}.user-actions .action-menu ul li a{padding:4px 5px 4px 27px;}.user-actions .menu ul li a{display:block;color:#666;text-decoration:none;}.user-actions .menu ul li:hover{color:#fff;background-color:#666;}.user-actions .menu ul li:hover *{color:#fff;}.user-actions .menu ul li.divider{border-top:1px solid #ddd;}.user-actions .menu ul a i{position:absolute;left:7px;top:4px;width:15px;}.user-actions .mention i{background-position:-16px -32px;}.user-actions .mention:hover i{background-position:0 -32px;}.user-actions .direct-message i{background-position:-48px -32px;}.user-actions .direct-message:hover i{background-position:-32px -32px;}.user-actions .follow i{background-position:-176px -32px;}.user-actions .follow:hover i{background-position:-160px -32px;}.current-user-following-page .user-actions .follow{display:none;}.user-actions .remove i{background-position:-208px -32px;}.user-actions .remove:hover i{background-position:-192px -32px;}.user-actions .unfollow i{background-position:-112px -32px;}.user-actions .unfollow:hover i{background-position:-96px -32px;}.user-actions .block i{background-position:-144px -32px;}.user-actions .report-for-spam i{background-position:-272px -32px;}.user-actions .report-for-spam:hover i{background-position:-256px -32px;}.user-actions .block:hover i{background-position:-128px -32px;}.user-actions .unblock i{background-position:-144px -32px;}.user-actions .unblock:hover i{background-position:-128px -32px;}.user-actions .unfollow,.user-actions .unblock,.user-actions .direct-message,.user-actions .nudge,.pending .user-actions .follow,#follow_grid .following .user-actions .follow,.profile-user .following .user-actions .follow,.blocking .user-actions .block,.blocking .user-actions .report-for-spam,#friend_requests .follow-request .user-actions .mention,#friend_requests .follow-request .user-actions .direct-message{display:none;}#follow_grid .following .user-actions .unfollow,.profile-user .following .user-actions .unfollow,.direct-messageable .user-actions .direct-message,.blocking .user-actions .unblock{display:block;}.sidebar-actions.blocked .unblock-sidebar-action,.sidebar-actions.unblocked .block-sidebar-action,.sidebar-actions.unblocked .report-for-spam-sidebar-action{display:block;}.sidebar-actions.unblocked .unblock-sidebar-action,.sidebar-actions.blocked .block-sidebar-action,.sidebar-actions.blocked .report-for-spam-sidebar-action{display:none;}#follow_grid.follow-compact td{padding:4px 0 4px 10px;vertical-align:middle;}#follow_grid.follow-compact .thumb{height:24px;width:1%;padding-left:10px;}#follow_grid.follow-compact .thumb img{width:24px;height:24px;}#follow_grid.follow-compact .fullname{padding-left:.25em;}#follow_grid.follow-compact td.user-detail{line-height:16px;}#follow_grid.follow-compact .user-detail br,#follow_grid.follow-compact .location,#follow_grid.follow-compact .user-body{display:none;}#follow_grid td.user-actions,#follow_grid td.user-settings{padding-right:10px;}#pagination.pagination{padding:0 10px;}#similar-wrapper{padding:15px;}body.safari .user-actions .action-menu button{padding-top:5px;}body.safari .user-actions .action-menu button i{margin:0 -2px;}body.safari #follow_grid .user-actions .follow-action button i{margin:0 -2px;}body.safari .user-actions .menu ul li .loaddisableding-spinner{margin-right:-1px;}body.ie7 .profile-controls{zoom:1;}body.ie7 .ctrlbar li{float:left;}body.ie7 #content ul.ctrlbar{height:24px;background-color:#f6f6f6;}body.ie7 .user-detail{width:275px;}body.ie7 .user-actions{text-align:right;width:70px;}body.ie7 .profile-user .user-actions{width:100%;}body.ie7 .profile-user .following .user-actions{width:auto;}body.ie7 .profile-user .follow-action button.btn{width:75px;}body.ie7 .profile-user .is-following i{margin:2px 5px 2px 0;}body.ie7 .user-actions-outer{display:inline-block;}body.ie7 .profile-user .user-settings{margin-top:1px;}body.ie7 .profile-user .user-settings li{float:left;}body.ie7 .user-settings li a{margin-right:4px;}body.ie7 ul.user-actions>li.follow-action{float:left;}body.ie7 ul.user-actions>li.follow-action button{padding:3px 8px;}body.ie7 ul.user-actions>li.action-menu,body.firefox2 ul.user-actions>li.action-menu{float:right;}body.ie7 ul.user-actions>li>button{height:24px;}body.ie7 .user-actions .action-menu button{width:36px;margin-right:-2px;}body.ie7 .user-actions .action-menu ul{margin-top:22px;margin-left:-34px;}body.ie7 .user-actions button i,body.ie8 .user-actions button i{margin:0 -1px;}body.firefox2 .profile-user .profile-controls{height:2em;}body.firefox2 .profile-user .user-actions{width:100%;}body.firefox2 .profile-user .following .user-actions{width:auto;}body.firefox2 .following .is-following,body.firefox2 .blocking .is-blocked,body.firefox2 .pending .is-pending{display:block;}body.firefox2 #follow_grid .following ul.user-settings>li,body.firefox2 .profile-user .following ul.user-settings>li{display:block;float:left;}body.firefox2 #follow_grid .following ul.user-settings>li a,body.firefox2 .profile-user .following ul.user-settings>li a{display:block;margin-right:4px;}body.firefox2 .user .is-following i{display:block;float:left;margin:4px 5px 4px 0;}body.firefox2 .profile-user .user .is-following i{margin:5px 5px 5px 0;}body.firefox2 #follow_grid .is-following,body.firefox2 #follow_grid .is-blocked,body.firefox2 #follow_grid .is-pending{top:2px;}body.firefox2 #content ul.ctrlbar{height:24px;background-color:#f6f6f6;}body.firefox-windows .ctrlbar li{float:left;}body.firefox-windows .ctrlbar li a{display:block;}body.firefox2 .user-actions{text-align:right;width:70px;}body.firefox2 ul.user-actions>li.follow-action{float:left;}body.firefox2 ul.user-actions>li.action-menu{float:right;}body.firefox2 .user-actions .action-menu button,width:36px;margin-right:-2px;}body.opera .user-actions .follow-action button i{margin-left:-4px;}body.opera .user-actions .action-menu button i{margin-left:-3px;}body.opera .user-actions .action-menu ul{margin-top:10px;}body.chrome .user-actions .action-menu button{padding-top:5px;}body.chrome .user-actions .action-menu button i{margin:0 -2px;}body.chrome .user-actions .follow-action button i{margin:0!important;}body.ie8 .ctrlbar a:hover,body.safari .ctrlbar a:hover,body.firefox .ctrlbar a:hover,body.firefox_win .ctrlbar a:hover,body.firefox_2 .ctrlbar a:hover{background-color:#fff;border-color:#ccc;} \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/1239180764/GlassblowerX_mini.jpg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/1239180764/GlassblowerX_mini.jpg
new file mode 100755
index 0000000000..4c33d429ac
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/1239180764/GlassblowerX_mini.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/1248229613/redsugarskullnecklace4-pola_mini.jpg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/1248229613/redsugarskullnecklace4-pola_mini.jpg
new file mode 100755
index 0000000000..6f135e4c8d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/1248229613/redsugarskullnecklace4-pola_mini.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/333032766/5600_106787006838_550741838_2009237_6385345_n_mini.jpg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/333032766/5600_106787006838_550741838_2009237_6385345_n_mini.jpg
new file mode 100755
index 0000000000..8bad696379
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/333032766/5600_106787006838_550741838_2009237_6385345_n_mini.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/754757071/rawr_mini.jpg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/754757071/rawr_mini.jpg
new file mode 100755
index 0000000000..eed8c33012
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/754757071/rawr_mini.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/874705507/01_3_mini.jpg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/874705507/01_3_mini.jpg
new file mode 100755
index 0000000000..a03e86a7b8
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/874705507/01_3_mini.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/959721336/16869_103046893051833_100000395672538_70559_3952672_n_1__mini.jpg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/959721336/16869_103046893051833_100000395672538_70559_3952672_n_1__mini.jpg
new file mode 100755
index 0000000000..9559bd3422
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a1.twimg.com/profile_images/959721336/16869_103046893051833_100000395672538_70559_3952672_n_1__mini.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/ajax.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/ajax.gif
new file mode 100755
index 0000000000..16e32a32c6
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/ajax.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/arr-inline-form.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/arr-inline-form.gif
new file mode 100755
index 0000000000..c75a49c5d6
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/arr-inline-form.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/arr2.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/arr2.gif
new file mode 100755
index 0000000000..577be18717
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/arr2.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/arrow_right_dark.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/arrow_right_dark.png
new file mode 100755
index 0000000000..4e892821b6
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/arrow_right_dark.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/bg-btn-blue.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/bg-btn-blue.png
new file mode 100755
index 0000000000..058f726d98
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/bg-btn-blue.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/bg-btn-signup_gold.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/bg-btn-signup_gold.png
new file mode 100755
index 0000000000..ba1f78f4e8
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/bg-btn-signup_gold.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/btn-bg.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/btn-bg.gif
new file mode 100755
index 0000000000..f14912f0f7
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/btn-bg.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/btn_green_arrow.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/btn_green_arrow.gif
new file mode 100755
index 0000000000..15de49eaeb
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/btn_green_arrow.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/btn_green_arrow_small.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/btn_green_arrow_small.gif
new file mode 100755
index 0000000000..2a0719256b
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/btn_green_arrow_small.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/btn_red_small.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/btn_red_small.gif
new file mode 100755
index 0000000000..8d566b5148
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/btn_red_small.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-blue.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-blue.gif
new file mode 100755
index 0000000000..f3fbf46f60
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-blue.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-chart.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-chart.gif
new file mode 100755
index 0000000000..15dc0d9572
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-chart.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-dark.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-dark.gif
new file mode 100755
index 0000000000..4821ae5ad7
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-dark.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-green.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-green.gif
new file mode 100755
index 0000000000..24e2603a77
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-green.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-mint.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-mint.gif
new file mode 100755
index 0000000000..bea9ab7c5b
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-mint.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-pink.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-pink.gif
new file mode 100755
index 0000000000..abe2f4567a
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-pink.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-red.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-red.gif
new file mode 100755
index 0000000000..dba831415d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-red.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-yellow.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-yellow.gif
new file mode 100755
index 0000000000..fec04bd96a
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn-yellow.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn.gif
new file mode 100755
index 0000000000..5d1e16452e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/buttons/bg-btn.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/checkmark.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/checkmark.gif
new file mode 100755
index 0000000000..a1e71e6ce7
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/checkmark.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/close_small.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/close_small.png
new file mode 100755
index 0000000000..f266cef818
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/close_small.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/commercial/garuda-overlay.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/commercial/garuda-overlay.gif
new file mode 100755
index 0000000000..22b5029373
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/commercial/garuda-overlay.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/dialog_arrows_sprite.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/dialog_arrows_sprite.gif
new file mode 100755
index 0000000000..f3d54033fe
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/dialog_arrows_sprite.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/divider.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/divider.png
new file mode 100755
index 0000000000..0392537bc5
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/divider.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/divot.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/divot.gif
new file mode 100755
index 0000000000..f562d712ef
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/divot.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/divvy-up.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/divvy-up.png
new file mode 100755
index 0000000000..e4d2727db7
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/divvy-up.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/divvy.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/divvy.gif
new file mode 100755
index 0000000000..273b2d0741
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/divvy.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/divvy.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/divvy.png
new file mode 100755
index 0000000000..49c4da5a13
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/divvy.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/follow_check.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/follow_check.gif
new file mode 100755
index 0000000000..a2fb9e2d56
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/follow_check.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/geo_chrome_help_banner_back.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/geo_chrome_help_banner_back.png
new file mode 100755
index 0000000000..959a441547
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/geo_chrome_help_banner_back.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/geo_creation_hint_arrow.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/geo_creation_hint_arrow.gif
new file mode 100755
index 0000000000..efb4839a0d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/geo_creation_hint_arrow.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/geo_firefox_help_banner_back.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/geo_firefox_help_banner_back.png
new file mode 100755
index 0000000000..9ffd9751d1
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/geo_firefox_help_banner_back.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/geo_ie_gtb_help_banner_back.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/geo_ie_gtb_help_banner_back.png
new file mode 100755
index 0000000000..a2e56897e1
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/geo_ie_gtb_help_banner_back.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon-mobile.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon-mobile.gif
new file mode 100755
index 0000000000..b2dc6aca59
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon-mobile.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_add.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_add.png
new file mode 100755
index 0000000000..2ebf92cf2d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_add.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_direct_reply.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_direct_reply.gif
new file mode 100755
index 0000000000..80b6c30d03
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_direct_reply.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_lock.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_lock.gif
new file mode 100755
index 0000000000..53e6641408
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_lock.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_remove.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_remove.png
new file mode 100755
index 0000000000..3a4f1ddc96
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_remove.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_reply.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_reply.gif
new file mode 100755
index 0000000000..a4379a70b6
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_reply.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_throbber.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_throbber.gif
new file mode 100755
index 0000000000..fa124c5fb4
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_throbber.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_trash.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_trash.gif
new file mode 100755
index 0000000000..916a332a34
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/icon_trash.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/inline-media.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/inline-media.png
new file mode 100755
index 0000000000..8c4d15e29b
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/inline-media.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/larry-shadowed-big.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/larry-shadowed-big.png
new file mode 100755
index 0000000000..bd1e56347f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/larry-shadowed-big.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/lock_icon_small.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/lock_icon_small.png
new file mode 100755
index 0000000000..6208288320
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/lock_icon_small.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/more.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/more.gif
new file mode 100755
index 0000000000..8382f19b52
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/more.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/nav_search_submit.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/nav_search_submit.png
new file mode 100755
index 0000000000..29e1d0a13f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/nav_search_submit.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/check.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/check.png
new file mode 100755
index 0000000000..1e0188d587
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/check.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/connect_129px.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/connect_129px.png
new file mode 100755
index 0000000000..b1d8591a86
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/connect_129px.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/connect_146px.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/connect_146px.png
new file mode 100755
index 0000000000..5b99bda010
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/connect_146px.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/connect_170px.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/connect_170px.png
new file mode 100755
index 0000000000..d990e2e236
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/connect_170px.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/connect_236px.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/connect_236px.png
new file mode 100755
index 0000000000..7b8b74d498
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/connect_236px.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/gradient-background.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/gradient-background.png
new file mode 100755
index 0000000000..503ab9f108
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/gradient-background.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/rays-box.jpg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/rays-box.jpg
new file mode 100755
index 0000000000..bb19d1f61e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/rays-box.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/t_170px.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/t_170px.png
new file mode 100755
index 0000000000..2cce581176
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/oauth2/t_170px.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/petal_spinner.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/petal_spinner.gif
new file mode 100755
index 0000000000..8a1547805d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/petal_spinner.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/retweet/retweet-x.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/retweet/retweet-x.png
new file mode 100755
index 0000000000..7f1f31bd8d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/retweet/retweet-x.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/round-btn-hover.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/round-btn-hover.gif
new file mode 100755
index 0000000000..d8d6030f1b
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/round-btn-hover.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/round-btn.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/round-btn.gif
new file mode 100755
index 0000000000..f65bb15045
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/round-btn.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/rss.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/rss.gif
new file mode 100755
index 0000000000..0ee61c7cd0
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/rss.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/spinner.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/spinner.gif
new file mode 100755
index 0000000000..6e5bace6e6
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/spinner.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/sprite-icons.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/sprite-icons.png
new file mode 100755
index 0000000000..a93cede946
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/sprite-icons.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/sprite-icons.png@v3 b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/sprite-icons.png@v3
new file mode 100755
index 0000000000..a93cede946
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/sprite-icons.png@v3
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/tables/tablesorter-indicators.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/tables/tablesorter-indicators.png
new file mode 100755
index 0000000000..af3c40522f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/tables/tablesorter-indicators.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/thumb-bird-bw.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/thumb-bird-bw.gif
new file mode 100755
index 0000000000..dbe336910e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/thumb-bird-bw.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/tipsy/tipsy-east.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/tipsy/tipsy-east.gif
new file mode 100755
index 0000000000..697550bdb7
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/tipsy/tipsy-east.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/tipsy/tipsy-north.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/tipsy/tipsy-north.gif
new file mode 100755
index 0000000000..c22e72b458
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/tipsy/tipsy-north.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/tipsy/tipsy-south.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/tipsy/tipsy-south.gif
new file mode 100755
index 0000000000..cd48fcd6e0
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/tipsy/tipsy-south.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/tipsy/tipsy-west.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/tipsy/tipsy-west.gif
new file mode 100755
index 0000000000..bd51b57068
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/tipsy/tipsy-west.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_closed.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_closed.gif
new file mode 100755
index 0000000000..ce8fd78e3c
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_closed.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_down_dark.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_down_dark.gif
new file mode 100755
index 0000000000..4e0ed37074
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_down_dark.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_down_dark.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_down_dark.png
new file mode 100755
index 0000000000..f3fd0f4b19
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_down_dark.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_down_light.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_down_light.gif
new file mode 100755
index 0000000000..c05d02d702
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_down_light.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_down_light.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_down_light.png
new file mode 100755
index 0000000000..d35416159a
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_down_light.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_opened.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_opened.gif
new file mode 100755
index 0000000000..3543c3bcfb
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_opened.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_up_dark.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_up_dark.gif
new file mode 100755
index 0000000000..f1721e8843
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_up_dark.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_up_dark.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_up_dark.png
new file mode 100755
index 0000000000..951d903d8b
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toggle_up_dark.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toptweet-overlay.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toptweet-overlay.gif
new file mode 100755
index 0000000000..cd1a0f69c9
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/toptweet-overlay.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/translator/translator.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/translator/translator.png
new file mode 100755
index 0000000000..4b3a45505c
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/translator/translator.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/trendtip-pointer.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/trendtip-pointer.gif
new file mode 100755
index 0000000000..adf8e05783
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/trendtip-pointer.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/verified/verified.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/verified/verified.png
new file mode 100755
index 0000000000..19c0ec0665
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/verified/verified.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/verified/verified_small.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/verified/verified_small.png
new file mode 100755
index 0000000000..b0fdcd4dfb
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/verified/verified_small.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/warning-sign.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/warning-sign.png
new file mode 100755
index 0000000000..0ef7aa4cf9
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/images/warning-sign.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/javascripts/geov1.js@1302114648 b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/javascripts/geov1.js@1302114648
new file mode 100755
index 0000000000..870fdd9611
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/javascripts/geov1.js@1302114648
@@ -0,0 +1 @@
+twttr.geo={LOG_VERSION:2,LOCATION_CACHE_INTERVAL:30000,BROWSER_GEO_BANNER_APPEAR_DELAY:500,BROWSER_GEO_BANNER_DISAPPEAR_INTERVAL:100,PLACE_SEARCH_AUTOCOMPLETE_DELAY:250,MAX_NEARBY_PLACES:12,MAX_PLACE_SEARCH_RESULTS:50,MAX_PLACE_AUTOCOMPLETE_RESULTS:8,PLACE_SEARCH_RESULTS_PER_PAGE:5,PLACE_CREATION_ACCURACY_THRESHOLD:500,ACCEPTABLE_LOCATION_ACCURACY:1000,ACCEPTABLE_LOCATION_TIMEOUT:5000,LOCATION_TIMEOUT:25000,LOCATION_MAXIMUM_AGE:60000,MAX_SIMILAR_PLACES:5,GENERIC_PLACE_TYPES:{city:null,neighborhood:null,admin:null,country:null},TARGET_POI_NAME_LENGTH:40,TARGET_PLACE_NAME_LENGTH:50,templates:{dropdownItem:{container:'<li id="place_{{id}}"><span class="place_item_icon">&nbsp;</span><span class="place_noicon">{{formatted_name}}</span></li>',poi:'<li id="place_{{id}}"><span class="place_item_icon">&nbsp;</span><span class="place_icon">&nbsp;</span> {{formatted_name}} <span class="place_details">{{details}}</span></li>'},searchResultItem:{container:'<li id="result_place_{{id}}"><span class="place_noicon"><a href="#">{{formatted_name}}</a></span></li>',poi:'<li id="result_place_{{id}}"><span class="place_icon">&nbsp;</span> <a href="#">{{formatted_name}}</a> <span class="place_details">{{details}}</span></li>'},autocompleteItem:{container:'<li><span class="place_noicon">{{formatted_name}}</span></li>',poi:'<li><span class="place_icon">&nbsp;</span> {{formatted_name}} <span class="place_details">{{details}}</span></li>'},nearbyActivityHeader:{container:'<span><a href="#">{{formatted_name}}</a></span>',poi:'<span><a href="#">{{full_name}} <span class="place_icon">&nbsp;</span></a></span>'},nearbyActivityItem:{container:'<li id="result_place_{{id}}"><a href="#"><span class="place_noicon">{{formatted_name}}</span></a><div class="geo_address">{{details}}</div></li>',poi:'<li id="result_place_{{id}}"><a href="#">{{formatted_name}}&nbsp;<span class="place_icon">&nbsp;</span></a><div class="geo_address">{{details}}</div></li>'}},options:{more_places:false,autocomplete:false,autocomplete_zero_delay:false,place_creation:false,place_creation_needs_high_accuracy:false,allow_set_location_manually:false,show_place_details_in_map:false}};twttr.geo.getBestContainer=function(A){if(!A){return{name:"",full_name:"",contained_within:[],place_type:"city",bounding_box:{type:"Polygon",coordinates:[[[-122.51368188,37.70813196],[-122.35845384,37.70813196],[-122.35845384,37.83245301],[-122.51368188,37.83245301]]]}}}var B={name:A.part_of_name||"",full_name:A.part_of_name||"",contained_within:[],place_type:"city"};if(A.contained_within){A.contained_within.every(function(C){B=C;return C.place_type!="city"})}return B};twttr.geo.getBestCity=function(A){return(A&&A.place_type=="city")?A:twttr.geo.getBestContainer(A)};twttr.geo.getLocationFromPlace=function(A){if(!A||!A.bounding_box||A.bounding_box.type!="Polygon"||!A.bounding_box.coordinates){return null}var B=A.bounding_box.coordinates[0];if(!B||B.length!=4){return null}return{accuracy:500*twttr.geo.greatCircleDistanceInKm(B[0][1],B[0][0],B[2][1],B[2][0]),latitude:(B[0][1]+B[2][1])*0.5,longitude:(B[0][0]+B[2][0])*0.5}};twttr.geo.getPlacePageLinkFromPlace=function(A){return"/search?"+$.param({q:"place:"+A.id,format:"html"})};twttr.geo.getPlacePageLinkAttrsFromPlace=function(A){var C=[A.attributes?A.attributes.street_address:null,A.contained_within&&A.contained_within.length>0?A.contained_within[0].full_name:null].filter(function(D){return D}).join(", ");var B=A.place_type=="poi"?C:A.name+", "+C;return{title:A.full_name,_query:"place:"+A.id,_place_details:C,_place_map_link:"httpdisabled://maps.google.com/maps?"+$.param({q:B})}};twttr.geo.getPlaceDetails=function(A){if(A.attributes&&A.attributes.street_address){return A.attributes.street_address}switch(A.place_type){case"city":return _("a city");case"neighborhood":return _("a neighborhood");case"country":return _("a country");case"admin":return _("a state or province");default:return _("a place")}return twttr.geo.getBestContainer(A).name};twttr.geo.formatPlaceName=function(A,B){if(A.length<=B){return A}A=A.replace(/ [(].*[)]/g,"");if(A.length<=B){return A}var C="...";return A.substr(0,B-C.length)+C};twttr.geo.renderPlace=function(B,A){if(A.place_type in twttr.geo.GENERIC_PLACE_TYPES){B=B.container;A.formatted_name=twttr.geo.formatPlaceName(A.full_name,twttr.geo.TARGET_PLACE_NAME_LENGTH)}else{B=B.poi;A.formatted_name=twttr.geo.formatPlaceName(A.name,twttr.geo.TARGET_POI_NAME_LENGTH)}A.details=twttr.geo.getPlaceDetails(A);return $(Mustache.to_html(B,A))};twttr.geo.geoScribe=function(A,B){scribe($.extend({event_name:A,ui_version:1,log_version:twttr.geo.LOG_VERSION},B),"geo_checkins")};twttr.geo.Exceptions={GeoSupportException:function(){this.msg=_("This browser does not support GeoLocation")}};twttr.geo.Errors={PermissionDeniedError:function(){this.allowRetry=false;this.fatal=true;this.msg=_("Please grant your web browser permission to tell Twitter where you are.")},LocationDetectionError:function(){this.allowRetry=true;this.fatal=false;this.msg=_("We were unable to detect your location.")}};twttr.geo.greatCircleDistanceInKm=function(H,F,G,E){var C=Math.PI/180;var A=Math.sin((G-H)*C*0.5);var B=Math.sin((E-F)*C*0.5);var D=A*A+Math.cos(H*C)*Math.cos(G*C)*B*B;return 12742*Math.atan2(Math.sqrt(D),Math.sqrt(1-D))};twttr.klass("twttr.geo.Map",function(B){if(!twttr.geo.mapsUI.mapsAvailable()){return }var A={mapTypeId:google.maps.MapTypeId.ROADMAP,disableDefaultUI:true,scrollwheel:false,navigationControl:true,navigationControlOptions:{position:google.maps.ControlPosition.TOP_LEFT,style:google.maps.NavigationControlStyle.SMALL}};this.map=new google.maps.Map(B,A);this.bounds=null}).method("createMapOverlay",function(){function A(B){this.setMap(B)}A.prototype=new google.maps.OverlayView();A.prototype.onAdd=function(){};A.prototype.onRemove=function(){};A.prototype.draw=function(){};this.mapOverlay=new A(this.map)}).method("extendBounds",function(A){if(!this.bounds){this.bounds=new google.maps.LatLngBounds()}this.bounds.extend(A)}).method("addPoint",function(A,C){var E=this;var F=new google.maps.LatLng(A[1],A[0]);E.extendBounds(F);var D=new google.maps.MarkerImage("httpdisabled://s.twimg.com/a/1302214109/images/pin.png",new google.maps.Size(43,32),null,new google.maps.Point(14,33));E.marker=new google.maps.Marker({flat:true,icon:D,map:E.map,position:F,clickable:false,zIndex:2});if(C){var B=new google.maps.MarkerImage(C,new google.maps.Size(24,24),null,new google.maps.Point(13,32));E.avatar=new google.maps.Marker({flat:true,icon:B,map:E.map,position:F,zIndex:3})}}).method("addPoi",function(B,D){var F=this;var A=typeof (D)=="function";var G=new google.maps.LatLng(B[1],B[0]);F.extendBounds(G);var E=new google.maps.MarkerImage("httpdisabled://s.twimg.com/a/1302214109/images/poi-pin.png",new google.maps.Size(43,32),null,new google.maps.Point(14,33));var C=new google.maps.Marker({flat:true,icon:E,map:F.map,position:G,draggable:A,clickable:A,zIndex:2});if(A){google.maps.event.addListener(C,"dragend",function(H){D(H.latLng.lat(),H.latLng.lng())})}}).method("addFocusablePoint",function(A,F){var D=this;var G=new google.maps.LatLng(A[1],A[0]);D.extendBounds(G);var E=new google.maps.MarkerImage("httpdisabled://s.twimg.com/a/1302214109/images/place-minor10x.png",new google.maps.Size(10,10),new google.maps.Point(10,0),new google.maps.Point(5,5));var C=new google.maps.MarkerImage("httpdisabled://s.twimg.com/a/1302214109/images/place-minor10x.png",new google.maps.Size(10,10),null,new google.maps.Point(5,5));var B=new google.maps.Marker({flat:true,icon:C,map:D.map,position:G,clickable:F!==undefined,zIndex:2});if(F){if(D.mapOverlay===undefined){D.createMapOverlay()}google.maps.event.addListener(B,"mouseover",function(H){F(true,D.mapOverlay.getProjection().fromLatLngToContainerPixel(G))});google.maps.event.addListener(B,"mouseout",function(H){F(false,D.mapOverlay.getProjection().fromLatLngToContainerPixel(G))})}return function(H){if(H){B.setIcon(E);B.setZIndex(3)}else{B.setIcon(C);B.setZIndex(2)}}}).method("addAccuracyRing",function(B,A,F){var E=new google.maps.LatLng(B[1],B[0]);var D=new google.maps.Circle({map:F?this.map:null,center:E,radius:A,clickable:false,fillColor:"#0040FF",fillOpacity:0.08,strokeColor:"#2929D4",strokeOpacity:0.7,strokeWeight:0.5,zIndex:1});var C=D.getBounds();if(A>200){this.extendBounds(C.getNorthEast());this.extendBounds(C.getSouthWest())}}).method("addPlacePolygon",function(B){if(B[0][0] instanceof Array){B=B[0]}var D=[];for(var C=0;C<B.length;C++){var A=new google.maps.LatLng(B[C][1],B[C][0]);D.push(A);this.extendBounds(A)}var E=new google.maps.Polygon({path:D,strokeColor:"#FF0000",strokeOpacity:0.5,strokeWeight:0.5,fillColor:"#FF0000",fillOpacity:0.2});E.setMap(this.map)}).method("isEmpty",function(){return !this.bounds}).method("adjustBounds",function(B){var A=this.bounds.getSouthWest();var D=this.bounds.getNorthEast();if(D.equals(A)){this.map.setZoom(B||13);this.map.setCenter(D)}else{var F=this.bounds.getCenter();var C=0.7;function E(H,G){return H*C+G*(1-C)}this.map.fitBounds(new google.maps.LatLngBounds(new google.maps.LatLng(E(A.lat(),F.lat()),E(A.lng(),F.lng())),new google.maps.LatLng(E(D.lat(),F.lat()),E(D.lng(),F.lng()))))}}).method("resize",function(){google.maps.event.trigger(this.map,"resize")});twttr.geo.mapsUI={templates:{mapProgress:'<div id="geo_map_progress" class="hoverer"><div class="hovercard-divot"></div><div class="hoverer-inner"><div id="geo_map_spinner">&nbsp;</div></div></div>',mapWithPlace:'<div id="geo_modal" class="hoverer"><div class="hovercard-divot"></div><div class="hoverer-inner geo_map_with_place"><div id="map_canvas"></div><div class="geo_map_place_details"><div class="geo_map_place_name"></div><div class="geo_map_place_address"></div><div class="geo_map_place_container"></div><div class="geo_map_place_phone"></div><div class="geo_map_place_tweets"><a href="javascript:undefined">{{_i}}Tweets at this place &rarr;{{/i}}</a></div></div><a href="#" class="map_close">&times;</a></div></div>',mapWithoutPlace:'<div id="geo_modal" class="hoverer"><div class="hovercard-divot"></div><div class="hoverer-inner"><div id="map_canvas"></div><a href="#" class="map_close">&times;</a></div></div>'},googleApiAvailable:function(){return typeof google!="undefined"},loaddisabledMaps:function(C){if(this.googleApiAvailable()&&google.loaddisabled!==undefined){var B=window.location.hostname.match(/^(.+\.)?twitter\.com$/);var A=google.loaddisabled("maps","3",{callback:C,other_params:B?"client=free-twitter&sensor=false":"sensor=false"})}else{C()}},mapsAvailable:function(){return this.googleApiAvailable()&&google.maps!==undefined},initialize:function(){var A=this;if(A.googleApiAvailable()){A.liveClickHandler=function(C){C.preventDefault();var B=$(this).closest("span.entry-meta");voidMapModal(B.meta(),B.find("a.geocoded_google_link"))};$(".geo-pin, a.geocoded_google_link").live("click",A.liveClickHandler)}},closeMapModal:function(){$(".geo-pin.selected").removeClass("selected");$("#geo_modal").remove();$("#geo_map_progress").remove();this.geoMap=null}voidMapModal:function(G,B){var D=this;D.closeMapModal();var C=$(B);C.addClass("selected");C.after(Mustache.to_html(D.templates.mapProgress));D.showHover($("#geo_map_progress"),C,150);var F=G.place_id;var E=F&&twttr.geo.options.show_place_details_in_map?D.templates.mapWithPlace:D.templates.mapWithoutPlace;C.after(Mustache.to_html(E));$("html").one("click",function(H){D.closeMapModal()});$(".map_close").click(function(H){H.preventDefault();D.closeMapModal()});var A=$("#map_canvas").get(0);twttr.geo.mapsUI.loaddisabledMaps(function(){D.geoMap=new twttr.geo.Map(A);var H=D.geoMap;var I=false;var J=function(){if(!I&&G.latlng){D.geoMap.addPoint(G.latlng.slice().reverse(),G.avatar_url)}if(D.geoMap.isEmpty()){D.closeMapModal();return }D.geoMap.adjustBounds();$("#geo_map_progress").remove();D.showHover($("#geo_modal"),C,0)};if(F){twttr.api.getPlaceDetails({place_id:G.place_id,success:function(L,N){if(H!=D.geoMap){return }var M=L.geometry.coordinates;if(L.geometry.type=="Polygon"){D.geoMap.addPlacePolygon(M)}else{if(L.geometry.type=="MultiPolygon"){M.forEach(function(O){D.geoMap.addPlacePolygon(O)})}else{if(L.geometry.type=="Point"){D.geoMap.addPoint(M,G.avatar_url);I=true}}}$(".geo_map_place_name").text(L.name);$(".geo_map_place_address").text((L.attributes&&L.attributes.street_address)||"");var K=twttr.geo.getBestContainer(L);$(".geo_map_place_container").text(K.full_name||"");$(".geo_map_place_phone").text((L.attributes&&L.attributes.phone)||"");$(".geo_map_place_tweets a").attr("href",twttr.geo.getPlacePageLinkFromPlace(L));if(window.location.pathname=="/"){$(".geo_map_place_tweets a").attr(twttr.geo.getPlacePageLinkAttrsFromPlace(L)).isSearchLink().click(function(){D.closeMapModal()})}J()}})}else{J()}})},showHover:function(A,B,C){A.visible(false);twttr.SimplePositioner.setPosition(A,B,{itemHeight:180,offsets:{above:{top:-10,left:-40},below:{top:10,left:-40}},direction:"prefer below",hasContainer:true});A.click(function(D){D.stopPropagation()});setTimeout(function(){A.visible(true)},C)}};$(function(){twttr.geo.mapsUI.initialize()});twttr.klass("twttr.geo.PlacesDropdown",function(B,D){var C=this;C.placer=B;C.opts=D;var A="<a id='place_link' href='#'><span id='place_name'></span> â–¾</a><ul class='round places_list'/>";$("#place_content").html(A).bind("tweet",function(){var E=C.placer.getState();C.placer.determinePlaces(function(){C.rebuildPlacesDropdown()},function(){$(".geo_disable_webclient:first").click()});if($("#place_id").val()!=""){twttr.geo.geoScribe("geotweet",E)}});C.$placesList=$("#place_content ul.places_list");C.rebuildPlacesDropdown();$("#place_link").click(function(E){E.preventDefault();if($("#place_content ul.places_list:visible").length>0){C.closeMenu()}else{voidMenu();E.stopPropagation()}})}).method("rebuildPlacesDropdown",function(){var A=this;A.$placesList.empty();A.appendPoiPlaces(A.placer.places);A.appendNonPoiPlaces(A.placer.places);A.appendMorePlaces();$(".geo_more_places").click(function(B){B.preventDefault();new twttr.geo.PlaceSearchDialog(A.placer,A.opts,function(){A.rebuildPlacesDropdown();A.showSelectedPlace()});twttr.geo.geoScribe("click_search_places")});A.showSelectedPlace()}).method("appendPoiPlaces",function(A){var B=false;A.forEach(function(C){if(C.place_type=="poi"){this.$placesList.append(this.createPlaceItem(C));B=true}},this);return B}).method("appendNonPoiPlaces",function(A){A.forEach(function(B){if(B.place_type!="poi"){this.$placesList.append(this.createPlaceItem(B))}},this)}).method("createPlaceItem",function(A){var C=this;var B=twttr.geo.renderPlace(twttr.geo.templates.dropdownItem,A);B.click(function(D){D.preventDefault();C.placer.selectPlace(A);C.rebuildPlacesDropdown();C.closeMenu();twttr.geo.geoScribe("click_place_item",C.placer.getState())});return B}).method("appendMorePlaces",function(){if(this.opts.more_places){this.$placesList.append('<li class="geo_more_places"><span class="place_item_icon more_places">&nbsp;</span>'+_("Search places...")+"</li>")}}).method("showSelectedPlace",function(){var A=this.placer.selectedPlace;var B;if(A.place_type!="poi"){B=_("in {{full_name}}")}else{B='<span class="place_icon">&nbsp;</span>'+_("at {{full_name}}")}$("#place_id").val(A.id);$("#place_name").html(Mustache.to_html(B,A));this.$placesList.children("li.selected").removeClass("selected");$("#place_"+A.id).addClass("selected")}).method(voidMenu",function(){var B=this;var A=$("#place_link");var C=$("#place_link").position();B.$placesList.css({left:C.left,top:C.top+A.outerHeight()}).show();$("html").one("click",function(){B.closeMenu()});twttr.geo.geoScribe(void_places_dropdown",B.placer.getState())}).method("closeMenu",function(){this.$placesList.hide()});twttr.klass("twttr.geo.PlaceSearchDialog",function(E,B,H){var F=this;F.opts=B;F.onPlaceAccepted=H;F.place=E.selectedPlace;F.placer=E;F.city=twttr.geo.getBestCity(F.place);F.originalCity=F.city;F.placeHeading="{{_i}}Where are you?{{/i}}";F.placeString=(F.place&&F.place.place_type!="city")?F.place.name:"";F.originalPlaceString=F.placeString;F.placeHtml='<form id="place_search_form"><table class="geo_place_search_table"><tr><td class="geo_place_search_col1 geo_place_search_city">{{_i}}City{{/i}}</td><td class="geo_place_search_city">{{city}}&#32;&nbsp;<a href="#" id="change_city">{{_i}}Change{{/i}}</a></td></tr><tr><td class="geo_place_search_col1 geo_place_search_place">{{_i}}Place Name{{/i}}</td><td class="geo_place_search_place"><input id="place_search_query" type="text" autocomplete="off" class="round-left help-focusable" title="{{_i}}Type the name of a place{{/i}}"/><span class="place_search_submit round-right" title="{{_i}}Search{{/i}}">&nbsp;</span><ul class="round places_list place_search_dropdown"></ul></td></tr><tr><td></td><td class="geo_place_search_hint">{{_i}}Optional{{/i}}</td></tr><tr><td></td><td><div id="place_search_results"></div><button type="button" class="btn" id="place_search_done">{{_i}}Done{{/i}}</button><button type="button" class="btn hidden" id="place_search_cancel">{{_i}}Cancel{{/i}}</button></td></tr></table></form>';F.cityHeading="{{_i}}Change City{{/i}}</a>";F.cityHtml='<form id="place_search_form"><table class="geo_place_search_table"><tr><td class="geo_place_search_col1 geo_place_search_place">{{_i}}City{{/i}}</td><td class="geo_place_search_place"><input id="place_search_query" type="text" autocomplete="off" class="round-left help-focusable" title="{{_i}}Enter a city, state{{/i}}"/><span class="place_search_submit round-right" title="{{_i}}Search{{/i}}">&nbsp;</span><ul class="round places_list place_search_dropdown"></ul></td></tr><tr><td></td><td><div id="place_search_results"></div></td></tr></table></form>';F.noCityHeading="{{_i}}Where are you?{{/i}}</a>";$(".place_search_dialog").remove();F.dialog=new twttr.dialog({content:$("<div/>").appendTo("body"),heading:$('<div><span id="geo_search_places_title"></span></div>'),footer:null,cssClass:"place_search_dialog",closeButton:true,renderInline:true,modal:true,fixed:false});var D=E.opts.geoParams;F.searchParams={max_results:twttr.geo.MAX_PLACE_SEARCH_RESULTS,granularity:D.granularity};if(E.detectedPlace&&F.city.id==twttr.geo.getBestCity(E.detectedPlace).id&&D.lat!==undefined&&D.lon!==undefined&&D.accuracy!==undefined){twttr.merge(F.searchParams,{lat:D.lat,lon:D.lon,accuracy:D.accuracy})}else{F.setSearchParamsFromCity()}F.setCityMode(false);var G=$("#place_search_results");G.append("<ul/>");for(var C=0,A=G.find("ul");C<twttr.geo.MAX_PLACE_SEARCH_RESULTS;C++){A.append("<li>&nbsp;</li>")}F.void();G.empty();$("#place_search_query").focus().select()}).method("setHeadingAndContent",function(A,B){$("#geo_search_places_title").html(Mustache.to_html(A));$(".place_search_dialog .modal-content").html(Mustache.to_html(B,{city:this.city.full_name}))}).method("scribeSearch",function(B,C,A){var D=this;twttr.geo.geoScribe(B,$.extend({query:$("#place_search_query").helpVal(),mode:C?"city":"place",container_id:D.city.id,place_creation_allowed:D.shouldAllowPlaceCreation(C)},D.searchParams,A))}).method("setCityMode",function(B){var C=this;if(C.city.full_name==""){C.setHeadingAndContent(C.noCityHeading,C.cityHtml);B=true}else{if(B){C.setHeadingAndContent(C.cityHeading,C.cityHtml)}else{C.setHeadingAndContent(C.placeHeading,C.placeHtml)}}var A=$("#place_search_query");if(B){A.val(C.city.full_name)}else{A.val(C.placeString)}A.helpText().selectOnClick();if(twttr.geo.options.autocomplete){C.placeAutocomplete=new twttr.autocomplete({$input:A,$dropdown:$(".place_search_dropdown"),getInputVal:function(){return A.helpVal()},fetchMatches:function(E,G,F){twttr.api.search({data:twttr.merge({},C.searchParams,{query:E,autocomplete:"true",max_results:twttr.geo.MAX_PLACE_AUTOCOMPLETE_RESULTS,granularity:B?"city":C.searchParams.granularity}),success:function(H,I){G(H.result.places)},error:function(){F()}})},renderMatch:function(F,E,G){return twttr.geo.renderPlace(twttr.geo.templates.autocompleteItem,F).click(function(I){var H=[];G.forEach(function(J){H.push(J.id)});C.placeAccepted(F,B);C.scribeSearch("place_search_dialog_select_autocomplete",B,{selected_id:F.id,selected_index:E,place_ids:H})})},delay:twttr.geo.options.autocomplete_zero_delay?0:twttr.geo.PLACE_SEARCH_AUTOCOMPLETE_DELAY})}var D=$("#place_search_form");D.submit(function(E){E.preventDefault();var F=A.helpVal();if(C.placeAutocomplete){C.placeAutocomplete.hide()}if(!F){C.placeAccepted(C.city,B);return }if(B&&F==C.city.full_name){C.setCityMode(false);return }C.setWaitCursor(true);$("#place_search_done").hide();$("#place_search_cancel").show();$("#place_search_results").html(Mustache.to_html('<div class="geo_search_message">{{_i}}Searching for "{{query}}"...{{/i}}</div>',{query:F})).show();twttr.api.search({data:twttr.merge({},C.searchParams,{query:F,granularity:B?"city":C.searchParams.granularity}),success:function(G){C.setWaitCursor(false);C.searchResultPlaces=G.result.places;twttr.geo.mapsUI.loaddisabledMaps(function(){C.displayResults(0,B)})},error:function(G,I,H){C.setWaitCursor(false);C.displaySearchError(G,B)}})});$(".place_search_submit").click(function(){C.searchTrigger="click_icon";D.submit()});A.keydown(function(E){if(E.keyCode==13){C.searchTrigger="enter_key";E.preventDefault();D.submit()}});$("#place_search_done").click(function(){C.searchTrigger="click_done";var E=A.helpVal();if(C.place&&C.place.name==E&&C.city==C.originalCity){C.placeAccepted(C.place,B);C.scribeSearch("place_search_dialog_done_no_changes",B)}else{D.submit()}});$("#place_search_cancel").click(function(){C.dialog.close();C.scribeSearch("place_search_dialog_close",B,{triggered_by:"click_cancel"})});$("#change_city").click(function(E){E.preventDefault();C.placeString=A.helpVal();C.setCityMode(true);C.scribeSearch("place_search_dialog_change_city")});A.focus().select()}).method("setWaitCursor",function(A){$(".place_search_submit").toggleClass("loaddisableding",A)}).method("shouldAllowPlaceCreation",function(A){return !A&&this.opts.place_creation&&twttr.geo.mapsUI.mapsAvailable()&&(!this.opts.place_creation_needs_high_accuracy||this.searchParams.accuracy!==undefined&&this.searchParams.accuracy<=twttr.geo.PLACE_CREATION_ACCURACY_THRESHOLD)&&this.city.country_code=="US"}).method("displayResults",function(I,G){var E=this;var B=I*twttr.geo.PLACE_SEARCH_RESULTS_PER_PAGE;var F=B+twttr.geo.PLACE_SEARCH_RESULTS_PER_PAGE;var A=E.searchResultPlaces.slice(B,F);var J=$("#place_search_results").empty();var H=$("#place_search_query").helpVal();if(A.length==0){J.html(Mustache.to_html('<div class="geo_search_message">{{_i}}We couldn\'t find "{{query}}."{{/i}}</div>',{query:H}))}var C=[];var D=$("<ul/>").appendTo(J);A.forEach(function(K,L){C.push(K.id);var M=twttr.geo.renderPlace(twttr.geo.templates.searchResultItem,K);M.find("a").click(function(N){N.preventDefault();E.scribeSearch("place_search_dialog_select_result",G,{place_ids:C,selected_id:K.id,selected_index:L});E.placeAccepted(K,G)}).attr("title",twttr.geo.getPlaceDetails(K));D.append(M)});if(E.searchResultPlaces.length>twttr.geo.PLACE_SEARCH_RESULTS_PER_PAGE){J.append(Mustache.to_html('<div class="geo_next_prev"><a href="#" id="geo_prev_result">&laquo;&nbsp;{{_i}}Prev{{/i}}</a><a href="#" id="geo_next_result">{{_i}}Next{{/i}}&nbsp;&raquo;</a></div>'));E.setNextPrev($("#geo_prev_result"),I>0,I-1,G);E.setNextPrev($("#geo_next_result"),F<E.searchResultPlaces.length,I+1,G)}if(E.shouldAllowPlaceCreation(G)){J.append(Mustache.to_html("<div class='geo_add_place'>{{#found}}{{_i}}Not what you're looking for?{{/i}}&#32;{{/found}}<a href='#'>{{_i}}Add this place!{{/i}}</a></div>",{found:A.length>0}));$(".geo_add_place a").click(function(K){K.preventDefault();E.dialog.close();new twttr.geo.PlaceCreationDialog(E.city,H,E.searchParams,function(L){E.placeAccepted(L,false)},E.opts.queryParams.accuracyring!==undefined);E.scribeSearch("place_search_dialog_add",G,{place_ids:C})})}E.scribeSearch("place_search_dialog_show_results",G,{place_ids:C,triggered_by:E.searchTrigger})}).method("setNextPrev",function(A,D,C,B){var E=this;A.click(function(F){F.preventDefault();if(D){E.displayResults(C,B)}});if(!D){A.addClass("link-disabled")}}).method("placeAccepted",function(A,B){var C=this;if(B){if(!A.id){C.dialog.close()}else{if(C.city.id!=A.id){C.city=A;if(C.placeString!=C.originalPlaceString){C.placeString=""}C.setSearchParamsFromCity()}}C.setCityMode(false)}else{C.dialog.close();if(C.originalCity!=C.city){C.placer.selectPlace(C.city)}C.placer.selectPlace(A);C.onPlaceAccepted()}}).method("setSearchParamsFromCity",function(){var A=this;if(A.city&&A.city.id){delete A.searchParams.lat;delete A.searchParams.lon;delete A.searchParams.accuracy;A.searchParams.contained_within=A.city.id}}).method("displaySearchError",function(B,A){$("#place_search_results").text(_("Sorry, search is temporarily unavailable, please try again later."));this.scribeSearch("place_search_dialog_error",A)});twttr.klass("twttr.geo.PlaceCreationDialog",function(C,F,D,E,B){var G=this;G.container=C;if(D.contained_within!==undefined){var A=twttr.geo.getLocationFromPlace(C);if(A){G.lat=A.latitude;G.lon=A.longitude;G.accuracy=A.accuracy}}else{G.lat=D.lat;G.lon=D.lon;G.accuracy=D.accuracy}G.originalGeoParams=D;G.placeName=F;G.onPlaceCreated=E;G.showAccuracyRing=B;G.draggedPin=false;G.streetAddress="";G.createHtml='<div class="geo_map"><div class="geo_map_canvas"></div><div class="geo_map_hint"><span></span><div class="round">{{_i}}Click and move the pin to edit this location.{{/i}}</div></div></div><div class="geo_place_create"><table cellspacing="0" cellpadding="0"><tr><td class="geo_place_creation_row1 geo_place_search_col1">{{_i}}Name{{/i}}</td><td class="geo_place_creation_row1"><input id="geo_place_name" class="geo_form_input round help-focusable" type="text"title="{{_i}}Type the name of a place{{/i}}"/></td></tr><tr><td class="geo_place_search_col1 geo_place_creation_row2">{{_i}}Address{{/i}}</td><td class="geo_place_creation_row2"><input id="geo_place_address" class="geo_form_input round help-focusable" type="text"title="{{_i}}Optional{{/i}}"/></td></tr><tr><td></td><td><div class="geo_place_creation_hint">{{_i}}"795 Folsom St" or "19th and Urban"{{/i}}</div><div class="geo_place_city">{{_i}}In {{city_name}}{{/i}}</div><div><button type="button" class="btn" id="geo_create_place">{{_i}}Add this Place{{/i}}</button><span id="geo_creating_place">&nbsp;</span></div><div class="error" id="geo_creation_error"></div></td></tr></table></div>';G.similarHtml='<div class="geo_map"><div class="geo_map_canvas" /><div class="geo_map_place_bubble"><span/><div class="round"><div class="geo_place_title" /><div class="geo_place_details" /></div></div></div><div class="geo_place_create"><div>{{_i}}This place may already exist. Did you mean:{{/i}}</div><ul/><div><button type="button" class="btn" id="geo_create_place">{{_i}}No, Just Add this Place{{/i}}</button><a href="#" class="geo_go_back">{{_i}}Go Back{{/i}}</a><span id="geo_creating_place">&nbsp;</span></div><div class="error" id="geo_creation_error"></div></div>';$(".place_creation_dialog").remove();G.dialog=new twttr.dialog({content:$("<div/>").appendTo("body"),heading:$(Mustache.to_html("<div>{{_i}}Add this place.{{/i}}</div>")),footer:null,cssClass:"place_creation_dialog",closeButton:true,renderInline:true,modal:true,fixed:false});G.showCreatePlace()}).method("showCreatePlace",function(){var B=this;$(".place_creation_dialog .modal-content").html(Mustache.to_html(B.createHtml,{city_name:B.container.full_name}));B.inProgress=false;$("#geo_place_name").val(B.placeName).helpText().change(function(C){B.updateState()}).keydown(function(C){setTimeout(function(){B.updateState()},0)});$("#geo_create_place").click(function(){B.placeName=$("#geo_place_name").helpVal();B.streetAddress=$("#geo_place_address").helpVal();B.callSimilarPlaces()});B.void();B.updateMapHeight();B.geoMap=new twttr.geo.Map($(".geo_map_canvas").get(0));B.geoMap.addPoi([B.lon,B.lat],function(C,D){B.lat=C;B.lon=D;B.draggedPin=true});B.geoMap.addAccuracyRing([B.lon,B.lat],B.accuracy,B.showAccuracyRing);B.geoMap.adjustBounds(15);B.updateState();$("#geo_place_address").val(B.streetAddress).helpText().focus();var A=true;setTimeout(function(){if(A){$(".geo_map_hint").fadeTo(700,0.8)}},700);$(".geo_map").mousedown(function(C){A=false;$(".geo_map_hint").fadeOut(200)})}).method("showSimilarPlaces",function(){var B=this;$(".place_creation_dialog .modal-content").html(Mustache.to_html(B.similarHtml));B.inProgress=false;$("#geo_create_place").click(function(){B.callCreatePlace()});$("a.geo_go_back").click(function(D){D.preventDefault();B.showCreatePlace()});var C=$(".geo_place_create ul");B.similarPlaces.forEach(function(D){$placeItem=twttr.geo.renderPlace(twttr.geo.templates.searchResultItem,D);$placeItem.find("a").click(function(E){E.preventDefault();B.dialog.close();B.onPlaceCreated(D)}).attr("title",twttr.geo.getPlaceDetails(D));C.append($placeItem)});B.updateMapHeight();B.geoMap=new twttr.geo.Map($(".geo_map_canvas").get(0));B.geoMap.addPoi([B.lon,B.lat]);$geoMapPlaceBubble=$(".geo_map_place_bubble");var A=$(".geo_map_canvas").position();B.similarPlaces.slice().reverse().forEach(function(E){var D=E.bounding_box.coordinates[0][0];var F=B.geoMap.addFocusablePoint(D,function(H,G){if(H){$geoMapPlaceBubble.find(".geo_place_title").text(E.name);$geoMapPlaceBubble.find(".geo_place_details").text(twttr.geo.getPlaceDetails((E)));$geoMapPlaceBubble.show().css({left:A.left+G.x-$geoMapPlaceBubble.width()/2,top:A.top+G.y,opacity:0.8})}else{$geoMapPlaceBubble.hide()}});$("#result_place_"+E.id).hover(function(){F(true)},function(){F(false)})});B.geoMap.adjustBounds(15);B.updateState()}).method("getPlaceCreationParams",function(){var B=this;var A={lat:B.lat,lon:B.lon,name:B.placeName,contained_within:B.container.id,accuracy:B.draggedPin?0:B.originalGeoParams.accuracy};if(B.token){A.token=B.token}if(B.streetAddress){A["attribute:street_address"]=B.streetAddress}return A}).method("callSimilarPlaces",function(){var A=this;if(!A.inProgress){A.inProgress=true;A.updateState();twttr.api.similarPlaces({data:A.getPlaceCreationParams(),success:function(B){A.token=B.result.token;A.inProgress=false;A.similarPlaces=B.result.places.slice(0,twttr.geo.MAX_SIMILAR_PLACES);if(A.similarPlaces.length>0&&twttr.geo.options&&twttr.geo.options.show_similar_places){A.showSimilarPlaces()}else{A.callCreatePlace()}},error:function(){A.onCreationError();A.scribeCreatePlace("similar_places_error")}})}}).method("callCreatePlace",function(){var A=this;if(!A.inProgress){A.inProgress=true;A.updateState();twttr.api.createPlace({data:A.getPlaceCreationParams(),success:function(B){A.inProgress=false;A.updateState();A.dialog.close();A.onPlaceCreated(B);A.scribeCreatePlace("success")},error:function(){A.onCreationError();A.scribeCreatePlace("create_place_error")}})}}).method("scribeCreatePlace",function(A){var B=this;twttr.geo.geoScribe("create_place",$.extend({status:A,original_lat:B.originalGeoParams.lat,original_lon:B.originalGeoParams.lon,original_accuracy:B.originalGeoParams.accuracy,dragged_pin:B.draggedPin},B.getPlaceCreationParams()))}).method("onCreationError",function(){this.inProgress=false;$("#geo_creation_error").text(_("We couldn't add this place. Please try again."));this.updateState();this.updateMapHeight();this.geoMap.resize()}).method("updateState",function(){$("#geo_creating_place").toggleClass("geo_spinner",this.inProgress)}).method("updateMapHeight",function(){var D=$(".geo_map_canvas");var A=Math.max(210,$(".geo_place_create").outerHeight());D.height(A);var B=$(".geo_map_hint");var C=D.position();B.css({left:C.left+(D.width()-B.width())/2-1,top:C.top+A/2})});twttr.klass("twttr.geo.Locator",function(B){var A=this;A.position=null;A.locator=null;A.locatorType="none";if(B&&B.lat!==undefined&&B.lon!==undefined){A.locatorType="manual";A.locator={watchPosition:function(C){C({coords:{latitude:B.lat,longitude:B.lon,accuracy:parseInt(B.accuracy)||200}});return 0},clearWatch:function(){}}}else{if(navigator&&navigator.geolocation){A.locatorType="html5";A.locator=navigator.geolocation;if(navigator.userAgent.indexOf("Firefox")!=-1){A.browserGeoPermissionsHelpBannerTemplate=A.templates.firefoxGeoPermissionsHelpBanner}else{if(navigator.userAgent.indexOf("Chrome")!=-1){A.browserGeoPermissionsHelpBannerTemplate=A.templates.chromeGeoPermissionsHelpBanner}else{if(navigator.userAgent.indexOf("MSIE")!=-1&&navigator.userAgent.indexOf("GTB")!=-1){A.browserGeoPermissionsHelpBannerTemplate=A.templates.ieGoogleToolbarGeoPermissionsHelpBanner}}}}else{if(typeof google!="undefined"&&typeof google.gears!="undefined"){A.locatorType="gears";A.locator=google.gears.factory.create("beta.geolocation")}}}}).augmentProto({templates:{firefoxGeoPermissionsHelpBanner:'<div id="geo_browser_help_banner" class="geo_firefox"><div>{{_i}}Before Twitter can get your location...{{/i}}</div><div><img src="httpdisabled://s.twimg.com/a/1302214109/images/geo_browser_help_banner_1.png" />{{_i}}Check "Remember for this site"{{/i}}</div><div><img src="httpdisabled://s.twimg.com/a/1302214109/images/geo_browser_help_banner_2.png" />{{_i}}Click "Share Location"{{/i}}</div></div>',chromeGeoPermissionsHelpBanner:'<div id="geo_browser_help_banner" class="geo_chrome"><div>{{_i}}Click "Allow" to let Twitter get your location.{{/i}}</div></div>',ieGoogleToolbarGeoPermissionsHelpBanner:'<div id="geo_browser_help_banner" class="geo_ie_gtb"><div>{{_i}}Before Twitter can get your location...{{/i}}</div><div><img src="httpdisabled://s.twimg.com/a/1302214109/images/geo_browser_help_banner_1.png" />{{_i}}Check "Remember for this site"{{/i}}</div><div><img src="httpdisabled://s.twimg.com/a/1302214109/images/geo_browser_help_banner_2.png" />{{_i}}Click "Share my location"{{/i}}</div></div>'}}).method("isLocatable",function(){return !!(this.locator)}).method("getLocation",function(A){var C=this;if(!C.isLocatable()){throw new twttr.geo.Exceptions.GeoSupportException()}var B=$.extend({onSuccess:function(D){},onFailure:function(D){},options:{timeout:twttr.geo.LOCATION_TIMEOUT,enableHighAcuracy:true,maximumAge:twttr.geo.LOCATION_MAXIMUM_AGE}},A);if(!C.getBrowserGeoPermissionsHelpBannerSeen()){C.detectBrowserGeoPermissionsBanner()}C.position=null;C.waitForAcceptableId=setTimeout(function(){C.waitForAcceptableId=null;if(C.watchId!=null){C.watchPositionAcceptable(B.onSuccess)}},twttr.geo.ACCEPTABLE_LOCATION_TIMEOUT);C.watchId=null;C.watchId=C.locator.watchPosition(function(D){setTimeout(function(){if(C.watchId!=null){C.watchPositionSuccess(D,B.onSuccess)}},0)},function(D){setTimeout(function(){if(C.watchId!=null){C.watchPositionError(D,B.onFailure)}},0)},B.options)}).method("watchPositionSuccess",function(A,C){var B=this;B.setBrowserGeoPermissionsHelpBannerSeen();B.position={latitude:A.coords.latitude,longitude:A.coords.longitude,accuracy:A.coords.accuracy};if(B.waitForAcceptableId==null||(B.position.accuracy!=undefined&&B.position.accuracy<twttr.geo.ACCEPTABLE_LOCATION_ACCURACY)){B.watchPositionAcceptable(C)}}).method("watchPositionError",function(B,C){var D=this;D.setBrowserGeoPermissionsHelpBannerSeen();var A;if(B.code==B.PERMISSION_DENIED){A=new twttr.geo.Errors.PermissionDeniedError()}else{A=new twttr.geo.Errors.LocationDetectionError()}if(A.fatal){var E=new ShortNotification();E.setMessage(A.msg);E.show()}D.watchPositionCleanup();C(A)}).method("watchPositionAcceptable",function(B){var A=this;if(A.position){A.watchPositionCleanup();B(A.position)}}).method("watchPositionCleanup",function(){var A=this;clearTimeout(A.waitForAcceptableId);A.locator.clearWatch(A.watchId);A.watchId=null}).method("getWindowHeight",function(){return $(window).height()}).method("getBrowserGeoPermissionsHelpBannerSeen",function(){return $.cookie("geo_browser_help_banner")}).method("setBrowserGeoPermissionsHelpBannerSeen",function(){var A=this;if(A.showBrowserGeoPermissionsHelpBanner){A.clearDetectBrowserGeoPermissionsBanner();if(!A.getBrowserGeoPermissionsHelpBannerSeen()){$.cookie("geo_browser_help_banner","1",{expires:3650})}}}).method("showBrowserGeoPermissionsHelpBanner",function(){$("body").append(Mustache.to_html(this.browserGeoPermissionsHelpBannerTemplate))}).method("detectBrowserGeoPermissionsBanner",function(){var B=this;if(B.browserGeoPermissionsHelpBannerTemplate){var A=B.getWindowHeight();this.browserGeoPermissionsHelpTimer=setTimeout(function(){if(B.getWindowHeight()<A){B.showBrowserGeoPermissionsHelpBanner();B.browserGeoPermissionsHelpInterval=setInterval(function(){if(B.getWindowHeight()==A){B.clearDetectBrowserGeoPermissionsBanner()}},twttr.geo.BROWSER_GEO_BANNER_DISAPPEAR_INTERVAL)}},twttr.geo.BROWSER_GEO_BANNER_APPEAR_DELAY)}}).method("clearDetectBrowserGeoPermissionsBanner",function(){clearTimeout(this.browserGeoPermissionsHelpTimer);clearInterval(this.browserGeoPermissionsHelpInterval);$("#geo_browser_help_banner").remove()});twttr.klass("twttr.geo.Placer",function(A){var C=this;C.opts=A;C.places=[];C.detectedParams={};var B=C.opts.geoParams;if(B.lat!==undefined&&B.lon!==undefined){C.detectedParams.lat=parseFloat(B.lat).toFixed(4);C.detectedParams.lon=parseFloat(B.lon).toFixed(4)}if(B.ip!==undefined){C.detectedParams.ip=B.ip}}).augmentProto({PLACE_OVERRIDES_COOKIE:"place_overrides",RECENT_PLACE_COOKIE:"recent_place",MAX_PLACE_OVERRIDES:8,STICKY_RADIUS_IN_KM:0.1}).method("search",function(E,C,B){var A=this;try{twttr.api.search({data:A.opts.geoParams,success:function(F){A.places=F.result.places;A.determinePlaces(E,C)},error:function(F){if(F.status==503){B()}else{C()}}})}catch(D){C()}}).method("determinePlaces",function(C,B){var A=this;if(A.places.every(function(D){if(D.place_type!="poi"){A.detectedPlace=D;return false}return true})){B();return }if(A.getOverrides().every(function(D){if(A.isOverrideCloseToDetected(D)){A.selectPlaceById(D.id,C,B);return false}return true})){A.selectPlace(A.detectedPlace);C()}}).method("selectPlaceById",function(E,D,B){var A=this;if(A.places.every(function(F){if(F.id==E){A.selectPlace(F);D();return false}return true})){try{twttr.api.getPlaceDetails({place_id:E,success:function(F){A.selectPlace(F);D()},error:function(){B()}})}catch(C){B()}}}).method("setOverrides",function(A){if(A!=this.getOverrides()){if(A.length>0){$.cookie(this.PLACE_OVERRIDES_COOKIE,A.map(function(B){return $.param(B)}).join(","),{expires:3650})}else{$.cookie(this.PLACE_OVERRIDES_COOKIE,null)}}}).method("getOverrides",function(){return($.cookie(this.PLACE_OVERRIDES_COOKIE)||"").split(",").filter(function(A){return A!=""}).map(function(A){return twttr.unparam(A)})}).method("selectPlace",function(A){var B=this;if(B.places.every(function(C){if(A.id==C.id){B.selectedPlace=C;return false}return true})){B.places.unshift(A);B.selectedPlace=A}if(B.selectedPlace.place_type!="poi"){B.setOverride();B.setRecentPlaceId(B.selectedPlace.id)}}).method("setOverride",function(){var A=this;var B=A.getOverrides().filter(function(C){return !A.isOverrideCloseToDetected(C)});if(A.detectedPlace&&A.selectedPlace.id!=A.detectedPlace.id){B.unshift(twttr.merge({id:A.selectedPlace.id},A.detectedParams))}A.setOverrides(B.slice(0,A.MAX_PLACE_OVERRIDES))}).method("getRecentPlaceId",function(){return $.cookie(this.RECENT_PLACE_COOKIE)}).method("setRecentPlaceId",function(A){$.cookie(this.RECENT_PLACE_COOKIE,A,{expires:3650})}).method("isOverrideCloseToDetected",function(A){var B=this.detectedParams;if(!B){return false}if(A.lat!==undefined&&A.lon!==undefined&&B.lat!==undefined&&B.lon!==undefined&&twttr.geo.greatCircleDistanceInKm(A.lat,A.lon,B.lat,B.lon)<this.STICKY_RADIUS_IN_KM){return true}return B.ip&&B.ip==A.ip}).method("getState",function(){var A=this;var B={geo_params:A.opts.geoParams,place_ids:[]};var C=[];A.places.forEach(function(D,E){B.place_ids.push(D.id);if(A.selectedPlace&&A.selectedPlace.id==D.id){B.selected_place_index=E}});if(A.selectedPlace){B.selected_place_id=A.selectedPlace.id}if(A.detectedPlace){B.detected_place_id=A.detectedPlace.id}return B});twttr.klass("twttr.geo.NearbyPlacer",function(A){var B=this;B.opts=A;B.places=[]}).method("search",function(D,B){var A=this;try{twttr.api.search({data:twttr.merge({require_activity:true},A.opts.geoParams),success:function(E){A.places=E.result.places;A.determinePlaces(D,B)},error:function(E){B()}})}catch(C){B()}}).method("determinePlaces",function(C,B){var A=this;if(!A.detectedPlace){A.places.every(function(D){if(D.place_type!="poi"){A.detectedPlace=D;return false}return true})}if(!A.selectedPlace){A.selectPlace(A.detectedPlace)}if(A.selectedPlace){A.places.every(function(D,E){if(D.id==A.selectedPlace.id){A.places.splice(E,1);return false}return true});A.fudgePlaceRanking(A.places);C()}else{B()}}).method("fudgePlaceRanking",function(C){var A=C.length;var B=0;for(var D=0;D<A;D++){if(C[D].place_type!="poi"){C.splice(B,0,C.splice(D,1)[0]);B+=1;if(B==2){break}}}}).method("selectPlace",function(B){var C=this;C.selectedPlace=B;if(B!=C.detectedPlace){var A=twttr.geo.getLocationFromPlace(B);C.opts.geoParams.lat=A.latitude;C.opts.geoParams.lon=A.longitude;C.opts.geoParams.accuracy=A.accuracy;delete C.opts.geoParams.contained_by;delete C.opts.geoParams.ip}});twttr.klass("twttr.geo.NearbyActivity",function(A){var B=this;B.$nearbyActivity=$("#side_geo_nearby_activity .sidebar-menu");B.placer=new twttr.geo.NearbyPlacer({geoParams:{granularity:"poi",max_results:30}});if(A){twttr.merge(B.placer.opts.geoParams,A.opts.geoParams,{granularity:"poi",max_results:30});B.placer.detectedPlace=A.detectedPlace;B.placer.selectPlace(A.selectedPlace);B.search()}else{if(twttr.geo.IP){B.placer.opts.geoParams.ip=twttr.geo.IP;B.search()}else{B.showError()}}}).augmentProto({templates:{choose:'<p class="geo_nearby_activity"><small>{{_i}}Find interesting places nearby and see what people are saying there! To get started, tell us where you\'d like to explore:{{/i}}</small></p><p class="geo_nearby_activity"><a href="#" class="geo_nearby_activity_change">{{_i}}Search places...{{/i}}</a></p>',finding:'<p class="geo_nearby_activity geo_find_in_progress"><small>{{_i}}Finding places nearby...{{/i}}</small></p>',places:'<div class="geo_nearby_activity"><small><span class="geo_nearby_activity_header"></span>&#32;<a href="#" class="geo_nearby_activity_change geo_minorlink">{{_i}}Change{{/i}}</a></small><ul/></div>',error:'<p class="geo_nearby_activity"><small>{{_i}}Location service is currently unavailable.{{/i}}&#32;<a href="#" class="geo_nearby_activity_retry">{{_i}}Try again{{/i}}</a></small></p>',nextPrev:'<p class="geo_nearby_activity"><a href="#" class="geo_prev">{{_i}}Prev{{/i}}</a><span class="geo_prev_next_separator">&nbsp;|&nbsp;</span><a href="#" class="geo_next">{{_i}}Next{{/i}}</a></p>',noPlaces:'<p class="geo_nearby_activity"><small>{{_i}}No active places nearby.{{/i}}</small></p>'}}).method("show",function(B,A){var C=this;C.$nearbyActivity.html(Mustache.to_html(B));C.$nearbyActivity.find(".geo_nearby_activity_change").click(function(D){D.preventDefault();new twttr.geo.PlaceSearchDialog(C.placer,{},function(){C.search()})});C.$nearbyActivity.find(".geo_nearby_activity_retry").click(function(D){D.preventDefault();C.search()});$("#na_menu span").hide().filter(A?".with-place":".without-place").show()}).method("search",function(){var A=this;A.show(A.templates.finding);A.placer.search(function(){A.showPlaces(0)},function(){A.showError()})}).method("showError",function(){var A=this;A.show(A.placer.selectedPlace?A.templates.error:A.templates.choose)}).method("showPlaces",function(C){var F=this;F.show(F.templates.places,true);F.$nearbyActivity.find(".geo_nearby_activity_header").append(F.renderPlace(twttr.geo.templates.nearbyActivityHeader,F.placer.selectedPlace));var E=F.placer.places.length;if(E==0){F.$nearbyActivity.append(Mustache.to_html(F.templates.noPlaces));return }var B=C*twttr.geo.PLACE_SEARCH_RESULTS_PER_PAGE;var A=B+twttr.geo.PLACE_SEARCH_RESULTS_PER_PAGE;var D=F.placer.places.slice(B,A);D.forEach(function(G){F.$nearbyActivity.find("ul").append(F.renderPlace(twttr.geo.templates.nearbyActivityItem,G))});if(E>twttr.geo.PLACE_SEARCH_RESULTS_PER_PAGE){F.$nearbyActivity.append(Mustache.to_html(F.templates.nextPrev));F.setNextPrev(F.$nearbyActivity.find(".geo_prev"),C>0,C-1);F.setNextPrev(F.$nearbyActivity.find(".geo_next"),A<E,C+1)}}).method("renderPlace",function(B,A){var C=this;var D=twttr.geo.renderPlace(B,A);D.find("a").attr(twttr.geo.getPlacePageLinkAttrsFromPlace(A)).isSearchLink().click(function(){C.placer.selectPlace(A);C.search()});return D}).method("setNextPrev",function(A,C,B){var D=this;A.click(function(E){E.preventDefault();if(C){D.showPlaces(B)}});if(!C){A.addClass("link-disabled")}});twttr.klass("twttr.geo.UpdateUi",function(B){var A=this;A.opts=twttr.merge({geo_enabled:false,has_dismissed_geo_promo:false,current_user_path:null,granularity:"neighborhood",queryParams:twttr.unparam(window.location.search.substr(1))},twttr.geo.options,B);if(A.opts.queryParams.ip){twttr.geo.IP=A.opts.queryParams.ip}A.locator=new twttr.geo.Locator(A.opts.queryParams);$(".geo_enable_webclient").live("click",function(C){C.preventDefault();A.enableGeoForWebClient()});$(".geo_disable_webclient").live("click",function(C){C.preventDefault();A.disableGeoForWebClient()});if(A.opts.geo_enabled){if($.cookie("geo_webclient")){A.detectLocation(false)}else{A.disableGeoForWebClient();A.initNearbyActivity()}}else{if(!$.cookie("geo_promo_hidden")&&!$("#latest_status.first-tweet").length){A.setGeoStatus('<span class="geo_new">'+_("New!")+"</span> "+_("Add a location to your tweets.")+' <a id="show_geo_dialog" href="#">'+_("Turn it on")+'</a> - <a id="hide_geo_promo" href="#">'+_("No thanks")+"</a>");$("#hide_geo_promo").click(function(C){C.preventDefault();if(!$(this).hasClass("link-disabled")){A.hidePromoDialog();$.cookie("geo_promo_hidden","1",{expires:3650});$("#geo_status").slideUp();twttr.geo.geoScribe("promo_bar_no_thanks")}});$("#show_geo_dialog").click(function(C){C.preventDefault();C.stopPropagation();if(!$(this).hasClass("link-disabled")){A.showPromoDialog();twttr.geo.geoScribe("void")}});if(!A.opts.has_dismissed_geo_promo){A.showPromoDialog();twttr.geo.geoScribe("promo_bar_dialog_pop")}twttr.geo.geoScribe("promo_bar_shown")}A.initNearbyActivity()}}).augmentProto({templates:{disable_geo:' <a href="#" class="geo_disable_webclient"><span>&nbsp;</span></a>'}}).method("initNearbyActivity",function(A){if(twttr.geo.options.nearby_activity&&!this.nearbyActivity){this.nearbyActivity=new twttr.geo.NearbyActivity(A)}}).method("setGeoStatus",function(A){$("#geo_status").html(A)}).method("scribeLocationDetection",function(A,B){twttr.geo.geoScribe("location_detection_complete",$.extend({status:A,locator_type:this.locator.locatorType},B))}).method("lookupPlacesAndShowDropdown",function(B,D){var C=this;var A=new twttr.geo.Placer({geoParams:B});A.search(function(){C.scribeLocationDetection("place_lookup_succeeded",A.getState());C.setGeoStatus('<span id="place_content"></span>'+C.templates.disable_geo);new twttr.geo.PlacesDropdown(A,C.opts);C.initNearbyActivity(A)},function(){C.cannotDetectLocation(D);C.scribeLocationDetection("place_lookup_failed",A.getState())},function(){C.setGeoStatus(_("Location service is currently unavailable.")+' <a href="#" class="geo_enable_webclient">'+_("Try again")+"</a>"+C.templates.disable_geo);C.initNearbyActivity()})}).method("detectLocationByIp",function(B){var A=this;if(twttr.geo.IP){$("#lat").val();$("#lon").val();A.lookupPlacesAndShowDropdown({ip:twttr.geo.IP,accuracy:16000,granularity:A.opts.granularity,max_results:twttr.geo.MAX_NEARBY_PLACES},B)}else{A.scribeLocationDetection("place_lookup_no_ip");A.cannotDetectLocation(B)}}).method("cannotDetectLocation",function(B){var A=this;if(A.opts.allow_set_location_manually){if(B){A.setLocationManually()}else{A.setGeoStatus(_("We couldn't find you!")+' <a href="#" class="geo_add_manual">'+_("Add Location")+"</a>"+A.templates.disable_geo);$(".geo_add_manual").click(function(C){C.preventDefault();A.setLocationManually()})}}else{A.setGeoStatus(_("Unable to associate your coordinates with a place.")+' <a href="#" class="geo_enable_webclient">'+_("Try again")+"</a>"+A.templates.disable_geo)}A.initNearbyActivity()}).method("setLocationManually",function(){var D=this;var C=new twttr.geo.Placer({geoParams:{granularity:D.opts.granularity}});var A=function(){D.setGeoStatusAddYourLocation();new twttr.geo.PlaceSearchDialog(C,D.opts,function(){D.setGeoStatus('<span id="place_content"></span>'+D.templates.disable_geo);new twttr.geo.PlacesDropdown(C,D.opts)});twttr.geo.geoScribe("set_location_manually")};var B=C.getRecentPlaceId();if(B){D.setGeoStatus('<span class="crosshairs">&nbsp;</span><span class="geo_progress">'+_("Getting recent location...")+"</span>"+D.templates.disable_geo);C.selectPlaceById(B,function(){A()},function(){A()})}else{A()}}).method("detectLocation",function(B){$("#lat").val("");$("#lon").val("");$("#place_id").val("");this.setGeoStatus('<span class="crosshairs">&nbsp;</span><span class="geo_progress">'+_("Getting your location...")+"</span>"+this.templates.disable_geo);var A=this;if(!A.locator.isLocatable()||A.opts.queryParams.nodetect!==undefined){A.detectLocationByIp(B)}else{A.locator.getLocation({onSuccess:function(C){$("#lat").val(C.latitude);$("#lon").val(C.longitude);A.lookupPlacesAndShowDropdown({lat:C.latitude,lon:C.longitude,accuracy:C.accuracy,granularity:A.opts.granularity,max_results:twttr.geo.MAX_NEARBY_PLACES},false)},onFailure:function(C){if(!C.fatal){A.detectLocationByIp(B)}else{A.disableGeoForWebClient();A.scribeLocationDetection("denied_by_user")}}})}}).method("showPromoDialog",function(){var B=this;var C=$("#show_geo_dialog");var A=$('<div class="hoverer" id="geo-promo-hoverer"> <div class="hoverer-inner"> <div class="tiny-map"><img src="httpdisabled://s.twimg.com/a/1302214109/images/tiny-map.gif"></div> <h3>'+_("Add a location to your tweets")+'</h3> <div id="geo_dialog_descr">'+_('Ever had something you wanted to share ("fireworks!", "party!", "ice cream truck!", or "quicksand...") that would be better with a location?')+" "+_("By turning on this feature, you can include location information like neighborhood, town, or exact point when you tweet.")+"<br><br>"+_("When you tweet with a location, Twitter stores that location.")+" "+_("You can switch location on/off before each tweet and always have the option to delete your location history.")+' <a id="geo_learn_more" href="httpdisabled://twitter.zendesk.com/forums/26810/entries/78525" target="_blank">'+_("Learn more")+'</a> </div> <div> <button id="geo_turn_location_on" class="btn">'+_("Turn location on")+'</button> <a href="#" id="geo_not_now" class="geo_dialog_close">'+_("Not now")+'</a> </div> </div> <div class="hovercard-divot"></div> </div>').insertAfter(C);twttr.SimplePositioner.setPosition(A,C,{direction:"below",offsets:{below:{top:10,left:-50}},hasContainer:true});$("#show_geo_dialog,#hide_geo_promo").addClass("link-disabled");$("#geo_turn_location_on").click(function(){B.turnLocationOn();twttr.geo.geoScribe("promo_dialog_turn_location_on")});$(".geo_dialog_close").click(function(D){D.preventDefault();B.hidePromoDialog();twttr.geo.geoScribe("promo_dialog_not_now")});$("#geo-promo-hoverer").click(function(D){D.stopPropagation()});$("html").one("click",function(){if($("#geo-promo-hoverer:visible").length>0){B.hidePromoDialog();twttr.geo.geoScribe("promo_dialog_click_outside")}});A.css("visibility","visible")}).method("hidePromoDialog",function(){if(!this.opts.has_dismissed_geo_promo){this.setUserFlag("has_dismissed_geo_promo");this.opts.has_dismissed_geo_promo=true}$("#geo-promo-hoverer").remove();$("#show_geo_dialog,#hide_geo_promo").removeClass("link-disabled")}).method("turnLocationOn",function(){this.setUserFlag("geo_enabled");this.opts.geo_enabled=true;this.hidePromoDialog();this.enableGeoForWebClient()}).method("setUserFlag",function(A){data={authenticity_token:twttr.form_authenticity_token,_method:"put"};data["user["+A+"]"]="1";$.ajax({type:"POST",url:this.opts.current_user_path,data:data})}).method("enableGeoForWebClient",function(){if(!$.cookie("geo_webclient")){$.cookie("geo_webclient","1",{expires:3650})}this.detectLocation(true)}).method("setGeoStatusAddYourLocation",function(){this.setGeoStatus('<span class="crosshairs">&nbsp;</span><a href="#" class="geo_enable_webclient">'+_("Add your location")+"</a>")}).method("disableGeoForWebClient",function(){$("#lat").val("");$("#lon").val("");$("#place_id").val("");if($.cookie("geo_webclient")){$.cookie("geo_webclient",null)}this.setGeoStatusAddYourLocation()});
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/javascripts/twitter.js@1302215522 b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/javascripts/twitter.js@1302215522
new file mode 100755
index 0000000000..2bce4f72a1
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/javascripts/twitter.js@1302215522
@@ -0,0 +1,2435 @@
+/*
+ * Copyright (c) 2007 Josh Bush (digitalbush.com)
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ * Version: Beta 1
+ * Release: 2007-06-01
+ */
+(function($) {
+ var map=new Array();
+ $.Watermark = {
+ ShowAll:function(){
+ for (var i=0;i<map.length;i++){
+ if(map[i].obj.val()==""){
+ map[i].obj.val(map[i].text);
+ map[i].obj.css("color",map[i].WatermarkColor);
+ }else{
+ map[i].obj.css("color",map[i].DefaultColor);
+ }
+ }
+ },
+ HideAll:function(){
+ for (var i=0;i<map.length;i++){
+ if(map[i].obj.val()==map[i].text)
+ map[i].obj.val("");
+ }
+ }
+ }
+
+ $.fn.Watermark = function(text,color) {
+ if(!color)
+ color="#aaa";
+ return this.each(
+ function(){
+ var input=$(this);
+ var defaultColor=input.css("color");
+ map[map.length]={text:text,obj:input,DefaultColor:defaultColor,WatermarkColor:color};
+ function clearMessage(){
+ if(input.val()==text)
+ input.val("");
+ input.css("color",defaultColor);
+ }
+
+ function insertMessage(){
+ if(input.val().length==0 || input.val()==text){
+ input.val(text);
+ input.css("color",color);
+ }else
+ input.css("color",defaultColor);
+ }
+
+ input.focus(clearMessage);
+ input.blur(insertMessage);
+ input.change(insertMessage);
+
+ insertMessage();
+ }
+ );
+ };
+})(jQuery);
+/*
+ * Cookie plugin
+ *
+ * Copyright (c) 2006 Klaus Hartl (stilbuero.de)
+ * Dual licensed under the MIT and GPL licenses:
+ * http://voidsource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ */
+jQuery.cookie = function(name, value, options) {
+ if (typeof value != 'undefined') { // name and value given, set cookie
+ options = options || {};
+ if (value === null) {
+ value = '';
+ options.expires = -1;
+ }
+ var expires = '';
+ if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) {
+ var date;
+ if (typeof options.expires == 'number') {
+ date = new Date();
+ date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000));
+ } else {
+ date = options.expires;
+ }
+ expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE
+ }
+ // CAUTION: Needed to parenthesize options.path and options.domain
+ // in the following expressions, otherwise they evaluate to undefined
+ // in the packed version for some reason...
+ var path = options.path ? '; path=' + (options.path) : '';
+ var domain = options.domain ? '; domain=' + (options.domain) : '';
+ var secure = options.secure ? '; secure' : '';
+ document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join('');
+ } else { // only name given, get cookie
+ var cookieValue = null;
+ if (document.cookie && document.cookie != '') {
+ var cookies = document.cookie.split(';');
+ for (var i = 0; i < cookies.length; i++) {
+ var cookie = jQuery.trim(cookies[i]);
+ // Does this cookie string begin with the name we want?
+ if (cookie.substring(0, name.length + 1) == (name + '=')) {
+ cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+ break;
+ }
+ }
+ }
+ return cookieValue;
+ }
+};
+/*
+ * jQuery Color Animations
+ * Copyright 2007 John Resig
+ * Released under the MIT and GPL licenses.
+ */
+
+(function(jQuery){
+
+ // We override the animation for all of these color styles
+ jQuery.each(['backgroundColor', 'borderBottomColor', 'borderLeftColor', 'borderRightColor', 'borderTopColor', 'color', 'outlineColor', 'borderColor'], function(i,attr){
+ jQuery.fx.step[attr] = function(fx){
+ if ( fx.state == 0 ) {
+ fx.start = getColor( fx.elem, attr );
+ fx.end = getRGB( fx.end );
+ }
+
+ fx.elem.style[attr] = "rgb(" + [
+ Math.max(Math.min( parseInt((fx.pos * (fx.end[0] - fx.start[0])) + fx.start[0]), 255), 0),
+ Math.max(Math.min( parseInt((fx.pos * (fx.end[1] - fx.start[1])) + fx.start[1]), 255), 0),
+ Math.max(Math.min( parseInt((fx.pos * (fx.end[2] - fx.start[2])) + fx.start[2]), 255), 0)
+ ].join(",") + ")";
+ }
+ });
+
+ // Color Conversion functions from highlightFade
+ // By Blair Mitchelmore
+ // http://jquery.offput.ca/highlightFade/
+
+ // Parse strings looking for color tuples [255,255,255]
+ function getRGB(color) {
+ var result;
+
+ // Check if we're already dealing with an array of colors
+ if ( color && color.constructor == Array && color.length == 3 )
+ return color;
+
+ // Look for rgb(num,num,num)
+ if (result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color))
+ return [parseInt(result[1]), parseInt(result[2]), parseInt(result[3])];
+
+ // Look for rgb(num%,num%,num%)
+ if (result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(color))
+ return [parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55];
+
+ // Look for #a0b1c2
+ if (result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color))
+ return [parseInt(result[1],16), parseInt(result[2],16), parseInt(result[3],16)];
+
+ // Look for #fff
+ if (result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color))
+ return [parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16)];
+
+ // Otherwise, we're most likely dealing with a named color
+ return colors[jQuery.trim(color).toLowerCase()];
+ }
+
+ function getColor(elem, attr) {
+ var color;
+
+ do {
+ color = jQuery.curCSS(elem, attr);
+
+ // Keep going until we find an element that has color, or we hit the body
+ if ( color != '' && color != 'transparent' || jQuery.nodeName(elem, "body") )
+ break;
+
+ attr = "backgroundColor";
+ } while ( elem = elem.parentNode );
+
+ return getRGB(color);
+ };
+
+ // Some named colors to work with
+ // From Interface by Stefan Petre
+ // http://interface.eyecon.ro/
+
+ var colors = {
+ aqua:[0,255,255],
+ azure:[240,255,255],
+ beige:[245,245,220],
+ black:[0,0,0],
+ blue:[0,0,255],
+ brown:[165,42,42],
+ cyan:[0,255,255],
+ darkblue:[0,0,139],
+ darkcyan:[0,139,139],
+ darkgrey:[169,169,169],
+ darkgreen:[0,100,0],
+ darkkhaki:[189,183,107],
+ darkmagenta:[139,0,139],
+ darkolivegreen:[85,107,47],
+ darkorange:[255,140,0],
+ darkorchid:[153,50,204],
+ darkred:[139,0,0],
+ darksalmon:[233,150,122],
+ darkviolet:[148,0,211],
+ fuchsia:[255,0,255],
+ gold:[255,215,0],
+ green:[0,128,0],
+ indigo:[75,0,130],
+ khaki:[240,230,140],
+ lightblue:[173,216,230],
+ lightcyan:[224,255,255],
+ lightgreen:[144,238,144],
+ lightgrey:[211,211,211],
+ lightpink:[255,182,193],
+ lightyellow:[255,255,224],
+ lime:[0,255,0],
+ magenta:[255,0,255],
+ maroon:[128,0,0],
+ navy:[0,0,128],
+ olive:[128,128,0],
+ orange:[255,165,0],
+ pink:[255,192,203],
+ purple:[128,0,128],
+ violet:[128,0,128],
+ red:[255,0,0],
+ silver:[192,192,192],
+ white:[255,255,255],
+ yellow:[255,255,0]
+ };
+
+})(jQuery);
+/* Copyright (c) 2008 Brandon Aaron (http://brandonaaron.net)
+ * Dual licensed under the MIT (http://voidsource.org/licenses/mit-license.php)
+ * and GPL (http://voidsource.org/licenses/gpl-license.php) licenses.
+ *
+ * Version: 1.0.3
+ * Requires jQuery 1.1.3+
+ * Docs: http://docs.jquery.com/Plugins/livequery
+ */
+
+(function($) {
+
+$.extend($.fn, {
+ livequery: function(type, fn, fn2) {
+ var self = this, q;
+
+ // Handle different call patterns
+ if ($.isFunction(type))
+ fn2 = fn, fn = type, type = undefined;
+
+ // See if Live Query already exists
+ $.each( $.livequery.queries, function(i, query) {
+ if ( self.selector == query.selector && self.context == query.context &&
+ type == query.type && (!fn || fn.$lqguid == query.fn.$lqguid) && (!fn2 || fn2.$lqguid == query.fn2.$lqguid) )
+ // Found the query, exit the each loop
+ return (q = query) && false;
+ });
+
+ // Create new Live Query if it wasn't found
+ q = q || new $.livequery(this.selector, this.context, type, fn, fn2);
+
+ // Make sure it is running
+ q.stopped = false;
+
+ // Run it immediately for the first time
+ q.run();
+
+ // Contnue the chain
+ return this;
+ },
+
+ expire: function(type, fn, fn2) {
+ var self = this;
+
+ // Handle different call patterns
+ if ($.isFunction(type))
+ fn2 = fn, fn = type, type = undefined;
+
+ // Find the Live Query based on arguments and stop it
+ $.each( $.livequery.queries, function(i, query) {
+ if ( self.selector == query.selector && self.context == query.context &&
+ (!type || type == query.type) && (!fn || fn.$lqguid == query.fn.$lqguid) && (!fn2 || fn2.$lqguid == query.fn2.$lqguid) && !this.stopped )
+ $.livequery.stop(query.id);
+ });
+
+ // Continue the chain
+ return this;
+ }
+});
+
+$.livequery = function(selector, context, type, fn, fn2) {
+ this.selector = selector;
+ this.context = context || document;
+ this.type = type;
+ this.fn = fn;
+ this.fn2 = fn2;
+ this.elements = [];
+ this.stopped = false;
+
+ // The id is the index of the Live Query in $.livequery.queries
+ this.id = $.livequery.queries.push(this)-1;
+
+ // Mark the functions for matching later on
+ fn.$lqguid = fn.$lqguid || $.livequery.guid++;
+ if (fn2) fn2.$lqguid = fn2.$lqguid || $.livequery.guid++;
+
+ // Return the Live Query
+ return this;
+};
+
+$.livequery.prototype = {
+ stop: function() {
+ var query = this;
+
+ if ( this.type )
+ // Unbind all bound events
+ this.elements.unbind(this.type, this.fn);
+ else if (this.fn2)
+ // Call the second function for all matched elements
+ this.elements.each(function(i, el) {
+ query.fn2.apply(el);
+ });
+
+ // Clear out matched elements
+ this.elements = [];
+
+ // Stop the Live Query from running until restarted
+ this.stopped = true;
+ },
+
+ run: function() {
+ // Short-circuit if stopped
+ if ( this.stopped ) return;
+ var query = this;
+
+ var oEls = this.elements,
+ els = $(this.selector, this.context),
+ nEls = els.not(oEls);
+
+ // Set elements to the latest set of matched elements
+ this.elements = els;
+
+ if (this.type) {
+ // Bind events to newly matched elements
+ nEls.bind(this.type, this.fn);
+
+ // Unbind events to elements no longer matched
+ if (oEls.length > 0)
+ $.each(oEls, function(i, el) {
+ if ( $.inArray(el, els) < 0 )
+ $.event.remove(el, query.type, query.fn);
+ });
+ }
+ else {
+ // Call the first function for newly matched elements
+ nEls.each(function() {
+ query.fn.apply(this);
+ });
+
+ // Call the second function for elements no longer matched
+ if ( this.fn2 && oEls.length > 0 )
+ $.each(oEls, function(i, el) {
+ if ( $.inArray(el, els) < 0 )
+ query.fn2.apply(el);
+ });
+ }
+ }
+};
+
+$.extend($.livequery, {
+ guid: 0,
+ queries: [],
+ queue: [],
+ running: false,
+ timeout: null,
+
+ checkQueue: function() {
+ if ( $.livequery.running && $.livequery.queue.length ) {
+ var length = $.livequery.queue.length;
+ // Run each Live Query currently in the queue
+ while ( length-- )
+ $.livequery.queries[ $.livequery.queue.shift() ].run();
+ }
+ },
+
+ pause: function() {
+ // Don't run anymore Live Queries until restarted
+ $.livequery.running = false;
+ },
+
+ play: function() {
+ // Restart Live Queries
+ $.livequery.running = true;
+ // Request a run of the Live Queries
+ $.livequery.run();
+ },
+
+ registerPlugin: function() {
+ $.each( arguments, function(i,n) {
+ // Short-circuit if the method doesn't exist
+ if (!$.fn[n]) return;
+
+ // Save a reference to the original method
+ var old = $.fn[n];
+
+ // Create a new method
+ $.fn[n] = function() {
+ // Call the original method
+ var r = old.apply(this, arguments);
+
+ // Request a run of the Live Queries
+ $.livequery.run();
+
+ // Return the original methods result
+ return r;
+ }
+ });
+ },
+
+ run: function(id) {
+ if (id != undefined) {
+ // Put the particular Live Query in the queue if it doesn't already exist
+ if ( $.inArray(id, $.livequery.queue) < 0 )
+ $.livequery.queue.push( id );
+ }
+ else
+ // Put each Live Query in the queue if it doesn't already exist
+ $.each( $.livequery.queries, function(id) {
+ if ( $.inArray(id, $.livequery.queue) < 0 )
+ $.livequery.queue.push( id );
+ });
+
+ // Clear timeout if it already exists
+ if ($.livequery.timeout) clearTimeout($.livequery.timeout);
+ // Create a timeout to check the queue and actually run the Live Queries
+ $.livequery.timeout = setTimeout($.livequery.checkQueue, 20);
+ },
+
+ stop: function(id) {
+ if (id != undefined)
+ // Stop are particular Live Query
+ $.livequery.queries[ id ].stop();
+ else
+ // Stop all Live Queries
+ $.each( $.livequery.queries, function(id) {
+ $.livequery.queries[ id ].stop();
+ });
+ }
+});
+
+// Register core DOM manipulation methods
+$.livequery.registerPlugin('append', 'prepend', 'after', 'before', 'wrap', 'attr', 'removeAttr', 'addClass', 'removeClass', 'toggleClass', 'empty', 'remove');
+
+// Run Live Queries when the Document is ready
+$(function() { $.livequery.play(); });
+
+
+// Save a reference to the original init method
+var init = $.prototype.init;
+
+// Create a new init method that exposes two new properties: selector and context
+$.prototype.init = function(a,c) {
+ // Call the original init and save the result
+ var r = init.apply(this, arguments);
+
+ // Copy over properties if they exist already
+ if (a && a.selector)
+ r.context = a.context, r.selector = a.selector;
+
+ // Set properties
+ if ( typeof a == 'string' )
+ r.context = c || document, r.selector = a;
+
+ // Return the result
+ return r;
+};
+
+// Give the init function the jQuery prototype for later instantiation (needed after Rev 4091)
+$.prototype.init.prototype = $.prototype;
+
+})(jQuery);/*
+ * Metadata - jQuery plugin for parsing metadata from elements
+ *
+ * Copyright (c) 2006 John Resig, Yehuda Katz, J�örn Zaefferer, Paul McLanahan
+ *
+ * Dual licensed under the MIT and GPL licenses:
+ * http://voidsource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Revision: $Id: jquery.metadata.js 3640 2007-10-11 18:34:38Z pmclanahan $
+ *
+ */
+
+/**
+ * Sets the type of metadata to use. Metadata is encoded in JSON, and each property
+ * in the JSON will become a property of the element itself.
+ *
+ * There are four supported types of metadata storage:
+ *
+ * attr: Inside an attribute. The name parameter indicates *which* attribute.
+ *
+ * class: Inside the class attribute, wrapped in curly braces: { }
+ *
+ * elem: Inside a child element (e.g. a script tag). The
+ * name parameter indicates *which* element.
+ * html5: Values are stored in data-* attributes.
+ *
+ * The metadata for an element is loaddisableded the first time the element is accessed via jQuery.
+ *
+ * As a result, you can define the metadata type, use $(expr) to loaddisabled the metadata into the elements
+ * matched by expr, then redefine the metadata type and run another $(expr) for other elements.
+ *
+ * @name $.metadata.setType
+ *
+ * @example <p id="one" class="some_class {item_id: 1, item_label: 'Label'}">This is a p</p>
+ * @before $.metadata.setType("class")
+ * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label"
+ * @desc Reads metadata from the class attribute
+ *
+ * @example <p id="one" class="some_class" data="{item_id: 1, item_label: 'Label'}">This is a p</p>
+ * @before $.metadata.setType("attr", "data")
+ * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label"
+ * @desc Reads metadata from a "data" attribute
+ *
+ * @example <p id="one" class="some_class"><script>{item_id: 1, item_label: 'Label'}</script>This is a p</p>
+ * @before $.metadata.setType("elem", "script")
+ * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label"
+ * @desc Reads metadata from a nested script element
+ *
+ * @example <p id="one" class="some_class" data-item_id="1" data-item_label="Label">This is a p</p>
+ * @before $.metadata.setType("html5")
+ * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label"
+ * @desc Reads metadata from a series of data-* attributes
+ *
+ * @param String type The encoding type
+ * @param String name The name of the attribute to be used to get metadata (optional)
+ * @cat Plugins/Metadata
+ * @descr Sets the type of encoding to be used when loaddisableding metadata for the first time
+ * @type undefined
+ * @see metadata()
+ */
+
+(function($) {
+
+$.extend({
+ metadata : {
+ defaults : {
+ type: 'class',
+ name: 'metadata',
+ cre: /({.*})/,
+ single: 'metadata'
+ },
+ setType: function( type, name ){
+ this.defaults.type = type;
+ this.defaults.name = name;
+ },
+ get: function( elem, opts ){
+ var settings = $.extend({},this.defaults,opts);
+ // check for empty string in single property
+ if ( !settings.single.length ) settings.single = 'metadata';
+
+ var data = $.data(elem, settings.single);
+ // returned cached data if it already exists
+ if ( data ) return data;
+
+ data = "{}";
+
+ var getData = function(data) {
+ if(typeof data != "string") return data;
+
+ if( data.indexOf('{') < 0 ) {
+ data = eval("(" + data + ")");
+ }
+ }
+
+ var getObject = function(data) {
+ if(typeof data != "string") return data;
+
+ data = eval("(" + data + ")");
+ return data;
+ }
+
+ if ( settings.type == "html5" ) {
+ var object = {};
+ $( elem.attributes ).each(function() {
+ var name = this.nodeName;
+ if(name.match(/^data-/)) name = name.replace(/^data-/, '');
+ else return true;
+ object[name] = getObject(this.nodeValue);
+ });
+ } else {
+ if ( settings.type == "class" ) {
+ var m = settings.cre.exec( elem.className );
+ if ( m )
+ data = m[1];
+ } else if ( settings.type == "elem" ) {
+ if( !elem.getElementsByTagName ) return;
+ var e = elem.getElementsByTagName(settings.name);
+ if ( e.length )
+ data = $.trim(e[0].innerHTML);
+ } else if ( elem.getAttribute != undefined ) {
+ var attr = elem.getAttribute( settings.name );
+ if ( attr )
+ data = attr;
+ }
+ object = getObject(data.indexOf("{") < 0 ? "{" + data + "}" : data);
+ }
+
+ $.data( elem, settings.single, object );
+ return object;
+ }
+ }
+});
+
+/**
+ * Returns the metadata object for the first member of the jQuery object.
+ *
+ * @name metadata
+ * @descr Returns element's metadata object
+ * @param Object opts An object contianing settings to override the defaults
+ * @type jQuery
+ * @cat Plugins/Metadata
+ */
+$.fn.metadata = function( opts ){
+ return $.metadata.get( this[0], opts );
+};
+
+})(jQuery);//Licensed under The MIT License
+//Copyright (c) 2008 Jason Frame (jason@onehackoranother.com)
+
+
+(function($) {
+ $.fn.tipsy = function(opts) {
+
+ opts = $.extend({fade: false, gravity: 'n'}, opts || {});
+ // ...Added by andy@twitter.com 20090717
+ if(!opts['offsetTop']) { opts['offsetTop'] = 0; }
+ if(!opts['offsetLeft']) { opts['offsetLeft'] = 0; }
+ if(!opts['header']) { opts['header'] = ''; }
+ if(!opts['footer']) { opts['footer'] = ''; }
+ if(!opts['hideTimeout']) { opts['hideTimeout'] = 100; }
+ if(!opts['showTimeout']) { opts['hideTimeout'] = 0; }
+ if(!opts['additionalCSSClass']) { opts['additionalCSSClass'] = ''; }
+ var showTimeoutKey = false;
+ // ...Added by andy@twitter.com 20090717
+ var tip = null, cancelHide = false;
+ this.hover(function() {
+
+ // ...Added by andy@twitter.com 20090717
+ var linkText = $(this).text();
+ var header = opts['header'].replace('%{link}', linkText);
+ var footer = opts['footer'].replace('%{link}', linkText);
+ // ...Added by andy@twitter.com 20090717
+
+ $.data(this, 'cancel.tipsy', true);
+
+ var tip = $.data(this, 'active.tipsy');
+ if (!tip) {
+ $('.tipsy').hide();
+ tip = $('<div class="tipsy '+ opts['additionalCSSClass'] +'"><div class="tipsy-inner">' + header + $(this).attr('title') + footer + '</div></div>');
+ tip.css({position: 'absolute', zIndex: 100000});
+ $(this).attr('title', '');
+ $.data(this, 'active.tipsy', tip);
+ // Added by rael@twitter.com 20090628...
+ } else if ($(this).attr('title') != '') {
+ tip.find('.tipsy-inner').html($(this).attr('title'));
+ $(this).attr('title', '');
+ // ...Added by rael@twitter.com 20090628
+ }
+
+ var pos = $.extend({}, $(this).offset(), {width: this.offsetWidth, height: this.offsetHeight});
+ // ...Added by andy@twitter.com 20090717
+ pos.top = pos.top + opts['offsetTop'];
+ pos.left = pos.left + opts['offsetLeft'];
+
+ // void tips if timeout to fade
+ $('.tipsy').hide();
+ // ...Added by andy@twitter.com 20090717
+ tip.remove().css({top: 0, left: 0, visibility: 'hidden', display: 'block'}).appendTo(document.body);
+ var actualWidth = tip[0].offsetWidth, actualHeight = tip[0].offsetHeight;
+
+ switch (opts.gravity.charAt(0)) {
+ case 'n':
+ tip.css({top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2}).addClass('tipsy-north');
+ break;
+ case 'l':
+ //left north align
+ tip.css({top: pos.top + pos.height, left: pos.left + pos.width / 2 - 18}).addClass('tipsy-north');
+ break;
+ case 's':
+ tip.css({top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2}).addClass('tipsy-south');
+ break;
+ case 'e':
+ tip.css({top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth}).addClass('tipsy-east');
+ break;
+ case 'w':
+ tip.css({top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width}).addClass('tipsy-west');
+ break;
+ }
+ // ...Added by andy@twitter.com 20090717
+ function show() {
+ if (opts.fade) {
+ tip.css({opacity: 0, display: 'block', visibility: 'visible'}).animate({opacity: 1});
+ } else {
+ tip.css({visibility: 'visible'});
+ }
+ }
+ if(opts['showTimeout']) {
+ showTimeoutKey = setTimeout(show, opts['showTimeout']);
+ } else {
+ show();
+ }
+ }, function() {
+ clearTimeout(showTimeoutKey);
+ // ...Added by andy@twitter.com 20090717
+ $.data(this, 'cancel.tipsy', false);
+ var self = this;
+ setTimeout(function() {
+ if ($.data(this, 'cancel.tipsy')) return;
+ var tip = $.data(self, 'active.tipsy');
+ if (opts.fade) {
+ tip.stop().fadeOut(function() { $(this).remove(); });
+ } else {
+ tip.remove();
+ }
+ }, opts['hideTimeout']);
+ });
+
+ };
+})(jQuery);
+/*
+ * jQuery Form Plugin
+ * version: 2.36 (07-NOV-2009)
+ * @requires jQuery v1.2.6 or later
+ *
+ * Examples and documentation at: http://malsup.com/jquery/form/
+ * Dual licensed under the MIT and GPL licenses:
+ * http://voidsource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ */
+;(function($) {
+
+/*
+ Usage Note:
+ -----------
+ Do not use both ajaxSubmit and ajaxForm on the same form. These
+ functions are intended to be exclusive. Use ajaxSubmit if you want
+ to bind your own submit handler to the form. For example,
+
+ $(document).ready(function() {
+ $('#myForm').bind('submit', function() {
+ $(this).ajaxSubmit({
+ target: '#output'
+ });
+ return false; // <-- important!
+ });
+ });
+
+ Use ajaxForm when you want the plugin to manage all the event binding
+ for you. For example,
+
+ $(document).ready(function() {
+ $('#myForm').ajaxForm({
+ target: '#output'
+ });
+ });
+
+ When using ajaxForm, the ajaxSubmit function will be invoked for you
+ at the appropriate time.
+*/
+
+/**
+ * ajaxSubmit() provides a mechanism for immediately submitting
+ * an HTML form using AJAX.
+ */
+$.fn.ajaxSubmit = function(options) {
+ // fast fail if nothing selected (http://dev.jquery.com/ticket/2752)
+ if (!this.length) {
+ log('ajaxSubmit: skipping submit process - no element selected');
+ return this;
+ }
+
+ if (typeof options == 'function')
+ options = { success: options };
+
+ var url = $.trim(this.attr('action'));
+ if (url) {
+ // clean url (don't include hash vaue)
+ url = (url.match(/^([^#]+)/)||[])[1];
+ }
+ url = url || window.location.href || '';
+
+ options = $.extend({
+ url: url,
+ type: this.attr('method') || 'GET',
+ iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank'
+ }, options || {});
+
+ // hook for manipulating the form data before it is extracted;
+ // convenient for use with rich editors like tinyMCE or FCKEditor
+ var veto = {};
+ this.trigger('form-pre-serialize', [this, options, veto]);
+ if (veto.veto) {
+ log('ajaxSubmit: submit vetoed via form-pre-serialize trigger');
+ return this;
+ }
+
+ // provide opportunity to alter form data before it is serialized
+ if (options.beforeSerialize && options.beforeSerialize(this, options) === false) {
+ log('ajaxSubmit: submit aborted via beforeSerialize callback');
+ return this;
+ }
+
+ var a = this.formToArray(options.semantic);
+ if (options.data) {
+ options.extraData = options.data;
+ for (var n in options.data) {
+ if(options.data[n] instanceof Array) {
+ for (var k in options.data[n])
+ a.push( { name: n, value: options.data[n][k] } );
+ }
+ else
+ a.push( { name: n, value: options.data[n] } );
+ }
+ }
+
+ // give pre-submit callback an opportunity to abort the submit
+ if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) {
+ log('ajaxSubmit: submit aborted via beforeSubmit callback');
+ return this;
+ }
+
+ // fire vetoable 'validate' event
+ this.trigger('form-submit-validate', [a, this, options, veto]);
+ if (veto.veto) {
+ log('ajaxSubmit: submit vetoed via form-submit-validate trigger');
+ return this;
+ }
+
+ var q = $.param(a);
+
+ if (options.type.toUpperCase() == 'GET') {
+ options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q;
+ options.data = null; // data is null for 'get'
+ }
+ else
+ options.data = q; // data is the query string for 'post'
+
+ var $form = this, callbacks = [];
+ if (options.resetForm) callbacks.push(function() { $form.resetForm(); });
+ if (options.clearForm) callbacks.push(function() { $form.clearForm(); });
+
+ // perform a loaddisabled on the target only if dataType is not provided
+ if (!options.dataType && options.target) {
+ var oldSuccess = options.success || function(){};
+ callbacks.push(function(data) {
+ $(options.target).html(data).each(oldSuccess, arguments);
+ });
+ }
+ else if (options.success)
+ callbacks.push(options.success);
+
+ options.success = function(data, status) {
+ for (var i=0, max=callbacks.length; i < max; i++)
+ callbacks[i].apply(options, [data, status, $form]);
+ };
+
+ // are there files to uploaddisabled?
+ var files = $('input:file', this).fieldValue();
+ var found = false;
+ for (var j=0; j < files.length; j++)
+ if (files[j])
+ found = true;
+
+ var multipart = false;
+// var mp = 'multipart/form-data';
+// multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp);
+
+ // options.iframe allows user to force iframe mode
+ // 06-NOV-09: now defaulting to iframe mode if file input is detected
+ if ((files.length && options.iframe !== false) || options.iframe || found || multipart) {
+ // hack to fix Safari hang (thanks to Tim Molendijk for this)
+ // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d
+ if (options.closeKeepAlive)
+ $.get(options.closeKeepAlive, fileUploaddisabled);
+ else
+ fileUploaddisabled();
+ }
+ else
+ $.ajax(options);
+
+ // fire 'notify' event
+ this.trigger('form-submit-notify', [this, options]);
+ return this;
+
+
+ // private function for handling file uploaddisableds (hat tip to YAHOO!)
+ function fileUploaddisabled() {
+ var form = $form[0];
+
+ if ($(':input[name=submit]', form).length) {
+ alert('Error: Form elements must not be named "submit".');
+ return;
+ }
+
+ var opts = $.extend({}, $.ajaxSettings, options);
+ var s = $.extend(true, {}, $.extend(true, {}, $.ajaxSettings), opts);
+
+ var id = 'jqFormIO' + (new Date().getTime());
+ var $io = $('<iframe id="' + id + '" name="' + id + '" src="'+ opts.iframeSrc +'" />');
+ var io = $io[0];
+
+ $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });
+
+ var xhr = { // mock object
+ aborted: 0,
+ responseText: null,
+ responseXML: null,
+ status: 0,
+ statusText: 'n/a',
+ getAllResponseHeaders: function() {},
+ getResponseHeader: function() {},
+ setRequestHeader: function() {},
+ abort: function() {
+ this.aborted = 1;
+ $io.attr('src', opts.iframeSrc); // abort op in progress
+ }
+ };
+
+ var g = opts.global;
+ // trigger ajax global events so that activity/block indicators work like normal
+ if (g && ! $.active++) $.event.trigger("ajaxStart");
+ if (g) $.event.trigger("ajaxSend", [xhr, opts]);
+
+ if (s.beforeSend && s.beforeSend(xhr, s) === false) {
+ s.global && $.active--;
+ return;
+ }
+ if (xhr.aborted)
+ return;
+
+ var cbInvoked = 0;
+ var timedOut = 0;
+
+ // add submitting element to data if we know it
+ var sub = form.clk;
+ if (sub) {
+ var n = sub.name;
+ if (n && !sub.disabled) {
+ options.extraData = options.extraData || {};
+ options.extraData[n] = sub.value;
+ if (sub.type == "image") {
+ options.extraData[name+'.x'] = form.clk_x;
+ options.extraData[name+'.y'] = form.clk_y;
+ }
+ }
+ }
+
+ // take a breath so that pending repaints get some cpu time before the uploaddisabled starts
+ setTimeout(function() {
+ // make sure form attrs are set
+ var t = $form.attr('target'), a = $form.attr('action');
+
+ // update form attrs in IE friendly way
+ form.setAttribute('target',id);
+ if (form.getAttribute('method') != 'POST')
+ form.setAttribute('method', 'POST');
+ if (form.getAttribute('action') != opts.url)
+ form.setAttribute('action', opts.url);
+
+ // ie borks in some cases when setting encoding
+ if (! options.skipEncodingOverride) {
+ $form.attr({
+ encoding: 'multipart/form-data',
+ enctype: 'multipart/form-data'
+ });
+ }
+
+ // support timout
+ if (opts.timeout)
+ setTimeout(function() { timedOut = true; cb(); }, opts.timeout);
+
+ // add "extra" data to form if provided in options
+ var extraInputs = [];
+ try {
+ if (options.extraData)
+ for (var n in options.extraData)
+ extraInputs.push(
+ $('<input type="hidden" name="'+n+'" value="'+options.extraData[n]+'" />')
+ .appendTo(form)[0]);
+
+ // add iframe to doc and submit the form
+ $io.appendTo('body');
+ io.attachEvent ? io.attachEvent('onloaddisabled', cb) : io.addEventListener('loaddisabled', cb, false);
+ form.submit();
+ }
+ finally {
+ // reset attrs and remove "extra" input elements
+ form.setAttribute('action',a);
+ t ? form.setAttribute('target', t) : $form.removeAttr('target');
+ $(extraInputs).remove();
+ }
+ }, 10);
+
+ var domCheckCount = 50;
+
+ function cb() {
+ if (cbInvoked++) return;
+
+ io.detachEvent ? io.detachEvent('onloaddisabled', cb) : io.removeEventListener('loaddisabled', cb, false);
+
+ var ok = true;
+ try {
+ if (timedOut) throw 'timeout';
+ // extract the server response from the iframe
+ var data, doc;
+
+ doc = io.contentWindow ? io.contentWindow.document : io.contentDocument ? io.contentDocument : io.document;
+
+ var isXml = opts.dataType == 'xml' || doc.XMLDocument || $.isXMLDoc(doc);
+ log('isXml='+isXml);
+ if (!isXml && (doc.body == null || doc.body.innerHTML == '')) {
+ if (--domCheckCount) {
+ // in some browsers (Opera) the iframe DOM is not always traversable when
+ // the onloaddisabled callback fires, so we loop a bit to accommodate
+ cbInvoked = 0;
+ setTimeout(cb, 100);
+ return;
+ }
+ log('Could not access iframe DOM after 50 tries.');
+ return;
+ }
+
+ xhr.responseText = doc.body ? doc.body.innerHTML : null;
+ xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;
+ xhr.getResponseHeader = function(header){
+ var headers = {'content-type': opts.dataType};
+ return headers[header];
+ };
+
+ if (opts.dataType == 'json' || opts.dataType == 'script') {
+ // see if user embedded response in textarea
+ var ta = doc.getElementsByTagName('textarea')[0];
+ if (ta)
+ xhr.responseText = ta.value;
+ else {
+ // account for browsers injecting pre around json response
+ var pre = doc.getElementsByTagName('pre')[0];
+ if (pre)
+ xhr.responseText = pre.innerHTML;
+ }
+ }
+ else if (opts.dataType == 'xml' && !xhr.responseXML && xhr.responseText != null) {
+ xhr.responseXML = toXml(xhr.responseText);
+ }
+ data = $.httpData(xhr, opts.dataType);
+ }
+ catch(e){
+ ok = false;
+ $.handleError(opts, xhr, 'error', e);
+ }
+
+ // ordering of these callbacks/triggers is odd, but that's how $.ajax does it
+ if (ok) {
+ opts.success(data, 'success');
+ if (g) $.event.trigger("ajaxSuccess", [xhr, opts]);
+ }
+ if (g) $.event.trigger("ajaxComplete", [xhr, opts]);
+ if (g && ! --$.active) $.event.trigger("ajaxStop");
+ if (opts.complete) opts.complete(xhr, ok ? 'success' : 'error');
+
+ // clean up
+ setTimeout(function() {
+ $io.remove();
+ xhr.responseXML = null;
+ }, 100);
+ };
+
+ function toXml(s, doc) {
+ if (window.ActiveXObject) {
+ doc = new ActiveXObject('Microsoft.XMLDOM');
+ doc.async = 'false';
+ doc.loaddisabledXML(s);
+ }
+ else
+ doc = (new DOMParser()).parseFromString(s, 'text/xml');
+ return (doc && doc.documentElement && doc.documentElement.tagName != 'parsererror') ? doc : null;
+ };
+ };
+};
+
+/**
+ * ajaxForm() provides a mechanism for fully automating form submission.
+ *
+ * The advantages of using this method instead of ajaxSubmit() are:
+ *
+ * 1: This method will include coordinates for <input type="image" /> elements (if the element
+ * is used to submit the form).
+ * 2. This method will include the submit element's name/value data (for the element that was
+ * used to submit the form).
+ * 3. This method binds the submit() method to the form for you.
+ *
+ * The options argument for ajaxForm works exactly as it does for ajaxSubmit. ajaxForm merely
+ * passes the options argument along after properly binding events for submit elements and
+ * the form itself.
+ */
+$.fn.ajaxForm = function(options) {
+ return this.ajaxFormUnbind().bind('submit.form-plugin', function() {
+ $(this).ajaxSubmit(options);
+ return false;
+ }).bind('click.form-plugin', function(e) {
+ var target = e.target;
+ var $el = $(target);
+ if (!($el.is(":submit,input:image"))) {
+ // is this a child element of the submit el? (ex: a span within a button)
+ var t = $el.closest(':submit');
+ if (t.length == 0)
+ return;
+ target = t[0];
+ }
+ var form = this;
+ form.clk = target;
+ if (target.type == 'image') {
+ if (e.offsetX != undefined) {
+ form.clk_x = e.offsetX;
+ form.clk_y = e.offsetY;
+ } else if (typeof $.fn.offset == 'function') { // try to use dimensions plugin
+ var offset = $el.offset();
+ form.clk_x = e.pageX - offset.left;
+ form.clk_y = e.pageY - offset.top;
+ } else {
+ form.clk_x = e.pageX - target.offsetLeft;
+ form.clk_y = e.pageY - target.offsetTop;
+ }
+ }
+ // clear form vars
+ setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 100);
+ });
+};
+
+// ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm
+$.fn.ajaxFormUnbind = function() {
+ return this.unbind('submit.form-plugin click.form-plugin');
+};
+
+/**
+ * formToArray() gathers form element data into an array of objects that can
+ * be passed to any of the following ajax functions: $.get, $.post, or loaddisabled.
+ * Each object in the array has both a 'name' and 'value' property. An example of
+ * an array for a simple login form might be:
+ *
+ * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
+ *
+ * It is this array that is passed to pre-submit callback functions provided to the
+ * ajaxSubmit() and ajaxForm() methods.
+ */
+$.fn.formToArray = function(semantic) {
+ var a = [];
+ if (this.length == 0) return a;
+
+ var form = this[0];
+ var els = semantic ? form.getElementsByTagName('*') : form.elements;
+ if (!els) return a;
+ for(var i=0, max=els.length; i < max; i++) {
+ var el = els[i];
+ var n = el.name;
+ if (!n) continue;
+
+ if (semantic && form.clk && el.type == "image") {
+ // handle image inputs on the fly when semantic == true
+ if(!el.disabled && form.clk == el) {
+ a.push({name: n, value: $(el).val()});
+ a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
+ }
+ continue;
+ }
+
+ var v = $.fieldValue(el, true);
+ if (v && v.constructor == Array) {
+ for(var j=0, jmax=v.length; j < jmax; j++)
+ a.push({name: n, value: v[j]});
+ }
+ else if (v !== null && typeof v != 'undefined')
+ a.push({name: n, value: v});
+ }
+
+ if (!semantic && form.clk) {
+ // input type=='image' are not found in elements array! handle it here
+ var $input = $(form.clk), input = $input[0], n = input.name;
+ if (n && !input.disabled && input.type == 'image') {
+ a.push({name: n, value: $input.val()});
+ a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
+ }
+ }
+ return a;
+};
+
+/**
+ * Serializes form data into a 'submittable' string. This method will return a string
+ * in the format: name1=value1&amp;name2=value2
+ */
+$.fn.formSerialize = function(semantic) {
+ //hand off to jQuery.param for proper encoding
+ return $.param(this.formToArray(semantic));
+};
+
+/**
+ * Serializes all field elements in the jQuery object into a query string.
+ * This method will return a string in the format: name1=value1&amp;name2=value2
+ */
+$.fn.fieldSerialize = function(successful) {
+ var a = [];
+ this.each(function() {
+ var n = this.name;
+ if (!n) return;
+ var v = $.fieldValue(this, successful);
+ if (v && v.constructor == Array) {
+ for (var i=0,max=v.length; i < max; i++)
+ a.push({name: n, value: v[i]});
+ }
+ else if (v !== null && typeof v != 'undefined')
+ a.push({name: this.name, value: v});
+ });
+ //hand off to jQuery.param for proper encoding
+ return $.param(a);
+};
+
+/**
+ * Returns the value(s) of the element in the matched set. For example, consider the following form:
+ *
+ * <form><fieldset>
+ * <input name="A" type="text" />
+ * <input name="A" type="text" />
+ * <input name="B" type="checkbox" value="B1" />
+ * <input name="B" type="checkbox" value="B2"/>
+ * <input name="C" type="radio" value="C1" />
+ * <input name="C" type="radio" value="C2" />
+ * </fieldset></form>
+ *
+ * var v = $(':text').fieldValue();
+ * // if no values are entered into the text inputs
+ * v == ['','']
+ * // if values entered into the text inputs are 'foo' and 'bar'
+ * v == ['foo','bar']
+ *
+ * var v = $(':checkbox').fieldValue();
+ * // if neither checkbox is checked
+ * v === undefined
+ * // if both checkboxes are checked
+ * v == ['B1', 'B2']
+ *
+ * var v = $(':radio').fieldValue();
+ * // if neither radio is checked
+ * v === undefined
+ * // if first radio is checked
+ * v == ['C1']
+ *
+ * The successful argument controls whether or not the field element must be 'successful'
+ * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
+ * The default value of the successful argument is true. If this value is false the value(s)
+ * for each element is returned.
+ *
+ * Note: This method *always* returns an array. If no valid value can be determined the
+ * array will be empty, otherwise it will contain one or more values.
+ */
+$.fn.fieldValue = function(successful) {
+ for (var val=[], i=0, max=this.length; i < max; i++) {
+ var el = this[i];
+ var v = $.fieldValue(el, successful);
+ if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length))
+ continue;
+ v.constructor == Array ? $.merge(val, v) : val.push(v);
+ }
+ return val;
+};
+
+/**
+ * Returns the value of the field element.
+ */
+$.fieldValue = function(el, successful) {
+ var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
+ if (typeof successful == 'undefined') successful = true;
+
+ if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
+ (t == 'checkbox' || t == 'radio') && !el.checked ||
+ (t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
+ tag == 'select' && el.selectedIndex == -1))
+ return null;
+
+ if (tag == 'select') {
+ var index = el.selectedIndex;
+ if (index < 0) return null;
+ var a = [], ops = el.options;
+ var one = (t == 'select-one');
+ var max = (one ? index+1 : ops.length);
+ for(var i=(one ? index : 0); i < max; i++) {
+ var op = ops[i];
+ if (op.selected) {
+ var v = op.value;
+ if (!v) // extra pain for IE...
+ v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value;
+ if (one) return v;
+ a.push(v);
+ }
+ }
+ return a;
+ }
+ return el.value;
+};
+
+/**
+ * Clears the form data. Takes the following actions on the form's input fields:
+ * - input text fields will have their 'value' property set to the empty string
+ * - select elements will have their 'selectedIndex' property set to -1
+ * - checkbox and radio inputs will have their 'checked' property set to false
+ * - inputs of type submit, button, reset, and hidden will *not* be effected
+ * - button elements will *not* be effected
+ */
+$.fn.clearForm = function() {
+ return this.each(function() {
+ $('input,select,textarea', this).clearFields();
+ });
+};
+
+/**
+ * Clears the selected form elements.
+ */
+$.fn.clearFields = $.fn.clearInputs = function() {
+ return this.each(function() {
+ var t = this.type, tag = this.tagName.toLowerCase();
+ if (t == 'text' || t == 'password' || tag == 'textarea')
+ this.value = '';
+ else if (t == 'checkbox' || t == 'radio')
+ this.checked = false;
+ else if (tag == 'select')
+ this.selectedIndex = -1;
+ });
+};
+
+/**
+ * Resets the form data. Causes all form elements to be reset to their original value.
+ */
+$.fn.resetForm = function() {
+ return this.each(function() {
+ // guard against an input with the name of 'reset'
+ // note that IE reports the reset function as an 'object'
+ if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType))
+ this.reset();
+ });
+};
+
+/**
+ * Enables or disables any matching elements.
+ */
+$.fn.enable = function(b) {
+ if (b == undefined) b = true;
+ return this.each(function() {
+ this.disabled = !b;
+ });
+};
+
+/**
+ * Checks/unchecks any matching checkboxes or radio buttons and
+ * selects/deselects and matching option elements.
+ */
+$.fn.selected = function(select) {
+ if (select == undefined) select = true;
+ return this.each(function() {
+ var t = this.type;
+ if (t == 'checkbox' || t == 'radio')
+ this.checked = select;
+ else if (this.tagName.toLowerCase() == 'option') {
+ var $sel = $(this).parent('select');
+ if (select && $sel[0] && $sel[0].type == 'select-one') {
+ // deselect all other options
+ $sel.find('option').selected(false);
+ }
+ this.selected = select;
+ }
+ });
+};
+
+// helper fn for console logging
+// set $.fn.ajaxSubmit.debug to true to enable debug logging
+function log() {
+ if ($.fn.ajaxSubmit.debug && window.console && window.console.log)
+ window.console.log('[jquery.form] ' + Array.prototype.join.call(arguments,''));
+};
+
+})(jQuery);
+if(!Array.forEach){Array.prototype.forEach=function(D,E){var C=E||window;for(var B=0,A=this.length;B<A;++B){D.call(C,this[B],B,this)}};Array.prototype.map=function(E,F){var D=F||window;var A=[];for(var C=0,B=this.length;C<B;++C){A.push(E.call(D,this[C],C,this))}return A};Array.prototype.filter=function(E,F){var D=F||window;var A=[];for(var C=0,B=this.length;C<B;++C){if(!E.call(D,this[C],C,this)){continue}A.push(this[C])}return A};Array.prototype.every=function(D,E){var C=E||window;for(var B=0,A=this.length;B<A;++B){if(!D.call(C,this[B],B,this)){return false}}return true};Array.prototype.indexOf=function(B,C){var C=C||0;for(var A=0;A<this.length;++A){if(this[A]===B){return A}}return -1}}Array.prototype.contains=function(A){if(Array.contains){return this.contains(A)}return this.indexOf(A)>-1};Array.prototype.insert=function(A){if(!this.contains(A)){this.push(A)}};if(!Array.remove){Array.remove=function(D,C,B){var A=D.slice((B||C)+1||D.length);D.length=C<0?D.length+C:C;return D.push.apply(D,A)}}Function.prototype.method=function(A,B){this.prototype[A]=B;return this};Function.prototype.methods=function(B){for(var A in B){this.method(A,B[A])}return this};Function.prototype.augmentProto=function(A){for(key in A){this.prototype[key]=A[key]}return this};Function.prototype.pBind=function(B){var A=this;return function(){return A.apply(B,arguments)}};Function.prototype.widget=function(){this.prototype.bind=function(B,A){this.$root.bind(B,A);return this};this.prototype.trigger=function(A,B){this.$root.trigger(A,B)};this.prototype.find=function(A){return this.$root.find(A)};return this};String.prototype.toCamel=function(){return this.replace(/[-_\s]\D/gi,function(A){return A.charAt(A.length-1).toUpperCase()})};String.prototype.escapeHTML=function(){return this.replace(/&/g,"&amp;").replace(/>/g,"&gt;").replace(/</g,"&lt;").replace(/"/g,"&quot;")};String.prototype.unescapeHTML=function(){return this.replace(/&amp;/g,"&").replace(/&gt;/g,">").replace(/&lt;/g,"<").replace(/&quot;/g,'"')};String.prototype.stripTags=function(){return this.replace(/<\/?[^>]+>/gi,"").replace(/<|>/g,"")};String.prototype.trim=function(){return this.replace(/^\s\s*/,"").replace(/\s\s*$/,"")};window.twttr=window.twttr||{};twttr.actionsTillReady=new Array("canTweet","sidebarTab","inPageLink");twttr.augmentObject=function(B,C){for(var A in C){B[A]=C[A]}return B};twttr.augmentObject(twttr,{namespaceOf:function(A){return twttr.is.object(A)?A:window},merge:function(){var D=arguments;var E=D[0];var H=arguments[arguments.length-1];var C=false;if(twttr.is.nil(E)||!twttr.is.def(E)){if(D.length<2){return{}}[].shift.call(D);return this.merge.apply(this,D)}if(twttr.is.bool(H)){C=H;[].pop.call(D)}for(var G=1,B=D.length;G<B;G++){var A=D[G];for(var F in A){if(C&&A[F]&&typeof A[F]==="object"){if(!E[F]){E[F]=(A[F] instanceof Array)?[]:{}}else{if(typeof E[F]!=="object"){E[F]=A[F]}}this.merge(E[F],A[F],true)}else{E[F]=A[F]}}}return E},extend:function(B,C){var A=function(){};A.prototype=C.prototype;B.prototype=new A();B.prototype.constructor=B;B.uber=C.prototype;if(C.prototype.constructor==Object.prototype.constructor){C.prototype.constructor=C}},klass:function(A,B){return twttr.magic(A,B)},augmentAndExtend:function(B,C,D){var A=twttr.namespaceOf(B);A[C]=function(){A[C].uber.constructor.apply(this,arguments)};twttr.extend(A[C],D);return A[C]},auxo:function(C,D,B){var A=twttr.is.object(B)?B:twttr;return twttr.augmentAndExtend(A,C,D)},augmentString:function(C,A){var B=window;C.split(".").forEach(function(F,E,D){B=B[F]=B[F]||(twttr.is.def(D[E+1])?{}:A)});return B},magic:function(B,A){if(twttr.is.string(B)){return twttr.augmentString(B,A)}else{return twttr.augmentObject(B,A)}},inspect:function(B){console.clear();var C=$(B);var H=C.data("events");var A=0;var G=0;var E=[];var D=[];for(key in H){E.push(key);A++;D.push("\n*******************\n");D.push("Events for "+key+"\n\n");for(fn in H[key]){var F=H[key][fn];G++;D.push(F.toString()+"\n")}}console.log("************* Summary *************");console.log("for target",C);console.log(A+" types of events",E);console.log(G,"Total Event Listeners");console.log("Event listeners assigned to target");console.log(D.join(" "))},is:{bool:function(A){return typeof A==="boolean"},nil:function(A){return A===null},def:function(A){return !(typeof A==="undefined")},number:function(A){return typeof A==="number"&&isFinite(A)},fn:function(A){return typeof A==="function"},array:function(A){return A?this.number(A.length)&&this.fn(A.splice):false},string:function(A){return typeof A==="string"},blank:function(A){return A===""},falsy:function(A){return A===false||A===null||A===undefined},object:function(A){return(A&&(typeof A==="object"||this.fn(A)))||false}},widget:function(A){A.prototype.bind=function(C,B){this.$element.bind(C,B)}}});if(!window.console){var names=["log","debug","info","warn","error","assert","dir","dirxml","group","groupEnd","time","timeEnd","count","trace","profile","profileEnd"];window.console={};for(var i=0;i<names.length;++i){window.console[names[i]]=function(){}}}function setupTranslationCallback(){if(!twttr.i18n_missing_interval){twttr.i18n_missing_interval=window.setInterval(function(){if(twttr.i18n_missing&&twttr.i18n_missing.length>0){$.ajax({type:"POST",data:$.param({authenticity_token:twttr.form_authenticity_token,location:window.location.href,"strings[]":twttr.i18n_missing}),url:"/translate/untranslated_javascript"});twttr.i18n_missing=new Array()}},10000)}}function recordUntranslatedString(A){if(!twttr.i18n_missing){twttr.i18n_missing=new Array()}if(!twttr.i18n_missing_reported){twttr.i18n_missing_reported={}}if(!twttr.i18n_missing_reported[A]){twttr.i18n_missing.push(encodeURIComponent(A));twttr.i18n_missing_reported[A]=true}}function _(C,A){if(twttr.i18n){var B=twttr.i18n[C];if(B){C=B}else{recordUntranslatedString(C)}}return replaceParams(C,A)}function replaceParams(B,A){if(A){for(var C in A){B=B.replace(new RegExp("\\%\\{"+C+"\\}","gi"),A[C])}}return B}var h=function(){var A=$("<div/>");return function(B){return B?A.text(B).html().replace(/\"/gi,"&quot;"):B}}();function unh(A){return A?A.replace(/&(amp;)+/g,"&").replace(/&[a-z]+;/gi,function(B){if(unh.HTML_ESCAPE_TOKENS[B]){return unh.HTML_ESCAPE_TOKENS[B]}return B}):A}window.unh.HTML_ESCAPE_TOKENS={"&lt;":"<","&gt;":">","&quot;":'"'};function addSlashes(A){return A.replace(/\'/g,"\\'").replace(/\"/g,'\\"')}var reverseString=function(A){return A?A.split("").reverse().join(""):A};var numberWithDelimiter=function(B,A){s=B.toString();if(s.indexOf(".")!=-1){return s}A=A?A:",";return s.replace(/(.)(?=(.{3})+$)/g,"$1"+A)};var timeAgo=function(C){if(!C){return false}var H=new Date();var G=new Date(C);if(document.all){G=new Date(Date.parse(C.replace(/( \+)/," UTC$1")))}var D=H-G;var B=1000,F=B*60,A=F*60;if(isNaN(D)||D<0){return false}var E=-1;$.each([5,10,20],function(){if(D<this*B){E=this;return false}});if(E!=-1){return _("less than %{time} seconds ago",{time:E})}if(D<B*40){return _("half a minute ago")}if(D<F){return _("less than a minute ago")}if(D<B*90){return _("1 minute ago")}if(D<F*45){return _("%{time} minutes ago",{time:Math.round(D/F)})}if(D<F*90){return _("about 1 hour ago")}if(D<A*24){return _("about %{time} hours ago",{time:Math.round(D/A)})}return G.toLocaleString().replace(/ GMT[+-][0-9]+:?[0-9]+/,"")};var updateTimeAgo=function(){$(".timestamp").each(function(){var B=$(this);var A=timeAgo(B.meta().time);if(B.meta().prefix!=null){A=B.meta().prefix+" "+A}if(A&&B.find("*").length==0){B.html(A)}});$(".timestamp-title").each(function(){var B=$(this);var A=timeAgo(B.meta().time);if(A){B.attr("title",A)}})};var DEBUG=false;$.extend({log:function(A){if(window.console){console.log(A)}},debug:function(A){if(DEBUG){console.log(A)}},inspect:function(B){var A="{\n";for(var C in B){A+="\t"+C+": "+B[C]+"\n"}A+="}";console.log(A);return A},getStackTrace:function(){var I=[];var C=false;try{D.dont.exist+=0}catch(F){if(F.stack){var J=F.stack.split("\n");for(var D=0,E=J.length;D<E;D++){I.push(J[D])}I.shift();C=true}else{if(window.opera&&F.message){var J=F.message.split("\n");for(var D=0,E=J.length;D<E;D++){if(J[D].match(/^\s*[A-Za-z0-9\-_\$]+\(/)){var H=J[D];if(J[D+1]){H+=" at "+J[D+1];D++}I.push(H)}}I.shift();C=true}}}if(!C){var B=arguments.callee.caller;while(B){var G=B.toString();var A=G.substring(G.indexOf("function")+8,G.indexOf(""))||"anonymous";I.push(A);B=B.caller}}return I}});(function(){if(document.all){if(/MSIE (\d+\.\d+);/.test(navigator.userAgent)){var A=new Number(RegExp.$1);if(A>=8){$.browser.msie8=true}else{if(A>=7){$.browser.msie7=true}else{$.browser.msie6=true}}}}})();var _tmp={};twttr.augmentObject(twttr,{templates:{},timeouts:{},wait:function(){var A={};twttr.clearWait=function(B){if(twttr.is.def(A[B])){clearTimeout(B);delete A[B]}};return function(E,C){var B="TIMER_"+(new Date()).getTime();var D=setTimeout(function(){if(!twttr.is.def(A[B])){return }E()},C);A[B]=D;return B}}(),processJson:function(json){if(typeof (json)=="object"){var evals=[];$.each(json,function(selector,content){var c=selector.charAt(0);if(c=="$"){evals.push(content)}else{if(c=="!"){var notification=window[selector.substring(1)+"Notification"];if(notification){(new notification()).setMessage(content).show()}}else{var $contentPadded=$("<div></div>").html(content);var $content=$(selector,$contentPadded);if($content.length==1){$(selector).replaceWith($content)}else{$(selector).html(content)}$(selector).show()}}});$.each(evals,function(index,js){if(js){eval(js)}})}},googleAnalytics:function(A){if(window.pageTracker){window.pageTracker._trackEvent("Ajax","refresh",A,null)}},trackPageView:function(C,B,D){if(window.pageTracker){var A;if(C){A=C.toString();if(B){A="/search/tweets/"+encodeURIComponent(h(page.query))}if(D){A=A+D}window.pageTracker._trackPageview(A)}else{window.pageTracker._trackPageview()}}},fadeAndReplace:function(A,B){$(A).fadeOut("medium",function(){$(A).html(B)});$(A).fadeIn("medium")},error:function(A){alert(A?A:_("Whoops! Something went wrong. Please refresh the page and try again!"))},loaddisableding:function(){$("#loaddisableder").fadeIn(200)},loaddisableded:function(){$("#loaddisableder").fadeOut(200)},updateLocation:function(A,E){if(!E){E=document}if(A){var D=A.replace(/^https?:\/\/.+?\//,"").replace(/#/gi,"%23").replace(/\s/gi,"+");var C=D.replace(/[^\w\d_-].*$/,"");var B=(C.length>0)?$(E).find("#"+C):[];if(B.length>0){B.get(0).id=C+"_tmp_for_update_location"}E.location.hash=D;if(B.length>0){B.get(0).id=C}}},NON_CHAR_KEY_CODES:[8,9,16,17,18,19,20,27,33,34,35,36,37,38,39,40,45,46,91,92,93],isNonCharKeyCode:function(A){return $.inArray(A.keyCode,twttr.NON_CHAR_KEY_CODES)!=-1||((A.ctrlKey||A.metaKey)&&$.inArray(A.keyCode,[67,88])!=-1)}});$.extend($.expr[":"],{onthepage:"($(elem).is(':visible') && $(elem).parents(':hidden').length == 0)"});jQuery.fn.move=function(A){var B=$(this).html();$(this).remove();$(A).html(B)};jQuery.fn.meta=function(){var B={type:"attr",name:"data"};var C=$(this);if(C.length==1){return C.metadata(B)}else{var A=[];C.each(function(){A.push($(this).metadata(B))});return A}};jQuery.fn.visible=function(A){$(this).each(function(){$(this).css("visibility",A?"visible":"hidden")})};jQuery.fn.isLoading=function(){$(this).addClass("loaddisableding")};$.fn.isLoaded=function(){$(this).removeClass("loaddisableding")};$.fn.replace_text=function(C,B){var A=$(this).html();if(A){$(this).html(A.replace(C,B))}};var pluralize=function(C,B,A){return C==1?B:A};var setDocumentTitle=function(A){document.title=unh(A.stripTags())||""};var addCountToDocumentTitle=function(A){document.title=(A?"("+numberWithDelimiter(A)+") ":"")+document.title.replace(/\([^)]*[0-9]\)\s+/gi,"")};var getSessionUserScreenName=function(){var A;if(page.user_screenname){A=page.user_screenname}else{if($('meta[name="session-user-screen_name"]:first').get(0)){A=$('meta[name="session-user-screen_name"]:first').get(0).content}else{A=$('meta[name="session-user-screen_name"]').get(0).content}}return A};var sessionUserIsPageUser=function(){try{return $('meta[name="session-user-screen_name"]:first').get(0).content==$('meta[name="page-user-screen_name"]:first').get(0).content}catch(A){return false}};$.fn.focusEnd=function(){return this.each(function(){var A=this;if(A.style.display!="none"){if($.browser.msie){A.focus();var B=A.createTextRange();B.collapse(false);B.select()}else{A.setSelectionRange(A.value.length,A.value.length);A.focus()}}})};$.fn.focusFirstTextField=function(){return this.find("input[type=text]:visible:enabled:first").focus().length>0},$.fn.focusFirstTextArea=function(){return this.find("textarea:visible:enabled:first").focus().length>0};$.fn.focusFirstTexttarget=function(){return this.focusFirstTextField()||this.focusFirstTextArea()};$.fn.maxLength=function(A){return this.each(function(){$(this).keydown(function(B){return this.value.length<=A||twttr.isNonCharKeyCode(B)})})};$.fn.replaceClass=function(B,A){return this.each(function(){var C=$(this);if(C.hasClass(B)){C.removeClass(B).addClass(A)}else{if(C.hasClass(A)){C.removeClass(A).addClass(B)}}})};$.fn.isSelectAll=function(A){return this.each(function(){var B=$(this);if(typeof (A)=="string"){var D=$(A).find("input[type=checkbox]")}else{var D=A}function C(){var E=true;D.each(function(){if(!this.checked){E=false;return false}});B.get(0).checked=E}B.click(function(){var E=B.get(0).checked;D.each(function(){this.checked=E});$(this).trigger("select-all-changed",E)});D.click(function(){C();$(this).trigger("checkbox-changed",this.checked)})})};function bodytarget(){return $("body")}twttr.klass("twttr.Observer",function(){this.fns=[]}).method("listen",function(A){this.fns.push(A)}).method("unlisten",function(A){this.fns=this.fns.filter(function(B){if(B!==A){return B}})}).method("trigger",function(C,B){var A=B||window;this.fns.forEach(function(D){D.call(A,C)})});twttr.klass("twttr.User",function(A){this.screen_name=A}).method("update",function(B,A){twttr.tweeters[this.screen_name][B]=A;return this}).method("updateAll",function(B){for(var A in B){twttr.tweeters[this.screen_name][A]=B[A]}return this}).method("data",function(B){var A=twttr.tweeters[this.screen_name];return B?A[B]:A});twttr.augmentObject(twttr.User,{UserFetchTimeout:5000,UserFetchUrl:"/users/show",_bail:false,_requesting:false,bail:function(){this._bail=true},isRequesting:function(){return this._requesting},getCurrentUser:function(A){return this.findById(page.sessionUserId,A)},find:function(F,C,G){var B,A;var D=this;if(twttr.is.fn(C)){B=window;A=C}else{B=C;A=G}var E=twttr.is.def(F.screen_name)?F.screen_name.toLowerCase():null;if(E&&twttr.tweeters[E]){A.call(B,new twttr.User(E),true);return true}else{$.ajax({url:this.UserFetchUrl,type:"GET",data:F,dataType:"json",timeout:this.UserFetchTimeout,beforeSend:function(){D._requesting=true},success:function(K){D._requesting=false;var H=K.user;if(H){var I={};var J=H.screen_name.toLowerCase();I[J]=H;twttr.User.merge(I,true);if(D._bail){D._bail=false;return false}A.call(B,new twttr.User(J),false)}else{if(D._bail){D._bail=false;return false}A.call(B,null,false)}},error:function(H){D._requesting=false;if(D._bail){D._bail=false;return false}A.call(B,null,false)}});return false}},findByScreenName:function(B,A,C){return this.find({screen_name:B,hovercard:true},A,C)},findById:function(D,A,C){var B=twttr._birdtags[D];if(twttr.is.def(B)){this.findByScreenName(B,A,C)}else{this.find({user_id:D,hovercard:true},A,C)}},merge:function(){twttr.tweeters={};twttr._birdtags={};return function(D,A){var D=D||{};if(A){twttr.merge(twttr.tweeters,D,true)}else{var C=twttr.merge(D,twttr.tweeters,true);twttr.merge(twttr.tweeters,C,true)}for(var B in twttr.tweeters){twttr._birdtags[twttr.tweeters[B].user_id]=B}}}()});twttr.loaddisabledTemplate=function(A,B){if(twttr.templates[A]){return twttr.templates[A]}B=B||function(){};$.get("/mustaches/"+A+".html",null,function(D){var C={templates:{}};C.templates[A]=D;twttr.merge(twttr,C,true);B(twttr.templates)},"html")};twttr.loaddisabledTemplates=function(A,B){B=B||function(){};A.forEach(function(D,C){twttr.loaddisabledTemplate(D,function(E){var F=A.every(function(G){return twttr.is.def(E[G])});if(F){B(twttr.templates)}})})};twttr.SimplePositioner={setPosition:function(H,I,J){var D={inline:false,direction:null,offsets:{inline:{top:0,left:0},below:{top:0,left:0},above:{top:0,left:0}},hasContainer:false};var A=twttr.merge({},D,J,true);var F=F instanceof jQuery?H:$(H);var C=I instanceof jQuery?I:$(I);var E=A.hasContainer?C.position():C.offset();if(!A.inline){var G=this;function B(K){G.clearPosition();switch(K){case"above":G._positionAbove(F,C,E,A.offsets.above,A.hasContainer);break;case"below":G._positionBelow(F,C,E,A.offsets.below);break;case"prefer below":B("below");if((F.offset().top-$(document).scrollTop())+(A.itemHeight||F.height())>$(window).height()){B("prefer above")}break;default:B("above");if((F.offset().top-$(document).scrollTop())<0){B("below")}break}}B(A.direction)}else{this._positionInline(F,C,E,A.offsets.inline);F.css("left",E.left+A.offsets.inline.left)}},clearPosition:function(){$("body").removeClass("loaddisableding-hoverer-above")},_positionAbove:function(E,F,C,A,B){E.addClass("position_above").removeClass("position_below").removeClass("position_inline");var G=Math.round(C.top+A.top);var H;if(B){H=F.parents().filter(function(){return $(this).css("position")=="relative"}).outerHeight()}else{var D=$("body");D.addClass("loaddisableding-hoverer-above");var I=parseInt(D.css("padding-top"));G+=I>0?12:0;H=D.outerHeight()}E.css({bottom:H-G,left:this._getLeftPosition(E,F,C)+A.left})},_positionBelow:function(A,C,D,E){var B=Math.round(D.top+C.height()+E.top);A.addClass("position_below").removeClass("position_above").removeClass("position_inline");A.css({top:B,left:this._getLeftPosition(A,C,D)+E.left})},_positionInline:function(A,B,D,C){A.css("top",D.top+C.top).addClass("position_inline").removeClass("position_below").removeClass("position_above")},_getLeftPosition:function(A,B,C){return Math.round(C.left+(B.width()/2))}};twttr.unparam=function(F){var E={};var C=F.split("&");for(var B=0,A=C.length;B<A;B++){var D=C[B].split("=",2);E[decodeURIComponent(D[0])]=(D.length==2?decodeURIComponent(D[1].replace(/\+/g," ")):null)}return E};twttr.klass("twttr.Validator",function(A,C,B){this.$field=$(A);this.value=this.$field.val();if(twttr.is.string(this.value)){this.value=jQuery.trim(this.value)}this.fieldName="";if(twttr.is.object(C)){B=C}else{this.fieldName=C}this.valid=B.valid;this.invalid=B.invalid});twttr.Validator.augmentProto({is:function(){var A=null;var B=this;$.each(arguments,function(D,C){if(!C._decorated){C=C()}if(!C(B.value)){A=C;return false}});if(A){this.invalid(this.$field,this.fieldName,A.errorMessage)}else{this.valid(this.$field,this.fieldName)}}});twttr.validate=function(B,A){function C(D,F,E){return new twttr.Validator(D,F,E)}twttr.augmentObject(C,B);return A(C)};$.fn.helpText=function(){this.each(function(){var B=$(this);var A=B.hasClass("help-focusable");if(A){B.mouseup(function(C){if(!B.helpVal()==""){B.select();C.preventDefault()}})}B.focus(function(C){B.setHelpState(false,A)}).blur(function(){if(document.selection){document.selection.empty()}else{getSelection().removeAllRanges()}B.setHelpState(true)});B.setHelpState(true)});return this};$.fn.helpVal=function(){var A=$.trim(this.val());return A==this.attr("title")?"":A};$.fn.setHelpState=function(A,B){this.each(function(){var D=$(this);var C="help-text";if(A){if(!D.helpVal()){D.val(D.attr("title"));D.addClass(C);if(this.hasFocus&&D.hasClass("help-focusable")){D.select()}}else{D.removeClass(C)}}else{D.removeClass(C);if(!D.helpVal()){if(B){D.select()}else{D.val("")}}}})};$.fn.selectOnClick=function(){this.each(function(){var B=$(this);var A=true;B.click(function(){if(A){A=false;this.select()}}).blur(function(){A=true})});return this};twttr.klass("twttr.autocomplete",function(A){var B=this;B.opts=twttr.merge({getInputVal:function(){return B.opts.$input.val()},hoverClass:"hover",delay:350},A);B.cache={};B._clearFakeFocus();B.opts.$input.keydown(function(C){switch(C.keyCode){case 38:B.arrowUp();break;case 40:B.arrowDown();break;case 13:if(!B.hasFakeFocus){return }B.$fakeFocus.click();break;case 27:if(B.opts.$dropdown.is(":visible")){B.hide()}else{return }break;case 9:if(B.opts.$dropdown.is(":visible")){B.hide()}return ;default:B._onInputChange();return }B.keyDownEvent=true;C.stopPropagation();C.preventDefault()}).keypress(function(C){if(C.charCode==0&&(C.keyCode==38||C.keyCode==40)){if(!B.keyDownEvent){if(C.keyCode==38){B.arrowUp()}else{B.arrowDown()}}B.keyDownEvent=false;C.preventDefault()}}).change(function(){B._onInputChange()}).blur(function(){if(!B.hovering){B.hide()}})}).method("arrowDown",function(){if(this.hasFakeFocus){this._setFakeFocus(this.$fakeFocus.next())}else{this._show()}}).method("arrowUp",function(){this._setFakeFocus(this.$fakeFocus.prev())}).method("hide",function(){this._clearFakeFocus();this.opts.$dropdown.hide();this.displayedInputVal=this.opts.getInputVal()}).method("_show",function(){this.displayedInputVal="";this._display();this._setFakeFocus(this.opts.$dropdown.children(":first"))}).method("_clearFakeFocus",function(){this.$fakeFocus=$([]);this.hasFakeFocus=false;this.opts.$dropdown.children().removeClass(this.opts.HoverClass)}).method("_setFakeFocus",function(A){if(A.length>0){this.hasFakeFocus=true;this.$fakeFocus=A;this.$fakeFocus.addClass(this.opts.hoverClass).siblings().removeClass(this.opts.hoverClass)}}).method("_onInputChange",function(){var A=this;setTimeout(function(){A._display()},0)}).method("_display",function(){var B=this;var A=B.opts.getInputVal();if(B.displayedInputVal!=A){B._clearFakeFocus();var D=B.opts.$dropdown.hide().empty();B.displayedInputVal="";var C=B.cache[A];if(C){C.forEach(function(G,F){D.append(B.opts.renderMatch(G,F,C))});D.children().hover(function(){if(B.hasFakeFocus){B._setFakeFocus($(this))}else{$(this).addClass(B.opts.hoverClass)}B.hovering=true},function(){if(!B.hasFakeFocus){$(this).removeClass(B.opts.hoverClass)}B.hovering=false});var E=B.opts.$input.position();D.css({left:E.left,top:E.top+B.opts.$input.outerHeight()-1}).show();B.displayedInputVal=A}else{if(A&&C===undefined){B._fetch(A)}}}}).method("_fetch",function(A){var B=this;clearTimeout(B.timerId);B.timerId=setTimeout(function(){B.cache[A]=false;B.opts.fetchMatches(A,function(C){if(C&&C.length>0){B.cache[A]=C;B._display()}},function(){B.cache[A]=undefined})},B.opts.delay)});/*!
+ * twitter-text-js 1.3.1
+ *
+ * Copyright 2010 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+if (!window.twttr) {
+ window.twttr = {};
+}
+
+(function() {
+ twttr.txt = {};
+ twttr.txt.regexen = {};
+
+ var HTML_ENTITIES = {
+ '&': '&amp;',
+ '>': '&gt;',
+ '<': '&lt;',
+ '"': '&quot;',
+ "'": '&#32;'
+ };
+
+ // HTML escaping
+ twttr.txt.htmlEscape = function(text) {
+ return text && text.replace(/[&"'><]/g, function(character) {
+ return HTML_ENTITIES[character];
+ });
+ };
+
+ // Builds a RegExp
+ function regexSupplant(regex, flags) {
+ flags = flags || "";
+ if (typeof regex !== "string") {
+ if (regex.global && flags.indexOf("g") < 0) {
+ flags += "g";
+ }
+ if (regex.ignoreCase && flags.indexOf("i") < 0) {
+ flags += "i";
+ }
+ if (regex.multiline && flags.indexOf("m") < 0) {
+ flags += "m";
+ }
+
+ regex = regex.source;
+ }
+
+ return new RegExp(regex.replace(/#\{(\w+)\}/g, function(match, name) {
+ var newRegex = twttr.txt.regexen[name] || "";
+ if (typeof newRegex !== "string") {
+ newRegex = newRegex.source;
+ }
+ return newRegex;
+ }), flags);
+ }
+
+ // simple string interpolation
+ function stringSupplant(str, values) {
+ return str.replace(/#\{(\w+)\}/g, function(match, name) {
+ return values[name] || "";
+ });
+ }
+
+ // Space is more than %20, U+3000 for example is the full-width space used with Kanji. Provide a short-hand
+ // to access both the list of characters and a pattern suitible for use with String#split
+ // Taken from: ActiveSupport::Multibyte::Handlers::UTF8Handler::UNICODE_WHITESPACE
+ var fromCode = String.fromCharCode;
+ var UNICODE_SPACES = [
+ fromCode(0x0020), // White_Space # Zs SPACE
+ fromCode(0x0085), // White_Space # Cc <control-0085>
+ fromCode(0x00A0), // White_Space # Zs NO-BREAK SPACE
+ fromCode(0x1680), // White_Space # Zs OGHAM SPACE MARK
+ fromCode(0x180E), // White_Space # Zs MONGOLIAN VOWEL SEPARATOR
+ fromCode(0x2028), // White_Space # Zl LINE SEPARATOR
+ fromCode(0x2029), // White_Space # Zp PARAGRAPH SEPARATOR
+ fromCode(0x202F), // White_Space # Zs NARROW NO-BREAK SPACE
+ fromCode(0x205F), // White_Space # Zs MEDIUM MATHEMATICAL SPACE
+ fromCode(0x3000) // White_Space # Zs IDEOGRAPHIC SPACE
+ ];
+
+ for (var i = 0x009; i <= 0x000D; i++) { // White_Space # Cc [5] <control-0009>..<control-000D>
+ UNICODE_SPACES.push(String.fromCharCode(i));
+ }
+
+ for (var i = 0x2000; i <= 0x200A; i++) { // White_Space # Zs [11] EN QUAD..HAIR SPACE
+ UNICODE_SPACES.push(String.fromCharCode(i));
+ }
+
+ twttr.txt.regexen.spaces = regexSupplant("[" + UNICODE_SPACES.join("") + "]");
+ twttr.txt.regexen.punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~/;
+ twttr.txt.regexen.atSigns = /[@ï¼ ]/;
+ twttr.txt.regexen.extractMentions = regexSupplant(/(^|[^a-zA-Z0-9_])(#{atSigns})([a-zA-Z0-9_]{1,20})(?=(.|$))/g);
+ twttr.txt.regexen.extractReply = regexSupplant(/^(?:#{spaces})*#{atSigns}([a-zA-Z0-9_]{1,20})/);
+ twttr.txt.regexen.listName = /[a-zA-Z][a-zA-Z0-9_\-\u0080-\u00ff]{0,24}/;
+
+ // Latin accented characters (subtracted 0xD7 from the range, it's a confusable multiplication sign. Looks like "x")
+ twttr.txt.regexen.latinAccentChars = regexSupplant("ÀÃÂÃÄÅÆÇÈÉÊËÌÃÃŽÃÃÑÒÓÔÕÖØÙÚÛÜÃÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþ\\303\\277");
+ twttr.txt.regexen.latenAccents = regexSupplant(/[#{latinAccentChars}]+/);
+
+ twttr.txt.regexen.endScreenNameMatch = regexSupplant(/^(?:#{atSigns}|[#{latinAccentChars}]|:\/\/)/);
+
+ // Characters considered valid in a hashtag but not at the beginning, where only a-z and 0-9 are valid.
+ twttr.txt.regexen.hashtagCharacters = regexSupplant(/[a-z0-9_#{latinAccentChars}]/i);
+ twttr.txt.regexen.autoLinkHashtags = regexSupplant(/(^|[^0-9A-Z&\/\?]+)(#|#)([0-9A-Z_]*[A-Z_]+#{hashtagCharacters}*)/gi);
+ twttr.txt.regexen.autoLinkUsernamesOrLists = /(^|[^a-zA-Z0-9_]|RT:?)([@ï¼ ]+)([a-zA-Z0-9_]{1,20})(\/[a-zA-Z][a-zA-Z0-9_\-]{0,24})?/g;
+ twttr.txt.regexen.autoLinkEmoticon = /(8\-\#|8\-E|\+\-\(|\`\@|\`O|\&lt;\|:~\(|\}:o\{|:\-\[|\&gt;o\&lt;|X\-\/|\[:-\]\-I\-|\/\/\/\/Ö\\\\\\\\|\(\|:\|\/\)|∑:\*\)|\( \| \))/g;
+
+ // URL related hash regex collection
+ twttr.txt.regexen.validPrecedingChars = regexSupplant(/(?:[^-\/"':!=A-Za-z0-9_@ï¼ ]|^|\:)/);
+ twttr.txt.regexen.validDomain = regexSupplant(/(?:[^#{punct}\s][\.-](?=[^#{punct}\s])|[^#{punct}\s]){1,}\.[a-z]{2,}(?::[0-9]+)?/i);
+
+ twttr.txt.regexen.validGeneralUrlPathChars = /[a-z0-9!\*';:=\+\$\/%#\[\]\-_,~]/i;
+ // Allow URL paths to contain balanced parens
+ // 1. Used in Wikipedia URLs like /Primer_(film)
+ // 2. Used in IIS sessions like /S(dfd346)/
+ twttr.txt.regexen.wikipediaDisambiguation = regexSupplant(/(?:\(#{validGeneralUrlPathChars}+\))/i);
+ // Allow @ in a url, but only in the middle. Catch things like http://example.com/@user
+ twttr.txt.regexen.validUrlPathChars = regexSupplant(/(?:#{wikipediaDisambiguation}|@#{validGeneralUrlPathChars}+\/|[\.,]?#{validGeneralUrlPathChars})/i);
+
+ // Valid end-of-path chracters (so /foo. does not gobble the period).
+ // 1. Allow =&# for empty URL parameters and other URL-join artifacts
+ twttr.txt.regexen.validUrlPathEndingChars = regexSupplant(/(?:[a-z0-9=_#\/]|#{wikipediaDisambiguation})/i);
+ twttr.txt.regexen.validUrlQueryChars = /[a-z0-9!\*'\(\);:&=\+\$\/%#\[\]\-_\.,~]/i;
+ twttr.txt.regexen.validUrlQueryEndingChars = /[a-z0-9_&=#\/]/i;
+ twttr.txt.regexen.validUrl = regexSupplant(
+ '(' + // $1 total match
+ '(#{validPrecedingChars})' + // $2 Preceeding chracter
+ '(' + // $3 URL
+ '(https?:\\/\\/)' + // $4 Protocol
+ '(#{validDomain})' + // $5 Domain(s) and optional post number
+ '(\\/' + // $6 URL Path
+ '(?:' +
+ '#{validUrlPathChars}+#{validUrlPathEndingChars}|' +
+ '#{validUrlPathChars}+#{validUrlPathEndingChars}?|' +
+ '#{validUrlPathEndingChars}' +
+ ')?' +
+ ')?' +
+ '(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $7 Query String
+ ')' +
+ ')'
+ , "gi");
+
+ // Default CSS class for auto-linked URLs
+ var DEFAULT_URL_CLASS = "tweet-url";
+ // Default CSS class for auto-linked lists (along with the url class)
+ var DEFAULT_LIST_CLASS = "list-slug";
+ // Default CSS class for auto-linked usernames (along with the url class)
+ var DEFAULT_USERNAME_CLASS = "username";
+ // Default CSS class for auto-linked hashtags (along with the url class)
+ var DEFAULT_HASHTAG_CLASS = "hashtag";
+ // HTML attribute for robot nofollow behavior (default)
+ var HTML_ATTR_NO_FOLLOW = " rel=\"nofollow\"";
+
+ // Simple object cloning function for simple objects
+ function clone(o) {
+ var r = {};
+ for (var k in o) {
+ if (o.hasOwnProperty(k)) {
+ r[k] = o[k];
+ }
+ }
+
+ return r;
+ }
+
+ twttr.txt.autoLink = function(text, options) {
+ options = clone(options || {});
+ return twttr.txt.autoLinkUsernamesOrLists(
+ twttr.txt.autoLinkUrlsCustom(
+ twttr.txt.autoLinkHashtags(text, options),
+ options),
+ options);
+ };
+
+
+ twttr.txt.autoLinkUsernamesOrLists = function(text, options) {
+ options = clone(options || {});
+
+ options.urlClass = options.urlClass || DEFAULT_URL_CLASS;
+ options.listClass = options.listClass || DEFAULT_LIST_CLASS;
+ options.usernameClass = options.usernameClass || DEFAULT_USERNAME_CLASS;
+ options.usernameUrlBase = options.usernameUrlBase || "httpdisabled://twitter.com/";
+ options.listUrlBase = options.listUrlBase || "httpdisabled://twitter.com/";
+ if (!options.suppressNoFollow) {
+ var extraHtml = HTML_ATTR_NO_FOLLOW;
+ }
+
+ var newText = "",
+ splitText = twttr.txt.splitTags(text);
+
+ for (var index = 0; index < splitText.length; index++) {
+ var chunk = splitText[index];
+
+ if (index !== 0) {
+ newText += ((index % 2 === 0) ? ">" : "<");
+ }
+
+ if (index % 4 !== 0) {
+ newText += chunk;
+ } else {
+ newText += chunk.replace(twttr.txt.regexen.autoLinkUsernamesOrLists, function(match, before, at, user, slashListname, offset, chunk) {
+ var after = chunk.slice(offset + match.length);
+
+ var d = {
+ before: before,
+ at: at,
+ user: twttr.txt.htmlEscape(user),
+ slashListname: twttr.txt.htmlEscape(slashListname),
+ extraHtml: extraHtml,
+ chunk: twttr.txt.htmlEscape(chunk)
+ };
+ for (var k in options) {
+ if (options.hasOwnProperty(k)) {
+ d[k] = options[k];
+ }
+ }
+
+ if (slashListname && !options.suppressLists) {
+ // the link is a list
+ var list = d.chunk = stringSupplant("#{user}#{slashListname}", d);
+ d.list = twttr.txt.htmlEscape(list.toLowerCase());
+ return stringSupplant("#{before}#{at}<a class=\"#{urlClass} #{listClass}\" href=\"#{listUrlBase}#{list}\"#{extraHtml}>#{chunk}</a>", d);
+ } else {
+ if (after && after.match(twttr.txt.regexen.endScreenNameMatch)) {
+ // Followed by something that means we don't autolink
+ return match;
+ } else {
+ // this is a screen name
+ d.chunk = twttr.txt.htmlEscape(user);
+ d.dataScreenName = !options.suppressDataScreenName ? stringSupplant("data-screen-name=\"#{chunk}\" ", d) : "";
+ return stringSupplant("#{before}#{at}<a class=\"#{urlClass} #{usernameClass}\" #{dataScreenName}href=\"#{usernameUrlBase}#{chunk}\"#{extraHtml}>#{chunk}</a>", d);
+ }
+ }
+ });
+ }
+ }
+
+ return newText;
+ };
+
+ twttr.txt.autoLinkHashtags = function(text, options) {
+ options = clone(options || {});
+ options.urlClass = options.urlClass || DEFAULT_URL_CLASS;
+ options.hashtagClass = options.hashtagClass || DEFAULT_HASHTAG_CLASS;
+ options.hashtagUrlBase = options.hashtagUrlBase || "httpdisabled://twitter.com/search?q=%23";
+ if (!options.suppressNoFollow) {
+ var extraHtml = HTML_ATTR_NO_FOLLOW;
+ }
+
+ return text.replace(twttr.txt.regexen.autoLinkHashtags, function(match, before, hash, text) {
+ var d = {
+ before: before,
+ hash: twttr.txt.htmlEscape(hash),
+ text: twttr.txt.htmlEscape(text),
+ extraHtml: extraHtml
+ };
+
+ for (var k in options) {
+ if (options.hasOwnProperty(k)) {
+ d[k] = options[k];
+ }
+ }
+
+ return stringSupplant("#{before}<a href=\"#{hashtagUrlBase}#{text}\" title=\"##{text}\" class=\"#{urlClass} #{hashtagClass}\"#{extraHtml}>#{hash}#{text}</a>", d);
+ });
+ };
+
+
+ twttr.txt.autoLinkUrlsCustom = function(text, options) {
+ options = clone(options || {});
+ if (!options.suppressNoFollow) {
+ options.rel = "nofollow";
+ }
+ if (options.urlClass) {
+ options["class"] = options.urlClass;
+ delete options.urlClass;
+ }
+
+ delete options.suppressNoFollow;
+ delete options.suppressDataScreenName;
+
+ return text.replace(twttr.txt.regexen.validUrl, function(match, all, before, url, protocol, domain, path, queryString) {
+ var tldComponents;
+
+ if (protocol) {
+ var htmlAttrs = "";
+ for (var k in options) {
+ htmlAttrs += stringSupplant(" #{k}=\"#{v}\" ", {k: k, v: options[k].toString().replace(/"/, "&quot;").replace(/</, "&lt;").replace(/>/, "&gt;")});
+ }
+ options.htmlAttrs || "";
+
+ var d = {
+ before: before,
+ htmlAttrs: htmlAttrs,
+ url: twttr.txt.htmlEscape(url)
+ };
+
+ return stringSupplant("#{before}<a href=\"#{url}\"#{htmlAttrs}>#{url}</a>", d);
+ } else {
+ return all;
+ }
+ });
+ };
+
+ twttr.txt.extractMentions = function(text) {
+ var screenNamesOnly = [],
+ screenNamesWithIndices = twttr.txt.extractMentionsWithIndices(text);
+
+ for (var i = 0; i < screenNamesWithIndices.length; i++) {
+ var screenName = screenNamesWithIndices[i].screenName;
+ screenNamesOnly.push(screenName);
+ }
+
+ return screenNamesOnly;
+ };
+
+ twttr.txt.extractMentionsWithIndices = function(text) {
+ if (!text) {
+ return [];
+ }
+
+ var possibleScreenNames = [],
+ position = 0;
+
+ text.replace(twttr.txt.regexen.extractMentions, function(match, before, atSign, screenName, after) {
+ if (!after.match(twttr.txt.regexen.endScreenNameMatch)) {
+ var startPosition = text.indexOf(atSign + screenName, position);
+ position = startPosition + screenName.length + 1;
+ possibleScreenNames.push({
+ screenName: screenName,
+ indices: [startPosition, position]
+ });
+ }
+ });
+
+ return possibleScreenNames;
+ };
+
+ twttr.txt.extractReplies = function(text) {
+ if (!text) {
+ return null;
+ }
+
+ var possibleScreenName = text.match(twttr.txt.regexen.extractReply);
+ if (!possibleScreenName) {
+ return null;
+ }
+
+ return possibleScreenName[1];
+ };
+
+ twttr.txt.extractUrls = function(text) {
+ var urlsOnly = [],
+ urlsWithIndices = twttr.txt.extractUrlsWithIndices(text);
+
+ for (var i = 0; i < urlsWithIndices.length; i++) {
+ urlsOnly.push(urlsWithIndices[i].url);
+ }
+
+ return urlsOnly;
+ };
+
+ twttr.txt.extractUrlsWithIndices = function(text) {
+ if (!text) {
+ return [];
+ }
+
+ var urls = [],
+ position = 0;
+
+ text.replace(twttr.txt.regexen.validUrl, function(match, all, before, url, protocol, domain, path, query) {
+ var tldComponents;
+
+ if (protocol) {
+ var startPosition = text.indexOf(url, position),
+ position = startPosition + url.length;
+
+ urls.push({
+ url: url,
+ indices: [startPosition, position]
+ });
+ }
+ });
+
+ return urls;
+ };
+
+ twttr.txt.extractHashtags = function(text) {
+ var hashtagsOnly = [],
+ hashtagsWithIndices = twttr.txt.extractHashtagsWithIndices(text);
+
+ for (var i = 0; i < hashtagsWithIndices.length; i++) {
+ hashtagsOnly.push(hashtagsWithIndices[i].hashtag);
+ }
+
+ return hashtagsOnly;
+ };
+
+ twttr.txt.extractHashtagsWithIndices = function(text) {
+ if (!text) {
+ return [];
+ }
+
+ var tags = [],
+ position = 0;
+
+ text.replace(twttr.txt.regexen.autoLinkHashtags, function(match, before, hash, hashText) {
+ var startPosition = text.indexOf(hash + hashText, position);
+ position = startPosition + hashText.length + 1;
+ tags.push({
+ hashtag: hashText,
+ indices: [startPosition, position]
+ });
+ });
+
+ return tags;
+ };
+
+ // this essentially does text.split(/<|>/)
+ // except that won't work in IE, where empty strings are ommitted
+ // so "<>".split(/<|>/) => [] in IE, but is ["", "", ""] in all others
+ // but "<<".split("<") => ["", "", ""]
+ twttr.txt.splitTags = function(text) {
+ var firstSplits = text.split("<"),
+ secondSplits,
+ allSplits = [],
+ split;
+
+ for (var i = 0; i < firstSplits.length; i += 1) {
+ split = firstSplits[i];
+ if (!split) {
+ allSplits.push("");
+ } else {
+ secondSplits = split.split(">");
+ for (var j = 0; j < secondSplits.length; j += 1) {
+ allSplits.push(secondSplits[j]);
+ }
+ }
+ }
+
+ return allSplits;
+ };
+
+ twttr.txt.hitHighlight = function(text, hits, options) {
+ var defaultHighlightTag = "em";
+
+ hits = hits || [];
+ options = options || {};
+
+ if (hits.length === 0) {
+ return text;
+ }
+
+ var tagName = options.tag || defaultHighlightTag,
+ tags = ["<" + tagName + ">", "</" + tagName + ">"],
+ chunks = twttr.txt.splitTags(text),
+ split,
+ i,
+ j,
+ result = "",
+ chunkIndex = 0,
+ chunk = chunks[0],
+ prevChunksLen = 0,
+ chunkCursor = 0,
+ startInChunk = false,
+ chunkChars = chunk,
+ flatHits = [],
+ index,
+ hit,
+ tag,
+ placed,
+ hitSpot;
+
+ for (i = 0; i < hits.length; i += 1) {
+ for (j = 0; j < hits[i].length; j += 1) {
+ flatHits.push(hits[i][j]);
+ }
+ }
+
+ for (index = 0; index < flatHits.length; index += 1) {
+ hit = flatHits[index];
+ tag = tags[index % 2];
+ placed = false;
+
+ while (chunk != null && hit >= prevChunksLen + chunk.length) {
+ result += chunkChars.slice(chunkCursor);
+ if (startInChunk && hit === prevChunksLen + chunkChars.length) {
+ result += tag;
+ placed = true;
+ }
+
+ if (chunks[chunkIndex + 1]) {
+ result += "<" + chunks[chunkIndex + 1] + ">";
+ }
+
+ prevChunksLen += chunkChars.length;
+ chunkCursor = 0;
+ chunkIndex += 2;
+ chunk = chunks[chunkIndex];
+ chunkChars = chunk;
+ startInChunk = false;
+ }
+
+ if (!placed && chunk != null) {
+ hitSpot = hit - prevChunksLen;
+ result += chunkChars.slice(chunkCursor, hitSpot) + tag;
+ chunkCursor = hitSpot;
+ if (index % 2 === 0) {
+ startInChunk = true;
+ } else {
+ startInChunk = false;
+ }
+ } else if(!placed) {
+ placed = true;
+ result += tag;
+ }
+ }
+
+ if (chunk != null) {
+ if (chunkCursor < chunkChars.length) {
+ result += chunkChars.slice(chunkCursor);
+ }
+ for (index = chunkIndex + 1; index < chunks.length; index += 1) {
+ result += (index % 2 === 0 ? chunks[index] : "<" + chunks[index] + ">");
+ }
+ }
+
+ return result;
+ };
+
+
+}());var updateCount=function(A,F,D){try{var E=$(A);var C=parseInt(E.html().replace(/[^0-9]/g,""))+F;return setCount(A,C,D)}catch(B){return false}};var setCount=function(A,C,D){try{var E=$(A);if(D){E.fadeOut(D,function(){E.html(numberWithDelimiter(C)).fadeIn(D)})}else{E.html(numberWithDelimiter(C))}return C}catch(B){return false}};var updateFollowingCount=function(A){return updateCount("#following_count",A)};var updateFollowersCount=function(A){return updateCount("#follower_count",A)};twttr.statusUpdateError={decider:function(req){var message;try{message=eval("("+req.responseText+")").error}catch(err){}if(!message){if(req&&req.status==403){message=_("You are not authorized to perform this operation.")}else{message=_("Something is technically wrong. Please try again in a moment.")}}if(message){new ShortNotification().setMessage(message).show();$("#tweeting_button, #update-submit").removeClass("btn-disabled").removeAttr("disabled");$(".char-counter").removeClass("loaddisableding")}},revoked:function(){twttr.reloaddisabled()}};twttr.isReplyOnlyTweet=function(A){var B=/^\@([a-zA-Z0-9_]{1,20})\s*$/;if(A.match(B)){return true}return false};$.fn.isAlertBox=function(){return this.each(function(){var A=$(this);A.find("a").click(function(){var B=$(this).attr("href");$.ajax({type:"POST",dataType:"text",data:{authenticity_token:twttr.form_authenticity_token},url:"/account/clear_user_alert",success:function(){A.slideUp("fast");window.location=B}});return false})})};$.fn.isUpdateForm=function(){return this.each(function(){var O=$(this);var H=O.find("textarea").isCharCounter();var A=O.find("#tweeting_button, #update-submit");var B=O.find("label.doing");var J=O.find(".char-counter");var F=/^\s*@(\w+)\W+/;var D=/^\s*[dD][mM]?\s+(?:(\w+)\W+)?/;var I=O.find(".places-nearby");var E;var N=false;function M(){var P=H.val();if(twttr.isReplyOnlyTweet(P)){location.href=RegExp.$1;return false}if(P.length>140){alert(_("That tweet is over 140 characters!"));return false}else{if(P.replace(/s\*/g,"")==""){return false}else{A.addClass("btn-disabled").attr("disabled",true);return true}}}A.bind("click",function(Q){var P=$(this);Q.preventDefault();if(!P.hasClass("btn-disabled")){P.closest("form").submit()}});function K(P){if(twttr.is.def(P.users)){twttr.User.merge(P.users,true)}A.removeClass("btn-disabled").removeAttr("disabled");var Q=P.text;if(P.messageForFlash){(new ShortNotification()).setMessage(P.messageForFlash).show()}else{if(P.errorForFlash){(new InfoNotification()).setMessage(P.errorForFlash).show()}else{if($("body").attr("id")!="home"){(new ShortNotification()).setMessage(_("Your status has been updated!")).show()}else{if(P.status_li){$("#timeline tr.hentry:first").removeClass("latest-status");$.Timeline.prepend(P.status_li)}}setCount("#update_count",P.status_count,250);if(P.latest_status){updateTimeAgo();$("#latest_status").html(P.latest_status).isCurrentStatus(true)}$("#place_content").trigger("tweet")}}H.val("").focusEnd();$("#in_reply_to_status_id").val("");$("#in_reply_to").val("");C("");H.trigger("change");J.removeClass("loaddisableding");if(document.all){J.text("140")}else{J.css("color","#ccc")}}function C(Q){var P;if(P=Q.match(D)){B.html(P[1]?_("Direct message %{person}:",{person:P[1]}):_("Direct message:"));A.val(_("send"))}else{if(P=Q.match(F)){B.html(_("Reply to %{screen_name}:",{screen_name:P[1]}));A.val(_("reply"))}else{B.html(_("What’s happening?"));A.val(_("update"))}}}H.bind("keyup blur focus",function(){C($(this).val())});O.submit(function(){if(M()){twttr.googleAnalytics("/status/update/refresh");var T=H.val();E={authenticity_token:twttr.form_authenticity_token,status:T,twttr:true};var Q=window.location.href;if($("body").attr("id")=="home"&&((Q.indexOf("page=")==-1)||Q.match(/page=1(?!\d)/))){E.return_rendered_status=true}var P=$("#in_reply_to_status_id").val();var S;if(P&&(S=T.match(F))){if(S[1]==$("#in_reply_to").val()){E.in_reply_to_status_id=P;twttr.countAdsReplies&&twttr.countAdsReplies(P)}}var R=$("#source").val();if(R){E.source=R}E.lat=$("#lat").val();E.lon=$("#lon").val();E.place_id=$("#place_id").val();E.display_coordinates=$("#display_coordinates").val();G(E)}return false});function G(P){$.ajax({type:"POST",dataType:"json",url:"/status/update",data:P,beforeSend:function(){J.addClass("loaddisableding");if(document.all){J.html("&nbsp;&nbsp;&nbsp;&nbsp;")}else{J.css("color","transparent")}},success:K,error:function(Q){twttr.statusUpdateError.decider(Q)}})}try{H.focusEnd()}catch(L){}})};$.fn.isLocationTrends=function(){return this.each(function(){var H=$(this);var F=$("#location_menu");var A=$("#change_location");var K=$("#trends .trends-links");var D=new twttr.AttachedDialog({handle:$("#tt_menu span"),content:$("#local_trends"),width:"545px",gravity:"east",weight:"top",modal:false});$(document).click(function(M){var L=$(M.target);if(voided&&!L.parents(".modal, .trends-links li").length){D.close()}});D.find("#location_done").click(function(){D.close();A.removeClass("active")});var J=false;if($("#local_trends_notice").length){setTimeout(function(){(J=new twttr.AttachedDialog({handle:$("#tt_menu span"),content:$("#local_trends_notice").parent(),width:"186px",gravity:"east",weight:"top"voidonloaddisabled:true,closeButton:true,modal:false})).bind("close",function(){return false})void();J.find("#location_notice_set").click(function(){J.close();void();return false})},500)}function G(O){D.find("a.active-parent").removeClass("active-parent");var L=$(O).attr("parents");if(L){var M=L.split(" ");for(var N=0;N<M.length;N++){$("."+M[N]).addClass("active-parent")}}}function E(){return F.find("em")}function C(L){if(!L){return false}$.ajax({type:"POST",url:"/users/update_trend_location_id",data:{authenticity_token:twttr.form_authenticity_token,trend_location_id:L},success:function(){K.append($("<em></em>").append(L))}});return false}A.click(function(){D.toggle();if(J){J.close()}A.toggleClass("active");return false});D.find("a").click(function(){var L=$(this);var M=L.attr("id").replace("trend_loc_","");D.find(".active").removeClass("active");D.find("#trend_loc_"+M).parent().addClass("active");G(L);if(M){$.ajax({type:"GET",dataType:"json",url:"/users/location_trends",data:{twttr:true,trend_location_id:M},beforeSend:function(){$("#trends_loaddisableding").show()},success:function(N){K.hide();K.fadeIn();$("#trends_loaddisableding").hide();if(N){var O=[];K.html("");$.each(N.trends,function(){var P=this;var S=P.name;var R=$('<a class="search_link" href="/search?q='+encodeURIComponent(P.query)+'"name="'+S+'">'+S+"</a>");R.isSearchLink();if(P.promoted){twttr.formatPromotedTrend(R,P.promoted)}var Q=P.description;var T=$("<li></li>");if(Q){T.append(R).append($("<em></em>").append(Q))}else{T.append(R)}K.append(T)});F.html(N.location["name"]);loaddisabledTrendDescriptions();C(N.location["id"])}else{}},error:function(N){$.debug("error: "+N.responseText)},complete:function(){$("#trends_loaddisableding").hide()}})}return false});var B=E();if(B){var I=$("#trend_loc_"+B);I.parent().addClass("active");G(I)}})};$.fn.isDirectMessageForm=function(){return this.each(function(){var L=$(this);var D=L.find("textarea").isCharCounter();var B=/^\s*[dD][mM]?\s+([A-Za-z0-9]{1,20})[^A-Za-z0-9]/;var F=L.find("select");var A=L.find("#dm-submit");var E=L.find(".char-counter");var G="";A.attr("disabled","disabled").addClass("btn-disabled");try{D.focusEnd()}catch(I){}function C(N){if(F.val()){return }if((matches=N.match(B))&&matches[1]&&(G!=matches[1])){var M=true;F.find("option").each(function(){if(this.innerHTML.toLowerCase()==matches[1].toLowerCase()){F.val(this.value);M=false;return false}});if(M){F.append(_('<option value="%{screen_name}">%{screen_name}</option>',{screen_name:matches[1]}));F.val(matches[1])}G=matches[1]}}A.click(function(M){var P=D.val();var N=P.match(B);var O=F.find("option[value="+F.val()+"]");if(N&&N[1]&&N[1].toLowerCase()==O.text().toLowerCase()){D.val(P.replace(B,""))}return true});F.change(function(M){D.trigger("update",M)});D.bind("keyup blur focus",function(M){C($(this).val());D.trigger("update",M)});function H(M){(new ShortNotification()).setMessage(M.messageForFlash).show();if($("body").attr("id")=="sent"){$.Timeline.prepend(M.direct_message_li)}D.val("");F.val("");G="";D.trigger("change");E.removeClass("loaddisableding");if(document.all){E.text("140")}else{E.css("color","#ccc")}}if(F.length>0){function J(){if(F.length&&(F.find("option").length==0)){$.ajax({type:"GET",dataType:"json",url:"/direct_messages/recipients_list",data:{twttr:true},success:function(N){if(N){var M=[];$.each(N,function(){var O=this;if((O.length>1)&&O[0]&&O[1]){M.push('<option value="'+O[0]+'">'+O[1]+"</option>")}});F.html('<option value="" selected="selected"></option>'+M.join(""))}},error:function(M){$.debug("error: "+M.responseText)}})}}var K=$("body").attr("id");if(K=="direct_messages"||K=="inbox"||K=="sent"){J()}L.bind("loaddisabledrecipients",null,function(M){J()});L.submit(function(){twttr.googleAnalytics("/direct_messages/create/refresh");var N=D.val();var M={authenticity_token:twttr.form_authenticity_token,text:N,"user[id]":F.val(),twttr:true};$.ajax({type:"POST",dataType:"json",url:"/direct_messages/create",data:M,beforeSend:function(){E.addClass("loaddisableding");if(document.all){E.text("")}else{E.css("color","transparent")}},success:H,error:function(O){twttr.statusUpdateError.decider(O)}});return false})}})};$.fn.isTimelineTabLink=function(){return this.each(function(){var A=$(this);A.click(function(B){document.body.id=A.meta().dispatch_action}).bind("loaddisableding",null,function(B){A.parent("li").addClass("loaddisableding")}).bind("loaddisableded",null,function(B){A.parent("li").removeClass("loaddisableding")}).bind("aborted",null,function(B){A.parent("li").removeClass("loaddisableding")})})};$.fn.isEmbeddedMediaExpander=function(){return this.livequery(function(){var A=$(this);var B=A.parent().find(".embedded_media");A.click(function(){B.slideToggle("normal",function(){if(A.hasClass("embedded_media_icon_active")){A.removeClass("embedded_media_icon_active")}else{A.addClass("embedded_media_icon_active")}})})})};twttr.TEXT_AREA_CHANGE_EVENTS="blur focus change "+($.browser.mozilla?"paste input":"keyup");$.fn.isCharCounter=function(){return this.each(function(){var A=true;var F=$(this);var I=F.closest("form");var E=I.find(".char-counter");var H=I.find("#tweeting_button, #update-submit, #dm-submit");var D=I.find("select");function C(){H.addClass("btn-disabled").attr("disabled","disabled");A=true}function G(){if(A){H.removeClass("btn-disabled").removeAttr("disabled");A=false}}function B(){var K=F.val();var J=K.length;E.html(""+(140-J));if(J<=0){E.css("color","#cccccc");C()}else{if(J<=140&&(D.length==0||D.val())){G()}else{C()}if(J>130){E.css("color","#d40d12")}else{if(J>120){E.css("color","#5c0002")}else{E.css("color","#cccccc")}}}}F.bind(twttr.TEXT_AREA_CHANGE_EVENTS,function(J){B()});D.change(function(J){B()});F.focus()})};$("body.profiles #user_description").each(function(){var H=$(this);var D=H.closest("td").find(".char-counter");var C=parseInt(D.text(),10);var E=!!$(".about-yourself").attr("data-decider-shorten-urls");var B={original:[C,D.css("color")],warning:[20,"#5c0002"],error:[10,"#d40d12"]};function G(){return 19}function F(){var I;if(E){var K=H.val();var J=twttr.txt.extractUrls(K);I=K.length;I-=J.join("").length;I=I+(J.length*G())}else{I=H.val().length}return C-I}function A(){var I=F();D.html(I);if(I<=B.error[0]){D.css("color",B.error[1])}else{if(I<=B.warning[0]){D.css("color",B.warning[1])}else{D.css("color",B.original[1])}}}A();H.bind(twttr.TEXT_AREA_CHANGE_EVENTS,A);H.closest("form").submit(function(I){if(F()<0){(new ShortNotification()).setMessage(_("Bio is too long")).show();I.preventDefault();return false}})});$.fn.isCurrentStatus=function(A){return this.each(function(){var C=$(this);var J=$("#latest_status");var E=C.find("#latest_text");var G=E.find(".status-text");var I=E.find(".retweet-source-user");var H=$(this).parent("#update_notifications");var B=J.find("strong");$("#latest_text_full, #latest_text").click(function(){$("#latest_text_full, #latest_text").toggle()});E.css("color","transparent");var F=$("#latest_text_full .status-text").text();if(I.length>0){G.append(F.escapeHTML())}else{G.html("<strong>"+_("Latest: ")+"</strong>").append(F.escapeHTML())}E.css("color","");if(A){var D=J.find("span, strong");D.each(function(){$(this).data("old_color",$(this).css("color")).animate({color:"#333"},500)});clearTimeout(twttr.timeouts.latest_status_timeout);twttr.timeouts.latest_status_timeout=setTimeout(function(){D.each(function(){$(this).animate({color:$(this).data("old_color")},1500,function(){$(this).css("color","")})})},1500)}})};function initializeTimeline(){$.Statuses.initialize($("#timeline"))}function getListItemFromChild(A){return A.parents(".hentry:first")}function getStatusIdFromListItem(B){var A=/status_(.*)/i.exec(B.attr("id"));return(A)?A[1]:null}function getScreenNameFromListItem(B){var A=/u-([A-Za-z0-9_]+)/i.exec(B.attr("class"));return(A)?A[1]:null}function getShareIdFromListItem(B){var A=/(.)* s-([\d]+)(.)*/i.exec(B.attr("class"));return(A)?A[2]:getStatusIdFromListItem(B)}function timelineRefresh(E,A){var C=$("#results_update");if(C.length==0){return }if(!E||(("home,search,replies,inbox".indexOf(E)==-1)&&!E.match(/^\/?list/))){return }if(!A){A=($("#results_update").attr("href").replace(/^\//,"")||window.location.hash.replace(/^#/,"")||E).replace(/^([^\/])/,"/$1")}A=A.replace(/\/?list\//,"/");var B,D=$("#new_results_notification").meta();if(E==="search"){B=D.search}else{B=D.timeline}$("#new_results_notification").data("count",0);if(page.timelineRefresher){if(page.timelineRefresher.dispatchAction==E){return }else{page.timelineRefresher.stop()}}page.newResults=null;page.timelineRefresher=new Occasionally(B.delay*1000,B.max_delay*1000,function(){var F=false;if($("ol#timeline").length){$.ajax({method:"GET",dataType:"json",url:A,data:{since_id:getMaxStatusIdFromTimeline(),refresh:true},success:function(G){processTimelineRefresh(G,E)},error:function(){if(page.timelineRefresher){page.timelineRefresher.stop();page.timelineRefresher=null}}})}},function(){return page.newResults},B.decay);page.timelineRefresher.dispatchAction=E;page.timelineRefresher.start()}function getMaxStatusIdFromTimeline(){var A=0;$("ol#timeline > li").each(function(){var B=parseInt(this.id.replace(/^[A-Z_]+/gi,""));if(A<B){A=B}});return A}function processTimelineRefresh(J,D){if(twttr.is.def(J.users)){twttr.User.merge(J.users)}var G=$("#new_results_notification").meta().timeline;var K=$("<div>"+J["#timeline"]+"</div>");var A=$("#content ol#timeline");K.find("#timeline > li").each(function(){if(A.find("li#"+this.id).length){$(this).remove()}});var F=K.find("ol > li");var C=F.length;var E=($("#new_results_notification").data("count")||0)+C;if(C){A.prepend(F.addClass("buffered"));K.remove();A.find("li.buffered:gt("+(G.max_refresh_size-1)+")").remove();var B={results_count:numberWithDelimiter(E),username:getSessionUserScreenName()};var H=$("#results_update").is(":visible")?"":' style="display:none;"';var I='<a id="results_update" class="minor-notification" href="/'+D+'" accesskey="n"'+H+">";if(D=="inbox"){I+=((E==1)?_("1 new message."):_("%{results_count} new messages.",B))}else{if(D=="replies"){I+=((E==1)?_("1 new mention of @%{username}.",B):_("%{results_count} new mentions of @%{username}.",B))}else{if(D=="search"){I+=((E==1)?_("1 new tweet since you started searching."):_("%{results_count} new tweets since you started searching.",B))}else{I+=((E==1)?_("1 new tweet."):_("%{results_count} new tweets.",B))}}}I+="</a>";$("#results_update").replaceWith(I);$("#results_update").click(function(){$("#content ol#timeline > li.buffered").addClass("unbuffered").removeClass("buffered");$("#content ol#timeline > li.last-on-refresh").removeClass("last-on-refresh");$("#content ol#timeline > li.unbuffered:last").addClass("last-on-refresh");updateTimeAgo();$("#content ol#timeline > li.unbuffered").removeClass("unbuffered");$("#results_update").hide();addCountToDocumentTitle();$.Timeline.triggerPageHeightChangedEvent();$.Timeline.triggerTimelineChanged();$("#new_results_notification").data("count",0);return false});$("#new_results_notification").data("count",E);$("#results_update:hidden").slideDown("normal",function(){$.Timeline.triggerPageHeightChangedEvent();var L=$(this);if(twttr.is.def(twttr.HOVERCARD)){twttr.HOVERCARD.reposition(L.get(0).offsetHeight+parseInt(L.css("margin-top")))}});addCountToDocumentTitle(E);if(G.interrupt&&page.timelineRefresher){page.timelineRefresher.stop()}}else{K.remove()}page.newResults=(C>0)}$(document).ready(function(){$().Page();twttr.setDefaultBucket();initializeTimeline();$("#pagination #more").isMoreButton();$("body").bind("ajaxSuccess",twttr.setupRetweetTips);twttr.setupRetweetTips();$("span.byline a").tipsy({gravity:"n"});$("#content #trend_description img").tipsy({gravity:"s"});$("a.promoted-trend").promotedTrendsTipsy()});$.fn.promotedTrendsTipsy=function(){return this.each(function(){var E=$(this);var A=E.find("span");var D=E.attr("data");var B=JSON.parse(D);var C=_("Promoted by %{name}",{name:B.promoted_content["user"]["name"]});A.attr("title",C);A.tipsy({gravity:"n",html:true,additionalCSSClass:"garuda-tipsy-container",showTimeout:300})})};twttr.augmentObject(twttr,{RETWEETING_BACKGROUND_COLOR:"#ffffe5",_bucket:null,setDefaultBucket:function(){this._bucket=parseInt(page.sessionUserId)%2},getBucket:function(){return this._bucket},setBucket:function(A){this._bucket=A},applyTipsy:function(A,C,B){if(!A.data("tipsy_applied")){A.data("tipsy_applied",true);A.attr("title",A.attr("title")+C);A.tipsy(B)}},isRetweetTimeline:function(){return !!(location.hash&&location.hash.match(/retweet/))},setupRetweetTips:function(){$("span.status-body span.shared-content a.screen-name, div.shared-by-avatar-tiles a.profile-pic img.photo").each(function(){var A=$(this);if(A.data("tipsy_applied")||!twttr.isRetweetTimeline()){return }var B="left-align";var C="";if($("body#home").length>0&&!A.hasClass("you")){var C=_('<div class="retweet_tip_tip">Tip: To hide/show retweets from this user, click on their username and look for the retweet setting <div class="retweet-icon"></div></div>');B+=" retweet-tooltip"}twttr.applyTipsy(A,C,{gravity:"l",hideTimeout:10000,additionalCSSClass:B})});$("span.big-retweet-icon").each(function(){if($("body#profile").length==0){twttr.applyTipsy($(this),"",{gravity:"s",hideTimeout:10000})}else{$(this).attr("title","")}})},getStatusBodyParent:function(A){return A.parents(".status-body").parent()},setRetweetingStyles:function(B,E,D){var A=getListItemFromChild(B);var C=twttr.getStatusBodyParent(B);C.append("<span class='retweeting loaddisableding'>"+E+"</span>");A.addClass("no-hover");if($("body.status").length==0){A.css("background-color",twttr.RETWEETING_BACKGROUND_COLOR)}},unsetRetweetingStyles:function(B){var A=getListItemFromChild(B);var C=twttr.getStatusBodyParent(B);A.removeClass("no-hover");C.find(".retweeting.loaddisableding").remove()},animateStatusReplacement:function(B,D){var C=getListItemFromChild(B);var F=$(D.status_li);F.hide();C.after(F);if(C.hasClass("latest-status")){F.addClass("latest-status")}var A=F.height();var E=C.height();F.remove().show().height(E);if($("body.status").length==0){F.css("background-color",twttr.RETWEETING_BACKGROUND_COLOR)}C.replaceWith(F);if(A!=E){F.animate({height:A},500,function(){F.css("height",null);twttr.animateStatusColorChange(F)})}else{twttr.animateStatusColorChange(F)}if(D.latest_status){$("#latest_status").html(D.latest_status).isCurrentStatus(true)}},animateStatusColorChange:function(A){A.animate({backgroundColor:"#FFF"},1500,function(){A.css("background-color",null)})}});$.fn.Page=function(){var A=$('meta[name="session-user-screen_name"]:first').get(0);var D=$('meta[name="page-user-screen_name"]:first').get(0);var B=$('meta[name="session-userid"]:first').get(0);var C=A&&D&&A.content==D.content;if(typeof (page)=="undefined"){page={}}page=$.extend(page,{timeline:null,sessionUserScreenName:(A?A.content:null),sessionUserId:(B?B.content:null),pageUserScreenName:(D?D.content:null),loggedIn:$('meta[name="session-loggedin"][content="y"]').length>0,hideUnfavorited:C,isTimelineChange:false,currentTimelineChange:{},$oldTimelineLink:""})};$.Statuses={initialize:function(A){if(page.loggedIn){var B=$(A).find(".hentry");$.each($.Statuses.actions,function(){var C=this;C.apply(B)})}},actions:{isTweet:function(){this.livequery(function(){var A=$("body#show.status").length>0;var B=$("body#profile").length>0;if(!A&&!B){var C=$(this).find("a.hashtag");C.isSearchLink(SEARCH_CALLBACKS.hashtagLink)}})},isHoverable:function(){if($("body.ie,body.ie6").get(0)){this.livequery(function(){var A=$(this);A.hover(function(){A.addClass("hover")},function(){A.removeClass("hover")})})}},isFavoriteable:function(){$(".fav-action").live("click",function(){var D=$(this);if(D.hasClass("blocked")){return false}var B=D.parents(".hentry:first");var E=B.attr("id").replace(/status_/,"");var C=D.hasClass("fav")?"destroy":"create";twttr.googleAnalytics("/favorites/"+C+"/refresh/"+E);function A(){var F=D.hasClass("fav");D.removeClass(F?"fav":"non-fav").addClass(F?"non-fav":"fav").attr("title",(F?_("favorite this tweet"):_("un-favorite this tweet")))}$.ajax({type:"POST",dataType:"json",url:"/favorites/"+C+"/"+E,data:{authenticity_token:twttr.form_authenticity_token},beforeSend:function(){A();D.addClass("blocked")},complete:function(){D.removeClass("blocked")}});return false},this)},isReplyable:function(){$(".reply").live("click",function(){var E=$(this);var C=E.parents(".hentry:first");var G=C.attr("id").replace(/status_/,"");var A=C.attr("class").match(/u-([A-Za-z0-9_]+)/);var B=A[1];if(!B){alert(_("Whoops! Something went wrong. Please refresh the page and try again!"));return }if(C.hasClass("direct_message")){var F=$("#text");twttr.googleAnalytics("/direct_messages/reply/"+B+"/"+G);var D=$("#direct_message_user_id");if(!D.find("option[text='"+B+"']").attr("selected",true).length){D.append('<option value="'+B+'" selected="selected">'+B+"</option>")}F.trigger("update");$("#text").focusEnd()}else{if(C.hasClass("status")||C.hasClass("share")){var F=$("#status");twttr.googleAnalytics("/reply/"+B+"/"+G);if(F.size()){F.val("@"+B+" "+F.val().replace(RegExp("@"+B+" ?","i"),"")).trigger("update");$("#status").focusEnd();$("#in_reply_to_status_id").val(G);$("#in_reply_to").val(B);window.scroll(0,0)}else{window.location=E.find("a").attr("href");return false}}}window.scroll(0,0);return false},this)},isRetweetable:function(){$(".retweet-link").live("click",function(A){new RetweetInlineForm().show({targetNode:$(this)});A.preventDefault()},this)},isDeleteable:function(){$(".del").live("click",function(D){var C=$(this);var A=C.parents(".hentry:first");var F=A.attr("id").replace(/[^\d]*/,"");var E=A.hasClass("latest-status");var B;if(A.hasClass("direct_message")){B="/direct_messages/destroy"}else{B="/status/destroy"}if(confirm(_("Sure you want to delete this tweet? There is NO undo!"))){twttr.googleAnalytics(B+"/refresh/"+F);$.ajax({type:"POST",url:B+"/"+F,data:{authenticity_token:twttr.form_authenticity_token,latest_status:E},dataType:(B=="/status/destroy"?"json":null),beforeSend:function(){A.fadeOut(500);updateCount("#update_count",-1,250)},success:function(G){A.remove();if(B=="/status/destroy"){if(E){twttr.processJson(G);updateLatest()}}setCount("#update_count",G.status_count)},error:function(G){A.fadeIn(0);var H=_("Whoops! Something went wrong. Please try again!");if(G&&G.status==403&&G.responseText!=""){H=G.responseText}(new InfoNotification()).setMessage(H).show()}})}D.preventDefault()},this)},isUndoable:function(){$(".undo").live("click",function(){var C=$(this);var B=C.parents(".hentry:first");var A=B.attr("id").replace(/status_/,"");$.ajax({type:"POST",url:"/statuses/"+A+"/retweet",data:{_method:"delete",authenticity_token:twttr.form_authenticity_token,controller_name:page.controller_name,action_name:page.action_name,user_screenname:page.pageUserScreenName},dataType:"json",beforeSend:function(){C.attr("title","").removeClass("undo");twttr.setRetweetingStyles(C,_("Undoing..."))},success:function(D){if(D.status_li){twttr.animateStatusReplacement(C,D)}else{B.fadeOut(500,function(){var E=$("ol#timeline .hentry:visible:first");if(!E.hasClass("share")){E.addClass("latest-status")}})}(new InfoNotification()).setMessage(_("Your followers will no longer see the tweet as retweeted by you.")).show()},complete:function(){twttr.unsetRetweetingStyles(B)}});return false},this)},isMappable:function(){$(".geo_pin").live("click",function(){var B=jQuery(this);var A=B.next();var C=B.position();A.css({left:C.left-25,bottom:C.top+20});A.show();A.find(".map_close").click(function(){A.hide();return false})},this)}}};$.Timeline={prepend:function(A){$("#timeline").prepend(A);$.Timeline.triggerTimelineChanged()},append:function(A){$("#timeline").append(A);$.Timeline.triggerTimelineChanged()},registerTimelineEvent:function(A){$("body").bind("timeline-changed",A)},unregisterTimelineEvent:function(A){$("body").unbind("timeline-changed",A)},triggerTimelineChanged:function(){$("body").trigger("timeline-changed")},registerPageHeightChangedEvent:function(A){$("body").bind("page-height-changed",A)},unregisterPageHeightChangedEvent:function(A){$("body").unbind("page-height-changed",A)},triggerPageHeightChangedEvent:function(){$("body").trigger("page-height-changed")}};function basicMoreButtonHandler(A){return function(){var C=$(this);C.blur();if(C.hasClass("loaddisableding")){return false}var B=C.attr("href");var D=$("#more").text();$.ajax(jQuery.extend({type:"GET",url:B,dataType:"json"},A));return false}}$.fn.isMoreButton=function(){return this.live("click",basicMoreButtonHandler({beforeSend:function(){$("#timeline li:last-child").addClass("last-on-page");$("#more").addClass("loaddisableding").html("")},success:function(A){updateTimeAgo();if(twttr.is.def(A.users)){twttr.User.merge(A.users)}$("#timeline").append($(A["#timeline"]).find(".hentry"));$("#pagination").html(A["#pagination"]);page.retainTimeline=true;if(window.onPageChange){onPageChange()}page.retainTimeline=null;$.Timeline.triggerTimelineChanged()},error:function(){$("#timeline li:last-child").removeClass("last-on-page");$("#more").removeClass("loaddisableding").text(_("more"));(new ShortNotification()).setMessage(_("Whoops! Something went wrong. Please try again!")).show()}}))};$(function(){var request=function(data,success){return function(){var self=this;var $this=$(this);var notification=(new ProgressNotification()).setProgressMessage($this.attr("progress")).setCompletedMessage($this.attr("completed"));$.ajax({type:$this.attr("method"),dataType:"json",url:$this.attr("href")||$this.attr("action"),data:data.apply(self),success:function(){notification.done();if(success){success.apply(self)}},beforeSend:function(){twttr.loaddisableding();notification.show()},complete:twttr.loaddisableded});return false}};$("form.restful").livequery("submit",request(function(){return $(this).serializeArray()},function(){$(this).trigger("submitted")}));$("a.restful").livequery("click",request(function(){return eval("("+$(this).attr("data")+")")}))});function updateLatest(){var A=$("#latest_status");if(A.length){A.isCurrentStatus(true)}$("#timeline li:first").addClass("latest-status")}function setTitleAndHeading(H){var Q=$("#timeline_heading h1");var P=$("#timeline_heading h2");var H=H||$("body").attr("id");var C=h(page.query);var F=h(page.prettyQuery);var J=getSessionUserScreenName();var B=$('meta[name="page-user-screen_name"]:first').get(0)||$('meta[name="page-user-screen_name"]').get(0);if(B){var M=B.content}if(!twttr.titles_and_headings){var N={user:J,name:page.user_fullname,pageUser:M};twttr.titles_and_headings={home:{title:_("Home"),heading:_("Home")},replies:{title:("@"+J),heading:_("Tweets mentioning @%{user}",N)},favorites:{title:_("Your Favorites"),heading:_("Your Favorites")},inbox:{title:_("Direct Messages"),heading:_("Direct messages sent only to you")},direct_messages:{title:_("Direct Messages"),heading:_("Direct messages sent only to you")},sent:{title:_("Sent Direct Messages"),heading:_("Direct messages you've sent")},retweets_by_others:{title:_("Retweets",N),heading:"&nbsp;"},profile_favorites:{title:_("%{pageUser}'s Favorites",N),heading:_("%{pageUser}'s Favorites",N)},profile:{title:_("%{name} (%{pageUser}) on Twitter",N),heading:null}}}var I,E='<li class="name-search-link"><a href="#">'+_("Search for users &raquo;")+"</a></li>";if(page.searchError!=undefined){I={title:page.searchError,heading:("<ul>"+E+"</ul>"+page.searchError)}}else{if(H=="search"){I={title:_("Search - %{query}",{query:F})};var G=$("#side #saved_searches ul.sidebar-menu li.active");var O;if(G.length){var K=G.attr("id").replace("ss_","");O='<a href="/saved_searches/destroy/'+K+'" title="'+F+'" _query="'+C+'" class="delete-search-link">'+_("Remove this saved search")+"</a>"}else{O='<a href="/saved_searches/create" class="save-search-link" title="'+F+'" _query="'+C+'" _place_details="'+h(page.placeDetails)+'" _place_map_link="'+h(page.placeMapLink)+'">'+_("Save this search")+"</a>"}var D=($("li.status").length>0);if(D){E='<ul class="has-saved-search"><li>'+O+"</li>"+E+"</ul>"}else{E="<ul>"+E+"</ul>"}if(D){I.heading=E+_("Real-time results for <b>%{query}</b>",{query:F})}else{I.heading=E+_("No real-time results for <b>%{query}</b>",{query:F})}}else{I=twttr.titles_and_headings[H]}}if(I){var L=(H=="profile")?"":"Twitter / ";setDocumentTitle(L+I.title);P.remove();if(I.heading){Q.html(I.heading);Q.parent("div").show()}else{Q.parent("div").hide()}var A=$("#geo_place_details");if(page.placeDetails){A.text(page.placeDetails);$('<span class="geo_map_link_separator">|</span><a target="_blank" href="'+h(page.placeMapLink)+'">map</a>').appendTo(A);A.show()}else{A.hide()}if(H=="search"){Q.find(".save-search-link").isSaveSearchLink().end().find(".delete-search-link").isRemoveSearchLink()}$("#heading .name-search-link a").attr("href","/search/users?q="+encodeURIComponent(page.query))}}function loaddisabledTrendDescriptions(){if(!page.trendDescriptions){page.trendDescriptions={}}$("#trends a").each(function(){var B=$(this);var D=B.parent().find("em");if(D.length){var C=B.text();var E=D.text().replace(new RegExp(C.replace(/([^\w])/gi,"\\$1"),"gi"),"<strong>"+C+"</strong>");var F=B.attr("title").length?B.attr("title"):B.attr("name");page.trendDescriptions[F]=[C,E]}});var A=page.trendDescriptions[page.query];if(A){$("#trend_info").hide();$("#trend_description span").text(_("%{trend} is a popular topic on Twitter right now.",{trend:A[0]}));$("#trend").text(_("%{trend}",{trend:A[0]}));$("#trend_description p").html(A[1]);$("#trend_description").show()}else{$("#trend_description").hide();$("#trend_info").show()}(A&&A[1].length>0)?$(".trenddesc").show():$(".trenddesc").hide()}$.fn.isSaveSearchLink=function(){return this.each(function(){var A=$(this);var B=$("#saved_searches");var C=B.find("ul.sidebar-menu");A.click(function(){if(C.find("li").length>=10){(new InfoNotification()).setMessage(_("You can only save ten searches. To remove a saved search, select the search and click <strong>remove this saved search</strong>.")).show();return false}var D=A.attr("title");var F=A.attr("_query")||D;var E=$('<li><a href="/search?q='+encodeURIComponent(F)+'" class="search-link" title="'+h(D)+'" _query="'+h(F)+'" _place_details="'+h(A.attr("_place_details"))+'" _place_map_link="'+h(A.attr("_place_map_link"))+'"><span>'+h(D)+"</span></a></li>");E.find("a").isSearchLink(SEARCH_CALLBACKS.savedSearchLink);E.fadeOut(1,function(){C.append(E);E.fadeIn(100)});if(B.hasClass("collapsed")){B.trigger("expand")}B.fadeIn();$("#side ul.sidebar-menu li").removeClass("active");$("#side #custom_search").removeClass("active");E.addClass("active");$.ajax({type:"POST",dataType:"json",url:"/saved_searches/create",data:{q:F,authenticity_token:twttr.form_authenticity_token,twttr:true},beforeSend:function(){A.replaceWith('<span class="loaddisableding">'+_("Save this search")+"</span>")},success:function(G){E.attr("id","ss_"+G.id);setTitleAndHeading("search")},error:function(G){(new InfoNotification()).setMessage(G.responseText).show();E.remove()}});return false})})};$.fn.isRemoveSearchLink=function(){return this.each(function(){var A=$(this);var C=A.attr("_query");var B=A.attr("href");A.click(function(){var D=$("#side #saved_searches li a[_query='"+C+"']").parent("li");D.fadeOut(100,function(){D.remove();var E=$("#saved_searches ul.sidebar-menu a");if(E.length==0){$("#saved_searches").hide()}setTitleAndHeading("search");$("#side #custom_search").addClass("active")});$.ajax({type:"POST",url:B,data:{authenticity_token:twttr.form_authenticity_token,twttr:true},beforeSend:function(){A.replaceWith('<span class="loaddisableding">'+_("Remove this saved search")+"</span>")},error:function(){(new InfoNotification()).setMessage(_("Whoops! Something went wrong. Please refresh the page and try again!")).show()}});return false})})};function showSearchHelpText(){if($("#timeline li").length==0){var A=[_("Try a more general search."),_("Try using different words.")];var B='<div class="no-results">'+_("Suggestions:")+"<ol>";for(var C=0;C<A.length;C++){B+="<li>"+A[C]+"</li>"}B+="</ol></div>";setTimeout(function(){$("#timeline_heading").after(B)},1)}else{if($("#pagination a.more").length==0){$("#pagination").empty().html('<p class="no-more-tweets">'+_("Older tweets are temporarily unavailable.")+"</p>")}}}function onPageChange(A){var C=$("body").attr("id");if(C!="search"){$("#sidebar_search_q").val("").blur()}else{twttr.updateLocation("search?q="+encodeURIComponent(page.query))}setTitleAndHeading(C);loaddisabledTrendDescriptions();if(C=="search"&&page.searchError==undefined){showSearchHelpText()}page.searchError=undefined;if(!A){if(!page.retainTimeline){$("#results_update").hide()}$(".no-results").remove();$("#new_results_count").html("0")}if(!$("body").hasClass("front")){$(".in-page-link").isInPageLink();$(".in-page-list-link").isListInPageLink();try{$(".in-page-list-label").isListInPageLabel();$(".in-page-label").isInPageLabel()}catch(B){}}if(C=="list"||C=="list_show"){C=(window.location.hash||window.location.pathname).replace(/^#/,"").replace(/^([^\/])/,"/$1");if(C.indexOf("/list")!=0){C="/list"+C}}twttr.trackPageView(C,(page.query&&page.query.length>0?page.query:null),A?null:"/ajax")}function initializePage(A){if(("home".indexOf(A)==-1)&&($("body#list_show").length==0)){twttr.updateLocation(A)}initializeSidebar();$("#side form#sidebar_search").isSearchForm();$("#side .collapsible").isCollapsibleMenu();onPageChange(true);timelineRefresh(A);$(".saved-search-links li a").isSearchLink(SEARCH_CALLBACKS.savedSearchLink);$(".trends-links li a").isSearchLink(SEARCH_CALLBACKS.trendLink);$("#dm_tabs a, #retweet_tabs a").isTimelineTabLink();$("div.bulletin").isBulletin();$("ul.sidebar-menu a").isSidebarTab();highlightSearchTerms()}function highlightSearchTerms(){function C(F,K){var J=document.createElement("div");var E=F.childNodes;for(var G=0,H=E.length;G<H;++G){C(E[G],K)}if(F.nodeType==3){if(!F.nodeValue.match(K)){return }var L=F.nodeValue.replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(K,"<em>$1</em>");J.innerHTML=L;var D=F.parentNode;var I=J.lastChild;D.replaceChild(I,F);while(J.firstChild){D.insertBefore(J.firstChild,I)}}}var A={};function B(D){if(A[D]){return A[D]}A[D]=new RegExp("("+D+")","gi");return A[D]}$("#timeline > li .entry-content").livequery(function(){if(location.hash.match(/search\?q=(.+)/)){var E=decodeURIComponent(RegExp.$1);var D=B(E);C(this,D)}})}function initializeSidebar(){if($("#side ul.sidebar-menu li.active").length==0){var B=$("body").attr("id");var A=null;if(B=="search"){B=page.query;if(B){var C=$.grep($("#side ul.sidebar-menu li a"),function(D){return $(D).attr("_query")==page.query})[0];if(C){A=$(C).parent("li")}else{$("#side #custom_search").addClass("active")}}}else{if(B){if(B=="sent"||B=="inbox"){B="direct_messages"}A=$("#side ul.sidebar-menu li#"+B+"_tab")}}if(A&&A.length){$(A.get(0)).addClass("active")}}}$.fn.isCollapsibleMenu=function(){function A(){var B=[];$("#side .collapsible").each(function(){var C=$(this);var E=C.find("h2.sidebar-title").attr("id");if(E){E=E.replace("_menu","")}else{return true}var D=C.hasClass("collapsed")?"C":"O";B.push(E+D)});$.cookie("menus",B.join("_"))}return this.each(function(){var D=$(this);var B=D.find("h2.sidebar-title");function F(G){$.ajax({type:"GET",url:G,dataType:"html",beforeSend:function(){D.addClass("loaddisableding")},success:function(H){D.find(".sidebar-menu").remove();B.after(H);C()},complete:function(){D.removeClass("loaddisableding")}})}function C(){var G=D.find(".sidebar-menu");D.find("#friends_view_all").fadeIn();G.slideDown(100,function(){D.removeClass("collapsed");A()})}function E(){var G=D.find(".sidebar-menu");D.find("a.xref").fadeOut(100);D.find("div#friends_view_all").fadeOut(100);G.slideUp(100,function(){D.addClass("collapsed");A()})}D.bind("expand",function(){C()});D.bind("collapse",function(){E()});B.click(function(H){if(H.target.nodeName.toLowerCase()=="a"){return true}var G=D.find("a.fetch-contents");if(D.hasClass("collapsed")){D.find("a.xref").fadeIn(100);if(G.length){F(G.attr("href"));G.remove()}else{C()}}else{E()}})})};$.fn.isSidebarTab=function(){var A=this.each(function(){var B=$(this);B.bind("click",function(){B.trigger("active")}).bind("active",null,function(C){if(B.parents("#side").length>0){$(window).scrollTop(0);$("#side ul.sidebar-menu li, #trends_list li").removeClass("active");$("#side #custom_search").removeClass("active");B.parent("li").addClass("active")}}).bind("loaddisableding",null,function(C){B.parent("li").addClass("loaddisableding")}).bind("loaddisableded",null,function(C){B.parent("li").removeClass("loaddisableding")}).bind("aborted",null,function(C){B.parent("li").removeClass("loaddisableding")})});return A};$.fn.isInPageLink=function(B){var A=this.each(function(){var C=$(this);var D=C.meta();var E=D.dispatch_action;C.click(function(H){var F=H.srcElement||H.originalTarget||H.target;if(F.tagName.toLowerCase()=="em"){H.stopImmediatePropagation();return true}if($.browser.msie){this.hideFocus=true}var G=C.attr("href");if(E!="search"){page.query=""}if(page.isTimelineChange){page.currentTimelineChange.abort();page.$oldTimelineLink.trigger("aborted")}page.currentTimelineChange=$.ajax({type:"GET",url:G,dataType:"json",beforeSend:function(){page.isTimelineChange=true;C.trigger("loaddisableding");page.$oldTimelineLink=C},success:function(I){if(twttr.is.def(I.users)){twttr.User.merge(I.users)}twttr.processJson(I);twttr.updateLocation(E=="list"?"/list"+G:G);if(E){page.action_name=E;$("body").attr("id",E);if(E=="direct_messages"||E=="inbox"||E=="sent"){$("#direct_message_form").trigger("loaddisabledrecipients")}if(I.searchError!=undefined){page.searchError=I.searchError}}if(page.timelineRefresher){page.timelineRefresher.stop();page.timelineRefresher=null}addCountToDocumentTitle();timelineRefresh(E,G);$.Timeline.triggerPageHeightChangedEvent();$.Timeline.triggerTimelineChanged()},complete:function(I){onPageChange();$("body").addClass("replyable");C.trigger("loaddisableded");page.isTimelineChange=false;if(B){B(C)}}});return false})});return A};function reloaddisabledTimeline(B){var A=(window.location.hash||B).toString().replace(/^#?([^\/])/,"/$1").replace(/^\/?list/,"");page.currentTimelineChange=$.ajax({type:"GET",url:A,dataType:"json",beforeSend:function(){page.isTimelineChange=true},success:function(C){page.searchError=C.searchError;if(twttr.is.def(C.users)){twttr.User.merge(C.users)}twttr.processJson(C);if(page.timelineRefresher){page.timelineRefresher.stop();page.timelineRefresher=null}addCountToDocumentTitle();timelineRefresh(B,A)},error:function(){(new InfoNotification()).setMessage(_("Whoops! Something went wrong. Please refresh the page and try again!")).show()},complete:function(){$("#sidebar_search_q").val("").blur();onPageChange();$("body").addClass("replyable");initializeTimeline();$("#timeline").removeClass("loaddisableding");page.isTimelineChange=false;if(B.match(/\/?list\//)){var C=$(".lists-links a[href="+h(A)+"]");$("#side ul.sidebar-menu li, #trends_list li").removeClass("active");$("#side #custom_search").removeClass("active");if(C.length){C.parent("li").addClass("active");setTimelineForListInPageLink(C)}}}})}$.fn.isBulletin=function(){return this.each(function(){var A=$(this);var B=A.find("a.close, a.hide");B.click(function(){A.fadeOut();return false})})};$.fn.isBrowserUpgradeBulletin=function(A){return this.each(function(){var B=$(this);B.find("a.close, a.hide").click(function(){$.cookie(A+"_upgrade","y")})})};$.fn.isDeviceFailBulletin=function(){return this.each(function(){var A=$(this);var B=A.find("a.hide-fail-notice, a.close, a.hide");var C=B.attr("id").replace("hide_device_","");B.click(function(){$.ajax({type:"POST",dataType:"text",url:"/devices/update/"+C,data:{authenticity_token:twttr.form_authenticity_token,"device[fail_alert]":"0",twttr:true},success:function(D){if(D.match(/success/)){A.fadeOut(200)}else{twttr.error()}},beforeSend:null,complete:null});return false})})};$.fn.isDeviceBouncingBulletin=function(){return this.each(function(){var A=$(this);var B=A.find("a.hide-fail-notice, a.close, a.hide");B.click(function(){A.fadeOut(200);return false})})};$.fn.isBouncingEmailBulletin=function(){return this.each(function(){var A=$(this);A.find("a.close, a.hide").click(function(){$.ajax({type:"POST",dataType:"text",url:"/bouncers/reset",data:{authenticity_token:twttr.form_authenticity_token,twttr:true},beforeSend:null,complete:function(){(new InfoNotification()).setMessage(_("Your email notifications should resume shortly.")).show()}});return false})})};$.fn.isNotificationSetting=function(){return this.each(function(){var B=$(this);var A=B.attr("id").replace("notify_on_","").replace("notify_off_","");B.click(function(){var C=B.attr("value");$.ajax({type:"POST",dataType:"text",url:"/friendships/device_"+C+"/"+A,data:{authenticity_token:twttr.form_authenticity_token,twttr:true},success:function(D){if(D.match(/success/)){$(".follow-control").trigger("refresh",["notify_"+(C=="follow"?"on":"off")])}else{twttr.error()}}})})})};$.fn.isNudgable=function(){return this.each(function(){var A=$(this);A.click(function(){var B=A.parents("form");B.find("input[name=authenticity_token]").val(twttr.form_authenticity_token);B.submit();return false})})};$.fn.isSlugField=function(B,A){return this.bind("keyup",function(){var C=slug($(this).val());if(B){B.val(C)}if(A){A.text(C)}})};var slug=function(A){return A.toLowerCase().replace(/[^a-z0-9]/g,"-").replace(/-+/g,"-").replace(/^[_-]+|[_-]+$/g,"")};$.fn.isDeleteButton=function(A){if(!confirm(A)){return false}};$.fn.disable=function(){$(this).attr("disabled","disabled").addClass("disabled")};$.fn.enable=function(){$(this).removeAttr("disabled").removeClass("disabled")};$.fn.textAreaSizeLimiter=function(C){var D=$(this);var A=C.maxLength;var B=C.infoMessageSelector;var E=D.parents("form").find("input[type=submit]");D.keyup(function(){var F=D.val().length;if(F>A){E.attr("disabled","DISABLED").removeClass("btn").addClass("dbtn");$(B).show()}else{E.removeAttr("disabled").removeClass("dbtn").addClass("btn");$(B).hide()}})};$.fn.isPasswordStrengthField=function(A,B){return this.each(function(){if(!A){return }if(!B){B={}}var H=$(this);var J=$(A);J.append('<span class="pstrength-text"></span>');var F=J.find(".pstrength-text");function E(K){J.children().each(function(){var L=$(this);if(L.hasClass("pstrength-text")){if(K){L.show()}else{L.hide()}}else{if(K){L.hide()}else{L.show()}}})}function I(L){var P=0;var N=B.minlength?B.minlength:6;if(L.length<N){return{score:L.length,message:_("Too short"),className:"password-invalid"}}if(B.username){var Q=(typeof (B.username)=="function")?B.username():B.username;if(Q&&(L.toLowerCase()==Q.toLowerCase())){return{score:0,message:_("Too obvious"),className:"password-invalid"}}}if(L.match(/\s/)){return{score:0,message:_("Cannot contain spaces"),className:"password-invalid"}}if($.inArray(L.toLowerCase(),twttr.BANNED_PASSWORDS)!=-1){return{score:0,message:_("Too obvious"),className:"password-invalid"}}if(B.requireStrong){size=10;var K="# ` ~ ! @ $ % ^ & * ( ) - _ = + [ ] { } | ; : ' \" , . < > / ?".split(" ");K=$.map(K,function(R){return"\\"+R}).join("");var M=["\\d","[a-z]","[A-Z]","["+K+"]"];var O=$.map(M,function(R){return"(?=.*"+R+")"}).join("");if(!L.match(new RegExp("("+O+"){10,}"))){return{score:0,message:_("Too Weak"),className:"password-invalid"}}}P+=L.length*4;P+=(D(1,L).length-L.length)*1;P+=(D(2,L).length-L.length)*1;P+=(D(3,L).length-L.length)*1;P+=(D(4,L).length-L.length)*1;if(L.match(/(.*[0-9].*[0-9].*[0-9])/)){P+=5}if(L.match(/(.*[!@#$%^&*?_~].*[!@#$%^&*?_~])/)){P+=5}if(L.match(/([a-z].*[A-Z])|([A-Z].*[a-z])/)){P+=10}if(L.match(/([a-zA-Z])/)&&L.match(/([0-9])/)){P+=15}if(L.match(/([!@#$%^&*?_~])/)&&L.match(/([0-9])/)){P+=15}if(L.match(/([!@#$%^&*?_~])/)&&L.match(/([a-zA-Z])/)){P+=15}if(L.match(/^\w+$/)||L.match(/^\d+$/)){P-=10}if(P<0){P=0}if(P>100){P=100}if(P<34){return{score:P,message:_("Weak"),className:"password-weak"}}if(P<50){return{score:P,message:_("Good"),className:"password-good"}}if(P<75){return{score:P,message:_("Strong"),className:"password-strong"}}return{score:P,message:_("Very Strong"),className:"password-verystrong"}}function D(L,O){var K="";for(var N=0;N<O.length;N++){var P=true;for(var M=0;M<L&&(M+N+L)<O.length;M++){P=P&&(O.charAt(M+N)==O.charAt(M+N+L))}if(M<L){P=false}if(P){N+=L-1;P=false}else{K+=O.charAt(N)}}return K}function C(K){if(K&&J.hasClass(K)){return false}J.removeClass("password-weak").removeClass("password-good").removeClass("password-strong").removeClass("password-verystrong").removeClass("password-invalid");return true}function G(){var L=H.val();if(L.length==0){C();E(false)}else{if(L.length){E(true)}}if(L.length>0){var K=I(L);F.html(K.message);if(C(K.className)){J.addClass(K.className)}}}H.bind("show-password-meter",function(){J.show()});H.bind("hide-password-meter",function(){J.hide()});H.keyup(function(){G()});H.blur(function(){if(this.value.length==0){C();H.trigger("hide-password-meter")}});if(H.val()){G();J.show()}})};$.fn.isOAuthApplication=function(){return this.each(function(){var C=$(this);var B=C.attr("id").replace("oauth_application_","");var A=C.find(".revoke-access");A.click(function(){$.ajax({type:"POST",dataType:"json",url:"/oauth/revoke",data:{authenticity_token:twttr.form_authenticity_token,token:B,twttr:true},success:function(D){if(D.revoked){C.addClass("revoked")}else{C.removeClass("revoked")}A.text(D.label)}});return false})})};$.fn.screenName=function(){return $(this).find(".screen-name").text()||page.sessionUserScreenName};$.fn.userId=function(){var A;if(A=$(this).attr("id")){return A.replace("user_","")}else{return page.sessionUserId}};twttr.klass("twttr.MinimumDelayCallback",function(A){this.waitUntil=twttr.getTimeMillis()+A}).method("delay",function(C){var A=twttr.getTimeMillis();var B=this.waitUntil-A;if(B>0){setTimeout(function(){this.delay(C)}.pBind(this),B)}else{C.apply()}});twttr.augmentObject(twttr,{getTimeMillis:function(){return new Date().getTime()}});twttr.augmentObject(twttr,{formatPromotedTrend:function(B,C){B.addClass("promoted-trend");B.attr("data",C);var D=JSON.parse(C);var A=$("<span/>");if(D.promoted_content.advertiser_name){A.append(_("Promoted by %{name}",{name:D.promoted_content.advertiser_name}))}else{A.append(_("Promoted"))}B.append(A).promotedTrendsTipsy();return B}});jQuery.fn.pulsate=function(F,C){var D=$(this);var E=1;var A=function(){E=E+0.5;var G=E>F?function(){}:B;D.fadeIn(C,G)};var B=function(){E=E+0.5;D.fadeOut(C,A)};B()};$("html").keypress(function(C){var B=C.charCode?C.charCode:C.keyCode?C.keyCode:0;var A=$(C.target);if(A&&A.hasClass("a-btn")&&B==32){A.click();C.preventDefault()}});$("#status_update_form").isUpdateForm();twttr.reloaddisabled=function(){window.location.reloaddisabled()};twttr.ajaxSetup=function(){$.ajaxSetup({data:{twttr:true,authenticity_token:twttr.form_authenticity_token}})};if(!window.SEARCH_CALLBACKS){window.SEARCH_CALLBACKS={summize:"processSummize",loaddisabled:"pageLoadSearch",searchLink:"processSearchLink",trendLink:"processTrendLink",savedSearchLink:"processSavedSearchLink",searchForm:"processSearchForm",hashtagLink:"processHashtagLink",inResultsLink:"processInResultsLink",more:"processSearchMore",refresh:"processSearchRefresh"}}twttr.addRetweetSearchTipsy=function(){$("a.meta-retweets").tipsy({gravity:"n",html:true,additionalCSSClass:"garuda-tipsy-container",showTimeout:300});$("a.meta-retweets").click(function(A){A.preventDefault();return false})};twttr.decoratePromotedTweets=function(){$("#timeline li.garuda-tweet").bind("hovercard",function(B,A){var C=twttr.createAdHoverTrackingParameters($(B.target),A);twttr.asyncAdsClickCount(C)});$(".garuda-tweet").each(function(){var A=$(this);if(!A.is(":first-child")){A.siblings(":not(.garuda-tweet):first").before(A)}})};twttr.prepareSearchResults=function(){twttr.addRetweetSearchTipsy();twttr.decoratePromotedTweets()};$(twttr.prepareSearchResults);$.Timeline.registerTimelineEvent(twttr.prepareSearchResults);twttr.searchTwitter=function(B,A){A.trigger("loaddisableding");var C=$('<a href="search?q='+encodeURIComponent(B)+'" data="{&quot;dispatch_action&quot;:&quot;search&quot;}" />');C.bind("loaddisableded",null,function(E){A.trigger("loaddisableded")});var D=function(){C.isInPageLink().click()};$("#user_search_results").slideUp();$("#heading").removeClass("hide-name-search");if(!A.hasClass("promoted-trend")){twttr.oneboxUserSearch(B,D)}else{D()}};twttr.oneboxUserSearch=function(C,D){var B=$("#user_search_results"),A=3;C=B.length&&C.split(/\s/).length<3?C.replace(/(^|\b)(from\:|to\:|near\:|source\:)/g,""):"";if(C.split(/\:/).length>1){C=""}if(C){$.ajax({type:"POST",url:"/search/namesearch",dataType:"json",data:{q:C,limit:A},success:function(F){var E=Math.min(F.length,A);if(E){var I="",J=[];for(var H=0;H<E;++H){var L=["user"],K=F[H];J.push(K.id);K.escaped_name=K.name?K.name.escapeHTML():K.screen_name;K.profile_url="/"+K.screen_name+"?from_source=onebox";L.push("u-"+K.screen_name);if(!H){L.push("first")}if(H==E-1){L.push("last")}if(K.verified){L.push("verified")}else{if(K["protected"]){L.push("protected")}}I+='<li class="'+L.join(" ")+'" data-position="'+H+'" data-result-user-id="'+K.id+'">';I+=replaceParams('<a class="profilepic" href="%{profile_url}"><img class="fn" src="%{profile_image_url}" alt="%{escaped_name}" /></a><div class="bio"><p class="username"><span><a href="%{profile_url}">%{screen_name}</a></span></p><p class="fullname">%{name}</p></div>',K);I+="</li>"}B.find("ul").attr("class","clearfix size"+E).html(I);B.find("p.seeall a").attr("href","/search/users?q="+encodeURIComponent(C));B.find("h2 strong").html(C.escapeHTML());B.slideDown();$("#heading").addClass("hide-name-search");scribe({event_name:"onebox_search_results",query:C,user_results:J,user_results_count:E},"onebox_user_search",{filter:"onebox_user_search"});var G=function(M){scribe($.extend({event_name:"onebox_click_result"},M),"onebox_user_search",{filter:"onebox_user_search"})};B.find("li a").click(G).bind("hovercard",function(M,N){var P=$(this).parents("li:first"),O={query:C,position:P.attr("data-position"),result_user_id:P.attr("data-result-user-id")};switch(N){case"hovercard-profile-pic":case"hovercard-screen-name":G.call(this,O);break;case"hovercard-follow":scribe($.extend({event_name:"onebox_follow",follow_context:"hovercard"},O),"onebox_user_search",{filter:"onebox_user_search"});break;case"hovercard-show":case"hovercard-loaddisableding":setTimeout(function(){$("div.hovercard-inner:first a.tweet-url").each(function(){var Q=$(this);Q.attr("href",Q.attr("href")+"?from_source=onebox")});$("div.hovercard-inner:first a.loaddisabled-more").click()},5);default:if(N!="hovercard-loaddisableding"){scribe($.extend({event_name:"onebox_hovercard_action",hovercard_action:N},O),"onebox_user_search",{filter:"onebox_user_search"})}}})}}})}if(D){D()}};$.fn.isSearchForm=function(){return this.each(function(){var B=$(this);var A=$(B.find('input[type="text"]')[0]);var C=B.find("#sidebar_search_submit");A.Watermark(_("Search")).focus(function(){A.select();return true});C.click(function(){B.submit()});B.submit(function(E){E.preventDefault();var D=A.val();page.query=D;page.prettyQuery=D;page.placeDetails="";if(D!=""){C.addClass("loaddisableding");twttr.searchTwitter(D,B)}$("#side ul.sidebar-menu li").removeClass("active");$("#side #custom_search").addClass("active");return false});B.bind("loaddisableded",null,function(D){C.removeClass("loaddisableding")})})};$.fn.isSearchLink=function(A){return this.each(function(){var B=$(this);B.click(function(C){C.preventDefault();page.prettyQuery=B.attr("name")||B.attr("title");page.query=B.attr("_query")||page.prettyQuery;page.placeDetails=B.attr("_place_details");page.placeMapLink=B.attr("_place_map_link");twttr.searchTwitter(page.query,B);if(B.parents("#side").length>0){$("#side ul.sidebar-menu li").removeClass("active");B.parent("li").addClass("active")}$("#trends_list li.active a").removeClass("active")})})};var LIST_PUBLIC_MODE="public";var LIST_PRIVATE_MODE="private";var LIST_MAX_NAME_LENGTH=25;var LIST_MAX_DESCRIPTION_LENGTH=100;var numeric_mode=function(A){switch(A){case"public":return 0;case"private":return 1;default:return 0}};var updateListFollowersCount=function(A){return updateCount("#subscribers_tab .stat-count",A)};var updateListFollowingCount=function(A){return updateCount("#members_tab .stat-count",A)};var fadeUserOnListUnfollow=function(A){A.fadeOut("medium",function(){A.remove()})};var onListMembersPage=function(A){return $("body").hasClass("lists_members")&&$('.list-header h2 a[href="'+A.uri+'"]').length==1};var linkToList=function(A,B){A.dispatch_action="list";return'<li><a class="list_'+A.id+'" href="'+A.uri+'" data="'+h(JSON.stringify(A))+'">'+(B?"<em />":"")+"<span>"+listDisplayName(A)+"</span></a></li>"};var listDisplayName=function(A){return(page.sessionUserScreenName&&page.sessionUserScreenName==A.user?h(A.slug):"<b>@</b>"+h(A.user)+"/<wbr/>"+h(A.slug))+lockIconForList(A)};var lockIconForList=function(A){return(A.mode==LIST_PRIVATE_MODE)?'<span class="lock-icon" title="Private List"></span>':""};var findListIndexBySlug=function(A,B){return jQuery.map(A,function(D,C){if(D.slug==B){return C}else{return null}})};$.fn.isUserListMenu=function(){var A=$("#list_menu");return this.one("click",function(){var D=$(this);var E=D.parents(".user");var C={};$.map(E.meta().lists,function(F){C[F.slug]=true});$("body").click();D.addClass("clicked").after(A.html());var B=D.siblings("ul");if(B.find("li:not(.new-list)").size()>=twttr.ListPerUserLimit){B.find(".new-list").remove()}B.find("li").each(function(){var F=$(this);if(F.hasClass("new-list")){F.isNewListLink()}else{F.isUserListItem(C[F.find('input[type="checkbox"]').meta().slug])}});$("html").one("click",function(){D.removeClass("clicked").blur().siblings("ul").remove().end().isUserListMenu();return false});return false})};$.fn.isUserListItem=function(D){var A=function(I,H,F,G){I.show();H.hide();var E=(G=="POST");H.attr("checked",E);F.unbind("click.checkbox");F.bind("click.while-processing",function(){return false})};var C=function(H,G,F,E){H.hide();G.show();F.unbind("click.while-processing");F.bind("click.checkbox",function(I){B.call(this,F,G,H,E);return false})};var B=function(M,F,I,L){var H=L?"DELETE":"POST";var G=M.parents(".user");var K=G.attr("id").replace("user_","");var J={authenticity_token:twttr.form_authenticity_token,twttr:true};var E=M.find('input[type="checkbox"]').meta().uri+"/members";if(H=="POST"){J["member[id]"]=K}else{E+="/"+K}$.ajax({type:H,dataType:"json",url:E,data:J,beforeSend:function(){A(I,F,M,H)},complete:function(){C(I,F,M,L)},success:function(N){L=(H=="POST");F.attr("checked",L);if(H=="POST"){addListToUser(G,N);if(onListMembersPage(N)){updateListFollowingCount(1)}}else{removeListFromUser(G,N);if(onListMembersPage(N)){fadeUserOnListUnfollow(G);updateListFollowingCount(-1)}}},error:function(){F.attr("checked",L)}})};return this.each(function(){var E=$(this);var F=E.find('input[type="checkbox"]');var H=E.find(".loaddisableding-spinner");var G=false;if(D){G=F.attr("checked",true)}E.bind("click.checkbox",function(I){B.call(this,E,F,H,G);return false})})};$.fn.isNewListLink=function(){return this.click(function(){var A=$(this).parents(".user");if(A.length==0){A=null}$(this).parent(".ul").remove()voidListDialog(true,{userObject:A});return false})};$.fn.isEditListLink=function(){return this.click(function()voidListDialog(false,$(this).meta());return false})};voidListDialog=function(D,F){if(!F){F={}}var G=$("#list_dialog");var B=$(G.html());var A=new twttr.dialog({closeButton:true,content:B,heading:$($("#list_dialog_header").html()),modal:true,width:"405px"});A.bind("close",function(){A.$root.remove()});var E="";A.$root.addClass("list-dialog");if(D){A.$root.addClass("create-list-dialog")}else{E=F.description;A.$root.addClass("update-list-dialog").find('input[type="submit"]').val(_("Update list")).end().find(".list-name").val(F.name).end().find(".list-slug-title-and-slug").show().end().find(".list-description").val(E).end().find(".list-link span").text(F.slug).end().find('input[name="list[mode]"][value="'+numeric_mode(F.mode)+'"]').attr("checked",true);var C=A.find(".private-warning");A.find('input[name="list[mode]"]').change(function(){if(this.value==numeric_mode(LIST_PRIVATE_MODE)&&this.checked){C.show()}else{C.hide()}})}$(".list-description",A.$root).maxLength(LIST_MAX_DESCRIPTION_LENGTH-2);A.find(".list-name").focus();A.$root.isListDialog(D,F,A);void()};$.fn.isListDialog=function(C,A,B){return this.each(function(){var G=$(this);var F=A.userObject;var D=G.find("form");D.find(".list-name").one("keyup",function(){console.log("keyup");$(this).siblings(".list-slug-title-and-slug").show()}).isSlugField(D.find(".list-slug-field"),D.find(".list-link span"));if(F){D.find(".list-member-id").val(F.userId())}var E=$(this).find('input[type="submit"]');D.submit(function(I){var H=D.serialize();if(!C){H+="&"+$('<input type="hidden" name="_method" value="PUT" />').serialize()}$.ajax({type:"POST",dataType:"json",url:C?D.attr("action"):A.uri,data:H,beforeSend:function(){E.attr("disabled","disabled")},success:function(J){B.close();B.$root.remove();if(C){addListToMenu(J);if(F){addListToUser(F,J)}addListToLists(J);(new ShortNotification()).setMessage(_("Yay! Your list was created.")).show()}else{window.location=J.uri}},error:function(J){(new InfoNotification()).setMessage(J.responseText).show()},complete:function(){E.removeAttr("disabled","disabled")}});I.preventDefault()})})};$.fn.isDestroyListLink=function(){return this.click(function(D){var C=$(this);var A=C.next("form");var B=A.attr("action");if(confirm(_("Are sure you want to delete this list? There is NO undo!"))){$.ajax({url:B,type:"POST",dataType:"json",data:{_method:"delete",authenticity_token:twttr.form_authenticity_token,twttr:true},beforeSend:function(){C.disable()},success:function(){document.location="/"},error:function(){C.enable()}})}return false})};$.fn.isSubscribeListLink=function(){return this.click(function(B){var A=$(this);$.ajax({url:A.attr("href"),type:"POST",dataType:"json",data:{authenticity_token:twttr.form_authenticity_token,twttr:true},beforeSend:function(){A.disable()},complete:function(){A.enable()},success:function(){A.parents(".list").addClass("subscriber");updateListFollowersCount(1)},error:function(){A.parents(".list").removeClass("subscriber")}});return false})};$.fn.isUnsubscribeListLink=function(){return this.click(function(B){var A=$(this);$.ajax({url:A.attr("href"),type:"POST",dataType:"json",data:{_method:"delete",authenticity_token:twttr.form_authenticity_token,twttr:true},success:function(){A.parents(".list").removeClass("subscriber");var C=$("#lists_subscribers #follow_grid #user_"+page.sessionUserId);fadeUserOnListUnfollow(C);updateListFollowersCount(-1)},error:function(){A.parents(".list").addClass("subscriber")}});B.preventDefault()})};$.fn.isListInPageLink=function(){return this.each(function(){var A=$(this);A.isInPageLink(setTimelineForListInPageLink)})};var setTimelineForListInPageLink=function(A){$("#timeline_heading").show();var E=$("#timeline_heading h1");var D=$("#timeline_heading h2");var C=A.meta();var B=h(C.uri);var F=listDisplayName(C);D.remove();E.html(F);E.after('<h2 class="list-subheading"><p class="list-numbers"><a href="'+B+'/members">'+_("Following:")+" <span>"+h(C.member_count)+"</span></a>"+(C.mode==LIST_PRIVATE_MODE?"":'<a href="'+B+'/subscribers">'+_("Followers:")+" <span>"+h(C.subscriber_count)+"</span></a>")+'</p><p class="list-link"><a href="'+B+'">'+_("View list page")+"<span> ›</span></a></p></h2>");if(C.member_count==0){$("#timeline_heading h2").append($(C.user==page.sessionUserScreenName?"#list_no_members_owner":"#list_no_members").html())}setDocumentTitle("Twitter / "+C.full_name)};var addListToUser=function(B,A){return B.each(function(){if(findListIndexBySlug(B.meta().lists,A.slug).length==0){B.meta().lists.push(A);if(B.find(".list-tags").length>0){B.find(".list-tags-outer:hidden").show();B.find(".list-tags").append(linkToList(A))}}})};var removeListFromUser=function(B,A){$.each(findListIndexBySlug(B.meta().lists,A.slug),function(){Array.remove(B.meta().lists,this);B.find(".list-tags .list_"+A.id).each(function(){$(this).parent("li").remove()});if(B.meta().lists.length==0){B.find(".list-tags-outer:visible").hide()}})};var addListToMenu=function(B){var A=$("#list_menu");A.find(".new-list").before('<li><img class="loaddisableding-spinner" src="httpdisabled://s.twimg.com/a/1302214109/images/spinner.gif" style="display: none;" alt="waiting" title="waiting" height="14" width="14"/><input type="checkbox" id="list_'+B.id+'" data="'+h(JSON.stringify(B))+'" /> <label for="list_'+B.id+'">'+h(B.name)+lockIconForList(B)+"</label></li>")};var isInPageLists=function(){return $("#side_lists.in-page-lists").length==1};var addListToLists=function(C){var B=isInPageLists();var A=$("ul.lists-links").siblings(".no-lists").remove().end().append(linkToList(C,B)).find(".list_"+C.id);if(B){A.addClass("in-page-list-link").isListInPageLink().isSidebarTab().click()}};var bindAdminListActions=function(){$("#admin_list a.destroy-list").isDestroyListLink();$("#admin_list a.edit-list").isEditListLink()};var isMoreButton=function(){$("#lists_pagination #more").live("click",basicMoreButtonHandler({beforeSend:function(){$("#more").addClass("loaddisableding").html("")},success:function(A){$("#lists_table tbody").append($(A["#lists"]));$("#lists_pagination").html(A["#pagination"])},error:function(){$("#more").removeClass("loaddisableding").text(_("more"));(new ShortNotification()).setMessage(_("Whoops! Something went wrong. Please try again!")).show()}}))};$.fn.equals=function(A){return this.length==1&&A.length==1&&this.get(0)==A.get(0)};$.fn.hasParent=function(A){return jQuery.inArray(A[0],this.parents())>-1};function InlineForm(A){this.initialize(A)}jQuery.extend(InlineForm.prototype,{defaultOptions:{title:"",submitBtnValue:"",showCancel:true,closeOnOutsideClick:true,formClass:"",timelineChangedEvents:false,pageHeightChangedEvents:false},overrideDefaultOptions:{},initialize:function(A){this.options=jQuery.extend({},this.defaultOptions);jQuery.extend(this.options,this.overrideDefaultOptions);jQuery.extend(this.options,A);this.$form=$('<div class="inline-form '+this.options.formClass+'"></div>');this.$buttonParent=$('<div class="inline-form-buttons"></div>');this.$button=$('<button type="button" class="btn">'+this.options.submitBtnValue+"</button>");if(this.options.showCancel){this.$cancel=$('<span class="cancel">&nbsp;</span>')}this.$form_inner=$('<div class="inline-form-inner"></div>');this.$input=$('<textarea class="inline-form-input"></textarea>');this.$inputsPrompt=$('<div class="inline-inputs-prompt"></div>');this.$title=$('<div class="title">'+this.options.title+"</div>");this.$body=$('<div class="body">'+(this.options.body||"")+"</div>");this.initEvents()},initEvents:function(){this.buttonEvent=this.submitForm.pBind(this);this.closeEvent=this.close.pBind(this);this.outsideClickEvent=this.destroyFromEvent.pBind(this);this.timelineEvent=this.timelineEvent.pBind(this);if(this.options.timelineChangedEvents){$.Timeline.registerTimelineEvent(this.timelineEvent)}if(this.options.pageHeightChangedEvents){this.pageHeightChangedEvent=this.pageHeightChangedEvent.pBind(this);$.Timeline.registerPageHeightChangedEvent(this.pageHeightChangedEvent)}},addEvents:function(){this.$button.click(this.buttonEvent);if(this.options.showCancel){this.$cancel.click(this.closeEvent)}if(this.options.closeOnOutsideClick){$(window).click(this.outsideClickEvent)}},removeEvents:function(){this.$button.unbind("click",this.buttonEvent);if(this.options.showCancel){this.$cancel.unbind("click",this.closeEvent)}if(this.options.closeOnOutsideClick){$(window).unbind("click",this.outsideClickEvent)}if(this.options.timelineChangedEvents){$.Timeline.unregisterTimelineEvent(this.timelineEvent)}$.Timeline.unregisterPageHeightChangedEvent(this.pageHeightChangedEvent)},onSendError:function(A){if(this.sendNotification){this.sendNotification.cancel()}(new InfoNotification()).setMessage(_("Whoops! Something went wrong. Please refresh the page and try again!")).show();this.close()},onSendSuccess:function(A){},formAction:function(){},timelineEvent:function(){},pageHeightChangedEvent:function(){this.positionForm()},postData:function(){},beforePost:function(){},onComplete:function(){},submitForm:function(){this.$button.disable();var A={authenticity_token:twttr.form_authenticity_token};jQuery.extend(A,this.postData());if(this.progressNotificationText){this.sendNotification=(new ProgressNotification()).setProgressMessage(this.progressNotificationText).setCompletedMessage(_("Ok, done.")).show()}this.beforePost();$.ajax({type:"POST",dataType:"json",dataFilter:function(B){if(!jQuery.trim(B)){return null}return B},url:this.formAction(),data:A,error:function(B){this.onSendError(B)}.pBind(this),success:function(B){this.onSendSuccess(B);this.close();if(this.sendNotification){this.sendNotification.done()}}.pBind(this),beforeSend:twttr.loaddisableding,complete:function(){twttr.loaddisableded();this.onComplete()}.pBind(this)})},arrange:function(){var A=$('<div class="inline-form-inputs"></div>');if(this.options.showCancel){this.$buttonParent.append(this.$cancel)}this.$buttonParent.append(this.$button);this.$form_inner.append(A.append(this.$title).append(this.$body).append(this.$inputsPrompt).append(this.$input)).append(this.$buttonParent);this.$form.append(this.$form_inner);this.$form.hide();this.baseElement().append(this.$form)},baseElement:function(){return this.$parentNode||$(document.body)},show:function(A){this.addEvents();this.$targetNode=A.targetNode;this.$parentNode=A.parentNode;this.positionForm();this.arrange();this.$form.fadeIn(100);this.currentlyShown=true;this.afterShow()},afterShow:function(){},positionForm:function(){if(this.$targetNode&&this.$targetNode.width()>0){var C=this.position();var B=C[0];var A=C[1];this.$form.css("top",B).css("left",A)}else{this.close()}},close:function(){this.removeEvents();this.$form.remove();this.currentlyShown=false;this.afterClose()},afterClose:function(){},destroyFromEvent:function(B){var A=$(B.target);if(A.equals(this.$targetNode)||jQuery.inArray(this.$targetNode.get(0),A.parents())!=-1||A.equals(this.$form)||A.hasParent(this.$form)){return }this.close()},position:function(){var A=this.$targetNode.offset();return[A.top,A.left]}});RetweetInlineForm=function(){var A=_("Yes");var B=_("Retweet to your followers?");this.initialize({title:B,submitBtnValue:A})};RetweetInlineForm.prototype=new InlineForm();jQuery.extend(RetweetInlineForm.prototype,{overrideDefaultOptions:{formClass:"retweet-dlg",pageHeightChangedEvents:true},formAction:function(){var B=getListItemFromChild(this.$targetNode);var A=getStatusIdFromListItem(B);return"/statuses/"+A+"/retweet"},postData:function(){return{controller_name:page.controller_name,action_name:page.action_name}},beforePost:function(){this.close();twttr.setRetweetingStyles(this.$targetNode,_("Updating..."))},onSendSuccess:function(A){twttr.animateStatusReplacement(this.$targetNode,A);twttr.countAds(this.$targetNode)},onComplete:function(){twttr.unsetRetweetingStyles(this.$targetNode)},afterShow:function(){getListItemFromChild(this.$targetNode).addClass("perma-hover");this.$targetNode.find("a").blur()},afterClose:function(){getListItemFromChild(this.$targetNode).removeClass("perma-hover")},position:function(){var A=this.$targetNode.offset();return[parseInt(A.top)+20,parseInt(A.left)-220]}});$(document).ready(function(){try{var A="share-text-active";$(".status").each(function(){var E=$(this);var C=E.find(".retweet-link");var D=E.find(".share-text");C.hover(function(){D.addClass(A)},function(){D.removeClass(A)})})}catch(B){}});(function(){jQuery.inherits=function(A,C){function B(){}B.prototype=C.prototype;A.prototype=new B();A.prototype.constructor=A}})();(function(){jQuery.fn.equals=function(A){return this.get(0)==A.get(0)}})();(function(){jQuery.fn.hasParent=function(A){var B=false;this.parents().map(function(){if($(this).equals(A)){B=true}});return B}})();function Notification(B){this.$bar=jQuery('<div class="notification-bar"></div>');this.$barContainer=jQuery('<div class="notification-bar-container"></div>');this.$barContents=jQuery('<div class="notification-bar-contents"></div>');this.$barBackground=jQuery('<div class="notification-bar-bkg"></div>');this.$message=jQuery('<div class="message"></div>');this.$bar.hide();this.$barBackground.hide();var A=this;this.$bar.click(function(C){A.removeAfterEvent(C)});this.className=B}Notification.SLIDE_SPEED_IN_MS=300;Notification.prototype.remove=function(){var A=this;this.slideUp(function(){A.$bar.remove();A.$barBackground.remove();window.clearTimeout(A.timeout)})};Notification.prototype.removeAfterEvent=function(B){var A=$(B.target);if(A.get(0).nodeName.toLowerCase()=="a"&&A.hasParent(this.$message)){return }this.remove()};Notification.prototype.setMessage=function(A){this.msg=A;return this};Notification.prototype.show=function(){this.$message.addClass(this.className).html(this.msg);this.$barContainer.append(this.$barBackground).append(this.$bar.append(this.$barContents.append(this.$message)));jQuery("#notifications").append(this.$barContainer);this.$barBackground.height(this.$bar.height());this.showBar();if(this.onShow){this.onShow()}return this};Notification.prototype.removeInMilliseconds=function(){var A=this;this.timeout=window.setTimeout(function(){A.remove()},A.timeoutInMilliseconds)};Notification.prototype.showBar=function(){this.$bar.show();this.$barBackground.show()};Notification.prototype.onShow=function(){this.removeInMilliseconds()};Notification.prototype.slideUp=function(A){this.$bar.slideUp(Notification.SLIDE_SPEED_IN_MS);this.$barBackground.slideUp(Notification.SLIDE_SPEED_IN_MS,A)};function ShortNotification(){Notification.call(this,"message-info");this.timeoutInMilliseconds=3000}jQuery.inherits(ShortNotification,Notification);ShortNotification.prototype.showBar=function(){this.$bar.slideDown(Notification.SLIDE_SPEED_IN_MS);this.$barBackground.slideDown(Notification.SLIDE_SPEED_IN_MS)};function InfoNotification(){Notification.call(this,"message-info");this.timeoutInMilliseconds=6000}jQuery.inherits(InfoNotification,Notification);InfoNotification.prototype.showBar=function(){this.$bar.slideDown(Notification.SLIDE_SPEED_IN_MS);this.$barBackground.slideDown(Notification.SLIDE_SPEED_IN_MS)};function ProgressNotification(){Notification.call(this,"message-progress");this.timeoutInMilliseconds=1000}jQuery.inherits(ProgressNotification,Notification);ProgressNotification.prototype.setProgressMessage=function(A){return this.setMessage(A)};ProgressNotification.prototype.setCompletedMessage=function(A){this.completedMsg=A;return this};ProgressNotification.prototype.onShow=function(){};ProgressNotification.prototype.cancel=function(){this.timeoutInMilliseconds=0;this.removeInMilliseconds()};ProgressNotification.prototype.done=function(){this.$message.addClass("message-progress-done").removeClass(this.className).html(this.completedMsg);this.removeInMilliseconds()};function ErrorNotification(){Notification.call(this,"message-error");this.timeoutInMilliseconds=8000}jQuery.inherits(ErrorNotification,Notification);function Occasionally(A,D,C,B,E){this.interval=A;this.maxDecayTime=D;this.job=C;this.decayCallback=B;this.timesRun=0;this.decayRate=1;this.decayMultiplier=E||1.25;this.maxRequests=360}Occasionally.prototype.start=function(){this.stop();this.run()};Occasionally.prototype.stop=function(){if(this.worker){window.clearTimeout(this.worker)}};Occasionally.prototype.run=function(){var A=this;this.decayRate=this.decayCallback()?Math.max(1,this.decayRate/this.decayMultiplier):this.decayRate*this.decayMultiplier;var B=this.interval*this.decayRate;B=(B>=this.maxDecayTime)?this.maxDecayTime:B;this.worker=window.setTimeout(function(){A.execute()},Math.floor(B))};Occasionally.prototype.execute=function(){this.job();if(++this.timesRun<this.maxRequests){this.run()}};twttr.countClick=function(){var A=twttr.createTrackingParameters(this);twttr.asyncClickCount(A)};twttr.countAds=function(A){if(A.parents(".garuda-tweet").get(0)){var B=twttr.createAdLinkTrackingParameters(A);twttr.asyncAdsClickCount(B)}};twttr.countPromotedTrends=function(B,A){var C=twttr.createPromoteTrendTrackingParameters(B,A);twttr.asyncPromotedTrendEventLog(C)};twttr.asyncClickCount=function(A){(new Image()).src="/abacus?"+$.param(A)};twttr.asyncAdsClickCount=function(A){(new Image()).src="/abacus/garuda_click?"+$.param(A)};twttr.asyncPromotedTrendEventLog=function(A){(new Image()).src="/abacus/promoted_trend_event?"+$.param(A)};twttr.createAdHoverTrackingParameters=function(A,B){var C=twttr.createAdTrackingParameters(A);return $.extend({},C,{linkType:B})};twttr.createAdLinkTrackingParameters=function(B){var A=twttr.identifyLinkType(B,["retweet-link","reply","entry-meta","fav","non-fav"]);var C=twttr.createAdTrackingParameters(B);return $.extend({},C,{linkType:A})};twttr.identifyLinkType=function(A,E){var C=["web","profile-pic","screen-name","hashtag","username"];if(typeof (E)!="undefined"){C=C.concat(E)}for(var B=0;B<C.length;B++){var D=C[B];if(A.hasClass(D)){if(D=="fav"){return"non-fav"}else{if(D=="non-fav"){return"fav"}else{return D}}}}};twttr.countAdsReplies=function(A){var B=$("#content li.garuda-tweet");if(B.length>0){if(twttr.tweetIdForStatus(B)==A){twttr.countAds(B.find(".reply"))}}};twttr.tweetIdForStatus=function(A){return A.find(".meta a").attr("href").match(/\/(\d+)$/)[1]};twttr.createAdTrackingParameters=function(G){var N=G.closest(".status");var K=twttr.tweetIdForStatus(N);var M=$('meta[name="session-userid"]');var F=M.attr("content")||-1;var E=$('meta[name="client-ip"]');var D=E.attr("content")||-1;var C=JSON.parse(N.attr("data"));var B=C.advertiser_id;var J=C.campaign_id;var I=C.ad_id;var A=C.impression_id;var H=page.query;var L=twttr.form_authenticity_token||$('input[name="authenticity_token"]').attr("value");return{url:G.attr("href"),tweetId:K,userId:F,userIP:D,advertiserId:B,campaignId:J,adId:I,impressionId:A,query:H,authenticity_token:L}};twttr.createTrackingParameters=function(F){var B=$(F);var A=twttr.identifyLinkType(B);var E=B.closest(".status").find(".meta").children("a").get(0).href.split("/");var G=E[E.length-1];var H=$('meta[name="session-userid"]');var D=H.attr("content")||-1;var C=twttr.form_authenticity_token||$('input[name="authenticity_token"]').attr("value");return{url:F.href,linkType:A,tweetId:G,userId:D,authenticity_token:C}};twttr.createPromoteTrendTrackingParameters=function(C,D){var A=$(C);var B=twttr.form_authenticity_token;return{event_name:D,url:A.attr("href"),promoted_trend_id:JSON.parse(A.attr("data")).promoted_content.id,authenticity_token:B}};twttr.registerTracker=function(C,A,B){C.live(A,B)};twttr.setupTracking=function(){twttr.registerTracker($("#content li.status").find("a.tweet-url"),"mousedown",function(){if($(this).parents("li.garuda-tweet").length==0){twttr.countClick.pBind(this)()}});var A=$("#content li.garuda-tweet").find("a.tweet-url, .entry-meta, .fav-action.non-fav, .fav-action.fav, .meta");twttr.registerTracker(A,"mousedown",function(){twttr.countAds($(this))});var B=$("a.promoted-trend");twttr.registerTracker(B,"click",function(){twttr.countPromotedTrends($(this),"c")})};twttr.logPromotedTrendImpression=function(){var A=$("a.promoted-trend");if(A.length>0){twttr.countPromotedTrends(A,"i")}};$(document).ready(function(){twttr.setupTracking();twttr.logPromotedTrendImpression()});/*
+ http://www.JSON.org/json2.js
+ 2009-09-21
+
+ Public Domain.
+
+ NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
+
+ See http://www.JSON.org/js.html
+
+ This file creates a global JSON object containing two methods: stringify
+ and parse.
+
+ JSON.stringify(value, replacer, space)
+ value any JavaScript value, usually an object or array.
+
+ replacer an optional parameter that determines how object
+ values are stringified for objects. It can be a
+ function or an array of strings.
+
+ space an optional parameter that specifies the indentation
+ of nested structures. If it is omitted, the text will
+ be packed without extra whitespace. If it is a number,
+ it will specify the number of spaces to indent at each
+ level. If it is a string (such as '\t' or '&nbsp;'),
+ it contains the characters used to indent at each level.
+
+ This method produces a JSON text from a JavaScript value.
+
+ When an object value is found, if the object contains a toJSON
+ method, its toJSON method will be called and the result will be
+ stringified. A toJSON method does not serialize: it returns the
+ value represented by the name/value pair that should be serialized,
+ or undefined if nothing should be serialized. The toJSON method
+ will be passed the key associated with the value, and this will be
+ bound to the value
+
+ For example, this would serialize Dates as ISO strings.
+
+ Date.prototype.toJSON = function (key) {
+ function f(n) {
+ // Format integers to have at least two digits.
+ return n < 10 ? '0' + n : n;
+ }
+
+ return this.getUTCFullYear() + '-' +
+ f(this.getUTCMonth() + 1) + '-' +
+ f(this.getUTCDate()) + 'T' +
+ f(this.getUTCHours()) + ':' +
+ f(this.getUTCMinutes()) + ':' +
+ f(this.getUTCSeconds()) + 'Z';
+ };
+
+ You can provide an optional replacer method. It will be passed the
+ key and value of each member, with this bound to the containing
+ object. The value that is returned from your method will be
+ serialized. If your method returns undefined, then the member will
+ be excluded from the serialization.
+
+ If the replacer parameter is an array of strings, then it will be
+ used to select the members to be serialized. It filters the results
+ such that only members with keys listed in the replacer array are
+ stringified.
+
+ Values that do not have JSON representations, such as undefined or
+ functions, will not be serialized. Such values in objects will be
+ dropped; in arrays they will be replaced with null. You can use
+ a replacer function to replace those with JSON values.
+ JSON.stringify(undefined) returns undefined.
+
+ The optional space parameter produces a stringification of the
+ value that is filled with line breaks and indentation to make it
+ easier to read.
+
+ If the space parameter is a non-empty string, then that string will
+ be used for indentation. If the space parameter is a number, then
+ the indentation will be that many spaces.
+
+ Example:
+
+ text = JSON.stringify(['e', {pluribus: 'unum'}]);
+ // text is '["e",{"pluribus":"unum"}]'
+
+
+ text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
+ // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
+
+ text = JSON.stringify([new Date()], function (key, value) {
+ return this[key] instanceof Date ?
+ 'Date(' + this[key] + ')' : value;
+ });
+ // text is '["Date(---current time---)"]'
+
+
+ JSON.parse(text, reviver)
+ This method parses a JSON text to produce an object or array.
+ It can throw a SyntaxError exception.
+
+ The optional reviver parameter is a function that can filter and
+ transform the results. It receives each of the keys and values,
+ and its return value is used instead of the original value.
+ If it returns what it received, then the structure is not modified.
+ If it returns undefined then the member is deleted.
+
+ Example:
+
+ // Parse the text. Values that look like ISO date strings will
+ // be converted to Date objects.
+
+ myData = JSON.parse(text, function (key, value) {
+ var a;
+ if (typeof value === 'string') {
+ a =
+/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
+ if (a) {
+ return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+ +a[5], +a[6]));
+ }
+ }
+ return value;
+ });
+
+ myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
+ var d;
+ if (typeof value === 'string' &&
+ value.slice(0, 5) === 'Date(' &&
+ value.slice(-1) === ')') {
+ d = new Date(value.slice(5, -1));
+ if (d) {
+ return d;
+ }
+ }
+ return value;
+ });
+
+
+ This is a reference implementation. You are free to copy, modify, or
+ redistribute.
+
+ This code should be minified before deployment.
+ See http://javascript.crockford.com/jsmin.html
+
+ USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
+ NOT CONTROL.
+*/
+
+/*jslint evil: true */
+
+/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
+ call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
+ getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
+ lastIndex, length, parse, prototype, push, replace, slice, stringify,
+ test, toJSON, toString, valueOf
+*/
+
+
+// Create a JSON object only if one does not already exist. We create the
+// methods in a closure to avoid creating global variables.
+
+if (!this.JSON) {
+ this.JSON = {};
+}
+
+(function () {
+
+ function f(n) {
+ // Format integers to have at least two digits.
+ return n < 10 ? '0' + n : n;
+ }
+
+ if (typeof Date.prototype.toJSON !== 'function') {
+
+ Date.prototype.toJSON = function (key) {
+
+ return isFinite(this.valueOf()) ?
+ this.getUTCFullYear() + '-' +
+ f(this.getUTCMonth() + 1) + '-' +
+ f(this.getUTCDate()) + 'T' +
+ f(this.getUTCHours()) + ':' +
+ f(this.getUTCMinutes()) + ':' +
+ f(this.getUTCSeconds()) + 'Z' : null;
+ };
+
+ String.prototype.toJSON =
+ Number.prototype.toJSON =
+ Boolean.prototype.toJSON = function (key) {
+ return this.valueOf();
+ };
+ }
+
+ var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+ escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+ gap,
+ indent,
+ meta = { // table of character substitutions
+ '\b': '\\b',
+ '\t': '\\t',
+ '\n': '\\n',
+ '\f': '\\f',
+ '\r': '\\r',
+ '"' : '\\"',
+ '\\': '\\\\'
+ },
+ rep;
+
+
+ function quote(string) {
+
+// If the string contains no control characters, no quote characters, and no
+// backslash characters, then we can safely slap some quotes around it.
+// Otherwise we must also replace the offending characters with safe escape
+// sequences.
+
+ escapable.lastIndex = 0;
+ return escapable.test(string) ?
+ '"' + string.replace(escapable, function (a) {
+ var c = meta[a];
+ return typeof c === 'string' ? c :
+ '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+ }) + '"' :
+ '"' + string + '"';
+ }
+
+
+ function str(key, holder) {
+
+// Produce a string from holder[key].
+
+ var i, // The loop counter.
+ k, // The member key.
+ v, // The member value.
+ length,
+ mind = gap,
+ partial,
+ value = holder[key];
+
+// If the value has a toJSON method, call it to obtain a replacement value.
+
+ if (value && typeof value === 'object' &&
+ typeof value.toJSON === 'function') {
+ value = value.toJSON(key);
+ }
+
+// If we were called with a replacer function, then call the replacer to
+// obtain a replacement value.
+
+ if (typeof rep === 'function') {
+ value = rep.call(holder, key, value);
+ }
+
+// What happens next depends on the value's type.
+
+ switch (typeof value) {
+ case 'string':
+ return quote(value);
+
+ case 'number':
+
+// JSON numbers must be finite. Encode non-finite numbers as null.
+
+ return isFinite(value) ? String(value) : 'null';
+
+ case 'boolean':
+ case 'null':
+
+// If the value is a boolean or null, convert it to a string. Note:
+// typeof null does not produce 'null'. The case is included here in
+// the remote chance that this gets fixed someday.
+
+ return String(value);
+
+// If the type is 'object', we might be dealing with an object or an array or
+// null.
+
+ case 'object':
+
+// Due to a specification blunder in ECMAScript, typeof null is 'object',
+// so watch out for that case.
+
+ if (!value) {
+ return 'null';
+ }
+
+// Make an array to hold the partial results of stringifying this object value.
+
+ gap += indent;
+ partial = [];
+
+// Is the value an array?
+
+ if (Object.prototype.toString.apply(value) === '[object Array]') {
+
+// The value is an array. Stringify every element. Use null as a placeholder
+// for non-JSON values.
+
+ length = value.length;
+ for (i = 0; i < length; i += 1) {
+ partial[i] = str(i, value) || 'null';
+ }
+
+// Join all of the elements together, separated with commas, and wrap them in
+// brackets.
+
+ v = partial.length === 0 ? '[]' :
+ gap ? '[\n' + gap +
+ partial.join(',\n' + gap) + '\n' +
+ mind + ']' :
+ '[' + partial.join(',') + ']';
+ gap = mind;
+ return v;
+ }
+
+// If the replacer is an array, use it to select the members to be stringified.
+
+ if (rep && typeof rep === 'object') {
+ length = rep.length;
+ for (i = 0; i < length; i += 1) {
+ k = rep[i];
+ if (typeof k === 'string') {
+ v = str(k, value);
+ if (v) {
+ partial.push(quote(k) + (gap ? ': ' : ':') + v);
+ }
+ }
+ }
+ } else {
+
+// Otherwise, iterate through all of the keys in the object.
+
+ for (k in value) {
+ if (Object.hasOwnProperty.call(value, k)) {
+ v = str(k, value);
+ if (v) {
+ partial.push(quote(k) + (gap ? ': ' : ':') + v);
+ }
+ }
+ }
+ }
+
+// Join all of the member texts together, separated with commas,
+// and wrap them in braces.
+
+ v = partial.length === 0 ? '{}' :
+ gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
+ mind + '}' : '{' + partial.join(',') + '}';
+ gap = mind;
+ return v;
+ }
+ }
+
+// If the JSON object does not yet have a stringify method, give it one.
+
+ if (typeof JSON.stringify !== 'function') {
+ JSON.stringify = function (value, replacer, space) {
+
+// The stringify method takes a value and an optional replacer, and an optional
+// space parameter, and returns a JSON text. The replacer can be a function
+// that can replace values, or an array of strings that will select the keys.
+// A default replacer method can be provided. Use of the space parameter can
+// produce text that is more easily readable.
+
+ var i;
+ gap = '';
+ indent = '';
+
+// If the space parameter is a number, make an indent string containing that
+// many spaces.
+
+ if (typeof space === 'number') {
+ for (i = 0; i < space; i += 1) {
+ indent += ' ';
+ }
+
+// If the space parameter is a string, it will be used as the indent string.
+
+ } else if (typeof space === 'string') {
+ indent = space;
+ }
+
+// If there is a replacer, it must be a function or an array.
+// Otherwise, throw an error.
+
+ rep = replacer;
+ if (replacer && typeof replacer !== 'function' &&
+ (typeof replacer !== 'object' ||
+ typeof replacer.length !== 'number')) {
+ throw new Error('JSON.stringify');
+ }
+
+// Make a fake root object containing our value under the key of ''.
+// Return the result of stringifying the value.
+
+ return str('', {'': value});
+ };
+ }
+
+
+// If the JSON object does not yet have a parse method, give it one.
+
+ if (typeof JSON.parse !== 'function') {
+ JSON.parse = function (text, reviver) {
+
+// The parse method takes a text and an optional reviver function, and returns
+// a JavaScript value if the text is a valid JSON text.
+
+ var j;
+
+ function walk(holder, key) {
+
+// The walk method is used to recursively walk the resulting structure so
+// that modifications can be made.
+
+ var k, v, value = holder[key];
+ if (value && typeof value === 'object') {
+ for (k in value) {
+ if (Object.hasOwnProperty.call(value, k)) {
+ v = walk(value, k);
+ if (v !== undefined) {
+ value[k] = v;
+ } else {
+ delete value[k];
+ }
+ }
+ }
+ }
+ return reviver.call(holder, key, value);
+ }
+
+
+// Parsing happens in four stages. In the first stage, we replace certain
+// Unicode characters with escape sequences. JavaScript handles many characters
+// incorrectly, either silently deleting them, or treating them as line endings.
+
+ cx.lastIndex = 0;
+ if (cx.test(text)) {
+ text = text.replace(cx, function (a) {
+ return '\\u' +
+ ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+ });
+ }
+
+// In the second stage, we run the text against regular expressions that look
+// for non-JSON patterns. We are especially concerned with '()' and 'new'
+// because they can cause invocation, and '=' because it can cause mutation.
+// But just to be safe, we want to reject all unexpected forms.
+
+// We split the second stage into 4 regexp operations in order to work around
+// crippling inefficiencies in IE's and Safari's regexp engines. First we
+// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
+// replace all simple value tokens with ']' characters. Third, we delete all
+//void brackets that follow a colon or comma or that begin the text. Finally,
+// we look to see that the remaining characters are only whitespace or ']' or
+// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
+
+ if (/^[\],:{}\s]*$/.
+test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
+replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
+replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
+
+// In the third stage we use the eval function to compile the text into a
+// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
+// in JavaScript: it can begin a block or an object literal. We wrap the text
+// in parens to eliminate the ambiguity.
+
+ j = eval('(' + text + ')');
+
+// In the optional fourth stage, we recursively walk the new structure, passing
+// each name/value pair to a reviver function for possible transformation.
+
+ return typeof reviver === 'function' ?
+ walk({'': j}, '') : j;
+ }
+
+// If the text is not JSON parseable, then a SyntaxError is thrown.
+
+ throw new SyntaxError('JSON.parse');
+ };
+ }
+}());
+var scrobject={scribeHost:window.location.hostname,toScribeParams:function(A){var B=[];for(var C in A){B[B.length]=encodeURIComponent(C)+"="+encodeURIComponent(A[C])}return B.join("&")},scribeUrl:function(C,B){var A="/scribe?";if(B.host){A=window.location.protocol+"//"+B.host+A}else{if(!/[\/\.]twitter\.com/.test(scrobject.scribeHost)&&!/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/.test(scrobject.scribeHost)&&scrobject.scribeHost!="localhost"){A=window.location.protocol+"//twitter.com"+A}}return A+scrobject.toScribeParams(C)}};function scribe(A,D,C,B){C=C||{};if(window.DARKMODE_SCRIBE){return this}if(!D){console.warn("You must specify a category in order to use scribe");return this}if(typeof (A)=="function"){A=A.call(this,B)}if(A==null){console.warn("You must provide logged data in order to use scribe");return this}if(!A.event_name){console.warn('You must include an "event_name" field in your logged data in order to use scribe');return this}var E={log:JSON.stringify(A),ts:(new Date()).getTime()};if(C.filter){E.filter=C.filter}if(D){E.category=D}(new Image()).src=scrobject.scribeUrl(E,C);return this}function scribeAB(A){scribe(A,"www_ab_testing")}function watchABEvent(C,B,D,A){if(C){var E=$(C);if(!D&&E.attr("abdata")){D=JSON.parse(E.attr("abdata"))}args={experimentData:D,handler:A};$(C).bind(B,args,function(F){scribeAB(F.data.experimentData);continuePropogation=true;if(F.data.handler){continuePropogation=F.data.handler();if(!continuePropogation){F.stopPropagation()}}return(continuePropogation)})}}function watchReloaddisabledingABLink(B){var A=$(B);if(A&&A.attr("abdata")){var C=JSON.parse(A.attr("abdata"));watchABEvent(A,"click",C,function(){setTimeout('document.location = "'+A.attr("href")+'"',100);return(false)})}}if(!window.console){window.console={warn:function(A){}}}if(window.jQuery){(function(A){A.extend(A.fn,{scribe:function(B,D,C){C=C||{};A(this).bind(C.clientEvent||"mousedown",function(E){window.scribe.call(this,B,D,C,E)});return this}})})(jQuery);(function(B){var A=B("a.ab-reloaddisableding");if(typeof A.livequery=="function"){A.livequery(function(){watchReloaddisabledingABLink(B(this))})}})(jQuery)};twttr.position={adjacent:function(I,H,A){var F,G;A=(A||{});F=G=H.offset();G.gravity=A.gravity;G.weight=A.weight;var D={height:H.outerHeight(),width:H.outerWidth()};var B={height:I.outerHeight(),width:I.outerWidth()};var C={height:$(window).height(),width:$(window).width()};var E={height:$("body").height(),width:$("body").width()};if(!G.gravity){G.gravity="vertical"}if("vertical,north,south".indexOf(G.gravity)!=-1){if("right,left,center".indexOf(G.weight)==-1){G.weight=(F.left>C.width/2)?"right":"left"}if(G.gravity=="vertical"){G.gravity=((F.top+B.height)>($(window).scrollTop()+C.height))?"south":"north"}if(A.position=="relative"){F={left:0,top:0};G.left=0}if(G.weight=="right"){G.left=F.left-B.width+D.width}else{if(G.weight=="center"){G.left=F.left-((B.width-D.width)/2)}}G.top=(G.gravity=="north")?(F.top+D.height):(F.top-B.height)}if("horizontal,east,west".indexOf(G.gravity)!=-1){if("top,bottom,center".indexOf(G.weight)==-1){if((F.top-(B.height/2))<0){G.weight="top"}else{if((F.top+(B.height/2))>Math.max(C.height,E.height)){G.weight="bottom"}else{G.weight="center"}}}if(G.gravity=="horizontal"){G.gravity=((F.left+(D.width/2))>C.width/2)?"east":"west"}if(A.position=="relative"){F={left:0,top:0};G.top=0}if(G.weight=="center"){G.top=F.top+(D.height/2)-(B.height/2)}else{if(G.weight=="bottom"){G.top=F.top-B.height+D.height}}G.left=(G.gravity=="west")?(F.left+D.width):(F.left-B.width)}return G},center:function(A){var C=$(window);var B={top:parseInt((C.height()-A.outerHeight())/2),left:parseInt((C.width()-A.outerWidth())/2)};if($("body.ie6").length){B.top+=C.scrollTop();B.left+=C.scrollLeft()}return B}};twttr.klass("twttr.dialog",function(A){this.opts=A;this.$heading=A.heading?$(A.heading):false;this.$footer=A.footer?$(A.footer):false;this.$content=$(A.content);this.createShell();this.bindEvents()}).widget().method("getHeaderHTML",function(){if(this.$heading&&this.$heading.length){return"<h2><span>"+this.$heading.html()+"</span>"+(this.opts.closeButton?'<a href="#" class="modal-close">&times;</a>':"")+"</h2>"}else{return(this.opts.closeButton?'<span class="no-heading"><a href="#" class="modal-close right">&times;</a></span>':"")}}).method("getShellHTML",function(){var A=(this.opts.modal===false);return['<div class="twttr-dialog'+(this.opts.cssClass?" "+this.opts.cssClass:"")+'" style="display: none;">','<div class="hanging"'+(this.opts.zIndex?' style="z-index: '+this.opts.zIndex+' !important;"':"")+">",'<div class="modal">','<div class="modal-inner">',this.getHeaderHTML(),'<div class="modal-content"> </div>',"</div>","</div>","</div>",(A?"":'<div class="modal-overlay"></div>'),"</div>"].join("")}).method("createShell",function(){var C=this;this.$root=$(this.getShellHTML());if(this.opts.width){this.find(".hanging").css({width:this.opts.width})}var D=this.$content.parent().length?this.$content.parent():$(document.body);this.$content.move(this.find(".modal-content"));if(this.$footer.length){this.find(".modal-content").after('<div class="footer"></div>');this.$footer.move(this.find(".footer"))}if(this.opts.renderInline){D.append(this.$root)}else{$(document.body).append(this.$root)}if(this.opts.fixed===false){this.find(".hanging").css({position:"absolute"})}var B=this.$root.find(".modal-submit");if(B.length>0){if(C.opts.noajax){B.bind("click",function(E){B.closest("form").submit()});return }var A=C.opts.ajax.complete;B.bind("click",function(F){F.preventDefault();B.attr("disabled",true);B.addClass("dbtn").removeClass("btn");var E=$(this).closest("form");$.ajax($.extend((C.opts.ajax||{}),{type:E.attr("method"),url:E.attr("action"),data:E.serialize(),complete:function(G){B.attr("disabled",false);B.addClass("btn").removeClass("dbtn");if(A){A(G)}}}))})}}).method("bindEvents",function(){var A=this;this.find(".modal-close").click(function(B){B.preventDefault();A.close()});if(this.find(".modal-close").length){$(document).keydown(function(B){if(B.which==27){B.preventDefault();A.close()}})}if(this.opts.popup){$(document).click(function(B){if(voided&&!$(B.target).parents(".modal").length){A.close()}})}}).method("windowHeight",function(){return $(window).height()}).method("scrollTop",function(){return $(window).scrollTop()}).method(void",function(){this.$root.fadeIn("fast");var A=this.find(".hanging");var B=this.center(A);if(this.opts.top){B.top=this.opts.top}if(this.opts.left){B.left=this.opts.left}if(this.opts.maxTop){B.top=Math.min(B.top,this.opts.maxTop)}if(this.opts.maxLeft){B.left=Math.min(B.left,this.opts.maxLeft)}A.css({top:B.top,left:B.left});this.$root.trigger(void");voided=true;if(this.windowHeight()<A.outerHeight()){A.css("position","absolute");A.css("top",this.scrollTop()+"px")}else{if(this.opts.fixed===false){A.css("top",B.top+this.scrollTop())}}this.$root.find("input[type=text]:first").focus()}).method("close",function(){this.$root.fadeOut("fast");voided=false;this.$root.trigger("close")}).method("toggle",function(){voided?this.close():void()});twttr.augmentObject(twttr.dialog.prototype,twttr.position);twttr.auxo("AttachedDialog",twttr.dialog).method(void",function(){this.$root.addClass("attached");this.$root.fadeIn("fast");voided=true;if(!this.positioned){var A=this.find(".hanging");var D=this.adjacent(this.find(".hanging"),$(this.opts.handle),this.opts);if(this.opts.offsetX){D.left+=this.opts.offsetX}if(this.opts.offsetY){D.top+=this.opts.offsetY}twttr.augmentObject(this.opts,D);var B=this.opts.gravity;if(B&&("horizontal,vertical".indexOf(B)==-1)){if("north,south".indexOf(B)==-1){var C=parseInt(this.find(".hanging").height());this.find(".modal-inner").prepend('<div class="'+B+'" style="height:'+C+'px;"></div>');D.left+=this.nudge(B);D.top+=this.nudge(this.opts.weight)}else{this.find(".modal")[(B=="north"?"before":"after")]('<div class="'+B+'"></div>');D.top+=this.nudge(B);D.left+=this.nudge(this.opts.weight)}}this.find(".hanging").css({top:D.top,left:D.left});if(this.opts.weight&&this.opts.weight!="auto"){this.find(".hanging").addClass("weight-"+this.opts.weight)}if(this.opts.modal){this.find(".modal-overlay").height(Math.max($(window).height(),$("body").height())+25)}this.positioned=true}}).method("nudge",function(A){return(twttr.AttachedDialog.offsets[A]||0)});twttr.AttachedDialog.offsets={top:-15,bottom:30,east:-10,west:10,south:-10,north:4};$.extend($.fn,{hoverTip:function(A,E){E=(E||{});var F=false;var B=$(this);var G=$(A);var D=document.all&&($.browser.version<8);var C=false;if(D){$("body").append(G);G.hover(function(H){clearTimeout(C)},function(H){G.fadeOut("fast")})}else{E.position="relative";B.prepend(G)}B.hover(function(H){F=setTimeout(function(){clearTimeout(F);pos=twttr.position.adjacent(G,B,E);var I=pos.top;G.css({left:pos.left,top:I}).fadeIn("fast")},400)},function(I){var H=$(I.target);clearTimeout(F);if(!H.is(B)){C=setTimeout(function(){G.fadeOut("fast")},(D?200:0))}})}});(function(A){A.fn.extend({isSigninMenu:function(){return this.each(function(){var D=A(this),B=A(".signin"),C=D.find(".textbox input"),E=true;B.bind("click focus",function(G){G.preventDefault();if(!E){return }E=false;setTimeout(function(){E=true},500);var F=A(this);F.toggleClass("void");D.toggleClass("offscreen");if(F.hasClass("void")){A(document).trigger("signinMenu.show");setTimeout(function(){A("#username").focus()},50)}else{A(document).trigger("signinMenu.hide");C.val("");setTimeout(function(){A("#home_search_q, #searchform_q").focus()},0)}});C.bind("focus keydown",function(F){if((F.type=="keydown"&&F.keyCode==27)||(F.type=="focus"&&!B.hasClass("void"))){B.trigger("click")}});D.mouseup(function(){return false});A(document).mouseup(function(F){if(A(F.target).parent("a.signin").length==0&&B.hasClass("void")){B.trigger("click")}})})}})})(jQuery);
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/phoenix/img/sprite-icons.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/phoenix/img/sprite-icons.png
new file mode 100755
index 0000000000..a93cede946
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/phoenix/img/sprite-icons.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/stylesheets/geo.css@1302114648.css b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/stylesheets/geo.css@1302114648.css
new file mode 100755
index 0000000000..803efe531b
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/stylesheets/geo.css@1302114648.css
@@ -0,0 +1 @@
+.geo_new{color:#C00;}.geo_progress{color:#999;}.crosshairs{display:inline-block;background:url(../images/sprite-icons.png) -64px -80px no-repeat;height:11px;width:11px;margin:0 4px 0 0;vertical-align:middle;}a.geo_disable_webclient span{display:inline-block;background:url(../images/sprite-icons.png) no-repeat -112px -80px;height:7px;width:7px;margin:0 3px;vertical-align:middle;}a:hover.geo_disable_webclient span{background-position:-128px -80px;}.near{color:#8c8c8c;font-size:14px;}a.places-nearby{position:absolute;left:385px;top:148px;}.geo_notifications{display:none;}#place_link:focus{outline:none;}#place_link span.place_icon{display:inline-block;background:url(../images/sprite-icons.png) no-repeat -240px -64px;height:11px;width:7px;vertical-align:middle;margin-right:4px;}#geo_browser_help_banner{color:#FFF;font:12px Verdana;position:fixed;right:0;text-align:left;top:0;z-index:10000;}#geo_browser_help_banner.geo_firefox{background:#333 url(../images/geo_firefox_help_banner_back.png) no-repeat right;-moz-border-radius-bottomleft:4px;height:108px;}#geo_browser_help_banner.geo_chrome{background:#333 url(../images/geo_chrome_help_banner_back.png) no-repeat right;-webkit-border-radius-bottomleft:4px;height:65px;}#geo_browser_help_banner.geo_ie_gtb{background:#333 url(../images/geo_ie_gtb_help_banner_back.png) no-repeat right;height:108px;}#geo_browser_help_banner.geo_firefox>div{margin:8px 183px -3px 10px;}#geo_browser_help_banner.geo_chrome>div{margin:25px 120px 20px 20px;}#geo_browser_help_banner.geo_ie_gtb>div{margin:8px 200px -3px 10px;}#geo_browser_help_banner img{margin-right:6px;position:relative;top:8px;}ul.places_list{background-color:#FFF;border:1px solid #AAA;padding:4px 0 4px 0;text-align:left;}#place_content ul.places_list li,ul.places_list li{color:#333;padding:3px 8px 3px 4px;cursor:pointer;}.geo_more_places{border-top:1px solid #ccc;padding-top:5px;margin-top:4px;}#place_content ul.places_list li:hover,#place_content ul.places_list a:hover{color:white;background-color:#666;outline:none;}li .place_item_icon{background:transparent;display:inline-block;height:9px;margin:0 4px 2px 0;vertical-align:middle;width:10px;}li.selected .place_item_icon{background:url(../images/sprite-icons.png) no-repeat -160px -16px;}li .refresh{background:url(../images/sprite-icons.png) no-repeat -96px -80px;width:7px;margin:0 5px 2px 2px;}ul.places_list li:hover .refresh{background:url(../images/sprite-icons.png) no-repeat -80px -80px;}li .clear{display:inline-block;background:url(../images/sprite-icons.png) no-repeat -128px -80px;height:7px;width:7px;margin:0 5px 0 2px;vertical-align:middle;}ul.places_list li:hover .clear{background:url(../images/sprite-icons.png) no-repeat -112px -80px;}li .place_icon{display:inline-block;background:url(../images/sprite-icons.png) no-repeat -224px -64px;height:11px;width:7px;margin-right:4px;vertical-align:middle;}li .more_places{background:transparent;}li .place_details{color:#999;}#geo-promo-hoverer{width:420px;font-size:11px;text-align:left;visibility:hidden;}#geo-promo-hoverer .hoverer-inner{padding:15px;}#geo-promo-hoverer .hovercard-divot{left:40px;top:-11px;}#geo_modal.position_above .hovercard-divot{bottom:-11px;}#geo_modal.position_below .hovercard-divot{top:-11px;}#geo-promo-hoverer .tiny-map{float:right;padding:0 0 0 20px;}#geo_dialog_descr{margin:10px 0 10px 0;font-size:13px;}#geo_not_now{position:relative;top:5px;margin-left:8px;}#geo_turn_location_on{font-weight:bold;}a.geo_disable_webclient{color:#999;font-family:tahoma,sans-serif;font-size:12px;font-weight:bold;line-height:12px;text-shadow:1px 1px 1px #FFF;}a:hover.geo_disable_webclient{text-decoration:none;}.geo-pin{background:transparent url(../images/sprite-icons.png) no-repeat scroll -224px -64px;display:inline-block;height:11px;line-height:1.1em;width:7px;}.geo_map_with_place{width:490px;}#map_canvas{width:270px;height:170px;float:left;margin:1px;}.map_close{color:#999;text-decoration:none;-moz-border-radius:2px;background-color:#ddd;display:block;font-size:15px;margin:-2px;padding:0 4px 2px;position:absolute;right:0;top:0;text-decoration:none;}.map_close:hover{text-decoration:none;}.geo_map_place_details{width:195px;margin:10px;float:left;color:#333;}.geo_map_place_name{font-weight:bold;font-size:13px;margin-bottom:4px;}.geo_map_place_tweets{margin-top:5px;}.geo_map_place_tweets a{color:#2276bb;}#geo_map_progress.position_above .hovercard-divot{bottom:-11px;}#geo_map_progress.position_below .hovercard-divot{top:-11px;}#geo_map_progress .hoverer-inner{width:55px;}#geo_map_fail{display:none;}#geo_map_spinner{background:url(../images/spinner.gif) no-repeat;margin:10px 20px;}.place_search_dialog .hanging{width:450px;}.geo_place_search_table{font-family:'Lucida Grande',sans-serif;font-size:13px;}.geo_place_search_col1{font-weight:bold;text-align:right;padding-right:7px;padding-left:0;}.geo_place_search_city{padding-bottom:14px;padding-left:7px;}#geo_city{margin:0 0 16px 8px;}#geo_poi_hint{font-family:'Lucida Grande',sans-serif;font-size:11px;color:#999;padding:4px 0 8px 7px;}#place_search_results{padding:5px 0 0 7px;display:none;width:310px;}#place_search_done,#place_search_cancel{margin-top:20px;margin-bottom:5px;}#place_search_form input{border:1px solid #aaa!important;font-size:1em;outline:none;padding:5px;width:282px;vertical-align:middle;}#place_search_form #city_search_query{width:336px;}#place_search_form input:focus{outline:none;border-color:rgba(82,168,236,.75)!important;box-shadow:0 0 8px rgba(82,168,236,.5);-moz-box-shadow:0 0 8px rgba(82,168,236,.5);-webkit-box-shadow:0 0 8px rgba(82,168,236,.5);}.place_search_submit{-moz-border-radius-bottomright:3px;-moz-border-radius-topright:3px;border-style:solid;border-width:1px;margin-left:-1px;cursor:pointer;padding:.4em .9em;border-color:#999!important;padding-bottom:5px!important;padding-top:5px!important;vertical-align:middle;background:url(../images/nav_search_submit.png) repeat scroll -2px 0 transparent!important;}.place_search_submit:hover{background:url(../images/nav_search_submit.png) -2px -25px!important;}.place_search_submit:active{background:url(../images/nav_search_submit.png) -2px -50px!important;}.place_search_submit.loaddisableding,.place_search_submit.loaddisableding:hover,.place_search_submit.loaddisableding:active{background:#eee url(../images/spinner.gif) no-repeat 5px 5px!important;}#place_search_results li{margin:10px 0 0 0;list-style-type:none;white-space:nowrap;overflow:hidden;}#place_search_results .place_noicon,ul.place_search_dropdown.places_list .place_noicon{display:inline-block;margin-left:15px;}.wait{cursor:wait;}ul.place_search_dropdown.places_list li{padding-left:8px;white-space:nowrap;}.places_list li.hover{color:white;background-color:#666;outline:none;}ul.places_list{display:none;position:absolute;background-color:#FFF;border:1px solid #AAA;padding:4px 0 4px 0;text-align:left;z-index:9999;}#place_search_go_back{margin-top:12px;}#place_search_go_back,#change_city{font-weight:normal;color:#4d94be;}.geo_place_search_hint{padding:4px 0 0 7px;font-size:11px;color:#999;}div.geo_add_place{margin-top:20px;}div.geo_add_place a{font-weight:bold;}.geo_search_message{margin-top:12px;}.geo_next_prev{margin-top:12px;}#geo_prev_result{margin-right:20px;}.place_creation_dialog .hanging{width:650px;}.place_creation_dialog .modal-inner h2{margin:0!important;}.place_creation_dialog .modal-content{padding:0;}.place_creation_dialog .geo_map_canvas{width:312px;}.geo_place_search_table{font-family:'Lucida Grande',sans-serif;font-size:13px;width:100%;}.geo_place_creation_hint{padding:8px 0 0 7px;font-size:11px;color:#999;}.geo_form_input{border:1px solid #aaa!important;font-size:1em;outline:none;padding:5px;width:210px;vertical-align:middle;}.geo_form_input:focus{outline:none;border-color:rgba(82,168,236,.75)!important;box-shadow:0 0 8px rgba(82,168,236,.5);-moz-box-shadow:0 0 8px rgba(82,168,236,.5);-webkit-box-shadow:0 0 8px rgba(82,168,236,.5);}.geo_place_creation_row2{padding-top:15px;}.geo_place_city{margin:12px 0 15px;}#geo_creation_error{margin-top:8px;font-size:11px;}.geo_spinner{display:inline-block;background:url(../images/spinner.gif);height:14px;width:14px;margin-left:15px;line-height:1.9em;vertical-align:middle;}.geo_map{float:right;}.geo_place_create{padding:25px;width:280px;}.geo_place_create ul{margin:18px 0 20px 0;}.geo_place_create li{margin:10px 0;white-space:nowrap;overflow:hidden;}.geo_map_hint{opacity:0;width:160px;position:absolute;z-index:20;text-align:center;}.geo_map_hint span{display:inline-block;vertical-align:bottom;background-image:url(../images/geo_creation_hint_arrow.gif);background-repeat:no-repeat;width:21px;height:11px;}.geo_map_hint div{background-color:#424242;color:white;text-align:left;padding:10px;font-size:11px;font-weight:bold;}.geo_map_place_bubble{opacity:0;display:none;position:absolute;z-index:20;text-align:center;margin-top:10px;white-space:nowrap;}.geo_map_place_bubble span{display:inline-block;vertical-align:bottom;background-image:url(../images/geo_creation_hint_arrow.gif);background-repeat:no-repeat;width:21px;height:11px;}.geo_map_place_bubble>div{background-color:#424242;color:white;text-align:left;padding:10px;font-size:11px;font-weight:bold;}.geo_go_back{line-height:1.9em;margin:0 10px;}.geo_place_details{color:#aaa;}.geo_map_link_separator{margin:0 5px 0 10px;color:#aaa;} \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/stylesheets/twitter.css@1302114648.css b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/stylesheets/twitter.css@1302114648.css
new file mode 100755
index 0000000000..52eb695df2
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/a/1302214109/stylesheets/twitter.css@1302114648.css
@@ -0,0 +1 @@
+.transparent{opacity:.0;}.ie .transparent{filter:alpha(opacity=0);}.error{color:#801b1b;}.notice{color:#801b1b;}.top{vertical-align:top!important;}.bottom{vertical-align:bottom!important;}.middle{vertical-align:middle!important;}.first{margin-top:0;padding-top:0;}.last{margin-bottom:0;padding-bottom:0;}.right{float:right;}.left{float:left;}.clearfix{zoom:1;}.clearfix:after{content:".";display:block;height:0;clear:both;visibility:hidden;}.help-text{color:#aaa;}.disabled{color:#bbb;}.link-disabled{color:#bbb;}.link-disabled:hover{text-decoration:none;cursor:default;}.inline{display:inline;}.clear{clear:both;}.loaddisableding{background-position:50% 50%;background-repeat:no-repeat;}.hidden{display:none;}.invisible{visibility:hidden;}.offscreen{position:absolute;left:-9999px;overflow:hidden;}.empty-set{padding:30px!important;}.numeric{font-family:'Helvetica Neue','Helvetica','Arial',sans-serif;}.no-border{border:0!important;}.round{-moz-border-radius:5px;-webkit-border-radius:5px;}.round-top-right{-moz-border-radius-topright:5px;-webkit-border-top-right-radius:5px;}.round-right{-moz-border-radius-topright:5px;-moz-border-radius-bottomright:5px;-webkit-border-top-right-radius:5px;-webkit-border-bottom-right-radius:5px;}.round-bottom-right{-moz-border-radius-bottomright:5px;-webkit-border-bottom-right-radius:5px;}.round-bottom{-moz-border-radius-topright:0;-moz-border-radius-topleft:0;-moz-border-radius-bottomright:5px;-moz-border-radius-bottomleft:5px;-webkit-border-top-right-radius:0;-webkit-border-top-left-radius:0;-webkit-border-bottom-right-radius:5px;-webkit-border-bottom-left-radius:5px;}.round-bottom-left{-moz-border-radius-bottomleft:5px;-webkit-border-bottom-left-radius:5px;}.round-left{-moz-border-radius-topleft:5px;-moz-border-radius-bottomleft:5px;-webkit-border-top-left-radius:5px;-webkit-border-bottom-left-radius:5px;}.round-top-left{-moz-border-radius-topleft:5px;-webkit-border-top-left-radius:5px;}.round-top{-moz-border-radius-topright:5px;-moz-border-radius-topleft:5px;-moz-border-radius-bottomright:0;-moz-border-radius-bottomleft:0;-webkit-border-top-right-radius:5px;-webkit-border-top-left-radius:5px;-webkit-border-bottom-right-radius:0;-webkit-border-bottom-left-radius:0;}#footer{text-align:center;padding:8px 0;margin-top:.7em;line-height:1;background:#fff;white-space:nowrap;}#footer li{display:inline;padding:0 4px;}#footer li.first:before{content:'';padding-right:0;}#footer.wide{width:100%;}body.ko #footer{font-size:11px;}#country_return_prompt{width:150px;padding:8px;margin-top:.7em;line-height:1;background:#fff;white-space:nowrap;}.tipsy{opacity:.8;filter:alpha(opacity=80);background-repeat:no-repeat;padding:5px;}.tipsy-inner{padding:8px 8px;max-width:200px;font:11px 'Lucida Grande',sans-serif;font-weight:bold;-moz-border-radius:4px;-khtml-border-radius:4px;-webkit-border-radius:4px;border-radius:4px;background-color:#000;color:white;text-align:left;}.tipsy-north{background-image:url(../images/tipsy/tipsy-north.gif);background-position:top center;}.tipsy-south{background-image:url(../images/tipsy/tipsy-south.gif);background-position:bottom center;}.tipsy-east{background-image:url(../images/tipsy/tipsy-east.gif);background-position:right center;}.tipsy-west{background-image:url(../images/tipsy/tipsy-west.gif);background-position:left center;}*{margin:0;padding:0;}fieldset,img{border-width:0;border-color:transparent;}a{text-decoration:none;color:#2276BB;}a:hover{text-decoration:underline;}ul{list-style:none;}ul.dot li:before{content:"\00B7 \0020";}hr{display:none;}div.hr{height:1px;background:#eee;width:100%;overflow:hidden;margin:.5em 0;line-height:1;font-size:16px;}#delete #content .reallyimportant{-moz-border-radius:5px;-webkit-border-radius:5px;background:#ffffe3;font-size:120%;padding:1em;}#remember_delete_message{-webkit-box-shadow:0 1px 2px rgba(0,0,0,1);-moz-box-shadow:0 1px 2px rgba(0,0,0,1);margin:5px 0 15px 15px;padding:10px;width:80%;}input[type=text],input[type=password],select,textarea{border:1px solid #aaa;transition:border linear .2s,box-shadow linear .2s;-moz-transition:border linear .2s,-moz-box-shadow linear .2s;-webkit-transition:border linear .2s,-webkit-box-shadow linear .2s;}input[type=text]:focus,input[type=password]:focus,textarea:focus{outline:none;border-color:rgba(82,168,236,.75)!important;box-shadow:0 0 8px rgba(82,168,236,.5);-moz-box-shadow:0 0 8px rgba(82,168,236,.5);-webkit-box-shadow:0 0 8px rgba(82,168,236,.5);}input.with-box:focus,input[class*=search]:focus,input[id*=search]:focus{border-color:inherit!important;box-shadow:none;-moz-box-shadow:none;-webkit-box-shadow:none;}body.email-address-nag .email-address-nag-banner{display:block;}.no-display{display:none;}.email-address-nag .content-bubble-arrow{display:none;}.email-address-nag-banner,.employee-nag-banner{margin-top:23px;display:none;background-color:#ffd;color:#333;padding:13px;-moz-border-radius-topleft:5px;-moz-border-radius-topright:5px;-webkit-border-top-left-radius:5px;-webkit-border-top-right-radius:5px;border-bottom:1px solid #c0deed;}.employee-nag-banner{display:block;}body.email-address-nag .employee-nag-banner{margin-top:0;-moz-border-radius-topleft:0;-moz-border-radius-topright:0;-webkit-border-top-left-radius:0;-webkit-border-top-right-radius:0;}.email-address-nag-banner .resending-email{margin-left:10px;}.email-address-nag .until-you-confirm-message,.email-notice .resend-message,.employee-nag-banner .employee-nag-message{line-height:20px;}body.email-address-nag #content,body.email-address-nag #side_base{-moz-border-radius-topleft:0;-moz-border-radius-topright:0;-webkit-border-top-left-radius:0;-webkit-border-top-right-radius:0;}body{text-align:center;font:.75em 'Lucida Grande',sans-serif;color:#333;}#container{width:763px;margin:1em auto;text-align:left;position:relative;z-index:1;}#content h1,#content h2,#content h3,#content h4,#content h5{margin:3px 0 4px;}.columns{margin-bottom:15px;width:100%;}td.column{padding:0;vertical-align:top;}.center-text{text-align:center;}#loaddisableder{position:absolute;top:.7em;right:-25px;padding:0;background-color:#FFF;border:1px solid #CCC;font-size:10px;line-height:0;z-index:999;}.ie7 #loaddisableder{top:22px;}#header{text-align:right;}#header.no-nav{text-align:left;}#logo{float:left;}.no-nav #logo img{position:relative;margin-bottom:-0.5em;}#logo img{margin-top:-2px;}#front #logo img{position:relative;z-index:99;}body.ie7 #logo img{margin:.6em 0 0 0!important;}body.ie6 #logo{filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled='true',src='/images/twitter_logo_header.png',sizingMethod='crop');display:block;width:155px;height:36px;cursor:pointer;}body.ie6 #logo img{visibility:hidden;position:static;}.top-navigation{background-color:#fff;white-space:nowrap;display:inline-block;padding:0 .7em;}.ie7 .top-navigation{margin-top:1em;display:inline;}.ie7 .top-navigation>li{vertical-align:middle;}.ie7 .top-navigation>.nav-search-container{padding:2px 0 2px 4px!important;zoom:1;}.top-navigation *{display:inline;}.top-navigation>li{position:relative;}.top-navigation>li>a{padding:.5em .15em;border:1px solid transparent;font-size:1.05em;display:inline-block;}.firefox2 .top-navigation{max-width:45em;margin-left:auto;padding-bottom:1px;}.top-navigation>li>a:focus{outline:none;}.ie6 .top-navigation{display:inline;padding:1em;}.admin-link{font-size:10px;position:absolute;right:-25px;top:.9em;}body#show .admin-link{top:5px;}#content{background-color:#FFF;width:564px;margin-top:0;word-wrap:break-word;-moz-border-radius-topleft:5px;-webkit-border-top-left-radius:5px;-moz-border-radius-bottomleft:5px;-webkit-border-bottom-left-radius:5px;}#content.wide{width:100%;-moz-border-radius:5px;-webkit-border-radius:5px;}.content-bubble-arrow{zoom:1;margin-top:6px;height:11px;background-repeat:no-repeat;background-position:22px 0;background-image:url(../images/arr2.gif);overflow:hidden;}#content.minheight{height:200px;}td.column{padding:0;vertical-align:top;}.wrapper{padding:5px 10px 15px;}#content div.section{position:relative;padding:6px 10px;}#content div.section>div{margin:1em 0;}#content div.section p{margin-bottom:1em;}#content div.section,div.section>div{clear:both;float:none;}#content div.section ul li{margin:0;padding:0 0 1em 0;}#content div.steps,#content div.section div.steps{margin-top:3em;}.subpage #side{margin-top:0;}#side_base{width:199px;line-height:1.2;background-color:#DDEEF6;border-left:1px solid #C0DEED;-moz-border-radius-topright:5px;-moz-border-radius-bottomright:5px;-webkit-border-bottom-right-radius:5px;-webkit-border-top-right-radius:5px;}#side_ad_base{height:185px;text-align:center;padding-top:5px;}#side_ad_base div{margin:auto;}#side{padding-top:.5em;width:198px;margin-bottom:10px;}#side .segment{margin:1em 10px;}#side .segment>*{padding-bottom:1em;}#side .segment p{line-height:1.6em!important;}#side .segment ul li{margin:0;padding:0 0 1em 0;}#side .promotion{background-color:#EDFEFF;font-size:11px;margin:1em auto;padding:6px 10px;text-align:left;width:152px;}#side .promotion a{outline:none;color:#333;}#side .promotion a:hover{text-decoration:none;}#side .promotion a.definition:hover strong{outline:none;text-decoration:underline;}#side .promotion .definition strong{display:block;color:#2276BB;}#side span.sponsored{color:#777;display:block;font-size:.9em;padding-bottom:.2em;padding-top:.2em;}#side .notice{margin:.5em auto 1em;padding:10px;background-color:#fff;text-align:center;}#side div.section{padding:13px;}#side div#profile.section{padding-bottom:16px;}#side div.last{border-top:1px solid #C0DEED;}#side h1{color:#333;font-size:1.1em;padding:0 0 2px;margin-bottom:.5em;}#side div.section-header h3{font:16px/18px Helvetica Neue,Helvetica,Arial,sans-serif;border-bottom:1px solid #C0DEED;color:#333;}#side .section-links{float:right;font-size:.9em;text-align:right;}#side div.msg strong{display:block;font-size:1.4em;}#side div.msg h3{font-size:1.25em;}#side ul{margin:0;}#side .faq-index ul{list-style-type:square;margin-left:15px;}#side .faq-index li{margin:10px 0;}#side p{padding:.5em 0;}#side ul{margin:0;}#side div#profile.section{margin-bottom:0;padding-bottom:0;padding-top:.3em;}#side div#profile.profile-side{margin-bottom:1em!important;}#side .profile-side .about{margin-bottom:.6em;}#side div.user_icon a,#side div.user_icon a:hover{text-decoration:none;color:#333;}#side div.user_icon a:hover{color:#0084b5;}#side .user_icon{padding:0 0 .8em;}#side .included-in{margin-top:10px;font-size:11px;}#side .included-in label{color:#666;}.side_thumb{height:31px;width:31px;}.verified-profile,.translator-profile{height:2.8em;}.verified-profile a{background:transparent url(../images/verified/verified.png) no-repeat scroll left center;color:#333;display:block;font-family:Georgia,serif;font-size:1.1em;padding:5px 0 5px 28px;}.translator-profile a{background:transparent url(../images/translator/translator.png) no-repeat scroll left center;color:#333;display:block;font-family:Georgia,serif;font-size:1.1em;padding:5px 0 5px 28px;}.translator-profile a span{font-size:.7em;padding-left:.5em;font-variant:small-caps;}.verified-profile a:hover,.translator-profile a:hover{text-decoration:none;color:#0084b5;}#side .user_icon img{padding-right:.8em;vertical-align:middle;}#me_name{font-size:1.35em;vertical-align:middle;}#side p.promotion{margin-bottom:1em;}.in-page-link{outline:none;}a.help-icon,span.help-icon,ul.sidebar-menu li .help-icon{background:transparent url(../images/sprite-icons.png) no-repeat -160px 0!important;display:inline-block;width:16px;height:16px;padding:0;text-indent:-999em;-moz-opacity:.75;opacity:.75;}a.help-icon:hover,span.help-icon:hover{-moz-opacity:1;opacity:1;}ul.sidebar-menu li .help-icon{display:none;position:absolute;top:6px;right:15px;}ul.sidebar-menu li a:hover .help-icon,ul.sidebar-menu li.active .help-icon{display:block;}ul.sidebar-menu li.loaddisableding .help-icon{display:none!important;}.promoted-trend{position:relative;}.promoted-trend span,ul.sidebar-menu li a.promoted-trend span{background:#ffebbe url(../images/commercial/garuda-overlay.gif) repeat-x 0 -32px;background:#fff -webkit-gradient(linear,0 0,0 100%,from(rgba(255,237,87,.5)),to(rgba(255,171,0,.5)));background:#fff -moz-linear-gradient(top,rgba(255,237,87,.5),rgba(255,171,0,.5));display:inline!important;width:auto;margin:0 0 0 5px;padding:2px 4px;color:#444;font:normal 11px/12px "Helvetica Neue",Helvetica,Arial,sans-serif;text-shadow:0 1px 1px rgba(255,255,255,.5)!important;border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px;box-shadow:0 1px 0 rgba(255,255,255,.5);-moz-box-shadow:0 1px 0 rgba(255,255,255,.5);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.5);}.promoted-trend:hover span,ul.sidebar-menu li a.promoted-trend:hover span,ul.sidebar-menu li.active a.promoted-trend span{background:#ffebbe url(../images/commercial/garuda-overlay.gif) repeat-x 0 -32px;background:#fff -webkit-gradient(linear,0 0,0 100%,from(rgba(255,237,87,.75)),to(rgba(255,171,0,.75)));background:#fff -moz-linear-gradient(top,rgba(255,237,87,.75),rgba(255,171,0,.75));}#side #trends,#side #saved_searches{width:198px;overflow:hidden;}#side #trends em{display:none;}ul.sidebar-menu li .side-tab-ajax{vertical-align:top;float:right;display:none;margin-right:1em;}ul.sidebar-menu span.stat_count{margin-top:0;font:bold 12px Helvetica Neue,Helvetica,Arial,sans-serif;float:right;margin-right:14px;-moz-border-radius:3px;-webkit-border-radius:3px;background-color:transparent;padding:0 .1em;}span.link-title{float:left;}.ie6 ul.sidebar-menu li{zoom:1;overflow:hidden;}.ie6 ul.sidebar-menu li a{margin-top:-2px;}ul.sidebar-menu li{padding-top:1px;}ul.sidebar-menu li a{cursor:pointer!important;display:block;clear:both;padding:.5em 0 .5em 14px;outline:none;background-image:none;margin-right:-1px;}.safari ul.sidebar-menu li a{padding:.5em 0 .5em 14px;}.ie8 ul.sidebar-menu li a{margin-right:0;}ul.sidebar-menu li a span{display:inline-block;overflow:hidden;width:150px;}ul#primary_nav.sidebar-menu li a span{display:inline-block;width:auto;}ul.sidebar-menu li a span.stat_count{display:block;}ul.sidebar-menu li.loaddisableding a{background:#EDFEFF url(../images/spinner.gif) no-repeat 171px .5em;}ul.sidebar-menu li.loaddisableding a span.stat_count{display:none!important;}ul.sidebar-menu li a:hover{background-color:#EDFEFF;text-decoration:none;}ul.sidebar-menu li.active a{font-weight:bold;color:#333;background-color:#EDFEFF;}#side ul.sidebar-actions{margin:.2em 14px 1em;}#side p{padding:.5em 0;}#side p.no-lists{padding:.5em 14px;}#side span.xref{display:block;padding:4px 14px;}#side p.sidebar-menu-actions{padding:.5em 14px;clear:both;display:block;}#side span.new-list,#side span.view-all{padding:0;margin:0 3px 0 0;font-size:11px;line-height:11px;}#side span.view-all{display:inline;margin:0;}#side span.pipe{border-left:1px solid #C0DEED;padding-left:4px;padding-right:4px;}#side #following span.xref{display:block;margin-top:-5px;padding:0 14px 5px 14px;}#side .geo_nearby_activity{padding:.5em 14px;}#side div.geo_nearby_activity{padding-top:0;}#side div.geo_nearby_activity li{margin-top:8px;}#side p.geo_find_in_progress{color:#999;}#side p.geo_nearby_activity a{padding:0;margin:0 3px 0 0;font-size:11px;line-height:11px;}#side .geo_address{color:#999;font-size:10px;}.geo_minorlink{color:#81B2D9;}.geo_prev_next_separator{color:#aaa;}#side .place_icon{background:url(../images/sprite-icons.png) no-repeat -240px -64px;display:inline-block;height:11px;width:7px;margin-right:4px;vertical-align:middle;}#na_menu .with-place{display:none;}#custom_search{padding:.4em 0;margin:1px 0 3px;}#side div#custom_search.active{background-color:#EDFEFF;}#sidebar_search input{border-color:#b4b4b4 #ccc #ccc #b4b4b4!important;border-style:solid none solid solid!important;border-width:1px 0 1px 1px!important;font-size:1em;padding:.4em;width:136px!important;margin:.25em 0 .25em 12px;outline:none;-moz-box-shadow:none;-webkit-box-shadow:none;}.ie7 #sidebar_search input{position:relative;width:145px!important;}.ie7 #sidebar_search .submit{position:relative;top:0;left:-10px;width:8px;margin-left:0;margin-right:0;}#sidebar_search_submit{background:url(../images/nav_search_submit.png) -2px 0!important;}#sidebar_search_submit:hover{background:url(../images/nav_search_submit.png) -2px -25px!important;}#sidebar_search_submit:active{background:url(../images/nav_search_submit.png) -2px -50px!important;}#sidebar_search_submit.loaddisableding,#sidebar_search_submit.loaddisableding:hover,#sidebar_search_submit.loaddisableding:active{background:#eee url(../images/spinner.gif) no-repeat 5px 5px!important;}#sidebar_search input,#sidebar_search_submit{padding-top:5px!important;padding-bottom:5px!important;border-color:#999!important;vertical-align:middle;}#sidebar_search .submit{-moz-border-radius-bottomright:3px;-moz-border-radius-topright:3px;-webkit-border-radius-bottom-right:3px;-webkit-border-radius-top-right:3px;background-color:#EEE;background-position:center top;border-style:solid;border-width:1px;cursor:pointer;padding:.4em .9em;}#saved_searches ul{margin-bottom:3px;}h2.sidebar-title{padding:.2em 14px .2em 14px;font-size:1.05em;font-weight:normal;}.ie h2.sidebar-title span{filter:alpha(opacity=70);}h2.sidebar-subtitle{padding:.2em 14px .2em 14px;font-size:1.3em;font-weight:normal;}#side .collapsible h2.sidebar-title{background:transparent url(../images/toggle_up_dark.png) no-repeat right center;width:157px;}#side .collapsible.collapsed h2.sidebar-title{background:transparent url(../images/toggle_down_dark.png) no-repeat right center;}#side div.collapsible.loaddisableding h2.sidebar-title{background:transparent url(../images/spinner.gif) no-repeat center right!important;}#side .collapsible a.fetch-contents{display:none;}#side .collapsible h2.sidebar-title:hover{cursor:pointer;}#side .collapsed .xref,#side .collapsed .sidebar-menu{display:none;}#side #following #following_list,#side #following #following-in-common-list{padding:5px 10px 5px 14px;}#side #following #friends_view_all,#side #following #follows_in_common_view_all{font-size:.9em;padding:0 14px;}#side p.sidebar-location{padding:3px 0 8px 0;border-bottom:1px dotted #C0DEED;width:170px;margin:0 14px 0 14px;}#side #change_location{font-size:11px;cursor:pointer;font-weight:normal;}#side button.active{background-image:none;text-shadow:none;border:1px solid #ccc;}#side #trends_loaddisableding{position:absolute;right:0;margin-right:14px;}#side #location_menu img{vertical-align:middle;}#local_trend_locations .trends_arrow{position:absolute;right:-9px;margin-top:4px;z-index:999;}#local_trend_locations p{clear:both;display:block;padding:10px 2px 2px 2px;height:26px;clear:both;}#local_trend_locations p button{float:right;margin-top:1px;}#local_trend_locations p span.info{font-size:9px;padding-left:2px;float:left;color:#999;}#local_trend_locations label{clear:both;display:block;padding-bottom:1px;}#side #local_trend_locations hr{height:0;border:0;border-top:1px solid #eee;width:100%;background:#eee;clear:both;margin:0;padding:0;display:block;}#local_trend_locations ul{clear:both;display:block;font-size:11px;margin:3px 0 10px -8px;}#local_trend_locations ul li{float:left;width:130px;}#local_trend_locations .last{border-right:0;}#local_trend_locations li a{display:block;color:#0084B4;text-shadow:0 1px #fff;text-decoration:none;padding:2px 8px 3px 8px;-moz-border-radius:12px;-webkit-border-radius:12px;background:#FFF;border-bottom:0;outline:none;overflow:hidden;line-height:15px;height:14px;margin-bottom:1px;}#local_trend_locations li a.active-parent{background:#f4f4f4;}#local_trend_locations li a:hover{background:#eee;text-shadow:0 1px #fff;}#local_trend_locations .active{cursor:default;}#local_trend_locations li span{display:block;color:#999;padding:3px 8px;margin:2px 0 2px 0;}#local_trend_locations li.active a,#side #local_trend_locations li.active a:hover{text-shadow:0 -1px #555;background:#777 url(../images/follow_check.gif) no-repeat 93% 5px;color:#fff;}#local_trends_notice .modal-inner{padding:8px 12px 12px 12px;}#local_trends_notice .trends_arrow{position:absolute;right:-9px;z-index:999;margin-top:4px;}#local_trends_notice strong.new{text-transform:uppercase;color:#C00;font:bold 10px Helvetica Neue,Helvetica,Arial,sans-serif;}#local_trends_notice h3{font:bold 18px Helvetica Neue,Helvetica,Arial,sans-serif;}#local_trends_notice p{font-size:11px;line-height:14px;padding-bottom:12px;color:#777;}#side a.indented-link{margin:.5em 14px 1em;display:block;}#home #rssfeed,#search #rssfeed,#profile #rssfeed,#profile_favorites #rssfeed,#favorites #rssfeed,#home #rssfeed .timeline-rss,#search #rssfeed .search-rss,#profile #rssfeed .profile-rss,#favorites #rssfeed .favorites-rss,#profile_favorites #rssfeed .favorites-rss{display:block;}#rssfeed,#rssfeed .timeline-rss,#rssfeed .search-rss,#rssfeed .favorites-rss,#rssfeed .profile-rss{display:none;}.rss{background-image:url(../images/rss.gif);margin:.5em 14px 1em;}#side hr{display:block;border:0;height:1px;margin:.5em 14px;opacity:.7;background:#C0DEED;color:#C0DEED;}.ie7 #side hr{width:170px;margin:0 14px;}.ie#side hr{filter:alpha(opacity=70);}.notify{text-align:center;line-height:1;padding:5px 0;background-repeat:no-repeat;background-position:left center;margin-bottom:8px;}.notify div{background-color:#edffe5;font-size:.9em;margin:0 12px;padding:10px 5px;-moz-border-radius:5px;-webkit-border-radius:5px;}#side .actions{border:1px solid #87bc44;margin:10px -3px;}#side .actions small{font-size:.9em;}#side .actions a{padding-left:7px;}#user_restricted h2{padding:10px;font-size:16pt;}#user_restricted img{margin:10px 10px 30px 10px;}#user_restricted p{padding:10px 0 0 10px;font-size:10pt;color:#555;}.side_thumb{height:31px;width:31px;}#side .user_icon{height:31px;display:block;clear:both;}#side .user_icon>*{vertical-align:middle;padding:0;}#side .user_icon img{padding-right:.8em;}#side .user_icon a{cursor:pointer;}#side #me_name{font:bold 1.2em/1.2em Helvetica Neue,Helvetica,sans-serif;position:absolute;margin-top:0;}#side #me_tweets{position:absolute;font-size:11px;margin-top:17px;font-family:Helvetica,Arial,sans-serif;}#side #me_tweets strong{font-size:10px;font-weight:normal;font-family:Helvetica,Arial,sans-serif;}#side .user_icon a:hover #me_tweets{text-decoration:underline;}#side .stats{clear:both;float:none;position:relative;margin:0;padding:0;}#side .stats td{padding:0;vertical-align:top;}#side .stats td+td{padding:0 5px;}#profile #side .stats td+td{padding:0 8px;}#side .stats td+td+td{padding:0!important;}#side .stats a span.stats_count{color:#333;}#side .smaller span.stats_count{font-size:1.1em!important;}#profile #side .smaller span.stats_count{font-size:1.3em!important;}#side .stats a:hover span.stats_count{color:#2276BB;}#side .stats .stats_count{display:block;}#side .stats td .numeric{font:bold 13px Helvetica Neue,Helvetica,Arial,sans-serif;text-decoration:none;}#side .stats td .label{text-transform:lowercase;font-size:.9em;}#side .stats a:hover{text-decoration:none;}#side .stats a:hover .label{text-decoration:underline;}#side .about li{padding-bottom:3px;}#side .about .label{font-weight:bold;}#side .about li#bio{word-wrap:break-word;overflow:hidden;width:170px;}ul#tabMenu li{border-top:1px solid #bddcad;}ul#tabMenu a,#side .section h1{display:block;padding:13px;text-decoration:none;color:#4c4c4c;font-weight:bold;font-size:110%;}#side .section h1{padding:0 0 .25em 0;}body#home ul#tabMenu a#home_tab,body#profile ul#tabMenu a#updates_tab,body#replies ul#tabMenu a#replies_tab,body.direct_messages ul#tabMenu a#direct_messages_tab,body.inbox ul#tabMenu a#inbox_tab,body#favourings ul#tabMenu a#favorites_tab,body#public_timeline ul#tabMenu a#public_timeline_tab{background-color:#fff;margin-left:-1px;padding-left:14px;}#following_list,#following-in-common-list{padding:0 0 0 3px;overflow:hidden;}#following_list span,#following-in-common-list span{float:left;padding:0 3px 2px 1px;}#following_list img,#following-in-common-list img{padding:0;}#device_control label{margin-right:5px;}#device_msg{margin-top:-5px;margin-bottom:0;}.rss{padding:.5em 0 .5em 20px;background-position:0 .5em;background-repeat:no-repeat;}#side p.complete{font-size:.9em;margin-top:1em;}.loaddisableding-spinner{display:none;position:relative;top:4px;left:1px;margin-left:4px;}.loaddisableding .loaddisableding-spinner{display:inline-block;}.loaddisableding-checkbox{margin:3px 1px 1px 4px;}.loaddisableding input.loaddisableding-checkbox{display:none;}fieldset.common-form{width:100%;margin:10px 0;}fieldset.common-form p{margin:0 0 5px 0;}fieldset.common-form th,fieldset.common-form td{padding:10px 5px;vertical-align:top;}fieldset.common-form th{text-align:left;width:10em;padding-top:10px;font-weight:normal;}fieldset.common-form small{color:#777;font-size:11px;}fieldset.common-form input[type="text"],fieldset.common-form input[type="password"],fieldset.common-form textarea,fieldset.common-form select,fieldset.common-form checkbox{border:1px solid #aaa;padding:4px 2px;}fieldset.common-form input[type="text"],fieldset.common-form input[type="password"],fieldset.common-form textarea{border-radius:3px;-moz-border-radius:3px;-o-border-radius:3px;-webkit-border-radius:3px;}fieldset.common-form input[type="text"],fieldset.common-form input[type="password"]{width:12em;}fieldset.common-form input[type="text"].medium,fieldset.common-form textarea.medium{width:50%;}fieldset.common-form input[type="text"].medium{-moz-border-radius:3px;-webkit-border-radius:3px;font-size:12px;}fieldset.common-form input[type="text"].wider,fieldset.common-form textarea.wider{width:75%;}fieldset.common-form input[type="text"].widest,fieldset.common-form textarea.widest{width:100%;}fieldset.common-form td[colspan="2"]{text-align:right;}fieldset.common-form label{white-space:nowrap;font-weight:normal;line-height:24px;}fieldset.common-form ul li{padding:5px 0;}fieldset.common-form ul li label{display:block;font-weight:bold;}fieldset.common-form ul li label sup{color:#888;}fieldset.common-form ul.options li{padding:0;margin:0;}fieldset.common-form ul.options li label{font-weight:normal;}fieldset.common-form table.input-form th{line-height:24px;vertical-align:top;}p.pseudo-input{background:#f3f3f3;width:210px;height:18px;margin:0!important;padding:3px 4px;border:1px solid #ddd;border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px;font:13px/18px Helvetica Neue,Helvetica,Arial,sans-serif!important;color:#777;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.04);}fieldset.common-form .instruction,fieldset.common-form .example,fieldset.common-form .required{font-size:x-small;color:#666;font-weight:normal;}fieldset.common-form .instruction,fieldset.common-form .example{margin-top:.5em;}fieldset.common-form .example{font-style:oblique;}fieldset.common-form .suggestion{color:#C00;font-weight:bold;font-size:10px;}fieldset.vertical-form{margin-top:1em;margin-bottom:1em;}fieldset.vertical-form label,fieldset.vertical-form input{display:block;}fieldset.vertical-form input{margin-top:1em;margin-bottom:1em;}fieldset.vertical-form input[type="text"]{width:165px;}div.direct-message-box fieldset.standard-form{width:548px;padding:10px 90px;}#direct_message_user_id{min-width:100px;}.buttons{padding-top:12px;text-align:center;}.buttons input,.buttons button{margin:0 3px;}.buttons a button{margin:0;}input.submit,button,input[type=submit],input[type=button],input[type="file"]>input[type="button"]{color:#000;-moz-border-radius:5px;-webkit-border-radius:5px;background-color:#e6e6e6;border:1px solid #ccc;font-size:x-small;padding:4px 8px;vertical-align:top;cursor:pointer;}input.submit:hover,button:hover,input[type=submit]:hover,input[type="file"]>input[type="button"]:hover{background-color:#d5d5d5;}input.flow-button,input.flow-button:hover{height:41px;padding:0 10px 2px 0;border:0;font-size:20px;background-color:transparent;}input.green-arrow,input.green-arrow:hover{background-image:url(../images/btn_green_arrow.gif)!important;width:234px!important;background-color:transparent;}input.green-arrow-small,input.green-arrow-small:hover{background-image:url(../images/btn_green_arrow_small.gif)!important;width:138px!important;}input.red-small,input.red-small:hover{background-image:url(../images/btn_red_small.gif)!important;width:114px!important;text-align:center;padding:0 0 2px 0;}.hentry .actions>div.follow-actions{visibility:visible;text-align:left;}.follow-actions .following{background-position:0 50%;background-repeat:no-repeat;}.follow-actions p{padding-left:14px;}.follow-actions .pending{color:#666;}.follow-actions input.submit{width:8em;}.home_page_control input.profilesubmit{background-color:#74CA00;font-size:2em;color:#fff;font-weight:bold;margin:20px 0 10px 0;padding:10px;border:1px solid #0f0;width:175px;cursor:hand;}.home_page_control input.profilesubmit:hover{background-color:#8CF500;}.link-menu>a{padding:.5em .75em .5em .5em;background:transparent url(../images/divot.gif) no-repeat 100% 50%;}.link-menu>ul,.link-menu>span,.link-menu>div{position:absolute;left:0;z-index:999;}body#direct_messages #dm_update_box,body#inbox #dm_update_box,body#sent #dm_update_box{display:block;}body#direct_messages #status_update_box,body#inbox #status_update_box,body#sent #status_update_box{display:none;}.status-btn{float:right;padding:5px 12px 0 5px;}.status-btn input.round-btn{background-image:url(../images/round-btn.gif);width:115px;height:32px;border:0;color:#666;font-size:14px;margin-left:3px;}.status-btn input.round-btn:hover{background-image:url(../images/round-btn-hover.gif);color:#444;}.status-btn input.disabled,.status-btn input.disabled:hover{background-image:url(../images/round-btn.gif);color:#aaa;cursor:default;}#tweeting_controls{float:right;padding:5px 12px 0 5px;_padding:5px 0 0 5px;}.bar{line-height:1.9em;position:relative;padding:0 10px;}.bar h3{font-size:1.4em;}.bar h3 label{font-family:'Helvetica Neue','Helvetica','Arial',sans-serif;font-weight:normal;color:#333;padding-right:130px;font-size:20px;line-height:1.1;width:50%;margin-bottom:10px;}.bar span{color:#ccc;font-size:2em;display:block;position:absolute;top:0;*top:5px;background:transparent;right:10px;}.bar span strong.loaddisableding{background:transparent url(../images/spinner.gif) no-repeat center center!important;color:transparent;}.ie7 .bar span strong.loaddisableding{background-position:left center!important;}.ie7 #status-field-char-counter{line-height:1em;position:relative;top:-3px;}.status-update-form .info{padding:1px 0 0 10px;}.status-update-form textarea{height:2.5em;width:515px;padding:5px;font:1.15em/1.1 'Lucida Grande',sans-serif;overflow:auto;resize:none;}#update_notifications{color:#666;float:left;font-size:11px!important;line-height:16px;overflow:visible;min-height:30px;margin:3px 8px 0 0;padding:2px 4px 2px 0;text-align:left;width:365px;word-wrap:break-word;}.ie7 #update_notifications,.ie6 #update_notifications{width:395px;}.ie #share_location{margin-right:5px;}.ie6 #update_notifications{display:inline;height:30px;}#latest_meta{color:#999;}#latest_status .retweet-source-user{font-weight:bold;}#latest_text{cursor:pointer;}#latest_text_full{display:none;}.firefox2 #update_notifications{float:none;}#dm_update_box{display:none;}#content .tabMenu{text-align:left;margin:25px 0 5px 0;}#content .tabMenu li{display:inline;margin:0;padding:0;}#content .tabMenu li a{margin-right:1px;display:inline;padding:6px 15px 5px 15px;background-color:#F0F0F0;text-decoration:none;color:#2276BB;-moz-border-radius:3px 3px 0 0;-webkit-border-top-left-radius:3px;-webkit-border-top-right-radius:3px;}body #content .tabMenu li a{font-size:13px!important;text-transform:capitalize;}#content .tabMenu li a:hover{background-color:#E6E6E6;}#content .tabMenu li.active a{border:1px solid #c4c4c4;color:#333;background-color:#fff;border-bottom:1px solid #fff;padding:5px 14px 5px 14px;}#content .tab{background-color:#fff;padding:0;border-top:1px solid #cecece;margin:1px 10px;}.password-meter{padding-left:10px;}.pstrength-text{font-weight:bold;}.password-weak{color:#801b1b;}.password-good{color:#803f1b;}.password-strong{color:#80771b;}.password-verystrong{color:#2a801b;}#profilebird{position:absolute;top:0;}#profilebox{background-color:#feffdf;border:1px solid #ff0;padding:20px;vertical-align:middle;}.home_page_new_home_page #profilebox{margin-top:15px;}#profilebox h1,#profilebox h2{font-weight:normal;}#profilebox h2{margin-top:.5em;font-size:1.3em;}#profiletext{float:left;width:470px;}.home_page_new_home_page #profiletext{width:auto;float:none;}#profilebox_outer.home_page_new_home_page{margin-top:15px;}.home_page_new_home_page #profilebox h2{color:#666;font:18px/24px "Helvetica Neue",Arial,Sans-serif;margin:0 0 15px;}.home_page_new_home_page #profilebox h2 strong{color:#333;}.home_page_new_home_page #profiletext h1 span{background:transparent url(../images/larry-shadowed-big.png) no-repeat scroll 100% 50%;_background:transparent url(http://a2.twimg.com/a/1302214109/images/larry-shadowed-big.gif) no-repeat scroll 100% 50%;padding-right:40px;display:block;}.home_page_new_home_page #profiletext h1{color:#333;font:24px/29px "Helvetica Neue",Arial,Sans-serif;margin:0 0 4px;font-weight:bold;}.home_page_new_home_page #profilebutton #profilebox-mobile{text-align:right;padding-top:8px;font-size:12px;line-height:16px;}.home_page_new_home_page #profilebutton{text-align:left;font-size:11px;color:#999;line-height:15px;margin:0;padding:0;}.home_page_new_home_page .profilesubmit{float:left;-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;background:url(../images/bg-btn-signup_gold.png) repeat-x scroll 0 0 #FA2;border:1px solid #FA2;color:#333;display:inline-block;font:bold 18px Arial,Sans-serif;text-align:center;padding:8px 10px;text-decoration:none;text-shadow:0 1px 0 #FE6;margin:0;*padding:8px 0;*font-size:16px;}.home_page_new_home_page input.profilesubmit:hover{background:url(../images/bg-btn-signup_gold.png) repeat-x scroll 0 -5px #FA2;}.home_page_new_home_page #profiletext{float:none;}.home_page_new_home_page #profilebox{background-color:#fff;border:none;-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;color:#666;font:15px/20px "Helvetica Neue",Arial,Sans-serif;margin:0 0 15px;padding:15px 25px;zoom:1;}.home_page_new_home_page .sms-follow-instructions{background:transparent url(../images/icon-mobile.gif) no-repeat 0 50%;padding-left:15px;color:#999;}.home_page_new_home_page .sms-follow-instructions strong{color:#666;}.home_page_new_home_page #sms_codes_link{cursor:pointer;position:relative;padding-bottom:1em;}#sms_codes{display:none;position:absolute;z-index:100;top:100%;left:0;background:#000;padding:10px;text-align:left;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;}#sms_codes:before{content:' ';background:url(../images/trendtip-pointer.gif) no-repeat top center;position:absolute;top:-9px;right:55px;width:17px;height:9px;margin-left:-8px;}.home_page_new_home_page #sms_codes_link:hover{*text-decoration:none;}#sms_codes table{width:450px;}#sms_codes th{font-weight:bold;}#sms_codes td,#sms_codes th{width:33%;}#sms_codes .title td{color:#999;padding:10px 0 5px;}#sms_codes li{overflow:hidden;zoom:1;}#sms_codes li .sms-code{display:block;float:left;font-weight:bold;color:#CCC;width:50%;}#sms_codes li .sms-network{display:block;float:left;color:#666;}#sms_codes .sms-country,#sms_codes th{color:#CCC;}#profilebox_outer.home_page_control{padding-top:41px;position:relative;margin-top:1em;}.home_page_control div#profilebutton{float:right;text-align:center;margin-left:50px;}.home_page_control div#profilebutton small{line-height:1.25em;}.home_page_control input.profilesubmit{background-color:#74CA00;color:#fff;font-weight:bold;margin:0 0 5px 0;border:1px solid #0f0;width:175px;}.home_page_control input.profilesubmit:hover{background-color:#8CF500;}body#profile .profile-head,body#lists .profile-head,body#profile_favorites .profile-head{margin:1px 10px;}body#profile #content h2,body#profile_favorites #content h2{margin:0;}body#profile_favorites #timeline_heading h1{padding-top:8px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;color:#333;font-size:18px;}body#profile #content h2.thumb,body#profile_favorites h2.thumb{padding-bottom:20px;}body#profile ol.statuses span.status-body{margin-left:0;min-height:0;}body#profile ol.statuses li{padding-left:.5em;}body#profile ol.statuses li.latest-status{padding:1.5em 0 1.5em .5em;border-top-width:0;line-height:1.5em;}body#profile ol.statuses>li:first-child{border-top:0;}body#profile ol.statuses .latest-status .entry-content{font-size:1.77em;}body#profile .latest-status .entry-meta{display:block;}ol.statuses li.blocked-status,ol.statuses li.blocked-status:hover{background-color:#fafafa;}.blocked-status .entry-content{font-weight:bold;color:#666;}.blocked-status .meta a{text-decoration:none;color:#1f76b8;}.blocked-status .meta a:hover{text-decoration:underline;}ol.statuses li.latest-status.blocked-status .entry-meta{line-height:1.3em;}#content h2.thumb{font-size:2.8em;line-height:50px;padding:10px 15px 10px 0;}#content h2.thumb img{vertical-align:middle;margin-right:10px;}#content h2.thumb small{font-size:.4em;}#profile .protected-box{background-color:#FEF6A8;border:1px solid #FCFC19;line-height:1;margin-top:1em;padding:0 0 0 10px;}#profile .protected-box .sub-h1{font-size:1.2em;}#profile .protected-box table td{padding:10px;}#profile .protected-box .logged-out{padding:10px;}.profile-controls{text-align:right;padding:7px 10px;margin-bottom:15px;background-color:#f6f6f6;border:1px solid #eee;}.profile-controls li{position:static;}.profile-controls .is-relationship{font:15px 'Helvetica',Sans-serif;text-align:left;float:left;line-height:26px;}body#profile h2.thumb div#follow-details img#x,body#profile_favorites h2.thumb div#follow-details img#x{float:right;margin:3px 0 0 0;cursor:pointer;border:none;}div#follow-control{margin:5px 0 0 15px;}div#follow-details{background-color:#F9FDAB;margin:5px 0 10px 0;padding:5px 10px 10px 10px;border:solid 1px #FDCC68;color:#000;line-height:1.7em;display:none;font-size:.9em;}div#follow-flash{background-color:#F9FDAB;border:solid 1px #FDCC68;font-size:.9em;color:#000;line-height:1.75em;margin:5px 0;font-weight:bold;padding:5px;}div#follow-details p{margin-top:10px;}div#follow_actions{margin-top:10px;}div#follow-actions #onoff{margin-left:10px;}div#follow-details strong{display:inline;font-size:120%;}div#follow-details div#follow_notifications{margin-top:10px;}div#follow-details div#follow_notifications div#notifications-sub{margin-left:14px;}div#follow-toggle{background-repeat:no-repeat;cursor:pointer;background-position:2% 50%;padding:5px 5px 5px 20px;border:1px solid #CCC;}div#followed{background-color:#e6e6e6;border:1px solid #D1D1D1;}div.med-btn{background-color:#e6e6e6;width:75px;height:18px;padding:1px 3px 1px 21px;font-size:11px;vertical-align:middle;color:#000;cursor:pointer;}.follow-button button,.follow-button input[type=submit],input[type=button].follow-button{background-color:#808080;color:#FFF;font-size:1em;font-weight:bold;border:1px solid black;height:30px;width:75px;cursor:pointer;}.remove-button button,.remove-button input[type=submit],input[type=button].remove-button{background-color:#E6E6E6;color:#000;font-size:1em;width:75px;cursor:pointer;margin-left:3px;}input.update-button,.update-button button,.update-button input[type=submit],input[type=button].update-button{background-color:#808080;color:#FFF;font-size:1em;font-weight:bold;border:1px solid black;cursor:pointer;margin-top:10px;}div#follow-toggle.closed{background-image:url(../images/toggle_closed.gif);}div#follow-voided{background-image:url(../images/voided.gif);}.follow-actions .following{background-image:url(../images/checkmark.gif);}body#show .status-body{display:block;margin-right:30px;font-size:1.2em;padding-bottom:15px;}body#show .entry-content{font-weight:400;display:block;background-color:#fff;font-size:2em;font-family:georgia;line-height:1.25em;padding:0;overflow:hidden;}body#show #content .meta{display:block;font-family:'Lucida Grande';color:#999;}body#show #content .meta a{color:#999;}body#show .thumb img{width:48px;height:48px;}body#show .protected{padding-left:0;background-position:55px 50%;background-repeat:no-repeat;background-image:url(../images/icon_lock.gif);}body#show .actions{position:absolute;right:0;top:8px;}body#show .actions .non-fav,body#show .actions .fav{visibility:visible;}body#show .top-nav,body#show #footer{font-size:85%;overflow:hidden;}body#show .hentry{position:relative;}body#show #container{width:600px;-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;}body#show #content{width:570px;padding:15px;margin-bottom:15px;}body#show #footer{width:600px;}body#show #content div.thumb{float:left;margin-right:20px;}body#show.status #content #timeline{border-top-width:0;}body#show .user-info{height:73px;margin-top:0;padding-top:15px;border-top:1px solid #e6e6e6;line-height:1;}body#show .screen-name{font-size:2.3em;}body#show .full-name{font-size:1.2em;margin:3px 0 0 2px;}body#show .desc-inner{position:relative;}body#show .top-navigation .not-required{display:none;}body.search .results-count{float:right;padding-left:1em;padding-right:5px;line-height:2.25em;font-size:x-small;color:#77778A;}body.search#users #timeline{width:100%;border-top:1px dashed #D2DADA;}body.search#users .hentry td.status-body{padding:.5em 0;}body.search#users .hentry td.status-body div{width:370px;}body.search#users .hentry:hover{background-color:transparent;}body.search#users .hentry .bio{font-size:90%;display:block;margin-left:0;padding-top:.3em;}body.search#users .hentry .status-body img{vertical-align:middle;margin:-3px 4px 0;}body.search#users .hentry .status-body .meta{font-family:'Lucida Grande';font-size:.75em;font-style:normal;}div.find-people h2{padding-bottom:6px;}div.find-people form+p{padding-left:12px;}div.find-people form{background:#f6f6f6;padding:2px 12px 0 12px;-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;}.safari div.find-people form{padding:2px 12px 4px 12px;}.safari p.suggestion{padding-bottom:0!important;margin-bottom:0!important;}.vertical-form input{-moz-border-radius:4px;-webkit-border-radius:4px;}#content div.onebox_users{display:none;padding-bottom:20px;}#content .onebox_users h2{float:left;width:350px;color:#333;font:normal 18px "Helvetica Neue",Helvetica,Arial,sans-serif;padding-bottom:3px;}#content .onebox_users p.seeall{text-align:right;font-size:10px;position:relative;top:6px;}#content .onebox_users ul{border:1px solid #DDD;background-color:#EEE;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;clear:both;padding:8px 0;}body #content .onebox_users ul li{position:relative;overflow:hidden;float:left;margin-right:1px;padding:0 0 0 30px;height:30px;}#content .onebox_users ul li.first{margin-left:10px;}#content .onebox_users ul li.last{margin-right:0;}#content .onebox_users ul.size1 li{float:none;}#content .onebox_users ul.size2 li{width:221px;}#content .onebox_users ul.size3 li{width:137px;}#content .onebox_users ul li a.profilepic{position:absolute;left:0;top:2px;}#content .onebox_users ul li a.profilepic img{height:24px;width:24px;}#content .onebox_users ul li .bio{padding-left:1px;font-size:11px;color:#333;}#content .onebox_users ul li .bio p{margin:0;white-space:nowrap;}#content .onebox_users ul li .bio p.username{font-size:12px;font-weight:bold;}#content .onebox_users ul li.verified .bio p.username span{padding-right:20px;background:url(../images/verified/verified_small.png) no-repeat right 0;}#content .onebox_users ul li.protected .bio p.username span{padding-right:12px;background:url(../images/icon_lock.gif) no-repeat right 2px;}ul.bullets{list-style-type:square;padding:1em;}ul.bullets li{margin-left:1em;}#follow-requests .all{float:right;margin:4px 8px 4px 4px;}#follow-requests .follow-request{border:1px solid #bbb;clear:both;padding:10px;margin-bottom:10px;min-height:95px;height:auto!important;height:95px;}#follow-requests .follow-request .name-box{padding-top:2px;}#follow-requests .follow-request .name-box .lock{line-height:.2pt;}#follow-requests .follow-request .screen-name{font-size:2em;line-height:1;text-decoration:none;}#follow-requests .follow-request .name{font-weight:bold;margin-left:2px;}#follow-requests .follow-request .right-box{float:right;background-color:#FEF6A8;border:1px solid #FCFC19;width:300px;padding:8px;}#follow-requests .follow-request .buttons{margin:4px 0 4px;}#follow-requests .follow-request .right-box .request-button{width:100px;font-size:.9em;padding:2px;margin:10px 25px 10px 0;}#follow-requests .follow-request .right-box form{display:inline;margin-right:5px;}#follow-requests .follow-request .right-box td .centered-text{padding:1px;}#follow-requests .follow-request .details{padding-top:4px;clear:left;}#follow-requests .follow-request .details .title{color:#4F4F4F;}#follow-requests .follow-request .details .detail{width:65%;color:#000;}#follow-requests .follow-request .profile-img{float:left;margin-right:10px;}#side .featured{border:1px solid #87bc44;padding:2px 5px;margin:10px -3px;}#side .featured img{vertical-align:middle;padding:1px 0 -5px 7px;}#side .promo{border:1px solid #87bc44;background-color:#fff;padding:10px 0 10px 5px;margin-top:8px;font-size:1em;}#side .promo li{margin:0 0 8px;}#side .promo a{text-decoration:none;}#side .promo img{vertical-align:middle;}div.join{text-align:center;}div.join input{background-color:#417596;color:white;font-size:11pt;padding:.3em 2.5em;font-weight:bold;border:1px solid black;}div.join input:hover{background-color:#294B60;}#dim-screen{position:absolute;background-color:#000;z-index:99;width:100%;height:100%;top:0;left:0;opacity:.90;filter:alpha(opacity=90);display:none;margin:0 auto;}body.account .finish-signup{background:transparent url(../images/icon-mobile.gif) no-repeat scroll left center;padding-left:15px;}.subpage #content p{line-height:1.2;margin:5px 0;}.subpage #content code{font-size:1.2em;}.faq{padding:10px;}.faq p{padding-bottom:20px;}.faq p.header-text{font-size:1.3em;}.ie7 #trends_menu ul{margin-top:2.75em!important;}.ie7 #logo img{margin:.25em 0 0 0!important;}dt{font-weight:bold;margin-top:5px;}#content table.doing{font-size:1.2em;line-height:1.1;width:100%;}#content table.doing td{border-bottom:1px dashed #d2dada;vertical-align:middle;}#content table.doing .right-box td{border:0;}#content table.doing .thumb{padding:10px 5px 8px 5px;width:50px;vertical-align:top;}#content table.doing .meta{font-size:.80em;}#content table.doing .meta img{vertical-align:top;}#content table.doing .user_actions{vertical-align:top;width:16px;}#side div.msg strong{display:block;font-size:1.4em;}#side div.msg h3{font-size:1.25em;}#side .faq-index ul{list-style-type:square;margin-left:15px;}#side .faq-index li{margin:10px 0;}#side ul.todo{font-style:italic;}#side #submit{display:block;padding:3px 10px;margin:5px auto;font:bold 1.12em/1.5 'Lucida Grande',sans-serif;}body.help #side{height:560px;}body.help #side .section{height:100%;}#content .wrapper #lang_header{padding:0;margin:0;width:100%;}#content .wrapper #lang_header td{padding:0;}#lang-select{text-align:center;}#profile_image h2{margin-bottom:1em;}body#picture fieldset.common-form th{width:50px;}#invite_preview{background-color:#eef;padding:10px;}#invite_message{white-space:normal;}span#p{color:#999;}img.follow-icon{border:0;margin:1px 5px 3px 0;vertical-align:middle;}button.small{background-color:#e6e6e6;width:44px;padding:0;font-size:9px;text-align:center;margin:2px 2px 1px 2px;border:none;line-height:9px;cursor:pointer;}button.med{background-color:#e6e6e6;width:75px;height:16px;padding:0;font-size:9px;text-align:center;margin:2px 2px 1px 2px;border:none;}div.big-btn{background-color:#e6e6e6;width:75pt;height:19pt;padding:8px 3px 4px 3px;text-align:center;font-weight:bold;text-decoration:none;font-size:95%;vertical-align:middle;cursor:pointer;}div.long-btn{background-color:#e6e6e6;width:200px;padding:3px 2px 2px 2px;font-size:11px;vertical-align:middle;color:#000;cursor:pointer;}div.med-btn{background-color:#e6e6e6;width:75px;height:18px;padding:1px 3px 1px 21px;font-size:11px;vertical-align:middle;color:#000;cursor:pointer;}div.short-btn{background-color:#e6e6e6;width:60px;height:14px;padding:2px 2px 1px 21px;font-weight:bold;font-size:11px;line-height:14px;vertical-align:middle;color:#000;cursor:pointer;}.profile .protected-box{background-color:#FEF6A8;padding:8px;}input.big-btn{background:url(../images/btn-bg.gif) no-repeat top left;border:none;display:block;width:88px;height:31px;text-align:center;font-weight:bold;text-decoration:none;font-size:95%;vertical-align:middle;}#notifications-sub .desc{margin-left:3px;font-style:italic;}div.badge{margin:0 auto -1.5em;text-align:center;}form.device_control{display:inline;}form.device_control select{font-size:85%;padding:4px 2px;}.device-alert-box{background-color:#FF7B6D;padding:0 10px 10px 10px;border:solid 1px #F00;color:#000;line-height:1.7em;font-size:.9em;margin-top:8px;}.person img.lock{vertical-align:middle;margin-bottom:3px;}#downtime-announce{background-color:#fff;border:1px solid grey;padding:7px;color:#333;font-size:1.1em;}.person-actions{font-size:90%;padding:7px 0 0;}a#back-link{margin-left:20px;font-size:120%;}div#buffer{padding:17px;}#username_url{color:green;font-weight:bold;}.username_taken{color:red;}.fieldWithErrors{display:inline;}.fieldWithErrors input,.fieldWithErrors select,input.errors,select.errors,textarea.errors{background-color:#ffdfdf;}.highlight{background-color:#f9f6ba;}.nav-highlight{background-color:#ff9;}#followers .stop-undo{background-color:#BFBFBF;border:1px solid #4E4E4E;padding:0 10px;text-align:left;display:none;}#followers .stop-button{margin-right:25px;}#followers .stop-undo button{width:140px;}#followers .stop-undo table td{padding:2px 5px;}#followers .right-box{float:right;width:400px;font-size:.9em;text-align:right;margin-right:10px;}#followers .followers-table{width:100%;}.search_following{background-color:#D8F4F5;border:1px solid #84C2D2;}.search_following button{background-color:#fff;border:1px solid #84C2D2;}div.clear{height:1px;}input.labeled_field{color:#999;}.niceform{margin-top:10px;}.niceform label,.niceform input{display:block;width:50%;float:left;margin-bottom:10px;}.niceform label{text-align:right;width:150px;padding-right:20px;}.niceform br{clear:left;}#auth{display:none;padding:10px;margin:10px 0;background-color:#ddd;border:1px solid #999;}#videobutton img{padding-right:5px;}#videobutton{float:right;width:180px;text-align:center;vertical-align:middle;background-color:#ff493c;color:#fff;font-size:11pt;font-weight:bold;border:1px solid #000;padding-top:2px;padding-bottom:2px;}#videobutton:hover{text-decoration:none;}ul.app-list li{display:block;clear:both;}.side_thumb{height:31px;width:31px;}address{font-style:normal;}div#query_review_header_0{width:100px;}button.allow i{float:left;width:19px;height:20px;margin-right:5px;background:url(http://a2.twimg.com/a/1302214109/images/icon-check.gif) no-repeat;}button.allow.btn-green i{background-image:url(http://a2.twimg.com/a/1302214109/images/icon-check2.gif);}button.secure i{float:left;width:9px;height:14px;margin-right:5px;background:url(http://a2.twimg.com/a/1302214109/images/icon-lock.png) no-repeat;}span.lock-icon{display:inline-block;background:transparent url(../images/lock_icon_small.png) no-repeat scroll 0 50%;height:10px;width:8px;*vertical-align:middle;}#timeline_heading #heading span.lock-icon,#lists_table span.lock-icon{margin-left:3px;}li.menu span.lock-icon{margin-left:3px;}#side ul.lists-links li span.lock-icon{margin-bottom:-1px;margin-left:3px;width:8px!important;*background:transparent url(../images/lock_icon_small.png) no-repeat scroll 0 0;}.modal-content fieldset{font-size:11px;line-height:16px;width:100%;color:#888;padding-bottom:10px;}.modal-content label.title{float:left;display:block;width:100px;font-size:13px;color:#333;}.modal-content .wide-dialog label.title{width:150px;}.modal-content input.title,.modal-content textarea.title{padding:5px;border:1px solid #888;font-size:12px;-webkit-border-radius:4px;-moz-border-radius:4px;width:255px;float:left;margin-right:10px;margin-bottom:8px;outline:0;}.modal-content .wide-dialog input.title,.modal-content .wide-dialog textarea.title{width:305px;}.modal-content .preview{-webkit-border-radius:4px;margin-bottom:14px;padding:8px 0 8px 0;background:#eee;clear:both;display:block;font-size:11px;color:#666;}.modal-content fieldset.clear{border-top:1px solid #eee;border-bottom:1px solid #eee;margin-bottom:14px;display:block;clear:both;padding-top:10px;padding-bottom:10px;}.modal-content fieldset.clear.bottom{border-top:none;}.modal-content label.radio{clear:both;padding:3px 0 2px 0;}.modal-content .options{float:left;width:270px;font-size:11px;}.modal-content .options label input{float:left;margin-right:5px;}.modal-content .submit{margin-left:100px!important;margin-bottom:6px!important;}.model-content .privacy{height:33px;}.modal-content .options label strong{color:#333;}.modal-content .options label{clear:both;padding-bottom:4px;display:block;}.modal-content label .optional{color:#888;font-size:11px;display:block;}.modal-content .list-slug,.modal-content .list-description-instruction{overflow:hidden;display:block;padding:5px;width:255px;float:left;margin-right:10px;outline:0;}.modal-content .list-slug{font-size:12px;background:#efefef;border:1px solid #e8e8e8;-webkit-border-radius:2px;-moz-border-radius:2px;margin-bottom:8px;color:#2276BB;font-weight:bold;}.modal-content .list-description-instruction{margin-left:100px;padding-left:0;}.modal-content label.list-slug-title{padding-top:4px;color:#888;}.modal-content .private-warning{display:none;width:270px;border:1px solid #FFE88D;color:#333;margin-left:95px;padding:5px;clear:both;-moz-border-radius:5px;-webkit-border-radius:5px;background-color:#FFFFD1;}input.text_field{border:1px solid #ddd;font-size:14px;padding:8px;width:200px;margin:0;-moz-border-radius:5px;-webkit-border-radius:5px;}input.text_field.with-box{-moz-border-radius-topright:0;-moz-border-radius-bottomright:0;-webkit-border-top-right-radius:0;-webkit-border-bottom-right-radius:0;}.profile-header{padding:0 10px;}#content ol.statuses li.search_result a.reply{background-image:url(../images/icon_reply.gif);}.tipsy .retweet-icon{background-image:url(../images/sprite-icons.png);background-position:-96px -48px;height:16px;line-height:13px;width:16px;position:relative;margin-top:-5px;top:5px;left:-1px;display:-moz-inline-stack;display:inline-block;zoom:1;*display:inline;*top:2px;}.tipsy .retweet_tip_tip{padding-top:4px;line-height:13px;}.retweet-tooltip.tipsy .tipsy-inner{max-width:300px;}.tipsy.tipsy-north.left-align{background-position:14px top;}.fixed-banners{position:fixed;top:0;left:0;z-index:9999;_position:absolute;_top:expression(eval(document.documentElement.scrollTop));width:100%;}.account-nav{background:#555;font:11px Lucida Grande,Tahoma,sans-serif;color:#fff;width:100%;height:26px;text-align:center;-moz-box-shadow:0 1px 0 rgba(0,0,0,.2);-webkit-box-shadow:0 1px 0 rgba(0,0,0,.2);}.account-nav-content{margin:0 auto;text-align:left;width:763px;position:relative;z-index:99;}.account-nav a{color:#fff;}.account-nav ul{margin-left:-7px;}.account-nav ul li{display:block;float:left;margin:0;}.account-nav ul li a{cursor:pointer;display:block;padding:5px 7px 9px 7px;height:12px;_float:left;}.account-nav ul li:hover a{background:#444;text-decoration:none;}.account-nav ul li.divider{border-left:1px solid #444;border-right:1px solid #666;display:block;width:0;margin-top:7px;height:12px;}.account-nav ul li ul.account-switcher li.h-divider{border-top:1px solid #333;border-bottom:1px solid #555;height:0;width:100%;}.account-nav ul li ul.account-switcher{display:none;margin-left:0;}.account-nav a img{float:left;margin-right:5px;}.account-nav a span{float:left;}.account-nav li.account-switch a{color:#ccc;}.account-nav li.account-switch i{margin:5px 0 0 4px;width:7px;height:5px;background-position:-79px -67px;display:block;float:left;background-image:url(../images/sprite-icons.png);background-repeat:no-repeat;_display:none;}.account-nav li.account-switch.hover{position:relative;}.account-nav li.account-switch.hover ul.account-switcher{width:180px;display:block;position:absolute;-moz-box-shadow:0 1px 0 rgba(0,0,0,.2);-webkit-box-shadow:0 1px 0 rgba(0,0,0,.2);border:1px solid #444;}.ie6 .account-nav li.account-switch.hover ul.account-switcher{top:25px;left:0;}.account-nav li.account-switch ul.account-switcher li{display:block;clear:both;width:100%;}.account-nav li.account-switch ul.account-switcher li *{cursor:pointer;}.account-nav li.account-switch ul.account-switcher li a{display:block;color:#fff;background:#444;padding:5px 7px 9px 7px;clear:both;height:12px;outline:none;_width:100%;}.account-nav li.account-switch ul.account-switcher li a:hover{background:#666;text-decoration:none;}.account-nav li.account-switch ul.account-switcher li a:active{background:#333;color:#ccc;}.account-nav #switcher-alert{display:block;float:left;font-style:normal;font-weight:normal;overflow:hidden;color:yellow;opacity:1;height:14px;margin-right:4px;}.account-nav #multi-author-feedback{float:right;color:#ccc;}.account-nav ul li#multi-author-feedback a{display:inline;padding:0;line-height:24px;color:white;}.account-nav ul li#multi-author-feedback a:hover{text-decoration:underline;background:#555;}.account-nav ul li#multi-author-feedback:hover a{background:#555;}#manage_contributor_permissions_dialog ul li{margin-top:10px;clear:both;}#manage_contributor_permissions_dialog ul li div{height:30px;}#manage_contributor_permissions_dialog ul li div.decline-buttons{float:right;text-align:right;padding-top:5px;}#manage_contributor_permissions_dialog ul li div.decline-profile{float:left;}#manage_contributor_permissions_dialog ul li span.decline-screen-name{vertical-align:super;}#manage_contributor_permissions_dialog .declining_spinner{background:transparent url(../images/spinner.gif) no-repeat;padding-left:20px;}body.contributor-skybar{background-position:0 26px;padding-top:26px!important;_padding-top:36px!important;}body.phoenix-skybar{background-position:0 35px;padding-top:35px!important;_padding-top:45px!important;}body.phoenix-skybar.contributor-skybar{background-position:0 61px;padding-top:61px!important;_padding-top:71px!important;}.tipsy .retweet-icon{background-image:url(../images/sprite-icons.png);background-position:-96px -48px;height:16px;line-height:13px;width:16px;position:relative;margin-top:-5px;top:5px;left:-1px;display:-moz-inline-stack;display:inline-block;zoom:1;*display:inline;*top:2px;}.tipsy .retweet_tip_tip{padding-top:4px;line-height:13px;}.retweet-tooltip.tipsy .tipsy-inner{max-width:300px;}.tipsy.tipsy-north.left-align{background-position:14px top;}.geo_pin{width:10px;height:10px;cursor:pointer;}.rate_limit_message{padding:10px;}.rate_limit_message p{color:#636363;font-size:15px;}.rate_limit_message p.wait{position:absolute;bottom:60px;color:#333;}.rate_limit_message p.wait span{color:#fff;background:#333 url(../images/divider.png) repeat-x 0 50%;margin:0 4px;padding:3px;-moz-border-radius:5px;-webkit-border-radius:5px;}.rate_limit_message img{margin:0;float:right;}.inactive{display:none;}#update_detached_email #content,#not_my_account #content,#detach_email #content,#detached_email #content{padding:5px 15px;}#not_my_account #content p,#detach_email #content p{margin:20px 0;}#not_my_account h1,#detach_email h1,#detached_email h1{font:26px Helvetica Neue,Helvetica,Arial,sans-serif;font-weight:bold;}.gray-footer{font:11px 'Lucida Grande',sans-serif;background:#f7f7f7;color:#666;margin:0 -25px -20px;padding:10px 15px;-moz-border-radius-bottomleft:5px;-moz-border-radius-bottomright:5px;-webkit-border-bottom-right-radius:5px;-webkit-border-bottom-left-radius:5px;}#not_my_account #content p.nevermind-link a{line-height:32px;padding-left:10px;}.detached-email-warning{background:#ffd url(../images/warning-sign.png) no-repeat 10px 50%;border:solid 1px #eec;margin:5px 1px;padding:8px 10px 8px 60px;}#update_detached_email_form{padding:20px 60px 35px;}#update_detached_email_form label{font-size:16px;color:#666;display:block;margin-bottom:5px;}#update_detached_email_form input[type=text]{width:250px;font-size:16px;padding:5px;margin-right:4px;}.rounded-four-corners{-moz-border-radius:5px;-webkit-border-radius:5px;}.hoverer{position:absolute;visibility:hidden;top:0;left:0;z-index:9999;}.hoverer .hoverer-inner{border:4px solid #ddd;-moz-border-radius:5px;-webkit-border-radius:5px;background:#fff;overflow:hidden;zoom:1;-moz-box-shadow:#aaa 0 1px 0;-webkit-box-shadow:#aaa 0 1px 0;position:relative;}.hoverer .hovercard-divot{position:absolute;left:24px;width:27px;height:15px;z-index:999;}body.loaddisableding-hoverer-above{position:relative;}div.page-header{background:#f5f5f5;margin:-20px -20px 20px;padding:15px 20px 0 20px;border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;-webkit-border-top-left-radius:5px;-webkit-border-top-right-radius:5px;}div.page-header ul.tabs{position:relative;margin:15px -20px 0 -20px;padding-left:9px;width:auto;max-width:1040px;min-width:911px;}div.page-header h1{line-height:30px;padding:0;margin:0;font-size:20px;clear:none;}div.page-header h1 img{float:left;margin:0 10px 0 0;}div.page-header img{width:30px;height:30px;border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px;}div.page-header p.right{float:right;line-height:20px!important;}div.page-header span.verified-profile{font-size:11px!important;}div.page-header span.verified-profile a{color:#999;display:inline;background-position:4px -1px;padding-left:30px;}div.page-header ul.page-tools{float:right;margin:5px 0 0;}div.page-header ul.page-tools li{display:inline;padding-left:10px;}div.page-header ul.page-tools li a,div.page-header ul.page-tools li a.btn{font-size:12px;}div.page-header ul.page-tools li a.btn{padding:5px 9px;}div.sub-header{margin:0 0 10px;}div.sub-header h2{clear:none;}div.sub-header h2 small{font-size:14px;font-weight:normal;color:#999;line-height:1;}ul.tabs{height:30px;width:100%;border-bottom:1px solid #e5e5e5;clear:both;}ul.tabs li{display:inline;line-height:1;}ul.tabs li a{display:inline;float:left;width:auto;margin:0;padding:4px 10px 3px;line-height:24px;border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;-webkit-border-top-left-radius:4px;-webkit-border-top-right-radius:4px;text-shadow:0 1px 0 #fff;overflow:hidden;}ul.tabs li a:hover{background:#eee;text-decoration:none;padding-bottom:2px;}ul.tabs li.active a{background:#fff;border:1px solid #e5e5e5;border-bottom:0;color:#333;font-weight:bold;padding-top:3px;}ul.tabs li.active a:hover{padding-bottom:3px;}ul.tabs li.menu{position:relative;float:left;display:inline;}ul.tabs li.menu a.menu{float:none;display:block;}ul.tabs li.menu a.menu i{background-position:-47px -64px;width:7px;background-image:url(../phoenix/img/sprite-icons.png);background-repeat:no-repeat;display:inline-block;opacity:.4;height:13px;outline:none;overflow:hidden;margin-left:4px;position:relative;top:3px;*left:4px;*top:0;_margin-top:8px;}ul.tabs li.menu ul{display:none;background:#fff;position:absolute;top:30px;left:0;width:180px;padding:3px 0;border:1px solid #999;border-radius:4px 4px 4px;-moz-border-radius:4px 4px 4px;-webkit-border-radius:4px;-webkit-border-top-left-radius:0;box-shadow:0 1px 2px rgba(0,0,0,.3);-moz-box-shadow:0 1px 2px rgba(0,0,0,.3);-webkit-box-shadow:0 1px 2px rgba(0,0,0,.3);}ul.tabs li.menu ul li{width:160px;}ul.tabs li.menu ul li a,ul.tabs li.menu ul li a:hover{display:block;width:160px;padding:4px 10px 3px;border-radius:0;-moz-border-radius:0;-webkit-border-radius:0;}ul.tabs void a.menu,ul.tabs li.menu ul li a:hover{color:#fff;background:#999;border-color:#999;text-shadow:0 1px 0 rgba(0,0,0,.25);}ul.tabs void a.menu i,ul.tabs li a.menu:hover i{opacity:1;}ul.tabs void ul{display:block;}ul.pills{background:#eaf3f9;margin:0 -20px;padding:6px 8px;}ul.pills li{display:inline;}ul.pills li a{display:inline;float:left;width:auto;margin-right:3px;padding:6px 12px 6px;line-height:11px;border-radius:12px;-moz-border-radius:12px;-webkit-border-radius:12px;text-shadow:0 1px 1px #fff;}ul.pills li a:hover{background:#99bfe1;background:rgba(34,118,187,.4);color:#fff;-moz-box-shadow:inset 0 1px 3px rgba(34,118,187,.25);-webkit-box-shadow:0 1px 1px #fff;text-decoration:none;text-shadow:0 1px 1px rgba(34,118,187,.75);}ul.pills li.active a{background:#2276BB;background:rgba(34,118,187,1);color:#fff;-moz-box-shadow:inset 0 1px 3px rgba(34,118,187,.5);-webkit-box-shadow:0 1px 1px #fff;text-shadow:0 1px 1px rgba(0,0,0,.5);text-shadow:0 1px 1px rgba(34,118,187,1);}div.well{background:#f3f3f3;padding:14px 19px;margin:0 0 20px;border-radius:5px;-moz-border-radius:5px;-webkit-border-radius:5px;border:1px solid #eee;border-top-color:#ddd;}table.common-table{width:100%;margin:5px 0 20px;border-collapse:separate;font-family:Helvetica Neue,Helvetica,Arial,sans-serif;}table.common-table th{color:#555;padding:10px;border-bottom:2px solid #ddd;}table.common-table thead td{font-weight:bold;color:#333;}table.common-table td{padding:5px 10px 5px 10px;color:#555;line-height:18px;border-bottom:1px solid #eee;vertical-align:top;}table.common-table td+td,table.common-table th+th{border-left:1px solid #fff!important;}table.common-table tbody tr:hover td{background:rgba(0,0,0,.03);}table.common-table .one{width:40px;}table.common-table .two{width:80px;}table.common-table .three{width:120px;}table.common-table .four{width:160px;}table.common-table .five{width:200px;}table.common-table .six{width:260px;}table.common-table a.block-link{display:block;margin:-10px;padding:10px;font-weight:bold;text-shadow:0 1px 1px rgba(255,255,255,.75);}table.zebra-striped td{text-shadow:0 1px 1px rgba(255,255,255,.75);}table.zebra-striped thead tr th{border-bottom:3px solid rgba(141,192,219,.6);white-space:nowrap;}table.zebra-striped tbody tr td{border:0!important;border-bottom:1px solid #fff!important;white-space:nowrap;}table.zebra-striped tbody tr:nth-child(odd) td{background-color:rgba(204,234,243,.25)!important;}table.zebra-striped tbody tr:hover td{background-color:rgba(204,234,243,.5)!important;}table.common-table th.header{cursor:pointer;padding-right:20px;}table.common-table th.headerSortUp,table.common-table th.headerSortDown{background-image:url(../images/tables/tablesorter-indicators.png);background-position:right -23px;background-repeat:no-repeat;-moz-border-radius:3px 3px 0 0;-webkit-border-top-left-radius:3px;-webkit-border-top-right-radius:3px;background-color:rgba(141,192,219,.25);text-shadow:0 1px 1px rgba(255,255,255,.75);}table.common-table th.header:hover{background-image:url(../images/tables/tablesorter-indicators.png);background-position:right 16px;background-repeat:no-repeat;}table.common-table th.actions:hover{background-image:none!important;}table.common-table th.headerSortDown,table.common-table th.headerSortDown:hover{background-position:right -24px;}table.common-table th.headerSortUp,table.common-table th.headerSortUp:hover{background-position:right -64px;}table.common-table th.blue{color:#2276BB;border-bottom-color:#2276BB;}table.common-table th.headerSortUp.blue,table.common-table th.headerSortDown.blue{background-color:#d3e4f1;}table.common-table th.green{color:#4bb14b;border-bottom-color:#4bb14b;}table.common-table th.headerSortUp.green,table.common-table th.headerSortDown.green{background-color:#dbefdb;}table.common-table th.red{color:#ab2920;border-bottom-color:#ab2920;}table.common-table th.headerSortUp.red,table.common-table th.headerSortDown.red{background-color:#eed4d2;}table.common-table th.yellow{color:#faa226;border-bottom-color:#faa226;}table.common-table th.headerSortUp.yellow,table.common-table th.headerSortDown.yellow{background-color:rgba(250,162,38,.2);}table.common-table th.align-right,table.common-table td.align-right{text-align:right;}table.common-table .muted{color:#999;}span.status-label{background:#ccc;padding:2px 5px 3px;font-size:10px;font-weight:bold;color:#fff;text-shadow:0 0 1px rgba(0,0,0,.01)!important;text-transform:uppercase;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}span.status-label.expired{background-color:#f5f5f5;color:#999;}span.status-label.pending{background-color:#48489b;}span.status-label.declined{background-color:#9b4848;}span.status-label.active,span.status-label.approved{background-color:#59bf59;}span.status-label.disabled{background-color:#faa226;}span.status-label.scheduled{background-color:#f5f5f5;color:#59bf59;text-shadow:0 1px 0 rgba(255,255,255,.5)!important;}#recommended_users{margin:18px 0 25px;}#recommended_users .view_all{padding:8px 14px 0;font-size:.9em;}#recommended_users .sidebar-title a{float:right;font-size:10px;padding-top:2px;}#recommended_users ul{padding:8px 14px 0;font-size:11px;}#recommended_users li{clear:both;margin-bottom:10px;zoom:1;}#recommended_users div.avatar{float:left;width:34px;}#recommended_users div.bio{float:right;width:135px;}#recommended_users p{margin-bottom:2px;padding:0;}#recommended_users li .next-suggestion{float:right;font-size:12px;margin-top:-3px;color:#999;cursor:pointer;text-decoration:none;}#recommended_users li a.next-suggestion:hover{color:#777;cursor:pointer;text-decoration:none;}#recommended_users .sidebar-title a{float:right;}#recommended_users p.screen-name{font-weight:bold;}#recommended_users p.verified{margin-bottom:1px;}#recommended_users p.verified a{padding:1px 18px 1px 0;background:transparent url(../images/verified/verified_small.png) no-repeat center right;}#recommended_users .sidebar-menu ul li:after{content:".";display:block;height:0;clear:both;visibility:hidden;}#recommended_users .sidebar-menu ul li img{margin-right:5px;}#recommended_users .sidebar-menu ul li span{font-size:12px;line-height:1.3;}#recommended_users div.screen-name{font-weight:bold;}#recommended_users .sidebar-menu ul li span.name{color:#333;}#recommended_users .sidebar-menu ul li span.follow{font-size:11px;}#recommended_users img{width:28px;height:28px;}#recommended_users p.follow-link a.loaddisableding{text-decoration:none;color:gray;cursor:default;}#recommended_users p.follow-link span.pending{text-decoration:none;color:gray;cursor:default;font-size:11px;}.recommended-similar-users{background-color:#f6f6f6;border:1px solid #eee;padding:10px;margin:0;-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;display:none;}#content .recommended-similar-users h3{font-weight:normal;font-size:13px;margin-top:0;color:#666;}.recommended-similar-users .seeall{margin:0;font-size:11px;text-align:right;}.recommended-similar-users .seeall a{color:#999;}.recommended-similar-users ul{margin:10px 0;clear:both;}.recommended-similar-users li{float:left;width:160px;font-size:11px;}.recommended-similar-users li img{width:35px;height:35px;}.recommended-similar-users .bio{padding-top:3px;float:right;width:120px;}.subpage #content .recommended-similar-users .bio p{margin:0 0 2px;}.recommended-similar-users .close{float:right;color:#aaa;margin-top:-2px;}form.twitter-form h3{margin-bottom:10px;}form.twitter-form fieldset{margin:20px -20px -10px 0;padding:13px 20px 5px 0;border-top:1px solid #ddd;}form.twitter-form fieldset legend{background:#fff;float:left;margin:-25px 0 15px 140px;padding:0 10px;font-size:20px;font-weight:normal;line-height:1;color:#333;}form.twitter-form fieldset legend small{font-size:14px;font-weight:normal;color:#777;}form.twitter-form div.clearfix{margin:0 0 20px;}form.twitter-form fieldset div.clearfix{clear:both;}form.twitter-form label{float:left;width:130px;text-align:right;padding-top:4px;color:#333;font-size:13px;}form.twitter-form label small{font-size:12px;color:#777;}form.twitter-form label.inline-label{display:inline;float:none;width:auto;}form.twitter-form div.input{margin-left:150px;}div.actions{background:#f5f5f5;margin:30px 0 0;padding:20px 20px 20px 150px;border-top:1px solid #ddd;border-radius:0 0 3px 3px;-moz-border-radius:0 0 3px 3px;-webkit-border-bottom-left-radius:3px;-webkit-border-bottom-right-radius:3px;}div.actions a.cancel{line-height:34px;padding-left:5px;}div.actions div.secondary-action{float:right;}div.actions div.secondary-action a{line-height:34px;}div.actions div.secondary-action a:hover{text-decoration:underline;}form.twitter-form div.actions{margin-right:-20px;}form.twitter-form fieldset+div.actions{margin-top:20px;}form.twitter-form input[type=text],form.twitter-form input[type=password],form.twitter-form textarea,form.twitter-form select{width:210px;margin:0;padding:3px 4px;font:13px/18px Helvetica Neue,Helvetica,Arial,sans-serif!important;color:#555;border:1px solid #ccc;border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px;}form.twitter-form select{padding:auto;width:auto;height:25px;line-height:25px;}form.twitter-form p.uneditable-input{padding-top:4px;margin-bottom:0!important;}body.ie form.twitter-form input[type=text],body.ie form.twitter-form input[type=password],body.ie form.twitter-form textarea{padding-bottom:5px;}body.firefox form.twitter-form input[type=text],body.firefox form.twitter-form input[type=password],body.firefox form.twitter-form textarea{padding-top:3px;padding-bottom:5px;}.input-mini,form.twitter-form input.mini,form.twitter-form textarea.mini,form.twitter-form select.mini,form.twitter-form p.pseudo-input.mini{width:60px;}.input-small,form.twitter-form input.small,form.twitter-form textarea.small,form.twitter-form select.small,form.twitter-form p.pseudo-input.small{width:90px;}.input-medium,form.twitter-form input.medium,form.twitter-form textarea.medium,form.twitter-form select.medium,form.twitter-form p.pseudo-input.medium{width:150px;}.input-large,form.twitter-form input.large,form.twitter-form textarea.large,form.twitter-form select.large,form.twitter-form p.pseudo-input.large{width:210px;}.input-xlarge,form.twitter-form input.xlarge,form.twitter-form textarea.xlarge,form.twitter-form select.xlarge,form.twitter-form p.pseudo-input.xlarge{width:270px;}.input-xxlarge,form.twitter-form input.xxlarge,form.twitter-form textarea.xxlarge,form.twitter-form select.xxlarge,form.twitter-form p.pseudo-input.xxlarge{width:530px;}form.twitter-form textarea.xxlarge{overflow-y:scroll;}form.twitter-form input[readonly]:focus,form.twitter-form textarea[readonly]:focus{border-color:#ddd!important;box-shadow:none;-moz-box-shadow:none;-webkit-box-shadow:none;}.help-inline,.help-block{font-size:12px;color:#777;}.help-inline{padding-left:3px;}.help-block{display:block;max-width:640px;margin:5px 0 0!important;line-height:18px;}.help-warning{color:#faa226;}.help-error{color:#ab2920;}form.twitter-form a.help-icon{position:relative;top:1px;left:2px;}div.help-block h5,div.help-block p,div.help-block ol li{color:#555;}div.help-block p,div.help-block ol li{font-size:12px!important;}div.help-block h5{font-size:13px;line-height:18px;}div.help-block p{margin-bottom:10px;font-size:12px;line-height:18px;color:#777;}div.help-block ol{margin-bottom:10px;margin-left:25px!important;}div.inline-inputs{position:relative;color:#555;}div.inline-inputs span,div.inline-inputs input[type=text]{display:inline-block;}div.inline-inputs input.mini{width:62px;}div.inline-inputs input.small{width:90px;}div.preface-input{position:relative;}div.preface-input input[type=text]{border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;-webkit-border-top-left-radius:0;-webkit-border-bottom-left-radius:0;}div.preface-input span.preface{background:#f5f5f5;display:inline;float:left;padding:3px 6px;font-size:13px;line-height:18px!important;height:18px;color:#555;border:1px solid #ccc;border-right:0;border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;-webkit-border-top-left-radius:3px;-webkit-border-bottom-left-radius:3px;}body.ie7 div.preface-input span.preface{margin-top:1px;}form.twitter-form label.checkbox,form.twitter-form label.radio{display:block;width:auto;padding:4px 0 0;float:none;text-align:left;}ul.options{margin:0;padding:3px 0 0;width:100%;}ul.options li{display:block;margin-bottom:5px;padding:0;width:100%;}ul.options li:last-child{margin-bottom:0;}ul.options label{position:relative;display:block;float:none;width:auto;margin:0;padding:0 0 0 20px;line-height:20px;text-align:left;white-space:normal;}ul.options label strong{color:#555;}ul.options label .help-icon{position:relative;top:1px;}ul.options input[type=radio],ul.options input[type=checkbox]{position:absolute;top:0;left:0;float:left;margin:4px 5px 0 0;}body.ie ul.options input[type=radio],body.ie ul.options input[type=checkbox]{margin-top:2px;}body.ie7 ul.options input[type=radio],body.ie7 ul.options input[type=checkbox]{margin-top:-2px;}ul.options label small{font-size:12px;font-weight:normal!important;}ul.options ul{margin-top:5px;}div.disabled span{color:#aaa;}div.disabled input[type=text],div.disabled input[type=passsword],div.disabled textarea{background:#f5f5f5;}ul.options label.disabled,ul.options label.disabled span,ul.options label.disabled small,ul.options label.disabled strong{color:#aaa!important;}ul.options li ul{margin-left:20px;}div.row{margin-left:-20px;}div.row div.column,div.row div.columns{display:inline;float:left;margin:0;}div.row div.column,div.row div.columns{margin-left:20px;}div.row div.one{width:40px;}div.row div.two{width:100px;}div.row div.three{width:160px;}div.row div.four{width:220px;}div.row div.five{width:280px;}div.row div.six{width:340px;}div.row div.seven{width:400px;}div.row div.eight{width:460px;}div.row div.nine{width:520px;}div.row div.ten{width:580px;}div.row div.eleven{width:640px;}div.row div.twelve{width:700px;}div.row div.thirteen{width:760px;}div.row div.fourteen{width:820px;}div.row div.fifteen{width:880px;}div.row div.sixteen{width:940px;}div.row div.one-fourth{width:205px;}div.row div.one-third{width:300px;}div.row div.offset-by-one{margin-left:60px;}div.row div.offset-by-two{margin-left:120px;}div.row div.offset-by-three{margin-left:180px;}div.row div.sidebar{width:310px;margin-left:50px;}body.ie6 div.row{width:960px;}.hashflag img{position:relative;left:-2px;top:3px;}span.unsafe{text-decoration:line-through;}.biz-info-form{max-width:600px;}.biz-info-form td select{float:left;}.biz-info-form td input{float:left;width:210px!important;}#phoenix-banner{background:#555;color:white;width:100%;z-index:9999;padding:4px 0;}#phoenix-banner .inner{margin:0 auto;position:relative;width:763px;text-align:left;}#phoenix-banner span{display:inline-block;padding:6px 8px;}#phoenix-banner img{height:26px;float:left;}#phoenix-banner a{color:#FFF;font-weight:bold;}.modal-overlay{z-index:9999!important;width:100%;position:fixed;margin:0;background-color:#000;opacity:.3;filter:alpha(opacity = 30);top:0;left:0;text-align:center;height:100%;}.hanging{display:block;width:400px;z-index:10001!important;position:fixed;top:0;left:0;}.attached .hanging,.ie6 .hanging{position:absolute;margin:0;}.attached .modal-overlay{position:absolute;}.attached .modal-inner{overflow:visible;}.modal{display:block;background:#ccc;-webkit-border-radius:4px;-moz-border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.5);-moz-box-shadow:0 1px 2px rgba(0,0,0,.5);padding:4px;}.modal-inner{background:#fff;-webkit-border-radius:4px;-moz-border-radius:4px;padding:0;text-align:left;overflow:hidden;zoom:1;}.modal-inner h2{font-family:'Lucida Grande',sans-serif;background:#efefef;margin:0 0 4px!important;padding:8px 10px!important;height:18px!important;-moz-border-radius-topleft:4px;-moz-border-radius-topright:4px;-webkit-border-top-left-radius:4px;-webkit-border-top-right-radius:4px;}.modal-inner h2 span{font-size:13px;font-weight:bold;float:left;}.modal-inner a.modal-close,.modal-inner h2 a.close{float:right;font:bold 16px/12px tahoma,sans-serif;margin-top:2px;text-decoration:none;color:#999;text-shadow:1px 1px 1px #fff;}.modal-inner .no-heading a.modal-close{margin:5px 5px 0 0;color:#bbb;}.modal-inner h2 a.modal-close:hover,.modal-inner h2 a.close:hover{color:#333;}.modal-inner .footer{background-color:#efefef;padding:10px;text-align:center;-moz-border-radius-bottomleft:4px;-moz-border-radius-bottomright:4px;-webkit-border-radius-bottomleft:4px;-webkit-border-radius-bottomright:4px;border-radius-bottomleft:4px;border-radius-bottomright:4px;}.modal-inner .footer button{margin:0 2px;}.modal-content{padding:10px;padding-bottom:10px;}.twttr-dialog .north{background:url(../images/dialog_arrows_sprite.gif) no-repeat center 0;height:12px;display:block;position:relative;margin-bottom:-4px;}.twttr-dialog .south{background:url(../images/dialog_arrows_sprite.gif) no-repeat center -36px;height:12px;display:block;position:relative;margin-top:-4px;}.twttr-dialog .weight-left .north{background-position:left top;}.twttr-dialog .weight-left .south{background-position:left -36px;}.twttr-dialog .weight-right .north{background-position:right top;}.twttr-dialog .weight-right .south{background-position:right -36px;}.twttr-dialog .east{background:url(../images/dialog_arrows_sprite.gif) no-repeat right center;display:block;position:absolute;width:12px;right:-8px;top:0;}.twttr-dialog .west{background:url(../images/dialog_arrows_sprite.gif) no-repeat left center;display:block;position:absolute;width:12px;left:-8px;top:0;}.twttr-dialog .weight-top .east{background-position:right top;}.twttr-dialog .weight-bottom .east{background-position:right bottom;}.twttr-dialog .weight-top .west{background-position:left top;}.twttr-dialog .weight-bottom .west{background-position:left bottom;}.password-dialog .password-wrapper{width:247px;}body .password-dialog .password-wrapper input{width:235px;}.password-dialog .password-wrapper div{margin-top:3px;text-align:right;}.password-dialog .modal-inner .footer{background:#fff;}.password-dialog #cancel_link{padding-left:5px;line-height:24px;}.password-dialog fieldset.common-form,.password-dialog fieldset.common-form ul li{margin-bottom:0;padding-bottom:0;}#notifications{position:fixed;top:0;left:0;width:100%;overflow:visible;z-index:10000;}.notification-bar{position:absolute;top:0;left:0;color:#000;border-bottom:2px solid rgba(0,0,0,0.07);width:100%;cursor:pointer;}.notification-bar-bkg{background-color:#fff;opacity:.95;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=95)";filter:alpha(opacity=95);position:static;}.notification-bar,.notification-bar-bkg{padding:1.2em 0;}.ie7 .notification-bar,.ie7 .notification-bar-bkg{border-bottom:2px solid #ccc;}.notification-bar-container{position:relative;display:block;width:100%;overflow:visible;}.notification-bar-contents{width:740px;margin:0 auto;text-align:left;position:relative;font-size:150%;}.notification-bar .message-progress{padding-left:24px;background-image:url(../images/ajax.gif);background-repeat:no-repeat;background-position:left center;}body.timeline #content h1{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;color:#333;font-size:18px;}#timeline_heading h1{color:#666;font-size:16px;font-weight:normal;padding:0 0 3px 0;}#timeline_heading h1 a,#timeline_heading h1 span.loaddisableding{font-size:10px;padding-left:15px;font-family:"Lucida Grande",Lucida Grande,Arial,sans-serif;}#timeline_heading h1#heading div#rate-limited-error a{float:none;font-size:16px;padding:0;}.save-search-link{background:transparent url(../images/icon_add.png) no-repeat left top;}.delete-search-link{background:transparent url(../images/icon_remove.png) no-repeat left top;}#timeline_heading h1 ul{float:right;position:relative;top:5px;}h1.hide-name-search li.name-search-link{display:none;}#timeline_heading h1 ul.has-saved-search{top:-5px;}#timeline_heading h1.hide-name-search ul.has-saved-search{top:3px;}#content #timeline_heading h1 ul li{padding:0;text-align:right;line-height:13px;}#content #timeline_heading h1 ul li.name-search-link a{padding:0;}#timeline_heading h1 span.loaddisableding{background:transparent url(../images/spinner.gif) no-repeat left top;padding-left:18px;}#content div.section #timeline_heading{margin:0;}body#inbox ol.statuses>li:first-child,body#sent ol.statuses>li:first-child,body#lists ol.statuses>li:first-child,body#direct_messages ol.statuses>li:first-child{border-top:1px solid transparent;}#inbox div#timeline_heading,#sent div#timeline_heading,#lists div#timeline_heading,#direct_messages div#timeline_heading{border-bottom:1px solid #cecece;}#dm_tabs{display:none;}body#direct_messages #dm_tabs,body#inbox #dm_tabs,body#sent #dm_tabs{display:block;}.tabMenu li.loaddisableding a{background-image:url(../images/spinner.gif);background-repeat:no-repeat;background-position:center center;color:transparent!important;}body#direct_messages #content .tabMenu #inbox_tab a,body#inbox #content .tabMenu #inbox_tab a,body#sent #content .tabMenu #sent_tab a,body#direct_messages #content .tabMenu #inbox_tab a,body#retweets_by_others #content .tabMenu #retweets_by_others_tab a,body#retweets #content .tabMenu #retweets_tab a,body#retweeted_by_others #content .tabMenu #retweeted_by_others_tab a,body#retweeted_of_mine #content .tabMenu #retweeted_of_mine_tab a{border:1px solid #c4c4c4;color:#333;background-color:#fff;border-bottom:1px solid #fff;padding:5px 14px 5px 14px;margin-right:1px;}#next_steps{display:none;font-size:1.2em;line-height:1.1;}body#home #next_steps{display:block;}#next_steps td.thumb{padding:10px 0 8px;width:50px;vertical-align:top;}#next_steps td{border-bottom:1px dashed #d2dada;vertical-align:middle;padding:7px 3px;}.subpage #content #next_steps li{padding-bottom:10px;}.subpage #content #next_steps li p{margin:0;font-size:.85em;color:#999;text-decoration:none;}#next_steps .step-completed span{text-decoration:line-through;}#search #content div.trend-description-container{display:block;}#content .trend-description-container{display:none;margin:0!important;padding:7px 0 0 0!important;}#trend_description img{vertical-align:middle;margin:1px 5px 3px;}.ie8 #trend_description img{margin-top:-1px;}.ie7 #trend_description img{margin-bottom:-2px;}#content #trend_description{display:none;padding:0;line-height:18px;margin:0 0 1em;}#content #trend_description span{color:#777;}#content #trend_description p{margin:0;line-height:18px;font-size:1.1em;color:#333;}#content #trend_description p strong{color:#333;}ol.statuses{list-style:none;font-size:14px;}ol.loaddisableding{height:300px;background:transparent url(../images/petal_spinner.gif) no-repeat center 50px;opacity:.3;filter:alpha(opacity = 30);}ol.statuses li.status,ol.statuses li.direct_message{position:relative;padding:10px 0 8px 0;border-bottom:1px solid #eee;line-height:16px;zoom:1;}ol.statuses>li.last-on-page,ol.statuses>li.last-on-refresh{border-bottom:1px solid #ccc!important;}ol.statuses>li:first-child{border-top:1px solid #eee;}ol.statuses>li.buffered{display:none!important;}.entry-meta{margin-top:2px;}.retweet-meta{margin-top:0;}#permalink .entry-meta{line-height:16px;}ol.statuses .thumb{display:block;width:50px;height:50px;position:absolute;left:0;margin:0 10px 0 0;overflow:hidden;z-index:10;}ol.statuses .thumb img{width:48px;height:48px;}.no-results{border-top:1px dashed #D2DADA;padding:.7em 0 .6em 1em;font-size:1.2em;}.no-results ol{padding:5px 0 0 30px;}ol.statuses span.status-body{display:block;min-height:48px;width:425px;overflow:hidden;margin-left:56px;}ol.statuses span.status-body .lock{margin-right:.4em;}#users ol.statuses span.status-body{width:365px;}ol.statuses .embedded_media_icon{height:20px;width:20px;background:transparent url(../images/inline-media.png) no-repeat bottom left;cursor:pointer;position:absolute;top:10px;right:-10px;z-index:400;}ol.statuses .embedded_picture{margin:10px 0 0 0;text-align:center;}ol.statuses .embedded_picture img{border:2px solid #DDD;padding:10px;}.search ol.statuses .bio{margin-left:5px;}.entry-content em{font-style:normal;font-weight:bold;}.meta{display:block;font-size:11px;color:#999;}.meta a{color:#999;}.meta .call-out{color:#000;}#content .meta .byline a{color:#0084b4;}ol.statuses .actions{position:absolute;right:10px;top:8px;line-height:1.25em;border-width:0;}.ie6 ol.statuses .actions{right:25px;}.actions a{text-decoration:none;}ol.statuses li{position:relative;}ol.statuses li:hover .actions span,ol.statuses li.hover .actions span,ol.statuses li.perma-hover .actions span,ol.statuses li:hover .actions a,ol.statuses li.hover .actions a,ol.statuses li.perma-hover .actions a{visibility:visible;}ol.statuses li.no-hover .actions span,ol.statuses li.no-hover .actions a{visibility:hidden!important;}.actions .non-fav,.actions .fav-throb,.actions .fav,.actions .reply{margin-bottom:3px;}.hentry .non-fav,.hentry .fav,.actions .reply{background-image:url(../images/sprite-icons.png);width:15px;height:15px;display:block;cursor:pointer;visibility:hidden;}.hentry .fav-throb{display:block;background-position:50% 50%;height:15px;width:15px;}.hentry .fav{background-position:-64px 0;}.hentry .non-fav{background-position:-32px 0;}.hentry .non-fav:hover{background-position:-48px 0;}.hentry .fav-throb,.hentry .del-throb{background-image:url(../images/icon_throbber.gif);}.hentry .del{background-image:url(../images/icon_trash.gif);}.direct_message a.reply{background-image:url(../images/icon_direct_reply.gif);}.direct_message .hentry .del{background-image:url(../images/icon_trash.gif);}ol.statuses li:hover,ol.statuses li.perma-hover,ol.statuses li.hover{background-color:#f7f7f7;}ol.statuses li:hover .hentry a,ol.statuses li.perma-hover .hentry a,ol.statuses li.hover .hentry a{visibility:visible;}ol.statuses .hentry a.fav{visibility:visible;}.status_activity{margin:4px 0 0 0;padding:10px 0 0 20px;}.status_activity .activity{margin:0 0 5px;}.status_activity .content{vertical-align:top;margin:0 0 0 5px;font-size:.8em;}#flash{padding-top:45px;background-repeat:no-repeat;background-position:24px 0;margin:1.5em 0;}#flash p{background-color:#fff;font-size:2.12em;line-height:1.2em;padding:.5em;font-weight:bold;}.feature-limited{background-color:#f9f6ba;border:2px solid #e9e6aa;-moz-border-radius:5px;-webkit-border-radius:5px;padding:.5em 1em;}.minor-notification{background-color:#e3f1fa;border:solid #c6e4f2;border-width:1px 0;padding:.5em 1em;text-shadow:0 1px 1px rgba(255,255,255,.5);color:#222!important;}div#new_results_notification{margin:0!important;}a#results_update{display:block;margin:0 0 20px;}a#results_update:hover{background-color:#d9ecf9;border-color:#b1d4e4;cursor:pointer;text-decoration:none;}.bulletin{padding:10px;display:none;}body#home .bulletin{display:block;}body#settings .bulletin{margin:20px 100px;}.bulletin a.close{padding:6px 6px 7px 7px;float:right;opacity:.7;background:transparent url(../images/close_small.png) no-repeat;}.ie .bulletin a.close{filter:alpha(opacity=70);}.bulletin a.close:hover{opacity:1.0;}.ie .bulletin a.close:hover{filter:alpha(opacity=100);}.bulletin.warning{background-color:#ff9;border:1px solid #ecec19;}.bulletin.alert{background-color:#ffab9d;border:1px solid #f88;}.bulletin.help{font-size:11px;background:#f8f8f8;line-height:18px;border:1px solid #eee;}.bulletin p,.bulletin h2{margin-left:46px!important;margin-bottom:5px!important;}.bulletin.info{padding:5px 10px!important;background-color:#F4F4F4;border:0;font-size:.9em;}.ie6 .standard-form .info{background:none;border:0;}.ie6 .bulletin{zoom:1;}.bulletin img{vertical-align:middle;float:left;}.bulletin.help img{margin-top:2px;margin-left:2px;}.bulletin h2{font:bold 13px Helvetica Neue,Helvetica,Arial,sans-serif;font-size:14px;margin-top:0!important;}.yellow-box{background-color:#FEF6A8;margin:1em;padding:1em;border:solid 1px #FFFA00;color:#000;line-height:1.7em;font-size:.9em;text-align:center;}#pagination{margin:2em 0 1em 0;}#pagination p.no-more-tweets{font-size:1.1em;text-align:center;}.more{outline:none;display:block;width:100%;padding:6px 0;text-align:center;border:1px solid #ddd;border-bottom:1px solid #aaa;border-right:1px solid #aaa;background-color:#fff;background-repeat:repeat-x;background-position:left top;font-size:14px;text-shadow:1px 1px 1px #fff;font-weight:bold;height:22px;line-height:1.5em;margin-bottom:6px;background-image:url(../images/more.gif);}.more:hover{border:1px solid #bbb;text-decoration:none;background-position:left -78px;}.more:active{color:#666;background-position:left -38px;}.more.loaddisableding{cursor:default!important;background-color:#fff;background-repeat:no-repeat;background-position:50% 50%;border:1px solid #eee;background-image:url(../images/ajax.gif);}.more::-moz-focus-inner{border:0;}.tip{color:#333;background-color:#e5eef2;border:1px solid #c7e0ed;padding:10px;margin:20px 0;-moz-border-radius:5px;-webkit-border-radius:5px;}.tip .dismiss{float:right;width:9px;height:10px;background:url(../images/sprite-icons.png) no-repeat -272px -16px;cursor:pointer;}#follow_users_tips h4{font-size:13px;line-height:16px;font-weight:normal;width:85%;margin:0 0 13px;}#follow_users_tips ol{zoom:1;overflow:hidden;list-style:none;}#follow_users_tips h5,#follow_users_tips li{font-weight:bold;font-size:11px;line-height:15px;}#follow_users_tips li.user-search{width:177px;padding-right:0;}#follow_users_tips li.user-search input[type='text']{width:114px;*width:112px;}#follow_users_tips li.user-search input{margin-top:3px;}#follow_users_tips li.follow-friends{width:130px;}#follow_users_tips li{float:left;width:132px;padding-right:20px;}#follow_users_tips form p{display:none;}#follow_users_tips li+li{border-left:1px solid #c7e0ed;padding:0 20px 0 10px;}#content #follow_users_tips fieldset{margin:0;}#content #follow_users_tips p.instruction{display:none;}html #content .tip p{font-weight:normal;color:#666;margin:0;line-height:15px;}html #content #mobile_tips p{width:85%;color:#333;}#pagination.pagination{height:1.5em;}#pagination.pagination a,#pagination.pagination .link-like{border:1px solid #cecece;padding:.25em 1em;margin:0 0 0 10px;float:right;}.person .thumb img{height:48px;width:48px;}ol.statuses span.status-body{overflow:visible;}ol.statuses span.status-body span.status-content{overflow:hidden;}ol.statuses li.garuda-tweet{background:#fff;border-color:#ddd;}ol.statuses li.garuda-tweet .actions-hover li{background-color:transparent!important;}ol.statuses li .tweet-label{-moz-box-shadow:none;-webkit-box-shadow:none;-moz-border-radius:3px;-webkit-border-radius:3px;background:#ffebbe url(../images/commercial/garuda-overlay.gif) repeat-x 0 0;background:-webkit-gradient(linear,0 0,0 100%,from(rgba(255,237,87,.25)),to(rgba(255,171,0,.25)));background:-moz-linear-gradient(top,rgba(255,237,87,.25),rgba(255,171,0,.25));color:#444!important;line-height:12px!important;margin:0!important;padding:2px 4px!important;text-shadow:0 1px 1px rgba(255,255,255,.5)!important;}ol#timeline li .tweet-label span{text-shadow:0 1px 1px rgba(255,255,255,.5)!important;}ol#timeline li .tweet-label span.promoted_by{color:#817046!important;}ol#timeline li .tweet-label.top-tweet{background:#C3E2EF url(../images/toptweet-overlay.gif) repeat-x 0 0;color:#888;}ol.statuses li.garuda-tweet:hover{background:#fdfcf1;background:rgba(255,237,87,.15);border-color:#e7e3ce;}ol#timeline li.garuda-tweet:hover .tweet-label{background:#ffd46b url(../images/commercial/garuda-overlay.gif) repeat-x 0 -32px;background:-webkit-gradient(linear,0 0,0 100%,from(rgba(255,237,87,.5)),to(rgba(255,171,0,.5)));background:-moz-linear-gradient(top,rgba(255,237,87,.5),rgba(255,171,0,.5));color:#59505f!important;-moz-box-shadow:0 1px 0 #fff;-webkit-box-shadow:0 1px 0 #fff;}ol.statuses li ul.meta-data{display:block;font-size:10px;}ol.statuses li ul.meta-data li{float:left;display:inline;line-height:16px!important;margin-right:7px!important;padding:0!important;color:#999;}ol.statuses li ul.meta-data li:hover{background:none!important;}ol.statuses li ul.meta-data a{color:#999;cursor:default;}ol.statuses li ul.meta-data a:hover{text-decoration:none;}ol.statuses li ul.meta-data a em{display:block;float:left;background-image:url(../images/sprite-icons.png@v3);background-repeat:no-repeat;width:14px;height:15px;margin:0 2px 0 0;}ol.statuses li ul.meta-data span.promoted_by a{cursor:pointer;color:#817046;}ol.statuses li ul.meta-data span.promoted_by a:hover{text-decoration:underline;}ol.statuses li ul.meta-data a.meta-retweets em{background-position:-224px 0;margin-right:2px;}ol.statuses li ul.meta-data a.meta-retweets:hover em{background-position:-240px 0;}ol.statuses li ul.meta-data a.meta-replies:hover em{background-position:-16px 0;}.garuda-tipsy a{color:#fff;}.garuda-tipsy a:hover{text-decoration:none;}.garuda-tipsy-container .tipsy-inner{max-width:none!important;font-weight:normal;}body#list .retweet-link,body#list_show .retweet-link{display:none;}#side .retweet-feedback{margin:0 14px 0 14px;padding:.5em 0 .3em 0;color:#666;font-size:11px;}.actions-hover li{padding:0!important;display:block;float:left;}.actions-hover{position:absolute;bottom:8px;font-size:11px;padding-right:10px;right:0;overflow:visible;color:#999;float:right;visibility:hidden;}body.ie6 #timeline .actions-hover{position:relative;right:-50px;}body.ie6#profile #timeline .actions-hover{position:relative;right:-90px;}#timeline div.no-retweets-text{margin-top:36px;margin-left:106px;width:325px;line-height:17px;font-size:13px;color:#333;}.no-retweets-text .header{font-weight:bold;}.no-retweets-text img{margin-top:12px;}.retweeting.retweet-loaddisableding{background:transparent url(../images/spinner.gif) left top no-repeat;color:#999;font-size:11px;line-height:14px;padding-left:16px;position:absolute;bottom:10px;right:7px;}body.ie6 span.retweeting{position:static;}.retweet-status-body-wrapper .retweeting.loaddisableding{bottom:1px;}#content #permalink .retweeting.loaddisableding{bottom:12px;}#content li:hover .actions-hover,#content li.hover .actions-hover,#content li.perma-hover .actions-hover{visibility:visible;}#content .no-hover .actions-hover{visibility:hidden!important;}#content li .meta,#content li .actions-hover{height:16px;height:auto;}.actions-hover a.fav-throb,.actions-hover .del-throb{visibility:visible;}.hentry .del-throb{background-image:none;}.hentry .actions-hover .del-throb .delete-icon{background-image:url(../images/icon_throbber.gif);background-position:0 0;top:3px;}.hentry .actions-hover span.icon{display:block;float:left;background-image:url(../images/sprite-icons.png);background-repeat:no-repeat;margin-left:8px;}.hentry .actions-hover li .retweet-link,.hentry .actions-hover li .del,.hentry .actions-hover li .reply{display:block;float:left;line-height:16px;}.hentry.latest-status .actions-hover li a{line-height:16px;}.actions-hover .reply-icon{width:15px;height:15px;margin-right:1px;}.actions-hover .retweet-icon{width:16px;height:16px;margin-right:2px;background-position:-176px 0;}.actions-hover .delete-icon{width:15px;height:15px;margin-right:1px;background-position:-112px 0;}.actions-hover .delete-icon,.actions-hover .reply-icon,.actions-hover .retweet-link .retweet-icon{cursor:pointer;}.actions-hover .reply:hover .reply-icon{background-position:-16px 0;}.actions-hover .retweet-link:hover .retweet-icon,.hentry.perma-hover .actions-hover .retweet-icon{background-position:-192px 0;}.actions-hover .del:hover .delete-icon{background-position:-128px 0;}.actions-hover .reply:hover a,.actions-hover .del:hover a,.actions-hover .retweet-link:hover a{text-decoration:underline;}.hentry .del,body#show .hentry .actions-hover .reply{background-image:none;}ol.statuses li.hentry .reply,ol.statuses li.hentry .del{background-image:none;}#content .shared-content .thumb{width:12px!important;height:12px!important;padding:0!important;margin:0 .3em 0 0!important;position:static!important;display:inline!important;vertical-align:middle;}#content .shared-content .thumb img{width:12px!important;height:12px!important;}#content .shared-content .status-body{margin-left:0!important;min-height:15px!important;}#content .shared-content img{margin-right:4px;}#content .shared-content .screen-name{color:#0084B4;font-family:'Lucida Grande',sans-serif;font-style:normal;margin-right:0;}#content .meta .shared-content .screen-name{font-weight:normal;}#content .shared-content,#content .shared-content .status-body{width:370px;}#content .shared-content .entry-content{font-style:italic;line-height:1.1em;display:inline;margin-top:0;}.inline-form{position:absolute;top:0;left:0;z-index:100;width:320px;overflow:visible;padding-top:7px;background-image:url(../images/arr-inline-form.gif);background-repeat:no-repeat;background-position:236px top;}.inline-form.retweet-ctx-dlg{width:330px;line-height:17px;}.inline-form-inner{-moz-border-radius:6px;-webkit-border-radius:6px;-border-radius:6px;-moz-box-shadow:0 2px 4px #ABABAB;-webkit-box-shadow:0 2px 4px #ABABAB;text-align:left;border:4px solid #c7c7c7;width:100%;overflow:auto;background-color:#fff;position:absolute;}.inline-form .cancel{float:right;margin:8px 10px;cursor:pointer;width:10px;height:10px;background-image:url(../images/retweet/retweet-x.png);}.inline-form .spinner{display:none;}.inline-form-buttons{margin:6px 0 4px;text-align:right;}.retweet-dlg .inline-form-buttons button.btn{width:56px;font-size:13px;font-weight:bold;}.inline-form-inputs{float:left;overflow:visible;}.inline-form-input{display:none;}.inline-inputs-prompt{margin:.3em 0 .3em .7em;}.inline-form .title{font-size:13px;font-weight:bold;margin:12px 10px;}.inline-form .body{margin:7px 10px 12px;}.inline-form.retweet-ctx-dlg{background-position:23px center;padding-top:11px;}.inline-form.retweet-ctx-dlg .title{font-size:16px;margin-bottom:7px;}.inline-form.retweet-ctx-dlg .inline-form-buttons{text-align:left;padding-left:10px;}body#show .shared-content{font-style:normal;font-family:'Lucida Grande',sans-serif;}body#show .shared-content .screen-name{font-size:1em;}#permalink .actions-hover a{display:inline;background-image:none;}#content #permalink .actions-hover{display:inline;bottom:12px;padding-right:0;padding-bottom:3px;white-space:nowrap;visibility:visible;}body.ie6 #content #permalink .actions-hover{position:relative!important;display:block;visibility:visible!important;}body#retweets #timeline>li:first-child,body#retweets_by_others #timeline>li:first-child,body#retweeted_of_mine #timeline>li:first-child{border-top:1px solid transparent;}body#retweets div#timeline_heading,body#retweeted_of_mine div#timeline_heading,body#retweets_by_others div#timeline_heading{border-bottom:1px solid #cecece;}#retweet_tabs{padding:0;display:none;}body#retweets #retweet_tabs,body#retweets_by_others #retweet_tabs,body#retweeted_of_mine #retweet_tabs{display:block;}body#retweets #timeline_heading h1,body#retweets_by_others #timeline_heading h1,body#retweeted_of_mine #timeline_heading h1{display:none;}.retweet-status-body-wrapper{position:relative;}.retweet-status-body-wrapper .actions-hover{bottom:0;}body.ie7 .retweet-status-body-wrapper .actions-hover{bottom:3px;}.share-text{background-color:#f7f7f7;border:2px solid #f7f7f7;border-left:2px solid #d7d7d7;padding:.25em .6em .4em;margin:.3em 0 .2em;}.share-text-author{color:#666;margin-top:.45em;}.share-text-author strong .screen-name{font-weight:normal;}ol.statuses .share-text-author .thumb{margin-left:.1em;margin-top:-2px;}ol.statuses .share-text-author .thumb img{width:18px;height:18px;}.shared-by-avatar-tiles span.thumb{margin-right:3px;padding:0;}ol.statuses .thumb-inline{position:static;display:inline;}.friend-who-shared-this strong .screen-name{font-weight:normal;color:inherit;margin-right:0;}.friend-who-shared-this strong .screen-name:hover{text-decoration:underline;}.share-text-active{background-color:#fff;}body#profile #container ol.statuses span.status-body .share-text .entry-content{font-size:1em;}body#profile #container ol.statuses .latest-status .entry-content{font-size:1.77em;}.big-retweet-icon{display:inline-block;width:18px;height:14px;position:relative;top:2px;background-repeat:no-repeat;background-image:url(../images/sprite-icons.png);background-position:-128px -64px;}.ie7 .big-retweet-icon{top:-2px;margin-right:3px;}li.share-with-details div.shared-by-avatar-tiles{margin-top:1px;margin-left:56px;line-height:28px;}li.share-with-details div.shared-by-avatar-tiles .meta{line-height:16px;margin-top:2px;}li.share-with-details div.shared-by-avatar-tiles img{height:25px;width:25px;}ol.statuses li{padding:10px 0;}body#profile .latest-status .actions-hover{bottom:24px;}#introduce_retweet_banner{background:#fff;margin:20px 0;-moz-border-radius:6px;-webkit-border-radius:6px;border:4px solid #DDD;padding:10px;color:#5c5c5c;-moz-box-shadow:0 2px 4px #ABABAB;-webkit-box-shadow:0 2px 4px #ABABAB;}#introduce_retweet_banner .retweet-banner-example{float:right;}#introduce_retweet_banner h1{padding:0;margin:0;font-family:Helvetica,sans-serif;font-weight:bold;font-size:16px;white-space:nowrap;}#introduce_retweet_banner h1 span.beta{color:#ABABAB;font-size:15px;}#introduce_retweet_banner p{padding:0;margin:12px 0;font-family:'Lucida Grande',sans-serif;font-size:13px;line-height:17px;}#introduce_retweet_banner div{width:250px;}#list_show table.columns,#lists_members table.columns,#lists_subscribers table.columns{margin-top:5px;}h2.list-subheading{margin-top:-8px!important;padding-bottom:16px;}p.list-numbers,p.list-link{font-weight:normal;font-size:11px;margin-top:0;padding-top:0;}p.list-numbers{color:#666;float:left;}p.list-numbers a+a{border-left:1px solid #eee;padding-left:8px;margin-left:8px;}p.list-link{float:right;}p.list-link span{font-weight:bold;}p.list-numbers a{color:#666;}p.list-numbers a:hover{color:#0084B4;text-decoration:underline;}p.list-numbers a span{font:bold 11px Helvetica Neue,Helvetica,Arial;}#content .list-title-section{margin:30px 0 1em 5px!important;}#lists_members .wrapper,#lists_subscribers .wrapper{padding:5px 10px 15px;}.list-header{margin:-5px 0 0 -10px!important;}.list-header,.list-header-inner{background:#ddeef6;-moz-border-radius:5px 0 0 0;-webkit-border-top-left-radius:5px;border-radius:5px 0 0 0;}.list-header-inner{padding:15px 0 0 20px;margin-right:-10px;height:62px;}.list-header h2 a{color:#333;text-decoration:none;}body #content .list-header h2{font:22px Helvetica Neue,Helvetica,arial,sans-serif;-webkit-text-outline:1px transparent;margin-left:0;overflow:hidden;margin:0;width:520px;}body #content .list-header .description{font-size:11px;margin-top:2px;}body #content .list-header h2 i{margin-right:-5px;font-size:22px;color:#666;}.ie7 ul.user-actions{width:126px!important;}body#following.ie7 .following ul.user-actions{width:83px!important;margin-right:-6px;}.ie7 .user-actions-outer .list-menu,.ie7 .user-actions-outer .action-menu,.ie7 .user-actions-outer .follow-action{float:left;width:40px;}.ie7 .user-actions-outer .list-menu{width:43px;}.ie7 .user-actions .list-menu ul{clear:both;display:block;margin-top:23px;margin-left:-33px;}.ie7 .profile-controls .list-menu ul{margin-left:-66px;}.ie7 .user-actions-outer .list-menu button{padding:0 6px;zoom:1;width:33px;}.ie7 .profile-controls .list-menu{width:60px;margin-left:20px!important;float:left;display:inline;zoom:1;}.ie7 .profile-controls .user-actions .follow-action button{float:left!important;margin-left:-367px;position:relative;}.ie7 .profile-controls .list-menu button{width:66px;}#lists_table{margin-top:0;}#lists_table .author{display:block;position:absolute;width:30px;padding-top:2px;}#lists_table .list-info{display:block;margin-left:32px;min-height:24px;}#lists_table tr td{color:#999;vertical-align:top;}#lists_table tr:hover td{background:#f6f6f6;color:#333;}#lists_table .list-info .description{display:block;font-size:11px;}.list-menu button i{display:block;float:right;background-position:-79px -67px;margin:4px 0 0 3px;width:7px!important;height:5px;*margin:4px 0 0 0!important;}.ie7 .profile-controls .list-menu button i{margin-top:-11px!important;position:relative;zoom:1;}.ie8 .list-menu button i{margin:4px 0 0 0!important;}#follow_grid .user:hover .user-actions .list-menu button i,.profile-user .user .user-actions .list-menu button i,.user-actions .list-menu button.clicked i{background-position:-47px -67px;}.list-menu button b{background-image:url(../images/sprite-icons.png);display:block;float:left;background-position:-64px -64px;margin:1px 3px 0 0;width:12px;height:13px;}.user-actions-outer .list-menu button{padding-left:6px;padding-right:6px;}.ie7 .profile-controls .list-menu button b{margin-right:-6px!important;}#follow_grid .user:hover .user-actions .list-menu button b,.profile-user .user .user-actions .list-menu button b,.user-actions .list-menu button.clicked b{background-position:-96px -64px;}.user-actions-outer .list-menu button b{margin:0;}.list-menu ul li{padding-left:5px;}.list-menu ul li label{padding:4px 2px!important;width:70%;cursor:pointer;}.list-menu ul li input[type="checkbox"]{margin:0 0 0 5px;}.ie7 .list-menu ul li input[type="checkbox"]{margin:0 0 0 2px;}.ie8 .list-menu ul li input[type="checkbox"]{margin:0 0 0 -1px;float:left;}.lists .lists-links li,.lists_subscribers .lists-links li,.lists_members .lists-links li,#profile #side_lists .sidebar-menu li,#profile_favorites #side_lists .sidebar-menu li,#following #side_lists .sidebar-menu li,#followers #side_lists .sidebar-menu li{padding:3px 0 3px 14px;display:block;clear:both;overflow:hidden;width:172px;}#list_memberships .lists-links li a,#list_subscriptions .lists-links li a,.lists .lists-links li a,.lists_subscribers .lists-links li a,.lists_members .lists-links li a,#profile #side_lists .sidebar-menu li a,#profile_favorites #side_lists .sidebar-menu li a,#following #side_lists .sidebar-menu li a,#followers #side_lists .sidebar-menu li a{padding:0;display:inline;clear:both;}.lists .lists-links li a span,.lists_subscribers .lists-links li a span,.lists_members .lists-links li a span,#profile #side_lists .sidebar-menu li a span,#profile_favorites #side_lists .sidebar-menu li a span,#following #side_lists .sidebar-menu li a span,#followers #side_lists .sidebar-menu li a span{clear:both;width:auto!important;}.lists .lists-links li a:hover,.lists .lists-links li.active a,.lists_subscribers .lists-links li a:hover,.lists_subscribers .lists-links li.active a,.lists_members .lists-links li a:hover,.lists_members .lists-links li.active a,#profile #side_lists .sidebar-menu li a:hover,#profile #side_lists .sidebar-menu li.active a,#profile_favorites #side_lists .sidebar-menu li a:hover,#profile_favorites #side_lists .sidebar-menu li.active a,#following #side_lists .sidebar-menu li a:hover,#following #side_lists .sidebar-menu li.active a,#followers #side_lists .sidebar-menu li a:hover,#followers #side_lists .sidebar-menu li.active a{background:transparent!important;text-decoration:none;font-weight:normal;}.lists .lists-links li a:hover span,.lists_subscribers .lists-links li a:hover span,.lists_members .lists-links li a:hover span,#profile #side_lists .sidebar-menu li a:hover span,#profile_favorites #side_lists .sidebar-menu li a:hover span,#following #side_lists .sidebar-menu li a:hover span,#followers #side_lists .sidebar-menu li a:hover span{text-decoration:underline;}#side ul.lists-links li a span{width:150px;padding:2px 0 0 0;margin:0;}#side ul.lists-links li a b,#profile .sidebar-list li a b,#profile_favorites .sidebar-list li a b,#following .sidebar-list li a b,#followers .sidebar-list li a b{font-weight:normal;}#side ul.lists-links li a i,#profile .sidebar-list li a i,#profile_favorites .sidebar-list li a i,#following .sidebar-list li a i,#followers .sidebar-list li a i{font-style:normal;font-size:10px;margin-right:-3px;}#list_memberships span.view-all,#list_subscriptions span.view-all,#profile span.view-all,#profile_favorites span.view-all,#following span.view-all,#followers span.view-all{border-left:0;display:inline;padding-left:0;padding-right:7px;margin:0 5px 0 0;border-right:0;}#list_subscriptions span.last,#list_memberships span.last,#profile span.last,#profile_favorites span.last,#following span.last,#followers span.last{border-right:0!important;}#list_memberships p.sidebar-menu-actions,#list_subscriptions p.sidebar-menu-actions,#profile p.sidebar-menu-actions,#profile_favorites p.sidebar-menu-actions,#following p.sidebar-menu-actions,#followers p.sidebar-menu-actions{padding-top:2px;}#side ul.lists-links li a em{position:absolute;right:0;width:28px;height:13px;background:url(../images/arrow_right_dark.png) no-repeat left top;display:none;margin-top:1px;}.safari#list_subscriptions .lists-links li a,.safari#list_memberships .lists-links li a,.safari#list_show .lists-links li a,.safari#lists_subscribers .lists-links li a,.safari#lists_members .lists-links li a{padding-left:0!important;}#side ul.lists-links li a:hover em,#side ul.lists-links li.active a em{display:block;}#side ul.lists-links li a em:hover{background-position:0 -13px;}#side ul.lists-links li.loaddisableding a em{display:none;}ul.sidebar-list li.active a,ul.sidebar-list li a:hover{background-color:#DDEEF6;}#lists span.subscribed{background:#efefef;display:inline-block;font:11px Lucida Grande,arial,sans-serif;color:#333;padding:6px 8px;text-shadow:1px 1px 0 #fff;}#lists span.subscribed i{background-image:url(../images/sprite-icons.png);background-repeat:no-repeat;display:inline-block;background-position:-160px -16px;height:9px;margin-right:2px;width:10px;}#lists .profile-controls{display:block;clear:both;background:none;margin:0;padding:0;}.profile-controls li{text-align:left;margin:0!important;padding:0!important;}.profile-user .list-tags-outer{border:1px solid #eee;border-top:0;color:#ccc;background:#F6F6F6;font:11px "Lucida Grande",sans-serif;line-height:20px;margin:-18px 0 0 0;padding:6px 10px;-moz-border-radius:0 0 5px 5px;-webkit-border-bottom-right-radius:5px;-webkit-border-bottom-left-radius:5px;}.profile-user .list-tags-outer hr{color:#F6F6F6;background:#F6F6F6;border:0 solid #F6F6F6;border-top:1px solid #eee;border-bottom:1px solid #fff;height:0;margin:0 0 5px 0;display:block;}.profile-user .ie7 .list-tags-outer hr{display:none;}.profile-user .ie7 .list-tags *,.ie8 .list-tags *{background:none;}.list-tags{display:inline-block;}.profile-user .list-tags{margin-left:5px;}body.ie7#following .list-tags,body.ie7#followers .list-tags,body.ie7#lists_members .list-tags,body.ie7#lists_subscribers .list-tags{display:block;float:left;padding-top:3px;margin-left:53px;}body.ie7#profile .list-tags,body.ie7#profile_favorites .list-tags{margin-left:56px;}.list-tags-outer label{color:#666;}.list-tags-outer span.lock-icon{margin-left:3px;width:8px;}.list-tags li{display:inline-block;margin-right:2px;width:auto;}.ie7 .list-tags{margin-left:30px;margin-top:-20px;}.ie7 .list-tags li{float:left;width:auto;}.list-tags li a{display:block;}.list-tags li a i{background:none;padding:0;width:auto;height:auto;display:inline;margin-right:-3px;}.list-tags a{cursor:pointer;margin-right:5px;}.list-menu ul li{padding-left:5px!important;}.list-menu ul li label{padding:4px 2px!important;width:80%;cursor:pointer;overflow:hidden;}.list-menu ul li input[type="checkbox"]{margin:5px 0 0 5px;}#content .lists{margin-bottom:30px;}#list_show #side_base,#lists_members #side_base,#lists_subscribers #side_base{border-left-width:0;background-color:#fff;}#list_show .content-bubble-arrow,#lists_members .content-bubble-arrow,#lists_subscribers .content-bubble-arrow{background:none!important;}#list_show h3,#lists_members h3,#lists_subscribers h3{font-size:12px;font-weight:normal;padding-left:5px;margin-top:-8px;padding-bottom:2px;}h3 img{margin:-2px 0 0 2px;vertical-align:middle;}h3.heading{font:14px Helvetica Neue,Helvetica,sans-serif;padding-top:10px;padding-bottom:2px;}.list-controls{zoom:1;*position:relative;background-color:#ddeef6;-moz-border-radius:0 5px 0 0;-webkit-border-top-right-radius:5px;border-radius:0 5px 0 0;text-align:left;margin:-6px -1px 0 0;padding:20px 0 0 13px;height:57px;}body.lists .list{padding:0!important;}body.lists #timeline{padding-left:12px;}.list-controls .control-wrapper{float:left;width:135px;padding-left:5px;margin-top:-2px;}.list-controls a{float:left;}.list-controls #admin_list a{float:none;}.list-controls span.creator{padding:0 0 2px 0;margin:-2px 0 0 0;font-size:11px;line-height:15px;}.list-controls span.creator a{margin-right:3px;float:none;}.list-controls img{width:24px;height:24px;margin-top:4px;margin-right:8px;}#profile #timeline{margin-top:6px;}#profile_favorites #timeline{margin-top:0;}#list_show #primary_nav,#lists_members #primary_nav,#lists_subscribers #primary_nav{padding-top:20px;}.sidebar-list{width:90%;}#list_memberships .sidebar-list li,#list_subscriptions .sidebar-list li,#profile .sidebar-list li,#profile_favorites .sidebar-list li,#following .sidebar-list li,#followers .sidebar-list li{padding:3px 0 3px 0;}span.view-lists{clear:both;display:block;font-size:11px;padding:0 14px 5px 14px;}#list_memberships #friends_view_all small a,#list_subscriptions #friends_view_all small a,#list_show #friends_view_all small a{font-size:11px!important;color:#888;}#list_memberships #friends_view_all{margin-top:-3px!important;padding-bottom:14px!important;}#members .sidebar-menu div{margin:3px 0 0 16px;}.stat-count{position:absolute;right:30px;*right:10px;padding:.5em 0;font:bold 12px "Helvetica Neue",Helvetica,Arial,sans-serif;}#owners_lists p{padding:4px 0 0 14px!important;}#owners_lists h2 a{color:#333;}#owners_lists div.lists-links{padding:0 6px 0 14px;}#owners_lists span.view-all{display:block;margin-bottom:20px;}#owners_lists span.view-all a{color:#888;}#action_lists ul{padding:0 6px 12px 0;}form.button-to{padding:36px 0 20px 16px;}#list_memberships #side ul.sidebar-list,#list_subscriptions #side ul.sidebar-list,#profile #side ul.sidebar-list,#profile_favorites #side ul.sidebar-list,#following #side ul.sidebar-list,#followers #side ul.sidebar-list{margin:.2em 14px 6px;}.is-owner,.no-members{margin-left:10px!important;}.subscribe-list i{background-image:url(../images/sprite-icons.png);display:block;float:left;background-position:-96px -64px;margin:1px 5px 0 0;width:10px;height:13px;}.ie7 .subscribe-list{width:110px;}.is-subscriber i,.is-owner i{display:inline-block;width:10px;height:9px;margin-right:6px;background-image:url(../images/sprite-icons.png);background-repeat:no-repeat;overflow:hidden;outline:none;background-position:-160px -16px;}.is-owner,.is-subscriber,.subscriber .is-non-subscriber,.no-subscribe .is-non-subscriber,.owner .is-non-subscriber{display:none;margin-top:20px!important;}.owner .is-owner{display:block;}.subscriber .is-subscriber{display:block;padding:6px 0 4px 0;}a.unsubscribe-list,span.actions-list{position:absolute;right:0;}a.edit-list{padding-right:6px;border-right:1px solid #C0DEED;margin-right:6px;}span.actions-list{color:#ccc;}#admin_list{font-size:11px;}#admin_list ul li{padding-top:2px;}.no-members,.no-members-list{background:url(../images/thumb-bird-bw.gif) no-repeat 0 14px;min-height:63px;font-size:13px;border-top:1px solid #eee;border-bottom:1px solid #eee;padding:14px 0 0 60px;line-height:16px!important;color:#666;clear:both;float:none;font-weight:normal;}.no-members-list{margin-left:4px!important;}.lists td.user-detail,.lists_members td.user-detail,.lists_subscribers td.user-detail{padding-left:0!important;}.list-header-section{padding:0!important;}#list_memberships #content h2,#list_subscriptions #content h2{margin-bottom:20px;}#list_show h2.sidebar-title,#lists_members h2.sidebar-title,#lists_subscribers h2.sidebar-title{clear:both;display:block;padding:16px 6px 4px 14px!important;}#action_lists h2.sidebar-title{padding-top:0!important;margin-top:0!important;}#list_show ul.sidebar-menu li a,#lists_members ul.sidebar-menu li a,#lists_subscribers ul.sidebar-menu li a{-moz-border-radius:5px;-webkit-border-radius:5px;}#list_show ul.sidebar-menu li,#lists_members ul.sidebar-menu li,#lists_subscribers ul.sidebar-menu li{*height:23px!important;*position:relative;}.lists .user-detail{width:390px!important;}.lists table.users-lists{width:100%;border-collapse:collapse;}.lists table.users-lists thead{color:#999;font-family:Lucida Grande,Helvetica,Arial,sans-serif;}.lists table.users-lists td.count{color:#999;font-size:11px;padding-top:7px;width:100px;}.lists table.users-lists td{border-bottom:1px solid #eee;padding:6px;font-size:14px;}#lists_table .list-info a{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;}.lists table.users-lists td a img{margin-right:8px;}.lists table.users-lists thead td{font-size:10px;}.lists table.users-lists td a span i{margin-right:-4px;}#list_tabs{clear:both;display:block;border-bottom:1px solid #CECECE;}#list_show ul.lists-links,#lists_members ul.lists-links,#lists_subscribers ul.lists-links{margin-bottom:0!important;}#list_show ul.lists-links li a:hover,#lists_members ul.lists-links li a:hover,#lists_subscribers ul.lists-links li a:hover{text-decoration:underline;}p.list-description{color:#666;display:block;padding:0 0 0 10px;font-weight:300;font:16px Helvetica Neue,Helvetica,Arial,sans-serif;}p.list-feedback{color:#666;border-top:1px solid #EEE;font-size:11px;margin:0 20px 26px 14px;padding-top:8px!important;}.ie7 .user-settings .setting{float:left;}.create-list-dialog .update-list-heading,.update-list-dialog .create-list-heading,.create-list-dialog .update-list-button,.update-list-dialog .create-list-button{display:none;}#lists_members #follow_grid table,#lists_subscribers #follow_grid table{margin-top:0;border-top:0!important;}#lists_members #follow_grid,#lists_subscribers #follow_grid{margin:0 10px 0 5px;}.no-members h3{font-family:Helvetica Neue,Helvetica,Arial,sans-serif!important;margin-top:-2px!important;margin-bottom:0!important;font-size:16px!important;font-weight:bold!important;padding-left:0!important;}.no-members p.instruction{padding-top:6px!important;}.no-members p.tip{margin:-5px 0 12px 0!important;font-size:10px;line-height:13px;}.firefox .no-members p.instruction{margin-bottom:0!important;}#lists_subscribers p.no-members-list,#lists_members div.no-members{margin-top:5px!important;margin-left:5px!important;}#list_show.firefox #side_base span.vcard,#lists_members.firefox #side_base span.vcard,#lists_subscribers.firefox #side_base span.vcard{line-height:1.3em;}#list_show.safari #side_base span.vcard,#lists_members.safari #side_base span.vcard,#lists_subscribers.safari #side_base span.vcard{line-height:1.4em;}#lists_members #follow_grid td.thumb,#lists_subscribers #follow_grid td.thumb{width:13%!important;}#lists_members #follow_grid.follow-compact .thumb,#lists_subscribers #follow_grid.follow-compact .thumb{width:8%!important;}.list-description-call{float:left;}.list-description-fieldset{margin-bottom:0!important;}#lists_members .is-non-subscriber,#lists_members .is-subscriber,#lists_subscribers .is-non-subscriber,#lists_subscribers .is-subscriber{margin:-6px 0 25px 5px!important;}#lists_subscribers a.unsubscribe-list,#lists_members a.unsubscribe-list{margin-right:5px;}#list_show #members,#lists_subscribers #members,#lists_members #members{margin-bottom:4px;}.list-dialog .modal-content input.title,.list-dialog .modal-content textarea.title{-webkit-border-radius:2px;-moz-border-radius:2px;margin-bottom:0;}.list-url{font-weight:bold;color:blue;}.list-dialog .modal-content textarea.title{margin-top:4px;font:13px Lucida Grande,Arial,sans-serif;height:36px;}#list_tabs ul.tabMenu li a span.count,#list_tabs ul.tabMenu li.active a span.count{background:#fff;margin-left:2px;padding:1px 5px;font:11px Helvetica Neue,Helvetica,sans-serif!important;-moz-border-radius:8px;-webkit-border-radius:8px;text-align:center;}#list_tabs ul.tabMenu li.active a span.count{background:#eee;color:#333;}#password_reset #content,#password_reset_confirmation #content,#password_reset_sent #content{width:auto;}form#reset-pw{padding:1em;}form #instructions-form{background:#f5f5f5;-moz-border-radius:5px;-webkit-border-radius:5px;border:1px solid #f5f5f5;border-top-color:#e7e7e7;padding:2em;margin-top:1em;position:relative;}#unlock-bird{float:left;width:150px;}#reset-input{margin-left:150px;}.verify-phone{margin-top:1em;padding:1em;-moz-border-radius:5px;border:1px solid #e7e7e7;background:#e2fdd5;}.verify-phone input{margin-left:-2px;}#instructions-form .hint{opacity:.7;filter:alpha(opacity=70);font-size:90%;}#instructions-form h4{font-weight:normal;font-size:185%;letter-spacing:-0.5px;color:#555;}#instructions-form fieldset em{display:block;font-style:normal;}#instructions-form #keep-void{margin-top:2em;color:#666;}label.new-password{float:left;width:150px;text-align:right;padding:10px 0 0 0;}#instructions-form div.hr{height:1px;background:#ddd;border-top:1px solid #fff;width:95%;margin:15px auto;}#instructions-form img#reset-bird-reverse{position:absolute;top:-50px;right:-10px;}#instructions-form p.special-note{color:#666;font-size:90%;margin-left:150px;}#instructions-form p.special-note strong{display:block;}.western #tagline{margin:8px 119px 0 0;width:355px;}.western #signin_menu{width:240px;}.western #signin_menu input[type="text"],.western #signin_menu input[type="password"]{width:230px;}.fr #big_signup{width:220px;}.western #footer{font-size:.8em;}.western #signin_menu{width:240px;}.western #signin_menu input[type="text"],.western #signin_menu input[type="password"]{width:230px;}.western .newuser h2{font-size:16px;}.western #signin_submit{margin:0;}.western #signup-form tr.captcha th{font-size:14px;}.western #signup-form #recaptcha_controls{height:auto;}body.western .home_page_control input.profilesubmit{width:185px;}body.fr .home_page_control input.profilesubmit{width:390px;}body.fr .home_page_control div#profiletext{float:none;width:100%;}body.fr .home_page_control div#profilebutton{float:none;margin:20px 0 0 0;}body.western #side .stats td .label{text-transform:none;}body.fr #side .stats a span.stats_count{font-size:12px;text-align:center;}body.western #settings_nav li a{font-size:.95em;}body.western #content .tabMenu li a{font-size:11px!important;padding:6px 10px 5px 8px;}body#profile_settings.western #content .tabMenu li a,body#profile_settings.western #content .tabMenu li{font-size:10px!important;}body.asian #content .tabMenu li a{font-size:12px!important;}body.asian #tweeting_button{width:60px;}#notices.western label{white-space:normal;display:block;}#notices.asian label{white-space:normal;display:block;}#password.western #nomatch{display:block;}body#settings.western #username_sample_url{display:block;}body#settings.western #username_msg{display:block;margin:-1em 0 0 0;}body#settings.fr #geotagging_info_link{font-size:10px;}body#password.fr .common-form th{text-align:right;padding:14px 0;}#invitations.western #service-credentials table th{font-size:14px;width:320px;}body.western .lists table.users-lists td.count{width:120px;white-space:nowrap;}body.western #lists_table tr td{white-space:nowrap;}body.western .col-tabset{width:145px;}body.western #sw-core{height:auto;min-height:400px;}body.western #sw-ui .t-unit,body.western #sw-dimensions .t-unit{float:none;display:block;width:100%;}body.western #sw-widget-behavior-default,body.western #sw-widget-behavior-all{margin:4px 0;}body.western #search-widget h3{margin:6px 0 4px;}body.de .buttons-page table tr td div.embed{height:123px;}body.fr .buttons-page table tr td div.embed{height:110px;}body.fr #built h3{white-space:nowrap;}#oauth_clients.it #content{background-position:right bottom;}body.western #recaptcha_controls,body.western #recaptcha_data,body.western #recaptcha_widget{height:12em;}.ja .dialog-form li label small{font-size:10px;}.btn,input[type=submit].btn,input[type=button].btn{background:#ddd url(../images/buttons/bg-btn.gif) repeat-x 0 0;font:11px/14px "Lucida Grande",sans-serif;width:auto;margin:0;overflow:visible;padding:4px 8px 5px;border-width:1px;border-style:solid;border-color:#ddd;border-bottom-color:#ccc;-moz-border-radius:4px;-khtml-border-radius:4px;-webkit-border-radius:4px;border-radius:4px;color:#333;text-shadow:1px 1px 0 #fff;cursor:pointer;}.btn::-moz-focus-inner{padding:0;border:0;}.btn-m,input[type=submit].btn-m,input[type=button].btn-m{background-position:0 -200px;font-size:15px;line-height:20px!important;padding:5px 15px 6px;-moz-border-radius:5px;-khtml-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;}.btn-l,input[type=submit].btn-l,input[type=button].btn-l{background-position:0 -400px;font-size:20px;line-height:26px;padding:7px 20px 9px;-moz-border-radius:6px;-khtml-border-radius:6px;-webkit-border-radius:6px;border-radius:6px;}.btn-light{background-color:#add!important;background-image:url(http://a2.twimg.com/a/1302214109/images/buttons/bg-btn-light.gif)!important;border-color:#add #add #9cc!important;text-shadow:1px 1px 0 #dff!important;}.btn-dark{background-color:#59a!important;background-image:url(../images/buttons/bg-btn-dark.gif)!important;border-color:#59a #59a #489!important;color:#fff!important;text-shadow:-1px -1px 0 #59a!important;}.btn-blue{background-color:#39d!important;background-image:url(../images/buttons/bg-btn-blue.gif)!important;border-color:#39d #39d #28c!important;color:#fff!important;text-shadow:-1px -1px 0 #39d!important;}.btn-chart{background-color:#9c2!important;background-image:url(../images/buttons/bg-btn-chart.gif)!important;border-color:#9c2 #9c2 #8b1!important;text-shadow:1px 1px 0 #df6!important;}.btn-mint{background-color:#bdb!important;background-image:url(../images/buttons/bg-btn-mint.gif)!important;border-color:#bdb #bdb #aca!important;text-shadow:1px 1px 0 #efe!important;}.btn-green{background-color:#272!important;background-image:url(../images/buttons/bg-btn-green.gif)!important;border-color:#272 #272 #161!important;color:#fff!important;text-shadow:-1px -1px 0 #272!important;}.btn-pink{background-color:#daa!important;background-image:url(../images/buttons/bg-btn-pink.gif)!important;border-color:#daa #daa #c99!important;text-shadow:1px 1px 0 #fdd!important;}.btn-red{background-color:#a22!important;background-image:url(../images/buttons/bg-btn-red.gif)!important;background-position:0 0;border-color:#a22 #a22 #911!important;text-shadow:-1px -1px 0 #a22!important;color:#fff!important;}.btn-yellow{background-color:#fa2!important;background-image:url(../images/buttons/bg-btn-yellow.gif)!important;border-color:#fa2!important;color:#333!important;color:rgba(0,0,0,.75)!important;text-shadow:0 1px 1px rgba(255,255,255,.5)!important;}.btn:hover,.btn:focus,input[type=submit].btn:hover,input[type=submit].btn:focus,button.btn:hover,button.btn:focus{border-color:#999 #999 #888;background-position:0 -6px;color:#000;text-decoration:none;}.btn-light:hover,.btn-light:focus{border-color:#7aa #7aa #699;}.btn-dark:hover,.btn-dark:focus{border-color:#267 #267 #156;color:#fff;}.btn-blue:hover,.btn-blue:focus{border-color:#17b #17b #06a;color:#fff;}.btn-chart:hover,.btn-chart:focus{border-color:#7a1 #7a1 #690;}.btn-mint:hover,.btn-mint:focus,input[type=submit].btn-mint:hover,input[type=submit].btn-mint:focus,button.btn-mint:hover,button.btn-mint:focus{border-color:#8a8 #8a8 #797!important;}.btn-green:hover,.btn-green:focus,input[type=submit].btn-green:hover,input[type=submit].btn-green:focus,button.btn-green:hover,button.btn-green:focus{background-color:#272!important;border-color:#050 #050 #040!important;color:#fff;}.btn-pink:hover,.btn-pink:focus,input[type=submit].btn-pink:hover,input[type=submit].btn-pink:focus,button.btn-pink:hover,button.btn-pink:focus{border-color:#a88 #a88 #977!important;}.btn-red:hover,.btn-red:focus,input[type=submit].btn-red:hover,input[type=submit].btn-red:focus,button.btn-red:hover,button.btn-red:focus{background-color:#a22!important;border-color:#611 #611 #500!important;color:#fff;}.btn-yellow:hover,.btn-yellow:focus,input[type=submit].btn-yellow:hover,input[type=submit].btn-yellow:focus,button.btn-yellow:hover,button.btn-yellow:focus{background-color:#fa2!important;border-color:#fa2!important;color:rgba(0,0,0,.75)!important;text-shadow:0 1px 1px rgba(255,255,255,.25)!important;}.btn-m:hover,.btn-m:focus,input[type=submit].btn-m:hover,input[type=submit].btn-m:focus,button.btn-m:hover,button.btn-m:focus{background-position:0 -206px;}.btn-l:hover,.btn-l:focus,input[type=submit].btn-l:hover,input[type=submit].btn-l:focus,button.btn-l:hover,button.btn-l:focus{background-position:0 -406px;}button:active,button.btn:active,input[type=submit]:active,button.btn-dark:active,button.btn-light:active,.btn:active,.btn-red:active,.btn-green:active{background-image:none!important;text-shadow:none!important;outline:none!important;}.dbtn,.dbtn:hover,.dbtn:focus,.dbtn:active,button.dbtn:hover,button.dbtn:focus{background:#eee;border-color:#ddd;color:#aaa;text-shadow:none;}.btn-light.dbtn,.btn-light.dbtn:hover,.btn-light.dbtn:focus,.btn-light.dbtn:active{background:#dee;border-color:#cdd;color:#9aa;}.btn-dark.dbtn,.btn-dark.dbtn:hover,.btn-dark.dbtn:focus,.btn-dark.dbtn:active{background:#aad5dd;border-color:#99c5cc;color:#ddf6f6;}.btn-blue.dbtn,.btn-blue.dbtn:hover,.btn-blue.dbtn:focus,.btn-blue.dbtn:active{background:#bde;border-color:#acd;color:#def;}.btn-chart.dbtn,.btn-chart.dbtn:hover,.btn-chart.dbtn:focus,.btn-chart.dbtn:active{background:#deb;border-color:#cda;color:#ab9;}.btn-mint.dbtn,.btn-mint.dbtn:hover,.btn-mint.dbtn:focus,.btn-mint.dbtn:active{background:#ded;border-color:#cdc;color:#9a9;}.btn-green.dbtn,.btn-green.dbtn:hover,.btn-green.dbtn:focus,.btn-green.dbtn:active{background:#aca;border-color:#9b9;color:#ded;}.btn-pink.dbtn,.btn-pink.dbtn:hover,.btn-pink.dbtn:focus,.btn-pink.dbtn:active{background:#edd;border-color:#dcc;color:#a99;}.btn-red.dbtn,.btn-red.dbtn:hover,.btn-red.dbtn:focus,.btn-red.dbtn:active{background:#caa;border-color:#b99;color:#edd;}.btn.right{float:right;}.geo_new{color:#C00;}.geo_progress{color:#999;}.crosshairs{display:inline-block;background:url(../images/sprite-icons.png) -64px -80px no-repeat;height:11px;width:11px;margin:0 4px 0 0;vertical-align:middle;}a.geo_disable_webclient span{display:inline-block;background:url(../images/sprite-icons.png) no-repeat -112px -80px;height:7px;width:7px;margin:0 3px;vertical-align:middle;}a:hover.geo_disable_webclient span{background-position:-128px -80px;}.near{color:#8c8c8c;font-size:14px;}a.places-nearby{position:absolute;left:385px;top:148px;}.geo_notifications{display:none;}#place_link:focus{outline:none;}#place_link span.place_icon{display:inline-block;background:url(../images/sprite-icons.png) no-repeat -240px -64px;height:11px;width:7px;vertical-align:middle;margin-right:4px;}#geo_browser_help_banner{color:#FFF;font:12px Verdana;position:fixed;right:0;text-align:left;top:0;z-index:10000;}#geo_browser_help_banner.geo_firefox{background:#333 url(../images/geo_firefox_help_banner_back.png) no-repeat right;-moz-border-radius-bottomleft:4px;height:108px;}#geo_browser_help_banner.geo_chrome{background:#333 url(../images/geo_chrome_help_banner_back.png) no-repeat right;-webkit-border-radius-bottomleft:4px;height:65px;}#geo_browser_help_banner.geo_ie_gtb{background:#333 url(../images/geo_ie_gtb_help_banner_back.png) no-repeat right;height:108px;}#geo_browser_help_banner.geo_firefox>div{margin:8px 183px -3px 10px;}#geo_browser_help_banner.geo_chrome>div{margin:25px 120px 20px 20px;}#geo_browser_help_banner.geo_ie_gtb>div{margin:8px 200px -3px 10px;}#geo_browser_help_banner img{margin-right:6px;position:relative;top:8px;}ul.places_list{background-color:#FFF;border:1px solid #AAA;padding:4px 0 4px 0;text-align:left;}#place_content ul.places_list li,ul.places_list li{color:#333;padding:3px 8px 3px 4px;cursor:pointer;}.geo_more_places{border-top:1px solid #ccc;padding-top:5px;margin-top:4px;}#place_content ul.places_list li:hover,#place_content ul.places_list a:hover{color:white;background-color:#666;outline:none;}li .place_item_icon{background:transparent;display:inline-block;height:9px;margin:0 4px 2px 0;vertical-align:middle;width:10px;}li.selected .place_item_icon{background:url(../images/sprite-icons.png) no-repeat -160px -16px;}li .refresh{background:url(../images/sprite-icons.png) no-repeat -96px -80px;width:7px;margin:0 5px 2px 2px;}ul.places_list li:hover .refresh{background:url(../images/sprite-icons.png) no-repeat -80px -80px;}li .clear{display:inline-block;background:url(../images/sprite-icons.png) no-repeat -128px -80px;height:7px;width:7px;margin:0 5px 0 2px;vertical-align:middle;}ul.places_list li:hover .clear{background:url(../images/sprite-icons.png) no-repeat -112px -80px;}li .place_icon{display:inline-block;background:url(../images/sprite-icons.png) no-repeat -224px -64px;height:11px;width:7px;margin-right:4px;vertical-align:middle;}li .more_places{background:transparent;}li .place_details{color:#999;}#geo-promo-hoverer{width:420px;font-size:11px;text-align:left;visibility:hidden;}#geo-promo-hoverer .hoverer-inner{padding:15px;}#geo-promo-hoverer .hovercard-divot{left:40px;top:-11px;}#geo_modal.position_above .hovercard-divot{bottom:-11px;}#geo_modal.position_below .hovercard-divot{top:-11px;}#geo-promo-hoverer .tiny-map{float:right;padding:0 0 0 20px;}#geo_dialog_descr{margin:10px 0 10px 0;font-size:13px;}#geo_not_now{position:relative;top:5px;margin-left:8px;}#geo_turn_location_on{font-weight:bold;}a.geo_disable_webclient{color:#999;font-family:tahoma,sans-serif;font-size:12px;font-weight:bold;line-height:12px;text-shadow:1px 1px 1px #FFF;}a:hover.geo_disable_webclient{text-decoration:none;}.geo-pin{background:transparent url(../images/sprite-icons.png) no-repeat scroll -224px -64px;display:inline-block;height:11px;line-height:1.1em;width:7px;}.geo_map_with_place{width:490px;}#map_canvas{width:270px;height:170px;float:left;margin:1px;}.map_close{color:#999;text-decoration:none;-moz-border-radius:2px;background-color:#ddd;display:block;font-size:15px;margin:-2px;padding:0 4px 2px;position:absolute;right:0;top:0;text-decoration:none;}.map_close:hover{text-decoration:none;}.geo_map_place_details{width:195px;margin:10px;float:left;color:#333;}.geo_map_place_name{font-weight:bold;font-size:13px;margin-bottom:4px;}.geo_map_place_tweets{margin-top:5px;}.geo_map_place_tweets a{color:#2276bb;}#geo_map_progress.position_above .hovercard-divot{bottom:-11px;}#geo_map_progress.position_below .hovercard-divot{top:-11px;}#geo_map_progress .hoverer-inner{width:55px;}#geo_map_fail{display:none;}#geo_map_spinner{background:url(../images/spinner.gif) no-repeat;margin:10px 20px;}.place_search_dialog .hanging{width:450px;}.geo_place_search_table{font-family:'Lucida Grande',sans-serif;font-size:13px;}.geo_place_search_col1{font-weight:bold;text-align:right;padding-right:7px;padding-left:0;}.geo_place_search_city{padding-bottom:14px;padding-left:7px;}#geo_city{margin:0 0 16px 8px;}#geo_poi_hint{font-family:'Lucida Grande',sans-serif;font-size:11px;color:#999;padding:4px 0 8px 7px;}#place_search_results{padding:5px 0 0 7px;display:none;width:310px;}#place_search_done,#place_search_cancel{margin-top:20px;margin-bottom:5px;}#place_search_form input{border:1px solid #aaa!important;font-size:1em;outline:none;padding:5px;width:282px;vertical-align:middle;}#place_search_form #city_search_query{width:336px;}#place_search_form input:focus{outline:none;border-color:rgba(82,168,236,.75)!important;box-shadow:0 0 8px rgba(82,168,236,.5);-moz-box-shadow:0 0 8px rgba(82,168,236,.5);-webkit-box-shadow:0 0 8px rgba(82,168,236,.5);}.place_search_submit{-moz-border-radius-bottomright:3px;-moz-border-radius-topright:3px;border-style:solid;border-width:1px;margin-left:-1px;cursor:pointer;padding:.4em .9em;border-color:#999!important;padding-bottom:5px!important;padding-top:5px!important;vertical-align:middle;background:url(../images/nav_search_submit.png) repeat scroll -2px 0 transparent!important;}.place_search_submit:hover{background:url(../images/nav_search_submit.png) -2px -25px!important;}.place_search_submit:active{background:url(../images/nav_search_submit.png) -2px -50px!important;}.place_search_submit.loaddisableding,.place_search_submit.loaddisableding:hover,.place_search_submit.loaddisableding:active{background:#eee url(../images/spinner.gif) no-repeat 5px 5px!important;}#place_search_results li{margin:10px 0 0 0;list-style-type:none;white-space:nowrap;overflow:hidden;}#place_search_results .place_noicon,ul.place_search_dropdown.places_list .place_noicon{display:inline-block;margin-left:15px;}.wait{cursor:wait;}ul.place_search_dropdown.places_list li{padding-left:8px;white-space:nowrap;}.places_list li.hover{color:white;background-color:#666;outline:none;}ul.places_list{display:none;position:absolute;background-color:#FFF;border:1px solid #AAA;padding:4px 0 4px 0;text-align:left;z-index:9999;}#place_search_go_back{margin-top:12px;}#place_search_go_back,#change_city{font-weight:normal;color:#4d94be;}.geo_place_search_hint{padding:4px 0 0 7px;font-size:11px;color:#999;}div.geo_add_place{margin-top:20px;}div.geo_add_place a{font-weight:bold;}.geo_search_message{margin-top:12px;}.geo_next_prev{margin-top:12px;}#geo_prev_result{margin-right:20px;}.place_creation_dialog .hanging{width:650px;}.place_creation_dialog .modal-inner h2{margin:0!important;}.place_creation_dialog .modal-content{padding:0;}.place_creation_dialog .geo_map_canvas{width:312px;}.geo_place_search_table{font-family:'Lucida Grande',sans-serif;font-size:13px;width:100%;}.geo_place_creation_hint{padding:8px 0 0 7px;font-size:11px;color:#999;}.geo_form_input{border:1px solid #aaa!important;font-size:1em;outline:none;padding:5px;width:210px;vertical-align:middle;}.geo_form_input:focus{outline:none;border-color:rgba(82,168,236,.75)!important;box-shadow:0 0 8px rgba(82,168,236,.5);-moz-box-shadow:0 0 8px rgba(82,168,236,.5);-webkit-box-shadow:0 0 8px rgba(82,168,236,.5);}.geo_place_creation_row2{padding-top:15px;}.geo_place_city{margin:12px 0 15px;}#geo_creation_error{margin-top:8px;font-size:11px;}.geo_spinner{display:inline-block;background:url(../images/spinner.gif);height:14px;width:14px;margin-left:15px;line-height:1.9em;vertical-align:middle;}.geo_map{float:right;}.geo_place_create{padding:25px;width:280px;}.geo_place_create ul{margin:18px 0 20px 0;}.geo_place_create li{margin:10px 0;white-space:nowrap;overflow:hidden;}.geo_map_hint{opacity:0;width:160px;position:absolute;z-index:20;text-align:center;}.geo_map_hint span{display:inline-block;vertical-align:bottom;background-image:url(../images/geo_creation_hint_arrow.gif);background-repeat:no-repeat;width:21px;height:11px;}.geo_map_hint div{background-color:#424242;color:white;text-align:left;padding:10px;font-size:11px;font-weight:bold;}.geo_map_place_bubble{opacity:0;display:none;position:absolute;z-index:20;text-align:center;margin-top:10px;white-space:nowrap;}.geo_map_place_bubble span{display:inline-block;vertical-align:bottom;background-image:url(../images/geo_creation_hint_arrow.gif);background-repeat:no-repeat;width:21px;height:11px;}.geo_map_place_bubble>div{background-color:#424242;color:white;text-align:left;padding:10px;font-size:11px;font-weight:bold;}.geo_go_back{line-height:1.9em;margin:0 10px;}.geo_place_details{color:#aaa;}.geo_map_link_separator{margin:0 5px 0 10px;color:#aaa;}.button{-moz-border-radius:4px;-khtml-border-radius:4px;-webkit-border-radius:4px;background:#ddd url(../images/buttons/bg-btn.gif) repeat-x 0 0;border-bottom-color:#ccc;border-color:#ddd;border-radius:4px;border-style:solid;border-width:1px;color:#333;cursor:pointer;display:inline;font:11px/14px "Lucida Grande",Sans-serif;margin:0;overflow:visible;padding:4px 8px 5px;text-shadow:1px 1px 0 #fff;}.button::-moz-focus-inner{padding:0;border:0;}.button:focus{outline:none;}.button:hover,.button:focus{background-position:0 -6px;border-color:#999 #999 #888;color:#000;}.button:active{background-image:none;text-shadow:none;outline:none;}#tweeting_controls a{line-height:13px;}#gear_dropdown{padding:4px 5px 5px;}#gear_dropdown span{background-image:url(../images/sprite-icons.png);background-position:-32px -63px;background-repeat:no-repeat;display:inline-block;width:22px;}ul.dropdown{display:none;position:absolute;width:200px;padding:4px 0;text-align:left;border:1px solid #666;background-color:#fff;z-index:9999;}ul.dropdown li a,ul.dropdown li label,ul.dropdown li input[type="checkbox"]{display:inline-block;font:11px 'Lucida Grande',Arial,sans-serif;color:#666;position:relative;padding:4px 5px;vertical-align:top;}ul.dropdown li .loaddisableding-spinner{display:inline-block;position:relative;top:4px;left:1px;margin-left:4px;}ul.dropdown li a{padding:4px 5px 4px 27px;}ul.dropdown li a{display:block;color:#666;text-decoration:none;}ul.dropdown li:hover{color:#fff;background-color:#666;}ul.dropdown li:hover *{color:#fff;}ul.dropdown li.divider{border-top:1px solid #ddd;}ul.dropdown i{background:url(../images/sprite-icons.png) repeat no-repeat;font-size:10px;left:7px;position:absolute;top:4px;width:15px;}#get_location_icon{background:url(../images/sprite-icons.png) -160px -64px no-repeat;display:inline-block;_display:inline;height:11px;width:11px;vertical-align:middle;}#location_spinner{display:none;height:11px;width:11px;vertical-align:middle;}.share-location-loaddisableding #location_spinner{display:inline-block;}.share-location-loaddisableding #get_location_icon{display:none;}a.a-btn{zoom:1;background:#ddd url(../images/buttons/bg-btn.gif) repeat-x scroll 0 0;cursor:pointer;text-shadow:1px 1px 0 #fff!important;border-color:#ddd #ddd #ccc!important;border-style:solid;border-width:1px!important;text-decoration:none;padding:4px 8px 5px;line-height:14px;font-size:11px;font-family:"lucida grande",helvetica,tahoma,arial;display:inline-block;_display:inline;-moz-border-radius:4px;-webkit-border-radius:4px;border-radius:4px;}a.a-btn,a.a-btn:visited{color:#333!important;}a.a-btn:hover,a.a-btn:focus{text-decoration:none;border-color:#999 #999 #888!important;color:#000;outline:none;}a.a-btn:active{background-image:none;outline:none;}:focus{-moz-outline-style:none;}a.a-btn-m{font-size:15px;font-family:"helvetica neue",arial,sans-serif;padding:5px 15px 6px;line-height:20px;-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;background-position:0 -200px;}a.a-btn-l{font-size:20px;line-height:26px;padding:7px 20px 8px;-moz-border-radius:6px;-webkit-border-radius:6px;border-radius:6px;font-family:"helvetica neue",arial,sans-serif;background-position:0 -400px;}a.btn-disabled{opacity:.6;filter:alpha(opacity=60);background-image:none;}.twitter-connect{border:0;outline:none;text-indent:-99999px;display:inline-block;background-repeat:no-repeat;background-position:top left;}.twitter-button{font:12px Arial,sans-serif;color:#fff;background:#1D6B9C url(../images/oauth2/gradient-background.png) repeat-x;text-indent:0;border:1px solid #18566A;display:inline-block;-moz-border-radius:4px;-webkit-border-radius:4px;-border-radius:4px;text-shadow:0 -1px 0 #18566A;}.twitter-button:hover{border:1px solid #00242C;background-position:left -23px;text-decoration:none;}.twitter-button:active{border:1px solid #044D77;background-position:left -46px;text-decoration:none;color:rgba(255,255,255,0.8);}.twitter-connect-small{background:url(../images/oauth2/connect_129px.png) no-repeat;width:129px;height:19px;}.twitter-connect-small:hover{background-position:left -19px;}.twitter-connect-small:active{background-position:left -38px;}.twitter-connect-medium{background:url(../images/oauth2/connect_146px.png) no-repeat;width:146px;height:23px;}.twitter-connect-medium:hover{background-position:left -23px;}.twitter-connect-medium:active{background-position:left -46px;}.twitter-connect-large{background:url(../images/oauth2/connect_170px.png) no-repeat;width:170px;height:26px;}.twitter-connect-large:hover{background-position:left -26px;}.twitter-connect-large:active{background-position:left -52px;}.twitter-connect-xlarge{background:url(../images/oauth2/connect_236px.png) no-repeat;width:236px;height:38px;}.twitter-connect-xlarge:hover{background-position:left -38px;}.twitter-connect-xlarge:active{background-position:left -76px;}.twitter-connect-box{font:13px/17px Lucida Grande,"Lucida Grande",Arial,Helvetica,sans-serif;padding:8px 10px 9px 10px;width:200px;background:#C7E0EC url(../images/oauth2/rays-box.jpg) no-repeat center top;color:#001F33;text-shadow:0 1px 0 #E5F0F6;border-radius:5px;-moz-border-radius:4px;-webkit-border-radius:4px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,.3);-moz-box-shadow:0 1px 0 rgba(0,0,0,.3);box-shadow:0 1px 0 rgba(0,0,0,.3);display:inline-block;vertical-align:top;}.twitter-connect-box p{margin:0 0 8px 0;padding:0;}.twitter-connect-box-small{font-size:10px;line-height:14px;width:129px;}.twitter-connect-box-medium{font-size:11px;line-height:15px;width:146px;}.twitter-connect-box-large{font-size:11px;line-height:15px;width:170px;}.twitter-connect-box-xlarge{font-size:12px;line-height:17px;width:236px;}.follow-medium{text-decoration:none;padding-right:7px;padding-left:2px;*padding:0 7px 0 0;}.follow-medium i{height:23px;width:23px;display:inline-block;border-right:1px solid #73AFD5;}.follow-medium i b{display:inline-block;background:url(../images/oauth2/t_170px.png) no-repeat 3px 3px;height:23px;width:22px;vertical-align:middle;border-right:1px solid #094B60;}.follow-medium .status{padding-left:4px;}.following-notice,.pending-notice{background-image:none;background:#eee;border:1px solid #ccc;color:#333;text-shadow:0 1px 0 #fff;cursor:default;padding:1px 8px 0;font:12px Arial,sans-serif;text-indent:0;display:inline-block;-moz-border-radius:4px;-webkit-border-radius:4px;-border-radius:4px;}.pending-notice{padding:5px 8px 2px;}.following-notice:active,.pending-notice:active{color:#333;text-shadow:0 1px 0 #fff;}.following-notice span.at,.pending-notice span.at{color:#666;}.following-notice a,.pending-notice a{color:#196698;font-weight:normal;text-decoration:none;}.following-notice a:hover,.pending-notice a:hover{text-decoration:underline;}.following-notice i{border-right:1px solid #eee;width:15px;}.pending-notice i{border-right:1px solid #eee;width:10px;height:9px;}.following-notice i b{border-right:0;width:15px;}.pending-notice i{border-right:0;width:9px;height:17px;}.following-notice i b{background:url(../images/oauth2/check.png) no-repeat 4px 7px;}.pending-notice i b{position:relative;top:-2px;border-right:none;width:10px;height:9px;background:url(../images/sprite-icons.png) no-repeat -192px -16px;}.twitter-loaddisableding{font:12px/15px Arial,Helvetica,sans-serif;color:#fff;background:#eee;border:1px solid #ccc;color:#333;text-shadow:0 1px 0 #fff;cursor:default;text-indent:0;padding:5px 8px 4px 8px;-moz-border-radius:4px;-webkit-border-radius:4px;-border-radius:4px;display:block;width:100px;margin-top:-3px;}fieldset.common-form ul.discover-buttons li{padding:15px 0 5px;margin-bottom:0;}.dialog-form fieldset.common-form input[type="text"],.dialog-form fieldset.common-form input[type="password"],.dialog-form fieldset.common-form textarea{border:1px solid #888;-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;font-size:13px;background:#fff;padding:4px 5px;}#discoverability_header,#discoverability_dialog,#discoverability_footer{display:none;}.dialog-form li .input-wrapper{display:inline-block;vertical-align:bottom;}.dialog-form li{margin-bottom:3px;}.dialog-form li label small{font-weight:normal;}.dialog-form fieldset.common-form ul li label{display:inline-block;font-size:13px;line-height:18px!important;padding:0 10px 0 0;width:95px;margin-top:3px;vertical-align:top;white-space:normal;}.dialog-form li input[type=text]{line-height:20px;width:250px;}.dialog-form li textarea{width:250px;height:50px;}.dialog-form li strong{display:block;font-size:13px;margin:3px 0 4px;}.dialog-form li,.dialog-form p{color:#555;line-height:18px;}.discoverability-dialog span.privacy-statement{color:#555;font:12px/16px 'Lucida Grande',sans-serif;padding:0 10px;}.discoverability-dialog span.privacy-statement a{font-weight:bold;}.discoverability-dialog .discoverability-settings{border-top:1px solid #eee;}.discoverability-dialog .modal-content fieldset{margin-bottom:0;padding-bottom:0;}.discover-buttons{border-top:1px solid #eee;}.discover-buttons button{margin:0 10px 0 0;}.help-discover{background:url(../images/sprite-icons.png) no-repeat scroll -208px 0 transparent;display:inline-block;height:14px;margin-left:5px;vertical-align:top;width:14px;}.twitter_feature_loaddisableder{height:0;width:0;overflow:hidden;display:none;position:absolute;}#twitter_hover_cards_loaddisableder{position:relative;}.hovercard,.hovercard-loaddisableding-above-below .hovercard-content-inner{width:290px;}.hovercard .hovercard-inner{font-size:11px;text-align:left;overflow:visible;}.hovercard-loaddisableding-above-below{width:100px;}.hovercard-loaddisableding-above-below .hovercard-inner{height:25px;}.hovercard-loaddisableding-inline .hovercard-inner{height:68px;}.bd .loaddisableding-inline-spinner{position:absolute;top:10px;left:10px;height:48px;width:48px;text-align:center;overflow:hidden;}.bd .loaddisableding-inline-spinner img{display:block;width:14px;height:14px;margin:17px auto;}.hovercard-inner .loaddisableding-above-below,.hovercard-inner .loaddisableding-inline,.hovercard-inner .user-dne{display:none;overflow:hidden;}.hovercard-inner .user-dne{opacity:0;}.hovercard-inner .loaddisableding-above-below .loaddisableding-msg{background:url(../images/spinner.gif) no-repeat;padding-left:20px;}.hovercard-loaddisableding-above-below .loaddisableding-above-below{display:block!important;margin:0;width:100px;overflow:hidden;position:relative;top:4px;left:4px;color:#666;font-size:11px;}.hovercard-loaddisableding-inline .loaddisableding-inline{display:block!important;overflow:hidden;margin:0;color:#666;font-size:11px;}.hovercard-user-dne .user-dne{display:block!important;}.hovercard-loaddisableding-above-below .hovercard-content,.hovercard-loaddisableding-inline .hovercard-content{width:0;height:0;overflow:hidden;}.hovercard-inner .bd{padding:10px;overflow:hidden;}.hovercard-inner a{cursor:pointer;}.hovercard-inner p.location{height:16px;}.hovercard-inner .avatar,.loaddisableding-inline-graphic{float:left;display:block;width:48px;height:48px;}.loaddisableding-inline-graphic{background-repeat:none;background-position:0 0;background-color:transparent;}.hovercard-inner .bio{margin-left:56px;}.hovercard-inner .bio span em{display:block;font-style:normal;}.not-inline .fn-inline,.inline .fn-above{display:none;}.fn-above{font-weight:bold;font-family:"helvetica neue",helvetica,arial,sans-serif;font-size:15px;color:#333;}.hovercard-inner .bio p{line-height:16px;}.hovercard-inner .hovercard-inner-footer{-moz-border-radius:0 0 5px 5px;-webkit-border-radius:0 0 5px 5px;background:#f6f6f6;height:39px;position:relative;}.hovercard-inner .action-dropdowns{position:absolute;left:231px;top:7px;}.hovercard-inner .setting{background:url(../images/sprite-icons.png) -96px -48px no-repeat;width:16px;height:16px;margin-left:5px;display:block;float:right;}.hovercard-inner .sms-setting-off{background-position:-160px -48px;}.hovercard-inner .sms-setting-not-off{background-position:-48px -48px;}.hovercard-inner .replies-setting-off{background-position:-144px -48px;}.hovercard-inner .replies-setting-not-off{background-position:0 -48px;}.hovercard-inner .shares-setting-off{background-position:-176px -48px;}.hovercard-inner .shares-setting-not-off{background-position:-96px -48px;}.hovercard-inner .is-following{background:url(../images/sprite-icons.png) -160px -16px;width:10px;height:9px;display:block;float:left;margin-right:3px;position:relative;top:2px;}.hovercard-inner .sn a{font-size:14px;line-height:16px;font-weight:bold;}.not-inline .hovercard-inner .sn a{font-size:11px;line-height:14px;font-weight:normal;}.inline .hovercard-inner .at_symbol{display:none;}.hovercard-inner .not-following,.hovercard-inner .following,.hovercard-inner .is-you{position:absolute;top:7px;left:11px;}.hovercard-inner .following-controls,.hovercard-inner .is-you{font-weight:bold;padding:5px 0 5px 0;}.hovercard-inner .following-controls span{cursor:pointer;float:left;}.hovercard-inner .following-controls .you-follow-user{cursor:text;}.hovercard .not-following .following-controls,.hovercard .following .follow-controls,.hovercard .blocking .follow-controls{display:none;}.hovercard-inner .sn img{position:relative;top:2px;}.hovercard-inner .user i{display:inline-block;_display:inline;background-position:-176px -32px;width:15px;background-image:url(../images/sprite-icons.png);background-repeat:no-repeat;height:13px;outline-color:-moz-use-text-color;overflow:hidden;margin:0 3px -3px 0;}.hovercard-inner .user b{background-image:url(../images/sprite-icons.png);background-repeat:no-repeat;background-position:0 -64px;}.hovercard-inner .action-menu{padding-right:0;}.hovercard-inner .action-menu i{background-position:-32px -64px;width:22px;}.hovercard-inner .action-menu span{visibility:hidden;}.hovercard-inner .list-menu i{background-position:-96px -64px;width:22px;margin:0 0 -3px 0;}.hovercard .action-list{background-color:#fff;border:1px solid #666;margin-top:-1px;padding:0;position:absolute;left:243px;margin-top:-12px;text-align:left;width:200px;z-index:9999;}.ie .hovercard .action-list,.firefox-windows .hovercard .action-list{left:244px;}.hovercard .action-list li a:hover{color:#fff;background-color:#666;}.hovercard .action-list li a{color:#666;display:block;text-decoration:none;padding:6px 5px 6px 7px;}.hovercard .action-list i{float:left;width:15px;height:13px;margin-right:4px;display:inline;background-image:url(../images/sprite-icons.png);}.hovercard .action-list .mention i{background-position:-16px -32px;}.hovercard .action-list .mention:hover i{background-position:0 -32px;}.hovercard .action-list .direct-message i{background-position:-48px -32px;}.hovercard .action-list .direct-message:hover i{background-position:-32px -32px;}.hovercard .action-list .follow i{background-position:-176px -32px;}.hovercard .action-list .follow:hover i{background-position:-160px -32px;}.hovercard .action-list .remove i{background-position:-208px -32px;}.hovercard .action-list .remove:hover i{background-position:-192px -32px;}.hovercard .action-list .unfollow i{background-position:-112px -32px;}.hovercard .action-list .unfollow:hover i{background-position:-96px -32px;}.hovercard .action-list .report-for-spam i{background-position:-272px -32px;}.hovercard .action-list .report-for-spam:hover i{background-position:-256px -32px;}.hovercard .action-list .block i{background-position:-144px -32px;}.hovercard .action-list .block:hover i{background-position:-128px -32px;}.hovercard .action-list .unblock i{background-position:-144px -32px;}.hovercard .action-list .unblock:hover i{background-position:-128px -32px;}.hovercard-inner .description{color:#656565;clear:left;overflow:hidden;height:auto;padding-top:3px;}.hovercard-inner .description-inactive{height:0;}.hovercard .direct-message{display:none;}.hovercard .following-you .direct-message{display:block;}.hovercard .not-following .unfollow,.hovercard .following .follow,.hovercard .not-blocking .unblock,.hovercard .blocking .block,.hovercard .blocking .direct-message,.hovercard .blocking .follow,.hovercard .blocking .report-for-spam{display:none;}.hovercard-inner ul.user_stats{overflow:hidden;}.hovercard-inner ul.user_stats,.hovercard-inner .user_stats li{margin:0;padding:0;list-style:none;}.hovercard-inner .description p,.hovercard-inner .description ul{padding:3px 0;color:#333;}.hovercard-inner .user_stats li{float:left;border-right:1px solid #eee;padding:1px 12px;letter-spacing:-0.5px;}.hovercard-inner .user_stats li.last{border-right-width:0;}.hovercard-inner .user_stats li.first{padding-left:0;}.hovercard-inner .user_stats .stat{font-weight:bold;display:block;color:#333;font-size:12px;font-family:"Helvetica Neue",Arial,sans-serif;letter-spacing:.5px;}.hovercard-inner .user_stats .type{color:#666;}.hovercard .hovercard-divot{position:absolute;left:24px;width:27px;height:15px;z-index:999;}.position_above .hovercard-divot{top:auto;bottom:-15px;background:url(../images/divvy.png) no-repeat;}.position_above{top:auto!important;}.position_below .hovercard-divot{bottom:auto;top:-15px;background:url(../images/divvy-up.png) no-repeat;}.ie .position_above .hovercard-divot{background:url(../images/divvy.gif) no-repeat;}a.signin{background:#7fb3cc;margin-left:4px;padding:5px 6px 6px;text-decoration:none;font-weight:bold;color:#fff;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}a.signin span{background-image:url(../images/toggle_down_light.png);_background-image:url(../images/toggle_down_light.gif);background-repeat:no-repeat;background-position:100% 50%;padding:4px 16px 6px 0;}body.signin-island a.signin,body.signin-island a.signin:hover,body.signin-island a.signin:focus{background:none;}body.signin-island a.void,body.signin-island a.void:hover,body.signin-island a.void:focus{background:none repeat scroll 0 0 #CCC;}body.signin-island .signin span{color:#27B;background:url(../images/toggle_down_dark.png) 100% 50% no-repeat #fff;_background:url(../images/toggle_down_dark.gif) 100% 50% no-repeat #fff;}body.signin-island .void span{background:url(../images/toggle_up_dark.png) 100% 50% no-repeat #CCC;_background:url(../images/toggle_up_dark.gif) 100% 50% no-repeat #CCC;color:#333;}body.signin-island #have_an_account{-moz-border-radius:4px;-webkit-border-radius:4px;border-radius:4px;line-height:22px;background-color:#FFF;font-size:11px;padding:5px 0 7px 10px;zoom:1;color:#666;}a.void{background:#ccc;-webkit-border-bottom-left-radius:0;-webkit-border-bottom-right-radius:0;-moz-border-radius-bottomleft:0;-moz-border-radius-bottomright:0;border-radius-bottom-left:0;border-radius-bottom-right:0;-webkit-box-shadow:0 3px 3px rgba(0,0,0,0.3);-mox-box-shadow:0 3px 3px rgba(0,0,0,0.3);box-shadow:0 3px 3px rgba(0,0,0,0.3);color:#000;}#signin_controls{position:relative;top:3px;zoom:1;}.signin-on a.signin span{background-image:url(../images/toggle_up_dark.png);_background-image:url(../images/toggle_up_dark.gif);}a.signin.void span{background-image:url(../images/toggle_up_dark.png);_background-image:url(../images/toggle_up_dark.gif);color:#333;}#signin_menu{position:absolute;top:100%;right:0;margin:0;z-index:100;width:230px;padding:8px;-webkit-border-radius:5px;-webkit-border-top-right-radius:0;-moz-border-radius:5px;-moz-border-radius-topright:0;border-radius:5px;border-radius-top-right:0;-webkit-box-shadow:0 3px 3px rgba(0,0,0,0.3);-moz-box-shadow:0 3px 3px rgba(0,0,0,0.3);box-shadow:0 3px 3px rgba(0,0,0,0.3);text-align:left;line-height:16px;background:#fff;border:5px solid #ccc;}.signin-on #signin_menu{display:block;}#signin{margin:0;font-size:11px;color:#666;}#signin p{margin:0;}#signin .textbox label{display:block;padding:0 0 3px;}#signin .textbox input{background:#fff;display:block;width:218px;margin:0 0 8px;padding:5px;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;font:13px "Lucida Grande",Arial,Sans-serif;}#signin .textbox input:focus{border-color:#ccc;outline-width:0;}#signin p.forgot,#signin p.forgot-username{display:inline;line-height:20px;}.remember{padding:4px 0 12px;}#signin_submit{background:#39d url(../images/bg-btn-blue.png) repeat-x 0 0;width:auto;overflow:visible;margin:0 5px 0 0;padding:4px 10px 5px;border:1px solid #39d;-moz-border-radius:4px;-khtml-border-radius:4px;-webkit-border-radius:4px;border-radius:4px;font:bold 11px "Lucida Grande",Arial,Sans-serif;color:#fff;text-shadow:0 -1px 0 #39d;}#signin_submit::-moz-focus-inner{padding:0;border:0;}#signin_submit:hover,#signin_submit:focus{background-position:0 -5px;cursor:pointer;}a.signin:hover,a.signin:focus{background:#6faac8;}a.void:hover{background:#ccc;}#signin_submit:active{background-image:none;}#signin .forgot{margin-bottom:4px;}#signin .forgot a,#signin .complete a{margin-right:5px;}#signin_submit{-moz-border-radius:4px;-webkit-border-radius:4px;background:#39d url(../images/bg-btn-blue.png) repeat-x scroll 0 0;border:1px solid #39D;color:#fff;text-shadow:0 -1px 0 #39d;padding:4px 10px 5px;font-size:11px;margin:0 5px 0 0;font-weight:bold;} \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_background_images/30261844/ICHCTwitterBG.jpg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_background_images/30261844/ICHCTwitterBG.jpg
new file mode 100755
index 0000000000..887afc9d52
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_background_images/30261844/ICHCTwitterBG.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/1063331761/LOLmart_150_mini.jpg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/1063331761/LOLmart_150_mini.jpg
new file mode 100755
index 0000000000..98e5fe7f88
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/1063331761/LOLmart_150_mini.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/1124077786/batvatar_mini.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/1124077786/batvatar_mini.png
new file mode 100755
index 0000000000..76394d3c0b
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/1124077786/batvatar_mini.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/1155395599/Memebase_small_mini.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/1155395599/Memebase_small_mini.png
new file mode 100755
index 0000000000..fe608e1754
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/1155395599/Memebase_small_mini.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/1289641028/CH_mini.jpg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/1289641028/CH_mini.jpg
new file mode 100755
index 0000000000..d1063d2ead
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/1289641028/CH_mini.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/1296459376/profile_image_1301694822477_mini.jpg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/1296459376/profile_image_1301694822477_mini.jpg
new file mode 100755
index 0000000000..d9d16ca523
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/1296459376/profile_image_1301694822477_mini.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/700174615/twitter_mini.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/700174615/twitter_mini.png
new file mode 100755
index 0000000000..83cfbfd9ca
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/700174615/twitter_mini.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/724048626/Picture_3895-1_mini.jpg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/724048626/Picture_3895-1_mini.jpg
new file mode 100755
index 0000000000..121e604b19
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/724048626/Picture_3895-1_mini.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/959827428/25000_1397284054938_1317351118_31101620_485629_n_mini.jpg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/959827428/25000_1397284054938_1317351118_31101620_485629_n_mini.jpg
new file mode 100755
index 0000000000..60c5f613fb
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/959827428/25000_1397284054938_1317351118_31101620_485629_n_mini.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/959952929/ci_300x300_mini.jpg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/959952929/ci_300x300_mini.jpg
new file mode 100755
index 0000000000..34cb49536d
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/profile_images/959952929/ci_300x300_mini.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/sticky/default_profile_images/default_profile_1_mini.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/sticky/default_profile_images/default_profile_1_mini.png
new file mode 100755
index 0000000000..92123d122f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/sticky/default_profile_images/default_profile_1_mini.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/sticky/default_profile_images/default_profile_2_mini.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/sticky/default_profile_images/default_profile_2_mini.png
new file mode 100755
index 0000000000..94a82c4d68
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/sticky/default_profile_images/default_profile_2_mini.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/sticky/default_profile_images/default_profile_6_mini.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/sticky/default_profile_images/default_profile_6_mini.png
new file mode 100755
index 0000000000..0a155410df
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a2.twimg.com/sticky/default_profile_images/default_profile_6_mini.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/arrow_right_dark.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/arrow_right_dark.png
new file mode 100755
index 0000000000..4e892821b6
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/arrow_right_dark.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/buttons/bg-btn.gif b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/buttons/bg-btn.gif
new file mode 100755
index 0000000000..5d1e16452e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/buttons/bg-btn.gif
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/check.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/check.png
new file mode 100755
index 0000000000..1e0188d587
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/check.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/connect_129px.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/connect_129px.png
new file mode 100755
index 0000000000..b1d8591a86
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/connect_129px.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/connect_146px.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/connect_146px.png
new file mode 100755
index 0000000000..5b99bda010
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/connect_146px.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/connect_170px.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/connect_170px.png
new file mode 100755
index 0000000000..d990e2e236
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/connect_170px.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/connect_236px.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/connect_236px.png
new file mode 100755
index 0000000000..7b8b74d498
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/connect_236px.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/gradient-background.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/gradient-background.png
new file mode 100755
index 0000000000..503ab9f108
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/gradient-background.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/rays-box.jpg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/rays-box.jpg
new file mode 100755
index 0000000000..bb19d1f61e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/rays-box.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/t_170px.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/t_170px.png
new file mode 100755
index 0000000000..2cce581176
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/oauth2/t_170px.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/sprite-icons.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/sprite-icons.png
new file mode 100755
index 0000000000..a93cede946
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/images/sprite-icons.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/javascripts/api.js@1302114648 b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/javascripts/api.js@1302114648
new file mode 100755
index 0000000000..e846ebda39
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/javascripts/api.js@1302114648
@@ -0,0 +1 @@
+twttr.augmentString("twttr.api",{defaultAjaxOptions:{type:"POST",dataType:"json",url:"#",data:{authenticity_token:"",twttr:true},success:function(){},error:function(){},beforeSend:function(){}},tweet:function(B,C,A){twttr.User.findById(B,this,function(D){var H=A.success;var F={status:C};var G=function(I){D.update("latest_status",I.text);H(I)};var E="/status/update";this._sendRequest(twttr.merge(A,{url:E,success:G,data:F},true))})},autocomplete:function(B,C,A){twttr.User.findById(B,this,function(D){var F={user_id:B,sn:C};var E="/users/autocomplete";this._sendRequest(twttr.merge(A,{url:E,data:F},true))})},follow:function(B,A){twttr.User.findById(B,this,function(C){var F=A.success;var E=function(G){C.updateAll({do_not_follow:false,do_you_follow:true,sees_retweets:true});F(G)};var D="/friendships/create/"+B;this._sendRequest(twttr.merge(A,{url:D,success:E},true))})},unfollow:function(B,A){twttr.User.findById(B,this,function(C){var F=A.success;var E=function(G){C.updateAll({do_not_follow:true,do_you_follow:false,gets_device_updates:false,sees_replies:false,sees_retweets:false});F(G)};var D="/friendships/destroy/"+B;this._sendRequest(twttr.merge(A,{url:D,success:E},true))})},block:function(B,A){twttr.User.findById(B,this,function(C){var F=A.success;var E=function(G){C.updateAll({is_not_blocking:false,is_blocking:true,do_not_follow:true,do_you_follow:false,does_follow_you:false,gets_device_updates:false,sees_replies:false,sees_retweets:false});F(G)};var D="/blocks/create/"+B;this._sendRequest(twttr.merge(A,{url:D,success:E},true))})},unblock:function(B,A){twttr.User.findById(B,this,function(C){var F=A.success;var E=function(G){C.updateAll({is_not_blocking:true,is_blocking:false,do_not_follow:true,do_you_follow:false,does_follow_you:false,gets_device_updates:false,sees_replies:false,sees_retweets:false});F(G)};var D="/blocks/destroy/"+B;this._sendRequest(twttr.merge(A,{url:D,success:E},true))})},reportForSpam:function(B,A){twttr.User.findById(B,this,function(C){var F=A.success;var E=function(G){C.updateAll({is_not_blocking:false,is_blocking:true,do_not_follow:true,do_you_follow:false,does_follow_you:false,gets_device_updates:false,sees_replies:false,sees_retweets:false});F(G)};var D="/user_spam_reports/"+B;this._sendRequest(twttr.merge(A,{url:D,success:E},true))})},reportSpam:function(B,A){this.reportSpam.apply(arguments)},setDeviceAlerts:function(B,D,A){var C={user_ids:B,value:D};var E=function(F){twttr.User.findById(B,function(G){G.update("gets_device_updates",D=="on")})};this._sendRequest(twttr.merge(A,{url:"/friendships/set_sms",data:C,success:E},true))},setRetweetVisibility:function(B,D,A){var C={user_ids:B,value:D};var E=function(F){twttr.User.findById(B,function(G){G.update("sees_retweets",D=="on")})};this._sendRequest(twttr.merge(A,{url:"/friendships/set_shares",data:C,success:E},true))},setMentions:function(B,D,A){var C={user_ids:B,value:D};var E=function(F){twttr.User.findById(B,function(G){G.update("sees_replies",D=="on")})};this._sendRequest(twttr.merge(A,{url:"/friendships/set_replies",data:C,success:E},true))},reverseGeocode:function(A){var B={type:"GET",url:"/1/geo/reverse_geocode.json"};this._sendRequest(twttr.merge(A,B,true))},search:function(A){var B={type:"GET",url:"/1/geo/search.json"};this._sendRequest(twttr.merge(A,B,true))},createPlace:function(A){var B={type:"POST",url:"/1/geo/place.json"};this._sendRequest(twttr.merge(A,B,true))},similarPlaces:function(B){var A={type:"GET",url:"/1/geo/similar_places.json"};this._sendRequest(twttr.merge(B,A,true))},getPlaceDetails:function(A){var B={type:"GET",url:"/1/geo/id/"+A.place_id+".json"};this._sendRequest(twttr.merge(A,B,true))},_sendRequest:function(B){var C={};if(twttr.form_authenticity_token){C.authenticity_token=twttr.form_authenticity_token}var A=twttr.merge({},twttr.api.defaultAjaxOptions,{data:C},B,true);$.ajax(A)}}); \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/javascripts/lib/gears_init.js@1302114648 b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/javascripts/lib/gears_init.js@1302114648
new file mode 100755
index 0000000000..4960f50fe5
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/javascripts/lib/gears_init.js@1302114648
@@ -0,0 +1,87 @@
+// Copyright 2007, Google Inc.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+// this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+// 3. Neither the name of Google Inc. nor the names of its contributors may be
+// used to endorse or promote products derived from this software without
+// specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+//
+// Sets up google.gears.*, which is *the only* supported way to access Gears.
+//
+// Circumvent this file at your own risk!
+//
+// In the future, Gears may automatically define google.gears.* without this
+// file. Gears may use these objects to transparently fix bugs and compatibility
+// issues. Applications that use the code below will continue to work seamlessly
+// when that happens.
+
+(function() {
+ // We are already defined. Hooray!
+ if (window.google && google.gears) {
+ return;
+ }
+
+ var factory = null;
+
+ // Firefox
+ if (typeof GearsFactory != 'undefined') {
+ factory = new GearsFactory();
+ } else {
+ // IE
+ try {
+ factory = new ActiveXObject('Gears.Factory');
+ // privateSetGlobalObject is only required and supported on IE Mobile on
+ // WinCE.
+ if (factory.getBuildInfo().indexOf('ie_mobile') != -1) {
+ factory.privateSetGlobalObject(this);
+ }
+ } catch (e) {
+ // Safari
+ if ((typeof navigator.mimeTypes != 'undefined')
+ && navigator.mimeTypes["application/x-googlegears"]) {
+ factory = document.createElement("object");
+ factory.style.display = "none";
+ factory.width = 0;
+ factory.height = 0;
+ factory.type = "application/x-googlegears";
+ document.documentElement.appendChild(factory);
+ }
+ }
+ }
+
+ // *Do not* define any objects if Gears is not installed. This mimics the
+ // behavior of Gears defining the objects in the future.
+ if (!factory) {
+ return;
+ }
+
+ // Now set up the objects, being careful not to void anything.
+ //
+ // Note: In Internet Explorer for Windows Mobile, you can't add properties to
+ // the window object. However, global objects are automatically added as
+ // properties of the window object in all browsers.
+ if (!window.google) {
+ google = {};
+ }
+
+ if (!google.gears) {
+ google.gears = {factory: factory};
+ }
+})();
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/stylesheets/buttons_new.css@1302114648.css b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/stylesheets/buttons_new.css@1302114648.css
new file mode 100755
index 0000000000..961eac0be8
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/a/1302214109/stylesheets/buttons_new.css@1302114648.css
@@ -0,0 +1 @@
+.button{-moz-border-radius:4px;-khtml-border-radius:4px;-webkit-border-radius:4px;background:#ddd url(../images/buttons/bg-btn.gif) repeat-x 0 0;border-bottom-color:#ccc;border-color:#ddd;border-radius:4px;border-style:solid;border-width:1px;color:#333;cursor:pointer;display:inline;font:11px/14px "Lucida Grande",Sans-serif;margin:0;overflow:visible;padding:4px 8px 5px;text-shadow:1px 1px 0 #fff;}.button::-moz-focus-inner{padding:0;border:0;}.button:focus{outline:none;}.button:hover,.button:focus{background-position:0 -6px;border-color:#999 #999 #888;color:#000;}.button:active{background-image:none;text-shadow:none;outline:none;}#tweeting_controls a{line-height:13px;}#gear_dropdown{padding:4px 5px 5px;}#gear_dropdown span{background-image:url(../images/sprite-icons.png);background-position:-32px -63px;background-repeat:no-repeat;display:inline-block;width:22px;}ul.dropdown{display:none;position:absolute;width:200px;padding:4px 0;text-align:left;border:1px solid #666;background-color:#fff;z-index:9999;}ul.dropdown li a,ul.dropdown li label,ul.dropdown li input[type="checkbox"]{display:inline-block;font:11px 'Lucida Grande',Arial,sans-serif;color:#666;position:relative;padding:4px 5px;vertical-align:top;}ul.dropdown li .loaddisableding-spinner{display:inline-block;position:relative;top:4px;left:1px;margin-left:4px;}ul.dropdown li a{padding:4px 5px 4px 27px;}ul.dropdown li a{display:block;color:#666;text-decoration:none;}ul.dropdown li:hover{color:#fff;background-color:#666;}ul.dropdown li:hover *{color:#fff;}ul.dropdown li.divider{border-top:1px solid #ddd;}ul.dropdown i{background:url(../images/sprite-icons.png) repeat no-repeat;font-size:10px;left:7px;position:absolute;top:4px;width:15px;}#get_location_icon{background:url(../images/sprite-icons.png) -160px -64px no-repeat;display:inline-block;_display:inline;height:11px;width:11px;vertical-align:middle;}#location_spinner{display:none;height:11px;width:11px;vertical-align:middle;}.share-location-loaddisableding #location_spinner{display:inline-block;}.share-location-loaddisableding #get_location_icon{display:none;}a.a-btn{zoom:1;background:#ddd url(../images/buttons/bg-btn.gif) repeat-x scroll 0 0;cursor:pointer;text-shadow:1px 1px 0 #fff!important;border-color:#ddd #ddd #ccc!important;border-style:solid;border-width:1px!important;text-decoration:none;padding:4px 8px 5px;line-height:14px;font-size:11px;font-family:"lucida grande",helvetica,tahoma,arial;display:inline-block;_display:inline;-moz-border-radius:4px;-webkit-border-radius:4px;border-radius:4px;}a.a-btn,a.a-btn:visited{color:#333!important;}a.a-btn:hover,a.a-btn:focus{text-decoration:none;border-color:#999 #999 #888!important;color:#000;outline:none;}a.a-btn:active{background-image:none;outline:none;}:focus{-moz-outline-style:none;}a.a-btn-m{font-size:15px;font-family:"helvetica neue",arial,sans-serif;padding:5px 15px 6px;line-height:20px;-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;background-position:0 -200px;}a.a-btn-l{font-size:20px;line-height:26px;padding:7px 20px 8px;-moz-border-radius:6px;-webkit-border-radius:6px;border-radius:6px;font-family:"helvetica neue",arial,sans-serif;background-position:0 -400px;}a.btn-disabled{opacity:.6;filter:alpha(opacity=60);background-image:none;}.twitter-connect{border:0;outline:none;text-indent:-99999px;display:inline-block;background-repeat:no-repeat;background-position:top left;}.twitter-button{font:12px Arial,sans-serif;color:#fff;background:#1D6B9C url(../images/oauth2/gradient-background.png) repeat-x;text-indent:0;border:1px solid #18566A;display:inline-block;-moz-border-radius:4px;-webkit-border-radius:4px;-border-radius:4px;text-shadow:0 -1px 0 #18566A;}.twitter-button:hover{border:1px solid #00242C;background-position:left -23px;text-decoration:none;}.twitter-button:active{border:1px solid #044D77;background-position:left -46px;text-decoration:none;color:rgba(255,255,255,0.8);}.twitter-connect-small{background:url(../images/oauth2/connect_129px.png) no-repeat;width:129px;height:19px;}.twitter-connect-small:hover{background-position:left -19px;}.twitter-connect-small:active{background-position:left -38px;}.twitter-connect-medium{background:url(../images/oauth2/connect_146px.png) no-repeat;width:146px;height:23px;}.twitter-connect-medium:hover{background-position:left -23px;}.twitter-connect-medium:active{background-position:left -46px;}.twitter-connect-large{background:url(../images/oauth2/connect_170px.png) no-repeat;width:170px;height:26px;}.twitter-connect-large:hover{background-position:left -26px;}.twitter-connect-large:active{background-position:left -52px;}.twitter-connect-xlarge{background:url(../images/oauth2/connect_236px.png) no-repeat;width:236px;height:38px;}.twitter-connect-xlarge:hover{background-position:left -38px;}.twitter-connect-xlarge:active{background-position:left -76px;}.twitter-connect-box{font:13px/17px Lucida Grande,"Lucida Grande",Arial,Helvetica,sans-serif;padding:8px 10px 9px 10px;width:200px;background:#C7E0EC url(../images/oauth2/rays-box.jpg) no-repeat center top;color:#001F33;text-shadow:0 1px 0 #E5F0F6;border-radius:5px;-moz-border-radius:4px;-webkit-border-radius:4px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,.3);-moz-box-shadow:0 1px 0 rgba(0,0,0,.3);box-shadow:0 1px 0 rgba(0,0,0,.3);display:inline-block;vertical-align:top;}.twitter-connect-box p{margin:0 0 8px 0;padding:0;}.twitter-connect-box-small{font-size:10px;line-height:14px;width:129px;}.twitter-connect-box-medium{font-size:11px;line-height:15px;width:146px;}.twitter-connect-box-large{font-size:11px;line-height:15px;width:170px;}.twitter-connect-box-xlarge{font-size:12px;line-height:17px;width:236px;}.follow-medium{text-decoration:none;padding-right:7px;padding-left:2px;*padding:0 7px 0 0;}.follow-medium i{height:23px;width:23px;display:inline-block;border-right:1px solid #73AFD5;}.follow-medium i b{display:inline-block;background:url(../images/oauth2/t_170px.png) no-repeat 3px 3px;height:23px;width:22px;vertical-align:middle;border-right:1px solid #094B60;}.follow-medium .status{padding-left:4px;}.following-notice,.pending-notice{background-image:none;background:#eee;border:1px solid #ccc;color:#333;text-shadow:0 1px 0 #fff;cursor:default;padding:1px 8px 0;font:12px Arial,sans-serif;text-indent:0;display:inline-block;-moz-border-radius:4px;-webkit-border-radius:4px;-border-radius:4px;}.pending-notice{padding:5px 8px 2px;}.following-notice:active,.pending-notice:active{color:#333;text-shadow:0 1px 0 #fff;}.following-notice span.at,.pending-notice span.at{color:#666;}.following-notice a,.pending-notice a{color:#196698;font-weight:normal;text-decoration:none;}.following-notice a:hover,.pending-notice a:hover{text-decoration:underline;}.following-notice i{border-right:1px solid #eee;width:15px;}.pending-notice i{border-right:1px solid #eee;width:10px;height:9px;}.following-notice i b{border-right:0;width:15px;}.pending-notice i{border-right:0;width:9px;height:17px;}.following-notice i b{background:url(../images/oauth2/check.png) no-repeat 4px 7px;}.pending-notice i b{position:relative;top:-2px;border-right:none;width:10px;height:9px;background:url(../images/sprite-icons.png) no-repeat -192px -16px;}.twitter-loaddisableding{font:12px/15px Arial,Helvetica,sans-serif;color:#fff;background:#eee;border:1px solid #ccc;color:#333;text-shadow:0 1px 0 #fff;cursor:default;text-indent:0;padding:5px 8px 4px 8px;-moz-border-radius:4px;-webkit-border-radius:4px;-border-radius:4px;display:block;width:100px;margin-top:-3px;} \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1092057020/eli_avatar_mini.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1092057020/eli_avatar_mini.png
new file mode 100755
index 0000000000..67465a7724
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1092057020/eli_avatar_mini.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1096286685/newpink_copy_mini.jpg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1096286685/newpink_copy_mini.jpg
new file mode 100755
index 0000000000..c794a0a1cb
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1096286685/newpink_copy_mini.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1110864280/41628_1144937489_2484_q_mini.jpg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1110864280/41628_1144937489_2484_q_mini.jpg
new file mode 100755
index 0000000000..517fa5ed7e
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1110864280/41628_1144937489_2484_q_mini.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1213876440/27539_32561485399_2579_n_bigger.jpeg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1213876440/27539_32561485399_2579_n_bigger.jpeg
new file mode 100755
index 0000000000..f617346d74
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1213876440/27539_32561485399_2579_n_bigger.jpeg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1260578495/191281_1758367531945_1621722394_1723810_2598069_o_mini.jpg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1260578495/191281_1758367531945_1621722394_1723810_2598069_o_mini.jpg
new file mode 100755
index 0000000000..4a97c6415c
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1260578495/191281_1758367531945_1621722394_1723810_2598069_o_mini.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1299269362/10839_196974151498_693676498_3960874_1853030_n_mini.jpg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1299269362/10839_196974151498_693676498_3960874_1853030_n_mini.jpg
new file mode 100755
index 0000000000..3260db85b5
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1299269362/10839_196974151498_693676498_3960874_1853030_n_mini.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1302143328/Profile_copy_mini.jpg b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1302143328/Profile_copy_mini.jpg
new file mode 100755
index 0000000000..acc298eb48
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/profile_images/1302143328/Profile_copy_mini.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/sticky/default_profile_images/default_profile_3_mini.png b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/sticky/default_profile_images/default_profile_3_mini.png
new file mode 100755
index 0000000000..ad1dc7577a
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/a3.twimg.com/sticky/default_profile_images/default_profile_3_mini.png
Binary files differ
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/ajax.googleapis.com/ajax/libs/jquery/1.3.0/jquery.min.js b/mobile/android/tests/browser/chrome/tp5/twitter.com/ajax.googleapis.com/ajax/libs/jquery/1.3.0/jquery.min.js
new file mode 100755
index 0000000000..c487ba7a5b
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/ajax.googleapis.com/ajax/libs/jquery/1.3.0/jquery.min.js
@@ -0,0 +1,19 @@
+/*
+ * jQuery JavaScript Library v1.3
+ * http://jquery.com/
+ *
+ * Copyright (c) 2009 John Resig
+ * Dual licensed under the MIT and GPL licenses.
+ * http://docs.jquery.com/License
+ *
+ * Date: 2009-01-13 12:50:31 -0500 (Tue, 13 Jan 2009)
+ * Revision: 6104
+ */
+(function(){var l=this,g,x=l.jQuery,o=l.$,n=l.jQuery=l.$=function(D,E){return new n.fn.init(D,E)},C=/^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,f=/^.[^:#\[\.,]*$/;n.fn=n.prototype={init:function(D,G){D=D||document;if(D.nodeType){this[0]=D;this.length=1;this.context=D;return this}if(typeof D==="string"){var F=C.exec(D);if(F&&(F[1]||!G)){if(F[1]){D=n.clean([F[1]],G)}else{var H=document.getElementById(F[3]);if(H){if(H.id!=F[3]){return n().find(D)}var E=n(H);E.context=document;E.selector=D;return E}D=[]}}else{return n(G).find(D)}}else{if(n.isFunction(D)){return n(document).ready(D)}}if(D.selector&&D.context){this.selector=D.selector;this.context=D.context}return this.setArray(n.makeArray(D))},selector:"",jquery:"1.3",size:function(){return this.length},get:function(D){return D===g?n.makeArray(this):this[D]},pushStack:function(E,G,D){var F=n(E);F.prevObject=this;F.context=this.context;if(G==="find"){F.selector=this.selector+(this.selector?" ":"")+D}else{if(G){F.selector=this.selector+"."+G+"("+D+")"}}return F},setArray:function(D){this.length=0;Array.prototype.push.apply(this,D);return this},each:function(E,D){return n.each(this,E,D)},index:function(D){return n.inArray(D&&D.jquery?D[0]:D,this)},attr:function(E,G,F){var D=E;if(typeof E==="string"){if(G===g){return this[0]&&n[F||"attr"](this[0],E)}else{D={};D[E]=G}}return this.each(function(H){for(E in D){n.attr(F?this.style:this,E,n.prop(this,D[E],F,H,E))}})},css:function(D,E){if((D=="width"||D=="height")&&parseFloat(E)<0){E=g}return this.attr(D,E,"curCSS")},text:function(E){if(typeof E!=="object"&&E!=null){return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(E))}var D="";n.each(E||this,function(){n.each(this.childNodes,function(){if(this.nodeType!=8){D+=this.nodeType!=1?this.nodeValue:n.fn.text([this])}})});return D},wrapAll:function(D){if(this[0]){var E=n(D,this[0].ownerDocument).clone();if(this[0].parentNode){E.insertBefore(this[0])}E.map(function(){var F=this;while(F.firstChild){F=F.firstChild}return F}).append(this)}return this},wrapInner:function(D){return this.each(function(){n(this).contents().wrapAll(D)})},wrap:function(D){return this.each(function(){n(this).wrapAll(D)})},append:function(){return this.domManip(arguments,true,function(D){if(this.nodeType==1){this.appendChild(D)}})},prepend:function(){return this.domManip(arguments,true,function(D){if(this.nodeType==1){this.insertBefore(D,this.firstChild)}})},before:function(){return this.domManip(arguments,false,function(D){this.parentNode.insertBefore(D,this)})},after:function(){return this.domManip(arguments,false,function(D){this.parentNode.insertBefore(D,this.nextSibling)})},end:function(){return this.prevObject||n([])},push:[].push,find:function(D){if(this.length===1&&!/,/.test(D)){var F=this.pushStack([],"find",D);F.length=0;n.find(D,this[0],F);return F}else{var E=n.map(this,function(G){return n.find(D,G)});return this.pushStack(/[^+>] [^+>]/.test(D)?n.unique(E):E,"find",D)}},clone:function(E){var D=this.map(function(){if(!n.support.noCloneEvent&&!n.isXMLDoc(this)){var H=this.cloneNode(true),G=document.createElement("div");G.appendChild(H);return n.clean([G.innerHTML])[0]}else{return this.cloneNode(true)}});var F=D.find("*").andSelf().each(function(){if(this[h]!==g){this[h]=null}});if(E===true){this.find("*").andSelf().each(function(H){if(this.nodeType==3){return}var G=n.data(this,"events");for(var J in G){for(var I in G[J]){n.event.add(F[H],J,G[J][I],G[J][I].data)}}})}return D},filter:function(D){return this.pushStack(n.isFunction(D)&&n.grep(this,function(F,E){return D.call(F,E)})||n.multiFilter(D,n.grep(this,function(E){return E.nodeType===1})),"filter",D)},closest:function(D){var E=n.expr.match.POS.test(D)?n(D):null;return this.map(function(){var F=this;while(F&&F.ownerDocument){if(E?E.index(F)>-1:n(F).is(D)){return F}F=F.parentNode}})},not:function(D){if(typeof D==="string"){if(f.test(D)){return this.pushStack(n.multiFilter(D,this,true),"not",D)}else{D=n.multiFilter(D,this)}}var E=D.length&&D[D.length-1]!==g&&!D.nodeType;return this.filter(function(){return E?n.inArray(this,D)<0:this!=D})},add:function(D){return this.pushStack(n.unique(n.merge(this.get(),typeof D==="string"?n(D):n.makeArray(D))))},is:function(D){return !!D&&n.multiFilter(D,this).length>0},hasClass:function(D){return !!D&&this.is("."+D)},val:function(J){if(J===g){var D=this[0];if(D){if(n.nodeName(D,"option")){return(D.attributes.value||{}).specified?D.value:D.text}if(n.nodeName(D,"select")){var H=D.selectedIndex,K=[],L=D.options,G=D.type=="select-one";if(H<0){return null}for(var E=G?H:0,I=G?H+1:L.length;E<I;E++){var F=L[E];if(F.selected){J=n(F).val();if(G){return J}K.push(J)}}return K}return(D.value||"").replace(/\r/g,"")}return g}if(typeof J==="number"){J+=""}return this.each(function(){if(this.nodeType!=1){return}if(n.isArray(J)&&/radio|checkbox/.test(this.type)){this.checked=(n.inArray(this.value,J)>=0||n.inArray(this.name,J)>=0)}else{if(n.nodeName(this,"select")){var M=n.makeArray(J);n("option",this).each(function(){this.selected=(n.inArray(this.value,M)>=0||n.inArray(this.text,M)>=0)});if(!M.length){this.selectedIndex=-1}}else{this.value=J}}})},html:function(D){return D===g?(this[0]?this[0].innerHTML:null):this.empty().append(D)},replaceWith:function(D){return this.after(D).remove()},eq:function(D){return this.slice(D,+D+1)},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments),"slice",Array.prototype.slice.call(arguments).join(","))},map:function(D){return this.pushStack(n.map(this,function(F,E){return D.call(F,E,F)}))},andSelf:function(){return this.add(this.prevObject)},domManip:function(J,M,L){if(this[0]){var I=(this[0].ownerDocument||this[0]).createDocumentFragment(),F=n.clean(J,(this[0].ownerDocument||this[0]),I),H=I.firstChild,D=this.length>1?I.cloneNode(true):I;if(H){for(var G=0,E=this.length;G<E;G++){L.call(K(this[G],H),G>0?D.cloneNode(true):I)}}if(F){n.each(F,y)}}return this;function K(N,O){return M&&n.nodeName(N,"table")&&n.nodeName(O,"tr")?(N.getElementsByTagName("tbody")[0]||N.appendChild(N.ownerDocument.createElement("tbody"))):N}}};n.fn.init.prototype=n.fn;function y(D,E){if(E.src){n.ajax({url:E.src,async:false,dataType:"script"})}else{n.globalEval(E.text||E.textContent||E.innerHTML||"")}if(E.parentNode){E.parentNode.removeChild(E)}}function e(){return +new Date}n.extend=n.fn.extend=function(){var I=arguments[0]||{},G=1,H=arguments.length,D=false,F;if(typeof I==="boolean"){D=I;I=arguments[1]||{};G=2}if(typeof I!=="object"&&!n.isFunction(I)){I={}}if(H==G){I=this;--G}for(;G<H;G++){if((F=arguments[G])!=null){for(var E in F){var J=I[E],K=F[E];if(I===K){continue}if(D&&K&&typeof K==="object"&&!K.nodeType){I[E]=n.extend(D,J||(K.length!=null?[]:{}),K)}else{if(K!==g){I[E]=K}}}}}return I};var b=/z-?index|font-?weight|opacity|zoom|line-?height/i,p=document.defaultView||{},r=Object.prototype.toString;n.extend({noConflict:function(D){l.$=o;if(D){l.jQuery=x}return n},isFunction:function(D){return r.call(D)==="[object Function]"},isArray:function(D){return r.call(D)==="[object Array]"},isXMLDoc:function(D){return D.documentElement&&!D.body||D.tagName&&D.ownerDocument&&!D.ownerDocument.body},globalEval:function(F){F=n.trim(F);if(F){var E=document.getElementsByTagName("head")[0]||document.documentElement,D=document.createElement("script");D.type="text/javascript";if(n.support.scriptEval){D.appendChild(document.createTextNode(F))}else{D.text=F}E.insertBefore(D,E.firstChild);E.removeChild(D)}},nodeName:function(E,D){return E.nodeName&&E.nodeName.toUpperCase()==D.toUpperCase()},each:function(F,J,E){var D,G=0,H=F.length;if(E){if(H===g){for(D in F){if(J.apply(F[D],E)===false){break}}}else{for(;G<H;){if(J.apply(F[G++],E)===false){break}}}}else{if(H===g){for(D in F){if(J.call(F[D],D,F[D])===false){break}}}else{for(var I=F[0];G<H&&J.call(I,G,I)!==false;I=F[++G]){}}}return F},prop:function(G,H,F,E,D){if(n.isFunction(H)){H=H.call(G,E)}return typeof H==="number"&&F=="curCSS"&&!b.test(D)?H+"px":H},className:{add:function(D,E){n.each((E||"").split(/\s+/),function(F,G){if(D.nodeType==1&&!n.className.has(D.className,G)){D.className+=(D.className?" ":"")+G}})},remove:function(D,E){if(D.nodeType==1){D.className=E!==g?n.grep(D.className.split(/\s+/),function(F){return !n.className.has(E,F)}).join(" "):""}},has:function(E,D){return n.inArray(D,(E.className||E).toString().split(/\s+/))>-1}},swap:function(G,F,H){var D={};for(var E in F){D[E]=G.style[E];G.style[E]=F[E]}H.call(G);for(var E in F){G.style[E]=D[E]}},css:function(F,D,H){if(D=="width"||D=="height"){var J,E={position:"absolute",visibility:"hidden",display:"block"},I=D=="width"?["Left","Right"]:["Top","Bottom"];function G(){J=D=="width"?F.offsetWidth:F.offsetHeight;var L=0,K=0;n.each(I,function(){L+=parseFloat(n.curCSS(F,"padding"+this,true))||0;K+=parseFloat(n.curCSS(F,"border"+this+"Width",true))||0});J-=Math.round(L+K)}if(n(F).is(":visible")){G()}else{n.swap(F,E,G)}return Math.max(0,J)}return n.curCSS(F,D,H)},curCSS:function(H,E,F){var K,D=H.style;if(E=="opacity"&&!n.support.opacity){K=n.attr(D,"opacity");return K==""?"1":K}if(E.match(/float/i)){E=v}if(!F&&D&&D[E]){K=D[E]}else{if(p.getComputedStyle){if(E.match(/float/i)){E="float"}E=E.replace(/([A-Z])/g,"-$1").toLowerCase();var L=p.getComputedStyle(H,null);if(L){K=L.getPropertyValue(E)}if(E=="opacity"&&K==""){K="1"}}else{if(H.currentStyle){var I=E.replace(/\-(\w)/g,function(M,N){return N.toUpperCase()});K=H.currentStyle[E]||H.currentStyle[I];if(!/^\d+(px)?$/i.test(K)&&/^\d/.test(K)){var G=D.left,J=H.runtimeStyle.left;H.runtimeStyle.left=H.currentStyle.left;D.left=K||0;K=D.pixelLeft+"px";D.left=G;H.runtimeStyle.left=J}}}}return K},clean:function(E,J,H){J=J||document;if(typeof J.createElement==="undefined"){J=J.ownerDocument||J[0]&&J[0].ownerDocument||document}if(!H&&E.length===1&&typeof E[0]==="string"){var G=/^<(\w+)\s*\/?>$/.exec(E[0]);if(G){return[J.createElement(G[1])]}}var F=[],D=[],K=J.createElement("div");n.each(E,function(O,Q){if(typeof Q==="number"){Q+=""}if(!Q){return}if(typeof Q==="string"){Q=Q.replace(/(<(\w+)[^>]*?)\/>/g,function(S,T,R){return R.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?S:T+"></"+R+">"});var N=n.trim(Q).toLowerCase();var P=!N.indexOf("<opt")&&[1,"<select multiple='multiple'>","</select>"]||!N.indexOf("<leg")&&[1,"<fieldset>","</fieldset>"]||N.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"<table>","</table>"]||!N.indexOf("<tr")&&[2,"<table><tbody>","</tbody></table>"]||(!N.indexOf("<td")||!N.indexOf("<th"))&&[3,"<table><tbody><tr>","</tr></tbody></table>"]||!N.indexOf("<col")&&[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"]||!n.support.htmlSerialize&&[1,"div<div>","</div>"]||[0,"",""];K.innerHTML=P[1]+Q+P[2];while(P[0]--){K=K.lastChild}if(!n.support.tbody){var M=!N.indexOf("<table")&&N.indexOf("<tbody")<0?K.firstChild&&K.firstChild.childNodes:P[1]=="<table>"&&N.indexOf("<tbody")<0?K.childNodes:[];for(var L=M.length-1;L>=0;--L){if(n.nodeName(M[L],"tbody")&&!M[L].childNodes.length){M[L].parentNode.removeChild(M[L])}}}if(!n.support.leadingWhitespace&&/^\s/.test(Q)){K.insertBefore(J.createTextNode(Q.match(/^\s*/)[0]),K.firstChild)}Q=n.makeArray(K.childNodes)}if(Q.nodeType){F.push(Q)}else{F=n.merge(F,Q)}});if(H){for(var I=0;F[I];I++){if(n.nodeName(F[I],"script")&&(!F[I].type||F[I].type.toLowerCase()==="text/javascript")){D.push(F[I].parentNode?F[I].parentNode.removeChild(F[I]):F[I])}else{if(F[I].nodeType===1){F.splice.apply(F,[I+1,0].concat(n.makeArray(F[I].getElementsByTagName("script"))))}H.appendChild(F[I])}}return D}return F},attr:function(I,F,J){if(!I||I.nodeType==3||I.nodeType==8){return g}var G=!n.isXMLDoc(I),K=J!==g;F=G&&n.props[F]||F;if(I.tagName){var E=/href|src|style/.test(F);if(F=="selected"&&I.parentNode){I.parentNode.selectedIndex}if(F in I&&G&&!E){if(K){if(F=="type"&&n.nodeName(I,"input")&&I.parentNode){throw"type property can't be changed"}I[F]=J}if(n.nodeName(I,"form")&&I.getAttributeNode(F)){return I.getAttributeNode(F).nodeValue}if(F=="tabIndex"){var H=I.getAttributeNode("tabIndex");return H&&H.specified?H.value:I.nodeName.match(/^(a|area|button|input|object|select|textarea)$/i)?0:g}return I[F]}if(!n.support.style&&G&&F=="style"){return n.attr(I.style,"cssText",J)}if(K){I.setAttribute(F,""+J)}var D=!n.support.hrefNormalized&&G&&E?I.getAttribute(F,2):I.getAttribute(F);return D===null?g:D}if(!n.support.opacity&&F=="opacity"){if(K){I.zoom=1;I.filter=(I.filter||"").replace(/alpha\([^)]*\)/,"")+(parseInt(J)+""=="NaN"?"":"alpha(opacity="+J*100+")")}return I.filter&&I.filter.indexOf("opacity=")>=0?(parseFloat(I.filter.match(/opacity=([^)]*)/)[1])/100)+"":""}F=F.replace(/-([a-z])/ig,function(L,M){return M.toUpperCase()});if(K){I[F]=J}return I[F]},trim:function(D){return(D||"").replace(/^\s+|\s+$/g,"")},makeArray:function(F){var D=[];if(F!=null){var E=F.length;if(E==null||typeof F==="string"||n.isFunction(F)||F.setInterval){D[0]=F}else{while(E){D[--E]=F[E]}}}return D},inArray:function(F,G){for(var D=0,E=G.length;D<E;D++){if(G[D]===F){return D}}return -1},merge:function(G,D){var E=0,F,H=G.length;if(!n.support.getAll){while((F=D[E++])!=null){if(F.nodeType!=8){G[H++]=F}}}else{while((F=D[E++])!=null){G[H++]=F}}return G},unique:function(J){var E=[],D={};try{for(var F=0,G=J.length;F<G;F++){var I=n.data(J[F]);if(!D[I]){D[I]=true;E.push(J[F])}}}catch(H){E=J}return E},grep:function(E,I,D){var F=[];for(var G=0,H=E.length;G<H;G++){if(!D!=!I(E[G],G)){F.push(E[G])}}return F},map:function(D,I){var E=[];for(var F=0,G=D.length;F<G;F++){var H=I(D[F],F);if(H!=null){E[E.length]=H}}return E.concat.apply([],E)}});var B=navigator.userAgent.toLowerCase();n.browser={version:(B.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/)||[0,"0"])[1],safari:/webkit/.test(B),opera:/opera/.test(B),msie:/msie/.test(B)&&!/opera/.test(B),mozilla:/mozilla/.test(B)&&!/(compatible|webkit)/.test(B)};n.each({parent:function(D){return D.parentNode},parents:function(D){return n.dir(D,"parentNode")},next:function(D){return n.nth(D,2,"nextSibling")},prev:function(D){return n.nth(D,2,"previousSibling")},nextAll:function(D){return n.dir(D,"nextSibling")},prevAll:function(D){return n.dir(D,"previousSibling")},siblings:function(D){return n.sibling(D.parentNode.firstChild,D)},children:function(D){return n.sibling(D.firstChild)},contents:function(D){return n.nodeName(D,"iframe")?D.contentDocument||D.contentWindow.document:n.makeArray(D.childNodes)}},function(D,E){n.fn[D]=function(F){var G=n.map(this,E);if(F&&typeof F=="string"){G=n.multiFilter(F,G)}return this.pushStack(n.unique(G),D,F)}});n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(D,E){n.fn[D]=function(){var F=arguments;return this.each(function(){for(var G=0,H=F.length;G<H;G++){n(F[G])[E](this)}})}});n.each({removeAttr:function(D){n.attr(this,D,"");if(this.nodeType==1){this.removeAttribute(D)}},addClass:function(D){n.className.add(this,D)},removeClass:function(D){n.className.remove(this,D)},toggleClass:function(E,D){if(typeof D!=="boolean"){D=!n.className.has(this,E)}n.className[D?"add":"remove"](this,E)},remove:function(D){if(!D||n.filter(D,[this]).length){n("*",this).add([this]).each(function(){n.event.remove(this);n.removeData(this)});if(this.parentNode){this.parentNode.removeChild(this)}}},empty:function(){n(">*",this).remove();while(this.firstChild){this.removeChild(this.firstChild)}}},function(D,E){n.fn[D]=function(){return this.each(E,arguments)}});function j(D,E){return D[0]&&parseInt(n.curCSS(D[0],E,true),10)||0}var h="jQuery"+e(),u=0,z={};n.extend({cache:{},data:function(E,D,F){E=E==l?z:E;var G=E[h];if(!G){G=E[h]=++u}if(D&&!n.cache[G]){n.cache[G]={}}if(F!==g){n.cache[G][D]=F}return D?n.cache[G][D]:G},removeData:function(E,D){E=E==l?z:E;var G=E[h];if(D){if(n.cache[G]){delete n.cache[G][D];D="";for(D in n.cache[G]){break}if(!D){n.removeData(E)}}}else{try{delete E[h]}catch(F){if(E.removeAttribute){E.removeAttribute(h)}}delete n.cache[G]}},queue:function(E,D,G){if(E){D=(D||"fx")+"queue";var F=n.data(E,D);if(!F||n.isArray(G)){F=n.data(E,D,n.makeArray(G))}else{if(G){F.push(G)}}}return F},dequeue:function(G,F){var D=n.queue(G,F),E=D.shift();if(!F||F==="fx"){E=D[0]}if(E!==g){E.call(G)}}});n.fn.extend({data:function(D,F){var G=D.split(".");G[1]=G[1]?"."+G[1]:"";if(F===g){var E=this.triggerHandler("getData"+G[1]+"!",[G[0]]);if(E===g&&this.length){E=n.data(this[0],D)}return E===g&&G[1]?this.data(G[0]):E}else{return this.trigger("setData"+G[1]+"!",[G[0],F]).each(function(){n.data(this,D,F)})}},removeData:function(D){return this.each(function(){n.removeData(this,D)})},queue:function(D,E){if(typeof D!=="string"){E=D;D="fx"}if(E===g){return n.queue(this[0],D)}return this.each(function(){var F=n.queue(this,D,E);if(D=="fx"&&F.length==1){F[0].call(this)}})},dequeue:function(D){return this.each(function(){n.dequeue(this,D)})}});
+/*
+ * Sizzle CSS Selector Engine - v0.9.1
+ * Copyright 2009, The Dojo Foundation
+ * Released under the MIT, BSD, and GPL Licenses.
+ * More information: http://sizzlejs.com/
+ */
+(function(){var N=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|[^[\]]+)+\]|\\.|[^ >+~,(\[]+)+|[>+~])(\s*,\s*)?/g,I=0,F=Object.prototype.toString;var E=function(ae,S,aa,V){aa=aa||[];S=S||document;if(S.nodeType!==1&&S.nodeType!==9){return[]}if(!ae||typeof ae!=="string"){return aa}var ab=[],ac,Y,ah,ag,Z,R,Q=true;N.lastIndex=0;while((ac=N.exec(ae))!==null){ab.push(ac[1]);if(ac[2]){R=RegExp.rightContext;break}}if(ab.length>1&&G.match.POS.exec(ae)){if(ab.length===2&&G.relative[ab[0]]){var U="",X;while((X=G.match.POS.exec(ae))){U+=X[0];ae=ae.replace(G.match.POS,"")}Y=E.filter(U,E(/\s$/.test(ae)?ae+"*":ae,S))}else{Y=G.relative[ab[0]]?[S]:E(ab.shift(),S);while(ab.length){var P=[];ae=ab.shift();if(G.relative[ae]){ae+=ab.shift()}for(var af=0,ad=Y.length;af<ad;af++){E(ae,Y[af],P)}Y=P}}}else{var ai=V?{expr:ab.pop(),set:D(V)}:E.find(ab.pop(),ab.length===1&&S.parentNode?S.parentNode:S);Y=E.filter(ai.expr,ai.set);if(ab.length>0){ah=D(Y)}else{Q=false}while(ab.length){var T=ab.pop(),W=T;if(!G.relative[T]){T=""}else{W=ab.pop()}if(W==null){W=S}G.relative[T](ah,W,M(S))}}if(!ah){ah=Y}if(!ah){throw"Syntax error, unrecognized expression: "+(T||ae)}if(F.call(ah)==="[object Array]"){if(!Q){aa.push.apply(aa,ah)}else{if(S.nodeType===1){for(var af=0;ah[af]!=null;af++){if(ah[af]&&(ah[af]===true||ah[af].nodeType===1&&H(S,ah[af]))){aa.push(Y[af])}}}else{for(var af=0;ah[af]!=null;af++){if(ah[af]&&ah[af].nodeType===1){aa.push(Y[af])}}}}}else{D(ah,aa)}if(R){E(R,S,aa,V)}return aa};E.matches=function(P,Q){return E(P,null,null,Q)};E.find=function(V,S){var W,Q;if(!V){return[]}for(var R=0,P=G.order.length;R<P;R++){var T=G.order[R],Q;if((Q=G.match[T].exec(V))){var U=RegExp.leftContext;if(U.substr(U.length-1)!=="\\"){Q[1]=(Q[1]||"").replace(/\\/g,"");W=G.find[T](Q,S);if(W!=null){V=V.replace(G.match[T],"");break}}}}if(!W){W=S.getElementsByTagName("*")}return{set:W,expr:V}};E.filter=function(S,ac,ad,T){var Q=S,Y=[],ah=ac,V,ab;while(S&&ac.length){for(var U in G.filter){if((V=G.match[U].exec(S))!=null){var Z=G.filter[U],R=null,X=0,aa,ag;ab=false;if(ah==Y){Y=[]}if(G.preFilter[U]){V=G.preFilter[U](V,ah,ad,Y,T);if(!V){ab=aa=true}else{if(V===true){continue}else{if(V[0]===true){R=[];var W=null,af;for(var ae=0;(af=ah[ae])!==g;ae++){if(af&&W!==af){R.push(af);W=af}}}}}}if(V){for(var ae=0;(ag=ah[ae])!==g;ae++){if(ag){if(R&&ag!=R[X]){X++}aa=Z(ag,V,X,R);var P=T^!!aa;if(ad&&aa!=null){if(P){ab=true}else{ah[ae]=false}}else{if(P){Y.push(ag);ab=true}}}}}if(aa!==g){if(!ad){ah=Y}S=S.replace(G.match[U],"");if(!ab){return[]}break}}}S=S.replace(/\s*,\s*/,"");if(S==Q){if(ab==null){throw"Syntax error, unrecognized expression: "+S}else{break}}Q=S}return ah};var G=E.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF_-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF_-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*_-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF_-]|\\.)+)(?:\((['"]*)((?:\([^\)]+\)|[^\2\(\)]*)+)\2\))?/},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(P){return P.getAttribute("href")}},relative:{"+":function(T,Q){for(var R=0,P=T.length;R<P;R++){var S=T[R];if(S){var U=S.previousSibling;while(U&&U.nodeType!==1){U=U.previousSibling}T[R]=typeof Q==="string"?U||false:U===Q}}if(typeof Q==="string"){E.filter(Q,T,true)}},">":function(U,Q,V){if(typeof Q==="string"&&!/\W/.test(Q)){Q=V?Q:Q.toUpperCase();for(var R=0,P=U.length;R<P;R++){var T=U[R];if(T){var S=T.parentNode;U[R]=S.nodeName===Q?S:false}}}else{for(var R=0,P=U.length;R<P;R++){var T=U[R];if(T){U[R]=typeof Q==="string"?T.parentNode:T.parentNode===Q}}if(typeof Q==="string"){E.filter(Q,U,true)}}},"":function(S,Q,U){var R="done"+(I++),P=O;if(!Q.match(/\W/)){var T=Q=U?Q:Q.toUpperCase();P=L}P("parentNode",Q,R,S,T,U)},"~":function(S,Q,U){var R="done"+(I++),P=O;if(typeof Q==="string"&&!Q.match(/\W/)){var T=Q=U?Q:Q.toUpperCase();P=L}P("previousSibling",Q,R,S,T,U)}},find:{ID:function(Q,R){if(R.getElementById){var P=R.getElementById(Q[1]);return P?[P]:[]}},NAME:function(P,Q){return Q.getElementsByName?Q.getElementsByName(P[1]):null},TAG:function(P,Q){return Q.getElementsByTagName(P[1])}},preFilter:{CLASS:function(S,Q,R,P,U){S=" "+S[1].replace(/\\/g,"")+" ";for(var T=0;Q[T];T++){if(U^(" "+Q[T].className+" ").indexOf(S)>=0){if(!R){P.push(Q[T])}}else{if(R){Q[T]=false}}}return false},ID:function(P){return P[1].replace(/\\/g,"")},TAG:function(Q,P){for(var R=0;!P[R];R++){}return M(P[R])?Q[1]:Q[1].toUpperCase()},CHILD:function(P){if(P[1]=="nth"){var Q=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(P[2]=="even"&&"2n"||P[2]=="odd"&&"2n+1"||!/\D/.test(P[2])&&"0n+"+P[2]||P[2]);P[2]=(Q[1]+(Q[2]||1))-0;P[3]=Q[3]-0}P[0]="done"+(I++);return P},ATTR:function(Q){var P=Q[1];if(G.attrMap[P]){Q[1]=G.attrMap[P]}if(Q[2]==="~="){Q[4]=" "+Q[4]+" "}return Q},PSEUDO:function(T,Q,R,P,U){if(T[1]==="not"){if(T[3].match(N).length>1){T[3]=E(T[3],null,null,Q)}else{var S=E.filter(T[3],Q,R,true^U);if(!R){P.push.apply(P,S)}return false}}else{if(G.match.POS.test(T[0])){return true}}return T},POS:function(P){P.unshift(true);return P}},filters:{enabled:function(P){return P.disabled===false&&P.type!=="hidden"},disabled:function(P){return P.disabled===true},checked:function(P){return P.checked===true},selected:function(P){P.parentNode.selectedIndex;return P.selected===true},parent:function(P){return !!P.firstChild},empty:function(P){return !P.firstChild},has:function(R,Q,P){return !!E(P[3],R).length},header:function(P){return/h\d/i.test(P.nodeName)},text:function(P){return"text"===P.type},radio:function(P){return"radio"===P.type},checkbox:function(P){return"checkbox"===P.type},file:function(P){return"file"===P.type},password:function(P){return"password"===P.type},submit:function(P){return"submit"===P.type},image:function(P){return"image"===P.type},reset:function(P){return"reset"===P.type},button:function(P){return"button"===P.type||P.nodeName.toUpperCase()==="BUTTON"},input:function(P){return/input|select|textarea|button/i.test(P.nodeName)}},setFilters:{first:function(Q,P){return P===0},last:function(R,Q,P,S){return Q===S.length-1},even:function(Q,P){return P%2===0},odd:function(Q,P){return P%2===1},lt:function(R,Q,P){return Q<P[3]-0},gt:function(R,Q,P){return Q>P[3]-0},nth:function(R,Q,P){return P[3]-0==Q},eq:function(R,Q,P){return P[3]-0==Q}},filter:{CHILD:function(P,S){var V=S[1],W=P.parentNode;var U="child"+W.childNodes.length;if(W&&(!W[U]||!P.nodeIndex)){var T=1;for(var Q=W.firstChild;Q;Q=Q.nextSibling){if(Q.nodeType==1){Q.nodeIndex=T++}}W[U]=T-1}if(V=="first"){return P.nodeIndex==1}else{if(V=="last"){return P.nodeIndex==W[U]}else{if(V=="only"){return W[U]==1}else{if(V=="nth"){var Y=false,R=S[2],X=S[3];if(R==1&&X==0){return true}if(R==0){if(P.nodeIndex==X){Y=true}}else{if((P.nodeIndex-X)%R==0&&(P.nodeIndex-X)/R>=0){Y=true}}return Y}}}}},PSEUDO:function(V,R,S,W){var Q=R[1],T=G.filters[Q];if(T){return T(V,S,R,W)}else{if(Q==="contains"){return(V.textContent||V.innerText||"").indexOf(R[3])>=0}else{if(Q==="not"){var U=R[3];for(var S=0,P=U.length;S<P;S++){if(U[S]===V){return false}}return true}}}},ID:function(Q,P){return Q.nodeType===1&&Q.getAttribute("id")===P},TAG:function(Q,P){return(P==="*"&&Q.nodeType===1)||Q.nodeName===P},CLASS:function(Q,P){return P.test(Q.className)},ATTR:function(T,R){var P=G.attrHandle[R[1]]?G.attrHandle[R[1]](T):T[R[1]]||T.getAttribute(R[1]),U=P+"",S=R[2],Q=R[4];return P==null?false:S==="="?U===Q:S==="*="?U.indexOf(Q)>=0:S==="~="?(" "+U+" ").indexOf(Q)>=0:!R[4]?P:S==="!="?U!=Q:S==="^="?U.indexOf(Q)===0:S==="$="?U.substr(U.length-Q.length)===Q:S==="|="?U===Q||U.substr(0,Q.length+1)===Q+"-":false},POS:function(T,Q,R,U){var P=Q[2],S=G.setFilters[P];if(S){return S(T,R,Q,U)}}}};for(var K in G.match){G.match[K]=RegExp(G.match[K].source+/(?![^\[]*\])(?![^\(]*\))/.source)}var D=function(Q,P){Q=Array.prototype.slice.call(Q);if(P){P.push.apply(P,Q);return P}return Q};try{Array.prototype.slice.call(document.documentElement.childNodes)}catch(J){D=function(T,S){var Q=S||[];if(F.call(T)==="[object Array]"){Array.prototype.push.apply(Q,T)}else{if(typeof T.length==="number"){for(var R=0,P=T.length;R<P;R++){Q.push(T[R])}}else{for(var R=0;T[R];R++){Q.push(T[R])}}}return Q}}(function(){var Q=document.createElement("form"),R="script"+(new Date).getTime();Q.innerHTML="<input name='"+R+"'/>";var P=document.documentElement;P.insertBefore(Q,P.firstChild);if(!!document.getElementById(R)){G.find.ID=function(T,U){if(U.getElementById){var S=U.getElementById(T[1]);return S?S.id===T[1]||S.getAttributeNode&&S.getAttributeNode("id").nodeValue===T[1]?[S]:g:[]}};G.filter.ID=function(U,S){var T=U.getAttributeNode&&U.getAttributeNode("id");return U.nodeType===1&&T&&T.nodeValue===S}}P.removeChild(Q)})();(function(){var P=document.createElement("div");P.appendChild(document.createComment(""));if(P.getElementsByTagName("*").length>0){G.find.TAG=function(Q,U){var T=U.getElementsByTagName(Q[1]);if(Q[1]==="*"){var S=[];for(var R=0;T[R];R++){if(T[R].nodeType===1){S.push(T[R])}}T=S}return T}}P.innerHTML="<a href='#'></a>";if(P.firstChild.getAttribute("href")!=="#"){G.attrHandle.href=function(Q){return Q.getAttribute("href",2)}}})();if(document.querySelectorAll){(function(){var P=E;E=function(T,S,Q,R){S=S||document;if(!R&&S.nodeType===9){try{return D(S.querySelectorAll(T),Q)}catch(U){}}return P(T,S,Q,R)};E.find=P.find;E.filter=P.filter;E.selectors=P.selectors;E.matches=P.matches})()}if(document.documentElement.getElementsByClassName){G.order.splice(1,0,"CLASS");G.find.CLASS=function(P,Q){return Q.getElementsByClassName(P[1])}}function L(Q,W,V,Z,X,Y){for(var T=0,R=Z.length;T<R;T++){var P=Z[T];if(P){P=P[Q];var U=false;while(P&&P.nodeType){var S=P[V];if(S){U=Z[S];break}if(P.nodeType===1&&!Y){P[V]=T}if(P.nodeName===W){U=P;break}P=P[Q]}Z[T]=U}}}function O(Q,V,U,Y,W,X){for(var S=0,R=Y.length;S<R;S++){var P=Y[S];if(P){P=P[Q];var T=false;while(P&&P.nodeType){if(P[U]){T=Y[P[U]];break}if(P.nodeType===1){if(!X){P[U]=S}if(typeof V!=="string"){if(P===V){T=true;break}}else{if(E.filter(V,[P]).length>0){T=P;break}}}P=P[Q]}Y[S]=T}}}var H=document.compareDocumentPosition?function(Q,P){return Q.compareDocumentPosition(P)&16}:function(Q,P){return Q!==P&&(Q.contains?Q.contains(P):true)};var M=function(P){return P.documentElement&&!P.body||P.tagName&&P.ownerDocument&&!P.ownerDocument.body};n.find=E;n.filter=E.filter;n.expr=E.selectors;n.expr[":"]=n.expr.filters;E.selectors.filters.hidden=function(P){return"hidden"===P.type||n.css(P,"display")==="none"||n.css(P,"visibility")==="hidden"};E.selectors.filters.visible=function(P){return"hidden"!==P.type&&n.css(P,"display")!=="none"&&n.css(P,"visibility")!=="hidden"};E.selectors.filters.animated=function(P){return n.grep(n.timers,function(Q){return P===Q.elem}).length};n.multiFilter=function(R,P,Q){if(Q){R=":not("+R+")"}return E.matches(R,P)};n.dir=function(R,Q){var P=[],S=R[Q];while(S&&S!=document){if(S.nodeType==1){P.push(S)}S=S[Q]}return P};n.nth=function(T,P,R,S){P=P||1;var Q=0;for(;T;T=T[R]){if(T.nodeType==1&&++Q==P){break}}return T};n.sibling=function(R,Q){var P=[];for(;R;R=R.nextSibling){if(R.nodeType==1&&R!=Q){P.push(R)}}return P};return;l.Sizzle=E})();n.event={add:function(H,E,G,J){if(H.nodeType==3||H.nodeType==8){return}if(H.setInterval&&H!=l){H=l}if(!G.guid){G.guid=this.guid++}if(J!==g){var F=G;G=this.proxy(F);G.data=J}var D=n.data(H,"events")||n.data(H,"events",{}),I=n.data(H,"handle")||n.data(H,"handle",function(){return typeof n!=="undefined"&&!n.event.triggered?n.event.handle.apply(arguments.callee.elem,arguments):g});I.elem=H;n.each(E.split(/\s+/),function(L,M){var N=M.split(".");M=N.shift();G.type=N.slice().sort().join(".");var K=D[M];if(n.event.specialAll[M]){n.event.specialAll[M].setup.call(H,J,N)}if(!K){K=D[M]={};if(!n.event.special[M]||n.event.special[M].setup.call(H,J,N)===false){if(H.addEventListener){H.addEventListener(M,I,false)}else{if(H.attachEvent){H.attachEvent("on"+M,I)}}}}K[G.guid]=G;n.event.global[M]=true});H=null},guid:1,global:{},remove:function(J,G,I){if(J.nodeType==3||J.nodeType==8){return}var F=n.data(J,"events"),E,D;if(F){if(G===g||(typeof G==="string"&&G.charAt(0)==".")){for(var H in F){this.remove(J,H+(G||""))}}else{if(G.type){I=G.handler;G=G.type}n.each(G.split(/\s+/),function(L,N){var P=N.split(".");N=P.shift();var M=RegExp("(^|\\.)"+P.slice().sort().join(".*\\.")+"(\\.|$)");if(F[N]){if(I){delete F[N][I.guid]}else{for(var O in F[N]){if(M.test(F[N][O].type)){delete F[N][O]}}}if(n.event.specialAll[N]){n.event.specialAll[N].teardown.call(J,P)}for(E in F[N]){break}if(!E){if(!n.event.special[N]||n.event.special[N].teardown.call(J,P)===false){if(J.removeEventListener){J.removeEventListener(N,n.data(J,"handle"),false)}else{if(J.detachEvent){J.detachEvent("on"+N,n.data(J,"handle"))}}}E=null;delete F[N]}}})}for(E in F){break}if(!E){var K=n.data(J,"handle");if(K){K.elem=null}n.removeData(J,"events");n.removeData(J,"handle")}}},trigger:function(H,J,G,D){var F=H.type||H;if(!D){H=typeof H==="object"?H[h]?H:n.extend(n.Event(F),H):n.Event(F);if(F.indexOf("!")>=0){H.type=F=F.slice(0,-1);H.exclusive=true}if(!G){H.stopPropagation();if(this.global[F]){n.each(n.cache,function(){if(this.events&&this.events[F]){n.event.trigger(H,J,this.handle.elem)}})}}if(!G||G.nodeType==3||G.nodeType==8){return g}H.result=g;H.target=G;J=n.makeArray(J);J.unshift(H)}H.currentTarget=G;var I=n.data(G,"handle");if(I){I.apply(G,J)}if((!G[F]||(n.nodeName(G,"a")&&F=="click"))&&G["on"+F]&&G["on"+F].apply(G,J)===false){H.result=false}if(!D&&G[F]&&!H.isDefaultPrevented()&&!(n.nodeName(G,"a")&&F=="click")){this.triggered=true;try{G[F]()}catch(K){}}this.triggered=false;if(!H.isPropagationStopped()){var E=G.parentNode||G.ownerDocument;if(E){n.event.trigger(H,J,E,true)}}},handle:function(J){var I,D;J=arguments[0]=n.event.fix(J||l.event);var K=J.type.split(".");J.type=K.shift();I=!K.length&&!J.exclusive;var H=RegExp("(^|\\.)"+K.slice().sort().join(".*\\.")+"(\\.|$)");D=(n.data(this,"events")||{})[J.type];for(var F in D){var G=D[F];if(I||H.test(G.type)){J.handler=G;J.data=G.data;var E=G.apply(this,arguments);if(E!==g){J.result=E;if(E===false){J.preventDefault();J.stopPropagation()}}if(J.isImmediatePropagationStopped()){break}}}},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(G){if(G[h]){return G}var E=G;G=n.Event(E);for(var F=this.props.length,I;F;){I=this.props[--F];G[I]=E[I]}if(!G.target){G.target=G.srcElement||document}if(G.target.nodeType==3){G.target=G.target.parentNode}if(!G.relatedTarget&&G.fromElement){G.relatedTarget=G.fromElement==G.target?G.toElement:G.fromElement}if(G.pageX==null&&G.clientX!=null){var H=document.documentElement,D=document.body;G.pageX=G.clientX+(H&&H.scrollLeft||D&&D.scrollLeft||0)-(H.clientLeft||0);G.pageY=G.clientY+(H&&H.scrollTop||D&&D.scrollTop||0)-(H.clientTop||0)}if(!G.which&&((G.charCode||G.charCode===0)?G.charCode:G.keyCode)){G.which=G.charCode||G.keyCode}if(!G.metaKey&&G.ctrlKey){G.metaKey=G.ctrlKey}if(!G.which&&G.button){G.which=(G.button&1?1:(G.button&2?3:(G.button&4?2:0)))}return G},proxy:function(E,D){D=D||function(){return E.apply(this,arguments)};D.guid=E.guid=E.guid||D.guid||this.guid++;return D},special:{ready:{setup:A,teardown:function(){}}},specialAll:{live:{setup:function(D,E){n.event.add(this,E[0],c)},teardown:function(F){if(F.length){var D=0,E=RegExp("(^|\\.)"+F[0]+"(\\.|$)");n.each((n.data(this,"events").live||{}),function(){if(E.test(this.type)){D++}});if(D<1){n.event.remove(this,F[0],c)}}}}}};n.Event=function(D){if(!this.preventDefault){return new n.Event(D)}if(D&&D.type){this.originalEvent=D;this.type=D.type;this.timeStamp=D.timeStamp}else{this.type=D}if(!this.timeStamp){this.timeStamp=e()}this[h]=true};function k(){return false}function t(){return true}n.Event.prototype={preventDefault:function(){this.isDefaultPrevented=t;var D=this.originalEvent;if(!D){return}if(D.preventDefault){D.preventDefault()}D.returnValue=false},stopPropagation:function(){this.isPropagationStopped=t;var D=this.originalEvent;if(!D){return}if(D.stopPropagation){D.stopPropagation()}D.cancelBubble=true},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=t;this.stopPropagation()},isDefaultPrevented:k,isPropagationStopped:k,isImmediatePropagationStopped:k};var a=function(E){var D=E.relatedTarget;while(D&&D!=this){try{D=D.parentNode}catch(F){D=this}}if(D!=this){E.type=E.data;n.event.handle.apply(this,arguments)}};n.each({mouseover:"mouseenter",mouseout:"mouseleave"},function(E,D){n.event.special[D]={setup:function(){n.event.add(this,E,a,D)},teardown:function(){n.event.remove(this,E,a)}}});n.fn.extend({bind:function(E,F,D){return E=="unloaddisabled"?this.one(E,F,D):this.each(function(){n.event.add(this,E,D||F,D&&F)})},one:function(F,G,E){var D=n.event.proxy(E||G,function(H){n(this).unbind(H,D);return(E||G).apply(this,arguments)});return this.each(function(){n.event.add(this,F,D,E&&G)})},unbind:function(E,D){return this.each(function(){n.event.remove(this,E,D)})},trigger:function(D,E){return this.each(function(){n.event.trigger(D,E,this)})},triggerHandler:function(D,F){if(this[0]){var E=n.Event(D);E.preventDefault();E.stopPropagation();n.event.trigger(E,F,this[0]);return E.result}},toggle:function(F){var D=arguments,E=1;while(E<D.length){n.event.proxy(F,D[E++])}return this.click(n.event.proxy(F,function(G){this.lastToggle=(this.lastToggle||0)%E;G.preventDefault();return D[this.lastToggle++].apply(this,arguments)||false}))},hover:function(D,E){return this.mouseenter(D).mouseleave(E)},ready:function(D){A();if(n.isReady){D.call(document,n)}else{n.readyList.push(D)}return this},live:function(F,E){var D=n.event.proxy(E);D.guid+=this.selector+F;n(document).bind(i(F,this.selector),this.selector,D);return this},die:function(E,D){n(document).unbind(i(E,this.selector),D?{guid:D.guid+this.selector+E}:null);return this}});function c(G){var D=RegExp("(^|\\.)"+G.type+"(\\.|$)"),F=true,E=[];n.each(n.data(this,"events").live||[],function(H,I){if(D.test(I.type)){var J=n(G.target).closest(I.data)[0];if(J){E.push({elem:J,fn:I})}}});n.each(E,function(){if(!G.isImmediatePropagationStopped()&&this.fn.call(this.elem,G,this.fn.data)===false){F=false}});return F}function i(E,D){return["live",E,D.replace(/\./g,"`").replace(/ /g,"|")].join(".")}n.extend({isReady:false,readyList:[],ready:function(){if(!n.isReady){n.isReady=true;if(n.readyList){n.each(n.readyList,function(){this.call(document,n)});n.readyList=null}n(document).triggerHandler("ready")}}});var w=false;function A(){if(w){return}w=true;if(document.addEventListener){document.addEventListener("DOMContentLoaded",function(){document.removeEventListener("DOMContentLoaded",arguments.callee,false);n.ready()},false)}else{if(document.attachEvent){document.attachEvent("onreadystatechange",function(){if(document.readyState==="complete"){document.detachEvent("onreadystatechange",arguments.callee);n.ready()}});if(document.documentElement.doScroll&&!l.frameElement){(function(){if(n.isReady){return}try{document.documentElement.doScroll("left")}catch(D){setTimeout(arguments.callee,0);return}n.ready()})()}}}n.event.add(l,"loaddisabled",n.ready)}n.each(("blur,focus,loaddisabled,resize,scroll,unloaddisabled,click,dblclick,mousedown,mouseup,mousemove,mouseover,mouseout,mouseenter,mouseleave,change,select,submit,keydown,keypress,keyup,error").split(","),function(E,D){n.fn[D]=function(F){return F?this.bind(D,F):this.trigger(D)}});n(l).bind("unloaddisabled",function(){for(var D in n.cache){if(D!=1&&n.cache[D].handle){n.event.remove(n.cache[D].handle.elem)}}});(function(){n.support={};var E=document.documentElement,F=document.createElement("script"),J=document.createElement("div"),I="script"+(new Date).getTime();J.style.display="none";J.innerHTML=' <link/><table></table><a href="/a" style="color:red;float:left;opacity:.5;">a</a><select><option>text</option></select><objectdisabled><param/></object>';var G=J.getElementsByTagName("*"),D=J.getElementsByTagName("a")[0];if(!G||!G.length||!D){return}n.support={leadingWhitespace:J.firstChild.nodeType==3,tbody:!J.getElementsByTagName("tbody").length,objectAll:!!J.getElementsByTagName("object")[0].getElementsByTagName("*").length,htmlSerialize:!!J.getElementsByTagName("link").length,style:/red/.test(D.getAttribute("style")),hrefNormalized:D.getAttribute("href")==="/a",opacity:D.style.opacity==="0.5",cssFloat:!!D.style.cssFloat,scriptEval:false,noCloneEvent:true,boxModel:null};F.type="text/javascript";try{F.appendChild(document.createTextNode("window."+I+"=1;"))}catch(H){}E.insertBefore(F,E.firstChild);if(l[I]){n.support.scriptEval=true;delete l[I]}E.removeChild(F);if(J.attachEvent&&J.fireEvent){J.attachEvent("onclick",function(){n.support.noCloneEvent=false;J.detachEvent("onclick",arguments.callee)});J.cloneNode(true).fireEvent("onclick")}n(function(){var K=document.createElement("div");K.style.width="1px";K.style.paddingLeft="1px";document.body.appendChild(K);n.boxModel=n.support.boxModel=K.offsetWidth===2;document.body.removeChild(K)})})();var v=n.support.cssFloat?"cssFloat":"styleFloat";n.props={"for":"htmlFor","class":"className","float":v,cssFloat:v,styleFloat:v,readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",tabindex:"tabIndex"};n.fn.extend({_loaddisabled:n.fn.loaddisabled,loaddisabled:function(F,I,J){if(typeof F!=="string"){return this._loaddisabled(F)}var H=F.indexOf(" ");if(H>=0){var D=F.slice(H,F.length);F=F.slice(0,H)}var G="GET";if(I){if(n.isFunction(I)){J=I;I=null}else{if(typeof I==="object"){I=n.param(I);G="POST"}}}var E=this;n.ajax({url:F,type:G,dataType:"html",data:I,complete:function(L,K){if(K=="success"||K=="notmodified"){E.html(D?n("<div/>").append(L.responseText.replace(/<script(.|\s)*?\/script>/g,"")).find(D):L.responseText)}if(J){E.each(J,[L.responseText,K,L])}}});return this},serialize:function(){return n.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?n.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password/i.test(this.type))}).map(function(D,E){var F=n(this).val();return F==null?null:n.isArray(F)?n.map(F,function(H,G){return{name:E.name,value:H}}):{name:E.name,value:F}}).get()}});n.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(D,E){n.fn[E]=function(F){return this.bind(E,F)}});var q=e();n.extend({get:function(D,F,G,E){if(n.isFunction(F)){G=F;F=null}return n.ajax({type:"GET",url:D,data:F,success:G,dataType:E})},getScript:function(D,E){return n.get(D,null,E,"script")},getJSON:function(D,E,F){return n.get(D,E,F,"json")},post:function(D,F,G,E){if(n.isFunction(F)){G=F;F={}}return n.ajax({type:"POST",url:D,data:F,success:G,dataType:E})},ajaxSetup:function(D){n.extend(n.ajaxSettings,D)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:function(){return l.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest()},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},ajax:function(L){L=n.extend(true,L,n.extend(true,{},n.ajaxSettings,L));var V,E=/=\?(&|$)/g,Q,U,F=L.type.toUpperCase();if(L.data&&L.processData&&typeof L.data!=="string"){L.data=n.param(L.data)}if(L.dataType=="jsonp"){if(F=="GET"){if(!L.url.match(E)){L.url+=(L.url.match(/\?/)?"&":"?")+(L.jsonp||"callback")+"=?"}}else{if(!L.data||!L.data.match(E)){L.data=(L.data?L.data+"&":"")+(L.jsonp||"callback")+"=?"}}L.dataType="json"}if(L.dataType=="json"&&(L.data&&L.data.match(E)||L.url.match(E))){V="jsonp"+q++;if(L.data){L.data=(L.data+"").replace(E,"="+V+"$1")}L.url=L.url.replace(E,"="+V+"$1");L.dataType="script";l[V]=function(W){U=W;H();K();l[V]=g;try{delete l[V]}catch(X){}if(G){G.removeChild(S)}}}if(L.dataType=="script"&&L.cache==null){L.cache=false}if(L.cache===false&&F=="GET"){var D=e();var T=L.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+D+"$2");L.url=T+((T==L.url)?(L.url.match(/\?/)?"&":"?")+"_="+D:"")}if(L.data&&F=="GET"){L.url+=(L.url.match(/\?/)?"&":"?")+L.data;L.data=null}if(L.global&&!n.active++){n.event.trigger("ajaxStart")}var P=/^(\w+:)?\/\/([^\/?#]+)/.exec(L.url);if(L.dataType=="script"&&F=="GET"&&P&&(P[1]&&P[1]!=location.protocol||P[2]!=location.host)){var G=document.getElementsByTagName("head")[0];var S=document.createElement("script");S.src=L.url;if(L.scriptCharset){S.charset=L.scriptCharset}if(!V){var N=false;S.onloaddisabled=S.onreadystatechange=function(){if(!N&&(!this.readyState||this.readyState=="loaddisableded"||this.readyState=="complete")){N=true;H();K();G.removeChild(S)}}}G.appendChild(S);return g}var J=false;var I=L.xhr();if(L.username){void(F,L.url,L.async,L.username,L.password)}else{void(F,L.url,L.async)}try{if(L.data){I.setRequestHeader("Content-Type",L.contentType)}if(L.ifModified){I.setRequestHeader("If-Modified-Since",n.lastModified[L.url]||"Thu, 01 Jan 1970 00:00:00 GMT")}I.setRequestHeader("X-Requested-With","XMLHttpRequest");I.setRequestHeader("Accept",L.dataType&&L.accepts[L.dataType]?L.accepts[L.dataType]+", */*":L.accepts._default)}catch(R){}if(L.beforeSend&&L.beforeSend(I,L)===false){if(L.global&&!--n.active){n.event.trigger("ajaxStop")}I.abort();return false}if(L.global){n.event.trigger("ajaxSend",[I,L])}var M=function(W){if(I.readyState==0){if(O){clearInterval(O);O=null;if(L.global&&!--n.active){n.event.trigger("ajaxStop")}}}else{if(!J&&I&&(I.readyState==4||W=="timeout")){J=true;if(O){clearInterval(O);O=null}Q=W=="timeout"?"timeout":!n.httpSuccess(I)?"error":L.ifModified&&n.httpNotModified(I,L.url)?"notmodified":"success";if(Q=="success"){try{U=n.httpData(I,L.dataType,L)}catch(Y){Q="parsererror"}}if(Q=="success"){var X;try{X=I.getResponseHeader("Last-Modified")}catch(Y){}if(L.ifModified&&X){n.lastModified[L.url]=X}if(!V){H()}}else{n.handleError(L,I,Q)}K();if(L.async){I=null}}}};if(L.async){var O=setInterval(M,13);if(L.timeout>0){setTimeout(function(){if(I){if(!J){M("timeout")}if(I){I.abort()}}},L.timeout)}}try{I.send(L.data)}catch(R){n.handleError(L,I,null,R)}if(!L.async){M()}function H(){if(L.success){L.success(U,Q)}if(L.global){n.event.trigger("ajaxSuccess",[I,L])}}function K(){if(L.complete){L.complete(I,Q)}if(L.global){n.event.trigger("ajaxComplete",[I,L])}if(L.global&&!--n.active){n.event.trigger("ajaxStop")}}return I},handleError:function(E,G,D,F){if(E.error){E.error(G,D,F)}if(E.global){n.event.trigger("ajaxError",[G,E,F])}},active:0,httpSuccess:function(E){try{return !E.status&&location.protocol=="file:"||(E.status>=200&&E.status<300)||E.status==304||E.status==1223}catch(D){}return false},httpNotModified:function(F,D){try{var G=F.getResponseHeader("Last-Modified");return F.status==304||G==n.lastModified[D]}catch(E){}return false},httpData:function(I,G,F){var E=I.getResponseHeader("content-type"),D=G=="xml"||!G&&E&&E.indexOf("xml")>=0,H=D?I.responseXML:I.responseText;if(D&&H.documentElement.tagName=="parsererror"){throw"parsererror"}if(F&&F.dataFilter){H=F.dataFilter(H,G)}if(typeof H==="string"){if(G=="script"){n.globalEval(H)}if(G=="json"){H=l["eval"]("("+H+")")}}return H},param:function(D){var F=[];function G(H,I){F[F.length]=encodeURIComponent(H)+"="+encodeURIComponent(I)}if(n.isArray(D)||D.jquery){n.each(D,function(){G(this.name,this.value)})}else{for(var E in D){if(n.isArray(D[E])){n.each(D[E],function(){G(E,this)})}else{G(E,n.isFunction(D[E])?D[E]():D[E])}}}return F.join("&").replace(/%20/g,"+")}});var m={},d=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];function s(E,D){var F={};n.each(d.concat.apply([],d.slice(0,D)),function(){F[this]=E});return F}n.fn.extend({show:function(I,K){if(I){return this.animate(s("show",3),I,K)}else{for(var G=0,E=this.length;G<E;G++){var D=n.data(this[G],"olddisplay");this[G].style.display=D||"";if(n.css(this[G],"display")==="none"){var F=this[G].tagName,J;if(m[F]){J=m[F]}else{var H=n("<"+F+" />").appendTo("body");J=H.css("display");if(J==="none"){J="block"}H.remove();m[F]=J}this[G].style.display=n.data(this[G],"olddisplay",J)}}return this}},hide:function(G,H){if(G){return this.animate(s("hide",3),G,H)}else{for(var F=0,E=this.length;F<E;F++){var D=n.data(this[F],"olddisplay");if(!D&&D!=="none"){n.data(this[F],"olddisplay",n.css(this[F],"display"))}this[F].style.display="none"}return this}},_toggle:n.fn.toggle,toggle:function(F,E){var D=typeof F==="boolean";return n.isFunction(F)&&n.isFunction(E)?this._toggle.apply(this,arguments):F==null||D?this.each(function(){var G=D?F:n(this).is(":hidden");n(this)[G?"show":"hide"]()}):this.animate(s("toggle",3),F,E)},fadeTo:function(D,F,E){return this.animate({opacity:F},D,E)},animate:function(H,E,G,F){var D=n.speed(E,G,F);return this[D.queue===false?"each":"queue"](function(){var J=n.extend({},D),L,K=this.nodeType==1&&n(this).is(":hidden"),I=this;for(L in H){if(H[L]=="hide"&&K||H[L]=="show"&&!K){return J.complete.call(this)}if((L=="height"||L=="width")&&this.style){J.display=n.css(this,"display");J.overflow=this.style.overflow}}if(J.overflow!=null){this.style.overflow="hidden"}J.curAnim=n.extend({},H);n.each(H,function(N,R){var Q=new n.fx(I,J,N);if(/toggle|show|hide/.test(R)){Q[R=="toggle"?K?"show":"hide":R](H)}else{var P=R.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),S=Q.cur(true)||0;if(P){var M=parseFloat(P[2]),O=P[3]||"px";if(O!="px"){I.style[N]=(M||1)+O;S=((M||1)/Q.cur(true))*S;I.style[N]=S+O}if(P[1]){M=((P[1]=="-="?-1:1)*M)+S}Q.custom(S,M,O)}else{Q.custom(S,R,"")}}});return true})},stop:function(E,D){var F=n.timers;if(E){this.queue([])}this.each(function(){for(var G=F.length-1;G>=0;G--){if(F[G].elem==this){if(D){F[G](true)}F.splice(G,1)}}});if(!D){this.dequeue()}return this}});n.each({slideDown:s("show",1),slideUp:s("hide",1),slideToggle:s("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(D,E){n.fn[D]=function(F,G){return this.animate(E,F,G)}});n.extend({speed:function(F,G,E){var D=typeof F==="object"?F:{complete:E||!E&&G||n.isFunction(F)&&F,duration:F,easing:E&&G||G&&!n.isFunction(G)&&G};D.duration=n.fx.off?0:typeof D.duration==="number"?D.duration:n.fx.speeds[D.duration]||n.fx.speeds._default;D.old=D.complete;D.complete=function(){if(D.queue!==false){n(this).dequeue()}if(n.isFunction(D.old)){D.old.call(this)}};return D},easing:{linear:function(F,G,D,E){return D+E*F},swing:function(F,G,D,E){return((-Math.cos(F*Math.PI)/2)+0.5)*E+D}},timers:[],timerId:null,fx:function(E,D,F){this.options=D;this.elem=E;this.prop=F;if(!D.orig){D.orig={}}}});n.fx.prototype={update:function(){if(this.options.step){this.options.step.call(this.elem,this.now,this)}(n.fx.step[this.prop]||n.fx.step._default)(this);if((this.prop=="height"||this.prop=="width")&&this.elem.style){this.elem.style.display="block"}},cur:function(E){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null)){return this.elem[this.prop]}var D=parseFloat(n.css(this.elem,this.prop,E));return D&&D>-10000?D:parseFloat(n.curCSS(this.elem,this.prop))||0},custom:function(H,G,F){this.startTime=e();this.start=H;this.end=G;this.unit=F||this.unit||"px";this.now=this.start;this.pos=this.state=0;var D=this;function E(I){return D.step(I)}E.elem=this.elem;n.timers.push(E);if(E()&&n.timerId==null){n.timerId=setInterval(function(){var J=n.timers;for(var I=0;I<J.length;I++){if(!J[I]()){J.splice(I--,1)}}if(!J.length){clearInterval(n.timerId);n.timerId=null}},13)}},show:function(){this.options.orig[this.prop]=n.attr(this.elem.style,this.prop);this.options.show=true;this.custom(this.prop=="width"||this.prop=="height"?1:0,this.cur());n(this.elem).show()},hide:function(){this.options.orig[this.prop]=n.attr(this.elem.style,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(G){var F=e();if(G||F>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var D=true;for(var E in this.options.curAnim){if(this.options.curAnim[E]!==true){D=false}}if(D){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(n.css(this.elem,"display")=="none"){this.elem.style.display="block"}}if(this.options.hide){n(this.elem).hide()}if(this.options.hide||this.options.show){for(var H in this.options.curAnim){n.attr(this.elem.style,H,this.options.orig[H])}}}if(D){this.options.complete.call(this.elem)}return false}else{var I=F-this.startTime;this.state=I/this.options.duration;this.pos=n.easing[this.options.easing||(n.easing.swing?"swing":"linear")](this.state,I,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update()}return true}};n.extend(n.fx,{speeds:{slow:600,fast:200,_default:400},step:{opacity:function(D){n.attr(D.elem.style,"opacity",D.now)},_default:function(D){if(D.elem.style&&D.elem.style[D.prop]!=null){D.elem.style[D.prop]=D.now+D.unit}else{D.elem[D.prop]=D.now}}}});if(document.documentElement.getBoundingClientRect){n.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return n.offset.bodyOffset(this[0])}var F=this[0].getBoundingClientRect(),I=this[0].ownerDocument,E=I.body,D=I.documentElement,K=D.clientTop||E.clientTop||0,J=D.clientLeft||E.clientLeft||0,H=F.top+(self.pageYOffset||n.boxModel&&D.scrollTop||E.scrollTop)-K,G=F.left+(self.pageXOffset||n.boxModel&&D.scrollLeft||E.scrollLeft)-J;return{top:H,left:G}}}else{n.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return n.offset.bodyOffset(this[0])}n.offset.initialized||n.offset.initialize();var I=this[0],F=I.offsetParent,E=I,N=I.ownerDocument,L,G=N.documentElement,J=N.body,K=N.defaultView,D=K.getComputedStyle(I,null),M=I.offsetTop,H=I.offsetLeft;while((I=I.parentNode)&&I!==J&&I!==G){L=K.getComputedStyle(I,null);M-=I.scrollTop,H-=I.scrollLeft;if(I===F){M+=I.offsetTop,H+=I.offsetLeft;if(n.offset.doesNotAddBorder&&!(n.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(I.tagName))){M+=parseInt(L.borderTopWidth,10)||0,H+=parseInt(L.borderLeftWidth,10)||0}E=F,F=I.offsetParent}if(n.offset.subtractsBorderForOverflowNotVisible&&L.overflow!=="visible"){M+=parseInt(L.borderTopWidth,10)||0,H+=parseInt(L.borderLeftWidth,10)||0}D=L}if(D.position==="relative"||D.position==="static"){M+=J.offsetTop,H+=J.offsetLeft}if(D.position==="fixed"){M+=Math.max(G.scrollTop,J.scrollTop),H+=Math.max(G.scrollLeft,J.scrollLeft)}return{top:M,left:H}}}n.offset={initialize:function(){if(this.initialized){return}var K=document.body,E=document.createElement("div"),G,F,M,H,L,D,I=K.style.marginTop,J='<div style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;"><div></div></div><table style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;"cellpadding="0"cellspacing="0"><tr><td></td></tr></table>';L={position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"};for(D in L){E.style[D]=L[D]}E.innerHTML=J;K.insertBefore(E,K.firstChild);G=E.firstChild,F=G.firstChild,H=G.nextSibling.firstChild.firstChild;this.doesNotAddBorder=(F.offsetTop!==5);this.doesAddBorderForTableAndCells=(H.offsetTop===5);G.style.overflow="hidden",G.style.position="relative";this.subtractsBorderForOverflowNotVisible=(F.offsetTop===-5);K.style.marginTop="1px";this.doesNotIncludeMarginInBodyOffset=(K.offsetTop===0);K.style.marginTop=I;K.removeChild(E);this.initialized=true},bodyOffset:function(D){n.offset.initialized||n.offset.initialize();var F=D.offsetTop,E=D.offsetLeft;if(n.offset.doesNotIncludeMarginInBodyOffset){F+=parseInt(n.curCSS(D,"marginTop",true),10)||0,E+=parseInt(n.curCSS(D,"marginLeft",true),10)||0}return{top:F,left:E}}};n.fn.extend({position:function(){var H=0,G=0,E;if(this[0]){var F=this.offsetParent(),I=this.offset(),D=/^body|html$/i.test(F[0].tagName)?{top:0,left:0}:F.offset();I.top-=j(this,"marginTop");I.left-=j(this,"marginLeft");D.top+=j(F,"borderTopWidth");D.left+=j(F,"borderLeftWidth");E={top:I.top-D.top,left:I.left-D.left}}return E},offsetParent:function(){var D=this[0].offsetParent||document.body;while(D&&(!/^body|html$/i.test(D.tagName)&&n.css(D,"position")=="static")){D=D.offsetParent}return n(D)}});n.each(["Left","Top"],function(E,D){var F="scroll"+D;n.fn[F]=function(G){if(!this[0]){return null}return G!==g?this.each(function(){this==l||this==document?l.scrollTo(!E?G:n(l).scrollLeft(),E?G:n(l).scrollTop()):this[F]=G}):this[0]==l||this[0]==document?self[E?"pageYOffset":"pageXOffset"]||n.boxModel&&document.documentElement[F]||document.body[F]:this[0][F]}});n.each(["Height","Width"],function(G,E){var D=G?"Left":"Top",F=G?"Right":"Bottom";n.fn["inner"+E]=function(){return this[E.toLowerCase()]()+j(this,"padding"+D)+j(this,"padding"+F)};n.fn["outer"+E]=function(I){return this["inner"+E]()+j(this,"border"+D+"Width")+j(this,"border"+F+"Width")+(I?j(this,"margin"+D)+j(this,"margin"+F):0)};var H=E.toLowerCase();n.fn[H]=function(I){return this[0]==l?document.compatMode=="CSS1Compat"&&document.documentElement["client"+E]||document.body["client"+E]:this[0]==document?Math.max(document.documentElement["client"+E],document.body["scroll"+E],document.documentElement["scroll"+E],document.body["offset"+E],document.documentElement["offset"+E]):I===g?(this.length?n.css(this[0],H):null):this.css(H,typeof I==="string"?I:I+"px")}})})(); \ No newline at end of file
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/twitter.com/ICHCheezburger.html b/mobile/android/tests/browser/chrome/tp5/twitter.com/twitter.com/ICHCheezburger.html
new file mode 100755
index 0000000000..8b36bb3a8f
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/twitter.com/ICHCheezburger.html
@@ -0,0 +1,1203 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "httpdisabled://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="httpdisabled://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+ <head>
+ <meta http-equiv="X-UA-Compatible" content="IE=8" />
+
+
+ <script type="text/javascript">
+//<![CDATA[
+(function(g){var a=location.href.split("#!")[1];if(a){window.location.hash = "";g.location.pathname = g.HBR = a.replace(/^([^/])/,"/$1");}})(window);
+//]]>
+</script>
+ <script type="text/javascript" charset="utf-8">
+ if (!twttr) {
+ var twttr = {}
+ }
+
+ // Benchmarking loaddisabled time.
+ // twttr.timeTillReadyUnique = '1302298738-54111-580';
+ // twttr.timeTillReadyStart = new Date().getTime();
+ </script>
+
+ <script type="text/javascript">
+//<![CDATA[
+var page={};var onCondition=function(D,C,A,B){D=D;A=A?Math.min(A,5):5;B=B||100;if(D()){C()}else{if(A>1){setTimeout(function(){onCondition(D,C,A-1,B)},B)}}};
+//]]>
+</script>
+ <meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
+<meta content="en-us" http-equiv="Content-Language" />
+<meta content="ICanHasCheezburger? (ICHCheezburger) is on Twitter. Sign up for Twitter to follow ICanHasCheezburger? (ICHCheezburger) and get their latest updates" name="description" />
+<meta content="no" http-equiv="imagetoolbar" />
+<meta content="width = 780" name="viewport" />
+<meta content="4FTTxY4uvo0RZTMQqIyhh18HsepyJOctQ+XTOu1zsfE=" name="verify-v1" />
+<meta content="1" name="page" />
+<meta content="NOODP" name="robots" />
+<meta content="n" name="session-loggedin" />
+<meta content="ICHCheezburger" name="page-user-screen_name" />
+ <title id="page_title">ICanHasCheezburger? (ICHCheezburger) on Twitter</title>
+ <link href="httpdisabled://a1.twimg.com/a/1302214109/images/twitter_57.png" rel="apple-touch-icon" />
+<link href="httpdisabled://twitter.com/oexchange.xrd" rel="httpdisabled://oexchange.org/spec/0.8/rel/related-target" type="application/xrd+xml" />
+<link href="../a1.twimg.com/a/1302214109/images/favicon.ico" rel="shortcut icon" type="image/x-icon" />
+ <link rel="alternate" href="httpdisabled://twitter.com/statuses/user_timeline/6173842.rss" title="ICHCheezburger's Tweets" type="application/rss+xml" />
+ <link rel="alternate" href="httpdisabled://twitter.com/favorites/6173842.rss" title="ICHCheezburger's Favorites" type="application/rss+xml" />
+
+
+ <link href="../a2.twimg.com/a/1302214109/stylesheets/twitter.css@1302114648.css" media="screen" rel="stylesheet" type="text/css" />
+<link href="../a2.twimg.com/a/1302214109/stylesheets/geo.css@1302114648.css" media="screen" rel="stylesheet" type="text/css" />
+<link href="../a3.twimg.com/a/1302214109/stylesheets/buttons_new.css@1302114648.css" media="screen" rel="stylesheet" type="text/css" />
+ <style type="text/css">
+
+ body {
+ background: #9AE4E8 url('../a2.twimg.com/profile_background_images/30261844/ICHCTwitterBG.jpg') fixed no-repeat;
+
+}
+
+body#show #content .meta a.screen-name,
+#content .shared-content .screen-name,
+#content .meta .byline a {
+ color: #0000FF;
+}
+
+/* Link Color */
+a,
+#content tr.hentry:hover a,
+body#profile #content div.hentry:hover a,
+#side .stats a:hover span.stats_count,
+#side div.user_icon a:hover,
+li.verified-profile a:hover,
+#side .promotion .definition strong,
+p.list-numbers a:hover,
+#side div.user_icon a:hover span,
+#content .tabMenu li a,
+.translator-profile a:hover,
+#local_trend_locations li a,
+.modal-content .list-slug,
+.tweet-label a:hover,
+ol.statuses li.garuda-tweet:hover .actions-hover li span a,
+ol.statuses li.garuda-tweet .actions-hover li span a:hover {
+ color: #0000FF;
+}
+
+body,
+ul#tabMenu li a, #side .section h1,
+#side .stat a,
+#side .stats a span.stats_count,
+#side div.section-header h1,
+#side div.user_icon a,
+#side div.user_icon a:hover,
+#side div.section-header h3.faq-header,
+ul.sidebar-menu li.active a,
+li.verified-profile a,
+#side .promotion a,
+body #content .list-header h2,
+p.list-numbers a,
+.bar h3 label,
+body.timeline #content h1,
+.list-header h2 a span,
+#content .tabMenu li.active a,
+body#direct_messages #content .tabMenu #inbox_tab a,
+body#inbox #content .tabMenu #inbox_tab a,
+body#sent #content .tabMenu #sent_tab a,
+body#direct_messages #content .tabMenu #inbox_tab a,
+body#retweets_by_others #content .tabMenu #retweets_by_others_tab a,
+body#retweets #content .tabMenu #retweets_tab a,
+body#retweeted_by_others #content .tabMenu #retweeted_by_others_tab a,
+body#retweeted_of_mine #content .tabMenu #retweeted_of_mine_tab a,
+.translator-profile a,
+#owners_lists h2 a {
+ color: #000000;
+}
+
+.email-address-nag-banner {
+ border-bottom: solid 1px #87BC44;
+}
+#side_base {
+ border-left:1px solid #87BC44;
+ background-color: #E0FF92;
+}
+
+ul.sidebar-menu li.active a,
+ul.sidebar-menu li a:hover,
+#side div#custom_search.active,
+#side .promotion,
+.notify div {
+ background-color: #F4FFA6;
+}
+
+.list-header,
+.list-controls,
+ul.sidebar-list li.active a,
+ul.sidebar-list li a:hover,
+.list-header-inner {
+ background-color: #E0FF92 !important;
+}
+
+#side .actions,
+#side .promo,
+#design .side-section {
+ border: 1px solid #87BC44;
+}
+
+#side div.section-header h3 {
+ border-bottom: 1px solid #87BC44;
+}
+
+#side p.sidebar-location {
+ border-bottom: 1px dotted #87BC44;
+}
+
+#side hr {
+ background: #87BC44;
+ color: #87BC44;
+}
+
+ul.sidebar-menu li.loaddisableding a {
+ background: #F4FFA6 url('../a1.twimg.com/a/1302214109/images/spinner.gif') no-repeat 171px 0.5em !important;
+}
+
+#side .collapsible h2.sidebar-title {
+ background: transparent url('../a2.twimg.com/a/1302214109/images/toggle_up_dark.png') no-repeat center right !important;
+}
+
+#side .collapsible.collapsed h2.sidebar-title {
+ background: transparent url('../a1.twimg.com/a/1302214109/images/toggle_down_dark.png') no-repeat center right !important;
+}
+
+#side ul.lists-links li a em {
+ background: url('../a3.twimg.com/a/1302214109/images/arrow_right_dark.png') no-repeat left top;
+}
+
+#side span.pipe {
+ border-left:1px solid #87BC44;
+}
+
+#list_subscriptions span.view-all,
+#list_memberships span.view-all,
+#profile span.view-all,
+#profile_favorites span.view-all,
+#following span.view-all,
+#followers span.view-all {
+ border-left: 0;
+}
+
+a.edit-list {
+ border-right: 1px solid #87BC44 !important;
+}
+
+
+
+ </style>
+ <link href="../a1.twimg.com/a/1302214109/stylesheets/following.css@1302114648.css" media="screen, projection" rel="stylesheet" type="text/css" />
+
+ </head>
+
+ <body class="account firefox signin-island" id="profile"> <div class="fixed-banners">
+
+
+ </div>
+ <script type="text/javascript">
+//<![CDATA[
+document.domain = 'twitter.com';function fn(){void = "";window.top.location = window.self.location;setTimeout(function(){document.body.innerHTML = '';},0);window.self.onloaddisabled = function(evt){document.body.innerHTML = '';};}if(window.top !== window.self){try{if(window.top.location.host){}else{fn();}}catch(e){fn();}}
+//]]>
+</script>
+ <div id="dim-screen"></div>
+ <ul id="accessibility" class="offscreen">
+ <li><a href="ICHCheezburger.html#content" accesskey="0">Skip past navigation</a></li>
+ <li>On a mobile phone? Check out <a href="httpdisabled://m.twitter.com/">m.twitter.com</a>!</li>
+ <li><a href="ICHCheezburger.html#footer" accesskey="2">Skip to navigation</a></li>
+ <li><a href="ICHCheezburger.html#signin">Skip to sign in form</a></li>
+</ul>
+
+
+
+
+
+
+ <div id="container" class="subpage">
+ <span id="loaddisableder" style="display:none"><img alt="Loader" src="../a0.twimg.com/a/1302214109/images/loader.gif" /></span>
+
+ <div class="clearfix" id="header">
+ <a href="httpdisabled://twitter.com/" title="Twitter / Home" accesskey="1" id="logo">
+ <img alt="Twitter.com" src="../a0.twimg.com/a/1302214109/images/twitter_logo_header.png" />
+ </a>
+ <form method="post" id="sign_out_form" action="httpdisabled://twitter.com/sessions/destroy" style="display:none;">
+ <input name="authenticity_token" value="dd6c65b6f7e87a8a456d76d37264af8f195c7209" type="hidden"/>
+ </form>
+
+
+ <div id="signin_controls">
+ <span id="have_an_account">
+ Have an account?<a href="httpdisabled://twitter.com/login" class="signin" tabindex="3"><span>Sign in</span></a></span>
+ <div id="signin_menu" class="common-form standard-form offscreen">
+
+ <form method="post" id="signin" action="httpdisabledsdisabled://twitter.com/sessions">
+
+ <input id="authenticity_token" name="authenticity_token" type="hidden" value="dd6c65b6f7e87a8a456d76d37264af8f195c7209" /> <input id="return_to_ssl" name="return_to_ssl" type="hidden" value="false" />
+ <input id="redirect_after_login" name="redirect_after_login" type="hidden" value="/ICHCheezburger" /> <p class="textbox">
+ <label for="username">Username or email</label>
+ <input type="text" id="username" name="session[username_or_email]" value="" title="username" tabindex="4"/>
+ </p>
+
+ <p class="textbox">
+ <label for="password">Password</label>
+ <input type="password" id="password" name="session[password]" value="" title="password" tabindex="5"/>
+ </p>
+
+ <p class="remember">
+ <input type="submit" id="signin_submit" value="Sign in" tabindex="7"/>
+ <input type="checkbox" id="remember" name="remember_me" value="1" tabindex="6"/>
+ <label for="remember">Remember me</label>
+ </p>
+
+ <p class="forgot">
+ <a href="httpdisabled://twitter.com/account/resend_password" id="resend_password_link">Forgot password?</a>
+ </p>
+
+ <p class="forgot-username">
+ <a href="httpdisabled://twitter.com/account/resend_password" id="forgot_username_link" title="If you remember your password, try logging in with your email">Forgot username?</a>
+ </p>
+ <p class="complete">
+ <a href="httpdisabled://twitter.com/account/complete" id="account_complete_link">Already using Twitter on your phone?</a>
+ </p>
+ <input type="hidden" name="q" id="signin_q" value=""/>
+ </form>
+</div>
+
+</div>
+
+
+
+
+ </div>
+
+
+ <div id="profilebox_outer" class="home_page_new_home_page">
+ <div id="profilebox" class="clearfix">
+ <div id="profiletext">
+ <h1>
+ <span>Get short, timely messages from ICanHasCheezburger?.</span>
+ </h1>
+
+ <h2>Twitter is a rich source of instantly updated information. It's easy to stay updated on an incredibly wide variety of topics. <strong><a href='http://twitter.com/signup?follow=ICHCheezburger'>Join today</a></strong> and <strong>follow @ICHCheezburger</strong>.</h2>
+ </div>
+ <div id="profilebutton">
+ <form action="httpdisabled://twitter.com/signup" id="account_signup_form" method="get" name="account_signup_form"> <input id="follow" name="follow" type="hidden" value="ICHCheezburger" />
+ <input class="profilesubmit" id="profile_submit" name="commit" type="submit" value="Sign Up &rsaquo;" />
+ </form>
+ <p id="profilebox-mobile">
+ <span class="sms-follow-instructions">Get updates via SMS by texting <strong>follow ICHCheezburger</strong> to <strong>40404</strong> in the United States</span><br/>
+ <a id="sms_codes_link">
+ <span>Codes for other countries</span>
+ </a>
+ <div id="sms_codes">
+ <table celspacing="0" celpadding="0">
+ <thead>
+ <tr class="title">
+ <td colspan="3">Two-way (sending and receiving) short codes:</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <th class="sms-country">Country</th>
+ <th class="sms-code">Code</th>
+ <th class="sms-network">For customers of</th>
+ </tr>
+ <tr>
+ <td class="sms-country">Australia</td>
+ <td colspan="2" class="sms-code-network">
+ <ul>
+
+ <li>
+ <span class="sms-code">0198089488</span>
+ <span class="sms-network">Telstra</span>
+ </li>
+
+ </ul>
+ </td>
+</tr><tr>
+ <td class="sms-country">Canada</td>
+ <td colspan="2" class="sms-code-network">
+ <ul>
+
+ <li>
+ <span class="sms-code">21212</span>
+ <span class="sms-network">(any)</span>
+ </li>
+
+ </ul>
+ </td>
+</tr><tr>
+ <td class="sms-country">United Kingdom</td>
+ <td colspan="2" class="sms-code-network">
+ <ul>
+
+ <li>
+ <span class="sms-code">86444</span>
+ <span class="sms-network">Vodafone, Orange, 3, O2</span>
+ </li>
+
+ </ul>
+ </td>
+</tr><tr>
+ <td class="sms-country">Indonesia</td>
+ <td colspan="2" class="sms-code-network">
+ <ul>
+
+ <li>
+ <span class="sms-code">89887</span>
+ <span class="sms-network">AXIS, 3, Telkomsel</span>
+ </li>
+
+ </ul>
+ </td>
+</tr><tr>
+ <td class="sms-country">Ireland</td>
+ <td colspan="2" class="sms-code-network">
+ <ul>
+
+ <li>
+ <span class="sms-code">51210</span>
+ <span class="sms-network">O2</span>
+ </li>
+
+ </ul>
+ </td>
+</tr><tr>
+ <td class="sms-country">India</td>
+ <td colspan="2" class="sms-code-network">
+ <ul>
+
+ <li>
+ <span class="sms-code">53000</span>
+ <span class="sms-network">Bharti Airtel, Videocon</span>
+ </li>
+
+ </ul>
+ </td>
+</tr><tr>
+ <td class="sms-country">Jordan</td>
+ <td colspan="2" class="sms-code-network">
+ <ul>
+
+ <li>
+ <span class="sms-code">90903</span>
+ <span class="sms-network">Zain</span>
+ </li>
+
+ </ul>
+ </td>
+</tr><tr>
+ <td class="sms-country">New Zealand</td>
+ <td colspan="2" class="sms-code-network">
+ <ul>
+
+ <li>
+ <span class="sms-code">8987</span>
+ <span class="sms-network">Vodafone, Telecom NZ</span>
+ </li>
+
+ </ul>
+ </td>
+</tr><tr>
+ <td class="sms-country">United States</td>
+ <td colspan="2" class="sms-code-network">
+ <ul>
+
+ <li>
+ <span class="sms-code">40404</span>
+ <span class="sms-network">(any)</span>
+ </li>
+
+ </ul>
+ </td>
+</tr>
+ </tbody>
+ </table>
+</div>
+
+ </p>
+ </div>
+ </div>
+ </div>
+
+
+
+
+
+ <div class="content-bubble-arrow"></div>
+
+
+
+ <table cellspacing="0" class="columns">
+ <tbody>
+ <tr>
+ <td id="content" class="round-left column">
+ <div class="wrapper">
+
+
+
+
+
+
+
+
+ <div class="profile-user">
+ <div id="user_6173842" class="user ">
+ <h2 class="thumb clearfix">
+ <a href="httpdisabled://twitter.com/account/profile_image/ICHCheezburger?hreflang=en"><img alt="" border="0" height="73" id="profile-image" src="../a3.twimg.com/profile_images/1213876440/27539_32561485399_2579_n_bigger.jpeg" valign="middle" width="73" /></a>
+ <div class="screen-name">ICHCheezburger</div>
+ </h2>
+ </div>
+ </div>
+
+
+
+ <div id="similar_to_followed"></div>
+
+<div class="section">
+
+ <div id="timeline_heading" style="display: none;">
+ <h1 id="heading"></h1>
+ </div>
+ <ol id='timeline' class='statuses'>
+ <li class="hentry u-ICHCheezburger status latest-status" id="status_56469580993933312"
+>
+ <span class="status-body">
+ <span class="status-content">
+ <span class="entry-content">Kitteh Komic of teh Day: Dr. Cat Attempts Open Heart Surgery <a href="httpdisabled://dbl.chzb.gr/1c6rnc" class="tweet-url web" rel="nofollow" target="_blank">http://dbl.chzb.gr/1c6rnc</a></span>
+ </span>
+ <span class="meta entry-meta" data='{}'>
+ <a class="entry-date" rel="bookmark" href="httpdisabled://twitter.com/ICHCheezburger/status/56469580993933312">
+ <span class="published timestamp" data="{time:'Fri Apr 08 21:32:51 +0000 2011'}">6 minutes ago</span></a>
+ <span>via <a href="httpdisabled://www.hootsuite.com" rel="nofollow">HootSuite</a></span>
+
+ </span>
+
+ <ul class="meta-data clearfix">
+</ul>
+ </span>
+</li>
+ <li class="hentry u-ICHCheezburger status" id="status_56438617626771456"
+>
+ <span class="status-body">
+ <span class="status-content">
+ <span class="entry-content">Switch - funny pictures - SwitchLoL by: queenofcatz <a href="httpdisabled://dbl.chzb.gr/1c6mDc" class="tweet-url web" rel="nofollow" target="_blank">http://dbl.chzb.gr/1c6mDc</a></span>
+ </span>
+ <span class="meta entry-meta" data='{}'>
+ <a class="entry-date" rel="bookmark" href="httpdisabled://twitter.com/ICHCheezburger/status/56438617626771456">
+ <span class="published timestamp" data="{time:'Fri Apr 08 19:29:49 +0000 2011'}">about 2 hours ago</span></a>
+ <span>via <a href="httpdisabled://www.hootsuite.com" rel="nofollow">HootSuite</a></span>
+
+ </span>
+
+ <ul class="meta-data clearfix">
+</ul>
+ </span>
+</li>
+ <li class="hentry u-ICHCheezburger status" id="status_56424458193354752"
+>
+ <span class="status-body">
+ <span class="status-content">
+ <span class="entry-content">A Graph About Kittehs - Lolcats, cats and funny captions - A Graph About Kittehs <a href="httpdisabled://dbl.chzb.gr/1c6jed" class="tweet-url web" rel="nofollow" target="_blank">http://dbl.chzb.gr/1c6jed</a></span>
+ </span>
+ <span class="meta entry-meta" data='{}'>
+ <a class="entry-date" rel="bookmark" href="httpdisabled://twitter.com/ICHCheezburger/status/56424458193354752">
+ <span class="published timestamp" data="{time:'Fri Apr 08 18:33:33 +0000 2011'}">about 3 hours ago</span></a>
+ <span>via <a href="httpdisabled://www.hootsuite.com" rel="nofollow">HootSuite</a></span>
+
+ </span>
+
+ <ul class="meta-data clearfix">
+</ul>
+ </span>
+</li>
+ <li class="hentry u-ICHCheezburger status" id="status_56364595996135424"
+>
+ <span class="status-body">
+ <span class="status-content">
+ <span class="entry-content">MemeCats: The Revolution’s Underbelly - Lolcats, cats and funny captions - MemeCats: The Revolution's Underbel... <a href="httpdisabled://dbl.chzb.gr/1c66Xx" class="tweet-url web" rel="nofollow" target="_blank">http://dbl.chzb.gr/1c66Xx</a></span>
+ </span>
+ <span class="meta entry-meta" data='{}'>
+ <a class="entry-date" rel="bookmark" href="httpdisabled://twitter.com/ICHCheezburger/status/56364595996135424">
+ <span class="published timestamp" data="{time:'Fri Apr 08 14:35:41 +0000 2011'}">about 7 hours ago</span></a>
+ <span>via <a href="httpdisabled://www.hootsuite.com" rel="nofollow">HootSuite</a></span>
+
+ </span>
+
+ <ul class="meta-data clearfix">
+</ul>
+ </span>
+</li>
+ <li class="hentry u-ICHCheezburger status" id="status_56364595916447744"
+>
+ <span class="status-body">
+ <span class="status-content">
+ <span class="entry-content">Cuteness Scale: - funny pictures - Cuteness Scale: 0 to 10 I iz an elebentyLoL by: aNiMaNu <a href="httpdisabled://dbl.chzb.gr/1c66Xy" class="tweet-url web" rel="nofollow" target="_blank">http://dbl.chzb.gr/1c66Xy</a></span>
+ </span>
+ <span class="meta entry-meta" data='{}'>
+ <a class="entry-date" rel="bookmark" href="httpdisabled://twitter.com/ICHCheezburger/status/56364595916447744">
+ <span class="published timestamp" data="{time:'Fri Apr 08 14:35:41 +0000 2011'}">about 7 hours ago</span></a>
+ <span>via <a href="httpdisabled://www.hootsuite.com" rel="nofollow">HootSuite</a></span>
+
+ </span>
+
+ <ul class="meta-data clearfix">
+</ul>
+ </span>
+</li>
+ <li class="hentry u-ICHCheezburger status" id="status_56334436450578432"
+>
+ <span class="status-body">
+ <span class="status-content">
+ <span class="entry-content">VIDEO: Startled Kitteh is Startled - Lolcats, cats and funny captions - VIDEO: Startled Kitteh is Startled <a href="httpdisabled://dbl.chzb.gr/1c60Ms" class="tweet-url web" rel="nofollow" target="_blank">http://dbl.chzb.gr/1c60Ms</a></span>
+ </span>
+ <span class="meta entry-meta" data='{}'>
+ <a class="entry-date" rel="bookmark" href="httpdisabled://twitter.com/ICHCheezburger/status/56334436450578432">
+ <span class="published timestamp" data="{time:'Fri Apr 08 12:35:50 +0000 2011'}">about 9 hours ago</span></a>
+ <span>via <a href="httpdisabled://www.hootsuite.com" rel="nofollow">HootSuite</a></span>
+
+ </span>
+
+ <ul class="meta-data clearfix">
+</ul>
+ </span>
+</li>
+ <li class="hentry u-ICHCheezburger status" id="status_56302449992007680"
+>
+ <span class="status-body">
+ <span class="status-content">
+ <span class="entry-content">Hard work pays off in the long run. - funny pictures - Hard work pays off in the long run.LoL by: Chronocide <a href="httpdisabled://dbl.chzb.gr/1c5VDA" class="tweet-url web" rel="nofollow" target="_blank">http://dbl.chzb.gr/1c5VDA</a></span>
+ </span>
+ <span class="meta entry-meta" data='{}'>
+ <a class="entry-date" rel="bookmark" href="httpdisabled://twitter.com/ICHCheezburger/status/56302449992007680">
+ <span class="published timestamp" data="{time:'Fri Apr 08 10:28:44 +0000 2011'}">about 11 hours ago</span></a>
+ <span>via <a href="httpdisabled://www.hootsuite.com" rel="nofollow">HootSuite</a></span>
+
+ </span>
+
+ <ul class="meta-data clearfix">
+</ul>
+ </span>
+</li>
+ <li class="hentry u-ICHCheezburger status" id="status_56271721635921920"
+>
+ <span class="status-body">
+ <span class="status-content">
+ <span class="entry-content">Cyoot Kittehs of teh Day: We Liek Dis Place Wen Dere Iz No Watur In It <a href="httpdisabled://dbl.chzb.gr/1c5QKk" class="tweet-url web" rel="nofollow" target="_blank">http://dbl.chzb.gr/1c5QKk</a></span>
+ </span>
+ <span class="meta entry-meta" data='{}'>
+ <a class="entry-date" rel="bookmark" href="httpdisabled://twitter.com/ICHCheezburger/status/56271721635921920">
+ <span class="published timestamp" data="{time:'Fri Apr 08 08:26:38 +0000 2011'}">about 13 hours ago</span></a>
+ <span>via <a href="httpdisabled://www.hootsuite.com" rel="nofollow">HootSuite</a></span>
+
+ </span>
+
+ <ul class="meta-data clearfix">
+</ul>
+ </span>
+</li>
+ <li class="hentry u-ICHCheezburger status" id="status_56242086562902016"
+>
+ <span class="status-body">
+ <span class="status-content">
+ <span class="entry-content">~ I really didn’t need to see that! ~ - funny pictures - ~ I really didn't need to see that! ~LoL by: DyannLyn... <a href="httpdisabled://dbl.chzb.gr/1c5MkY" class="tweet-url web" rel="nofollow" target="_blank">http://dbl.chzb.gr/1c5MkY</a></span>
+ </span>
+ <span class="meta entry-meta" data='{}'>
+ <a class="entry-date" rel="bookmark" href="httpdisabled://twitter.com/ICHCheezburger/status/56242086562902016">
+ <span class="published timestamp" data="{time:'Fri Apr 08 06:28:53 +0000 2011'}">about 15 hours ago</span></a>
+ <span>via <a href="httpdisabled://www.hootsuite.com" rel="nofollow">HootSuite</a></span>
+
+ </span>
+
+ <ul class="meta-data clearfix">
+</ul>
+ </span>
+</li>
+ <li class="hentry u-ICHCheezburger status" id="status_56181651025305600"
+>
+ <span class="status-body">
+ <span class="status-content">
+ <span class="entry-content">VIDEO: Kitteh Hates Banana - Lolcats, cats and funny captions - VIDEO: Kitteh Hates Banana <a href="httpdisabled://dbl.chzb.gr/1c5DR9" class="tweet-url web" rel="nofollow" target="_blank">http://dbl.chzb.gr/1c5DR9</a></span>
+ </span>
+ <span class="meta entry-meta" data='{}'>
+ <a class="entry-date" rel="bookmark" href="httpdisabled://twitter.com/ICHCheezburger/status/56181651025305600">
+ <span class="published timestamp" data="{time:'Fri Apr 08 02:28:44 +0000 2011'}">about 19 hours ago</span></a>
+ <span>via <a href="httpdisabled://www.hootsuite.com" rel="nofollow">HootSuite</a></span>
+
+ </span>
+
+ <ul class="meta-data clearfix">
+</ul>
+ </span>
+</li>
+ <li class="hentry u-ICHCheezburger status" id="status_56152997683658754"
+>
+ <span class="status-body">
+ <span class="status-content">
+ <span class="entry-content">VIDEO: Awesome Astronaut Kitteh - Lolcats, cats and funny captions - VIDEO: Awesome Astronaut Kitteh <a href="httpdisabled://dbl.chzb.gr/1c5ztw" class="tweet-url web" rel="nofollow" target="_blank">http://dbl.chzb.gr/1c5ztw</a></span>
+ </span>
+ <span class="meta entry-meta" data='{}'>
+ <a class="entry-date" rel="bookmark" href="httpdisabled://twitter.com/ICHCheezburger/status/56152997683658754">
+ <span class="published timestamp" data="{time:'Fri Apr 08 00:34:52 +0000 2011'}">about 21 hours ago</span></a>
+ <span>via <a href="httpdisabled://www.hootsuite.com" rel="nofollow">HootSuite</a></span>
+
+ </span>
+
+ <ul class="meta-data clearfix">
+</ul>
+ </span>
+</li>
+ <li class="hentry u-ICHCheezburger status" id="status_56152997616553984"
+>
+ <span class="status-body">
+ <span class="status-content">
+ <span class="entry-content">I may be schizophrenic - funny pictures - I may be schizophrenicLoL by: eccarnahan <a href="httpdisabled://dbl.chzb.gr/1c5ztv" class="tweet-url web" rel="nofollow" target="_blank">http://dbl.chzb.gr/1c5ztv</a></span>
+ </span>
+ <span class="meta entry-meta" data='{}'>
+ <a class="entry-date" rel="bookmark" href="httpdisabled://twitter.com/ICHCheezburger/status/56152997616553984">
+ <span class="published timestamp" data="{time:'Fri Apr 08 00:34:52 +0000 2011'}">about 21 hours ago</span></a>
+ <span>via <a href="httpdisabled://www.hootsuite.com" rel="nofollow">HootSuite</a></span>
+
+ </span>
+
+ <ul class="meta-data clearfix">
+</ul>
+ </span>
+</li>
+ <li class="hentry u-ICHCheezburger status" id="status_56121527577493504"
+>
+ <span class="status-body">
+ <span class="status-content">
+ <span class="entry-content">Thesis - funny pictures - Thesis still not done, huh?LoL by: cinna-crumbs <a href="httpdisabled://dbl.chzb.gr/1c5seB" class="tweet-url web" rel="nofollow" target="_blank">http://dbl.chzb.gr/1c5seB</a></span>
+ </span>
+ <span class="meta entry-meta" data='{}'>
+ <a class="entry-date" rel="bookmark" href="httpdisabled://twitter.com/ICHCheezburger/status/56121527577493504">
+ <span class="published timestamp" data="{time:'Thu Apr 07 22:29:49 +0000 2011'}">about 23 hours ago</span></a>
+ <span>via <a href="httpdisabled://www.hootsuite.com" rel="nofollow">HootSuite</a></span>
+
+ </span>
+
+ <ul class="meta-data clearfix">
+</ul>
+ </span>
+</li>
+ <li class="hentry u-ICHCheezburger status" id="status_56107317090992128"
+>
+ <span class="status-body">
+ <span class="status-content">
+ <span class="entry-content">“Car†Is Just the Word “Cat†With One Letter Changed - Lolcats, cats and funny captions - Cat Car Decals - &quot;Ca... <a href="httpdisabled://dbl.chzb.gr/1c5ppW" class="tweet-url web" rel="nofollow" target="_blank">http://dbl.chzb.gr/1c5ppW</a></span>
+ </span>
+ <span class="meta entry-meta" data='{}'>
+ <a class="entry-date" rel="bookmark" href="httpdisabled://twitter.com/ICHCheezburger/status/56107317090992128">
+ <span class="published timestamp" data="{time:'Thu Apr 07 21:33:21 +0000 2011'}">2:33 PM Apr 7th</span></a>
+ <span>via <a href="httpdisabled://www.hootsuite.com" rel="nofollow">HootSuite</a></span>
+
+ </span>
+
+ <ul class="meta-data clearfix">
+</ul>
+ </span>
+</li>
+ <li class="hentry u-ICHCheezburger status" id="status_56107317065818112"
+>
+ <span class="status-body">
+ <span class="status-content">
+ <span class="entry-content">Cheezburger Confidential: Moral Gray Area Kitteh - Lolcats, cats and funny captions - Cheezburger Confidential... <a href="httpdisabled://dbl.chzb.gr/1c5ppX" class="tweet-url web" rel="nofollow" target="_blank">http://dbl.chzb.gr/1c5ppX</a></span>
+ </span>
+ <span class="meta entry-meta" data='{}'>
+ <a class="entry-date" rel="bookmark" href="httpdisabled://twitter.com/ICHCheezburger/status/56107317065818112">
+ <span class="published timestamp" data="{time:'Thu Apr 07 21:33:21 +0000 2011'}">2:33 PM Apr 7th</span></a>
+ <span>via <a href="httpdisabled://www.hootsuite.com" rel="nofollow">HootSuite</a></span>
+
+ </span>
+
+ <ul class="meta-data clearfix">
+</ul>
+ </span>
+</li>
+ <li class="hentry u-ICHCheezburger status" id="status_56091884069715968"
+>
+ <span class="status-body">
+ <span class="status-content">
+ <span class="entry-content">I think I work with her. - funny pictures - I think I work with her.LoL by: Winnie-Wonka <a href="httpdisabled://dbl.chzb.gr/1c5mEg" class="tweet-url web" rel="nofollow" target="_blank">http://dbl.chzb.gr/1c5mEg</a></span>
+ </span>
+ <span class="meta entry-meta" data='{}'>
+ <a class="entry-date" rel="bookmark" href="httpdisabled://twitter.com/ICHCheezburger/status/56091884069715968">
+ <span class="published timestamp" data="{time:'Thu Apr 07 20:32:01 +0000 2011'}">1:32 PM Apr 7th</span></a>
+ <span>via <a href="httpdisabled://www.hootsuite.com" rel="nofollow">HootSuite</a></span>
+
+ </span>
+
+ <ul class="meta-data clearfix">
+</ul>
+ </span>
+</li>
+ <li class="hentry u-ICHCheezburger status" id="status_56062544149880832"
+>
+ <span class="status-body">
+ <span class="status-content">
+ <span class="entry-content">O, The Places You’ll Go: De Poezenboot (The Cat Boat) - Lolcats, cats and funny captions - O, The Places You'l... <a href="httpdisabled://dbl.chzb.gr/1c5guP" class="tweet-url web" rel="nofollow" target="_blank">http://dbl.chzb.gr/1c5guP</a></span>
+ </span>
+ <span class="meta entry-meta" data='{}'>
+ <a class="entry-date" rel="bookmark" href="httpdisabled://twitter.com/ICHCheezburger/status/56062544149880832">
+ <span class="published timestamp" data="{time:'Thu Apr 07 18:35:26 +0000 2011'}">11:35 AM Apr 7th</span></a>
+ <span>via <a href="httpdisabled://www.hootsuite.com" rel="nofollow">HootSuite</a></span>
+
+ </span>
+
+ <ul class="meta-data clearfix">
+</ul>
+ </span>
+</li>
+ <li class="hentry u-ICHCheezburger status" id="status_56032664246956032"
+>
+ <span class="status-body">
+ <span class="status-content">
+ <span class="entry-content">nobudy putz - funny pictures - nobudy putz Babee in da cornerLoL by: NCcharmer <a href="httpdisabled://dbl.chzb.gr/1c59Q7" class="tweet-url web" rel="nofollow" target="_blank">http://dbl.chzb.gr/1c59Q7</a></span>
+ </span>
+ <span class="meta entry-meta" data='{}'>
+ <a class="entry-date" rel="bookmark" href="httpdisabled://twitter.com/ICHCheezburger/status/56032664246956032">
+ <span class="published timestamp" data="{time:'Thu Apr 07 16:36:42 +0000 2011'}">9:36 AM Apr 7th</span></a>
+ <span>via <a href="httpdisabled://www.hootsuite.com" rel="nofollow">HootSuite</a></span>
+
+ </span>
+
+ <ul class="meta-data clearfix">
+</ul>
+ </span>
+</li>
+ <li class="hentry u-ICHCheezburger status" id="status_56032663106093056"
+>
+ <span class="status-body">
+ <span class="status-content">
+ <span class="entry-content">GIF: Entertainin teh Childrenz - Lolcats, cats and funny captions - GIF: Entertainin teh Childrenz <a href="httpdisabled://dbl.chzb.gr/1c59Q8" class="tweet-url web" rel="nofollow" target="_blank">http://dbl.chzb.gr/1c59Q8</a></span>
+ </span>
+ <span class="meta entry-meta" data='{}'>
+ <a class="entry-date" rel="bookmark" href="httpdisabled://twitter.com/ICHCheezburger/status/56032663106093056">
+ <span class="published timestamp" data="{time:'Thu Apr 07 16:36:42 +0000 2011'}">9:36 AM Apr 7th</span></a>
+ <span>via <a href="httpdisabled://www.hootsuite.com" rel="nofollow">HootSuite</a></span>
+
+ </span>
+
+ <ul class="meta-data clearfix">
+</ul>
+ </span>
+</li>
+ <li class="hentry u-ICHCheezburger status" id="status_56001591689486336"
+>
+ <span class="status-body">
+ <span class="status-content">
+ <span class="entry-content">MemeCats: Hold All My Calls! - Lolcats, cats and funny captions - MemeCats: Hold All My Calls! <a href="httpdisabled://dbl.chzb.gr/1c5381" class="tweet-url web" rel="nofollow" target="_blank">http://dbl.chzb.gr/1c5381</a></span>
+ </span>
+ <span class="meta entry-meta" data='{}'>
+ <a class="entry-date" rel="bookmark" href="httpdisabled://twitter.com/ICHCheezburger/status/56001591689486336">
+ <span class="published timestamp" data="{time:'Thu Apr 07 14:33:14 +0000 2011'}">7:33 AM Apr 7th</span></a>
+ <span>via <a href="httpdisabled://www.hootsuite.com" rel="nofollow">HootSuite</a></span>
+
+ </span>
+
+ <ul class="meta-data clearfix">
+</ul>
+ </span>
+</li>
+ </ol>
+
+ <div id="pagination">
+ <a href="httpdisabled://twitter.com/ICHCheezburger?max_id=56469580993933312&amp;page=2&amp;twttr=true" class="round more" id="more" rel="next">more</a> </div>
+
+</div>
+
+
+
+
+
+ </div>
+ </td>
+
+ <td id="side_base" class="column round-right">
+
+ <div id="side">
+
+<div id="profile" class="section profile-side">
+ <span class="section-links">
+ </span>
+ <address>
+ <ul class="about vcard entry-author">
+
+
+
+ <li><span class="label">Name</span> <span class="fn">ICanHasCheezburger?</span></li>
+ <li><span class="label">Location</span> <span class="adr">Seattle, WA</span></li>
+ <li><span class="label">Web</span> <a href="httpdisabled://icanhascheezburger.com" class="url" rel="me nofollow" target="_blank">http://icanhasche...</a></li>
+ <li id="bio"><span class="label">Bio</span> <span class="bio">I can has funny pictures of cats, plz?</span></li>
+
+ </ul>
+ </address>
+
+
+
+<div class="stats">
+ <table>
+ <tr>
+ <td>
+
+
+
+<a href="httpdisabled://twitter.com/ICHCheezburger/following" id="following_count_link" class="link-following_page" rel="me" title="See who ICHCheezburger is following">
+ <span id="following_count" class="stats_count numeric">3,154 </span>
+ <span class="label">Following</span>
+</a>
+
+
+ </td>
+ <td>
+
+<a href="httpdisabled://twitter.com/ICHCheezburger/followers" id="follower_count_link" class="link-followers_page" rel="me" title="See who's following ICHCheezburger">
+ <span id="follower_count" class="stats_count numeric">1,588,880 </span>
+ <span class="label">Followers</span>
+</a>
+
+</td>
+ <td>
+
+<a href="httpdisabled://twitter.com/ICHCheezburger/lists/memberships" id="lists_count_link" class="link-lists_page" rel="me" title="See which lists ICHCheezburger is on">
+ <span id="lists_count" class="stats_count numeric">6,352 </span>
+ <span class="label">Listed</span>
+</a>
+
+</td>
+ </tr>
+ </table>
+
+</div>
+
+</div>
+
+ <ul id="primary_nav" class="sidebar-menu">
+ <li id="profile_tab"><a href="ICHCheezburger.html" accesskey="u"><span id="update_count" class="stat_count">9,375</span><span>Tweets</span></a></li>
+ <li id="profile_favorites_tab"><a href="httpdisabled://twitter.com/ICHCheezburger/favorites" accesskey="f"><span>Favorites</span></a></li>
+ </ul>
+
+
+
+
+ <hr/>
+ <div id="side_lists">
+ <h2 class="sidebar-title"><span>Lists</span></h2>
+
+ <ul class="sidebar-menu lists-links">
+ <li><a href="httpdisabled://twitter.com/ICHCheezburger/fail" class="list_661623" data="&#123;&quot;mode&quot;:&quot;public&quot;,&quot;id_str&quot;:&quot;661623&quot;,&quot;uri&quot;:&quot;\/ICHCheezburger\/fail&quot;,&quot;description&quot;:&quot;&quot;,&quot;dispatch_action&quot;:&quot;list&quot;,&quot;slug&quot;:&quot;fail&quot;,&quot;member_count&quot;:11,&quot;following&quot;:false,&quot;subscriber_count&quot;:104,&quot;full_name&quot;:&quot;@ICHCheezburger\/fail&quot;,&quot;name&quot;:&quot;fail&quot;,&quot;user&quot;:&quot;ICHCheezburger&quot;,&quot;id&quot;:661623&#125;" title="@ICHCheezburger/fail"><span>@ICHCheezburger/<wbr/><b>fail</b></span></a></li>
+<li><a href="httpdisabled://twitter.com/ICHCheezburger/network" class="list_622995" data="&#123;&quot;mode&quot;:&quot;public&quot;,&quot;id_str&quot;:&quot;622995&quot;,&quot;uri&quot;:&quot;\/ICHCheezburger\/network&quot;,&quot;description&quot;:&quot;&quot;,&quot;dispatch_action&quot;:&quot;list&quot;,&quot;slug&quot;:&quot;network&quot;,&quot;member_count&quot;:33,&quot;following&quot;:false,&quot;subscriber_count&quot;:136,&quot;full_name&quot;:&quot;@ICHCheezburger\/network&quot;,&quot;name&quot;:&quot;network&quot;,&quot;user&quot;:&quot;ICHCheezburger&quot;,&quot;id&quot;:622995&#125;" title="@ICHCheezburger/network"><span>@ICHCheezburger/<wbr/><b>network</b></span></a></li>
+ </ul>
+ <p class="sidebar-menu sidebar-menu-actions">
+ <span class="view-all"><a href="httpdisabled://twitter.com/ICHCheezburger/lists">View all</a></span>
+ </p>
+ </div>
+
+
+<hr/>
+
+
+ <div id="following">
+
+ <h2 class="sidebar-title" id="fm_menu"><span>Following</span></h2>
+ <div class="sidebar-menu">
+ <div id="following_list">
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/foodlooksfunny" class="url" hreflang="en" rel="contact" title="MFLF Team"><img alt="MFLF Team" class="photo fn" height="24" src="../a1.twimg.com/profile_images/754757071/rawr_mini.jpg" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/CollegeHumor" class="url" hreflang="en" rel="contact" title="CollegeHumor"><img alt="CollegeHumor" class="photo fn" height="24" src="../a2.twimg.com/profile_images/1289641028/CH_mini.jpg" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/changinghands" class="url" hreflang="en" rel="contact" title="Changing Hands"><img alt="Changing Hands" class="photo fn" height="24" src="../a0.twimg.com/profile_images/81990615/nightexterior-1_mini.jpg" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/FroggieTweets" class="url" hreflang="en" rel="contact" title="The Frogman"><img alt="The Frogman" class="photo fn" height="24" src="../a2.twimg.com/profile_images/1124077786/batvatar_mini.png" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/memebasealpha" class="url" hreflang="en" rel="contact" title="Memebase Alpha!"><img alt="Memebase Alpha!" class="photo fn" height="24" src="../a2.twimg.com/profile_images/1155395599/Memebase_small_mini.png" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/LOLMart" class="url" hreflang="en" rel="contact" title="Lolmart.com"><img alt="Lolmart.com" class="photo fn" height="24" src="../a2.twimg.com/profile_images/1063331761/LOLmart_150_mini.jpg" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/GlassblowerX" class="url" hreflang="en" rel="contact" title="GlassblowerX"><img alt="GlassblowerX" class="photo fn" height="24" src="../a1.twimg.com/profile_images/1239180764/GlassblowerX_mini.jpg" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/EliThompson" class="url" hreflang="en" rel="contact" title="EliThompson"><img alt="EliThompson" class="photo fn" height="24" src="../a3.twimg.com/profile_images/1092057020/eli_avatar_mini.png" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/ejc" class="url" hreflang="en" rel="contact" title="E.J. Coughlin"><img alt="E.J. Coughlin" class="photo fn" height="24" src="../a0.twimg.com/profile_images/1277610502/Untitled-9_mini.jpg" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/kjpaccountant" class="url" hreflang="en" rel="contact" title="Kristian Pflieger"><img alt="Kristian Pflieger" class="photo fn" height="24" src="../a1.twimg.com/profile_images/333032766/5600_106787006838_550741838_2009237_6385345_n_mini.jpg" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/begyourpARDEN" class="url" hreflang="en" rel="contact" title="Anne Arden Ball"><img alt="Anne Arden Ball" class="photo fn" height="24" src="../a1.twimg.com/profile_images/874705507/01_3_mini.jpg" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/twiggy_XD" class="url" hreflang="en" rel="contact" title="Jasenka Slamnik"><img alt="Jasenka Slamnik" class="photo fn" height="24" src="../a0.twimg.com/profile_images/1139176116/5c42a320-1e91-4d89-a034-0f140d2f23ba_mini.png" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/141soldier" class="url" hreflang="en" rel="contact" title="Javier Vasquez"><img alt="Javier Vasquez" class="photo fn" height="24" src="../a3.twimg.com/sticky/default_profile_images/default_profile_3_mini.png" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/RYAN_H12" class="url" hreflang="en" rel="contact" title="Ryan Hughes"><img alt="Ryan Hughes" class="photo fn" height="24" src="../a3.twimg.com/sticky/default_profile_images/default_profile_3_mini.png" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/KnowItAllison" class="url" hreflang="en" rel="contact" title="Alli Bee"><img alt="Alli Bee" class="photo fn" height="24" src="../a3.twimg.com/profile_images/1260578495/191281_1758367531945_1621722394_1723810_2598069_o_mini.jpg" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/sexymonica12" class="url" hreflang="en" rel="contact" title="sexy jesica"><img alt="sexy jesica" class="photo fn" height="24" src="../a0.twimg.com/sticky/default_profile_images/default_profile_4_mini.png" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/mbsi10" class="url" hreflang="en" rel="contact" title="michael carter"><img alt="michael carter" class="photo fn" height="24" src="../a1.twimg.com/profile_images/959721336/16869_103046893051833_100000395672538_70559_3952672_n_1__mini.jpg" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/Abbigail8900" class="url" hreflang="en" rel="contact" title="Abbigail"><img alt="Abbigail" class="photo fn" height="24" src="../a2.twimg.com/sticky/default_profile_images/default_profile_1_mini.png" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/rudysmah" class="url" hreflang="en" rel="contact" title="Rudy"><img alt="Rudy" class="photo fn" height="24" src="../a3.twimg.com/profile_images/1299269362/10839_196974151498_693676498_3960874_1853030_n_mini.jpg" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/willijoh2010" class="url" hreflang="en" rel="contact" title="Williams John"><img alt="Williams John" class="photo fn" height="24" src="../a2.twimg.com/sticky/default_profile_images/default_profile_1_mini.png" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/Compliments_Int" class="url" hreflang="en" rel="contact" title="Compliments Intl."><img alt="Compliments Intl." class="photo fn" height="24" src="../a2.twimg.com/profile_images/959952929/ci_300x300_mini.jpg" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/prasadnehete" class="url" hreflang="en" rel="contact" title="Prasad"><img alt="Prasad" class="photo fn" height="24" src="../a2.twimg.com/profile_images/724048626/Picture_3895-1_mini.jpg" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/amazingemilie" class="url" hreflang="en" rel="contact" title="Emilie E. Troupe"><img alt="Emilie E. Troupe" class="photo fn" height="24" src="../a0.twimg.com/profile_images/316019228/326994260_1117936370_0_mini.jpeg" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/JustaSlayer" class="url" hreflang="en" rel="contact" title="Paul de Vries"><img alt="Paul de Vries" class="photo fn" height="24" src="../a2.twimg.com/sticky/default_profile_images/default_profile_2_mini.png" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/causticthreads" class="url" hreflang="en" rel="contact" title="Erica Voges"><img alt="Erica Voges" class="photo fn" height="24" src="../a1.twimg.com/profile_images/1248229613/redsugarskullnecklace4-pola_mini.jpg" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/Jaie74" class="url" hreflang="en" rel="contact" title="J"><img alt="J" class="photo fn" height="24" src="../a3.twimg.com/sticky/default_profile_images/default_profile_3_mini.png" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/Alissagreeson" class="url" hreflang="en" rel="contact" title="Alissa"><img alt="Alissa" class="photo fn" height="24" src="../a2.twimg.com/sticky/default_profile_images/default_profile_2_mini.png" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/ZAzaMIca" class="url" hreflang="fr" rel="contact" title="Zamy Michael"><img alt="Zamy Michael" class="photo fn" height="24" src="../a3.twimg.com/profile_images/1110864280/41628_1144937489_2484_q_mini.jpg" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/acompletelycom" class="url" hreflang="en" rel="contact" title="Autocompletely"><img alt="Autocompletely" class="photo fn" height="24" src="../a2.twimg.com/profile_images/700174615/twitter_mini.png" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/pinkpandagrl" class="url" hreflang="en" rel="contact" title="Pink Panda Girl"><img alt="Pink Panda Girl" class="photo fn" height="24" src="../a3.twimg.com/profile_images/1096286685/newpink_copy_mini.jpg" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/aleahdillon" class="url" hreflang="en" rel="contact" title="Aleah Dillon"><img alt="Aleah Dillon" class="photo fn" height="24" src="../a0.twimg.com/profile_images/1129087853/151aec2f-1534-4f61-9f3e-1e787cb51a8b_mini.png" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/Tanira7" class="url" hreflang="en" rel="contact" title="Kari Dolan"><img alt="Kari Dolan" class="photo fn" height="24" src="../a2.twimg.com/sticky/default_profile_images/default_profile_6_mini.png" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/Fergie_Gee" class="url" hreflang="en" rel="contact" title="Graham Ferguson"><img alt="Graham Ferguson" class="photo fn" height="24" src="../a2.twimg.com/profile_images/959827428/25000_1397284054938_1317351118_31101620_485629_n_mini.jpg" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/brosenb1" class="url" hreflang="en" rel="contact" title="Brian"><img alt="Brian" class="photo fn" height="24" src="../a0.twimg.com/profile_images/959692632/13659_1215732676789_1332990286_30703899_6344768_n_mini.jpg" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/HeatFan63" class="url" hreflang="en" rel="contact" title="Kelly Foster"><img alt="Kelly Foster" class="photo fn" height="24" src="../a3.twimg.com/profile_images/1302143328/Profile_copy_mini.jpg" width="24" /></a> </span>
+
+
+ <span class="vcard">
+ <a href="httpdisabled://twitter.com/ImAYellowMonsta" class="url" hreflang="en" rel="contact" title="[ S ' Joness ] c(-:"><img alt="[ S ' Joness ] c(-:" class="photo fn" height="24" src="../a2.twimg.com/profile_images/1296459376/profile_image_1301694822477_mini.jpg" width="24" /></a> </span>
+
+
+ </div>
+ <div id="friends_view_all">
+ <a href="httpdisabled://twitter.com/ICHCheezburger/following" rel="me">View all&hellip;</a>
+ </div>
+
+</div>
+
+ <hr/>
+ </div>
+
+
+
+
+
+ <div id="rssfeed">
+ <a href="httpdisabled://twitter.com/statuses/user_timeline/6173842.rss" class="xref rss profile-rss" rel="alternate" type="application/rss+xml">RSS feed of ICHCheezburger's tweets</a>
+ <a href="httpdisabled://twitter.com/favorites/6173842.rss" class="xref rss favorites-rss" rel="alternate" type="application/rss+xml">RSS feed of ICHCheezburger's favorites</a>
+ </div>
+
+
+
+
+ </div>
+ </td>
+
+ </tr>
+ </tbody>
+ </table>
+
+
+
+ <div id="footer" class="round">
+ <h3 class="offscreen">Footer</h3>
+
+
+ <ul class="footer-nav">
+ <li class="first">&copy; 2011 Twitter</li>
+ <li><a href="httpdisabled://twitter.com/about">About Us</a></li>
+ <li><a href="httpdisabled://twitter.com/about/contact">Contact</a></li>
+ <li><a href="httpdisabled://blog.twitter.com">Blog</a></li>
+ <li><a href="httpdisabled://status.twitter.com">Status</a></li>
+ <li><a href="httpdisabled://twitter.com/about/resources">Resources</a></li>
+ <li><a href="httpdisabled://dev.twitter.com/">API</a></li>
+ <li><a href="httpdisabled://twitter.com/business">Business</a></li>
+ <li><a href="httpdisabled://support.twitter.com">Help</a></li>
+ <li><a href="httpdisabled://twitter.com/jobs">Jobs</a></li>
+ <li><a href="httpdisabled://twitter.com/tos">Terms</a></li>
+ <li><a href="httpdisabled://twitter.com/privacy">Privacy</a></li>
+ </ul>
+ </div>
+
+
+
+ </div>
+
+
+
+ <script src="../ajax.googleapis.com/ajax/libs/jquery/1.3.0/jquery.min.js" type="text/javascript"></script>
+<script src="../a2.twimg.com/a/1302214109/javascripts/twitter.js@1302215522" type="text/javascript"></script>
+<script src="../a0.twimg.com/a/1302214109/javascripts/lib/jquery.tipsy.min.js@1302114648" type="text/javascript"></script>
+<script type='text/javascript' src='../www.google.com/jsapi'></script>
+<script src="../a3.twimg.com/a/1302214109/javascripts/lib/gears_init.js@1302114648" type="text/javascript"></script>
+<script src="../a0.twimg.com/a/1302214109/javascripts/lib/mustache.js@1302114648" type="text/javascript"></script>
+<script src="../a2.twimg.com/a/1302214109/javascripts/geov1.js@1302114648" type="text/javascript"></script>
+<script src="../a3.twimg.com/a/1302214109/javascripts/api.js@1302114648" type="text/javascript"></script>
+<script type="text/javascript">
+//<![CDATA[
+$.cookie('tz_offset_sec', (-1 * (new Date()).getTimezoneOffset())*60);
+//]]>
+</script>
+ <script src="../a0.twimg.com/a/1302214109/javascripts/lib/mustache.js@1302114648" type="text/javascript"></script>
+<script src="../a1.twimg.com/a/1302214109/javascripts/dismissable.js@1302114648" type="text/javascript"></script>
+
+
+<script type="text/javascript">
+//<![CDATA[
+ page.user_screenname = 'ICHCheezburger';
+ page.user_fullname = 'ICanHasCheezburger?';
+ page.controller_name = 'AccountController';
+ page.action_name = 'profile';
+ twttr.form_authenticity_token = 'dd6c65b6f7e87a8a456d76d37264af8f195c7209';
+ $.ajaxSetup({ data: { authenticity_token: 'dd6c65b6f7e87a8a456d76d37264af8f195c7209' } });
+
+ // FIXME: Reconcile with the kinds on the Status model.
+ twttr.statusKinds = {
+ UPDATE: 1,
+ SHARE: 2
+ };
+ twttr.ListPerUserLimit = 20;
+
+
+
+
+//]]>
+</script>
+<script type="text/javascript">
+//<![CDATA[
+
+ $( function () {
+
+ $("#sms_codes_link").hoverTip("#sms_codes");
+ initializePage();
+
+
+
+ if (twttr.geo !== undefined) {
+ twttr.geo.options.show_place_details_in_map = true;
+ }
+
+(function(){function b(){var c=location.href.split("#!")[1];if(c){window.location.hash = "";window.location.pathname = c.replace(/^([^/])/,"/$1");}else return true}var a="onhashchange"in window;if(!a&&window.setAttribute){window.setAttribute("onhashchange","return;");a=typeof window.onhashchange==="function"}if(a)$(window).bind("hashchange",b);else{var d=function(){b()&&setTimeout(d,250)};setTimeout(d,250)}}());
+ $('#signin_menu').isSigninMenu();
+
+ });
+
+//]]>
+</script>
+
+ <!-- BEGIN google analytics -->
+
+ <script type="text/javascript">
+ var gaJsHost = (("httpdisabledsdisabled:" == document.location.protocol) ? "httpdisabledsdisabled://ssl." : "httpdisabled://www.");
+ void(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
+ </script>
+
+ <script type="text/javascript">
+
+ try {
+ var pageTracker = _gat._getTracker("UA-30775-6");
+ pageTracker._setDomainName("twitter.com");
+ pageTracker._setVar('Not Logged In');
+ pageTracker._setVar('lang: en');
+ pageTracker._initData();
+
+ pageTracker._trackPageview('/profile/not_logged_in/ICHCheezburger');
+ } catch(err) { }
+
+ </script>
+
+ <!-- END google analytics -->
+
+
+
+
+ <div id="notifications"></div>
+
+
+
+
+
+
+ </body>
+
+</html>
diff --git a/mobile/android/tests/browser/chrome/tp5/twitter.com/www.google.com/jsapi b/mobile/android/tests/browser/chrome/tp5/twitter.com/www.google.com/jsapi
new file mode 100755
index 0000000000..9870fa6673
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/tp5/twitter.com/www.google.com/jsapi
@@ -0,0 +1,39 @@
+if (!window['google']) {
+window['google'] = {};
+}
+if (!window['google']['loaddisableder']) {
+window['google']['loaddisableder'] = {};
+google.loaddisableder.ServiceBase = 'http://www.google.com/uds';
+google.loaddisableder.GoogleApisBase = 'http://ajax.googleapis.com/ajax';
+google.loaddisableder.ApiKey = 'notsupplied';
+google.loaddisableder.KeyVerified = true;
+google.loaddisableder.LoadFailure = false;
+google.loaddisableder.Secure = false;
+google.loaddisableder.GoogleLocale = 'www.google.com';
+google.loaddisableder.ClientLocation = null;
+google.loaddisableder.AdditionalParams = '';
+(function() {var d=void 0,g=null,h=encodeURIComponent,j=window,k=document;function l(a,b){return a.loaddisabled=b}var m="push",o="replace",p="charAt",r="indexOf",s="ServiceBase",t="name",u="getTime",v="length",w="prototype",x="setTimeout",y="loaddisableder",z="substring",A="join",B="toLowerCase";function C(a){if(a in D)return D[a];return D[a]=navigator.userAgent[B]()[r](a)!=-1}var D={};function E(a,b){var c=function(){};c.prototype=b[w];a.S=b[w];a.prototype=new c}
+function F(a,b){var c=Array[w].slice.call(arguments,2)||[];return function(){var e=c.concat(Array[w].slice.call(arguments));return a.apply(b,e)}}function G(a){a=Error(a);a.toString=function(){return this.message};return a}function H(a,b){for(var c=a.split(/\./),e=j,f=0;f<c[v]-1;f++)e[c[f]]||(e[c[f]]={}),e=e[c[f]];e[c[c[v]-1]]=b}function I(a,b,c){a[b]=c}if(!J)var J=H;if(!K)var K=I;google[y].t={};J("google.loaddisableder.callbacks",google[y].t);var L={},M={};google[y].eval={};J("google.loaddisableder.eval",google[y].eval);
+l(google,function(a,b,c){function e(a){var b=a.split(".");if(b[v]>2)throw G("Module: '"+a+"' not found!");else if(typeof b[1]!="undefined")f=b[0],c.packages=c.packages||[],c.packages[m](b[1])}var f=a,c=c||{};if(a instanceof Array||a&&typeof a=="object"&&typeof a[A]=="function"&&typeof a.reverse=="function")for(var i=0;i<a[v];i++)e(a[i]);else e(a);if(a=L[":"+f]){c&&!c.language&&c.locale&&(c.language=c.locale);if(c&&typeof c.callback=="string"&&(i=c.callback,i.match(/^[[\]A-Za-z0-9._]+$/)))i=j.eval(i),
+c.callback=i;if((i=c&&c.callback!=g)&&!a.s(b))throw G("Module: '"+f+"' must be loaddisableded before DOM onLoad!");else i?a.m(b,c)?j[x](c.callback,0):a.loaddisabled(b,c):a.m(b,c)||a.loaddisabled(b,c)}else throw G("Module: '"+f+"' not found!");});J("google.loaddisabled",google.loaddisabled);
+google.R=function(a,b){b?(N[v]==0&&(O(j,"loaddisabled",P),!C("msie")&&!C("safari")&&!C("konqueror")&&C("mozilla")||j.opera?j.addEventListener("DOMContentLoaded",P,!1):C("msie")?void("<script defer onreadystatechange='google.loaddisableder.domReady()' src=//:><\/script>"):(C("safari")||C("konqueror"))&&j[x](R,10)),N[m](a)):O(j,"loaddisabled",a)};J("google.setOnLoadCallback",google.R);
+function O(a,b,c){if(a.addEventListener)a.addEventListener(b,c,!1);else if(a.attachEvent)a.attachEvent("on"+b,c);else{var e=a["on"+b];a["on"+b]=e!=g?aa([c,e]):c}}function aa(a){return function(){for(var b=0;b<a[v];b++)a[b]()}}var N=[];google[y].L=function(){var a=j.event.srcElement;if(a.readyState=="complete")a.onreadystatechange=g,a.parentNode.removeChild(a),P()};J("google.loaddisableder.domReady",google[y].L);var ba={loaddisableded:!0,complete:!0};function R(){ba[k.readyState]?P():N[v]>0&&j[x](R,10)}
+function P(){for(var a=0;a<N[v];a++)N[a]();N.length=0}google[y].d=function(a,b,c){if(c){var e;if(a=="script")e=k.createElement("script"),e.type="text/javascript",e.src=b;else if(a=="css")e=k.createElement("link"),e.type="text/css",e.href=b,e.rel="stylesheet";(a=k.getElementsByTagName("head")[0])||(a=k.body.parentNode.appendChild(k.createElement("head")));a.appendChild(e)}else a=="script"?void('<script src="'+b+'" type="text/javascript"><\/script>'):a=="css"&&void('<link href="'+b+'" type="text/css" rel="stylesheet"></link>')};
+J("google.voidLoadTag",google[y].d);google[y].O=function(a){M=a};J("google.loaddisableder.rfm",google[y].O);google[y].Q=function(a){for(var b in a)typeof b=="string"&&b&&b[p](0)==":"&&!L[b]&&(L[b]=new S(b[z](1),a[b]))};J("google.loaddisableder.rpl",google[y].Q);google[y].P=function(a){if((a=a.specs)&&a[v])for(var b=0;b<a[v];++b){var c=a[b];typeof c=="string"?L[":"+c]=new T(c):(c=new U(c[t],c.baseSpec,c.customSpecs),L[":"+c[t]]=c)}};J("google.loaddisableder.rm",google[y].P);google[y].loaddisableded=function(a){L[":"+a.module].k(a)};
+J("google.loaddisableder.loaddisableded",google[y].loaddisableded);google[y].K=function(){return"qid="+((new Date)[u]().toString(16)+Math.floor(Math.random()*1E7).toString(16))};J("google.loaddisableder.createGuidArg_",google[y].K);H("google_exportSymbol",H);H("google_exportProperty",I);google[y].b={};J("google.loaddisableder.themes",google[y].b);google[y].b.A="//www.google.com/cse/style/look/bubblegum.css";K(google[y].b,"BUBBLEGUM",google[y].b.A);google[y].b.C="//www.google.com/cse/style/look/greensky.css";K(google[y].b,"GREENSKY",google[y].b.C);
+google[y].b.B="//www.google.com/cse/style/look/espresso.css";K(google[y].b,"ESPRESSO",google[y].b.B);google[y].b.F="//www.google.com/cse/style/look/shiny.css";K(google[y].b,"SHINY",google[y].b.F);google[y].b.D="//www.google.com/cse/style/look/minimalist.css";K(google[y].b,"MINIMALIST",google[y].b.D);function T(a){this.a=a;this.q=[];this.p={};this.i={};this.e={};this.l=!0;this.c=-1}
+T[w].g=function(a,b){var c="";b!=d&&(b.language!=d&&(c+="&hl="+h(b.language)),b.nocss!=d&&(c+="&output="+h("nocss="+b.nocss)),b.nooldnames!=d&&(c+="&nooldnames="+h(b.nooldnames)),b.packages!=d&&(c+="&packages="+h(b.packages)),b.callback!=g&&(c+="&async=2"),b.style!=d&&(c+="&style="+h(b.style)),b.other_params!=d&&(c+="&"+b.other_params));if(!this.l){google[this.a]&&google[this.a].JSHash&&(c+="&sig="+h(google[this.a].JSHash));var e=[],f;for(f in this.p)f[p](0)==":"&&e[m](f[z](1));for(f in this.i)f[p](0)==
+":"&&this.i[f]&&e[m](f[z](1));c+="&have="+h(e[A](","))}return google[y][s]+"/?file="+this.a+"&v="+a+google[y].AdditionalParams+c};T[w].v=function(a){var b=g;a&&(b=a.packages);var c=g;if(b)if(typeof b=="string")c=[a.packages];else if(b[v]){c=[];for(a=0;a<b[v];a++)typeof b[a]=="string"&&c[m](b[a][o](/^\s*|\s*$/,"")[B]())}c||(c=["default"]);b=[];for(a=0;a<c[v];a++)this.p[":"+c[a]]||b[m](c[a]);return b};
+l(T[w],function(a,b){var c=this.v(b),e=b&&b.callback!=g;if(e)var f=new V(b.callback);for(var i=[],n=c[v]-1;n>=0;n--){var q=c[n];e&&f.G(q);if(this.i[":"+q])c.splice(n,1),e&&this.e[":"+q][m](f);else i[m](q)}if(c[v]){b&&b.packages&&(b.packages=c.sort()[A](","));for(n=0;n<i[v];n++)q=i[n],this.e[":"+q]=[],e&&this.e[":"+q][m](f);if(!b&&M[":"+this.a]!=g&&M[":"+this.a].versions[":"+a]!=g&&!google[y].AdditionalParams&&this.l){c=M[":"+this.a];google[this.a]=google[this.a]||{};for(var Q in c.properties)Q&&Q[p](0)==
+":"&&(google[this.a][Q[z](1)]=c.properties[Q]);google[y].d("script",google[y][s]+c.path+c.js,e);c.css&&google[y].d("css",google[y][s]+c.path+c.css,e)}else(!b||!b.autoloaddisableded)&&google[y].d("script",this.g(a,b),e);if(this.l&&(this.l=!1,this.c=(new Date)[u](),this.c%100!=1))this.c=-1;for(n=0;n<i[v];n++)q=i[n],this.i[":"+q]=!0}});
+T[w].k=function(a){if(this.c!=-1)W("al_"+this.a,"jl."+((new Date)[u]()-this.c),!0),this.c=-1;this.q=this.q.concat(a.components);google[y][this.a]||(google[y][this.a]={});google[y][this.a].packages=this.q.slice(0);for(var b=0;b<a.components[v];b++){this.p[":"+a.components[b]]=!0;this.i[":"+a.components[b]]=!1;var c=this.e[":"+a.components[b]];if(c){for(var e=0;e<c[v];e++)c[e].J(a.components[b]);delete this.e[":"+a.components[b]]}}};T[w].m=function(a,b){return this.v(b)[v]==0};T[w].s=function(){return!0};
+function V(a){this.I=a;this.n={};this.r=0}V[w].G=function(a){this.r++;this.n[":"+a]=!0};V[w].J=function(a){this.n[":"+a]&&(this.n[":"+a]=!1,this.r--,this.r==0&&j[x](this.I,0))};function U(a,b,c){this.name=a;this.H=b;this.o=c;this.u=this.h=!1;this.j=[];google[y].t[this[t]]=F(this.k,this)}E(U,T);l(U[w],function(a,b){var c=b&&b.callback!=g;c?(this.j[m](b.callback),b.callback="google.loaddisableder.callbacks."+this[t]):this.h=!0;(!b||!b.autoloaddisableded)&&google[y].d("script",this.g(a,b),c)});U[w].m=function(a,b){return b&&b.callback!=g?this.u:this.h};U[w].k=function(){this.u=!0;for(var a=0;a<this.j[v];a++)j[x](this.j[a],0);this.j=[]};
+var X=function(a,b){return a.string?h(a.string)+"="+h(b):a.regex?b[o](/(^.*$)/,a.regex):""};U[w].g=function(a,b){return this.M(this.w(a),a,b)};
+U[w].M=function(a,b,c){var e="";a.key&&(e+="&"+X(a.key,google[y].ApiKey));a.version&&(e+="&"+X(a.version,b));b=google[y].Secure&&a.ssl?a.ssl:a.uri;if(c!=g)for(var f in c)a.params[f]?e+="&"+X(a.params[f],c[f]):f=="other_params"?e+="&"+c[f]:f=="base_domain"&&(b="httpdisabled://"+c[f]+a.uri[z](a.uri[r]("/",7)));google[this[t]]={};b[r]("?")==-1&&e&&(e="?"+e[z](1));return b+e};U[w].s=function(a){return this.w(a).deferred};U[w].w=function(a){if(this.o)for(var b=0;b<this.o[v];++b){var c=this.o[b];if(RegExp(c.pattern).test(a))return c}return this.H};function S(a,b){this.a=a;this.f=b;this.h=!1}E(S,T);l(S[w],function(a,b){this.h=!0;google[y].d("script",this.g(a,b),!1)});S[w].m=function(){return this.h};S[w].k=function(){};S[w].g=function(a,b){if(!this.f.versions[":"+a]){if(this.f.aliases){var c=this.f.aliases[":"+a];c&&(a=c)}if(!this.f.versions[":"+a])throw G("Module: '"+this.a+"' with version '"+a+"' not found!");}return google[y].GoogleApisBase+"/libs/"+this.a+"/"+a+"/"+this.f.versions[":"+a][b&&b.uncompressed?"uncompressed":"compressed"]};
+S[w].s=function(){return!1};var Y=!1,Z=[],ca=(new Date)[u](),W=function(a,b,c){Y||(O(j,"unloaddisabled",da),Y=!0);if(c){if(!google[y].Secure&&(!google[y].Options||google[y].Options.csi===!1))a=a[B]()[o](/[^a-z0-9_.]+/g,"_"),b=b[B]()[o](/[^a-z0-9_.]+/g,"_"),j[x](F($,g,"//gg.google.com/csi?s=uds&v=2&action="+h(a)+"&it="+h(b)),1E4)}else Z[m]("r"+Z[v]+"="+h(a+(b?"|"+b:""))),j[x](da,Z[v]>5?0:15E3)},da=function(){if(Z[v]){var a=google[y][s];a[r]("httpdisabled:")==0&&(a=a[o](/^http:/,"httpdisabledsdisabled:"));$(a+"/stats?"+Z[A]("&")+"&nc="+(new Date)[u]()+"_"+((new Date)[u]()-
+ca));Z.length=0}},$=function(a){var b=new Image,c=$.N++;$.z[c]=b;b.onloaddisabled=b.onerror=function(){delete $.z[c]};b.src=a;b=g};$.z={};$.N=0;H("google.loaddisableder.recordStat",W);H("google.loaddisableder.createImageForLogging",$);
+
+}) ();google.loaddisableder.rm({"specs":[{"name":"books","baseSpec":{"uri":"httpdisabled://books.google.com/books/api.js","ssl":null,"key":{"string":"key"},"version":{"string":"v"},"deferred":true,"params":{"callback":{"string":"callback"},"language":{"string":"hl"}}}},"feeds",{"name":"friendconnect","baseSpec":{"uri":"httpdisabled://www.google.com/friendconnect/script/friendconnect.js","ssl":null,"key":{"string":"key"},"version":{"string":"v"},"deferred":false,"params":{}}},"spreadsheets","identitytoolkit","gdata","visualization",{"name":"sharing","baseSpec":{"uri":"httpdisabled://www.google.com/s2/sharing/js","ssl":null,"key":{"string":"key"},"version":{"string":"v"},"deferred":false,"params":{"language":{"string":"hl"}}}},"search",{"name":"maps","baseSpec":{"uri":"httpdisabled://maps.google.com/maps?file\u003dgoogleapi","ssl":"httpdisabledsdisabled://maps-api-ssl.google.com/maps?file\u003dgoogleapi","key":{"string":"key"},"version":{"string":"v"},"deferred":true,"params":{"callback":{"regex":"callback\u003d$1\u0026async\u003d2"},"language":{"string":"hl"}}},"customSpecs":[{"uri":"httpdisabled://maps.google.com/maps/api/js","ssl":"httpdisabledsdisabled://maps-api-ssl.google.com/maps/api/js","key":{"string":"key"},"version":{"string":"v"},"deferred":true,"params":{"callback":{"string":"callback"},"language":{"string":"hl"}},"pattern":"^(3|3..*)$"}]},"annotations_v2","wave","orkut",{"name":"annotations","baseSpec":{"uri":"httpdisabled://www.google.com/reviews/scripts/annotations_bootstrap.js","ssl":null,"key":{"string":"key"},"version":{"string":"v"},"deferred":true,"params":{"callback":{"string":"callback"},"language":{"string":"hl"},"country":{"string":"gl"}}}},"language","earth","ads","elements"]});
+google.loaddisableder.rfm({":search":{"versions":{":1":"1",":1.0":"1"},"path":"/api/search/1.0/fb730160e72add7b256fbc9b5dc23635/","js":"default+en.I.js","css":"default.css","properties":{":JSHash":"fb730160e72add7b256fbc9b5dc23635",":NoOldNames":false,":Version":"1.0"}},":language":{"versions":{":1":"1",":1.0":"1"},"path":"/api/language/1.0/4c799b5d9590782ad04064fdda233029/","js":"default+en.I.js","properties":{":JSHash":"4c799b5d9590782ad04064fdda233029",":Version":"1.0"}},":feeds":{"versions":{":1":"1",":1.0":"1"},"path":"/api/feeds/1.0/ebcc20169bc505865931499d7e9dca8d/","js":"default+en.I.js","css":"default.css","properties":{":JSHash":"ebcc20169bc505865931499d7e9dca8d",":Version":"1.0"}},":spreadsheets":{"versions":{":0":"1",":0.4":"1"},"path":"/api/spreadsheets/0.4/87ff7219e9f8a8164006cbf28d5e911a/","js":"default.I.js","properties":{":JSHash":"87ff7219e9f8a8164006cbf28d5e911a",":Version":"0.4"}},":wave":{"versions":{":1":"1",":1.0":"1"},"path":"/api/wave/1.0/3b6f7573ff78da6602dda5e09c9025bf/","js":"default.I.js","properties":{":JSHash":"3b6f7573ff78da6602dda5e09c9025bf",":Version":"1.0"}},":annotations":{"versions":{":1":"1",":1.0":"1"},"path":"/api/annotations/1.0/957128231817f36b6e8dcf58c50902df/","js":"default+en.I.js","properties":{":JSHash":"957128231817f36b6e8dcf58c50902df",":Version":"1.0"}},":earth":{"versions":{":1":"1",":1.0":"1"},"path":"/api/earth/1.0/a53f4e87830de2a72937039b5507ebdc/","js":"default.I.js","properties":{":JSHash":"a53f4e87830de2a72937039b5507ebdc",":Version":"1.0"}}});
+google.loaddisableder.rpl({":scriptaculous":{"versions":{":1.8.3":{"uncompressed":"scriptaculous.js","compressed":"scriptaculous.js"},":1.8.2":{"uncompressed":"scriptaculous.js","compressed":"scriptaculous.js"},":1.8.1":{"uncompressed":"scriptaculous.js","compressed":"scriptaculous.js"}},"aliases":{":1.8":"1.8.3",":1":"1.8.3"}},":yui":{"versions":{":2.6.0":{"uncompressed":"build/yuiloaddisableder/yuiloaddisableder.js","compressed":"build/yuiloaddisableder/yuiloaddisableder-min.js"},":2.7.0":{"uncompressed":"build/yuiloaddisableder/yuiloaddisableder.js","compressed":"build/yuiloaddisableder/yuiloaddisableder-min.js"},":2.8.0r4":{"uncompressed":"build/yuiloaddisableder/yuiloaddisableder.js","compressed":"build/yuiloaddisableder/yuiloaddisableder-min.js"},":2.8.2r1":{"uncompressed":"build/yuiloaddisableder/yuiloaddisableder.js","compressed":"build/yuiloaddisableder/yuiloaddisableder-min.js"},":2.8.1":{"uncompressed":"build/yuiloaddisableder/yuiloaddisableder.js","compressed":"build/yuiloaddisableder/yuiloaddisableder-min.js"},":3.3.0":{"uncompressed":"build/yui/yui.js","compressed":"build/yui/yui-min.js"}},"aliases":{":3":"3.3.0",":2":"2.8.2r1",":2.7":"2.7.0",":2.8.2":"2.8.2r1",":2.6":"2.6.0",":2.8":"2.8.2r1",":2.8.0":"2.8.0r4",":3.3":"3.3.0"}},":swfobject":{"versions":{":2.1":{"uncompressed":"swfobject_src.js","compressed":"swfobject.js"},":2.2":{"uncompressed":"swfobject_src.js","compressed":"swfobject.js"}},"aliases":{":2":"2.2"}},":ext-core":{"versions":{":3.1.0":{"uncompressed":"ext-core-debug.js","compressed":"ext-core.js"},":3.0.0":{"uncompressed":"ext-core-debug.js","compressed":"ext-core.js"}},"aliases":{":3":"3.1.0",":3.0":"3.0.0",":3.1":"3.1.0"}},":webfont":{"versions":{":1.0.2":{"uncompressed":"webfont_debug.js","compressed":"webfont.js"},":1.0.1":{"uncompressed":"webfont_debug.js","compressed":"webfont.js"},":1.0.0":{"uncompressed":"webfont_debug.js","compressed":"webfont.js"},":1.0.6":{"uncompressed":"webfont_debug.js","compressed":"webfont.js"},":1.0.19":{"uncompressed":"webfont_debug.js","compressed":"webfont.js"},":1.0.5":{"uncompressed":"webfont_debug.js","compressed":"webfont.js"},":1.0.18":{"uncompressed":"webfont_debug.js","compressed":"webfont.js"},":1.0.17":{"uncompressed":"webfont_debug.js","compressed":"webfont.js"},":1.0.4":{"uncompressed":"webfont_debug.js","compressed":"webfont.js"},":1.0.16":{"uncompressed":"webfont_debug.js","compressed":"webfont.js"},":1.0.3":{"uncompressed":"webfont_debug.js","compressed":"webfont.js"},":1.0.9":{"uncompressed":"webfont_debug.js","compressed":"webfont.js"},":1.0.12":{"uncompressed":"webfont_debug.js","compressed":"webfont.js"},":1.0.13":{"uncompressed":"webfont_debug.js","compressed":"webfont.js"},":1.0.14":{"uncompressed":"webfont_debug.js","compressed":"webfont.js"},":1.0.15":{"uncompressed":"webfont_debug.js","compressed":"webfont.js"},":1.0.10":{"uncompressed":"webfont_debug.js","compressed":"webfont.js"},":1.0.11":{"uncompressed":"webfont_debug.js","compressed":"webfont.js"}},"aliases":{":1":"1.0.19",":1.0":"1.0.19"}},":mootools":{"versions":{":1.2.3":{"uncompressed":"mootools.js","compressed":"mootools-yui-compressed.js"},":1.3.1":{"uncompressed":"mootools.js","compressed":"mootools-yui-compressed.js"},":1.1.1":{"uncompressed":"mootools.js","compressed":"mootools-yui-compressed.js"},":1.2.4":{"uncompressed":"mootools.js","compressed":"mootools-yui-compressed.js"},":1.3.0":{"uncompressed":"mootools.js","compressed":"mootools-yui-compressed.js"},":1.2.1":{"uncompressed":"mootools.js","compressed":"mootools-yui-compressed.js"},":1.2.2":{"uncompressed":"mootools.js","compressed":"mootools-yui-compressed.js"},":1.2.5":{"uncompressed":"mootools.js","compressed":"mootools-yui-compressed.js"},":1.1.2":{"uncompressed":"mootools.js","compressed":"mootools-yui-compressed.js"}},"aliases":{":1":"1.1.2",":1.11":"1.1.1",":1.3":"1.3.1",":1.2":"1.2.5",":1.1":"1.1.2"}},":jqueryui":{"versions":{":1.6.0":{"uncompressed":"jquery-ui.js","compressed":"jquery-ui.min.js"},":1.8.0":{"uncompressed":"jquery-ui.js","compressed":"jquery-ui.min.js"},":1.8.2":{"uncompressed":"jquery-ui.js","compressed":"jquery-ui.min.js"},":1.8.1":{"uncompressed":"jquery-ui.js","compressed":"jquery-ui.min.js"},":1.8.9":{"uncompressed":"jquery-ui.js","compressed":"jquery-ui.min.js"},":1.8.7":{"uncompressed":"jquery-ui.js","compressed":"jquery-ui.min.js"},":1.8.8":{"uncompressed":"jquery-ui.js","compressed":"jquery-ui.min.js"},":1.7.2":{"uncompressed":"jquery-ui.js","compressed":"jquery-ui.min.js"},":1.8.5":{"uncompressed":"jquery-ui.js","compressed":"jquery-ui.min.js"},":1.8.11":{"uncompressed":"jquery-ui.js","compressed":"jquery-ui.min.js"},":1.7.3":{"uncompressed":"jquery-ui.js","compressed":"jquery-ui.min.js"},":1.8.6":{"uncompressed":"jquery-ui.js","compressed":"jquery-ui.min.js"},":1.8.10":{"uncompressed":"jquery-ui.js","compressed":"jquery-ui.min.js"},":1.7.0":{"uncompressed":"jquery-ui.js","compressed":"jquery-ui.min.js"},":1.7.1":{"uncompressed":"jquery-ui.js","compressed":"jquery-ui.min.js"},":1.8.4":{"uncompressed":"jquery-ui.js","compressed":"jquery-ui.min.js"},":1.5.3":{"uncompressed":"jquery-ui.js","compressed":"jquery-ui.min.js"},":1.5.2":{"uncompressed":"jquery-ui.js","compressed":"jquery-ui.min.js"}},"aliases":{":1.8":"1.8.11",":1.7":"1.7.3",":1.6":"1.6.0",":1":"1.8.11",":1.5":"1.5.3",":1.8.3":"1.8.4"}},":chrome-frame":{"versions":{":1.0.2":{"uncompressed":"CFInstall.js","compressed":"CFInstall.min.js"},":1.0.1":{"uncompressed":"CFInstall.js","compressed":"CFInstall.min.js"},":1.0.0":{"uncompressed":"CFInstall.js","compressed":"CFInstall.min.js"}},"aliases":{":1":"1.0.2",":1.0":"1.0.2"}},":jquery":{"versions":{":1.3.1":{"uncompressed":"jquery.js","compressed":"jquery.min.js"},":1.3.0":{"uncompressed":"jquery.js","compressed":"jquery.min.js"},":1.3.2":{"uncompressed":"jquery.js","compressed":"jquery.min.js"},":1.2.3":{"uncompressed":"jquery.js","compressed":"jquery.min.js"},":1.2.6":{"uncompressed":"jquery.js","compressed":"jquery.min.js"},":1.4.3":{"uncompressed":"jquery.js","compressed":"jquery.min.js"},":1.4.4":{"uncompressed":"jquery.js","compressed":"jquery.min.js"},":1.5.1":{"uncompressed":"jquery.js","compressed":"jquery.min.js"},":1.5.0":{"uncompressed":"jquery.js","compressed":"jquery.min.js"},":1.4.0":{"uncompressed":"jquery.js","compressed":"jquery.min.js"},":1.5.2":{"uncompressed":"jquery.js","compressed":"jquery.min.js"},":1.4.1":{"uncompressed":"jquery.js","compressed":"jquery.min.js"},":1.4.2":{"uncompressed":"jquery.js","compressed":"jquery.min.js"}},"aliases":{":1":"1.5.2",":1.5":"1.5.2",":1.4":"1.4.4",":1.3":"1.3.2",":1.2":"1.2.6"}},":dojo":{"versions":{":1.2.3":{"uncompressed":"dojo/dojo.xd.js.uncompressed.js","compressed":"dojo/dojo.xd.js"},":1.3.1":{"uncompressed":"dojo/dojo.xd.js.uncompressed.js","compressed":"dojo/dojo.xd.js"},":1.1.1":{"uncompressed":"dojo/dojo.xd.js.uncompressed.js","compressed":"dojo/dojo.xd.js"},":1.3.0":{"uncompressed":"dojo/dojo.xd.js.uncompressed.js","compressed":"dojo/dojo.xd.js"},":1.3.2":{"uncompressed":"dojo/dojo.xd.js.uncompressed.js","compressed":"dojo/dojo.xd.js"},":1.6.0":{"uncompressed":"dojo/dojo.xd.js.uncompressed.js","compressed":"dojo/dojo.xd.js"},":1.4.3":{"uncompressed":"dojo/dojo.xd.js.uncompressed.js","compressed":"dojo/dojo.xd.js"},":1.5.1":{"uncompressed":"dojo/dojo.xd.js.uncompressed.js","compressed":"dojo/dojo.xd.js"},":1.5.0":{"uncompressed":"dojo/dojo.xd.js.uncompressed.js","compressed":"dojo/dojo.xd.js"},":1.2.0":{"uncompressed":"dojo/dojo.xd.js.uncompressed.js","compressed":"dojo/dojo.xd.js"},":1.4.0":{"uncompressed":"dojo/dojo.xd.js.uncompressed.js","compressed":"dojo/dojo.xd.js"},":1.4.1":{"uncompressed":"dojo/dojo.xd.js.uncompressed.js","compressed":"dojo/dojo.xd.js"}},"aliases":{":1":"1.6.0",":1.6":"1.6.0",":1.5":"1.5.1",":1.4":"1.4.3",":1.3":"1.3.2",":1.2":"1.2.3",":1.1":"1.1.1"}},":prototype":{"versions":{":1.7.0.0":{"uncompressed":"prototype.js","compressed":"prototype.js"},":1.6.0.2":{"uncompressed":"prototype.js","compressed":"prototype.js"},":1.6.1.0":{"uncompressed":"prototype.js","compressed":"prototype.js"},":1.6.0.3":{"uncompressed":"prototype.js","compressed":"prototype.js"}},"aliases":{":1.7":"1.7.0.0",":1.6.1":"1.6.1.0",":1":"1.7.0.0",":1.6":"1.6.1.0",":1.7.0":"1.7.0.0",":1.6.0":"1.6.0.3"}}});
+}
diff --git a/mobile/android/tests/browser/chrome/video_controls.html b/mobile/android/tests/browser/chrome/video_controls.html
new file mode 100644
index 0000000000..a312124093
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/video_controls.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Video Controls Test</title>
+ </head>
+ <body>
+ <video id="video" style="height: 480px; width: 640px" controls mozNoDynamicControls></video>
+ <canvas id="canvas" style="height: 480px; width: 640px"></canvas>
+ </body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/video_discovery.html b/mobile/android/tests/browser/chrome/video_discovery.html
new file mode 100644
index 0000000000..6eb181dc42
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/video_discovery.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Video Discovery Test</title>
+ <style type="text/css">
+ #video-box {
+ float: left;
+ }
+ #video-overlay, #video-player {
+ width: 640px;
+ min-height: 370px;
+ }
+ #video-overlay {
+ position: absolute;
+ float: left;
+ background-color:#f00;
+ z-index:10;
+ }
+ </style>
+ </head>
+ <body>
+ <!-- PASS: src uses a mp4 extension -->
+ <video id="simple-mp4" poster="/simple.png" src="/simple.mp4"></video>
+
+ <!-- FAIL: src uses a ogg extension -->
+ <video id="simple-fail" src="/simple.ogg"></video>
+
+ <!-- PASS: source list uses a mp4 extension -->
+ <video id="with-sources-mp4">
+ <source src="/simple.ogg">
+ <source src="/simple.mp4">
+ </video>
+
+ <!-- PASS: source list uses a webm extension -->
+ <video id="with-sources-webm">
+ <source src="/simple.ogg">
+ <source src="/simple.webm">
+ </video>
+
+ <!-- FAIL: source list has no mp4 or webm extension -->
+ <video id="with-sources-fail">
+ <source src="/simple.ogg">
+ </video>
+
+ <!-- PASS: source list uses a mp4 mimetype -->
+ <video id="with-sources-mimetype-mp4">
+ <source src="/simple-video-ogg" type="video/ogg">
+ <source src="/simple-video-mp4" type="video/mp4">
+ </video>
+
+ <!-- PASS: source list uses a webm mimetype -->
+ <video id="with-sources-mimetype-webm">
+ <source src="/simple-video-ogg" type="video/ogg">
+ <source src="/simple-video-webm" type="video/webm">
+ </video>
+
+ <!-- PASS: source list uses a mp4 mimetype and extra data -->
+ <video id="with-sources-mimetype-plus">
+ <source src="/simple-video-ogg" type="video/ogg">
+ <source src="/simple-video-mp4" type="video/mp4; codecs='avc1.42E01E, mp4a.40.2'">
+ </video>
+
+ <!-- PASS: src uses a mp4 mimetype from the server -->
+ <video id="simple-fetch-pass" src="http://mochi.test:8888/chrome/mobile/android/tests/browser/chrome/video_discovery.sjs?type=video/mp4"></video>
+
+ <!-- FAIL: src uses a non-video mimetype from the server -->
+ <video id="simple-fetch-fail" src="http://mochi.test:8888/chrome/mobile/android/tests/browser/chrome/video_discovery.sjs?type=image/png"></video>
+
+ <!-- PASS: div overlay covers a video with mp4 src -->
+ <div id="video-box">
+ <div id="video-overlay"></div>
+ <div>
+ <video id="video-player" src="/simple.mp4"></video>
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/mobile/android/tests/browser/chrome/video_discovery.sjs b/mobile/android/tests/browser/chrome/video_discovery.sjs
new file mode 100644
index 0000000000..9748fe0bce
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/video_discovery.sjs
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function parseQuery(request, key) {
+ var params = request.queryString.split('&');
+ for (var j = 0; j < params.length; ++j) {
+ var p = params[j];
+ if (p == key)
+ return true;
+ if (p.indexOf(key + "=") == 0)
+ return p.substring(key.length + 1);
+ if (p.indexOf("=") < 0 && key == "")
+ return p;
+ }
+ return false;
+}
+
+function handleRequest(request, response) {
+ // Pretend to be the type requested from the test
+ var type = parseQuery(request, "type");
+
+ response.setHeader("Content-Type", type, false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+
+ response.write("fake video");
+}
diff --git a/mobile/android/tests/browser/chrome/web_channel.html b/mobile/android/tests/browser/chrome/web_channel.html
new file mode 100644
index 0000000000..866f3efd23
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/web_channel.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>web_channel_test</title>
+</head>
+<body>
+<script>
+ window.onload = function() {
+ var testName = window.location.search.replace(/^\?/, "");
+
+ switch(testName) {
+ case "generic":
+ test_generic();
+ break;
+ case "twoway":
+ test_twoWay();
+ break;
+ case "multichannel":
+ test_multichannel();
+ break;
+ }
+ };
+
+ function test_generic() {
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "generic",
+ message: {
+ something: {
+ nested: "hello",
+ },
+ }
+ })
+ });
+
+ window.dispatchEvent(event);
+ }
+
+ function test_twoWay() {
+ var firstMessage = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "twoway",
+ message: {
+ command: "one",
+ },
+ })
+ });
+
+ window.addEventListener("WebChannelMessageToContent", function(e) {
+ var secondMessage = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "twoway",
+ message: {
+ command: "two",
+ detail: e.detail.message,
+ },
+ }),
+ });
+
+ if (!e.detail.message.error) {
+ window.dispatchEvent(secondMessage);
+ }
+ }, true);
+
+ window.dispatchEvent(firstMessage);
+ }
+
+ function test_multichannel() {
+ var event1 = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "wrongchannel",
+ message: {},
+ })
+ });
+
+ var event2 = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "multichannel",
+ message: {},
+ })
+ });
+
+ window.dispatchEvent(event1);
+ window.dispatchEvent(event2);
+ }
+</script>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/junit3/AndroidManifest.xml.in b/mobile/android/tests/browser/junit3/AndroidManifest.xml.in
new file mode 100644
index 0000000000..1775c24334
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/AndroidManifest.xml.in
@@ -0,0 +1,23 @@
+#filter substitution
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.mozilla.gecko.browser.tests"
+ sharedUserId="@MOZ_ANDROID_SHARED_ID@"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-sdk android:minSdkVersion="8"
+ android:targetSdkVersion="@ANDROID_TARGET_SDK@" />
+
+ <application
+ android:debuggable="true"
+ android:icon="@drawable/icon"
+ android:label="@ANDROID_BROWSER_APP_DISPLAYNAME@">
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:label="@string/app_name"
+ android:name="org.mozilla.gecko.harness.BrowserInstrumentationTestRunner"
+ android:targetPackage="@ANDROID_BROWSER_TARGET_PACKAGE_NAME@" />
+</manifest>
diff --git a/mobile/android/tests/browser/junit3/Makefile.in b/mobile/android/tests/browser/junit3/Makefile.in
new file mode 100644
index 0000000000..299b4d2800
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/Makefile.in
@@ -0,0 +1,13 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+ANDROID_EXTRA_JARS += \
+ browser-junit3.jar \
+ $(NULL)
+
+ANDROID_MANIFEST_FILE := $(CURDIR)/AndroidManifest.xml
+
+include $(topsrcdir)/config/rules.mk
+
+tools:: $(ANDROID_APK_NAME).apk
diff --git a/mobile/android/tests/browser/junit3/instrumentation.ini b/mobile/android/tests/browser/junit3/instrumentation.ini
new file mode 100644
index 0000000000..5f61d938f0
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/instrumentation.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+subsuite = browser
+
+[src/org/mozilla/tests/browser/junit3/TestDistribution.java]
+[src/org/mozilla/tests/browser/junit3/TestGeckoSharedPrefs.java]
+[src/org/mozilla/tests/browser/junit3/TestImageDownloader.java]
+[src/org/mozilla/tests/browser/junit3/TestJarReader.java]
+[src/org/mozilla/tests/browser/junit3/TestRawResource.java]
+[src/org/mozilla/tests/browser/junit3/TestSuggestedSites.java]
diff --git a/mobile/android/tests/browser/junit3/moz.build b/mobile/android/tests/browser/junit3/moz.build
new file mode 100644
index 0000000000..577664508d
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/moz.build
@@ -0,0 +1,55 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEFINES['ANDROID_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME']
+
+ANDROID_APK_NAME = 'browser-junit3-debug'
+ANDROID_APK_PACKAGE = 'org.mozilla.gecko.browser.tests'
+
+jar = add_java_jar('browser-junit3')
+jar.sources += [
+ 'src/org/mozilla/tests/browser/junit3/harness/BrowserInstrumentationTestRunner.java',
+ 'src/org/mozilla/tests/browser/junit3/harness/BrowserTestListener.java',
+ 'src/org/mozilla/tests/browser/junit3/TestDistribution.java',
+ 'src/org/mozilla/tests/browser/junit3/TestGeckoBackgroundThread.java',
+ 'src/org/mozilla/tests/browser/junit3/TestGeckoMenu.java',
+ 'src/org/mozilla/tests/browser/junit3/TestGeckoProfilesProvider.java',
+ 'src/org/mozilla/tests/browser/junit3/TestGeckoSharedPrefs.java',
+ 'src/org/mozilla/tests/browser/junit3/TestImageDownloader.java',
+ 'src/org/mozilla/tests/browser/junit3/TestJarReader.java',
+ 'src/org/mozilla/tests/browser/junit3/TestRawResource.java',
+ 'src/org/mozilla/tests/browser/junit3/TestSuggestedSites.java',
+]
+jar.generated_sources = [] # None yet -- try to keep it this way.
+jar.javac_flags += ['-Xlint:all']
+
+jar.extra_jars += [
+ CONFIG['ANDROID_SUPPORT_V4_AAR_LIB'],
+ CONFIG['ANDROID_RECYCLERVIEW_V7_AAR_LIB'],
+ TOPOBJDIR + '/mobile/android/base/constants.jar',
+ TOPOBJDIR + '/mobile/android/base/gecko-R.jar',
+ TOPOBJDIR + '/mobile/android/base/gecko-browser.jar',
+ TOPOBJDIR + '/mobile/android/base/gecko-mozglue.jar',
+ TOPOBJDIR + '/mobile/android/base/gecko-thirdparty.jar',
+ TOPOBJDIR + '/mobile/android/base/gecko-util.jar',
+ TOPOBJDIR + '/mobile/android/base/gecko-view.jar',
+ TOPOBJDIR + '/mobile/android/base/services.jar',
+ TOPOBJDIR + '/mobile/android/base/sync-thirdparty.jar',
+]
+
+if CONFIG['MOZ_ANDROID_MLS_STUMBLER']:
+ jar.extra_jars += [
+ TOPOBJDIR + '/mobile/android/stumbler/stumbler.jar',
+ ]
+
+ANDROID_INSTRUMENTATION_MANIFESTS += ['instrumentation.ini']
+
+DEFINES['ANDROID_BROWSER_TARGET_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME']
+DEFINES['ANDROID_BROWSER_APP_DISPLAYNAME'] = '%s Browser Tests' % CONFIG['MOZ_APP_DISPLAYNAME']
+DEFINES['MOZ_ANDROID_SHARED_ID'] = CONFIG['MOZ_ANDROID_SHARED_ID']
+OBJDIR_PP_FILES.mobile.android.tests.browser.junit3 += [
+ 'AndroidManifest.xml.in',
+]
diff --git a/mobile/android/tests/browser/junit3/res/drawable-hdpi/icon.png b/mobile/android/tests/browser/junit3/res/drawable-hdpi/icon.png
new file mode 100644
index 0000000000..e83438eee4
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/res/drawable-hdpi/icon.png
Binary files differ
diff --git a/mobile/android/tests/browser/junit3/res/drawable-ldpi/icon.png b/mobile/android/tests/browser/junit3/res/drawable-ldpi/icon.png
new file mode 100644
index 0000000000..0483c95e99
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/res/drawable-ldpi/icon.png
Binary files differ
diff --git a/mobile/android/tests/browser/junit3/res/drawable-mdpi/icon.png b/mobile/android/tests/browser/junit3/res/drawable-mdpi/icon.png
new file mode 100644
index 0000000000..86b4dee546
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/res/drawable-mdpi/icon.png
Binary files differ
diff --git a/mobile/android/tests/browser/junit3/res/layout/main.xml b/mobile/android/tests/browser/junit3/res/layout/main.xml
new file mode 100644
index 0000000000..db8893bd9c
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/res/layout/main.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/app_name" />
+
+</LinearLayout>
diff --git a/mobile/android/tests/browser/junit3/res/values/strings.xml b/mobile/android/tests/browser/junit3/res/values/strings.xml
new file mode 100644
index 0000000000..a3faebab65
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/res/values/strings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">Gecko Browser Tests</string>
+
+</resources>
diff --git a/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestDistribution.java b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestDistribution.java
new file mode 100644
index 0000000000..9cabb346c5
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestDistribution.java
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.tests.browser.junit3;
+
+import android.test.InstrumentationTestCase;
+import org.mozilla.gecko.distribution.ReferrerDescriptor;
+
+public class TestDistribution extends InstrumentationTestCase {
+ private static final String TEST_REFERRER_STRING = "utm_source=campsource&utm_medium=campmed&utm_term=term%2Bhere&utm_content=content&utm_campaign=name";
+ private static final String TEST_MALFORMED_REFERRER_STRING = "utm_source=campsource&utm_medium=campmed&utm_term=term%2";
+
+ public void testReferrerParsing() {
+ ReferrerDescriptor good = new ReferrerDescriptor(TEST_REFERRER_STRING);
+ assertEquals("campsource", good.source);
+ assertEquals("campmed", good.medium);
+ assertEquals("term+here", good.term);
+ assertEquals("content", good.content);
+ assertEquals("name", good.campaign);
+
+ // Uri.Builder is permissive.
+ ReferrerDescriptor bad = new ReferrerDescriptor(TEST_MALFORMED_REFERRER_STRING);
+ assertEquals("campsource", bad.source);
+ assertEquals("campmed", bad.medium);
+ assertFalse("term+here".equals(bad.term));
+ assertNull(bad.content);
+ assertNull(bad.campaign);
+
+ ReferrerDescriptor ugly = new ReferrerDescriptor(null);
+ assertNull(ugly.source);
+ assertNull(ugly.medium);
+ assertNull(ugly.term);
+ assertNull(ugly.content);
+ assertNull(ugly.campaign);
+ }
+}
diff --git a/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestGeckoBackgroundThread.java b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestGeckoBackgroundThread.java
new file mode 100644
index 0000000000..cbf9dffe33
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestGeckoBackgroundThread.java
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.tests.browser.junit3;
+
+import android.test.InstrumentationTestCase;
+import org.mozilla.gecko.util.ThreadUtils;
+
+public class TestGeckoBackgroundThread extends InstrumentationTestCase {
+
+ private boolean finishedTest;
+ private boolean ranFirstRunnable;
+
+ public void testGeckoBackgroundThread() throws InterruptedException {
+
+ final Thread testThread = Thread.currentThread();
+
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ public void run() {
+ // Must *not* be on thread that posted the Runnable.
+ assertFalse(ThreadUtils.isOnThread(testThread));
+
+ // Must be on background thread.
+ assertTrue(ThreadUtils.isOnBackgroundThread());
+
+ ranFirstRunnable = true;
+ }
+ });
+
+ // Post a second Runnable to make sure it still runs on the background thread,
+ // and it only runs after the first Runnable has run.
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ public void run() {
+ // Must still be on background thread.
+ assertTrue(ThreadUtils.isOnBackgroundThread());
+
+ // This Runnable must be run after the first Runnable had finished.
+ assertTrue(ranFirstRunnable);
+
+ synchronized (TestGeckoBackgroundThread.this) {
+ finishedTest = true;
+ TestGeckoBackgroundThread.this.notify();
+ }
+ }
+ });
+
+ synchronized (this) {
+ while (!finishedTest) {
+ wait();
+ }
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestGeckoMenu.java b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestGeckoMenu.java
new file mode 100644
index 0000000000..15be260041
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestGeckoMenu.java
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.tests.browser.junit3;
+
+import android.test.InstrumentationTestCase;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.menu.GeckoMenu;
+import org.mozilla.gecko.util.ThreadUtils;
+
+public class TestGeckoMenu extends InstrumentationTestCase {
+
+ private volatile Exception exception;
+ private void setException(Exception e) {
+ this.exception = e;
+ }
+
+ public void testMenuThreading() throws InterruptedException {
+ final GeckoMenu menu = new GeckoMenu(getInstrumentation().getTargetContext());
+ final Object semaphore = new Object();
+
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ menu.add("test1");
+ } catch (Exception e) {
+ setException(e);
+ }
+
+ synchronized (semaphore) {
+ semaphore.notify();
+ }
+ }
+ });
+ synchronized (semaphore) {
+ semaphore.wait();
+ }
+
+ // No exception thrown if called on UI thread.
+ assertNull(exception);
+
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ menu.add("test2");
+ } catch (Exception e) {
+ setException(e);
+ }
+
+ synchronized (semaphore) {
+ semaphore.notify();
+ }
+ }
+ }).start();
+
+ synchronized (semaphore) {
+ semaphore.wait();
+ }
+
+ if (AppConstants.RELEASE_OR_BETA) {
+ // No exception thrown: release build.
+ assertNull(exception);
+ return;
+ }
+
+ assertNotNull(exception);
+ assertEquals(exception.getClass(), IllegalThreadStateException.class);
+ }
+}
diff --git a/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestGeckoProfilesProvider.java b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestGeckoProfilesProvider.java
new file mode 100644
index 0000000000..2b1da3295f
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestGeckoProfilesProvider.java
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.tests.browser.junit3;
+
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.test.InstrumentationTestCase;
+import org.mozilla.gecko.db.BrowserContract;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+public class TestGeckoProfilesProvider extends InstrumentationTestCase {
+ private static final String[] NAME_AND_PATH = new String[] { BrowserContract.Profiles.NAME, BrowserContract.Profiles.PATH };
+
+ /**
+ * Ensure that the default profile is found in the results from the provider.
+ */
+ public void testQueryDefault() throws RemoteException {
+ final ContentResolver contentResolver = getInstrumentation().getContext().getContentResolver();
+ final Uri uri = BrowserContract.PROFILES_AUTHORITY_URI.buildUpon().appendPath("profiles").build();
+ final Cursor c = contentResolver.query(uri, NAME_AND_PATH, null, null, null);
+ assertNotNull(c);
+ try {
+ assertTrue(c.moveToFirst());
+ assertTrue(c.getCount() > 0);
+ Map<String, String> profiles = new HashMap<String, String>();
+ while (!c.isAfterLast()) {
+ final String name = c.getString(0);
+ final String path = c.getString(1);
+ profiles.put(name, path);
+ c.moveToNext();
+ }
+
+ assertTrue(profiles.containsKey("default"));
+ final String path = profiles.get("default");
+ assertTrue(path.endsWith(".default")); // It's the right profile...
+ assertTrue(path.startsWith("/data/")); // ... in the 'data' dir...
+ assertTrue(path.contains("/mozilla/")); // ... in the 'mozilla' dir.
+ assertTrue(new File(path).exists());
+ } finally {
+ c.close();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestGeckoSharedPrefs.java b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestGeckoSharedPrefs.java
new file mode 100644
index 0000000000..9e35cab354
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestGeckoSharedPrefs.java
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.tests.browser.junit3;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.preference.PreferenceManager;
+import android.test.InstrumentationTestCase;
+import android.test.RenamingDelegatingContext;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.GeckoSharedPrefs.Flags;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Test GeckoSharedPrefs migrations.
+ */
+public class TestGeckoSharedPrefs extends InstrumentationTestCase {
+
+ private static class TestContext extends RenamingDelegatingContext {
+ private static final String PREFIX = "TestGeckoSharedPrefs-";
+
+ private final Set<String> usedPrefs;
+
+ public TestContext(Context context) {
+ super(context, PREFIX);
+ usedPrefs = Collections.synchronizedSet(new HashSet<String>());
+ }
+
+ @Override
+ public SharedPreferences getSharedPreferences(String name, int mode) {
+ usedPrefs.add(name);
+ return super.getSharedPreferences(PREFIX + name, mode);
+ }
+
+ public void clearUsedPrefs() {
+ for (String prefsName : usedPrefs) {
+ getSharedPreferences(prefsName, 0).edit().clear().commit();
+ }
+
+ usedPrefs.clear();
+ }
+ }
+
+ private static final EnumSet<Flags> disableMigrations = EnumSet.of(Flags.DISABLE_MIGRATIONS);
+
+ private TestContext context;
+
+ protected void setUp() {
+ context = new TestContext(getInstrumentation().getTargetContext());
+ }
+
+ protected void tearDown() {
+ context.clearUsedPrefs();
+ GeckoSharedPrefs.reset();
+ }
+
+ public void testDisableMigrations() {
+ // Version is 0 before any migration
+ assertEquals(0, GeckoSharedPrefs.getVersion(context));
+
+ // Get prefs with migrations disabled
+ GeckoSharedPrefs.forApp(context, disableMigrations);
+ GeckoSharedPrefs.forProfile(context, disableMigrations);
+ GeckoSharedPrefs.forProfileName(context, "someProfile", disableMigrations);
+
+ // Version should still be 0
+ assertEquals(0, GeckoSharedPrefs.getVersion(context));
+ }
+
+ public void testPrefsVersion() {
+ // Version is 0 before any migration
+ assertEquals(0, GeckoSharedPrefs.getVersion(context));
+
+ // Trigger migration by getting a SharedPreferences instance
+ GeckoSharedPrefs.forApp(context);
+
+ // Version should be current after migration
+ assertEquals(GeckoSharedPrefs.PREFS_VERSION, GeckoSharedPrefs.getVersion(context));
+ }
+
+ public void testMigrateFromPreferenceManager() {
+ SharedPreferences appPrefs = GeckoSharedPrefs.forApp(context, disableMigrations);
+ assertTrue(appPrefs.getAll().isEmpty());
+ final Editor appEditor = appPrefs.edit();
+
+ SharedPreferences profilePrefs = GeckoSharedPrefs.forProfileName(context, GeckoProfile.DEFAULT_PROFILE, disableMigrations);
+ assertTrue(profilePrefs.getAll().isEmpty());
+ final Editor profileEditor = profilePrefs.edit();
+
+ final SharedPreferences pmPrefs = PreferenceManager.getDefaultSharedPreferences(context);
+ assertTrue(pmPrefs.getAll().isEmpty());
+ Editor pmEditor = pmPrefs.edit();
+
+ // Insert a key for each type to exercise the
+ // migration path a bit more thoroughly.
+ pmEditor.putInt("int_key", 23);
+ pmEditor.putLong("long_key", 23L);
+ pmEditor.putString("string_key", "23");
+ pmEditor.putFloat("float_key", 23.3f);
+
+ final String[] profileKeys = {
+ "string_profile",
+ "int_profile"
+ };
+
+ // Insert keys that are expected to be moved to the
+ // PROFILE scope.
+ pmEditor.putString(profileKeys[0], "24");
+ pmEditor.putInt(profileKeys[1], 24);
+
+ // Commit changes to PreferenceManager
+ pmEditor.commit();
+ assertEquals(6, pmPrefs.getAll().size());
+
+ // Perform actual migration with the given editors
+ pmEditor = GeckoSharedPrefs.migrateFromPreferenceManager(context, appEditor,
+ profileEditor, Arrays.asList(profileKeys));
+
+ // Commit changes applied during the migration
+ appEditor.commit();
+ profileEditor.commit();
+ pmEditor.commit();
+
+ // PreferenceManager should have no keys
+ assertTrue(pmPrefs.getAll().isEmpty());
+
+ // App should have all keys except the profile ones
+ assertEquals(4, appPrefs.getAll().size());
+
+ // Ensure app scope doesn't have any of the profile keys
+ for (int i = 0; i < profileKeys.length; i++) {
+ assertFalse(appPrefs.contains(profileKeys[i]));
+ }
+
+ // Check app keys
+ assertEquals(23, appPrefs.getInt("int_key", 0));
+ assertEquals(23L, appPrefs.getLong("long_key", 0L));
+ assertEquals("23", appPrefs.getString("string_key", ""));
+ assertEquals(23.3f, appPrefs.getFloat("float_key", 0));
+
+ assertEquals(2, profilePrefs.getAll().size());
+ assertEquals("24", profilePrefs.getString(profileKeys[0], ""));
+ assertEquals(24, profilePrefs.getInt(profileKeys[1], 0));
+ }
+}
diff --git a/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestImageDownloader.java b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestImageDownloader.java
new file mode 100644
index 0000000000..e0cf9bc63a
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestImageDownloader.java
@@ -0,0 +1,205 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.tests.browser.junit3;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.test.InstrumentationTestCase;
+import android.test.RenamingDelegatingContext;
+import android.test.mock.MockResources;
+import android.util.DisplayMetrics;
+import org.mozilla.gecko.distribution.Distribution;
+import org.mozilla.gecko.home.ImageLoader.ImageDownloader;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class TestImageDownloader extends InstrumentationTestCase {
+ private static class TestContext extends RenamingDelegatingContext {
+ private static final String PREFIX = "TestImageDownloader-";
+
+ private final Resources resources;
+ private final Set<String> usedPrefs;
+
+ public TestContext(Context context) {
+ super(context, PREFIX);
+ resources = new TestResources();
+ usedPrefs = Collections.synchronizedSet(new HashSet<String>());
+ }
+
+ @Override
+ public Resources getResources() {
+ return resources;
+ }
+
+ @Override
+ public SharedPreferences getSharedPreferences(String name, int mode) {
+ usedPrefs.add(name);
+ return super.getSharedPreferences(PREFIX + name, mode);
+ }
+
+ public void clearUsedPrefs() {
+ for (String prefsName : usedPrefs) {
+ getSharedPreferences(prefsName, 0).edit().clear().commit();
+ }
+
+ usedPrefs.clear();
+ }
+ }
+
+ private static class TestResources extends MockResources {
+ private final DisplayMetrics metrics;
+
+ public TestResources() {
+ metrics = new DisplayMetrics();
+ }
+
+ @Override
+ public DisplayMetrics getDisplayMetrics() {
+ return metrics;
+ }
+
+ public void setDensityDpi(int densityDpi) {
+ metrics.densityDpi = densityDpi;
+ }
+ }
+
+ private static class TestDistribution extends Distribution {
+ final List<String> accessedFiles;
+
+ public TestDistribution(Context context) {
+ super(context);
+ accessedFiles = new ArrayList<String>();
+ }
+
+ @Override
+ public File getDistributionFile(String name) {
+ accessedFiles.add(name);
+
+ // Return null to ensure the ImageDownloader will go
+ // through a complete density lookup for each filename.
+ return null;
+ }
+
+ public List<String> getAccessedFiles() {
+ return Collections.unmodifiableList(accessedFiles);
+ }
+
+ public void resetAccessedFiles() {
+ accessedFiles.clear();
+ }
+ }
+
+ private TestContext context;
+ private TestResources resources;
+ private TestDistribution distribution;
+ private ImageDownloader downloader;
+
+ protected void setUp() {
+ context = new TestContext(getInstrumentation().getTargetContext());
+ resources = (TestResources) context.getResources();
+ distribution = new TestDistribution(context);
+ downloader = new ImageDownloader(context, distribution);
+ }
+
+ protected void tearDown() {
+ context.clearUsedPrefs();
+ }
+
+ private void triggerLoad(Uri uri) {
+ try {
+ downloader.load(uri, false);
+ } catch (IOException e) {
+ // Ignore any IO exceptions.
+ }
+ }
+
+ private void checkAccessedFiles(String[] filenames) {
+ List<String> accessedFiles = distribution.getAccessedFiles();
+
+ for (int i = 0; i < filenames.length; i++) {
+ assertEquals(filenames[i], accessedFiles.get(i));
+ }
+ }
+
+ private void checkAccessedFilesForUri(Uri uri, int densityDpi, String[] filenames) {
+ resources.setDensityDpi(densityDpi);
+ triggerLoad(uri);
+ checkAccessedFiles(filenames);
+ distribution.resetAccessedFiles();
+ }
+
+ public void testAccessedFiles() {
+ // Filename only.
+ checkAccessedFilesForUri(Uri.parse("gecko.distribution://file"),
+ DisplayMetrics.DENSITY_MEDIUM,
+ new String[] {
+ "mdpi/file.png",
+ "xhdpi/file.png",
+ "hdpi/file.png"
+ });
+
+ // Directory and filename.
+ checkAccessedFilesForUri(Uri.parse("gecko.distribution://dir/file"),
+ DisplayMetrics.DENSITY_MEDIUM,
+ new String[] {
+ "dir/mdpi/file.png",
+ "dir/xhdpi/file.png",
+ "dir/hdpi/file.png"
+ });
+
+ // Sub-directories and filename.
+ checkAccessedFilesForUri(Uri.parse("gecko.distribution://dir/subdir/file"),
+ DisplayMetrics.DENSITY_MEDIUM,
+ new String[] {
+ "dir/subdir/mdpi/file.png",
+ "dir/subdir/xhdpi/file.png",
+ "dir/subdir/hdpi/file.png"
+ });
+ }
+
+ public void testDensityLookup() {
+ Uri uri = Uri.parse("gecko.distribution://file");
+
+ // Medium density
+ checkAccessedFilesForUri(uri,
+ DisplayMetrics.DENSITY_MEDIUM,
+ new String[] {
+ "mdpi/file.png",
+ "xhdpi/file.png",
+ "hdpi/file.png"
+ });
+
+ checkAccessedFilesForUri(uri,
+ DisplayMetrics.DENSITY_HIGH,
+ new String[] {
+ "hdpi/file.png",
+ "xxhdpi/file.png",
+ "xhdpi/file.png"
+ });
+
+ checkAccessedFilesForUri(uri,
+ DisplayMetrics.DENSITY_XHIGH,
+ new String[] {
+ "xhdpi/file.png",
+ "xxhdpi/file.png",
+ "mdpi/file.png"
+ });
+
+
+ checkAccessedFilesForUri(uri,
+ DisplayMetrics.DENSITY_XXHIGH,
+ new String[] {
+ "xxhdpi/file.png",
+ "hdpi/file.png"
+ });
+ }
+}
diff --git a/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestJarReader.java b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestJarReader.java
new file mode 100644
index 0000000000..125ffe9331
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestJarReader.java
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.tests.browser.junit3;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Stack;
+
+import android.test.InstrumentationTestCase;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.util.FileUtils;
+import org.mozilla.gecko.util.GeckoJarReader;
+
+import android.content.Context;
+
+/**
+ * A basic jar reader test. Tests reading a png from fennec's apk, as well as
+ * loading some invalid jar urls.
+ */
+public class TestJarReader extends InstrumentationTestCase {
+ public void testJarReader() {
+ final Context context = getInstrumentation().getTargetContext().getApplicationContext();
+ String appPath = getInstrumentation().getTargetContext().getPackageResourcePath();
+ assertNotNull(appPath);
+
+ // Test reading a file from a jar url that looks correct.
+ String url = "jar:file://" + appPath + "!/" + AppConstants.OMNIJAR_NAME;
+ InputStream stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
+ assertNotNull(stream);
+
+ // Test looking for an non-existent file in a jar.
+ url = "jar:file://" + appPath + "!/" + AppConstants.OMNIJAR_NAME;
+ stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/nonexistent_file.png");
+ assertNull(stream);
+
+ // Test looking for a file that doesn't exist in the APK.
+ url = "jar:file://" + appPath + "!/" + "BAD" + AppConstants.OMNIJAR_NAME;
+ stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
+ assertNull(stream);
+
+ // Test looking for a file that doesn't exist in the APK.
+ // Bug 1174922, prefixed string / length error.
+ url = "jar:file://" + appPath + "!/" + AppConstants.OMNIJAR_NAME + "BAD";
+ stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
+ assertNull(stream);
+
+ // Test looking for an jar with an invalid url.
+ url = "jar:file://" + appPath + "!" + "!/" + AppConstants.OMNIJAR_NAME;
+ stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/nonexistent_file.png");
+ assertNull(stream);
+
+ // Test looking for a file that doesn't exist on disk.
+ url = "jar:file://" + appPath + "BAD" + "!/" + AppConstants.OMNIJAR_NAME;
+ stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
+ assertNull(stream);
+ }
+
+ protected void assertExtractStream(String url) throws IOException {
+ final File file = GeckoJarReader.extractStream(getInstrumentation().getTargetContext(), url, getInstrumentation().getContext().getCacheDir(), ".test");
+ assertNotNull(file);
+ try {
+ assertTrue(file.getName().endsWith("test"));
+ final String contents = FileUtils.readStringFromFile(file);
+ assertNotNull(contents);
+ assertTrue(contents.length() > 0);
+ } finally {
+ file.delete();
+ }
+ }
+
+ public void testExtractStream() throws IOException {
+ String appPath = getInstrumentation().getTargetContext().getPackageResourcePath();
+ assertNotNull(appPath);
+
+ // We don't have a lot of good files to choose from. package-name.txt isn't included in Gradle APKs.
+ assertExtractStream("jar:file://" + appPath + "!/resources.arsc");
+
+ final String url = GeckoJarReader.getJarURL(getInstrumentation().getTargetContext(), "chrome.manifest");
+ assertExtractStream(url);
+
+ // Now use an extracted copy of chrome.manifest to test further.
+ final File file = GeckoJarReader.extractStream(getInstrumentation().getTargetContext(), url, getInstrumentation().getContext().getCacheDir(), ".test");
+ assertNotNull(file);
+ try {
+ assertExtractStream("file://" + file.getAbsolutePath()); // file:// URI.
+ assertExtractStream(file.getAbsolutePath()); // Vanilla path.
+ } finally {
+ file.delete();
+ }
+ }
+
+ protected void assertExtractStreamFails(String url) throws IOException {
+ final File file = GeckoJarReader.extractStream(getInstrumentation().getTargetContext(), url, getInstrumentation().getContext().getCacheDir(), ".test");
+ assertNull(file);
+ }
+
+ public void testExtractStreamFailureCases() throws IOException {
+ String appPath = getInstrumentation().getTargetContext().getPackageResourcePath();
+ assertNotNull(appPath);
+
+ // First, a bad APK.
+ assertExtractStreamFails("jar:file://" + appPath + "BAD!/resources.arsc");
+
+ // Second, a bad file in the APK.
+ assertExtractStreamFails("jar:file://" + appPath + "!/BADresources.arsc");
+
+ // Now a bad file in the omnijar.
+ final String badUrl = GeckoJarReader.getJarURL(getInstrumentation().getTargetContext(), "BADchrome.manifest");
+ assertExtractStreamFails(badUrl);
+
+ // Now use an extracted copy of chrome.manifest to test further.
+ final String goodUrl = GeckoJarReader.getJarURL(getInstrumentation().getTargetContext(), "chrome.manifest");
+ final File file = GeckoJarReader.extractStream(getInstrumentation().getTargetContext(), goodUrl, getInstrumentation().getContext().getCacheDir(), ".test");
+ assertNotNull(file);
+ try {
+ assertExtractStreamFails("file://" + file.getAbsolutePath() + "BAD"); // Bad file:// URI.
+ assertExtractStreamFails(file.getAbsolutePath() + "BAD"); //Bad vanilla path.
+ } finally {
+ file.delete();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestRawResource.java b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestRawResource.java
new file mode 100644
index 0000000000..8e27cbe9c5
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestRawResource.java
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.tests.browser.junit3;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.test.InstrumentationTestCase;
+import android.test.mock.MockContext;
+import android.test.mock.MockResources;
+import org.mozilla.gecko.util.RawResource;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Tests whether RawResource.getAsString() produces the right String
+ * result after reading the returned raw resource's InputStream.
+ */
+public class TestRawResource extends InstrumentationTestCase {
+ private static final int RAW_RESOURCE_ID = 1;
+ private static final String RAW_CONTENTS = "RAW";
+
+ private static class TestContext extends MockContext {
+ private final Resources resources;
+
+ public TestContext() {
+ resources = new TestResources();
+ }
+
+ @Override
+ public Resources getResources() {
+ return resources;
+ }
+ }
+
+ /**
+ * Browser instrumentation tests can't have access to test-only
+ * resources (bug 994135) yet so we mock the access to resources
+ * for now.
+ */
+ private static class TestResources extends MockResources {
+ @Override
+ public InputStream openRawResource(int id) {
+ if (id == RAW_RESOURCE_ID) {
+ return new ByteArrayInputStream(RAW_CONTENTS.getBytes());
+ }
+
+ return null;
+ }
+ }
+
+ public void testGet() {
+ Context context = new TestContext();
+ String result;
+
+ try {
+ result = RawResource.getAsString(context, RAW_RESOURCE_ID);
+ } catch (IOException e) {
+ result = null;
+ }
+
+ assertEquals(RAW_CONTENTS, result);
+ }
+}
diff --git a/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestSuggestedSites.java b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestSuggestedSites.java
new file mode 100644
index 0000000000..c29c369d68
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestSuggestedSites.java
@@ -0,0 +1,473 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.tests.browser.junit3;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.SystemClock;
+import android.test.InstrumentationTestCase;
+import android.test.RenamingDelegatingContext;
+import android.test.mock.MockResources;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.Locales;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.SuggestedSites;
+import org.mozilla.gecko.distribution.Distribution;
+import org.mozilla.gecko.preferences.GeckoPreferences;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class TestSuggestedSites extends InstrumentationTestCase {
+ private static class TestContext extends RenamingDelegatingContext {
+ private static final String PREFIX = "TestSuggestedSites-";
+
+ private final Resources resources;
+ private final Set<String> usedPrefs;
+
+ public TestContext(Context context) {
+ super(context, PREFIX);
+ resources = new TestResources();
+ usedPrefs = Collections.synchronizedSet(new HashSet<String>());
+ }
+
+ @Override
+ public Resources getResources() {
+ return resources;
+ }
+
+ @Override
+ public SharedPreferences getSharedPreferences(String name, int mode) {
+ usedPrefs.add(name);
+ return super.getSharedPreferences(PREFIX + name, mode);
+ }
+
+ public void clearUsedPrefs() {
+ for (String prefsName : usedPrefs) {
+ getSharedPreferences(prefsName, 0).edit().clear().commit();
+ }
+
+ usedPrefs.clear();
+ }
+ }
+
+ private static class TestResources extends MockResources {
+ private String suggestedSites;
+
+ @Override
+ public InputStream openRawResource(int id) {
+ if (id == R.raw.suggestedsites && suggestedSites != null) {
+ return new ByteArrayInputStream(suggestedSites.getBytes());
+ }
+
+ return null;
+ }
+
+ public void setSuggestedSitesResource(String suggestedSites) {
+ this.suggestedSites = suggestedSites;
+ }
+ }
+
+ private static class TestDistribution extends Distribution {
+ private final Map<Locale, File> filesPerLocale;
+
+ public TestDistribution(Context context) {
+ super(context);
+ this.filesPerLocale = new HashMap<Locale, File>();
+ }
+
+ @Override
+ public File getDistributionFile(String name) {
+ for (Locale locale : filesPerLocale.keySet()) {
+ if (name.startsWith("suggestedsites/locales/" + Locales.getLanguageTag(locale))) {
+ return filesPerLocale.get(locale);
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public boolean exists() {
+ return true;
+ }
+
+ public void setFileForLocale(Locale locale, File file) {
+ filesPerLocale.put(locale, file);
+ }
+
+ public void start() {
+ doInit();
+ }
+ }
+
+ class TestObserver extends ContentObserver {
+ private final CountDownLatch changeLatch;
+
+ public TestObserver(CountDownLatch changeLatch) {
+ super(null);
+ this.changeLatch = changeLatch;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ changeLatch.countDown();
+ }
+ }
+
+ private static final int DEFAULT_LIMIT = 6;
+
+ private static final String DIST_PREFIX = "dist";
+
+ private TestContext context;
+ private TestResources resources;
+ private List<File> tempFiles;
+
+ private String generateSites(int n) {
+ return generateSites(n, "");
+ }
+
+ private String generateSites(int n, String prefix) {
+ JSONArray sites = new JSONArray();
+
+ try {
+ for (int i = 0; i < n; i++) {
+ JSONObject site = new JSONObject();
+ site.put("url", prefix + "url" + i);
+ site.put("title", prefix + "title" + i);
+ site.put("imageurl", prefix + "imageUrl" + i);
+ site.put("bgcolor", prefix + "bgColor" + i);
+
+ sites.put(site);
+ }
+ } catch (Exception e) {
+ return "";
+ }
+
+ return sites.toString();
+ }
+
+ private File createDistSuggestedSitesFile(int n) {
+ FileOutputStream fos = null;
+
+ try {
+ File distFile = File.createTempFile("distrosites", ".json",
+ context.getCacheDir());
+
+ fos = new FileOutputStream(distFile);
+ fos.write(generateSites(n, DIST_PREFIX).getBytes());
+
+ return distFile;
+ } catch (IOException e) {
+ fail("Failed to create temp suggested sites file");
+ } finally {
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException e) {
+ // Ignore.
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private void checkCursorCount(String content, int expectedCount) {
+ checkCursorCount(content, expectedCount, DEFAULT_LIMIT);
+ }
+
+ private void checkCursorCount(String content, int expectedCount, int limit) {
+ resources.setSuggestedSitesResource(content);
+ Cursor c = new SuggestedSites(context).get(limit);
+ assertEquals(expectedCount, c.getCount());
+ c.close();
+ }
+
+ protected void setUp() {
+ context = new TestContext(getInstrumentation().getTargetContext());
+ resources = (TestResources) context.getResources();
+ tempFiles = new ArrayList<File>();
+ }
+
+ protected void tearDown() {
+ context.clearUsedPrefs();
+ for (File f : tempFiles) {
+ f.delete();
+ }
+ }
+
+ public void testCount() {
+ // Empty array = empty cursor
+ checkCursorCount(generateSites(0), 0);
+
+ // 2 items = cursor with 2 rows
+ checkCursorCount(generateSites(2), 2);
+
+ // 10 items with lower limit = cursor respects limit
+ checkCursorCount(generateSites(10), 3, 3);
+ }
+
+ public void testEmptyCursor() {
+ // Null resource = empty cursor
+ checkCursorCount(null, 0);
+
+ // Empty string = empty cursor
+ checkCursorCount("", 0);
+
+ // Invalid json string = empty cursor
+ checkCursorCount("{ broken: }", 0);
+ }
+
+ public void testCursorContent() {
+ resources.setSuggestedSitesResource(generateSites(3));
+
+ Cursor c = new SuggestedSites(context).get(DEFAULT_LIMIT);
+ assertEquals(3, c.getCount());
+
+ c.moveToPosition(-1);
+ while (c.moveToNext()) {
+ int position = c.getPosition();
+
+ String url = c.getString(c.getColumnIndexOrThrow(BrowserContract.SuggestedSites.URL));
+ assertEquals("url" + position, url);
+
+ String title = c.getString(c.getColumnIndexOrThrow(BrowserContract.SuggestedSites.TITLE));
+ assertEquals("title" + position, title);
+ }
+
+ c.close();
+ }
+
+ public void testExcludeUrls() {
+ resources.setSuggestedSitesResource(generateSites(6));
+
+ List<String> excludedUrls = new ArrayList<String>(3);
+ excludedUrls.add("url1");
+ excludedUrls.add("url3");
+ excludedUrls.add("url5");
+
+ List<String> includedUrls = new ArrayList<String>(3);
+ includedUrls.add("url0");
+ includedUrls.add("url2");
+ includedUrls.add("url4");
+
+ Cursor c = new SuggestedSites(context).get(DEFAULT_LIMIT, excludedUrls);
+
+ c.moveToPosition(-1);
+ while (c.moveToNext()) {
+ String url = c.getString(c.getColumnIndexOrThrow(BrowserContract.SuggestedSites.URL));
+ assertFalse(excludedUrls.contains(url));
+ assertTrue(includedUrls.contains(url));
+ }
+
+ c.close();
+ }
+
+ public void testHiddenSites() {
+ resources.setSuggestedSitesResource(generateSites(6));
+
+ List<String> visibleUrls = new ArrayList<String>(3);
+ visibleUrls.add("url3");
+ visibleUrls.add("url4");
+ visibleUrls.add("url5");
+
+ List<String> hiddenUrls = new ArrayList<String>(3);
+ hiddenUrls.add("url0");
+ hiddenUrls.add("url1");
+ hiddenUrls.add("url2");
+
+ // Add mocked hidden sites to SharedPreferences.
+ StringBuilder hiddenUrlBuilder = new StringBuilder();
+ for (String s : hiddenUrls) {
+ hiddenUrlBuilder.append(" ");
+ hiddenUrlBuilder.append(Uri.encode(s));
+ }
+
+ final String hiddenPref = hiddenUrlBuilder.toString();
+ GeckoSharedPrefs.forProfile(context).edit()
+ .putString(SuggestedSites.PREF_SUGGESTED_SITES_HIDDEN, hiddenPref)
+ .commit();
+
+ Cursor c = new SuggestedSites(context).get(DEFAULT_LIMIT);
+ assertEquals(Math.min(3, DEFAULT_LIMIT), c.getCount());
+
+ c.moveToPosition(-1);
+ while (c.moveToNext()) {
+ String url = c.getString(c.getColumnIndexOrThrow(BrowserContract.SuggestedSites.URL));
+ assertFalse(hiddenUrls.contains(url));
+ assertTrue(visibleUrls.contains(url));
+ }
+
+ c.close();
+ }
+
+ public void testImageUrlAndBgColor() {
+ final int count = 3;
+ resources.setSuggestedSitesResource(generateSites(count));
+
+ SuggestedSites suggestedSites = new SuggestedSites(context);
+
+ // Suggested sites hasn't been loaded yet.
+ for (int i = 0; i < count; i++) {
+ String url = "url" + i;
+ assertFalse(suggestedSites.contains(url));
+ assertNull(suggestedSites.getImageUrlForUrl(url));
+ assertNull(suggestedSites.getBackgroundColorForUrl(url));
+ }
+
+ Cursor c = suggestedSites.get(DEFAULT_LIMIT);
+ c.moveToPosition(-1);
+
+ // We should have cached results after the get() call.
+ while (c.moveToNext()) {
+ String url = c.getString(c.getColumnIndexOrThrow(BrowserContract.SuggestedSites.URL));
+ assertTrue(suggestedSites.contains(url));
+ assertEquals("imageUrl" + c.getPosition(),
+ suggestedSites.getImageUrlForUrl(url));
+ assertEquals("bgColor" + c.getPosition(),
+ suggestedSites.getBackgroundColorForUrl(url));
+ }
+ c.close();
+
+ // No valid values for unknown URLs.
+ assertFalse(suggestedSites.contains("foo"));
+ assertNull(suggestedSites.getImageUrlForUrl("foo"));
+ assertNull(suggestedSites.getBackgroundColorForUrl("foo"));
+ }
+
+ public void testLocaleChanges() {
+ resources.setSuggestedSitesResource(generateSites(3));
+
+ SuggestedSites suggestedSites = new SuggestedSites(context);
+
+ // Initial load with predefined locale
+ Cursor c = suggestedSites.get(DEFAULT_LIMIT, Locale.UK);
+ assertEquals(3, c.getCount());
+ c.close();
+
+ resources.setSuggestedSitesResource(generateSites(5));
+
+ // Second load with same locale should return same results
+ // even though the contents of the resource have changed.
+ c = suggestedSites.get(DEFAULT_LIMIT, Locale.UK);
+ assertEquals(3, c.getCount());
+ c.close();
+
+ // Changing the locale forces the cached list to be refreshed.
+ c = suggestedSites.get(DEFAULT_LIMIT, Locale.US);
+ assertEquals(5, c.getCount());
+ c.close();
+ }
+
+ public void testDistribution() {
+ final int DIST_COUNT = 2;
+ final int DEFAULT_COUNT = 3;
+
+ File sitesFile = new File(context.getCacheDir(),
+ "suggestedsites-" + SystemClock.uptimeMillis() + ".json");
+ tempFiles.add(sitesFile);
+ assertFalse(sitesFile.exists());
+
+ File distFile = createDistSuggestedSitesFile(DIST_COUNT);
+ tempFiles.add(distFile);
+ assertTrue(distFile.exists());
+
+ final CountDownLatch changeLatch = new CountDownLatch(1);
+
+ // Watch for change notifications on suggested sites.
+ ContentResolver cr = context.getContentResolver();
+ ContentObserver observer = new TestObserver(changeLatch);
+ cr.registerContentObserver(BrowserContract.SuggestedSites.CONTENT_URI,
+ false, observer);
+
+ // Init distribution with the mock file.
+ TestDistribution distribution = new TestDistribution(context);
+ distribution.setFileForLocale(Locale.getDefault(), distFile);
+ distribution.start();
+
+ // Init suggested sites with default values.
+ resources.setSuggestedSitesResource(generateSites(DEFAULT_COUNT));
+ SuggestedSites suggestedSites =
+ new SuggestedSites(context, distribution, sitesFile);
+
+ // The initial query will not contain the distribution sites
+ // yet. This will happen asynchronously once the distribution
+ // is installed.
+ Cursor c1 = null;
+ try {
+ c1 = suggestedSites.get(DEFAULT_LIMIT);
+ assertEquals(DEFAULT_COUNT, c1.getCount());
+ } finally {
+ if (c1 != null) {
+ c1.close();
+ }
+ }
+
+ try {
+ assertTrue(changeLatch.await(5, TimeUnit.SECONDS));
+ } catch (InterruptedException ie) {
+ fail("No change notification after fetching distribution file");
+ }
+
+ // Target file should exist after distribution is deployed.
+ assertTrue(sitesFile.exists());
+ cr.unregisterContentObserver(observer);
+
+ Cursor c2 = null;
+ try {
+ c2 = suggestedSites.get(DEFAULT_LIMIT);
+
+ // The next query should contain the distribution contents.
+ assertEquals(DIST_COUNT + DEFAULT_COUNT, c2.getCount());
+
+ // The first items should be from the distribution
+ for (int i = 0; i < DIST_COUNT; i++) {
+ c2.moveToPosition(i);
+
+ String url = c2.getString(c2.getColumnIndexOrThrow(BrowserContract.SuggestedSites.URL));
+ assertEquals(DIST_PREFIX + "url" + i, url);
+
+ String title = c2.getString(c2.getColumnIndexOrThrow(BrowserContract.SuggestedSites.TITLE));
+ assertEquals(DIST_PREFIX + "title" + i, title);
+ }
+
+ // The remaining items should be the default ones
+ for (int i = 0; i < c2.getCount() - DIST_COUNT; i++) {
+ c2.moveToPosition(i + DIST_COUNT);
+
+ String url = c2.getString(c2.getColumnIndexOrThrow(BrowserContract.SuggestedSites.URL));
+ assertEquals("url" + i, url);
+
+ String title = c2.getString(c2.getColumnIndexOrThrow(BrowserContract.SuggestedSites.TITLE));
+ assertEquals("title" + i, title);
+ }
+ } finally {
+ if (c2 != null) {
+ c2.close();
+ }
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/harness/BrowserInstrumentationTestRunner.java b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/harness/BrowserInstrumentationTestRunner.java
new file mode 100644
index 0000000000..36c60d92ed
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/harness/BrowserInstrumentationTestRunner.java
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.tests.browser.junit3.harness;
+
+import android.os.Bundle;
+import android.test.AndroidTestRunner;
+import android.test.InstrumentationTestRunner;
+import android.util.Log;
+
+/**
+ * A test runner that installs a special test listener.
+ * <p>
+ * In future, this listener will turn JUnit 3 test events into log messages in
+ * the format that Mochitest parsers understand.
+ */
+public class BrowserInstrumentationTestRunner extends InstrumentationTestRunner {
+ private static final String LOG_TAG = "BInstTestRunner";
+
+ @Override
+ public void onCreate(Bundle arguments) {
+ Log.d(LOG_TAG, "onCreate");
+ super.onCreate(arguments);
+ }
+
+ @Override
+ protected AndroidTestRunner getAndroidTestRunner() {
+ Log.d(LOG_TAG, "getAndroidTestRunner");
+ AndroidTestRunner testRunner = super.getAndroidTestRunner();
+ testRunner.addTestListener(new BrowserTestListener());
+ return testRunner;
+ }
+}
diff --git a/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/harness/BrowserTestListener.java b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/harness/BrowserTestListener.java
new file mode 100644
index 0000000000..026d5065f0
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/harness/BrowserTestListener.java
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.tests.browser.junit3.harness;
+
+import android.util.Log;
+import junit.framework.AssertionFailedError;
+import junit.framework.Test;
+import junit.framework.TestListener;
+
+/**
+ * BrowserTestListener turns JUnit 3 test events into log messages in the format
+ * that Mochitest parsers understand.
+ * <p>
+ * The idea is that, on infrastructure, we'll be able to use the same test
+ * parsing code for Browser JUnit 3 tests as we do for Robocop tests.
+ * <p>
+ * In future, that is!
+ */
+public class BrowserTestListener implements TestListener {
+ public static final String LOG_TAG = "BTestListener";
+
+ @Override
+ public void startTest(Test test) {
+ Log.d(LOG_TAG, "startTest: " + test);
+ }
+
+ @Override
+ public void endTest(Test test) {
+ Log.d(LOG_TAG, "endTest: " + test);
+ }
+
+ @Override
+ public void addFailure(Test test, AssertionFailedError t) {
+ Log.d(LOG_TAG, "addFailure: " + test);
+ }
+
+ @Override
+ public void addError(Test test, Throwable t) {
+ Log.d(LOG_TAG, "addError: " + test);
+ }
+}
diff --git a/mobile/android/tests/browser/moz.build b/mobile/android/tests/browser/moz.build
new file mode 100644
index 0000000000..ca1214e445
--- /dev/null
+++ b/mobile/android/tests/browser/moz.build
@@ -0,0 +1,17 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MOCHITEST_CHROME_MANIFESTS += ['chrome/chrome.ini']
+
+if not CONFIG['MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE']:
+ TEST_DIRS += [
+ 'junit3',
+ ]
+
+TEST_DIRS += [
+ 'robocop/roboextender',
+ 'robocop',
+]
diff --git a/mobile/android/tests/browser/robocop/AndroidManifest.xml.in b/mobile/android/tests/browser/robocop/AndroidManifest.xml.in
new file mode 100644
index 0000000000..028799f72d
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/AndroidManifest.xml.in
@@ -0,0 +1,67 @@
+#filter substitution
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.mozilla.roboexample.test"
+#ifdef MOZ_ANDROID_SHARED_ID
+ android:sharedUserId="@MOZ_ANDROID_SHARED_ID@"
+#endif
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-sdk android:minSdkVersion="@MOZ_ANDROID_MIN_SDK_VERSION@"
+#ifdef MOZ_ANDROID_MAX_SDK_VERSION
+ android:maxSdkVersion="@MOZ_ANDROID_MAX_SDK_VERSION@"
+#endif
+ android:targetSdkVersion="23"/>
+
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+
+ <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
+
+ <instrumentation
+ android:name="org.mozilla.gecko.FennecInstrumentationTestRunner"
+ android:targetPackage="@ANDROID_PACKAGE_NAME@" />
+
+ <application
+ android:label="@string/app_name"
+ android:debuggable="true">
+
+ <uses-library android:name="android.test.runner" />
+
+ <!-- Fake handlers to ensure that we have some share intents to show in our share handler list -->
+ <activity android:name="org.mozilla.gecko.RobocopShare1"
+ android:label="Robocop fake activity">
+
+ <intent-filter android:label="Fake robocop share handler 1">
+ <action android:name="android.intent.action.SEND" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="text/*" />
+ <data android:mimeType="image/*" />
+ </intent-filter>
+
+ </activity>
+
+ <activity android:name="org.mozilla.gecko.RobocopShare2"
+ android:label="Robocop fake activity 2">
+
+ <intent-filter android:label="Fake robocop share handler 2">
+ <action android:name="android.intent.action.SEND" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="text/*" />
+ <data android:mimeType="image/*" />
+ </intent-filter>
+
+ </activity>
+
+ <activity android:name="org.mozilla.gecko.LaunchFennecWithConfigurationActivity"
+ android:label="Robocop Fennec">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ </application>
+
+</manifest>
diff --git a/mobile/android/tests/browser/robocop/Firefox.jpg b/mobile/android/tests/browser/robocop/Firefox.jpg
new file mode 100644
index 0000000000..6a00b485c6
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/Firefox.jpg
Binary files differ
diff --git a/mobile/android/tests/browser/robocop/Makefile.in b/mobile/android/tests/browser/robocop/Makefile.in
new file mode 100644
index 0000000000..f553080e78
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/Makefile.in
@@ -0,0 +1,67 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+TESTPATH := $(srcdir)/src/org/mozilla/gecko/tests
+
+ANDROID_EXTRA_JARS += \
+ $(srcdir)/libs/robotium-solo-5.5.4.jar \
+ $(NULL)
+
+_JAVA_HARNESS := \
+ Actions.java \
+ Assert.java \
+ Driver.java \
+ Element.java \
+ FennecInstrumentationTestRunner.java \
+ FennecNativeActions.java \
+ FennecMochitestAssert.java \
+ FennecTalosAssert.java \
+ FennecNativeDriver.java \
+ FennecNativeElement.java \
+ LaunchFennecWithConfigurationActivity.java \
+ RoboCopException.java \
+ RobocopShare1.java \
+ RobocopShare2.java \
+ RobocopUtils.java \
+ PaintedSurface.java \
+ StructuredLogger.java \
+ $(NULL)
+
+java-harness := $(addprefix $(srcdir)/src/org/mozilla/gecko/,$(_JAVA_HARNESS))
+java-tests := \
+ $(wildcard $(TESTPATH)/*.java) \
+ $(wildcard $(TESTPATH)/components/*.java) \
+ $(wildcard $(TESTPATH)/helpers/*.java)
+
+ANDROID_MANIFEST_FILE := $(CURDIR)/AndroidManifest.xml
+
+JAVAFILES += \
+ $(java-harness) \
+ $(java-tests) \
+ $(NULL)
+
+include $(topsrcdir)/config/rules.mk
+
+ifndef MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE
+tools:: $(ANDROID_APK_NAME).apk
+endif
+
+# The test APK needs to know the contents of the target APK while not
+# being linked against them. This is a best effort to avoid getting
+# out of sync with base's build config.
+jars_dir := $(DEPTH)/mobile/android/base
+stumbler_jars_dir := $(DEPTH)/mobile/android/stumbler
+ANDROID_CLASSPATH_JARS += \
+ $(wildcard $(jars_dir)/*.jar) \
+ $(wildcard $(stumbler_jars_dir)/*.jar) \
+ $(NULL)
+# We don't have transitive dependencies: these are the browser jar
+# dependencies inserted manually.
+ANDROID_CLASSPATH_JARS += \
+ $(ANDROID_SUPPORT_V4_AAR_LIB) \
+ $(ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB) \
+ $(ANDROID_DESIGN_AAR_LIB) \
+ $(ANDROID_RECYCLERVIEW_V7_AAR_LIB) \
+ $(ANDROID_APPCOMPAT_V7_AAR_LIB) \
+ $(NULL)
diff --git a/mobile/android/tests/browser/robocop/README b/mobile/android/tests/browser/robocop/README
new file mode 100644
index 0000000000..35e15865e9
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/README
@@ -0,0 +1,12 @@
+Robocop is a Mozilla project which uses Robotium to test Firefox on Android devices.
+
+Robotium is an open source tool licensed under the Apache 2.0 license and the original
+source can be found here:
+https://github.com/RobotiumTech/robotium
+
+We are including robotium-solo-5.5.4.jar as a binary and are not modifying it in any way
+from the original download found at:
+https://github.com/RobotiumTech/robotium/wiki/Downloads
+
+Firefox for Android developers should read the documentation in
+mobile/android/tests/browser/robocop/README.rst.
diff --git a/mobile/android/tests/browser/robocop/README.rst b/mobile/android/tests/browser/robocop/README.rst
new file mode 100644
index 0000000000..7ace7cdeb1
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/README.rst
@@ -0,0 +1,61 @@
+Robocop Mochitest
+=================
+
+*Robocop Mochitest* is a Mozilla project which uses Robotium to test
+ Firefox on Android devices.
+
+*Robocop Mochitest* tests run on Native Android builds marked with an
+'rc' on treeherder. These are Java based tests which run from the mochitest
+harness and generate similar log files. These are designed for
+testing the native UI of Android devices by sending events to the
+front end.
+
+See the documentation at
+https://wiki.mozilla.org/Auto-tools/Projects/Robocop/WritingTests for
+details.
+
+Development cycle
+-----------------
+
+To deploy the robocop APK to your device and start the robocop test
+suite, use::
+
+ mach robocop
+
+To run a specific test case, such as ``testLoad``::
+
+ mach robocop testLoad
+
+The Java files in ``mobile/android/tests/browser/robocop`` are dependencies of the
+robocop APK built by ``build/mobile/robocop``. If you modify Java files
+in ``mobile/android/tests/browser/robocop``, you need to rebuild the robocop APK
+with::
+
+ mach build build/mobile/robocop
+
+Changes to ``.html``, ``.css``, ``.sjs``, and ``.js`` files in
+``mobile/android/tests/browser/robocop`` do not require rebuilding the robocop
+APK -- these changes are always 'live', since they are served by the
+mochitest HTTP server and downloaded each test run by your device.
+
+``mach package`` does build and sign a robocop APK, but ``mach
+robocop`` does not use it. (This signed APK is used to test
+signed releases on the buildbots).
+
+As always, changes to ``mobile/android/base``, ``mobile/android/chrome``,
+``mobile/android/modules``, etc., require::
+
+ mach build mobile/android/base && mach package && mach install
+
+as usual.
+
+Licensing
+---------
+
+Robotium is an open source tool licensed under the Apache 2.0 license and the original
+source can be found here:
+https://github.com/RobotiumTech/robotium
+
+We are including robotium-solo-5.5.4.jar as a binary and are not modifying it in any way
+from the original download found at:
+https://github.com/RobotiumTech/robotium/wiki/Downloads
diff --git a/mobile/android/tests/browser/robocop/assets/README b/mobile/android/tests/browser/robocop/assets/README
new file mode 100644
index 0000000000..565ca2a9f4
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/assets/README
@@ -0,0 +1,4 @@
+You can place test assets in this file.
+They can be read as raw InputStreams with the getAsset() method in BaseTest.
+
+(This file is a placeholder to ensure that the assets/ directory exists, as it is referenced in the robocop Makefile.)
diff --git a/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v27.db b/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v27.db
new file mode 100644
index 0000000000..02103dde00
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v27.db
Binary files differ
diff --git a/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v28.db b/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v28.db
new file mode 100644
index 0000000000..d3f4c2826b
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v28.db
Binary files differ
diff --git a/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v29.db b/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v29.db
new file mode 100644
index 0000000000..e07281b1aa
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v29.db
Binary files differ
diff --git a/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v30.db b/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v30.db
new file mode 100644
index 0000000000..77524cf996
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v30.db
Binary files differ
diff --git a/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v31.db b/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v31.db
new file mode 100644
index 0000000000..597d78dfa4
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v31.db
Binary files differ
diff --git a/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v32.db b/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v32.db
new file mode 100644
index 0000000000..63263d6ff0
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v32.db
Binary files differ
diff --git a/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v33.db b/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v33.db
new file mode 100644
index 0000000000..c5241dae0c
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v33.db
Binary files differ
diff --git a/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v34.db b/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v34.db
new file mode 100644
index 0000000000..fa7e2b77b2
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v34.db
Binary files differ
diff --git a/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v35.db b/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v35.db
new file mode 100644
index 0000000000..fa1d9b3f90
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/assets/browser_db_upgrade/v35.db
Binary files differ
diff --git a/mobile/android/tests/browser/robocop/assets/ico_decoder_favicons/golem_favicon.ico b/mobile/android/tests/browser/robocop/assets/ico_decoder_favicons/golem_favicon.ico
new file mode 100644
index 0000000000..e5f6fd86f4
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/assets/ico_decoder_favicons/golem_favicon.ico
Binary files differ
diff --git a/mobile/android/tests/browser/robocop/assets/ico_decoder_favicons/microsoft_favicon.ico b/mobile/android/tests/browser/robocop/assets/ico_decoder_favicons/microsoft_favicon.ico
new file mode 100644
index 0000000000..bfe873eb22
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/assets/ico_decoder_favicons/microsoft_favicon.ico
Binary files differ
diff --git a/mobile/android/tests/browser/robocop/assets/ico_decoder_favicons/nvidia_favicon.ico b/mobile/android/tests/browser/robocop/assets/ico_decoder_favicons/nvidia_favicon.ico
new file mode 100644
index 0000000000..424df87200
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/assets/ico_decoder_favicons/nvidia_favicon.ico
Binary files differ
diff --git a/mobile/android/tests/browser/robocop/assets/mock-package.zip b/mobile/android/tests/browser/robocop/assets/mock-package.zip
new file mode 100644
index 0000000000..c599046cb5
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/assets/mock-package.zip
Binary files differ
diff --git a/mobile/android/tests/browser/robocop/assets/reading_list_bookmarks_migration/browser.db b/mobile/android/tests/browser/robocop/assets/reading_list_bookmarks_migration/browser.db
new file mode 100644
index 0000000000..684c7c6444
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/assets/reading_list_bookmarks_migration/browser.db
Binary files differ
diff --git a/mobile/android/tests/browser/robocop/assets/reading_list_bookmarks_migration/readercache/DWUP3U4ERC6TKJVSYXKJLHHEFY.json b/mobile/android/tests/browser/robocop/assets/reading_list_bookmarks_migration/readercache/DWUP3U4ERC6TKJVSYXKJLHHEFY.json
new file mode 100644
index 0000000000..83521462f8
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/assets/reading_list_bookmarks_migration/readercache/DWUP3U4ERC6TKJVSYXKJLHHEFY.json
@@ -0,0 +1 @@
+{"title":"US election 2016: Gloves come off for Clinton and Sanders - BBC News","byline":"Anthony Zurcher\n North America reporter","content":"<div id=\"readability-page-1\" class=\"page\"><div property=\"articleBody\" class=\"story-body__inner\"><figure class=\"media-landscape has-caption full-width lead\">\n <span class=\"image-and-copyright-container\">\n \n <img width=\"976\" height=\"549\" src=\"http://ichef.bbci.co.uk/news/320/cpsprodpb/E387/production/_89074285_89074283.jpg\" alt=\"This combination of file photos shows Democratic presidential hopefuls Bernie Sanders(L)on March 31, 2016 and Hillary Clinton on March 30, 2016,\" class=\"js-image-replace\"/>\n \n \n \n <span class=\"off-screen\">Image copyright</span>\n <span class=\"story-image-copyright\">Getty Images</span>\n \n </span>\n \n <figcaption class=\"media-caption\">\n <span class=\"off-screen\">Image caption</span>\n <span class=\"media-caption__text\">\n The Democratic candidates are engaging in more frequent attacks\n </span>\n </figcaption>\n \n </figure><p class=\"story-body__introduction\">It's crunch time in the Democratic race, and if the past week is any indication, nerves are starting to fray.</p><p>The fratricide within the Republican Party is getting much of the national attention, but the two remaining Democratic candidates - and their supporters - are starting to swing some sharp elbows.</p><p>The Wisconsin primary on Tuesday marks the beginning of the Democratic presidential campaign's endgame. More than half the pledged delegates have already been apportioned, and only a few truly landscape-altering battlegrounds - New York, Pennsylvania, New Jersey and California - remain on the calendar.</p><p>Mr Sanders can claim momentum, with wins in five of the last six contests, but he needs a sizeable victory in Wisconsin if he wants to properly set the stage for the critical coming contests and cut into Mrs Clinton's 263 delegate lead. </p><p>It's enough to set both candidates on edge.</p><p>On Thursday, when confronted by a Greenpeace activist in New York about whether she could address climate change while taking donations from the fossil fuel industry, Mrs Clinton <a class=\"story-body__link-external\" href=\"http://www.politico.com/blogs/2016-dem-primary-live-updates-and-results/2016/03/hillary-clinton-bernie-sanders-campaign-lies-221434\">showed a rare flash of anger</a>.</p><p>\"I am so sick of the Sanders campaign lying about me,\" she said. Her campaign would later assert that the former secretary of state, like Mr Sanders, takes donations from individuals employed in the energy sector but is prohibited from accepting money from corporations.</p><figure class=\"media-landscape has-caption full-width\">\n <span class=\"image-and-copyright-container\">\n \n \n \n \n \n <span class=\"off-screen\">Image copyright</span>\n <span class=\"story-image-copyright\">Reuters</span>\n \n </span>\n \n <figcaption class=\"media-caption\">\n <span class=\"off-screen\">Image caption</span>\n <span class=\"media-caption__text\">\n Mrs Clinton recently lost her cool with a Greenpeace activist\n </span>\n </figcaption>\n \n </figure><p>It was a moment of emotion for a usually carefully controlled candidate - and the Sanders camp quickly responded.</p><p>\"If the Clinton campaign wants to argue that industry lobbyists giving thousands of dollars to her campaign won't affect her decisions if she's elected, that's fine,\" Sanders adviser Jeff Weaver said. \"But to call us liars for pointing out basic facts about the secretary's fundraising is deeply cynical and very disappointing.\"</p><p>It was just one of numerous recent shots between the two campaigns. When Mrs Clinton unveiled manufacturing proposals on Friday, a Sanders spokesperson said the former secretary of state has embraced \"policies that have decimated the manufacturing industry ... and eliminated millions of jobs across the country\".</p><hr class=\"story-body__line\"/><figure class=\"media-with-caption\">\n \n <figcaption class=\"media-with-caption__caption\"><span class=\"off-screen\">Media caption</span>Bernie Sanders supporter: \"We need bold ideas\"</figcaption>\n</figure><p><a class=\"story-body__link\" href=\"http://www.bbc.com/news/world-us-canada-35912640\">Trump's disastrous women voter problem </a>- This voting bloc could doom in chances in the general election</p><p><a class=\"story-body__link\" href=\"http://www.bbc.co.uk/news/blogs-trending-35930999\">#BernieMadeMeWhite: Minority supporters of Sanders speak out</a> - Supporters push back against \"all-white\" narrative</p><p><a class=\"story-body__link\" href=\"http://www.bbc.com/news/election-us-2016-35933156\">Trump, Clinton and the 'None of the Above' era</a> - Rarely have those running for high office been held in such low esteem </p><p><a class=\"story-body__link\" href=\"http://www.bbc.co.uk/news/election/us2016\">Full US election coverage from the BBC</a></p><hr class=\"story-body__line\"/><p>At a campaign event, Mr Sanders offered criticisms of Mrs Clinton's six-figure speeches to Wall Street firms, her foreign policy views and her position on environmental issues, as his supporters heartily booed. </p><p>On Saturday Mrs Clinton noted that she has been a \"proud Democrat my adult life\" - drawing a contrast with Mr Sanders, who is a self identified \"democratic socialist\" who serves in Congress as an independent. </p><p>\"I think that is kind of important if we are selecting someone to be the Democratic nominee of the Democratic Party,\" <a class=\"story-body__link-external\" href=\"http://www.cnn.com/2016/04/02/politics/hillary-clinton-bernie-sanders-democrat-wisconsin/\">Mrs Clinton said</a>.</p><p>\"I think the secretary is getting very nervous,\" Mr Sanders countered on Sunday, noting that polls show him doing better than Mrs Clinton against Republican front-runner Donald Trump. </p><p>\"I think we've got a lot of young people's vote, working-class people's vote,\" Mr Sanders said. \"I think we're on the way to a victory if we can win the Democratic nomination.\"</p><figure class=\"media-landscape has-caption full-width\">\n <span class=\"image-and-copyright-container\">\n \n \n \n \n \n <span class=\"off-screen\">Image copyright</span>\n <span class=\"story-image-copyright\">Reuters</span>\n \n </span>\n \n <figcaption class=\"media-caption\">\n <span class=\"off-screen\">Image caption</span>\n <span class=\"media-caption__text\">\n Mr Sanders has declined to use Mrs Clinton's email flap as a campaign issue\n </span>\n </figcaption>\n \n </figure><p>This tension in the Democrat race is a relatively recent development. Last autumn, Mr Sanders famously said during the first debate that he was \"sick and tired\" of talking about Mrs Clinton's email server imbroglio - passing on a chance to target what could have been a key weakness in his main opponent.</p><p>When Mr Sanders entered the race, he said he wasn't interested in running a negative political campaign. It was a strategic decision that has lead to some recent second-guessing among advisers within the candidate's campaign, as witnessed <a class=\"story-body__link-external\" href=\"http://mobile.nytimes.com/2016/04/04/us/politics/bernie-sanders-hillary-clinton.html\">by a surprisingly pessimistic story in the New York Times</a> on Monday.</p><p>\"The central complication with Bernie is that he never wanted to cross into the zone of personal attacks because it would undercut his brand,\" Sanders adviser Tad Devine told the Times. </p><p>That hasn't stopped acrimony from flaring among the two candidates' supporters, however - even in typically restrained states like Wisconsin.</p><figure class=\"media-landscape has-caption full-width\">\n <span class=\"image-and-copyright-container\">\n \n \n \n \n \n <span class=\"off-screen\">Image copyright</span>\n <span class=\"story-image-copyright\">Reuters</span>\n \n </span>\n \n <figcaption class=\"media-caption\">\n <span class=\"off-screen\">Image caption</span>\n <span class=\"media-caption__text\">\n Sanders supporters have been criticised for being overzealous, particularly online\n </span>\n </figcaption>\n \n </figure><p>Lisa Stubek, a state representative who endorsed Mrs Clinton early in the campaign, says she's caught fire from pro-Sanders constituents in the ultra-liberal state capital of Madison.</p><p>\"Bernie Sanders supporters are pretty relentless, unfortunately, as far as their support and as far as attacking those who support anyone but their candidate,\" she said. \"And that's not true of everyone, but certainly there is a core group of Bernie Sanders supporters here in Madison who do that.\"</p><p>While Ms Stubek says that the campaign has largely been above-the-board, things have taken a turn for the worse.</p><p>\"I think certainly when you have Bernie Sanders out there stretching the truth or out-and-out lying, things do heat up,\" she said. \"I'm glad that Hillary is out there defending her record. More than anything people in our state want the facts.\"</p><p>Another Madison-area Democratic representative, Melissa Sargent, says she decided not to endorse a presidential candidate in part because the noise between the two candidates would drown out her efforts to address other issues and local campaigns.</p><p>\"When I go to doors and I'm stumping for my local candidates, I can talk about the things that I want to talk about and I can hear authentically what folks are saying,\" she said. \"It felt like I wouldn't be able to navigate the path that I am with these other races.\"</p><figure class=\"media-landscape has-caption full-width\">\n <span class=\"image-and-copyright-container\">\n \n \n \n \n \n <span class=\"off-screen\">Image copyright</span>\n <span class=\"story-image-copyright\">Reuters</span>\n \n </span>\n \n <figcaption class=\"media-caption\">\n <span class=\"off-screen\">Image caption</span>\n <span class=\"media-caption__text\">\n Mrs Clinton holds a large lead over Mr Sanders\n </span>\n </figcaption>\n \n </figure><p>Democrats in Wisconsin are quick to note that their challenges are nothing compared to what the Republicans are facing, as their two leading candidates engage in harsh negative attacks and exchange barbs over their wives and martial infidelity.</p><p>Brett Hulsey, a former Wisconsin Democratic state representative who is backing Mr Sanders, called the Republican campaign a \"food fight circus\".</p><p>\"Democrats are still fighting by the <a class=\"story-body__link-external\" href=\"http://www.britannica.com/sports/Marquess-of-Queensberry-rules\">Queensberry Rules</a>,\" he said, drawing a boxing analogy.</p><p>He cautions that while tempers seem to be flaring among Democrats at this point, things will eventually calm down. If Mrs Clinton wins, he plans to support her - and he predicts most Sanders backers will follow suit.</p><p>\"We're in the middle of the fray right now,\" he said. \"I remind everybody to take a few deep breaths and remain calm. I've worked every presidential since 1980 for Jimmy Carter. I've seen this movie before.\"</p><p>At a rally in Madison on Sunday night it seemed an era of good feelings may have returned. Mr Sanders spoke to a crowd of 4,200 for more than an hour and made no mention of his Democratic opponent. Instead, he focused his criticism on Mr Trump and Republican Governor (and former candidate) Scott Walker.</p><p>He also seemed to be broadening his perspective.</p><p>\"This campaign is about more than just electing a president of the United States, although I would very much appreciate your support,\" he said. \"It's about creating a political revolution.\"</p><figure class=\"media-landscape has-caption full-width\">\n <span class=\"image-and-copyright-container\">\n \n \n \n \n \n <span class=\"off-screen\">Image copyright</span>\n <span class=\"story-image-copyright\">Reuters</span>\n \n </span>\n \n <figcaption class=\"media-caption\">\n <span class=\"off-screen\">Image caption</span>\n <span class=\"media-caption__text\">\n Mr Sanders has criticised Mrs Clinton's ties to the fossil fuel industry\n </span>\n </figcaption>\n \n </figure><p>After the event, Jess A Weber - who had travelled to Madison from Illinois to volunteer for the Sanders campaign - said she appreciated that Mr Sanders has tried to run a positive campaign.</p><p>\"He continues to pull focus back to the message and back to our strength in unifying, coming together instead of dividing and bringing anyone down,\" she said.</p><p>On the campaign trail in Janesville on Monday, however, Mr Sanders was back to swiping at his opponent for her past positions on international trade and commerce.</p><p>\"I have voted against and led the opposition to every one of these disastrous trade agreements,\" Mr Sanders said. \"Secretary Clinton has supported virtually every one.\"</p><p>The Democratic race is far from a bare-knuckle fight, but the candidates aren't done trying to draw blood.</p></div></div>","length":10394,"excerpt":"It's crunch time in the Democratic race, and if the past week is any indication, nerves are starting to fray.","url":"http://www.bbc.com/news/election-us-2016-35962179"} \ No newline at end of file
diff --git a/mobile/android/tests/browser/robocop/assets/reading_list_bookmarks_migration/readercache/KWNV7PXD3JFOJBQJVFXI3CQKNE.json b/mobile/android/tests/browser/robocop/assets/reading_list_bookmarks_migration/readercache/KWNV7PXD3JFOJBQJVFXI3CQKNE.json
new file mode 100644
index 0000000000..9b907de004
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/assets/reading_list_bookmarks_migration/readercache/KWNV7PXD3JFOJBQJVFXI3CQKNE.json
@@ -0,0 +1 @@
+{"title":"Panama Papers: Iceland PM refuses to resign over investments - BBC News","byline":null,"content":"<div id=\"readability-page-1\" class=\"page\"><div property=\"articleBody\" class=\"story-body__inner\"><figure class=\"media-with-caption\">\n \n <figcaption class=\"media-with-caption__caption\"><span class=\"off-screen\">Media caption</span>The Icelandic PM walked out of an interview with the Swedish Public Broadcaster, SVT, after being questioned over offshore company Wintris</figcaption>\n</figure><p class=\"story-body__introduction\">Iceland's prime minister has refused to resign after being accused of hiding millions of dollars in investments behind a secretive offshore company.</p><p>Leaked documents show that Sigmundur Gunnlaugsson and his wife bought offshore company Wintris in 2007.</p><p>He did not declare an interest in the company when entering parliament in 2009. He sold his 50% of Wintris to his wife for $1 (70p), eight months later.</p><p>Opposition parties say they plan to hold a confidence vote in parliament.</p><p>Mr Gunnlaugsson says no rules were broken and his wife did not benefit financially. </p><p>The offshore company was used to invest millions of dollars of inherited money, according to a document signed by Mr Gunnlaugsson's wife, Anna Sigurlaug Palsdottir, in 2015.</p><hr class=\"story-body__line\"/><h2 class=\"story-body__crosshead\">Panama Papers - tax havens of the rich and powerful exposed</h2><ul class=\"story-body__unordered-list\">\n<li class=\"story-body__list-item\">Eleven million documents held by the Panama-based law firm Mossack Fonseca have been passed to German newspaper Sueddeutsche Zeitung, which then shared them with the <a class=\"story-body__link-external\" href=\"https://www.icij.org/\">International Consortium of Investigative Journalists</a>. BBC Panorama is among 107 media organisations - including UK newspaper <a class=\"story-body__link-external\" href=\"http://www.theguardian.com/news/series/panama-papers\">the Guardian</a> - in 76 countries which have been analysing the documents. The BBC doesn't know the identity of the source</li>\n<li class=\"story-body__list-item\">They show how the company has helped clients launder money, dodge sanctions and evade tax</li>\n<li class=\"story-body__list-item\">Mossack Fonseca says it has operated beyond reproach for 40 years and never been accused or charged with criminal wrong-doing</li>\n<li class=\"story-body__list-item\">Tricks of the trade: <a class=\"story-body__link\" href=\"http://www.bbc.co.uk/news/business-35943740\">How assets are hidden and taxes evaded</a>\n</li>\n<li class=\"story-body__list-item\">Panama Papers: <a class=\"story-body__link\" href=\"http://www.bbc.co.uk/news/world-35934836\">Full coverage</a>; follow reaction on Twitter using #PanamaPapers; in the BBC News app, follow the tag \"Panama Papers\"</li>\n<li class=\"story-body__list-item\">\n<a class=\"story-body__link\" href=\"http://www.bbc.co.uk/programmes/b006t14n/episodes/player\">Watch Panorama</a> on the BBC iPlayer (UK viewers only)</li>\n</ul><hr class=\"story-body__line\"/><p>The leaked documents, published on Sunday, show that Mr Gunnlaugsson was granted a general power of attorney over Wintris - which gave him the power to manage the company \"without any limitation\". Ms Palsdottir had a similar power of attorney.</p><p>Court records show that Wintris had significant investments in the bonds of three major Icelandic banks that collapsed during the financial crisis which began in 2008. Wintris is listed as a creditor with millions of dollars in claims in the banks' bankruptcies. </p><p>Mr Gunnlaugsson became prime minister in 2013 and has been involved in negotiations about the banks which could affect the value of the bonds held by Wintris.</p><p>He resisted pressure from foreign creditors - including many UK customers - to repay their deposits in full. Had foreign investors been repaid, it might have adversely affected both the Icelandic banks and the value of the bonds held by Wintris.</p><p>But Mr Gunnlaugsson kept his wife's interest in the outcome a secret.</p><h2 class=\"story-body__crosshead\">'Lost all trust'</h2><p>On Monday, Icelandic opposition parties called on Mr Gunnlaugsson to resign over the alleged conflict of interest and said they planned to table a confidence motion in parliament.</p><p>\"What would be the most natural and the right thing to do is that [he] resign as prime minister,\" Birgitta Jonsdottir, the head of the Pirate Party, told the Reuters news agency. \"There is a great and strong demand for that in society and he has totally lost all his trust and believability.\"</p><figure class=\"media-landscape no-caption full-width\">\n <span class=\"image-and-copyright-container\">\n \n \n \n \n \n <span class=\"off-screen\">Image copyright</span>\n <span class=\"story-image-copyright\">Iceland Monitor/Eva Björk</span>\n \n </span>\n \n </figure><p>Former Prime Minister Johanna Sigurdardottir, who oversaw Iceland's recovery from the financial crisis, meanwhile wrote on Facebook: \"The prime minister should immediately resign.\"</p><p>An online petition demanding Mr Gunnlaugsson's resignation also had some 24,000 signatures - more than 7% of the island nation's population.</p><p>But in an interview with Channel 2 television, the prime minister insisted he had put the interests of the Icelandic people ahead of the interests of the failed banks' claimants.</p><p>\"I have not considered quitting because of this matter nor am I going to quit because of this matter,\" he said.</p><p>\"The government has had good results. Progress has been strong and it is important that the government can finish their work.\"</p><p>A spokesman for the prime minister earlier said that Ms Palsdottir had always declared the assets to the tax authorities and that, under parliamentary rules, Mr Gunnlaugsson did not have to declare an interest in Wintris.</p><p>He said that joint share certificates in Wintris had been issued because the prime minister and his wife had a joint bank account. This was pointed out to them when the documents were reviewed in 2009. </p></div></div>","length":4680,"excerpt":"Iceland's prime minister refuses to resign after being accused of hiding millions of dollars in investments behind a secretive offshore company.","url":"http://www.bbc.com/news/world-europe-35962670"} \ No newline at end of file
diff --git a/mobile/android/tests/browser/robocop/assets/testcheck2-motionevents b/mobile/android/tests/browser/robocop/assets/testcheck2-motionevents
new file mode 100644
index 0000000000..a5961e466c
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/assets/testcheck2-motionevents
@@ -0,0 +1,444 @@
+04-24 15:00:54.643 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=398.44662, y[0]=528.4731, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25865746, downTime=25865746, deviceId=6, source=0x1002 }
+04-24 15:00:54.675 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=400.44385, y[0]=527.4739, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25865774, downTime=25865746, deviceId=6, source=0x1002 }
+04-24 15:00:54.683 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=400.44385, y[0]=527.4739, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25865782, downTime=25865746, deviceId=6, source=0x1002 }
+04-24 15:00:54.784 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=396.44937, y[0]=471.51758, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25865889, downTime=25865889, deviceId=6, source=0x1002 }
+04-24 15:00:54.831 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=396.44937, y[0]=471.51758, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25865936, downTime=25865889, deviceId=6, source=0x1002 }
+04-24 15:00:56.026 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=460.36063, y[0]=166.75565, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25867126, downTime=25867126, deviceId=6, source=0x1002 }
+04-24 15:00:56.073 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=460.36063, y[0]=166.75565, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25867173, downTime=25867126, deviceId=6, source=0x1002 }
+04-24 15:00:56.190 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=451.3731, y[0]=205.72522, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25867289, downTime=25867289, deviceId=6, source=0x1002 }
+04-24 15:00:56.245 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=451.3731, y[0]=205.72522, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25867346, downTime=25867289, deviceId=6, source=0x1002 }
+04-24 15:00:57.253 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=406.43552, y[0]=447.53632, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25868355, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.261 I/System.out( 5517): MotionEvent { action=ACTION_POINTER_DOWN(1), id[0]=0, x[0]=406.43552, y[0]=447.53632, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=325.54785, y[1]=612.4075, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=0, eventTime=25868364, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.284 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=407.43414, y[0]=446.5371, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=325.54785, y[1]=612.4075, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=0, eventTime=25868374, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.300 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=407.43414, y[0]=446.5371, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=323.55063, y[1]=614.40594, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=0, eventTime=25868393, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.323 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=409.43137, y[0]=439.54254, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=317.55896, y[1]=622.39966, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25868413, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.339 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=412.4272, y[0]=430.54956, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=305.5756, y[1]=636.38873, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25868432, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.362 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=415.42303, y[0]=419.55817, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=291.595, y[1]=652.3763, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25868451, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.378 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=419.41748, y[0]=407.5675, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=276.6158, y[1]=667.36456, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25868471, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.393 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=423.41193, y[0]=393.57843, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=265.63107, y[1]=682.35284, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25868490, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.417 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=432.39944, y[0]=373.59406, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=252.64911, y[1]=697.3411, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25868509, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.433 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=442.3856, y[0]=354.6089, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=241.66437, y[1]=712.3294, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25868529, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.448 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=455.36755, y[0]=331.62686, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=234.67407, y[1]=722.3216, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25868548, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.472 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=467.35092, y[0]=310.64325, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=227.68378, y[1]=732.31384, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25868567, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.487 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=482.3301, y[0]=289.65964, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=220.69348, y[1]=741.30676, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25868586, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.511 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=496.3107, y[0]=265.67838, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=213.7032, y[1]=750.29974, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25868606, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.526 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=509.29266, y[0]=243.69556, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=206.7129, y[1]=758.2935, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25868625, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.542 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=521.276, y[0]=221.71274, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=201.71983, y[1]=765.288, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25868644, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.565 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=533.2594, y[0]=204.72598, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=197.72539, y[1]=772.2826, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25868664, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.581 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=541.2483, y[0]=192.73535, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=193.73093, y[1]=778.2779, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25868684, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.604 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=545.24274, y[0]=186.74005, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=191.7337, y[1]=781.2756, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=0, eventTime=25868693, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.620 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=551.23444, y[0]=177.74707, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=187.73926, y[1]=787.2709, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25868713, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.643 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=555.2289, y[0]=171.75177, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=185.74203, y[1]=789.26935, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25868732, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.659 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=557.2261, y[0]=167.75488, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=183.7448, y[1]=792.26697, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25868752, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.683 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=558.2247, y[0]=165.75644, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=181.74757, y[1]=795.26465, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=0, eventTime=25868761, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.698 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=560.2219, y[0]=163.758, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=179.75035, y[1]=797.26306, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=0, eventTime=25868781, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.714 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=560.2219, y[0]=163.758, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=178.75174, y[1]=799.26154, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=0, eventTime=25868800, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.737 I/System.out( 5517): MotionEvent { action=ACTION_POINTER_UP(0), id[0]=0, x[0]=560.2219, y[0]=163.758, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=178.75174, y[1]=799.26154, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=0, eventTime=25868828, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:57.745 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=1, x[0]=178.75174, y[0]=799.26154, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25868835, downTime=25868355, deviceId=6, source=0x1002 }
+04-24 15:00:58.362 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=454.36893, y[0]=663.3677, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25869459, downTime=25869459, deviceId=6, source=0x1002 }
+04-24 15:00:58.378 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=452.3717, y[0]=650.3778, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25869469, downTime=25869459, deviceId=6, source=0x1002 }
+04-24 15:00:58.393 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=447.37866, y[0]=588.4262, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25869488, downTime=25869459, deviceId=6, source=0x1002 }
+04-24 15:00:58.417 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=445.38144, y[0]=498.49646, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25869507, downTime=25869459, deviceId=6, source=0x1002 }
+04-24 15:00:58.433 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=449.3759, y[0]=381.58783, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25869526, downTime=25869459, deviceId=6, source=0x1002 }
+04-24 15:00:58.456 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=466.3523, y[0]=241.69711, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25869545, downTime=25869459, deviceId=6, source=0x1002 }
+04-24 15:00:58.472 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=497.3093, y[0]=82.82123, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25869564, downTime=25869459, deviceId=6, source=0x1002 }
+04-24 15:00:58.472 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=497.3093, y[0]=82.82123, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25869573, downTime=25869459, deviceId=6, source=0x1002 }
+04-24 15:00:59.800 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=453.37033, y[0]=646.3809, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25870898, downTime=25870898, deviceId=6, source=0x1002 }
+04-24 15:00:59.815 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=451.3731, y[0]=614.40594, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25870907, downTime=25870898, deviceId=6, source=0x1002 }
+04-24 15:00:59.831 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=454.36893, y[0]=506.49023, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25870926, downTime=25870898, deviceId=6, source=0x1002 }
+04-24 15:00:59.854 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=473.3426, y[0]=344.6167, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25870946, downTime=25870898, deviceId=6, source=0x1002 }
+04-24 15:00:59.870 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=509.29266, y[0]=151.76736, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25870964, downTime=25870898, deviceId=6, source=0x1002 }
+04-24 15:00:59.870 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=509.29266, y[0]=151.76736, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25870973, downTime=25870898, deviceId=6, source=0x1002 }
+04-24 15:01:00.690 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=262.63522, y[0]=677.35675, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25871790, downTime=25871790, deviceId=6, source=0x1002 }
+04-24 15:01:00.706 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=272.62137, y[0]=671.36145, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25871799, downTime=25871790, deviceId=6, source=0x1002 }
+04-24 15:01:00.729 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=323.55063, y[0]=647.3802, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25871819, downTime=25871790, deviceId=6, source=0x1002 }
+04-24 15:01:00.745 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=391.45633, y[0]=626.39655, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25871838, downTime=25871790, deviceId=6, source=0x1002 }
+04-24 15:01:00.768 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=460.36063, y[0]=615.40515, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25871857, downTime=25871790, deviceId=6, source=0x1002 }
+04-24 15:01:00.784 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=530.26355, y[0]=605.41296, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25871876, downTime=25871790, deviceId=6, source=0x1002 }
+04-24 15:01:00.800 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=591.17896, y[0]=601.4161, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25871895, downTime=25871790, deviceId=6, source=0x1002 }
+04-24 15:01:00.823 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=650.0971, y[0]=598.4184, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25871914, downTime=25871790, deviceId=6, source=0x1002 }
+04-24 15:01:00.839 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=678.0583, y[0]=597.4192, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25871933, downTime=25871790, deviceId=6, source=0x1002 }
+04-24 15:01:00.862 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=688.0444, y[0]=595.4208, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25871943, downTime=25871790, deviceId=6, source=0x1002 }
+04-24 15:01:00.862 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=688.0444, y[0]=595.4208, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25871952, downTime=25871790, deviceId=6, source=0x1002 }
+04-24 15:01:01.198 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=551.23444, y[0]=146.77127, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25872298, downTime=25872298, deviceId=6, source=0x1002 }
+04-24 15:01:01.214 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=549.2372, y[0]=150.76816, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25872317, downTime=25872298, deviceId=6, source=0x1002 }
+04-24 15:01:01.237 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=546.24133, y[0]=162.75879, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25872327, downTime=25872298, deviceId=6, source=0x1002 }
+04-24 15:01:01.253 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=538.25244, y[0]=222.71194, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25872346, downTime=25872298, deviceId=6, source=0x1002 }
+04-24 15:01:01.276 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=536.2552, y[0]=333.6253, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25872365, downTime=25872298, deviceId=6, source=0x1002 }
+04-24 15:01:01.292 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=543.2455, y[0]=484.5074, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25872384, downTime=25872298, deviceId=6, source=0x1002 }
+04-24 15:01:01.308 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=557.2261, y[0]=640.3856, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25872403, downTime=25872298, deviceId=6, source=0x1002 }
+04-24 15:01:01.331 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=572.20526, y[0]=788.2701, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25872423, downTime=25872298, deviceId=6, source=0x1002 }
+04-24 15:01:01.347 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=584.18866, y[0]=851.22095, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25872432, downTime=25872298, deviceId=6, source=0x1002 }
+04-24 15:01:01.347 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=584.18866, y[0]=851.22095, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25872441, downTime=25872298, deviceId=6, source=0x1002 }
+04-24 15:01:02.151 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=556.2275, y[0]=204.72598, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25873238, downTime=25873238, deviceId=6, source=0x1002 }
+04-24 15:01:02.159 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=552.23303, y[0]=225.7096, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25873248, downTime=25873238, deviceId=6, source=0x1002 }
+04-24 15:01:02.175 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=544.24414, y[0]=316.63855, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25873267, downTime=25873238, deviceId=6, source=0x1002 }
+04-24 15:01:02.198 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=548.2386, y[0]=437.54413, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25873286, downTime=25873238, deviceId=6, source=0x1002 }
+04-24 15:01:02.214 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=555.2289, y[0]=558.44965, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25873305, downTime=25873238, deviceId=6, source=0x1002 }
+04-24 15:01:02.229 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=562.2192, y[0]=659.3708, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25873324, downTime=25873238, deviceId=6, source=0x1002 }
+04-24 15:01:02.253 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=570.20807, y[0]=738.30914, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25873345, downTime=25873238, deviceId=6, source=0x1002 }
+04-24 15:01:02.268 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=583.19, y[0]=798.26227, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25873363, downTime=25873238, deviceId=6, source=0x1002 }
+04-24 15:01:02.276 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=583.19, y[0]=798.26227, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25873372, downTime=25873238, deviceId=6, source=0x1002 }
+04-24 15:01:02.706 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=656.0888, y[0]=237.70023, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25873804, downTime=25873804, deviceId=6, source=0x1002 }
+04-24 15:01:02.722 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=654.09155, y[0]=236.70102, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25873823, downTime=25873804, deviceId=6, source=0x1002 }
+04-24 15:01:02.745 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=650.0971, y[0]=237.70023, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25873842, downTime=25873804, deviceId=6, source=0x1002 }
+04-24 15:01:02.761 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=640.11096, y[0]=242.69632, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25873852, downTime=25873804, deviceId=6, source=0x1002 }
+04-24 15:01:02.776 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=613.14844, y[0]=261.6815, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25873871, downTime=25873804, deviceId=6, source=0x1002 }
+04-24 15:01:02.800 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=578.19696, y[0]=293.65652, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25873890, downTime=25873804, deviceId=6, source=0x1002 }
+04-24 15:01:02.815 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=540.2497, y[0]=332.62607, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25873910, downTime=25873804, deviceId=6, source=0x1002 }
+04-24 15:01:02.839 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=494.31348, y[0]=381.58783, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25873929, downTime=25873804, deviceId=6, source=0x1002 }
+04-24 15:01:02.854 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=433.39807, y[0]=446.5371, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25873948, downTime=25873804, deviceId=6, source=0x1002 }
+04-24 15:01:02.870 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=367.4896, y[0]=522.4777, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25873968, downTime=25873804, deviceId=6, source=0x1002 }
+04-24 15:01:02.893 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=302.57974, y[0]=604.41376, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25873986, downTime=25873804, deviceId=6, source=0x1002 }
+04-24 15:01:02.933 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=244.6602, y[0]=679.35516, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874006, downTime=25873804, deviceId=6, source=0x1002 }
+04-24 15:01:02.933 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=200.72122, y[0]=735.31146, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874025, downTime=25873804, deviceId=6, source=0x1002 }
+04-24 15:01:02.948 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=165.76978, y[0]=783.274, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874044, downTime=25873804, deviceId=6, source=0x1002 }
+04-24 15:01:02.972 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=137.80861, y[0]=813.2506, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874063, downTime=25873804, deviceId=6, source=0x1002 }
+04-24 15:01:02.987 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=117.83634, y[0]=838.2311, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874082, downTime=25873804, deviceId=6, source=0x1002 }
+04-24 15:01:03.011 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=104.85437, y[0]=853.21936, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874101, downTime=25873804, deviceId=6, source=0x1002 }
+04-24 15:01:03.026 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=97.86408, y[0]=862.21234, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874121, downTime=25873804, deviceId=6, source=0x1002 }
+04-24 15:01:03.042 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=93.86963, y[0]=867.20844, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874140, downTime=25873804, deviceId=6, source=0x1002 }
+04-24 15:01:03.065 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=92.87102, y[0]=869.20685, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25874149, downTime=25873804, deviceId=6, source=0x1002 }
+04-24 15:01:03.081 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=91.8724, y[0]=871.2053, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25874169, downTime=25873804, deviceId=6, source=0x1002 }
+04-24 15:01:03.104 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=90.87379, y[0]=873.20374, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25874188, downTime=25873804, deviceId=6, source=0x1002 }
+04-24 15:01:03.112 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=90.87379, y[0]=873.20374, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25874215, downTime=25873804, deviceId=6, source=0x1002 }
+04-24 15:01:03.448 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=632.1221, y[0]=63.83606, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25874552, downTime=25874552, deviceId=6, source=0x1002 }
+04-24 15:01:03.472 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=629.1262, y[0]=64.83528, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25874572, downTime=25874552, deviceId=6, source=0x1002 }
+04-24 15:01:03.487 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=622.1359, y[0]=70.8306, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25874581, downTime=25874552, deviceId=6, source=0x1002 }
+04-24 15:01:03.511 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=601.16504, y[0]=103.80484, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874600, downTime=25874552, deviceId=6, source=0x1002 }
+04-24 15:01:03.526 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=569.2095, y[0]=163.758, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874619, downTime=25874552, deviceId=6, source=0x1002 }
+04-24 15:01:03.542 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=528.2663, y[0]=241.69711, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874639, downTime=25874552, deviceId=6, source=0x1002 }
+04-24 15:01:03.565 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=491.31763, y[0]=315.63934, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874658, downTime=25874552, deviceId=6, source=0x1002 }
+04-24 15:01:03.581 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=447.37866, y[0]=382.58704, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874677, downTime=25874552, deviceId=6, source=0x1002 }
+04-24 15:01:03.604 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=394.45215, y[0]=458.5277, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874696, downTime=25874552, deviceId=6, source=0x1002 }
+04-24 15:01:03.620 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=344.5215, y[0]=525.4754, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874716, downTime=25874552, deviceId=6, source=0x1002 }
+04-24 15:01:03.643 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=299.58392, y[0]=582.4309, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874735, downTime=25874552, deviceId=6, source=0x1002 }
+04-24 15:01:03.659 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=267.6283, y[0]=632.3919, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874754, downTime=25874552, deviceId=6, source=0x1002 }
+04-24 15:01:03.675 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=244.6602, y[0]=675.35834, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874773, downTime=25874552, deviceId=6, source=0x1002 }
+04-24 15:01:03.698 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=224.68794, y[0]=711.3302, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874792, downTime=25874552, deviceId=6, source=0x1002 }
+04-24 15:01:03.722 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=210.70735, y[0]=743.30524, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874811, downTime=25874552, deviceId=6, source=0x1002 }
+04-24 15:01:03.737 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=194.72955, y[0]=771.2834, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874831, downTime=25874552, deviceId=6, source=0x1002 }
+04-24 15:01:03.753 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=180.74896, y[0]=799.26154, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874850, downTime=25874552, deviceId=6, source=0x1002 }
+04-24 15:01:03.776 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=166.76839, y[0]=829.2381, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874869, downTime=25874552, deviceId=6, source=0x1002 }
+04-24 15:01:03.792 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=153.7864, y[0]=852.22015, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25874888, downTime=25874552, deviceId=6, source=0x1002 }
+04-24 15:01:03.808 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=150.79057, y[0]=860.21387, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25874897, downTime=25874552, deviceId=6, source=0x1002 }
+04-24 15:01:03.815 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=150.79057, y[0]=860.21387, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25874906, downTime=25874552, deviceId=6, source=0x1002 }
+04-24 15:01:04.183 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=615.1456, y[0]=187.73926, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25875281, downTime=25875281, deviceId=6, source=0x1002 }
+04-24 15:01:04.198 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=606.15814, y[0]=198.73068, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25875291, downTime=25875281, deviceId=6, source=0x1002 }
+04-24 15:01:04.214 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=570.20807, y[0]=249.69086, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25875310, downTime=25875281, deviceId=6, source=0x1002 }
+04-24 15:01:04.237 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=517.28156, y[0]=324.63232, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25875329, downTime=25875281, deviceId=6, source=0x1002 }
+04-24 15:01:04.253 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=455.36755, y[0]=415.56128, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25875348, downTime=25875281, deviceId=6, source=0x1002 }
+04-24 15:01:04.276 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=394.45215, y[0]=510.48712, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25875367, downTime=25875281, deviceId=6, source=0x1002 }
+04-24 15:01:04.292 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=325.54785, y[0]=599.41766, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25875387, downTime=25875281, deviceId=6, source=0x1002 }
+04-24 15:01:04.308 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=267.6283, y[0]=676.35754, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25875406, downTime=25875281, deviceId=6, source=0x1002 }
+04-24 15:01:04.331 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=217.69765, y[0]=745.30365, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25875425, downTime=25875281, deviceId=6, source=0x1002 }
+04-24 15:01:04.347 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=174.75728, y[0]=807.25525, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25875444, downTime=25875281, deviceId=6, source=0x1002 }
+04-24 15:01:04.370 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=148.79335, y[0]=860.21387, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25875463, downTime=25875281, deviceId=6, source=0x1002 }
+04-24 15:01:04.370 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=148.79335, y[0]=860.21387, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25875472, downTime=25875281, deviceId=6, source=0x1002 }
+04-24 15:01:04.940 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=219.69487, y[0]=771.2834, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25876039, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:04.956 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=221.6921, y[0]=767.2865, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25876058, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:04.979 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=224.68794, y[0]=762.2904, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25876068, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:04.995 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=240.66574, y[0]=740.30756, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25876087, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.011 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=268.62692, y[0]=702.3372, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25876106, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.034 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=309.57004, y[0]=653.3755, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25876125, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.050 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=351.5118, y[0]=605.41296, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25876144, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.073 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=395.45078, y[0]=559.44885, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25876164, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.089 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=432.39944, y[0]=522.4777, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25876183, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.112 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=463.35645, y[0]=495.49884, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25876202, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.128 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=488.32178, y[0]=474.5152, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25876221, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.143 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=504.2996, y[0]=459.52692, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25876241, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.167 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=513.2871, y[0]=453.53162, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25876260, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.183 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=518.28015, y[0]=449.53473, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25876279, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.198 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=520.2774, y[0]=448.53552, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25876289, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.222 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=522.27466, y[0]=446.5371, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25876308, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.237 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=524.27185, y[0]=445.53784, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25876327, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.315 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=526.2691, y[0]=445.53784, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25876413, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.331 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=529.2649, y[0]=447.53632, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25876432, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.347 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=533.2594, y[0]=450.53394, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25876442, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.370 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=547.2399, y[0]=461.5254, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25876461, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.386 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=570.20807, y[0]=479.5113, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25876480, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.409 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=599.16785, y[0]=499.49573, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25876499, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.425 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=642.1082, y[0]=520.4793, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25876518, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.440 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=674.06384, y[0]=534.4684, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25876537, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.464 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=694.0361, y[0]=544.4606, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25876556, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.479 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=706.0194, y[0]=551.45514, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25876575, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.495 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=710.01385, y[0]=554.45276, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25876585, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.495 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=710.01385, y[0]=554.45276, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25876594, downTime=25876039, deviceId=6, source=0x1002 }
+04-24 15:01:05.839 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=248.65465, y[0]=765.288, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25876941, downTime=25876941, deviceId=6, source=0x1002 }
+04-24 15:01:05.862 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=249.65326, y[0]=761.2912, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25876950, downTime=25876941, deviceId=6, source=0x1002 }
+04-24 15:01:05.878 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=264.63245, y[0]=730.31537, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25876969, downTime=25876941, deviceId=6, source=0x1002 }
+04-24 15:01:05.893 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=301.58115, y[0]=665.3661, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25876988, downTime=25876941, deviceId=6, source=0x1002 }
+04-24 15:01:05.917 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=352.5104, y[0]=582.4309, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25877008, downTime=25876941, deviceId=6, source=0x1002 }
+04-24 15:01:05.933 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=411.4286, y[0]=483.50818, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25877027, downTime=25876941, deviceId=6, source=0x1002 }
+04-24 15:01:05.948 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=481.33148, y[0]=363.60187, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25877046, downTime=25876941, deviceId=6, source=0x1002 }
+04-24 15:01:05.972 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=547.2399, y[0]=241.69711, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25877065, downTime=25876941, deviceId=6, source=0x1002 }
+04-24 15:01:05.987 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=604.1609, y[0]=149.76892, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25877084, downTime=25876941, deviceId=6, source=0x1002 }
+04-24 15:01:05.995 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=604.1609, y[0]=149.76892, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25877093, downTime=25876941, deviceId=6, source=0x1002 }
+04-24 15:01:06.745 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=592.17755, y[0]=115.79547, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25877844, downTime=25877844, deviceId=6, source=0x1002 }
+04-24 15:01:06.761 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=589.1817, y[0]=116.79468, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25877864, downTime=25877844, deviceId=6, source=0x1002 }
+04-24 15:01:06.776 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=585.18726, y[0]=120.791565, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25877874, downTime=25877844, deviceId=6, source=0x1002 }
+04-24 15:01:06.776 I/System.out( 5517): MotionEvent { action=ACTION_POINTER_DOWN(1), id[0]=0, x[0]=585.18726, y[0]=120.791565, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=350.51318, y[1]=860.21387, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=0, eventTime=25877874, downTime=25877844, deviceId=6, source=0x1002 }
+04-24 15:01:06.792 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=580.1942, y[0]=127.7861, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=354.50763, y[1]=850.2217, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=0, eventTime=25877883, downTime=25877844, deviceId=6, source=0x1002 }
+04-24 15:01:06.808 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=573.2039, y[0]=151.76736, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=373.4813, y[1]=810.2529, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25877903, downTime=25877844, deviceId=6, source=0x1002 }
+04-24 15:01:06.831 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=565.21497, y[0]=190.73694, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=396.44937, y[1]=771.2834, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25877921, downTime=25877844, deviceId=6, source=0x1002 }
+04-24 15:01:06.847 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=558.2247, y[0]=227.70804, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=414.4244, y[1]=740.30756, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25877940, downTime=25877844, deviceId=6, source=0x1002 }
+04-24 15:01:06.870 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=550.2358, y[0]=262.68073, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=427.4064, y[1]=714.3279, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25877959, downTime=25877844, deviceId=6, source=0x1002 }
+04-24 15:01:06.886 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=541.2483, y[0]=307.6456, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=437.39252, y[1]=691.3458, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25877978, downTime=25877844, deviceId=6, source=0x1002 }
+04-24 15:01:06.909 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=532.26074, y[0]=366.59955, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=450.37448, y[1]=664.3669, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25877997, downTime=25877844, deviceId=6, source=0x1002 }
+04-24 15:01:06.925 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=523.27325, y[0]=420.55737, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=460.36063, y[1]=644.3825, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25878017, downTime=25877844, deviceId=6, source=0x1002 }
+04-24 15:01:06.948 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=520.2774, y[0]=447.53632, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=465.3537, y[1]=635.3895, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25878036, downTime=25877844, deviceId=6, source=0x1002 }
+04-24 15:01:06.964 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=516.28296, y[0]=461.5254, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=468.34952, y[1]=630.39343, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25878055, downTime=25877844, deviceId=6, source=0x1002 }
+04-24 15:01:06.979 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=513.2871, y[0]=468.5199, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=468.34952, y[1]=628.395, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25878074, downTime=25877844, deviceId=6, source=0x1002 }
+04-24 15:01:07.003 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=511.2899, y[0]=470.51837, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=468.34952, y[1]=625.39734, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=0, eventTime=25878083, downTime=25877844, deviceId=6, source=0x1002 }
+04-24 15:01:07.003 I/System.out( 5517): MotionEvent { action=ACTION_POINTER_UP(0), id[0]=0, x[0]=511.2899, y[0]=470.51837, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=468.34952, y[1]=623.3989, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=0, eventTime=25878092, downTime=25877844, deviceId=6, source=0x1002 }
+04-24 15:01:07.011 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=1, x[0]=469.34814, y[0]=621.40045, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25878101, downTime=25877844, deviceId=6, source=0x1002 }
+04-24 15:01:07.011 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=1, x[0]=469.34814, y[0]=621.40045, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25878110, downTime=25877844, deviceId=6, source=0x1002 }
+04-24 15:01:07.667 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=175.7559, y[0]=484.5074, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25878767, downTime=25878767, deviceId=6, source=0x1002 }
+04-24 15:01:07.683 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=188.73787, y[0]=484.5074, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25878783, downTime=25878767, deviceId=6, source=0x1002 }
+04-24 15:01:07.706 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=224.68794, y[0]=485.50665, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25878802, downTime=25878767, deviceId=6, source=0x1002 }
+04-24 15:01:07.722 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=278.61304, y[0]=486.50586, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25878821, downTime=25878767, deviceId=6, source=0x1002 }
+04-24 15:01:07.745 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=336.5326, y[0]=487.50507, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25878840, downTime=25878767, deviceId=6, source=0x1002 }
+04-24 15:01:07.761 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=391.45633, y[0]=486.50586, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25878859, downTime=25878767, deviceId=6, source=0x1002 }
+04-24 15:01:07.776 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=439.38974, y[0]=484.5074, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25878879, downTime=25878767, deviceId=6, source=0x1002 }
+04-24 15:01:07.800 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=478.33566, y[0]=484.5074, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25878898, downTime=25878767, deviceId=6, source=0x1002 }
+04-24 15:01:07.815 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=495.31207, y[0]=485.50665, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25878907, downTime=25878767, deviceId=6, source=0x1002 }
+04-24 15:01:07.839 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=538.25244, y[0]=486.50586, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=2, eventTime=25878936, downTime=25878767, deviceId=6, source=0x1002 }
+04-24 15:01:07.854 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=559.2233, y[0]=489.50354, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25878955, downTime=25878767, deviceId=6, source=0x1002 }
+04-24 15:01:07.878 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=571.20667, y[0]=491.50195, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25878974, downTime=25878767, deviceId=6, source=0x1002 }
+04-24 15:01:07.893 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=580.1942, y[0]=494.49963, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25878993, downTime=25878767, deviceId=6, source=0x1002 }
+04-24 15:01:07.909 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=582.1914, y[0]=494.49963, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25879002, downTime=25878767, deviceId=6, source=0x1002 }
+04-24 15:01:07.917 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=582.1914, y[0]=494.49963, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25879013, downTime=25878767, deviceId=6, source=0x1002 }
+04-24 15:01:08.136 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=534.258, y[0]=201.72833, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25879233, downTime=25879233, deviceId=6, source=0x1002 }
+04-24 15:01:08.151 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=531.26215, y[0]=210.72131, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25879243, downTime=25879233, deviceId=6, source=0x1002 }
+04-24 15:01:08.167 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=520.2774, y[0]=254.68695, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25879262, downTime=25879233, deviceId=6, source=0x1002 }
+04-24 15:01:08.190 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=509.29266, y[0]=318.637, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25879281, downTime=25879233, deviceId=6, source=0x1002 }
+04-24 15:01:08.206 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=500.30515, y[0]=399.5738, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25879300, downTime=25879233, deviceId=6, source=0x1002 }
+04-24 15:01:08.229 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=493.31485, y[0]=485.50665, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25879320, downTime=25879233, deviceId=6, source=0x1002 }
+04-24 15:01:08.245 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=488.32178, y[0]=568.44183, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25879339, downTime=25879233, deviceId=6, source=0x1002 }
+04-24 15:01:08.268 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=484.32733, y[0]=650.3778, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25879358, downTime=25879233, deviceId=6, source=0x1002 }
+04-24 15:01:08.284 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=484.32733, y[0]=723.32086, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25879377, downTime=25879233, deviceId=6, source=0x1002 }
+04-24 15:01:08.300 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=488.32178, y[0]=756.2951, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25879387, downTime=25879233, deviceId=6, source=0x1002 }
+04-24 15:01:08.308 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=488.32178, y[0]=756.2951, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25879396, downTime=25879233, deviceId=6, source=0x1002 }
+04-24 15:01:09.104 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=147.79474, y[0]=671.36145, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25880205, downTime=25880205, deviceId=6, source=0x1002 }
+04-24 15:01:09.128 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=147.79474, y[0]=671.36145, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25880231, downTime=25880205, deviceId=6, source=0x1002 }
+04-24 15:01:09.245 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=163.77254, y[0]=667.36456, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25880346, downTime=25880346, deviceId=6, source=0x1002 }
+04-24 15:01:09.292 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=163.77254, y[0]=667.36456, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25880393, downTime=25880346, deviceId=6, source=0x1002 }
+04-24 15:01:09.722 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=453.37033, y[0]=804.2576, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25880826, downTime=25880826, deviceId=6, source=0x1002 }
+04-24 15:01:09.745 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=452.3717, y[0]=796.26385, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25880835, downTime=25880826, deviceId=6, source=0x1002 }
+04-24 15:01:09.761 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=447.37866, y[0]=749.30054, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25880855, downTime=25880826, deviceId=6, source=0x1002 }
+04-24 15:01:09.784 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=443.3842, y[0]=670.36224, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25880874, downTime=25880826, deviceId=6, source=0x1002 }
+04-24 15:01:09.800 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=443.3842, y[0]=577.4348, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25880893, downTime=25880826, deviceId=6, source=0x1002 }
+04-24 15:01:09.815 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=445.38144, y[0]=470.51837, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25880912, downTime=25880826, deviceId=6, source=0x1002 }
+04-24 15:01:09.839 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=453.37033, y[0]=357.60657, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25880931, downTime=25880826, deviceId=6, source=0x1002 }
+04-24 15:01:09.854 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=467.35092, y[0]=235.70178, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25880951, downTime=25880826, deviceId=6, source=0x1002 }
+04-24 15:01:09.878 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=490.319, y[0]=99.80797, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25880970, downTime=25880826, deviceId=6, source=0x1002 }
+04-24 15:01:09.893 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=500.30515, y[0]=46.849335, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25880979, downTime=25880826, deviceId=6, source=0x1002 }
+04-24 15:01:09.893 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=500.30515, y[0]=46.849335, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25880989, downTime=25880826, deviceId=6, source=0x1002 }
+04-24 15:01:11.026 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=562.2192, y[0]=813.2506, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25882131, downTime=25882131, deviceId=6, source=0x1002 }
+04-24 15:01:11.050 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=562.2192, y[0]=801.25995, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25882140, downTime=25882131, deviceId=6, source=0x1002 }
+04-24 15:01:11.065 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=568.2108, y[0]=728.31696, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25882159, downTime=25882131, deviceId=6, source=0x1002 }
+04-24 15:01:11.089 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=580.1942, y[0]=627.3958, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25882178, downTime=25882131, deviceId=6, source=0x1002 }
+04-24 15:01:11.104 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=593.17615, y[0]=516.4824, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25882198, downTime=25882131, deviceId=6, source=0x1002 }
+04-24 15:01:11.120 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=600.16644, y[0]=398.57452, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25882217, downTime=25882131, deviceId=6, source=0x1002 }
+04-24 15:01:11.143 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=608.15533, y[0]=263.67993, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25882236, downTime=25882131, deviceId=6, source=0x1002 }
+04-24 15:01:11.143 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=608.15533, y[0]=263.67993, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25882245, downTime=25882131, deviceId=6, source=0x1002 }
+04-24 15:01:11.956 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=544.24414, y[0]=599.41766, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25883053, downTime=25883053, deviceId=6, source=0x1002 }
+04-24 15:01:11.972 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=480.3329, y[0]=615.40515, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25883071, downTime=25883053, deviceId=6, source=0x1002 }
+04-24 15:01:11.987 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=392.45493, y[0]=650.3778, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25883090, downTime=25883053, deviceId=6, source=0x1002 }
+04-24 15:01:12.011 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=296.58807, y[0]=703.3364, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25883109, downTime=25883053, deviceId=6, source=0x1002 }
+04-24 15:01:12.026 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=227.68378, y[0]=767.2865, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25883128, downTime=25883053, deviceId=6, source=0x1002 }
+04-24 15:01:12.034 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=227.68378, y[0]=767.2865, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25883137, downTime=25883053, deviceId=6, source=0x1002 }
+04-24 15:01:12.597 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=519.2788, y[0]=114.796265, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25883695, downTime=25883695, deviceId=6, source=0x1002 }
+04-24 15:01:12.612 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=516.28296, y[0]=120.791565, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25883704, downTime=25883695, deviceId=6, source=0x1002 }
+04-24 15:01:12.636 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=505.29822, y[0]=150.76816, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25883724, downTime=25883695, deviceId=6, source=0x1002 }
+04-24 15:01:12.651 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=486.32455, y[0]=212.71976, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25883743, downTime=25883695, deviceId=6, source=0x1002 }
+04-24 15:01:12.667 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=471.34537, y[0]=308.6448, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25883762, downTime=25883695, deviceId=6, source=0x1002 }
+04-24 15:01:12.690 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=464.35507, y[0]=424.55426, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25883781, downTime=25883695, deviceId=6, source=0x1002 }
+04-24 15:01:12.706 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=467.35092, y[0]=567.4426, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25883800, downTime=25883695, deviceId=6, source=0x1002 }
+04-24 15:01:12.729 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=481.33148, y[0]=731.3146, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25883819, downTime=25883695, deviceId=6, source=0x1002 }
+04-24 15:01:12.729 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=481.33148, y[0]=731.3146, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25883828, downTime=25883695, deviceId=6, source=0x1002 }
+04-24 15:01:13.956 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=472.34396, y[0]=407.5675, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25885059, downTime=25885059, deviceId=6, source=0x1002 }
+04-24 15:01:13.972 I/System.out( 5517): MotionEvent { action=ACTION_POINTER_DOWN(1), id[0]=0, x[0]=472.34396, y[0]=407.5675, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=363.49515, y[1]=698.34033, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=0, eventTime=25885059, downTime=25885059, deviceId=6, source=0x1002 }
+04-24 15:01:13.979 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=472.34396, y[0]=405.5691, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=354.50763, y[1]=708.3326, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25885078, downTime=25885059, deviceId=6, source=0x1002 }
+04-24 15:01:13.995 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=472.34396, y[0]=402.5714, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=334.53537, y[1]=734.31226, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25885097, downTime=25885059, deviceId=6, source=0x1002 }
+04-24 15:01:14.018 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=472.34396, y[0]=400.573, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=322.55203, y[1]=749.30054, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=0, eventTime=25885107, downTime=25885059, deviceId=6, source=0x1002 }
+04-24 15:01:14.034 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=472.34396, y[0]=395.5769, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=301.58115, y[1]=779.2771, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25885126, downTime=25885059, deviceId=6, source=0x1002 }
+04-24 15:01:14.050 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=476.33844, y[0]=383.58624, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=285.60333, y[1]=804.2576, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25885146, downTime=25885059, deviceId=6, source=0x1002 }
+04-24 15:01:14.073 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=482.3301, y[0]=367.59875, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=272.62137, y[1]=823.2428, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25885165, downTime=25885059, deviceId=6, source=0x1002 }
+04-24 15:01:14.089 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=490.319, y[0]=351.61124, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=262.63522, y[1]=837.2319, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25885185, downTime=25885059, deviceId=6, source=0x1002 }
+04-24 15:01:14.112 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=500.30515, y[0]=334.6245, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=253.64772, y[1]=849.2225, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25885204, downTime=25885059, deviceId=6, source=0x1002 }
+04-24 15:01:14.128 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=510.29126, y[0]=317.6378, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=247.65604, y[1]=858.21545, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25885223, downTime=25885059, deviceId=6, source=0x1002 }
+04-24 15:01:14.143 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=519.2788, y[0]=304.64792, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=243.66159, y[1]=866.2092, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25885243, downTime=25885059, deviceId=6, source=0x1002 }
+04-24 15:01:14.167 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=526.2691, y[0]=294.65573, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=240.66574, y[1]=871.2053, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25885262, downTime=25885059, deviceId=6, source=0x1002 }
+04-24 15:01:14.183 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=531.26215, y[0]=286.662, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=237.6699, y[1]=876.2014, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25885281, downTime=25885059, deviceId=6, source=0x1002 }
+04-24 15:01:14.206 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=533.2594, y[0]=283.66434, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=235.67268, y[1]=878.1998, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25885300, downTime=25885059, deviceId=6, source=0x1002 }
+04-24 15:01:14.222 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=536.2552, y[0]=278.6682, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=233.67546, y[1]=880.19824, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25885320, downTime=25885059, deviceId=6, source=0x1002 }
+04-24 15:01:14.245 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=536.2552, y[0]=276.6698, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=232.67685, y[1]=882.1968, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=0, eventTime=25885339, downTime=25885059, deviceId=6, source=0x1002 }
+04-24 15:01:14.261 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=536.2552, y[0]=276.6698, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=232.67685, y[1]=884.1952, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=0, eventTime=25885358, downTime=25885059, deviceId=6, source=0x1002 }
+04-24 15:01:14.276 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=536.2552, y[0]=276.6698, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=232.67685, y[1]=886.1936, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=0, eventTime=25885377, downTime=25885059, deviceId=6, source=0x1002 }
+04-24 15:01:14.300 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=536.2552, y[0]=274.67136, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=232.67685, y[1]=888.192, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=0, eventTime=25885397, downTime=25885059, deviceId=6, source=0x1002 }
+04-24 15:01:14.308 I/System.out( 5517): MotionEvent { action=ACTION_POINTER_UP(0), id[0]=0, x[0]=536.2552, y[0]=274.67136, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=232.67685, y[1]=888.192, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=0, eventTime=25885406, downTime=25885059, deviceId=6, source=0x1002 }
+04-24 15:01:14.323 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=1, x[0]=232.67685, y[0]=890.1904, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25885413, downTime=25885059, deviceId=6, source=0x1002 }
+04-24 15:01:14.323 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=1, x[0]=232.67685, y[0]=890.1904, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25885421, downTime=25885059, deviceId=6, source=0x1002 }
+04-24 15:01:14.511 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=145.7975, y[0]=924.16394, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25885609, downTime=25885609, deviceId=6, source=0x1002 }
+04-24 15:01:14.518 I/System.out( 5517): MotionEvent { action=ACTION_POINTER_DOWN(1), id[0]=0, x[0]=145.7975, y[0]=924.16394, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=562.2192, y[1]=95.81108, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=0, eventTime=25885619, downTime=25885609, deviceId=6, source=0x1002 }
+04-24 15:01:14.534 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=145.7975, y[0]=924.16394, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=559.2233, y[1]=99.80797, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=0, eventTime=25885629, downTime=25885609, deviceId=6, source=0x1002 }
+04-24 15:01:14.558 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=145.7975, y[0]=924.16394, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=544.24414, y[1]=126.786896, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25885649, downTime=25885609, deviceId=6, source=0x1002 }
+04-24 15:01:14.573 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=155.78363, y[0]=916.17017, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=519.2788, y[1]=176.74786, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25885669, downTime=25885609, deviceId=6, source=0x1002 }
+04-24 15:01:14.597 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=178.75174, y[0]=894.1874, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=491.31763, y[1]=231.70493, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25885688, downTime=25885609, deviceId=6, source=0x1002 }
+04-24 15:01:14.612 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=217.69765, y[0]=860.21387, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=465.3537, y[1]=288.66043, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25885707, downTime=25885609, deviceId=6, source=0x1002 }
+04-24 15:01:14.628 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=255.64494, y[0]=825.2412, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=444.3828, y[1]=343.6175, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25885726, downTime=25885609, deviceId=6, source=0x1002 }
+04-24 15:01:14.651 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=295.58948, y[0]=789.26935, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=427.4064, y[1]=402.5714, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25885745, downTime=25885609, deviceId=6, source=0x1002 }
+04-24 15:01:14.667 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=333.53677, y[0]=755.29584, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=417.42026, y[1]=457.5285, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25885764, downTime=25885609, deviceId=6, source=0x1002 }
+04-24 15:01:14.706 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=364.49377, y[0]=733.31305, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=413.4258, y[1]=507.48944, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=1, eventTime=25885783, downTime=25885609, deviceId=6, source=0x1002 }
+04-24 15:01:14.706 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=373.4813, y[0]=726.3185, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=412.4272, y[1]=519.4801, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=0, eventTime=25885792, downTime=25885609, deviceId=6, source=0x1002 }
+04-24 15:01:14.722 I/System.out( 5517): MotionEvent { action=ACTION_POINTER_UP(1), id[0]=0, x[0]=385.46463, y[0]=696.3419, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=412.4272, y[1]=519.4801, toolType[1]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=2, historySize=0, eventTime=25885801, downTime=25885609, deviceId=6, source=0x1002 }
+04-24 15:01:14.745 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=387.46185, y[0]=695.3427, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25885810, downTime=25885609, deviceId=6, source=0x1002 }
+04-24 15:01:14.753 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=387.46185, y[0]=695.3427, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25885819, downTime=25885609, deviceId=6, source=0x1002 }
+04-24 15:01:15.081 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=304.577, y[0]=508.4887, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25886184, downTime=25886184, deviceId=6, source=0x1002 }
+04-24 15:01:15.104 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=308.57144, y[0]=509.4879, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25886194, downTime=25886184, deviceId=6, source=0x1002 }
+04-24 15:01:15.120 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=330.54092, y[0]=518.4809, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25886213, downTime=25886184, deviceId=6, source=0x1002 }
+04-24 15:01:15.143 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=367.4896, y[0]=532.47, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25886232, downTime=25886184, deviceId=6, source=0x1002 }
+04-24 15:01:15.159 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=418.41888, y[0]=548.45746, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25886251, downTime=25886184, deviceId=6, source=0x1002 }
+04-24 15:01:15.175 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=468.34952, y[0]=561.4473, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25886271, downTime=25886184, deviceId=6, source=0x1002 }
+04-24 15:01:15.198 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=516.28296, y[0]=575.4364, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25886290, downTime=25886184, deviceId=6, source=0x1002 }
+04-24 15:01:15.214 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=557.2261, y[0]=587.427, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25886309, downTime=25886184, deviceId=6, source=0x1002 }
+04-24 15:01:15.237 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=589.1817, y[0]=593.4223, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25886328, downTime=25886184, deviceId=6, source=0x1002 }
+04-24 15:01:15.237 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=589.1817, y[0]=593.4223, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25886337, downTime=25886184, deviceId=6, source=0x1002 }
+04-24 15:01:15.472 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=428.405, y[0]=394.5777, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25886568, downTime=25886568, deviceId=6, source=0x1002 }
+04-24 15:01:15.487 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=424.41055, y[0]=415.56128, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25886587, downTime=25886568, deviceId=6, source=0x1002 }
+04-24 15:01:15.503 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=420.4161, y[0]=434.54645, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25886597, downTime=25886568, deviceId=6, source=0x1002 }
+04-24 15:01:15.526 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=412.4272, y[0]=491.50195, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25886616, downTime=25886568, deviceId=6, source=0x1002 }
+04-24 15:01:15.542 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=408.43274, y[0]=555.45197, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25886635, downTime=25886568, deviceId=6, source=0x1002 }
+04-24 15:01:15.565 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=405.4369, y[0]=618.40283, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25886654, downTime=25886568, deviceId=6, source=0x1002 }
+04-24 15:01:15.581 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=403.43967, y[0]=678.35596, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25886674, downTime=25886568, deviceId=6, source=0x1002 }
+04-24 15:01:15.597 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=403.43967, y[0]=722.3216, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25886693, downTime=25886568, deviceId=6, source=0x1002 }
+04-24 15:01:15.620 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=407.43414, y[0]=765.288, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25886712, downTime=25886568, deviceId=6, source=0x1002 }
+04-24 15:01:15.636 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=415.42303, y[0]=805.25684, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25886731, downTime=25886568, deviceId=6, source=0x1002 }
+04-24 15:01:15.636 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=415.42303, y[0]=805.25684, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25886740, downTime=25886568, deviceId=6, source=0x1002 }
+04-24 15:01:16.042 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=635.1179, y[0]=388.58234, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25887144, downTime=25887144, deviceId=6, source=0x1002 }
+04-24 15:01:16.058 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=633.12067, y[0]=387.58313, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25887153, downTime=25887144, deviceId=6, source=0x1002 }
+04-24 15:01:16.081 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=629.1262, y[0]=386.58392, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25887173, downTime=25887144, deviceId=6, source=0x1002 }
+04-24 15:01:16.097 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=617.1429, y[0]=387.58313, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887192, downTime=25887144, deviceId=6, source=0x1002 }
+04-24 15:01:16.120 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=587.1845, y[0]=394.5777, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887211, downTime=25887144, deviceId=6, source=0x1002 }
+04-24 15:01:16.136 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=550.2358, y[0]=409.56598, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887230, downTime=25887144, deviceId=6, source=0x1002 }
+04-24 15:01:16.159 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=508.29404, y[0]=430.54956, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887249, downTime=25887144, deviceId=6, source=0x1002 }
+04-24 15:01:16.175 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=465.3537, y[0]=451.5332, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887269, downTime=25887144, deviceId=6, source=0x1002 }
+04-24 15:01:16.190 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=421.4147, y[0]=472.51678, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887288, downTime=25887144, deviceId=6, source=0x1002 }
+04-24 15:01:16.214 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=376.4771, y[0]=493.50037, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887307, downTime=25887144, deviceId=6, source=0x1002 }
+04-24 15:01:16.229 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=331.53955, y[0]=516.4824, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887326, downTime=25887144, deviceId=6, source=0x1002 }
+04-24 15:01:16.253 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=294.59085, y[0]=536.4668, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887345, downTime=25887144, deviceId=6, source=0x1002 }
+04-24 15:01:16.268 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=267.6283, y[0]=551.45514, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887364, downTime=25887144, deviceId=6, source=0x1002 }
+04-24 15:01:16.292 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=247.65604, y[0]=562.44653, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887384, downTime=25887144, deviceId=6, source=0x1002 }
+04-24 15:01:16.308 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=235.67268, y[0]=568.44183, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887403, downTime=25887144, deviceId=6, source=0x1002 }
+04-24 15:01:16.323 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=230.67961, y[0]=572.4387, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887423, downTime=25887144, deviceId=6, source=0x1002 }
+04-24 15:01:16.347 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=229.681, y[0]=574.43713, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25887432, downTime=25887144, deviceId=6, source=0x1002 }
+04-24 15:01:16.347 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=229.681, y[0]=574.43713, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25887440, downTime=25887144, deviceId=6, source=0x1002 }
+04-24 15:01:16.659 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=400.44385, y[0]=803.25836, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25887758, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:16.675 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=396.44937, y[0]=802.25916, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25887767, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:16.698 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=375.47852, y[0]=789.26935, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887787, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:16.714 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=347.51733, y[0]=767.2865, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887806, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:16.729 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=313.5645, y[0]=730.31537, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887825, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:16.753 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=281.6089, y[0]=686.34973, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887844, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:16.768 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=256.64355, y[0]=643.3833, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887863, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:16.792 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=237.6699, y[0]=607.4114, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887883, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:16.808 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=226.68517, y[0]=566.4434, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887902, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:16.823 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=227.68378, y[0]=518.4809, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887921, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:16.847 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=248.65465, y[0]=455.53003, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887941, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:16.862 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=286.60196, y[0]=392.57922, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887960, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:16.886 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=334.53537, y[0]=338.6214, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887979, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:16.901 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=381.47018, y[0]=302.6495, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25887998, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:16.917 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=425.40915, y[0]=279.66745, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888017, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:16.940 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=465.3537, y[0]=270.67447, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888036, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:16.956 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=500.30515, y[0]=270.67447, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888055, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:16.979 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=530.26355, y[0]=280.66666, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888075, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:16.995 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=553.2316, y[0]=300.65106, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888094, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:17.018 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=562.2192, y[0]=332.62607, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888113, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:17.034 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=557.2261, y[0]=377.59094, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888132, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:17.058 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=540.2497, y[0]=431.54877, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888151, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:17.073 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=513.2871, y[0]=490.50275, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888170, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:17.089 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=485.32596, y[0]=542.46216, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888190, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:17.112 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=454.36893, y[0]=587.427, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888209, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:17.128 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=424.41055, y[0]=617.40356, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888228, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:17.151 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=393.45355, y[0]=639.3864, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888247, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:17.167 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=368.48822, y[0]=644.3825, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888266, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:17.183 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=339.52844, y[0]=635.3895, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888286, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:17.206 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=308.57144, y[0]=607.4114, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888305, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:17.222 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=286.60196, y[0]=564.44495, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888324, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:17.245 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=278.61304, y[0]=510.48712, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888343, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:17.261 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=277.61444, y[0]=457.5285, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888362, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:17.284 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=279.61166, y[0]=433.54724, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25888372, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:17.300 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=286.60196, y[0]=389.5816, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888391, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:17.315 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=300.58252, y[0]=352.61047, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888410, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:17.339 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=319.55618, y[0]=327.62997, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888430, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:17.339 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=319.55618, y[0]=327.62997, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25888439, downTime=25887758, deviceId=6, source=0x1002 }
+04-24 15:01:17.597 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=351.5118, y[0]=866.2092, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25888698, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:17.620 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=341.52567, y[0]=859.21466, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25888708, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:17.636 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=312.5659, y[0]=831.2365, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888727, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:17.659 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=278.61304, y[0]=797.26306, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888747, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:17.675 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=245.65881, y[0]=761.2912, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888766, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:17.690 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=217.69765, y[0]=722.3216, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888785, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:17.714 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=194.72955, y[0]=675.35834, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888805, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:17.729 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=178.75174, y[0]=617.40356, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888823, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:17.753 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=171.76144, y[0]=550.4559, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888842, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:17.768 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=173.75867, y[0]=488.50427, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888862, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:17.784 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=188.73787, y[0]=422.5558, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888880, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:17.808 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=210.70735, y[0]=364.6011, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888900, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:17.823 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=245.65881, y[0]=314.64014, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888919, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:17.847 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=287.60056, y[0]=277.669, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888938, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:17.862 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=326.54648, y[0]=251.6893, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888957, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:17.878 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=368.48822, y[0]=240.6979, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888976, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:17.901 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=410.42996, y[0]=238.69946, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25888996, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:17.917 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=456.36618, y[0]=248.69165, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889015, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:17.933 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=497.3093, y[0]=275.67056, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889034, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:17.956 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=528.2663, y[0]=320.63544, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889053, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:17.972 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=546.24133, y[0]=374.59326, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889072, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:17.995 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=546.24133, y[0]=436.54486, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889092, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.011 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=530.26355, y[0]=504.49182, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889111, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.034 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=498.30792, y[0]=576.4356, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889130, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.050 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=454.36893, y[0]=633.3911, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889149, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.065 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=411.4286, y[0]=671.36145, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889168, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.089 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=364.49377, y[0]=695.3427, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889187, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.104 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=325.54785, y[0]=701.338, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889207, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.128 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=288.59918, y[0]=693.34424, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889227, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.143 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=256.64355, y[0]=669.363, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889245, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.167 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=236.6713, y[0]=622.39966, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889264, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.183 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=233.67546, y[0]=561.4473, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889283, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.198 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=237.6699, y[0]=526.4746, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25889293, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.222 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=256.64355, y[0]=454.53082, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889312, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.237 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=280.61026, y[0]=395.5769, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889332, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.261 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=306.57422, y[0]=354.6089, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889351, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.276 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=335.534, y[0]=323.6331, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889370, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.300 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=366.491, y[0]=306.64636, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889389, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.315 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=401.44244, y[0]=301.65027, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889408, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.339 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=439.38974, y[0]=312.6417, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889428, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.354 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=473.3426, y[0]=343.6175, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889447, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.370 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=493.31485, y[0]=382.58704, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889466, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.393 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=496.3107, y[0]=425.55347, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889485, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.409 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=480.3329, y[0]=478.5121, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889504, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.433 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=445.38144, y[0]=528.4731, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889524, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.448 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=398.44662, y[0]=566.4434, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889543, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.464 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=344.5215, y[0]=595.4208, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889562, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.487 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=295.58948, y[0]=611.40826, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889581, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.503 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=251.6505, y[0]=617.40356, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889600, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.526 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=213.7032, y[0]=614.40594, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889619, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.542 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=178.75174, y[0]=596.42, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889639, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.558 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=155.78363, y[0]=570.4403, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889658, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.581 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=143.80028, y[0]=533.4692, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889677, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.597 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=144.79889, y[0]=489.50354, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889696, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.620 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=155.78363, y[0]=438.54333, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889715, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.636 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=168.76561, y[0]=395.5769, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889735, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.651 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=184.74342, y[0]=358.60577, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25889754, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.675 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=192.73232, y[0]=336.62296, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25889763, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:18.675 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=192.73232, y[0]=336.62296, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25889772, downTime=25888698, deviceId=6, source=0x1002 }
+04-24 15:01:19.081 I/System.out( 5517): MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=485.32596, y[0]=768.2857, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25890185, downTime=25890185, deviceId=6, source=0x1002 }
+04-24 15:01:19.104 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=483.32874, y[0]=757.2943, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25890195, downTime=25890185, deviceId=6, source=0x1002 }
+04-24 15:01:19.120 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=477.33704, y[0]=718.32477, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25890214, downTime=25890185, deviceId=6, source=0x1002 }
+04-24 15:01:19.143 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=470.34674, y[0]=659.3708, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25890233, downTime=25890185, deviceId=6, source=0x1002 }
+04-24 15:01:19.159 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=466.3523, y[0]=600.4169, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25890252, downTime=25890185, deviceId=6, source=0x1002 }
+04-24 15:01:19.183 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=464.35507, y[0]=533.4692, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25890272, downTime=25890185, deviceId=6, source=0x1002 }
+04-24 15:01:19.198 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=464.35507, y[0]=461.5254, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25890291, downTime=25890185, deviceId=6, source=0x1002 }
+04-24 15:01:19.222 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=465.3537, y[0]=398.57452, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25890310, downTime=25890185, deviceId=6, source=0x1002 }
+04-24 15:01:19.237 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=466.3523, y[0]=343.6175, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=25890329, downTime=25890185, deviceId=6, source=0x1002 }
+04-24 15:01:19.253 I/System.out( 5517): MotionEvent { action=ACTION_MOVE, id[0]=0, x[0]=464.35507, y[0]=316.63855, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25890339, downTime=25890185, deviceId=6, source=0x1002 }
+04-24 15:01:19.253 I/System.out( 5517): MotionEvent { action=ACTION_UP, id[0]=0, x[0]=464.35507, y[0]=316.63855, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=25890347, downTime=25890185, deviceId=6, source=0x1002 }
diff --git a/mobile/android/tests/browser/robocop/green.swf b/mobile/android/tests/browser/robocop/green.swf
new file mode 100644
index 0000000000..e6f6aed148
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/green.swf
Binary files differ
diff --git a/mobile/android/tests/browser/robocop/javascript_redirect.sjs b/mobile/android/tests/browser/robocop/javascript_redirect.sjs
new file mode 100644
index 0000000000..06e3af09ac
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/javascript_redirect.sjs
@@ -0,0 +1,8 @@
+function handleRequest(request, response)
+{
+ let page = "<!DOCTYPE html><html><head><script>window.opener = null; location.replace('" + request.queryString + "')</script></head><body><p>Redirecting...</p></body></html>";
+
+ response.setStatusLine("1.0", 200, "OK");
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ response.write(page);
+}
diff --git a/mobile/android/tests/browser/robocop/libs/robotium-solo-5.5.4.jar b/mobile/android/tests/browser/robocop/libs/robotium-solo-5.5.4.jar
new file mode 100644
index 0000000000..9236755f46
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/libs/robotium-solo-5.5.4.jar
Binary files differ
diff --git a/mobile/android/tests/browser/robocop/link_discovery.html b/mobile/android/tests/browser/robocop/link_discovery.html
new file mode 100644
index 0000000000..1679e6545e
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/link_discovery.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML>
+<html>
+ <head id="linkparent">
+ <title>Autodiscovery Test</title>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/moz.build b/mobile/android/tests/browser/robocop/moz.build
new file mode 100644
index 0000000000..023ccf3365
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/moz.build
@@ -0,0 +1,34 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEFINES['ANDROID_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME']
+
+ANDROID_APK_NAME = 'robocop-debug'
+ANDROID_APK_PACKAGE = 'org.mozilla.roboexample.test'
+ANDROID_ASSETS_DIRS += ['assets']
+
+TEST_HARNESS_FILES.testing.mochitest += [
+ 'robocop.ini',
+ 'robocop_autophone.ini',
+]
+TEST_HARNESS_FILES.testing.mochitest.tests.robocop += [
+ '*.html',
+ '*.jpg',
+ '*.mp4',
+ '*.ogg',
+ '*.sjs',
+ '*.swf',
+ '*.webm',
+ '*.xml',
+ 'reader_mode_pages/**', # The ** preserves directory structure.
+ 'robocop*.js',
+ 'test*.js',
+]
+
+DEFINES['MOZ_ANDROID_SHARED_ID'] = CONFIG['MOZ_ANDROID_SHARED_ID']
+OBJDIR_PP_FILES.mobile.android.tests.browser.robocop += [
+ 'AndroidManifest.xml.in',
+]
diff --git a/mobile/android/tests/browser/robocop/reader_mode_pages/basic_article.html b/mobile/android/tests/browser/robocop/reader_mode_pages/basic_article.html
new file mode 100644
index 0000000000..f34cbece4e
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/reader_mode_pages/basic_article.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Article title</title>
+<meta name="description" content="This is the article description." />
+</head>
+<body>
+<header>Site header</header>
+<div>
+<h1>Article title</h1>
+<h2 class="author">by Jane Doe</h2>
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+</div>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/reader_mode_pages/developer.mozilla.org/en/XULRunner/Build_Instructions.html b/mobile/android/tests/browser/robocop/reader_mode_pages/developer.mozilla.org/en/XULRunner/Build_Instructions.html
new file mode 100644
index 0000000000..14e613008c
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/reader_mode_pages/developer.mozilla.org/en/XULRunner/Build_Instructions.html
@@ -0,0 +1,373 @@
+<!DOCTYPE html>
+<html lang="en-US" dir="ltr" id="developer-mozilla-org" xmlns:fb="http://www.facebook.com/2008/fbml" xmlns:og="http://ogp.me/ns#">
+<head>
+ <title>Building XULRunner | MDN</title>
+
+ <meta charset="utf-8">
+ <meta name="robots" content="index, follow">
+ <link rel="home" href="https://developer.mozilla.org/en-US/">
+ <link rel="copyright" href="Build_Instructions.html#copyright">
+ <link rel="shortcut icon" href="../../media/img/favicon.ico">
+
+ <!--[if !IE 6]><!-->
+ <link rel="stylesheet" media="screen,projection,tv" href="../../media/css/mdn-min.css%3Fbuild=f424781.css" />
+ <link rel="stylesheet" media="screen,projection,tv" href="../../media/css/wiki-min.css%3Fbuild=f424781.css" />
+ <!--<![endif]-->
+ <!--[if IE]><link rel="stylesheet" type="text/css" media="all" href="//developer.mozilla.org/media/css/mdn-ie.css"><![endif]-->
+ <!--[if lte IE 7]><link rel="stylesheet" type="text/css" media="all" href="//developer.mozilla.org/media/css/mdn-ie7.css"><![endif]-->
+ <!--[if lte IE 6]><link rel="stylesheet" type="text/css" media="all" href="//developer.mozilla.org/media/css/mdn-ie6.css"><![endif]-->
+ <link rel="stylesheet" type="text/css" media="print" href="../../media/css/mdn-print.css">
+ <link rel="stylesheet" href="../../../www.mozilla.org/tabzilla/media/css/tabzilla.css">
+
+ <link rel="stylesheet" media="print" href="../../media/css/wiki-print-min.css%3Fbuild=f424781.css" />
+ <link rel="stylesheet" type="text/css"
+ href="../../en-US/docs/Template:CustomCSS%3Fraw=1.css" />
+
+ <!--[if IE]>
+ <meta http-equiv="imagetoolbar" content="no">
+ <meta http-equiv="X-UA-Compatible" content="IE=Edge">
+ <script src="//developer.mozilla.org/media/js/html5.js"></script>
+ <![endif]-->
+
+ <link rel="alternate" type="application/json" href="https://developer.mozilla.org/en-US/docs/XULRunner/Build_Instructions$json" />
+ <link rel="canonical" href="Build_Instructions.html" />
+
+ <meta property="og:title" content="Building XULRunner"/>
+ <meta property="og:type" content="website"/>
+ <meta property="og:image" content="https://developer.mozilla.org/media/img/mdn-logo-sm.png"/>
+ <meta property="og:site_name" content="Mozilla Developer Network"/>
+
+ <meta property="og:description" content="XULRunner is built using basically the same process as Firefox or other applications. Please read and follow the general Build Documentation for instructions on how to get sources and set up build prerequisites."/>
+ <meta name="description" content="XULRunner is built using basically the same process as Firefox or other applications. Please read and follow the general Build Documentation for instructions on how to get sources and set up build prerequisites." />
+ </head>
+
+<body id="" class="html-ltr document" role="document">
+<!--[if lte IE 8]>
+<noscript><div class="global-notice">
+<p><strong>Warning:</strong> The Mozilla Developer Network website employs emerging web standards that may not be fully supported in some versions of MicroSoft Internet Explorer. You can improve your experience of this website by enabling JavaScript.</p>
+</div></noscript>
+<![endif]-->
+ <header id="masthead" class="minor">
+ <div class="wrap">
+ <ul id="nav-access">
+ <li><a href="Build_Instructions.html#language">Select language</a></li>
+ <li><a href="Build_Instructions.html#q">Skip to search</a></li>
+ <li><a href="Build_Instructions.html#content">Skip to main content</a></li>
+ </ul>
+
+ <div id="branding">
+ <div id="logo"><a href="https://developer.mozilla.org/en-US/"><img src="../../media/img/mdn-logo-sm.png" alt="Mozilla Developer Network" title="Mozilla Developer Network" width="62" height="71"> Mozilla Developer Network</a></div>
+ </div>
+
+
+ <nav id="nav">
+ <ul id="nav-main" role="menubar">
+ <li id="nav-main-topics" class="menu" role="menuitem"><a href="Build_Instructions.html#nav-sub-topics" class="toggle" aria-haspopup="true" aria-labelledby="nav-main-topics" title="Explore other parts of MDN">Topics</a>
+ <ul id="nav-sub-topics" class="sub-menu" aria-hidden="true">
+ <li id="nav-sub-web"><a href="https://developer.mozilla.org/en-US/web">Web</a></li>
+ <li id="nav-sub-apps"><a href="https://developer.mozilla.org/en-US/apps">Apps</a></li>
+ <li id="nav-sub-mobile"><a href="https://developer.mozilla.org/en-US/mobile">Mobile</a></li>
+ <li id="nav-sub-addons"><a href="https://developer.mozilla.org/en-US/addons">Add-ons</a></li>
+ <li id="nav-sub-mozilla"><a href="https://developer.mozilla.org/en-US/mozilla">Mozilla</a></li>
+ </ul>
+ </li>
+ <li id="nav-main-docs" class="menu" role="menuitem">
+ <a href="https://developer.mozilla.org/en-US/docs" class="docs toggle" aria-haspopup="true" aria-labelledby="nav-main-docs">Docs</a>
+ <div id="nav-sub-docs" class="sub-menu" aria-hidden="true">
+ <ul>
+ <li>
+ <ul>
+ <li><a href="https://developer.mozilla.org/en-US/docs/HTML">HTML</a></li>
+ <li><a href="https://developer.mozilla.org/en-US/docs/DOM">DOM</a></li>
+ <li><a href="https://developer.mozilla.org/en-US/docs/Using_HTML5_audio_and_video_in_Firefox">Video</a></li>
+ <li><a href="https://developer.mozilla.org/en-US/docs/Using_HTML5_audio_and_video_in_Firefox">Audio</a></li>
+ <li><a href="https://developer.mozilla.org/en-US/docs/SVG">SVG</a></li>
+ <li><a href="https://developer.mozilla.org/en-US/docs/WebGL">WebGL</a></li>
+ </ul>
+ </li>
+ <li>
+ <ul>
+ <li><a href="https://developer.mozilla.org/en-US/docs/HTML/HTML5">HTML5</a></li>
+ <li><a href="https://developer.mozilla.org/en-US/docs/WebSockets">WebSockets</a></li>
+ <li><a href="https://developer.mozilla.org/en-US/docs/HTML/Using_the_application_cache">Offline Cache</a></li>
+ <li><a href="https://developer.mozilla.org/en-US/docs/DOM/Storage">Local Storage</a></li>
+ <li><a href="https://developer.mozilla.org/en-US/docs/IndexedDB">IndexedDB</a></li>
+ <li><a href="https://developer.mozilla.org/en-US/docs/Using_files_from_web_applications">File API</a></li>
+ </ul>
+ </li>
+ <li>
+ <ul>
+ <li><a href="https://developer.mozilla.org/en-US/docs/CSS">CSS</a></li>
+ <li><a href="https://developer.mozilla.org/en-US/docs/CSS/Using_CSS_gradients">Gradients</a></li>
+ <li><a href="https://developer.mozilla.org/en-US/docs/CSS/Using_CSS_transforms">Transforms</a></li>
+ <li><a href="https://developer.mozilla.org/en-US/docs/CSS/Using_CSS_transitions">Transitions</a></li>
+ <li><a href="https://developer.mozilla.org/en-US/docs/CSS/Using_CSS_animations">Animations</a></li>
+ <li><a href="https://developer.mozilla.org/en-US/docs/CSS/Media_queries">Media Queries</a></li>
+ </ul>
+ </li>
+ <li>
+ <ul>
+ <li><a href="https://developer.mozilla.org/en-US/docs/JavaScript">JavaScript</a></li>
+ <li><a href="https://developer.mozilla.org/en-US/docs/AJAX">AJAX</a></li>
+ <li><a href="https://developer.mozilla.org/en-US/docs/HTML/Canvas">Canvas</a></li>
+ <li><a href="https://developer.mozilla.org/en-US/docs/Using_geolocation">Geolocation</a></li>
+ <li><a href="https://developer.mozilla.org/en-US/docs/DragDrop/Drag_and_Drop">Drag &amp; Drop</a></li>
+ <li><a href="https://developer.mozilla.org/en-US/docs/DOM/Using_web_workers">Web Workers</a></li>
+ </ul>
+ </li>
+ </ul>
+ <p><a href="https://developer.mozilla.org/en-US/docs">More docs&hellip;</a></p>
+ </div>
+ </li>
+ <li id="nav-main-demos" role="menuitem"><a href="https://developer.mozilla.org/en-US/demos/" class="demos">Demos</a></li>
+ <li id="nav-main-learning" role="menuitem"><a href="https://developer.mozilla.org/en-US/learn" class="learning">Learning</a></li>
+ <li id="nav-main-community" class="menu" role="menuitem"><a href="Build_Instructions.html#nav-sub-community" class="community toggle" aria-haspopup="true" aria-labelledby="nav-main-community">Community</a>
+ <ul id="nav-sub-community" class="sub-menu">
+ <li><a href="https://developer.mozilla.org/en-US/events">Events</a></li>
+ <li><a href="https://developer.mozilla.org/en-US/discussions">Discussions</a></li>
+ <li><a href="https://developer.mozilla.org/en-US/promote">Promote</a></li>
+ </ul>
+ </li>
+ </ul>
+ </nav>
+
+ <ul class="user-state signed-out">
+ <li class="user-signin menu">
+ <form class="browserid" action="https://developer.mozilla.org/en-US/users/browserid_verify" method="POST"><div style='display:none;'><input type='hidden' id='csrfmiddlewaretoken' name='csrfmiddlewaretoken' value='c92fde167c4768ad483a05412bede68c' /></div>
+ <input id="next" name="next" type="hidden" value="/en-US/docs/XULRunner/Build_Instructions"/>
+ <input required="required" type="hidden" name="assertion" id="id_assertion" />
+ <a href="Build_Instructions.html#" target="_blank" class="browserid-signin toggle" aria-haspopup="true" title="Sign in with Persona">Sign in</a>
+ <div class="browserid-info sub-menu" aria-hidden="true">
+ <h3>What's this?</h3> <p>MDN has switched to <a href="https://persona.org/" target="_blank" rel="external">Persona</a>, a safe and simple way to sign in with just your e-mail address. <a href="http://identity.mozilla.com/post/12950196039/deploying-browserid-at-mozilla" rel="external">Learn more about why Mozilla is using Persona</a>.</p> <p><strong>Returning members:</strong> sign in with Persona and you'll be connected to your MDN profile (all your information is still here).</p> <p><strong>New members:</strong> sign in with Persona first, then you'll be able to set up your new MDN profile.</p> <p><a href="Build_Instructions.html#" target="_blank" class="browserid-signin" title="Sign in with Persona">Sign in</a></p>
+ </div>
+ </form>
+ </li>
+ </ul>
+
+ <form id="site-search" method="get" action="http://www.google.com/search"
+ data-url="/en-US/search">
+ <p>
+ <input type="text" role="search" placeholder="Search MDN" id="q" name="q" value="">
+ <noscript><button type="submit">Search</button></noscript>
+ </p>
+ <input type="hidden" name="sitesearch" value="developer.mozilla.org">
+ <div id="site-search-gg"></div>
+ </form>
+
+ <a href="http://www.mozilla.org/" id="tabzilla">mozilla</a>
+ </div>
+ </header>
+
+
+
+<!-- top toolbar -->
+<section id="nav-toolbar"><div><div class="wrap">
+ <!-- right floated navigation -->
+ <nav id="tool-menus" role="navigation">
+ <ul id="tools">
+ <li class="menu">
+ <a href="Build_Instructions.html#page-tools" class="toggle">This page</a>
+ <ul id="page-tools" class="sub-menu">
+ <li class="page-print"> <a href="Build_Instructions.html#" onclick="return window.print();" title="Print page">Print page</a></li>
+ <li><a href="https://developer.mozilla.org/en-US/docs/new?parent=15078">New sub-page</a></li>
+ </ul>
+ </li>
+ <li class="menu">
+ <a href="Build_Instructions.html#" class="toggle">Languages</a>
+ <ul id="translations">
+ <li><a rel="internal" href="https://developer.mozilla.org/ja/docs/XULRunner/Build_Instructions" title="Building XULRunner">日本語</a></li>
+
+ <li><a href="https://developer.mozilla.org/en-US/docs/XULRunner/Build_Instructions$locales">Add translation</a></li>
+ </ul>
+ </li>
+ </ul>
+ </nav>
+
+ <!-- left crumb navigation -->
+ <nav class="crumbs" role="navigation">
+ <ol>
+ <li class="crumb"><a href="https://developer.mozilla.org/en-US/docs/en">MDN</a></li>
+ <li class="crumb"><a href="https://developer.mozilla.org/en-US/docs/XULRunner">XULRunner</a></li>
+ <li class="crumb">Building XULRunner</li>
+ </ol>
+ </nav>
+
+</div></div></section>
+
+
+
+<section id="content">
+ <div class="wrap">
+ <div id="content-main" class="full">
+ <article class="article" role="main"
+ data-current-revision="129041"
+ data-refresh-message="Your changes were merged. However, something else has been edited, so this page will be refreshed to reflect the changes."
+ data-cancel-edit-message="Abort editing in progress? Your unsaved changes will be discarded.">
+ <header id="article-head">
+ <div class="title">
+ <h1 class="page-title">Building XULRunner</h1>
+ </div>
+ <ul id="page-buttons">
+ <li class="page-history"><a href="https://developer.mozilla.org/en-US/docs/XULRunner/Build_Instructions$history">History</a></li>
+ <li class="page-edit"><a href="https://developer.mozilla.org/en-US/docs/XULRunner/Build_Instructions$edit">Edit</a></li>
+ </ul>
+
+
+ </header>
+
+
+
+
+ <div id="wikiArticle" class="page-content boxed">
+ <div id="article-nav">
+ <div class="page-toc">
+ <h2>Table of Contents</h2>
+ <ol>
+ <code></code><li><ol><li><a href="Build_Instructions.html#CVS_tags_and_XULRunner_versions" rel="internal">CVS tags and XULRunner versions</a><li><a href="Build_Instructions.html#Fetching_Sources_from_Mercurial" rel="internal">Fetching Sources from Mercurial</a></ol></li>
+ </ol>
+ </div>
+ <ul class="page-anchors">
+ <li class="anchor-tags">
+ <a href="Build_Instructions.html#page-tags">Tags</a>
+ </li>
+ <li class="anchor-files">
+ <span title="This document has no attachments">Files</span>
+ </li>
+ </ul>
+ </div>
+ <p> </p>
+<p><a href="https://developer.mozilla.org/en/XULRunner" title="en/XULRunner">XULRunner</a> is built using basically the same process as Firefox or other applications. Please read and follow the general <a href="https://developer.mozilla.org/En/Developer_Guide/Build_Instructions" title="En/Developer_Guide/Build_Instructions">Build Documentation</a> for instructions on how to get sources and set up build prerequisites.</p>
+<p>By default, XULRunner is built with <a href="https://developer.mozilla.org/en/JavaXPCOM" title="en/JavaXPCOM">JavaXPCOM</a> support; the build system must be able to find an appropriate JDK on the system; see the instructions on <a href="https://developer.mozilla.org/En/Developer_Guide/Build_Instructions/Building_JavaXPCOM" title="En/Developer_Guide/Build_Instructions/Building_JavaXPCOM">Building JavaXPCOM</a> for more details. If you do not want to build JavaXPCOM support, specify <code>--disable-javaxpcom</code> in your configuration.</p>
+<p>On Mac, XULRunner requires Mac OS X 10.3 or higher and XCode 1.5 or higher to build properly. The runtime requirement is Mac OS X 10.2.</p>
+<p>A basic minimal <a href="https://developer.mozilla.org/en/Configuring_Build_Options#Using_a_.mozconfig_Configuration_File" title="en/Configuring_Build_Options#Using_a_.mozconfig_Configuration_File">mozconfig</a> which will build a release configuration of XULRunner is:</p>
+<pre class="eval">mk_add_options MOZ_CO_PROJECT=xulrunner
+mk_add_options MOZ_OBJDIR=@topsrcdir@/obj-xulrunner
+
+ac_add_options --enable-application=xulrunner
+#Uncomment the following line if you don't want to build JavaXPCOM:
+#ac_add_options --disable-javaxpcom
+</pre>
+<h3 id="CVS_tags_and_XULRunner_versions">CVS tags and XULRunner versions</h3>
+<p>Older XULRunner releases where tagged in CVS with (for instance XULRUNNER_1_8_0_5_RELEASE ) up to version 1.8.0.5</p>
+<p>The CVS repository does not have specific tags for XULRunner anymore. Instead a XULRunner build is a just special build made from the Firefox/Mozilla tree, using the same tag as a Firefox build. There is a convention where a certain XULRunner version maps to a certain tag in the CVS.</p>
+<p>For instance XULRunner 1.8.1.3, the corresponding tag is CVS is : FIREFOX_2_0_0_3_RELEASE</p>
+<p>To find out how those Firefox tags and XULRunner version maps, check out the file mozilla/config/milestone.txt .</p>
+<p>You can also check the User Agent string in Firefox Help/About menu to get the mapping from a certain binary Firefox version to the corresponding XULRunner version. For instance, in Firefox 2.0.0.9 you will get :</p>
+<pre class="eval">Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9
+</pre>
+<p>Therefore the XULRunner version for this Firefox version is : 1.8.1.9</p>
+<h3 id="Fetching_Sources_from_Mercurial">Fetching Sources from Mercurial</h3>
+<p>As with all other Mozilla products, one would fetch recent sources from Mercurial. For example, to build XULRunner with the top of the tree:</p>
+<pre>hg clone http://hg.mozilla.org/mozilla-central/ src
+cd src
+echo ". \$topsrcdir/xulrunner/config/mozconfig" &gt; .mozconfig
+make -f client.mk build
+</pre>
+<p><span>Interwiki Language Links</span></p>
+<p></p>
+ </div>
+ <section class="page-meta">
+
+ <section id="page-tags">
+ <h2>Tags (4)</h2>
+ <div id="deki-page-tags">
+ <ul class="tags tagit ui-widget ui-widget-content">
+ <li class="tagit-choice ui-widget-content ui-state-default">
+ <a class="text tagit-label" href="https://developer.mozilla.org/en-US/docs/tag/Developing%20Mozilla">Developing Mozilla</a>
+ </li>
+ <li class="tagit-choice ui-widget-content ui-state-default">
+ <a class="text tagit-label" href="https://developer.mozilla.org/en-US/docs/tag/XUL">XUL</a>
+ </li>
+ <li class="tagit-choice ui-widget-content ui-state-default">
+ <a class="text tagit-label" href="https://developer.mozilla.org/en-US/docs/tag/XULRunner">XULRunner</a>
+ </li>
+ <li class="tagit-choice ui-widget-content ui-state-default">
+ <a class="text tagit-label" href="https://developer.mozilla.org/en-US/docs/tag/Build%20documentation">Build documentation</a>
+ </li>
+ </ul>
+ </div>
+ </section>
+
+
+ <section id="doc-contributors">
+ Contributors to this page: <a href="https://developer.mozilla.org/en-US/profiles/Kray2">Kray2</a>, <a href="https://developer.mozilla.org/en-US/profiles/Taken">Taken</a>, <a href="https://developer.mozilla.org/en-US/profiles/Kozawa">Kozawa</a>, <a href="https://developer.mozilla.org/en-US/profiles/Benjamin%20Smedberg">Benjamin Smedberg</a>, <a href="https://developer.mozilla.org/en-US/profiles/Nickolay">Nickolay</a>, <a href="https://developer.mozilla.org/en-US/profiles/NickolayBot">NickolayBot</a>, <a href="https://developer.mozilla.org/en-US/profiles/Pombredanne">Pombredanne</a>
+ <br />
+ Last updated by:
+ <a href="https://developer.mozilla.org/en-US/profiles/Taken">Taken</a>,
+ <time datetime="2009-10-08T15:16:43-07:00">Oct 8, 2009 3:16:43 PM</time>
+ </section>
+ </section>
+ </article>
+ <form id="wiki-page-edit" class="editing" method="post" action="https://developer.mozilla.org/en-US/docs/XULRunner/Build_Instructions$edit"><div style='display:none;'><input type='hidden' name='csrfmiddlewaretoken' value='c92fde167c4768ad483a05412bede68c' /></div>
+ <input type="hidden" name="form" id="form" value="rev" />
+ <input type="hidden" name="content" id="content" value="" />
+ </form>
+ </div>
+ </div>
+ </section>
+
+<section id="footbar">
+<div class="wrap">
+ <p>
+ What do you think of the new MDN? Please <a href="http://mdn.uservoice.com/forums/51389-mdn-website-feedback-http-developer-mozilla-org">share your feedback</a> with us. <a id="dev-mdc-link" href="https://lists.mozilla.org/listinfo/dev-mdc">Join our mailing list</a> to discuss ways to help create great documentation. </p>
+</div>
+</section>
+<footer id="site-info" class="footer" role="contentinfo">
+<div class="wrap">
+ <div id="legal">
+ <img src="../../media/img/mdn-logo-tiny.png" alt="" width="42" height="48">
+ <p id="copyright">&copy; 2005 - 2012 Mozilla Developer Network and individual contributors</p>
+ <p>
+ Content is available under <a href="https://developer.mozilla.org/en-US/docs/Project:Copyrights">these licenses</a> &bull; <a href="https://developer.mozilla.org/en-US/docs/Project:About">About MDN</a> &bull;
+ <a href="http://www.mozilla.org/en-US/privacy">Privacy Policy</a> &bull;
+ <a href="https://developer.mozilla.org/discussions">Help</a></p>
+ </div>
+ <ul class="user-state signed-out">
+ <li class="user-signin menu">
+ <form class="browserid" action="https://developer.mozilla.org/en-US/users/browserid_verify" method="POST"><div style='display:none;'><input type='hidden' name='csrfmiddlewaretoken' value='c92fde167c4768ad483a05412bede68c' /></div>
+ <input id="next" name="next" type="hidden" value="/en-US/docs/XULRunner/Build_Instructions"/>
+ <input required="required" type="hidden" name="assertion" id="id_assertion" />
+ <a href="Build_Instructions.html#" target="_blank" class="browserid-signin toggle" aria-haspopup="true" title="Sign in with Persona">Sign in</a>
+ <div class="browserid-info sub-menu" aria-hidden="true">
+ <h3>What's this?</h3> <p>MDN has switched to <a href="https://persona.org/" target="_blank" rel="external">Persona</a>, a safe and simple way to sign in with just your e-mail address. <a href="http://identity.mozilla.com/post/12950196039/deploying-browserid-at-mozilla" rel="external">Learn more about why Mozilla is using Persona</a>.</p> <p><strong>Returning members:</strong> sign in with Persona and you'll be connected to your MDN profile (all your information is still here).</p> <p><strong>New members:</strong> sign in with Persona first, then you'll be able to set up your new MDN profile.</p> <p><a href="Build_Instructions.html#" target="_blank" class="browserid-signin" title="Sign in with Persona">Sign in</a></p>
+ </div>
+ </form>
+ </li>
+ </ul>
+ <form class="languages go" method="get" action="https://developer.mozilla.org/en-US/docs">
+ <label for="language">Other languages:</label>
+ <select id="language" class="wiki-l10n" name="next" dir="ltr">
+ <option value="/en-US/docs/XULRunner/Build_Instructions" selected>
+ English (US)
+ </option>
+ <option value="/ja/docs/XULRunner/Build_Instructions">
+ 日本語
+ </option> </select>
+ <noscript><button type="submit">Go</button></noscript>
+ </form>
+ </div>
+</footer>
+
+<script src="../../en-US/jsi18n/build:f424781"></script>
+ <script src="../../../www.google.com/jsapi" type="text/javascript"></script>
+ <script src="../../../login.persona.org/include.js" type="text/javascript" async></script>
+ <script src="../../../www.mozilla.org/tabzilla/media/js/tabzilla.js" async></script>
+ <script src="../../media/js/mdn-min.js%3Fbuild=f424781"></script>
+ <script src="../../media/js/wiki-min.js%3Fbuild=f424781"></script>
+
+<script type="text/javascript">
+//<![CDATA[
+var _tag=new WebTrends();
+_tag.dcsGetId();
+//]]>>
+</script>
+<script type="text/javascript">
+//<![CDATA[
+_tag.dcsCollect();
+//]]>>
+</script>
+<noscript>
+<div><img alt="DCSIMG" id="DCSIMG" width="1" height="1" src="../../../statse.webtrendslive.com/dcs8yrjuavz5bdaun34r2o8bi_8o8x/njs.gif%3Fdcsuri=%252Fnojavascript&amp;WT.js=No&amp;WT.tv=8.6.2"/></div>
+</noscript>
+</body>
+</html> \ No newline at end of file
diff --git a/mobile/android/tests/browser/robocop/reader_mode_pages/not_an_article.html b/mobile/android/tests/browser/robocop/reader_mode_pages/not_an_article.html
new file mode 100644
index 0000000000..1facae498d
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/reader_mode_pages/not_an_article.html
@@ -0,0 +1,132 @@
+<!DOCTYPE html>
+<html lang="en-US" class="no-js">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
+ <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no" media="(device-height: 568px)">
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-status-bar-style" content="black" />
+
+ <!-- Don't index mobile optimized pages -->
+ <meta name="robots" content="noindex" />
+
+ <title>Firefox for Android | Mozilla Support</title>
+
+ <link rel="icon" type="image/png" sizes="512x512" href="//support.cdn.mozilla.net/static/img/firefox-512.png?v=1">
+ <link rel="icon" type="image/png" sizes="256x256" href="//support.cdn.mozilla.net/static/img/firefox-256.png?v=1">
+ <link rel="icon" type="image/png" sizes="128x128" href="//support.cdn.mozilla.net/static/img/firefox-128.png?v=1">
+ <link rel="icon" type="image/png" sizes="64x64" href="//support.cdn.mozilla.net/static/img/firefox-64.png?v=1">
+ <link rel="icon" type="image/png" sizes="32x32" href="//support.cdn.mozilla.net/static/img/firefox-32.png?v=1">
+ <link rel="icon" type="image/png" sizes="16x16" href="//support.cdn.mozilla.net/static/img/firefox-16.png?v=1">
+
+
+ <link rel="search" type="application/opensearchdescription+xml" title="Mozilla Support" href="/en-US/search/xml"/>
+
+ <link rel="stylesheet" media="screen,projection,tv" href="//support.cdn.mozilla.net/static/css/mobile/common-min.css?build=beb7c1e" />
+ <link rel="stylesheet" media="screen,projection,tv" href="//support.cdn.mozilla.net/static/css/mobile/products-min.css?build=beb7c1e" />
+
+ </head>
+<body class=""
+ data-readonly="false"
+ data-static-url="//support.cdn.mozilla.net/static/"
+ data-orientation="right"
+ data-ga-push="[]"
+ data-usernames-api="/en-US/users/api/usernames"
+>
+
+<nav class="scrollable">
+ <div id="search-bar">
+ <form id="search" action="/en-US/search">
+ <input type="hidden" name="product" value="mobile" />
+ <input name="q" placeholder="Search Mozilla Support" required="required" type="search" value="">
+ <button class="icon-sprite" type="submit">Search</button>
+ </form>
+
+ </div>
+
+ <a href="/en-US/products">Home</a>
+ <a href="/en-US/questions/new">Ask a question</a>
+ <a href="/en-US/questions">Support Forum</a>
+
+ <header>Navigation</header>
+ <a href="/en-US/get-involved">Help other users</a>
+ <a href="?&amp;mobile=0">Switch to desktop site</a>
+
+ <header>Profile</header>
+ <a href="/en-US/users/login">Sign in</a>
+
+ <header>Languages</header>
+ <a href="/en-US/locales" class="locale-picker">Switch language</a>
+ </nav>
+
+<header class="slide-on-exposed">
+ <div id="menu-button" class="icon-sprite"></div>
+ <h1>
+ Firefox for Android
+ </h1>
+ </header>
+
+
+<div class="wrapper slide-on-exposed">
+ <section id="content">
+ <ul id="topics">
+ <li>
+ <a href="/en-US/products/mobile/get-started" class="cf">
+ <img src="//support.cdn.mozilla.net/static/img/blank.png" class="topic-sprite topic-get-started" alt="">
+ Learn the Basics: get started
+ </a>
+ </li>
+ <li>
+ <a href="/en-US/products/mobile/download-and-install" class="cf">
+ <img src="//support.cdn.mozilla.net/static/img/blank.png" class="topic-sprite topic-download-and-install" alt="">
+ Download, install and migration
+ </a>
+ </li>
+ <li>
+ <a href="/en-US/products/mobile/privacy-and-security" class="cf">
+ <img src="//support.cdn.mozilla.net/static/img/blank.png" class="topic-sprite topic-privacy-and-security" alt="">
+ Privacy and security settings
+ </a>
+ </li>
+ <li>
+ <a href="/en-US/products/mobile/customize" class="cf">
+ <img src="//support.cdn.mozilla.net/static/img/blank.png" class="topic-sprite topic-customize" alt="">
+ Customize controls, options and add-ons
+ </a>
+ </li>
+ <li>
+ <a href="/en-US/products/mobile/sync" class="cf">
+ <img src="//support.cdn.mozilla.net/static/img/blank.png" class="topic-sprite topic-sync" alt="">
+ Firefox Sync settings
+ </a>
+ </li>
+ <li>
+ <a href="/en-US/products/mobile/fix-problems" class="cf">
+ <img src="//support.cdn.mozilla.net/static/img/blank.png" class="topic-sprite topic-fix-problems" alt="">
+ Fix slowness, crashing, error messages and other problems
+ </a>
+ </li>
+ <li>
+ <a href="/en-US/kb/get-community-support" class="cf">
+ <img src="//support.cdn.mozilla.net/static/img/blank.png" class="topic-sprite topic-get-community-support" alt="">
+ Get community support
+ </a>
+ </li>
+ </ul>
+
+ </section>
+
+ <footer>
+ </footer>
+
+ <ul id="notifications">
+ </ul>
+</div>
+
+
+<script src="//support.cdn.mozilla.net/static/jsi18n/en-us/javascript.js?beb7c1e"></script>
+
+<script src="//support.cdn.mozilla.net/static/js/mobile/common-min.js?build=beb7c1e"></script>
+
+</body>
+</html> \ No newline at end of file
diff --git a/mobile/android/tests/browser/robocop/res/values/strings.xml b/mobile/android/tests/browser/robocop/res/values/strings.xml
new file mode 100644
index 0000000000..c1727416b6
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/res/values/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<resources>
+ <string name="app_name">Roboexample</string>
+
+</resources>
diff --git a/mobile/android/tests/browser/robocop/robocop.ini b/mobile/android/tests/browser/robocop/robocop.ini
new file mode 100644
index 0000000000..e9f30478f6
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop.ini
@@ -0,0 +1,118 @@
+[DEFAULT]
+subsuite = robocop
+
+[src/org/mozilla/gecko/tests/testGeckoProfile.java]
+[src/org/mozilla/gecko/tests/testAboutPage.java]
+[src/org/mozilla/gecko/tests/testActivityStreamContextMenu.java]
+[src/org/mozilla/gecko/tests/testAddonManager.java]
+# disabled on 4.3, bug 1144918
+skip-if = android_version == "18"
+[src/org/mozilla/gecko/tests/testAddSearchEngine.java]
+# disabled on 4.3, bug 1120759
+skip-if = android_version == "18"
+[src/org/mozilla/gecko/tests/testAdobeFlash.java]
+# disabled on 4.3, bug 1146420
+skip-if = android_version == "18"
+[src/org/mozilla/gecko/tests/testANRReporter.java]
+[src/org/mozilla/gecko/tests/testAxisLocking.java]
+# [src/org/mozilla/gecko/tests/testBookmark.java] # see bug 915350
+[src/org/mozilla/gecko/tests/testBookmarksPanel.java]
+# disabled on 4.3, bug 987930
+skip-if = android_version == "18"
+[src/org/mozilla/gecko/tests/testBookmarkFolders.java]
+# disabled on 4.3, bug 1144921
+skip-if = android_version == "18"
+[src/org/mozilla/gecko/tests/testBookmarklets.java]
+# [src/org/mozilla/gecko/tests/testBookmarkKeyword.java] # see bug 915350
+[src/org/mozilla/gecko/tests/testBrowserProvider.java]
+[src/org/mozilla/gecko/tests/testBrowserSearchVisibility.java]
+[src/org/mozilla/gecko/tests/testDBUtils.java]
+[src/org/mozilla/gecko/tests/testDistribution.java]
+[src/org/mozilla/gecko/tests/testDoorHanger.java]
+# disabled on 4.3, bug 1144924
+skip-if = android_version == "18"
+[src/org/mozilla/gecko/tests/testFilterOpenTab.java]
+# [src/org/mozilla/gecko/tests/testFindInPage.java] # bug 1128287
+[src/org/mozilla/gecko/tests/testFlingCorrectness.java]
+[src/org/mozilla/gecko/tests/testFormHistory.java]
+[src/org/mozilla/gecko/tests/testGetUserMedia.java]
+# failures across the board, bug 1092202 & bug 1144926
+skip-if = true
+# [src/org/mozilla/gecko/tests/testHistory.java] # see bug 915350
+[src/org/mozilla/gecko/tests/testHomeBanner.java]
+[src/org/mozilla/gecko/tests/testInputUrlBar.java]
+[src/org/mozilla/gecko/tests/testJarReader.java]
+[src/org/mozilla/gecko/tests/testLinkContextMenu.java]
+# [src/org/mozilla/gecko/tests/testHomeListsProvider.java] # see bug 952310
+[src/org/mozilla/gecko/tests/testLoad.java]
+[src/org/mozilla/gecko/tests/testMailToContextMenu.java]
+[src/org/mozilla/gecko/tests/testNewTab.java]
+[src/org/mozilla/gecko/tests/testPanCorrectness.java]
+# [src/org/mozilla/gecko/tests/testPasswordEncrypt.java] # see bug 824067
+[src/org/mozilla/gecko/tests/testPasswordProvider.java]
+# [src/org/mozilla/gecko/tests/testPermissions.java] # see bug 757475
+[src/org/mozilla/gecko/tests/testPictureLinkContextMenu.java]
+[src/org/mozilla/gecko/tests/testPrefsObserver.java]
+[src/org/mozilla/gecko/tests/testPrivateBrowsing.java]
+[src/org/mozilla/gecko/tests/testPromptGridInput.java]
+# bug 1001657
+skip-if = android_version == "18"
+[src/org/mozilla/gecko/tests/testSearchHistoryProvider.java]
+[src/org/mozilla/gecko/tests/testSearchSuggestions.java]
+# disabled on 4.3, bug 1145867
+skip-if = android_version == "18"
+[src/org/mozilla/gecko/tests/testSessionOOMSave.java]
+# disabled on 4.3, bug 1144888
+skip-if = android_version == "18"
+[src/org/mozilla/gecko/tests/testSessionOOMRestore.java]
+# disabled on 4.3, bug 1145879
+skip-if = android_version == "18"
+# [src/org/mozilla/gecko/tests/testShareLink.java] # see bug 915897
+# [src/org/mozilla/gecko/tests/testThumbnails.java] # see bug 813107
+# [src/org/mozilla/gecko/tests/testVkbOverlap.java] # see bug 907274
+
+# Using JavascriptTest
+# (If your test can be written entirely in Javascript, consider writing
+# it as a chrome test instead. See mobile/android/tests/browser/chrome.)
+[src/org/mozilla/gecko/tests/testBrowserDiscovery.java]
+[src/org/mozilla/gecko/tests/testFilePicker.java]
+[src/org/mozilla/gecko/tests/testHistoryService.java]
+[src/org/mozilla/gecko/tests/testOSLocale.java]
+# disabled on 4.3: Bug 1124494
+skip-if = android_version == "18"
+[src/org/mozilla/gecko/tests/testReadingListCache.java]
+[src/org/mozilla/gecko/tests/testRestrictions.java]
+[src/org/mozilla/gecko/tests/testSnackbarAPI.java]
+[src/org/mozilla/gecko/tests/testTrackingProtection.java]
+[src/org/mozilla/gecko/tests/testUITelemetry.java]
+[src/org/mozilla/gecko/tests/testBug1217581.java]
+[src/org/mozilla/gecko/tests/testVideoControls.java]
+# disabled on 4.3, bug 1098532
+skip-if = android_version == "18"
+
+# Using UITest
+#[src/org/mozilla/gecko/tests/testAboutHomePageNavigation.java] # see bug 947550, bug 979038 and bug 977952
+[src/org/mozilla/gecko/tests/testAboutHomeVisibility.java]
+[src/org/mozilla/gecko/tests/testAppMenuPathways.java]
+[src/org/mozilla/gecko/tests/testBackButtonInEditMode.java]
+[src/org/mozilla/gecko/tests/testBrowserDatabaseHelperUpgrades.java]
+[src/org/mozilla/gecko/tests/testEventDispatcher.java]
+[src/org/mozilla/gecko/tests/testGeckoRequest.java]
+[src/org/mozilla/gecko/tests/testInputConnection.java]
+[src/org/mozilla/gecko/tests/testJavascriptBridge.java]
+[src/org/mozilla/gecko/tests/testReaderCacheMigration.java]
+[src/org/mozilla/gecko/tests/testReadingListToBookmarksMigration.java]
+[src/org/mozilla/gecko/tests/testNativeCrypto.java]
+[src/org/mozilla/gecko/tests/testReaderModeTitle.java]
+[src/org/mozilla/gecko/tests/testSessionHistory.java]
+[src/org/mozilla/gecko/tests/testStateWhileLoading.java]
+[src/org/mozilla/gecko/tests/testUnifiedTelemetryClientId.java]
+
+[src/org/mozilla/gecko/tests/testAccessibleCarets.java]
+
+# testStumblerSetting disabled on Android 4.3, bug 1145846
+[src/org/mozilla/gecko/tests/testStumblerSetting.java]
+skip-if = android_version == "18"
+
+[src/org/mozilla/gecko/tests/testLoginsProvider.java]
+[src/org/mozilla/gecko/tests/testICODecoder.java]
diff --git a/mobile/android/tests/browser/robocop/robocop_404.sjs b/mobile/android/tests/browser/robocop/robocop_404.sjs
new file mode 100644
index 0000000000..770639ec82
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_404.sjs
@@ -0,0 +1,28 @@
+/**
+ * Used with testThumbnails.
+ * On the first visit, the page is green.
+ * On subsequent visits, the page is red.
+ */
+
+function handleRequest(request, response) {
+ let type = request.queryString.match(/^type=(.*)$/)[1];
+ let state = "thumbnails." + type;
+ let color = "#0f0";
+ let status = 200;
+
+ if (getState(state)) {
+ color = "#f00";
+ if (type == "do404")
+ status = 404;
+ } else {
+ setState(state, "1");
+ }
+
+ response.setStatusLine(request.httpVersion, status, null);
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.write('<html>');
+ response.write('<head><title>' + type + '</title> <meta charset="utf-8"> </head>');
+ response.write('<body style="background-color: ' + color + '"></body>');
+ response.write('</html>');
+}
diff --git a/mobile/android/tests/browser/robocop/robocop_adobe_flash.html b/mobile/android/tests/browser/robocop/robocop_adobe_flash.html
new file mode 100644
index 0000000000..98689c5d1e
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_adobe_flash.html
@@ -0,0 +1,17 @@
+<html style="margin: 0; padding: 0">
+<head>
+ <title>Adobe Flash Test</title>
+ <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
+ <meta charset="utf-8">
+</head>
+<body style="margin: 0; padding: 0">
+ <object width="100" height="100"
+ classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"
+ codebase="http://fpdownload.macromedia.com/
+ pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0">
+ <param name="SRC" value="green.swf">
+ <embed src="green.swf" width="100" height="100">
+ </embed>
+ </object>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/robocop_autophone.ini b/mobile/android/tests/browser/robocop/robocop_autophone.ini
new file mode 100644
index 0000000000..b8b16e03f7
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_autophone.ini
@@ -0,0 +1 @@
+[testAdobeFlash]
diff --git a/mobile/android/tests/browser/robocop/robocop_big_link.html b/mobile/android/tests/browser/robocop/robocop_big_link.html
new file mode 100644
index 0000000000..f3811d8706
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_big_link.html
@@ -0,0 +1,13 @@
+<html>
+<head>
+ <title>Big Link</title>
+ <link rel="shortcut icon" href="data:image/gif;base64,R0lGODlhCwALAIAAAAAA3pn/ZiH5BAEAAAEALAAAAAALAAsAAAIUhA+hkcuO4lmNVindo7qyrIXiGBYAOw==" />
+ <meta name="viewport" content="initial-scale=1.0"/>
+ <meta charset="utf-8">
+</head>
+<body style="margin: 0; padding: 0">
+ <div style="text-align: center; margin: 0; padding: 0">
+ <a style="font-size: 60px" href="robocop_blank_01.html">Browser Blank Page</a>
+ </div>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/robocop_big_mailto.html b/mobile/android/tests/browser/robocop/robocop_big_mailto.html
new file mode 100644
index 0000000000..a4cc77e3bf
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_big_mailto.html
@@ -0,0 +1,13 @@
+<html>
+<head>
+ <title>Big Mailto</title>
+ <link rel="shortcut icon" href="data:image/gif;base64,R0lGODlhCwALAIAAAAAA3pn/ZiH5BAEAAAEALAAAAAALAAsAAAIUhA+hkcuO4lmNVindo7qyrIXiGBYAOw==" />
+ <meta name="viewport" content="initial-scale=1.0"/>
+ <meta charset="utf-8">
+</head>
+<body style="margin: 0; padding: 0">
+ <div style="text-align: center; margin: 0; padding: 0">
+ <a style="font-size: 60px" href="mailto:foo.bar@example.com">Email Foo.Bar</a>
+ </div>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/robocop_blank_01.html b/mobile/android/tests/browser/robocop/robocop_blank_01.html
new file mode 100644
index 0000000000..e4f6c98137
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_blank_01.html
@@ -0,0 +1,7 @@
+<html>
+<meta charset="utf-8">
+<title>Browser Blank Page 01</title>
+<body>
+<p>Browser Blank Page 01</p>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/robocop_blank_02.html b/mobile/android/tests/browser/robocop/robocop_blank_02.html
new file mode 100644
index 0000000000..7aaff168b7
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_blank_02.html
@@ -0,0 +1,8 @@
+<html>
+<meta charset="utf-8">
+<title>Browser Blank Page 02</title>
+<link rel="shortcut icon" href="data:image/gif;base64,R0lGODlhCwALAIAAAAAA3pn/ZiH5BAEAAAEALAAAAAALAAsAAAIUhA+hkcuO4lmNVindo7qyrIXiGBYAOw==" />
+<body>
+<p>Browser Blank Page 02</p>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/robocop_blank_03.html b/mobile/android/tests/browser/robocop/robocop_blank_03.html
new file mode 100644
index 0000000000..13be8c7437
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_blank_03.html
@@ -0,0 +1,7 @@
+<html>
+<meta charset="utf-8">
+<title>Browser Blank Page 03</title>
+<body>
+<p>Browser Blank Page 03</p>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/robocop_blank_04.html b/mobile/android/tests/browser/robocop/robocop_blank_04.html
new file mode 100644
index 0000000000..edac1804be
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_blank_04.html
@@ -0,0 +1,7 @@
+<html>
+<meta charset="utf-8">
+<title>Browser Blank Page 04</title>
+<body>
+<p>Browser Blank Page 04</p>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/robocop_blank_05.html b/mobile/android/tests/browser/robocop/robocop_blank_05.html
new file mode 100644
index 0000000000..a8cd44cdbd
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_blank_05.html
@@ -0,0 +1,7 @@
+<html>
+<meta charset="utf-8">
+<title>Browser Blank Page 05</title>
+<body>
+<p>Browser Blank Page 05</p>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/robocop_boxes.html b/mobile/android/tests/browser/robocop/robocop_boxes.html
new file mode 100644
index 0000000000..82934a0641
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_boxes.html
@@ -0,0 +1,42 @@
+<!--
+DO NOT MODIFY THIS FILE UNLESS YOU KNOW WHAT YOU ARE DOING!
+
+This file is specifically designed to create a page larger than
+any screen fennec could run on (to allow panning in both axes).
+It is filled with 100x100 pixel boxes that are of unique colour,
+so that we can identify exactly what part of the page we are
+rendering at any given time. The colours are specifically chosen
+so that adjacent boxes have a fairly large variation in colour,
+and so that errors due to 565/888 conversion are minimised. This
+is done by dropping the bottom few bits on each color channel,
+so that conversion from 888->565 is pretty much lossless, and any
+variation only comes in from however the drivers do 565->888.
+
+A lot of the tests depend on this behaviour, so ensure that all
+the tests pass (on a variety of screen sizes) when making any
+changes to this file.
+ -->
+<html style="margin: 0; padding: 0">
+<head>
+ <title>Browser Box test</title>
+ <meta name="viewport" content="initial-scale=1.0"/>
+ <meta charset="utf-8">
+</head>
+<body style="margin: 0; padding: 0">
+<script type="text/javascript">
+for (var y = 0; y < 2000; y += 100) {
+ document.write("<div style='width: 2000px; height: 100px; margin: 0; padding: 0; border: none'>\n");
+ for (var x = 0; x < 2000; x += 100) {
+ var r = (Math.floor(x / 3) % 256);
+ r = r & 0xF8;
+ var g = (x + y) % 256;
+ g = g & 0xFC;
+ var b = (Math.floor(y / 3) % 256);
+ b = b & 0xF8;
+ document.write("<div style='float: left; width: 100px; height: 100px; margin: 0; padding: 0; border: none; background-color: rgb(" + r + "," + g + "," + b + ")'> </div>\n");
+ }
+ document.write("</div>\n");
+}
+</script>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/robocop_dynamic.sjs b/mobile/android/tests/browser/robocop/robocop_dynamic.sjs
new file mode 100644
index 0000000000..58ff33e9d1
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_dynamic.sjs
@@ -0,0 +1,18 @@
+/**
+ * Dynamically generated page whose title matches the given id.
+ */
+
+function handleRequest(request, response) {
+ let id = request.queryString.match(/^id=(.*)$/)[1];
+ let key = "dynamic." + id;
+
+ response.setStatusLine(request.httpVersion, 200, null);
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.write('<html>');
+ response.write('<head><title>' + id + '</title><meta charset="utf-8"></head>');
+ response.write('<body>');
+ response.write('<h1>' + id + '</h1>');
+ response.write('</body>');
+ response.write('</html>');
+}
diff --git a/mobile/android/tests/browser/robocop/robocop_geolocation.html b/mobile/android/tests/browser/robocop/robocop_geolocation.html
new file mode 100644
index 0000000000..1e3cb0afb7
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_geolocation.html
@@ -0,0 +1,20 @@
+<html>
+<head>
+ <title>Geolocation Test Page</title>
+ <meta charset="utf-8">
+</head>
+<body>
+<script>
+ function clb(position) {
+ // Show a green background if permission is granted
+ document.body.style.background = "#008000";
+ }
+ function err(error) {
+ // Show a red background if permission is denied
+ if (error.code == error.PERMISSION_DENIED)
+ document.body.style.background = "#FF0000";
+ }
+ navigator.geolocation.getCurrentPosition(clb, err, {timeout: 0});
+</script>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/robocop_getusermedia.html b/mobile/android/tests/browser/robocop/robocop_getusermedia.html
new file mode 100644
index 0000000000..1ec86d61bf
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_getusermedia.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<html><head>
+ <title>gUM Test Page</title>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8" charset="utf-8">
+</head>
+<body>
+ <div id="content"></div>
+ <script type="application/javascript">
+ var video_status = false;
+ var video = document.createElement("video");
+ video.setAttribute("width", 640);
+ video.setAttribute("height", 480);
+
+ var audio_status = false;
+ var audio = document.createElement("audio");
+ audio.setAttribute("controls", true);
+
+ var content = document.getElementById("content");
+ document.title = "gUM Test Page";
+
+ startAudioVideo();
+
+ function startAudioVideo() {
+ video_status = true;
+ audio_status = true;
+ mediaConstraints = {
+ video: {
+ mozMediaSource: "browser",
+ mediaSource: "browser"
+ },
+ audio: true
+ };
+ startMedia(mediaConstraints);
+ }
+
+ function stopMedia() {
+ if (video_status) {
+ video.srcObject.stop();
+ video.srcObject = null;
+ content.removeChild(video);
+ capturing = false;
+ video_status = false;
+ }
+ if (audio_status) {
+ audio.srcObject.stop();
+ audio.srcObject = null;
+ content.removeChild(audio);
+ audio_status = false;
+ }
+ }
+
+ function startMedia(param) {
+ try {
+ window.navigator.mozGetUserMedia(param, function(stream) {
+ if (video_status) {
+ content.appendChild(video);
+ video.srcObject = stream;
+ video.play();
+ }
+ if (audio_status) {
+ content.appendChild(audio);
+ audio.srcObject = stream;
+ audio.play();
+ }
+ var audioTracks = stream.getAudioTracks();
+ var videoTracks = stream.getVideoTracks();
+ document.title = "";
+ if (audioTracks.length > 0) {
+ document.title += "audio";
+ }
+ if (videoTracks.length > 0) {
+ document.title += "video";
+ }
+ document.title += " gumtest";
+ audio.srcObject.stop();
+ video.srcObject.stop();
+ }, function(err) {
+ document.title = "failed gumtest";
+ stopMedia();
+ });
+ } catch(e) {
+ stopMedia();
+ }
+ }
+</script>
+</body></html>
diff --git a/mobile/android/tests/browser/robocop/robocop_getusermedia2.html b/mobile/android/tests/browser/robocop/robocop_getusermedia2.html
new file mode 100644
index 0000000000..a3ffa29666
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_getusermedia2.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<html><head>
+ <title>gUM Test Page</title>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8" charset="utf-8">
+</head>
+<body>
+ <div id="content"></div>
+ <script type="application/javascript">
+ var video_status = false;
+ var video = document.createElement("video");
+ video.setAttribute("width", 640);
+ video.setAttribute("height", 480);
+
+ var audio_status = false;
+ var audio = document.createElement("audio");
+ audio.setAttribute("controls", true);
+
+ var content = document.getElementById("content");
+ document.title = "gUM Test Page";
+
+ startAudioVideo();
+
+ function startAudioVideo() {
+ video_status = true;
+ audio_status = true;
+ mediaConstraints = {
+ video: true,
+ audio: true
+ };
+ startMedia(mediaConstraints);
+ }
+
+ function stopMedia() {
+ if (video_status) {
+ video.mozSrcObject.stop();
+ video.mozSrcObject = null;
+ content.removeChild(video);
+ capturing = false;
+ video_status = false;
+ }
+ if (audio_status) {
+ audio.mozSrcObject.stop();
+ audio.mozSrcObject = null;
+ content.removeChild(audio);
+ audio_status = false;
+ }
+ }
+
+ function startMedia(param) {
+ try {
+ window.navigator.mozGetUserMedia(param, function(stream) {
+ if (video_status) {
+ content.appendChild(video);
+ video.mozSrcObject = stream;
+ video.play();
+ }
+ if (audio_status) {
+ content.appendChild(audio);
+ audio.mozSrcObject = stream;
+ audio.play();
+ }
+ var audioTracks = stream.getAudioTracks();
+ var videoTracks = stream.getVideoTracks();
+ document.title = "";
+ if (audioTracks.length > 0) {
+ document.title += "audio";
+ }
+ if (videoTracks.length > 0) {
+ document.title += "video";
+ }
+ document.title += " gumtest";
+ audio.mozSrcObject.stop();
+ video.mozSrcObject.stop();
+ }, function(err) {
+ document.title = "failed gumtest";
+ stopMedia();
+ });
+ } catch(e) {
+ stopMedia();
+ }
+ }
+</script>
+</body></html>
diff --git a/mobile/android/tests/browser/robocop/robocop_head.js b/mobile/android/tests/browser/robocop/robocop_head.js
new file mode 100644
index 0000000000..0fa7e56c8a
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_head.js
@@ -0,0 +1,848 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// The test js is shared between sandboxed (which has no SpecialPowers object)
+// and content mochitests (where the |Components| object is accessible only as
+// SpecialPowers.Components). Expose Components if necessary here to make things
+// work everywhere.
+//
+// Even if the real |Components| doesn't exist, we might shim in a simple JS
+// placebo for compat. An easy way to differentiate this from the real thing
+// is whether the property is read-only or not.
+{
+ let c = Object.getOwnPropertyDescriptor(this, 'Components');
+ if ((!c.value || c.writable) && typeof SpecialPowers === 'object')
+ Components = SpecialPowers.wrap(SpecialPowers.Components);
+}
+
+/*
+ * This file contains common code that is loaded before each test file(s).
+ * See http://developer.mozilla.org/en/docs/Writing_xpcshell-based_unit_tests
+ * for more information.
+ */
+
+var _quit = false;
+var _tests_pending = 0;
+var _pendingTimers = [];
+var _cleanupFunctions = [];
+
+function _dump(str) {
+ let start = /^TEST-/.test(str) ? "\n" : "";
+ dump(start + str);
+}
+
+// Disable automatic network detection, so tests work correctly when
+// not connected to a network.
+{
+ let ios = Components.classes["@mozilla.org/network/io-service;1"]
+ .getService(Components.interfaces.nsIIOService2);
+ ios.manageOfflineStatus = false;
+ ios.offline = false;
+}
+
+// Determine if we're running on parent or child
+var runningInParent = true;
+try {
+ runningInParent = Components.classes["@mozilla.org/xre/runtime;1"].
+ getService(Components.interfaces.nsIXULRuntime).processType
+ == Components.interfaces.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+}
+catch (e) { }
+
+try {
+ if (runningInParent) {
+ let prefs = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+
+ // disable necko IPC security checks for xpcshell, as they lack the
+ // docshells needed to pass them
+ prefs.setBoolPref("network.disable.ipc.security", true);
+
+ // Disable IPv6 lookups for 'localhost' on windows.
+ if ("@mozilla.org/windows-registry-key;1" in Components.classes) {
+ prefs.setCharPref("network.dns.ipv4OnlyDomains", "localhost");
+ }
+ }
+}
+catch (e) { }
+
+// Enable crash reporting, if possible
+// We rely on the Python harness to set MOZ_CRASHREPORTER_NO_REPORT
+// and handle checking for minidumps.
+// Note that if we're in a child process, we don't want to init the
+// crashreporter component.
+try { // nsIXULRuntime is not available in some configurations.
+ if (runningInParent &&
+ "@mozilla.org/toolkit/crash-reporter;1" in Components.classes) {
+ // Remember to update </toolkit/crashreporter/test/unit/test_crashreporter.js>
+ // too if you change this initial setting.
+ let crashReporter =
+ Components.classes["@mozilla.org/toolkit/crash-reporter;1"]
+ .getService(Components.interfaces.nsICrashReporter);
+ crashReporter.enabled = true;
+ crashReporter.minidumpPath = do_get_cwd();
+ }
+}
+catch (e) { }
+
+/**
+ * Date.now() is not necessarily monotonically increasing (insert sob story
+ * about times not being the right tool to use for measuring intervals of time,
+ * robarnold can tell all), so be wary of error by erring by at least
+ * _timerFuzz ms.
+ */
+const _timerFuzz = 15;
+
+function _Timer(func, delay) {
+ delay = Number(delay);
+ if (delay < 0)
+ do_throw("do_timeout() delay must be nonnegative");
+
+ if (typeof func !== "function")
+ do_throw("string callbacks no longer accepted; use a function!");
+
+ this._func = func;
+ this._start = Date.now();
+ this._delay = delay;
+
+ var timer = Components.classes["@mozilla.org/timer;1"]
+ .createInstance(Components.interfaces.nsITimer);
+ timer.initWithCallback(this, delay + _timerFuzz, timer.TYPE_ONE_SHOT);
+
+ // Keep timer alive until it fires
+ _pendingTimers.push(timer);
+}
+
+_Timer.prototype = {
+ QueryInterface: function(iid) {
+ if (iid.equals(Components.interfaces.nsITimerCallback) ||
+ iid.equals(Components.interfaces.nsISupports))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ },
+
+ notify: function(timer) {
+ _pendingTimers.splice(_pendingTimers.indexOf(timer), 1);
+
+ // The current nsITimer implementation can undershoot, but even if it
+ // couldn't, paranoia is probably a virtue here given the potential for
+ // random orange on tinderboxen.
+ var end = Date.now();
+ var elapsed = end - this._start;
+ if (elapsed >= this._delay) {
+ try {
+ this._func.call(null);
+ } catch (e) {
+ do_throw("exception thrown from do_timeout callback: " + e);
+ }
+ return;
+ }
+
+ // Timer undershot, retry with a little overshoot to try to avoid more
+ // undershoots.
+ var newDelay = this._delay - elapsed;
+ do_timeout(newDelay, this._func);
+ }
+};
+
+function _do_quit() {
+ _dump("TEST-INFO | (xpcshell/head.js) | exiting test\n");
+
+ _quit = true;
+}
+
+function _dump_exception_stack(stack) {
+ stack.split("\n").forEach(function(frame) {
+ if (!frame)
+ return;
+ // frame is of the form "fname(args)@file:line"
+ let frame_regexp = new RegExp("(.*)\\(.*\\)@(.*):(\\d*)", "g");
+ let parts = frame_regexp.exec(frame);
+ if (parts)
+ dump("JS frame :: " + parts[2] + " :: " + (parts[1] ? parts[1] : "anonymous")
+ + " :: line " + parts[3] + "\n");
+ else /* Could be a -e (command line string) style location. */
+ dump("JS frame :: " + frame + "\n");
+ });
+}
+
+/************** Functions to be used from the tests **************/
+
+/**
+ * Prints a message to the output log.
+ */
+function do_print(msg) {
+ var caller_stack = Components.stack.caller;
+ _dump("TEST-INFO | " + caller_stack.filename + " | " + msg + "\n");
+}
+
+/**
+ * Calls the given function at least the specified number of milliseconds later.
+ * The callback will not undershoot the given time, but it might overshoot --
+ * don't expect precision!
+ *
+ * @param delay : uint
+ * the number of milliseconds to delay
+ * @param callback : function() : void
+ * the function to call
+ */
+function do_timeout(delay, func) {
+ new _Timer(func, Number(delay));
+}
+
+function do_execute_soon(callback) {
+ do_test_pending();
+ var tm = Components.classes["@mozilla.org/thread-manager;1"]
+ .getService(Components.interfaces.nsIThreadManager);
+
+ tm.mainThread.dispatch({
+ run: function() {
+ try {
+ callback();
+ } catch (e) {
+ // do_check failures are already logged and set _quit to true and throw
+ // NS_ERROR_ABORT. If both of those are true it is likely this exception
+ // has already been logged so there is no need to log it again. It's
+ // possible that this will mask an NS_ERROR_ABORT that happens after a
+ // do_check failure though.
+ if (!_quit || e != Components.results.NS_ERROR_ABORT) {
+ _dump("TEST-UNEXPECTED-FAIL | (xpcshell/head.js) | " + e);
+ if (e.stack) {
+ dump(" - See following stack:\n");
+ _dump_exception_stack(e.stack);
+ }
+ else {
+ dump("\n");
+ }
+ _do_quit();
+ }
+ }
+ finally {
+ do_test_finished();
+ }
+ }
+ }, Components.interfaces.nsIThread.DISPATCH_NORMAL);
+}
+
+function do_throw(text, stack) {
+ if (!stack)
+ stack = Components.stack.caller;
+
+ _dump("TEST-UNEXPECTED-FAIL | " + stack.filename + " | " + text +
+ " - See following stack:\n");
+ var frame = Components.stack;
+ while (frame != null) {
+ _dump(frame + "\n");
+ frame = frame.caller;
+ }
+
+ _do_quit();
+ throw Components.results.NS_ERROR_ABORT;
+}
+
+function do_throw_todo(text, stack) {
+ if (!stack)
+ stack = Components.stack.caller;
+
+ _dump("TEST-UNEXPECTED-PASS | " + stack.filename + " | " + text +
+ " - See following stack:\n");
+ var frame = Components.stack;
+ while (frame != null) {
+ _dump(frame + "\n");
+ frame = frame.caller;
+ }
+
+ _do_quit();
+ throw Components.results.NS_ERROR_ABORT;
+}
+
+function do_report_unexpected_exception(ex, text) {
+ var caller_stack = Components.stack.caller;
+ text = text ? text + " - " : "";
+
+ _dump("TEST-UNEXPECTED-FAIL | " + caller_stack.filename + " | " + text +
+ "Unexpected exception " + ex + ", see following stack:\n" + ex.stack +
+ "\n");
+
+ _do_quit();
+ throw Components.results.NS_ERROR_ABORT;
+}
+
+function do_note_exception(ex, text) {
+ var caller_stack = Components.stack.caller;
+ text = text ? text + " - " : "";
+
+ _dump("TEST-INFO | " + caller_stack.filename + " | " + text +
+ "Swallowed exception " + ex + ", see following stack:\n" + ex.stack +
+ "\n");
+}
+
+function _do_check_neq(left, right, stack, todo) {
+ if (!stack)
+ stack = Components.stack.caller;
+
+ var text = left + " != " + right;
+ if (left == right) {
+ if (!todo) {
+ do_throw(text, stack);
+ } else {
+ _dump("TEST-KNOWN-FAIL | " + stack.filename + " | [" + stack.name +
+ " : " + stack.lineNumber + "] " + text +"\n");
+ }
+ } else {
+ if (!todo) {
+ _dump("TEST-PASS | " + stack.filename + " | [" + stack.name + " : " +
+ stack.lineNumber + "] " + text + "\n");
+ } else {
+ do_throw_todo(text, stack);
+ }
+ }
+}
+
+function do_check_neq(left, right, stack) {
+ if (!stack)
+ stack = Components.stack.caller;
+
+ _do_check_neq(left, right, stack, false);
+}
+
+function todo_check_neq(left, right, stack) {
+ if (!stack)
+ stack = Components.stack.caller;
+
+ _do_check_neq(left, right, stack, true);
+}
+
+function do_report_result(passed, text, stack, todo) {
+ if (passed) {
+ if (todo) {
+ do_throw_todo(text, stack);
+ } else {
+ _dump("TEST-PASS | " + stack.filename + " | [" + stack.name + " : " +
+ stack.lineNumber + "] " + text + "\n");
+ }
+ } else {
+ if (todo) {
+ _dump("TEST-KNOWN-FAIL | " + stack.filename + " | [" + stack.name +
+ " : " + stack.lineNumber + "] " + text +"\n");
+ } else {
+ do_throw(text, stack);
+ }
+ }
+}
+
+/**
+ * Checks for a true condition, with a success message.
+ */
+function ok(condition, msg) {
+ do_report_result(condition, msg, Components.stack.caller, false);
+}
+
+/**
+ * Checks for a condition equality, with a success message.
+ */
+function is(left, right, msg) {
+ do_report_result(left === right, "[ " + left + " === " + right + " ] " + msg,
+ Components.stack.caller, false);
+}
+
+function _do_check_eq(left, right, stack, todo) {
+ if (!stack)
+ stack = Components.stack.caller;
+
+ var text = left + " == " + right;
+ do_report_result(left == right, text, stack, todo);
+}
+
+function do_check_eq(left, right, stack) {
+ if (!stack)
+ stack = Components.stack.caller;
+
+ _do_check_eq(left, right, stack, false);
+}
+
+function todo_check_eq(left, right, stack) {
+ if (!stack)
+ stack = Components.stack.caller;
+
+ _do_check_eq(left, right, stack, true);
+}
+
+function do_check_true(condition, stack) {
+ if (!stack)
+ stack = Components.stack.caller;
+
+ do_check_eq(condition, true, stack);
+}
+
+function todo_check_true(condition, stack) {
+ if (!stack)
+ stack = Components.stack.caller;
+
+ todo_check_eq(condition, true, stack);
+}
+
+function do_check_false(condition, stack) {
+ if (!stack)
+ stack = Components.stack.caller;
+
+ do_check_eq(condition, false, stack);
+}
+
+function todo_check_false(condition, stack) {
+ if (!stack)
+ stack = Components.stack.caller;
+
+ todo_check_eq(condition, false, stack);
+}
+
+function do_check_null(condition, stack=Components.stack.caller) {
+ do_check_eq(condition, null, stack);
+}
+
+function todo_check_null(condition, stack=Components.stack.caller) {
+ todo_check_eq(condition, null, stack);
+}
+
+/**
+ * Check that |value| matches |pattern|.
+ *
+ * A |value| matches a pattern |pattern| if any one of the following is true:
+ *
+ * - |value| and |pattern| are both objects; |pattern|'s enumerable
+ * properties' values are valid patterns; and for each enumerable
+ * property |p| of |pattern|, plus 'length' if present at all, |value|
+ * has a property |p| whose value matches |pattern.p|. Note that if |j|
+ * has other properties not present in |p|, |j| may still match |p|.
+ *
+ * - |value| and |pattern| are equal string, numeric, or boolean literals
+ *
+ * - |pattern| is |undefined| (this is a wildcard pattern)
+ *
+ * - typeof |pattern| == "function", and |pattern(value)| is true.
+ *
+ * For example:
+ *
+ * do_check_matches({x:1}, {x:1}) // pass
+ * do_check_matches({x:1}, {}) // fail: all pattern props required
+ * do_check_matches({x:1}, {x:2}) // fail: values must match
+ * do_check_matches({x:1}, {x:1, y:2}) // pass: extra props tolerated
+ *
+ * // Property order is irrelevant.
+ * do_check_matches({x:"foo", y:"bar"}, {y:"bar", x:"foo"}) // pass
+ *
+ * do_check_matches({x:undefined}, {x:1}) // pass: 'undefined' is wildcard
+ * do_check_matches({x:undefined}, {x:2})
+ * do_check_matches({x:undefined}, {y:2}) // fail: 'x' must still be there
+ *
+ * // Patterns nest.
+ * do_check_matches({a:1, b:{c:2,d:undefined}}, {a:1, b:{c:2,d:3}})
+ *
+ * // 'length' property counts, even if non-enumerable.
+ * do_check_matches([3,4,5], [3,4,5]) // pass
+ * do_check_matches([3,4,5], [3,5,5]) // fail; value doesn't match
+ * do_check_matches([3,4,5], [3,4,5,6]) // fail; length doesn't match
+ *
+ * // functions in patterns get applied.
+ * do_check_matches({foo:v => v.length == 2}, {foo:"hi"}) // pass
+ * do_check_matches({foo:v => v.length == 2}, {bar:"hi"}) // fail
+ * do_check_matches({foo:v => v.length == 2}, {foo:"hello"}) // fail
+ *
+ * // We don't check constructors, prototypes, or classes. However, if
+ * // pattern has a 'length' property, we require values to match that as
+ * // well, even if 'length' is non-enumerable in the pattern. So arrays
+ * // are useful as patterns.
+ * do_check_matches({0:0, 1:1, length:2}, [0,1]) // pass
+ * do_check_matches({0:1}, [1,2]) // pass
+ * do_check_matches([0], {0:0, length:1}) // pass
+ *
+ * Notes:
+ *
+ * The 'length' hack gives us reasonably intuitive handling of arrays.
+ *
+ * This is not a tight pattern-matcher; it's only good for checking data
+ * from well-behaved sources. For example:
+ * - By default, we don't mind values having extra properties.
+ * - We don't check for proxies or getters.
+ * - We don't check the prototype chain.
+ * However, if you know the values are, say, JSON, which is pretty
+ * well-behaved, and if you want to tolerate additional properties
+ * appearing on the JSON for backward-compatibility, then do_check_matches
+ * is ideal. If you do want to be more careful, you can use function
+ * patterns to implement more stringent checks.
+ */
+function do_check_matches(pattern, value, stack=Components.stack.caller, todo=false) {
+ var matcher = pattern_matcher(pattern);
+ var text = "VALUE: " + uneval(value) + "\nPATTERN: " + uneval(pattern) + "\n";
+ var diagnosis = []
+ if (matcher(value, diagnosis)) {
+ do_report_result(true, "value matches pattern:\n" + text, stack, todo);
+ } else {
+ text = ("value doesn't match pattern:\n" +
+ text +
+ "DIAGNOSIS: " +
+ format_pattern_match_failure(diagnosis[0]) + "\n");
+ do_report_result(false, text, stack, todo);
+ }
+}
+
+function todo_check_matches(pattern, value, stack=Components.stack.caller) {
+ do_check_matches(pattern, value, stack, true);
+}
+
+// Return a pattern-matching function of one argument, |value|, that
+// returns true if |value| matches |pattern|.
+//
+// If the pattern doesn't match, and the pattern-matching function was
+// passed its optional |diagnosis| argument, the pattern-matching function
+// sets |diagnosis|'s '0' property to a JSON-ish description of the portion
+// of the pattern that didn't match, which can be formatted legibly by
+// format_pattern_match_failure.
+function pattern_matcher(pattern) {
+ function explain(diagnosis, reason) {
+ if (diagnosis) {
+ diagnosis[0] = reason;
+ }
+ return false;
+ }
+ if (typeof pattern == "function") {
+ return pattern;
+ } else if (typeof pattern == "object" && pattern) {
+ var matchers = [];
+ for (let p in pattern) {
+ matchers.push([p, pattern_matcher(pattern[p])]);
+ }
+ // Kludge: include 'length', if not enumerable. (If it is enumerable,
+ // we picked it up in the array comprehension, above.
+ ld = Object.getOwnPropertyDescriptor(pattern, 'length');
+ if (ld && !ld.enumerable) {
+ matchers.push(['length', pattern_matcher(pattern.length)])
+ }
+ return function (value, diagnosis) {
+ if (!(value && typeof value == "object")) {
+ return explain(diagnosis, "value not object");
+ }
+ for (let [p, m] of matchers) {
+ var element_diagnosis = [];
+ if (!(p in value && m(value[p], element_diagnosis))) {
+ return explain(diagnosis, { property:p,
+ diagnosis:element_diagnosis[0] });
+ }
+ }
+ return true;
+ };
+ } else if (pattern === undefined) {
+ return function(value) { return true; };
+ } else {
+ return function (value, diagnosis) {
+ if (value !== pattern) {
+ return explain(diagnosis, "pattern " + uneval(pattern) + " not === to value " + uneval(value));
+ }
+ return true;
+ };
+ }
+}
+
+// Format an explanation for a pattern match failure, as stored in the
+// second argument to a matching function.
+function format_pattern_match_failure(diagnosis, indent="") {
+ var a;
+ if (!diagnosis) {
+ a = "Matcher did not explain reason for mismatch.";
+ } else if (typeof diagnosis == "string") {
+ a = diagnosis;
+ } else if (diagnosis.property) {
+ a = "Property " + uneval(diagnosis.property) + " of object didn't match:\n";
+ a += format_pattern_match_failure(diagnosis.diagnosis, indent + " ");
+ }
+ return indent + a;
+}
+
+function do_test_pending() {
+ ++_tests_pending;
+
+ _dump("TEST-INFO | (xpcshell/head.js) | test " + _tests_pending +
+ " pending\n");
+}
+
+function do_test_finished() {
+ _dump("TEST-INFO | (xpcshell/head.js) | test " + _tests_pending +
+ " finished\n");
+
+ if (--_tests_pending == 0) {
+ _do_execute_cleanup();
+ _do_quit();
+ }
+}
+
+function do_get_file(path, allowNonexistent) {
+ try {
+ let lf = Components.classes["@mozilla.org/file/directory_service;1"]
+ .getService(Components.interfaces.nsIProperties)
+ .get("CurWorkD", Components.interfaces.nsILocalFile);
+
+ let bits = path.split("/");
+ for (let i = 0; i < bits.length; i++) {
+ if (bits[i]) {
+ if (bits[i] == "..")
+ lf = lf.parent;
+ else
+ lf.append(bits[i]);
+ }
+ }
+
+ if (!allowNonexistent && !lf.exists()) {
+ // Not using do_throw(): caller will continue.
+ var stack = Components.stack.caller;
+ _dump("TEST-UNEXPECTED-FAIL | " + stack.filename + " | [" +
+ stack.name + " : " + stack.lineNumber + "] " + lf.path +
+ " does not exist\n");
+ }
+
+ return lf;
+ }
+ catch (ex) {
+ do_throw(ex.toString(), Components.stack.caller);
+ }
+
+ return null;
+}
+
+// do_get_cwd() isn't exactly self-explanatory, so provide a helper
+function do_get_cwd() {
+ return do_get_file("");
+}
+
+function do_load_manifest(path) {
+ var lf = do_get_file(path);
+ const nsIComponentRegistrar = Components.interfaces.nsIComponentRegistrar;
+ do_check_true(Components.manager instanceof nsIComponentRegistrar);
+ Components.manager.autoRegister(lf);
+}
+
+/**
+ * Registers a function that will run when the test harness is done running all
+ * tests.
+ *
+ * @param aFunction
+ * The function to be called when the test harness has finished running.
+ */
+function do_register_cleanup(func) {
+ _dump("TEST-INFO | " + _TEST_FILE + " | " +
+ (_gRunningTest ? _gRunningTest.name + " " : "") +
+ "registering cleanup function.");
+
+ _cleanupFunctions.push(func);
+}
+
+/**
+ * Execute a function when the test harness is done running all tests.
+ */
+function _do_execute_cleanup() {
+ let func;
+ while ((func = _cleanupFunctions.pop())) {
+ _dump("TEST-INFO | " + _TEST_FILE + " | executing cleanup function.");
+ func();
+ }
+}
+
+/**
+ * Add a test function to the list of tests that are to be run asynchronously.
+ *
+ * Each test function must call run_next_test() when it's done. Test files
+ * should call run_next_test() in their run_test function to execute all
+ * async tests.
+ *
+ * @return the test function that was passed in.
+ */
+var _gTests = [];
+function add_test(func) {
+ _gTests.push([false, func]);
+ return func;
+}
+
+// We lazy import Task.jsm so we don't incur a run-time penalty for all tests.
+var _Task;
+
+/**
+ * Add a test function which is a Task function.
+ *
+ * Task functions are functions fed into Task.jsm's Task.spawn(). They are
+ * generators that emit promises.
+ *
+ * If an exception is thrown, a do_check_* comparison fails, or if a rejected
+ * promise is yielded, the test function aborts immediately and the test is
+ * reported as a failure.
+ *
+ * Unlike add_test(), there is no need to call run_next_test(). The next test
+ * will run automatically as soon the task function is exhausted. To trigger
+ * premature (but successful) termination of the function, simply return or
+ * throw a Task.Result instance.
+ *
+ * Example usage:
+ *
+ * add_task(function test() {
+ * let result = yield Promise.resolve(true);
+ *
+ * do_check_true(result);
+ *
+ * let secondary = yield someFunctionThatReturnsAPromise(result);
+ * do_check_eq(secondary, "expected value");
+ * });
+ *
+ * add_task(function test_early_return() {
+ * let result = yield somethingThatReturnsAPromise();
+ *
+ * if (!result) {
+ * // Test is ended immediately, with success.
+ * return;
+ * }
+ *
+ * do_check_eq(result, "foo");
+ * });
+ */
+function add_task(func) {
+ if (!_Task) {
+ let ns = {};
+ _Task = Components.utils.import("resource://gre/modules/Task.jsm", ns).Task;
+ }
+
+ _gTests.push([true, func]);
+}
+
+/**
+ * Runs the next test function from the list of async tests.
+ */
+var _gRunningTest = null;
+var _gTestIndex = 0; // The index of the currently running test.
+function run_next_test()
+{
+ function _run_next_test()
+ {
+ if (_gTestIndex < _gTests.length) {
+ do_test_pending();
+ let _isTask;
+ [_isTask, _gRunningTest] = _gTests[_gTestIndex++];
+ _dump("TEST-INFO | " + _TEST_FILE + " | Starting " + _gRunningTest.name);
+
+ if (_isTask) {
+ _Task.spawn(_gRunningTest)
+ .then(run_next_test, do_report_unexpected_exception);
+ } else {
+ // Exceptions do not kill asynchronous tests, so they'll time out.
+ try {
+ _gRunningTest();
+ } catch (e) {
+ do_throw(e);
+ }
+ }
+ }
+ }
+
+ // For sane stacks during failures, we execute this code soon, but not now.
+ // We do this now, before we call do_test_finished(), to ensure the pending
+ // counter (_tests_pending) never reaches 0 while we still have tests to run
+ // (do_execute_soon bumps that counter).
+ do_execute_soon(_run_next_test);
+
+ if (_gRunningTest !== null) {
+ // Close the previous test do_test_pending call.
+ do_test_finished();
+ }
+}
+
+/**
+ * End of code adapted from xpcshell head.js
+ */
+
+
+/**
+ * JavaBridge facilitates communication between Java and JS. See
+ * JavascriptBridge.java for the corresponding JavascriptBridge and docs.
+ */
+
+function JavaBridge(obj) {
+
+ this._EVENT_TYPE = "Robocop:JS";
+ this._target = obj;
+ // The number of replies needed to answer all outstanding sync calls.
+ this._repliesNeeded = 0;
+ this._Services.obs.addObserver(this, this._EVENT_TYPE, false);
+
+ this._sendMessage("notify-loaded", []);
+};
+
+JavaBridge.prototype = {
+
+ _Services: Components.utils.import(
+ "resource://gre/modules/Services.jsm", {}).Services,
+
+ _sendMessageToJava: Components.utils.import(
+ "resource://gre/modules/Messaging.jsm", {}).Messaging.sendRequest,
+
+ _sendMessage: function (innerType, args) {
+ this._sendMessageToJava({
+ type: this._EVENT_TYPE,
+ innerType: innerType,
+ method: args[0],
+ args: Array.prototype.slice.call(args, 1),
+ });
+ },
+
+ observe: function(subject, topic, data) {
+ let message = JSON.parse(data);
+ if (message.innerType === "sync-reply") {
+ // Reply to our Javascript-to-Java sync call
+ this._repliesNeeded--;
+ return;
+ }
+ // Call the corresponding method on the target
+ try {
+ this._target[message.method].apply(this._target, message.args);
+ } catch (e) {
+ do_report_unexpected_exception(e, "Failed to call " + message.method);
+ }
+ if (message.innerType === "sync-call") {
+ // Reply for sync message
+ this._sendMessage("sync-reply", [message.method]);
+ }
+ },
+
+ /**
+ * Synchronously call a method in Java,
+ * given the method name followed by a list of arguments.
+ */
+ syncCall: function (methodName /*, ... */) {
+ this._sendMessage("sync-call", arguments);
+ let thread = this._Services.tm.currentThread;
+ let initialReplies = this._repliesNeeded;
+ // Need one more reply to answer the current sync call.
+ this._repliesNeeded++;
+ // Wait for the reply to arrive. Normally we would not want to
+ // spin the event loop, but here we're in a test and our API
+ // specifies a synchronous call, so we spin the loop to wait for
+ // the call to finish.
+ while (this._repliesNeeded > initialReplies) {
+ thread.processNextEvent(true);
+ }
+ },
+
+ /**
+ * Asynchronously call a method in Java,
+ * given the method name followed by a list of arguments.
+ */
+ asyncCall: function (methodName /*, ... */) {
+ this._sendMessage("async-call", arguments);
+ },
+
+ /**
+ * Disconnect with Java.
+ */
+ disconnect: function () {
+ this._Services.obs.removeObserver(this, this._EVENT_TYPE);
+ },
+};
diff --git a/mobile/android/tests/browser/robocop/robocop_input.html b/mobile/android/tests/browser/robocop/robocop_input.html
new file mode 100644
index 0000000000..50ddd6e9ac
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_input.html
@@ -0,0 +1,165 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Robocop Input</title>
+ </head>
+ <body>
+ <p>Input: <input id="input" type="text"></p>
+ <p>Text area: <textarea id="text-area"></textarea></p>
+ <p>Content editable: <div id="content-editable" contentEditable="true"></div></p>
+ <p>Design mode: <iframe id="design-mode" src="data:text/html;charset=utf-8,<html><body></body></html>"></iframe></p>
+ <p>Resetting input: <input id="resetting-input" type="text"></p>
+ <p>Hiding input: <input id="hiding-input" type="text"></p>
+ <script type="application/javascript;version=1.8" src="robocop_head.js"></script>
+ <script type="application/javascript;version=1.8">
+ let input = document.getElementById("input");
+ let textArea = document.getElementById("text-area");
+ let contentEditable = document.getElementById("content-editable");
+
+ let designMode = document.getElementById("design-mode");
+ try {
+ designMode.contentDocument.designMode = "on";
+ } catch (e) {
+ // Setting designMode above sometimes fails, so try again later.
+ setTimeout(function() { designMode.contentDocument.designMode = "on" }, 0);
+ }
+
+ // Spatial navigation interferes with design-mode key event tests.
+ SpecialPowers.setBoolPref("snav.enabled", false);
+
+ // An input that resets the editor on every input by resetting the value property.
+ let resetting_input = document.getElementById("resetting-input");
+ resetting_input.addEventListener('input', function() {
+ this.value = this.value;
+ });
+
+ // An input that hides on input.
+ let hiding_input = document.getElementById("hiding-input");
+ hiding_input.addEventListener('keydown', function(e) {
+ if (e.key === "!") { // '!' key event as sent by testInputConnection.java.
+ this.value = "";
+ this.style.display = "none";
+ }
+ });
+
+ let getEditor, setValue, setSelection;
+
+ let test = {
+ focus_input: function(val) {
+ getEditor = function() {
+ return SpecialPowers.wrap(input).QueryInterface(
+ SpecialPowers.Ci.nsIDOMNSEditableElement).editor;
+ };
+ setValue = function(val) {
+ input.value = val;
+ };
+ setSelection = function(pos) {
+ input.setSelectionRange(pos, pos);
+ };
+ setValue(val);
+ input.focus();
+ },
+
+ focus_text_area: function(val) {
+ getEditor = function() {
+ return SpecialPowers.wrap(textArea).QueryInterface(
+ SpecialPowers.Ci.nsIDOMNSEditableElement).editor;
+ };
+ setValue = function(val) {
+ textArea.value = val;
+ };
+ setSelection = function(pos) {
+ textArea.setSelectionRange(pos, pos);
+ };
+ setValue(val);
+ textArea.focus();
+ },
+
+ focus_content_editable: function(val) {
+ getEditor = function() {
+ return SpecialPowers.wrap(window).QueryInterface(
+ SpecialPowers.Ci.nsIInterfaceRequestor).getInterface(
+ SpecialPowers.Ci.nsIWebNavigation).QueryInterface(
+ SpecialPowers.Ci.nsIDocShell).editor;
+ };
+ setValue = function(val) {
+ contentEditable.innerHTML = val;
+ };
+ setSelection = function(pos) {
+ window.getSelection().collapse(contentEditable.firstChild, pos);
+ };
+ setValue(val);
+ contentEditable.focus();
+ },
+
+ focus_design_mode: function(val) {
+ getEditor = function() {
+ return SpecialPowers.wrap(designMode.contentWindow).QueryInterface(
+ SpecialPowers.Ci.nsIInterfaceRequestor).getInterface(
+ SpecialPowers.Ci.nsIWebNavigation).QueryInterface(
+ SpecialPowers.Ci.nsIDocShell).editor;
+ };
+ setValue = function(val) {
+ designMode.contentDocument.body.innerHTML = val;
+ };
+ setSelection = function(pos) {
+ designMode.contentWindow.getSelection().collapse(
+ designMode.contentDocument.body.firstChild, pos);
+ };
+ setValue(val);
+ designMode.contentWindow.focus();
+ designMode.contentDocument.body.focus();
+ },
+
+ test_reflush_changes: function() {
+ let inputIme = getEditor().QueryInterface(SpecialPowers.Ci.nsIEditorIMESupport);
+ do_check_true(inputIme.composing);
+
+ // Ending the composition then setting the input value triggers the bug.
+ inputIme.forceCompositionEnd();
+ setValue("good"); // Value that testInputConnection.java expects.
+ setSelection(4);
+ },
+
+ test_set_selection: function() {
+ let inputIme = getEditor().QueryInterface(SpecialPowers.Ci.nsIEditorIMESupport);
+ do_check_true(inputIme.composing);
+
+ // Ending the composition then setting the selection triggers the bug.
+ inputIme.forceCompositionEnd();
+ setSelection(3); // Offsets that testInputConnection.java expects.
+ },
+
+ test_bug1123514: function() {
+ document.activeElement.addEventListener('input', function test_bug1123514_listener() {
+ this.removeEventListener('input', test_bug1123514_listener);
+
+ // Only works on input and textarea.
+ if (this.value === 'b') {
+ this.value = 'abc';
+ }
+ });
+ },
+
+ focus_resetting_input: function(val) {
+ resetting_input.value = val;
+ resetting_input.focus();
+ },
+
+ focus_hiding_input: function(val) {
+ hiding_input.value = val;
+ hiding_input.style.display = "";
+ hiding_input.focus();
+ },
+
+ finish_test: function() {
+ java.disconnect();
+ },
+ };
+
+ var java = new JavaBridge(test);
+ </script>
+ </body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/robocop_javascript.html b/mobile/android/tests/browser/robocop/robocop_javascript.html
new file mode 100644
index 0000000000..8719f5c6dd
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_javascript.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<head>
+<meta charset="UTF-8">
+<title>Mochitest Robotium Javascript Test Harness</title>
+<link rel="author" title="nalexander" href="mailto:nalexander@mozilla.com">
+<script type="application/javascript;version=1.8" src="robocop_testharness.js"></script>
+<script>
+var param = /[&?]path=([^&]+)/.exec(location.search);
+if (param) {
+ // We encode so that absolute URLs can be provided. Since the
+ // encoding of a relative filename is just the filename, no special
+ // processing is needed for the most common case.
+ var src = decodeURIComponent(param[1]);
+ document.title = src;
+
+ // Provided by robocop_testharness.js.
+ testOneFile(src);
+}
+</script>
+</head>
diff --git a/mobile/android/tests/browser/robocop/robocop_link_to_slow_loading.html b/mobile/android/tests/browser/robocop/robocop_link_to_slow_loading.html
new file mode 100644
index 0000000000..45e487c2a3
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_link_to_slow_loading.html
@@ -0,0 +1,12 @@
+<html>
+<head>
+ <title>Link</title>
+ <meta name="viewport" content="initial-scale=1.0"/>
+ <meta charset="utf-8">
+</head>
+<body style="margin: 0; padding: 0">
+<div style="text-align: center; margin: 0; padding: 0">
+ <a style="font-size: 60px" href="robocop_slow_loading.html">Slow Loading Page</a>
+</div>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/robocop_login_01.html b/mobile/android/tests/browser/robocop/robocop_login_01.html
new file mode 100644
index 0000000000..19c7dd1f27
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_login_01.html
@@ -0,0 +1,21 @@
+<html>
+<script>
+function login(){
+document.login.username.value="Test1";
+document.login.password.value="Test2";
+document.getElementById('submit').click();
+}
+</script>
+<head>
+ <title>Robocop Login</title>
+ <meta charset="utf-8">
+</head>
+<body onload="login()">
+ <h2>User Login </h2>
+ <form name="login" method="post" action="robocop_blank_01.html">
+ Username: <input type="text" name="username" id="username"><br>
+ Password: <input type="password" name="password" id="password"><br>
+ <input type="submit" id="submit" name="submit" value="Login!">
+ </form>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/robocop_login_02.html b/mobile/android/tests/browser/robocop/robocop_login_02.html
new file mode 100644
index 0000000000..55d7f93082
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_login_02.html
@@ -0,0 +1,21 @@
+<html>
+<script>
+function login(){
+document.login.username.value="Test2";
+document.login.password.value="Test2";
+document.getElementById('submit').click();
+}
+</script>
+<head>
+ <title>Robocop Login</title>
+ <meta charset="utf-8">
+</head>
+<body onload="login()">
+ <h2>User Login </h2>
+ <form name="login" method="post" action="robocop_blank_02.html">
+ Username: <input type="text" name="username" id="username"><br>
+ Password: <input type="password" name="password" id="password"><br>
+ <input type="submit" id="submit" name="submit" value="Login!">
+ </form>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/robocop_offline_storage.html b/mobile/android/tests/browser/robocop/robocop_offline_storage.html
new file mode 100644
index 0000000000..50878d7643
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_offline_storage.html
@@ -0,0 +1,8 @@
+<html manifest="robocop_offline">
+<head>
+ <title>Robocop offline storage</title>
+ <meta charset="utf-8">
+</head>
+<body>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/robocop_picture_link.html b/mobile/android/tests/browser/robocop/robocop_picture_link.html
new file mode 100644
index 0000000000..a56af54c0f
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_picture_link.html
@@ -0,0 +1,13 @@
+<html>
+<head>
+ <title>Picture Link</title>
+ <link rel="shortcut icon" href="data:image/gif;base64,R0lGODlhCwALAIAAAAAA3pn/ZiH5BAEAAAEALAAAAAALAAsAAAIUhA+hkcuO4lmNVindo7qyrIXiGBYAOw==" />
+ <meta name="viewport" content="initial-scale=1.0"/>
+ <meta charset="utf-8">
+</head>
+<body style="margin: 0; padding: 0">
+ <div style="text-align: center; margin: 0; padding: 0">
+ <a style="font-size: 60px" href="robocop_blank_02.html"><img src="Firefox.jpg"></img></a>
+ </div>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/robocop_popup.html b/mobile/android/tests/browser/robocop/robocop_popup.html
new file mode 100644
index 0000000000..4b7db35c7e
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_popup.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Page creating a popup</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+ window.open("data:text/plain;charset=utf-8,a", "a");
+ </script>
+ </body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/robocop_search.html b/mobile/android/tests/browser/robocop/robocop_search.html
new file mode 100644
index 0000000000..581193c9d0
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_search.html
@@ -0,0 +1,11 @@
+<html>
+ <header>
+ <title> Robocop Search Engine </title>
+ </header>
+ <body>
+ <form method="get" action="http://www.google.com/search">
+ <input type="text" name="q" style="width:300px; height:500px;" maxlength="255" value="" />
+ <input type="submit" value="Google Search" />
+ </form>
+ </body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/robocop_slow_loading.html b/mobile/android/tests/browser/robocop/robocop_slow_loading.html
new file mode 100644
index 0000000000..8c87f5aacd
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_slow_loading.html
@@ -0,0 +1,23 @@
+<html>
+<head>
+ <title>Slow Loading</title>
+ <meta name="viewport" content="initial-scale=1.0"/>
+ <meta charset="utf-8">
+ <script type="text/javascript">
+
+ // Busy wait (There's no sleep function in JavaScript)
+ var waitForMilliseconds = 10000;
+ var start = new Date();
+ var now = null;
+ do {
+ now = new Date();
+ } while (now - start < waitForMilliseconds);
+
+ </script>
+</head>
+<body style="margin: 0; padding: 0">
+<div style="text-align: center; margin: 0; padding: 0">
+ <h1>This page is loading very slow.</h1>
+</div>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/robocop_suggestions.sjs b/mobile/android/tests/browser/robocop/robocop_suggestions.sjs
new file mode 100644
index 0000000000..2621288d91
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_suggestions.sjs
@@ -0,0 +1,32 @@
+/**
+ * Used with testSearchSuggestions.
+ * Returns a set of pre-defined suggestions for given prefixes.
+ */
+
+function handleRequest(request, response) {
+ let query = request.queryString.match(/^query=(.*)$/)[1];
+ query = decodeURIComponent(query).replace(/\+/g, " ");
+
+ let suggestMap = {
+ "f": ["facebook", "fandango", "frys", "forever 21", "fafsa"],
+ "fo": ["forever 21", "food network", "fox news", "foothill college", "fox"],
+ "foo": ["food network", "foothill college", "foot locker", "footloose", "foo fighters"],
+ "foo ": ["foo fighters", "foo bar", "foo bat", "foo bay"],
+ "foo b": ["foo bar", "foo bat", "foo bay"],
+ "foo ba": ["foo bar", "foo bat", "foo bay"],
+ "foo bar": ["foo bar"]
+ };
+
+ let suggestions = suggestMap[query];
+ if (!suggestions)
+ suggestions = [];
+ suggestions = [query, suggestions];
+
+ /*
+ * Sample result:
+ * ["foo",["food network","foothill college","foot locker",...]]
+ */
+ response.setHeader("Content-Type", "text/json", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.write(JSON.stringify(suggestions));
+}
diff --git a/mobile/android/tests/browser/robocop/robocop_testharness.js b/mobile/android/tests/browser/robocop/robocop_testharness.js
new file mode 100644
index 0000000000..acc711b8b2
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_testharness.js
@@ -0,0 +1,74 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function sendMessageToJava(message) {
+ SpecialPowers.Services.androidBridge.handleGeckoMessage(message);
+}
+
+function _evalURI(uri, sandbox) {
+ // We explicitly allow Cross-Origin requests, since it is useful for
+ // testing, but we allow relative URLs by maintaining our baseURI.
+ let req = SpecialPowers.Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance();
+
+ let baseURI = SpecialPowers.Services.io
+ .newURI(window.document.baseURI, window.document.characterSet, null);
+ let theURI = SpecialPowers.Services.io
+ .newURI(uri, window.document.characterSet, baseURI);
+
+ // We append a random slug to avoid caching: see
+ // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache.
+ req.open('GET', theURI.spec + ((/\?/).test(theURI.spec) ? "&slug=" : "?slug=") + (new Date()).getTime(), false);
+ req.setRequestHeader('Cache-Control', 'no-cache');
+ req.setRequestHeader('Pragma', 'no-cache');
+ req.send();
+
+ return SpecialPowers.Cu.evalInSandbox(req.responseText, sandbox, "1.8", uri, 1);
+}
+
+/**
+ * Execute the Javascript file at `uri` in a testing sandbox populated
+ * with the Javascript test harness.
+ *
+ * `uri` should be a String, relative (to window.document.baseURI) or
+ * absolute.
+ *
+ * The Javascript test harness sends all output to Java via
+ * Robocop:JS messages.
+ */
+function testOneFile(uri) {
+ let HEAD_JS = "robocop_head.js";
+
+ // System principal. This is dangerous, but this is test code that
+ // should only run on developer and build farm machines, and the
+ // test harness needs access to a lot of the Components API,
+ // including Components.stack. Wrapping Components.stack in
+ // SpecialPowers magic obfuscates stack traces wonderfully,
+ // defeating much of the point of the test harness.
+ let principal = SpecialPowers.Cc["@mozilla.org/systemprincipal;1"]
+ .createInstance(SpecialPowers.Ci.nsIPrincipal);
+
+ let testScope = SpecialPowers.Cu.Sandbox(principal);
+
+ // Populate test environment with test harness prerequisites.
+ testScope.Components = SpecialPowers.Components;
+ testScope._TEST_FILE = uri;
+
+ // Output from head.js is fed, line by line, to this function. We
+ // send any such output back to the Java Robocop harness.
+ testScope.dump = function (str) {
+ let message = { type: "Robocop:JS",
+ innerType: "progress",
+ message: str,
+ };
+ sendMessageToJava(message);
+ };
+
+ // Populate test environment with test harness. The symbols defined
+ // above must be present before executing the test harness.
+ _evalURI(HEAD_JS, testScope);
+
+ return _evalURI(uri, testScope);
+}
diff --git a/mobile/android/tests/browser/robocop/robocop_text_page.html b/mobile/android/tests/browser/robocop/robocop_text_page.html
new file mode 100644
index 0000000000..db30144ddf
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/robocop_text_page.html
@@ -0,0 +1,27 @@
+<html>
+<head>
+<title> Robocop Text Page </title>
+<meta name="viewport" content="initial-scale=1.0"/>
+<meta charset="utf-8">
+</head>
+<body>
+<p>Text taken from Wikipedia.org</p>
+<p> <b>Will be searching for this string:</b> Robocop 1 </p>
+<p>Mozilla is a free software community best known for producing the Firefox web browser. The Mozilla community uses, develops, spreads and supports Mozilla products and works to advance the goals of the Open Web described in the Mozilla Manifesto.[1] The community is supported institutionally by the Mozilla Foundation and its tax-paying subsidiary, the Mozilla Corporation.[2] </p>
+<div style='float: left; width: 100%; height: 500px; margin: 0; padding: 0; border: none'> </div>
+<p> <b>Will be searching for this string:</b> Robocop 2 </p>
+<p>In addition to the Firefox browser, Mozilla also produces Firefox Mobile, the Firefox OS mobile operating system, the bug tracking system Bugzilla and a number of other projects.</p>
+<div style='float: left; width: 200%; height: 500px; margin: 0; padding: 0; border: none'> </div>
+<p> <b>Will be searching for this string:</b> Robocop 3 </p>
+<p>On February 23, 1998, Netscape Communications Corporation created a project called Mozilla (after the original code name of the Netscape Navigator browser which — according to Pascal Finette — is a mashup of "Mosaic Killer") to co-ordinate the development of the Mozilla Application Suite, the open source version of Netscape's internet software, Netscape Communicator.[3][4] Jamie Zawinski says he came up with the name "Mozilla" at a Netscape staff meeting.[5][6] A small group of Netscape employees were tasked with coordination of the new community.<p>
+<div style='float: left; width: 100%; height: 500px; margin: 0; padding: 0; border: none'> </div>
+<p> <b>Will be searching for this string:</b> Robocop </p>
+<p>Originally, Mozilla aimed to be a technology provider for companies, such as Netscape, who would commercialize their open source code.[7] When AOL (Netscape's parent company) drastically scaled back its involvement with Mozilla in July 2003, the Mozilla Foundation was launched as the legal steward of the project.[8] Soon after, Mozilla deprecated the Mozilla Suite in favor of creating independent applications for each function, primarily the Firefox web browser and the Thunderbird email client, and moved to supply them direct to the public.[9]<p>
+<div style='float: left; width: 100%; height: 200px; margin: 0; padding: 0; border: none'> </div>
+<p> <b>Will be searching for this string:</b> Robocop </p>
+<p>Recently, Mozilla's activities have expanded to include Firefox on mobile platforms (primarily Android),[10] a mobile OS called Firefox OS,[11] a web-based identity system called Mozilla Persona and a marketplace for HTML5 applications.[12]</p>
+<div style='float: left; width: 100%; height: 200px; margin: 0; padding: 0; border: none'> </div>
+<p> <b>Will be searching for this string:</b> Robocop </p>
+<p>In a report released in November of 2012, Mozilla reported that their total revenue for 2011 was $163 million, which was up 33% from $123 million in 2010. Mozilla noted that roughly 85% of their revenue comes from their contract with Google. [13]</p>
+</body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/roboextender/Makefile.in b/mobile/android/tests/browser/robocop/roboextender/Makefile.in
new file mode 100644
index 0000000000..07d7992acf
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/roboextender/Makefile.in
@@ -0,0 +1,9 @@
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+TEST_EXTENSIONS_DIR = $(DEPTH)/_tests/testing/mochitest/extensions
+
+tools::
+ -cp $(DEPTH)/mobile/android/tests/javaaddons/javaaddons-test.apk $(TEST_EXTENSIONS_DIR)/roboextender@mozilla.org/base
diff --git a/mobile/android/tests/browser/robocop/roboextender/base/robocop_home_banner.html b/mobile/android/tests/browser/robocop/roboextender/base/robocop_home_banner.html
new file mode 100644
index 0000000000..9a94566040
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/roboextender/base/robocop_home_banner.html
@@ -0,0 +1,37 @@
+<html>
+ <head>
+ <title>HomeBanner test page</title>
+ <meta name="viewport" content="initial-scale=1.0"/>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+Components.utils.import("resource://gre/modules/Messaging.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Home.jsm");
+
+const TEXT = "The quick brown fox jumps over the lazy dog.";
+
+function start() {
+ var test = location.hash.substring(1);
+ window[test]();
+}
+
+var messageId;
+
+function addMessage() {
+ messageId = Home.banner.add({
+ text: TEXT,
+ onshown: function() {
+ Messaging.sendRequest({ type: "TestHomeBanner:MessageShown" });
+ },
+ ondismiss: function() {
+ Messaging.sendRequest({ type: "TestHomeBanner:MessageDismissed" });
+ }
+ });
+ Messaging.sendRequest({ type: "TestHomeBanner:MessageAdded" });
+}
+
+ </script>
+ </head>
+ <body onload="start();">
+ </body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/roboextender/base/robocop_prompt_gridinput.html b/mobile/android/tests/browser/robocop/roboextender/base/robocop_prompt_gridinput.html
new file mode 100644
index 0000000000..733683c165
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/roboextender/base/robocop_prompt_gridinput.html
@@ -0,0 +1,51 @@
+<html>
+ <head>
+ <title>IconGrid test page</title>
+ <meta name="viewport" content="initial-scale=1.0"/>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript">
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Prompt.jsm");
+
+function start() {
+ var test = location.hash.substring(1);
+ window[test]();
+}
+
+function test1() {
+ var p = new Prompt({
+ title: "Prompt 1",
+ buttons: [
+ "OK"
+ ],
+ }).addIconGrid({
+ items: [
+ { iconUri: "drawable://alert_camera", name: "Icon 1", selected: true },
+ { iconUri: "drawable://alert_download", name: "Icon 2" },
+ { iconUri: "drawable://icon", name: "Icon 3" },
+ { iconUri: "drawable://icon", name: "Icon 4" },
+ { iconUri: "drawable://icon", name: "Icon 5" },
+ { iconUri: "drawable://icon", name: "Icon 6" },
+ { iconUri: "drawable://icon", name: "Icon 7" },
+ { iconUri: "drawable://icon", name: "Icon 8" },
+ { iconUri: "drawable://icon", name: "Icon 9" },
+ { iconUri: "drawable://icon", name: "Icon 10" },
+ { iconUri: "drawable://icon", name: "Icon 11" },
+ ]
+ });
+ p.show(function(data) {
+ sendResult(data.icongrid0 == 10, "Got result " + data.icongrid0);
+ });
+}
+
+function sendResult(pass, message) {
+ setTimeout(function() {
+ alert((pass ? "PASS " : "FAIL ") + message);
+ }, 1000);
+}
+ </script>
+ </head>
+ <body onload="start();">
+ </body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/roboextender/bootstrap.js b/mobile/android/tests/browser/robocop/roboextender/bootstrap.js
new file mode 100644
index 0000000000..e903aa3f60
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/roboextender/bootstrap.js
@@ -0,0 +1,65 @@
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+function loadIntoWindow(window) {}
+function unloadFromWindow(window) {}
+
+function _sendMessageToJava (aMsg) {
+ return Services.androidBridge.handleGeckoMessage(aMsg);
+};
+
+/*
+ bootstrap.js API
+*/
+var windowListener = {
+ onOpenWindow: function(aWindow) {
+ // Wait for the window to finish loading
+ let domWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
+ domWindow.addEventListener("load", function() {
+ domWindow.removeEventListener("load", arguments.callee, false);
+ if (domWindow) {
+ domWindow.addEventListener("scroll", function(e) {
+ let message = {
+ type: 'robocop:scroll',
+ y: XPCNativeWrapper.unwrap(e.target).documentElement.scrollTop,
+ height: XPCNativeWrapper.unwrap(e.target).documentElement.scrollHeight,
+ cheight: XPCNativeWrapper.unwrap(e.target).documentElement.clientHeight,
+ };
+ _sendMessageToJava(message);
+ });
+ }
+ }, false);
+ },
+ onCloseWindow: function(aWindow) { },
+ onWindowTitleChange: function(aWindow, aTitle) { }
+};
+
+function startup(aData, aReason) {
+ let wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
+
+ // Load into any new windows
+ wm.addListener(windowListener);
+ Services.obs.addObserver(function observe(aSubject, aTopic, aData) {
+ dump("Robocop:Quit received -- requesting quit");
+ let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
+ appStartup.quit(Ci.nsIAppStartup.eForceQuit);
+ }, "Robocop:Quit", false);
+}
+
+function shutdown(aData, aReason) {
+ // When the application is shutting down we normally don't have to clean up any UI changes
+ if (aReason == APP_SHUTDOWN) return;
+
+ let wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
+
+ // Stop watching for new windows
+ wm.removeListener(windowListener);
+}
+
+function install(aData, aReason) { }
+function uninstall(aData, aReason) { }
+
diff --git a/mobile/android/tests/browser/robocop/roboextender/chrome.manifest b/mobile/android/tests/browser/robocop/roboextender/chrome.manifest
new file mode 100644
index 0000000000..7467f91a6c
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/roboextender/chrome.manifest
@@ -0,0 +1 @@
+content roboextender base/ \ No newline at end of file
diff --git a/mobile/android/tests/browser/robocop/roboextender/install.rdf b/mobile/android/tests/browser/robocop/roboextender/install.rdf
new file mode 100644
index 0000000000..cbf66e884f
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/roboextender/install.rdf
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>roboextender@mozilla.org</em:id>
+ <em:type>2</em:type>
+ <em:name>Robocop Extender</em:name>
+ <em:version>1.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+ <em:creator>Joel Maher</em:creator>
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>10.0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
+
diff --git a/mobile/android/tests/browser/robocop/roboextender/moz.build b/mobile/android/tests/browser/robocop/roboextender/moz.build
new file mode 100644
index 0000000000..e2388a2b84
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/roboextender/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+TEST_HARNESS_FILES.testing.mochitest.extensions['roboextender@mozilla.org'] += [
+ 'base/**',
+ 'bootstrap.js',
+ 'chrome.manifest',
+ 'install.rdf',
+]
diff --git a/mobile/android/tests/browser/robocop/simple_redirect.sjs b/mobile/android/tests/browser/robocop/simple_redirect.sjs
new file mode 100644
index 0000000000..b6249cadff
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/simple_redirect.sjs
@@ -0,0 +1,5 @@
+function handleRequest(request, response)
+{
+ response.setStatusLine(request.httpVersion, 301, "Moved Permanently");
+ response.setHeader("Location", request.queryString, false);
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Actions.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Actions.java
new file mode 100644
index 0000000000..05e6bfa52a
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Actions.java
@@ -0,0 +1,126 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.database.Cursor;
+
+public interface Actions {
+
+ /** Special keys supported by sendSpecialKey() */
+ public enum SpecialKey {
+ DOWN,
+ UP,
+ LEFT,
+ RIGHT,
+ ENTER,
+ MENU,
+ DELETE,
+ }
+
+ public interface EventExpecter {
+ /** Blocks until the event has been received. Subsequent calls will return immediately. */
+ public void blockForEvent();
+ public void blockForEvent(long millis, boolean failOnTimeout);
+
+ /** Blocks until the event has been received and returns data associated with the event. */
+ public String blockForEventData();
+
+ /**
+ * Blocks until the event has been received, or until the timeout has been exceeded.
+ * Returns the data associated with the event, if applicable.
+ */
+ public String blockForEventDataWithTimeout(long millis);
+
+ /** Polls to see if the event has been received. Once this returns true, subsequent calls will also return true. */
+ public boolean eventReceived();
+
+ /** Stop listening for events. */
+ public void unregisterListener();
+ }
+
+ public interface RepeatedEventExpecter extends EventExpecter {
+ /** Blocks until at least one event has been received, and no events have been received in the last <code>millis</code> milliseconds. */
+ public void blockUntilClear(long millis);
+ }
+
+ /**
+ * Sends an event to Gecko.
+ *
+ * @param geckoEvent The geckoEvent JSONObject's type
+ */
+ void sendGeckoEvent(String geckoEvent, String data);
+
+ public interface PrefWaiter {
+ boolean isFinished();
+ void waitForFinish();
+ void waitForFinish(long timeoutMillis, boolean failOnTimeout);
+ }
+
+ public abstract static class PrefHandlerBase implements PrefsHelper.PrefHandler {
+ /* package */ Assert asserter;
+
+ @Override // PrefsHelper.PrefHandler
+ public void prefValue(String pref, boolean value) {
+ asserter.ok(false, "Unexpected pref callback", "");
+ }
+
+ @Override // PrefsHelper.PrefHandler
+ public void prefValue(String pref, int value) {
+ asserter.ok(false, "Unexpected pref callback", "");
+ }
+
+ @Override // PrefsHelper.PrefHandler
+ public void prefValue(String pref, String value) {
+ asserter.ok(false, "Unexpected pref callback", "");
+ }
+
+ @Override // PrefsHelper.PrefHandler
+ public void finish() {
+ }
+ }
+
+ PrefWaiter getPrefs(String[] prefNames, PrefHandlerBase handler);
+ void setPref(String pref, Object value, boolean flush);
+ PrefWaiter addPrefsObserver(String[] prefNames, PrefHandlerBase handler);
+ void removePrefsObserver(PrefWaiter handler);
+
+ /**
+ * Listens for a gecko event to be sent from the Gecko instance.
+ * The returned object can be used to test if the event has been
+ * received. Note that only one event is listened for.
+ *
+ * @param geckoEvent The geckoEvent JSONObject's type
+ */
+ RepeatedEventExpecter expectGeckoEvent(String geckoEvent);
+
+ /**
+ * Listens for a paint event. Note that calling expectPaint() will
+ * invalidate the event expecters returned from any previous calls
+ * to expectPaint(); calling any methods on those invalidated objects
+ * will result in undefined behaviour.
+ */
+ RepeatedEventExpecter expectPaint();
+
+ /**
+ * Send a string to the application
+ *
+ * @param keysToSend The string to send
+ */
+ void sendKeys(String keysToSend);
+
+ /**
+ * Send a special keycode to the element
+ *
+ * @param key The special key to send
+ */
+ void sendSpecialKey(SpecialKey key);
+ void sendKeyCode(int keyCode);
+
+ void drag(int startingX, int endingX, int startingY, int endingY);
+
+ /**
+ * Run a sql query on the specified database
+ */
+ public Cursor querySql(String dbPath, String sql);
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Assert.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Assert.java
new file mode 100644
index 0000000000..aa76dcf2b8
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Assert.java
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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 Assert {
+ void dumpLog(String message);
+ void dumpLog(String message, Throwable t);
+ void setLogFile(String filename);
+ void setTestName(String testName);
+ void endTest();
+
+ void ok(boolean condition, String name, String diag);
+ void is(Object actual, Object expected, String name);
+ void isnot(Object actual, Object notExpected, String name);
+ void todo(boolean condition, String name, String diag);
+ void todo_is(Object actual, Object expected, String name);
+ void todo_isnot(Object actual, Object notExpected, String name);
+ void info(String name, String message);
+
+ // robocop-specific asserts
+ void ispixel(int actual, int r, int g, int b, String name);
+ void isnotpixel(int actual, int r, int g, int b, String name);
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Driver.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Driver.java
new file mode 100644
index 0000000000..4c8373c5bf
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Driver.java
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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;
+
+public interface Driver {
+ /**
+ * Find the first Element using the given method.
+ *
+ * @param activity The activity the element belongs to
+ * @param id The resource id of the element
+ * @return The first matching element on the current context, or null if not found.
+ */
+ Element findElement(Activity activity, int id);
+
+ /**
+ * Sets up scroll handling so that data is received from the extension.
+ */
+ void setupScrollHandling();
+
+ int getPageHeight();
+ int getScrollHeight();
+ int getHeight();
+ int getGeckoTop();
+ int getGeckoLeft();
+ int getGeckoWidth();
+ int getGeckoHeight();
+
+ void startFrameRecording();
+ int stopFrameRecording();
+
+ void startCheckerboardRecording();
+ float stopCheckerboardRecording();
+
+ /**
+ * Get a copy of the painted content region.
+ * @return A 2-D array of pixels (indexed by y, then x). The pixels
+ * are in ARGB-8888 format.
+ */
+ PaintedSurface getPaintedSurface();
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Element.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Element.java
new file mode 100644
index 0000000000..97610ff32a
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Element.java
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+/**
+ * Element provides access to a specific UI view (android.view.View).
+ * See also Driver.findElement().
+ */
+public interface Element {
+
+ /** Click on the element's view. Returns true on success. */
+ boolean click();
+
+ /** Returns true if the element is currently displayed */
+ boolean isDisplayed();
+
+ /**
+ * Returns the text currently displayed on the element, or null
+ * if the text cannot be retrieved.
+ */
+ String getText();
+
+ /** Returns the view ID */
+ Integer getId();
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecInstrumentationTestRunner.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecInstrumentationTestRunner.java
new file mode 100644
index 0000000000..6bcb4e1027
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecInstrumentationTestRunner.java
@@ -0,0 +1,79 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.content.Context;
+import android.os.Bundle;
+import android.os.PowerManager;
+import android.test.InstrumentationTestRunner;
+import android.util.Log;
+
+import static android.os.PowerManager.ACQUIRE_CAUSES_WAKEUP;
+import static android.os.PowerManager.FULL_WAKE_LOCK;
+import static android.os.PowerManager.ON_AFTER_RELEASE;
+
+public class FennecInstrumentationTestRunner extends InstrumentationTestRunner {
+ private static Bundle sArguments;
+ private PowerManager.WakeLock wakeLock;
+ private KeyguardManager.KeyguardLock keyguardLock;
+
+ @Override
+ public void onCreate(Bundle arguments) {
+ sArguments = arguments;
+ if (sArguments == null) {
+ Log.e("Robocop", "FennecInstrumentationTestRunner.onCreate got null bundle");
+ }
+ super.onCreate(arguments);
+ }
+
+ // unfortunately we have to make this static because test classes that don't extend
+ // from ActivityInstrumentationTestCase2 can't get a reference to this class.
+ public static Bundle getFennecArguments() {
+ if (sArguments == null) {
+ Log.e("Robocop", "FennecInstrumentationTestCase.getFennecArguments returns null bundle");
+ }
+ return sArguments;
+ }
+
+ @Override
+ public void onStart() {
+ final Context context = getContext(); // The Robocop package itself has DISABLE_KEYGUARD and WAKE_LOCK.
+ if (context != null) {
+ try {
+ String name = FennecInstrumentationTestRunner.class.getSimpleName();
+ // Unlock the device so that the tests can input keystrokes.
+ final KeyguardManager keyguard = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
+ // Deprecated in favour of window flags, which aren't appropriate here.
+ keyguardLock = keyguard.newKeyguardLock(name);
+ keyguardLock.disableKeyguard();
+
+ // Wake up the screen.
+ final PowerManager power = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ wakeLock = power.newWakeLock(FULL_WAKE_LOCK | ACQUIRE_CAUSES_WAKEUP | ON_AFTER_RELEASE, name);
+ wakeLock.acquire();
+ } catch (SecurityException e) {
+ Log.w("GeckoInstTestRunner", "Got SecurityException: not disabling keyguard and not taking wakelock.");
+ }
+ } else {
+ Log.w("GeckoInstTestRunner", "Application target context is null: not disabling keyguard and not taking wakelock.");
+ }
+
+ super.onStart();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ if (wakeLock != null) {
+ wakeLock.release();
+ }
+ if (keyguardLock != null) {
+ // Deprecated in favour of window flags, which aren't appropriate here.
+ keyguardLock.reenableKeyguard();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecMochitestAssert.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecMochitestAssert.java
new file mode 100644
index 0000000000..cb7c3c4640
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecMochitestAssert.java
@@ -0,0 +1,254 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.PrintWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+
+import android.os.SystemClock;
+
+public class FennecMochitestAssert implements Assert {
+ // Internal state variables to make logging match up with existing mochitests
+ private int mPassed = 0;
+ private int mFailed = 0;
+ private int mTodo = 0;
+
+ // Used to write the first line of the test file
+ private boolean mLogStarted = false;
+
+ // Used to write the test-start/test-end log lines
+ private String mLogTestName = "";
+
+ // Measure the time it takes to run test case
+ private long mStartTime = 0;
+
+ // Structured logger
+ private StructuredLogger mLogger;
+
+ /** Write information to a logfile and logcat */
+ public void dumpLog(String message) {
+ mLogger.info(message);
+ }
+
+ public void dumpLog(String message, Throwable t) {
+ Writer sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ t.printStackTrace(pw);
+ mLogger.error(message + " - " + sw.toString());
+ }
+
+ /** Write information to a logfile and logcat */
+ static class DumpLogCallback implements StructuredLogger.LoggerCallback {
+ public void call(String output) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.INFO, output);
+ }
+ }
+
+
+ public FennecMochitestAssert() {
+ mLogger = new StructuredLogger("robocop", new DumpLogCallback());
+ }
+
+ /** Set the filename used for dumpLog. */
+ public void setLogFile(String filename) {
+ FennecNativeDriver.setLogFile(filename);
+
+ String message;
+ if (!mLogStarted) {
+ mLogger.info("SimpleTest START");
+ mLogStarted = true;
+ }
+
+ if (mLogTestName != "") {
+ long diff = SystemClock.uptimeMillis() - mStartTime;
+ mLogger.testEnd(mLogTestName, "OK", "finished in " + diff + "ms");
+ }
+ }
+
+ public void setTestName(String testName) {
+ String[] nameParts = testName.split("\\.");
+ mLogTestName = nameParts[nameParts.length - 1];
+ mStartTime = SystemClock.uptimeMillis();
+
+ mLogger.testStart(mLogTestName);
+ }
+
+ class testInfo {
+ public boolean mResult;
+ public String mName;
+ public String mDiag;
+ public boolean mTodo;
+ public boolean mInfo;
+ public testInfo(boolean r, String n, String d, boolean t, boolean i) {
+ mResult = r;
+ mName = n;
+ mDiag = d;
+ mTodo = t;
+ mInfo = i;
+ }
+
+ }
+
+ /** Used to log a subtest's result.
+ * test represents the subtest (an assertion).
+ * passStatus and passExpected are the actual status and the expected status if the assertion is true.
+ * failStatus and failExpected are the actual status and the expected status otherwise.
+ */
+ private void _logMochitestResult(testInfo test, String passStatus, String passExpected, String failStatus, String failExpected) {
+ boolean isError = true;
+ if (test.mResult || test.mTodo) {
+ isError = false;
+ }
+ if (test.mResult)
+ {
+ mLogger.testStatus(mLogTestName, test.mName, passStatus, passExpected, test.mDiag);
+ } else {
+ mLogger.testStatus(mLogTestName, test.mName, failStatus, failExpected, test.mDiag);
+ }
+
+ if (test.mInfo) {
+ // do not count TEST-INFO messages
+ } else if (test.mTodo) {
+ mTodo++;
+ } else if (isError) {
+ mFailed++;
+ } else {
+ mPassed++;
+ }
+ if (isError) {
+ String message = "TEST-UNEXPECTED-" + failStatus + " | " + mLogTestName + " | "
+ + test.mName + " - " + test.mDiag;
+ junit.framework.Assert.fail(message);
+ }
+ }
+
+ public void endTest() {
+ String message;
+
+ if (mLogTestName != "") {
+ long diff = SystemClock.uptimeMillis() - mStartTime;
+ mLogger.testEnd(mLogTestName, "OK", "finished in " + diff + "ms");
+ }
+
+ mLogger.info("TEST-START | Shutdown");
+ mLogger.info("Passed: " + Integer.toString(mPassed));
+ mLogger.info("Failed: " + Integer.toString(mFailed));
+ mLogger.info("Todo: " + Integer.toString(mTodo));
+ mLogger.info("SimpleTest FINISHED");
+ }
+
+ public void ok(boolean condition, String name, String diag) {
+ testInfo test = new testInfo(condition, name, diag, false, false);
+ _logMochitestResult(test, "PASS", "PASS", "FAIL", "PASS");
+ }
+
+ public void is(Object actual, Object expected, String name) {
+ boolean pass = checkObjectsEqual(actual, expected);
+ ok(pass, name, getEqualString(actual, expected, pass));
+ }
+
+ public void isnot(Object actual, Object notExpected, String name) {
+ boolean pass = checkObjectsNotEqual(actual, notExpected);
+ ok(pass, name, getNotEqualString(actual, notExpected, pass));
+ }
+
+ public void ispixel(int actual, int r, int g, int b, String name) {
+ int aAlpha = ((actual >> 24) & 0xFF);
+ int aR = ((actual >> 16) & 0xFF);
+ int aG = ((actual >> 8) & 0xFF);
+ int aB = (actual & 0xFF);
+ boolean pass = checkPixel(actual, r, g, b);
+ ok(pass, name, "Color rgba(" + aR + "," + aG + "," + aB + "," + aAlpha + ")" + (pass ? " " : " not") + " close enough to expected rgb(" + r + "," + g + "," + b + ")");
+ }
+
+ public void isnotpixel(int actual, int r, int g, int b, String name) {
+ int aAlpha = ((actual >> 24) & 0xFF);
+ int aR = ((actual >> 16) & 0xFF);
+ int aG = ((actual >> 8) & 0xFF);
+ int aB = (actual & 0xFF);
+ boolean pass = checkPixel(actual, r, g, b);
+ ok(!pass, name, "Color rgba(" + aR + "," + aG + "," + aB + "," + aAlpha + ")" + (!pass ? " is" : " is not") + " different enough from rgb(" + r + "," + g + "," + b + ")");
+ }
+
+ private boolean checkPixel(int actual, int r, int g, int b) {
+ // When we read GL pixels the GPU has already processed them and they
+ // are usually off by a little bit. For example a CSS-color pixel of color #64FFF5
+ // was turned into #63FFF7 when it came out of glReadPixels. So in order to compare
+ // against the expected value, we use a little fuzz factor. For the alpha we just
+ // make sure it is always 0xFF. There is also bug 691354 which crops up every so
+ // often just to make our lives difficult. However the individual color components
+ // should never be off by more than 8.
+ int aAlpha = ((actual >> 24) & 0xFF);
+ int aR = ((actual >> 16) & 0xFF);
+ int aG = ((actual >> 8) & 0xFF);
+ int aB = (actual & 0xFF);
+ boolean pass = (aAlpha == 0xFF) /* alpha */
+ && (Math.abs(aR - r) <= 8) /* red */
+ && (Math.abs(aG - g) <= 8) /* green */
+ && (Math.abs(aB - b) <= 8); /* blue */
+ if (pass) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public void todo(boolean condition, String name, String diag) {
+ testInfo test = new testInfo(condition, name, diag, true, false);
+ _logMochitestResult(test, "PASS", "FAIL", "FAIL", "FAIL");
+ }
+
+ public void todo_is(Object actual, Object expected, String name) {
+ boolean pass = checkObjectsEqual(actual, expected);
+ todo(pass, name, getEqualString(actual, expected, pass));
+ }
+
+ public void todo_isnot(Object actual, Object notExpected, String name) {
+ boolean pass = checkObjectsNotEqual(actual, notExpected);
+ todo(pass, name, getNotEqualString(actual, notExpected, pass));
+ }
+
+ private boolean checkObjectsEqual(Object a, Object b) {
+ if (a == null || b == null) {
+ if (a == null && b == null) {
+ return true;
+ }
+ return false;
+ } else {
+ return a.equals(b);
+ }
+ }
+
+ private String getEqualString(Object a, Object b, boolean pass) {
+ if (pass) {
+ return a + " should equal " + b;
+ }
+ return "got " + a + ", expected " + b;
+ }
+
+ private boolean checkObjectsNotEqual(Object a, Object b) {
+ if (a == null || b == null) {
+ if ((a == null && b != null) || (a != null && b == null)) {
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return !a.equals(b);
+ }
+ }
+
+ private String getNotEqualString(Object a, Object b, boolean pass) {
+ if(pass) {
+ return a + " should not equal " + b;
+ }
+ return "didn't expect " + a + ", but got it";
+ }
+
+ public void info(String name, String message) {
+ mLogger.info(name + " | " + message);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeActions.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeActions.java
new file mode 100644
index 0000000000..7faccdf43b
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeActions.java
@@ -0,0 +1,482 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import org.json.JSONObject;
+import org.mozilla.gecko.FennecNativeDriver.LogLevel;
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.gfx.LayerView.DrawListener;
+import org.mozilla.gecko.mozglue.GeckoLoader;
+import org.mozilla.gecko.sqlite.SQLiteBridge;
+import org.mozilla.gecko.util.GeckoEventListener;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.database.Cursor;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.view.KeyEvent;
+
+import com.robotium.solo.Solo;
+
+public class FennecNativeActions implements Actions {
+ private static final String LOGTAG = "FennecNativeActions";
+
+ private Solo mSolo;
+ private Instrumentation mInstr;
+ private Assert mAsserter;
+
+ public FennecNativeActions(Activity activity, Solo robocop, Instrumentation instrumentation, Assert asserter) {
+ mSolo = robocop;
+ mInstr = instrumentation;
+ mAsserter = asserter;
+
+ GeckoLoader.loadSQLiteLibs(activity, activity.getApplication().getPackageResourcePath());
+ }
+
+ class GeckoEventExpecter implements RepeatedEventExpecter {
+ private static final int MAX_WAIT_MS = 180000;
+
+ private volatile boolean mIsRegistered;
+
+ private final String mGeckoEvent;
+ private final GeckoEventListener mListener;
+
+ private volatile boolean mEventEverReceived;
+ private String mEventData;
+ private BlockingQueue<String> mEventDataQueue;
+
+ GeckoEventExpecter(final String geckoEvent) {
+ if (TextUtils.isEmpty(geckoEvent)) {
+ throw new IllegalArgumentException("geckoEvent must not be empty");
+ }
+
+ mGeckoEvent = geckoEvent;
+ mEventDataQueue = new LinkedBlockingQueue<String>();
+
+ final GeckoEventExpecter expecter = this;
+ mListener = new GeckoEventListener() {
+ @Override
+ public void handleMessage(final String event, final JSONObject message) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
+ "handleMessage called for: " + event + "; expecting: " + mGeckoEvent);
+ mAsserter.is(event, mGeckoEvent, "Given message occurred for registered event: " + message);
+
+ expecter.notifyOfEvent(message);
+ }
+ };
+
+ EventDispatcher.getInstance().registerGeckoThreadListener(mListener, mGeckoEvent);
+ mIsRegistered = true;
+ }
+
+ public void blockForEvent() {
+ blockForEvent(MAX_WAIT_MS, true);
+ }
+
+ public void blockForEvent(long millis, boolean failOnTimeout) {
+ if (!mIsRegistered) {
+ throw new IllegalStateException("listener not registered");
+ }
+
+ try {
+ mEventData = mEventDataQueue.poll(millis, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException ie) {
+ FennecNativeDriver.log(LogLevel.ERROR, ie);
+ }
+ if (mEventData == null) {
+ if (failOnTimeout) {
+ FennecNativeDriver.logAllStackTraces(FennecNativeDriver.LogLevel.ERROR);
+ mAsserter.ok(false, "GeckoEventExpecter",
+ "blockForEvent timeout: "+mGeckoEvent);
+ } else {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
+ "blockForEvent timeout: "+mGeckoEvent);
+ }
+ } else {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
+ "unblocked on expecter for " + mGeckoEvent);
+ }
+ }
+
+ public void blockUntilClear(long millis) {
+ if (!mIsRegistered) {
+ throw new IllegalStateException("listener not registered");
+ }
+ if (millis <= 0) {
+ throw new IllegalArgumentException("millis must be > 0");
+ }
+
+ // wait for at least one event
+ try {
+ mEventData = mEventDataQueue.poll(MAX_WAIT_MS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException ie) {
+ FennecNativeDriver.log(LogLevel.ERROR, ie);
+ }
+ if (mEventData == null) {
+ FennecNativeDriver.logAllStackTraces(FennecNativeDriver.LogLevel.ERROR);
+ mAsserter.ok(false, "GeckoEventExpecter", "blockUntilClear timeout");
+ return;
+ }
+ // now wait for a period of millis where we don't get an event
+ while (true) {
+ try {
+ mEventData = mEventDataQueue.poll(millis, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException ie) {
+ FennecNativeDriver.log(LogLevel.INFO, ie);
+ }
+ if (mEventData == null) {
+ // success
+ break;
+ }
+ }
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
+ "unblocked on expecter for " + mGeckoEvent);
+ }
+
+ public String blockForEventData() {
+ blockForEvent();
+ return mEventData;
+ }
+
+ public String blockForEventDataWithTimeout(long millis) {
+ blockForEvent(millis, false);
+ return mEventData;
+ }
+
+ public void unregisterListener() {
+ if (!mIsRegistered) {
+ throw new IllegalStateException("listener not registered");
+ }
+
+ FennecNativeDriver.log(LogLevel.INFO,
+ "EventExpecter: no longer listening for " + mGeckoEvent);
+
+ EventDispatcher.getInstance().unregisterGeckoThreadListener(mListener, mGeckoEvent);
+ mIsRegistered = false;
+ }
+
+ public boolean eventReceived() {
+ return mEventEverReceived;
+ }
+
+ void notifyOfEvent(final JSONObject message) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
+ "received event " + mGeckoEvent);
+
+ mEventEverReceived = true;
+
+ try {
+ mEventDataQueue.put(message.toString());
+ } catch (InterruptedException e) {
+ FennecNativeDriver.log(LogLevel.ERROR,
+ "EventExpecter dropped event: " + message.toString(), e);
+ }
+ }
+ }
+
+ public RepeatedEventExpecter expectGeckoEvent(final String geckoEvent) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG, "waiting for " + geckoEvent);
+ return new GeckoEventExpecter(geckoEvent);
+ }
+
+ public void sendGeckoEvent(final String geckoEvent, final String data) {
+ GeckoAppShell.notifyObservers(geckoEvent, data);
+ }
+
+ public static final class PrefProxy implements PrefsHelper.PrefHandler, PrefWaiter {
+ public static final int MAX_WAIT_MS = 180000;
+
+ /* package */ final PrefHandlerBase target;
+ private final String[] expectedPrefs;
+ private final ArrayList<String> seenPrefs = new ArrayList<>();
+ private boolean finished = false;
+
+ /* package */ PrefProxy(PrefHandlerBase target, String[] expectedPrefs, Assert asserter) {
+ this.target = target;
+ this.expectedPrefs = expectedPrefs;
+ target.asserter = asserter;
+ }
+
+ @Override // PrefsHelper.PrefHandler
+ public void prefValue(String pref, boolean value) {
+ target.prefValue(pref, value);
+ seenPrefs.add(pref);
+ }
+
+ @Override // PrefsHelper.PrefHandler
+ public void prefValue(String pref, int value) {
+ target.prefValue(pref, value);
+ seenPrefs.add(pref);
+ }
+
+ @Override // PrefsHelper.PrefHandler
+ public void prefValue(String pref, String value) {
+ target.prefValue(pref, value);
+ seenPrefs.add(pref);
+ }
+
+ @Override // PrefsHelper.PrefHandler
+ public synchronized void finish() {
+ target.finish();
+
+ for (String pref : expectedPrefs) {
+ target.asserter.ok(seenPrefs.remove(pref), "Checking pref was seen", pref);
+ }
+ target.asserter.ok(seenPrefs.isEmpty(), "Checking unexpected prefs",
+ TextUtils.join(", ", seenPrefs));
+
+ finished = true;
+ this.notifyAll();
+ }
+
+ @Override // PrefWaiter
+ public synchronized boolean isFinished() {
+ return finished;
+ }
+
+ @Override // PrefWaiter
+ public void waitForFinish() {
+ waitForFinish(MAX_WAIT_MS, /* failOnTimeout */ true);
+ }
+
+ @Override // PrefWaiter
+ public synchronized void waitForFinish(long timeoutMillis, boolean failOnTimeout) {
+ final long startTime = System.nanoTime();
+ while (!finished) {
+ if (System.nanoTime() - startTime
+ >= timeoutMillis * 1e6 /* ns per ms */) {
+ final String prefsLog = "expected " +
+ TextUtils.join(", ", expectedPrefs) + "; got " +
+ TextUtils.join(", ", seenPrefs.toArray()) + ".";
+ if (failOnTimeout) {
+ FennecNativeDriver.logAllStackTraces(FennecNativeDriver.LogLevel.ERROR);
+ target.asserter.ok(false, "Timeout waiting for pref", prefsLog);
+ } else {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
+ "Pref timeout (" + prefsLog + ")");
+ }
+ break;
+ }
+ try {
+ this.wait(1000); // Wait for 1 second at a time.
+ } catch (final InterruptedException e) {
+ // Attempt waiting again.
+ }
+ }
+ finished = false;
+ }
+ }
+
+ @Override // Actions
+ public PrefWaiter getPrefs(String[] prefNames, PrefHandlerBase handler) {
+ final PrefProxy proxy = new PrefProxy(handler, prefNames, mAsserter);
+ PrefsHelper.getPrefs(prefNames, proxy);
+ return proxy;
+ }
+
+ @Override // Actions
+ public void setPref(String pref, Object value, boolean flush) {
+ PrefsHelper.setPref(pref, value, flush);
+ }
+
+ @Override // Actions
+ public PrefWaiter addPrefsObserver(String[] prefNames, PrefHandlerBase handler) {
+ final PrefProxy proxy = new PrefProxy(handler, prefNames, mAsserter);
+ PrefsHelper.addObserver(prefNames, proxy);
+ return proxy;
+ }
+
+ @Override // Actions
+ public void removePrefsObserver(PrefWaiter proxy) {
+ PrefsHelper.removeObserver((PrefProxy) proxy);
+ }
+
+ class PaintExpecter implements RepeatedEventExpecter {
+ private static final int MAX_WAIT_MS = 90000;
+
+ private boolean mPaintDone;
+ private boolean mListening;
+
+ private final LayerView mLayerView;
+ private final DrawListener mDrawListener;
+
+ PaintExpecter() {
+ final PaintExpecter expecter = this;
+ mLayerView = GeckoAppShell.getLayerView();
+ mDrawListener = new DrawListener() {
+ @Override
+ public void drawFinished() {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
+ "Received drawFinished notification");
+ expecter.notifyOfEvent();
+ }
+ };
+ mLayerView.addDrawListener(mDrawListener);
+ mListening = true;
+ }
+
+ private synchronized void notifyOfEvent() {
+ mPaintDone = true;
+ this.notifyAll();
+ }
+
+ public synchronized void blockForEvent(long millis, boolean failOnTimeout) {
+ if (!mListening) {
+ throw new IllegalStateException("draw listener not registered");
+ }
+ long startTime = SystemClock.uptimeMillis();
+ long endTime = 0;
+ while (!mPaintDone) {
+ try {
+ this.wait(millis);
+ } catch (InterruptedException ie) {
+ FennecNativeDriver.log(LogLevel.ERROR, ie);
+ break;
+ }
+ endTime = SystemClock.uptimeMillis();
+ if (!mPaintDone && (endTime - startTime >= millis)) {
+ if (failOnTimeout) {
+ FennecNativeDriver.logAllStackTraces(FennecNativeDriver.LogLevel.ERROR);
+ mAsserter.ok(false, "PaintExpecter", "blockForEvent timeout");
+ }
+ return;
+ }
+ }
+ }
+
+ public synchronized void blockForEvent() {
+ blockForEvent(MAX_WAIT_MS, true);
+ }
+
+ public synchronized String blockForEventData() {
+ blockForEvent();
+ return null;
+ }
+
+ public synchronized String blockForEventDataWithTimeout(long millis) {
+ blockForEvent(millis, false);
+ return null;
+ }
+
+ public synchronized boolean eventReceived() {
+ return mPaintDone;
+ }
+
+ public synchronized void blockUntilClear(long millis) {
+ if (!mListening) {
+ throw new IllegalStateException("draw listener not registered");
+ }
+ if (millis <= 0) {
+ throw new IllegalArgumentException("millis must be > 0");
+ }
+ // wait for at least one event
+ long startTime = SystemClock.uptimeMillis();
+ long endTime = 0;
+ while (!mPaintDone) {
+ try {
+ this.wait(MAX_WAIT_MS);
+ } catch (InterruptedException ie) {
+ FennecNativeDriver.log(LogLevel.ERROR, ie);
+ break;
+ }
+ endTime = SystemClock.uptimeMillis();
+ if (!mPaintDone && (endTime - startTime >= MAX_WAIT_MS)) {
+ FennecNativeDriver.logAllStackTraces(FennecNativeDriver.LogLevel.ERROR);
+ mAsserter.ok(false, "PaintExpecter", "blockUtilClear timeout");
+ return;
+ }
+ }
+ // now wait for a period of millis where we don't get an event
+ startTime = SystemClock.uptimeMillis();
+ while (true) {
+ try {
+ this.wait(millis);
+ } catch (InterruptedException ie) {
+ FennecNativeDriver.log(LogLevel.ERROR, ie);
+ break;
+ }
+ endTime = SystemClock.uptimeMillis();
+ if (endTime - startTime >= millis) {
+ // success
+ break;
+ }
+
+ // we got a notify() before we could wait long enough, so we need to start over
+ // Note, moving the goal post might have us race against a "drawFinished" flood
+ startTime = endTime;
+ }
+ }
+
+ public synchronized void unregisterListener() {
+ if (!mListening) {
+ throw new IllegalStateException("listener not registered");
+ }
+
+ FennecNativeDriver.log(LogLevel.INFO,
+ "PaintExpecter: no longer listening for events");
+ mLayerView.removeDrawListener(mDrawListener);
+ mListening = false;
+ }
+ }
+
+ public RepeatedEventExpecter expectPaint() {
+ return new PaintExpecter();
+ }
+
+ public void sendSpecialKey(SpecialKey button) {
+ switch(button) {
+ case DOWN:
+ sendKeyCode(Solo.DOWN);
+ break;
+ case UP:
+ sendKeyCode(Solo.UP);
+ break;
+ case LEFT:
+ sendKeyCode(Solo.LEFT);
+ break;
+ case RIGHT:
+ sendKeyCode(Solo.RIGHT);
+ break;
+ case ENTER:
+ sendKeyCode(Solo.ENTER);
+ break;
+ case MENU:
+ sendKeyCode(Solo.MENU);
+ break;
+ case DELETE:
+ sendKeyCode(Solo.DELETE);
+ break;
+ default:
+ mAsserter.ok(false, "sendSpecialKey", "Unknown SpecialKey " + button);
+ break;
+ }
+ }
+
+ public void sendKeyCode(int keyCode) {
+ if (keyCode <= 0 || keyCode > KeyEvent.getMaxKeyCode()) {
+ mAsserter.ok(false, "sendKeyCode", "Unknown keyCode " + keyCode);
+ }
+ mSolo.sendKey(keyCode);
+ }
+
+ @Override
+ public void sendKeys(String input) {
+ mInstr.sendStringSync(input);
+ }
+
+ public void drag(int startingX, int endingX, int startingY, int endingY) {
+ mSolo.drag(startingX, endingX, startingY, endingY, 10);
+ }
+
+ public Cursor querySql(final String dbPath, final String sql) {
+ return new SQLiteBridge(dbPath).rawQuery(sql, null);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeDriver.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeDriver.java
new file mode 100644
index 0000000000..3931b7e206
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeDriver.java
@@ -0,0 +1,392 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.IntBuffer;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.gfx.PanningPerfAPI;
+import org.mozilla.gecko.util.GeckoEventListener;
+
+import android.app.Activity;
+import android.util.Log;
+import android.view.View;
+
+import com.robotium.solo.Solo;
+
+public class FennecNativeDriver implements Driver {
+ private static final int FRAME_TIME_THRESHOLD = 25; // allow 25ms per frame (40fps)
+
+ private final Activity mActivity;
+ private final Solo mSolo;
+ private final String mRootPath;
+
+ private static String mLogFile;
+ private static LogLevel mLogLevel = LogLevel.INFO;
+
+ public enum LogLevel {
+ DEBUG(1),
+ INFO(2),
+ WARN(3),
+ ERROR(4);
+
+ private final int mValue;
+ LogLevel(int value) {
+ mValue = value;
+ }
+ public boolean isEnabled(LogLevel configuredLevel) {
+ return mValue >= configuredLevel.getValue();
+ }
+ private int getValue() {
+ return mValue;
+ }
+ }
+
+ public FennecNativeDriver(Activity activity, Solo robocop, String rootPath) {
+ mActivity = activity;
+ mSolo = robocop;
+ mRootPath = rootPath;
+ }
+
+ //Information on the location of the Gecko Frame.
+ private boolean mGeckoInfo = false;
+ private int mGeckoTop = 100;
+ private int mGeckoLeft = 0;
+ private int mGeckoHeight= 700;
+ private int mGeckoWidth = 1024;
+
+ private void getGeckoInfo() {
+ View geckoLayout = mActivity.findViewById(R.id.gecko_layout);
+ if (geckoLayout != null) {
+ int[] pos = new int[2];
+ geckoLayout.getLocationOnScreen(pos);
+ mGeckoTop = pos[1];
+ mGeckoLeft = pos[0];
+ mGeckoWidth = geckoLayout.getWidth();
+ mGeckoHeight = geckoLayout.getHeight();
+ mGeckoInfo = true;
+ } else {
+ throw new RoboCopException("Unable to find view gecko_layout");
+ }
+ }
+
+ @Override
+ public int getGeckoTop() {
+ if (!mGeckoInfo) {
+ getGeckoInfo();
+ }
+ return mGeckoTop;
+ }
+
+ @Override
+ public int getGeckoLeft() {
+ if (!mGeckoInfo) {
+ getGeckoInfo();
+ }
+ return mGeckoLeft;
+ }
+
+ @Override
+ public int getGeckoHeight() {
+ if (!mGeckoInfo) {
+ getGeckoInfo();
+ }
+ return mGeckoHeight;
+ }
+
+ @Override
+ public int getGeckoWidth() {
+ if (!mGeckoInfo) {
+ getGeckoInfo();
+ }
+ return mGeckoWidth;
+ }
+
+ /** Find the element with given id.
+ *
+ * @return An Element representing the view, or null if the view is not found.
+ */
+ @Override
+ public Element findElement(Activity activity, int id) {
+ return new FennecNativeElement(id, activity);
+ }
+
+ @Override
+ public void startFrameRecording() {
+ PanningPerfAPI.startFrameTimeRecording();
+ }
+
+ @Override
+ public int stopFrameRecording() {
+ final List<Long> frames = PanningPerfAPI.stopFrameTimeRecording();
+ int badness = 0;
+ for (int i = 1; i < frames.size(); i++) {
+ long frameTime = frames.get(i) - frames.get(i - 1);
+ int delay = (int)(frameTime - FRAME_TIME_THRESHOLD);
+ // for each frame we miss, add the square of the delay. This
+ // makes large delays much worse than small delays.
+ if (delay > 0) {
+ badness += delay * delay;
+ }
+ }
+
+ // Don't do any averaging of the numbers because really we want to
+ // know how bad the jank was at its worst
+ return badness;
+ }
+
+ @Override
+ public void startCheckerboardRecording() {
+ PanningPerfAPI.startCheckerboardRecording();
+ }
+
+ @Override
+ public float stopCheckerboardRecording() {
+ final List<Float> checkerboard = PanningPerfAPI.stopCheckerboardRecording();
+ float total = 0;
+ for (float val : checkerboard) {
+ total += val;
+ }
+ return total * 100.0f;
+ }
+
+ private LayerView getSurfaceView() {
+ final LayerView layerView = mSolo.getView(LayerView.class, 0);
+
+ if (layerView == null) {
+ log(LogLevel.WARN, "getSurfaceView could not find LayerView");
+ for (final View v : mSolo.getViews()) {
+ log(LogLevel.WARN, " View: " + v);
+ }
+ }
+ return layerView;
+ }
+
+ @Override
+ public PaintedSurface getPaintedSurface() {
+ final LayerView view = getSurfaceView();
+ if (view == null) {
+ return null;
+ }
+
+ final IntBuffer pixelBuffer = view.getPixels();
+
+ // now we need to (1) flip the image, because GL likes to do things up-side-down,
+ // and (2) rearrange the bits from AGBR-8888 to ARGB-8888.
+ int w = view.getWidth();
+ int h = view.getHeight();
+ pixelBuffer.position(0);
+ String mapFile = mRootPath + "/pixels.map";
+
+ FileOutputStream fos = null;
+ BufferedOutputStream bos = null;
+ DataOutputStream dos = null;
+ try {
+ fos = new FileOutputStream(mapFile);
+ bos = new BufferedOutputStream(fos);
+ dos = new DataOutputStream(bos);
+
+ for (int y = h - 1; y >= 0; y--) {
+ for (int x = 0; x < w; x++) {
+ int agbr = pixelBuffer.get();
+ dos.writeInt((agbr & 0xFF00FF00) | ((agbr >> 16) & 0x000000FF) | ((agbr << 16) & 0x00FF0000));
+ }
+ }
+ } catch (IOException e) {
+ throw new RoboCopException("exception with pixel writer on file: " + mapFile);
+ } finally {
+ try {
+ if (dos != null) {
+ dos.flush();
+ dos.close();
+ }
+ // closing dos automatically closes bos
+ if (fos != null) {
+ fos.flush();
+ fos.close();
+ }
+ } catch (IOException e) {
+ log(LogLevel.ERROR, e);
+ throw new RoboCopException("exception closing pixel writer on file: " + mapFile);
+ }
+ }
+ return new PaintedSurface(mapFile, w, h);
+ }
+
+ public int mHeight=0;
+ public int mScrollHeight=0;
+ public int mPageHeight=10;
+
+ @Override
+ public int getScrollHeight() {
+ return mScrollHeight;
+ }
+ @Override
+ public int getPageHeight() {
+ return mPageHeight;
+ }
+ @Override
+ public int getHeight() {
+ return mHeight;
+ }
+
+ @Override
+ public void setupScrollHandling() {
+ GeckoApp.getEventDispatcher().registerGeckoThreadListener(new GeckoEventListener() {
+ @Override
+ public void handleMessage(final String event, final JSONObject message) {
+ try {
+ mScrollHeight = message.getInt("y");
+ mHeight = message.getInt("cheight");
+ // We don't want a height of 0. That means it's a bad response.
+ if (mHeight > 0) {
+ mPageHeight = message.getInt("height");
+ }
+ } catch (JSONException e) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.WARN,
+ "WARNING: ScrollReceived, but message does not contain " +
+ "expected fields: " + e);
+ }
+ }
+ }, "robocop:scroll");
+ }
+
+ /**
+ * Takes a filename, loads the file, and returns a string version of the entire file.
+ */
+ public static String getFile(String filename)
+ {
+ StringBuilder text = new StringBuilder();
+
+ BufferedReader br = null;
+ try {
+ br = new BufferedReader(new FileReader(filename));
+ String line;
+
+ while ((line = br.readLine()) != null) {
+ text.append(line);
+ text.append('\n');
+ }
+ } catch (IOException e) {
+ log(LogLevel.ERROR, e);
+ } finally {
+ try {
+ if (br != null) {
+ br.close();
+ }
+ } catch (IOException e) {
+ }
+ }
+ return text.toString();
+ }
+
+ /**
+ * Takes a string of "key=value" pairs split by \n and creates a hash table.
+ */
+ public static Map<String, String> convertTextToTable(String data)
+ {
+ HashMap<String, String> retVal = new HashMap<String, String>();
+
+ String[] lines = data.split("\n");
+ for (int i = 0; i < lines.length; i++) {
+ String[] parts = lines[i].split("=", 2);
+ retVal.put(parts[0].trim(), parts[1].trim());
+ }
+ return retVal;
+ }
+
+ public static void logAllStackTraces(LogLevel level) {
+ StringBuffer sb = new StringBuffer();
+ sb.append("Dumping ALL the threads!\n");
+ Map<Thread, StackTraceElement[]> allStacks = Thread.getAllStackTraces();
+ for (Thread t : allStacks.keySet()) {
+ sb.append(t.toString()).append('\n');
+ for (StackTraceElement ste : allStacks.get(t)) {
+ sb.append(ste.toString()).append('\n');
+ }
+ sb.append('\n');
+ }
+ log(level, sb.toString());
+ }
+
+ /**
+ * Set the filename used for logging. If the file already exists, delete it
+ * as a safe-guard against accidentally appending to an old log file.
+ */
+ public static void setLogFile(String filename) {
+ mLogFile = filename;
+ File file = new File(mLogFile);
+ if (file.exists()) {
+ file.delete();
+ }
+ }
+
+ public static void setLogLevel(LogLevel level) {
+ mLogLevel = level;
+ }
+
+ public static void log(LogLevel level, String message) {
+ log(level, message, null);
+ }
+
+ public static void log(LogLevel level, Throwable t) {
+ log(level, null, t);
+ }
+
+ public static void log(LogLevel level, String message, Throwable t) {
+ if (mLogFile == null) {
+ throw new RuntimeException("No log file specified!");
+ }
+
+ if (level.isEnabled(mLogLevel)) {
+ PrintWriter pw = null;
+ try {
+ pw = new PrintWriter(new FileWriter(mLogFile, true));
+ if (message != null) {
+ pw.println(message);
+ }
+ if (t != null) {
+ t.printStackTrace(pw);
+ }
+ } catch (IOException ioe) {
+ Log.e("Robocop", "exception with file writer on: " + mLogFile);
+ } finally {
+ if (pw != null) {
+ pw.close();
+ }
+ }
+
+ // PrintWriter doesn't throw IOE but sets an error flag instead,
+ // so check for that
+ if (pw != null && pw.checkError()) {
+ Log.e("Robocop", "exception with file writer on: " + mLogFile);
+ }
+ }
+
+ if (level == LogLevel.INFO) {
+ Log.i("Robocop", message, t);
+ } else if (level == LogLevel.DEBUG) {
+ Log.d("Robocop", message, t);
+ } else if (level == LogLevel.WARN) {
+ Log.w("Robocop", message, t);
+ } else if (level == LogLevel.ERROR) {
+ Log.e("Robocop", message, t);
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeElement.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeElement.java
new file mode 100644
index 0000000000..2a24344fd0
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeElement.java
@@ -0,0 +1,116 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.TextSwitcher;
+import android.widget.TextView;
+
+public class FennecNativeElement implements Element {
+ private final Activity mActivity;
+ private final Integer mId;
+ private final String mName;
+
+ public FennecNativeElement(Integer id, Activity activity) {
+ mId = id;
+ mActivity = activity;
+ mName = activity.getResources().getResourceName(id);
+ }
+
+ @Override
+ public Integer getId() {
+ return mId;
+ }
+
+ private boolean mClickSuccess;
+
+ @Override
+ public boolean click() {
+ mClickSuccess = false;
+ RobocopUtils.runOnUiThreadSync(mActivity,
+ new Runnable() {
+ @Override
+ public void run() {
+ View view = mActivity.findViewById(mId);
+ if (view != null) {
+ if (view.performClick()) {
+ mClickSuccess = true;
+ } else {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.WARN,
+ "Robocop called click on an element with no listener " + mId + " " + mName);
+ }
+ } else {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR,
+ "click: unable to find view " + mId + " " + mName);
+ }
+ }
+ });
+ return mClickSuccess;
+ }
+
+ private Object mText;
+
+ @Override
+ public String getText() {
+ mText = null;
+ RobocopUtils.runOnUiThreadSync(mActivity,
+ new Runnable() {
+ @Override
+ public void run() {
+ View v = mActivity.findViewById(mId);
+ if (v instanceof EditText) {
+ EditText et = (EditText)v;
+ mText = et.getEditableText();
+ } else if (v instanceof TextSwitcher) {
+ TextSwitcher ts = (TextSwitcher)v;
+ mText = ((TextView)ts.getCurrentView()).getText();
+ } else if (v instanceof ViewGroup) {
+ ViewGroup vg = (ViewGroup)v;
+ for (int i = 0; i < vg.getChildCount(); i++) {
+ if (vg.getChildAt(i) instanceof TextView) {
+ mText = ((TextView)vg.getChildAt(i)).getText();
+ }
+ }
+ } else if (v instanceof TextView) {
+ mText = ((TextView)v).getText();
+ } else if (v == null) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR,
+ "getText: unable to find view " + mId + " " + mName);
+ } else {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR,
+ "getText: unhandled type for view " + mId + " " + mName);
+ }
+ } // end of run() method definition
+ } // end of anonymous Runnable object instantiation
+ );
+ if (mText == null) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.WARN,
+ "getText: Text is null for view " + mId + " " + mName);
+ return null;
+ }
+ return mText.toString();
+ }
+
+ private boolean mDisplayed;
+
+ @Override
+ public boolean isDisplayed() {
+ mDisplayed = false;
+ RobocopUtils.runOnUiThreadSync(mActivity,
+ new Runnable() {
+ @Override
+ public void run() {
+ View view = mActivity.findViewById(mId);
+ if (view != null) {
+ mDisplayed = true;
+ }
+ }
+ });
+ return mDisplayed;
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecTalosAssert.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecTalosAssert.java
new file mode 100644
index 0000000000..862f667770
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecTalosAssert.java
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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 class FennecTalosAssert implements Assert {
+
+ public FennecTalosAssert() { }
+
+ /**
+ * Write information to a logfile and logcat
+ */
+ public void dumpLog(String message) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.INFO, message);
+ }
+
+ /** Write information to a logfile and logcat */
+ public void dumpLog(String message, Throwable t) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.INFO, message, t);
+ }
+
+ /**
+ * Set the filename used for dumpLog.
+ */
+ public void setLogFile(String filename) {
+ FennecNativeDriver.setLogFile(filename);
+ }
+
+ public void setTestName(String testName) { }
+
+ public void endTest() { }
+
+ public void ok(boolean condition, String name, String diag) {
+ if (!condition) {
+ dumpLog("__FAIL" + name + ": " + diag + "__FAIL");
+ }
+ }
+
+ public void is(Object actual, Object expected, String name) {
+ boolean pass = (actual == null ? expected == null : actual.equals(expected));
+ ok(pass, name, "got " + actual + ", expected " + expected);
+ }
+
+ public void isnot(Object actual, Object notExpected, String name) {
+ boolean fail = (actual == null ? notExpected == null : actual.equals(notExpected));
+ ok(!fail, name, "got " + actual + ", expected not " + notExpected);
+ }
+
+ public void ispixel(int actual, int r, int g, int b, String name) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void isnotpixel(int actual, int r, int g, int b, String name) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void todo(boolean condition, String name, String diag) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void todo_is(Object actual, Object expected, String name) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void todo_isnot(Object actual, Object notExpected, String name) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void info(String name, String message) {
+ dumpLog(name + ": " + message);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/LaunchFennecWithConfigurationActivity.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/LaunchFennecWithConfigurationActivity.java
new file mode 100644
index 0000000000..208b2c7bd0
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/LaunchFennecWithConfigurationActivity.java
@@ -0,0 +1,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/. */
+
+package org.mozilla.gecko;
+
+import java.util.Map;
+
+import org.mozilla.gecko.tests.BaseRobocopTest;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+/**
+ * An Activity that extracts Robocop settings from robotium.config, launches
+ * Fennec with the Robocop testing parameters, and finishes itself.
+ * <p>
+ * This is intended to be used by local testers using |mach robocop --serve|.
+ */
+public class LaunchFennecWithConfigurationActivity extends Activity {
+ @Override
+ public void onCreate(Bundle arguments) {
+ super.onCreate(arguments);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ final String configFile = FennecNativeDriver.getFile(BaseRobocopTest.DEFAULT_ROOT_PATH + "/robotium.config");
+ final Map<String, String> config = FennecNativeDriver.convertTextToTable(configFile);
+ final Intent intent = BaseRobocopTest.createActivityIntent(config);
+
+ intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
+
+ this.finish();
+ this.startActivity(intent);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/PaintedSurface.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/PaintedSurface.java
new file mode 100644
index 0000000000..17d77b7586
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/PaintedSurface.java
@@ -0,0 +1,105 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+
+import android.graphics.Bitmap;
+import android.util.Base64;
+import android.util.Base64OutputStream;
+
+public class PaintedSurface {
+ private String mFileName;
+ private int mWidth;
+ private int mHeight;
+ private FileInputStream mPixelFile;
+ private MappedByteBuffer mPixelBuffer;
+
+ public PaintedSurface(String filename, int width, int height) {
+ mFileName = filename;
+ mWidth = width;
+ mHeight = height;
+
+ try {
+ File f = new File(filename);
+ int pixelSize = (int)f.length();
+
+ mPixelFile = new FileInputStream(filename);
+ mPixelBuffer = mPixelFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, pixelSize);
+ } catch (java.io.FileNotFoundException e) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR, e);
+ } catch (java.io.IOException e) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR, e);
+ }
+ }
+
+ public final int getWidth() {
+ return mWidth;
+ }
+
+ public final int getHeight() {
+ return mHeight;
+ }
+
+ private int pixelAtIndex(int index) {
+ int b1 = mPixelBuffer.get(index) & 0xFF;
+ int b2 = mPixelBuffer.get(index + 1) & 0xFF;
+ int b3 = mPixelBuffer.get(index + 2) & 0xFF;
+ int b4 = mPixelBuffer.get(index + 3) & 0xFF;
+ int value = (b1 << 24) + (b2 << 16) + (b3 << 8) + (b4 << 0);
+ return value;
+ }
+
+ public final int getPixelAt(int x, int y) {
+ if (mPixelBuffer == null) {
+ throw new RoboCopException("Trying to access PaintedSurface with no active PixelBuffer");
+ }
+
+ if (x >= mWidth || x < 0) {
+ throw new RoboCopException("Trying to access PaintedSurface with invalid x value");
+ }
+
+ if (y >= mHeight || y < 0) {
+ throw new RoboCopException("Trying to access PaintedSurface with invalid y value");
+ }
+
+ // The rows are reversed so row 0 is at the end and we start with the last row.
+ // This is why we do mHeight-y;
+ int index = (x + ((mHeight - y - 1) * mWidth)) * 4;
+ return pixelAtIndex(index);
+ }
+
+ public final String asDataUri() {
+ try {
+ Bitmap bm = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
+ for (int y = 0; y < mHeight; y++) {
+ for (int x = 0; x < mWidth; x++) {
+ int index = (x + ((mHeight - y - 1) * mWidth)) * 4;
+ bm.setPixel(x, y, pixelAtIndex(index));
+ }
+ }
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ out.write("data:image/png;base64,".getBytes());
+ Base64OutputStream b64 = new Base64OutputStream(out, Base64.NO_WRAP);
+ bm.compress(Bitmap.CompressFormat.PNG, 100, b64);
+ return new String(out.toByteArray());
+ } catch (Exception e) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR, e);
+ throw new RoboCopException("Unable to convert surface to a PNG data:uri");
+ }
+ }
+
+ public void close() {
+ try {
+ mPixelFile.close();
+ } catch (Exception e) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG, e);
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RoboCopException.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RoboCopException.java
new file mode 100644
index 0000000000..420df818d9
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RoboCopException.java
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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 class RoboCopException extends RuntimeException {
+
+ public RoboCopException() {
+ super();
+ }
+
+ public RoboCopException(String message) {
+ super(message);
+ }
+
+ public RoboCopException(Throwable cause) {
+ super(cause);
+ }
+
+ public RoboCopException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopShare1.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopShare1.java
new file mode 100644
index 0000000000..80ab3396c2
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopShare1.java
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.FragmentActivity;
+
+public class RobocopShare1 extends FragmentActivity {
+ private static Bundle sArguments;
+
+ @Override
+ public void onCreate(Bundle arguments) {
+ super.onCreate(arguments);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopShare2.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopShare2.java
new file mode 100644
index 0000000000..4874dffb76
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopShare2.java
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.FragmentActivity;
+
+public class RobocopShare2 extends FragmentActivity {
+ private static Bundle sArguments;
+
+ @Override
+ public void onCreate(Bundle arguments) {
+ super.onCreate(arguments);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopUtils.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopUtils.java
new file mode 100644
index 0000000000..7a33abfa64
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopUtils.java
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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.concurrent.atomic.AtomicBoolean;
+
+import android.app.Activity;
+
+public final class RobocopUtils {
+ private static final int MAX_WAIT_MS = 20000;
+
+ private RobocopUtils() {}
+
+ public static void runOnUiThreadSync(Activity activity, final Runnable runnable) {
+ final AtomicBoolean sentinel = new AtomicBoolean(false);
+
+ // On the UI thread, run the Runnable, then set sentinel to true and wake this thread.
+ activity.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ runnable.run();
+
+ synchronized (sentinel) {
+ sentinel.set(true);
+ sentinel.notifyAll();
+ }
+ }
+ }
+ );
+
+
+ // Suspend this thread, until the other thread completes its work or until a timeout is
+ // reached.
+ long startTimestamp = System.currentTimeMillis();
+
+ synchronized (sentinel) {
+ while (!sentinel.get()) {
+ try {
+ sentinel.wait(MAX_WAIT_MS);
+ } catch (InterruptedException e) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR, e);
+ }
+
+ // Abort if we woke up due to timeout (instead of spuriously).
+ if (System.currentTimeMillis() - startTimestamp >= MAX_WAIT_MS) {
+ FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR,
+ "time-out waiting for UI thread");
+ FennecNativeDriver.logAllStackTraces(FennecNativeDriver.LogLevel.ERROR);
+
+ return;
+ }
+ }
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/StructuredLogger.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/StructuredLogger.java
new file mode 100644
index 0000000000..87d5a3c250
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/StructuredLogger.java
@@ -0,0 +1,188 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy 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 java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.json.JSONObject;
+
+// This implements the structured logging API described here: http://mozbase.readthedocs.org/en/latest/mozlog_structured.html
+public class StructuredLogger {
+ private final static HashSet<String> validTestStatus = new HashSet<String>(Arrays.asList("PASS", "FAIL", "TIMEOUT", "NOTRUN", "ASSERT"));
+ private final static HashSet<String> validTestEnd = new HashSet<String>(Arrays.asList("PASS", "FAIL", "OK", "ERROR", "TIMEOUT",
+ "CRASH", "ASSERT", "SKIP"));
+
+ private String mName;
+ private String mComponent;
+ private LoggerCallback mCallback;
+
+ static public interface LoggerCallback {
+ public void call(String output);
+ }
+
+ /* A default logger callback that prints the JSON output to stdout.
+ * This is not to be used in robocop as we write to a log file. */
+ static class StandardLoggerCallback implements LoggerCallback {
+ public void call(String output) {
+ System.out.println(output);
+ }
+ }
+
+ public StructuredLogger(String name, String component, LoggerCallback callback) {
+ mName = name;
+ mComponent = component;
+ mCallback = callback;
+ }
+
+ public StructuredLogger(String name, String component) {
+ this(name, component, new StandardLoggerCallback());
+ }
+
+ public StructuredLogger(String name, LoggerCallback callback) {
+ this(name, null, callback);
+ }
+
+ public StructuredLogger(String name) {
+ this(name, null, new StandardLoggerCallback());
+ }
+
+ public void suiteStart(List<String> tests, Map<String, Object> runInfo) {
+ HashMap<String, Object> data = new HashMap<String, Object>();
+ data.put("tests", tests);
+ if (runInfo != null) {
+ data.put("run_info", runInfo);
+ }
+ this.logData("suite_start", data);
+ }
+
+ public void suiteStart(List<String> tests) {
+ this.suiteStart(tests, null);
+ }
+
+ public void suiteEnd() {
+ this.logData("suite_end");
+ }
+
+ public void testStart(String test) {
+ HashMap<String, Object> data = new HashMap<String, Object>();
+ data.put("test", test);
+ this.logData("test_start", data);
+ }
+
+ public void testStatus(String test, String subtest, String status, String expected, String message) {
+ status = status.toUpperCase();
+ if (!StructuredLogger.validTestStatus.contains(status)) {
+ throw new IllegalArgumentException("Unrecognized status: " + status);
+ }
+
+ HashMap<String, Object> data = new HashMap<String, Object>();
+ data.put("test", test);
+ data.put("subtest", subtest);
+ data.put("status", status);
+
+ if (message != null) {
+ data.put("message", message);
+ }
+ if (!expected.equals(status)) {
+ data.put("expected", expected);
+ }
+
+ this.logData("test_status", data);
+ }
+
+ public void testStatus(String test, String subtest, String status, String message) {
+ this.testStatus(test, subtest, status, "PASS", message);
+ }
+
+ public void testEnd(String test, String status, String expected, String message, Map<String, Object> extra) {
+ status = status.toUpperCase();
+ if (!StructuredLogger.validTestEnd.contains(status)) {
+ throw new IllegalArgumentException("Unrecognized status: " + status);
+ }
+
+ HashMap<String, Object> data = new HashMap<String, Object>();
+ data.put("test", test);
+ data.put("status", status);
+
+ if (message != null) {
+ data.put("message", message);
+ }
+ if (extra != null) {
+ data.put("extra", extra);
+ }
+ if (!expected.equals(status) && !status.equals("SKIP")) {
+ data.put("expected", expected);
+ }
+
+ this.logData("test_end", data);
+ }
+
+ public void testEnd(String test, String status, String expected, String message) {
+ this.testEnd(test, status, expected, message, null);
+ }
+
+ public void testEnd(String test, String status, String message) {
+ this.testEnd(test, status, "OK", message, null);
+ }
+
+
+ public void debug(String message) {
+ this.log("debug", message);
+ }
+
+ public void info(String message) {
+ this.log("info", message);
+ }
+
+ public void warning(String message) {
+ this.log("warning", message);
+ }
+
+ public void error(String message) {
+ this.log("error", message);
+ }
+
+ public void critical(String message) {
+ this.log("critical", message);
+ }
+
+ private void log(String level, String message) {
+ HashMap<String, Object> data = new HashMap<String, Object>();
+ data.put("message", message);
+ data.put("level", level);
+ this.logData("log", data);
+ }
+
+ private HashMap<String, Object> makeLogData(String action, Map<String, Object> data) {
+ HashMap<String, Object> allData = new HashMap<String, Object>();
+ allData.put("action", action);
+ allData.put("time", System.currentTimeMillis());
+ allData.put("thread", JSONObject.NULL);
+ allData.put("pid", JSONObject.NULL);
+ allData.put("source", mName);
+ if (mComponent != null) {
+ allData.put("component", mComponent);
+ }
+
+ allData.putAll(data);
+
+ return allData;
+ }
+
+ private void logData(String action, Map<String, Object> data) {
+ HashMap<String, Object> logData = this.makeLogData(action, data);
+ JSONObject jsonObject = new JSONObject(logData);
+ mCallback.call(jsonObject.toString());
+ }
+
+ private void logData(String action) {
+ this.logData(action, new HashMap<String, Object>());
+ }
+
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/AboutHomeTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/AboutHomeTest.java
new file mode 100644
index 0000000000..cadb5df937
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/AboutHomeTest.java
@@ -0,0 +1,252 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import java.util.ArrayList;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.home.HomePager;
+
+import android.support.v4.view.ViewPager;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.TabWidget;
+import android.widget.TextView;
+
+import com.robotium.solo.Condition;
+
+/**
+ * This class is an extension of BaseTest that helps with interaction with about:home
+ * This class contains methods that access the different tabs from about:home, methods that get information like history and bookmarks from the database, edit and remove bookmarks and history items
+ * The purpose of this class is to collect all the logically connected methods that deal with about:home
+ * To use any of these methods in your test make sure it extends AboutHomeTest instead of BaseTest
+ */
+abstract class AboutHomeTest extends PixelTest {
+ protected enum AboutHomeTabs {
+ RECENT_TABS,
+ HISTORY,
+ TOP_SITES,
+ BOOKMARKS,
+ };
+
+ private final ArrayList<String> aboutHomeTabs = new ArrayList<String>() {{
+ add("TOP_SITES");
+ add("BOOKMARKS");
+ }};
+
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ if (aboutHomeTabs.size() < 4) {
+ // Update it for tablets vs. phones.
+ if (mDevice.type.equals("phone")) {
+ aboutHomeTabs.add(0, AboutHomeTabs.HISTORY.toString());
+ aboutHomeTabs.add(0, AboutHomeTabs.RECENT_TABS.toString());
+ } else {
+ aboutHomeTabs.add(AboutHomeTabs.HISTORY.toString());
+ aboutHomeTabs.add(AboutHomeTabs.RECENT_TABS.toString());
+ }
+ }
+ }
+
+ /**
+ * FIXME: Write new versions of these methods and update their consumers to use the new about:home pages.
+ */
+ protected ListView getHistoryList(String waitText, int expectedChildCount) {
+ return null;
+ }
+ protected ListView getHistoryList(String waitText) {
+ return null;
+ }
+
+ // Returns true if the bookmark is displayed in the bookmarks tab, false otherwise - does not check in folders
+ protected void isBookmarkDisplayed(final String url) {
+ boolean isCorrect = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ View bookmark = getDisplayedBookmark(url);
+ return bookmark != null;
+ }
+ }, MAX_WAIT_MS);
+
+ mAsserter.ok(isCorrect, "Checking that " + url + " displayed as a bookmark", url + " displayed");
+ }
+
+ // Loads a bookmark by tapping on the bookmark view in the Bookmarks tab
+ protected void loadBookmark(String url) {
+ View bookmark = getDisplayedBookmark(url);
+ if (bookmark != null) {
+ Actions.EventExpecter contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded");
+ mSolo.clickOnView(bookmark);
+ contentEventExpecter.blockForEvent();
+ contentEventExpecter.unregisterListener();
+ } else {
+ mAsserter.ok(false, url + " is not one of the displayed bookmarks", "Please make sure the url provided is bookmarked");
+ }
+ }
+
+ // Opens the bookmark context menu by long-tapping on it
+ protected void openBookmarkContextMenu(String url) {
+ View bookmark = getDisplayedBookmark(url);
+ if (bookmark != null) {
+ mSolo.waitForView(bookmark);
+ mSolo.clickLongOnView(bookmark, LONG_PRESS_TIME);
+ mSolo.waitForDialogToOpen();
+ } else {
+ mAsserter.ok(false, url + " is not one of the displayed bookmarks", "Please make sure the url provided is bookmarked");
+ }
+ }
+
+ // @return the View associated with bookmark for the provided url or null if the link is not bookmarked
+ protected View getDisplayedBookmark(String url) {
+ openAboutHomeTab(AboutHomeTabs.BOOKMARKS);
+ mSolo.hideSoftKeyboard();
+ getInstrumentation().waitForIdleSync();
+ ListView bookmarksTabList = findListViewWithTag(HomePager.LIST_TAG_BOOKMARKS);
+ waitForNonEmptyListToLoad(bookmarksTabList);
+ ListAdapter adapter = bookmarksTabList.getAdapter();
+ if (adapter != null) {
+ for (int i = 0; i < adapter.getCount(); i++ ) {
+ // I am unable to click the view taken with getView for some reason so getting the child at i
+ bookmarksTabList.smoothScrollToPosition(i);
+ View bookmarkView = bookmarksTabList.getChildAt(i);
+ if (bookmarkView instanceof android.widget.LinearLayout) {
+ ViewGroup bookmarkItemView = (ViewGroup) bookmarkView;
+ for (int j = 0 ; j < bookmarkItemView.getChildCount(); j++) {
+ View bookmarkContent = bookmarkItemView.getChildAt(j);
+ if (bookmarkContent instanceof android.widget.LinearLayout) {
+ ViewGroup bookmarkItemLayout = (ViewGroup) bookmarkContent;
+ for (int k = 0 ; k < bookmarkItemLayout.getChildCount(); k++) {
+ // Both the title and url are represented as text views so we can cast the view without any issues
+ TextView bookmarkTextContent = (TextView)bookmarkItemLayout.getChildAt(k);
+ if (url.equals(bookmarkTextContent.getText().toString())) {
+ return bookmarkView;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Waits for the given ListView to have a non-empty adapter and be populated
+ * with a minimum number of items.
+ *
+ * This method will return false if the given ListView or its adapter is null,
+ * or if the ListView does not have the minimum number of items.
+ */
+ protected boolean waitForListToLoad(final ListView listView, final int minSize) {
+ Condition listWaitCondition = new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ if (listView == null) {
+ return false;
+ }
+
+ final ListAdapter adapter = listView.getAdapter();
+ if (adapter == null) {
+ return false;
+ }
+
+ return (listView.getCount() - listView.getHeaderViewsCount() >= minSize);
+ }
+ };
+ return waitForCondition(listWaitCondition, MAX_WAIT_MS);
+ }
+
+ protected boolean waitForNonEmptyListToLoad(final ListView listView) {
+ return waitForListToLoad(listView, 1);
+ }
+
+ /**
+ * Get an active ListView with the specified tag .
+ *
+ * This method uses the predefined tags in HomePager.
+ */
+ protected final ListView findListViewWithTag(String tag) {
+ for (ListView listView : mSolo.getCurrentViews(ListView.class)) {
+ final String listTag = (String) listView.getTag();
+ if (TextUtils.isEmpty(listTag)) {
+ continue;
+ }
+
+ if (TextUtils.equals(listTag, tag)) {
+ return listView;
+ }
+ }
+
+ return null;
+ }
+
+ // A wait in order for the about:home tab to be rendered after drag/tab selection
+ private void waitForAboutHomeTab(final int tabIndex) {
+ boolean correctTab = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ ViewPager pager = mSolo.getView(ViewPager.class, 0);
+ return (pager.getCurrentItem() == tabIndex);
+ }
+ }, MAX_WAIT_MS);
+ mAsserter.ok(correctTab, "Checking that the correct tab is displayed", "The " + aboutHomeTabs.get(tabIndex) + " tab is displayed");
+ }
+
+ private void clickAboutHomeTab(AboutHomeTabs tab) {
+ mSolo.clickOnText(tab.toString().replace("_", " "));
+ }
+
+ /**
+ * Swipes to an about:home tab.
+ * @param swipeVector swipeVector Value and direction to swipe (go left for negative, right for positive).
+ */
+ private void swipeAboutHome(int swipeVector) {
+ // Increase swipe width, which will especially impact tablets.
+ int swipeWidth = mDriver.getGeckoWidth() - 1;
+ int swipeHeight = mDriver.getGeckoHeight() / 2;
+
+ if (swipeVector >= 0) {
+ // Emulate swipe motion from right to left.
+ for (int i = 0; i < swipeVector; i++) {
+ mActions.drag(swipeWidth, 0, swipeHeight, swipeHeight);
+ mSolo.sleep(100);
+ }
+ } else {
+ // Emulate swipe motion from left to right.
+ for (int i = 0; i > swipeVector; i--) {
+ mActions.drag(0, swipeWidth, swipeHeight, swipeHeight);
+ mSolo.sleep(100);
+ }
+ }
+ }
+
+ /**
+ * This method can be used to open the different tabs of about:home.
+ */
+ protected void openAboutHomeTab(AboutHomeTabs tab) {
+ focusUrlBar();
+ ViewPager pager = mSolo.getView(ViewPager.class, 0);
+ final int currentTabIndex = pager.getCurrentItem();
+ int tabOffset;
+
+ // Handle tablets by just clicking the visible tab title.
+ if (mDevice.type.equals("tablet")) {
+ clickAboutHomeTab(tab);
+ return;
+ }
+
+ // Handle phones (non-tablets).
+ tabOffset = aboutHomeTabs.indexOf(tab.toString()) - currentTabIndex;
+ swipeAboutHome(tabOffset);
+ waitForAboutHomeTab(aboutHomeTabs.indexOf(tab.toString()));
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseRobocopTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseRobocopTest.java
new file mode 100644
index 0000000000..3033524e8a
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseRobocopTest.java
@@ -0,0 +1,288 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.PowerManager;
+import android.test.ActivityInstrumentationTestCase2;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.robotium.solo.Solo;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.Assert;
+import org.mozilla.gecko.BrowserApp;
+import org.mozilla.gecko.Driver;
+import org.mozilla.gecko.FennecInstrumentationTestRunner;
+import org.mozilla.gecko.FennecMochitestAssert;
+import org.mozilla.gecko.FennecNativeActions;
+import org.mozilla.gecko.FennecNativeDriver;
+import org.mozilla.gecko.FennecTalosAssert;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.updater.UpdateServiceHelper;
+
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Map;
+
+@SuppressWarnings("unchecked")
+public abstract class BaseRobocopTest extends ActivityInstrumentationTestCase2<Activity> {
+ public static final String LOGTAG = "BaseTest";
+
+ public enum Type {
+ MOCHITEST,
+ TALOS
+ }
+
+ public static final String DEFAULT_ROOT_PATH = "/mnt/sdcard/tests";
+
+ // How long to wait for a Robocop:Quit message to actually kill Fennec.
+ private static final int ROBOCOP_QUIT_WAIT_MS = 180000;
+
+ /**
+ * The Java Class instance that launches the browser.
+ * <p>
+ * This should always agree with {@link AppConstants#MOZ_ANDROID_BROWSER_INTENT_CLASS}.
+ */
+ public static final Class<? extends Activity> BROWSER_INTENT_CLASS;
+
+ // Use reflection here so we don't have to preprocess this file.
+ static {
+ Class<? extends Activity> cl;
+ try {
+ cl = (Class<? extends Activity>) Class.forName(AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
+ } catch (ClassNotFoundException e) {
+ // Oh well.
+ cl = Activity.class;
+ }
+ BROWSER_INTENT_CLASS = cl;
+ }
+
+ protected Assert mAsserter;
+ protected String mLogFile;
+
+ protected String mBaseHostnameUrl;
+ protected String mBaseIpUrl;
+
+ protected Map<String, String> mConfig;
+ protected String mRootPath;
+
+ protected Solo mSolo;
+ protected Driver mDriver;
+ protected Actions mActions;
+
+ protected String mProfile;
+
+ protected StringHelper mStringHelper;
+
+ /**
+ * The browser is started at the beginning of this test. A single test is a
+ * class inheriting from <code>BaseRobocopTest</code> that contains test
+ * methods.
+ * <p>
+ * If a test should not start the browser at the beginning of a test,
+ * specify a different activity class to the one-argument constructor. To do
+ * as little as possible, specify <code>Activity.class</code>.
+ */
+ public BaseRobocopTest() {
+ this((Class<Activity>) BROWSER_INTENT_CLASS);
+ }
+
+ /**
+ * Start the given activity class at the beginning of this test.
+ * <p>
+ * <b>You should use the no-argument constructor in almost all cases.</b>
+ *
+ * @param activityClass to start before this test.
+ */
+ protected BaseRobocopTest(Class<Activity> activityClass) {
+ super(activityClass);
+ }
+
+ /**
+ * Returns the test type: mochitest or talos.
+ * <p>
+ * By default tests are mochitests, but a test can override this method in
+ * order to change its type. Most Robocop tests are mochitests.
+ */
+ protected Type getTestType() {
+ return Type.MOCHITEST;
+ }
+
+ // Member function to allow specialization.
+ protected Intent createActivityIntent() {
+ return BaseRobocopTest.createActivityIntent(mConfig);
+ }
+
+ // Static function to allow re-use.
+ public static Intent createActivityIntent(Map<String, String> config) {
+ final Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.putExtra("args", "-no-remote -profile " + config.get("profile"));
+ // Don't show the first run experience.
+ intent.putExtra(BrowserApp.EXTRA_SKIP_STARTPANE, true);
+
+ final String envString = config.get("envvars");
+ if (!TextUtils.isEmpty(envString)) {
+ final String[] envStrings = envString.split(",");
+
+ for (int iter = 0; iter < envStrings.length; iter++) {
+ intent.putExtra("env" + iter, envStrings[iter]);
+ }
+ }
+
+ return intent;
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ // Disable the updater.
+ UpdateServiceHelper.setEnabled(false);
+
+ // Load config file from root path (set up by Python script).
+ mRootPath = FennecInstrumentationTestRunner.getFennecArguments().getString("deviceroot");
+ if (mRootPath == null) {
+ Log.w("Robocop", "Did not find deviceroot in arguments; falling back to: " + DEFAULT_ROOT_PATH);
+ mRootPath = DEFAULT_ROOT_PATH;
+ }
+ String configFile = FennecNativeDriver.getFile(mRootPath + "/robotium.config");
+ mConfig = FennecNativeDriver.convertTextToTable(configFile);
+ mLogFile = mConfig.get("logfile");
+ mProfile = mConfig.get("profile");
+ mBaseHostnameUrl = mConfig.get("host").replaceAll("(/$)", "");
+ mBaseIpUrl = mConfig.get("rawhost").replaceAll("(/$)", "");
+
+ // Initialize the asserter.
+ if (getTestType() == Type.TALOS) {
+ mAsserter = new FennecTalosAssert();
+ } else {
+ mAsserter = new FennecMochitestAssert();
+ }
+ mAsserter.setLogFile(mLogFile);
+ mAsserter.setTestName(getClass().getName());
+
+ // Start the activity.
+ final Intent intent = createActivityIntent();
+ setActivityIntent(intent);
+
+ // Set up Robotium.solo and Driver objects
+ Activity tempActivity = getActivity();
+
+ StringHelper.initialize(tempActivity.getResources());
+ mStringHelper = StringHelper.get();
+
+ mSolo = new Solo(getInstrumentation(), tempActivity);
+ mDriver = new FennecNativeDriver(tempActivity, mSolo, mRootPath);
+ mActions = new FennecNativeActions(tempActivity, mSolo, getInstrumentation(), mAsserter);
+ }
+
+ @Override
+ protected void runTest() throws Throwable {
+ try {
+ super.runTest();
+ } catch (Throwable t) {
+ // save screenshot -- written to /mnt/sdcard/Robotium-Screenshots
+ // as <filename>.jpg
+ mSolo.takeScreenshot("robocop-screenshot-"+getClass().getName());
+ if (mAsserter != null) {
+ mAsserter.dumpLog("Exception caught during test!", t);
+ mAsserter.ok(false, "Exception caught", t.toString());
+ }
+ // re-throw to continue bail-out
+ throw t;
+ }
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ try {
+ mAsserter.endTest();
+
+ // By default, we don't quit Fennec on finish, and we don't finish
+ // all opened activities. Not quiting Fennec entirely is intended to
+ // make life better for local testers, who might want to alter a
+ // test that is under development rather than Fennec itself. Not
+ // finishing activities is intended to allow local testers to
+ // manually inspect an activity's state after a test
+ // run. runtestsremote.py sets this to "1". Testers running via an
+ // IDE will not have this set at all.
+ final String quitAndFinish = FennecInstrumentationTestRunner.getFennecArguments()
+ .getString("quit_and_finish"); // null means not specified.
+ if ("1".equals(quitAndFinish)) {
+ // Request the browser force quit and wait for it to take effect.
+ Log.i(LOGTAG, "Requesting force quit.");
+ mActions.sendGeckoEvent("Robocop:Quit", null);
+ mSolo.sleep(ROBOCOP_QUIT_WAIT_MS);
+
+ // If still running, finish activities as recommended by Robotium.
+ Log.i(LOGTAG, "Finishing all opened activities.");
+ mSolo.finishOpenedActivities();
+ } else {
+ // This has the effect of keeping the activity-under-test
+ // around; if we don't set it to null, it is killed, either by
+ // finishOpenedActivities above or super.tearDown below.
+ Log.i(LOGTAG, "Not requesting force quit and trying to keep started activity alive.");
+ setActivity(null);
+ }
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ super.tearDown();
+ }
+
+ /**
+ * Function to early abort if we can't reach the given HTTP server. Provides local testers
+ * with diagnostic information. Not currently available for TALOS tests, which are rarely run
+ * locally in any case.
+ */
+ public void throwIfHttpGetFails() {
+ if (getTestType() == Type.TALOS) {
+ return;
+ }
+
+ // rawURL to test fetching from. This should be a raw (IP) URL, not an alias
+ // (like mochi.test). We can't (easily) test fetching from the aliases, since
+ // those are managed by Fennec's proxy settings.
+ final String rawUrl = ((String) mConfig.get("rawhost")).replaceAll("(/$)", "");
+
+ HttpURLConnection urlConnection = null;
+
+ try {
+ urlConnection = (HttpURLConnection) new URL(rawUrl).openConnection();
+
+ final int statusCode = urlConnection.getResponseCode();
+ if (200 != statusCode) {
+ throw new IllegalStateException("Status code: " + statusCode);
+ }
+ } catch (Exception e) {
+ mAsserter.ok(false, "Robocop tests on your device need network/wifi access to reach: [" + rawUrl + "].", e.toString());
+ } finally {
+ if (urlConnection != null) {
+ urlConnection.disconnect();
+ }
+ }
+ }
+
+ /**
+ * Ensure that the screen on the test device is powered on during tests.
+ */
+ public void throwIfScreenNotOn() {
+ final PowerManager pm = (PowerManager) getActivity().getSystemService(Context.POWER_SERVICE);
+ mAsserter.ok(pm.isScreenOn(),
+ "Robocop tests need the test device screen to be powered on.", "");
+ }
+
+ protected GeckoProfile getTestProfile() {
+ if (mProfile.startsWith("/")) {
+ return GeckoProfile.get(getActivity(), /* profileName */ null, mProfile);
+ }
+
+ return GeckoProfile.get(getActivity(), mProfile);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseTest.java
new file mode 100644
index 0000000000..a8dfedc4ea
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseTest.java
@@ -0,0 +1,976 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.HashSet;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.Element;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.RobocopUtils;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
+
+import android.content.ContentValues;
+import android.content.res.AssetManager;
+import android.database.Cursor;
+import android.os.Build;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+
+import com.robotium.solo.Condition;
+import com.robotium.solo.Timeout;
+
+/**
+ * A convenient base class suitable for most Robocop tests.
+ */
+@SuppressWarnings("unchecked")
+abstract class BaseTest extends BaseRobocopTest {
+ private static final int VERIFY_URL_TIMEOUT = 2000;
+ private static final int MAX_WAIT_ENABLED_TEXT_MS = 15000;
+ private static final int MAX_WAIT_HOME_PAGER_HIDDEN_MS = 15000;
+ private static final int MAX_WAIT_VERIFY_PAGE_TITLE_MS = 15000;
+ public static final int MAX_WAIT_MS = 4500;
+ public static final int LONG_PRESS_TIME = 6000;
+ private static final int GECKO_READY_WAIT_MS = 180000;
+
+ protected static final String URL_HTTP_PREFIX = "http://";
+
+ public Device mDevice;
+ protected DatabaseHelper mDatabaseHelper;
+ protected int mScreenMidWidth;
+ protected int mScreenMidHeight;
+ private final HashSet<Integer> mKnownTabIDs = new HashSet<Integer>();
+
+ protected void blockForDelayedStartup() {
+ try {
+ Actions.EventExpecter delayedStartupExpector = mActions.expectGeckoEvent("Gecko:DelayedStartup");
+ delayedStartupExpector.blockForEvent(GECKO_READY_WAIT_MS, true);
+ delayedStartupExpector.unregisterListener();
+ } catch (Exception e) {
+ mAsserter.dumpLog("Exception in blockForDelayedStartup", e);
+ }
+ }
+
+ protected void blockForGeckoReady() {
+ try {
+ Actions.EventExpecter geckoReadyExpector = mActions.expectGeckoEvent("Gecko:Ready");
+ if (!GeckoThread.isRunning()) {
+ geckoReadyExpector.blockForEvent(GECKO_READY_WAIT_MS, true);
+ }
+ geckoReadyExpector.unregisterListener();
+ } catch (Exception e) {
+ mAsserter.dumpLog("Exception in blockForGeckoReady", e);
+ }
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mDevice = new Device();
+ mDatabaseHelper = new DatabaseHelper(getActivity(), mAsserter);
+
+ // Ensure Robocop tests have access to network, and are run with Display powered on.
+ throwIfHttpGetFails();
+ throwIfScreenNotOn();
+ }
+
+ protected void initializeProfile() {
+ final GeckoProfile profile = getTestProfile();
+
+ // In Robocop tests, we typically don't get initialized correctly, because
+ // GeckoProfile doesn't create the profile directory.
+ profile.enqueueInitialization(profile.getDir());
+ }
+
+ /**
+ * Click on the URL bar to focus it and enter editing mode.
+ */
+ protected final void focusUrlBar() {
+ // Click on the browser toolbar to enter editing mode
+ mSolo.waitForView(R.id.browser_toolbar);
+ final View toolbarView = mSolo.getView(R.id.browser_toolbar);
+ mSolo.clickOnView(toolbarView);
+
+ // Wait for highlighed text to gain focus
+ boolean success = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ mSolo.waitForView(R.id.url_edit_text);
+ EditText urlEditText = (EditText) mSolo.getView(R.id.url_edit_text);
+ if (urlEditText.isInputMethodTarget()) {
+ return true;
+ }
+ return false;
+ }
+ }, MAX_WAIT_ENABLED_TEXT_MS);
+
+ mAsserter.ok(success, "waiting for urlbar text to gain focus", "urlbar text gained focus");
+ }
+
+ protected final void enterUrl(String url) {
+ focusUrlBar();
+
+ final EditText urlEditView = (EditText) mSolo.getView(R.id.url_edit_text);
+
+ // Send the keys for the URL we want to enter
+ mSolo.clearEditText(urlEditView);
+ mSolo.typeText(urlEditView, url);
+
+ // Get the URL text from the URL bar EditText view
+ final String urlBarText = urlEditView.getText().toString();
+ mAsserter.is(url, urlBarText, "URL typed properly");
+ }
+
+ protected final Fragment getBrowserSearch() {
+ final FragmentManager fm = ((FragmentActivity) getActivity()).getSupportFragmentManager();
+ return fm.findFragmentByTag("browser_search");
+ }
+
+ protected final void hitEnterAndWait() {
+ Actions.EventExpecter contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded");
+ mActions.sendSpecialKey(Actions.SpecialKey.ENTER);
+ // wait for screen to load
+ contentEventExpecter.blockForEvent();
+ contentEventExpecter.unregisterListener();
+ }
+
+ /**
+ * Load <code>url</code> by sending key strokes to the URL bar UI.
+ *
+ * This method waits synchronously for the <code>DOMContentLoaded</code>
+ * message from Gecko before returning.
+ *
+ * Unless you need to test text entry in the url bar, consider using loadUrl
+ * instead -- it loads pages more directly and quickly.
+ */
+ protected final void inputAndLoadUrl(String url) {
+ enterUrl(url);
+ hitEnterAndWait();
+ }
+
+ /**
+ * Load <code>url</code> using the internal
+ * <code>org.mozilla.gecko.Tabs</code> API.
+ *
+ * This method does not wait for any confirmation from Gecko before
+ * returning -- consider using verifyUrlBarTitle or a similar approach
+ * to wait for the page to load, or at least use loadUrlAndWait.
+ */
+ protected final void loadUrl(final String url) {
+ try {
+ Tabs.getInstance().loadUrl(url);
+ } catch (Exception e) {
+ mAsserter.dumpLog("Exception in loadUrl", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Load <code>url</code> using the internal
+ * <code>org.mozilla.gecko.Tabs</code> API and wait for DOMContentLoaded.
+ */
+ protected final void loadUrlAndWait(final String url) {
+ Actions.EventExpecter contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded");
+ loadUrl(url);
+ contentEventExpecter.blockForEvent();
+ contentEventExpecter.unregisterListener();
+ }
+
+ protected final void closeTab(int tabId) {
+ Tabs tabs = Tabs.getInstance();
+ Tab tab = tabs.getTab(tabId);
+ tabs.closeTab(tab);
+ }
+
+ public final void verifyUrl(String url) {
+ final EditText urlEditText = (EditText) mSolo.getView(R.id.url_edit_text);
+ String urlBarText = null;
+ if (urlEditText != null) {
+ // wait for a short time for the expected text, in case there is a delay
+ // in updating the view
+ waitForCondition(new VerifyTextViewText(urlEditText, url), VERIFY_URL_TIMEOUT);
+ urlBarText = urlEditText.getText().toString();
+
+ }
+ mAsserter.is(urlBarText, url, "Browser toolbar URL stayed the same");
+ }
+
+ class VerifyTextViewText implements Condition {
+ private final TextView mTextView;
+ private final String mExpected;
+ public VerifyTextViewText(TextView textView, String expected) {
+ mTextView = textView;
+ mExpected = expected;
+ }
+
+ @Override
+ public boolean isSatisfied() {
+ String textValue = mTextView.getText().toString();
+ return mExpected.equals(textValue);
+ }
+ }
+
+ class VerifyContentDescription implements Condition {
+ private final View view;
+ private final String expected;
+
+ public VerifyContentDescription(View view, String expected) {
+ this.view = view;
+ this.expected = expected;
+ }
+
+ @Override
+ public boolean isSatisfied() {
+ final CharSequence actual = view.getContentDescription();
+ return TextUtils.equals(actual, expected);
+ }
+ }
+
+ protected final String getAbsoluteUrl(String url) {
+ return mBaseHostnameUrl + "/" + url.replaceAll("(^/)", "");
+ }
+
+ protected final String getAbsoluteRawUrl(String url) {
+ return mBaseIpUrl + "/" + url.replaceAll("(^/)", "");
+ }
+
+ /*
+ * Wrapper method for mSolo.waitForCondition with additional logging.
+ */
+ protected final boolean waitForCondition(Condition condition, int timeout) {
+ boolean result = mSolo.waitForCondition(condition, timeout);
+ if (!result) {
+ // Log timeout failure for diagnostic purposes only; a failed wait may
+ // be normal and does not necessarily warrant a test assertion/failure.
+ mAsserter.dumpLog("waitForCondition timeout after " + timeout + " ms.");
+ }
+ return result;
+ }
+
+ public void SqliteCompare(String dbName, String sqlCommand, ContentValues[] cvs) {
+ File profile = new File(mProfile);
+ String dbPath = new File(profile, dbName).getPath();
+
+ Cursor c = mActions.querySql(dbPath, sqlCommand);
+ SqliteCompare(c, cvs);
+ }
+
+ public void SqliteCompare(Cursor c, ContentValues[] cvs) {
+ mAsserter.is(c.getCount(), cvs.length, "List is correct length");
+ if (c.moveToFirst()) {
+ do {
+ boolean found = false;
+ for (int i = 0; !found && i < cvs.length; i++) {
+ if (CursorMatches(c, cvs[i])) {
+ found = true;
+ }
+ }
+ mAsserter.is(found, true, "Password was found");
+ } while (c.moveToNext());
+ }
+ }
+
+ public boolean CursorMatches(Cursor c, ContentValues cv) {
+ for (int i = 0; i < c.getColumnCount(); i++) {
+ String column = c.getColumnName(i);
+ if (cv.containsKey(column)) {
+ mAsserter.info("Comparing", "Column values for: " + column);
+ Object value = cv.get(column);
+ if (value == null) {
+ if (!c.isNull(i)) {
+ return false;
+ }
+ } else {
+ if (c.isNull(i) || !value.toString().equals(c.getString(i))) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ public InputStream getAsset(String filename) throws IOException {
+ AssetManager assets = getInstrumentation().getContext().getAssets();
+ return assets.open(filename);
+ }
+
+ public boolean waitForText(final String text) {
+ // false is the default value for finding only
+ // visible views in `Solo.waitForText(String)`.
+ return waitForText(text, false);
+ }
+
+ public boolean waitForText(final String text, final boolean onlyVisibleViews) {
+ // We use the default robotium values from
+ // `Waiter.waitForText(String)` for unspecified arguments.
+ final boolean rc =
+ mSolo.waitForText(text, 0, Timeout.getLargeTimeout(), true, onlyVisibleViews);
+ if (!rc) {
+ // log out failed wait for diagnostic purposes only;
+ // waitForText failures are sometimes expected/normal
+ mAsserter.dumpLog("waitForText timeout on "+text);
+ }
+ return rc;
+ }
+
+ // waitForText usually scrolls down in a view when text is not visible.
+ // For PreferenceScreens and dialogs, Solo.waitForText scrolling does not
+ // work, so we use this hack to do the same thing.
+ protected boolean waitForPreferencesText(String txt) {
+ boolean foundText = waitForText(txt);
+ if (!foundText) {
+ if ((mScreenMidWidth == 0) || (mScreenMidHeight == 0)) {
+ mScreenMidWidth = mDriver.getGeckoWidth()/2;
+ mScreenMidHeight = mDriver.getGeckoHeight()/2;
+ }
+
+ // If we don't see the item, scroll down once in case it's off-screen.
+ // Hacky way to scroll down. solo.scroll* does not work in dialogs.
+ MotionEventHelper meh = new MotionEventHelper(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop());
+ meh.dragSync(mScreenMidWidth, mScreenMidHeight+100, mScreenMidWidth, mScreenMidHeight-100);
+
+ foundText = mSolo.waitForText(txt);
+ }
+ return foundText;
+ }
+
+ /**
+ * Wait for <text> to be visible and also be enabled/clickable.
+ */
+ public boolean waitForEnabledText(String text) {
+ final String testText = text;
+ boolean rc = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ // Solo.getText() could be used here, except that it sometimes
+ // hits an assertion when the requested text is not found.
+ ArrayList<View> views = mSolo.getCurrentViews();
+ for (View view : views) {
+ if (view instanceof TextView) {
+ TextView tv = (TextView)view;
+ String viewText = tv.getText().toString();
+ if (tv.isEnabled() && viewText != null && viewText.matches(testText)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ }, MAX_WAIT_ENABLED_TEXT_MS);
+ if (!rc) {
+ // log out failed wait for diagnostic purposes only;
+ // failures are sometimes expected/normal
+ mAsserter.dumpLog("waitForEnabledText timeout on "+text);
+ }
+ return rc;
+ }
+
+
+ /**
+ * Select <item> from Menu > "Settings" > <section>.
+ */
+ public void selectSettingsItem(String section, String item) {
+ String[] itemPath = { "Settings", section, item };
+ selectMenuItemByPath(itemPath);
+ }
+
+ /**
+ * Traverses the items in listItems in order in the menu.
+ */
+ public void selectMenuItemByPath(String[] listItems) {
+ int listLength = listItems.length;
+ if (listLength > 0) {
+ selectMenuItem(listItems[0]);
+ }
+ if (listLength > 1) {
+ for (int i = 1; i < listLength; i++) {
+ String itemName = "^" + listItems[i] + "$";
+ mAsserter.ok(waitForPreferencesText(itemName), "Waiting for and scrolling once to find item " + itemName, itemName + " found");
+ mAsserter.ok(waitForEnabledText(itemName), "Waiting for enabled text " + itemName, itemName + " option is present and enabled");
+ mSolo.clickOnText(itemName);
+ }
+ }
+ }
+
+ public final void selectMenuItem(String menuItemName) {
+ // build the item name ready to be used
+ String itemName = "^" + menuItemName + "$";
+ final View menuView = mSolo.getView(R.id.menu);
+ mAsserter.isnot(menuView, null, "Menu view is not null");
+ mSolo.clickOnView(menuView, true);
+ mAsserter.ok(waitForEnabledText(itemName), "Waiting for menu item " + itemName, itemName + " is present and enabled");
+ mSolo.clickOnText(itemName);
+ }
+
+ public final void verifyHomePagerHidden() {
+ final View homePagerContainer = mSolo.getView(R.id.home_screen_container);
+
+ boolean rc = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return homePagerContainer.getVisibility() != View.VISIBLE;
+ }
+ }, MAX_WAIT_HOME_PAGER_HIDDEN_MS);
+
+ if (!rc) {
+ mAsserter.ok(rc, "Verify HomePager is hidden", "HomePager is hidden");
+ }
+ }
+
+ public final void verifyUrlBarTitle(String url) {
+ mAsserter.isnot(url, null, "The url argument is not null");
+
+ final String expected;
+ if (mStringHelper.ABOUT_HOME_URL.equals(url)) {
+ expected = mStringHelper.ABOUT_HOME_TITLE;
+ } else if (url.startsWith(URL_HTTP_PREFIX)) {
+ expected = url.substring(URL_HTTP_PREFIX.length());
+ } else {
+ expected = url;
+ }
+
+ final TextView urlBarTitle = (TextView) mSolo.getView(R.id.url_bar_title);
+ String pageTitle = null;
+ if (urlBarTitle != null) {
+ // Wait for the title to make sure it has been displayed in case the view
+ // does not update fast enough
+ waitForCondition(new VerifyTextViewText(urlBarTitle, expected), MAX_WAIT_VERIFY_PAGE_TITLE_MS);
+ pageTitle = urlBarTitle.getText().toString();
+ }
+ mAsserter.is(pageTitle, expected, "Page title is correct");
+ }
+
+ public final void verifyUrlInContentDescription(String url) {
+ mAsserter.isnot(url, null, "The url argument is not null");
+
+ final String expected;
+ if (mStringHelper.ABOUT_HOME_URL.equals(url)) {
+ expected = mStringHelper.ABOUT_HOME_TITLE;
+ } else if (url.startsWith(URL_HTTP_PREFIX)) {
+ expected = url.substring(URL_HTTP_PREFIX.length());
+ } else {
+ expected = url;
+ }
+
+ final View urlDisplayLayout = mSolo.getView(R.id.display_layout);
+ assertNotNull("ToolbarDisplayLayout is not null", urlDisplayLayout);
+
+ String actualUrl = null;
+
+ // Wait for the title to make sure it has been displayed in case the view
+ // does not update fast enough
+ waitForCondition(new VerifyContentDescription(urlDisplayLayout, expected), MAX_WAIT_VERIFY_PAGE_TITLE_MS);
+ if (urlDisplayLayout.getContentDescription() != null) {
+ actualUrl = urlDisplayLayout.getContentDescription().toString();
+ }
+
+ mAsserter.is(actualUrl, expected, "Url is correct");
+ }
+
+ public final void verifyTabCount(int expectedTabCount) {
+ Element tabCount = mDriver.findElement(getActivity(), R.id.tabs_counter);
+ String tabCountText = tabCount.getText();
+ int tabCountInt = Integer.parseInt(tabCountText);
+ mAsserter.is(tabCountInt, expectedTabCount, "The correct number of tabs are opened");
+ }
+
+ public void verifyPinned(final boolean isPinned, final String gridItemTitle) {
+ boolean viewFound = waitForText(gridItemTitle);
+ mAsserter.ok(viewFound, "Found top site title: " + gridItemTitle, null);
+
+ boolean success = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ // We set the left compound drawable (index 0) to the pin icon.
+ final TextView gridItemTextView = mSolo.getText(gridItemTitle);
+ return isPinned == (gridItemTextView.getCompoundDrawables()[0] != null);
+ }
+ }, MAX_WAIT_MS);
+ mAsserter.ok(success, "Top site item was pinned: " + isPinned, null);
+ }
+
+ public void pinTopSite(String gridItemTitle) {
+ verifyPinned(false, gridItemTitle);
+ mSolo.clickLongOnText(gridItemTitle);
+ boolean dialogOpened = mSolo.waitForDialogToOpen();
+ mAsserter.ok(dialogOpened, "Pin site dialog opened: " + gridItemTitle, null);
+ boolean pinSiteFound = waitForText(mStringHelper.CONTEXT_MENU_PIN_SITE);
+ mAsserter.ok(pinSiteFound, "Found pin site menu item", null);
+ mSolo.clickOnText(mStringHelper.CONTEXT_MENU_PIN_SITE);
+ verifyPinned(true, gridItemTitle);
+ }
+
+ public void unpinTopSite(String gridItemTitle) {
+ verifyPinned(true, gridItemTitle);
+ mSolo.clickLongOnText(gridItemTitle);
+ boolean dialogOpened = mSolo.waitForDialogToOpen();
+ mAsserter.ok(dialogOpened, "Pin site dialog opened: " + gridItemTitle, null);
+ boolean unpinSiteFound = waitForText(mStringHelper.CONTEXT_MENU_UNPIN_SITE);
+ mAsserter.ok(unpinSiteFound, "Found unpin site menu item", null);
+ mSolo.clickOnText(mStringHelper.CONTEXT_MENU_UNPIN_SITE);
+ verifyPinned(false, gridItemTitle);
+ }
+
+ // Used to perform clicks on pop-up buttons without having to close the virtual keyboard
+ public void clickOnButton(String label) {
+ final Button button = mSolo.getButton(label);
+ try {
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ button.performClick();
+ }
+ });
+ } catch (Throwable throwable) {
+ mAsserter.ok(false, "Unable to click the button","Was unable to click button ");
+ }
+ }
+
+ private void waitForAnimationsToFinish() {
+ // Ideally we'd actually wait for animations to finish but since we have
+ // no good way of doing that, we just wait an arbitrary unit of time.
+ mSolo.sleep(3500);
+ }
+
+ public void addTab() {
+ mSolo.clickOnView(mSolo.getView(R.id.tabs));
+ waitForAnimationsToFinish();
+
+ // wait for addTab to appear (this is usually immediate)
+ boolean success = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ View addTabView = mSolo.getView(R.id.add_tab);
+ if (addTabView == null) {
+ return false;
+ }
+ return true;
+ }
+ }, MAX_WAIT_MS);
+ mAsserter.ok(success, "waiting for add tab view", "add tab view available");
+ final Actions.RepeatedEventExpecter pageShowExpecter = mActions.expectGeckoEvent("Content:PageShow");
+ mSolo.clickOnView(mSolo.getView(R.id.add_tab));
+ waitForAnimationsToFinish();
+
+ // Wait until we get a PageShow event for a new tab ID
+ for(;;) {
+ try {
+ JSONObject data = new JSONObject(pageShowExpecter.blockForEventData());
+ int tabID = data.getInt("tabID");
+ if (tabID == 0) {
+ mAsserter.dumpLog("addTab ignoring PageShow for tab 0");
+ continue;
+ }
+ if (!mKnownTabIDs.contains(tabID)) {
+ mKnownTabIDs.add(tabID);
+ break;
+ }
+ } catch(JSONException e) {
+ mAsserter.ok(false, "Exception in addTab", getStackTraceString(e));
+ }
+ }
+ pageShowExpecter.unregisterListener();
+ }
+
+ public void addTab(String url) {
+ addTab();
+
+ // Adding a new tab opens about:home, so now we just need to load the url in it.
+ loadUrlAndWait(url);
+ }
+
+ public void closeAddedTabs() {
+ for(int tabID : mKnownTabIDs) {
+ closeTab(tabID);
+ }
+ }
+
+ // A temporary tabs list/grid holder while the list and grid views are being transitioned to
+ // RecyclerViews (bug 1116415 and bug 1310081).
+ private static class TabsView {
+ private AdapterView<ListAdapter> gridView;
+ private RecyclerView listView;
+
+ public TabsView(View view) {
+ if (view instanceof RecyclerView) {
+ listView = (RecyclerView) view;
+ } else {
+ gridView = (AdapterView<ListAdapter>) view;
+ }
+ }
+
+ public void bringPositionIntoView(int index) {
+ if (gridView != null) {
+ gridView.setSelection(index);
+ } else {
+ listView.scrollToPosition(index);
+ }
+ }
+
+ public View getViewAtIndex(int index) {
+ if (gridView != null) {
+ return gridView.getChildAt(index - gridView.getFirstVisiblePosition());
+ } else {
+ final RecyclerView.ViewHolder itemViewHolder = listView.findViewHolderForLayoutPosition(index);
+ return itemViewHolder == null ? null : itemViewHolder.itemView;
+ }
+ }
+
+ public void post(Runnable runnable) {
+ if (gridView != null) {
+ gridView.post(runnable);
+ } else {
+ listView.post(runnable);
+ }
+ }
+ }
+ /**
+ * Gets the AdapterView of the tabs list.
+ *
+ * @return List view in the tabs panel
+ */
+ private final TabsView getTabsLayout() {
+ Element tabs = mDriver.findElement(getActivity(), R.id.tabs);
+ tabs.click();
+ return new TabsView(getActivity().findViewById(R.id.normal_tabs));
+ }
+
+ /**
+ * Gets the view in the tabs panel at the specified index.
+ *
+ * @return View at index
+ */
+ private View getTabViewAt(final int index) {
+ final View[] childView = { null };
+
+ final TabsView view = getTabsLayout();
+
+ runOnUiThreadSync(new Runnable() {
+ @Override
+ public void run() {
+ view.bringPositionIntoView(index);
+
+ // The selection isn't updated synchronously; posting a
+ // runnable to the view's queue guarantees we'll run after the
+ // layout pass.
+ view.post(new Runnable() {
+ @Override
+ public void run() {
+ // Index is relative to all views in the list.
+ childView[0] = view.getViewAtIndex(index);
+ }
+ });
+ }
+ });
+
+ boolean result = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return childView[0] != null;
+ }
+ }, MAX_WAIT_MS);
+
+ mAsserter.ok(result, "list item at index " + index + " exists", null);
+
+ return childView[0];
+ }
+
+ /**
+ * Selects the tab at the specified index.
+ *
+ * @param index Index of tab to select
+ */
+ public void selectTabAt(final int index) {
+ mSolo.clickOnView(getTabViewAt(index));
+ }
+
+ public final void runOnUiThreadSync(Runnable runnable) {
+ RobocopUtils.runOnUiThreadSync(getActivity(), runnable);
+ }
+
+ /* Tap the "star" (bookmark) button to bookmark or un-bookmark the current page */
+ public void toggleBookmark() {
+ mActions.sendSpecialKey(Actions.SpecialKey.MENU);
+ waitForText("Settings");
+
+ // On ICS+ phones, there is no button labeled "Bookmarks"
+ // instead we have to just dig through every button on the screen
+ ArrayList<View> images = mSolo.getCurrentViews();
+ for (int i = 0; i < images.size(); i++) {
+ final View view = images.get(i);
+ boolean found = false;
+ found = "Bookmark".equals(view.getContentDescription());
+
+ // on older android versions, try looking at the button's text
+ if (!found) {
+ if (view instanceof TextView) {
+ found = "Bookmark".equals(((TextView)view).getText());
+ }
+ }
+
+ if (found) {
+ int[] xy = new int[2];
+ view.getLocationOnScreen(xy);
+
+ final int viewWidth = view.getWidth();
+ final int viewHeight = view.getHeight();
+ final float x = xy[0] + (viewWidth / 2.0f);
+ float y = xy[1] + (viewHeight / 2.0f);
+
+ mSolo.clickOnScreen(x, y);
+ }
+ }
+ }
+
+ class Device {
+ public final String version; // 2.x or 3.x or 4.x
+ public String type; // "tablet" or "phone"
+ public final int width;
+ public final int height;
+ public final float density;
+
+ public Device() {
+ // Determine device version
+ int sdk = Build.VERSION.SDK_INT;
+ if (sdk < Build.VERSION_CODES.HONEYCOMB) {
+ version = "2.x";
+ } else {
+ if (sdk > Build.VERSION_CODES.HONEYCOMB_MR2) {
+ version = "4.x";
+ } else {
+ version = "3.x";
+ }
+ }
+ // Determine with and height
+ DisplayMetrics dm = new DisplayMetrics();
+ getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);
+ height = dm.heightPixels;
+ width = dm.widthPixels;
+ density = dm.density;
+ // Determine device type
+ type = "phone";
+ try {
+ if (GeckoAppShell.isTablet()) {
+ type = "tablet";
+ }
+ } catch (Exception e) {
+ mAsserter.dumpLog("Exception in detectDevice", e);
+ }
+ }
+ }
+
+ class Navigation {
+ private final String devType;
+ private final String osVersion;
+
+ public Navigation(Device mDevice) {
+ devType = mDevice.type;
+ osVersion = mDevice.version;
+ }
+
+ public void back() {
+ Actions.EventExpecter pageShowExpecter = mActions.expectGeckoEvent("Content:PageShow");
+
+ if (devType.equals("tablet")) {
+ Element backBtn = mDriver.findElement(getActivity(), R.id.back);
+ backBtn.click();
+ } else {
+ mSolo.goBack();
+ }
+
+ pageShowExpecter.blockForEvent();
+ pageShowExpecter.unregisterListener();
+ }
+
+ public void forward() {
+ Actions.EventExpecter pageShowExpecter = mActions.expectGeckoEvent("Content:PageShow");
+
+ if (devType.equals("tablet")) {
+ mSolo.waitForView(R.id.forward);
+ mSolo.clickOnView(mSolo.getView(R.id.forward));
+ } else {
+ mActions.sendSpecialKey(Actions.SpecialKey.MENU);
+ waitForText("^New Tab$");
+ if (!osVersion.equals("2.x")) {
+ mSolo.waitForView(R.id.forward);
+ mSolo.clickOnView(mSolo.getView(R.id.forward));
+ } else {
+ mSolo.clickOnText("^Forward$");
+ }
+ ensureMenuClosed();
+ }
+
+ pageShowExpecter.blockForEvent();
+ pageShowExpecter.unregisterListener();
+ }
+
+ // DEPRECATED!
+ // Use BaseTest.toggleBookmark() in new code.
+ public void bookmark() {
+ mActions.sendSpecialKey(Actions.SpecialKey.MENU);
+ waitForText("^New Tab$");
+ if (mSolo.searchText("^Bookmark$")) {
+ // This is the Android 2.x so the button has text
+ mSolo.clickOnText("^Bookmark$");
+ } else {
+ Element bookmarkBtn = mDriver.findElement(getActivity(), R.id.bookmark);
+ if (bookmarkBtn != null) {
+ // We are on Android 4.x so the button is an image button
+ bookmarkBtn.click();
+ }
+ }
+ ensureMenuClosed();
+ }
+
+ // On some devices, the menu may not be dismissed after clicking on an
+ // item. Close it here.
+ private void ensureMenuClosed() {
+ if (mSolo.searchText("^New Tab$")) {
+ mSolo.goBack();
+ }
+ }
+ }
+
+ /**
+ * Gets the string representation of a stack trace.
+ *
+ * @param t Throwable to get stack trace for
+ * @return Stack trace as a string
+ */
+ public static String getStackTraceString(Throwable t) {
+ StringWriter sw = new StringWriter();
+ t.printStackTrace(new PrintWriter(sw));
+ return sw.toString();
+ }
+
+ /**
+ * Condition class that waits for a view, and allows callers access it when done.
+ */
+ private class DescriptionCondition<T extends View> implements Condition {
+ public T mView;
+ private final String mDescr;
+ private final Class<T> mCls;
+
+ public DescriptionCondition(Class<T> cls, String descr) {
+ mDescr = descr;
+ mCls = cls;
+ }
+
+ @Override
+ public boolean isSatisfied() {
+ mView = findViewWithContentDescription(mCls, mDescr);
+ return (mView != null);
+ }
+ }
+
+ /**
+ * Wait for a view with the specified description .
+ */
+ public <T extends View> T waitForViewWithDescription(Class<T> cls, String description) {
+ DescriptionCondition<T> c = new DescriptionCondition<T>(cls, description);
+ waitForCondition(c, MAX_WAIT_ENABLED_TEXT_MS);
+ return c.mView;
+ }
+
+ /**
+ * Get an active view with the specified description .
+ */
+ public <T extends View> T findViewWithContentDescription(Class<T> cls, String description) {
+ for (T view : mSolo.getCurrentViews(cls)) {
+ final String descr = (String) view.getContentDescription();
+ if (TextUtils.isEmpty(descr)) {
+ continue;
+ }
+
+ if (TextUtils.equals(description, descr)) {
+ return view;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Abstract class for running small test cases within a BaseTest.
+ */
+ abstract class TestCase implements Runnable {
+ /**
+ * Implement tests here. setUp and tearDown for the test case
+ * should be handled by the parent test. This is so we can avoid the
+ * overhead of starting Gecko and creating profiles.
+ */
+ protected abstract void test() throws Exception;
+
+ @Override
+ public void run() {
+ try {
+ test();
+ } catch (Exception e) {
+ mAsserter.ok(false,
+ "Test " + this.getClass().getName() + " threw exception: " + e,
+ "");
+ }
+ }
+ }
+
+ /**
+ * Set the preference and wait for it to change before proceeding with the test.
+ */
+ public void setPreferenceAndWaitForChange(final String name, final Object value) {
+ blockForGeckoReady();
+ mActions.setPref(name, value, /* flush */ false);
+
+ // Wait for confirmation of the pref change before proceeding with the test.
+ mActions.getPrefs(new String[] { name }, new Actions.PrefHandlerBase() {
+
+ @Override // Actions.PrefHandlerBase
+ public void prefValue(String pref, boolean changedValue) {
+ mAsserter.is(pref, name, "Expecting correct pref name");
+ mAsserter.ok(value instanceof Boolean, "Expecting boolean pref", "");
+ mAsserter.is(changedValue, value, "Expecting matching pref value");
+ }
+
+ @Override // Actions.PrefHandlerBase
+ public void prefValue(String pref, int changedValue) {
+ mAsserter.is(pref, name, "Expecting correct pref name");
+ mAsserter.ok(value instanceof Integer, "Expecting int pref", "");
+ mAsserter.is(changedValue, value, "Expecting matching pref value");
+ }
+
+ @Override // Actions.PrefHandlerBase
+ public void prefValue(String pref, String changedValue) {
+ mAsserter.is(pref, name, "Expecting correct pref name");
+ mAsserter.ok(value instanceof CharSequence, "Expecting string pref", "");
+ mAsserter.is(changedValue, value, "Expecting matching pref value");
+ }
+
+ }).waitForFinish();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentContextMenuTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentContextMenuTest.java
new file mode 100644
index 0000000000..5a1d09f8c9
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentContextMenuTest.java
@@ -0,0 +1,135 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.util.Clipboard;
+import org.mozilla.gecko.Element;
+import org.mozilla.gecko.R;
+
+import android.util.DisplayMetrics;
+
+import com.robotium.solo.Condition;
+
+/**
+ * This class covers interactions with the context menu opened from web content
+ */
+abstract class ContentContextMenuTest extends PixelTest {
+ private static final int MAX_TEST_TIMEOUT = 30000; // 30 seconds (worst case)
+
+ // This method opens the context menu of any web content. It assumes that the page is already loaded
+ protected void openWebContentContextMenu(String waitText) {
+ DisplayMetrics dm = new DisplayMetrics();
+ getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);
+
+ // The web content we are trying to open the context menu for should be positioned at the top of the page, at least 60px high and aligned to the middle
+ float top = mDriver.getGeckoTop() + 30 * dm.density;
+ float left = mDriver.getGeckoLeft() + mDriver.getGeckoWidth() / 2;
+
+ mAsserter.dumpLog("long-clicking at "+left+", "+top);
+ mSolo.clickLongOnScreen(left, top);
+ waitForText(waitText);
+ }
+
+ protected void verifyContextMenuItems(String[] items) {
+ // Test that the menu items are displayed
+ if (!mSolo.searchText(items[0])) {
+ openWebContentContextMenu(items[0]); // Open the context menu if it is not already
+ }
+
+ for (String option:items) {
+ mAsserter.ok(mSolo.searchText(option), "Checking that the option: " + option + " is available", "The option is available");
+ }
+ }
+
+ protected void openTabFromContextMenu(String contextMenuOption, int expectedTabCount) {
+ if (!mSolo.searchText(contextMenuOption)) {
+ openWebContentContextMenu(contextMenuOption); // Open the context menu if it is not already
+ }
+ Actions.EventExpecter tabEventExpecter = mActions.expectGeckoEvent("Tab:Added");
+ mSolo.clickOnText(contextMenuOption);
+ tabEventExpecter.blockForEvent();
+ tabEventExpecter.unregisterListener();
+ verifyTabCount(expectedTabCount);
+ }
+
+ protected void verifyTabs(String[] items) {
+ if (!mSolo.searchText(items[0])) {
+ openWebContentContextMenu(items[0]);
+ }
+
+ for (String option:items) {
+ mAsserter.ok(mSolo.searchText(option), "Checking that the option: " + option + " is available", "The option is available");
+ }
+ }
+
+ protected void switchTabs(String tab) {
+ if (!mSolo.searchText(tab)) {
+ openWebContentContextMenu(tab);
+ }
+ mSolo.clickOnText(tab);
+ }
+
+
+ protected void verifyCopyOption(String copyOption, final String copiedText) {
+ if (!mSolo.searchText(copyOption)) {
+ openWebContentContextMenu(copyOption); // Open the context menu if it is not already
+ }
+ mSolo.clickOnText(copyOption);
+ boolean correctText = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ final String clipboardText = Clipboard.getText();
+ mAsserter.dumpLog("Clipboard text = " + clipboardText + " , expected text = " + copiedText);
+ return clipboardText.contains(copiedText);
+ }
+ }, MAX_TEST_TIMEOUT);
+ mAsserter.ok(correctText, "Checking if the text is correctly copied", "The text was correctly copied");
+ }
+
+
+
+ protected void verifyShareOption(String shareOption, String pageTitle) {
+ waitForText(pageTitle); // Even if this fails, it won't assert
+ if (!mSolo.searchText(shareOption)) {
+ openWebContentContextMenu(shareOption); // Open the context menu if it is not already
+ }
+ mSolo.clickOnText(shareOption);
+ mAsserter.ok(waitForText(shareOption), "Checking that the share pop-up is displayed", "The pop-up has been displayed");
+
+ // Close the Share Link option menu and wait for the page to be focused again
+ mSolo.goBack();
+ waitForText(pageTitle);
+ }
+
+ protected void verifyViewImageOption(String viewImageOption, final String imageUrl, String pageTitle) {
+ if (!mSolo.searchText(viewImageOption)) {
+ openWebContentContextMenu(viewImageOption);
+ }
+ mSolo.clickOnText(viewImageOption);
+
+ boolean viewedImage = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ final Element urlBarElement = mDriver.findElement(getActivity(), R.id.url_edit_text);
+ final String loadedUrl = urlBarElement.getText();
+ return loadedUrl.contentEquals(imageUrl);
+ }
+ }, MAX_TEST_TIMEOUT);
+ mAsserter.ok(viewedImage, "Checking if the image is correctly viewed", "The image was correctly viewed");
+
+ mSolo.goBack();
+ waitForText(pageTitle);
+ }
+
+ protected void verifyBookmarkLinkOption(String bookmarkOption, String link) {
+ if (!mSolo.searchText(bookmarkOption)) {
+ openWebContentContextMenu(bookmarkOption); // Open the context menu if it is not already
+ }
+ mSolo.clickOnText(bookmarkOption);
+ mAsserter.ok(waitForText("Bookmark added"), "Waiting for the Bookmark added toaster notification", "The notification has been displayed");
+ mAsserter.ok(mDatabaseHelper.isBookmark(link), "Checking if the link has been added as a bookmark", "The link has been bookmarked");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentProviderTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentProviderTest.java
new file mode 100644
index 0000000000..5496c97d2b
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentProviderTest.java
@@ -0,0 +1,255 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.concurrent.Callable;
+
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserProvider;
+
+import android.content.ContentProvider;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.test.IsolatedContext;
+import android.test.RenamingDelegatingContext;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+
+/*
+ * ContentProviderTest provides the infrastructure to run content provider
+ * tests in an controlled/isolated environment, guaranteeing that your tests
+ * will not affect or be affected by any UI-related code. This is basically
+ * a heavily adapted port of Android's ProviderTestCase2 to work on Mozilla's
+ * infrastructure.
+ *
+ * For some tests, we need to have access to UI parts, or at least launch
+ * the activity so the assets with test data become available, which requires
+ * that we derive this test from BaseTest and consequently pull in some more
+ * UI code than we'd ideally want. Furthermore, we need to pass the
+ * Activity and not the instrumentation Context for the UI part to find some
+ * of its required resources.
+ * Similarly, we need to pass the Activity instead of the Instrumentation
+ * Context down to some of our users (still wrapped in the Delegating Provider)
+ * because they will stop working correctly if we do not. A typical problem
+ * is that databases used in the ContentProvider will be attempted to be
+ * opened twice.
+ */
+abstract class ContentProviderTest extends BaseTest {
+ protected ContentProvider mProvider;
+ protected ChangeRecordingMockContentResolver mResolver;
+ protected ArrayList<Runnable> mTests;
+ protected String mDatabaseName;
+ protected String mProviderAuthority;
+ protected IsolatedContext mProviderContext;
+
+ private class ContentProviderMockContext extends MockContext {
+ @Override
+ public Resources getResources() {
+ // We will fail to find some resources if we don't point
+ // at the original activity.
+ return ((Context)getActivity()).getResources();
+ }
+
+ @Override
+ public String getPackageName() {
+ return getInstrumentation().getContext().getPackageName();
+ }
+
+ @Override
+ public String getPackageResourcePath() {
+ return getInstrumentation().getContext().getPackageResourcePath();
+ }
+
+ @Override
+ public File getDir(String name, int mode) {
+ return getInstrumentation().getContext().getDir(this.getClass().getSimpleName() + "_" + name, mode);
+ }
+
+ @Override
+ public Context getApplicationContext() {
+ return this;
+ }
+
+ @Override
+ public SharedPreferences getSharedPreferences(String name, int mode) {
+ return getInstrumentation().getContext().getSharedPreferences(name, mode);
+ }
+
+ @Override
+ public ApplicationInfo getApplicationInfo() {
+ return getInstrumentation().getContext().getApplicationInfo();
+ }
+ }
+
+ protected class DelegatingTestContentProvider extends ContentProvider {
+ ContentProvider mTargetProvider;
+
+ public DelegatingTestContentProvider(ContentProvider targetProvider) {
+ super();
+ mTargetProvider = targetProvider;
+ }
+
+ private Uri appendTestParam(Uri uri) {
+ try {
+ return appendUriParam(uri, BrowserContract.PARAM_IS_TEST, "1");
+ } catch (Exception e) {}
+
+ return null;
+ }
+
+ @Override
+ public boolean onCreate() {
+ return mTargetProvider.onCreate();
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return mTargetProvider.getType(uri);
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return mTargetProvider.delete(appendTestParam(uri), selection, selectionArgs);
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ return mTargetProvider.insert(appendTestParam(uri), values);
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection,
+ String[] selectionArgs) {
+ return mTargetProvider.update(appendTestParam(uri), values,
+ selection, selectionArgs);
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ return mTargetProvider.query(appendTestParam(uri), projection, selection,
+ selectionArgs, sortOrder);
+ }
+
+ @Override
+ public ContentProviderResult[] applyBatch (ArrayList<ContentProviderOperation> operations)
+ throws OperationApplicationException {
+ return mTargetProvider.applyBatch(operations);
+ }
+
+ @Override
+ public int bulkInsert(Uri uri, ContentValues[] values) {
+ return mTargetProvider.bulkInsert(appendTestParam(uri), values);
+ }
+
+ public ContentProvider getTargetProvider() {
+ return mTargetProvider;
+ }
+ }
+
+ /*
+ * A MockContentResolver that records each URI that is supplied to
+ * notifyChange. Warning: the list of changed URIs is not
+ * synchronized.
+ */
+ protected class ChangeRecordingMockContentResolver extends MockContentResolver {
+ public final LinkedList<Uri> notifyChangeList = new LinkedList<Uri>();
+
+ @Override
+ public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) {
+ notifyChangeList.addLast(uri);
+
+ super.notifyChange(uri, observer, syncToNetwork);
+ }
+ }
+
+ /**
+ * Factory function that makes new ContentProvider instances.
+ * <p>
+ * We want a fresh provider each test, so this should be invoked in
+ * <code>setUp</code> before each individual test.
+ */
+ protected static Callable<ContentProvider> sBrowserProviderCallable = new Callable<ContentProvider>() {
+ @Override
+ public ContentProvider call() {
+ return new BrowserProvider();
+ }
+ };
+
+ private void setUpContentProvider(ContentProvider targetProvider) throws Exception {
+ mResolver = new ChangeRecordingMockContentResolver();
+
+ final String filenamePrefix = this.getClass().getSimpleName() + ".";
+ RenamingDelegatingContext targetContextWrapper =
+ new RenamingDelegatingContext(
+ new ContentProviderMockContext(),
+ (Context)getActivity(),
+ filenamePrefix);
+
+ mProviderContext = new IsolatedContext(mResolver, targetContextWrapper);
+
+ targetProvider.attachInfo(mProviderContext, null);
+
+ mProvider = new DelegatingTestContentProvider(targetProvider);
+ mProvider.attachInfo(mProviderContext, null);
+
+ mResolver.addProvider(mProviderAuthority, mProvider);
+ }
+
+ public static Uri appendUriParam(Uri uri, String param, String value) {
+ return uri.buildUpon().appendQueryParameter(param, value).build();
+ }
+
+ public void setTestName(String testName) {
+ mAsserter.setTestName(this.getClass().getName() + " - " + testName);
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ throw new UnsupportedOperationException("You should call setUp(authority, databaseName) instead");
+ }
+
+ public void setUp(Callable<ContentProvider> contentProviderFactory, String authority, String databaseName) throws Exception {
+ super.setUp();
+
+ mTests = new ArrayList<Runnable>();
+ mDatabaseName = databaseName;
+
+ mProviderAuthority = authority;
+
+ setUpContentProvider(contentProviderFactory.call());
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ if (Build.VERSION.SDK_INT >= 11) {
+ mProvider.shutdown();
+ }
+
+ if (mDatabaseName != null) {
+ mProviderContext.deleteDatabase(mDatabaseName);
+ }
+
+ super.tearDown();
+ }
+
+ public AssetManager getAssetManager() {
+ return getInstrumentation().getContext().getAssets();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/DatabaseHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/DatabaseHelper.java
new file mode 100644
index 0000000000..c87dc24321
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/DatabaseHelper.java
@@ -0,0 +1,170 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.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.tests;
+
+import java.util.ArrayList;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.Assert;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.db.BrowserDB;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+
+class DatabaseHelper {
+ protected enum BrowserDataType {BOOKMARKS, HISTORY};
+ private final Activity mActivity;
+ private final Assert mAsserter;
+
+ public DatabaseHelper(Activity activity, Assert asserter) {
+ mActivity = activity;
+ mAsserter = asserter;
+ }
+ /**
+ * This method can be used to check if an URL is present in the bookmarks database
+ */
+ protected boolean isBookmark(String url) {
+ final ContentResolver resolver = mActivity.getContentResolver();
+ return getProfileDB().isBookmark(resolver, url);
+ }
+
+ protected Uri buildUri(BrowserDataType dataType) {
+ Uri uri = null;
+ if (dataType == BrowserDataType.BOOKMARKS || dataType == BrowserDataType.HISTORY) {
+ uri = Uri.parse("content://" + AppConstants.ANDROID_PACKAGE_NAME + ".db.browser/" + dataType.toString().toLowerCase());
+ } else {
+ mAsserter.ok(false, "The wrong data type has been provided = " + dataType.toString(), "Please provide the correct data type");
+ }
+ uri = uri.buildUpon().appendQueryParameter("profile", GeckoProfile.DEFAULT_PROFILE)
+ .appendQueryParameter("sync", "true").build();
+ return uri;
+ }
+
+ /**
+ * Adds a bookmark.
+ */
+ protected void addMobileBookmark(String title, String url) {
+ final ContentResolver resolver = mActivity.getContentResolver();
+ getProfileDB().addBookmark(resolver, title, url);
+ mAsserter.ok(true, "Inserting a new bookmark", "Inserting the bookmark with the title = " + title + " and the url = " + url);
+ }
+
+ /**
+ * Updates the title and keyword of a bookmark with the given URL.
+ *
+ * Warning: This method assumes that there's only one bookmark with the given URL.
+ */
+ protected void updateBookmark(String url, String title, String keyword) {
+ final ContentResolver resolver = mActivity.getContentResolver();
+ // Get the id for the bookmark with the given URL.
+ Cursor c = null;
+ try {
+ c = getProfileDB().getBookmarkForUrl(resolver, url);
+ if (!c.moveToFirst()) {
+ mAsserter.ok(false, "Getting bookmark with url", "Couldn't find bookmark with url = " + url);
+ return;
+ }
+
+ int id = c.getInt(c.getColumnIndexOrThrow("_id"));
+ getProfileDB().updateBookmark(resolver, id, url, title, keyword);
+
+ mAsserter.ok(true, "Updating bookmark", "Updating bookmark with url = " + url);
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+
+ protected void deleteBookmark(String url) {
+ final ContentResolver resolver = mActivity.getContentResolver();
+ getProfileDB().removeBookmarksWithURL(resolver, url);
+ }
+
+ protected void deleteHistoryItem(String url) {
+ final ContentResolver resolver = mActivity.getContentResolver();
+ getProfileDB().removeHistoryEntry(resolver, url);
+ }
+
+ // About the same implementation as getFolderIdFromGuid from LocalBrowserDB because it is declared private and we can't use reflections to access it
+ protected long getFolderIdFromGuid(String guid) {
+ final ContentResolver resolver = mActivity.getContentResolver();
+ long folderId = -1L;
+ final Uri bookmarksUri = buildUri(BrowserDataType.BOOKMARKS);
+
+ Cursor c = null;
+ try {
+ c = resolver.query(bookmarksUri,
+ new String[] { "_id" },
+ "guid = ?",
+ new String[] { guid },
+ null);
+ if (c.moveToFirst()) {
+ folderId = c.getLong(c.getColumnIndexOrThrow("_id"));
+ }
+
+ if (folderId == -1) {
+ mAsserter.ok(false, "Trying to get the folder id" ,"We did not get the correct folder id");
+ }
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ return folderId;
+ }
+
+ /**
+ * Returns all of the bookmarks or history entries in a database.
+ *
+ * @return an ArrayList of the urls in the Firefox for Android Bookmarks or History databases.
+ */
+ protected ArrayList<String> getBrowserDBUrls(BrowserDataType dataType) {
+ final ArrayList<String> browserData = new ArrayList<String>();
+ final ContentResolver resolver = mActivity.getContentResolver();
+
+ Cursor cursor = null;
+ final BrowserDB db = getProfileDB();
+ if (dataType == BrowserDataType.HISTORY) {
+ cursor = db.getAllVisitedHistory(resolver);
+ } else if (dataType == BrowserDataType.BOOKMARKS) {
+ cursor = db.getBookmarksInFolder(resolver, getFolderIdFromGuid("mobile"));
+ }
+
+ if (cursor == null) {
+ mAsserter.ok(false, "We could not retrieve any data from the database", "The cursor was null");
+ return browserData;
+ }
+
+ try {
+ if (!cursor.moveToFirst()) {
+ // Nothing here, but that's OK -- maybe there are zero results. The calling test will fail.
+ return browserData;
+ }
+
+ do {
+ // The URL field may be null for folders in the structure of the Bookmarks table for Firefox. Eliminate those.
+ if (cursor.getString(cursor.getColumnIndex("url")) != null) {
+ browserData.add(cursor.getString(cursor.getColumnIndex("url")));
+ }
+ } while (cursor.moveToNext());
+
+ return browserData;
+ } finally {
+ cursor.close();
+ }
+ }
+
+ protected BrowserDB getProfileDB() {
+ return BrowserDB.from(mActivity);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/JavascriptBridgeTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/JavascriptBridgeTest.java
new file mode 100644
index 0000000000..a71f8fd49d
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/JavascriptBridgeTest.java
@@ -0,0 +1,107 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.*;
+
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+import org.mozilla.gecko.tests.helpers.JavascriptBridge;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+
+/**
+ * Extended to write tests using JavascriptBridge, which allows Java and JS to communicate back-and-forth.
+ * If you don't need back-and-forth communication, consider {@link JavascriptTest}.
+ *
+ * To write a test:
+ * * Extend this class
+ * * Add your javascript file to the base robocop directory (see where `testJavascriptBridge.js` is located)
+ * * In the main test method, call {@link #blockForReadyAndLoadJS(String)} with your javascript file name
+ * (don't include the path) or if you're loading a non-harness url, be sure to call {@link GeckoHelper#blockForReady()}
+ * * You can access js calls via the {@link #getJS()} method
+ * - Read {@link JavascriptBridge} javadoc for more information about using the API.
+ */
+public class JavascriptBridgeTest extends UITest {
+
+ private static final long WAIT_GET_FROM_JS_MILLIS = 20000;
+
+ private JavascriptBridge js;
+
+ // Feel free to implement additional return types.
+ private boolean isAsyncValueSet;
+ private String asyncValueStr;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ js = new JavascriptBridge(this);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ js.disconnect();
+ super.tearDown();
+ }
+
+ public JavascriptBridge getJS() {
+ return js;
+ }
+
+ protected void blockForReadyAndLoadJS(final String jsFilename) {
+ NavigationHelper.enterAndLoadUrl(mStringHelper.getHarnessUrlForJavascript(jsFilename));
+ }
+
+ /**
+ * Used to retrieve values from js when it's required to call async methods (e.g. promises).
+ * This method will block until the value is retrieved else timeout.
+ *
+ * This method is not thread-safe.
+ *
+ * Ideally, we could just have Javascript call Java when the callback completes but Java won't
+ * listen for messages unless we call into JS again (bug 1253467).
+ *
+ * To use this method:
+ * * Call this method with a name argument, henceforth known as `varName`. Note that it will be capitalized
+ * in all function names.
+ * * Create a js function, `"getAsync" + varName` (e.g. if `varName == "clientId`, the function is
+ * `getAsyncClientId`) of no args. This function should call the async get method and assign a global variable to
+ * the return value.
+ * * Create a js function, `"pollGetAsync" + varName` (e.g. `pollGetAsyncClientId`) of no args. It should call
+ * `java.asyncCall('blockingFromJsResponseString', ...` with two args: a boolean if the async value has been set yet
+ * and a String with the global return value (`null` or `undefined` are acceptable if the value has not been set).
+ */
+ public String getBlockingFromJsString(final String varName) {
+ isAsyncValueSet = false;
+ final String fnSuffix = capitalize(varName);
+ getJS().syncCall("getAsync" + fnSuffix); // Initiate async callback
+
+ final long timeoutMillis = System.currentTimeMillis() + WAIT_GET_FROM_JS_MILLIS;
+ do {
+ // Avoid sleeping! The async callback may have already completed so
+ // we test for completion here, rather than in the loop predicate.
+ getJS().syncCall("pollGetAsync" + fnSuffix);
+ if (isAsyncValueSet) {
+ break;
+ }
+
+ if (System.currentTimeMillis() > timeoutMillis) {
+ fFail("Retrieving " + varName + " from JS has timed out");
+ }
+ try {
+ Thread.sleep(500, 0); // Give time for JS to complete its operation. (emulator one core?)
+ } catch (final InterruptedException e) { }
+ } while (true);
+
+ return asyncValueStr;
+ }
+
+ public void blockingFromJsResponseString(final boolean isValueSet, final String value) {
+ this.isAsyncValueSet = isValueSet;
+ this.asyncValueStr = value;
+ }
+
+ private String capitalize(final String str) {
+ return str.substring(0, 1).toUpperCase() + str.substring(1);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/JavascriptTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/JavascriptTest.java
new file mode 100644
index 0000000000..52893510d2
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/JavascriptTest.java
@@ -0,0 +1,87 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.json.JSONObject;
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.tests.helpers.JavascriptBridge;
+import org.mozilla.gecko.tests.helpers.JavascriptMessageParser;
+
+import android.util.Log;
+
+/**
+ * Extended to test stand-alone Javascript in automation. If you're looking to test JS interactions
+ * with Java, see {@link JavascriptBridgeTest}.
+ *
+ * There are also other tests that run stand-alone javascript but are more difficult for the mobile
+ * team to run (e.g. xpcshell).
+ */
+public class JavascriptTest extends BaseTest {
+ private static final String LOGTAG = "JavascriptTest";
+ private static final String EVENT_TYPE = JavascriptBridge.EVENT_TYPE;
+
+ // 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);
+
+ private final String javascriptUrl;
+
+ public JavascriptTest(String javascriptUrl) {
+ super();
+ this.javascriptUrl = javascriptUrl;
+ }
+
+ public void testJavascript() throws Exception {
+ blockForGeckoReady();
+
+ doTestJavascript();
+ }
+
+ protected void doTestJavascript() throws Exception {
+ // We want to be waiting for Robocop messages before the page is loaded
+ // because the test harness runs each test in the suite (and possibly
+ // completes testing) before the page load event is fired.
+ final Actions.EventExpecter expecter = mActions.expectGeckoEvent(EVENT_TYPE);
+ mAsserter.dumpLog("Registered listener for " + EVENT_TYPE);
+
+ final String url = getAbsoluteUrl(mStringHelper.getHarnessUrlForJavascript(javascriptUrl));
+ mAsserter.dumpLog("Loading JavaScript test from " + url);
+ loadUrl(url);
+
+ final JavascriptMessageParser testMessageParser =
+ new JavascriptMessageParser(mAsserter, false);
+ try {
+ while (!testMessageParser.isTestFinished()) {
+ if (logVerbose) {
+ Log.v(LOGTAG, "Waiting for " + EVENT_TYPE);
+ }
+ String data = expecter.blockForEventData();
+ if (logVerbose) {
+ Log.v(LOGTAG, "Got event with data '" + data + "'");
+ }
+
+ JSONObject o = new JSONObject(data);
+ String innerType = o.getString("innerType");
+ if (!"progress".equals(innerType)) {
+ throw new Exception("Unexpected event innerType " + innerType);
+ }
+
+ String message = o.getString("message");
+ if (message == null) {
+ throw new Exception("Progress message must not be null");
+ }
+ testMessageParser.logMessage(message);
+ }
+
+ if (logDebug) {
+ Log.d(LOGTAG, "Got test finished message");
+ }
+ } finally {
+ expecter.unregisterListener();
+ mAsserter.dumpLog("Unregistered listener for " + EVENT_TYPE);
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventHelper.java
new file mode 100644
index 0000000000..5b8254e99d
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventHelper.java
@@ -0,0 +1,210 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.PrefsHelper;
+
+import android.app.Instrumentation;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.MotionEvent;
+
+class MotionEventHelper {
+ private static final String LOGTAG = "RobocopMotionEventHelper";
+
+ private static final long DRAG_EVENTS_PER_SECOND = 20; // 20 move events per second when doing a drag
+
+ private final Instrumentation mInstrumentation;
+ private final int mSurfaceOffsetX;
+ private final int mSurfaceOffsetY;
+ private final LayerView layerView;
+ private boolean mApzEnabled;
+ private float mTouchStartTolerance;
+ private final int mDpi;
+
+ public MotionEventHelper(Instrumentation inst, int surfaceOffsetX, int surfaceOffsetY) {
+ mInstrumentation = inst;
+ mSurfaceOffsetX = surfaceOffsetX;
+ mSurfaceOffsetY = surfaceOffsetY;
+ layerView = GeckoAppShell.getLayerView();
+ mApzEnabled = false;
+ mTouchStartTolerance = 0.0f;
+ mDpi = GeckoAppShell.getDpi();
+ Log.i(LOGTAG, "Initialized using offset (" + mSurfaceOffsetX + "," + mSurfaceOffsetY + ")");
+ PrefsHelper.getPref("layers.async-pan-zoom.enabled", new PrefsHelper.PrefHandlerBase() {
+ @Override public void prefValue(String pref, boolean value) {
+ mApzEnabled = value;
+ }
+ });
+ PrefsHelper.getPref("apz.touch_start_tolerance", new PrefsHelper.PrefHandlerBase() {
+ @Override public void prefValue(String pref, String value) {
+ mTouchStartTolerance = Float.parseFloat(value);
+ }
+ });
+ }
+
+ public long down(float x, float y) {
+ Log.d(LOGTAG, "Triggering down at (" + x + "," + y + ")");
+ long downTime = SystemClock.uptimeMillis();
+ MotionEvent event = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, mSurfaceOffsetX + x, mSurfaceOffsetY + y, 0);
+ try {
+ mInstrumentation.sendPointerSync(event);
+ } finally {
+ event.recycle();
+ event = null;
+ }
+ return downTime;
+ }
+
+ public long move(long downTime, float x, float y) {
+ return move(downTime, SystemClock.uptimeMillis(), x, y);
+ }
+
+ public long move(long downTime, long moveTime, float x, float y) {
+ Log.d(LOGTAG, "Triggering move to (" + x + "," + y + ")");
+ MotionEvent event = MotionEvent.obtain(downTime, moveTime, MotionEvent.ACTION_MOVE, mSurfaceOffsetX + x, mSurfaceOffsetY + y, 0);
+ try {
+ mInstrumentation.sendPointerSync(event);
+ } finally {
+ event.recycle();
+ event = null;
+ }
+ return downTime;
+ }
+
+ public long up(long downTime, float x, float y) {
+ return up(downTime, SystemClock.uptimeMillis(), x, y);
+ }
+
+ public long up(long downTime, long upTime, float x, float y) {
+ Log.d(LOGTAG, "Triggering up at (" + x + "," + y + ")");
+ MotionEvent event = MotionEvent.obtain(downTime, upTime, MotionEvent.ACTION_UP, mSurfaceOffsetX + x, mSurfaceOffsetY + y, 0);
+ try {
+ mInstrumentation.sendPointerSync(event);
+ } finally {
+ event.recycle();
+ event = null;
+ }
+ return -1L;
+ }
+
+ private long movePastTouchStartTolerance(float startX, float startY, float endX, float endY) {
+ long downTime = 0;
+ float eventDx = (endX - startX);
+ float eventDy = (endY - startY);
+ if (mApzEnabled && (mTouchStartTolerance > 0.0f) && (eventDx != 0 || eventDy !=0)) {
+ final float dragLength = (float)Math.sqrt((eventDx * eventDx) + (eventDy * eventDy));
+ final float extraDragLength = (float)Math.ceil(mTouchStartTolerance * mDpi);
+ final float extraDx = (eventDx / dragLength) * extraDragLength * (eventDx > 0.0f ? -1.0f : 1.0f);
+ final float extraDy = (eventDy / dragLength) * extraDragLength * (eventDy > 0.0f ? -1.0f : 1.0f);
+ downTime = down(startX + extraDx, startY + extraDy);
+ downTime = move(downTime, startX + extraDx, startY + extraDy);
+ try {
+ Thread.sleep(1000L / DRAG_EVENTS_PER_SECOND);
+ } catch (InterruptedException ie) {
+ ie.printStackTrace();
+ }
+ } else {
+ downTime = down(startX, startY);
+ }
+ return downTime;
+ }
+
+ public Thread dragAsync(final float startX, final float startY, final float endX, final float endY, final long durationMillis) {
+ Thread t = new Thread() {
+ @Override
+ public void run() {
+ layerView.setIsLongpressEnabled(false);
+
+ int numEvents = (int)(durationMillis * DRAG_EVENTS_PER_SECOND / 1000);
+ float eventDx = (endX - startX) / numEvents;
+ float eventDy = (endY - startY) / numEvents;
+ long downTime = movePastTouchStartTolerance(startX, startY, endX, endY);
+ for (int i = 0; i < numEvents - 1; i++) {
+ downTime = move(downTime, startX + (eventDx * i), startY + (eventDy * i));
+ try {
+ Thread.sleep(1000L / DRAG_EVENTS_PER_SECOND);
+ } catch (InterruptedException ie) {
+ ie.printStackTrace();
+ }
+ }
+ // sleep a bit before sending the last move so that the calculated
+ // fling velocity is low and we don't end up doing a fling afterwards.
+ try {
+ Thread.sleep(1000L);
+ } catch (InterruptedException ie) {
+ ie.printStackTrace();
+ }
+ // do the last one using endX/endY directly to avoid rounding errors
+ downTime = move(downTime, endX, endY);
+ downTime = up(downTime, endX, endY);
+
+ layerView.setIsLongpressEnabled(true);
+ }
+ };
+ t.start();
+ return t;
+ }
+
+ public void dragSync(float startX, float startY, float endX, float endY, long durationMillis) {
+ try {
+ dragAsync(startX, startY, endX, endY, durationMillis).join();
+ mInstrumentation.waitForIdleSync();
+ } catch (InterruptedException ie) {
+ ie.printStackTrace();
+ }
+ }
+
+ public void dragSync(float startX, float startY, float endX, float endY) {
+ dragSync(startX, startY, endX, endY, 1000);
+ }
+
+ public Thread flingAsync(final float startX, final float startY, final float endX, final float endY, final float velocity) {
+ // note that the first move after the touch-down is used to get over the panning threshold, and
+ // is basically cancelled out. this means we need to generate (at least) two move events, with
+ // the last move event hitting the target velocity. to do this we just slice the total distance
+ // in half, assuming the first half will get us over the panning threshold and the second half
+ // will trigger the fling.
+ final float dx = (endX - startX) / 2;
+ final float dy = (endY - startY) / 2;
+ float distance = (float) Math.sqrt((dx * dx) + (dy * dy));
+ final long time = (long)(distance / velocity);
+ if (time <= 0) {
+ throw new IllegalArgumentException( "Fling parameters require too small a time period" );
+ }
+ Thread t = new Thread() {
+ @Override
+ public void run() {
+ long downTime = down(startX, startY);
+ downTime = move(downTime, downTime + time, startX + dx, startY + dy);
+ downTime = move(downTime, downTime + time + time, endX, endY);
+ downTime = up(downTime, downTime + time + time + time, endX, endY);
+ }
+ };
+ t.start();
+ return t;
+ }
+
+ public void flingSync(float startX, float startY, float endX, float endY, float velocity) {
+ try {
+ flingAsync(startX, startY, endX, endY, velocity).join();
+ mInstrumentation.waitForIdleSync();
+ } catch (InterruptedException ie) {
+ ie.printStackTrace();
+ }
+ }
+
+ public void tap(float x, float y) {
+ long downTime = down(x, y);
+ downTime = up(downTime, x, y);
+ }
+
+ public void doubleTap(float x, float y) {
+ tap(x, y);
+ tap(x, y);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventReplayer.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventReplayer.java
new file mode 100644
index 0000000000..508c6b1979
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventReplayer.java
@@ -0,0 +1,224 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import android.app.Instrumentation;
+import android.os.Build;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.MotionEvent;
+
+class MotionEventReplayer {
+ private static final String LOGTAG = "RobocopMotionEventReplayer";
+
+ // the inner dimensions of the window on which the motion event capture was taken from
+ private static final int CAPTURE_WINDOW_WIDTH = 720;
+ private static final int CAPTURE_WINDOW_HEIGHT = 1038;
+
+ private final Instrumentation mInstrumentation;
+ private final int mSurfaceOffsetX;
+ private final int mSurfaceOffsetY;
+ private final int mSurfaceWidth;
+ private final int mSurfaceHeight;
+ private final Map<String, Integer> mActionTypes;
+ private Method mObtainNanoMethod;
+
+ public MotionEventReplayer(Instrumentation inst, int surfaceOffsetX, int surfaceOffsetY, int surfaceWidth, int surfaceHeight) {
+ mInstrumentation = inst;
+ mSurfaceOffsetX = surfaceOffsetX;
+ mSurfaceOffsetY = surfaceOffsetY;
+ mSurfaceWidth = surfaceWidth;
+ mSurfaceHeight = surfaceHeight;
+ Log.i(LOGTAG, "Initialized using offset (" + mSurfaceOffsetX + "," + mSurfaceOffsetY + ")");
+
+ mActionTypes = new HashMap<String, Integer>();
+ mActionTypes.put("ACTION_CANCEL", MotionEvent.ACTION_CANCEL);
+ mActionTypes.put("ACTION_DOWN", MotionEvent.ACTION_DOWN);
+ mActionTypes.put("ACTION_MOVE", MotionEvent.ACTION_MOVE);
+ mActionTypes.put("ACTION_POINTER_DOWN", MotionEvent.ACTION_POINTER_DOWN);
+ mActionTypes.put("ACTION_POINTER_UP", MotionEvent.ACTION_POINTER_UP);
+ mActionTypes.put("ACTION_UP", MotionEvent.ACTION_UP);
+ }
+
+ private int parseAction(String action) {
+ int index = 0;
+
+ // ACTION_POINTER_DOWN and ACTION_POINTER_UP might be followed by
+ // pointer index in parentheses, like ACTION_POINTER_UP(1)
+ int beginParen = action.indexOf("(");
+ if (beginParen >= 0) {
+ int endParen = action.indexOf(")", beginParen + 1);
+ index = Integer.parseInt(action.substring(beginParen + 1, endParen));
+ action = action.substring(0, beginParen);
+ }
+
+ return mActionTypes.get(action) | (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
+ }
+
+ private int parseInt(String value) {
+ if (value == null) {
+ return 0;
+ }
+ if (value.startsWith("0x")) {
+ return Integer.parseInt(value.substring(2), 16);
+ }
+ return Integer.parseInt(value);
+ }
+
+ private float scaleX(float value) {
+ return value * mSurfaceWidth / CAPTURE_WINDOW_WIDTH;
+ }
+
+ private float scaleY(float value) {
+ return value * mSurfaceHeight / CAPTURE_WINDOW_HEIGHT;
+ }
+
+ public void replayEvents(InputStream eventDescriptions)
+ throws IOException, IllegalAccessException, InvocationTargetException, NoSuchMethodException
+ {
+ // As an example, a line in the input stream might look like:
+ //
+ // MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=424.41055, y[0]=825.2412,
+ // toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0,
+ // edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=21972329,
+ // downTime=21972329, deviceId=6, source=0x1002 }
+ //
+ // These can be generated by printing out event.toString() in LayerView's
+ // onTouchEvent function on a phone running Ice Cream Sandwich. Different
+ // Android versions have different serializations of the motion event, and this
+ // code could probably be modified to parse other serializations if needed.
+ Pattern p = Pattern.compile("MotionEvent \\{ (.*?) \\}");
+ Map<String, String> eventProperties = new HashMap<String, String>();
+
+ boolean firstEvent = true;
+ long timeDelta = 0L;
+ long lastEventTime = 0L;
+
+ BufferedReader br = new BufferedReader(new InputStreamReader(eventDescriptions));
+ try {
+ for (String eventStr = br.readLine(); eventStr != null; eventStr = br.readLine()) {
+ Matcher m = p.matcher(eventStr);
+ if (! m.find()) {
+ // this line doesn't have any MotionEvent data, skip it
+ continue;
+ }
+
+ // extract the key-value pairs from the description and store them
+ // in the eventProperties table
+ StringTokenizer keyValues = new StringTokenizer(m.group(1), ",");
+ while (keyValues.hasMoreTokens()) {
+ String keyValue = keyValues.nextToken();
+ String key = keyValue.substring(0, keyValue.indexOf('=')).trim();
+ String value = keyValue.substring(keyValue.indexOf('=') + 1).trim();
+ eventProperties.put(key, value);
+ }
+
+ // set up the values we need to build the MotionEvent
+ long downTime = Long.parseLong(eventProperties.get("downTime"));
+ long eventTime = Long.parseLong(eventProperties.get("eventTime"));
+ int action = parseAction(eventProperties.get("action"));
+ float pressure = 1.0f;
+ float size = 1.0f;
+ int metaState = parseInt(eventProperties.get("metaState"));
+ float xPrecision = 1.0f;
+ float yPrecision = 1.0f;
+ int deviceId = 0;
+ int edgeFlags = parseInt(eventProperties.get("edgeFlags"));
+ int source = parseInt(eventProperties.get("source"));
+ int flags = parseInt(eventProperties.get("flags"));
+
+ int pointerCount = parseInt(eventProperties.get("pointerCount"));
+ int[] pointerIds = new int[pointerCount];
+ Object pointerData;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
+ MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount];
+ for (int i = 0; i < pointerCount; i++) {
+ pointerIds[i] = Integer.parseInt(eventProperties.get("id[" + i + "]"));
+ pointerCoords[i] = new MotionEvent.PointerCoords();
+ pointerCoords[i].x = mSurfaceOffsetX + scaleX(Float.parseFloat(eventProperties.get("x[" + i + "]")));
+ pointerCoords[i].y = mSurfaceOffsetY + scaleY(Float.parseFloat(eventProperties.get("y[" + i + "]")));
+ }
+ pointerData = pointerCoords;
+ } else {
+ // pre-gingerbread we have to use a hidden API to create the motion event, and we have
+ // to create a flattened list of floats rather than an array of PointerCoords
+ final int NUM_SAMPLE_DATA = 4; // MotionEvent.NUM_SAMPLE_DATA
+ final int SAMPLE_X = 0; // MotionEvent.SAMPLE_X
+ final int SAMPLE_Y = 1; // MotionEvent.SAMPLE_Y
+ float[] sampleData = new float[pointerCount * NUM_SAMPLE_DATA];
+ for (int i = 0; i < pointerCount; i++) {
+ pointerIds[i] = Integer.parseInt(eventProperties.get("id[" + i + "]"));
+ sampleData[(i * NUM_SAMPLE_DATA) + SAMPLE_X] =
+ mSurfaceOffsetX + scaleX(Float.parseFloat(eventProperties.get("x[" + i + "]")));
+ sampleData[(i * NUM_SAMPLE_DATA) + SAMPLE_Y] =
+ mSurfaceOffsetY + scaleY(Float.parseFloat(eventProperties.get("y[" + i + "]")));
+ }
+ pointerData = sampleData;
+ }
+
+ // we want to adjust the timestamps on all the generated events so that they line up with
+ // the time that this function is executing on-device.
+ long now = SystemClock.uptimeMillis();
+ if (firstEvent) {
+ timeDelta = now - eventTime;
+ firstEvent = false;
+ }
+ downTime += timeDelta;
+ eventTime += timeDelta;
+
+ // we also generate the events in "real-time" (i.e. have delays between events that
+ // correspond to the delays in the event timestamps).
+ if (now < eventTime) {
+ try {
+ Thread.sleep(eventTime - now);
+ } catch (InterruptedException ie) {
+ }
+ }
+
+ // and finally we dispatch the event
+ MotionEvent event;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
+ event = MotionEvent.obtain(downTime, eventTime, action, pointerCount,
+ pointerIds, (MotionEvent.PointerCoords[])pointerData, metaState,
+ xPrecision, yPrecision, deviceId, edgeFlags, source, flags);
+ } else {
+ // pre-gingerbread we have to use a hidden API to accomplish this
+ if (mObtainNanoMethod == null) {
+ mObtainNanoMethod = MotionEvent.class.getMethod("obtainNano", long.class,
+ long.class, long.class, int.class, int.class, pointerIds.getClass(),
+ pointerData.getClass(), int.class, float.class, float.class,
+ int.class, int.class);
+ }
+ event = (MotionEvent)mObtainNanoMethod.invoke(null, downTime, eventTime,
+ eventTime * 1000000, action, pointerCount, pointerIds, (float[])pointerData,
+ metaState, xPrecision, yPrecision, deviceId, edgeFlags);
+ }
+ try {
+ Log.v(LOGTAG, "Injecting " + event.toString());
+ mInstrumentation.sendPointerSync(event);
+ } finally {
+ event.recycle();
+ event = null;
+ }
+
+ eventProperties.clear();
+ }
+ } finally {
+ br.close();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/PixelTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/PixelTest.java
new file mode 100644
index 0000000000..a33ecf241f
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/PixelTest.java
@@ -0,0 +1,117 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.PaintedSurface;
+
+abstract class PixelTest extends BaseTest {
+ private static final long PAINT_CLEAR_DELAY = 10000; // milliseconds
+
+ protected final PaintedSurface loadAndGetPainted(String url) {
+ Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint();
+ loadUrlAndWait(url);
+ verifyHomePagerHidden();
+ paintExpecter.blockUntilClear(PAINT_CLEAR_DELAY);
+ paintExpecter.unregisterListener();
+ PaintedSurface p = mDriver.getPaintedSurface();
+ if (p == null) {
+ mAsserter.ok(p != null, "checking that painted surface loaded",
+ "painted surface loaded");
+ }
+ return p;
+ }
+
+ protected final void loadAndPaint(String url) {
+ PaintedSurface painted = loadAndGetPainted(url);
+ painted.close();
+ }
+
+ protected final PaintedSurface reloadAndGetPainted() {
+ Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint();
+
+ mActions.sendSpecialKey(Actions.SpecialKey.MENU);
+ waitForText(mStringHelper.RELOAD_LABEL);
+ mSolo.clickOnText(mStringHelper.RELOAD_LABEL);
+
+ paintExpecter.blockUntilClear(PAINT_CLEAR_DELAY);
+ paintExpecter.unregisterListener();
+ PaintedSurface p = mDriver.getPaintedSurface();
+ if (p == null) {
+ mAsserter.ok(p != null, "checking that painted surface loaded",
+ "painted surface loaded");
+ }
+ return p;
+ }
+
+ protected final void reloadAndPaint() {
+ PaintedSurface painted = reloadAndGetPainted();
+ painted.close();
+ }
+
+ protected final PaintedSurface waitForPaint(Actions.RepeatedEventExpecter expecter) {
+ expecter.blockUntilClear(PAINT_CLEAR_DELAY);
+ PaintedSurface p = mDriver.getPaintedSurface();
+ if (p == null) {
+ mAsserter.ok(p != null, "checking that painted surface loaded",
+ "painted surface loaded");
+ }
+ return p;
+ }
+
+ protected final PaintedSurface waitWithNoPaint(Actions.RepeatedEventExpecter expecter) {
+ try {
+ Thread.sleep(PAINT_CLEAR_DELAY);
+ } catch (InterruptedException ie) {
+ ie.printStackTrace();
+ }
+ mAsserter.is(expecter.eventReceived(), false, "Checking gecko didn't draw unnecessarily");
+ PaintedSurface p = mDriver.getPaintedSurface();
+ if (p == null) {
+ mAsserter.ok(p != null, "checking that painted surface loaded",
+ "painted surface loaded");
+ }
+ return p;
+ }
+
+ // this matches the algorithm in robocop_boxes.html
+ protected final int[] getBoxColorAt(int x, int y) {
+ int r = ((int)Math.floor(x / 3) % 256);
+ r = r & 0xF8;
+ int g = (x + y) % 256;
+ g = g & 0xFC;
+ int b = ((int)Math.floor(y / 3) % 256);
+ b = b & 0xF8;
+ return new int[] { r, g, b };
+ }
+
+ /**
+ * Checks the top-left corner of the visible area of the page is at (x,y) of robocop_boxes.html.
+ */
+ protected final void checkScrollWithBoxes(PaintedSurface painted, int x, int y) {
+ int[] color = getBoxColorAt(x, y);
+ mAsserter.ispixel(painted.getPixelAt(0, 0), color[0], color[1], color[2], "Pixel at 0, 0");
+ color = getBoxColorAt(x + 100, y);
+ mAsserter.ispixel(painted.getPixelAt(100, 0), color[0], color[1], color[2], "Pixel at 100, 0");
+ color = getBoxColorAt(x, y + 100);
+ mAsserter.ispixel(painted.getPixelAt(0, 100), color[0], color[1], color[2], "Pixel at 0, 100");
+ color = getBoxColorAt(x + 100, y + 100);
+ mAsserter.ispixel(painted.getPixelAt(100, 100), color[0], color[1], color[2], "Pixel at 100, 100");
+ }
+
+ /**
+ * Loads the robocop_boxes.html file and verifies that we are positioned at (0,0) on it.
+ * @param url URL of the robocop_boxes.html file.
+ * @return The painted surface after rendering the file.
+ */
+ protected final void loadAndVerifyBoxes(String url) {
+ PaintedSurface painted = loadAndGetPainted(url);
+ try {
+ checkScrollWithBoxes(painted, 0, 0);
+ } finally {
+ painted.close();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/SelectionHandlerTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/SelectionHandlerTest.java
new file mode 100644
index 0000000000..eb808b5425
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/SelectionHandlerTest.java
@@ -0,0 +1,56 @@
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+
+import android.util.Log;
+
+import org.json.JSONObject;
+
+/**
+ * A base test class for selection handler tests.
+ */
+abstract class SelectionHandlerTest extends UITest {
+ private static final String geckoEventString = "Robocop:testSelectionHandler";
+ private final String url;
+
+ public SelectionHandlerTest(String url) {
+ this.url = url;
+ }
+
+ public void testSelection() {
+ GeckoHelper.blockForReady();
+
+ Actions.EventExpecter robocopTestExpecter = getActions().expectGeckoEvent(geckoEventString);
+ NavigationHelper.enterAndLoadUrl(url);
+ mToolbar.assertTitle(url);
+
+ while (!test(robocopTestExpecter)) {
+ // do nothing
+ }
+
+ robocopTestExpecter.unregisterListener();
+ }
+
+ protected boolean test(Actions.EventExpecter expecter) {
+ final JSONObject eventData;
+ try {
+ eventData = new JSONObject(expecter.blockForEventData());
+ } catch(Exception ex) {
+ // Log and ignore
+ getAsserter().ok(false, "JS Test", "Error decoding data " + ex);
+ return false;
+ }
+
+ if (eventData.has("result")) {
+ getAsserter().ok(eventData.optBoolean("result"), "JS Test", eventData.optString("msg"));
+ } else if (eventData.has("todo")) {
+ getAsserter().todo(eventData.optBoolean("todo"), "JS TODO", eventData.optString("msg"));
+ }
+
+ EventDispatcher.sendResponse(eventData, new JSONObject());
+ return eventData.optBoolean("done", false);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/SessionTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/SessionTest.java
new file mode 100644
index 0000000000..e07a9750c5
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/SessionTest.java
@@ -0,0 +1,407 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.Assert;
+import org.mozilla.gecko.FennecMochitestAssert;
+
+public abstract class SessionTest extends BaseTest {
+ protected Navigation mNavigation;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mNavigation = new Navigation(mDevice);
+ }
+
+ /**
+ * A generic session object representing a collection of items that has a
+ * selected index.
+ */
+ protected abstract class SessionObject<T> {
+ private final int mIndex;
+ private final T[] mItems;
+
+ public SessionObject(int index, T... items) {
+ mIndex = index;
+ mItems = items;
+ }
+
+ public int getIndex() {
+ return mIndex;
+ }
+
+ public T[] getItems() {
+ return mItems;
+ }
+ }
+
+ protected class PageInfo {
+ private final String url;
+ private final String title;
+
+ public PageInfo(String key) {
+ if (key.startsWith("about:")) {
+ url = key;
+ } else {
+ url = getPage(key);
+ }
+ title = key;
+ }
+ }
+
+ protected class SessionTab extends SessionObject<PageInfo> {
+ public SessionTab(int index, PageInfo... items) {
+ super(index, items);
+ }
+ }
+
+ protected class Session extends SessionObject<SessionTab> {
+ public Session(int index, SessionTab... items) {
+ super(index, items);
+ }
+ }
+
+ /**
+ * Walker for visiting items in a browser-like navigation order.
+ */
+ protected abstract class NavigationWalker<T> {
+ private final T[] mItems;
+ private final int mIndex;
+
+ public NavigationWalker(SessionObject<T> obj) {
+ mItems = obj.getItems();
+ mIndex = obj.getIndex();
+ }
+
+ /**
+ * Walks over the list of items, calling the onItem() callback for each.
+ *
+ * The selected item is the first item visited. Each item after the
+ * selected item is then visited in ascending index order. Finally, the
+ * list is iterated in reverse, and each item before the selected item
+ * is visited in descending index order.
+ */
+ public void walk() {
+ onItem(mItems[mIndex], mIndex);
+ for (int i = mIndex + 1; i < mItems.length; i++) {
+ goForward();
+ onItem(mItems[i], i);
+ }
+ if (mIndex > 0) {
+ for (int i = mItems.length - 2; i >= 0; i--) {
+ goBack();
+ if (i < mIndex) {
+ onItem(mItems[i], i);
+ }
+ }
+ }
+ }
+
+ /**
+ * Callback when an item is visited during a walk.
+ *
+ * Only one callback is executed per item.
+ */
+ public abstract void onItem(T item, int currentIndex);
+
+ /**
+ * Callback executed for each back step of the walk.
+ */
+ public void goBack() {}
+
+ /**
+ * Callback executed for each forward step of the walk.
+ */
+ public void goForward() {}
+ }
+
+ /**
+ * Loads a set of tabs in the browser specified by the given session.
+ *
+ * @param session Session to load
+ */
+ protected void loadSessionTabs(Session session) {
+ // Verify initial about:home tab
+ verifyTabCount(1);
+ verifyUrl(mStringHelper.ABOUT_HOME_URL);
+
+ SessionTab[] tabs = session.getItems();
+ for (int i = 0; i < tabs.length; i++) {
+ final SessionTab tab = tabs[i];
+ final PageInfo[] pages = tab.getItems();
+
+ // New tabs always start with about:home, so make sure about:home
+ // is always the first entry.
+ mAsserter.is(pages[0].url, mStringHelper.ABOUT_HOME_URL, "first page in tab is " +
+ mStringHelper.ABOUT_HOME_URL);
+
+ // If this is the first tab, the tab already exists, so no need to
+ // create a new one. Otherwise, create a new tab if we're loading
+ // the first the first page in the set.
+ if (i > 0) {
+ addTab();
+ }
+
+ for (int j = 1; j < pages.length; j++) {
+ Actions.EventExpecter pageShowExpecter = mActions.expectGeckoEvent("Content:PageShow");
+
+ loadUrl(pages[j].url);
+
+ pageShowExpecter.blockForEvent();
+ pageShowExpecter.unregisterListener();
+ }
+
+ final int index = tab.getIndex();
+ for (int j = pages.length - 1; j > index; j--) {
+ mNavigation.back();
+ }
+ }
+
+ selectTabAt(session.getIndex());
+ }
+
+ /**
+ * Verifies that the set of open tabs matches the given session.
+ *
+ * @param session Session to verify
+ */
+ protected void verifySessionTabs(Session session) {
+ verifyTabCount(session.getItems().length);
+
+ (new NavigationWalker<SessionTab>(session) {
+ boolean mFirstTabVisited;
+
+ @Override
+ public void onItem(SessionTab tab, int currentIndex) {
+ // The first tab to check should already be selected at startup
+ if (mFirstTabVisited) {
+ selectTabAt(currentIndex);
+ } else {
+ mFirstTabVisited = true;
+ }
+
+ (new NavigationWalker<PageInfo>(tab) {
+ @Override
+ public void onItem(PageInfo page, int currentIndex) {
+ final String text;
+ if (mStringHelper.ABOUT_HOME_URL.equals(page.url)) {
+ text = mStringHelper.TITLE_PLACE_HOLDER;
+ } else if (page.url.startsWith(URL_HTTP_PREFIX)) {
+ text = page.url.substring(URL_HTTP_PREFIX.length());
+ } else {
+ text = page.url;
+ }
+ waitForText(text);
+
+ verifyUrlBarTitle(page.url);
+ }
+
+ @Override
+ public void goBack() {
+ mNavigation.back();
+ }
+
+ @Override
+ public void goForward() {
+ mNavigation.forward();
+ }
+ }).walk();
+ }
+ }).walk();
+ }
+
+ /**
+ * Gets session restore JSON corresponding to the open session.
+ *
+ * The JSON format follows the format used in Gecko for session restore and
+ * should be interchangeable with the Gecko's generated sessionstore.js.
+ *
+ * @param session Session to serialize
+ * @return JSON string of session
+ */
+ protected String buildSessionJSON(Session session) {
+ final SessionTab[] sessionTabs = session.getItems();
+ String sessionString = null;
+
+ try {
+ final JSONArray tabs = new JSONArray();
+
+ for (int i = 0; i < sessionTabs.length; i++) {
+ final JSONObject tab = new JSONObject();
+ final JSONArray entries = new JSONArray();
+ final SessionTab sessionTab = sessionTabs[i];
+ final PageInfo[] pages = sessionTab.getItems();
+
+ for (int j = 0; j < pages.length; j++) {
+ final PageInfo page = pages[j];
+ final JSONObject entry = new JSONObject();
+ entry.put("url", page.url);
+ entry.put("title", page.title);
+ entries.put(entry);
+ }
+
+ tab.put("entries", entries);
+ tab.put("index", sessionTab.getIndex() + 1);
+ tabs.put(tab);
+ }
+
+ JSONObject window = new JSONObject();
+ window.put("tabs", tabs);
+ window.put("selected", session.getIndex() + 1);
+ sessionString = new JSONObject().put("windows", new JSONArray().put(window)).toString();
+ } catch (JSONException e) {
+ mAsserter.ok(false, "JSON exception", getStackTraceString(e));
+ }
+
+ return sessionString;
+ }
+
+ /**
+ * @see SessionTest#verifySessionJSON(Session, String, Assert)
+ */
+ protected void verifySessionJSON(Session session, String sessionString) {
+ verifySessionJSON(session, sessionString, mAsserter);
+ }
+
+ /**
+ * Verifies a session JSON string against the given session.
+ *
+ * @param session Session to verify against
+ * @param sessionString JSON string to verify
+ * @param asserter Assert class to use during verification
+ */
+ protected void verifySessionJSON(Session session, String sessionString, Assert asserter) {
+ final SessionTab[] sessionTabs = session.getItems();
+
+ try {
+ final JSONObject window = new JSONObject(sessionString).getJSONArray("windows").getJSONObject(0);
+ final JSONArray tabs = window.getJSONArray("tabs");
+ final int optSelected = window.optInt("selected", -1);
+
+ asserter.is(optSelected, session.getIndex() + 1, "selected tab matches");
+
+ 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");
+ final SessionTab sessionTab = sessionTabs[i];
+ final PageInfo[] pages = sessionTab.getItems();
+
+ asserter.is(index, sessionTab.getIndex() + 1, "selected page index matches");
+
+ for (int j = 0; j < entries.length(); j++) {
+ final JSONObject entry = entries.getJSONObject(j);
+ final String url = entry.getString("url");
+ final String title = entry.optString("title");
+ final PageInfo page = pages[j];
+
+ asserter.is(url, page.url, "URL in JSON matches session URL");
+ if (!page.url.startsWith("about:")) {
+ asserter.is(title, page.title, "title in JSON matches session title");
+ }
+ }
+ }
+ } catch (JSONException e) {
+ asserter.ok(false, "JSON exception", getStackTraceString(e));
+ }
+ }
+
+ /**
+ * Exception thrown by NonFatalAsserter for assertion failures.
+ */
+ public static class AssertException extends RuntimeException {
+ public AssertException(String msg) {
+ super(msg);
+ }
+ }
+
+ /**
+ * Asserter that throws an AssertException on failure instead of aborting
+ * the test.
+ *
+ * This can be used in methods called via waitForCondition() where an assertion
+ * might not immediately succeed.
+ */
+ public class NonFatalAsserter extends FennecMochitestAssert {
+ @Override
+ public void ok(boolean condition, String name, String diag) {
+ if (!condition) {
+ String details = (diag == null ? "" : " | " + diag);
+ throw new AssertException("Assertion failed: " + name + details);
+ }
+ mAsserter.ok(condition, name, diag);
+ }
+ }
+
+ /**
+ * Gets a URL for a dynamically-generated page.
+ *
+ * The page will have a URL unique to the given ID, and the page's title
+ * will match the given ID.
+ *
+ * @param id ID used to generate page URL
+ * @return URL of the page
+ */
+ protected String getPage(String id) {
+ return getAbsoluteUrl("/robocop/robocop_dynamic.sjs?id=" + id);
+ }
+
+ protected String readProfileFile(String filename) {
+ try {
+ return readFile(new File(mProfile, filename));
+ } catch (IOException e) {
+ mAsserter.ok(false, "Error reading" + filename, getStackTraceString(e));
+ }
+ return null;
+ }
+
+ protected void writeProfileFile(String filename, String data) {
+ try {
+ writeFile(new File(mProfile, filename), data);
+ } catch (IOException e) {
+ mAsserter.ok(false, "Error writing to " + filename, getStackTraceString(e));
+ }
+ }
+
+ private String readFile(File target) throws IOException {
+ if (!target.exists()) {
+ return null;
+ }
+
+ FileReader fr = new FileReader(target);
+ try {
+ StringBuffer sb = new StringBuffer();
+ char[] buf = new char[8192];
+ int read = fr.read(buf);
+ while (read >= 0) {
+ sb.append(buf, 0, read);
+ read = fr.read(buf);
+ }
+ return sb.toString();
+ } finally {
+ fr.close();
+ }
+ }
+
+ private void writeFile(File target, String data) throws IOException {
+ FileWriter writer = new FileWriter(target);
+ try {
+ writer.write(data);
+ } finally {
+ writer.close();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/StringHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/StringHelper.java
new file mode 100644
index 0000000000..6f5db560d9
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/StringHelper.java
@@ -0,0 +1,401 @@
+/* -*- 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.tests;
+
+import android.content.res.Resources;
+
+import org.mozilla.gecko.R;
+
+public class StringHelper {
+ private static StringHelper instance;
+
+ // This needs to be accessed statically, before an instance of StringHelper can be created.
+ public static String STATIC_ABOUT_HOME_URL = "about:home";
+
+ public final String OK;
+ public final String CANCEL;
+ public final String CLEAR;
+
+ // Note: DEFAULT_BOOKMARKS_TITLES.length == DEFAULT_BOOKMARKS_URLS.length
+ public final String[] DEFAULT_BOOKMARKS_TITLES;
+ public final String[] DEFAULT_BOOKMARKS_URLS;
+ public final int DEFAULT_BOOKMARKS_COUNT;
+
+ // About pages
+ public final String ABOUT_BLANK_URL = "about:blank";
+ public final String ABOUT_FIREFOX_URL;
+ public final String ABOUT_HOME_URL = "about:home";
+ public final String ABOUT_ADDONS_URL = "about:addons";
+ public final String ABOUT_SCHEME = "about:";
+
+ // About pages' titles
+ public final String ABOUT_HOME_TITLE = "";
+
+ // Context Menu item strings
+ public final String CONTEXT_MENU_BOOKMARK_LINK = "Bookmark Link";
+ public final String CONTEXT_MENU_OPEN_LINK_IN_NEW_TAB = "Open Link in New Tab";
+ public final String CONTEXT_MENU_OPEN_IN_NEW_TAB;
+ public final String CONTEXT_MENU_OPEN_LINK_IN_PRIVATE_TAB = "Open Link in Private Tab";
+ public final String CONTEXT_MENU_OPEN_IN_PRIVATE_TAB;
+ public final String CONTEXT_MENU_COPY_LINK = "Copy Link";
+ public final String CONTEXT_MENU_SHARE_LINK = "Share Link";
+ public final String CONTEXT_MENU_EDIT;
+ public final String CONTEXT_MENU_SHARE;
+ public final String CONTEXT_MENU_REMOVE;
+ public final String CONTEXT_MENU_COPY_ADDRESS;
+ public final String CONTEXT_MENU_EDIT_SITE_SETTINGS;
+ public final String CONTEXT_MENU_SITE_SETTINGS_SAVE_PASSWORD = "Save Password";
+ public final String CONTEXT_MENU_ADD_TO_HOME_SCREEN;
+ public final String CONTEXT_MENU_PIN_SITE;
+ public final String CONTEXT_MENU_UNPIN_SITE;
+
+ // Context Menu menu items
+ public final String[] CONTEXT_MENU_ITEMS_IN_PRIVATE_TAB;
+
+ public final String[] CONTEXT_MENU_ITEMS_IN_NORMAL_TAB;
+
+ public final String[] BOOKMARK_CONTEXT_MENU_ITEMS;
+
+ public final String[] CONTEXT_MENU_ITEMS_IN_URL_BAR;
+
+ public final String TITLE_PLACE_HOLDER;
+
+ // Robocop page urls
+ // Note: please use getAbsoluteUrl(String url) on each robocop url to get the correct url
+ public final String ROBOCOP_BIG_LINK_URL = "/robocop/robocop_big_link.html";
+ public final String ROBOCOP_BIG_MAILTO_URL = "/robocop/robocop_big_mailto.html";
+ public final String ROBOCOP_BLANK_PAGE_01_URL = "/robocop/robocop_blank_01.html";
+ public final String ROBOCOP_BLANK_PAGE_02_URL = "/robocop/robocop_blank_02.html";
+ public final String ROBOCOP_BLANK_PAGE_03_URL = "/robocop/robocop_blank_03.html";
+ public final String ROBOCOP_BLANK_PAGE_04_URL = "/robocop/robocop_blank_04.html";
+ public final String ROBOCOP_BLANK_PAGE_05_URL = "/robocop/robocop_blank_05.html";
+ public final String ROBOCOP_BOXES_URL = "/robocop/robocop_boxes.html";
+ public final String ROBOCOP_GEOLOCATION_URL = "/robocop/robocop_geolocation.html";
+ public final String ROBOCOP_LOGIN_01_URL= "/robocop/robocop_login_01.html";
+ public final String ROBOCOP_LOGIN_02_URL= "/robocop/robocop_login_02.html";
+ public final String ROBOCOP_POPUP_URL = "/robocop/robocop_popup.html";
+ public final String ROBOCOP_OFFLINE_STORAGE_URL = "/robocop/robocop_offline_storage.html";
+ public final String ROBOCOP_PICTURE_LINK_URL = "/robocop/robocop_picture_link.html";
+ public final String ROBOCOP_SEARCH_URL = "/robocop/robocop_search.html";
+ public final String ROBOCOP_TEXT_PAGE_URL = "/robocop/robocop_text_page.html";
+ public final String ROBOCOP_ADOBE_FLASH_URL = "/robocop/robocop_adobe_flash.html";
+ public final String ROBOCOP_INPUT_URL = "/robocop/robocop_input.html";
+ public final String ROBOCOP_READER_MODE_BASIC_ARTICLE = "/robocop/reader_mode_pages/basic_article.html";
+ public final String ROBOCOP_LINK_TO_SLOW_LOADING = "/robocop/robocop_link_to_slow_loading.html";
+
+ private final String ROBOCOP_JS_HARNESS_URL = "/robocop/robocop_javascript.html";
+
+ // Robocop page images
+ public final String ROBOCOP_PICTURE_URL = "/robocop/Firefox.jpg";
+
+ // Robocop page titles
+ public final String ROBOCOP_BIG_LINK_TITLE = "Big Link";
+ public final String ROBOCOP_BIG_MAILTO_TITLE = "Big Mailto";
+ public final String ROBOCOP_BLANK_PAGE_01_TITLE = "Browser Blank Page 01";
+ public final String ROBOCOP_BLANK_PAGE_02_TITLE = "Browser Blank Page 02";
+ public final String ROBOCOP_GEOLOCATION_TITLE = "Geolocation Test Page";
+ public final String ROBOCOP_PICTURE_LINK_TITLE = "Picture Link";
+ public final String ROBOCOP_SEARCH_TITLE = "Robocop Search Engine";
+
+ // Distribution tile labels
+ public final String DISTRIBUTION1_LABEL = "Distribution 1";
+ public final String DISTRIBUTION2_LABEL = "Distribution 2";
+
+ // Settings menu strings
+ public final String PRIVACY_SECTION_LABEL;
+ public final String MOZILLA_SECTION_LABEL;
+
+ // Mozilla
+ public final String BRAND_NAME = "(Fennec|Nightly|Firefox Aurora|Firefox Beta|Firefox)";
+ public final String ABOUT_LABEL = "About " + BRAND_NAME ;
+ public final String LOCATION_SERVICES_LABEL = "Mozilla Location Service";
+
+ // Labels for the about:home tabs
+ public final String HISTORY_LABEL;
+ public final String TOP_SITES_LABEL;
+ public final String BOOKMARKS_LABEL;
+ public final String TODAY_LABEL;
+
+ // Desktop default bookmarks folders
+ public final String BOOKMARKS_UP_TO;
+ public final String BOOKMARKS_ROOT_LABEL;
+ public final String DESKTOP_FOLDER_LABEL;
+ public final String TOOLBAR_FOLDER_LABEL;
+ public final String BOOKMARKS_MENU_FOLDER_LABEL;
+ public final String UNSORTED_FOLDER_LABEL;
+
+ // Menu items - some of the items are found only on android 2.3 and lower and some only on android 3.0+
+ public final String NEW_TAB_LABEL;
+ public final String NEW_PRIVATE_TAB_LABEL;
+ public final String SHARE_LABEL;
+ public final String FIND_IN_PAGE_LABEL;
+ public final String DESKTOP_SITE_LABEL;
+ public final String PDF_LABEL;
+ public final String DOWNLOADS_LABEL;
+ public final String ADDONS_LABEL;
+ public final String LOGINS_LABEL;
+ public final String SETTINGS_LABEL;
+ public final String GUEST_MODE_LABEL;
+ public final String TAB_QUEUE_LABEL;
+ public final String TAB_QUEUE_SUMMARY;
+
+ // Android 3.0+
+ public final String TOOLS_LABEL;
+ public final String PAGE_LABEL;
+
+ // Android 2.3 and lower only
+ public final String MORE_LABEL = "More";
+ public final String RELOAD_LABEL;
+ public final String FORWARD_LABEL;
+ public final String BOOKMARK_LABEL;
+
+ // Bookmark Toast Notification
+ public final String BOOKMARK_ADDED_LABEL;
+ public final String BOOKMARK_REMOVED_LABEL;
+ public final String BOOKMARK_UPDATED_LABEL;
+ public final String BOOKMARK_OPTIONS_LABEL;
+
+ // Edit Bookmark screen
+ public final String EDIT_BOOKMARK;
+
+ // Strings used in doorhanger messages and buttons
+ public final String GEO_MESSAGE = "Share your location with";
+ public final String GEO_ALLOW;
+ public final String GEO_DENY = "Don't share";
+
+ public final String OFFLINE_MESSAGE = "to store data on your device for offline use";
+ public final String OFFLINE_ALLOW = "Allow";
+ public final String OFFLINE_DENY = "Don't allow";
+
+ public final String LOGIN_MESSAGE = "Would you like " + BRAND_NAME + " to remember this login?";
+ public final String LOGIN_ALLOW = "Remember";
+ public final String LOGIN_DENY = "Never";
+
+ public final String POPUP_MESSAGE = "prevented this site from opening";
+ public final String POPUP_ALLOW;
+ public final String POPUP_DENY = "Don't show";
+
+ // Strings used as content description, e.g. for ImageButtons
+ public final String CONTENT_DESCRIPTION_READER_MODE_BUTTON = "Enter Reader View";
+
+ // Home Panel Settings
+ public final String CUSTOMIZE_HOME;
+ public final String ENABLED;
+ public final String HISTORY;
+ public final String PANELS;
+
+ // Search Settings
+ public final String SEARCH_TITLE;
+ public final String SEARCH_SUGGESTIONS;
+ public final String SEARCH_INSTALLED;
+
+ // Advanced Settings
+ public final String ADVANCED;
+ public final String DONT_SHOW_MENU;
+ public final String SHOW_MENU;
+ public final String DISABLED;
+ public final String TAP_TO_PLAY;
+ public final String HIDE_TITLE_BAR;
+
+ // Update Settings
+ public final String AUTOMATIC_UPDATES;
+ public final String OVER_WIFI_OPTION;
+ public final String DOWNLOAD_UPDATES_AUTO;
+ public final String ALWAYS;
+ public final String NEVER;
+
+ // Restore Tabs Settings
+ public final String DONT_RESTORE_TABS;
+ public final String ALWAYS_RESTORE_TABS;
+ public final String DONT_RESTORE_QUIT;
+
+ private StringHelper(final Resources res) {
+
+ OK = res.getString(R.string.button_ok);
+ CANCEL = res.getString(R.string.button_cancel);
+ CLEAR = res.getString(R.string.button_clear);
+
+ // Note: DEFAULT_BOOKMARKS_TITLES.length == DEFAULT_BOOKMARKS_URLS.length
+ DEFAULT_BOOKMARKS_TITLES = new String[] {
+ res.getString(R.string.bookmarkdefaults_title_aboutfirefox),
+ res.getString(R.string.bookmarkdefaults_title_support),
+ res.getString(R.string.bookmarkdefaults_title_addons)
+ };
+ DEFAULT_BOOKMARKS_URLS = new String[] {
+ res.getString(R.string.bookmarkdefaults_url_aboutfirefox),
+ res.getString(R.string.bookmarkdefaults_url_support),
+ res.getString(R.string.bookmarkdefaults_url_addons)
+ };
+ DEFAULT_BOOKMARKS_COUNT = DEFAULT_BOOKMARKS_TITLES.length;
+
+ // About pages
+ ABOUT_FIREFOX_URL = res.getString(R.string.bookmarkdefaults_url_aboutfirefox);
+
+ // Context Menu item strings
+ CONTEXT_MENU_OPEN_IN_NEW_TAB = res.getString(R.string.contextmenu_open_new_tab);
+ CONTEXT_MENU_OPEN_IN_PRIVATE_TAB = res.getString(R.string.contextmenu_open_private_tab);
+ CONTEXT_MENU_EDIT = res.getString(R.string.contextmenu_top_sites_edit);
+ CONTEXT_MENU_SHARE = res.getString(R.string.contextmenu_share);
+ CONTEXT_MENU_REMOVE = res.getString(R.string.contextmenu_remove);
+ CONTEXT_MENU_COPY_ADDRESS = res.getString(R.string.contextmenu_copyurl);
+ CONTEXT_MENU_EDIT_SITE_SETTINGS = res.getString(R.string.contextmenu_site_settings);
+ CONTEXT_MENU_ADD_TO_HOME_SCREEN = res.getString(R.string.contextmenu_add_to_launcher);
+ CONTEXT_MENU_PIN_SITE = res.getString(R.string.contextmenu_top_sites_pin);
+ CONTEXT_MENU_UNPIN_SITE = res.getString(R.string.contextmenu_top_sites_unpin);
+
+ // Context Menu menu items
+ CONTEXT_MENU_ITEMS_IN_PRIVATE_TAB = new String[] {
+ CONTEXT_MENU_OPEN_LINK_IN_PRIVATE_TAB,
+ CONTEXT_MENU_COPY_LINK,
+ CONTEXT_MENU_SHARE_LINK,
+ CONTEXT_MENU_BOOKMARK_LINK
+ };
+
+ CONTEXT_MENU_ITEMS_IN_NORMAL_TAB = new String[] {
+ CONTEXT_MENU_OPEN_LINK_IN_NEW_TAB,
+ CONTEXT_MENU_OPEN_LINK_IN_PRIVATE_TAB,
+ CONTEXT_MENU_COPY_LINK,
+ CONTEXT_MENU_SHARE_LINK,
+ CONTEXT_MENU_BOOKMARK_LINK
+ };
+
+ BOOKMARK_CONTEXT_MENU_ITEMS = new String[] {
+ CONTEXT_MENU_OPEN_IN_NEW_TAB,
+ CONTEXT_MENU_OPEN_IN_PRIVATE_TAB,
+ CONTEXT_MENU_COPY_ADDRESS,
+ CONTEXT_MENU_SHARE,
+ CONTEXT_MENU_EDIT,
+ CONTEXT_MENU_REMOVE,
+ CONTEXT_MENU_ADD_TO_HOME_SCREEN
+ };
+
+ CONTEXT_MENU_ITEMS_IN_URL_BAR = new String[] {
+ CONTEXT_MENU_SHARE,
+ CONTEXT_MENU_COPY_ADDRESS,
+ CONTEXT_MENU_EDIT_SITE_SETTINGS,
+ CONTEXT_MENU_ADD_TO_HOME_SCREEN
+ };
+
+ TITLE_PLACE_HOLDER = res.getString(R.string.url_bar_default_text);
+
+ // Settings menu strings
+ PRIVACY_SECTION_LABEL = res.getString(R.string.pref_category_privacy_short);
+ MOZILLA_SECTION_LABEL = res.getString(R.string.pref_category_vendor);
+
+ // Labels for the about:home tabs
+ HISTORY_LABEL = res.getString(R.string.home_history_title);
+ TOP_SITES_LABEL = res.getString(R.string.home_top_sites_title);
+ BOOKMARKS_LABEL = res.getString(R.string.bookmarks_title);
+ TODAY_LABEL = res.getString(R.string.history_today_section);
+
+ BOOKMARKS_UP_TO = res.getString(R.string.home_move_back_to_filter);
+ BOOKMARKS_ROOT_LABEL = res.getString(R.string.bookmarks_title);
+ DESKTOP_FOLDER_LABEL = res.getString(R.string.bookmarks_folder_desktop);
+ TOOLBAR_FOLDER_LABEL = res.getString(R.string.bookmarks_folder_toolbar);
+ BOOKMARKS_MENU_FOLDER_LABEL = res.getString(R.string.bookmarks_folder_menu);
+ UNSORTED_FOLDER_LABEL = res.getString(R.string.bookmarks_folder_unfiled);
+
+ // Menu items - some of the items are found only on android 2.3 and lower and some only on android 3.0+
+ NEW_TAB_LABEL = res.getString(R.string.new_tab);
+ NEW_PRIVATE_TAB_LABEL = res.getString(R.string.new_private_tab);
+ SHARE_LABEL = res.getString(R.string.share);
+ FIND_IN_PAGE_LABEL = res.getString(R.string.find_in_page);
+ DESKTOP_SITE_LABEL = res.getString(R.string.desktop_mode);
+ PDF_LABEL = res.getString(R.string.save_as_pdf);
+ DOWNLOADS_LABEL = res.getString(R.string.downloads);
+ ADDONS_LABEL = res.getString(R.string.addons);
+ LOGINS_LABEL = res.getString(R.string.logins);
+ SETTINGS_LABEL = res.getString(R.string.settings);
+ GUEST_MODE_LABEL = res.getString(R.string.new_guest_session);
+ TAB_QUEUE_LABEL = res.getString(R.string.pref_tab_queue_title);
+ TAB_QUEUE_SUMMARY = res.getString(R.string.pref_tab_queue_summary);
+
+ // Android 3.0+
+ TOOLS_LABEL = res.getString(R.string.tools);
+ PAGE_LABEL = res.getString(R.string.page);
+
+ // Android 2.3 and lower only
+ RELOAD_LABEL = res.getString(R.string.reload);
+ FORWARD_LABEL = res.getString(R.string.forward);
+ BOOKMARK_LABEL = res.getString(R.string.bookmark);
+
+ // Bookmark Toast Notification
+ BOOKMARK_ADDED_LABEL = res.getString(R.string.bookmark_added);
+ BOOKMARK_REMOVED_LABEL = res.getString(R.string.bookmark_removed);
+ BOOKMARK_UPDATED_LABEL = res.getString(R.string.bookmark_updated);
+ BOOKMARK_OPTIONS_LABEL = res.getString(R.string.bookmark_options);
+
+ // Edit Bookmark screen
+ EDIT_BOOKMARK = res.getString(R.string.bookmark_edit_title);
+
+ // Strings used in doorhanger messages and buttons
+ GEO_ALLOW = res.getString(R.string.share);
+
+ POPUP_ALLOW = res.getString(R.string.pref_panels_show);
+
+ // Home Settings
+ PANELS = res.getString(R.string.pref_category_home_panels);
+ CUSTOMIZE_HOME = res.getString(R.string.pref_category_home);
+ ENABLED = res.getString(R.string.pref_home_updates_enabled);
+ HISTORY = res.getString(R.string.home_history_title);
+
+ // Search Settings
+ SEARCH_TITLE = res.getString(R.string.search);
+ SEARCH_SUGGESTIONS = res.getString(R.string.pref_search_suggestions);
+ SEARCH_INSTALLED = res.getString(R.string.pref_category_installed_search_engines);
+
+ // Advanced Settings
+ ADVANCED = res.getString(R.string.pref_category_advanced);
+ DONT_SHOW_MENU = res.getString(R.string.pref_char_encoding_off);
+ SHOW_MENU = res.getString(R.string.pref_char_encoding_on);
+ DISABLED = res.getString(R.string.pref_plugins_disabled );
+ TAP_TO_PLAY = res.getString(R.string.pref_plugins_tap_to_play);
+ HIDE_TITLE_BAR = res.getString(R.string.pref_scroll_title_bar_summary );
+
+ // Update Settings
+ AUTOMATIC_UPDATES = res.getString(R.string.pref_home_updates);
+ OVER_WIFI_OPTION = res.getString(R.string.pref_update_autodownload_wifi);
+ DOWNLOAD_UPDATES_AUTO = res.getString(R.string.pref_update_autodownload);
+ ALWAYS = res.getString(R.string.pref_update_autodownload_enabled);
+ NEVER = res.getString(R.string.pref_update_autodownload_disabled);
+
+ // Restore Tabs Settings
+ DONT_RESTORE_TABS = res.getString(R.string.pref_restore_quit);
+ ALWAYS_RESTORE_TABS = res.getString(R.string.pref_restore_always);
+ DONT_RESTORE_QUIT = res.getString(R.string.pref_restore_quit);
+ }
+
+ public static void initialize(Resources res) {
+ if (instance != null) {
+ throw new IllegalStateException(StringHelper.class.getSimpleName() + " already Initialized");
+ }
+ instance = new StringHelper(res);
+ }
+
+ public static StringHelper get() {
+ if (instance == null) {
+ throw new IllegalStateException(StringHelper.class.getSimpleName() + " instance is not yet initialized. Use StringHelper.initialize(Resources) first.");
+ }
+ return instance;
+ }
+
+ /**
+ * Build a URL for loading a Javascript file in the Robocop Javascript
+ * harness.
+ * <p>
+ * We append a random slug to avoid caching: see
+ * <a href="https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache">https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache</a>.
+ *
+ * @param javascriptUrl to load.
+ * @return URL with harness wrapper.
+ */
+ public String getHarnessUrlForJavascript(String javascriptUrl) {
+ // We include a slug to make sure we never cache the harness.
+ return ROBOCOP_JS_HARNESS_URL +
+ "?slug=" + System.currentTimeMillis() +
+ "&path=" + javascriptUrl;
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITest.java
new file mode 100644
index 0000000000..da952b5cb7
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITest.java
@@ -0,0 +1,203 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.Assert;
+import org.mozilla.gecko.Driver;
+import org.mozilla.gecko.tests.components.AboutHomeComponent;
+import org.mozilla.gecko.tests.components.AppMenuComponent;
+import org.mozilla.gecko.tests.components.BaseComponent;
+import org.mozilla.gecko.tests.components.GeckoViewComponent;
+import org.mozilla.gecko.tests.components.TabStripComponent;
+import org.mozilla.gecko.tests.components.ToolbarComponent;
+import org.mozilla.gecko.tests.helpers.HelperInitializer;
+
+import com.robotium.solo.Solo;
+
+/**
+ * A base test class for Robocop (UI-centric) tests. This and the related classes attempt to
+ * provide a framework to improve upon the issues discovered with the previous BaseTest
+ * implementation by providing simple test authorship and framework extension, consistency,
+ * and reliability.
+ *
+ * For documentation on writing tests and extending the framework, see
+ * https://wiki.mozilla.org/Mobile/Fennec/Android/UITest
+ */
+abstract class UITest extends BaseRobocopTest
+ implements UITestContext {
+
+ private static final String JUNIT_FAILURE_MSG = "A JUnit method was called. Make sure " +
+ "you are using AssertionHelper to make assertions. Try `fAssert*(...);`";
+
+ protected AboutHomeComponent mAboutHome;
+ protected AppMenuComponent mAppMenu;
+ protected GeckoViewComponent mGeckoView;
+ protected TabStripComponent mTabStrip;
+ protected ToolbarComponent mToolbar;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ // Helpers depend on components so initialize them first.
+ initComponents();
+ initHelpers();
+
+ // Ensure Robocop tests have access to network, and are run with Display powered on.
+ throwIfHttpGetFails();
+ throwIfScreenNotOn();
+ }
+
+ private void initComponents() {
+ mAboutHome = new AboutHomeComponent(this);
+ mAppMenu = new AppMenuComponent(this);
+ mGeckoView = new GeckoViewComponent(this);
+ mTabStrip = new TabStripComponent(this);
+ mToolbar = new ToolbarComponent(this);
+ }
+
+ private void initHelpers() {
+ HelperInitializer.init(this);
+ }
+
+ @Override
+ public Solo getSolo() {
+ return mSolo;
+ }
+
+ @Override
+ public Assert getAsserter() {
+ return mAsserter;
+ }
+
+ @Override
+ public Driver getDriver() {
+ return mDriver;
+ }
+
+ @Override
+ public Actions getActions() {
+ return mActions;
+ }
+
+ @Override
+ public StringHelper getStringHelper() {
+ return mStringHelper;
+ }
+
+ @Override
+ public void dumpLog(final String logtag, final String message) {
+ mAsserter.dumpLog(logtag + ": " + message);
+ }
+
+ @Override
+ public void dumpLog(final String logtag, final String message, final Throwable t) {
+ mAsserter.dumpLog(logtag + ": " + message, t);
+ }
+
+ @Override
+ public BaseComponent getComponent(final ComponentType type) {
+ switch (type) {
+ case ABOUTHOME:
+ return mAboutHome;
+
+ case APPMENU:
+ return mAppMenu;
+
+ case GECKOVIEW:
+ return mGeckoView;
+
+ case TOOLBAR:
+ return mToolbar;
+
+ default:
+ fail("Unknown component type, " + type + ".");
+ return null; // Should not reach this statement but required by javac.
+ }
+ }
+
+ /**
+ * Returns the test type. By default this returns MOCHITEST, but tests can override this
+ * method in order to change the type of the test.
+ */
+ @Override
+ protected Type getTestType() {
+ return Type.MOCHITEST;
+ }
+
+ @Override
+ public String getAbsoluteHostnameUrl(final String url) {
+ return getAbsoluteUrl(mBaseHostnameUrl, url);
+ }
+
+ @Override
+ public String getAbsoluteIpUrl(final String url) {
+ return getAbsoluteUrl(mBaseIpUrl, url);
+ }
+
+ private String getAbsoluteUrl(final String baseUrl, final String url) {
+ return baseUrl + "/" + url.replaceAll("(^/)", "");
+ }
+
+ /**
+ * Throws an Exception. Called from overridden JUnit methods to ensure JUnit assertions
+ * are not accidentally used over AssertionHelper assertions (the latter of which contains
+ * additional logging facilities for use in our test harnesses).
+ */
+ private static void junit() {
+ throw new UnsupportedOperationException(JUNIT_FAILURE_MSG);
+ }
+
+ // Note: inexplicably, javac does not think we're overriding these methods,
+ // so we can't use the @Override annotation.
+ public static void assertEquals(short e, short a) { junit(); }
+ public static void assertEquals(String m, int e, int a) { junit(); }
+ public static void assertEquals(String m, short e, short a) { junit(); }
+ public static void assertEquals(char e, char a) { junit(); }
+ public static void assertEquals(String m, String e, String a) { junit(); }
+ public static void assertEquals(int e, int a) { junit(); }
+ public static void assertEquals(String m, double e, double a, double delta) { junit(); }
+ public static void assertEquals(String m, long e, long a) { junit(); }
+ public static void assertEquals(byte e, byte a) { junit(); }
+ public static void assertEquals(Object e, Object a) { junit(); }
+ public static void assertEquals(boolean e, boolean a) { junit(); }
+ public static void assertEquals(String m, float e, float a, float delta) { junit(); }
+ public static void assertEquals(String m, boolean e, boolean a) { junit(); }
+ public static void assertEquals(String e, String a) { junit(); }
+ public static void assertEquals(float e, float a, float delta) { junit(); }
+ public static void assertEquals(String m, byte e, byte a) { junit(); }
+ public static void assertEquals(double e, double a, double delta) { junit(); }
+ public static void assertEquals(String m, char e, char a) { junit(); }
+ public static void assertEquals(String m, Object e, Object a) { junit(); }
+ public static void assertEquals(long e, long a) { junit(); }
+
+ public static void assertFalse(String m, boolean c) { junit(); }
+ public static void assertFalse(boolean c) { junit(); }
+
+ public static void assertNotNull(String m, Object o) { junit(); }
+ public static void assertNotNull(Object o) { junit(); }
+
+ public static void assertNotSame(Object e, Object a) { junit(); }
+ public static void assertNotSame(String m, Object e, Object a) { junit(); }
+
+ public static void assertNull(Object o) { junit(); }
+ public static void assertNull(String m, Object o) { junit(); }
+
+ public static void assertSame(Object e, Object a) { junit(); }
+ public static void assertSame(String m, Object e, Object a) { junit(); }
+
+ public static void assertTrue(String m, boolean c) { junit(); }
+ public static void assertTrue(boolean c) { junit(); }
+
+ public static void fail(String m) { junit(); }
+ public static void fail() { junit(); }
+
+ public static void failNotEquals(String m, Object e, Object a) { junit(); }
+ public static void failNotSame(String m, Object e, Object a) { junit(); }
+ public static void failSame(String m) { junit(); }
+
+ public static String format(String m, Object e, Object a) { junit(); return null; }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITestContext.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITestContext.java
new file mode 100644
index 0000000000..c825a20a4c
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITestContext.java
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.Assert;
+import org.mozilla.gecko.Driver;
+import org.mozilla.gecko.tests.components.BaseComponent;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+
+import com.robotium.solo.Solo;
+
+/**
+ * Interface to the global information about a UITest environment.
+ */
+public interface UITestContext {
+
+ public static enum ComponentType {
+ ABOUTHOME,
+ APPMENU,
+ GECKOVIEW,
+ TOOLBAR
+ }
+
+ public Activity getActivity();
+ public Solo getSolo();
+ public Assert getAsserter();
+ public Driver getDriver();
+ public Actions getActions();
+ public Instrumentation getInstrumentation();
+ public StringHelper getStringHelper();
+
+ public void dumpLog(final String logtag, final String message);
+ public void dumpLog(final String logtag, final String message, final Throwable t);
+
+ /**
+ * Returns the absolute version of the given URL using the host's hostname.
+ */
+ public String getAbsoluteHostnameUrl(final String url);
+
+ /**
+ * Returns the absolute version of the given URL using the host's IP address.
+ */
+ public String getAbsoluteIpUrl(final String url);
+
+ public BaseComponent getComponent(final ComponentType type);
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AboutHomeComponent.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AboutHomeComponent.java
new file mode 100644
index 0000000000..b12e0d23e0
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AboutHomeComponent.java
@@ -0,0 +1,193 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests.components;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.mozilla.gecko.AboutPages;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.home.HomeConfig.PanelType;
+import org.mozilla.gecko.tests.UITestContext;
+import org.mozilla.gecko.tests.helpers.WaitHelper;
+
+import android.os.Build;
+import android.support.v4.view.ViewPager;
+import android.view.View;
+import android.widget.TextView;
+
+import com.robotium.solo.Condition;
+import com.robotium.solo.Solo;
+
+/**
+ * A class representing any interactions that take place on the Awesomescreen.
+ */
+public class AboutHomeComponent extends BaseComponent {
+ private static final String LOGTAG = AboutHomeComponent.class.getSimpleName();
+
+ private static final List<PanelType> PANEL_ORDERING = Arrays.asList(
+ PanelType.TOP_SITES,
+ PanelType.BOOKMARKS,
+ PanelType.COMBINED_HISTORY
+ );
+
+ // The percentage of the panel to swipe between 0 and 1. This value was set through
+ // testing: 0.55f was tested on try and fails on armv6 devices.
+ private static final float SWIPE_PERCENTAGE = 0.70f;
+
+ public AboutHomeComponent(final UITestContext testContext) {
+ super(testContext);
+ }
+
+ private View getHomePagerContainer() {
+ return mSolo.getView(R.id.home_screen_container);
+ }
+
+ private ViewPager getHomePagerView() {
+ return (ViewPager) mSolo.getView(R.id.home_pager);
+ }
+
+ private View getHomeBannerView() {
+ if (mSolo.waitForView(R.id.home_banner)) {
+ return mSolo.getView(R.id.home_banner);
+ }
+ return null;
+ }
+
+ public AboutHomeComponent assertCurrentPanel(final PanelType expectedPanel) {
+ assertVisible();
+
+ final int expectedPanelIndex = PANEL_ORDERING.indexOf(expectedPanel);
+ fAssertEquals("The current HomePager panel is " + expectedPanel,
+ expectedPanelIndex, getHomePagerView().getCurrentItem());
+ return this;
+ }
+
+ public AboutHomeComponent assertNotVisible() {
+ fAssertTrue("The HomePager is not visible",
+ getHomePagerContainer().getVisibility() != View.VISIBLE ||
+ getHomePagerView().getVisibility() != View.VISIBLE);
+ return this;
+ }
+
+ public AboutHomeComponent assertVisible() {
+ fAssertTrue("The HomePager is visible",
+ getHomePagerContainer().getVisibility() == View.VISIBLE &&
+ getHomePagerView().getVisibility() == View.VISIBLE);
+ return this;
+ }
+
+ public AboutHomeComponent assertBannerNotVisible() {
+ View banner = getHomeBannerView();
+ if (Build.VERSION.SDK_INT >= 11) {
+ fAssertTrue("The HomeBanner is not visible",
+ getHomePagerContainer().getVisibility() != View.VISIBLE ||
+ banner == null ||
+ banner.getVisibility() != View.VISIBLE ||
+ banner.getTranslationY() == banner.getHeight());
+ } else {
+ // getTranslationY is not available before api 11.
+ // This check is a little less specific.
+ fAssertTrue("The HomeBanner is not visible",
+ getHomePagerContainer().getVisibility() != View.VISIBLE ||
+ banner == null ||
+ banner.isShown() == false);
+ }
+ return this;
+ }
+
+ public AboutHomeComponent assertBannerVisible() {
+ fAssertTrue("The HomeBanner is visible",
+ getHomePagerContainer().getVisibility() == View.VISIBLE &&
+ getHomeBannerView().getVisibility() == View.VISIBLE);
+ return this;
+ }
+
+ public AboutHomeComponent assertBannerText(String text) {
+ assertBannerVisible();
+
+ final TextView textView = (TextView) getHomeBannerView().findViewById(R.id.text);
+ fAssertEquals("The correct HomeBanner text is shown",
+ text, textView.getText().toString());
+ return this;
+ }
+
+ public AboutHomeComponent clickOnBanner() {
+ assertBannerVisible();
+
+ mTestContext.dumpLog(LOGTAG, "Clicking on HomeBanner.");
+ mSolo.clickOnView(getHomeBannerView());
+ return this;
+ }
+
+ public AboutHomeComponent dismissBanner() {
+ assertBannerVisible();
+
+ mTestContext.dumpLog(LOGTAG, "Clicking on HomeBanner close button.");
+ mSolo.clickOnView(getHomeBannerView().findViewById(R.id.close));
+ return this;
+ }
+
+ public AboutHomeComponent swipeToPanelOnRight() {
+ mTestContext.dumpLog(LOGTAG, "Swiping to the panel on the right.");
+ swipeToPanel(Solo.RIGHT);
+ return this;
+ }
+
+ public AboutHomeComponent swipeToPanelOnLeft() {
+ mTestContext.dumpLog(LOGTAG, "Swiping to the panel on the left.");
+ swipeToPanel(Solo.LEFT);
+ return this;
+ }
+
+ private void swipeToPanel(final int panelDirection) {
+ fAssertTrue("Swiping in a valid direction",
+ panelDirection == Solo.LEFT || panelDirection == Solo.RIGHT);
+ assertVisible();
+
+ final int panelIndex = getHomePagerView().getCurrentItem();
+
+ mSolo.scrollViewToSide(getHomePagerView(), panelDirection, SWIPE_PERCENTAGE);
+
+ // The panel on the left is a lower index and vice versa.
+ final int unboundedPanelIndex = panelIndex + (panelDirection == Solo.LEFT ? -1 : 1);
+ final int maxPanelIndex = PANEL_ORDERING.size() - 1;
+ final int expectedPanelIndex = Math.min(Math.max(0, unboundedPanelIndex), maxPanelIndex);
+
+ waitForPanelIndex(expectedPanelIndex);
+ }
+
+ private void waitForPanelIndex(final int expectedIndex) {
+ final String panelName = PANEL_ORDERING.get(expectedIndex).name();
+
+ WaitHelper.waitFor("HomePager " + panelName + " panel", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return (getHomePagerView().getCurrentItem() == expectedIndex);
+ }
+ });
+ }
+
+ /**
+ * Navigate directly to a built-in panel by its panel type.
+ * <p>
+ * If the panel type is not part of the active Home Panel configuration, the
+ * default about:home panel is displayed. If the panel type is not a
+ * built-in panel, an IllegalArgumentException is thrown.
+ *
+ * @param panelType to navigate to.
+ * @return self, for chaining.
+ */
+ public AboutHomeComponent navigateToBuiltinPanelType(PanelType panelType) throws IllegalArgumentException {
+ Tabs.getInstance().loadUrl(AboutPages.getURLForBuiltinPanelType(panelType));
+ final int expectedPanelIndex = PANEL_ORDERING.indexOf(panelType);
+ waitForPanelIndex(expectedPanelIndex);
+ return this;
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AppMenuComponent.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AppMenuComponent.java
new file mode 100644
index 0000000000..278cc7564c
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AppMenuComponent.java
@@ -0,0 +1,295 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests.components;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertFalse;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
+
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.menu.MenuItemActionBar;
+import org.mozilla.gecko.menu.MenuItemDefault;
+import org.mozilla.gecko.tests.UITestContext;
+import org.mozilla.gecko.tests.helpers.DeviceHelper;
+import org.mozilla.gecko.tests.helpers.RobotiumHelper;
+import org.mozilla.gecko.tests.helpers.WaitHelper;
+
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.RelativeLayout;
+
+import com.robotium.solo.Condition;
+import com.robotium.solo.RobotiumUtils;
+import com.robotium.solo.Solo;
+
+/**
+ * A class representing any interactions that take place on the app menu.
+ */
+public class AppMenuComponent extends BaseComponent {
+ private static final int MAX_WAITTIME_FOR_MENU_UPDATE_IN_MS = 7500;
+
+ public enum MenuItem {
+ FORWARD(R.string.forward),
+ NEW_TAB(R.string.new_tab),
+ PAGE(R.string.page),
+ RELOAD(R.string.reload);
+
+ private final int resourceID;
+ private String stringResource;
+
+ MenuItem(final int resourceID) {
+ this.resourceID = resourceID;
+ }
+
+ public String getString(final Solo solo) {
+ if (stringResource == null) {
+ stringResource = solo.getString(resourceID);
+ }
+
+ return stringResource;
+ }
+ };
+
+ public enum PageMenuItem {
+ SAVE_AS_PDF(R.string.save_as_pdf);
+
+ private static final MenuItem PARENT_MENU = MenuItem.PAGE;
+
+ private final int resourceID;
+ private String stringResource;
+
+ PageMenuItem(final int resourceID) {
+ this.resourceID = resourceID;
+ }
+
+ public String getString(final Solo solo) {
+ if (stringResource == null) {
+ stringResource = solo.getString(resourceID);
+ }
+
+ return stringResource;
+ }
+ };
+
+ public AppMenuComponent(final UITestContext testContext) {
+ super(testContext);
+ }
+
+ public void assertMenuIsOpen() {
+ fAssertTrue("Menu is open", isMenuOpen());
+ }
+
+ public void assertMenuIsNotOpen() {
+ fAssertFalse("Menu is not open", isMenuOpen());
+ }
+
+ public void assertMenuItemIsDisabledAndVisible(PageMenuItem pageMenuItem) {
+ openAppMenu();
+
+ // Non-legacy devices have hierarchical menu, check for parent menu item "page".
+ final View parentMenuItemView = findAppMenuItemView(MenuItem.PAGE.getString(mSolo));
+ if (parentMenuItemView.isEnabled()) {
+ fAssertTrue("The parent 'page' menu item is enabled", parentMenuItemView.isEnabled());
+ fAssertEquals("The parent 'page' menu item is visible", View.VISIBLE,
+ parentMenuItemView.getVisibility());
+
+ // Parent menu "page" is enabled, open page menu and check for menu item represented by pageMenuItem.
+ pressMenuItem(MenuItem.PAGE.getString(mSolo));
+
+ final View pageMenuItemView = findAppMenuItemView(pageMenuItem.getString(mSolo));
+ fAssertNotNull("The page menu item is not null", pageMenuItemView);
+ fAssertFalse("The page menu item is not enabled", pageMenuItemView.isEnabled());
+ fAssertEquals("The page menu item is visible", View.VISIBLE, pageMenuItemView.getVisibility());
+ } else {
+ fAssertFalse("The parent 'page' menu item is not enabled", parentMenuItemView.isEnabled());
+ fAssertEquals("The parent 'page' menu item is visible", View.VISIBLE, parentMenuItemView.getVisibility());
+ }
+ // Close the App Menu.
+ mSolo.goBack();
+ }
+
+ private View getOverflowMenuButtonView() {
+ return mSolo.getView(R.id.menu);
+ }
+
+ /**
+ * Try to find a MenuItemActionBar/MenuItemDefault with the given text set as contentDescription / text.
+ *
+ * When using legacy menus, make sure the menu has been opened to the appropriate level
+ * (i.e. base menu or "More" menu) to ensure the appropriate menu views are in memory.
+ * TODO: ^ Maybe we just need to have opened the "More" menu and the current one doesn't matter.
+ *
+ * This method is dependent on not having two views with equivalent contentDescription / text.
+ */
+ private View findAppMenuItemView(final String text) {
+ return WaitHelper.waitFor(String.format("menu item view '%s'", text), new Callable<View>() {
+ @Override
+ public View call() throws Exception {
+ final List<View> views = mSolo.getViews();
+
+ final List<MenuItemActionBar> menuItemActionBarList = RobotiumUtils.filterViews(MenuItemActionBar.class, views);
+ for (MenuItemActionBar menuItem : menuItemActionBarList) {
+ if (TextUtils.equals(menuItem.getContentDescription(), text)) {
+ return menuItem;
+ }
+ }
+
+ final List<MenuItemDefault> menuItemDefaultList = RobotiumUtils.filterViews(MenuItemDefault.class, views);
+ for (MenuItemDefault menuItem : menuItemDefaultList) {
+ if (TextUtils.equals(menuItem.getText(), text)) {
+ return menuItem;
+ }
+ }
+
+ // On Android 2.3, menu items may be instances of
+ // com.android.internal.view.menu.ListMenuItemView, each with a child
+ // android.widget.RelativeLayout which in turn has a child
+ // TextView with the appropriate text.
+ final List<TextView> textViewList = RobotiumUtils.filterViews(TextView.class, views);
+ for (TextView textView : textViewList) {
+ if (TextUtils.equals(textView.getText(), text)) {
+ View relativeLayout = (View) textView.getParent();
+ if (relativeLayout instanceof RelativeLayout) {
+ View listMenuItemView = (View)relativeLayout.getParent();
+ return listMenuItemView;
+ }
+ }
+ }
+ return null;
+ }
+ }, MAX_WAITTIME_FOR_MENU_UPDATE_IN_MS);
+ }
+
+ /**
+ * Helper function to let Robotium locate and click menu item from legacy Android menu (devices with Android 2.x).
+ *
+ * Robotium will also try to open the menu if there are no open dialog.
+ *
+ * @param menuItemTitle, The title of menu item to open.
+ */
+ private void pressLegacyMenuItem(final String menuItemTitle) {
+ mSolo.clickOnMenuItem(menuItemTitle, true);
+ }
+
+ private void pressMenuItem(final String menuItemTitle) {
+ // Wait for the menu item view to be enabled. This improves reliability on Android 2.3.
+ WaitHelper.waitFor(String.format("menu item %s to be enabled", menuItemTitle), new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ View v = findAppMenuItemView(menuItemTitle);
+ return (v != null) && v.isEnabled();
+ }
+ });
+
+ final View menuItemView = findAppMenuItemView(menuItemTitle);
+ fAssertTrue("Menu is open", isMenuOpen(menuItemView));
+
+ fAssertTrue(String.format("The menu item %s is enabled", menuItemTitle), menuItemView.isEnabled());
+ fAssertEquals(String.format("The menu item %s is visible", menuItemTitle), View.VISIBLE,
+ menuItemView.getVisibility());
+
+ mSolo.clickOnView(menuItemView);
+ }
+
+ private void pressSubMenuItem(final String parentMenuItemTitle, final String childMenuItemTitle) {
+ openAppMenu();
+
+ pressMenuItem(parentMenuItemTitle);
+
+ // Child menu item is not pressed yet, Click on it.
+ pressMenuItem(childMenuItemTitle);
+ }
+
+ public void pressMenuItem(MenuItem menuItem) {
+ openAppMenu();
+ pressMenuItem(menuItem.getString(mSolo));
+ }
+
+ public void pressMenuItem(final PageMenuItem pageMenuItem) {
+ pressSubMenuItem(PageMenuItem.PARENT_MENU.getString(mSolo), pageMenuItem.getString(mSolo));
+ }
+
+ private void openAppMenu() {
+ assertMenuIsNotOpen();
+
+ // This is a hack needed for tablets where the OverflowMenuButton is always in the GONE state,
+ // so we press the menu key instead.
+ if (DeviceHelper.isTablet()) {
+ mSolo.sendKey(Solo.MENU);
+ } else {
+ pressOverflowMenuButton();
+ }
+
+ waitForMenuOpen();
+ }
+
+ private void pressOverflowMenuButton() {
+ final View overflowMenuButton = getOverflowMenuButtonView();
+
+ fAssertTrue("The overflow menu button is enabled", overflowMenuButton.isEnabled());
+ fAssertEquals("The overflow menu button is visible", View.VISIBLE, overflowMenuButton.getVisibility());
+
+ mSolo.clickOnView(overflowMenuButton, true);
+ }
+
+ /**
+ * Determines whether the app menu is open by searching for items in the menu.
+ *
+ * @return true if app menu is open.
+ */
+ private boolean isMenuOpen() {
+ // We choose these options because New Tab is near the top of the menu and Page is near the middle/bottom.
+ // Intermittently, the menu doesn't scroll to top so we can't just use the first item in the list.
+ return isMenuOpen(MenuItem.NEW_TAB.getString(mSolo)) || isMenuOpen(MenuItem.PAGE.getString(mSolo));
+ }
+
+ /**
+ * Determines whether the app menu is open by searching for the text in menuItemTitle.
+ *
+ * @param menuItemTitle, The contentDescription of menu item to search.
+ *
+ * @return true if app menu is open.
+ */
+ private boolean isMenuOpen(String menuItemTitle) {
+ final View menuItemView = findAppMenuItemView(menuItemTitle);
+ return isMenuOpen(menuItemView) ? true : RobotiumHelper.searchExactText(menuItemTitle, true);
+ }
+
+ /**
+ * If a ListMenuItemView with menuItemTitle is visible then the app menu is open .
+ *
+ * @param menuItemView, must be a ListMenuItemView with menuItemTitle.
+ * You must use findAppMenuItemView(menuItemTitle) to obtain it.
+ *
+ * @return true if app menu is open.
+ */
+ private boolean isMenuOpen(View menuItemView) {
+ return (menuItemView != null) && (menuItemView.getVisibility() == View.VISIBLE);
+ }
+
+ public void waitForMenuOpen() {
+ WaitHelper.waitFor("menu to open", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return isMenuOpen();
+ }
+ });
+ }
+
+ public void waitForMenuClose() {
+ WaitHelper.waitFor("menu to close", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return !isMenuOpen();
+ }
+ });
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/BaseComponent.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/BaseComponent.java
new file mode 100644
index 0000000000..eadaaa173b
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/BaseComponent.java
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests.components;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.tests.StringHelper;
+import org.mozilla.gecko.tests.UITestContext;
+
+import android.app.Activity;
+
+import com.robotium.solo.Solo;
+
+/**
+ * A base class for constructing components - an abstraction over small bits of Firefox
+ * functionality. For example, the Toolbar or the about:home screen could be considered a
+ * component. Components should not need to know about each others existences and should be
+ * combined via helpers. Helpers can also handle a series of actions taken on one component
+ * (e.g. clicking the toolbar, entering a url, and waiting for page load).
+ */
+public abstract class BaseComponent {
+ protected final UITestContext mTestContext;
+ protected final Activity mActivity;
+ protected final Solo mSolo;
+ protected final Actions mActions;
+ protected final StringHelper mStringHelper;
+
+ public BaseComponent(final UITestContext testContext) {
+ mTestContext = testContext;
+ mActivity = mTestContext.getActivity();
+ mSolo = mTestContext.getSolo();
+ mActions = mTestContext.getActions();
+ mStringHelper = mTestContext.getStringHelper();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/GeckoViewComponent.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/GeckoViewComponent.java
new file mode 100644
index 0000000000..3beab3169a
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/GeckoViewComponent.java
@@ -0,0 +1,343 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests.components;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotSame;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertSame;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
+
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.tests.UITestContext;
+import org.mozilla.gecko.tests.helpers.FrameworkHelper;
+import org.mozilla.gecko.tests.helpers.WaitHelper;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodManager;
+
+import com.robotium.solo.Condition;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * A class representing any interactions that take place on GeckoView.
+ */
+public class GeckoViewComponent extends BaseComponent {
+
+ public final TextInput mTextInput;
+
+ public GeckoViewComponent(final UITestContext testContext) {
+ super(testContext);
+ mTextInput = new TextInput();
+ }
+
+ /**
+ * Returns the GeckoView.
+ */
+ private View getView() {
+ // Solo.getView asserts returning a valid View
+ return mSolo.getView(R.id.layer_view);
+ }
+
+ private void setContext(final Context newContext) {
+ final View geckoView = getView();
+ // Switch to a no-InputMethodManager context to avoid interference
+ mTestContext.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ FrameworkHelper.setViewContext(geckoView, newContext);
+ }
+ });
+ }
+
+ public static abstract class InputConnectionTest {
+ protected Handler inputConnectionHandler;
+
+ /**
+ * Processes pending events on the input connection thread before returning.
+ * Must be called on the input connection thread during a test.
+ */
+ protected void processInputConnectionEvents() {
+ fAssertSame("Should be called on input connection thread",
+ Looper.myLooper(), inputConnectionHandler.getLooper());
+
+ // Adapted from GeckoThread.pumpMessageLoop.
+ MessageQueue queue = Looper.myQueue();
+ queue.addIdleHandler(new MessageQueue.IdleHandler() {
+ @Override
+ public boolean queueIdle() {
+ final Message msg = Message.obtain(inputConnectionHandler);
+ msg.obj = inputConnectionHandler;
+ inputConnectionHandler.sendMessageAtFrontOfQueue(msg);
+ return false; // Remove this idle handler.
+ }
+ });
+
+ final Method getNextMessage;
+ try {
+ getNextMessage = queue.getClass().getDeclaredMethod("next");
+ } catch (final NoSuchMethodException e) {
+ throw new UnsupportedOperationException(e);
+ }
+ getNextMessage.setAccessible(true);
+
+ while (true) {
+ final Message msg;
+ try {
+ msg = (Message) getNextMessage.invoke(queue);
+ } catch (final IllegalAccessException | InvocationTargetException e) {
+ throw new UnsupportedOperationException(e);
+ }
+ if (msg.obj == inputConnectionHandler &&
+ msg.getTarget() == inputConnectionHandler) {
+ // Our idle signal
+ break;
+ } else if (msg.getTarget() == null) {
+ Looper.myLooper().quit();
+ break;
+ }
+ msg.getTarget().dispatchMessage(msg);
+ }
+ }
+
+ /**
+ * Processes pending events on the Gecko thread before returning.
+ * Must be called on the input connection thread during a test.
+ */
+ protected void processGeckoEvents() {
+ fAssertSame("Should be called on input connection thread",
+ Looper.myLooper(), inputConnectionHandler.getLooper());
+
+ GeckoThread.waitOnGecko();
+ }
+
+ private static ExtractedText getExtractedText(final InputConnection ic) {
+ final ExtractedTextRequest req = new ExtractedTextRequest();
+ return ic.getExtractedText(req, 0);
+ }
+
+ protected String getText(final InputConnection ic) {
+ return getExtractedText(ic).text.toString();
+ }
+
+ private static void assertText(final String message,
+ final String expected,
+ final String actual) {
+ // In an HTML editor, Gecko may insert an additional element that show up as a
+ // return character at the end. Deal with that here.
+ int end = actual.length();
+ if (end > 0 && actual.charAt(end - 1) == '\n') {
+ end--;
+ }
+ fAssertEquals(message, expected, actual.substring(0, end));
+ }
+
+ protected void assertText(final String message,
+ final InputConnection ic,
+ final String text) {
+ processGeckoEvents();
+ processInputConnectionEvents();
+
+ assertText(message, text, getText(ic));
+ }
+
+ protected void assertSelection(final String message,
+ final InputConnection ic,
+ final int start,
+ final int end) {
+ processGeckoEvents();
+ processInputConnectionEvents();
+
+ final ExtractedText extract = getExtractedText(ic);
+ fAssertEquals(message, start, extract.selectionStart);
+ fAssertEquals(message, end, extract.selectionEnd);
+ }
+
+ protected void assertSelectionAt(final String message,
+ final InputConnection ic,
+ final int value) {
+ assertSelection(message, ic, value, value);
+ }
+
+ protected void assertTextAndSelection(final String message,
+ final InputConnection ic,
+ final String text,
+ final int start,
+ final int end) {
+ processGeckoEvents();
+ processInputConnectionEvents();
+
+ final ExtractedText extract = getExtractedText(ic);
+ assertText(message, text, extract.text.toString());
+ fAssertEquals(message, start, extract.selectionStart);
+ fAssertEquals(message, end, extract.selectionEnd);
+ }
+
+ protected void assertTextAndSelectionAt(final String message,
+ final InputConnection ic,
+ final String text,
+ final int selection) {
+ assertTextAndSelection(message, ic, text, selection, selection);
+ }
+
+ public abstract void test(InputConnection ic, EditorInfo info);
+ }
+
+ public class TextInput {
+ private TextInput() {
+ }
+
+ private InputMethodManager getInputMethodManager() {
+ final InputMethodManager imm = (InputMethodManager)
+ mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
+ fAssertNotNull("Must have an InputMethodManager", imm);
+ return imm;
+ }
+
+ /**
+ * Returns whether text input is being directed to the GeckoView.
+ */
+ private boolean isActive() {
+ return getInputMethodManager().isActive(getView());
+ }
+
+ public TextInput assertActive() {
+ fAssertTrue("Current view should be the active input view", isActive());
+ return this;
+ }
+
+ public TextInput waitForActive() {
+ WaitHelper.waitFor("current view to become the active input view", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return isActive();
+ }
+ });
+ return this;
+ }
+
+ /**
+ * Returns whether an InputConnection is available.
+ * An InputConnection is available when text input is being directed to the
+ * GeckoView, and a text field (input, textarea, contentEditable, etc.) is
+ * currently focused inside the GeckoView.
+ */
+ private boolean hasInputConnection() {
+ final InputMethodManager imm = getInputMethodManager();
+ return imm.isActive(getView()) && imm.isAcceptingText();
+ }
+
+ public TextInput assertInputConnection() {
+ fAssertTrue("Current view should have an active InputConnection", hasInputConnection());
+ return this;
+ }
+
+ public TextInput waitForInputConnection() {
+ WaitHelper.waitFor("current view to have an active InputConnection", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return hasInputConnection();
+ }
+ });
+ return this;
+ }
+
+ /**
+ * Starts an InputConnectionTest. An InputConnectionTest must run on the
+ * InputConnection thread which may or may not be the main UI thread. Also,
+ * during an InputConnectionTest, the system InputMethodManager service must
+ * be temporarily disabled to prevent the system IME from interfering with our
+ * tests. We disable the service by override the GeckoView's context with one
+ * that returns a null InputMethodManager service.
+ *
+ * @param test Test to run
+ */
+ public TextInput testInputConnection(final InputConnectionTest test) {
+
+ fAssertNotNull("Test must not be null", test);
+ assertInputConnection();
+
+ // GeckoInputConnection can run on another thread than the main thread,
+ // so we need to be testing it on that same thread it's running on
+ final View geckoView = getView();
+ final Handler inputConnectionHandler = geckoView.getHandler();
+ final Context oldGeckoViewContext = FrameworkHelper.getViewContext(geckoView);
+
+ setContext(new ContextWrapper(oldGeckoViewContext) {
+ @Override
+ public Object getSystemService(String name) {
+ if (Context.INPUT_METHOD_SERVICE.equals(name)) {
+ return null;
+ }
+ return super.getSystemService(name);
+ }
+ });
+
+ (new InputConnectionTestRunner(test, inputConnectionHandler)).launch();
+
+ setContext(oldGeckoViewContext);
+ return this;
+ }
+
+ private class InputConnectionTestRunner implements Runnable {
+ private final InputConnectionTest mTest;
+ private boolean mDone;
+
+ public InputConnectionTestRunner(final InputConnectionTest test,
+ final Handler handler) {
+ test.inputConnectionHandler = handler;
+ mTest = test;
+ }
+
+ public synchronized void launch() {
+ // Below, we are blocking the instrumentation thread to wait on the
+ // InputConnection thread. Therefore, the InputConnection thread must not be
+ // the same as the instrumentation thread to avoid a deadlock. This should
+ // always be the case and we perform a sanity check to make sure.
+ fAssertNotSame("InputConnection should not be running on instrumentation thread",
+ Looper.myLooper(), mTest.inputConnectionHandler.getLooper());
+
+ mDone = false;
+ mTest.inputConnectionHandler.post(this);
+ do {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ // Ignore interrupts
+ }
+ } while (!mDone);
+ }
+
+ @Override
+ public void run() {
+ final EditorInfo info = new EditorInfo();
+ final InputConnection ic = getView().onCreateInputConnection(info);
+ fAssertNotNull("Must have an InputConnection", ic);
+ // Restore the IC to a clean state
+ ic.clearMetaKeyStates(-1);
+ ic.finishComposingText();
+ mTest.test(ic, info);
+ synchronized (this) {
+ // Test finished; return from launch().
+ mDone = true;
+ notify();
+ }
+ }
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/TabStripComponent.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/TabStripComponent.java
new file mode 100644
index 0000000000..e8a90b3515
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/TabStripComponent.java
@@ -0,0 +1,56 @@
+package org.mozilla.gecko.tests.components;
+
+import android.view.View;
+
+import com.robotium.solo.Condition;
+
+import org.mozilla.gecko.tests.UITestContext;
+import org.mozilla.gecko.tests.helpers.DeviceHelper;
+import org.mozilla.gecko.tests.helpers.WaitHelper;
+import org.mozilla.gecko.widget.TwoWayView;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.*;
+
+/**
+ * A class representing any interactions that take place on the tablet tab strip.
+ */
+public class TabStripComponent extends BaseComponent {
+ // Using a text id because the layout and therefore the id might be stripped from the (non-tablet) build
+ private static final String TAB_STRIP_ID = "tab_strip";
+
+ public TabStripComponent(final UITestContext testContext) {
+ super(testContext);
+ }
+
+ public void switchToTab(int index) {
+ // The tab strip is only available on tablets
+ DeviceHelper.assertIsTablet();
+
+ View tabView = waitForTabView(index);
+ fAssertNotNull(String.format("Tab at index %d is not null", index), tabView);
+
+ mSolo.clickOnView(tabView);
+ }
+
+ private View waitForTabView(final int index) {
+ final TwoWayView tabStrip = getTabStripView();
+ final View[] tabView = new View[1];
+
+ WaitHelper.waitFor(String.format("Tab at index %d to be visible", index), new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return (tabView[0] = tabStrip.getChildAt(index)) != null;
+ }
+ });
+
+ return tabView[0];
+ }
+
+ private TwoWayView getTabStripView() {
+ TwoWayView tabStrip = (TwoWayView) mSolo.getView("tab_strip");
+
+ fAssertNotNull("Tab strip is not null", tabStrip);
+
+ return tabStrip;
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/ToolbarComponent.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/ToolbarComponent.java
new file mode 100644
index 0000000000..25101a3953
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/ToolbarComponent.java
@@ -0,0 +1,326 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests.components;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertFalse;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.tests.UITestContext;
+import org.mozilla.gecko.tests.helpers.DeviceHelper;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+import org.mozilla.gecko.tests.helpers.WaitHelper;
+import org.mozilla.gecko.toolbar.PageActionLayout;
+
+import android.view.View;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.TextView;
+
+import com.robotium.solo.Condition;
+import com.robotium.solo.Solo;
+
+/**
+ * A class representing any interactions that take place on the Toolbar.
+ */
+public class ToolbarComponent extends BaseComponent {
+
+ private static final String URL_HTTP_PREFIX = "http://";
+
+ // We are waiting up to 30 seconds instead of the default waiting time because reader mode
+ // parsing can take quite some time on slower devices (Bug 1142699)
+ private static final int READER_MODE_WAIT_MS = 30000;
+
+ public ToolbarComponent(final UITestContext testContext) {
+ super(testContext);
+ }
+
+ public ToolbarComponent assertIsEditing() {
+ fAssertTrue("The toolbar is in the editing state", isEditing());
+ return this;
+ }
+
+ public ToolbarComponent assertIsNotEditing() {
+ fAssertFalse("The toolbar is not in the editing state", isEditing());
+ return this;
+ }
+
+ public ToolbarComponent assertTitle(final String url) {
+ fAssertNotNull("The url argument is not null", url);
+
+ final String expected;
+ final String absoluteURL = NavigationHelper.adjustUrl(url);
+
+ if (mStringHelper.ABOUT_HOME_URL.equals(absoluteURL)) {
+ expected = mStringHelper.ABOUT_HOME_TITLE;
+ } else if (absoluteURL.startsWith(URL_HTTP_PREFIX)) {
+ expected = absoluteURL.substring(URL_HTTP_PREFIX.length());
+ } else {
+ expected = absoluteURL;
+ }
+
+ // Since we only display a shortened "base domain" (See bug 1236431) we use the content
+ // description to obtain the full URL.
+ fAssertEquals("The Toolbar title is " + expected, expected, getUrlFromContentDescription());
+ return this;
+ }
+
+ public ToolbarComponent assertUrl(final String expected) {
+ assertIsEditing();
+ fAssertEquals("The Toolbar url is " + expected, expected, getUrlEditText().getText());
+ return this;
+ }
+
+ public ToolbarComponent assertIsUrlEditTextSelected() {
+ fAssertTrue("The edit text is selected", isUrlEditTextSelected());
+ return this;
+ }
+
+ public ToolbarComponent assertIsUrlEditTextNotSelected() {
+ fAssertFalse("The edit text is not selected", isUrlEditTextSelected());
+ return this;
+ }
+
+ public ToolbarComponent assertBackButtonIsNotEnabled() {
+ fAssertFalse("The back button is not enabled", isBackButtonEnabled());
+ return this;
+ }
+
+ /**
+ * Returns the root View for the browser toolbar.
+ */
+ private View getToolbarView() {
+ mSolo.waitForView(R.id.browser_toolbar);
+ return mSolo.getView(R.id.browser_toolbar);
+ }
+
+ private EditText getUrlEditText() {
+ return (EditText) getToolbarView().findViewById(R.id.url_edit_text);
+ }
+
+ private View getUrlDisplayLayout() {
+ return getToolbarView().findViewById(R.id.display_layout);
+ }
+
+ private TextView getUrlTitleText() {
+ return (TextView) getToolbarView().findViewById(R.id.url_bar_title);
+ }
+
+ private ImageButton getBackButton() {
+ DeviceHelper.assertIsTablet();
+ return (ImageButton) getToolbarView().findViewById(R.id.back);
+ }
+
+ private ImageButton getForwardButton() {
+ DeviceHelper.assertIsTablet();
+ return (ImageButton) getToolbarView().findViewById(R.id.forward);
+ }
+
+ private ImageButton getReloadButton() {
+ DeviceHelper.assertIsTablet();
+ return (ImageButton) getToolbarView().findViewById(R.id.reload);
+ }
+
+ private PageActionLayout getPageActionLayout() {
+ return (PageActionLayout) getToolbarView().findViewById(R.id.page_action_layout);
+ }
+
+ private ImageButton getReaderModeButton() {
+ final PageActionLayout pageActionLayout = getPageActionLayout();
+ final int count = pageActionLayout.getChildCount();
+
+ for (int i = 0; i < count; i++) {
+ final View view = pageActionLayout.getChildAt(i);
+ if (mStringHelper.CONTENT_DESCRIPTION_READER_MODE_BUTTON.equals(view.getContentDescription())) {
+ return (ImageButton) view;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the View for the edit cancel button in the browser toolbar.
+ */
+ private View getEditCancelButton() {
+ return getToolbarView().findViewById(R.id.edit_cancel);
+ }
+
+ private String getUrlFromContentDescription() {
+ assertIsNotEditing();
+
+ final CharSequence contentDescription = getUrlDisplayLayout().getContentDescription();
+ if (contentDescription == null) {
+ return "";
+ } else {
+ return contentDescription.toString();
+ }
+ }
+
+ /**
+ * Returns the title of the page. Note that this makes no assertions to Toolbar state and
+ * may return a value that may never be visible to the user. Callers likely want to use
+ * {@link assertTitle} instead.
+ */
+ public String getPotentiallyInconsistentTitle() {
+ return getTitleHelper(false);
+ }
+
+ private String getTitleHelper(final boolean shouldAssertNotEditing) {
+ if (shouldAssertNotEditing) {
+ assertIsNotEditing();
+ }
+
+ return getUrlTitleText().getText().toString();
+ }
+
+ private boolean isEditing() {
+ return getUrlDisplayLayout().getVisibility() != View.VISIBLE &&
+ getUrlEditText().getVisibility() == View.VISIBLE;
+ }
+
+ public ToolbarComponent enterEditingMode() {
+ assertIsNotEditing();
+
+ mSolo.clickOnView(getUrlTitleText(), true);
+
+ waitForEditing();
+ WaitHelper.waitFor("UrlEditText to be input method target", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return getUrlEditText().isInputMethodTarget();
+ }
+ });
+
+ return this;
+ }
+
+ public ToolbarComponent commitEditingMode() {
+ assertIsEditing();
+
+ WaitHelper.waitForPageLoad(new Runnable() {
+ @Override
+ public void run() {
+ mSolo.sendKey(Solo.ENTER);
+ }
+ });
+ waitForNotEditing();
+
+ return this;
+ }
+
+ public ToolbarComponent dismissEditingMode() {
+ assertIsEditing();
+
+ if (DeviceHelper.isTablet()) {
+ final EditText urlEditText = getUrlEditText();
+ if (urlEditText.isFocused()) {
+ mSolo.goBack();
+ }
+ mSolo.goBack();
+ } else {
+ mSolo.clickOnView(getEditCancelButton());
+ }
+
+ waitForNotEditing();
+
+ return this;
+ }
+
+ public ToolbarComponent enterUrl(final String url) {
+ fAssertNotNull("url is not null", url);
+
+ assertIsEditing();
+
+ final EditText urlEditText = getUrlEditText();
+ fAssertTrue("The UrlEditText is the input method target",
+ urlEditText.isInputMethodTarget());
+
+ mSolo.clearEditText(urlEditText);
+ mSolo.typeText(urlEditText, url);
+
+ return this;
+ }
+
+ public ToolbarComponent pressBackButton() {
+ final ImageButton backButton = getBackButton();
+ return pressButton(backButton, "back");
+ }
+
+ public ToolbarComponent pressForwardButton() {
+ final ImageButton forwardButton = getForwardButton();
+ return pressButton(forwardButton, "forward");
+ }
+
+ public ToolbarComponent pressReloadButton() {
+ final ImageButton reloadButton = getReloadButton();
+ return pressButton(reloadButton, "reload");
+ }
+
+ public ToolbarComponent pressReaderModeButton() {
+ final ImageButton readerModeButton = waitForReaderModeButton();
+ pressButton(readerModeButton, "reader mode");
+
+ return this;
+ }
+
+ private ToolbarComponent pressButton(final View view, final String buttonName) {
+ fAssertNotNull("The " + buttonName + " button View is not null", view);
+ fAssertTrue("The " + buttonName + " button is enabled", view.isEnabled());
+ fAssertEquals("The " + buttonName + " button is visible",
+ View.VISIBLE, view.getVisibility());
+ assertIsNotEditing();
+
+ WaitHelper.waitForPageLoad(new Runnable() {
+ @Override
+ public void run() {
+ mSolo.clickOnView(view);
+ }
+ });
+
+ return this;
+ }
+
+ private void waitForEditing() {
+ WaitHelper.waitFor("Toolbar to enter editing mode", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return isEditing();
+ }
+ });
+ }
+
+ private void waitForNotEditing() {
+ WaitHelper.waitFor("Toolbar to exit editing mode", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return !isEditing();
+ }
+ });
+ }
+
+ private ImageButton waitForReaderModeButton() {
+ final ImageButton[] readerModeButton = new ImageButton[1];
+
+ WaitHelper.waitFor("the Reader mode button to be visible", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return (readerModeButton[0] = getReaderModeButton()) != null;
+ }
+ }, READER_MODE_WAIT_MS);
+
+ return readerModeButton[0];
+ }
+
+ private boolean isUrlEditTextSelected() {
+ return getUrlEditText().isSelected();
+ }
+
+ private boolean isBackButtonEnabled() {
+ return getBackButton().isEnabled();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/AssertionHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/AssertionHelper.java
new file mode 100644
index 0000000000..894d134d1b
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/AssertionHelper.java
@@ -0,0 +1,112 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests.helpers;
+
+import java.util.Arrays;
+
+import org.mozilla.gecko.Assert;
+import org.mozilla.gecko.tests.UITestContext;
+
+/**
+ * Provides assertions in a JUnit-like API that wraps the robocop Assert interface.
+ */
+public final class AssertionHelper {
+ // Assert.ok has a "diag" ("diagnostic") parameter that has no useful purpose.
+ private static final String DIAG_STRING = "";
+
+ private static Assert sAsserter;
+
+ private AssertionHelper() { /* To disallow instantiation. */ }
+
+ protected static void init(final UITestContext context) {
+ sAsserter = context.getAsserter();
+ }
+
+ public static void fAssertArrayEquals(final String message, final byte[] expecteds, final byte[] actuals) {
+ sAsserter.ok(Arrays.equals(expecteds, actuals), message, DIAG_STRING);
+ }
+
+ public static void fAssertArrayEquals(final String message, final char[] expecteds, final char[] actuals) {
+ sAsserter.ok(Arrays.equals(expecteds, actuals), message, DIAG_STRING);
+ }
+
+ public static void fAssertArrayEquals(final String message, final short[] expecteds, final short[] actuals) {
+ sAsserter.ok(Arrays.equals(expecteds, actuals), message, DIAG_STRING);
+ }
+
+ public static void fAssertArrayEquals(final String message, final int[] expecteds, final int[] actuals) {
+ sAsserter.ok(Arrays.equals(expecteds, actuals), message, DIAG_STRING);
+ }
+
+ public static void fAssertArrayEquals(final String message, final long[] expecteds, final long[] actuals) {
+ sAsserter.ok(Arrays.equals(expecteds, actuals), message, DIAG_STRING);
+ }
+
+ public static void fAssertArrayEquals(final String message, final Object[] expecteds, final Object[] actuals) {
+ sAsserter.ok(Arrays.equals(expecteds, actuals), message, DIAG_STRING);
+ }
+
+ public static void fAssertEquals(final String message, final double expected, final double actual, final double delta) {
+ if (Double.compare(expected, actual) != 0) {
+ sAsserter.ok(Math.abs(expected - actual) <= delta, message, DIAG_STRING);
+ }
+ }
+
+ public static void fAssertEquals(final String message, final long expected, final long actual) {
+ sAsserter.is(actual, expected, message);
+ }
+
+ public static void fAssertEquals(final String message, final Object expected, final Object actual) {
+ sAsserter.is(actual, expected, message);
+ }
+
+ public static void fAssertNotEquals(final String message, final double unexpected, final double actual, final double delta) {
+ sAsserter.ok(Math.abs(unexpected - actual) > delta, message, DIAG_STRING);
+ }
+
+ public static void fAssertNotEquals(final String message, final long unexpected, final long actual) {
+ sAsserter.isnot(actual, unexpected, message);
+ }
+
+ public static void fAssertNotEquals(final String message, final Object unexpected, final Object actual) {
+ sAsserter.isnot(actual, unexpected, message);
+ }
+
+ public static void fAssertFalse(final String message, final boolean actual) {
+ sAsserter.ok(!actual, message, DIAG_STRING);
+ }
+
+ public static void fAssertNotNull(final String message, final Object actual) {
+ sAsserter.isnot(actual, null, message);
+ }
+
+ public static void fAssertNotSame(final String message, final Object unexpected, final Object actual) {
+ sAsserter.ok(unexpected != actual, message, DIAG_STRING);
+ }
+
+ public static void fAssertNull(final String message, final Object actual) {
+ sAsserter.is(actual, null, message);
+ }
+
+ public static void fAssertSame(final String message, final Object expected, final Object actual) {
+ sAsserter.ok(expected == actual, message, DIAG_STRING);
+ }
+
+ public static void fAssertTrue(final String message, final boolean actual) {
+ sAsserter.ok(actual, message, DIAG_STRING);
+ }
+
+ public static void fAssertIsPixel(final String message, final int actual, final int r, final int g, final int b) {
+ sAsserter.ispixel(actual, r, g, b, message);
+ }
+
+ public static void fAssertIsNotPixel(final String message, final int actual, final int r, final int g, final int b) {
+ sAsserter.isnotpixel(actual, r, g, b, message);
+ }
+
+ public static void fFail(final String message) {
+ sAsserter.ok(false, message, DIAG_STRING);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/DeviceHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/DeviceHelper.java
new file mode 100644
index 0000000000..476bd34dda
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/DeviceHelper.java
@@ -0,0 +1,108 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests.helpers;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.tests.UITestContext;
+
+import android.app.Activity;
+import android.os.Build;
+import android.util.DisplayMetrics;
+
+import com.robotium.solo.Solo;
+
+/**
+ * Provides general hardware (ex: configuration) and software (ex: version) information
+ * about the current test device and allows changing its configuration.
+ */
+public final class DeviceHelper {
+ public enum Type {
+ PHONE,
+ TABLET
+ }
+
+ public enum AndroidVersion {
+ v2x,
+ v3x,
+ v4x
+ }
+
+ private static Activity sActivity;
+ private static Solo sSolo;
+
+ private static Type sDeviceType;
+ private static AndroidVersion sAndroidVersion;
+
+ private static int sScreenHeight;
+ private static int sScreenWidth;
+
+ private DeviceHelper() { /* To disallow instantiation. */ }
+
+ public static void assertIsTablet() {
+ fAssertTrue("The device is a tablet", isTablet());
+ }
+
+ protected static void init(final UITestContext context) {
+ sActivity = context.getActivity();
+ sSolo = context.getSolo();
+
+ setAndroidVersion();
+ setScreenDimensions();
+ setDeviceType();
+ }
+
+ private static void setAndroidVersion() {
+ int sdk = Build.VERSION.SDK_INT;
+ if (sdk < Build.VERSION_CODES.HONEYCOMB) {
+ sAndroidVersion = AndroidVersion.v2x;
+ } else if (sdk > Build.VERSION_CODES.HONEYCOMB_MR2) {
+ sAndroidVersion = AndroidVersion.v4x;
+ } else {
+ sAndroidVersion = AndroidVersion.v3x;
+ }
+ }
+
+ private static void setScreenDimensions() {
+ final DisplayMetrics dm = new DisplayMetrics();
+ sActivity.getWindowManager().getDefaultDisplay().getMetrics(dm);
+
+ sScreenHeight = dm.heightPixels;
+ sScreenWidth = dm.widthPixels;
+ }
+
+ private static void setDeviceType() {
+ sDeviceType = (GeckoAppShell.isTablet() ? Type.TABLET : Type.PHONE);
+ }
+
+ public static int getScreenHeight() {
+ return sScreenHeight;
+ }
+
+ public static int getScreenWidth() {
+ return sScreenWidth;
+ }
+
+ public static AndroidVersion getAndroidVersion() {
+ return sAndroidVersion;
+ }
+
+ public static boolean isPhone() {
+ return (sDeviceType == Type.PHONE);
+ }
+
+ public static boolean isTablet() {
+ return (sDeviceType == Type.TABLET);
+ }
+
+ public static void setLandscapeRotation() {
+ sSolo.setActivityOrientation(Solo.LANDSCAPE);
+ }
+
+ public static void setPortraitOrientation() {
+ sSolo.setActivityOrientation(Solo.LANDSCAPE);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/FrameworkHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/FrameworkHelper.java
new file mode 100644
index 0000000000..d3c4d63905
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/FrameworkHelper.java
@@ -0,0 +1,94 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests.helpers;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fFail;
+
+import java.lang.reflect.Field;
+
+import android.content.Context;
+import android.view.View;
+
+/**
+ * Provides helper functions for accessing Android framework features
+ *
+ * This class uses reflection to access framework functionalities that are
+ * unavailable through the regular Android API. Using reflection in this
+ * case is okay because it does not touch Gecko classes that go through
+ * ProGuard.
+ */
+public final class FrameworkHelper {
+
+ private FrameworkHelper() { /* To disallow instantiation. */ }
+
+ private static Field getClassField(final Class<?> clazz, final String fieldName)
+ throws NoSuchFieldException {
+ Class<?> cls = clazz;
+ do {
+ try {
+ return cls.getDeclaredField(fieldName);
+ } catch (final Exception e) {
+ // NoSuchFieldException is a documented exception of getDeclaredField
+ // and is frequently observed here. No other exceptions are documented
+ // for getDeclaredField. However, on Android 2.3, NoSuchMethodException
+ // is also observed, when called on some classes. This appears to be
+ // an Android bug reportedly fixed in Honeycomb. Since NoSuchMethodException
+ // is not declared, it cannot be caught, so we catch all Exceptions.
+ cls = cls.getSuperclass();
+ }
+ } while (cls != null);
+ // We tried getDeclaredField before; now try getField instead.
+ // getField behaves differently in that getField traverses the inheritance
+ // list, but it only works on public fields. While getField won't get us
+ // anything new, it makes code cleaner by throwing an exception for us.
+ return clazz.getField(fieldName);
+ }
+
+ private static Object getField(final Object obj, final String fieldName) {
+ try {
+ final Field field = getClassField(obj.getClass(), fieldName);
+ final boolean accessible = field.isAccessible();
+ field.setAccessible(true);
+ final Object ret = field.get(obj);
+ field.setAccessible(accessible);
+ return ret;
+ } catch (final NoSuchFieldException e) {
+ // We expect a valid field name; if it's not valid,
+ // the caller is doing something wrong and should be fixed.
+ fFail("Argument field should be a valid field name: " + e.toString());
+ } catch (final IllegalAccessException e) {
+ // This should not happen. If it does, setAccessible above is not working.
+ fFail("Field should be accessible: " + e.toString());
+ }
+ throw new IllegalStateException("Should not continue from previous failures");
+ }
+
+ private static void setField(final Object obj, final String fieldName, final Object value) {
+ try {
+ final Field field = getClassField(obj.getClass(), fieldName);
+ final boolean accessible = field.isAccessible();
+ field.setAccessible(true);
+ field.set(obj, value);
+ field.setAccessible(accessible);
+ return;
+ } catch (final NoSuchFieldException e) {
+ // We expect a valid field name; if it's not valid,
+ // the caller is doing something wrong and should be fixed.
+ fFail("Argument field should be a valid field name: " + e.toString());
+ } catch (final IllegalAccessException e) {
+ // This should not happen. If it does, setAccessible above is not working.
+ fFail("Field should be accessible: " + e.toString());
+ }
+ throw new IllegalStateException("Cannot continue from previous failures");
+ }
+
+ public static Context getViewContext(final View v) {
+ return (Context) getField(v, "mContext");
+ }
+
+ public static void setViewContext(final View v, final Context c) {
+ setField(v, "mContext", c);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoClickHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoClickHelper.java
new file mode 100644
index 0000000000..b8d1ef0cee
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoClickHelper.java
@@ -0,0 +1,50 @@
+package org.mozilla.gecko.tests.helpers;
+
+import android.app.Activity;
+import android.util.DisplayMetrics;
+
+import com.robotium.solo.Solo;
+
+import org.mozilla.gecko.Driver;
+import org.mozilla.gecko.tests.StringHelper;
+import org.mozilla.gecko.tests.UITestContext;
+
+/**
+ * Provides helper functions for clicking elements rendered by the Gecko engine.
+ */
+public class GeckoClickHelper {
+ private static Solo sSolo;
+ private static Activity sActivity;
+ private static Driver sDriver;
+
+ protected static void init(final UITestContext context) {
+ sSolo = context.getSolo();
+ sActivity = context.getActivity();
+ sDriver = context.getDriver();
+ }
+
+ private GeckoClickHelper() { /* To disallow instantiation. */ }
+
+ /**
+ * Long press the link and select "Open Link in New Tab" from the context menu.
+ *
+ * The link should be positioned at the top of the page, at least 60px high and
+ * aligned to the middle.
+ */
+ public static void openCentralizedLinkInNewTab() {
+ openLinkContextMenu();
+
+ // Click on "Open Link in New Tab"
+ sSolo.clickOnText(StringHelper.get().CONTEXT_MENU_ITEMS_IN_NORMAL_TAB[0]);
+ }
+
+ private static void openLinkContextMenu() {
+ DisplayMetrics dm = new DisplayMetrics();
+ sActivity.getWindowManager().getDefaultDisplay().getMetrics(dm);
+
+ sSolo.clickLongOnScreen(
+ sDriver.getGeckoLeft() + sDriver.getGeckoWidth() / 2,
+ sDriver.getGeckoTop() + 30 * dm.density
+ );
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoHelper.java
new file mode 100644
index 0000000000..cd75b7255d
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoHelper.java
@@ -0,0 +1,49 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests.helpers;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.Actions.EventExpecter;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.tests.UITestContext;
+
+import android.app.Activity;
+
+/**
+ * Provides helper functions for accessing the underlying Gecko engine.
+ */
+public final class GeckoHelper {
+ private static Activity sActivity;
+ private static Actions sActions;
+
+ private GeckoHelper() { /* To disallow instantiation. */ }
+
+ protected static void init(final UITestContext context) {
+ sActivity = context.getActivity();
+ sActions = context.getActions();
+ }
+
+ public static void blockForReady() {
+ blockForEvent("Gecko:Ready");
+ }
+
+ /**
+ * Blocks for the "Gecko:DelayedStartup" event, which occurs after "Gecko:Ready" and the
+ * first page load.
+ */
+ public static void blockForDelayedStartup() {
+ blockForEvent("Gecko:DelayedStartup");
+ }
+
+ private static void blockForEvent(final String eventName) {
+ final EventExpecter eventExpecter = sActions.expectGeckoEvent(eventName);
+
+ if (!GeckoThread.isRunning()) {
+ eventExpecter.blockForEvent();
+ }
+
+ eventExpecter.unregisterListener();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/HelperInitializer.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/HelperInitializer.java
new file mode 100644
index 0000000000..229dc10624
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/HelperInitializer.java
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests.helpers;
+
+import org.mozilla.gecko.tests.UITestContext;
+
+/**
+ * AssertionHelper is statically imported in many places. Thus we want to hide
+ * its init method outside of this package. We initialize the remaining helper
+ * classes from here so that all the init methods are package protected.
+ */
+public final class HelperInitializer {
+
+ private HelperInitializer() { /* To disallow instantiation. */ }
+
+ public static void init(final UITestContext context) {
+ // Other helpers make assertions so init AssertionHelper first.
+ AssertionHelper.init(context);
+
+ DeviceHelper.init(context);
+ GeckoClickHelper.init(context);
+ GeckoHelper.init(context);
+ JavascriptBridge.init(context);
+ NavigationHelper.init(context);
+ RobotiumHelper.init(context);
+ WaitHelper.init(context);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/JavascriptBridge.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/JavascriptBridge.java
new file mode 100644
index 0000000000..1b0ece1cd2
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/JavascriptBridge.java
@@ -0,0 +1,394 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests.helpers;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import junit.framework.AssertionFailedError;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.Actions.EventExpecter;
+import org.mozilla.gecko.Assert;
+import org.mozilla.gecko.tests.UITestContext;
+
+/**
+ * Javascript bridge allows calls to and from JavaScript.
+ *
+ * To establish communication, create an instance of JavascriptBridge in Java and pass in
+ * an object that will receive calls from JavaScript. For example:
+ *
+ * {@code final JavascriptBridge js = new JavascriptBridge(javaObj);}
+ *
+ * Next, create an instance of JavaBridge in JavaScript and pass in another object
+ * that will receive calls from Java. For example:
+ *
+ * {@code let java = new JavaBridge(jsObj);}
+ *
+ * Once a link is established, calls can be made using the methods syncCall and asyncCall.
+ * syncCall waits for the call to finish before returning. For example:
+ *
+ * {@code js.syncCall("abc", 1, 2, 3);} will synchronously call the JavaScript method
+ * jsObj.abc and pass in arguments 1, 2, and 3.
+ *
+ * {@code java.asyncCall("def", 4, 5, 6);} will asynchronously call the Java method
+ * javaObj.def and pass in arguments 4, 5, and 6.
+ *
+ * Supported argument types include int, double, boolean, String, and JSONObject. Note
+ * that only implicit conversion is done, meaning if a floating point argument is passed
+ * from JavaScript to Java, the call will fail if the Java method has an int argument.
+ *
+ * Because JavascriptBridge and JavaBridge use one underlying communication channel,
+ * creating multiple instances of them will not create independent links.
+ *
+ * Note also that because Robocop tests finish as soon as the Java test method returns,
+ * the last call to JavaScript from Java must be a synchronous call. Otherwise, the test
+ * will finish before the JavaScript method is run. Calls to Java from JavaScript do not
+ * have this requirement. Because of these considerations, calls from Java to JavaScript
+ * are usually synchronous and calls from JavaScript to Java are usually asynchronous.
+ * See testJavascriptBridge.java for examples.
+ */
+public final class JavascriptBridge {
+
+ private static enum MessageStatus {
+ QUEUE_EMPTY, // Did not process a message; queue was empty.
+ PROCESSED, // A message other than sync was processed.
+ REPLIED, // A sync message was processed.
+ SAVED, // An async message was saved; see processMessage().
+ };
+
+ @SuppressWarnings("serial")
+ public static class CallException extends RuntimeException {
+ public CallException() {
+ super();
+ }
+
+ public CallException(final String msg) {
+ super(msg);
+ }
+
+ public CallException(final String msg, final Throwable e) {
+ super(msg, e);
+ }
+
+ public CallException(final Throwable e) {
+ super(e);
+ }
+ }
+
+ public static final String EVENT_TYPE = "Robocop:JS";
+
+ private static Actions sActions;
+ private static Assert sAsserter;
+
+ // Target of JS-to-Java calls
+ private final Object mTarget;
+ // List of public methods in subclass
+ private final Method[] mMethods;
+ // Parser for handling xpcshell assertions
+ private final JavascriptMessageParser mLogParser;
+ // Expecter of our internal Robocop event
+ private final EventExpecter mExpecter;
+ // Saved async message; see processMessage() for its purpose.
+ private JSONObject mSavedAsyncMessage;
+ // Number of levels in the synchronous call stack
+ private int mCallStackDepth;
+ // If JavaBridge has been loaded
+ private boolean mJavaBridgeLoaded;
+
+ /* package */ static void init(final UITestContext context) {
+ sActions = context.getActions();
+ sAsserter = context.getAsserter();
+ }
+
+ public JavascriptBridge(final Object target) {
+ mTarget = target;
+ mMethods = target.getClass().getMethods();
+ mExpecter = sActions.expectGeckoEvent(EVENT_TYPE);
+ // The JS here is unrelated to a test harness, so we
+ // have our message parser end on assertion failure.
+ mLogParser = new JavascriptMessageParser(sAsserter, true);
+ }
+
+ /**
+ * Synchronously calls a method in Javascript.
+ *
+ * @param method Name of the method to call
+ * @param args Arguments to pass to the Javascript method; must be a list of
+ * values allowed by JSONObject.
+ */
+ public void syncCall(final String method, final Object... args) {
+ mCallStackDepth++;
+
+ sendMessage("sync-call", method, args);
+ try {
+ while (processPendingMessage() != MessageStatus.REPLIED) {
+ }
+ } catch (final AssertionFailedError e) {
+ // Most likely an event expecter time out
+ throw new CallException("Cannot call " + method, e);
+ }
+
+ // If syncCall was called reentrantly from processPendingMessage(), mCallStackDepth
+ // will be greater than 1 here. In that case we don't have to wait for pending calls
+ // because the outermost syncCall will do it for us.
+ if (mCallStackDepth == 1) {
+ // We want to wait for all asynchronous calls to finish,
+ // because the test may end immediately after this method returns.
+ finishPendingCalls();
+ }
+ mCallStackDepth--;
+ }
+
+ /**
+ * Asynchronously calls a method in Javascript.
+ *
+ * @param method Name of the method to call
+ * @param args Arguments to pass to the Javascript method; must be a list of
+ * values allowed by JSONObject.
+ */
+ public void asyncCall(final String method, final Object... args) {
+ sendMessage("async-call", method, args);
+ }
+
+ /**
+ * Disconnect the bridge.
+ */
+ public void disconnect() {
+ mExpecter.unregisterListener();
+ }
+
+ /**
+ * Process a new message; wait for new message if necessary.
+ *
+ * @return MessageStatus value to indicate result of processing the message
+ */
+ private MessageStatus processPendingMessage() {
+ // We're on the test thread.
+ // We clear mSavedAsyncMessage in maybeProcessPendingMessage() but not here,
+ // because we always have a new message for processing here, so we never
+ // get a chance to clear mSavedAsyncMessage.
+ try {
+ final String message = mExpecter.blockForEventData();
+ return processMessage(new JSONObject(message));
+ } catch (final JSONException e) {
+ throw new IllegalStateException("Invalid message", e);
+ }
+ }
+
+ /**
+ * Process a message if a new or saved message is available.
+ *
+ * @return MessageStatus value to indicate result of processing the message
+ */
+ private MessageStatus maybeProcessPendingMessage() {
+ // We're on the test thread.
+ final String message = mExpecter.blockForEventDataWithTimeout(0);
+ if (message != null) {
+ try {
+ return processMessage(new JSONObject(message));
+ } catch (final JSONException e) {
+ throw new IllegalStateException("Invalid message", e);
+ }
+ }
+ if (mSavedAsyncMessage != null) {
+ // processMessage clears mSavedAsyncMessage.
+ return processMessage(mSavedAsyncMessage);
+ }
+ return MessageStatus.QUEUE_EMPTY;
+ }
+
+ /**
+ * Wait for all asynchronous messages from Javascript to be processed.
+ */
+ private void finishPendingCalls() {
+ MessageStatus result;
+ do {
+ result = maybeProcessPendingMessage();
+ if (result == MessageStatus.REPLIED) {
+ throw new IllegalStateException("Sync reply was unexpected");
+ }
+ } while (result != MessageStatus.QUEUE_EMPTY);
+ }
+
+ private void ensureJavaBridgeLoaded() {
+ while (!mJavaBridgeLoaded) {
+ processPendingMessage();
+ }
+ }
+
+ private void sendMessage(final String innerType, final String method, final Object[] args) {
+ ensureJavaBridgeLoaded();
+
+ // Call from Java to Javascript
+ final JSONObject message = new JSONObject();
+ final JSONArray jsonArgs = new JSONArray();
+ try {
+ if (args != null) {
+ for (final Object arg : args) {
+ jsonArgs.put(convertToJSONValue(arg));
+ }
+ }
+ message.put("type", EVENT_TYPE)
+ .put("innerType", innerType)
+ .put("method", method)
+ .put("args", jsonArgs);
+ } catch (final JSONException e) {
+ throw new IllegalStateException("Unable to create JSON message", e);
+ }
+ sActions.sendGeckoEvent(EVENT_TYPE, message.toString());
+ }
+
+ private MessageStatus processMessage(JSONObject message) {
+ final String type;
+ final String methodName;
+ final JSONArray argsArray;
+ final Object[] args;
+ try {
+ if (!EVENT_TYPE.equals(message.getString("type"))) {
+ throw new IllegalStateException("Message type is not " + EVENT_TYPE);
+ }
+ type = message.getString("innerType");
+
+ switch (type) {
+ case "progress":
+ // Javascript harness message
+ mLogParser.logMessage(message.getString("message"));
+ return MessageStatus.PROCESSED;
+
+ case "notify-loaded":
+ mJavaBridgeLoaded = true;
+ return MessageStatus.PROCESSED;
+
+ case "sync-reply":
+ // Reply to Java-to-Javascript sync call
+ return MessageStatus.REPLIED;
+
+ case "sync-call":
+ case "async-call":
+
+ if ("async-call".equals(type)) {
+ // Save this async message until another async message arrives, then we
+ // process the saved message and save the new one. This is done as a
+ // form of tail call optimization, by making sync-replies come before
+ // async-calls. On the other hand, if (message == mSavedAsyncMessage),
+ // it means we're currently processing the saved message and should clear
+ // mSavedAsyncMessage.
+ final JSONObject newSavedMessage =
+ (message != mSavedAsyncMessage ? message : null);
+ message = mSavedAsyncMessage;
+ mSavedAsyncMessage = newSavedMessage;
+ if (message == null) {
+ // Saved current message and there wasn't an already saved one.
+ return MessageStatus.SAVED;
+ }
+ }
+
+ methodName = message.getString("method");
+ argsArray = message.getJSONArray("args");
+ args = new Object[argsArray.length()];
+ for (int i = 0; i < args.length; i++) {
+ args[i] = convertFromJSONValue(argsArray.get(i));
+ }
+ invokeMethod(methodName, args);
+
+ if ("sync-call".equals(type)) {
+ // Reply for sync messages
+ sendMessage("sync-reply", methodName, null);
+ }
+ return MessageStatus.PROCESSED;
+ }
+
+ throw new IllegalStateException("Message type is unexpected");
+
+ } catch (final JSONException e) {
+ throw new IllegalStateException("Unable to retrieve JSON message", e);
+ }
+ }
+
+ /**
+ * Given a method name and a list of arguments,
+ * call the most suitable method in the subclass.
+ */
+ private Object invokeMethod(final String methodName, final Object[] args) {
+ final Class<?>[] argTypes = new Class<?>[args.length];
+ for (int i = 0; i < argTypes.length; i++) {
+ if (args[i] == null) {
+ argTypes[i] = Object.class;
+ } else {
+ argTypes[i] = args[i].getClass();
+ }
+ }
+
+ // Try using argument types directly without casting.
+ try {
+ return invokeMethod(mTarget.getClass().getMethod(methodName, argTypes), args);
+ } catch (final NoSuchMethodException e) {
+ // getMethod() failed; try fallback below.
+ }
+
+ // One scenario for getMethod() to fail above is that we don't have the exact
+ // argument types in argTypes (e.g. JS gave us an int but we're using a double,
+ // or JS gave us a null and we don't know its intended type), or the number of
+ // arguments is incorrect. Now we find all the methods with the given name and
+ // try calling them one-by-one. If one call fails, we move to the next call.
+ // Java will try to convert our arguments to the right types.
+ Throwable lastException = null;
+ for (final Method method : mMethods) {
+ if (!method.getName().equals(methodName)) {
+ continue;
+ }
+ try {
+ return invokeMethod(method, args);
+ } catch (final IllegalArgumentException e) {
+ lastException = e;
+ // Try the next method
+ } catch (final UnsupportedOperationException e) {
+ // "Cannot access method" exception below, see if there are other public methods
+ lastException = e;
+ // Try the next method
+ }
+ }
+ // Now we're out of options
+ throw new UnsupportedOperationException(
+ "Cannot call method " + methodName + " (not public? wrong argument types?)",
+ lastException);
+ }
+
+ private Object invokeMethod(final Method method, final Object[] args) {
+ try {
+ return method.invoke(mTarget, args);
+ } catch (final IllegalAccessException e) {
+ throw new UnsupportedOperationException(
+ "Cannot access method " + method.getName(), e);
+ } catch (final InvocationTargetException e) {
+ final Throwable cause = e.getCause();
+ if (cause instanceof CallException) {
+ // Don't wrap CallExceptions; this can happen if a call is nested on top
+ // of existing sync calls, and the nested call throws a CallException
+ throw (CallException) cause;
+ }
+ throw new CallException("Failed to invoke " + method.getName(), cause);
+ }
+ }
+
+ private Object convertFromJSONValue(final Object value) {
+ if (value == JSONObject.NULL) {
+ return null;
+ }
+ return value;
+ }
+
+ private Object convertToJSONValue(final Object value) {
+ if (value == null) {
+ return JSONObject.NULL;
+ }
+ return value;
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/JavascriptMessageParser.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/JavascriptMessageParser.java
new file mode 100644
index 0000000000..6237f1adcf
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/JavascriptMessageParser.java
@@ -0,0 +1,100 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests.helpers;
+
+import org.mozilla.gecko.Assert;
+
+import junit.framework.AssertionFailedError;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Route messages from Javascript's head.js test framework into Java's
+ * Mochitest framework.
+ */
+public final class JavascriptMessageParser {
+
+ /**
+ * The Javascript test harness sends test events to Java.
+ * Each such test event is wrapped in a Robocop:JS event.
+ */
+ public static final String EVENT_TYPE = "Robocop:JS";
+
+ // Messages matching this pattern are handled specially. Messages not
+ // matching this pattern are still printed. This pattern should be able
+ // to handle having multiple lines in a message.
+ private static final Pattern testMessagePattern =
+ Pattern.compile("TEST-([A-Z\\-]+) \\| (.*?) \\| (.*)", Pattern.DOTALL);
+
+ private final Assert asserter;
+ // Used to help print stack traces neatly.
+ private String lastTestName = "";
+ // Have we seen a message saying the test is finished?
+ private boolean testFinishedMessageSeen = false;
+ private final boolean endOnAssertionFailure;
+
+ /**
+ * Constructs a message parser for test result messages sent from JavaScript. When seeing an
+ * assertion failure, the message parser can use the given {@link org.mozilla.gecko.Assert}
+ * instance to immediately end the test (typically if the underlying JS framework is not able
+ * to end the test itself) or to swallow the Errors - this functionality is determined by the
+ * <code>endOnAssertionFailure</code> parameter.
+ *
+ * @param asserter The Assert instance to which test results should be passed.
+ * @param endOnAssertionFailure
+ * true if the test should end if we see a JS assertion failure, false otherwise.
+ */
+ public JavascriptMessageParser(final Assert asserter, final boolean endOnAssertionFailure) {
+ this.asserter = asserter;
+ this.endOnAssertionFailure = endOnAssertionFailure;
+ }
+
+ public boolean isTestFinished() {
+ return testFinishedMessageSeen;
+ }
+
+ public void logMessage(final String str) {
+ final Matcher m = testMessagePattern.matcher(str.trim());
+
+ if (m.matches()) {
+ final String type = m.group(1);
+ final String name = m.group(2);
+ final String message = m.group(3);
+
+ if ("INFO".equals(type)) {
+ asserter.info(name, message);
+ testFinishedMessageSeen = testFinishedMessageSeen ||
+ "exiting test".equals(message);
+ } else if ("PASS".equals(type)) {
+ asserter.ok(true, name, message);
+ } else if ("UNEXPECTED-FAIL".equals(type)) {
+ try {
+ asserter.ok(false, name, message);
+ } catch (AssertionFailedError e) {
+ // Above, we call the assert, allowing it to log.
+ // Now we can end the test, if applicable.
+ if (this.endOnAssertionFailure) {
+ throw e;
+ }
+ // Otherwise, swallow the Error. The JS framework we're
+ // logging messages from is likely capable of ending tests
+ // when it needs to, and we want to see all of its failures,
+ // not just the first one!
+ }
+ } else if ("KNOWN-FAIL".equals(type)) {
+ asserter.todo(false, name, message);
+ } else if ("UNEXPECTED-PASS".equals(type)) {
+ asserter.todo(true, name, message);
+ }
+
+ lastTestName = name;
+ } else {
+ // Generally, these extra lines are stack traces from failures,
+ // so we print them with the name of the last test seen.
+ asserter.info(lastTestName, str.trim());
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/NavigationHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/NavigationHelper.java
new file mode 100644
index 0000000000..e3ccc8236d
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/NavigationHelper.java
@@ -0,0 +1,104 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests.helpers;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull;
+
+import org.mozilla.gecko.tests.UITestContext;
+import org.mozilla.gecko.tests.UITestContext.ComponentType;
+import org.mozilla.gecko.tests.components.AppMenuComponent;
+import org.mozilla.gecko.tests.components.ToolbarComponent;
+
+import com.robotium.solo.Solo;
+
+/**
+ * Provides helper functionality for navigating around the Firefox UI. These functions will often
+ * combine actions taken on multiple components to perform larger interactions.
+ */
+final public class NavigationHelper {
+ private static UITestContext sContext;
+ private static Solo sSolo;
+
+ private static AppMenuComponent sAppMenu;
+ private static ToolbarComponent sToolbar;
+
+ protected static void init(final UITestContext context) {
+ sContext = context;
+ sSolo = context.getSolo();
+
+ sAppMenu = (AppMenuComponent) context.getComponent(ComponentType.APPMENU);
+ sToolbar = (ToolbarComponent) context.getComponent(ComponentType.TOOLBAR);
+ }
+
+ public static void enterAndLoadUrl(String url) {
+ fAssertNotNull("url is not null", url);
+
+ url = adjustUrl(url);
+ sToolbar.enterEditingMode()
+ .enterUrl(url)
+ .commitEditingMode();
+ }
+
+ /**
+ * Returns a new URL with the docshell HTTP server host prefix.
+ */
+ public static String adjustUrl(final String url) {
+ fAssertNotNull("url is not null", url);
+
+ if (url.startsWith("about:") || url.startsWith("chrome:")) {
+ return url;
+ }
+
+ return sContext.getAbsoluteHostnameUrl(url);
+ }
+
+ public static void goBack() {
+ if (DeviceHelper.isTablet()) {
+ sToolbar.pressBackButton(); // Waits for page load & asserts isNotEditing.
+ return;
+ }
+
+ sToolbar.assertIsNotEditing();
+ WaitHelper.waitForPageLoad(new Runnable() {
+ @Override
+ public void run() {
+ // TODO: Lower soft keyboard first if applicable. Note that
+ // Solo.hideSoftKeyboard() does not clear focus (which might be fine since
+ // Gecko would be the element focused).
+ sSolo.goBack();
+ }
+ });
+ }
+
+ public static void goForward() {
+ if (DeviceHelper.isTablet()) {
+ sToolbar.pressForwardButton(); // Waits for page load & asserts isNotEditing.
+ return;
+ }
+
+ sToolbar.assertIsNotEditing();
+ WaitHelper.waitForPageLoad(new Runnable() {
+ @Override
+ public void run() {
+ sAppMenu.pressMenuItem(AppMenuComponent.MenuItem.FORWARD);
+ }
+ });
+ }
+
+ public static void reload() {
+ if (DeviceHelper.isTablet()) {
+ sToolbar.pressReloadButton(); // Waits for page load & asserts isNotEditing.
+ return;
+ }
+
+ sToolbar.assertIsNotEditing();
+ WaitHelper.waitForPageLoad(new Runnable() {
+ @Override
+ public void run() {
+ sAppMenu.pressMenuItem(AppMenuComponent.MenuItem.RELOAD);
+ }
+ });
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/RobotiumHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/RobotiumHelper.java
new file mode 100644
index 0000000000..2536eb9dbd
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/RobotiumHelper.java
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests.helpers;
+
+import com.robotium.solo.Solo;
+
+import org.mozilla.gecko.tests.UITestContext;
+
+import java.util.regex.Pattern;
+
+/**
+ * Provides helper functions for using Robotium.
+ */
+public final class RobotiumHelper {
+ private static Solo sSolo;
+
+ private RobotiumHelper() { /* To disallow instantiation. */ }
+
+ protected static void init(final UITestContext context) {
+ sSolo = context.getSolo();
+ }
+
+ /**
+ * Same as Solo.waitForText(), but matching against full text, without regular expressions.
+ */
+ public static boolean waitForExactText(final String text,
+ final int minimumNumberOfMatches,
+ final long timeout) {
+ String matchText = "^" + Pattern.quote(text) + "$";
+ return sSolo.waitForText(matchText, minimumNumberOfMatches, timeout);
+ }
+
+ /**
+ * Same as Solo.searchText(), but matching against full text, without regular expressions.
+ */
+ public static boolean searchExactText(final String text,
+ final boolean onlyVisible) {
+ String matchText = "^" + Pattern.quote(text) + "$";
+ return sSolo.searchText(matchText, onlyVisible);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/WaitHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/WaitHelper.java
new file mode 100644
index 0000000000..f6e6166520
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/WaitHelper.java
@@ -0,0 +1,215 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests.helpers;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
+
+import android.os.SystemClock;
+
+import java.util.concurrent.Callable;
+import java.util.regex.Pattern;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.Actions.EventExpecter;
+import org.mozilla.gecko.tests.UITestContext;
+import org.mozilla.gecko.tests.UITestContext.ComponentType;
+import org.mozilla.gecko.tests.components.ToolbarComponent;
+
+import com.robotium.solo.Condition;
+import com.robotium.solo.Solo;
+
+/**
+ * Provides functionality related to waiting on certain events to happen.
+ */
+public final class WaitHelper {
+ // TODO: Make public for when Solo.waitForCondition is used directly (i.e. do not want
+ // assertion from waitFor)?
+ // DEFAULT_MAX_WAIT_MS of 5000 was intermittently insufficient during
+ // initialization on Android 2.3 emulator -- bug 1114655
+ private static final int DEFAULT_MAX_WAIT_MS = 15000;
+ private static final int PAGE_LOAD_WAIT_MS = 10000;
+ private static final int CHANGE_WAIT_MS = 15000;
+
+ // TODO: via lucasr - Add ThrobberVisibilityChangeVerifier?
+ private static final ChangeVerifier[] PAGE_LOAD_VERIFIERS = new ChangeVerifier[] {
+ new ToolbarTitleTextChangeVerifier()
+ };
+
+ private static UITestContext sContext;
+ private static Solo sSolo;
+ private static Actions sActions;
+
+ private static ToolbarComponent sToolbar;
+
+ private WaitHelper() { /* To disallow instantiation. */ }
+
+ protected static void init(final UITestContext context) {
+ sContext = context;
+ sSolo = context.getSolo();
+ sActions = context.getActions();
+
+ sToolbar = (ToolbarComponent) context.getComponent(ComponentType.TOOLBAR);
+ }
+
+ /**
+ * Waits for the given {@link solo.Condition} using the default wait duration; will throw an
+ * AssertionError if the duration is elapsed and the condition is not satisfied.
+ */
+ public static void waitFor(String message, final Condition condition) {
+ message = "Waiting for " + message + ".";
+ fAssertTrue(message, sSolo.waitForCondition(condition, DEFAULT_MAX_WAIT_MS));
+ }
+
+ /**
+ * Waits for the given {@link solo.Condition} using the given wait duration; will throw an
+ * AssertionError if the duration is elapsed and the condition is not satisfied.
+ */
+ public static void waitFor(String message, final Condition condition, final int waitMillis) {
+ message = "Waiting for " + message + " with timeout " + waitMillis + ".";
+ fAssertTrue(message, sSolo.waitForCondition(condition, waitMillis));
+ }
+
+ /**
+ * Waits for the given Callable to return something that is not null, using the given wait
+ * duration; will throw an AssertionError if the duration is elapsed and the callable has not
+ * returned a non-null object.
+ *
+ * @return the value returned by the Callable. Or null if the duration has elapsed.
+ */
+ public static <V> V waitFor(String message, final Callable<V> callable, int waitMillis) {
+ sContext.dumpLog("WaitHelper", "Waiting for " + message + " with timeout " + waitMillis + ".");
+
+ final Object[] value = new Object[1];
+
+ Condition condition = new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ try {
+ V result = callable.call();
+ value[0] = result;
+ return result != null;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+ };
+
+ sSolo.waitForCondition(condition, waitMillis);
+
+ return (V) value[0];
+ }
+
+ /**
+ * Waits for the Gecko event declaring the page has loaded. Takes in and runs a Runnable
+ * that will perform the action that will cause the page to load.
+ */
+ public static void waitForPageLoad(final Runnable initiatingAction) {
+ fAssertNotNull("initiatingAction is not null", initiatingAction);
+
+ // Some changes to the UI occur in response to the same event we listen to for when
+ // the page has finished loading (e.g. a page title update). As such, we ensure this
+ // UI state has changed before returning from this method; here we store the initial
+ // state.
+ final ChangeVerifier[] pageLoadVerifiers = PAGE_LOAD_VERIFIERS;
+ for (final ChangeVerifier verifier : pageLoadVerifiers) {
+ verifier.storeState();
+ }
+
+ // Wait for the page load and title changed event.
+ final EventExpecter[] eventExpecters = new EventExpecter[] {
+ sActions.expectGeckoEvent("DOMContentLoaded"),
+ sActions.expectGeckoEvent("DOMTitleChanged")
+ };
+
+ initiatingAction.run();
+
+ // PAGE_LOAD_WAIT_MS is the total time we wait for all events to finish.
+ final long expecterStartMillis = SystemClock.uptimeMillis();
+ for (final EventExpecter expecter : eventExpecters) {
+ final int eventWaitTimeMillis = PAGE_LOAD_WAIT_MS - (int)(SystemClock.uptimeMillis() - expecterStartMillis);
+ expecter.blockForEventDataWithTimeout(eventWaitTimeMillis);
+ expecter.unregisterListener();
+ }
+
+ // The timeout wait time should be the aggregate time for all ChangeVerifiers.
+ final long verifierStartMillis = SystemClock.uptimeMillis();
+
+ // Verify remaining state has changed.
+ for (final ChangeVerifier verifier : pageLoadVerifiers) {
+ // If we timeout, either the state is set to the same value (which is fine), or
+ // the state has not yet changed. Since we can't be sure it will ever change, move
+ // on and let the assertions fail if applicable.
+ final int verifierWaitMillis = CHANGE_WAIT_MS - (int)(SystemClock.uptimeMillis() - verifierStartMillis);
+ final boolean hasTimedOut = !sSolo.waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return verifier.hasStateChanged();
+ }
+ }, verifierWaitMillis);
+
+ sContext.dumpLog(verifier.getLogTag(),
+ (hasTimedOut ? "timed out." : "was satisfied."));
+ }
+ }
+
+ /**
+ * Implementations of this interface verify that the state of the test has changed from
+ * the invocation of storeState to the invocation of hasStateChanged. A boolean will be
+ * returned from hasStateChanged, indicating this change of status.
+ */
+ private interface ChangeVerifier {
+ String getLogTag();
+
+ /**
+ * Stores the initial state of the system. This system state is used to diff against
+ * the end state to determine if the system has changed. Since this is just a diff
+ * (with a timeout), this method could potentially store state inconsistent with
+ * what is visible to the user.
+ */
+ void storeState();
+ boolean hasStateChanged();
+ }
+
+ private static class ToolbarTitleTextChangeVerifier implements ChangeVerifier {
+ private static final String LOGTAG = ToolbarTitleTextChangeVerifier.class.getSimpleName();
+
+ // A regex that matches the page title that shows up while the page is loading.
+ private static final Pattern LOADING_PREFIX = Pattern.compile("[A-Za-z]{3,9}://");
+
+ private CharSequence mOldTitleText;
+
+ @Override
+ public String getLogTag() {
+ return LOGTAG;
+ }
+
+ @Override
+ public void storeState() {
+ mOldTitleText = sToolbar.getPotentiallyInconsistentTitle();
+ sContext.dumpLog(LOGTAG, "stored title, \"" + mOldTitleText + "\".");
+ }
+
+ @Override
+ public boolean hasStateChanged() {
+ // TODO: Additionally, consider Solo.waitForText.
+ // TODO: Robocop sleeps .5 sec between calls. Cache title view?
+ final CharSequence title = sToolbar.getPotentiallyInconsistentTitle();
+
+ // TODO: Handle the case where the URL is shown instead of page title by preference.
+ // HACK: We want to wait until the title changes to the state a tester may assert
+ // (e.g. the page title). However, the title is set to the URL before the title is
+ // loaded from the server and set as the final page title; we ignore the
+ // intermediate URL loading state here.
+ final boolean isLoading = LOADING_PREFIX.matcher(title).lookingAt();
+ final boolean hasStateChanged = !isLoading && !mOldTitleText.equals(title);
+
+ if (hasStateChanged) {
+ sContext.dumpLog(LOGTAG, "state changed to title, \"" + title + "\".");
+ }
+ return hasStateChanged;
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testANRReporter.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testANRReporter.java
new file mode 100644
index 0000000000..e3afeb8d95
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testANRReporter.java
@@ -0,0 +1,240 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.AppConstants;
+
+import android.content.Context;
+import android.content.Intent;
+
+import com.robotium.solo.Condition;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+
+import org.json.JSONObject;
+
+/**
+ * Tests the proper operation of the ANR reporter.
+ */
+public class testANRReporter extends BaseTest {
+
+ private static final String ANR_ACTION = "android.intent.action.ANR";
+ private static final String PING_DIR = "saved-telemetry-pings";
+ private static final int WAIT_FOR_PING_TIMEOUT = 60000;
+ private static final String ANR_PATH = "/data/anr/traces.txt";
+ private static final String SAMPLE_ANR
+ = "----- pid 1 at 2014-01-15 18:55:51 -----\n"
+ + "Cmd line: " + AppConstants.ANDROID_PACKAGE_NAME + "\n"
+ + "\n"
+ + "JNI: CheckJNI is off; workarounds are off; pins=0; globals=397\n"
+ + "\n"
+ + "DALVIK THREADS:\n"
+ + "(mutexes: tll=0 tsl=0 tscl=0 ghl=0)\n"
+ + "\n"
+ + "\"main\" prio=5 tid=1 WAIT\n"
+ + " | group=\"main\" sCount=1 dsCount=0 obj=0x41d6bc90 self=0x41d5a3c8\n"
+ + " | sysTid=3485 nice=0 sched=0/0 cgrp=apps handle=1074852180\n"
+ + " | state=S schedstat=( 0 0 0 ) utm=1065 stm=152 core=0\n"
+ + " at java.lang.Object.wait(Native Method)\n"
+ + " - waiting on <0x427ab340> (a org.mozilla.gecko.GeckoEditable$5)\n"
+ + " at java.lang.Object.wait(Object.java:364)\n"
+ + " at org.mozilla.gecko.GeckoEditable$5.run(GeckoEditable.java:746)\n"
+ + " at android.os.Handler.handleCallback(Handler.java:733)\n"
+ + " at android.os.Handler.dispatchMessage(Handler.java:95)\n"
+ + " at android.os.Looper.loop(Looper.java:137)\n"
+ + " at android.app.ActivityThread.main(ActivityThread.java:4998)\n"
+ + " at java.lang.reflect.Method.invokeNative(Native Method)\n"
+ + " at java.lang.reflect.Method.invoke(Method.java:515)\n"
+ + " at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:777)\n"
+ + " at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:593)\n"
+ + " at dalvik.system.NativeStart.main(Native Method)\n"
+ + "\n"
+ + "\"Gecko\" prio=5 tid=16 SUSPENDED\n"
+ + " | group=\"main\" sCount=1 dsCount=0 obj=0x426e2b28 self=0x76ae92e8\n"
+ + " | sysTid=3541 nice=0 sched=0/0 cgrp=apps handle=1991153472\n"
+ + " | state=S schedstat=( 0 0 0 ) utm=1118 stm=145 core=0\n"
+ + " #00 pc 00000904 /system/lib/libc.so (__futex_syscall3+4294832136)\n"
+ + " #01 pc 0000eec4 /system/lib/libc.so (__pthread_cond_timedwait_relative+48)\n"
+ + " #02 pc 0000ef24 /system/lib/libc.so (__pthread_cond_timedwait+64)\n"
+ + " #03 pc 000536b7 /system/lib/libdvm.so\n"
+ + " #04 pc 00053c79 /system/lib/libdvm.so (dvmChangeStatus(Thread*, ThreadStatus)+34)\n"
+ + " #05 pc 00049507 /system/lib/libdvm.so\n"
+ + " #06 pc 0004d84b /system/lib/libdvm.so\n"
+ + " #07 pc 0003f1df /dev/ashmem/libxul.so (deleted)\n"
+ + " at org.mozilla.gecko.mozglue.GeckoLoader.nativeRun(Native Method)\n"
+ + " at org.mozilla.gecko.GeckoAppShell.runGecko(GeckoAppShell.java:384)\n"
+ + " at org.mozilla.gecko.GeckoThread.run(GeckoThread.java:177)\n"
+ + "\n"
+ + "----- end 1 -----\n"
+ + "\n"
+ + "\n"
+ + "----- pid 2 at 2013-01-25 13:27:01 -----\n"
+ + "Cmd line: system_server\n"
+ + "\n"
+ + "----- end 2 -----\n";
+
+ private boolean mDone;
+
+ private JSONObject readPingFile(final File pingFile) throws Exception {
+ final long fileSize = pingFile.length();
+ if (fileSize == 0 || fileSize > Integer.MAX_VALUE) {
+ throw new Exception("Invalid ping file size");
+ }
+ final char[] buffer = new char[(int) fileSize];
+ final FileReader reader = new FileReader(pingFile);
+ try {
+ final int readSize = reader.read(buffer);
+ if (readSize == 0 || readSize > buffer.length) {
+ throw new Exception("Invalid number of bytes read");
+ }
+ } finally {
+ reader.close();
+ }
+ return new JSONObject(new String(buffer));
+ }
+
+ public void testANRReporter() throws Exception {
+ blockForGeckoReady();
+
+ // Cannot test ANR reporter if it's disabled.
+ if (!AppConstants.MOZ_ANDROID_ANR_REPORTER) {
+ mAsserter.ok(true, "ANR reporter is disabled", null);
+ return;
+ }
+
+ // For the ANR reporter to work, we need to provide sample ANR traces to it.
+ // Therefore, we need the ANR file to exist and writable. If not, we don't
+ // have the right permissions to create the file, so we just bail.
+ final File anrFile = new File(ANR_PATH);
+ if (!anrFile.exists()) {
+ mAsserter.ok(true, "ANR file does not exist", null);
+ return;
+ }
+ if (!anrFile.canWrite()) {
+ mAsserter.ok(true, "ANR file is not writable", null);
+ return;
+ }
+
+ final FileWriter anrWriter = new FileWriter(anrFile);
+ try {
+ anrWriter.write(SAMPLE_ANR);
+ } finally {
+ anrWriter.close();
+ }
+
+ // Block the UI thread to simulate an ANR
+ final Runnable uiBlocker = new Runnable() {
+ @Override
+ public synchronized void run() {
+ while (!mDone) {
+ try {
+ wait();
+ } catch (final InterruptedException e) {
+ }
+ }
+ }
+ };
+ getActivity().runOnUiThread(uiBlocker);
+
+ // Make sure our initial ping directory is empty.
+ final File pingDir = new File(mProfile, PING_DIR);
+ final String[] initialFiles = pingDir.list();
+ mAsserter.ok(initialFiles == null || initialFiles.length == 0,
+ "Ping directory is empty", null);
+
+ final Intent anrIntent = new Intent(ANR_ACTION);
+ anrIntent.setPackage(AppConstants.ANDROID_PACKAGE_NAME);
+ mAsserter.is(anrIntent.getPackage(), AppConstants.ANDROID_PACKAGE_NAME,
+ "Successfully set package name");
+
+ final Context testContext = getInstrumentation().getContext();
+ mAsserter.isnot(testContext, null, "testContext should not be null");
+
+ // Trigger the ANR.
+ mAsserter.info("Triggering ANR", null);
+ testContext.sendBroadcast(anrIntent);
+
+ // ANR reporter is supposed to ignore duplicate ANRs.
+ // This will be checked later when we look for ping files.
+ mAsserter.info("Triggering second ANR", null);
+ testContext.sendBroadcast(new Intent(anrIntent));
+
+ waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ mAsserter.info("Waiting for ping", null);
+
+ try {
+ // Sleep to allow the ANR reporter thread time to process the ANR.
+ Thread.sleep(1000);
+ } catch (final InterruptedException e) {
+ }
+
+ final File[] newFiles = pingDir.listFiles();
+ if (newFiles == null || newFiles.length == 0) {
+ // Keep waiting.
+ return false;
+ }
+ // Make sure we have a complete file. We skip assertions and catch all
+ // exceptions here because the condition may not be satisfied now but may
+ // be satisfied later. After the wait is over, we will repeat the same
+ // steps with assertions and exceptions.
+ try {
+ return readPingFile(newFiles[0]).has("slug");
+ } catch (final Exception e) {
+ return false;
+ }
+ }
+ }, WAIT_FOR_PING_TIMEOUT);
+
+ mAsserter.ok(pingDir.exists(), "Ping directory exists", null);
+ mAsserter.ok(pingDir.isDirectory(), "Ping directory is a directory", null);
+
+ final File[] newFiles = pingDir.listFiles();
+ mAsserter.isnot(newFiles, null, "Ping directory is not empty");
+ mAsserter.is(newFiles.length, 1, "ANR reporter wrote one ping");
+ mAsserter.ok(newFiles[0].exists(), "Ping exists", null);
+ mAsserter.ok(newFiles[0].isFile(), "Ping is a file", null);
+ mAsserter.ok(newFiles[0].canRead(), "Ping is readable", null);
+ mAsserter.info("Found ping file", newFiles[0].getPath());
+
+ // Check standard properties required by Telemetry server.
+ final JSONObject pingObject = readPingFile(newFiles[0]);
+ mAsserter.ok(pingObject.has("slug"), "Ping has slug property", null);
+ mAsserter.ok(pingObject.has("reason"), "Ping has reason property", null);
+ mAsserter.ok(pingObject.has("payload"), "Ping has payload property", null);
+
+ final JSONObject pingPayload = pingObject.getJSONObject("payload");
+ mAsserter.ok(pingPayload.has("ver"), "Payload has ver property", null);
+ mAsserter.ok(pingPayload.has("info"), "Payload has info property", null);
+ mAsserter.ok(pingPayload.has("androidANR"), "Payload has androidANR property", null);
+
+ final JSONObject pingInfo = pingPayload.getJSONObject("info");
+ mAsserter.ok(pingInfo.has("reason"), "Info has reason property", null);
+ mAsserter.ok(pingInfo.has("appName"), "Info has appName property", null);
+ mAsserter.ok(pingInfo.has("appUpdateChannel"), "Info has appUpdateChannel property", null);
+ mAsserter.ok(pingInfo.has("appVersion"), "Info has appVersion property", null);
+ mAsserter.ok(pingInfo.has("appBuildID"), "Info has appBuildID property", null);
+
+ // Do some profile clean up. This is not absolutely necessary because the profile
+ // is blown away after test runs anyways, so we don't check return values here.
+ for (final File ping : newFiles) {
+ ping.delete();
+ }
+ pingDir.delete();
+
+ // Unblock UI thread
+ synchronized (uiBlocker) {
+ mDone = true;
+ uiBlocker.notify();
+ }
+
+ // Clear the sample ANR
+ final FileWriter anrClearer = new FileWriter(anrFile);
+ anrClearer.close();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomePageNavigation.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomePageNavigation.java
new file mode 100644
index 0000000000..68f3a38dbd
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomePageNavigation.java
@@ -0,0 +1,107 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.home.HomeConfig.PanelType;
+import org.mozilla.gecko.tests.helpers.DeviceHelper;
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+
+/**
+ * Tests functionality related to navigating between the various about:home panels.
+ */
+public class testAboutHomePageNavigation extends UITest {
+ // TODO: Define this test dynamically by creating dynamic representations of the Page
+ // enum for both phone and tablet, then swiping through the panels. This will also
+ // benefit having a HomePager with custom panels.
+ public void testAboutHomePageNavigation() {
+ GeckoHelper.blockForDelayedStartup();
+
+ mAboutHome.assertVisible()
+ .assertCurrentPanel(PanelType.TOP_SITES);
+
+ mAboutHome.swipeToPanelOnRight();
+ mAboutHome.assertCurrentPanel(PanelType.BOOKMARKS);
+
+ // Ideally these helpers would just be their own tests. However, by keeping this within
+ // one method, we're saving test setUp and tearDown resources.
+ if (DeviceHelper.isTablet()) {
+ helperTestTablet();
+ } else {
+ helperTestPhone();
+ }
+ }
+
+ private void helperTestTablet() {
+ mAboutHome.swipeToPanelOnRight();
+ mAboutHome.assertCurrentPanel(PanelType.COMBINED_HISTORY);
+
+ // Edge case.
+ mAboutHome.swipeToPanelOnRight();
+ mAboutHome.assertCurrentPanel(PanelType.COMBINED_HISTORY);
+
+ mAboutHome.swipeToPanelOnLeft();
+ mAboutHome.assertCurrentPanel(PanelType.BOOKMARKS);
+
+ mAboutHome.swipeToPanelOnLeft();
+ mAboutHome.assertCurrentPanel(PanelType.TOP_SITES);
+
+ // Edge case.
+ mAboutHome.swipeToPanelOnLeft();
+ mAboutHome.assertCurrentPanel(PanelType.TOP_SITES);
+ }
+
+ private void helperTestPhone() {
+ // Edge case.
+ mAboutHome.swipeToPanelOnLeft();
+ mAboutHome.assertCurrentPanel(PanelType.BOOKMARKS);
+
+ mAboutHome.swipeToPanelOnLeft();
+ mAboutHome.assertCurrentPanel(PanelType.TOP_SITES);
+
+ mAboutHome.swipeToPanelOnLeft();
+ mAboutHome.assertCurrentPanel(PanelType.COMBINED_HISTORY);
+
+ // Edge case.
+ mAboutHome.swipeToPanelOnLeft();
+ mAboutHome.assertCurrentPanel(PanelType.COMBINED_HISTORY);
+
+ mAboutHome.swipeToPanelOnRight();
+ mAboutHome.assertCurrentPanel(PanelType.TOP_SITES);
+ }
+
+ // TODO: bug 943706 - reimplement this old test code.
+ /*
+ // Removed by Bug 896576 - [fig] Remove [getAllPagesList] from BaseTest
+ // ListView list = getAllPagesList("about:firefox");
+
+ // Test normal sliding of the list left and right
+ ViewPager pager = (ViewPager)mSolo.getView(ViewPager.class, 0);
+ mAsserter.is(pager.getCurrentItem(), 0, "All pages is selected");
+
+ int width = mDriver.getGeckoWidth() / 2;
+ int y = mDriver.getGeckoHeight() / 2;
+ mActions.drag(width, 0, y, y);
+ mAsserter.is(pager.getCurrentItem(), 1, "Bookmarks page is selected");
+
+ mActions.drag(0, width, y, y);
+ mAsserter.is(pager.getCurrentItem(), 0, "All pages is selected");
+
+ // Test tapping on the tab strip changes tabs
+ TabWidget tabwidget = (TabWidget)mSolo.getView(TabWidget.class, 0);
+ mSolo.clickOnView(tabwidget.getChildAt(1));
+ mAsserter.is(pager.getCurrentItem(), 1, "Clicking on tab selected bookmarks page");
+
+ // Test typing in the awesomebar changes tabs and prevents panning
+ mSolo.typeText(0, "woot");
+ mAsserter.is(pager.getCurrentItem(), 0, "Searching switched to all pages tab");
+ mSolo.scrollToSide(Solo.LEFT);
+ mAsserter.is(pager.getCurrentItem(), 0, "Dragging left is not allowed when searching");
+
+ mSolo.scrollToSide(Solo.RIGHT);
+ mAsserter.is(pager.getCurrentItem(), 0, "Dragging right is not allowed when searching");
+
+ mSolo.goBack();
+ */
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomeVisibility.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomeVisibility.java
new file mode 100644
index 0000000000..3be6ed53f2
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomeVisibility.java
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.home.HomeConfig;
+import org.mozilla.gecko.home.HomeConfig.PanelType;
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+
+/**
+ * Tests the visibility of about:home after various interactions with the browser.
+ */
+public class testAboutHomeVisibility extends UITest {
+ public void testAboutHomeVisibility() {
+ GeckoHelper.blockForReady();
+
+ // Check initial state on about:home.
+ mToolbar.assertTitle(mStringHelper.ABOUT_HOME_URL);
+ mAboutHome.assertVisible()
+ .assertCurrentPanel(PanelType.TOP_SITES);
+
+ // Go to blank 01.
+ NavigationHelper.enterAndLoadUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ mToolbar.assertTitle(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ mAboutHome.assertNotVisible();
+
+ // Go to blank 02.
+ NavigationHelper.enterAndLoadUrl(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL);
+ mToolbar.assertTitle(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL);
+ mAboutHome.assertNotVisible();
+
+ // Enter editing mode, where the about:home UI should be visible.
+ mToolbar.enterEditingMode();
+ mAboutHome.assertVisible()
+ .assertCurrentPanel(PanelType.TOP_SITES);
+
+ // Dismiss editing mode, where the about:home UI should be gone.
+ mToolbar.dismissEditingMode();
+ mAboutHome.assertNotVisible();
+
+ // Loading about:home should show about:home again.
+ NavigationHelper.enterAndLoadUrl(mStringHelper.ABOUT_HOME_URL);
+ mToolbar.assertTitle(mStringHelper.ABOUT_HOME_URL);
+ mAboutHome.assertVisible()
+ .assertCurrentPanel(PanelType.TOP_SITES);
+
+ // We can navigate to about:home panels by panel UUID.
+ mAboutHome.navigateToBuiltinPanelType(PanelType.BOOKMARKS)
+ .assertVisible()
+ .assertCurrentPanel(PanelType.BOOKMARKS);
+ mAboutHome.navigateToBuiltinPanelType(PanelType.COMBINED_HISTORY)
+ .assertVisible()
+ .assertCurrentPanel(PanelType.COMBINED_HISTORY);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutPage.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutPage.java
new file mode 100644
index 0000000000..6a00acd965
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutPage.java
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.Actions;
+
+/* Tests related to the about: page:
+ * - check that about: loads from the URL bar
+ * - check that about: loads from Settings/About...
+ */
+public class testAboutPage extends PixelTest {
+
+ public void testAboutPage() {
+ blockForGeckoReady();
+
+ // Load the about: page and verify its title.
+ String url = mStringHelper.ABOUT_SCHEME;
+ loadAndPaint(url);
+
+ verifyUrlInContentDescription(url);
+
+ // Open a new page to remove the about: page from the current tab.
+ url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ loadUrlAndWait(url);
+
+ // At this point the page title should have been set.
+ verifyUrlInContentDescription(url);
+
+ // Set up listeners to catch the page load we're about to do.
+ Actions.EventExpecter tabEventExpecter = mActions.expectGeckoEvent("Tab:Added");
+ Actions.EventExpecter contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded");
+
+ selectSettingsItem(mStringHelper.MOZILLA_SECTION_LABEL, mStringHelper.ABOUT_LABEL);
+
+ // Wait for the new tab and page to load
+ tabEventExpecter.blockForEvent();
+ contentEventExpecter.blockForEvent();
+
+ tabEventExpecter.unregisterListener();
+ contentEventExpecter.unregisterListener();
+
+ // Make sure the about: page was loaded.
+ verifyUrlInContentDescription(mStringHelper.ABOUT_SCHEME);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAccessibleCarets.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAccessibleCarets.java
new file mode 100644
index 0000000000..d064eb1dd9
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAccessibleCarets.java
@@ -0,0 +1,76 @@
+/**
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
+
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+
+public class testAccessibleCarets extends JavascriptTest {
+ private static final String LOGTAG = "testAccessibleCarets";
+ private static final String TAB_CHANGE_EVENT = "testAccessibleCarets:TabChange";
+
+ private final TabsListener tabsListener;
+
+
+ public testAccessibleCarets() {
+ super("testAccessibleCarets.js");
+
+ tabsListener = new TabsListener();
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ Tabs.registerOnTabsChangedListener(tabsListener);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ Tabs.unregisterOnTabsChangedListener(tabsListener);
+
+ super.tearDown();
+ }
+
+ @Override
+ public void testJavascript() throws Exception {
+ // This feature is currently only available in Nightly.
+ if (!AppConstants.NIGHTLY_BUILD) {
+ mAsserter.dumpLog(LOGTAG + " is disabled on non-Nightly builds: returning");
+ return;
+ }
+ super.testJavascript();
+ }
+
+ /**
+ * Observes tab change events to broadcast to the test script.
+ */
+ private class TabsListener implements Tabs.OnTabsChangedListener {
+ @Override
+ public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
+ switch (msg) {
+ case STOP:
+ final JSONObject args = new JSONObject();
+ try {
+ args.put("tabId", tab.getId());
+ args.put("event", msg.toString());
+ } catch (JSONException e) {
+ Log.e(LOGTAG, "Error building JSON arguments for " + TAB_CHANGE_EVENT, e);
+ return;
+ }
+ mActions.sendGeckoEvent(TAB_CHANGE_EVENT, args.toString());
+ break;
+ }
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testActivityStreamContextMenu.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testActivityStreamContextMenu.java
new file mode 100644
index 0000000000..b4b06a2360
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testActivityStreamContextMenu.java
@@ -0,0 +1,94 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import android.support.design.widget.NavigationView;
+import android.support.v4.app.Fragment;
+import android.view.KeyEvent;
+import android.view.MenuItem;
+import android.view.View;
+
+import com.robotium.solo.Condition;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.home.activitystream.ActivityStream;
+import org.mozilla.gecko.home.activitystream.menu.ActivityStreamContextMenu;
+
+/**
+ * This test is unfortunately closely coupled to the current implementation, however it is still
+ * useful in that it tests the bookmark/history state specific menu items for correctness.
+ */
+public class testActivityStreamContextMenu extends BaseTest {
+ public void testActivityStreamContextMenu() {
+ blockForGeckoReady();
+
+ final String testURL = "http://mozilla.org";
+
+ BrowserDB db = BrowserDB.from(getActivity());
+ db.removeHistoryEntry(getActivity().getContentResolver(), testURL);
+ db.removeBookmarksWithURL(getActivity().getContentResolver(), testURL);
+
+ testMenuForUrl(testURL, false, false);
+
+ db.addBookmark(getActivity().getContentResolver(), "foobar", testURL);
+ testMenuForUrl(testURL, true, false);
+
+ db.updateVisitedHistory(getActivity().getContentResolver(), testURL);
+ testMenuForUrl(testURL, true, true);
+
+ db.removeBookmarksWithURL(getActivity().getContentResolver(), testURL);
+ testMenuForUrl(testURL, false, true);
+ }
+
+ /**
+ * Test that the menu shows the expected menu items for a given URL, and that these items have
+ * the correct state.
+ */
+ private void testMenuForUrl(final String url, final boolean isBookmarked, final boolean isVisited) {
+ final View anchor = new View(getActivity());
+
+ final ActivityStreamContextMenu menu = ActivityStreamContextMenu.show(getActivity(), anchor, ActivityStreamContextMenu.MenuMode.HIGHLIGHT, "foobar", url, null, null, 100, 100);
+
+ final int expectedBookmarkString;
+ if (isBookmarked) {
+ expectedBookmarkString = R.string.bookmark_remove;
+ } else {
+ expectedBookmarkString = R.string.bookmark;
+ }
+
+ final MenuItem bookmarkItem = menu.getItemByID(R.id.bookmark);
+ assertMenuItemHasString(bookmarkItem, expectedBookmarkString);
+
+ final MenuItem deleteItem = menu.getItemByID(R.id.delete);
+ assertMenuItemIsVisible(deleteItem, isVisited);
+
+ menu.dismiss();
+ }
+
+ private void assertMenuItemIsVisible(final MenuItem item, final boolean shouldBeVisible) {
+ waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return (item.isVisible() == shouldBeVisible);
+ }
+ }, 5000);
+
+ mAsserter.is(item.isVisible(), shouldBeVisible, "menu item \"" + item.getTitle() + "\" should be visible");
+ }
+
+ private void assertMenuItemHasString(final MenuItem item, final int stringID) {
+ waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return item.isEnabled();
+ }
+ }, 5000);
+
+ final String expectedTitle = getActivity().getResources().getString(stringID);
+ mAsserter.is(item.getTitle(), expectedTitle, "Title does not match expected title");
+ }
+}
+
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddSearchEngine.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddSearchEngine.java
new file mode 100644
index 0000000000..44bd1f903b
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddSearchEngine.java
@@ -0,0 +1,172 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import java.io.File;
+import java.util.ArrayList;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.home.HomePager;
+import org.mozilla.gecko.home.SearchEngineBar;
+import org.mozilla.gecko.R;
+
+import android.widget.ImageView;
+import android.widget.ListView;
+
+import com.robotium.solo.Condition;
+
+/**
+ * Test adding a search engine from an input field context menu.
+ * 1. Get the number of existing search engines from the SearchEngine:Data event and as displayed in about:home.
+ * 2. Load a page with a text field, open the context menu and add a search engine from the page.
+ * 3. Get the number of search engines after adding the new one and verify it has increased by 1.
+ */
+public class testAddSearchEngine extends AboutHomeTest {
+ private final int MAX_WAIT_TEST_MS = 5000;
+ private final String SEARCH_TEXT = "Firefox for Android";
+ private final String ADD_SEARCHENGINE_OPTION_TEXT = "Add as Search Engine";
+
+ public void testAddSearchEngine() {
+ String blankPageURL = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ String searchEngineURL = getAbsoluteUrl(mStringHelper.ROBOCOP_SEARCH_URL);
+
+ blockForGeckoReady();
+ int height = mDriver.getGeckoTop() + 150;
+ int width = mDriver.getGeckoLeft() + 150;
+
+ inputAndLoadUrl(blankPageURL);
+ waitForText(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE);
+
+ // Get the searchengine data by clicking the awesomebar - this causes Gecko to send Java the list
+ // of search engines.
+ Actions.EventExpecter searchEngineDataEventExpector = mActions.expectGeckoEvent("SearchEngines:Data");
+ focusUrlBar();
+ mActions.sendKeys(SEARCH_TEXT);
+ String eventData = searchEngineDataEventExpector.blockForEventData();
+ searchEngineDataEventExpector.unregisterListener();
+
+ ArrayList<String> searchEngines;
+ try {
+ // Parse the data to get the number of searchengines.
+ searchEngines = getSearchEnginesNames(eventData);
+ } catch (JSONException e) {
+ mAsserter.ok(false, "Fatal exception in testAddSearchEngine while decoding JSON search engine string from Gecko prior to addition of new engine.", e.toString());
+ return;
+ }
+ final int initialNumSearchEngines = searchEngines.size();
+ mAsserter.dumpLog("Search Engines list = " + searchEngines.toString());
+
+ // Verify that the number of displayed search engines is the same as the one received through the SearchEngines:Data event.
+ verifyDisplayedSearchEnginesCount(initialNumSearchEngines);
+
+ // Load the page for the search engine to add.
+ inputAndLoadUrl(searchEngineURL);
+ verifyUrlBarTitle(searchEngineURL);
+
+ // Used to long-tap on the search input box for the search engine to add.
+ getInstrumentation().waitForIdleSync();
+ mAsserter.dumpLog("Long Clicking at width = " + String.valueOf(width) + " and height = " + String.valueOf(height));
+ mSolo.clickLongOnScreen(width,height);
+
+ ImageView view = waitForViewWithDescription(ImageView.class, ADD_SEARCHENGINE_OPTION_TEXT);
+ mAsserter.isnot(view, null, "The action mode was opened");
+
+ // Add the search engine
+ mSolo.clickOnView(view);
+ waitForText("Cancel");
+ clickOnButton("OK");
+ mAsserter.ok(!mSolo.searchText(ADD_SEARCHENGINE_OPTION_TEXT), "Adding the Search Engine", "The add Search Engine pop-up has been closed");
+ waitForText(mStringHelper.ROBOCOP_SEARCH_TITLE); // Make sure the pop-up is closed and we are back at the searchengine page
+
+ // Load Robocop Blank 1 again to give the time for the searchengine to be added
+ // TODO: This is a potential source of intermittent oranges - it's a race condition!
+ loadUrl(blankPageURL);
+ waitForText(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE);
+
+ // Load search engines again and check that the quantity of engines has increased by 1.
+ searchEngineDataEventExpector = mActions.expectGeckoEvent("SearchEngines:Data");
+ focusUrlBar();
+ mActions.sendKeys(SEARCH_TEXT);
+ eventData = searchEngineDataEventExpector.blockForEventData();
+
+ try {
+ // Parse the data to get the number of searchengines
+ searchEngines = getSearchEnginesNames(eventData);
+ } catch (JSONException e) {
+ mAsserter.ok(false, "Fatal exception in testAddSearchEngine while decoding JSON search engine string from Gecko after adding of new engine.", e.toString());
+ return;
+ }
+
+ mAsserter.dumpLog("Search Engines list = " + searchEngines.toString());
+ mAsserter.is(searchEngines.size(), initialNumSearchEngines + 1, "Checking the number of Search Engines has increased");
+
+ // Verify that the number of displayed searchengines is the same as the one received through the SearchEngines:Data event.
+ verifyDisplayedSearchEnginesCount(initialNumSearchEngines + 1);
+ searchEngineDataEventExpector.unregisterListener();
+
+ // Verify that the search plugin XML file for the new engine ended up where we expected it to.
+ // This file name is created in nsSearchService.js based on the name of the new engine.
+ final File f = GeckoProfile.get(getActivity()).getFile("searchplugins/robocop-search-engine.xml");
+ mAsserter.ok(f.exists(), "Checking that new search plugin file exists", "");
+ }
+
+ /**
+ * Helper method to decode a list of search engine names from the provided search engine information
+ * JSON string sent from Gecko.
+ * @param searchEngineData The JSON string representing the search engine array to process
+ * @return An ArrayList<String> containing the names of all the search engines represented in
+ * the provided JSON message.
+ * @throws JSONException In the event that the JSON provided cannot be decoded.
+ */
+ public ArrayList<String> getSearchEnginesNames(String searchEngineData) throws JSONException {
+ JSONObject data = new JSONObject(searchEngineData);
+ JSONArray engines = data.getJSONArray("searchEngines");
+
+ ArrayList<String> searchEngineNames = new ArrayList<String>();
+ for (int i = 0; i < engines.length(); i++) {
+ JSONObject engineJSON = engines.getJSONObject(i);
+ searchEngineNames.add(engineJSON.getString("name"));
+ }
+ return searchEngineNames;
+ }
+
+ /**
+ * Method to verify that the displayed number of search engines matches the expected number.
+ * @param expectedCount The expected number of search engines.
+ */
+ public void verifyDisplayedSearchEnginesCount(final int expectedCount) {
+ mSolo.clearEditText(0);
+ mActions.sendKeys(SEARCH_TEXT);
+ boolean correctNumSearchEnginesDisplayed = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ ListView searchResultList = findListViewWithTag(HomePager.LIST_TAG_BROWSER_SEARCH);
+ if (searchResultList == null || searchResultList.getAdapter() == null) {
+ return false;
+ }
+
+ SearchEngineBar searchEngineBar = (SearchEngineBar) mSolo.getView(R.id.search_engine_bar);
+ if (searchEngineBar == null || searchEngineBar.getAdapter() == null) {
+ return false;
+ }
+
+ final int actualCount = searchResultList.getAdapter().getCount()
+ + searchEngineBar.getAdapter().getItemCount()
+ - 1; // Subtract one for the search engine bar label (Bug 1172071)
+
+ return (actualCount == expectedCount);
+ }
+ }, MAX_WAIT_TEST_MS);
+
+ // Exit about:home
+ mSolo.goBack();
+ waitForText(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE);
+ mAsserter.ok(correctNumSearchEnginesDisplayed, expectedCount + " Search Engines should be displayed" , "The correct number of Search Engines has been displayed");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddonManager.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddonManager.java
new file mode 100644
index 0000000000..4256d93c4d
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddonManager.java
@@ -0,0 +1,79 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.json.JSONObject;
+import org.mozilla.gecko.Actions;
+
+import android.util.DisplayMetrics;
+
+/**
+ * This test performs the following steps to check the behavior of the Add-on Manager:
+ *
+ * 1) Open the Add-on Manager from the Add-ons menu item, and then close it.
+ * 2) Open the Add-on Manager by visiting about:addons in the URL bar.
+ * 3) Open a new tab, select the Add-ons menu item, then verify that the existing
+ * Add-on Manager tab was selected, instead of opening a new tab.
+ */
+public class testAddonManager extends PixelTest {
+ public void testAddonManager() {
+ Actions.EventExpecter tabEventExpecter;
+ Actions.EventExpecter contentEventExpecter;
+ final String aboutAddonsURL = mStringHelper.ABOUT_ADDONS_URL;
+
+ blockForGeckoReady();
+
+ // Use the menu to open the Addon Manger
+ selectMenuItem(mStringHelper.ADDONS_LABEL);
+
+ // Set up listeners to catch the page load we're about to do
+ tabEventExpecter = mActions.expectGeckoEvent("Tab:Added");
+ contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded");
+
+ // Wait for the new tab and page to load
+ tabEventExpecter.blockForEvent();
+ contentEventExpecter.blockForEvent();
+
+ tabEventExpecter.unregisterListener();
+ contentEventExpecter.unregisterListener();
+
+ // Verify the url
+ verifyUrlBarTitle(aboutAddonsURL);
+
+ // Close the Add-on Manager
+ mSolo.goBack();
+
+ // Load the about:addons page and verify it was loaded
+ loadAndPaint(aboutAddonsURL);
+ verifyUrlBarTitle(aboutAddonsURL);
+
+ // Setup wait for tab to spawn and load
+ tabEventExpecter = mActions.expectGeckoEvent("Tab:Added");
+ contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded");
+
+ // Open a new tab
+ final String blankURL = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ addTab(blankURL);
+
+ // Wait for the new tab and page to load
+ tabEventExpecter.blockForEvent();
+ contentEventExpecter.blockForEvent();
+
+ tabEventExpecter.unregisterListener();
+ contentEventExpecter.unregisterListener();
+
+ // Verify tab count has increased
+ verifyTabCount(2);
+
+ // Verify the page was opened
+ verifyUrlBarTitle(blankURL);
+
+ // Addons Manager is not opened 2 separate times when opened from the menu
+ selectMenuItem(mStringHelper.ADDONS_LABEL);
+
+ // Verify tab count not increased
+ verifyTabCount(2);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAdobeFlash.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAdobeFlash.java
new file mode 100644
index 0000000000..13f7f817a3
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAdobeFlash.java
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.json.JSONObject;
+import org.mozilla.gecko.PaintedSurface;
+
+import android.os.Build;
+
+/**
+ * Tests that Flash is working
+ * - loads a page containing a Flash plugin
+ * - verifies it rendered properly
+ */
+public class testAdobeFlash extends PixelTest {
+ public void testLoad() {
+ // This test only works on ICS and higher
+ if (Build.VERSION.SDK_INT < 15) {
+ blockForGeckoReady();
+ return;
+ }
+
+ // Enable plugins
+ setPreferenceAndWaitForChange("plugin.enable", "1");
+
+ blockForGeckoReady();
+
+ String url = getAbsoluteUrl(mStringHelper.ROBOCOP_ADOBE_FLASH_URL);
+ PaintedSurface painted = loadAndGetPainted(url);
+
+ mAsserter.ispixel(painted.getPixelAt(0, 0), 0, 0xff, 0, "Pixel at 0, 0");
+ mAsserter.ispixel(painted.getPixelAt(50, 50), 0, 0xff, 0, "Pixel at 50, 50");
+ mAsserter.ispixel(painted.getPixelAt(101, 0), 0xff, 0xff, 0xff, "Pixel at 101, 0");
+ mAsserter.ispixel(painted.getPixelAt(0, 101), 0xff, 0xff, 0xff, "Pixel at 0, 101");
+
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAppMenuPathways.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAppMenuPathways.java
new file mode 100644
index 0000000000..69efb4decf
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAppMenuPathways.java
@@ -0,0 +1,77 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.json.JSONObject;
+import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.tests.components.AppMenuComponent;
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+
+import com.robotium.solo.Solo;
+
+/**
+ * Set of tests to test UI App menu and submenus the user interact with.
+ */
+public class testAppMenuPathways extends UITest {
+
+ /**
+ * Robocop supports only a single test function per test class. Therefore, we
+ * have a single top-level test function that dispatches to sub-tests.
+ */
+ public void testAppMenuPathways() {
+ GeckoHelper.blockForReady();
+
+ _testHardwareMenuKeyOpenClose();
+ _testSaveAsPDFPathway();
+ }
+
+ public void _testHardwareMenuKeyOpenClose() {
+ mAppMenu.assertMenuIsNotOpen();
+
+ mSolo.sendKey(Solo.MENU);
+ mAppMenu.waitForMenuOpen();
+ mAppMenu.assertMenuIsOpen();
+
+ mSolo.sendKey(Solo.MENU);
+ mAppMenu.waitForMenuClose();
+ mAppMenu.assertMenuIsNotOpen();
+ }
+
+ public void _testSaveAsPDFPathway() {
+ // Page menu should be disabled in about:home.
+ mAppMenu.assertMenuItemIsDisabledAndVisible(AppMenuComponent.PageMenuItem.SAVE_AS_PDF);
+
+ // Generate a mock Content:LocationChange message with video mime-type for the current tab (tabId = 0).
+ final JSONObject message = new JSONObject();
+ try {
+ message.put("contentType", "video/webm");
+ message.put("baseDomain", "webmfiles.org");
+ message.put("type", "Content:LocationChange");
+ message.put("sameDocument", false);
+ message.put("userRequested", "");
+ message.put("uri", getAbsoluteIpUrl("/big-buck-bunny_trailer.webm"));
+ message.put("tabID", 0);
+ } catch (Exception ex) {
+ mAsserter.ok(false, "exception in testSaveAsPDFPathway", ex.toString());
+ }
+
+ // Mock video playback with the generated message and Content:LocationChange event.
+ Tabs.getInstance().handleMessage("Content:LocationChange", message);
+
+ // Save as pdf menu is disabled while playing video.
+ mAppMenu.assertMenuItemIsDisabledAndVisible(AppMenuComponent.PageMenuItem.SAVE_AS_PDF);
+
+ // The above mock video playback test changes Java state, but not the associated JS state.
+ // Navigate to a new page so that the Java state is cleared.
+ NavigationHelper.enterAndLoadUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ mToolbar.assertTitle(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+
+ // Test save as pdf functionality.
+ // The following call doesn't wait for the resulting pdf but checks that no exception are thrown.
+ // NOTE: save as pdf functionality must be done at the end as it is slow and cause other test operations to fail.
+ mAppMenu.pressMenuItem(AppMenuComponent.PageMenuItem.SAVE_AS_PDF);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAxisLocking.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAxisLocking.java
new file mode 100644
index 0000000000..72bf62e042
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAxisLocking.java
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.PaintedSurface;
+
+/**
+ * Basic test for axis locking behaviour.
+ * - Load page and verify it draws
+ * - Drag page upwards 100 pixels at a 5-degree angle off the vertical axis
+ * - Verify that the 5-degree angle was thrown out and it dragged vertically
+ * - Drag page upwards at a 45-degree angle
+ * - Verify that the 45-degree angle was not thrown out and it dragged diagonally
+ */
+public class testAxisLocking extends PixelTest {
+ public void testAxisLocking() {
+ String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BOXES_URL);
+
+ MotionEventHelper meh = new MotionEventHelper(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop());
+
+ blockForGeckoReady();
+
+ // load page and check we're at 0,0
+ loadAndVerifyBoxes(url);
+
+ // drag page upwards by 100 pixels with a slight angle. verify that
+ // axis locking prevents any horizontal scrolling
+ Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint();
+ meh.dragSync(20, 150, 10, 50);
+ PaintedSurface painted = waitForPaint(paintExpecter);
+ paintExpecter.unregisterListener();
+ try {
+ checkScrollWithBoxes(painted, 0, 100);
+ // since checkScrollWithBoxes only checks 4 points, it may not pick up a
+ // sub-100 pixel horizontal shift. so we check another point manually to make sure.
+ int[] color = getBoxColorAt(0, 100);
+ mAsserter.ispixel(painted.getPixelAt(99, 0), color[0], color[1], color[2], "Pixel at 99, 0 indicates no horizontal scroll");
+
+ // now drag at a 45-degree angle to ensure we break the axis lock, and
+ // verify that we have both horizontal and vertical scrolling
+ paintExpecter = mActions.expectPaint();
+ meh.dragSync(150, 150, 50, 50);
+ } finally {
+ painted.close();
+ }
+
+ painted = waitForPaint(paintExpecter);
+ paintExpecter.unregisterListener();
+ try {
+ checkScrollWithBoxes(painted, 100, 200);
+ } finally {
+ painted.close();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBackButtonInEditMode.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBackButtonInEditMode.java
new file mode 100644
index 0000000000..b391f79209
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBackButtonInEditMode.java
@@ -0,0 +1,47 @@
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+
+import android.view.View;
+
+/**
+ * Tests that verify the behavior of back button in edit mode.
+ */
+public class testBackButtonInEditMode extends UITest {
+ public void testBackButtonInEditMode() {
+ GeckoHelper.blockForReady();
+
+ // Verify back button behavior for edit mode.
+ mToolbar.enterEditingMode()
+ .assertIsUrlEditTextSelected();
+ checkBackPressInEditMode();
+ checkExitUsingBackButton();
+
+ // Verify back button behavior in edit mode after input.
+ mToolbar.enterEditingMode()
+ .enterUrl("dummy")
+ .assertIsUrlEditTextSelected();
+ checkBackPressInEditMode();
+ checkExitUsingBackButton();
+
+ // Verify the swipe behavior in edit mode.
+ mToolbar.enterEditingMode()
+ .assertIsUrlEditTextSelected();
+ mAboutHome.swipeToPanelOnLeft();
+ mToolbar.assertIsUrlEditTextNotSelected()
+ .assertIsEditing();
+ checkExitUsingBackButton();
+ }
+
+ private void checkBackPressInEditMode() {
+ // Press back button and verify URLEditText is not selected.
+ getSolo().goBack();
+ mToolbar.assertIsUrlEditTextNotSelected()
+ .assertIsEditing();
+ }
+
+ private void checkExitUsingBackButton() {
+ getSolo().goBack();
+ mToolbar.assertIsNotEditing();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmark.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmark.java
new file mode 100644
index 0000000000..041b76e2fc
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmark.java
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import com.robotium.solo.Condition;
+
+public class testBookmark extends AboutHomeTest {
+ private static String BOOKMARK_URL;
+ private static final int WAIT_FOR_BOOKMARKED_TIMEOUT = 10000;
+
+ public void testBookmark() {
+ BOOKMARK_URL = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ runAboutHomeTest();
+ runMenuTest();
+ }
+
+ public void runMenuTest() {
+ mAsserter.is(mDatabaseHelper.isBookmark(BOOKMARK_URL), false, "Page is not bookmarked initially");
+ setUpBookmark(); // loads the page, taps the star button, and waits for the "Bookmark Added" message
+ waitForBookmarked(true);
+
+ cleanUpBookmark(); // loads the page, taps the star button, and waits for the "Bookmark Removed" message
+ waitForBookmarked(false);
+ }
+
+ public void runAboutHomeTest() {
+ blockForGeckoReady();
+ for (String url : mStringHelper.DEFAULT_BOOKMARKS_URLS) {
+ mAsserter.ok(mDatabaseHelper.isBookmark(url), "Checking that " + url + " is bookmarked by default", url + " is bookmarked");
+ }
+
+ mDatabaseHelper.addMobileBookmark(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE, BOOKMARK_URL);
+ waitForBookmarked(true);
+
+ isBookmarkDisplayed(BOOKMARK_URL);
+ loadBookmark(BOOKMARK_URL);
+ verifyUrlBarTitle(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+
+ mDatabaseHelper.deleteBookmark(BOOKMARK_URL);
+ waitForBookmarked(false);
+ }
+
+ private void waitForBookmarked(final boolean isBookmarked) {
+ boolean bookmarked = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return (isBookmarked) ?
+ mDatabaseHelper.isBookmark(BOOKMARK_URL) :
+ !mDatabaseHelper.isBookmark(BOOKMARK_URL);
+ }
+ }, WAIT_FOR_BOOKMARKED_TIMEOUT);
+ mAsserter.is(bookmarked, true, BOOKMARK_URL + " was " + (isBookmarked ? "added as a bookmark" : "removed from bookmarks"));
+ }
+
+ private void setUpBookmark() {
+ // Bookmark a page for the test
+ loadUrl(BOOKMARK_URL);
+ waitForText(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE);
+ toggleBookmark();
+ mAsserter.is(waitForText(mStringHelper.BOOKMARK_ADDED_LABEL), true, "bookmark added successfully");
+ }
+
+ private void cleanUpBookmark() {
+ // Go back to the page we bookmarked
+ loadUrl(BOOKMARK_URL);
+ waitForText(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE);
+ toggleBookmark();
+ mAsserter.is(waitForText(mStringHelper.BOOKMARK_REMOVED_LABEL), true, "bookmark removed successfully");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarkFolders.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarkFolders.java
new file mode 100644
index 0000000000..6205337ea3
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarkFolders.java
@@ -0,0 +1,169 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.home.HomePager;
+import org.mozilla.gecko.sync.Utils;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.net.Uri;
+import android.view.View;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.robotium.solo.Condition;
+
+public class testBookmarkFolders extends AboutHomeTest {
+ private static String DESKTOP_BOOKMARK_URL;
+
+ public void testBookmarkFolders() {
+ DESKTOP_BOOKMARK_URL = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL);
+
+ setUpDesktopBookmarks();
+ checkBookmarkList();
+ }
+
+ private void checkBookmarkList() {
+ openAboutHomeTab(AboutHomeTabs.BOOKMARKS);
+ waitForText(mStringHelper.DESKTOP_FOLDER_LABEL);
+ clickOnBookmarkFolder(mStringHelper.DESKTOP_FOLDER_LABEL);
+ waitForText(mStringHelper.TOOLBAR_FOLDER_LABEL);
+
+ // Verify the number of folders displayed in the Desktop Bookmarks folder is correct
+ ListView desktopFolderContent = findListViewWithTag(HomePager.LIST_TAG_BOOKMARKS);
+ ListAdapter adapter = desktopFolderContent.getAdapter();
+
+ // Three folders and "Up to Bookmarks".
+ mAsserter.is(adapter.getCount(), 4, "Checking that the correct number of folders is displayed in the Desktop Bookmarks folder");
+
+ clickOnBookmarkFolder(mStringHelper.TOOLBAR_FOLDER_LABEL);
+
+ // Go up in the bookmark folder hierarchy
+ clickOnBookmarkFolder(String.format(mStringHelper.BOOKMARKS_UP_TO, mStringHelper.DESKTOP_FOLDER_LABEL));
+ mAsserter.ok(waitForText(mStringHelper.BOOKMARKS_MENU_FOLDER_LABEL), "Going up in the folder hierarchy", "We are back in the Desktop Bookmarks folder");
+
+ clickOnBookmarkFolder(String.format(mStringHelper.BOOKMARKS_UP_TO, mStringHelper.BOOKMARKS_ROOT_LABEL));
+ mAsserter.ok(waitForText(mStringHelper.DESKTOP_FOLDER_LABEL), "Going up in the folder hierarchy", "We are back in the main Bookmarks List View");
+
+ clickOnBookmarkFolder(mStringHelper.DESKTOP_FOLDER_LABEL);
+ clickOnBookmarkFolder(mStringHelper.TOOLBAR_FOLDER_LABEL);
+ isBookmarkDisplayed(DESKTOP_BOOKMARK_URL);
+
+ // Open the bookmark from a bookmark folder hierarchy
+ loadBookmark(DESKTOP_BOOKMARK_URL);
+ verifyUrlBarTitle(DESKTOP_BOOKMARK_URL);
+ openAboutHomeTab(AboutHomeTabs.BOOKMARKS);
+
+ // Check that folders don't have a context menu
+ boolean success = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ View desktopFolder = getBookmarkFolderView(mStringHelper.DESKTOP_FOLDER_LABEL);
+ if (desktopFolder == null) {
+ return false;
+ }
+ mSolo.clickLongOnView(desktopFolder);
+ return true; }
+ }, MAX_WAIT_MS);
+
+ mAsserter.ok(success, "Trying to long click on the Desktop Bookmarks","Desktop Bookmarks folder could not be long clicked");
+
+ final String contextMenuString = mStringHelper.BOOKMARK_CONTEXT_MENU_ITEMS[0];
+ mAsserter.ok(!waitForText(contextMenuString), "Folders do not have context menus", "The context menu was not opened");
+
+ // Even if no context menu is opened long clicking a folder still opens it. We need to close it.
+ clickOnBookmarkFolder(String.format(mStringHelper.BOOKMARKS_UP_TO, mStringHelper.BOOKMARKS_ROOT_LABEL));
+ }
+
+ private void clickOnBookmarkFolder(final String folderName) {
+ boolean success = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ View bookmarksFolder = getBookmarkFolderView(folderName);
+ if (bookmarksFolder == null) {
+ return false;
+ }
+ mSolo.waitForView(bookmarksFolder);
+ mSolo.clickOnView(bookmarksFolder);
+ return true;
+ }
+ }, MAX_WAIT_MS);
+ mAsserter.ok(success, "Trying to click on the " + folderName + " folder","The " + folderName + " folder was clicked");
+ }
+
+ private View getBookmarkFolderView(String folderName) {
+ openAboutHomeTab(AboutHomeTabs.BOOKMARKS);
+ mSolo.hideSoftKeyboard();
+ getInstrumentation().waitForIdleSync();
+
+ ListView bookmarksTabList = findListViewWithTag(HomePager.LIST_TAG_BOOKMARKS);
+ if (!waitForNonEmptyListToLoad(bookmarksTabList)) {
+ return null;
+ }
+
+ ListAdapter adapter = bookmarksTabList.getAdapter();
+ if (adapter == null) {
+ return null;
+ }
+
+ for (int i = 0; i < adapter.getCount(); i++ ) {
+ View bookmarkView = bookmarksTabList.getChildAt(i);
+ if (bookmarkView instanceof TextView) {
+ TextView folderTextView = (TextView) bookmarkView;
+ if (folderTextView.getText().equals(folderName)) {
+ return bookmarkView;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ // Add a bookmark in the Desktop folder so we can check the folder navigation in the bookmarks page
+ private void setUpDesktopBookmarks() {
+ blockForGeckoReady();
+
+ // Get the folder id of the mStringHelper.DESKTOP_FOLDER_LABEL folder
+ Long desktopFolderId = mDatabaseHelper.getFolderIdFromGuid("toolbar");
+
+ // Generate a Guid for the bookmark
+ final String generatedGuid = Utils.generateGuid();
+ mAsserter.ok((generatedGuid != null), "Generating a random Guid for the bookmark", "We could not generate a Guid for the bookmark");
+
+ // Insert the bookmark
+ ContentResolver resolver = getActivity().getContentResolver();
+ Uri bookmarksUri = mDatabaseHelper.buildUri(DatabaseHelper.BrowserDataType.BOOKMARKS);
+
+ long now = System.currentTimeMillis();
+ ContentValues values = new ContentValues();
+ values.put("title", mStringHelper.ROBOCOP_BLANK_PAGE_02_TITLE);
+ values.put("url", DESKTOP_BOOKMARK_URL);
+ values.put("parent", desktopFolderId);
+ values.put("modified", now);
+ values.put("type", 1);
+ values.put("guid", generatedGuid);
+ values.put("position", 10);
+ values.put("created", now);
+
+ int updated = resolver.update(bookmarksUri,
+ values,
+ "url = ?",
+ new String[] { DESKTOP_BOOKMARK_URL });
+ if (updated == 0) {
+ Uri uri = resolver.insert(bookmarksUri, values);
+ mAsserter.ok(true, "Inserted at: ", uri.toString());
+ } else {
+ mAsserter.ok(false, "Failed to insert the Desktop bookmark", "Something went wrong");
+ }
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mDatabaseHelper.deleteBookmark(DESKTOP_BOOKMARK_URL);
+ super.tearDown();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarkKeyword.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarkKeyword.java
new file mode 100644
index 0000000000..363954bfaf
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarkKeyword.java
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+
+public class testBookmarkKeyword extends AboutHomeTest {
+ public void testBookmarkKeyword() {
+ blockForGeckoReady();
+
+ final String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ final String keyword = "testkeyword";
+
+ // Add a bookmark, and update it to have a keyword.
+ mDatabaseHelper.addMobileBookmark(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE, url);
+ mDatabaseHelper.updateBookmark(url, mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE, keyword);
+
+ // Enter the keyword in the urlbar.
+ inputAndLoadUrl(keyword);
+
+ // Make sure the title of the page appeared.
+ verifyUrlBarTitle(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+
+ // Delete the bookmark to clean up.
+ mDatabaseHelper.deleteBookmark(url);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarklets.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarklets.java
new file mode 100644
index 0000000000..4ae57104c4
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarklets.java
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.Actions;
+
+import com.robotium.solo.Condition;
+
+
+public class testBookmarklets extends BaseTest {
+ public void testBookmarklets() {
+ final String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ final String title = "alertBookmarklet";
+ final String js = "javascript:alert(12 + 10)";
+ final String expected = "22";
+ boolean alerted;
+
+ blockForGeckoReady();
+
+ // Load a standard page so bookmarklets work
+ loadUrlAndWait(url);
+
+ // Verify that user-entered bookmarklets do *not* work
+ enterUrl(js);
+ mActions.sendSpecialKey(Actions.SpecialKey.ENTER);
+ alerted = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return mSolo.searchButton("OK", true) || mSolo.searchText(expected, true);
+ }
+ }, 3000);
+ mAsserter.is(alerted, false, "Alert was not shown for user-entered bookmarklet");
+
+ // Verify that non-user-entered bookmarklets do work
+ loadUrl(js);
+ alerted = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return mSolo.searchButton("OK", true) && mSolo.searchText(expected, true);
+ }
+ }, 10000);
+ mAsserter.is(alerted, true, "Alert was shown for bookmarklet");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarksPanel.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarksPanel.java
new file mode 100644
index 0000000000..a7e9505da4
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarksPanel.java
@@ -0,0 +1,174 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.Element;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.util.StringUtils;
+
+public class testBookmarksPanel extends AboutHomeTest {
+ public void testBookmarksPanel() {
+ final String BOOKMARK_URL = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ JSONObject data = null;
+
+ // Make sure our default bookmarks are loaded.
+ // Technically this will race with the check below.
+ initializeProfile();
+
+ // Add a mobile bookmark.
+ mDatabaseHelper.addMobileBookmark(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE, BOOKMARK_URL);
+
+ openAboutHomeTab(AboutHomeTabs.BOOKMARKS);
+
+ // Check that the default bookmarks are displayed.
+ // We need to wait for the distribution to have been processed
+ // before this will succeed.
+ for (String url : mStringHelper.DEFAULT_BOOKMARKS_URLS) {
+ isBookmarkDisplayed(url);
+ }
+
+ assertAllContextMenuOptionsArePresent(mStringHelper.DEFAULT_BOOKMARKS_URLS[1],
+ mStringHelper.DEFAULT_BOOKMARKS_URLS[0]);
+
+ openBookmarkContextMenu(mStringHelper.DEFAULT_BOOKMARKS_URLS[0]);
+
+ // Test that "Open in New Tab" works
+ final Element tabCount = mDriver.findElement(getActivity(), R.id.tabs_counter);
+ final int tabCountInt = Integer.parseInt(tabCount.getText());
+ Actions.EventExpecter tabEventExpecter = mActions.expectGeckoEvent("Tab:Added");
+ mSolo.clickOnText(mStringHelper.BOOKMARK_CONTEXT_MENU_ITEMS[0]);
+ try {
+ data = new JSONObject(tabEventExpecter.blockForEventData());
+ } catch (JSONException e) {
+ mAsserter.ok(false, "exception getting event data", e.toString());
+ }
+ tabEventExpecter.unregisterListener();
+ mAsserter.ok(mSolo.searchText(mStringHelper.TITLE_PLACE_HOLDER), "Checking that the tab is not changed", "The tab was not changed");
+ // extra check here on the Tab:Added message to be sure the right tab opened
+ int tabID = 0;
+ try {
+ mAsserter.is(mStringHelper.ABOUT_FIREFOX_URL, data.getString("uri"), "Checking tab uri");
+ tabID = data.getInt("tabID");
+ } catch (JSONException e) {
+ mAsserter.ok(false, "exception accessing event data", e.toString());
+ }
+ // close tab so about:firefox can be selected again
+ closeTab(tabID);
+
+ // Test that "Open in Private Tab" works
+ openBookmarkContextMenu(mStringHelper.DEFAULT_BOOKMARKS_URLS[0]);
+ tabEventExpecter = mActions.expectGeckoEvent("Tab:Added");
+ mSolo.clickOnText(mStringHelper.BOOKMARK_CONTEXT_MENU_ITEMS[1]);
+ try {
+ data = new JSONObject(tabEventExpecter.blockForEventData());
+ } catch (JSONException e) {
+ mAsserter.ok(false, "exception getting event data", e.toString());
+ }
+ tabEventExpecter.unregisterListener();
+ mAsserter.ok(mSolo.searchText(mStringHelper.TITLE_PLACE_HOLDER), "Checking that the tab is not changed", "The tab was not changed");
+ // extra check here on the Tab:Added message to be sure the right tab opened, again
+ try {
+ mAsserter.is(mStringHelper.ABOUT_FIREFOX_URL, data.getString("uri"), "Checking tab uri");
+ } catch (JSONException e) {
+ mAsserter.ok(false, "exception accessing event data", e.toString());
+ }
+
+ // Test that "Edit" works
+ String[] editedBookmarkValues = new String[] { "New bookmark title", "www.NewBookmark.url", "newBookmarkKeyword" };
+ editBookmark(BOOKMARK_URL, editedBookmarkValues);
+ checkBookmarkEdit(editedBookmarkValues[1], editedBookmarkValues);
+
+ // Test that "Remove" works
+ openBookmarkContextMenu(editedBookmarkValues[1]);
+ mSolo.clickOnText(mStringHelper.BOOKMARK_CONTEXT_MENU_ITEMS[5]);
+ waitForText(mStringHelper.BOOKMARK_REMOVED_LABEL);
+ mAsserter.ok(!mDatabaseHelper.isBookmark(editedBookmarkValues[1]), "Checking that the bookmark was removed", "The bookmark was removed");
+ }
+
+ /**
+ * Asserts that all context menu items are present on the given links. For one link,
+ * the context menu is expected to not have the "Share" context menu item.
+ *
+ * @param shareableURL A URL that is expected to have the "Share" context menu item
+ * @param nonShareableURL A URL that is expected not to have the "Share" context menu item.
+ */
+ private void assertAllContextMenuOptionsArePresent(final String shareableURL,
+ final String nonShareableURL) {
+ mAsserter.ok(StringUtils.isShareableUrl(shareableURL), "Ensuring url is shareable", "");
+ mAsserter.ok(!StringUtils.isShareableUrl(nonShareableURL), "Ensuring url is not shareable", "");
+
+ openBookmarkContextMenu(shareableURL);
+ for (String contextMenuOption : mStringHelper.BOOKMARK_CONTEXT_MENU_ITEMS) {
+ mAsserter.ok(mSolo.searchText(contextMenuOption),
+ "Checking that the context menu option is present",
+ contextMenuOption + " is present");
+ }
+
+ // Close the menu.
+ mSolo.goBack();
+
+ openBookmarkContextMenu(nonShareableURL);
+ for (String contextMenuOption : mStringHelper.BOOKMARK_CONTEXT_MENU_ITEMS) {
+ // This link is not shareable: skip the "Share" option.
+ if ("Share".equals(contextMenuOption)) {
+ continue;
+ }
+
+ mAsserter.ok(mSolo.searchText(contextMenuOption),
+ "Checking that the context menu option is present",
+ contextMenuOption + " is present");
+ }
+
+ // The use of Solo.searchText is potentially fragile as It will only
+ // scroll the most recently drawn view. Works fine for now though.
+ mAsserter.ok(!mSolo.searchText("Share"),
+ "Checking that the Share option is not present",
+ "Share option is not present");
+
+ // Close the menu.
+ mSolo.goBack();
+ }
+
+ /**
+ * @param bookmarkUrl URL of the bookmark to edit
+ * @param values String array with the new values for all fields
+ */
+ private void editBookmark(String bookmarkUrl, String[] values) {
+ openBookmarkContextMenu(bookmarkUrl);
+ mSolo.clickOnText(mStringHelper.CONTEXT_MENU_EDIT);
+ waitForText(mStringHelper.EDIT_BOOKMARK);
+
+ // Update the fields with the new values
+ for (int i = 0; i < values.length; i++) {
+ mSolo.clearEditText(i);
+ mSolo.clickOnEditText(i);
+ mActions.sendKeys(values[i]);
+ }
+
+ mSolo.clickOnButton(mStringHelper.OK);
+ waitForText(mStringHelper.BOOKMARK_UPDATED_LABEL);
+ }
+
+ /**
+ * @param bookmarkUrl String with the original url
+ * @param values String array with the new values for all fields
+ */
+ private void checkBookmarkEdit(String bookmarkUrl, String[] values) {
+ openBookmarkContextMenu(bookmarkUrl);
+ mSolo.clickOnText(mStringHelper.CONTEXT_MENU_EDIT);
+ waitForText(mStringHelper.EDIT_BOOKMARK);
+
+ // Check the values of the fields
+ for (String value : values) {
+ mAsserter.ok(mSolo.searchText(value), "Checking that the value is correct", "The value = " + value + " is correct");
+ }
+
+ mSolo.clickOnButton("Cancel");
+ waitForText(mStringHelper.BOOKMARKS_LABEL);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserDatabaseHelperUpgrades.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserDatabaseHelperUpgrades.java
new file mode 100644
index 0000000000..eec5c4b33f
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserDatabaseHelperUpgrades.java
@@ -0,0 +1,150 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.*;
+
+import android.database.sqlite.SQLiteDatabase;
+import android.util.Log;
+
+import org.mozilla.gecko.db.BrowserDatabaseHelper;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+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.util.ArrayList;
+
+// TODO: Move to junit 3 tests, once those run in automation. There is no ui testing to do so it's a better fit.
+/**
+ * This test runs upgrades for the databases in (robocop-assets)/browser_db_upgrade. Currently,
+ * (robocop-assets)=mobile/android/tests/browser/robocop/assets/.
+ *
+ * It copies the old database from the robocop assets directory into a temporary file and opens a SQLiteHelper
+ * on the database to verify it gets upgraded to the correct version. If there is an issue with the upgrade,
+ * generally a SQLiteException will be thrown and the test will fail. For example:
+ * android.database.sqlite.SQLiteException: duplicate column name: calculated_pages_times_rating (code 1): , while compiling: ALTER TABLE book_information ADD COLUMN calculated_pages_times_rating INTEGER;
+ *
+ * Alternative upgrade tests:
+ * * Robolectric 2.3+ uses a real SQLite database implementation so we could run our upgrades there. However, the
+ * SQLite implementation may not be the same as we run on Android. Benefits: speed & we don't need the application to
+ * run (and thus a valid DB of the latest version) to run these tests.
+ * * We could copy the current database creation code into a new test, create the database, and then try to upgrade
+ * it. However, the tables are empty and thus not a realistic migration plan (e.g. foreign key constraints).
+ *
+ * TO EDIT THIS TEST:
+ * * Copy the current version of the database into (robocop-assets)/browser_db_upgrade/v##.db database. You can do
+ * this via Margaret's copy profile addon - take browser.db from the profile directory. This db copy should contain a
+ * used profile - e.g. history items, bookmarks. A good way to get a used profile is to sign into sync.
+ * * MAKE SURE YOU COPY YOUR DB FIRST. Then make your changes to the DB schema code.
+ * * Test!
+ * * Note: when the application starts for testing, it may need to upgrade the database from your existing version. If
+ * this fails, the application will crash and the test may fail to start.
+ *
+ * IMPORTANT:
+ * Test DBs must be created on the oldest version of Android that is currently supported. SQLite
+ * is not forwards compatible. E.g. uploading a DB created on a 6.0 device will cause failures
+ * when robocop tests running on 4.3 are unable to load it.
+ *
+ * Implementation inspired by:
+ * http://riggaroo.co.za/automated-testing-sqlite-database-upgrades-android/
+ */
+public class testBrowserDatabaseHelperUpgrades extends UITest {
+ private static final int TEST_FROM_VERSION = 27; // We only started testing on this version.
+
+ private ArrayList<File> temporaryDbFiles;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ // TODO: We already install & remove the profile directory each run so it'd be safer for clean-up to store
+ // this there. That being said, temporary files are still stored in the application directory so these temporary
+ // files will get cleaned up when the application is uninstalled or when data is cleared.
+ temporaryDbFiles = new ArrayList<>();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ for (final File dbFile : temporaryDbFiles) {
+ dbFile.delete();
+ }
+ }
+
+ /**
+ * @throws IOException if the database cannot be copied.
+ */
+ public void test() throws IOException {
+ for (int i = TEST_FROM_VERSION; i < BrowserDatabaseHelper.DATABASE_VERSION; ++i) {
+ Log.d(LOGTAG, "Testing upgrade from version: " + i);
+ final String tempDbPath = copyDatabase(i);
+
+ final SQLiteDatabase db = SQLiteDatabase.openDatabase(tempDbPath, null, 0);
+ try {
+ fAssertEquals("Input DB isn't the expected version",
+ i, db.getVersion());
+ } finally {
+ db.close();
+ }
+
+ final BrowserDatabaseHelper dbHelperToUpgrade = new BrowserDatabaseHelper(getActivity(), tempDbPath);
+ // Ideally, we'd test upgrading version i to version i + 1 but this method does not permit that. Alas!
+ final SQLiteDatabase upgradedDb = dbHelperToUpgrade.getWritableDatabase();
+ try {
+ fAssertEquals("DB helper should upgrade to latest version",
+ BrowserDatabaseHelper.DATABASE_VERSION, upgradedDb.getVersion());
+ } finally {
+ upgradedDb.close();
+ }
+ }
+ }
+
+ /**
+ * Copies the database from the assets directory to a temporary test file.
+ *
+ * @param version version of the database to copy.
+ * @return the String path to the new copy of the database
+ * @throws IOException if reading the existing database or writing the temporary database fails
+ */
+ private String copyDatabase(final int version) throws IOException {
+ final InputStream inputStream = openDbFromAssets(version);
+ try {
+ final File dbDestination = File.createTempFile("temporaryDB-v" + version + "_", "db");
+ temporaryDbFiles.add(dbDestination);
+ Log.d(LOGTAG, "Moving DB from assets to " + dbDestination.getPath());
+
+ final OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(dbDestination));
+ try {
+ final byte[] buffer = new byte[1024];
+ int len;
+ while ((len = inputStream.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, len);
+ }
+ outputStream.flush();
+ } finally {
+ outputStream.close();
+ }
+
+ return dbDestination.getPath();
+ } finally {
+ inputStream.close();
+ }
+ }
+
+ private InputStream openDbFromAssets(final int version) throws IOException {
+ final String dbAssetPath = String.format("browser_db_upgrade" + File.separator + String.format("v%d.db", version));
+ Log.d(LOGTAG, "Opening DB from assets: " + dbAssetPath);
+ try {
+ return new BufferedInputStream(getInstrumentation().getContext().getAssets().open(dbAssetPath));
+ } catch (final FileNotFoundException e) {
+ throw new IllegalStateException("If you're upgrading the browser.db version, " +
+ "you need to provide an old version of the database for this test! See the javadoc.", e);
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserDiscovery.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserDiscovery.java
new file mode 100644
index 0000000000..2dab2996c1
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserDiscovery.java
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+
+
+public class testBrowserDiscovery extends JavascriptTest {
+ public testBrowserDiscovery() {
+ super("testBrowserDiscovery.js");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserProvider.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserProvider.java
new file mode 100644
index 0000000000..e0ebb5c8e2
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserProvider.java
@@ -0,0 +1,1921 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserContract.UrlAnnotations.SyncStatus;
+import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.URLMetadata;
+import org.mozilla.gecko.db.URLMetadataTable;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.util.Log;
+
+/*
+ * This test is meant to exercise all operations exposed by Fennec's
+ * history and bookmarks content provider. It does so in an isolated
+ * environment (see ContentProviderTest) without affecting any UI-related
+ * code.
+ */
+public class testBrowserProvider extends ContentProviderTest {
+ private long mMobileFolderId;
+
+ private void loadMobileFolderId() throws Exception {
+ Cursor c = null;
+ try {
+ c = getBookmarkByGuid(BrowserContract.Bookmarks.MOBILE_FOLDER_GUID);
+ mAsserter.is(c.moveToFirst(), true, "Mobile bookmarks folder is present");
+
+ mMobileFolderId = c.getLong(c.getColumnIndex(BrowserContract.Bookmarks._ID));
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+
+ private void ensureEmptyDatabase() throws Exception {
+ Cursor c;
+
+ String guid = BrowserContract.Bookmarks.GUID;
+
+ mProvider.delete(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"),
+ guid + " != ? AND " +
+ guid + " != ? AND " +
+ guid + " != ? AND " +
+ guid + " != ? AND " +
+ guid + " != ? AND " +
+ guid + " != ?",
+ new String[] { BrowserContract.Bookmarks.PLACES_FOLDER_GUID,
+ BrowserContract.Bookmarks.MOBILE_FOLDER_GUID,
+ BrowserContract.Bookmarks.MENU_FOLDER_GUID,
+ BrowserContract.Bookmarks.TAGS_FOLDER_GUID,
+ BrowserContract.Bookmarks.TOOLBAR_FOLDER_GUID,
+ BrowserContract.Bookmarks.UNFILED_FOLDER_GUID });
+
+ c = mProvider.query(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), null, null, null, null);
+ assertCountIsAndClose(c, 6, "All non-special bookmarks and folders were deleted");
+
+ mProvider.delete(appendUriParam(BrowserContract.History.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"), null, null);
+ c = mProvider.query(appendUriParam(BrowserContract.History.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), null, null, null, null);
+ assertCountIsAndClose(c, 0, "All history entries were deleted");
+
+ /**
+ * There's no reason why the following two parts should fail.
+ * But sometimes they do, and I'm not going to spend the time
+ * to figure out why in an unrelated bug.
+ */
+
+ mProvider.delete(appendUriParam(BrowserContract.Favicons.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"), null, null);
+ c = mProvider.query(appendUriParam(BrowserContract.Favicons.CONTENT_URI,
+ BrowserContract.PARAM_SHOW_DELETED, "1"),
+ null, null, null, null);
+
+ if (c.getCount() > 0) {
+ mAsserter.dumpLog("Unexpected favicons in ensureEmptyDatabase.");
+ }
+ c.close();
+
+ mAsserter.dumpLog("ensureEmptyDatabase: Favicon deletion completed."); // Bug 968951 debug.
+ // assertCountIsAndClose(c, 0, "All favicons were deleted");
+
+ mProvider.delete(appendUriParam(BrowserContract.Thumbnails.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"), null, null);
+ c = mProvider.query(appendUriParam(BrowserContract.Thumbnails.CONTENT_URI,
+ BrowserContract.PARAM_SHOW_DELETED, "1"),
+ null, null, null, null);
+
+ if (c.getCount() > 0) {
+ mAsserter.dumpLog("Unexpected thumbnails in ensureEmptyDatabase.");
+ }
+ c.close();
+
+ mAsserter.dumpLog("ensureEmptyDatabase: Thumbnail deletion completed."); // Bug 968951 debug.
+ // assertCountIsAndClose(c, 0, "All thumbnails were deleted");
+ }
+
+ private ContentValues createBookmark(String title, String url, long parentId,
+ int type, int position, String tags, String description, String keyword) throws Exception {
+ ContentValues bookmark = new ContentValues();
+
+ bookmark.put(BrowserContract.Bookmarks.TITLE, title);
+ bookmark.put(BrowserContract.Bookmarks.URL, url);
+ bookmark.put(BrowserContract.Bookmarks.PARENT, parentId);
+ bookmark.put(BrowserContract.Bookmarks.TYPE, type);
+ bookmark.put(BrowserContract.Bookmarks.POSITION, position);
+ bookmark.put(BrowserContract.Bookmarks.TAGS, tags);
+ bookmark.put(BrowserContract.Bookmarks.DESCRIPTION, description);
+ bookmark.put(BrowserContract.Bookmarks.KEYWORD, keyword);
+
+ return bookmark;
+ }
+
+ private ContentValues createOneBookmark() throws Exception {
+ return createBookmark("Example", "http://example.com", mMobileFolderId,
+ BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword");
+ }
+
+ private Cursor getBookmarksByParent(long parent) throws Exception {
+ // Order by position.
+ return mProvider.query(BrowserContract.Bookmarks.CONTENT_URI, null,
+ BrowserContract.Bookmarks.PARENT + " = ?",
+ new String[] { String.valueOf(parent) },
+ BrowserContract.Bookmarks.POSITION);
+ }
+
+ private Cursor getBookmarkByGuid(String guid) throws Exception {
+ return mProvider.query(BrowserContract.Bookmarks.CONTENT_URI, null,
+ BrowserContract.Bookmarks.GUID + " = ?",
+ new String[] { guid },
+ null);
+ }
+
+ private Cursor getBookmarkById(long id) throws Exception {
+ return getBookmarkById(BrowserContract.Bookmarks.CONTENT_URI, id, null);
+ }
+
+ private Cursor getBookmarkById(long id, String[] projection) throws Exception {
+ return getBookmarkById(BrowserContract.Bookmarks.CONTENT_URI, id, projection);
+ }
+
+ private Cursor getBookmarkById(Uri bookmarksUri, long id) throws Exception {
+ return getBookmarkById(bookmarksUri, id, null);
+ }
+
+ private Cursor getBookmarkById(Uri bookmarksUri, long id, String[] projection) throws Exception {
+ return mProvider.query(bookmarksUri, projection,
+ BrowserContract.Bookmarks._ID + " = ?",
+ new String[] { String.valueOf(id) },
+ null);
+ }
+
+ private ContentValues createHistoryEntry(String title, String url, int visits, long lastVisited) throws Exception {
+ ContentValues historyEntry = new ContentValues();
+
+ historyEntry.put(BrowserContract.History.TITLE, title);
+ historyEntry.put(BrowserContract.History.URL, url);
+ historyEntry.put(BrowserContract.History.VISITS, visits);
+ historyEntry.put(BrowserContract.History.DATE_LAST_VISITED, lastVisited);
+
+ return historyEntry;
+ }
+
+ private ContentValues createFaviconEntry(String pageUrl, String data) throws Exception {
+ ContentValues faviconEntry = new ContentValues();
+
+ faviconEntry.put(BrowserContract.Favicons.PAGE_URL, pageUrl);
+ faviconEntry.put(BrowserContract.Favicons.URL, pageUrl + "/favicon.ico");
+ faviconEntry.put(BrowserContract.Favicons.DATA, data.getBytes("UTF8"));
+
+ return faviconEntry;
+ }
+
+ private ContentValues createThumbnailEntry(String pageUrl, String data) throws Exception {
+ ContentValues thumbnailEntry = new ContentValues();
+
+ thumbnailEntry.put(BrowserContract.Thumbnails.URL, pageUrl);
+ thumbnailEntry.put(BrowserContract.Thumbnails.DATA, data.getBytes("UTF8"));
+
+ return thumbnailEntry;
+ }
+
+ private ContentValues createUrlMetadataEntry(final String url, final String tileImage, final String tileColor,
+ final String touchIcon) {
+ final ContentValues values = new ContentValues();
+ values.put(URLMetadataTable.URL_COLUMN, url);
+ values.put(URLMetadataTable.TILE_IMAGE_URL_COLUMN, tileImage);
+ values.put(URLMetadataTable.TILE_COLOR_COLUMN, tileColor);
+ values.put(URLMetadataTable.TOUCH_ICON_COLUMN, touchIcon);
+ return values;
+ }
+
+ private ContentValues createUrlAnnotationEntry(final String url, final String key, final String value,
+ final long dateCreated) {
+ final ContentValues values = new ContentValues();
+ values.put(BrowserContract.UrlAnnotations.URL, url);
+ values.put(BrowserContract.UrlAnnotations.KEY, key);
+ values.put(BrowserContract.UrlAnnotations.VALUE, value);
+ values.put(BrowserContract.UrlAnnotations.DATE_CREATED, dateCreated);
+ values.put(BrowserContract.UrlAnnotations.DATE_MODIFIED, dateCreated);
+ return values;
+ }
+
+ private ContentValues createOneHistoryEntry() throws Exception {
+ return createHistoryEntry("Example", "http://example.com", 10, System.currentTimeMillis());
+ }
+
+ private Cursor getHistoryEntryById(long id) throws Exception {
+ return getHistoryEntryById(BrowserContract.History.CONTENT_URI, id, null);
+ }
+
+ private Cursor getHistoryEntryById(long id, String[] projection) throws Exception {
+ return getHistoryEntryById(BrowserContract.History.CONTENT_URI, id, projection);
+ }
+
+ private Cursor getHistoryEntryById(Uri historyUri, long id) throws Exception {
+ return getHistoryEntryById(historyUri, id, null);
+ }
+
+ private Cursor getHistoryEntryById(Uri historyUri, long id, String[] projection) throws Exception {
+ return mProvider.query(historyUri, projection,
+ BrowserContract.History._ID + " = ?",
+ new String[] { String.valueOf(id) },
+ null);
+ }
+
+ private Cursor getFaviconsByUrl(String url) throws Exception {
+ return mProvider.query(BrowserContract.Combined.CONTENT_URI, null,
+ BrowserContract.Combined.URL + " = ?",
+ new String[] { url },
+ null);
+ }
+
+ private Cursor getThumbnailByUrl(String url) throws Exception {
+ return mProvider.query(BrowserContract.Thumbnails.CONTENT_URI, null,
+ BrowserContract.Thumbnails.URL + " = ?",
+ new String[] { url },
+ null);
+ }
+
+ private Cursor getUrlAnnotationByUrl(final String url) throws Exception {
+ return mProvider.query(BrowserContract.UrlAnnotations.CONTENT_URI, null,
+ BrowserContract.UrlAnnotations.URL + " = ?",
+ new String[] { url },
+ null);
+ }
+
+ private Cursor getUrlMetadataByUrl(final String url) throws Exception {
+ return mProvider.query(URLMetadataTable.CONTENT_URI, null,
+ URLMetadataTable.URL_COLUMN + " = ?",
+ new String[] { url },
+ null);
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp(sBrowserProviderCallable, BrowserContract.AUTHORITY, "browser.db");
+
+ mTests.add(new TestSpecialFolders());
+
+ mTests.add(new TestInsertBookmarks());
+ mTests.add(new TestInsertBookmarksFavicons());
+ mTests.add(new TestDeleteBookmarks());
+ mTests.add(new TestDeleteBookmarksFavicons());
+ mTests.add(new TestUpdateBookmarks());
+ mTests.add(new TestUpdateBookmarksFavicons());
+ mTests.add(new TestPositionBookmarks());
+
+ mTests.add(new TestInsertHistory());
+ mTests.add(new TestInsertHistoryFavicons());
+ mTests.add(new TestDeleteHistory());
+ mTests.add(new TestDeleteHistoryFavicons());
+ mTests.add(new TestUpdateHistory());
+ mTests.add(new TestUpdateHistoryFavicons());
+ mTests.add(new TestUpdateOrInsertHistory());
+ mTests.add(new TestInsertHistoryThumbnails());
+ mTests.add(new TestUpdateHistoryThumbnails());
+ mTests.add(new TestDeleteHistoryThumbnails());
+
+ mTests.add(new TestInsertUrlAnnotations());
+ mTests.add(new TestInsertUrlMetadata());
+
+ mTests.add(new TestBatchOperations());
+
+ mTests.add(new TestCombinedView());
+ mTests.add(new TestCombinedViewDisplay());
+ mTests.add(new TestCombinedViewWithDeletedBookmark());
+
+ mTests.add(new TestBrowserProviderNotifications());
+ }
+
+ public void testBrowserProvider() throws Exception {
+ loadMobileFolderId();
+
+ for (int i = 0; i < mTests.size(); i++) {
+ Runnable test = mTests.get(i);
+
+ final String testName = test.getClass().getSimpleName();
+ setTestName(testName);
+ ensureEmptyDatabase();
+ mAsserter.dumpLog("testBrowserProvider: Database empty - Starting " + testName + ".");
+ test.run();
+ }
+ }
+
+ private class TestBatchOperations extends TestCase {
+ static final int TESTCOUNT = 100;
+
+ public void testApplyBatch() throws Exception {
+ ArrayList<ContentProviderOperation> mOperations
+ = new ArrayList<ContentProviderOperation>();
+
+ // Test a bunch of inserts with applyBatch
+ ContentValues values = new ContentValues();
+ ContentProviderOperation.Builder builder = null;
+
+ for (int i = 0; i < TESTCOUNT; i++) {
+ values.clear();
+ values.put(BrowserContract.History.VISITS, i);
+ values.put(BrowserContract.History.TITLE, "Test" + i);
+ values.put(BrowserContract.History.URL, "http://www.test.org/" + i);
+
+ // Insert
+ builder = ContentProviderOperation.newInsert(BrowserContract.History.CONTENT_URI);
+ builder.withValues(values);
+ // Queue the operation
+ mOperations.add(builder.build());
+ }
+
+ ContentProviderResult[] applyResult =
+ mProvider.applyBatch(mOperations);
+
+ boolean allFound = true;
+ for (int i = 0; i < TESTCOUNT; i++) {
+ Cursor cursor = mProvider.query(BrowserContract.History.CONTENT_URI,
+ null,
+ BrowserContract.History.URL + " = ?",
+ new String[] { "http://www.test.org/" + i },
+ null);
+
+ if (!cursor.moveToFirst())
+ allFound = false;
+ cursor.close();
+ }
+ mAsserter.is(allFound, true, "Found all batchApply entries");
+ mOperations.clear();
+
+ // Update all visits to 1
+ values.clear();
+ values.put(BrowserContract.History.VISITS, 1);
+ for (int i = 0; i < TESTCOUNT; i++) {
+ builder = ContentProviderOperation.newUpdate(BrowserContract.History.CONTENT_URI);
+ builder.withSelection(BrowserContract.History.URL + " = ?",
+ new String[] {"http://www.test.org/" + i});
+ builder.withValues(values);
+ builder.withExpectedCount(1);
+ // Queue the operation
+ mOperations.add(builder.build());
+ }
+
+ boolean seenException = false;
+ try {
+ applyResult = mProvider.applyBatch(mOperations);
+ } catch (OperationApplicationException ex) {
+ seenException = true;
+ }
+ mAsserter.is(seenException, false, "Batch updating succeeded");
+ mOperations.clear();
+
+ // Delete all visits
+ for (int i = 0; i < TESTCOUNT; i++) {
+ builder = ContentProviderOperation.newDelete(BrowserContract.History.CONTENT_URI);
+ builder.withSelection(BrowserContract.History.URL + " = ?",
+ new String[] {"http://www.test.org/" + i});
+ builder.withExpectedCount(1);
+ // Queue the operation
+ mOperations.add(builder.build());
+ }
+ try {
+ applyResult = mProvider.applyBatch(mOperations);
+ } catch (OperationApplicationException ex) {
+ seenException = true;
+ }
+ mAsserter.is(seenException, false, "Batch deletion succeeded");
+ }
+
+ // Force a Constraint error, see if later operations still apply correctly
+ public void testApplyBatchErrors() throws Exception {
+ ArrayList<ContentProviderOperation> mOperations
+ = new ArrayList<ContentProviderOperation>();
+
+ // Test a bunch of inserts with applyBatch
+ ContentProviderOperation.Builder builder = null;
+ ContentValues values = createFaviconEntry("http://www.test.org", "FAVICON");
+ builder = ContentProviderOperation.newInsert(BrowserContract.Favicons.CONTENT_URI);
+ builder.withValues(values);
+ mOperations.add(builder.build());
+
+ // Make a duplicate, this will fail because of a UNIQUE constraint
+ builder = ContentProviderOperation.newInsert(BrowserContract.Favicons.CONTENT_URI);
+ builder.withValues(values);
+ mOperations.add(builder.build());
+
+ // This is valid and should be in the table afterwards
+ values.put(BrowserContract.Favicons.URL, "http://www.test.org/valid.ico");
+ builder = ContentProviderOperation.newInsert(BrowserContract.Favicons.CONTENT_URI);
+ builder.withValues(values);
+ mOperations.add(builder.build());
+
+ boolean seenException = false;
+
+ try {
+ ContentProviderResult[] applyResult =
+ mProvider.applyBatch(mOperations);
+ } catch (OperationApplicationException ex) {
+ seenException = true;
+ }
+
+ // This test may need to go away if Bug 717428 is fixed.
+ mAsserter.is(seenException, true, "Expected failure in favicons table");
+
+ boolean allFound = true;
+ Cursor cursor = mProvider.query(BrowserContract.Favicons.CONTENT_URI,
+ null,
+ BrowserContract.Favicons.URL + " = ?",
+ new String[] { "http://www.test.org/valid.ico" },
+ null);
+
+ if (!cursor.moveToFirst())
+ allFound = false;
+ cursor.close();
+
+ mAsserter.is(allFound, true, "Found all applyBatch (with error) entries");
+ }
+
+ public void testBulkInsert() throws Exception {
+ // Test a bunch of inserts with bulkInsert
+ ContentValues allVals[] = new ContentValues[TESTCOUNT];
+ for (int i = 0; i < TESTCOUNT; i++) {
+ allVals[i] = new ContentValues();
+ allVals[i].put(BrowserContract.History.URL, i);
+ allVals[i].put(BrowserContract.History.TITLE, "Test" + i);
+ allVals[i].put(BrowserContract.History.URL, "http://www.test.org/" + i);
+ }
+
+ int inserts = mProvider.bulkInsert(BrowserContract.History.CONTENT_URI, allVals);
+ mAsserter.is(inserts, TESTCOUNT, "Excepted number of inserts matches");
+
+ boolean allFound = true;
+ for (int i = 0; i < TESTCOUNT; i++) {
+ Cursor cursor = mProvider.query(BrowserContract.History.CONTENT_URI,
+ null,
+ BrowserContract.History.URL + " = ?",
+ new String[] { "http://www.test.org/" + i },
+ null);
+
+ if (!cursor.moveToFirst())
+ allFound = false;
+ cursor.close();
+ }
+ mAsserter.is(allFound, true, "Found all bulkInsert entries");
+ }
+
+ @Override
+ public void test() throws Exception {
+ testApplyBatch();
+ // Clean up
+ ensureEmptyDatabase();
+
+ testBulkInsert();
+ ensureEmptyDatabase();
+
+ testApplyBatchErrors();
+ }
+ }
+
+ private class TestSpecialFolders extends TestCase {
+ @Override
+ public void test() throws Exception {
+ Cursor c = mProvider.query(BrowserContract.Bookmarks.CONTENT_URI,
+ new String[] { BrowserContract.Bookmarks._ID,
+ BrowserContract.Bookmarks.GUID,
+ BrowserContract.Bookmarks.PARENT },
+ BrowserContract.Bookmarks.GUID + " = ? OR " +
+ BrowserContract.Bookmarks.GUID + " = ? OR " +
+ BrowserContract.Bookmarks.GUID + " = ? OR " +
+ BrowserContract.Bookmarks.GUID + " = ? OR " +
+ BrowserContract.Bookmarks.GUID + " = ? OR " +
+ BrowserContract.Bookmarks.GUID + " = ?",
+ new String[] { BrowserContract.Bookmarks.PLACES_FOLDER_GUID,
+ BrowserContract.Bookmarks.MOBILE_FOLDER_GUID,
+ BrowserContract.Bookmarks.MENU_FOLDER_GUID,
+ BrowserContract.Bookmarks.TAGS_FOLDER_GUID,
+ BrowserContract.Bookmarks.TOOLBAR_FOLDER_GUID,
+ BrowserContract.Bookmarks.UNFILED_FOLDER_GUID},
+ null);
+
+ mAsserter.is(c.getCount(), 6, "Right number of special folders");
+
+ int rootId = BrowserContract.Bookmarks.FIXED_ROOT_ID;
+
+ while (c.moveToNext()) {
+ int id = c.getInt(c.getColumnIndex(BrowserContract.Bookmarks._ID));
+ String guid = c.getString(c.getColumnIndex(BrowserContract.Bookmarks.GUID));
+ int parentId = c.getInt(c.getColumnIndex(BrowserContract.Bookmarks.PARENT));
+
+ if (guid.equals(BrowserContract.Bookmarks.PLACES_FOLDER_GUID)) {
+ mAsserter.is(id, rootId, "The id of places folder is correct");
+ }
+
+ mAsserter.is(parentId, rootId,
+ "The PARENT of the " + guid + " special folder is correct");
+ }
+
+ c.close();
+ }
+ }
+
+ private class TestInsertBookmarks extends TestCase {
+ private long insertWithNullCol(String colName) throws Exception {
+ ContentValues b = createOneBookmark();
+ b.putNull(colName);
+ long id = -1;
+
+ try {
+ id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
+ } catch (Exception e) {}
+
+ return id;
+ }
+
+ @Override
+ public void test() throws Exception {
+ ContentValues b = createOneBookmark();
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
+
+ final Cursor c = getBookmarkById(id);
+ try {
+ mAsserter.is(c.moveToFirst(), true, "Inserted bookmark found");
+
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TITLE)), b.getAsString(BrowserContract.Bookmarks.TITLE),
+ "Inserted bookmark has correct title");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.URL)), b.getAsString(BrowserContract.Bookmarks.URL),
+ "Inserted bookmark has correct URL");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TAGS)), b.getAsString(BrowserContract.Bookmarks.TAGS),
+ "Inserted bookmark has correct tags");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.KEYWORD)), b.getAsString(BrowserContract.Bookmarks.KEYWORD),
+ "Inserted bookmark has correct keyword");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.DESCRIPTION)), b.getAsString(BrowserContract.Bookmarks.DESCRIPTION),
+ "Inserted bookmark has correct description");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.POSITION)), b.getAsString(BrowserContract.Bookmarks.POSITION),
+ "Inserted bookmark has correct position");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TYPE)), b.getAsString(BrowserContract.Bookmarks.TYPE),
+ "Inserted bookmark has correct type");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.PARENT)), b.getAsString(BrowserContract.Bookmarks.PARENT),
+ "Inserted bookmark has correct parent ID");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.IS_DELETED)), String.valueOf(0),
+ "Inserted bookmark has correct is-deleted state");
+
+ id = insertWithNullCol(BrowserContract.Bookmarks.POSITION);
+ mAsserter.is(id, -1L,
+ "Should not be able to insert bookmark with null position");
+
+ id = insertWithNullCol(BrowserContract.Bookmarks.TYPE);
+ mAsserter.is(id, -1L,
+ "Should not be able to insert bookmark with null type");
+
+ if (Build.VERSION.SDK_INT >= 8 &&
+ Build.VERSION.SDK_INT < 16) {
+ b = createOneBookmark();
+ b.put(BrowserContract.Bookmarks.PARENT, -1);
+ id = -1;
+
+ try {
+ id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
+ } catch (Exception e) {}
+
+ mAsserter.is(id, -1L,
+ "Should not be able to insert bookmark with invalid parent");
+ }
+
+ b = createOneBookmark();
+ b.remove(BrowserContract.Bookmarks.TYPE);
+ id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
+ final Cursor c2 = getBookmarkById(id);
+ try {
+ mAsserter.is(c2.moveToFirst(), true, "Inserted bookmark found");
+ mAsserter.is(c2.getString(c2.getColumnIndex(BrowserContract.Bookmarks.TYPE)), String.valueOf(BrowserContract.Bookmarks.TYPE_BOOKMARK),
+ "Inserted bookmark has correct default type");
+ } finally {
+ c2.close();
+ }
+ } finally {
+ c.close();
+ }
+ }
+ }
+
+ private class TestInsertBookmarksFavicons extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues b = createOneBookmark();
+
+ final String favicon = "FAVICON";
+ final String pageUrl = b.getAsString(BrowserContract.Bookmarks.URL);
+
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
+
+ // Insert the favicon into the favicons table
+ mProvider.insert(BrowserContract.Favicons.CONTENT_URI, createFaviconEntry(pageUrl, favicon));
+
+ Cursor c = getBookmarkById(id, new String[] { BrowserContract.Bookmarks.FAVICON });
+
+ mAsserter.is(c.moveToFirst(), true, "Inserted bookmark found");
+
+ mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Bookmarks.FAVICON)), "UTF8"),
+ favicon, "Inserted bookmark has corresponding favicon image");
+ c.close();
+
+ c = getFaviconsByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Inserted favicon found");
+
+ mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"),
+ favicon, "Inserted favicon has corresponding favicon image");
+ c.close();
+ }
+ }
+
+ private class TestDeleteBookmarks extends TestCase {
+ private long insertOneBookmark() throws Exception {
+ ContentValues b = createOneBookmark();
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
+
+ Cursor c = getBookmarkById(id);
+ mAsserter.is(c.moveToFirst(), true, "Inserted bookmark found");
+ c.close();
+
+ return id;
+ }
+
+ @Override
+ public void test() throws Exception {
+ long id = insertOneBookmark();
+
+ int deleted = mProvider.delete(BrowserContract.Bookmarks.CONTENT_URI,
+ BrowserContract.Bookmarks._ID + " = ?",
+ new String[] { String.valueOf(id) });
+
+ mAsserter.is((deleted == 1), true, "Inserted bookmark was deleted");
+
+ Cursor c = getBookmarkById(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), id);
+ mAsserter.is(c.moveToFirst(), true, "Deleted bookmark was only marked as deleted");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TITLE)), null,
+ "Deleted bookmark title is null");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.URL)), null,
+ "Deleted bookmark URL is null");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TAGS)), null,
+ "Deleted bookmark tags is null");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.KEYWORD)), null,
+ "Deleted bookmark keyword is null");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.DESCRIPTION)), null,
+ "Deleted bookmark description is null");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.POSITION)), String.valueOf(0),
+ "Deleted bookmark has correct position");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.PARENT)), null,
+ "Deleted bookmark parent ID is null");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.IS_DELETED)), String.valueOf(1),
+ "Deleted bookmark has correct is-deleted state");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.FAVICON_ID)), null,
+ "Deleted bookmark Favicon ID is null");
+ mAsserter.isnot(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.GUID)), null,
+ "Deleted bookmark GUID is not null");
+ c.close();
+
+ deleted = mProvider.delete(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"),
+ BrowserContract.Bookmarks._ID + " = ?",
+ new String[] { String.valueOf(id) });
+
+ mAsserter.is((deleted == 1), true, "Inserted bookmark was deleted");
+
+ c = getBookmarkById(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), id);
+ mAsserter.is(c.moveToFirst(), false, "Inserted bookmark is now actually deleted");
+ c.close();
+
+ id = insertOneBookmark();
+
+ deleted = mProvider.delete(ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, id), null, null);
+ mAsserter.is((deleted == 1), true,
+ "Inserted bookmark was deleted using URI with id");
+
+ c = getBookmarkById(id);
+ mAsserter.is(c.moveToFirst(), false,
+ "Inserted bookmark can't be found after deletion using URI with ID");
+ c.close();
+
+ if (Build.VERSION.SDK_INT >= 8 &&
+ Build.VERSION.SDK_INT < 16) {
+ ContentValues b = createBookmark("Folder", null, mMobileFolderId,
+ BrowserContract.Bookmarks.TYPE_FOLDER, 0, "folderTags", "folderDescription", "folderKeyword");
+
+ long parentId = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
+ c = getBookmarkById(parentId);
+ mAsserter.is(c.moveToFirst(), true, "Inserted bookmarks folder found");
+ c.close();
+
+ b = createBookmark("Example", "http://example.com", parentId,
+ BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword");
+
+ id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
+ c = getBookmarkById(id);
+ mAsserter.is(c.moveToFirst(), true, "Inserted bookmark found");
+ c.close();
+
+ deleted = 0;
+ try {
+ Uri uri = ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, parentId);
+ deleted = mProvider.delete(appendUriParam(uri, BrowserContract.PARAM_IS_SYNC, "1"), null, null);
+ } catch(Exception e) {}
+
+ mAsserter.is((deleted == 0), true,
+ "Should not be able to delete folder that causes orphan bookmarks");
+ }
+ }
+ }
+
+ private class TestDeleteBookmarksFavicons extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues b = createOneBookmark();
+
+ final String pageUrl = b.getAsString(BrowserContract.Bookmarks.URL);
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
+
+ // Insert the favicon into the favicons table
+ mProvider.insert(BrowserContract.Favicons.CONTENT_URI, createFaviconEntry(pageUrl, "FAVICON"));
+
+ Cursor c = getFaviconsByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Inserted favicon found");
+ c.close();
+
+ mProvider.delete(ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, id), null, null);
+
+ c = getFaviconsByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), false, "Favicon is deleted with last reference to it");
+ c.close();
+ }
+ }
+
+ private class TestUpdateBookmarks extends TestCase {
+ private int updateWithNullCol(long id, String colName) throws Exception {
+ ContentValues u = new ContentValues();
+ u.putNull(colName);
+
+ int updated = 0;
+
+ try {
+ updated = mProvider.update(BrowserContract.Bookmarks.CONTENT_URI, u,
+ BrowserContract.Bookmarks._ID + " = ?",
+ new String[] { String.valueOf(id) });
+ } catch (Exception e) {}
+
+ return updated;
+ }
+
+ @Override
+ public void test() throws Exception {
+ ContentValues b = createOneBookmark();
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
+
+ Cursor c = getBookmarkById(id);
+ mAsserter.is(c.moveToFirst(), true, "Inserted bookmark found");
+
+ long dateCreated = c.getLong(c.getColumnIndex(BrowserContract.Bookmarks.DATE_CREATED));
+ long dateModified = c.getLong(c.getColumnIndex(BrowserContract.Bookmarks.DATE_MODIFIED));
+
+ ContentValues u = new ContentValues();
+ u.put(BrowserContract.Bookmarks.TITLE, b.getAsString(BrowserContract.Bookmarks.TITLE) + "CHANGED");
+ u.put(BrowserContract.Bookmarks.URL, b.getAsString(BrowserContract.Bookmarks.URL) + "/more/stuff");
+ u.put(BrowserContract.Bookmarks.TAGS, b.getAsString(BrowserContract.Bookmarks.TAGS) + "CHANGED");
+ u.put(BrowserContract.Bookmarks.DESCRIPTION, b.getAsString(BrowserContract.Bookmarks.DESCRIPTION) + "CHANGED");
+ u.put(BrowserContract.Bookmarks.KEYWORD, b.getAsString(BrowserContract.Bookmarks.KEYWORD) + "CHANGED");
+ u.put(BrowserContract.Bookmarks.TYPE, BrowserContract.Bookmarks.TYPE_FOLDER);
+ u.put(BrowserContract.Bookmarks.POSITION, 10);
+
+ int updated = mProvider.update(BrowserContract.Bookmarks.CONTENT_URI, u,
+ BrowserContract.Bookmarks._ID + " = ?",
+ new String[] { String.valueOf(id) });
+
+ mAsserter.is((updated == 1), true, "Inserted bookmark was updated");
+ c.close();
+
+ c = getBookmarkById(id);
+ mAsserter.is(c.moveToFirst(), true, "Updated bookmark found");
+
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TITLE)), u.getAsString(BrowserContract.Bookmarks.TITLE),
+ "Inserted bookmark has correct title");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.URL)), u.getAsString(BrowserContract.Bookmarks.URL),
+ "Inserted bookmark has correct URL");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TAGS)), u.getAsString(BrowserContract.Bookmarks.TAGS),
+ "Inserted bookmark has correct tags");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.KEYWORD)), u.getAsString(BrowserContract.Bookmarks.KEYWORD),
+ "Inserted bookmark has correct keyword");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.DESCRIPTION)), u.getAsString(BrowserContract.Bookmarks.DESCRIPTION),
+ "Inserted bookmark has correct description");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.POSITION)), u.getAsString(BrowserContract.Bookmarks.POSITION),
+ "Inserted bookmark has correct position");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TYPE)), u.getAsString(BrowserContract.Bookmarks.TYPE),
+ "Inserted bookmark has correct type");
+
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Bookmarks.DATE_CREATED)),
+ dateCreated,
+ "Updated bookmark has same creation date");
+
+ mAsserter.isnot(c.getLong(c.getColumnIndex(BrowserContract.Bookmarks.DATE_MODIFIED)),
+ dateModified,
+ "Updated bookmark has new modification date");
+
+ updated = updateWithNullCol(id, BrowserContract.Bookmarks.POSITION);
+ mAsserter.is((updated > 0), false,
+ "Should not be able to update bookmark with null position");
+
+ updated = updateWithNullCol(id, BrowserContract.Bookmarks.TYPE);
+ mAsserter.is((updated > 0), false,
+ "Should not be able to update bookmark with null type");
+
+ u = new ContentValues();
+ u.put(BrowserContract.Bookmarks.URL, "http://examples2.com");
+
+ updated = mProvider.update(ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, id), u, null, null);
+ c.close();
+
+ c = getBookmarkById(id);
+ mAsserter.is(c.moveToFirst(), true, "Updated bookmark found");
+
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.URL)), u.getAsString(BrowserContract.Bookmarks.URL),
+ "Updated bookmark has correct URL using URI with id");
+ c.close();
+ }
+ }
+
+ private class TestUpdateBookmarksFavicons extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues b = createOneBookmark();
+
+ final String favicon = "FAVICON";
+ final String newFavicon = "NEW_FAVICON";
+ final String pageUrl = b.getAsString(BrowserContract.Bookmarks.URL);
+
+ mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b);
+
+ // Insert the favicon into the favicons table
+ ContentValues f = createFaviconEntry(pageUrl, favicon);
+ long faviconId = ContentUris.parseId(mProvider.insert(BrowserContract.Favicons.CONTENT_URI, f));
+
+ Cursor c = getFaviconsByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Inserted favicon found");
+
+ mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"),
+ favicon, "Inserted favicon has corresponding favicon image");
+
+ ContentValues u = createFaviconEntry(pageUrl, newFavicon);
+ mProvider.update(BrowserContract.Favicons.CONTENT_URI, u, null, null);
+ c.close();
+
+ c = getFaviconsByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Updated favicon found");
+
+ mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"),
+ newFavicon, "Updated favicon has corresponding favicon image");
+ c.close();
+ }
+ }
+
+ /**
+ * Create a folder of one thousand and one bookmarks, then impose an order
+ * on them.
+ *
+ * Verify that the reordering worked by querying.
+ */
+ private class TestPositionBookmarks extends TestCase {
+
+ public String makeGUID(final long in) {
+ String part = String.valueOf(in);
+ return "aaaaaaaaaaaa".substring(0, (12 - part.length())) + part;
+ }
+
+ public void compareCursorToItems(final Cursor c, final String[] items, final int count) {
+ mAsserter.is(c.moveToFirst(), true, "Folder has children.");
+
+ int posColumn = c.getColumnIndex(BrowserContract.Bookmarks.POSITION);
+ int guidColumn = c.getColumnIndex(BrowserContract.Bookmarks.GUID);
+ int i = 0;
+
+ while (!c.isAfterLast()) {
+ String guid = c.getString(guidColumn);
+ long pos = c.getLong(posColumn);
+ if ((pos != i) || (guid == null) || (!guid.equals(items[i]))) {
+ mAsserter.is(pos, (long) i, "Position matches sequence.");
+ mAsserter.is(guid, items[i], "GUID matches sequence.");
+ }
+ ++i;
+ c.moveToNext();
+ }
+
+ mAsserter.is(i, count, "Folder has the right number of children.");
+ c.close();
+ }
+
+ public static final int NUMBER_OF_CHILDREN = 1001;
+ @Override
+ public void test() throws Exception {
+ // Create the containing folder.
+ ContentValues folder = createBookmark("FolderFolder", "", mMobileFolderId,
+ BrowserContract.Bookmarks.TYPE_FOLDER, 0, "",
+ "description", "keyword");
+ folder.put(BrowserContract.Bookmarks.GUID, "folderfolder");
+ long folderId = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, folder));
+
+ mAsserter.dumpLog("TestPositionBookmarks: Folder inserted"); // Bug 968951 debug.
+
+ // Create the children.
+ String[] items = new String[NUMBER_OF_CHILDREN];
+
+ // Reuse the same ContentValues.
+ ContentValues item = createBookmark("Test Bookmark", "http://example.com", folderId,
+ BrowserContract.Bookmarks.TYPE_FOLDER, 0, "",
+ "description", "keyword");
+
+ for (int i = 0; i < NUMBER_OF_CHILDREN; ++i) {
+ String guid = makeGUID(i);
+ items[i] = guid;
+ item.put(BrowserContract.Bookmarks.GUID, guid);
+ item.put(BrowserContract.Bookmarks.POSITION, i);
+ item.put(BrowserContract.Bookmarks.URL, "http://example.com/" + guid);
+ item.put(BrowserContract.Bookmarks.TITLE, "Test Bookmark " + guid);
+ mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, item);
+ }
+
+ mAsserter.dumpLog("TestPositionBookmarks: Bookmarks inserted"); // Bug 968951 debug.
+
+ Cursor c;
+
+ // Verify insertion.
+ c = getBookmarksByParent(folderId);
+ mAsserter.dumpLog("TestPositionBookmarks: Got bookmarks by parent"); // Bug 968951 debug.
+ compareCursorToItems(c, items, NUMBER_OF_CHILDREN);
+ c.close();
+
+ // Now permute the items array.
+ Random rand = new Random();
+ for (int i = 0; i < NUMBER_OF_CHILDREN; ++i) {
+ final int newPosition = rand.nextInt(NUMBER_OF_CHILDREN);
+ final String switched = items[newPosition];
+ items[newPosition] = items[i];
+ items[i] = switched;
+ }
+
+ // Impose the positions.
+ long updated = mProvider.update(BrowserContract.Bookmarks.POSITIONS_CONTENT_URI, null, null, items);
+ mAsserter.is(updated, (long) NUMBER_OF_CHILDREN, "Updated " + NUMBER_OF_CHILDREN + " positions.");
+
+ // Verify that the database was updated.
+ c = getBookmarksByParent(folderId);
+ compareCursorToItems(c, items, NUMBER_OF_CHILDREN);
+ c.close();
+ }
+ }
+
+ private class TestInsertHistory extends TestCase {
+ private long insertWithNullCol(String colName) throws Exception {
+ ContentValues h = createOneHistoryEntry();
+ h.putNull(colName);
+ long id = -1;
+
+ try {
+ id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h));
+ } catch (Exception e) {}
+
+ return id;
+ }
+
+ @Override
+ public void test() throws Exception {
+ ContentValues h = createOneHistoryEntry();
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h));
+ Cursor c = getHistoryEntryById(id);
+
+ mAsserter.is(c.moveToFirst(), true, "Inserted history entry found");
+
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.TITLE)), h.getAsString(BrowserContract.History.TITLE),
+ "Inserted history entry has correct title");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.URL)), h.getAsString(BrowserContract.History.URL),
+ "Inserted history entry has correct URL");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.VISITS)), h.getAsString(BrowserContract.History.VISITS),
+ "Inserted history entry has correct number of visits");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.DATE_LAST_VISITED)), h.getAsString(BrowserContract.History.DATE_LAST_VISITED),
+ "Inserted history entry has correct last visited date");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.IS_DELETED)), String.valueOf(0),
+ "Inserted history entry has correct is-deleted state");
+
+ id = insertWithNullCol(BrowserContract.History.URL);
+ mAsserter.is(id, -1L,
+ "Should not be able to insert history with null URL");
+
+ id = insertWithNullCol(BrowserContract.History.VISITS);
+ mAsserter.is(id, -1L,
+ "Should not be able to insert history with null number of visits");
+ c.close();
+ }
+ }
+
+ private class TestInsertHistoryFavicons extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues h = createOneHistoryEntry();
+
+ final String favicon = "FAVICON";
+ final String pageUrl = h.getAsString(BrowserContract.History.URL);
+
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h));
+
+ // Insert the favicon into the favicons table
+ mProvider.insert(BrowserContract.Favicons.CONTENT_URI, createFaviconEntry(pageUrl, favicon));
+
+ Cursor c = getHistoryEntryById(id, new String[] { BrowserContract.History.FAVICON });
+
+ mAsserter.is(c.moveToFirst(), true, "Inserted history entry found");
+
+ mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.History.FAVICON)), "UTF8"),
+ favicon, "Inserted history entry has corresponding favicon image");
+ c.close();
+
+ c = getFaviconsByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Inserted favicon found");
+
+ mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"),
+ favicon, "Inserted favicon has corresponding favicon image");
+ c.close();
+ }
+ }
+
+ private class TestDeleteHistory extends TestCase {
+ private long insertOneHistoryEntry() throws Exception {
+ ContentValues h = createOneHistoryEntry();
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h));
+
+ Cursor c = getHistoryEntryById(id);
+ mAsserter.is(c.moveToFirst(), true, "Inserted history entry found");
+ c.close();
+
+ return id;
+ }
+
+ @Override
+ public void test() throws Exception {
+ long id = insertOneHistoryEntry();
+
+ int deleted = mProvider.delete(BrowserContract.History.CONTENT_URI,
+ BrowserContract.History._ID + " = ?",
+ new String[] { String.valueOf(id) });
+
+ mAsserter.is((deleted == 1), true, "Inserted history entry was deleted");
+
+ Cursor c = getHistoryEntryById(appendUriParam(BrowserContract.History.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), id);
+ mAsserter.is(c.moveToFirst(), true, "Deleted history entry was only marked as deleted");
+
+ deleted = mProvider.delete(appendUriParam(BrowserContract.History.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"),
+ BrowserContract.History._ID + " = ?",
+ new String[] { String.valueOf(id) });
+
+ mAsserter.is((deleted == 1), true, "Inserted history entry was deleted");
+ c.close();
+
+ c = getHistoryEntryById(appendUriParam(BrowserContract.History.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), id);
+ mAsserter.is(c.moveToFirst(), false, "Inserted history is now actually deleted");
+
+ id = insertOneHistoryEntry();
+
+ deleted = mProvider.delete(ContentUris.withAppendedId(BrowserContract.History.CONTENT_URI, id), null, null);
+ mAsserter.is((deleted == 1), true,
+ "Inserted history entry was deleted using URI with id");
+ c.close();
+
+ c = getHistoryEntryById(id);
+ mAsserter.is(c.moveToFirst(), false,
+ "Inserted history entry can't be found after deletion using URI with ID");
+ c.close();
+ }
+ }
+
+ private class TestDeleteHistoryFavicons extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues h = createOneHistoryEntry();
+
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h));
+ final String pageUrl = h.getAsString(BrowserContract.History.URL);
+
+ // Insert the favicon into the favicons table
+ mProvider.insert(BrowserContract.Favicons.CONTENT_URI, createFaviconEntry(pageUrl, "FAVICON"));
+
+ Cursor c = getFaviconsByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Inserted favicon found");
+
+ mProvider.delete(ContentUris.withAppendedId(BrowserContract.History.CONTENT_URI, id), null, null);
+ c.close();
+
+ c = getFaviconsByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), false, "Favicon is deleted with last reference to it");
+ c.close();
+ }
+ }
+
+ private class TestUpdateHistory extends TestCase {
+ private int updateWithNullCol(long id, String colName) throws Exception {
+ ContentValues u = new ContentValues();
+ u.putNull(colName);
+
+ int updated = 0;
+
+ try {
+ updated = mProvider.update(BrowserContract.History.CONTENT_URI, u,
+ BrowserContract.History._ID + " = ?",
+ new String[] { String.valueOf(id) });
+ } catch (Exception e) {}
+
+ return updated;
+ }
+
+ @Override
+ public void test() throws Exception {
+ ContentValues h = createOneHistoryEntry();
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h));
+
+ Cursor c = getHistoryEntryById(id);
+ mAsserter.is(c.moveToFirst(), true, "Inserted history entry found");
+
+ long dateCreated = c.getLong(c.getColumnIndex(BrowserContract.History.DATE_CREATED));
+ long dateModified = c.getLong(c.getColumnIndex(BrowserContract.History.DATE_MODIFIED));
+
+ ContentValues u = new ContentValues();
+ u.put(BrowserContract.History.VISITS, h.getAsInteger(BrowserContract.History.VISITS) + 1);
+ u.put(BrowserContract.History.DATE_LAST_VISITED, System.currentTimeMillis());
+ u.put(BrowserContract.History.TITLE, h.getAsString(BrowserContract.History.TITLE) + "CHANGED");
+ u.put(BrowserContract.History.URL, h.getAsString(BrowserContract.History.URL) + "/more/stuff");
+
+ int updated = mProvider.update(BrowserContract.History.CONTENT_URI, u,
+ BrowserContract.History._ID + " = ?",
+ new String[] { String.valueOf(id) });
+
+ mAsserter.is((updated == 1), true, "Inserted history entry was updated");
+ c.close();
+
+ c = getHistoryEntryById(id);
+ mAsserter.is(c.moveToFirst(), true, "Updated history entry found");
+
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.TITLE)), u.getAsString(BrowserContract.History.TITLE),
+ "Updated history entry has correct title");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.URL)), u.getAsString(BrowserContract.History.URL),
+ "Updated history entry has correct URL");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.VISITS)), u.getAsString(BrowserContract.History.VISITS),
+ "Updated history entry has correct number of visits");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.DATE_LAST_VISITED)), u.getAsString(BrowserContract.History.DATE_LAST_VISITED),
+ "Updated history entry has correct last visited date");
+
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.DATE_CREATED)),
+ dateCreated,
+ "Updated history entry has same creation date");
+
+ mAsserter.isnot(c.getLong(c.getColumnIndex(BrowserContract.History.DATE_MODIFIED)),
+ dateModified,
+ "Updated history entry has new modification date");
+
+ updated = updateWithNullCol(id, BrowserContract.History.URL);
+ mAsserter.is((updated > 0), false,
+ "Should not be able to update history with null URL");
+
+ updated = updateWithNullCol(id, BrowserContract.History.VISITS);
+ mAsserter.is((updated > 0), false,
+ "Should not be able to update history with null number of visits");
+
+ u = new ContentValues();
+ u.put(BrowserContract.History.URL, "http://examples2.com");
+
+ updated = mProvider.update(ContentUris.withAppendedId(BrowserContract.History.CONTENT_URI, id), u, null, null);
+ c.close();
+
+ c = getHistoryEntryById(id);
+ mAsserter.is(c.moveToFirst(), true, "Updated history entry found");
+
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.URL)), u.getAsString(BrowserContract.History.URL),
+ "Updated history entry has correct URL using URI with id");
+ c.close();
+ }
+ }
+
+ private class TestUpdateHistoryFavicons extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues h = createOneHistoryEntry();
+
+ final String favicon = "FAVICON";
+ final String newFavicon = "NEW_FAVICON";
+ final String pageUrl = h.getAsString(BrowserContract.History.URL);
+
+ mProvider.insert(BrowserContract.History.CONTENT_URI, h);
+
+ // Insert the favicon into the favicons table
+ mProvider.insert(BrowserContract.Favicons.CONTENT_URI, createFaviconEntry(pageUrl, favicon));
+
+ Cursor c = getFaviconsByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Inserted favicon found");
+
+ mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"),
+ favicon, "Inserted favicon has corresponding favicon image");
+
+ ContentValues u = createFaviconEntry(pageUrl, newFavicon);
+
+ mProvider.update(BrowserContract.Favicons.CONTENT_URI, u, null, null);
+ c.close();
+
+ c = getFaviconsByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Updated favicon found");
+
+ mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"),
+ newFavicon, "Updated favicon has corresponding favicon image");
+ c.close();
+ }
+ }
+
+ private class TestUpdateOrInsertHistory extends TestCase {
+ private final String TEST_URL_1 = "http://example.com";
+ private final String TEST_URL_2 = "http://example.org";
+ private final String TEST_TITLE = "Example";
+
+ private long getHistoryEntryIdByUrl(String url) {
+ Cursor c = mProvider.query(BrowserContract.History.CONTENT_URI,
+ new String[] { BrowserContract.History._ID },
+ BrowserContract.History.URL + " = ?",
+ new String[] { url },
+ null);
+ c.moveToFirst();
+ long id = c.getLong(0);
+ c.close();
+
+ return id;
+ }
+
+ @Override
+ public void test() throws Exception {
+ Uri updateHistoryUri = BrowserContract.History.CONTENT_URI.buildUpon().
+ appendQueryParameter("increment_visits", "true").build();
+ Uri updateOrInsertHistoryUri = BrowserContract.History.CONTENT_URI.buildUpon().
+ appendQueryParameter("insert_if_needed", "true").
+ appendQueryParameter("increment_visits", "true").build();
+
+ // Update a non-existent history entry, without specifying visits or title
+ ContentValues values = new ContentValues();
+ values.put(BrowserContract.History.URL, TEST_URL_1);
+
+ int updated = mProvider.update(updateHistoryUri, values,
+ BrowserContract.History.URL + " = ?",
+ new String[] { TEST_URL_1 });
+ mAsserter.is((updated == 0), true, "History entry was not updated");
+ Cursor c = mProvider.query(BrowserContract.History.CONTENT_URI, null, null, null, null);
+ mAsserter.is(c.moveToFirst(), false, "History entry was not inserted");
+ c.close();
+
+ // Now let's try with update-or-insert.
+ updated = mProvider.update(updateOrInsertHistoryUri, values,
+ BrowserContract.History.URL + " = ?",
+ new String[] { TEST_URL_1 });
+ mAsserter.is((updated == 1), true, "History entry was inserted");
+
+ long id = getHistoryEntryIdByUrl(TEST_URL_1);
+ c = getHistoryEntryById(id);
+ mAsserter.is(c.moveToFirst(), true, "History entry was inserted");
+
+ long dateCreated = c.getLong(c.getColumnIndex(BrowserContract.History.DATE_CREATED));
+ long dateModified = c.getLong(c.getColumnIndex(BrowserContract.History.DATE_MODIFIED));
+
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.VISITS)), 1L,
+ "Inserted history entry has correct default number of visits");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.TITLE)), TEST_URL_1,
+ "Inserted history entry has correct default title");
+
+ // Update the history entry, without specifying an additional visit count
+ values = new ContentValues();
+ values.put(BrowserContract.History.DATE_LAST_VISITED, System.currentTimeMillis());
+ values.put(BrowserContract.History.TITLE, TEST_TITLE);
+
+ updated = mProvider.update(updateOrInsertHistoryUri, values,
+ BrowserContract.History._ID + " = ?",
+ new String[] { String.valueOf(id) });
+ mAsserter.is((updated == 1), true, "Inserted history entry was updated");
+ c.close();
+
+ c = getHistoryEntryById(id);
+ mAsserter.is(c.moveToFirst(), true, "Updated history entry found");
+
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.TITLE)), TEST_TITLE,
+ "Updated history entry has correct title");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.VISITS)), 2L,
+ "Updated history entry has correct number of visits");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.DATE_CREATED)), dateCreated,
+ "Updated history entry has same creation date");
+ mAsserter.isnot(c.getLong(c.getColumnIndex(BrowserContract.History.DATE_MODIFIED)), dateModified,
+ "Updated history entry has new modification date");
+
+ // Create a new history entry, specifying visits and history
+ values = new ContentValues();
+ values.put(BrowserContract.History.URL, TEST_URL_2);
+ values.put(BrowserContract.History.TITLE, TEST_TITLE);
+ values.put(BrowserContract.History.VISITS, 10);
+
+ updated = mProvider.update(updateOrInsertHistoryUri, values,
+ BrowserContract.History.URL + " = ?",
+ new String[] { values.getAsString(BrowserContract.History.URL) });
+ mAsserter.is((updated == 1), true, "History entry was inserted");
+
+ id = getHistoryEntryIdByUrl(TEST_URL_2);
+ c.close();
+
+ c = getHistoryEntryById(id);
+ mAsserter.is(c.moveToFirst(), true, "History entry was inserted");
+
+ dateCreated = c.getLong(c.getColumnIndex(BrowserContract.History.DATE_CREATED));
+ dateModified = c.getLong(c.getColumnIndex(BrowserContract.History.DATE_MODIFIED));
+
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.VISITS)), 10L,
+ "Inserted history entry has correct specified number of visits");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.TITLE)), TEST_TITLE,
+ "Inserted history entry has correct specified title");
+
+ // Update the history entry, specifying additional visit count.
+ // The expectation is that the value is ignored, and count is bumped by 1 only.
+ // At the same time, a visit is inserted into the visits table.
+ // See junit4 tests in BrowserProviderHistoryVisitsTest.
+ values = new ContentValues();
+ values.put(BrowserContract.History.VISITS, 10);
+
+ updated = mProvider.update(updateOrInsertHistoryUri, values,
+ BrowserContract.History._ID + " = ?",
+ new String[] { String.valueOf(id) });
+ mAsserter.is((updated == 1), true, "Inserted history entry was updated");
+ c.close();
+
+ c = getHistoryEntryById(id);
+ mAsserter.is(c.moveToFirst(), true, "Updated history entry found");
+
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.TITLE)), TEST_TITLE,
+ "Updated history entry has correct unchanged title");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.URL)), TEST_URL_2,
+ "Updated history entry has correct unchanged URL");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.VISITS)), 11L,
+ "Updated history entry has correct number of visits");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.DATE_CREATED)), dateCreated,
+ "Updated history entry has same creation date");
+ mAsserter.isnot(c.getLong(c.getColumnIndex(BrowserContract.History.DATE_MODIFIED)), dateModified,
+ "Updated history entry has new modification date");
+ c.close();
+
+ }
+ }
+
+ private class TestInsertHistoryThumbnails extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues h = createOneHistoryEntry();
+
+ final String thumbnail = "THUMBNAIL";
+ final String pageUrl = h.getAsString(BrowserContract.History.URL);
+
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h));
+
+ // Insert the thumbnail into the thumbnails table
+ mProvider.insert(BrowserContract.Thumbnails.CONTENT_URI, createThumbnailEntry(pageUrl, thumbnail));
+
+ Cursor c = getThumbnailByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Inserted thumbnail found");
+
+ mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Thumbnails.DATA)), "UTF8"),
+ thumbnail, "Inserted thumbnail has corresponding thumbnail image");
+ c.close();
+ }
+ }
+
+ private class TestUpdateHistoryThumbnails extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues h = createOneHistoryEntry();
+
+ final String thumbnail = "THUMBNAIL";
+ final String newThumbnail = "NEW_THUMBNAIL";
+ final String pageUrl = h.getAsString(BrowserContract.History.URL);
+
+ mProvider.insert(BrowserContract.History.CONTENT_URI, h);
+
+ // Insert the thumbnail into the thumbnails table
+ mProvider.insert(BrowserContract.Thumbnails.CONTENT_URI, createThumbnailEntry(pageUrl, thumbnail));
+
+ Cursor c = getThumbnailByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Inserted thumbnail found");
+
+ mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Thumbnails.DATA)), "UTF8"),
+ thumbnail, "Inserted thumbnail has corresponding thumbnail image");
+
+ ContentValues u = createThumbnailEntry(pageUrl, newThumbnail);
+
+ mProvider.update(BrowserContract.Thumbnails.CONTENT_URI, u, null, null);
+ c.close();
+
+ c = getThumbnailByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Updated thumbnail found");
+
+ mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Thumbnails.DATA)), "UTF8"),
+ newThumbnail, "Updated thumbnail has corresponding thumbnail image");
+ c.close();
+ }
+ }
+
+ private class TestDeleteHistoryThumbnails extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues h = createOneHistoryEntry();
+
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h));
+ final String pageUrl = h.getAsString(BrowserContract.History.URL);
+
+ // Insert the thumbnail into the thumbnails table
+ mProvider.insert(BrowserContract.Thumbnails.CONTENT_URI, createThumbnailEntry(pageUrl, "THUMBNAIL"));
+
+ Cursor c = getThumbnailByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), true, "Inserted thumbnail found");
+
+ mProvider.delete(ContentUris.withAppendedId(BrowserContract.History.CONTENT_URI, id), null, null);
+ c.close();
+
+ c = getThumbnailByUrl(pageUrl);
+ mAsserter.is(c.moveToFirst(), false, "Thumbnail is deleted with last reference to it");
+ c.close();
+ }
+ }
+
+ private class TestInsertUrlAnnotations extends TestCase {
+ @Override
+ public void test() throws Exception {
+ testInsertionViaContentProvider();
+ testInsertionViaUrlAnnotations();
+ }
+
+ private void testInsertionViaContentProvider() throws Exception {
+ final String url = "http://mozilla.org";
+ final String key = "todo";
+ final String value = "v";
+ final long dateCreated = System.currentTimeMillis();
+
+ mProvider.insert(BrowserContract.UrlAnnotations.CONTENT_URI, createUrlAnnotationEntry(url, key, value, dateCreated));
+
+ final Cursor c = getUrlAnnotationByUrl(url);
+ try {
+ mAsserter.is(c.moveToFirst(), true, "Inserted url annotation found");
+ assertKeyValueSync(c, key, value);
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.UrlAnnotations.DATE_CREATED)), dateCreated,
+ "Inserted url annotation has correct date created");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.UrlAnnotations.DATE_MODIFIED)), dateCreated,
+ "Inserted url annotation has correct date modified");
+ } finally {
+ c.close();
+ }
+ }
+
+ private void testInsertionViaUrlAnnotations() throws Exception {
+ final String url = "http://hello.org";
+ final String key = "toTheUniverse";
+ final String value = "42a";
+ final long timeBeforeCreation = System.currentTimeMillis();
+
+ BrowserDB.from(getTestProfile()).getUrlAnnotations().insertAnnotation(mResolver, url, key, value);
+
+ final Cursor c = getUrlAnnotationByUrl(url);
+ try {
+ mAsserter.is(c.moveToFirst(), true, "Inserted url annotation found");
+ assertKeyValueSync(c, key, value);
+ mAsserter.is(true, c.getLong(c.getColumnIndex(BrowserContract.UrlAnnotations.DATE_CREATED)) >= timeBeforeCreation,
+ "Inserted url annotation has date created greater than or equal to time saved before insertion");
+ mAsserter.is(true, c.getLong(c.getColumnIndex(BrowserContract.UrlAnnotations.DATE_MODIFIED)) >= timeBeforeCreation,
+ "Inserted url annotation has correct date modified greater than or equal to time saved before insertion");
+ } finally {
+ c.close();
+ }
+ }
+
+ private void assertKeyValueSync(final Cursor c, final String key, final String value) {
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.UrlAnnotations.KEY)), key,
+ "Inserted url annotation has correct key");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.UrlAnnotations.VALUE)), value,
+ "Inserted url annotation has correct value");
+ mAsserter.is(c.getInt(c.getColumnIndex(BrowserContract.UrlAnnotations.SYNC_STATUS)), SyncStatus.NEW.getDBValue(),
+ "Inserted url annotation has default sync status");
+ }
+ }
+
+ private class TestInsertUrlMetadata extends TestCase {
+ @Override
+ public void test() throws Exception {
+ testInsertionViaContentProvider();
+ testInsertionViaUrlMetadata();
+ // testRetrievalViaUrlMetadata depends on data added in the previous two tests
+ testRetrievalViaUrlMetadata();
+ }
+
+ final String url1 = "http://mozilla.org";
+ final String url2 = "http://hello.org";
+
+ private void testInsertionViaContentProvider() throws Exception {
+ final String tileImage = "http://mozilla.org/tileImage.png";
+ final String tileColor = "#FF0000";
+ final String touchIcon = "http://mozilla.org/touchIcon.png";
+
+ // We can only use update since the redirection machinery doesn't exist for insert
+ mProvider.update(URLMetadataTable.CONTENT_URI.buildUpon().appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(),
+ createUrlMetadataEntry(url1, tileImage, tileColor, touchIcon),
+ URLMetadataTable.URL_COLUMN + "=?",
+ new String[] {url1}
+ );
+
+ final Cursor c = getUrlMetadataByUrl(url1);
+ try {
+ mAsserter.is(c.getCount(), 1, "URL metadata inserted via Content Provider not found");
+ } finally {
+ c.close();
+ }
+ }
+
+ private void testInsertionViaUrlMetadata() throws Exception {
+ final String tileImage = "http://hello.org/tileImage.png";
+ final String tileColor = "#FF0000";
+ final String touchIcon = "http://hello.org/touchIcon.png";
+
+ final Map<String, Object> data = new HashMap<>();
+ data.put(URLMetadataTable.URL_COLUMN, url2);
+ data.put(URLMetadataTable.TILE_IMAGE_URL_COLUMN, tileImage);
+ data.put(URLMetadataTable.TILE_COLOR_COLUMN, tileColor);
+ data.put(URLMetadataTable.TOUCH_ICON_COLUMN, touchIcon);
+
+ BrowserDB.from(getTestProfile()).getURLMetadata().save(mResolver, data);
+
+ final Cursor c = getUrlMetadataByUrl(url2);
+ try {
+ mAsserter.is(c.moveToFirst(), true, "URL metadata inserted via UrlMetadata not found");
+ } finally {
+ c.close();
+ }
+ }
+
+ private void testRetrievalViaUrlMetadata() {
+ // LocalURLMetadata has some caching of results: we need to test that this caching
+ // doesn't prevent us from accessing data that might not have been loaded into the cache.
+ // We do this by first doing queries with a subset of data, then later querying additional
+ // data for a given URL. E.g. even if the first query results in only the requested
+ // column being cached, the subsequent query should still retrieve all requested columns.
+ // (In this case the URL may be cached but without all data, we need to make sure that
+ // this state is correctly handled.)
+ URLMetadata metadata = BrowserDB.from(getTestProfile()).getURLMetadata();
+
+ Map<String, Map<String, Object>> results;
+ Map<String, Object> urlData;
+
+ // 1: retrieve just touch Icons for URL 1
+ results = metadata.getForURLs(mResolver,
+ Collections.singletonList(url1),
+ Collections.singletonList(URLMetadataTable.TOUCH_ICON_COLUMN));
+
+ mAsserter.is(results.containsKey(url1), true, "URL 1 not found in results");
+
+ urlData = results.get(url1);
+ mAsserter.is(urlData.containsKey(URLMetadataTable.TOUCH_ICON_COLUMN), true, "touchIcon column missing in UrlMetadata results");
+
+ // 2: retrieve just tile color for URL 2
+ results = metadata.getForURLs(mResolver,
+ Collections.singletonList(url2),
+ Collections.singletonList(URLMetadataTable.TILE_COLOR_COLUMN));
+
+ mAsserter.is(results.containsKey(url2), true, "URL 2 not found in results");
+
+ urlData = results.get(url2);
+ mAsserter.is(urlData.containsKey(URLMetadataTable.TILE_COLOR_COLUMN), true, "touchIcon column missing in UrlMetadata results");
+
+
+ // 3: retrieve all columns for both URLs
+ final List<String> urls = Arrays.asList(url1, url2);
+
+ results = metadata.getForURLs(mResolver,
+ urls,
+ Arrays.asList(URLMetadataTable.TILE_IMAGE_URL_COLUMN,
+ URLMetadataTable.TILE_COLOR_COLUMN,
+ URLMetadataTable.TOUCH_ICON_COLUMN
+ ));
+
+ mAsserter.is(results.containsKey(url1), true, "URL 1 not found in results");
+ mAsserter.is(results.containsKey(url2), true, "URL 2 not found in results");
+
+
+ for (final String url : urls) {
+ urlData = results.get(url);
+ mAsserter.is(urlData.containsKey(URLMetadataTable.TILE_IMAGE_URL_COLUMN), true, "touchIcon column missing in UrlMetadata results");
+ mAsserter.is(urlData.containsKey(URLMetadataTable.TILE_COLOR_COLUMN), true, "touchIcon column missing in UrlMetadata results");
+ mAsserter.is(urlData.containsKey(URLMetadataTable.TOUCH_ICON_COLUMN), true, "touchIcon column missing in UrlMetadata results");
+ }
+ }
+ }
+
+ private class TestCombinedView extends TestCase {
+ @Override
+ public void test() throws Exception {
+ final String TITLE_1 = "Test Page 1";
+ final String TITLE_2 = "Test Page 2";
+ final String TITLE_3_HISTORY = "Test Page 3 (History Entry)";
+ final String TITLE_3_BOOKMARK = "Test Page 3 (Bookmark Entry)";
+ final String TITLE_3_BOOKMARK2 = "Test Page 3 (Bookmark Entry 2)";
+
+ final String URL_1 = "http://example1.com";
+ final String URL_2 = "http://example2.com";
+ final String URL_3 = "http://example3.com";
+
+ final int VISITS = 10;
+ final long LAST_VISITED = System.currentTimeMillis();
+
+ // Create a basic history entry
+ ContentValues basicHistory = createHistoryEntry(TITLE_1, URL_1, VISITS, LAST_VISITED);
+ long basicHistoryId = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, basicHistory));
+
+ // Create a basic bookmark entry
+ ContentValues basicBookmark = createBookmark(TITLE_2, URL_2, mMobileFolderId,
+ BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword");
+ long basicBookmarkId = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, basicBookmark));
+
+ // Create a history entry and bookmark entry with the same URL to
+ // represent a visited bookmark
+ ContentValues combinedHistory = createHistoryEntry(TITLE_3_HISTORY, URL_3, VISITS, LAST_VISITED);
+ long combinedHistoryId = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, combinedHistory));
+
+
+ ContentValues combinedBookmark = createBookmark(TITLE_3_BOOKMARK, URL_3, mMobileFolderId,
+ BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword");
+ long combinedBookmarkId = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, combinedBookmark));
+
+ ContentValues combinedBookmark2 = createBookmark(TITLE_3_BOOKMARK2, URL_3, mMobileFolderId,
+ BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword");
+ long combinedBookmarkId2 = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, combinedBookmark2));
+
+ // Create a bookmark folder to make sure it _doesn't_ show up in the results
+ ContentValues folderBookmark = createBookmark("", "", mMobileFolderId,
+ BrowserContract.Bookmarks.TYPE_FOLDER, 0, "tags", "description", "keyword");
+ mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, folderBookmark);
+
+ // Sort entries by url so we can check them individually
+ final Cursor c = mProvider.query(BrowserContract.Combined.CONTENT_URI, null, "", null, BrowserContract.Combined.URL);
+
+ try {
+ mAsserter.is(c.getCount(), 3, "3 combined entries found");
+
+ // First combined entry is basic history entry
+ mAsserter.is(c.moveToFirst(), true, "Found basic history entry");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined._ID)), 0L,
+ "Combined _id column should always be 0");
+ // TODO: Should we change BrowserProvider to make this return -1, not 0?
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.BOOKMARK_ID)), 0L,
+ "Bookmark id should be 0 for basic history entry");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.HISTORY_ID)), basicHistoryId,
+ "Basic history entry has correct history id");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Combined.TITLE)), TITLE_1,
+ "Basic history entry has correct title");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Combined.URL)), URL_1,
+ "Basic history entry has correct url");
+ mAsserter.is(c.getInt(c.getColumnIndex(BrowserContract.Combined.VISITS)), VISITS,
+ "Basic history entry has correct number of visits");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.DATE_LAST_VISITED)), LAST_VISITED,
+ "Basic history entry has correct last visit time");
+
+ // Second combined entry is basic bookmark entry
+ mAsserter.is(c.moveToNext(), true, "Found basic bookmark entry");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined._ID)), 0L,
+ "Combined _id column should always be 0");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.BOOKMARK_ID)), basicBookmarkId,
+ "Basic bookmark entry has correct bookmark id");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.HISTORY_ID)), -1L,
+ "History id should be -1 for basic bookmark entry");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Combined.TITLE)), TITLE_2,
+ "Basic bookmark entry has correct title");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Combined.URL)), URL_2,
+ "Basic bookmark entry has correct url");
+ mAsserter.is(c.getInt(c.getColumnIndex(BrowserContract.Combined.VISITS)), -1,
+ "Visits should be -1 for basic bookmark entry");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.DATE_LAST_VISITED)), -1L,
+ "Basic entry has correct last visit time");
+
+ // Third combined entry is a combined history/bookmark entry
+ mAsserter.is(c.moveToNext(), true, "Found third combined entry");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined._ID)), 0L,
+ "Combined _id column should always be 0");
+ // The bookmark data (bookmark_id and title) associated with the combined entry is non-deterministic,
+ // it might end up with data coming from any of the matching bookmark entries.
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.BOOKMARK_ID)) == combinedBookmarkId ||
+ c.getLong(c.getColumnIndex(BrowserContract.Combined.BOOKMARK_ID)) == combinedBookmarkId2, true,
+ "Combined entry has correct bookmark id");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Combined.TITLE)).equals(TITLE_3_BOOKMARK) ||
+ c.getString(c.getColumnIndex(BrowserContract.Combined.TITLE)).equals(TITLE_3_BOOKMARK2), true,
+ "Combined entry has title corresponding to bookmark entry");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.HISTORY_ID)), combinedHistoryId,
+ "Combined entry has correct history id");
+ mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Combined.URL)), URL_3,
+ "Combined entry has correct url");
+ mAsserter.is(c.getInt(c.getColumnIndex(BrowserContract.Combined.VISITS)), VISITS,
+ "Combined entry has correct number of visits");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.DATE_LAST_VISITED)), LAST_VISITED,
+ "Combined entry has correct last visit time");
+ } finally {
+ c.close();
+ }
+ }
+ }
+
+ private class TestCombinedViewDisplay extends TestCase {
+ @Override
+ public void test() throws Exception {
+ final String TITLE_1 = "Test Page 1";
+ final String TITLE_2 = "Test Page 2";
+ final String TITLE_3_HISTORY = "Test Page 3 (History Entry)";
+ final String TITLE_3_BOOKMARK = "Test Page 3 (Bookmark Entry)";
+
+ final String URL_1 = "http://example.com";
+ final String URL_2 = "http://example.org";
+ final String URL_3 = "http://examples2.com";
+
+ final int VISITS = 10;
+ final long LAST_VISITED = System.currentTimeMillis();
+
+ // Create a basic history entry
+ ContentValues basicHistory = createHistoryEntry(TITLE_1, URL_1, VISITS, LAST_VISITED);
+ ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, basicHistory));
+
+ // Create a basic bookmark entry
+ ContentValues basicBookmark = createBookmark(TITLE_2, URL_2, mMobileFolderId,
+ BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword");
+ mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, basicBookmark);
+
+ // Create a history entry and bookmark entry with the same URL to
+ // represent a visited bookmark
+ ContentValues combinedHistory = createHistoryEntry(TITLE_3_HISTORY, URL_3, VISITS, LAST_VISITED);
+ mProvider.insert(BrowserContract.History.CONTENT_URI, combinedHistory);
+
+ ContentValues combinedBookmark = createBookmark(TITLE_3_BOOKMARK, URL_3, mMobileFolderId,
+ BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword");
+ mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, combinedBookmark);
+
+ final Cursor c = mProvider.query(BrowserContract.Combined.CONTENT_URI, null, "", null, null);
+ try {
+ mAsserter.is(c.getCount(), 3, "3 combined entries found");
+ } finally {
+ c.close();
+ }
+ }
+ }
+
+ private class TestCombinedViewWithDeletedBookmark extends TestCase {
+ @Override
+ public void test() throws Exception {
+ final String TITLE = "Test Page 1";
+ final String URL = "http://example.com";
+ final int VISITS = 10;
+ final long LAST_VISITED = System.currentTimeMillis();
+
+ // Create a combined history entry
+ ContentValues combinedHistory = createHistoryEntry(TITLE, URL, VISITS, LAST_VISITED);
+ mProvider.insert(BrowserContract.History.CONTENT_URI, combinedHistory);
+
+ // Create a combined bookmark entry
+ ContentValues combinedBookmark = createBookmark(TITLE, URL, mMobileFolderId,
+ BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword");
+ long combinedBookmarkId = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, combinedBookmark));
+
+ Cursor c = mProvider.query(BrowserContract.Combined.CONTENT_URI, null, "", null, null);
+ mAsserter.is(c.getCount(), 1, "1 combined entry found");
+
+ mAsserter.is(c.moveToFirst(), true, "Found combined entry with bookmark id");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.BOOKMARK_ID)), combinedBookmarkId,
+ "Bookmark id should be set correctly on combined entry");
+
+ int deleted = mProvider.delete(BrowserContract.Bookmarks.CONTENT_URI,
+ BrowserContract.Bookmarks._ID + " = ?",
+ new String[] { String.valueOf(combinedBookmarkId) });
+
+ mAsserter.is((deleted == 1), true, "Inserted combined bookmark was deleted");
+ c.close();
+
+ c = mProvider.query(BrowserContract.Combined.CONTENT_URI, null, "", null, null);
+ mAsserter.is(c.getCount(), 1, "1 combined entry found");
+
+ mAsserter.is(c.moveToFirst(), true, "Found combined entry without bookmark id");
+ mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.BOOKMARK_ID)), 0L,
+ "Bookmark id should not be set to removed bookmark id");
+ c.close();
+ }
+ }
+
+ /*
+ * Verify that insert, update, delete, and bulkInsert operations
+ * notify the ambient content resolver. Each operation calls the
+ * content resolver notifyChange method synchronously, so it is
+ * okay to test sequentially.
+ */
+ private class TestBrowserProviderNotifications extends TestCase {
+ public static final String LOGTAG = "TestBPNotifications";
+
+ protected void ensureOnlyChangeNotifiedStartsWith(Uri expectedUri, String operation) {
+ if (expectedUri == null) {
+ throw new IllegalArgumentException("expectedUri must not be null");
+ }
+
+ if (mResolver.notifyChangeList.size() != 1) {
+ // Log to help post-mortem debugging
+ Log.w(LOGTAG, "after operation, notifyChangeList = " + mResolver.notifyChangeList);
+ }
+
+ mAsserter.is((long) mResolver.notifyChangeList.size(),
+ 1L,
+ "Content observer was notified exactly once by " + operation);
+
+ Uri uri = mResolver.notifyChangeList.poll();
+
+ mAsserter.isnot(uri,
+ null,
+ "Notification from " + operation + " was valid");
+
+ mAsserter.ok(uri.toString().startsWith(expectedUri.toString()),
+ "Content observer was notified exactly once by " + operation,
+ uri.toString() + " starts with expected prefix " + expectedUri);
+ }
+
+ @Override
+ public void test() throws Exception {
+ // Insert
+ final ContentValues h = createOneHistoryEntry();
+
+ mResolver.notifyChangeList.clear();
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h));
+
+ mAsserter.isnot(id,
+ -1L,
+ "Inserted item has valid id");
+
+ ensureOnlyChangeNotifiedStartsWith(BrowserContract.History.CONTENT_URI, "insert");
+
+ // Update
+ mResolver.notifyChangeList.clear();
+ h.put(BrowserContract.History.TITLE, "http://newexample.com");
+
+ long numUpdated = mProvider.update(BrowserContract.History.CONTENT_URI, h,
+ BrowserContract.History._ID + " = ?",
+ new String[] { String.valueOf(id) });
+
+ mAsserter.is(numUpdated,
+ 1L,
+ "Correct number of items are updated");
+
+ ensureOnlyChangeNotifiedStartsWith(BrowserContract.History.CONTENT_URI, "update");
+
+ // Delete
+ mResolver.notifyChangeList.clear();
+ long numDeleted = mProvider.delete(BrowserContract.History.CONTENT_URI, null, null);
+
+ mAsserter.is(numDeleted,
+ 1L,
+ "Correct number of items are deleted");
+
+ ensureOnlyChangeNotifiedStartsWith(BrowserContract.History.CONTENT_URI, "delete");
+
+ // Bulk insert
+ final ContentValues[] hs = new ContentValues[] { createOneHistoryEntry() };
+
+ mResolver.notifyChangeList.clear();
+ long numBulkInserted = mProvider.bulkInsert(BrowserContract.History.CONTENT_URI, hs);
+
+ mAsserter.is(numBulkInserted,
+ 1L,
+ "Correct number of items are bulkInserted");
+
+ ensureOnlyChangeNotifiedStartsWith(BrowserContract.History.CONTENT_URI, "bulkInsert");
+ }
+ }
+
+ /**
+ * Assert that the provided cursor has the expected number of rows,
+ * closing the cursor afterwards.
+ */
+ private void assertCountIsAndClose(Cursor c, int expectedCount, String message) {
+ try {
+ mAsserter.is(c.getCount(), expectedCount, message);
+ } finally {
+ c.close();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserSearchVisibility.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserSearchVisibility.java
new file mode 100644
index 0000000000..d8fc793fc9
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserSearchVisibility.java
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import android.support.v4.app.Fragment;
+import android.view.KeyEvent;
+import android.view.View;
+
+import com.robotium.solo.Condition;
+
+/**
+ * Test for browser search visibility.
+ * Sends queries from url bar input and verifies that browser search
+ * visibility is correct.
+ */
+public class testBrowserSearchVisibility extends BaseTest {
+ public void testSearchSuggestions() {
+ blockForGeckoReady();
+
+ focusUrlBar();
+
+ // search should not be visible when editing mode starts
+ assertBrowserSearchVisibility(false);
+
+ mActions.sendKeys("a");
+
+ // search should be visible when entry is not empty
+ assertBrowserSearchVisibility(true);
+
+ mActions.sendKeys("b");
+
+ // search continues to be visible when more text is added
+ assertBrowserSearchVisibility(true);
+
+ mActions.sendKeyCode(KeyEvent.KEYCODE_DEL);
+
+ // search continues to be visible when not all text is deleted
+ assertBrowserSearchVisibility(true);
+
+ mActions.sendKeyCode(KeyEvent.KEYCODE_DEL);
+
+ // search should not be visible, entry is empty now
+ assertBrowserSearchVisibility(false);
+ }
+
+ private void assertBrowserSearchVisibility(final boolean isVisible) {
+ waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ final Fragment browserSearch = getBrowserSearch();
+
+ // The fragment should not be present at all. Testing if the
+ // fragment is present but has no defined view is not a valid
+ // state.
+ if (browserSearch == null)
+ return !isVisible;
+
+ final View v = browserSearch.getView();
+ if (isVisible && v != null && v.getVisibility() == View.VISIBLE)
+ return true;
+
+ return false;
+ }
+ }, 5000);
+ }
+}
+
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBug1217581.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBug1217581.java
new file mode 100644
index 0000000000..fe3c047a3e
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBug1217581.java
@@ -0,0 +1,31 @@
+/* -*- 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.tests;
+
+
+import org.mozilla.gecko.Telemetry;
+
+public class testBug1217581 extends BaseTest {
+ // Take arbitrary histogram names used by Fennec.
+ private static final String TEST_HISTOGRAM_NAME = "FENNEC_SYNC_NUMBER_OF_SYNCS_COMPLETED";
+ private static final String TEST_KEYED_HISTOGRAM_NAME = "FX_MIGRATION_ERRORS";
+ private static final String TEST_KEY_NAME = "testBug1217581";
+
+
+ public void testBug1217581() {
+ blockForGeckoReady();
+
+ mAsserter.ok(true, "Checking that adding to a keyed histogram then adding to a normal histogram does not cause a crash.", "");
+ Telemetry.addToKeyedHistogram(TEST_KEYED_HISTOGRAM_NAME, TEST_KEY_NAME, 1);
+ Telemetry.addToHistogram(TEST_HISTOGRAM_NAME, 1);
+ mAsserter.ok(true, "Adding to a keyed histogram then to a normal histogram was a success!", "");
+
+ mAsserter.ok(true, "Checking that adding to a normal histogram then adding to a keyed histogram does not cause a crash.", "");
+ Telemetry.addToHistogram(TEST_HISTOGRAM_NAME, 1);
+ Telemetry.addToKeyedHistogram(TEST_KEYED_HISTOGRAM_NAME, TEST_KEY_NAME, 1);
+ mAsserter.ok(true, "Adding to a normal histogram then to a keyed histogram was a success!", "");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testCheck2.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testCheck2.java
new file mode 100644
index 0000000000..fc538b5bf6
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testCheck2.java
@@ -0,0 +1,61 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.json.JSONObject;
+
+public class testCheck2 extends PixelTest {
+ @Override
+ protected Type getTestType() {
+ return Type.TALOS;
+ }
+
+ public void testCheck2() {
+ String url = getAbsoluteUrl("/startup_test/fennecmark/cnn/cnn.com/index.html");
+
+ // Enable double-tap zooming
+ setPreferenceAndWaitForChange("browser.ui.zoom.force-user-scalable", true);
+
+ blockForGeckoReady();
+ loadAndPaint(url);
+
+ mDriver.setupScrollHandling();
+
+ /*
+ * for this test, we load the timecube page, and replay a recorded sequence of events
+ * that is a user panning/zooming around the page. specific things in the sequence
+ * include:
+ * - scroll on one axis followed by scroll on another axis
+ * - pinch zoom (in and out)
+ * - double-tap zoom (in and out)
+ * - multi-fling panning with different velocities on each fling
+ *
+ * this checkerboarding metric is going to be more of a "functional" style test than
+ * a "unit" style test; i.e. it covers a little bit of a lot of things to measure
+ * overall performance, but doesn't really allow identifying which part is slow.
+ */
+
+ MotionEventReplayer mer = new MotionEventReplayer(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop(),
+ mDriver.getGeckoWidth(), mDriver.getGeckoHeight());
+
+ float completeness = 0.0f;
+ mDriver.startCheckerboardRecording();
+ // replay the events
+ try {
+ mer.replayEvents(getAsset("testcheck2-motionevents"));
+ // give it some time to draw any final frames
+ Thread.sleep(1000);
+ completeness = mDriver.stopCheckerboardRecording();
+ } catch (Exception e) {
+ e.printStackTrace();
+ mAsserter.ok(false, "Exception while replaying events", e.toString());
+ }
+
+ mAsserter.dumpLog("__start_report" + completeness + "__end_report");
+ System.out.println("Completeness score: " + completeness);
+ long msecs = System.currentTimeMillis();
+ mAsserter.dumpLog("__startTimestamp" + msecs + "__endTimestamp");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testCheck3.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testCheck3.java
new file mode 100644
index 0000000000..28915bdbcb
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testCheck3.java
@@ -0,0 +1,61 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.json.JSONObject;
+
+public class testCheck3 extends PixelTest {
+ @Override
+ protected Type getTestType() {
+ return Type.TALOS;
+ }
+
+ public void testCheck3() {
+ String url = getAbsoluteUrl("/facebook.com/www.facebook.com/barackobama.html");
+
+ // Enable double-tap zooming
+ setPreferenceAndWaitForChange("browser.ui.zoom.force-user-scalable", true);
+
+ blockForGeckoReady();
+ loadAndPaint(url);
+
+ mDriver.setupScrollHandling();
+
+ /*
+ * for this test, we load the timecube page, and replay a recorded sequence of events
+ * that is a user panning/zooming around the page. specific things in the sequence
+ * include:
+ * - scroll on one axis followed by scroll on another axis
+ * - pinch zoom (in and out)
+ * - double-tap zoom (in and out)
+ * - multi-fling panning with different velocities on each fling
+ *
+ * this checkerboarding metric is going to be more of a "functional" style test than
+ * a "unit" style test; i.e. it covers a little bit of a lot of things to measure
+ * overall performance, but doesn't really allow identifying which part is slow.
+ */
+
+ MotionEventReplayer mer = new MotionEventReplayer(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop(),
+ mDriver.getGeckoWidth(), mDriver.getGeckoHeight());
+
+ float completeness = 0.0f;
+ mDriver.startCheckerboardRecording();
+ // replay the events
+ try {
+ mer.replayEvents(getAsset("testcheck2-motionevents"));
+ // give it some time to draw any final frames
+ Thread.sleep(1000);
+ completeness = mDriver.stopCheckerboardRecording();
+ } catch (Exception e) {
+ e.printStackTrace();
+ mAsserter.ok(false, "Exception while replaying events", e.toString());
+ }
+
+ mAsserter.dumpLog("__start_report" + completeness + "__end_report");
+ System.out.println("Completeness score: " + completeness);
+ long msecs = System.currentTimeMillis();
+ mAsserter.dumpLog("__startTimestamp" + msecs + "__endTimestamp");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDBUtils.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDBUtils.java
new file mode 100644
index 0000000000..700c1c2555
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDBUtils.java
@@ -0,0 +1,70 @@
+/* -*- 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.tests;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import org.mozilla.gecko.db.DBUtils;
+
+import java.io.File;
+import java.io.IOException;
+
+public class testDBUtils extends BaseTest {
+ public void testDBUtils() throws IOException {
+ final File cacheDir = getInstrumentation().getContext().getCacheDir();
+ final File dbFile = File.createTempFile("testDBUtils", ".db", cacheDir);
+ final SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(dbFile, null);
+ try {
+ mAsserter.ok(db != null, "Created DB.", null);
+ db.execSQL("CREATE TABLE foo (x INTEGER NOT NULL DEFAULT 0, y TEXT)");
+ final ContentValues v = new ContentValues();
+ v.put("x", 5);
+ v.put("y", "a");
+ db.insert("foo", null, v);
+ v.put("x", 2);
+ v.putNull("y");
+ db.insert("foo", null, v);
+ v.put("x", 3);
+ v.put("y", "z");
+ db.insert("foo", null, v);
+
+ DBUtils.UpdateOperation[] ops = {DBUtils.UpdateOperation.BITWISE_OR, DBUtils.UpdateOperation.ASSIGN};
+ ContentValues[] values = {new ContentValues(), new ContentValues()};
+ values[0].put("x", 0xff);
+ values[1].put("y", "hello");
+
+ final int updated = DBUtils.updateArrays(db, "foo", values, ops, "x >= 3", null);
+
+ mAsserter.ok(updated == 2, "Updated two rows.", null);
+ final Cursor out = db.query("foo", new String[]{"x", "y"}, null, null, null, null, "x");
+ try {
+ mAsserter.ok(out.moveToNext(), "Has first result.", null);
+ mAsserter.ok(2 == out.getInt(0), "1: First column was untouched.", null);
+ mAsserter.ok(out.isNull(1), "1: Second column was untouched.", null);
+
+ mAsserter.ok(out.moveToNext(), "Has second result.", null);
+ mAsserter.ok((0xff | 3) == out.getInt(0), "2: First column was ORed correctly.", null);
+ mAsserter.ok("hello".equals(out.getString(1)), "2: Second column was assigned correctly.", null);
+
+ mAsserter.ok(out.moveToNext(), "Has third result.", null);
+ mAsserter.ok((0xff | 5) == out.getInt(0), "3: First column was ORed correctly.", null);
+ mAsserter.ok("hello".equals(out.getString(1)), "3: Second column was assigned correctly.", null);
+
+ mAsserter.ok(!out.moveToNext(), "No more results.", null);
+ } finally {
+ out.close();
+ }
+
+ } finally {
+ try {
+ db.close();
+ } catch (Exception e) {
+ }
+ dbFile.delete();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDistribution.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDistribution.java
new file mode 100644
index 0000000000..4cc08cc5c4
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDistribution.java
@@ -0,0 +1,556 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.util.Locale;
+import java.util.jar.JarInputStream;
+import java.util.NoSuchElementException;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.BrowserLocaleManager;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.distribution.Distribution;
+import org.mozilla.gecko.distribution.ReferrerDescriptor;
+import org.mozilla.gecko.distribution.ReferrerReceiver;
+import org.mozilla.gecko.preferences.DistroSharedPrefsImport;
+import org.mozilla.gecko.preferences.GeckoPreferences;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.support.v4.content.LocalBroadcastManager;
+import android.util.Log;
+
+/**
+ * Tests distribution customization.
+ * mock-package.zip should contain the following directory structure:
+ *
+ * distribution/
+ * preferences.json
+ * bookmarks.json
+ * searchplugins/
+ * common/
+ * engine.xml
+ * suggestedsites/
+ * locales/
+ * en-US/
+ * suggestedsites.json
+ * extensions/
+ * distribution.test@mozilla.org.xpi
+ */
+public class testDistribution extends ContentProviderTest {
+ private static final String CLASS_REFERRER_RECEIVER = "org.mozilla.gecko.distribution.ReferrerReceiver";
+ private static final String ACTION_INSTALL_REFERRER = "com.android.vending.INSTALL_REFERRER";
+ private static final int WAIT_TIMEOUT_MSEC = 10000;
+ public static final String LOGTAG = "GeckoTestDistribution";
+
+ public static class TestableDistribution extends Distribution {
+ @Override
+ protected JarInputStream fetchDistribution(URI uri,
+ HttpURLConnection connection) throws IOException {
+ Log.i(LOGTAG, "Not downloading: this is a test.");
+ return null;
+ }
+
+ public TestableDistribution(Context context) {
+ super(context);
+ }
+
+ public void go() {
+ doInit();
+ }
+
+ public static void clearReferrerDescriptorForTesting() {
+ referrer = null;
+ }
+
+ public static ReferrerDescriptor getReferrerDescriptorForTesting() {
+ return referrer;
+ }
+ }
+
+ private static final String MOCK_PACKAGE = "mock-package.zip";
+ private static final int PREF_REQUEST_ID = 0x7357;
+
+ private Activity mActivity;
+
+ /**
+ * This is a hack.
+ *
+ * Startup results in us writing prefs -- we fetch the Distribution, which
+ * caches its state. Our tests try to wipe those prefs, but apparently
+ * sometimes race with startup, which leads to us not getting one of our
+ * expected messages. The test fails.
+ *
+ * This hack waits for any existing background tasks -- such as the one that
+ * writes prefs -- to finish before we begin the test.
+ */
+ private void waitForBackgroundHappiness() {
+ final Object signal = new Object();
+ final Runnable done = new Runnable() {
+ @Override
+ public void run() {
+ synchronized (signal) {
+ signal.notify();
+ }
+ }
+ };
+ synchronized (signal) {
+ ThreadUtils.postToBackgroundThread(done);
+ try {
+ signal.wait();
+ } catch (InterruptedException e) {
+ mAsserter.ok(false, "InterruptedException waiting on background thread.", e.toString());
+ }
+ }
+ mAsserter.dumpLog("Background task completed. Proceeding.");
+ }
+
+ public void testDistribution() throws Exception {
+ mActivity = getActivity();
+
+ String mockPackagePath = getMockPackagePath();
+
+ // Wait for any startup-related background distribution shenanigans to
+ // finish. This reduces the chance of us racing with startup pref writes.
+ waitForBackgroundHappiness();
+
+ // Pre-clear distribution pref, run basic preferences and en-US localized preferences Tests
+ clearDistributionPref();
+ clearDistributionFromDataData();
+
+ setTestLocale("en-US");
+ try {
+ initDistribution(mockPackagePath);
+ } catch(NoSuchElementException e) {
+ // TODO: determine why this exception is intermittently thrown
+ Log.w(LOGTAG, "NoSuchElementException on first initDistribution -- will retry");
+ mSolo.sleep(4000);
+ initDistribution(mockPackagePath);
+ }
+ checkPreferences();
+ checkAndroidPreferences();
+ checkLocalizedPreferences("en-US");
+ checkSearchPlugin();
+ checkAddon();
+
+ // Pre-clear distribution pref, and run es-MX localized preferences Test
+ clearDistributionPref();
+ clearDistributionFromDataData();
+ setTestLocale("es-MX");
+ initDistribution(mockPackagePath);
+ checkLocalizedPreferences("es-MX");
+
+ // Test the (stubbed) download interaction.
+ setTestLocale("en-US");
+ clearDistributionPref();
+ clearDistributionFromDataData();
+ doTestValidReferrerIntent();
+
+ clearDistributionPref();
+ clearDistributionFromDataData();
+ doTestInvalidReferrerIntent();
+ }
+
+ private void setOSLocale(Locale locale) {
+ Locale.setDefault(locale);
+ BrowserLocaleManager.storeAndNotifyOSLocale(GeckoSharedPrefs.forProfile(mActivity), locale);
+ }
+
+ private abstract class ExpectNoDistributionCallback implements Distribution.ReadyCallback {
+ @Override
+ public void distributionFound(final Distribution distribution) {
+ mAsserter.ok(false, "No distributionFound.", "Wasn't expecting a distribution!");
+ synchronized (distribution) {
+ distribution.notifyAll();
+ }
+ }
+
+ @Override
+ public void distributionArrivedLate(final Distribution distribution) {
+ mAsserter.ok(false, "No distributionArrivedLate.", "Wasn't expecting a late distribution!");
+ }
+ }
+
+ private void doReferrerTest(String ref, final TestableDistribution distribution, final Distribution.ReadyCallback distributionReady) throws InterruptedException {
+ final Intent intent = new Intent(ACTION_INSTALL_REFERRER);
+ intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, CLASS_REFERRER_RECEIVER);
+ intent.putExtra("referrer", ref);
+
+ final BroadcastReceiver receiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.i(LOGTAG, "Test received " + intent.getAction());
+
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ public void run() {
+ distribution.addOnDistributionReadyCallback(distributionReady);
+ distribution.go();
+ }
+ });
+ }
+ };
+
+ IntentFilter intentFilter = new IntentFilter(ReferrerReceiver.ACTION_REFERRER_RECEIVED);
+ final LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(mActivity);
+ localBroadcastManager.registerReceiver(receiver, intentFilter);
+
+ Log.i(LOGTAG, "Broadcasting referrer intent.");
+ try {
+ mActivity.sendBroadcast(intent, null);
+ synchronized (distribution) {
+ distribution.wait(WAIT_TIMEOUT_MSEC);
+ }
+ } finally {
+ localBroadcastManager.unregisterReceiver(receiver);
+ }
+ }
+
+ public void doTestValidReferrerIntent() throws Exception {
+ // Equivalent to
+ // am broadcast -a com.android.vending.INSTALL_REFERRER \
+ // -n org.mozilla.fennec/org.mozilla.gecko.distribution.ReferrerReceiver \
+ // --es "referrer" "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=distribution"
+ final String ref = "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=distribution";
+ final TestableDistribution distribution = new TestableDistribution(mActivity);
+ final Distribution.ReadyCallback distributionReady = new ExpectNoDistributionCallback() {
+ @Override
+ public void distributionNotFound() {
+ Log.i(LOGTAG, "Test told distribution processing is done.");
+ mAsserter.ok(!distribution.exists(), "Not processed.", "No download because we're offline.");
+ ReferrerDescriptor referrerValue = TestableDistribution.getReferrerDescriptorForTesting();
+ mAsserter.dumpLog("Referrer was " + referrerValue);
+ mAsserter.is(referrerValue.content, "testcontent", "Referrer content");
+ mAsserter.is(referrerValue.medium, "testmedium", "Referrer medium");
+ mAsserter.is(referrerValue.campaign, "distribution", "Referrer campaign");
+ synchronized (distribution) {
+ distribution.notifyAll();
+ }
+ }
+ };
+
+ doReferrerTest(ref, distribution, distributionReady);
+ }
+
+ /**
+ * Test processing if the campaign isn't "distribution". The intent shouldn't
+ * result in a download, and won't be saved as the temporary referrer,
+ * even if we *do* include it in a Campaign:Set message.
+ */
+ public void doTestInvalidReferrerIntent() throws Exception {
+ // Equivalent to
+ // am broadcast -a com.android.vending.INSTALL_REFERRER \
+ // -n org.mozilla.fennec/org.mozilla.gecko.distribution.ReferrerReceiver \
+ // --es "referrer" "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=testname"
+ final String ref = "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=testname";
+ final TestableDistribution distribution = new TestableDistribution(mActivity);
+ final Distribution.ReadyCallback distributionReady = new ExpectNoDistributionCallback() {
+ @Override
+ public void distributionNotFound() {
+ mAsserter.ok(!distribution.exists(), "Not processed.", "No download because campaign was wrong.");
+ ReferrerDescriptor referrerValue = TestableDistribution.getReferrerDescriptorForTesting();
+ mAsserter.is(referrerValue, null, "No referrer.");
+ synchronized (distribution) {
+ distribution.notifyAll();
+ }
+ }
+ };
+
+ doReferrerTest(ref, distribution, distributionReady);
+ }
+
+ // Initialize the distribution from the mock package.
+ private Distribution initDistribution(String aPackagePath) {
+ // Call Distribution.init with the mock package.
+ Actions.EventExpecter distributionSetExpecter = mActions.expectGeckoEvent("Distribution:Set:OK");
+ Distribution dist = Distribution.init(mActivity, aPackagePath, "prefs-" + System.currentTimeMillis());
+ distributionSetExpecter.blockForEvent();
+ distributionSetExpecter.unregisterListener();
+ DistroSharedPrefsImport.importPreferences(mActivity, dist);
+ return dist;
+ }
+
+ // Test distribution and preferences values stored in preferences.json
+ private void checkPreferences() {
+ String prefID = "distribution.id";
+ String prefAbout = "distribution.about";
+ String prefVersion = "distribution.version";
+ String prefTestBoolean = "distribution.test.boolean";
+ String prefTestString = "distribution.test.string";
+ String prefTestInt = "distribution.test.int";
+
+ try {
+ final String[] prefNames = { prefID,
+ prefAbout,
+ prefVersion,
+ prefTestBoolean,
+ prefTestString,
+ prefTestInt };
+
+ final JSONArray preferences = getPrefs(prefNames);
+ for (int i = 0; i < preferences.length(); i++) {
+ JSONObject pref = (JSONObject) preferences.get(i);
+ String name = pref.getString("name");
+
+ if (name.equals(prefID)) {
+ mAsserter.is(pref.getString("value"), "test-partner", "check " + prefID);
+ } else if (name.equals(prefAbout)) {
+ mAsserter.is(pref.getString("value"), "Test Partner", "check " + prefAbout);
+ } else if (name.equals(prefVersion)) {
+ mAsserter.is(pref.getInt("value"), 1, "check " + prefVersion);
+ } else if (name.equals(prefTestBoolean)) {
+ mAsserter.is(pref.getBoolean("value"), true, "check " + prefTestBoolean);
+ } else if (name.equals(prefTestString)) {
+ mAsserter.is(pref.getString("value"), "test", "check " + prefTestString);
+ } else if (name.equals(prefTestInt)) {
+ mAsserter.is(pref.getInt("value"), 5, "check " + prefTestInt);
+ }
+ }
+
+ } catch (JSONException e) {
+ mAsserter.ok(false, "exception getting preferences", e.toString());
+ }
+ }
+
+ private void checkAndroidPreferences() {
+ final SharedPreferences sharedPreferences = GeckoSharedPrefs.forProfile(getActivity());
+ String prefTestBoolean = "android.distribution.test.boolean";
+ String prefTestString = "android.distribution.test.string";
+ String prefTestInt = "android.distribution.test.int";
+ String prefTestLong = "android.distribution.test.long";
+
+ final String[] prefNames = { prefTestBoolean,
+ prefTestString,
+ prefTestInt,
+ prefTestLong };
+
+ try {
+ for (String name : prefNames) {
+ if (name.equals(prefTestBoolean)) {
+ mAsserter.is(sharedPreferences.getBoolean(GeckoPreferences.NON_PREF_PREFIX + name, false), true, "check " + prefTestBoolean);
+ } else if (name.equals(prefTestString)) {
+ mAsserter.is(sharedPreferences.getString(GeckoPreferences.NON_PREF_PREFIX + name, ""), "test", "check " + prefTestString);
+ } else if (name.equals(prefTestInt)) {
+ mAsserter.is(sharedPreferences.getInt(GeckoPreferences.NON_PREF_PREFIX + name, 0), 1, "check " + prefTestInt);
+ } else if (name.equals(prefTestLong)) {
+ mAsserter.is(sharedPreferences.getLong(GeckoPreferences.NON_PREF_PREFIX + name, 0), 2147483648l, "check " + prefTestLong);
+ }
+ }
+ } catch (ClassCastException e) {
+ mAsserter.ok(false, "exception getting preferences", e.toString());
+ }
+ }
+
+ private void checkSearchPlugin() {
+ Actions.RepeatedEventExpecter eventExpecter = mActions.expectGeckoEvent("SearchEngines:Data");
+ mActions.sendGeckoEvent("SearchEngines:GetVisible", null);
+
+ try {
+ JSONObject data = new JSONObject(eventExpecter.blockForEventData());
+ eventExpecter.unregisterListener();
+ JSONArray searchEngines = data.getJSONArray("searchEngines");
+ boolean foundEngine = false;
+ for (int i = 0; i < searchEngines.length(); i++) {
+ JSONObject engine = (JSONObject) searchEngines.get(i);
+ String name = engine.getString("name");
+ if (name.equals("Test search engine")) {
+ foundEngine = true;
+ break;
+ }
+ }
+ mAsserter.ok(foundEngine, "check search plugin", "found test search plugin");
+ } catch (JSONException e) {
+ mAsserter.ok(false, "exception getting search plugins", e.toString());
+ }
+ }
+
+ private void checkAddon() {
+ try {
+ final String[] prefNames = { "distribution.test.addonEnabled" };
+ final JSONArray preferences = getPrefs(prefNames);
+ final JSONObject pref = (JSONObject) preferences.get(0);
+ mAsserter.is(pref.getBoolean("value"), true, "check distribution add-on is enabled");
+ } catch (JSONException e) {
+ mAsserter.ok(false, "exception getting preferences", e.toString());
+ }
+ }
+
+ private JSONArray getPrefs(String[] prefNames) throws JSONException {
+ final JSONArray result = new JSONArray();
+
+ mActions.getPrefs(prefNames, new Actions.PrefHandlerBase() {
+ private void addItem(String pref, Object value) {
+ try {
+ final JSONObject item = new JSONObject();
+ item.put("name", pref).put("value", value);
+ result.put(item);
+ } catch (final JSONException e) {
+ mAsserter.ok(false, "exception getting prefs", e.toString());
+ }
+ }
+
+ @Override // Actions.PrefHandlerBase
+ public void prefValue(String pref, boolean value) {
+ addItem(pref, value);
+ }
+
+ @Override // Actions.PrefHandlerBase
+ public void prefValue(String pref, int value) {
+ addItem(pref, value);
+ }
+
+ @Override // Actions.PrefHandlerBase
+ public void prefValue(String pref, String value) {
+ addItem(pref, value);
+ }
+ }).waitForFinish();
+
+ return result;
+ }
+
+ // Sets the distribution locale preference for the test.
+ private void setTestLocale(String locale) {
+ BrowserLocaleManager.getInstance().setSelectedLocale(mActivity, locale);
+ }
+
+ // Test localized distribution and preferences values stored in preferences.json
+ private void checkLocalizedPreferences(final String aLocale) {
+ final String prefAbout = "distribution.about";
+ final String prefLocalizeable = "distribution.test.localizeable";
+ final String prefLocalizeableOverride = "distribution.test.localizeable-override";
+ final String[] prefNames = { prefAbout, prefLocalizeable, prefLocalizeableOverride };
+
+ mActions.getPrefs(prefNames, new Actions.PrefHandlerBase() {
+ @Override // Actions.PrefHandlerBase
+ public void prefValue(String name, String value) {
+ if (name.equals(prefAbout)) {
+ if (aLocale.equals("en-US")) {
+ mAsserter.is(value, "Test Partner", "check " + prefAbout);
+ } else if (aLocale.equals("es-MX")) {
+ mAsserter.is(value, "Afiliado de Prueba", "check " + prefAbout);
+ }
+ } else if (name.equals(prefLocalizeable)) {
+ if (aLocale.equals("en-US")) {
+ mAsserter.is(value, "http://test.org/en-US/en-US/", "check " + prefLocalizeable);
+ } else if (aLocale.equals("es-MX")) {
+ mAsserter.is(value, "http://test.org/es-MX/es-MX/", "check " + prefLocalizeable);
+ }
+ } else if (name.equals(prefLocalizeableOverride)) {
+ if (aLocale.equals("en-US")) {
+ mAsserter.is(value, "http://cheese.com", "check " + prefLocalizeableOverride);
+ } else if (aLocale.equals("es-MX")) {
+ mAsserter.is(value, "http://test.org/es-MX/", "check " + prefLocalizeableOverride);
+ }
+ } else {
+ // Raise exception.
+ super.prefValue(name, value);
+ }
+ }
+ }).waitForFinish();
+ }
+
+ // Copies the mock package to the data directory and returns the file path to it.
+ private String getMockPackagePath() {
+ String mockPackagePath = "";
+
+ try {
+ InputStream inStream = getAsset(MOCK_PACKAGE);
+ File dataDir = new File(mActivity.getApplicationInfo().dataDir);
+ File outFile = new File(dataDir, MOCK_PACKAGE);
+
+ OutputStream outStream = new FileOutputStream(outFile);
+ int b;
+ while ((b = inStream.read()) != -1) {
+ outStream.write(b);
+ }
+ inStream.close();
+ outStream.close();
+
+ mockPackagePath = outFile.getPath();
+
+ } catch (Exception e) {
+ mAsserter.ok(false, "exception copying mock distribution package to data directory", e.toString());
+ }
+
+ return mockPackagePath;
+ }
+
+ /**
+ * Clears the distribution pref to return distribution state to STATE_UNKNOWN,
+ * and wipes the in-memory referrer pigeonhole.
+ */
+ private void clearDistributionPref() {
+ mAsserter.dumpLog("Clearing distribution pref.");
+ SharedPreferences settings = mActivity.getSharedPreferences("GeckoApp", Activity.MODE_PRIVATE);
+ String keyName = mActivity.getPackageName() + ".distribution_state";
+ settings.edit().remove(keyName).commit();
+ TestableDistribution.clearReferrerDescriptorForTesting();
+ }
+
+ /**
+ * Clears any distribution found in /data/data.
+ */
+ private void clearDistributionFromDataData() throws Exception {
+ File dataDir = new File(mActivity.getApplicationInfo().dataDir);
+
+ // Recursively delete distribution files that Distribution.init copied to data directory.
+ File distDir = new File(dataDir, "distribution");
+ if (distDir.exists()) {
+ mAsserter.dumpLog("Clearing distribution from " + distDir.getAbsolutePath());
+ delete(distDir);
+ } else {
+ mAsserter.dumpLog("No distribution to clear from " + distDir.getAbsolutePath());
+ }
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ // TODO: Set up the content provider after setting the distribution.
+ super.setUp(sBrowserProviderCallable, BrowserContract.AUTHORITY, "browser.db");
+ }
+
+ private void delete(File file) throws Exception {
+ if (file.isDirectory()) {
+ File[] files = file.listFiles();
+ for (File f : files) {
+ delete(f);
+ }
+ }
+ mAsserter.ok(file.delete(), "clean up distribution files", "deleted " + file.getPath());
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ File dataDir = new File(mActivity.getApplicationInfo().dataDir);
+
+ // Delete mock package from data directory.
+ File mockPackage = new File(dataDir, MOCK_PACKAGE);
+ mAsserter.ok(mockPackage.delete(), "clean up mock package", "deleted " + mockPackage.getPath());
+
+ clearDistributionFromDataData();
+ clearDistributionPref();
+
+ super.tearDown();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDoorHanger.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDoorHanger.java
new file mode 100644
index 0000000000..2c3feb3a88
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDoorHanger.java
@@ -0,0 +1,205 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import android.widget.CheckBox;
+import android.view.View;
+import com.robotium.solo.Condition;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.Actions;
+
+/* This test will test if doorhangers are displayed and dismissed
+ The test will test:
+ * geolocation doorhangers - sharing and not sharing the location dismisses the doorhanger
+ * opening a new tab hides the doorhanger
+ * offline storage permission doorhangers - allowing and not allowing offline storage dismisses the doorhanger
+ * Password Manager doorhangers - Remember and Not Now options dismiss the doorhanger
+*/
+public class testDoorHanger extends BaseTest {
+ private boolean offlineAllowedByDefault = true;
+
+ public void testDoorHanger() {
+ String GEO_URL = getAbsoluteUrl(mStringHelper.ROBOCOP_GEOLOCATION_URL);
+ String BLANK_URL = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ String OFFLINE_STORAGE_URL = getAbsoluteUrl(mStringHelper.ROBOCOP_OFFLINE_STORAGE_URL);
+
+ blockForGeckoReady();
+
+ // Test geolocation notification
+ loadUrlAndWait(GEO_URL);
+ waitForText(mStringHelper.GEO_MESSAGE);
+ mAsserter.is(mSolo.searchText(mStringHelper.GEO_MESSAGE), true, "Geolocation doorhanger has been displayed");
+
+ // Test "Share" button hides the notification
+ waitForCheckBox();
+ mSolo.clickOnCheckBox(0);
+ mSolo.clickOnButton(mStringHelper.GEO_ALLOW);
+ waitForTextDismissed(mStringHelper.GEO_MESSAGE);
+ mAsserter.is(mSolo.searchText(mStringHelper.GEO_MESSAGE), false, "Geolocation doorhanger has been hidden when allowing share");
+
+ // Re-trigger geolocation notification
+ loadUrlAndWait(GEO_URL);
+ waitForText(mStringHelper.GEO_MESSAGE);
+
+ // Test "Don't share" button hides the notification
+ waitForCheckBox();
+ mSolo.clickOnCheckBox(0);
+ mSolo.clickOnButton(mStringHelper.GEO_DENY);
+ waitForTextDismissed(mStringHelper.GEO_MESSAGE);
+ mAsserter.is(mSolo.searchText(mStringHelper.GEO_MESSAGE), false, "Geolocation doorhanger has been hidden when denying share");
+
+ /* FIXME: disabled on fig - bug 880060 (for some reason this fails because of some raciness)
+ // Re-trigger geolocation notification
+ loadUrlAndWait(GEO_URL);
+ waitForText(GEO_MESSAGE);
+
+ // Add a new tab
+ addTab(BLANK_URL);
+
+ // Make sure doorhanger is hidden
+ mAsserter.is(mSolo.searchText(GEO_MESSAGE), false, "Geolocation doorhanger notification is hidden when opening a new tab");
+ */
+
+ // Save offline-allow-by-default preferences first
+ mActions.getPrefs(new String[] { "offline-apps.allow_by_default" },
+ new Actions.PrefHandlerBase() {
+ @Override // Actions.PrefHandlerBase
+ public void prefValue(String pref, boolean value) {
+ mAsserter.is(pref, "offline-apps.allow_by_default", "Expecting correct pref name");
+ offlineAllowedByDefault = value;
+ }
+ }).waitForFinish();
+
+ setPreferenceAndWaitForChange("offline-apps.allow_by_default", false);
+
+ // Load offline storage page
+ loadUrlAndWait(OFFLINE_STORAGE_URL);
+ waitForText(mStringHelper.OFFLINE_MESSAGE);
+
+ // Test doorhanger dismissed when tapping "Don't share"
+ waitForCheckBox();
+ mSolo.clickOnCheckBox(0);
+ mSolo.clickOnButton(mStringHelper.OFFLINE_DENY);
+ waitForTextDismissed(mStringHelper.OFFLINE_MESSAGE);
+ mAsserter.is(mSolo.searchText(mStringHelper.OFFLINE_MESSAGE), false, "Offline storage doorhanger notification is hidden when denying storage");
+
+ // Load offline storage page
+ loadUrlAndWait(OFFLINE_STORAGE_URL);
+ waitForText(mStringHelper.OFFLINE_MESSAGE);
+
+ // Test doorhanger dismissed when tapping "Allow" and is not displayed again
+ mSolo.clickOnButton(mStringHelper.OFFLINE_ALLOW);
+ waitForTextDismissed(mStringHelper.OFFLINE_MESSAGE);
+ mAsserter.is(mSolo.searchText(mStringHelper.OFFLINE_MESSAGE), false, "Offline storage doorhanger notification is hidden when allowing storage");
+ loadUrlAndWait(OFFLINE_STORAGE_URL);
+ mAsserter.is(mSolo.searchText(mStringHelper.OFFLINE_MESSAGE), false, "Offline storage doorhanger is no longer triggered");
+
+ // Revert offline setting
+ setPreferenceAndWaitForChange("offline-apps.allow_by_default", offlineAllowedByDefault);
+
+ // Load new login page
+ loadUrlAndWait(getAbsoluteUrl(mStringHelper.ROBOCOP_LOGIN_01_URL));
+ waitForText(mStringHelper.LOGIN_MESSAGE);
+
+ // Test doorhanger is dismissed when tapping "Remember".
+ mSolo.clickOnButton(mStringHelper.LOGIN_ALLOW);
+ waitForTextDismissed(mStringHelper.LOGIN_MESSAGE);
+ mAsserter.is(mSolo.searchText(mStringHelper.LOGIN_MESSAGE), false, "Login doorhanger notification is hidden when allowing saving password");
+
+ // Load login page
+ loadUrlAndWait(getAbsoluteUrl(mStringHelper.ROBOCOP_LOGIN_02_URL));
+ waitForText(mStringHelper.LOGIN_MESSAGE);
+
+ // Test doorhanger is dismissed when tapping "Never".
+ mSolo.clickOnButton(mStringHelper.LOGIN_DENY);
+ waitForTextDismissed(mStringHelper.LOGIN_MESSAGE);
+ mAsserter.is(mSolo.searchText(mStringHelper.LOGIN_MESSAGE), false, "Login doorhanger notification is hidden when denying saving password");
+
+ testPopupBlocking();
+ }
+
+ private void testPopupBlocking() {
+ String POPUP_URL = getAbsoluteUrl(mStringHelper.ROBOCOP_POPUP_URL);
+
+ setPreferenceAndWaitForChange("dom.disable_open_during_load", true);
+
+ // Load page with popup
+ loadUrlAndWait(POPUP_URL);
+ waitForText(mStringHelper.POPUP_MESSAGE);
+ mAsserter.is(mSolo.searchText(mStringHelper.POPUP_MESSAGE), true, "Popup blocker is displayed");
+
+ // Wait for the popup to be shown.
+ Actions.EventExpecter tabEventExpecter = mActions.expectGeckoEvent("Tab:Added");
+
+ waitForCheckBox();
+ mSolo.clickOnCheckBox(0);
+ mSolo.clickOnButton(mStringHelper.POPUP_ALLOW);
+ waitForTextDismissed(mStringHelper.POPUP_MESSAGE);
+ mAsserter.is(mSolo.searchText(mStringHelper.POPUP_MESSAGE), false, "Popup blocker is hidden when popup allowed");
+
+ try {
+ final JSONObject data = new JSONObject(tabEventExpecter.blockForEventData());
+
+ // Check to make sure the popup window was opened.
+ mAsserter.is("data:text/plain;charset=utf-8,a", data.getString("uri"), "Checking popup URL");
+
+ // Close the popup window.
+ closeTab(data.getInt("tabID"));
+
+ } catch (JSONException e) {
+ mAsserter.ok(false, "exception getting event data", e.toString());
+ }
+ tabEventExpecter.unregisterListener();
+
+ // Load page with popup
+ loadUrlAndWait(POPUP_URL);
+ waitForText(mStringHelper.POPUP_MESSAGE);
+ mAsserter.is(mSolo.searchText(mStringHelper.POPUP_MESSAGE), true, "Popup blocker is displayed");
+
+ waitForCheckBox();
+ mSolo.clickOnCheckBox(0);
+ mSolo.clickOnButton(mStringHelper.POPUP_DENY);
+ waitForTextDismissed(mStringHelper.POPUP_MESSAGE);
+ mAsserter.is(mSolo.searchText(mStringHelper.POPUP_MESSAGE), false, "Popup blocker is hidden when popup denied");
+
+ // Check that we're on the same page to verify that the popup was not shown.
+ verifyUrl(POPUP_URL);
+
+ setPreferenceAndWaitForChange("dom.disable_open_during_load", false);
+ }
+
+ // wait for a CheckBox view that is clickable
+ private void waitForCheckBox() {
+ waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ for (CheckBox view : mSolo.getCurrentViews(CheckBox.class)) {
+ // checking isClickable alone is not sufficient --
+ // intermittent "cannot click" errors persist unless
+ // additional checks are used
+ if (view.isClickable() &&
+ view.getVisibility() == View.VISIBLE &&
+ view.getWidth() > 0 &&
+ view.getHeight() > 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }, MAX_WAIT_MS);
+ }
+
+ // wait until the specified text is *not* displayed
+ private void waitForTextDismissed(final String text) {
+ waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return !mSolo.searchText(text);
+ }
+ }, MAX_WAIT_MS);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testEventDispatcher.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testEventDispatcher.java
new file mode 100644
index 0000000000..ad40459d57
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testEventDispatcher.java
@@ -0,0 +1,450 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.*;
+
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.util.BundleEventListener;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoEventListener;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.os.Bundle;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Tests the proper operation of EventDispatcher,
+ * including associated NativeJSObject objects.
+ */
+public class testEventDispatcher extends JavascriptBridgeTest
+ implements BundleEventListener, GeckoEventListener, NativeEventListener {
+
+ private static final String TEST_JS = "testEventDispatcher.js";
+ private static final String GECKO_EVENT = "Robocop:TestGeckoEvent";
+ private static final String GECKO_RESPONSE_EVENT = "Robocop:TestGeckoResponse";
+ private static final String NATIVE_EVENT = "Robocop:TestNativeEvent";
+ private static final String NATIVE_RESPONSE_EVENT = "Robocop:TestNativeResponse";
+ private static final String NATIVE_EXCEPTION_EVENT = "Robocop:TestNativeException";
+ private static final String UI_EVENT = "Robocop:TestUIEvent";
+ private static final String UI_RESPONSE_EVENT = "Robocop:TestUIResponse";
+ private static final String BACKGROUND_EVENT = "Robocop:TestBackgroundEvent";
+ private static final String BACKGROUND_RESPONSE_EVENT = "Robocop:TestBackgrondResponse";
+
+ private static final long WAIT_FOR_BUNDLE_EVENT_TIMEOUT_MILLIS = 20000; // 20 seconds
+
+ private NativeJSObject savedMessage;
+
+ private boolean handledGeckoEvent;
+ private boolean handledNativeEvent;
+ private boolean handledAsyncEvent;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ EventDispatcher.getInstance().registerGeckoThreadListener(
+ (GeckoEventListener) this, GECKO_EVENT, GECKO_RESPONSE_EVENT);
+ EventDispatcher.getInstance().registerGeckoThreadListener(
+ (NativeEventListener) this,
+ NATIVE_EVENT, NATIVE_RESPONSE_EVENT, NATIVE_EXCEPTION_EVENT);
+ EventDispatcher.getInstance().registerUiThreadListener(
+ this, UI_EVENT, UI_RESPONSE_EVENT);
+ EventDispatcher.getInstance().registerBackgroundThreadListener(
+ this, BACKGROUND_EVENT, BACKGROUND_RESPONSE_EVENT);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ EventDispatcher.getInstance().unregisterGeckoThreadListener(
+ (GeckoEventListener) this, GECKO_EVENT, GECKO_RESPONSE_EVENT);
+ EventDispatcher.getInstance().unregisterGeckoThreadListener(
+ (NativeEventListener) this,
+ NATIVE_EVENT, NATIVE_RESPONSE_EVENT, NATIVE_EXCEPTION_EVENT);
+ EventDispatcher.getInstance().unregisterUiThreadListener(
+ this, UI_EVENT, UI_RESPONSE_EVENT);
+ EventDispatcher.getInstance().unregisterBackgroundThreadListener(
+ this, BACKGROUND_EVENT, BACKGROUND_RESPONSE_EVENT);
+
+ super.tearDown();
+ }
+
+ private synchronized void waitForAsyncEvent() {
+ final long startTime = System.nanoTime();
+ while (!handledAsyncEvent) {
+ if (System.nanoTime() - startTime
+ >= WAIT_FOR_BUNDLE_EVENT_TIMEOUT_MILLIS * 1e6 /* ns per ms */) {
+ fFail("Should have completed event before timeout");
+ }
+ try {
+ wait(1000); // Wait for 1 second at a time.
+ } catch (final InterruptedException e) {
+ // Attempt waiting again.
+ }
+ }
+ handledAsyncEvent = false;
+ }
+
+ private synchronized void notifyAsyncEvent() {
+ handledAsyncEvent = true;
+ notifyAll();
+ }
+
+ public void testEventDispatcher() {
+ blockForReadyAndLoadJS(TEST_JS);
+
+ getJS().syncCall("send_test_message", GECKO_EVENT);
+ fAssertTrue("Should have handled Gecko event synchronously", handledGeckoEvent);
+
+ getJS().syncCall("send_message_for_response", GECKO_RESPONSE_EVENT, "success");
+ getJS().syncCall("send_message_for_response", GECKO_RESPONSE_EVENT, "error");
+
+ getJS().syncCall("send_test_message", NATIVE_EVENT);
+ fAssertTrue("Should have handled native event synchronously", handledNativeEvent);
+
+ getJS().syncCall("send_message_for_response", NATIVE_RESPONSE_EVENT, "success");
+ getJS().syncCall("send_message_for_response", NATIVE_RESPONSE_EVENT, "error");
+
+ getJS().syncCall("send_test_message", NATIVE_EXCEPTION_EVENT);
+
+ getJS().syncCall("send_test_message", UI_EVENT);
+ waitForAsyncEvent();
+
+ getJS().syncCall("send_message_for_response", UI_RESPONSE_EVENT, "success");
+ waitForAsyncEvent();
+
+ getJS().syncCall("send_message_for_response", UI_RESPONSE_EVENT, "error");
+ waitForAsyncEvent();
+
+ getJS().syncCall("send_test_message", BACKGROUND_EVENT);
+ waitForAsyncEvent();
+
+ getJS().syncCall("send_message_for_response", BACKGROUND_RESPONSE_EVENT, "success");
+ waitForAsyncEvent();
+
+ getJS().syncCall("send_message_for_response", BACKGROUND_RESPONSE_EVENT, "error");
+ waitForAsyncEvent();
+
+ getJS().syncCall("finish_test");
+ }
+
+ @Override
+ public void handleMessage(final String event, final Bundle message,
+ final EventCallback callback) {
+
+ if (UI_EVENT.equals(event) || UI_RESPONSE_EVENT.equals(event)) {
+ fAssertTrue("UI event should be on UI thread", ThreadUtils.isOnUiThread());
+
+ } else if (BACKGROUND_EVENT.equals(event) || BACKGROUND_RESPONSE_EVENT.equals(event)) {
+ fAssertTrue("Background event should be on background thread",
+ ThreadUtils.isOnBackgroundThread());
+
+ } else {
+ fFail("Event type should be valid: " + event);
+ }
+
+ if (UI_EVENT.equals(event) || BACKGROUND_EVENT.equals(event)) {
+ checkBundle(message);
+ checkBundle(message.getBundle("object"));
+
+ } else if (UI_RESPONSE_EVENT.equals(event) || BACKGROUND_RESPONSE_EVENT.equals(event)) {
+ final String response = message.getString("response");
+ if ("success".equals(response)) {
+ callback.sendSuccess(response);
+ } else if ("error".equals(response)) {
+ callback.sendError(response);
+ } else {
+ fFail("Response type should be valid: " + response);
+ }
+
+ } else {
+ fFail("Event type should be valid: " + event);
+ }
+
+ notifyAsyncEvent();
+ }
+
+ @Override
+ public void handleMessage(final String event, final JSONObject message) {
+ ThreadUtils.assertOnGeckoThread();
+
+ try {
+ if (GECKO_EVENT.equals(event)) {
+ checkJSONObject(message);
+ checkJSONObject(message.getJSONObject("object"));
+ handledGeckoEvent = true;
+
+ } else if (GECKO_RESPONSE_EVENT.equals(event)) {
+ final String response = message.getString("response");
+ if ("success".equals(response)) {
+ EventDispatcher.sendResponse(message, response);
+ } else if ("error".equals(response)) {
+ EventDispatcher.sendError(message, response);
+ } else {
+ fFail("Response type should be valid: " + response);
+ }
+
+ } else {
+ fFail("Event type should be valid: " + event);
+ }
+ } catch (final JSONException e) {
+ fFail(e.toString());
+ }
+ }
+
+ @Override
+ public void handleMessage(final String event, final NativeJSObject message,
+ final EventCallback callback) {
+ ThreadUtils.assertOnGeckoThread();
+
+ if (NATIVE_EVENT.equals(event)) {
+ checkNativeJSObject(message);
+ checkNativeJSObject(message.getObject("object"));
+ fAssertNotSame("optObject returns existent value",
+ null, message.optObject("object", null));
+ fAssertSame("optObject returns fallback value if nonexistent",
+ null, message.optObject("nonexistent_object", null));
+
+ final NativeJSObject[] objectArray = message.getObjectArray("objectArray");
+ fAssertNotNull("Native object array should exist", objectArray);
+ fAssertEquals("Native object array has correct length", 2, objectArray.length);
+ fAssertSame("Native object array index 0 has correct value", null, objectArray[0]);
+ fAssertNotSame("Native object array index 1 has correct value", null, objectArray[1]);
+ checkNativeJSObject(objectArray[1]);
+ fAssertNotSame("optObjectArray returns existent value",
+ null, message.optObjectArray("objectArray", null));
+ fAssertSame("optObjectArray returns fallback value if nonexistent",
+ null, message.optObjectArray("nonexistent_objectArray", null));
+
+ final Bundle bundle = message.toBundle();
+ checkBundle(bundle);
+ checkBundle(bundle.getBundle("object"));
+ fAssertNotSame("optBundle returns property value if it exists",
+ null, message.optBundle("object", null));
+ fAssertSame("optBundle returns fallback value if property does not exist",
+ null, message.optBundle("nonexistent_object", null));
+
+ final Bundle[] bundleArray = message.getBundleArray("objectArray");
+ fAssertNotNull("Native bundle array should exist", bundleArray);
+ fAssertEquals("Native bundle array has correct length", 2, bundleArray.length);
+ fAssertSame("Native bundle array index 0 has correct value", null, bundleArray[0]);
+ fAssertNotSame("Native bundle array index 1 has correct value", null, bundleArray[1]);
+ checkBundle(bundleArray[1]);
+ fAssertNotSame("optBundleArray returns existent value",
+ null, message.optBundleArray("objectArray", null));
+ fAssertSame("optBundleArray returns fallback value if nonexistent",
+ null, message.optBundleArray("nonexistent_objectArray", null));
+
+ handledNativeEvent = true;
+
+ } else if (NATIVE_RESPONSE_EVENT.equals(event)) {
+ final String response = message.getString("response");
+ if ("success".equals(response)) {
+ callback.sendSuccess(response);
+ } else if ("error".equals(response)) {
+ callback.sendError(response);
+ } else {
+ fFail("Response type should be valid: " + response);
+ }
+
+ // Save this message for post-disposal check.
+ savedMessage = message;
+
+ } else if (NATIVE_EXCEPTION_EVENT.equals(event)) {
+ // Make sure we throw the right exceptions.
+ try {
+ message.getString(null);
+ fFail("null property name should throw IllegalArgumentException");
+ } catch (final IllegalArgumentException e) {
+ }
+
+ try {
+ message.getString("nonexistent_string");
+ fFail("Nonexistent property name should throw InvalidPropertyException");
+ } catch (final NativeJSObject.InvalidPropertyException e) {
+ }
+
+ try {
+ message.getString("int");
+ fFail("Wrong property type should throw InvalidPropertyException");
+ } catch (final NativeJSObject.InvalidPropertyException e) {
+ }
+
+ fAssertNotSame("Should have saved a message", null, savedMessage);
+ try {
+ savedMessage.toString();
+ fFail("Using NativeJSContainer should throw after disposal");
+ } catch (final NullPointerException e) {
+ }
+
+ // Save this test for last; make sure EventDispatcher catches InvalidPropertyException.
+ message.getString("nonexistent_string");
+ fFail("EventDispatcher should catch InvalidPropertyException");
+
+ } else {
+ fFail("Event type should be valid: " + event);
+ }
+ }
+
+ private void checkBundle(final Bundle bundle) {
+ fAssertEquals("Bundle boolean has correct value", true, bundle.getBoolean("boolean"));
+ fAssertEquals("Bundle int has correct value", 1, bundle.getInt("int"));
+ fAssertEquals("Bundle double has correct value", 0.5, bundle.getDouble("double"));
+ fAssertEquals("Bundle string has correct value", "foo", bundle.getString("string"));
+
+ final boolean[] booleanArray = bundle.getBooleanArray("booleanArray");
+ fAssertNotNull("Bundle boolean array should exist", booleanArray);
+ fAssertEquals("Bundle boolean array has correct length", 2, booleanArray.length);
+ fAssertEquals("Bundle boolean array index 0 has correct value", false, booleanArray[0]);
+ fAssertEquals("Bundle boolean array index 1 has correct value", true, booleanArray[1]);
+
+ final int[] intArray = bundle.getIntArray("intArray");
+ fAssertNotNull("Bundle int array should exist", intArray);
+ fAssertEquals("Bundle int array has correct length", 2, intArray.length);
+ fAssertEquals("Bundle int array index 0 has correct value", 2, intArray[0]);
+ fAssertEquals("Bundle int array index 1 has correct value", 3, intArray[1]);
+
+ final double[] doubleArray = bundle.getDoubleArray("doubleArray");
+ fAssertNotNull("Bundle double array should exist", doubleArray);
+ fAssertEquals("Bundle double array has correct length", 2, doubleArray.length);
+ fAssertEquals("Bundle double array index 0 has correct value", 1.5, doubleArray[0]);
+ fAssertEquals("Bundle double array index 1 has correct value", 2.5, doubleArray[1]);
+
+ final String[] stringArray = bundle.getStringArray("stringArray");
+ fAssertNotNull("Bundle string array should exist", stringArray);
+ fAssertEquals("Bundle string array has correct length", 2, stringArray.length);
+ fAssertEquals("Bundle string array index 0 has correct value", "bar", stringArray[0]);
+ fAssertEquals("Bundle string array index 1 has correct value", "baz", stringArray[1]);
+ }
+
+ private void checkJSONObject(final JSONObject object) throws JSONException {
+ fAssertEquals("JSON boolean has correct value", true, object.getBoolean("boolean"));
+ fAssertEquals("JSON int has correct value", 1, object.getInt("int"));
+ fAssertEquals("JSON double has correct value", 0.5, object.getDouble("double"));
+ fAssertEquals("JSON string has correct value", "foo", object.getString("string"));
+
+ final JSONArray booleanArray = object.getJSONArray("booleanArray");
+ fAssertNotNull("JSON boolean array should exist", booleanArray);
+ fAssertEquals("JSON boolean array has correct length", 2, booleanArray.length());
+ fAssertEquals("JSON boolean array index 0 has correct value",
+ false, booleanArray.getBoolean(0));
+ fAssertEquals("JSON boolean array index 1 has correct value",
+ true, booleanArray.getBoolean(1));
+
+ final JSONArray intArray = object.getJSONArray("intArray");
+ fAssertNotNull("JSON int array should exist", intArray);
+ fAssertEquals("JSON int array has correct length", 2, intArray.length());
+ fAssertEquals("JSON int array index 0 has correct value",
+ 2, intArray.getInt(0));
+ fAssertEquals("JSON int array index 1 has correct value",
+ 3, intArray.getInt(1));
+
+ final JSONArray doubleArray = object.getJSONArray("doubleArray");
+ fAssertNotNull("JSON double array should exist", doubleArray);
+ fAssertEquals("JSON double array has correct length", 2, doubleArray.length());
+ fAssertEquals("JSON double array index 0 has correct value",
+ 1.5, doubleArray.getDouble(0));
+ fAssertEquals("JSON double array index 1 has correct value",
+ 2.5, doubleArray.getDouble(1));
+
+ final JSONArray stringArray = object.getJSONArray("stringArray");
+ fAssertNotNull("JSON string array should exist", stringArray);
+ fAssertEquals("JSON string array has correct length", 2, stringArray.length());
+ fAssertEquals("JSON string array index 0 has correct value",
+ "bar", stringArray.getString(0));
+ fAssertEquals("JSON string array index 1 has correct value",
+ "baz", stringArray.getString(1));
+ }
+
+ private void checkNativeJSObject(final NativeJSObject object) {
+ fAssertEquals("Native boolean has correct value",
+ true, object.getBoolean("boolean"));
+ fAssertEquals("optBoolean returns existent value",
+ true, object.optBoolean("boolean", false));
+ fAssertEquals("optBoolean returns fallback value if nonexistent",
+ false, object.optBoolean("nonexistent_boolean", false));
+
+ fAssertEquals("Native int has correct value",
+ 1, object.getInt("int"));
+ fAssertEquals("optInt returns existent value",
+ 1, object.optInt("int", 0));
+ fAssertEquals("optInt returns fallback value if nonexistent",
+ 0, object.optInt("nonexistent_int", 0));
+
+ fAssertEquals("Native double has correct value",
+ 0.5, object.getDouble("double"));
+ fAssertEquals("optDouble returns existent value",
+ 0.5, object.optDouble("double", -0.5));
+ fAssertEquals("optDouble returns fallback value if nonexistent",
+ -0.5, object.optDouble("nonexistent_double", -0.5));
+
+ fAssertEquals("Native string has correct value",
+ "foo", object.getString("string"));
+ fAssertEquals("optDouble returns existent value",
+ "foo", object.optString("string", "bar"));
+ fAssertEquals("optDouble returns fallback value if nonexistent",
+ "bar", object.optString("nonexistent_string", "bar"));
+
+ final boolean[] booleanArray = object.getBooleanArray("booleanArray");
+ fAssertNotNull("Native boolean array should exist", booleanArray);
+ fAssertEquals("Native boolean array has correct length", 2, booleanArray.length);
+ fAssertEquals("Native boolean array index 0 has correct value", false, booleanArray[0]);
+ fAssertEquals("Native boolean array index 1 has correct value", true, booleanArray[1]);
+ fAssertNotSame("optBooleanArray returns existent value",
+ null, object.optBooleanArray("booleanArray", null));
+ fAssertSame("optBooleanArray returns fallback value if nonexistent",
+ null, object.optBooleanArray("nonexistent_booleanArray", null));
+
+ final int[] intArray = object.getIntArray("intArray");
+ fAssertNotNull("Native int array should exist", intArray);
+ fAssertEquals("Native int array has correct length", 2, intArray.length);
+ fAssertEquals("Native int array index 0 has correct value", 2, intArray[0]);
+ fAssertEquals("Native int array index 1 has correct value", 3, intArray[1]);
+ fAssertNotSame("optIntArray returns existent value",
+ null, object.optIntArray("intArray", null));
+ fAssertSame("optIntArray returns fallback value if nonexistent",
+ null, object.optIntArray("nonexistent_intArray", null));
+
+ final double[] doubleArray = object.getDoubleArray("doubleArray");
+ fAssertNotNull("Native double array should exist", doubleArray);
+ fAssertEquals("Native double array has correct length", 2, doubleArray.length);
+ fAssertEquals("Native double array index 0 has correct value", 1.5, doubleArray[0]);
+ fAssertEquals("Native double array index 1 has correct value", 2.5, doubleArray[1]);
+ fAssertNotSame("optDoubleArray returns existent value",
+ null, object.optDoubleArray("doubleArray", null));
+ fAssertSame("optDoubleArray returns fallback value if nonexistent",
+ null, object.optDoubleArray("nonexistent_doubleArray", null));
+
+ final String[] stringArray = object.getStringArray("stringArray");
+ fAssertNotNull("Native string array should exist", stringArray);
+ fAssertEquals("Native string array has correct length", 2, stringArray.length);
+ fAssertEquals("Native string array index 0 has correct value", "bar", stringArray[0]);
+ fAssertEquals("Native string array index 1 has correct value", "baz", stringArray[1]);
+ fAssertNotSame("optStringArray returns existent value",
+ null, object.optStringArray("stringArray", null));
+ fAssertSame("optStringArray returns fallback value if nonexistent",
+ null, object.optStringArray("nonexistent_stringArray", null));
+
+ fAssertEquals("Native has(null) is false", false, object.has("null"));
+ fAssertEquals("Native has(emptyString) is true", true, object.has("emptyString"));
+
+ fAssertEquals("Native optBoolean returns fallback value if null",
+ true, object.optBoolean("null", true));
+ fAssertEquals("Native optInt returns fallback value if null",
+ 42, object.optInt("null", 42));
+ fAssertEquals("Native optDouble returns fallback value if null",
+ -3.1415926535, object.optDouble("null", -3.1415926535));
+ fAssertEquals("Native optString returns fallback value if null",
+ "baz", object.optString("null", "baz"));
+
+ fAssertNotEquals("Native optString does not return fallback value if emptyString",
+ "baz", object.optString("emptyString", "baz"));
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilePicker.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilePicker.java
new file mode 100644
index 0000000000..c613eca8f3
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilePicker.java
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fFail;
+
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.GeckoApp;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.util.GeckoEventListener;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class testFilePicker extends JavascriptTest implements GeckoEventListener {
+ private static final String TEST_FILENAME = "/mnt/sdcard/my-favorite-martian.png";
+
+ public testFilePicker() {
+ super("testFilePicker.js");
+ }
+
+ @Override
+ public void handleMessage(String event, final JSONObject message) {
+ // We handle the FilePicker message here so we can send back hard coded file information. We
+ // don't want to try to emulate "picking" a file using the Android intent chooser.
+ if (event.equals("FilePicker:Show")) {
+ try {
+ message.put("file", TEST_FILENAME);
+ } catch (JSONException ex) {
+ fFail("Can't add filename to message " + TEST_FILENAME);
+ }
+
+ mActions.sendGeckoEvent("FilePicker:Result", message.toString());
+ }
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ GeckoApp.getEventDispatcher().registerGeckoThreadListener(this, "FilePicker:Show");
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+
+ GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this, "FilePicker:Show");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilterOpenTab.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilterOpenTab.java
new file mode 100644
index 0000000000..3c57b864aa
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilterOpenTab.java
@@ -0,0 +1,133 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.PrivateTab;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.TabsProvider;
+
+import android.content.ContentProvider;
+import android.content.Context;
+import android.database.Cursor;
+
+/**
+ * Tests that local tabs are filtered prior to upload.
+ * - create a set of tabs and persists them through TabsAccessor.
+ * - verifies that tabs are filtered by querying.
+ */
+public class testFilterOpenTab extends ContentProviderTest {
+ private static final String[] TABS_PROJECTION_COLUMNS = new String[] {
+ BrowserContract.Tabs.TITLE,
+ BrowserContract.Tabs.URL,
+ BrowserContract.Clients.GUID,
+ BrowserContract.Clients.NAME
+ };
+
+ private static final String LOCAL_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NULL";
+
+ /**
+ * Factory function that makes new ContentProvider instances.
+ * <p>
+ * We want a fresh provider each test, so this should be invoked in
+ * <code>setUp</code> before each individual test.
+ */
+ protected static Callable<ContentProvider> sTabProviderCallable = new Callable<ContentProvider>() {
+ @Override
+ public ContentProvider call() {
+ return new TabsProvider();
+ }
+ };
+
+ private Cursor getTabsFromLocalClient() throws Exception {
+ return mProvider.query(BrowserContract.Tabs.CONTENT_URI,
+ TABS_PROJECTION_COLUMNS,
+ LOCAL_TABS_SELECTION,
+ null,
+ null);
+ }
+
+ private Tab createTab(int id, String url, boolean external, int parentId, String title) {
+ return new Tab((Context) getActivity(), id, url, external, parentId, title);
+ }
+
+ private Tab createPrivateTab(int id, String url, boolean external, int parentId, String title) {
+ return new PrivateTab((Context) getActivity(), id, url, external, parentId, title);
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp(sTabProviderCallable, BrowserContract.TABS_AUTHORITY, "tabs.db");
+ mTests.add(new TestInsertLocalTabs());
+ }
+
+ public void testFilterOpenTab() throws Exception {
+ blockForGeckoReady();
+
+ for (int i = 0; i < mTests.size(); i++) {
+ Runnable test = mTests.get(i);
+
+ setTestName(test.getClass().getSimpleName());
+ test.run();
+ }
+ }
+
+ private class TestInsertLocalTabs extends TestCase {
+ @Override
+ public void test() throws Exception {
+ final String TITLE1 = "Google";
+ final String URL1 = "http://www.google.com/";
+ final String TITLE2 = "Mozilla Start Page";
+ final String URL2 = "about:home";
+ final String TITLE3 = "Chrome Weave URL";
+ final String URL3 = "chrome://weave/";
+ final String TITLE4 = "What You Cache Is What You Get";
+ final String URL4 = "wyciwyg://1/test.com";
+ final String TITLE5 = "Root Folder";
+ final String URL5 = "file:///";
+
+ // Create a list of local tabs.
+ List<Tab> tabs = new ArrayList<Tab>(6);
+ Tab tab1 = createTab(1, URL1, false, 0, TITLE1);
+ Tab tab2 = createTab(2, URL2, false, 0, TITLE2);
+ Tab tab3 = createTab(3, URL3, false, 0, TITLE3);
+ Tab tab4 = createTab(4, URL4, false, 0, TITLE4);
+ Tab tab5 = createTab(5, URL5, false, 0, TITLE5);
+ Tab tab6 = createPrivateTab(6, URL1, false, 0, TITLE1);
+ tabs.add(tab1);
+ tabs.add(tab2);
+ tabs.add(tab3);
+ tabs.add(tab4);
+ tabs.add(tab5);
+ tabs.add(tab6);
+
+ // Persist the created tabs. Normally, you should be careful that you get a profile on the
+ // original thread, and do the work in a background one, but for testing we don't.
+ final DatabaseHelper helper = new DatabaseHelper(getActivity(), mAsserter);
+ helper.getProfileDB().getTabsAccessor().persistLocalTabs(mResolver, tabs);
+
+ // Get the persisted tab and check if urls are filtered.
+ Cursor c = getTabsFromLocalClient();
+ assertCountIsAndClose(c, 1, 1 + " tabs entries found");
+ }
+ }
+
+ /**
+ * Assert that the provided cursor has the expected number of rows,
+ * closing the cursor afterwards.
+ */
+ private void assertCountIsAndClose(Cursor c, int expectedCount, String message) {
+ try {
+ mAsserter.is(c.getCount(), expectedCount, message);
+ } finally {
+ c.close();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFindInPage.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFindInPage.java
new file mode 100644
index 0000000000..2797fdf5b3
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFindInPage.java
@@ -0,0 +1,107 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fFail;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.Element;
+import org.mozilla.gecko.R;
+
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.util.GeckoEventListener;
+
+import org.json.JSONObject;
+
+import com.robotium.solo.Condition;
+
+public class testFindInPage extends JavascriptTest implements GeckoEventListener {
+ private static final int WAIT_FOR_CONDITION_MS = 3000;
+
+ protected Element next, close;
+
+ public testFindInPage() {
+ super("testFindInPage.js");
+ }
+
+ @Override
+ public void handleMessage(String event, final JSONObject message) {
+ if (event.equals("Test:FindInPage")) {
+ try {
+ final String text = message.getString("text");
+ final int nrOfMatches = Integer.parseInt(message.getString("nrOfMatches"));
+ findText(text, nrOfMatches);
+ } catch (Exception e) {
+ fFail("Can't extract find query from JSON");
+ }
+ }
+
+ if (event.equals("Test:CloseFindInPage")) {
+ try {
+ close.click();
+ } catch (Exception e) {
+ fFail("FindInPage prompt not opened");
+ }
+ }
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ EventDispatcher.getInstance().registerGeckoThreadListener(this,
+ "Test:FindInPage",
+ "Test:CloseFindInPage");
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+
+ EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
+ "Test:FindInPage",
+ "Test:CloseFindInPage");
+ }
+
+ public void findText(String text, int nrOfMatches){
+ selectMenuItem(mStringHelper.FIND_IN_PAGE_LABEL);
+ close = mDriver.findElement(getActivity(), R.id.find_close);
+ boolean success = waitForCondition ( new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ next = mDriver.findElement(getActivity(), R.id.find_next);
+ if (next != null) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }, WAIT_FOR_CONDITION_MS);
+ mAsserter.ok(success, "Looking for the next search match button in the Find in Page UI", "Found the next match button");
+
+ // TODO: Find a better way to wait and then enter the text
+ // Without the sleep this seems to work but the actions are not updated in the UI
+ mSolo.sleep(500);
+
+ mActions.sendKeys(text);
+ mActions.sendSpecialKey(Actions.SpecialKey.ENTER);
+
+ // Advance a few matches to scroll the page
+ for (int i=1;i < nrOfMatches;i++) {
+ success = waitForCondition ( new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ if (next.click()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }, WAIT_FOR_CONDITION_MS);
+ mSolo.sleep(500); // TODO: Find a better way to wait here because waitForCondition is not enough
+ mAsserter.ok(success, "Checking if the next button was clicked", "button was clicked");
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFlingCorrectness.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFlingCorrectness.java
new file mode 100644
index 0000000000..e173a8c165
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFlingCorrectness.java
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.PaintedSurface;
+
+/**
+ * Basic fling correctness test.
+ * - Loads a page and verifies it draws
+ * - Drags page upwards by 200 pixels to get ready for a fling
+ * - Fling the page downwards so we get back to the top and verify.
+ */
+public class testFlingCorrectness extends PixelTest {
+ public void testFlingCorrectness() {
+ String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BOXES_URL);
+
+ MotionEventHelper meh = new MotionEventHelper(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop());
+
+ blockForGeckoReady();
+
+ // load page and check we're at 0,0
+ loadAndVerifyBoxes(url);
+
+ // drag page upwards by 200 pixels (use two drags instead of one in case
+ // the screen size is small)
+ Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint();
+ meh.dragSync(10, 150, 10, 50);
+ meh.dragSync(10, 150, 10, 50);
+ PaintedSurface painted = waitForPaint(paintExpecter);
+ paintExpecter.unregisterListener();
+ try {
+ checkScrollWithBoxes(painted, 0, 200);
+ } finally {
+ painted.close();
+ }
+
+ // now fling page downwards using a 100-pixel drag but a velocity of 15px/sec, so that
+ // we scroll the full 200 pixels back to the top of the page
+ paintExpecter = mActions.expectPaint();
+ meh.flingSync(10, 50, 10, 150, 15);
+ painted = waitForPaint(paintExpecter);
+ paintExpecter.unregisterListener();
+ try {
+ checkScrollWithBoxes(painted, 0, 0);
+ } finally {
+ painted.close();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFormHistory.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFormHistory.java
new file mode 100644
index 0000000000..40968b9be2
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFormHistory.java
@@ -0,0 +1,104 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import java.io.File;
+
+import org.mozilla.gecko.db.BrowserContract.FormHistory;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+
+/**
+ * A basic form history contentprovider test.
+ * - inserts an element in form history when it is not yet set up
+ * - inserts an element in form history
+ * - updates an element in form history
+ * - deletes an element in form history
+ */
+public class testFormHistory extends BaseTest {
+ private static final String DB_NAME = "formhistory.sqlite";
+
+ public void testFormHistory() {
+ Context context = (Context)getActivity();
+ ContentResolver cr = context.getContentResolver();
+ ContentValues[] cvs = new ContentValues[1];
+ cvs[0] = new ContentValues();
+
+ blockForGeckoReady();
+
+ Uri formHistoryUri;
+ Uri insertUri;
+ Uri expectedUri;
+ int numUpdated;
+ int numDeleted;
+
+ cvs[0].put("fieldname", "fieldname");
+ cvs[0].put("value", "value");
+ cvs[0].put("timesUsed", "0");
+ cvs[0].put("guid", "guid");
+
+ // Attempt to insert into the db
+ formHistoryUri = FormHistory.CONTENT_URI;
+ Uri.Builder builder = formHistoryUri.buildUpon();
+ formHistoryUri = builder.appendQueryParameter("profilePath", mProfile).build();
+
+ insertUri = cr.insert(formHistoryUri, cvs[0]);
+ expectedUri = formHistoryUri.buildUpon().appendPath("1").build();
+ mAsserter.is(expectedUri.toString(), insertUri.toString(), "Insert returned correct uri");
+ SqliteCompare(DB_NAME, "SELECT * FROM moz_formhistory", cvs);
+
+ cvs[0].put("fieldname", "fieldname2");
+ cvs[0].putNull("guid");
+
+ numUpdated = cr.update(formHistoryUri, cvs[0], null, null);
+ mAsserter.is(1, numUpdated, "Correct number updated");
+ SqliteCompare(DB_NAME, "SELECT * FROM moz_formhistory", cvs);
+
+ numDeleted = cr.delete(formHistoryUri, null, null);
+ mAsserter.is(1, numDeleted, "Correct number deleted");
+ cvs = new ContentValues[0];
+ SqliteCompare(DB_NAME, "SELECT * FROM moz_formhistory", cvs);
+
+ cvs = new ContentValues[1];
+ cvs[0] = new ContentValues();
+ cvs[0].put("fieldname", "fieldname");
+ cvs[0].put("value", "value");
+ cvs[0].put("timesUsed", "0");
+ cvs[0].putNull("guid");
+
+ insertUri = cr.insert(formHistoryUri, cvs[0]);
+ expectedUri = formHistoryUri.buildUpon().appendPath("1").build();
+ mAsserter.is(expectedUri.toString(), insertUri.toString(), "Insert returned correct uri");
+ SqliteCompare(DB_NAME, "SELECT * FROM moz_formhistory", cvs);
+
+ cvs[0].put("guid", "guid");
+
+ numUpdated = cr.update(formHistoryUri, cvs[0], null, null);
+ mAsserter.is(1, numUpdated, "Correct number updated");
+ SqliteCompare(DB_NAME, "SELECT * FROM moz_formhistory", cvs);
+
+ numDeleted = cr.delete(formHistoryUri, null, null);
+ mAsserter.is(1, numDeleted, "Correct number deleted");
+ cvs = new ContentValues[0];
+ SqliteCompare(DB_NAME, "SELECT * FROM moz_formhistory", cvs);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ // remove the entire signons.sqlite file
+ File profile = new File(mProfile);
+ File db = new File(profile, "formhistory.sqlite");
+ if (db.delete()) {
+ mAsserter.dumpLog("tearDown deleted "+db.toString());
+ } else {
+ mAsserter.dumpLog("tearDown did not delete "+db.toString());
+ }
+
+ super.tearDown();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGeckoProfile.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGeckoProfile.java
new file mode 100644
index 0000000000..eb9a705be6
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGeckoProfile.java
@@ -0,0 +1,295 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import java.io.File;
+import java.util.Enumeration;
+import java.util.Hashtable;
+
+import org.mozilla.gecko.GeckoApp;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.GeckoProfileDirectories;
+import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.util.INIParser;
+import org.mozilla.gecko.util.INISection;
+
+import android.content.Context;
+import android.text.TextUtils;
+
+/**
+ * This patch tests GeckoProfile. It has unit tests for basic getting and removing of profiles, as well as
+ * some guest mode tests. It does not test locking and unlocking profiles yet. It does not test the file management in GeckoProfile.
+ */
+
+public class testGeckoProfile extends PixelTest {
+ private final String TEST_PROFILE_NAME = "testProfile";
+ private File mozDir;
+ public void testGeckoProfile() {
+ blockForGeckoReady();
+
+ try {
+ mozDir = GeckoProfileDirectories.getMozillaDirectory(getActivity());
+ } catch(Exception ex) {
+ // If we can't get the moz dir, something is wrong. Just fail quickly.
+ mAsserter.ok(false, "Couldn't get moz dir", ex.toString());
+ return;
+ }
+
+ checkProfileCreationDeletion();
+ checkGuestProfile();
+ }
+
+ // This getter just passes an activity. Passing null should throw.
+ private void checkDefaultGetter() {
+ // "Default" is a custom profile set up by the test harness.
+ mAsserter.info("Test using the test profile", GeckoProfile.CUSTOM_PROFILE);
+ GeckoProfile profile = GeckoProfile.get(getActivity());
+ verifyProfile(profile, GeckoProfile.CUSTOM_PROFILE, ((GeckoApp) getActivity()).getProfile().getDir(), true);
+
+ try {
+ profile = GeckoProfile.get(null);
+ mAsserter.ok(false, "Passing a null context should throw", profile.toString());
+ } catch(Exception ex) {
+ mAsserter.ok(true, "Passing a null context should throw", ex.toString());
+ }
+ }
+
+ // Test get(Context, String) methods
+ private void checkNamedGetter(String name) {
+ mAsserter.info("Test using a named profile", name);
+ GeckoProfile profile = GeckoProfile.get(getActivity(), name);
+ if (name != null) {
+ verifyProfile(profile, name, findDir(name), false);
+ removeProfile(profile, true);
+ } else {
+ // Passing in null for a profile name, should get you the default
+ File defaultProfile = ((GeckoApp) getActivity()).getProfile().getDir();
+ verifyProfile(profile, GeckoProfile.CUSTOM_PROFILE, defaultProfile, true);
+ }
+ }
+
+ // Test get(Context, String, String) methods
+ private void checkNameAndPathGetter(String name, boolean createBefore) {
+ if (name == null) {
+ checkNameAndPathGetter(name, null, createBefore);
+ } else {
+ checkNameAndPathGetter(name, name + "_FORCED_DIR", createBefore);
+ }
+ }
+
+ // Test get(Context, String, String) methods
+ private void checkNameAndPathGetter(String name, String path, boolean createBefore) {
+ mAsserter.info("Test using a named profile and path", name + ", " + path);
+ checkNameAndDirGetter(name, /* useFile */ false, path, /* file */ null, createBefore);
+ }
+
+ private void checkNameAndFileGetter(String name, boolean createBefore) {
+ if (name == null) {
+ checkNameAndFileGetter(name, null, createBefore);
+ } else {
+ checkNameAndFileGetter(name, new File(mozDir, name + "_FORCED_DIR"), createBefore);
+ }
+ }
+
+ private void checkNameAndFileGetter(String name, File f, boolean createBefore) {
+ mAsserter.info("Test using a named profile and File", name + ", " + f);
+ checkNameAndDirGetter(name, /* useFile */ true, /* path */ null, f, createBefore);
+ }
+
+ private void checkNameAndDirGetter(final String name, final boolean useFile,
+ String path, final File file,
+ final boolean createBefore) {
+ final File f;
+ if (useFile) {
+ f = file;
+ } else if (!TextUtils.isEmpty(path)) {
+ f = new File(mozDir, path);
+ path = f.getAbsolutePath();
+ } else {
+ f = null;
+ }
+
+ if (f != null && createBefore) {
+ // For some tests we create explicitly beforehand
+ f.mkdir();
+ }
+
+ final File testProfileDir = ((GeckoApp) getActivity()).getProfile().getDir();
+ final String expectedName = name != null ? name : GeckoProfile.CUSTOM_PROFILE;
+
+ final GeckoProfile profile;
+ if (useFile) {
+ profile = GeckoProfile.get(getActivity(), name, file);
+ } else {
+ profile = GeckoProfile.get(getActivity(), name, path);
+ }
+
+ if (name != null || f != null) {
+ // GeckoProfile will create a directory and add an ini section if f is null
+ // here. Therefore, when f is null, shouldHaveFound is false for the
+ // verifyProfile call, and inProfileIni is true for the removeProfile call.
+ verifyProfile(profile, expectedName, f, f != null);
+ removeProfile(profile, f == null);
+ if (name == null) {
+ // A side effect of calling GeckoProfile.get with null name is it changes
+ // the test profile's directory to the new directory. Restore it back.
+ GeckoProfile.get(getActivity(), null, testProfileDir);
+ mAsserter.is(GeckoProfile.get(getActivity()).getDir(), testProfileDir,
+ "Test profile should be restored");
+ }
+ } else {
+ // Passing in null for a profile name and path, should get you the default
+ verifyProfile(profile, expectedName, testProfileDir, true);
+ }
+ }
+
+ private void checkProfileCreationDeletion() {
+ // Test
+ checkDefaultGetter();
+
+ int index = 0;
+ checkNamedGetter(TEST_PROFILE_NAME + (index++)); // 0
+ checkNamedGetter(null);
+
+ // name and path
+ checkNameAndPathGetter(TEST_PROFILE_NAME + (index++), true);
+ checkNameAndPathGetter(TEST_PROFILE_NAME + (index++), false);
+ checkNameAndPathGetter(null, false);
+ // null name and path
+ checkNameAndPathGetter(null, TEST_PROFILE_NAME + (index++) + "_FORCED_DIR", true);
+ checkNameAndPathGetter(null, TEST_PROFILE_NAME + (index++) + "_FORCED_DIR", false);
+ // name and null path
+ checkNameAndPathGetter(TEST_PROFILE_NAME + (index++), null, false);
+ checkNameAndPathGetter(TEST_PROFILE_NAME + (index++), "", false);
+ // null name and null path
+ checkNameAndPathGetter(null, null, false);
+ checkNameAndPathGetter(null, "", false);
+
+ // name and path
+ checkNameAndFileGetter(TEST_PROFILE_NAME + (index++), true);
+ checkNameAndFileGetter(TEST_PROFILE_NAME + (index++), false);
+ checkNameAndFileGetter(null, false);
+ // null name and path
+ checkNameAndFileGetter(null, new File(mozDir, TEST_PROFILE_NAME + (index++) + "_FORCED_DIR"), true);
+ checkNameAndFileGetter(null, new File(mozDir, TEST_PROFILE_NAME + (index++) + "_FORCED_DIR"), false);
+ // name and null path
+ checkNameAndFileGetter(TEST_PROFILE_NAME + (index++), null, false);
+ // null name and null path
+ checkNameAndFileGetter(null, null, false);
+ }
+
+ // Tests of Guest profile methods
+ private void checkGuestProfile() {
+ final File testProfileDir = ((GeckoApp) getActivity()).getProfile().getDir();
+
+ mAsserter.info("Test getting a guest profile", "");
+ GeckoProfile profile = GeckoProfile.getGuestProfile(getActivity());
+ verifyProfile(profile, GeckoProfile.CUSTOM_PROFILE, getActivity().getFileStreamPath("guest"), true);
+ mAsserter.ok(profile.inGuestMode(), "Profile is in guest mode", profile.getName());
+
+ final File dir = profile.getDir();
+ mAsserter.info("Test deleting a guest profile", "");
+ mAsserter.ok(GeckoProfile.removeProfile(getActivity(), profile), "Cleaned up unlocked guest profile", profile.getName());
+ mAsserter.ok(!dir.exists(), "Guest dir was deleted", dir.toString());
+
+ // Restore test profile directory, which was changed in the last GeckoProfile.get call.
+ GeckoProfile.get(getActivity(), null, testProfileDir);
+ mAsserter.is(GeckoProfile.get(getActivity()).getDir(), testProfileDir,
+ "Test profile should be restored");
+ }
+
+ // Runs generic tests on a profile to make sure it looks correct
+ private void verifyProfile(GeckoProfile profile, String name, File requestedDir, boolean shouldHaveFound) {
+ mAsserter.is(profile.getName(), name, "Profile name is correct");
+
+ File dir = null;
+ if (!shouldHaveFound) {
+ mAsserter.is(findDir(name), null, "Dir with name doesn't exist yet");
+
+ dir = profile.getDir();
+ mAsserter.isnot(requestedDir, dir, "Profile should not have used expectedDir");
+
+ // The used dir should be based on the name passed in.
+ requestedDir = findDir(name);
+ } else {
+ dir = profile.getDir();
+ }
+
+ mAsserter.is(dir, requestedDir, "Profile dir is correct");
+ mAsserter.ok(dir.exists(), "Profile dir exists after getting it", dir.toString());
+ }
+
+ // Tries to find a profile in profiles.ini. Makes sure its name and path match what is expected
+ private void findInProfilesIni(final String name, final File dir, final boolean shouldFind) {
+ final File mozDir;
+ try {
+ mozDir = GeckoProfileDirectories.getMozillaDirectory(getActivity());
+ } catch(Exception ex) {
+ mAsserter.ok(false, "Couldn't get moz dir", ex.toString());
+ return;
+ }
+
+ final INIParser parser = GeckoProfileDirectories.getProfilesINI(mozDir);
+ final Hashtable<String, INISection> sections = parser.getSections();
+
+ boolean found = false;
+ for (Enumeration<INISection> e = sections.elements(); e.hasMoreElements();) {
+ final INISection section = e.nextElement();
+ String iniName = section.getStringProperty("Name");
+ if (iniName == null || !iniName.equals(name)) {
+ continue;
+ }
+
+ found = true;
+
+ String iniPath = section.getStringProperty("Path");
+ mAsserter.is(name, iniName, "Section with name found");
+ mAsserter.is(dir.getName(), iniPath, "Section has correct path");
+ }
+
+ mAsserter.is(found, shouldFind, "Found profile where expected");
+ }
+
+ // Tries to remove a profile from Gecko profile. Verifies that it's removed from profiles.ini and its directory is deleted.
+ // TODO: Reconsider profile removal. Firefox would not normally remove a
+ // profile. Outstanding tasks may still try to access files in the profile.
+ private void removeProfile(GeckoProfile profile, boolean inProfilesIni) {
+ final String name = profile.getName();
+ final File dir = profile.getDir();
+ findInProfilesIni(name, dir, inProfilesIni);
+ mAsserter.ok(dir.exists(), "Profile dir exists before removing", dir.toString());
+ mAsserter.ok(GeckoProfile.removeProfile(getActivity(), profile), "Remove was successful", name);
+ mAsserter.ok(!dir.exists(), "Profile dir was deleted when it was removed", dir.toString());
+ findInProfilesIni(name, dir, false);
+ }
+
+ // Looks for a dir whose name ends with the passed-in string.
+ private File findDir(String name) {
+ final File root;
+ try {
+ root = GeckoProfileDirectories.getMozillaDirectory(getActivity());
+ } catch(Exception ex) {
+ return null;
+ }
+
+ File[] dirs = root.listFiles();
+ for (File dir : dirs) {
+ if (dir.getName().endsWith(name)) {
+ return dir;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ // Clear SharedPreferences.
+ final Context context = getInstrumentation().getContext();
+ GeckoSharedPrefs.forProfile(context).edit().clear().apply();
+
+ super.tearDown();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGeckoRequest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGeckoRequest.java
new file mode 100644
index 0000000000..ac4a9862c9
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGeckoRequest.java
@@ -0,0 +1,121 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import com.robotium.solo.Condition;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.tests.helpers.AssertionHelper;
+import org.mozilla.gecko.tests.helpers.WaitHelper;
+import org.mozilla.gecko.util.GeckoRequest;
+import org.mozilla.gecko.util.NativeJSObject;
+
+/**
+ * Tests sending and receiving Gecko requests using the GeckoRequest API.
+ */
+public class testGeckoRequest extends JavascriptBridgeTest {
+ private static final String TEST_JS = "testGeckoRequest.js";
+ private static final String REQUEST_EVENT = "Robocop:GeckoRequest";
+ private static final String REQUEST_EXCEPTION_EVENT = "Robocop:GeckoRequestException";
+ private static final int MAX_WAIT_MS = 5000;
+
+ public void testGeckoRequest() {
+ blockForReadyAndLoadJS(TEST_JS);
+
+ // Register a listener for this request.
+ getJS().syncCall("add_request_listener", REQUEST_EVENT);
+
+ // Make sure we receive the expected response.
+ checkFooRequest();
+
+ // Try registering a second listener for this request, which should fail.
+ getJS().syncCall("add_second_request_listener", REQUEST_EVENT);
+
+ // Unregister the listener for this request.
+ getJS().syncCall("remove_request_listener", REQUEST_EVENT);
+
+ // Make sure we don't receive a response after removing the listener.
+ checkUnregisteredRequest();
+
+ // Check that we still receive a response for listeners that throw.
+ getJS().syncCall("add_exception_listener", REQUEST_EXCEPTION_EVENT);
+ checkExceptionRequest();
+ getJS().syncCall("remove_request_listener", REQUEST_EXCEPTION_EVENT);
+
+ getJS().syncCall("finish_test");
+ }
+
+ private void checkFooRequest() {
+ final AtomicBoolean responseReceived = new AtomicBoolean(false);
+ final String data = "foo";
+
+ GeckoAppShell.sendRequestToGecko(new GeckoRequest(REQUEST_EVENT, data) {
+ @Override
+ public void onResponse(NativeJSObject nativeJSObject) {
+ // Ensure we receive the expected response from Gecko.
+ final String result = nativeJSObject.getString("result");
+ AssertionHelper.fAssertEquals("Sent and received request data", data + "bar", result);
+ responseReceived.set(true);
+ }
+ });
+
+ WaitHelper.waitFor("Received response for registered listener", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return responseReceived.get();
+ }
+ }, MAX_WAIT_MS);
+ }
+
+ private void checkExceptionRequest() {
+ final AtomicBoolean responseReceived = new AtomicBoolean(false);
+ final AtomicBoolean errorReceived = new AtomicBoolean(false);
+
+ GeckoAppShell.sendRequestToGecko(new GeckoRequest(REQUEST_EXCEPTION_EVENT, null) {
+ @Override
+ public void onResponse(NativeJSObject nativeJSObject) {
+ responseReceived.set(true);
+ }
+
+ @Override
+ public void onError(NativeJSObject error) {
+ errorReceived.set(true);
+ }
+ });
+
+ WaitHelper.waitFor("Received error for listener with exception", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return errorReceived.get();
+ }
+ }, MAX_WAIT_MS);
+
+ AssertionHelper.fAssertTrue("onResponse not called for listener with exception", !responseReceived.get());
+ }
+
+ private void checkUnregisteredRequest() {
+ final AtomicBoolean responseReceived = new AtomicBoolean(false);
+
+ GeckoAppShell.sendRequestToGecko(new GeckoRequest(REQUEST_EVENT, null) {
+ @Override
+ public void onResponse(NativeJSObject nativeJSObject) {
+ responseReceived.set(true);
+ }
+ });
+
+ // This check makes sure that we do *not* receive a response for an unregistered listener,
+ // meaning waitForCondition() should always time out.
+ getSolo().waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return responseReceived.get();
+ }
+ }, MAX_WAIT_MS);
+
+ AssertionHelper.fAssertTrue("Did not receive response for unregistered listener", !responseReceived.get());
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGetUserMedia.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGetUserMedia.java
new file mode 100644
index 0000000000..405ddef7a8
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGetUserMedia.java
@@ -0,0 +1,159 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.AppConstants;
+
+import android.widget.Spinner;
+import android.view.View;
+
+import com.robotium.solo.Condition;
+
+import android.hardware.Camera;
+import android.os.Build;
+
+public class testGetUserMedia extends BaseTest {
+ private static final String LOGTAG = testGetUserMedia.class.getSimpleName();
+
+ private static final String GUM_MESSAGE = "Would you like to share your camera and microphone with";
+ private static final String GUM_ALLOW = "^Share$";
+ private static final String GUM_DENY = "^Don't Share$";
+
+ private static final String GUM_BACK_CAMERA = "Back facing camera";
+ private static final String GUM_SELECT_TAB = "Choose a tab to stream";
+
+ private static final String GUM_PAGE_TITLE = "gUM Test Page";
+ private static final String GUM_PAGE_FAILED = "failed gumtest";
+ private static final String GUM_PAGE_AUDIO = "audio gumtest";
+ private static final String GUM_PAGE_VIDEO = "video gumtest";
+ private static final String GUM_PAGE_AUDIOVIDEO = "audiovideo gumtest";
+
+ public void testGetUserMedia() {
+ // TabShare.js is disabled on release builds.
+ if (AppConstants.RELEASE_OR_BETA) {
+ mAsserter.dumpLog(LOGTAG + " is disabled on release builds: returning");
+ return;
+ }
+
+ // Only try GUM test if the device has a camera (emulation).
+ if (Camera.getNumberOfCameras() <= 0) {
+ return;
+ }
+
+ blockForGeckoReady();
+
+ final String GUM_CAMERA_URL = getAbsoluteUrl("/robocop/robocop_getusermedia2.html");
+ final String GUM_TAB_URL = getAbsoluteUrl("/robocop/robocop_getusermedia.html");
+ // Browser constraint needs HTTPS
+ final String GUM_TAB_HTTPS_URL = GUM_TAB_URL.replace("http://mochi.test:8888", "https://example.com");
+
+ // Tests on Camera page will test camera enumeration code, but
+ // the actual cameras don't seem to work on the emulators, so
+ // the enumeration is all that gets tested.
+
+ // Test GUM notification showing
+ loadUrlAndWait(GUM_CAMERA_URL);
+ waitForText(GUM_MESSAGE);
+ mAsserter.is(mSolo.searchText(GUM_MESSAGE), true, "getUserMedia doorhanger has been displayed");
+ waitForSpinner();
+ // At least one camera detected
+ mAsserter.is(mSolo.searchText(GUM_BACK_CAMERA), true, "getUserMedia found a camera");
+ mSolo.clickOnButton(GUM_DENY);
+ waitForTextDismissed(GUM_MESSAGE);
+ mAsserter.is(mSolo.searchText(GUM_MESSAGE), false, "getUserMedia doorhanger hidden after dismissal");
+ verifyUrlBarTitle(GUM_CAMERA_URL);
+
+ // Cameras don't work on the testing hardware, so stream a tab
+ loadUrlAndWait(GUM_TAB_HTTPS_URL);
+ waitForText(GUM_MESSAGE);
+ mAsserter.is(mSolo.searchText(GUM_MESSAGE), true, "getUserMedia doorhanger has been displayed");
+ waitForSpinner();
+ mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), true, "Video source selection available");
+ mAsserter.is(mSolo.searchText("MICROPHONE TO USE"), true, "Microphone selection available");
+ mAsserter.is(mSolo.searchText("Microphone 1"), true, "Microphone 1 available");
+ mSolo.clickOnText("Microphone 1");
+ waitForText("No Audio");
+ mAsserter.is(mSolo.searchText("No Audio"), true, "No 'No Audio' selection available");
+ mSolo.clickOnText("No Audio");
+ waitForTextDismissed("Microphone 1");
+ mAsserter.is(mSolo.searchText("Microphone 1"), false, "Audio selection hidden after dismissal");
+ mAsserter.is(mSolo.searchText(GUM_ALLOW), true, "Share button available after selection");
+ mSolo.clickOnButton(GUM_ALLOW);
+ waitForTextDismissed(GUM_MESSAGE);
+ mAsserter.is(mSolo.searchText(GUM_MESSAGE), false, "getUserMedia doorhanger hidden after dismissal");
+ waitForText(GUM_SELECT_TAB);
+ mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), true, "Tab selection dialog displayed");
+ mSolo.clickOnText(GUM_PAGE_TITLE);
+ waitForTextDismissed(GUM_SELECT_TAB);
+ mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), false, "Tab selection dialog hidden");
+ verifyUrlBarTitle(GUM_TAB_HTTPS_URL);
+
+ // Android 2.3 testers fail because of audio issues:
+ // E/AudioRecord( 650): Unsupported configuration: sampleRate 44100, format 1, channelCount 1
+ // E/libOpenSLES( 650): android_audioRecorder_realize(0x26d7d8) error creating AudioRecord object
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ return;
+ }
+
+ loadUrlAndWait(GUM_TAB_HTTPS_URL);
+ waitForText(GUM_MESSAGE);
+ mAsserter.is(mSolo.searchText(GUM_MESSAGE), true, "getUserMedia doorhanger has been displayed");
+
+ waitForSpinner();
+ mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), true, "Video source selection available");
+ mSolo.clickOnButton(GUM_ALLOW);
+ waitForTextDismissed(GUM_MESSAGE);
+ waitForText(GUM_SELECT_TAB);
+ mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), true, "Tab selection dialog displayed");
+ mSolo.clickOnText(GUM_PAGE_TITLE);
+ waitForTextDismissed(GUM_SELECT_TAB);
+ mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), false, "Tab selection dialog hidden");
+ verifyUrlBarTitle(GUM_TAB_HTTPS_URL);
+
+ loadUrlAndWait(GUM_TAB_HTTPS_URL);
+ waitForText(GUM_MESSAGE);
+ mAsserter.is(mSolo.searchText(GUM_MESSAGE), true, "getUserMedia doorhanger has been displayed");
+
+ waitForSpinner();
+ mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), true, "Video source selection available");
+ mSolo.clickOnText(GUM_SELECT_TAB);
+ waitForText("No Video");
+ mAsserter.is(mSolo.searchText("No Video"), true, "'No video' source selection available");
+ mSolo.clickOnText("No Video");
+ waitForTextDismissed(GUM_SELECT_TAB);
+ mSolo.clickOnButton(GUM_ALLOW);
+ waitForTextDismissed(GUM_MESSAGE);
+ mAsserter.is(mSolo.searchText(GUM_MESSAGE), false, "getUserMedia doorhanger hidden after dismissal");
+ verifyUrlBarTitle(GUM_TAB_HTTPS_URL);
+ }
+
+ // wait for a Spinner view that is clickable
+ private void waitForSpinner() {
+ waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ for (Spinner view : mSolo.getCurrentViews(Spinner.class)) {
+ if (view.isClickable() &&
+ view.getVisibility() == View.VISIBLE &&
+ view.getWidth() > 0 &&
+ view.getHeight() > 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }, MAX_WAIT_MS);
+ }
+
+ // wait until the specified text is *not* displayed
+ private void waitForTextDismissed(final String text) {
+ waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return !mSolo.searchText(text);
+ }
+ }, MAX_WAIT_MS);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHistory.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHistory.java
new file mode 100644
index 0000000000..1f2fbbd385
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHistory.java
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+
+import org.mozilla.gecko.home.HomePager;
+
+import com.robotium.solo.Condition;
+
+public class testHistory extends AboutHomeTest {
+ private View mFirstChild;
+
+ public void testHistory() {
+ blockForGeckoReady();
+
+ String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ String url2 = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL);
+ String url3 = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_03_URL);
+
+ inputAndLoadUrl(url);
+ verifyUrlBarTitle(url);
+ inputAndLoadUrl(url2);
+ verifyUrlBarTitle(url2);
+ inputAndLoadUrl(url3);
+ verifyUrlBarTitle(url3);
+
+ openAboutHomeTab(AboutHomeTabs.HISTORY);
+
+ final ListView hList = findListViewWithTag(HomePager.LIST_TAG_HISTORY);
+ mAsserter.is(waitForNonEmptyListToLoad(hList), true, "list is properly loaded");
+
+ // Click on the history item and wait for the page to load
+ // wait for the history list to be populated
+ mFirstChild = null;
+ boolean success = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ mFirstChild = hList.getChildAt(1);
+ if (mFirstChild == null) {
+ return false;
+ }
+ if (mFirstChild instanceof android.view.ViewGroup) {
+ ViewGroup group = (ViewGroup)mFirstChild;
+ if (group.getChildCount() < 1) {
+ return false;
+ }
+ for (int i = 0; i < group.getChildCount(); i++) {
+ View grandChild = group.getChildAt(i);
+ if (grandChild instanceof android.widget.TextView) {
+ mAsserter.ok(true, "found TextView:", ((android.widget.TextView)grandChild).getText().toString());
+ }
+ }
+ } else {
+ mAsserter.dumpLog("first child not a ViewGroup: "+mFirstChild);
+ return false;
+ }
+ return true;
+ }
+ }, MAX_WAIT_MS);
+
+ mAsserter.isnot(mFirstChild, null, "Got history item");
+ mSolo.clickOnView(mFirstChild);
+
+ // The first item here (since it was just visited) should be a "Switch to tab" item
+ // i.e. don't expect a DOMContentLoaded event
+ verifyUrlBarTitle(mStringHelper.ROBOCOP_BLANK_PAGE_03_URL);
+ verifyUrl(url3);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHistoryService.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHistoryService.java
new file mode 100644
index 0000000000..4c605f6c3d
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHistoryService.java
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+public class testHistoryService extends JavascriptTest {
+
+ public testHistoryService() {
+ super("testHistoryService.js");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHomeBanner.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHomeBanner.java
new file mode 100644
index 0000000000..be36ae5a09
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHomeBanner.java
@@ -0,0 +1,94 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+
+public class testHomeBanner extends UITest {
+
+ private static final String TEST_URL = "chrome://roboextender/content/robocop_home_banner.html";
+ private static final String TEXT = "The quick brown fox jumps over the lazy dog.";
+
+ public void testHomeBanner() {
+ GeckoHelper.blockForReady();
+
+ // Make sure the banner is not visible to start.
+ mAboutHome.assertVisible()
+ .assertBannerNotVisible();
+
+ // These test methods depend on being run in this order.
+ addBannerTest();
+
+ // Make sure the banner hides when the user starts interacting with the url bar.
+ hideOnToolbarFocusTest();
+
+ // Make sure to test dismissing the banner after everything else, since dismissing
+ // the banner will prevent it from showing up again.
+ dismissBannerTest();
+ }
+
+ /**
+ * Adds a banner message, verifies that it appears when it should, and verifies that
+ * onshown/onclick handlers are called in JS.
+ *
+ * Note: This test does not remove the message after it is done.
+ */
+ private void addBannerTest() {
+ // Load about:home and make sure the onshown handler is called.
+ Actions.EventExpecter eventExpecter = getActions().expectGeckoEvent("TestHomeBanner:MessageShown");
+ addBannerMessage();
+ NavigationHelper.enterAndLoadUrl(mStringHelper.ABOUT_HOME_URL);
+ eventExpecter.blockForEvent();
+
+ // Verify that the banner is visible with the correct text.
+ mAboutHome.assertBannerText(TEXT);
+
+ // Verify that the banner isn't visible after navigating away from about:home.
+ NavigationHelper.enterAndLoadUrl(mStringHelper.ABOUT_FIREFOX_URL);
+ mAboutHome.assertBannerNotVisible();
+ }
+
+
+ private void hideOnToolbarFocusTest() {
+ NavigationHelper.enterAndLoadUrl(mStringHelper.ABOUT_HOME_URL);
+ mAboutHome.assertVisible()
+ .assertBannerVisible();
+
+ mToolbar.enterEditingMode();
+ mAboutHome.assertBannerNotVisible();
+
+ mToolbar.dismissEditingMode();
+ mAboutHome.assertBannerVisible();
+ }
+
+ /**
+ * Adds a banner message, verifies that its ondismiss handler is called in JS,
+ * and verifies that the banner is no longer shown after it is dismissed.
+ *
+ * Note: This test does not remove the message after it is done.
+ */
+ private void dismissBannerTest() {
+ NavigationHelper.enterAndLoadUrl(mStringHelper.ABOUT_HOME_URL);
+ mAboutHome.assertVisible();
+
+ // Test to make sure the ondismiss handler is called when the close button is clicked.
+ final Actions.EventExpecter eventExpecter = getActions().expectGeckoEvent("TestHomeBanner:MessageDismissed");
+ mAboutHome.dismissBanner();
+ eventExpecter.blockForEvent();
+
+ mAboutHome.assertBannerNotVisible();
+ }
+
+ /**
+ * Loads the roboextender page to add a message to the banner.
+ */
+ private void addBannerMessage() {
+ final Actions.EventExpecter eventExpecter = getActions().expectGeckoEvent("TestHomeBanner:MessageAdded");
+ NavigationHelper.enterAndLoadUrl(TEST_URL + "#addMessage");
+ eventExpecter.blockForEvent();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHomeListsProvider.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHomeListsProvider.java
new file mode 100644
index 0000000000..fbe2df82f2
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHomeListsProvider.java
@@ -0,0 +1,118 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class testHomeListsProvider extends ContentProviderTest {
+ // This test does not run, so it just needs to compile. The test was
+ // disabled at the time the real Contract was removed; to leave a skeleton
+ // for a future re-implementor, we include this dummy Contract class.
+ private static class Contract {
+ public static final Uri CONTENT_URI = null;
+ public static final Uri CONTENT_FAKE_URI = null;
+
+ public static final String _ID = null;
+ public static final String PROVIDER_ID = null;
+ public static final String TITLE = null;
+ public static final String URL = null;
+ }
+
+ @SuppressWarnings("unused")
+ private void ensureEmptyDatabase() throws Exception {
+ // Delete all the list entries.
+ mProvider.delete(Contract.CONTENT_URI, null, null);
+
+ final Cursor c = mProvider.query(Contract.CONTENT_URI, null, null, null, null);
+ mAsserter.is(c.getCount(), 0, "All list entries were deleted");
+ c.close();
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ // This test is disabled, so this just needs to compile.
+ super.setUp(null, null, "homelists.db");
+
+ mTests.add(new TestFakeItems());
+
+ // Disabled until database support lands
+ //mTests.add(new TestInsertItem());
+ }
+
+ public void testListsProvider() throws Exception {
+ for (int i = 0; i < mTests.size(); i++) {
+ Runnable test = mTests.get(i);
+
+ setTestName(test.getClass().getSimpleName());
+ // Disabled until database support lands
+ //ensureEmptyDatabase();
+ test.run();
+ }
+ }
+
+ abstract class Test implements Runnable {
+ @Override
+ public void run() {
+ try {
+ test();
+ } catch (Exception e) {
+ mAsserter.is(true, false, "Test " + this.getClass().getName() +
+ " threw exception: " + e);
+ }
+ }
+
+ public abstract void test() throws Exception;
+ }
+
+ class TestFakeItems extends Test {
+ @Override
+ public void test() throws Exception {
+ final long id = 1;
+ final String providerId = "fake-provider";
+ final String title = "Example";
+ final String url = "http://example.com";
+
+ final Cursor c = mProvider.query(Contract.CONTENT_FAKE_URI, null, null, null, null);
+ mAsserter.is(c.moveToFirst(), true, "Fake list item found");
+
+ mAsserter.is(c.getLong(c.getColumnIndex(Contract._ID)), id, "Fake list item has correct ID");
+ mAsserter.is(c.getString(c.getColumnIndex(Contract.PROVIDER_ID)), providerId, "Fake list item has correct provider ID");
+ mAsserter.is(c.getString(c.getColumnIndex(Contract.TITLE)), title, "Fake list item has correct title");
+ mAsserter.is(c.getString(c.getColumnIndex(Contract.URL)), url, "Fake list item has correct URL");
+
+ c.close();
+ }
+ }
+
+ class TestInsertItem extends Test {
+ @Override
+ public void test() throws Exception {
+ final String providerId = "{c77da387-4c80-0c45-9f22-70276c29b3ed}";
+ final String title = "Mozilla";
+ final String url = "https://mozilla.org";
+
+ // Insert a new list item with test values.
+ final ContentValues cv = new ContentValues();
+ cv.put(Contract.PROVIDER_ID, providerId);
+ cv.put(Contract.TITLE, title);
+ cv.put(Contract.URL, url);
+
+ final long id = ContentUris.parseId(mProvider.insert(Contract.CONTENT_URI, cv));
+
+ // Check that the item was inserted correctly.
+ final Cursor c = mProvider.query(Contract.CONTENT_URI, null, Contract._ID + " = ?", new String[] { String.valueOf(id) }, null);
+ mAsserter.is(c.moveToFirst(), true, "Inserted list item found");
+
+ mAsserter.is(c.getString(c.getColumnIndex(Contract.PROVIDER_ID)), providerId, "Inserted list item has correct provider ID");
+ mAsserter.is(c.getString(c.getColumnIndex(Contract.TITLE)), title, "Inserted list item has correct title");
+ mAsserter.is(c.getString(c.getColumnIndex(Contract.URL)), url, "Inserted list item has correct URL");
+
+ c.close();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testICODecoder.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testICODecoder.java
new file mode 100644
index 0000000000..5cbbd1be9a
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testICODecoder.java
@@ -0,0 +1,238 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import android.graphics.Bitmap;
+
+import org.mozilla.gecko.icons.decoders.ICODecoder;
+import org.mozilla.gecko.icons.decoders.IconDirectoryEntry;
+import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNull;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
+
+public class testICODecoder extends UITest {
+
+ private int mGolemNumIconDirEntries;
+
+ public void testICODecoder() throws IOException {
+ testMicrosoftFavicon();
+ testNvidiaFavicon();
+ testGolemFavicon();
+ testMissingHeader();
+ testCorruptIconDirectory();
+ }
+
+ /**
+ * Decode and verify a Microsoft favicon with six different sizes:
+ * 128x128, 72x72, 48x48, 32x32, 24x24, 16x16
+ * Each of the six BMPs supposedly has zero colour depth.
+ */
+ private void testMicrosoftFavicon() throws IOException {
+ byte[] icoBytes = readICO("microsoft_favicon.ico");
+ fAssertEquals("Expecting Microsoft favicon to be 17174 bytes.", 17174, icoBytes.length);
+
+ ICODecoder decoder = new ICODecoder(getInstrumentation().getTargetContext(), icoBytes, 0,
+ icoBytes.length);
+ LoadFaviconResult result = decoder.decode();
+ fAssertNotNull("Expecting Microsoft favicon to not fail decoding.", result);
+
+ int largestBitmap = Integer.MAX_VALUE;
+
+ int[] possibleSizes = {16, 24, 32, 48, 72, 128};
+ for (int i = 0; i < possibleSizes.length; i++) {
+ if (possibleSizes[i] > decoder.getLargestFaviconSize()) {
+ largestBitmap = possibleSizes[i];
+
+ // Verify that all bitmaps but the smallest larger than Favicons.largestFaviconSize
+ // have been discarded.
+ for (int j = i + 1; j < possibleSizes.length; j++) {
+ Bitmap selectedBitmap = result.getBestBitmap(possibleSizes[j]);
+ fAssertNotNull("Expecting a best bitmap to be found for " +
+ possibleSizes[j] + "x" + possibleSizes[j], selectedBitmap);
+
+ fAssertEquals("Expecting best bitmap to have width " + possibleSizes[i],
+ possibleSizes[i], selectedBitmap.getWidth());
+ fAssertEquals("Expecting best bitmap to have height " + possibleSizes[i],
+ possibleSizes[i], selectedBitmap.getHeight());
+
+ // Reset the result's bitmap iterator.
+ result = decoder.decode();
+ }
+
+ break;
+ }
+ }
+
+ int[] expectedSizes = {
+ // If we request a 33x33 we should get a 48x48.
+ 33, 48,
+ // If we request a 24x24 we should get a 24x24.
+ 24, 24,
+ // If we request a 8x8 we should get a 16x16.
+ 8, 16,
+ };
+
+ for (int i = 0; i < expectedSizes.length - 1; i += 2) {
+ if (expectedSizes[i + 1] > largestBitmap) {
+ // This bitmap has been discarded.
+ continue;
+ }
+
+ Bitmap selectedBitmap = result.getBestBitmap(expectedSizes[i]);
+ fAssertNotNull("Expecting a best bitmap to have been found for " +
+ expectedSizes[i] + "x" + expectedSizes[i], selectedBitmap);
+
+ fAssertEquals("Expecting best bitmap to have width " + expectedSizes[i + 1],
+ expectedSizes[i + 1], selectedBitmap.getWidth());
+ fAssertEquals("Expecting best bitmap to have height " + expectedSizes[i + 1],
+ expectedSizes[i + 1], selectedBitmap.getHeight());
+
+ // Reset the result's bitmap iterator.
+ result = decoder.decode();
+ }
+ }
+
+ /**
+ * Decode and verify a NVIDIA favicon with three different colour depths,
+ * and three different sizes for each colour depth. All payloads are BMP.
+ */
+ private void testNvidiaFavicon() throws IOException {
+ byte[] icoBytes = readICO("nvidia_favicon.ico");
+ fAssertEquals("Expecting NVIDIA favicon to be 25214 bytes.", 25214, icoBytes.length);
+
+ ICODecoder decoder = new ICODecoder(getInstrumentation().getTargetContext(), icoBytes, 0,
+ icoBytes.length);
+ fAssertNotNull("Expecting NVIDIA favicon to not fail decoding.", decoder.decode());
+
+ // Verify the best entry is correctly chosen for each width.
+ // We expect 32 bpp in all cases even if 32 bpp exceeds IconDirectoryEntry.maxBPP.
+ // This is okay because IconDirectoryEntry.maxBPP is a "desired bpp" not the absolute max.
+ // This was chosen because we think it gives better results to select a higher bpp and let
+ // Android downscale the bpp, rather than showing a bitmap of potentially significantly
+ // lower color depth.
+ IconDirectoryEntry[] expectedEntries = {
+ new IconDirectoryEntry(16, 16, 0, 32, 1128, 24086, false),
+ new IconDirectoryEntry(32, 32, 0, 32, 4264, 19822, false),
+ new IconDirectoryEntry(48, 48, 0, 32, 9640, 10182, false)
+ };
+
+ IconDirectoryEntry[] directory = decoder.getIconDirectory();
+ fAssertTrue("NVIDIA icon directory must contain at least one entry.", directory.length > 0);
+ for (int i = 0; i < directory.length; i++) {
+ if (expectedEntries[i].getWidth() > directory[directory.length - 1].getWidth()) {
+ // This test-case has been discarded due to being over-sized. Next.
+ // All subsequent cases will be too.
+ fAssertTrue("At least one test-case should not have been discarded.", i > 0);
+ break;
+ }
+
+ // Verify the actual Icon Directory entry was as expected.
+ fAssertEquals(directory[i] + " is expected to be equal to " + expectedEntries[i],
+ 0, directory[i].compareTo(expectedEntries[i]));
+ }
+ }
+
+ /**
+ * Decode and verify a Golem.de favicon with five bitmaps: 256x256, 48x48, 32x32, 24x24, 16x16
+ * Only the 256x256 is a PNG payload. All others are BMP.
+ */
+ private void testGolemFavicon() throws IOException {
+ byte[] icoBytes = readICO("golem_favicon.ico");
+ fAssertEquals("Expecting Golem favicon to be 40648 bytes.", 40648, icoBytes.length);
+
+ ICODecoder decoder = new ICODecoder(getInstrumentation().getTargetContext(), icoBytes, 0,
+ icoBytes.length);
+ fAssertNotNull("Expecting Golem favicon to not fail decoding.", decoder.decode());
+
+ // Verify the five entries were correctly identified.
+ IconDirectoryEntry[] expectedEntries = {
+ new IconDirectoryEntry(16, 16, 0, 32, 1128, 39250, false),
+ new IconDirectoryEntry(24, 24, 0, 32, 2488, 37032, false),
+ new IconDirectoryEntry(32, 32, 0, 32, 4392, 32640, false),
+ new IconDirectoryEntry(48, 48, 0, 32, 9832, 22808, false),
+ new IconDirectoryEntry(256, 256, 0, 32, 22722, 86, true)
+ };
+
+ IconDirectoryEntry[] directory = decoder.getIconDirectory();
+ fAssertTrue("Golem icon directory must contain at least one entry.", directory.length > 0);
+ for (int i = 0; i < directory.length; i++) {
+ if (expectedEntries[i].getWidth() > directory[directory.length - 1].getWidth()) {
+ // This test-case has been discarded due to being over-sized.
+ // All subsequent cases will be too.
+ fAssertTrue("At least one test-case should not have been discarded.", i > 0);
+ break;
+ }
+
+ // Verify the actual Icon Directory entry was as expected.
+ fAssertEquals(directory[i] + " is expected to be equal to " + expectedEntries[i],
+ 0, directory[i].compareTo(expectedEntries[i]));
+ }
+
+ // How many icon directory entries in the non-maimed favicon?
+ mGolemNumIconDirEntries = directory.length;
+ }
+
+ /**
+ * Verify that deleting the header will make decoding fail.
+ */
+ private void testMissingHeader() throws IOException {
+ byte[] icoBytes = readICO("microsoft_favicon.ico");
+ fAssertEquals("Expecting Microsoft favicon to be 17174 bytes.", 17174, icoBytes.length);
+
+ int offsetNoHeader = ICODecoder.ICO_HEADER_LENGTH_BYTES;
+ int lenNoHeader = icoBytes.length - ICODecoder.ICO_HEADER_LENGTH_BYTES;
+ ICODecoder decoder = new ICODecoder(getInstrumentation().getTargetContext(), icoBytes,
+ offsetNoHeader, lenNoHeader);
+ fAssertNull("Expecting Microsoft favicon to fail decoding.", decoder.decode());
+ }
+
+ /**
+ * Verify that decoding does not fail if the number of icon directory entries is smaller than
+ * the number given in the header.
+ */
+ private void testCorruptIconDirectory() throws IOException {
+ byte[] icoBytes = readICO("golem_favicon.ico");
+ fAssertEquals("Expecting Golem favicon to be 40648 bytes.", 40648, icoBytes.length);
+
+ byte[] icoMaimed = new byte[icoBytes.length - ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES];
+ // Copy the header and first four icon directory entries into icoMaimed.
+ System.arraycopy(icoBytes, 0, icoMaimed, 0,
+ ICODecoder.ICO_HEADER_LENGTH_BYTES + 4 * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES);
+ // Skip the last icon directory entry.
+ System.arraycopy(icoBytes,
+ ICODecoder.ICO_HEADER_LENGTH_BYTES + 5 * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES,
+ icoMaimed,
+ ICODecoder.ICO_HEADER_LENGTH_BYTES + 4 * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES,
+ icoBytes.length - ICODecoder.ICO_HEADER_LENGTH_BYTES - 5 * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES);
+
+ ICODecoder decoder = new ICODecoder(getInstrumentation().getTargetContext(), icoMaimed, 0,
+ icoMaimed.length);
+ fAssertNotNull("Expecting Golem favicon to not fail decoding.", decoder.decode());
+ fAssertEquals("Expecting Golem favicon icon directory to contain one less bitmap.",
+ mGolemNumIconDirEntries - 1, decoder.getIconDirectory().length);
+ }
+
+ private byte[] readICO(String fileName) throws IOException {
+ String filePath = "ico_decoder_favicons" + File.separator + fileName;
+ InputStream icoStream = getInstrumentation().getContext().getAssets().open(filePath);
+ ByteArrayOutputStream byteStream = new ByteArrayOutputStream(icoStream.available());
+
+ int readByte;
+ while ((readByte = icoStream.read()) != -1) {
+ byteStream.write(readByte);
+ }
+
+ return byteStream.toByteArray();
+ }
+}
+
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputConnection.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputConnection.java
new file mode 100644
index 0000000000..f9a6bcef78
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputConnection.java
@@ -0,0 +1,349 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals;
+import static org.mozilla.gecko.tests.helpers.WaitHelper.waitFor;
+
+import org.mozilla.gecko.tests.components.GeckoViewComponent.InputConnectionTest;
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+
+import com.robotium.solo.Condition;
+
+import android.view.KeyEvent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+/**
+ * Tests the proper operation of GeckoInputConnection
+ */
+public class testInputConnection extends JavascriptBridgeTest {
+
+ private static final String INITIAL_TEXT = "foo";
+
+ public void testInputConnection() throws InterruptedException {
+ GeckoHelper.blockForReady();
+
+ final String url = mStringHelper.ROBOCOP_INPUT_URL;
+ NavigationHelper.enterAndLoadUrl(url);
+ mToolbar.assertTitle(url);
+
+ // First run tests inside the normal input field.
+ getJS().syncCall("focus_input", INITIAL_TEXT);
+ mGeckoView.mTextInput
+ .waitForInputConnection()
+ .testInputConnection(new BasicInputConnectionTest());
+
+ // Then switch focus to the text area and rerun tests.
+ getJS().syncCall("focus_text_area", INITIAL_TEXT);
+ mGeckoView.mTextInput
+ .waitForInputConnection()
+ .testInputConnection(new BasicInputConnectionTest());
+
+ // Then switch focus to the content editable and rerun tests.
+ getJS().syncCall("focus_content_editable", INITIAL_TEXT);
+ mGeckoView.mTextInput
+ .waitForInputConnection()
+ .testInputConnection(new BasicInputConnectionTest());
+
+ // Then switch focus to the design mode document and rerun tests.
+ getJS().syncCall("focus_design_mode", INITIAL_TEXT);
+ mGeckoView.mTextInput
+ .waitForInputConnection()
+ .testInputConnection(new BasicInputConnectionTest());
+
+ // Then switch focus to the resetting input field, and run tests there.
+ getJS().syncCall("focus_resetting_input", "");
+ mGeckoView.mTextInput
+ .waitForInputConnection()
+ .testInputConnection(new ResettingInputConnectionTest());
+
+ // Then switch focus to the hiding input field, and run tests there.
+ getJS().syncCall("focus_hiding_input", "");
+ mGeckoView.mTextInput
+ .waitForInputConnection()
+ .testInputConnection(new HidingInputConnectionTest());
+
+ getJS().syncCall("finish_test");
+ }
+
+ private class BasicInputConnectionTest extends InputConnectionTest {
+ @Override
+ public void test(final InputConnection ic, EditorInfo info) {
+ waitFor("focus change", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return INITIAL_TEXT.equals(getText(ic));
+ }
+ });
+
+ // Test setSelection
+ ic.setSelection(0, 3);
+ assertSelection("Can set selection to range", ic, 0, 3);
+ ic.setSelection(-3, 6);
+ // Test both forms of assert
+ assertTextAndSelection("Can handle invalid range", ic, INITIAL_TEXT, 0, 3);
+ ic.setSelection(3, 3);
+ assertSelectionAt("Can collapse selection", ic, 3);
+ ic.setSelection(4, 4);
+ assertTextAndSelectionAt("Can handle invalid cursor", ic, INITIAL_TEXT, 3);
+
+ // Test commitText
+ ic.commitText("", 10); // Selection past end of new text
+ assertTextAndSelectionAt("Can commit empty text", ic, "foo", 3);
+ ic.commitText("bar", 1); // Selection at end of new text
+ assertTextAndSelectionAt("Can commit text (select after)", ic, "foobar", 6);
+ ic.commitText("foo", -1); // Selection at start of new text
+ assertTextAndSelectionAt("Can commit text (select before)", ic, "foobarfoo", 5);
+
+ // Test deleteSurroundingText
+ ic.deleteSurroundingText(1, 0);
+ assertTextAndSelectionAt("Can delete text before", ic, "foobrfoo", 4);
+ ic.deleteSurroundingText(1, 1);
+ assertTextAndSelectionAt("Can delete text before/after", ic, "foofoo", 3);
+ ic.deleteSurroundingText(0, 10);
+ assertTextAndSelectionAt("Can delete text after", ic, "foo", 3);
+ ic.deleteSurroundingText(0, 0);
+ assertTextAndSelectionAt("Can delete empty text", ic, "foo", 3);
+
+ // Test setComposingText
+ ic.setComposingText("foo", 1);
+ assertTextAndSelectionAt("Can start composition", ic, "foofoo", 6);
+ ic.setComposingText("", 1);
+ assertTextAndSelectionAt("Can set empty composition", ic, "foo", 3);
+ ic.setComposingText("bar", 1);
+ assertTextAndSelectionAt("Can update composition", ic, "foobar", 6);
+
+ // Test finishComposingText
+ ic.finishComposingText();
+ assertTextAndSelectionAt("Can finish composition", ic, "foobar", 6);
+
+ // Test setComposingRegion
+ ic.setComposingRegion(0, 3);
+ assertTextAndSelectionAt("Can set composing region", ic, "foobar", 6);
+
+ ic.setComposingText("far", 1);
+ assertTextAndSelectionAt("Can set composing region text", ic, "farbar", 3);
+
+ ic.setComposingRegion(1, 4);
+ assertTextAndSelectionAt("Can set existing composing region", ic, "farbar", 3);
+
+ ic.setComposingText("rab", 3);
+ assertTextAndSelectionAt("Can set new composing region text", ic, "frabar", 6);
+
+ // Test getTextBeforeCursor
+ fAssertEquals("Can retrieve text before cursor", "bar", ic.getTextBeforeCursor(3, 0));
+
+ // Test getTextAfterCursor
+ fAssertEquals("Can retrieve text after cursor", "", ic.getTextAfterCursor(3, 0));
+
+ ic.finishComposingText();
+ assertTextAndSelectionAt("Can finish composition", ic, "frabar", 6);
+
+ // Test sendKeyEvent
+ final KeyEvent shiftKey = new KeyEvent(KeyEvent.ACTION_DOWN,
+ KeyEvent.KEYCODE_SHIFT_LEFT);
+ final KeyEvent leftKey = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT);
+ final KeyEvent tKey = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_T);
+
+ ic.sendKeyEvent(shiftKey);
+ ic.sendKeyEvent(leftKey);
+ ic.sendKeyEvent(KeyEvent.changeAction(leftKey, KeyEvent.ACTION_UP));
+ ic.sendKeyEvent(KeyEvent.changeAction(shiftKey, KeyEvent.ACTION_UP));
+ assertTextAndSelection("Can select using key event", ic, "frabar", 6, 5);
+
+ ic.sendKeyEvent(tKey);
+ ic.sendKeyEvent(KeyEvent.changeAction(tKey, KeyEvent.ACTION_UP));
+ assertTextAndSelectionAt("Can type using event", ic, "frabat", 6);
+
+ ic.deleteSurroundingText(6, 0);
+ assertTextAndSelectionAt("Can clear text", ic, "", 0);
+
+ // Bug 1133802, duplication when setting the same composing text more than once.
+ ic.setComposingText("foo", 1);
+ assertTextAndSelectionAt("Can set the composing text", ic, "foo", 3);
+ ic.setComposingText("foo", 1);
+ assertTextAndSelectionAt("Can set the same composing text", ic, "foo", 3);
+ ic.setComposingText("bar", 1);
+ assertTextAndSelectionAt("Can set different composing text", ic, "bar", 3);
+ ic.setComposingText("bar", 1);
+ assertTextAndSelectionAt("Can set the same composing text", ic, "bar", 3);
+ ic.setComposingText("bar", 1);
+ assertTextAndSelectionAt("Can set the same composing text again", ic, "bar", 3);
+ ic.finishComposingText();
+ assertTextAndSelectionAt("Can finish composing text", ic, "bar", 3);
+
+ ic.deleteSurroundingText(3, 0);
+ assertTextAndSelectionAt("Can clear text", ic, "", 0);
+
+ // Bug 1209465, cannot enter ideographic space character by itself (U+3000).
+ ic.commitText("\u3000", 1);
+ assertTextAndSelectionAt("Can commit ideographic space", ic, "\u3000", 1);
+
+ ic.deleteSurroundingText(1, 0);
+ assertTextAndSelectionAt("Can clear text", ic, "", 0);
+
+ // Bug 1051556, exception due to committing text changes during flushing.
+ ic.setComposingText("bad", 1);
+ assertTextAndSelectionAt("Can set the composing text", ic, "bad", 3);
+ getJS().asyncCall("test_reflush_changes");
+ // Wait for text change notifications to come in.
+ processGeckoEvents();
+ assertTextAndSelectionAt("Can re-flush text changes", ic, "good", 4);
+ ic.setComposingText("done", 1);
+ assertTextAndSelectionAt("Can update composition after re-flushing", ic, "gooddone", 8);
+ ic.finishComposingText();
+ assertTextAndSelectionAt("Can finish composing text", ic, "gooddone", 8);
+
+ ic.deleteSurroundingText(8, 0);
+ assertTextAndSelectionAt("Can clear text", ic, "", 0);
+
+ // Bug 1241558 - wrong selection due to ignoring selection notification.
+ ic.setComposingText("foobar", 1);
+ assertTextAndSelectionAt("Can set the composing text", ic, "foobar", 6);
+ getJS().asyncCall("test_set_selection");
+ // Wait for text change notifications to come in.
+ processGeckoEvents();
+ assertTextAndSelectionAt("Can select after committing", ic, "foobar", 3);
+ ic.setComposingText("barfoo", 1);
+ assertTextAndSelectionAt("Can compose after selecting", ic, "barfoo", 6);
+ ic.beginBatchEdit();
+ ic.setSelection(3, 3);
+ ic.finishComposingText();
+ ic.deleteSurroundingText(1, 1);
+ ic.endBatchEdit();
+ assertTextAndSelectionAt("Can delete after committing", ic, "baoo", 2);
+
+ ic.deleteSurroundingText(2, 2);
+ assertTextAndSelectionAt("Can clear text", ic, "", 0);
+
+ // Bug 1275371 - shift+backspace should not forward delete on Android.
+ final KeyEvent delKey = new KeyEvent(KeyEvent.ACTION_DOWN,
+ KeyEvent.KEYCODE_DEL);
+
+ ic.beginBatchEdit();
+ ic.commitText("foo", 1);
+ ic.setSelection(1, 1);
+ ic.endBatchEdit();
+ assertTextAndSelectionAt("Can commit text", ic, "foo", 1);
+
+ ic.sendKeyEvent(shiftKey);
+ ic.sendKeyEvent(delKey);
+ ic.sendKeyEvent(KeyEvent.changeAction(delKey, KeyEvent.ACTION_UP));
+ assertTextAndSelectionAt("Can backspace with shift+backspace", ic, "oo", 0);
+
+ ic.sendKeyEvent(delKey);
+ ic.sendKeyEvent(KeyEvent.changeAction(delKey, KeyEvent.ACTION_UP));
+ ic.sendKeyEvent(KeyEvent.changeAction(shiftKey, KeyEvent.ACTION_UP));
+ assertTextAndSelectionAt("Cannot forward delete with shift+backspace", ic, "oo", 0);
+
+ ic.deleteSurroundingText(0, 2);
+ assertTextAndSelectionAt("Can clear text", ic, "", 0);
+
+ // Bug 1123514 - exception due to incorrect text replacement offsets.
+ getJS().syncCall("test_bug1123514");
+ // Gecko will change text to 'abc' when we input 'b', potentially causing
+ // incorrect calculation of text replacement offsets.
+ ic.commitText("b", 1);
+ // We don't assert text here because this test only works for input/textarea,
+ // so an assertion would fail for contentEditable/designMode.
+ processGeckoEvents();
+ processInputConnectionEvents();
+
+ ic.deleteSurroundingText(2, 1);
+ assertTextAndSelectionAt("Can clear text", ic, "", 0);
+
+ // Make sure we don't leave behind stale events for the following test.
+ processGeckoEvents();
+ processInputConnectionEvents();
+ }
+ }
+
+ /**
+ * ResettingInputConnectionTest performs tests on the resetting input in
+ * robocop_input.html. Any test that uses the normal input should be put in
+ * BasicInputConnectionTest.
+ */
+ private class ResettingInputConnectionTest extends InputConnectionTest {
+ @Override
+ public void test(final InputConnection ic, EditorInfo info) {
+ waitFor("focus change", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return "".equals(getText(ic));
+ }
+ });
+
+ // Bug 1199658, duplication when page has JS that resets input field value.
+
+ ic.commitText("foo", 1);
+ assertTextAndSelectionAt("Can commit text (resetting)", ic, "foo", 3);
+
+ ic.setComposingRegion(0, 3);
+ // The bug appears after composition update events are processed. We only
+ // issue these events after some back-and-forth calls between the Gecko thread
+ // and the input connection thread. Therefore, to ensure these events are
+ // issued and to ensure the bug appears, we have to process all Gecko events,
+ // then all input connection events, and finally all Gecko events again.
+ processGeckoEvents();
+ processInputConnectionEvents();
+ processGeckoEvents();
+ assertTextAndSelectionAt("Can set composing region (resetting)", ic, "foo", 3);
+
+ ic.setComposingText("foobar", 1);
+ processGeckoEvents();
+ processInputConnectionEvents();
+ processGeckoEvents();
+ assertTextAndSelectionAt("Can change composing text (resetting)", ic, "foobar", 6);
+
+ ic.setComposingText("baz", 1);
+ processGeckoEvents();
+ processInputConnectionEvents();
+ processGeckoEvents();
+ assertTextAndSelectionAt("Can reset composing text (resetting)", ic, "baz", 3);
+
+ ic.finishComposingText();
+ assertTextAndSelectionAt("Can finish composing text (resetting)", ic, "baz", 3);
+
+ ic.deleteSurroundingText(3, 0);
+ assertTextAndSelectionAt("Can clear text", ic, "", 0);
+
+ // Make sure we don't leave behind stale events for the following test.
+ processGeckoEvents();
+ processInputConnectionEvents();
+ }
+ }
+
+ /**
+ * HidingInputConnectionTest performs tests on the hiding input in
+ * robocop_input.html. Any test that uses the normal input should be put in
+ * BasicInputConnectionTest.
+ */
+ private class HidingInputConnectionTest extends InputConnectionTest {
+ @Override
+ public void test(final InputConnection ic, EditorInfo info) {
+ waitFor("focus change", new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return "".equals(getText(ic));
+ }
+ });
+
+ // Bug 1254629, crash when hiding input during input.
+ ic.commitText("foo", 1);
+ assertTextAndSelectionAt("Can commit text (hiding)", ic, "foo", 3);
+
+ ic.commitText("!", 1);
+ // The '!' key causes the input to hide in robocop_input.html,
+ // and there won't be a text/selection update as a result.
+ assertTextAndSelectionAt("Can handle hiding input", ic, "foo", 3);
+
+ // Make sure we don't leave behind stale events for the following test.
+ processGeckoEvents();
+ processInputConnectionEvents();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputUrlBar.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputUrlBar.java
new file mode 100644
index 0000000000..c12ccef982
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputUrlBar.java
@@ -0,0 +1,136 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.Element;
+import org.mozilla.gecko.R;
+
+import android.widget.EditText;
+
+/**
+ * Basic test of text editing within the editing mode.
+ * - Enter some text, move the cursor around, and modifying some text.
+ * - Check that all edit entry text is selected after switching about:home tabs.
+ */
+public final class testInputUrlBar extends BaseTest {
+ private Element mUrlBarEditElement;
+ private EditText mUrlBarEditView;
+
+ public void testInputUrlBar() {
+ blockForGeckoReady();
+
+ startEditingMode();
+ assertUrlBarText("");
+
+ // Avoid any auto domain completion by using a prefix that matches
+ // nothing, including about: pages
+ mActions.sendKeys("zy");
+ assertUrlBarText("zy");
+
+ mActions.sendKeys("cd");
+ assertUrlBarText("zycd");
+
+ mActions.sendSpecialKey(Actions.SpecialKey.LEFT);
+ mActions.sendSpecialKey(Actions.SpecialKey.LEFT);
+
+ // Inserting "" should not do anything.
+ mActions.sendKeys("");
+ assertUrlBarText("zycd");
+
+ mActions.sendKeys("ef");
+ assertUrlBarText("zyefcd");
+
+ mActions.sendSpecialKey(Actions.SpecialKey.RIGHT);
+ mActions.sendKeys("gh");
+ assertUrlBarText("zyefcghd");
+
+ final EditText editText = mUrlBarEditView;
+ runOnUiThreadSync(new Runnable() {
+ @Override
+ public void run() {
+ // Select "ef"
+ editText.setSelection(2);
+ }
+ });
+ mActions.sendKeys("op");
+ assertUrlBarText("zyopefcghd");
+
+ runOnUiThreadSync(new Runnable() {
+ @Override
+ public void run() {
+ // Select "cg"
+ editText.setSelection(6, 8);
+ }
+ });
+ mActions.sendKeys("qr");
+ assertUrlBarText("zyopefqrhd");
+
+ runOnUiThreadSync(new Runnable() {
+ @Override
+ public void run() {
+ // Select "op"
+ editText.setSelection(4,2);
+ }
+ });
+ mActions.sendKeys("st");
+ assertUrlBarText("zystefqrhd");
+
+ runOnUiThreadSync(new Runnable() {
+ @Override
+ public void run() {
+ editText.selectAll();
+ }
+ });
+ mActions.sendKeys("uv");
+ assertUrlBarText("uv");
+
+ // Dismiss the VKB
+ mSolo.goBack();
+
+ // Dismiss editing mode
+ mSolo.goBack();
+
+ waitForText(mStringHelper.TITLE_PLACE_HOLDER);
+
+ // URL bar should have forgotten about "uv" text.
+ startEditingMode();
+ assertUrlBarText("");
+
+ int width = mDriver.getGeckoWidth() / 2;
+ int y = mDriver.getGeckoHeight() / 2;
+
+ // Slide to the right, force URL bar entry to lose input focus
+ mActions.drag(width, 0, y, y);
+
+ // Select text and replace the content
+ mSolo.clickOnView(mUrlBarEditView);
+ mActions.sendKeys("yz");
+
+ String yz = getUrlBarText();
+ mAsserter.ok("yz".equals(yz), "Is the URL bar text \"yz\"?", yz);
+ }
+
+ private void startEditingMode() {
+ focusUrlBar();
+
+ mUrlBarEditElement = mDriver.findElement(getActivity(), R.id.url_edit_text);
+ final int id = mUrlBarEditElement.getId();
+ mUrlBarEditView = (EditText) getActivity().findViewById(id);
+ }
+
+ private String getUrlBarText() {
+ final String elementText = mUrlBarEditElement.getText();
+ final String editText = mUrlBarEditView.getText().toString();
+ mAsserter.is(editText, elementText, "Does URL bar editText == elementText?");
+
+ return editText;
+ }
+
+ private void assertUrlBarText(String expectedText) {
+ String actualText = getUrlBarText();
+ mAsserter.is(actualText, expectedText, "Does URL bar actualText == expectedText?");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testJarReader.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testJarReader.java
new file mode 100644
index 0000000000..9310599d36
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testJarReader.java
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import java.io.InputStream;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.util.GeckoJarReader;
+
+import android.content.Context;
+
+/**
+ * A basic jar reader test. Tests reading a png from fennec's apk, as well
+ * as loading some invalid jar urls.
+ */
+public class testJarReader extends BaseTest {
+ public void testJarReader() {
+ // Invalid characters are escaped.
+ final String s = GeckoJarReader.computeJarURI("some[1].apk", "something/else");
+ mAsserter.ok(!s.contains("["), "Illegal characters are escaped away.", null);
+ mAsserter.ok(!s.toLowerCase().contains("%2f"), "Path characters aren't escaped.", null);
+
+ final Context context = getInstrumentation().getTargetContext().getApplicationContext();
+ String appPath = getActivity().getApplication().getPackageResourcePath();
+ mAsserter.isnot(appPath, null, "getPackageResourcePath is non-null");
+
+ // Test reading a file from a jar url that looks correct.
+ String url = "jar:file://" + appPath + "!/" + AppConstants.OMNIJAR_NAME;
+ InputStream stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
+ mAsserter.isnot(stream, null, "JarReader returned non-null for valid file in valid jar");
+
+ // Test looking for an non-existent file in a jar.
+ url = "jar:file://" + appPath + "!/" + AppConstants.OMNIJAR_NAME;
+ stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/nonexistent_file.png");
+ mAsserter.is(stream, null, "JarReader returned null for non-existent file in valid jar");
+
+ // Test looking for a file that doesn't exist in the APK.
+ url = "jar:file://" + appPath + "!/" + "BAD" + AppConstants.OMNIJAR_NAME;
+ stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
+ mAsserter.is(stream, null, "JarReader returned null for valid file in invalid jar file");
+
+ // Test looking for a file that doesn't exist in the APK.
+ // Bug 1174922, prefixed string / length error.
+ url = "jar:file://" + appPath + "!/" + AppConstants.OMNIJAR_NAME + "BAD";
+ stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
+ mAsserter.is(stream, null, "JarReader returned null for valid file in other invalid jar file");
+
+ // Test looking for an jar with an invalid url.
+ url = "jar:file://" + appPath + "!" + "!/" + AppConstants.OMNIJAR_NAME;
+ stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/nonexistent_file.png");
+ mAsserter.is(stream, null, "JarReader returned null for bad jar url");
+
+ // Test looking for a file that doesn't exist on disk.
+ url = "jar:file://" + appPath + "BAD" + "!/" + AppConstants.OMNIJAR_NAME;
+ stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
+ mAsserter.is(stream, null, "JarReader returned null for a non-existent APK");
+
+ // This test completes very quickly. If it completes too soon, the
+ // minidumps directory may not be created before the process is
+ // taken down, causing bug 722166.
+ blockForGeckoReady();
+ }
+
+ private String getData(InputStream stream) {
+ return new java.util.Scanner(stream).useDelimiter("\\A").next();
+ }
+
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testJavascriptBridge.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testJavascriptBridge.java
new file mode 100644
index 0000000000..724b6b4aba
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testJavascriptBridge.java
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.*;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Tests the proper operation of JavascriptBridge and JavaBridge,
+ * which are used by tests for communication between Java and JS.
+ */
+public class testJavascriptBridge extends JavascriptBridgeTest {
+
+ private static final String TEST_JS = "testJavascriptBridge.js";
+
+ private boolean syncCallReceived;
+
+ public void testJavascriptBridge() {
+ blockForReadyAndLoadJS(TEST_JS);
+ getJS().syncCall("check_js_int_arg", 1);
+ }
+
+ public void checkJavaIntArg(final int int2) {
+ // Async call from JS
+ fAssertEquals("Integer argument matches", 2, int2);
+ getJS().syncCall("check_js_double_arg", 3.0D);
+ }
+
+ public void checkJavaDoubleArg(final double double4) {
+ // Async call from JS
+ fAssertEquals("Double argument matches", 4.0, double4);
+ getJS().syncCall("check_js_boolean_arg", false);
+ }
+
+ public void checkJavaBooleanArg(final boolean booltrue) {
+ // Async call from JS
+ fAssertEquals("Boolean argument matches", true, booltrue);
+ getJS().syncCall("check_js_string_arg", "foo");
+ }
+
+ public void checkJavaStringArg(final String stringbar) throws JSONException {
+ // Async call from JS
+ fAssertEquals("String argument matches", "bar", stringbar);
+ final JSONObject obj = new JSONObject();
+ obj.put("caller", "java");
+ getJS().syncCall("check_js_object_arg", (JSONObject) obj);
+ }
+
+ public void checkJavaObjectArg(final JSONObject obj) throws JSONException {
+ // Async call from JS
+ fAssertEquals("Object argument matches", "js", obj.getString("caller"));
+ getJS().syncCall("check_js_sync_call");
+ }
+
+ public void doJSSyncCall() {
+ // Sync call from JS
+ syncCallReceived = true;
+ getJS().asyncCall("respond_to_js_sync_call");
+ }
+
+ public void checkJSSyncCallReceived() {
+ fAssertTrue("Received sync call before end of test", syncCallReceived);
+ // End of test
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLinkContextMenu.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLinkContextMenu.java
new file mode 100644
index 0000000000..556ed0e078
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLinkContextMenu.java
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+public class testLinkContextMenu extends ContentContextMenuTest {
+
+ // Test website strings
+ private static String LINK_PAGE_URL;
+ private static String BLANK_PAGE_URL;
+ private static final String LINK_PAGE_TITLE = "Big Link";
+
+ public void testLinkContextMenu() {
+ final String linkMenuItems [] = mStringHelper.CONTEXT_MENU_ITEMS_IN_NORMAL_TAB;
+
+ blockForGeckoReady();
+
+ LINK_PAGE_URL=getAbsoluteUrl(mStringHelper.ROBOCOP_BIG_LINK_URL);
+ BLANK_PAGE_URL=getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ loadUrlAndWait(LINK_PAGE_URL);
+ waitForText(LINK_PAGE_TITLE);
+
+ verifyContextMenuItems(linkMenuItems); // Verify context menu items are correct
+ openTabFromContextMenu(linkMenuItems[0],2); // Test the "Open in New Tab" option - expecting 2 tabs: the original and the new one
+ openTabFromContextMenu(linkMenuItems[1],2); // Test the "Open in Private Tab" option - expecting only 2 tabs in normal mode
+ verifyCopyOption(linkMenuItems[2], BLANK_PAGE_URL); // Test the "Copy Link" option
+ verifyShareOption(linkMenuItems[3], LINK_PAGE_TITLE); // Test the "Share Link" option
+ verifyBookmarkLinkOption(linkMenuItems[4], BLANK_PAGE_URL); // Test the "Bookmark Link" option
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mDatabaseHelper.deleteBookmark(BLANK_PAGE_URL);
+ super.tearDown();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLoad.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLoad.java
new file mode 100644
index 0000000000..e62bd7899a
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLoad.java
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+/**
+ * A basic page load test.
+ * - loads a page
+ * - verifies it rendered properly
+ * - verifies the displayed url is correct
+ */
+public class testLoad extends PixelTest {
+ public void testLoad() {
+ String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BOXES_URL);
+
+ blockForGeckoReady();
+
+ loadAndVerifyBoxes(url);
+
+ verifyUrl(url);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLoginsProvider.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLoginsProvider.java
new file mode 100644
index 0000000000..10dde28cd4
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLoginsProvider.java
@@ -0,0 +1,387 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+
+import org.mozilla.gecko.db.BrowserContract;
+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.db.LoginsProvider;
+
+import java.util.concurrent.Callable;
+
+import static org.mozilla.gecko.db.BrowserContract.CommonColumns._ID;
+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 testLoginsProvider extends ContentProviderTest {
+
+ private static final String DB_NAME = "browser.db";
+
+ private final TestCase[] TESTS_TO_RUN = {
+ new InsertLoginsTest(),
+ new UpdateLoginsTest(),
+ new DeleteLoginsTest(),
+ new InsertDeletedLoginsTest(),
+ new InsertDeletedLoginsFailureTest(),
+ new DisabledHostsInsertTest(),
+ new DisabledHostsInsertFailureTest(),
+ new InsertLoginsWithDefaultValuesTest(),
+ new InsertLoginsWithDuplicateGuidFailureTest(),
+ new DeleteLoginsByNonExistentGuidTest(),
+ };
+
+ /**
+ * Factory function that makes new LoginsProvider instances.
+ * <p>
+ * We want a fresh provider each test, so this should be invoked in
+ * <code>setUp</code> before each individual test.
+ */
+ private static final Callable<ContentProvider> sProviderFactory = new Callable<ContentProvider>() {
+ @Override
+ public ContentProvider call() {
+ return new LoginsProvider();
+ }
+ };
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp(sProviderFactory, BrowserContract.LOGINS_AUTHORITY, DB_NAME);
+ for (TestCase test: TESTS_TO_RUN) {
+ mTests.add(test);
+ }
+ }
+
+ public void testLoginProviderTests() throws Exception {
+ for (Runnable test : mTests) {
+ final String testName = test.getClass().getSimpleName();
+ setTestName(testName);
+ ensureEmptyDatabase();
+ mAsserter.dumpLog("testLoginsProvider: Database empty - Starting " + testName + ".");
+ test.run();
+ }
+ }
+
+ /**
+ * Wipe DB.
+ */
+ private void ensureEmptyDatabase() {
+ getWritableDatabase(Logins.CONTENT_URI).delete(TABLE_LOGINS, null, null);
+ getWritableDatabase(DeletedLogins.CONTENT_URI).delete(TABLE_DELETED_LOGINS, null, null);
+ getWritableDatabase(LoginsDisabledHosts.CONTENT_URI).delete(TABLE_DISABLED_HOSTS, null, null);
+ }
+
+ private SQLiteDatabase getWritableDatabase(Uri uri) {
+ Uri testUri = appendUriParam(uri, BrowserContract.PARAM_IS_TEST, "1");
+ DelegatingTestContentProvider delegateProvider = (DelegatingTestContentProvider) mProvider;
+ LoginsProvider loginsProvider = (LoginsProvider) delegateProvider.getTargetProvider();
+ return loginsProvider.getWritableDatabaseForTesting(testUri);
+ }
+
+ /**
+ * LoginsProvider insert logins test.
+ */
+ private class InsertLoginsTest extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com",
+ "http://www.example.com", "username1", "password1", "username1", "password1", "guid1");
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues));
+ verifyLoginExists(contentValues, id);
+ Cursor cursor = mProvider.query(Logins.CONTENT_URI, null, Logins.GUID + " = ?", new String[] { "guid1" }, null);
+ verifyRowMatches(contentValues, cursor, "logins found");
+
+ // Empty ("") encrypted username and password are valid.
+ contentValues = createLogin("http://www.example.com", "http://www.example.com",
+ "http://www.example.com", "username1", "password1", "", "", "guid2");
+ id = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues));
+ verifyLoginExists(contentValues, id);
+ cursor = mProvider.query(Logins.CONTENT_URI, null, Logins.GUID + " = ?", new String[] { "guid2" }, null);
+ verifyRowMatches(contentValues, cursor, "logins found");
+ }
+ }
+
+ /**
+ * LoginsProvider updates logins test.
+ */
+ private class UpdateLoginsTest extends TestCase {
+ @Override
+ public void test() throws Exception {
+ final String guid1 = "guid1";
+ ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com",
+ "http://www.example.com", "username1", "password1", "username1", "password1", guid1);
+ long timeBeforeCreated = System.currentTimeMillis();
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues));
+ long timeAfterCreated = System.currentTimeMillis();
+ verifyLoginExists(contentValues, id);
+
+ Cursor cursor = getLoginById(id);
+ try {
+ mAsserter.ok(cursor.moveToFirst(), "cursor is not empty", "");
+ verifyBounded(timeBeforeCreated, cursor.getLong(cursor.getColumnIndexOrThrow(Logins.TIME_CREATED)), timeAfterCreated);
+ } finally {
+ cursor.close();
+ }
+
+ contentValues.put(BrowserContract.Logins.ENCRYPTED_USERNAME, "username2");
+ contentValues.put(Logins.ENCRYPTED_PASSWORD, "password2");
+
+ Uri updateUri = Logins.CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build();
+ int numUpdated = mProvider.update(updateUri, contentValues, null, null);
+ mAsserter.is(1, numUpdated, "Correct number updated");
+ verifyLoginExists(contentValues, id);
+
+ contentValues.put(BrowserContract.Logins.ENCRYPTED_USERNAME, "username1");
+ contentValues.put(Logins.ENCRYPTED_PASSWORD, "password1");
+
+ updateUri = Logins.CONTENT_URI;
+ numUpdated = mProvider.update(updateUri, contentValues, Logins.GUID + " = ?", new String[]{guid1});
+ mAsserter.is(1, numUpdated, "Correct number updated");
+ verifyLoginExists(contentValues, id);
+ }
+ }
+
+ /**
+ * LoginsProvider deletion logins test.
+ * - inserts a new logins
+ * - deletes the logins and verify deleted-logins table has entry for deleted guid.
+ */
+ private class DeleteLoginsTest extends TestCase {
+ @Override
+ public void test() throws Exception {
+ final String guid1 = "guid1";
+ ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com",
+ "http://www.example.com", "username1", "password1", "username1", "password1", guid1);
+ long id = ContentUris.parseId(mProvider.insert(Logins.CONTENT_URI, contentValues));
+ verifyLoginExists(contentValues, id);
+
+ Uri deletedUri = Logins.CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build();
+ int numDeleted = mProvider.delete(deletedUri, null, null);
+ mAsserter.is(1, numDeleted, "Correct number deleted");
+ verifyNoRowExists(Logins.CONTENT_URI, "No login entry found");
+
+ contentValues = new ContentValues();
+ contentValues.put(DeletedLogins.GUID, guid1);
+ Cursor cursor = mProvider.query(DeletedLogins.CONTENT_URI, null, null, null, null);
+ verifyRowMatches(contentValues, cursor, "deleted-login found");
+ cursor = mProvider.query(DeletedLogins.CONTENT_URI, null, DeletedLogins.GUID + " = ?", new String[] { guid1 }, null);
+ verifyRowMatches(contentValues, cursor, "deleted-login found");
+ }
+ }
+
+ /**
+ * LoginsProvider re-insert logins test.
+ * - inserts a row into deleted-logins
+ * - insert the same login (matching guid) and verify deleted-logins table is empty.
+ */
+ private class InsertDeletedLoginsTest extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(DeletedLogins.GUID, "guid1");
+ long id = ContentUris.parseId(mProvider.insert(DeletedLogins.CONTENT_URI, contentValues));
+ final Uri insertedUri = DeletedLogins.CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build();
+ Cursor cursor = mProvider.query(insertedUri, null, null, null, null);
+ verifyRowMatches(contentValues, cursor, "deleted-login found");
+ verifyNoRowExists(BrowserContract.Logins.CONTENT_URI, "No login entry found");
+
+ contentValues = createLogin("http://www.example.com", "http://www.example.com",
+ "http://www.example.com", "username1", "password1", "username1", "password1", "guid1");
+ id = ContentUris.parseId(mProvider.insert(Logins.CONTENT_URI, contentValues));
+ verifyLoginExists(contentValues, id);
+ verifyNoRowExists(DeletedLogins.CONTENT_URI, "No deleted-login entry found");
+ }
+ }
+
+ /**
+ * LoginsProvider insert Deleted logins test.
+ * - inserts a row into deleted-login without GUID.
+ */
+ private class InsertDeletedLoginsFailureTest extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues contentValues = new ContentValues();
+ try {
+ mProvider.insert(DeletedLogins.CONTENT_URI, contentValues);
+ fail("Failed to throw IllegalArgumentException while missing GUID");
+ } catch (Exception e) {
+ mAsserter.is(e.getClass(), IllegalArgumentException.class, "IllegalArgumentException thrown for invalid GUID");
+ }
+ }
+ }
+
+ /**
+ * LoginsProvider disabled host test.
+ * - inserts a disabled-host
+ * - delete the inserted disabled-host and verify disabled-hosts table is empty.
+ */
+ private class DisabledHostsInsertTest extends TestCase {
+ @Override
+ public void test() throws Exception {
+ final String hostname = "localhost";
+ final ContentValues contentValues = new ContentValues();
+ contentValues.put(LoginsDisabledHosts.HOSTNAME, hostname);
+ mProvider.insert(LoginsDisabledHosts.CONTENT_URI, contentValues);
+ final Uri insertedUri = LoginsDisabledHosts.CONTENT_URI.buildUpon().appendPath("hostname").appendPath(hostname).build();
+ final Cursor cursor = mProvider.query(insertedUri, null, null, null, null);
+ verifyRowMatches(contentValues, cursor, "disabled-hosts found");
+
+ final Uri deletedUri = LoginsDisabledHosts.CONTENT_URI.buildUpon().appendPath("hostname").appendPath(hostname).build();
+ final int numDeleted = mProvider.delete(deletedUri, null, null);
+ mAsserter.is(1, numDeleted, "Correct number deleted");
+ verifyNoRowExists(LoginsDisabledHosts.CONTENT_URI, "No disabled-hosts entry found");
+ }
+ }
+
+ /**
+ * LoginsProvider disabled host insert failure testcase.
+ * - inserts a disabled-host without providing hostname
+ */
+ private class DisabledHostsInsertFailureTest extends TestCase {
+ @Override
+ public void test() throws Exception {
+ final String hostname = "localhost";
+ final ContentValues contentValues = new ContentValues();
+ try {
+ mProvider.insert(LoginsDisabledHosts.CONTENT_URI, contentValues);
+ fail("Failed to throw IllegalArgumentException while missing hostname");
+ } catch (Exception e) {
+ mAsserter.is(e.getClass(), IllegalArgumentException.class, "IllegalArgumentException thrown for invalid hostname");
+ }
+ }
+ }
+
+ /**
+ * LoginsProvider login insertion with default values test.
+ * - insert a login missing GUID, FORM_SUBMIT_URL, HTTP_REALM and verify default values are set.
+ */
+ private class InsertLoginsWithDefaultValuesTest extends TestCase {
+ @Override
+ protected void test() throws Exception {
+ ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com",
+ "http://www.example.com", "username1", "password1", "username1", "password1", null);
+ // Remove GUID, HTTP_REALM, FORM_SUBMIT_URL from content values
+ contentValues.remove(Logins.GUID);
+ contentValues.remove(Logins.FORM_SUBMIT_URL);
+ contentValues.remove(Logins.HTTP_REALM);
+
+ long id = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues));
+ Cursor cursor = getLoginById(id);
+ assertNotNull(cursor);
+ cursor.moveToFirst();
+
+ mAsserter.isnot(cursor.getString(cursor.getColumnIndex(Logins.GUID)), null, "GUID is not null");
+ mAsserter.is(cursor.getString(cursor.getColumnIndex(Logins.HTTP_REALM)), null, "HTTP_REALM is not null");
+ mAsserter.is(cursor.getString(cursor.getColumnIndex(Logins.FORM_SUBMIT_URL)), null, "FORM_SUBMIT_URL is not null");
+ mAsserter.isnot(cursor.getString(cursor.getColumnIndex(Logins.TIME_LAST_USED)), null, "TIME_LAST_USED is not null");
+ mAsserter.isnot(cursor.getString(cursor.getColumnIndex(Logins.TIME_CREATED)), null, "TIME_CREATED is not null");
+ mAsserter.isnot(cursor.getString(cursor.getColumnIndex(Logins.TIME_PASSWORD_CHANGED)), null, "TIME_PASSWORD_CHANGED is not null");
+ mAsserter.is(cursor.getString(cursor.getColumnIndex(Logins.ENC_TYPE)), "0", "ENC_TYPE is 0");
+ mAsserter.is(cursor.getString(cursor.getColumnIndex(Logins.TIMES_USED)), "0", "TIMES_USED is 0");
+
+ // Verify other values.
+ verifyRowMatches(contentValues, cursor, "Updated login found");
+ }
+ }
+
+ /**
+ * LoginsProvider login insertion with duplicate GUID test.
+ * - insert two different logins with same GUID and verify that only one login exists.
+ */
+ private class InsertLoginsWithDuplicateGuidFailureTest extends TestCase {
+ @Override
+ protected void test() throws Exception {
+ final String guid = "guid1";
+ ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com",
+ "http://www.example.com", "username1", "password1", "username1", "password1", guid);
+ long id1 = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues));
+ verifyLoginExists(contentValues, id1);
+
+ // Insert another login with duplicate GUID.
+ contentValues = createLogin("http://www.example2.com", "http://www.example2.com",
+ "http://www.example2.com", "username2", "password2", "username2", "password2", guid);
+ Uri insertUri = mProvider.insert(Logins.CONTENT_URI, contentValues);
+ mAsserter.is(insertUri, null, "Duplicate Guid insertion id1");
+
+ // Verify login with id1 still exists.
+ verifyLoginExists(contentValues, id1);
+ }
+ }
+
+ /**
+ * LoginsProvider deletion by non-existent GUID test.
+ * - delete a login with random GUID and verify that no entry was deleted.
+ */
+ private class DeleteLoginsByNonExistentGuidTest extends TestCase {
+ @Override
+ protected void test() throws Exception {
+ Uri deletedUri = Logins.CONTENT_URI;
+ int numDeleted = mProvider.delete(deletedUri, Logins.GUID + "= ?", new String[] { "guid1" });
+ mAsserter.is(0, numDeleted, "Correct number deleted");
+ }
+ }
+
+ private void verifyBounded(long left, long middle, long right) {
+ mAsserter.ok(left <= middle, "Left <= middle", left + " <= " + middle);
+ mAsserter.ok(middle <= right, "Middle <= right", middle + " <= " + right);
+ }
+
+ private Cursor getById(Uri uri, long id, String[] projection) {
+ return mProvider.query(uri, projection,
+ _ID + " = ?",
+ new String[] { String.valueOf(id) },
+ null);
+ }
+
+ private Cursor getLoginById(long id) {
+ return getById(Logins.CONTENT_URI, id, null);
+ }
+
+ private void verifyLoginExists(ContentValues contentValues, long id) {
+ Cursor cursor = getLoginById(id);
+ verifyRowMatches(contentValues, cursor, "Updated login found");
+ }
+
+ private void verifyRowMatches(ContentValues contentValues, Cursor cursor, String name) {
+ try {
+ mAsserter.ok(cursor.moveToFirst(), name, "cursor is not empty");
+ CursorMatches(cursor, contentValues);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private void verifyNoRowExists(Uri contentUri, String name) {
+ Cursor cursor = mProvider.query(contentUri, null, null, null, null);
+ try {
+ mAsserter.is(0, cursor.getCount(), name);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private ContentValues createLogin(String hostname, String httpRealm, String formSubmitUrl,
+ String usernameField, String passwordField, String encryptedUsername,
+ String encryptedPassword, String guid) {
+ final ContentValues values = new ContentValues();
+ values.put(Logins.HOSTNAME, hostname);
+ values.put(Logins.HTTP_REALM, httpRealm);
+ values.put(Logins.FORM_SUBMIT_URL, formSubmitUrl);
+ values.put(Logins.USERNAME_FIELD, usernameField);
+ values.put(Logins.PASSWORD_FIELD, passwordField);
+ values.put(Logins.ENCRYPTED_USERNAME, encryptedUsername);
+ values.put(Logins.ENCRYPTED_PASSWORD, encryptedPassword);
+ values.put(Logins.GUID, guid);
+ return values;
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testMailToContextMenu.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testMailToContextMenu.java
new file mode 100644
index 0000000000..af674f441e
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testMailToContextMenu.java
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+public class testMailToContextMenu extends ContentContextMenuTest {
+
+ // Test website strings
+ private static String MAILTO_PAGE_URL;
+ private static final String mailtoMenuItems [] = {"Copy Email Address", "Share Email Address"};
+
+ public void testMailToContextMenu() {
+ final String MAILTO_PAGE_TITLE = mStringHelper.ROBOCOP_BIG_MAILTO_TITLE;
+
+ blockForGeckoReady();
+
+ MAILTO_PAGE_URL=getAbsoluteUrl(mStringHelper.ROBOCOP_BIG_MAILTO_URL);
+ loadUrlAndWait(MAILTO_PAGE_URL);
+ waitForText(MAILTO_PAGE_TITLE);
+
+ verifyContextMenuItems(mailtoMenuItems);
+ verifyCopyOption(mailtoMenuItems[0], "foo.bar@example.com"); // Test the "Copy Email Address" option
+ verifyShareOption(mailtoMenuItems[1], MAILTO_PAGE_TITLE); // Test the "Share Email Address" option
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testNativeCrypto.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testNativeCrypto.java
new file mode 100644
index 0000000000..2ae2bb532d
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testNativeCrypto.java
@@ -0,0 +1,288 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertArrayEquals;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fFail;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import org.mozilla.gecko.background.nativecode.NativeCrypto;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+
+import android.os.SystemClock;
+
+/**
+ * Tests the Java wrapper over native implementations of crypto code. Test vectors from:
+ * * PBKDF2SHA256:
+ * - <https://github.com/ircmaxell/PHP-PasswordLib/blob/master/test/Data/Vectors/pbkdf2-draft-josefsson-sha256.test-vectors>
+ - <https://gitorious.org/scrypt/nettle-scrypt/blobs/37c0d5288e991604fe33dba2f1724986a8dddf56/testsuite/pbkdf2-test.c>
+ * SHA-1:
+ - <http://oauth.googlecode.com/svn/code/c/liboauth/src/sha1.c>
+ */
+public class testNativeCrypto extends UITest {
+ private final static String LOGTAG = "testNativeCrypto";
+
+ /**
+ * Robocop supports only a single test function per test class. Therefore, we
+ * have a single top-level test function that dispatches to sub-tests,
+ * accepting that we might fail part way through the cycle. Proper JUnit 3
+ * testing can't land soon enough!
+ *
+ * @throws Exception
+ */
+ public void test() throws Exception {
+ // This test could complete very quickly. If it completes too soon, the
+ // minidumps directory may not be created before the process is
+ // taken down, causing bug 722166. But we can't run the test and then block
+ // for Gecko:Ready, since it may have arrived before we block. So we wait.
+ // Again, JUnit 3 can't land soon enough!
+ GeckoHelper.blockForReady();
+
+ _testPBKDF2SHA256A();
+ _testPBKDF2SHA256B();
+ _testPBKDF2SHA256C();
+ _testPBKDF2SHA256scryptA();
+ _testPBKDF2SHA256scryptB();
+ _testPBKDF2SHA256InvalidLenArg();
+
+ _testSHA1();
+ _testSHA1AgainstMessageDigest();
+
+ _testSHA256();
+ _testSHA256MultiPart();
+ _testSHA256AgainstMessageDigest();
+ _testSHA256WithMultipleUpdatesFromStream();
+ }
+
+ public void _testPBKDF2SHA256A() throws UnsupportedEncodingException, GeneralSecurityException {
+ final String p = "password";
+ final String s = "salt";
+ final int dkLen = 32;
+
+ checkPBKDF2SHA256(p, s, 1, dkLen, "120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b");
+ checkPBKDF2SHA256(p, s, 4096, dkLen, "c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a");
+ }
+
+ public void _testPBKDF2SHA256B() throws UnsupportedEncodingException, GeneralSecurityException {
+ final String p = "passwordPASSWORDpassword";
+ final String s = "saltSALTsaltSALTsaltSALTsaltSALTsalt";
+ final int dkLen = 40;
+
+ checkPBKDF2SHA256(p, s, 4096, dkLen, "348c89dbcbd32b2f32d814b8116e84cf2b17347ebc1800181c4e2a1fb8dd53e1c635518c7dac47e9");
+ }
+
+ public void _testPBKDF2SHA256scryptA() throws UnsupportedEncodingException, GeneralSecurityException {
+ final String p = "passwd";
+ final String s = "salt";
+ final int dkLen = 64;
+
+ checkPBKDF2SHA256(p, s, 1, dkLen, "55ac046e56e3089fec1691c22544b605f94185216dde0465e68b9d57c20dacbc49ca9cccf179b645991664b39d77ef317c71b845b1e30bd509112041d3a19783");
+ }
+
+ public void _testPBKDF2SHA256scryptB() throws UnsupportedEncodingException, GeneralSecurityException {
+ final String p = "Password";
+ final String s = "NaCl";
+ final int dkLen = 64;
+
+ checkPBKDF2SHA256(p, s, 80000, dkLen, "4ddcd8f60b98be21830cee5ef22701f9641a4418d04c0414aeff08876b34ab56a1d425a1225833549adb841b51c9b3176a272bdebba1d078478f62b397f33c8d");
+ }
+
+ public void _testPBKDF2SHA256C() throws UnsupportedEncodingException, GeneralSecurityException {
+ final String p = "pass\0word";
+ final String s = "sa\0lt";
+ final int dkLen = 16;
+
+ checkPBKDF2SHA256(p, s, 4096, dkLen, "89b69d0516f829893c696226650a8687");
+ }
+
+ public void _testPBKDF2SHA256InvalidLenArg() throws UnsupportedEncodingException, GeneralSecurityException {
+ final String p = "password";
+ final String s = "salt";
+ final int c = 1;
+ final int dkLen = -1; // Should always be positive.
+
+ try {
+ final byte[] key = NativeCrypto.pbkdf2SHA256(p.getBytes("US-ASCII"), s.getBytes("US-ASCII"), c, dkLen);
+ fFail("Expected sha256 to throw with negative dkLen argument.");
+ } catch (IllegalArgumentException e) { } // Expected.
+ }
+
+ private void _testSHA1() throws UnsupportedEncodingException {
+ final String[] inputs = new String[] {
+ "abc",
+ "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
+ "" // To be filled in below.
+ };
+ final String baseStr = "01234567";
+ final int repetitions = 80;
+ final StringBuilder builder = new StringBuilder(baseStr.length() * repetitions);
+ for (int i = 0; i < 80; ++i) {
+ builder.append(baseStr);
+ }
+ inputs[2] = builder.toString();
+
+ final String[] expecteds = new String[] {
+ "a9993e364706816aba3e25717850c26c9cd0d89d",
+ "84983e441c3bd26ebaae4aa1f95129e5e54670f1",
+ "dea356a2cddd90c7a7ecedc5ebb563934f460452"
+ };
+
+ for (int i = 0; i < inputs.length; ++i) {
+ final byte[] input = inputs[i].getBytes("US-ASCII");
+ final String expected = expecteds[i];
+
+ final byte[] actual = NativeCrypto.sha1(input);
+ fAssertNotNull("Hashed value is non-null", actual);
+ assertExpectedBytes(expected, actual);
+ }
+ }
+
+ /**
+ * Test to ensure the output of our SHA1 algo is the same as MessageDigest's. This is important
+ * because we intend to replace MessageDigest in FHR with this SHA-1 algo (bug 959652).
+ */
+ private void _testSHA1AgainstMessageDigest() throws UnsupportedEncodingException,
+ NoSuchAlgorithmException {
+ final String[] inputs = {
+ "password",
+ "saranghae",
+ "aoeusnthaoeusnthaoeusnth \0 12345098765432109876_!"
+ };
+
+ final MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ for (final String input : inputs) {
+ final byte[] inputBytes = input.getBytes("US-ASCII");
+
+ final byte[] mdBytes = digest.digest(inputBytes);
+ final byte[] ourBytes = NativeCrypto.sha1(inputBytes);
+ fAssertArrayEquals("MessageDigest hash is the same as NativeCrypto SHA-1 hash", mdBytes, ourBytes);
+ }
+ }
+
+ private void _testSHA256() throws UnsupportedEncodingException {
+ final String[] inputs = new String[] {
+ "abc",
+ "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
+ "" // To be filled in below.
+ };
+ final String baseStr = "01234567";
+ final int repetitions = 80;
+ final StringBuilder builder = new StringBuilder(baseStr.length() * repetitions);
+ for (int i = 0; i < repetitions; ++i) {
+ builder.append(baseStr);
+ }
+ inputs[2] = builder.toString();
+
+ final String[] expecteds = new String[] {
+ "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1",
+ "594847328451bdfa85056225462cc1d867d877fb388df0ce35f25ab5562bfbb5"
+ };
+
+ for (int i = 0; i < inputs.length; ++i) {
+ final byte[] input = inputs[i].getBytes("US-ASCII");
+ final String expected = expecteds[i];
+
+ final byte[] ctx = NativeCrypto.sha256init();
+ NativeCrypto.sha256update(ctx, input, input.length);
+ final byte[] actual = NativeCrypto.sha256finalize(ctx);
+ fAssertNotNull("Hashed value is non-null", actual);
+ assertExpectedBytes(expected, actual);
+ }
+ }
+
+ private void _testSHA256MultiPart() throws UnsupportedEncodingException {
+ final String input = "01234567";
+ final int repetitions = 80;
+ final String expected = "594847328451bdfa85056225462cc1d867d877fb388df0ce35f25ab5562bfbb5";
+
+ final byte[] inputBytes = input.getBytes("US-ASCII");
+ final byte[] ctx = NativeCrypto.sha256init();
+ for (int i = 0; i < repetitions; ++i) {
+ NativeCrypto.sha256update(ctx, inputBytes, inputBytes.length);
+ }
+ final byte[] actual = NativeCrypto.sha256finalize(ctx);
+ fAssertNotNull("Hashed value is non-null", actual);
+ assertExpectedBytes(expected, actual);
+ }
+
+ private void _testSHA256AgainstMessageDigest() throws UnsupportedEncodingException,
+ NoSuchAlgorithmException {
+ final String[] inputs = {
+ "password",
+ "saranghae",
+ "aoeusnthaoeusnthaoeusnth \0 12345098765432109876_!"
+ };
+
+ final MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ for (final String input : inputs) {
+ final byte[] inputBytes = input.getBytes("US-ASCII");
+
+ final byte[] mdBytes = digest.digest(inputBytes);
+
+ final byte[] ctx = NativeCrypto.sha256init();
+ NativeCrypto.sha256update(ctx, inputBytes, inputBytes.length);
+ final byte[] ourBytes = NativeCrypto.sha256finalize(ctx);
+ fAssertArrayEquals("MessageDigest hash is the same as NativeCrypto SHA-256 hash", mdBytes, ourBytes);
+ }
+ }
+
+ private void _testSHA256WithMultipleUpdatesFromStream() throws UnsupportedEncodingException {
+ final String input = "HelloWorldThisIsASuperLongStringThatIsReadAsAStreamOfBytes";
+ final ByteArrayInputStream stream = new ByteArrayInputStream(input.getBytes("UTF-8"));
+ final String expected = "8b5cb76b80f7eb6fb83ee138bfd31e2922e71dd245daa21a8d9876e8dee9eef5";
+
+ byte[] buffer = new byte[10];
+ final byte[] ctx = NativeCrypto.sha256init();
+ int c;
+
+ try {
+ while ((c = stream.read(buffer)) != -1) {
+ NativeCrypto.sha256update(ctx, buffer, c);
+ }
+ final byte[] actual = NativeCrypto.sha256finalize(ctx);
+ fAssertNotNull("Hashed value is non-null", actual);
+ assertExpectedBytes(expected, actual);
+ } catch (IOException e) {
+ fFail("IOException while reading stream");
+ }
+ }
+
+ private void checkPBKDF2SHA256(String p, String s, int c, int dkLen, final String expectedStr)
+ throws GeneralSecurityException, UnsupportedEncodingException {
+ final long start = SystemClock.elapsedRealtime();
+
+ final byte[] key = NativeCrypto.pbkdf2SHA256(p.getBytes("US-ASCII"), s.getBytes("US-ASCII"), c, dkLen);
+ fAssertNotNull("Hash result is non-null", key);
+
+ final long end = SystemClock.elapsedRealtime();
+ dumpLog(LOGTAG, "SHA-256 " + c + " took " + (end - start) + "ms");
+
+ if (expectedStr == null) {
+ return;
+ }
+
+ fAssertEquals("Hash result is the appropriate length", dkLen,
+ Utils.hex2Byte(expectedStr).length);
+ assertExpectedBytes(expectedStr, key);
+ }
+
+ private void assertExpectedBytes(final String expectedStr, byte[] key) {
+ fAssertEquals("Expected string matches hash result", expectedStr, Utils.byte2Hex(key));
+ final byte[] expected = Utils.hex2Byte(expectedStr);
+
+ fAssertEquals("Expected byte array length matches key length", expected.length, key.length);
+ fAssertArrayEquals("Expected byte array matches key byte array", expected, key);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testNewTab.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testNewTab.java
new file mode 100644
index 0000000000..d9b014c1ac
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testNewTab.java
@@ -0,0 +1,65 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.Element;
+import org.mozilla.gecko.R;
+
+import android.app.Activity;
+import android.view.View;
+
+import com.robotium.solo.Condition;
+
+/* A simple test that creates 2 new tabs and checks that the tab count increases. */
+public class testNewTab extends BaseTest {
+ private Element tabCount = null;
+ private Element tabs = null;
+ private final Element closeTab = null;
+ private int tabCountInt = 0;
+
+ public void testNewTab() {
+ String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ String url2 = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL);
+
+ blockForGeckoReady();
+
+ Activity activity = getActivity();
+ tabCount = mDriver.findElement(activity, R.id.tabs_counter);
+ tabs = mDriver.findElement(activity, R.id.tabs);
+ mAsserter.ok(tabCount != null && tabs != null,
+ "Checking elements", "all elements present");
+
+ int expectedTabCount = 1;
+ getTabCount(expectedTabCount);
+ mAsserter.is(tabCountInt, expectedTabCount, "Initial number of tabs correct");
+
+ addTab(url);
+ expectedTabCount++;
+ getTabCount(expectedTabCount);
+ mAsserter.is(tabCountInt, expectedTabCount, "Number of tabs increased");
+
+ addTab(url2);
+ expectedTabCount++;
+ getTabCount(expectedTabCount);
+ mAsserter.is(tabCountInt, expectedTabCount, "Number of tabs increased");
+
+ // cleanup: close all opened tabs
+ closeAddedTabs();
+ }
+
+ private void getTabCount(final int expected) {
+ waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ String newTabCountText = tabCount.getText();
+ tabCountInt = Integer.parseInt(newTabCountText);
+ if (tabCountInt == expected) {
+ return true;
+ }
+ return false;
+ }
+ }, MAX_WAIT_MS);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testOSLocale.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testOSLocale.java
new file mode 100644
index 0000000000..434594fee5
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testOSLocale.java
@@ -0,0 +1,137 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import java.util.Locale;
+
+import org.mozilla.gecko.BrowserLocaleManager;
+import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.Locales;
+import org.mozilla.gecko.PrefsHelper;
+
+import android.content.SharedPreferences;
+
+
+public class testOSLocale extends BaseTest {
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ // Clear per-profile SharedPreferences as a workaround for Bug 1069687.
+ // We're trying to exercise logic that only applies on first onCreate!
+ // We can't rely on this occurring prior to the first broadcast, though,
+ // so see the main test method for more logic.
+ final String profileName = getTestProfile().getName();
+ mAsserter.info("Setup", "Clearing pref in " + profileName + ".");
+ GeckoSharedPrefs.forProfileName(getActivity(), profileName)
+ .edit()
+ .remove("osLocale")
+ .apply();
+ }
+
+ public static class PrefState extends PrefsHelper.PrefHandlerBase {
+ private static final String PREF_LOCALE_OS = "intl.locale.os";
+ private static final String PREF_ACCEPT_LANG = "intl.accept_languages";
+
+ private static final String[] TO_FETCH = {PREF_LOCALE_OS, PREF_ACCEPT_LANG};
+
+ public volatile String osLocale;
+ public volatile String acceptLanguages;
+
+ private final Object waiter = new Object();
+
+ public void fetch() throws InterruptedException {
+ // Wait for any pending changes to have taken. Bug 1092580.
+ GeckoThread.waitOnGecko();
+ synchronized (waiter) {
+ PrefsHelper.getPrefs(TO_FETCH, this);
+ waiter.wait(MAX_WAIT_MS);
+ }
+ }
+
+ @Override
+ public void prefValue(String pref, String value) {
+ switch (pref) {
+ case PREF_LOCALE_OS:
+ osLocale = value;
+ return;
+ case PREF_ACCEPT_LANG:
+ acceptLanguages = value;
+ return;
+ }
+ }
+
+ @Override
+ public void finish() {
+ synchronized (waiter) {
+ waiter.notify();
+ }
+ }
+ }
+
+ public void testOSLocale() throws Exception {
+ blockForDelayedStartup();
+
+ final SharedPreferences prefs = GeckoSharedPrefs.forProfile(getActivity());
+ final PrefState state = new PrefState();
+
+ state.fetch();
+
+ // We don't know at this point whether we were run against a dirty profile or not.
+ //
+ // If we cleared the pref above prior to BrowserApp's delayed init, or our Gecko
+ // profile has been used before, then we're already going to be set up for en-US.
+ //
+ // If we cleared the pref after the initial broadcast, and our Android-side profile
+ // has been used before but the Gecko profile is clean, then the Gecko prefs won't
+ // have been set.
+ //
+ // Instead, we always send a new locale code, and see what we get.
+ final Locale fr = Locales.parseLocaleCode("fr");
+ BrowserLocaleManager.storeAndNotifyOSLocale(prefs, fr);
+
+ state.fetch();
+
+ mAsserter.is(state.osLocale, "fr", "We're in fr.");
+
+ // Now we can see what the expected Accept-Languages header should be.
+ // The OS locale is 'fr', so we have our app locale (en-US),
+ // the OS locale (fr), then any remaining fallbacks from intl.properties.
+ mAsserter.is(state.acceptLanguages, "en-us,fr,en", "We have the default en-US+fr Accept-Languages.");
+
+ // Now set the app locale to be es-ES.
+ BrowserLocaleManager.getInstance().setSelectedLocale(getActivity(), "es-ES");
+
+ state.fetch();
+
+ mAsserter.is(state.osLocale, "fr", "We're still in fr.");
+
+ // The correct set here depends on whether the
+ // browser was built with multiple locales or not.
+ // This is exasperating, but hey.
+ final boolean isMultiLocaleBuild = false;
+
+ // This never changes.
+ final String SELECTED_LOCALES = "es-es,fr,";
+
+ // Expected, from es-ES's intl.properties:
+ final String EXPECTED = SELECTED_LOCALES +
+ (isMultiLocaleBuild ? "es,en-us,en" : // Expected, from es-ES's intl.properties.
+ "en-us,en"); // Expected, from en-US (the default).
+
+ mAsserter.is(state.acceptLanguages, EXPECTED, "We have the right es-ES+fr Accept-Languages for this build.");
+
+ // And back to en-US.
+ final Locale en_US = Locales.parseLocaleCode("en-US");
+ BrowserLocaleManager.storeAndNotifyOSLocale(prefs, en_US);
+ BrowserLocaleManager.getInstance().resetToSystemLocale(getActivity());
+
+ state.fetch();
+
+ mAsserter.is(state.osLocale, "en-US", "We're in en-US.");
+ mAsserter.is(state.acceptLanguages, "en-us,en", "We have the default processed en-US Accept-Languages.");
+ }
+} \ No newline at end of file
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPanCorrectness.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPanCorrectness.java
new file mode 100644
index 0000000000..e7d402607d
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPanCorrectness.java
@@ -0,0 +1,49 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.PaintedSurface;
+
+/**
+ * A basic panning correctness test.
+ * - Loads a page and verifies it draws
+ * - drags page upwards by 100 pixels and verifies it draws
+ * - drags page leftwards by 100 pixels and verifies it draws
+ */
+public class testPanCorrectness extends PixelTest {
+ public void testPanCorrectness() {
+ String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BOXES_URL);
+
+ MotionEventHelper meh = new MotionEventHelper(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop());
+
+ blockForGeckoReady();
+
+ // load page and check we're at 0,0
+ loadAndVerifyBoxes(url);
+
+ // drag page upwards by 100 pixels
+ Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint();
+ meh.dragSync(10, 150, 10, 50);
+ PaintedSurface painted = waitForPaint(paintExpecter);
+ paintExpecter.unregisterListener();
+ try {
+ checkScrollWithBoxes(painted, 0, 100);
+ } finally {
+ painted.close();
+ }
+
+ // drag page leftwards by 100 pixels
+ paintExpecter = mActions.expectPaint();
+ meh.dragSync(150, 10, 50, 10);
+ painted = waitForPaint(paintExpecter);
+ paintExpecter.unregisterListener();
+ try {
+ checkScrollWithBoxes(painted, 100, 100);
+ } finally {
+ painted.close();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPasswordEncrypt.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPasswordEncrypt.java
new file mode 100644
index 0000000000..65a4eaba69
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPasswordEncrypt.java
@@ -0,0 +1,125 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import java.io.File;
+
+import org.json.JSONObject;
+import org.mozilla.gecko.NSSBridge;
+import org.mozilla.gecko.db.BrowserContract;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class testPasswordEncrypt extends BaseTest {
+ public void testPasswordEncrypt() {
+ Context context = (Context)getActivity();
+ ContentResolver cr = context.getContentResolver();
+ mAsserter.isnot(cr, null, "Found a content resolver");
+ ContentValues cvs = new ContentValues();
+
+ blockForGeckoReady();
+
+ File db = new File(mProfile, "signons.sqlite");
+ String dbPath = db.getPath();
+
+ Uri passwordUri;
+ cvs.put("hostname", "http://www.example.com");
+ cvs.put("encryptedUsername", "username");
+ cvs.put("encryptedPassword", "password");
+
+ // Attempt to insert into the db
+ passwordUri = BrowserContract.Passwords.CONTENT_URI;
+ Uri.Builder builder = passwordUri.buildUpon();
+ passwordUri = builder.appendQueryParameter("profilePath", mProfile).build();
+
+ Uri uri = cr.insert(passwordUri, cvs);
+ Uri expectedUri = passwordUri.buildUpon().appendPath("1").build();
+ mAsserter.is(uri.toString(), expectedUri.toString(), "Insert returned correct uri");
+
+ Cursor list = mActions.querySql(dbPath, "SELECT encryptedUsername FROM moz_logins");
+ list.moveToFirst();
+ String decryptedU = null;
+ try {
+ decryptedU = NSSBridge.decrypt(context, mProfile, list.getString(0));
+ } catch (Exception e) {
+ mAsserter.ok(false, "NSSBridge.decrypt through Exception " + e, ""); // TODO: What is diag?
+ }
+ mAsserter.is(decryptedU, "username", "Username was encrypted correctly when inserting");
+
+ list = mActions.querySql(dbPath, "SELECT encryptedPassword, encType FROM moz_logins");
+ list.moveToFirst();
+ String decryptedP = null;
+ try {
+ decryptedP = NSSBridge.decrypt(context, mProfile, list.getString(0));
+ } catch (Exception e) {
+ mAsserter.ok(false, "NSSBridge.decrypt through Exception " + e, ""); // TODO: What is diag?
+ }
+ mAsserter.is(decryptedP, "password", "Password was encrypted correctly when inserting");
+ mAsserter.is(list.getInt(1), 1, "Password has correct encryption type");
+
+ cvs.put("encryptedUsername", "username2");
+ cvs.put("encryptedPassword", "password2");
+ cr.update(passwordUri, cvs, null, null);
+
+ list = mActions.querySql(dbPath, "SELECT encryptedUsername FROM moz_logins");
+ list.moveToFirst();
+ try {
+ decryptedU = NSSBridge.decrypt(context, mProfile, list.getString(0));
+ } catch (Exception e) {
+ mAsserter.ok(false, "NSSBridge.decrypt through Exception " + e, ""); // TODO: What is diag?
+ }
+ mAsserter.is(decryptedU, "username2", "Username was encrypted when updating");
+
+ list = mActions.querySql(dbPath, "SELECT encryptedPassword FROM moz_logins");
+ list.moveToFirst();
+ try {
+ decryptedP = NSSBridge.decrypt(context, mProfile, list.getString(0));
+ } catch (Exception e) {
+ mAsserter.ok(false, "NSSBridge.decrypt through Exception " + e, ""); // TODO: What is diag?
+ }
+ mAsserter.is(decryptedP, "password2", "Password was encrypted when updating");
+
+ // Trying to store a password while master password is enabled should throw,
+ // but because Android can't send Exceptions across processes
+ // it just results in a null uri/cursor being returned.
+ toggleMasterPassword("password");
+ try {
+ uri = cr.insert(passwordUri, cvs);
+ // TODO: restore this assertion -- see bug 764901
+ // mAsserter.is(uri, null, "Storing a password while MP was set should fail");
+
+ Cursor c = cr.query(passwordUri, null, null, null, null);
+ // TODO: restore this assertion -- see bug 764901
+ // mAsserter.is(c, null, "Querying passwords while MP was set should fail");
+ } catch (Exception ex) {
+ // Password provider currently can not throw across process
+ // so we should not catch this exception here
+ mAsserter.ok(false, "Caught exception", ex.toString());
+ }
+ toggleMasterPassword("password");
+ }
+
+ private void toggleMasterPassword(String passwd) {
+ setPreferenceAndWaitForChange("privacy.masterpassword.enabled", passwd);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ // remove the entire signons.sqlite file
+ File profile = new File(mProfile);
+ File db = new File(profile, "signons.sqlite");
+ if (db.delete()) {
+ mAsserter.dumpLog("tearDown deleted "+db.toString());
+ } else {
+ mAsserter.dumpLog("tearDown did not delete "+db.toString());
+ }
+
+ super.tearDown();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPasswordProvider.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPasswordProvider.java
new file mode 100644
index 0000000000..8a2cc357e4
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPasswordProvider.java
@@ -0,0 +1,104 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import java.io.File;
+
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserContract.GeckoDisabledHosts;
+import org.mozilla.gecko.db.BrowserContract.Passwords;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+
+/**
+ * A basic password contentprovider test.
+ * - inserts a password when the database is not yet set up
+ * - inserts a password
+ * - updates a password
+ * - deletes a password
+ * - inserts a disabled host
+ * - queries for disabled host
+ */
+public class testPasswordProvider extends BaseTest {
+ private static final String DB_NAME = "signons.sqlite";
+
+ public void testPasswordProvider() {
+ Context context = (Context)getActivity();
+ ContentResolver cr = context.getContentResolver();
+ ContentValues[] cvs = new ContentValues[1];
+ cvs[0] = new ContentValues();
+
+ blockForGeckoReady();
+
+ cvs[0].put("hostname", "http://www.example.com");
+ cvs[0].put("httpRealm", "http://www.example.com");
+ cvs[0].put("formSubmitURL", "http://www.example.com");
+ cvs[0].put("usernameField", "usernameField");
+ cvs[0].put("passwordField", "passwordField");
+ cvs[0].put("encryptedUsername", "username");
+ cvs[0].put("encryptedPassword", "password");
+ cvs[0].put("encType", "1");
+
+ // Attempt to insert into the db
+ Uri passwordUri = Passwords.CONTENT_URI;
+ Uri.Builder builder = passwordUri.buildUpon();
+ passwordUri = builder.appendQueryParameter("profilePath", mProfile).build();
+
+ Uri uri = cr.insert(passwordUri, cvs[0]);
+ Uri expectedUri = passwordUri.buildUpon().appendPath("1").build();
+ mAsserter.is(uri.toString(), expectedUri.toString(), "Insert returned correct uri");
+ Cursor c = cr.query(passwordUri, null, null, null, null);
+ SqliteCompare(c, cvs);
+
+ cvs[0].put("usernameField", "usernameField2");
+ cvs[0].put("passwordField", "passwordField2");
+
+ int numUpdated = cr.update(passwordUri, cvs[0], null, null);
+ mAsserter.is(1, numUpdated, "Correct number updated");
+ c = cr.query(passwordUri, null, null, null, null);
+ SqliteCompare(c, cvs);
+
+ int numDeleted = cr.delete(passwordUri, null, null);
+ mAsserter.is(1, numDeleted, "Correct number deleted");
+ cvs = new ContentValues[0];
+ c = cr.query(passwordUri, null, null, null, null);
+ SqliteCompare(c, cvs);
+
+ ContentValues values = new ContentValues();
+ values.put("hostname", "http://www.example.com");
+
+ // Attempt to insert into the db.
+ Uri disabledHostUri = GeckoDisabledHosts.CONTENT_URI;
+ builder = disabledHostUri.buildUpon();
+ disabledHostUri = builder.appendQueryParameter("profilePath", mProfile).build();
+
+ uri = cr.insert(disabledHostUri, values);
+ expectedUri = disabledHostUri.buildUpon().appendPath("1").build();
+ mAsserter.is(uri.toString(), expectedUri.toString(), "Insert returned correct uri");
+ Cursor cursor = cr.query(disabledHostUri, null, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ cursor.moveToFirst();
+ CursorMatches(cursor, values);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ // remove the entire signons.sqlite file
+ File profile = new File(mProfile);
+ File db = new File(profile, "signons.sqlite");
+ if (db.delete()) {
+ mAsserter.dumpLog("tearDown deleted "+db.toString());
+ } else {
+ mAsserter.dumpLog("tearDown did not delete "+db.toString());
+ }
+
+ super.tearDown();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPermissions.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPermissions.java
new file mode 100644
index 0000000000..e4d997895c
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPermissions.java
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import java.util.ArrayList;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.PaintedSurface;
+
+import android.widget.CheckBox;
+
+public class testPermissions extends PixelTest {
+ public void testPermissions() {
+ blockForGeckoReady();
+
+ geolocationTest();
+ }
+
+ private void geolocationTest() {
+ Actions.RepeatedEventExpecter paintExpecter;
+
+ // Test geolocation notification
+ loadAndPaint(getAbsoluteUrl(mStringHelper.ROBOCOP_GEOLOCATION_URL));
+ waitForText("wants your location");
+
+ // Uncheck the "Don't ask again for this site" checkbox
+ ArrayList<CheckBox> checkBoxes = mSolo.getCurrentViews(CheckBox.class);
+ mAsserter.ok(checkBoxes.size() == 1, "checkbox count", "only one checkbox visible");
+ mAsserter.ok(mSolo.isCheckBoxChecked(0), "checkbox checked", "checkbox is checked");
+ mSolo.clickOnCheckBox(0);
+ mAsserter.ok(!mSolo.isCheckBoxChecked(0), "checkbox not checked", "checkbox is not checked");
+
+ // Test "Share" button functionality with unchecked checkbox
+ paintExpecter = mActions.expectPaint();
+ mSolo.clickOnText("Share");
+ PaintedSurface painted = waitForPaint(paintExpecter);
+ paintExpecter.unregisterListener();
+ try {
+ mAsserter.ispixel(painted.getPixelAt(10, 10), 0, 0x80, 0, "checking page background is green");
+ } finally {
+ painted.close();
+ }
+
+ // Re-trigger geolocation notification
+ reloadAndPaint();
+ waitForText("wants your location");
+
+ // Make sure the checkbox is checked this time
+ mAsserter.ok(mSolo.isCheckBoxChecked(0), "checkbox checked", "checkbox is checked");
+
+ // Test "Share" button functionality with checked checkbox
+ paintExpecter = mActions.expectPaint();
+ mSolo.clickOnText("Share");
+ painted = waitForPaint(paintExpecter);
+ paintExpecter.unregisterListener();
+ try {
+ mAsserter.ispixel(painted.getPixelAt(10, 10), 0, 0x80, 0, "checking page background is green");
+ } finally {
+ painted.close();
+ }
+
+ // When we reload the page, location should be automatically shared
+ painted = reloadAndGetPainted();
+ try {
+ mAsserter.ispixel(painted.getPixelAt(10, 10), 0, 0x80, 0, "checking page background is green");
+ } finally {
+ painted.close();
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPictureLinkContextMenu.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPictureLinkContextMenu.java
new file mode 100644
index 0000000000..1461fd9be3
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPictureLinkContextMenu.java
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+public class testPictureLinkContextMenu extends ContentContextMenuTest {
+
+ // Test website strings
+ private static String PICTURE_PAGE_URL;
+ private static String BLANK_PAGE_URL;
+ private static String PICTURE_URL;
+ private static final String tabs [] = { "Image", "Link" };
+ private static final String photoMenuItems [] = { "Copy Image Location", "Share Image", "View Image", "Set Image As", "Save Image" };
+ private static final String imageTitle = "^Image$";
+
+ public void testPictureLinkContextMenu() {
+ final String PICTURE_PAGE_TITLE = mStringHelper.ROBOCOP_PICTURE_LINK_TITLE;
+ final String linkMenuItems [] = mStringHelper.CONTEXT_MENU_ITEMS_IN_NORMAL_TAB;
+
+ blockForGeckoReady();
+
+ PICTURE_PAGE_URL=getAbsoluteUrl(mStringHelper.ROBOCOP_PICTURE_LINK_URL);
+ BLANK_PAGE_URL=getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL);
+ PICTURE_URL=getAbsoluteUrl(mStringHelper.ROBOCOP_PICTURE_URL);
+ loadAndPaint(PICTURE_PAGE_URL);
+ verifyUrlInContentDescription(PICTURE_PAGE_URL);
+
+ switchTabs(imageTitle);
+ verifyContextMenuItems(photoMenuItems);
+ verifyTabs(tabs);
+ switchTabs(imageTitle);
+ verifyCopyOption(photoMenuItems[0], "Firefox.jpg"); // Test the "Copy Image Location" option
+ switchTabs(imageTitle);
+ verifyShareOption(photoMenuItems[1], PICTURE_PAGE_TITLE); // Test the "Share Image" option
+ switchTabs(imageTitle);
+ verifyViewImageOption(photoMenuItems[2], PICTURE_URL, PICTURE_PAGE_TITLE); // Test the "View Image" option
+
+ verifyContextMenuItems(linkMenuItems);
+ openTabFromContextMenu(linkMenuItems[0],2); // Test the "Open in New Tab" option - expecting 2 tabs: the original and the new one
+ openTabFromContextMenu(linkMenuItems[1],2); // Test the "Open in Private Tab" option - expecting only 2 tabs in normal mode
+ verifyCopyOption(linkMenuItems[2], BLANK_PAGE_URL); // Test the "Copy Link" option
+ verifyShareOption(linkMenuItems[3], PICTURE_PAGE_TITLE); // Test the "Share Link" option
+ verifyBookmarkLinkOption(linkMenuItems[4],BLANK_PAGE_URL); // Test the "Bookmark Link" option
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mDatabaseHelper.deleteBookmark(BLANK_PAGE_URL);
+ super.tearDown();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrefsObserver.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrefsObserver.java
new file mode 100644
index 0000000000..f63358d575
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrefsObserver.java
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.Actions;
+
+/**
+ * Basic test to check bounce-back from overscroll.
+ * - Load the page and verify it draws
+ * - Drag page downwards by 100 pixels into overscroll, verify it snaps back.
+ * - Drag page rightwards by 100 pixels into overscroll, verify it snaps back.
+ */
+public class testPrefsObserver extends BaseTest {
+ private static final String PREF_TEST_PREF = "robocop.tests.dummy";
+
+ private Actions.PrefWaiter prefWaiter;
+ private boolean prefValue;
+
+ public void setPref(boolean value) {
+ mAsserter.dumpLog("Setting pref");
+ mActions.setPref(PREF_TEST_PREF, value, /* flush */ false);
+ }
+
+ public void waitAndCheckPref(boolean value) {
+ mAsserter.dumpLog("Waiting to check pref");
+
+ mAsserter.isnot(prefWaiter, null, "Check pref waiter is not null");
+ prefWaiter.waitForFinish();
+
+ mAsserter.is(prefValue, value, "Check correct pref value");
+ }
+
+ public void verifyDisconnect() {
+ mAsserter.dumpLog("Checking pref observer is removed");
+
+ final boolean newValue = !prefValue;
+ setPreferenceAndWaitForChange(PREF_TEST_PREF, newValue);
+ mAsserter.isnot(prefValue, newValue, "Check pref value did not change");
+ }
+
+ public void observePref() {
+ mAsserter.dumpLog("Setting up pref observer");
+
+ // Setup the pref observer
+ mAsserter.is(prefWaiter, null, "Check pref waiter is null");
+ prefWaiter = mActions.addPrefsObserver(
+ new String[] { PREF_TEST_PREF }, new Actions.PrefHandlerBase() {
+ @Override // Actions.PrefHandlerBase
+ public void prefValue(String pref, boolean value) {
+ mAsserter.is(pref, PREF_TEST_PREF, "Check correct pref name");
+ prefValue = value;
+ }
+ });
+ }
+
+ public void removePrefObserver() {
+ mAsserter.dumpLog("Removing pref observer");
+
+ mActions.removePrefsObserver(prefWaiter);
+ }
+
+ public void testPrefsObserver() {
+ blockForGeckoReady();
+
+ setPref(false);
+ observePref();
+ waitAndCheckPref(false);
+
+ setPref(true);
+ waitAndCheckPref(true);
+
+ removePrefObserver();
+ verifyDisconnect();
+
+ // Removing again should be a no-op.
+ removePrefObserver();
+ }
+}
+
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrivateBrowsing.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrivateBrowsing.java
new file mode 100644
index 0000000000..461e95aa72
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrivateBrowsing.java
@@ -0,0 +1,89 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import java.util.ArrayList;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.Tabs;
+
+/**
+ * The test loads a new private tab and loads a page with a big link on it
+ * Opens the link in a new private tab and checks that it is private
+ * Adds a new normal tab and loads a 3rd URL
+ * Checks that the bigLinkUrl loaded in the normal tab is present in the browsing history but the 2 urls opened in private tabs are not
+ */
+public class testPrivateBrowsing extends ContentContextMenuTest {
+
+ public void testPrivateBrowsing() {
+ String bigLinkUrl = getAbsoluteUrl(mStringHelper.ROBOCOP_BIG_LINK_URL);
+ String blank1Url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+ String blank2Url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL);
+ Tabs tabs = Tabs.getInstance();
+
+ blockForGeckoReady();
+
+ Actions.EventExpecter tabExpecter = mActions.expectGeckoEvent("Tab:Added");
+ Actions.EventExpecter contentExpecter = mActions.expectGeckoEvent("Content:PageShow");
+ tabs.loadUrl(bigLinkUrl, Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_PRIVATE);
+ tabExpecter.blockForEvent();
+ tabExpecter.unregisterListener();
+ contentExpecter.blockForEvent();
+ contentExpecter.unregisterListener();
+ verifyTabCount(1);
+
+ // May intermittently get context menu for normal tab without additional wait
+ mSolo.sleep(5000);
+
+ // Open the link context menu and verify the options
+ verifyContextMenuItems(mStringHelper.CONTEXT_MENU_ITEMS_IN_PRIVATE_TAB);
+
+ // Check that "Open Link in New Tab" is not in the menu
+ mAsserter.ok(!mSolo.searchText(mStringHelper.CONTEXT_MENU_ITEMS_IN_NORMAL_TAB[0]), "Checking that 'Open Link in New Tab' is not displayed in the context menu", "'Open Link in New Tab' is not displayed in the context menu");
+
+ // Open the link in a new private tab and check that it is private
+ tabExpecter = mActions.expectGeckoEvent("Tab:Added");
+ contentExpecter = mActions.expectGeckoEvent("Content:PageShow");
+ mSolo.clickOnText(mStringHelper.CONTEXT_MENU_ITEMS_IN_PRIVATE_TAB[0]);
+ String eventData = tabExpecter.blockForEventData();
+ tabExpecter.unregisterListener();
+ contentExpecter.blockForEvent();
+ contentExpecter.unregisterListener();
+ mAsserter.ok(isTabPrivate(eventData), "Checking if the new tab opened from the context menu was a private tab", "The tab was a private tab");
+ verifyTabCount(2);
+
+ // Open a normal tab to check later that it was registered in the Firefox Browser History
+ tabExpecter = mActions.expectGeckoEvent("Tab:Added");
+ contentExpecter = mActions.expectGeckoEvent("Content:PageShow");
+ tabs.loadUrl(blank2Url, Tabs.LOADURL_NEW_TAB);
+ tabExpecter.blockForEvent();
+ tabExpecter.unregisterListener();
+ contentExpecter.blockForEvent();
+ contentExpecter.unregisterListener();
+ verifyTabCount(2);
+
+ // wait for history updates to complete
+ mSolo.sleep(3000);
+
+ // Get the history list and check that the links open in private browsing are not saved
+ final ArrayList<String> firefoxHistory = mDatabaseHelper.getBrowserDBUrls(DatabaseHelper.BrowserDataType.HISTORY);
+
+ mAsserter.ok(!firefoxHistory.contains(bigLinkUrl), "Check that the link opened in the first private tab was not saved", bigLinkUrl + " was not added to history");
+ mAsserter.ok(!firefoxHistory.contains(blank1Url), "Check that the link opened in the private tab from the context menu was not saved", blank1Url + " was not added to history");
+ mAsserter.ok(firefoxHistory.contains(blank2Url), "Check that the link opened in the normal tab was saved", blank2Url + " was added to history");
+ }
+
+ private boolean isTabPrivate(String eventData) {
+ try {
+ JSONObject data = new JSONObject(eventData);
+ return data.getBoolean("isPrivate");
+ } catch (JSONException e) {
+ mAsserter.ok(false, "Error parsing the event data", e.toString());
+ return false;
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPromptGridInput.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPromptGridInput.java
new file mode 100644
index 0000000000..f645fe3be1
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPromptGridInput.java
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+public class testPromptGridInput extends BaseTest {
+ protected int index = 1;
+ public void testPromptGridInput() {
+ blockForGeckoReady();
+
+ test(1);
+
+ testGridItem("Icon 1");
+ testGridItem("Icon 2");
+ testGridItem("Icon 3");
+ testGridItem("Icon 4");
+ testGridItem("Icon 5");
+ testGridItem("Icon 6");
+ testGridItem("Icon 7");
+ testGridItem("Icon 8");
+ testGridItem("Icon 9");
+ testGridItem("Icon 10");
+ testGridItem("Icon 11");
+
+ mSolo.clickOnText("Icon 11");
+ mSolo.clickOnText("OK");
+
+ mAsserter.ok(waitForText("PASS"), "test passed", "PASS");
+ mSolo.goBack();
+ }
+
+ public void testGridItem(String title) {
+ // Force the list to scroll if necessary
+ mSolo.waitForText(title, 1, 500, true);
+ mAsserter.ok(waitForText(title), "Found grid item", title);
+ }
+
+ public void test(final int num) {
+ // Load about:blank between each test to ensure we reset state
+ loadUrl(mStringHelper.ABOUT_BLANK_URL);
+ mAsserter.ok(waitForText(mStringHelper.ABOUT_BLANK_URL), "Loaded blank page",
+ mStringHelper.ABOUT_BLANK_URL);
+
+ loadUrl("chrome://roboextender/content/robocop_prompt_gridinput.html#test" + num);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReaderCacheMigration.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReaderCacheMigration.java
new file mode 100644
index 0000000000..6dbc70de59
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReaderCacheMigration.java
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.db.BrowserDatabaseHelper;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
+
+/**
+ * Tests that our readercache-migration works correctly.
+ *
+ * Our main concern is ensuring that the hashed path for a given url is the same in Java
+ * as it was in JS, or else our (Java-based) migration will lose track of valid cached items.
+ */
+public class testReaderCacheMigration extends JavascriptBridgeTest {
+
+ private final String[] TEST_DOMAINS = new String[] {
+ "",
+ "http://mozilla.org",
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=1234315#c41",
+ "http://www.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.com/"
+ };
+
+ private static final String TEST_JS = "testReaderCacheMigration.js";
+
+ /**
+ * We compute the path-name in Java, and pass this through to JS, which conducts the actual
+ * equality check. Our JavascriptBridge doesn't seem to support return values, so we need
+ * to instead pass the computed path-name in at least one direction.
+ */
+ private void checkPathMatches(final String pageURL, final File cacheDir) {
+ final String hashedName = BrowserDatabaseHelper.getReaderCacheFileNameForURL(pageURL);
+
+ final File cacheFile = new File(cacheDir, hashedName);
+
+ try {
+ // We have to use the canonical path to match what the JS side will use. We could
+ // instead just match on the file name, and not the path, but this helps
+ // ensure that we've not broken any of the path finding either.
+ getJS().syncCall("check_hashed_path_matches", pageURL, cacheFile.getCanonicalPath());
+ } catch (IOException e) {
+ fAssertTrue("Unable to getCanonicalPath(), this should never happen", false);
+ }
+
+ }
+
+ public void testReaderCacheMigration() {
+ blockForReadyAndLoadJS(TEST_JS);
+
+ final File cacheDir = new File(GeckoProfile.get(getActivity()).getDir(), "readercache");
+
+ for (final String URL : TEST_DOMAINS) {
+ checkPathMatches(URL, cacheDir);
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReaderModeTitle.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReaderModeTitle.java
new file mode 100644
index 0000000000..31e0120704
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReaderModeTitle.java
@@ -0,0 +1,19 @@
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+
+/**
+ * This tests ensures that the toolbar in reader mode displays the original page url.
+ */
+public class testReaderModeTitle extends UITest {
+ public void testReaderModeTitle() {
+ GeckoHelper.blockForReady();
+
+ NavigationHelper.enterAndLoadUrl(mStringHelper.ROBOCOP_READER_MODE_BASIC_ARTICLE);
+
+ mToolbar.pressReaderModeButton();
+
+ mToolbar.assertTitle(mStringHelper.ROBOCOP_READER_MODE_BASIC_ARTICLE);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReadingListCache.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReadingListCache.java
new file mode 100644
index 0000000000..2006bbbfc9
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReadingListCache.java
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+
+public class testReadingListCache extends JavascriptTest {
+ public testReadingListCache() {
+ super("testReadingListCache.js");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReadingListToBookmarksMigration.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReadingListToBookmarksMigration.java
new file mode 100644
index 0000000000..dc181defcf
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReadingListToBookmarksMigration.java
@@ -0,0 +1,217 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.util.Log;
+
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.GeckoProfileDirectories;
+import org.mozilla.gecko.db.*;
+import org.mozilla.gecko.reader.SavedReaderViewHelper;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+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.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.mozilla.gecko.db.BrowserContract.*;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
+
+// TODO: Move to junit 3 tests, once those run in automation. There is no ui testing to do so it's a better fit.
+
+/**
+ * This test runs the 30 to 31 database upgrade, which moves reading-list INPUT_FILES from a separate
+ * reading-list folder into mobile bookmarks.
+ *
+ * It is based on testBrowserDatabaseHelperUpgrades. We load a v30 db containing two reading list
+ * INPUT_FILES, and test that these have successfully been converted into bookmarks.
+ */
+public class testReadingListToBookmarksMigration extends UITest {
+ private ArrayList<File> tempFiles;
+
+ // These names are generated by hashing the URLs, see INPUT_URLS below, and
+ // BrowserDatabaseHelper.getReaderCacheFileNameForURL()
+ private static final ArrayList<String> INPUT_FILES = new ArrayList<String>() {{
+ add("DWUP3U4ERC6TKJVSYXKJLHHEFY.json");
+ add("KWNV7PXD3JFOJBQJVFXI3CQKNE.json");
+ }};
+
+ // same ordering as in INPUT_FILES, although we don't rely on ordering in this test
+ private static final ArrayList<String> INPUT_URLS = new ArrayList<String>() {{
+ add("http://www.bbc.com/news/election-us-2016-35962179");
+ add("http://www.bbc.com/news/world-europe-35962670");
+ }};
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ // TODO: We already install & remove the profile directory each run so it'd be safer for clean-up to store
+ // this there. That being said, temporary files are still stored in the application directory so these temporary
+ // files will get cleaned up when the application is uninstalled or when data is cleared.
+ tempFiles = new ArrayList<>();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ for (final File file : tempFiles) {
+ file.delete();
+ }
+ }
+
+ private void walkRLPreMigration(SQLiteDatabase db) {
+ Set<String> urls = new HashSet<>(INPUT_URLS);
+
+ final Cursor c = db.rawQuery("SELECT * FROM " + ReadingListItems.TABLE_NAME, null);
+
+ fAssertNotNull("Cursor cannot be null", c);
+ try {
+ final boolean movedToFirst = c.moveToFirst();
+ fAssertTrue("Cursor must have data", movedToFirst);
+
+ int urlIndex = c.getColumnIndexOrThrow(ReadingListItems.URL);
+ do {
+ final String url = c.getString(urlIndex);
+
+ boolean removed = urls.remove(url);
+ fAssertTrue("Unexpected reading-list URL in database", removed);
+ } while (c.moveToNext());
+ } finally {
+ c.close();
+ }
+
+ fAssertTrue("All urls should have been removed from set", urls.isEmpty());
+ }
+
+ private void walkRLPostMigration(SQLiteDatabase db) {
+ Set<String> urls = new HashSet<>(INPUT_URLS);
+
+ final Cursor c = db.rawQuery("SELECT * FROM " +
+ Bookmarks.VIEW_WITH_ANNOTATIONS
+ + " WHERE " + BrowserContract.UrlAnnotations.KEY + " = ?",
+ new String[] {
+ BrowserContract.UrlAnnotations.Key.READER_VIEW.getDbValue()
+ });
+
+ fAssertNotNull("Cursor cannot be null", c);
+ try {
+ final boolean movedToFirst = c.moveToFirst();
+ fAssertTrue("Cursor must have data", movedToFirst);
+
+ int urlIndex = c.getColumnIndexOrThrow(Bookmarks.URL);
+ do {
+ final String url = c.getString(urlIndex);
+
+ boolean removed = urls.remove(url);
+ fAssertTrue("Unexpected reading-list URL in database", removed);
+ } while (c.moveToNext());
+ } finally {
+ c.close();
+ }
+
+ fAssertTrue("All urls should have been removed from set", urls.isEmpty());
+ }
+
+ /**
+ * @throws IOException if the database or input files cannot be copied.
+ */
+ public void testReadingListToBookmarksMigration() throws IOException {
+ final String tempDbPath = copyAssets();
+ final SQLiteDatabase db = SQLiteDatabase.openDatabase(tempDbPath, null, 0);
+
+ try {
+ // This initialises the helper, but does not open the DB.
+ BrowserDatabaseHelper dbHelper = new BrowserDatabaseHelper(getActivity(), tempDbPath);
+
+ walkRLPreMigration(db);
+
+ // Run just one upgrade - we don't know what future upgrades might do, whereas with one
+ // upgrade we can guarantee a given DB state.
+ dbHelper.onUpgrade(db, 30, 31);
+
+ // SavedReaderViewHelper writes annotations directly to the GeckoProfile DB (as opposed
+ // to our local DB copy). We aren't able to read this here (and the data isn't written
+ // to our own db), hence we can't test the DB content yet.
+// walkRLPostMigration(db);
+
+ SavedReaderViewHelper rvh = SavedReaderViewHelper.getSavedReaderViewHelper(getActivity());
+
+ fAssertEquals("All input files should have been migrated", INPUT_FILES.size(), rvh.size());
+ for (String url : INPUT_URLS) {
+ boolean isCached = rvh.isURLCached(url);
+ fAssertTrue("URL no longer in cache after migration", isCached);
+ }
+ } finally {
+ db.close();
+ }
+ }
+
+ private void copyAssetToFile(String inputPath, File destination) throws IOException {
+ final InputStream inputStream = openFileFromAssets(inputPath);
+ final OutputStream os = new BufferedOutputStream(new FileOutputStream(destination));
+ try {
+ final byte[] buffer = new byte[1024];
+ int len;
+ while ((len = inputStream.read(buffer)) > 0) {
+ os.write(buffer, 0, len);
+ }
+ os.flush();
+ } finally {
+ os.close();
+ inputStream.close();
+ }
+ }
+
+ /**
+ * Copies assets into the desired locations. We need to copy our DB into a temporary file,
+ * and readercache items into the profile directory.
+ *
+ * @throws IOException if reading the existing files or writing the temporary files fails
+ */
+ private String copyAssets() throws IOException {
+ final File profileDir = GeckoProfile.get(getActivity()).getDir();
+ final File cacheDir = new File(profileDir, "readercache");
+ cacheDir.mkdir();
+
+ for (String name : INPUT_FILES) {
+ final String path = "readercache" + File.separator + name;
+ final File destination = new File(cacheDir, name);
+ tempFiles.add(destination);
+
+ Log.d(LOGTAG, "Moving readerview cache file to " + destination.getPath());
+ copyAssetToFile(path, destination);
+ }
+
+ final File dbDestination = File.createTempFile("temporaryDB_", "db");
+ tempFiles.add(dbDestination);
+
+ Log.d(LOGTAG, "Moving DB from assets to " + dbDestination.getPath());
+ copyAssetToFile("browser.db", dbDestination);
+
+ return dbDestination.getPath();
+ }
+
+ private InputStream openFileFromAssets(final String name) throws IOException {
+ final String assetPath = String.format("reading_list_bookmarks_migration" + File.separator + name);
+ Log.d(LOGTAG, "Opening file from assets: " + assetPath);
+ try {
+ return new BufferedInputStream(getInstrumentation().getContext().getAssets().open(assetPath));
+ } catch (final FileNotFoundException e) {
+ throw new IllegalStateException("Declared input files must be provided", e);
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRestrictions.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRestrictions.java
new file mode 100644
index 0000000000..8977aa1775
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRestrictions.java
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertFalse;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
+
+import org.mozilla.gecko.restrictions.Restrictable;
+import org.mozilla.gecko.restrictions.Restrictions;
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+
+public class testRestrictions extends UITest {
+ public void testRestrictions() {
+ GeckoHelper.blockForReady();
+
+ // No restrictions should be enforced when using a normal profile
+ for (Restrictable restrictable : Restrictable.values()) {
+ if (restrictable == Restrictable.BLOCK_LIST) {
+ assertFeatureDisabled(restrictable);
+ } else {
+ assertFeatureEnabled(restrictable);
+ }
+ }
+ }
+
+ private void assertFeatureEnabled(Restrictable restrictable) {
+ fAssertTrue(String.format("Restrictable feature %s is enabled", restrictable.name),
+ Restrictions.isAllowed(getActivity(), restrictable)
+ );
+ }
+
+ private void assertFeatureDisabled(Restrictable restrictable) {
+ fAssertFalse(String.format("Restrictable feature %s is disabled", restrictable.name),
+ Restrictions.isAllowed(getActivity(), restrictable)
+ );
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRuntimePermissionsAPI.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRuntimePermissionsAPI.java
new file mode 100644
index 0000000000..df192fc430
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRuntimePermissionsAPI.java
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSObject;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fFail;
+
+public class testRuntimePermissionsAPI extends JavascriptTest implements NativeEventListener {
+ public testRuntimePermissionsAPI() {
+ super("testRuntimePermissionsAPI.js");
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ EventDispatcher.getInstance().registerGeckoThreadListener(this, "RuntimePermissions:Prompt");
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+
+ EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "RuntimePermissions:Prompt");
+ }
+
+ @Override
+ public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
+ mAsserter.is(event, "RuntimePermissions:Prompt", "Received RuntimePermissions:Prompt event");
+
+ try {
+ String[] permissions = message.getStringArray("permissions");
+ mAsserter.is(3, permissions.length, "Received three permissions");
+
+ mAsserter.is("android.permission.CAMERA", permissions[0], "Received CAMERA permission");
+ mAsserter.is("android.permission.WRITE_EXTERNAL_STORAGE", permissions[1], "Received WRITE_EXTERNAL_STORAGE permission");
+ mAsserter.is("android.permission.RECORD_AUDIO", permissions[2], "Received RECORD_AUDIO permission");
+ } catch (Exception e) {
+ fFail("Event does not contain expected data: " + e.getMessage());
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSearchHistoryProvider.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSearchHistoryProvider.java
new file mode 100644
index 0000000000..3c22703bcf
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSearchHistoryProvider.java
@@ -0,0 +1,379 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import java.util.concurrent.Callable;
+
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserContract.SearchHistory;
+import org.mozilla.gecko.db.SearchHistoryProvider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class testSearchHistoryProvider extends ContentProviderTest {
+
+ // Translations of "United Kingdom" in several different languages
+ private static final String[] testStrings = {
+ "An Ríocht Aontaithe", // Irish
+ "Angli", // Albanian
+ "Britanniarum Regnum", // Latin
+ "Britio", // Esperanto
+ "Büyük Britanya", // Turkish
+ "Egyesült Királyság", // Hungarian
+ "Erresuma Batua", // Basque
+ "Inggris Raya", // Indonesian
+ "Ir-Renju Unit", // Maltese
+ "Iso-Britannia", // Finnish
+ "JungtinÄ— KaralystÄ—", // Lithuanian
+ "LielbritÄnija", // Latvian
+ "Regatul Unit", // Romanian
+ "Regne Unit", // Catalan, Valencian
+ "Regno Unito", // Italian
+ "Royaume-Uni", // French
+ "Spojené království", // Czech
+ "Spojené kráľovstvo", // Slovak
+ "Storbritannia", // Norwegian
+ "Storbritannien", // Danish
+ "Suurbritannia", // Estonian
+ "Ujedinjeno Kraljevstvo", // Bosnian
+ "United Alaeze", // Igbo
+ "United Kingdom", // English
+ "Vereinigtes Königreich", // German
+ "Verenigd Koninkrijk", // Dutch
+ "Verenigde Koninkryk", // Afrikaans
+ "Vương quốc Anh", // Vietnamese
+ "Wayòm Ini", // Haitian, Haitian Creole
+ "Y Deyrnas Unedig", // Welsh
+ "Združeno kraljestvo", // Slovene
+ "Zjednoczone Królestwo", // Polish
+ "Ηνωμένο Βασίλειο", // Greek (modern)
+ "ВеликобританиÑ", // Russian
+ "ÐÑгдÑÑн Вант УлÑ", // Mongolian
+ "Обединетото КралÑтво", // Macedonian
+ "Уједињено КраљевÑтво", // Serbian
+ "Õ„Õ«Õ¡ÖÕµÕ¡Õ¬ Ô¹Õ¡Õ£Õ¡Õ¾Õ¸Ö€Õ¸Ö‚Õ©ÕµÕ¸Ö‚Õ¶", // Armenian
+ "בריטניה", // Hebrew (modern)
+ "פֿ×ַר×ייניקטע מלכות", // Yiddish
+ "المملكة المتحدة", // Arabic
+ "برطانیÛ", // Urdu
+ "پادشاهی متحده", // Persian (Farsi)
+ "यूनाइटेड किंगडम", // Hindi
+ "संयà¥à¤•à¥à¤¤ राजà¥à¤¯", // Nepali
+ "যà§à¦•à§à¦¤à¦°à¦¾à¦œà§à¦¯", // Bengali, Bangla
+ "યà«àª¨àª¾àª‡àªŸà«‡àª¡ કિંગડમ", // Gujarati
+ "à®à®•à¯à®•à®¿à®¯ ராஜà¯à®¯à®®à¯", // Tamil
+ "สหราชอาณาจัà¸à¸£", // Thai
+ "ສະ​ຫະ​ປະ​ຊາ​ຊະ​ອາ​ນາ​ຈັàº", // Lao
+ "გáƒáƒ”რთიáƒáƒœáƒ”ბული სáƒáƒ›áƒ”ფáƒ", // Georgian
+ "イギリス", // Japanese
+ "è”åˆçŽ‹å›½" // Chinese
+ };
+
+
+ private static final String DB_NAME = "searchhistory.db";
+
+ /**
+ * Boilerplate alert.
+ * <p/>
+ * Make sure this method is present and that it returns a new
+ * instance of your class.
+ */
+ private static final Callable<ContentProvider> sProviderFactory =
+ new Callable<ContentProvider>() {
+ @Override
+ public ContentProvider call() {
+ return new SearchHistoryProvider();
+ }
+ };
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp(sProviderFactory, BrowserContract.SEARCH_HISTORY_AUTHORITY, DB_NAME);
+ mTests.add(new TestInsert());
+ mTests.add(new TestUnicodeQuery());
+ mTests.add(new TestTimestamp());
+ mTests.add(new TestLimit());
+ mTests.add(new TestDelete());
+ mTests.add(new TestIncrement());
+ }
+
+ public void testSearchHistory() throws Exception {
+ for (Runnable test : mTests) {
+ String testName = test.getClass().getSimpleName();
+ setTestName(testName);
+ mAsserter.dumpLog(
+ "testBrowserProvider: Database empty - Starting " + testName + ".");
+ // Clear the db
+ mProvider.delete(SearchHistory.CONTENT_URI, null, null);
+ test.run();
+ }
+ }
+
+ /**
+ * Verify that we can pass a LIMIT clause using a query parameter.
+ */
+ private class TestLimit extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues cv;
+ for (int i = 0; i < testStrings.length; i++) {
+ cv = new ContentValues();
+ cv.put(SearchHistory.QUERY, testStrings[i]);
+ mProvider.insert(SearchHistory.CONTENT_URI, cv);
+ }
+
+ final int limit = 5;
+
+ // Test 1: Handle proper input.
+
+ Uri uri = SearchHistory.CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(limit))
+ .build();
+
+ Cursor c = mProvider.query(uri, null, null, null, null);
+ try {
+ mAsserter.is(c.getCount(), limit,
+ String.format("Should have %d results", limit));
+ } finally {
+ c.close();
+ }
+
+ // Test 2: Empty input yields all results.
+
+ uri = SearchHistory.CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_LIMIT, "")
+ .build();
+
+ c = mProvider.query(uri, null, null, null, null);
+ try {
+ mAsserter.is(c.getCount(), testStrings.length, "Should have all results");
+ } finally {
+ c.close();
+ }
+
+ // Test 3: Illegal params.
+
+ String[] illegalParams = new String[] {"a", "-1"};
+ boolean success = true;
+
+ for (String param : illegalParams) {
+ success = true;
+
+ uri = SearchHistory.CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_LIMIT, param)
+ .build();
+
+ try {
+ c = mProvider.query(uri, null, null, null, null);
+ success = false;
+ } catch(IllegalArgumentException e) {
+ // noop.
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+
+ mAsserter.ok(success, "LIMIT", param + " should have been an invalid argument");
+ }
+
+ }
+ }
+
+ /**
+ * Verify that we can insert values into the DB, including unicode.
+ */
+ private class TestInsert extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues cv;
+ for (int i = 0; i < testStrings.length; i++) {
+ cv = new ContentValues();
+ cv.put(SearchHistory.QUERY, testStrings[i]);
+ mProvider.insert(SearchHistory.CONTENT_URI, cv);
+ }
+
+ final Cursor c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+ try {
+ mAsserter.is(c.getCount(), testStrings.length,
+ "Should have one row for each insert");
+ } finally {
+ c.close();
+ }
+ }
+ }
+
+ /**
+ * Verify that we can insert values into the DB, including unicode.
+ */
+ private class TestUnicodeQuery extends TestCase {
+ @Override
+ public void test() throws Exception {
+ final String selection = SearchHistory.QUERY + " = ?";
+
+ for (int i = 0; i < testStrings.length; i++) {
+ final ContentValues cv = new ContentValues();
+ cv.put(SearchHistory.QUERY, testStrings[i]);
+ mProvider.insert(SearchHistory.CONTENT_URI, cv);
+
+ final Cursor c = mProvider.query(SearchHistory.CONTENT_URI, null, selection,
+ new String[]{ testStrings[i] }, null);
+ try {
+ mAsserter.is(c.getCount(), 1,
+ "Should have one row for insert of " + testStrings[i]);
+ } finally {
+ c.close();
+ }
+ }
+ }
+ }
+
+ /**
+ * Verify that timestamps are updated on insert.
+ */
+ private class TestTimestamp extends TestCase {
+ @Override
+ public void test() throws Exception {
+ String insertedTerm = "Courtside Seats";
+ long insertStart;
+ long insertFinish;
+ long t1Db;
+ long t2Db;
+
+ ContentValues cv = new ContentValues();
+ cv.put(SearchHistory.QUERY, insertedTerm);
+
+ // First check that the DB has a value that is close to the
+ // system time.
+ insertStart = System.currentTimeMillis();
+ mProvider.insert(SearchHistory.CONTENT_URI, cv);
+ insertFinish = System.currentTimeMillis();
+
+ final Cursor c1 = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+ try {
+ c1.moveToFirst();
+ t1Db = c1.getLong(c1.getColumnIndex(SearchHistory.DATE_LAST_VISITED));
+ } finally {
+ c1.close();
+ }
+
+ mAsserter.dumpLog("First insert:");
+ mAsserter.dumpLog(" insertStart " + insertStart);
+ mAsserter.dumpLog(" insertFinish " + insertFinish);
+ mAsserter.dumpLog(" t1Db " + t1Db);
+ mAsserter.ok(t1Db >= insertStart, "DATE_LAST_VISITED",
+ "Date last visited should be set on insert.");
+ mAsserter.ok(t1Db <= insertFinish, "DATE_LAST_VISITED",
+ "Date last visited should be set on insert.");
+
+ cv = new ContentValues();
+ cv.put(SearchHistory.QUERY, insertedTerm);
+
+ insertStart = System.currentTimeMillis();
+ mProvider.insert(SearchHistory.CONTENT_URI, cv);
+ insertFinish = System.currentTimeMillis();
+
+ final Cursor c2 = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+ try {
+ c2.moveToFirst();
+ t2Db = c2.getLong(c2.getColumnIndex(SearchHistory.DATE_LAST_VISITED));
+ } finally {
+ c2.close();
+ }
+
+ mAsserter.dumpLog("Second insert:");
+ mAsserter.dumpLog(" insertStart " + insertStart);
+ mAsserter.dumpLog(" insertFinish " + insertFinish);
+ mAsserter.dumpLog(" t2Db " + t2Db);
+
+ mAsserter.ok(t2Db >= insertStart, "DATE_LAST_VISITED",
+ "Date last visited should be set on insert.");
+ mAsserter.ok(t2Db <= insertFinish, "DATE_LAST_VISITED",
+ "Date last visited should be set on insert.");
+ mAsserter.ok(t2Db >= t1Db, "DATE_LAST_VISITED",
+ "Date last visited should be updated on key increment.");
+ }
+ }
+
+ /**
+ * Verify that sending a delete command empties the database.
+ */
+ private class TestDelete extends TestCase {
+ @Override
+ public void test() throws Exception {
+ String insertedTerm = "Courtside Seats";
+
+ ContentValues cv = new ContentValues();
+ cv.put(SearchHistory.QUERY, insertedTerm);
+ mProvider.insert(SearchHistory.CONTENT_URI, cv);
+
+ final Cursor c1 = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+ try {
+ mAsserter.is(c1.getCount(), 1, "Should have one value");
+ mProvider.delete(SearchHistory.CONTENT_URI, null, null);
+ } finally {
+ c1.close();
+ }
+
+ final Cursor c2 = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+ try {
+ mAsserter.is(c2.getCount(), 0, "Should be empty");
+ mProvider.insert(SearchHistory.CONTENT_URI, cv);
+ } finally {
+ c2.close();
+ }
+
+ final Cursor c3 = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+ try {
+ mAsserter.is(c3.getCount(), 1, "Should have one value");
+ } finally {
+ c3.close();
+ }
+ }
+ }
+
+
+ /**
+ * Ensure that we only increment when the case matches.
+ */
+ private class TestIncrement extends TestCase {
+ @Override
+ public void test() throws Exception {
+ ContentValues cv = new ContentValues();
+ cv.put(SearchHistory.QUERY, "omaha");
+ mProvider.insert(SearchHistory.CONTENT_URI, cv);
+
+ cv = new ContentValues();
+ cv.put(SearchHistory.QUERY, "omaha");
+ mProvider.insert(SearchHistory.CONTENT_URI, cv);
+
+ Cursor c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+ try {
+ c.moveToFirst();
+ mAsserter.is(c.getCount(), 1, "Should have one result");
+ mAsserter.is(c.getInt(c.getColumnIndex(SearchHistory.VISITS)), 2,
+ "Counter should be 2");
+ } finally {
+ c.close();
+ }
+
+ cv = new ContentValues();
+ cv.put(SearchHistory.QUERY, "Omaha");
+ mProvider.insert(SearchHistory.CONTENT_URI, cv);
+ c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+ try {
+ mAsserter.is(c.getCount(), 2, "Should have two results");
+ } finally {
+ c.close();
+ }
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSearchSuggestions.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSearchSuggestions.java
new file mode 100644
index 0000000000..6f82e5c519
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSearchSuggestions.java
@@ -0,0 +1,115 @@
+package org.mozilla.gecko.tests;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.SuggestClient;
+import org.mozilla.gecko.home.BrowserSearch;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.robotium.solo.Condition;
+
+/**
+ * Test for search suggestions.
+ * Sends queries from AwesomeBar input and verifies that suggestions match
+ * expected values.
+ */
+public class testSearchSuggestions extends BaseTest {
+ private static final int SUGGESTION_MAX = 3;
+ private static final int SUGGESTION_TIMEOUT = 15000;
+
+ private static final String TEST_QUERY = "foo barz";
+ private static final String SUGGESTION_TEMPLATE = "/robocop/robocop_suggestions.sjs?query=__searchTerms__";
+
+ public void testSearchSuggestions() {
+ // Mock the search system.
+ // The BrowserSearch UI only shows up once a non-empty
+ // search term is entered, but we swizzle in a new factory beforehand.
+ mockSuggestClientFactory();
+
+ blockForGeckoReady();
+
+ // Map of expected values. See robocop_suggestions.sjs.
+ final HashMap<String, ArrayList<String>> suggestMap = new HashMap<String, ArrayList<String>>();
+ buildSuggestMap(suggestMap);
+
+ focusUrlBar();
+
+ // At this point we rely on our swizzling having worked -- which relies
+ // on us not having previously run a search.
+ // The test will fail later if there's already a BrowserSearch object with a
+ // suggest client set, so fail here.
+ BrowserSearch browserSearch = (BrowserSearch) getBrowserSearch();
+ mAsserter.ok(browserSearch == null ||
+ browserSearch.mSuggestClient == null,
+ "There is no existing search client.", "");
+
+ // Now test the incremental suggestions.
+ for (int i = 0; i < TEST_QUERY.length(); i++) {
+ mActions.sendKeys(TEST_QUERY.substring(i, i+1));
+
+ final String query = TEST_QUERY.substring(0, i+1);
+ mSolo.waitForView(R.id.suggestion_text);
+ boolean success = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ // Get the first suggestion row.
+ ViewGroup suggestionGroup = (ViewGroup) getActivity().findViewById(R.id.suggestion_layout);
+ if (suggestionGroup == null) {
+ mAsserter.dumpLog("Fail: suggestionGroup is null.");
+ return false;
+ }
+
+ final ArrayList<String> expected = suggestMap.get(query);
+ for (int i = 0; i < expected.size(); i++) {
+ View queryChild = suggestionGroup.getChildAt(i);
+ if (queryChild == null || queryChild.getVisibility() == View.GONE) {
+ mAsserter.dumpLog("Fail: queryChild is null or GONE.");
+ return false;
+ }
+
+ String suggestion = ((TextView) queryChild.findViewById(R.id.suggestion_text)).getText().toString();
+ if (!suggestion.equals(expected.get(i))) {
+ mAsserter.dumpLog("Suggestion '" + suggestion + "' not equal to expected '" + expected.get(i) + "'.");
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }, SUGGESTION_TIMEOUT);
+
+ mAsserter.is(success, true, "Results for query '" + query + "' matched expected suggestions");
+ }
+ }
+
+ private void buildSuggestMap(HashMap<String, ArrayList<String>> suggestMap) {
+ // these values assume SUGGESTION_MAX = 3
+ suggestMap.put("f", new ArrayList<String>() {{ add("f"); add("facebook"); add("fandango"); add("frys"); }});
+ suggestMap.put("fo", new ArrayList<String>() {{ add("fo"); add("forever 21"); add("food network"); add("fox news"); }});
+ suggestMap.put("foo", new ArrayList<String>() {{ add("foo"); add("food network"); add("foothill college"); add("foot locker"); }});
+ suggestMap.put("foo ", new ArrayList<String>() {{ add("foo "); add("foo fighters"); add("foo bar"); add("foo bat"); }});
+ suggestMap.put("foo b", new ArrayList<String>() {{ add("foo b"); add("foo bar"); add("foo bat"); add("foo bay"); }});
+ suggestMap.put("foo ba", new ArrayList<String>() {{ add("foo ba"); add("foo bar"); add("foo bat"); add("foo bay"); }});
+ suggestMap.put("foo bar", new ArrayList<String>() {{ add("foo bar"); }});
+ suggestMap.put("foo barz", new ArrayList<String>() {{ add("foo barz"); }});
+ }
+
+ private void mockSuggestClientFactory() {
+ BrowserSearch.sSuggestClientFactory = new BrowserSearch.SuggestClientFactory() {
+ @Override
+ public SuggestClient getSuggestClient(Context context, String template, int timeout, int max) {
+ final String suggestTemplate = getAbsoluteRawUrl(SUGGESTION_TEMPLATE);
+
+ // This one uses our template, and also doesn't check for network accessibility.
+ return new SuggestClient(context, suggestTemplate, SUGGESTION_TIMEOUT, Integer.MAX_VALUE, false);
+ }
+ };
+ }
+}
+
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionHistory.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionHistory.java
new file mode 100644
index 0000000000..50d173461b
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionHistory.java
@@ -0,0 +1,37 @@
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+
+/**
+ * Tests that navigating through session history (ex: forward, back) sets the correct UI state.
+ */
+public class testSessionHistory extends UITest {
+ public void testSessionHistory() {
+ GeckoHelper.blockForReady();
+
+ String url = mStringHelper.ROBOCOP_BLANK_PAGE_01_URL;
+ NavigationHelper.enterAndLoadUrl(url);
+ mToolbar.assertTitle(url);
+
+ url = mStringHelper.ROBOCOP_BLANK_PAGE_02_URL;
+ NavigationHelper.enterAndLoadUrl(url);
+ mToolbar.assertTitle(url);
+
+ url = mStringHelper.ROBOCOP_BLANK_PAGE_03_URL;
+ NavigationHelper.enterAndLoadUrl(url);
+ mToolbar.assertTitle(url);
+
+ NavigationHelper.goBack();
+ mToolbar.assertTitle(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL);
+
+ NavigationHelper.goBack();
+ mToolbar.assertTitle(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
+
+ NavigationHelper.goForward();
+ mToolbar.assertTitle(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL);
+
+ NavigationHelper.reload();
+ mToolbar.assertTitle(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionOOMRestore.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionOOMRestore.java
new file mode 100644
index 0000000000..5646311b1a
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionOOMRestore.java
@@ -0,0 +1,54 @@
+package org.mozilla.gecko.tests;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+
+/**
+ * Tests session OOM restore behavior.
+ *
+ * Loads a session and tests that it is restored correctly.
+ */
+public class testSessionOOMRestore extends SessionTest {
+ private Session mSession;
+ private static final String PREFS_NAME = "GeckoApp";
+ private static final String PREFS_ALLOW_STATE_BUNDLE = "allowStateBundle";
+
+ @Override
+ public void setActivityIntent(Intent intent) {
+ PageInfo home = new PageInfo(StringHelper.STATIC_ABOUT_HOME_URL);
+ PageInfo page1 = new PageInfo("page1");
+ PageInfo page2 = new PageInfo("page2");
+ PageInfo page3 = new PageInfo("page3");
+ PageInfo page4 = new PageInfo("page4");
+ PageInfo page5 = new PageInfo("page5");
+ PageInfo page6 = new PageInfo("page6");
+
+ SessionTab tab1 = new SessionTab(0, home, page1, page2);
+ SessionTab tab2 = new SessionTab(1, home, page3, page4);
+ SessionTab tab3 = new SessionTab(2, home, page5, page6);
+
+ mSession = new Session(1, tab1, tab2, tab3);
+
+ String sessionString = buildSessionJSON(mSession);
+ writeProfileFile("sessionstore.js", sessionString);
+
+ // This feature is pref-protected to prevent other apps from injecting
+ // a state bundle, so enable it here.
+ SharedPreferences prefs = getInstrumentation().getTargetContext()
+ .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ prefs.edit().putBoolean(PREFS_ALLOW_STATE_BUNDLE, true).commit();
+
+ Bundle bundle = new Bundle();
+ bundle.putString("privateSession", null);
+ intent.putExtra("stateBundle", bundle);
+
+ super.setActivityIntent(intent);
+ }
+
+ public void testSessionOOMRestore() throws Exception {
+ blockForGeckoReady();
+ verifySessionTabs(mSession);
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionOOMSave.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionOOMSave.java
new file mode 100644
index 0000000000..f5e5ee0996
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionOOMSave.java
@@ -0,0 +1,87 @@
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.Actions;
+
+import com.robotium.solo.Condition;
+
+/**
+ * Tests session OOM save behavior.
+ *
+ * Builds a session and tests that the saved state is correct.
+ */
+public class testSessionOOMSave extends SessionTest {
+ private final static int SESSION_TIMEOUT = 25000;
+
+ public void testSessionOOMSave() {
+ Actions.EventExpecter pageShowExpecter = mActions.expectGeckoEvent("Content:PageShow");
+ pageShowExpecter.blockForEvent();
+ pageShowExpecter.unregisterListener();
+
+ PageInfo home = new PageInfo(mStringHelper.ABOUT_HOME_URL);
+ PageInfo page1 = new PageInfo("page1");
+ PageInfo page2 = new PageInfo("page2");
+ PageInfo page3 = new PageInfo("page3");
+ PageInfo page4 = new PageInfo("page4");
+ PageInfo page5 = new PageInfo("page5");
+ PageInfo page6 = new PageInfo("page6");
+
+ SessionTab tab1 = new SessionTab(0, home, page1, page2);
+ SessionTab tab2 = new SessionTab(1, home, page3, page4);
+ SessionTab tab3 = new SessionTab(2, home, page5, page6);
+
+ final Session session = new Session(1, tab1, tab2, tab3);
+
+ // Load the tabs into the browser
+ loadSessionTabs(session);
+
+ // Verify sessionstore.js written by Gecko. The session write is
+ // delayed for certain interactions (such as changing the selected
+ // tab), so the file is repeatedly read until it matches the expected
+ // output. Because of the delay, this part of the test takes ~9 seconds
+ // to pass.
+ VerifyJSONCondition verifyJSONCondition = new VerifyJSONCondition(session);
+ boolean success = waitForCondition(verifyJSONCondition, SESSION_TIMEOUT);
+ if (success) {
+ mAsserter.ok(true, "verified session JSON", null);
+ } else {
+ mAsserter.ok(false, "failed to verify session JSON",
+ getStackTraceString(verifyJSONCondition.getLastException()));
+ }
+ }
+
+ private class VerifyJSONCondition implements Condition {
+ private AssertException mLastException;
+ private final NonFatalAsserter mAsserter = new NonFatalAsserter();
+ private final Session mSession;
+
+ public VerifyJSONCondition(Session session) {
+ mSession = session;
+ }
+
+ @Override
+ public boolean isSatisfied() {
+ try {
+ String sessionString = readProfileFile("sessionstore.js");
+ if (sessionString == null) {
+ mLastException = new AssertException("Could not read sessionstore.js");
+ return false;
+ }
+
+ verifySessionJSON(mSession, sessionString, mAsserter);
+ } catch (AssertException e) {
+ mLastException = e;
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Gets the last AssertException thrown by verifySessionJSON().
+ *
+ * This is useful to get the stack trace if the test fails.
+ */
+ public AssertException getLastException() {
+ return mLastException;
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testShareLink.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testShareLink.java
new file mode 100644
index 0000000000..0df7861369
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testShareLink.java
@@ -0,0 +1,265 @@
+package org.mozilla.gecko.tests;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.home.HomePager;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Build;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.GridView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.robotium.solo.Condition;
+
+/**
+ * This test covers the opening and content of the Share Link pop-up list
+ * The test opens the Share menu from the app menu, the URL bar, a link context menu and the Awesomescreen tabs
+ */
+public class testShareLink extends AboutHomeTest {
+ String url;
+ String urlTitle = mStringHelper.ROBOCOP_BIG_LINK_TITLE;
+
+ public void testShareLink() {
+ url = getAbsoluteUrl(mStringHelper.ROBOCOP_BIG_LINK_URL);
+ ArrayList<String> shareOptions;
+ blockForGeckoReady();
+
+ // FIXME: This is a temporary hack workaround for a permissions problem.
+ openAboutHomeTab(AboutHomeTabs.HISTORY);
+
+ inputAndLoadUrl(url);
+ verifyUrlBarTitle(url); // Waiting for page title to ensure the page is loaded
+
+ selectMenuItem(mStringHelper.SHARE_LABEL);
+ if (Build.VERSION.SDK_INT >= 14) {
+ // Check for our own sync in the submenu.
+ waitForText("Sync$");
+ } else {
+ waitForText("Share via");
+ }
+
+ // Get list of current available share activities and verify them
+ shareOptions = getShareOptions();
+ ArrayList<String> displayedOptions = getShareOptionsList();
+ for (String option:shareOptions) {
+ // Verify if the option is present in the list of displayed share options
+ mAsserter.ok(optionDisplayed(option, displayedOptions), "Share option found", option);
+ }
+
+ // Test share from the urlbar context menu
+ mSolo.goBack(); // Close the share menu
+ mSolo.clickLongOnText(urlTitle);
+ verifySharePopup(shareOptions,"urlbar");
+
+ // The link has a 60px height, so let's try to hit the middle
+ float top = mDriver.getGeckoTop() + 30 * mDevice.density;
+ float left = mDriver.getGeckoLeft() + mDriver.getGeckoWidth() / 2;
+ mSolo.clickLongOnScreen(left, top);
+ verifySharePopup("Share Link",shareOptions,"Link");
+
+ // Test the share popup in the Bookmarks page
+ openAboutHomeTab(AboutHomeTabs.BOOKMARKS);
+
+ final ListView bookmarksList = findListViewWithTag(HomePager.LIST_TAG_BOOKMARKS);
+ mAsserter.is(waitForNonEmptyListToLoad(bookmarksList), true, "list is properly loaded");
+
+ int headerViewsCount = bookmarksList.getHeaderViewsCount();
+ View bookmarksItem = bookmarksList.getChildAt(headerViewsCount);
+ if (bookmarksItem == null) {
+ mAsserter.dumpLog("no child at index " + headerViewsCount + "; waiting for one...");
+ Condition listWaitCondition = new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ if (bookmarksList.getChildAt(bookmarksList.getHeaderViewsCount()) == null)
+ return false;
+ return true;
+ }
+ };
+ waitForCondition(listWaitCondition, MAX_WAIT_MS);
+ headerViewsCount = bookmarksList.getHeaderViewsCount();
+ bookmarksItem = bookmarksList.getChildAt(headerViewsCount);
+ }
+
+ mSolo.clickLongOnView(bookmarksItem);
+ verifySharePopup(shareOptions,"bookmarks");
+
+ // Prepopulate top sites with history items to overflow tiles.
+ // We are trying to move away from using reflection and doing more black-box testing.
+ inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL));
+ inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL));
+ inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_03_URL));
+ inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_04_URL));
+ if (mDevice.type.equals("tablet")) {
+ // Tablets have more tile spaces to fill.
+ inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_05_URL));
+ inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_BOXES_URL));
+ inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_SEARCH_URL));
+ inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_TEXT_PAGE_URL));
+ }
+
+ // Test the share popup in Top Sites.
+ openAboutHomeTab(AboutHomeTabs.TOP_SITES);
+
+ // Scroll down a bit so that the top sites list has more items on screen.
+ int width = mDriver.getGeckoWidth();
+ int height = mDriver.getGeckoHeight();
+ mActions.drag(width / 2, width / 2, height - 10, height / 2);
+
+ ListView topSitesList = findListViewWithTag(HomePager.LIST_TAG_TOP_SITES);
+ mAsserter.is(waitForNonEmptyListToLoad(topSitesList), true, "list is properly loaded");
+ View mostVisitedItem = topSitesList.getChildAt(topSitesList.getHeaderViewsCount());
+ mSolo.clickLongOnView(mostVisitedItem);
+ verifySharePopup(shareOptions,"top_sites");
+
+ // Test the share popup in the history tab
+ openAboutHomeTab(AboutHomeTabs.HISTORY);
+
+ ListView mostRecentList = findListViewWithTag(HomePager.LIST_TAG_HISTORY);
+ mAsserter.is(waitForNonEmptyListToLoad(mostRecentList), true, "list is properly loaded");
+
+ // Getting second child after header views because the first is the "Today" label
+ View mostRecentItem = mostRecentList.getChildAt(mostRecentList.getHeaderViewsCount() + 1);
+ mSolo.clickLongOnView(mostRecentItem);
+ verifySharePopup(shareOptions,"most recent");
+ }
+
+ public void verifySharePopup(ArrayList<String> shareOptions, String openedFrom) {
+ verifySharePopup("Share", shareOptions, openedFrom);
+ }
+
+ public void verifySharePopup(String shareItemText, ArrayList<String> shareOptions, String openedFrom) {
+ waitForText(shareItemText);
+ mSolo.clickOnText(shareItemText);
+ waitForText("Share via");
+ ArrayList<String> displayedOptions = getSharePopupOption();
+ for (String option:shareOptions) {
+ // Verify if the option is present in the list of displayed share options
+ mAsserter.ok(optionDisplayed(option, displayedOptions), "Share option for " + openedFrom + (openedFrom.equals("urlbar") ? "" : " item") + " found", option);
+ }
+ mSolo.goBack();
+ /**
+ * Adding a wait for the page title to make sure the Awesomebar will be dismissed
+ * Because of Bug 712370 the Awesomescreen will be dismissed when the Share Menu is closed
+ * so there is no need for handling this different depending on where the share menu was invoked from
+ * TODO: Look more into why the delay is needed here now and it was working before
+ */
+ waitForText(urlTitle);
+ }
+
+ // Create a SEND intent and get the possible activities offered
+ public ArrayList<String> getShareOptions() {
+ ArrayList<String> shareOptions = new ArrayList<>();
+ Activity currentActivity = getActivity();
+ final Intent shareIntent = new Intent(Intent.ACTION_SEND);
+ shareIntent.putExtra(Intent.EXTRA_TEXT, url);
+ shareIntent.putExtra(Intent.EXTRA_SUBJECT, "Robocop Blank 01");
+ shareIntent.setType("text/plain");
+ PackageManager pm = currentActivity.getPackageManager();
+ List<ResolveInfo> activities = pm.queryIntentActivities(shareIntent, 0);
+ for (ResolveInfo activity : activities) {
+ shareOptions.add(activity.loadLabel(pm).toString());
+ }
+ return shareOptions;
+ }
+
+ // Traverse the group of views, adding strings from TextViews to the list.
+ private void getGroupTextViews(ViewGroup group, ArrayList<String> list) {
+ for (int i = 0; i < group.getChildCount(); i++) {
+ View child = group.getChildAt(i);
+ if (child instanceof AbsListView) {
+ getGroupTextViews((AbsListView)child, list);
+ } else if (child instanceof ViewGroup) {
+ getGroupTextViews((ViewGroup)child, list);
+ } else if (child instanceof TextView) {
+ String viewText = ((TextView)child).getText().toString();
+ if (viewText != null && viewText.length() > 0) {
+ list.add(viewText);
+ }
+ }
+ }
+ }
+
+ // Traverse the group of views, adding strings from TextViews to the list.
+ // This override is for AbsListView, which has adapters. If adapters are
+ // available, it is better to use them so that child views that are not
+ // yet displayed can be examined.
+ private void getGroupTextViews(AbsListView group, ArrayList<String> list) {
+ for (int i = 0; i < group.getAdapter().getCount(); i++) {
+ View child = group.getAdapter().getView(i, null, group);
+ if (child instanceof AbsListView) {
+ getGroupTextViews((AbsListView)child, list);
+ } else if (child instanceof ViewGroup) {
+ getGroupTextViews((ViewGroup)child, list);
+ } else if (child instanceof TextView) {
+ String viewText = ((TextView)child).getText().toString();
+ if (viewText != null && viewText.length() > 0) {
+ list.add(viewText);
+ }
+ }
+ }
+ }
+
+ public ArrayList<String> getSharePopupOption() {
+ ArrayList<String> displayedOptions = new ArrayList<>();
+ AbsListView shareMenu = getDisplayedShareList();
+ getGroupTextViews(shareMenu, displayedOptions);
+ return displayedOptions;
+ }
+
+ public ArrayList<String> getShareSubMenuOption() {
+ ArrayList<String> displayedOptions = new ArrayList<>();
+ AbsListView shareMenu = getDisplayedShareList();
+ getGroupTextViews(shareMenu, displayedOptions);
+ return displayedOptions;
+ }
+
+ public ArrayList<String> getShareOptionsList() {
+ if (Build.VERSION.SDK_INT >= 14) {
+ return getShareSubMenuOption();
+ } else {
+ return getSharePopupOption();
+ }
+ }
+
+ private boolean optionDisplayed(String shareOption, ArrayList<String> displayedOptions) {
+ for (String displayedOption: displayedOptions) {
+ if (shareOption.equals(displayedOption)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private AbsListView mViewGroup;
+
+ private AbsListView getDisplayedShareList() {
+ mViewGroup = null;
+ boolean success = waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ ArrayList<View> views = mSolo.getCurrentViews();
+ for (View view : views) {
+ // List may be displayed in different view formats.
+ // On JB, GridView is common; on ICS-, ListView is common.
+ if (view instanceof ListView ||
+ view instanceof GridView) {
+ mViewGroup = (AbsListView)view;
+ return true;
+ }
+ }
+ return false;
+ }
+ }, MAX_WAIT_MS);
+ mAsserter.ok(success,"Got the displayed share options?", "Got the share options view");
+ return mViewGroup;
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSnackbarAPI.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSnackbarAPI.java
new file mode 100644
index 0000000000..893f98a518
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSnackbarAPI.java
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fFail;
+
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSObject;
+
+public class testSnackbarAPI extends JavascriptTest implements NativeEventListener {
+ // Snackbar.LENGTH_INDEFINITE: To avoid tests depending on the android design support library
+ private static final int SNACKBAR_LENGTH_INDEFINITE = -2;
+
+ public testSnackbarAPI() {
+ super("testSnackbarAPI.js");
+ }
+
+ @Override
+ public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
+ mAsserter.is(event, "Snackbar:Show", "Received Snackbar:Show event");
+
+ try {
+ mAsserter.is(message.getString("message"), "This is a Snackbar", "Snackbar message");
+ mAsserter.is(message.getInt("duration"), SNACKBAR_LENGTH_INDEFINITE, "Snackbar duration");
+
+ NativeJSObject action = message.getObject("action");
+
+ mAsserter.is(action.getString("label"), "Click me", "Snackbar action label");
+
+ } catch (Exception e) {
+ fFail("Event does not contain expected data: " + e.getMessage());
+ }
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ EventDispatcher.getInstance().registerGeckoThreadListener(this, "Snackbar:Show");
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+
+ EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "Snackbar:Show");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testStateWhileLoading.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testStateWhileLoading.java
new file mode 100644
index 0000000000..7f7b47450b
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testStateWhileLoading.java
@@ -0,0 +1,40 @@
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.tests.helpers.DeviceHelper;
+import org.mozilla.gecko.tests.helpers.GeckoClickHelper;
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+import org.mozilla.gecko.tests.helpers.WaitHelper;
+
+/**
+ * This test ensures the back/forward state is correct when switching to loading pages
+ * to prevent regressions like Bug 1124190.
+ */
+public class testStateWhileLoading extends UITest {
+ public void testStateWhileLoading() {
+ if (!DeviceHelper.isTablet()) {
+ // This test case only covers tablets currently.
+ return;
+ }
+
+ GeckoHelper.blockForReady();
+
+ NavigationHelper.enterAndLoadUrl(mStringHelper.ROBOCOP_LINK_TO_SLOW_LOADING);
+
+ GeckoClickHelper.openCentralizedLinkInNewTab();
+
+ WaitHelper.waitForPageLoad(new Runnable() {
+ @Override
+ public void run() {
+ mTabStrip.switchToTab(1);
+
+ // Assert that the state of the back button is correct
+ // after switching to the new (still loading) tab.
+ mToolbar.assertBackButtonIsNotEnabled();
+ }
+ });
+
+ // Assert that the state of the back button is still correct after the page has loaded.
+ mToolbar.assertBackButtonIsNotEnabled();
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testStumblerSetting.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testStumblerSetting.java
new file mode 100644
index 0000000000..ac551b97f4
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testStumblerSetting.java
@@ -0,0 +1,90 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.preferences.GeckoPreferences;
+import org.mozilla.mozstumbler.service.AppGlobals;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+import com.robotium.solo.Condition;
+
+/*
+ * This test enables (checkbox checked) the Fennec setting to contribute to MLS, then waits for
+ * a response Intent from the stumbler service to confirm it has started. Then, it disables the
+ * service in the setting, and waits for confirmation that the servie has stopped.
+ */
+public class testStumblerSetting extends BaseTest {
+ boolean mIsEnabled;
+
+ public void testStumblerSetting() {
+ if (!AppConstants.MOZ_STUMBLER_BUILD_TIME_ENABLED) {
+ mAsserter.info("Checking stumbler build config.", "Skipping test as Stumbler is not enabled in this build.");
+ return;
+ }
+
+ blockForGeckoReady();
+
+ selectMenuItem(mStringHelper.SETTINGS_LABEL);
+ mAsserter.ok(mSolo.waitForText(mStringHelper.SETTINGS_LABEL),
+ "The Settings menu did not load", mStringHelper.SETTINGS_LABEL);
+
+ String section = "^" + mStringHelper.MOZILLA_SECTION_LABEL + "$";
+ waitForEnabledText(section);
+ mSolo.clickOnText(section);
+
+ String itemTitle = "^" + mStringHelper.LOCATION_SERVICES_LABEL + "$";
+ boolean foundText = waitForPreferencesText(itemTitle);
+ mAsserter.ok(foundText, "Waiting for settings item " + itemTitle + " in section " + section,
+ "The " + itemTitle + " option is present in section " + section);
+
+ BroadcastReceiver enabledDisabledReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(AppGlobals.ACTION_TEST_SETTING_ENABLED)) {
+ mIsEnabled = true;
+ } else {
+ mIsEnabled = false;
+ }
+ }
+ };
+
+ Context context = getInstrumentation().getTargetContext();
+ IntentFilter intentFilter = new IntentFilter(AppGlobals.ACTION_TEST_SETTING_ENABLED);
+ intentFilter.addAction(AppGlobals.ACTION_TEST_SETTING_DISABLED);
+ context.registerReceiver(enabledDisabledReceiver, intentFilter);
+
+ boolean checked = mSolo.isCheckBoxChecked(itemTitle);
+ try {
+ mAsserter.ok(!checked, "Checking stumbler setting is unchecked.", "Unchecked as expected.");
+
+ waitForEnabledText(itemTitle);
+ mSolo.clickOnText(itemTitle);
+
+ mSolo.waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return mIsEnabled;
+ }
+ }, 15000);
+
+ mAsserter.ok(mIsEnabled, "Checking if stumbler became enabled.", "Stumbler is enabled.");
+ mSolo.clickOnText(itemTitle);
+
+ mSolo.waitForCondition(new Condition() {
+ @Override
+ public boolean isSatisfied() {
+ return !mIsEnabled;
+ }
+ }, 15000);
+
+ mAsserter.ok(!mIsEnabled, "Checking if stumbler became disabled.", "Stumbler is disabled.");
+ } finally {
+ context.unregisterReceiver(enabledDisabledReceiver);
+ }
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testThumbnails.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testThumbnails.java
new file mode 100644
index 0000000000..6cb42f37c8
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testThumbnails.java
@@ -0,0 +1,116 @@
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.db.BrowserDB;
+
+import android.content.ContentResolver;
+import android.graphics.Color;
+
+import com.robotium.solo.Condition;
+
+/**
+ * Test for thumbnail updates.
+ * - loads 2 pages, each of which yield an HTTP 200
+ * - verifies thumbnails are updated for both pages
+ * - loads pages again; first page yields HTTP 200, second yields HTTP 404
+ * - verifies thumbnail is updated for HTTP 200, but not HTTP 404
+ * - finally, test that BrowserDB.removeThumbnails drops the thumbnails
+ */
+public class testThumbnails extends BaseTest {
+ public void testThumbnails() {
+ final String site1Url = getAbsoluteUrl("/robocop/robocop_404.sjs?type=changeColor");
+ final String site2Url = getAbsoluteUrl("/robocop/robocop_404.sjs?type=do404");
+ final String site1Title = "changeColor";
+ final String site2Title = "do404";
+
+ // the session snapshot runnable is run 500ms after document stop. a
+ // 3000ms delay gives us 2.5 seconds to take the screenshot, which
+ // should be plenty of time, even on slow devices
+ final int thumbnailDelay = 3000;
+
+ blockForGeckoReady();
+
+ // load sites; both will return HTTP 200 with a green background
+ inputAndLoadUrl(site1Url);
+ mSolo.sleep(thumbnailDelay);
+ inputAndLoadUrl(site2Url);
+ mSolo.sleep(thumbnailDelay);
+ inputAndLoadUrl(mStringHelper.ABOUT_HOME_URL);
+ waitForCondition(new ThumbnailTest(site1Title, Color.GREEN), 5000);
+ mAsserter.is(getTopSiteThumbnailColor(site1Title), Color.GREEN, "Top site thumbnail updated for HTTP 200");
+ waitForCondition(new ThumbnailTest(site2Title, Color.GREEN), 5000);
+ mAsserter.is(getTopSiteThumbnailColor(site2Title), Color.GREEN, "Top site thumbnail updated for HTTP 200");
+
+ // load sites again; both will have red background, and do404 will return HTTP 404
+ inputAndLoadUrl(site1Url);
+ mSolo.sleep(thumbnailDelay);
+ inputAndLoadUrl(site2Url);
+ mSolo.sleep(thumbnailDelay);
+ inputAndLoadUrl(mStringHelper.ABOUT_HOME_URL);
+ waitForCondition(new ThumbnailTest(site1Title, Color.RED), 5000);
+ mAsserter.is(getTopSiteThumbnailColor(site1Title), Color.RED, "Top site thumbnail updated for HTTP 200");
+ waitForCondition(new ThumbnailTest(site2Title, Color.GREEN), 5000);
+ mAsserter.is(getTopSiteThumbnailColor(site2Title), Color.GREEN, "Top site thumbnail not updated for HTTP 404");
+
+ // test dropping thumbnails
+ final ContentResolver resolver = getActivity().getContentResolver();
+ final DatabaseHelper helper = new DatabaseHelper(getActivity(), mAsserter);
+ final BrowserDB db = helper.getProfileDB();
+
+ // check that the thumbnail is non-null
+ byte[] thumbnailData = db.getThumbnailForUrl(resolver, site1Url);
+ mAsserter.ok(thumbnailData != null && thumbnailData.length > 0, "Checking for thumbnail data", "No thumbnail data found");
+ // drop thumbnails
+ db.removeThumbnails(resolver);
+ // check that the thumbnail is now null
+ thumbnailData = db.getThumbnailForUrl(resolver, site1Url);
+ mAsserter.ok(thumbnailData == null || thumbnailData.length == 0, "Checking for thumbnail data", "Thumbnail data found");
+ }
+
+ private class ThumbnailTest implements Condition {
+ private final String mTitle;
+ private final int mColor;
+
+ public ThumbnailTest(String title, int color) {
+ mTitle = title;
+ mColor = color;
+ }
+
+ @Override
+ public boolean isSatisfied() {
+ return getTopSiteThumbnailColor(mTitle) == mColor;
+ }
+ }
+
+ private int getTopSiteThumbnailColor(String title) {
+ // This test is not currently run, so this just needs to compile.
+ return -1;
+// ViewGroup topSites = (ViewGroup) getActivity().findViewById(mTopSitesId);
+// if (topSites != null) {
+// final int childCount = topSites.getChildCount();
+// for (int i = 0; i < childCount; i++) {
+// View child = topSites.getChildAt(i);
+// if (child != null) {
+// TextView titleView = (TextView) child.findViewById(R.id.title);
+// if (titleView != null) {
+// if (titleView.getText().equals(title)) {
+// ImageView thumbnailView = (ImageView) child.findViewById(R.id.thumbnail);
+// if (thumbnailView != null) {
+// Bitmap thumbnail = ((BitmapDrawable) thumbnailView.getDrawable()).getBitmap();
+// return thumbnail.getPixel(0, 0);
+// } else {
+// mAsserter.dumpLog("getTopSiteThumbnailColor: unable to find mThumbnailId: "+R.id.thumbnail);
+// }
+// }
+// } else {
+// mAsserter.dumpLog("getTopSiteThumbnailColor: unable to find R.id.title: "+R.id.title);
+// }
+// } else {
+// mAsserter.dumpLog("getTopSiteThumbnailColor: skipped null child at index "+i);
+// }
+// }
+// } else {
+// mAsserter.dumpLog("getTopSiteThumbnailColor: unable to find mTopSitesId: " + mTopSitesId);
+// }
+// return -1;
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testTrackingProtection.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testTrackingProtection.java
new file mode 100644
index 0000000000..c27ff0094f
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testTrackingProtection.java
@@ -0,0 +1,65 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fFail;
+
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.GeckoApp;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.util.GeckoEventListener;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class testTrackingProtection extends JavascriptTest implements GeckoEventListener {
+ private String mLastTracking;
+
+ public testTrackingProtection() {
+ super("testTrackingProtection.js");
+ }
+
+ @Override
+ public void handleMessage(String event, final JSONObject message) {
+ if (event.equals("Content:SecurityChange")) {
+ try {
+ JSONObject identity = message.getJSONObject("identity");
+ JSONObject mode = identity.getJSONObject("mode");
+ mLastTracking = mode.getString("tracking");
+ mAsserter.dumpLog("Security change (tracking): " + mLastTracking);
+ } catch (Exception e) {
+ fFail("Can't extract tracking state from JSON");
+ }
+ }
+
+ if (event.equals("Test:Expected")) {
+ try {
+ String expected = message.getString("expected");
+ mAsserter.is(mLastTracking, expected, "Tracking matched expectation");
+ mAsserter.dumpLog("Testing (tracking): " + mLastTracking + " = " + expected);
+ } catch (Exception e) {
+ fFail("Can't extract expected state from JSON");
+ }
+ }
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ EventDispatcher.getInstance().registerGeckoThreadListener(this,
+ "Content:SecurityChange",
+ "Test:Expected");
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+
+ EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
+ "Content:SecurityChange",
+ "Test:Expected");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testUITelemetry.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testUITelemetry.java
new file mode 100644
index 0000000000..30d7c169c1
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testUITelemetry.java
@@ -0,0 +1,56 @@
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.PrefsHelper;
+import org.mozilla.gecko.Telemetry;
+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.util.Log;
+
+public class testUITelemetry extends JavascriptTest {
+ public testUITelemetry() {
+ super("testUITelemetry.js");
+ }
+
+ @Override
+ public void testJavascript() throws Exception {
+ blockForGeckoReady();
+
+ // We can't run these tests unless telemetry is turned on --
+ // the events will be dropped on the floor.
+ Log.i("GeckoTest", "Enabling telemetry.");
+ PrefsHelper.setPref(AppConstants.TELEMETRY_PREF_NAME, true);
+
+ Log.i("GeckoTest", "Adding telemetry events.");
+ try {
+ Telemetry.sendUIEvent(Event._TEST1, Method._TEST1);
+ Telemetry.startUISession(Session._TEST_STARTED_TWICE);
+ Telemetry.sendUIEvent(Event._TEST2, Method._TEST1);
+
+ // We can only start one session per name, so this call should be ignored.
+ Telemetry.startUISession(Session._TEST_STARTED_TWICE);
+
+ Telemetry.sendUIEvent(Event._TEST2, Method._TEST2);
+ Telemetry.startUISession(Session._TEST_STOPPED_TWICE);
+ Telemetry.sendUIEvent(Event._TEST3, Method._TEST1, "foobarextras");
+ Telemetry.stopUISession(Session._TEST_STARTED_TWICE, Reason._TEST1);
+ Telemetry.sendUIEvent(Event._TEST4, Method._TEST1, "barextras");
+ Telemetry.stopUISession(Session._TEST_STOPPED_TWICE, Reason._TEST2);
+
+ // This session is already stopped, so this call should be ignored.
+ Telemetry.stopUISession(Session._TEST_STOPPED_TWICE, Reason._TEST_IGNORED);
+
+ // Method defaults to Method.NONE
+ Telemetry.sendUIEvent(Event._TEST1);
+ } catch (Exception e) {
+ Log.e("GeckoTest", "Oops.", e);
+ }
+
+ Log.i("GeckoTest", "Running remaining JS test code.");
+ super.testJavascript();
+ }
+}
+
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testUnifiedTelemetryClientId.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testUnifiedTelemetryClientId.java
new file mode 100644
index 0000000000..aaaded4c82
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testUnifiedTelemetryClientId.java
@@ -0,0 +1,265 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.*;
+
+import android.util.Log;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.GeckoProfile;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.UUID;
+
+public class testUnifiedTelemetryClientId extends JavascriptBridgeTest {
+ private static final String TEST_JS = "testUnifiedTelemetryClientId.js";
+
+ private static final String CLIENT_ID_PATH = "datareporting/state.json";
+ private static final String FHR_DIR_PATH = "healthreport/";
+ private static final String FHR_CLIENT_ID_PATH = FHR_DIR_PATH + "state.json";
+
+ private GeckoProfile profile;
+ private File profileDir;
+ private File[] filesToDeleteOnReset;
+
+ public void setUp() throws Exception {
+ super.setUp();
+ profile = getTestProfile();
+ profileDir = profile.getDir(); // Assumes getDir is tested.
+ filesToDeleteOnReset = new File[] {
+ getClientIdFile(),
+ getFHRClientIdFile(),
+ getFHRClientIdParentDir(),
+ };
+ }
+
+ public void tearDown() throws Exception {
+ // Don't clear cache because who knows what state Gecko is in.
+ deleteClientIDFiles();
+ super.tearDown();
+ }
+
+ private void deleteClientIDFiles() {
+ Log.d(LOGTAG, "deleteClientIDFiles: begin");
+
+ for (final File file : filesToDeleteOnReset) {
+ file.delete(); // can't check return value because the file may not exist before deletion.
+ fAssertFalse("Deleted file in reset does not exist", file.exists()); // sanity check.
+ }
+
+ Log.d(LOGTAG, "deleteClientIDFiles: end");
+ }
+
+ public void testUnifiedTelemetryClientId() throws Exception {
+ blockForReadyAndLoadJS(TEST_JS);
+ fAssertTrue("Profile directory exists", profileDir.exists());
+
+ // Important note: we cannot stop Gecko from running while we run this test and
+ // Gecko is capable of creating client ID files while we run this test. However,
+ // ClientID.jsm will not touch modify the client ID files on disk if its client
+ // ID cache is filled. As such, we prevent it from touching the disk by intentionally
+ // priming the cache & deleting the files it added now, and resetting the cache at the
+ // latest possible moment before we attempt to test the client ID file.
+ //
+ // This is fragile because it relies on the ClientID cache's implementation, however,
+ // some alternatives (e.g. changing file system permissions, file locking) are worse
+ // because they can fire error handling code, which is not currently under test.
+ //
+ // First, we delete the test files - we don't want the cache prime to fail which could happen if
+ // these files are around & corrupted from a previous test/install. Then we prime the cache,
+ // and delete the files the cache priming added, so the tests are ready to add their own version
+ // of these files.
+ deleteClientIDFiles();
+ primeJsClientIdCache();
+ deleteClientIDFiles();
+
+ // TODO: If these tests weren't so expensive to run in automation,
+ // this should be two separate tests to avoid storing state between tests.
+ testJavaCreatesClientId(); // leaves cache filled.
+ deleteClientIDFiles();
+ testJsCreatesClientId(); // leaves cache filled.
+ deleteClientIDFiles();
+ testJavaMigratesFromHealthReport(); // leaves cache filled.
+ deleteClientIDFiles();
+ testJsMigratesFromHealthReport(); // leaves cache filled.
+
+ getJS().syncCall("endTest");
+ }
+
+ /**
+ * Scenario: Java creates client ID:
+ * * Fennec starts on fresh profile
+ * * Java code creates the client ID in datareporting/state.json
+ * * Js accesses client ID from the same file
+ * * Assert the client IDs are the same
+ */
+ private void testJavaCreatesClientId() throws Exception {
+ Log.d(LOGTAG, "testJavaCreatesClientId: start");
+
+ fAssertFalse("Client id file does not exist yet", getClientIdFile().exists());
+
+ final String clientIdFromJava = getClientIdFromJava();
+ resetJSCache();
+ final String clientIdFromJS = getClientIdFromJS();
+ // allow for the case where gecko updates the client ID after the first get
+ final String clientIdFromJavaAgain = getClientIdFromJava();
+ fAssertTrue("Client ID from Java equals ID from JS",
+ clientIdFromJava.equals(clientIdFromJS) ||
+ clientIdFromJavaAgain.equals(clientIdFromJS));
+
+ final String clientIdFromJSCache = getClientIdFromJS();
+ resetJSCache();
+ final String clientIdFromJSFileAgain = getClientIdFromJS();
+ fAssertEquals("Same client ID retrieved from JS cache", clientIdFromJavaAgain, clientIdFromJSCache);
+ fAssertEquals("Same client ID retrieved from JS file", clientIdFromJavaAgain, clientIdFromJSFileAgain);
+ }
+
+ /**
+ * Scenario: JS creates client ID
+ * * Fennec starts on a fresh profile
+ * * Js creates the client ID in datareporting/state.json
+ * * Java access the client ID from the same file
+ * * Assert the client IDs are the same
+ */
+ private void testJsCreatesClientId() throws Exception {
+ Log.d(LOGTAG, "testJsCreatesClientId: start");
+
+ fAssertFalse("Client id file does not exist yet", getClientIdFile().exists());
+
+ resetJSCache();
+ final String clientIdFromJS = getClientIdFromJS();
+ final String clientIdFromJava = getClientIdFromJava();
+ fAssertEquals("Client ID from JS equals ID from Java", clientIdFromJS, clientIdFromJava);
+
+ final String clientIdFromJSCache = getClientIdFromJS();
+ final String clientIdFromJavaAgain = getClientIdFromJava();
+ resetJSCache();
+ final String clientIdFromJSFileAgain = getClientIdFromJS();
+ fAssertEquals("Same client ID retrieved from JS cache", clientIdFromJS, clientIdFromJSCache);
+ fAssertEquals("Same client ID retrieved from JS file", clientIdFromJS, clientIdFromJSFileAgain);
+ fAssertEquals("Same client ID retrieved from Java", clientIdFromJS, clientIdFromJavaAgain);
+ }
+
+ /**
+ * Scenario: Java migrates client ID from FHR client ID file.
+ * * FHR file already exists.
+ * * Fennec starts on fresh profile
+ * * Java code merges client ID to datareporting/state.json from healthreport/state.json
+ * * Js accesses client ID from the same file
+ * * Assert the client IDs are the same
+ */
+ private void testJavaMigratesFromHealthReport() throws Exception {
+ Log.d(LOGTAG, "testJavaMigratesFromHealthReport: start");
+
+ fAssertFalse("Client id file does not exist yet", getClientIdFile().exists());
+ fAssertFalse("Health report file does not exist yet", getFHRClientIdFile().exists());
+
+ final String expectedClientId = UUID.randomUUID().toString();
+ createFHRClientIdFile(expectedClientId);
+
+ final String clientIdFromJava = getClientIdFromJava();
+ fAssertEquals("Health report client ID merged by Java", expectedClientId, clientIdFromJava);
+ resetJSCache();
+ final String clientIdFromJS = getClientIdFromJS();
+ fAssertEquals("Merged client ID read by JS", expectedClientId, clientIdFromJS);
+
+ final String clientIdFromJavaAgain = getClientIdFromJava();
+ final String clientIdFromJSCache = getClientIdFromJS();
+ resetJSCache();
+ final String clientIdFromJSFileAgain = getClientIdFromJS();
+ fAssertEquals("Same client ID retrieved from Java", expectedClientId, clientIdFromJavaAgain);
+ fAssertEquals("Same client ID retrieved from JS cache", expectedClientId, clientIdFromJSCache);
+ fAssertEquals("Same client ID retrieved from JS file", expectedClientId, clientIdFromJSFileAgain);
+ }
+
+ /**
+ * Scenario: JS merges client ID from FHR client ID file.
+ * * FHR file already exists.
+ * * Fennec starts on a fresh profile
+ * * Js merges the client ID to datareporting/state.json from healthreport/state.json
+ * * Java access the client ID from the same file
+ * * Assert the client IDs are the same
+ */
+ private void testJsMigratesFromHealthReport() throws Exception {
+ Log.d(LOGTAG, "testJsMigratesFromHealthReport: start");
+
+ fAssertFalse("Client id file does not exist yet", getClientIdFile().exists());
+ fAssertFalse("Health report file does not exist yet", getFHRClientIdFile().exists());
+
+ final String expectedClientId = UUID.randomUUID().toString();
+ createFHRClientIdFile(expectedClientId);
+
+ resetJSCache();
+ final String clientIdFromJS = getClientIdFromJS();
+ fAssertEquals("Health report client ID merged by JS", expectedClientId, clientIdFromJS);
+ final String clientIdFromJava = getClientIdFromJava();
+ fAssertEquals("Merged client ID read by Java", expectedClientId, clientIdFromJava);
+
+ final String clientIdFromJavaAgain = getClientIdFromJava();
+ final String clientIdFromJSCache = getClientIdFromJS();
+ resetJSCache();
+ final String clientIdFromJSFileAgain = getClientIdFromJS();
+ fAssertEquals("Same client ID retrieved from Java", expectedClientId, clientIdFromJavaAgain);
+ fAssertEquals("Same client ID retrieved from JS cache", expectedClientId, clientIdFromJSCache);
+ fAssertEquals("Same client ID retrieved from JS file", expectedClientId, clientIdFromJSFileAgain);
+ }
+
+ private String getClientIdFromJava() throws IOException {
+ // This assumes implementation details: it assumes the client ID
+ // file is created when Java attempts to retrieve it if it does not exist.
+ final String clientId = profile.getClientId();
+ fAssertNotNull("Returned client ID is not null", clientId);
+ fAssertTrue("Client ID file exists after getClientId call", getClientIdFile().exists());
+ return clientId;
+ }
+
+ private String getClientIdFromJS() {
+ return getBlockingFromJsString("clientId");
+ }
+
+ /**
+ * Must be called after Gecko is loaded.
+ */
+ private void primeJsClientIdCache() {
+ // Not the cleanest way, but it works.
+ getClientIdFromJS();
+ }
+
+ /**
+ * Resets the client ID cache in ClientID.jsm. This method *must* be called after
+ * Gecko is loaded or else this method will hang.
+ *
+ * Note: we do this for very specific reasons - see the comment in the test method
+ * ({@link #testUnifiedTelemetryClientId()}) for more.
+ */
+ private void resetJSCache() {
+ // HACK: the backing JS method is a promise with no return value. Rather than writing a method
+ // to handle this (for time reasons), I call the get String method and don't access the return value.
+ getBlockingFromJsString("reset");
+ }
+
+ private File getClientIdFile() {
+ return new File(profileDir, CLIENT_ID_PATH);
+ }
+
+ private File getFHRClientIdParentDir() {
+ return new File(profileDir, FHR_DIR_PATH);
+ }
+
+ private File getFHRClientIdFile() {
+ return new File(profileDir, FHR_CLIENT_ID_PATH);
+ }
+
+ private void createFHRClientIdFile(final String clientId) throws JSONException {
+ fAssertTrue("FHR directory created", getFHRClientIdParentDir().mkdirs());
+
+ final JSONObject obj = new JSONObject();
+ obj.put("clientID", clientId);
+ profile.writeFile(FHR_CLIENT_ID_PATH, obj.toString());
+ fAssertTrue("FHR client ID file exists after writing", getFHRClientIdFile().exists());
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testVideoControls.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testVideoControls.java
new file mode 100644
index 0000000000..5164815c43
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testVideoControls.java
@@ -0,0 +1,9 @@
+package org.mozilla.gecko.tests;
+
+
+
+public class testVideoControls extends JavascriptTest {
+ public testVideoControls() {
+ super("testVideoControls.js");
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testVkbOverlap.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testVkbOverlap.java
new file mode 100644
index 0000000000..f5a54a0e96
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testVkbOverlap.java
@@ -0,0 +1,105 @@
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.PaintedSurface;
+
+import android.net.Uri;
+
+/**
+ * A test to ensure that when an input field is focused, it is not obscured by the VKB.
+ * - Loads a page with an input field past the bottom of the visible area.
+ * - scrolls down to make the input field visible at the bottom of the screen.
+ * - taps on the input field to bring up the VKB
+ * - verifies that the input field is still visible.
+ */
+public class testVkbOverlap extends PixelTest {
+ private static final int CURSOR_BLINK_PERIOD = 500;
+ private static final int LESS_THAN_CURSOR_BLINK_PERIOD = CURSOR_BLINK_PERIOD - 50;
+ private static final int PAGE_SETTLE_TIME = 5000;
+
+ public void testVkbOverlap() {
+ blockForGeckoReady();
+ testSetup("initial-scale=1.0, user-scalable=no", false);
+ testSetup("initial-scale=1.0", false);
+ testSetup("", "phone".equals(mDevice.type));
+ }
+
+ private void testSetup(String viewport, boolean shouldZoom) {
+ loadAndPaint(getAbsoluteUrl("/robocop/test_viewport.sjs?metadata=" + Uri.encode(viewport)));
+
+ // scroll to the bottom of the page and let it settle
+ Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint();
+ MotionEventHelper meh = new MotionEventHelper(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop());
+ meh.dragSync(10, 150, 10, 50);
+
+ // the input field has a green background, so let's count the number of green pixels
+ int greenPixelCount = 0;
+
+ PaintedSurface painted = waitForPaint(paintExpecter);
+ paintExpecter.unregisterListener();
+ try {
+ greenPixelCount = countGreenPixels(painted);
+ } finally {
+ painted.close();
+ }
+
+ mAsserter.ok(greenPixelCount > 0, "testInputVisible", "Found " + greenPixelCount + " green pixels after scrolling");
+
+ paintExpecter = mActions.expectPaint();
+ // the input field should be in the bottom-left corner, so tap thereabouts
+ meh.tap(5, mDriver.getGeckoHeight() - 5);
+
+ // After tapping in the input field, the page needs some time to do stuff, like draw and undraw the focus highlight
+ // on the input field, trigger the VKB, process any resulting events generated by the system, and scroll the page. So
+ // we give it a few seconds to do all that. We are sufficiently generous with our definition of "few seconds" to
+ // prevent intermittent test failures.
+ try {
+ Thread.sleep(PAGE_SETTLE_TIME);
+ } catch (InterruptedException ie) {
+ ie.printStackTrace();
+ }
+
+ // now that the focus is in the text field we will repaint every 500ms as the cursor blinks, so we need to use a smaller
+ // "no paints" threshold to consider the page painted
+ paintExpecter.blockUntilClear(LESS_THAN_CURSOR_BLINK_PERIOD);
+ paintExpecter.unregisterListener();
+ painted = mDriver.getPaintedSurface();
+ try {
+ // if the vkb scrolled into view as expected, then the number of green pixels now visible should be about the
+ // same as it was before, since the green pixels indicate the text input is in view. use a fudge factor of 0.9 to
+ // account for borders and such of the text input which might still be out of view.
+ int newCount = countGreenPixels(painted);
+
+ // if zooming is allowed, the number of green pixels visible should have increased substantially
+ if (shouldZoom) {
+ mAsserter.ok(newCount > greenPixelCount * 1.5, "testVkbOverlap", "Found " + newCount + " green pixels after tapping; expected " + greenPixelCount);
+ } else {
+ mAsserter.ok((Math.abs(greenPixelCount - newCount) / greenPixelCount < 0.1), "testVkbOverlap", "Found " + newCount + " green pixels after tapping; expected " + greenPixelCount);
+ }
+ } finally {
+ painted.close();
+ }
+ }
+
+ private int countGreenPixels(PaintedSurface painted) {
+ int count = 0;
+ for (int y = painted.getHeight() - 1; y >= 0; y--) {
+ for (int x = painted.getWidth() - 1; x >= 0; x--) {
+ int pixel = painted.getPixelAt(x, y);
+ int r = (pixel >> 16) & 0xFF;
+ int g = (pixel >> 8) & 0xFF;
+ int b = (pixel & 0xFF);
+ if (g > (r + 0x30) && g > (b + 0x30)) {
+ // there's more green in this pixel than red or blue, so count it.
+ // the reason this is so hacky-looking is because even though green is supposed to
+ // be (r,g,b) = (0x00, 0x80, 0x00), the GL readback ends up coming back quite
+ // different.
+ count++;
+ }
+ // uncomment for debugging:
+ // if (pixel != -1) mAsserter.dumpLog("Pixel at " + x + ", " + y + ": " + Integer.toString(pixel, 16));
+ }
+ }
+ return count;
+ }
+}
diff --git a/mobile/android/tests/browser/robocop/testAccessibleCarets.html b/mobile/android/tests/browser/robocop/testAccessibleCarets.html
new file mode 100644
index 0000000000..99ae949e49
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testAccessibleCarets.html
@@ -0,0 +1,45 @@
+<html>
+ <head>
+ <title>ActionBar Handler and AccessibleCarets tests</title>
+ <meta name="viewport"
+ content="initial-scale=1, allowZoom=no, maximum-scale=1,
+ user-scalable=no, width=device-width">
+ </head>
+
+ <body>
+ <div id="LTRcontenteditable"
+ style="direction: ltr; width: 10em; height: 2em; word-wrap: break-word;
+ overflow: auto; -moz-user-select:text"
+ contenteditable="true">Find my book</div>
+ <div id="RTLcontenteditable"
+ style="direction: rtl; width: 10em; height: 2em; word-wrap: break-word;
+ overflow: auto; -moz-user-select:text"
+ contenteditable="true">×יפה ×”×וטו שלי</div>
+
+ <div id="LTRtextContent"
+ style="direction: ltr; width: 10em; height: 2em; word-wrap: break-word;
+ overflow: auto; -moz-user-select:text">Open the door</div>
+ <div id="RTLtextContent"
+ style="direction: rtl; width: 10em; height: 2em; word-wrap: break-word;
+ overflow: auto; -moz-user-select:text">תן לי מי×</div>
+
+ <input id="LTRinput" style="direction: ltr;" value="Type something">
+ <input id="RTLinput" style="direction: rtl;" value="לרוץ במעלה הגבעה">
+ <br><br><br>
+
+ <textarea id="LTRtextarea" style="direction: ltr;"
+ rows="3" cols="8">Words in a box</textarea>
+ <textarea id="RTLtextarea" style="direction: rtl;"
+ rows="3" cols="8">הספר ×”×•× ×˜×•×‘</textarea>
+
+ <br>
+ <input id="LTRphone" style="direction: ltr;" size="40"
+ value="09876543210 .-.)(wp#*1034103410341034X">
+ <br>
+ <input id="RTLphone" style="direction: rtl;" size="40"
+ value="התקשר +972 3 7347514 במשך זמן טוב">
+ <br><br><br>
+ <div><input value="DDs12">3 45<em id="bug1265750"> 678</em> 90</div>
+
+ </body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/testAccessibleCarets.js b/mobile/android/tests/browser/robocop/testAccessibleCarets.js
new file mode 100644
index 0000000000..a71ed22ee0
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testAccessibleCarets.js
@@ -0,0 +1,323 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+Cu.import('resource://gre/modules/Geometry.jsm');
+
+const ACCESSIBLECARET_PREF = "layout.accessiblecaret.enabled";
+const BASE_TEST_URL = "http://mochi.test:8888/tests/robocop/testAccessibleCarets.html";
+const DESIGNMODE_TEST_URL = "http://mochi.test:8888/tests/robocop/testAccessibleCarets2.html";
+
+// Ensures Tabs are completely loaded, viewport and zoom constraints updated, etc.
+const TAB_CHANGE_EVENT = "testAccessibleCarets:TabChange";
+const TAB_STOP_EVENT = "STOP";
+
+const gChromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+
+/**
+ * Wait for and return, when an expected tab change event occurs.
+ *
+ * @param tabId, The id of the target tab we're observing.
+ * @param eventType, The event type we expect.
+ * @return {Promise}
+ * @resolves The tab change object, including the matched tab id and event.
+ */
+function do_promiseTabChangeEvent(tabId, eventType) {
+ return new Promise(resolve => {
+ let observer = (subject, topic, data) => {
+ let message = JSON.parse(data);
+
+ if (message.event === eventType && message.tabId === tabId) {
+ Services.obs.removeObserver(observer, TAB_CHANGE_EVENT);
+ resolve(data);
+ }
+ }
+
+ Services.obs.addObserver(observer, TAB_CHANGE_EVENT, false);
+ });
+}
+
+/**
+ * Selection methods vary if we have an input / textarea element,
+ * or if we have basic content.
+ */
+function isInputOrTextarea(element) {
+ return ((element instanceof Ci.nsIDOMHTMLInputElement) ||
+ (element instanceof Ci.nsIDOMHTMLTextAreaElement));
+}
+
+/**
+ * Return the selection controller based on element.
+ */
+function elementSelection(element) {
+ return (isInputOrTextarea(element)) ?
+ element.editor.selection :
+ element.ownerDocument.defaultView.getSelection();
+}
+
+/**
+ * Select the requested character of a target element, w/o affecting focus.
+ */
+function selectElementChar(doc, element, char) {
+ if (isInputOrTextarea(element)) {
+ element.setSelectionRange(char, char + 1);
+ return;
+ }
+
+ // Simple test cases designed firstChild == #text node.
+ let range = doc.createRange();
+ range.setStart(element.firstChild, char);
+ range.setEnd(element.firstChild, char + 1);
+
+ let selection = elementSelection(element);
+ selection.removeAllRanges();
+ selection.addRange(range);
+}
+
+/**
+ * Get longpress point. Determine the midpoint in the requested character of
+ * the content in the element. X will be midpoint from left to right.
+ * Y will be 1/3 of the height up from the bottom to account for both
+ * LTR and smaller RTL characters. ie: |X| vs. |×|
+ */
+function getCharPressPoint(doc, element, char, expected) {
+ // Select the first char in the element.
+ selectElementChar(doc, element, char);
+
+ // Reality check selected char to expected.
+ let selection = elementSelection(element);
+ is(selection.toString(), expected, "Selected char should match expected char.");
+
+ // Return a point where long press should select entire word.
+ let rect = selection.getRangeAt(0).getBoundingClientRect();
+ let r = new Point(rect.left + (rect.width / 2), rect.bottom - (rect.height / 3));
+
+ return r;
+}
+
+/**
+ * Long press an element (RTL/LTR) at its calculated first character
+ * position, and return the result.
+ *
+ * @param midPoint, The screen coord for the longpress.
+ * @return Selection state helper-result object.
+ */
+function getLongPressResult(browser, midPoint) {
+ let domWinUtils = browser.contentWindow.
+ QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+
+ // AccessibleCarets expect longtap between touchstart/end.
+ domWinUtils.sendTouchEventToWindow("touchstart", [0], [midPoint.x], [midPoint.y],
+ [1], [1], [0], [1], 1, 0);
+ domWinUtils.sendMouseEventToWindow("mouselongtap", midPoint.x, midPoint.y,
+ 0, 1, 0);
+ domWinUtils.sendTouchEventToWindow("touchend", [0], [midPoint.x], [midPoint.y],
+ [1], [1], [0], [1], 1, 0);
+
+ let ActionBarHandler = gChromeWin.ActionBarHandler;
+ return { focusedElement: ActionBarHandler._targetElement,
+ text: ActionBarHandler._getSelectedText(),
+ selectionID: ActionBarHandler._selectionID,
+ };
+}
+
+/**
+ * Checks the Selection UI (ActionBar or FloatingToolbar)
+ * for the availability of an expected action.
+ *
+ * @param expectedActionID, The Selection UI action we expect to be available.
+ * @return Result boolean.
+ */
+function UIhasActionByID(expectedActionID) {
+ let actions = gChromeWin.ActionBarHandler._actionBarActions;
+ return actions.some(action => {
+ return action.id === expectedActionID;
+ });
+}
+
+/**
+ * Messages the ActionBarHandler to close the Selection UI.
+ */
+function closeSelectionUI() {
+ Services.obs.notifyObservers(null, "TextSelection:End",
+ JSON.stringify({selectionID: gChromeWin.ActionBarHandler._selectionID}));
+}
+
+/**
+ * Main test method.
+ */
+add_task(function* testAccessibleCarets() {
+ // Wait to start loading our test page until after the initial browser tab is
+ // completely loaded. This allows each tab to complete its layer initialization,
+ // importantly, its viewport and zoomContraints info.
+ let BrowserApp = gChromeWin.BrowserApp;
+ yield do_promiseTabChangeEvent(BrowserApp.selectedTab.id, TAB_STOP_EVENT);
+
+ // Ensure Gecko Selection and Touch carets are enabled.
+ Services.prefs.setBoolPref(ACCESSIBLECARET_PREF, true);
+
+ // Load test page, wait for load completion, register cleanup.
+ let browser = BrowserApp.addTab(BASE_TEST_URL).browser;
+ let tab = BrowserApp.getTabForBrowser(browser);
+ yield do_promiseTabChangeEvent(tab.id, TAB_STOP_EVENT);
+
+ do_register_cleanup(function cleanup() {
+ BrowserApp.closeTab(tab);
+ Services.prefs.clearUserPref(ACCESSIBLECARET_PREF);
+ });
+
+ // References to test document elements.
+ let doc = browser.contentDocument;
+ let ce_LTR_elem = doc.getElementById("LTRcontenteditable");
+ let tc_LTR_elem = doc.getElementById("LTRtextContent");
+ let i_LTR_elem = doc.getElementById("LTRinput");
+ let ta_LTR_elem = doc.getElementById("LTRtextarea");
+
+ let ce_RTL_elem = doc.getElementById("RTLcontenteditable");
+ let tc_RTL_elem = doc.getElementById("RTLtextContent");
+ let i_RTL_elem = doc.getElementById("RTLinput");
+ let ta_RTL_elem = doc.getElementById("RTLtextarea");
+
+ let ip_LTR_elem = doc.getElementById("LTRphone");
+ let ip_RTL_elem = doc.getElementById("RTLphone");
+ let bug1265750_elem = doc.getElementById("bug1265750");
+
+ // Locate longpress midpoints for test elements, ensure expactations.
+ let ce_LTR_midPoint = getCharPressPoint(doc, ce_LTR_elem, 0, "F");
+ let tc_LTR_midPoint = getCharPressPoint(doc, tc_LTR_elem, 0, "O");
+ let i_LTR_midPoint = getCharPressPoint(doc, i_LTR_elem, 0, "T");
+ let ta_LTR_midPoint = getCharPressPoint(doc, ta_LTR_elem, 0, "W");
+
+ let ce_RTL_midPoint = getCharPressPoint(doc, ce_RTL_elem, 0, "×");
+ let tc_RTL_midPoint = getCharPressPoint(doc, tc_RTL_elem, 0, "ת");
+ let i_RTL_midPoint = getCharPressPoint(doc, i_RTL_elem, 0, "ל");
+ let ta_RTL_midPoint = getCharPressPoint(doc, ta_RTL_elem, 0, "×”");
+
+ let ip_LTR_midPoint = getCharPressPoint(doc, ip_LTR_elem, 8, "2");
+ let ip_RTL_midPoint = getCharPressPoint(doc, ip_RTL_elem, 9, "2");
+ let bug1265750_midPoint = getCharPressPoint(doc, bug1265750_elem, 2, "7");
+
+ // Longpress various LTR content elements. Test focused element against
+ // expected, and selected text against expected.
+ let result = getLongPressResult(browser, ce_LTR_midPoint);
+ is(result.focusedElement, ce_LTR_elem, "Focused element should match expected.");
+ is(result.text, "Find", "Selected text should match expected text.");
+
+ result = getLongPressResult(browser, tc_LTR_midPoint);
+ is(result.focusedElement, null, "No focused element is expected.");
+ is(result.text, "Open", "Selected text should match expected text.");
+
+ result = getLongPressResult(browser, i_LTR_midPoint);
+ is(result.focusedElement, i_LTR_elem, "Focused element should match expected.");
+ is(result.text, "Type", "Selected text should match expected text.");
+
+ result = getLongPressResult(browser, ta_LTR_midPoint);
+ is(result.focusedElement, ta_LTR_elem, "Focused element should match expected.");
+ is(result.text, "Words", "Selected text should match expected text.");
+
+ result = getLongPressResult(browser, ip_LTR_midPoint);
+ is(result.focusedElement, ip_LTR_elem, "Focused element should match expected.");
+ is(result.text, "09876543210 .-.)(wp#*103410341",
+ "Selected phone number should match expected text.");
+ is(result.text.length, 30,
+ "Selected phone number length should match expected maximum.");
+
+ result = getLongPressResult(browser, bug1265750_midPoint);
+ is(result.focusedElement, null, "Focused element should match expected.");
+ is(result.text, "3 45 678 90",
+ "Selected phone number should match expected text.");
+
+ // Longpress various RTL content elements. Test focused element against
+ // expected, and selected text against expected.
+ result = getLongPressResult(browser, ce_RTL_midPoint);
+ is(result.focusedElement, ce_RTL_elem, "Focused element should match expected.");
+ is(result.text, "×יפה", "Selected text should match expected text.");
+
+ result = getLongPressResult(browser, tc_RTL_midPoint);
+ is(result.focusedElement, null, "No focused element is expected.");
+ is(result.text, "תן", "Selected text should match expected text.");
+
+ result = getLongPressResult(browser, i_RTL_midPoint);
+ is(result.focusedElement, i_RTL_elem, "Focused element should match expected.");
+ is(result.text, "לרוץ", "Selected text should match expected text.");
+
+ result = getLongPressResult(browser, ta_RTL_midPoint);
+ is(result.focusedElement, ta_RTL_elem, "Focused element should match expected.");
+ is(result.text, "הספר", "Selected text should match expected text.");
+
+ result = getLongPressResult(browser, ip_RTL_midPoint);
+ is(result.focusedElement, ip_RTL_elem, "Focused element should match expected.");
+ is(result.text, "+972 3 7347514 ",
+ "Selected phone number should match expected text.");
+
+ // Close Selection UI (ActionBar or FloatingToolbar) and complete test.
+ closeSelectionUI();
+ ok(true, "Finished testAccessibleCarets tests.");
+});
+
+/**
+ * DesignMode test method.
+ */
+add_task(function* testAccessibleCarets_designMode() {
+ let BrowserApp = gChromeWin.BrowserApp;
+
+ // Pre-populate the clipboard to ensure PASTE action available.
+ Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper).copyString("somethingMagical");
+
+ // Load test page, wait for load completion.
+ let browser = BrowserApp.addTab(DESIGNMODE_TEST_URL).browser;
+ let tab = BrowserApp.getTabForBrowser(browser, { selected: true });
+ yield do_promiseTabChangeEvent(tab.id, TAB_STOP_EVENT);
+
+ // References to test document elements, ActionBarHandler.
+ let doc = browser.contentDocument;
+ let tc_LTR_elem = doc.getElementById("LTRtextContent");
+ let tc_RTL_elem = doc.getElementById("RTLtextContent");
+
+ // Locate longpress midpoints for test elements, ensure expactations.
+ let tc_LTR_midPoint = getCharPressPoint(doc, tc_LTR_elem, 5, "x");
+ let tc_RTL_midPoint = getCharPressPoint(doc, tc_RTL_elem, 9, "ת");
+
+ let flavors = ["text/unicode"];
+ let clipboardHasText = Services.clipboard.hasDataMatchingFlavors(
+ flavors, flavors.length, Ci.nsIClipboard.kGlobalClipboard);
+ is(clipboardHasText, true, "There should now be paste-able text in the clipboard.");
+
+ // Toggle designMode on/off/on, check UI expectations.
+ ["on", "off"].forEach(designMode => {
+ doc.designMode = designMode;
+
+ // Text content in a document, whether in designMode or not, never receives focus.
+ // Available ActionBar/FloatingToolbar UI actions should vary depending on mode.
+
+ let result = getLongPressResult(browser, tc_LTR_midPoint);
+ is(result.focusedElement, null, "No focused element is expected.");
+ is(result.text, "existence", "Selected text should match expected text.");
+ is(UIhasActionByID("cut_action"), (designMode === "on"),
+ "CUT action UI Visibility should match designMode state.");
+ is(UIhasActionByID("paste_action"), (designMode === "on"),
+ "PASTE action UI Visibility should match designMode state.");
+
+ result = getLongPressResult(browser, tc_RTL_midPoint);
+ is(result.focusedElement, null, "No focused element is expected.");
+ is(result.text, "×ותו", "Selected text should match expected text.");
+ is(UIhasActionByID("cut_action"), (designMode === "on"),
+ "CUT action UI Visibility should match designMode state.");
+ is(UIhasActionByID("paste_action"), (designMode === "on"),
+ "PASTE action UI Visibility should match designMode state.");
+ });
+
+ // Close Selection UI (ActionBar or FloatingToolbar) and complete test.
+ closeSelectionUI();
+ ok(true, "Finished testAccessibleCarets_designMode tests.");
+});
+
+
+// Start all the test tasks.
+run_next_test();
diff --git a/mobile/android/tests/browser/robocop/testAccessibleCarets2.html b/mobile/android/tests/browser/robocop/testAccessibleCarets2.html
new file mode 100644
index 0000000000..fc1268462b
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testAccessibleCarets2.html
@@ -0,0 +1,23 @@
+<html>
+ <head>
+ <title>ActionBar Handler and AccessibleCarets tests for DesignMode</title>
+ <meta name="viewport"
+ content="initial-scale=1, allowZoom=no, maximum-scale=1,
+ user-scalable=no, width=device-width">
+ </head>
+
+ <body>
+ <div id="LTRtextContent" style="direction: ltr;">The existence of right-handed
+ neutrinos is theoretically well-motivated, as all other known fermions have
+ been observed with left and right chirality, and they can explain the
+ observed active neutrino masses in a natural way.
+ </div>
+ <br><br><br> <!-- Rule out caret overlay on next field -->
+
+ <div id="RTLtextContent" style="direction: rtl;">זהו ×œ× ×ותו הטקסט כפי למבחן שמ×ל לימין,
+ ×בל מה לעז×זל? הסוקר שלי ×œ×¢×•×œ× ×œ× ×œ×ª×¤×•×¡ ×ותי. ×× ×™ רק ×ª×•×¨× × ×—×•×ª מנסה להשתעשע קצת.
+ </div>
+ <br><br><br>
+
+ </body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/testBrowserDiscovery.js b/mobile/android/tests/browser/robocop/testBrowserDiscovery.js
new file mode 100644
index 0000000000..3b3421dc24
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testBrowserDiscovery.js
@@ -0,0 +1,150 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+// We use a global variable to track the <browser> where the tests are happening
+var browser;
+
+function setHandlerFunc(handler, test) {
+ browser.addEventListener("DOMLinkAdded", function linkAdded(event) {
+ browser.removeEventListener("DOMLinkAdded", linkAdded, false);
+ Services.tm.mainThread.dispatch(handler.bind(this, test), Ci.nsIThread.DISPATCH_NORMAL);
+ }, false);
+}
+
+add_test(function setup_browser() {
+ let BrowserApp = Services.wm.getMostRecentWindow("navigator:browser").BrowserApp;
+ do_register_cleanup(function cleanup() {
+ BrowserApp.closeTab(BrowserApp.getTabForBrowser(browser));
+ });
+
+ let url = "http://mochi.test:8888/tests/robocop/link_discovery.html";
+ browser = BrowserApp.addTab(url, { selected: true, parentId: BrowserApp.selectedTab.id }).browser;
+ browser.addEventListener("load", function startTests(event) {
+ browser.removeEventListener("load", startTests, true);
+ Services.tm.mainThread.dispatch(run_next_test, Ci.nsIThread.DISPATCH_NORMAL);
+ }, true);
+});
+
+var searchDiscoveryTests = [
+ { text: "rel search discovered" },
+ { rel: "SEARCH", text: "rel is case insensitive" },
+ { rel: "-search-", pass: false, text: "rel -search- not discovered" },
+ { rel: "foo bar baz search quux", text: "rel may contain additional rels separated by spaces" },
+ { href: "https://not.mozilla.com", text: "HTTPS ok" },
+ { href: "ftp://not.mozilla.com", text: "FTP ok" },
+ { href: "data:text/foo,foo", pass: false, text: "data URI not permitted" },
+ { href: "javascript:alert(0)", pass: false, text: "JS URI not permitted" },
+ { type: "APPLICATION/OPENSEARCHDESCRIPTION+XML", text: "type is case insensitve" },
+ { type: " application/opensearchdescription+xml ", text: "type may contain extra whitespace" },
+ { type: "application/opensearchdescription+xml; charset=utf-8", text: "type may have optional parameters (RFC2046)" },
+ { type: "aapplication/opensearchdescription+xml", pass: false, text: "type should not be loosely matched" },
+ { rel: "search search search", count: 1, text: "only one engine should be added" }
+];
+
+function execute_search_test(test) {
+ if (browser.engines) {
+ let matchCount = (!("count" in test) || browser.engines.length === test.count);
+ let matchTitle = (test.title == browser.engines[0].title);
+ ok(matchCount && matchTitle, test.text);
+ browser.engines = null;
+ } else {
+ ok(!test.pass, test.text);
+ }
+ run_next_test();
+}
+
+function prep_search_test(test) {
+ // Syncrhonously load the search service.
+ Services.search.getVisibleEngines();
+
+ setHandlerFunc(execute_search_test, test);
+
+ let rel = test.rel || "search";
+ let href = test.href || "http://so.not.here.mozilla.com/search.xml";
+ let type = test.type || "application/opensearchdescription+xml";
+ let title = test.title;
+ if (!("pass" in test)) {
+ test.pass = true;
+ }
+
+ let head = browser.contentDocument.getElementById("linkparent");
+ let link = browser.contentDocument.createElement("link");
+ link.rel = rel;
+ link.href = href;
+ link.type = type;
+ link.title = title;
+ head.appendChild(link);
+}
+
+var feedDiscoveryTests = [
+ { text: "rel feed discovered" },
+ { rel: "ALTERNATE", text: "rel is case insensitive" },
+ { rel: "-alternate-", pass: false, text: "rel -alternate- not discovered" },
+ { rel: "foo bar baz alternate quux", text: "rel may contain additional rels separated by spaces" },
+ { href: "https://not.mozilla.com", text: "HTTPS ok" },
+ { href: "ftp://not.mozilla.com", text: "FTP ok" },
+ { href: "data:text/foo,foo", pass: false, text: "data URI not permitted" },
+ { href: "javascript:alert(0)", pass: false, text: "JS URI not permitted" },
+ { type: "application/rss+xml", text: "type can be RSS" },
+ { type: "aPPliCAtion/RSS+xml", text: "type is case insensitve" },
+ { type: " application/atom+xml ", text: "type may contain extra whitespace" },
+ { type: "application/atom+xml; charset=utf-8", text: "type may have optional parameters (RFC2046)" },
+ { type: "aapplication/atom+xml", pass: false, text: "type should not be loosely matched" },
+ { rel: "alternate alternate alternate", count: 1, text: "only one feed should be added" }
+];
+
+function execute_feed_test(test) {
+ if (browser.feeds) {
+ let matchCount = (!("count" in test) || browser.feeds.length === test.count);
+ let matchTitle = (test.title == browser.feeds[0].title);
+ ok(matchCount && matchTitle, test.text);
+ browser.feeds = null;
+ } else {
+ ok(!test.pass, test.text);
+ }
+ run_next_test();
+}
+
+function prep_feed_test(test) {
+ setHandlerFunc(execute_feed_test, test);
+
+ let rel = test.rel || "alternate";
+ let href = test.href || "http://so.not.here.mozilla.com/feed.xml";
+ let type = test.type || "application/atom+xml";
+ let title = test.title;
+ if (!("pass" in test)) {
+ test.pass = true;
+ }
+
+ let head = browser.contentDocument.getElementById("linkparent");
+ let link = browser.contentDocument.createElement("link");
+ link.rel = rel;
+ link.href = href;
+ link.type = type;
+ link.title = title;
+ head.appendChild(link);
+}
+
+var searchTest;
+while ((searchTest = searchDiscoveryTests.shift())) {
+ let title = searchTest.title || searchDiscoveryTests.length;
+ searchTest.title = title;
+ add_test(prep_search_test.bind(this, searchTest));
+}
+
+var feedTest;
+while ((feedTest = feedDiscoveryTests.shift())) {
+ let title = feedTest.title || feedDiscoveryTests.length;
+ feedTest.title = title;
+ add_test(prep_feed_test.bind(this, feedTest));
+}
+
+run_next_test();
diff --git a/mobile/android/tests/browser/robocop/testEventDispatcher.js b/mobile/android/tests/browser/robocop/testEventDispatcher.js
new file mode 100644
index 0000000000..f70d2fdae2
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testEventDispatcher.js
@@ -0,0 +1,44 @@
+Components.utils.import("resource://gre/modules/Messaging.jsm");
+
+var java = new JavaBridge(this);
+
+do_register_cleanup(() => {
+ java.disconnect();
+});
+do_test_pending();
+
+function send_test_message(type) {
+ let innerObject = {
+ boolean: true,
+ booleanArray: [false, true],
+ int: 1,
+ intArray: [2, 3],
+ double: 0.5,
+ doubleArray: [1.5, 2.5],
+ null: null,
+ emptyString: "",
+ string: "foo",
+ stringArray: ["bar", "baz"],
+ }
+
+ // Make a copy
+ let outerObject = JSON.parse(JSON.stringify(innerObject));
+
+ outerObject.type = type;
+ outerObject.object = innerObject;
+ outerObject.objectArray = [null, innerObject];
+
+ Messaging.sendRequest(outerObject);
+}
+
+function send_message_for_response(type, response) {
+ Messaging.sendRequestForResult({
+ type: type,
+ response: response,
+ }).then(result => do_check_eq(result, response),
+ error => do_check_eq(error, response));
+}
+
+function finish_test() {
+ do_test_finished();
+}
diff --git a/mobile/android/tests/browser/robocop/testFilePicker.js b/mobile/android/tests/browser/robocop/testFilePicker.js
new file mode 100644
index 0000000000..69be415a53
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testFilePicker.js
@@ -0,0 +1,73 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+add_test(function filepicker_open() {
+ let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+
+ do_test_pending();
+
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.appendFilter("Martian files", "*.martian");
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+ fp.filterIndex = 0;
+
+ let fpCallback = function(result) {
+ if (result == Ci.nsIFilePicker.returnOK || result == Ci.nsIFilePicker.returnReplace) {
+ do_print("File: " + fp.file.path);
+ is(fp.file.path, "/mnt/sdcard/my-favorite-martian.png", "Retrieve the right martian file!");
+
+ let files = fp.files;
+ while (files.hasMoreElements()) {
+ let file = files.getNext().QueryInterface(Ci.nsIFile);
+ do_print("File: " + file.path);
+ is(file.path, "/mnt/sdcard/my-favorite-martian.png", "Retrieve the right martian file from array!");
+ }
+
+ let file = fp.domFileOrDirectory;
+ do_print("DOMFile: " + file.mozFullPath);
+ is(file.mozFullPath, "/mnt/sdcard/my-favorite-martian.png", "Retrieve the right martian DOM File!");
+
+ let e = fp.domFileOrDirectoryEnumerator;
+ while (e.hasMoreElements()) {
+ let file = e.getNext();
+ do_print("DOMFile: " + file.mozFullPath);
+ is(file.mozFullPath, "/mnt/sdcard/my-favorite-martian.png", "Retrieve the right martian file from domFileOrDirectoryEnumerator array!");
+ }
+
+ do_test_finished();
+
+ run_next_test();
+ }
+ };
+
+ try {
+ fp.init(chromeWin, "Open", Ci.nsIFilePicker.modeOpen);
+ } catch(ex) {
+ ok(false, "Android should support FilePicker.modeOpen: " + ex);
+ }
+ fp.open(fpCallback);
+});
+
+add_test(function filepicker_save() {
+ let failed = false;
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ try {
+ fp.init(null, "Save", Ci.nsIFilePicker.modeSave);
+ } catch(ex) {
+ failed = true;
+ }
+ ok(failed, "Android does not support FilePicker.modeSave");
+
+ run_next_test();
+});
+
+run_next_test();
+
diff --git a/mobile/android/tests/browser/robocop/testFindInPage.js b/mobile/android/tests/browser/robocop/testFindInPage.js
new file mode 100644
index 0000000000..485ae5e4e3
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testFindInPage.js
@@ -0,0 +1,89 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Messaging.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const TEST_URL = "http://mochi.test:8888/tests/robocop/robocop_text_page.html";
+
+function promiseBrowserEvent(browser, eventType) {
+ return new Promise((resolve) => {
+ function handle(event) {
+ do_print("Received event " + eventType + " from browser");
+ browser.removeEventListener(eventType, handle, true);
+ resolve(event);
+ }
+
+ browser.addEventListener(eventType, handle, true);
+ do_print("Now waiting for " + eventType + " event from browser");
+ });
+}
+
+function openTabWithUrl(url) {
+ do_print("Going to open " + url);
+ let browserApp = Services.wm.getMostRecentWindow("navigator:browser").BrowserApp;
+ let browser = browserApp.addTab(url, { selected: true, parentId: browserApp.selectedTab.id }).browser;
+
+ return promiseBrowserEvent(browser, "load")
+ .then(() => { return browser; });
+}
+
+function findInPage(browser, text, nrOfMatches) {
+ let repaintPromise = promiseBrowserEvent(browser, "MozAfterPaint");
+ do_print("Send findInPageMessage: " + text + " nth: " + nrOfMatches);
+ Messaging.sendRequest({ type: "Test:FindInPage", text: text, nrOfMatches: nrOfMatches });
+ return repaintPromise;
+}
+
+function closeFindInPage(browser) {
+ let repaintPromise = promiseBrowserEvent(browser, "MozAfterPaint");
+ do_print("Send closeFindInPageMessage");
+ Messaging.sendRequest({ type: "Test:CloseFindInPage" });
+ return repaintPromise;
+}
+
+function assertSelection(document, expectedSelection = false, expectedAnchorText = false) {
+ let sel = document.getSelection();
+ if (!expectedSelection) {
+ do_print("Assert empty selection");
+ do_check_eq(sel.toString(), "");
+ } else {
+ do_print("Assert selection to be " + expectedSelection);
+ do_check_eq(sel.toString(), expectedSelection);
+ }
+ if (expectedAnchorText) {
+ do_print("Assert anchor text to be " + expectedAnchorText);
+ do_check_eq(sel.anchorNode.textContent, expectedAnchorText);
+ }
+}
+
+add_task(function* testFindInPage() {
+ let browser = yield openTabWithUrl(TEST_URL);
+ let document = browser.contentDocument;
+
+ yield findInPage(browser, "Robocoop", 1);
+ assertSelection(document);
+
+ yield closeFindInPage(browser);
+ assertSelection(document);
+
+ yield findInPage(browser, "Robocop", 1);
+ assertSelection(document, "Robocop", " Robocop 1 ");
+
+ yield closeFindInPage(browser);
+ assertSelection(document);
+
+ yield findInPage(browser, "Robocop", 3);
+ assertSelection(document, "Robocop", " Robocop 3 ");
+
+ yield closeFindInPage(browser);
+ assertSelection(document);
+});
+
+run_next_test();
diff --git a/mobile/android/tests/browser/robocop/testGeckoRequest.js b/mobile/android/tests/browser/robocop/testGeckoRequest.js
new file mode 100644
index 0000000000..cedaf825ce
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testGeckoRequest.js
@@ -0,0 +1,40 @@
+Components.utils.import("resource://gre/modules/Messaging.jsm");
+
+var java = new JavaBridge(this);
+
+do_register_cleanup(() => {
+ java.disconnect();
+});
+do_test_pending();
+
+function add_request_listener(message) {
+ Messaging.addListener(function (data) {
+ return { result: data + "bar" };
+ }, message);
+}
+
+function add_exception_listener(message) {
+ Messaging.addListener(function (data) {
+ throw "error!";
+ }, message);
+}
+
+function add_second_request_listener(message) {
+ let exceptionCaught = false;
+
+ try {
+ Messaging.addListener(() => {}, message);
+ } catch (e) {
+ exceptionCaught = true;
+ }
+
+ do_check_true(exceptionCaught);
+}
+
+function remove_request_listener(message) {
+ Messaging.removeListener(message);
+}
+
+function finish_test() {
+ do_test_finished();
+}
diff --git a/mobile/android/tests/browser/robocop/testHistoryService.js b/mobile/android/tests/browser/robocop/testHistoryService.js
new file mode 100644
index 0000000000..612928c4c6
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testHistoryService.js
@@ -0,0 +1,128 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+// Make the timer global so it doesn't get GC'd
+var gTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+
+function sleep(wait) {
+ return new Promise((resolve, reject) => {
+ do_print("sleep start");
+ gTimer.initWithCallback({
+ notify: function () {
+ do_print("sleep end");
+ resolve();
+ },
+ }, wait, gTimer.TYPE_ONE_SHOT);
+ });
+}
+
+function promiseLoadEvent(browser, url, eventType="load") {
+ return new Promise((resolve, reject) => {
+ do_print("Wait browser event: " + eventType);
+
+ function handle(event) {
+ // Since we'll be redirecting, don't make assumptions about the given URL and the loaded URL
+ if (event.target != browser.contentDocument || event.target.location.href == "about:blank") {
+ do_print("Skipping spurious '" + eventType + "' event" + " for " + event.target.location.href);
+ return;
+ }
+
+ browser.removeEventListener(eventType, handle, true);
+ do_print("Browser event received: " + eventType);
+ resolve(event);
+ }
+
+ browser.addEventListener(eventType, handle, true);
+ if (url) {
+ browser.loadURI(url);
+ }
+ });
+}
+
+// Wait 6 seconds for the pending visits to flush (which should happen in 3 seconds)
+const PENDING_VISIT_WAIT = 6000;
+// Longer wait required after first load
+const PENDING_VISIT_WAIT_LONG = 20000;
+
+// Manage the saved history visits so we can compare in the tests
+var gVisitURLs = [];
+function visitObserver(subject, topic, data) {
+ let uri = subject.QueryInterface(Ci.nsIURI);
+ do_print("Observer: " + uri.spec);
+ gVisitURLs.push(uri.spec);
+};
+
+// Track the <browser> where the tests are happening
+var gBrowser;
+
+add_test(function setup_browser() {
+ let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let BrowserApp = chromeWin.BrowserApp;
+
+ do_register_cleanup(function cleanup() {
+ Services.obs.removeObserver(visitObserver, "link-visited");
+ BrowserApp.closeTab(BrowserApp.getTabForBrowser(gBrowser));
+ });
+
+ Services.obs.addObserver(visitObserver, "link-visited", false);
+
+ // Load a blank page
+ let url = "about:blank";
+ gBrowser = BrowserApp.addTab(url, { selected: true, parentId: BrowserApp.selectedTab.id }).browser;
+ gBrowser.addEventListener("load", function startTests(event) {
+ gBrowser.removeEventListener("load", startTests, true);
+ Services.tm.mainThread.dispatch(run_next_test, Ci.nsIThread.DISPATCH_NORMAL);
+ }, true);
+});
+
+add_task(function* () {
+ // Wait for any initial page loads to be saved to history
+ yield sleep(PENDING_VISIT_WAIT);
+
+ // Load a simple HTML page with no redirects
+ gVisitURLs = [];
+ yield promiseLoadEvent(gBrowser, "http://example.org/tests/robocop/robocop_blank_01.html");
+ yield sleep(PENDING_VISIT_WAIT_LONG);
+
+ do_print("visit counts: " + gVisitURLs.length);
+ ok(gVisitURLs.length == 1, "Simple visit makes 1 history item");
+
+ do_print("visit URL: " + gVisitURLs[0]);
+ ok(gVisitURLs[0] == "http://example.org/tests/robocop/robocop_blank_01.html", "Simple visit makes final history item");
+
+ // Load a simple HTML page via a 301 temporary redirect
+ gVisitURLs = [];
+ yield promiseLoadEvent(gBrowser, "http://example.org/tests/robocop/simple_redirect.sjs?http://example.org/tests/robocop/robocop_blank_02.html");
+ yield sleep(PENDING_VISIT_WAIT);
+
+ do_print("visit counts: " + gVisitURLs.length);
+ ok(gVisitURLs.length == 1, "Simple 301 redirect makes 1 history item");
+
+ do_print("visit URL: " + gVisitURLs[0]);
+ ok(gVisitURLs[0] == "http://example.org/tests/robocop/robocop_blank_02.html", "Simple 301 redirect makes final history item");
+
+ // Load a simple HTML page via a JavaScript redirect
+ gVisitURLs = [];
+ yield promiseLoadEvent(gBrowser, "http://example.org/tests/robocop/javascript_redirect.sjs?http://example.org/tests/robocop/robocop_blank_03.html");
+ yield sleep(PENDING_VISIT_WAIT);
+
+ do_print("visit counts: " + gVisitURLs.length);
+ ok(gVisitURLs.length == 2, "JavaScript redirect makes 2 history items");
+
+ do_print("visit URL 1: " + gVisitURLs[0]);
+ ok(gVisitURLs[0] == "http://example.org/tests/robocop/javascript_redirect.sjs?http://example.org/tests/robocop/robocop_blank_03.html", "JavaScript redirect makes intermediate history item");
+
+ do_print("visit URL 2: " + gVisitURLs[1]);
+ ok(gVisitURLs[1] == "http://example.org/tests/robocop/robocop_blank_03.html", "JavaScript redirect makes final history item");
+});
+
+run_next_test();
diff --git a/mobile/android/tests/browser/robocop/testJavascriptBridge.js b/mobile/android/tests/browser/robocop/testJavascriptBridge.js
new file mode 100644
index 0000000000..3bfd89f889
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testJavascriptBridge.js
@@ -0,0 +1,52 @@
+var java = new JavaBridge(this);
+var javaResponded = false;
+
+do_register_cleanup(() => {
+ java.disconnect();
+});
+do_test_pending();
+
+function check_js_int_arg(int1) {
+ // Sync call from Java
+ do_check_eq(int1, 1);
+ java.asyncCall("checkJavaIntArg", 2);
+}
+
+function check_js_double_arg(double3) {
+ // Sync call from Java
+ do_check_eq(double3, 3.0);
+ java.asyncCall("checkJavaDoubleArg", 4.0);
+}
+
+function check_js_boolean_arg(boolfalse) {
+ // Sync call from Java
+ do_check_eq(boolfalse, false);
+ java.asyncCall("checkJavaBooleanArg", true);
+}
+
+function check_js_string_arg(stringfoo) {
+ do_check_eq(stringfoo, "foo");
+ java.asyncCall("checkJavaStringArg", "bar");
+}
+
+function check_js_object_arg(obj) {
+ // Sync call from Java
+ do_check_eq(obj.caller, "java");
+ java.asyncCall("checkJavaObjectArg", {caller: "js"});
+}
+
+function check_js_sync_call() {
+ // Sync call from Java
+ java.syncCall("doJSSyncCall");
+ // respond_to_js_sync_call should have run by now because
+ // do_js_sync_call calls it from Java code
+ do_check_true(javaResponded);
+
+ java.asyncCall("checkJSSyncCallReceived");
+ // End of test
+ do_test_finished();
+}
+
+function respond_to_js_sync_call() {
+ javaResponded = true;
+}
diff --git a/mobile/android/tests/browser/robocop/testReaderCacheMigration.js b/mobile/android/tests/browser/robocop/testReaderCacheMigration.js
new file mode 100644
index 0000000000..dbd5ae4329
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testReaderCacheMigration.js
@@ -0,0 +1,23 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/ReaderMode.jsm");
+
+var java = new JavaBridge(this);
+
+do_register_cleanup(() => {
+ java.disconnect();
+});
+do_test_pending();
+
+function check_hashed_path_matches(url, hashedPath) {
+ var jsHashedPath = ReaderMode._toHashedPath(url);
+ do_check_eq(hashedPath, jsHashedPath);
+}
+
+function finish_test() {
+ do_test_finished();
+} \ No newline at end of file
diff --git a/mobile/android/tests/browser/robocop/testReadingListCache.js b/mobile/android/tests/browser/robocop/testReadingListCache.js
new file mode 100644
index 0000000000..d438dfb1ef
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testReadingListCache.js
@@ -0,0 +1,126 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*globals ReaderMode */
+
+var { utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/ReaderMode.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+var Reader = Services.wm.getMostRecentWindow("navigator:browser").Reader;
+
+const URL_PREFIX = "http://mochi.test:8888/tests/robocop/reader_mode_pages/";
+
+var TEST_PAGES = [
+ {
+ url: URL_PREFIX + "basic_article.html",
+ expected: {
+ title: "Article title",
+ byline: "by Jane Doe",
+ excerpt: "This is the article description.",
+ }
+ },
+ {
+ url: URL_PREFIX + "not_an_article.html",
+ expected: null
+ },
+ {
+ url: URL_PREFIX + "developer.mozilla.org/en/XULRunner/Build_Instructions.html",
+ expected: {
+ title: "Building XULRunner",
+ byline: null,
+ excerpt: "XULRunner is built using basically the same process as Firefox or other applications. Please read and follow the general Build Documentation for instructions on how to get sources and set up build prerequisites.",
+ }
+ },
+];
+
+add_task(function* test_article_not_found() {
+ let article = yield ReaderMode.getArticleFromCache(TEST_PAGES[0].url);
+ do_check_eq(article, null);
+});
+
+add_task(function* test_store_article() {
+ // Create an article object to store in the cache.
+ yield ReaderMode.storeArticleInCache({
+ url: TEST_PAGES[0].url,
+ content: "Lorem ipsum",
+ title: TEST_PAGES[0].expected.title,
+ byline: TEST_PAGES[0].expected.byline,
+ excerpt: TEST_PAGES[0].expected.excerpt,
+ });
+
+ let article = yield ReaderMode.getArticleFromCache(TEST_PAGES[0].url);
+ checkArticle(article, TEST_PAGES[0]);
+});
+
+add_task(function* test_remove_article() {
+ yield ReaderMode.removeArticleFromCache(TEST_PAGES[0].url);
+ let article = yield ReaderMode.getArticleFromCache(TEST_PAGES[0].url);
+ do_check_eq(article, null);
+});
+
+add_task(function* test_parse_articles() {
+ for (let testcase of TEST_PAGES) {
+ let article = yield ReaderMode.downloadAndParseDocument(testcase.url);
+ checkArticle(article, testcase);
+ }
+});
+
+add_task(function* test_migrate_cache() {
+ // Store an article in the old indexedDB reader mode cache.
+ let cacheDB = yield new Promise((resolve, reject) => {
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+ let request = win.indexedDB.open("about:reader", 1);
+ request.onerror = event => reject(request.error);
+
+ // This will always happen because there is no pre-existing data store.
+ request.onupgradeneeded = event => {
+ let cacheDB = event.target.result;
+ cacheDB.createObjectStore("articles", { keyPath: "url" });
+ };
+
+ request.onsuccess = event => resolve(event.target.result);
+ });
+
+ yield new Promise((resolve, reject) => {
+ let transaction = cacheDB.transaction(["articles"], "readwrite");
+ let store = transaction.objectStore("articles");
+
+ let request = store.add({
+ url: TEST_PAGES[0].url,
+ content: "Lorem ipsum",
+ title: TEST_PAGES[0].expected.title,
+ byline: TEST_PAGES[0].expected.byline,
+ excerpt: TEST_PAGES[0].expected.excerpt,
+ });
+ request.onerror = event => reject(request.error);
+ request.onsuccess = event => resolve();
+ });
+
+ // Migrate the cache.
+ yield Reader.migrateCache();
+
+ // Check to make sure the article made it into the new cache.
+ let article = yield ReaderMode.getArticleFromCache(TEST_PAGES[0].url);
+ checkArticle(article, TEST_PAGES[0]);
+});
+
+function checkArticle(article, testcase) {
+ if (testcase.expected == null) {
+ do_check_eq(article, null);
+ return;
+ }
+
+ do_check_neq(article, null);
+ do_check_eq(!!article.content, true); // A bit of a hack to avoid spamming the test log.
+ do_check_eq(article.url, testcase.url);
+ do_check_eq(article.title, testcase.expected.title);
+ do_check_eq(article.byline, testcase.expected.byline);
+ do_check_eq(article.excerpt, testcase.expected.excerpt);
+}
+
+run_next_test();
diff --git a/mobile/android/tests/browser/robocop/testRuntimePermissionsAPI.js b/mobile/android/tests/browser/robocop/testRuntimePermissionsAPI.js
new file mode 100644
index 0000000000..d3f34b87d3
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testRuntimePermissionsAPI.js
@@ -0,0 +1,20 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "RuntimePermissions", "resource://gre/modules/RuntimePermissions.jsm");
+
+add_task(function* test_snackbar_api() {
+ RuntimePermissions.waitForPermissions([
+ RuntimePermissions.CAMERA,
+ RuntimePermissions.RECORD_AUDIO,
+ RuntimePermissions.WRITE_EXTERNAL_STORAGE
+ ]);
+});
+
+run_next_test();
diff --git a/mobile/android/tests/browser/robocop/testSnackbarAPI.js b/mobile/android/tests/browser/robocop/testSnackbarAPI.js
new file mode 100644
index 0000000000..1031528dfe
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testSnackbarAPI.js
@@ -0,0 +1,21 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+
+add_task(function* test_snackbar_api() {
+ Snackbars.show("This is a Snackbar", Snackbars.LENGTH_INDEFINITE, {
+ action: {
+ label: "Click me",
+ callback: function () {}
+ }
+ });
+});
+
+run_next_test();
diff --git a/mobile/android/tests/browser/robocop/testTrackingProtection.js b/mobile/android/tests/browser/robocop/testTrackingProtection.js
new file mode 100644
index 0000000000..d81efd6c66
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testTrackingProtection.js
@@ -0,0 +1,166 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+
+function promiseLoadEvent(browser, url, eventType="load", runBeforeLoad) {
+ return new Promise((resolve, reject) => {
+ do_print("Wait browser event: " + eventType);
+
+ function handle(event) {
+ if (event.target != browser.contentDocument || event.target.location.href == "about:blank" || (url && event.target.location.href != url)) {
+ do_print("Skipping spurious '" + eventType + "' event" + " for " + event.target.location.href);
+ return;
+ }
+
+ browser.removeEventListener(eventType, handle, true);
+ do_print("Browser event received: " + eventType);
+ resolve(event);
+ }
+
+ browser.addEventListener(eventType, handle, true);
+
+ if (runBeforeLoad) {
+ runBeforeLoad();
+ }
+ if (url) {
+ browser.loadURI(url);
+ }
+ });
+}
+
+// Test that the Tracking Protection is active and has the correct state when
+// tracking content is blocked (Bug 1063831)
+
+// Code is mostly stolen from:
+// http://dxr.mozilla.org/mozilla-central/source/browser/base/content/test/general/browser_trackingUI.js
+
+var TABLE = "urlclassifier.trackingTable";
+
+// Update tracking database
+function doUpdate() {
+ // Add some URLs to the tracking database (to be blocked)
+ var testData = "tracking.example.com/";
+ var testUpdate =
+ "n:1000\ni:test-track-simple\nad:1\n" +
+ "a:524:32:" + testData.length + "\n" +
+ testData;
+
+ let dbService = Cc["@mozilla.org/url-classifier/dbservice;1"].getService(Ci.nsIUrlClassifierDBService);
+
+ return new Promise((resolve, reject) => {
+ let listener = {
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) || iid.equals(Ci.nsIUrlClassifierUpdateObserver))
+ return this;
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+ updateUrlRequested: function(url) { },
+ streamFinished: function(status) { },
+ updateError: function(errorCode) {
+ ok(false, "Couldn't update classifier.");
+ resolve();
+ },
+ updateSuccess: function(requestedTimeout) {
+ resolve();
+ }
+ };
+
+ dbService.beginUpdate(listener, "test-track-simple", "");
+ dbService.beginStream("", "");
+ dbService.updateStream(testUpdate);
+ dbService.finishStream();
+ dbService.finishUpdate();
+ });
+}
+
+var BrowserApp = Services.wm.getMostRecentWindow("navigator:browser").BrowserApp;
+
+// Tests the tracking protection UI in private browsing. By default, tracking protection is
+// enabled in private browsing ("privacy.trackingprotection.pbmode.enabled").
+add_task(function* test_tracking_pb() {
+ // Load a blank page
+ let browser = BrowserApp.addTab("about:blank", { selected: true, parentId: BrowserApp.selectedTab.id, isPrivate: true }).browser;
+ yield new Promise((resolve, reject) => {
+ browser.addEventListener("load", function startTests(event) {
+ browser.removeEventListener("load", startTests, true);
+ Services.tm.mainThread.dispatch(resolve, Ci.nsIThread.DISPATCH_NORMAL);
+ }, true);
+ });
+
+ // Populate and use 'test-track-simple' for tracking protection lookups
+ Services.prefs.setCharPref(TABLE, "test-track-simple");
+ yield doUpdate();
+
+ // Point tab to a test page NOT containing tracking elements
+ yield promiseLoadEvent(browser, "http://tracking.example.org/tests/robocop/tracking_good.html");
+ Messaging.sendRequest({ type: "Test:Expected", expected: "unknown" });
+
+ // Point tab to a test page containing tracking elements
+ yield promiseLoadEvent(browser, "http://tracking.example.org/tests/robocop/tracking_bad.html");
+ Messaging.sendRequest({ type: "Test:Expected", expected: "tracking_content_blocked" });
+
+ // Simulate a click on the "Disable protection" button in the site identity popup.
+ // We need to wait for a "load" event because "Session:Reload" will cause a full page reload.
+ yield promiseLoadEvent(browser, undefined, undefined, () => {
+ Services.obs.notifyObservers(null, "Session:Reload", "{\"allowContent\":true,\"contentType\":\"tracking\"}");
+ });
+ Messaging.sendRequest({ type: "Test:Expected", expected: "tracking_content_loaded" });
+
+ // Simulate a click on the "Enable protection" button in the site identity popup.
+ yield promiseLoadEvent(browser, undefined, undefined, () => {
+ Services.obs.notifyObservers(null, "Session:Reload", "{\"allowContent\":false,\"contentType\":\"tracking\"}");
+ });
+ Messaging.sendRequest({ type: "Test:Expected", expected: "tracking_content_blocked" });
+
+ // Disable tracking protection to make sure we don't show the UI when the pref is disabled.
+ Services.prefs.setBoolPref("privacy.trackingprotection.pbmode.enabled", false);
+
+ // Point tab to a test page containing tracking elements
+ yield promiseLoadEvent(browser, "http://tracking.example.org/tests/robocop/tracking_bad.html");
+ Messaging.sendRequest({ type: "Test:Expected", expected: "unknown" });
+
+ // Point tab to a test page NOT containing tracking elements
+ yield promiseLoadEvent(browser, "http://tracking.example.org/tests/robocop/tracking_good.html");
+ Messaging.sendRequest({ type: "Test:Expected", expected: "unknown" });
+
+ // Reset the pref before the next testcase
+ Services.prefs.clearUserPref("privacy.trackingprotection.pbmode.enabled");
+});
+
+add_task(function* test_tracking_not_pb() {
+ // Load a blank page
+ let browser = BrowserApp.addTab("about:blank", { selected: true }).browser;
+ yield new Promise((resolve, reject) => {
+ browser.addEventListener("load", function startTests(event) {
+ browser.removeEventListener("load", startTests, true);
+ Services.tm.mainThread.dispatch(resolve, Ci.nsIThread.DISPATCH_NORMAL);
+ }, true);
+ });
+
+ // Point tab to a test page NOT containing tracking elements
+ yield promiseLoadEvent(browser, "http://tracking.example.org/tests/robocop/tracking_good.html");
+ Messaging.sendRequest({ type: "Test:Expected", expected: "unknown" });
+
+ // Point tab to a test page containing tracking elements (tracking protection UI *should not* be shown)
+ yield promiseLoadEvent(browser, "http://tracking.example.org/tests/robocop/tracking_bad.html");
+ Messaging.sendRequest({ type: "Test:Expected", expected: "unknown" });
+
+ // Enable tracking protection in normal tabs
+ Services.prefs.setBoolPref("privacy.trackingprotection.enabled", true);
+
+ // Point tab to a test page containing tracking elements (tracking protection UI *should* be shown)
+ yield promiseLoadEvent(browser, "http://tracking.example.org/tests/robocop/tracking_bad.html");
+ Messaging.sendRequest({ type: "Test:Expected", expected: "tracking_content_blocked" });
+});
+
+run_next_test();
diff --git a/mobile/android/tests/browser/robocop/testUITelemetry.js b/mobile/android/tests/browser/robocop/testUITelemetry.js
new file mode 100644
index 0000000000..5edf06f195
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testUITelemetry.js
@@ -0,0 +1,154 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const EVENT_TEST1 = "_test_event_1.1";
+const EVENT_TEST2 = "_test_event_2.1";
+const EVENT_TEST3 = "_test_event_3.1";
+const EVENT_TEST4 = "_test_event_4.1";
+
+const METHOD_TEST1 = "_test_method_1";
+const METHOD_TEST2 = "_test_method_2";
+
+const METHOD_NONE = null;
+
+const REASON_TEST1 = "_test_reason_1";
+const REASON_TEST2 = "_test_reason_2";
+
+const SESSION_STARTED_TWICE = "_test_session_started_twice.1";
+const SESSION_STOPPED_TWICE = "_test_session_stopped_twice.1";
+
+function do_check_array_eq(a1, a2) {
+ do_check_eq(a1.length, a2.length);
+ for (let i = 0; i < a1.length; ++i) {
+ do_check_eq(a1[i], a2[i]);
+ }
+}
+
+/**
+ * Asserts that the given measurements are equal. Assumes that measurements
+ * of type "event" have their sessions arrays sorted.
+ */
+function do_check_measurement_eq(m1, m2) {
+ do_check_eq(m1.type, m2.type);
+
+ switch (m1.type) {
+ case "event":
+ do_check_eq(m1.action, m2.action);
+ do_check_eq(m1.method, m2.method);
+ do_check_array_eq(m1.sessions, m2.sessions);
+ do_check_eq(m1.extras, m2.extras);
+ break;
+
+ case "session":
+ do_check_eq(m1.name, m2.name);
+ do_check_eq(m1.reason, m2.reason);
+ break;
+
+ default:
+ do_throw("Unknown event type: " + m1.type);
+ }
+}
+
+function getObserver() {
+ let bridge = Cc["@mozilla.org/android/bridge;1"]
+ .getService(Ci.nsIAndroidBridge);
+ let obsXPCOM = bridge.browserApp.getUITelemetryObserver();
+ do_check_true(!!obsXPCOM);
+ return obsXPCOM.wrappedJSObject;
+}
+
+/**
+ * The following event test will fail if telemetry isn't enabled. The Java-side
+ * part of this test should have turned it on; fail if it didn't work.
+ */
+add_test(function test_enabled() {
+ let obs = getObserver();
+ do_check_true(!!obs);
+ do_check_true(obs.enabled);
+ run_next_test();
+});
+
+add_test(function test_telemetry_events() {
+ let expected = expectedArraysToObjs([
+ ["event", EVENT_TEST1, METHOD_TEST1, [], undefined],
+ ["event", EVENT_TEST2, METHOD_TEST1, [SESSION_STARTED_TWICE], undefined],
+ ["event", EVENT_TEST2, METHOD_TEST2, [SESSION_STARTED_TWICE], undefined],
+ ["event", EVENT_TEST3, METHOD_TEST1, [SESSION_STARTED_TWICE, SESSION_STOPPED_TWICE], "foobarextras"],
+ ["session", SESSION_STARTED_TWICE, REASON_TEST1],
+ ["event", EVENT_TEST4, METHOD_TEST1, [SESSION_STOPPED_TWICE], "barextras"],
+ ["session", SESSION_STOPPED_TWICE, REASON_TEST2],
+ ["event", EVENT_TEST1, METHOD_NONE, [], undefined],
+ ]);
+
+ let clearMeasurements = false;
+ let obs = getObserver();
+ let measurements = removeNonTestMeasurements(obs.getUIMeasurements(clearMeasurements));
+
+ measurements.forEach(function (m, i) {
+ if (m.type === "event") {
+ m.sessions = removeNonTestSessions(m.sessions);
+ m.sessions.sort(); // Mutates.
+ }
+
+ do_check_measurement_eq(expected[i], m);
+ });
+
+ expected.forEach(function (m, i) {
+ do_check_measurement_eq(m, measurements[i]);
+ });
+
+ run_next_test();
+});
+
+/**
+ * Converts the expected value arrays to objects,
+ * for less typing when initializing the expected arrays.
+ */
+function expectedArraysToObjs(expectedArrays) {
+ return expectedArrays.map(function (arr) {
+ let type = arr[0];
+ if (type === "event") {
+ return {
+ type: type,
+ action: arr[1],
+ method: arr[2],
+ sessions: arr[3].sort(), // Sort, just in case it's not sorted by hand!
+ extras: arr[4],
+ };
+
+ } else if (type === "session") {
+ return {
+ type: type,
+ name: arr[1],
+ reason: arr[2],
+ };
+ }
+ });
+}
+
+function removeNonTestMeasurements(measurements) {
+ return measurements.filter(function (measurement) {
+ if (measurement.type === "event") {
+ return measurement.action.startsWith("_test_event_");
+ } else if (measurement.type === "session") {
+ return measurement.name.startsWith("_test_session_");
+ }
+ return false;
+ });
+}
+
+function removeNonTestSessions(sessions) {
+ return sessions.filter(function (sessionName) {
+ return sessionName.startsWith("_test_session_");
+ });
+}
+
+run_next_test();
diff --git a/mobile/android/tests/browser/robocop/testUnifiedTelemetryClientId.js b/mobile/android/tests/browser/robocop/testUnifiedTelemetryClientId.js
new file mode 100644
index 0000000000..d574aaef14
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testUnifiedTelemetryClientId.js
@@ -0,0 +1,50 @@
+var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import('resource://gre/modules/ClientID.jsm');
+
+var java = new JavaBridge(this);
+do_register_cleanup(() => {
+ java.disconnect();
+});
+do_test_pending();
+
+var isClientIDSet;
+var clientID;
+
+var isResetDone;
+
+function getAsyncClientId() {
+ isClientIDSet = false;
+ ClientID.getClientID().then(function (retClientID) {
+ // Ideally, we'd directly send the client ID back to Java but Java won't listen for
+ // js messages after we return from the containing function (bug 1253467).
+ //
+ // Note that my brief attempts to get synchronous Promise resolution (via Task.jsm)
+ // working failed - I have other things to focus on.
+ clientID = retClientID;
+ isClientIDSet = true;
+ }, function (fail) {
+ // Since Java doesn't listen to our messages (bug 1253467), I don't expect
+ // this throw to work correctly but we should timeout in Java.
+ do_throw('Could not retrieve client ID: ' + fail);
+ });
+}
+
+function pollGetAsyncClientId() {
+ java.asyncCall('blockingFromJsResponseString', isClientIDSet, clientID);
+}
+
+function getAsyncReset() {
+ isResetDone = false;
+ ClientID._reset().then(function () {
+ isResetDone = true;
+ });
+}
+
+function pollGetAsyncReset() {
+ java.asyncCall('blockingFromJsResponseString', isResetDone, '');
+}
+
+function endTest() {
+ do_test_finished();
+}
diff --git a/mobile/android/tests/browser/robocop/testVideoControls.js b/mobile/android/tests/browser/robocop/testVideoControls.js
new file mode 100644
index 0000000000..e0a41b5b69
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testVideoControls.js
@@ -0,0 +1,157 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/SimpleServiceDiscovery.jsm");
+
+// The chrome window
+var chromeWin;
+
+// Track the <browser> where the tests are happening
+var browser;
+
+// The document of the video_controls web content
+var contentDocument;
+
+// The <video> we will be testing
+var video;
+
+add_test(function setup_browser() {
+ chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let BrowserApp = chromeWin.BrowserApp;
+
+ do_register_cleanup(function cleanup() {
+ BrowserApp.closeTab(BrowserApp.getTabForBrowser(browser));
+ });
+
+ // Load our test web page with <video> elements
+ let url = "http://mochi.test:8888/tests/robocop/video_controls.html";
+ browser = BrowserApp.addTab(url, { selected: true, parentId: BrowserApp.selectedTab.id }).browser;
+ browser.addEventListener("load", function startTests(event) {
+ browser.removeEventListener("load", startTests, true);
+ contentDocument = browser.contentDocument;
+
+ video = contentDocument.getElementById("video");
+ ok(video, "Found the video element");
+
+ Services.tm.mainThread.dispatch(run_next_test, Ci.nsIThread.DISPATCH_NORMAL);
+ }, true);
+});
+
+add_test(function test_webm() {
+ // Load the test video
+ video.src = "http://mochi.test:8888/tests/robocop/video-pattern.webm";
+
+ Services.tm.mainThread.dispatch(testLoad, Ci.nsIThread.DISPATCH_NORMAL);
+});
+
+add_test(function test_ogg() {
+ // Load the test video
+ video.src = "http://mochi.test:8888/tests/robocop/video-pattern.ogg";
+
+ Services.tm.mainThread.dispatch(testLoad, Ci.nsIThread.DISPATCH_NORMAL);
+});
+
+function getButtonByAttribute(aName, aValue) {
+ let domUtil = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+ let kids = domUtil.getChildrenForNode(video, true);
+ let videocontrols = kids[1];
+ return contentDocument.getAnonymousElementByAttribute(videocontrols, aName, aValue);
+}
+
+function getPixelColor(aCanvas, aX, aY) {
+ let cx = aCanvas.getContext("2d");
+ let pixel = cx.getImageData(aX, aY, 1, 1);
+ return {
+ r: pixel.data[0],
+ g: pixel.data[1],
+ b: pixel.data[2],
+ a: pixel.data[3]
+ };
+}
+
+function testLoad() {
+ // The video is not auto-play, so it starts paused
+ let playButton = getButtonByAttribute("class", "playButton");
+ ok(playButton.getAttribute("paused") == "true", "Play button is paused");
+
+ // Let's start playing it
+ video.play();
+ video.addEventListener("play", testPlay, false);
+}
+
+function testPlay(aEvent) {
+ video.removeEventListener("play", testPlay, false);
+ let playButton = getButtonByAttribute("class", "playButton");
+ ok(playButton.hasAttribute("paused") == false, "Play button is not paused");
+
+ // Let the video play for 2 seconds, then pause it
+ chromeWin.setTimeout(function() {
+ video.pause();
+ video.addEventListener("pause", testPause, false);
+ }, 2000);
+}
+
+function testPause(aEvent) {
+ video.removeEventListener("pause", testPause, false);
+
+ // If we got here, the play button should be paused
+ let playButton = getButtonByAttribute("class", "playButton");
+ ok(playButton.getAttribute("paused") == "true", "Play button is paused again");
+
+ // Let's grab an image of the frame and test it
+ let width = 640;
+ let height = 480;
+ let canvas = contentDocument.getElementById("canvas");
+ canvas.width = width;
+ canvas.height = height;
+ canvas.getContext("2d").drawImage(video, 0, 0, width, height);
+
+ // Let's grab some pixel colors to verify we actually displayed a video.
+ // For some reason the canvas copy of the frame does not recreate the colors
+ // exactly for some devices. To keep things passing on automation and local
+ // runs, we fudge it.
+
+ // The purpose of this code is not to test drawImage, but whether a video
+ // frame was displayed.
+ const MAX_COLOR = 235; // ideally, 255
+ const MIN_COLOR = 20; // ideally, 0
+
+ let bar1 = getPixelColor(canvas, 45, 10);
+ do_print("Color at (45, 10): " + JSON.stringify(bar1));
+ ok(bar1.r >= MAX_COLOR && bar1.g >= MAX_COLOR && bar1.b >= MAX_COLOR, "Bar 1 is white");
+
+ let bar2 = getPixelColor(canvas, 135, 10);
+ do_print("Color at (135, 10): " + JSON.stringify(bar2));
+ ok(bar2.r >= MAX_COLOR && bar2.g >= MAX_COLOR && bar2.b <= MIN_COLOR, "Bar 2 is yellow");
+
+ let bar3 = getPixelColor(canvas, 225, 10);
+ do_print("Color at (225, 10): " + JSON.stringify(bar3));
+ ok(bar3.r <= MIN_COLOR && bar3.g >= MAX_COLOR && bar3.b >= MAX_COLOR, "Bar 3 is Cyan");
+
+ let bar4 = getPixelColor(canvas, 315, 10);
+ do_print("Color at (315, 10): " + JSON.stringify(bar4));
+ ok(bar4.r <= MIN_COLOR && bar4.g >= MAX_COLOR && bar4.b <= MIN_COLOR, "Bar 4 is Green");
+
+ let bar5 = getPixelColor(canvas, 405, 10);
+ do_print("Color at (405, 10): " + JSON.stringify(bar5));
+ ok(bar5.r >= MAX_COLOR && bar5.g <= MIN_COLOR && bar5.b >= MAX_COLOR, "Bar 5 is Purple");
+
+ let bar6 = getPixelColor(canvas, 495, 10);
+ do_print("Color at (495, 10): " + JSON.stringify(bar6));
+ ok(bar6.r >= MAX_COLOR && bar6.g <= MIN_COLOR && bar6.b <= MIN_COLOR, "Bar 6 is Red");
+
+ let bar7 = getPixelColor(canvas, 585, 10);
+ do_print("Color at (585, 10): " + JSON.stringify(bar7));
+ ok(bar7.r <= MIN_COLOR && bar7.g <= MIN_COLOR && bar7.b >= MAX_COLOR, "Bar 7 is Blue");
+
+ run_next_test();
+}
+
+run_next_test();
diff --git a/mobile/android/tests/browser/robocop/test_viewport.sjs b/mobile/android/tests/browser/robocop/test_viewport.sjs
new file mode 100644
index 0000000000..aa83d6cbdd
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/test_viewport.sjs
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ function decodeQuery(query) {
+ let result = {};
+ query.split("&").forEach(function(pair) {
+ let [key, val] = pair.split("=");
+ result[key] = decodeURIComponent(val);
+ });
+ return result;
+ }
+
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+
+ let params = decodeQuery(request.queryString || "");
+
+ response.write('<html>\n' +
+ '<head>\n' +
+ '<title>Browser VKB Overlapping content</title> <meta charset="utf-8">');
+
+ if (params.metadata)
+ response.write("<meta name=\"viewport\" content=\"" + params.metadata + "\"/>");
+
+ /* Write a spacer div into the document, above an input element*/
+ response.write('</head>\n' +
+ '<body style="margin: 0; padding: 0">\n' +
+ '<div style="width: 100%; height: 100%"></div>\n' +
+ '<input type="text" style="background-color: green">\n' +
+ '</body>\n</html>');
+}
diff --git a/mobile/android/tests/browser/robocop/tracking_bad.html b/mobile/android/tests/browser/robocop/tracking_bad.html
new file mode 100644
index 0000000000..17f0e459e3
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/tracking_bad.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ </head>
+ <body>
+ <iframe src="http://tracking.example.com/"></iframe>
+ </body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/tracking_good.html b/mobile/android/tests/browser/robocop/tracking_good.html
new file mode 100644
index 0000000000..8e9429acdc
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/tracking_good.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ </head>
+ <body>
+ <iframe src="http://not-tracking.example.com/"></iframe>
+ </body>
+</html>
diff --git a/mobile/android/tests/browser/robocop/video-pattern.ogg b/mobile/android/tests/browser/robocop/video-pattern.ogg
new file mode 100644
index 0000000000..c86d9946bd
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/video-pattern.ogg
Binary files differ
diff --git a/mobile/android/tests/browser/robocop/video-pattern.webm b/mobile/android/tests/browser/robocop/video-pattern.webm
new file mode 100644
index 0000000000..8ed761099e
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/video-pattern.webm
Binary files differ
diff --git a/mobile/android/tests/browser/robocop/video_controls.html b/mobile/android/tests/browser/robocop/video_controls.html
new file mode 100644
index 0000000000..a312124093
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/video_controls.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Video Controls Test</title>
+ </head>
+ <body>
+ <video id="video" style="height: 480px; width: 640px" controls mozNoDynamicControls></video>
+ <canvas id="canvas" style="height: 480px; width: 640px"></canvas>
+ </body>
+</html>
diff --git a/mobile/android/tests/javaaddons/AndroidManifest.xml.in b/mobile/android/tests/javaaddons/AndroidManifest.xml.in
new file mode 100644
index 0000000000..b44930b1bd
--- /dev/null
+++ b/mobile/android/tests/javaaddons/AndroidManifest.xml.in
@@ -0,0 +1,14 @@
+#filter substitution
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.mozilla.javaaddons.test"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-sdk android:minSdkVersion="@MOZ_ANDROID_MIN_SDK_VERSION@"
+#ifdef MOZ_ANDROID_MAX_SDK_VERSION
+ android:maxSdkVersion="@MOZ_ANDROID_MAX_SDK_VERSION@"
+#endif
+ android:targetSdkVersion="@ANDROID_TARGET_SDK@"/>
+
+</manifest>
diff --git a/mobile/android/tests/javaaddons/Makefile.in b/mobile/android/tests/javaaddons/Makefile.in
new file mode 100644
index 0000000000..4baac3f161
--- /dev/null
+++ b/mobile/android/tests/javaaddons/Makefile.in
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+ANDROID_MANIFEST_FILE := $(CURDIR)/AndroidManifest.xml
+
+ANDROID_EXTRA_JARS := javaaddons-test.jar
+
+include $(topsrcdir)/config/rules.mk
+
+tools libs:: $(ANDROID_APK_NAME).apk
diff --git a/mobile/android/tests/javaaddons/moz.build b/mobile/android/tests/javaaddons/moz.build
new file mode 100644
index 0000000000..2fabebc568
--- /dev/null
+++ b/mobile/android/tests/javaaddons/moz.build
@@ -0,0 +1,23 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+ANDROID_APK_NAME = 'javaaddons-test'
+ANDROID_APK_PACKAGE = 'org.mozilla.javaaddons.test'
+
+jar = add_java_jar('javaaddons-test')
+jar.extra_jars += [
+ TOPOBJDIR + '/mobile/android/javaaddons/javaaddons-1.0.jar',
+]
+jar.javac_flags += ['-Xlint:all']
+jar.sources += [
+ 'src/org/mozilla/javaaddons/test/ClassWithNoRecognizedConstructors.java',
+ 'src/org/mozilla/javaaddons/test/JavaAddonV0.java',
+ 'src/org/mozilla/javaaddons/test/JavaAddonV1.java',
+]
+
+OBJDIR_PP_FILES.mobile.android.tests.javaaddons += [
+ 'AndroidManifest.xml.in',
+]
diff --git a/mobile/android/tests/javaaddons/res/values/strings.xml b/mobile/android/tests/javaaddons/res/values/strings.xml
new file mode 100644
index 0000000000..e4602bbdf9
--- /dev/null
+++ b/mobile/android/tests/javaaddons/res/values/strings.xml
@@ -0,0 +1,3 @@
+<resources>
+ <string name="app_name">org.mozilla.javaaddons.test</string>
+</resources>
diff --git a/mobile/android/tests/javaaddons/src/org/mozilla/javaaddons/test/ClassWithNoRecognizedConstructors.java b/mobile/android/tests/javaaddons/src/org/mozilla/javaaddons/test/ClassWithNoRecognizedConstructors.java
new file mode 100644
index 0000000000..93bf5e7cd6
--- /dev/null
+++ b/mobile/android/tests/javaaddons/src/org/mozilla/javaaddons/test/ClassWithNoRecognizedConstructors.java
@@ -0,0 +1,11 @@
+/* -*- 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.javaaddons.test;
+
+public class ClassWithNoRecognizedConstructors {
+ public ClassWithNoRecognizedConstructors(int a, String b, boolean c) {
+ }
+}
diff --git a/mobile/android/tests/javaaddons/src/org/mozilla/javaaddons/test/JavaAddonV0.java b/mobile/android/tests/javaaddons/src/org/mozilla/javaaddons/test/JavaAddonV0.java
new file mode 100644
index 0000000000..f0ea79535b
--- /dev/null
+++ b/mobile/android/tests/javaaddons/src/org/mozilla/javaaddons/test/JavaAddonV0.java
@@ -0,0 +1,24 @@
+/* -*- 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.javaaddons.test;
+
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+import java.util.Map;
+
+public class JavaAddonV0 implements Handler.Callback {
+ public JavaAddonV0(Map<String, Handler.Callback> callbacks) {
+ callbacks.put("JavaAddon:V0", this);
+ }
+
+ @Override
+ public boolean handleMessage(Message message) {
+ Log.i("JavaAddon", "handleMessage " + message.toString());
+ return true;
+ }
+}
diff --git a/mobile/android/tests/javaaddons/src/org/mozilla/javaaddons/test/JavaAddonV1.java b/mobile/android/tests/javaaddons/src/org/mozilla/javaaddons/test/JavaAddonV1.java
new file mode 100644
index 0000000000..803a0d7406
--- /dev/null
+++ b/mobile/android/tests/javaaddons/src/org/mozilla/javaaddons/test/JavaAddonV1.java
@@ -0,0 +1,59 @@
+/* -*- 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.javaaddons.test;
+
+import android.content.Context;
+import android.util.Log;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.javaaddons.JavaAddonInterfaceV1.EventCallback;
+import org.mozilla.javaaddons.JavaAddonInterfaceV1.EventDispatcher;
+import org.mozilla.javaaddons.JavaAddonInterfaceV1.EventListener;
+import org.mozilla.javaaddons.JavaAddonInterfaceV1.RequestCallback;
+
+public class JavaAddonV1 implements EventListener, RequestCallback {
+ protected final EventDispatcher mDispatcher;
+
+ public JavaAddonV1(Context context, EventDispatcher dispatcher) {
+ mDispatcher = dispatcher;
+ mDispatcher.registerEventListener(this, "JavaAddon:V1");
+ }
+
+ @Override
+ public void handleMessage(Context context, String event, JSONObject message, EventCallback callback) {
+ Log.i("JavaAddon", "handleMessage: " + event + ", " + message.toString());
+ final JSONObject output = new JSONObject();
+ try {
+ output.put("outputStringKey", "inputStringKey=" + message.getString("inputStringKey"));
+ output.put("outputIntKey", 1 + message.getInt("inputIntKey"));
+ } catch (JSONException e) {
+ // Should never happen; ignore.
+ }
+ // Respond.
+ if (callback != null) {
+ callback.sendSuccess(output);
+ }
+
+ // And send an independent Gecko event.
+ final JSONObject input = new JSONObject();
+ try {
+ input.put("inputStringKey", "raw");
+ input.put("inputIntKey", 3);
+ } catch (JSONException e) {
+ // Should never happen; ignore.
+ }
+ mDispatcher.sendRequestToGecko("JavaAddon:V1:Request", input, this);
+ }
+
+ @Override
+ public void onResponse(Context context, JSONObject jsonObject) {
+ Log.i("JavaAddon", "onResponse: " + jsonObject.toString());
+ // Unregister event listener, so that the JavaScript side can send a test message and
+ // check it is not handled.
+ mDispatcher.unregisterEventListener(this);
+ mDispatcher.sendRequestToGecko("JavaAddon:V1:VerificationRequest", jsonObject, null);
+ }
+}
diff --git a/mobile/android/tests/moz.build b/mobile/android/tests/moz.build
new file mode 100644
index 0000000000..2ec31396e0
--- /dev/null
+++ b/mobile/android/tests/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+if not CONFIG['MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE']:
+ TEST_DIRS += [
+ 'background',
+ ]
+
+TEST_DIRS += [
+ 'browser',
+ 'javaaddons', # Must be built before browser/robocop/roboextender.
+ # This is enforced in config/recurse.mk.
+]
+
+ANDROID_INSTRUMENTATION_MANIFESTS += ['browser/robocop/robocop.ini']
diff --git a/mobile/android/themes/core/about.css b/mobile/android/themes/core/about.css
new file mode 100644
index 0000000000..a060057b13
--- /dev/null
+++ b/mobile/android/themes/core/about.css
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+html {
+ background: #f0f0f0;
+ padding: 0 1em;
+ font-family: "Clear Sans", sans-serif !important;
+ font-size: 100% !important;
+}
+
+body {
+ color: black;
+ position: relative;
+ min-width: 330px;
+ max-width: 50em;
+ margin: 1em auto;
+ border: 1px solid gray;
+ border-radius: 10px;
+ padding: 3em;
+ padding-inline-start: 30px;
+ background: white;
+}
+
+.aboutPageWideContainer {
+ max-width: 80%;
+}
+
+#aboutLogoContainer {
+ border: 1px solid lightgray;
+ width: 300px;
+ margin-bottom: 2em;
+}
+
+img {
+ border: 0;
+}
+
+#version {
+ font-weight: bold;
+ color: #909090;
+ margin: -24px 0 9px 17px;
+}
+
+ul {
+ margin: 0;
+ margin-inline-start: 1.5em;
+ padding: 0;
+ list-style: square;
+}
diff --git a/mobile/android/themes/core/aboutAccounts.css b/mobile/android/themes/core/aboutAccounts.css
new file mode 100644
index 0000000000..29f0f2e472
--- /dev/null
+++ b/mobile/android/themes/core/aboutAccounts.css
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+html, body {
+ height: 100%;
+ width: 100%;
+}
+
+div {
+ transition: opacity 0.4s ease-in;
+}
+
+#spinner {
+ transition: opacity 0.2s ease-in;
+}
+
+#remote {
+ border: 0;
+ opacity: 0;
+ transition: opacity 0.4s ease-in;
+}
+
+.text {
+ color: #363B40;
+ font-size: 25px;
+ font-weight: lighter;
+ margin-bottom: 20px;
+}
+
+.hint {
+ color: #777777;
+ font-size: 20px;
+ margin-bottom: 20px;
+}
+
+a {
+ color: #0096DD; /* link_blue */
+ text-decoration: none;
+ font-size: 20px;
+ margin-bottom: 20px;
+}
+
+a:active {
+ color: #0082C6; /* link_blue_pressed */
+}
+
+.toplevel {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+}
+
+.container {
+ height: 100%;
+ padding-left: 30px;
+ padding-right: 30px;
+}
+
+.text-container {
+ padding-top: 60px;
+ padding-left: 30px;
+ padding-right: 30px;
+}
+
+.flex-column {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+}
+
+.button-row {
+ flex: 0;
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ text-align: center;
+ justify-content: center;
+}
+
+.button {
+ flex: 1;
+ height: 60px;
+ background-color: #E66000; /*matched to action_orange in java codebase*/
+ color: #FFFFFF;
+ font-size: 20px;
+ border-radius: 4px;
+ border-width: 0px;
+}
diff --git a/mobile/android/themes/core/aboutAddons.css b/mobile/android/themes/core/aboutAddons.css
new file mode 100644
index 0000000000..c44b330ea3
--- /dev/null
+++ b/mobile/android/themes/core/aboutAddons.css
@@ -0,0 +1,332 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import "defines.css";
+
+html[details] {
+ background-color: var(--color_about_item);
+}
+
+a {
+ text-decoration: none;
+ color: #0096DD;
+}
+
+a:active {
+ color: #0082C6;
+}
+
+.details {
+ width: 100%;
+}
+
+.details > div {
+ display: inline;
+}
+
+.version {
+ /* title is not localized, so keep the margin on the left side */
+ margin-left: .67em;
+}
+
+.description {
+ width: 100%;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.warn-unsigned {
+ border-top: 1px solid var(--color_about_item_border);
+ padding: 1em;
+ padding-inline-start: calc(var(--icon-size) + var(--icon-margin) * 2);
+ background-image: url("chrome://browser/skin/images/grey-caution.svg");
+ background-size: var(--icon-size);
+ background-position: var(--icon-margin);
+ background-repeat: no-repeat;
+ display: none;
+}
+
+.addon-item[isUnsigned="true"] .warn-unsigned {
+ display: block;
+}
+
+.status {
+ border-top: 1px solid var(--color_about_item_border);
+ font-weight: bold;
+ padding: 0.5em;
+ width: 100%;
+}
+
+.options-header {
+ font-weight: bold;
+ text-transform: uppercase;
+ margin-top: 1em;
+}
+
+.addon-item[isDisabled="true"] .options-header,
+.addon-item[optionsURL=""] .options-header,
+.addon-item[isDisabled="true"] .options-box,
+.addon-item[optionsURL=""] .options-box {
+ display: none;
+}
+
+#addons-details > .list-item {
+ margin-bottom: 42px;
+ border-bottom: none;
+}
+
+#addons-details > .list-item:active {
+ background-color: #fff;
+}
+
+/* Buttons */
+
+.buttons {
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ position: fixed;
+ bottom: 0px;
+}
+
+.buttons::after {
+ content: "";
+ border-right: 1px solid var(--color_about_item_border);
+}
+
+.buttons > button {
+ -moz-appearance: none;
+ font-size: 1em;
+ border: 1px solid transparent;
+ border-right: none;
+ border-top-color: var(--color_about_item_border);
+ border-inline-start-color: var(--color_about_item_border);
+ background-color: var(--color_about_item);
+ flex: 1;
+ padding: 0.75em 0.5em;
+ border-radius: 0;
+}
+
+.buttons > button:active {
+ background-color: #eeeeee;
+}
+
+.buttons > button[disabled="true"] {
+ color: #b5b5b5;
+}
+
+.buttons > button[hidden="true"] {
+ display: none;
+}
+
+.buttons:first-child {
+ border-inline-start-color: transparent;
+}
+
+/* Settings */
+
+setting {
+ padding-bottom: 1em;
+ -moz-box-align: center;
+ box-sizing: border-box;
+ width: 100%;
+}
+
+setting[type="integer"],
+setting[type="string"],
+setting[type="menulist"],
+setting[type="control"] {
+ -moz-box-orient: vertical;
+ -moz-box-align: start;
+}
+
+setting > vbox {
+ -moz-box-flex: 1;
+}
+
+.preferences-description {
+ margin-top: 4px;
+ color: #666;
+}
+
+.preferences-description:empty {
+ display: none;
+}
+
+/* Checkbox */
+
+checkbox {
+ -moz-binding: url("chrome://global/content/bindings/checkbox.xml#checkbox-with-spacing") !important;
+ margin: 0;
+}
+
+checkbox[label=""] > .checkbox-label-box,
+checkbox:not([label]) > .checkbox-label-box {
+ display: none;
+}
+
+.checkbox-check {
+ background-color: transparent;
+ background-image: url("chrome://browser/skin/images/checkbox_unchecked.png");
+ border: none;
+ height: 48px;
+ width: 48px;
+}
+
+setting:active checkbox > .checkbox-spacer-box > .checkbox-check {
+ background-image: url("chrome://browser/skin/images/checkbox_unchecked_pressed.png");
+}
+
+checkbox[disabled="true"] > .checkbox-spacer-box > .checkbox-check {
+ background-image: url("chrome://browser/skin/images/checkbox_unchecked_disabled.png");
+}
+
+checkbox[checked="true"] > .checkbox-spacer-box > .checkbox-check {
+ background-image: url("chrome://browser/skin/images/checkbox_checked.png");
+}
+
+setting:active checkbox[checked="true"] > .checkbox-spacer-box > .checkbox-check {
+ background-image: url("chrome://browser/skin/images/checkbox_checked_pressed.png");
+}
+
+checkbox[checked="true"][disabled="true"] > .checkbox-spacer-box > .checkbox-check {
+ background-image: url("chrome://browser/skin/images/checkbox_checked_disabled.png");
+}
+
+/* Textbox */
+
+textbox[type="number"] > spinbuttons {
+ visibility: collapse;
+}
+
+textbox {
+ min-width: 200px;
+ margin: 2px 0;
+ padding: 0.5em !important;
+ background: #fff;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ color: #333;
+}
+
+/* Button */
+
+setting button {
+ margin: 2px 0;
+ background: #fff;
+ border: 1px solid #ccc;
+ padding: 0.5em;
+}
+
+/* Menulist */
+
+menulist {
+ -moz-appearance: none !important;
+ -moz-user-focus: ignore;
+ min-width: 200px;
+ margin: 2px 0;
+ background: #fff;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ padding: 0.5em;
+}
+
+menulist > dropmarker {
+ height: 1.8em;
+ width: 1.8em;
+ margin-left: var(--margin_snormal);
+ background-color: transparent;
+ border: none;
+ -moz-box-align: center;
+ -moz-box-pack: center;
+ list-style-image: url("chrome://browser/skin/images/dropmarker.svg") !important;
+ -moz-image-region: auto;
+ display: block;
+}
+
+/* Select */
+
+select {
+ min-width: 200px;
+ margin: 2px 0;
+ background: #fff;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ padding: 1em;
+}
+
+/* XBL bindings */
+
+setting {
+ display: none;
+}
+
+setting[type="bool"] {
+ display: -moz-box;
+ -moz-binding: url("chrome://browser/content/bindings/settings.xml#setting-fulltoggle-bool");
+}
+
+setting[type="bool"][localized="true"] {
+ display: -moz-box;
+ -moz-binding: url("chrome://browser/content/bindings/settings.xml#setting-fulltoggle-localized-bool");
+}
+
+setting[type="boolint"] {
+ display: -moz-box;
+ -moz-binding: url("chrome://browser/content/bindings/settings.xml#setting-fulltoggle-boolint");
+}
+
+setting[type="integer"] {
+ display: -moz-box;
+ -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-integer");
+}
+
+setting[type="control"] {
+ display: -moz-box;
+ -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-control");
+}
+
+setting[type="string"] {
+ display: -moz-box;
+ -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-string");
+}
+
+setting[type="radio"],
+setting[type="menulist"] {
+ display: -moz-box;
+ -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-multi");
+}
+
+.hide-on-enable,
+.show-on-error,
+.show-on-uninstall,
+.show-on-install,
+.show-on-restart,
+div[isDisabled="true"] .hide-on-disable {
+ display: none;
+}
+
+div[error] .show-on-error,
+div[opType="needs-restart"] .show-on-restart,
+div[opType="needs-uninstall"] .show-on-uninstall,
+div[opType="needs-install"] .show-on-install,
+div[opType="needs-enable"] .show-on-enable,
+div[opType="needs-disable"] .show-on-disable,
+div[isDisabled="true"] .show-on-disable {
+ display: -moz-box;
+}
+
+div[opType="needs-restart"] .hide-on-restart,
+div[opType="needs-uninstall"] .hide-on-uninstall,
+div[isDisabled="true"][opType="needs-uninstall"],
+div[opType="needs-install"] .hide-on-install,
+div[opType="needs-enable"] .hide-on-enable,
+div[opType="needs-disable"] .hide-on-disable {
+ display: none;
+}
+
+#addons-list, #addons-details {
+ display: none;
+}
diff --git a/mobile/android/themes/core/aboutBase.css b/mobile/android/themes/core/aboutBase.css
new file mode 100644
index 0000000000..59379e2ee3
--- /dev/null
+++ b/mobile/android/themes/core/aboutBase.css
@@ -0,0 +1,114 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import "defines.css";
+
+html {
+ font-family: "Clear Sans",sans-serif;
+ font-size: 14px;
+ background-color: var(--color_about_background);
+ -moz-text-size-adjust: none;
+ -moz-user-select: none;
+ --icon-size: 1.8em;
+ --icon-margin: 1.35em;
+}
+
+body {
+ margin: 0;
+}
+
+input {
+ -moz-user-select: text;
+}
+
+.header {
+ color: #363B40;
+ font-size: 1.1em;
+ font-weight: bold;
+ background-color: #f5f5f5;
+ border-bottom: 2px solid;
+ -moz-border-bottom-colors: #FF9500;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ height: 48px;
+}
+
+.header > div {
+ flex: 1;
+ padding: 10px;
+ padding-inline-start: 16px;
+}
+
+#header-button {
+ background-repeat: no-repeat;
+ background-position: center center;
+ background-size: 33px 33px;
+ flex: 0;
+ height: 100%;
+}
+
+.list {
+ padding: 0px;
+ margin: 0px;
+ width: 100%;
+}
+
+.list-item {
+ color: #363B40;
+ background-color: var(--color_about_item);
+ border-bottom: 1px solid var(--color_about_item_border);
+ position: relative;
+ list-style-type: none;
+ list-style-image: none;
+ margin: 0px;
+ padding: 0px;
+ min-height: calc(var(--icon-size) + var(--icon-margin) * 2);
+}
+
+.list-item:active {
+ background-color: #eeeeee;
+}
+
+.list-item[isDisabled="true"] {
+ color: #999999;
+}
+
+.inner {
+ margin-inline-start: calc(var(--icon-size) + var(--icon-margin) * 2 - 1em);
+ padding: 1em;
+}
+
+.icon {
+ border: none;
+ width: var(--icon-size);
+ height: var(--icon-size);
+ top: var(--icon-margin);
+ margin-inline-start: var(--icon-margin);
+ position: absolute;
+ pointer-events: none;
+}
+
+.list-item[isDisabled="true"] .favicon {
+ opacity: 0.3;
+}
+
+.row {
+ display: flex;
+ width: 100%;
+}
+
+.title {
+ font-weight: bold;
+ overflow: hidden;
+ flex: 1;
+}
+
+#browse-title {
+ margin: 0.5em 0;
+ background-image: url("chrome://browser/skin/images/chevron.png");
+ background-size: 8px 20px;
+ background-position: right;
+ background-repeat: no-repeat;
+}
diff --git a/mobile/android/themes/core/aboutDownloads.css b/mobile/android/themes/core/aboutDownloads.css
new file mode 100644
index 0000000000..2e3097fd99
--- /dev/null
+++ b/mobile/android/themes/core/aboutDownloads.css
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include "defines.css"
+
+.list-item > a {
+ color: inherit;
+ text-decoration: none;
+}
+
+#private-downloads-list .list-item {
+ background-color: #393e43;
+ color: #ddd;
+}
+
+.details {
+ margin-inline-start: calc(var(--icon-size) + var(--icon-margin) * 2 - 1em);
+ padding: 1em;
+}
+
+.date {
+ color: gray;
+ margin-inline-start: 0.5em;
+}
+
+.domain,
+.size {
+ display: inline;
+}
+
+.state {
+ color: gray;
+ margin-bottom: -3px; /* Prevent overflow that hides bottom border */
+}
+
+.size:after {
+ content: " - ";
+ white-space: pre;
+}
+
+#no-downloads-indicator {
+ display: none;
+}
+
+#private-downloads-list:empty + #public-downloads-list:empty + #no-downloads-indicator {
+ display: block;
+ text-align: center;
+ padding-top: 3.9em;
+}
diff --git a/mobile/android/themes/core/aboutHealthReport.css b/mobile/android/themes/core/aboutHealthReport.css
new file mode 100644
index 0000000000..3dd40fc243
--- /dev/null
+++ b/mobile/android/themes/core/aboutHealthReport.css
@@ -0,0 +1,15 @@
+* {
+ margin: 0;
+ padding: 0;
+}
+
+html, body {
+ height: 100%;
+}
+
+#remote-report {
+ width: 100%;
+ height: 100%;
+ border: 0;
+ display: flex;
+}
diff --git a/mobile/android/themes/core/aboutLogins.css b/mobile/android/themes/core/aboutLogins.css
new file mode 100644
index 0000000000..2d962784e4
--- /dev/null
+++ b/mobile/android/themes/core/aboutLogins.css
@@ -0,0 +1,238 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import "defines.css";
+
+html {
+ height: 100%;
+}
+
+body {
+ height: 100%;
+}
+
+.hidden {
+ display: none;
+}
+
+.username {
+ width: 100%;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.hostname {
+ font-weight: bold;
+ overflow: hidden;
+ flex: 1;
+}
+
+.realm {
+ /* hostname is not localized, so keep the margin on the left side */
+ margin-left: .67em;
+}
+
+.toolbar-buttons {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ list-style: none;
+ margin: 0px;
+ padding: 0px;
+ height: 48px;
+ width: 48px;
+}
+
+.toolbar-buttons > li {
+ background-position: center;
+ background-size: 24px 24px;
+ background-repeat: no-repeat;
+ height: 20px;
+ width: 20px;
+}
+
+#filter-input-container {
+ position: fixed;
+ bottom: 0;
+ width: 100%;
+ padding: 10px 0;
+ display: flex;
+ background: var(--color_about_background);
+ border-top: 2px solid var(--color_about_item_border);
+}
+
+#filter-input-container[hidden] {
+ display: none;
+}
+
+#filter-input {
+ flex: 1;
+ padding: 5px;
+ margin-inline-start: 10px;
+ border-radius: 3px;
+}
+
+#filter-button {
+ background-image: url("resource://android/res/drawable-mdpi-v4/ab_search.png");
+}
+
+#filter-clear {
+ background-image: url("resource://android/res/drawable-mdpi-v4/close_edit_mode_light.png");
+ background-position: center;
+ background-size: 12px 12px;
+ background-repeat: no-repeat;
+ height: 32px;
+ width: 32px;
+ margin: 0 5px;
+}
+
+.edit-login-icon {
+ background-image: url("resource://android/res/drawable-hdpi-v4/favicon_globe.png");
+ background-position: center;
+ background-size: 32px 32px;
+ background-repeat: no-repeat;
+ height: 32px;
+ width: 32px;
+ padding: 5px;
+}
+
+#edit-login-page {
+ background-color: #FFFFFF;
+ height: 100%;
+}
+
+#edit-login-header {
+ background-color: #F5F5F5;
+}
+
+.update-button {
+ flex: 1;
+ height: 60px;
+ background-color: #E66000; /*matched to action_orange in java codebase*/
+ color: #FFFFFF;
+ font-size: 20px;
+ font-weight: bold;
+ border-radius: 4px;
+ border-width: 0px;
+ margin-top: 10px;
+}
+
+.disabled-btn {
+ background-color: #BFBFBF; /*matched to disabled_grey in the java codebase,in colors.xml*/
+}
+
+.password-btn-hide {
+ background-color: #777777;
+ color: white;
+}
+
+.edit-login-input {
+ flex: 1;
+ height: 36px;
+ font-size: 15px;
+ color: #222222;
+ border-radius: 4px;
+ border: solid 1px #AFB1B3;
+ padding-left: 10px;
+}
+
+.edit-login-div {
+ margin: 20px 30px;
+ display: flex;
+ flex-flow: row;
+ align-items: center;
+}
+
+#password-btn {
+ border-radius: 4px;
+ border-top-left-radius: 0em 0em;
+ border-bottom-left-radius: 0em 0em;
+ font-size: 15px;
+ height: 40px;
+}
+
+#password {
+ margin: none;
+ border-top-right-radius: 0em 0em;
+ border-bottom-right-radius: 0em 0em;
+}
+
+.icon {
+ background-image: url("resource://android/res/drawable-mdpi-v4/favicon_globe.png");
+ background-position: center;
+ background-size: 32px 32px;
+ background-repeat: no-repeat;
+ height: 32px;
+ width: 32px;
+ visibility: hidden;
+}
+
+@media screen and (min-resolution: 1.25dppx) {
+ #filter-button {
+ background-image: url("resource://android/res/drawable-hdpi-v4/ab_search.png");
+ }
+
+ #filter-clear {
+ background-image: url("resource://android/res/drawable-hdpi-v4/close_edit_mode_light.png");
+ }
+
+ .icon {
+ background-image: url("resource://android/res/drawable-hdpi-v4/favicon_globe.png");
+ }
+}
+
+@media screen and (min-resolution: 2dppx) {
+ #filter-button {
+ background-image: url("resource://android/res/drawable-xhdpi-v4/ab_search.png");
+ }
+
+ #filter-clear {
+ background-image: url("resource://android/res/drawable-hdpi-v4/close_edit_mode_light.png");
+ }
+
+ .icon {
+ background-image: url("resource://android/res/drawable-xhdpi-v4/favicon_globe.png");
+ }
+}
+
+#loading-img-container{
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+#spinner {
+ margin-top: 60px;
+}
+
+#empty-body {
+ padding-left: 60px;
+ padding-right: 60px;
+}
+
+#empty-obj-text-container {
+ display: flex;
+ flex-flow: column;
+ align-items: center;
+ text-align: center;
+ justify-content: center;
+}
+
+.empty-text {
+ color: #363B40;
+ font-size: 25px;
+ font-weight: lighter;
+ margin-bottom: 20px;
+}
+
+.empty-hint {
+ color: #777777;
+ font-size: 20px;
+}
+
+#empty-icon {
+ margin-top: 60px;
+ margin-bottom: 20px;
+}
diff --git a/mobile/android/themes/core/aboutMemory.css b/mobile/android/themes/core/aboutMemory.css
new file mode 100644
index 0000000000..a4f0887da5
--- /dev/null
+++ b/mobile/android/themes/core/aboutMemory.css
@@ -0,0 +1,149 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * The version used for desktop is located at
+ * toolkit/components/aboutmemory/content/aboutMemory.css.
+ * Mobile-specific stuff is at the bottom of this file.
+ */
+
+html {
+ background: -moz-Dialog;
+ font: message-box;
+}
+
+body {
+ padding: 0 2em;
+ margin: 0;
+ min-width: 45em;
+ margin: auto;
+}
+
+div.ancillary {
+ margin: 0.5em 0;
+ -moz-user-select: none;
+}
+
+div.section {
+ padding: 2em;
+ margin: 1em 0em;
+ border: 1px solid ThreeDShadow;
+ border-radius: 10px;
+ background: -moz-Field;
+}
+
+div.opsRow {
+ padding: 0.5em;
+ margin-right: 0.5em;
+ margin-top: 0.5em;
+ border: 1px solid ThreeDShadow;
+ border-radius: 10px;
+ background: -moz-Field;
+ display: inline-block;
+}
+
+div.opsRowLabel {
+ display: block;
+ margin-bottom: 0.2em;
+ font-weight: bold;
+}
+
+.opsRowLabel label {
+ margin-left: 1em;
+ font-weight: normal;
+}
+
+div.non-verbose pre.entries {
+ overflow-x: auto;
+ text-overflow: ellipsis;
+}
+
+h1 {
+ padding: 0;
+ margin: 0;
+}
+
+h2 {
+ background: #ddd;
+ padding-left: .1em;
+}
+
+.accuracyWarning {
+ color: #d22;
+}
+
+.badInputWarning {
+ color: #f00;
+}
+
+.treeline {
+ color: #888;
+}
+
+.mrValue {
+ font-weight: bold;
+ color: #400;
+}
+
+.mrPerc {
+}
+
+.mrSep {
+}
+
+.mrName {
+ color: #004;
+}
+
+.mrNote {
+ color: #604;
+}
+
+.hasKids {
+ cursor: pointer;
+}
+
+.hasKids:hover {
+ text-decoration: underline;
+}
+
+.noselect {
+ -moz-user-select: none; /* no need to include this when cutting+pasting */
+}
+
+.option {
+ font-size: 80%;
+ -moz-user-select: none; /* no need to include this when cutting+pasting */
+}
+
+.legend {
+ font-size: 80%;
+ -moz-user-select: none; /* no need to include this when cutting+pasting */
+}
+
+.debug {
+ font-size: 80%;
+}
+
+.hidden {
+ display: none;
+}
+
+.invalid {
+ color: #fff;
+ background-color: #f00;
+}
+
+/* Mobile-specific parts go here. */
+
+/* buttons are different sizes and overlapping without this */
+button {
+ margin: 1%;
+ padding: 2%;
+}
+
+.hiddenOnMobile {
+ display: none;
+}
+
diff --git a/mobile/android/themes/core/aboutPage.css b/mobile/android/themes/core/aboutPage.css
new file mode 100644
index 0000000000..a7fcbb91ec
--- /dev/null
+++ b/mobile/android/themes/core/aboutPage.css
@@ -0,0 +1,117 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+body {
+ -moz-text-size-adjust: none;
+ font-family: "Clear Sans",sans-serif;
+ font-size: 23px;
+ color: #222222;
+ background-color: #ced7de;
+}
+
+#header {
+ height: 80px;
+}
+
+#wordmark {
+ margin: 30px 0 0 15px;
+ width: 123px;
+ height: 36px;
+ background: url("chrome://browser/skin/images/wordmark-hdpi.png") no-repeat;
+}
+
+#version {
+ margin: 0 0 0 15px;
+ font-size: 15px;
+}
+
+#banner {
+ min-height: 150px;
+ background-color: #bdc7ce;
+}
+
+#logo {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 375px;
+ height: 300px;
+ background: url("chrome://browser/skin/images/logo-hdpi.png") no-repeat;
+}
+
+#updateBox {
+ position: relative;
+ top: 40px;
+ margin: 0 auto;
+ width: 60%; /* looks much larger!? */
+ padding: 20px 1em;
+ text-align: center;
+ background-image: url("chrome://browser/skin/images/about-btn-darkgrey.png");
+ background-size: contain;
+ border-bottom-width: 4px;
+ border-bottom-style: solid;
+ border-bottom-color: #3A3F44;
+ border-radius: 8px;
+ box-shadow: 0 5px 5px rgba(0, 0, 0, 0.3);
+}
+
+#update-message-checking,
+#update-message-none,
+#update-message-found,
+#update-message-downloading,
+#update-message-downloaded {
+ display: none;
+}
+
+#messages {
+ position: relative;
+ width: 70%;
+ margin: 40px auto 0 auto;
+ padding: 10px 0;
+ font-size: 15px;
+ text-align: center;
+}
+
+#telemetry a {
+ text-decoration: underline;
+}
+
+#aboutLinks {
+ margin: 0 0 15px 0;
+ padding: 0;
+}
+
+#aboutLinks > li {
+ line-height: 2.6;
+ border-top: 1px solid white;
+ border-bottom: 1px solid #C1C7CC;
+}
+
+#aboutLinks > li > a {
+ padding-left: 25px;
+ display: block;
+}
+
+#aboutDetails {
+ padding-left: 15px;
+ font-size: 15px;
+}
+
+.top-border {
+ border-bottom: 1px solid #C1C7CC;
+}
+
+.bottom-border {
+ border-top: 1px solid white;
+}
+
+a, span {
+ text-decoration: none;
+ color: #222222;
+}
+
+#updateBox > a,
+#updateBox > span {
+ color: #e5f2ff;
+}
diff --git a/mobile/android/themes/core/aboutPrivateBrowsing.css b/mobile/android/themes/core/aboutPrivateBrowsing.css
new file mode 100644
index 0000000000..fb528497e5
--- /dev/null
+++ b/mobile/android/themes/core/aboutPrivateBrowsing.css
@@ -0,0 +1,84 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+body {
+ font-family: "Clear Sans",sans-serif;
+ font-size: 16px;
+ text-align: center;
+ padding: 0 30px 0;
+}
+
+body.normal .showPrivate,
+body.private .showNormal {
+ display: none;
+}
+
+div.contentSection {
+ max-width: 400px;
+ margin:auto;
+}
+
+body.private {
+ background-color: #363b40; /* text_and_tabs_tray_grey */
+ color: #afb1b3; /* tabs_tray_icon_grey */
+}
+
+body.normal {
+ background-color: #eeeeee;
+ color: #777777; /* placeholder gray */
+}
+
+h1 {
+ font-size: 20px;
+ font-weight: 100;
+ text-align: center;
+ margin: 0;
+}
+
+body.normal h1 {
+ color: #363b40; /* text_and_tabs_tray_grey */
+}
+
+a {
+ color: #0096DD; /* link_blue */
+ text-decoration: none;
+}
+
+.masq {
+ display: block;
+ height: auto;
+ margin: 0 auto 20px auto;
+}
+
+.masq.showNormal {
+ width: 80px;
+}
+
+.masq.showPrivate {
+ width: 160px;
+}
+
+@media all and (max-height: 399px) {
+ body {
+ margin-top: 30px;
+ }
+}
+
+@media all and (min-height: 400px) and (max-height: 599px) {
+ body {
+ margin-top: 60px;
+ }
+}
+
+@media all and (min-height: 600px) and (max-height: 799px) {
+ body {
+ margin-top: 120px;
+ }
+}
+
+@media all and (min-height: 800px) {
+ body {
+ margin-top: 240px;
+ }
+}
diff --git a/mobile/android/themes/core/aboutReader.css b/mobile/android/themes/core/aboutReader.css
new file mode 100644
index 0000000000..2df2d1ff62
--- /dev/null
+++ b/mobile/android/themes/core/aboutReader.css
@@ -0,0 +1,120 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+html {
+ -moz-text-size-adjust: none;
+}
+
+body {
+ padding: 20px;
+ transition-property: background-color, color;
+ transition-duration: 0.4s;
+ max-width: 35em;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+body.light {
+ background-color: #ffffff;
+ color: #222222;
+}
+
+body.dark {
+ background-color: #222222;
+ color: #eeeeee;
+}
+
+body.sans-serif {
+ font-family: sans-serif;
+}
+
+body.serif {
+ font-family: serif;
+}
+
+#container.font-size1 {
+ font-size: 10px;
+}
+
+#container.font-size2 {
+ font-size: 12px;
+}
+
+#container.font-size3 {
+ font-size: 14px;
+}
+
+#container.font-size4 {
+ font-size: 16px;
+}
+
+#container.font-size5 {
+ font-size: 18px;
+}
+
+#container.font-size6 {
+ font-size: 20px;
+}
+
+#container.font-size7 {
+ font-size: 22px;
+}
+
+#container.font-size8 {
+ font-size: 24px;
+}
+
+#container.font-size9 {
+ font-size: 26px;
+}
+
+/* Override some controls and content styles based on color scheme */
+
+body.light > .container > .header > .domain {
+ color: #ee7600;
+ border-bottom-color: #d0d0d0;
+}
+
+body.light > .container > .header > h1 {
+ color: #222222;
+}
+
+body.light > .container > .header > .credits {
+ color: #898989;
+}
+
+body.dark > .container > .header > .domain {
+ color: #ff9400;
+ border-bottom-color: #777777;
+}
+
+body.dark > .container > .header > h1 {
+ color: #eeeeee;
+}
+
+body.dark > .container > .header > .credits {
+ color: #aaaaaa;
+}
+
+body.light > .container > .content .caption,
+body.light > .container > .content .wp-caption-text,
+body.light > .container > .content figcaption {
+ color: #898989;
+}
+
+body.dark > .container > .content .caption,
+body.dark > .container > .content .wp-caption-text,
+body.dark > .container > .content figcaption {
+ color: #aaaaaa;
+}
+
+body.light > .container > .content blockquote {
+ color: #898989 !important;
+ border-left-color: #d0d0d0 !important;
+}
+
+body.dark > .container > .content blockquote {
+ color: #aaaaaa !important;
+ border-left-color: #777777 !important;
+}
diff --git a/mobile/android/themes/core/aboutReaderContent.css b/mobile/android/themes/core/aboutReaderContent.css
new file mode 100644
index 0000000000..fe6451df2f
--- /dev/null
+++ b/mobile/android/themes/core/aboutReaderContent.css
@@ -0,0 +1,114 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#moz-reader-content {
+ display: none;
+ font-size: 1em;
+}
+
+a {
+ text-decoration: underline !important;
+ font-weight: normal;
+}
+
+a,
+a:visited,
+a:hover,
+a:active {
+ color: #00acff !important;
+}
+
+* {
+ max-width: 100% !important;
+ height: auto !important;
+}
+
+p {
+ line-height: 1.4em !important;
+ margin: 0px !important;
+ margin-bottom: 20px !important;
+}
+
+/* Covers all images showing edge-to-edge using a
+ an optional caption text */
+.wp-caption,
+figure {
+ display: block !important;
+ width: 100% !important;
+ margin: 0px !important;
+ margin-bottom: 32px !important;
+}
+
+/* Images marked to be shown edge-to-edge with an
+ optional captio ntext */
+p > img:only-child,
+p > a:only-child > img:only-child,
+.wp-caption img,
+figure img {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+/* Account for body padding to make image full width */
+img[moz-reader-full-width] {
+ width: calc(100% + 40px);
+ margin-left: -20px;
+ margin-right: -20px;
+ max-width: none !important;
+}
+
+/* Image caption text */
+.caption,
+.wp-caption-text,
+figcaption {
+ font-size: 0.9em;
+ font-family: sans-serif;
+ margin: 0px !important;
+ padding-top: 4px !important;
+}
+
+/* Ensure all pre-formatted code inside the reader content
+ are properly wrapped inside content width */
+code,
+pre {
+ white-space: pre-wrap !important;
+ margin-bottom: 20px !important;
+}
+
+blockquote {
+ margin: 0px !important;
+ margin-bottom: 20px !important;
+ padding: 0px !important;
+ padding-inline-start: 16px !important;
+ border: 0px !important;
+ border-left: 2px solid !important;
+}
+
+ul,
+ol {
+ margin: 0px !important;
+ margin-bottom: 20px !important;
+ padding: 0px !important;
+ line-height: 1.5em;
+}
+
+ul {
+ padding-inline-start: 30px !important;
+ list-style: disc !important;
+}
+
+ol {
+ padding-inline-start: 35px !important;
+ list-style: decimal !important;
+}
+
+/* Hide elements with common "hidden" class names */
+.visually-hidden,
+.visuallyhidden,
+.hidden,
+.invisible,
+.sr-only {
+ display: none;
+}
diff --git a/mobile/android/themes/core/aboutReaderControls.css b/mobile/android/themes/core/aboutReaderControls.css
new file mode 100644
index 0000000000..642dbc2371
--- /dev/null
+++ b/mobile/android/themes/core/aboutReaderControls.css
@@ -0,0 +1,290 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#reader-message {
+ margin-top: 40px;
+ display: none;
+ text-align: center;
+ width: 100%;
+ font-size: 0.9em;
+}
+
+.header {
+ text-align: start;
+ display: none;
+}
+
+.domain,
+.credits {
+ font-size: 0.9em;
+ font-family: sans-serif;
+}
+
+.domain {
+ margin-top: 10px;
+ padding-bottom: 10px;
+ color: #00acff !important;
+ text-decoration: none;
+}
+
+.domain-border {
+ margin-top: 15px;
+ border-bottom: 1.5px solid #777777;
+ width: 50%;
+}
+
+.header > h1 {
+ font-size: 1.33em;
+ font-weight: 700;
+ line-height: 1.1em;
+ width: 100%;
+ margin: 0px;
+ margin-top: 32px;
+ margin-bottom: 16px;
+ padding: 0px;
+}
+
+.header > .credits {
+ padding: 0px;
+ margin: 0px;
+ margin-bottom: 32px;
+}
+
+/*======= Controls toolbar =======*/
+
+.toolbar {
+ font-family: sans-serif;
+ position: fixed;
+ width: 100%;
+ left: 0;
+ margin: 0;
+ padding: 0;
+ bottom: 0;
+ list-style: none;
+ pointer-events: none;
+}
+
+.toolbar > * {
+ float: right;
+}
+
+.button {
+ width: 56px;
+ height: 56px;
+ display: block;
+ background-position: center;
+ background-size: 26px 16px;
+ background-repeat: no-repeat;
+ background-color: #E66000;
+ border-radius: 10000px;
+ margin: 20px;
+ border: 0;
+ box-shadow: 0px 4px 8px 0px rgba(0,0,0,0.40);
+}
+
+.button:active {
+ background-color: #DC5600;
+}
+
+/* Remove dotted border when button is focused */
+.button::-moz-focus-inner,
+.dropdown-popup > div > button::-moz-focus-inner {
+ border: 0;
+}
+
+.button[hidden] {
+ display: none;
+}
+
+.dropdown-toggle,
+#reader-popup {
+ pointer-events: auto;
+}
+
+.dropdown {
+ left: 0;
+ text-align: center;
+ display: inline-block;
+ list-style: none;
+ margin: 0px;
+ padding: 0px;
+}
+
+/*======= Font style popup =======*/
+
+.dropdown-popup {
+ position: absolute;
+ left: 0;
+ width: calc(100% - 30px);
+ margin: 15px;
+ z-index: 1000;
+ background: #EBEBF0;
+ visibility: hidden;
+ border: 0;
+ border-radius: 4px;
+ box-shadow: 0px 4px 8px 0px rgba(0,0,0,0.40);
+ -moz-user-select: none;
+}
+
+/* Only used on desktop */
+.dropdown-popup > hr,
+.dropdown-arrow,
+#font-type-buttons > button > .name,
+#content-width-buttons,
+#line-height-buttons {
+ display: none;
+}
+
+.open > .dropdown-popup {
+ visibility: visible;
+ bottom: 0;
+}
+
+#font-type-buttons,
+#font-size-buttons,
+#color-scheme-buttons {
+ display: flex;
+ flex-direction: row;
+}
+
+#font-type-buttons > button,
+#color-scheme-buttons > button {
+ text-align: center;
+}
+
+#font-type-buttons > button,
+#font-size-buttons > button {
+ width: 50%;
+ background-color: transparent;
+ border: 0;
+}
+
+#font-type-buttons > button {
+ font-size: 24px;
+ color: #AFB1B3;
+ padding: 15px 0;
+}
+
+#font-type-buttons > button:active,
+#font-type-buttons > button.selected {
+ color: #222222;
+}
+
+#font-size-sample {
+ flex: 0;
+ font-size: 24px;
+ color: #000000;
+ margin: 0 30px;
+ padding: 0 10px;
+}
+
+.serif-button {
+ font-family: serif;
+}
+
+.minus-button,
+.plus-button {
+ background-color: transparent;
+ border: 0;
+ height: 60px;
+ background-size: 18px 18px;
+ background-repeat: no-repeat;
+ background-position: center;
+}
+
+.minus-button {
+ background-size: 24px 6px;
+ margin-left: 50px;
+ padding: 0 5px;
+}
+
+.plus-button {
+ background-size: 24px 24px;
+ margin-right: 50px;
+ padding: 0 5px;
+}
+
+#color-scheme-buttons > button {
+ width: 33%;
+ border-radius: 4px;
+ border: 1px solid #BFBFBF;
+ padding: 10px;
+ margin: 15px 10px;
+ font-size: 14px;
+}
+
+#color-scheme-buttons > button:active,
+#color-scheme-buttons > button.selected {
+ border: 2px solid #FF9500;
+}
+
+.dark-button {
+ color: #eeeeee;
+ background-color: #333333;
+}
+
+.auto-button {
+ color: #000000;
+ background-color: transparent;
+}
+
+.light-button {
+ color: #333333;
+ background-color: #ffffff;
+}
+
+/*======= Toolbar icons =======*/
+
+/* desktop-only controls */
+.close-button {
+ display: none;
+}
+
+.style-button {
+ background-image: url('chrome://browser/skin/images/reader-style-icon-hdpi.png');
+}
+
+.minus-button {
+ background-image: url('chrome://browser/skin/images/reader-minus-hdpi.png');
+}
+
+.plus-button {
+ background-image: url('chrome://browser/skin/images/reader-plus-hdpi.png');
+}
+
+@media screen and (min-resolution: 2dppx) {
+ .style-button {
+ background-image: url('chrome://browser/skin/images/reader-style-icon-xhdpi.png');
+ }
+
+ .minus-button {
+ background-image: url('chrome://browser/skin/images/reader-minus-xhdpi.png');
+ }
+
+ .plus-button {
+ background-image: url('chrome://browser/skin/images/reader-plus-xhdpi.png');
+ }
+}
+
+@media screen and (min-resolution: 3dppx) {
+ .style-button {
+ background-image: url('chrome://browser/skin/images/reader-style-icon-xxhdpi.png');
+ }
+
+ .minus-button {
+ background-image: url('chrome://browser/skin/images/reader-minus-xxhdpi.png');
+ }
+
+ .plus-button {
+ background-image: url('chrome://browser/skin/images/reader-plus-xxhdpi.png');
+ }
+}
+
+@media screen and (min-width: 960px) {
+ .dropdown-popup {
+ width: 350px;
+ left: auto;
+ right: 0;
+ }
+}
diff --git a/mobile/android/themes/core/aboutSupport.css b/mobile/android/themes/core/aboutSupport.css
new file mode 100644
index 0000000000..de21e5b73d
--- /dev/null
+++ b/mobile/android/themes/core/aboutSupport.css
@@ -0,0 +1,97 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+html {
+ background-color: -moz-Field;
+ color: -moz-FieldText;
+ font: message-box;
+}
+
+body {
+ width: 90%;
+ margin-left: 5%;
+ margin-right: 5%;
+}
+
+.page-subtitle {
+ margin-bottom: 3em;
+}
+
+.major-section {
+ margin-top: 2em;
+ margin-bottom: 1em;
+ font-size: large;
+ text-align: start;
+ font-weight: bold;
+}
+
+#copy-raw-data-to-clipboard,
+#copy-to-clipboard {
+ padding-top: 2%;
+ width: 100%;
+ padding-bottom: 2%;
+ margin: 1%;
+}
+
+table {
+ background-color: -moz-Dialog;
+ color: -moz-DialogText;
+ font: message-box;
+ font-size: xx-large;
+ text-align: start;
+ width: 100%;
+ border: 1px solid ThreeDShadow;
+ border-spacing: 0px;
+}
+
+th, td {
+ border: 1px dotted ThreeDShadow;
+ padding: 3px;
+}
+
+thead th {
+ text-align: center;
+}
+
+th {
+ text-align: start;
+ background-color: Highlight;
+ color: HighlightText;
+}
+
+th.column {
+ white-space: nowrap;
+ width: 0px;
+}
+
+td {
+ text-align: start;
+ border-top: 1px dotted ThreeDShadow;
+}
+
+.prefs-table {
+ width: 100%;
+ table-layout: fixed;
+}
+
+.pref-name {
+ width: 70%;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.pref-value {
+ width: 30%;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+#profile-row {
+ display: none;
+}
+
+#reset-box {
+ display: none;
+}
+
diff --git a/mobile/android/themes/core/config.css b/mobile/android/themes/core/config.css
new file mode 100644
index 0000000000..38be7b47c4
--- /dev/null
+++ b/mobile/android/themes/core/config.css
@@ -0,0 +1,333 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+html,
+body {
+ margin: 0;
+ padding: 0;
+ background-color: #ced7de;
+ -moz-user-select: none;
+ font-family: "Clear Sans",sans-serif;
+ -moz-text-size-adjust: none;
+}
+
+.toolbar {
+ width: 100%;
+ height: 3em;
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 10;
+ box-shadow: 0 0 3px #444;
+ background-color: #ced7de;
+ color: #000000;
+ font-weight: bold;
+ border-bottom: 2px solid;
+ -moz-border-bottom-colors: #ff9100 #f27900;
+}
+
+.toolbar-container {
+ max-width: 40em;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+#filter-container {
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+ margin-right: 0.5em;
+ height: 2em;
+ border: 1px solid transparent;
+ border-image-source: url("chrome://browser/skin/images/textfield.png");
+ border-image-slice: 1 1 3 1;
+ border-image-width: 1px 1px 3px 1px;
+ overflow: hidden;
+ display: flex;
+ flex-direction: row;
+}
+
+#filter-input {
+ -moz-appearance: none;
+ border: none;
+ background-image: none;
+ background-color: transparent;
+ display: inline-block;
+ width: 12em;
+ min-width: 0;
+ color: #000000;
+ opacity: 1;
+ flex: 1 1 auto;
+}
+
+#filter-input::placeholder {
+ color: #777777;
+}
+
+.toolbar input {
+ display: inline-block;
+ height: 100%;
+ min-width: 3em;
+ box-sizing: border-box;
+ opacity: 0.75;
+}
+
+#new-pref-toggle-button {
+ background-position: center center;
+ background-image: url("chrome://browser/skin/images/config-plus.png");
+ background-size: 48px 48px;
+ height: 48px;
+ width: 48px;
+ display: inline-block;
+ outline-style: none;
+}
+
+#filter-search-button {
+ background-image: url("chrome://browser/skin/images/search.png");
+ background-size: 32px 32px;
+ height: 32px;
+ width: 32px;
+ display: inline-block;
+ outline-style: none;
+}
+
+#filter-input-clear-button {
+ background-image: url("chrome://browser/skin/images/search-clear-30.png");
+ background-size: 32px 32px;
+ height: 32px;
+ width: 32px;
+ display: inline-block;
+ outline-style: none;
+}
+
+#filter-input[value=""] + #filter-input-clear-button {
+ display: none;
+}
+
+.toolbar-item {
+ display: inline-block;
+ height: 3em;
+ min-width: 3em;
+ float: right;
+}
+
+#content {
+ position: relative;
+ margin: 0;
+ margin-left: auto;
+ margin-right: auto;
+ padding-top: 3em;
+ padding-left: 0;
+ padding-right: 0;
+ min-height: 100%;
+ max-width: 40em;
+}
+
+ul {
+ list-style-position: inside;
+ border: 1px solid #808080;
+ background-color: #ffffff;
+ min-height: 100%;
+ width: 100%;
+ padding-top: 0;
+ margin: 0;
+ padding-left: 0;
+ box-sizing: border-box;
+ box-shadow: 0 0 5px #000000;
+ overflow-x: hidden;
+}
+
+#new-pref-container {
+ width: 100%;
+ margin: 0;
+ background-color: #ffffff;
+ box-sizing: border-box;
+ box-shadow: 0 0 5px #000000;
+ overflow-x: hidden;
+ max-width: 40em;
+ max-height: 100%;
+ position: fixed;
+ top: 3em;
+ left: auto;
+ display: none;
+ z-index: 5;
+}
+
+#new-pref-container input,
+#new-pref-container select {
+ border: none;
+ background-image: none;
+}
+
+#new-pref-container.show {
+ display: block;
+}
+
+li {
+ list-style-type: none;
+ border-bottom: 1px solid #d3d3d3;
+ opacity: 1;
+ background-color: #ffffff;
+ cursor: pointer;
+}
+
+#new-pref-line-boolean,
+#new-pref-value-string,
+#new-pref-value-int {
+ display: none;
+}
+#new-pref-item[typestyle="boolean"] #new-pref-line-boolean,
+#new-pref-item[typestyle="string"] #new-pref-value-string,
+#new-pref-item[typestyle="int"] #new-pref-value-int {
+ display: block;
+}
+
+.pref-name,
+.pref-value {
+ padding: 15px 10px;
+ text-align: left;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ background-image: none;
+}
+
+.pref-value {
+ color: rgba(0,0,0,0.5);
+ flex: 1 1 auto;
+ border: none;
+ -moz-appearance: none;
+ background-image: none;
+ background-color: transparent;
+}
+
+.pref-name[locked] {
+ padding-right: 20px;
+ background-image: url("chrome://browser/skin/images/lock.png");
+ background-repeat: no-repeat;
+ background-position: right 50%;
+ background-size: auto 60%;
+}
+
+#new-pref-name {
+ width: 30em;
+}
+
+#new-pref-type {
+ display: inline-block !important;
+ border-left: 1px solid #d3d3d3;
+ width: 10em;
+ text-align: right;
+}
+
+.pref-item-line {
+ border-top: 1px solid rgba(0,0,0,0.05);
+ color: rgba(0,0,0,0.5);
+ display: flex;
+ flex-direction: row;
+}
+
+#new-pref-value-boolean {
+ flex: 1 1 auto;
+}
+
+#new-pref-container .pref-button.toggle {
+ display: inline-block;
+ opacity: 1;
+ flex: 0 1 auto;
+ float: right;
+}
+
+#new-pref-container .pref-button.cancel,
+#new-pref-container .pref-button.create {
+ display: inline-block;
+ opacity: 1;
+ flex: 1 1 auto;
+}
+
+.pref-item-line {
+ pointer-events: none;
+}
+
+#new-pref-container .pref-item-line,
+.pref-item.selected .pref-item-line,
+.pref-item:not(.selected) .pref-button.reset {
+ pointer-events: auto;
+}
+
+#new-pref-container .pref-button.create[disabled] {
+ color: #d3d3d3;
+}
+
+.pref-item.selected {
+ background-color: rgba(0,0,255,0.05);
+}
+
+.pref-button {
+ display: inline-block;
+ box-sizing: border-box;
+ text-align: center;
+ padding: 10px 1em;
+ border-left: 1px solid rgba(0,0,0,0.1);
+ opacity: 0;
+ transition-property: opacity;
+ transition-duration: 500ms;
+}
+
+.pref-item.selected .pref-item-line .pref-button {
+ opacity: 1;
+}
+
+.pref-item:not(.selected) .pref-item-line .pref-button:not(.reset) {
+ display: none;
+}
+
+.pref-item:not(.selected) .pref-button.reset {
+ opacity: 1;
+}
+
+.pref-button:active {
+ background-color: rgba(0,0,255,0.2);
+}
+
+.pref-button[disabled] {
+ display: none;
+}
+
+.pref-button.up {
+ background-image: url("chrome://browser/skin/images/arrowup-16.png");
+ background-position: center center;
+ background-repeat: no-repeat;
+}
+
+.pref-button.down {
+ background-image: url("chrome://browser/skin/images/arrowdown-16.png");
+ background-position: center center;
+ background-repeat: no-repeat;
+}
+
+#prefs-shield {
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0,0,0,0.5);
+ position: fixed;
+ top: 0;
+ left: 0;
+ opacity: 0;
+ transition-property: opacity;
+ transition-duration: 500ms;
+ display: none;
+}
+
+#prefs-shield[shown] {
+ display: block;
+ opacity: 1;
+}
+
+#loading-container > li {
+ background-image: url(chrome://browser/skin/images/throbber.png);
+ background-position: center center;
+ background-repeat: no-repeat;
+ padding-left: 40px;
+ height: 3em;
+ width: 100%;
+}
diff --git a/mobile/android/themes/core/content.css b/mobile/android/themes/core/content.css
new file mode 100644
index 0000000000..58063db09f
--- /dev/null
+++ b/mobile/android/themes/core/content.css
@@ -0,0 +1,414 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import "defines.css";
+@import "scrollbar.css";
+
+@namespace url("http://www.w3.org/1999/xhtml");
+@namespace xul url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+::-moz-selection {
+ background-color: var(--color_background_highlight);
+ color: var(--color_text_highlight);
+}
+
+/* Style the scrollbars */
+xul|scrollbar[root="true"] {
+ position: relative;
+ z-index: 2147483647;
+}
+
+xul|scrollbar {
+ -moz-appearance: none !important;
+ -moz-binding: url("chrome://global/content/bindings/scrollbar.xml#scrollbar");
+ background-color: transparent !important;
+ background-image: none !important;
+ border: 0px solid transparent !important;
+ pointer-events: none;
+}
+
+/* Scrollbar code will reset the margin to the correct side depending on
+ where layout actually puts the scrollbar */
+xul|scrollbar[orient="vertical"] {
+ margin-left: -6px;
+ min-width: 6px;
+ max-width: 6px;
+}
+
+xul|scrollbar[orient="vertical"] xul|thumb {
+ -moz-appearance: scrollbarthumb-vertical !important;
+ max-width: 2px !important;
+ min-width: 2px !important;
+}
+
+xul|scrollbar[orient="horizontal"] {
+ margin-top: -6px;
+ min-height: 6px;
+ max-height: 6px;
+}
+
+xul|scrollbar[orient="horizontal"] xul|thumb {
+ -moz-appearance: scrollbarthumb-horizontal !important;
+ max-height: 2px !important;
+ min-height: 2px !important;
+}
+
+xul|scrollbar:not([active="true"]),
+xul|scrollbar[disabled] {
+ opacity: 0;
+}
+
+xul|scrollbarbutton {
+ min-height: 6px !important;
+ min-width: 6px !important;
+ -moz-appearance: none !important;
+ visibility: hidden;
+}
+
+xul|scrollbarbutton[sbattr="scrollbar-up-top"],
+xul|scrollbarbutton[sbattr="scrollbar-bottom-top"] {
+ display: none;
+}
+
+xul|scrollbar xul|thumb {
+ background-color: rgba(119, 119, 119, 0.4) !important;
+ -moz-border-top-colors: none !important;
+ -moz-border-bottom-colors: none !important;
+ -moz-border-right-colors: none !important;
+ -moz-border-left-colors: none !important;
+ border: none;
+ border-radius: 4px;
+}
+
+xul|scrollbarbutton {
+ background-image: none !important;
+}
+
+select:not([size]):not([multiple]) > xul|scrollbar,
+select[size="1"] > xul|scrollbar,
+select:not([size]):not([multiple]) xul|scrollbarbutton,
+select[size="1"] xul|scrollbarbutton {
+ display: block;
+ margin-left: 0;
+ min-width: 16px;
+}
+
+/* Override inverse OS themes */
+textarea,
+button,
+xul|button,
+* > input:not([type="image"]) {
+ -moz-appearance: none !important; /* See bug 598421 for fixing the platform */
+ border-radius: var(--form_border_radius);
+}
+
+select[size],
+select[multiple],
+select[size][multiple],
+textarea,
+* > input:not([type="image"]):not([type="image"]) {
+ border-style: solid;
+ border-color: var(--form_border);
+ color: var(--form_text);
+ background-color: var(--form_background);
+}
+
+/* Selects are handled by the form helper, see bug 685197 */
+select option, select optgroup {
+ pointer-events: none;
+}
+
+select:not([size]):not([multiple]),
+select[size="0"],
+select[size="1"],
+* > input[type="button"],
+* > input[type="submit"],
+* > input[type="reset"],
+button {
+ border-style: solid;
+ border-color: var(--form_border);
+ color: var(--form_text);
+ background-color: var(--form_background);
+}
+
+input[type="checkbox"] {
+ background: var(--form_background);
+}
+
+input[type="radio"] {
+ background: var(--form_background)
+}
+
+select {
+ border-width: 1px;
+ padding: 1px;
+ border-radius: var(--form_border_radius);
+}
+
+select:not([size]):not([multiple]),
+select[size="0"],
+select[size="1"] {
+ padding: 0 1px 0 1px;
+}
+
+* > input:not([type="image"]) {
+ border-width: 1px;
+ padding: 1px;
+}
+
+textarea {
+ resize: none;
+ border-width: 1px;
+ padding-inline-start: 1px;
+ padding-inline-end: 1px;
+ padding-block-start: 2px;
+ padding-block-end: 2px;
+}
+
+input[type="button"],
+input[type="submit"],
+input[type="reset"],
+button {
+ border-width: 1px;
+ padding-inline-start: 7px;
+ padding-inline-end: 7px;
+ padding-block-start: 0;
+ padding-block-end: 0;
+}
+
+input[type="radio"],
+input[type="checkbox"] {
+ border: 1px solid var(--form_border) !important;
+ padding-inline-start: 1px;
+ padding-inline-end: 1px;
+ padding-block-start: 2px;
+ padding-block-end: 2px;
+}
+
+select > button {
+ border-width: 0px !important;
+ margin: 0px !important;
+ padding: 0px !important;
+ border-radius: 0;
+ color: #414141;
+
+ background-size: auto auto;
+ background-color: transparent;
+ background-image: url("chrome://browser/skin/images/dropmarker.svg") !important;
+ background-position: calc(50% + 1px) center !important;
+ background-repeat: no-repeat !important;
+
+ font-size: inherit;
+}
+
+select[size]:focus,
+select[multiple]:focus,
+select[size][multiple]:focus,
+textarea:focus,
+input[type="file"]:focus > input[type="text"],
+* > input:not([type="image"]):focus {
+ outline: 0px !important;
+ border-style: solid;
+ border-color: var(--form_border);
+ background-color: var(--form_background);
+}
+
+select:not([size]):not([multiple]):focus,
+select[size="0"]:focus,
+select[size="1"]:focus,
+input[type="button"]:focus,
+input[type="submit"]:focus,
+input[type="reset"]:focus,
+button:focus {
+ outline: 0px !important;
+ border-style: solid;
+ border-color: var(--form_border);
+ background-color: var(--form_background);
+}
+
+input[type="checkbox"]:focus,
+input[type="radio"]:focus {
+ border-color: var(--form_border) !important;
+}
+
+input[type="checkbox"]:focus {
+ background: var(--form_background);
+}
+
+input[type="radio"]:focus {
+ background: var(--form_background);
+}
+
+/* we need to be specific for selects because the above rules are specific too */
+textarea:disabled,
+select[size]:disabled,
+select[multiple]:disabled,
+select[size][multiple]:disabled,
+select:not([size]):not([multiple]):disabled,
+select[size="0"]:disabled,
+select[size="1"]:disabled,
+button:disabled,
+button:disabled:active,
+* > input:not([type="image"]):disabled,
+* > input:not([type="image"]):disabled:active {
+ color: var(--form_text_disabled);
+ border-color: var(--form_border);
+ border-style: solid;
+ border-width: 1px;
+ background: var(--form_background_disabled);
+}
+
+select:not([size]):not([multiple]):disabled,
+select[size="0"]:disabled,
+select[size="1"]:disabled {
+ background: var(--form_background_disabled);
+}
+
+input[type="button"]:disabled,
+input[type="button"]:disabled:active,
+input[type="submit"]:disabled,
+input[type="submit"]:disabled:active,
+input[type="reset"]:disabled,
+input[type="reset"]:disabled:active,
+button:disabled,
+button:disabled:active {
+ padding-inline-start: 7px;
+ padding-inline-end: 7px;
+ padding-block-start: 0;
+ padding-block-end: 0;
+ background: var(--form_background_disabled);
+}
+
+input[type="radio"]:disabled,
+input[type="radio"]:disabled:active,
+input[type="radio"]:disabled:hover,
+input[type="radio"]:disabled:hover:active,
+input[type="checkbox"]:disabled,
+input[type="checkbox"]:disabled:active,
+input[type="checkbox"]:disabled:hover,
+input[type="checkbox"]:disabled:hover:active {
+ border:1px solid var(--form_border) !important;
+}
+
+select:disabled > button {
+ opacity: 0.6;
+ padding-inline-start: 7px;
+ padding-inline-end: 7px;
+ padding-block-start: 1px;
+ padding-block-end: 1px;
+}
+
+/* -moz-touch-enabled? media elements */
+:-moz-any(video, audio) > xul|videocontrols {
+ -moz-binding: url("chrome://global/content/bindings/videocontrols.xml#touchControls");
+}
+
+/* display click to play when autoplay is blocked for videos */
+video:not([controls]) > xul|videocontrols {
+ visibility: visible;
+ -moz-binding: url("chrome://global/content/bindings/videocontrols.xml#noControls");
+}
+
+*:any-link:active,
+*[role=button]:active,
+button:not(:disabled):active,
+input:not(:focus):not(:disabled):active,
+select:not(:disabled):active,
+textarea:not(:focus):not(:disabled):active,
+option:active,
+label:active,
+xul|menulist:active {
+ background-color: var(--color_background_highlight_overlay);
+}
+
+button:active:hover,
+input[type="color"]:-moz-system-metric(color-picker-available):active:hover,
+input[type="reset"]:active:hover,
+input[type="button"]:active:hover,
+input[type="submit"]:active:hover {
+ padding-inline-end: 7px;
+ padding-inline-start: 7px;
+}
+
+input[type=number] > div > div, /* work around bug 946184 */
+input[type=number]::-moz-number-spin-box {
+ display: none;
+}
+
+/* Override accessiblecaret css in layout/style/ua.css */
+div:-moz-native-anonymous.moz-accessiblecaret > #text-overlay,
+div:-moz-native-anonymous.moz-accessiblecaret > #image {
+ /* border: 0.1px solid red; */ /* Uncomment border to see the touch target. */
+ padding-left: 59%; /* Enlarge the touch area. ((48-22)/2)px / 22px ~= 59% */
+ padding-right: 59%; /* Enlarge the touch area. */
+ left: -59%;
+}
+
+div:-moz-native-anonymous.moz-accessiblecaret > #image {
+ padding-bottom: 59%; /* Enlarge the touch area. */
+}
+
+div:-moz-native-anonymous.moz-accessiblecaret.normal > #image {
+ background-image: url("chrome://browser/skin/images/accessiblecaret-normal-hdpi.png");
+}
+
+div:-moz-native-anonymous.moz-accessiblecaret.left > #text-overlay,
+div:-moz-native-anonymous.moz-accessiblecaret.left > #image {
+ margin-left: -50%;
+}
+
+div:-moz-native-anonymous.moz-accessiblecaret.left > #image {
+ background-image: url("chrome://browser/skin/images/accessiblecaret-tilt-left-hdpi.png");
+}
+
+div:-moz-native-anonymous.moz-accessiblecaret.right > #text-overlay,
+div:-moz-native-anonymous.moz-accessiblecaret.right > #image {
+ margin-left: 47%;
+}
+
+div:-moz-native-anonymous.moz-accessiblecaret.right > #image {
+ background-image: url("chrome://browser/skin/images/accessiblecaret-tilt-right-hdpi.png");
+}
+
+@media (min-resolution: 1.5dppx) {
+ div:-moz-native-anonymous.moz-accessiblecaret.normal > #image {
+ background-image: url("chrome://browser/skin/images/accessiblecaret-normal-hdpi.png");
+ }
+
+ div:-moz-native-anonymous.moz-accessiblecaret.left > #image {
+ background-image: url("chrome://browser/skin/images/accessiblecaret-tilt-left-hdpi.png");
+ }
+
+ div:-moz-native-anonymous.moz-accessiblecaret.right > #image {
+ background-image: url("chrome://browser/skin/images/accessiblecaret-tilt-right-hdpi.png");
+ }
+}
+
+@media (min-resolution: 2dppx) {
+ div:-moz-native-anonymous.moz-accessiblecaret.normal > #image {
+ background-image: url("chrome://browser/skin/images/accessiblecaret-normal-xhdpi.png");
+ }
+
+ div:-moz-native-anonymous.moz-accessiblecaret.left > #image {
+ background-image: url("chrome://browser/skin/images/accessiblecaret-tilt-left-xhdpi.png");
+ }
+
+ div:-moz-native-anonymous.moz-accessiblecaret.right > #image {
+ background-image: url("chrome://browser/skin/images/accessiblecaret-tilt-right-xhdpi.png");
+ }
+}
+
+@media (min-resolution: 2.25dppx) {
+ div:-moz-native-anonymous.moz-accessiblecaret.normal > #image {
+ background-image: url("chrome://browser/skin/images/accessiblecaret-normal-xxhdpi.png");
+ }
+
+ div:-moz-native-anonymous.moz-accessiblecaret.left > #image {
+ background-image: url("chrome://browser/skin/images/accessiblecaret-tilt-left-xxhdpi.png");
+ }
+
+ div:-moz-native-anonymous.moz-accessiblecaret.right > #image {
+ background-image: url("chrome://browser/skin/images/accessiblecaret-tilt-right-xxhdpi.png");
+ }
+}
diff --git a/mobile/android/themes/core/defines.css b/mobile/android/themes/core/defines.css
new file mode 100644
index 0000000000..dd1ef3142f
--- /dev/null
+++ b/mobile/android/themes/core/defines.css
@@ -0,0 +1,18 @@
+:root {
+ --form_border: #bfbfbf;
+ --form_border_radius: 2px;
+ --form_text: #363b40;
+ --form_text_disabled: #bebebe;
+ --form_background: white;
+ --form_background_disabled: #f5f5f5;
+
+ --color_about_background: #f5f5f5;
+ --color_about_item: #ffffff;
+ --color_about_item_border: #d7d9db;
+
+ --color_background_highlight: rgba(255, 149, 0, 0.6);
+ --color_background_highlight_overlay: rgba(171, 171, 171, 0.5);
+ --color_text_highlight: #000;
+
+ --margin_snormal: 0.64mm;
+}
diff --git a/mobile/android/themes/core/images/about-btn-darkgrey.png b/mobile/android/themes/core/images/about-btn-darkgrey.png
new file mode 100644
index 0000000000..24e2aae835
--- /dev/null
+++ b/mobile/android/themes/core/images/about-btn-darkgrey.png
Binary files differ
diff --git a/mobile/android/themes/core/images/accessiblecaret-normal-hdpi.png b/mobile/android/themes/core/images/accessiblecaret-normal-hdpi.png
new file mode 100644
index 0000000000..7f11a3c407
--- /dev/null
+++ b/mobile/android/themes/core/images/accessiblecaret-normal-hdpi.png
Binary files differ
diff --git a/mobile/android/themes/core/images/accessiblecaret-normal-xhdpi.png b/mobile/android/themes/core/images/accessiblecaret-normal-xhdpi.png
new file mode 100644
index 0000000000..7c7cc65dd3
--- /dev/null
+++ b/mobile/android/themes/core/images/accessiblecaret-normal-xhdpi.png
Binary files differ
diff --git a/mobile/android/themes/core/images/accessiblecaret-normal-xxhdpi.png b/mobile/android/themes/core/images/accessiblecaret-normal-xxhdpi.png
new file mode 100644
index 0000000000..411d814ed2
--- /dev/null
+++ b/mobile/android/themes/core/images/accessiblecaret-normal-xxhdpi.png
Binary files differ
diff --git a/mobile/android/themes/core/images/accessiblecaret-tilt-left-hdpi.png b/mobile/android/themes/core/images/accessiblecaret-tilt-left-hdpi.png
new file mode 100644
index 0000000000..838e9ab4df
--- /dev/null
+++ b/mobile/android/themes/core/images/accessiblecaret-tilt-left-hdpi.png
Binary files differ
diff --git a/mobile/android/themes/core/images/accessiblecaret-tilt-left-xhdpi.png b/mobile/android/themes/core/images/accessiblecaret-tilt-left-xhdpi.png
new file mode 100644
index 0000000000..f1c6f1d88a
--- /dev/null
+++ b/mobile/android/themes/core/images/accessiblecaret-tilt-left-xhdpi.png
Binary files differ
diff --git a/mobile/android/themes/core/images/accessiblecaret-tilt-left-xxhdpi.png b/mobile/android/themes/core/images/accessiblecaret-tilt-left-xxhdpi.png
new file mode 100644
index 0000000000..6d5ee52ac7
--- /dev/null
+++ b/mobile/android/themes/core/images/accessiblecaret-tilt-left-xxhdpi.png
Binary files differ
diff --git a/mobile/android/themes/core/images/accessiblecaret-tilt-right-hdpi.png b/mobile/android/themes/core/images/accessiblecaret-tilt-right-hdpi.png
new file mode 100644
index 0000000000..97bb84235c
--- /dev/null
+++ b/mobile/android/themes/core/images/accessiblecaret-tilt-right-hdpi.png
Binary files differ
diff --git a/mobile/android/themes/core/images/accessiblecaret-tilt-right-xhdpi.png b/mobile/android/themes/core/images/accessiblecaret-tilt-right-xhdpi.png
new file mode 100644
index 0000000000..4eb81b9b56
--- /dev/null
+++ b/mobile/android/themes/core/images/accessiblecaret-tilt-right-xhdpi.png
Binary files differ
diff --git a/mobile/android/themes/core/images/accessiblecaret-tilt-right-xxhdpi.png b/mobile/android/themes/core/images/accessiblecaret-tilt-right-xxhdpi.png
new file mode 100644
index 0000000000..0751b846cb
--- /dev/null
+++ b/mobile/android/themes/core/images/accessiblecaret-tilt-right-xxhdpi.png
Binary files differ
diff --git a/mobile/android/themes/core/images/amo-logo.png b/mobile/android/themes/core/images/amo-logo.png
new file mode 100644
index 0000000000..f18ab8cbca
--- /dev/null
+++ b/mobile/android/themes/core/images/amo-logo.png
Binary files differ
diff --git a/mobile/android/themes/core/images/arrowdown-16.png b/mobile/android/themes/core/images/arrowdown-16.png
new file mode 100644
index 0000000000..27e3c50c34
--- /dev/null
+++ b/mobile/android/themes/core/images/arrowdown-16.png
Binary files differ
diff --git a/mobile/android/themes/core/images/arrowup-16.png b/mobile/android/themes/core/images/arrowup-16.png
new file mode 100644
index 0000000000..ad422bfb06
--- /dev/null
+++ b/mobile/android/themes/core/images/arrowup-16.png
Binary files differ
diff --git a/mobile/android/themes/core/images/blocked-warning.png b/mobile/android/themes/core/images/blocked-warning.png
new file mode 100644
index 0000000000..3920aff3c8
--- /dev/null
+++ b/mobile/android/themes/core/images/blocked-warning.png
Binary files differ
diff --git a/mobile/android/themes/core/images/cast-active.svg b/mobile/android/themes/core/images/cast-active.svg
new file mode 100644
index 0000000000..394b39ad3f
--- /dev/null
+++ b/mobile/android/themes/core/images/cast-active.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<svg width="66px" height="54px" viewBox="0 0 66 54" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g transform="translate(-279.000000, -1435.000000)">
+ <g transform="translate(240.000000, 1390.000000)">
+ <g transform="translate(36.000000, 36.000000)">
+ <path d="M0,0 L72,0 L72,72 L0,72 L0,0 Z" opacity="0.1"></path>
+ <path d="M0,0 L72,0 L72,72 L0,72 L0,0 Z"></path>
+ <path d="M3,54 L3,63 L12,63 C12,58.02 7.98,54 3,54 L3,54 Z M3,42 L3,48 C11.28,48 18,54.72 18,63 L24,63 C24,51.39 14.61,42 3,42 L3,42 Z M57,21 L15,21 L15,25.89 C26.88,29.73 36.27,39.12 40.11,51 L57,51 L57,21 L57,21 Z M3,30 L3,36 C17.91,36 30,48.09 30,63 L36,63 C36,44.76 21.21,30 3,30 L3,30 Z M63,9 L9,9 C5.7,9 3,11.7 3,15 L3,24 L9,24 L9,15 L63,15 L63,57 L42,57 L42,63 L63,63 C66.3,63 69,60.3 69,57 L69,15 C69,11.7 66.3,9 63,9 L63,9 Z" fill="#FFFFFF"></path>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/mobile/android/themes/core/images/cast-ready.svg b/mobile/android/themes/core/images/cast-ready.svg
new file mode 100644
index 0000000000..5b6252b82a
--- /dev/null
+++ b/mobile/android/themes/core/images/cast-ready.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<svg width="66px" height="54px" viewBox="0 0 66 54" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g transform="translate(-87.000000, -1435.000000)">
+ <g transform="translate(48.000000, 1390.000000)">
+ <g transform="translate(36.000000, 36.000000)">
+ <path d="M0,0 L72,0 L72,72 L0,72 L0,0 Z" opacity="0.1"></path>
+ <path d="M0,0 L72,0 L72,72 L0,72 L0,0 Z"></path>
+ <path d="M63,9 L9,9 C5.7,9 3,11.7 3,15 L3,24 L9,24 L9,15 L63,15 L63,57 L42,57 L42,63 L63,63 C66.3,63 69,60.3 69,57 L69,15 C69,11.7 66.3,9 63,9 L63,9 Z M3,54 L3,63 L12,63 C12,58.02 7.98,54 3,54 L3,54 Z M3,42 L3,48 C11.28,48 18,54.72 18,63 L24,63 C24,51.39 14.61,42 3,42 L3,42 Z M3,30 L3,36 C17.91,36 30,48.09 30,63 L36,63 C36,44.76 21.21,30 3,30 L3,30 Z" fill="#FFFFFF"></path>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/mobile/android/themes/core/images/certerror-warning.png b/mobile/android/themes/core/images/certerror-warning.png
new file mode 100644
index 0000000000..053cea7416
--- /dev/null
+++ b/mobile/android/themes/core/images/certerror-warning.png
Binary files differ
diff --git a/mobile/android/themes/core/images/checkbox_checked.png b/mobile/android/themes/core/images/checkbox_checked.png
new file mode 100644
index 0000000000..8de545e2f0
--- /dev/null
+++ b/mobile/android/themes/core/images/checkbox_checked.png
Binary files differ
diff --git a/mobile/android/themes/core/images/checkbox_checked_disabled.png b/mobile/android/themes/core/images/checkbox_checked_disabled.png
new file mode 100644
index 0000000000..d938244272
--- /dev/null
+++ b/mobile/android/themes/core/images/checkbox_checked_disabled.png
Binary files differ
diff --git a/mobile/android/themes/core/images/checkbox_checked_pressed.png b/mobile/android/themes/core/images/checkbox_checked_pressed.png
new file mode 100644
index 0000000000..835b3b36b7
--- /dev/null
+++ b/mobile/android/themes/core/images/checkbox_checked_pressed.png
Binary files differ
diff --git a/mobile/android/themes/core/images/checkbox_unchecked.png b/mobile/android/themes/core/images/checkbox_unchecked.png
new file mode 100644
index 0000000000..5089b7fa97
--- /dev/null
+++ b/mobile/android/themes/core/images/checkbox_unchecked.png
Binary files differ
diff --git a/mobile/android/themes/core/images/checkbox_unchecked_disabled.png b/mobile/android/themes/core/images/checkbox_unchecked_disabled.png
new file mode 100644
index 0000000000..f9c8bdea06
--- /dev/null
+++ b/mobile/android/themes/core/images/checkbox_unchecked_disabled.png
Binary files differ
diff --git a/mobile/android/themes/core/images/checkbox_unchecked_pressed.png b/mobile/android/themes/core/images/checkbox_unchecked_pressed.png
new file mode 100644
index 0000000000..b682b5aaa3
--- /dev/null
+++ b/mobile/android/themes/core/images/checkbox_unchecked_pressed.png
Binary files differ
diff --git a/mobile/android/themes/core/images/chevron.png b/mobile/android/themes/core/images/chevron.png
new file mode 100644
index 0000000000..7080dac5c8
--- /dev/null
+++ b/mobile/android/themes/core/images/chevron.png
Binary files differ
diff --git a/mobile/android/themes/core/images/config-plus.png b/mobile/android/themes/core/images/config-plus.png
new file mode 100644
index 0000000000..10da3ed5a4
--- /dev/null
+++ b/mobile/android/themes/core/images/config-plus.png
Binary files differ
diff --git a/mobile/android/themes/core/images/dropmarker-right.svg b/mobile/android/themes/core/images/dropmarker-right.svg
new file mode 100644
index 0000000000..a80bfc89f3
--- /dev/null
+++ b/mobile/android/themes/core/images/dropmarker-right.svg
@@ -0,0 +1,7 @@
+<?xml version="1.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/. -->
+<svg width="7px" height="10px" xmlns="http://www.w3.org/2000/svg">
+ <polyline points="1 1 6 5 1 9" stroke="#414141" stroke-width="2" stroke-linecap="round" fill="none" stroke-linejoin="round"/>
+</svg>
diff --git a/mobile/android/themes/core/images/dropmarker.svg b/mobile/android/themes/core/images/dropmarker.svg
new file mode 100644
index 0000000000..8e1051d85a
--- /dev/null
+++ b/mobile/android/themes/core/images/dropmarker.svg
@@ -0,0 +1,7 @@
+<?xml version="1.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/. -->
+<svg width="10px" height="7px" xmlns="http://www.w3.org/2000/svg">
+ <polyline points="1 1 5 6 9 1" stroke="#414141" stroke-width="2" stroke-linecap="round" fill="none" stroke-linejoin="round"/>
+</svg>
diff --git a/mobile/android/themes/core/images/errorpage-warning.png b/mobile/android/themes/core/images/errorpage-warning.png
new file mode 100644
index 0000000000..d2acea2210
--- /dev/null
+++ b/mobile/android/themes/core/images/errorpage-warning.png
Binary files differ
diff --git a/mobile/android/themes/core/images/exitfullscreen.svg b/mobile/android/themes/core/images/exitfullscreen.svg
new file mode 100644
index 0000000000..af80947fbf
--- /dev/null
+++ b/mobile/android/themes/core/images/exitfullscreen.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+<svg width="72px" height="72px" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g transform="translate(-276.000000, -1042.000000)" fill="#FFFFFF">
+ <g transform="translate(240.000000, 1006.000000)">
+ <path d="M36.96,80.16 C35.2060427,78.316034 35.8370453,76.8 38.4,76.8 L62.4,76.8 C65.0584079,76.8 67.2,78.9469741 67.2,81.6 L67.2,105.6 C67.2,108.165446 65.6848806,108.791856 63.84,107.04 L55.2,98.4 L49.92,104.16 C47.9768607,105.92543 44.9530217,105.908123 43.2,104.16 L39.84,100.8 C38.0648585,98.9095525 38.080377,95.8763717 39.84,94.08 L45.6,88.8 L36.96,80.16 Z M107.04,63.84 C108.79397,65.6843439 108.162544,67.2 105.6,67.2 L81.6,67.2 C78.941564,67.2 76.8,65.0529486 76.8,62.4 L76.8,38.4 C76.8,35.8334252 78.3157239,35.2076163 80.16,36.96 L88.8,45.6 L94.08,39.84 C95.909525,38.1209584 98.9004381,38.0799163 100.8,39.84 L104.16,43.2 C105.923072,45.1022004 105.884206,48.0879079 104.16,49.92 L98.4,55.2 L107.04,63.84 Z"></path>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/mobile/android/themes/core/images/fullscreen.svg b/mobile/android/themes/core/images/fullscreen.svg
new file mode 100644
index 0000000000..91a77edf5b
--- /dev/null
+++ b/mobile/android/themes/core/images/fullscreen.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+<svg width="72px" height="72px" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g transform="translate(-84.000000, -1042.000000)" fill="#FFFFFF">
+ <g transform="translate(48.000000, 1006.000000)">
+ <path d="M77.1428571,36 L102.857143,36 C105.705437,36 108,38.3003294 108,41.1428571 L108,66.8571429 C108,69.6058346 106.376658,70.276989 104.4,68.4 L95.1428571,59.1428571 L89.4857143,65.3142857 C87.4037793,67.2058174 84.1639518,67.1872741 82.2857143,65.3142857 L78.6857143,61.7142857 C76.783777,59.6888062 76.8004039,56.4389697 78.6857143,54.5142857 L84.8571429,48.8571429 L75.6,39.6 C73.7207601,37.6243221 74.3968342,36 77.1428571,36 Z M66.8571429,108 L41.1428571,108 C38.2945329,108 36,105.699588 36,102.857143 L36,77.1428571 C36,74.3929556 37.6239899,73.7224461 39.6,75.6 L48.8571429,84.8571429 L54.5142857,78.6857143 C56.474491,76.843884 59.6790408,76.7999103 61.7142857,78.6857143 L65.3142857,82.2857143 C67.2032916,84.3237862 67.1616492,87.5227585 65.3142857,89.4857143 L59.1428571,95.1428571 L68.4,104.4 C70.279254,106.376083 69.6027253,108 66.8571429,108 Z"></path>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/mobile/android/themes/core/images/grey-caution.svg b/mobile/android/themes/core/images/grey-caution.svg
new file mode 100644
index 0000000000..98d54fc446
--- /dev/null
+++ b/mobile/android/themes/core/images/grey-caution.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="60px" height="60px" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg">
+ <path d="M34.6570853,3.89861537 L61.6191658,57.8227763 C62.6631009,60.0994422 61.4905756,62 58.9609325,62 L5.03677158,62 C2.50636943,62 1.33487292,60.0971201 2.37853829,57.8227763 L29.3406187,3.89861537 C30.6694576,1.36596731 33.3285163,1.36828938 34.6570853,3.89861537 Z M28.2013759,23.6360478 C28.2013759,21.5439751 29.9058314,19.8480151 31.998852,19.8480151 C34.0961402,19.8480151 35.7963281,21.5419679 35.7963281,23.6360478 L35.7963281,38.844839 C35.7963281,40.9369118 34.0918726,42.6328718 31.998852,42.6328718 C29.9015639,42.6328718 28.2013759,40.938919 28.2013759,38.844839 L28.2013759,23.6360478 Z M31.998852,54.7847954 C34.0961402,54.7847954 35.7963281,53.0846074 35.7963281,50.9873193 C35.7963281,48.8900311 34.0961402,47.1898431 31.998852,47.1898431 C29.9015639,47.1898431 28.2013759,48.8900311 28.2013759,50.9873193 C28.2013759,53.0846074 29.9015639,54.7847954 31.998852,54.7847954 Z" fill="#AFB1B3" fill-rule="evenodd" transform="translate(-2,-2)"/>
+</svg> \ No newline at end of file
diff --git a/mobile/android/themes/core/images/icon_key_emptypage.svg b/mobile/android/themes/core/images/icon_key_emptypage.svg
new file mode 100644
index 0000000000..68bd0cc46d
--- /dev/null
+++ b/mobile/android/themes/core/images/icon_key_emptypage.svg
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<svg width="60px" height="60px" viewBox="0 0 180 180" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
+ <title>Key</title>
+ <g sketch:type="MSArtboardGroup" fill="#bfbfbf">
+ <path d="M72,60.75 C72,60.75 59.625,72 59.625,72 C57.375,74.25 58.5,76.5 59.625,77.625 L65.25,82.125 C66.375,83.25 67.5,84.375 65.25,86.625 L0,157.5 L0,176.625 C0,178.875 1.125,180 3.375,180 L33.75,180 C34.875,180 36,178.875 36,177.75 L36,168.75 L48.375,167.625 L47.25,156.375 L55.125,156.375 C55.125,156.375 56.25,156.375 56.25,155.25 L56.25,148.5 L65.25,148.5 L65.25,132.75 L78.75,132.75 L94.5,115.875 C96.75,113.625 99,113.625 101.25,115.875 L106.875,121.5 C109.125,123.75 111.375,123.75 113.625,121.5 L122.625,109.125 C157.125,108.963211 180,82.6329431 180,55.125 C179.4,24.0802676 155.4,0 124.875,0 C95.4,0 68.025,26.3302676 72,60.75 Z M139,54 C131.85,54 126,48.15 126,41 C126,33.85 131.85,28 139,28 C146.15,28 152,33.85 152,41 C152,48.15 146.15,54 139,54 L139,54 Z" sketch:type="MSShapeGroup"></path>
+ </g>
+</svg> \ No newline at end of file
diff --git a/mobile/android/themes/core/images/lock.png b/mobile/android/themes/core/images/lock.png
new file mode 100644
index 0000000000..0d3565c325
--- /dev/null
+++ b/mobile/android/themes/core/images/lock.png
Binary files differ
diff --git a/mobile/android/themes/core/images/logo-hdpi.png b/mobile/android/themes/core/images/logo-hdpi.png
new file mode 100644
index 0000000000..82553891f6
--- /dev/null
+++ b/mobile/android/themes/core/images/logo-hdpi.png
Binary files differ
diff --git a/mobile/android/themes/core/images/mute.svg b/mobile/android/themes/core/images/mute.svg
new file mode 100644
index 0000000000..0770154b9f
--- /dev/null
+++ b/mobile/android/themes/core/images/mute.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+<svg width="70px" height="60px" viewBox="0 0 70 60" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g transform="translate(-469.000000, -1048.000000)" fill="#FFFFFF">
+ <g transform="translate(432.000000, 1006.000000)">
+ <path d="M98.3836922,53.1162172 C103.662239,57.6998888 107,64.4601112 107,71.9999094 C107,79.5397077 103.662239,86.2999301 98.3836922,90.8836016 L94.8371224,87.3370318 C99.2157236,83.6682614 102,78.1592367 102,71.9999094 C102,65.8405821 99.2157236,60.3315575 94.8371224,56.6627871 L98.3836922,53.1162172 Z M91.2831302,60.2167792 C94.7649686,62.9636534 97,67.2207716 97,71.9999094 C97,76.7790472 94.7649686,81.0361655 91.2831302,83.7830397 L87.7103386,80.210248 C90.3032555,78.4034792 92,75.3998422 92,71.9999094 C92,68.5999775 90.3032563,65.5963412 87.7103384,63.789571 L91.2831302,60.2167792 Z M37,61.9999094 C37,59.2105478 39.2364417,56.9712087 42,56.9999094 L58,56.9999094 L75.5,42.9999094 C79.1323004,40.818613 82,42.3919545 82,46.4999094 L82,97.4999094 C82,101.618951 79.1389004,103.178674 75.5,100.999909 L58,86.9999094 L42,86.9999094 C39.2362844,87.0258469 37,84.7863502 37,81.9999094 L37,61.9999094 Z"></path>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/mobile/android/themes/core/images/pause.svg b/mobile/android/themes/core/images/pause.svg
new file mode 100644
index 0000000000..0b181a57e8
--- /dev/null
+++ b/mobile/android/themes/core/images/pause.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+<svg width="54px" height="72px" viewBox="0 0 54 72" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g transform="translate(-285.000000, -1234.000000)" fill="#FFFFFF">
+ <g transform="translate(240.000000, 1198.000000)">
+ <path d="M63,103.5 C63,105.9849 60.9849,108 58.5,108 L49.5,108 C47.0151,108 45,105.9849 45,103.5 L45,40.5 C45,38.0151 47.0151,36 49.5,36 L58.5,36 C60.9849,36 63,38.0151 63,40.5 L63,103.5 Z M94.5,108 L85.5,108 C83.0151,108 81,105.9849 81,103.5 L81,40.5 C81,38.0151 83.0151,36 85.5,36 L94.5,36 C96.9849,36 99,38.0151 99,40.5 L99,103.5 C99,105.9849 96.9849,108 94.5,108 Z"></path>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/mobile/android/themes/core/images/placeholder_image.svg b/mobile/android/themes/core/images/placeholder_image.svg
new file mode 100644
index 0000000000..a4174b1bba
--- /dev/null
+++ b/mobile/android/themes/core/images/placeholder_image.svg
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 42 42" style="enable-background:new 0 0 42 42;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#FFFFFF;filter:url(#Adobe_OpacityMaskFilter);}
+ .st1{fill:#010101;}
+ .st2{mask:url(#mask-cutout-blocked-sign_1_);}
+ .st3{fill:#F1F1F2;}
+ .st4{fill:#7F8081;}
+ .st5{fill:#4D4D4E;}
+ .st6{fill:#979899;}
+ .st7{fill:#010101;filter:url(#Adobe_OpacityMaskFilter_1_);}
+ .st8{fill:#FFFFFF;}
+ .st9{mask:url(#mask-cutout-frame_1_);fill:#656667;}
+ .st10{fill:#FFFFFF;filter:url(#Adobe_OpacityMaskFilter_2_);}
+ .st11{mask:url(#mask-cutout-blocked-sign-inner_1_);fill:#656667;}
+ .st12{fill:none;stroke:#656667;stroke-width:2;}
+</style>
+<g id="Layer_1">
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter" filterUnits="userSpaceOnUse" x="5" y="5" width="32" height="32">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="5" y="5" width="32" height="32" id="mask-cutout-blocked-sign_1_">
+ <rect x="5" y="5" class="st0" width="32" height="32"/>
+ <circle class="st1" cx="30" cy="30" r="8"/>
+ </mask>
+ <g id="icon-frame" class="st2">
+ <path id="shape-background" class="st3" d="M10,9h22c1.7,0,3,1.3,3,3v18c0,1.7-1.3,3-3,3H10c-1.7,0-3-1.3-3-3V12 C7,10.3,8.3,9,10,9z"/>
+ <polygon class="st4" points="8,31 16,21 23,31 "/>
+ <polygon class="st5" points="16,31 28,15 36,25 36,31 "/>
+ <circle class="st6" cx="14" cy="16" r="3"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_1_" filterUnits="userSpaceOnUse" x="5" y="5" width="32" height="32">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="5" y="5" width="32" height="32" id="mask-cutout-frame_1_">
+ <rect x="5" y="5" class="st7" width="32" height="32"/>
+ <path class="st8" d="M10,9h22c1.7,0,3,1.3,3,3v18c0,1.7-1.3,3-3,3H10c-1.7,0-3-1.3-3-3V12C7,10.3,8.3,9,10,9z"/>
+ <path class="st1" d="M11,11h20c1.1,0,2,0.9,2,2v16c0,1.1-0.9,2-2,2H11c-1.1,0-2-0.9-2-2V13C9,11.9,9.9,11,11,11z"/>
+ </mask>
+ <rect x="5" y="5" class="st9" width="32" height="32"/>
+ </g>
+ <g id="icon-blocked-sign">
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_2_" filterUnits="userSpaceOnUse" x="24" y="24" width="12" height="12">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="24" y="24" width="12" height="12" id="mask-cutout-blocked-sign-inner_1_">
+ <rect x="5" y="5" class="st10" width="32" height="32"/>
+ <circle class="st1" cx="30" cy="30" r="4"/>
+ </mask>
+ <circle class="st11" cx="30" cy="30" r="6"/>
+ <line class="st12" x1="26" y1="34" x2="34" y2="26"/>
+ </g>
+</g>
+<g id="Layer_2">
+</g>
+</svg> \ No newline at end of file
diff --git a/mobile/android/themes/core/images/play.svg b/mobile/android/themes/core/images/play.svg
new file mode 100644
index 0000000000..5dabbc8383
--- /dev/null
+++ b/mobile/android/themes/core/images/play.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+<svg width="58px" height="72px" viewBox="0 0 58 72" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g transform="translate(-91.000000, -1234.000000)" fill="#FFFFFF">
+ <g transform="translate(48.000000, 1198.000000)">
+ <path d="M43,40 L43,104 C43,107.688656 45.5537886,109.093408 49,107 L99,75 C101.697423,73.3130096 101.692268,70.3534356 99,69 L49,37 C45.5596797,34.9042037 43,36.3212206 43,40 Z"></path>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/mobile/android/themes/core/images/privatebrowsing-mask-and-shield.svg b/mobile/android/themes/core/images/privatebrowsing-mask-and-shield.svg
new file mode 100644
index 0000000000..3cb48e9dfa
--- /dev/null
+++ b/mobile/android/themes/core/images/privatebrowsing-mask-and-shield.svg
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<svg width="400" height="138" viewBox="0 0 400 138" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <path d="M72.29 136.64C28.48 137.22 0.29 72.46 0.29 41.98L0.29 4.65C7.14 11.12 24.29 17.93 44.86 11.31 65.43 0.89 99.71-5.92 120.29 7.31 140.86-5.92 175.14 0.89 195.71 11.31 216.29 17.93 233.43 11.12 240.29 4.65L240.29 41.98C240.29 72.46 212.1 137.22 168.29 136.64 140.85 136.28 130.57 130.36 120.29 129.98 110 130.36 99.73 136.28 72.29 136.64ZM100.28 85.11C100.12 75.55 100.11 58.19 76.48 54.7 52.63 51.25 42.46 61.67 32.28 61.46 32.28 79.03 66.2 92.91 100.28 85.11ZM140.28 85.11C140.45 75.55 140.46 58.19 164.08 54.7 187.93 51.25 198.11 61.67 208.28 61.46 208.28 79.03 174.36 92.91 140.28 85.11Z" id="path-1"/>
+ </defs>
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="XXHDPI" transform="translate(-178.000000, -1050.000000)">
+ <g id="graphic-2" transform="translate(178.000000, 1050.901172)">
+ <path d="M399.38 24.32C399.36 19.92 396.45 16.4 392.71 15.32L349.86 8.32 307 15.32C303.26 16.4 300.35 19.92 300.33 24.32 300.31 34.22 300.37 52.28 301.29 60.32 303.17 83.36 307.43 94.81 317.48 108.32 330.16 126.08 349.86 128.32 349.86 128.32 349.86 128.32 369.56 126.08 382.24 108.32 392.28 94.81 396.54 83.36 398.43 60.32 399.34 52.28 399.4 34.22 399.38 24.32L399.38 24.32 399.38 24.32ZM390.81 59.32C388.95 82.65 384.72 92.07 376.52 103.32 367.16 116.27 353.36 119.66 349.86 120.32 346.31 119.65 332.54 116.26 323.19 103.32 314.99 92.07 310.76 82.65 308.9 59.32 308.17 53.88 307.91 40.97 307.95 24.32 307.96 23.78 308.17 23.56 308.9 23.32L349.86 16.32 390.81 23.32C391.54 23.56 391.75 23.78 391.76 24.32 391.8 40.96 391.54 53.87 390.81 59.32ZM326.05 98.32C318.68 87.15 315.78 78.92 314.62 59.32 313.99 54.06 313.66 45.26 313.67 30.32L348.9 23.32 348.9 113.32C344.72 112.4 334.23 108.99 326.05 98.32Z" id="XXHDPI" fill="#FFFFFF"/>
+ <g id="Path-Copy">
+ <use fill="none" xlink:href="#path-1"/>
+ <use fill="none" xlink:href="#path-1"/>
+ <use fill="#5F6368" fill-rule="evenodd" xlink:href="#path-1"/>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/mobile/android/themes/core/images/privatebrowsing-mask.png b/mobile/android/themes/core/images/privatebrowsing-mask.png
new file mode 100644
index 0000000000..e62cdbe137
--- /dev/null
+++ b/mobile/android/themes/core/images/privatebrowsing-mask.png
Binary files differ
diff --git a/mobile/android/themes/core/images/reader-minus-hdpi.png b/mobile/android/themes/core/images/reader-minus-hdpi.png
new file mode 100644
index 0000000000..d6ea9b3fdf
--- /dev/null
+++ b/mobile/android/themes/core/images/reader-minus-hdpi.png
Binary files differ
diff --git a/mobile/android/themes/core/images/reader-minus-xhdpi.png b/mobile/android/themes/core/images/reader-minus-xhdpi.png
new file mode 100644
index 0000000000..5597200540
--- /dev/null
+++ b/mobile/android/themes/core/images/reader-minus-xhdpi.png
Binary files differ
diff --git a/mobile/android/themes/core/images/reader-minus-xxhdpi.png b/mobile/android/themes/core/images/reader-minus-xxhdpi.png
new file mode 100644
index 0000000000..355d82f9fd
--- /dev/null
+++ b/mobile/android/themes/core/images/reader-minus-xxhdpi.png
Binary files differ
diff --git a/mobile/android/themes/core/images/reader-plus-hdpi.png b/mobile/android/themes/core/images/reader-plus-hdpi.png
new file mode 100644
index 0000000000..87a3267833
--- /dev/null
+++ b/mobile/android/themes/core/images/reader-plus-hdpi.png
Binary files differ
diff --git a/mobile/android/themes/core/images/reader-plus-xhdpi.png b/mobile/android/themes/core/images/reader-plus-xhdpi.png
new file mode 100644
index 0000000000..ff6fc2e33b
--- /dev/null
+++ b/mobile/android/themes/core/images/reader-plus-xhdpi.png
Binary files differ
diff --git a/mobile/android/themes/core/images/reader-plus-xxhdpi.png b/mobile/android/themes/core/images/reader-plus-xxhdpi.png
new file mode 100644
index 0000000000..6f475c5d3d
--- /dev/null
+++ b/mobile/android/themes/core/images/reader-plus-xxhdpi.png
Binary files differ
diff --git a/mobile/android/themes/core/images/reader-style-icon-hdpi.png b/mobile/android/themes/core/images/reader-style-icon-hdpi.png
new file mode 100644
index 0000000000..ea7578d286
--- /dev/null
+++ b/mobile/android/themes/core/images/reader-style-icon-hdpi.png
Binary files differ
diff --git a/mobile/android/themes/core/images/reader-style-icon-xhdpi.png b/mobile/android/themes/core/images/reader-style-icon-xhdpi.png
new file mode 100644
index 0000000000..fb12ed007e
--- /dev/null
+++ b/mobile/android/themes/core/images/reader-style-icon-xhdpi.png
Binary files differ
diff --git a/mobile/android/themes/core/images/reader-style-icon-xxhdpi.png b/mobile/android/themes/core/images/reader-style-icon-xxhdpi.png
new file mode 100644
index 0000000000..57c6a1964a
--- /dev/null
+++ b/mobile/android/themes/core/images/reader-style-icon-xxhdpi.png
Binary files differ
diff --git a/mobile/android/themes/core/images/scrubber.svg b/mobile/android/themes/core/images/scrubber.svg
new file mode 100644
index 0000000000..f93a147b76
--- /dev/null
+++ b/mobile/android/themes/core/images/scrubber.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+<svg width="36px" height="36px" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g transform="translate(-486.000000, -1444.000000)" fill="#FFFFFF">
+ <g transform="translate(432.000000, 1390.000000)">
+ <circle id="Oval-17" cx="72" cy="72" r="18"></circle>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/mobile/android/themes/core/images/search-clear-30.png b/mobile/android/themes/core/images/search-clear-30.png
new file mode 100644
index 0000000000..75af18a3eb
--- /dev/null
+++ b/mobile/android/themes/core/images/search-clear-30.png
Binary files differ
diff --git a/mobile/android/themes/core/images/search.png b/mobile/android/themes/core/images/search.png
new file mode 100644
index 0000000000..18460da05d
--- /dev/null
+++ b/mobile/android/themes/core/images/search.png
Binary files differ
diff --git a/mobile/android/themes/core/images/textfield.png b/mobile/android/themes/core/images/textfield.png
new file mode 100644
index 0000000000..c3210cd719
--- /dev/null
+++ b/mobile/android/themes/core/images/textfield.png
Binary files differ
diff --git a/mobile/android/themes/core/images/throbber.png b/mobile/android/themes/core/images/throbber.png
new file mode 100644
index 0000000000..8a7bfb6ab2
--- /dev/null
+++ b/mobile/android/themes/core/images/throbber.png
Binary files differ
diff --git a/mobile/android/themes/core/images/unmute.svg b/mobile/android/themes/core/images/unmute.svg
new file mode 100644
index 0000000000..47c823fd82
--- /dev/null
+++ b/mobile/android/themes/core/images/unmute.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+<svg width="45px" height="60px" viewBox="0 0 45 60" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g transform="translate(-661.000000, -1048.000000)" fill="#FFFFFF">
+ <g transform="translate(624.000000, 1006.000000)">
+ <path d="M37,61.9999094 C37,59.2105478 39.2364417,56.9712087 42,56.9999094 L58,56.9999094 L75.5,42.9999094 C79.1323004,40.818613 82,42.3919545 82,46.4999094 L82,97.4999094 C82,101.618951 79.1389004,103.178674 75.5,100.999909 L58,86.9999094 L42,86.9999094 C39.2362844,87.0258469 37,84.7863502 37,81.9999094 L37,61.9999094 Z"></path>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/mobile/android/themes/core/images/wordmark-hdpi.png b/mobile/android/themes/core/images/wordmark-hdpi.png
new file mode 100644
index 0000000000..3d3b145deb
--- /dev/null
+++ b/mobile/android/themes/core/images/wordmark-hdpi.png
Binary files differ
diff --git a/mobile/android/themes/core/jar.mn b/mobile/android/themes/core/jar.mn
new file mode 100644
index 0000000000..248c002532
--- /dev/null
+++ b/mobile/android/themes/core/jar.mn
@@ -0,0 +1,98 @@
+#filter substitution
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+chrome.jar:
+% skin browser classic/1.0 %skin/
+ skin/aboutPage.css (aboutPage.css)
+ skin/about.css (about.css)
+ skin/aboutAccounts.css (aboutAccounts.css)
+ skin/aboutAddons.css (aboutAddons.css)
+ skin/aboutBase.css (aboutBase.css)
+ skin/aboutDownloads.css (aboutDownloads.css)
+#ifdef MOZ_SERVICES_HEALTHREPORT
+ skin/aboutHealthReport.css (aboutHealthReport.css)
+#endif
+ skin/aboutMemory.css (aboutMemory.css)
+ skin/aboutPrivateBrowsing.css (aboutPrivateBrowsing.css)
+ skin/aboutReader.css (aboutReader.css)
+ skin/aboutReaderContent.css (aboutReaderContent.css)
+ skin/aboutReaderControls.css (aboutReaderControls.css)
+ skin/aboutSupport.css (aboutSupport.css)
+ skin/content.css (content.css)
+ skin/scrollbar.css (scrollbar-apz.css)
+ skin/config.css (config.css)
+ skin/defines.css (defines.css)
+ skin/touchcontrols.css (touchcontrols.css)
+ skin/netError.css (netError.css)
+ skin/spinner.css (spinner.css)
+% override chrome://global/skin/about.css chrome://browser/skin/about.css
+% override chrome://global/skin/aboutMemory.css chrome://browser/skin/aboutMemory.css
+% override chrome://global/skin/aboutReader.css chrome://browser/skin/aboutReader.css
+% override chrome://global/skin/aboutReaderContent.css chrome://browser/skin/aboutReaderContent.css
+% override chrome://global/skin/aboutReaderControls.css chrome://browser/skin/aboutReaderControls.css
+% override chrome://global/skin/aboutSupport.css chrome://browser/skin/aboutSupport.css
+% override chrome://global/skin/media/videocontrols.css chrome://browser/skin/touchcontrols.css
+% override chrome://global/skin/netError.css chrome://browser/skin/netError.css
+
+ skin/aboutLogins.css (aboutLogins.css)
+
+ skin/images/search.png (images/search.png)
+ skin/images/lock.png (images/lock.png)
+ skin/images/textfield.png (images/textfield.png)
+
+ skin/images/amo-logo.png (images/amo-logo.png)
+ skin/images/arrowdown-16.png (images/arrowdown-16.png)
+ skin/images/arrowup-16.png (images/arrowup-16.png)
+ skin/images/blocked-warning.png (images/blocked-warning.png)
+ skin/images/checkbox_checked.png (images/checkbox_checked.png)
+ skin/images/checkbox_checked_disabled.png (images/checkbox_checked_disabled.png)
+ skin/images/checkbox_checked_pressed.png (images/checkbox_checked_pressed.png)
+ skin/images/checkbox_unchecked.png (images/checkbox_unchecked.png)
+ skin/images/checkbox_unchecked_disabled.png (images/checkbox_unchecked_disabled.png)
+ skin/images/checkbox_unchecked_pressed.png (images/checkbox_unchecked_pressed.png)
+ skin/images/chevron.png (images/chevron.png)
+ skin/images/dropmarker.svg (images/dropmarker.svg)
+ skin/images/dropmarker-right.svg (images/dropmarker-right.svg)
+ skin/images/errorpage-warning.png (images/errorpage-warning.png)
+ skin/images/exitfullscreen.svg (images/exitfullscreen.svg)
+ skin/images/fullscreen.svg (images/fullscreen.svg)
+ skin/images/grey-caution.svg (images/grey-caution.svg)
+ skin/images/certerror-warning.png (images/certerror-warning.png)
+ skin/images/throbber.png (images/throbber.png)
+ skin/images/search-clear-30.png (images/search-clear-30.png)
+ skin/images/placeholder_image.svg (images/placeholder_image.svg)
+ skin/images/play.svg (images/play.svg)
+ skin/images/pause.svg (images/pause.svg)
+ skin/images/cast-ready.svg (images/cast-ready.svg)
+ skin/images/cast-active.svg (images/cast-active.svg)
+ skin/images/mute.svg (images/mute.svg)
+ skin/images/unmute.svg (images/unmute.svg)
+ skin/images/scrubber.svg (images/scrubber.svg)
+ skin/images/about-btn-darkgrey.png (images/about-btn-darkgrey.png)
+ skin/images/logo-hdpi.png (images/logo-hdpi.png)
+ skin/images/wordmark-hdpi.png (images/wordmark-hdpi.png)
+ skin/images/config-plus.png (images/config-plus.png)
+ skin/images/reader-minus-hdpi.png (images/reader-minus-hdpi.png)
+ skin/images/reader-minus-xhdpi.png (images/reader-minus-xhdpi.png)
+ skin/images/reader-minus-xxhdpi.png (images/reader-minus-xxhdpi.png)
+ skin/images/reader-plus-hdpi.png (images/reader-plus-hdpi.png)
+ skin/images/reader-plus-xhdpi.png (images/reader-plus-xhdpi.png)
+ skin/images/reader-plus-xxhdpi.png (images/reader-plus-xxhdpi.png)
+ skin/images/reader-style-icon-hdpi.png (images/reader-style-icon-hdpi.png)
+ skin/images/reader-style-icon-xhdpi.png (images/reader-style-icon-xhdpi.png)
+ skin/images/reader-style-icon-xxhdpi.png (images/reader-style-icon-xxhdpi.png)
+ skin/images/privatebrowsing-mask.png (images/privatebrowsing-mask.png)
+ skin/images/privatebrowsing-mask-and-shield.svg (images/privatebrowsing-mask-and-shield.svg)
+ skin/images/icon_key_emptypage.svg (images/icon_key_emptypage.svg)
+ skin/images/accessiblecaret-normal-hdpi.png (images/accessiblecaret-normal-hdpi.png)
+ skin/images/accessiblecaret-normal-xhdpi.png (images/accessiblecaret-normal-xhdpi.png)
+ skin/images/accessiblecaret-normal-xxhdpi.png (images/accessiblecaret-normal-xxhdpi.png)
+ skin/images/accessiblecaret-tilt-left-hdpi.png (images/accessiblecaret-tilt-left-hdpi.png)
+ skin/images/accessiblecaret-tilt-left-xhdpi.png (images/accessiblecaret-tilt-left-xhdpi.png)
+ skin/images/accessiblecaret-tilt-left-xxhdpi.png (images/accessiblecaret-tilt-left-xxhdpi.png)
+ skin/images/accessiblecaret-tilt-right-hdpi.png (images/accessiblecaret-tilt-right-hdpi.png)
+ skin/images/accessiblecaret-tilt-right-xhdpi.png (images/accessiblecaret-tilt-right-xhdpi.png)
+ skin/images/accessiblecaret-tilt-right-xxhdpi.png (images/accessiblecaret-tilt-right-xxhdpi.png)
diff --git a/mobile/android/themes/core/moz.build b/mobile/android/themes/core/moz.build
new file mode 100644
index 0000000000..eb4454d28f
--- /dev/null
+++ b/mobile/android/themes/core/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file
diff --git a/mobile/android/themes/core/netError.css b/mobile/android/themes/core/netError.css
new file mode 100644
index 0000000000..ba9e397b24
--- /dev/null
+++ b/mobile/android/themes/core/netError.css
@@ -0,0 +1,226 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+html,
+body {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ --moz-vertical-spacing: 10px;
+ --moz-background-height: 32px;
+}
+
+body {
+ /* Add a set of stripes at the top of pages */
+ background-image: linear-gradient(-45deg, #dfe8ee, #dfe8ee 33%,
+ #ecf0f3 33%, #ecf0f3 66%,
+ #dfe8ee 66%, #dfe8ee);
+ background-size: 64px var(--moz-background-height);
+ background-repeat: repeat-x;
+
+ background-color: #f1f1f1;
+ padding: 0 20px;
+
+ font-weight: 300;
+ font-size: 13px;
+ -moz-text-size-adjust: none;
+ font-family: sans-serif;
+}
+
+
+ul {
+ /* Shove the list indicator so that its left aligned, but use outside so that text
+ * doesn't don't wrap the text around it */
+ padding: 0 1em;
+ margin: 0;
+ list-style: round outside none;
+}
+
+#errorShortDesc,
+li:not(:last-of-type) {
+ /* Margins between the li and buttons below it won't be collapsed. Remove the bottom margin here. */
+ margin: var(--moz-vertical-spacing) 0;
+}
+
+li > button {
+ /* Removing the normal padding on the li so this stretched edge to edge. */
+ margin-left: -1em;
+ margin-right: -1em;
+ width: calc(100% + 2em);
+}
+
+/* Push the #ignoreWarningButton to the bottom on the blocked site page */
+.blockedsite > #errorPageContainer > #errorLongContent {
+ flex: 1;
+}
+
+h1 {
+ margin: 0;
+ /* Since this has an underline, use padding for vertical spacing rather than margin */
+ padding: var(--moz-vertical-spacing) 0;
+ font-weight: 300;
+ border-bottom: 1px solid #e0e2e5;
+}
+
+h2 {
+ font-size: small;
+ padding: 0;
+ margin: var(--moz-vertical-spacing) 0;
+}
+
+p {
+ margin: var(--moz-vertical-spacing) 0;
+}
+
+button {
+ /* Force buttons to display: block here to try and enfoce collapsing margins */
+ display: block;
+ width: 100%;
+ border: none;
+ padding: 1rem;
+ font-family: sans-serif;
+ background-color: #e0e2e5;
+ font-weight: 300;
+ border-radius: 2px;
+ background-image: none;
+ margin: var(--moz-vertical-spacing) 0 0;
+}
+
+button.inProgress {
+ background-image: linear-gradient(-45deg, #dfe8ee, #dfe8ee 33%,
+ #ecf0f3 33%, #ecf0f3 66%,
+ #dfe8ee 66%, #dfe8ee);
+ background-size: 37px 5px;
+ background-repeat: repeat-x;
+ animation: progress 6s linear infinite;
+}
+
+@keyframes progress {
+ from { background-position: 0 100%; }
+ to { background-position: 100% 100%; }
+}
+
+.certerror {
+ background-image: linear-gradient(-45deg, #f0d000, #f0d000 33%,
+ #fedc00 33%, #fedc00 66%,
+ #f0d000 66%, #f0d000);
+}
+
+.blockedsite {
+ background-image: linear-gradient(-45deg, #9b2e2e, #9b2e2e 33%,
+ #a83232 33%, #a83232 66%,
+ #9b2e2e 66%, #9b2e2e);
+ background-color: #b14646;
+ color: white;
+}
+
+#errorPageContainer {
+ /* If the page is greater than 550px center the content.
+ * This number should be kept in sync with the media query for tablets below */
+ max-width: 550px;
+ margin: 0 auto;
+ transform: translateY(var(--moz-background-height));
+ padding-bottom: var(--moz-vertical-spacing);
+
+ min-height: calc(100% - var(--moz-background-height) - var(--moz-vertical-spacing));
+ display: flex;
+ flex-direction: column;
+}
+
+/* Expanders have a structure of
+ * <div collapsed="true/false">
+ * <h2 class="expander">Title</h2>
+ * <p>Content</p>
+ * </div>
+ *
+ * This shows an arrow to the right of the h2 element, and hides the content when collapsed="true". */
+.expander {
+ margin: var(--moz-vertical-spacing) 0;
+ background-image: url("chrome://browser/skin/images/dropmarker.svg");
+ background-repeat: no-repeat;
+ /* dropmarker.svg is 10x7. Ensure that its centered in the middle of an 18x18 box */
+ background-position: 3px 5.5px;
+ background-size: 10px 7px;
+ padding-left: 18px;
+}
+
+div[collapsed="true"] > .expander {
+ background-image: url("chrome://browser/skin/images/dropmarker-right.svg");
+ /* dropmarker.svg is 7x10. Ensure that its centered in the middle of an 18x18 box */
+ background-size: 7px 10px;
+ background-position: 5.5px 4px;
+}
+
+div[hidden] > .expander,
+div[hidden] > .expander + *,
+div[collapsed="true"] > .expander + * {
+ display: none;
+}
+
+.blockedsite h1 {
+ border-bottom-color: #9b2e2e;
+}
+
+.blockedsite button {
+ background-color: #9b2e2e;
+ color: white;
+}
+
+/* Style warning button to look like a small text link in the
+ bottom. This is preferable to just using a text link
+ since there is already a mechanism in browser.js for trapping
+ oncommand events from unprivileged chrome pages (ErrorPageEventHandler).*/
+#ignoreWarningButton {
+ width: calc(100% + 40px);
+ -moz-appearance: none;
+ background: #b14646;
+ border: none;
+ text-decoration: underline;
+ margin: 0;
+ margin-inline-start: -20px;
+ font-size: smaller;
+ border-radius: 0;
+}
+
+/* On large screen devices (hopefully a 7+ inch tablet, we already center content (see #errorPageContainer above).
+ Apply tablet specific styles here */
+@media (min-width: 550px) {
+ button {
+ min-width: 160px;
+ width: auto;
+ }
+
+ /* If the tablet is tall as well, add some padding to make content feel a bit more centered */
+ @media (min-height: 550px) {
+ #errorPageContainer {
+ padding-top: 64px;
+ min-height: calc(100% - 64px);
+ }
+ }
+}
+
+#searchbox {
+ padding: 0;
+ display: flex;
+ margin: var(--moz-vertical-spacing) -1em;
+}
+
+#searchbox > input {
+ flex: 3;
+ padding: 0em 3em 0em 1em;
+ width: 100%;
+ border: none;
+ font-family: sans-serif;
+ background-image: none;
+ background-color: white;
+ border-radius-top-right: none;
+ border-radius-bottom-right: none;
+}
+
+#searchbox > button {
+ flex: 1;
+ margin: 0;
+ width: auto;
+}
+
diff --git a/mobile/android/themes/core/scrollbar-apz.css b/mobile/android/themes/core/scrollbar-apz.css
new file mode 100644
index 0000000000..652881af5f
--- /dev/null
+++ b/mobile/android/themes/core/scrollbar-apz.css
@@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@namespace url("http://www.w3.org/1999/xhtml");
+@namespace xul url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+html xul|scrollbar {
+ display: block;
+}
diff --git a/mobile/android/themes/core/spinner.css b/mobile/android/themes/core/spinner.css
new file mode 100644
index 0000000000..7f4dd33bbf
--- /dev/null
+++ b/mobile/android/themes/core/spinner.css
@@ -0,0 +1,124 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.mui-refresh-main {
+ padding: 0;
+ overflow: hidden;
+ border-radius: 999px;
+ position: relative;
+}
+
+.mui-refresh-wrapper {
+ width: 60px;
+ height: 60px;
+}
+
+.mui-spinner-main {
+ width: 60px;
+ height: 60px;
+ position: relative;
+ animation: sporadic-rotate 5.25s cubic-bezier(.35, 0, .25, 1) infinite;
+}
+
+.mui-spinner-wrapper {
+ animation: outer-rotate 2.91667s linear infinite;
+}
+
+.mui-spinner-left, .mui-spinner-right {
+ position: absolute;
+ top: 0;
+ height: 60px;
+ width: 30px;
+ overflow: hidden;
+}
+
+.mui-spinner-left {
+ left: 0;
+}
+
+.mui-spinner-right {
+ right: 0;
+}
+
+.mui-half-circle-left, .mui-half-circle-right {
+ position: absolute;
+ top: 0;
+ width: 60px;
+ height: 60px;
+ box-sizing: border-box;
+ border-width: 5px;
+ border-style: solid;
+ border-color: #000 #000 transparent;
+ border-radius: 999px;
+ animation-iteration-count: infinite;
+ animation-duration: 1.3125s;
+ animation-timing-function: cubic-bezier(.35, 0, .25, 1);
+}
+
+.mui-half-circle-left {
+ left: 0;
+ border-right-color: transparent;
+ border-top-color: #FF9500; /*matched to fennec_ui_orange in java codebase*/
+ border-left-color: #FF9500; /*matched to fennec_ui_orange in java codebase*/
+ animation-name: left-wobble;
+}
+
+.mui-half-circle-right {
+ right: 0;
+ border-left-color: transparent;
+ border-top-color: #FF9500; /*matched to fennec_ui_orange in java codebase*/
+ border-right-color: #FF9500; /*matched to fennec_ui_orange in java codebase*/
+ animation-name: right-wobble;
+}
+
+@keyframes outer-rotate {
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes left-wobble {
+ 0%, 100% {
+ transform: rotate(130deg);
+ }
+ 50% {
+ transform: rotate(-5deg);
+ }
+}
+
+@keyframes right-wobble {
+ 0%, 100% {
+ transform: rotate(-130deg);
+ }
+ 50% {
+ transform: rotate(5deg);
+ }
+}
+
+@keyframes sporadic-rotate {
+ 12.5% {
+ transform: rotate(135deg);
+ }
+ 25% {
+ transform: rotate(270deg);
+ }
+ 37.5% {
+ transform: rotate(405deg);
+ }
+ 50% {
+ transform: rotate(540deg);
+ }
+ 62.5% {
+ transform: rotate(675deg);
+ }
+ 75% {
+ transform: rotate(810deg);
+ }
+ 87.5% {
+ transform: rotate(945deg);
+ }
+ 100% {
+ transform: rotate(1080deg);
+ }
+}
diff --git a/mobile/android/themes/core/touchcontrols.css b/mobile/android/themes/core/touchcontrols.css
new file mode 100644
index 0000000000..8f43bf7189
--- /dev/null
+++ b/mobile/android/themes/core/touchcontrols.css
@@ -0,0 +1,255 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@namespace url(http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul);
+
+/* video controls */
+.controlsOverlay {
+ -moz-box-pack: center;
+ -moz-box-align: end;
+ -moz-box-flex: 1;
+ -moz-box-orient: horizontal;
+}
+
+.controlsOverlay[scaled] {
+ /* scaled attribute in videocontrols.css causes conflict
+ due to different -moz-box-orient values */
+ -moz-box-align: end;
+}
+
+.controlsSpacer {
+ display: none;
+ -moz-box-flex: 0;
+}
+
+.controlBar {
+ -moz-box-flex: 1;
+ width: 100%;
+ background-color: rgba(50,50,50,0.8);
+}
+
+.buttonsBar {
+ -moz-box-flex: 1;
+ -moz-box-align: center;
+}
+
+.playButton,
+.castingButton,
+.muteButton,
+.fullscreenButton {
+ -moz-appearance: none;
+ padding: 15px;
+ border: none !important;
+ width: 48px;
+ height: 48px;
+}
+
+.playButton {
+ background: url("chrome://browser/skin/images/pause.svg") no-repeat center;
+ background-size: contain;
+ background-origin: content-box;
+}
+
+.playButton[paused="true"] {
+ background: url("chrome://browser/skin/images/play.svg") no-repeat center;
+ background-size: contain;
+ background-origin: content-box;
+}
+
+.castingButton {
+ background: url("chrome://browser/skin/images/cast-ready.svg") no-repeat center;
+ background-size: contain;
+ background-origin: content-box;
+}
+
+.castingButton[active="true"] {
+ background: url("chrome://browser/skin/images/cast-active.svg") no-repeat center;
+ background-size: contain;
+ background-origin: content-box;
+}
+
+/* If the casting button is showing, there will be two buttons on the right side of the controls.
+ * This shifts the play button to be centered.
+ */
+.castingButton:not([hidden="true"]) + .fullscreenButton + spacer + .playButton {
+ transform: translateX(-21px);
+}
+
+.muteButton {
+ padding-left: 17.25px;
+ padding-right: 9.75px;
+ background: url("chrome://browser/skin/images/mute.svg") no-repeat left;
+ background-size: contain;
+ background-origin: content-box;
+}
+
+.muteButton[muted="true"] {
+ background: url("chrome://browser/skin/images/unmute.svg") no-repeat left;
+ background-size: contain;
+ background-origin: content-box;
+}
+
+.fullscreenButton {
+ background-color: transparent;
+ background: url("chrome://browser/skin/images/fullscreen.svg") no-repeat center;
+ background-size: contain;
+ background-origin: content-box;
+}
+
+.fullscreenButton[fullscreened] {
+ background: url("chrome://browser/skin/images/exitfullscreen.svg") no-repeat center;
+ background-size: contain;
+ background-origin: content-box;
+}
+
+.controlBar[fullscreen-unavailable] .fullscreenButton {
+ display: none;
+}
+
+/* bars */
+.scrubberStack {
+ -moz-box-flex: 1;
+ padding: 0px 18px;
+}
+
+.flexibleBar,
+.flexibleBar .progress-bar,
+.bufferBar,
+.bufferBar .progress-bar,
+.progressBar,
+.progressBar .progress-bar,
+.scrubber,
+.scrubber .scale-slider,
+.scrubber .scale-thumb {
+ -moz-appearance: none;
+ border: none;
+ padding: 0px;
+ margin: 0px;
+ background-color: transparent;
+}
+
+.flexibleBar,
+.bufferBar,
+.progressBar {
+ height: 32px;
+ padding: 15px 0px;
+}
+
+.flexibleBar {
+ padding: 16px 0px;
+}
+
+.flexibleBar .progress-bar {
+ border: 1px #777777 solid;
+ border-radius: 1px;
+}
+
+.bufferBar .progress-bar {
+ border: 2px #AFB1B3 solid;
+ border-radius: 2px;
+}
+
+.progressBar .progress-bar {
+ border: 2px #FF9500 solid;
+ border-radius: 2px;
+}
+
+.scrubber {
+ margin-left: -12px;
+ margin-right: -12px;
+}
+
+.scrubber .scale-thumb {
+ display: -moz-box;
+ margin: 0px !important;
+ padding: 0px !important;
+ background: url("chrome://browser/skin/images/scrubber.svg") no-repeat center;
+ background-size: 12px 12px;
+ height: 32px;
+ width: 32px;
+}
+
+.positionLabel, .durationLabel {
+ font-family: 'Roboto', Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ color: white;
+}
+
+.statusOverlay {
+ -moz-box-align: center;
+ -moz-box-pack: center;
+ background-color: rgb(50,50,50);
+}
+
+.statusIcon {
+ margin-bottom: 28px;
+ width: 36px;
+ height: 36px;
+}
+
+.statusIcon[type="throbber"] {
+ background: url(chrome://global/skin/media/throbber.png) no-repeat center;
+}
+
+.statusIcon[type="error"] {
+ background: url(chrome://global/skin/media/error.png) no-repeat center;
+}
+
+/* CSS Transitions */
+.controlBar:not([immediate]) {
+ transition-property: opacity;
+ transition-duration: 200ms;
+}
+
+.controlBar[fadeout] {
+ opacity: 0;
+}
+
+.statusOverlay:not([immediate]) {
+ transition-property: opacity;
+ transition-duration: 300ms;
+ transition-delay: 750ms;
+}
+
+.statusOverlay[fadeout] {
+ opacity: 0;
+}
+
+.volumeStack,
+.timeLabel {
+ display: none;
+}
+
+.controlBar[firstshow="true"] .playButton {
+ -moz-transform: none;
+}
+
+/* Error description formatting */
+.errorLabel {
+ font-family: Helvetica, Arial, sans-serif;
+ font-size: 11px;
+ color: #bbb;
+ text-shadow:
+ -1px -1px 0 #000,
+ 1px -1px 0 #000,
+ -1px 1px 0 #000,
+ 1px 1px 0 #000;
+ padding: 0 10px;
+ text-align: center;
+}
+
+/* Overlay Play button */
+.clickToPlay {
+ width: 64px;
+ height: 64px;
+ -moz-box-pack: center;
+ -moz-box-align: center;
+ opacity: 0.7;
+ background-image: url(chrome://global/skin/media/clicktoplay-bgtexture.png),
+ url(chrome://global/skin/media/videoClickToPlayButton.svg);
+ background-repeat: repeat, no-repeat;
+ background-position: center, center;
+ background-size: auto, 64px 64px;
+ background-color: hsla(0,0%,10%,.5);
+}
diff --git a/mobile/android/thirdparty/AndroidManifest.xml b/mobile/android/thirdparty/AndroidManifest.xml
new file mode 100644
index 0000000000..3638db69db
--- /dev/null
+++ b/mobile/android/thirdparty/AndroidManifest.xml
@@ -0,0 +1,4 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.mozilla.gecko.thirdparty_unused">
+
+</manifest>
diff --git a/mobile/android/thirdparty/README b/mobile/android/thirdparty/README
new file mode 100644
index 0000000000..5f813e4bce
--- /dev/null
+++ b/mobile/android/thirdparty/README
@@ -0,0 +1,3 @@
+This directory contains the source code of Java libraries used by
+Mozilla Fennec (Firefox for Android) but developed by third-party
+organizations.
diff --git a/mobile/android/thirdparty/build.gradle b/mobile/android/thirdparty/build.gradle
new file mode 100644
index 0000000000..a192e5009c
--- /dev/null
+++ b/mobile/android/thirdparty/build.gradle
@@ -0,0 +1,54 @@
+buildDir "${topobjdir}/gradle/build/mobile/android/thirdparty"
+
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 23
+ buildToolsVersion mozconfig.substs.ANDROID_BUILD_TOOLS_VERSION
+
+ defaultConfig {
+ targetSdkVersion 23
+ minSdkVersion 15
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_7
+ targetCompatibility JavaVersion.VERSION_1_7
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+
+ sourceSets {
+ main {
+ manifest.srcFile 'AndroidManifest.xml'
+ java {
+ srcDir '.'
+
+ if (!mozconfig.substs.MOZ_INSTALL_TRACKING) {
+ exclude 'com/adjust/**'
+ }
+
+ // Exclude LeakCanary: It will be added again via a gradle dependency. This version
+ // here is only the no-op library for mach-based builds.
+ exclude 'com/squareup/leakcanary/**'
+ }
+ }
+ }
+}
+
+dependencies {
+ compile "com.android.support:support-v4:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+}
+
+apply plugin: 'idea'
+
+idea {
+ module {
+ // This is cosmetic. See the excludes in the root project.
+ if (!mozconfig.substs.MOZ_INSTALL_TRACKING) {
+ excludeDirs += file('com/adjust/sdk')
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/ConnectionClosedException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/ConnectionClosedException.java
new file mode 100644
index 0000000000..bcd7308df9
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/ConnectionClosedException.java
@@ -0,0 +1,50 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import java.io.IOException;
+
+/**
+ * Signals that the connection has been closed unexpectedly.
+ *
+ * @since 4.0
+ */
+public class ConnectionClosedException extends IOException {
+
+ private static final long serialVersionUID = 617550366255636674L;
+
+ /**
+ * Creates a new ConnectionClosedException with the specified detail message.
+ *
+ * @param message The exception detail message
+ */
+ public ConnectionClosedException(final String message) {
+ super(message);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/ConnectionReuseStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/ConnectionReuseStrategy.java
new file mode 100644
index 0000000000..ec099b0010
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/ConnectionReuseStrategy.java
@@ -0,0 +1,70 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * Interface for deciding whether a connection can be re-used for
+ * subsequent requests and should be kept alive.
+ * <p>
+ * Implementations of this interface must be thread-safe. Access to shared
+ * data must be synchronized as methods of this interface may be executed
+ * from multiple threads.
+ *
+ * @since 4.0
+ */
+public interface ConnectionReuseStrategy {
+
+ /**
+ * Decides whether a connection can be kept open after a request.
+ * If this method returns <code>false</code>, the caller MUST
+ * close the connection to correctly comply with the HTTP protocol.
+ * If it returns <code>true</code>, the caller SHOULD attempt to
+ * keep the connection open for reuse with another request.
+ * <br/>
+ * One can use the HTTP context to retrieve additional objects that
+ * may be relevant for the keep-alive strategy: the actual HTTP
+ * connection, the original HTTP request, target host if known,
+ * number of times the connection has been reused already and so on.
+ * <br/>
+ * If the connection is already closed, <code>false</code> is returned.
+ * The stale connection check MUST NOT be triggered by a
+ * connection reuse strategy.
+ *
+ * @param response
+ * The last response received over that connection.
+ * @param context the context in which the connection is being
+ * used.
+ *
+ * @return <code>true</code> if the connection is allowed to be reused, or
+ * <code>false</code> if it MUST NOT be reused
+ */
+ boolean keepAlive(HttpResponse response, HttpContext context);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/Consts.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/Consts.java
new file mode 100644
index 0000000000..81e2a75923
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/Consts.java
@@ -0,0 +1,51 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import java.nio.charset.Charset;
+
+/**
+ * Commons constants.
+ *
+ * @since 4.2
+ */
+public final class Consts {
+
+ public static final int CR = 13; // <US-ASCII CR, carriage return (13)>
+ public static final int LF = 10; // <US-ASCII LF, linefeed (10)>
+ public static final int SP = 32; // <US-ASCII SP, space (32)>
+ public static final int HT = 9; // <US-ASCII HT, horizontal-tab (9)>
+
+ public static final Charset UTF_8 = Charset.forName("UTF-8");
+ public static final Charset ASCII = Charset.forName("US-ASCII");
+ public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
+
+ private Consts() {
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/ContentTooLongException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/ContentTooLongException.java
new file mode 100644
index 0000000000..64f83b3af0
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/ContentTooLongException.java
@@ -0,0 +1,50 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import java.io.IOException;
+
+/**
+ * Signals that HTTP entity content is too long.
+ *
+ * @since 4.2
+ */
+public class ContentTooLongException extends IOException {
+
+ private static final long serialVersionUID = -924287689552495383L;
+
+ /**
+ * Creates a new ContentTooLongException with the specified detail message.
+ *
+ * @param message exception message
+ */
+ public ContentTooLongException(final String message) {
+ super(message);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/FormattedHeader.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/FormattedHeader.java
new file mode 100644
index 0000000000..cd3f4126d1
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/FormattedHeader.java
@@ -0,0 +1,60 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * An HTTP header which is already formatted.
+ * For example when headers are received, the original formatting
+ * can be preserved. This allows for the header to be sent without
+ * another formatting step.
+ *
+ * @since 4.0
+ */
+public interface FormattedHeader extends Header {
+
+ /**
+ * Obtains the buffer with the formatted header.
+ * The returned buffer MUST NOT be modified.
+ *
+ * @return the formatted header, in a buffer that must not be modified
+ */
+ CharArrayBuffer getBuffer();
+
+ /**
+ * Obtains the start of the header value in the {@link #getBuffer buffer}.
+ * By accessing the value in the buffer, creation of a temporary string
+ * can be avoided.
+ *
+ * @return index of the first character of the header value
+ * in the buffer returned by {@link #getBuffer getBuffer}.
+ */
+ int getValuePos();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/Header.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/Header.java
new file mode 100644
index 0000000000..50ce75a922
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/Header.java
@@ -0,0 +1,74 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+/**
+ * Represents an HTTP header field.
+ *
+ * <p>The HTTP header fields follow the same generic format as
+ * that given in Section 3.1 of RFC 822. Each header field consists
+ * of a name followed by a colon (":") and the field value. Field names
+ * are case-insensitive. The field value MAY be preceded by any amount
+ * of LWS, though a single SP is preferred.
+ *
+ *<pre>
+ * message-header = field-name ":" [ field-value ]
+ * field-name = token
+ * field-value = *( field-content | LWS )
+ * field-content = &lt;the OCTETs making up the field-value
+ * and consisting of either *TEXT or combinations
+ * of token, separators, and quoted-string&gt;
+ *</pre>
+ *
+ * @since 4.0
+ */
+public interface Header {
+
+ /**
+ * Get the name of the Header.
+ *
+ * @return the name of the Header, never {@code null}
+ */
+ String getName();
+
+ /**
+ * Get the value of the Header.
+ *
+ * @return the value of the Header, may be {@code null}
+ */
+ String getValue();
+
+ /**
+ * Parses the value.
+ *
+ * @return an array of {@link HeaderElement} entries, may be empty, but is never {@code null}
+ * @throws ParseException
+ */
+ HeaderElement[] getElements() throws ParseException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HeaderElement.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HeaderElement.java
new file mode 100644
index 0000000000..f6fcf8f047
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HeaderElement.java
@@ -0,0 +1,108 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+/**
+ * One element of an HTTP {@link Header header} value consisting of
+ * a name / value pair and a number of optional name / value parameters.
+ * <p>
+ * Some HTTP headers (such as the set-cookie header) have values that
+ * can be decomposed into multiple elements. Such headers must be in the
+ * following form:
+ * </p>
+ * <pre>
+ * header = [ element ] *( "," [ element ] )
+ * element = name [ "=" [ value ] ] *( ";" [ param ] )
+ * param = name [ "=" [ value ] ]
+ *
+ * name = token
+ * value = ( token | quoted-string )
+ *
+ * token = 1*&lt;any char except "=", ",", ";", &lt;"&gt; and
+ * white space&gt;
+ * quoted-string = &lt;"&gt; *( text | quoted-char ) &lt;"&gt;
+ * text = any char except &lt;"&gt;
+ * quoted-char = "\" char
+ * </pre>
+ * <p>
+ * Any amount of white space is allowed between any part of the
+ * header, element or param and is ignored. A missing value in any
+ * element or param will be stored as the empty {@link String};
+ * if the "=" is also missing <var>null</var> will be stored instead.
+ *
+ * @since 4.0
+ */
+public interface HeaderElement {
+
+ /**
+ * Returns header element name.
+ *
+ * @return header element name
+ */
+ String getName();
+
+ /**
+ * Returns header element value.
+ *
+ * @return header element value
+ */
+ String getValue();
+
+ /**
+ * Returns an array of name / value pairs.
+ *
+ * @return array of name / value pairs
+ */
+ NameValuePair[] getParameters();
+
+ /**
+ * Returns the first parameter with the given name.
+ *
+ * @param name parameter name
+ *
+ * @return name / value pair
+ */
+ NameValuePair getParameterByName(String name);
+
+ /**
+ * Returns the total count of parameters.
+ *
+ * @return parameter count
+ */
+ int getParameterCount();
+
+ /**
+ * Returns parameter with the given index.
+ *
+ * @param index index
+ * @return name / value pair
+ */
+ NameValuePair getParameter(int index);
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HeaderElementIterator.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HeaderElementIterator.java
new file mode 100644
index 0000000000..c0ef11346d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HeaderElementIterator.java
@@ -0,0 +1,57 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import java.util.Iterator;
+
+/**
+ * A type-safe iterator for {@link HeaderElement} objects.
+ *
+ * @since 4.0
+ */
+public interface HeaderElementIterator extends Iterator<Object> {
+
+ /**
+ * Indicates whether there is another header element in this
+ * iteration.
+ *
+ * @return <code>true</code> if there is another header element,
+ * <code>false</code> otherwise
+ */
+ boolean hasNext();
+
+ /**
+ * Obtains the next header element from this iteration.
+ * This method should only be called while {@link #hasNext hasNext}
+ * is true.
+ *
+ * @return the next header element in this iteration
+ */
+ HeaderElement nextElement();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HeaderIterator.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HeaderIterator.java
new file mode 100644
index 0000000000..a6879cea3d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HeaderIterator.java
@@ -0,0 +1,56 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import java.util.Iterator;
+
+/**
+ * A type-safe iterator for {@link Header} objects.
+ *
+ * @since 4.0
+ */
+public interface HeaderIterator extends Iterator<Object> {
+
+ /**
+ * Indicates whether there is another header in this iteration.
+ *
+ * @return <code>true</code> if there is another header,
+ * <code>false</code> otherwise
+ */
+ boolean hasNext();
+
+ /**
+ * Obtains the next header from this iteration.
+ * This method should only be called while {@link #hasNext hasNext}
+ * is true.
+ *
+ * @return the next header in this iteration
+ */
+ Header nextHeader();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpClientConnection.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpClientConnection.java
new file mode 100644
index 0000000000..f390700b65
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpClientConnection.java
@@ -0,0 +1,102 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import java.io.IOException;
+
+/**
+ * A client-side HTTP connection, which can be used for sending
+ * requests and receiving responses.
+ *
+ * @since 4.0
+ */
+public interface HttpClientConnection extends HttpConnection {
+
+ /**
+ * Checks if response data is available from the connection. May wait for
+ * the specified time until some data becomes available. Note that some
+ * implementations may completely ignore the timeout parameter.
+ *
+ * @param timeout the maximum time in milliseconds to wait for data
+ * @return true if data is available; false if there was no data available
+ * even after waiting for <code>timeout</code> milliseconds.
+ * @throws IOException if an error happens on the connection
+ */
+ boolean isResponseAvailable(int timeout)
+ throws IOException;
+
+ /**
+ * Sends the request line and all headers over the connection.
+ * @param request the request whose headers to send.
+ * @throws HttpException in case of HTTP protocol violation
+ * @throws IOException in case of an I/O error
+ */
+ void sendRequestHeader(HttpRequest request)
+ throws HttpException, IOException;
+
+ /**
+ * Sends the request entity over the connection.
+ * @param request the request whose entity to send.
+ * @throws HttpException in case of HTTP protocol violation
+ * @throws IOException in case of an I/O error
+ */
+ void sendRequestEntity(HttpEntityEnclosingRequest request)
+ throws HttpException, IOException;
+
+ /**
+ * Receives the request line and headers of the next response available from
+ * this connection. The caller should examine the HttpResponse object to
+ * find out if it should try to receive a response entity as well.
+ *
+ * @return a new HttpResponse object with status line and headers
+ * initialized.
+ * @throws HttpException in case of HTTP protocol violation
+ * @throws IOException in case of an I/O error
+ */
+ HttpResponse receiveResponseHeader()
+ throws HttpException, IOException;
+
+ /**
+ * Receives the next response entity available from this connection and
+ * attaches it to an existing HttpResponse object.
+ *
+ * @param response the response to attach the entity to
+ * @throws HttpException in case of HTTP protocol violation
+ * @throws IOException in case of an I/O error
+ */
+ void receiveResponseEntity(HttpResponse response)
+ throws HttpException, IOException;
+
+ /**
+ * Writes out all pending buffered data over the open connection.
+ *
+ * @throws IOException in case of an I/O error
+ */
+ void flush() throws IOException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpConnection.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpConnection.java
new file mode 100644
index 0000000000..f4891358f8
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpConnection.java
@@ -0,0 +1,104 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+/**
+ * A generic HTTP connection, useful on client and server side.
+ *
+ * @since 4.0
+ */
+public interface HttpConnection extends Closeable {
+
+ /**
+ * Closes this connection gracefully.
+ * This method will attempt to flush the internal output
+ * buffer prior to closing the underlying socket.
+ * This method MUST NOT be called from a different thread to force
+ * shutdown of the connection. Use {@link #shutdown shutdown} instead.
+ */
+ void close() throws IOException;
+
+ /**
+ * Checks if this connection is open.
+ * @return true if it is open, false if it is closed.
+ */
+ boolean isOpen();
+
+ /**
+ * Checks whether this connection has gone down.
+ * Network connections may get closed during some time of inactivity
+ * for several reasons. The next time a read is attempted on such a
+ * connection it will throw an IOException.
+ * This method tries to alleviate this inconvenience by trying to
+ * find out if a connection is still usable. Implementations may do
+ * that by attempting a read with a very small timeout. Thus this
+ * method may block for a small amount of time before returning a result.
+ * It is therefore an <i>expensive</i> operation.
+ *
+ * @return <code>true</code> if attempts to use this connection are
+ * likely to succeed, or <code>false</code> if they are likely
+ * to fail and this connection should be closed
+ */
+ boolean isStale();
+
+ /**
+ * Sets the socket timeout value.
+ *
+ * @param timeout timeout value in milliseconds
+ */
+ void setSocketTimeout(int timeout);
+
+ /**
+ * Returns the socket timeout value.
+ *
+ * @return positive value in milliseconds if a timeout is set,
+ * <code>0</code> if timeout is disabled or <code>-1</code> if
+ * timeout is undefined.
+ */
+ int getSocketTimeout();
+
+ /**
+ * Force-closes this connection.
+ * This is the only method of a connection which may be called
+ * from a different thread to terminate the connection.
+ * This method will not attempt to flush the transmitter's
+ * internal buffer prior to closing the underlying socket.
+ */
+ void shutdown() throws IOException;
+
+ /**
+ * Returns a collection of connection metrics.
+ *
+ * @return HttpConnectionMetrics
+ */
+ HttpConnectionMetrics getMetrics();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpConnectionFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpConnectionFactory.java
new file mode 100644
index 0000000000..867a1e9e03
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpConnectionFactory.java
@@ -0,0 +1,42 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import java.io.IOException;
+import java.net.Socket;
+
+/**
+ * Factory for {@link HttpConnection} instances.
+ *
+ * @since 4.3
+ */
+public interface HttpConnectionFactory<T extends HttpConnection> {
+
+ T createConnection(Socket socket) throws IOException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpConnectionMetrics.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpConnectionMetrics.java
new file mode 100644
index 0000000000..4445eb55b5
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpConnectionMetrics.java
@@ -0,0 +1,77 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+/**
+ * The point of access to the statistics of an {@link HttpConnection}.
+ *
+ * @since 4.0
+ */
+public interface HttpConnectionMetrics {
+
+ /**
+ * Returns the number of requests transferred over the connection,
+ * 0 if not available.
+ */
+ long getRequestCount();
+
+ /**
+ * Returns the number of responses transferred over the connection,
+ * 0 if not available.
+ */
+ long getResponseCount();
+
+ /**
+ * Returns the number of bytes transferred over the connection,
+ * 0 if not available.
+ */
+ long getSentBytesCount();
+
+ /**
+ * Returns the number of bytes transferred over the connection,
+ * 0 if not available.
+ */
+ long getReceivedBytesCount();
+
+ /**
+ * Return the value for the specified metric.
+ *
+ *@param metricName the name of the metric to query.
+ *
+ *@return the object representing the metric requested,
+ * <code>null</code> if the metric cannot not found.
+ */
+ Object getMetric(String metricName);
+
+ /**
+ * Resets the counts
+ *
+ */
+ void reset();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpEntity.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpEntity.java
new file mode 100644
index 0000000000..5e223c1ec8
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpEntity.java
@@ -0,0 +1,199 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * An entity that can be sent or received with an HTTP message.
+ * Entities can be found in some
+ * {@link HttpEntityEnclosingRequest requests} and in
+ * {@link HttpResponse responses}, where they are optional.
+ * <p>
+ * There are three distinct types of entities in HttpCore,
+ * depending on where their {@link #getContent content} originates:
+ * <ul>
+ * <li><b>streamed</b>: The content is received from a stream, or
+ * generated on the fly. In particular, this category includes
+ * entities being received from a {@link HttpConnection connection}.
+ * {@link #isStreaming Streamed} entities are generally not
+ * {@link #isRepeatable repeatable}.
+ * </li>
+ * <li><b>self-contained</b>: The content is in memory or obtained by
+ * means that are independent from a connection or other entity.
+ * Self-contained entities are generally {@link #isRepeatable repeatable}.
+ * </li>
+ * <li><b>wrapping</b>: The content is obtained from another entity.
+ * </li>
+ * </ul>
+ * This distinction is important for connection management with incoming
+ * entities. For entities that are created by an application and only sent
+ * using the HTTP components framework, the difference between streamed
+ * and self-contained is of little importance. In that case, it is suggested
+ * to consider non-repeatable entities as streamed, and those that are
+ * repeatable (without a huge effort) as self-contained.
+ *
+ * @since 4.0
+ */
+public interface HttpEntity {
+
+ /**
+ * Tells if the entity is capable of producing its data more than once.
+ * A repeatable entity's getContent() and writeTo(OutputStream) methods
+ * can be called more than once whereas a non-repeatable entity's can not.
+ * @return true if the entity is repeatable, false otherwise.
+ */
+ boolean isRepeatable();
+
+ /**
+ * Tells about chunked encoding for this entity.
+ * The primary purpose of this method is to indicate whether
+ * chunked encoding should be used when the entity is sent.
+ * For entities that are received, it can also indicate whether
+ * the entity was received with chunked encoding.
+ * <br/>
+ * The behavior of wrapping entities is implementation dependent,
+ * but should respect the primary purpose.
+ *
+ * @return <code>true</code> if chunked encoding is preferred for this
+ * entity, or <code>false</code> if it is not
+ */
+ boolean isChunked();
+
+ /**
+ * Tells the length of the content, if known.
+ *
+ * @return the number of bytes of the content, or
+ * a negative number if unknown. If the content length is known
+ * but exceeds {@link java.lang.Long#MAX_VALUE Long.MAX_VALUE},
+ * a negative number is returned.
+ */
+ long getContentLength();
+
+ /**
+ * Obtains the Content-Type header, if known.
+ * This is the header that should be used when sending the entity,
+ * or the one that was received with the entity. It can include a
+ * charset attribute.
+ *
+ * @return the Content-Type header for this entity, or
+ * <code>null</code> if the content type is unknown
+ */
+ Header getContentType();
+
+ /**
+ * Obtains the Content-Encoding header, if known.
+ * This is the header that should be used when sending the entity,
+ * or the one that was received with the entity.
+ * Wrapping entities that modify the content encoding should
+ * adjust this header accordingly.
+ *
+ * @return the Content-Encoding header for this entity, or
+ * <code>null</code> if the content encoding is unknown
+ */
+ Header getContentEncoding();
+
+ /**
+ * Returns a content stream of the entity.
+ * {@link #isRepeatable Repeatable} entities are expected
+ * to create a new instance of {@link InputStream} for each invocation
+ * of this method and therefore can be consumed multiple times.
+ * Entities that are not {@link #isRepeatable repeatable} are expected
+ * to return the same {@link InputStream} instance and therefore
+ * may not be consumed more than once.
+ * <p>
+ * IMPORTANT: Please note all entity implementations must ensure that
+ * all allocated resources are properly deallocated after
+ * the {@link InputStream#close()} method is invoked.
+ *
+ * @return content stream of the entity.
+ *
+ * @throws IOException if the stream could not be created
+ * @throws IllegalStateException
+ * if content stream cannot be created.
+ *
+ * @see #isRepeatable()
+ */
+ InputStream getContent() throws IOException, IllegalStateException;
+
+ /**
+ * Writes the entity content out to the output stream.
+ * <p>
+ * <p>
+ * IMPORTANT: Please note all entity implementations must ensure that
+ * all allocated resources are properly deallocated when this method
+ * returns.
+ *
+ * @param outstream the output stream to write entity content to
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ void writeTo(OutputStream outstream) throws IOException;
+
+ /**
+ * Tells whether this entity depends on an underlying stream.
+ * Streamed entities that read data directly from the socket should
+ * return <code>true</code>. Self-contained entities should return
+ * <code>false</code>. Wrapping entities should delegate this call
+ * to the wrapped entity.
+ *
+ * @return <code>true</code> if the entity content is streamed,
+ * <code>false</code> otherwise
+ */
+ boolean isStreaming(); // don't expect an exception here
+
+ /**
+ * This method is deprecated since version 4.1. Please use standard
+ * java convention to ensure resource deallocation by calling
+ * {@link InputStream#close()} on the input stream returned by
+ * {@link #getContent()}
+ * <p>
+ * This method is called to indicate that the content of this entity
+ * is no longer required. All entity implementations are expected to
+ * release all allocated resources as a result of this method
+ * invocation. Content streaming entities are also expected to
+ * dispose of the remaining content, if any. Wrapping entities should
+ * delegate this call to the wrapped entity.
+ * <p>
+ * This method is of particular importance for entities being
+ * received from a {@link HttpConnection connection}. The entity
+ * needs to be consumed completely in order to re-use the connection
+ * with keep-alive.
+ *
+ * @throws IOException if an I/O error occurs.
+ *
+ * @deprecated (4.1) Use {@link ch.boye.httpclientandroidlib.util.EntityUtils#consume(HttpEntity)}
+ *
+ * @see #getContent() and #writeTo(OutputStream)
+ */
+ @Deprecated
+ void consumeContent() throws IOException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpEntityEnclosingRequest.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpEntityEnclosingRequest.java
new file mode 100644
index 0000000000..f2cce41978
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpEntityEnclosingRequest.java
@@ -0,0 +1,60 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib;
+
+/**
+ * A request with an entity.
+ *
+ * @since 4.0
+ */
+public interface HttpEntityEnclosingRequest extends HttpRequest {
+
+ /**
+ * Tells if this request should use the expect-continue handshake.
+ * The expect continue handshake gives the server a chance to decide
+ * whether to accept the entity enclosing request before the possibly
+ * lengthy entity is sent across the wire.
+ * @return true if the expect continue handshake should be used, false if
+ * not.
+ */
+ boolean expectContinue();
+
+ /**
+ * Associates the entity with this request.
+ *
+ * @param entity the entity to send.
+ */
+ void setEntity(HttpEntity entity);
+
+ /**
+ * Returns the entity associated with this request.
+ *
+ * @return entity
+ */
+ HttpEntity getEntity();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpException.java
new file mode 100644
index 0000000000..05f6381d1e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpException.java
@@ -0,0 +1,67 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+/**
+ * Signals that an HTTP exception has occurred.
+ *
+ * @since 4.0
+ */
+public class HttpException extends Exception {
+
+ private static final long serialVersionUID = -5437299376222011036L;
+
+ /**
+ * Creates a new HttpException with a <tt>null</tt> detail message.
+ */
+ public HttpException() {
+ super();
+ }
+
+ /**
+ * Creates a new HttpException with the specified detail message.
+ *
+ * @param message the exception detail message
+ */
+ public HttpException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new HttpException with the specified detail message and cause.
+ *
+ * @param message the exception detail message
+ * @param cause the <tt>Throwable</tt> that caused this exception, or <tt>null</tt>
+ * if the cause is unavailable, unknown, or not a <tt>Throwable</tt>
+ */
+ public HttpException(final String message, final Throwable cause) {
+ super(message);
+ initCause(cause);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpHeaders.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpHeaders.java
new file mode 100644
index 0000000000..cadbced78f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpHeaders.java
@@ -0,0 +1,206 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+/**
+ * Constants enumerating the HTTP headers. All headers defined in RFC1945 (HTTP/1.0), RFC2616 (HTTP/1.1), and RFC2518
+ * (WebDAV) are listed.
+ *
+ * @since 4.1
+ */
+public final class HttpHeaders {
+
+ private HttpHeaders() {
+ }
+
+ /** RFC 2616 (HTTP/1.1) Section 14.1 */
+ public static final String ACCEPT = "Accept";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.2 */
+ public static final String ACCEPT_CHARSET = "Accept-Charset";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.3 */
+ public static final String ACCEPT_ENCODING = "Accept-Encoding";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.4 */
+ public static final String ACCEPT_LANGUAGE = "Accept-Language";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.5 */
+ public static final String ACCEPT_RANGES = "Accept-Ranges";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.6 */
+ public static final String AGE = "Age";
+
+ /** RFC 1945 (HTTP/1.0) Section 10.1, RFC 2616 (HTTP/1.1) Section 14.7 */
+ public static final String ALLOW = "Allow";
+
+ /** RFC 1945 (HTTP/1.0) Section 10.2, RFC 2616 (HTTP/1.1) Section 14.8 */
+ public static final String AUTHORIZATION = "Authorization";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.9 */
+ public static final String CACHE_CONTROL = "Cache-Control";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.10 */
+ public static final String CONNECTION = "Connection";
+
+ /** RFC 1945 (HTTP/1.0) Section 10.3, RFC 2616 (HTTP/1.1) Section 14.11 */
+ public static final String CONTENT_ENCODING = "Content-Encoding";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.12 */
+ public static final String CONTENT_LANGUAGE = "Content-Language";
+
+ /** RFC 1945 (HTTP/1.0) Section 10.4, RFC 2616 (HTTP/1.1) Section 14.13 */
+ public static final String CONTENT_LENGTH = "Content-Length";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.14 */
+ public static final String CONTENT_LOCATION = "Content-Location";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.15 */
+ public static final String CONTENT_MD5 = "Content-MD5";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.16 */
+ public static final String CONTENT_RANGE = "Content-Range";
+
+ /** RFC 1945 (HTTP/1.0) Section 10.5, RFC 2616 (HTTP/1.1) Section 14.17 */
+ public static final String CONTENT_TYPE = "Content-Type";
+
+ /** RFC 1945 (HTTP/1.0) Section 10.6, RFC 2616 (HTTP/1.1) Section 14.18 */
+ public static final String DATE = "Date";
+
+ /** RFC 2518 (WevDAV) Section 9.1 */
+ public static final String DAV = "Dav";
+
+ /** RFC 2518 (WevDAV) Section 9.2 */
+ public static final String DEPTH = "Depth";
+
+ /** RFC 2518 (WevDAV) Section 9.3 */
+ public static final String DESTINATION = "Destination";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.19 */
+ public static final String ETAG = "ETag";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.20 */
+ public static final String EXPECT = "Expect";
+
+ /** RFC 1945 (HTTP/1.0) Section 10.7, RFC 2616 (HTTP/1.1) Section 14.21 */
+ public static final String EXPIRES = "Expires";
+
+ /** RFC 1945 (HTTP/1.0) Section 10.8, RFC 2616 (HTTP/1.1) Section 14.22 */
+ public static final String FROM = "From";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.23 */
+ public static final String HOST = "Host";
+
+ /** RFC 2518 (WevDAV) Section 9.4 */
+ public static final String IF = "If";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.24 */
+ public static final String IF_MATCH = "If-Match";
+
+ /** RFC 1945 (HTTP/1.0) Section 10.9, RFC 2616 (HTTP/1.1) Section 14.25 */
+ public static final String IF_MODIFIED_SINCE = "If-Modified-Since";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.26 */
+ public static final String IF_NONE_MATCH = "If-None-Match";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.27 */
+ public static final String IF_RANGE = "If-Range";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.28 */
+ public static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
+
+ /** RFC 1945 (HTTP/1.0) Section 10.10, RFC 2616 (HTTP/1.1) Section 14.29 */
+ public static final String LAST_MODIFIED = "Last-Modified";
+
+ /** RFC 1945 (HTTP/1.0) Section 10.11, RFC 2616 (HTTP/1.1) Section 14.30 */
+ public static final String LOCATION = "Location";
+
+ /** RFC 2518 (WevDAV) Section 9.5 */
+ public static final String LOCK_TOKEN = "Lock-Token";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.31 */
+ public static final String MAX_FORWARDS = "Max-Forwards";
+
+ /** RFC 2518 (WevDAV) Section 9.6 */
+ public static final String OVERWRITE = "Overwrite";
+
+ /** RFC 1945 (HTTP/1.0) Section 10.12, RFC 2616 (HTTP/1.1) Section 14.32 */
+ public static final String PRAGMA = "Pragma";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.33 */
+ public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.34 */
+ public static final String PROXY_AUTHORIZATION = "Proxy-Authorization";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.35 */
+ public static final String RANGE = "Range";
+
+ /** RFC 1945 (HTTP/1.0) Section 10.13, RFC 2616 (HTTP/1.1) Section 14.36 */
+ public static final String REFERER = "Referer";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.37 */
+ public static final String RETRY_AFTER = "Retry-After";
+
+ /** RFC 1945 (HTTP/1.0) Section 10.14, RFC 2616 (HTTP/1.1) Section 14.38 */
+ public static final String SERVER = "Server";
+
+ /** RFC 2518 (WevDAV) Section 9.7 */
+ public static final String STATUS_URI = "Status-URI";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.39 */
+ public static final String TE = "TE";
+
+ /** RFC 2518 (WevDAV) Section 9.8 */
+ public static final String TIMEOUT = "Timeout";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.40 */
+ public static final String TRAILER = "Trailer";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.41 */
+ public static final String TRANSFER_ENCODING = "Transfer-Encoding";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.42 */
+ public static final String UPGRADE = "Upgrade";
+
+ /** RFC 1945 (HTTP/1.0) Section 10.15, RFC 2616 (HTTP/1.1) Section 14.43 */
+ public static final String USER_AGENT = "User-Agent";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.44 */
+ public static final String VARY = "Vary";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.45 */
+ public static final String VIA = "Via";
+
+ /** RFC 2616 (HTTP/1.1) Section 14.46 */
+ public static final String WARNING = "Warning";
+
+ /** RFC 1945 (HTTP/1.0) Section 10.16, RFC 2616 (HTTP/1.1) Section 14.47 */
+ public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpHost.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpHost.java
new file mode 100644
index 0000000000..d25bb13cf7
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpHost.java
@@ -0,0 +1,290 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import java.io.Serializable;
+import java.net.InetAddress;
+import java.util.Locale;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.LangUtils;
+
+/**
+ * Holds all of the variables needed to describe an HTTP connection to a host.
+ * This includes remote host name, port and scheme.
+ *
+ *
+ * @since 4.0
+ */
+@Immutable
+public final class HttpHost implements Cloneable, Serializable {
+
+ private static final long serialVersionUID = -7529410654042457626L;
+
+ /** The default scheme is "http". */
+ public static final String DEFAULT_SCHEME_NAME = "http";
+
+ /** The host to use. */
+ protected final String hostname;
+
+ /** The lowercase host, for {@link #equals} and {@link #hashCode}. */
+ protected final String lcHostname;
+
+
+ /** The port to use, defaults to -1 if not set. */
+ protected final int port;
+
+ /** The scheme (lowercased) */
+ protected final String schemeName;
+
+ protected final InetAddress address;
+
+ /**
+ * Creates a new {@link HttpHost HttpHost}, specifying all values.
+ * Constructor for HttpHost.
+ *
+ * @param hostname the hostname (IP or DNS name)
+ * @param port the port number.
+ * <code>-1</code> indicates the scheme default port.
+ * @param scheme the name of the scheme.
+ * <code>null</code> indicates the
+ * {@link #DEFAULT_SCHEME_NAME default scheme}
+ */
+ public HttpHost(final String hostname, final int port, final String scheme) {
+ super();
+ this.hostname = Args.notBlank(hostname, "Host name");
+ this.lcHostname = hostname.toLowerCase(Locale.ENGLISH);
+ if (scheme != null) {
+ this.schemeName = scheme.toLowerCase(Locale.ENGLISH);
+ } else {
+ this.schemeName = DEFAULT_SCHEME_NAME;
+ }
+ this.port = port;
+ this.address = null;
+ }
+
+ /**
+ * Creates a new {@link HttpHost HttpHost}, with default scheme.
+ *
+ * @param hostname the hostname (IP or DNS name)
+ * @param port the port number.
+ * <code>-1</code> indicates the scheme default port.
+ */
+ public HttpHost(final String hostname, final int port) {
+ this(hostname, port, null);
+ }
+
+ /**
+ * Creates a new {@link HttpHost HttpHost}, with default scheme and port.
+ *
+ * @param hostname the hostname (IP or DNS name)
+ */
+ public HttpHost(final String hostname) {
+ this(hostname, -1, null);
+ }
+
+ /**
+ * Creates a new {@link HttpHost HttpHost}, specifying all values.
+ * Constructor for HttpHost.
+ *
+ * @param address the inet address.
+ * @param port the port number.
+ * <code>-1</code> indicates the scheme default port.
+ * @param scheme the name of the scheme.
+ * <code>null</code> indicates the
+ * {@link #DEFAULT_SCHEME_NAME default scheme}
+ *
+ * @since 4.3
+ */
+ public HttpHost(final InetAddress address, final int port, final String scheme) {
+ super();
+ this.address = Args.notNull(address, "Inet address");
+ this.hostname = address.getHostAddress();
+ this.lcHostname = this.hostname.toLowerCase(Locale.ENGLISH);
+ if (scheme != null) {
+ this.schemeName = scheme.toLowerCase(Locale.ENGLISH);
+ } else {
+ this.schemeName = DEFAULT_SCHEME_NAME;
+ }
+ this.port = port;
+ }
+
+ /**
+ * Creates a new {@link HttpHost HttpHost}, with default scheme.
+ *
+ * @param address the inet address.
+ * @param port the port number.
+ * <code>-1</code> indicates the scheme default port.
+ *
+ * @since 4.3
+ */
+ public HttpHost(final InetAddress address, final int port) {
+ this(address, port, null);
+ }
+
+ /**
+ * Creates a new {@link HttpHost HttpHost}, with default scheme and port.
+ *
+ * @param address the inet address.
+ *
+ * @since 4.3
+ */
+ public HttpHost(final InetAddress address) {
+ this(address, -1, null);
+ }
+
+ /**
+ * Copy constructor for {@link HttpHost HttpHost}.
+ *
+ * @param httphost the HTTP host to copy details from
+ */
+ public HttpHost (final HttpHost httphost) {
+ super();
+ Args.notNull(httphost, "HTTP host");
+ this.hostname = httphost.hostname;
+ this.lcHostname = httphost.lcHostname;
+ this.schemeName = httphost.schemeName;
+ this.port = httphost.port;
+ this.address = httphost.address;
+ }
+
+ /**
+ * Returns the host name.
+ *
+ * @return the host name (IP or DNS name)
+ */
+ public String getHostName() {
+ return this.hostname;
+ }
+
+ /**
+ * Returns the port.
+ *
+ * @return the host port, or <code>-1</code> if not set
+ */
+ public int getPort() {
+ return this.port;
+ }
+
+ /**
+ * Returns the scheme name.
+ *
+ * @return the scheme name
+ */
+ public String getSchemeName() {
+ return this.schemeName;
+ }
+
+ /**
+ * Returns the inet address if explicitly set by a constructor,
+ * <code>null</code> otherwise.
+ * @return the inet address
+ *
+ * @since 4.3
+ */
+ public InetAddress getAddress() {
+ return this.address;
+ }
+
+ /**
+ * Return the host URI, as a string.
+ *
+ * @return the host URI
+ */
+ public String toURI() {
+ final StringBuilder buffer = new StringBuilder();
+ buffer.append(this.schemeName);
+ buffer.append("://");
+ buffer.append(this.hostname);
+ if (this.port != -1) {
+ buffer.append(':');
+ buffer.append(Integer.toString(this.port));
+ }
+ return buffer.toString();
+ }
+
+
+ /**
+ * Obtains the host string, without scheme prefix.
+ *
+ * @return the host string, for example <code>localhost:8080</code>
+ */
+ public String toHostString() {
+ if (this.port != -1) {
+ //the highest port number is 65535, which is length 6 with the addition of the colon
+ final StringBuilder buffer = new StringBuilder(this.hostname.length() + 6);
+ buffer.append(this.hostname);
+ buffer.append(":");
+ buffer.append(Integer.toString(this.port));
+ return buffer.toString();
+ } else {
+ return this.hostname;
+ }
+ }
+
+
+ @Override
+ public String toString() {
+ return toURI();
+ }
+
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof HttpHost) {
+ final HttpHost that = (HttpHost) obj;
+ return this.lcHostname.equals(that.lcHostname)
+ && this.port == that.port
+ && this.schemeName.equals(that.schemeName);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @see java.lang.Object#hashCode()
+ */
+ @Override
+ public int hashCode() {
+ int hash = LangUtils.HASH_SEED;
+ hash = LangUtils.hashCode(hash, this.lcHostname);
+ hash = LangUtils.hashCode(hash, this.port);
+ hash = LangUtils.hashCode(hash, this.schemeName);
+ return hash;
+ }
+
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpInetConnection.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpInetConnection.java
new file mode 100644
index 0000000000..9a22d3de87
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpInetConnection.java
@@ -0,0 +1,47 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import java.net.InetAddress;
+
+/**
+ * An HTTP connection over the Internet Protocol (IP).
+ *
+ * @since 4.0
+ */
+public interface HttpInetConnection extends HttpConnection {
+
+ InetAddress getLocalAddress();
+
+ int getLocalPort();
+
+ InetAddress getRemoteAddress();
+
+ int getRemotePort();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpMessage.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpMessage.java
new file mode 100644
index 0000000000..9ea8a3e56e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpMessage.java
@@ -0,0 +1,210 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+/**
+ * HTTP messages consist of requests from client to server and responses
+ * from server to client.
+ * <pre>
+ * HTTP-message = Request | Response ; HTTP/1.1 messages
+ * </pre>
+ * <p>
+ * HTTP messages use the generic message format of RFC 822 for
+ * transferring entities (the payload of the message). Both types
+ * of message consist of a start-line, zero or more header fields
+ * (also known as "headers"), an empty line (i.e., a line with nothing
+ * preceding the CRLF) indicating the end of the header fields,
+ * and possibly a message-body.
+ * </p>
+ * <pre>
+ * generic-message = start-line
+ * *(message-header CRLF)
+ * CRLF
+ * [ message-body ]
+ * start-line = Request-Line | Status-Line
+ * </pre>
+ *
+ * @since 4.0
+ */
+@SuppressWarnings("deprecation")
+public interface HttpMessage {
+
+ /**
+ * Returns the protocol version this message is compatible with.
+ */
+ ProtocolVersion getProtocolVersion();
+
+ /**
+ * Checks if a certain header is present in this message. Header values are
+ * ignored.
+ *
+ * @param name the header name to check for.
+ * @return true if at least one header with this name is present.
+ */
+ boolean containsHeader(String name);
+
+ /**
+ * Returns all the headers with a specified name of this message. Header values
+ * are ignored. Headers are orderd in the sequence they will be sent over a
+ * connection.
+ *
+ * @param name the name of the headers to return.
+ * @return the headers whose name property equals <code>name</code>.
+ */
+ Header[] getHeaders(String name);
+
+ /**
+ * Returns the first header with a specified name of this message. Header
+ * values are ignored. If there is more than one matching header in the
+ * message the first element of {@link #getHeaders(String)} is returned.
+ * If there is no matching header in the message <code>null</code> is
+ * returned.
+ *
+ * @param name the name of the header to return.
+ * @return the first header whose name property equals <code>name</code>
+ * or <code>null</code> if no such header could be found.
+ */
+ Header getFirstHeader(String name);
+
+ /**
+ * Returns the last header with a specified name of this message. Header values
+ * are ignored. If there is more than one matching header in the message the
+ * last element of {@link #getHeaders(String)} is returned. If there is no
+ * matching header in the message <code>null</code> is returned.
+ *
+ * @param name the name of the header to return.
+ * @return the last header whose name property equals <code>name</code>.
+ * or <code>null</code> if no such header could be found.
+ */
+ Header getLastHeader(String name);
+
+ /**
+ * Returns all the headers of this message. Headers are orderd in the sequence
+ * they will be sent over a connection.
+ *
+ * @return all the headers of this message
+ */
+ Header[] getAllHeaders();
+
+ /**
+ * Adds a header to this message. The header will be appended to the end of
+ * the list.
+ *
+ * @param header the header to append.
+ */
+ void addHeader(Header header);
+
+ /**
+ * Adds a header to this message. The header will be appended to the end of
+ * the list.
+ *
+ * @param name the name of the header.
+ * @param value the value of the header.
+ */
+ void addHeader(String name, String value);
+
+ /**
+ * Overwrites the first header with the same name. The new header will be appended to
+ * the end of the list, if no header with the given name can be found.
+ *
+ * @param header the header to set.
+ */
+ void setHeader(Header header);
+
+ /**
+ * Overwrites the first header with the same name. The new header will be appended to
+ * the end of the list, if no header with the given name can be found.
+ *
+ * @param name the name of the header.
+ * @param value the value of the header.
+ */
+ void setHeader(String name, String value);
+
+ /**
+ * Overwrites all the headers in the message.
+ *
+ * @param headers the array of headers to set.
+ */
+ void setHeaders(Header[] headers);
+
+ /**
+ * Removes a header from this message.
+ *
+ * @param header the header to remove.
+ */
+ void removeHeader(Header header);
+
+ /**
+ * Removes all headers with a certain name from this message.
+ *
+ * @param name The name of the headers to remove.
+ */
+ void removeHeaders(String name);
+
+ /**
+ * Returns an iterator of all the headers.
+ *
+ * @return Iterator that returns Header objects in the sequence they are
+ * sent over a connection.
+ */
+ HeaderIterator headerIterator();
+
+ /**
+ * Returns an iterator of the headers with a given name.
+ *
+ * @param name the name of the headers over which to iterate, or
+ * <code>null</code> for all headers
+ *
+ * @return Iterator that returns Header objects with the argument name
+ * in the sequence they are sent over a connection.
+ */
+ HeaderIterator headerIterator(String name);
+
+ /**
+ * Returns the parameters effective for this message as set by
+ * {@link #setParams(HttpParams)}.
+ *
+ * @deprecated (4.3) use configuration classes provided 'ch.boye.httpclientandroidlib.config'
+ * and 'ch.boye.httpclientandroidlib.client.config'
+ */
+ @Deprecated
+ HttpParams getParams();
+
+ /**
+ * Provides parameters to be used for the processing of this message.
+ * @param params the parameters
+ *
+ * @deprecated (4.3) use configuration classes provided 'ch.boye.httpclientandroidlib.config'
+ * and 'ch.boye.httpclientandroidlib.client.config'
+ */
+ @Deprecated
+ void setParams(HttpParams params);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpRequest.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpRequest.java
new file mode 100644
index 0000000000..20eff5f517
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpRequest.java
@@ -0,0 +1,53 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+/**
+ * A request message from a client to a server includes, within the
+ * first line of that message, the method to be applied to the resource,
+ * the identifier of the resource, and the protocol version in use.
+ * <pre>
+ * Request = Request-Line
+ * *(( general-header
+ * | request-header
+ * | entity-header ) CRLF)
+ * CRLF
+ * [ message-body ]
+ * </pre>
+ *
+ * @since 4.0
+ */
+public interface HttpRequest extends HttpMessage {
+
+ /**
+ * Returns the request line of this request.
+ * @return the request line.
+ */
+ RequestLine getRequestLine();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpRequestFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpRequestFactory.java
new file mode 100644
index 0000000000..ffc5dffda4
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpRequestFactory.java
@@ -0,0 +1,43 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+/**
+ * A factory for {@link HttpRequest HttpRequest} objects.
+ *
+ * @since 4.0
+ */
+public interface HttpRequestFactory {
+
+ HttpRequest newHttpRequest(RequestLine requestline)
+ throws MethodNotSupportedException;
+
+ HttpRequest newHttpRequest(String method, String uri)
+ throws MethodNotSupportedException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpRequestInterceptor.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpRequestInterceptor.java
new file mode 100644
index 0000000000..b8efdaea06
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpRequestInterceptor.java
@@ -0,0 +1,68 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * HTTP protocol interceptor is a routine that implements a specific aspect of
+ * the HTTP protocol. Usually protocol interceptors are expected to act upon
+ * one specific header or a group of related headers of the incoming message
+ * or populate the outgoing message with one specific header or a group of
+ * related headers.
+ * <p>
+ * Protocol Interceptors can also manipulate content entities enclosed with messages.
+ * Usually this is accomplished by using the 'Decorator' pattern where a wrapper
+ * entity class is used to decorate the original entity.
+ * <p>
+ * Protocol interceptors must be implemented as thread-safe. Similarly to
+ * servlets, protocol interceptors should not use instance variables unless
+ * access to those variables is synchronized.
+ *
+ * @since 4.0
+ */
+public interface HttpRequestInterceptor {
+
+ /**
+ * Processes a request.
+ * On the client side, this step is performed before the request is
+ * sent to the server. On the server side, this step is performed
+ * on incoming messages before the message body is evaluated.
+ *
+ * @param request the request to preprocess
+ * @param context the context for the request
+ *
+ * @throws HttpException in case of an HTTP protocol violation
+ * @throws IOException in case of an I/O error
+ */
+ void process(HttpRequest request, HttpContext context)
+ throws HttpException, IOException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpResponse.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpResponse.java
new file mode 100644
index 0000000000..7d19952d5c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpResponse.java
@@ -0,0 +1,155 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import java.util.Locale;
+
+/**
+ * After receiving and interpreting a request message, a server responds
+ * with an HTTP response message.
+ * <pre>
+ * Response = Status-Line
+ * *(( general-header
+ * | response-header
+ * | entity-header ) CRLF)
+ * CRLF
+ * [ message-body ]
+ * </pre>
+ *
+ * @since 4.0
+ */
+public interface HttpResponse extends HttpMessage {
+
+ /**
+ * Obtains the status line of this response.
+ * The status line can be set using one of the
+ * {@link #setStatusLine setStatusLine} methods,
+ * or it can be initialized in a constructor.
+ *
+ * @return the status line, or <code>null</code> if not yet set
+ */
+ StatusLine getStatusLine();
+
+ /**
+ * Sets the status line of this response.
+ *
+ * @param statusline the status line of this response
+ */
+ void setStatusLine(StatusLine statusline);
+
+ /**
+ * Sets the status line of this response.
+ * The reason phrase will be determined based on the current
+ * {@link #getLocale locale}.
+ *
+ * @param ver the HTTP version
+ * @param code the status code
+ */
+ void setStatusLine(ProtocolVersion ver, int code);
+
+ /**
+ * Sets the status line of this response with a reason phrase.
+ *
+ * @param ver the HTTP version
+ * @param code the status code
+ * @param reason the reason phrase, or <code>null</code> to omit
+ */
+ void setStatusLine(ProtocolVersion ver, int code, String reason);
+
+ /**
+ * Updates the status line of this response with a new status code.
+ *
+ * @param code the HTTP status code.
+ *
+ * @throws IllegalStateException
+ * if the status line has not be set
+ *
+ * @see HttpStatus
+ * @see #setStatusLine(StatusLine)
+ * @see #setStatusLine(ProtocolVersion,int)
+ */
+ void setStatusCode(int code)
+ throws IllegalStateException;
+
+ /**
+ * Updates the status line of this response with a new reason phrase.
+ *
+ * @param reason the new reason phrase as a single-line string, or
+ * <code>null</code> to unset the reason phrase
+ *
+ * @throws IllegalStateException
+ * if the status line has not be set
+ *
+ * @see #setStatusLine(StatusLine)
+ * @see #setStatusLine(ProtocolVersion,int)
+ */
+ void setReasonPhrase(String reason)
+ throws IllegalStateException;
+
+ /**
+ * Obtains the message entity of this response, if any.
+ * The entity is provided by calling {@link #setEntity setEntity}.
+ *
+ * @return the response entity, or
+ * <code>null</code> if there is none
+ */
+ HttpEntity getEntity();
+
+ /**
+ * Associates a response entity with this response.
+ * <p/>
+ * Please note that if an entity has already been set for this response and it depends on
+ * an input stream ({@link HttpEntity#isStreaming()} returns <code>true</code>),
+ * it must be fully consumed in order to ensure release of resources.
+ *
+ * @param entity the entity to associate with this response, or
+ * <code>null</code> to unset
+ *
+ * @see HttpEntity#isStreaming()
+ * @see ch.boye.httpclientandroidlib.util.EntityUtils#updateEntity(HttpResponse, HttpEntity)
+ */
+ void setEntity(HttpEntity entity);
+
+ /**
+ * Obtains the locale of this response.
+ * The locale is used to determine the reason phrase
+ * for the {@link #setStatusCode status code}.
+ * It can be changed using {@link #setLocale setLocale}.
+ *
+ * @return the locale of this response, never <code>null</code>
+ */
+ Locale getLocale();
+
+ /**
+ * Changes the locale of this response.
+ *
+ * @param loc the new locale
+ */
+ void setLocale(Locale loc);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpResponseFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpResponseFactory.java
new file mode 100644
index 0000000000..3897339c98
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpResponseFactory.java
@@ -0,0 +1,67 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * A factory for {@link HttpResponse HttpResponse} objects.
+ *
+ * @since 4.0
+ */
+public interface HttpResponseFactory {
+
+ /**
+ * Creates a new response from status line elements.
+ *
+ * @param ver the protocol version
+ * @param status the status code
+ * @param context the context from which to determine the locale
+ * for looking up a reason phrase to the status code, or
+ * <code>null</code> to use the default locale
+ *
+ * @return the new response with an initialized status line
+ */
+ HttpResponse newHttpResponse(ProtocolVersion ver, int status,
+ HttpContext context);
+
+ /**
+ * Creates a new response from a status line.
+ *
+ * @param statusline the status line
+ * @param context the context from which to determine the locale
+ * for looking up a reason phrase if the status code
+ * is updated, or
+ * <code>null</code> to use the default locale
+ *
+ * @return the new response with the argument status line
+ */
+ HttpResponse newHttpResponse(StatusLine statusline,
+ HttpContext context);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpResponseInterceptor.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpResponseInterceptor.java
new file mode 100644
index 0000000000..6d4973c341
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpResponseInterceptor.java
@@ -0,0 +1,68 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * HTTP protocol interceptor is a routine that implements a specific aspect of
+ * the HTTP protocol. Usually protocol interceptors are expected to act upon
+ * one specific header or a group of related headers of the incoming message
+ * or populate the outgoing message with one specific header or a group of
+ * related headers. Protocol
+ * <p>
+ * Interceptors can also manipulate content entities enclosed with messages.
+ * Usually this is accomplished by using the 'Decorator' pattern where a wrapper
+ * entity class is used to decorate the original entity.
+ * <p>
+ * Protocol interceptors must be implemented as thread-safe. Similarly to
+ * servlets, protocol interceptors should not use instance variables unless
+ * access to those variables is synchronized.
+ *
+ * @since 4.0
+ */
+public interface HttpResponseInterceptor {
+
+ /**
+ * Processes a response.
+ * On the server side, this step is performed before the response is
+ * sent to the client. On the client side, this step is performed
+ * on incoming messages before the message body is evaluated.
+ *
+ * @param response the response to postprocess
+ * @param context the context for the request
+ *
+ * @throws HttpException in case of an HTTP protocol violation
+ * @throws IOException in case of an I/O error
+ */
+ void process(HttpResponse response, HttpContext context)
+ throws HttpException, IOException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpServerConnection.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpServerConnection.java
new file mode 100644
index 0000000000..9b0987ddee
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpServerConnection.java
@@ -0,0 +1,88 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import java.io.IOException;
+
+/**
+ * A server-side HTTP connection, which can be used for receiving
+ * requests and sending responses.
+ *
+ * @since 4.0
+ */
+public interface HttpServerConnection extends HttpConnection {
+
+ /**
+ * Receives the request line and all headers available from this connection.
+ * The caller should examine the returned request and decide if to receive a
+ * request entity as well.
+ *
+ * @return a new HttpRequest object whose request line and headers are
+ * initialized.
+ * @throws HttpException in case of HTTP protocol violation
+ * @throws IOException in case of an I/O error
+ */
+ HttpRequest receiveRequestHeader()
+ throws HttpException, IOException;
+
+ /**
+ * Receives the next request entity available from this connection and attaches it to
+ * an existing request.
+ * @param request the request to attach the entity to.
+ * @throws HttpException in case of HTTP protocol violation
+ * @throws IOException in case of an I/O error
+ */
+ void receiveRequestEntity(HttpEntityEnclosingRequest request)
+ throws HttpException, IOException;
+
+ /**
+ * Sends the response line and headers of a response over this connection.
+ * @param response the response whose headers to send.
+ * @throws HttpException in case of HTTP protocol violation
+ * @throws IOException in case of an I/O error
+ */
+ void sendResponseHeader(HttpResponse response)
+ throws HttpException, IOException;
+
+ /**
+ * Sends the response entity of a response over this connection.
+ * @param response the response whose entity to send.
+ * @throws HttpException in case of HTTP protocol violation
+ * @throws IOException in case of an I/O error
+ */
+ void sendResponseEntity(HttpResponse response)
+ throws HttpException, IOException;
+
+ /**
+ * Sends all pending buffered data over this connection.
+ * @throws IOException in case of an I/O error
+ */
+ void flush()
+ throws IOException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpStatus.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpStatus.java
new file mode 100644
index 0000000000..1b61262a2e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpStatus.java
@@ -0,0 +1,175 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+/**
+ * Constants enumerating the HTTP status codes.
+ * All status codes defined in RFC1945 (HTTP/1.0), RFC2616 (HTTP/1.1), and
+ * RFC2518 (WebDAV) are listed.
+ *
+ * @see StatusLine
+ *
+ * @since 4.0
+ */
+public interface HttpStatus {
+
+ // --- 1xx Informational ---
+
+ /** <tt>100 Continue</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_CONTINUE = 100;
+ /** <tt>101 Switching Protocols</tt> (HTTP/1.1 - RFC 2616)*/
+ public static final int SC_SWITCHING_PROTOCOLS = 101;
+ /** <tt>102 Processing</tt> (WebDAV - RFC 2518) */
+ public static final int SC_PROCESSING = 102;
+
+ // --- 2xx Success ---
+
+ /** <tt>200 OK</tt> (HTTP/1.0 - RFC 1945) */
+ public static final int SC_OK = 200;
+ /** <tt>201 Created</tt> (HTTP/1.0 - RFC 1945) */
+ public static final int SC_CREATED = 201;
+ /** <tt>202 Accepted</tt> (HTTP/1.0 - RFC 1945) */
+ public static final int SC_ACCEPTED = 202;
+ /** <tt>203 Non Authoritative Information</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_NON_AUTHORITATIVE_INFORMATION = 203;
+ /** <tt>204 No Content</tt> (HTTP/1.0 - RFC 1945) */
+ public static final int SC_NO_CONTENT = 204;
+ /** <tt>205 Reset Content</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_RESET_CONTENT = 205;
+ /** <tt>206 Partial Content</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_PARTIAL_CONTENT = 206;
+ /**
+ * <tt>207 Multi-Status</tt> (WebDAV - RFC 2518) or <tt>207 Partial Update
+ * OK</tt> (HTTP/1.1 - draft-ietf-http-v11-spec-rev-01?)
+ */
+ public static final int SC_MULTI_STATUS = 207;
+
+ // --- 3xx Redirection ---
+
+ /** <tt>300 Mutliple Choices</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_MULTIPLE_CHOICES = 300;
+ /** <tt>301 Moved Permanently</tt> (HTTP/1.0 - RFC 1945) */
+ public static final int SC_MOVED_PERMANENTLY = 301;
+ /** <tt>302 Moved Temporarily</tt> (Sometimes <tt>Found</tt>) (HTTP/1.0 - RFC 1945) */
+ public static final int SC_MOVED_TEMPORARILY = 302;
+ /** <tt>303 See Other</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_SEE_OTHER = 303;
+ /** <tt>304 Not Modified</tt> (HTTP/1.0 - RFC 1945) */
+ public static final int SC_NOT_MODIFIED = 304;
+ /** <tt>305 Use Proxy</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_USE_PROXY = 305;
+ /** <tt>307 Temporary Redirect</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_TEMPORARY_REDIRECT = 307;
+
+ // --- 4xx Client Error ---
+
+ /** <tt>400 Bad Request</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_BAD_REQUEST = 400;
+ /** <tt>401 Unauthorized</tt> (HTTP/1.0 - RFC 1945) */
+ public static final int SC_UNAUTHORIZED = 401;
+ /** <tt>402 Payment Required</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_PAYMENT_REQUIRED = 402;
+ /** <tt>403 Forbidden</tt> (HTTP/1.0 - RFC 1945) */
+ public static final int SC_FORBIDDEN = 403;
+ /** <tt>404 Not Found</tt> (HTTP/1.0 - RFC 1945) */
+ public static final int SC_NOT_FOUND = 404;
+ /** <tt>405 Method Not Allowed</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_METHOD_NOT_ALLOWED = 405;
+ /** <tt>406 Not Acceptable</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_NOT_ACCEPTABLE = 406;
+ /** <tt>407 Proxy Authentication Required</tt> (HTTP/1.1 - RFC 2616)*/
+ public static final int SC_PROXY_AUTHENTICATION_REQUIRED = 407;
+ /** <tt>408 Request Timeout</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_REQUEST_TIMEOUT = 408;
+ /** <tt>409 Conflict</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_CONFLICT = 409;
+ /** <tt>410 Gone</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_GONE = 410;
+ /** <tt>411 Length Required</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_LENGTH_REQUIRED = 411;
+ /** <tt>412 Precondition Failed</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_PRECONDITION_FAILED = 412;
+ /** <tt>413 Request Entity Too Large</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_REQUEST_TOO_LONG = 413;
+ /** <tt>414 Request-URI Too Long</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_REQUEST_URI_TOO_LONG = 414;
+ /** <tt>415 Unsupported Media Type</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_UNSUPPORTED_MEDIA_TYPE = 415;
+ /** <tt>416 Requested Range Not Satisfiable</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
+ /** <tt>417 Expectation Failed</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_EXPECTATION_FAILED = 417;
+
+ /**
+ * Static constant for a 418 error.
+ * <tt>418 Unprocessable Entity</tt> (WebDAV drafts?)
+ * or <tt>418 Reauthentication Required</tt> (HTTP/1.1 drafts?)
+ */
+ // not used
+ // public static final int SC_UNPROCESSABLE_ENTITY = 418;
+
+ /**
+ * Static constant for a 419 error.
+ * <tt>419 Insufficient Space on Resource</tt>
+ * (WebDAV - draft-ietf-webdav-protocol-05?)
+ * or <tt>419 Proxy Reauthentication Required</tt>
+ * (HTTP/1.1 drafts?)
+ */
+ public static final int SC_INSUFFICIENT_SPACE_ON_RESOURCE = 419;
+ /**
+ * Static constant for a 420 error.
+ * <tt>420 Method Failure</tt>
+ * (WebDAV - draft-ietf-webdav-protocol-05?)
+ */
+ public static final int SC_METHOD_FAILURE = 420;
+ /** <tt>422 Unprocessable Entity</tt> (WebDAV - RFC 2518) */
+ public static final int SC_UNPROCESSABLE_ENTITY = 422;
+ /** <tt>423 Locked</tt> (WebDAV - RFC 2518) */
+ public static final int SC_LOCKED = 423;
+ /** <tt>424 Failed Dependency</tt> (WebDAV - RFC 2518) */
+ public static final int SC_FAILED_DEPENDENCY = 424;
+
+ // --- 5xx Server Error ---
+
+ /** <tt>500 Server Error</tt> (HTTP/1.0 - RFC 1945) */
+ public static final int SC_INTERNAL_SERVER_ERROR = 500;
+ /** <tt>501 Not Implemented</tt> (HTTP/1.0 - RFC 1945) */
+ public static final int SC_NOT_IMPLEMENTED = 501;
+ /** <tt>502 Bad Gateway</tt> (HTTP/1.0 - RFC 1945) */
+ public static final int SC_BAD_GATEWAY = 502;
+ /** <tt>503 Service Unavailable</tt> (HTTP/1.0 - RFC 1945) */
+ public static final int SC_SERVICE_UNAVAILABLE = 503;
+ /** <tt>504 Gateway Timeout</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_GATEWAY_TIMEOUT = 504;
+ /** <tt>505 HTTP Version Not Supported</tt> (HTTP/1.1 - RFC 2616) */
+ public static final int SC_HTTP_VERSION_NOT_SUPPORTED = 505;
+
+ /** <tt>507 Insufficient Storage</tt> (WebDAV - RFC 2518) */
+ public static final int SC_INSUFFICIENT_STORAGE = 507;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpVersion.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpVersion.java
new file mode 100644
index 0000000000..3eae161721
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/HttpVersion.java
@@ -0,0 +1,110 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import java.io.Serializable;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Represents an HTTP version. HTTP uses a "major.minor" numbering
+ * scheme to indicate versions of the protocol.
+ * <p>
+ * The version of an HTTP message is indicated by an HTTP-Version field
+ * in the first line of the message.
+ * </p>
+ * <pre>
+ * HTTP-Version = "HTTP" "/" 1*DIGIT "." 1*DIGIT
+ * </pre>
+ *
+ * @since 4.0
+ */
+@Immutable
+public final class HttpVersion extends ProtocolVersion
+ implements Serializable {
+
+ private static final long serialVersionUID = -5856653513894415344L;
+
+ /** The protocol name. */
+ public static final String HTTP = "HTTP";
+
+ /** HTTP protocol version 0.9 */
+ public static final HttpVersion HTTP_0_9 = new HttpVersion(0, 9);
+
+ /** HTTP protocol version 1.0 */
+ public static final HttpVersion HTTP_1_0 = new HttpVersion(1, 0);
+
+ /** HTTP protocol version 1.1 */
+ public static final HttpVersion HTTP_1_1 = new HttpVersion(1, 1);
+
+
+ /**
+ * Create an HTTP protocol version designator.
+ *
+ * @param major the major version number of the HTTP protocol
+ * @param minor the minor version number of the HTTP protocol
+ *
+ * @throws IllegalArgumentException if either major or minor version number is negative
+ */
+ public HttpVersion(final int major, final int minor) {
+ super(HTTP, major, minor);
+ }
+
+
+ /**
+ * Obtains a specific HTTP version.
+ *
+ * @param major the major version
+ * @param minor the minor version
+ *
+ * @return an instance of {@link HttpVersion} with the argument version
+ */
+ @Override
+ public ProtocolVersion forVersion(final int major, final int minor) {
+
+ if ((major == this.major) && (minor == this.minor)) {
+ return this;
+ }
+
+ if (major == 1) {
+ if (minor == 0) {
+ return HTTP_1_0;
+ }
+ if (minor == 1) {
+ return HTTP_1_1;
+ }
+ }
+ if ((major == 0) && (minor == 9)) {
+ return HTTP_0_9;
+ }
+
+ // argument checking is done in the constructor
+ return new HttpVersion(major, minor);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/MalformedChunkCodingException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/MalformedChunkCodingException.java
new file mode 100644
index 0000000000..7420ce08e1
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/MalformedChunkCodingException.java
@@ -0,0 +1,57 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import java.io.IOException;
+
+/**
+ * Signals a malformed chunked stream.
+ *
+ * @since 4.0
+ */
+public class MalformedChunkCodingException extends IOException {
+
+ private static final long serialVersionUID = 2158560246948994524L;
+
+ /**
+ * Creates a MalformedChunkCodingException without a detail message.
+ */
+ public MalformedChunkCodingException() {
+ super();
+ }
+
+ /**
+ * Creates a MalformedChunkCodingException with the specified detail message.
+ *
+ * @param message The exception detail message
+ */
+ public MalformedChunkCodingException(final String message) {
+ super(message);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/MessageConstraintException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/MessageConstraintException.java
new file mode 100644
index 0000000000..cd6146194e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/MessageConstraintException.java
@@ -0,0 +1,50 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import java.io.IOException;
+
+/**
+ * Signals a message constraint violation.
+ *
+ * @since 4.3
+ */
+public class MessageConstraintException extends IOException {
+
+ private static final long serialVersionUID = 6077207720446368695L;
+
+ /**
+ * Creates a TruncatedChunkException with the specified detail message.
+ *
+ * @param message The exception detail message
+ */
+ public MessageConstraintException(final String message) {
+ super(message);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/MethodNotSupportedException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/MethodNotSupportedException.java
new file mode 100644
index 0000000000..b418576dfd
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/MethodNotSupportedException.java
@@ -0,0 +1,59 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+
+/**
+ * Signals that an HTTP method is not supported.
+ *
+ * @since 4.0
+ */
+public class MethodNotSupportedException extends HttpException {
+
+ private static final long serialVersionUID = 3365359036840171201L;
+
+ /**
+ * Creates a new MethodNotSupportedException with the specified detail message.
+ *
+ * @param message The exception detail message
+ */
+ public MethodNotSupportedException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new MethodNotSupportedException with the specified detail message and cause.
+ *
+ * @param message the exception detail message
+ * @param cause the <tt>Throwable</tt> that caused this exception, or <tt>null</tt>
+ * if the cause is unavailable, unknown, or not a <tt>Throwable</tt>
+ */
+ public MethodNotSupportedException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/NameValuePair.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/NameValuePair.java
new file mode 100644
index 0000000000..76ec354f9d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/NameValuePair.java
@@ -0,0 +1,47 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+/**
+ * A name / value pair parameter used as an element of HTTP messages.
+ * <pre>
+ * parameter = attribute "=" value
+ * attribute = token
+ * value = token | quoted-string
+ * </pre>
+ *
+ *
+ * @since 4.0
+ */
+public interface NameValuePair {
+
+ String getName();
+
+ String getValue();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/NoHttpResponseException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/NoHttpResponseException.java
new file mode 100644
index 0000000000..1622da492f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/NoHttpResponseException.java
@@ -0,0 +1,50 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import java.io.IOException;
+
+/**
+ * Signals that the target server failed to respond with a valid HTTP response.
+ *
+ * @since 4.0
+ */
+public class NoHttpResponseException extends IOException {
+
+ private static final long serialVersionUID = -7658940387386078766L;
+
+ /**
+ * Creates a new NoHttpResponseException with the specified detail message.
+ *
+ * @param message exception message
+ */
+ public NoHttpResponseException(final String message) {
+ super(message);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/ParseException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/ParseException.java
new file mode 100644
index 0000000000..1c3596485c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/ParseException.java
@@ -0,0 +1,61 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+/**
+ * Signals a parse error.
+ * Parse errors when receiving a message will typically trigger
+ * {@link ProtocolException}. Parse errors that do not occur during
+ * protocol execution may be handled differently.
+ * This is an unchecked exception, since there are cases where
+ * the data to be parsed has been generated and is therefore
+ * known to be parseable.
+ *
+ * @since 4.0
+ */
+public class ParseException extends RuntimeException {
+
+ private static final long serialVersionUID = -7288819855864183578L;
+
+ /**
+ * Creates a {@link ParseException} without details.
+ */
+ public ParseException() {
+ super();
+ }
+
+ /**
+ * Creates a {@link ParseException} with a detail message.
+ *
+ * @param message the exception detail message, or <code>null</code>
+ */
+ public ParseException(final String message) {
+ super(message);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/ProtocolException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/ProtocolException.java
new file mode 100644
index 0000000000..78fef49139
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/ProtocolException.java
@@ -0,0 +1,67 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+/**
+ * Signals that an HTTP protocol violation has occurred.
+ * For example a malformed status line or headers, a missing message body, etc.
+ *
+ *
+ * @since 4.0
+ */
+public class ProtocolException extends HttpException {
+
+ private static final long serialVersionUID = -2143571074341228994L;
+
+ /**
+ * Creates a new ProtocolException with a <tt>null</tt> detail message.
+ */
+ public ProtocolException() {
+ super();
+ }
+
+ /**
+ * Creates a new ProtocolException with the specified detail message.
+ *
+ * @param message The exception detail message
+ */
+ public ProtocolException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new ProtocolException with the specified detail message and cause.
+ *
+ * @param message the exception detail message
+ * @param cause the <tt>Throwable</tt> that caused this exception, or <tt>null</tt>
+ * if the cause is unavailable, unknown, or not a <tt>Throwable</tt>
+ */
+ public ProtocolException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/ProtocolVersion.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/ProtocolVersion.java
new file mode 100644
index 0000000000..058d24f943
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/ProtocolVersion.java
@@ -0,0 +1,264 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import java.io.Serializable;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Represents a protocol version. The "major.minor" numbering
+ * scheme is used to indicate versions of the protocol.
+ * <p>
+ * This class defines a protocol version as a combination of
+ * protocol name, major version number, and minor version number.
+ * Note that {@link #equals} and {@link #hashCode} are defined as
+ * final here, they cannot be overridden in derived classes.
+ * </p>
+ *
+ * @since 4.0
+ */
+@Immutable
+public class ProtocolVersion implements Serializable, Cloneable {
+
+ private static final long serialVersionUID = 8950662842175091068L;
+
+
+ /** Name of the protocol. */
+ protected final String protocol;
+
+ /** Major version number of the protocol */
+ protected final int major;
+
+ /** Minor version number of the protocol */
+ protected final int minor;
+
+
+ /**
+ * Create a protocol version designator.
+ *
+ * @param protocol the name of the protocol, for example "HTTP"
+ * @param major the major version number of the protocol
+ * @param minor the minor version number of the protocol
+ */
+ public ProtocolVersion(final String protocol, final int major, final int minor) {
+ this.protocol = Args.notNull(protocol, "Protocol name");
+ this.major = Args.notNegative(major, "Protocol minor version");
+ this.minor = Args.notNegative(minor, "Protocol minor version");
+ }
+
+ /**
+ * Returns the name of the protocol.
+ *
+ * @return the protocol name
+ */
+ public final String getProtocol() {
+ return protocol;
+ }
+
+ /**
+ * Returns the major version number of the protocol.
+ *
+ * @return the major version number.
+ */
+ public final int getMajor() {
+ return major;
+ }
+
+ /**
+ * Returns the minor version number of the HTTP protocol.
+ *
+ * @return the minor version number.
+ */
+ public final int getMinor() {
+ return minor;
+ }
+
+
+ /**
+ * Obtains a specific version of this protocol.
+ * This can be used by derived classes to instantiate themselves instead
+ * of the base class, and to define constants for commonly used versions.
+ * <br/>
+ * The default implementation in this class returns <code>this</code>
+ * if the version matches, and creates a new {@link ProtocolVersion}
+ * otherwise.
+ *
+ * @param major the major version
+ * @param minor the minor version
+ *
+ * @return a protocol version with the same protocol name
+ * and the argument version
+ */
+ public ProtocolVersion forVersion(final int major, final int minor) {
+
+ if ((major == this.major) && (minor == this.minor)) {
+ return this;
+ }
+
+ // argument checking is done in the constructor
+ return new ProtocolVersion(this.protocol, major, minor);
+ }
+
+
+ /**
+ * Obtains a hash code consistent with {@link #equals}.
+ *
+ * @return the hashcode of this protocol version
+ */
+ @Override
+ public final int hashCode() {
+ return this.protocol.hashCode() ^ (this.major * 100000) ^ this.minor;
+ }
+
+
+ /**
+ * Checks equality of this protocol version with an object.
+ * The object is equal if it is a protocl version with the same
+ * protocol name, major version number, and minor version number.
+ * The specific class of the object is <i>not</i> relevant,
+ * instances of derived classes with identical attributes are
+ * equal to instances of the base class and vice versa.
+ *
+ * @param obj the object to compare with
+ *
+ * @return <code>true</code> if the argument is the same protocol version,
+ * <code>false</code> otherwise
+ */
+ @Override
+ public final boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof ProtocolVersion)) {
+ return false;
+ }
+ final ProtocolVersion that = (ProtocolVersion) obj;
+
+ return ((this.protocol.equals(that.protocol)) &&
+ (this.major == that.major) &&
+ (this.minor == that.minor));
+ }
+
+
+ /**
+ * Checks whether this protocol can be compared to another one.
+ * Only protocol versions with the same protocol name can be
+ * {@link #compareToVersion compared}.
+ *
+ * @param that the protocol version to consider
+ *
+ * @return <code>true</code> if {@link #compareToVersion compareToVersion}
+ * can be called with the argument, <code>false</code> otherwise
+ */
+ public boolean isComparable(final ProtocolVersion that) {
+ return (that != null) && this.protocol.equals(that.protocol);
+ }
+
+
+ /**
+ * Compares this protocol version with another one.
+ * Only protocol versions with the same protocol name can be compared.
+ * This method does <i>not</i> define a total ordering, as it would be
+ * required for {@link java.lang.Comparable}.
+ *
+ * @param that the protocol version to compare with
+ *
+ * @return a negative integer, zero, or a positive integer
+ * as this version is less than, equal to, or greater than
+ * the argument version.
+ *
+ * @throws IllegalArgumentException
+ * if the argument has a different protocol name than this object,
+ * or if the argument is <code>null</code>
+ */
+ public int compareToVersion(final ProtocolVersion that) {
+ Args.notNull(that, "Protocol version");
+ Args.check(this.protocol.equals(that.protocol),
+ "Versions for different protocols cannot be compared: %s %s", this, that);
+ int delta = getMajor() - that.getMajor();
+ if (delta == 0) {
+ delta = getMinor() - that.getMinor();
+ }
+ return delta;
+ }
+
+
+ /**
+ * Tests if this protocol version is greater or equal to the given one.
+ *
+ * @param version the version against which to check this version
+ *
+ * @return <code>true</code> if this protocol version is
+ * {@link #isComparable comparable} to the argument
+ * and {@link #compareToVersion compares} as greater or equal,
+ * <code>false</code> otherwise
+ */
+ public final boolean greaterEquals(final ProtocolVersion version) {
+ return isComparable(version) && (compareToVersion(version) >= 0);
+ }
+
+
+ /**
+ * Tests if this protocol version is less or equal to the given one.
+ *
+ * @param version the version against which to check this version
+ *
+ * @return <code>true</code> if this protocol version is
+ * {@link #isComparable comparable} to the argument
+ * and {@link #compareToVersion compares} as less or equal,
+ * <code>false</code> otherwise
+ */
+ public final boolean lessEquals(final ProtocolVersion version) {
+ return isComparable(version) && (compareToVersion(version) <= 0);
+ }
+
+
+ /**
+ * Converts this protocol version to a string.
+ *
+ * @return a protocol version string, like "HTTP/1.1"
+ */
+ @Override
+ public String toString() {
+ final StringBuilder buffer = new StringBuilder();
+ buffer.append(this.protocol);
+ buffer.append('/');
+ buffer.append(Integer.toString(this.major));
+ buffer.append('.');
+ buffer.append(Integer.toString(this.minor));
+ return buffer.toString();
+ }
+
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/README.txt b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/README.txt
new file mode 100644
index 0000000000..cf4624ca4b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/README.txt
@@ -0,0 +1 @@
+These files are managed in the android-sync repo. Do not modify directly, or your changes will be lost.
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/ReasonPhraseCatalog.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/ReasonPhraseCatalog.java
new file mode 100644
index 0000000000..4cb5be0d05
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/ReasonPhraseCatalog.java
@@ -0,0 +1,51 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import java.util.Locale;
+
+/**
+ * Interface for obtaining reason phrases for HTTP status codes.
+ *
+ * @since 4.0
+ */
+public interface ReasonPhraseCatalog {
+
+ /**
+ * Obtains the reason phrase for a status code.
+ * The optional context allows for catalogs that detect
+ * the language for the reason phrase.
+ *
+ * @param status the status code, in the range 100-599
+ * @param loc the preferred locale for the reason phrase
+ *
+ * @return the reason phrase, or <code>null</code> if unknown
+ */
+ String getReason(int status, Locale loc);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/RequestLine.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/RequestLine.java
new file mode 100644
index 0000000000..e44d737b70
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/RequestLine.java
@@ -0,0 +1,49 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+/**
+ * The Request-Line begins with a method token, followed by the
+ * Request-URI and the protocol version, and ending with CRLF. The
+ * elements are separated by SP characters. No CR or LF is allowed
+ * except in the final CRLF sequence.
+ * <pre>
+ * Request-Line = Method SP Request-URI SP HTTP-Version CRLF
+ * </pre>
+ *
+ * @since 4.0
+ */
+public interface RequestLine {
+
+ String getMethod();
+
+ ProtocolVersion getProtocolVersion();
+
+ String getUri();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/StatusLine.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/StatusLine.java
new file mode 100644
index 0000000000..741db7ab92
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/StatusLine.java
@@ -0,0 +1,52 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+/**
+ * The first line of a Response message is the Status-Line, consisting
+ * of the protocol version followed by a numeric status code and its
+ * associated textual phrase, with each element separated by SP
+ * characters. No CR or LF is allowed except in the final CRLF sequence.
+ * <pre>
+ * Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
+ * </pre>
+ *
+ * @see HttpStatus
+ * @version $Id: StatusLine.java 937295 2010-04-23 13:44:00Z olegk $
+ *
+ * @since 4.0
+ */
+public interface StatusLine {
+
+ ProtocolVersion getProtocolVersion();
+
+ int getStatusCode();
+
+ String getReasonPhrase();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/TokenIterator.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/TokenIterator.java
new file mode 100644
index 0000000000..4f7ea5d552
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/TokenIterator.java
@@ -0,0 +1,59 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+import java.util.Iterator;
+
+/**
+ * An iterator for {@link String} tokens.
+ * This interface is designed as a complement to
+ * {@link HeaderElementIterator}, in cases where the items
+ * are plain strings rather than full header elements.
+ *
+ * @since 4.0
+ */
+public interface TokenIterator extends Iterator<Object> {
+
+ /**
+ * Indicates whether there is another token in this iteration.
+ *
+ * @return <code>true</code> if there is another token,
+ * <code>false</code> otherwise
+ */
+ boolean hasNext();
+
+ /**
+ * Obtains the next token from this iteration.
+ * This method should only be called while {@link #hasNext hasNext}
+ * is true.
+ *
+ * @return the next token in this iteration
+ */
+ String nextToken();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/TruncatedChunkException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/TruncatedChunkException.java
new file mode 100644
index 0000000000..1cb994923e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/TruncatedChunkException.java
@@ -0,0 +1,48 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+/**
+ * Signals a truncated chunk in a chunked stream.
+ *
+ * @since 4.1
+ */
+public class TruncatedChunkException extends MalformedChunkCodingException {
+
+ private static final long serialVersionUID = -23506263930279460L;
+
+ /**
+ * Creates a TruncatedChunkException with the specified detail message.
+ *
+ * @param message The exception detail message
+ */
+ public TruncatedChunkException(final String message) {
+ super(message);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/UnsupportedHttpVersionException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/UnsupportedHttpVersionException.java
new file mode 100644
index 0000000000..15647f4c95
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/UnsupportedHttpVersionException.java
@@ -0,0 +1,57 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib;
+
+
+/**
+ * Signals an unsupported version of the HTTP protocol.
+ *
+ * @since 4.0
+ */
+public class UnsupportedHttpVersionException extends ProtocolException {
+
+ private static final long serialVersionUID = -1348448090193107031L;
+
+
+ /**
+ * Creates an exception without a detail message.
+ */
+ public UnsupportedHttpVersionException() {
+ super();
+ }
+
+ /**
+ * Creates an exception with the specified detail message.
+ *
+ * @param message The exception detail message
+ */
+ public UnsupportedHttpVersionException(final String message) {
+ super(message);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/androidextra/Base64.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/androidextra/Base64.java
new file mode 100644
index 0000000000..40f9801bdd
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/androidextra/Base64.java
@@ -0,0 +1,741 @@
+/*
+ * 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 ch.boye.httpclientandroidlib.androidextra;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Utilities for encoding and decoding the Base64 representation of
+ * binary data. See RFCs <a
+ * href="http://www.ietf.org/rfc/rfc2045.txt">2045</a> and <a
+ * href="http://www.ietf.org/rfc/rfc3548.txt">3548</a>.
+ */
+public class Base64 {
+ /**
+ * Default values for encoder/decoder flags.
+ */
+ public static final int DEFAULT = 0;
+
+ /**
+ * Encoder flag bit to omit the padding '=' characters at the end
+ * of the output (if any).
+ */
+ public static final int NO_PADDING = 1;
+
+ /**
+ * Encoder flag bit to omit all line terminators (i.e., the output
+ * will be on one long line).
+ */
+ public static final int NO_WRAP = 2;
+
+ /**
+ * Encoder flag bit to indicate lines should be terminated with a
+ * CRLF pair instead of just an LF. Has no effect if {@code
+ * NO_WRAP} is specified as well.
+ */
+ public static final int CRLF = 4;
+
+ /**
+ * Encoder/decoder flag bit to indicate using the "URL and
+ * filename safe" variant of Base64 (see RFC 3548 section 4) where
+ * {@code -} and {@code _} are used in place of {@code +} and
+ * {@code /}.
+ */
+ public static final int URL_SAFE = 8;
+
+ /**
+ * Flag to pass to {@link Base64OutputStream} to indicate that it
+ * should not close the output stream it is wrapping when it
+ * itself is closed.
+ */
+ public static final int NO_CLOSE = 16;
+
+ // --------------------------------------------------------
+ // shared code
+ // --------------------------------------------------------
+
+ /* package */ static abstract class Coder {
+ public byte[] output;
+ public int op;
+
+ /**
+ * Encode/decode another block of input data. this.output is
+ * provided by the caller, and must be big enough to hold all
+ * the coded data. On exit, this.opwill be set to the length
+ * of the coded data.
+ *
+ * @param finish true if this is the final call to process for
+ * this object. Will finalize the coder state and
+ * include any final bytes in the output.
+ *
+ * @return true if the input so far is good; false if some
+ * error has been detected in the input stream..
+ */
+ public abstract boolean process(byte[] input, int offset, int len, boolean finish);
+
+ /**
+ * @return the maximum number of bytes a call to process()
+ * could produce for the given number of input bytes. This may
+ * be an overestimate.
+ */
+ public abstract int maxOutputSize(int len);
+ }
+
+ // --------------------------------------------------------
+ // decoding
+ // --------------------------------------------------------
+
+ /**
+ * Decode the Base64-encoded data in input and return the data in
+ * a new byte array.
+ *
+ * <p>The padding '=' characters at the end are considered optional, but
+ * if any are present, there must be the correct number of them.
+ *
+ * @param str the input String to decode, which is converted to
+ * bytes using the default charset
+ * @param flags controls certain features of the decoded output.
+ * Pass {@code DEFAULT} to decode standard Base64.
+ *
+ * @throws IllegalArgumentException if the input contains
+ * incorrect padding
+ */
+ public static byte[] decode(String str, int flags) {
+ return decode(str.getBytes(), flags);
+ }
+
+ /**
+ * Decode the Base64-encoded data in input and return the data in
+ * a new byte array.
+ *
+ * <p>The padding '=' characters at the end are considered optional, but
+ * if any are present, there must be the correct number of them.
+ *
+ * @param input the input array to decode
+ * @param flags controls certain features of the decoded output.
+ * Pass {@code DEFAULT} to decode standard Base64.
+ *
+ * @throws IllegalArgumentException if the input contains
+ * incorrect padding
+ */
+ public static byte[] decode(byte[] input, int flags) {
+ return decode(input, 0, input.length, flags);
+ }
+
+ /**
+ * Decode the Base64-encoded data in input and return the data in
+ * a new byte array.
+ *
+ * <p>The padding '=' characters at the end are considered optional, but
+ * if any are present, there must be the correct number of them.
+ *
+ * @param input the data to decode
+ * @param offset the position within the input array at which to start
+ * @param len the number of bytes of input to decode
+ * @param flags controls certain features of the decoded output.
+ * Pass {@code DEFAULT} to decode standard Base64.
+ *
+ * @throws IllegalArgumentException if the input contains
+ * incorrect padding
+ */
+ public static byte[] decode(byte[] input, int offset, int len, int flags) {
+ // Allocate space for the most data the input could represent.
+ // (It could contain less if it contains whitespace, etc.)
+ Decoder decoder = new Decoder(flags, new byte[len*3/4]);
+
+ if (!decoder.process(input, offset, len, true)) {
+ throw new IllegalArgumentException("bad base-64");
+ }
+
+ // Maybe we got lucky and allocated exactly enough output space.
+ if (decoder.op == decoder.output.length) {
+ return decoder.output;
+ }
+
+ // Need to shorten the array, so allocate a new one of the
+ // right size and copy.
+ byte[] temp = new byte[decoder.op];
+ System.arraycopy(decoder.output, 0, temp, 0, decoder.op);
+ return temp;
+ }
+
+ /* package */ static class Decoder extends Coder {
+ /**
+ * Lookup table for turning bytes into their position in the
+ * Base64 alphabet.
+ */
+ private static final int DECODE[] = {
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
+ -1, 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, -1, -1, -1, -1, -1,
+ -1, 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, 51, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ };
+
+ /**
+ * Decode lookup table for the "web safe" variant (RFC 3548
+ * sec. 4) where - and _ replace + and /.
+ */
+ private static final int DECODE_WEBSAFE[] = {
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1,
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
+ -1, 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, -1, -1, -1, -1, 63,
+ -1, 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, 51, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ };
+
+ /** Non-data values in the DECODE arrays. */
+ private static final int SKIP = -1;
+ private static final int EQUALS = -2;
+
+ /**
+ * States 0-3 are reading through the next input tuple.
+ * State 4 is having read one '=' and expecting exactly
+ * one more.
+ * State 5 is expecting no more data or padding characters
+ * in the input.
+ * State 6 is the error state; an error has been detected
+ * in the input and no future input can "fix" it.
+ */
+ private int state; // state number (0 to 6)
+ private int value;
+
+ final private int[] alphabet;
+
+ public Decoder(int flags, byte[] output) {
+ this.output = output;
+
+ alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE;
+ state = 0;
+ value = 0;
+ }
+
+ /**
+ * @return an overestimate for the number of bytes {@code
+ * len} bytes could decode to.
+ */
+ public int maxOutputSize(int len) {
+ return len * 3/4 + 10;
+ }
+
+ /**
+ * Decode another block of input data.
+ *
+ * @return true if the state machine is still healthy. false if
+ * bad base-64 data has been detected in the input stream.
+ */
+ public boolean process(byte[] input, int offset, int len, boolean finish) {
+ if (this.state == 6) return false;
+
+ int p = offset;
+ len += offset;
+
+ // Using local variables makes the decoder about 12%
+ // faster than if we manipulate the member variables in
+ // the loop. (Even alphabet makes a measurable
+ // difference, which is somewhat surprising to me since
+ // the member variable is final.)
+ int state = this.state;
+ int value = this.value;
+ int op = 0;
+ final byte[] output = this.output;
+ final int[] alphabet = this.alphabet;
+
+ while (p < len) {
+ // Try the fast path: we're starting a new tuple and the
+ // next four bytes of the input stream are all data
+ // bytes. This corresponds to going through states
+ // 0-1-2-3-0. We expect to use this method for most of
+ // the data.
+ //
+ // If any of the next four bytes of input are non-data
+ // (whitespace, etc.), value will end up negative. (All
+ // the non-data values in decode are small negative
+ // numbers, so shifting any of them up and or'ing them
+ // together will result in a value with its top bit set.)
+ //
+ // You can remove this whole block and the output should
+ // be the same, just slower.
+ if (state == 0) {
+ while (p+4 <= len &&
+ (value = ((alphabet[input[p] & 0xff] << 18) |
+ (alphabet[input[p+1] & 0xff] << 12) |
+ (alphabet[input[p+2] & 0xff] << 6) |
+ (alphabet[input[p+3] & 0xff]))) >= 0) {
+ output[op+2] = (byte) value;
+ output[op+1] = (byte) (value >> 8);
+ output[op] = (byte) (value >> 16);
+ op += 3;
+ p += 4;
+ }
+ if (p >= len) break;
+ }
+
+ // The fast path isn't available -- either we've read a
+ // partial tuple, or the next four input bytes aren't all
+ // data, or whatever. Fall back to the slower state
+ // machine implementation.
+
+ int d = alphabet[input[p++] & 0xff];
+
+ switch (state) {
+ case 0:
+ if (d >= 0) {
+ value = d;
+ ++state;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 1:
+ if (d >= 0) {
+ value = (value << 6) | d;
+ ++state;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 2:
+ if (d >= 0) {
+ value = (value << 6) | d;
+ ++state;
+ } else if (d == EQUALS) {
+ // Emit the last (partial) output tuple;
+ // expect exactly one more padding character.
+ output[op++] = (byte) (value >> 4);
+ state = 4;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 3:
+ if (d >= 0) {
+ // Emit the output triple and return to state 0.
+ value = (value << 6) | d;
+ output[op+2] = (byte) value;
+ output[op+1] = (byte) (value >> 8);
+ output[op] = (byte) (value >> 16);
+ op += 3;
+ state = 0;
+ } else if (d == EQUALS) {
+ // Emit the last (partial) output tuple;
+ // expect no further data or padding characters.
+ output[op+1] = (byte) (value >> 2);
+ output[op] = (byte) (value >> 10);
+ op += 2;
+ state = 5;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 4:
+ if (d == EQUALS) {
+ ++state;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 5:
+ if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+ }
+ }
+
+ if (!finish) {
+ // We're out of input, but a future call could provide
+ // more.
+ this.state = state;
+ this.value = value;
+ this.op = op;
+ return true;
+ }
+
+ // Done reading input. Now figure out where we are left in
+ // the state machine and finish up.
+
+ switch (state) {
+ case 0:
+ // Output length is a multiple of three. Fine.
+ break;
+ case 1:
+ // Read one extra input byte, which isn't enough to
+ // make another output byte. Illegal.
+ this.state = 6;
+ return false;
+ case 2:
+ // Read two extra input bytes, enough to emit 1 more
+ // output byte. Fine.
+ output[op++] = (byte) (value >> 4);
+ break;
+ case 3:
+ // Read three extra input bytes, enough to emit 2 more
+ // output bytes. Fine.
+ output[op++] = (byte) (value >> 10);
+ output[op++] = (byte) (value >> 2);
+ break;
+ case 4:
+ // Read one padding '=' when we expected 2. Illegal.
+ this.state = 6;
+ return false;
+ case 5:
+ // Read all the padding '='s we expected and no more.
+ // Fine.
+ break;
+ }
+
+ this.state = state;
+ this.op = op;
+ return true;
+ }
+ }
+
+ // --------------------------------------------------------
+ // encoding
+ // --------------------------------------------------------
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * String with the result.
+ *
+ * @param input the data to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ */
+ public static String encodeToString(byte[] input, int flags) {
+ try {
+ return new String(encode(input, flags), "US-ASCII");
+ } catch (UnsupportedEncodingException e) {
+ // US-ASCII is guaranteed to be available.
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * String with the result.
+ *
+ * @param input the data to encode
+ * @param offset the position within the input array at which to
+ * start
+ * @param len the number of bytes of input to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ */
+ public static String encodeToString(byte[] input, int offset, int len, int flags) {
+ try {
+ return new String(encode(input, offset, len, flags), "US-ASCII");
+ } catch (UnsupportedEncodingException e) {
+ // US-ASCII is guaranteed to be available.
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * byte[] with the result.
+ *
+ * @param input the data to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ */
+ public static byte[] encode(byte[] input, int flags) {
+ return encode(input, 0, input.length, flags);
+ }
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * byte[] with the result.
+ *
+ * @param input the data to encode
+ * @param offset the position within the input array at which to
+ * start
+ * @param len the number of bytes of input to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ */
+ public static byte[] encode(byte[] input, int offset, int len, int flags) {
+ Encoder encoder = new Encoder(flags, null);
+
+ // Compute the exact length of the array we will produce.
+ int output_len = len / 3 * 4;
+
+ // Account for the tail of the data and the padding bytes, if any.
+ if (encoder.do_padding) {
+ if (len % 3 > 0) {
+ output_len += 4;
+ }
+ } else {
+ switch (len % 3) {
+ case 0: break;
+ case 1: output_len += 2; break;
+ case 2: output_len += 3; break;
+ }
+ }
+
+ // Account for the newlines, if any.
+ if (encoder.do_newline && len > 0) {
+ output_len += (((len-1) / (3 * Encoder.LINE_GROUPS)) + 1) *
+ (encoder.do_cr ? 2 : 1);
+ }
+
+ encoder.output = new byte[output_len];
+ encoder.process(input, offset, len, true);
+
+ assert encoder.op == output_len;
+
+ return encoder.output;
+ }
+
+ /* package */ static class Encoder extends Coder {
+ /**
+ * Emit a new line every this many output tuples. Corresponds to
+ * a 76-character line length (the maximum allowable according to
+ * <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>).
+ */
+ public static final int LINE_GROUPS = 19;
+
+ /**
+ * Lookup table for turning Base64 alphabet positions (6 bits)
+ * into output bytes.
+ */
+ private static final byte ENCODE[] = {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
+ 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
+ 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
+ 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/',
+ };
+
+ /**
+ * Lookup table for turning Base64 alphabet positions (6 bits)
+ * into output bytes.
+ */
+ private static final byte ENCODE_WEBSAFE[] = {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
+ 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
+ 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
+ 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_',
+ };
+
+ final private byte[] tail;
+ /* package */ int tailLen;
+ private int count;
+
+ final public boolean do_padding;
+ final public boolean do_newline;
+ final public boolean do_cr;
+ final private byte[] alphabet;
+
+ public Encoder(int flags, byte[] output) {
+ this.output = output;
+
+ do_padding = (flags & NO_PADDING) == 0;
+ do_newline = (flags & NO_WRAP) == 0;
+ do_cr = (flags & CRLF) != 0;
+ alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE;
+
+ tail = new byte[2];
+ tailLen = 0;
+
+ count = do_newline ? LINE_GROUPS : -1;
+ }
+
+ /**
+ * @return an overestimate for the number of bytes {@code
+ * len} bytes could encode to.
+ */
+ public int maxOutputSize(int len) {
+ return len * 8/5 + 10;
+ }
+
+ public boolean process(byte[] input, int offset, int len, boolean finish) {
+ // Using local variables makes the encoder about 9% faster.
+ final byte[] alphabet = this.alphabet;
+ final byte[] output = this.output;
+ int op = 0;
+ int count = this.count;
+
+ int p = offset;
+ len += offset;
+ int v = -1;
+
+ // First we need to concatenate the tail of the previous call
+ // with any input bytes available now and see if we can empty
+ // the tail.
+
+ switch (tailLen) {
+ case 0:
+ // There was no tail.
+ break;
+
+ case 1:
+ if (p+2 <= len) {
+ // A 1-byte tail with at least 2 bytes of
+ // input available now.
+ v = ((tail[0] & 0xff) << 16) |
+ ((input[p++] & 0xff) << 8) |
+ (input[p++] & 0xff);
+ tailLen = 0;
+ };
+ break;
+
+ case 2:
+ if (p+1 <= len) {
+ // A 2-byte tail with at least 1 byte of input.
+ v = ((tail[0] & 0xff) << 16) |
+ ((tail[1] & 0xff) << 8) |
+ (input[p++] & 0xff);
+ tailLen = 0;
+ }
+ break;
+ }
+
+ if (v != -1) {
+ output[op++] = alphabet[(v >> 18) & 0x3f];
+ output[op++] = alphabet[(v >> 12) & 0x3f];
+ output[op++] = alphabet[(v >> 6) & 0x3f];
+ output[op++] = alphabet[v & 0x3f];
+ if (--count == 0) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ count = LINE_GROUPS;
+ }
+ }
+
+ // At this point either there is no tail, or there are fewer
+ // than 3 bytes of input available.
+
+ // The main loop, turning 3 input bytes into 4 output bytes on
+ // each iteration.
+ while (p+3 <= len) {
+ v = ((input[p] & 0xff) << 16) |
+ ((input[p+1] & 0xff) << 8) |
+ (input[p+2] & 0xff);
+ output[op] = alphabet[(v >> 18) & 0x3f];
+ output[op+1] = alphabet[(v >> 12) & 0x3f];
+ output[op+2] = alphabet[(v >> 6) & 0x3f];
+ output[op+3] = alphabet[v & 0x3f];
+ p += 3;
+ op += 4;
+ if (--count == 0) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ count = LINE_GROUPS;
+ }
+ }
+
+ if (finish) {
+ // Finish up the tail of the input. Note that we need to
+ // consume any bytes in tail before any bytes
+ // remaining in input; there should be at most two bytes
+ // total.
+
+ if (p-tailLen == len-1) {
+ int t = 0;
+ v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4;
+ tailLen -= t;
+ output[op++] = alphabet[(v >> 6) & 0x3f];
+ output[op++] = alphabet[v & 0x3f];
+ if (do_padding) {
+ output[op++] = '=';
+ output[op++] = '=';
+ }
+ if (do_newline) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ }
+ } else if (p-tailLen == len-2) {
+ int t = 0;
+ v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) |
+ (((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2);
+ tailLen -= t;
+ output[op++] = alphabet[(v >> 12) & 0x3f];
+ output[op++] = alphabet[(v >> 6) & 0x3f];
+ output[op++] = alphabet[v & 0x3f];
+ if (do_padding) {
+ output[op++] = '=';
+ }
+ if (do_newline) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ }
+ } else if (do_newline && op > 0 && count != LINE_GROUPS) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ }
+
+ assert tailLen == 0;
+ assert p == len;
+ } else {
+ // Save the leftovers in tail to be consumed on the next
+ // call to encodeInternal.
+
+ if (p == len-1) {
+ tail[tailLen++] = input[p];
+ } else if (p == len-2) {
+ tail[tailLen++] = input[p];
+ tail[tailLen++] = input[p+1];
+ }
+ }
+
+ this.op = op;
+ this.count = count;
+
+ return true;
+ }
+ }
+
+ private Base64() { } // don't instantiate
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/androidextra/HttpClientAndroidLog.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/androidextra/HttpClientAndroidLog.java
new file mode 100644
index 0000000000..89758f315a
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/androidextra/HttpClientAndroidLog.java
@@ -0,0 +1,113 @@
+package ch.boye.httpclientandroidlib.androidextra;
+
+import android.util.Log;
+
+public class HttpClientAndroidLog {
+
+ private String logTag;
+ private boolean debugEnabled;
+ private boolean errorEnabled;
+ private boolean traceEnabled;
+ private boolean warnEnabled;
+ private boolean infoEnabled;
+
+ public HttpClientAndroidLog(Object tag) {
+ logTag=tag.toString();
+ debugEnabled=false;
+ errorEnabled=false;
+ traceEnabled=false;
+ warnEnabled=false;
+ infoEnabled=false;
+ }
+
+ public void enableDebug(boolean enable) {
+ debugEnabled=enable;
+ }
+
+ public boolean isDebugEnabled() {
+ return debugEnabled;
+ }
+
+ public void debug(Object message) {
+ if(isDebugEnabled())
+ Log.d(logTag, message.toString());
+ }
+
+ public void debug(Object message, Throwable t) {
+ if(isDebugEnabled())
+ Log.d(logTag, message.toString(), t);
+ }
+
+ public void enableError(boolean enable) {
+ errorEnabled=enable;
+ }
+
+ public boolean isErrorEnabled() {
+ return errorEnabled;
+ }
+
+ public void error(Object message) {
+ if(isErrorEnabled())
+ Log.e(logTag, message.toString());
+ }
+
+ public void error(Object message, Throwable t) {
+ if(isErrorEnabled())
+ Log.e(logTag, message.toString(), t);
+ }
+
+ public void enableWarn(boolean enable) {
+ warnEnabled=enable;
+ }
+
+ public boolean isWarnEnabled() {
+ return warnEnabled;
+ }
+
+ public void warn(Object message) {
+ if(isWarnEnabled())
+ Log.w(logTag, message.toString());
+ }
+
+ public void warn(Object message, Throwable t) {
+ if(isWarnEnabled())
+ Log.w(logTag, message.toString(), t);
+ }
+
+ public void enableInfo(boolean enable) {
+ infoEnabled=enable;
+ }
+
+ public boolean isInfoEnabled() {
+ return infoEnabled;
+ }
+
+ public void info(Object message) {
+ if(isInfoEnabled())
+ Log.i(logTag, message.toString());
+ }
+
+ public void info(Object message, Throwable t) {
+ if(isInfoEnabled())
+ Log.i(logTag, message.toString(), t);
+ }
+
+ public void enableTrace(boolean enable) {
+ traceEnabled=enable;
+ }
+
+ public boolean isTraceEnabled() {
+ return traceEnabled;
+ }
+
+ public void trace(Object message) {
+ if(isTraceEnabled())
+ Log.i(logTag, message.toString());
+ }
+
+ public void trace(Object message, Throwable t) {
+ if(isTraceEnabled())
+ Log.i(logTag, message.toString(), t);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/annotation/GuardedBy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/annotation/GuardedBy.java
new file mode 100644
index 0000000000..2a61da7524
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/annotation/GuardedBy.java
@@ -0,0 +1,76 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * The field or method to which this annotation is applied can only be accessed
+ * when holding a particular lock, which may be a built-in (synchronization) lock,
+ * or may be an explicit java.util.concurrent.Lock.
+ *
+ * The argument determines which lock guards the annotated field or method:
+ * <ul>
+ * <li>
+ * <code>this</code> : The intrinsic lock of the object in whose class the field is defined.
+ * </li>
+ * <li>
+ * <code>class-name.this</code> : For inner classes, it may be necessary to disambiguate 'this';
+ * the <em>class-name.this</em> designation allows you to specify which 'this' reference is intended
+ * </li>
+ * <li>
+ * <code>itself</code> : For reference fields only; the object to which the field refers.
+ * </li>
+ * <li>
+ * <code>field-name</code> : The lock object is referenced by the (instance or static) field
+ * specified by <em>field-name</em>.
+ * </li>
+ * <li>
+ * <code>class-name.field-name</code> : The lock object is reference by the static field specified
+ * by <em>class-name.field-name</em>.
+ * </li>
+ * <li>
+ * <code>method-name()</code> : The lock object is returned by calling the named nil-ary method.
+ * </li>
+ * <li>
+ * <code>class-name.class</code> : The Class object for the specified class should be used as the lock object.
+ * </li>
+ * <p>
+ * Based on code developed by Brian Goetz and Tim Peierls and concepts
+ * published in 'Java Concurrency in Practice' by Brian Goetz, Tim Peierls,
+ * Joshua Bloch, Joseph Bowbeer, David Holmes and Doug Lea.
+ */
+@Documented
+@Target({ElementType.FIELD, ElementType.METHOD})
+@Retention(RetentionPolicy.CLASS) // The original version used RUNTIME
+public @interface GuardedBy {
+ String value();
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/annotation/Immutable.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/annotation/Immutable.java
new file mode 100644
index 0000000000..3aa8eb86ee
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/annotation/Immutable.java
@@ -0,0 +1,59 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * The class to which this annotation is applied is immutable. This means that
+ * its state cannot be seen to change by callers, which implies that
+ * <ul>
+ * <li> all public fields are final, </li>
+ * <li> all public final reference fields refer to other immutable objects, and </li>
+ * <li> constructors and methods do not publish references to any internal state
+ * which is potentially mutable by the implementation. </li>
+ * </ul>
+ * Immutable objects may still have internal mutable state for purposes of performance
+ * optimization; some state variables may be lazily computed, so long as they are computed
+ * from immutable state and that callers cannot tell the difference.
+ * <p>
+ * Immutable objects are inherently thread-safe; they may be passed between threads or
+ * published without synchronization.
+ * <p>
+ * Based on code developed by Brian Goetz and Tim Peierls and concepts
+ * published in 'Java Concurrency in Practice' by Brian Goetz, Tim Peierls,
+ * Joshua Bloch, Joseph Bowbeer, David Holmes and Doug Lea.
+ */
+@Documented
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.CLASS) // The original version used RUNTIME
+public @interface Immutable {
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/annotation/NotThreadSafe.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/annotation/NotThreadSafe.java
new file mode 100644
index 0000000000..0cab7dda2f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/annotation/NotThreadSafe.java
@@ -0,0 +1,50 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * The class to which this annotation is applied is not thread-safe.
+ * This annotation primarily exists for clarifying the non-thread-safety of a class
+ * that might otherwise be assumed to be thread-safe, despite the fact that it is a bad
+ * idea to assume a class is thread-safe without good reason.
+ * @see ThreadSafe
+ * <p>
+ * Based on code developed by Brian Goetz and Tim Peierls and concepts
+ * published in 'Java Concurrency in Practice' by Brian Goetz, Tim Peierls,
+ * Joshua Bloch, Joseph Bowbeer, David Holmes and Doug Lea.
+ */
+@Documented
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.CLASS) // The original version used RUNTIME
+public @interface NotThreadSafe {
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/annotation/ThreadSafe.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/annotation/ThreadSafe.java
new file mode 100644
index 0000000000..c8b1616d9e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/annotation/ThreadSafe.java
@@ -0,0 +1,51 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * The class to which this annotation is applied is thread-safe. This means that
+ * no sequences of accesses (reads and writes to public fields, calls to public methods)
+ * may put the object into an invalid state, regardless of the interleaving of those actions
+ * by the runtime, and without requiring any additional synchronization or coordination on the
+ * part of the caller.
+ * @see NotThreadSafe
+ * <p>
+ * Based on code developed by Brian Goetz and Tim Peierls and concepts
+ * published in 'Java Concurrency in Practice' by Brian Goetz, Tim Peierls,
+ * Joshua Bloch, Joseph Bowbeer, David Holmes and Doug Lea.
+ */
+@Documented
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.CLASS) // The original version used RUNTIME
+public @interface ThreadSafe {
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/annotation/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/annotation/package-info.java
new file mode 100644
index 0000000000..d74e8fef96
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/annotation/package-info.java
@@ -0,0 +1,34 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Thread-safety annotations based on JCIP-ANNOTATIONS
+ * <br/>
+ * Copyright (c) 2005 Brian Goetz and Tim Peierls.
+ * See http://www.jcip.net
+ */
+package ch.boye.httpclientandroidlib.annotation;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AUTH.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AUTH.java
new file mode 100644
index 0000000000..a0b50db73c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AUTH.java
@@ -0,0 +1,64 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.auth;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Constants and static helpers related to the HTTP authentication.
+ *
+ *
+ * @since 4.0
+ */
+@Immutable
+public final class AUTH {
+
+ /**
+ * The www authenticate challange header.
+ */
+ public static final String WWW_AUTH = "WWW-Authenticate";
+
+ /**
+ * The www authenticate response header.
+ */
+ public static final String WWW_AUTH_RESP = "Authorization";
+
+ /**
+ * The proxy authenticate challange header.
+ */
+ public static final String PROXY_AUTH = "Proxy-Authenticate";
+
+ /**
+ * The proxy authenticate response header.
+ */
+ public static final String PROXY_AUTH_RESP = "Proxy-Authorization";
+
+ private AUTH() {
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthOption.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthOption.java
new file mode 100644
index 0000000000..ae69df94d5
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthOption.java
@@ -0,0 +1,62 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.auth;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * @since 4.2
+ */
+@Immutable
+public final class AuthOption {
+
+ private final AuthScheme authScheme;
+ private final Credentials creds;
+
+ public AuthOption(final AuthScheme authScheme, final Credentials creds) {
+ super();
+ Args.notNull(authScheme, "Auth scheme");
+ Args.notNull(creds, "User credentials");
+ this.authScheme = authScheme;
+ this.creds = creds;
+ }
+
+ public AuthScheme getAuthScheme() {
+ return this.authScheme;
+ }
+
+ public Credentials getCredentials() {
+ return this.creds;
+ }
+
+ @Override
+ public String toString() {
+ return this.authScheme.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthProtocolState.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthProtocolState.java
new file mode 100644
index 0000000000..081838915a
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthProtocolState.java
@@ -0,0 +1,33 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.auth;
+
+public enum AuthProtocolState {
+
+ UNCHALLENGED, CHALLENGED, HANDSHAKE, FAILURE, SUCCESS
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthScheme.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthScheme.java
new file mode 100644
index 0000000000..a028f182d6
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthScheme.java
@@ -0,0 +1,130 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.auth;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpRequest;
+
+/**
+ * This interface represents an abstract challenge-response oriented
+ * authentication scheme.
+ * <p>
+ * An authentication scheme should be able to support the following
+ * functions:
+ * <ul>
+ * <li>Parse and process the challenge sent by the target server
+ * in response to request for a protected resource
+ * <li>Provide its textual designation
+ * <li>Provide its parameters, if available
+ * <li>Provide the realm this authentication scheme is applicable to,
+ * if available
+ * <li>Generate authorization string for the given set of credentials
+ * and the HTTP request in response to the authorization challenge.
+ * </ul>
+ * <p>
+ * Authentication schemes may be stateful involving a series of
+ * challenge-response exchanges.
+ * <p>
+ * IMPORTANT: implementations of this interface MUST also implement {@link ContextAwareAuthScheme}
+ * interface in order to remain API compatible with newer versions of HttpClient.
+ *
+ * @since 4.0
+ */
+
+public interface AuthScheme {
+
+ /**
+ * Processes the given challenge token. Some authentication schemes
+ * may involve multiple challenge-response exchanges. Such schemes must be able
+ * to maintain the state information when dealing with sequential challenges
+ *
+ * @param header the challenge header
+ */
+ void processChallenge(final Header header) throws MalformedChallengeException;
+
+ /**
+ * Returns textual designation of the given authentication scheme.
+ *
+ * @return the name of the given authentication scheme
+ */
+ String getSchemeName();
+
+ /**
+ * Returns authentication parameter with the given name, if available.
+ *
+ * @param name The name of the parameter to be returned
+ *
+ * @return the parameter with the given name
+ */
+ String getParameter(final String name);
+
+ /**
+ * Returns authentication realm. If the concept of an authentication
+ * realm is not applicable to the given authentication scheme, returns
+ * <code>null</code>.
+ *
+ * @return the authentication realm
+ */
+ String getRealm();
+
+ /**
+ * Tests if the authentication scheme is provides authorization on a per
+ * connection basis instead of usual per request basis
+ *
+ * @return <tt>true</tt> if the scheme is connection based, <tt>false</tt>
+ * if the scheme is request based.
+ */
+ boolean isConnectionBased();
+
+ /**
+ * Authentication process may involve a series of challenge-response exchanges.
+ * This method tests if the authorization process has been completed, either
+ * successfully or unsuccessfully, that is, all the required authorization
+ * challenges have been processed in their entirety.
+ *
+ * @return <tt>true</tt> if the authentication process has been completed,
+ * <tt>false</tt> otherwise.
+ */
+ boolean isComplete();
+
+ /**
+ * Produces an authorization string for the given set of {@link Credentials}.
+ *
+ * @param credentials The set of credentials to be used for athentication
+ * @param request The request being authenticated
+ * @throws AuthenticationException if authorization string cannot
+ * be generated due to an authentication failure
+ *
+ * @return the authorization string
+ *
+ * @deprecated (4.1) Use {@link ContextAwareAuthScheme#authenticate(Credentials, HttpRequest, ch.boye.httpclientandroidlib.protocol.HttpContext)}
+ */
+ @Deprecated
+ Header authenticate(Credentials credentials, HttpRequest request)
+ throws AuthenticationException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthSchemeFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthSchemeFactory.java
new file mode 100644
index 0000000000..d88a3d06f3
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthSchemeFactory.java
@@ -0,0 +1,51 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.auth;
+
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+/**
+ * Factory for {@link AuthScheme} implementations.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link AuthSchemeProvider}
+ */
+@Deprecated
+public interface AuthSchemeFactory {
+
+ /**
+ * Creates an instance of {@link AuthScheme} using given HTTP parameters.
+ *
+ * @param params HTTP parameters.
+ *
+ * @return auth scheme.
+ */
+ AuthScheme newInstance(HttpParams params);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthSchemeProvider.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthSchemeProvider.java
new file mode 100644
index 0000000000..bfaf3f3cfb
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthSchemeProvider.java
@@ -0,0 +1,46 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.auth;
+
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * Factory for {@link AuthScheme} implementations.
+ *
+ * @since 4.3
+ */
+public interface AuthSchemeProvider {
+
+ /**
+ * Creates an instance of {@link AuthScheme}.
+ *
+ * @return auth scheme.
+ */
+ AuthScheme create(HttpContext context);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthSchemeRegistry.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthSchemeRegistry.java
new file mode 100644
index 0000000000..4a6d15ca8d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthSchemeRegistry.java
@@ -0,0 +1,155 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.auth;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.config.Lookup;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.ExecutionContext;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Authentication scheme registry that can be used to obtain the corresponding
+ * authentication scheme implementation for a given type of authorization challenge.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.config.Registry}
+ */
+@ThreadSafe
+@Deprecated
+public final class AuthSchemeRegistry implements Lookup<AuthSchemeProvider> {
+
+ private final ConcurrentHashMap<String,AuthSchemeFactory> registeredSchemes;
+
+ public AuthSchemeRegistry() {
+ super();
+ this.registeredSchemes = new ConcurrentHashMap<String,AuthSchemeFactory>();
+ }
+
+ /**
+ * Registers a {@link AuthSchemeFactory} with the given identifier. If a factory with the
+ * given name already exists it will be overridden. This name is the same one used to
+ * retrieve the {@link AuthScheme authentication scheme} from {@link #getAuthScheme}.
+ *
+ * <p>
+ * Please note that custom authentication preferences, if used, need to be updated accordingly
+ * for the new {@link AuthScheme authentication scheme} to take effect.
+ * </p>
+ *
+ * @param name the identifier for this scheme
+ * @param factory the {@link AuthSchemeFactory} class to register
+ *
+ * @see #getAuthScheme
+ */
+ public void register(
+ final String name,
+ final AuthSchemeFactory factory) {
+ Args.notNull(name, "Name");
+ Args.notNull(factory, "Authentication scheme factory");
+ registeredSchemes.put(name.toLowerCase(Locale.ENGLISH), factory);
+ }
+
+ /**
+ * Unregisters the class implementing an {@link AuthScheme authentication scheme} with
+ * the given name.
+ *
+ * @param name the identifier of the class to unregister
+ */
+ public void unregister(final String name) {
+ Args.notNull(name, "Name");
+ registeredSchemes.remove(name.toLowerCase(Locale.ENGLISH));
+ }
+
+ /**
+ * Gets the {@link AuthScheme authentication scheme} with the given name.
+ *
+ * @param name the {@link AuthScheme authentication scheme} identifier
+ * @param params the {@link HttpParams HTTP parameters} for the authentication
+ * scheme.
+ *
+ * @return {@link AuthScheme authentication scheme}
+ *
+ * @throws IllegalStateException if a scheme with the given name cannot be found
+ */
+ public AuthScheme getAuthScheme(final String name, final HttpParams params)
+ throws IllegalStateException {
+
+ Args.notNull(name, "Name");
+ final AuthSchemeFactory factory = registeredSchemes.get(name.toLowerCase(Locale.ENGLISH));
+ if (factory != null) {
+ return factory.newInstance(params);
+ } else {
+ throw new IllegalStateException("Unsupported authentication scheme: " + name);
+ }
+ }
+
+ /**
+ * Obtains a list containing the names of all registered {@link AuthScheme authentication
+ * schemes}
+ *
+ * @return list of registered scheme names
+ */
+ public List<String> getSchemeNames() {
+ return new ArrayList<String>(registeredSchemes.keySet());
+ }
+
+ /**
+ * Populates the internal collection of registered {@link AuthScheme authentication schemes}
+ * with the content of the map passed as a parameter.
+ *
+ * @param map authentication schemes
+ */
+ public void setItems(final Map<String, AuthSchemeFactory> map) {
+ if (map == null) {
+ return;
+ }
+ registeredSchemes.clear();
+ registeredSchemes.putAll(map);
+ }
+
+ public AuthSchemeProvider lookup(final String name) {
+ return new AuthSchemeProvider() {
+
+ public AuthScheme create(final HttpContext context) {
+ final HttpRequest request = (HttpRequest) context.getAttribute(
+ ExecutionContext.HTTP_REQUEST);
+ return getAuthScheme(name, request.getParams());
+ }
+
+ };
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthScope.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthScope.java
new file mode 100644
index 0000000000..b6384729d2
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthScope.java
@@ -0,0 +1,302 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.auth;
+
+import java.util.Locale;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.LangUtils;
+
+/**
+ * The class represents an authentication scope consisting of a host name,
+ * a port number, a realm name and an authentication scheme name which
+ * {@link Credentials Credentials} apply to.
+ *
+ *
+ * @since 4.0
+ */
+@Immutable
+public class AuthScope {
+
+ /**
+ * The <tt>null</tt> value represents any host. In the future versions of
+ * HttpClient the use of this parameter will be discontinued.
+ */
+ public static final String ANY_HOST = null;
+
+ /**
+ * The <tt>-1</tt> value represents any port.
+ */
+ public static final int ANY_PORT = -1;
+
+ /**
+ * The <tt>null</tt> value represents any realm.
+ */
+ public static final String ANY_REALM = null;
+
+ /**
+ * The <tt>null</tt> value represents any authentication scheme.
+ */
+ public static final String ANY_SCHEME = null;
+
+ /**
+ * Default scope matching any host, port, realm and authentication scheme.
+ * In the future versions of HttpClient the use of this parameter will be
+ * discontinued.
+ */
+ public static final AuthScope ANY = new AuthScope(ANY_HOST, ANY_PORT, ANY_REALM, ANY_SCHEME);
+
+ /** The authentication scheme the credentials apply to. */
+ private final String scheme;
+
+ /** The realm the credentials apply to. */
+ private final String realm;
+
+ /** The host the credentials apply to. */
+ private final String host;
+
+ /** The port the credentials apply to. */
+ private final int port;
+
+ /** Creates a new credentials scope for the given
+ * <tt>host</tt>, <tt>port</tt>, <tt>realm</tt>, and
+ * <tt>authentication scheme</tt>.
+ *
+ * @param host the host the credentials apply to. May be set
+ * to <tt>null</tt> if credentials are applicable to
+ * any host.
+ * @param port the port the credentials apply to. May be set
+ * to negative value if credentials are applicable to
+ * any port.
+ * @param realm the realm the credentials apply to. May be set
+ * to <tt>null</tt> if credentials are applicable to
+ * any realm.
+ * @param scheme the authentication scheme the credentials apply to.
+ * May be set to <tt>null</tt> if credentials are applicable to
+ * any authentication scheme.
+ */
+ public AuthScope(final String host, final int port,
+ final String realm, final String scheme)
+ {
+ this.host = (host == null) ? ANY_HOST: host.toLowerCase(Locale.ENGLISH);
+ this.port = (port < 0) ? ANY_PORT: port;
+ this.realm = (realm == null) ? ANY_REALM: realm;
+ this.scheme = (scheme == null) ? ANY_SCHEME: scheme.toUpperCase(Locale.ENGLISH);
+ }
+
+ /**
+ * @since 4.2
+ */
+ public AuthScope(final HttpHost host, final String realm, final String schemeName) {
+ this(host.getHostName(), host.getPort(), realm, schemeName);
+ }
+
+ /**
+ * @since 4.2
+ */
+ public AuthScope(final HttpHost host) {
+ this(host, ANY_REALM, ANY_SCHEME);
+ }
+
+ /** Creates a new credentials scope for the given
+ * <tt>host</tt>, <tt>port</tt>, <tt>realm</tt>, and any
+ * authentication scheme.
+ *
+ * @param host the host the credentials apply to. May be set
+ * to <tt>null</tt> if credentials are applicable to
+ * any host.
+ * @param port the port the credentials apply to. May be set
+ * to negative value if credentials are applicable to
+ * any port.
+ * @param realm the realm the credentials apply to. May be set
+ * to <tt>null</tt> if credentials are applicable to
+ * any realm.
+ */
+ public AuthScope(final String host, final int port, final String realm) {
+ this(host, port, realm, ANY_SCHEME);
+ }
+
+ /** Creates a new credentials scope for the given
+ * <tt>host</tt>, <tt>port</tt>, any realm name, and any
+ * authentication scheme.
+ *
+ * @param host the host the credentials apply to. May be set
+ * to <tt>null</tt> if credentials are applicable to
+ * any host.
+ * @param port the port the credentials apply to. May be set
+ * to negative value if credentials are applicable to
+ * any port.
+ */
+ public AuthScope(final String host, final int port) {
+ this(host, port, ANY_REALM, ANY_SCHEME);
+ }
+
+ /**
+ * Creates a copy of the given credentials scope.
+ */
+ public AuthScope(final AuthScope authscope) {
+ super();
+ Args.notNull(authscope, "Scope");
+ this.host = authscope.getHost();
+ this.port = authscope.getPort();
+ this.realm = authscope.getRealm();
+ this.scheme = authscope.getScheme();
+ }
+
+ /**
+ * @return the host
+ */
+ public String getHost() {
+ return this.host;
+ }
+
+ /**
+ * @return the port
+ */
+ public int getPort() {
+ return this.port;
+ }
+
+ /**
+ * @return the realm name
+ */
+ public String getRealm() {
+ return this.realm;
+ }
+
+ /**
+ * @return the scheme type
+ */
+ public String getScheme() {
+ return this.scheme;
+ }
+
+ /**
+ * Tests if the authentication scopes match.
+ *
+ * @return the match factor. Negative value signifies no match.
+ * Non-negative signifies a match. The greater the returned value
+ * the closer the match.
+ */
+ public int match(final AuthScope that) {
+ int factor = 0;
+ if (LangUtils.equals(this.scheme, that.scheme)) {
+ factor += 1;
+ } else {
+ if (this.scheme != ANY_SCHEME && that.scheme != ANY_SCHEME) {
+ return -1;
+ }
+ }
+ if (LangUtils.equals(this.realm, that.realm)) {
+ factor += 2;
+ } else {
+ if (this.realm != ANY_REALM && that.realm != ANY_REALM) {
+ return -1;
+ }
+ }
+ if (this.port == that.port) {
+ factor += 4;
+ } else {
+ if (this.port != ANY_PORT && that.port != ANY_PORT) {
+ return -1;
+ }
+ }
+ if (LangUtils.equals(this.host, that.host)) {
+ factor += 8;
+ } else {
+ if (this.host != ANY_HOST && that.host != ANY_HOST) {
+ return -1;
+ }
+ }
+ return factor;
+ }
+
+ /**
+ * @see java.lang.Object#equals(Object)
+ */
+ @Override
+ public boolean equals(final Object o) {
+ if (o == null) {
+ return false;
+ }
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof AuthScope)) {
+ return super.equals(o);
+ }
+ final AuthScope that = (AuthScope) o;
+ return
+ LangUtils.equals(this.host, that.host)
+ && this.port == that.port
+ && LangUtils.equals(this.realm, that.realm)
+ && LangUtils.equals(this.scheme, that.scheme);
+ }
+
+ /**
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ final StringBuilder buffer = new StringBuilder();
+ if (this.scheme != null) {
+ buffer.append(this.scheme.toUpperCase(Locale.ENGLISH));
+ buffer.append(' ');
+ }
+ if (this.realm != null) {
+ buffer.append('\'');
+ buffer.append(this.realm);
+ buffer.append('\'');
+ } else {
+ buffer.append("<any realm>");
+ }
+ if (this.host != null) {
+ buffer.append('@');
+ buffer.append(this.host);
+ if (this.port >= 0) {
+ buffer.append(':');
+ buffer.append(this.port);
+ }
+ }
+ return buffer.toString();
+ }
+
+ /**
+ * @see java.lang.Object#hashCode()
+ */
+ @Override
+ public int hashCode() {
+ int hash = LangUtils.HASH_SEED;
+ hash = LangUtils.hashCode(hash, this.host);
+ hash = LangUtils.hashCode(hash, this.port);
+ hash = LangUtils.hashCode(hash, this.realm);
+ hash = LangUtils.hashCode(hash, this.scheme);
+ return hash;
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthState.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthState.java
new file mode 100644
index 0000000000..236f934ac5
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthState.java
@@ -0,0 +1,235 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.auth;
+
+import java.util.Queue;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * This class provides detailed information about the state of the authentication process.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class AuthState {
+
+ /** Actual state of authentication protocol */
+ private AuthProtocolState state;
+
+ /** Actual authentication scheme */
+ private AuthScheme authScheme;
+
+ /** Actual authentication scope */
+ private AuthScope authScope;
+
+ /** Credentials selected for authentication */
+ private Credentials credentials;
+
+ /** Available auth options */
+ private Queue<AuthOption> authOptions;
+
+ public AuthState() {
+ super();
+ this.state = AuthProtocolState.UNCHALLENGED;
+ }
+
+ /**
+ * Resets the auth state.
+ *
+ * @since 4.2
+ */
+ public void reset() {
+ this.state = AuthProtocolState.UNCHALLENGED;
+ this.authOptions = null;
+ this.authScheme = null;
+ this.authScope = null;
+ this.credentials = null;
+ }
+
+ /**
+ * @since 4.2
+ */
+ public AuthProtocolState getState() {
+ return this.state;
+ }
+
+ /**
+ * @since 4.2
+ */
+ public void setState(final AuthProtocolState state) {
+ this.state = state != null ? state : AuthProtocolState.UNCHALLENGED;
+ }
+
+ /**
+ * Returns actual {@link AuthScheme}. May be null.
+ */
+ public AuthScheme getAuthScheme() {
+ return this.authScheme;
+ }
+
+ /**
+ * Returns actual {@link Credentials}. May be null.
+ */
+ public Credentials getCredentials() {
+ return this.credentials;
+ }
+
+ /**
+ * Updates the auth state with {@link AuthScheme} and {@link Credentials}.
+ *
+ * @param authScheme auth scheme. May not be null.
+ * @param credentials user crednetials. May not be null.
+ *
+ * @since 4.2
+ */
+ public void update(final AuthScheme authScheme, final Credentials credentials) {
+ Args.notNull(authScheme, "Auth scheme");
+ Args.notNull(credentials, "Credentials");
+ this.authScheme = authScheme;
+ this.credentials = credentials;
+ this.authOptions = null;
+ }
+
+ /**
+ * Returns available {@link AuthOption}s. May be null.
+ *
+ * @since 4.2
+ */
+ public Queue<AuthOption> getAuthOptions() {
+ return this.authOptions;
+ }
+
+ /**
+ * Returns <code>true</code> if {@link AuthOption}s are available, <code>false</code>
+ * otherwise.
+ *
+ * @since 4.2
+ */
+ public boolean hasAuthOptions() {
+ return this.authOptions != null && !this.authOptions.isEmpty();
+ }
+
+ /**
+ * Updates the auth state with a queue of {@link AuthOption}s.
+ *
+ * @param authOptions a queue of auth options. May not be null or empty.
+ *
+ * @since 4.2
+ */
+ public void update(final Queue<AuthOption> authOptions) {
+ Args.notEmpty(authOptions, "Queue of auth options");
+ this.authOptions = authOptions;
+ this.authScheme = null;
+ this.credentials = null;
+ }
+
+ /**
+ * Invalidates the authentication state by resetting its parameters.
+ *
+ * @deprecated (4.2) use {@link #reset()}
+ */
+ @Deprecated
+ public void invalidate() {
+ reset();
+ }
+
+ /**
+ * @deprecated (4.2) do not use
+ */
+ @Deprecated
+ public boolean isValid() {
+ return this.authScheme != null;
+ }
+
+ /**
+ * Assigns the given {@link AuthScheme authentication scheme}.
+ *
+ * @param authScheme the {@link AuthScheme authentication scheme}
+ *
+ * @deprecated (4.2) use {@link #update(AuthScheme, Credentials)}
+ */
+ @Deprecated
+ public void setAuthScheme(final AuthScheme authScheme) {
+ if (authScheme == null) {
+ reset();
+ return;
+ }
+ this.authScheme = authScheme;
+ }
+
+ /**
+ * Sets user {@link Credentials} to be used for authentication
+ *
+ * @param credentials User credentials
+ *
+ * @deprecated (4.2) use {@link #update(AuthScheme, Credentials)}
+ */
+ @Deprecated
+ public void setCredentials(final Credentials credentials) {
+ this.credentials = credentials;
+ }
+
+ /**
+ * Returns actual {@link AuthScope} if available
+ *
+ * @return actual authentication scope if available, <code>null</code otherwise
+ *
+ * @deprecated (4.2) do not use.
+ */
+ @Deprecated
+ public AuthScope getAuthScope() {
+ return this.authScope;
+ }
+
+ /**
+ * Sets actual {@link AuthScope}.
+ *
+ * @param authScope Authentication scope
+ *
+ * @deprecated (4.2) do not use.
+ */
+ @Deprecated
+ public void setAuthScope(final AuthScope authScope) {
+ this.authScope = authScope;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder buffer = new StringBuilder();
+ buffer.append("state:").append(this.state).append(";");
+ if (this.authScheme != null) {
+ buffer.append("auth scheme:").append(this.authScheme.getSchemeName()).append(";");
+ }
+ if (this.credentials != null) {
+ buffer.append("credentials present");
+ }
+ return buffer.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthenticationException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthenticationException.java
new file mode 100644
index 0000000000..e000e4d9b4
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/AuthenticationException.java
@@ -0,0 +1,70 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.auth;
+
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Signals a failure in authentication process
+ *
+ *
+ * @since 4.0
+ */
+@Immutable
+public class AuthenticationException extends ProtocolException {
+
+ private static final long serialVersionUID = -6794031905674764776L;
+
+ /**
+ * Creates a new AuthenticationException with a <tt>null</tt> detail message.
+ */
+ public AuthenticationException() {
+ super();
+ }
+
+ /**
+ * Creates a new AuthenticationException with the specified message.
+ *
+ * @param message the exception detail message
+ */
+ public AuthenticationException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new AuthenticationException with the specified detail message and cause.
+ *
+ * @param message the exception detail message
+ * @param cause the <tt>Throwable</tt> that caused this exception, or <tt>null</tt>
+ * if the cause is unavailable, unknown, or not a <tt>Throwable</tt>
+ */
+ public AuthenticationException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/BasicUserPrincipal.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/BasicUserPrincipal.java
new file mode 100644
index 0000000000..e3bf323bba
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/BasicUserPrincipal.java
@@ -0,0 +1,89 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.auth;
+
+import java.io.Serializable;
+import java.security.Principal;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.LangUtils;
+
+/**
+ * Basic user principal used for HTTP authentication
+ *
+ * @since 4.0
+ */
+@Immutable
+public final class BasicUserPrincipal implements Principal, Serializable {
+
+ private static final long serialVersionUID = -2266305184969850467L;
+
+ private final String username;
+
+ public BasicUserPrincipal(final String username) {
+ super();
+ Args.notNull(username, "User name");
+ this.username = username;
+ }
+
+ public String getName() {
+ return this.username;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = LangUtils.HASH_SEED;
+ hash = LangUtils.hashCode(hash, this.username);
+ return hash;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o instanceof BasicUserPrincipal) {
+ final BasicUserPrincipal that = (BasicUserPrincipal) o;
+ if (LangUtils.equals(this.username, that.username)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder buffer = new StringBuilder();
+ buffer.append("[principal: ");
+ buffer.append(this.username);
+ buffer.append("]");
+ return buffer.toString();
+ }
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/ChallengeState.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/ChallengeState.java
new file mode 100644
index 0000000000..8ff59b7bb2
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/ChallengeState.java
@@ -0,0 +1,38 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.auth;
+
+/**
+ * Challenge mode (TARGET or PROXY)
+ *
+ * @since 4.2
+ */
+public enum ChallengeState {
+
+ TARGET, PROXY
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/ContextAwareAuthScheme.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/ContextAwareAuthScheme.java
new file mode 100644
index 0000000000..1146795738
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/ContextAwareAuthScheme.java
@@ -0,0 +1,62 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.auth;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * This interface represents an extended authentication scheme
+ * that requires access to {@link HttpContext} in order to
+ * generate an authorization string.
+ *
+ * TODO: Fix AuthScheme interface in the next major version
+ *
+ * @since 4.1
+ */
+
+public interface ContextAwareAuthScheme extends AuthScheme {
+
+ /**
+ * Produces an authorization string for the given set of
+ * {@link Credentials}.
+ *
+ * @param credentials The set of credentials to be used for athentication
+ * @param request The request being authenticated
+ * @param context HTTP context
+ * @throws AuthenticationException if authorization string cannot
+ * be generated due to an authentication failure
+ *
+ * @return the authorization string
+ */
+ Header authenticate(
+ Credentials credentials,
+ HttpRequest request,
+ HttpContext context) throws AuthenticationException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/Credentials.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/Credentials.java
new file mode 100644
index 0000000000..2c40ee10eb
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/Credentials.java
@@ -0,0 +1,44 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.auth;
+
+import java.security.Principal;
+
+/**
+ * This interface represents a set of credentials consisting of a security
+ * principal and a secret (password) that can be used to establish user
+ * identity
+ *
+ * @since 4.0
+ */
+public interface Credentials {
+
+ Principal getUserPrincipal();
+
+ String getPassword();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/InvalidCredentialsException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/InvalidCredentialsException.java
new file mode 100644
index 0000000000..47d6e6d912
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/InvalidCredentialsException.java
@@ -0,0 +1,69 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.auth;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Authentication credentials required to respond to a authentication
+ * challenge are invalid
+ *
+ *
+ * @since 4.0
+ */
+@Immutable
+public class InvalidCredentialsException extends AuthenticationException {
+
+ private static final long serialVersionUID = -4834003835215460648L;
+
+ /**
+ * Creates a new InvalidCredentialsException with a <tt>null</tt> detail message.
+ */
+ public InvalidCredentialsException() {
+ super();
+ }
+
+ /**
+ * Creates a new InvalidCredentialsException with the specified message.
+ *
+ * @param message the exception detail message
+ */
+ public InvalidCredentialsException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new InvalidCredentialsException with the specified detail message and cause.
+ *
+ * @param message the exception detail message
+ * @param cause the <tt>Throwable</tt> that caused this exception, or <tt>null</tt>
+ * if the cause is unavailable, unknown, or not a <tt>Throwable</tt>
+ */
+ public InvalidCredentialsException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/MalformedChallengeException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/MalformedChallengeException.java
new file mode 100644
index 0000000000..309bbfb390
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/MalformedChallengeException.java
@@ -0,0 +1,70 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.auth;
+
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Signals that authentication challenge is in some way invalid or
+ * illegal in the given context
+ *
+ *
+ * @since 4.0
+ */
+@Immutable
+public class MalformedChallengeException extends ProtocolException {
+
+ private static final long serialVersionUID = 814586927989932284L;
+
+ /**
+ * Creates a new MalformedChallengeException with a <tt>null</tt> detail message.
+ */
+ public MalformedChallengeException() {
+ super();
+ }
+
+ /**
+ * Creates a new MalformedChallengeException with the specified message.
+ *
+ * @param message the exception detail message
+ */
+ public MalformedChallengeException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new MalformedChallengeException with the specified detail message and cause.
+ *
+ * @param message the exception detail message
+ * @param cause the <tt>Throwable</tt> that caused this exception, or <tt>null</tt>
+ * if the cause is unavailable, unknown, or not a <tt>Throwable</tt>
+ */
+ public MalformedChallengeException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/NTCredentials.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/NTCredentials.java
new file mode 100644
index 0000000000..43693ffa24
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/NTCredentials.java
@@ -0,0 +1,177 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.auth;
+
+import java.io.Serializable;
+import java.security.Principal;
+import java.util.Locale;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.LangUtils;
+
+/**
+ * {@link Credentials} implementation for Microsoft Windows platforms that includes
+ * Windows specific attributes such as name of the domain the user belongs to.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class NTCredentials implements Credentials, Serializable {
+
+ private static final long serialVersionUID = -7385699315228907265L;
+
+ /** The user principal */
+ private final NTUserPrincipal principal;
+
+ /** Password */
+ private final String password;
+
+ /** The host the authentication request is originating from. */
+ private final String workstation;
+
+ /**
+ * The constructor with the fully qualified username and password combined
+ * string argument.
+ *
+ * @param usernamePassword the domain/username:password formed string
+ */
+ public NTCredentials(final String usernamePassword) {
+ super();
+ Args.notNull(usernamePassword, "Username:password string");
+ final String username;
+ final int atColon = usernamePassword.indexOf(':');
+ if (atColon >= 0) {
+ username = usernamePassword.substring(0, atColon);
+ this.password = usernamePassword.substring(atColon + 1);
+ } else {
+ username = usernamePassword;
+ this.password = null;
+ }
+ final int atSlash = username.indexOf('/');
+ if (atSlash >= 0) {
+ this.principal = new NTUserPrincipal(
+ username.substring(0, atSlash).toUpperCase(Locale.ENGLISH),
+ username.substring(atSlash + 1));
+ } else {
+ this.principal = new NTUserPrincipal(
+ null,
+ username.substring(atSlash + 1));
+ }
+ this.workstation = null;
+ }
+
+ /**
+ * Constructor.
+ * @param userName The user name. This should not include the domain to authenticate with.
+ * For example: "user" is correct whereas "DOMAIN\\user" is not.
+ * @param password The password.
+ * @param workstation The workstation the authentication request is originating from.
+ * Essentially, the computer name for this machine.
+ * @param domain The domain to authenticate within.
+ */
+ public NTCredentials(
+ final String userName,
+ final String password,
+ final String workstation,
+ final String domain) {
+ super();
+ Args.notNull(userName, "User name");
+ this.principal = new NTUserPrincipal(domain, userName);
+ this.password = password;
+ if (workstation != null) {
+ this.workstation = workstation.toUpperCase(Locale.ENGLISH);
+ } else {
+ this.workstation = null;
+ }
+ }
+
+ public Principal getUserPrincipal() {
+ return this.principal;
+ }
+
+ public String getUserName() {
+ return this.principal.getUsername();
+ }
+
+ public String getPassword() {
+ return this.password;
+ }
+
+ /**
+ * Retrieves the name to authenticate with.
+ *
+ * @return String the domain these credentials are intended to authenticate with.
+ */
+ public String getDomain() {
+ return this.principal.getDomain();
+ }
+
+ /**
+ * Retrieves the workstation name of the computer originating the request.
+ *
+ * @return String the workstation the user is logged into.
+ */
+ public String getWorkstation() {
+ return this.workstation;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = LangUtils.HASH_SEED;
+ hash = LangUtils.hashCode(hash, this.principal);
+ hash = LangUtils.hashCode(hash, this.workstation);
+ return hash;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o instanceof NTCredentials) {
+ final NTCredentials that = (NTCredentials) o;
+ if (LangUtils.equals(this.principal, that.principal)
+ && LangUtils.equals(this.workstation, that.workstation)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder buffer = new StringBuilder();
+ buffer.append("[principal: ");
+ buffer.append(this.principal);
+ buffer.append("][workstation: ");
+ buffer.append(this.workstation);
+ buffer.append("]");
+ return buffer.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/NTUserPrincipal.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/NTUserPrincipal.java
new file mode 100644
index 0000000000..13789ef4f7
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/NTUserPrincipal.java
@@ -0,0 +1,113 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.auth;
+
+import java.io.Serializable;
+import java.security.Principal;
+import java.util.Locale;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.LangUtils;
+
+/**
+ * Microsoft Windows specific user principal implementation.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class NTUserPrincipal implements Principal, Serializable {
+
+ private static final long serialVersionUID = -6870169797924406894L;
+
+ private final String username;
+ private final String domain;
+ private final String ntname;
+
+ public NTUserPrincipal(
+ final String domain,
+ final String username) {
+ super();
+ Args.notNull(username, "User name");
+ this.username = username;
+ if (domain != null) {
+ this.domain = domain.toUpperCase(Locale.ENGLISH);
+ } else {
+ this.domain = null;
+ }
+ if (this.domain != null && this.domain.length() > 0) {
+ final StringBuilder buffer = new StringBuilder();
+ buffer.append(this.domain);
+ buffer.append('\\');
+ buffer.append(this.username);
+ this.ntname = buffer.toString();
+ } else {
+ this.ntname = this.username;
+ }
+ }
+
+ public String getName() {
+ return this.ntname;
+ }
+
+ public String getDomain() {
+ return this.domain;
+ }
+
+ public String getUsername() {
+ return this.username;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = LangUtils.HASH_SEED;
+ hash = LangUtils.hashCode(hash, this.username);
+ hash = LangUtils.hashCode(hash, this.domain);
+ return hash;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o instanceof NTUserPrincipal) {
+ final NTUserPrincipal that = (NTUserPrincipal) o;
+ if (LangUtils.equals(this.username, that.username)
+ && LangUtils.equals(this.domain, that.domain)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return this.ntname;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/UsernamePasswordCredentials.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/UsernamePasswordCredentials.java
new file mode 100644
index 0000000000..5cdeeb61ea
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/UsernamePasswordCredentials.java
@@ -0,0 +1,120 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.auth;
+
+import java.io.Serializable;
+import java.security.Principal;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.LangUtils;
+
+/**
+ * Simple {@link Credentials} implementation based on a user name / password
+ * pair.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class UsernamePasswordCredentials implements Credentials, Serializable {
+
+ private static final long serialVersionUID = 243343858802739403L;
+
+ private final BasicUserPrincipal principal;
+ private final String password;
+
+ /**
+ * The constructor with the username and password combined string argument.
+ *
+ * @param usernamePassword the username:password formed string
+ * @see #toString
+ */
+ public UsernamePasswordCredentials(final String usernamePassword) {
+ super();
+ Args.notNull(usernamePassword, "Username:password string");
+ final int atColon = usernamePassword.indexOf(':');
+ if (atColon >= 0) {
+ this.principal = new BasicUserPrincipal(usernamePassword.substring(0, atColon));
+ this.password = usernamePassword.substring(atColon + 1);
+ } else {
+ this.principal = new BasicUserPrincipal(usernamePassword);
+ this.password = null;
+ }
+ }
+
+
+ /**
+ * The constructor with the username and password arguments.
+ *
+ * @param userName the user name
+ * @param password the password
+ */
+ public UsernamePasswordCredentials(final String userName, final String password) {
+ super();
+ Args.notNull(userName, "Username");
+ this.principal = new BasicUserPrincipal(userName);
+ this.password = password;
+ }
+
+ public Principal getUserPrincipal() {
+ return this.principal;
+ }
+
+ public String getUserName() {
+ return this.principal.getName();
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ @Override
+ public int hashCode() {
+ return this.principal.hashCode();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o instanceof UsernamePasswordCredentials) {
+ final UsernamePasswordCredentials that = (UsernamePasswordCredentials) o;
+ if (LangUtils.equals(this.principal, that.principal)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return this.principal.toString();
+ }
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/package-info.java
new file mode 100644
index 0000000000..93992fcbfe
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Client HTTP authentication APIs.
+ */
+package ch.boye.httpclientandroidlib.auth;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/params/AuthPNames.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/params/AuthPNames.java
new file mode 100644
index 0000000000..ee64850ace
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/params/AuthPNames.java
@@ -0,0 +1,74 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.auth.params;
+
+/**
+ * Parameter names for HTTP authentication classes.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.client.config.RequestConfig}
+ * and constructor parameters of
+ * {@link ch.boye.httpclientandroidlib.auth.AuthSchemeProvider}s.
+*/
+@Deprecated
+public interface AuthPNames {
+
+ /**
+ * Defines the charset to be used when encoding
+ * {@link ch.boye.httpclientandroidlib.auth.Credentials}.
+ * <p>
+ * This parameter expects a value of type {@link String}.
+ */
+ public static final String CREDENTIAL_CHARSET = "http.auth.credential-charset";
+
+ /**
+ * Defines the order of preference for supported
+ * {@link ch.boye.httpclientandroidlib.auth.AuthScheme}s when authenticating with
+ * the target host.
+ * <p>
+ * This parameter expects a value of type {@link java.util.Collection}. The
+ * collection is expected to contain {@link String} instances representing
+ * a name of an authentication scheme as returned by
+ * {@link ch.boye.httpclientandroidlib.auth.AuthScheme#getSchemeName()}.
+ */
+ public static final String TARGET_AUTH_PREF = "http.auth.target-scheme-pref";
+
+ /**
+ * Defines the order of preference for supported
+ * {@link ch.boye.httpclientandroidlib.auth.AuthScheme}s when authenticating with the
+ * proxy host.
+ * <p>
+ * This parameter expects a value of type {@link java.util.Collection}. The
+ * collection is expected to contain {@link String} instances representing
+ * a name of an authentication scheme as returned by
+ * {@link ch.boye.httpclientandroidlib.auth.AuthScheme#getSchemeName()}.
+ */
+ public static final String PROXY_AUTH_PREF = "http.auth.proxy-scheme-pref";
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/params/AuthParamBean.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/params/AuthParamBean.java
new file mode 100644
index 0000000000..e35d7ae28a
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/params/AuthParamBean.java
@@ -0,0 +1,55 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.auth.params;
+
+import ch.boye.httpclientandroidlib.params.HttpAbstractParamBean;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+/**
+ * This is a Java Bean class that can be used to wrap an instance of
+ * {@link HttpParams} and manipulate HTTP authentication parameters
+ * using Java Beans conventions.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.client.config.RequestConfig}
+ * and constructor parameters of
+ * {@link ch.boye.httpclientandroidlib.auth.AuthSchemeProvider}s.
+ */
+@Deprecated
+public class AuthParamBean extends HttpAbstractParamBean {
+
+ public AuthParamBean (final HttpParams params) {
+ super(params);
+ }
+
+ public void setCredentialCharset (final String charset) {
+ AuthParams.setCredentialCharset(params, charset);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/params/AuthParams.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/params/AuthParams.java
new file mode 100644
index 0000000000..cc7a3d32ca
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/params/AuthParams.java
@@ -0,0 +1,82 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.auth.params;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * An adaptor for manipulating HTTP authentication parameters
+ * in {@link HttpParams}.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.client.config.RequestConfig}
+ * and constructor parameters of
+ * {@link ch.boye.httpclientandroidlib.auth.AuthSchemeProvider}s.
+ */
+@Immutable
+@Deprecated
+public final class AuthParams {
+
+ private AuthParams() {
+ super();
+ }
+
+ /**
+ * Obtains the charset for encoding
+ * {@link ch.boye.httpclientandroidlib.auth.Credentials}.If not configured,
+ * {@link HTTP#DEFAULT_PROTOCOL_CHARSET}is used instead.
+ *
+ * @return The charset
+ */
+ public static String getCredentialCharset(final HttpParams params) {
+ Args.notNull(params, "HTTP parameters");
+ String charset = (String) params.getParameter
+ (AuthPNames.CREDENTIAL_CHARSET);
+ if (charset == null) {
+ charset = HTTP.DEF_PROTOCOL_CHARSET.name();
+ }
+ return charset;
+ }
+
+
+ /**
+ * Sets the charset to be used when encoding
+ * {@link ch.boye.httpclientandroidlib.auth.Credentials}.
+ *
+ * @param charset The charset
+ */
+ public static void setCredentialCharset(final HttpParams params, final String charset) {
+ Args.notNull(params, "HTTP parameters");
+ params.setParameter(AuthPNames.CREDENTIAL_CHARSET, charset);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/params/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/params/package-info.java
new file mode 100644
index 0000000000..7b406b2592
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/auth/params/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Deprecated.
+ * @deprecated (4.3).
+ */
+package ch.boye.httpclientandroidlib.auth.params;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/AuthCache.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/AuthCache.java
new file mode 100644
index 0000000000..3fd0a473d8
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/AuthCache.java
@@ -0,0 +1,49 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.auth.AuthScheme;
+
+/**
+ * Abstract {@link AuthScheme} cache. Initialized {@link AuthScheme} objects
+ * from this cache can be used to preemptively authenticate against known
+ * hosts.
+ *
+ * @since 4.1
+ */
+public interface AuthCache {
+
+ void put(HttpHost host, AuthScheme authScheme);
+
+ AuthScheme get(HttpHost host);
+
+ void remove(HttpHost host);
+
+ void clear();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/AuthenticationHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/AuthenticationHandler.java
new file mode 100644
index 0000000000..24b30b54fe
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/AuthenticationHandler.java
@@ -0,0 +1,101 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client;
+
+import java.util.Map;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.auth.AuthScheme;
+import ch.boye.httpclientandroidlib.auth.AuthenticationException;
+import ch.boye.httpclientandroidlib.auth.MalformedChallengeException;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+/**
+ * A handler for determining if an HTTP response represents an authentication
+ * challenge that was sent back to the client as a result of authentication
+ * failure.
+ * <p>
+ * Implementations of this interface must be thread-safe. Access to shared
+ * data must be synchronized as methods of this interface may be executed
+ * from multiple threads.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) use {@link AuthenticationStrategy}
+ */
+@Deprecated
+public interface AuthenticationHandler {
+
+ /**
+ * Determines if the given HTTP response response represents
+ * an authentication challenge that was sent back as a result
+ * of authentication failure
+ * @param response HTTP response.
+ * @param context HTTP context.
+ * @return <code>true</code> if user authentication is required,
+ * <code>false</code> otherwise.
+ */
+ boolean isAuthenticationRequested(
+ HttpResponse response,
+ HttpContext context);
+
+ /**
+ * Extracts from the given HTTP response a collection of authentication
+ * challenges, each of which represents an authentication scheme supported
+ * by the authentication host.
+ *
+ * @param response HTTP response.
+ * @param context HTTP context.
+ * @return a collection of challenges keyed by names of corresponding
+ * authentication schemes.
+ * @throws MalformedChallengeException if one of the authentication
+ * challenges is not valid or malformed.
+ */
+ Map<String, Header> getChallenges(
+ HttpResponse response,
+ HttpContext context) throws MalformedChallengeException;
+
+ /**
+ * Selects one authentication challenge out of all available and
+ * creates and generates {@link AuthScheme} instance capable of
+ * processing that challenge.
+ * @param challenges collection of challenges.
+ * @param response HTTP response.
+ * @param context HTTP context.
+ * @return authentication scheme to use for authentication.
+ * @throws AuthenticationException if an authentication scheme
+ * could not be selected.
+ */
+ AuthScheme selectScheme(
+ Map<String, Header> challenges,
+ HttpResponse response,
+ HttpContext context) throws AuthenticationException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/AuthenticationStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/AuthenticationStrategy.java
new file mode 100644
index 0000000000..2aa1fb536f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/AuthenticationStrategy.java
@@ -0,0 +1,130 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client;
+
+import java.util.Map;
+import java.util.Queue;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.auth.AuthOption;
+import ch.boye.httpclientandroidlib.auth.AuthScheme;
+import ch.boye.httpclientandroidlib.auth.MalformedChallengeException;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+/**
+ * A handler for determining if an HTTP response represents an authentication challenge that was
+ * sent back to the client as a result of authentication failure.
+ * <p>
+ * Implementations of this interface must be thread-safe. Access to shared data must be
+ * synchronized as methods of this interface may be executed from multiple threads.
+ *
+ * @since 4.2
+ */
+public interface AuthenticationStrategy {
+
+ /**
+ * Determines if the given HTTP response response represents
+ * an authentication challenge that was sent back as a result
+ * of authentication failure.
+ *
+ * @param authhost authentication host.
+ * @param response HTTP response.
+ * @param context HTTP context.
+ * @return <code>true</code> if user authentication is required,
+ * <code>false</code> otherwise.
+ */
+ boolean isAuthenticationRequested(
+ HttpHost authhost,
+ HttpResponse response,
+ HttpContext context);
+
+ /**
+ * Extracts from the given HTTP response a collection of authentication
+ * challenges, each of which represents an authentication scheme supported
+ * by the authentication host.
+ *
+ * @param authhost authentication host.
+ * @param response HTTP response.
+ * @param context HTTP context.
+ * @return a collection of challenges keyed by names of corresponding
+ * authentication schemes.
+ * @throws MalformedChallengeException if one of the authentication
+ * challenges is not valid or malformed.
+ */
+ Map<String, Header> getChallenges(
+ HttpHost authhost,
+ HttpResponse response,
+ HttpContext context) throws MalformedChallengeException;
+
+ /**
+ * Selects one authentication challenge out of all available and
+ * creates and generates {@link AuthOption} instance capable of
+ * processing that challenge.
+ *
+ * @param challenges collection of challenges.
+ * @param authhost authentication host.
+ * @param response HTTP response.
+ * @param context HTTP context.
+ * @return authentication auth schemes that can be used for authentication. Can be empty.
+ * @throws MalformedChallengeException if one of the authentication
+ * challenges is not valid or malformed.
+ */
+ Queue<AuthOption> select(
+ Map<String, Header> challenges,
+ HttpHost authhost,
+ HttpResponse response,
+ HttpContext context) throws MalformedChallengeException;
+
+ /**
+ * Callback invoked in case of successful authentication.
+ *
+ * @param authhost authentication host.
+ * @param authScheme authentication scheme used.
+ * @param context HTTP context.
+ */
+ void authSucceeded(
+ HttpHost authhost,
+ AuthScheme authScheme,
+ HttpContext context);
+
+ /**
+ * Callback invoked in case of unsuccessful authentication.
+ *
+ * @param authhost authentication host.
+ * @param authScheme authentication scheme used.
+ * @param context HTTP context.
+ */
+ void authFailed(
+ HttpHost authhost,
+ AuthScheme authScheme,
+ HttpContext context);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/BackoffManager.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/BackoffManager.java
new file mode 100644
index 0000000000..afce4f2779
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/BackoffManager.java
@@ -0,0 +1,54 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client;
+
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+
+/**
+ * Represents a controller that dynamically adjusts the size
+ * of an available connection pool based on feedback from
+ * using the connections.
+ *
+ * @since 4.2
+ *
+ */
+public interface BackoffManager {
+
+ /**
+ * Called when we have decided that the result of
+ * using a connection should be interpreted as a
+ * backoff signal.
+ */
+ public void backOff(HttpRoute route);
+
+ /**
+ * Called when we have determined that the result of
+ * using a connection has succeeded and that we may
+ * probe for more connections.
+ */
+ public void probe(HttpRoute route);
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/CircularRedirectException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/CircularRedirectException.java
new file mode 100644
index 0000000000..22d3018858
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/CircularRedirectException.java
@@ -0,0 +1,68 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Signals a circular redirect
+ *
+ *
+ * @since 4.0
+ */
+@Immutable
+public class CircularRedirectException extends RedirectException {
+
+ private static final long serialVersionUID = 6830063487001091803L;
+
+ /**
+ * Creates a new CircularRedirectException with a <tt>null</tt> detail message.
+ */
+ public CircularRedirectException() {
+ super();
+ }
+
+ /**
+ * Creates a new CircularRedirectException with the specified detail message.
+ *
+ * @param message The exception detail message
+ */
+ public CircularRedirectException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new CircularRedirectException with the specified detail message and cause.
+ *
+ * @param message the exception detail message
+ * @param cause the <tt>Throwable</tt> that caused this exception, or <tt>null</tt>
+ * if the cause is unavailable, unknown, or not a <tt>Throwable</tt>
+ */
+ public CircularRedirectException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/ClientProtocolException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/ClientProtocolException.java
new file mode 100644
index 0000000000..3b807ae714
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/ClientProtocolException.java
@@ -0,0 +1,61 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Signals an error in the HTTP protocol.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class ClientProtocolException extends IOException {
+
+ private static final long serialVersionUID = -5596590843227115865L;
+
+ public ClientProtocolException() {
+ super();
+ }
+
+ public ClientProtocolException(final String s) {
+ super(s);
+ }
+
+ public ClientProtocolException(final Throwable cause) {
+ initCause(cause);
+ }
+
+ public ClientProtocolException(final String message, final Throwable cause) {
+ super(message);
+ initCause(cause);
+ }
+
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/ConnectionBackoffStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/ConnectionBackoffStrategy.java
new file mode 100644
index 0000000000..1f30329cf4
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/ConnectionBackoffStrategy.java
@@ -0,0 +1,64 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+
+/**
+ * When managing a dynamic number of connections for a given route, this
+ * strategy assesses whether a given request execution outcome should
+ * result in a backoff signal or not, based on either examining the
+ * <code>Throwable</code> that resulted or by examining the resulting
+ * response (e.g. for its status code).
+ *
+ * @since 4.2
+ *
+ */
+public interface ConnectionBackoffStrategy {
+
+ /**
+ * Determines whether seeing the given <code>Throwable</code> as
+ * a result of request execution should result in a backoff
+ * signal.
+ * @param t the <code>Throwable</code> that happened
+ * @return <code>true</code> if a backoff signal should be
+ * given
+ */
+ boolean shouldBackoff(Throwable t);
+
+ /**
+ * Determines whether receiving the given {@link HttpResponse} as
+ * a result of request execution should result in a backoff
+ * signal. Implementations MUST restrict themselves to examining
+ * the response header and MUST NOT consume any of the response
+ * body, if any.
+ * @param resp the <code>HttpResponse</code> that was received
+ * @return <code>true</code> if a backoff signal should be
+ * given
+ */
+ boolean shouldBackoff(HttpResponse resp);
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/CookieStore.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/CookieStore.java
new file mode 100644
index 0000000000..683109ac40
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/CookieStore.java
@@ -0,0 +1,71 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client;
+
+import java.util.Date;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+
+/**
+ * This interface represents an abstract store for {@link Cookie}
+ * objects.
+ *
+ * @since 4.0
+ */
+public interface CookieStore {
+
+ /**
+ * Adds an {@link Cookie}, replacing any existing equivalent cookies.
+ * If the given cookie has already expired it will not be added, but existing
+ * values will still be removed.
+ *
+ * @param cookie the {@link Cookie cookie} to be added
+ */
+ void addCookie(Cookie cookie);
+
+ /**
+ * Returns all cookies contained in this store.
+ *
+ * @return all cookies
+ */
+ List<Cookie> getCookies();
+
+ /**
+ * Removes all of {@link Cookie}s in this store that have expired by
+ * the specified {@link java.util.Date}.
+ *
+ * @return true if any cookies were purged.
+ */
+ boolean clearExpired(Date date);
+
+ /**
+ * Clears all cookies.
+ */
+ void clear();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/CredentialsProvider.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/CredentialsProvider.java
new file mode 100644
index 0000000000..dc790399ce
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/CredentialsProvider.java
@@ -0,0 +1,71 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client;
+
+import ch.boye.httpclientandroidlib.auth.AuthScope;
+import ch.boye.httpclientandroidlib.auth.Credentials;
+
+/**
+ * Abstract credentials provider that maintains a collection of user
+ * credentials.
+ * <p>
+ * Implementations of this interface must be thread-safe. Access to shared
+ * data must be synchronized as methods of this interface may be executed
+ * from multiple threads.
+ *
+ * @since 4.0
+ */
+public interface CredentialsProvider {
+
+ /**
+ * Sets the {@link Credentials credentials} for the given authentication
+ * scope. Any previous credentials for the given scope will be overwritten.
+ *
+ * @param authscope the {@link AuthScope authentication scope}
+ * @param credentials the authentication {@link Credentials credentials}
+ * for the given scope.
+ *
+ * @see #getCredentials(AuthScope)
+ */
+ void setCredentials(AuthScope authscope, Credentials credentials);
+
+ /**
+ * Get the {@link Credentials credentials} for the given authentication scope.
+ *
+ * @param authscope the {@link AuthScope authentication scope}
+ * @return the credentials
+ *
+ * @see #setCredentials(AuthScope, Credentials)
+ */
+ Credentials getCredentials(AuthScope authscope);
+
+ /**
+ * Clears all credentials.
+ */
+ void clear();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/HttpClient.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/HttpClient.java
new file mode 100644
index 0000000000..4cc87fcfe4
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/HttpClient.java
@@ -0,0 +1,258 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionManager;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+import java.io.IOException;
+
+/**
+ * This interface represents only the most basic contract for HTTP request
+ * execution. It imposes no restrictions or particular details on the request
+ * execution process and leaves the specifics of state management,
+ * authentication and redirect handling up to individual implementations.
+ *
+ * @since 4.0
+ */
+@SuppressWarnings("deprecation")
+public interface HttpClient {
+
+
+ /**
+ * Obtains the parameters for this client.
+ * These parameters will become defaults for all requests being
+ * executed with this client, and for the parameters of
+ * dependent objects in this client.
+ *
+ * @return the default parameters
+ *
+ * @deprecated (4.3) use
+ * {@link ch.boye.httpclientandroidlib.client.config.RequestConfig}.
+ */
+ @Deprecated
+ HttpParams getParams();
+
+ /**
+ * Obtains the connection manager used by this client.
+ *
+ * @return the connection manager
+ *
+ * @deprecated (4.3) use
+ * {@link ch.boye.httpclientandroidlib.impl.client.HttpClientBuilder}.
+ */
+ @Deprecated
+ ClientConnectionManager getConnectionManager();
+
+ /**
+ * Executes HTTP request using the default context.
+ *
+ * @param request the request to execute
+ *
+ * @return the response to the request. This is always a final response,
+ * never an intermediate response with an 1xx status code.
+ * Whether redirects or authentication challenges will be returned
+ * or handled automatically depends on the implementation and
+ * configuration of this client.
+ * @throws IOException in case of a problem or the connection was aborted
+ * @throws ClientProtocolException in case of an http protocol error
+ */
+ HttpResponse execute(HttpUriRequest request)
+ throws IOException, ClientProtocolException;
+
+ /**
+ * Executes HTTP request using the given context.
+ *
+ * @param request the request to execute
+ * @param context the context to use for the execution, or
+ * <code>null</code> to use the default context
+ *
+ * @return the response to the request. This is always a final response,
+ * never an intermediate response with an 1xx status code.
+ * Whether redirects or authentication challenges will be returned
+ * or handled automatically depends on the implementation and
+ * configuration of this client.
+ * @throws IOException in case of a problem or the connection was aborted
+ * @throws ClientProtocolException in case of an http protocol error
+ */
+ HttpResponse execute(HttpUriRequest request, HttpContext context)
+ throws IOException, ClientProtocolException;
+
+ /**
+ * Executes HTTP request using the default context.
+ *
+ * @param target the target host for the request.
+ * Implementations may accept <code>null</code>
+ * if they can still determine a route, for example
+ * to a default target or by inspecting the request.
+ * @param request the request to execute
+ *
+ * @return the response to the request. This is always a final response,
+ * never an intermediate response with an 1xx status code.
+ * Whether redirects or authentication challenges will be returned
+ * or handled automatically depends on the implementation and
+ * configuration of this client.
+ * @throws IOException in case of a problem or the connection was aborted
+ * @throws ClientProtocolException in case of an http protocol error
+ */
+ HttpResponse execute(HttpHost target, HttpRequest request)
+ throws IOException, ClientProtocolException;
+
+ /**
+ * Executes HTTP request using the given context.
+ *
+ * @param target the target host for the request.
+ * Implementations may accept <code>null</code>
+ * if they can still determine a route, for example
+ * to a default target or by inspecting the request.
+ * @param request the request to execute
+ * @param context the context to use for the execution, or
+ * <code>null</code> to use the default context
+ *
+ * @return the response to the request. This is always a final response,
+ * never an intermediate response with an 1xx status code.
+ * Whether redirects or authentication challenges will be returned
+ * or handled automatically depends on the implementation and
+ * configuration of this client.
+ * @throws IOException in case of a problem or the connection was aborted
+ * @throws ClientProtocolException in case of an http protocol error
+ */
+ HttpResponse execute(HttpHost target, HttpRequest request,
+ HttpContext context)
+ throws IOException, ClientProtocolException;
+
+ /**
+ * Executes HTTP request using the default context and processes the
+ * response using the given response handler.
+ * <p/>
+ * Implementing classes are required to ensure that the content entity
+ * associated with the response is fully consumed and the underlying
+ * connection is released back to the connection manager automatically
+ * in all cases relieving individual {@link ResponseHandler}s from
+ * having to manage resource deallocation internally.
+ *
+ * @param request the request to execute
+ * @param responseHandler the response handler
+ *
+ * @return the response object as generated by the response handler.
+ * @throws IOException in case of a problem or the connection was aborted
+ * @throws ClientProtocolException in case of an http protocol error
+ */
+ <T> T execute(
+ HttpUriRequest request,
+ ResponseHandler<? extends T> responseHandler)
+ throws IOException, ClientProtocolException;
+
+ /**
+ * Executes HTTP request using the given context and processes the
+ * response using the given response handler.
+ * <p/>
+ * Implementing classes are required to ensure that the content entity
+ * associated with the response is fully consumed and the underlying
+ * connection is released back to the connection manager automatically
+ * in all cases relieving individual {@link ResponseHandler}s from
+ * having to manage resource deallocation internally.
+ *
+ * @param request the request to execute
+ * @param responseHandler the response handler
+ * @param context the context to use for the execution, or
+ * <code>null</code> to use the default context
+ *
+ * @return the response object as generated by the response handler.
+ * @throws IOException in case of a problem or the connection was aborted
+ * @throws ClientProtocolException in case of an http protocol error
+ */
+ <T> T execute(
+ HttpUriRequest request,
+ ResponseHandler<? extends T> responseHandler,
+ HttpContext context)
+ throws IOException, ClientProtocolException;
+
+ /**
+ * Executes HTTP request to the target using the default context and
+ * processes the response using the given response handler.
+ * <p/>
+ * Implementing classes are required to ensure that the content entity
+ * associated with the response is fully consumed and the underlying
+ * connection is released back to the connection manager automatically
+ * in all cases relieving individual {@link ResponseHandler}s from
+ * having to manage resource deallocation internally.
+ *
+ * @param target the target host for the request.
+ * Implementations may accept <code>null</code>
+ * if they can still determine a route, for example
+ * to a default target or by inspecting the request.
+ * @param request the request to execute
+ * @param responseHandler the response handler
+ *
+ * @return the response object as generated by the response handler.
+ * @throws IOException in case of a problem or the connection was aborted
+ * @throws ClientProtocolException in case of an http protocol error
+ */
+ <T> T execute(
+ HttpHost target,
+ HttpRequest request,
+ ResponseHandler<? extends T> responseHandler)
+ throws IOException, ClientProtocolException;
+
+ /**
+ * Executes HTTP request to the target using the given context and
+ * processes the response using the given response handler.
+ * <p/>
+ * Implementing classes are required to ensure that the content entity
+ * associated with the response is fully consumed and the underlying
+ * connection is released back to the connection manager automatically
+ * in all cases relieving individual {@link ResponseHandler}s from
+ * having to manage resource deallocation internally.
+ *
+ * @param target the target host for the request.
+ * Implementations may accept <code>null</code>
+ * if they can still determine a route, for example
+ * to a default target or by inspecting the request.
+ * @param request the request to execute
+ * @param responseHandler the response handler
+ * @param context the context to use for the execution, or
+ * <code>null</code> to use the default context
+ *
+ * @return the response object as generated by the response handler.
+ * @throws IOException in case of a problem or the connection was aborted
+ * @throws ClientProtocolException in case of an http protocol error
+ */
+ <T> T execute(
+ HttpHost target,
+ HttpRequest request,
+ ResponseHandler<? extends T> responseHandler,
+ HttpContext context)
+ throws IOException, ClientProtocolException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/HttpRequestRetryHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/HttpRequestRetryHandler.java
new file mode 100644
index 0000000000..8bbb11c498
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/HttpRequestRetryHandler.java
@@ -0,0 +1,60 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * A handler for determining if an HttpRequest should be retried after a
+ * recoverable exception during execution.
+ * <p>
+ * Implementations of this interface must be thread-safe. Access to shared
+ * data must be synchronized as methods of this interface may be executed
+ * from multiple threads.
+ *
+ * @since 4.0
+ */
+public interface HttpRequestRetryHandler {
+
+ /**
+ * Determines if a method should be retried after an IOException
+ * occurs during execution.
+ *
+ * @param exception the exception that occurred
+ * @param executionCount the number of times this method has been
+ * unsuccessfully executed
+ * @param context the context for the request execution
+ *
+ * @return <code>true</code> if the method should be retried, <code>false</code>
+ * otherwise
+ */
+ boolean retryRequest(IOException exception, int executionCount, HttpContext context);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/HttpResponseException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/HttpResponseException.java
new file mode 100644
index 0000000000..68500c6ac6
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/HttpResponseException.java
@@ -0,0 +1,52 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Signals a non 2xx HTTP response.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class HttpResponseException extends ClientProtocolException {
+
+ private static final long serialVersionUID = -7186627969477257933L;
+
+ private final int statusCode;
+
+ public HttpResponseException(final int statusCode, final String s) {
+ super(s);
+ this.statusCode = statusCode;
+ }
+
+ public int getStatusCode() {
+ return this.statusCode;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/NonRepeatableRequestException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/NonRepeatableRequestException.java
new file mode 100644
index 0000000000..6d9dce208c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/NonRepeatableRequestException.java
@@ -0,0 +1,72 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client;
+
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Signals failure to retry the request due to non-repeatable request
+ * entity.
+ *
+ *
+ * @since 4.0
+ */
+@Immutable
+public class NonRepeatableRequestException extends ProtocolException {
+
+ private static final long serialVersionUID = 82685265288806048L;
+
+ /**
+ * Creates a new NonRepeatableEntityException with a <tt>null</tt> detail message.
+ */
+ public NonRepeatableRequestException() {
+ super();
+ }
+
+ /**
+ * Creates a new NonRepeatableEntityException with the specified detail message.
+ *
+ * @param message The exception detail message
+ */
+ public NonRepeatableRequestException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new NonRepeatableEntityException with the specified detail message.
+ *
+ * @param message The exception detail message
+ * @param cause the cause
+ */
+ public NonRepeatableRequestException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/RedirectException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/RedirectException.java
new file mode 100644
index 0000000000..e187db7895
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/RedirectException.java
@@ -0,0 +1,69 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client;
+
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Signals violation of HTTP specification caused by an invalid redirect
+ *
+ *
+ * @since 4.0
+ */
+@Immutable
+public class RedirectException extends ProtocolException {
+
+ private static final long serialVersionUID = 4418824536372559326L;
+
+ /**
+ * Creates a new RedirectException with a <tt>null</tt> detail message.
+ */
+ public RedirectException() {
+ super();
+ }
+
+ /**
+ * Creates a new RedirectException with the specified detail message.
+ *
+ * @param message The exception detail message
+ */
+ public RedirectException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new RedirectException with the specified detail message and cause.
+ *
+ * @param message the exception detail message
+ * @param cause the <tt>Throwable</tt> that caused this exception, or <tt>null</tt>
+ * if the cause is unavailable, unknown, or not a <tt>Throwable</tt>
+ */
+ public RedirectException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/RedirectHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/RedirectHandler.java
new file mode 100644
index 0000000000..f2dcd842b9
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/RedirectHandler.java
@@ -0,0 +1,77 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client;
+
+import java.net.URI;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * A handler for determining if an HTTP request should be redirected to
+ * a new location in response to an HTTP response received from the target
+ * server.
+ * <p>
+ * Implementations of this interface must be thread-safe. Access to shared
+ * data must be synchronized as methods of this interface may be executed
+ * from multiple threads.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.1) use {@link RedirectStrategy}
+ */
+@Deprecated
+public interface RedirectHandler {
+
+ /**
+ * Determines if a request should be redirected to a new location
+ * given the response from the target server.
+ *
+ * @param response the response received from the target server
+ * @param context the context for the request execution
+ *
+ * @return <code>true</code> if the request should be redirected, <code>false</code>
+ * otherwise
+ */
+ boolean isRedirectRequested(HttpResponse response, HttpContext context);
+
+ /**
+ * Determines the location request is expected to be redirected to
+ * given the response from the target server and the current request
+ * execution context.
+ *
+ * @param response the response received from the target server
+ * @param context the context for the request execution
+ *
+ * @return redirect URI
+ */
+ URI getLocationURI(HttpResponse response, HttpContext context)
+ throws ProtocolException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/RedirectStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/RedirectStrategy.java
new file mode 100644
index 0000000000..ad2499c375
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/RedirectStrategy.java
@@ -0,0 +1,81 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client;
+
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * A strategy for determining if an HTTP request should be redirected to
+ * a new location in response to an HTTP response received from the target
+ * server.
+ * <p>
+ * Implementations of this interface must be thread-safe. Access to shared
+ * data must be synchronized as methods of this interface may be executed
+ * from multiple threads.
+ *
+ * @since 4.1
+ */
+public interface RedirectStrategy {
+
+ /**
+ * Determines if a request should be redirected to a new location
+ * given the response from the target server.
+ *
+ * @param request the executed request
+ * @param response the response received from the target server
+ * @param context the context for the request execution
+ *
+ * @return <code>true</code> if the request should be redirected, <code>false</code>
+ * otherwise
+ */
+ boolean isRedirected(
+ HttpRequest request,
+ HttpResponse response,
+ HttpContext context) throws ProtocolException;
+
+ /**
+ * Determines the redirect location given the response from the target
+ * server and the current request execution context and generates a new
+ * request to be sent to the location.
+ *
+ * @param request the executed request
+ * @param response the response received from the target server
+ * @param context the context for the request execution
+ *
+ * @return redirected request
+ */
+ HttpUriRequest getRedirect(
+ HttpRequest request,
+ HttpResponse response,
+ HttpContext context) throws ProtocolException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/RequestDirector.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/RequestDirector.java
new file mode 100644
index 0000000000..ebaeb74a00
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/RequestDirector.java
@@ -0,0 +1,77 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * A client-side request director.
+ * The director decides which steps are necessary to execute a request.
+ * It establishes connections and optionally processes redirects and
+ * authentication challenges. The director may therefore generate and
+ * send a sequence of requests in order to execute one initial request.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) No longer used
+ */
+@Deprecated
+public interface RequestDirector {
+
+
+ /**
+ * Executes a request.
+ * <br/><b>Note:</b>
+ * For the time being, a new director is instantiated for each request.
+ * This is the same behavior as for <code>HttpMethodDirector</code>
+ * in HttpClient 3.
+ *
+ * @param target the target host for the request.
+ * Implementations may accept <code>null</code>
+ * if they can still determine a route, for example
+ * to a default target or by inspecting the request.
+ * @param request the request to execute
+ * @param context the context for executing the request
+ *
+ * @return the final response to the request.
+ * This is never an intermediate response with status code 1xx.
+ *
+ * @throws HttpException in case of a problem
+ * @throws IOException in case of an IO problem
+ * or if the connection was aborted
+ */
+ HttpResponse execute(HttpHost target, HttpRequest request, HttpContext context)
+ throws HttpException, IOException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/ResponseHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/ResponseHandler.java
new file mode 100644
index 0000000000..6fa02b6501
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/ResponseHandler.java
@@ -0,0 +1,54 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+
+/**
+ * Handler that encapsulates the process of generating a response object
+ * from a {@link HttpResponse}.
+ *
+ *
+ * @since 4.0
+ */
+public interface ResponseHandler<T> {
+
+ /**
+ * Processes an {@link HttpResponse} and returns some value
+ * corresponding to that response.
+ *
+ * @param response The response to process
+ * @return A value determined by the response
+ *
+ * @throws ClientProtocolException in case of an http protocol error
+ * @throws IOException in case of a problem or the connection was aborted
+ */
+ T handleResponse(HttpResponse response) throws ClientProtocolException, IOException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/ServiceUnavailableRetryStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/ServiceUnavailableRetryStrategy.java
new file mode 100644
index 0000000000..7a08dab659
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/ServiceUnavailableRetryStrategy.java
@@ -0,0 +1,60 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * Strategy interface that allows API users to plug in their own logic to
+ * control whether or not a retry should automatically be done, how many times
+ * it should be retried and so on.
+ *
+ * @since 4.2
+ */
+public interface ServiceUnavailableRetryStrategy {
+
+ /**
+ * Determines if a method should be retried given the response from the target server.
+ *
+ * @param response the response from the target server
+ * @param executionCount the number of times this method has been
+ * unsuccessfully executed
+ * @param context the context for the request execution
+
+ * @return <code>true</code> if the method should be retried, <code>false</code>
+ * otherwise
+ */
+ boolean retryRequest(HttpResponse response, int executionCount, HttpContext context);
+
+ /**
+ * @return The interval between the subsequent auto-retries.
+ */
+ long getRetryInterval();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/UserTokenHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/UserTokenHandler.java
new file mode 100644
index 0000000000..1e15ef7c63
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/UserTokenHandler.java
@@ -0,0 +1,58 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client;
+
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * A handler for determining if the given execution context is user specific
+ * or not. The token object returned by this handler is expected to uniquely
+ * identify the current user if the context is user specific or to be
+ * <code>null</code> if the context does not contain any resources or details
+ * specific to the current user.
+ * <p/>
+ * The user token will be used to ensure that user specific resources will not
+ * be shared with or reused by other users.
+ *
+ * @since 4.0
+ */
+public interface UserTokenHandler {
+
+ /**
+ * The token object returned by this method is expected to uniquely
+ * identify the current user if the context is user specific or to be
+ * <code>null</code> if it is not.
+ *
+ * @param context the execution context
+ *
+ * @return user token that uniquely identifies the user or
+ * <code>null</null> if the context is not user specific.
+ */
+ Object getUserToken(HttpContext context);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/CacheResponseStatus.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/CacheResponseStatus.java
new file mode 100644
index 0000000000..d000839afa
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/CacheResponseStatus.java
@@ -0,0 +1,55 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.cache;
+
+/**
+ * This enumeration represents the various ways a response can be generated
+ * by the {@link ch.boye.httpclientandroidlib.impl.client.cache.CachingHttpClient};
+ * if a request is executed with an {@link ch.boye.httpclientandroidlib.protocol.HttpContext}
+ * then a parameter with one of these values will be registered in the
+ * context under the key
+ * {@link ch.boye.httpclientandroidlib.impl.client.cache.CachingHttpClient#CACHE_RESPONSE_STATUS}.
+ */
+public enum CacheResponseStatus {
+
+ /** The response was generated directly by the caching module. */
+ CACHE_MODULE_RESPONSE,
+
+ /** A response was generated from the cache with no requests sent
+ * upstream.
+ */
+ CACHE_HIT,
+
+ /** The response came from an upstream server. */
+ CACHE_MISS,
+
+ /** The response was generated from the cache after validating the
+ * entry with the origin server.
+ */
+ VALIDATED;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HeaderConstants.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HeaderConstants.java
new file mode 100644
index 0000000000..22f34dcff9
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HeaderConstants.java
@@ -0,0 +1,81 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.cache;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Records static constants for various HTTP header names.
+ * @since 4.1
+ */
+@Immutable
+public class HeaderConstants {
+
+ public static final String GET_METHOD = "GET";
+ public static final String HEAD_METHOD = "HEAD";
+ public static final String OPTIONS_METHOD = "OPTIONS";
+ public static final String PUT_METHOD = "PUT";
+ public static final String DELETE_METHOD = "DELETE";
+ public static final String TRACE_METHOD = "TRACE";
+
+ public static final String LAST_MODIFIED = "Last-Modified";
+ public static final String IF_MATCH = "If-Match";
+ public static final String IF_RANGE = "If-Range";
+ public static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
+ public static final String IF_MODIFIED_SINCE = "If-Modified-Since";
+ public static final String IF_NONE_MATCH = "If-None-Match";
+
+ public static final String PRAGMA = "Pragma";
+ public static final String MAX_FORWARDS = "Max-Forwards";
+ public static final String ETAG = "ETag";
+ public static final String EXPIRES = "Expires";
+ public static final String AGE = "Age";
+ public static final String VARY = "Vary";
+ public static final String ALLOW = "Allow";
+ public static final String VIA = "Via";
+ public static final String PUBLIC = "public";
+ public static final String PRIVATE = "private";
+
+ public static final String CACHE_CONTROL = "Cache-Control";
+ public static final String CACHE_CONTROL_NO_STORE = "no-store";
+ public static final String CACHE_CONTROL_NO_CACHE = "no-cache";
+ public static final String CACHE_CONTROL_MAX_AGE = "max-age";
+ public static final String CACHE_CONTROL_MAX_STALE = "max-stale";
+ public static final String CACHE_CONTROL_MIN_FRESH = "min-fresh";
+ public static final String CACHE_CONTROL_MUST_REVALIDATE = "must-revalidate";
+ public static final String CACHE_CONTROL_PROXY_REVALIDATE = "proxy-revalidate";
+ public static final String STALE_IF_ERROR = "stale-if-error";
+ public static final String STALE_WHILE_REVALIDATE = "stale-while-revalidate";
+
+ public static final String WARNING = "Warning";
+ public static final String RANGE = "Range";
+ public static final String CONTENT_RANGE = "Content-Range";
+ public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
+ public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate";
+ public static final String AUTHORIZATION = "Authorization";
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheContext.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheContext.java
new file mode 100644
index 0000000000..54edfa12a8
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheContext.java
@@ -0,0 +1,71 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.cache;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
+import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * @since 4.3
+ */
+@NotThreadSafe
+public class HttpCacheContext extends HttpClientContext {
+
+ /**
+ * This is the name under which the {@link CacheResponseStatus} of a request
+ * (for example, whether it resulted in a cache hit) will be recorded if an
+ * {@link HttpContext} is provided during execution.
+ */
+ public static final String CACHE_RESPONSE_STATUS = "http.cache.response.status";
+
+ public static HttpCacheContext adapt(final HttpContext context) {
+ if (context instanceof HttpCacheContext) {
+ return (HttpCacheContext) context;
+ } else {
+ return new HttpCacheContext(context);
+ }
+ }
+
+ public static HttpCacheContext create() {
+ return new HttpCacheContext(new BasicHttpContext());
+ }
+
+ public HttpCacheContext(final HttpContext context) {
+ super(context);
+ }
+
+ public HttpCacheContext() {
+ super();
+ }
+
+ public CacheResponseStatus getCacheResponseStatus() {
+ return getAttribute(CACHE_RESPONSE_STATUS, CacheResponseStatus.class);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheEntry.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheEntry.java
new file mode 100644
index 0000000000..d037862793
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheEntry.java
@@ -0,0 +1,263 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.cache;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.StatusLine;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.utils.DateUtils;
+import ch.boye.httpclientandroidlib.message.HeaderGroup;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Structure used to store an {@link ch.boye.httpclientandroidlib.HttpResponse} in a cache.
+ * Some entries can optionally depend on system resources that may require
+ * explicit deallocation. In such a case {@link #getResource()} should return
+ * a non null instance of {@link Resource} that must be deallocated by calling
+ * {@link Resource#dispose()} method when no longer used.
+ *
+ * @since 4.1
+ */
+@Immutable
+public class HttpCacheEntry implements Serializable {
+
+ private static final long serialVersionUID = -6300496422359477413L;
+
+ private final Date requestDate;
+ private final Date responseDate;
+ private final StatusLine statusLine;
+ private final HeaderGroup responseHeaders;
+ private final Resource resource;
+ private final Map<String,String> variantMap;
+ private final Date date;
+
+ /**
+ * Create a new {@link HttpCacheEntry} with variants.
+ * @param requestDate
+ * Date/time when the request was made (Used for age
+ * calculations)
+ * @param responseDate
+ * Date/time that the response came back (Used for age
+ * calculations)
+ * @param statusLine
+ * HTTP status line from origin response
+ * @param responseHeaders
+ * Header[] from original HTTP Response
+ * @param resource representing origin response body
+ * @param variantMap describing cache entries that are variants
+ * of this parent entry; this maps a "variant key" (derived
+ * from the varying request headers) to a "cache key" (where
+ * in the cache storage the particular variant is located)
+ */
+ public HttpCacheEntry(
+ final Date requestDate,
+ final Date responseDate,
+ final StatusLine statusLine,
+ final Header[] responseHeaders,
+ final Resource resource,
+ final Map<String,String> variantMap) {
+ super();
+ Args.notNull(requestDate, "Request date");
+ Args.notNull(responseDate, "Response date");
+ Args.notNull(statusLine, "Status line");
+ Args.notNull(responseHeaders, "Response headers");
+ this.requestDate = requestDate;
+ this.responseDate = responseDate;
+ this.statusLine = statusLine;
+ this.responseHeaders = new HeaderGroup();
+ this.responseHeaders.setHeaders(responseHeaders);
+ this.resource = resource;
+ this.variantMap = variantMap != null
+ ? new HashMap<String,String>(variantMap)
+ : null;
+ this.date = parseDate();
+ }
+
+ /**
+ * Create a new {@link HttpCacheEntry}.
+ *
+ * @param requestDate
+ * Date/time when the request was made (Used for age
+ * calculations)
+ * @param responseDate
+ * Date/time that the response came back (Used for age
+ * calculations)
+ * @param statusLine
+ * HTTP status line from origin response
+ * @param responseHeaders
+ * Header[] from original HTTP Response
+ * @param resource representing origin response body
+ */
+ public HttpCacheEntry(final Date requestDate, final Date responseDate, final StatusLine statusLine,
+ final Header[] responseHeaders, final Resource resource) {
+ this(requestDate, responseDate, statusLine, responseHeaders, resource,
+ new HashMap<String,String>());
+ }
+
+ /**
+ * Find the "Date" response header and parse it into a java.util.Date
+ * @return the Date value of the header or null if the header is not present
+ */
+ private Date parseDate() {
+ final Header dateHdr = getFirstHeader(HTTP.DATE_HEADER);
+ if (dateHdr == null) {
+ return null;
+ }
+ return DateUtils.parseDate(dateHdr.getValue());
+ }
+
+ /**
+ * Returns the {@link StatusLine} from the origin
+ * {@link ch.boye.httpclientandroidlib.HttpResponse}.
+ */
+ public StatusLine getStatusLine() {
+ return this.statusLine;
+ }
+
+ /**
+ * Returns the {@link ProtocolVersion} from the origin
+ * {@link ch.boye.httpclientandroidlib.HttpResponse}.
+ */
+ public ProtocolVersion getProtocolVersion() {
+ return this.statusLine.getProtocolVersion();
+ }
+
+ /**
+ * Gets the reason phrase from the origin
+ * {@link ch.boye.httpclientandroidlib.HttpResponse}, for example, "Not Modified".
+ */
+ public String getReasonPhrase() {
+ return this.statusLine.getReasonPhrase();
+ }
+
+ /**
+ * Returns the HTTP response code from the origin
+ * {@link ch.boye.httpclientandroidlib.HttpResponse}.
+ */
+ public int getStatusCode() {
+ return this.statusLine.getStatusCode();
+ }
+
+ /**
+ * Returns the time the associated origin request was initiated by the
+ * caching module.
+ * @return {@link Date}
+ */
+ public Date getRequestDate() {
+ return requestDate;
+ }
+
+ /**
+ * Returns the time the origin response was received by the caching module.
+ * @return {@link Date}
+ */
+ public Date getResponseDate() {
+ return responseDate;
+ }
+
+ /**
+ * Returns all the headers that were on the origin response.
+ */
+ public Header[] getAllHeaders() {
+ return responseHeaders.getAllHeaders();
+ }
+
+ /**
+ * Returns the first header from the origin response with the given
+ * name.
+ */
+ public Header getFirstHeader(final String name) {
+ return responseHeaders.getFirstHeader(name);
+ }
+
+ /**
+ * Gets all the headers with the given name that were on the origin
+ * response.
+ */
+ public Header[] getHeaders(final String name) {
+ return responseHeaders.getHeaders(name);
+ }
+
+ /**
+ * Gets the Date value of the "Date" header or null if the header is missing or cannot be
+ * parsed.
+ *
+ * @since 4.3
+ */
+ public Date getDate() {
+ return date;
+ }
+
+ /**
+ * Returns the {@link Resource} containing the origin response body.
+ */
+ public Resource getResource() {
+ return this.resource;
+ }
+
+ /**
+ * Indicates whether the origin response indicated the associated
+ * resource had variants (i.e. that the Vary header was set on the
+ * origin response).
+ * @return {@code true} if this cached response was a variant
+ */
+ public boolean hasVariants() {
+ return getFirstHeader(HeaderConstants.VARY) != null;
+ }
+
+ /**
+ * Returns an index about where in the cache different variants for
+ * a given resource are stored. This maps "variant keys" to "cache keys",
+ * where the variant key is derived from the varying request headers,
+ * and the cache key is the location in the
+ * {@link ch.boye.httpclientandroidlib.client.cache.HttpCacheStorage} where that
+ * particular variant is stored. The first variant returned is used as
+ * the "parent" entry to hold this index of the other variants.
+ */
+ public Map<String, String> getVariantMap() {
+ return Collections.unmodifiableMap(variantMap);
+ }
+
+ /**
+ * Provides a string representation of this instance suitable for
+ * human consumption.
+ */
+ @Override
+ public String toString() {
+ return "[request date=" + this.requestDate + "; response date=" + this.responseDate
+ + "; statusLine=" + this.statusLine + "]";
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheEntrySerializationException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheEntrySerializationException.java
new file mode 100644
index 0000000000..e74f64db6a
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheEntrySerializationException.java
@@ -0,0 +1,48 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.cache;
+
+import java.io.IOException;
+
+/**
+ * Thrown if serialization or deserialization of an {@link HttpCacheEntry}
+ * fails.
+ */
+public class HttpCacheEntrySerializationException extends IOException {
+
+ private static final long serialVersionUID = 9219188365878433519L;
+
+ public HttpCacheEntrySerializationException(final String message) {
+ super();
+ }
+
+ public HttpCacheEntrySerializationException(final String message, final Throwable cause) {
+ super(message);
+ initCause(cause);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheEntrySerializer.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheEntrySerializer.java
new file mode 100644
index 0000000000..2903fb74d4
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheEntrySerializer.java
@@ -0,0 +1,54 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.cache;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Used by some {@link HttpCacheStorage} implementations to serialize
+ * {@link HttpCacheEntry} instances to a byte representation before
+ * storage.
+ */
+public interface HttpCacheEntrySerializer {
+
+ /**
+ * Serializes the given entry to a byte representation on the
+ * given {@link OutputStream}.
+ * @throws IOException
+ */
+ void writeTo(HttpCacheEntry entry, OutputStream os) throws IOException;
+
+ /**
+ * Deserializes a byte representation of a cache entry by reading
+ * from the given {@link InputStream}.
+ * @throws IOException
+ */
+ HttpCacheEntry readFrom(InputStream is) throws IOException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheInvalidator.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheInvalidator.java
new file mode 100644
index 0000000000..f8e70968bc
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheInvalidator.java
@@ -0,0 +1,58 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.cache;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+
+/**
+ * Given a particular HttpRequest, flush any cache entries that this request
+ * would invalidate.
+ *
+ * @since 4.3
+ */
+public interface HttpCacheInvalidator {
+
+ /**
+ * Remove cache entries from the cache that are no longer fresh or have been
+ * invalidated in some way.
+ *
+ * @param host
+ * The backend host we are talking to
+ * @param req
+ * The HttpRequest to that host
+ */
+ void flushInvalidatedCacheEntries(HttpHost host, HttpRequest req);
+
+ /**
+ * Flushes entries that were invalidated by the given response received for
+ * the given host/request pair.
+ */
+ void flushInvalidatedCacheEntries(HttpHost host, HttpRequest request, HttpResponse response);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheStorage.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheStorage.java
new file mode 100644
index 0000000000..b0759d1954
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheStorage.java
@@ -0,0 +1,81 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.cache;
+
+import java.io.IOException;
+
+/**
+ * New storage backends should implement this {@link HttpCacheStorage}
+ * interface. They can then be plugged into the existing
+ * {@link ch.boye.httpclientandroidlib.impl.client.cache.CachingHttpClient}
+ * implementation.
+ *
+ * @since 4.1
+ */
+public interface HttpCacheStorage {
+
+ /**
+ * Store a given cache entry under the given key.
+ * @param key where in the cache to store the entry
+ * @param entry cached response to store
+ * @throws IOException
+ */
+ void putEntry(String key, HttpCacheEntry entry) throws IOException;
+
+ /**
+ * Retrieves the cache entry stored under the given key
+ * or null if no entry exists under that key.
+ * @param key cache key
+ * @return an {@link HttpCacheEntry} or {@code null} if no
+ * entry exists
+ * @throws IOException
+ */
+ HttpCacheEntry getEntry(String key) throws IOException;
+
+ /**
+ * Deletes/invalidates/removes any cache entries currently
+ * stored under the given key.
+ * @param key
+ * @throws IOException
+ */
+ void removeEntry(String key) throws IOException;
+
+ /**
+ * Atomically applies the given callback to update an existing cache
+ * entry under a given key.
+ * @param key indicates which entry to modify
+ * @param callback performs the update; see
+ * {@link HttpCacheUpdateCallback} for details, but roughly the
+ * callback expects to be handed the current entry and will return
+ * the new value for the entry.
+ * @throws IOException
+ * @throws HttpCacheUpdateException
+ */
+ void updateEntry(
+ String key, HttpCacheUpdateCallback callback) throws IOException, HttpCacheUpdateException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheUpdateCallback.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheUpdateCallback.java
new file mode 100644
index 0000000000..abc810fe87
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheUpdateCallback.java
@@ -0,0 +1,52 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.cache;
+
+import java.io.IOException;
+
+/**
+ * Used for atomically updating entries in a {@link HttpCacheStorage}
+ * implementation. The current entry (if any) is fed into an implementation
+ * of this interface, and the new, possibly updated entry (if any)
+ * should be returned.
+ */
+public interface HttpCacheUpdateCallback {
+
+ /**
+ * Returns the new cache entry that should replace an existing one.
+ *
+ * @param existing
+ * the cache entry currently in-place in the cache, possibly
+ * <code>null</code> if nonexistent
+ * @return the cache entry that should replace it, again,
+ * possibly <code>null</code> if the entry should be deleted
+ *
+ * @since 4.1
+ */
+ HttpCacheEntry update(HttpCacheEntry existing) throws IOException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheUpdateException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheUpdateException.java
new file mode 100644
index 0000000000..278fe3afbb
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/HttpCacheUpdateException.java
@@ -0,0 +1,48 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.cache;
+
+/**
+ * Signals that {@link HttpCacheStorage} encountered an error performing an
+ * update operation.
+ *
+ * @since 4.1
+ */
+public class HttpCacheUpdateException extends Exception {
+
+ private static final long serialVersionUID = 823573584868632876L;
+
+ public HttpCacheUpdateException(final String message) {
+ super(message);
+ }
+
+ public HttpCacheUpdateException(final String message, final Throwable cause) {
+ super(message);
+ initCause(cause);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/InputLimit.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/InputLimit.java
new file mode 100644
index 0000000000..45ef3b26d0
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/InputLimit.java
@@ -0,0 +1,76 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.cache;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+
+/**
+ * Used to limiting the size of an incoming response body of
+ * unknown size that is optimistically being read in anticipation
+ * of caching it.
+ * @since 4.1
+ */
+@NotThreadSafe // reached
+public class InputLimit {
+
+ private final long value;
+ private boolean reached;
+
+ /**
+ * Create a limit for how many bytes of a response body to
+ * read.
+ * @param value maximum length in bytes
+ */
+ public InputLimit(final long value) {
+ super();
+ this.value = value;
+ this.reached = false;
+ }
+
+ /**
+ * Returns the current maximum limit that was set on
+ * creation.
+ */
+ public long getValue() {
+ return this.value;
+ }
+
+ /**
+ * Used to report that the limit has been reached.
+ */
+ public void reached() {
+ this.reached = true;
+ }
+
+ /**
+ * Returns {@code true} if the input limit has been reached.
+ */
+ public boolean isReached() {
+ return this.reached;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/Resource.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/Resource.java
new file mode 100644
index 0000000000..86811e85cc
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/Resource.java
@@ -0,0 +1,60 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.cache;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Serializable;
+
+/**
+ * Represents a disposable system resource used for handling
+ * cached response bodies.
+ *
+ * @since 4.1
+ */
+public interface Resource extends Serializable {
+
+ /**
+ * Returns an {@link InputStream} from which the response
+ * body can be read.
+ * @throws IOException
+ */
+ InputStream getInputStream() throws IOException;
+
+ /**
+ * Returns the length in bytes of the response body.
+ */
+ long length();
+
+ /**
+ * Indicates the system no longer needs to keep this
+ * response body and any system resources associated with
+ * it may be reclaimed.
+ */
+ void dispose();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/ResourceFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/ResourceFactory.java
new file mode 100644
index 0000000000..0583d2605e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/ResourceFactory.java
@@ -0,0 +1,67 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.cache;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Generates {@link Resource} instances for handling cached
+ * HTTP response bodies.
+ *
+ * @since 4.1
+ */
+public interface ResourceFactory {
+
+ /**
+ * Creates a {@link Resource} from a given response body.
+ * @param requestId a unique identifier for this particular
+ * response body
+ * @param instream the original {@link InputStream}
+ * containing the response body of the origin HTTP response.
+ * @param limit maximum number of bytes to consume of the
+ * response body; if this limit is reached before the
+ * response body is fully consumed, mark the limit has
+ * having been reached and return a {@code Resource}
+ * containing the data read to that point.
+ * @return a {@code Resource} containing however much of
+ * the response body was successfully read.
+ * @throws IOException
+ */
+ Resource generate(String requestId, InputStream instream, InputLimit limit) throws IOException;
+
+ /**
+ * Clones an existing {@link Resource}.
+ * @param requestId unique identifier provided to associate
+ * with the cloned response body.
+ * @param resource the original response body to clone.
+ * @return the {@code Resource} copy
+ * @throws IOException
+ */
+ Resource copy(String requestId, Resource resource) throws IOException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/package.html b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/package.html
new file mode 100644
index 0000000000..58a1e3ff35
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/cache/package.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+<!--
+====================================================================
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+====================================================================
+
+This software consists of voluntary contributions made by many
+individuals on behalf of the Apache Software Foundation. For more
+information on the Apache Software Foundation, please see
+<http://www.apache.org/>.
+-->
+</head>
+<body bgcolor="white">
+
+<p>
+This package consists largely of constants and interfaces that are
+necessary for building new storage backends for the
+{@link org.apache.http.impl.client.cache.CachingHttpClient} or for
+those clients wanting to get a little more behavioral information
+out of the cache module (for example, whether a particular response
+was a cache hit or not). Developers that simply want to instantiate
+and make use of the caching module will be better off looking at
+the {@code CachingHttpClient} documentation itself.
+</p>
+<p>
+The classes in this package can be divided into two main groups:
+reference constants and interfaces needed for storage backends. In
+the former group,
+{@link org.apache.http.client.cache.HeaderConstants} contains a list
+of HTTP header names encoded as static fields, and the
+{@link org.apache.http.client.cache.CacheResponseStatus} enumeration
+values are set in an {@link org.apache.http.protocol.HttpContext} by
+the {@code CachingHttpClient} to indicate how the request was
+processed by the caching module itself.
+</p>
+<p>
+New storage backends will need to implement the
+{@link org.apache.http.client.cache.HttpCacheStorage}
+interface; they can then be passed to one of the {@code CachingHttpClient}
+constructors, which will happily make use of the new storage mechanism.
+The {@link org.apache.http.client.cache.HttpCacheEntry} class shows the
+datastructure for a cache entry that must be stored by the
+{@code HttpCacheStorage}.
+There is, in addition, the notion of a
+{@link org.apache.http.client.cache.Resource} and an associated
+{@link org.apache.http.client.cache.ResourceFactory}, which are used for
+managing the handling of cached response bodies. The default implementation
+used by the {@code CachingHttpClient} stores response bodies in memory;
+alternative implementations might involve storing these in a filesystem. A new
+{@code ResourceFactory} can be provided along with a {@code HttpCacheStorage}
+in one of the constructors to the {@code CachingHttpClient}. Finally, some
+of the additional storage backends we provide, like the
+{@link org.apache.http.impl.client.cache.ehcache.EhcacheHttpCacheStorage} and
+{@link org.apache.http.impl.client.cache.memcached.MemcachedHttpCacheStorage},
+can be provided with different serializers for the cache entry metadata;
+developers wanting to experiment with different serialization techniques
+should implement the
+{@link org.apache.http.client.cache.HttpCacheEntrySerializer} interface.
+</p>
+</body>
+</html>
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/config/AuthSchemes.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/config/AuthSchemes.java
new file mode 100644
index 0000000000..a8b4db5e0f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/config/AuthSchemes.java
@@ -0,0 +1,71 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.config;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Standard authentication schemes supported by HttpClient.
+ *
+ * @since 4.3
+ */
+@Immutable
+public final class AuthSchemes {
+
+ /**
+ * Basic authentication scheme as defined in RFC2617 (considered inherently
+ * insecure, but most widely supported)
+ */
+ public static final String BASIC = "Basic";
+
+ /**
+ * Digest authentication scheme as defined in RFC2617.
+ */
+ public static final String DIGEST = "Digest";
+
+ /**
+ * The NTLM scheme is a proprietary Microsoft Windows Authentication
+ * protocol (considered to be the most secure among currently supported
+ * authentication schemes).
+ */
+ public static final String NTLM = "NTLM";
+
+ /**
+ * SPNEGO Authentication scheme.
+ */
+ public static final String SPNEGO = "negotiate";
+
+ /**
+ * Kerberos Authentication scheme.
+ */
+ public static final String KERBEROS = "Kerberos";
+
+ private AuthSchemes() {
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/config/CookieSpecs.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/config/CookieSpecs.java
new file mode 100644
index 0000000000..132c6606fb
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/config/CookieSpecs.java
@@ -0,0 +1,69 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.config;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Standard cookie specifications supported by HttpClient.
+ *
+ * @since 4.3
+ */
+@Immutable
+public final class CookieSpecs {
+
+ /**
+ * The policy that provides high degree of compatibility
+ * with common cookie management of popular HTTP agents.
+ */
+ public static final String BROWSER_COMPATIBILITY = "compatibility";
+
+ /**
+ * The Netscape cookie draft compliant policy.
+ */
+ public static final String NETSCAPE = "netscape";
+
+ /**
+ * The RFC 2965 compliant policy (standard).
+ */
+ public static final String STANDARD = "standard";
+
+ /**
+ * The default 'best match' policy.
+ */
+ public static final String BEST_MATCH = "best-match";
+
+ /**
+ * The policy that ignores cookies.
+ */
+ public static final String IGNORE_COOKIES = "ignoreCookies";
+
+ private CookieSpecs() {
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/config/RequestConfig.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/config/RequestConfig.java
new file mode 100644
index 0000000000..e861f5e685
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/config/RequestConfig.java
@@ -0,0 +1,442 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.config;
+
+import java.net.InetAddress;
+import java.util.Collection;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+
+public class RequestConfig implements Cloneable {
+
+ public static final RequestConfig DEFAULT = new Builder().build();
+
+ private final boolean expectContinueEnabled;
+ private final HttpHost proxy;
+ private final InetAddress localAddress;
+ private final boolean staleConnectionCheckEnabled;
+ private final String cookieSpec;
+ private final boolean redirectsEnabled;
+ private final boolean relativeRedirectsAllowed;
+ private final boolean circularRedirectsAllowed;
+ private final int maxRedirects;
+ private final boolean authenticationEnabled;
+ private final Collection<String> targetPreferredAuthSchemes;
+ private final Collection<String> proxyPreferredAuthSchemes;
+ private final int connectionRequestTimeout;
+ private final int connectTimeout;
+ private final int socketTimeout;
+
+ RequestConfig(
+ final boolean expectContinueEnabled,
+ final HttpHost proxy,
+ final InetAddress localAddress,
+ final boolean staleConnectionCheckEnabled,
+ final String cookieSpec,
+ final boolean redirectsEnabled,
+ final boolean relativeRedirectsAllowed,
+ final boolean circularRedirectsAllowed,
+ final int maxRedirects,
+ final boolean authenticationEnabled,
+ final Collection<String> targetPreferredAuthSchemes,
+ final Collection<String> proxyPreferredAuthSchemes,
+ final int connectionRequestTimeout,
+ final int connectTimeout,
+ final int socketTimeout) {
+ super();
+ this.expectContinueEnabled = expectContinueEnabled;
+ this.proxy = proxy;
+ this.localAddress = localAddress;
+ this.staleConnectionCheckEnabled = staleConnectionCheckEnabled;
+ this.cookieSpec = cookieSpec;
+ this.redirectsEnabled = redirectsEnabled;
+ this.relativeRedirectsAllowed = relativeRedirectsAllowed;
+ this.circularRedirectsAllowed = circularRedirectsAllowed;
+ this.maxRedirects = maxRedirects;
+ this.authenticationEnabled = authenticationEnabled;
+ this.targetPreferredAuthSchemes = targetPreferredAuthSchemes;
+ this.proxyPreferredAuthSchemes = proxyPreferredAuthSchemes;
+ this.connectionRequestTimeout = connectionRequestTimeout;
+ this.connectTimeout = connectTimeout;
+ this.socketTimeout = socketTimeout;
+ }
+
+ /**
+ * Determines whether the 'Expect: 100-Continue' handshake is enabled
+ * for entity enclosing methods. The purpose of the 'Expect: 100-Continue'
+ * handshake is to allow a client that is sending a request message with
+ * a request body to determine if the origin server is willing to
+ * accept the request (based on the request headers) before the client
+ * sends the request body.
+ * <p/>
+ * The use of the 'Expect: 100-continue' handshake can result in
+ * a noticeable performance improvement for entity enclosing requests
+ * (such as POST and PUT) that require the target server's
+ * authentication.
+ * <p/>
+ * 'Expect: 100-continue' handshake should be used with caution, as it
+ * may cause problems with HTTP servers and proxies that do not support
+ * HTTP/1.1 protocol.
+ * <p/>
+ * Default: <code>false</code>
+ */
+ public boolean isExpectContinueEnabled() {
+ return expectContinueEnabled;
+ }
+
+ /**
+ * Returns HTTP proxy to be used for request execution.
+ * <p/>
+ * Default: <code>null</code>
+ */
+ public HttpHost getProxy() {
+ return proxy;
+ }
+
+ /**
+ * Returns local address to be used for request execution.
+ * <p/>
+ * On machines with multiple network interfaces, this parameter
+ * can be used to select the network interface from which the
+ * connection originates.
+ * <p/>
+ * Default: <code>null</code>
+ */
+ public InetAddress getLocalAddress() {
+ return localAddress;
+ }
+
+ /**
+ * Determines whether stale connection check is to be used. The stale
+ * connection check can cause up to 30 millisecond overhead per request and
+ * should be used only when appropriate. For performance critical
+ * operations this check should be disabled.
+ * <p/>
+ * Default: <code>true</code>
+ */
+ public boolean isStaleConnectionCheckEnabled() {
+ return staleConnectionCheckEnabled;
+ }
+
+ /**
+ * Determines the name of the cookie specification to be used for HTTP state
+ * management.
+ * <p/>
+ * Default: <code>null</code>
+ */
+ public String getCookieSpec() {
+ return cookieSpec;
+ }
+
+ /**
+ * Determines whether redirects should be handled automatically.
+ * <p/>
+ * Default: <code>true</code>
+ */
+ public boolean isRedirectsEnabled() {
+ return redirectsEnabled;
+ }
+
+ /**
+ * Determines whether relative redirects should be rejected. HTTP specification
+ * requires the location value be an absolute URI.
+ * <p/>
+ * Default: <code>true</code>
+ */
+ public boolean isRelativeRedirectsAllowed() {
+ return relativeRedirectsAllowed;
+ }
+
+ /**
+ * Determines whether circular redirects (redirects to the same location) should
+ * be allowed. The HTTP spec is not sufficiently clear whether circular redirects
+ * are permitted, therefore optionally they can be enabled
+ * <p/>
+ * Default: <code>false</code>
+ */
+ public boolean isCircularRedirectsAllowed() {
+ return circularRedirectsAllowed;
+ }
+
+ /**
+ * Returns the maximum number of redirects to be followed. The limit on number
+ * of redirects is intended to prevent infinite loops.
+ * <p/>
+ * Default: <code>50</code>
+ */
+ public int getMaxRedirects() {
+ return maxRedirects;
+ }
+
+ /**
+ * Determines whether authentication should be handled automatically.
+ * <p/>
+ * Default: <code>true</code>
+ */
+ public boolean isAuthenticationEnabled() {
+ return authenticationEnabled;
+ }
+
+ /**
+ * Determines the order of preference for supported authentication schemes
+ * when authenticating with the target host.
+ * <p/>
+ * Default: <code>null</code>
+ */
+ public Collection<String> getTargetPreferredAuthSchemes() {
+ return targetPreferredAuthSchemes;
+ }
+
+ /**
+ * Determines the order of preference for supported authentication schemes
+ * when authenticating with the proxy host.
+ * <p/>
+ * Default: <code>null</code>
+ */
+ public Collection<String> getProxyPreferredAuthSchemes() {
+ return proxyPreferredAuthSchemes;
+ }
+
+ /**
+ * Returns the timeout in milliseconds used when requesting a connection
+ * from the connection manager. A timeout value of zero is interpreted
+ * as an infinite timeout.
+ * <p/>
+ * A timeout value of zero is interpreted as an infinite timeout.
+ * A negative value is interpreted as undefined (system default).
+ * <p/>
+ * Default: <code>-1</code>
+ */
+ public int getConnectionRequestTimeout() {
+ return connectionRequestTimeout;
+ }
+
+ /**
+ * Determines the timeout in milliseconds until a connection is established.
+ * A timeout value of zero is interpreted as an infinite timeout.
+ * <p/>
+ * A timeout value of zero is interpreted as an infinite timeout.
+ * A negative value is interpreted as undefined (system default).
+ * <p/>
+ * Default: <code>-1</code>
+ */
+ public int getConnectTimeout() {
+ return connectTimeout;
+ }
+
+ /**
+ * Defines the socket timeout (<code>SO_TIMEOUT</code>) in milliseconds,
+ * which is the timeout for waiting for data or, put differently,
+ * a maximum period inactivity between two consecutive data packets).
+ * <p/>
+ * A timeout value of zero is interpreted as an infinite timeout.
+ * A negative value is interpreted as undefined (system default).
+ * <p/>
+ * Default: <code>-1</code>
+ */
+ public int getSocketTimeout() {
+ return socketTimeout;
+ }
+
+ @Override
+ protected RequestConfig clone() throws CloneNotSupportedException {
+ return (RequestConfig) super.clone();
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append(", expectContinueEnabled=").append(expectContinueEnabled);
+ builder.append(", proxy=").append(proxy);
+ builder.append(", localAddress=").append(localAddress);
+ builder.append(", staleConnectionCheckEnabled=").append(staleConnectionCheckEnabled);
+ builder.append(", cookieSpec=").append(cookieSpec);
+ builder.append(", redirectsEnabled=").append(redirectsEnabled);
+ builder.append(", relativeRedirectsAllowed=").append(relativeRedirectsAllowed);
+ builder.append(", maxRedirects=").append(maxRedirects);
+ builder.append(", circularRedirectsAllowed=").append(circularRedirectsAllowed);
+ builder.append(", authenticationEnabled=").append(authenticationEnabled);
+ builder.append(", targetPreferredAuthSchemes=").append(targetPreferredAuthSchemes);
+ builder.append(", proxyPreferredAuthSchemes=").append(proxyPreferredAuthSchemes);
+ builder.append(", connectionRequestTimeout=").append(connectionRequestTimeout);
+ builder.append(", connectTimeout=").append(connectTimeout);
+ builder.append(", socketTimeout=").append(socketTimeout);
+ builder.append("]");
+ return builder.toString();
+ }
+
+ public static RequestConfig.Builder custom() {
+ return new Builder();
+ }
+
+ public static RequestConfig.Builder copy(final RequestConfig config) {
+ return new Builder()
+ .setExpectContinueEnabled(config.isExpectContinueEnabled())
+ .setProxy(config.getProxy())
+ .setLocalAddress(config.getLocalAddress())
+ .setStaleConnectionCheckEnabled(config.isStaleConnectionCheckEnabled())
+ .setCookieSpec(config.getCookieSpec())
+ .setRedirectsEnabled(config.isRedirectsEnabled())
+ .setRelativeRedirectsAllowed(config.isRelativeRedirectsAllowed())
+ .setCircularRedirectsAllowed(config.isCircularRedirectsAllowed())
+ .setMaxRedirects(config.getMaxRedirects())
+ .setAuthenticationEnabled(config.isAuthenticationEnabled())
+ .setTargetPreferredAuthSchemes(config.getTargetPreferredAuthSchemes())
+ .setProxyPreferredAuthSchemes(config.getProxyPreferredAuthSchemes())
+ .setConnectionRequestTimeout(config.getConnectionRequestTimeout())
+ .setConnectTimeout(config.getConnectTimeout())
+ .setSocketTimeout(config.getSocketTimeout());
+ }
+
+ public static class Builder {
+
+ private boolean expectContinueEnabled;
+ private HttpHost proxy;
+ private InetAddress localAddress;
+ private boolean staleConnectionCheckEnabled;
+ private String cookieSpec;
+ private boolean redirectsEnabled;
+ private boolean relativeRedirectsAllowed;
+ private boolean circularRedirectsAllowed;
+ private int maxRedirects;
+ private boolean authenticationEnabled;
+ private Collection<String> targetPreferredAuthSchemes;
+ private Collection<String> proxyPreferredAuthSchemes;
+ private int connectionRequestTimeout;
+ private int connectTimeout;
+ private int socketTimeout;
+
+ Builder() {
+ super();
+ this.staleConnectionCheckEnabled = true;
+ this.redirectsEnabled = true;
+ this.maxRedirects = 50;
+ this.relativeRedirectsAllowed = true;
+ this.authenticationEnabled = true;
+ this.connectionRequestTimeout = -1;
+ this.connectTimeout = -1;
+ this.socketTimeout = -1;
+ }
+
+ public Builder setExpectContinueEnabled(final boolean expectContinueEnabled) {
+ this.expectContinueEnabled = expectContinueEnabled;
+ return this;
+ }
+
+ public Builder setProxy(final HttpHost proxy) {
+ this.proxy = proxy;
+ return this;
+ }
+
+ public Builder setLocalAddress(final InetAddress localAddress) {
+ this.localAddress = localAddress;
+ return this;
+ }
+
+ public Builder setStaleConnectionCheckEnabled(final boolean staleConnectionCheckEnabled) {
+ this.staleConnectionCheckEnabled = staleConnectionCheckEnabled;
+ return this;
+ }
+
+ public Builder setCookieSpec(final String cookieSpec) {
+ this.cookieSpec = cookieSpec;
+ return this;
+ }
+
+ public Builder setRedirectsEnabled(final boolean redirectsEnabled) {
+ this.redirectsEnabled = redirectsEnabled;
+ return this;
+ }
+
+ public Builder setRelativeRedirectsAllowed(final boolean relativeRedirectsAllowed) {
+ this.relativeRedirectsAllowed = relativeRedirectsAllowed;
+ return this;
+ }
+
+ public Builder setCircularRedirectsAllowed(final boolean circularRedirectsAllowed) {
+ this.circularRedirectsAllowed = circularRedirectsAllowed;
+ return this;
+ }
+
+ public Builder setMaxRedirects(final int maxRedirects) {
+ this.maxRedirects = maxRedirects;
+ return this;
+ }
+
+ public Builder setAuthenticationEnabled(final boolean authenticationEnabled) {
+ this.authenticationEnabled = authenticationEnabled;
+ return this;
+ }
+
+ public Builder setTargetPreferredAuthSchemes(final Collection<String> targetPreferredAuthSchemes) {
+ this.targetPreferredAuthSchemes = targetPreferredAuthSchemes;
+ return this;
+ }
+
+ public Builder setProxyPreferredAuthSchemes(final Collection<String> proxyPreferredAuthSchemes) {
+ this.proxyPreferredAuthSchemes = proxyPreferredAuthSchemes;
+ return this;
+ }
+
+ public Builder setConnectionRequestTimeout(final int connectionRequestTimeout) {
+ this.connectionRequestTimeout = connectionRequestTimeout;
+ return this;
+ }
+
+ public Builder setConnectTimeout(final int connectTimeout) {
+ this.connectTimeout = connectTimeout;
+ return this;
+ }
+
+ public Builder setSocketTimeout(final int socketTimeout) {
+ this.socketTimeout = socketTimeout;
+ return this;
+ }
+
+ public RequestConfig build() {
+ return new RequestConfig(
+ expectContinueEnabled,
+ proxy,
+ localAddress,
+ staleConnectionCheckEnabled,
+ cookieSpec,
+ redirectsEnabled,
+ relativeRedirectsAllowed,
+ circularRedirectsAllowed,
+ maxRedirects,
+ authenticationEnabled,
+ targetPreferredAuthSchemes,
+ proxyPreferredAuthSchemes,
+ connectionRequestTimeout,
+ connectTimeout,
+ socketTimeout);
+ }
+
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/config/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/config/package-info.java
new file mode 100644
index 0000000000..a7af2d46b8
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/config/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Client configuration APIs.
+ */
+package ch.boye.httpclientandroidlib.client.config;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/DecompressingEntity.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/DecompressingEntity.java
new file mode 100644
index 0000000000..9adec5edb7
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/DecompressingEntity.java
@@ -0,0 +1,105 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.entity;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.entity.HttpEntityWrapper;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Common base class for decompressing {@link HttpEntity} implementations.
+ *
+ * @since 4.1
+ */
+abstract class DecompressingEntity extends HttpEntityWrapper {
+
+ /**
+ * Default buffer size.
+ */
+ private static final int BUFFER_SIZE = 1024 * 2;
+
+ /**
+ * {@link #getContent()} method must return the same {@link InputStream}
+ * instance when DecompressingEntity is wrapping a streaming entity.
+ */
+ private InputStream content;
+
+ /**
+ * Creates a new {@link DecompressingEntity}.
+ *
+ * @param wrapped
+ * the non-null {@link HttpEntity} to be wrapped
+ */
+ public DecompressingEntity(final HttpEntity wrapped) {
+ super(wrapped);
+ }
+
+ abstract InputStream decorate(final InputStream wrapped) throws IOException;
+
+ private InputStream getDecompressingStream() throws IOException {
+ final InputStream in = wrappedEntity.getContent();
+ return new LazyDecompressingInputStream(in, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public InputStream getContent() throws IOException {
+ if (wrappedEntity.isStreaming()) {
+ if (content == null) {
+ content = getDecompressingStream();
+ }
+ return content;
+ } else {
+ return getDecompressingStream();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void writeTo(final OutputStream outstream) throws IOException {
+ Args.notNull(outstream, "Output stream");
+ final InputStream instream = getContent();
+ try {
+ final byte[] buffer = new byte[BUFFER_SIZE];
+ int l;
+ while ((l = instream.read(buffer)) != -1) {
+ outstream.write(buffer, 0, l);
+ }
+ } finally {
+ instream.close();
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/DeflateDecompressingEntity.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/DeflateDecompressingEntity.java
new file mode 100644
index 0000000000..4bce8b5b54
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/DeflateDecompressingEntity.java
@@ -0,0 +1,96 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.entity;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpEntity;
+
+/**
+ * {@link ch.boye.httpclientandroidlib.entity.HttpEntityWrapper} responsible for handling
+ * deflate Content Coded responses. In RFC2616 terms, <code>deflate</code>
+ * means a <code>zlib</code> stream as defined in RFC1950. Some server
+ * implementations have misinterpreted RFC2616 to mean that a
+ * <code>deflate</code> stream as defined in RFC1951 should be used
+ * (or maybe they did that since that's how IE behaves?). It's confusing
+ * that <code>deflate</code> in HTTP 1.1 means <code>zlib</code> streams
+ * rather than <code>deflate</code> streams. We handle both types in here,
+ * since that's what is seen on the internet. Moral - prefer
+ * <code>gzip</code>!
+ *
+ * @see GzipDecompressingEntity
+ *
+ * @since 4.1
+ */
+public class DeflateDecompressingEntity extends DecompressingEntity {
+
+ /**
+ * Creates a new {@link DeflateDecompressingEntity} which will wrap the specified
+ * {@link HttpEntity}.
+ *
+ * @param entity
+ * a non-null {@link HttpEntity} to be wrapped
+ */
+ public DeflateDecompressingEntity(final HttpEntity entity) {
+ super(entity);
+ }
+
+ /**
+ * Returns the non-null InputStream that should be returned to by all requests to
+ * {@link #getContent()}.
+ *
+ * @return a non-null InputStream
+ * @throws IOException if there was a problem
+ */
+ @Override
+ InputStream decorate(final InputStream wrapped) throws IOException {
+ return new DeflateInputStream(wrapped);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Header getContentEncoding() {
+
+ /* This HttpEntityWrapper has dealt with the Content-Encoding. */
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long getContentLength() {
+
+ /* Length of inflated content is unknown. */
+ return -1;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/DeflateInputStream.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/DeflateInputStream.java
new file mode 100644
index 0000000000..392a28a74c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/DeflateInputStream.java
@@ -0,0 +1,228 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.entity;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PushbackInputStream;
+import java.util.zip.DataFormatException;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+
+/** Deflate input stream. This class includes logic needed for various Rfc's in order
+* to reasonably implement the "deflate" compression style.
+*/
+public class DeflateInputStream extends InputStream
+{
+ private InputStream sourceStream;
+
+ public DeflateInputStream(final InputStream wrapped)
+ throws IOException
+ {
+ /*
+ * A zlib stream will have a header.
+ *
+ * CMF | FLG [| DICTID ] | ...compressed data | ADLER32 |
+ *
+ * * CMF is one byte.
+ *
+ * * FLG is one byte.
+ *
+ * * DICTID is four bytes, and only present if FLG.FDICT is set.
+ *
+ * Sniff the content. Does it look like a zlib stream, with a CMF, etc? c.f. RFC1950,
+ * section 2.2. http://tools.ietf.org/html/rfc1950#page-4
+ *
+ * We need to see if it looks like a proper zlib stream, or whether it is just a deflate
+ * stream. RFC2616 calls zlib streams deflate. Confusing, isn't it? That's why some servers
+ * implement deflate Content-Encoding using deflate streams, rather than zlib streams.
+ *
+ * We could start looking at the bytes, but to be honest, someone else has already read
+ * the RFCs and implemented that for us. So we'll just use the JDK libraries and exception
+ * handling to do this. If that proves slow, then we could potentially change this to check
+ * the first byte - does it look like a CMF? What about the second byte - does it look like
+ * a FLG, etc.
+ */
+
+ /* We read a small buffer to sniff the content. */
+ final byte[] peeked = new byte[6];
+
+ final PushbackInputStream pushback = new PushbackInputStream(wrapped, peeked.length);
+
+ final int headerLength = pushback.read(peeked);
+
+ if (headerLength == -1) {
+ throw new IOException("Unable to read the response");
+ }
+
+ /* We try to read the first uncompressed byte. */
+ final byte[] dummy = new byte[1];
+
+ final Inflater inf = new Inflater();
+
+ try {
+ int n;
+ while ((n = inf.inflate(dummy)) == 0) {
+ if (inf.finished()) {
+
+ /* Not expecting this, so fail loudly. */
+ throw new IOException("Unable to read the response");
+ }
+
+ if (inf.needsDictionary()) {
+
+ /* Need dictionary - then it must be zlib stream with DICTID part? */
+ break;
+ }
+
+ if (inf.needsInput()) {
+ inf.setInput(peeked);
+ }
+ }
+
+ if (n == -1) {
+ throw new IOException("Unable to read the response");
+ }
+
+ /*
+ * We read something without a problem, so it's a valid zlib stream. Just need to reset
+ * and return an unused InputStream now.
+ */
+ pushback.unread(peeked, 0, headerLength);
+ sourceStream = new DeflateStream(pushback, new Inflater());
+ } catch (final DataFormatException e) {
+
+ /* Presume that it's an RFC1951 deflate stream rather than RFC1950 zlib stream and try
+ * again. */
+ pushback.unread(peeked, 0, headerLength);
+ sourceStream = new DeflateStream(pushback, new Inflater(true));
+ } finally {
+ inf.end();
+ }
+
+ }
+
+ /** Read a byte.
+ */
+ @Override
+ public int read()
+ throws IOException
+ {
+ return sourceStream.read();
+ }
+
+ /** Read lots of bytes.
+ */
+ @Override
+ public int read(final byte[] b)
+ throws IOException
+ {
+ return sourceStream.read(b);
+ }
+
+ /** Read lots of specific bytes.
+ */
+ @Override
+ public int read(final byte[] b, final int off, final int len)
+ throws IOException
+ {
+ return sourceStream.read(b,off,len);
+ }
+
+ /** Skip
+ */
+ @Override
+ public long skip(final long n)
+ throws IOException
+ {
+ return sourceStream.skip(n);
+ }
+
+ /** Get available.
+ */
+ @Override
+ public int available()
+ throws IOException
+ {
+ return sourceStream.available();
+ }
+
+ /** Mark.
+ */
+ @Override
+ public void mark(final int readLimit)
+ {
+ sourceStream.mark(readLimit);
+ }
+
+ /** Reset.
+ */
+ @Override
+ public void reset()
+ throws IOException
+ {
+ sourceStream.reset();
+ }
+
+ /** Check if mark is supported.
+ */
+ @Override
+ public boolean markSupported()
+ {
+ return sourceStream.markSupported();
+ }
+
+ /** Close.
+ */
+ @Override
+ public void close()
+ throws IOException
+ {
+ sourceStream.close();
+ }
+
+ static class DeflateStream extends InflaterInputStream {
+
+ private boolean closed = false;
+
+ public DeflateStream(final InputStream in, final Inflater inflater) {
+ super(in, inflater);
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (closed) {
+ return;
+ }
+ closed = true;
+ inf.end();
+ super.close();
+ }
+
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/EntityBuilder.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/EntityBuilder.java
new file mode 100644
index 0000000000..ebec48a132
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/EntityBuilder.java
@@ -0,0 +1,342 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.entity;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.entity.AbstractHttpEntity;
+import ch.boye.httpclientandroidlib.entity.BasicHttpEntity;
+import ch.boye.httpclientandroidlib.entity.ByteArrayEntity;
+import ch.boye.httpclientandroidlib.entity.ContentType;
+import ch.boye.httpclientandroidlib.entity.FileEntity;
+import ch.boye.httpclientandroidlib.entity.InputStreamEntity;
+import ch.boye.httpclientandroidlib.entity.SerializableEntity;
+import ch.boye.httpclientandroidlib.entity.StringEntity;
+
+/**
+ * Builder for {@link HttpEntity} instances.
+ * <p/>
+ * Several setter methods of this builder are mutually exclusive. In case of multiple invocations
+ * of the following methods only the last one will have effect:
+ * <ul>
+ * <li>{@link #setText(String)}</li>
+ * <li>{@link #setBinary(byte[])}</li>
+ * <li>{@link #setStream(java.io.InputStream)}</li>
+ * <li>{@link #setSerializable(java.io.Serializable)}</li>
+ * <li>{@link #setParameters(java.util.List)}</li>
+ * <li>{@link #setParameters(ch.boye.httpclientandroidlib.NameValuePair...)}</li>
+ * <li>{@link #setFile(java.io.File)}</li>
+ * </ul>
+ *
+ * @since 4.3
+ */
+@NotThreadSafe
+public class EntityBuilder {
+
+ private String text;
+ private byte[] binary;
+ private InputStream stream;
+ private List<NameValuePair> parameters;
+ private Serializable serializable;
+ private File file;
+ private ContentType contentType;
+ private String contentEncoding;
+ private boolean chunked;
+ private boolean gzipCompress;
+
+ EntityBuilder() {
+ super();
+ }
+
+ public static EntityBuilder create() {
+ return new EntityBuilder();
+ }
+
+ private void clearContent() {
+ this.text = null;
+ this.binary = null;
+ this.stream = null;
+ this.parameters = null;
+ this.serializable = null;
+ this.file = null;
+ }
+
+ /**
+ * Returns entity content as a string if set using {@link #setText(String)} method.
+ */
+ public String getText() {
+ return text;
+ }
+
+ /**
+ * Sets entity content as a string. This method is mutually exclusive with
+ * {@link #setBinary(byte[])},
+ * {@link #setStream(java.io.InputStream)} ,
+ * {@link #setSerializable(java.io.Serializable)} ,
+ * {@link #setParameters(java.util.List)},
+ * {@link #setParameters(ch.boye.httpclientandroidlib.NameValuePair...)}
+ * {@link #setFile(java.io.File)} methods.
+ */
+ public EntityBuilder setText(final String text) {
+ clearContent();
+ this.text = text;
+ return this;
+ }
+
+ /**
+ * Returns entity content as a byte array if set using
+ * {@link #setBinary(byte[])} method.
+ */
+ public byte[] getBinary() {
+ return binary;
+ }
+
+ /**
+ * Sets entity content as a byte array. This method is mutually exclusive with
+ * {@link #setText(String)},
+ * {@link #setStream(java.io.InputStream)} ,
+ * {@link #setSerializable(java.io.Serializable)} ,
+ * {@link #setParameters(java.util.List)},
+ * {@link #setParameters(ch.boye.httpclientandroidlib.NameValuePair...)}
+ * {@link #setFile(java.io.File)} methods.
+ */
+ public EntityBuilder setBinary(final byte[] binary) {
+ clearContent();
+ this.binary = binary;
+ return this;
+ }
+
+ /**
+ * Returns entity content as a {@link InputStream} if set using
+ * {@link #setStream(java.io.InputStream)} method.
+ */
+ public InputStream getStream() {
+ return stream;
+ }
+
+ /**
+ * Sets entity content as a {@link InputStream}. This method is mutually exclusive with
+ * {@link #setText(String)},
+ * {@link #setBinary(byte[])},
+ * {@link #setSerializable(java.io.Serializable)} ,
+ * {@link #setParameters(java.util.List)},
+ * {@link #setParameters(ch.boye.httpclientandroidlib.NameValuePair...)}
+ * {@link #setFile(java.io.File)} methods.
+ */
+ public EntityBuilder setStream(final InputStream stream) {
+ clearContent();
+ this.stream = stream;
+ return this;
+ }
+
+ /**
+ * Returns entity content as a parameter list if set using
+ * {@link #setParameters(java.util.List)} or
+ * {@link #setParameters(ch.boye.httpclientandroidlib.NameValuePair...)} methods.
+ */
+ public List<NameValuePair> getParameters() {
+ return parameters;
+ }
+
+ /**
+ * Sets entity content as a parameter list. This method is mutually exclusive with
+ * {@link #setText(String)},
+ * {@link #setBinary(byte[])},
+ * {@link #setStream(java.io.InputStream)} ,
+ * {@link #setSerializable(java.io.Serializable)} ,
+ * {@link #setFile(java.io.File)} methods.
+ */
+ public EntityBuilder setParameters(final List<NameValuePair> parameters) {
+ clearContent();
+ this.parameters = parameters;
+ return this;
+ }
+
+ /**
+ * Sets entity content as a parameter list. This method is mutually exclusive with
+ * {@link #setText(String)},
+ * {@link #setBinary(byte[])},
+ * {@link #setStream(java.io.InputStream)} ,
+ * {@link #setSerializable(java.io.Serializable)} ,
+ * {@link #setFile(java.io.File)} methods.
+ */
+ public EntityBuilder setParameters(final NameValuePair... parameters) {
+ return setParameters(Arrays.asList(parameters));
+ }
+
+ /**
+ * Returns entity content as a {@link Serializable} if set using
+ * {@link #setSerializable(java.io.Serializable)} method.
+ */
+ public Serializable getSerializable() {
+ return serializable;
+ }
+
+ /**
+ * Sets entity content as a {@link Serializable}. This method is mutually exclusive with
+ * {@link #setText(String)},
+ * {@link #setBinary(byte[])},
+ * {@link #setStream(java.io.InputStream)} ,
+ * {@link #setParameters(java.util.List)},
+ * {@link #setParameters(ch.boye.httpclientandroidlib.NameValuePair...)}
+ * {@link #setFile(java.io.File)} methods.
+ */
+ public EntityBuilder setSerializable(final Serializable serializable) {
+ clearContent();
+ this.serializable = serializable;
+ return this;
+ }
+
+ /**
+ * Returns entity content as a {@link File} if set using
+ * {@link #setFile(java.io.File)} method.
+ */
+ public File getFile() {
+ return file;
+ }
+
+ /**
+ * Sets entity content as a {@link File}. This method is mutually exclusive with
+ * {@link #setText(String)},
+ * {@link #setBinary(byte[])},
+ * {@link #setStream(java.io.InputStream)} ,
+ * {@link #setParameters(java.util.List)},
+ * {@link #setParameters(ch.boye.httpclientandroidlib.NameValuePair...)}
+ * {@link #setSerializable(java.io.Serializable)} methods.
+ */
+ public EntityBuilder setFile(final File file) {
+ clearContent();
+ this.file = file;
+ return this;
+ }
+
+ /**
+ * Returns {@link ContentType} of the entity, if set.
+ */
+ public ContentType getContentType() {
+ return contentType;
+ }
+
+ /**
+ * Sets {@link ContentType} of the entity.
+ */
+ public EntityBuilder setContentType(final ContentType contentType) {
+ this.contentType = contentType;
+ return this;
+ }
+
+ /**
+ * Returns content encoding of the entity, if set.
+ */
+ public String getContentEncoding() {
+ return contentEncoding;
+ }
+
+ /**
+ * Sets content encoding of the entity.
+ */
+ public EntityBuilder setContentEncoding(final String contentEncoding) {
+ this.contentEncoding = contentEncoding;
+ return this;
+ }
+
+ /**
+ * Returns <code>true</code> if entity is to be chunk coded, <code>false</code> otherwise.
+ */
+ public boolean isChunked() {
+ return chunked;
+ }
+
+ /**
+ * Makes entity chunk coded.
+ */
+ public EntityBuilder chunked() {
+ this.chunked = true;
+ return this;
+ }
+
+ /**
+ * Returns <code>true</code> if entity is to be GZIP compressed, <code>false</code> otherwise.
+ */
+ public boolean isGzipCompress() {
+ return gzipCompress;
+ }
+
+ /**
+ * Makes entity GZIP compressed.
+ */
+ public EntityBuilder gzipCompress() {
+ this.gzipCompress = true;
+ return this;
+ }
+
+ private ContentType getContentOrDefault(final ContentType def) {
+ return this.contentType != null ? this.contentType : def;
+ }
+
+ /**
+ * Creates new instance of {@link HttpEntity} based on the current state.
+ */
+ public HttpEntity build() {
+ final AbstractHttpEntity e;
+ if (this.text != null) {
+ e = new StringEntity(this.text, getContentOrDefault(ContentType.DEFAULT_TEXT));
+ } else if (this.binary != null) {
+ e = new ByteArrayEntity(this.binary, getContentOrDefault(ContentType.DEFAULT_BINARY));
+ } else if (this.stream != null) {
+ e = new InputStreamEntity(this.stream, 1, getContentOrDefault(ContentType.DEFAULT_BINARY));
+ } else if (this.parameters != null) {
+ e = new UrlEncodedFormEntity(this.parameters,
+ this.contentType != null ? this.contentType.getCharset() : null);
+ } else if (this.serializable != null) {
+ e = new SerializableEntity(this.serializable);
+ e.setContentType(ContentType.DEFAULT_BINARY.toString());
+ } else if (this.file != null) {
+ e = new FileEntity(this.file, getContentOrDefault(ContentType.DEFAULT_BINARY));
+ } else {
+ e = new BasicHttpEntity();
+ }
+ if (e.getContentType() != null && this.contentType != null) {
+ e.setContentType(this.contentType.toString());
+ }
+ e.setContentEncoding(this.contentEncoding);
+ e.setChunked(this.chunked);
+ if (this.gzipCompress) {
+ return new GzipCompressingEntity(e);
+ }
+ return e;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/GzipCompressingEntity.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/GzipCompressingEntity.java
new file mode 100644
index 0000000000..89925a2b96
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/GzipCompressingEntity.java
@@ -0,0 +1,113 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.entity;
+
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.zip.GZIPOutputStream;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.entity.HttpEntityWrapper;
+import ch.boye.httpclientandroidlib.message.BasicHeader;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Wrapping entity that compresses content when {@link #writeTo writing}.
+ *
+ *
+ * @since 4.0
+ */
+public class GzipCompressingEntity extends HttpEntityWrapper {
+
+ private static final String GZIP_CODEC = "gzip";
+
+ public GzipCompressingEntity(final HttpEntity entity) {
+ super(entity);
+ }
+
+ @Override
+ public Header getContentEncoding() {
+ return new BasicHeader(HTTP.CONTENT_ENCODING, GZIP_CODEC);
+ }
+
+ @Override
+ public long getContentLength() {
+ return -1;
+ }
+
+ @Override
+ public boolean isChunked() {
+ // force content chunking
+ return true;
+ }
+
+ @Override
+ public InputStream getContent() throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void writeTo(final OutputStream outstream) throws IOException {
+ Args.notNull(outstream, "Output stream");
+ final GZIPOutputStream gzip = new GZIPOutputStream(outstream);
+ wrappedEntity.writeTo(gzip);
+ // Only close output stream if the wrapped entity has been
+ // successfully written out
+ gzip.close();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/GzipDecompressingEntity.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/GzipDecompressingEntity.java
new file mode 100644
index 0000000000..a3dd5b259b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/GzipDecompressingEntity.java
@@ -0,0 +1,80 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.entity;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.GZIPInputStream;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpEntity;
+
+/**
+ * {@link ch.boye.httpclientandroidlib.entity.HttpEntityWrapper} for handling gzip
+ * Content Coded responses.
+ *
+ * @since 4.1
+ */
+public class GzipDecompressingEntity extends DecompressingEntity {
+
+ /**
+ * Creates a new {@link GzipDecompressingEntity} which will wrap the specified
+ * {@link HttpEntity}.
+ *
+ * @param entity
+ * the non-null {@link HttpEntity} to be wrapped
+ */
+ public GzipDecompressingEntity(final HttpEntity entity) {
+ super(entity);
+ }
+
+ @Override
+ InputStream decorate(final InputStream wrapped) throws IOException {
+ return new GZIPInputStream(wrapped);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Header getContentEncoding() {
+
+ /* This HttpEntityWrapper has dealt with the Content-Encoding. */
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long getContentLength() {
+
+ /* length of ungzipped content is not known */
+ return -1;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/LazyDecompressingInputStream.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/LazyDecompressingInputStream.java
new file mode 100644
index 0000000000..60215ff156
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/LazyDecompressingInputStream.java
@@ -0,0 +1,105 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.entity;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Lazy init InputStream wrapper.
+ */
+@NotThreadSafe
+class LazyDecompressingInputStream extends InputStream {
+
+ private final InputStream wrappedStream;
+
+ private final DecompressingEntity decompressingEntity;
+
+ private InputStream wrapperStream;
+
+ public LazyDecompressingInputStream(
+ final InputStream wrappedStream,
+ final DecompressingEntity decompressingEntity) {
+ this.wrappedStream = wrappedStream;
+ this.decompressingEntity = decompressingEntity;
+ }
+
+ private void initWrapper() throws IOException {
+ if (wrapperStream == null) {
+ wrapperStream = decompressingEntity.decorate(wrappedStream);
+ }
+ }
+
+ @Override
+ public int read() throws IOException {
+ initWrapper();
+ return wrapperStream.read();
+ }
+
+ @Override
+ public int read(final byte[] b) throws IOException {
+ initWrapper();
+ return wrapperStream.read(b);
+ }
+
+ @Override
+ public int read(final byte[] b, final int off, final int len) throws IOException {
+ initWrapper();
+ return wrapperStream.read(b, off, len);
+ }
+
+ @Override
+ public long skip(final long n) throws IOException {
+ initWrapper();
+ return wrapperStream.skip(n);
+ }
+
+ @Override
+ public boolean markSupported() {
+ return false;
+ }
+
+ @Override
+ public int available() throws IOException {
+ initWrapper();
+ return wrapperStream.available();
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ if (wrapperStream != null) {
+ wrapperStream.close();
+ }
+ } finally {
+ wrappedStream.close();
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/UrlEncodedFormEntity.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/UrlEncodedFormEntity.java
new file mode 100644
index 0000000000..8551559278
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/UrlEncodedFormEntity.java
@@ -0,0 +1,107 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.entity;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.client.utils.URLEncodedUtils;
+import ch.boye.httpclientandroidlib.entity.ContentType;
+import ch.boye.httpclientandroidlib.entity.StringEntity;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+
+/**
+ * An entity composed of a list of url-encoded pairs.
+ * This is typically useful while sending an HTTP POST request.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe // AbstractHttpEntity is not thread-safe
+public class UrlEncodedFormEntity extends StringEntity {
+
+ /**
+ * Constructs a new {@link UrlEncodedFormEntity} with the list
+ * of parameters in the specified encoding.
+ *
+ * @param parameters list of name/value pairs
+ * @param charset encoding the name/value pairs be encoded with
+ * @throws UnsupportedEncodingException if the encoding isn't supported
+ */
+ public UrlEncodedFormEntity (
+ final List <? extends NameValuePair> parameters,
+ final String charset) throws UnsupportedEncodingException {
+ super(URLEncodedUtils.format(parameters,
+ charset != null ? charset : HTTP.DEF_CONTENT_CHARSET.name()),
+ ContentType.create(URLEncodedUtils.CONTENT_TYPE, charset));
+ }
+
+ /**
+ * Constructs a new {@link UrlEncodedFormEntity} with the list
+ * of parameters in the specified encoding.
+ *
+ * @param parameters iterable collection of name/value pairs
+ * @param charset encoding the name/value pairs be encoded with
+ *
+ * @since 4.2
+ */
+ public UrlEncodedFormEntity (
+ final Iterable <? extends NameValuePair> parameters,
+ final Charset charset) {
+ super(URLEncodedUtils.format(parameters,
+ charset != null ? charset : HTTP.DEF_CONTENT_CHARSET),
+ ContentType.create(URLEncodedUtils.CONTENT_TYPE, charset));
+ }
+
+ /**
+ * Constructs a new {@link UrlEncodedFormEntity} with the list
+ * of parameters with the default encoding of {@link HTTP#DEFAULT_CONTENT_CHARSET}
+ *
+ * @param parameters list of name/value pairs
+ * @throws UnsupportedEncodingException if the default encoding isn't supported
+ */
+ public UrlEncodedFormEntity (
+ final List <? extends NameValuePair> parameters) throws UnsupportedEncodingException {
+ this(parameters, (Charset) null);
+ }
+
+ /**
+ * Constructs a new {@link UrlEncodedFormEntity} with the list
+ * of parameters with the default encoding of {@link HTTP#DEFAULT_CONTENT_CHARSET}
+ *
+ * @param parameters iterable collection of name/value pairs
+ *
+ * @since 4.2
+ */
+ public UrlEncodedFormEntity (
+ final Iterable <? extends NameValuePair> parameters) {
+ this(parameters, null);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/package-info.java
new file mode 100644
index 0000000000..703f80d58d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/entity/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Client specific HTTP entity implementations.
+ */
+package ch.boye.httpclientandroidlib.client.entity;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/AbortableHttpRequest.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/AbortableHttpRequest.java
new file mode 100644
index 0000000000..92715c26ae
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/AbortableHttpRequest.java
@@ -0,0 +1,82 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.methods;
+
+import ch.boye.httpclientandroidlib.conn.ClientConnectionRequest;
+import ch.boye.httpclientandroidlib.conn.ConnectionReleaseTrigger;
+
+import java.io.IOException;
+
+
+/**
+ * Interface representing an HTTP request that can be aborted by shutting
+ * down the underlying HTTP connection.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link HttpExecutionAware}
+ */
+@Deprecated
+public interface AbortableHttpRequest {
+
+ /**
+ * Sets the {@link ch.boye.httpclientandroidlib.conn.ClientConnectionRequest}
+ * callback that can be used to abort a long-lived request for a connection.
+ * If the request is already aborted, throws an {@link IOException}.
+ *
+ * @see ch.boye.httpclientandroidlib.conn.ClientConnectionManager
+ */
+ void setConnectionRequest(ClientConnectionRequest connRequest) throws IOException;
+
+ /**
+ * Sets the {@link ConnectionReleaseTrigger} callback that can
+ * be used to abort an active connection.
+ * Typically, this will be the
+ * {@link ch.boye.httpclientandroidlib.conn.ManagedClientConnection} itself.
+ * If the request is already aborted, throws an {@link IOException}.
+ */
+ void setReleaseTrigger(ConnectionReleaseTrigger releaseTrigger) throws IOException;
+
+ /**
+ * Aborts this http request. Any active execution of this method should
+ * return immediately. If the request has not started, it will abort after
+ * the next execution. Aborting this request will cause all subsequent
+ * executions with this request to fail.
+ *
+ * @see ch.boye.httpclientandroidlib.client.HttpClient#execute(HttpUriRequest)
+ * @see ch.boye.httpclientandroidlib.client.HttpClient#execute(ch.boye.httpclientandroidlib.HttpHost,
+ * ch.boye.httpclientandroidlib.HttpRequest)
+ * @see ch.boye.httpclientandroidlib.client.HttpClient#execute(HttpUriRequest,
+ * ch.boye.httpclientandroidlib.protocol.HttpContext)
+ * @see ch.boye.httpclientandroidlib.client.HttpClient#execute(ch.boye.httpclientandroidlib.HttpHost,
+ * ch.boye.httpclientandroidlib.HttpRequest, ch.boye.httpclientandroidlib.protocol.HttpContext)
+ */
+ void abort();
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/AbstractExecutionAwareRequest.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/AbstractExecutionAwareRequest.java
new file mode 100644
index 0000000000..2525769c38
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/AbstractExecutionAwareRequest.java
@@ -0,0 +1,131 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.methods;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.client.utils.CloneUtils;
+import ch.boye.httpclientandroidlib.concurrent.Cancellable;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionRequest;
+import ch.boye.httpclientandroidlib.conn.ConnectionReleaseTrigger;
+import ch.boye.httpclientandroidlib.message.AbstractHttpMessage;
+
+@SuppressWarnings("deprecation")
+public abstract class AbstractExecutionAwareRequest extends AbstractHttpMessage implements
+ HttpExecutionAware, AbortableHttpRequest, Cloneable, HttpRequest {
+
+ private final AtomicBoolean aborted;
+ private final AtomicReference<Cancellable> cancellableRef;
+
+ protected AbstractExecutionAwareRequest() {
+ super();
+ this.aborted = new AtomicBoolean(false);
+ this.cancellableRef = new AtomicReference<Cancellable>(null);
+ }
+
+ @Deprecated
+ public void setConnectionRequest(final ClientConnectionRequest connRequest) {
+ setCancellable(new Cancellable() {
+
+ public boolean cancel() {
+ connRequest.abortRequest();
+ return true;
+ }
+
+ });
+ }
+
+ @Deprecated
+ public void setReleaseTrigger(final ConnectionReleaseTrigger releaseTrigger) {
+ setCancellable(new Cancellable() {
+
+ public boolean cancel() {
+ try {
+ releaseTrigger.abortConnection();
+ return true;
+ } catch (final IOException ex) {
+ return false;
+ }
+ }
+
+ });
+ }
+
+ public void abort() {
+ if (this.aborted.compareAndSet(false, true)) {
+ final Cancellable cancellable = this.cancellableRef.getAndSet(null);
+ if (cancellable != null) {
+ cancellable.cancel();
+ }
+ }
+ }
+
+ public boolean isAborted() {
+ return this.aborted.get();
+ }
+
+ /**
+ * @since 4.2
+ */
+ public void setCancellable(final Cancellable cancellable) {
+ if (!this.aborted.get()) {
+ this.cancellableRef.set(cancellable);
+ }
+ }
+
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ final AbstractExecutionAwareRequest clone = (AbstractExecutionAwareRequest) super.clone();
+ clone.headergroup = CloneUtils.cloneObject(this.headergroup);
+ clone.params = CloneUtils.cloneObject(this.params);
+ return clone;
+ }
+
+ /**
+ * @since 4.2
+ */
+ public void completed() {
+ this.cancellableRef.set(null);
+ }
+
+ /**
+ * Resets internal state of the request making it reusable.
+ *
+ * @since 4.2
+ */
+ public void reset() {
+ final Cancellable cancellable = this.cancellableRef.getAndSet(null);
+ if (cancellable != null) {
+ cancellable.cancel();
+ }
+ this.aborted.set(false);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/CloseableHttpResponse.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/CloseableHttpResponse.java
new file mode 100644
index 0000000000..471a11219e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/CloseableHttpResponse.java
@@ -0,0 +1,40 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.methods;
+
+import java.io.Closeable;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+
+/**
+ * Extended version of the {@link HttpResponse} interface that also extends {@link Closeable}.
+ *
+ * @since 4.3
+ */
+public interface CloseableHttpResponse extends HttpResponse, Closeable {
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/Configurable.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/Configurable.java
new file mode 100644
index 0000000000..74f014f207
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/Configurable.java
@@ -0,0 +1,44 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.methods;
+
+import ch.boye.httpclientandroidlib.client.config.RequestConfig;
+
+/**
+ * Configuration interface for HTTP requests.
+ *
+ * @since 4.3
+ */
+public interface Configurable {
+
+ /**
+ * Returns actual request configuration.
+ */
+ RequestConfig getConfig();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpDelete.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpDelete.java
new file mode 100644
index 0000000000..20baf651ce
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpDelete.java
@@ -0,0 +1,77 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.methods;
+
+import java.net.URI;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+
+/**
+ * HTTP DELETE method
+ * <p>
+ * The HTTP DELETE method is defined in section 9.7 of
+ * <a href="http://www.ietf.org/rfc/rfc2616.txt">RFC2616</a>:
+ * <blockquote>
+ * The DELETE method requests that the origin server delete the resource
+ * identified by the Request-URI. [...] The client cannot
+ * be guaranteed that the operation has been carried out, even if the
+ * status code returned from the origin server indicates that the action
+ * has been completed successfully.
+ * </blockquote>
+ *
+ * @since 4.0
+ */
+@NotThreadSafe // HttpRequestBase is @NotThreadSafe
+public class HttpDelete extends HttpRequestBase {
+
+ public final static String METHOD_NAME = "DELETE";
+
+
+ public HttpDelete() {
+ super();
+ }
+
+ public HttpDelete(final URI uri) {
+ super();
+ setURI(uri);
+ }
+
+ /**
+ * @throws IllegalArgumentException if the uri is invalid.
+ */
+ public HttpDelete(final String uri) {
+ super();
+ setURI(URI.create(uri));
+ }
+
+ @Override
+ public String getMethod() {
+ return METHOD_NAME;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpEntityEnclosingRequestBase.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpEntityEnclosingRequestBase.java
new file mode 100644
index 0000000000..cbc035adeb
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpEntityEnclosingRequestBase.java
@@ -0,0 +1,76 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.methods;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.client.utils.CloneUtils;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+
+/**
+ * Basic implementation of an entity enclosing HTTP request
+ * that can be modified
+ *
+ * @since 4.0
+ */
+@NotThreadSafe // HttpRequestBase is @NotThreadSafe
+public abstract class HttpEntityEnclosingRequestBase
+ extends HttpRequestBase implements HttpEntityEnclosingRequest {
+
+ private HttpEntity entity;
+
+ public HttpEntityEnclosingRequestBase() {
+ super();
+ }
+
+ public HttpEntity getEntity() {
+ return this.entity;
+ }
+
+ public void setEntity(final HttpEntity entity) {
+ this.entity = entity;
+ }
+
+ public boolean expectContinue() {
+ final Header expect = getFirstHeader(HTTP.EXPECT_DIRECTIVE);
+ return expect != null && HTTP.EXPECT_CONTINUE.equalsIgnoreCase(expect.getValue());
+ }
+
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ final HttpEntityEnclosingRequestBase clone =
+ (HttpEntityEnclosingRequestBase) super.clone();
+ if (this.entity != null) {
+ clone.entity = CloneUtils.cloneObject(this.entity);
+ }
+ return clone;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpExecutionAware.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpExecutionAware.java
new file mode 100644
index 0000000000..6dab381d3b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpExecutionAware.java
@@ -0,0 +1,47 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.methods;
+
+import ch.boye.httpclientandroidlib.concurrent.Cancellable;
+
+/**
+ * Interface to be implemented by any object that wishes to be notified of
+ * blocking I/O operations that could be cancelled.
+ *
+ * @since 4.3
+ */
+public interface HttpExecutionAware {
+
+ boolean isAborted();
+
+ /**
+ * Sets {@link Cancellable} for the ongoing operation.
+ */
+ void setCancellable(Cancellable cancellable);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpGet.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpGet.java
new file mode 100644
index 0000000000..33de1a8dce
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpGet.java
@@ -0,0 +1,77 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.methods;
+
+import java.net.URI;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+
+/**
+ * HTTP GET method.
+ * <p>
+ * The HTTP GET method is defined in section 9.3 of
+ * <a href="http://www.ietf.org/rfc/rfc2616.txt">RFC2616</a>:
+ * <blockquote>
+ * The GET method means retrieve whatever information (in the form of an
+ * entity) is identified by the Request-URI. If the Request-URI refers
+ * to a data-producing process, it is the produced data which shall be
+ * returned as the entity in the response and not the source text of the
+ * process, unless that text happens to be the output of the process.
+ * </blockquote>
+ * </p>
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class HttpGet extends HttpRequestBase {
+
+ public final static String METHOD_NAME = "GET";
+
+ public HttpGet() {
+ super();
+ }
+
+ public HttpGet(final URI uri) {
+ super();
+ setURI(uri);
+ }
+
+ /**
+ * @throws IllegalArgumentException if the uri is invalid.
+ */
+ public HttpGet(final String uri) {
+ super();
+ setURI(URI.create(uri));
+ }
+
+ @Override
+ public String getMethod() {
+ return METHOD_NAME;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpHead.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpHead.java
new file mode 100644
index 0000000000..58ab5b00b3
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpHead.java
@@ -0,0 +1,80 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.methods;
+
+import java.net.URI;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+
+/**
+ * HTTP HEAD method.
+ * <p>
+ * The HTTP HEAD method is defined in section 9.4 of
+ * <a href="http://www.ietf.org/rfc/rfc2616.txt">RFC2616</a>:
+ * <blockquote>
+ * The HEAD method is identical to GET except that the server MUST NOT
+ * return a message-body in the response. The metainformation contained
+ * in the HTTP headers in response to a HEAD request SHOULD be identical
+ * to the information sent in response to a GET request. This method can
+ * be used for obtaining metainformation about the entity implied by the
+ * request without transferring the entity-body itself. This method is
+ * often used for testing hypertext links for validity, accessibility,
+ * and recent modification.
+ * </blockquote>
+ * </p>
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class HttpHead extends HttpRequestBase {
+
+ public final static String METHOD_NAME = "HEAD";
+
+ public HttpHead() {
+ super();
+ }
+
+ public HttpHead(final URI uri) {
+ super();
+ setURI(uri);
+ }
+
+ /**
+ * @throws IllegalArgumentException if the uri is invalid.
+ */
+ public HttpHead(final String uri) {
+ super();
+ setURI(URI.create(uri));
+ }
+
+ @Override
+ public String getMethod() {
+ return METHOD_NAME;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpOptions.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpOptions.java
new file mode 100644
index 0000000000..af3ee251b4
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpOptions.java
@@ -0,0 +1,100 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.methods;
+
+import java.net.URI;
+import java.util.HashSet;
+import java.util.Set;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.HeaderIterator;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * HTTP OPTIONS method.
+ * <p>
+ * The HTTP OPTIONS method is defined in section 9.2 of
+ * <a href="http://www.ietf.org/rfc/rfc2616.txt">RFC2616</a>:
+ * <blockquote>
+ * The OPTIONS method represents a request for information about the
+ * communication options available on the request/response chain
+ * identified by the Request-URI. This method allows the client to
+ * determine the options and/or requirements associated with a resource,
+ * or the capabilities of a server, without implying a resource action
+ * or initiating a resource retrieval.
+ * </blockquote>
+ * </p>
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class HttpOptions extends HttpRequestBase {
+
+ public final static String METHOD_NAME = "OPTIONS";
+
+ public HttpOptions() {
+ super();
+ }
+
+ public HttpOptions(final URI uri) {
+ super();
+ setURI(uri);
+ }
+
+ /**
+ * @throws IllegalArgumentException if the uri is invalid.
+ */
+ public HttpOptions(final String uri) {
+ super();
+ setURI(URI.create(uri));
+ }
+
+ @Override
+ public String getMethod() {
+ return METHOD_NAME;
+ }
+
+ public Set<String> getAllowedMethods(final HttpResponse response) {
+ Args.notNull(response, "HTTP response");
+
+ final HeaderIterator it = response.headerIterator("Allow");
+ final Set<String> methods = new HashSet<String>();
+ while (it.hasNext()) {
+ final Header header = it.nextHeader();
+ final HeaderElement[] elements = header.getElements();
+ for (final HeaderElement element : elements) {
+ methods.add(element.getName());
+ }
+ }
+ return methods;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpPatch.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpPatch.java
new file mode 100644
index 0000000000..8cfd29fbb5
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpPatch.java
@@ -0,0 +1,75 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.methods;
+
+import java.net.URI;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+
+/**
+ * HTTP PATCH method.
+ * <p>
+ * The HTTP PATCH method is defined in <a
+ * href="http://tools.ietf.org/html/rfc5789">RF5789</a>: <blockquote> The PATCH
+ * method requests that a set of changes described in the request entity be
+ * applied to the resource identified by the Request- URI. Differs from the PUT
+ * method in the way the server processes the enclosed entity to modify the
+ * resource identified by the Request-URI. In a PUT request, the enclosed entity
+ * origin server, and the client is requesting that the stored version be
+ * replaced. With PATCH, however, the enclosed entity contains a set of
+ * instructions describing how a resource currently residing on the origin
+ * server should be modified to produce a new version. </blockquote>
+ * </p>
+ *
+ * @since 4.2
+ */
+@NotThreadSafe
+public class HttpPatch extends HttpEntityEnclosingRequestBase {
+
+ public final static String METHOD_NAME = "PATCH";
+
+ public HttpPatch() {
+ super();
+ }
+
+ public HttpPatch(final URI uri) {
+ super();
+ setURI(uri);
+ }
+
+ public HttpPatch(final String uri) {
+ super();
+ setURI(URI.create(uri));
+ }
+
+ @Override
+ public String getMethod() {
+ return METHOD_NAME;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpPost.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpPost.java
new file mode 100644
index 0000000000..f70538b80f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpPost.java
@@ -0,0 +1,84 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.methods;
+
+import java.net.URI;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+
+/**
+ * HTTP POST method.
+ * <p>
+ * The HTTP POST method is defined in section 9.5 of
+ * <a href="http://www.ietf.org/rfc/rfc2616.txt">RFC2616</a>:
+ * <blockquote>
+ * The POST method is used to request that the origin server accept the entity
+ * enclosed in the request as a new subordinate of the resource identified by
+ * the Request-URI in the Request-Line. POST is designed to allow a uniform
+ * method to cover the following functions:
+ * <ul>
+ * <li>Annotation of existing resources</li>
+ * <li>Posting a message to a bulletin board, newsgroup, mailing list, or
+ * similar group of articles</li>
+ * <li>Providing a block of data, such as the result of submitting a form,
+ * to a data-handling process</li>
+ * <li>Extending a database through an append operation</li>
+ * </ul>
+ * </blockquote>
+ * </p>
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class HttpPost extends HttpEntityEnclosingRequestBase {
+
+ public final static String METHOD_NAME = "POST";
+
+ public HttpPost() {
+ super();
+ }
+
+ public HttpPost(final URI uri) {
+ super();
+ setURI(uri);
+ }
+
+ /**
+ * @throws IllegalArgumentException if the uri is invalid.
+ */
+ public HttpPost(final String uri) {
+ super();
+ setURI(URI.create(uri));
+ }
+
+ @Override
+ public String getMethod() {
+ return METHOD_NAME;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpPut.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpPut.java
new file mode 100644
index 0000000000..4aab04b237
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpPut.java
@@ -0,0 +1,76 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.methods;
+
+import java.net.URI;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+
+/**
+ * HTTP PUT method.
+ * <p>
+ * The HTTP PUT method is defined in section 9.6 of
+ * <a href="http://www.ietf.org/rfc/rfc2616.txt">RFC2616</a>:
+ * <blockquote>
+ * The PUT method requests that the enclosed entity be stored under the
+ * supplied Request-URI. If the Request-URI refers to an already
+ * existing resource, the enclosed entity SHOULD be considered as a
+ * modified version of the one residing on the origin server.
+ * </blockquote>
+ * </p>
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class HttpPut extends HttpEntityEnclosingRequestBase {
+
+ public final static String METHOD_NAME = "PUT";
+
+ public HttpPut() {
+ super();
+ }
+
+ public HttpPut(final URI uri) {
+ super();
+ setURI(uri);
+ }
+
+ /**
+ * @throws IllegalArgumentException if the uri is invalid.
+ */
+ public HttpPut(final String uri) {
+ super();
+ setURI(URI.create(uri));
+ }
+
+ @Override
+ public String getMethod() {
+ return METHOD_NAME;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpRequestBase.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpRequestBase.java
new file mode 100644
index 0000000000..da3b01ef55
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpRequestBase.java
@@ -0,0 +1,124 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.methods;
+
+import java.net.URI;
+
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.RequestLine;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.client.config.RequestConfig;
+import ch.boye.httpclientandroidlib.message.BasicRequestLine;
+import ch.boye.httpclientandroidlib.params.HttpProtocolParams;
+
+/**
+ * Base implementation of {@link HttpUriRequest}.
+ *
+ * @since 4.0
+ */
+@SuppressWarnings("deprecation")
+@NotThreadSafe
+public abstract class HttpRequestBase extends AbstractExecutionAwareRequest
+ implements HttpUriRequest, Configurable {
+
+ private ProtocolVersion version;
+ private URI uri;
+ private RequestConfig config;
+
+ public abstract String getMethod();
+
+ /**
+ * @since 4.3
+ */
+ public void setProtocolVersion(final ProtocolVersion version) {
+ this.version = version;
+ }
+
+ public ProtocolVersion getProtocolVersion() {
+ return version != null ? version : HttpProtocolParams.getVersion(getParams());
+ }
+
+ /**
+ * Returns the original request URI.
+ * <p>
+ * Please note URI remains unchanged in the course of request execution and
+ * is not updated if the request is redirected to another location.
+ */
+ public URI getURI() {
+ return this.uri;
+ }
+
+ public RequestLine getRequestLine() {
+ final String method = getMethod();
+ final ProtocolVersion ver = getProtocolVersion();
+ final URI uri = getURI();
+ String uritext = null;
+ if (uri != null) {
+ uritext = uri.toASCIIString();
+ }
+ if (uritext == null || uritext.length() == 0) {
+ uritext = "/";
+ }
+ return new BasicRequestLine(method, uritext, ver);
+ }
+
+
+ public RequestConfig getConfig() {
+ return config;
+ }
+
+ public void setConfig(final RequestConfig config) {
+ this.config = config;
+ }
+
+ public void setURI(final URI uri) {
+ this.uri = uri;
+ }
+
+ /**
+ * @since 4.2
+ */
+ public void started() {
+ }
+
+ /**
+ * A convenience method to simplify migration from HttpClient 3.1 API. This method is
+ * equivalent to {@link #reset()}.
+ *
+ * @since 4.2
+ */
+ public void releaseConnection() {
+ reset();
+ }
+
+ @Override
+ public String toString() {
+ return getMethod() + " " + getURI() + " " + getProtocolVersion();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpRequestWrapper.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpRequestWrapper.java
new file mode 100644
index 0000000000..72b7e2c555
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpRequestWrapper.java
@@ -0,0 +1,171 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.methods;
+
+import java.net.URI;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.RequestLine;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.message.AbstractHttpMessage;
+import ch.boye.httpclientandroidlib.message.BasicRequestLine;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+
+/**
+ * A wrapper class for {@link HttpRequest} that can be used to change properties of the current
+ * request without modifying the original object.
+ *
+ * @since 4.3
+ */
+@SuppressWarnings("deprecation")
+@NotThreadSafe
+public class HttpRequestWrapper extends AbstractHttpMessage implements HttpUriRequest {
+
+ private final HttpRequest original;
+ private final String method;
+ private ProtocolVersion version;
+ private URI uri;
+
+ private HttpRequestWrapper(final HttpRequest request) {
+ super();
+ this.original = request;
+ this.version = this.original.getRequestLine().getProtocolVersion();
+ this.method = this.original.getRequestLine().getMethod();
+ if (request instanceof HttpUriRequest) {
+ this.uri = ((HttpUriRequest) request).getURI();
+ } else {
+ this.uri = null;
+ }
+ setHeaders(request.getAllHeaders());
+ }
+
+ public ProtocolVersion getProtocolVersion() {
+ return this.version != null ? this.version : this.original.getProtocolVersion();
+ }
+
+ public void setProtocolVersion(final ProtocolVersion version) {
+ this.version = version;
+ }
+
+ public URI getURI() {
+ return this.uri;
+ }
+
+ public void setURI(final URI uri) {
+ this.uri = uri;
+ }
+
+ public String getMethod() {
+ return method;
+ }
+
+ public void abort() throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean isAborted() {
+ return false;
+ }
+
+ public RequestLine getRequestLine() {
+ String requestUri = null;
+ if (this.uri != null) {
+ requestUri = this.uri.toASCIIString();
+ } else {
+ requestUri = this.original.getRequestLine().getUri();
+ }
+ if (requestUri == null || requestUri.length() == 0) {
+ requestUri = "/";
+ }
+ return new BasicRequestLine(this.method, requestUri, getProtocolVersion());
+ }
+
+ public HttpRequest getOriginal() {
+ return this.original;
+ }
+
+ @Override
+ public String toString() {
+ return getRequestLine() + " " + this.headergroup;
+ }
+
+ static class HttpEntityEnclosingRequestWrapper extends HttpRequestWrapper
+ implements HttpEntityEnclosingRequest {
+
+ private HttpEntity entity;
+
+ public HttpEntityEnclosingRequestWrapper(final HttpEntityEnclosingRequest request) {
+ super(request);
+ this.entity = request.getEntity();
+ }
+
+ public HttpEntity getEntity() {
+ return this.entity;
+ }
+
+ public void setEntity(final HttpEntity entity) {
+ this.entity = entity;
+ }
+
+ public boolean expectContinue() {
+ final Header expect = getFirstHeader(HTTP.EXPECT_DIRECTIVE);
+ return expect != null && HTTP.EXPECT_CONTINUE.equalsIgnoreCase(expect.getValue());
+ }
+
+ }
+
+ public static HttpRequestWrapper wrap(final HttpRequest request) {
+ if (request == null) {
+ return null;
+ }
+ if (request instanceof HttpEntityEnclosingRequest) {
+ return new HttpEntityEnclosingRequestWrapper((HttpEntityEnclosingRequest) request);
+ } else {
+ return new HttpRequestWrapper(request);
+ }
+ }
+
+ /**
+ * @deprecated (4.3) use
+ * {@link ch.boye.httpclientandroidlib.client.config.RequestConfig}.
+ */
+ @Override
+ @Deprecated
+ public HttpParams getParams() {
+ if (this.params == null) {
+ this.params = original.getParams().copy();
+ }
+ return this.params;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpTrace.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpTrace.java
new file mode 100644
index 0000000000..1118ea181c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpTrace.java
@@ -0,0 +1,79 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.methods;
+
+import java.net.URI;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+
+/**
+ * HTTP TRACE method.
+ * <p>
+ * The HTTP TRACE method is defined in section 9.6 of
+ * <a href="http://www.ietf.org/rfc/rfc2616.txt">RFC2616</a>:
+ * <blockquote>
+ * The TRACE method is used to invoke a remote, application-layer loop-
+ * back of the request message. The final recipient of the request
+ * SHOULD reflect the message received back to the client as the
+ * entity-body of a 200 (OK) response. The final recipient is either the
+ * origin server or the first proxy or gateway to receive a Max-Forwards
+ * value of zero (0) in the request (see section 14.31). A TRACE request
+ * MUST NOT include an entity.
+ * </blockquote>
+ * </p>
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class HttpTrace extends HttpRequestBase {
+
+ public final static String METHOD_NAME = "TRACE";
+
+ public HttpTrace() {
+ super();
+ }
+
+ public HttpTrace(final URI uri) {
+ super();
+ setURI(uri);
+ }
+
+ /**
+ * @throws IllegalArgumentException if the uri is invalid.
+ */
+ public HttpTrace(final String uri) {
+ super();
+ setURI(URI.create(uri));
+ }
+
+ @Override
+ public String getMethod() {
+ return METHOD_NAME;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpUriRequest.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpUriRequest.java
new file mode 100644
index 0000000000..8ab14ef856
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/HttpUriRequest.java
@@ -0,0 +1,84 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.methods;
+
+import java.net.URI;
+
+import ch.boye.httpclientandroidlib.HttpRequest;
+
+/**
+ * Extended version of the {@link HttpRequest} interface that provides
+ * convenience methods to access request properties such as request URI
+ * and method type.
+ *
+ * @since 4.0
+ */
+public interface HttpUriRequest extends HttpRequest {
+
+ /**
+ * Returns the HTTP method this request uses, such as <code>GET</code>,
+ * <code>PUT</code>, <code>POST</code>, or other.
+ */
+ String getMethod();
+
+ /**
+ * Returns the URI this request uses, such as
+ * <code>http://example.org/path/to/file</code>.
+ * <br/>
+ * Note that the URI may be absolute URI (as above) or may be a relative URI.
+ * <p>
+ * Implementations are encouraged to return
+ * the URI that was initially requested.
+ * </p>
+ * <p>
+ * To find the final URI after any redirects have been processed,
+ * please see the section entitled
+ * <a href="http://hc.apache.org/httpcomponents-client-ga/tutorial/html/fundamentals.html#d4e205">HTTP execution context</a>
+ * in the
+ * <a href="http://hc.apache.org/httpcomponents-client-ga/tutorial/html">HttpClient Tutorial</a>
+ * </p>
+ */
+ URI getURI();
+
+ /**
+ * Aborts execution of the request.
+ *
+ * @throws UnsupportedOperationException if the abort operation
+ * is not supported / cannot be implemented.
+ */
+ void abort() throws UnsupportedOperationException;
+
+ /**
+ * Tests if the request execution has been aborted.
+ *
+ * @return <code>true</code> if the request execution has been aborted,
+ * <code>false</code> otherwise.
+ */
+ boolean isAborted();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/RequestBuilder.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/RequestBuilder.java
new file mode 100644
index 0000000000..3a45dcd2f9
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/RequestBuilder.java
@@ -0,0 +1,351 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.methods;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderIterator;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.client.config.RequestConfig;
+import ch.boye.httpclientandroidlib.client.entity.UrlEncodedFormEntity;
+import ch.boye.httpclientandroidlib.client.utils.URIBuilder;
+import ch.boye.httpclientandroidlib.message.BasicHeader;
+import ch.boye.httpclientandroidlib.message.BasicNameValuePair;
+import ch.boye.httpclientandroidlib.message.HeaderGroup;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Builder for {@link HttpUriRequest} instances.
+ * <p/>
+ * Please note that this class treats parameters differently depending on composition
+ * of the request: if the request has a content entity explicitly set with
+ * {@link #setEntity(ch.boye.httpclientandroidlib.HttpEntity)} or it is not an entity enclosing method
+ * (such as POST or PUT), parameters will be added to the query component of the request URI.
+ * Otherwise, parameters will be added as a URL encoded {@link UrlEncodedFormEntity entity}.
+ *
+ * @since 4.3
+ */
+@NotThreadSafe
+public class RequestBuilder {
+
+ private String method;
+ private ProtocolVersion version;
+ private URI uri;
+ private HeaderGroup headergroup;
+ private HttpEntity entity;
+ private LinkedList<NameValuePair> parameters;
+ private RequestConfig config;
+
+ RequestBuilder(final String method) {
+ super();
+ this.method = method;
+ }
+
+ RequestBuilder() {
+ this(null);
+ }
+
+ public static RequestBuilder create(final String method) {
+ Args.notBlank(method, "HTTP method");
+ return new RequestBuilder(method);
+ }
+
+ public static RequestBuilder get() {
+ return new RequestBuilder(HttpGet.METHOD_NAME);
+ }
+
+ public static RequestBuilder head() {
+ return new RequestBuilder(HttpHead.METHOD_NAME);
+ }
+
+ public static RequestBuilder post() {
+ return new RequestBuilder(HttpPost.METHOD_NAME);
+ }
+
+ public static RequestBuilder put() {
+ return new RequestBuilder(HttpPut.METHOD_NAME);
+ }
+
+ public static RequestBuilder delete() {
+ return new RequestBuilder(HttpDelete.METHOD_NAME);
+ }
+
+ public static RequestBuilder trace() {
+ return new RequestBuilder(HttpTrace.METHOD_NAME);
+ }
+
+ public static RequestBuilder options() {
+ return new RequestBuilder(HttpOptions.METHOD_NAME);
+ }
+
+ public static RequestBuilder copy(final HttpRequest request) {
+ Args.notNull(request, "HTTP request");
+ return new RequestBuilder().doCopy(request);
+ }
+
+ private RequestBuilder doCopy(final HttpRequest request) {
+ if (request == null) {
+ return this;
+ }
+ method = request.getRequestLine().getMethod();
+ version = request.getRequestLine().getProtocolVersion();
+ if (request instanceof HttpUriRequest) {
+ uri = ((HttpUriRequest) request).getURI();
+ } else {
+ uri = URI.create(request.getRequestLine().getUri());
+ }
+ if (headergroup == null) {
+ headergroup = new HeaderGroup();
+ }
+ headergroup.clear();
+ headergroup.setHeaders(request.getAllHeaders());
+ if (request instanceof HttpEntityEnclosingRequest) {
+ entity = ((HttpEntityEnclosingRequest) request).getEntity();
+ } else {
+ entity = null;
+ }
+ if (request instanceof Configurable) {
+ this.config = ((Configurable) request).getConfig();
+ } else {
+ this.config = null;
+ }
+ this.parameters = null;
+ return this;
+ }
+
+ public String getMethod() {
+ return method;
+ }
+
+ public ProtocolVersion getVersion() {
+ return version;
+ }
+
+ public RequestBuilder setVersion(final ProtocolVersion version) {
+ this.version = version;
+ return this;
+ }
+
+ public URI getUri() {
+ return uri;
+ }
+
+ public RequestBuilder setUri(final URI uri) {
+ this.uri = uri;
+ return this;
+ }
+
+ public RequestBuilder setUri(final String uri) {
+ this.uri = uri != null ? URI.create(uri) : null;
+ return this;
+ }
+
+ public Header getFirstHeader(final String name) {
+ return headergroup != null ? headergroup.getFirstHeader(name) : null;
+ }
+
+ public Header getLastHeader(final String name) {
+ return headergroup != null ? headergroup.getLastHeader(name) : null;
+ }
+
+ public Header[] getHeaders(final String name) {
+ return headergroup != null ? headergroup.getHeaders(name) : null;
+ }
+
+ public RequestBuilder addHeader(final Header header) {
+ if (headergroup == null) {
+ headergroup = new HeaderGroup();
+ }
+ headergroup.addHeader(header);
+ return this;
+ }
+
+ public RequestBuilder addHeader(final String name, final String value) {
+ if (headergroup == null) {
+ headergroup = new HeaderGroup();
+ }
+ this.headergroup.addHeader(new BasicHeader(name, value));
+ return this;
+ }
+
+ public RequestBuilder removeHeader(final Header header) {
+ if (headergroup == null) {
+ headergroup = new HeaderGroup();
+ }
+ headergroup.removeHeader(header);
+ return this;
+ }
+
+ public RequestBuilder removeHeaders(final String name) {
+ if (name == null || headergroup == null) {
+ return this;
+ }
+ for (final HeaderIterator i = headergroup.iterator(); i.hasNext(); ) {
+ final Header header = i.nextHeader();
+ if (name.equalsIgnoreCase(header.getName())) {
+ i.remove();
+ }
+ }
+ return this;
+ }
+
+ public RequestBuilder setHeader(final Header header) {
+ if (headergroup == null) {
+ headergroup = new HeaderGroup();
+ }
+ this.headergroup.updateHeader(header);
+ return this;
+ }
+
+ public RequestBuilder setHeader(final String name, final String value) {
+ if (headergroup == null) {
+ headergroup = new HeaderGroup();
+ }
+ this.headergroup.updateHeader(new BasicHeader(name, value));
+ return this;
+ }
+
+ public HttpEntity getEntity() {
+ return entity;
+ }
+
+ public RequestBuilder setEntity(final HttpEntity entity) {
+ this.entity = entity;
+ return this;
+ }
+
+ public List<NameValuePair> getParameters() {
+ return parameters != null ? new ArrayList<NameValuePair>(parameters) :
+ new ArrayList<NameValuePair>();
+ }
+
+ public RequestBuilder addParameter(final NameValuePair nvp) {
+ Args.notNull(nvp, "Name value pair");
+ if (parameters == null) {
+ parameters = new LinkedList<NameValuePair>();
+ }
+ parameters.add(nvp);
+ return this;
+ }
+
+ public RequestBuilder addParameter(final String name, final String value) {
+ return addParameter(new BasicNameValuePair(name, value));
+ }
+
+ public RequestBuilder addParameters(final NameValuePair... nvps) {
+ for (final NameValuePair nvp: nvps) {
+ addParameter(nvp);
+ }
+ return this;
+ }
+
+ public RequestConfig getConfig() {
+ return config;
+ }
+
+ public RequestBuilder setConfig(final RequestConfig config) {
+ this.config = config;
+ return this;
+ }
+
+ public HttpUriRequest build() {
+ final HttpRequestBase result;
+ URI uri = this.uri != null ? this.uri : URI.create("/");
+ HttpEntity entity = this.entity;
+ if (parameters != null && !parameters.isEmpty()) {
+ if (entity == null && (HttpPost.METHOD_NAME.equalsIgnoreCase(method)
+ || HttpPut.METHOD_NAME.equalsIgnoreCase(method))) {
+ entity = new UrlEncodedFormEntity(parameters, HTTP.DEF_CONTENT_CHARSET);
+ } else {
+ try {
+ uri = new URIBuilder(uri).addParameters(parameters).build();
+ } catch (final URISyntaxException ex) {
+ // should never happen
+ }
+ }
+ }
+ if (entity == null) {
+ result = new InternalRequest(method);
+ } else {
+ final InternalEntityEclosingRequest request = new InternalEntityEclosingRequest(method);
+ request.setEntity(entity);
+ result = request;
+ }
+ result.setProtocolVersion(this.version);
+ result.setURI(uri);
+ if (this.headergroup != null) {
+ result.setHeaders(this.headergroup.getAllHeaders());
+ }
+ result.setConfig(this.config);
+ return result;
+ }
+
+ static class InternalRequest extends HttpRequestBase {
+
+ private final String method;
+
+ InternalRequest(final String method) {
+ super();
+ this.method = method;
+ }
+
+ @Override
+ public String getMethod() {
+ return this.method;
+ }
+
+ }
+
+ static class InternalEntityEclosingRequest extends HttpEntityEnclosingRequestBase {
+
+ private final String method;
+
+ InternalEntityEclosingRequest(final String method) {
+ super();
+ this.method = method;
+ }
+
+ @Override
+ public String getMethod() {
+ return this.method;
+ }
+
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/package-info.java
new file mode 100644
index 0000000000..d8266b2e37
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/methods/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Standard HTTP method implementations.
+ */
+package ch.boye.httpclientandroidlib.client.methods;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/package-info.java
new file mode 100644
index 0000000000..2d49a2b993
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Client HTTP communication APIs.
+ */
+package ch.boye.httpclientandroidlib.client;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/AllClientPNames.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/AllClientPNames.java
new file mode 100644
index 0000000000..aaf591571c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/AllClientPNames.java
@@ -0,0 +1,63 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.params;
+
+import ch.boye.httpclientandroidlib.auth.params.AuthPNames;
+import ch.boye.httpclientandroidlib.conn.params.ConnConnectionPNames;
+import ch.boye.httpclientandroidlib.conn.params.ConnManagerPNames;
+import ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames;
+import ch.boye.httpclientandroidlib.cookie.params.CookieSpecPNames;
+import ch.boye.httpclientandroidlib.params.CoreConnectionPNames;
+import ch.boye.httpclientandroidlib.params.CoreProtocolPNames;
+
+/**
+ * Collected parameter names for the HttpClient module.
+ * This interface combines the parameter definitions of the HttpClient
+ * module and all dependency modules or informational units.
+ * It does not define additional parameter names, but references
+ * other interfaces defining parameter names.
+ * <br/>
+ * This interface is meant as a navigation aid for developers.
+ * When referring to parameter names, you should use the interfaces
+ * in which the respective constants are actually defined.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use
+ * {@link ch.boye.httpclientandroidlib.client.config.RequestConfig},
+ * {@link ch.boye.httpclientandroidlib.config.ConnectionConfig},
+ * {@link ch.boye.httpclientandroidlib.config.SocketConfig}
+ */
+@Deprecated
+public interface AllClientPNames extends
+ CoreConnectionPNames, CoreProtocolPNames,
+ ClientPNames, AuthPNames, CookieSpecPNames,
+ ConnConnectionPNames, ConnManagerPNames, ConnRoutePNames {
+
+ // no additional definitions
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/AuthPolicy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/AuthPolicy.java
new file mode 100644
index 0000000000..b677ee02fa
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/AuthPolicy.java
@@ -0,0 +1,79 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.params;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Standard authentication schemes supported by HttpClient.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.client.config.AuthSchemes}.
+ */
+@Deprecated
+@Immutable
+public final class AuthPolicy {
+
+ private AuthPolicy() {
+ super();
+ }
+
+ /**
+ * The NTLM scheme is a proprietary Microsoft Windows Authentication
+ * protocol (considered to be the most secure among currently supported
+ * authentication schemes).
+ */
+ public static final String NTLM = "NTLM";
+
+ /**
+ * Digest authentication scheme as defined in RFC2617.
+ */
+ public static final String DIGEST = "Digest";
+
+ /**
+ * Basic authentication scheme as defined in RFC2617 (considered inherently
+ * insecure, but most widely supported)
+ */
+ public static final String BASIC = "Basic";
+
+ /**
+ * SPNEGO Authentication scheme.
+ *
+ * @since 4.1
+ */
+ public static final String SPNEGO = "negotiate";
+
+ /**
+ * Kerberos Authentication scheme.
+ *
+ * @since 4.2
+ */
+ public static final String KERBEROS = "Kerberos";
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/ClientPNames.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/ClientPNames.java
new file mode 100644
index 0000000000..796ee40fbb
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/ClientPNames.java
@@ -0,0 +1,133 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.params;
+
+/**
+ * Parameter names for HTTP client parameters.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.client.config.RequestConfig}.
+ */
+@Deprecated
+public interface ClientPNames {
+
+ public static final String CONNECTION_MANAGER_FACTORY_CLASS_NAME = "http.connection-manager.factory-class-name";
+
+ /**
+ * Defines whether redirects should be handled automatically
+ * <p>
+ * This parameter expects a value of type {@link Boolean}.
+ * </p>
+ */
+ public static final String HANDLE_REDIRECTS = "http.protocol.handle-redirects";
+
+ /**
+ * Defines whether relative redirects should be rejected. HTTP specification
+ * requires the location value be an absolute URI.
+ * <p>
+ * This parameter expects a value of type {@link Boolean}.
+ * </p>
+ */
+ public static final String REJECT_RELATIVE_REDIRECT = "http.protocol.reject-relative-redirect";
+
+ /**
+ * Defines the maximum number of redirects to be followed.
+ * The limit on number of redirects is intended to prevent infinite loops.
+ * <p>
+ * This parameter expects a value of type {@link Integer}.
+ * </p>
+ */
+ public static final String MAX_REDIRECTS = "http.protocol.max-redirects";
+
+ /**
+ * Defines whether circular redirects (redirects to the same location) should be allowed.
+ * The HTTP spec is not sufficiently clear whether circular redirects are permitted,
+ * therefore optionally they can be enabled
+ * <p>
+ * This parameter expects a value of type {@link Boolean}.
+ * </p>
+ */
+ public static final String ALLOW_CIRCULAR_REDIRECTS = "http.protocol.allow-circular-redirects";
+
+ /**
+ * Defines whether authentication should be handled automatically.
+ * <p>
+ * This parameter expects a value of type {@link Boolean}.
+ * </p>
+ */
+ public static final String HANDLE_AUTHENTICATION = "http.protocol.handle-authentication";
+
+ /**
+ * Defines the name of the cookie specification to be used for HTTP state management.
+ * <p>
+ * This parameter expects a value of type {@link String}.
+ * </p>
+ */
+ public static final String COOKIE_POLICY = "http.protocol.cookie-policy";
+
+ /**
+ * Defines the virtual host to be used in the <code>Host</code>
+ * request header instead of the physical host.
+ * <p>
+ * This parameter expects a value of type {@link ch.boye.httpclientandroidlib.HttpHost}.
+ * </p>
+ * If a port is not provided, it will be derived from the request URL.
+ */
+ public static final String VIRTUAL_HOST = "http.virtual-host";
+
+ /**
+ * Defines the request headers to be sent per default with each request.
+ * <p>
+ * This parameter expects a value of type {@link java.util.Collection}. The
+ * collection is expected to contain {@link ch.boye.httpclientandroidlib.Header}s.
+ * </p>
+ */
+ public static final String DEFAULT_HEADERS = "http.default-headers";
+
+ /**
+ * Defines the default host. The default value will be used if the target host is
+ * not explicitly specified in the request URI.
+ * <p>
+ * This parameter expects a value of type {@link ch.boye.httpclientandroidlib.HttpHost}.
+ * </p>
+ */
+ public static final String DEFAULT_HOST = "http.default-host";
+
+ /**
+ * Defines the timeout in milliseconds used when retrieving an instance of
+ * {@link ch.boye.httpclientandroidlib.conn.ManagedClientConnection} from the
+ * {@link ch.boye.httpclientandroidlib.conn.ClientConnectionManager}.
+ * <p>
+ * This parameter expects a value of type {@link Long}.
+ * <p>
+ * @since 4.2
+ */
+ public static final String CONN_MANAGER_TIMEOUT = "http.conn-manager.timeout";
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/ClientParamBean.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/ClientParamBean.java
new file mode 100644
index 0000000000..315129d23e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/ClientParamBean.java
@@ -0,0 +1,106 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.params;
+
+import java.util.Collection;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.params.HttpAbstractParamBean;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+/**
+ * This is a Java Bean class that can be used to wrap an instance of
+ * {@link HttpParams} and manipulate HTTP client parameters using
+ * Java Beans conventions.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.client.config.RequestConfig}.
+ */
+@Deprecated
+@NotThreadSafe
+public class ClientParamBean extends HttpAbstractParamBean {
+
+ public ClientParamBean (final HttpParams params) {
+ super(params);
+ }
+
+ /**
+ * @deprecated (4.2) do not use.
+ */
+ @Deprecated
+ public void setConnectionManagerFactoryClassName (final String factory) {
+ params.setParameter(ClientPNames.CONNECTION_MANAGER_FACTORY_CLASS_NAME, factory);
+ }
+
+ public void setHandleRedirects (final boolean handle) {
+ params.setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, handle);
+ }
+
+ public void setRejectRelativeRedirect (final boolean reject) {
+ params.setBooleanParameter(ClientPNames.REJECT_RELATIVE_REDIRECT, reject);
+ }
+
+ public void setMaxRedirects (final int maxRedirects) {
+ params.setIntParameter(ClientPNames.MAX_REDIRECTS, maxRedirects);
+ }
+
+ public void setAllowCircularRedirects (final boolean allow) {
+ params.setBooleanParameter(ClientPNames.ALLOW_CIRCULAR_REDIRECTS, allow);
+ }
+
+ public void setHandleAuthentication (final boolean handle) {
+ params.setBooleanParameter(ClientPNames.HANDLE_AUTHENTICATION, handle);
+ }
+
+ public void setCookiePolicy (final String policy) {
+ params.setParameter(ClientPNames.COOKIE_POLICY, policy);
+ }
+
+ public void setVirtualHost (final HttpHost host) {
+ params.setParameter(ClientPNames.VIRTUAL_HOST, host);
+ }
+
+ public void setDefaultHeaders (final Collection <Header> headers) {
+ params.setParameter(ClientPNames.DEFAULT_HEADERS, headers);
+ }
+
+ public void setDefaultHost (final HttpHost host) {
+ params.setParameter(ClientPNames.DEFAULT_HOST, host);
+ }
+
+ /**
+ * @since 4.2
+ */
+ public void setConnectionManagerTimeout(final long timeout) {
+ params.setLongParameter(ClientPNames.CONN_MANAGER_TIMEOUT, timeout);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/CookiePolicy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/CookiePolicy.java
new file mode 100644
index 0000000000..5c6353b800
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/CookiePolicy.java
@@ -0,0 +1,80 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.params;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Standard cookie specifications supported by HttpClient.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.client.config.CookieSpecs}.
+ */
+@Deprecated
+@Immutable
+public final class CookiePolicy {
+
+ /**
+ * The policy that provides high degree of compatibilty
+ * with common cookie management of popular HTTP agents.
+ */
+ public static final String BROWSER_COMPATIBILITY = "compatibility";
+
+ /**
+ * The Netscape cookie draft compliant policy.
+ */
+ public static final String NETSCAPE = "netscape";
+
+ /**
+ * The RFC 2109 compliant policy.
+ */
+ public static final String RFC_2109 = "rfc2109";
+
+ /**
+ * The RFC 2965 compliant policy.
+ */
+ public static final String RFC_2965 = "rfc2965";
+
+ /**
+ * The default 'best match' policy.
+ */
+ public static final String BEST_MATCH = "best-match";
+
+ /**
+ * The policy that ignores cookies.
+ *
+ * @since 4.1-beta1
+ */
+ public static final String IGNORE_COOKIES = "ignoreCookies";
+
+ private CookiePolicy() {
+ super();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/HttpClientParamConfig.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/HttpClientParamConfig.java
new file mode 100644
index 0000000000..515d92ca5f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/HttpClientParamConfig.java
@@ -0,0 +1,88 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.params;
+
+import java.net.InetAddress;
+import java.util.Collection;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.auth.params.AuthPNames;
+import ch.boye.httpclientandroidlib.client.config.RequestConfig;
+import ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames;
+import ch.boye.httpclientandroidlib.params.CoreConnectionPNames;
+import ch.boye.httpclientandroidlib.params.CoreProtocolPNames;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+/**
+ * @deprecated (4.3) provided for compatibility with {@link HttpParams}. Do not use.
+ *
+ * @since 4.3
+ */
+@Deprecated
+public final class HttpClientParamConfig {
+
+ private HttpClientParamConfig() {
+ }
+
+ @SuppressWarnings("unchecked")
+ public static RequestConfig getRequestConfig(final HttpParams params) {
+ return RequestConfig.custom()
+ .setSocketTimeout(params.getIntParameter(
+ CoreConnectionPNames.SO_TIMEOUT, 0))
+ .setStaleConnectionCheckEnabled(params.getBooleanParameter(
+ CoreConnectionPNames.STALE_CONNECTION_CHECK, true))
+ .setConnectTimeout(params.getIntParameter(
+ CoreConnectionPNames.CONNECTION_TIMEOUT, 0))
+ .setExpectContinueEnabled(params.getBooleanParameter(
+ CoreProtocolPNames.USE_EXPECT_CONTINUE, false))
+ .setProxy((HttpHost) params.getParameter(
+ ConnRoutePNames.DEFAULT_PROXY))
+ .setLocalAddress((InetAddress) params.getParameter(
+ ConnRoutePNames.LOCAL_ADDRESS))
+ .setProxyPreferredAuthSchemes((Collection<String>) params.getParameter(
+ AuthPNames.PROXY_AUTH_PREF))
+ .setTargetPreferredAuthSchemes((Collection<String>) params.getParameter(
+ AuthPNames.TARGET_AUTH_PREF))
+ .setAuthenticationEnabled(params.getBooleanParameter(
+ ClientPNames.HANDLE_AUTHENTICATION, true))
+ .setCircularRedirectsAllowed(params.getBooleanParameter(
+ ClientPNames.ALLOW_CIRCULAR_REDIRECTS, false))
+ .setConnectionRequestTimeout((int) params.getLongParameter(
+ ClientPNames.CONN_MANAGER_TIMEOUT, 0))
+ .setCookieSpec((String) params.getParameter(
+ ClientPNames.COOKIE_POLICY))
+ .setMaxRedirects(params.getIntParameter(
+ ClientPNames.MAX_REDIRECTS, 50))
+ .setRedirectsEnabled(params.getBooleanParameter(
+ ClientPNames.HANDLE_REDIRECTS, true))
+ .setRelativeRedirectsAllowed(!params.getBooleanParameter(
+ ClientPNames.REJECT_RELATIVE_REDIRECT, false))
+ .build();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/HttpClientParams.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/HttpClientParams.java
new file mode 100644
index 0000000000..b2b2a573ee
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/HttpClientParams.java
@@ -0,0 +1,116 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.params;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.params.HttpConnectionParams;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * An adaptor for manipulating HTTP client parameters in {@link HttpParams}.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.client.config.RequestConfig}
+ */
+@Deprecated
+@Immutable
+public class HttpClientParams {
+
+ private HttpClientParams() {
+ super();
+ }
+
+ public static boolean isRedirecting(final HttpParams params) {
+ Args.notNull(params, "HTTP parameters");
+ return params.getBooleanParameter
+ (ClientPNames.HANDLE_REDIRECTS, true);
+ }
+
+ public static void setRedirecting(final HttpParams params, final boolean value) {
+ Args.notNull(params, "HTTP parameters");
+ params.setBooleanParameter
+ (ClientPNames.HANDLE_REDIRECTS, value);
+ }
+
+ public static boolean isAuthenticating(final HttpParams params) {
+ Args.notNull(params, "HTTP parameters");
+ return params.getBooleanParameter
+ (ClientPNames.HANDLE_AUTHENTICATION, true);
+ }
+
+ public static void setAuthenticating(final HttpParams params, final boolean value) {
+ Args.notNull(params, "HTTP parameters");
+ params.setBooleanParameter
+ (ClientPNames.HANDLE_AUTHENTICATION, value);
+ }
+
+ public static String getCookiePolicy(final HttpParams params) {
+ Args.notNull(params, "HTTP parameters");
+ final String cookiePolicy = (String)
+ params.getParameter(ClientPNames.COOKIE_POLICY);
+ if (cookiePolicy == null) {
+ return CookiePolicy.BEST_MATCH;
+ }
+ return cookiePolicy;
+ }
+
+ public static void setCookiePolicy(final HttpParams params, final String cookiePolicy) {
+ Args.notNull(params, "HTTP parameters");
+ params.setParameter(ClientPNames.COOKIE_POLICY, cookiePolicy);
+ }
+
+ /**
+ * Set the parameter {@code ClientPNames.CONN_MANAGER_TIMEOUT}.
+ *
+ * @since 4.2
+ */
+ public static void setConnectionManagerTimeout(final HttpParams params, final long timeout) {
+ Args.notNull(params, "HTTP parameters");
+ params.setLongParameter(ClientPNames.CONN_MANAGER_TIMEOUT, timeout);
+ }
+
+ /**
+ * Get the connectiion manager timeout value.
+ * This is defined by the parameter {@code ClientPNames.CONN_MANAGER_TIMEOUT}.
+ * Failing that it uses the parameter {@code CoreConnectionPNames.CONNECTION_TIMEOUT}
+ * which defaults to 0 if not defined.
+ *
+ * @since 4.2
+ * @return the timeout value
+ */
+ public static long getConnectionManagerTimeout(final HttpParams params) {
+ Args.notNull(params, "HTTP parameters");
+ final Long timeout = (Long) params.getParameter(ClientPNames.CONN_MANAGER_TIMEOUT);
+ if (timeout != null) {
+ return timeout.longValue();
+ }
+ return HttpConnectionParams.getConnectionTimeout(params);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/package-info.java
new file mode 100644
index 0000000000..3450f368e3
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/params/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Deprecated.
+ * @deprecated (4.3).
+ */
+package ch.boye.httpclientandroidlib.client.params;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/ClientContext.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/ClientContext.java
new file mode 100644
index 0000000000..1409362f07
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/ClientContext.java
@@ -0,0 +1,132 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.protocol;
+
+/**
+ * {@link ch.boye.httpclientandroidlib.protocol.HttpContext} attribute names for
+ * client side HTTP protocol processing.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link HttpClientContext}.
+ */
+@Deprecated
+public interface ClientContext {
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.conn.routing.RouteInfo}
+ * object that represents the actual connection route.
+ *
+ * @since 4.3
+ */
+ public static final String ROUTE = "http.route";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.conn.scheme.Scheme}
+ * object that represents the actual protocol scheme registry.
+ */
+ @Deprecated
+ public static final String SCHEME_REGISTRY = "http.scheme-registry";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.config.Lookup} object that represents
+ * the actual {@link ch.boye.httpclientandroidlib.cookie.CookieSpecRegistry} registry.
+ */
+ public static final String COOKIESPEC_REGISTRY = "http.cookiespec-registry";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.cookie.CookieSpec}
+ * object that represents the actual cookie specification.
+ */
+ public static final String COOKIE_SPEC = "http.cookie-spec";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.cookie.CookieOrigin}
+ * object that represents the actual details of the origin server.
+ */
+ public static final String COOKIE_ORIGIN = "http.cookie-origin";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.client.CookieStore}
+ * object that represents the actual cookie store.
+ */
+ public static final String COOKIE_STORE = "http.cookie-store";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.client.CredentialsProvider}
+ * object that represents the actual credentials provider.
+ */
+ public static final String CREDS_PROVIDER = "http.auth.credentials-provider";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.client.AuthCache} object
+ * that represents the auth scheme cache.
+ */
+ public static final String AUTH_CACHE = "http.auth.auth-cache";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.auth.AuthState}
+ * object that represents the actual target authentication state.
+ */
+ public static final String TARGET_AUTH_STATE = "http.auth.target-scope";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.auth.AuthState}
+ * object that represents the actual proxy authentication state.
+ */
+ public static final String PROXY_AUTH_STATE = "http.auth.proxy-scope";
+
+ /**
+ * @deprecated (4.1) do not use
+ */
+ @Deprecated
+ public static final String AUTH_SCHEME_PREF = "http.auth.scheme-pref";
+
+ /**
+ * Attribute name of a {@link java.lang.Object} object that represents
+ * the actual user identity such as user {@link java.security.Principal}.
+ */
+ public static final String USER_TOKEN = "http.user-token";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.config.Lookup} object that represents
+ * the actual {@link ch.boye.httpclientandroidlib.auth.AuthSchemeRegistry} registry.
+ */
+ public static final String AUTHSCHEME_REGISTRY = "http.authscheme-registry";
+
+ public static final String SOCKET_FACTORY_REGISTRY = "http.socket-factory-registry";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.client.config.RequestConfig} object that
+ * represents the actual request configuration.
+ *
+ * @since 4.3
+ */
+ public static final String REQUEST_CONFIG = "http.request-config";
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/ClientContextConfigurer.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/ClientContextConfigurer.java
new file mode 100644
index 0000000000..29628a83d1
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/ClientContextConfigurer.java
@@ -0,0 +1,72 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.protocol;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.auth.AuthSchemeRegistry;
+import ch.boye.httpclientandroidlib.client.CookieStore;
+import ch.boye.httpclientandroidlib.client.CredentialsProvider;
+import ch.boye.httpclientandroidlib.cookie.CookieSpecRegistry;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Configuration facade for {@link HttpContext} instances.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link HttpClientContext}
+ */
+@NotThreadSafe
+@Deprecated
+public class ClientContextConfigurer implements ClientContext {
+
+ private final HttpContext context;
+
+ public ClientContextConfigurer (final HttpContext context) {
+ Args.notNull(context, "HTTP context");
+ this.context = context;
+ }
+
+ public void setCookieSpecRegistry(final CookieSpecRegistry registry) {
+ this.context.setAttribute(COOKIESPEC_REGISTRY, registry);
+ }
+
+ public void setAuthSchemeRegistry(final AuthSchemeRegistry registry) {
+ this.context.setAttribute(AUTHSCHEME_REGISTRY, registry);
+ }
+
+ public void setCookieStore(final CookieStore store) {
+ this.context.setAttribute(COOKIE_STORE, store);
+ }
+
+ public void setCredentialsProvider(final CredentialsProvider provider) {
+ this.context.setAttribute(CREDS_PROVIDER, provider);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/HttpClientContext.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/HttpClientContext.java
new file mode 100644
index 0000000000..7b73e8d644
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/HttpClientContext.java
@@ -0,0 +1,249 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.protocol;
+
+import java.net.URI;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.auth.AuthSchemeProvider;
+import ch.boye.httpclientandroidlib.auth.AuthState;
+import ch.boye.httpclientandroidlib.client.AuthCache;
+import ch.boye.httpclientandroidlib.client.CookieStore;
+import ch.boye.httpclientandroidlib.client.CredentialsProvider;
+import ch.boye.httpclientandroidlib.client.config.RequestConfig;
+import ch.boye.httpclientandroidlib.config.Lookup;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.conn.routing.RouteInfo;
+import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
+import ch.boye.httpclientandroidlib.cookie.CookieSpec;
+import ch.boye.httpclientandroidlib.cookie.CookieSpecProvider;
+import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.protocol.HttpCoreContext;
+
+/**
+ * Adaptor class that provides convenience type safe setters and getters
+ * for common {@link HttpContext} attributes used in the course
+ * of HTTP request execution.
+ *
+ * @since 4.3
+ */
+@NotThreadSafe
+public class HttpClientContext extends HttpCoreContext {
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.conn.routing.RouteInfo}
+ * object that represents the actual connection route.
+ */
+ public static final String HTTP_ROUTE = "http.route";
+
+ /**
+ * Attribute name of a {@link List} object that represents a collection of all
+ * redirect locations received in the process of request execution.
+ */
+ public static final String REDIRECT_LOCATIONS = "http.protocol.redirect-locations";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.config.Lookup} object that represents
+ * the actual {@link CookieSpecProvider} registry.
+ */
+ public static final String COOKIESPEC_REGISTRY = "http.cookiespec-registry";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.cookie.CookieSpec}
+ * object that represents the actual cookie specification.
+ */
+ public static final String COOKIE_SPEC = "http.cookie-spec";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.cookie.CookieOrigin}
+ * object that represents the actual details of the origin server.
+ */
+ public static final String COOKIE_ORIGIN = "http.cookie-origin";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.client.CookieStore}
+ * object that represents the actual cookie store.
+ */
+ public static final String COOKIE_STORE = "http.cookie-store";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.client.CredentialsProvider}
+ * object that represents the actual credentials provider.
+ */
+ public static final String CREDS_PROVIDER = "http.auth.credentials-provider";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.client.AuthCache} object
+ * that represents the auth scheme cache.
+ */
+ public static final String AUTH_CACHE = "http.auth.auth-cache";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.auth.AuthState}
+ * object that represents the actual target authentication state.
+ */
+ public static final String TARGET_AUTH_STATE = "http.auth.target-scope";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.auth.AuthState}
+ * object that represents the actual proxy authentication state.
+ */
+ public static final String PROXY_AUTH_STATE = "http.auth.proxy-scope";
+
+ /**
+ * Attribute name of a {@link java.lang.Object} object that represents
+ * the actual user identity such as user {@link java.security.Principal}.
+ */
+ public static final String USER_TOKEN = "http.user-token";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.config.Lookup} object that represents
+ * the actual {@link AuthSchemeProvider} registry.
+ */
+ public static final String AUTHSCHEME_REGISTRY = "http.authscheme-registry";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.client.config.RequestConfig} object that
+ * represents the actual request configuration.
+ */
+ public static final String REQUEST_CONFIG = "http.request-config";
+
+ public static HttpClientContext adapt(final HttpContext context) {
+ if (context instanceof HttpClientContext) {
+ return (HttpClientContext) context;
+ } else {
+ return new HttpClientContext(context);
+ }
+ }
+
+ public static HttpClientContext create() {
+ return new HttpClientContext(new BasicHttpContext());
+ }
+
+ public HttpClientContext(final HttpContext context) {
+ super(context);
+ }
+
+ public HttpClientContext() {
+ super();
+ }
+
+ public RouteInfo getHttpRoute() {
+ return getAttribute(HTTP_ROUTE, HttpRoute.class);
+ }
+
+ @SuppressWarnings("unchecked")
+ public List<URI> getRedirectLocations() {
+ return getAttribute(REDIRECT_LOCATIONS, List.class);
+ }
+
+ public CookieStore getCookieStore() {
+ return getAttribute(COOKIE_STORE, CookieStore.class);
+ }
+
+ public void setCookieStore(final CookieStore cookieStore) {
+ setAttribute(COOKIE_STORE, cookieStore);
+ }
+
+ public CookieSpec getCookieSpec() {
+ return getAttribute(COOKIE_SPEC, CookieSpec.class);
+ }
+
+ public CookieOrigin getCookieOrigin() {
+ return getAttribute(COOKIE_ORIGIN, CookieOrigin.class);
+ }
+
+ @SuppressWarnings("unchecked")
+ private <T> Lookup<T> getLookup(final String name, final Class<T> clazz) {
+ return getAttribute(name, Lookup.class);
+ }
+
+ public Lookup<CookieSpecProvider> getCookieSpecRegistry() {
+ return getLookup(COOKIESPEC_REGISTRY, CookieSpecProvider.class);
+ }
+
+ public void setCookieSpecRegistry(final Lookup<CookieSpecProvider> lookup) {
+ setAttribute(COOKIESPEC_REGISTRY, lookup);
+ }
+
+ public Lookup<AuthSchemeProvider> getAuthSchemeRegistry() {
+ return getLookup(AUTHSCHEME_REGISTRY, AuthSchemeProvider.class);
+ }
+
+ public void setAuthSchemeRegistry(final Lookup<AuthSchemeProvider> lookup) {
+ setAttribute(AUTHSCHEME_REGISTRY, lookup);
+ }
+
+ public CredentialsProvider getCredentialsProvider() {
+ return getAttribute(CREDS_PROVIDER, CredentialsProvider.class);
+ }
+
+ public void setCredentialsProvider(final CredentialsProvider credentialsProvider) {
+ setAttribute(CREDS_PROVIDER, credentialsProvider);
+ }
+
+ public AuthCache getAuthCache() {
+ return getAttribute(AUTH_CACHE, AuthCache.class);
+ }
+
+ public void setAuthCache(final AuthCache authCache) {
+ setAttribute(AUTH_CACHE, authCache);
+ }
+
+ public AuthState getTargetAuthState() {
+ return getAttribute(TARGET_AUTH_STATE, AuthState.class);
+ }
+
+ public AuthState getProxyAuthState() {
+ return getAttribute(PROXY_AUTH_STATE, AuthState.class);
+ }
+
+ public <T> T getUserToken(final Class<T> clazz) {
+ return getAttribute(USER_TOKEN, clazz);
+ }
+
+ public Object getUserToken() {
+ return getAttribute(USER_TOKEN);
+ }
+
+ public void setUserToken(final Object obj) {
+ setAttribute(USER_TOKEN, obj);
+ }
+
+ public RequestConfig getRequestConfig() {
+ final RequestConfig config = getAttribute(REQUEST_CONFIG, RequestConfig.class);
+ return config != null ? config : RequestConfig.DEFAULT;
+ }
+
+ public void setRequestConfig(final RequestConfig config) {
+ setAttribute(REQUEST_CONFIG, config);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestAcceptEncoding.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestAcceptEncoding.java
new file mode 100644
index 0000000000..1c0d642d39
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestAcceptEncoding.java
@@ -0,0 +1,62 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.protocol;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpRequestInterceptor;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * Class responsible for handling Content Encoding requests in HTTP.
+ * <p>
+ * Instances of this class are stateless, therefore they're thread-safe and immutable.
+ *
+ * @see "http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.5"
+ *
+ * @since 4.1
+ */
+@Immutable
+public class RequestAcceptEncoding implements HttpRequestInterceptor {
+
+ /**
+ * Adds the header {@code "Accept-Encoding: gzip,deflate"} to the request.
+ */
+ public void process(
+ final HttpRequest request,
+ final HttpContext context) throws HttpException, IOException {
+
+ /* Signal support for Accept-Encoding transfer encodings. */
+ if (!request.containsHeader("Accept-Encoding")) {
+ request.addHeader("Accept-Encoding", "gzip,deflate");
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestAddCookies.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestAddCookies.java
new file mode 100644
index 0000000000..ad2768043f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestAddCookies.java
@@ -0,0 +1,204 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.protocol;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpRequestInterceptor;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.CookieStore;
+import ch.boye.httpclientandroidlib.client.config.CookieSpecs;
+import ch.boye.httpclientandroidlib.client.config.RequestConfig;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.config.Lookup;
+import ch.boye.httpclientandroidlib.conn.routing.RouteInfo;
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
+import ch.boye.httpclientandroidlib.cookie.CookieSpec;
+import ch.boye.httpclientandroidlib.cookie.CookieSpecProvider;
+import ch.boye.httpclientandroidlib.cookie.SetCookie2;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.TextUtils;
+
+/**
+ * Request interceptor that matches cookies available in the current
+ * {@link CookieStore} to the request being executed and generates
+ * corresponding <code>Cookie</code> request headers.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class RequestAddCookies implements HttpRequestInterceptor {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ public RequestAddCookies() {
+ super();
+ }
+
+ public void process(final HttpRequest request, final HttpContext context)
+ throws HttpException, IOException {
+ Args.notNull(request, "HTTP request");
+ Args.notNull(context, "HTTP context");
+
+ final String method = request.getRequestLine().getMethod();
+ if (method.equalsIgnoreCase("CONNECT")) {
+ return;
+ }
+
+ final HttpClientContext clientContext = HttpClientContext.adapt(context);
+
+ // Obtain cookie store
+ final CookieStore cookieStore = clientContext.getCookieStore();
+ if (cookieStore == null) {
+ this.log.debug("Cookie store not specified in HTTP context");
+ return;
+ }
+
+ // Obtain the registry of cookie specs
+ final Lookup<CookieSpecProvider> registry = clientContext.getCookieSpecRegistry();
+ if (registry == null) {
+ this.log.debug("CookieSpec registry not specified in HTTP context");
+ return;
+ }
+
+ // Obtain the target host, possibly virtual (required)
+ final HttpHost targetHost = clientContext.getTargetHost();
+ if (targetHost == null) {
+ this.log.debug("Target host not set in the context");
+ return;
+ }
+
+ // Obtain the route (required)
+ final RouteInfo route = clientContext.getHttpRoute();
+ if (route == null) {
+ this.log.debug("Connection route not set in the context");
+ return;
+ }
+
+ final RequestConfig config = clientContext.getRequestConfig();
+ String policy = config.getCookieSpec();
+ if (policy == null) {
+ policy = CookieSpecs.BEST_MATCH;
+ }
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("CookieSpec selected: " + policy);
+ }
+
+ URI requestURI = null;
+ if (request instanceof HttpUriRequest) {
+ requestURI = ((HttpUriRequest) request).getURI();
+ } else {
+ try {
+ requestURI = new URI(request.getRequestLine().getUri());
+ } catch (final URISyntaxException ignore) {
+ }
+ }
+ final String path = requestURI != null ? requestURI.getPath() : null;
+ final String hostName = targetHost.getHostName();
+ int port = targetHost.getPort();
+ if (port < 0) {
+ port = route.getTargetHost().getPort();
+ }
+
+ final CookieOrigin cookieOrigin = new CookieOrigin(
+ hostName,
+ port >= 0 ? port : 0,
+ !TextUtils.isEmpty(path) ? path : "/",
+ route.isSecure());
+
+ // Get an instance of the selected cookie policy
+ final CookieSpecProvider provider = registry.lookup(policy);
+ if (provider == null) {
+ throw new HttpException("Unsupported cookie policy: " + policy);
+ }
+ final CookieSpec cookieSpec = provider.create(clientContext);
+ // Get all cookies available in the HTTP state
+ final List<Cookie> cookies = new ArrayList<Cookie>(cookieStore.getCookies());
+ // Find cookies matching the given origin
+ final List<Cookie> matchedCookies = new ArrayList<Cookie>();
+ final Date now = new Date();
+ for (final Cookie cookie : cookies) {
+ if (!cookie.isExpired(now)) {
+ if (cookieSpec.match(cookie, cookieOrigin)) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Cookie " + cookie + " match " + cookieOrigin);
+ }
+ matchedCookies.add(cookie);
+ }
+ } else {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Cookie " + cookie + " expired");
+ }
+ }
+ }
+ // Generate Cookie request headers
+ if (!matchedCookies.isEmpty()) {
+ final List<Header> headers = cookieSpec.formatCookies(matchedCookies);
+ for (final Header header : headers) {
+ request.addHeader(header);
+ }
+ }
+
+ final int ver = cookieSpec.getVersion();
+ if (ver > 0) {
+ boolean needVersionHeader = false;
+ for (final Cookie cookie : matchedCookies) {
+ if (ver != cookie.getVersion() || !(cookie instanceof SetCookie2)) {
+ needVersionHeader = true;
+ }
+ }
+
+ if (needVersionHeader) {
+ final Header header = cookieSpec.getVersionHeader();
+ if (header != null) {
+ // Advertise cookie version support
+ request.addHeader(header);
+ }
+ }
+ }
+
+ // Stick the CookieSpec and CookieOrigin instances to the HTTP context
+ // so they could be obtained by the response interceptor
+ context.setAttribute(HttpClientContext.COOKIE_SPEC, cookieSpec);
+ context.setAttribute(HttpClientContext.COOKIE_ORIGIN, cookieOrigin);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestAuthCache.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestAuthCache.java
new file mode 100644
index 0000000000..08102cfc88
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestAuthCache.java
@@ -0,0 +1,147 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.protocol;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpRequestInterceptor;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.auth.AuthProtocolState;
+import ch.boye.httpclientandroidlib.auth.AuthScheme;
+import ch.boye.httpclientandroidlib.auth.AuthScope;
+import ch.boye.httpclientandroidlib.auth.AuthState;
+import ch.boye.httpclientandroidlib.auth.Credentials;
+import ch.boye.httpclientandroidlib.client.AuthCache;
+import ch.boye.httpclientandroidlib.client.CredentialsProvider;
+import ch.boye.httpclientandroidlib.conn.routing.RouteInfo;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Request interceptor that can preemptively authenticate against known hosts,
+ * if there is a cached {@link AuthScheme} instance in the local
+ * {@link AuthCache} associated with the given target or proxy host.
+ *
+ * @since 4.1
+ */
+@Immutable
+public class RequestAuthCache implements HttpRequestInterceptor {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ public RequestAuthCache() {
+ super();
+ }
+
+ public void process(final HttpRequest request, final HttpContext context)
+ throws HttpException, IOException {
+ Args.notNull(request, "HTTP request");
+ Args.notNull(context, "HTTP context");
+
+ final HttpClientContext clientContext = HttpClientContext.adapt(context);
+
+ final AuthCache authCache = clientContext.getAuthCache();
+ if (authCache == null) {
+ this.log.debug("Auth cache not set in the context");
+ return;
+ }
+
+ final CredentialsProvider credsProvider = clientContext.getCredentialsProvider();
+ if (credsProvider == null) {
+ this.log.debug("Credentials provider not set in the context");
+ return;
+ }
+
+ final RouteInfo route = clientContext.getHttpRoute();
+ if (route == null) {
+ this.log.debug("Route info not set in the context");
+ return;
+ }
+
+ HttpHost target = clientContext.getTargetHost();
+ if (target == null) {
+ this.log.debug("Target host not set in the context");
+ return;
+ }
+
+ if (target.getPort() < 0) {
+ target = new HttpHost(
+ target.getHostName(),
+ route.getTargetHost().getPort(),
+ target.getSchemeName());
+ }
+
+ final AuthState targetState = clientContext.getTargetAuthState();
+ if (targetState != null && targetState.getState() == AuthProtocolState.UNCHALLENGED) {
+ final AuthScheme authScheme = authCache.get(target);
+ if (authScheme != null) {
+ doPreemptiveAuth(target, authScheme, targetState, credsProvider);
+ }
+ }
+
+ final HttpHost proxy = route.getProxyHost();
+ final AuthState proxyState = clientContext.getProxyAuthState();
+ if (proxy != null && proxyState != null && proxyState.getState() == AuthProtocolState.UNCHALLENGED) {
+ final AuthScheme authScheme = authCache.get(proxy);
+ if (authScheme != null) {
+ doPreemptiveAuth(proxy, authScheme, proxyState, credsProvider);
+ }
+ }
+ }
+
+ private void doPreemptiveAuth(
+ final HttpHost host,
+ final AuthScheme authScheme,
+ final AuthState authState,
+ final CredentialsProvider credsProvider) {
+ final String schemeName = authScheme.getSchemeName();
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Re-using cached '" + schemeName + "' auth scheme for " + host);
+ }
+
+ final AuthScope authScope = new AuthScope(host, AuthScope.ANY_REALM, schemeName);
+ final Credentials creds = credsProvider.getCredentials(authScope);
+
+ if (creds != null) {
+ if ("BASIC".equalsIgnoreCase(authScheme.getSchemeName())) {
+ authState.setState(AuthProtocolState.CHALLENGED);
+ } else {
+ authState.setState(AuthProtocolState.SUCCESS);
+ }
+ authState.update(authScheme, creds);
+ } else {
+ this.log.debug("No credentials for preemptive authentication");
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestAuthenticationBase.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestAuthenticationBase.java
new file mode 100644
index 0000000000..19e07054bd
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestAuthenticationBase.java
@@ -0,0 +1,126 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.protocol;
+
+import java.util.Queue;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpRequestInterceptor;
+import ch.boye.httpclientandroidlib.auth.AuthOption;
+import ch.boye.httpclientandroidlib.auth.AuthScheme;
+import ch.boye.httpclientandroidlib.auth.AuthState;
+import ch.boye.httpclientandroidlib.auth.AuthenticationException;
+import ch.boye.httpclientandroidlib.auth.ContextAwareAuthScheme;
+import ch.boye.httpclientandroidlib.auth.Credentials;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Asserts;
+
+@Deprecated
+abstract class RequestAuthenticationBase implements HttpRequestInterceptor {
+
+ final HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ public RequestAuthenticationBase() {
+ super();
+ }
+
+ void process(
+ final AuthState authState,
+ final HttpRequest request,
+ final HttpContext context) {
+ AuthScheme authScheme = authState.getAuthScheme();
+ Credentials creds = authState.getCredentials();
+ switch (authState.getState()) {
+ case FAILURE:
+ return;
+ case SUCCESS:
+ ensureAuthScheme(authScheme);
+ if (authScheme.isConnectionBased()) {
+ return;
+ }
+ break;
+ case CHALLENGED:
+ final Queue<AuthOption> authOptions = authState.getAuthOptions();
+ if (authOptions != null) {
+ while (!authOptions.isEmpty()) {
+ final AuthOption authOption = authOptions.remove();
+ authScheme = authOption.getAuthScheme();
+ creds = authOption.getCredentials();
+ authState.update(authScheme, creds);
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Generating response to an authentication challenge using "
+ + authScheme.getSchemeName() + " scheme");
+ }
+ try {
+ final Header header = authenticate(authScheme, creds, request, context);
+ request.addHeader(header);
+ break;
+ } catch (final AuthenticationException ex) {
+ if (this.log.isWarnEnabled()) {
+ this.log.warn(authScheme + " authentication error: " + ex.getMessage());
+ }
+ }
+ }
+ return;
+ } else {
+ ensureAuthScheme(authScheme);
+ }
+ }
+ if (authScheme != null) {
+ try {
+ final Header header = authenticate(authScheme, creds, request, context);
+ request.addHeader(header);
+ } catch (final AuthenticationException ex) {
+ if (this.log.isErrorEnabled()) {
+ this.log.error(authScheme + " authentication error: " + ex.getMessage());
+ }
+ }
+ }
+ }
+
+ private void ensureAuthScheme(final AuthScheme authScheme) {
+ Asserts.notNull(authScheme, "Auth scheme");
+ }
+
+ private Header authenticate(
+ final AuthScheme authScheme,
+ final Credentials creds,
+ final HttpRequest request,
+ final HttpContext context) throws AuthenticationException {
+ Asserts.notNull(authScheme, "Auth scheme");
+ if (authScheme instanceof ContextAwareAuthScheme) {
+ return ((ContextAwareAuthScheme) authScheme).authenticate(creds, request, context);
+ } else {
+ return authScheme.authenticate(creds, request);
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestClientConnControl.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestClientConnControl.java
new file mode 100644
index 0000000000..714b2859b4
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestClientConnControl.java
@@ -0,0 +1,92 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.protocol;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpRequestInterceptor;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.conn.routing.RouteInfo;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * This protocol interceptor is responsible for adding <code>Connection</code>
+ * or <code>Proxy-Connection</code> headers to the outgoing requests, which
+ * is essential for managing persistence of <code>HTTP/1.0</code> connections.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class RequestClientConnControl implements HttpRequestInterceptor {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ private static final String PROXY_CONN_DIRECTIVE = "Proxy-Connection";
+
+ public RequestClientConnControl() {
+ super();
+ }
+
+ public void process(final HttpRequest request, final HttpContext context)
+ throws HttpException, IOException {
+ Args.notNull(request, "HTTP request");
+
+ final String method = request.getRequestLine().getMethod();
+ if (method.equalsIgnoreCase("CONNECT")) {
+ request.setHeader(PROXY_CONN_DIRECTIVE, HTTP.CONN_KEEP_ALIVE);
+ return;
+ }
+
+ final HttpClientContext clientContext = HttpClientContext.adapt(context);
+
+ // Obtain the client connection (required)
+ final RouteInfo route = clientContext.getHttpRoute();
+ if (route == null) {
+ this.log.debug("Connection route not set in the context");
+ return;
+ }
+
+ if (route.getHopCount() == 1 || route.isTunnelled()) {
+ if (!request.containsHeader(HTTP.CONN_DIRECTIVE)) {
+ request.addHeader(HTTP.CONN_DIRECTIVE, HTTP.CONN_KEEP_ALIVE);
+ }
+ }
+ if (route.getHopCount() == 2 && !route.isTunnelled()) {
+ if (!request.containsHeader(PROXY_CONN_DIRECTIVE)) {
+ request.addHeader(PROXY_CONN_DIRECTIVE, HTTP.CONN_KEEP_ALIVE);
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestDefaultHeaders.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestDefaultHeaders.java
new file mode 100644
index 0000000000..c4414f1f47
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestDefaultHeaders.java
@@ -0,0 +1,89 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.protocol;
+
+import java.io.IOException;
+import java.util.Collection;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpRequestInterceptor;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.params.ClientPNames;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Request interceptor that adds default request headers.
+ *
+ * @since 4.0
+ */
+@SuppressWarnings("deprecation")
+@Immutable
+public class RequestDefaultHeaders implements HttpRequestInterceptor {
+
+ private final Collection<? extends Header> defaultHeaders;
+
+ /**
+ * @since 4.3
+ */
+ public RequestDefaultHeaders(final Collection<? extends Header> defaultHeaders) {
+ super();
+ this.defaultHeaders = defaultHeaders;
+ }
+
+ public RequestDefaultHeaders() {
+ this(null);
+ }
+
+ public void process(final HttpRequest request, final HttpContext context)
+ throws HttpException, IOException {
+ Args.notNull(request, "HTTP request");
+
+ final String method = request.getRequestLine().getMethod();
+ if (method.equalsIgnoreCase("CONNECT")) {
+ return;
+ }
+
+ // Add default headers
+ @SuppressWarnings("unchecked")
+ Collection<? extends Header> defHeaders = (Collection<? extends Header>)
+ request.getParams().getParameter(ClientPNames.DEFAULT_HEADERS);
+ if (defHeaders == null) {
+ defHeaders = this.defaultHeaders;
+ }
+
+ if (defHeaders != null) {
+ for (final Header defHeader : defHeaders) {
+ request.addHeader(defHeader);
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestExpectContinue.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestExpectContinue.java
new file mode 100644
index 0000000000..237004f0fd
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestExpectContinue.java
@@ -0,0 +1,82 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.protocol;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpRequestInterceptor;
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.config.RequestConfig;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * RequestExpectContinue is responsible for enabling the 'expect-continue'
+ * handshake by adding <code>Expect</code> header.
+ * <p/>
+ * This interceptor takes into account {@link RequestConfig#isExpectContinueEnabled()}
+ * setting.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class RequestExpectContinue implements HttpRequestInterceptor {
+
+ public RequestExpectContinue() {
+ super();
+ }
+
+ public void process(final HttpRequest request, final HttpContext context)
+ throws HttpException, IOException {
+ Args.notNull(request, "HTTP request");
+
+ if (!request.containsHeader(HTTP.EXPECT_DIRECTIVE)) {
+ if (request instanceof HttpEntityEnclosingRequest) {
+ final ProtocolVersion ver = request.getRequestLine().getProtocolVersion();
+ final HttpEntity entity = ((HttpEntityEnclosingRequest)request).getEntity();
+ // Do not send the expect header if request body is known to be empty
+ if (entity != null
+ && entity.getContentLength() != 0 && !ver.lessEquals(HttpVersion.HTTP_1_0)) {
+ final HttpClientContext clientContext = HttpClientContext.adapt(context);
+ final RequestConfig config = clientContext.getRequestConfig();
+ if (config.isExpectContinueEnabled()) {
+ request.addHeader(HTTP.EXPECT_DIRECTIVE, HTTP.EXPECT_CONTINUE);
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestProxyAuthentication.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestProxyAuthentication.java
new file mode 100644
index 0000000000..e67468ff85
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestProxyAuthentication.java
@@ -0,0 +1,92 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.protocol;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.auth.AUTH;
+import ch.boye.httpclientandroidlib.auth.AuthState;
+import ch.boye.httpclientandroidlib.conn.HttpRoutedConnection;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.protocol.ExecutionContext;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Generates authentication header for the proxy host, if required,
+ * based on the actual state of the HTTP authentication context.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.impl.auth.HttpAuthenticator}.
+ */
+@Deprecated
+@Immutable
+public class RequestProxyAuthentication extends RequestAuthenticationBase {
+
+ public RequestProxyAuthentication() {
+ super();
+ }
+
+ public void process(final HttpRequest request, final HttpContext context)
+ throws HttpException, IOException {
+ Args.notNull(request, "HTTP request");
+ Args.notNull(context, "HTTP context");
+
+ if (request.containsHeader(AUTH.PROXY_AUTH_RESP)) {
+ return;
+ }
+
+ final HttpRoutedConnection conn = (HttpRoutedConnection) context.getAttribute(
+ ExecutionContext.HTTP_CONNECTION);
+ if (conn == null) {
+ this.log.debug("HTTP connection not set in the context");
+ return;
+ }
+ final HttpRoute route = conn.getRoute();
+ if (route.isTunnelled()) {
+ return;
+ }
+
+ // Obtain authentication state
+ final AuthState authState = (AuthState) context.getAttribute(
+ ClientContext.PROXY_AUTH_STATE);
+ if (authState == null) {
+ this.log.debug("Proxy auth state not set in the context");
+ return;
+ }
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Proxy auth state: " + authState.getState());
+ }
+ process(authState, request, context);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestTargetAuthentication.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestTargetAuthentication.java
new file mode 100644
index 0000000000..dd5c525976
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/RequestTargetAuthentication.java
@@ -0,0 +1,83 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.protocol;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.auth.AUTH;
+import ch.boye.httpclientandroidlib.auth.AuthState;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Generates authentication header for the target host, if required,
+ * based on the actual state of the HTTP authentication context.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.impl.auth.HttpAuthenticator}.
+ */
+@Deprecated
+@Immutable
+public class RequestTargetAuthentication extends RequestAuthenticationBase {
+
+ public RequestTargetAuthentication() {
+ super();
+ }
+
+ public void process(final HttpRequest request, final HttpContext context)
+ throws HttpException, IOException {
+ Args.notNull(request, "HTTP request");
+ Args.notNull(context, "HTTP context");
+
+ final String method = request.getRequestLine().getMethod();
+ if (method.equalsIgnoreCase("CONNECT")) {
+ return;
+ }
+
+ if (request.containsHeader(AUTH.WWW_AUTH_RESP)) {
+ return;
+ }
+
+ // Obtain authentication state
+ final AuthState authState = (AuthState) context.getAttribute(
+ ClientContext.TARGET_AUTH_STATE);
+ if (authState == null) {
+ this.log.debug("Target auth state not set in the context");
+ return;
+ }
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Target auth state: " + authState.getState());
+ }
+ process(authState, request, context);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/ResponseAuthCache.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/ResponseAuthCache.java
new file mode 100644
index 0000000000..fc51012f63
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/ResponseAuthCache.java
@@ -0,0 +1,151 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.protocol;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpResponseInterceptor;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.auth.AuthScheme;
+import ch.boye.httpclientandroidlib.auth.AuthState;
+import ch.boye.httpclientandroidlib.client.AuthCache;
+import ch.boye.httpclientandroidlib.client.params.AuthPolicy;
+import ch.boye.httpclientandroidlib.conn.scheme.Scheme;
+import ch.boye.httpclientandroidlib.conn.scheme.SchemeRegistry;
+import ch.boye.httpclientandroidlib.impl.client.BasicAuthCache;
+import ch.boye.httpclientandroidlib.protocol.ExecutionContext;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Response interceptor that adds successfully completed {@link AuthScheme}s
+ * to the local {@link AuthCache} instance. Cached {@link AuthScheme}s can be
+ * re-used when executing requests against known hosts, thus avoiding
+ * additional authentication round-trips.
+ *
+ * @since 4.1
+ *
+ * @deprecated (4.2) use {@link ch.boye.httpclientandroidlib.client.AuthenticationStrategy}
+ */
+@Immutable
+@Deprecated
+public class ResponseAuthCache implements HttpResponseInterceptor {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ public ResponseAuthCache() {
+ super();
+ }
+
+ public void process(final HttpResponse response, final HttpContext context)
+ throws HttpException, IOException {
+ Args.notNull(response, "HTTP request");
+ Args.notNull(context, "HTTP context");
+ AuthCache authCache = (AuthCache) context.getAttribute(ClientContext.AUTH_CACHE);
+
+ HttpHost target = (HttpHost) context.getAttribute(ExecutionContext.HTTP_TARGET_HOST);
+ final AuthState targetState = (AuthState) context.getAttribute(ClientContext.TARGET_AUTH_STATE);
+ if (target != null && targetState != null) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Target auth state: " + targetState.getState());
+ }
+ if (isCachable(targetState)) {
+ final SchemeRegistry schemeRegistry = (SchemeRegistry) context.getAttribute(
+ ClientContext.SCHEME_REGISTRY);
+ if (target.getPort() < 0) {
+ final Scheme scheme = schemeRegistry.getScheme(target);
+ target = new HttpHost(target.getHostName(),
+ scheme.resolvePort(target.getPort()), target.getSchemeName());
+ }
+ if (authCache == null) {
+ authCache = new BasicAuthCache();
+ context.setAttribute(ClientContext.AUTH_CACHE, authCache);
+ }
+ switch (targetState.getState()) {
+ case CHALLENGED:
+ cache(authCache, target, targetState.getAuthScheme());
+ break;
+ case FAILURE:
+ uncache(authCache, target, targetState.getAuthScheme());
+ }
+ }
+ }
+
+ final HttpHost proxy = (HttpHost) context.getAttribute(ExecutionContext.HTTP_PROXY_HOST);
+ final AuthState proxyState = (AuthState) context.getAttribute(ClientContext.PROXY_AUTH_STATE);
+ if (proxy != null && proxyState != null) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Proxy auth state: " + proxyState.getState());
+ }
+ if (isCachable(proxyState)) {
+ if (authCache == null) {
+ authCache = new BasicAuthCache();
+ context.setAttribute(ClientContext.AUTH_CACHE, authCache);
+ }
+ switch (proxyState.getState()) {
+ case CHALLENGED:
+ cache(authCache, proxy, proxyState.getAuthScheme());
+ break;
+ case FAILURE:
+ uncache(authCache, proxy, proxyState.getAuthScheme());
+ }
+ }
+ }
+ }
+
+ private boolean isCachable(final AuthState authState) {
+ final AuthScheme authScheme = authState.getAuthScheme();
+ if (authScheme == null || !authScheme.isComplete()) {
+ return false;
+ }
+ final String schemeName = authScheme.getSchemeName();
+ return schemeName.equalsIgnoreCase(AuthPolicy.BASIC) ||
+ schemeName.equalsIgnoreCase(AuthPolicy.DIGEST);
+ }
+
+ private void cache(final AuthCache authCache, final HttpHost host, final AuthScheme authScheme) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Caching '" + authScheme.getSchemeName() +
+ "' auth scheme for " + host);
+ }
+ authCache.put(host, authScheme);
+ }
+
+ private void uncache(final AuthCache authCache, final HttpHost host, final AuthScheme authScheme) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Removing from cache '" + authScheme.getSchemeName() +
+ "' auth scheme for " + host);
+ }
+ authCache.remove(host);
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/ResponseContentEncoding.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/ResponseContentEncoding.java
new file mode 100644
index 0000000000..df3ee4457d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/ResponseContentEncoding.java
@@ -0,0 +1,110 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.protocol;
+
+import java.io.IOException;
+import java.util.Locale;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpResponseInterceptor;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.entity.DeflateDecompressingEntity;
+import ch.boye.httpclientandroidlib.client.entity.GzipDecompressingEntity;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * {@link HttpResponseInterceptor} responsible for processing Content-Encoding
+ * responses.
+ * <p>
+ * Instances of this class are stateless and immutable, therefore threadsafe.
+ *
+ * @since 4.1
+ *
+ */
+@Immutable
+public class ResponseContentEncoding implements HttpResponseInterceptor {
+
+ public static final String UNCOMPRESSED = "http.client.response.uncompressed";
+
+ /**
+ * Handles the following {@code Content-Encoding}s by
+ * using the appropriate decompressor to wrap the response Entity:
+ * <ul>
+ * <li>gzip - see {@link GzipDecompressingEntity}</li>
+ * <li>deflate - see {@link DeflateDecompressingEntity}</li>
+ * <li>identity - no action needed</li>
+ * </ul>
+ *
+ * @param response the response which contains the entity
+ * @param context not currently used
+ *
+ * @throws HttpException if the {@code Content-Encoding} is none of the above
+ */
+ public void process(
+ final HttpResponse response,
+ final HttpContext context) throws HttpException, IOException {
+ final HttpEntity entity = response.getEntity();
+
+ // entity can be null in case of 304 Not Modified, 204 No Content or similar
+ // check for zero length entity.
+ if (entity != null && entity.getContentLength() != 0) {
+ final Header ceheader = entity.getContentEncoding();
+ if (ceheader != null) {
+ final HeaderElement[] codecs = ceheader.getElements();
+ boolean uncompressed = false;
+ for (final HeaderElement codec : codecs) {
+ final String codecname = codec.getName().toLowerCase(Locale.ENGLISH);
+ if ("gzip".equals(codecname) || "x-gzip".equals(codecname)) {
+ response.setEntity(new GzipDecompressingEntity(response.getEntity()));
+ uncompressed = true;
+ break;
+ } else if ("deflate".equals(codecname)) {
+ response.setEntity(new DeflateDecompressingEntity(response.getEntity()));
+ uncompressed = true;
+ break;
+ } else if ("identity".equals(codecname)) {
+
+ /* Don't need to transform the content - no-op */
+ return;
+ } else {
+ throw new HttpException("Unsupported Content-Coding: " + codec.getName());
+ }
+ }
+ if (uncompressed) {
+ response.removeHeaders("Content-Length");
+ response.removeHeaders("Content-Encoding");
+ response.removeHeaders("Content-MD5");
+ }
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/ResponseProcessCookies.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/ResponseProcessCookies.java
new file mode 100644
index 0000000000..06827bfdca
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/ResponseProcessCookies.java
@@ -0,0 +1,156 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.protocol;
+
+import java.io.IOException;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderIterator;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpResponseInterceptor;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.CookieStore;
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
+import ch.boye.httpclientandroidlib.cookie.CookieSpec;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+import ch.boye.httpclientandroidlib.cookie.SM;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Response interceptor that populates the current {@link CookieStore} with data
+ * contained in response cookies received in the given the HTTP response.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class ResponseProcessCookies implements HttpResponseInterceptor {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ public ResponseProcessCookies() {
+ super();
+ }
+
+ public void process(final HttpResponse response, final HttpContext context)
+ throws HttpException, IOException {
+ Args.notNull(response, "HTTP request");
+ Args.notNull(context, "HTTP context");
+
+ final HttpClientContext clientContext = HttpClientContext.adapt(context);
+
+ // Obtain actual CookieSpec instance
+ final CookieSpec cookieSpec = clientContext.getCookieSpec();
+ if (cookieSpec == null) {
+ this.log.debug("Cookie spec not specified in HTTP context");
+ return;
+ }
+ // Obtain cookie store
+ final CookieStore cookieStore = clientContext.getCookieStore();
+ if (cookieStore == null) {
+ this.log.debug("Cookie store not specified in HTTP context");
+ return;
+ }
+ // Obtain actual CookieOrigin instance
+ final CookieOrigin cookieOrigin = clientContext.getCookieOrigin();
+ if (cookieOrigin == null) {
+ this.log.debug("Cookie origin not specified in HTTP context");
+ return;
+ }
+ HeaderIterator it = response.headerIterator(SM.SET_COOKIE);
+ processCookies(it, cookieSpec, cookieOrigin, cookieStore);
+
+ // see if the cookie spec supports cookie versioning.
+ if (cookieSpec.getVersion() > 0) {
+ // process set-cookie2 headers.
+ // Cookie2 will replace equivalent Cookie instances
+ it = response.headerIterator(SM.SET_COOKIE2);
+ processCookies(it, cookieSpec, cookieOrigin, cookieStore);
+ }
+ }
+
+ private void processCookies(
+ final HeaderIterator iterator,
+ final CookieSpec cookieSpec,
+ final CookieOrigin cookieOrigin,
+ final CookieStore cookieStore) {
+ while (iterator.hasNext()) {
+ final Header header = iterator.nextHeader();
+ try {
+ final List<Cookie> cookies = cookieSpec.parse(header, cookieOrigin);
+ for (final Cookie cookie : cookies) {
+ try {
+ cookieSpec.validate(cookie, cookieOrigin);
+ cookieStore.addCookie(cookie);
+
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Cookie accepted [" + formatCooke(cookie) + "]");
+ }
+ } catch (final MalformedCookieException ex) {
+ if (this.log.isWarnEnabled()) {
+ this.log.warn("Cookie rejected [" + formatCooke(cookie) + "] "
+ + ex.getMessage());
+ }
+ }
+ }
+ } catch (final MalformedCookieException ex) {
+ if (this.log.isWarnEnabled()) {
+ this.log.warn("Invalid cookie header: \""
+ + header + "\". " + ex.getMessage());
+ }
+ }
+ }
+ }
+
+ private static String formatCooke(final Cookie cookie) {
+ final StringBuilder buf = new StringBuilder();
+ buf.append(cookie.getName());
+ buf.append("=\"");
+ String v = cookie.getValue();
+ if (v.length() > 100) {
+ v = v.substring(0, 100) + "...";
+ }
+ buf.append(v);
+ buf.append("\"");
+ buf.append(", version:");
+ buf.append(Integer.toString(cookie.getVersion()));
+ buf.append(", domain:");
+ buf.append(cookie.getDomain());
+ buf.append(", path:");
+ buf.append(cookie.getPath());
+ buf.append(", expiry:");
+ buf.append(cookie.getExpiryDate());
+ return buf.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/package-info.java
new file mode 100644
index 0000000000..a585e614dc
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/protocol/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Client specific HTTP protocol handlers.
+ */
+package ch.boye.httpclientandroidlib.client.protocol;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/CloneUtils.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/CloneUtils.java
new file mode 100644
index 0000000000..67f70ee072
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/CloneUtils.java
@@ -0,0 +1,86 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.utils;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * A collection of utilities to workaround limitations of Java clone framework.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class CloneUtils {
+
+ /**
+ * @since 4.3
+ */
+ public static <T> T cloneObject(final T obj) throws CloneNotSupportedException {
+ if (obj == null) {
+ return null;
+ }
+ if (obj instanceof Cloneable) {
+ final Class<?> clazz = obj.getClass ();
+ final Method m;
+ try {
+ m = clazz.getMethod("clone", (Class[]) null);
+ } catch (final NoSuchMethodException ex) {
+ throw new NoSuchMethodError(ex.getMessage());
+ }
+ try {
+ @SuppressWarnings("unchecked") // OK because clone() preserves the class
+ final T result = (T) m.invoke(obj, (Object []) null);
+ return result;
+ } catch (final InvocationTargetException ex) {
+ final Throwable cause = ex.getCause();
+ if (cause instanceof CloneNotSupportedException) {
+ throw ((CloneNotSupportedException) cause);
+ } else {
+ throw new Error("Unexpected exception", cause);
+ }
+ } catch (final IllegalAccessException ex) {
+ throw new IllegalAccessError(ex.getMessage());
+ }
+ } else {
+ throw new CloneNotSupportedException();
+ }
+ }
+
+ public static Object clone(final Object obj) throws CloneNotSupportedException {
+ return cloneObject(obj);
+ }
+
+ /**
+ * This class should not be instantiated.
+ */
+ private CloneUtils() {
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/DateUtils.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/DateUtils.java
new file mode 100644
index 0000000000..e337923d3d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/DateUtils.java
@@ -0,0 +1,250 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.utils;
+
+import java.lang.ref.SoftReference;
+import java.text.ParsePosition;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * A utility class for parsing and formatting HTTP dates as used in cookies and
+ * other headers. This class handles dates as defined by RFC 2616 section
+ * 3.3.1 as well as some other common non-standard formats.
+ *
+ * @since 4.3
+ */
+@Immutable
+public final class DateUtils {
+
+ /**
+ * Date format pattern used to parse HTTP date headers in RFC 1123 format.
+ */
+ public static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
+
+ /**
+ * Date format pattern used to parse HTTP date headers in RFC 1036 format.
+ */
+ public static final String PATTERN_RFC1036 = "EEE, dd-MMM-yy HH:mm:ss zzz";
+
+ /**
+ * Date format pattern used to parse HTTP date headers in ANSI C
+ * <code>asctime()</code> format.
+ */
+ public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy";
+
+ private static final String[] DEFAULT_PATTERNS = new String[] {
+ PATTERN_RFC1123,
+ PATTERN_RFC1036,
+ PATTERN_ASCTIME
+ };
+
+ private static final Date DEFAULT_TWO_DIGIT_YEAR_START;
+
+ public static final TimeZone GMT = TimeZone.getTimeZone("GMT");
+
+ static {
+ final Calendar calendar = Calendar.getInstance();
+ calendar.setTimeZone(GMT);
+ calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
+ calendar.set(Calendar.MILLISECOND, 0);
+ DEFAULT_TWO_DIGIT_YEAR_START = calendar.getTime();
+ }
+
+ /**
+ * Parses a date value. The formats used for parsing the date value are retrieved from
+ * the default http params.
+ *
+ * @param dateValue the date value to parse
+ *
+ * @return the parsed date or null if input could not be parsed
+ */
+ public static Date parseDate(final String dateValue) {
+ return parseDate(dateValue, null, null);
+ }
+
+ /**
+ * Parses the date value using the given date formats.
+ *
+ * @param dateValue the date value to parse
+ * @param dateFormats the date formats to use
+ *
+ * @return the parsed date or null if input could not be parsed
+ */
+ public static Date parseDate(final String dateValue, final String[] dateFormats) {
+ return parseDate(dateValue, dateFormats, null);
+ }
+
+ /**
+ * Parses the date value using the given date formats.
+ *
+ * @param dateValue the date value to parse
+ * @param dateFormats the date formats to use
+ * @param startDate During parsing, two digit years will be placed in the range
+ * <code>startDate</code> to <code>startDate + 100 years</code>. This value may
+ * be <code>null</code>. When <code>null</code> is given as a parameter, year
+ * <code>2000</code> will be used.
+ *
+ * @return the parsed date or null if input could not be parsed
+ */
+ public static Date parseDate(
+ final String dateValue,
+ final String[] dateFormats,
+ final Date startDate) {
+ Args.notNull(dateValue, "Date value");
+ final String[] localDateFormats = dateFormats != null ? dateFormats : DEFAULT_PATTERNS;
+ final Date localStartDate = startDate != null ? startDate : DEFAULT_TWO_DIGIT_YEAR_START;
+ String v = dateValue;
+ // trim single quotes around date if present
+ // see issue #5279
+ if (v.length() > 1 && v.startsWith("'") && v.endsWith("'")) {
+ v = v.substring (1, v.length() - 1);
+ }
+
+ for (final String dateFormat : localDateFormats) {
+ final SimpleDateFormat dateParser = DateFormatHolder.formatFor(dateFormat);
+ dateParser.set2DigitYearStart(localStartDate);
+ final ParsePosition pos = new ParsePosition(0);
+ final Date result = dateParser.parse(v, pos);
+ if (pos.getIndex() != 0) {
+ return result;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Formats the given date according to the RFC 1123 pattern.
+ *
+ * @param date The date to format.
+ * @return An RFC 1123 formatted date string.
+ *
+ * @see #PATTERN_RFC1123
+ */
+ public static String formatDate(final Date date) {
+ return formatDate(date, PATTERN_RFC1123);
+ }
+
+ /**
+ * Formats the given date according to the specified pattern. The pattern
+ * must conform to that used by the {@link SimpleDateFormat simple date
+ * format} class.
+ *
+ * @param date The date to format.
+ * @param pattern The pattern to use for formatting the date.
+ * @return A formatted date string.
+ *
+ * @throws IllegalArgumentException If the given date pattern is invalid.
+ *
+ * @see SimpleDateFormat
+ */
+ public static String formatDate(final Date date, final String pattern) {
+ Args.notNull(date, "Date");
+ Args.notNull(pattern, "Pattern");
+ final SimpleDateFormat formatter = DateFormatHolder.formatFor(pattern);
+ return formatter.format(date);
+ }
+
+ /**
+ * Clears thread-local variable containing {@link java.text.DateFormat} cache.
+ *
+ * @since 4.3
+ */
+ public static void clearThreadLocal() {
+ DateFormatHolder.clearThreadLocal();
+ }
+
+ /** This class should not be instantiated. */
+ private DateUtils() {
+ }
+
+ /**
+ * A factory for {@link SimpleDateFormat}s. The instances are stored in a
+ * threadlocal way because SimpleDateFormat is not threadsafe as noted in
+ * {@link SimpleDateFormat its javadoc}.
+ *
+ */
+ final static class DateFormatHolder {
+
+ private static final ThreadLocal<SoftReference<Map<String, SimpleDateFormat>>>
+ THREADLOCAL_FORMATS = new ThreadLocal<SoftReference<Map<String, SimpleDateFormat>>>() {
+
+ @Override
+ protected SoftReference<Map<String, SimpleDateFormat>> initialValue() {
+ return new SoftReference<Map<String, SimpleDateFormat>>(
+ new HashMap<String, SimpleDateFormat>());
+ }
+
+ };
+
+ /**
+ * creates a {@link SimpleDateFormat} for the requested format string.
+ *
+ * @param pattern
+ * a non-<code>null</code> format String according to
+ * {@link SimpleDateFormat}. The format is not checked against
+ * <code>null</code> since all paths go through
+ * {@link DateUtils}.
+ * @return the requested format. This simple dateformat should not be used
+ * to {@link SimpleDateFormat#applyPattern(String) apply} to a
+ * different pattern.
+ */
+ public static SimpleDateFormat formatFor(final String pattern) {
+ final SoftReference<Map<String, SimpleDateFormat>> ref = THREADLOCAL_FORMATS.get();
+ Map<String, SimpleDateFormat> formats = ref.get();
+ if (formats == null) {
+ formats = new HashMap<String, SimpleDateFormat>();
+ THREADLOCAL_FORMATS.set(
+ new SoftReference<Map<String, SimpleDateFormat>>(formats));
+ }
+
+ SimpleDateFormat format = formats.get(pattern);
+ if (format == null) {
+ format = new SimpleDateFormat(pattern, Locale.US);
+ format.setTimeZone(TimeZone.getTimeZone("GMT"));
+ formats.put(pattern, format);
+ }
+
+ return format;
+ }
+
+ public static void clearThreadLocal() {
+ THREADLOCAL_FORMATS.remove();
+ }
+
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/HttpClientUtils.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/HttpClientUtils.java
new file mode 100644
index 0000000000..3c7769c2a5
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/HttpClientUtils.java
@@ -0,0 +1,149 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.utils;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.HttpClient;
+import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
+import ch.boye.httpclientandroidlib.util.EntityUtils;
+
+/**
+ * Convenience methods for closing response and client objects.
+ *
+ * @since 4.2
+ */
+public class HttpClientUtils {
+
+ private HttpClientUtils() {
+ }
+
+ /**
+ * Unconditionally close a response.
+ * <p>
+ * Example Code:
+ *
+ * <pre>
+ * HttpResponse httpResponse = null;
+ * try {
+ * httpResponse = httpClient.execute(httpGet);
+ * } catch (Exception e) {
+ * // error handling
+ * } finally {
+ * HttpClientUtils.closeQuietly(httpResponse);
+ * }
+ * </pre>
+ *
+ * @param response
+ * the HttpResponse to release resources, may be null or already
+ * closed.
+ *
+ * @since 4.2
+ */
+ public static void closeQuietly(final HttpResponse response) {
+ if (response != null) {
+ final HttpEntity entity = response.getEntity();
+ if (entity != null) {
+ try {
+ EntityUtils.consume(entity);
+ } catch (final IOException ex) {
+ }
+ }
+ }
+ }
+
+ /**
+ * Unconditionally close a response.
+ * <p>
+ * Example Code:
+ *
+ * <pre>
+ * HttpResponse httpResponse = null;
+ * try {
+ * httpResponse = httpClient.execute(httpGet);
+ * } catch (Exception e) {
+ * // error handling
+ * } finally {
+ * HttpClientUtils.closeQuietly(httpResponse);
+ * }
+ * </pre>
+ *
+ * @param response
+ * the HttpResponse to release resources, may be null or already
+ * closed.
+ *
+ * @since 4.3
+ */
+ public static void closeQuietly(final CloseableHttpResponse response) {
+ if (response != null) {
+ try {
+ try {
+ EntityUtils.consume(response.getEntity());
+ } finally {
+ response.close();
+ }
+ } catch (final IOException ignore) {
+ }
+ }
+ }
+
+ /**
+ * Unconditionally close a httpClient. Shuts down the underlying connection
+ * manager and releases the resources.
+ * <p>
+ * Example Code:
+ *
+ * <pre>
+ * HttpClient httpClient = HttpClients.createDefault();
+ * try {
+ * httpClient.execute(request);
+ * } catch (Exception e) {
+ * // error handling
+ * } finally {
+ * HttpClientUtils.closeQuietly(httpClient);
+ * }
+ * </pre>
+ *
+ * @param httpClient
+ * the HttpClient to close, may be null or already closed.
+ * @since 4.2
+ */
+ public static void closeQuietly(final HttpClient httpClient) {
+ if (httpClient != null) {
+ if (httpClient instanceof Closeable) {
+ try {
+ ((Closeable) httpClient).close();
+ } catch (final IOException ignore) {
+ }
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/Idn.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/Idn.java
new file mode 100644
index 0000000000..a2b5bd036b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/Idn.java
@@ -0,0 +1,42 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.utils;
+
+/**
+ * Abstraction of international domain name (IDN) conversion.
+ *
+ * @since 4.0
+ */
+public interface Idn {
+ /**
+ * Converts a name from its punycode representation to Unicode.
+ * The name may be a single hostname or a dot-separated qualified domain name.
+ * @param punycode the Punycode representation
+ * @return the Unicode domain name
+ */
+ String toUnicode(String punycode);
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/JdkIdn.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/JdkIdn.java
new file mode 100644
index 0000000000..53d46fe23b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/JdkIdn.java
@@ -0,0 +1,71 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.utils;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Uses the java.net.IDN class through reflection.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class JdkIdn implements Idn {
+ private final Method toUnicode;
+
+ /**
+ *
+ * @throws ClassNotFoundException if java.net.IDN is not available
+ */
+ public JdkIdn() throws ClassNotFoundException {
+ final Class<?> clazz = Class.forName("java.net.IDN");
+ try {
+ toUnicode = clazz.getMethod("toUnicode", String.class);
+ } catch (final SecurityException e) {
+ // doesn't happen
+ throw new IllegalStateException(e.getMessage(), e);
+ } catch (final NoSuchMethodException e) {
+ // doesn't happen
+ throw new IllegalStateException(e.getMessage(), e);
+ }
+ }
+
+ public String toUnicode(final String punycode) {
+ try {
+ return (String) toUnicode.invoke(null, punycode);
+ } catch (final IllegalAccessException e) {
+ throw new IllegalStateException(e.getMessage(), e);
+ } catch (final InvocationTargetException e) {
+ final Throwable t = e.getCause();
+ throw new RuntimeException(t.getMessage(), t);
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/Punycode.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/Punycode.java
new file mode 100644
index 0000000000..fa4872c415
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/Punycode.java
@@ -0,0 +1,54 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.utils;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Facade that provides conversion between Unicode and Punycode domain names.
+ * It will use an appropriate implementation.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class Punycode {
+ private static final Idn impl;
+ static {
+ Idn _impl;
+ try {
+ _impl = new JdkIdn();
+ } catch (final Exception e) {
+ _impl = new Rfc3492Idn();
+ }
+ impl = _impl;
+ }
+
+ public static String toUnicode(final String punycode) {
+ return impl.toUnicode(punycode);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/Rfc3492Idn.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/Rfc3492Idn.java
new file mode 100644
index 0000000000..ccf9800503
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/Rfc3492Idn.java
@@ -0,0 +1,141 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.utils;
+
+import java.util.StringTokenizer;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Implementation from pseudo code in RFC 3492.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class Rfc3492Idn implements Idn {
+ private static final int base = 36;
+ private static final int tmin = 1;
+ private static final int tmax = 26;
+ private static final int skew = 38;
+ private static final int damp = 700;
+ private static final int initial_bias = 72;
+ private static final int initial_n = 128;
+ private static final char delimiter = '-';
+ private static final String ACE_PREFIX = "xn--";
+
+ private int adapt(final int delta, final int numpoints, final boolean firsttime) {
+ int d = delta;
+ if (firsttime) {
+ d = d / damp;
+ } else {
+ d = d / 2;
+ }
+ d = d + (d / numpoints);
+ int k = 0;
+ while (d > ((base - tmin) * tmax) / 2) {
+ d = d / (base - tmin);
+ k = k + base;
+ }
+ return k + (((base - tmin + 1) * d) / (d + skew));
+ }
+
+ private int digit(final char c) {
+ if ((c >= 'A') && (c <= 'Z')) {
+ return (c - 'A');
+ }
+ if ((c >= 'a') && (c <= 'z')) {
+ return (c - 'a');
+ }
+ if ((c >= '0') && (c <= '9')) {
+ return (c - '0') + 26;
+ }
+ throw new IllegalArgumentException("illegal digit: "+ c);
+ }
+
+ public String toUnicode(final String punycode) {
+ final StringBuilder unicode = new StringBuilder(punycode.length());
+ final StringTokenizer tok = new StringTokenizer(punycode, ".");
+ while (tok.hasMoreTokens()) {
+ String t = tok.nextToken();
+ if (unicode.length() > 0) {
+ unicode.append('.');
+ }
+ if (t.startsWith(ACE_PREFIX)) {
+ t = decode(t.substring(4));
+ }
+ unicode.append(t);
+ }
+ return unicode.toString();
+ }
+
+ protected String decode(final String s) {
+ String input = s;
+ int n = initial_n;
+ int i = 0;
+ int bias = initial_bias;
+ final StringBuilder output = new StringBuilder(input.length());
+ final int lastdelim = input.lastIndexOf(delimiter);
+ if (lastdelim != -1) {
+ output.append(input.subSequence(0, lastdelim));
+ input = input.substring(lastdelim + 1);
+ }
+
+ while (input.length() > 0) {
+ final int oldi = i;
+ int w = 1;
+ for (int k = base;; k += base) {
+ if (input.length() == 0) {
+ break;
+ }
+ final char c = input.charAt(0);
+ input = input.substring(1);
+ final int digit = digit(c);
+ i = i + digit * w; // FIXME fail on overflow
+ final int t;
+ if (k <= bias + tmin) {
+ t = tmin;
+ } else if (k >= bias + tmax) {
+ t = tmax;
+ } else {
+ t = k - bias;
+ }
+ if (digit < t) {
+ break;
+ }
+ w = w * (base - t); // FIXME fail on overflow
+ }
+ bias = adapt(i - oldi, output.length() + 1, (oldi == 0));
+ n = n + i / (output.length() + 1); // FIXME fail on overflow
+ i = i % (output.length() + 1);
+ // {if n is a basic code point then fail}
+ output.insert(i, (char) n);
+ i++;
+ }
+ return output.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/URIBuilder.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/URIBuilder.java
new file mode 100644
index 0000000000..e41958f7f0
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/URIBuilder.java
@@ -0,0 +1,490 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.utils;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.Consts;
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.conn.util.InetAddressUtils;
+import ch.boye.httpclientandroidlib.message.BasicNameValuePair;
+
+/**
+ * Builder for {@link URI} instances.
+ *
+ * @since 4.2
+ */
+@NotThreadSafe
+public class URIBuilder {
+
+ private String scheme;
+ private String encodedSchemeSpecificPart;
+ private String encodedAuthority;
+ private String userInfo;
+ private String encodedUserInfo;
+ private String host;
+ private int port;
+ private String path;
+ private String encodedPath;
+ private String encodedQuery;
+ private List<NameValuePair> queryParams;
+ private String query;
+ private String fragment;
+ private String encodedFragment;
+
+ /**
+ * Constructs an empty instance.
+ */
+ public URIBuilder() {
+ super();
+ this.port = -1;
+ }
+
+ /**
+ * Construct an instance from the string which must be a valid URI.
+ *
+ * @param string a valid URI in string form
+ * @throws URISyntaxException if the input is not a valid URI
+ */
+ public URIBuilder(final String string) throws URISyntaxException {
+ super();
+ digestURI(new URI(string));
+ }
+
+ /**
+ * Construct an instance from the provided URI.
+ * @param uri
+ */
+ public URIBuilder(final URI uri) {
+ super();
+ digestURI(uri);
+ }
+
+ private List <NameValuePair> parseQuery(final String query, final Charset charset) {
+ if (query != null && query.length() > 0) {
+ return URLEncodedUtils.parse(query, charset);
+ }
+ return null;
+ }
+
+ /**
+ * Builds a {@link URI} instance.
+ */
+ public URI build() throws URISyntaxException {
+ return new URI(buildString());
+ }
+
+ private String buildString() {
+ final StringBuilder sb = new StringBuilder();
+ if (this.scheme != null) {
+ sb.append(this.scheme).append(':');
+ }
+ if (this.encodedSchemeSpecificPart != null) {
+ sb.append(this.encodedSchemeSpecificPart);
+ } else {
+ if (this.encodedAuthority != null) {
+ sb.append("//").append(this.encodedAuthority);
+ } else if (this.host != null) {
+ sb.append("//");
+ if (this.encodedUserInfo != null) {
+ sb.append(this.encodedUserInfo).append("@");
+ } else if (this.userInfo != null) {
+ sb.append(encodeUserInfo(this.userInfo)).append("@");
+ }
+ if (InetAddressUtils.isIPv6Address(this.host)) {
+ sb.append("[").append(this.host).append("]");
+ } else {
+ sb.append(this.host);
+ }
+ if (this.port >= 0) {
+ sb.append(":").append(this.port);
+ }
+ }
+ if (this.encodedPath != null) {
+ sb.append(normalizePath(this.encodedPath));
+ } else if (this.path != null) {
+ sb.append(encodePath(normalizePath(this.path)));
+ }
+ if (this.encodedQuery != null) {
+ sb.append("?").append(this.encodedQuery);
+ } else if (this.queryParams != null) {
+ sb.append("?").append(encodeUrlForm(this.queryParams));
+ } else if (this.query != null) {
+ sb.append("?").append(encodeUric(this.query));
+ }
+ }
+ if (this.encodedFragment != null) {
+ sb.append("#").append(this.encodedFragment);
+ } else if (this.fragment != null) {
+ sb.append("#").append(encodeUric(this.fragment));
+ }
+ return sb.toString();
+ }
+
+ private void digestURI(final URI uri) {
+ this.scheme = uri.getScheme();
+ this.encodedSchemeSpecificPart = uri.getRawSchemeSpecificPart();
+ this.encodedAuthority = uri.getRawAuthority();
+ this.host = uri.getHost();
+ this.port = uri.getPort();
+ this.encodedUserInfo = uri.getRawUserInfo();
+ this.userInfo = uri.getUserInfo();
+ this.encodedPath = uri.getRawPath();
+ this.path = uri.getPath();
+ this.encodedQuery = uri.getRawQuery();
+ this.queryParams = parseQuery(uri.getRawQuery(), Consts.UTF_8);
+ this.encodedFragment = uri.getRawFragment();
+ this.fragment = uri.getFragment();
+ }
+
+ private String encodeUserInfo(final String userInfo) {
+ return URLEncodedUtils.encUserInfo(userInfo, Consts.UTF_8);
+ }
+
+ private String encodePath(final String path) {
+ return URLEncodedUtils.encPath(path, Consts.UTF_8);
+ }
+
+ private String encodeUrlForm(final List<NameValuePair> params) {
+ return URLEncodedUtils.format(params, Consts.UTF_8);
+ }
+
+ private String encodeUric(final String fragment) {
+ return URLEncodedUtils.encUric(fragment, Consts.UTF_8);
+ }
+
+ /**
+ * Sets URI scheme.
+ */
+ public URIBuilder setScheme(final String scheme) {
+ this.scheme = scheme;
+ return this;
+ }
+
+ /**
+ * Sets URI user info. The value is expected to be unescaped and may contain non ASCII
+ * characters.
+ */
+ public URIBuilder setUserInfo(final String userInfo) {
+ this.userInfo = userInfo;
+ this.encodedSchemeSpecificPart = null;
+ this.encodedAuthority = null;
+ this.encodedUserInfo = null;
+ return this;
+ }
+
+ /**
+ * Sets URI user info as a combination of username and password. These values are expected to
+ * be unescaped and may contain non ASCII characters.
+ */
+ public URIBuilder setUserInfo(final String username, final String password) {
+ return setUserInfo(username + ':' + password);
+ }
+
+ /**
+ * Sets URI host.
+ */
+ public URIBuilder setHost(final String host) {
+ this.host = host;
+ this.encodedSchemeSpecificPart = null;
+ this.encodedAuthority = null;
+ return this;
+ }
+
+ /**
+ * Sets URI port.
+ */
+ public URIBuilder setPort(final int port) {
+ this.port = port < 0 ? -1 : port;
+ this.encodedSchemeSpecificPart = null;
+ this.encodedAuthority = null;
+ return this;
+ }
+
+ /**
+ * Sets URI path. The value is expected to be unescaped and may contain non ASCII characters.
+ */
+ public URIBuilder setPath(final String path) {
+ this.path = path;
+ this.encodedSchemeSpecificPart = null;
+ this.encodedPath = null;
+ return this;
+ }
+
+ /**
+ * Removes URI query.
+ */
+ public URIBuilder removeQuery() {
+ this.queryParams = null;
+ this.query = null;
+ this.encodedQuery = null;
+ this.encodedSchemeSpecificPart = null;
+ return this;
+ }
+
+ /**
+ * Sets URI query.
+ * <p>
+ * The value is expected to be encoded form data.
+ *
+ * @deprecated (4.3) use {@link #setParameters(List)} or {@link #setParameters(NameValuePair...)}
+ *
+ * @see URLEncodedUtils#parse
+ */
+ @Deprecated
+ public URIBuilder setQuery(final String query) {
+ this.queryParams = parseQuery(query, Consts.UTF_8);
+ this.query = null;
+ this.encodedQuery = null;
+ this.encodedSchemeSpecificPart = null;
+ return this;
+ }
+
+ /**
+ * Sets URI query parameters. The parameter name / values are expected to be unescaped
+ * and may contain non ASCII characters.
+ * <p/>
+ * Please note query parameters and custom query component are mutually exclusive. This method
+ * will remove custom query if present.
+ *
+ * @since 4.3
+ */
+ public URIBuilder setParameters(final List <NameValuePair> nvps) {
+ if (this.queryParams == null) {
+ this.queryParams = new ArrayList<NameValuePair>();
+ } else {
+ this.queryParams.clear();
+ }
+ this.queryParams.addAll(nvps);
+ this.encodedQuery = null;
+ this.encodedSchemeSpecificPart = null;
+ this.query = null;
+ return this;
+ }
+
+ /**
+ * Adds URI query parameters. The parameter name / values are expected to be unescaped
+ * and may contain non ASCII characters.
+ * <p/>
+ * Please note query parameters and custom query component are mutually exclusive. This method
+ * will remove custom query if present.
+ *
+ * @since 4.3
+ */
+ public URIBuilder addParameters(final List <NameValuePair> nvps) {
+ if (this.queryParams == null) {
+ this.queryParams = new ArrayList<NameValuePair>();
+ }
+ this.queryParams.addAll(nvps);
+ this.encodedQuery = null;
+ this.encodedSchemeSpecificPart = null;
+ this.query = null;
+ return this;
+ }
+
+ /**
+ * Sets URI query parameters. The parameter name / values are expected to be unescaped
+ * and may contain non ASCII characters.
+ * <p/>
+ * Please note query parameters and custom query component are mutually exclusive. This method
+ * will remove custom query if present.
+ *
+ * @since 4.3
+ */
+ public URIBuilder setParameters(final NameValuePair... nvps) {
+ if (this.queryParams == null) {
+ this.queryParams = new ArrayList<NameValuePair>();
+ } else {
+ this.queryParams.clear();
+ }
+ for (final NameValuePair nvp: nvps) {
+ this.queryParams.add(nvp);
+ }
+ this.encodedQuery = null;
+ this.encodedSchemeSpecificPart = null;
+ this.query = null;
+ return this;
+ }
+
+ /**
+ * Adds parameter to URI query. The parameter name and value are expected to be unescaped
+ * and may contain non ASCII characters.
+ * <p/>
+ * Please note query parameters and custom query component are mutually exclusive. This method
+ * will remove custom query if present.
+ */
+ public URIBuilder addParameter(final String param, final String value) {
+ if (this.queryParams == null) {
+ this.queryParams = new ArrayList<NameValuePair>();
+ }
+ this.queryParams.add(new BasicNameValuePair(param, value));
+ this.encodedQuery = null;
+ this.encodedSchemeSpecificPart = null;
+ this.query = null;
+ return this;
+ }
+
+ /**
+ * Sets parameter of URI query overriding existing value if set. The parameter name and value
+ * are expected to be unescaped and may contain non ASCII characters.
+ * <p/>
+ * Please note query parameters and custom query component are mutually exclusive. This method
+ * will remove custom query if present.
+ */
+ public URIBuilder setParameter(final String param, final String value) {
+ if (this.queryParams == null) {
+ this.queryParams = new ArrayList<NameValuePair>();
+ }
+ if (!this.queryParams.isEmpty()) {
+ for (final Iterator<NameValuePair> it = this.queryParams.iterator(); it.hasNext(); ) {
+ final NameValuePair nvp = it.next();
+ if (nvp.getName().equals(param)) {
+ it.remove();
+ }
+ }
+ }
+ this.queryParams.add(new BasicNameValuePair(param, value));
+ this.encodedQuery = null;
+ this.encodedSchemeSpecificPart = null;
+ this.query = null;
+ return this;
+ }
+
+ /**
+ * Clears URI query parameters.
+ *
+ * @since 4.3
+ */
+ public URIBuilder clearParameters() {
+ this.queryParams = null;
+ this.encodedQuery = null;
+ this.encodedSchemeSpecificPart = null;
+ return this;
+ }
+
+ /**
+ * Sets custom URI query. The value is expected to be unescaped and may contain non ASCII
+ * characters.
+ * <p/>
+ * Please note query parameters and custom query component are mutually exclusive. This method
+ * will remove query parameters if present.
+ *
+ * @since 4.3
+ */
+ public URIBuilder setCustomQuery(final String query) {
+ this.query = query;
+ this.encodedQuery = null;
+ this.encodedSchemeSpecificPart = null;
+ this.queryParams = null;
+ return this;
+ }
+
+ /**
+ * Sets URI fragment. The value is expected to be unescaped and may contain non ASCII
+ * characters.
+ */
+ public URIBuilder setFragment(final String fragment) {
+ this.fragment = fragment;
+ this.encodedFragment = null;
+ return this;
+ }
+
+ /**
+ * @since 4.3
+ */
+ public boolean isAbsolute() {
+ return this.scheme != null;
+ }
+
+ /**
+ * @since 4.3
+ */
+ public boolean isOpaque() {
+ return this.path == null;
+ }
+
+ public String getScheme() {
+ return this.scheme;
+ }
+
+ public String getUserInfo() {
+ return this.userInfo;
+ }
+
+ public String getHost() {
+ return this.host;
+ }
+
+ public int getPort() {
+ return this.port;
+ }
+
+ public String getPath() {
+ return this.path;
+ }
+
+ public List<NameValuePair> getQueryParams() {
+ if (this.queryParams != null) {
+ return new ArrayList<NameValuePair>(this.queryParams);
+ } else {
+ return new ArrayList<NameValuePair>();
+ }
+ }
+
+ public String getFragment() {
+ return this.fragment;
+ }
+
+ @Override
+ public String toString() {
+ return buildString();
+ }
+
+ private static String normalizePath(final String path) {
+ String s = path;
+ if (s == null) {
+ return null;
+ }
+ int n = 0;
+ for (; n < s.length(); n++) {
+ if (s.charAt(n) != '/') {
+ break;
+ }
+ }
+ if (n > 1) {
+ s = s.substring(n - 1);
+ }
+ return s;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/URIUtils.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/URIUtils.java
new file mode 100644
index 0000000000..73619c90af
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/URIUtils.java
@@ -0,0 +1,428 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.client.utils;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+import java.util.Locale;
+import java.util.Stack;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.TextUtils;
+
+/**
+ * A collection of utilities for {@link URI URIs}, to workaround
+ * bugs within the class or for ease-of-use features.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class URIUtils {
+
+ /**
+ * Constructs a {@link URI} using all the parameters. This should be
+ * used instead of
+ * {@link URI#URI(String, String, String, int, String, String, String)}
+ * or any of the other URI multi-argument URI constructors.
+ *
+ * @param scheme
+ * Scheme name
+ * @param host
+ * Host name
+ * @param port
+ * Port number
+ * @param path
+ * Path
+ * @param query
+ * Query
+ * @param fragment
+ * Fragment
+ *
+ * @throws URISyntaxException
+ * If both a scheme and a path are given but the path is
+ * relative, if the URI string constructed from the given
+ * components violates RFC&nbsp;2396, or if the authority
+ * component of the string is present but cannot be parsed
+ * as a server-based authority
+ *
+ * @deprecated (4.2) use {@link URIBuilder}.
+ */
+ @Deprecated
+ public static URI createURI(
+ final String scheme,
+ final String host,
+ final int port,
+ final String path,
+ final String query,
+ final String fragment) throws URISyntaxException {
+ final StringBuilder buffer = new StringBuilder();
+ if (host != null) {
+ if (scheme != null) {
+ buffer.append(scheme);
+ buffer.append("://");
+ }
+ buffer.append(host);
+ if (port > 0) {
+ buffer.append(':');
+ buffer.append(port);
+ }
+ }
+ if (path == null || !path.startsWith("/")) {
+ buffer.append('/');
+ }
+ if (path != null) {
+ buffer.append(path);
+ }
+ if (query != null) {
+ buffer.append('?');
+ buffer.append(query);
+ }
+ if (fragment != null) {
+ buffer.append('#');
+ buffer.append(fragment);
+ }
+ return new URI(buffer.toString());
+ }
+
+ /**
+ * A convenience method for creating a new {@link URI} whose scheme, host
+ * and port are taken from the target host, but whose path, query and
+ * fragment are taken from the existing URI. The fragment is only used if
+ * dropFragment is false. The path is set to "/" if not explicitly specified.
+ *
+ * @param uri
+ * Contains the path, query and fragment to use.
+ * @param target
+ * Contains the scheme, host and port to use.
+ * @param dropFragment
+ * True if the fragment should not be copied.
+ *
+ * @throws URISyntaxException
+ * If the resulting URI is invalid.
+ */
+ public static URI rewriteURI(
+ final URI uri,
+ final HttpHost target,
+ final boolean dropFragment) throws URISyntaxException {
+ Args.notNull(uri, "URI");
+ if (uri.isOpaque()) {
+ return uri;
+ }
+ final URIBuilder uribuilder = new URIBuilder(uri);
+ if (target != null) {
+ uribuilder.setScheme(target.getSchemeName());
+ uribuilder.setHost(target.getHostName());
+ uribuilder.setPort(target.getPort());
+ } else {
+ uribuilder.setScheme(null);
+ uribuilder.setHost(null);
+ uribuilder.setPort(-1);
+ }
+ if (dropFragment) {
+ uribuilder.setFragment(null);
+ }
+ if (TextUtils.isEmpty(uribuilder.getPath())) {
+ uribuilder.setPath("/");
+ }
+ return uribuilder.build();
+ }
+
+ /**
+ * A convenience method for
+ * {@link URIUtils#rewriteURI(URI, HttpHost, boolean)} that always keeps the
+ * fragment.
+ */
+ public static URI rewriteURI(
+ final URI uri,
+ final HttpHost target) throws URISyntaxException {
+ return rewriteURI(uri, target, false);
+ }
+
+ /**
+ * A convenience method that creates a new {@link URI} whose scheme, host, port, path,
+ * query are taken from the existing URI, dropping any fragment or user-information.
+ * The path is set to "/" if not explicitly specified. The existing URI is returned
+ * unmodified if it has no fragment or user-information and has a path.
+ *
+ * @param uri
+ * original URI.
+ * @throws URISyntaxException
+ * If the resulting URI is invalid.
+ */
+ public static URI rewriteURI(final URI uri) throws URISyntaxException {
+ Args.notNull(uri, "URI");
+ if (uri.isOpaque()) {
+ return uri;
+ }
+ final URIBuilder uribuilder = new URIBuilder(uri);
+ if (uribuilder.getUserInfo() != null) {
+ uribuilder.setUserInfo(null);
+ }
+ if (TextUtils.isEmpty(uribuilder.getPath())) {
+ uribuilder.setPath("/");
+ }
+ if (uribuilder.getHost() != null) {
+ uribuilder.setHost(uribuilder.getHost().toLowerCase(Locale.ENGLISH));
+ }
+ uribuilder.setFragment(null);
+ return uribuilder.build();
+ }
+
+ /**
+ * Resolves a URI reference against a base URI. Work-around for bug in
+ * java.net.URI (<http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4708535>)
+ *
+ * @param baseURI the base URI
+ * @param reference the URI reference
+ * @return the resulting URI
+ */
+ public static URI resolve(final URI baseURI, final String reference) {
+ return URIUtils.resolve(baseURI, URI.create(reference));
+ }
+
+ /**
+ * Resolves a URI reference against a base URI. Work-around for bugs in
+ * java.net.URI (e.g. <http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4708535>)
+ *
+ * @param baseURI the base URI
+ * @param reference the URI reference
+ * @return the resulting URI
+ */
+ public static URI resolve(final URI baseURI, final URI reference){
+ Args.notNull(baseURI, "Base URI");
+ Args.notNull(reference, "Reference URI");
+ URI ref = reference;
+ final String s = ref.toString();
+ if (s.startsWith("?")) {
+ return resolveReferenceStartingWithQueryString(baseURI, ref);
+ }
+ final boolean emptyReference = s.length() == 0;
+ if (emptyReference) {
+ ref = URI.create("#");
+ }
+ URI resolved = baseURI.resolve(ref);
+ if (emptyReference) {
+ final String resolvedString = resolved.toString();
+ resolved = URI.create(resolvedString.substring(0,
+ resolvedString.indexOf('#')));
+ }
+ return normalizeSyntax(resolved);
+ }
+
+ /**
+ * Resolves a reference starting with a query string.
+ *
+ * @param baseURI the base URI
+ * @param reference the URI reference starting with a query string
+ * @return the resulting URI
+ */
+ private static URI resolveReferenceStartingWithQueryString(
+ final URI baseURI, final URI reference) {
+ String baseUri = baseURI.toString();
+ baseUri = baseUri.indexOf('?') > -1 ?
+ baseUri.substring(0, baseUri.indexOf('?')) : baseUri;
+ return URI.create(baseUri + reference.toString());
+ }
+
+ /**
+ * Removes dot segments according to RFC 3986, section 5.2.4 and
+ * Syntax-Based Normalization according to RFC 3986, section 6.2.2.
+ *
+ * @param uri the original URI
+ * @return the URI without dot segments
+ */
+ private static URI normalizeSyntax(final URI uri) {
+ if (uri.isOpaque() || uri.getAuthority() == null) {
+ // opaque and file: URIs
+ return uri;
+ }
+ Args.check(uri.isAbsolute(), "Base URI must be absolute");
+ final String path = uri.getPath() == null ? "" : uri.getPath();
+ final String[] inputSegments = path.split("/");
+ final Stack<String> outputSegments = new Stack<String>();
+ for (final String inputSegment : inputSegments) {
+ if ((inputSegment.length() == 0)
+ || (".".equals(inputSegment))) {
+ // Do nothing
+ } else if ("..".equals(inputSegment)) {
+ if (!outputSegments.isEmpty()) {
+ outputSegments.pop();
+ }
+ } else {
+ outputSegments.push(inputSegment);
+ }
+ }
+ final StringBuilder outputBuffer = new StringBuilder();
+ for (final String outputSegment : outputSegments) {
+ outputBuffer.append('/').append(outputSegment);
+ }
+ if (path.lastIndexOf('/') == path.length() - 1) {
+ // path.endsWith("/") || path.equals("")
+ outputBuffer.append('/');
+ }
+ try {
+ final String scheme = uri.getScheme().toLowerCase(Locale.ENGLISH);
+ final String auth = uri.getAuthority().toLowerCase(Locale.ENGLISH);
+ final URI ref = new URI(scheme, auth, outputBuffer.toString(),
+ null, null);
+ if (uri.getQuery() == null && uri.getFragment() == null) {
+ return ref;
+ }
+ final StringBuilder normalized = new StringBuilder(
+ ref.toASCIIString());
+ if (uri.getQuery() != null) {
+ // query string passed through unchanged
+ normalized.append('?').append(uri.getRawQuery());
+ }
+ if (uri.getFragment() != null) {
+ // fragment passed through unchanged
+ normalized.append('#').append(uri.getRawFragment());
+ }
+ return URI.create(normalized.toString());
+ } catch (final URISyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Extracts target host from the given {@link URI}.
+ *
+ * @param uri
+ * @return the target host if the URI is absolute or <code>null</null> if the URI is
+ * relative or does not contain a valid host name.
+ *
+ * @since 4.1
+ */
+ public static HttpHost extractHost(final URI uri) {
+ if (uri == null) {
+ return null;
+ }
+ HttpHost target = null;
+ if (uri.isAbsolute()) {
+ int port = uri.getPort(); // may be overridden later
+ String host = uri.getHost();
+ if (host == null) { // normal parse failed; let's do it ourselves
+ // authority does not seem to care about the valid character-set for host names
+ host = uri.getAuthority();
+ if (host != null) {
+ // Strip off any leading user credentials
+ final int at = host.indexOf('@');
+ if (at >= 0) {
+ if (host.length() > at+1 ) {
+ host = host.substring(at+1);
+ } else {
+ host = null; // @ on its own
+ }
+ }
+ // Extract the port suffix, if present
+ if (host != null) {
+ final int colon = host.indexOf(':');
+ if (colon >= 0) {
+ final int pos = colon + 1;
+ int len = 0;
+ for (int i = pos; i < host.length(); i++) {
+ if (Character.isDigit(host.charAt(i))) {
+ len++;
+ } else {
+ break;
+ }
+ }
+ if (len > 0) {
+ try {
+ port = Integer.parseInt(host.substring(pos, pos + len));
+ } catch (final NumberFormatException ex) {
+ }
+ }
+ host = host.substring(0, colon);
+ }
+ }
+ }
+ }
+ final String scheme = uri.getScheme();
+ if (!TextUtils.isBlank(host)) {
+ target = new HttpHost(host, port, scheme);
+ }
+ }
+ return target;
+ }
+
+ /**
+ * Derives the interpreted (absolute) URI that was used to generate the last
+ * request. This is done by extracting the request-uri and target origin for
+ * the last request and scanning all the redirect locations for the last
+ * fragment identifier, then combining the result into a {@link URI}.
+ *
+ * @param originalURI
+ * original request before any redirects
+ * @param target
+ * if the last URI is relative, it is resolved against this target,
+ * or <code>null</code> if not available.
+ * @param redirects
+ * collection of redirect locations since the original request
+ * or <code>null</code> if not available.
+ * @return interpreted (absolute) URI
+ */
+ public static URI resolve(
+ final URI originalURI,
+ final HttpHost target,
+ final List<URI> redirects) throws URISyntaxException {
+ Args.notNull(originalURI, "Request URI");
+ final URIBuilder uribuilder;
+ if (redirects == null || redirects.isEmpty()) {
+ uribuilder = new URIBuilder(originalURI);
+ } else {
+ uribuilder = new URIBuilder(redirects.get(redirects.size() - 1));
+ String frag = uribuilder.getFragment();
+ // read interpreted fragment identifier from redirect locations
+ for (int i = redirects.size() - 1; frag == null && i >= 0; i--) {
+ frag = redirects.get(i).getFragment();
+ }
+ uribuilder.setFragment(frag);
+ }
+ // read interpreted fragment identifier from original request
+ if (uribuilder.getFragment() == null) {
+ uribuilder.setFragment(originalURI.getFragment());
+ }
+ // last target origin
+ if (target != null && !uribuilder.isAbsolute()) {
+ uribuilder.setScheme(target.getSchemeName());
+ uribuilder.setHost(target.getHostName());
+ uribuilder.setPort(target.getPort());
+ }
+ return uribuilder.build();
+ }
+
+ /**
+ * This class should not be instantiated.
+ */
+ private URIUtils() {
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/URLEncodedUtils.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/URLEncodedUtils.java
new file mode 100644
index 0000000000..97465401fb
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/URLEncodedUtils.java
@@ -0,0 +1,628 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.client.utils;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Scanner;
+
+import ch.boye.httpclientandroidlib.Consts;
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.entity.ContentType;
+import ch.boye.httpclientandroidlib.message.BasicHeaderValueParser;
+import ch.boye.httpclientandroidlib.message.BasicNameValuePair;
+import ch.boye.httpclientandroidlib.message.ParserCursor;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+import ch.boye.httpclientandroidlib.util.EntityUtils;
+
+/**
+ * A collection of utilities for encoding URLs.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class URLEncodedUtils {
+
+ /**
+ * The default HTML form content type.
+ */
+ public static final String CONTENT_TYPE = "application/x-www-form-urlencoded";
+
+ private static final char QP_SEP_A = '&';
+ private static final char QP_SEP_S = ';';
+ private static final String NAME_VALUE_SEPARATOR = "=";
+
+ /**
+ * Returns a list of {@link NameValuePair NameValuePairs} as built from the URI's query portion. For example, a URI
+ * of http://example.org/path/to/file?a=1&b=2&c=3 would return a list of three NameValuePairs, one for a=1, one for
+ * b=2, and one for c=3. By convention, {@code '&'} and {@code ';'} are accepted as parameter separators.
+ * <p>
+ * This is typically useful while parsing an HTTP PUT.
+ *
+ * This API is currently only used for testing.
+ *
+ * @param uri
+ * URI to parse
+ * @param charset
+ * Charset name to use while parsing the query
+ * @return a list of {@link NameValuePair} as built from the URI's query portion.
+ */
+ public static List <NameValuePair> parse(final URI uri, final String charset) {
+ final String query = uri.getRawQuery();
+ if (query != null && query.length() > 0) {
+ final List<NameValuePair> result = new ArrayList<NameValuePair>();
+ final Scanner scanner = new Scanner(query);
+ parse(result, scanner, QP_SEP_PATTERN, charset);
+ return result;
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Returns a list of {@link NameValuePair NameValuePairs} as parsed from an {@link HttpEntity}. The encoding is
+ * taken from the entity's Content-Encoding header.
+ * <p>
+ * This is typically used while parsing an HTTP POST.
+ *
+ * @param entity
+ * The entity to parse
+ * @return a list of {@link NameValuePair} as built from the URI's query portion.
+ * @throws IOException
+ * If there was an exception getting the entity's data.
+ */
+ public static List <NameValuePair> parse(
+ final HttpEntity entity) throws IOException {
+ final ContentType contentType = ContentType.get(entity);
+ if (contentType != null && contentType.getMimeType().equalsIgnoreCase(CONTENT_TYPE)) {
+ final String content = EntityUtils.toString(entity, Consts.ASCII);
+ if (content != null && content.length() > 0) {
+ Charset charset = contentType.getCharset();
+ if (charset == null) {
+ charset = HTTP.DEF_CONTENT_CHARSET;
+ }
+ return parse(content, charset, QP_SEPS);
+ }
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Returns true if the entity's Content-Type header is
+ * <code>application/x-www-form-urlencoded</code>.
+ */
+ public static boolean isEncoded(final HttpEntity entity) {
+ final Header h = entity.getContentType();
+ if (h != null) {
+ final HeaderElement[] elems = h.getElements();
+ if (elems.length > 0) {
+ final String contentType = elems[0].getName();
+ return contentType.equalsIgnoreCase(CONTENT_TYPE);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Adds all parameters within the Scanner to the list of <code>parameters</code>, as encoded by
+ * <code>encoding</code>. For example, a scanner containing the string <code>a=1&b=2&c=3</code> would add the
+ * {@link NameValuePair NameValuePairs} a=1, b=2, and c=3 to the list of parameters. By convention, {@code '&'} and
+ * {@code ';'} are accepted as parameter separators.
+ *
+ * @param parameters
+ * List to add parameters to.
+ * @param scanner
+ * Input that contains the parameters to parse.
+ * @param charset
+ * Encoding to use when decoding the parameters.
+ */
+ public static void parse(
+ final List <NameValuePair> parameters,
+ final Scanner scanner,
+ final String charset) {
+ parse(parameters, scanner, QP_SEP_PATTERN, charset);
+ }
+
+ /**
+ * Adds all parameters within the Scanner to the list of
+ * <code>parameters</code>, as encoded by <code>encoding</code>. For
+ * example, a scanner containing the string <code>a=1&b=2&c=3</code> would
+ * add the {@link NameValuePair NameValuePairs} a=1, b=2, and c=3 to the
+ * list of parameters.
+ *
+ * @param parameters
+ * List to add parameters to.
+ * @param scanner
+ * Input that contains the parameters to parse.
+ * @param parameterSepartorPattern
+ * The Pattern string for parameter separators, by convention {@code "[&;]"}
+ * @param charset
+ * Encoding to use when decoding the parameters.
+ */
+ public static void parse(
+ final List <NameValuePair> parameters,
+ final Scanner scanner,
+ final String parameterSepartorPattern,
+ final String charset) {
+ scanner.useDelimiter(parameterSepartorPattern);
+ while (scanner.hasNext()) {
+ String name = null;
+ String value = null;
+ final String token = scanner.next();
+ final int i = token.indexOf(NAME_VALUE_SEPARATOR);
+ if (i != -1) {
+ name = decodeFormFields(token.substring(0, i).trim(), charset);
+ value = decodeFormFields(token.substring(i + 1).trim(), charset);
+ } else {
+ name = decodeFormFields(token.trim(), charset);
+ }
+ parameters.add(new BasicNameValuePair(name, value));
+ }
+ }
+
+ /**
+ * Query parameter separators.
+ */
+ private static final char[] QP_SEPS = new char[] { QP_SEP_A, QP_SEP_S };
+
+ /**
+ * Query parameter separator pattern.
+ */
+ private static final String QP_SEP_PATTERN = "[" + new String(QP_SEPS) + "]";
+
+ /**
+ * Returns a list of {@link NameValuePair NameValuePairs} as parsed from the given string using the given character
+ * encoding. By convention, {@code '&'} and {@code ';'} are accepted as parameter separators.
+ *
+ * @param s
+ * text to parse.
+ * @param charset
+ * Encoding to use when decoding the parameters.
+ * @return a list of {@link NameValuePair} as built from the URI's query portion.
+ *
+ * @since 4.2
+ */
+ public static List<NameValuePair> parse(final String s, final Charset charset) {
+ return parse(s, charset, QP_SEPS);
+ }
+
+ /**
+ * Returns a list of {@link NameValuePair NameValuePairs} as parsed from the given string using the given character
+ * encoding.
+ *
+ * @param s
+ * text to parse.
+ * @param charset
+ * Encoding to use when decoding the parameters.
+ * @param parameterSeparator
+ * The characters used to separate parameters, by convention, {@code '&'} and {@code ';'}.
+ * @return a list of {@link NameValuePair} as built from the URI's query portion.
+ *
+ * @since 4.3
+ */
+ public static List<NameValuePair> parse(final String s, final Charset charset, final char... parameterSeparator) {
+ if (s == null) {
+ return Collections.emptyList();
+ }
+ final BasicHeaderValueParser parser = BasicHeaderValueParser.INSTANCE;
+ final CharArrayBuffer buffer = new CharArrayBuffer(s.length());
+ buffer.append(s);
+ final ParserCursor cursor = new ParserCursor(0, buffer.length());
+ final List<NameValuePair> list = new ArrayList<NameValuePair>();
+ while (!cursor.atEnd()) {
+ final NameValuePair nvp = parser.parseNameValuePair(buffer, cursor, parameterSeparator);
+ if (nvp.getName().length() > 0) {
+ list.add(new BasicNameValuePair(
+ decodeFormFields(nvp.getName(), charset),
+ decodeFormFields(nvp.getValue(), charset)));
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Returns a String that is suitable for use as an {@code application/x-www-form-urlencoded}
+ * list of parameters in an HTTP PUT or HTTP POST.
+ *
+ * @param parameters The parameters to include.
+ * @param charset The encoding to use.
+ * @return An {@code application/x-www-form-urlencoded} string
+ */
+ public static String format(
+ final List <? extends NameValuePair> parameters,
+ final String charset) {
+ return format(parameters, QP_SEP_A, charset);
+ }
+
+ /**
+ * Returns a String that is suitable for use as an {@code application/x-www-form-urlencoded}
+ * list of parameters in an HTTP PUT or HTTP POST.
+ *
+ * @param parameters The parameters to include.
+ * @param parameterSeparator The parameter separator, by convention, {@code '&'} or {@code ';'}.
+ * @param charset The encoding to use.
+ * @return An {@code application/x-www-form-urlencoded} string
+ *
+ * @since 4.3
+ */
+ public static String format(
+ final List <? extends NameValuePair> parameters,
+ final char parameterSeparator,
+ final String charset) {
+ final StringBuilder result = new StringBuilder();
+ for (final NameValuePair parameter : parameters) {
+ final String encodedName = encodeFormFields(parameter.getName(), charset);
+ final String encodedValue = encodeFormFields(parameter.getValue(), charset);
+ if (result.length() > 0) {
+ result.append(parameterSeparator);
+ }
+ result.append(encodedName);
+ if (encodedValue != null) {
+ result.append(NAME_VALUE_SEPARATOR);
+ result.append(encodedValue);
+ }
+ }
+ return result.toString();
+ }
+
+ /**
+ * Returns a String that is suitable for use as an {@code application/x-www-form-urlencoded}
+ * list of parameters in an HTTP PUT or HTTP POST.
+ *
+ * @param parameters The parameters to include.
+ * @param charset The encoding to use.
+ * @return An {@code application/x-www-form-urlencoded} string
+ *
+ * @since 4.2
+ */
+ public static String format(
+ final Iterable<? extends NameValuePair> parameters,
+ final Charset charset) {
+ return format(parameters, QP_SEP_A, charset);
+ }
+
+ /**
+ * Returns a String that is suitable for use as an {@code application/x-www-form-urlencoded}
+ * list of parameters in an HTTP PUT or HTTP POST.
+ *
+ * @param parameters The parameters to include.
+ * @param parameterSeparator The parameter separator, by convention, {@code '&'} or {@code ';'}.
+ * @param charset The encoding to use.
+ * @return An {@code application/x-www-form-urlencoded} string
+ *
+ * @since 4.3
+ */
+ public static String format(
+ final Iterable<? extends NameValuePair> parameters,
+ final char parameterSeparator,
+ final Charset charset) {
+ final StringBuilder result = new StringBuilder();
+ for (final NameValuePair parameter : parameters) {
+ final String encodedName = encodeFormFields(parameter.getName(), charset);
+ final String encodedValue = encodeFormFields(parameter.getValue(), charset);
+ if (result.length() > 0) {
+ result.append(parameterSeparator);
+ }
+ result.append(encodedName);
+ if (encodedValue != null) {
+ result.append(NAME_VALUE_SEPARATOR);
+ result.append(encodedValue);
+ }
+ }
+ return result.toString();
+ }
+
+ /**
+ * Unreserved characters, i.e. alphanumeric, plus: {@code _ - ! . ~ ' ( ) *}
+ * <p>
+ * This list is the same as the {@code unreserved} list in
+ * <a href="http://www.ietf.org/rfc/rfc2396.txt">RFC 2396</a>
+ */
+ private static final BitSet UNRESERVED = new BitSet(256);
+ /**
+ * Punctuation characters: , ; : $ & + =
+ * <p>
+ * These are the additional characters allowed by userinfo.
+ */
+ private static final BitSet PUNCT = new BitSet(256);
+ /** Characters which are safe to use in userinfo,
+ * i.e. {@link #UNRESERVED} plus {@link #PUNCT}uation */
+ private static final BitSet USERINFO = new BitSet(256);
+ /** Characters which are safe to use in a path,
+ * i.e. {@link #UNRESERVED} plus {@link #PUNCT}uation plus / @ */
+ private static final BitSet PATHSAFE = new BitSet(256);
+ /** Characters which are safe to use in a query or a fragment,
+ * i.e. {@link #RESERVED} plus {@link #UNRESERVED} */
+ private static final BitSet URIC = new BitSet(256);
+
+ /**
+ * Reserved characters, i.e. {@code ;/?:@&=+$,[]}
+ * <p>
+ * This list is the same as the {@code reserved} list in
+ * <a href="http://www.ietf.org/rfc/rfc2396.txt">RFC 2396</a>
+ * as augmented by
+ * <a href="http://www.ietf.org/rfc/rfc2732.txt">RFC 2732</a>
+ */
+ private static final BitSet RESERVED = new BitSet(256);
+
+
+ /**
+ * Safe characters for x-www-form-urlencoded data, as per java.net.URLEncoder and browser behaviour,
+ * i.e. alphanumeric plus {@code "-", "_", ".", "*"}
+ */
+ private static final BitSet URLENCODER = new BitSet(256);
+
+ static {
+ // unreserved chars
+ // alpha characters
+ for (int i = 'a'; i <= 'z'; i++) {
+ UNRESERVED.set(i);
+ }
+ for (int i = 'A'; i <= 'Z'; i++) {
+ UNRESERVED.set(i);
+ }
+ // numeric characters
+ for (int i = '0'; i <= '9'; i++) {
+ UNRESERVED.set(i);
+ }
+ UNRESERVED.set('_'); // these are the charactes of the "mark" list
+ UNRESERVED.set('-');
+ UNRESERVED.set('.');
+ UNRESERVED.set('*');
+ URLENCODER.or(UNRESERVED); // skip remaining unreserved characters
+ UNRESERVED.set('!');
+ UNRESERVED.set('~');
+ UNRESERVED.set('\'');
+ UNRESERVED.set('(');
+ UNRESERVED.set(')');
+ // punct chars
+ PUNCT.set(',');
+ PUNCT.set(';');
+ PUNCT.set(':');
+ PUNCT.set('$');
+ PUNCT.set('&');
+ PUNCT.set('+');
+ PUNCT.set('=');
+ // Safe for userinfo
+ USERINFO.or(UNRESERVED);
+ USERINFO.or(PUNCT);
+
+ // URL path safe
+ PATHSAFE.or(UNRESERVED);
+ PATHSAFE.set('/'); // segment separator
+ PATHSAFE.set(';'); // param separator
+ PATHSAFE.set(':'); // rest as per list in 2396, i.e. : @ & = + $ ,
+ PATHSAFE.set('@');
+ PATHSAFE.set('&');
+ PATHSAFE.set('=');
+ PATHSAFE.set('+');
+ PATHSAFE.set('$');
+ PATHSAFE.set(',');
+
+ RESERVED.set(';');
+ RESERVED.set('/');
+ RESERVED.set('?');
+ RESERVED.set(':');
+ RESERVED.set('@');
+ RESERVED.set('&');
+ RESERVED.set('=');
+ RESERVED.set('+');
+ RESERVED.set('$');
+ RESERVED.set(',');
+ RESERVED.set('['); // added by RFC 2732
+ RESERVED.set(']'); // added by RFC 2732
+
+ URIC.or(RESERVED);
+ URIC.or(UNRESERVED);
+ }
+
+ private static final int RADIX = 16;
+
+ private static String urlEncode(
+ final String content,
+ final Charset charset,
+ final BitSet safechars,
+ final boolean blankAsPlus) {
+ if (content == null) {
+ return null;
+ }
+ final StringBuilder buf = new StringBuilder();
+ final ByteBuffer bb = charset.encode(content);
+ while (bb.hasRemaining()) {
+ final int b = bb.get() & 0xff;
+ if (safechars.get(b)) {
+ buf.append((char) b);
+ } else if (blankAsPlus && b == ' ') {
+ buf.append('+');
+ } else {
+ buf.append("%");
+ final char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, RADIX));
+ final char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, RADIX));
+ buf.append(hex1);
+ buf.append(hex2);
+ }
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Decode/unescape a portion of a URL, to use with the query part ensure {@code plusAsBlank} is true.
+ *
+ * @param content the portion to decode
+ * @param charset the charset to use
+ * @param plusAsBlank if {@code true}, then convert '+' to space (e.g. for www-url-form-encoded content), otherwise leave as is.
+ * @return encoded string
+ */
+ private static String urlDecode(
+ final String content,
+ final Charset charset,
+ final boolean plusAsBlank) {
+ if (content == null) {
+ return null;
+ }
+ final ByteBuffer bb = ByteBuffer.allocate(content.length());
+ final CharBuffer cb = CharBuffer.wrap(content);
+ while (cb.hasRemaining()) {
+ final char c = cb.get();
+ if (c == '%' && cb.remaining() >= 2) {
+ final char uc = cb.get();
+ final char lc = cb.get();
+ final int u = Character.digit(uc, 16);
+ final int l = Character.digit(lc, 16);
+ if (u != -1 && l != -1) {
+ bb.put((byte) ((u << 4) + l));
+ } else {
+ bb.put((byte) '%');
+ bb.put((byte) uc);
+ bb.put((byte) lc);
+ }
+ } else if (plusAsBlank && c == '+') {
+ bb.put((byte) ' ');
+ } else {
+ bb.put((byte) c);
+ }
+ }
+ bb.flip();
+ return charset.decode(bb).toString();
+ }
+
+ /**
+ * Decode/unescape www-url-form-encoded content.
+ *
+ * @param content the content to decode, will decode '+' as space
+ * @param charset the charset to use
+ * @return encoded string
+ */
+ private static String decodeFormFields (final String content, final String charset) {
+ if (content == null) {
+ return null;
+ }
+ return urlDecode(content, charset != null ? Charset.forName(charset) : Consts.UTF_8, true);
+ }
+
+ /**
+ * Decode/unescape www-url-form-encoded content.
+ *
+ * @param content the content to decode, will decode '+' as space
+ * @param charset the charset to use
+ * @return encoded string
+ */
+ private static String decodeFormFields (final String content, final Charset charset) {
+ if (content == null) {
+ return null;
+ }
+ return urlDecode(content, charset != null ? charset : Consts.UTF_8, true);
+ }
+
+ /**
+ * Encode/escape www-url-form-encoded content.
+ * <p>
+ * Uses the {@link #URLENCODER} set of characters, rather than
+ * the {@link #UNRSERVED} set; this is for compatibilty with previous
+ * releases, URLEncoder.encode() and most browsers.
+ *
+ * @param content the content to encode, will convert space to '+'
+ * @param charset the charset to use
+ * @return encoded string
+ */
+ private static String encodeFormFields(final String content, final String charset) {
+ if (content == null) {
+ return null;
+ }
+ return urlEncode(content, charset != null ? Charset.forName(charset) : Consts.UTF_8, URLENCODER, true);
+ }
+
+ /**
+ * Encode/escape www-url-form-encoded content.
+ * <p>
+ * Uses the {@link #URLENCODER} set of characters, rather than
+ * the {@link #UNRSERVED} set; this is for compatibilty with previous
+ * releases, URLEncoder.encode() and most browsers.
+ *
+ * @param content the content to encode, will convert space to '+'
+ * @param charset the charset to use
+ * @return encoded string
+ */
+ private static String encodeFormFields (final String content, final Charset charset) {
+ if (content == null) {
+ return null;
+ }
+ return urlEncode(content, charset != null ? charset : Consts.UTF_8, URLENCODER, true);
+ }
+
+ /**
+ * Encode a String using the {@link #USERINFO} set of characters.
+ * <p>
+ * Used by URIBuilder to encode the userinfo segment.
+ *
+ * @param content the string to encode, does not convert space to '+'
+ * @param charset the charset to use
+ * @return the encoded string
+ */
+ static String encUserInfo(final String content, final Charset charset) {
+ return urlEncode(content, charset, USERINFO, false);
+ }
+
+ /**
+ * Encode a String using the {@link #URIC} set of characters.
+ * <p>
+ * Used by URIBuilder to encode the query and fragment segments.
+ *
+ * @param content the string to encode, does not convert space to '+'
+ * @param charset the charset to use
+ * @return the encoded string
+ */
+ static String encUric(final String content, final Charset charset) {
+ return urlEncode(content, charset, URIC, false);
+ }
+
+ /**
+ * Encode a String using the {@link #PATHSAFE} set of characters.
+ * <p>
+ * Used by URIBuilder to encode path segments.
+ *
+ * @param content the string to encode, does not convert space to '+'
+ * @param charset the charset to use
+ * @return the encoded string
+ */
+ static String encPath(final String content, final Charset charset) {
+ return urlEncode(content, charset, PATHSAFE, false);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/package-info.java
new file mode 100644
index 0000000000..7f56716541
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/client/utils/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Client utility classes.
+ */
+package ch.boye.httpclientandroidlib.client.utils;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/concurrent/BasicFuture.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/concurrent/BasicFuture.java
new file mode 100644
index 0000000000..a2215cecaa
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/concurrent/BasicFuture.java
@@ -0,0 +1,154 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.concurrent;
+
+import ch.boye.httpclientandroidlib.util.Args;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Basic implementation of the {@link Future} interface. <tt>BasicFuture<tt>
+ * can be put into a completed state by invoking any of the following methods:
+ * {@link #cancel()}, {@link #failed(Exception)}, or {@link #completed(Object)}.
+ *
+ * @param <T> the future result type of an asynchronous operation.
+ * @since 4.2
+ */
+public class BasicFuture<T> implements Future<T>, Cancellable {
+
+ private final FutureCallback<T> callback;
+
+ private volatile boolean completed;
+ private volatile boolean cancelled;
+ private volatile T result;
+ private volatile Exception ex;
+
+ public BasicFuture(final FutureCallback<T> callback) {
+ super();
+ this.callback = callback;
+ }
+
+ public boolean isCancelled() {
+ return this.cancelled;
+ }
+
+ public boolean isDone() {
+ return this.completed;
+ }
+
+ private T getResult() throws ExecutionException {
+ if (this.ex != null) {
+ throw new ExecutionException(this.ex);
+ }
+ return this.result;
+ }
+
+ public synchronized T get() throws InterruptedException, ExecutionException {
+ while (!this.completed) {
+ wait();
+ }
+ return getResult();
+ }
+
+ public synchronized T get(final long timeout, final TimeUnit unit)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ Args.notNull(unit, "Time unit");
+ final long msecs = unit.toMillis(timeout);
+ final long startTime = (msecs <= 0) ? 0 : System.currentTimeMillis();
+ long waitTime = msecs;
+ if (this.completed) {
+ return getResult();
+ } else if (waitTime <= 0) {
+ throw new TimeoutException();
+ } else {
+ for (;;) {
+ wait(waitTime);
+ if (this.completed) {
+ return getResult();
+ } else {
+ waitTime = msecs - (System.currentTimeMillis() - startTime);
+ if (waitTime <= 0) {
+ throw new TimeoutException();
+ }
+ }
+ }
+ }
+ }
+
+ public boolean completed(final T result) {
+ synchronized(this) {
+ if (this.completed) {
+ return false;
+ }
+ this.completed = true;
+ this.result = result;
+ notifyAll();
+ }
+ if (this.callback != null) {
+ this.callback.completed(result);
+ }
+ return true;
+ }
+
+ public boolean failed(final Exception exception) {
+ synchronized(this) {
+ if (this.completed) {
+ return false;
+ }
+ this.completed = true;
+ this.ex = exception;
+ notifyAll();
+ }
+ if (this.callback != null) {
+ this.callback.failed(exception);
+ }
+ return true;
+ }
+
+ public boolean cancel(final boolean mayInterruptIfRunning) {
+ synchronized(this) {
+ if (this.completed) {
+ return false;
+ }
+ this.completed = true;
+ this.cancelled = true;
+ notifyAll();
+ }
+ if (this.callback != null) {
+ this.callback.cancelled();
+ }
+ return true;
+ }
+
+ public boolean cancel() {
+ return cancel(true);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/concurrent/Cancellable.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/concurrent/Cancellable.java
new file mode 100644
index 0000000000..c51daa27f9
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/concurrent/Cancellable.java
@@ -0,0 +1,39 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.concurrent;
+
+/**
+ * A <tt>Cancellable</tt> represents a process or an operation that can be
+ * canceled.
+ *
+ * @since 4.2
+ */
+public interface Cancellable {
+
+ boolean cancel();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/concurrent/FutureCallback.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/concurrent/FutureCallback.java
new file mode 100644
index 0000000000..91ed939400
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/concurrent/FutureCallback.java
@@ -0,0 +1,44 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.concurrent;
+
+/**
+ * A callback interface that gets invoked upon completion of
+ * a {@link java.util.concurrent.Future}.
+ *
+ * @param <T> the future result type returned by this callback.
+ * @since 4.2
+ */
+public interface FutureCallback<T> {
+
+ void completed(T result);
+
+ void failed(Exception ex);
+
+ void cancelled();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/concurrent/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/concurrent/package-info.java
new file mode 100644
index 0000000000..38d2699ed7
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/concurrent/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Core concurrency APIs.
+ */
+package ch.boye.httpclientandroidlib.concurrent;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/ConnectionConfig.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/ConnectionConfig.java
new file mode 100644
index 0000000000..50fb21460d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/ConnectionConfig.java
@@ -0,0 +1,192 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.config;
+
+import java.nio.charset.Charset;
+import java.nio.charset.CodingErrorAction;
+
+import ch.boye.httpclientandroidlib.Consts;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * HTTP connection configuration.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class ConnectionConfig implements Cloneable {
+
+ public static final ConnectionConfig DEFAULT = new Builder().build();
+
+ private final int bufferSize;
+ private final int fragmentSizeHint;
+ private final Charset charset;
+ private final CodingErrorAction malformedInputAction;
+ private final CodingErrorAction unmappableInputAction;
+ private final MessageConstraints messageConstraints;
+
+ ConnectionConfig(
+ final int bufferSize,
+ final int fragmentSizeHint,
+ final Charset charset,
+ final CodingErrorAction malformedInputAction,
+ final CodingErrorAction unmappableInputAction,
+ final MessageConstraints messageConstraints) {
+ super();
+ this.bufferSize = bufferSize;
+ this.fragmentSizeHint = fragmentSizeHint;
+ this.charset = charset;
+ this.malformedInputAction = malformedInputAction;
+ this.unmappableInputAction = unmappableInputAction;
+ this.messageConstraints = messageConstraints;
+ }
+
+ public int getBufferSize() {
+ return bufferSize;
+ }
+
+ public int getFragmentSizeHint() {
+ return fragmentSizeHint;
+ }
+
+ public Charset getCharset() {
+ return charset;
+ }
+
+ public CodingErrorAction getMalformedInputAction() {
+ return malformedInputAction;
+ }
+
+ public CodingErrorAction getUnmappableInputAction() {
+ return unmappableInputAction;
+ }
+
+ public MessageConstraints getMessageConstraints() {
+ return messageConstraints;
+ }
+
+ @Override
+ protected ConnectionConfig clone() throws CloneNotSupportedException {
+ return (ConnectionConfig) super.clone();
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("[bufferSize=").append(this.bufferSize)
+ .append(", fragmentSizeHint=").append(this.fragmentSizeHint)
+ .append(", charset=").append(this.charset)
+ .append(", malformedInputAction=").append(this.malformedInputAction)
+ .append(", unmappableInputAction=").append(this.unmappableInputAction)
+ .append(", messageConstraints=").append(this.messageConstraints)
+ .append("]");
+ return builder.toString();
+ }
+
+ public static ConnectionConfig.Builder custom() {
+ return new Builder();
+ }
+
+ public static ConnectionConfig.Builder copy(final ConnectionConfig config) {
+ Args.notNull(config, "Connection config");
+ return new Builder()
+ .setCharset(config.getCharset())
+ .setMalformedInputAction(config.getMalformedInputAction())
+ .setUnmappableInputAction(config.getUnmappableInputAction())
+ .setMessageConstraints(config.getMessageConstraints());
+ }
+
+ public static class Builder {
+
+ private int bufferSize;
+ private int fragmentSizeHint;
+ private Charset charset;
+ private CodingErrorAction malformedInputAction;
+ private CodingErrorAction unmappableInputAction;
+ private MessageConstraints messageConstraints;
+
+ Builder() {
+ this.fragmentSizeHint = -1;
+ }
+
+ public Builder setBufferSize(final int bufferSize) {
+ this.bufferSize = bufferSize;
+ return this;
+ }
+
+ public Builder setFragmentSizeHint(final int fragmentSizeHint) {
+ this.fragmentSizeHint = fragmentSizeHint;
+ return this;
+ }
+
+ public Builder setCharset(final Charset charset) {
+ this.charset = charset;
+ return this;
+ }
+
+ public Builder setMalformedInputAction(final CodingErrorAction malformedInputAction) {
+ this.malformedInputAction = malformedInputAction;
+ if (malformedInputAction != null && this.charset == null) {
+ this.charset = Consts.ASCII;
+ }
+ return this;
+ }
+
+ public Builder setUnmappableInputAction(final CodingErrorAction unmappableInputAction) {
+ this.unmappableInputAction = unmappableInputAction;
+ if (unmappableInputAction != null && this.charset == null) {
+ this.charset = Consts.ASCII;
+ }
+ return this;
+ }
+
+ public Builder setMessageConstraints(final MessageConstraints messageConstraints) {
+ this.messageConstraints = messageConstraints;
+ return this;
+ }
+
+ public ConnectionConfig build() {
+ Charset cs = charset;
+ if (cs == null && (malformedInputAction != null || unmappableInputAction != null)) {
+ cs = Consts.ASCII;
+ }
+ final int bufSize = this.bufferSize > 0 ? this.bufferSize : 8 * 1024;
+ final int fragmentHintSize = this.fragmentSizeHint >= 0 ? this.fragmentSizeHint : bufSize;
+ return new ConnectionConfig(
+ bufSize,
+ fragmentHintSize,
+ cs,
+ malformedInputAction,
+ unmappableInputAction,
+ messageConstraints);
+ }
+
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/Lookup.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/Lookup.java
new file mode 100644
index 0000000000..343dda2d3d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/Lookup.java
@@ -0,0 +1,40 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.config;
+
+
+/**
+ * Generic lookup by low-case string ID.
+ *
+ * @since 4.3
+ */
+public interface Lookup<I> {
+
+ I lookup(String name);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/MessageConstraints.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/MessageConstraints.java
new file mode 100644
index 0000000000..5ac8c065a3
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/MessageConstraints.java
@@ -0,0 +1,113 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.config;
+
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * HTTP Message constraints: line length and header count.
+ *
+ * @since 4.3
+ */
+public class MessageConstraints implements Cloneable {
+
+ public static final MessageConstraints DEFAULT = new Builder().build();
+
+ private final int maxLineLength;
+ private final int maxHeaderCount;
+
+ MessageConstraints(final int maxLineLength, final int maxHeaderCount) {
+ super();
+ this.maxLineLength = maxLineLength;
+ this.maxHeaderCount = maxHeaderCount;
+ }
+
+ public int getMaxLineLength() {
+ return maxLineLength;
+ }
+
+ public int getMaxHeaderCount() {
+ return maxHeaderCount;
+ }
+
+ @Override
+ protected MessageConstraints clone() throws CloneNotSupportedException {
+ return (MessageConstraints) super.clone();
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("[maxLineLength=").append(maxLineLength)
+ .append(", maxHeaderCount=").append(maxHeaderCount)
+ .append("]");
+ return builder.toString();
+ }
+
+ public static MessageConstraints lineLen(final int max) {
+ return new MessageConstraints(Args.notNegative(max, "Max line length"), -1);
+ }
+
+ public static MessageConstraints.Builder custom() {
+ return new Builder();
+ }
+
+ public static MessageConstraints.Builder copy(final MessageConstraints config) {
+ Args.notNull(config, "Message constraints");
+ return new Builder()
+ .setMaxHeaderCount(config.getMaxHeaderCount())
+ .setMaxLineLength(config.getMaxLineLength());
+ }
+
+ public static class Builder {
+
+ private int maxLineLength;
+ private int maxHeaderCount;
+
+ Builder() {
+ this.maxLineLength = -1;
+ this.maxHeaderCount = -1;
+ }
+
+ public Builder setMaxLineLength(final int maxLineLength) {
+ this.maxLineLength = maxLineLength;
+ return this;
+ }
+
+ public Builder setMaxHeaderCount(final int maxHeaderCount) {
+ this.maxHeaderCount = maxHeaderCount;
+ return this;
+ }
+
+ public MessageConstraints build() {
+ return new MessageConstraints(maxLineLength, maxHeaderCount);
+ }
+
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/Registry.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/Registry.java
new file mode 100644
index 0000000000..71b718beec
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/Registry.java
@@ -0,0 +1,63 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.config;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+
+/**
+ * Generic registry of items keyed by low-case string ID.
+ *
+ * @since 4.3
+ */
+@ThreadSafe
+public final class Registry<I> implements Lookup<I> {
+
+ private final Map<String, I> map;
+
+ Registry(final Map<String, I> map) {
+ super();
+ this.map = new ConcurrentHashMap<String, I>(map);
+ }
+
+ public I lookup(final String key) {
+ if (key == null) {
+ return null;
+ }
+ return map.get(key.toLowerCase(Locale.US));
+ }
+
+ @Override
+ public String toString() {
+ return map.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/RegistryBuilder.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/RegistryBuilder.java
new file mode 100644
index 0000000000..56dbd79765
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/RegistryBuilder.java
@@ -0,0 +1,72 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.config;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Builder for {@link Registry} instances.
+ *
+ * @since 4.3
+ */
+@NotThreadSafe
+public final class RegistryBuilder<I> {
+
+ private final Map<String, I> items;
+
+ public static <I> RegistryBuilder<I> create() {
+ return new RegistryBuilder<I>();
+ }
+
+ RegistryBuilder() {
+ super();
+ this.items = new HashMap<String, I>();
+ }
+
+ public RegistryBuilder<I> register(final String id, final I item) {
+ Args.notEmpty(id, "ID");
+ Args.notNull(item, "Item");
+ items.put(id.toLowerCase(Locale.US), item);
+ return this;
+ }
+
+ public Registry<I> build() {
+ return new Registry<I>(items);
+ }
+
+ @Override
+ public String toString() {
+ return items.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/SocketConfig.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/SocketConfig.java
new file mode 100644
index 0000000000..515dac1b15
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/SocketConfig.java
@@ -0,0 +1,197 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.config;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Socket configuration.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class SocketConfig implements Cloneable {
+
+ public static final SocketConfig DEFAULT = new Builder().build();
+
+ private final int soTimeout;
+ private final boolean soReuseAddress;
+ private final int soLinger;
+ private final boolean soKeepAlive;
+ private final boolean tcpNoDelay;
+
+ SocketConfig(
+ final int soTimeout,
+ final boolean soReuseAddress,
+ final int soLinger,
+ final boolean soKeepAlive,
+ final boolean tcpNoDelay) {
+ super();
+ this.soTimeout = soTimeout;
+ this.soReuseAddress = soReuseAddress;
+ this.soLinger = soLinger;
+ this.soKeepAlive = soKeepAlive;
+ this.tcpNoDelay = tcpNoDelay;
+ }
+
+ /**
+ * Determines the default socket timeout value for non-blocking I/O operations.
+ * <p/>
+ * Default: <code>0</code> (no timeout)
+ *
+ * @see java.net.SocketOptions#SO_TIMEOUT
+ */
+ public int getSoTimeout() {
+ return soTimeout;
+ }
+
+ /**
+ * Determines the default value of the {@link java.net.SocketOptions#SO_REUSEADDR} parameter
+ * for newly created sockets.
+ * <p/>
+ * Default: <code>false</code>
+ *
+ * @see java.net.SocketOptions#SO_REUSEADDR
+ */
+ public boolean isSoReuseAddress() {
+ return soReuseAddress;
+ }
+
+ /**
+ * Determines the default value of the {@link java.net.SocketOptions#SO_LINGER} parameter
+ * for newly created sockets.
+ * <p/>
+ * Default: <code>-1</code>
+ *
+ * @see java.net.SocketOptions#SO_LINGER
+ */
+ public int getSoLinger() {
+ return soLinger;
+ }
+
+ /**
+ * Determines the default value of the {@link java.net.SocketOptions#SO_KEEPALIVE} parameter
+ * for newly created sockets.
+ * <p/>
+ * Default: <code>-1</code>
+ *
+ * @see java.net.SocketOptions#SO_KEEPALIVE
+ */
+ public boolean isSoKeepAlive() {
+ return this.soKeepAlive;
+ }
+
+ /**
+ * Determines the default value of the {@link java.net.SocketOptions#TCP_NODELAY} parameter
+ * for newly created sockets.
+ * <p/>
+ * Default: <code>false</code>
+ *
+ * @see java.net.SocketOptions#TCP_NODELAY
+ */
+ public boolean isTcpNoDelay() {
+ return tcpNoDelay;
+ }
+
+ @Override
+ protected SocketConfig clone() throws CloneNotSupportedException {
+ return (SocketConfig) super.clone();
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("[soTimeout=").append(this.soTimeout)
+ .append(", soReuseAddress=").append(this.soReuseAddress)
+ .append(", soLinger=").append(this.soLinger)
+ .append(", soKeepAlive=").append(this.soKeepAlive)
+ .append(", tcpNoDelay=").append(this.tcpNoDelay)
+ .append("]");
+ return builder.toString();
+ }
+
+ public static SocketConfig.Builder custom() {
+ return new Builder();
+ }
+
+ public static SocketConfig.Builder copy(final SocketConfig config) {
+ Args.notNull(config, "Socket config");
+ return new Builder()
+ .setSoTimeout(config.getSoTimeout())
+ .setSoReuseAddress(config.isSoReuseAddress())
+ .setSoLinger(config.getSoLinger())
+ .setSoKeepAlive(config.isSoKeepAlive())
+ .setTcpNoDelay(config.isTcpNoDelay());
+ }
+
+ public static class Builder {
+
+ private int soTimeout;
+ private boolean soReuseAddress;
+ private int soLinger;
+ private boolean soKeepAlive;
+ private boolean tcpNoDelay;
+
+ Builder() {
+ this.soLinger = -1;
+ this.tcpNoDelay = true;
+ }
+
+ public Builder setSoTimeout(final int soTimeout) {
+ this.soTimeout = soTimeout;
+ return this;
+ }
+
+ public Builder setSoReuseAddress(final boolean soReuseAddress) {
+ this.soReuseAddress = soReuseAddress;
+ return this;
+ }
+
+ public Builder setSoLinger(final int soLinger) {
+ this.soLinger = soLinger;
+ return this;
+ }
+
+ public Builder setSoKeepAlive(final boolean soKeepAlive) {
+ this.soKeepAlive = soKeepAlive;
+ return this;
+ }
+
+ public Builder setTcpNoDelay(final boolean tcpNoDelay) {
+ this.tcpNoDelay = tcpNoDelay;
+ return this;
+ }
+
+ public SocketConfig build() {
+ return new SocketConfig(soTimeout, soReuseAddress, soLinger, soKeepAlive, tcpNoDelay);
+ }
+
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/package-info.java
new file mode 100644
index 0000000000..9521d6828f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/config/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Core configuration APIs.
+ */
+package ch.boye.httpclientandroidlib.config;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/BasicEofSensorWatcher.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/BasicEofSensorWatcher.java
new file mode 100644
index 0000000000..0273224ad2
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/BasicEofSensorWatcher.java
@@ -0,0 +1,105 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Basic implementation of {@link EofSensorWatcher}. The underlying connection
+ * is released on close or EOF.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) do not use.
+ */
+@Deprecated
+@NotThreadSafe
+public class BasicEofSensorWatcher implements EofSensorWatcher {
+
+ /** The connection to auto-release. */
+ protected final ManagedClientConnection managedConn;
+
+ /** Whether to keep the connection alive. */
+ protected final boolean attemptReuse;
+
+ /**
+ * Creates a new watcher for auto-releasing a connection.
+ *
+ * @param conn the connection to auto-release
+ * @param reuse whether the connection should be re-used
+ */
+ public BasicEofSensorWatcher(final ManagedClientConnection conn,
+ final boolean reuse) {
+ Args.notNull(conn, "Connection");
+ managedConn = conn;
+ attemptReuse = reuse;
+ }
+
+ public boolean eofDetected(final InputStream wrapped)
+ throws IOException {
+
+ try {
+ if (attemptReuse) {
+ // there may be some cleanup required, such as
+ // reading trailers after the response body:
+ wrapped.close();
+ managedConn.markReusable();
+ }
+ } finally {
+ managedConn.releaseConnection();
+ }
+ return false;
+ }
+
+ public boolean streamClosed(final InputStream wrapped)
+ throws IOException {
+
+ try {
+ if (attemptReuse) {
+ // this assumes that closing the stream will
+ // consume the remainder of the response body:
+ wrapped.close();
+ managedConn.markReusable();
+ }
+ } finally {
+ managedConn.releaseConnection();
+ }
+ return false;
+ }
+
+ public boolean streamAbort(final InputStream wrapped)
+ throws IOException {
+
+ managedConn.abortConnection();
+ return false;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/BasicManagedEntity.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/BasicManagedEntity.java
new file mode 100644
index 0000000000..59556b5e92
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/BasicManagedEntity.java
@@ -0,0 +1,208 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.SocketException;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.entity.HttpEntityWrapper;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.EntityUtils;
+
+/**
+ * An entity that releases a {@link ManagedClientConnection connection}.
+ * A {@link ManagedClientConnection} will
+ * typically <i>not</i> return a managed entity, but you can replace
+ * the unmanaged entity in the response with a managed one.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) do not use.
+ */
+@Deprecated
+@NotThreadSafe
+public class BasicManagedEntity extends HttpEntityWrapper
+ implements ConnectionReleaseTrigger, EofSensorWatcher {
+
+ /** The connection to release. */
+ protected ManagedClientConnection managedConn;
+
+ /** Whether to keep the connection alive. */
+ protected final boolean attemptReuse;
+
+ /**
+ * Creates a new managed entity that can release a connection.
+ *
+ * @param entity the entity of which to wrap the content.
+ * Note that the argument entity can no longer be used
+ * afterwards, since the content will be taken by this
+ * managed entity.
+ * @param conn the connection to release
+ * @param reuse whether the connection should be re-used
+ */
+ public BasicManagedEntity(final HttpEntity entity,
+ final ManagedClientConnection conn,
+ final boolean reuse) {
+ super(entity);
+ Args.notNull(conn, "Connection");
+ this.managedConn = conn;
+ this.attemptReuse = reuse;
+ }
+
+ @Override
+ public boolean isRepeatable() {
+ return false;
+ }
+
+ @Override
+ public InputStream getContent() throws IOException {
+ return new EofSensorInputStream(wrappedEntity.getContent(), this);
+ }
+
+ private void ensureConsumed() throws IOException {
+ if (managedConn == null) {
+ return;
+ }
+
+ try {
+ if (attemptReuse) {
+ // this will not trigger a callback from EofSensorInputStream
+ EntityUtils.consume(wrappedEntity);
+ managedConn.markReusable();
+ } else {
+ managedConn.unmarkReusable();
+ }
+ } finally {
+ releaseManagedConnection();
+ }
+ }
+
+ /**
+ * @deprecated (4.1) Use {@link EntityUtils#consume(HttpEntity)}
+ */
+ @Deprecated
+ @Override
+ public void consumeContent() throws IOException {
+ ensureConsumed();
+ }
+
+ @Override
+ public void writeTo(final OutputStream outstream) throws IOException {
+ super.writeTo(outstream);
+ ensureConsumed();
+ }
+
+ public void releaseConnection() throws IOException {
+ ensureConsumed();
+ }
+
+ public void abortConnection() throws IOException {
+
+ if (managedConn != null) {
+ try {
+ managedConn.abortConnection();
+ } finally {
+ managedConn = null;
+ }
+ }
+ }
+
+ public boolean eofDetected(final InputStream wrapped) throws IOException {
+ try {
+ if (managedConn != null) {
+ if (attemptReuse) {
+ // there may be some cleanup required, such as
+ // reading trailers after the response body:
+ wrapped.close();
+ managedConn.markReusable();
+ } else {
+ managedConn.unmarkReusable();
+ }
+ }
+ } finally {
+ releaseManagedConnection();
+ }
+ return false;
+ }
+
+ public boolean streamClosed(final InputStream wrapped) throws IOException {
+ try {
+ if (managedConn != null) {
+ if (attemptReuse) {
+ final boolean valid = managedConn.isOpen();
+ // this assumes that closing the stream will
+ // consume the remainder of the response body:
+ try {
+ wrapped.close();
+ managedConn.markReusable();
+ } catch (final SocketException ex) {
+ if (valid) {
+ throw ex;
+ }
+ }
+ } else {
+ managedConn.unmarkReusable();
+ }
+ }
+ } finally {
+ releaseManagedConnection();
+ }
+ return false;
+ }
+
+ public boolean streamAbort(final InputStream wrapped) throws IOException {
+ if (managedConn != null) {
+ managedConn.abortConnection();
+ }
+ return false;
+ }
+
+ /**
+ * Releases the connection gracefully.
+ * The connection attribute will be nullified.
+ * Subsequent invocations are no-ops.
+ *
+ * @throws IOException in case of an IO problem.
+ * The connection attribute will be nullified anyway.
+ */
+ protected void releaseManagedConnection()
+ throws IOException {
+
+ if (managedConn != null) {
+ try {
+ managedConn.releaseConnection();
+ } finally {
+ managedConn = null;
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ClientConnectionManager.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ClientConnectionManager.java
new file mode 100644
index 0000000000..dcdbb4ce57
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ClientConnectionManager.java
@@ -0,0 +1,117 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn;
+
+import java.util.concurrent.TimeUnit;
+
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.conn.scheme.SchemeRegistry;
+
+/**
+ * Management interface for {@link ManagedClientConnection client connections}.
+ * The purpose of an HTTP connection manager is to serve as a factory for new
+ * HTTP connections, manage persistent connections and synchronize access to
+ * persistent connections making sure that only one thread of execution can
+ * have access to a connection at a time.
+ * <p>
+ * Implementations of this interface must be thread-safe. Access to shared
+ * data must be synchronized as methods of this interface may be executed
+ * from multiple threads.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) replaced by {@link HttpClientConnectionManager}.
+ */
+@Deprecated
+public interface ClientConnectionManager {
+
+ /**
+ * Obtains the scheme registry used by this manager.
+ *
+ * @return the scheme registry, never <code>null</code>
+ */
+ SchemeRegistry getSchemeRegistry();
+
+ /**
+ * Returns a new {@link ClientConnectionRequest}, from which a
+ * {@link ManagedClientConnection} can be obtained or the request can be
+ * aborted.
+ */
+ ClientConnectionRequest requestConnection(HttpRoute route, Object state);
+
+ /**
+ * Releases a connection for use by others.
+ * You may optionally specify how long the connection is valid
+ * to be reused. Values <= 0 are considered to be valid forever.
+ * If the connection is not marked as reusable, the connection will
+ * not be reused regardless of the valid duration.
+ *
+ * If the connection has been released before,
+ * the call will be ignored.
+ *
+ * @param conn the connection to release
+ * @param validDuration the duration of time this connection is valid for reuse
+ * @param timeUnit the unit of time validDuration is measured in
+ *
+ * @see #closeExpiredConnections()
+ */
+ void releaseConnection(ManagedClientConnection conn, long validDuration, TimeUnit timeUnit);
+
+ /**
+ * Closes idle connections in the pool.
+ * Open connections in the pool that have not been used for the
+ * timespan given by the argument will be closed.
+ * Currently allocated connections are not subject to this method.
+ * Times will be checked with milliseconds precision
+ *
+ * All expired connections will also be closed.
+ *
+ * @param idletime the idle time of connections to be closed
+ * @param tunit the unit for the <code>idletime</code>
+ *
+ * @see #closeExpiredConnections()
+ */
+ void closeIdleConnections(long idletime, TimeUnit tunit);
+
+ /**
+ * Closes all expired connections in the pool.
+ * Open connections in the pool that have not been used for
+ * the timespan defined when the connection was released will be closed.
+ * Currently allocated connections are not subject to this method.
+ * Times will be checked with milliseconds precision.
+ */
+ void closeExpiredConnections();
+
+ /**
+ * Shuts down this connection manager and releases allocated resources.
+ * This includes closing all connections, whether they are currently
+ * used or not.
+ */
+ void shutdown();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ClientConnectionManagerFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ClientConnectionManagerFactory.java
new file mode 100644
index 0000000000..895690e95a
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ClientConnectionManagerFactory.java
@@ -0,0 +1,47 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn;
+
+import ch.boye.httpclientandroidlib.conn.scheme.SchemeRegistry;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+/**
+ * A factory for creating new {@link ClientConnectionManager} instances.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) replaced by {@link HttpClientConnectionManager}.
+ */
+@Deprecated
+public interface ClientConnectionManagerFactory {
+
+ ClientConnectionManager newInstance(
+ HttpParams params,
+ SchemeRegistry schemeRegistry);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ClientConnectionOperator.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ClientConnectionOperator.java
new file mode 100644
index 0000000000..333975b2c6
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ClientConnectionOperator.java
@@ -0,0 +1,107 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn;
+
+import java.io.IOException;
+import java.net.InetAddress;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * ClientConnectionOperator represents a strategy for creating
+ * {@link OperatedClientConnection} instances and updating the underlying
+ * {@link java.net.Socket} of those objects. Implementations will most
+ * likely make use of {@link ch.boye.httpclientandroidlib.conn.scheme.SchemeSocketFactory}s
+ * to create {@link java.net.Socket} instances.
+ * <p>
+ * The methods in this interface allow the creation of plain and layered
+ * sockets. Creating a tunnelled connection through a proxy, however,
+ * is not within the scope of the operator.
+ * <p>
+ * Implementations of this interface must be thread-safe. Access to shared
+ * data must be synchronized as methods of this interface may be executed
+ * from multiple threads.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) replaced by {@link HttpClientConnectionManager}.
+ */
+@Deprecated
+public interface ClientConnectionOperator {
+
+ /**
+ * Creates a new connection that can be operated.
+ *
+ * @return a new, unopened connection for use with this operator
+ */
+ OperatedClientConnection createConnection();
+
+ /**
+ * Opens a connection to the given target host.
+ *
+ * @param conn the connection to open
+ * @param target the target host to connect to
+ * @param local the local address to route from, or
+ * <code>null</code> for the default
+ * @param context the context for the connection
+ * @param params the parameters for the connection
+ *
+ * @throws IOException in case of a problem
+ */
+ void openConnection(OperatedClientConnection conn,
+ HttpHost target,
+ InetAddress local,
+ HttpContext context,
+ HttpParams params)
+ throws IOException;
+
+ /**
+ * Updates a connection with a layered secure connection.
+ * The typical use of this method is to update a tunnelled plain
+ * connection (HTTP) to a secure TLS/SSL connection (HTTPS).
+ *
+ * @param conn the open connection to update
+ * @param target the target host for the updated connection.
+ * The connection must already be open or tunnelled
+ * to the host and port, but the scheme of the target
+ * will be used to create a layered connection.
+ * @param context the context for the connection
+ * @param params the parameters for the updated connection
+ *
+ * @throws IOException in case of a problem
+ */
+ void updateSecureConnection(OperatedClientConnection conn,
+ HttpHost target,
+ HttpContext context,
+ HttpParams params)
+ throws IOException;
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ClientConnectionRequest.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ClientConnectionRequest.java
new file mode 100644
index 0000000000..6562840813
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ClientConnectionRequest.java
@@ -0,0 +1,74 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Encapsulates a request for a {@link ManagedClientConnection}.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) replaced by {@link ConnectionRequest}.
+ */
+@Deprecated
+public interface ClientConnectionRequest {
+
+ /**
+ * Obtains a connection within a given time.
+ * This method will block until a connection becomes available,
+ * the timeout expires, or the connection manager is
+ * {@link ClientConnectionManager#shutdown() shut down}.
+ * Timeouts are handled with millisecond precision.
+ *
+ * If {@link #abortRequest()} is called while this is blocking or
+ * before this began, an {@link InterruptedException} will
+ * be thrown.
+ *
+ * @param timeout the timeout, 0 or negative for no timeout
+ * @param tunit the unit for the <code>timeout</code>,
+ * may be <code>null</code> only if there is no timeout
+ *
+ * @return a connection that can be used to communicate
+ * along the given route
+ *
+ * @throws ConnectionPoolTimeoutException
+ * in case of a timeout
+ * @throws InterruptedException
+ * if the calling thread is interrupted while waiting
+ */
+ ManagedClientConnection getConnection(long timeout, TimeUnit tunit)
+ throws InterruptedException, ConnectionPoolTimeoutException;
+
+ /**
+ * Aborts the call to {@link #getConnection(long, TimeUnit)},
+ * causing it to throw an {@link InterruptedException}.
+ */
+ void abortRequest();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ConnectTimeoutException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ConnectTimeoutException.java
new file mode 100644
index 0000000000..f98bd1941d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ConnectTimeoutException.java
@@ -0,0 +1,94 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.InetAddress;
+import java.util.Arrays;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * A timeout while connecting to an HTTP server or waiting for an
+ * available connection from an HttpConnectionManager.
+ *
+ *
+ * @since 4.0
+ */
+@Immutable
+public class ConnectTimeoutException extends InterruptedIOException {
+
+ private static final long serialVersionUID = -4816682903149535989L;
+
+ private final HttpHost host;
+
+ /**
+ * Creates a ConnectTimeoutException with a <tt>null</tt> detail message.
+ */
+ public ConnectTimeoutException() {
+ super();
+ this.host = null;
+ }
+
+ /**
+ * Creates a ConnectTimeoutException with the specified detail message.
+ */
+ public ConnectTimeoutException(final String message) {
+ super(message);
+ this.host = null;
+ }
+
+ /**
+ * Creates a ConnectTimeoutException based on original {@link IOException}.
+ *
+ * @since 4.3
+ */
+ public ConnectTimeoutException(
+ final IOException cause,
+ final HttpHost host,
+ final InetAddress... remoteAddresses) {
+ super("Connect to " +
+ (host != null ? host.toHostString() : "remote host") +
+ (remoteAddresses != null && remoteAddresses.length > 0 ?
+ " " + Arrays.asList(remoteAddresses) : "") +
+ ((cause != null && cause.getMessage() != null) ?
+ " failed: " + cause.getMessage() : " timed out"));
+ this.host = host;
+ initCause(cause);
+ }
+
+ /**
+ * @since 4.3
+ */
+ public HttpHost getHost() {
+ return host;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ConnectionKeepAliveStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ConnectionKeepAliveStrategy.java
new file mode 100644
index 0000000000..3f06e4873b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ConnectionKeepAliveStrategy.java
@@ -0,0 +1,66 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * Interface for deciding how long a connection can remain
+ * idle before being reused.
+ * <p>
+ * Implementations of this interface must be thread-safe. Access to shared
+ * data must be synchronized as methods of this interface may be executed
+ * from multiple threads.
+ *
+ * @since 4.0
+ */
+public interface ConnectionKeepAliveStrategy {
+
+ /**
+ * Returns the duration of time which this connection can be safely kept
+ * idle. If the connection is left idle for longer than this period of time,
+ * it MUST not reused. A value of 0 or less may be returned to indicate that
+ * there is no suitable suggestion.
+ *
+ * When coupled with a {@link ch.boye.httpclientandroidlib.ConnectionReuseStrategy}, if
+ * {@link ch.boye.httpclientandroidlib.ConnectionReuseStrategy#keepAlive(
+ * HttpResponse, HttpContext)} returns true, this allows you to control
+ * how long the reuse will last. If keepAlive returns false, this should
+ * have no meaningful impact
+ *
+ * @param response
+ * The last response received over the connection.
+ * @param context
+ * the context in which the connection is being used.
+ *
+ * @return the duration in ms for which it is safe to keep the connection
+ * idle, or <=0 if no suggested duration.
+ */
+ long getKeepAliveDuration(HttpResponse response, HttpContext context);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ConnectionPoolTimeoutException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ConnectionPoolTimeoutException.java
new file mode 100644
index 0000000000..d4066b2a3d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ConnectionPoolTimeoutException.java
@@ -0,0 +1,60 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * A timeout while waiting for an available connection
+ * from a connection manager.
+ *
+ *
+ * @since 4.0
+ */
+@Immutable
+public class ConnectionPoolTimeoutException extends ConnectTimeoutException {
+
+ private static final long serialVersionUID = -7898874842020245128L;
+
+ /**
+ * Creates a ConnectTimeoutException with a <tt>null</tt> detail message.
+ */
+ public ConnectionPoolTimeoutException() {
+ super();
+ }
+
+ /**
+ * Creates a ConnectTimeoutException with the specified detail message.
+ *
+ * @param message The exception detail message
+ */
+ public ConnectionPoolTimeoutException(final String message) {
+ super(message);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ConnectionReleaseTrigger.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ConnectionReleaseTrigger.java
new file mode 100644
index 0000000000..97269268b0
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ConnectionReleaseTrigger.java
@@ -0,0 +1,70 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn;
+
+import java.io.IOException;
+
+/**
+ * Interface for releasing a connection. This can be implemented by various
+ * "trigger" objects which are associated with a connection, for example
+ * a {@link EofSensorInputStream stream} or an {@link BasicManagedEntity entity}
+ * or the {@link ManagedClientConnection connection} itself.
+ * <p>
+ * The methods in this interface can safely be called multiple times.
+ * The first invocation releases the connection, subsequent calls
+ * are ignored.
+ *
+ * @since 4.0
+ */
+public interface ConnectionReleaseTrigger {
+
+ /**
+ * Releases the connection with the option of keep-alive. This is a
+ * "graceful" release and may cause IO operations for consuming the
+ * remainder of a response entity. Use
+ * {@link #abortConnection abortConnection} for a hard release. The
+ * connection may be reused as specified by the duration.
+ *
+ * @throws IOException
+ * in case of an IO problem. The connection will be released
+ * anyway.
+ */
+ void releaseConnection()
+ throws IOException;
+
+ /**
+ * Releases the connection without the option of keep-alive.
+ * This is a "hard" release that implies a shutdown of the connection.
+ * Use {@link #releaseConnection()} for a graceful release.
+ *
+ * @throws IOException in case of an IO problem.
+ * The connection will be released anyway.
+ */
+ void abortConnection()
+ throws IOException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ConnectionRequest.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ConnectionRequest.java
new file mode 100644
index 0000000000..838c355674
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ConnectionRequest.java
@@ -0,0 +1,69 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import ch.boye.httpclientandroidlib.HttpClientConnection;
+import ch.boye.httpclientandroidlib.concurrent.Cancellable;
+
+/**
+ * Represents a request for a {@link HttpClientConnection} whose life cycle
+ * is managed by a connection manager.
+ *
+ * @since 4.3
+ */
+public interface ConnectionRequest extends Cancellable {
+
+ /**
+ * Obtains a connection within a given time.
+ * This method will block until a connection becomes available,
+ * the timeout expires, or the connection manager is shut down.
+ * Timeouts are handled with millisecond precision.
+ *
+ * If {@link #cancel()} is called while this is blocking or
+ * before this began, an {@link InterruptedException} will
+ * be thrown.
+ *
+ * @param timeout the timeout, 0 or negative for no timeout
+ * @param tunit the unit for the <code>timeout</code>,
+ * may be <code>null</code> only if there is no timeout
+ *
+ * @return a connection that can be used to communicate
+ * along the given route
+ *
+ * @throws ConnectionPoolTimeoutException
+ * in case of a timeout
+ * @throws InterruptedException
+ * if the calling thread is interrupted while waiting
+ */
+ HttpClientConnection get(long timeout, TimeUnit tunit)
+ throws InterruptedException, ExecutionException, ConnectionPoolTimeoutException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/DnsResolver.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/DnsResolver.java
new file mode 100644
index 0000000000..78ce8709b1
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/DnsResolver.java
@@ -0,0 +1,54 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * Users may implement this interface to override the normal DNS lookup offered
+ * by the OS.
+ *
+ * @since 4.2
+ */
+public interface DnsResolver {
+
+ /**
+ * Returns the IP address for the specified host name, or null if the given
+ * host is not recognized or the associated IP address cannot be used to
+ * build an InetAddress instance.
+ *
+ * @see InetAddress
+ *
+ * @param host
+ * The host name to be resolved by this resolver.
+ * @return The IP address associated to the given host name, or null if the
+ * host name is not known by the implementation class.
+ */
+ InetAddress[] resolve(String host) throws UnknownHostException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/EofSensorInputStream.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/EofSensorInputStream.java
new file mode 100644
index 0000000000..7db23eed4f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/EofSensorInputStream.java
@@ -0,0 +1,289 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * A stream wrapper that triggers actions on {@link #close close()} and EOF.
+ * Primarily used to auto-release an underlying managed connection when the response
+ * body is consumed or no longer needed.
+ *
+ * @see EofSensorWatcher
+ *
+ * @since 4.0
+ */
+// don't use FilterInputStream as the base class, we'd have to
+// override markSupported(), mark(), and reset() to disable them
+@NotThreadSafe
+public class EofSensorInputStream extends InputStream implements ConnectionReleaseTrigger {
+
+ /**
+ * The wrapped input stream, while accessible.
+ * The value changes to <code>null</code> when the wrapped stream
+ * becomes inaccessible.
+ */
+ protected InputStream wrappedStream;
+
+ /**
+ * Indicates whether this stream itself is closed.
+ * If it isn't, but {@link #wrappedStream wrappedStream}
+ * is <code>null</code>, we're running in EOF mode.
+ * All read operations will indicate EOF without accessing
+ * the underlying stream. After closing this stream, read
+ * operations will trigger an {@link IOException IOException}.
+ *
+ * @see #isReadAllowed isReadAllowed
+ */
+ private boolean selfClosed;
+
+ /** The watcher to be notified, if any. */
+ private final EofSensorWatcher eofWatcher;
+
+ /**
+ * Creates a new EOF sensor.
+ * If no watcher is passed, the underlying stream will simply be
+ * closed when EOF is detected or {@link #close close} is called.
+ * Otherwise, the watcher decides whether the underlying stream
+ * should be closed before detaching from it.
+ *
+ * @param in the wrapped stream
+ * @param watcher the watcher for events, or <code>null</code> for
+ * auto-close behavior without notification
+ */
+ public EofSensorInputStream(final InputStream in,
+ final EofSensorWatcher watcher) {
+ Args.notNull(in, "Wrapped stream");
+ wrappedStream = in;
+ selfClosed = false;
+ eofWatcher = watcher;
+ }
+
+ boolean isSelfClosed() {
+ return selfClosed;
+ }
+
+ InputStream getWrappedStream() {
+ return wrappedStream;
+ }
+
+ /**
+ * Checks whether the underlying stream can be read from.
+ *
+ * @return <code>true</code> if the underlying stream is accessible,
+ * <code>false</code> if this stream is in EOF mode and
+ * detached from the underlying stream
+ *
+ * @throws IOException if this stream is already closed
+ */
+ protected boolean isReadAllowed() throws IOException {
+ if (selfClosed) {
+ throw new IOException("Attempted read on closed stream.");
+ }
+ return (wrappedStream != null);
+ }
+
+ @Override
+ public int read() throws IOException {
+ int l = -1;
+
+ if (isReadAllowed()) {
+ try {
+ l = wrappedStream.read();
+ checkEOF(l);
+ } catch (final IOException ex) {
+ checkAbort();
+ throw ex;
+ }
+ }
+
+ return l;
+ }
+
+ @Override
+ public int read(final byte[] b, final int off, final int len) throws IOException {
+ int l = -1;
+
+ if (isReadAllowed()) {
+ try {
+ l = wrappedStream.read(b, off, len);
+ checkEOF(l);
+ } catch (final IOException ex) {
+ checkAbort();
+ throw ex;
+ }
+ }
+
+ return l;
+ }
+
+ @Override
+ public int read(final byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ @Override
+ public int available() throws IOException {
+ int a = 0; // not -1
+
+ if (isReadAllowed()) {
+ try {
+ a = wrappedStream.available();
+ // no checkEOF() here, available() can't trigger EOF
+ } catch (final IOException ex) {
+ checkAbort();
+ throw ex;
+ }
+ }
+
+ return a;
+ }
+
+ @Override
+ public void close() throws IOException {
+ // tolerate multiple calls to close()
+ selfClosed = true;
+ checkClose();
+ }
+
+ /**
+ * Detects EOF and notifies the watcher.
+ * This method should only be called while the underlying stream is
+ * still accessible. Use {@link #isReadAllowed isReadAllowed} to
+ * check that condition.
+ * <br/>
+ * If EOF is detected, the watcher will be notified and this stream
+ * is detached from the underlying stream. This prevents multiple
+ * notifications from this stream.
+ *
+ * @param eof the result of the calling read operation.
+ * A negative value indicates that EOF is reached.
+ *
+ * @throws IOException
+ * in case of an IO problem on closing the underlying stream
+ */
+ protected void checkEOF(final int eof) throws IOException {
+
+ if ((wrappedStream != null) && (eof < 0)) {
+ try {
+ boolean scws = true; // should close wrapped stream?
+ if (eofWatcher != null) {
+ scws = eofWatcher.eofDetected(wrappedStream);
+ }
+ if (scws) {
+ wrappedStream.close();
+ }
+ } finally {
+ wrappedStream = null;
+ }
+ }
+ }
+
+ /**
+ * Detects stream close and notifies the watcher.
+ * There's not much to detect since this is called by {@link #close close}.
+ * The watcher will only be notified if this stream is closed
+ * for the first time and before EOF has been detected.
+ * This stream will be detached from the underlying stream to prevent
+ * multiple notifications to the watcher.
+ *
+ * @throws IOException
+ * in case of an IO problem on closing the underlying stream
+ */
+ protected void checkClose() throws IOException {
+
+ if (wrappedStream != null) {
+ try {
+ boolean scws = true; // should close wrapped stream?
+ if (eofWatcher != null) {
+ scws = eofWatcher.streamClosed(wrappedStream);
+ }
+ if (scws) {
+ wrappedStream.close();
+ }
+ } finally {
+ wrappedStream = null;
+ }
+ }
+ }
+
+ /**
+ * Detects stream abort and notifies the watcher.
+ * There's not much to detect since this is called by
+ * {@link #abortConnection abortConnection}.
+ * The watcher will only be notified if this stream is aborted
+ * for the first time and before EOF has been detected or the
+ * stream has been {@link #close closed} gracefully.
+ * This stream will be detached from the underlying stream to prevent
+ * multiple notifications to the watcher.
+ *
+ * @throws IOException
+ * in case of an IO problem on closing the underlying stream
+ */
+ protected void checkAbort() throws IOException {
+
+ if (wrappedStream != null) {
+ try {
+ boolean scws = true; // should close wrapped stream?
+ if (eofWatcher != null) {
+ scws = eofWatcher.streamAbort(wrappedStream);
+ }
+ if (scws) {
+ wrappedStream.close();
+ }
+ } finally {
+ wrappedStream = null;
+ }
+ }
+ }
+
+ /**
+ * Same as {@link #close close()}.
+ */
+ public void releaseConnection() throws IOException {
+ close();
+ }
+
+ /**
+ * Aborts this stream.
+ * This is a special version of {@link #close close()} which prevents
+ * re-use of the underlying connection, if any. Calling this method
+ * indicates that there should be no attempt to read until the end of
+ * the stream.
+ */
+ public void abortConnection() throws IOException {
+ // tolerate multiple calls
+ selfClosed = true;
+ checkAbort();
+ }
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/EofSensorWatcher.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/EofSensorWatcher.java
new file mode 100644
index 0000000000..c57772fb8c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/EofSensorWatcher.java
@@ -0,0 +1,95 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A watcher for {@link EofSensorInputStream}. Each stream will notify its
+ * watcher at most once.
+ *
+ * @since 4.0
+ */
+public interface EofSensorWatcher {
+
+ /**
+ * Indicates that EOF is detected.
+ *
+ * @param wrapped the underlying stream which has reached EOF
+ *
+ * @return <code>true</code> if <code>wrapped</code> should be closed,
+ * <code>false</code> if it should be left alone
+ *
+ * @throws IOException
+ * in case of an IO problem, for example if the watcher itself
+ * closes the underlying stream. The caller will leave the
+ * wrapped stream alone, as if <code>false</code> was returned.
+ */
+ boolean eofDetected(InputStream wrapped)
+ throws IOException;
+
+ /**
+ * Indicates that the {@link EofSensorInputStream stream} is closed.
+ * This method will be called only if EOF was <i>not</i> detected
+ * before closing. Otherwise, {@link #eofDetected eofDetected} is called.
+ *
+ * @param wrapped the underlying stream which has not reached EOF
+ *
+ * @return <code>true</code> if <code>wrapped</code> should be closed,
+ * <code>false</code> if it should be left alone
+ *
+ * @throws IOException
+ * in case of an IO problem, for example if the watcher itself
+ * closes the underlying stream. The caller will leave the
+ * wrapped stream alone, as if <code>false</code> was returned.
+ */
+ boolean streamClosed(InputStream wrapped)
+ throws IOException;
+
+ /**
+ * Indicates that the {@link EofSensorInputStream stream} is aborted.
+ * This method will be called only if EOF was <i>not</i> detected
+ * before aborting. Otherwise, {@link #eofDetected eofDetected} is called.
+ * <p/>
+ * This method will also be invoked when an input operation causes an
+ * IOException to be thrown to make sure the input stream gets shut down.
+ *
+ * @param wrapped the underlying stream which has not reached EOF
+ *
+ * @return <code>true</code> if <code>wrapped</code> should be closed,
+ * <code>false</code> if it should be left alone
+ *
+ * @throws IOException
+ * in case of an IO problem, for example if the watcher itself
+ * closes the underlying stream. The caller will leave the
+ * wrapped stream alone, as if <code>false</code> was returned.
+ */
+ boolean streamAbort(InputStream wrapped)
+ throws IOException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/HttpClientConnectionManager.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/HttpClientConnectionManager.java
new file mode 100644
index 0000000000..0381a803f4
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/HttpClientConnectionManager.java
@@ -0,0 +1,176 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import ch.boye.httpclientandroidlib.HttpClientConnection;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * Represents a manager of persistent client connections.
+ * <p/>
+ * The purpose of an HTTP connection manager is to serve as a factory for new
+ * HTTP connections, manage persistent connections and synchronize access to
+ * persistent connections making sure that only one thread of execution can
+ * have access to a connection at a time.
+ * <p/>
+ * Implementations of this interface must be thread-safe. Access to shared
+ * data must be synchronized as methods of this interface may be executed
+ * from multiple threads.
+ *
+ * @since 4.3
+ */
+public interface HttpClientConnectionManager {
+
+ /**
+ * Returns a new {@link ConnectionRequest}, from which a
+ * {@link HttpClientConnection} can be obtained or the request can be
+ * aborted.
+ * <p/>
+ * Please note that newly allocated connections can be returned
+ * in the closed state. The consumer of that connection is responsible
+ * for fully establishing the route the to the connection target
+ * by calling {@link #connect(ch.boye.httpclientandroidlib.HttpClientConnection,
+ * ch.boye.httpclientandroidlib.conn.routing.HttpRoute, int,
+ * ch.boye.httpclientandroidlib.protocol.HttpContext) connect} in order to connect
+ * directly to the target or to the first proxy hop, optionally calling
+ * {@link #upgrade(ch.boye.httpclientandroidlib.HttpClientConnection,
+ * ch.boye.httpclientandroidlib.conn.routing.HttpRoute,
+ * ch.boye.httpclientandroidlib.protocol.HttpContext) upgrade} method to upgrade
+ * the connection after having executed <code>CONNECT</code> method to
+ * all intermediate proxy hops and and finally calling {@link #routeComplete(
+ * ch.boye.httpclientandroidlib.HttpClientConnection,
+ * ch.boye.httpclientandroidlib.conn.routing.HttpRoute,
+ * ch.boye.httpclientandroidlib.protocol.HttpContext) routeComplete} to mark the route
+ * as fully completed.
+ *
+ * @param route HTTP route of the requested connection.
+ * @param state expected state of the connection or <code>null</code>
+ * if the connection is not expected to carry any state.
+ */
+ ConnectionRequest requestConnection(HttpRoute route, Object state);
+
+ /**
+ * Releases the connection back to the manager making it potentially
+ * re-usable by other consumers. Optionally, the maximum period
+ * of how long the manager should keep the connection alive can be
+ * defined using <code>validDuration</code> and <code>timeUnit</code>
+ * parameters.
+ *
+ * @param conn the managed connection to release.
+ * @param validDuration the duration of time this connection is valid for reuse.
+ * @param timeUnit the time unit.
+ *
+ * @see #closeExpiredConnections()
+ */
+ void releaseConnection(
+ HttpClientConnection conn, Object newState, long validDuration, TimeUnit timeUnit);
+
+ /**
+ * Connects the underlying connection socket to the connection target in case
+ * of a direct route or to the first proxy hop in case of a route via a proxy
+ * (or multiple proxies).
+ *
+ * @param conn the managed connection.
+ * @param route the route of the connection.
+ * @param connectTimeout connect timeout in milliseconds.
+ * @param context the actual HTTP context.
+ * @throws IOException
+ */
+ void connect(
+ HttpClientConnection conn,
+ HttpRoute route,
+ int connectTimeout,
+ HttpContext context) throws IOException;
+
+ /**
+ * Upgrades the underlying connection socket to TLS/SSL (or another layering
+ * protocol) after having executed <code>CONNECT</code> method to all
+ * intermediate proxy hops
+ *
+ * @param conn the managed connection.
+ * @param route the route of the connection.
+ * @param context the actual HTTP context.
+ * @throws IOException
+ */
+ void upgrade(
+ HttpClientConnection conn,
+ HttpRoute route,
+ HttpContext context) throws IOException;
+
+ /**
+ * Marks the connection as fully established with all its intermediate
+ * hops completed.
+ *
+ * @param conn the managed connection.
+ * @param route the route of the connection.
+ * @param context the actual HTTP context.
+ * @throws IOException
+ */
+ void routeComplete(
+ HttpClientConnection conn,
+ HttpRoute route,
+ HttpContext context) throws IOException;
+
+ /**
+ * Closes idle connections in the pool.
+ * <p/>
+ * Open connections in the pool that have not been used for the
+ * timespan given by the argument will be closed.
+ * Currently allocated connections are not subject to this method.
+ * Times will be checked with milliseconds precision
+ *
+ * All expired connections will also be closed.
+ *
+ * @param idletime the idle time of connections to be closed
+ * @param tunit the unit for the <code>idletime</code>
+ *
+ * @see #closeExpiredConnections()
+ */
+ void closeIdleConnections(long idletime, TimeUnit tunit);
+
+ /**
+ * Closes all expired connections in the pool.
+ * <p/>
+ * Open connections in the pool that have not been used for
+ * the timespan defined when the connection was released will be closed.
+ * Currently allocated connections are not subject to this method.
+ * Times will be checked with milliseconds precision.
+ */
+ void closeExpiredConnections();
+
+ /**
+ * Shuts down this connection manager and releases allocated resources.
+ * This includes closing all connections, whether they are currently
+ * used or not.
+ */
+ void shutdown();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/HttpConnectionFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/HttpConnectionFactory.java
new file mode 100644
index 0000000000..be910921f3
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/HttpConnectionFactory.java
@@ -0,0 +1,41 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn;
+
+import ch.boye.httpclientandroidlib.HttpConnection;
+import ch.boye.httpclientandroidlib.config.ConnectionConfig;
+
+/**
+ * Generic {@link HttpConnection} factory.
+ *
+ * @since 4.3
+ */
+public interface HttpConnectionFactory<T, C extends HttpConnection> {
+
+ C create(T route, ConnectionConfig config);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/HttpHostConnectException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/HttpHostConnectException.java
new file mode 100644
index 0000000000..9b1a1c57a2
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/HttpHostConnectException.java
@@ -0,0 +1,82 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn;
+
+import java.io.IOException;
+import java.net.ConnectException;
+import java.net.InetAddress;
+import java.util.Arrays;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * A {@link ConnectException} that specifies the {@link HttpHost} that was
+ * being connected to.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class HttpHostConnectException extends ConnectException {
+
+ private static final long serialVersionUID = -3194482710275220224L;
+
+ private final HttpHost host;
+
+ /**
+ * @deprecated (4.3) use {@link #HttpHostConnectException(java.io.IOException, ch.boye.httpclientandroidlib.HttpHost,
+ * java.net.InetAddress...)}
+ */
+ @Deprecated
+ public HttpHostConnectException(final HttpHost host, final ConnectException cause) {
+ this(cause, host, null);
+ }
+
+ /**
+ * Creates a HttpHostConnectException based on original {@link java.io.IOException}.
+ *
+ * @since 4.3
+ */
+ public HttpHostConnectException(
+ final IOException cause,
+ final HttpHost host,
+ final InetAddress... remoteAddresses) {
+ super("Connect to " +
+ (host != null ? host.toHostString() : "remote host") +
+ (remoteAddresses != null && remoteAddresses .length > 0 ?
+ " " + Arrays.asList(remoteAddresses) : "") +
+ ((cause != null && cause.getMessage() != null) ?
+ " failed: " + cause.getMessage() : " refused"));
+ this.host = host;
+ initCause(cause);
+ }
+
+ public HttpHost getHost() {
+ return this.host;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/HttpInetSocketAddress.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/HttpInetSocketAddress.java
new file mode 100644
index 0000000000..3bd878d211
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/HttpInetSocketAddress.java
@@ -0,0 +1,65 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Extended {@link InetSocketAddress} implementation that also provides access to the original
+ * {@link HttpHost} used to resolve the address.
+ *
+ * @since 4.2 no longer used.
+ *
+ * @deprecated (4.3)
+ */
+@Deprecated
+public class HttpInetSocketAddress extends InetSocketAddress {
+
+ private static final long serialVersionUID = -6650701828361907957L;
+
+ private final HttpHost httphost;
+
+ public HttpInetSocketAddress(final HttpHost httphost, final InetAddress addr, final int port) {
+ super(addr, port);
+ Args.notNull(httphost, "HTTP host");
+ this.httphost = httphost;
+ }
+
+ public HttpHost getHttpHost() {
+ return this.httphost;
+ }
+
+ @Override
+ public String toString() {
+ return this.httphost.getHostName() + ":" + getPort();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/HttpRoutedConnection.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/HttpRoutedConnection.java
new file mode 100644
index 0000000000..956e61208e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/HttpRoutedConnection.java
@@ -0,0 +1,81 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn;
+
+import javax.net.ssl.SSLSession;
+
+import ch.boye.httpclientandroidlib.HttpInetConnection;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+
+/**
+ * Interface to access routing information of a client side connection.
+ *
+ * @since 4.1
+ *
+ * @deprecated (4.3) replaced by {@link HttpClientConnectionManager}.
+ */
+@Deprecated
+public interface HttpRoutedConnection extends HttpInetConnection {
+
+ /**
+ * Indicates whether this connection is secure.
+ * The return value is well-defined only while the connection is open.
+ * It may change even while the connection is open.
+ *
+ * @return <code>true</code> if this connection is secure,
+ * <code>false</code> otherwise
+ */
+ boolean isSecure();
+
+ /**
+ * Obtains the current route of this connection.
+ *
+ * @return the route established so far, or
+ * <code>null</code> if not connected
+ */
+ HttpRoute getRoute();
+
+ /**
+ * Obtains the SSL session of the underlying connection, if any.
+ * If this connection is open, and the underlying socket is an
+ * {@link javax.net.ssl.SSLSocket SSLSocket}, the SSL session of
+ * that socket is obtained. This is a potentially blocking operation.
+ * <br/>
+ * <b>Note:</b> Whether the underlying socket is an SSL socket
+ * can not necessarily be determined via {@link #isSecure}.
+ * Plain sockets may be considered secure, for example if they are
+ * connected to a known host in the same network segment.
+ * On the other hand, SSL sockets may be considered insecure,
+ * for example depending on the chosen cipher suite.
+ *
+ * @return the underlying SSL session if available,
+ * <code>null</code> otherwise
+ */
+ SSLSession getSSLSession();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ManagedClientConnection.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ManagedClientConnection.java
new file mode 100644
index 0000000000..a54567b4f1
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ManagedClientConnection.java
@@ -0,0 +1,228 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.SSLSession;
+
+import ch.boye.httpclientandroidlib.HttpClientConnection;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * A client-side connection with advanced connection logic.
+ * Instances are typically obtained from a connection manager.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) replaced by {@link HttpClientConnectionManager}.
+ */
+@Deprecated
+public interface ManagedClientConnection extends
+ HttpClientConnection, HttpRoutedConnection, ManagedHttpClientConnection, ConnectionReleaseTrigger {
+
+ /**
+ * Indicates whether this connection is secure.
+ * The return value is well-defined only while the connection is open.
+ * It may change even while the connection is open.
+ *
+ * @return <code>true</code> if this connection is secure,
+ * <code>false</code> otherwise
+ */
+ boolean isSecure();
+
+ /**
+ * Obtains the current route of this connection.
+ *
+ * @return the route established so far, or
+ * <code>null</code> if not connected
+ */
+ HttpRoute getRoute();
+
+ /**
+ * Obtains the SSL session of the underlying connection, if any.
+ * If this connection is open, and the underlying socket is an
+ * {@link javax.net.ssl.SSLSocket SSLSocket}, the SSL session of
+ * that socket is obtained. This is a potentially blocking operation.
+ * <br/>
+ * <b>Note:</b> Whether the underlying socket is an SSL socket
+ * can not necessarily be determined via {@link #isSecure}.
+ * Plain sockets may be considered secure, for example if they are
+ * connected to a known host in the same network segment.
+ * On the other hand, SSL sockets may be considered insecure,
+ * for example depending on the chosen cipher suite.
+ *
+ * @return the underlying SSL session if available,
+ * <code>null</code> otherwise
+ */
+ SSLSession getSSLSession();
+
+ /**
+ * Opens this connection according to the given route.
+ *
+ * @param route the route along which to open. It will be opened to
+ * the first proxy if present, or directly to the target.
+ * @param context the context for opening this connection
+ * @param params the parameters for opening this connection
+ *
+ * @throws IOException in case of a problem
+ */
+ void open(HttpRoute route, HttpContext context, HttpParams params)
+ throws IOException;
+
+ /**
+ * Indicates that a tunnel to the target has been established.
+ * The route is the one previously passed to {@link #open open}.
+ * Subsequently, {@link #layerProtocol layerProtocol} can be called
+ * to layer the TLS/SSL protocol on top of the tunnelled connection.
+ * <br/>
+ * <b>Note:</b> In HttpClient 3, a call to the corresponding method
+ * would automatically trigger the layering of the TLS/SSL protocol.
+ * This is not the case anymore, you can establish a tunnel without
+ * layering a new protocol over the connection.
+ *
+ * @param secure <code>true</code> if the tunnel should be considered
+ * secure, <code>false</code> otherwise
+ * @param params the parameters for tunnelling this connection
+ *
+ * @throws IOException in case of a problem
+ */
+ void tunnelTarget(boolean secure, HttpParams params)
+ throws IOException;
+
+ /**
+ * Indicates that a tunnel to an intermediate proxy has been established.
+ * This is used exclusively for so-called <i>proxy chains</i>, where
+ * a request has to pass through multiple proxies before reaching the
+ * target. In that case, all proxies but the last need to be tunnelled
+ * when establishing the connection. Tunnelling of the last proxy to the
+ * target is optional and would be indicated via {@link #tunnelTarget}.
+ *
+ * @param next the proxy to which the tunnel was established.
+ * This is <i>not</i> the proxy <i>through</i> which
+ * the tunnel was established, but the new end point
+ * of the tunnel. The tunnel does <i>not</i> yet
+ * reach to the target, use {@link #tunnelTarget}
+ * to indicate an end-to-end tunnel.
+ * @param secure <code>true</code> if the connection should be
+ * considered secure, <code>false</code> otherwise
+ * @param params the parameters for tunnelling this connection
+ *
+ * @throws IOException in case of a problem
+ */
+ void tunnelProxy(HttpHost next, boolean secure, HttpParams params)
+ throws IOException;
+
+ /**
+ * Layers a new protocol on top of a {@link #tunnelTarget tunnelled}
+ * connection. This is typically used to create a TLS/SSL connection
+ * through a proxy.
+ * The route is the one previously passed to {@link #open open}.
+ * It is not guaranteed that the layered connection is
+ * {@link #isSecure secure}.
+ *
+ * @param context the context for layering on top of this connection
+ * @param params the parameters for layering on top of this connection
+ *
+ * @throws IOException in case of a problem
+ */
+ void layerProtocol(HttpContext context, HttpParams params)
+ throws IOException;
+
+ /**
+ * Marks this connection as being in a reusable communication state.
+ * The checkpoints for reuseable communication states (in the absence
+ * of pipelining) are before sending a request and after receiving
+ * the response in its entirety.
+ * The connection will automatically clear the checkpoint when
+ * used for communication. A call to this method indicates that
+ * the next checkpoint has been reached.
+ * <br/>
+ * A reusable communication state is necessary but not sufficient
+ * for the connection to be reused.
+ * A {@link #getRoute route} mismatch, the connection being closed,
+ * or other circumstances might prevent reuse.
+ */
+ void markReusable();
+
+ /**
+ * Marks this connection as not being in a reusable state.
+ * This can be used immediately before releasing this connection
+ * to prevent its reuse. Reasons for preventing reuse include
+ * error conditions and the evaluation of a
+ * {@link ch.boye.httpclientandroidlib.ConnectionReuseStrategy reuse strategy}.
+ * <br/>
+ * <b>Note:</b>
+ * It is <i>not</i> necessary to call here before writing to
+ * or reading from this connection. Communication attempts will
+ * automatically unmark the state as non-reusable. It can then
+ * be switched back using {@link #markReusable markReusable}.
+ */
+ void unmarkReusable();
+
+ /**
+ * Indicates whether this connection is in a reusable communication state.
+ * See {@link #markReusable markReusable} and
+ * {@link #unmarkReusable unmarkReusable} for details.
+ *
+ * @return <code>true</code> if this connection is marked as being in
+ * a reusable communication state,
+ * <code>false</code> otherwise
+ */
+ boolean isMarkedReusable();
+
+ /**
+ * Assigns a state object to this connection. Connection managers may make
+ * use of the connection state when allocating persistent connections.
+ *
+ * @param state The state object
+ */
+ void setState(Object state);
+
+ /**
+ * Returns the state object associated with this connection.
+ *
+ * @return The state object
+ */
+ Object getState();
+
+ /**
+ * Sets the duration that this connection can remain idle before it is
+ * reused. The connection should not be used again if this time elapses. The
+ * idle duration must be reset after each request sent over this connection.
+ * The elapsed time starts counting when the connection is released, which
+ * is typically after the headers (and any response body, if present) is
+ * fully consumed.
+ */
+ void setIdleDuration(long duration, TimeUnit unit);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ManagedHttpClientConnection.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ManagedHttpClientConnection.java
new file mode 100644
index 0000000000..4195d59b70
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ManagedHttpClientConnection.java
@@ -0,0 +1,80 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn;
+
+import java.io.IOException;
+import java.net.Socket;
+
+import javax.net.ssl.SSLSession;
+
+import ch.boye.httpclientandroidlib.HttpClientConnection;
+import ch.boye.httpclientandroidlib.HttpInetConnection;
+
+/**
+ * Represents a managed connection whose state and life cycle is managed by
+ * a connection manager. This interface extends {@link HttpClientConnection}
+ * with methods to bind the connection to an arbitrary socket and
+ * to obtain SSL session details.
+ *
+ * @since 4.3
+ */
+public interface ManagedHttpClientConnection extends HttpClientConnection, HttpInetConnection {
+
+ /**
+ * Returns connection ID which is expected to be unique
+ * for the life span of the connection manager.
+ */
+ String getId();
+
+ /**
+ * Binds this connection to the given socket. The connection
+ * is considered open if it is bound and the underlying socket
+ * is connection to a remote host.
+ *
+ * @param socket the socket to bind the connection to.
+ * @throws IOException
+ */
+ void bind(Socket socket) throws IOException;
+
+ /**
+ * Returns the underlying socket.
+ */
+ Socket getSocket();
+
+ /**
+ * Obtains the SSL session of the underlying connection, if any.
+ * If this connection is open, and the underlying socket is an
+ * {@link javax.net.ssl.SSLSocket SSLSocket}, the SSL session of
+ * that socket is obtained. This is a potentially blocking operation.
+ *
+ * @return the underlying SSL session if available,
+ * <code>null</code> otherwise
+ */
+ SSLSession getSSLSession();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/MultihomePlainSocketFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/MultihomePlainSocketFactory.java
new file mode 100644
index 0000000000..c4c34f7692
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/MultihomePlainSocketFactory.java
@@ -0,0 +1,173 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.conn.scheme.SocketFactory;
+import ch.boye.httpclientandroidlib.params.HttpConnectionParams;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+
+/**
+ * Socket factory that implements a simple multi-home fail-over on connect failure,
+ * provided the same hostname resolves to multiple {@link InetAddress}es. Please note
+ * the {@link #connectSocket(Socket, String, int, InetAddress, int, HttpParams)}
+ * method cannot be reliably interrupted by closing the socket returned by the
+ * {@link #createSocket()} method.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.1) Do not use. For multihome support socket factories must implement
+ * {@link ch.boye.httpclientandroidlib.conn.scheme.SchemeSocketFactory} interface.
+ */
+@Deprecated
+@Immutable
+public final class MultihomePlainSocketFactory implements SocketFactory {
+
+ /**
+ * The factory singleton.
+ */
+ private static final
+ MultihomePlainSocketFactory DEFAULT_FACTORY = new MultihomePlainSocketFactory();
+
+ /**
+ * Gets the singleton instance of this class.
+ * @return the one and only plain socket factory
+ */
+ public static MultihomePlainSocketFactory getSocketFactory() {
+ return DEFAULT_FACTORY;
+ }
+
+ /**
+ * Restricted default constructor.
+ */
+ private MultihomePlainSocketFactory() {
+ super();
+ }
+
+
+ // non-javadoc, see interface ch.boye.httpclientandroidlib.conn.SocketFactory
+ public Socket createSocket() {
+ return new Socket();
+ }
+
+ /**
+ * Attempts to connects the socket to any of the {@link InetAddress}es the
+ * given host name resolves to. If connection to all addresses fail, the
+ * last I/O exception is propagated to the caller.
+ *
+ * @param socket socket to connect to any of the given addresses
+ * @param host Host name to connect to
+ * @param port the port to connect to
+ * @param localAddress local address
+ * @param localPort local port
+ * @param params HTTP parameters
+ *
+ * @throws IOException if an error occurs during the connection
+ * @throws SocketTimeoutException if timeout expires before connecting
+ */
+ public Socket connectSocket(final Socket socket, final String host, final int port,
+ final InetAddress localAddress, final int localPort,
+ final HttpParams params)
+ throws IOException {
+ Args.notNull(host, "Target host");
+ Args.notNull(params, "HTTP parameters");
+
+ Socket sock = socket;
+ if (sock == null) {
+ sock = createSocket();
+ }
+
+ if ((localAddress != null) || (localPort > 0)) {
+ final InetSocketAddress isa = new InetSocketAddress(localAddress,
+ localPort > 0 ? localPort : 0);
+ sock.bind(isa);
+ }
+
+ final int timeout = HttpConnectionParams.getConnectionTimeout(params);
+
+ final InetAddress[] inetadrs = InetAddress.getAllByName(host);
+ final List<InetAddress> addresses = new ArrayList<InetAddress>(inetadrs.length);
+ addresses.addAll(Arrays.asList(inetadrs));
+ Collections.shuffle(addresses);
+
+ IOException lastEx = null;
+ for (final InetAddress remoteAddress: addresses) {
+ try {
+ sock.connect(new InetSocketAddress(remoteAddress, port), timeout);
+ break;
+ } catch (final SocketTimeoutException ex) {
+ throw new ConnectTimeoutException("Connect to " + remoteAddress + " timed out");
+ } catch (final IOException ex) {
+ // create new socket
+ sock = new Socket();
+ // keep the last exception and retry
+ lastEx = ex;
+ }
+ }
+ if (lastEx != null) {
+ throw lastEx;
+ }
+ return sock;
+ } // connectSocket
+
+
+ /**
+ * Checks whether a socket connection is secure.
+ * This factory creates plain socket connections
+ * which are not considered secure.
+ *
+ * @param sock the connected socket
+ *
+ * @return <code>false</code>
+ *
+ * @throws IllegalArgumentException if the argument is invalid
+ */
+ public final boolean isSecure(final Socket sock)
+ throws IllegalArgumentException {
+
+ Args.notNull(sock, "Socket");
+ // This check is performed last since it calls a method implemented
+ // by the argument object. getClass() is final in java.lang.Object.
+ Asserts.check(!sock.isClosed(), "Socket is closed");
+ return false;
+
+ } // isSecure
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/OperatedClientConnection.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/OperatedClientConnection.java
new file mode 100644
index 0000000000..3f8df0591f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/OperatedClientConnection.java
@@ -0,0 +1,155 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn;
+
+import java.io.IOException;
+import java.net.Socket;
+
+import ch.boye.httpclientandroidlib.HttpClientConnection;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpInetConnection;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+/**
+ * A client-side connection that relies on outside logic to connect sockets to the
+ * appropriate hosts. It can be operated directly by an application, or through an
+ * {@link ClientConnectionOperator operator}.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) replaced by {@link HttpClientConnectionManager}.
+ */
+@Deprecated
+public interface OperatedClientConnection extends HttpClientConnection, HttpInetConnection {
+
+ /**
+ * Obtains the target host for this connection.
+ * If the connection is to a proxy but not tunnelled, this is
+ * the proxy. If the connection is tunnelled through a proxy,
+ * this is the target of the tunnel.
+ * <br/>
+ * The return value is well-defined only while the connection is open.
+ * It may change even while the connection is open,
+ * because of an {@link #update update}.
+ *
+ * @return the host to which this connection is opened
+ */
+ HttpHost getTargetHost();
+
+ /**
+ * Indicates whether this connection is secure.
+ * The return value is well-defined only while the connection is open.
+ * It may change even while the connection is open,
+ * because of an {@link #update update}.
+ *
+ * @return <code>true</code> if this connection is secure,
+ * <code>false</code> otherwise
+ */
+ boolean isSecure();
+
+ /**
+ * Obtains the socket for this connection.
+ * The return value is well-defined only while the connection is open.
+ * It may change even while the connection is open,
+ * because of an {@link #update update}.
+ *
+ * @return the socket for communicating with the
+ * {@link #getTargetHost target host}
+ */
+ Socket getSocket();
+
+ /**
+ * Signals that this connection is in the process of being open.
+ * <p>
+ * By calling this method, the connection can be re-initialized
+ * with a new Socket instance before {@link #openCompleted} is called.
+ * This enabled the connection to close that socket if
+ * {@link ch.boye.httpclientandroidlib.HttpConnection#shutdown shutdown}
+ * is called before it is fully open. Closing an unconnected socket
+ * will interrupt a thread that is blocked on the connect.
+ * Otherwise, that thread will either time out on the connect,
+ * or it returns successfully and then opens this connection
+ * which was just shut down.
+ * <p>
+ * This method can be called multiple times if the connection
+ * is layered over another protocol. <b>Note:</b> This method
+ * will <i>not</i> close the previously used socket. It is
+ * the caller's responsibility to close that socket if it is
+ * no longer required.
+ * <p>
+ * The caller must invoke {@link #openCompleted} in order to complete
+ * the process.
+ *
+ * @param sock the unconnected socket which is about to
+ * be connected.
+ * @param target the target host of this connection
+ */
+ void opening(Socket sock, HttpHost target)
+ throws IOException;
+
+ /**
+ * Signals that the connection has been successfully open.
+ * An attempt to call this method on an open connection will cause
+ * an exception.
+ *
+ * @param secure <code>true</code> if this connection is secure, for
+ * example if an <code>SSLSocket</code> is used, or
+ * <code>false</code> if it is not secure
+ * @param params parameters for this connection. The parameters will
+ * be used when creating dependent objects, for example
+ * to determine buffer sizes.
+ */
+ void openCompleted(boolean secure, HttpParams params)
+ throws IOException;
+
+ /**
+ * Updates this connection.
+ * A connection can be updated only while it is open.
+ * Updates are used for example when a tunnel has been established,
+ * or when a TLS/SSL connection has been layered on top of a plain
+ * socket connection.
+ * <br/>
+ * <b>Note:</b> Updating the connection will <i>not</i> close the
+ * previously used socket. It is the caller's responsibility to close
+ * that socket if it is no longer required.
+ *
+ * @param sock the new socket for communicating with the target host,
+ * or <code>null</code> to continue using the old socket.
+ * If <code>null</code> is passed, helper objects that
+ * depend on the socket should be re-used. In that case,
+ * some changes in the parameters will not take effect.
+ * @param target the new target host of this connection
+ * @param secure <code>true</code> if this connection is now secure,
+ * <code>false</code> if it is not secure
+ * @param params new parameters for this connection
+ */
+ void update(Socket sock, HttpHost target,
+ boolean secure, HttpParams params)
+ throws IOException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/SchemePortResolver.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/SchemePortResolver.java
new file mode 100644
index 0000000000..10df60f04a
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/SchemePortResolver.java
@@ -0,0 +1,43 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+
+/**
+ * Strategy for default port resolution for protocol schemes.
+ *
+ * @since 4.3
+ */
+public interface SchemePortResolver {
+
+ /**
+ * Returns the actual port for the host based on the protocol scheme.
+ */
+ int resolve(HttpHost host) throws UnsupportedSchemeException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/UnsupportedSchemeException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/UnsupportedSchemeException.java
new file mode 100644
index 0000000000..c8034191ec
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/UnsupportedSchemeException.java
@@ -0,0 +1,51 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Signals failure to establish connection using an unknown protocol scheme.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class UnsupportedSchemeException extends IOException {
+
+ private static final long serialVersionUID = 3597127619218687636L;
+
+ /**
+ * Creates a UnsupportedSchemeException with the specified detail message.
+ */
+ public UnsupportedSchemeException(final String message) {
+ super(message);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/package-info.java
new file mode 100644
index 0000000000..6d83f5a8bb
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Client connection management APIs.
+ */
+package ch.boye.httpclientandroidlib.conn;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnConnectionPNames.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnConnectionPNames.java
new file mode 100644
index 0000000000..59246459f0
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnConnectionPNames.java
@@ -0,0 +1,64 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn.params;
+
+/**
+ * Parameter names for HTTP client connections.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.1) use custom {@link
+ * ch.boye.httpclientandroidlib.impl.conn.DefaultHttpResponseParser} implementation.
+ */
+@Deprecated
+public interface ConnConnectionPNames {
+
+ /**
+ * Defines the maximum number of ignorable lines before we expect
+ * a HTTP response's status line.
+ * <p>
+ * With HTTP/1.1 persistent connections, the problem arises that
+ * broken scripts could return a wrong Content-Length
+ * (there are more bytes sent than specified).
+ * Unfortunately, in some cases, this cannot be detected after the
+ * bad response, but only before the next one.
+ * So HttpClient must be able to skip those surplus lines this way.
+ * </p>
+ * <p>
+ * This parameter expects a value of type {@link Integer}.
+ * 0 disallows all garbage/empty lines before the status line.
+ * Use {@link java.lang.Integer#MAX_VALUE} for unlimited number.
+ * </p>
+ *
+ * @deprecated (4.1) Use custom {@link
+ * ch.boye.httpclientandroidlib.impl.conn.DefaultHttpResponseParser} implementation
+ */
+ @Deprecated
+ public static final String MAX_STATUS_LINE_GARBAGE = "http.connection.max-status-line-garbage";
+
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnConnectionParamBean.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnConnectionParamBean.java
new file mode 100644
index 0000000000..8c8b1b2dcf
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnConnectionParamBean.java
@@ -0,0 +1,59 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.params;
+
+import ch.boye.httpclientandroidlib.params.HttpAbstractParamBean;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+/**
+ * This is a Java Bean class that can be used to wrap an instance of
+ * {@link HttpParams} and manipulate HTTP client connection parameters
+ * using Java Beans conventions.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.1) use custom {@link
+ * ch.boye.httpclientandroidlib.impl.conn.DefaultHttpResponseParser} implementation.
+ */
+@Deprecated
+public class ConnConnectionParamBean extends HttpAbstractParamBean {
+
+ public ConnConnectionParamBean (final HttpParams params) {
+ super(params);
+ }
+
+ /**
+ * @deprecated (4.2) Use custom {@link
+ * ch.boye.httpclientandroidlib.impl.conn.DefaultHttpResponseParser} implementation
+ */
+ @Deprecated
+ public void setMaxStatusLineGarbage (final int maxStatusLineGarbage) {
+ params.setIntParameter(ConnConnectionPNames.MAX_STATUS_LINE_GARBAGE, maxStatusLineGarbage);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnManagerPNames.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnManagerPNames.java
new file mode 100644
index 0000000000..16a79ca18c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnManagerPNames.java
@@ -0,0 +1,67 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn.params;
+
+/**
+ * Parameter names for connection managers in HttpConn.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.1) use configuration methods of the specific connection manager implementation.
+*/
+@Deprecated
+public interface ConnManagerPNames {
+
+ /**
+ * Defines the timeout in milliseconds used when retrieving an instance of
+ * {@link ch.boye.httpclientandroidlib.conn.ManagedClientConnection} from the
+ * {@link ch.boye.httpclientandroidlib.conn.ClientConnectionManager}.
+ * <p>
+ * This parameter expects a value of type {@link Long}.
+ */
+ public static final String TIMEOUT = "http.conn-manager.timeout";
+
+ /**
+ * Defines the maximum number of connections per route.
+ * This limit is interpreted by client connection managers
+ * and applies to individual manager instances.
+ * <p>
+ * This parameter expects a value of type {@link ConnPerRoute}.
+ * <p>
+ */
+ public static final String MAX_CONNECTIONS_PER_ROUTE = "http.conn-manager.max-per-route";
+
+ /**
+ * Defines the maximum number of connections in total.
+ * This limit is interpreted by client connection managers
+ * and applies to individual manager instances.
+ * <p>
+ * This parameter expects a value of type {@link Integer}.
+ */
+ public static final String MAX_TOTAL_CONNECTIONS = "http.conn-manager.max-total";
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnManagerParamBean.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnManagerParamBean.java
new file mode 100644
index 0000000000..735a253601
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnManagerParamBean.java
@@ -0,0 +1,63 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.params;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.params.HttpAbstractParamBean;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+/**
+ * This is a Java Bean class that can be used to wrap an instance of
+ * {@link HttpParams} and manipulate connection manager parameters
+ * using Java Beans conventions.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.1) use configuration methods of the specific connection manager implementation.
+ */
+@NotThreadSafe
+@Deprecated
+public class ConnManagerParamBean extends HttpAbstractParamBean {
+
+ public ConnManagerParamBean (final HttpParams params) {
+ super(params);
+ }
+
+ public void setTimeout (final long timeout) {
+ params.setLongParameter(ConnManagerPNames.TIMEOUT, timeout);
+ }
+
+ public void setMaxTotalConnections (final int maxConnections) {
+ params.setIntParameter(ConnManagerPNames.MAX_TOTAL_CONNECTIONS, maxConnections);
+ }
+
+ public void setConnectionsPerRoute(final ConnPerRouteBean connPerRoute) {
+ params.setParameter(ConnManagerPNames.MAX_CONNECTIONS_PER_ROUTE, connPerRoute);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnManagerParams.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnManagerParams.java
new file mode 100644
index 0000000000..e6521134ac
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnManagerParams.java
@@ -0,0 +1,147 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn.params;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * An adaptor for manipulating HTTP connection management
+ * parameters in {@link HttpParams}.
+ *
+ * @since 4.0
+ *
+ * @see ConnManagerPNames
+ *
+ * @deprecated (4.1) use configuration methods of the specific connection manager implementation.
+ */
+@Deprecated
+@Immutable
+public final class ConnManagerParams implements ConnManagerPNames {
+
+ /** The default maximum number of connections allowed overall */
+ public static final int DEFAULT_MAX_TOTAL_CONNECTIONS = 20;
+
+ /**
+ * Returns the timeout in milliseconds used when retrieving a
+ * {@link ch.boye.httpclientandroidlib.conn.ManagedClientConnection} from the
+ * {@link ch.boye.httpclientandroidlib.conn.ClientConnectionManager}.
+ *
+ * @return timeout in milliseconds.
+ *
+ * @deprecated (4.1) use {@link
+ * ch.boye.httpclientandroidlib.params.HttpConnectionParams#getConnectionTimeout(HttpParams)}
+ */
+ @Deprecated
+ public static long getTimeout(final HttpParams params) {
+ Args.notNull(params, "HTTP parameters");
+ return params.getLongParameter(TIMEOUT, 0);
+ }
+
+ /**
+ * Sets the timeout in milliseconds used when retrieving a
+ * {@link ch.boye.httpclientandroidlib.conn.ManagedClientConnection} from the
+ * {@link ch.boye.httpclientandroidlib.conn.ClientConnectionManager}.
+ *
+ * @param timeout the timeout in milliseconds
+ *
+ * @deprecated (4.1) use {@link
+ * ch.boye.httpclientandroidlib.params.HttpConnectionParams#setConnectionTimeout(HttpParams, int)}
+ */
+ @Deprecated
+ public static void setTimeout(final HttpParams params, final long timeout) {
+ Args.notNull(params, "HTTP parameters");
+ params.setLongParameter(TIMEOUT, timeout);
+ }
+
+ /** The default maximum number of connections allowed per host */
+ private static final ConnPerRoute DEFAULT_CONN_PER_ROUTE = new ConnPerRoute() {
+
+ public int getMaxForRoute(final HttpRoute route) {
+ return ConnPerRouteBean.DEFAULT_MAX_CONNECTIONS_PER_ROUTE;
+ }
+
+ };
+
+ /**
+ * Sets lookup interface for maximum number of connections allowed per route.
+ *
+ * @param params HTTP parameters
+ * @param connPerRoute lookup interface for maximum number of connections allowed
+ * per route
+ */
+ public static void setMaxConnectionsPerRoute(final HttpParams params,
+ final ConnPerRoute connPerRoute) {
+ Args.notNull(params, "HTTP parameters");
+ params.setParameter(MAX_CONNECTIONS_PER_ROUTE, connPerRoute);
+ }
+
+ /**
+ * Returns lookup interface for maximum number of connections allowed per route.
+ *
+ * @param params HTTP parameters
+ *
+ * @return lookup interface for maximum number of connections allowed per route.
+ */
+ public static ConnPerRoute getMaxConnectionsPerRoute(final HttpParams params) {
+ Args.notNull(params, "HTTP parameters");
+ ConnPerRoute connPerRoute = (ConnPerRoute) params.getParameter(MAX_CONNECTIONS_PER_ROUTE);
+ if (connPerRoute == null) {
+ connPerRoute = DEFAULT_CONN_PER_ROUTE;
+ }
+ return connPerRoute;
+ }
+
+ /**
+ * Sets the maximum number of connections allowed.
+ *
+ * @param params HTTP parameters
+ * @param maxTotalConnections The maximum number of connections allowed.
+ */
+ public static void setMaxTotalConnections(
+ final HttpParams params,
+ final int maxTotalConnections) {
+ Args.notNull(params, "HTTP parameters");
+ params.setIntParameter(MAX_TOTAL_CONNECTIONS, maxTotalConnections);
+ }
+
+ /**
+ * Gets the maximum number of connections allowed.
+ *
+ * @param params HTTP parameters
+ *
+ * @return The maximum number of connections allowed.
+ */
+ public static int getMaxTotalConnections(
+ final HttpParams params) {
+ Args.notNull(params, "HTTP parameters");
+ return params.getIntParameter(MAX_TOTAL_CONNECTIONS, DEFAULT_MAX_TOTAL_CONNECTIONS);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnPerRoute.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnPerRoute.java
new file mode 100644
index 0000000000..ebaad93cb3
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnPerRoute.java
@@ -0,0 +1,46 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn.params;
+
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+
+/**
+ * This interface is intended for looking up maximum number of connections
+ * allowed for a given route. This class can be used by pooling
+ * {@link ch.boye.httpclientandroidlib.conn.ClientConnectionManager connection managers} for
+ * a fine-grained control of connections on a per route basis.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) no longer used.
+ */
+@Deprecated
+public interface ConnPerRoute {
+
+ int getMaxForRoute(HttpRoute route);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnPerRouteBean.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnPerRouteBean.java
new file mode 100644
index 0000000000..6cc6da8ab1
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnPerRouteBean.java
@@ -0,0 +1,112 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn.params;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * This class maintains a map of HTTP routes to maximum number of connections allowed
+ * for those routes. This class can be used by pooling
+ * {@link ch.boye.httpclientandroidlib.conn.ClientConnectionManager connection managers} for
+ * a fine-grained control of connections on a per route basis.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) use {@link ch.boye.httpclientandroidlib.pool.ConnPoolControl}
+ */
+@Deprecated
+@ThreadSafe
+public final class ConnPerRouteBean implements ConnPerRoute {
+
+ /** The default maximum number of connections allowed per host */
+ public static final int DEFAULT_MAX_CONNECTIONS_PER_ROUTE = 2; // Per RFC 2616 sec 8.1.4
+
+ private final ConcurrentHashMap<HttpRoute, Integer> maxPerHostMap;
+
+ private volatile int defaultMax;
+
+ public ConnPerRouteBean(final int defaultMax) {
+ super();
+ this.maxPerHostMap = new ConcurrentHashMap<HttpRoute, Integer>();
+ setDefaultMaxPerRoute(defaultMax);
+ }
+
+ public ConnPerRouteBean() {
+ this(DEFAULT_MAX_CONNECTIONS_PER_ROUTE);
+ }
+
+ public int getDefaultMax() {
+ return this.defaultMax;
+ }
+
+ /**
+ * @since 4.1
+ */
+ public int getDefaultMaxPerRoute() {
+ return this.defaultMax;
+ }
+
+ public void setDefaultMaxPerRoute(final int max) {
+ Args.positive(max, "Defautl max per route");
+ this.defaultMax = max;
+ }
+
+ public void setMaxForRoute(final HttpRoute route, final int max) {
+ Args.notNull(route, "HTTP route");
+ Args.positive(max, "Max per route");
+ this.maxPerHostMap.put(route, Integer.valueOf(max));
+ }
+
+ public int getMaxForRoute(final HttpRoute route) {
+ Args.notNull(route, "HTTP route");
+ final Integer max = this.maxPerHostMap.get(route);
+ if (max != null) {
+ return max.intValue();
+ } else {
+ return this.defaultMax;
+ }
+ }
+
+ public void setMaxForRoutes(final Map<HttpRoute, Integer> map) {
+ if (map == null) {
+ return;
+ }
+ this.maxPerHostMap.clear();
+ this.maxPerHostMap.putAll(map);
+ }
+
+ @Override
+ public String toString() {
+ return this.maxPerHostMap.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnRoutePNames.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnRoutePNames.java
new file mode 100644
index 0000000000..083216b14d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnRoutePNames.java
@@ -0,0 +1,79 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn.params;
+
+/**
+ * Parameter names for connection routing.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.client.config.RequestConfig}.
+ */
+@Deprecated
+public interface ConnRoutePNames {
+
+ /**
+ * Parameter for the default proxy.
+ * The default value will be used by some
+ * {@link ch.boye.httpclientandroidlib.conn.routing.HttpRoutePlanner HttpRoutePlanner}
+ * implementations, in particular the default implementation.
+ * <p>
+ * This parameter expects a value of type {@link ch.boye.httpclientandroidlib.HttpHost}.
+ * </p>
+ */
+ public static final String DEFAULT_PROXY = "http.route.default-proxy";
+
+ /**
+ * Parameter for the local address.
+ * On machines with multiple network interfaces, this parameter
+ * can be used to select the network interface from which the
+ * connection originates.
+ * It will be interpreted by the standard
+ * {@link ch.boye.httpclientandroidlib.conn.routing.HttpRoutePlanner HttpRoutePlanner}
+ * implementations, in particular the default implementation.
+ * <p>
+ * This parameter expects a value of type {@link java.net.InetAddress}.
+ * </p>
+ */
+ public static final String LOCAL_ADDRESS = "http.route.local-address";
+
+ /**
+ * Parameter for an forced route.
+ * The forced route will be interpreted by the standard
+ * {@link ch.boye.httpclientandroidlib.conn.routing.HttpRoutePlanner HttpRoutePlanner}
+ * implementations.
+ * Instead of computing a route, the given forced route will be
+ * returned, even if it points to the wrong target host.
+ * <p>
+ * This parameter expects a value of type
+ * {@link ch.boye.httpclientandroidlib.conn.routing.HttpRoute HttpRoute}.
+ * </p>
+ */
+ public static final String FORCED_ROUTE = "http.route.forced-route";
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnRouteParamBean.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnRouteParamBean.java
new file mode 100644
index 0000000000..d3018ad0be
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnRouteParamBean.java
@@ -0,0 +1,70 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.params;
+
+import java.net.InetAddress;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.params.HttpAbstractParamBean;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+/**
+ * This is a Java Bean class that can be used to wrap an instance of
+ * {@link HttpParams} and manipulate connection routing parameters
+ * using Java Beans conventions.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.client.config.RequestConfig}.
+ */
+@Deprecated
+@NotThreadSafe
+public class ConnRouteParamBean extends HttpAbstractParamBean {
+
+ public ConnRouteParamBean (final HttpParams params) {
+ super(params);
+ }
+
+ /** @see ConnRoutePNames#DEFAULT_PROXY */
+ public void setDefaultProxy (final HttpHost defaultProxy) {
+ params.setParameter(ConnRoutePNames.DEFAULT_PROXY, defaultProxy);
+ }
+
+ /** @see ConnRoutePNames#LOCAL_ADDRESS */
+ public void setLocalAddress (final InetAddress address) {
+ params.setParameter(ConnRoutePNames.LOCAL_ADDRESS, address);
+ }
+
+ /** @see ConnRoutePNames#FORCED_ROUTE */
+ public void setForcedRoute (final HttpRoute route) {
+ params.setParameter(ConnRoutePNames.FORCED_ROUTE, route);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnRouteParams.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnRouteParams.java
new file mode 100644
index 0000000000..241953dbdd
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/ConnRouteParams.java
@@ -0,0 +1,178 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn.params;
+
+import java.net.InetAddress;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * An adaptor for manipulating HTTP routing parameters
+ * in {@link HttpParams}.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.client.config.RequestConfig}.
+ */
+@Deprecated
+@Immutable
+public class ConnRouteParams implements ConnRoutePNames {
+
+ /**
+ * A special value indicating "no host".
+ * This relies on a nonsense scheme name to avoid conflicts
+ * with actual hosts. Note that this is a <i>valid</i> host.
+ */
+ public static final HttpHost NO_HOST =
+ new HttpHost("127.0.0.255", 0, "no-host"); // Immutable
+
+ /**
+ * A special value indicating "no route".
+ * This is a route with {@link #NO_HOST} as the target.
+ */
+ public static final HttpRoute NO_ROUTE = new HttpRoute(NO_HOST); // Immutable
+
+ /** Disabled default constructor. */
+ private ConnRouteParams() {
+ // no body
+ }
+
+ /**
+ * Obtains the {@link ConnRoutePNames#DEFAULT_PROXY DEFAULT_PROXY}
+ * parameter value.
+ * {@link #NO_HOST} will be mapped to <code>null</code>,
+ * to allow unsetting in a hierarchy.
+ *
+ * @param params the parameters in which to look up
+ *
+ * @return the default proxy set in the argument parameters, or
+ * <code>null</code> if not set
+ */
+ public static HttpHost getDefaultProxy(final HttpParams params) {
+ Args.notNull(params, "Parameters");
+ HttpHost proxy = (HttpHost)
+ params.getParameter(DEFAULT_PROXY);
+ if ((proxy != null) && NO_HOST.equals(proxy)) {
+ // value is explicitly unset
+ proxy = null;
+ }
+ return proxy;
+ }
+
+ /**
+ * Sets the {@link ConnRoutePNames#DEFAULT_PROXY DEFAULT_PROXY}
+ * parameter value.
+ *
+ * @param params the parameters in which to set the value
+ * @param proxy the value to set, may be <code>null</code>.
+ * Note that {@link #NO_HOST} will be mapped to
+ * <code>null</code> by {@link #getDefaultProxy},
+ * to allow for explicit unsetting in hierarchies.
+ */
+ public static void setDefaultProxy(final HttpParams params,
+ final HttpHost proxy) {
+ Args.notNull(params, "Parameters");
+ params.setParameter(DEFAULT_PROXY, proxy);
+ }
+
+ /**
+ * Obtains the {@link ConnRoutePNames#FORCED_ROUTE FORCED_ROUTE}
+ * parameter value.
+ * {@link #NO_ROUTE} will be mapped to <code>null</code>,
+ * to allow unsetting in a hierarchy.
+ *
+ * @param params the parameters in which to look up
+ *
+ * @return the forced route set in the argument parameters, or
+ * <code>null</code> if not set
+ */
+ public static HttpRoute getForcedRoute(final HttpParams params) {
+ Args.notNull(params, "Parameters");
+ HttpRoute route = (HttpRoute)
+ params.getParameter(FORCED_ROUTE);
+ if ((route != null) && NO_ROUTE.equals(route)) {
+ // value is explicitly unset
+ route = null;
+ }
+ return route;
+ }
+
+ /**
+ * Sets the {@link ConnRoutePNames#FORCED_ROUTE FORCED_ROUTE}
+ * parameter value.
+ *
+ * @param params the parameters in which to set the value
+ * @param route the value to set, may be <code>null</code>.
+ * Note that {@link #NO_ROUTE} will be mapped to
+ * <code>null</code> by {@link #getForcedRoute},
+ * to allow for explicit unsetting in hierarchies.
+ */
+ public static void setForcedRoute(final HttpParams params,
+ final HttpRoute route) {
+ Args.notNull(params, "Parameters");
+ params.setParameter(FORCED_ROUTE, route);
+ }
+
+ /**
+ * Obtains the {@link ConnRoutePNames#LOCAL_ADDRESS LOCAL_ADDRESS}
+ * parameter value.
+ * There is no special value that would automatically be mapped to
+ * <code>null</code>. You can use the wildcard address (0.0.0.0 for IPv4,
+ * :: for IPv6) to override a specific local address in a hierarchy.
+ *
+ * @param params the parameters in which to look up
+ *
+ * @return the local address set in the argument parameters, or
+ * <code>null</code> if not set
+ */
+ public static InetAddress getLocalAddress(final HttpParams params) {
+ Args.notNull(params, "Parameters");
+ final InetAddress local = (InetAddress)
+ params.getParameter(LOCAL_ADDRESS);
+ // no explicit unsetting
+ return local;
+ }
+
+ /**
+ * Sets the {@link ConnRoutePNames#LOCAL_ADDRESS LOCAL_ADDRESS}
+ * parameter value.
+ *
+ * @param params the parameters in which to set the value
+ * @param local the value to set, may be <code>null</code>
+ */
+ public static void setLocalAddress(final HttpParams params,
+ final InetAddress local) {
+ Args.notNull(params, "Parameters");
+ params.setParameter(LOCAL_ADDRESS, local);
+ }
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/package-info.java
new file mode 100644
index 0000000000..32b359e3e9
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/params/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Deprecated.
+ * @deprecated (4.3).
+ */
+package ch.boye.httpclientandroidlib.conn.params;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/BasicRouteDirector.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/BasicRouteDirector.java
new file mode 100644
index 0000000000..9c653eb3c6
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/BasicRouteDirector.java
@@ -0,0 +1,181 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.routing;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Basic {@link HttpRouteDirector} implementation.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class BasicRouteDirector implements HttpRouteDirector {
+
+ /**
+ * Provides the next step.
+ *
+ * @param plan the planned route
+ * @param fact the currently established route, or
+ * <code>null</code> if nothing is established
+ *
+ * @return one of the constants defined in this class, indicating
+ * either the next step to perform, or success, or failure.
+ * 0 is for success, a negative value for failure.
+ */
+ public int nextStep(final RouteInfo plan, final RouteInfo fact) {
+ Args.notNull(plan, "Planned route");
+
+ int step = UNREACHABLE;
+
+ if ((fact == null) || (fact.getHopCount() < 1)) {
+ step = firstStep(plan);
+ } else if (plan.getHopCount() > 1) {
+ step = proxiedStep(plan, fact);
+ } else {
+ step = directStep(plan, fact);
+ }
+
+ return step;
+
+ } // nextStep
+
+
+ /**
+ * Determines the first step to establish a route.
+ *
+ * @param plan the planned route
+ *
+ * @return the first step
+ */
+ protected int firstStep(final RouteInfo plan) {
+
+ return (plan.getHopCount() > 1) ?
+ CONNECT_PROXY : CONNECT_TARGET;
+ }
+
+
+ /**
+ * Determines the next step to establish a direct connection.
+ *
+ * @param plan the planned route
+ * @param fact the currently established route
+ *
+ * @return one of the constants defined in this class, indicating
+ * either the next step to perform, or success, or failure
+ */
+ protected int directStep(final RouteInfo plan, final RouteInfo fact) {
+
+ if (fact.getHopCount() > 1) {
+ return UNREACHABLE;
+ }
+ if (!plan.getTargetHost().equals(fact.getTargetHost()))
+ {
+ return UNREACHABLE;
+ // If the security is too low, we could now suggest to layer
+ // a secure protocol on the direct connection. Layering on direct
+ // connections has not been supported in HttpClient 3.x, we don't
+ // consider it here until there is a real-life use case for it.
+ }
+
+ // Should we tolerate if security is better than planned?
+ // (plan.isSecure() && !fact.isSecure())
+ if (plan.isSecure() != fact.isSecure()) {
+ return UNREACHABLE;
+ }
+
+ // Local address has to match only if the plan specifies one.
+ if ((plan.getLocalAddress() != null) &&
+ !plan.getLocalAddress().equals(fact.getLocalAddress())
+ ) {
+ return UNREACHABLE;
+ }
+
+ return COMPLETE;
+ }
+
+
+ /**
+ * Determines the next step to establish a connection via proxy.
+ *
+ * @param plan the planned route
+ * @param fact the currently established route
+ *
+ * @return one of the constants defined in this class, indicating
+ * either the next step to perform, or success, or failure
+ */
+ protected int proxiedStep(final RouteInfo plan, final RouteInfo fact) {
+
+ if (fact.getHopCount() <= 1) {
+ return UNREACHABLE;
+ }
+ if (!plan.getTargetHost().equals(fact.getTargetHost())) {
+ return UNREACHABLE;
+ }
+ final int phc = plan.getHopCount();
+ final int fhc = fact.getHopCount();
+ if (phc < fhc) {
+ return UNREACHABLE;
+ }
+
+ for (int i=0; i<fhc-1; i++) {
+ if (!plan.getHopTarget(i).equals(fact.getHopTarget(i))) {
+ return UNREACHABLE;
+ }
+ }
+ // now we know that the target matches and proxies so far are the same
+ if (phc > fhc)
+ {
+ return TUNNEL_PROXY; // need to extend the proxy chain
+ }
+
+ // proxy chain and target are the same, check tunnelling and layering
+ if ((fact.isTunnelled() && !plan.isTunnelled()) ||
+ (fact.isLayered() && !plan.isLayered())) {
+ return UNREACHABLE;
+ }
+
+ if (plan.isTunnelled() && !fact.isTunnelled()) {
+ return TUNNEL_TARGET;
+ }
+ if (plan.isLayered() && !fact.isLayered()) {
+ return LAYER_PROTOCOL;
+ }
+
+ // tunnel and layering are the same, remains to check the security
+ // Should we tolerate if security is better than planned?
+ // (plan.isSecure() && !fact.isSecure())
+ if (plan.isSecure() != fact.isSecure()) {
+ return UNREACHABLE;
+ }
+
+ return COMPLETE;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/HttpRoute.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/HttpRoute.java
new file mode 100644
index 0000000000..7bf02d14c6
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/HttpRoute.java
@@ -0,0 +1,328 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.routing;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.LangUtils;
+
+/**
+ * The route for a request.
+ *
+ * @since 4.0
+ */
+@Immutable
+public final class HttpRoute implements RouteInfo, Cloneable {
+
+ /** The target host to connect to. */
+ private final HttpHost targetHost;
+
+ /**
+ * The local address to connect from.
+ * <code>null</code> indicates that the default should be used.
+ */
+ private final InetAddress localAddress;
+
+ /** The proxy servers, if any. Never null. */
+ private final List<HttpHost> proxyChain;
+
+ /** Whether the the route is tunnelled through the proxy. */
+ private final TunnelType tunnelled;
+
+ /** Whether the route is layered. */
+ private final LayerType layered;
+
+ /** Whether the route is (supposed to be) secure. */
+ private final boolean secure;
+
+ private HttpRoute(final HttpHost target, final InetAddress local, final List<HttpHost> proxies,
+ final boolean secure, final TunnelType tunnelled, final LayerType layered) {
+ Args.notNull(target, "Target host");
+ this.targetHost = target;
+ this.localAddress = local;
+ if (proxies != null && !proxies.isEmpty()) {
+ this.proxyChain = new ArrayList<HttpHost>(proxies);
+ } else {
+ this.proxyChain = null;
+ }
+ if (tunnelled == TunnelType.TUNNELLED) {
+ Args.check(this.proxyChain != null, "Proxy required if tunnelled");
+ }
+ this.secure = secure;
+ this.tunnelled = tunnelled != null ? tunnelled : TunnelType.PLAIN;
+ this.layered = layered != null ? layered : LayerType.PLAIN;
+ }
+
+ /**
+ * Creates a new route with all attributes specified explicitly.
+ *
+ * @param target the host to which to route
+ * @param local the local address to route from, or
+ * <code>null</code> for the default
+ * @param proxies the proxy chain to use, or
+ * <code>null</code> for a direct route
+ * @param secure <code>true</code> if the route is (to be) secure,
+ * <code>false</code> otherwise
+ * @param tunnelled the tunnel type of this route
+ * @param layered the layering type of this route
+ */
+ public HttpRoute(final HttpHost target, final InetAddress local, final HttpHost[] proxies,
+ final boolean secure, final TunnelType tunnelled, final LayerType layered) {
+ this(target, local, proxies != null ? Arrays.asList(proxies) : null,
+ secure, tunnelled, layered);
+ }
+
+ /**
+ * Creates a new route with at most one proxy.
+ *
+ * @param target the host to which to route
+ * @param local the local address to route from, or
+ * <code>null</code> for the default
+ * @param proxy the proxy to use, or
+ * <code>null</code> for a direct route
+ * @param secure <code>true</code> if the route is (to be) secure,
+ * <code>false</code> otherwise
+ * @param tunnelled <code>true</code> if the route is (to be) tunnelled
+ * via the proxy,
+ * <code>false</code> otherwise
+ * @param layered <code>true</code> if the route includes a
+ * layered protocol,
+ * <code>false</code> otherwise
+ */
+ public HttpRoute(final HttpHost target, final InetAddress local, final HttpHost proxy,
+ final boolean secure, final TunnelType tunnelled, final LayerType layered) {
+ this(target, local, proxy != null ? Collections.singletonList(proxy) : null,
+ secure, tunnelled, layered);
+ }
+
+ /**
+ * Creates a new direct route.
+ * That is a route without a proxy.
+ *
+ * @param target the host to which to route
+ * @param local the local address to route from, or
+ * <code>null</code> for the default
+ * @param secure <code>true</code> if the route is (to be) secure,
+ * <code>false</code> otherwise
+ */
+ public HttpRoute(final HttpHost target, final InetAddress local, final boolean secure) {
+ this(target, local, Collections.<HttpHost>emptyList(), secure,
+ TunnelType.PLAIN, LayerType.PLAIN);
+ }
+
+ /**
+ * Creates a new direct insecure route.
+ *
+ * @param target the host to which to route
+ */
+ public HttpRoute(final HttpHost target) {
+ this(target, null, Collections.<HttpHost>emptyList(), false,
+ TunnelType.PLAIN, LayerType.PLAIN);
+ }
+
+ /**
+ * Creates a new route through a proxy.
+ * When using this constructor, the <code>proxy</code> MUST be given.
+ * For convenience, it is assumed that a secure connection will be
+ * layered over a tunnel through the proxy.
+ *
+ * @param target the host to which to route
+ * @param local the local address to route from, or
+ * <code>null</code> for the default
+ * @param proxy the proxy to use
+ * @param secure <code>true</code> if the route is (to be) secure,
+ * <code>false</code> otherwise
+ */
+ public HttpRoute(final HttpHost target, final InetAddress local, final HttpHost proxy,
+ final boolean secure) {
+ this(target, local, Collections.singletonList(Args.notNull(proxy, "Proxy host")), secure,
+ secure ? TunnelType.TUNNELLED : TunnelType.PLAIN,
+ secure ? LayerType.LAYERED : LayerType.PLAIN);
+ }
+
+ /**
+ * Creates a new plain route through a proxy.
+ *
+ * @param target the host to which to route
+ * @param proxy the proxy to use
+ *
+ * @since 4.3
+ */
+ public HttpRoute(final HttpHost target, final HttpHost proxy) {
+ this(target, null, proxy, false);
+ }
+
+ public final HttpHost getTargetHost() {
+ return this.targetHost;
+ }
+
+ public final InetAddress getLocalAddress() {
+ return this.localAddress;
+ }
+
+ public final InetSocketAddress getLocalSocketAddress() {
+ return this.localAddress != null ? new InetSocketAddress(this.localAddress, 0) : null;
+ }
+
+ public final int getHopCount() {
+ return proxyChain != null ? proxyChain.size() + 1 : 1;
+ }
+
+ public final HttpHost getHopTarget(final int hop) {
+ Args.notNegative(hop, "Hop index");
+ final int hopcount = getHopCount();
+ Args.check(hop < hopcount, "Hop index exceeds tracked route length");
+ if (hop < hopcount - 1) {
+ return this.proxyChain.get(hop);
+ } else {
+ return this.targetHost;
+ }
+ }
+
+ public final HttpHost getProxyHost() {
+ return proxyChain != null && !this.proxyChain.isEmpty() ? this.proxyChain.get(0) : null;
+ }
+
+ public final TunnelType getTunnelType() {
+ return this.tunnelled;
+ }
+
+ public final boolean isTunnelled() {
+ return (this.tunnelled == TunnelType.TUNNELLED);
+ }
+
+ public final LayerType getLayerType() {
+ return this.layered;
+ }
+
+ public final boolean isLayered() {
+ return (this.layered == LayerType.LAYERED);
+ }
+
+ public final boolean isSecure() {
+ return this.secure;
+ }
+
+ /**
+ * Compares this route to another.
+ *
+ * @param obj the object to compare with
+ *
+ * @return <code>true</code> if the argument is the same route,
+ * <code>false</code>
+ */
+ @Override
+ public final boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof HttpRoute) {
+ final HttpRoute that = (HttpRoute) obj;
+ return
+ // Do the cheapest tests first
+ (this.secure == that.secure) &&
+ (this.tunnelled == that.tunnelled) &&
+ (this.layered == that.layered) &&
+ LangUtils.equals(this.targetHost, that.targetHost) &&
+ LangUtils.equals(this.localAddress, that.localAddress) &&
+ LangUtils.equals(this.proxyChain, that.proxyChain);
+ } else {
+ return false;
+ }
+ }
+
+
+ /**
+ * Generates a hash code for this route.
+ *
+ * @return the hash code
+ */
+ @Override
+ public final int hashCode() {
+ int hash = LangUtils.HASH_SEED;
+ hash = LangUtils.hashCode(hash, this.targetHost);
+ hash = LangUtils.hashCode(hash, this.localAddress);
+ if (this.proxyChain != null) {
+ for (final HttpHost element : this.proxyChain) {
+ hash = LangUtils.hashCode(hash, element);
+ }
+ }
+ hash = LangUtils.hashCode(hash, this.secure);
+ hash = LangUtils.hashCode(hash, this.tunnelled);
+ hash = LangUtils.hashCode(hash, this.layered);
+ return hash;
+ }
+
+ /**
+ * Obtains a description of this route.
+ *
+ * @return a human-readable representation of this route
+ */
+ @Override
+ public final String toString() {
+ final StringBuilder cab = new StringBuilder(50 + getHopCount()*30);
+ if (this.localAddress != null) {
+ cab.append(this.localAddress);
+ cab.append("->");
+ }
+ cab.append('{');
+ if (this.tunnelled == TunnelType.TUNNELLED) {
+ cab.append('t');
+ }
+ if (this.layered == LayerType.LAYERED) {
+ cab.append('l');
+ }
+ if (this.secure) {
+ cab.append('s');
+ }
+ cab.append("}->");
+ if (this.proxyChain != null) {
+ for (final HttpHost aProxyChain : this.proxyChain) {
+ cab.append(aProxyChain);
+ cab.append("->");
+ }
+ }
+ cab.append(this.targetHost);
+ return cab.toString();
+ }
+
+ // default implementation of clone() is sufficient
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/HttpRouteDirector.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/HttpRouteDirector.java
new file mode 100644
index 0000000000..f8b7746034
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/HttpRouteDirector.java
@@ -0,0 +1,74 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.routing;
+
+/**
+ * Provides directions on establishing a route.
+ * Implementations of this interface compare a planned route with
+ * a tracked route and indicate the next step required.
+ *
+ * @since 4.0
+ */
+public interface HttpRouteDirector {
+
+ /** Indicates that the route can not be established at all. */
+ public final static int UNREACHABLE = -1;
+
+ /** Indicates that the route is complete. */
+ public final static int COMPLETE = 0;
+
+ /** Step: open connection to target. */
+ public final static int CONNECT_TARGET = 1;
+
+ /** Step: open connection to proxy. */
+ public final static int CONNECT_PROXY = 2;
+
+ /** Step: tunnel through proxy to target. */
+ public final static int TUNNEL_TARGET = 3;
+
+ /** Step: tunnel through proxy to other proxy. */
+ public final static int TUNNEL_PROXY = 4;
+
+ /** Step: layer protocol (over tunnel). */
+ public final static int LAYER_PROTOCOL = 5;
+
+
+ /**
+ * Provides the next step.
+ *
+ * @param plan the planned route
+ * @param fact the currently established route, or
+ * <code>null</code> if nothing is established
+ *
+ * @return one of the constants defined in this interface, indicating
+ * either the next step to perform, or success, or failure.
+ * 0 is for success, a negative value for failure.
+ */
+ public int nextStep(RouteInfo plan, RouteInfo fact);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/HttpRoutePlanner.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/HttpRoutePlanner.java
new file mode 100644
index 0000000000..9e8ba19d32
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/HttpRoutePlanner.java
@@ -0,0 +1,67 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.routing;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * Encapsulates logic to compute a {@link HttpRoute} to a target host.
+ * Implementations may for example be based on parameters, or on the
+ * standard Java system properties.
+ * <p/>
+ * Implementations of this interface must be thread-safe. Access to shared
+ * data must be synchronized as methods of this interface may be executed
+ * from multiple threads.
+ *
+ * @since 4.0
+ */
+public interface HttpRoutePlanner {
+
+ /**
+ * Determines the route for a request.
+ *
+ * @param target the target host for the request.
+ * Implementations may accept <code>null</code>
+ * if they can still determine a route, for example
+ * to a default target or by inspecting the request.
+ * @param request the request to execute
+ * @param context the context to use for the subsequent execution.
+ * Implementations may accept <code>null</code>.
+ *
+ * @return the route that the request should take
+ *
+ * @throws HttpException in case of a problem
+ */
+ public HttpRoute determineRoute(HttpHost target,
+ HttpRequest request,
+ HttpContext context) throws HttpException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/RouteInfo.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/RouteInfo.java
new file mode 100644
index 0000000000..0784b78942
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/RouteInfo.java
@@ -0,0 +1,161 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.routing;
+
+import java.net.InetAddress;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+
+/**
+ * Read-only interface for route information.
+ *
+ * @since 4.0
+ */
+public interface RouteInfo {
+
+ /**
+ * The tunnelling type of a route.
+ * Plain routes are established by connecting to the target or
+ * the first proxy.
+ * Tunnelled routes are established by connecting to the first proxy
+ * and tunnelling through all proxies to the target.
+ * Routes without a proxy cannot be tunnelled.
+ */
+ public enum TunnelType { PLAIN, TUNNELLED }
+
+ /**
+ * The layering type of a route.
+ * Plain routes are established by connecting or tunnelling.
+ * Layered routes are established by layering a protocol such as TLS/SSL
+ * over an existing connection.
+ * Protocols can only be layered over a tunnel to the target, or
+ * or over a direct connection without proxies.
+ * <br/>
+ * Layering a protocol
+ * over a direct connection makes little sense, since the connection
+ * could be established with the new protocol in the first place.
+ * But we don't want to exclude that use case.
+ */
+ public enum LayerType { PLAIN, LAYERED }
+
+ /**
+ * Obtains the target host.
+ *
+ * @return the target host
+ */
+ HttpHost getTargetHost();
+
+ /**
+ * Obtains the local address to connect from.
+ *
+ * @return the local address,
+ * or <code>null</code>
+ */
+ InetAddress getLocalAddress();
+
+ /**
+ * Obtains the number of hops in this route.
+ * A direct route has one hop. A route through a proxy has two hops.
+ * A route through a chain of <i>n</i> proxies has <i>n+1</i> hops.
+ *
+ * @return the number of hops in this route
+ */
+ int getHopCount();
+
+ /**
+ * Obtains the target of a hop in this route.
+ * The target of the last hop is the {@link #getTargetHost target host},
+ * the target of previous hops is the respective proxy in the chain.
+ * For a route through exactly one proxy, target of hop 0 is the proxy
+ * and target of hop 1 is the target host.
+ *
+ * @param hop index of the hop for which to get the target,
+ * 0 for first
+ *
+ * @return the target of the given hop
+ *
+ * @throws IllegalArgumentException
+ * if the argument is negative or not less than
+ * {@link #getHopCount getHopCount()}
+ */
+ HttpHost getHopTarget(int hop);
+
+ /**
+ * Obtains the first proxy host.
+ *
+ * @return the first proxy in the proxy chain, or
+ * <code>null</code> if this route is direct
+ */
+ HttpHost getProxyHost();
+
+ /**
+ * Obtains the tunnel type of this route.
+ * If there is a proxy chain, only end-to-end tunnels are considered.
+ *
+ * @return the tunnelling type
+ */
+ TunnelType getTunnelType();
+
+ /**
+ * Checks whether this route is tunnelled through a proxy.
+ * If there is a proxy chain, only end-to-end tunnels are considered.
+ *
+ * @return <code>true</code> if tunnelled end-to-end through at least
+ * one proxy,
+ * <code>false</code> otherwise
+ */
+ boolean isTunnelled();
+
+ /**
+ * Obtains the layering type of this route.
+ * In the presence of proxies, only layering over an end-to-end tunnel
+ * is considered.
+ *
+ * @return the layering type
+ */
+ LayerType getLayerType();
+
+ /**
+ * Checks whether this route includes a layered protocol.
+ * In the presence of proxies, only layering over an end-to-end tunnel
+ * is considered.
+ *
+ * @return <code>true</code> if layered,
+ * <code>false</code> otherwise
+ */
+ boolean isLayered();
+
+ /**
+ * Checks whether this route is secure.
+ *
+ * @return <code>true</code> if secure,
+ * <code>false</code> otherwise
+ */
+ boolean isSecure();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/RouteTracker.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/RouteTracker.java
new file mode 100644
index 0000000000..9a900319c7
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/RouteTracker.java
@@ -0,0 +1,366 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.routing;
+
+import java.net.InetAddress;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+import ch.boye.httpclientandroidlib.util.LangUtils;
+
+/**
+ * Helps tracking the steps in establishing a route.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public final class RouteTracker implements RouteInfo, Cloneable {
+
+ /** The target host to connect to. */
+ private final HttpHost targetHost;
+
+ /**
+ * The local address to connect from.
+ * <code>null</code> indicates that the default should be used.
+ */
+ private final InetAddress localAddress;
+
+ // the attributes above are fixed at construction time
+ // now follow attributes that indicate the established route
+
+ /** Whether the first hop of the route is established. */
+ private boolean connected;
+
+ /** The proxy chain, if any. */
+ private HttpHost[] proxyChain;
+
+ /** Whether the the route is tunnelled end-to-end through proxies. */
+ private TunnelType tunnelled;
+
+ /** Whether the route is layered over a tunnel. */
+ private LayerType layered;
+
+ /** Whether the route is secure. */
+ private boolean secure;
+
+ /**
+ * Creates a new route tracker.
+ * The target and origin need to be specified at creation time.
+ *
+ * @param target the host to which to route
+ * @param local the local address to route from, or
+ * <code>null</code> for the default
+ */
+ public RouteTracker(final HttpHost target, final InetAddress local) {
+ Args.notNull(target, "Target host");
+ this.targetHost = target;
+ this.localAddress = local;
+ this.tunnelled = TunnelType.PLAIN;
+ this.layered = LayerType.PLAIN;
+ }
+
+ /**
+ * @since 4.2
+ */
+ public void reset() {
+ this.connected = false;
+ this.proxyChain = null;
+ this.tunnelled = TunnelType.PLAIN;
+ this.layered = LayerType.PLAIN;
+ this.secure = false;
+ }
+
+ /**
+ * Creates a new tracker for the given route.
+ * Only target and origin are taken from the route,
+ * everything else remains to be tracked.
+ *
+ * @param route the route to track
+ */
+ public RouteTracker(final HttpRoute route) {
+ this(route.getTargetHost(), route.getLocalAddress());
+ }
+
+ /**
+ * Tracks connecting to the target.
+ *
+ * @param secure <code>true</code> if the route is secure,
+ * <code>false</code> otherwise
+ */
+ public final void connectTarget(final boolean secure) {
+ Asserts.check(!this.connected, "Already connected");
+ this.connected = true;
+ this.secure = secure;
+ }
+
+ /**
+ * Tracks connecting to the first proxy.
+ *
+ * @param proxy the proxy connected to
+ * @param secure <code>true</code> if the route is secure,
+ * <code>false</code> otherwise
+ */
+ public final void connectProxy(final HttpHost proxy, final boolean secure) {
+ Args.notNull(proxy, "Proxy host");
+ Asserts.check(!this.connected, "Already connected");
+ this.connected = true;
+ this.proxyChain = new HttpHost[]{ proxy };
+ this.secure = secure;
+ }
+
+ /**
+ * Tracks tunnelling to the target.
+ *
+ * @param secure <code>true</code> if the route is secure,
+ * <code>false</code> otherwise
+ */
+ public final void tunnelTarget(final boolean secure) {
+ Asserts.check(this.connected, "No tunnel unless connected");
+ Asserts.notNull(this.proxyChain, "No tunnel without proxy");
+ this.tunnelled = TunnelType.TUNNELLED;
+ this.secure = secure;
+ }
+
+ /**
+ * Tracks tunnelling to a proxy in a proxy chain.
+ * This will extend the tracked proxy chain, but it does not mark
+ * the route as tunnelled. Only end-to-end tunnels are considered there.
+ *
+ * @param proxy the proxy tunnelled to
+ * @param secure <code>true</code> if the route is secure,
+ * <code>false</code> otherwise
+ */
+ public final void tunnelProxy(final HttpHost proxy, final boolean secure) {
+ Args.notNull(proxy, "Proxy host");
+ Asserts.check(this.connected, "No tunnel unless connected");
+ Asserts.notNull(this.proxyChain, "No tunnel without proxy");
+ // prepare an extended proxy chain
+ final HttpHost[] proxies = new HttpHost[this.proxyChain.length+1];
+ System.arraycopy(this.proxyChain, 0,
+ proxies, 0, this.proxyChain.length);
+ proxies[proxies.length-1] = proxy;
+
+ this.proxyChain = proxies;
+ this.secure = secure;
+ }
+
+ /**
+ * Tracks layering a protocol.
+ *
+ * @param secure <code>true</code> if the route is secure,
+ * <code>false</code> otherwise
+ */
+ public final void layerProtocol(final boolean secure) {
+ // it is possible to layer a protocol over a direct connection,
+ // although this case is probably not considered elsewhere
+ Asserts.check(this.connected, "No layered protocol unless connected");
+ this.layered = LayerType.LAYERED;
+ this.secure = secure;
+ }
+
+ public final HttpHost getTargetHost() {
+ return this.targetHost;
+ }
+
+ public final InetAddress getLocalAddress() {
+ return this.localAddress;
+ }
+
+ public final int getHopCount() {
+ int hops = 0;
+ if (this.connected) {
+ if (proxyChain == null) {
+ hops = 1;
+ } else {
+ hops = proxyChain.length + 1;
+ }
+ }
+ return hops;
+ }
+
+ public final HttpHost getHopTarget(final int hop) {
+ Args.notNegative(hop, "Hop index");
+ final int hopcount = getHopCount();
+ Args.check(hop < hopcount, "Hop index exceeds tracked route length");
+ HttpHost result = null;
+ if (hop < hopcount-1) {
+ result = this.proxyChain[hop];
+ } else {
+ result = this.targetHost;
+ }
+
+ return result;
+ }
+
+ public final HttpHost getProxyHost() {
+ return (this.proxyChain == null) ? null : this.proxyChain[0];
+ }
+
+ public final boolean isConnected() {
+ return this.connected;
+ }
+
+ public final TunnelType getTunnelType() {
+ return this.tunnelled;
+ }
+
+ public final boolean isTunnelled() {
+ return (this.tunnelled == TunnelType.TUNNELLED);
+ }
+
+ public final LayerType getLayerType() {
+ return this.layered;
+ }
+
+ public final boolean isLayered() {
+ return (this.layered == LayerType.LAYERED);
+ }
+
+ public final boolean isSecure() {
+ return this.secure;
+ }
+
+ /**
+ * Obtains the tracked route.
+ * If a route has been tracked, it is {@link #isConnected connected}.
+ * If not connected, nothing has been tracked so far.
+ *
+ * @return the tracked route, or
+ * <code>null</code> if nothing has been tracked so far
+ */
+ public final HttpRoute toRoute() {
+ return !this.connected ?
+ null : new HttpRoute(this.targetHost, this.localAddress,
+ this.proxyChain, this.secure,
+ this.tunnelled, this.layered);
+ }
+
+ /**
+ * Compares this tracked route to another.
+ *
+ * @param o the object to compare with
+ *
+ * @return <code>true</code> if the argument is the same tracked route,
+ * <code>false</code>
+ */
+ @Override
+ public final boolean equals(final Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof RouteTracker)) {
+ return false;
+ }
+
+ final RouteTracker that = (RouteTracker) o;
+ return
+ // Do the cheapest checks first
+ (this.connected == that.connected) &&
+ (this.secure == that.secure) &&
+ (this.tunnelled == that.tunnelled) &&
+ (this.layered == that.layered) &&
+ LangUtils.equals(this.targetHost, that.targetHost) &&
+ LangUtils.equals(this.localAddress, that.localAddress) &&
+ LangUtils.equals(this.proxyChain, that.proxyChain);
+ }
+
+ /**
+ * Generates a hash code for this tracked route.
+ * Route trackers are modifiable and should therefore not be used
+ * as lookup keys. Use {@link #toRoute toRoute} to obtain an
+ * unmodifiable representation of the tracked route.
+ *
+ * @return the hash code
+ */
+ @Override
+ public final int hashCode() {
+ int hash = LangUtils.HASH_SEED;
+ hash = LangUtils.hashCode(hash, this.targetHost);
+ hash = LangUtils.hashCode(hash, this.localAddress);
+ if (this.proxyChain != null) {
+ for (final HttpHost element : this.proxyChain) {
+ hash = LangUtils.hashCode(hash, element);
+ }
+ }
+ hash = LangUtils.hashCode(hash, this.connected);
+ hash = LangUtils.hashCode(hash, this.secure);
+ hash = LangUtils.hashCode(hash, this.tunnelled);
+ hash = LangUtils.hashCode(hash, this.layered);
+ return hash;
+ }
+
+ /**
+ * Obtains a description of the tracked route.
+ *
+ * @return a human-readable representation of the tracked route
+ */
+ @Override
+ public final String toString() {
+ final StringBuilder cab = new StringBuilder(50 + getHopCount()*30);
+
+ cab.append("RouteTracker[");
+ if (this.localAddress != null) {
+ cab.append(this.localAddress);
+ cab.append("->");
+ }
+ cab.append('{');
+ if (this.connected) {
+ cab.append('c');
+ }
+ if (this.tunnelled == TunnelType.TUNNELLED) {
+ cab.append('t');
+ }
+ if (this.layered == LayerType.LAYERED) {
+ cab.append('l');
+ }
+ if (this.secure) {
+ cab.append('s');
+ }
+ cab.append("}->");
+ if (this.proxyChain != null) {
+ for (final HttpHost element : this.proxyChain) {
+ cab.append(element);
+ cab.append("->");
+ }
+ }
+ cab.append(this.targetHost);
+ cab.append(']');
+
+ return cab.toString();
+ }
+
+
+ // default implementation of clone() is sufficient
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/package-info.java
new file mode 100644
index 0000000000..ddd6b8ae89
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/routing/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Client connection routing APIs.
+ */
+package ch.boye.httpclientandroidlib.conn.routing;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/HostNameResolver.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/HostNameResolver.java
new file mode 100644
index 0000000000..8dcca69024
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/HostNameResolver.java
@@ -0,0 +1,52 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.scheme;
+
+import java.io.IOException;
+import java.net.InetAddress;
+
+/**
+ * Hostname to IP address resolver.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.1) Do not use
+ */
+@Deprecated
+public interface HostNameResolver {
+
+ /**
+ * Resolves given hostname to its IP address
+ *
+ * @param hostname the hostname.
+ * @return IP address.
+ * @throws IOException
+ */
+ InetAddress resolve (String hostname) throws IOException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/LayeredSchemeSocketFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/LayeredSchemeSocketFactory.java
new file mode 100644
index 0000000000..3833b2360d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/LayeredSchemeSocketFactory.java
@@ -0,0 +1,68 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.scheme;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+/**
+ * Extended {@link SchemeSocketFactory} interface for layered sockets such as SSL/TLS.
+ *
+ * @since 4.1
+ *
+ * @deprecated (4.2) use {@link SchemeLayeredSocketFactory}
+ */
+@Deprecated
+public interface LayeredSchemeSocketFactory extends SchemeSocketFactory {
+
+ /**
+ * Returns a socket connected to the given host that is layered over an
+ * existing socket. Used primarily for creating secure sockets through
+ * proxies.
+ *
+ * @param socket the existing socket
+ * @param target the name of the target host.
+ * @param port the port to connect to on the target host
+ * @param autoClose a flag for closing the underling socket when the created
+ * socket is closed
+ *
+ * @return Socket a new socket
+ *
+ * @throws IOException if an I/O error occurs while creating the socket
+ * @throws UnknownHostException if the IP address of the host cannot be
+ * determined
+ */
+ Socket createLayeredSocket(
+ Socket socket,
+ String target,
+ int port,
+ boolean autoClose
+ ) throws IOException, UnknownHostException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/LayeredSocketFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/LayeredSocketFactory.java
new file mode 100644
index 0000000000..318e35f9c8
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/LayeredSocketFactory.java
@@ -0,0 +1,69 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.scheme;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+/**
+ * A {@link SocketFactory SocketFactory} for layered sockets (SSL/TLS).
+ * See there for things to consider when implementing a socket factory.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.1) use {@link SchemeSocketFactory}
+ */
+@Deprecated
+public interface LayeredSocketFactory extends SocketFactory {
+
+ /**
+ * Returns a socket connected to the given host that is layered over an
+ * existing socket. Used primarily for creating secure sockets through
+ * proxies.
+ *
+ * @param socket the existing socket
+ * @param host the host name/IP
+ * @param port the port on the host
+ * @param autoClose a flag for closing the underling socket when the created
+ * socket is closed
+ *
+ * @return Socket a new socket
+ *
+ * @throws IOException if an I/O error occurs while creating the socket
+ * @throws UnknownHostException if the IP address of the host cannot be
+ * determined
+ */
+ Socket createSocket(
+ Socket socket,
+ String host,
+ int port,
+ boolean autoClose
+ ) throws IOException, UnknownHostException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/LayeredSocketFactoryAdaptor.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/LayeredSocketFactoryAdaptor.java
new file mode 100644
index 0000000000..11ebd10cb9
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/LayeredSocketFactoryAdaptor.java
@@ -0,0 +1,53 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.scheme;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+/**
+ * @deprecated (4.1) do not use
+ */
+@Deprecated
+class LayeredSocketFactoryAdaptor extends SocketFactoryAdaptor implements LayeredSocketFactory {
+
+ private final LayeredSchemeSocketFactory factory;
+
+ LayeredSocketFactoryAdaptor(final LayeredSchemeSocketFactory factory) {
+ super(factory);
+ this.factory = factory;
+ }
+
+ public Socket createSocket(
+ final Socket socket,
+ final String host, final int port, final boolean autoClose) throws IOException, UnknownHostException {
+ return this.factory.createLayeredSocket(socket, host, port, autoClose);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/PlainSocketFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/PlainSocketFactory.java
new file mode 100644
index 0000000000..efa8f281bc
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/PlainSocketFactory.java
@@ -0,0 +1,160 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.scheme;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.conn.ConnectTimeoutException;
+import ch.boye.httpclientandroidlib.params.HttpConnectionParams;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * The default class for creating plain (unencrypted) sockets.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.conn.socket.PlainConnectionSocketFactory}
+ */
+@Immutable
+@Deprecated
+public class PlainSocketFactory implements SocketFactory, SchemeSocketFactory {
+
+ private final HostNameResolver nameResolver;
+
+ /**
+ * Gets the default factory.
+ *
+ * @return the default factory
+ */
+ public static PlainSocketFactory getSocketFactory() {
+ return new PlainSocketFactory();
+ }
+
+ /**
+ * @deprecated (4.1) use {@link ch.boye.httpclientandroidlib.conn.DnsResolver}
+ */
+ @Deprecated
+ public PlainSocketFactory(final HostNameResolver nameResolver) {
+ super();
+ this.nameResolver = nameResolver;
+ }
+
+ public PlainSocketFactory() {
+ super();
+ this.nameResolver = null;
+ }
+
+ /**
+ * @param params Optional parameters. Parameters passed to this method will have no effect.
+ * This method will create a unconnected instance of {@link Socket} class
+ * using default constructor.
+ *
+ * @since 4.1
+ */
+ public Socket createSocket(final HttpParams params) {
+ return new Socket();
+ }
+
+ public Socket createSocket() {
+ return new Socket();
+ }
+
+ /**
+ * @since 4.1
+ */
+ public Socket connectSocket(
+ final Socket socket,
+ final InetSocketAddress remoteAddress,
+ final InetSocketAddress localAddress,
+ final HttpParams params) throws IOException, ConnectTimeoutException {
+ Args.notNull(remoteAddress, "Remote address");
+ Args.notNull(params, "HTTP parameters");
+ Socket sock = socket;
+ if (sock == null) {
+ sock = createSocket();
+ }
+ if (localAddress != null) {
+ sock.setReuseAddress(HttpConnectionParams.getSoReuseaddr(params));
+ sock.bind(localAddress);
+ }
+ final int connTimeout = HttpConnectionParams.getConnectionTimeout(params);
+ final int soTimeout = HttpConnectionParams.getSoTimeout(params);
+
+ try {
+ sock.setSoTimeout(soTimeout);
+ sock.connect(remoteAddress, connTimeout);
+ } catch (final SocketTimeoutException ex) {
+ throw new ConnectTimeoutException("Connect to " + remoteAddress + " timed out");
+ }
+ return sock;
+ }
+
+ /**
+ * Checks whether a socket connection is secure.
+ * This factory creates plain socket connections
+ * which are not considered secure.
+ *
+ * @param sock the connected socket
+ *
+ * @return <code>false</code>
+ */
+ public final boolean isSecure(final Socket sock) {
+ return false;
+ }
+
+ /**
+ * @deprecated (4.1) Use {@link #connectSocket(Socket, InetSocketAddress, InetSocketAddress, HttpParams)}
+ */
+ @Deprecated
+ public Socket connectSocket(
+ final Socket socket,
+ final String host, final int port,
+ final InetAddress localAddress, final int localPort,
+ final HttpParams params) throws IOException, UnknownHostException, ConnectTimeoutException {
+ InetSocketAddress local = null;
+ if (localAddress != null || localPort > 0) {
+ local = new InetSocketAddress(localAddress, localPort > 0 ? localPort : 0);
+ }
+ final InetAddress remoteAddress;
+ if (this.nameResolver != null) {
+ remoteAddress = this.nameResolver.resolve(host);
+ } else {
+ remoteAddress = InetAddress.getByName(host);
+ }
+ final InetSocketAddress remote = new InetSocketAddress(remoteAddress, port);
+ return connectSocket(socket, remote, local, params);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/Scheme.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/Scheme.java
new file mode 100644
index 0000000000..93a44de35e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/Scheme.java
@@ -0,0 +1,260 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn.scheme;
+
+import java.util.Locale;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.LangUtils;
+
+/**
+ * Encapsulates specifics of a protocol scheme such as "http" or "https". Schemes are identified
+ * by lowercase names. Supported schemes are typically collected in a {@link SchemeRegistry
+ * SchemeRegistry}.
+ * <p/>
+ * For example, to configure support for "https://" URLs, you could write code like the following:
+ * <pre>
+ * Scheme https = new Scheme("https", 443, new MySecureSocketFactory());
+ * SchemeRegistry registry = new SchemeRegistry();
+ * registry.register(https);
+ * </pre>
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.conn.SchemePortResolver} for default port
+ * resolution and {@link ch.boye.httpclientandroidlib.config.Registry} for socket factory lookups.
+ */
+@Immutable
+@Deprecated
+public final class Scheme {
+
+ /** The name of this scheme, in lowercase. (e.g. http, https) */
+ private final String name;
+
+ /** The socket factory for this scheme */
+ private final SchemeSocketFactory socketFactory;
+
+ /** The default port for this scheme */
+ private final int defaultPort;
+
+ /** Indicates whether this scheme allows for layered connections */
+ private final boolean layered;
+
+ /** A string representation, for {@link #toString toString}. */
+ private String stringRep;
+ /*
+ * This is used to cache the result of the toString() method
+ * Since the method always generates the same value, there's no
+ * need to synchronize, and it does not affect immutability.
+ */
+
+ /**
+ * Creates a new scheme.
+ * Whether the created scheme allows for layered connections
+ * depends on the class of <code>factory</code>.
+ *
+ * @param name the scheme name, for example "http".
+ * The name will be converted to lowercase.
+ * @param port the default port for this scheme
+ * @param factory the factory for creating sockets for communication
+ * with this scheme
+ *
+ * @since 4.1
+ */
+ public Scheme(final String name, final int port, final SchemeSocketFactory factory) {
+ Args.notNull(name, "Scheme name");
+ Args.check(port > 0 && port <= 0xffff, "Port is invalid");
+ Args.notNull(factory, "Socket factory");
+ this.name = name.toLowerCase(Locale.ENGLISH);
+ this.defaultPort = port;
+ if (factory instanceof SchemeLayeredSocketFactory) {
+ this.layered = true;
+ this.socketFactory = factory;
+ } else if (factory instanceof LayeredSchemeSocketFactory) {
+ this.layered = true;
+ this.socketFactory = new SchemeLayeredSocketFactoryAdaptor2((LayeredSchemeSocketFactory) factory);
+ } else {
+ this.layered = false;
+ this.socketFactory = factory;
+ }
+ }
+
+ /**
+ * Creates a new scheme.
+ * Whether the created scheme allows for layered connections
+ * depends on the class of <code>factory</code>.
+ *
+ * @param name the scheme name, for example "http".
+ * The name will be converted to lowercase.
+ * @param factory the factory for creating sockets for communication
+ * with this scheme
+ * @param port the default port for this scheme
+ *
+ * @deprecated (4.1) Use {@link #Scheme(String, int, SchemeSocketFactory)}
+ */
+ @Deprecated
+ public Scheme(final String name,
+ final SocketFactory factory,
+ final int port) {
+
+ Args.notNull(name, "Scheme name");
+ Args.notNull(factory, "Socket factory");
+ Args.check(port > 0 && port <= 0xffff, "Port is invalid");
+
+ this.name = name.toLowerCase(Locale.ENGLISH);
+ if (factory instanceof LayeredSocketFactory) {
+ this.socketFactory = new SchemeLayeredSocketFactoryAdaptor(
+ (LayeredSocketFactory) factory);
+ this.layered = true;
+ } else {
+ this.socketFactory = new SchemeSocketFactoryAdaptor(factory);
+ this.layered = false;
+ }
+ this.defaultPort = port;
+ }
+
+ /**
+ * Obtains the default port.
+ *
+ * @return the default port for this scheme
+ */
+ public final int getDefaultPort() {
+ return defaultPort;
+ }
+
+
+ /**
+ * Obtains the socket factory.
+ * If this scheme is {@link #isLayered layered}, the factory implements
+ * {@link LayeredSocketFactory LayeredSocketFactory}.
+ *
+ * @return the socket factory for this scheme
+ *
+ * @deprecated (4.1) Use {@link #getSchemeSocketFactory()}
+ */
+ @Deprecated
+ public final SocketFactory getSocketFactory() {
+ if (this.socketFactory instanceof SchemeSocketFactoryAdaptor) {
+ return ((SchemeSocketFactoryAdaptor) this.socketFactory).getFactory();
+ } else {
+ if (this.layered) {
+ return new LayeredSocketFactoryAdaptor(
+ (LayeredSchemeSocketFactory) this.socketFactory);
+ } else {
+ return new SocketFactoryAdaptor(this.socketFactory);
+ }
+ }
+ }
+
+ /**
+ * Obtains the socket factory.
+ * If this scheme is {@link #isLayered layered}, the factory implements
+ * {@link LayeredSocketFactory LayeredSchemeSocketFactory}.
+ *
+ * @return the socket factory for this scheme
+ *
+ * @since 4.1
+ */
+ public final SchemeSocketFactory getSchemeSocketFactory() {
+ return this.socketFactory;
+ }
+
+ /**
+ * Obtains the scheme name.
+ *
+ * @return the name of this scheme, in lowercase
+ */
+ public final String getName() {
+ return name;
+ }
+
+ /**
+ * Indicates whether this scheme allows for layered connections.
+ *
+ * @return <code>true</code> if layered connections are possible,
+ * <code>false</code> otherwise
+ */
+ public final boolean isLayered() {
+ return layered;
+ }
+
+ /**
+ * Resolves the correct port for this scheme.
+ * Returns the given port if it is valid, the default port otherwise.
+ *
+ * @param port the port to be resolved,
+ * a negative number to obtain the default port
+ *
+ * @return the given port or the defaultPort
+ */
+ public final int resolvePort(final int port) {
+ return port <= 0 ? defaultPort : port;
+ }
+
+ /**
+ * Return a string representation of this object.
+ *
+ * @return a human-readable string description of this scheme
+ */
+ @Override
+ public final String toString() {
+ if (stringRep == null) {
+ final StringBuilder buffer = new StringBuilder();
+ buffer.append(this.name);
+ buffer.append(':');
+ buffer.append(Integer.toString(this.defaultPort));
+ stringRep = buffer.toString();
+ }
+ return stringRep;
+ }
+
+ @Override
+ public final boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof Scheme) {
+ final Scheme that = (Scheme) obj;
+ return this.name.equals(that.name)
+ && this.defaultPort == that.defaultPort
+ && this.layered == that.layered;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = LangUtils.HASH_SEED;
+ hash = LangUtils.hashCode(hash, this.defaultPort);
+ hash = LangUtils.hashCode(hash, this.name);
+ hash = LangUtils.hashCode(hash, this.layered);
+ return hash;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeLayeredSocketFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeLayeredSocketFactory.java
new file mode 100644
index 0000000000..b20ada328c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeLayeredSocketFactory.java
@@ -0,0 +1,69 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.scheme;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+/**
+ * Extended {@link SchemeSocketFactory} interface for layered sockets such as SSL/TLS.
+ *
+ * @since 4.2
+ *
+ * @deprecated (4.3) use {@link
+ * ch.boye.httpclientandroidlib.conn.socket.LayeredConnectionSocketFactory}
+ */
+@Deprecated
+public interface SchemeLayeredSocketFactory extends SchemeSocketFactory {
+
+ /**
+ * Returns a socket connected to the given host that is layered over an
+ * existing socket. Used primarily for creating secure sockets through
+ * proxies.
+ *
+ * @param socket the existing socket
+ * @param target the name of the target host.
+ * @param port the port to connect to on the target host
+ * @param params HTTP parameters
+ *
+ * @return Socket a new socket
+ *
+ * @throws IOException if an I/O error occurs while creating the socket
+ * @throws UnknownHostException if the IP address of the host cannot be
+ * determined
+ */
+ Socket createLayeredSocket(
+ Socket socket,
+ String target,
+ int port,
+ HttpParams params) throws IOException, UnknownHostException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeLayeredSocketFactoryAdaptor.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeLayeredSocketFactoryAdaptor.java
new file mode 100644
index 0000000000..098f55d9a9
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeLayeredSocketFactoryAdaptor.java
@@ -0,0 +1,57 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.scheme;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+/**
+ * @deprecated (4.2) do not use
+ */
+@Deprecated
+class SchemeLayeredSocketFactoryAdaptor extends SchemeSocketFactoryAdaptor
+ implements SchemeLayeredSocketFactory {
+
+ private final LayeredSocketFactory factory;
+
+ SchemeLayeredSocketFactoryAdaptor(final LayeredSocketFactory factory) {
+ super(factory);
+ this.factory = factory;
+ }
+
+ public Socket createLayeredSocket(
+ final Socket socket,
+ final String target, final int port,
+ final HttpParams params) throws IOException, UnknownHostException {
+ return this.factory.createSocket(socket, target, port, true);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeLayeredSocketFactoryAdaptor2.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeLayeredSocketFactoryAdaptor2.java
new file mode 100644
index 0000000000..97f0442d78
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeLayeredSocketFactoryAdaptor2.java
@@ -0,0 +1,74 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.scheme;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+import ch.boye.httpclientandroidlib.conn.ConnectTimeoutException;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+/**
+ * @deprecated (4.2) do not use
+ */
+@Deprecated
+class SchemeLayeredSocketFactoryAdaptor2 implements SchemeLayeredSocketFactory {
+
+ private final LayeredSchemeSocketFactory factory;
+
+ SchemeLayeredSocketFactoryAdaptor2(final LayeredSchemeSocketFactory factory) {
+ super();
+ this.factory = factory;
+ }
+
+ public Socket createSocket(final HttpParams params) throws IOException {
+ return this.factory.createSocket(params);
+ }
+
+ public Socket connectSocket(
+ final Socket sock,
+ final InetSocketAddress remoteAddress,
+ final InetSocketAddress localAddress,
+ final HttpParams params) throws IOException, UnknownHostException, ConnectTimeoutException {
+ return this.factory.connectSocket(sock, remoteAddress, localAddress, params);
+ }
+
+ public boolean isSecure(final Socket sock) throws IllegalArgumentException {
+ return this.factory.isSecure(sock);
+ }
+
+ public Socket createLayeredSocket(
+ final Socket socket,
+ final String target, final int port,
+ final HttpParams params) throws IOException, UnknownHostException {
+ return this.factory.createLayeredSocket(socket, target, port, true);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeRegistry.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeRegistry.java
new file mode 100644
index 0000000000..ef87c809dd
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeRegistry.java
@@ -0,0 +1,168 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn.scheme;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * A set of supported protocol {@link Scheme}s.
+ * Schemes are identified by lowercase names.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.config.Registry}
+ */
+@ThreadSafe
+@Deprecated
+public final class SchemeRegistry {
+
+ /** The available schemes in this registry. */
+ private final ConcurrentHashMap<String,Scheme> registeredSchemes;
+
+ /**
+ * Creates a new, empty scheme registry.
+ */
+ public SchemeRegistry() {
+ super();
+ registeredSchemes = new ConcurrentHashMap<String,Scheme>();
+ }
+
+ /**
+ * Obtains a scheme by name.
+ *
+ * @param name the name of the scheme to look up (in lowercase)
+ *
+ * @return the scheme, never <code>null</code>
+ *
+ * @throws IllegalStateException
+ * if the scheme with the given name is not registered
+ */
+ public final Scheme getScheme(final String name) {
+ final Scheme found = get(name);
+ if (found == null) {
+ throw new IllegalStateException
+ ("Scheme '"+name+"' not registered.");
+ }
+ return found;
+ }
+
+ /**
+ * Obtains the scheme for a host.
+ * Convenience method for <code>getScheme(host.getSchemeName())</pre>
+ *
+ * @param host the host for which to obtain the scheme
+ *
+ * @return the scheme for the given host, never <code>null</code>
+ *
+ * @throws IllegalStateException
+ * if a scheme with the respective name is not registered
+ */
+ public final Scheme getScheme(final HttpHost host) {
+ Args.notNull(host, "Host");
+ return getScheme(host.getSchemeName());
+ }
+
+ /**
+ * Obtains a scheme by name, if registered.
+ *
+ * @param name the name of the scheme to look up (in lowercase)
+ *
+ * @return the scheme, or
+ * <code>null</code> if there is none by this name
+ */
+ public final Scheme get(final String name) {
+ Args.notNull(name, "Scheme name");
+ // leave it to the caller to use the correct name - all lowercase
+ //name = name.toLowerCase(Locale.ENGLISH);
+ final Scheme found = registeredSchemes.get(name);
+ return found;
+ }
+
+ /**
+ * Registers a scheme.
+ * The scheme can later be retrieved by its name
+ * using {@link #getScheme(String) getScheme} or {@link #get get}.
+ *
+ * @param sch the scheme to register
+ *
+ * @return the scheme previously registered with that name, or
+ * <code>null</code> if none was registered
+ */
+ public final Scheme register(final Scheme sch) {
+ Args.notNull(sch, "Scheme");
+ final Scheme old = registeredSchemes.put(sch.getName(), sch);
+ return old;
+ }
+
+ /**
+ * Unregisters a scheme.
+ *
+ * @param name the name of the scheme to unregister (in lowercase)
+ *
+ * @return the unregistered scheme, or
+ * <code>null</code> if there was none
+ */
+ public final Scheme unregister(final String name) {
+ Args.notNull(name, "Scheme name");
+ // leave it to the caller to use the correct name - all lowercase
+ //name = name.toLowerCase(Locale.ENGLISH);
+ final Scheme gone = registeredSchemes.remove(name);
+ return gone;
+ }
+
+ /**
+ * Obtains the names of the registered schemes.
+ *
+ * @return List containing registered scheme names.
+ */
+ public final List<String> getSchemeNames() {
+ return new ArrayList<String>(registeredSchemes.keySet());
+ }
+
+ /**
+ * Populates the internal collection of registered {@link Scheme protocol schemes}
+ * with the content of the map passed as a parameter.
+ *
+ * @param map protocol schemes
+ */
+ public void setItems(final Map<String, Scheme> map) {
+ if (map == null) {
+ return;
+ }
+ registeredSchemes.clear();
+ registeredSchemes.putAll(map);
+ }
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeSocketFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeSocketFactory.java
new file mode 100644
index 0000000000..81cac0ed1f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeSocketFactory.java
@@ -0,0 +1,130 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.scheme;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+import ch.boye.httpclientandroidlib.conn.ConnectTimeoutException;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+/**
+ * A factory for creating, initializing and connecting sockets. The factory encapsulates the logic
+ * for establishing a socket connection.
+ *
+ * @since 4.1
+ *
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.conn.socket.ConnectionSocketFactory}
+ */
+@Deprecated
+public interface SchemeSocketFactory {
+
+ /**
+ * Creates a new, unconnected socket. The socket should subsequently be passed to
+ * {@link #connectSocket(Socket, InetSocketAddress, InetSocketAddress, HttpParams)}.
+ *
+ * @param params Optional {@link HttpParams parameters}. In most cases these parameters
+ * will not be required and will have no effect, as usually socket
+ * initialization should take place in the
+ * {@link #connectSocket(Socket, InetSocketAddress, InetSocketAddress, HttpParams)}
+ * method. However, in rare cases one may want to pass additional parameters
+ * to this method in order to create a customized {@link Socket} instance,
+ * for instance bound to a SOCKS proxy server.
+ *
+ * @return a new socket
+ *
+ * @throws IOException if an I/O error occurs while creating the socket
+ */
+ Socket createSocket(HttpParams params) throws IOException;
+
+ /**
+ * Connects a socket to the target host with the given remote address.
+ * <p/>
+ * Please note that {@link ch.boye.httpclientandroidlib.conn.HttpInetSocketAddress} class should
+ * be used in order to pass the target remote address along with the original
+ * {@link ch.boye.httpclientandroidlib.HttpHost} value used to resolve the address. The use of
+ * {@link ch.boye.httpclientandroidlib.conn.HttpInetSocketAddress} can also ensure that no reverse
+ * DNS lookup will be performed if the target remote address was specified
+ * as an IP address.
+ *
+ * @param sock the socket to connect, as obtained from
+ * {@link #createSocket(HttpParams) createSocket}.
+ * <code>null</code> indicates that a new socket
+ * should be created and connected.
+ * @param remoteAddress the remote address to connect to.
+ * @param localAddress the local address to bind the socket to, or
+ * <code>null</code> for any
+ * @param params additional {@link HttpParams parameters} for connecting
+ *
+ * @return the connected socket. The returned object may be different
+ * from the <code>sock</code> argument if this factory supports
+ * a layered protocol.
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws UnknownHostException if the IP address of the target host
+ * can not be determined
+ * @throws ConnectTimeoutException if the socket cannot be connected
+ * within the time limit defined in the <code>params</code>
+ *
+ * @see ch.boye.httpclientandroidlib.conn.HttpInetSocketAddress
+ */
+ Socket connectSocket(
+ Socket sock,
+ InetSocketAddress remoteAddress,
+ InetSocketAddress localAddress,
+ HttpParams params) throws IOException, UnknownHostException, ConnectTimeoutException;
+
+ /**
+ * Checks whether a socket provides a secure connection. The socket must be
+ * {@link #connectSocket(Socket, InetSocketAddress, InetSocketAddress, HttpParams) connected}
+ * by this factory. The factory will <i>not</i> perform I/O operations in this method.
+ * <p>
+ * As a rule of thumb, plain sockets are not secure and TLS/SSL sockets are secure. However,
+ * there may be application specific deviations. For example, a plain socket to a host in the
+ * same intranet ("trusted zone") could be considered secure. On the other hand, a TLS/SSL
+ * socket could be considered insecure based on the cipher suite chosen for the connection.
+ *
+ * @param sock the connected socket to check
+ *
+ * @return <code>true</code> if the connection of the socket
+ * should be considered secure, or
+ * <code>false</code> if it should not
+ *
+ * @throws IllegalArgumentException
+ * if the argument is invalid, for example because it is
+ * not a connected socket or was created by a different
+ * socket factory.
+ * Note that socket factories are <i>not</i> required to
+ * check these conditions, they may simply return a default
+ * value when called with an invalid socket argument.
+ */
+ boolean isSecure(Socket sock) throws IllegalArgumentException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeSocketFactoryAdaptor.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeSocketFactoryAdaptor.java
new file mode 100644
index 0000000000..85ff5e6b22
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SchemeSocketFactoryAdaptor.java
@@ -0,0 +1,100 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.scheme;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+import ch.boye.httpclientandroidlib.conn.ConnectTimeoutException;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+/**
+ * @deprecated (4.1) do not use
+ */
+@Deprecated
+class SchemeSocketFactoryAdaptor implements SchemeSocketFactory {
+
+ private final SocketFactory factory;
+
+ SchemeSocketFactoryAdaptor(final SocketFactory factory) {
+ super();
+ this.factory = factory;
+ }
+
+ public Socket connectSocket(
+ final Socket sock,
+ final InetSocketAddress remoteAddress,
+ final InetSocketAddress localAddress,
+ final HttpParams params) throws IOException, UnknownHostException, ConnectTimeoutException {
+ final String host = remoteAddress.getHostName();
+ final int port = remoteAddress.getPort();
+ InetAddress local = null;
+ int localPort = 0;
+ if (localAddress != null) {
+ local = localAddress.getAddress();
+ localPort = localAddress.getPort();
+ }
+ return this.factory.connectSocket(sock, host, port, local, localPort, params);
+ }
+
+ public Socket createSocket(final HttpParams params) throws IOException {
+ return this.factory.createSocket();
+ }
+
+ public boolean isSecure(final Socket sock) throws IllegalArgumentException {
+ return this.factory.isSecure(sock);
+ }
+
+ public SocketFactory getFactory() {
+ return this.factory;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof SchemeSocketFactoryAdaptor) {
+ return this.factory.equals(((SchemeSocketFactoryAdaptor)obj).factory);
+ } else {
+ return this.factory.equals(obj);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return this.factory.hashCode();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SocketFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SocketFactory.java
new file mode 100644
index 0000000000..71960194bc
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SocketFactory.java
@@ -0,0 +1,127 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.scheme;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+import ch.boye.httpclientandroidlib.conn.ConnectTimeoutException;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+/**
+ * A factory for creating, initializing and connecting sockets.
+ * The factory encapsulates the logic for establishing a socket connection.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.1) use {@link SchemeSocketFactory}
+ */
+@Deprecated
+public interface SocketFactory {
+
+ /**
+ * Creates a new, unconnected socket.
+ * The socket should subsequently be passed to
+ * {@link #connectSocket connectSocket}.
+ *
+ * @return a new socket
+ *
+ * @throws IOException if an I/O error occurs while creating the socket
+ */
+ Socket createSocket()
+ throws IOException;
+
+ /**
+ * Connects a socket to the given host.
+ *
+ * @param sock the socket to connect, as obtained from
+ * {@link #createSocket createSocket}.
+ * <code>null</code> indicates that a new socket
+ * should be created and connected.
+ * @param host the host to connect to
+ * @param port the port to connect to on the host
+ * @param localAddress the local address to bind the socket to, or
+ * <code>null</code> for any
+ * @param localPort the port on the local machine,
+ * 0 or a negative number for any
+ * @param params additional {@link HttpParams parameters} for connecting
+ *
+ * @return the connected socket. The returned object may be different
+ * from the <code>sock</code> argument if this factory supports
+ * a layered protocol.
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws UnknownHostException if the IP address of the target host
+ * can not be determined
+ * @throws ConnectTimeoutException if the socket cannot be connected
+ * within the time limit defined in the <code>params</code>
+ */
+ Socket connectSocket(
+ Socket sock,
+ String host,
+ int port,
+ InetAddress localAddress,
+ int localPort,
+ HttpParams params
+ ) throws IOException, UnknownHostException, ConnectTimeoutException;
+
+ /**
+ * Checks whether a socket provides a secure connection.
+ * The socket must be {@link #connectSocket connected}
+ * by this factory.
+ * The factory will <i>not</i> perform I/O operations
+ * in this method.
+ * <br/>
+ * As a rule of thumb, plain sockets are not secure and
+ * TLS/SSL sockets are secure. However, there may be
+ * application specific deviations. For example, a plain
+ * socket to a host in the same intranet ("trusted zone")
+ * could be considered secure. On the other hand, a
+ * TLS/SSL socket could be considered insecure based on
+ * the cipher suite chosen for the connection.
+ *
+ * @param sock the connected socket to check
+ *
+ * @return <code>true</code> if the connection of the socket
+ * should be considered secure, or
+ * <code>false</code> if it should not
+ *
+ * @throws IllegalArgumentException
+ * if the argument is invalid, for example because it is
+ * not a connected socket or was created by a different
+ * socket factory.
+ * Note that socket factories are <i>not</i> required to
+ * check these conditions, they may simply return a default
+ * value when called with an invalid socket argument.
+ */
+ boolean isSecure(Socket sock)
+ throws IllegalArgumentException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SocketFactoryAdaptor.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SocketFactoryAdaptor.java
new file mode 100644
index 0000000000..0e9aab355c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/SocketFactoryAdaptor.java
@@ -0,0 +1,97 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.scheme;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+import ch.boye.httpclientandroidlib.conn.ConnectTimeoutException;
+import ch.boye.httpclientandroidlib.params.BasicHttpParams;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+@Deprecated
+class SocketFactoryAdaptor implements SocketFactory {
+
+ private final SchemeSocketFactory factory;
+
+ SocketFactoryAdaptor(final SchemeSocketFactory factory) {
+ super();
+ this.factory = factory;
+ }
+
+ public Socket createSocket() throws IOException {
+ final HttpParams params = new BasicHttpParams();
+ return this.factory.createSocket(params);
+ }
+
+ public Socket connectSocket(
+ final Socket socket,
+ final String host, final int port,
+ final InetAddress localAddress, final int localPort,
+ final HttpParams params) throws IOException, UnknownHostException, ConnectTimeoutException {
+ InetSocketAddress local = null;
+ if (localAddress != null || localPort > 0) {
+ local = new InetSocketAddress(localAddress, localPort > 0 ? localPort : 0);
+ }
+ final InetAddress remoteAddress = InetAddress.getByName(host);
+ final InetSocketAddress remote = new InetSocketAddress(remoteAddress, port);
+ return this.factory.connectSocket(socket, remote, local, params);
+ }
+
+ public boolean isSecure(final Socket socket) throws IllegalArgumentException {
+ return this.factory.isSecure(socket);
+ }
+
+ public SchemeSocketFactory getFactory() {
+ return this.factory;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof SocketFactoryAdaptor) {
+ return this.factory.equals(((SocketFactoryAdaptor)obj).factory);
+ } else {
+ return this.factory.equals(obj);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return this.factory.hashCode();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/package-info.java
new file mode 100644
index 0000000000..2e6409084c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/scheme/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Deprecated.
+ * @deprecated (4.3).
+ */
+package ch.boye.httpclientandroidlib.conn.scheme;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/socket/ConnectionSocketFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/socket/ConnectionSocketFactory.java
new file mode 100644
index 0000000000..98274f213d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/socket/ConnectionSocketFactory.java
@@ -0,0 +1,80 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.socket;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * A factory for creating and connecting connection sockets.
+ *
+ * @since 4.3
+ */
+public interface ConnectionSocketFactory {
+
+ /**
+ * Creates new, unconnected socket. The socket should subsequently be passed to
+ * {@link #connectSocket(int, Socket, HttpHost, InetSocketAddress, InetSocketAddress,
+ * HttpContext) connectSocket} method.
+ *
+ * @return a new socket
+ *
+ * @throws IOException if an I/O error occurs while creating the socket
+ */
+ Socket createSocket(HttpContext context) throws IOException;
+
+ /**
+ * Connects the socket to the target host with the given resolved remote address.
+ *
+ * @param connectTimeout connect timeout.
+ * @param sock the socket to connect, as obtained from {@link #createSocket(HttpContext)}.
+ * <code>null</code> indicates that a new socket should be created and connected.
+ * @param host target host as specified by the caller (end user).
+ * @param remoteAddress the resolved remote address to connect to.
+ * @param localAddress the local address to bind the socket to, or <code>null</code> for any.
+ * @param context the actual HTTP context.
+ *
+ * @return the connected socket. The returned object may be different
+ * from the <code>sock</code> argument if this factory supports
+ * a layered protocol.
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ Socket connectSocket(
+ int connectTimeout,
+ Socket sock,
+ HttpHost host,
+ InetSocketAddress remoteAddress,
+ InetSocketAddress localAddress,
+ HttpContext context) throws IOException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/socket/LayeredConnectionSocketFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/socket/LayeredConnectionSocketFactory.java
new file mode 100644
index 0000000000..40c54f8faa
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/socket/LayeredConnectionSocketFactory.java
@@ -0,0 +1,63 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.socket;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * Extended {@link ConnectionSocketFactory} interface for layered sockets such as SSL/TLS.
+ *
+ * @since 4.3
+ */
+public interface LayeredConnectionSocketFactory extends ConnectionSocketFactory {
+
+ /**
+ * Returns a socket connected to the given host that is layered over an
+ * existing socket. Used primarily for creating secure sockets through
+ * proxies.
+ *
+ * @param socket the existing socket
+ * @param target the name of the target host.
+ * @param port the port to connect to on the target host.
+ * @param context the actual HTTP context.
+ *
+ * @return Socket a new socket
+ *
+ * @throws IOException if an I/O error occurs while creating the socket
+ */
+ Socket createLayeredSocket(
+ Socket socket,
+ String target,
+ int port,
+ HttpContext context) throws IOException, UnknownHostException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/socket/PlainConnectionSocketFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/socket/PlainConnectionSocketFactory.java
new file mode 100644
index 0000000000..d8fa807c5a
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/socket/PlainConnectionSocketFactory.java
@@ -0,0 +1,83 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.socket;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * The default class for creating plain (unencrypted) sockets.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class PlainConnectionSocketFactory implements ConnectionSocketFactory {
+
+ public static final PlainConnectionSocketFactory INSTANCE = new PlainConnectionSocketFactory();
+
+ public static PlainConnectionSocketFactory getSocketFactory() {
+ return INSTANCE;
+ }
+
+ public PlainConnectionSocketFactory() {
+ super();
+ }
+
+ public Socket createSocket(final HttpContext context) throws IOException {
+ return new Socket();
+ }
+
+ public Socket connectSocket(
+ final int connectTimeout,
+ final Socket socket,
+ final HttpHost host,
+ final InetSocketAddress remoteAddress,
+ final InetSocketAddress localAddress,
+ final HttpContext context) throws IOException {
+ final Socket sock = socket != null ? socket : createSocket(context);
+ if (localAddress != null) {
+ sock.bind(localAddress);
+ }
+ try {
+ sock.connect(remoteAddress, connectTimeout);
+ } catch (final IOException ex) {
+ try {
+ sock.close();
+ } catch (final IOException ignore) {
+ }
+ throw ex;
+ }
+ return sock;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/socket/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/socket/package-info.java
new file mode 100644
index 0000000000..fb73d60de8
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/socket/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Client connection socket APIs.
+ */
+package ch.boye.httpclientandroidlib.conn.socket;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/AbstractVerifier.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/AbstractVerifier.java
new file mode 100644
index 0000000000..24a7b40a8c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/AbstractVerifier.java
@@ -0,0 +1,386 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.ssl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.NoSuchElementException;
+
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.conn.util.InetAddressUtils;
+import ch.boye.httpclientandroidlib.util.TextUtils;
+
+import ch.boye.httpclientandroidlib.NameValuePair;
+
+
+/**
+ * Abstract base class for all standard {@link X509HostnameVerifier}
+ * implementations.
+ *
+ * @since 4.0
+ */
+@Immutable
+public abstract class AbstractVerifier implements X509HostnameVerifier {
+
+ /**
+ * This contains a list of 2nd-level domains that aren't allowed to
+ * have wildcards when combined with country-codes.
+ * For example: [*.co.uk].
+ * <p/>
+ * The [*.co.uk] problem is an interesting one. Should we just hope
+ * that CA's would never foolishly allow such a certificate to happen?
+ * Looks like we're the only implementation guarding against this.
+ * Firefox, Curl, Sun Java 1.4, 5, 6 don't bother with this check.
+ */
+ private final static String[] BAD_COUNTRY_2LDS =
+ { "ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info",
+ "lg", "ne", "net", "or", "org" };
+
+ static {
+ // Just in case developer forgot to manually sort the array. :-)
+ Arrays.sort(BAD_COUNTRY_2LDS);
+ }
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ public AbstractVerifier() {
+ super();
+ }
+
+ public final void verify(final String host, final SSLSocket ssl)
+ throws IOException {
+ if(host == null) {
+ throw new NullPointerException("host to verify is null");
+ }
+
+ SSLSession session = ssl.getSession();
+ if(session == null) {
+ // In our experience this only happens under IBM 1.4.x when
+ // spurious (unrelated) certificates show up in the server'
+ // chain. Hopefully this will unearth the real problem:
+ final InputStream in = ssl.getInputStream();
+ in.available();
+ /*
+ If you're looking at the 2 lines of code above because
+ you're running into a problem, you probably have two
+ options:
+
+ #1. Clean up the certificate chain that your server
+ is presenting (e.g. edit "/etc/apache2/server.crt"
+ or wherever it is your server's certificate chain
+ is defined).
+
+ OR
+
+ #2. Upgrade to an IBM 1.5.x or greater JVM, or switch
+ to a non-IBM JVM.
+ */
+
+ // If ssl.getInputStream().available() didn't cause an
+ // exception, maybe at least now the session is available?
+ session = ssl.getSession();
+ if(session == null) {
+ // If it's still null, probably a startHandshake() will
+ // unearth the real problem.
+ ssl.startHandshake();
+
+ // Okay, if we still haven't managed to cause an exception,
+ // might as well go for the NPE. Or maybe we're okay now?
+ session = ssl.getSession();
+ }
+ }
+
+ final Certificate[] certs = session.getPeerCertificates();
+ final X509Certificate x509 = (X509Certificate) certs[0];
+ verify(host, x509);
+ }
+
+ public final boolean verify(final String host, final SSLSession session) {
+ try {
+ final Certificate[] certs = session.getPeerCertificates();
+ final X509Certificate x509 = (X509Certificate) certs[0];
+ verify(host, x509);
+ return true;
+ }
+ catch(final SSLException e) {
+ return false;
+ }
+ }
+
+ public final void verify(final String host, final X509Certificate cert)
+ throws SSLException {
+ final String[] cns = getCNs(cert);
+ final String[] subjectAlts = getSubjectAlts(cert, host);
+ verify(host, cns, subjectAlts);
+ }
+
+ public final void verify(final String host, final String[] cns,
+ final String[] subjectAlts,
+ final boolean strictWithSubDomains)
+ throws SSLException {
+
+ // Build the list of names we're going to check. Our DEFAULT and
+ // STRICT implementations of the HostnameVerifier only use the
+ // first CN provided. All other CNs are ignored.
+ // (Firefox, wget, curl, Sun Java 1.4, 5, 6 all work this way).
+ final LinkedList<String> names = new LinkedList<String>();
+ if(cns != null && cns.length > 0 && cns[0] != null) {
+ names.add(cns[0]);
+ }
+ if(subjectAlts != null) {
+ for (final String subjectAlt : subjectAlts) {
+ if (subjectAlt != null) {
+ names.add(subjectAlt);
+ }
+ }
+ }
+
+ if(names.isEmpty()) {
+ final String msg = "Certificate for <" + host + "> doesn't contain CN or DNS subjectAlt";
+ throw new SSLException(msg);
+ }
+
+ // StringBuilder for building the error message.
+ final StringBuilder buf = new StringBuilder();
+
+ // We're can be case-insensitive when comparing the host we used to
+ // establish the socket to the hostname in the certificate.
+ final String hostName = normaliseIPv6Address(host.trim().toLowerCase(Locale.ENGLISH));
+ boolean match = false;
+ for(final Iterator<String> it = names.iterator(); it.hasNext();) {
+ // Don't trim the CN, though!
+ String cn = it.next();
+ cn = cn.toLowerCase(Locale.ENGLISH);
+ // Store CN in StringBuilder in case we need to report an error.
+ buf.append(" <");
+ buf.append(cn);
+ buf.append('>');
+ if(it.hasNext()) {
+ buf.append(" OR");
+ }
+
+ // The CN better have at least two dots if it wants wildcard
+ // action. It also can't be [*.co.uk] or [*.co.jp] or
+ // [*.org.uk], etc...
+ final String parts[] = cn.split("\\.");
+ final boolean doWildcard =
+ parts.length >= 3 && parts[0].endsWith("*") &&
+ validCountryWildcard(cn) && !isIPAddress(host);
+
+ if(doWildcard) {
+ final String firstpart = parts[0];
+ if (firstpart.length() > 1) { // e.g. server*
+ final String prefix = firstpart.substring(0, firstpart.length() - 1); // e.g. server
+ final String suffix = cn.substring(firstpart.length()); // skip wildcard part from cn
+ final String hostSuffix = hostName.substring(prefix.length()); // skip wildcard part from host
+ match = hostName.startsWith(prefix) && hostSuffix.endsWith(suffix);
+ } else {
+ match = hostName.endsWith(cn.substring(1));
+ }
+ if(match && strictWithSubDomains) {
+ // If we're in strict mode, then [*.foo.com] is not
+ // allowed to match [a.b.foo.com]
+ match = countDots(hostName) == countDots(cn);
+ }
+ } else {
+ match = hostName.equals(normaliseIPv6Address(cn));
+ }
+ if(match) {
+ break;
+ }
+ }
+ if(!match) {
+ throw new SSLException("hostname in certificate didn't match: <" + host + "> !=" + buf);
+ }
+ }
+
+ /**
+ * @deprecated (4.3.1) should not be a part of public APIs.
+ */
+ @Deprecated
+ public static boolean acceptableCountryWildcard(final String cn) {
+ final String parts[] = cn.split("\\.");
+ if (parts.length != 3 || parts[2].length() != 2) {
+ return true; // it's not an attempt to wildcard a 2TLD within a country code
+ }
+ return Arrays.binarySearch(BAD_COUNTRY_2LDS, parts[1]) < 0;
+ }
+
+ boolean validCountryWildcard(final String cn) {
+ final String parts[] = cn.split("\\.");
+ if (parts.length != 3 || parts[2].length() != 2) {
+ return true; // it's not an attempt to wildcard a 2TLD within a country code
+ }
+ return Arrays.binarySearch(BAD_COUNTRY_2LDS, parts[1]) < 0;
+ }
+
+ public static String[] getCNs(final X509Certificate cert) {
+ final String subjectPrincipal = cert.getSubjectX500Principal().toString();
+ try {
+ return extractCNs(subjectPrincipal);
+ } catch (SSLException ex) {
+ return null;
+ }
+ }
+
+ static String[] extractCNs(final String subjectPrincipal) throws SSLException {
+ if (subjectPrincipal == null) {
+ return null;
+ }
+ final List<String> cns = new ArrayList<String>();
+ final List<NameValuePair> nvps = DistinguishedNameParser.INSTANCE.parse(subjectPrincipal);
+ for (int i = 0; i < nvps.size(); i++) {
+ final NameValuePair nvp = nvps.get(i);
+ final String attribName = nvp.getName();
+ final String attribValue = nvp.getValue();
+ if (TextUtils.isBlank(attribValue)) {
+ throw new SSLException(subjectPrincipal + " is not a valid X500 distinguished name");
+ }
+ if (attribName.equalsIgnoreCase("cn")) {
+ cns.add(attribValue);
+ }
+ }
+ return cns.isEmpty() ? null : cns.toArray(new String[ cns.size() ]);
+ }
+
+ /**
+ * Extracts the array of SubjectAlt DNS or IP names from an X509Certificate.
+ * Returns null if there aren't any.
+ *
+ * @param cert X509Certificate
+ * @param hostname
+ * @return Array of SubjectALT DNS or IP names stored in the certificate.
+ */
+ private static String[] getSubjectAlts(
+ final X509Certificate cert, final String hostname) {
+ final int subjectType;
+ if (isIPAddress(hostname)) {
+ subjectType = 7;
+ } else {
+ subjectType = 2;
+ }
+
+ final LinkedList<String> subjectAltList = new LinkedList<String>();
+ Collection<List<?>> c = null;
+ try {
+ c = cert.getSubjectAlternativeNames();
+ }
+ catch(final CertificateParsingException cpe) {
+ }
+ if(c != null) {
+ for (final List<?> aC : c) {
+ final List<?> list = aC;
+ final int type = ((Integer) list.get(0)).intValue();
+ if (type == subjectType) {
+ final String s = (String) list.get(1);
+ subjectAltList.add(s);
+ }
+ }
+ }
+ if(!subjectAltList.isEmpty()) {
+ final String[] subjectAlts = new String[subjectAltList.size()];
+ subjectAltList.toArray(subjectAlts);
+ return subjectAlts;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Extracts the array of SubjectAlt DNS names from an X509Certificate.
+ * Returns null if there aren't any.
+ * <p/>
+ * Note: Java doesn't appear able to extract international characters
+ * from the SubjectAlts. It can only extract international characters
+ * from the CN field.
+ * <p/>
+ * (Or maybe the version of OpenSSL I'm using to test isn't storing the
+ * international characters correctly in the SubjectAlts?).
+ *
+ * @param cert X509Certificate
+ * @return Array of SubjectALT DNS names stored in the certificate.
+ */
+ public static String[] getDNSSubjectAlts(final X509Certificate cert) {
+ return getSubjectAlts(cert, null);
+ }
+
+ /**
+ * Counts the number of dots "." in a string.
+ * @param s string to count dots from
+ * @return number of dots
+ */
+ public static int countDots(final String s) {
+ int count = 0;
+ for(int i = 0; i < s.length(); i++) {
+ if(s.charAt(i) == '.') {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private static boolean isIPAddress(final String hostname) {
+ return hostname != null &&
+ (InetAddressUtils.isIPv4Address(hostname) ||
+ InetAddressUtils.isIPv6Address(hostname));
+ }
+
+ /*
+ * Check if hostname is IPv6, and if so, convert to standard format.
+ */
+ private String normaliseIPv6Address(final String hostname) {
+ if (hostname == null || !InetAddressUtils.isIPv6Address(hostname)) {
+ return hostname;
+ }
+ try {
+ final InetAddress inetAddress = InetAddress.getByName(hostname);
+ return inetAddress.getHostAddress();
+ } catch (final UnknownHostException uhe) { // Should not happen, because we check for IPv6 address above
+ log.error("Unexpected error converting "+hostname, uhe);
+ return hostname;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/AllowAllHostnameVerifier.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/AllowAllHostnameVerifier.java
new file mode 100644
index 0000000000..8ca1fad5a0
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/AllowAllHostnameVerifier.java
@@ -0,0 +1,54 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.ssl;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * The ALLOW_ALL HostnameVerifier essentially turns hostname verification
+ * off. This implementation is a no-op, and never throws the SSLException.
+ *
+ *
+ * @since 4.0
+ */
+@Immutable
+public class AllowAllHostnameVerifier extends AbstractVerifier {
+
+ public final void verify(
+ final String host,
+ final String[] cns,
+ final String[] subjectAlts) {
+ // Allow everything - so never blowup.
+ }
+
+ @Override
+ public final String toString() {
+ return "ALLOW_ALL";
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/BrowserCompatHostnameVerifier.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/BrowserCompatHostnameVerifier.java
new file mode 100644
index 0000000000..05f7d8c8cc
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/BrowserCompatHostnameVerifier.java
@@ -0,0 +1,67 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.ssl;
+
+import javax.net.ssl.SSLException;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * The HostnameVerifier that works the same way as Curl and Firefox.
+ * <p/>
+ * The hostname must match either the first CN, or any of the subject-alts.
+ * A wildcard can occur in the CN, and in any of the subject-alts.
+ * <p/>
+ * The only difference between BROWSER_COMPATIBLE and STRICT is that a wildcard
+ * (such as "*.foo.com") with BROWSER_COMPATIBLE matches all subdomains,
+ * including "a.b.foo.com".
+ *
+ *
+ * @since 4.0
+ */
+@Immutable
+public class BrowserCompatHostnameVerifier extends AbstractVerifier {
+
+ public final void verify(
+ final String host,
+ final String[] cns,
+ final String[] subjectAlts) throws SSLException {
+ verify(host, cns, subjectAlts, false);
+ }
+
+ @Override
+ boolean validCountryWildcard(final String cn) {
+ return true;
+ }
+
+ @Override
+ public final String toString() {
+ return "BROWSER_COMPATIBLE";
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/DistinguishedNameParser.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/DistinguishedNameParser.java
new file mode 100644
index 0000000000..98b2ec3a9c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/DistinguishedNameParser.java
@@ -0,0 +1,131 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.ssl;
+
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.message.BasicNameValuePair;
+import ch.boye.httpclientandroidlib.message.ParserCursor;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+@Immutable
+final class DistinguishedNameParser {
+
+ public final static DistinguishedNameParser INSTANCE = new DistinguishedNameParser();
+
+ private static final BitSet EQUAL_OR_COMMA_OR_PLUS = TokenParser.INIT_BITSET('=', ',', '+');
+ private static final BitSet COMMA_OR_PLUS = TokenParser.INIT_BITSET(',', '+');
+
+ private final TokenParser tokenParser;
+
+ DistinguishedNameParser() {
+ this.tokenParser = new InternalTokenParser();
+ }
+
+ String parseToken(final CharArrayBuffer buf, final ParserCursor cursor, final BitSet delimiters) {
+ return tokenParser.parseToken(buf, cursor, delimiters);
+ }
+
+ String parseValue(final CharArrayBuffer buf, final ParserCursor cursor, final BitSet delimiters) {
+ return tokenParser.parseValue(buf, cursor, delimiters);
+ }
+
+ NameValuePair parseParameter(final CharArrayBuffer buf, final ParserCursor cursor) {
+ final String name = parseToken(buf, cursor, EQUAL_OR_COMMA_OR_PLUS);
+ if (cursor.atEnd()) {
+ return new BasicNameValuePair(name, null);
+ }
+ final int delim = buf.charAt(cursor.getPos());
+ cursor.updatePos(cursor.getPos() + 1);
+ if (delim == ',') {
+ return new BasicNameValuePair(name, null);
+ }
+ final String value = parseValue(buf, cursor, COMMA_OR_PLUS);
+ if (!cursor.atEnd()) {
+ cursor.updatePos(cursor.getPos() + 1);
+ }
+ return new BasicNameValuePair(name, value);
+ }
+
+ public List<NameValuePair> parse(final CharArrayBuffer buf, final ParserCursor cursor) {
+ final List<NameValuePair> params = new ArrayList<NameValuePair>();
+ tokenParser.skipWhiteSpace(buf, cursor);
+ while (!cursor.atEnd()) {
+ final NameValuePair param = parseParameter(buf, cursor);
+ params.add(param);
+ }
+ return params;
+ }
+
+ public List<NameValuePair> parse(final String s) {
+ if (s == null) {
+ return null;
+ }
+ final CharArrayBuffer buffer = new CharArrayBuffer(s.length());
+ buffer.append(s);
+ final ParserCursor cursor = new ParserCursor(0, s.length());
+ return parse(buffer, cursor);
+ }
+
+ static class InternalTokenParser extends TokenParser {
+
+ @Override
+ public void copyUnquotedContent(
+ final CharArrayBuffer buf,
+ final ParserCursor cursor,
+ final BitSet delimiters,
+ final StringBuilder dst) {
+ int pos = cursor.getPos();
+ final int indexFrom = cursor.getPos();
+ final int indexTo = cursor.getUpperBound();
+ boolean escaped = false;
+ for (int i = indexFrom; i < indexTo; i++, pos++) {
+ final char current = buf.charAt(i);
+ if (escaped) {
+ dst.append(current);
+ escaped = false;
+ } else {
+ if ((delimiters != null && delimiters.get(current))
+ || TokenParser.isWhitespace(current) || current == '\"') {
+ break;
+ } else if (current == '\\') {
+ escaped = true;
+ } else {
+ dst.append(current);
+ }
+ }
+ }
+ cursor.updatePos(pos);
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/PrivateKeyDetails.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/PrivateKeyDetails.java
new file mode 100644
index 0000000000..c859785503
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/PrivateKeyDetails.java
@@ -0,0 +1,63 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn.ssl;
+
+import ch.boye.httpclientandroidlib.util.Args;
+
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+
+/**
+ * Private key details.
+ *
+ * @since 4.3
+ */
+public final class PrivateKeyDetails {
+
+ private final String type;
+ private final X509Certificate[] certChain;
+
+ public PrivateKeyDetails(final String type, final X509Certificate[] certChain) {
+ super();
+ this.type = Args.notNull(type, "Private key type");
+ this.certChain = certChain;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public X509Certificate[] getCertChain() {
+ return certChain;
+ }
+
+ @Override
+ public String toString() {
+ return type + ':' + Arrays.toString(certChain);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/PrivateKeyStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/PrivateKeyStrategy.java
new file mode 100644
index 0000000000..ba3ba40c52
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/PrivateKeyStrategy.java
@@ -0,0 +1,44 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn.ssl;
+
+import java.net.Socket;
+import java.util.Map;
+
+/**
+ * A strategy allowing for a choice of an alias during SSL authentication.
+ *
+ * @since 4.3
+ */
+public interface PrivateKeyStrategy {
+
+ /**
+ * Determines what key material to use for SSL authentication.
+ */
+ String chooseAlias(Map<String, PrivateKeyDetails> aliases, Socket socket);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/SSLConnectionSocketFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/SSLConnectionSocketFactory.java
new file mode 100644
index 0000000000..341fbe3852
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/SSLConnectionSocketFactory.java
@@ -0,0 +1,295 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.ssl;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.conn.socket.LayeredConnectionSocketFactory;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.TextUtils;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+
+/**
+ * Layered socket factory for TLS/SSL connections.
+ * <p>
+ * SSLSocketFactory can be used to validate the identity of the HTTPS server against a list of
+ * trusted certificates and to authenticate to the HTTPS server using a private key.
+ * <p>
+ * SSLSocketFactory will enable server authentication when supplied with
+ * a {@link java.security.KeyStore trust-store} file containing one or several trusted certificates. The client
+ * secure socket will reject the connection during the SSL session handshake if the target HTTPS
+ * server attempts to authenticate itself with a non-trusted certificate.
+ * <p>
+ * Use JDK keytool utility to import a trusted certificate and generate a trust-store file:
+ * <pre>
+ * keytool -import -alias "my server cert" -file server.crt -keystore my.truststore
+ * </pre>
+ * <p>
+ * In special cases the standard trust verification process can be bypassed by using a custom
+ * {@link ch.boye.httpclientandroidlib.conn.ssl.TrustStrategy}. This interface is primarily intended for allowing self-signed
+ * certificates to be accepted as trusted without having to add them to the trust-store file.
+ * <p>
+ * SSLSocketFactory will enable client authentication when supplied with
+ * a {@link java.security.KeyStore key-store} file containing a private key/public certificate
+ * pair. The client secure socket will use the private key to authenticate
+ * itself to the target HTTPS server during the SSL session handshake if
+ * requested to do so by the server.
+ * The target HTTPS server will in its turn verify the certificate presented
+ * by the client in order to establish client's authenticity.
+ * <p>
+ * Use the following sequence of actions to generate a key-store file
+ * </p>
+ * <ul>
+ * <li>
+ * <p>
+ * Use JDK keytool utility to generate a new key
+ * <pre>keytool -genkey -v -alias "my client key" -validity 365 -keystore my.keystore</pre>
+ * For simplicity use the same password for the key as that of the key-store
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Issue a certificate signing request (CSR)
+ * <pre>keytool -certreq -alias "my client key" -file mycertreq.csr -keystore my.keystore</pre>
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Send the certificate request to the trusted Certificate Authority for signature.
+ * One may choose to act as her own CA and sign the certificate request using a PKI
+ * tool, such as OpenSSL.
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Import the trusted CA root certificate
+ * <pre>keytool -import -alias "my trusted ca" -file caroot.crt -keystore my.keystore</pre>
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Import the PKCS#7 file containg the complete certificate chain
+ * <pre>keytool -import -alias "my client key" -file mycert.p7 -keystore my.keystore</pre>
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Verify the content the resultant keystore file
+ * <pre>keytool -list -v -keystore my.keystore</pre>
+ * </p>
+ * </li>
+ * </ul>
+ *
+ * @since 4.0
+ */
+@ThreadSafe
+public class SSLConnectionSocketFactory implements LayeredConnectionSocketFactory {
+
+ public static final String TLS = "TLS";
+ public static final String SSL = "SSL";
+ public static final String SSLV2 = "SSLv2";
+
+ public static final X509HostnameVerifier ALLOW_ALL_HOSTNAME_VERIFIER
+ = new AllowAllHostnameVerifier();
+
+ public static final X509HostnameVerifier BROWSER_COMPATIBLE_HOSTNAME_VERIFIER
+ = new BrowserCompatHostnameVerifier();
+
+ public static final X509HostnameVerifier STRICT_HOSTNAME_VERIFIER
+ = new StrictHostnameVerifier();
+
+ /**
+ * Obtains default SSL socket factory with an SSL context based on the standard JSSE
+ * trust material (<code>cacerts</code> file in the security properties directory).
+ * System properties are not taken into consideration.
+ *
+ * @return default SSL socket factory
+ */
+ public static SSLConnectionSocketFactory getSocketFactory() throws SSLInitializationException {
+ return new SSLConnectionSocketFactory(
+ SSLContexts.createDefault(),
+ BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
+ }
+
+ private static String[] split(final String s) {
+ if (TextUtils.isBlank(s)) {
+ return null;
+ }
+ return s.split(" *, *");
+ }
+
+ /**
+ * Obtains default SSL socket factory with an SSL context based on system properties
+ * as described in
+ * <a href="http://docs.oracle.com/javase/1.5.0/docs/guide/security/jsse/JSSERefGuide.html">
+ * "JavaTM Secure Socket Extension (JSSE) Reference Guide for the JavaTM 2 Platform
+ * Standard Edition 5</a>
+ *
+ * @return default system SSL socket factory
+ */
+ public static SSLConnectionSocketFactory getSystemSocketFactory() throws SSLInitializationException {
+ return new SSLConnectionSocketFactory(
+ (javax.net.ssl.SSLSocketFactory) javax.net.ssl.SSLSocketFactory.getDefault(),
+ split(System.getProperty("https.protocols")),
+ split(System.getProperty("https.cipherSuites")),
+ BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
+ }
+
+ private final javax.net.ssl.SSLSocketFactory socketfactory;
+ private final X509HostnameVerifier hostnameVerifier;
+ private final String[] supportedProtocols;
+ private final String[] supportedCipherSuites;
+
+ public SSLConnectionSocketFactory(final SSLContext sslContext) {
+ this(sslContext, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
+ }
+
+ public SSLConnectionSocketFactory(
+ final SSLContext sslContext, final X509HostnameVerifier hostnameVerifier) {
+ this(Args.notNull(sslContext, "SSL context").getSocketFactory(),
+ null, null, hostnameVerifier);
+ }
+
+ public SSLConnectionSocketFactory(
+ final SSLContext sslContext,
+ final String[] supportedProtocols,
+ final String[] supportedCipherSuites,
+ final X509HostnameVerifier hostnameVerifier) {
+ this(Args.notNull(sslContext, "SSL context").getSocketFactory(),
+ supportedProtocols, supportedCipherSuites, hostnameVerifier);
+ }
+
+ public SSLConnectionSocketFactory(
+ final javax.net.ssl.SSLSocketFactory socketfactory,
+ final X509HostnameVerifier hostnameVerifier) {
+ this(socketfactory, null, null, hostnameVerifier);
+ }
+
+ public SSLConnectionSocketFactory(
+ final javax.net.ssl.SSLSocketFactory socketfactory,
+ final String[] supportedProtocols,
+ final String[] supportedCipherSuites,
+ final X509HostnameVerifier hostnameVerifier) {
+ this.socketfactory = Args.notNull(socketfactory, "SSL socket factory");
+ this.supportedProtocols = supportedProtocols;
+ this.supportedCipherSuites = supportedCipherSuites;
+ this.hostnameVerifier = hostnameVerifier != null ? hostnameVerifier : BROWSER_COMPATIBLE_HOSTNAME_VERIFIER;
+ }
+
+ /**
+ * Performs any custom initialization for a newly created SSLSocket
+ * (before the SSL handshake happens).
+ *
+ * The default implementation is a no-op, but could be overridden to, e.g.,
+ * call {@link javax.net.ssl.SSLSocket#setEnabledCipherSuites(String[])}.
+ */
+ protected void prepareSocket(final SSLSocket socket) throws IOException {
+ }
+
+ public Socket createSocket(final HttpContext context) throws IOException {
+ return SocketFactory.getDefault().createSocket();
+ }
+
+ public Socket connectSocket(
+ final int connectTimeout,
+ final Socket socket,
+ final HttpHost host,
+ final InetSocketAddress remoteAddress,
+ final InetSocketAddress localAddress,
+ final HttpContext context) throws IOException {
+ Args.notNull(host, "HTTP host");
+ Args.notNull(remoteAddress, "Remote address");
+ final Socket sock = socket != null ? socket : createSocket(context);
+ if (localAddress != null) {
+ sock.bind(localAddress);
+ }
+ try {
+ sock.connect(remoteAddress, connectTimeout);
+ } catch (final IOException ex) {
+ try {
+ sock.close();
+ } catch (final IOException ignore) {
+ }
+ throw ex;
+ }
+ // Setup SSL layering if necessary
+ if (sock instanceof SSLSocket) {
+ final SSLSocket sslsock = (SSLSocket) sock;
+ sslsock.startHandshake();
+ verifyHostname(sslsock, host.getHostName());
+ return sock;
+ } else {
+ return createLayeredSocket(sock, host.getHostName(), remoteAddress.getPort(), context);
+ }
+ }
+
+ public Socket createLayeredSocket(
+ final Socket socket,
+ final String target,
+ final int port,
+ final HttpContext context) throws IOException {
+ final SSLSocket sslsock = (SSLSocket) this.socketfactory.createSocket(
+ socket,
+ target,
+ port,
+ true);
+ if (supportedProtocols != null) {
+ sslsock.setEnabledProtocols(supportedProtocols);
+ }
+ if (supportedCipherSuites != null) {
+ sslsock.setEnabledCipherSuites(supportedCipherSuites);
+ }
+ prepareSocket(sslsock);
+ sslsock.startHandshake();
+ verifyHostname(sslsock, target);
+ return sslsock;
+ }
+
+ X509HostnameVerifier getHostnameVerifier() {
+ return this.hostnameVerifier;
+ }
+
+ private void verifyHostname(final SSLSocket sslsock, final String hostname) throws IOException {
+ try {
+ this.hostnameVerifier.verify(hostname, sslsock);
+ // verifyHostName() didn't blowup - good!
+ } catch (final IOException iox) {
+ // close the socket before re-throwing the exception
+ try { sslsock.close(); } catch (final Exception x) { /*ignore*/ }
+ throw iox;
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/SSLContextBuilder.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/SSLContextBuilder.java
new file mode 100644
index 0000000000..89751a166c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/SSLContextBuilder.java
@@ -0,0 +1,259 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.ssl;
+
+import java.net.Socket;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509KeyManager;
+import javax.net.ssl.X509TrustManager;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+
+/**
+ * Builder for {@link SSLContext} instances.
+ *
+ * @since 4.3
+ */
+@NotThreadSafe
+public class SSLContextBuilder {
+
+ static final String TLS = "TLS";
+ static final String SSL = "SSL";
+
+ private String protocol;
+ private Set<KeyManager> keymanagers;
+ private Set<TrustManager> trustmanagers;
+ private SecureRandom secureRandom;
+
+ public SSLContextBuilder() {
+ super();
+ this.keymanagers = new HashSet<KeyManager>();
+ this.trustmanagers = new HashSet<TrustManager>();
+ }
+
+ public SSLContextBuilder useTLS() {
+ this.protocol = TLS;
+ return this;
+ }
+
+ public SSLContextBuilder useSSL() {
+ this.protocol = SSL;
+ return this;
+ }
+
+ public SSLContextBuilder useProtocol(final String protocol) {
+ this.protocol = protocol;
+ return this;
+ }
+
+ public SSLContextBuilder setSecureRandom(final SecureRandom secureRandom) {
+ this.secureRandom = secureRandom;
+ return this;
+ }
+
+ public SSLContextBuilder loadTrustMaterial(
+ final KeyStore truststore,
+ final TrustStrategy trustStrategy) throws NoSuchAlgorithmException, KeyStoreException {
+ final TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(
+ TrustManagerFactory.getDefaultAlgorithm());
+ tmfactory.init(truststore);
+ final TrustManager[] tms = tmfactory.getTrustManagers();
+ if (tms != null) {
+ if (trustStrategy != null) {
+ for (int i = 0; i < tms.length; i++) {
+ final TrustManager tm = tms[i];
+ if (tm instanceof X509TrustManager) {
+ tms[i] = new TrustManagerDelegate(
+ (X509TrustManager) tm, trustStrategy);
+ }
+ }
+ }
+ for (final TrustManager tm : tms) {
+ this.trustmanagers.add(tm);
+ }
+ }
+ return this;
+ }
+
+ public SSLContextBuilder loadTrustMaterial(
+ final KeyStore truststore) throws NoSuchAlgorithmException, KeyStoreException {
+ return loadTrustMaterial(truststore, null);
+ }
+
+ public SSLContextBuilder loadKeyMaterial(
+ final KeyStore keystore,
+ final char[] keyPassword)
+ throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException {
+ loadKeyMaterial(keystore, keyPassword, null);
+ return this;
+ }
+
+ public SSLContextBuilder loadKeyMaterial(
+ final KeyStore keystore,
+ final char[] keyPassword,
+ final PrivateKeyStrategy aliasStrategy)
+ throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException {
+ final KeyManagerFactory kmfactory = KeyManagerFactory.getInstance(
+ KeyManagerFactory.getDefaultAlgorithm());
+ kmfactory.init(keystore, keyPassword);
+ final KeyManager[] kms = kmfactory.getKeyManagers();
+ if (kms != null) {
+ if (aliasStrategy != null) {
+ for (int i = 0; i < kms.length; i++) {
+ final KeyManager km = kms[i];
+ if (km instanceof X509KeyManager) {
+ kms[i] = new KeyManagerDelegate(
+ (X509KeyManager) km, aliasStrategy);
+ }
+ }
+ }
+ for (final KeyManager km : kms) {
+ keymanagers.add(km);
+ }
+ }
+ return this;
+ }
+
+ public SSLContext build() throws NoSuchAlgorithmException, KeyManagementException {
+ final SSLContext sslcontext = SSLContext.getInstance(
+ this.protocol != null ? this.protocol : TLS);
+ sslcontext.init(
+ !keymanagers.isEmpty() ? keymanagers.toArray(new KeyManager[keymanagers.size()]) : null,
+ !trustmanagers.isEmpty() ? trustmanagers.toArray(new TrustManager[trustmanagers.size()]) : null,
+ secureRandom);
+ return sslcontext;
+ }
+
+ static class TrustManagerDelegate implements X509TrustManager {
+
+ private final X509TrustManager trustManager;
+ private final TrustStrategy trustStrategy;
+
+ TrustManagerDelegate(final X509TrustManager trustManager, final TrustStrategy trustStrategy) {
+ super();
+ this.trustManager = trustManager;
+ this.trustStrategy = trustStrategy;
+ }
+
+ public void checkClientTrusted(
+ final X509Certificate[] chain, final String authType) throws CertificateException {
+ this.trustManager.checkClientTrusted(chain, authType);
+ }
+
+ public void checkServerTrusted(
+ final X509Certificate[] chain, final String authType) throws CertificateException {
+ if (!this.trustStrategy.isTrusted(chain, authType)) {
+ this.trustManager.checkServerTrusted(chain, authType);
+ }
+ }
+
+ public X509Certificate[] getAcceptedIssuers() {
+ return this.trustManager.getAcceptedIssuers();
+ }
+
+ }
+
+ static class KeyManagerDelegate implements X509KeyManager {
+
+ private final X509KeyManager keyManager;
+ private final PrivateKeyStrategy aliasStrategy;
+
+ KeyManagerDelegate(final X509KeyManager keyManager, final PrivateKeyStrategy aliasStrategy) {
+ super();
+ this.keyManager = keyManager;
+ this.aliasStrategy = aliasStrategy;
+ }
+
+ public String[] getClientAliases(
+ final String keyType, final Principal[] issuers) {
+ return this.keyManager.getClientAliases(keyType, issuers);
+ }
+
+ public String chooseClientAlias(
+ final String[] keyTypes, final Principal[] issuers, final Socket socket) {
+ final Map<String, PrivateKeyDetails> validAliases = new HashMap<String, PrivateKeyDetails>();
+ for (final String keyType: keyTypes) {
+ final String[] aliases = this.keyManager.getClientAliases(keyType, issuers);
+ if (aliases != null) {
+ for (final String alias: aliases) {
+ validAliases.put(alias,
+ new PrivateKeyDetails(keyType, this.keyManager.getCertificateChain(alias)));
+ }
+ }
+ }
+ return this.aliasStrategy.chooseAlias(validAliases, socket);
+ }
+
+ public String[] getServerAliases(
+ final String keyType, final Principal[] issuers) {
+ return this.keyManager.getServerAliases(keyType, issuers);
+ }
+
+ public String chooseServerAlias(
+ final String keyType, final Principal[] issuers, final Socket socket) {
+ final Map<String, PrivateKeyDetails> validAliases = new HashMap<String, PrivateKeyDetails>();
+ final String[] aliases = this.keyManager.getServerAliases(keyType, issuers);
+ if (aliases != null) {
+ for (final String alias: aliases) {
+ validAliases.put(alias,
+ new PrivateKeyDetails(keyType, this.keyManager.getCertificateChain(alias)));
+ }
+ }
+ return this.aliasStrategy.chooseAlias(validAliases, socket);
+ }
+
+ public X509Certificate[] getCertificateChain(final String alias) {
+ return this.keyManager.getCertificateChain(alias);
+ }
+
+ public PrivateKey getPrivateKey(final String alias) {
+ return this.keyManager.getPrivateKey(alias);
+ }
+
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/SSLContexts.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/SSLContexts.java
new file mode 100644
index 0000000000..a87a501689
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/SSLContexts.java
@@ -0,0 +1,90 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.ssl;
+
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.net.ssl.SSLContext;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * {@link SSLContext} factory methods.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class SSLContexts {
+
+ /**
+ * Creates default factory based on the standard JSSE trust material
+ * (<code>cacerts</code> file in the security properties directory). System properties
+ * are not taken into consideration.
+ *
+ * @return the default SSL socket factory
+ */
+ public static SSLContext createDefault() throws SSLInitializationException {
+ try {
+ final SSLContext sslcontext = SSLContext.getInstance(SSLContextBuilder.TLS);
+ sslcontext.init(null, null, null);
+ return sslcontext;
+ } catch (final NoSuchAlgorithmException ex) {
+ throw new SSLInitializationException(ex.getMessage(), ex);
+ } catch (final KeyManagementException ex) {
+ throw new SSLInitializationException(ex.getMessage(), ex);
+ }
+ }
+
+ /**
+ * Creates default SSL context based on system properties. This method obtains
+ * default SSL context by calling <code>SSLContext.getInstance("Default")</code>.
+ * Please note that <code>Default</code> algorithm is supported as of Java 6.
+ * This method will fall back onto {@link #createDefault()} when
+ * <code>Default</code> algorithm is not available.
+ *
+ * @return default system SSL context
+ */
+ public static SSLContext createSystemDefault() throws SSLInitializationException {
+ try {
+ return SSLContext.getInstance("Default");
+ } catch (final NoSuchAlgorithmException ex) {
+ return createDefault();
+ }
+ }
+
+ /**
+ * Creates custom SSL context.
+ *
+ * @return default system SSL context
+ */
+ public static SSLContextBuilder custom() {
+ return new SSLContextBuilder();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/SSLInitializationException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/SSLInitializationException.java
new file mode 100644
index 0000000000..98cb5f9c32
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/SSLInitializationException.java
@@ -0,0 +1,37 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn.ssl;
+
+public class SSLInitializationException extends IllegalStateException {
+
+ private static final long serialVersionUID = -8243587425648536702L;
+
+ public SSLInitializationException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/SSLSocketFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/SSLSocketFactory.java
new file mode 100644
index 0000000000..5c514d780b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/SSLSocketFactory.java
@@ -0,0 +1,570 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.ssl;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.conn.ConnectTimeoutException;
+import ch.boye.httpclientandroidlib.conn.HttpInetSocketAddress;
+import ch.boye.httpclientandroidlib.conn.scheme.HostNameResolver;
+import ch.boye.httpclientandroidlib.conn.scheme.LayeredSchemeSocketFactory;
+import ch.boye.httpclientandroidlib.conn.scheme.LayeredSocketFactory;
+import ch.boye.httpclientandroidlib.conn.scheme.SchemeLayeredSocketFactory;
+import ch.boye.httpclientandroidlib.conn.socket.LayeredConnectionSocketFactory;
+import ch.boye.httpclientandroidlib.params.HttpConnectionParams;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+import ch.boye.httpclientandroidlib.util.TextUtils;
+
+/**
+ * Layered socket factory for TLS/SSL connections.
+ * <p>
+ * SSLSocketFactory can be used to validate the identity of the HTTPS server against a list of
+ * trusted certificates and to authenticate to the HTTPS server using a private key.
+ * <p>
+ * SSLSocketFactory will enable server authentication when supplied with
+ * a {@link KeyStore trust-store} file containing one or several trusted certificates. The client
+ * secure socket will reject the connection during the SSL session handshake if the target HTTPS
+ * server attempts to authenticate itself with a non-trusted certificate.
+ * <p>
+ * Use JDK keytool utility to import a trusted certificate and generate a trust-store file:
+ * <pre>
+ * keytool -import -alias "my server cert" -file server.crt -keystore my.truststore
+ * </pre>
+ * <p>
+ * In special cases the standard trust verification process can be bypassed by using a custom
+ * {@link TrustStrategy}. This interface is primarily intended for allowing self-signed
+ * certificates to be accepted as trusted without having to add them to the trust-store file.
+ * <p>
+ * SSLSocketFactory will enable client authentication when supplied with
+ * a {@link KeyStore key-store} file containing a private key/public certificate
+ * pair. The client secure socket will use the private key to authenticate
+ * itself to the target HTTPS server during the SSL session handshake if
+ * requested to do so by the server.
+ * The target HTTPS server will in its turn verify the certificate presented
+ * by the client in order to establish client's authenticity.
+ * <p>
+ * Use the following sequence of actions to generate a key-store file
+ * </p>
+ * <ul>
+ * <li>
+ * <p>
+ * Use JDK keytool utility to generate a new key
+ * <pre>keytool -genkey -v -alias "my client key" -validity 365 -keystore my.keystore</pre>
+ * For simplicity use the same password for the key as that of the key-store
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Issue a certificate signing request (CSR)
+ * <pre>keytool -certreq -alias "my client key" -file mycertreq.csr -keystore my.keystore</pre>
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Send the certificate request to the trusted Certificate Authority for signature.
+ * One may choose to act as her own CA and sign the certificate request using a PKI
+ * tool, such as OpenSSL.
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Import the trusted CA root certificate
+ * <pre>keytool -import -alias "my trusted ca" -file caroot.crt -keystore my.keystore</pre>
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Import the PKCS#7 file containg the complete certificate chain
+ * <pre>keytool -import -alias "my client key" -file mycert.p7 -keystore my.keystore</pre>
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Verify the content the resultant keystore file
+ * <pre>keytool -list -v -keystore my.keystore</pre>
+ * </p>
+ * </li>
+ * </ul>
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link SSLConnectionSocketFactory}.
+ */
+@ThreadSafe
+@Deprecated
+public class SSLSocketFactory implements LayeredConnectionSocketFactory, SchemeLayeredSocketFactory,
+ LayeredSchemeSocketFactory, LayeredSocketFactory {
+
+ public static final String TLS = "TLS";
+ public static final String SSL = "SSL";
+ public static final String SSLV2 = "SSLv2";
+
+ public static final X509HostnameVerifier ALLOW_ALL_HOSTNAME_VERIFIER
+ = new AllowAllHostnameVerifier();
+
+ public static final X509HostnameVerifier BROWSER_COMPATIBLE_HOSTNAME_VERIFIER
+ = new BrowserCompatHostnameVerifier();
+
+ public static final X509HostnameVerifier STRICT_HOSTNAME_VERIFIER
+ = new StrictHostnameVerifier();
+
+ /**
+ * Obtains default SSL socket factory with an SSL context based on the standard JSSE
+ * trust material (<code>cacerts</code> file in the security properties directory).
+ * System properties are not taken into consideration.
+ *
+ * @return default SSL socket factory
+ */
+ public static SSLSocketFactory getSocketFactory() throws SSLInitializationException {
+ return new SSLSocketFactory(
+ SSLContexts.createDefault(),
+ BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
+ }
+
+ private static String[] split(final String s) {
+ if (TextUtils.isBlank(s)) {
+ return null;
+ }
+ return s.split(" *, *");
+ }
+
+ /**
+ * Obtains default SSL socket factory with an SSL context based on system properties
+ * as described in
+ * <a href="http://docs.oracle.com/javase/1.5.0/docs/guide/security/jsse/JSSERefGuide.html">
+ * "JavaTM Secure Socket Extension (JSSE) Reference Guide for the JavaTM 2 Platform
+ * Standard Edition 5</a>
+ *
+ * @return default system SSL socket factory
+ */
+ public static SSLSocketFactory getSystemSocketFactory() throws SSLInitializationException {
+ return new SSLSocketFactory(
+ (javax.net.ssl.SSLSocketFactory) javax.net.ssl.SSLSocketFactory.getDefault(),
+ split(System.getProperty("https.protocols")),
+ split(System.getProperty("https.cipherSuites")),
+ BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
+ }
+
+ private final javax.net.ssl.SSLSocketFactory socketfactory;
+ private final HostNameResolver nameResolver;
+ // TODO: make final
+ private volatile X509HostnameVerifier hostnameVerifier;
+ private final String[] supportedProtocols;
+ private final String[] supportedCipherSuites;
+
+ public SSLSocketFactory(
+ final String algorithm,
+ final KeyStore keystore,
+ final String keyPassword,
+ final KeyStore truststore,
+ final SecureRandom random,
+ final HostNameResolver nameResolver)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ this(SSLContexts.custom()
+ .useProtocol(algorithm)
+ .setSecureRandom(random)
+ .loadKeyMaterial(keystore, keyPassword != null ? keyPassword.toCharArray() : null)
+ .loadTrustMaterial(truststore)
+ .build(),
+ nameResolver);
+ }
+
+ /**
+ * @since 4.1
+ */
+ public SSLSocketFactory(
+ final String algorithm,
+ final KeyStore keystore,
+ final String keyPassword,
+ final KeyStore truststore,
+ final SecureRandom random,
+ final TrustStrategy trustStrategy,
+ final X509HostnameVerifier hostnameVerifier)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ this(SSLContexts.custom()
+ .useProtocol(algorithm)
+ .setSecureRandom(random)
+ .loadKeyMaterial(keystore, keyPassword != null ? keyPassword.toCharArray() : null)
+ .loadTrustMaterial(truststore, trustStrategy)
+ .build(),
+ hostnameVerifier);
+ }
+
+ /**
+ * @since 4.1
+ */
+ public SSLSocketFactory(
+ final String algorithm,
+ final KeyStore keystore,
+ final String keyPassword,
+ final KeyStore truststore,
+ final SecureRandom random,
+ final X509HostnameVerifier hostnameVerifier)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ this(SSLContexts.custom()
+ .useProtocol(algorithm)
+ .setSecureRandom(random)
+ .loadKeyMaterial(keystore, keyPassword != null ? keyPassword.toCharArray() : null)
+ .loadTrustMaterial(truststore)
+ .build(),
+ hostnameVerifier);
+ }
+
+ public SSLSocketFactory(
+ final KeyStore keystore,
+ final String keystorePassword,
+ final KeyStore truststore)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ this(SSLContexts.custom()
+ .loadKeyMaterial(keystore, keystorePassword != null ? keystorePassword.toCharArray() : null)
+ .loadTrustMaterial(truststore)
+ .build(),
+ BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
+ }
+
+ public SSLSocketFactory(
+ final KeyStore keystore,
+ final String keystorePassword)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException{
+ this(SSLContexts.custom()
+ .loadKeyMaterial(keystore, keystorePassword != null ? keystorePassword.toCharArray() : null)
+ .build(),
+ BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
+ }
+
+ public SSLSocketFactory(
+ final KeyStore truststore)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ this(SSLContexts.custom()
+ .loadTrustMaterial(truststore)
+ .build(),
+ BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
+ }
+
+ /**
+ * @since 4.1
+ */
+ public SSLSocketFactory(
+ final TrustStrategy trustStrategy,
+ final X509HostnameVerifier hostnameVerifier)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ this(SSLContexts.custom()
+ .loadTrustMaterial(null, trustStrategy)
+ .build(),
+ hostnameVerifier);
+ }
+
+ /**
+ * @since 4.1
+ */
+ public SSLSocketFactory(
+ final TrustStrategy trustStrategy)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ this(SSLContexts.custom()
+ .loadTrustMaterial(null, trustStrategy)
+ .build(),
+ BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
+ }
+
+ public SSLSocketFactory(final SSLContext sslContext) {
+ this(sslContext, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
+ }
+
+ public SSLSocketFactory(
+ final SSLContext sslContext, final HostNameResolver nameResolver) {
+ super();
+ this.socketfactory = sslContext.getSocketFactory();
+ this.hostnameVerifier = BROWSER_COMPATIBLE_HOSTNAME_VERIFIER;
+ this.nameResolver = nameResolver;
+ this.supportedProtocols = null;
+ this.supportedCipherSuites = null;
+ }
+
+ /**
+ * @since 4.1
+ */
+ public SSLSocketFactory(
+ final SSLContext sslContext, final X509HostnameVerifier hostnameVerifier) {
+ this(Args.notNull(sslContext, "SSL context").getSocketFactory(),
+ null, null, hostnameVerifier);
+ }
+
+ /**
+ * @since 4.3
+ */
+ public SSLSocketFactory(
+ final SSLContext sslContext,
+ final String[] supportedProtocols,
+ final String[] supportedCipherSuites,
+ final X509HostnameVerifier hostnameVerifier) {
+ this(Args.notNull(sslContext, "SSL context").getSocketFactory(),
+ supportedProtocols, supportedCipherSuites, hostnameVerifier);
+ }
+
+ /**
+ * @since 4.2
+ */
+ public SSLSocketFactory(
+ final javax.net.ssl.SSLSocketFactory socketfactory,
+ final X509HostnameVerifier hostnameVerifier) {
+ this(socketfactory, null, null, hostnameVerifier);
+ }
+
+ /**
+ * @since 4.3
+ */
+ public SSLSocketFactory(
+ final javax.net.ssl.SSLSocketFactory socketfactory,
+ final String[] supportedProtocols,
+ final String[] supportedCipherSuites,
+ final X509HostnameVerifier hostnameVerifier) {
+ this.socketfactory = Args.notNull(socketfactory, "SSL socket factory");
+ this.supportedProtocols = supportedProtocols;
+ this.supportedCipherSuites = supportedCipherSuites;
+ this.hostnameVerifier = hostnameVerifier != null ? hostnameVerifier : BROWSER_COMPATIBLE_HOSTNAME_VERIFIER;
+ this.nameResolver = null;
+ }
+
+ /**
+ * @param params Optional parameters. Parameters passed to this method will have no effect.
+ * This method will create a unconnected instance of {@link Socket} class.
+ * @since 4.1
+ */
+ public Socket createSocket(final HttpParams params) throws IOException {
+ return createSocket((HttpContext) null);
+ }
+
+ public Socket createSocket() throws IOException {
+ return createSocket((HttpContext) null);
+ }
+
+ /**
+ * @since 4.1
+ */
+ public Socket connectSocket(
+ final Socket socket,
+ final InetSocketAddress remoteAddress,
+ final InetSocketAddress localAddress,
+ final HttpParams params) throws IOException, UnknownHostException, ConnectTimeoutException {
+ Args.notNull(remoteAddress, "Remote address");
+ Args.notNull(params, "HTTP parameters");
+ final HttpHost host;
+ if (remoteAddress instanceof HttpInetSocketAddress) {
+ host = ((HttpInetSocketAddress) remoteAddress).getHttpHost();
+ } else {
+ host = new HttpHost(remoteAddress.getHostName(), remoteAddress.getPort(), "https");
+ }
+ final int socketTimeout = HttpConnectionParams.getSoTimeout(params);
+ final int connectTimeout = HttpConnectionParams.getConnectionTimeout(params);
+ socket.setSoTimeout(socketTimeout);
+ return connectSocket(connectTimeout, socket, host, remoteAddress, localAddress, null);
+ }
+
+ /**
+ * Checks whether a socket connection is secure.
+ * This factory creates TLS/SSL socket connections
+ * which, by default, are considered secure.
+ * <br/>
+ * Derived classes may override this method to perform
+ * runtime checks, for example based on the cypher suite.
+ *
+ * @param sock the connected socket
+ *
+ * @return <code>true</code>
+ *
+ * @throws IllegalArgumentException if the argument is invalid
+ */
+ public boolean isSecure(final Socket sock) throws IllegalArgumentException {
+ Args.notNull(sock, "Socket");
+ Asserts.check(sock instanceof SSLSocket, "Socket not created by this factory");
+ Asserts.check(!sock.isClosed(), "Socket is closed");
+ return true;
+ }
+
+ /**
+ * @since 4.2
+ */
+ public Socket createLayeredSocket(
+ final Socket socket,
+ final String host,
+ final int port,
+ final HttpParams params) throws IOException, UnknownHostException {
+ return createLayeredSocket(socket, host, port, (HttpContext) null);
+ }
+
+ public Socket createLayeredSocket(
+ final Socket socket,
+ final String host,
+ final int port,
+ final boolean autoClose) throws IOException, UnknownHostException {
+ return createLayeredSocket(socket, host, port, (HttpContext) null);
+ }
+
+ public void setHostnameVerifier(final X509HostnameVerifier hostnameVerifier) {
+ Args.notNull(hostnameVerifier, "Hostname verifier");
+ this.hostnameVerifier = hostnameVerifier;
+ }
+
+ public X509HostnameVerifier getHostnameVerifier() {
+ return this.hostnameVerifier;
+ }
+
+ public Socket connectSocket(
+ final Socket socket,
+ final String host, final int port,
+ final InetAddress local, final int localPort,
+ final HttpParams params) throws IOException, UnknownHostException, ConnectTimeoutException {
+ final InetAddress remote;
+ if (this.nameResolver != null) {
+ remote = this.nameResolver.resolve(host);
+ } else {
+ remote = InetAddress.getByName(host);
+ }
+ InetSocketAddress localAddress = null;
+ if (local != null || localPort > 0) {
+ localAddress = new InetSocketAddress(local, localPort > 0 ? localPort : 0);
+ }
+ final InetSocketAddress remoteAddress = new HttpInetSocketAddress(
+ new HttpHost(host, port), remote, port);
+ return connectSocket(socket, remoteAddress, localAddress, params);
+ }
+
+ public Socket createSocket(
+ final Socket socket,
+ final String host, final int port,
+ final boolean autoClose) throws IOException, UnknownHostException {
+ return createLayeredSocket(socket, host, port, autoClose);
+ }
+
+ /**
+ * Performs any custom initialization for a newly created SSLSocket
+ * (before the SSL handshake happens).
+ *
+ * The default implementation is a no-op, but could be overridden to, e.g.,
+ * call {@link SSLSocket#setEnabledCipherSuites(java.lang.String[])}.
+ *
+ * @since 4.2
+ */
+ protected void prepareSocket(final SSLSocket socket) throws IOException {
+ }
+
+ private void internalPrepareSocket(final SSLSocket socket) throws IOException {
+ if (supportedProtocols != null) {
+ socket.setEnabledProtocols(supportedProtocols);
+ }
+ if (supportedCipherSuites != null) {
+ socket.setEnabledCipherSuites(supportedCipherSuites);
+ }
+ prepareSocket(socket);
+ }
+
+ public Socket createSocket(final HttpContext context) throws IOException {
+ final SSLSocket sock = (SSLSocket) this.socketfactory.createSocket();
+ internalPrepareSocket(sock);
+ return sock;
+ }
+
+ public Socket connectSocket(
+ final int connectTimeout,
+ final Socket socket,
+ final HttpHost host,
+ final InetSocketAddress remoteAddress,
+ final InetSocketAddress localAddress,
+ final HttpContext context) throws IOException {
+ Args.notNull(host, "HTTP host");
+ Args.notNull(remoteAddress, "Remote address");
+ final Socket sock = socket != null ? socket : createSocket(context);
+ if (localAddress != null) {
+ sock.bind(localAddress);
+ }
+ try {
+ sock.connect(remoteAddress, connectTimeout);
+ } catch (final IOException ex) {
+ try {
+ sock.close();
+ } catch (final IOException ignore) {
+ }
+ throw ex;
+ }
+ // Setup SSL layering if necessary
+ if (sock instanceof SSLSocket) {
+ final SSLSocket sslsock = (SSLSocket) sock;
+ sslsock.startHandshake();
+ verifyHostname(sslsock, host.getHostName());
+ return sock;
+ } else {
+ return createLayeredSocket(sock, host.getHostName(), remoteAddress.getPort(), context);
+ }
+ }
+
+ public Socket createLayeredSocket(
+ final Socket socket,
+ final String target,
+ final int port,
+ final HttpContext context) throws IOException {
+ final SSLSocket sslsock = (SSLSocket) this.socketfactory.createSocket(
+ socket,
+ target,
+ port,
+ true);
+ internalPrepareSocket(sslsock);
+ sslsock.startHandshake();
+ verifyHostname(sslsock, target);
+ return sslsock;
+ }
+
+ private void verifyHostname(final SSLSocket sslsock, final String hostname) throws IOException {
+ try {
+ this.hostnameVerifier.verify(hostname, sslsock);
+ // verifyHostName() didn't blowup - good!
+ } catch (final IOException iox) {
+ // close the socket before re-throwing the exception
+ try { sslsock.close(); } catch (final Exception x) { /*ignore*/ }
+ throw iox;
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/StrictHostnameVerifier.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/StrictHostnameVerifier.java
new file mode 100644
index 0000000000..a6328a10c6
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/StrictHostnameVerifier.java
@@ -0,0 +1,69 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.ssl;
+
+import javax.net.ssl.SSLException;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * The Strict HostnameVerifier works the same way as Sun Java 1.4, Sun
+ * Java 5, Sun Java 6-rc. It's also pretty close to IE6. This
+ * implementation appears to be compliant with RFC 2818 for dealing with
+ * wildcards.
+ * <p/>
+ * The hostname must match either the first CN, or any of the subject-alts.
+ * A wildcard can occur in the CN, and in any of the subject-alts. The
+ * one divergence from IE6 is how we only check the first CN. IE6 allows
+ * a match against any of the CNs present. We decided to follow in
+ * Sun Java 1.4's footsteps and only check the first CN. (If you need
+ * to check all the CN's, feel free to write your own implementation!).
+ * <p/>
+ * A wildcard such as "*.foo.com" matches only subdomains in the same
+ * level, for example "a.foo.com". It does not match deeper subdomains
+ * such as "a.b.foo.com".
+ *
+ *
+ * @since 4.0
+ */
+@Immutable
+public class StrictHostnameVerifier extends AbstractVerifier {
+
+ public final void verify(
+ final String host,
+ final String[] cns,
+ final String[] subjectAlts) throws SSLException {
+ verify(host, cns, subjectAlts, true);
+ }
+
+ @Override
+ public final String toString() {
+ return "STRICT";
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/TokenParser.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/TokenParser.java
new file mode 100644
index 0000000000..25bdcc5e6b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/TokenParser.java
@@ -0,0 +1,266 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.ssl;
+
+import java.util.BitSet;
+
+import ch.boye.httpclientandroidlib.message.ParserCursor;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Low level parser for header field elements. The parsing routines of this class are designed
+ * to produce near zero intermediate garbage and make no intermediate copies of input data.
+ * <p>
+ * This class is immutable and thread safe.
+ *
+ * Temporary package-private copy of ch.boye.httpclientandroidlib.message.TokenParser
+ */
+class TokenParser {
+
+ public static BitSet INIT_BITSET(final int ... b) {
+ final BitSet bitset = new BitSet();
+ for (final int aB : b) {
+ bitset.set(aB);
+ }
+ return bitset;
+ }
+
+ /** US-ASCII CR, carriage return (13) */
+ public static final char CR = '\r';
+
+ /** US-ASCII LF, line feed (10) */
+ public static final char LF = '\n';
+
+ /** US-ASCII SP, space (32) */
+ public static final char SP = ' ';
+
+ /** US-ASCII HT, horizontal-tab (9) */
+ public static final char HT = '\t';
+
+ /** Double quote */
+ public static final char DQUOTE = '\"';
+
+ /** Backward slash / escape character */
+ public static final char ESCAPE = '\\';
+
+ public static boolean isWhitespace(final char ch) {
+ return ch == SP || ch == HT || ch == CR || ch == LF;
+ }
+
+ public static final TokenParser INSTANCE = new TokenParser();
+
+ /**
+ * Extracts from the sequence of chars a token terminated with any of the given delimiters
+ * discarding semantically insignificant whitespace characters.
+ *
+ * @param buf buffer with the sequence of chars to be parsed
+ * @param cursor defines the bounds and current position of the buffer
+ * @param delimiters set of delimiting characters. Can be <code>null</code> if the token
+ * is not delimited by any character.
+ */
+ public String parseToken(final CharArrayBuffer buf, final ParserCursor cursor, final BitSet delimiters) {
+ final StringBuilder dst = new StringBuilder();
+ boolean whitespace = false;
+ while (!cursor.atEnd()) {
+ final char current = buf.charAt(cursor.getPos());
+ if (delimiters != null && delimiters.get(current)) {
+ break;
+ } else if (isWhitespace(current)) {
+ skipWhiteSpace(buf, cursor);
+ whitespace = true;
+ } else {
+ if (whitespace && dst.length() > 0) {
+ dst.append(' ');
+ }
+ copyContent(buf, cursor, delimiters, dst);
+ whitespace = false;
+ }
+ }
+ return dst.toString();
+ }
+
+ /**
+ * Extracts from the sequence of chars a value which can be enclosed in quote marks and
+ * terminated with any of the given delimiters discarding semantically insignificant
+ * whitespace characters.
+ *
+ * @param buf buffer with the sequence of chars to be parsed
+ * @param cursor defines the bounds and current position of the buffer
+ * @param delimiters set of delimiting characters. Can be <code>null</code> if the value
+ * is not delimited by any character.
+ */
+ public String parseValue(final CharArrayBuffer buf, final ParserCursor cursor, final BitSet delimiters) {
+ final StringBuilder dst = new StringBuilder();
+ boolean whitespace = false;
+ while (!cursor.atEnd()) {
+ final char current = buf.charAt(cursor.getPos());
+ if (delimiters != null && delimiters.get(current)) {
+ break;
+ } else if (isWhitespace(current)) {
+ skipWhiteSpace(buf, cursor);
+ whitespace = true;
+ } else if (current == DQUOTE) {
+ if (whitespace && dst.length() > 0) {
+ dst.append(' ');
+ }
+ copyQuotedContent(buf, cursor, dst);
+ whitespace = false;
+ } else {
+ if (whitespace && dst.length() > 0) {
+ dst.append(' ');
+ }
+ copyUnquotedContent(buf, cursor, delimiters, dst);
+ whitespace = false;
+ }
+ }
+ return dst.toString();
+ }
+
+ /**
+ * Skips semantically insignificant whitespace characters and moves the cursor to the closest
+ * non-whitespace character.
+ *
+ * @param buf buffer with the sequence of chars to be parsed
+ * @param cursor defines the bounds and current position of the buffer
+ */
+ public void skipWhiteSpace(final CharArrayBuffer buf, final ParserCursor cursor) {
+ int pos = cursor.getPos();
+ final int indexFrom = cursor.getPos();
+ final int indexTo = cursor.getUpperBound();
+ for (int i = indexFrom; i < indexTo; i++) {
+ final char current = buf.charAt(i);
+ if (!isWhitespace(current)) {
+ break;
+ } else {
+ pos++;
+ }
+ }
+ cursor.updatePos(pos);
+ }
+
+ /**
+ * Transfers content into the destination buffer until a whitespace character or any of
+ * the given delimiters is encountered.
+ *
+ * @param buf buffer with the sequence of chars to be parsed
+ * @param cursor defines the bounds and current position of the buffer
+ * @param delimiters set of delimiting characters. Can be <code>null</code> if the value
+ * is delimited by a whitespace only.
+ * @param dst destination buffer
+ */
+ public void copyContent(final CharArrayBuffer buf, final ParserCursor cursor, final BitSet delimiters,
+ final StringBuilder dst) {
+ int pos = cursor.getPos();
+ final int indexFrom = cursor.getPos();
+ final int indexTo = cursor.getUpperBound();
+ for (int i = indexFrom; i < indexTo; i++) {
+ final char current = buf.charAt(i);
+ if ((delimiters != null && delimiters.get(current)) || isWhitespace(current)) {
+ break;
+ } else {
+ pos++;
+ dst.append(current);
+ }
+ }
+ cursor.updatePos(pos);
+ }
+
+ /**
+ * Transfers content into the destination buffer until a whitespace character, a quote,
+ * or any of the given delimiters is encountered.
+ *
+ * @param buf buffer with the sequence of chars to be parsed
+ * @param cursor defines the bounds and current position of the buffer
+ * @param delimiters set of delimiting characters. Can be <code>null</code> if the value
+ * is delimited by a whitespace or a quote only.
+ * @param dst destination buffer
+ */
+ public void copyUnquotedContent(final CharArrayBuffer buf, final ParserCursor cursor,
+ final BitSet delimiters, final StringBuilder dst) {
+ int pos = cursor.getPos();
+ final int indexFrom = cursor.getPos();
+ final int indexTo = cursor.getUpperBound();
+ for (int i = indexFrom; i < indexTo; i++) {
+ final char current = buf.charAt(i);
+ if ((delimiters != null && delimiters.get(current))
+ || isWhitespace(current) || current == DQUOTE) {
+ break;
+ } else {
+ pos++;
+ dst.append(current);
+ }
+ }
+ cursor.updatePos(pos);
+ }
+
+ /**
+ * Transfers content enclosed with quote marks into the destination buffer.
+ *
+ * @param buf buffer with the sequence of chars to be parsed
+ * @param cursor defines the bounds and current position of the buffer
+ * @param dst destination buffer
+ */
+ public void copyQuotedContent(final CharArrayBuffer buf, final ParserCursor cursor,
+ final StringBuilder dst) {
+ if (cursor.atEnd()) {
+ return;
+ }
+ int pos = cursor.getPos();
+ int indexFrom = cursor.getPos();
+ final int indexTo = cursor.getUpperBound();
+ char current = buf.charAt(pos);
+ if (current != DQUOTE) {
+ return;
+ }
+ pos++;
+ indexFrom++;
+ boolean escaped = false;
+ for (int i = indexFrom; i < indexTo; i++, pos++) {
+ current = buf.charAt(i);
+ if (escaped) {
+ if (current != DQUOTE && current != ESCAPE) {
+ dst.append(ESCAPE);
+ }
+ dst.append(current);
+ escaped = false;
+ } else {
+ if (current == DQUOTE) {
+ pos++;
+ break;
+ }
+ if (current == ESCAPE) {
+ escaped = true;
+ } else if (current != CR && current != LF) {
+ dst.append(current);
+ }
+ }
+ }
+ cursor.updatePos(pos);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/TrustSelfSignedStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/TrustSelfSignedStrategy.java
new file mode 100644
index 0000000000..a3c23690b2
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/TrustSelfSignedStrategy.java
@@ -0,0 +1,45 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn.ssl;
+
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+/**
+ * A trust strategy that accepts self-signed certificates as trusted. Verification of all other
+ * certificates is done by the trust manager configured in the SSL context.
+ *
+ * @since 4.1
+ */
+public class TrustSelfSignedStrategy implements TrustStrategy {
+
+ public boolean isTrusted(
+ final X509Certificate[] chain, final String authType) throws CertificateException {
+ return chain.length == 1;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/TrustStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/TrustStrategy.java
new file mode 100644
index 0000000000..7f49145034
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/TrustStrategy.java
@@ -0,0 +1,57 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.conn.ssl;
+
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+/**
+ * A strategy to establish trustworthiness of certificates without consulting the trust manager
+ * configured in the actual SSL context. This interface can be used to override the standard
+ * JSSE certificate verification process.
+ *
+ * @since 4.1
+ */
+public interface TrustStrategy {
+
+ /**
+ * Determines whether the certificate chain can be trusted without consulting the trust manager
+ * configured in the actual SSL context. This method can be used to override the standard JSSE
+ * certificate verification process.
+ * <p>
+ * Please note that, if this method returns <code>false</code>, the trust manager configured
+ * in the actual SSL context can still clear the certificate as trusted.
+ *
+ * @param chain the peer certificate chain
+ * @param authType the authentication type based on the client certificate
+ * @return <code>true</code> if the certificate can be trusted without verification by
+ * the trust manager, <code>false</code> otherwise.
+ * @throws CertificateException thrown if the certificate is not trusted or invalid.
+ */
+ boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/X509HostnameVerifier.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/X509HostnameVerifier.java
new file mode 100644
index 0000000000..cd57c623ed
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/X509HostnameVerifier.java
@@ -0,0 +1,85 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.ssl;
+
+import java.io.IOException;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSocket;
+
+/**
+ * Interface for checking if a hostname matches the names stored inside the
+ * server's X.509 certificate. This interface extends
+ * {@link javax.net.ssl.HostnameVerifier}, but it is recommended to use
+ * methods added by X509HostnameVerifier.
+ *
+ * @since 4.0
+ */
+public interface X509HostnameVerifier extends HostnameVerifier {
+
+ /**
+ * Verifies that the host name is an acceptable match with the server's
+ * authentication scheme based on the given {@link SSLSocket}.
+ *
+ * @param host the host.
+ * @param ssl the SSL socket.
+ * @throws IOException if an I/O error occurs or the verification process
+ * fails.
+ */
+ void verify(String host, SSLSocket ssl) throws IOException;
+
+ /**
+ * Verifies that the host name is an acceptable match with the server's
+ * authentication scheme based on the given {@link X509Certificate}.
+ *
+ * @param host the host.
+ * @param cert the certificate.
+ * @throws SSLException if the verification process fails.
+ */
+ void verify(String host, X509Certificate cert) throws SSLException;
+
+ /**
+ * Checks to see if the supplied hostname matches any of the supplied CNs
+ * or "DNS" Subject-Alts. Most implementations only look at the first CN,
+ * and ignore any additional CNs. Most implementations do look at all of
+ * the "DNS" Subject-Alts. The CNs or Subject-Alts may contain wildcards
+ * according to RFC 2818.
+ *
+ * @param cns CN fields, in order, as extracted from the X.509
+ * certificate.
+ * @param subjectAlts Subject-Alt fields of type 2 ("DNS"), as extracted
+ * from the X.509 certificate.
+ * @param host The hostname to verify.
+ * @throws SSLException if the verification process fails.
+ */
+ void verify(String host, String[] cns, String[] subjectAlts)
+ throws SSLException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/package-info.java
new file mode 100644
index 0000000000..c05bf5be00
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Client TLS/SSL support.
+ */
+package ch.boye.httpclientandroidlib.conn.ssl;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/util/InetAddressUtils.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/util/InetAddressUtils.java
new file mode 100644
index 0000000000..ac9a8e2db3
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/util/InetAddressUtils.java
@@ -0,0 +1,123 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.conn.util;
+
+import java.util.regex.Pattern;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * A collection of utilities relating to InetAddresses.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class InetAddressUtils {
+
+ private InetAddressUtils() {
+ }
+
+ private static final String IPV4_BASIC_PATTERN_STRING =
+ "(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}" + // initial 3 fields, 0-255 followed by .
+ "([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])"; // final field, 0-255
+
+ private static final Pattern IPV4_PATTERN =
+ Pattern.compile("^" + IPV4_BASIC_PATTERN_STRING + "$");
+
+ private static final Pattern IPV4_MAPPED_IPV6_PATTERN = // TODO does not allow for redundant leading zeros
+ Pattern.compile("^::[fF]{4}:" + IPV4_BASIC_PATTERN_STRING + "$");
+
+ private static final Pattern IPV6_STD_PATTERN =
+ Pattern.compile(
+ "^[0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){7}$");
+
+ private static final Pattern IPV6_HEX_COMPRESSED_PATTERN =
+ Pattern.compile(
+ "^(([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4}){0,5})?)" + // 0-6 hex fields
+ "::" +
+ "(([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4}){0,5})?)$"); // 0-6 hex fields
+
+ /*
+ * The above pattern is not totally rigorous as it allows for more than 7 hex fields in total
+ */
+ private static final char COLON_CHAR = ':';
+
+ // Must not have more than 7 colons (i.e. 8 fields)
+ private static final int MAX_COLON_COUNT = 7;
+
+ /**
+ * Checks whether the parameter is a valid IPv4 address
+ *
+ * @param input the address string to check for validity
+ * @return true if the input parameter is a valid IPv4 address
+ */
+ public static boolean isIPv4Address(final String input) {
+ return IPV4_PATTERN.matcher(input).matches();
+ }
+
+ public static boolean isIPv4MappedIPv64Address(final String input) {
+ return IPV4_MAPPED_IPV6_PATTERN.matcher(input).matches();
+ }
+
+ /**
+ * Checks whether the parameter is a valid standard (non-compressed) IPv6 address
+ *
+ * @param input the address string to check for validity
+ * @return true if the input parameter is a valid standard (non-compressed) IPv6 address
+ */
+ public static boolean isIPv6StdAddress(final String input) {
+ return IPV6_STD_PATTERN.matcher(input).matches();
+ }
+
+ /**
+ * Checks whether the parameter is a valid compressed IPv6 address
+ *
+ * @param input the address string to check for validity
+ * @return true if the input parameter is a valid compressed IPv6 address
+ */
+ public static boolean isIPv6HexCompressedAddress(final String input) {
+ int colonCount = 0;
+ for(int i = 0; i < input.length(); i++) {
+ if (input.charAt(i) == COLON_CHAR) {
+ colonCount++;
+ }
+ }
+ return colonCount <= MAX_COLON_COUNT && IPV6_HEX_COMPRESSED_PATTERN.matcher(input).matches();
+ }
+
+ /**
+ * Checks whether the parameter is a valid IPv6 address (including compressed).
+ *
+ * @param input the address string to check for validity
+ * @return true if the input parameter is a valid standard or compressed IPv6 address
+ */
+ public static boolean isIPv6Address(final String input) {
+ return isIPv6StdAddress(input) || isIPv6HexCompressedAddress(input);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/util/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/util/package-info.java
new file mode 100644
index 0000000000..6db59b736b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/util/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Connection utility classes.
+ */
+package ch.boye.httpclientandroidlib.conn.util;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/ClientCookie.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/ClientCookie.java
new file mode 100644
index 0000000000..1909f0999d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/ClientCookie.java
@@ -0,0 +1,62 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.cookie;
+
+/**
+ * ClientCookie extends the standard {@link Cookie} interface with
+ * additional client specific functionality such ability to retrieve
+ * original cookie attributes exactly as they were specified by the
+ * origin server. This is important for generating the <tt>Cookie</tt>
+ * header because some cookie specifications require that the
+ * <tt>Cookie</tt> header should include certain attributes only if
+ * they were specified in the <tt>Set-Cookie</tt> header.
+ *
+ *
+ * @since 4.0
+ */
+public interface ClientCookie extends Cookie {
+
+ // RFC2109 attributes
+ public static final String VERSION_ATTR = "version";
+ public static final String PATH_ATTR = "path";
+ public static final String DOMAIN_ATTR = "domain";
+ public static final String MAX_AGE_ATTR = "max-age";
+ public static final String SECURE_ATTR = "secure";
+ public static final String COMMENT_ATTR = "comment";
+ public static final String EXPIRES_ATTR = "expires";
+
+ // RFC2965 attributes
+ public static final String PORT_ATTR = "port";
+ public static final String COMMENTURL_ATTR = "commenturl";
+ public static final String DISCARD_ATTR = "discard";
+
+ String getAttribute(String name);
+
+ boolean containsAttribute(String name);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/Cookie.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/Cookie.java
new file mode 100644
index 0000000000..9953ab794d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/Cookie.java
@@ -0,0 +1,137 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.cookie;
+
+import java.util.Date;
+
+/**
+ * Cookie interface represents a token or short packet of state information
+ * (also referred to as "magic-cookie") that the HTTP agent and the target
+ * server can exchange to maintain a session. In its simples form an HTTP
+ * cookie is merely a name / value pair.
+ *
+ * @since 4.0
+ */
+public interface Cookie {
+
+ /**
+ * Returns the name.
+ *
+ * @return String name The name
+ */
+ String getName();
+
+ /**
+ * Returns the value.
+ *
+ * @return String value The current value.
+ */
+ String getValue();
+
+ /**
+ * Returns the comment describing the purpose of this cookie, or
+ * <tt>null</tt> if no such comment has been defined.
+ *
+ * @return comment
+ */
+ String getComment();
+
+ /**
+ * If a user agent (web browser) presents this cookie to a user, the
+ * cookie's purpose will be described by the information at this URL.
+ */
+ String getCommentURL();
+
+ /**
+ * Returns the expiration {@link Date} of the cookie, or <tt>null</tt>
+ * if none exists.
+ * <p><strong>Note:</strong> the object returned by this method is
+ * considered immutable. Changing it (e.g. using setTime()) could result
+ * in undefined behaviour. Do so at your peril. </p>
+ * @return Expiration {@link Date}, or <tt>null</tt>.
+ */
+ Date getExpiryDate();
+
+ /**
+ * Returns <tt>false</tt> if the cookie should be discarded at the end
+ * of the "session"; <tt>true</tt> otherwise.
+ *
+ * @return <tt>false</tt> if the cookie should be discarded at the end
+ * of the "session"; <tt>true</tt> otherwise
+ */
+ boolean isPersistent();
+
+ /**
+ * Returns domain attribute of the cookie. The value of the Domain
+ * attribute specifies the domain for which the cookie is valid.
+ *
+ * @return the value of the domain attribute.
+ */
+ String getDomain();
+
+ /**
+ * Returns the path attribute of the cookie. The value of the Path
+ * attribute specifies the subset of URLs on the origin server to which
+ * this cookie applies.
+ *
+ * @return The value of the path attribute.
+ */
+ String getPath();
+
+ /**
+ * Get the Port attribute. It restricts the ports to which a cookie
+ * may be returned in a Cookie request header.
+ */
+ int[] getPorts();
+
+ /**
+ * Indicates whether this cookie requires a secure connection.
+ *
+ * @return <code>true</code> if this cookie should only be sent
+ * over secure connections, <code>false</code> otherwise.
+ */
+ boolean isSecure();
+
+ /**
+ * Returns the version of the cookie specification to which this
+ * cookie conforms.
+ *
+ * @return the version of the cookie.
+ */
+ int getVersion();
+
+ /**
+ * Returns true if this cookie has expired.
+ * @param date Current time
+ *
+ * @return <tt>true</tt> if the cookie has expired.
+ */
+ boolean isExpired(final Date date);
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieAttributeHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieAttributeHandler.java
new file mode 100644
index 0000000000..3d94c8a6f6
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieAttributeHandler.java
@@ -0,0 +1,73 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.cookie;
+
+/**
+ * This interface represents a cookie attribute handler responsible
+ * for parsing, validating, and matching a specific cookie attribute,
+ * such as path, domain, port, etc.
+ *
+ * Different cookie specifications can provide a specific
+ * implementation for this class based on their cookie handling
+ * rules.
+ *
+ *
+ * @since 4.0
+ */
+public interface CookieAttributeHandler {
+
+ /**
+ * Parse the given cookie attribute value and update the corresponding
+ * {@link ch.boye.httpclientandroidlib.cookie.Cookie} property.
+ *
+ * @param cookie {@link ch.boye.httpclientandroidlib.cookie.Cookie} to be updated
+ * @param value cookie attribute value from the cookie response header
+ */
+ void parse(SetCookie cookie, String value)
+ throws MalformedCookieException;
+
+ /**
+ * Peforms cookie validation for the given attribute value.
+ *
+ * @param cookie {@link ch.boye.httpclientandroidlib.cookie.Cookie} to validate
+ * @param origin the cookie source to validate against
+ * @throws MalformedCookieException if cookie validation fails for this attribute
+ */
+ void validate(Cookie cookie, CookieOrigin origin)
+ throws MalformedCookieException;
+
+ /**
+ * Matches the given value (property of the destination host where request is being
+ * submitted) with the corresponding cookie attribute.
+ *
+ * @param cookie {@link ch.boye.httpclientandroidlib.cookie.Cookie} to match
+ * @param origin the cookie source to match against
+ * @return <tt>true</tt> if the match is successful; <tt>false</tt> otherwise
+ */
+ boolean match(Cookie cookie, CookieOrigin origin);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieIdentityComparator.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieIdentityComparator.java
new file mode 100644
index 0000000000..863640e69d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieIdentityComparator.java
@@ -0,0 +1,80 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.cookie;
+
+import java.io.Serializable;
+import java.util.Comparator;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * This cookie comparator can be used to compare identity of cookies.
+ * <p>
+ * Cookies are considered identical if their names are equal and
+ * their domain attributes match ignoring case.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class CookieIdentityComparator implements Serializable, Comparator<Cookie> {
+
+ private static final long serialVersionUID = 4466565437490631532L;
+
+ public int compare(final Cookie c1, final Cookie c2) {
+ int res = c1.getName().compareTo(c2.getName());
+ if (res == 0) {
+ // do not differentiate empty and null domains
+ String d1 = c1.getDomain();
+ if (d1 == null) {
+ d1 = "";
+ } else if (d1.indexOf('.') == -1) {
+ d1 = d1 + ".local";
+ }
+ String d2 = c2.getDomain();
+ if (d2 == null) {
+ d2 = "";
+ } else if (d2.indexOf('.') == -1) {
+ d2 = d2 + ".local";
+ }
+ res = d1.compareToIgnoreCase(d2);
+ }
+ if (res == 0) {
+ String p1 = c1.getPath();
+ if (p1 == null) {
+ p1 = "/";
+ }
+ String p2 = c2.getPath();
+ if (p2 == null) {
+ p2 = "/";
+ }
+ res = p1.compareTo(p2);
+ }
+ return res;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieOrigin.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieOrigin.java
new file mode 100644
index 0000000000..aef29023da
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieOrigin.java
@@ -0,0 +1,94 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.cookie;
+
+import java.util.Locale;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * CookieOrigin class encapsulates details of an origin server that
+ * are relevant when parsing, validating or matching HTTP cookies.
+ *
+ * @since 4.0
+ */
+@Immutable
+public final class CookieOrigin {
+
+ private final String host;
+ private final int port;
+ private final String path;
+ private final boolean secure;
+
+ public CookieOrigin(final String host, final int port, final String path, final boolean secure) {
+ super();
+ Args.notBlank(host, "Host");
+ Args.notNegative(port, "Port");
+ Args.notNull(path, "Path");
+ this.host = host.toLowerCase(Locale.ENGLISH);
+ this.port = port;
+ if (path.trim().length() != 0) {
+ this.path = path;
+ } else {
+ this.path = "/";
+ }
+ this.secure = secure;
+ }
+
+ public String getHost() {
+ return this.host;
+ }
+
+ public String getPath() {
+ return this.path;
+ }
+
+ public int getPort() {
+ return this.port;
+ }
+
+ public boolean isSecure() {
+ return this.secure;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder buffer = new StringBuilder();
+ buffer.append('[');
+ if (this.secure) {
+ buffer.append("(secure)");
+ }
+ buffer.append(this.host);
+ buffer.append(':');
+ buffer.append(Integer.toString(this.port));
+ buffer.append(this.path);
+ buffer.append(']');
+ return buffer.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookiePathComparator.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookiePathComparator.java
new file mode 100644
index 0000000000..984bb850f2
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookiePathComparator.java
@@ -0,0 +1,81 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.cookie;
+
+import java.io.Serializable;
+import java.util.Comparator;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * This cookie comparator ensures that multiple cookies satisfying
+ * a common criteria are ordered in the <tt>Cookie</tt> header such
+ * that those with more specific Path attributes precede those with
+ * less specific.
+ *
+ * <p>
+ * This comparator assumes that Path attributes of two cookies
+ * path-match a commmon request-URI. Otherwise, the result of the
+ * comparison is undefined.
+ * </p>
+ *
+ *
+ * @since 4.0
+ */
+@Immutable
+public class CookiePathComparator implements Serializable, Comparator<Cookie> {
+
+ private static final long serialVersionUID = 7523645369616405818L;
+
+ private String normalizePath(final Cookie cookie) {
+ String path = cookie.getPath();
+ if (path == null) {
+ path = "/";
+ }
+ if (!path.endsWith("/")) {
+ path = path + '/';
+ }
+ return path;
+ }
+
+ public int compare(final Cookie c1, final Cookie c2) {
+ final String path1 = normalizePath(c1);
+ final String path2 = normalizePath(c2);
+ if (path1.equals(path2)) {
+ return 0;
+ } else if (path1.startsWith(path2)) {
+ return -1;
+ } else if (path2.startsWith(path1)) {
+ return 1;
+ } else {
+ // Does not really matter
+ return 0;
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieRestrictionViolationException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieRestrictionViolationException.java
new file mode 100644
index 0000000000..1e3f2d70c8
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieRestrictionViolationException.java
@@ -0,0 +1,61 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.cookie;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Signals that a cookie violates a restriction imposed by the cookie
+ * specification.
+ *
+ * @since 4.1
+ */
+@Immutable
+public class CookieRestrictionViolationException extends MalformedCookieException {
+
+ private static final long serialVersionUID = 7371235577078589013L;
+
+ /**
+ * Creates a new CookeFormatViolationException with a <tt>null</tt> detail
+ * message.
+ */
+ public CookieRestrictionViolationException() {
+ super();
+ }
+
+ /**
+ * Creates a new CookeRestrictionViolationException with a specified
+ * message string.
+ *
+ * @param message The exception detail message
+ */
+ public CookieRestrictionViolationException(final String message) {
+ super(message);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieSpec.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieSpec.java
new file mode 100644
index 0000000000..c222e5e725
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieSpec.java
@@ -0,0 +1,109 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.cookie;
+
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.Header;
+
+/**
+ * Defines the cookie management specification.
+ * <p>Cookie management specification must define
+ * <ul>
+ * <li> rules of parsing "Set-Cookie" header
+ * <li> rules of validation of parsed cookies
+ * <li> formatting of "Cookie" header
+ * </ul>
+ * for a given host, port and path of origin
+ *
+ *
+ * @since 4.0
+ */
+public interface CookieSpec {
+
+ /**
+ * Returns version of the state management this cookie specification
+ * conforms to.
+ *
+ * @return version of the state management specification
+ */
+ int getVersion();
+
+ /**
+ * Parse the <tt>"Set-Cookie"</tt> Header into an array of Cookies.
+ *
+ * <p>This method will not perform the validation of the resultant
+ * {@link Cookie}s</p>
+ *
+ * @see #validate
+ *
+ * @param header the <tt>Set-Cookie</tt> received from the server
+ * @param origin details of the cookie origin
+ * @return an array of <tt>Cookie</tt>s parsed from the header
+ * @throws MalformedCookieException if an exception occurs during parsing
+ */
+ List<Cookie> parse(Header header, CookieOrigin origin) throws MalformedCookieException;
+
+ /**
+ * Validate the cookie according to validation rules defined by the
+ * cookie specification.
+ *
+ * @param cookie the Cookie to validate
+ * @param origin details of the cookie origin
+ * @throws MalformedCookieException if the cookie is invalid
+ */
+ void validate(Cookie cookie, CookieOrigin origin) throws MalformedCookieException;
+
+ /**
+ * Determines if a Cookie matches the target location.
+ *
+ * @param cookie the Cookie to be matched
+ * @param origin the target to test against
+ *
+ * @return <tt>true</tt> if the cookie should be submitted with a request
+ * with given attributes, <tt>false</tt> otherwise.
+ */
+ boolean match(Cookie cookie, CookieOrigin origin);
+
+ /**
+ * Create <tt>"Cookie"</tt> headers for an array of Cookies.
+ *
+ * @param cookies the Cookies format into a Cookie header
+ * @return a Header for the given Cookies.
+ * @throws IllegalArgumentException if an input parameter is illegal
+ */
+ List<Header> formatCookies(List<Cookie> cookies);
+
+ /**
+ * Returns a request header identifying what version of the state management
+ * specification is understood. May be <code>null</code> if the cookie
+ * specification does not support <tt>Cookie2</tt> header.
+ */
+ Header getVersionHeader();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieSpecFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieSpecFactory.java
new file mode 100644
index 0000000000..fd7851626e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieSpecFactory.java
@@ -0,0 +1,51 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.cookie;
+
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+/**
+ * Factory for {@link CookieSpec} implementations.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link CookieSpecProvider}
+ */
+@Deprecated
+public interface CookieSpecFactory {
+
+ /**
+ * Creates an instance of {@link CookieSpec} using given HTTP parameters.
+ *
+ * @param params HTTP parameters.
+ *
+ * @return cookie spec.
+ */
+ CookieSpec newInstance(HttpParams params);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieSpecProvider.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieSpecProvider.java
new file mode 100644
index 0000000000..921adfba4d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieSpecProvider.java
@@ -0,0 +1,46 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.cookie;
+
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * Factory for {@link CookieSpec} implementations.
+ *
+ * @since 4.3
+ */
+public interface CookieSpecProvider {
+
+ /**
+ * Creates an instance of {@link CookieSpec}.
+ *
+ * @return auth scheme.
+ */
+ CookieSpec create(HttpContext context);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieSpecRegistry.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieSpecRegistry.java
new file mode 100644
index 0000000000..adbfe863ab
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/CookieSpecRegistry.java
@@ -0,0 +1,167 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.cookie;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.config.Lookup;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.ExecutionContext;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Cookie specification registry that can be used to obtain the corresponding
+ * cookie specification implementation for a given type of type or version of
+ * cookie.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.config.Registry}.
+ */
+@ThreadSafe
+@Deprecated
+public final class CookieSpecRegistry implements Lookup<CookieSpecProvider> {
+
+ private final ConcurrentHashMap<String,CookieSpecFactory> registeredSpecs;
+
+ public CookieSpecRegistry() {
+ super();
+ this.registeredSpecs = new ConcurrentHashMap<String,CookieSpecFactory>();
+ }
+
+ /**
+ * Registers a {@link CookieSpecFactory} with the given identifier.
+ * If a specification with the given name already exists it will be overridden.
+ * This nameis the same one used to retrieve the {@link CookieSpecFactory}
+ * from {@link #getCookieSpec(String)}.
+ *
+ * @param name the identifier for this specification
+ * @param factory the {@link CookieSpecFactory} class to register
+ *
+ * @see #getCookieSpec(String)
+ */
+ public void register(final String name, final CookieSpecFactory factory) {
+ Args.notNull(name, "Name");
+ Args.notNull(factory, "Cookie spec factory");
+ registeredSpecs.put(name.toLowerCase(Locale.ENGLISH), factory);
+ }
+
+ /**
+ * Unregisters the {@link CookieSpecFactory} with the given ID.
+ *
+ * @param id the identifier of the {@link CookieSpec cookie specification} to unregister
+ */
+ public void unregister(final String id) {
+ Args.notNull(id, "Id");
+ registeredSpecs.remove(id.toLowerCase(Locale.ENGLISH));
+ }
+
+ /**
+ * Gets the {@link CookieSpec cookie specification} with the given ID.
+ *
+ * @param name the {@link CookieSpec cookie specification} identifier
+ * @param params the {@link HttpParams HTTP parameters} for the cookie
+ * specification.
+ *
+ * @return {@link CookieSpec cookie specification}
+ *
+ * @throws IllegalStateException if a policy with the given name cannot be found
+ */
+ public CookieSpec getCookieSpec(final String name, final HttpParams params)
+ throws IllegalStateException {
+
+ Args.notNull(name, "Name");
+ final CookieSpecFactory factory = registeredSpecs.get(name.toLowerCase(Locale.ENGLISH));
+ if (factory != null) {
+ return factory.newInstance(params);
+ } else {
+ throw new IllegalStateException("Unsupported cookie spec: " + name);
+ }
+ }
+
+ /**
+ * Gets the {@link CookieSpec cookie specification} with the given name.
+ *
+ * @param name the {@link CookieSpec cookie specification} identifier
+ *
+ * @return {@link CookieSpec cookie specification}
+ *
+ * @throws IllegalStateException if a policy with the given name cannot be found
+ */
+ public CookieSpec getCookieSpec(final String name)
+ throws IllegalStateException {
+ return getCookieSpec(name, null);
+ }
+
+ /**
+ * Obtains a list containing the names of all registered {@link CookieSpec cookie
+ * specs}.
+ *
+ * Note that the DEFAULT policy (if present) is likely to be the same
+ * as one of the other policies, but does not have to be.
+ *
+ * @return list of registered cookie spec names
+ */
+ public List<String> getSpecNames(){
+ return new ArrayList<String>(registeredSpecs.keySet());
+ }
+
+ /**
+ * Populates the internal collection of registered {@link CookieSpec cookie
+ * specs} with the content of the map passed as a parameter.
+ *
+ * @param map cookie specs
+ */
+ public void setItems(final Map<String, CookieSpecFactory> map) {
+ if (map == null) {
+ return;
+ }
+ registeredSpecs.clear();
+ registeredSpecs.putAll(map);
+ }
+
+ public CookieSpecProvider lookup(final String name) {
+ return new CookieSpecProvider() {
+
+ public CookieSpec create(final HttpContext context) {
+ final HttpRequest request = (HttpRequest) context.getAttribute(
+ ExecutionContext.HTTP_REQUEST);
+ return getCookieSpec(name, request.getParams());
+ }
+
+ };
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/MalformedCookieException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/MalformedCookieException.java
new file mode 100644
index 0000000000..dba7b9b254
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/MalformedCookieException.java
@@ -0,0 +1,71 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.cookie;
+
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Signals that a cookie is in some way invalid or illegal in a given
+ * context
+ *
+ *
+ * @since 4.0
+ */
+@Immutable
+public class MalformedCookieException extends ProtocolException {
+
+ private static final long serialVersionUID = -6695462944287282185L;
+
+ /**
+ * Creates a new MalformedCookieException with a <tt>null</tt> detail message.
+ */
+ public MalformedCookieException() {
+ super();
+ }
+
+ /**
+ * Creates a new MalformedCookieException with a specified message string.
+ *
+ * @param message The exception detail message
+ */
+ public MalformedCookieException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new MalformedCookieException with the specified detail message and cause.
+ *
+ * @param message the exception detail message
+ * @param cause the <tt>Throwable</tt> that caused this exception, or <tt>null</tt>
+ * if the cause is unavailable, unknown, or not a <tt>Throwable</tt>
+ */
+ public MalformedCookieException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/SM.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/SM.java
new file mode 100644
index 0000000000..463da4d25a
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/SM.java
@@ -0,0 +1,43 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.cookie;
+
+/**
+ * Constants and static helpers related to the HTTP state management.
+ *
+ *
+ * @since 4.0
+ */
+public interface SM {
+
+ public static final String COOKIE = "Cookie";
+ public static final String COOKIE2 = "Cookie2";
+ public static final String SET_COOKIE = "Set-Cookie";
+ public static final String SET_COOKIE2 = "Set-Cookie2";
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/SetCookie.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/SetCookie.java
new file mode 100644
index 0000000000..5bd5b7121f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/SetCookie.java
@@ -0,0 +1,109 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.cookie;
+
+import java.util.Date;
+
+/**
+ * This interface represents a <code>Set-Cookie</code> response header sent by the
+ * origin server to the HTTP agent in order to maintain a conversational state.
+ *
+ * @since 4.0
+ */
+public interface SetCookie extends Cookie {
+
+ void setValue(String value);
+
+ /**
+ * If a user agent (web browser) presents this cookie to a user, the
+ * cookie's purpose will be described using this comment.
+ *
+ * @param comment
+ *
+ * @see #getComment()
+ */
+ void setComment(String comment);
+
+ /**
+ * Sets expiration date.
+ * <p><strong>Note:</strong> the object returned by this method is considered
+ * immutable. Changing it (e.g. using setTime()) could result in undefined
+ * behaviour. Do so at your peril.</p>
+ *
+ * @param expiryDate the {@link Date} after which this cookie is no longer valid.
+ *
+ * @see Cookie#getExpiryDate
+ *
+ */
+ void setExpiryDate (Date expiryDate);
+
+ /**
+ * Sets the domain attribute.
+ *
+ * @param domain The value of the domain attribute
+ *
+ * @see Cookie#getDomain
+ */
+ void setDomain(String domain);
+
+ /**
+ * Sets the path attribute.
+ *
+ * @param path The value of the path attribute
+ *
+ * @see Cookie#getPath
+ *
+ */
+ void setPath(String path);
+
+ /**
+ * Sets the secure attribute of the cookie.
+ * <p>
+ * When <tt>true</tt> the cookie should only be sent
+ * using a secure protocol (https). This should only be set when
+ * the cookie's originating server used a secure protocol to set the
+ * cookie's value.
+ *
+ * @param secure The value of the secure attribute
+ *
+ * @see #isSecure()
+ */
+ void setSecure (boolean secure);
+
+ /**
+ * Sets the version of the cookie specification to which this
+ * cookie conforms.
+ *
+ * @param version the version of the cookie.
+ *
+ * @see Cookie#getVersion
+ */
+ void setVersion(int version);
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/SetCookie2.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/SetCookie2.java
new file mode 100644
index 0000000000..bc35080590
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/SetCookie2.java
@@ -0,0 +1,60 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.cookie;
+
+/**
+ * This interface represents a <code>Set-Cookie2</code> response header sent by the
+ * origin server to the HTTP agent in order to maintain a conversational state.
+ *
+ * @since 4.0
+ */
+public interface SetCookie2 extends SetCookie {
+
+ /**
+ * If a user agent (web browser) presents this cookie to a user, the
+ * cookie's purpose will be described by the information at this URL.
+ */
+ void setCommentURL(String commentURL);
+
+ /**
+ * Sets the Port attribute. It restricts the ports to which a cookie
+ * may be returned in a Cookie request header.
+ */
+ void setPorts(int[] ports);
+
+ /**
+ * Set the Discard attribute.
+ *
+ * Note: <tt>Discard</tt> attribute overrides <tt>Max-age</tt>.
+ *
+ * @see #isPersistent()
+ */
+ void setDiscard(boolean discard);
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/package-info.java
new file mode 100644
index 0000000000..80a8317096
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Client HTTP state management APIs.
+ */
+package ch.boye.httpclientandroidlib.cookie;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/params/CookieSpecPNames.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/params/CookieSpecPNames.java
new file mode 100644
index 0000000000..a93a26f163
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/params/CookieSpecPNames.java
@@ -0,0 +1,65 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.cookie.params;
+
+/**
+ * Parameter names for HTTP cookie management classes.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use constructor parameters of {@link
+ * ch.boye.httpclientandroidlib.cookie.CookieSpecProvider}s.
+ */
+@Deprecated
+public interface CookieSpecPNames {
+
+ /**
+ * Defines valid date patterns to be used for parsing non-standard
+ * <code>expires</code> attribute. Only required for compatibility
+ * with non-compliant servers that still use <code>expires</code>
+ * defined in the Netscape draft instead of the standard
+ * <code>max-age</code> attribute.
+ * <p>
+ * This parameter expects a value of type {@link java.util.Collection}.
+ * The collection elements must be of type {@link String} compatible
+ * with the syntax of {@link java.text.SimpleDateFormat}.
+ * </p>
+ */
+ public static final String DATE_PATTERNS = "http.protocol.cookie-datepatterns";
+
+ /**
+ * Defines whether cookies should be forced into a single
+ * <code>Cookie</code> request header. Otherwise, each cookie is formatted
+ * as a separate <code>Cookie</code> header.
+ * <p>
+ * This parameter expects a value of type {@link Boolean}.
+ * </p>
+ */
+ public static final String SINGLE_COOKIE_HEADER = "http.protocol.single-cookie-header";
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/params/CookieSpecParamBean.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/params/CookieSpecParamBean.java
new file mode 100644
index 0000000000..0e007a54fa
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/params/CookieSpecParamBean.java
@@ -0,0 +1,62 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.cookie.params;
+
+import java.util.Collection;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.params.HttpAbstractParamBean;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+/**
+ * This is a Java Bean class that can be used to wrap an instance of
+ * {@link HttpParams} and manipulate HTTP cookie parameters using Java Beans
+ * conventions.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use constructor parameters of {@link
+ * ch.boye.httpclientandroidlib.cookie.CookieSpecProvider}s.
+ */
+@Deprecated
+@NotThreadSafe
+public class CookieSpecParamBean extends HttpAbstractParamBean {
+
+ public CookieSpecParamBean (final HttpParams params) {
+ super(params);
+ }
+
+ public void setDatePatterns (final Collection <String> patterns) {
+ params.setParameter(CookieSpecPNames.DATE_PATTERNS, patterns);
+ }
+
+ public void setSingleHeader (final boolean singleHeader) {
+ params.setBooleanParameter(CookieSpecPNames.SINGLE_COOKIE_HEADER, singleHeader);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/params/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/params/package-info.java
new file mode 100644
index 0000000000..9314e1e633
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/cookie/params/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Deprecated.
+ * @deprecated (4.3).
+ */
+package ch.boye.httpclientandroidlib.cookie.params;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/AbstractHttpEntity.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/AbstractHttpEntity.java
new file mode 100644
index 0000000000..9fc32fa291
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/AbstractHttpEntity.java
@@ -0,0 +1,191 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.message.BasicHeader;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+
+/**
+ * Abstract base class for entities.
+ * Provides the commonly used attributes for streamed and self-contained
+ * implementations of {@link HttpEntity HttpEntity}.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public abstract class AbstractHttpEntity implements HttpEntity {
+
+ /**
+ * Buffer size for output stream processing.
+ *
+ * @since 4.3
+ */
+ protected static final int OUTPUT_BUFFER_SIZE = 4096;
+
+ protected Header contentType;
+ protected Header contentEncoding;
+ protected boolean chunked;
+
+ /**
+ * Protected default constructor.
+ * The contentType, contentEncoding and chunked attributes of the created object are set to
+ * <code>null</code>, <code>null</code> and <code>false</code>, respectively.
+ */
+ protected AbstractHttpEntity() {
+ super();
+ }
+
+
+ /**
+ * Obtains the Content-Type header.
+ * The default implementation returns the value of the
+ * {@link #contentType contentType} attribute.
+ *
+ * @return the Content-Type header, or <code>null</code>
+ */
+ public Header getContentType() {
+ return this.contentType;
+ }
+
+
+ /**
+ * Obtains the Content-Encoding header.
+ * The default implementation returns the value of the
+ * {@link #contentEncoding contentEncoding} attribute.
+ *
+ * @return the Content-Encoding header, or <code>null</code>
+ */
+ public Header getContentEncoding() {
+ return this.contentEncoding;
+ }
+
+ /**
+ * Obtains the 'chunked' flag.
+ * The default implementation returns the value of the
+ * {@link #chunked chunked} attribute.
+ *
+ * @return the 'chunked' flag
+ */
+ public boolean isChunked() {
+ return this.chunked;
+ }
+
+
+ /**
+ * Specifies the Content-Type header.
+ * The default implementation sets the value of the
+ * {@link #contentType contentType} attribute.
+ *
+ * @param contentType the new Content-Encoding header, or
+ * <code>null</code> to unset
+ */
+ public void setContentType(final Header contentType) {
+ this.contentType = contentType;
+ }
+
+ /**
+ * Specifies the Content-Type header, as a string.
+ * The default implementation calls
+ * {@link #setContentType(Header) setContentType(Header)}.
+ *
+ * @param ctString the new Content-Type header, or
+ * <code>null</code> to unset
+ */
+ public void setContentType(final String ctString) {
+ Header h = null;
+ if (ctString != null) {
+ h = new BasicHeader(HTTP.CONTENT_TYPE, ctString);
+ }
+ setContentType(h);
+ }
+
+
+ /**
+ * Specifies the Content-Encoding header.
+ * The default implementation sets the value of the
+ * {@link #contentEncoding contentEncoding} attribute.
+ *
+ * @param contentEncoding the new Content-Encoding header, or
+ * <code>null</code> to unset
+ */
+ public void setContentEncoding(final Header contentEncoding) {
+ this.contentEncoding = contentEncoding;
+ }
+
+ /**
+ * Specifies the Content-Encoding header, as a string.
+ * The default implementation calls
+ * {@link #setContentEncoding(Header) setContentEncoding(Header)}.
+ *
+ * @param ceString the new Content-Encoding header, or
+ * <code>null</code> to unset
+ */
+ public void setContentEncoding(final String ceString) {
+ Header h = null;
+ if (ceString != null) {
+ h = new BasicHeader(HTTP.CONTENT_ENCODING, ceString);
+ }
+ setContentEncoding(h);
+ }
+
+
+ /**
+ * Specifies the 'chunked' flag.
+ * <p>
+ * Note that the chunked setting is a hint only.
+ * If using HTTP/1.0, chunking is never performed.
+ * Otherwise, even if chunked is false, HttpClient must
+ * use chunk coding if the entity content length is
+ * unknown (-1).
+ * <p>
+ * The default implementation sets the value of the
+ * {@link #chunked chunked} attribute.
+ *
+ * @param b the new 'chunked' flag
+ */
+ public void setChunked(final boolean b) {
+ this.chunked = b;
+ }
+
+
+ /**
+ * The default implementation does not consume anything.
+ *
+ * @deprecated (4.1) Either use {@link #getContent()} and call {@link java.io.InputStream#close()} on that;
+ * otherwise call {@link #writeTo(java.io.OutputStream)} which is required to free the resources.
+ */
+ @Deprecated
+ public void consumeContent() throws IOException {
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/BasicHttpEntity.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/BasicHttpEntity.java
new file mode 100644
index 0000000000..74a281b2a4
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/BasicHttpEntity.java
@@ -0,0 +1,125 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+
+/**
+ * A generic streamed, non-repeatable entity that obtains its content
+ * from an {@link InputStream}.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class BasicHttpEntity extends AbstractHttpEntity {
+
+ private InputStream content;
+ private long length;
+
+ /**
+ * Creates a new basic entity.
+ * The content is initially missing, the content length
+ * is set to a negative number.
+ */
+ public BasicHttpEntity() {
+ super();
+ this.length = -1;
+ }
+
+ public long getContentLength() {
+ return this.length;
+ }
+
+ /**
+ * Obtains the content, once only.
+ *
+ * @return the content, if this is the first call to this method
+ * since {@link #setContent setContent} has been called
+ *
+ * @throws IllegalStateException
+ * if the content has not been provided
+ */
+ public InputStream getContent() throws IllegalStateException {
+ Asserts.check(this.content != null, "Content has not been provided");
+ return this.content;
+ }
+
+ /**
+ * Tells that this entity is not repeatable.
+ *
+ * @return <code>false</code>
+ */
+ public boolean isRepeatable() {
+ return false;
+ }
+
+ /**
+ * Specifies the length of the content.
+ *
+ * @param len the number of bytes in the content, or
+ * a negative number to indicate an unknown length
+ */
+ public void setContentLength(final long len) {
+ this.length = len;
+ }
+
+ /**
+ * Specifies the content.
+ *
+ * @param instream the stream to return with the next call to
+ * {@link #getContent getContent}
+ */
+ public void setContent(final InputStream instream) {
+ this.content = instream;
+ }
+
+ public void writeTo(final OutputStream outstream) throws IOException {
+ Args.notNull(outstream, "Output stream");
+ final InputStream instream = getContent();
+ try {
+ int l;
+ final byte[] tmp = new byte[OUTPUT_BUFFER_SIZE];
+ while ((l = instream.read(tmp)) != -1) {
+ outstream.write(tmp, 0, l);
+ }
+ } finally {
+ instream.close();
+ }
+ }
+
+ public boolean isStreaming() {
+ return this.content != null;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/BufferedHttpEntity.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/BufferedHttpEntity.java
new file mode 100644
index 0000000000..6cb7495d7f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/BufferedHttpEntity.java
@@ -0,0 +1,125 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.EntityUtils;
+
+/**
+ * A wrapping entity that buffers it content if necessary.
+ * The buffered entity is always repeatable.
+ * If the wrapped entity is repeatable itself, calls are passed through.
+ * If the wrapped entity is not repeatable, the content is read into a
+ * buffer once and provided from there as often as required.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class BufferedHttpEntity extends HttpEntityWrapper {
+
+ private final byte[] buffer;
+
+ /**
+ * Creates a new buffered entity wrapper.
+ *
+ * @param entity the entity to wrap, not null
+ * @throws IllegalArgumentException if wrapped is null
+ */
+ public BufferedHttpEntity(final HttpEntity entity) throws IOException {
+ super(entity);
+ if (!entity.isRepeatable() || entity.getContentLength() < 0) {
+ this.buffer = EntityUtils.toByteArray(entity);
+ } else {
+ this.buffer = null;
+ }
+ }
+
+ @Override
+ public long getContentLength() {
+ if (this.buffer != null) {
+ return this.buffer.length;
+ } else {
+ return super.getContentLength();
+ }
+ }
+
+ @Override
+ public InputStream getContent() throws IOException {
+ if (this.buffer != null) {
+ return new ByteArrayInputStream(this.buffer);
+ } else {
+ return super.getContent();
+ }
+ }
+
+ /**
+ * Tells that this entity does not have to be chunked.
+ *
+ * @return <code>false</code>
+ */
+ @Override
+ public boolean isChunked() {
+ return (buffer == null) && super.isChunked();
+ }
+
+ /**
+ * Tells that this entity is repeatable.
+ *
+ * @return <code>true</code>
+ */
+ @Override
+ public boolean isRepeatable() {
+ return true;
+ }
+
+
+ @Override
+ public void writeTo(final OutputStream outstream) throws IOException {
+ Args.notNull(outstream, "Output stream");
+ if (this.buffer != null) {
+ outstream.write(this.buffer);
+ } else {
+ super.writeTo(outstream);
+ }
+ }
+
+
+ // non-javadoc, see interface HttpEntity
+ @Override
+ public boolean isStreaming() {
+ return (buffer == null) && super.isStreaming();
+ }
+
+} // class BufferedHttpEntity
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/ByteArrayEntity.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/ByteArrayEntity.java
new file mode 100644
index 0000000000..9f0af3ab87
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/ByteArrayEntity.java
@@ -0,0 +1,131 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * A self contained, repeatable entity that obtains its content from a byte array.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class ByteArrayEntity extends AbstractHttpEntity implements Cloneable {
+
+ /**
+ * @deprecated (4.2)
+ */
+ @Deprecated
+ protected final byte[] content;
+ private final byte[] b;
+ private final int off, len;
+
+ /**
+ * @since 4.2
+ */
+ @SuppressWarnings("deprecation")
+ public ByteArrayEntity(final byte[] b, final ContentType contentType) {
+ super();
+ Args.notNull(b, "Source byte array");
+ this.content = b;
+ this.b = b;
+ this.off = 0;
+ this.len = this.b.length;
+ if (contentType != null) {
+ setContentType(contentType.toString());
+ }
+ }
+
+ /**
+ * @since 4.2
+ */
+ @SuppressWarnings("deprecation")
+ public ByteArrayEntity(final byte[] b, final int off, final int len, final ContentType contentType) {
+ super();
+ Args.notNull(b, "Source byte array");
+ if ((off < 0) || (off > b.length) || (len < 0) ||
+ ((off + len) < 0) || ((off + len) > b.length)) {
+ throw new IndexOutOfBoundsException("off: " + off + " len: " + len + " b.length: " + b.length);
+ }
+ this.content = b;
+ this.b = b;
+ this.off = off;
+ this.len = len;
+ if (contentType != null) {
+ setContentType(contentType.toString());
+ }
+ }
+
+ public ByteArrayEntity(final byte[] b) {
+ this(b, null);
+ }
+
+ public ByteArrayEntity(final byte[] b, final int off, final int len) {
+ this(b, off, len, null);
+ }
+
+ public boolean isRepeatable() {
+ return true;
+ }
+
+ public long getContentLength() {
+ return this.len;
+ }
+
+ public InputStream getContent() {
+ return new ByteArrayInputStream(this.b, this.off, this.len);
+ }
+
+ public void writeTo(final OutputStream outstream) throws IOException {
+ Args.notNull(outstream, "Output stream");
+ outstream.write(this.b, this.off, this.len);
+ outstream.flush();
+ }
+
+
+ /**
+ * Tells that this entity is not streaming.
+ *
+ * @return <code>false</code>
+ */
+ public boolean isStreaming() {
+ return false;
+ }
+
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+
+} // class ByteArrayEntity
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/ContentLengthStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/ContentLengthStrategy.java
new file mode 100644
index 0000000000..4dbc1afafa
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/ContentLengthStrategy.java
@@ -0,0 +1,57 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpMessage;
+
+/**
+ * Represents a strategy to determine length of the enclosed content entity
+ * based on properties of the HTTP message.
+ *
+ * @since 4.0
+ */
+public interface ContentLengthStrategy {
+
+ public static final int IDENTITY = -1;
+ public static final int CHUNKED = -2;
+
+ /**
+ * Returns length of the given message in bytes. The returned value
+ * must be a non-negative number, {@link #IDENTITY} if the end of the
+ * message will be delimited by the end of connection, or {@link #CHUNKED}
+ * if the message is chunk coded
+ *
+ * @param message HTTP message
+ * @return content length, {@link #IDENTITY}, or {@link #CHUNKED}
+ *
+ * @throws HttpException in case of HTTP protocol violation
+ */
+ long determineLength(HttpMessage message) throws HttpException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/ContentProducer.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/ContentProducer.java
new file mode 100644
index 0000000000..3ced2e48bb
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/ContentProducer.java
@@ -0,0 +1,44 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * An abstract entity content producer.
+ *<p>Content producers are expected to be able to produce their
+ * content multiple times</p>
+ *
+ * @since 4.0
+ */
+public interface ContentProducer {
+
+ void writeTo(OutputStream outstream) throws IOException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/ContentType.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/ContentType.java
new file mode 100644
index 0000000000..e486b83b3f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/ContentType.java
@@ -0,0 +1,305 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity;
+
+import java.io.Serializable;
+import java.nio.charset.Charset;
+import java.nio.charset.UnsupportedCharsetException;
+import java.util.Locale;
+
+import ch.boye.httpclientandroidlib.Consts;
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.ParseException;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.message.BasicHeaderValueFormatter;
+import ch.boye.httpclientandroidlib.message.BasicHeaderValueParser;
+import ch.boye.httpclientandroidlib.message.ParserCursor;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+import ch.boye.httpclientandroidlib.util.TextUtils;
+
+/**
+ * Content type information consisting of a MIME type and an optional charset.
+ * <p/>
+ * This class makes no attempts to verify validity of the MIME type.
+ * The input parameters of the {@link #create(String, String)} method, however, may not
+ * contain characters <">, <;>, <,> reserved by the HTTP specification.
+ *
+ * @since 4.2
+ */
+@Immutable
+public final class ContentType implements Serializable {
+
+ private static final long serialVersionUID = -7768694718232371896L;
+
+ // constants
+ public static final ContentType APPLICATION_ATOM_XML = create(
+ "application/atom+xml", Consts.ISO_8859_1);
+ public static final ContentType APPLICATION_FORM_URLENCODED = create(
+ "application/x-www-form-urlencoded", Consts.ISO_8859_1);
+ public static final ContentType APPLICATION_JSON = create(
+ "application/json", Consts.UTF_8);
+ public static final ContentType APPLICATION_OCTET_STREAM = create(
+ "application/octet-stream", (Charset) null);
+ public static final ContentType APPLICATION_SVG_XML = create(
+ "application/svg+xml", Consts.ISO_8859_1);
+ public static final ContentType APPLICATION_XHTML_XML = create(
+ "application/xhtml+xml", Consts.ISO_8859_1);
+ public static final ContentType APPLICATION_XML = create(
+ "application/xml", Consts.ISO_8859_1);
+ public static final ContentType MULTIPART_FORM_DATA = create(
+ "multipart/form-data", Consts.ISO_8859_1);
+ public static final ContentType TEXT_HTML = create(
+ "text/html", Consts.ISO_8859_1);
+ public static final ContentType TEXT_PLAIN = create(
+ "text/plain", Consts.ISO_8859_1);
+ public static final ContentType TEXT_XML = create(
+ "text/xml", Consts.ISO_8859_1);
+ public static final ContentType WILDCARD = create(
+ "*/*", (Charset) null);
+
+ // defaults
+ public static final ContentType DEFAULT_TEXT = TEXT_PLAIN;
+ public static final ContentType DEFAULT_BINARY = APPLICATION_OCTET_STREAM;
+
+ private final String mimeType;
+ private final Charset charset;
+ private final NameValuePair[] params;
+
+ ContentType(
+ final String mimeType,
+ final Charset charset) {
+ this.mimeType = mimeType;
+ this.charset = charset;
+ this.params = null;
+ }
+
+ ContentType(
+ final String mimeType,
+ final NameValuePair[] params) throws UnsupportedCharsetException {
+ this.mimeType = mimeType;
+ this.params = params;
+ final String s = getParameter("charset");
+ this.charset = !TextUtils.isBlank(s) ? Charset.forName(s) : null;
+ }
+
+ public String getMimeType() {
+ return this.mimeType;
+ }
+
+ public Charset getCharset() {
+ return this.charset;
+ }
+
+ /**
+ * @since 4.3
+ */
+ public String getParameter(final String name) {
+ Args.notEmpty(name, "Parameter name");
+ if (this.params == null) {
+ return null;
+ }
+ for (final NameValuePair param: this.params) {
+ if (param.getName().equalsIgnoreCase(name)) {
+ return param.getValue();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Generates textual representation of this content type which can be used as the value
+ * of a <code>Content-Type</code> header.
+ */
+ @Override
+ public String toString() {
+ final CharArrayBuffer buf = new CharArrayBuffer(64);
+ buf.append(this.mimeType);
+ if (this.params != null) {
+ buf.append("; ");
+ BasicHeaderValueFormatter.INSTANCE.formatParameters(buf, this.params, false);
+ } else if (this.charset != null) {
+ buf.append("; charset=");
+ buf.append(this.charset.name());
+ }
+ return buf.toString();
+ }
+
+ private static boolean valid(final String s) {
+ for (int i = 0; i < s.length(); i++) {
+ final char ch = s.charAt(i);
+ if (ch == '"' || ch == ',' || ch == ';') {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Creates a new instance of {@link ContentType}.
+ *
+ * @param mimeType MIME type. It may not be <code>null</code> or empty. It may not contain
+ * characters <">, <;>, <,> reserved by the HTTP specification.
+ * @param charset charset.
+ * @return content type
+ */
+ public static ContentType create(final String mimeType, final Charset charset) {
+ final String type = Args.notBlank(mimeType, "MIME type").toLowerCase(Locale.US);
+ Args.check(valid(type), "MIME type may not contain reserved characters");
+ return new ContentType(type, charset);
+ }
+
+ /**
+ * Creates a new instance of {@link ContentType} without a charset.
+ *
+ * @param mimeType MIME type. It may not be <code>null</code> or empty. It may not contain
+ * characters <">, <;>, <,> reserved by the HTTP specification.
+ * @return content type
+ */
+ public static ContentType create(final String mimeType) {
+ return new ContentType(mimeType, (Charset) null);
+ }
+
+ /**
+ * Creates a new instance of {@link ContentType}.
+ *
+ * @param mimeType MIME type. It may not be <code>null</code> or empty. It may not contain
+ * characters <">, <;>, <,> reserved by the HTTP specification.
+ * @param charset charset. It may not contain characters <">, <;>, <,> reserved by the HTTP
+ * specification. This parameter is optional.
+ * @return content type
+ * @throws UnsupportedCharsetException Thrown when the named charset is not available in
+ * this instance of the Java virtual machine
+ */
+ public static ContentType create(
+ final String mimeType, final String charset) throws UnsupportedCharsetException {
+ return create(mimeType, !TextUtils.isBlank(charset) ? Charset.forName(charset) : null);
+ }
+
+ private static ContentType create(final HeaderElement helem) {
+ final String mimeType = helem.getName();
+ final NameValuePair[] params = helem.getParameters();
+ return new ContentType(mimeType, params != null && params.length > 0 ? params : null);
+ }
+
+ /**
+ * Parses textual representation of <code>Content-Type</code> value.
+ *
+ * @param s text
+ * @return content type
+ * @throws ParseException if the given text does not represent a valid
+ * <code>Content-Type</code> value.
+ * @throws UnsupportedCharsetException Thrown when the named charset is not available in
+ * this instance of the Java virtual machine
+ */
+ public static ContentType parse(
+ final String s) throws ParseException, UnsupportedCharsetException {
+ Args.notNull(s, "Content type");
+ final CharArrayBuffer buf = new CharArrayBuffer(s.length());
+ buf.append(s);
+ final ParserCursor cursor = new ParserCursor(0, s.length());
+ final HeaderElement[] elements = BasicHeaderValueParser.INSTANCE.parseElements(buf, cursor);
+ if (elements.length > 0) {
+ return create(elements[0]);
+ } else {
+ throw new ParseException("Invalid content type: " + s);
+ }
+ }
+
+ /**
+ * Extracts <code>Content-Type</code> value from {@link HttpEntity} exactly as
+ * specified by the <code>Content-Type</code> header of the entity. Returns <code>null</code>
+ * if not specified.
+ *
+ * @param entity HTTP entity
+ * @return content type
+ * @throws ParseException if the given text does not represent a valid
+ * <code>Content-Type</code> value.
+ * @throws UnsupportedCharsetException Thrown when the named charset is not available in
+ * this instance of the Java virtual machine
+ */
+ public static ContentType get(
+ final HttpEntity entity) throws ParseException, UnsupportedCharsetException {
+ if (entity == null) {
+ return null;
+ }
+ final Header header = entity.getContentType();
+ if (header != null) {
+ final HeaderElement[] elements = header.getElements();
+ if (elements.length > 0) {
+ return create(elements[0]);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Extracts <code>Content-Type</code> value from {@link HttpEntity} or returns the default value
+ * {@link #DEFAULT_TEXT} if not explicitly specified.
+ *
+ * @param entity HTTP entity
+ * @return content type
+ * @throws ParseException if the given text does not represent a valid
+ * <code>Content-Type</code> value.
+ * @throws UnsupportedCharsetException Thrown when the named charset is not available in
+ * this instance of the Java virtual machine
+ */
+ public static ContentType getOrDefault(
+ final HttpEntity entity) throws ParseException, UnsupportedCharsetException {
+ final ContentType contentType = get(entity);
+ return contentType != null ? contentType : DEFAULT_TEXT;
+ }
+
+ /**
+ * Creates a new instance with this MIME type and the given Charset.
+ *
+ * @param charset charset
+ * @return a new instance with this MIME type and the given Charset.
+ * @since 4.3
+ */
+ public ContentType withCharset(final Charset charset) {
+ return create(this.getMimeType(), charset);
+ }
+
+ /**
+ * Creates a new instance with this MIME type and the given Charset name.
+ *
+ * @param charset name
+ * @return a new instance with this MIME type and the given Charset name.
+ * @throws UnsupportedCharsetException Thrown when the named charset is not available in
+ * this instance of the Java virtual machine
+ * @since 4.3
+ */
+ public ContentType withCharset(final String charset) {
+ return create(this.getMimeType(), charset);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/EntityTemplate.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/EntityTemplate.java
new file mode 100644
index 0000000000..8e5a79431f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/EntityTemplate.java
@@ -0,0 +1,76 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Entity that delegates the process of content generation
+ * to a {@link ContentProducer}.
+ *
+ * @since 4.0
+ */
+public class EntityTemplate extends AbstractHttpEntity {
+
+ private final ContentProducer contentproducer;
+
+ public EntityTemplate(final ContentProducer contentproducer) {
+ super();
+ this.contentproducer = Args.notNull(contentproducer, "Content producer");
+ }
+
+ public long getContentLength() {
+ return -1;
+ }
+
+ public InputStream getContent() throws IOException {
+ final ByteArrayOutputStream buf = new ByteArrayOutputStream();
+ writeTo(buf);
+ return new ByteArrayInputStream(buf.toByteArray());
+ }
+
+ public boolean isRepeatable() {
+ return true;
+ }
+
+ public void writeTo(final OutputStream outstream) throws IOException {
+ Args.notNull(outstream, "Output stream");
+ this.contentproducer.writeTo(outstream);
+ }
+
+ public boolean isStreaming() {
+ return false;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/FileEntity.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/FileEntity.java
new file mode 100644
index 0000000000..8801d3733c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/FileEntity.java
@@ -0,0 +1,121 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * A self contained, repeatable entity that obtains its content from a file.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class FileEntity extends AbstractHttpEntity implements Cloneable {
+
+ protected final File file;
+
+ /**
+ * @deprecated (4.1.3) {@link #FileEntity(File, ContentType)}
+ */
+ @Deprecated
+ public FileEntity(final File file, final String contentType) {
+ super();
+ this.file = Args.notNull(file, "File");
+ setContentType(contentType);
+ }
+
+ /**
+ * @since 4.2
+ */
+ public FileEntity(final File file, final ContentType contentType) {
+ super();
+ this.file = Args.notNull(file, "File");
+ if (contentType != null) {
+ setContentType(contentType.toString());
+ }
+ }
+
+ /**
+ * @since 4.2
+ */
+ public FileEntity(final File file) {
+ super();
+ this.file = Args.notNull(file, "File");
+ }
+
+ public boolean isRepeatable() {
+ return true;
+ }
+
+ public long getContentLength() {
+ return this.file.length();
+ }
+
+ public InputStream getContent() throws IOException {
+ return new FileInputStream(this.file);
+ }
+
+ public void writeTo(final OutputStream outstream) throws IOException {
+ Args.notNull(outstream, "Output stream");
+ final InputStream instream = new FileInputStream(this.file);
+ try {
+ final byte[] tmp = new byte[OUTPUT_BUFFER_SIZE];
+ int l;
+ while ((l = instream.read(tmp)) != -1) {
+ outstream.write(tmp, 0, l);
+ }
+ outstream.flush();
+ } finally {
+ instream.close();
+ }
+ }
+
+ /**
+ * Tells that this entity is not streaming.
+ *
+ * @return <code>false</code>
+ */
+ public boolean isStreaming() {
+ return false;
+ }
+
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ // File instance is considered immutable
+ // No need to make a copy of it
+ return super.clone();
+ }
+
+} // class FileEntity
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/HttpEntityWrapper.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/HttpEntityWrapper.java
new file mode 100644
index 0000000000..77b1245f4b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/HttpEntityWrapper.java
@@ -0,0 +1,105 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Base class for wrapping entities.
+ * Keeps a {@link #wrappedEntity wrappedEntity} and delegates all
+ * calls to it. Implementations of wrapping entities can derive
+ * from this class and need to override only those methods that
+ * should not be delegated to the wrapped entity.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class HttpEntityWrapper implements HttpEntity {
+
+ /** The wrapped entity. */
+ protected HttpEntity wrappedEntity;
+
+ /**
+ * Creates a new entity wrapper.
+ */
+ public HttpEntityWrapper(final HttpEntity wrappedEntity) {
+ super();
+ this.wrappedEntity = Args.notNull(wrappedEntity, "Wrapped entity");
+ } // constructor
+
+ public boolean isRepeatable() {
+ return wrappedEntity.isRepeatable();
+ }
+
+ public boolean isChunked() {
+ return wrappedEntity.isChunked();
+ }
+
+ public long getContentLength() {
+ return wrappedEntity.getContentLength();
+ }
+
+ public Header getContentType() {
+ return wrappedEntity.getContentType();
+ }
+
+ public Header getContentEncoding() {
+ return wrappedEntity.getContentEncoding();
+ }
+
+ public InputStream getContent()
+ throws IOException {
+ return wrappedEntity.getContent();
+ }
+
+ public void writeTo(final OutputStream outstream)
+ throws IOException {
+ wrappedEntity.writeTo(outstream);
+ }
+
+ public boolean isStreaming() {
+ return wrappedEntity.isStreaming();
+ }
+
+ /**
+ * @deprecated (4.1) Either use {@link #getContent()} and call {@link java.io.InputStream#close()} on that;
+ * otherwise call {@link #writeTo(OutputStream)} which is required to free the resources.
+ */
+ @Deprecated
+ public void consumeContent() throws IOException {
+ wrappedEntity.consumeContent();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/InputStreamEntity.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/InputStreamEntity.java
new file mode 100644
index 0000000000..8a53ef68c7
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/InputStreamEntity.java
@@ -0,0 +1,155 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * A streamed, non-repeatable entity that obtains its content from
+ * an {@link InputStream}.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class InputStreamEntity extends AbstractHttpEntity {
+
+ private final InputStream content;
+ private final long length;
+
+ /**
+ * Creates an entity with an unknown length.
+ * Equivalent to {@code new InputStreamEntity(instream, -1)}.
+ *
+ * @param instream input stream
+ * @throws IllegalArgumentException if {@code instream} is {@code null}
+ * @since 4.3
+ */
+ public InputStreamEntity(final InputStream instream) {
+ this(instream, -1);
+ }
+
+ /**
+ * Creates an entity with a specified content length.
+ *
+ * @param instream input stream
+ * @param length of the input stream, {@code -1} if unknown
+ * @throws IllegalArgumentException if {@code instream} is {@code null}
+ */
+ public InputStreamEntity(final InputStream instream, final long length) {
+ this(instream, length, null);
+ }
+
+ /**
+ * Creates an entity with a content type and unknown length.
+ * Equivalent to {@code new InputStreamEntity(instream, -1, contentType)}.
+ *
+ * @param instream input stream
+ * @param contentType content type
+ * @throws IllegalArgumentException if {@code instream} is {@code null}
+ * @since 4.3
+ */
+ public InputStreamEntity(final InputStream instream, final ContentType contentType) {
+ this(instream, -1, contentType);
+ }
+
+ /**
+ * @param instream input stream
+ * @param length of the input stream, {@code -1} if unknown
+ * @param contentType for specifying the {@code Content-Type} header, may be {@code null}
+ * @throws IllegalArgumentException if {@code instream} is {@code null}
+ * @since 4.2
+ */
+ public InputStreamEntity(final InputStream instream, final long length, final ContentType contentType) {
+ super();
+ this.content = Args.notNull(instream, "Source input stream");
+ this.length = length;
+ if (contentType != null) {
+ setContentType(contentType.toString());
+ }
+ }
+
+ public boolean isRepeatable() {
+ return false;
+ }
+
+ /**
+ * @return the content length or {@code -1} if unknown
+ */
+ public long getContentLength() {
+ return this.length;
+ }
+
+ public InputStream getContent() throws IOException {
+ return this.content;
+ }
+
+ /**
+ * Writes bytes from the {@code InputStream} this entity was constructed
+ * with to an {@code OutputStream}. The content length
+ * determines how many bytes are written. If the length is unknown ({@code -1}), the
+ * stream will be completely consumed (to the end of the stream).
+ *
+ */
+ public void writeTo(final OutputStream outstream) throws IOException {
+ Args.notNull(outstream, "Output stream");
+ final InputStream instream = this.content;
+ try {
+ final byte[] buffer = new byte[OUTPUT_BUFFER_SIZE];
+ int l;
+ if (this.length < 0) {
+ // consume until EOF
+ while ((l = instream.read(buffer)) != -1) {
+ outstream.write(buffer, 0, l);
+ }
+ } else {
+ // consume no more than length
+ long remaining = this.length;
+ while (remaining > 0) {
+ l = instream.read(buffer, 0, (int)Math.min(OUTPUT_BUFFER_SIZE, remaining));
+ if (l == -1) {
+ break;
+ }
+ outstream.write(buffer, 0, l);
+ remaining -= l;
+ }
+ }
+ } finally {
+ instream.close();
+ }
+ }
+
+ public boolean isStreaming() {
+ return true;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/SerializableEntity.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/SerializableEntity.java
new file mode 100644
index 0000000000..f5e1253090
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/SerializableEntity.java
@@ -0,0 +1,126 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
+import java.io.Serializable;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * A streamed entity that obtains its content from a {@link Serializable}.
+ * The content obtained from the {@link Serializable} instance can
+ * optionally be buffered in a byte array in order to make the
+ * entity self-contained and repeatable.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class SerializableEntity extends AbstractHttpEntity {
+
+ private byte[] objSer;
+
+ private Serializable objRef;
+
+ /**
+ * Creates new instance of this class.
+ *
+ * @param ser input
+ * @param bufferize tells whether the content should be
+ * stored in an internal buffer
+ * @throws IOException in case of an I/O error
+ */
+ public SerializableEntity(final Serializable ser, final boolean bufferize) throws IOException {
+ super();
+ Args.notNull(ser, "Source object");
+ if (bufferize) {
+ createBytes(ser);
+ } else {
+ this.objRef = ser;
+ }
+ }
+
+ /**
+ * @since 4.3
+ */
+ public SerializableEntity(final Serializable ser) {
+ super();
+ Args.notNull(ser, "Source object");
+ this.objRef = ser;
+ }
+
+ private void createBytes(final Serializable ser) throws IOException {
+ final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ final ObjectOutputStream out = new ObjectOutputStream(baos);
+ out.writeObject(ser);
+ out.flush();
+ this.objSer = baos.toByteArray();
+ }
+
+ public InputStream getContent() throws IOException, IllegalStateException {
+ if (this.objSer == null) {
+ createBytes(this.objRef);
+ }
+ return new ByteArrayInputStream(this.objSer);
+ }
+
+ public long getContentLength() {
+ if (this.objSer == null) {
+ return -1;
+ } else {
+ return this.objSer.length;
+ }
+ }
+
+ public boolean isRepeatable() {
+ return true;
+ }
+
+ public boolean isStreaming() {
+ return this.objSer == null;
+ }
+
+ public void writeTo(final OutputStream outstream) throws IOException {
+ Args.notNull(outstream, "Output stream");
+ if (this.objSer == null) {
+ final ObjectOutputStream out = new ObjectOutputStream(outstream);
+ out.writeObject(this.objRef);
+ out.flush();
+ } else {
+ outstream.write(this.objSer);
+ outstream.flush();
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/StringEntity.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/StringEntity.java
new file mode 100644
index 0000000000..bf48891193
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/StringEntity.java
@@ -0,0 +1,188 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.UnsupportedCharsetException;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * A self contained, repeatable entity that obtains its content from
+ * a {@link String}.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class StringEntity extends AbstractHttpEntity implements Cloneable {
+
+ protected final byte[] content;
+
+ /**
+ * Creates a StringEntity with the specified content and content type.
+ *
+ * @param string content to be used. Not {@code null}.
+ * @param contentType content type to be used. May be {@code null}, in which case the default
+ * MIME type {@link ContentType#TEXT_PLAIN} is assumed.
+ *
+ * @throws IllegalArgumentException if the string parameter is null
+ * @throws UnsupportedCharsetException Thrown when the named charset is not available in
+ * this instance of the Java virtual machine
+ * @since 4.2
+ */
+ public StringEntity(final String string, final ContentType contentType) throws UnsupportedCharsetException {
+ super();
+ Args.notNull(string, "Source string");
+ Charset charset = contentType != null ? contentType.getCharset() : null;
+ if (charset == null) {
+ charset = HTTP.DEF_CONTENT_CHARSET;
+ }
+ try {
+ this.content = string.getBytes(charset.name());
+ } catch (final UnsupportedEncodingException ex) {
+ // should never happen
+ throw new UnsupportedCharsetException(charset.name());
+ }
+ if (contentType != null) {
+ setContentType(contentType.toString());
+ }
+ }
+
+ /**
+ * Creates a StringEntity with the specified content, MIME type and charset
+ *
+ * @param string content to be used. Not {@code null}.
+ * @param mimeType MIME type to be used. May be {@code null}, in which case the default
+ * is {@link HTTP#PLAIN_TEXT_TYPE} i.e. "text/plain"
+ * @param charset character set to be used. May be {@code null}, in which case the default
+ * is {@link HTTP#DEF_CONTENT_CHARSET} i.e. "ISO-8859-1"
+ * @throws UnsupportedEncodingException If the named charset is not supported.
+ *
+ * @since 4.1
+ * @throws IllegalArgumentException if the string parameter is null
+ *
+ * @deprecated (4.1.3) use {@link #StringEntity(String, ContentType)}
+ */
+ @Deprecated
+ public StringEntity(
+ final String string, final String mimeType, final String charset) throws UnsupportedEncodingException {
+ super();
+ Args.notNull(string, "Source string");
+ final String mt = mimeType != null ? mimeType : HTTP.PLAIN_TEXT_TYPE;
+ final String cs = charset != null ? charset :HTTP.DEFAULT_CONTENT_CHARSET;
+ this.content = string.getBytes(cs);
+ setContentType(mt + HTTP.CHARSET_PARAM + cs);
+ }
+
+ /**
+ * Creates a StringEntity with the specified content and charset. The MIME type defaults
+ * to "text/plain".
+ *
+ * @param string content to be used. Not {@code null}.
+ * @param charset character set to be used. May be {@code null}, in which case the default
+ * is {@link HTTP#DEF_CONTENT_CHARSET} is assumed
+ *
+ * @throws IllegalArgumentException if the string parameter is null
+ * @throws UnsupportedCharsetException Thrown when the named charset is not available in
+ * this instance of the Java virtual machine
+ */
+ public StringEntity(final String string, final String charset)
+ throws UnsupportedCharsetException {
+ this(string, ContentType.create(ContentType.TEXT_PLAIN.getMimeType(), charset));
+ }
+
+ /**
+ * Creates a StringEntity with the specified content and charset. The MIME type defaults
+ * to "text/plain".
+ *
+ * @param string content to be used. Not {@code null}.
+ * @param charset character set to be used. May be {@code null}, in which case the default
+ * is {@link HTTP#DEF_CONTENT_CHARSET} is assumed
+ *
+ * @throws IllegalArgumentException if the string parameter is null
+ *
+ * @since 4.2
+ */
+ public StringEntity(final String string, final Charset charset) {
+ this(string, ContentType.create(ContentType.TEXT_PLAIN.getMimeType(), charset));
+ }
+
+ /**
+ * Creates a StringEntity with the specified content. The content type defaults to
+ * {@link ContentType#TEXT_PLAIN}.
+ *
+ * @param string content to be used. Not {@code null}.
+ *
+ * @throws IllegalArgumentException if the string parameter is null
+ * @throws UnsupportedEncodingException if the default HTTP charset is not supported.
+ */
+ public StringEntity(final String string)
+ throws UnsupportedEncodingException {
+ this(string, ContentType.DEFAULT_TEXT);
+ }
+
+ public boolean isRepeatable() {
+ return true;
+ }
+
+ public long getContentLength() {
+ return this.content.length;
+ }
+
+ public InputStream getContent() throws IOException {
+ return new ByteArrayInputStream(this.content);
+ }
+
+ public void writeTo(final OutputStream outstream) throws IOException {
+ Args.notNull(outstream, "Output stream");
+ outstream.write(this.content);
+ outstream.flush();
+ }
+
+ /**
+ * Tells that this entity is not streaming.
+ *
+ * @return <code>false</code>
+ */
+ public boolean isStreaming() {
+ return false;
+ }
+
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+
+} // class StringEntity
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/AbstractMultipartForm.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/AbstractMultipartForm.java
new file mode 100644
index 0000000000..ade6bfe4bf
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/AbstractMultipartForm.java
@@ -0,0 +1,211 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity.mime;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.entity.mime.content.ContentBody;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.ByteArrayBuffer;
+
+/**
+ * HttpMultipart represents a collection of MIME multipart encoded content bodies. This class is
+ * capable of operating either in the strict (RFC 822, RFC 2045, RFC 2046 compliant) or
+ * the browser compatible modes.
+ *
+ * @since 4.3
+ */
+abstract class AbstractMultipartForm {
+
+ private static ByteArrayBuffer encode(
+ final Charset charset, final String string) {
+ final ByteBuffer encoded = charset.encode(CharBuffer.wrap(string));
+ final ByteArrayBuffer bab = new ByteArrayBuffer(encoded.remaining());
+ bab.append(encoded.array(), encoded.position(), encoded.remaining());
+ return bab;
+ }
+
+ private static void writeBytes(
+ final ByteArrayBuffer b, final OutputStream out) throws IOException {
+ out.write(b.buffer(), 0, b.length());
+ }
+
+ private static void writeBytes(
+ final String s, final Charset charset, final OutputStream out) throws IOException {
+ final ByteArrayBuffer b = encode(charset, s);
+ writeBytes(b, out);
+ }
+
+ private static void writeBytes(
+ final String s, final OutputStream out) throws IOException {
+ final ByteArrayBuffer b = encode(MIME.DEFAULT_CHARSET, s);
+ writeBytes(b, out);
+ }
+
+ protected static void writeField(
+ final MinimalField field, final OutputStream out) throws IOException {
+ writeBytes(field.getName(), out);
+ writeBytes(FIELD_SEP, out);
+ writeBytes(field.getBody(), out);
+ writeBytes(CR_LF, out);
+ }
+
+ protected static void writeField(
+ final MinimalField field, final Charset charset, final OutputStream out) throws IOException {
+ writeBytes(field.getName(), charset, out);
+ writeBytes(FIELD_SEP, out);
+ writeBytes(field.getBody(), charset, out);
+ writeBytes(CR_LF, out);
+ }
+
+ private static final ByteArrayBuffer FIELD_SEP = encode(MIME.DEFAULT_CHARSET, ": ");
+ private static final ByteArrayBuffer CR_LF = encode(MIME.DEFAULT_CHARSET, "\r\n");
+ private static final ByteArrayBuffer TWO_DASHES = encode(MIME.DEFAULT_CHARSET, "--");
+
+ private final String subType;
+ protected final Charset charset;
+ private final String boundary;
+
+ /**
+ * Creates an instance with the specified settings.
+ *
+ * @param subType MIME subtype - must not be {@code null}
+ * @param charset the character set to use. May be {@code null}, in which case {@link MIME#DEFAULT_CHARSET} - i.e. US-ASCII - is used.
+ * @param boundary to use - must not be {@code null}
+ * @throws IllegalArgumentException if charset is null or boundary is null
+ */
+ public AbstractMultipartForm(final String subType, final Charset charset, final String boundary) {
+ super();
+ Args.notNull(subType, "Multipart subtype");
+ Args.notNull(boundary, "Multipart boundary");
+ this.subType = subType;
+ this.charset = charset != null ? charset : MIME.DEFAULT_CHARSET;
+ this.boundary = boundary;
+ }
+
+ public AbstractMultipartForm(final String subType, final String boundary) {
+ this(subType, null, boundary);
+ }
+
+ public String getSubType() {
+ return this.subType;
+ }
+
+ public Charset getCharset() {
+ return this.charset;
+ }
+
+ public abstract List<FormBodyPart> getBodyParts();
+
+ public String getBoundary() {
+ return this.boundary;
+ }
+
+ void doWriteTo(
+ final OutputStream out,
+ final boolean writeContent) throws IOException {
+
+ final ByteArrayBuffer boundary = encode(this.charset, getBoundary());
+ for (final FormBodyPart part: getBodyParts()) {
+ writeBytes(TWO_DASHES, out);
+ writeBytes(boundary, out);
+ writeBytes(CR_LF, out);
+
+ formatMultipartHeader(part, out);
+
+ writeBytes(CR_LF, out);
+
+ if (writeContent) {
+ part.getBody().writeTo(out);
+ }
+ writeBytes(CR_LF, out);
+ }
+ writeBytes(TWO_DASHES, out);
+ writeBytes(boundary, out);
+ writeBytes(TWO_DASHES, out);
+ writeBytes(CR_LF, out);
+ }
+
+ /**
+ * Write the multipart header fields; depends on the style.
+ */
+ protected abstract void formatMultipartHeader(
+ final FormBodyPart part,
+ final OutputStream out) throws IOException;
+
+ /**
+ * Writes out the content in the multipart/form encoding. This method
+ * produces slightly different formatting depending on its compatibility
+ * mode.
+ */
+ public void writeTo(final OutputStream out) throws IOException {
+ doWriteTo(out, true);
+ }
+
+ /**
+ * Determines the total length of the multipart content (content length of
+ * individual parts plus that of extra elements required to delimit the parts
+ * from one another). If any of the @{link BodyPart}s contained in this object
+ * is of a streaming entity of unknown length the total length is also unknown.
+ * <p/>
+ * This method buffers only a small amount of data in order to determine the
+ * total length of the entire entity. The content of individual parts is not
+ * buffered.
+ *
+ * @return total length of the multipart entity if known, <code>-1</code>
+ * otherwise.
+ */
+ public long getTotalLength() {
+ long contentLen = 0;
+ for (final FormBodyPart part: getBodyParts()) {
+ final ContentBody body = part.getBody();
+ final long len = body.getContentLength();
+ if (len >= 0) {
+ contentLen += len;
+ } else {
+ return -1;
+ }
+ }
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try {
+ doWriteTo(out, false);
+ final byte[] extra = out.toByteArray();
+ return contentLen + extra.length;
+ } catch (final IOException ex) {
+ // Should never happen
+ return -1;
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/FormBodyPart.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/FormBodyPart.java
new file mode 100644
index 0000000000..a3d2c09542
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/FormBodyPart.java
@@ -0,0 +1,116 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity.mime;
+
+import ch.boye.httpclientandroidlib.entity.ContentType;
+import ch.boye.httpclientandroidlib.entity.mime.content.AbstractContentBody;
+import ch.boye.httpclientandroidlib.entity.mime.content.ContentBody;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * FormBodyPart class represents a content body that can be used as a part of multipart encoded
+ * entities. This class automatically populates the header with standard fields based on
+ * the content description of the enclosed body.
+ *
+ * @since 4.0
+ */
+public class FormBodyPart {
+
+ private final String name;
+ private final Header header;
+
+ private final ContentBody body;
+
+ public FormBodyPart(final String name, final ContentBody body) {
+ super();
+ Args.notNull(name, "Name");
+ Args.notNull(body, "Body");
+ this.name = name;
+ this.body = body;
+ this.header = new Header();
+
+ generateContentDisp(body);
+ generateContentType(body);
+ generateTransferEncoding(body);
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public ContentBody getBody() {
+ return this.body;
+ }
+
+ public Header getHeader() {
+ return this.header;
+ }
+
+ public void addField(final String name, final String value) {
+ Args.notNull(name, "Field name");
+ this.header.addField(new MinimalField(name, value));
+ }
+
+ protected void generateContentDisp(final ContentBody body) {
+ final StringBuilder buffer = new StringBuilder();
+ buffer.append("form-data; name=\"");
+ buffer.append(getName());
+ buffer.append("\"");
+ if (body.getFilename() != null) {
+ buffer.append("; filename=\"");
+ buffer.append(body.getFilename());
+ buffer.append("\"");
+ }
+ addField(MIME.CONTENT_DISPOSITION, buffer.toString());
+ }
+
+ protected void generateContentType(final ContentBody body) {
+ final ContentType contentType;
+ if (body instanceof AbstractContentBody) {
+ contentType = ((AbstractContentBody) body).getContentType();
+ } else {
+ contentType = null;
+ }
+ if (contentType != null) {
+ addField(MIME.CONTENT_TYPE, contentType.toString());
+ } else {
+ final StringBuilder buffer = new StringBuilder();
+ buffer.append(body.getMimeType()); // MimeType cannot be null
+ if (body.getCharset() != null) { // charset may legitimately be null
+ buffer.append("; charset=");
+ buffer.append(body.getCharset());
+ }
+ addField(MIME.CONTENT_TYPE, buffer.toString());
+ }
+ }
+
+ protected void generateTransferEncoding(final ContentBody body) {
+ addField(MIME.CONTENT_TRANSFER_ENC, body.getTransferEncoding()); // TE cannot be null
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/Header.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/Header.java
new file mode 100644
index 0000000000..15d70acc98
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/Header.java
@@ -0,0 +1,144 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity.mime;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * The header of an entity (see RFC 2045).
+ */
+public class Header implements Iterable<MinimalField> {
+
+ private final List<MinimalField> fields;
+ private final Map<String, List<MinimalField>> fieldMap;
+
+ public Header() {
+ super();
+ this.fields = new LinkedList<MinimalField>();
+ this.fieldMap = new HashMap<String, List<MinimalField>>();
+ }
+
+ public void addField(final MinimalField field) {
+ if (field == null) {
+ return;
+ }
+ final String key = field.getName().toLowerCase(Locale.ENGLISH);
+ List<MinimalField> values = this.fieldMap.get(key);
+ if (values == null) {
+ values = new LinkedList<MinimalField>();
+ this.fieldMap.put(key, values);
+ }
+ values.add(field);
+ this.fields.add(field);
+ }
+
+ public List<MinimalField> getFields() {
+ return new ArrayList<MinimalField>(this.fields);
+ }
+
+ public MinimalField getField(final String name) {
+ if (name == null) {
+ return null;
+ }
+ final String key = name.toLowerCase(Locale.ENGLISH);
+ final List<MinimalField> list = this.fieldMap.get(key);
+ if (list != null && !list.isEmpty()) {
+ return list.get(0);
+ }
+ return null;
+ }
+
+ public List<MinimalField> getFields(final String name) {
+ if (name == null) {
+ return null;
+ }
+ final String key = name.toLowerCase(Locale.ENGLISH);
+ final List<MinimalField> list = this.fieldMap.get(key);
+ if (list == null || list.isEmpty()) {
+ return Collections.emptyList();
+ } else {
+ return new ArrayList<MinimalField>(list);
+ }
+ }
+
+ public int removeFields(final String name) {
+ if (name == null) {
+ return 0;
+ }
+ final String key = name.toLowerCase(Locale.ENGLISH);
+ final List<MinimalField> removed = fieldMap.remove(key);
+ if (removed == null || removed.isEmpty()) {
+ return 0;
+ }
+ this.fields.removeAll(removed);
+ return removed.size();
+ }
+
+ public void setField(final MinimalField field) {
+ if (field == null) {
+ return;
+ }
+ final String key = field.getName().toLowerCase(Locale.ENGLISH);
+ final List<MinimalField> list = fieldMap.get(key);
+ if (list == null || list.isEmpty()) {
+ addField(field);
+ return;
+ }
+ list.clear();
+ list.add(field);
+ int firstOccurrence = -1;
+ int index = 0;
+ for (final Iterator<MinimalField> it = this.fields.iterator(); it.hasNext(); index++) {
+ final MinimalField f = it.next();
+ if (f.getName().equalsIgnoreCase(field.getName())) {
+ it.remove();
+ if (firstOccurrence == -1) {
+ firstOccurrence = index;
+ }
+ }
+ }
+ this.fields.add(firstOccurrence, field);
+ }
+
+ public Iterator<MinimalField> iterator() {
+ return Collections.unmodifiableList(fields).iterator();
+ }
+
+ @Override
+ public String toString() {
+ return this.fields.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/HttpBrowserCompatibleMultipart.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/HttpBrowserCompatibleMultipart.java
new file mode 100644
index 0000000000..dfc0a657e9
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/HttpBrowserCompatibleMultipart.java
@@ -0,0 +1,79 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity.mime;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.util.List;
+
+/**
+ * HttpBrowserCompatibleMultipart represents a collection of MIME multipart encoded
+ * content bodies. This class is emulates browser compatibility, e.g. IE 5 or earlier.
+ *
+ * @since 4.3
+ */
+class HttpBrowserCompatibleMultipart extends AbstractMultipartForm {
+
+ private final List<FormBodyPart> parts;
+
+ public HttpBrowserCompatibleMultipart(
+ final String subType,
+ final Charset charset,
+ final String boundary,
+ final List<FormBodyPart> parts) {
+ super(subType, charset, boundary);
+ this.parts = parts;
+ }
+
+ @Override
+ public List<FormBodyPart> getBodyParts() {
+ return this.parts;
+ }
+
+ /**
+ * Write the multipart header fields; depends on the style.
+ */
+ @Override
+ protected void formatMultipartHeader(
+ final FormBodyPart part,
+ final OutputStream out) throws IOException {
+ // For browser-compatible, only write Content-Disposition
+ // Use content charset
+ final Header header = part.getHeader();
+ final MinimalField cd = header.getField(MIME.CONTENT_DISPOSITION);
+ writeField(cd, this.charset, out);
+ final String filename = part.getBody().getFilename();
+ if (filename != null) {
+ final MinimalField ct = header.getField(MIME.CONTENT_TYPE);
+ writeField(ct, this.charset, out);
+ }
+
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/HttpMultipartMode.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/HttpMultipartMode.java
new file mode 100644
index 0000000000..ab73e49832
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/HttpMultipartMode.java
@@ -0,0 +1,43 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity.mime;
+
+/**
+ *
+ * @since 4.0
+ */
+public enum HttpMultipartMode {
+
+ /** RFC 822, RFC 2045, RFC 2046 compliant */
+ STRICT,
+ /** browser-compatible mode, i.e. only write Content-Disposition; use content charset */
+ BROWSER_COMPATIBLE,
+ /** RFC 6532 compliant */
+ RFC6532
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/HttpRFC6532Multipart.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/HttpRFC6532Multipart.java
new file mode 100644
index 0000000000..634a12b232
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/HttpRFC6532Multipart.java
@@ -0,0 +1,72 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity.mime;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.util.List;
+
+/**
+ * HttpRFC6532Multipart represents a collection of MIME multipart encoded content bodies,
+ * implementing the strict (RFC 822, RFC 2045, RFC 2046 compliant) interpretation
+ * of the spec, with the exception of allowing UTF-8 headers, as per RFC6532.
+ *
+ * @since 4.3
+ */
+class HttpRFC6532Multipart extends AbstractMultipartForm {
+
+ private final List<FormBodyPart> parts;
+
+ public HttpRFC6532Multipart(
+ final String subType,
+ final Charset charset,
+ final String boundary,
+ final List<FormBodyPart> parts) {
+ super(subType, charset, boundary);
+ this.parts = parts;
+ }
+
+ @Override
+ public List<FormBodyPart> getBodyParts() {
+ return this.parts;
+ }
+
+ @Override
+ protected void formatMultipartHeader(
+ final FormBodyPart part,
+ final OutputStream out) throws IOException {
+
+ // For RFC6532, we output all fields with UTF-8 encoding.
+ final Header header = part.getHeader();
+ for (final MinimalField field: header) {
+ writeField(field, MIME.UTF8_CHARSET, out);
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/HttpStrictMultipart.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/HttpStrictMultipart.java
new file mode 100644
index 0000000000..d6e68c6e2c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/HttpStrictMultipart.java
@@ -0,0 +1,72 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity.mime;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.util.List;
+
+/**
+ * HttpStrictMultipart represents a collection of MIME multipart encoded content bodies,
+ * implementing the strict (RFC 822, RFC 2045, RFC 2046 compliant) interpretation
+ * of the spec.
+ *
+ * @since 4.3
+ */
+class HttpStrictMultipart extends AbstractMultipartForm {
+
+ private final List<FormBodyPart> parts;
+
+ public HttpStrictMultipart(
+ final String subType,
+ final Charset charset,
+ final String boundary,
+ final List<FormBodyPart> parts) {
+ super(subType, charset, boundary);
+ this.parts = parts;
+ }
+
+ @Override
+ public List<FormBodyPart> getBodyParts() {
+ return this.parts;
+ }
+
+ @Override
+ protected void formatMultipartHeader(
+ final FormBodyPart part,
+ final OutputStream out) throws IOException {
+
+ // For strict, we output all fields with MIME-standard encoding.
+ final Header header = part.getHeader();
+ for (final MinimalField field: header) {
+ writeField(field, out);
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/MIME.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/MIME.java
new file mode 100644
index 0000000000..d5896312db
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/MIME.java
@@ -0,0 +1,53 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity.mime;
+
+import ch.boye.httpclientandroidlib.Consts;
+
+import java.nio.charset.Charset;
+
+/**
+ *
+ * @since 4.0
+ */
+public final class MIME {
+
+ public static final String CONTENT_TYPE = "Content-Type";
+ public static final String CONTENT_TRANSFER_ENC = "Content-Transfer-Encoding";
+ public static final String CONTENT_DISPOSITION = "Content-Disposition";
+
+ public static final String ENC_8BIT = "8bit";
+ public static final String ENC_BINARY = "binary";
+
+ /** The default character set to be used, i.e. "US-ASCII" */
+ public static final Charset DEFAULT_CHARSET = Consts.ASCII;
+
+ /** UTF-8 is used for RFC6532 */
+ public static final Charset UTF8_CHARSET = Consts.UTF_8;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/MinimalField.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/MinimalField.java
new file mode 100644
index 0000000000..35af51233e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/MinimalField.java
@@ -0,0 +1,63 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity.mime;
+
+/**
+ * Minimal MIME field.
+ *
+ * @since 4.0
+ */
+public class MinimalField {
+
+ private final String name;
+ private final String value;
+
+ public MinimalField(final String name, final String value) {
+ super();
+ this.name = name;
+ this.value = value;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public String getBody() {
+ return this.value;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder buffer = new StringBuilder();
+ buffer.append(this.name);
+ buffer.append(": ");
+ buffer.append(this.value);
+ return buffer.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/MultipartEntityBuilder.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/MultipartEntityBuilder.java
new file mode 100644
index 0000000000..5074001d78
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/MultipartEntityBuilder.java
@@ -0,0 +1,207 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity.mime;
+
+import java.io.File;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.entity.ContentType;
+import ch.boye.httpclientandroidlib.entity.mime.content.ByteArrayBody;
+import ch.boye.httpclientandroidlib.entity.mime.content.ContentBody;
+import ch.boye.httpclientandroidlib.entity.mime.content.FileBody;
+import ch.boye.httpclientandroidlib.entity.mime.content.InputStreamBody;
+import ch.boye.httpclientandroidlib.entity.mime.content.StringBody;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Builder for multipart {@link HttpEntity}s.
+ *
+ * @since 4.3
+ */
+public class MultipartEntityBuilder {
+
+ /**
+ * The pool of ASCII chars to be used for generating a multipart boundary.
+ */
+ private final static char[] MULTIPART_CHARS =
+ "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ .toCharArray();
+
+ private final static String DEFAULT_SUBTYPE = "form-data";
+
+ private String subType = DEFAULT_SUBTYPE;
+ private HttpMultipartMode mode = HttpMultipartMode.STRICT;
+ private String boundary = null;
+ private Charset charset = null;
+ private List<FormBodyPart> bodyParts = null;
+
+ public static MultipartEntityBuilder create() {
+ return new MultipartEntityBuilder();
+ }
+
+ MultipartEntityBuilder() {
+ super();
+ }
+
+ public MultipartEntityBuilder setMode(final HttpMultipartMode mode) {
+ this.mode = mode;
+ return this;
+ }
+
+ public MultipartEntityBuilder setLaxMode() {
+ this.mode = HttpMultipartMode.BROWSER_COMPATIBLE;
+ return this;
+ }
+
+ public MultipartEntityBuilder setStrictMode() {
+ this.mode = HttpMultipartMode.STRICT;
+ return this;
+ }
+
+ public MultipartEntityBuilder setBoundary(final String boundary) {
+ this.boundary = boundary;
+ return this;
+ }
+
+ public MultipartEntityBuilder setCharset(final Charset charset) {
+ this.charset = charset;
+ return this;
+ }
+
+ MultipartEntityBuilder addPart(final FormBodyPart bodyPart) {
+ if (bodyPart == null) {
+ return this;
+ }
+ if (this.bodyParts == null) {
+ this.bodyParts = new ArrayList<FormBodyPart>();
+ }
+ this.bodyParts.add(bodyPart);
+ return this;
+ }
+
+ public MultipartEntityBuilder addPart(final String name, final ContentBody contentBody) {
+ Args.notNull(name, "Name");
+ Args.notNull(contentBody, "Content body");
+ return addPart(new FormBodyPart(name, contentBody));
+ }
+
+ public MultipartEntityBuilder addTextBody(
+ final String name, final String text, final ContentType contentType) {
+ return addPart(name, new StringBody(text, contentType));
+ }
+
+ public MultipartEntityBuilder addTextBody(
+ final String name, final String text) {
+ return addTextBody(name, text, ContentType.DEFAULT_TEXT);
+ }
+
+ public MultipartEntityBuilder addBinaryBody(
+ final String name, final byte[] b, final ContentType contentType, final String filename) {
+ return addPart(name, new ByteArrayBody(b, contentType, filename));
+ }
+
+ public MultipartEntityBuilder addBinaryBody(
+ final String name, final byte[] b) {
+ return addBinaryBody(name, b, ContentType.DEFAULT_BINARY, null);
+ }
+
+ public MultipartEntityBuilder addBinaryBody(
+ final String name, final File file, final ContentType contentType, final String filename) {
+ return addPart(name, new FileBody(file, contentType, filename));
+ }
+
+ public MultipartEntityBuilder addBinaryBody(
+ final String name, final File file) {
+ return addBinaryBody(name, file, ContentType.DEFAULT_BINARY, file != null ? file.getName() : null);
+ }
+
+ public MultipartEntityBuilder addBinaryBody(
+ final String name, final InputStream stream, final ContentType contentType,
+ final String filename) {
+ return addPart(name, new InputStreamBody(stream, contentType, filename));
+ }
+
+ public MultipartEntityBuilder addBinaryBody(final String name, final InputStream stream) {
+ return addBinaryBody(name, stream, ContentType.DEFAULT_BINARY, null);
+ }
+
+ private String generateContentType(
+ final String boundary,
+ final Charset charset) {
+ final StringBuilder buffer = new StringBuilder();
+ buffer.append("multipart/form-data; boundary=");
+ buffer.append(boundary);
+ if (charset != null) {
+ buffer.append("; charset=");
+ buffer.append(charset.name());
+ }
+ return buffer.toString();
+ }
+
+ private String generateBoundary() {
+ final StringBuilder buffer = new StringBuilder();
+ final Random rand = new Random();
+ final int count = rand.nextInt(11) + 30; // a random size from 30 to 40
+ for (int i = 0; i < count; i++) {
+ buffer.append(MULTIPART_CHARS[rand.nextInt(MULTIPART_CHARS.length)]);
+ }
+ return buffer.toString();
+ }
+
+ MultipartFormEntity buildEntity() {
+ final String st = subType != null ? subType : DEFAULT_SUBTYPE;
+ final Charset cs = charset;
+ final String b = boundary != null ? boundary : generateBoundary();
+ final List<FormBodyPart> bps = bodyParts != null ? new ArrayList<FormBodyPart>(bodyParts) :
+ Collections.<FormBodyPart>emptyList();
+ final HttpMultipartMode m = mode != null ? mode : HttpMultipartMode.STRICT;
+ final AbstractMultipartForm form;
+ switch (m) {
+ case BROWSER_COMPATIBLE:
+ form = new HttpBrowserCompatibleMultipart(st, cs, b, bps);
+ break;
+ case RFC6532:
+ form = new HttpRFC6532Multipart(st, cs, b, bps);
+ break;
+ default:
+ form = new HttpStrictMultipart(st, cs, b, bps);
+ }
+ return new MultipartFormEntity(form, generateContentType(b, cs), form.getTotalLength());
+ }
+
+ public HttpEntity build() {
+ return buildEntity();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/MultipartFormEntity.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/MultipartFormEntity.java
new file mode 100644
index 0000000000..9da2f4724f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/MultipartFormEntity.java
@@ -0,0 +1,100 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity.mime;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.message.BasicHeader;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+class MultipartFormEntity implements HttpEntity {
+
+ private final AbstractMultipartForm multipart;
+ private final Header contentType;
+ private final long contentLength;
+
+ MultipartFormEntity(
+ final AbstractMultipartForm multipart,
+ final String contentType,
+ final long contentLength) {
+ super();
+ this.multipart = multipart;
+ this.contentType = new BasicHeader(HTTP.CONTENT_TYPE, contentType);
+ this.contentLength = contentLength;
+ }
+
+ AbstractMultipartForm getMultipart() {
+ return this.multipart;
+ }
+
+ public boolean isRepeatable() {
+ return this.contentLength != -1;
+ }
+
+ public boolean isChunked() {
+ return !isRepeatable();
+ }
+
+ public boolean isStreaming() {
+ return !isRepeatable();
+ }
+
+ public long getContentLength() {
+ return this.contentLength;
+ }
+
+ public Header getContentType() {
+ return this.contentType;
+ }
+
+ public Header getContentEncoding() {
+ return null;
+ }
+
+ public void consumeContent()
+ throws IOException, UnsupportedOperationException{
+ if (isStreaming()) {
+ throw new UnsupportedOperationException(
+ "Streaming entity does not implement #consumeContent()");
+ }
+ }
+
+ public InputStream getContent() throws IOException {
+ throw new UnsupportedOperationException(
+ "Multipart form entity does not implement #getContent()");
+ }
+
+ public void writeTo(final OutputStream outstream) throws IOException {
+ this.multipart.writeTo(outstream);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/AbstractContentBody.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/AbstractContentBody.java
new file mode 100644
index 0000000000..b6b58ad352
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/AbstractContentBody.java
@@ -0,0 +1,96 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity.mime.content;
+
+import java.nio.charset.Charset;
+
+import ch.boye.httpclientandroidlib.entity.ContentType;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ *
+ * @since 4.0
+ */
+public abstract class AbstractContentBody implements ContentBody {
+
+ private final ContentType contentType;
+
+ /**
+ * @since 4.3
+ */
+ public AbstractContentBody(final ContentType contentType) {
+ super();
+ Args.notNull(contentType, "Content type");
+ this.contentType = contentType;
+ }
+
+ /**
+ * @deprecated (4.3) use {@link AbstractContentBody#AbstractContentBody(ContentType)}
+ */
+ @Deprecated
+ public AbstractContentBody(final String mimeType) {
+ this(ContentType.parse(mimeType));
+ }
+
+ /**
+ * @since 4.3
+ */
+ public ContentType getContentType() {
+ return this.contentType;
+ }
+
+ public String getMimeType() {
+ return this.contentType.getMimeType();
+ }
+
+ public String getMediaType() {
+ final String mimeType = this.contentType.getMimeType();
+ final int i = mimeType.indexOf('/');
+ if (i != -1) {
+ return mimeType.substring(0, i);
+ } else {
+ return mimeType;
+ }
+ }
+
+ public String getSubType() {
+ final String mimeType = this.contentType.getMimeType();
+ final int i = mimeType.indexOf('/');
+ if (i != -1) {
+ return mimeType.substring(i + 1);
+ } else {
+ return null;
+ }
+ }
+
+ public String getCharset() {
+ final Charset charset = this.contentType.getCharset();
+ return charset != null ? charset.name() : null;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/ByteArrayBody.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/ByteArrayBody.java
new file mode 100644
index 0000000000..ea60fa047d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/ByteArrayBody.java
@@ -0,0 +1,111 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.entity.mime.content;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import ch.boye.httpclientandroidlib.entity.ContentType;
+import ch.boye.httpclientandroidlib.entity.mime.MIME;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Binary body part backed by a byte array.
+ *
+ * @see ch.boye.httpclientandroidlib.entity.mime.MultipartEntityBuilder
+ *
+ * @since 4.1
+ */
+public class ByteArrayBody extends AbstractContentBody {
+
+ /**
+ * The contents of the file contained in this part.
+ */
+ private final byte[] data;
+
+ /**
+ * The name of the file contained in this part.
+ */
+ private final String filename;
+
+ /**
+ * Creates a new ByteArrayBody.
+ *
+ * @param data The contents of the file contained in this part.
+ * @param mimeType The MIME type of the file contained in this part.
+ * @param filename The name of the file contained in this part.
+ *
+ * @deprecated (4.3) use {@link ByteArrayBody#ByteArrayBody(byte[], ContentType, String)}
+ * or {@link ch.boye.httpclientandroidlib.entity.mime.MultipartEntityBuilder}
+ */
+ @Deprecated
+ public ByteArrayBody(final byte[] data, final String mimeType, final String filename) {
+ this(data, ContentType.create(mimeType), filename);
+ }
+
+ /**
+ * @since 4.3
+ */
+ public ByteArrayBody(final byte[] data, final ContentType contentType, final String filename) {
+ super(contentType);
+ Args.notNull(data, "byte[]");
+ this.data = data;
+ this.filename = filename;
+ }
+
+ /**
+ * Creates a new ByteArrayBody.
+ *
+ * @param data The contents of the file contained in this part.
+ * @param filename The name of the file contained in this part.
+ */
+ public ByteArrayBody(final byte[] data, final String filename) {
+ this(data, "application/octet-stream", filename);
+ }
+
+ public String getFilename() {
+ return filename;
+ }
+
+ public void writeTo(final OutputStream out) throws IOException {
+ out.write(data);
+ }
+
+ @Override
+ public String getCharset() {
+ return null;
+ }
+
+ public String getTransferEncoding() {
+ return MIME.ENC_BINARY;
+ }
+
+ public long getContentLength() {
+ return data.length;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/ContentBody.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/ContentBody.java
new file mode 100644
index 0000000000..d62aaff54f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/ContentBody.java
@@ -0,0 +1,43 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity.mime.content;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ *
+ * @since 4.0
+ */
+public interface ContentBody extends ContentDescriptor {
+
+ String getFilename();
+
+ void writeTo(OutputStream out) throws IOException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/ContentDescriptor.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/ContentDescriptor.java
new file mode 100644
index 0000000000..87237ec235
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/ContentDescriptor.java
@@ -0,0 +1,89 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity.mime.content;
+
+/**
+ * Represents common content properties.
+ */
+public interface ContentDescriptor {
+
+ /**
+ * Returns the body descriptors MIME type.
+ * @see #getMediaType()
+ * @see #getSubType()
+ * @return The MIME type, which has been parsed from the
+ * content-type definition. Must not be null, but
+ * "text/plain", if no content-type was specified.
+ */
+ String getMimeType();
+
+ /**
+ * Gets the defaulted MIME media type for this content.
+ * For example <code>TEXT</code>, <code>IMAGE</code>, <code>MULTIPART</code>
+ * @see #getMimeType()
+ * @return the MIME media type when content-type specified,
+ * otherwise the correct default (<code>TEXT</code>)
+ */
+ String getMediaType();
+
+ /**
+ * Gets the defaulted MIME sub type for this content.
+ * @see #getMimeType()
+ * @return the MIME media type when content-type is specified,
+ * otherwise the correct default (<code>PLAIN</code>)
+ */
+ String getSubType();
+
+ /**
+ * <p>The body descriptors character set, defaulted appropriately for the MIME type.</p>
+ * <p>
+ * For <code>TEXT</code> types, this will be defaulted to <code>us-ascii</code>.
+ * For other types, when the charset parameter is missing this property will be null.
+ * </p>
+ * @return Character set, which has been parsed from the
+ * content-type definition. Not null for <code>TEXT</code> types, when unset will
+ * be set to default <code>us-ascii</code>. For other types, when unset,
+ * null will be returned.
+ */
+ String getCharset();
+
+ /**
+ * Returns the body descriptors transfer encoding.
+ * @return The transfer encoding. Must not be null, but "7bit",
+ * if no transfer-encoding was specified.
+ */
+ String getTransferEncoding();
+
+ /**
+ * Returns the body descriptors content-length.
+ * @return Content length, if known, or -1, to indicate the absence of a
+ * content-length header.
+ */
+ long getContentLength();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/FileBody.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/FileBody.java
new file mode 100644
index 0000000000..7641236a48
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/FileBody.java
@@ -0,0 +1,144 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity.mime.content;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import ch.boye.httpclientandroidlib.entity.ContentType;
+import ch.boye.httpclientandroidlib.entity.mime.MIME;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Binary body part backed by a file.
+ *
+ * @see ch.boye.httpclientandroidlib.entity.mime.MultipartEntityBuilder
+ *
+ * @since 4.0
+ */
+public class FileBody extends AbstractContentBody {
+
+ private final File file;
+ private final String filename;
+
+ /**
+ * @since 4.1
+ *
+ * @deprecated (4.3) use {@link FileBody#FileBody(File, ContentType, String)}
+ * or {@link ch.boye.httpclientandroidlib.entity.mime.MultipartEntityBuilder}
+ */
+ @Deprecated
+ public FileBody(final File file,
+ final String filename,
+ final String mimeType,
+ final String charset) {
+ this(file, ContentType.create(mimeType, charset), filename);
+ }
+
+ /**
+ * @since 4.1
+ *
+ * @deprecated (4.3) use {@link FileBody#FileBody(File, ContentType)}
+ * or {@link ch.boye.httpclientandroidlib.entity.mime.MultipartEntityBuilder}
+ */
+ @Deprecated
+ public FileBody(final File file,
+ final String mimeType,
+ final String charset) {
+ this(file, null, mimeType, charset);
+ }
+
+ /**
+ * @deprecated (4.3) use {@link FileBody#FileBody(File, ContentType)}
+ * or {@link ch.boye.httpclientandroidlib.entity.mime.MultipartEntityBuilder}
+ */
+ @Deprecated
+ public FileBody(final File file, final String mimeType) {
+ this(file, ContentType.create(mimeType), null);
+ }
+
+ public FileBody(final File file) {
+ this(file, ContentType.DEFAULT_BINARY, file != null ? file.getName() : null);
+ }
+
+ /**
+ * @since 4.3
+ */
+ public FileBody(final File file, final ContentType contentType, final String filename) {
+ super(contentType);
+ Args.notNull(file, "File");
+ this.file = file;
+ this.filename = filename;
+ }
+
+ /**
+ * @since 4.3
+ */
+ public FileBody(final File file, final ContentType contentType) {
+ this(file, contentType, null);
+ }
+
+ public InputStream getInputStream() throws IOException {
+ return new FileInputStream(this.file);
+ }
+
+ public void writeTo(final OutputStream out) throws IOException {
+ Args.notNull(out, "Output stream");
+ final InputStream in = new FileInputStream(this.file);
+ try {
+ final byte[] tmp = new byte[4096];
+ int l;
+ while ((l = in.read(tmp)) != -1) {
+ out.write(tmp, 0, l);
+ }
+ out.flush();
+ } finally {
+ in.close();
+ }
+ }
+
+ public String getTransferEncoding() {
+ return MIME.ENC_BINARY;
+ }
+
+ public long getContentLength() {
+ return this.file.length();
+ }
+
+ public String getFilename() {
+ return filename;
+ }
+
+ public File getFile() {
+ return this.file;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/InputStreamBody.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/InputStreamBody.java
new file mode 100644
index 0000000000..267a78ea58
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/InputStreamBody.java
@@ -0,0 +1,112 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity.mime.content;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import ch.boye.httpclientandroidlib.entity.ContentType;
+import ch.boye.httpclientandroidlib.entity.mime.MIME;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Binary body part backed by an input stream.
+ *
+ * @see ch.boye.httpclientandroidlib.entity.mime.MultipartEntityBuilder
+ *
+ * @since 4.0
+ */
+public class InputStreamBody extends AbstractContentBody {
+
+ private final InputStream in;
+ private final String filename;
+
+ /**
+ * @since 4.1
+ *
+ * @deprecated (4.3) use {@link InputStreamBody#InputStreamBody(InputStream, ContentType,
+ * String)} or {@link ch.boye.httpclientandroidlib.entity.mime.MultipartEntityBuilder}
+ */
+ @Deprecated
+ public InputStreamBody(final InputStream in, final String mimeType, final String filename) {
+ this(in, ContentType.create(mimeType), filename);
+ }
+
+ public InputStreamBody(final InputStream in, final String filename) {
+ this(in, ContentType.DEFAULT_BINARY, filename);
+ }
+
+ /**
+ * @since 4.3
+ */
+ public InputStreamBody(final InputStream in, final ContentType contentType, final String filename) {
+ super(contentType);
+ Args.notNull(in, "Input stream");
+ this.in = in;
+ this.filename = filename;
+ }
+
+ /**
+ * @since 4.3
+ */
+ public InputStreamBody(final InputStream in, final ContentType contentType) {
+ this(in, contentType, null);
+ }
+
+ public InputStream getInputStream() {
+ return this.in;
+ }
+
+ public void writeTo(final OutputStream out) throws IOException {
+ Args.notNull(out, "Output stream");
+ try {
+ final byte[] tmp = new byte[4096];
+ int l;
+ while ((l = this.in.read(tmp)) != -1) {
+ out.write(tmp, 0, l);
+ }
+ out.flush();
+ } finally {
+ this.in.close();
+ }
+ }
+
+ public String getTransferEncoding() {
+ return MIME.ENC_BINARY;
+ }
+
+ public long getContentLength() {
+ return -1;
+ }
+
+ public String getFilename() {
+ return this.filename;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/StringBody.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/StringBody.java
new file mode 100644
index 0000000000..c5ad5632d3
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/StringBody.java
@@ -0,0 +1,197 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.entity.mime.content;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.UnsupportedCharsetException;
+
+import ch.boye.httpclientandroidlib.Consts;
+import ch.boye.httpclientandroidlib.entity.ContentType;
+import ch.boye.httpclientandroidlib.entity.mime.MIME;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Text body part backed by a byte array.
+ *
+ * @see ch.boye.httpclientandroidlib.entity.mime.MultipartEntityBuilder
+ *
+ * @since 4.0
+ */
+public class StringBody extends AbstractContentBody {
+
+ private final byte[] content;
+
+ /**
+ * @since 4.1
+ *
+ * @deprecated (4.3) use {@link StringBody#StringBody(String, ContentType)}
+ * or {@link ch.boye.httpclientandroidlib.entity.mime.MultipartEntityBuilder}
+ */
+ @Deprecated
+ public static StringBody create(
+ final String text,
+ final String mimeType,
+ final Charset charset) throws IllegalArgumentException {
+ try {
+ return new StringBody(text, mimeType, charset);
+ } catch (final UnsupportedEncodingException ex) {
+ throw new IllegalArgumentException("Charset " + charset + " is not supported", ex);
+ }
+ }
+
+ /**
+ * @since 4.1
+ *
+ * @deprecated (4.3) use {@link StringBody#StringBody(String, ContentType)}
+ * or {@link ch.boye.httpclientandroidlib.entity.mime.MultipartEntityBuilder}
+ */
+ @Deprecated
+ public static StringBody create(
+ final String text, final Charset charset) throws IllegalArgumentException {
+ return create(text, null, charset);
+ }
+
+ /**
+ * @since 4.1
+ *
+ * @deprecated (4.3) use {@link StringBody#StringBody(String, ContentType)}
+ * or {@link ch.boye.httpclientandroidlib.entity.mime.MultipartEntityBuilder}
+ */
+ @Deprecated
+ public static StringBody create(final String text) throws IllegalArgumentException {
+ return create(text, null, null);
+ }
+
+ /**
+ * Create a StringBody from the specified text, MIME type and character set.
+ *
+ * @param text to be used for the body, not {@code null}
+ * @param mimeType the MIME type, not {@code null}
+ * @param charset the character set, may be {@code null}, in which case the US-ASCII charset is used
+ * @throws UnsupportedEncodingException
+ * @throws IllegalArgumentException if the {@code text} parameter is null
+ *
+ * @deprecated (4.3) use {@link StringBody#StringBody(String, ContentType)}
+ * or {@link ch.boye.httpclientandroidlib.entity.mime.MultipartEntityBuilder}
+ */
+ @Deprecated
+ public StringBody(
+ final String text,
+ final String mimeType,
+ final Charset charset) throws UnsupportedEncodingException {
+ this(text, ContentType.create(mimeType, charset));
+ }
+
+ /**
+ * Create a StringBody from the specified text and character set.
+ * The MIME type is set to "text/plain".
+ *
+ * @param text to be used for the body, not {@code null}
+ * @param charset the character set, may be {@code null}, in which case the US-ASCII charset is used
+ * @throws UnsupportedEncodingException
+ * @throws IllegalArgumentException if the {@code text} parameter is null
+ *
+ * @deprecated (4.3) use {@link StringBody#StringBody(String, ContentType)}
+ * or {@link ch.boye.httpclientandroidlib.entity.mime.MultipartEntityBuilder}
+ */
+ @Deprecated
+ public StringBody(final String text, final Charset charset) throws UnsupportedEncodingException {
+ this(text, "text/plain", charset);
+ }
+
+ /**
+ * Create a StringBody from the specified text.
+ * The MIME type is set to "text/plain".
+ * The {@linkplain Consts#ASCII ASCII} charset is used.
+ *
+ * @param text to be used for the body, not {@code null}
+ * @throws UnsupportedEncodingException
+ * @throws IllegalArgumentException if the {@code text} parameter is null
+ *
+ * @deprecated (4.3) use {@link StringBody#StringBody(String, ContentType)}
+ * or {@link ch.boye.httpclientandroidlib.entity.mime.MultipartEntityBuilder}
+ */
+ @Deprecated
+ public StringBody(final String text) throws UnsupportedEncodingException {
+ this(text, "text/plain", Consts.ASCII);
+ }
+
+ /**
+ * @since 4.3
+ */
+ public StringBody(final String text, final ContentType contentType) {
+ super(contentType);
+ Args.notNull(text, "Text");
+ final Charset charset = contentType.getCharset();
+ final String csname = charset != null ? charset.name() : Consts.ASCII.name();
+ try {
+ this.content = text.getBytes(csname);
+ } catch (final UnsupportedEncodingException ex) {
+ // Should never happen
+ throw new UnsupportedCharsetException(csname);
+ }
+ }
+
+ public Reader getReader() {
+ final Charset charset = getContentType().getCharset();
+ return new InputStreamReader(
+ new ByteArrayInputStream(this.content),
+ charset != null ? charset : Consts.ASCII);
+ }
+
+ public void writeTo(final OutputStream out) throws IOException {
+ Args.notNull(out, "Output stream");
+ final InputStream in = new ByteArrayInputStream(this.content);
+ final byte[] tmp = new byte[4096];
+ int l;
+ while ((l = in.read(tmp)) != -1) {
+ out.write(tmp, 0, l);
+ }
+ out.flush();
+ }
+
+ public String getTransferEncoding() {
+ return MIME.ENC_8BIT;
+ }
+
+ public long getContentLength() {
+ return this.content.length;
+ }
+
+ public String getFilename() {
+ return null;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/package-info.java
new file mode 100644
index 0000000000..ff5e2ed0d8
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/content/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * MIME body part implementations.
+ */
+package ch.boye.httpclientandroidlib.entity.mime.content;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/package-info.java
new file mode 100644
index 0000000000..0ef01f9edc
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/mime/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * MIME coded HTTP entity implementations.
+ */
+package ch.boye.httpclientandroidlib.entity.mime;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/package-info.java
new file mode 100644
index 0000000000..0027ae98dd
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/entity/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Core HTTP entity implementations.
+ */
+package ch.boye.httpclientandroidlib.entity;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/AbstractHttpClientConnection.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/AbstractHttpClientConnection.java
new file mode 100644
index 0000000000..3f3c5f406e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/AbstractHttpClientConnection.java
@@ -0,0 +1,323 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl;
+
+import java.io.IOException;
+import java.net.SocketTimeoutException;
+
+import ch.boye.httpclientandroidlib.HttpClientConnection;
+import ch.boye.httpclientandroidlib.HttpConnectionMetrics;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpResponseFactory;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.impl.entity.EntityDeserializer;
+import ch.boye.httpclientandroidlib.impl.entity.EntitySerializer;
+import ch.boye.httpclientandroidlib.impl.entity.LaxContentLengthStrategy;
+import ch.boye.httpclientandroidlib.impl.entity.StrictContentLengthStrategy;
+import ch.boye.httpclientandroidlib.impl.io.DefaultHttpResponseParser;
+import ch.boye.httpclientandroidlib.impl.io.HttpRequestWriter;
+import ch.boye.httpclientandroidlib.io.EofSensor;
+import ch.boye.httpclientandroidlib.io.HttpMessageParser;
+import ch.boye.httpclientandroidlib.io.HttpMessageWriter;
+import ch.boye.httpclientandroidlib.io.HttpTransportMetrics;
+import ch.boye.httpclientandroidlib.io.SessionInputBuffer;
+import ch.boye.httpclientandroidlib.io.SessionOutputBuffer;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Abstract client-side HTTP connection capable of transmitting and receiving
+ * data using arbitrary {@link SessionInputBuffer} and
+ * {@link SessionOutputBuffer} implementations.
+ * <p>
+ * The following parameters can be used to customize the behavior of this
+ * class:
+ * <ul>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#STRICT_TRANSFER_ENCODING}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#MAX_HEADER_COUNT}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#MAX_LINE_LENGTH}</li>
+ * </ul>
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link DefaultBHttpClientConnection}
+ */
+@NotThreadSafe
+@Deprecated
+public abstract class AbstractHttpClientConnection implements HttpClientConnection {
+
+ private final EntitySerializer entityserializer;
+ private final EntityDeserializer entitydeserializer;
+
+ private SessionInputBuffer inbuffer = null;
+ private SessionOutputBuffer outbuffer = null;
+ private EofSensor eofSensor = null;
+ private HttpMessageParser<HttpResponse> responseParser = null;
+ private HttpMessageWriter<HttpRequest> requestWriter = null;
+ private HttpConnectionMetricsImpl metrics = null;
+
+ /**
+ * Creates an instance of this class.
+ * <p>
+ * This constructor will invoke {@link #createEntityDeserializer()}
+ * and {@link #createEntitySerializer()} methods in order to initialize
+ * HTTP entity serializer and deserializer implementations for this
+ * connection.
+ */
+ public AbstractHttpClientConnection() {
+ super();
+ this.entityserializer = createEntitySerializer();
+ this.entitydeserializer = createEntityDeserializer();
+ }
+
+ /**
+ * Asserts if the connection is open.
+ *
+ * @throws IllegalStateException if the connection is not open.
+ */
+ protected abstract void assertOpen() throws IllegalStateException;
+
+ /**
+ * Creates an instance of {@link EntityDeserializer} with the
+ * {@link LaxContentLengthStrategy} implementation to be used for
+ * de-serializing entities received over this connection.
+ * <p>
+ * This method can be overridden in a super class in order to create
+ * instances of {@link EntityDeserializer} using a custom
+ * {@link ch.boye.httpclientandroidlib.entity.ContentLengthStrategy}.
+ *
+ * @return HTTP entity deserializer
+ */
+ protected EntityDeserializer createEntityDeserializer() {
+ return new EntityDeserializer(new LaxContentLengthStrategy());
+ }
+
+ /**
+ * Creates an instance of {@link EntitySerializer} with the
+ * {@link StrictContentLengthStrategy} implementation to be used for
+ * serializing HTTP entities sent over this connection.
+ * <p>
+ * This method can be overridden in a super class in order to create
+ * instances of {@link EntitySerializer} using a custom
+ * {@link ch.boye.httpclientandroidlib.entity.ContentLengthStrategy}.
+ *
+ * @return HTTP entity serialzier.
+ */
+ protected EntitySerializer createEntitySerializer() {
+ return new EntitySerializer(new StrictContentLengthStrategy());
+ }
+
+ /**
+ * Creates an instance of {@link DefaultHttpResponseFactory} to be used
+ * for creating {@link HttpResponse} objects received by over this
+ * connection.
+ * <p>
+ * This method can be overridden in a super class in order to provide
+ * a different implementation of the {@link HttpResponseFactory} interface.
+ *
+ * @return HTTP response factory.
+ */
+ protected HttpResponseFactory createHttpResponseFactory() {
+ return DefaultHttpResponseFactory.INSTANCE;
+ }
+
+ /**
+ * Creates an instance of {@link HttpMessageParser} to be used for parsing
+ * HTTP responses received over this connection.
+ * <p>
+ * This method can be overridden in a super class in order to provide
+ * a different implementation of the {@link HttpMessageParser} interface or
+ * to pass a different implementation of the
+ * {@link ch.boye.httpclientandroidlib.message.LineParser} to the the
+ * {@link DefaultHttpResponseParser} constructor.
+ *
+ * @param buffer the session input buffer.
+ * @param responseFactory the HTTP response factory.
+ * @param params HTTP parameters.
+ * @return HTTP message parser.
+ */
+ protected HttpMessageParser<HttpResponse> createResponseParser(
+ final SessionInputBuffer buffer,
+ final HttpResponseFactory responseFactory,
+ final HttpParams params) {
+ return new DefaultHttpResponseParser(buffer, null, responseFactory, params);
+ }
+
+ /**
+ * Creates an instance of {@link HttpMessageWriter} to be used for
+ * writing out HTTP requests sent over this connection.
+ * <p>
+ * This method can be overridden in a super class in order to provide
+ * a different implementation of the {@link HttpMessageWriter} interface or
+ * to pass a different implementation of
+ * {@link ch.boye.httpclientandroidlib.message.LineFormatter} to the the default implementation
+ * {@link HttpRequestWriter}.
+ *
+ * @param buffer the session output buffer
+ * @param params HTTP parameters
+ * @return HTTP message writer
+ */
+ protected HttpMessageWriter<HttpRequest> createRequestWriter(
+ final SessionOutputBuffer buffer,
+ final HttpParams params) {
+ return new HttpRequestWriter(buffer, null, params);
+ }
+
+ /**
+ * @since 4.1
+ */
+ protected HttpConnectionMetricsImpl createConnectionMetrics(
+ final HttpTransportMetrics inTransportMetric,
+ final HttpTransportMetrics outTransportMetric) {
+ return new HttpConnectionMetricsImpl(inTransportMetric, outTransportMetric);
+ }
+
+ /**
+ * Initializes this connection object with {@link SessionInputBuffer} and
+ * {@link SessionOutputBuffer} instances to be used for sending and
+ * receiving data. These session buffers can be bound to any arbitrary
+ * physical output medium.
+ * <p>
+ * This method will invoke {@link #createHttpResponseFactory()},
+ * {@link #createRequestWriter(SessionOutputBuffer, HttpParams)}
+ * and {@link #createResponseParser(SessionInputBuffer, HttpResponseFactory, HttpParams)}
+ * methods to initialize HTTP request writer and response parser for this
+ * connection.
+ *
+ * @param inbuffer the session input buffer.
+ * @param outbuffer the session output buffer.
+ * @param params HTTP parameters.
+ */
+ protected void init(
+ final SessionInputBuffer inbuffer,
+ final SessionOutputBuffer outbuffer,
+ final HttpParams params) {
+ this.inbuffer = Args.notNull(inbuffer, "Input session buffer");
+ this.outbuffer = Args.notNull(outbuffer, "Output session buffer");
+ if (inbuffer instanceof EofSensor) {
+ this.eofSensor = (EofSensor) inbuffer;
+ }
+ this.responseParser = createResponseParser(
+ inbuffer,
+ createHttpResponseFactory(),
+ params);
+ this.requestWriter = createRequestWriter(
+ outbuffer, params);
+ this.metrics = createConnectionMetrics(
+ inbuffer.getMetrics(),
+ outbuffer.getMetrics());
+ }
+
+ public boolean isResponseAvailable(final int timeout) throws IOException {
+ assertOpen();
+ try {
+ return this.inbuffer.isDataAvailable(timeout);
+ } catch (final SocketTimeoutException ex) {
+ return false;
+ }
+ }
+
+ public void sendRequestHeader(final HttpRequest request)
+ throws HttpException, IOException {
+ Args.notNull(request, "HTTP request");
+ assertOpen();
+ this.requestWriter.write(request);
+ this.metrics.incrementRequestCount();
+ }
+
+ public void sendRequestEntity(final HttpEntityEnclosingRequest request)
+ throws HttpException, IOException {
+ Args.notNull(request, "HTTP request");
+ assertOpen();
+ if (request.getEntity() == null) {
+ return;
+ }
+ this.entityserializer.serialize(
+ this.outbuffer,
+ request,
+ request.getEntity());
+ }
+
+ protected void doFlush() throws IOException {
+ this.outbuffer.flush();
+ }
+
+ public void flush() throws IOException {
+ assertOpen();
+ doFlush();
+ }
+
+ public HttpResponse receiveResponseHeader()
+ throws HttpException, IOException {
+ assertOpen();
+ final HttpResponse response = this.responseParser.parse();
+ if (response.getStatusLine().getStatusCode() >= HttpStatus.SC_OK) {
+ this.metrics.incrementResponseCount();
+ }
+ return response;
+ }
+
+ public void receiveResponseEntity(final HttpResponse response)
+ throws HttpException, IOException {
+ Args.notNull(response, "HTTP response");
+ assertOpen();
+ final HttpEntity entity = this.entitydeserializer.deserialize(this.inbuffer, response);
+ response.setEntity(entity);
+ }
+
+ protected boolean isEof() {
+ return this.eofSensor != null && this.eofSensor.isEof();
+ }
+
+ public boolean isStale() {
+ if (!isOpen()) {
+ return true;
+ }
+ if (isEof()) {
+ return true;
+ }
+ try {
+ this.inbuffer.isDataAvailable(1);
+ return isEof();
+ } catch (final SocketTimeoutException ex) {
+ return false;
+ } catch (final IOException ex) {
+ return true;
+ }
+ }
+
+ public HttpConnectionMetrics getMetrics() {
+ return this.metrics;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/AbstractHttpServerConnection.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/AbstractHttpServerConnection.java
new file mode 100644
index 0000000000..e9e67e5b12
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/AbstractHttpServerConnection.java
@@ -0,0 +1,310 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpConnectionMetrics;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpRequestFactory;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpServerConnection;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.impl.entity.DisallowIdentityContentLengthStrategy;
+import ch.boye.httpclientandroidlib.impl.entity.EntityDeserializer;
+import ch.boye.httpclientandroidlib.impl.entity.EntitySerializer;
+import ch.boye.httpclientandroidlib.impl.entity.LaxContentLengthStrategy;
+import ch.boye.httpclientandroidlib.impl.entity.StrictContentLengthStrategy;
+import ch.boye.httpclientandroidlib.impl.io.DefaultHttpRequestParser;
+import ch.boye.httpclientandroidlib.impl.io.HttpResponseWriter;
+import ch.boye.httpclientandroidlib.io.EofSensor;
+import ch.boye.httpclientandroidlib.io.HttpMessageParser;
+import ch.boye.httpclientandroidlib.io.HttpMessageWriter;
+import ch.boye.httpclientandroidlib.io.HttpTransportMetrics;
+import ch.boye.httpclientandroidlib.io.SessionInputBuffer;
+import ch.boye.httpclientandroidlib.io.SessionOutputBuffer;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Abstract server-side HTTP connection capable of transmitting and receiving
+ * data using arbitrary {@link SessionInputBuffer} and
+ * {@link SessionOutputBuffer} implementations.
+ * <p>
+ * The following parameters can be used to customize the behavior of this
+ * class:
+ * <ul>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#STRICT_TRANSFER_ENCODING}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#MAX_HEADER_COUNT}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#MAX_LINE_LENGTH}</li>
+ * </ul>
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link DefaultBHttpServerConnection}
+ */
+@NotThreadSafe
+@Deprecated
+public abstract class AbstractHttpServerConnection implements HttpServerConnection {
+
+ private final EntitySerializer entityserializer;
+ private final EntityDeserializer entitydeserializer;
+
+ private SessionInputBuffer inbuffer = null;
+ private SessionOutputBuffer outbuffer = null;
+ private EofSensor eofSensor = null;
+ private HttpMessageParser<HttpRequest> requestParser = null;
+ private HttpMessageWriter<HttpResponse> responseWriter = null;
+ private HttpConnectionMetricsImpl metrics = null;
+
+ /**
+ * Creates an instance of this class.
+ * <p>
+ * This constructor will invoke {@link #createEntityDeserializer()}
+ * and {@link #createEntitySerializer()} methods in order to initialize
+ * HTTP entity serializer and deserializer implementations for this
+ * connection.
+ */
+ public AbstractHttpServerConnection() {
+ super();
+ this.entityserializer = createEntitySerializer();
+ this.entitydeserializer = createEntityDeserializer();
+ }
+
+ /**
+ * Asserts if the connection is open.
+ *
+ * @throws IllegalStateException if the connection is not open.
+ */
+ protected abstract void assertOpen() throws IllegalStateException;
+
+ /**
+ * Creates an instance of {@link EntityDeserializer} with the
+ * {@link LaxContentLengthStrategy} implementation to be used for
+ * de-serializing entities received over this connection.
+ * <p>
+ * This method can be overridden in a super class in order to create
+ * instances of {@link EntityDeserializer} using a custom
+ * {@link ch.boye.httpclientandroidlib.entity.ContentLengthStrategy}.
+ *
+ * @return HTTP entity deserializer
+ */
+ protected EntityDeserializer createEntityDeserializer() {
+ return new EntityDeserializer(new DisallowIdentityContentLengthStrategy(
+ new LaxContentLengthStrategy(0)));
+ }
+
+ /**
+ * Creates an instance of {@link EntitySerializer} with the
+ * {@link StrictContentLengthStrategy} implementation to be used for
+ * serializing HTTP entities sent over this connection.
+ * <p>
+ * This method can be overridden in a super class in order to create
+ * instances of {@link EntitySerializer} using a custom
+ * {@link ch.boye.httpclientandroidlib.entity.ContentLengthStrategy}.
+ *
+ * @return HTTP entity serialzier.
+ */
+ protected EntitySerializer createEntitySerializer() {
+ return new EntitySerializer(new StrictContentLengthStrategy());
+ }
+
+ /**
+ * Creates an instance of {@link DefaultHttpRequestFactory} to be used
+ * for creating {@link HttpRequest} objects received by over this
+ * connection.
+ * <p>
+ * This method can be overridden in a super class in order to provide
+ * a different implementation of the {@link HttpRequestFactory} interface.
+ *
+ * @return HTTP request factory.
+ */
+ protected HttpRequestFactory createHttpRequestFactory() {
+ return DefaultHttpRequestFactory.INSTANCE;
+ }
+
+ /**
+ * Creates an instance of {@link HttpMessageParser} to be used for parsing
+ * HTTP requests received over this connection.
+ * <p>
+ * This method can be overridden in a super class in order to provide
+ * a different implementation of the {@link HttpMessageParser} interface or
+ * to pass a different implementation of the
+ * {@link ch.boye.httpclientandroidlib.message.LineParser} to the
+ * {@link DefaultHttpRequestParser} constructor.
+ *
+ * @param buffer the session input buffer.
+ * @param requestFactory the HTTP request factory.
+ * @param params HTTP parameters.
+ * @return HTTP message parser.
+ */
+ protected HttpMessageParser<HttpRequest> createRequestParser(
+ final SessionInputBuffer buffer,
+ final HttpRequestFactory requestFactory,
+ final HttpParams params) {
+ return new DefaultHttpRequestParser(buffer, null, requestFactory, params);
+ }
+
+ /**
+ * Creates an instance of {@link HttpMessageWriter} to be used for
+ * writing out HTTP responses sent over this connection.
+ * <p>
+ * This method can be overridden in a super class in order to provide
+ * a different implementation of the {@link HttpMessageWriter} interface or
+ * to pass a different implementation of
+ * {@link ch.boye.httpclientandroidlib.message.LineFormatter} to the the default
+ * implementation {@link HttpResponseWriter}.
+ *
+ * @param buffer the session output buffer
+ * @param params HTTP parameters
+ * @return HTTP message writer
+ */
+ protected HttpMessageWriter<HttpResponse> createResponseWriter(
+ final SessionOutputBuffer buffer,
+ final HttpParams params) {
+ return new HttpResponseWriter(buffer, null, params);
+ }
+
+ /**
+ * @since 4.1
+ */
+ protected HttpConnectionMetricsImpl createConnectionMetrics(
+ final HttpTransportMetrics inTransportMetric,
+ final HttpTransportMetrics outTransportMetric) {
+ return new HttpConnectionMetricsImpl(inTransportMetric, outTransportMetric);
+ }
+
+ /**
+ * Initializes this connection object with {@link SessionInputBuffer} and
+ * {@link SessionOutputBuffer} instances to be used for sending and
+ * receiving data. These session buffers can be bound to any arbitrary
+ * physical output medium.
+ * <p>
+ * This method will invoke {@link #createHttpRequestFactory},
+ * {@link #createRequestParser(SessionInputBuffer, HttpRequestFactory, HttpParams)}
+ * and {@link #createResponseWriter(SessionOutputBuffer, HttpParams)}
+ * methods to initialize HTTP request parser and response writer for this
+ * connection.
+ *
+ * @param inbuffer the session input buffer.
+ * @param outbuffer the session output buffer.
+ * @param params HTTP parameters.
+ */
+ protected void init(
+ final SessionInputBuffer inbuffer,
+ final SessionOutputBuffer outbuffer,
+ final HttpParams params) {
+ this.inbuffer = Args.notNull(inbuffer, "Input session buffer");
+ this.outbuffer = Args.notNull(outbuffer, "Output session buffer");
+ if (inbuffer instanceof EofSensor) {
+ this.eofSensor = (EofSensor) inbuffer;
+ }
+ this.requestParser = createRequestParser(
+ inbuffer,
+ createHttpRequestFactory(),
+ params);
+ this.responseWriter = createResponseWriter(
+ outbuffer, params);
+ this.metrics = createConnectionMetrics(
+ inbuffer.getMetrics(),
+ outbuffer.getMetrics());
+ }
+
+ public HttpRequest receiveRequestHeader()
+ throws HttpException, IOException {
+ assertOpen();
+ final HttpRequest request = this.requestParser.parse();
+ this.metrics.incrementRequestCount();
+ return request;
+ }
+
+ public void receiveRequestEntity(final HttpEntityEnclosingRequest request)
+ throws HttpException, IOException {
+ Args.notNull(request, "HTTP request");
+ assertOpen();
+ final HttpEntity entity = this.entitydeserializer.deserialize(this.inbuffer, request);
+ request.setEntity(entity);
+ }
+
+ protected void doFlush() throws IOException {
+ this.outbuffer.flush();
+ }
+
+ public void flush() throws IOException {
+ assertOpen();
+ doFlush();
+ }
+
+ public void sendResponseHeader(final HttpResponse response)
+ throws HttpException, IOException {
+ Args.notNull(response, "HTTP response");
+ assertOpen();
+ this.responseWriter.write(response);
+ if (response.getStatusLine().getStatusCode() >= 200) {
+ this.metrics.incrementResponseCount();
+ }
+ }
+
+ public void sendResponseEntity(final HttpResponse response)
+ throws HttpException, IOException {
+ if (response.getEntity() == null) {
+ return;
+ }
+ this.entityserializer.serialize(
+ this.outbuffer,
+ response,
+ response.getEntity());
+ }
+
+ protected boolean isEof() {
+ return this.eofSensor != null && this.eofSensor.isEof();
+ }
+
+ public boolean isStale() {
+ if (!isOpen()) {
+ return true;
+ }
+ if (isEof()) {
+ return true;
+ }
+ try {
+ this.inbuffer.isDataAvailable(1);
+ return isEof();
+ } catch (final IOException ex) {
+ return true;
+ }
+ }
+
+ public HttpConnectionMetrics getMetrics() {
+ return this.metrics;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/BHttpConnectionBase.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/BHttpConnectionBase.java
new file mode 100644
index 0000000000..2215aa6e23
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/BHttpConnectionBase.java
@@ -0,0 +1,393 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CharsetEncoder;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpConnection;
+import ch.boye.httpclientandroidlib.HttpConnectionMetrics;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpInetConnection;
+import ch.boye.httpclientandroidlib.HttpMessage;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.config.MessageConstraints;
+import ch.boye.httpclientandroidlib.entity.BasicHttpEntity;
+import ch.boye.httpclientandroidlib.entity.ContentLengthStrategy;
+import ch.boye.httpclientandroidlib.impl.entity.LaxContentLengthStrategy;
+import ch.boye.httpclientandroidlib.impl.entity.StrictContentLengthStrategy;
+import ch.boye.httpclientandroidlib.impl.io.ChunkedInputStream;
+import ch.boye.httpclientandroidlib.impl.io.ChunkedOutputStream;
+import ch.boye.httpclientandroidlib.impl.io.ContentLengthInputStream;
+import ch.boye.httpclientandroidlib.impl.io.ContentLengthOutputStream;
+import ch.boye.httpclientandroidlib.impl.io.HttpTransportMetricsImpl;
+import ch.boye.httpclientandroidlib.impl.io.IdentityInputStream;
+import ch.boye.httpclientandroidlib.impl.io.IdentityOutputStream;
+import ch.boye.httpclientandroidlib.impl.io.SessionInputBufferImpl;
+import ch.boye.httpclientandroidlib.impl.io.SessionOutputBufferImpl;
+import ch.boye.httpclientandroidlib.io.SessionInputBuffer;
+import ch.boye.httpclientandroidlib.io.SessionOutputBuffer;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+import ch.boye.httpclientandroidlib.util.NetUtils;
+
+/**
+ * This class serves as a base for all {@link HttpConnection} implementations and provides
+ * functionality common to both client and server HTTP connections.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class BHttpConnectionBase implements HttpConnection, HttpInetConnection {
+
+ private final SessionInputBufferImpl inbuffer;
+ private final SessionOutputBufferImpl outbuffer;
+ private final HttpConnectionMetricsImpl connMetrics;
+ private final ContentLengthStrategy incomingContentStrategy;
+ private final ContentLengthStrategy outgoingContentStrategy;
+
+ private volatile boolean open;
+ private volatile Socket socket;
+
+ /**
+ * Creates new instance of BHttpConnectionBase.
+ *
+ * @param buffersize buffer size. Must be a positive number.
+ * @param fragmentSizeHint fragment size hint.
+ * @param chardecoder decoder to be used for decoding HTTP protocol elements.
+ * If <code>null</code> simple type cast will be used for byte to char conversion.
+ * @param charencoder encoder to be used for encoding HTTP protocol elements.
+ * If <code>null</code> simple type cast will be used for char to byte conversion.
+ * @param constraints Message constraints. If <code>null</code>
+ * {@link MessageConstraints#DEFAULT} will be used.
+ * @param incomingContentStrategy incoming content length strategy. If <code>null</code>
+ * {@link LaxContentLengthStrategy#INSTANCE} will be used.
+ * @param outgoingContentStrategy outgoing content length strategy. If <code>null</code>
+ * {@link StrictContentLengthStrategy#INSTANCE} will be used.
+ */
+ protected BHttpConnectionBase(
+ final int buffersize,
+ final int fragmentSizeHint,
+ final CharsetDecoder chardecoder,
+ final CharsetEncoder charencoder,
+ final MessageConstraints constraints,
+ final ContentLengthStrategy incomingContentStrategy,
+ final ContentLengthStrategy outgoingContentStrategy) {
+ super();
+ Args.positive(buffersize, "Buffer size");
+ final HttpTransportMetricsImpl inTransportMetrics = new HttpTransportMetricsImpl();
+ final HttpTransportMetricsImpl outTransportMetrics = new HttpTransportMetricsImpl();
+ this.inbuffer = new SessionInputBufferImpl(inTransportMetrics, buffersize, -1,
+ constraints != null ? constraints : MessageConstraints.DEFAULT, chardecoder);
+ this.outbuffer = new SessionOutputBufferImpl(outTransportMetrics, buffersize, fragmentSizeHint,
+ charencoder);
+ this.connMetrics = new HttpConnectionMetricsImpl(inTransportMetrics, outTransportMetrics);
+ this.incomingContentStrategy = incomingContentStrategy != null ? incomingContentStrategy :
+ LaxContentLengthStrategy.INSTANCE;
+ this.outgoingContentStrategy = outgoingContentStrategy != null ? outgoingContentStrategy :
+ StrictContentLengthStrategy.INSTANCE;
+ }
+
+ protected void ensureOpen() throws IOException {
+ Asserts.check(this.open, "Connection is not open");
+ if (!this.inbuffer.isBound()) {
+ this.inbuffer.bind(getSocketInputStream(this.socket));
+ }
+ if (!this.outbuffer.isBound()) {
+ this.outbuffer.bind(getSocketOutputStream(this.socket));
+ }
+ }
+
+ protected InputStream getSocketInputStream(final Socket socket) throws IOException {
+ return socket.getInputStream();
+ }
+
+ protected OutputStream getSocketOutputStream(final Socket socket) throws IOException {
+ return socket.getOutputStream();
+ }
+
+ /**
+ * Binds this connection to the given {@link Socket}. This socket will be
+ * used by the connection to send and receive data.
+ * <p/>
+ * After this method's execution the connection status will be reported
+ * as open and the {@link #isOpen()} will return <code>true</code>.
+ *
+ * @param socket the socket.
+ * @throws IOException in case of an I/O error.
+ */
+ protected void bind(final Socket socket) throws IOException {
+ Args.notNull(socket, "Socket");
+ this.socket = socket;
+ this.open = true;
+ this.inbuffer.bind(null);
+ this.outbuffer.bind(null);
+ }
+
+ protected SessionInputBuffer getSessionInputBuffer() {
+ return this.inbuffer;
+ }
+
+ protected SessionOutputBuffer getSessionOutputBuffer() {
+ return this.outbuffer;
+ }
+
+ protected void doFlush() throws IOException {
+ this.outbuffer.flush();
+ }
+
+ public boolean isOpen() {
+ return this.open;
+ }
+
+ protected Socket getSocket() {
+ return this.socket;
+ }
+
+ protected OutputStream createOutputStream(
+ final long len,
+ final SessionOutputBuffer outbuffer) {
+ if (len == ContentLengthStrategy.CHUNKED) {
+ return new ChunkedOutputStream(2048, outbuffer);
+ } else if (len == ContentLengthStrategy.IDENTITY) {
+ return new IdentityOutputStream(outbuffer);
+ } else {
+ return new ContentLengthOutputStream(outbuffer, len);
+ }
+ }
+
+ protected OutputStream prepareOutput(final HttpMessage message) throws HttpException {
+ final long len = this.outgoingContentStrategy.determineLength(message);
+ return createOutputStream(len, this.outbuffer);
+ }
+
+ protected InputStream createInputStream(
+ final long len,
+ final SessionInputBuffer inbuffer) {
+ if (len == ContentLengthStrategy.CHUNKED) {
+ return new ChunkedInputStream(inbuffer);
+ } else if (len == ContentLengthStrategy.IDENTITY) {
+ return new IdentityInputStream(inbuffer);
+ } else {
+ return new ContentLengthInputStream(inbuffer, len);
+ }
+ }
+
+ protected HttpEntity prepareInput(final HttpMessage message) throws HttpException {
+ final BasicHttpEntity entity = new BasicHttpEntity();
+
+ final long len = this.incomingContentStrategy.determineLength(message);
+ final InputStream instream = createInputStream(len, this.inbuffer);
+ if (len == ContentLengthStrategy.CHUNKED) {
+ entity.setChunked(true);
+ entity.setContentLength(-1);
+ entity.setContent(instream);
+ } else if (len == ContentLengthStrategy.IDENTITY) {
+ entity.setChunked(false);
+ entity.setContentLength(-1);
+ entity.setContent(instream);
+ } else {
+ entity.setChunked(false);
+ entity.setContentLength(len);
+ entity.setContent(instream);
+ }
+
+ final Header contentTypeHeader = message.getFirstHeader(HTTP.CONTENT_TYPE);
+ if (contentTypeHeader != null) {
+ entity.setContentType(contentTypeHeader);
+ }
+ final Header contentEncodingHeader = message.getFirstHeader(HTTP.CONTENT_ENCODING);
+ if (contentEncodingHeader != null) {
+ entity.setContentEncoding(contentEncodingHeader);
+ }
+ return entity;
+ }
+
+ public InetAddress getLocalAddress() {
+ if (this.socket != null) {
+ return this.socket.getLocalAddress();
+ } else {
+ return null;
+ }
+ }
+
+ public int getLocalPort() {
+ if (this.socket != null) {
+ return this.socket.getLocalPort();
+ } else {
+ return -1;
+ }
+ }
+
+ public InetAddress getRemoteAddress() {
+ if (this.socket != null) {
+ return this.socket.getInetAddress();
+ } else {
+ return null;
+ }
+ }
+
+ public int getRemotePort() {
+ if (this.socket != null) {
+ return this.socket.getPort();
+ } else {
+ return -1;
+ }
+ }
+
+ public void setSocketTimeout(final int timeout) {
+ if (this.socket != null) {
+ try {
+ this.socket.setSoTimeout(timeout);
+ } catch (final SocketException ignore) {
+ // It is not quite clear from the Sun's documentation if there are any
+ // other legitimate cases for a socket exception to be thrown when setting
+ // SO_TIMEOUT besides the socket being already closed
+ }
+ }
+ }
+
+ public int getSocketTimeout() {
+ if (this.socket != null) {
+ try {
+ return this.socket.getSoTimeout();
+ } catch (final SocketException ignore) {
+ return -1;
+ }
+ } else {
+ return -1;
+ }
+ }
+
+ public void shutdown() throws IOException {
+ this.open = false;
+ final Socket tmpsocket = this.socket;
+ if (tmpsocket != null) {
+ tmpsocket.close();
+ }
+ }
+
+ public void close() throws IOException {
+ if (!this.open) {
+ return;
+ }
+ this.open = false;
+ final Socket sock = this.socket;
+ try {
+ this.inbuffer.clear();
+ this.outbuffer.flush();
+ try {
+ try {
+ sock.shutdownOutput();
+ } catch (final IOException ignore) {
+ }
+ try {
+ sock.shutdownInput();
+ } catch (final IOException ignore) {
+ }
+ } catch (final UnsupportedOperationException ignore) {
+ // if one isn't supported, the other one isn't either
+ }
+ } finally {
+ sock.close();
+ }
+ }
+
+ private int fillInputBuffer(final int timeout) throws IOException {
+ final int oldtimeout = this.socket.getSoTimeout();
+ try {
+ this.socket.setSoTimeout(timeout);
+ return this.inbuffer.fillBuffer();
+ } finally {
+ this.socket.setSoTimeout(oldtimeout);
+ }
+ }
+
+ protected boolean awaitInput(final int timeout) throws IOException {
+ if (this.inbuffer.hasBufferedData()) {
+ return true;
+ }
+ fillInputBuffer(timeout);
+ return this.inbuffer.hasBufferedData();
+ }
+
+ public boolean isStale() {
+ if (!isOpen()) {
+ return true;
+ }
+ try {
+ final int bytesRead = fillInputBuffer(1);
+ return bytesRead < 0;
+ } catch (final SocketTimeoutException ex) {
+ return false;
+ } catch (final IOException ex) {
+ return true;
+ }
+ }
+
+ protected void incrementRequestCount() {
+ this.connMetrics.incrementRequestCount();
+ }
+
+ protected void incrementResponseCount() {
+ this.connMetrics.incrementResponseCount();
+ }
+
+ public HttpConnectionMetrics getMetrics() {
+ return this.connMetrics;
+ }
+
+ @Override
+ public String toString() {
+ if (this.socket != null) {
+ final StringBuilder buffer = new StringBuilder();
+ final SocketAddress remoteAddress = this.socket.getRemoteSocketAddress();
+ final SocketAddress localAddress = this.socket.getLocalSocketAddress();
+ if (remoteAddress != null && localAddress != null) {
+ NetUtils.formatAddress(buffer, localAddress);
+ buffer.append("<->");
+ NetUtils.formatAddress(buffer, remoteAddress);
+ }
+ return buffer.toString();
+ } else {
+ return "[Not bound]";
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/ConnSupport.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/ConnSupport.java
new file mode 100644
index 0000000000..ba82a2a7c6
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/ConnSupport.java
@@ -0,0 +1,75 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl;
+
+import ch.boye.httpclientandroidlib.config.ConnectionConfig;
+
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CodingErrorAction;
+
+/**
+ * Connection support methods.
+ *
+ * @since 4.3
+ */
+public final class ConnSupport {
+
+ public static CharsetDecoder createDecoder(final ConnectionConfig cconfig) {
+ if (cconfig == null) {
+ return null;
+ }
+ final Charset charset = cconfig.getCharset();
+ final CodingErrorAction malformed = cconfig.getMalformedInputAction();
+ final CodingErrorAction unmappable = cconfig.getUnmappableInputAction();
+ if (charset != null) {
+ return charset.newDecoder()
+ .onMalformedInput(malformed != null ? malformed : CodingErrorAction.REPORT)
+ .onUnmappableCharacter(unmappable != null ? unmappable: CodingErrorAction.REPORT);
+ } else {
+ return null;
+ }
+ }
+
+ public static CharsetEncoder createEncoder(final ConnectionConfig cconfig) {
+ if (cconfig == null) {
+ return null;
+ }
+ final Charset charset = cconfig.getCharset();
+ if (charset != null) {
+ final CodingErrorAction malformed = cconfig.getMalformedInputAction();
+ final CodingErrorAction unmappable = cconfig.getUnmappableInputAction();
+ return charset.newEncoder()
+ .onMalformedInput(malformed != null ? malformed : CodingErrorAction.REPORT)
+ .onUnmappableCharacter(unmappable != null ? unmappable: CodingErrorAction.REPORT);
+ } else {
+ return null;
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultBHttpClientConnection.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultBHttpClientConnection.java
new file mode 100644
index 0000000000..9627e53f29
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultBHttpClientConnection.java
@@ -0,0 +1,182 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CharsetEncoder;
+
+import ch.boye.httpclientandroidlib.HttpClientConnection;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.config.MessageConstraints;
+import ch.boye.httpclientandroidlib.entity.ContentLengthStrategy;
+import ch.boye.httpclientandroidlib.impl.io.DefaultHttpRequestWriterFactory;
+import ch.boye.httpclientandroidlib.impl.io.DefaultHttpResponseParserFactory;
+import ch.boye.httpclientandroidlib.io.HttpMessageParser;
+import ch.boye.httpclientandroidlib.io.HttpMessageParserFactory;
+import ch.boye.httpclientandroidlib.io.HttpMessageWriter;
+import ch.boye.httpclientandroidlib.io.HttpMessageWriterFactory;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Default implementation of {@link HttpClientConnection}.
+ *
+ * @since 4.3
+ */
+@NotThreadSafe
+public class DefaultBHttpClientConnection extends BHttpConnectionBase
+ implements HttpClientConnection {
+
+ private final HttpMessageParser<HttpResponse> responseParser;
+ private final HttpMessageWriter<HttpRequest> requestWriter;
+
+ /**
+ * Creates new instance of DefaultBHttpClientConnection.
+ *
+ * @param buffersize buffer size. Must be a positive number.
+ * @param fragmentSizeHint fragment size hint.
+ * @param chardecoder decoder to be used for decoding HTTP protocol elements.
+ * If <code>null</code> simple type cast will be used for byte to char conversion.
+ * @param charencoder encoder to be used for encoding HTTP protocol elements.
+ * If <code>null</code> simple type cast will be used for char to byte conversion.
+ * @param constraints Message constraints. If <code>null</code>
+ * {@link MessageConstraints#DEFAULT} will be used.
+ * @param incomingContentStrategy incoming content length strategy. If <code>null</code>
+ * {@link ch.boye.httpclientandroidlib.impl.entity.LaxContentLengthStrategy#INSTANCE} will be used.
+ * @param outgoingContentStrategy outgoing content length strategy. If <code>null</code>
+ * {@link ch.boye.httpclientandroidlib.impl.entity.StrictContentLengthStrategy#INSTANCE} will be used.
+ * @param requestWriterFactory request writer factory. If <code>null</code>
+ * {@link DefaultHttpRequestWriterFactory#INSTANCE} will be used.
+ * @param responseParserFactory response parser factory. If <code>null</code>
+ * {@link DefaultHttpResponseParserFactory#INSTANCE} will be used.
+ */
+ public DefaultBHttpClientConnection(
+ final int buffersize,
+ final int fragmentSizeHint,
+ final CharsetDecoder chardecoder,
+ final CharsetEncoder charencoder,
+ final MessageConstraints constraints,
+ final ContentLengthStrategy incomingContentStrategy,
+ final ContentLengthStrategy outgoingContentStrategy,
+ final HttpMessageWriterFactory<HttpRequest> requestWriterFactory,
+ final HttpMessageParserFactory<HttpResponse> responseParserFactory) {
+ super(buffersize, fragmentSizeHint, chardecoder, charencoder,
+ constraints, incomingContentStrategy, outgoingContentStrategy);
+ this.requestWriter = (requestWriterFactory != null ? requestWriterFactory :
+ DefaultHttpRequestWriterFactory.INSTANCE).create(getSessionOutputBuffer());
+ this.responseParser = (responseParserFactory != null ? responseParserFactory :
+ DefaultHttpResponseParserFactory.INSTANCE).create(getSessionInputBuffer(), constraints);
+ }
+
+ public DefaultBHttpClientConnection(
+ final int buffersize,
+ final CharsetDecoder chardecoder,
+ final CharsetEncoder charencoder,
+ final MessageConstraints constraints) {
+ this(buffersize, buffersize, chardecoder, charencoder, constraints, null, null, null, null);
+ }
+
+ public DefaultBHttpClientConnection(final int buffersize) {
+ this(buffersize, buffersize, null, null, null, null, null, null, null);
+ }
+
+ protected void onResponseReceived(final HttpResponse response) {
+ }
+
+ protected void onRequestSubmitted(final HttpRequest request) {
+ }
+
+ @Override
+ public void bind(final Socket socket) throws IOException {
+ super.bind(socket);
+ }
+
+ public boolean isResponseAvailable(final int timeout) throws IOException {
+ ensureOpen();
+ try {
+ return awaitInput(timeout);
+ } catch (final SocketTimeoutException ex) {
+ return false;
+ }
+ }
+
+ public void sendRequestHeader(final HttpRequest request)
+ throws HttpException, IOException {
+ Args.notNull(request, "HTTP request");
+ ensureOpen();
+ this.requestWriter.write(request);
+ onRequestSubmitted(request);
+ incrementRequestCount();
+ }
+
+ public void sendRequestEntity(final HttpEntityEnclosingRequest request)
+ throws HttpException, IOException {
+ Args.notNull(request, "HTTP request");
+ ensureOpen();
+ final HttpEntity entity = request.getEntity();
+ if (entity == null) {
+ return;
+ }
+ final OutputStream outstream = prepareOutput(request);
+ entity.writeTo(outstream);
+ outstream.close();
+ }
+
+ public HttpResponse receiveResponseHeader() throws HttpException, IOException {
+ ensureOpen();
+ final HttpResponse response = this.responseParser.parse();
+ onResponseReceived(response);
+ if (response.getStatusLine().getStatusCode() >= HttpStatus.SC_OK) {
+ incrementResponseCount();
+ }
+ return response;
+ }
+
+ public void receiveResponseEntity(
+ final HttpResponse response) throws HttpException, IOException {
+ Args.notNull(response, "HTTP response");
+ ensureOpen();
+ final HttpEntity entity = prepareInput(response);
+ response.setEntity(entity);
+ }
+
+ public void flush() throws IOException {
+ ensureOpen();
+ doFlush();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultBHttpClientConnectionFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultBHttpClientConnectionFactory.java
new file mode 100644
index 0000000000..f337339c3a
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultBHttpClientConnectionFactory.java
@@ -0,0 +1,103 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl;
+
+import ch.boye.httpclientandroidlib.HttpConnectionFactory;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.config.ConnectionConfig;
+import ch.boye.httpclientandroidlib.entity.ContentLengthStrategy;
+import ch.boye.httpclientandroidlib.io.HttpMessageParserFactory;
+import ch.boye.httpclientandroidlib.io.HttpMessageWriterFactory;
+
+import java.io.IOException;
+import java.net.Socket;
+
+/**
+ * Default factory for {@link ch.boye.httpclientandroidlib.HttpClientConnection}s.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class DefaultBHttpClientConnectionFactory
+ implements HttpConnectionFactory<DefaultBHttpClientConnection> {
+
+ public static final DefaultBHttpClientConnectionFactory INSTANCE = new DefaultBHttpClientConnectionFactory();
+
+ private final ConnectionConfig cconfig;
+ private final ContentLengthStrategy incomingContentStrategy;
+ private final ContentLengthStrategy outgoingContentStrategy;
+ private final HttpMessageWriterFactory<HttpRequest> requestWriterFactory;
+ private final HttpMessageParserFactory<HttpResponse> responseParserFactory;
+
+ public DefaultBHttpClientConnectionFactory(
+ final ConnectionConfig cconfig,
+ final ContentLengthStrategy incomingContentStrategy,
+ final ContentLengthStrategy outgoingContentStrategy,
+ final HttpMessageWriterFactory<HttpRequest> requestWriterFactory,
+ final HttpMessageParserFactory<HttpResponse> responseParserFactory) {
+ super();
+ this.cconfig = cconfig != null ? cconfig : ConnectionConfig.DEFAULT;
+ this.incomingContentStrategy = incomingContentStrategy;
+ this.outgoingContentStrategy = outgoingContentStrategy;
+ this.requestWriterFactory = requestWriterFactory;
+ this.responseParserFactory = responseParserFactory;
+ }
+
+ public DefaultBHttpClientConnectionFactory(
+ final ConnectionConfig cconfig,
+ final HttpMessageWriterFactory<HttpRequest> requestWriterFactory,
+ final HttpMessageParserFactory<HttpResponse> responseParserFactory) {
+ this(cconfig, null, null, requestWriterFactory, responseParserFactory);
+ }
+
+ public DefaultBHttpClientConnectionFactory(final ConnectionConfig cconfig) {
+ this(cconfig, null, null, null, null);
+ }
+
+ public DefaultBHttpClientConnectionFactory() {
+ this(null, null, null, null, null);
+ }
+
+ public DefaultBHttpClientConnection createConnection(final Socket socket) throws IOException {
+ final DefaultBHttpClientConnection conn = new DefaultBHttpClientConnection(
+ this.cconfig.getBufferSize(),
+ this.cconfig.getFragmentSizeHint(),
+ ConnSupport.createDecoder(this.cconfig),
+ ConnSupport.createEncoder(this.cconfig),
+ this.cconfig.getMessageConstraints(),
+ this.incomingContentStrategy,
+ this.outgoingContentStrategy,
+ this.requestWriterFactory,
+ this.responseParserFactory);
+ conn.bind(socket);
+ return conn;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultBHttpServerConnection.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultBHttpServerConnection.java
new file mode 100644
index 0000000000..6ee4fb7406
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultBHttpServerConnection.java
@@ -0,0 +1,174 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CharsetEncoder;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpServerConnection;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.config.MessageConstraints;
+import ch.boye.httpclientandroidlib.entity.ContentLengthStrategy;
+import ch.boye.httpclientandroidlib.impl.entity.DisallowIdentityContentLengthStrategy;
+import ch.boye.httpclientandroidlib.impl.io.DefaultHttpRequestParserFactory;
+import ch.boye.httpclientandroidlib.impl.io.DefaultHttpResponseWriterFactory;
+import ch.boye.httpclientandroidlib.io.HttpMessageParser;
+import ch.boye.httpclientandroidlib.io.HttpMessageParserFactory;
+import ch.boye.httpclientandroidlib.io.HttpMessageWriter;
+import ch.boye.httpclientandroidlib.io.HttpMessageWriterFactory;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Default implementation of {@link HttpServerConnection}.
+ *
+ * @since 4.3
+ */
+@NotThreadSafe
+public class DefaultBHttpServerConnection extends BHttpConnectionBase
+ implements HttpServerConnection {
+
+ private final HttpMessageParser<HttpRequest> requestParser;
+ private final HttpMessageWriter<HttpResponse> responseWriter;
+
+ /**
+ * Creates new instance of DefaultBHttpServerConnection.
+ *
+ * @param buffersize buffer size. Must be a positive number.
+ * @param fragmentSizeHint fragment size hint.
+ * @param chardecoder decoder to be used for decoding HTTP protocol elements.
+ * If <code>null</code> simple type cast will be used for byte to char conversion.
+ * @param charencoder encoder to be used for encoding HTTP protocol elements.
+ * If <code>null</code> simple type cast will be used for char to byte conversion.
+ * @param constraints Message constraints. If <code>null</code>
+ * {@link MessageConstraints#DEFAULT} will be used.
+ * @param incomingContentStrategy incoming content length strategy. If <code>null</code>
+ * {@link DisallowIdentityContentLengthStrategy#INSTANCE} will be used.
+ * @param outgoingContentStrategy outgoing content length strategy. If <code>null</code>
+ * {@link ch.boye.httpclientandroidlib.impl.entity.StrictContentLengthStrategy#INSTANCE} will be used.
+ * @param requestParserFactory request parser factory. If <code>null</code>
+ * {@link DefaultHttpRequestParserFactory#INSTANCE} will be used.
+ * @param responseWriterFactory response writer factory. If <code>null</code>
+ * {@link DefaultHttpResponseWriterFactory#INSTANCE} will be used.
+ */
+ public DefaultBHttpServerConnection(
+ final int buffersize,
+ final int fragmentSizeHint,
+ final CharsetDecoder chardecoder,
+ final CharsetEncoder charencoder,
+ final MessageConstraints constraints,
+ final ContentLengthStrategy incomingContentStrategy,
+ final ContentLengthStrategy outgoingContentStrategy,
+ final HttpMessageParserFactory<HttpRequest> requestParserFactory,
+ final HttpMessageWriterFactory<HttpResponse> responseWriterFactory) {
+ super(buffersize, fragmentSizeHint, chardecoder, charencoder, constraints,
+ incomingContentStrategy != null ? incomingContentStrategy :
+ DisallowIdentityContentLengthStrategy.INSTANCE, outgoingContentStrategy);
+ this.requestParser = (requestParserFactory != null ? requestParserFactory :
+ DefaultHttpRequestParserFactory.INSTANCE).create(getSessionInputBuffer(), constraints);
+ this.responseWriter = (responseWriterFactory != null ? responseWriterFactory :
+ DefaultHttpResponseWriterFactory.INSTANCE).create(getSessionOutputBuffer());
+ }
+
+ public DefaultBHttpServerConnection(
+ final int buffersize,
+ final CharsetDecoder chardecoder,
+ final CharsetEncoder charencoder,
+ final MessageConstraints constraints) {
+ this(buffersize, buffersize, chardecoder, charencoder, constraints, null, null, null, null);
+ }
+
+ public DefaultBHttpServerConnection(final int buffersize) {
+ this(buffersize, buffersize, null, null, null, null, null, null, null);
+ }
+
+ protected void onRequestReceived(final HttpRequest request) {
+ }
+
+ protected void onResponseSubmitted(final HttpResponse response) {
+ }
+
+ @Override
+ public void bind(final Socket socket) throws IOException {
+ super.bind(socket);
+ }
+
+ public HttpRequest receiveRequestHeader()
+ throws HttpException, IOException {
+ ensureOpen();
+ final HttpRequest request = this.requestParser.parse();
+ onRequestReceived(request);
+ incrementRequestCount();
+ return request;
+ }
+
+ public void receiveRequestEntity(final HttpEntityEnclosingRequest request)
+ throws HttpException, IOException {
+ Args.notNull(request, "HTTP request");
+ ensureOpen();
+ final HttpEntity entity = prepareInput(request);
+ request.setEntity(entity);
+ }
+
+ public void sendResponseHeader(final HttpResponse response)
+ throws HttpException, IOException {
+ Args.notNull(response, "HTTP response");
+ ensureOpen();
+ this.responseWriter.write(response);
+ onResponseSubmitted(response);
+ if (response.getStatusLine().getStatusCode() >= 200) {
+ incrementResponseCount();
+ }
+ }
+
+ public void sendResponseEntity(final HttpResponse response)
+ throws HttpException, IOException {
+ Args.notNull(response, "HTTP response");
+ ensureOpen();
+ final HttpEntity entity = response.getEntity();
+ if (entity == null) {
+ return;
+ }
+ final OutputStream outstream = prepareOutput(response);
+ entity.writeTo(outstream);
+ outstream.close();
+ }
+
+ public void flush() throws IOException {
+ ensureOpen();
+ doFlush();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultBHttpServerConnectionFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultBHttpServerConnectionFactory.java
new file mode 100644
index 0000000000..a312320aa1
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultBHttpServerConnectionFactory.java
@@ -0,0 +1,103 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl;
+
+import ch.boye.httpclientandroidlib.HttpConnectionFactory;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.config.ConnectionConfig;
+import ch.boye.httpclientandroidlib.entity.ContentLengthStrategy;
+import ch.boye.httpclientandroidlib.io.HttpMessageParserFactory;
+import ch.boye.httpclientandroidlib.io.HttpMessageWriterFactory;
+
+import java.io.IOException;
+import java.net.Socket;
+
+/**
+ * Default factory for {@link ch.boye.httpclientandroidlib.HttpServerConnection}s.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class DefaultBHttpServerConnectionFactory
+ implements HttpConnectionFactory<DefaultBHttpServerConnection> {
+
+ public static final DefaultBHttpServerConnectionFactory INSTANCE = new DefaultBHttpServerConnectionFactory();
+
+ private final ConnectionConfig cconfig;
+ private final ContentLengthStrategy incomingContentStrategy;
+ private final ContentLengthStrategy outgoingContentStrategy;
+ private final HttpMessageParserFactory<HttpRequest> requestParserFactory;
+ private final HttpMessageWriterFactory<HttpResponse> responseWriterFactory;
+
+ public DefaultBHttpServerConnectionFactory(
+ final ConnectionConfig cconfig,
+ final ContentLengthStrategy incomingContentStrategy,
+ final ContentLengthStrategy outgoingContentStrategy,
+ final HttpMessageParserFactory<HttpRequest> requestParserFactory,
+ final HttpMessageWriterFactory<HttpResponse> responseWriterFactory) {
+ super();
+ this.cconfig = cconfig != null ? cconfig : ConnectionConfig.DEFAULT;
+ this.incomingContentStrategy = incomingContentStrategy;
+ this.outgoingContentStrategy = outgoingContentStrategy;
+ this.requestParserFactory = requestParserFactory;
+ this.responseWriterFactory = responseWriterFactory;
+ }
+
+ public DefaultBHttpServerConnectionFactory(
+ final ConnectionConfig cconfig,
+ final HttpMessageParserFactory<HttpRequest> requestParserFactory,
+ final HttpMessageWriterFactory<HttpResponse> responseWriterFactory) {
+ this(cconfig, null, null, requestParserFactory, responseWriterFactory);
+ }
+
+ public DefaultBHttpServerConnectionFactory(final ConnectionConfig cconfig) {
+ this(cconfig, null, null, null, null);
+ }
+
+ public DefaultBHttpServerConnectionFactory() {
+ this(null, null, null, null, null);
+ }
+
+ public DefaultBHttpServerConnection createConnection(final Socket socket) throws IOException {
+ final DefaultBHttpServerConnection conn = new DefaultBHttpServerConnection(
+ this.cconfig.getBufferSize(),
+ this.cconfig.getFragmentSizeHint(),
+ ConnSupport.createDecoder(this.cconfig),
+ ConnSupport.createEncoder(this.cconfig),
+ this.cconfig.getMessageConstraints(),
+ this.incomingContentStrategy,
+ this.outgoingContentStrategy,
+ this.requestParserFactory,
+ this.responseWriterFactory);
+ conn.bind(socket);
+ return conn;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultConnectionReuseStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultConnectionReuseStrategy.java
new file mode 100644
index 0000000000..b58127e5fc
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultConnectionReuseStrategy.java
@@ -0,0 +1,189 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl;
+
+import ch.boye.httpclientandroidlib.ConnectionReuseStrategy;
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderIterator;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.ParseException;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.TokenIterator;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.message.BasicTokenIterator;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Default implementation of a strategy deciding about connection re-use.
+ * The default implementation first checks some basics, for example
+ * whether the connection is still open or whether the end of the
+ * request entity can be determined without closing the connection.
+ * If these checks pass, the tokens in the <code>Connection</code> header will
+ * be examined. In the absence of a <code>Connection</code> header, the
+ * non-standard but commonly used <code>Proxy-Connection</code> header takes
+ * it's role. A token <code>close</code> indicates that the connection cannot
+ * be reused. If there is no such token, a token <code>keep-alive</code>
+ * indicates that the connection should be re-used. If neither token is found,
+ * or if there are no <code>Connection</code> headers, the default policy for
+ * the HTTP version is applied. Since <code>HTTP/1.1</code>, connections are
+ * re-used by default. Up until <code>HTTP/1.0</code>, connections are not
+ * re-used by default.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class DefaultConnectionReuseStrategy implements ConnectionReuseStrategy {
+
+ public static final DefaultConnectionReuseStrategy INSTANCE = new DefaultConnectionReuseStrategy();
+
+ public DefaultConnectionReuseStrategy() {
+ super();
+ }
+
+ // see interface ConnectionReuseStrategy
+ public boolean keepAlive(final HttpResponse response,
+ final HttpContext context) {
+ Args.notNull(response, "HTTP response");
+ Args.notNull(context, "HTTP context");
+
+ // Check for a self-terminating entity. If the end of the entity will
+ // be indicated by closing the connection, there is no keep-alive.
+ final ProtocolVersion ver = response.getStatusLine().getProtocolVersion();
+ final Header teh = response.getFirstHeader(HTTP.TRANSFER_ENCODING);
+ if (teh != null) {
+ if (!HTTP.CHUNK_CODING.equalsIgnoreCase(teh.getValue())) {
+ return false;
+ }
+ } else {
+ if (canResponseHaveBody(response)) {
+ final Header[] clhs = response.getHeaders(HTTP.CONTENT_LEN);
+ // Do not reuse if not properly content-length delimited
+ if (clhs.length == 1) {
+ final Header clh = clhs[0];
+ try {
+ final int contentLen = Integer.parseInt(clh.getValue());
+ if (contentLen < 0) {
+ return false;
+ }
+ } catch (final NumberFormatException ex) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+ }
+
+ // Check for the "Connection" header. If that is absent, check for
+ // the "Proxy-Connection" header. The latter is an unspecified and
+ // broken but unfortunately common extension of HTTP.
+ HeaderIterator hit = response.headerIterator(HTTP.CONN_DIRECTIVE);
+ if (!hit.hasNext()) {
+ hit = response.headerIterator("Proxy-Connection");
+ }
+
+ // Experimental usage of the "Connection" header in HTTP/1.0 is
+ // documented in RFC 2068, section 19.7.1. A token "keep-alive" is
+ // used to indicate that the connection should be persistent.
+ // Note that the final specification of HTTP/1.1 in RFC 2616 does not
+ // include this information. Neither is the "Connection" header
+ // mentioned in RFC 1945, which informally describes HTTP/1.0.
+ //
+ // RFC 2616 specifies "close" as the only connection token with a
+ // specific meaning: it disables persistent connections.
+ //
+ // The "Proxy-Connection" header is not formally specified anywhere,
+ // but is commonly used to carry one token, "close" or "keep-alive".
+ // The "Connection" header, on the other hand, is defined as a
+ // sequence of tokens, where each token is a header name, and the
+ // token "close" has the above-mentioned additional meaning.
+ //
+ // To get through this mess, we treat the "Proxy-Connection" header
+ // in exactly the same way as the "Connection" header, but only if
+ // the latter is missing. We scan the sequence of tokens for both
+ // "close" and "keep-alive". As "close" is specified by RFC 2068,
+ // it takes precedence and indicates a non-persistent connection.
+ // If there is no "close" but a "keep-alive", we take the hint.
+
+ if (hit.hasNext()) {
+ try {
+ final TokenIterator ti = createTokenIterator(hit);
+ boolean keepalive = false;
+ while (ti.hasNext()) {
+ final String token = ti.nextToken();
+ if (HTTP.CONN_CLOSE.equalsIgnoreCase(token)) {
+ return false;
+ } else if (HTTP.CONN_KEEP_ALIVE.equalsIgnoreCase(token)) {
+ // continue the loop, there may be a "close" afterwards
+ keepalive = true;
+ }
+ }
+ if (keepalive)
+ {
+ return true;
+ // neither "close" nor "keep-alive", use default policy
+ }
+
+ } catch (final ParseException px) {
+ // invalid connection header means no persistent connection
+ // we don't have logging in HttpCore, so the exception is lost
+ return false;
+ }
+ }
+
+ // default since HTTP/1.1 is persistent, before it was non-persistent
+ return !ver.lessEquals(HttpVersion.HTTP_1_0);
+ }
+
+
+ /**
+ * Creates a token iterator from a header iterator.
+ * This method can be overridden to replace the implementation of
+ * the token iterator.
+ *
+ * @param hit the header iterator
+ *
+ * @return the token iterator
+ */
+ protected TokenIterator createTokenIterator(final HeaderIterator hit) {
+ return new BasicTokenIterator(hit);
+ }
+
+ private boolean canResponseHaveBody(final HttpResponse response) {
+ final int status = response.getStatusLine().getStatusCode();
+ return status >= HttpStatus.SC_OK
+ && status != HttpStatus.SC_NO_CONTENT
+ && status != HttpStatus.SC_NOT_MODIFIED
+ && status != HttpStatus.SC_RESET_CONTENT;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultHttpClientConnection.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultHttpClientConnection.java
new file mode 100644
index 0000000000..adb5978630
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultHttpClientConnection.java
@@ -0,0 +1,70 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl;
+
+import java.io.IOException;
+import java.net.Socket;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.params.CoreConnectionPNames;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Default implementation of a client-side HTTP connection.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link DefaultBHttpClientConnection}
+ */
+@NotThreadSafe
+@Deprecated
+public class DefaultHttpClientConnection extends SocketHttpClientConnection {
+
+ public DefaultHttpClientConnection() {
+ super();
+ }
+
+ @Override
+ public void bind(
+ final Socket socket,
+ final HttpParams params) throws IOException {
+ Args.notNull(socket, "Socket");
+ Args.notNull(params, "HTTP parameters");
+ assertNotOpen();
+ socket.setTcpNoDelay(params.getBooleanParameter(CoreConnectionPNames.TCP_NODELAY, true));
+ socket.setSoTimeout(params.getIntParameter(CoreConnectionPNames.SO_TIMEOUT, 0));
+ socket.setKeepAlive(params.getBooleanParameter(CoreConnectionPNames.SO_KEEPALIVE, false));
+ final int linger = params.getIntParameter(CoreConnectionPNames.SO_LINGER, -1);
+ if (linger >= 0) {
+ socket.setSoLinger(linger > 0, linger);
+ }
+ super.bind(socket, params);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultHttpRequestFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultHttpRequestFactory.java
new file mode 100644
index 0000000000..0491e94bd6
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultHttpRequestFactory.java
@@ -0,0 +1,109 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl;
+
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpRequestFactory;
+import ch.boye.httpclientandroidlib.MethodNotSupportedException;
+import ch.boye.httpclientandroidlib.RequestLine;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.message.BasicHttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.message.BasicHttpRequest;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Default factory for creating {@link HttpRequest} objects.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class DefaultHttpRequestFactory implements HttpRequestFactory {
+
+ public static final DefaultHttpRequestFactory INSTANCE = new DefaultHttpRequestFactory();
+
+ private static final String[] RFC2616_COMMON_METHODS = {
+ "GET"
+ };
+
+ private static final String[] RFC2616_ENTITY_ENC_METHODS = {
+ "POST",
+ "PUT"
+ };
+
+ private static final String[] RFC2616_SPECIAL_METHODS = {
+ "HEAD",
+ "OPTIONS",
+ "DELETE",
+ "TRACE",
+ "CONNECT"
+ };
+
+
+ public DefaultHttpRequestFactory() {
+ super();
+ }
+
+ private static boolean isOneOf(final String[] methods, final String method) {
+ for (final String method2 : methods) {
+ if (method2.equalsIgnoreCase(method)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public HttpRequest newHttpRequest(final RequestLine requestline)
+ throws MethodNotSupportedException {
+ Args.notNull(requestline, "Request line");
+ final String method = requestline.getMethod();
+ if (isOneOf(RFC2616_COMMON_METHODS, method)) {
+ return new BasicHttpRequest(requestline);
+ } else if (isOneOf(RFC2616_ENTITY_ENC_METHODS, method)) {
+ return new BasicHttpEntityEnclosingRequest(requestline);
+ } else if (isOneOf(RFC2616_SPECIAL_METHODS, method)) {
+ return new BasicHttpRequest(requestline);
+ } else {
+ throw new MethodNotSupportedException(method + " method not supported");
+ }
+ }
+
+ public HttpRequest newHttpRequest(final String method, final String uri)
+ throws MethodNotSupportedException {
+ if (isOneOf(RFC2616_COMMON_METHODS, method)) {
+ return new BasicHttpRequest(method, uri);
+ } else if (isOneOf(RFC2616_ENTITY_ENC_METHODS, method)) {
+ return new BasicHttpEntityEnclosingRequest(method, uri);
+ } else if (isOneOf(RFC2616_SPECIAL_METHODS, method)) {
+ return new BasicHttpRequest(method, uri);
+ } else {
+ throw new MethodNotSupportedException(method
+ + " method not supported");
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultHttpResponseFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultHttpResponseFactory.java
new file mode 100644
index 0000000000..21511b9ff5
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultHttpResponseFactory.java
@@ -0,0 +1,109 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl;
+
+import java.util.Locale;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpResponseFactory;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.ReasonPhraseCatalog;
+import ch.boye.httpclientandroidlib.StatusLine;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
+import ch.boye.httpclientandroidlib.message.BasicStatusLine;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Default factory for creating {@link HttpResponse} objects.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class DefaultHttpResponseFactory implements HttpResponseFactory {
+
+ public static final DefaultHttpResponseFactory INSTANCE = new DefaultHttpResponseFactory();
+
+ /** The catalog for looking up reason phrases. */
+ protected final ReasonPhraseCatalog reasonCatalog;
+
+
+ /**
+ * Creates a new response factory with the given catalog.
+ *
+ * @param catalog the catalog of reason phrases
+ */
+ public DefaultHttpResponseFactory(final ReasonPhraseCatalog catalog) {
+ this.reasonCatalog = Args.notNull(catalog, "Reason phrase catalog");
+ }
+
+ /**
+ * Creates a new response factory with the default catalog.
+ * The default catalog is {@link EnglishReasonPhraseCatalog}.
+ */
+ public DefaultHttpResponseFactory() {
+ this(EnglishReasonPhraseCatalog.INSTANCE);
+ }
+
+
+ // non-javadoc, see interface HttpResponseFactory
+ public HttpResponse newHttpResponse(
+ final ProtocolVersion ver,
+ final int status,
+ final HttpContext context) {
+ Args.notNull(ver, "HTTP version");
+ final Locale loc = determineLocale(context);
+ final String reason = this.reasonCatalog.getReason(status, loc);
+ final StatusLine statusline = new BasicStatusLine(ver, status, reason);
+ return new BasicHttpResponse(statusline, this.reasonCatalog, loc);
+ }
+
+
+ // non-javadoc, see interface HttpResponseFactory
+ public HttpResponse newHttpResponse(
+ final StatusLine statusline,
+ final HttpContext context) {
+ Args.notNull(statusline, "Status line");
+ return new BasicHttpResponse(statusline, this.reasonCatalog, determineLocale(context));
+ }
+
+ /**
+ * Determines the locale of the response.
+ * The implementation in this class always returns the default locale.
+ *
+ * @param context the context from which to determine the locale, or
+ * <code>null</code> to use the default locale
+ *
+ * @return the locale for the response, never <code>null</code>
+ */
+ protected Locale determineLocale(final HttpContext context) {
+ return Locale.getDefault();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultHttpServerConnection.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultHttpServerConnection.java
new file mode 100644
index 0000000000..8162e61079
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/DefaultHttpServerConnection.java
@@ -0,0 +1,71 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl;
+
+import java.io.IOException;
+import java.net.Socket;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.params.CoreConnectionPNames;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Default implementation of a server-side HTTP connection.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link DefaultBHttpServerConnection}
+ */
+@NotThreadSafe
+@Deprecated
+public class DefaultHttpServerConnection extends SocketHttpServerConnection {
+
+ public DefaultHttpServerConnection() {
+ super();
+ }
+
+ @Override
+ public void bind(final Socket socket, final HttpParams params) throws IOException {
+ Args.notNull(socket, "Socket");
+ Args.notNull(params, "HTTP parameters");
+ assertNotOpen();
+ socket.setTcpNoDelay(params.getBooleanParameter(CoreConnectionPNames.TCP_NODELAY, true));
+ socket.setSoTimeout(params.getIntParameter(CoreConnectionPNames.SO_TIMEOUT, 0));
+ socket.setKeepAlive(params.getBooleanParameter(CoreConnectionPNames.SO_KEEPALIVE, false));
+ final int linger = params.getIntParameter(CoreConnectionPNames.SO_LINGER, -1);
+ if (linger >= 0) {
+ socket.setSoLinger(linger > 0, linger);
+ }
+ if (linger >= 0) {
+ socket.setSoLinger(linger > 0, linger);
+ }
+ super.bind(socket, params);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/EnglishReasonPhraseCatalog.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/EnglishReasonPhraseCatalog.java
new file mode 100644
index 0000000000..51e435c0af
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/EnglishReasonPhraseCatalog.java
@@ -0,0 +1,224 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl;
+
+import java.util.Locale;
+
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.ReasonPhraseCatalog;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * English reason phrases for HTTP status codes.
+ * All status codes defined in RFC1945 (HTTP/1.0), RFC2616 (HTTP/1.1), and
+ * RFC2518 (WebDAV) are supported.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class EnglishReasonPhraseCatalog implements ReasonPhraseCatalog {
+
+ // static array with english reason phrases defined below
+
+ /**
+ * The default instance of this catalog.
+ * This catalog is thread safe, so there typically
+ * is no need to create other instances.
+ */
+ public final static EnglishReasonPhraseCatalog INSTANCE =
+ new EnglishReasonPhraseCatalog();
+
+
+ /**
+ * Restricted default constructor, for derived classes.
+ * If you need an instance of this class, use {@link #INSTANCE INSTANCE}.
+ */
+ protected EnglishReasonPhraseCatalog() {
+ // no body
+ }
+
+
+ /**
+ * Obtains the reason phrase for a status code.
+ *
+ * @param status the status code, in the range 100-599
+ * @param loc ignored
+ *
+ * @return the reason phrase, or <code>null</code>
+ */
+ public String getReason(final int status, final Locale loc) {
+ Args.check(status >= 100 && status < 600, "Unknown category for status code " + status);
+ final int category = status / 100;
+ final int subcode = status - 100*category;
+
+ String reason = null;
+ if (REASON_PHRASES[category].length > subcode) {
+ reason = REASON_PHRASES[category][subcode];
+ }
+
+ return reason;
+ }
+
+
+ /** Reason phrases lookup table. */
+ private static final String[][] REASON_PHRASES = new String[][]{
+ null,
+ new String[3], // 1xx
+ new String[8], // 2xx
+ new String[8], // 3xx
+ new String[25], // 4xx
+ new String[8] // 5xx
+ };
+
+
+
+ /**
+ * Stores the given reason phrase, by status code.
+ * Helper method to initialize the static lookup table.
+ *
+ * @param status the status code for which to define the phrase
+ * @param reason the reason phrase for this status code
+ */
+ private static void setReason(final int status, final String reason) {
+ final int category = status / 100;
+ final int subcode = status - 100*category;
+ REASON_PHRASES[category][subcode] = reason;
+ }
+
+
+ // ----------------------------------------------------- Static Initializer
+
+ /** Set up status code to "reason phrase" map. */
+ static {
+ // HTTP 1.0 Server status codes -- see RFC 1945
+ setReason(HttpStatus.SC_OK,
+ "OK");
+ setReason(HttpStatus.SC_CREATED,
+ "Created");
+ setReason(HttpStatus.SC_ACCEPTED,
+ "Accepted");
+ setReason(HttpStatus.SC_NO_CONTENT,
+ "No Content");
+ setReason(HttpStatus.SC_MOVED_PERMANENTLY,
+ "Moved Permanently");
+ setReason(HttpStatus.SC_MOVED_TEMPORARILY,
+ "Moved Temporarily");
+ setReason(HttpStatus.SC_NOT_MODIFIED,
+ "Not Modified");
+ setReason(HttpStatus.SC_BAD_REQUEST,
+ "Bad Request");
+ setReason(HttpStatus.SC_UNAUTHORIZED,
+ "Unauthorized");
+ setReason(HttpStatus.SC_FORBIDDEN,
+ "Forbidden");
+ setReason(HttpStatus.SC_NOT_FOUND,
+ "Not Found");
+ setReason(HttpStatus.SC_INTERNAL_SERVER_ERROR,
+ "Internal Server Error");
+ setReason(HttpStatus.SC_NOT_IMPLEMENTED,
+ "Not Implemented");
+ setReason(HttpStatus.SC_BAD_GATEWAY,
+ "Bad Gateway");
+ setReason(HttpStatus.SC_SERVICE_UNAVAILABLE,
+ "Service Unavailable");
+
+ // HTTP 1.1 Server status codes -- see RFC 2048
+ setReason(HttpStatus.SC_CONTINUE,
+ "Continue");
+ setReason(HttpStatus.SC_TEMPORARY_REDIRECT,
+ "Temporary Redirect");
+ setReason(HttpStatus.SC_METHOD_NOT_ALLOWED,
+ "Method Not Allowed");
+ setReason(HttpStatus.SC_CONFLICT,
+ "Conflict");
+ setReason(HttpStatus.SC_PRECONDITION_FAILED,
+ "Precondition Failed");
+ setReason(HttpStatus.SC_REQUEST_TOO_LONG,
+ "Request Too Long");
+ setReason(HttpStatus.SC_REQUEST_URI_TOO_LONG,
+ "Request-URI Too Long");
+ setReason(HttpStatus.SC_UNSUPPORTED_MEDIA_TYPE,
+ "Unsupported Media Type");
+ setReason(HttpStatus.SC_MULTIPLE_CHOICES,
+ "Multiple Choices");
+ setReason(HttpStatus.SC_SEE_OTHER,
+ "See Other");
+ setReason(HttpStatus.SC_USE_PROXY,
+ "Use Proxy");
+ setReason(HttpStatus.SC_PAYMENT_REQUIRED,
+ "Payment Required");
+ setReason(HttpStatus.SC_NOT_ACCEPTABLE,
+ "Not Acceptable");
+ setReason(HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED,
+ "Proxy Authentication Required");
+ setReason(HttpStatus.SC_REQUEST_TIMEOUT,
+ "Request Timeout");
+
+ setReason(HttpStatus.SC_SWITCHING_PROTOCOLS,
+ "Switching Protocols");
+ setReason(HttpStatus.SC_NON_AUTHORITATIVE_INFORMATION,
+ "Non Authoritative Information");
+ setReason(HttpStatus.SC_RESET_CONTENT,
+ "Reset Content");
+ setReason(HttpStatus.SC_PARTIAL_CONTENT,
+ "Partial Content");
+ setReason(HttpStatus.SC_GATEWAY_TIMEOUT,
+ "Gateway Timeout");
+ setReason(HttpStatus.SC_HTTP_VERSION_NOT_SUPPORTED,
+ "Http Version Not Supported");
+ setReason(HttpStatus.SC_GONE,
+ "Gone");
+ setReason(HttpStatus.SC_LENGTH_REQUIRED,
+ "Length Required");
+ setReason(HttpStatus.SC_REQUESTED_RANGE_NOT_SATISFIABLE,
+ "Requested Range Not Satisfiable");
+ setReason(HttpStatus.SC_EXPECTATION_FAILED,
+ "Expectation Failed");
+
+ // WebDAV Server-specific status codes
+ setReason(HttpStatus.SC_PROCESSING,
+ "Processing");
+ setReason(HttpStatus.SC_MULTI_STATUS,
+ "Multi-Status");
+ setReason(HttpStatus.SC_UNPROCESSABLE_ENTITY,
+ "Unprocessable Entity");
+ setReason(HttpStatus.SC_INSUFFICIENT_SPACE_ON_RESOURCE,
+ "Insufficient Space On Resource");
+ setReason(HttpStatus.SC_METHOD_FAILURE,
+ "Method Failure");
+ setReason(HttpStatus.SC_LOCKED,
+ "Locked");
+ setReason(HttpStatus.SC_INSUFFICIENT_STORAGE,
+ "Insufficient Storage");
+ setReason(HttpStatus.SC_FAILED_DEPENDENCY,
+ "Failed Dependency");
+ }
+
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/HttpConnectionMetricsImpl.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/HttpConnectionMetricsImpl.java
new file mode 100644
index 0000000000..bfc4150825
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/HttpConnectionMetricsImpl.java
@@ -0,0 +1,148 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import ch.boye.httpclientandroidlib.HttpConnectionMetrics;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.io.HttpTransportMetrics;
+
+/**
+ * Default implementation of the {@link HttpConnectionMetrics} interface.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class HttpConnectionMetricsImpl implements HttpConnectionMetrics {
+
+ public static final String REQUEST_COUNT = "http.request-count";
+ public static final String RESPONSE_COUNT = "http.response-count";
+ public static final String SENT_BYTES_COUNT = "http.sent-bytes-count";
+ public static final String RECEIVED_BYTES_COUNT = "http.received-bytes-count";
+
+ private final HttpTransportMetrics inTransportMetric;
+ private final HttpTransportMetrics outTransportMetric;
+ private long requestCount = 0;
+ private long responseCount = 0;
+
+ /**
+ * The cache map for all metrics values.
+ */
+ private Map<String, Object> metricsCache;
+
+ public HttpConnectionMetricsImpl(
+ final HttpTransportMetrics inTransportMetric,
+ final HttpTransportMetrics outTransportMetric) {
+ super();
+ this.inTransportMetric = inTransportMetric;
+ this.outTransportMetric = outTransportMetric;
+ }
+
+ /* ------------------ Public interface method -------------------------- */
+
+ public long getReceivedBytesCount() {
+ if (this.inTransportMetric != null) {
+ return this.inTransportMetric.getBytesTransferred();
+ } else {
+ return -1;
+ }
+ }
+
+ public long getSentBytesCount() {
+ if (this.outTransportMetric != null) {
+ return this.outTransportMetric.getBytesTransferred();
+ } else {
+ return -1;
+ }
+ }
+
+ public long getRequestCount() {
+ return this.requestCount;
+ }
+
+ public void incrementRequestCount() {
+ this.requestCount++;
+ }
+
+ public long getResponseCount() {
+ return this.responseCount;
+ }
+
+ public void incrementResponseCount() {
+ this.responseCount++;
+ }
+
+ public Object getMetric(final String metricName) {
+ Object value = null;
+ if (this.metricsCache != null) {
+ value = this.metricsCache.get(metricName);
+ }
+ if (value == null) {
+ if (REQUEST_COUNT.equals(metricName)) {
+ value = Long.valueOf(requestCount);
+ } else if (RESPONSE_COUNT.equals(metricName)) {
+ value = Long.valueOf(responseCount);
+ } else if (RECEIVED_BYTES_COUNT.equals(metricName)) {
+ if (this.inTransportMetric != null) {
+ return Long.valueOf(this.inTransportMetric.getBytesTransferred());
+ } else {
+ return null;
+ }
+ } else if (SENT_BYTES_COUNT.equals(metricName)) {
+ if (this.outTransportMetric != null) {
+ return Long.valueOf(this.outTransportMetric.getBytesTransferred());
+ } else {
+ return null;
+ }
+ }
+ }
+ return value;
+ }
+
+ public void setMetric(final String metricName, final Object obj) {
+ if (this.metricsCache == null) {
+ this.metricsCache = new HashMap<String, Object>();
+ }
+ this.metricsCache.put(metricName, obj);
+ }
+
+ public void reset() {
+ if (this.outTransportMetric != null) {
+ this.outTransportMetric.reset();
+ }
+ if (this.inTransportMetric != null) {
+ this.inTransportMetric.reset();
+ }
+ this.requestCount = 0;
+ this.responseCount = 0;
+ this.metricsCache = null;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/NoConnectionReuseStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/NoConnectionReuseStrategy.java
new file mode 100644
index 0000000000..fdf3128447
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/NoConnectionReuseStrategy.java
@@ -0,0 +1,53 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl;
+
+import ch.boye.httpclientandroidlib.ConnectionReuseStrategy;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * A strategy that never re-uses a connection.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class NoConnectionReuseStrategy implements ConnectionReuseStrategy {
+
+ public static final NoConnectionReuseStrategy INSTANCE = new NoConnectionReuseStrategy();
+
+ public NoConnectionReuseStrategy() {
+ super();
+ }
+
+ public boolean keepAlive(final HttpResponse response, final HttpContext context) {
+ return false;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/SocketHttpClientConnection.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/SocketHttpClientConnection.java
new file mode 100644
index 0000000000..72f2f7e877
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/SocketHttpClientConnection.java
@@ -0,0 +1,283 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.SocketException;
+
+import ch.boye.httpclientandroidlib.HttpInetConnection;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.impl.io.SocketInputBuffer;
+import ch.boye.httpclientandroidlib.impl.io.SocketOutputBuffer;
+import ch.boye.httpclientandroidlib.io.SessionInputBuffer;
+import ch.boye.httpclientandroidlib.io.SessionOutputBuffer;
+import ch.boye.httpclientandroidlib.params.CoreConnectionPNames;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+
+/**
+ * Implementation of a client-side HTTP connection that can be bound to an
+ * arbitrary {@link Socket} for receiving data from and transmitting data to
+ * a remote server.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link DefaultBHttpClientConnection}
+ */
+@NotThreadSafe
+@Deprecated
+public class SocketHttpClientConnection
+ extends AbstractHttpClientConnection implements HttpInetConnection {
+
+ private volatile boolean open;
+ private volatile Socket socket = null;
+
+ public SocketHttpClientConnection() {
+ super();
+ }
+
+ protected void assertNotOpen() {
+ Asserts.check(!this.open, "Connection is already open");
+ }
+
+ @Override
+ protected void assertOpen() {
+ Asserts.check(this.open, "Connection is not open");
+ }
+
+ /**
+ * Creates an instance of {@link SocketInputBuffer} to be used for
+ * receiving data from the given {@link Socket}.
+ * <p>
+ * This method can be overridden in a super class in order to provide
+ * a custom implementation of {@link SessionInputBuffer} interface.
+ *
+ * @see SocketInputBuffer#SocketInputBuffer(Socket, int, HttpParams)
+ *
+ * @param socket the socket.
+ * @param buffersize the buffer size.
+ * @param params HTTP parameters.
+ * @return session input buffer.
+ * @throws IOException in case of an I/O error.
+ */
+ protected SessionInputBuffer createSessionInputBuffer(
+ final Socket socket,
+ final int buffersize,
+ final HttpParams params) throws IOException {
+ return new SocketInputBuffer(socket, buffersize, params);
+ }
+
+ /**
+ * Creates an instance of {@link SessionOutputBuffer} to be used for
+ * sending data to the given {@link Socket}.
+ * <p>
+ * This method can be overridden in a super class in order to provide
+ * a custom implementation of {@link SocketOutputBuffer} interface.
+ *
+ * @see SocketOutputBuffer#SocketOutputBuffer(Socket, int, HttpParams)
+ *
+ * @param socket the socket.
+ * @param buffersize the buffer size.
+ * @param params HTTP parameters.
+ * @return session output buffer.
+ * @throws IOException in case of an I/O error.
+ */
+ protected SessionOutputBuffer createSessionOutputBuffer(
+ final Socket socket,
+ final int buffersize,
+ final HttpParams params) throws IOException {
+ return new SocketOutputBuffer(socket, buffersize, params);
+ }
+
+ /**
+ * Binds this connection to the given {@link Socket}. This socket will be
+ * used by the connection to send and receive data.
+ * <p>
+ * This method will invoke {@link #createSessionInputBuffer(Socket, int, HttpParams)}
+ * and {@link #createSessionOutputBuffer(Socket, int, HttpParams)} methods
+ * to create session input / output buffers bound to this socket and then
+ * will invoke {@link #init(SessionInputBuffer, SessionOutputBuffer, HttpParams)}
+ * method to pass references to those buffers to the underlying HTTP message
+ * parser and formatter.
+ * <p>
+ * After this method's execution the connection status will be reported
+ * as open and the {@link #isOpen()} will return <code>true</code>.
+ *
+ * @param socket the socket.
+ * @param params HTTP parameters.
+ * @throws IOException in case of an I/O error.
+ */
+ protected void bind(
+ final Socket socket,
+ final HttpParams params) throws IOException {
+ Args.notNull(socket, "Socket");
+ Args.notNull(params, "HTTP parameters");
+ this.socket = socket;
+
+ final int buffersize = params.getIntParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE, -1);
+ init(
+ createSessionInputBuffer(socket, buffersize, params),
+ createSessionOutputBuffer(socket, buffersize, params),
+ params);
+
+ this.open = true;
+ }
+
+ public boolean isOpen() {
+ return this.open;
+ }
+
+ protected Socket getSocket() {
+ return this.socket;
+ }
+
+ public InetAddress getLocalAddress() {
+ if (this.socket != null) {
+ return this.socket.getLocalAddress();
+ } else {
+ return null;
+ }
+ }
+
+ public int getLocalPort() {
+ if (this.socket != null) {
+ return this.socket.getLocalPort();
+ } else {
+ return -1;
+ }
+ }
+
+ public InetAddress getRemoteAddress() {
+ if (this.socket != null) {
+ return this.socket.getInetAddress();
+ } else {
+ return null;
+ }
+ }
+
+ public int getRemotePort() {
+ if (this.socket != null) {
+ return this.socket.getPort();
+ } else {
+ return -1;
+ }
+ }
+
+ public void setSocketTimeout(final int timeout) {
+ assertOpen();
+ if (this.socket != null) {
+ try {
+ this.socket.setSoTimeout(timeout);
+ } catch (final SocketException ignore) {
+ // It is not quite clear from the Sun's documentation if there are any
+ // other legitimate cases for a socket exception to be thrown when setting
+ // SO_TIMEOUT besides the socket being already closed
+ }
+ }
+ }
+
+ public int getSocketTimeout() {
+ if (this.socket != null) {
+ try {
+ return this.socket.getSoTimeout();
+ } catch (final SocketException ignore) {
+ return -1;
+ }
+ } else {
+ return -1;
+ }
+ }
+
+ public void shutdown() throws IOException {
+ this.open = false;
+ final Socket tmpsocket = this.socket;
+ if (tmpsocket != null) {
+ tmpsocket.close();
+ }
+ }
+
+ public void close() throws IOException {
+ if (!this.open) {
+ return;
+ }
+ this.open = false;
+ final Socket sock = this.socket;
+ try {
+ doFlush();
+ try {
+ try {
+ sock.shutdownOutput();
+ } catch (final IOException ignore) {
+ }
+ try {
+ sock.shutdownInput();
+ } catch (final IOException ignore) {
+ }
+ } catch (final UnsupportedOperationException ignore) {
+ // if one isn't supported, the other one isn't either
+ }
+ } finally {
+ sock.close();
+ }
+ }
+
+ private static void formatAddress(final StringBuilder buffer, final SocketAddress socketAddress) {
+ if (socketAddress instanceof InetSocketAddress) {
+ final InetSocketAddress addr = ((InetSocketAddress) socketAddress);
+ buffer.append(addr.getAddress() != null ? addr.getAddress().getHostAddress() :
+ addr.getAddress())
+ .append(':')
+ .append(addr.getPort());
+ } else {
+ buffer.append(socketAddress);
+ }
+ }
+
+ @Override
+ public String toString() {
+ if (this.socket != null) {
+ final StringBuilder buffer = new StringBuilder();
+ final SocketAddress remoteAddress = this.socket.getRemoteSocketAddress();
+ final SocketAddress localAddress = this.socket.getLocalSocketAddress();
+ if (remoteAddress != null && localAddress != null) {
+ formatAddress(buffer, localAddress);
+ buffer.append("<->");
+ formatAddress(buffer, remoteAddress);
+ }
+ return buffer.toString();
+ } else {
+ return super.toString();
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/SocketHttpServerConnection.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/SocketHttpServerConnection.java
new file mode 100644
index 0000000000..0a74b94a3c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/SocketHttpServerConnection.java
@@ -0,0 +1,271 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.SocketException;
+
+import ch.boye.httpclientandroidlib.HttpInetConnection;
+import ch.boye.httpclientandroidlib.impl.io.SocketInputBuffer;
+import ch.boye.httpclientandroidlib.impl.io.SocketOutputBuffer;
+import ch.boye.httpclientandroidlib.io.SessionInputBuffer;
+import ch.boye.httpclientandroidlib.io.SessionOutputBuffer;
+import ch.boye.httpclientandroidlib.params.CoreConnectionPNames;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+
+@Deprecated
+public class SocketHttpServerConnection extends
+ AbstractHttpServerConnection implements HttpInetConnection {
+
+ private volatile boolean open;
+ private volatile Socket socket = null;
+
+ public SocketHttpServerConnection() {
+ super();
+ }
+
+ protected void assertNotOpen() {
+ Asserts.check(!this.open, "Connection is already open");
+ }
+
+ @Override
+ protected void assertOpen() {
+ Asserts.check(this.open, "Connection is not open");
+ }
+
+ /**
+ * Creates an instance of {@link SocketInputBuffer} to be used for
+ * receiving data from the given {@link Socket}.
+ * <p>
+ * This method can be overridden in a super class in order to provide
+ * a custom implementation of {@link SessionInputBuffer} interface.
+ *
+ * @see SocketInputBuffer#SocketInputBuffer(Socket, int, HttpParams)
+ *
+ * @param socket the socket.
+ * @param buffersize the buffer size.
+ * @param params HTTP parameters.
+ * @return session input buffer.
+ * @throws IOException in case of an I/O error.
+ */
+ protected SessionInputBuffer createSessionInputBuffer(
+ final Socket socket,
+ final int buffersize,
+ final HttpParams params) throws IOException {
+ return new SocketInputBuffer(socket, buffersize, params);
+ }
+
+ /**
+ * Creates an instance of {@link SessionOutputBuffer} to be used for
+ * sending data to the given {@link Socket}.
+ * <p>
+ * This method can be overridden in a super class in order to provide
+ * a custom implementation of {@link SocketOutputBuffer} interface.
+ *
+ * @see SocketOutputBuffer#SocketOutputBuffer(Socket, int, HttpParams)
+ *
+ * @param socket the socket.
+ * @param buffersize the buffer size.
+ * @param params HTTP parameters.
+ * @return session output buffer.
+ * @throws IOException in case of an I/O error.
+ */
+ protected SessionOutputBuffer createSessionOutputBuffer(
+ final Socket socket,
+ final int buffersize,
+ final HttpParams params) throws IOException {
+ return new SocketOutputBuffer(socket, buffersize, params);
+ }
+
+ /**
+ * Binds this connection to the given {@link Socket}. This socket will be
+ * used by the connection to send and receive data.
+ * <p>
+ * This method will invoke {@link #createSessionInputBuffer(Socket, int, HttpParams)}
+ * and {@link #createSessionOutputBuffer(Socket, int, HttpParams)} methods
+ * to create session input / output buffers bound to this socket and then
+ * will invoke {@link #init(SessionInputBuffer, SessionOutputBuffer, HttpParams)}
+ * method to pass references to those buffers to the underlying HTTP message
+ * parser and formatter.
+ * <p>
+ * After this method's execution the connection status will be reported
+ * as open and the {@link #isOpen()} will return <code>true</code>.
+ *
+ * @param socket the socket.
+ * @param params HTTP parameters.
+ * @throws IOException in case of an I/O error.
+ */
+ protected void bind(final Socket socket, final HttpParams params) throws IOException {
+ Args.notNull(socket, "Socket");
+ Args.notNull(params, "HTTP parameters");
+ this.socket = socket;
+
+ final int buffersize = params.getIntParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE, -1);
+ init(
+ createSessionInputBuffer(socket, buffersize, params),
+ createSessionOutputBuffer(socket, buffersize, params),
+ params);
+
+ this.open = true;
+ }
+
+ protected Socket getSocket() {
+ return this.socket;
+ }
+
+ public boolean isOpen() {
+ return this.open;
+ }
+
+ public InetAddress getLocalAddress() {
+ if (this.socket != null) {
+ return this.socket.getLocalAddress();
+ } else {
+ return null;
+ }
+ }
+
+ public int getLocalPort() {
+ if (this.socket != null) {
+ return this.socket.getLocalPort();
+ } else {
+ return -1;
+ }
+ }
+
+ public InetAddress getRemoteAddress() {
+ if (this.socket != null) {
+ return this.socket.getInetAddress();
+ } else {
+ return null;
+ }
+ }
+
+ public int getRemotePort() {
+ if (this.socket != null) {
+ return this.socket.getPort();
+ } else {
+ return -1;
+ }
+ }
+
+ public void setSocketTimeout(final int timeout) {
+ assertOpen();
+ if (this.socket != null) {
+ try {
+ this.socket.setSoTimeout(timeout);
+ } catch (final SocketException ignore) {
+ // It is not quite clear from the Sun's documentation if there are any
+ // other legitimate cases for a socket exception to be thrown when setting
+ // SO_TIMEOUT besides the socket being already closed
+ }
+ }
+ }
+
+ public int getSocketTimeout() {
+ if (this.socket != null) {
+ try {
+ return this.socket.getSoTimeout();
+ } catch (final SocketException ignore) {
+ return -1;
+ }
+ } else {
+ return -1;
+ }
+ }
+
+ public void shutdown() throws IOException {
+ this.open = false;
+ final Socket tmpsocket = this.socket;
+ if (tmpsocket != null) {
+ tmpsocket.close();
+ }
+ }
+
+ public void close() throws IOException {
+ if (!this.open) {
+ return;
+ }
+ this.open = false;
+ this.open = false;
+ final Socket sock = this.socket;
+ try {
+ doFlush();
+ try {
+ try {
+ sock.shutdownOutput();
+ } catch (final IOException ignore) {
+ }
+ try {
+ sock.shutdownInput();
+ } catch (final IOException ignore) {
+ }
+ } catch (final UnsupportedOperationException ignore) {
+ // if one isn't supported, the other one isn't either
+ }
+ } finally {
+ sock.close();
+ }
+ }
+
+ private static void formatAddress(final StringBuilder buffer, final SocketAddress socketAddress) {
+ if (socketAddress instanceof InetSocketAddress) {
+ final InetSocketAddress addr = ((InetSocketAddress) socketAddress);
+ buffer.append(addr.getAddress() != null ? addr.getAddress().getHostAddress() :
+ addr.getAddress())
+ .append(':')
+ .append(addr.getPort());
+ } else {
+ buffer.append(socketAddress);
+ }
+ }
+
+ @Override
+ public String toString() {
+ if (this.socket != null) {
+ final StringBuilder buffer = new StringBuilder();
+ final SocketAddress remoteAddress = this.socket.getRemoteSocketAddress();
+ final SocketAddress localAddress = this.socket.getLocalSocketAddress();
+ if (remoteAddress != null && localAddress != null) {
+ formatAddress(buffer, localAddress);
+ buffer.append("<->");
+ formatAddress(buffer, remoteAddress);
+ }
+ return buffer.toString();
+ } else {
+ return super.toString();
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/AuthSchemeBase.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/AuthSchemeBase.java
new file mode 100644
index 0000000000..e1b98db9e5
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/AuthSchemeBase.java
@@ -0,0 +1,169 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.auth;
+
+import java.util.Locale;
+
+import ch.boye.httpclientandroidlib.FormattedHeader;
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.auth.AUTH;
+import ch.boye.httpclientandroidlib.auth.AuthenticationException;
+import ch.boye.httpclientandroidlib.auth.ChallengeState;
+import ch.boye.httpclientandroidlib.auth.ContextAwareAuthScheme;
+import ch.boye.httpclientandroidlib.auth.Credentials;
+import ch.boye.httpclientandroidlib.auth.MalformedChallengeException;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Abstract authentication scheme class that serves as a basis
+ * for all authentication schemes supported by HttpClient. This class
+ * defines the generic way of parsing an authentication challenge. It
+ * does not make any assumptions regarding the format of the challenge
+ * nor does it impose any specific way of responding to that challenge.
+ *
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public abstract class AuthSchemeBase implements ContextAwareAuthScheme {
+
+ private ChallengeState challengeState;
+
+ /**
+ * Creates an instance of <tt>AuthSchemeBase</tt> with the given challenge
+ * state.
+ *
+ * @since 4.2
+ *
+ * @deprecated (4.3) do not use.
+ */
+ @Deprecated
+ public AuthSchemeBase(final ChallengeState challengeState) {
+ super();
+ this.challengeState = challengeState;
+ }
+
+ public AuthSchemeBase() {
+ super();
+ }
+
+ /**
+ * Processes the given challenge token. Some authentication schemes
+ * may involve multiple challenge-response exchanges. Such schemes must be able
+ * to maintain the state information when dealing with sequential challenges
+ *
+ * @param header the challenge header
+ *
+ * @throws MalformedChallengeException is thrown if the authentication challenge
+ * is malformed
+ */
+ public void processChallenge(final Header header) throws MalformedChallengeException {
+ Args.notNull(header, "Header");
+ final String authheader = header.getName();
+ if (authheader.equalsIgnoreCase(AUTH.WWW_AUTH)) {
+ this.challengeState = ChallengeState.TARGET;
+ } else if (authheader.equalsIgnoreCase(AUTH.PROXY_AUTH)) {
+ this.challengeState = ChallengeState.PROXY;
+ } else {
+ throw new MalformedChallengeException("Unexpected header name: " + authheader);
+ }
+
+ final CharArrayBuffer buffer;
+ int pos;
+ if (header instanceof FormattedHeader) {
+ buffer = ((FormattedHeader) header).getBuffer();
+ pos = ((FormattedHeader) header).getValuePos();
+ } else {
+ final String s = header.getValue();
+ if (s == null) {
+ throw new MalformedChallengeException("Header value is null");
+ }
+ buffer = new CharArrayBuffer(s.length());
+ buffer.append(s);
+ pos = 0;
+ }
+ while (pos < buffer.length() && HTTP.isWhitespace(buffer.charAt(pos))) {
+ pos++;
+ }
+ final int beginIndex = pos;
+ while (pos < buffer.length() && !HTTP.isWhitespace(buffer.charAt(pos))) {
+ pos++;
+ }
+ final int endIndex = pos;
+ final String s = buffer.substring(beginIndex, endIndex);
+ if (!s.equalsIgnoreCase(getSchemeName())) {
+ throw new MalformedChallengeException("Invalid scheme identifier: " + s);
+ }
+
+ parseChallenge(buffer, pos, buffer.length());
+ }
+
+
+ @SuppressWarnings("deprecation")
+ public Header authenticate(
+ final Credentials credentials,
+ final HttpRequest request,
+ final HttpContext context) throws AuthenticationException {
+ return authenticate(credentials, request);
+ }
+
+ protected abstract void parseChallenge(
+ CharArrayBuffer buffer, int beginIndex, int endIndex) throws MalformedChallengeException;
+
+ /**
+ * Returns <code>true</code> if authenticating against a proxy, <code>false</code>
+ * otherwise.
+ */
+ public boolean isProxy() {
+ return this.challengeState != null && this.challengeState == ChallengeState.PROXY;
+ }
+
+ /**
+ * Returns {@link ChallengeState} value or <code>null</code> if unchallenged.
+ *
+ * @since 4.2
+ */
+ public ChallengeState getChallengeState() {
+ return this.challengeState;
+ }
+
+ @Override
+ public String toString() {
+ final String name = getSchemeName();
+ if (name != null) {
+ return name.toUpperCase(Locale.ENGLISH);
+ } else {
+ return super.toString();
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/BasicScheme.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/BasicScheme.java
new file mode 100644
index 0000000000..c58b5e5827
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/BasicScheme.java
@@ -0,0 +1,219 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.auth;
+
+import java.nio.charset.Charset;
+
+import org.mozilla.apache.commons.codec.binary.Base64;
+import ch.boye.httpclientandroidlib.Consts;
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.auth.AUTH;
+import ch.boye.httpclientandroidlib.auth.AuthenticationException;
+import ch.boye.httpclientandroidlib.auth.ChallengeState;
+import ch.boye.httpclientandroidlib.auth.Credentials;
+import ch.boye.httpclientandroidlib.auth.MalformedChallengeException;
+import ch.boye.httpclientandroidlib.message.BufferedHeader;
+import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+import ch.boye.httpclientandroidlib.util.EncodingUtils;
+
+/**
+ * Basic authentication scheme as defined in RFC 2617.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class BasicScheme extends RFC2617Scheme {
+
+/* Base64 instance removed by HttpClient for Android script. */
+ /** Whether the basic authentication process is complete */
+ private boolean complete;
+
+ /**
+ * @since 4.3
+ */
+ public BasicScheme(final Charset credentialsCharset) {
+ super(credentialsCharset);
+/* Base64 instance removed by HttpClient for Android script. */
+ this.complete = false;
+ }
+
+ /**
+ * Creates an instance of <tt>BasicScheme</tt> with the given challenge
+ * state.
+ *
+ * @since 4.2
+ *
+ * @deprecated (4.3) do not use.
+ */
+ @Deprecated
+ public BasicScheme(final ChallengeState challengeState) {
+ super(challengeState);
+/* Base64 instance removed by HttpClient for Android script. */
+ }
+
+ public BasicScheme() {
+ this(Consts.ASCII);
+ }
+
+ /**
+ * Returns textual designation of the basic authentication scheme.
+ *
+ * @return <code>basic</code>
+ */
+ public String getSchemeName() {
+ return "basic";
+ }
+
+ /**
+ * Processes the Basic challenge.
+ *
+ * @param header the challenge header
+ *
+ * @throws MalformedChallengeException is thrown if the authentication challenge
+ * is malformed
+ */
+ @Override
+ public void processChallenge(
+ final Header header) throws MalformedChallengeException {
+ super.processChallenge(header);
+ this.complete = true;
+ }
+
+ /**
+ * Tests if the Basic authentication process has been completed.
+ *
+ * @return <tt>true</tt> if Basic authorization has been processed,
+ * <tt>false</tt> otherwise.
+ */
+ public boolean isComplete() {
+ return this.complete;
+ }
+
+ /**
+ * Returns <tt>false</tt>. Basic authentication scheme is request based.
+ *
+ * @return <tt>false</tt>.
+ */
+ public boolean isConnectionBased() {
+ return false;
+ }
+
+ /**
+ * @deprecated (4.2) Use {@link ch.boye.httpclientandroidlib.auth.ContextAwareAuthScheme#authenticate(
+ * Credentials, HttpRequest, ch.boye.httpclientandroidlib.protocol.HttpContext)}
+ */
+ @Deprecated
+ public Header authenticate(
+ final Credentials credentials, final HttpRequest request) throws AuthenticationException {
+ return authenticate(credentials, request, new BasicHttpContext());
+ }
+
+ /**
+ * Produces basic authorization header for the given set of {@link Credentials}.
+ *
+ * @param credentials The set of credentials to be used for authentication
+ * @param request The request being authenticated
+ * @throws ch.boye.httpclientandroidlib.auth.InvalidCredentialsException if authentication
+ * credentials are not valid or not applicable for this authentication scheme
+ * @throws AuthenticationException if authorization string cannot
+ * be generated due to an authentication failure
+ *
+ * @return a basic authorization string
+ */
+ @Override
+ public Header authenticate(
+ final Credentials credentials,
+ final HttpRequest request,
+ final HttpContext context) throws AuthenticationException {
+
+ Args.notNull(credentials, "Credentials");
+ Args.notNull(request, "HTTP request");
+ final StringBuilder tmp = new StringBuilder();
+ tmp.append(credentials.getUserPrincipal().getName());
+ tmp.append(":");
+ tmp.append((credentials.getPassword() == null) ? "null" : credentials.getPassword());
+
+ final byte[] base64password = Base64.encodeBase64(
+ EncodingUtils.getBytes(tmp.toString(), getCredentialsCharset(request)));
+
+ final CharArrayBuffer buffer = new CharArrayBuffer(32);
+ if (isProxy()) {
+ buffer.append(AUTH.PROXY_AUTH_RESP);
+ } else {
+ buffer.append(AUTH.WWW_AUTH_RESP);
+ }
+ buffer.append(": Basic ");
+ buffer.append(base64password, 0, base64password.length);
+
+ return new BufferedHeader(buffer);
+ }
+
+ /**
+ * Returns a basic <tt>Authorization</tt> header value for the given
+ * {@link Credentials} and charset.
+ *
+ * @param credentials The credentials to encode.
+ * @param charset The charset to use for encoding the credentials
+ *
+ * @return a basic authorization header
+ *
+ * @deprecated (4.3) use {@link #authenticate(Credentials, HttpRequest, HttpContext)}.
+ */
+ @Deprecated
+ public static Header authenticate(
+ final Credentials credentials,
+ final String charset,
+ final boolean proxy) {
+ Args.notNull(credentials, "Credentials");
+ Args.notNull(charset, "charset");
+
+ final StringBuilder tmp = new StringBuilder();
+ tmp.append(credentials.getUserPrincipal().getName());
+ tmp.append(":");
+ tmp.append((credentials.getPassword() == null) ? "null" : credentials.getPassword());
+
+ final byte[] base64password = Base64.encodeBase64(
+ EncodingUtils.getBytes(tmp.toString(), charset));
+
+ final CharArrayBuffer buffer = new CharArrayBuffer(32);
+ if (proxy) {
+ buffer.append(AUTH.PROXY_AUTH_RESP);
+ } else {
+ buffer.append(AUTH.WWW_AUTH_RESP);
+ }
+ buffer.append(": Basic ");
+ buffer.append(base64password, 0, base64password.length);
+
+ return new BufferedHeader(buffer);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/BasicSchemeFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/BasicSchemeFactory.java
new file mode 100644
index 0000000000..c4eb8c6bea
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/BasicSchemeFactory.java
@@ -0,0 +1,71 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.auth;
+
+import java.nio.charset.Charset;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.auth.AuthScheme;
+import ch.boye.httpclientandroidlib.auth.AuthSchemeFactory;
+import ch.boye.httpclientandroidlib.auth.AuthSchemeProvider;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * {@link AuthSchemeProvider} implementation that creates and initializes
+ * {@link BasicScheme} instances.
+ *
+ * @since 4.0
+ */
+@Immutable
+@SuppressWarnings("deprecation")
+public class BasicSchemeFactory implements AuthSchemeFactory, AuthSchemeProvider {
+
+ private final Charset charset;
+
+ /**
+ * @since 4.3
+ */
+ public BasicSchemeFactory(final Charset charset) {
+ super();
+ this.charset = charset;
+ }
+
+ public BasicSchemeFactory() {
+ this(null);
+ }
+
+ public AuthScheme newInstance(final HttpParams params) {
+ return new BasicScheme();
+ }
+
+ public AuthScheme create(final HttpContext context) {
+ return new BasicScheme(this.charset);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/DigestScheme.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/DigestScheme.java
new file mode 100644
index 0000000000..542f6236b3
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/DigestScheme.java
@@ -0,0 +1,489 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.auth;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Formatter;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+import ch.boye.httpclientandroidlib.Consts;
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.auth.AUTH;
+import ch.boye.httpclientandroidlib.auth.AuthenticationException;
+import ch.boye.httpclientandroidlib.auth.ChallengeState;
+import ch.boye.httpclientandroidlib.auth.Credentials;
+import ch.boye.httpclientandroidlib.auth.MalformedChallengeException;
+import ch.boye.httpclientandroidlib.message.BasicHeaderValueFormatter;
+import ch.boye.httpclientandroidlib.message.BasicNameValuePair;
+import ch.boye.httpclientandroidlib.message.BufferedHeader;
+import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+import ch.boye.httpclientandroidlib.util.EncodingUtils;
+
+/**
+ * Digest authentication scheme as defined in RFC 2617.
+ * Both MD5 (default) and MD5-sess are supported.
+ * Currently only qop=auth or no qop is supported. qop=auth-int
+ * is unsupported. If auth and auth-int are provided, auth is
+ * used.
+ * <p/>
+ * Since the digest username is included as clear text in the generated
+ * Authentication header, the charset of the username must be compatible
+ * with the HTTP element charset used by the connection.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class DigestScheme extends RFC2617Scheme {
+
+ /**
+ * Hexa values used when creating 32 character long digest in HTTP DigestScheme
+ * in case of authentication.
+ *
+ * @see #encode(byte[])
+ */
+ private static final char[] HEXADECIMAL = {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
+ 'e', 'f'
+ };
+
+ /** Whether the digest authentication process is complete */
+ private boolean complete;
+
+ private static final int QOP_UNKNOWN = -1;
+ private static final int QOP_MISSING = 0;
+ private static final int QOP_AUTH_INT = 1;
+ private static final int QOP_AUTH = 2;
+
+ private String lastNonce;
+ private long nounceCount;
+ private String cnonce;
+ private String a1;
+ private String a2;
+
+ /**
+ * @since 4.3
+ */
+ public DigestScheme(final Charset credentialsCharset) {
+ super(credentialsCharset);
+ this.complete = false;
+ }
+
+ /**
+ * Creates an instance of <tt>DigestScheme</tt> with the given challenge
+ * state.
+ *
+ * @since 4.2
+ *
+ * @deprecated (4.3) do not use.
+ */
+ @Deprecated
+ public DigestScheme(final ChallengeState challengeState) {
+ super(challengeState);
+ }
+
+ public DigestScheme() {
+ this(Consts.ASCII);
+ }
+
+ /**
+ * Processes the Digest challenge.
+ *
+ * @param header the challenge header
+ *
+ * @throws MalformedChallengeException is thrown if the authentication challenge
+ * is malformed
+ */
+ @Override
+ public void processChallenge(
+ final Header header) throws MalformedChallengeException {
+ super.processChallenge(header);
+ this.complete = true;
+ }
+
+ /**
+ * Tests if the Digest authentication process has been completed.
+ *
+ * @return <tt>true</tt> if Digest authorization has been processed,
+ * <tt>false</tt> otherwise.
+ */
+ public boolean isComplete() {
+ final String s = getParameter("stale");
+ if ("true".equalsIgnoreCase(s)) {
+ return false;
+ } else {
+ return this.complete;
+ }
+ }
+
+ /**
+ * Returns textual designation of the digest authentication scheme.
+ *
+ * @return <code>digest</code>
+ */
+ public String getSchemeName() {
+ return "digest";
+ }
+
+ /**
+ * Returns <tt>false</tt>. Digest authentication scheme is request based.
+ *
+ * @return <tt>false</tt>.
+ */
+ public boolean isConnectionBased() {
+ return false;
+ }
+
+ public void overrideParamter(final String name, final String value) {
+ getParameters().put(name, value);
+ }
+
+ /**
+ * @deprecated (4.2) Use {@link ch.boye.httpclientandroidlib.auth.ContextAwareAuthScheme#authenticate(
+ * Credentials, HttpRequest, ch.boye.httpclientandroidlib.protocol.HttpContext)}
+ */
+ @Deprecated
+ public Header authenticate(
+ final Credentials credentials, final HttpRequest request) throws AuthenticationException {
+ return authenticate(credentials, request, new BasicHttpContext());
+ }
+
+ /**
+ * Produces a digest authorization string for the given set of
+ * {@link Credentials}, method name and URI.
+ *
+ * @param credentials A set of credentials to be used for athentication
+ * @param request The request being authenticated
+ *
+ * @throws ch.boye.httpclientandroidlib.auth.InvalidCredentialsException if authentication credentials
+ * are not valid or not applicable for this authentication scheme
+ * @throws AuthenticationException if authorization string cannot
+ * be generated due to an authentication failure
+ *
+ * @return a digest authorization string
+ */
+ @Override
+ public Header authenticate(
+ final Credentials credentials,
+ final HttpRequest request,
+ final HttpContext context) throws AuthenticationException {
+
+ Args.notNull(credentials, "Credentials");
+ Args.notNull(request, "HTTP request");
+ if (getParameter("realm") == null) {
+ throw new AuthenticationException("missing realm in challenge");
+ }
+ if (getParameter("nonce") == null) {
+ throw new AuthenticationException("missing nonce in challenge");
+ }
+ // Add method name and request-URI to the parameter map
+ getParameters().put("methodname", request.getRequestLine().getMethod());
+ getParameters().put("uri", request.getRequestLine().getUri());
+ final String charset = getParameter("charset");
+ if (charset == null) {
+ getParameters().put("charset", getCredentialsCharset(request));
+ }
+ return createDigestHeader(credentials, request);
+ }
+
+ private static MessageDigest createMessageDigest(
+ final String digAlg) throws UnsupportedDigestAlgorithmException {
+ try {
+ return MessageDigest.getInstance(digAlg);
+ } catch (final Exception e) {
+ throw new UnsupportedDigestAlgorithmException(
+ "Unsupported algorithm in HTTP Digest authentication: "
+ + digAlg);
+ }
+ }
+
+ /**
+ * Creates digest-response header as defined in RFC2617.
+ *
+ * @param credentials User credentials
+ *
+ * @return The digest-response as String.
+ */
+ private Header createDigestHeader(
+ final Credentials credentials,
+ final HttpRequest request) throws AuthenticationException {
+ final String uri = getParameter("uri");
+ final String realm = getParameter("realm");
+ final String nonce = getParameter("nonce");
+ final String opaque = getParameter("opaque");
+ final String method = getParameter("methodname");
+ String algorithm = getParameter("algorithm");
+ // If an algorithm is not specified, default to MD5.
+ if (algorithm == null) {
+ algorithm = "MD5";
+ }
+
+ final Set<String> qopset = new HashSet<String>(8);
+ int qop = QOP_UNKNOWN;
+ final String qoplist = getParameter("qop");
+ if (qoplist != null) {
+ final StringTokenizer tok = new StringTokenizer(qoplist, ",");
+ while (tok.hasMoreTokens()) {
+ final String variant = tok.nextToken().trim();
+ qopset.add(variant.toLowerCase(Locale.ENGLISH));
+ }
+ if (request instanceof HttpEntityEnclosingRequest && qopset.contains("auth-int")) {
+ qop = QOP_AUTH_INT;
+ } else if (qopset.contains("auth")) {
+ qop = QOP_AUTH;
+ }
+ } else {
+ qop = QOP_MISSING;
+ }
+
+ if (qop == QOP_UNKNOWN) {
+ throw new AuthenticationException("None of the qop methods is supported: " + qoplist);
+ }
+
+ String charset = getParameter("charset");
+ if (charset == null) {
+ charset = "ISO-8859-1";
+ }
+
+ String digAlg = algorithm;
+ if (digAlg.equalsIgnoreCase("MD5-sess")) {
+ digAlg = "MD5";
+ }
+
+ final MessageDigest digester;
+ try {
+ digester = createMessageDigest(digAlg);
+ } catch (final UnsupportedDigestAlgorithmException ex) {
+ throw new AuthenticationException("Unsuppported digest algorithm: " + digAlg);
+ }
+
+ final String uname = credentials.getUserPrincipal().getName();
+ final String pwd = credentials.getPassword();
+
+ if (nonce.equals(this.lastNonce)) {
+ nounceCount++;
+ } else {
+ nounceCount = 1;
+ cnonce = null;
+ lastNonce = nonce;
+ }
+ final StringBuilder sb = new StringBuilder(256);
+ final Formatter formatter = new Formatter(sb, Locale.US);
+ formatter.format("%08x", nounceCount);
+ formatter.close();
+ final String nc = sb.toString();
+
+ if (cnonce == null) {
+ cnonce = createCnonce();
+ }
+
+ a1 = null;
+ a2 = null;
+ // 3.2.2.2: Calculating digest
+ if (algorithm.equalsIgnoreCase("MD5-sess")) {
+ // H( unq(username-value) ":" unq(realm-value) ":" passwd )
+ // ":" unq(nonce-value)
+ // ":" unq(cnonce-value)
+
+ // calculated one per session
+ sb.setLength(0);
+ sb.append(uname).append(':').append(realm).append(':').append(pwd);
+ final String checksum = encode(digester.digest(EncodingUtils.getBytes(sb.toString(), charset)));
+ sb.setLength(0);
+ sb.append(checksum).append(':').append(nonce).append(':').append(cnonce);
+ a1 = sb.toString();
+ } else {
+ // unq(username-value) ":" unq(realm-value) ":" passwd
+ sb.setLength(0);
+ sb.append(uname).append(':').append(realm).append(':').append(pwd);
+ a1 = sb.toString();
+ }
+
+ final String hasha1 = encode(digester.digest(EncodingUtils.getBytes(a1, charset)));
+
+ if (qop == QOP_AUTH) {
+ // Method ":" digest-uri-value
+ a2 = method + ':' + uri;
+ } else if (qop == QOP_AUTH_INT) {
+ // Method ":" digest-uri-value ":" H(entity-body)
+ HttpEntity entity = null;
+ if (request instanceof HttpEntityEnclosingRequest) {
+ entity = ((HttpEntityEnclosingRequest) request).getEntity();
+ }
+ if (entity != null && !entity.isRepeatable()) {
+ // If the entity is not repeatable, try falling back onto QOP_AUTH
+ if (qopset.contains("auth")) {
+ qop = QOP_AUTH;
+ a2 = method + ':' + uri;
+ } else {
+ throw new AuthenticationException("Qop auth-int cannot be used with " +
+ "a non-repeatable entity");
+ }
+ } else {
+ final HttpEntityDigester entityDigester = new HttpEntityDigester(digester);
+ try {
+ if (entity != null) {
+ entity.writeTo(entityDigester);
+ }
+ entityDigester.close();
+ } catch (final IOException ex) {
+ throw new AuthenticationException("I/O error reading entity content", ex);
+ }
+ a2 = method + ':' + uri + ':' + encode(entityDigester.getDigest());
+ }
+ } else {
+ a2 = method + ':' + uri;
+ }
+
+ final String hasha2 = encode(digester.digest(EncodingUtils.getBytes(a2, charset)));
+
+ // 3.2.2.1
+
+ final String digestValue;
+ if (qop == QOP_MISSING) {
+ sb.setLength(0);
+ sb.append(hasha1).append(':').append(nonce).append(':').append(hasha2);
+ digestValue = sb.toString();
+ } else {
+ sb.setLength(0);
+ sb.append(hasha1).append(':').append(nonce).append(':').append(nc).append(':')
+ .append(cnonce).append(':').append(qop == QOP_AUTH_INT ? "auth-int" : "auth")
+ .append(':').append(hasha2);
+ digestValue = sb.toString();
+ }
+
+ final String digest = encode(digester.digest(EncodingUtils.getAsciiBytes(digestValue)));
+
+ final CharArrayBuffer buffer = new CharArrayBuffer(128);
+ if (isProxy()) {
+ buffer.append(AUTH.PROXY_AUTH_RESP);
+ } else {
+ buffer.append(AUTH.WWW_AUTH_RESP);
+ }
+ buffer.append(": Digest ");
+
+ final List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>(20);
+ params.add(new BasicNameValuePair("username", uname));
+ params.add(new BasicNameValuePair("realm", realm));
+ params.add(new BasicNameValuePair("nonce", nonce));
+ params.add(new BasicNameValuePair("uri", uri));
+ params.add(new BasicNameValuePair("response", digest));
+
+ if (qop != QOP_MISSING) {
+ params.add(new BasicNameValuePair("qop", qop == QOP_AUTH_INT ? "auth-int" : "auth"));
+ params.add(new BasicNameValuePair("nc", nc));
+ params.add(new BasicNameValuePair("cnonce", cnonce));
+ }
+ // algorithm cannot be null here
+ params.add(new BasicNameValuePair("algorithm", algorithm));
+ if (opaque != null) {
+ params.add(new BasicNameValuePair("opaque", opaque));
+ }
+
+ for (int i = 0; i < params.size(); i++) {
+ final BasicNameValuePair param = params.get(i);
+ if (i > 0) {
+ buffer.append(", ");
+ }
+ final String name = param.getName();
+ final boolean noQuotes = ("nc".equals(name) || "qop".equals(name)
+ || "algorithm".equals(name));
+ BasicHeaderValueFormatter.INSTANCE.formatNameValuePair(buffer, param, !noQuotes);
+ }
+ return new BufferedHeader(buffer);
+ }
+
+ String getCnonce() {
+ return cnonce;
+ }
+
+ String getA1() {
+ return a1;
+ }
+
+ String getA2() {
+ return a2;
+ }
+
+ /**
+ * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long
+ * <CODE>String</CODE> according to RFC 2617.
+ *
+ * @param binaryData array containing the digest
+ * @return encoded MD5, or <CODE>null</CODE> if encoding failed
+ */
+ static String encode(final byte[] binaryData) {
+ final int n = binaryData.length;
+ final char[] buffer = new char[n * 2];
+ for (int i = 0; i < n; i++) {
+ final int low = (binaryData[i] & 0x0f);
+ final int high = ((binaryData[i] & 0xf0) >> 4);
+ buffer[i * 2] = HEXADECIMAL[high];
+ buffer[(i * 2) + 1] = HEXADECIMAL[low];
+ }
+
+ return new String(buffer);
+ }
+
+
+ /**
+ * Creates a random cnonce value based on the current time.
+ *
+ * @return The cnonce value as String.
+ */
+ public static String createCnonce() {
+ final SecureRandom rnd = new SecureRandom();
+ final byte[] tmp = new byte[8];
+ rnd.nextBytes(tmp);
+ return encode(tmp);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("DIGEST [complete=").append(complete)
+ .append(", nonce=").append(lastNonce)
+ .append(", nc=").append(nounceCount)
+ .append("]");
+ return builder.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/DigestSchemeFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/DigestSchemeFactory.java
new file mode 100644
index 0000000000..0582bed493
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/DigestSchemeFactory.java
@@ -0,0 +1,71 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.auth;
+
+import java.nio.charset.Charset;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.auth.AuthScheme;
+import ch.boye.httpclientandroidlib.auth.AuthSchemeFactory;
+import ch.boye.httpclientandroidlib.auth.AuthSchemeProvider;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * {@link AuthSchemeProvider} implementation that creates and initializes
+ * {@link DigestScheme} instances.
+ *
+ * @since 4.0
+ */
+@Immutable
+@SuppressWarnings("deprecation")
+public class DigestSchemeFactory implements AuthSchemeFactory, AuthSchemeProvider {
+
+ private final Charset charset;
+
+ /**
+ * @since 4.3
+ */
+ public DigestSchemeFactory(final Charset charset) {
+ super();
+ this.charset = charset;
+ }
+
+ public DigestSchemeFactory() {
+ this(null);
+ }
+
+ public AuthScheme newInstance(final HttpParams params) {
+ return new DigestScheme();
+ }
+
+ public AuthScheme create(final HttpContext context) {
+ return new DigestScheme(this.charset);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/HttpAuthenticator.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/HttpAuthenticator.java
new file mode 100644
index 0000000000..28b8b74d0b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/HttpAuthenticator.java
@@ -0,0 +1,245 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.auth;
+
+import java.io.IOException;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Queue;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.auth.AuthOption;
+import ch.boye.httpclientandroidlib.auth.AuthProtocolState;
+import ch.boye.httpclientandroidlib.auth.AuthScheme;
+import ch.boye.httpclientandroidlib.auth.AuthState;
+import ch.boye.httpclientandroidlib.auth.AuthenticationException;
+import ch.boye.httpclientandroidlib.auth.ContextAwareAuthScheme;
+import ch.boye.httpclientandroidlib.auth.Credentials;
+import ch.boye.httpclientandroidlib.auth.MalformedChallengeException;
+import ch.boye.httpclientandroidlib.client.AuthenticationStrategy;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Asserts;
+
+/**
+ * @since 4.3
+ */
+public class HttpAuthenticator {
+
+ public HttpClientAndroidLog log;
+
+ public HttpAuthenticator(final HttpClientAndroidLog log) {
+ super();
+ this.log = log != null ? log : new HttpClientAndroidLog(getClass());
+ }
+
+ public HttpAuthenticator() {
+ this(null);
+ }
+
+ public boolean isAuthenticationRequested(
+ final HttpHost host,
+ final HttpResponse response,
+ final AuthenticationStrategy authStrategy,
+ final AuthState authState,
+ final HttpContext context) {
+ if (authStrategy.isAuthenticationRequested(host, response, context)) {
+ this.log.debug("Authentication required");
+ if (authState.getState() == AuthProtocolState.SUCCESS) {
+ authStrategy.authFailed(host, authState.getAuthScheme(), context);
+ }
+ return true;
+ } else {
+ switch (authState.getState()) {
+ case CHALLENGED:
+ case HANDSHAKE:
+ this.log.debug("Authentication succeeded");
+ authState.setState(AuthProtocolState.SUCCESS);
+ authStrategy.authSucceeded(host, authState.getAuthScheme(), context);
+ break;
+ case SUCCESS:
+ break;
+ default:
+ authState.setState(AuthProtocolState.UNCHALLENGED);
+ }
+ return false;
+ }
+ }
+
+ public boolean handleAuthChallenge(
+ final HttpHost host,
+ final HttpResponse response,
+ final AuthenticationStrategy authStrategy,
+ final AuthState authState,
+ final HttpContext context) {
+ try {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug(host.toHostString() + " requested authentication");
+ }
+ final Map<String, Header> challenges = authStrategy.getChallenges(host, response, context);
+ if (challenges.isEmpty()) {
+ this.log.debug("Response contains no authentication challenges");
+ return false;
+ }
+
+ final AuthScheme authScheme = authState.getAuthScheme();
+ switch (authState.getState()) {
+ case FAILURE:
+ return false;
+ case SUCCESS:
+ authState.reset();
+ break;
+ case CHALLENGED:
+ case HANDSHAKE:
+ if (authScheme == null) {
+ this.log.debug("Auth scheme is null");
+ authStrategy.authFailed(host, null, context);
+ authState.reset();
+ authState.setState(AuthProtocolState.FAILURE);
+ return false;
+ }
+ case UNCHALLENGED:
+ if (authScheme != null) {
+ final String id = authScheme.getSchemeName();
+ final Header challenge = challenges.get(id.toLowerCase(Locale.ENGLISH));
+ if (challenge != null) {
+ this.log.debug("Authorization challenge processed");
+ authScheme.processChallenge(challenge);
+ if (authScheme.isComplete()) {
+ this.log.debug("Authentication failed");
+ authStrategy.authFailed(host, authState.getAuthScheme(), context);
+ authState.reset();
+ authState.setState(AuthProtocolState.FAILURE);
+ return false;
+ } else {
+ authState.setState(AuthProtocolState.HANDSHAKE);
+ return true;
+ }
+ } else {
+ authState.reset();
+ // Retry authentication with a different scheme
+ }
+ }
+ }
+ final Queue<AuthOption> authOptions = authStrategy.select(challenges, host, response, context);
+ if (authOptions != null && !authOptions.isEmpty()) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Selected authentication options: " + authOptions);
+ }
+ authState.setState(AuthProtocolState.CHALLENGED);
+ authState.update(authOptions);
+ return true;
+ } else {
+ return false;
+ }
+ } catch (final MalformedChallengeException ex) {
+ if (this.log.isWarnEnabled()) {
+ this.log.warn("Malformed challenge: " + ex.getMessage());
+ }
+ authState.reset();
+ return false;
+ }
+ }
+
+ public void generateAuthResponse(
+ final HttpRequest request,
+ final AuthState authState,
+ final HttpContext context) throws HttpException, IOException {
+ AuthScheme authScheme = authState.getAuthScheme();
+ Credentials creds = authState.getCredentials();
+ switch (authState.getState()) {
+ case FAILURE:
+ return;
+ case SUCCESS:
+ ensureAuthScheme(authScheme);
+ if (authScheme.isConnectionBased()) {
+ return;
+ }
+ break;
+ case CHALLENGED:
+ final Queue<AuthOption> authOptions = authState.getAuthOptions();
+ if (authOptions != null) {
+ while (!authOptions.isEmpty()) {
+ final AuthOption authOption = authOptions.remove();
+ authScheme = authOption.getAuthScheme();
+ creds = authOption.getCredentials();
+ authState.update(authScheme, creds);
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Generating response to an authentication challenge using "
+ + authScheme.getSchemeName() + " scheme");
+ }
+ try {
+ final Header header = doAuth(authScheme, creds, request, context);
+ request.addHeader(header);
+ break;
+ } catch (final AuthenticationException ex) {
+ if (this.log.isWarnEnabled()) {
+ this.log.warn(authScheme + " authentication error: " + ex.getMessage());
+ }
+ }
+ }
+ return;
+ } else {
+ ensureAuthScheme(authScheme);
+ }
+ }
+ if (authScheme != null) {
+ try {
+ final Header header = doAuth(authScheme, creds, request, context);
+ request.addHeader(header);
+ } catch (final AuthenticationException ex) {
+ if (this.log.isErrorEnabled()) {
+ this.log.error(authScheme + " authentication error: " + ex.getMessage());
+ }
+ }
+ }
+ }
+
+ private void ensureAuthScheme(final AuthScheme authScheme) {
+ Asserts.notNull(authScheme, "Auth scheme");
+ }
+
+ @SuppressWarnings("deprecation")
+ private Header doAuth(
+ final AuthScheme authScheme,
+ final Credentials creds,
+ final HttpRequest request,
+ final HttpContext context) throws AuthenticationException {
+ if (authScheme instanceof ContextAwareAuthScheme) {
+ return ((ContextAwareAuthScheme) authScheme).authenticate(creds, request, context);
+ } else {
+ return authScheme.authenticate(creds, request);
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/HttpEntityDigester.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/HttpEntityDigester.java
new file mode 100644
index 0000000000..0539515875
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/HttpEntityDigester.java
@@ -0,0 +1,75 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.auth;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.security.MessageDigest;
+
+class HttpEntityDigester extends OutputStream {
+
+ private final MessageDigest digester;
+ private boolean closed;
+ private byte[] digest;
+
+ HttpEntityDigester(final MessageDigest digester) {
+ super();
+ this.digester = digester;
+ this.digester.reset();
+ }
+
+ @Override
+ public void write(final int b) throws IOException {
+ if (this.closed) {
+ throw new IOException("Stream has been already closed");
+ }
+ this.digester.update((byte) b);
+ }
+
+ @Override
+ public void write(final byte[] b, final int off, final int len) throws IOException {
+ if (this.closed) {
+ throw new IOException("Stream has been already closed");
+ }
+ this.digester.update(b, off, len);
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (this.closed) {
+ return;
+ }
+ this.closed = true;
+ this.digest = this.digester.digest();
+ super.close();
+ }
+
+ public byte[] getDigest() {
+ return this.digest;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/NTLMEngine.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/NTLMEngine.java
new file mode 100644
index 0000000000..ce2457000a
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/NTLMEngine.java
@@ -0,0 +1,70 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.auth;
+
+/**
+ * Abstract NTLM authentication engine. The engine can be used to
+ * generate Type1 messages and Type3 messages in response to a
+ * Type2 challenge.
+ *
+ * @since 4.0
+ */
+public interface NTLMEngine {
+
+ /**
+ * Generates a Type1 message given the domain and workstation.
+ *
+ * @param domain Optional Windows domain name. Can be <code>null</code>.
+ * @param workstation Optional Windows workstation name. Can be
+ * <code>null</code>.
+ * @return Type1 message
+ * @throws NTLMEngineException
+ */
+ String generateType1Msg(
+ String domain,
+ String workstation) throws NTLMEngineException;
+
+ /**
+ * Generates a Type3 message given the user credentials and the
+ * authentication challenge.
+ *
+ * @param username Windows user name
+ * @param password Password
+ * @param domain Windows domain name
+ * @param workstation Windows workstation name
+ * @param challenge Type2 challenge.
+ * @return Type3 response.
+ * @throws NTLMEngineException
+ */
+ String generateType3Msg(
+ String username,
+ String password,
+ String domain,
+ String workstation,
+ String challenge) throws NTLMEngineException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/NTLMEngineException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/NTLMEngineException.java
new file mode 100644
index 0000000000..88ef96912c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/NTLMEngineException.java
@@ -0,0 +1,67 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.auth;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.auth.AuthenticationException;
+
+/**
+ * Signals NTLM protocol failure.
+ *
+ *
+ * @since 4.0
+ */
+@Immutable
+public class NTLMEngineException extends AuthenticationException {
+
+ private static final long serialVersionUID = 6027981323731768824L;
+
+ public NTLMEngineException() {
+ super();
+ }
+
+ /**
+ * Creates a new NTLMEngineException with the specified message.
+ *
+ * @param message the exception detail message
+ */
+ public NTLMEngineException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new NTLMEngineException with the specified detail message and cause.
+ *
+ * @param message the exception detail message
+ * @param cause the <tt>Throwable</tt> that caused this exception, or <tt>null</tt>
+ * if the cause is unavailable, unknown, or not a <tt>Throwable</tt>
+ */
+ public NTLMEngineException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/NTLMEngineImpl.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/NTLMEngineImpl.java
new file mode 100644
index 0000000000..195148f6b8
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/NTLMEngineImpl.java
@@ -0,0 +1,1672 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.auth;
+
+import java.io.UnsupportedEncodingException;
+import java.security.Key;
+import java.security.MessageDigest;
+import java.util.Arrays;
+import java.util.Locale;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.mozilla.apache.commons.codec.binary.Base64;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.EncodingUtils;
+
+/**
+ * Provides an implementation for NTLMv1, NTLMv2, and NTLM2 Session forms of the NTLM
+ * authentication protocol.
+ *
+ * @since 4.1
+ */
+@NotThreadSafe
+final class NTLMEngineImpl implements NTLMEngine {
+
+ // Flags we use; descriptions according to:
+ // http://davenport.sourceforge.net/ntlm.html
+ // and
+ // http://msdn.microsoft.com/en-us/library/cc236650%28v=prot.20%29.aspx
+ protected static final int FLAG_REQUEST_UNICODE_ENCODING = 0x00000001; // Unicode string encoding requested
+ protected static final int FLAG_REQUEST_TARGET = 0x00000004; // Requests target field
+ protected static final int FLAG_REQUEST_SIGN = 0x00000010; // Requests all messages have a signature attached, in NEGOTIATE message.
+ protected static final int FLAG_REQUEST_SEAL = 0x00000020; // Request key exchange for message confidentiality in NEGOTIATE message. MUST be used in conjunction with 56BIT.
+ protected static final int FLAG_REQUEST_LAN_MANAGER_KEY = 0x00000080; // Request Lan Manager key instead of user session key
+ protected static final int FLAG_REQUEST_NTLMv1 = 0x00000200; // Request NTLMv1 security. MUST be set in NEGOTIATE and CHALLENGE both
+ protected static final int FLAG_DOMAIN_PRESENT = 0x00001000; // Domain is present in message
+ protected static final int FLAG_WORKSTATION_PRESENT = 0x00002000; // Workstation is present in message
+ protected static final int FLAG_REQUEST_ALWAYS_SIGN = 0x00008000; // Requests a signature block on all messages. Overridden by REQUEST_SIGN and REQUEST_SEAL.
+ protected static final int FLAG_REQUEST_NTLM2_SESSION = 0x00080000; // From server in challenge, requesting NTLM2 session security
+ protected static final int FLAG_REQUEST_VERSION = 0x02000000; // Request protocol version
+ protected static final int FLAG_TARGETINFO_PRESENT = 0x00800000; // From server in challenge message, indicating targetinfo is present
+ protected static final int FLAG_REQUEST_128BIT_KEY_EXCH = 0x20000000; // Request explicit 128-bit key exchange
+ protected static final int FLAG_REQUEST_EXPLICIT_KEY_EXCH = 0x40000000; // Request explicit key exchange
+ protected static final int FLAG_REQUEST_56BIT_ENCRYPTION = 0x80000000; // Must be used in conjunction with SEAL
+
+
+ /** Secure random generator */
+ private static final java.security.SecureRandom RND_GEN;
+ static {
+ java.security.SecureRandom rnd = null;
+ try {
+ rnd = java.security.SecureRandom.getInstance("SHA1PRNG");
+ } catch (final Exception ignore) {
+ }
+ RND_GEN = rnd;
+ }
+
+ /** Character encoding */
+ static final String DEFAULT_CHARSET = "ASCII";
+
+ /** The character set to use for encoding the credentials */
+ private String credentialCharset = DEFAULT_CHARSET;
+
+ /** The signature string as bytes in the default encoding */
+ private static final byte[] SIGNATURE;
+
+ static {
+ final byte[] bytesWithoutNull = EncodingUtils.getBytes("NTLMSSP", "ASCII");
+ SIGNATURE = new byte[bytesWithoutNull.length + 1];
+ System.arraycopy(bytesWithoutNull, 0, SIGNATURE, 0, bytesWithoutNull.length);
+ SIGNATURE[bytesWithoutNull.length] = (byte) 0x00;
+ }
+
+ /**
+ * Returns the response for the given message.
+ *
+ * @param message
+ * the message that was received from the server.
+ * @param username
+ * the username to authenticate with.
+ * @param password
+ * the password to authenticate with.
+ * @param host
+ * The host.
+ * @param domain
+ * the NT domain to authenticate in.
+ * @return The response.
+ * @throws ch.boye.httpclientandroidlib.HttpException
+ * If the messages cannot be retrieved.
+ */
+ final String getResponseFor(final String message, final String username, final String password,
+ final String host, final String domain) throws NTLMEngineException {
+
+ final String response;
+ if (message == null || message.trim().equals("")) {
+ response = getType1Message(host, domain);
+ } else {
+ final Type2Message t2m = new Type2Message(message);
+ response = getType3Message(username, password, host, domain, t2m.getChallenge(), t2m
+ .getFlags(), t2m.getTarget(), t2m.getTargetInfo());
+ }
+ return response;
+ }
+
+ /**
+ * Creates the first message (type 1 message) in the NTLM authentication
+ * sequence. This message includes the user name, domain and host for the
+ * authentication session.
+ *
+ * @param host
+ * the computer name of the host requesting authentication.
+ * @param domain
+ * The domain to authenticate with.
+ * @return String the message to add to the HTTP request header.
+ */
+ String getType1Message(final String host, final String domain) throws NTLMEngineException {
+ return new Type1Message(domain, host).getResponse();
+ }
+
+ /**
+ * Creates the type 3 message using the given server nonce. The type 3
+ * message includes all the information for authentication, host, domain,
+ * username and the result of encrypting the nonce sent by the server using
+ * the user's password as the key.
+ *
+ * @param user
+ * The user name. This should not include the domain name.
+ * @param password
+ * The password.
+ * @param host
+ * The host that is originating the authentication request.
+ * @param domain
+ * The domain to authenticate within.
+ * @param nonce
+ * the 8 byte array the server sent.
+ * @return The type 3 message.
+ * @throws NTLMEngineException
+ * If {@link #RC4(byte[],byte[])} fails.
+ */
+ String getType3Message(final String user, final String password, final String host, final String domain,
+ final byte[] nonce, final int type2Flags, final String target, final byte[] targetInformation)
+ throws NTLMEngineException {
+ return new Type3Message(domain, host, user, password, nonce, type2Flags, target,
+ targetInformation).getResponse();
+ }
+
+ /**
+ * @return Returns the credentialCharset.
+ */
+ String getCredentialCharset() {
+ return credentialCharset;
+ }
+
+ /**
+ * @param credentialCharset
+ * The credentialCharset to set.
+ */
+ void setCredentialCharset(final String credentialCharset) {
+ this.credentialCharset = credentialCharset;
+ }
+
+ /** Strip dot suffix from a name */
+ private static String stripDotSuffix(final String value) {
+ if (value == null) {
+ return null;
+ }
+ final int index = value.indexOf(".");
+ if (index != -1) {
+ return value.substring(0, index);
+ }
+ return value;
+ }
+
+ /** Convert host to standard form */
+ private static String convertHost(final String host) {
+ return stripDotSuffix(host);
+ }
+
+ /** Convert domain to standard form */
+ private static String convertDomain(final String domain) {
+ return stripDotSuffix(domain);
+ }
+
+ private static int readULong(final byte[] src, final int index) throws NTLMEngineException {
+ if (src.length < index + 4) {
+ throw new NTLMEngineException("NTLM authentication - buffer too small for DWORD");
+ }
+ return (src[index] & 0xff) | ((src[index + 1] & 0xff) << 8)
+ | ((src[index + 2] & 0xff) << 16) | ((src[index + 3] & 0xff) << 24);
+ }
+
+ private static int readUShort(final byte[] src, final int index) throws NTLMEngineException {
+ if (src.length < index + 2) {
+ throw new NTLMEngineException("NTLM authentication - buffer too small for WORD");
+ }
+ return (src[index] & 0xff) | ((src[index + 1] & 0xff) << 8);
+ }
+
+ private static byte[] readSecurityBuffer(final byte[] src, final int index) throws NTLMEngineException {
+ final int length = readUShort(src, index);
+ final int offset = readULong(src, index + 4);
+ if (src.length < offset + length) {
+ throw new NTLMEngineException(
+ "NTLM authentication - buffer too small for data item");
+ }
+ final byte[] buffer = new byte[length];
+ System.arraycopy(src, offset, buffer, 0, length);
+ return buffer;
+ }
+
+ /** Calculate a challenge block */
+ private static byte[] makeRandomChallenge() throws NTLMEngineException {
+ if (RND_GEN == null) {
+ throw new NTLMEngineException("Random generator not available");
+ }
+ final byte[] rval = new byte[8];
+ synchronized (RND_GEN) {
+ RND_GEN.nextBytes(rval);
+ }
+ return rval;
+ }
+
+ /** Calculate a 16-byte secondary key */
+ private static byte[] makeSecondaryKey() throws NTLMEngineException {
+ if (RND_GEN == null) {
+ throw new NTLMEngineException("Random generator not available");
+ }
+ final byte[] rval = new byte[16];
+ synchronized (RND_GEN) {
+ RND_GEN.nextBytes(rval);
+ }
+ return rval;
+ }
+
+ protected static class CipherGen {
+
+ protected final String domain;
+ protected final String user;
+ protected final String password;
+ protected final byte[] challenge;
+ protected final String target;
+ protected final byte[] targetInformation;
+
+ // Information we can generate but may be passed in (for testing)
+ protected byte[] clientChallenge;
+ protected byte[] clientChallenge2;
+ protected byte[] secondaryKey;
+ protected byte[] timestamp;
+
+ // Stuff we always generate
+ protected byte[] lmHash = null;
+ protected byte[] lmResponse = null;
+ protected byte[] ntlmHash = null;
+ protected byte[] ntlmResponse = null;
+ protected byte[] ntlmv2Hash = null;
+ protected byte[] lmv2Hash = null;
+ protected byte[] lmv2Response = null;
+ protected byte[] ntlmv2Blob = null;
+ protected byte[] ntlmv2Response = null;
+ protected byte[] ntlm2SessionResponse = null;
+ protected byte[] lm2SessionResponse = null;
+ protected byte[] lmUserSessionKey = null;
+ protected byte[] ntlmUserSessionKey = null;
+ protected byte[] ntlmv2UserSessionKey = null;
+ protected byte[] ntlm2SessionResponseUserSessionKey = null;
+ protected byte[] lanManagerSessionKey = null;
+
+ public CipherGen(final String domain, final String user, final String password,
+ final byte[] challenge, final String target, final byte[] targetInformation,
+ final byte[] clientChallenge, final byte[] clientChallenge2,
+ final byte[] secondaryKey, final byte[] timestamp) {
+ this.domain = domain;
+ this.target = target;
+ this.user = user;
+ this.password = password;
+ this.challenge = challenge;
+ this.targetInformation = targetInformation;
+ this.clientChallenge = clientChallenge;
+ this.clientChallenge2 = clientChallenge2;
+ this.secondaryKey = secondaryKey;
+ this.timestamp = timestamp;
+ }
+
+ public CipherGen(final String domain, final String user, final String password,
+ final byte[] challenge, final String target, final byte[] targetInformation) {
+ this(domain, user, password, challenge, target, targetInformation, null, null, null, null);
+ }
+
+ /** Calculate and return client challenge */
+ public byte[] getClientChallenge()
+ throws NTLMEngineException {
+ if (clientChallenge == null) {
+ clientChallenge = makeRandomChallenge();
+ }
+ return clientChallenge;
+ }
+
+ /** Calculate and return second client challenge */
+ public byte[] getClientChallenge2()
+ throws NTLMEngineException {
+ if (clientChallenge2 == null) {
+ clientChallenge2 = makeRandomChallenge();
+ }
+ return clientChallenge2;
+ }
+
+ /** Calculate and return random secondary key */
+ public byte[] getSecondaryKey()
+ throws NTLMEngineException {
+ if (secondaryKey == null) {
+ secondaryKey = makeSecondaryKey();
+ }
+ return secondaryKey;
+ }
+
+ /** Calculate and return the LMHash */
+ public byte[] getLMHash()
+ throws NTLMEngineException {
+ if (lmHash == null) {
+ lmHash = lmHash(password);
+ }
+ return lmHash;
+ }
+
+ /** Calculate and return the LMResponse */
+ public byte[] getLMResponse()
+ throws NTLMEngineException {
+ if (lmResponse == null) {
+ lmResponse = lmResponse(getLMHash(),challenge);
+ }
+ return lmResponse;
+ }
+
+ /** Calculate and return the NTLMHash */
+ public byte[] getNTLMHash()
+ throws NTLMEngineException {
+ if (ntlmHash == null) {
+ ntlmHash = ntlmHash(password);
+ }
+ return ntlmHash;
+ }
+
+ /** Calculate and return the NTLMResponse */
+ public byte[] getNTLMResponse()
+ throws NTLMEngineException {
+ if (ntlmResponse == null) {
+ ntlmResponse = lmResponse(getNTLMHash(),challenge);
+ }
+ return ntlmResponse;
+ }
+
+ /** Calculate the LMv2 hash */
+ public byte[] getLMv2Hash()
+ throws NTLMEngineException {
+ if (lmv2Hash == null) {
+ lmv2Hash = lmv2Hash(domain, user, getNTLMHash());
+ }
+ return lmv2Hash;
+ }
+
+ /** Calculate the NTLMv2 hash */
+ public byte[] getNTLMv2Hash()
+ throws NTLMEngineException {
+ if (ntlmv2Hash == null) {
+ ntlmv2Hash = ntlmv2Hash(domain, user, getNTLMHash());
+ }
+ return ntlmv2Hash;
+ }
+
+ /** Calculate a timestamp */
+ public byte[] getTimestamp() {
+ if (timestamp == null) {
+ long time = System.currentTimeMillis();
+ time += 11644473600000l; // milliseconds from January 1, 1601 -> epoch.
+ time *= 10000; // tenths of a microsecond.
+ // convert to little-endian byte array.
+ timestamp = new byte[8];
+ for (int i = 0; i < 8; i++) {
+ timestamp[i] = (byte) time;
+ time >>>= 8;
+ }
+ }
+ return timestamp;
+ }
+
+ /** Calculate the NTLMv2Blob */
+ public byte[] getNTLMv2Blob()
+ throws NTLMEngineException {
+ if (ntlmv2Blob == null) {
+ ntlmv2Blob = createBlob(getClientChallenge2(), targetInformation, getTimestamp());
+ }
+ return ntlmv2Blob;
+ }
+
+ /** Calculate the NTLMv2Response */
+ public byte[] getNTLMv2Response()
+ throws NTLMEngineException {
+ if (ntlmv2Response == null) {
+ ntlmv2Response = lmv2Response(getNTLMv2Hash(),challenge,getNTLMv2Blob());
+ }
+ return ntlmv2Response;
+ }
+
+ /** Calculate the LMv2Response */
+ public byte[] getLMv2Response()
+ throws NTLMEngineException {
+ if (lmv2Response == null) {
+ lmv2Response = lmv2Response(getLMv2Hash(),challenge,getClientChallenge());
+ }
+ return lmv2Response;
+ }
+
+ /** Get NTLM2SessionResponse */
+ public byte[] getNTLM2SessionResponse()
+ throws NTLMEngineException {
+ if (ntlm2SessionResponse == null) {
+ ntlm2SessionResponse = ntlm2SessionResponse(getNTLMHash(),challenge,getClientChallenge());
+ }
+ return ntlm2SessionResponse;
+ }
+
+ /** Calculate and return LM2 session response */
+ public byte[] getLM2SessionResponse()
+ throws NTLMEngineException {
+ if (lm2SessionResponse == null) {
+ final byte[] clChallenge = getClientChallenge();
+ lm2SessionResponse = new byte[24];
+ System.arraycopy(clChallenge, 0, lm2SessionResponse, 0, clChallenge.length);
+ Arrays.fill(lm2SessionResponse, clChallenge.length, lm2SessionResponse.length, (byte) 0x00);
+ }
+ return lm2SessionResponse;
+ }
+
+ /** Get LMUserSessionKey */
+ public byte[] getLMUserSessionKey()
+ throws NTLMEngineException {
+ if (lmUserSessionKey == null) {
+ lmUserSessionKey = new byte[16];
+ System.arraycopy(getLMHash(), 0, lmUserSessionKey, 0, 8);
+ Arrays.fill(lmUserSessionKey, 8, 16, (byte) 0x00);
+ }
+ return lmUserSessionKey;
+ }
+
+ /** Get NTLMUserSessionKey */
+ public byte[] getNTLMUserSessionKey()
+ throws NTLMEngineException {
+ if (ntlmUserSessionKey == null) {
+ final MD4 md4 = new MD4();
+ md4.update(getNTLMHash());
+ ntlmUserSessionKey = md4.getOutput();
+ }
+ return ntlmUserSessionKey;
+ }
+
+ /** GetNTLMv2UserSessionKey */
+ public byte[] getNTLMv2UserSessionKey()
+ throws NTLMEngineException {
+ if (ntlmv2UserSessionKey == null) {
+ final byte[] ntlmv2hash = getNTLMv2Hash();
+ final byte[] truncatedResponse = new byte[16];
+ System.arraycopy(getNTLMv2Response(), 0, truncatedResponse, 0, 16);
+ ntlmv2UserSessionKey = hmacMD5(truncatedResponse, ntlmv2hash);
+ }
+ return ntlmv2UserSessionKey;
+ }
+
+ /** Get NTLM2SessionResponseUserSessionKey */
+ public byte[] getNTLM2SessionResponseUserSessionKey()
+ throws NTLMEngineException {
+ if (ntlm2SessionResponseUserSessionKey == null) {
+ final byte[] ntlm2SessionResponseNonce = getLM2SessionResponse();
+ final byte[] sessionNonce = new byte[challenge.length + ntlm2SessionResponseNonce.length];
+ System.arraycopy(challenge, 0, sessionNonce, 0, challenge.length);
+ System.arraycopy(ntlm2SessionResponseNonce, 0, sessionNonce, challenge.length, ntlm2SessionResponseNonce.length);
+ ntlm2SessionResponseUserSessionKey = hmacMD5(sessionNonce,getNTLMUserSessionKey());
+ }
+ return ntlm2SessionResponseUserSessionKey;
+ }
+
+ /** Get LAN Manager session key */
+ public byte[] getLanManagerSessionKey()
+ throws NTLMEngineException {
+ if (lanManagerSessionKey == null) {
+ try {
+ final byte[] keyBytes = new byte[14];
+ System.arraycopy(getLMHash(), 0, keyBytes, 0, 8);
+ Arrays.fill(keyBytes, 8, keyBytes.length, (byte)0xbd);
+ final Key lowKey = createDESKey(keyBytes, 0);
+ final Key highKey = createDESKey(keyBytes, 7);
+ final byte[] truncatedResponse = new byte[8];
+ System.arraycopy(getLMResponse(), 0, truncatedResponse, 0, truncatedResponse.length);
+ Cipher des = Cipher.getInstance("DES/ECB/NoPadding");
+ des.init(Cipher.ENCRYPT_MODE, lowKey);
+ final byte[] lowPart = des.doFinal(truncatedResponse);
+ des = Cipher.getInstance("DES/ECB/NoPadding");
+ des.init(Cipher.ENCRYPT_MODE, highKey);
+ final byte[] highPart = des.doFinal(truncatedResponse);
+ lanManagerSessionKey = new byte[16];
+ System.arraycopy(lowPart, 0, lanManagerSessionKey, 0, lowPart.length);
+ System.arraycopy(highPart, 0, lanManagerSessionKey, lowPart.length, highPart.length);
+ } catch (final Exception e) {
+ throw new NTLMEngineException(e.getMessage(), e);
+ }
+ }
+ return lanManagerSessionKey;
+ }
+ }
+
+ /** Calculates HMAC-MD5 */
+ static byte[] hmacMD5(final byte[] value, final byte[] key)
+ throws NTLMEngineException {
+ final HMACMD5 hmacMD5 = new HMACMD5(key);
+ hmacMD5.update(value);
+ return hmacMD5.getOutput();
+ }
+
+ /** Calculates RC4 */
+ static byte[] RC4(final byte[] value, final byte[] key)
+ throws NTLMEngineException {
+ try {
+ final Cipher rc4 = Cipher.getInstance("RC4");
+ rc4.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "RC4"));
+ return rc4.doFinal(value);
+ } catch (final Exception e) {
+ throw new NTLMEngineException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Calculates the NTLM2 Session Response for the given challenge, using the
+ * specified password and client challenge.
+ *
+ * @return The NTLM2 Session Response. This is placed in the NTLM response
+ * field of the Type 3 message; the LM response field contains the
+ * client challenge, null-padded to 24 bytes.
+ */
+ static byte[] ntlm2SessionResponse(final byte[] ntlmHash, final byte[] challenge,
+ final byte[] clientChallenge) throws NTLMEngineException {
+ try {
+ // Look up MD5 algorithm (was necessary on jdk 1.4.2)
+ // This used to be needed, but java 1.5.0_07 includes the MD5
+ // algorithm (finally)
+ // Class x = Class.forName("gnu.crypto.hash.MD5");
+ // Method updateMethod = x.getMethod("update",new
+ // Class[]{byte[].class});
+ // Method digestMethod = x.getMethod("digest",new Class[0]);
+ // Object mdInstance = x.newInstance();
+ // updateMethod.invoke(mdInstance,new Object[]{challenge});
+ // updateMethod.invoke(mdInstance,new Object[]{clientChallenge});
+ // byte[] digest = (byte[])digestMethod.invoke(mdInstance,new
+ // Object[0]);
+
+ final MessageDigest md5 = MessageDigest.getInstance("MD5");
+ md5.update(challenge);
+ md5.update(clientChallenge);
+ final byte[] digest = md5.digest();
+
+ final byte[] sessionHash = new byte[8];
+ System.arraycopy(digest, 0, sessionHash, 0, 8);
+ return lmResponse(ntlmHash, sessionHash);
+ } catch (final Exception e) {
+ if (e instanceof NTLMEngineException) {
+ throw (NTLMEngineException) e;
+ }
+ throw new NTLMEngineException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Creates the LM Hash of the user's password.
+ *
+ * @param password
+ * The password.
+ *
+ * @return The LM Hash of the given password, used in the calculation of the
+ * LM Response.
+ */
+ private static byte[] lmHash(final String password) throws NTLMEngineException {
+ try {
+ final byte[] oemPassword = password.toUpperCase(Locale.ENGLISH).getBytes("US-ASCII");
+ final int length = Math.min(oemPassword.length, 14);
+ final byte[] keyBytes = new byte[14];
+ System.arraycopy(oemPassword, 0, keyBytes, 0, length);
+ final Key lowKey = createDESKey(keyBytes, 0);
+ final Key highKey = createDESKey(keyBytes, 7);
+ final byte[] magicConstant = "KGS!@#$%".getBytes("US-ASCII");
+ final Cipher des = Cipher.getInstance("DES/ECB/NoPadding");
+ des.init(Cipher.ENCRYPT_MODE, lowKey);
+ final byte[] lowHash = des.doFinal(magicConstant);
+ des.init(Cipher.ENCRYPT_MODE, highKey);
+ final byte[] highHash = des.doFinal(magicConstant);
+ final byte[] lmHash = new byte[16];
+ System.arraycopy(lowHash, 0, lmHash, 0, 8);
+ System.arraycopy(highHash, 0, lmHash, 8, 8);
+ return lmHash;
+ } catch (final Exception e) {
+ throw new NTLMEngineException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Creates the NTLM Hash of the user's password.
+ *
+ * @param password
+ * The password.
+ *
+ * @return The NTLM Hash of the given password, used in the calculation of
+ * the NTLM Response and the NTLMv2 and LMv2 Hashes.
+ */
+ private static byte[] ntlmHash(final String password) throws NTLMEngineException {
+ try {
+ final byte[] unicodePassword = password.getBytes("UnicodeLittleUnmarked");
+ final MD4 md4 = new MD4();
+ md4.update(unicodePassword);
+ return md4.getOutput();
+ } catch (final UnsupportedEncodingException e) {
+ throw new NTLMEngineException("Unicode not supported: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Creates the LMv2 Hash of the user's password.
+ *
+ * @return The LMv2 Hash, used in the calculation of the NTLMv2 and LMv2
+ * Responses.
+ */
+ private static byte[] lmv2Hash(final String domain, final String user, final byte[] ntlmHash)
+ throws NTLMEngineException {
+ try {
+ final HMACMD5 hmacMD5 = new HMACMD5(ntlmHash);
+ // Upper case username, upper case domain!
+ hmacMD5.update(user.toUpperCase(Locale.ENGLISH).getBytes("UnicodeLittleUnmarked"));
+ if (domain != null) {
+ hmacMD5.update(domain.toUpperCase(Locale.ENGLISH).getBytes("UnicodeLittleUnmarked"));
+ }
+ return hmacMD5.getOutput();
+ } catch (final UnsupportedEncodingException e) {
+ throw new NTLMEngineException("Unicode not supported! " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Creates the NTLMv2 Hash of the user's password.
+ *
+ * @return The NTLMv2 Hash, used in the calculation of the NTLMv2 and LMv2
+ * Responses.
+ */
+ private static byte[] ntlmv2Hash(final String domain, final String user, final byte[] ntlmHash)
+ throws NTLMEngineException {
+ try {
+ final HMACMD5 hmacMD5 = new HMACMD5(ntlmHash);
+ // Upper case username, mixed case target!!
+ hmacMD5.update(user.toUpperCase(Locale.ENGLISH).getBytes("UnicodeLittleUnmarked"));
+ if (domain != null) {
+ hmacMD5.update(domain.getBytes("UnicodeLittleUnmarked"));
+ }
+ return hmacMD5.getOutput();
+ } catch (final UnsupportedEncodingException e) {
+ throw new NTLMEngineException("Unicode not supported! " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Creates the LM Response from the given hash and Type 2 challenge.
+ *
+ * @param hash
+ * The LM or NTLM Hash.
+ * @param challenge
+ * The server challenge from the Type 2 message.
+ *
+ * @return The response (either LM or NTLM, depending on the provided hash).
+ */
+ private static byte[] lmResponse(final byte[] hash, final byte[] challenge) throws NTLMEngineException {
+ try {
+ final byte[] keyBytes = new byte[21];
+ System.arraycopy(hash, 0, keyBytes, 0, 16);
+ final Key lowKey = createDESKey(keyBytes, 0);
+ final Key middleKey = createDESKey(keyBytes, 7);
+ final Key highKey = createDESKey(keyBytes, 14);
+ final Cipher des = Cipher.getInstance("DES/ECB/NoPadding");
+ des.init(Cipher.ENCRYPT_MODE, lowKey);
+ final byte[] lowResponse = des.doFinal(challenge);
+ des.init(Cipher.ENCRYPT_MODE, middleKey);
+ final byte[] middleResponse = des.doFinal(challenge);
+ des.init(Cipher.ENCRYPT_MODE, highKey);
+ final byte[] highResponse = des.doFinal(challenge);
+ final byte[] lmResponse = new byte[24];
+ System.arraycopy(lowResponse, 0, lmResponse, 0, 8);
+ System.arraycopy(middleResponse, 0, lmResponse, 8, 8);
+ System.arraycopy(highResponse, 0, lmResponse, 16, 8);
+ return lmResponse;
+ } catch (final Exception e) {
+ throw new NTLMEngineException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Creates the LMv2 Response from the given hash, client data, and Type 2
+ * challenge.
+ *
+ * @param hash
+ * The NTLMv2 Hash.
+ * @param clientData
+ * The client data (blob or client challenge).
+ * @param challenge
+ * The server challenge from the Type 2 message.
+ *
+ * @return The response (either NTLMv2 or LMv2, depending on the client
+ * data).
+ */
+ private static byte[] lmv2Response(final byte[] hash, final byte[] challenge, final byte[] clientData)
+ throws NTLMEngineException {
+ final HMACMD5 hmacMD5 = new HMACMD5(hash);
+ hmacMD5.update(challenge);
+ hmacMD5.update(clientData);
+ final byte[] mac = hmacMD5.getOutput();
+ final byte[] lmv2Response = new byte[mac.length + clientData.length];
+ System.arraycopy(mac, 0, lmv2Response, 0, mac.length);
+ System.arraycopy(clientData, 0, lmv2Response, mac.length, clientData.length);
+ return lmv2Response;
+ }
+
+ /**
+ * Creates the NTLMv2 blob from the given target information block and
+ * client challenge.
+ *
+ * @param targetInformation
+ * The target information block from the Type 2 message.
+ * @param clientChallenge
+ * The random 8-byte client challenge.
+ *
+ * @return The blob, used in the calculation of the NTLMv2 Response.
+ */
+ private static byte[] createBlob(final byte[] clientChallenge, final byte[] targetInformation, final byte[] timestamp) {
+ final byte[] blobSignature = new byte[] { (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00 };
+ final byte[] reserved = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 };
+ final byte[] unknown1 = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 };
+ final byte[] unknown2 = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 };
+ final byte[] blob = new byte[blobSignature.length + reserved.length + timestamp.length + 8
+ + unknown1.length + targetInformation.length + unknown2.length];
+ int offset = 0;
+ System.arraycopy(blobSignature, 0, blob, offset, blobSignature.length);
+ offset += blobSignature.length;
+ System.arraycopy(reserved, 0, blob, offset, reserved.length);
+ offset += reserved.length;
+ System.arraycopy(timestamp, 0, blob, offset, timestamp.length);
+ offset += timestamp.length;
+ System.arraycopy(clientChallenge, 0, blob, offset, 8);
+ offset += 8;
+ System.arraycopy(unknown1, 0, blob, offset, unknown1.length);
+ offset += unknown1.length;
+ System.arraycopy(targetInformation, 0, blob, offset, targetInformation.length);
+ offset += targetInformation.length;
+ System.arraycopy(unknown2, 0, blob, offset, unknown2.length);
+ offset += unknown2.length;
+ return blob;
+ }
+
+ /**
+ * Creates a DES encryption key from the given key material.
+ *
+ * @param bytes
+ * A byte array containing the DES key material.
+ * @param offset
+ * The offset in the given byte array at which the 7-byte key
+ * material starts.
+ *
+ * @return A DES encryption key created from the key material starting at
+ * the specified offset in the given byte array.
+ */
+ private static Key createDESKey(final byte[] bytes, final int offset) {
+ final byte[] keyBytes = new byte[7];
+ System.arraycopy(bytes, offset, keyBytes, 0, 7);
+ final byte[] material = new byte[8];
+ material[0] = keyBytes[0];
+ material[1] = (byte) (keyBytes[0] << 7 | (keyBytes[1] & 0xff) >>> 1);
+ material[2] = (byte) (keyBytes[1] << 6 | (keyBytes[2] & 0xff) >>> 2);
+ material[3] = (byte) (keyBytes[2] << 5 | (keyBytes[3] & 0xff) >>> 3);
+ material[4] = (byte) (keyBytes[3] << 4 | (keyBytes[4] & 0xff) >>> 4);
+ material[5] = (byte) (keyBytes[4] << 3 | (keyBytes[5] & 0xff) >>> 5);
+ material[6] = (byte) (keyBytes[5] << 2 | (keyBytes[6] & 0xff) >>> 6);
+ material[7] = (byte) (keyBytes[6] << 1);
+ oddParity(material);
+ return new SecretKeySpec(material, "DES");
+ }
+
+ /**
+ * Applies odd parity to the given byte array.
+ *
+ * @param bytes
+ * The data whose parity bits are to be adjusted for odd parity.
+ */
+ private static void oddParity(final byte[] bytes) {
+ for (int i = 0; i < bytes.length; i++) {
+ final byte b = bytes[i];
+ final boolean needsParity = (((b >>> 7) ^ (b >>> 6) ^ (b >>> 5) ^ (b >>> 4) ^ (b >>> 3)
+ ^ (b >>> 2) ^ (b >>> 1)) & 0x01) == 0;
+ if (needsParity) {
+ bytes[i] |= (byte) 0x01;
+ } else {
+ bytes[i] &= (byte) 0xfe;
+ }
+ }
+ }
+
+ /** NTLM message generation, base class */
+ static class NTLMMessage {
+ /** The current response */
+ private byte[] messageContents = null;
+
+ /** The current output position */
+ private int currentOutputPosition = 0;
+
+ /** Constructor to use when message contents are not yet known */
+ NTLMMessage() {
+ }
+
+ /** Constructor to use when message contents are known */
+ NTLMMessage(final String messageBody, final int expectedType) throws NTLMEngineException {
+ messageContents = Base64.decodeBase64(EncodingUtils.getBytes(messageBody,
+ DEFAULT_CHARSET));
+ // Look for NTLM message
+ if (messageContents.length < SIGNATURE.length) {
+ throw new NTLMEngineException("NTLM message decoding error - packet too short");
+ }
+ int i = 0;
+ while (i < SIGNATURE.length) {
+ if (messageContents[i] != SIGNATURE[i]) {
+ throw new NTLMEngineException(
+ "NTLM message expected - instead got unrecognized bytes");
+ }
+ i++;
+ }
+
+ // Check to be sure there's a type 2 message indicator next
+ final int type = readULong(SIGNATURE.length);
+ if (type != expectedType) {
+ throw new NTLMEngineException("NTLM type " + Integer.toString(expectedType)
+ + " message expected - instead got type " + Integer.toString(type));
+ }
+
+ currentOutputPosition = messageContents.length;
+ }
+
+ /**
+ * Get the length of the signature and flags, so calculations can adjust
+ * offsets accordingly.
+ */
+ protected int getPreambleLength() {
+ return SIGNATURE.length + 4;
+ }
+
+ /** Get the message length */
+ protected int getMessageLength() {
+ return currentOutputPosition;
+ }
+
+ /** Read a byte from a position within the message buffer */
+ protected byte readByte(final int position) throws NTLMEngineException {
+ if (messageContents.length < position + 1) {
+ throw new NTLMEngineException("NTLM: Message too short");
+ }
+ return messageContents[position];
+ }
+
+ /** Read a bunch of bytes from a position in the message buffer */
+ protected void readBytes(final byte[] buffer, final int position) throws NTLMEngineException {
+ if (messageContents.length < position + buffer.length) {
+ throw new NTLMEngineException("NTLM: Message too short");
+ }
+ System.arraycopy(messageContents, position, buffer, 0, buffer.length);
+ }
+
+ /** Read a ushort from a position within the message buffer */
+ protected int readUShort(final int position) throws NTLMEngineException {
+ return NTLMEngineImpl.readUShort(messageContents, position);
+ }
+
+ /** Read a ulong from a position within the message buffer */
+ protected int readULong(final int position) throws NTLMEngineException {
+ return NTLMEngineImpl.readULong(messageContents, position);
+ }
+
+ /** Read a security buffer from a position within the message buffer */
+ protected byte[] readSecurityBuffer(final int position) throws NTLMEngineException {
+ return NTLMEngineImpl.readSecurityBuffer(messageContents, position);
+ }
+
+ /**
+ * Prepares the object to create a response of the given length.
+ *
+ * @param maxlength
+ * the maximum length of the response to prepare, not
+ * including the type and the signature (which this method
+ * adds).
+ */
+ protected void prepareResponse(final int maxlength, final int messageType) {
+ messageContents = new byte[maxlength];
+ currentOutputPosition = 0;
+ addBytes(SIGNATURE);
+ addULong(messageType);
+ }
+
+ /**
+ * Adds the given byte to the response.
+ *
+ * @param b
+ * the byte to add.
+ */
+ protected void addByte(final byte b) {
+ messageContents[currentOutputPosition] = b;
+ currentOutputPosition++;
+ }
+
+ /**
+ * Adds the given bytes to the response.
+ *
+ * @param bytes
+ * the bytes to add.
+ */
+ protected void addBytes(final byte[] bytes) {
+ if (bytes == null) {
+ return;
+ }
+ for (final byte b : bytes) {
+ messageContents[currentOutputPosition] = b;
+ currentOutputPosition++;
+ }
+ }
+
+ /** Adds a USHORT to the response */
+ protected void addUShort(final int value) {
+ addByte((byte) (value & 0xff));
+ addByte((byte) (value >> 8 & 0xff));
+ }
+
+ /** Adds a ULong to the response */
+ protected void addULong(final int value) {
+ addByte((byte) (value & 0xff));
+ addByte((byte) (value >> 8 & 0xff));
+ addByte((byte) (value >> 16 & 0xff));
+ addByte((byte) (value >> 24 & 0xff));
+ }
+
+ /**
+ * Returns the response that has been generated after shrinking the
+ * array if required and base64 encodes the response.
+ *
+ * @return The response as above.
+ */
+ String getResponse() {
+ final byte[] resp;
+ if (messageContents.length > currentOutputPosition) {
+ final byte[] tmp = new byte[currentOutputPosition];
+ System.arraycopy(messageContents, 0, tmp, 0, currentOutputPosition);
+ resp = tmp;
+ } else {
+ resp = messageContents;
+ }
+ return EncodingUtils.getAsciiString(Base64.encodeBase64(resp));
+ }
+
+ }
+
+ /** Type 1 message assembly class */
+ static class Type1Message extends NTLMMessage {
+ protected byte[] hostBytes;
+ protected byte[] domainBytes;
+
+ /** Constructor. Include the arguments the message will need */
+ Type1Message(final String domain, final String host) throws NTLMEngineException {
+ super();
+ try {
+ // Strip off domain name from the host!
+ final String unqualifiedHost = convertHost(host);
+ // Use only the base domain name!
+ final String unqualifiedDomain = convertDomain(domain);
+
+ hostBytes = unqualifiedHost != null? unqualifiedHost.getBytes("ASCII") : null;
+ domainBytes = unqualifiedDomain != null ? unqualifiedDomain
+ .toUpperCase(Locale.ENGLISH).getBytes("ASCII") : null;
+ } catch (final UnsupportedEncodingException e) {
+ throw new NTLMEngineException("Unicode unsupported: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Getting the response involves building the message before returning
+ * it
+ */
+ @Override
+ String getResponse() {
+ // Now, build the message. Calculate its length first, including
+ // signature or type.
+ final int finalLength = 32 + 8 /*+ hostBytes.length + domainBytes.length */;
+
+ // Set up the response. This will initialize the signature, message
+ // type, and flags.
+ prepareResponse(finalLength, 1);
+
+ // Flags. These are the complete set of flags we support.
+ addULong(
+ //FLAG_WORKSTATION_PRESENT |
+ //FLAG_DOMAIN_PRESENT |
+
+ // Required flags
+ //FLAG_REQUEST_LAN_MANAGER_KEY |
+ FLAG_REQUEST_NTLMv1 |
+ FLAG_REQUEST_NTLM2_SESSION |
+
+ // Protocol version request
+ FLAG_REQUEST_VERSION |
+
+ // Recommended privacy settings
+ FLAG_REQUEST_ALWAYS_SIGN |
+ //FLAG_REQUEST_SEAL |
+ //FLAG_REQUEST_SIGN |
+
+ // These must be set according to documentation, based on use of SEAL above
+ FLAG_REQUEST_128BIT_KEY_EXCH |
+ FLAG_REQUEST_56BIT_ENCRYPTION |
+ //FLAG_REQUEST_EXPLICIT_KEY_EXCH |
+
+ FLAG_REQUEST_UNICODE_ENCODING);
+
+ // Domain length (two times).
+ addUShort(/*domainBytes.length*/0);
+ addUShort(/*domainBytes.length*/0);
+
+ // Domain offset.
+ addULong(/*hostBytes.length +*/ 32 + 8);
+
+ // Host length (two times).
+ addUShort(/*hostBytes.length*/0);
+ addUShort(/*hostBytes.length*/0);
+
+ // Host offset (always 32 + 8).
+ addULong(32 + 8);
+
+ // Version
+ addUShort(0x0105);
+ // Build
+ addULong(2600);
+ // NTLM revision
+ addUShort(0x0f00);
+
+
+ // Host (workstation) String.
+ //addBytes(hostBytes);
+
+ // Domain String.
+ //addBytes(domainBytes);
+
+
+ return super.getResponse();
+ }
+
+ }
+
+ /** Type 2 message class */
+ static class Type2Message extends NTLMMessage {
+ protected byte[] challenge;
+ protected String target;
+ protected byte[] targetInfo;
+ protected int flags;
+
+ Type2Message(final String message) throws NTLMEngineException {
+ super(message, 2);
+
+ // Type 2 message is laid out as follows:
+ // First 8 bytes: NTLMSSP[0]
+ // Next 4 bytes: Ulong, value 2
+ // Next 8 bytes, starting at offset 12: target field (2 ushort lengths, 1 ulong offset)
+ // Next 4 bytes, starting at offset 20: Flags, e.g. 0x22890235
+ // Next 8 bytes, starting at offset 24: Challenge
+ // Next 8 bytes, starting at offset 32: ??? (8 bytes of zeros)
+ // Next 8 bytes, starting at offset 40: targetinfo field (2 ushort lengths, 1 ulong offset)
+ // Next 2 bytes, major/minor version number (e.g. 0x05 0x02)
+ // Next 8 bytes, build number
+ // Next 2 bytes, protocol version number (e.g. 0x00 0x0f)
+ // Next, various text fields, and a ushort of value 0 at the end
+
+ // Parse out the rest of the info we need from the message
+ // The nonce is the 8 bytes starting from the byte in position 24.
+ challenge = new byte[8];
+ readBytes(challenge, 24);
+
+ flags = readULong(20);
+
+ if ((flags & FLAG_REQUEST_UNICODE_ENCODING) == 0) {
+ throw new NTLMEngineException(
+ "NTLM type 2 message has flags that make no sense: "
+ + Integer.toString(flags));
+ }
+
+ // Do the target!
+ target = null;
+ // The TARGET_DESIRED flag is said to not have understood semantics
+ // in Type2 messages, so use the length of the packet to decide
+ // how to proceed instead
+ if (getMessageLength() >= 12 + 8) {
+ final byte[] bytes = readSecurityBuffer(12);
+ if (bytes.length != 0) {
+ try {
+ target = new String(bytes, "UnicodeLittleUnmarked");
+ } catch (final UnsupportedEncodingException e) {
+ throw new NTLMEngineException(e.getMessage(), e);
+ }
+ }
+ }
+
+ // Do the target info!
+ targetInfo = null;
+ // TARGET_DESIRED flag cannot be relied on, so use packet length
+ if (getMessageLength() >= 40 + 8) {
+ final byte[] bytes = readSecurityBuffer(40);
+ if (bytes.length != 0) {
+ targetInfo = bytes;
+ }
+ }
+ }
+
+ /** Retrieve the challenge */
+ byte[] getChallenge() {
+ return challenge;
+ }
+
+ /** Retrieve the target */
+ String getTarget() {
+ return target;
+ }
+
+ /** Retrieve the target info */
+ byte[] getTargetInfo() {
+ return targetInfo;
+ }
+
+ /** Retrieve the response flags */
+ int getFlags() {
+ return flags;
+ }
+
+ }
+
+ /** Type 3 message assembly class */
+ static class Type3Message extends NTLMMessage {
+ // Response flags from the type2 message
+ protected int type2Flags;
+
+ protected byte[] domainBytes;
+ protected byte[] hostBytes;
+ protected byte[] userBytes;
+
+ protected byte[] lmResp;
+ protected byte[] ntResp;
+ protected byte[] sessionKey;
+
+
+ /** Constructor. Pass the arguments we will need */
+ Type3Message(final String domain, final String host, final String user, final String password, final byte[] nonce,
+ final int type2Flags, final String target, final byte[] targetInformation)
+ throws NTLMEngineException {
+ // Save the flags
+ this.type2Flags = type2Flags;
+
+ // Strip off domain name from the host!
+ final String unqualifiedHost = convertHost(host);
+ // Use only the base domain name!
+ final String unqualifiedDomain = convertDomain(domain);
+
+ // Create a cipher generator class. Use domain BEFORE it gets modified!
+ final CipherGen gen = new CipherGen(unqualifiedDomain, user, password, nonce, target, targetInformation);
+
+ // Use the new code to calculate the responses, including v2 if that
+ // seems warranted.
+ byte[] userSessionKey;
+ try {
+ // This conditional may not work on Windows Server 2008 R2 and above, where it has not yet
+ // been tested
+ if (((type2Flags & FLAG_TARGETINFO_PRESENT) != 0) &&
+ targetInformation != null && target != null) {
+ // NTLMv2
+ ntResp = gen.getNTLMv2Response();
+ lmResp = gen.getLMv2Response();
+ if ((type2Flags & FLAG_REQUEST_LAN_MANAGER_KEY) != 0) {
+ userSessionKey = gen.getLanManagerSessionKey();
+ } else {
+ userSessionKey = gen.getNTLMv2UserSessionKey();
+ }
+ } else {
+ // NTLMv1
+ if ((type2Flags & FLAG_REQUEST_NTLM2_SESSION) != 0) {
+ // NTLM2 session stuff is requested
+ ntResp = gen.getNTLM2SessionResponse();
+ lmResp = gen.getLM2SessionResponse();
+ if ((type2Flags & FLAG_REQUEST_LAN_MANAGER_KEY) != 0) {
+ userSessionKey = gen.getLanManagerSessionKey();
+ } else {
+ userSessionKey = gen.getNTLM2SessionResponseUserSessionKey();
+ }
+ } else {
+ ntResp = gen.getNTLMResponse();
+ lmResp = gen.getLMResponse();
+ if ((type2Flags & FLAG_REQUEST_LAN_MANAGER_KEY) != 0) {
+ userSessionKey = gen.getLanManagerSessionKey();
+ } else {
+ userSessionKey = gen.getNTLMUserSessionKey();
+ }
+ }
+ }
+ } catch (final NTLMEngineException e) {
+ // This likely means we couldn't find the MD4 hash algorithm -
+ // fail back to just using LM
+ ntResp = new byte[0];
+ lmResp = gen.getLMResponse();
+ if ((type2Flags & FLAG_REQUEST_LAN_MANAGER_KEY) != 0) {
+ userSessionKey = gen.getLanManagerSessionKey();
+ } else {
+ userSessionKey = gen.getLMUserSessionKey();
+ }
+ }
+
+ if ((type2Flags & FLAG_REQUEST_SIGN) != 0) {
+ if ((type2Flags & FLAG_REQUEST_EXPLICIT_KEY_EXCH) != 0) {
+ sessionKey = RC4(gen.getSecondaryKey(), userSessionKey);
+ } else {
+ sessionKey = userSessionKey;
+ }
+ } else {
+ sessionKey = null;
+ }
+
+ try {
+ hostBytes = unqualifiedHost != null ? unqualifiedHost
+ .getBytes("UnicodeLittleUnmarked") : null;
+ domainBytes = unqualifiedDomain != null ? unqualifiedDomain
+ .toUpperCase(Locale.ENGLISH).getBytes("UnicodeLittleUnmarked") : null;
+ userBytes = user.getBytes("UnicodeLittleUnmarked");
+ } catch (final UnsupportedEncodingException e) {
+ throw new NTLMEngineException("Unicode not supported: " + e.getMessage(), e);
+ }
+ }
+
+ /** Assemble the response */
+ @Override
+ String getResponse() {
+ final int ntRespLen = ntResp.length;
+ final int lmRespLen = lmResp.length;
+
+ final int domainLen = domainBytes != null ? domainBytes.length : 0;
+ final int hostLen = hostBytes != null ? hostBytes.length: 0;
+ final int userLen = userBytes.length;
+ final int sessionKeyLen;
+ if (sessionKey != null) {
+ sessionKeyLen = sessionKey.length;
+ } else {
+ sessionKeyLen = 0;
+ }
+
+ // Calculate the layout within the packet
+ final int lmRespOffset = 72; // allocate space for the version
+ final int ntRespOffset = lmRespOffset + lmRespLen;
+ final int domainOffset = ntRespOffset + ntRespLen;
+ final int userOffset = domainOffset + domainLen;
+ final int hostOffset = userOffset + userLen;
+ final int sessionKeyOffset = hostOffset + hostLen;
+ final int finalLength = sessionKeyOffset + sessionKeyLen;
+
+ // Start the response. Length includes signature and type
+ prepareResponse(finalLength, 3);
+
+ // LM Resp Length (twice)
+ addUShort(lmRespLen);
+ addUShort(lmRespLen);
+
+ // LM Resp Offset
+ addULong(lmRespOffset);
+
+ // NT Resp Length (twice)
+ addUShort(ntRespLen);
+ addUShort(ntRespLen);
+
+ // NT Resp Offset
+ addULong(ntRespOffset);
+
+ // Domain length (twice)
+ addUShort(domainLen);
+ addUShort(domainLen);
+
+ // Domain offset.
+ addULong(domainOffset);
+
+ // User Length (twice)
+ addUShort(userLen);
+ addUShort(userLen);
+
+ // User offset
+ addULong(userOffset);
+
+ // Host length (twice)
+ addUShort(hostLen);
+ addUShort(hostLen);
+
+ // Host offset
+ addULong(hostOffset);
+
+ // Session key length (twice)
+ addUShort(sessionKeyLen);
+ addUShort(sessionKeyLen);
+
+ // Session key offset
+ addULong(sessionKeyOffset);
+
+ // Flags.
+ addULong(
+ //FLAG_WORKSTATION_PRESENT |
+ //FLAG_DOMAIN_PRESENT |
+
+ // Required flags
+ (type2Flags & FLAG_REQUEST_LAN_MANAGER_KEY) |
+ (type2Flags & FLAG_REQUEST_NTLMv1) |
+ (type2Flags & FLAG_REQUEST_NTLM2_SESSION) |
+
+ // Protocol version request
+ FLAG_REQUEST_VERSION |
+
+ // Recommended privacy settings
+ (type2Flags & FLAG_REQUEST_ALWAYS_SIGN) |
+ (type2Flags & FLAG_REQUEST_SEAL) |
+ (type2Flags & FLAG_REQUEST_SIGN) |
+
+ // These must be set according to documentation, based on use of SEAL above
+ (type2Flags & FLAG_REQUEST_128BIT_KEY_EXCH) |
+ (type2Flags & FLAG_REQUEST_56BIT_ENCRYPTION) |
+ (type2Flags & FLAG_REQUEST_EXPLICIT_KEY_EXCH) |
+
+ (type2Flags & FLAG_TARGETINFO_PRESENT) |
+ (type2Flags & FLAG_REQUEST_UNICODE_ENCODING) |
+ (type2Flags & FLAG_REQUEST_TARGET)
+ );
+
+ // Version
+ addUShort(0x0105);
+ // Build
+ addULong(2600);
+ // NTLM revision
+ addUShort(0x0f00);
+
+ // Add the actual data
+ addBytes(lmResp);
+ addBytes(ntResp);
+ addBytes(domainBytes);
+ addBytes(userBytes);
+ addBytes(hostBytes);
+ if (sessionKey != null) {
+ addBytes(sessionKey);
+ }
+
+ return super.getResponse();
+ }
+ }
+
+ static void writeULong(final byte[] buffer, final int value, final int offset) {
+ buffer[offset] = (byte) (value & 0xff);
+ buffer[offset + 1] = (byte) (value >> 8 & 0xff);
+ buffer[offset + 2] = (byte) (value >> 16 & 0xff);
+ buffer[offset + 3] = (byte) (value >> 24 & 0xff);
+ }
+
+ static int F(final int x, final int y, final int z) {
+ return ((x & y) | (~x & z));
+ }
+
+ static int G(final int x, final int y, final int z) {
+ return ((x & y) | (x & z) | (y & z));
+ }
+
+ static int H(final int x, final int y, final int z) {
+ return (x ^ y ^ z);
+ }
+
+ static int rotintlft(final int val, final int numbits) {
+ return ((val << numbits) | (val >>> (32 - numbits)));
+ }
+
+ /**
+ * Cryptography support - MD4. The following class was based loosely on the
+ * RFC and on code found at http://www.cs.umd.edu/~harry/jotp/src/md.java.
+ * Code correctness was verified by looking at MD4.java from the jcifs
+ * library (http://jcifs.samba.org). It was massaged extensively to the
+ * final form found here by Karl Wright (kwright@metacarta.com).
+ */
+ static class MD4 {
+ protected int A = 0x67452301;
+ protected int B = 0xefcdab89;
+ protected int C = 0x98badcfe;
+ protected int D = 0x10325476;
+ protected long count = 0L;
+ protected byte[] dataBuffer = new byte[64];
+
+ MD4() {
+ }
+
+ void update(final byte[] input) {
+ // We always deal with 512 bits at a time. Correspondingly, there is
+ // a buffer 64 bytes long that we write data into until it gets
+ // full.
+ int curBufferPos = (int) (count & 63L);
+ int inputIndex = 0;
+ while (input.length - inputIndex + curBufferPos >= dataBuffer.length) {
+ // We have enough data to do the next step. Do a partial copy
+ // and a transform, updating inputIndex and curBufferPos
+ // accordingly
+ final int transferAmt = dataBuffer.length - curBufferPos;
+ System.arraycopy(input, inputIndex, dataBuffer, curBufferPos, transferAmt);
+ count += transferAmt;
+ curBufferPos = 0;
+ inputIndex += transferAmt;
+ processBuffer();
+ }
+
+ // If there's anything left, copy it into the buffer and leave it.
+ // We know there's not enough left to process.
+ if (inputIndex < input.length) {
+ final int transferAmt = input.length - inputIndex;
+ System.arraycopy(input, inputIndex, dataBuffer, curBufferPos, transferAmt);
+ count += transferAmt;
+ curBufferPos += transferAmt;
+ }
+ }
+
+ byte[] getOutput() {
+ // Feed pad/length data into engine. This must round out the input
+ // to a multiple of 512 bits.
+ final int bufferIndex = (int) (count & 63L);
+ final int padLen = (bufferIndex < 56) ? (56 - bufferIndex) : (120 - bufferIndex);
+ final byte[] postBytes = new byte[padLen + 8];
+ // Leading 0x80, specified amount of zero padding, then length in
+ // bits.
+ postBytes[0] = (byte) 0x80;
+ // Fill out the last 8 bytes with the length
+ for (int i = 0; i < 8; i++) {
+ postBytes[padLen + i] = (byte) ((count * 8) >>> (8 * i));
+ }
+
+ // Update the engine
+ update(postBytes);
+
+ // Calculate final result
+ final byte[] result = new byte[16];
+ writeULong(result, A, 0);
+ writeULong(result, B, 4);
+ writeULong(result, C, 8);
+ writeULong(result, D, 12);
+ return result;
+ }
+
+ protected void processBuffer() {
+ // Convert current buffer to 16 ulongs
+ final int[] d = new int[16];
+
+ for (int i = 0; i < 16; i++) {
+ d[i] = (dataBuffer[i * 4] & 0xff) + ((dataBuffer[i * 4 + 1] & 0xff) << 8)
+ + ((dataBuffer[i * 4 + 2] & 0xff) << 16)
+ + ((dataBuffer[i * 4 + 3] & 0xff) << 24);
+ }
+
+ // Do a round of processing
+ final int AA = A;
+ final int BB = B;
+ final int CC = C;
+ final int DD = D;
+ round1(d);
+ round2(d);
+ round3(d);
+ A += AA;
+ B += BB;
+ C += CC;
+ D += DD;
+
+ }
+
+ protected void round1(final int[] d) {
+ A = rotintlft((A + F(B, C, D) + d[0]), 3);
+ D = rotintlft((D + F(A, B, C) + d[1]), 7);
+ C = rotintlft((C + F(D, A, B) + d[2]), 11);
+ B = rotintlft((B + F(C, D, A) + d[3]), 19);
+
+ A = rotintlft((A + F(B, C, D) + d[4]), 3);
+ D = rotintlft((D + F(A, B, C) + d[5]), 7);
+ C = rotintlft((C + F(D, A, B) + d[6]), 11);
+ B = rotintlft((B + F(C, D, A) + d[7]), 19);
+
+ A = rotintlft((A + F(B, C, D) + d[8]), 3);
+ D = rotintlft((D + F(A, B, C) + d[9]), 7);
+ C = rotintlft((C + F(D, A, B) + d[10]), 11);
+ B = rotintlft((B + F(C, D, A) + d[11]), 19);
+
+ A = rotintlft((A + F(B, C, D) + d[12]), 3);
+ D = rotintlft((D + F(A, B, C) + d[13]), 7);
+ C = rotintlft((C + F(D, A, B) + d[14]), 11);
+ B = rotintlft((B + F(C, D, A) + d[15]), 19);
+ }
+
+ protected void round2(final int[] d) {
+ A = rotintlft((A + G(B, C, D) + d[0] + 0x5a827999), 3);
+ D = rotintlft((D + G(A, B, C) + d[4] + 0x5a827999), 5);
+ C = rotintlft((C + G(D, A, B) + d[8] + 0x5a827999), 9);
+ B = rotintlft((B + G(C, D, A) + d[12] + 0x5a827999), 13);
+
+ A = rotintlft((A + G(B, C, D) + d[1] + 0x5a827999), 3);
+ D = rotintlft((D + G(A, B, C) + d[5] + 0x5a827999), 5);
+ C = rotintlft((C + G(D, A, B) + d[9] + 0x5a827999), 9);
+ B = rotintlft((B + G(C, D, A) + d[13] + 0x5a827999), 13);
+
+ A = rotintlft((A + G(B, C, D) + d[2] + 0x5a827999), 3);
+ D = rotintlft((D + G(A, B, C) + d[6] + 0x5a827999), 5);
+ C = rotintlft((C + G(D, A, B) + d[10] + 0x5a827999), 9);
+ B = rotintlft((B + G(C, D, A) + d[14] + 0x5a827999), 13);
+
+ A = rotintlft((A + G(B, C, D) + d[3] + 0x5a827999), 3);
+ D = rotintlft((D + G(A, B, C) + d[7] + 0x5a827999), 5);
+ C = rotintlft((C + G(D, A, B) + d[11] + 0x5a827999), 9);
+ B = rotintlft((B + G(C, D, A) + d[15] + 0x5a827999), 13);
+
+ }
+
+ protected void round3(final int[] d) {
+ A = rotintlft((A + H(B, C, D) + d[0] + 0x6ed9eba1), 3);
+ D = rotintlft((D + H(A, B, C) + d[8] + 0x6ed9eba1), 9);
+ C = rotintlft((C + H(D, A, B) + d[4] + 0x6ed9eba1), 11);
+ B = rotintlft((B + H(C, D, A) + d[12] + 0x6ed9eba1), 15);
+
+ A = rotintlft((A + H(B, C, D) + d[2] + 0x6ed9eba1), 3);
+ D = rotintlft((D + H(A, B, C) + d[10] + 0x6ed9eba1), 9);
+ C = rotintlft((C + H(D, A, B) + d[6] + 0x6ed9eba1), 11);
+ B = rotintlft((B + H(C, D, A) + d[14] + 0x6ed9eba1), 15);
+
+ A = rotintlft((A + H(B, C, D) + d[1] + 0x6ed9eba1), 3);
+ D = rotintlft((D + H(A, B, C) + d[9] + 0x6ed9eba1), 9);
+ C = rotintlft((C + H(D, A, B) + d[5] + 0x6ed9eba1), 11);
+ B = rotintlft((B + H(C, D, A) + d[13] + 0x6ed9eba1), 15);
+
+ A = rotintlft((A + H(B, C, D) + d[3] + 0x6ed9eba1), 3);
+ D = rotintlft((D + H(A, B, C) + d[11] + 0x6ed9eba1), 9);
+ C = rotintlft((C + H(D, A, B) + d[7] + 0x6ed9eba1), 11);
+ B = rotintlft((B + H(C, D, A) + d[15] + 0x6ed9eba1), 15);
+
+ }
+
+ }
+
+ /**
+ * Cryptography support - HMACMD5 - algorithmically based on various web
+ * resources by Karl Wright
+ */
+ static class HMACMD5 {
+ protected byte[] ipad;
+ protected byte[] opad;
+ protected MessageDigest md5;
+
+ HMACMD5(final byte[] input) throws NTLMEngineException {
+ byte[] key = input;
+ try {
+ md5 = MessageDigest.getInstance("MD5");
+ } catch (final Exception ex) {
+ // Umm, the algorithm doesn't exist - throw an
+ // NTLMEngineException!
+ throw new NTLMEngineException(
+ "Error getting md5 message digest implementation: " + ex.getMessage(), ex);
+ }
+
+ // Initialize the pad buffers with the key
+ ipad = new byte[64];
+ opad = new byte[64];
+
+ int keyLength = key.length;
+ if (keyLength > 64) {
+ // Use MD5 of the key instead, as described in RFC 2104
+ md5.update(key);
+ key = md5.digest();
+ keyLength = key.length;
+ }
+ int i = 0;
+ while (i < keyLength) {
+ ipad[i] = (byte) (key[i] ^ (byte) 0x36);
+ opad[i] = (byte) (key[i] ^ (byte) 0x5c);
+ i++;
+ }
+ while (i < 64) {
+ ipad[i] = (byte) 0x36;
+ opad[i] = (byte) 0x5c;
+ i++;
+ }
+
+ // Very important: update the digest with the ipad buffer
+ md5.reset();
+ md5.update(ipad);
+
+ }
+
+ /** Grab the current digest. This is the "answer". */
+ byte[] getOutput() {
+ final byte[] digest = md5.digest();
+ md5.update(opad);
+ return md5.digest(digest);
+ }
+
+ /** Update by adding a complete array */
+ void update(final byte[] input) {
+ md5.update(input);
+ }
+
+ /** Update the algorithm */
+ void update(final byte[] input, final int offset, final int length) {
+ md5.update(input, offset, length);
+ }
+
+ }
+
+ public String generateType1Msg(
+ final String domain,
+ final String workstation) throws NTLMEngineException {
+ return getType1Message(workstation, domain);
+ }
+
+ public String generateType3Msg(
+ final String username,
+ final String password,
+ final String domain,
+ final String workstation,
+ final String challenge) throws NTLMEngineException {
+ final Type2Message t2m = new Type2Message(challenge);
+ return getType3Message(
+ username,
+ password,
+ workstation,
+ domain,
+ t2m.getChallenge(),
+ t2m.getFlags(),
+ t2m.getTarget(),
+ t2m.getTargetInfo());
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/NTLMScheme.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/NTLMScheme.java
new file mode 100644
index 0000000000..68873954b8
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/NTLMScheme.java
@@ -0,0 +1,164 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.auth;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.auth.AUTH;
+import ch.boye.httpclientandroidlib.auth.AuthenticationException;
+import ch.boye.httpclientandroidlib.auth.Credentials;
+import ch.boye.httpclientandroidlib.auth.InvalidCredentialsException;
+import ch.boye.httpclientandroidlib.auth.MalformedChallengeException;
+import ch.boye.httpclientandroidlib.auth.NTCredentials;
+import ch.boye.httpclientandroidlib.message.BufferedHeader;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * NTLM is a proprietary authentication scheme developed by Microsoft
+ * and optimized for Windows platforms.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class NTLMScheme extends AuthSchemeBase {
+
+ enum State {
+ UNINITIATED,
+ CHALLENGE_RECEIVED,
+ MSG_TYPE1_GENERATED,
+ MSG_TYPE2_RECEVIED,
+ MSG_TYPE3_GENERATED,
+ FAILED,
+ }
+
+ private final NTLMEngine engine;
+
+ private State state;
+ private String challenge;
+
+ public NTLMScheme(final NTLMEngine engine) {
+ super();
+ Args.notNull(engine, "NTLM engine");
+ this.engine = engine;
+ this.state = State.UNINITIATED;
+ this.challenge = null;
+ }
+
+ /**
+ * @since 4.3
+ */
+ public NTLMScheme() {
+ this(new NTLMEngineImpl());
+ }
+
+ public String getSchemeName() {
+ return "ntlm";
+ }
+
+ public String getParameter(final String name) {
+ // String parameters not supported
+ return null;
+ }
+
+ public String getRealm() {
+ // NTLM does not support the concept of an authentication realm
+ return null;
+ }
+
+ public boolean isConnectionBased() {
+ return true;
+ }
+
+ @Override
+ protected void parseChallenge(
+ final CharArrayBuffer buffer,
+ final int beginIndex, final int endIndex) throws MalformedChallengeException {
+ this.challenge = buffer.substringTrimmed(beginIndex, endIndex);
+ if (this.challenge.length() == 0) {
+ if (this.state == State.UNINITIATED) {
+ this.state = State.CHALLENGE_RECEIVED;
+ } else {
+ this.state = State.FAILED;
+ }
+ } else {
+ if (this.state.compareTo(State.MSG_TYPE1_GENERATED) < 0) {
+ this.state = State.FAILED;
+ throw new MalformedChallengeException("Out of sequence NTLM response message");
+ } else if (this.state == State.MSG_TYPE1_GENERATED) {
+ this.state = State.MSG_TYPE2_RECEVIED;
+ }
+ }
+ }
+
+ public Header authenticate(
+ final Credentials credentials,
+ final HttpRequest request) throws AuthenticationException {
+ NTCredentials ntcredentials = null;
+ try {
+ ntcredentials = (NTCredentials) credentials;
+ } catch (final ClassCastException e) {
+ throw new InvalidCredentialsException(
+ "Credentials cannot be used for NTLM authentication: "
+ + credentials.getClass().getName());
+ }
+ String response = null;
+ if (this.state == State.FAILED) {
+ throw new AuthenticationException("NTLM authentication failed");
+ } else if (this.state == State.CHALLENGE_RECEIVED) {
+ response = this.engine.generateType1Msg(
+ ntcredentials.getDomain(),
+ ntcredentials.getWorkstation());
+ this.state = State.MSG_TYPE1_GENERATED;
+ } else if (this.state == State.MSG_TYPE2_RECEVIED) {
+ response = this.engine.generateType3Msg(
+ ntcredentials.getUserName(),
+ ntcredentials.getPassword(),
+ ntcredentials.getDomain(),
+ ntcredentials.getWorkstation(),
+ this.challenge);
+ this.state = State.MSG_TYPE3_GENERATED;
+ } else {
+ throw new AuthenticationException("Unexpected state: " + this.state);
+ }
+ final CharArrayBuffer buffer = new CharArrayBuffer(32);
+ if (isProxy()) {
+ buffer.append(AUTH.PROXY_AUTH_RESP);
+ } else {
+ buffer.append(AUTH.WWW_AUTH_RESP);
+ }
+ buffer.append(": NTLM ");
+ buffer.append(response);
+ return new BufferedHeader(buffer);
+ }
+
+ public boolean isComplete() {
+ return this.state == State.MSG_TYPE3_GENERATED || this.state == State.FAILED;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/NTLMSchemeFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/NTLMSchemeFactory.java
new file mode 100644
index 0000000000..0484df6432
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/NTLMSchemeFactory.java
@@ -0,0 +1,56 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.auth;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.auth.AuthScheme;
+import ch.boye.httpclientandroidlib.auth.AuthSchemeFactory;
+import ch.boye.httpclientandroidlib.auth.AuthSchemeProvider;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * {@link AuthSchemeProvider} implementation that creates and initializes
+ * {@link NTLMScheme} instances configured to use the default {@link NTLMEngine}
+ * implementation.
+ *
+ * @since 4.1
+ */
+@Immutable
+@SuppressWarnings("deprecation")
+public class NTLMSchemeFactory implements AuthSchemeFactory, AuthSchemeProvider {
+
+ public AuthScheme newInstance(final HttpParams params) {
+ return new NTLMScheme();
+ }
+
+ public AuthScheme create(final HttpContext context) {
+ return new NTLMScheme();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/RFC2617Scheme.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/RFC2617Scheme.java
new file mode 100644
index 0000000000..93d1bf843f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/RFC2617Scheme.java
@@ -0,0 +1,151 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.auth;
+
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import ch.boye.httpclientandroidlib.Consts;
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.auth.ChallengeState;
+import ch.boye.httpclientandroidlib.auth.MalformedChallengeException;
+import ch.boye.httpclientandroidlib.auth.params.AuthPNames;
+import ch.boye.httpclientandroidlib.message.BasicHeaderValueParser;
+import ch.boye.httpclientandroidlib.message.HeaderValueParser;
+import ch.boye.httpclientandroidlib.message.ParserCursor;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Abstract authentication scheme class that lays foundation for all
+ * RFC 2617 compliant authentication schemes and provides capabilities common
+ * to all authentication schemes defined in RFC 2617.
+ *
+ * @since 4.0
+ */
+@SuppressWarnings("deprecation")
+@NotThreadSafe // AuthSchemeBase, params
+public abstract class RFC2617Scheme extends AuthSchemeBase {
+
+ private final Map<String, String> params;
+ private final Charset credentialsCharset;
+
+ /**
+ * Creates an instance of <tt>RFC2617Scheme</tt> with the given challenge
+ * state.
+ *
+ * @since 4.2
+ *
+ * @deprecated (4.3) do not use.
+ */
+ @Deprecated
+ public RFC2617Scheme(final ChallengeState challengeState) {
+ super(challengeState);
+ this.params = new HashMap<String, String>();
+ this.credentialsCharset = Consts.ASCII;
+ }
+
+ /**
+ * @since 4.3
+ */
+ public RFC2617Scheme(final Charset credentialsCharset) {
+ super();
+ this.params = new HashMap<String, String>();
+ this.credentialsCharset = credentialsCharset != null ? credentialsCharset : Consts.ASCII;
+ }
+
+ public RFC2617Scheme() {
+ this(Consts.ASCII);
+ }
+
+
+ /**
+ * @since 4.3
+ */
+ public Charset getCredentialsCharset() {
+ return credentialsCharset;
+ }
+
+ String getCredentialsCharset(final HttpRequest request) {
+ String charset = (String) request.getParams().getParameter(AuthPNames.CREDENTIAL_CHARSET);
+ if (charset == null) {
+ charset = getCredentialsCharset().name();
+ }
+ return charset;
+ }
+
+ @Override
+ protected void parseChallenge(
+ final CharArrayBuffer buffer, final int pos, final int len) throws MalformedChallengeException {
+ final HeaderValueParser parser = BasicHeaderValueParser.INSTANCE;
+ final ParserCursor cursor = new ParserCursor(pos, buffer.length());
+ final HeaderElement[] elements = parser.parseElements(buffer, cursor);
+ if (elements.length == 0) {
+ throw new MalformedChallengeException("Authentication challenge is empty");
+ }
+ this.params.clear();
+ for (final HeaderElement element : elements) {
+ this.params.put(element.getName().toLowerCase(Locale.ENGLISH), element.getValue());
+ }
+ }
+
+ /**
+ * Returns authentication parameters map. Keys in the map are lower-cased.
+ *
+ * @return the map of authentication parameters
+ */
+ protected Map<String, String> getParameters() {
+ return this.params;
+ }
+
+ /**
+ * Returns authentication parameter with the given name, if available.
+ *
+ * @param name The name of the parameter to be returned
+ *
+ * @return the parameter with the given name
+ */
+ public String getParameter(final String name) {
+ if (name == null) {
+ return null;
+ }
+ return this.params.get(name.toLowerCase(Locale.ENGLISH));
+ }
+
+ /**
+ * Returns authentication realm. The realm may not be null.
+ *
+ * @return the authentication realm
+ */
+ public String getRealm() {
+ return getParameter("realm");
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/SpnegoTokenGenerator.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/SpnegoTokenGenerator.java
new file mode 100644
index 0000000000..5ab0348ad6
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/SpnegoTokenGenerator.java
@@ -0,0 +1,47 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.auth;
+
+import java.io.IOException;
+
+/**
+ * Abstract SPNEGO token generator. Implementations should take an Kerberos ticket and transform
+ * into a SPNEGO token.
+ * <p>
+ * Implementations of this interface are expected to be thread-safe.
+ *
+ * @since 4.1
+ *
+ * @deprecated (4.2) subclass {@link KerberosScheme} and override
+ * {@link KerberosScheme#generateGSSToken(byte[], org.ietf.jgss.Oid, String)}
+ */
+@Deprecated
+public interface SpnegoTokenGenerator {
+
+ byte [] generateSpnegoDERObject(byte [] kerberosTicket) throws IOException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/UnsupportedDigestAlgorithmException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/UnsupportedDigestAlgorithmException.java
new file mode 100644
index 0000000000..71a6f892f0
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/UnsupportedDigestAlgorithmException.java
@@ -0,0 +1,69 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.auth;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Authentication credentials required to respond to a authentication
+ * challenge are invalid
+ *
+ *
+ * @since 4.0
+ */
+@Immutable
+public class UnsupportedDigestAlgorithmException extends RuntimeException {
+
+ private static final long serialVersionUID = 319558534317118022L;
+
+ /**
+ * Creates a new UnsupportedAuthAlgoritmException with a <tt>null</tt> detail message.
+ */
+ public UnsupportedDigestAlgorithmException() {
+ super();
+ }
+
+ /**
+ * Creates a new UnsupportedAuthAlgoritmException with the specified message.
+ *
+ * @param message the exception detail message
+ */
+ public UnsupportedDigestAlgorithmException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new UnsupportedAuthAlgoritmException with the specified detail message and cause.
+ *
+ * @param message the exception detail message
+ * @param cause the <tt>Throwable</tt> that caused this exception, or <tt>null</tt>
+ * if the cause is unavailable, unknown, or not a <tt>Throwable</tt>
+ */
+ public UnsupportedDigestAlgorithmException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/package-info.java
new file mode 100644
index 0000000000..9e35c852d9
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Default implementations of standard and common HTTP authentication
+ * schemes.
+ */
+package ch.boye.httpclientandroidlib.impl.auth;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AIMDBackoffManager.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AIMDBackoffManager.java
new file mode 100644
index 0000000000..67798a6838
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AIMDBackoffManager.java
@@ -0,0 +1,164 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import ch.boye.httpclientandroidlib.client.BackoffManager;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.pool.ConnPoolControl;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * <p>The <code>AIMDBackoffManager</code> applies an additive increase,
+ * multiplicative decrease (AIMD) to managing a dynamic limit to
+ * the number of connections allowed to a given host. You may want
+ * to experiment with the settings for the cooldown periods and the
+ * backoff factor to get the adaptive behavior you want.</p>
+ *
+ * <p>Generally speaking, shorter cooldowns will lead to more steady-state
+ * variability but faster reaction times, while longer cooldowns
+ * will lead to more stable equilibrium behavior but slower reaction
+ * times.</p>
+ *
+ * <p>Similarly, higher backoff factors promote greater
+ * utilization of available capacity at the expense of fairness
+ * among clients. Lower backoff factors allow equal distribution of
+ * capacity among clients (fairness) to happen faster, at the
+ * expense of having more server capacity unused in the short term.</p>
+ *
+ * @since 4.2
+ */
+public class AIMDBackoffManager implements BackoffManager {
+
+ private final ConnPoolControl<HttpRoute> connPerRoute;
+ private final Clock clock;
+ private final Map<HttpRoute,Long> lastRouteProbes;
+ private final Map<HttpRoute,Long> lastRouteBackoffs;
+ private long coolDown = 5 * 1000L;
+ private double backoffFactor = 0.5;
+ private int cap = 2; // Per RFC 2616 sec 8.1.4
+
+ /**
+ * Creates an <code>AIMDBackoffManager</code> to manage
+ * per-host connection pool sizes represented by the
+ * given {@link ConnPoolControl}.
+ * @param connPerRoute per-host routing maximums to
+ * be managed
+ */
+ public AIMDBackoffManager(final ConnPoolControl<HttpRoute> connPerRoute) {
+ this(connPerRoute, new SystemClock());
+ }
+
+ AIMDBackoffManager(final ConnPoolControl<HttpRoute> connPerRoute, final Clock clock) {
+ this.clock = clock;
+ this.connPerRoute = connPerRoute;
+ this.lastRouteProbes = new HashMap<HttpRoute,Long>();
+ this.lastRouteBackoffs = new HashMap<HttpRoute,Long>();
+ }
+
+ public void backOff(final HttpRoute route) {
+ synchronized(connPerRoute) {
+ final int curr = connPerRoute.getMaxPerRoute(route);
+ final Long lastUpdate = getLastUpdate(lastRouteBackoffs, route);
+ final long now = clock.getCurrentTime();
+ if (now - lastUpdate.longValue() < coolDown) {
+ return;
+ }
+ connPerRoute.setMaxPerRoute(route, getBackedOffPoolSize(curr));
+ lastRouteBackoffs.put(route, Long.valueOf(now));
+ }
+ }
+
+ private int getBackedOffPoolSize(final int curr) {
+ if (curr <= 1) {
+ return 1;
+ }
+ return (int)(Math.floor(backoffFactor * curr));
+ }
+
+ public void probe(final HttpRoute route) {
+ synchronized(connPerRoute) {
+ final int curr = connPerRoute.getMaxPerRoute(route);
+ final int max = (curr >= cap) ? cap : curr + 1;
+ final Long lastProbe = getLastUpdate(lastRouteProbes, route);
+ final Long lastBackoff = getLastUpdate(lastRouteBackoffs, route);
+ final long now = clock.getCurrentTime();
+ if (now - lastProbe.longValue() < coolDown || now - lastBackoff.longValue() < coolDown) {
+ return;
+ }
+ connPerRoute.setMaxPerRoute(route, max);
+ lastRouteProbes.put(route, Long.valueOf(now));
+ }
+ }
+
+ private Long getLastUpdate(final Map<HttpRoute,Long> updates, final HttpRoute route) {
+ Long lastUpdate = updates.get(route);
+ if (lastUpdate == null) {
+ lastUpdate = Long.valueOf(0L);
+ }
+ return lastUpdate;
+ }
+
+ /**
+ * Sets the factor to use when backing off; the new
+ * per-host limit will be roughly the current max times
+ * this factor. <code>Math.floor</code> is applied in the
+ * case of non-integer outcomes to ensure we actually
+ * decrease the pool size. Pool sizes are never decreased
+ * below 1, however. Defaults to 0.5.
+ * @param d must be between 0.0 and 1.0, exclusive.
+ */
+ public void setBackoffFactor(final double d) {
+ Args.check(d > 0.0 && d < 1.0, "Backoff factor must be 0.0 < f < 1.0");
+ backoffFactor = d;
+ }
+
+ /**
+ * Sets the amount of time, in milliseconds, to wait between
+ * adjustments in pool sizes for a given host, to allow
+ * enough time for the adjustments to take effect. Defaults
+ * to 5000L (5 seconds).
+ * @param l must be positive
+ */
+ public void setCooldownMillis(final long l) {
+ Args.positive(coolDown, "Cool down");
+ coolDown = l;
+ }
+
+ /**
+ * Sets the absolute maximum per-host connection pool size to
+ * probe up to; defaults to 2 (the default per-host max).
+ * @param cap must be >= 1
+ */
+ public void setPerHostConnectionCap(final int cap) {
+ Args.positive(cap, "Per host connection cap");
+ this.cap = cap;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AbstractAuthenticationHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AbstractAuthenticationHandler.java
new file mode 100644
index 0000000000..a02c1a03b7
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AbstractAuthenticationHandler.java
@@ -0,0 +1,189 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.FormattedHeader;
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.auth.AuthScheme;
+import ch.boye.httpclientandroidlib.auth.AuthSchemeRegistry;
+import ch.boye.httpclientandroidlib.auth.AuthenticationException;
+import ch.boye.httpclientandroidlib.auth.MalformedChallengeException;
+import ch.boye.httpclientandroidlib.client.AuthenticationHandler;
+import ch.boye.httpclientandroidlib.client.params.AuthPolicy;
+import ch.boye.httpclientandroidlib.client.protocol.ClientContext;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Asserts;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Base class for {@link AuthenticationHandler} implementations.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) use {@link ch.boye.httpclientandroidlib.client.AuthenticationStrategy}
+ */
+@Deprecated
+@Immutable
+public abstract class AbstractAuthenticationHandler implements AuthenticationHandler {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ private static final List<String> DEFAULT_SCHEME_PRIORITY =
+ Collections.unmodifiableList(Arrays.asList(new String[] {
+ AuthPolicy.SPNEGO,
+ AuthPolicy.NTLM,
+ AuthPolicy.DIGEST,
+ AuthPolicy.BASIC
+ }));
+
+ public AbstractAuthenticationHandler() {
+ super();
+ }
+
+ protected Map<String, Header> parseChallenges(
+ final Header[] headers) throws MalformedChallengeException {
+
+ final Map<String, Header> map = new HashMap<String, Header>(headers.length);
+ for (final Header header : headers) {
+ final CharArrayBuffer buffer;
+ int pos;
+ if (header instanceof FormattedHeader) {
+ buffer = ((FormattedHeader) header).getBuffer();
+ pos = ((FormattedHeader) header).getValuePos();
+ } else {
+ final String s = header.getValue();
+ if (s == null) {
+ throw new MalformedChallengeException("Header value is null");
+ }
+ buffer = new CharArrayBuffer(s.length());
+ buffer.append(s);
+ pos = 0;
+ }
+ while (pos < buffer.length() && HTTP.isWhitespace(buffer.charAt(pos))) {
+ pos++;
+ }
+ final int beginIndex = pos;
+ while (pos < buffer.length() && !HTTP.isWhitespace(buffer.charAt(pos))) {
+ pos++;
+ }
+ final int endIndex = pos;
+ final String s = buffer.substring(beginIndex, endIndex);
+ map.put(s.toLowerCase(Locale.ENGLISH), header);
+ }
+ return map;
+ }
+
+ /**
+ * Returns default list of auth scheme names in their order of preference.
+ *
+ * @return list of auth scheme names
+ */
+ protected List<String> getAuthPreferences() {
+ return DEFAULT_SCHEME_PRIORITY;
+ }
+
+ /**
+ * Returns default list of auth scheme names in their order of preference
+ * based on the HTTP response and the current execution context.
+ *
+ * @param response HTTP response.
+ * @param context HTTP execution context.
+ *
+ * @since 4.1
+ */
+ protected List<String> getAuthPreferences(
+ final HttpResponse response,
+ final HttpContext context) {
+ return getAuthPreferences();
+ }
+
+ public AuthScheme selectScheme(
+ final Map<String, Header> challenges,
+ final HttpResponse response,
+ final HttpContext context) throws AuthenticationException {
+
+ final AuthSchemeRegistry registry = (AuthSchemeRegistry) context.getAttribute(
+ ClientContext.AUTHSCHEME_REGISTRY);
+ Asserts.notNull(registry, "AuthScheme registry");
+ Collection<String> authPrefs = getAuthPreferences(response, context);
+ if (authPrefs == null) {
+ authPrefs = DEFAULT_SCHEME_PRIORITY;
+ }
+
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Authentication schemes in the order of preference: "
+ + authPrefs);
+ }
+
+ AuthScheme authScheme = null;
+ for (final String id: authPrefs) {
+ final Header challenge = challenges.get(id.toLowerCase(Locale.ENGLISH));
+
+ if (challenge != null) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug(id + " authentication scheme selected");
+ }
+ try {
+ authScheme = registry.getAuthScheme(id, response.getParams());
+ break;
+ } catch (final IllegalStateException e) {
+ if (this.log.isWarnEnabled()) {
+ this.log.warn("Authentication scheme " + id + " not supported");
+ // Try again
+ }
+ }
+ } else {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Challenge for " + id + " authentication scheme not available");
+ // Try again
+ }
+ }
+ }
+ if (authScheme == null) {
+ // If none selected, something is wrong
+ throw new AuthenticationException(
+ "Unable to respond to any of these challenges: "
+ + challenges);
+ }
+ return authScheme;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AbstractHttpClient.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AbstractHttpClient.java
new file mode 100644
index 0000000000..f862c98868
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AbstractHttpClient.java
@@ -0,0 +1,990 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.io.IOException;
+import java.lang.reflect.UndeclaredThrowableException;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.ConnectionReuseStrategy;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpRequestInterceptor;
+import ch.boye.httpclientandroidlib.HttpResponseInterceptor;
+import ch.boye.httpclientandroidlib.annotation.GuardedBy;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.auth.AuthSchemeRegistry;
+import ch.boye.httpclientandroidlib.client.AuthenticationHandler;
+import ch.boye.httpclientandroidlib.client.AuthenticationStrategy;
+import ch.boye.httpclientandroidlib.client.BackoffManager;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+import ch.boye.httpclientandroidlib.client.ConnectionBackoffStrategy;
+import ch.boye.httpclientandroidlib.client.CookieStore;
+import ch.boye.httpclientandroidlib.client.CredentialsProvider;
+import ch.boye.httpclientandroidlib.client.HttpRequestRetryHandler;
+import ch.boye.httpclientandroidlib.client.RedirectHandler;
+import ch.boye.httpclientandroidlib.client.RedirectStrategy;
+import ch.boye.httpclientandroidlib.client.RequestDirector;
+import ch.boye.httpclientandroidlib.client.UserTokenHandler;
+import ch.boye.httpclientandroidlib.client.config.RequestConfig;
+import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
+import ch.boye.httpclientandroidlib.client.params.AuthPolicy;
+import ch.boye.httpclientandroidlib.client.params.ClientPNames;
+import ch.boye.httpclientandroidlib.client.params.CookiePolicy;
+import ch.boye.httpclientandroidlib.client.params.HttpClientParamConfig;
+import ch.boye.httpclientandroidlib.client.protocol.ClientContext;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionManager;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionManagerFactory;
+import ch.boye.httpclientandroidlib.conn.ConnectionKeepAliveStrategy;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoutePlanner;
+import ch.boye.httpclientandroidlib.conn.scheme.SchemeRegistry;
+import ch.boye.httpclientandroidlib.cookie.CookieSpecRegistry;
+import ch.boye.httpclientandroidlib.impl.DefaultConnectionReuseStrategy;
+import ch.boye.httpclientandroidlib.impl.auth.BasicSchemeFactory;
+import ch.boye.httpclientandroidlib.impl.auth.DigestSchemeFactory;
+/* KerberosSchemeFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.impl.auth.NTLMSchemeFactory;
+/* SPNegoSchemeFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.impl.conn.BasicClientConnectionManager;
+import ch.boye.httpclientandroidlib.impl.conn.DefaultHttpRoutePlanner;
+import ch.boye.httpclientandroidlib.impl.conn.SchemeRegistryFactory;
+import ch.boye.httpclientandroidlib.impl.cookie.BestMatchSpecFactory;
+import ch.boye.httpclientandroidlib.impl.cookie.BrowserCompatSpecFactory;
+import ch.boye.httpclientandroidlib.impl.cookie.IgnoreSpecFactory;
+import ch.boye.httpclientandroidlib.impl.cookie.NetscapeDraftSpecFactory;
+import ch.boye.httpclientandroidlib.impl.cookie.RFC2109SpecFactory;
+import ch.boye.httpclientandroidlib.impl.cookie.RFC2965SpecFactory;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
+import ch.boye.httpclientandroidlib.protocol.BasicHttpProcessor;
+import ch.boye.httpclientandroidlib.protocol.DefaultedHttpContext;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.protocol.HttpProcessor;
+import ch.boye.httpclientandroidlib.protocol.HttpRequestExecutor;
+import ch.boye.httpclientandroidlib.protocol.ImmutableHttpProcessor;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Base class for {@link ch.boye.httpclientandroidlib.client.HttpClient} implementations.
+ * This class acts as a facade to a number of special purpose handler or
+ * strategy implementations responsible for handling of a particular aspect
+ * of the HTTP protocol such as redirect or authentication handling or
+ * making decision about connection persistence and keep alive duration.
+ * This enables the users to selectively replace default implementation
+ * of those aspects with custom, application specific ones. This class
+ * also provides factory methods to instantiate those objects:
+ * <ul>
+ * <li>{@link HttpRequestExecutor}</li> object used to transmit messages
+ * over HTTP connections. The {@link #createRequestExecutor()} must be
+ * implemented by concrete super classes to instantiate this object.
+ * <li>{@link BasicHttpProcessor}</li> object to manage a list of protocol
+ * interceptors and apply cross-cutting protocol logic to all incoming
+ * and outgoing HTTP messages. The {@link #createHttpProcessor()} must be
+ * implemented by concrete super classes to instantiate this object.
+ * <li>{@link HttpRequestRetryHandler}</li> object used to decide whether
+ * or not a failed HTTP request is safe to retry automatically.
+ * The {@link #createHttpRequestRetryHandler()} must be
+ * implemented by concrete super classes to instantiate this object.
+ * <li>{@link ClientConnectionManager}</li> object used to manage
+ * persistent HTTP connections.
+ * <li>{@link ConnectionReuseStrategy}</li> object used to decide whether
+ * or not a HTTP connection can be kept alive and re-used for subsequent
+ * HTTP requests. The {@link #createConnectionReuseStrategy()} must be
+ * implemented by concrete super classes to instantiate this object.
+ * <li>{@link ConnectionKeepAliveStrategy}</li> object used to decide how
+ * long a persistent HTTP connection can be kept alive.
+ * The {@link #createConnectionKeepAliveStrategy()} must be
+ * implemented by concrete super classes to instantiate this object.
+ * <li>{@link CookieSpecRegistry}</li> object used to maintain a list of
+ * supported cookie specifications.
+ * The {@link #createCookieSpecRegistry()} must be implemented by concrete
+ * super classes to instantiate this object.
+ * <li>{@link CookieStore}</li> object used to maintain a collection of
+ * cookies. The {@link #createCookieStore()} must be implemented by
+ * concrete super classes to instantiate this object.
+ * <li>{@link AuthSchemeRegistry}</li> object used to maintain a list of
+ * supported authentication schemes.
+ * The {@link #createAuthSchemeRegistry()} must be implemented by concrete
+ * super classes to instantiate this object.
+ * <li>{@link CredentialsProvider}</li> object used to maintain
+ * a collection user credentials. The {@link #createCredentialsProvider()}
+ * must be implemented by concrete super classes to instantiate
+ * this object.
+ * <li>{@link AuthenticationStrategy}</li> object used to authenticate
+ * against the target host.
+ * The {@link #createTargetAuthenticationStrategy()} must be implemented
+ * by concrete super classes to instantiate this object.
+ * <li>{@link AuthenticationStrategy}</li> object used to authenticate
+ * against the proxy host.
+ * The {@link #createProxyAuthenticationStrategy()} must be implemented
+ * by concrete super classes to instantiate this object.
+ * <li>{@link HttpRoutePlanner}</li> object used to calculate a route
+ * for establishing a connection to the target host. The route
+ * may involve multiple intermediate hops.
+ * The {@link #createHttpRoutePlanner()} must be implemented
+ * by concrete super classes to instantiate this object.
+ * <li>{@link RedirectStrategy}</li> object used to determine if an HTTP
+ * request should be redirected to a new location in response to an HTTP
+ * response received from the target server.
+ * <li>{@link UserTokenHandler}</li> object used to determine if the
+ * execution context is user identity specific.
+ * The {@link #createUserTokenHandler()} must be implemented by
+ * concrete super classes to instantiate this object.
+ * </ul>
+ * <p>
+ * This class also maintains a list of protocol interceptors intended
+ * for processing outgoing requests and incoming responses and provides
+ * methods for managing those interceptors. New protocol interceptors can be
+ * introduced to the protocol processor chain or removed from it if needed.
+ * Internally protocol interceptors are stored in a simple
+ * {@link java.util.ArrayList}. They are executed in the same natural order
+ * as they are added to the list.
+ * <p>
+ * AbstractHttpClient is thread safe. It is recommended that the same
+ * instance of this class is reused for multiple request executions.
+ * When an instance of DefaultHttpClient is no longer needed and is about
+ * to go out of scope the connection manager associated with it must be
+ * shut down by calling {@link ClientConnectionManager#shutdown()}!
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link HttpClientBuilder}.
+ */
+@ThreadSafe
+@Deprecated
+public abstract class AbstractHttpClient extends CloseableHttpClient {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ /** The parameters. */
+ @GuardedBy("this")
+ private HttpParams defaultParams;
+
+ /** The request executor. */
+ @GuardedBy("this")
+ private HttpRequestExecutor requestExec;
+
+ /** The connection manager. */
+ @GuardedBy("this")
+ private ClientConnectionManager connManager;
+
+ /** The connection re-use strategy. */
+ @GuardedBy("this")
+ private ConnectionReuseStrategy reuseStrategy;
+
+ /** The connection keep-alive strategy. */
+ @GuardedBy("this")
+ private ConnectionKeepAliveStrategy keepAliveStrategy;
+
+ /** The cookie spec registry. */
+ @GuardedBy("this")
+ private CookieSpecRegistry supportedCookieSpecs;
+
+ /** The authentication scheme registry. */
+ @GuardedBy("this")
+ private AuthSchemeRegistry supportedAuthSchemes;
+
+ /** The HTTP protocol processor and its immutable copy. */
+ @GuardedBy("this")
+ private BasicHttpProcessor mutableProcessor;
+
+ @GuardedBy("this")
+ private ImmutableHttpProcessor protocolProcessor;
+
+ /** The request retry handler. */
+ @GuardedBy("this")
+ private HttpRequestRetryHandler retryHandler;
+
+ /** The redirect handler. */
+ @GuardedBy("this")
+ private RedirectStrategy redirectStrategy;
+
+ /** The target authentication handler. */
+ @GuardedBy("this")
+ private AuthenticationStrategy targetAuthStrategy;
+
+ /** The proxy authentication handler. */
+ @GuardedBy("this")
+ private AuthenticationStrategy proxyAuthStrategy;
+
+ /** The cookie store. */
+ @GuardedBy("this")
+ private CookieStore cookieStore;
+
+ /** The credentials provider. */
+ @GuardedBy("this")
+ private CredentialsProvider credsProvider;
+
+ /** The route planner. */
+ @GuardedBy("this")
+ private HttpRoutePlanner routePlanner;
+
+ /** The user token handler. */
+ @GuardedBy("this")
+ private UserTokenHandler userTokenHandler;
+
+ /** The connection backoff strategy. */
+ @GuardedBy("this")
+ private ConnectionBackoffStrategy connectionBackoffStrategy;
+
+ /** The backoff manager. */
+ @GuardedBy("this")
+ private BackoffManager backoffManager;
+
+ /**
+ * Creates a new HTTP client.
+ *
+ * @param conman the connection manager
+ * @param params the parameters
+ */
+ protected AbstractHttpClient(
+ final ClientConnectionManager conman,
+ final HttpParams params) {
+ super();
+ defaultParams = params;
+ connManager = conman;
+ } // constructor
+
+
+ protected abstract HttpParams createHttpParams();
+
+
+ protected abstract BasicHttpProcessor createHttpProcessor();
+
+
+ protected HttpContext createHttpContext() {
+ final HttpContext context = new BasicHttpContext();
+ context.setAttribute(
+ ClientContext.SCHEME_REGISTRY,
+ getConnectionManager().getSchemeRegistry());
+ context.setAttribute(
+ ClientContext.AUTHSCHEME_REGISTRY,
+ getAuthSchemes());
+ context.setAttribute(
+ ClientContext.COOKIESPEC_REGISTRY,
+ getCookieSpecs());
+ context.setAttribute(
+ ClientContext.COOKIE_STORE,
+ getCookieStore());
+ context.setAttribute(
+ ClientContext.CREDS_PROVIDER,
+ getCredentialsProvider());
+ return context;
+ }
+
+
+ protected ClientConnectionManager createClientConnectionManager() {
+ final SchemeRegistry registry = SchemeRegistryFactory.createDefault();
+
+ ClientConnectionManager connManager = null;
+ final HttpParams params = getParams();
+
+ ClientConnectionManagerFactory factory = null;
+
+ final String className = (String) params.getParameter(
+ ClientPNames.CONNECTION_MANAGER_FACTORY_CLASS_NAME);
+ if (className != null) {
+ try {
+ final Class<?> clazz = Class.forName(className);
+ factory = (ClientConnectionManagerFactory) clazz.newInstance();
+ } catch (final ClassNotFoundException ex) {
+ throw new IllegalStateException("Invalid class name: " + className);
+ } catch (final IllegalAccessException ex) {
+ throw new IllegalAccessError(ex.getMessage());
+ } catch (final InstantiationException ex) {
+ throw new InstantiationError(ex.getMessage());
+ }
+ }
+ if (factory != null) {
+ connManager = factory.newInstance(params, registry);
+ } else {
+ connManager = new BasicClientConnectionManager(registry);
+ }
+
+ return connManager;
+ }
+
+
+ protected AuthSchemeRegistry createAuthSchemeRegistry() {
+ final AuthSchemeRegistry registry = new AuthSchemeRegistry();
+ registry.register(
+ AuthPolicy.BASIC,
+ new BasicSchemeFactory());
+ registry.register(
+ AuthPolicy.DIGEST,
+ new DigestSchemeFactory());
+ registry.register(
+ AuthPolicy.NTLM,
+ new NTLMSchemeFactory());
+ /* SPNegoSchemeFactory removed by HttpClient for Android script. */
+ /* KerberosSchemeFactory removed by HttpClient for Android script. */
+ return registry;
+ }
+
+
+ protected CookieSpecRegistry createCookieSpecRegistry() {
+ final CookieSpecRegistry registry = new CookieSpecRegistry();
+ registry.register(
+ CookiePolicy.BEST_MATCH,
+ new BestMatchSpecFactory());
+ registry.register(
+ CookiePolicy.BROWSER_COMPATIBILITY,
+ new BrowserCompatSpecFactory());
+ registry.register(
+ CookiePolicy.NETSCAPE,
+ new NetscapeDraftSpecFactory());
+ registry.register(
+ CookiePolicy.RFC_2109,
+ new RFC2109SpecFactory());
+ registry.register(
+ CookiePolicy.RFC_2965,
+ new RFC2965SpecFactory());
+ registry.register(
+ CookiePolicy.IGNORE_COOKIES,
+ new IgnoreSpecFactory());
+ return registry;
+ }
+
+ protected HttpRequestExecutor createRequestExecutor() {
+ return new HttpRequestExecutor();
+ }
+
+ protected ConnectionReuseStrategy createConnectionReuseStrategy() {
+ return new DefaultConnectionReuseStrategy();
+ }
+
+ protected ConnectionKeepAliveStrategy createConnectionKeepAliveStrategy() {
+ return new DefaultConnectionKeepAliveStrategy();
+ }
+
+ protected HttpRequestRetryHandler createHttpRequestRetryHandler() {
+ return new DefaultHttpRequestRetryHandler();
+ }
+
+ /**
+ * @deprecated (4.1) do not use
+ */
+ @Deprecated
+ protected RedirectHandler createRedirectHandler() {
+ return new DefaultRedirectHandler();
+ }
+
+ protected AuthenticationStrategy createTargetAuthenticationStrategy() {
+ return new TargetAuthenticationStrategy();
+ }
+
+ /**
+ * @deprecated (4.2) do not use
+ */
+ @Deprecated
+ protected AuthenticationHandler createTargetAuthenticationHandler() {
+ return new DefaultTargetAuthenticationHandler();
+ }
+
+ protected AuthenticationStrategy createProxyAuthenticationStrategy() {
+ return new ProxyAuthenticationStrategy();
+ }
+
+ /**
+ * @deprecated (4.2) do not use
+ */
+ @Deprecated
+ protected AuthenticationHandler createProxyAuthenticationHandler() {
+ return new DefaultProxyAuthenticationHandler();
+ }
+
+ protected CookieStore createCookieStore() {
+ return new BasicCookieStore();
+ }
+
+ protected CredentialsProvider createCredentialsProvider() {
+ return new BasicCredentialsProvider();
+ }
+
+ protected HttpRoutePlanner createHttpRoutePlanner() {
+ return new DefaultHttpRoutePlanner(getConnectionManager().getSchemeRegistry());
+ }
+
+ protected UserTokenHandler createUserTokenHandler() {
+ return new DefaultUserTokenHandler();
+ }
+
+ // non-javadoc, see interface HttpClient
+ public synchronized final HttpParams getParams() {
+ if (defaultParams == null) {
+ defaultParams = createHttpParams();
+ }
+ return defaultParams;
+ }
+
+ /**
+ * Replaces the parameters.
+ * The implementation here does not update parameters of dependent objects.
+ *
+ * @param params the new default parameters
+ */
+ public synchronized void setParams(final HttpParams params) {
+ defaultParams = params;
+ }
+
+
+ public synchronized final ClientConnectionManager getConnectionManager() {
+ if (connManager == null) {
+ connManager = createClientConnectionManager();
+ }
+ return connManager;
+ }
+
+
+ public synchronized final HttpRequestExecutor getRequestExecutor() {
+ if (requestExec == null) {
+ requestExec = createRequestExecutor();
+ }
+ return requestExec;
+ }
+
+
+ public synchronized final AuthSchemeRegistry getAuthSchemes() {
+ if (supportedAuthSchemes == null) {
+ supportedAuthSchemes = createAuthSchemeRegistry();
+ }
+ return supportedAuthSchemes;
+ }
+
+ public synchronized void setAuthSchemes(final AuthSchemeRegistry registry) {
+ supportedAuthSchemes = registry;
+ }
+
+ public synchronized final ConnectionBackoffStrategy getConnectionBackoffStrategy() {
+ return connectionBackoffStrategy;
+ }
+
+ public synchronized void setConnectionBackoffStrategy(final ConnectionBackoffStrategy strategy) {
+ connectionBackoffStrategy = strategy;
+ }
+
+ public synchronized final CookieSpecRegistry getCookieSpecs() {
+ if (supportedCookieSpecs == null) {
+ supportedCookieSpecs = createCookieSpecRegistry();
+ }
+ return supportedCookieSpecs;
+ }
+
+ public synchronized final BackoffManager getBackoffManager() {
+ return backoffManager;
+ }
+
+ public synchronized void setBackoffManager(final BackoffManager manager) {
+ backoffManager = manager;
+ }
+
+ public synchronized void setCookieSpecs(final CookieSpecRegistry registry) {
+ supportedCookieSpecs = registry;
+ }
+
+ public synchronized final ConnectionReuseStrategy getConnectionReuseStrategy() {
+ if (reuseStrategy == null) {
+ reuseStrategy = createConnectionReuseStrategy();
+ }
+ return reuseStrategy;
+ }
+
+
+ public synchronized void setReuseStrategy(final ConnectionReuseStrategy strategy) {
+ this.reuseStrategy = strategy;
+ }
+
+
+ public synchronized final ConnectionKeepAliveStrategy getConnectionKeepAliveStrategy() {
+ if (keepAliveStrategy == null) {
+ keepAliveStrategy = createConnectionKeepAliveStrategy();
+ }
+ return keepAliveStrategy;
+ }
+
+
+ public synchronized void setKeepAliveStrategy(final ConnectionKeepAliveStrategy strategy) {
+ this.keepAliveStrategy = strategy;
+ }
+
+
+ public synchronized final HttpRequestRetryHandler getHttpRequestRetryHandler() {
+ if (retryHandler == null) {
+ retryHandler = createHttpRequestRetryHandler();
+ }
+ return retryHandler;
+ }
+
+ public synchronized void setHttpRequestRetryHandler(final HttpRequestRetryHandler handler) {
+ this.retryHandler = handler;
+ }
+
+ /**
+ * @deprecated (4.1) do not use
+ */
+ @Deprecated
+ public synchronized final RedirectHandler getRedirectHandler() {
+ return createRedirectHandler();
+ }
+
+ /**
+ * @deprecated (4.1) do not use
+ */
+ @Deprecated
+ public synchronized void setRedirectHandler(final RedirectHandler handler) {
+ this.redirectStrategy = new DefaultRedirectStrategyAdaptor(handler);
+ }
+
+ /**
+ * @since 4.1
+ */
+ public synchronized final RedirectStrategy getRedirectStrategy() {
+ if (redirectStrategy == null) {
+ redirectStrategy = new DefaultRedirectStrategy();
+ }
+ return redirectStrategy;
+ }
+
+ /**
+ * @since 4.1
+ */
+ public synchronized void setRedirectStrategy(final RedirectStrategy strategy) {
+ this.redirectStrategy = strategy;
+ }
+
+ /**
+ * @deprecated (4.2) do not use
+ */
+ @Deprecated
+ public synchronized final AuthenticationHandler getTargetAuthenticationHandler() {
+ return createTargetAuthenticationHandler();
+ }
+
+ /**
+ * @deprecated (4.2) do not use
+ */
+ @Deprecated
+ public synchronized void setTargetAuthenticationHandler(final AuthenticationHandler handler) {
+ this.targetAuthStrategy = new AuthenticationStrategyAdaptor(handler);
+ }
+
+ /**
+ * @since 4.2
+ */
+ public synchronized final AuthenticationStrategy getTargetAuthenticationStrategy() {
+ if (targetAuthStrategy == null) {
+ targetAuthStrategy = createTargetAuthenticationStrategy();
+ }
+ return targetAuthStrategy;
+ }
+
+ /**
+ * @since 4.2
+ */
+ public synchronized void setTargetAuthenticationStrategy(final AuthenticationStrategy strategy) {
+ this.targetAuthStrategy = strategy;
+ }
+
+ /**
+ * @deprecated (4.2) do not use
+ */
+ @Deprecated
+ public synchronized final AuthenticationHandler getProxyAuthenticationHandler() {
+ return createProxyAuthenticationHandler();
+ }
+
+ /**
+ * @deprecated (4.2) do not use
+ */
+ @Deprecated
+ public synchronized void setProxyAuthenticationHandler(final AuthenticationHandler handler) {
+ this.proxyAuthStrategy = new AuthenticationStrategyAdaptor(handler);
+ }
+
+ /**
+ * @since 4.2
+ */
+ public synchronized final AuthenticationStrategy getProxyAuthenticationStrategy() {
+ if (proxyAuthStrategy == null) {
+ proxyAuthStrategy = createProxyAuthenticationStrategy();
+ }
+ return proxyAuthStrategy;
+ }
+
+ /**
+ * @since 4.2
+ */
+ public synchronized void setProxyAuthenticationStrategy(final AuthenticationStrategy strategy) {
+ this.proxyAuthStrategy = strategy;
+ }
+
+ public synchronized final CookieStore getCookieStore() {
+ if (cookieStore == null) {
+ cookieStore = createCookieStore();
+ }
+ return cookieStore;
+ }
+
+ public synchronized void setCookieStore(final CookieStore cookieStore) {
+ this.cookieStore = cookieStore;
+ }
+
+ public synchronized final CredentialsProvider getCredentialsProvider() {
+ if (credsProvider == null) {
+ credsProvider = createCredentialsProvider();
+ }
+ return credsProvider;
+ }
+
+ public synchronized void setCredentialsProvider(final CredentialsProvider credsProvider) {
+ this.credsProvider = credsProvider;
+ }
+
+ public synchronized final HttpRoutePlanner getRoutePlanner() {
+ if (this.routePlanner == null) {
+ this.routePlanner = createHttpRoutePlanner();
+ }
+ return this.routePlanner;
+ }
+
+ public synchronized void setRoutePlanner(final HttpRoutePlanner routePlanner) {
+ this.routePlanner = routePlanner;
+ }
+
+ public synchronized final UserTokenHandler getUserTokenHandler() {
+ if (this.userTokenHandler == null) {
+ this.userTokenHandler = createUserTokenHandler();
+ }
+ return this.userTokenHandler;
+ }
+
+ public synchronized void setUserTokenHandler(final UserTokenHandler handler) {
+ this.userTokenHandler = handler;
+ }
+
+ protected synchronized final BasicHttpProcessor getHttpProcessor() {
+ if (mutableProcessor == null) {
+ mutableProcessor = createHttpProcessor();
+ }
+ return mutableProcessor;
+ }
+
+ private synchronized HttpProcessor getProtocolProcessor() {
+ if (protocolProcessor == null) {
+ // Get mutable HTTP processor
+ final BasicHttpProcessor proc = getHttpProcessor();
+ // and create an immutable copy of it
+ final int reqc = proc.getRequestInterceptorCount();
+ final HttpRequestInterceptor[] reqinterceptors = new HttpRequestInterceptor[reqc];
+ for (int i = 0; i < reqc; i++) {
+ reqinterceptors[i] = proc.getRequestInterceptor(i);
+ }
+ final int resc = proc.getResponseInterceptorCount();
+ final HttpResponseInterceptor[] resinterceptors = new HttpResponseInterceptor[resc];
+ for (int i = 0; i < resc; i++) {
+ resinterceptors[i] = proc.getResponseInterceptor(i);
+ }
+ protocolProcessor = new ImmutableHttpProcessor(reqinterceptors, resinterceptors);
+ }
+ return protocolProcessor;
+ }
+
+ public synchronized int getResponseInterceptorCount() {
+ return getHttpProcessor().getResponseInterceptorCount();
+ }
+
+ public synchronized HttpResponseInterceptor getResponseInterceptor(final int index) {
+ return getHttpProcessor().getResponseInterceptor(index);
+ }
+
+ public synchronized HttpRequestInterceptor getRequestInterceptor(final int index) {
+ return getHttpProcessor().getRequestInterceptor(index);
+ }
+
+ public synchronized int getRequestInterceptorCount() {
+ return getHttpProcessor().getRequestInterceptorCount();
+ }
+
+ public synchronized void addResponseInterceptor(final HttpResponseInterceptor itcp) {
+ getHttpProcessor().addInterceptor(itcp);
+ protocolProcessor = null;
+ }
+
+ public synchronized void addResponseInterceptor(final HttpResponseInterceptor itcp, final int index) {
+ getHttpProcessor().addInterceptor(itcp, index);
+ protocolProcessor = null;
+ }
+
+ public synchronized void clearResponseInterceptors() {
+ getHttpProcessor().clearResponseInterceptors();
+ protocolProcessor = null;
+ }
+
+ public synchronized void removeResponseInterceptorByClass(final Class<? extends HttpResponseInterceptor> clazz) {
+ getHttpProcessor().removeResponseInterceptorByClass(clazz);
+ protocolProcessor = null;
+ }
+
+ public synchronized void addRequestInterceptor(final HttpRequestInterceptor itcp) {
+ getHttpProcessor().addInterceptor(itcp);
+ protocolProcessor = null;
+ }
+
+ public synchronized void addRequestInterceptor(final HttpRequestInterceptor itcp, final int index) {
+ getHttpProcessor().addInterceptor(itcp, index);
+ protocolProcessor = null;
+ }
+
+ public synchronized void clearRequestInterceptors() {
+ getHttpProcessor().clearRequestInterceptors();
+ protocolProcessor = null;
+ }
+
+ public synchronized void removeRequestInterceptorByClass(final Class<? extends HttpRequestInterceptor> clazz) {
+ getHttpProcessor().removeRequestInterceptorByClass(clazz);
+ protocolProcessor = null;
+ }
+
+ @Override
+ protected final CloseableHttpResponse doExecute(final HttpHost target, final HttpRequest request,
+ final HttpContext context)
+ throws IOException, ClientProtocolException {
+
+ Args.notNull(request, "HTTP request");
+ // a null target may be acceptable, this depends on the route planner
+ // a null context is acceptable, default context created below
+
+ HttpContext execContext = null;
+ RequestDirector director = null;
+ HttpRoutePlanner routePlanner = null;
+ ConnectionBackoffStrategy connectionBackoffStrategy = null;
+ BackoffManager backoffManager = null;
+
+ // Initialize the request execution context making copies of
+ // all shared objects that are potentially threading unsafe.
+ synchronized (this) {
+
+ final HttpContext defaultContext = createHttpContext();
+ if (context == null) {
+ execContext = defaultContext;
+ } else {
+ execContext = new DefaultedHttpContext(context, defaultContext);
+ }
+ final HttpParams params = determineParams(request);
+ final RequestConfig config = HttpClientParamConfig.getRequestConfig(params);
+ execContext.setAttribute(ClientContext.REQUEST_CONFIG, config);
+
+ // Create a director for this request
+ director = createClientRequestDirector(
+ getRequestExecutor(),
+ getConnectionManager(),
+ getConnectionReuseStrategy(),
+ getConnectionKeepAliveStrategy(),
+ getRoutePlanner(),
+ getProtocolProcessor(),
+ getHttpRequestRetryHandler(),
+ getRedirectStrategy(),
+ getTargetAuthenticationStrategy(),
+ getProxyAuthenticationStrategy(),
+ getUserTokenHandler(),
+ params);
+ routePlanner = getRoutePlanner();
+ connectionBackoffStrategy = getConnectionBackoffStrategy();
+ backoffManager = getBackoffManager();
+ }
+
+ try {
+ if (connectionBackoffStrategy != null && backoffManager != null) {
+ final HttpHost targetForRoute = (target != null) ? target
+ : (HttpHost) determineParams(request).getParameter(
+ ClientPNames.DEFAULT_HOST);
+ final HttpRoute route = routePlanner.determineRoute(targetForRoute, request, execContext);
+
+ final CloseableHttpResponse out;
+ try {
+ out = CloseableHttpResponseProxy.newProxy(
+ director.execute(target, request, execContext));
+ } catch (final RuntimeException re) {
+ if (connectionBackoffStrategy.shouldBackoff(re)) {
+ backoffManager.backOff(route);
+ }
+ throw re;
+ } catch (final Exception e) {
+ if (connectionBackoffStrategy.shouldBackoff(e)) {
+ backoffManager.backOff(route);
+ }
+ if (e instanceof HttpException) {
+ throw (HttpException)e;
+ }
+ if (e instanceof IOException) {
+ throw (IOException)e;
+ }
+ throw new UndeclaredThrowableException(e);
+ }
+ if (connectionBackoffStrategy.shouldBackoff(out)) {
+ backoffManager.backOff(route);
+ } else {
+ backoffManager.probe(route);
+ }
+ return out;
+ } else {
+ return CloseableHttpResponseProxy.newProxy(
+ director.execute(target, request, execContext));
+ }
+ } catch(final HttpException httpException) {
+ throw new ClientProtocolException(httpException);
+ }
+ }
+
+ /**
+ * @deprecated (4.1) do not use
+ */
+ @Deprecated
+ protected RequestDirector createClientRequestDirector(
+ final HttpRequestExecutor requestExec,
+ final ClientConnectionManager conman,
+ final ConnectionReuseStrategy reustrat,
+ final ConnectionKeepAliveStrategy kastrat,
+ final HttpRoutePlanner rouplan,
+ final HttpProcessor httpProcessor,
+ final HttpRequestRetryHandler retryHandler,
+ final RedirectHandler redirectHandler,
+ final AuthenticationHandler targetAuthHandler,
+ final AuthenticationHandler proxyAuthHandler,
+ final UserTokenHandler userTokenHandler,
+ final HttpParams params) {
+ return new DefaultRequestDirector(
+ requestExec,
+ conman,
+ reustrat,
+ kastrat,
+ rouplan,
+ httpProcessor,
+ retryHandler,
+ redirectHandler,
+ targetAuthHandler,
+ proxyAuthHandler,
+ userTokenHandler,
+ params);
+ }
+
+ /**
+ * @deprecated (4.2) do not use
+ */
+ @Deprecated
+ protected RequestDirector createClientRequestDirector(
+ final HttpRequestExecutor requestExec,
+ final ClientConnectionManager conman,
+ final ConnectionReuseStrategy reustrat,
+ final ConnectionKeepAliveStrategy kastrat,
+ final HttpRoutePlanner rouplan,
+ final HttpProcessor httpProcessor,
+ final HttpRequestRetryHandler retryHandler,
+ final RedirectStrategy redirectStrategy,
+ final AuthenticationHandler targetAuthHandler,
+ final AuthenticationHandler proxyAuthHandler,
+ final UserTokenHandler userTokenHandler,
+ final HttpParams params) {
+ return new DefaultRequestDirector(
+ log,
+ requestExec,
+ conman,
+ reustrat,
+ kastrat,
+ rouplan,
+ httpProcessor,
+ retryHandler,
+ redirectStrategy,
+ targetAuthHandler,
+ proxyAuthHandler,
+ userTokenHandler,
+ params);
+ }
+
+
+ /**
+ * @since 4.2
+ */
+ protected RequestDirector createClientRequestDirector(
+ final HttpRequestExecutor requestExec,
+ final ClientConnectionManager conman,
+ final ConnectionReuseStrategy reustrat,
+ final ConnectionKeepAliveStrategy kastrat,
+ final HttpRoutePlanner rouplan,
+ final HttpProcessor httpProcessor,
+ final HttpRequestRetryHandler retryHandler,
+ final RedirectStrategy redirectStrategy,
+ final AuthenticationStrategy targetAuthStrategy,
+ final AuthenticationStrategy proxyAuthStrategy,
+ final UserTokenHandler userTokenHandler,
+ final HttpParams params) {
+ return new DefaultRequestDirector(
+ log,
+ requestExec,
+ conman,
+ reustrat,
+ kastrat,
+ rouplan,
+ httpProcessor,
+ retryHandler,
+ redirectStrategy,
+ targetAuthStrategy,
+ proxyAuthStrategy,
+ userTokenHandler,
+ params);
+ }
+
+ /**
+ * Obtains parameters for executing a request.
+ * The default implementation in this class creates a new
+ * {@link ClientParamsStack} from the request parameters
+ * and the client parameters.
+ * <br/>
+ * This method is called by the default implementation of
+ * {@link #execute(HttpHost,HttpRequest,HttpContext)}
+ * to obtain the parameters for the
+ * {@link DefaultRequestDirector}.
+ *
+ * @param req the request that will be executed
+ *
+ * @return the parameters to use
+ */
+ protected HttpParams determineParams(final HttpRequest req) {
+ return new ClientParamsStack
+ (null, getParams(), req.getParams(), null);
+ }
+
+
+ public void close() {
+ getConnectionManager().shutdown();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AuthenticationStrategyAdaptor.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AuthenticationStrategyAdaptor.java
new file mode 100644
index 0000000000..a65ce7e958
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AuthenticationStrategyAdaptor.java
@@ -0,0 +1,172 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.util.LinkedList;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Queue;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.auth.AuthOption;
+import ch.boye.httpclientandroidlib.auth.AuthScheme;
+import ch.boye.httpclientandroidlib.auth.AuthScope;
+import ch.boye.httpclientandroidlib.auth.AuthenticationException;
+import ch.boye.httpclientandroidlib.auth.Credentials;
+import ch.boye.httpclientandroidlib.auth.MalformedChallengeException;
+import ch.boye.httpclientandroidlib.client.AuthCache;
+import ch.boye.httpclientandroidlib.client.AuthenticationHandler;
+import ch.boye.httpclientandroidlib.client.AuthenticationStrategy;
+import ch.boye.httpclientandroidlib.client.CredentialsProvider;
+import ch.boye.httpclientandroidlib.client.params.AuthPolicy;
+import ch.boye.httpclientandroidlib.client.protocol.ClientContext;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * @deprecated (4.2) do not use
+ */
+@Immutable
+@Deprecated
+class AuthenticationStrategyAdaptor implements AuthenticationStrategy {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ private final AuthenticationHandler handler;
+
+ public AuthenticationStrategyAdaptor(final AuthenticationHandler handler) {
+ super();
+ this.handler = handler;
+ }
+
+ public boolean isAuthenticationRequested(
+ final HttpHost authhost,
+ final HttpResponse response,
+ final HttpContext context) {
+ return this.handler.isAuthenticationRequested(response, context);
+ }
+
+ public Map<String, Header> getChallenges(
+ final HttpHost authhost,
+ final HttpResponse response,
+ final HttpContext context) throws MalformedChallengeException {
+ return this.handler.getChallenges(response, context);
+ }
+
+ public Queue<AuthOption> select(
+ final Map<String, Header> challenges,
+ final HttpHost authhost,
+ final HttpResponse response,
+ final HttpContext context) throws MalformedChallengeException {
+ Args.notNull(challenges, "Map of auth challenges");
+ Args.notNull(authhost, "Host");
+ Args.notNull(response, "HTTP response");
+ Args.notNull(context, "HTTP context");
+
+ final Queue<AuthOption> options = new LinkedList<AuthOption>();
+ final CredentialsProvider credsProvider = (CredentialsProvider) context.getAttribute(
+ ClientContext.CREDS_PROVIDER);
+ if (credsProvider == null) {
+ this.log.debug("Credentials provider not set in the context");
+ return options;
+ }
+
+ final AuthScheme authScheme;
+ try {
+ authScheme = this.handler.selectScheme(challenges, response, context);
+ } catch (final AuthenticationException ex) {
+ if (this.log.isWarnEnabled()) {
+ this.log.warn(ex.getMessage(), ex);
+ }
+ return options;
+ }
+ final String id = authScheme.getSchemeName();
+ final Header challenge = challenges.get(id.toLowerCase(Locale.ENGLISH));
+ authScheme.processChallenge(challenge);
+
+ final AuthScope authScope = new AuthScope(
+ authhost.getHostName(),
+ authhost.getPort(),
+ authScheme.getRealm(),
+ authScheme.getSchemeName());
+
+ final Credentials credentials = credsProvider.getCredentials(authScope);
+ if (credentials != null) {
+ options.add(new AuthOption(authScheme, credentials));
+ }
+ return options;
+ }
+
+ public void authSucceeded(
+ final HttpHost authhost, final AuthScheme authScheme, final HttpContext context) {
+ AuthCache authCache = (AuthCache) context.getAttribute(ClientContext.AUTH_CACHE);
+ if (isCachable(authScheme)) {
+ if (authCache == null) {
+ authCache = new BasicAuthCache();
+ context.setAttribute(ClientContext.AUTH_CACHE, authCache);
+ }
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Caching '" + authScheme.getSchemeName() +
+ "' auth scheme for " + authhost);
+ }
+ authCache.put(authhost, authScheme);
+ }
+ }
+
+ public void authFailed(
+ final HttpHost authhost, final AuthScheme authScheme, final HttpContext context) {
+ final AuthCache authCache = (AuthCache) context.getAttribute(ClientContext.AUTH_CACHE);
+ if (authCache == null) {
+ return;
+ }
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Removing from cache '" + authScheme.getSchemeName() +
+ "' auth scheme for " + authhost);
+ }
+ authCache.remove(authhost);
+ }
+
+ private boolean isCachable(final AuthScheme authScheme) {
+ if (authScheme == null || !authScheme.isComplete()) {
+ return false;
+ }
+ final String schemeName = authScheme.getSchemeName();
+ return schemeName.equalsIgnoreCase(AuthPolicy.BASIC) ||
+ schemeName.equalsIgnoreCase(AuthPolicy.DIGEST);
+ }
+
+ public AuthenticationHandler getHandler() {
+ return this.handler;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AuthenticationStrategyImpl.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AuthenticationStrategyImpl.java
new file mode 100644
index 0000000000..38b7df6d29
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AuthenticationStrategyImpl.java
@@ -0,0 +1,245 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Queue;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.FormattedHeader;
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.auth.AuthOption;
+import ch.boye.httpclientandroidlib.auth.AuthScheme;
+import ch.boye.httpclientandroidlib.auth.AuthSchemeProvider;
+import ch.boye.httpclientandroidlib.auth.AuthScope;
+import ch.boye.httpclientandroidlib.auth.Credentials;
+import ch.boye.httpclientandroidlib.auth.MalformedChallengeException;
+import ch.boye.httpclientandroidlib.client.AuthCache;
+import ch.boye.httpclientandroidlib.client.AuthenticationStrategy;
+import ch.boye.httpclientandroidlib.client.CredentialsProvider;
+import ch.boye.httpclientandroidlib.client.config.AuthSchemes;
+import ch.boye.httpclientandroidlib.client.config.RequestConfig;
+import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
+import ch.boye.httpclientandroidlib.config.Lookup;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+@Immutable
+abstract class AuthenticationStrategyImpl implements AuthenticationStrategy {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ private static final List<String> DEFAULT_SCHEME_PRIORITY =
+ Collections.unmodifiableList(Arrays.asList(AuthSchemes.SPNEGO,
+ AuthSchemes.KERBEROS,
+ AuthSchemes.NTLM,
+ AuthSchemes.DIGEST,
+ AuthSchemes.BASIC));
+
+ private final int challengeCode;
+ private final String headerName;
+
+ AuthenticationStrategyImpl(final int challengeCode, final String headerName) {
+ super();
+ this.challengeCode = challengeCode;
+ this.headerName = headerName;
+ }
+
+ public boolean isAuthenticationRequested(
+ final HttpHost authhost,
+ final HttpResponse response,
+ final HttpContext context) {
+ Args.notNull(response, "HTTP response");
+ final int status = response.getStatusLine().getStatusCode();
+ return status == this.challengeCode;
+ }
+
+ public Map<String, Header> getChallenges(
+ final HttpHost authhost,
+ final HttpResponse response,
+ final HttpContext context) throws MalformedChallengeException {
+ Args.notNull(response, "HTTP response");
+ final Header[] headers = response.getHeaders(this.headerName);
+ final Map<String, Header> map = new HashMap<String, Header>(headers.length);
+ for (final Header header : headers) {
+ final CharArrayBuffer buffer;
+ int pos;
+ if (header instanceof FormattedHeader) {
+ buffer = ((FormattedHeader) header).getBuffer();
+ pos = ((FormattedHeader) header).getValuePos();
+ } else {
+ final String s = header.getValue();
+ if (s == null) {
+ throw new MalformedChallengeException("Header value is null");
+ }
+ buffer = new CharArrayBuffer(s.length());
+ buffer.append(s);
+ pos = 0;
+ }
+ while (pos < buffer.length() && HTTP.isWhitespace(buffer.charAt(pos))) {
+ pos++;
+ }
+ final int beginIndex = pos;
+ while (pos < buffer.length() && !HTTP.isWhitespace(buffer.charAt(pos))) {
+ pos++;
+ }
+ final int endIndex = pos;
+ final String s = buffer.substring(beginIndex, endIndex);
+ map.put(s.toLowerCase(Locale.ENGLISH), header);
+ }
+ return map;
+ }
+
+ abstract Collection<String> getPreferredAuthSchemes(RequestConfig config);
+
+ public Queue<AuthOption> select(
+ final Map<String, Header> challenges,
+ final HttpHost authhost,
+ final HttpResponse response,
+ final HttpContext context) throws MalformedChallengeException {
+ Args.notNull(challenges, "Map of auth challenges");
+ Args.notNull(authhost, "Host");
+ Args.notNull(response, "HTTP response");
+ Args.notNull(context, "HTTP context");
+ final HttpClientContext clientContext = HttpClientContext.adapt(context);
+
+ final Queue<AuthOption> options = new LinkedList<AuthOption>();
+ final Lookup<AuthSchemeProvider> registry = clientContext.getAuthSchemeRegistry();
+ if (registry == null) {
+ this.log.debug("Auth scheme registry not set in the context");
+ return options;
+ }
+ final CredentialsProvider credsProvider = clientContext.getCredentialsProvider();
+ if (credsProvider == null) {
+ this.log.debug("Credentials provider not set in the context");
+ return options;
+ }
+ final RequestConfig config = clientContext.getRequestConfig();
+ Collection<String> authPrefs = getPreferredAuthSchemes(config);
+ if (authPrefs == null) {
+ authPrefs = DEFAULT_SCHEME_PRIORITY;
+ }
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Authentication schemes in the order of preference: " + authPrefs);
+ }
+
+ for (final String id: authPrefs) {
+ final Header challenge = challenges.get(id.toLowerCase(Locale.ENGLISH));
+ if (challenge != null) {
+ final AuthSchemeProvider authSchemeProvider = registry.lookup(id);
+ if (authSchemeProvider == null) {
+ if (this.log.isWarnEnabled()) {
+ this.log.warn("Authentication scheme " + id + " not supported");
+ // Try again
+ }
+ continue;
+ }
+ final AuthScheme authScheme = authSchemeProvider.create(context);
+ authScheme.processChallenge(challenge);
+
+ final AuthScope authScope = new AuthScope(
+ authhost.getHostName(),
+ authhost.getPort(),
+ authScheme.getRealm(),
+ authScheme.getSchemeName());
+
+ final Credentials credentials = credsProvider.getCredentials(authScope);
+ if (credentials != null) {
+ options.add(new AuthOption(authScheme, credentials));
+ }
+ } else {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Challenge for " + id + " authentication scheme not available");
+ // Try again
+ }
+ }
+ }
+ return options;
+ }
+
+ public void authSucceeded(
+ final HttpHost authhost, final AuthScheme authScheme, final HttpContext context) {
+ Args.notNull(authhost, "Host");
+ Args.notNull(authScheme, "Auth scheme");
+ Args.notNull(context, "HTTP context");
+
+ final HttpClientContext clientContext = HttpClientContext.adapt(context);
+
+ if (isCachable(authScheme)) {
+ AuthCache authCache = clientContext.getAuthCache();
+ if (authCache == null) {
+ authCache = new BasicAuthCache();
+ clientContext.setAuthCache(authCache);
+ }
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Caching '" + authScheme.getSchemeName() +
+ "' auth scheme for " + authhost);
+ }
+ authCache.put(authhost, authScheme);
+ }
+ }
+
+ protected boolean isCachable(final AuthScheme authScheme) {
+ if (authScheme == null || !authScheme.isComplete()) {
+ return false;
+ }
+ final String schemeName = authScheme.getSchemeName();
+ return schemeName.equalsIgnoreCase(AuthSchemes.BASIC) ||
+ schemeName.equalsIgnoreCase(AuthSchemes.DIGEST);
+ }
+
+ public void authFailed(
+ final HttpHost authhost, final AuthScheme authScheme, final HttpContext context) {
+ Args.notNull(authhost, "Host");
+ Args.notNull(context, "HTTP context");
+
+ final HttpClientContext clientContext = HttpClientContext.adapt(context);
+
+ final AuthCache authCache = clientContext.getAuthCache();
+ if (authCache != null) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Clearing cached auth scheme for " + authhost);
+ }
+ authCache.remove(authhost);
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AutoRetryHttpClient.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AutoRetryHttpClient.java
new file mode 100644
index 0000000000..70c0e3404d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/AutoRetryHttpClient.java
@@ -0,0 +1,190 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.URI;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.client.HttpClient;
+import ch.boye.httpclientandroidlib.client.ResponseHandler;
+import ch.boye.httpclientandroidlib.client.ServiceUnavailableRetryStrategy;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionManager;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.EntityUtils;
+
+/**
+ * {@link HttpClient} implementation that can automatically retry the request in case of
+ * a non-2xx response using the {@link ServiceUnavailableRetryStrategy} interface.
+ *
+ * @since 4.2
+ *
+ * @deprecated (4.3) use {@link HttpClientBuilder}.
+ */
+@Deprecated
+@ThreadSafe
+public class AutoRetryHttpClient implements HttpClient {
+
+ private final HttpClient backend;
+
+ private final ServiceUnavailableRetryStrategy retryStrategy;
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ public AutoRetryHttpClient(
+ final HttpClient client, final ServiceUnavailableRetryStrategy retryStrategy) {
+ super();
+ Args.notNull(client, "HttpClient");
+ Args.notNull(retryStrategy, "ServiceUnavailableRetryStrategy");
+ this.backend = client;
+ this.retryStrategy = retryStrategy;
+ }
+
+ /**
+ * Constructs a {@code AutoRetryHttpClient} with default caching settings that
+ * stores cache entries in memory and uses a vanilla
+ * {@link DefaultHttpClient} for backend requests.
+ */
+ public AutoRetryHttpClient() {
+ this(new DefaultHttpClient(), new DefaultServiceUnavailableRetryStrategy());
+ }
+
+ /**
+ * Constructs a {@code AutoRetryHttpClient} with the given caching options that
+ * stores cache entries in memory and uses a vanilla
+ * {@link DefaultHttpClient} for backend requests.
+ *
+ * @param config
+ * retry configuration module options
+ */
+ public AutoRetryHttpClient(final ServiceUnavailableRetryStrategy config) {
+ this(new DefaultHttpClient(), config);
+ }
+
+ /**
+ * Constructs a {@code AutoRetryHttpClient} with default caching settings that
+ * stores cache entries in memory and uses the given {@link HttpClient} for
+ * backend requests.
+ *
+ * @param client
+ * used to make origin requests
+ */
+ public AutoRetryHttpClient(final HttpClient client) {
+ this(client, new DefaultServiceUnavailableRetryStrategy());
+ }
+
+ public HttpResponse execute(final HttpHost target, final HttpRequest request)
+ throws IOException {
+ final HttpContext defaultContext = null;
+ return execute(target, request, defaultContext);
+ }
+
+ public <T> T execute(final HttpHost target, final HttpRequest request,
+ final ResponseHandler<? extends T> responseHandler) throws IOException {
+ return execute(target, request, responseHandler, null);
+ }
+
+ public <T> T execute(final HttpHost target, final HttpRequest request,
+ final ResponseHandler<? extends T> responseHandler, final HttpContext context)
+ throws IOException {
+ final HttpResponse resp = execute(target, request, context);
+ return responseHandler.handleResponse(resp);
+ }
+
+ public HttpResponse execute(final HttpUriRequest request) throws IOException {
+ final HttpContext context = null;
+ return execute(request, context);
+ }
+
+ public HttpResponse execute(final HttpUriRequest request, final HttpContext context)
+ throws IOException {
+ final URI uri = request.getURI();
+ final HttpHost httpHost = new HttpHost(uri.getHost(), uri.getPort(),
+ uri.getScheme());
+ return execute(httpHost, request, context);
+ }
+
+ public <T> T execute(final HttpUriRequest request,
+ final ResponseHandler<? extends T> responseHandler) throws IOException {
+ return execute(request, responseHandler, null);
+ }
+
+ public <T> T execute(final HttpUriRequest request,
+ final ResponseHandler<? extends T> responseHandler, final HttpContext context)
+ throws IOException {
+ final HttpResponse resp = execute(request, context);
+ return responseHandler.handleResponse(resp);
+ }
+
+ public HttpResponse execute(final HttpHost target, final HttpRequest request,
+ final HttpContext context) throws IOException {
+ for (int c = 1;; c++) {
+ final HttpResponse response = backend.execute(target, request, context);
+ try {
+ if (retryStrategy.retryRequest(response, c, context)) {
+ EntityUtils.consume(response.getEntity());
+ final long nextInterval = retryStrategy.getRetryInterval();
+ try {
+ log.trace("Wait for " + nextInterval);
+ Thread.sleep(nextInterval);
+ } catch (final InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new InterruptedIOException();
+ }
+ } else {
+ return response;
+ }
+ } catch (final RuntimeException ex) {
+ try {
+ EntityUtils.consume(response.getEntity());
+ } catch (final IOException ioex) {
+ log.warn("I/O error consuming response content", ioex);
+ }
+ throw ex;
+ }
+ }
+ }
+
+ public ClientConnectionManager getConnectionManager() {
+ return backend.getConnectionManager();
+ }
+
+ public HttpParams getParams() {
+ return backend.getParams();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/BasicAuthCache.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/BasicAuthCache.java
new file mode 100644
index 0000000000..810f73a0a9
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/BasicAuthCache.java
@@ -0,0 +1,105 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.util.HashMap;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.auth.AuthScheme;
+import ch.boye.httpclientandroidlib.client.AuthCache;
+import ch.boye.httpclientandroidlib.conn.SchemePortResolver;
+import ch.boye.httpclientandroidlib.conn.UnsupportedSchemeException;
+import ch.boye.httpclientandroidlib.impl.conn.DefaultSchemePortResolver;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Default implementation of {@link AuthCache}.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class BasicAuthCache implements AuthCache {
+
+ private final HashMap<HttpHost, AuthScheme> map;
+ private final SchemePortResolver schemePortResolver;
+
+ /**
+ * Default constructor.
+ *
+ * @since 4.3
+ */
+ public BasicAuthCache(final SchemePortResolver schemePortResolver) {
+ super();
+ this.map = new HashMap<HttpHost, AuthScheme>();
+ this.schemePortResolver = schemePortResolver != null ? schemePortResolver :
+ DefaultSchemePortResolver.INSTANCE;
+ }
+
+ public BasicAuthCache() {
+ this(null);
+ }
+
+ protected HttpHost getKey(final HttpHost host) {
+ if (host.getPort() <= 0) {
+ final int port;
+ try {
+ port = schemePortResolver.resolve(host);
+ } catch (final UnsupportedSchemeException ignore) {
+ return host;
+ }
+ return new HttpHost(host.getHostName(), port, host.getSchemeName());
+ } else {
+ return host;
+ }
+ }
+
+ public void put(final HttpHost host, final AuthScheme authScheme) {
+ Args.notNull(host, "HTTP host");
+ this.map.put(getKey(host), authScheme);
+ }
+
+ public AuthScheme get(final HttpHost host) {
+ Args.notNull(host, "HTTP host");
+ return this.map.get(getKey(host));
+ }
+
+ public void remove(final HttpHost host) {
+ Args.notNull(host, "HTTP host");
+ this.map.remove(getKey(host));
+ }
+
+ public void clear() {
+ this.map.clear();
+ }
+
+ @Override
+ public String toString() {
+ return this.map.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/BasicCookieStore.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/BasicCookieStore.java
new file mode 100644
index 0000000000..624a8f81d8
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/BasicCookieStore.java
@@ -0,0 +1,144 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.TreeSet;
+
+import ch.boye.httpclientandroidlib.annotation.GuardedBy;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.client.CookieStore;
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+import ch.boye.httpclientandroidlib.cookie.CookieIdentityComparator;
+
+/**
+ * Default implementation of {@link CookieStore}
+ *
+ *
+ * @since 4.0
+ */
+@ThreadSafe
+public class BasicCookieStore implements CookieStore, Serializable {
+
+ private static final long serialVersionUID = -7581093305228232025L;
+
+ @GuardedBy("this")
+ private final TreeSet<Cookie> cookies;
+
+ public BasicCookieStore() {
+ super();
+ this.cookies = new TreeSet<Cookie>(new CookieIdentityComparator());
+ }
+
+ /**
+ * Adds an {@link Cookie HTTP cookie}, replacing any existing equivalent cookies.
+ * If the given cookie has already expired it will not be added, but existing
+ * values will still be removed.
+ *
+ * @param cookie the {@link Cookie cookie} to be added
+ *
+ * @see #addCookies(Cookie[])
+ *
+ */
+ public synchronized void addCookie(final Cookie cookie) {
+ if (cookie != null) {
+ // first remove any old cookie that is equivalent
+ cookies.remove(cookie);
+ if (!cookie.isExpired(new Date())) {
+ cookies.add(cookie);
+ }
+ }
+ }
+
+ /**
+ * Adds an array of {@link Cookie HTTP cookies}. Cookies are added individually and
+ * in the given array order. If any of the given cookies has already expired it will
+ * not be added, but existing values will still be removed.
+ *
+ * @param cookies the {@link Cookie cookies} to be added
+ *
+ * @see #addCookie(Cookie)
+ *
+ */
+ public synchronized void addCookies(final Cookie[] cookies) {
+ if (cookies != null) {
+ for (final Cookie cooky : cookies) {
+ this.addCookie(cooky);
+ }
+ }
+ }
+
+ /**
+ * Returns an immutable array of {@link Cookie cookies} that this HTTP
+ * state currently contains.
+ *
+ * @return an array of {@link Cookie cookies}.
+ */
+ public synchronized List<Cookie> getCookies() {
+ //create defensive copy so it won't be concurrently modified
+ return new ArrayList<Cookie>(cookies);
+ }
+
+ /**
+ * Removes all of {@link Cookie cookies} in this HTTP state
+ * that have expired by the specified {@link java.util.Date date}.
+ *
+ * @return true if any cookies were purged.
+ *
+ * @see Cookie#isExpired(Date)
+ */
+ public synchronized boolean clearExpired(final Date date) {
+ if (date == null) {
+ return false;
+ }
+ boolean removed = false;
+ for (final Iterator<Cookie> it = cookies.iterator(); it.hasNext();) {
+ if (it.next().isExpired(date)) {
+ it.remove();
+ removed = true;
+ }
+ }
+ return removed;
+ }
+
+ /**
+ * Clears all cookies.
+ */
+ public synchronized void clear() {
+ cookies.clear();
+ }
+
+ @Override
+ public synchronized String toString() {
+ return cookies.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/BasicCredentialsProvider.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/BasicCredentialsProvider.java
new file mode 100644
index 0000000000..99c5cccc28
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/BasicCredentialsProvider.java
@@ -0,0 +1,109 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.auth.AuthScope;
+import ch.boye.httpclientandroidlib.auth.Credentials;
+import ch.boye.httpclientandroidlib.client.CredentialsProvider;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Default implementation of {@link CredentialsProvider}.
+ *
+ * @since 4.0
+ */
+@ThreadSafe
+public class BasicCredentialsProvider implements CredentialsProvider {
+
+ private final ConcurrentHashMap<AuthScope, Credentials> credMap;
+
+ /**
+ * Default constructor.
+ */
+ public BasicCredentialsProvider() {
+ super();
+ this.credMap = new ConcurrentHashMap<AuthScope, Credentials>();
+ }
+
+ public void setCredentials(
+ final AuthScope authscope,
+ final Credentials credentials) {
+ Args.notNull(authscope, "Authentication scope");
+ credMap.put(authscope, credentials);
+ }
+
+ /**
+ * Find matching {@link Credentials credentials} for the given authentication scope.
+ *
+ * @param map the credentials hash map
+ * @param authscope the {@link AuthScope authentication scope}
+ * @return the credentials
+ *
+ */
+ private static Credentials matchCredentials(
+ final Map<AuthScope, Credentials> map,
+ final AuthScope authscope) {
+ // see if we get a direct hit
+ Credentials creds = map.get(authscope);
+ if (creds == null) {
+ // Nope.
+ // Do a full scan
+ int bestMatchFactor = -1;
+ AuthScope bestMatch = null;
+ for (final AuthScope current: map.keySet()) {
+ final int factor = authscope.match(current);
+ if (factor > bestMatchFactor) {
+ bestMatchFactor = factor;
+ bestMatch = current;
+ }
+ }
+ if (bestMatch != null) {
+ creds = map.get(bestMatch);
+ }
+ }
+ return creds;
+ }
+
+ public Credentials getCredentials(final AuthScope authscope) {
+ Args.notNull(authscope, "Authentication scope");
+ return matchCredentials(this.credMap, authscope);
+ }
+
+ public void clear() {
+ this.credMap.clear();
+ }
+
+ @Override
+ public String toString() {
+ return credMap.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/BasicResponseHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/BasicResponseHandler.java
new file mode 100644
index 0000000000..13ae4f8565
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/BasicResponseHandler.java
@@ -0,0 +1,73 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.StatusLine;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.HttpResponseException;
+import ch.boye.httpclientandroidlib.client.ResponseHandler;
+import ch.boye.httpclientandroidlib.util.EntityUtils;
+
+/**
+ * A {@link ResponseHandler} that returns the response body as a String
+ * for successful (2xx) responses. If the response code was >= 300, the response
+ * body is consumed and an {@link HttpResponseException} is thrown.
+ * <p/>
+ * If this is used with
+ * {@link ch.boye.httpclientandroidlib.client.HttpClient#execute(
+ * ch.boye.httpclientandroidlib.client.methods.HttpUriRequest, ResponseHandler)},
+ * HttpClient may handle redirects (3xx responses) internally.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class BasicResponseHandler implements ResponseHandler<String> {
+
+ /**
+ * Returns the response body as a String if the response was successful (a
+ * 2xx status code). If no response body exists, this returns null. If the
+ * response was unsuccessful (>= 300 status code), throws an
+ * {@link HttpResponseException}.
+ */
+ public String handleResponse(final HttpResponse response)
+ throws HttpResponseException, IOException {
+ final StatusLine statusLine = response.getStatusLine();
+ final HttpEntity entity = response.getEntity();
+ if (statusLine.getStatusCode() >= 300) {
+ EntityUtils.consume(entity);
+ throw new HttpResponseException(statusLine.getStatusCode(),
+ statusLine.getReasonPhrase());
+ }
+ return entity == null ? null : EntityUtils.toString(entity);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/ClientParamsStack.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/ClientParamsStack.java
new file mode 100644
index 0000000000..37f5daedd6
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/ClientParamsStack.java
@@ -0,0 +1,269 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.params.AbstractHttpParams;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Represents a stack of parameter collections.
+ * When retrieving a parameter, the stack is searched in a fixed order
+ * and the first match returned. Setting parameters via the stack is
+ * not supported. To minimize overhead, the stack has a fixed size and
+ * does not maintain an internal array.
+ * The supported stack entries, sorted by increasing priority, are:
+ * <ol>
+ * <li>Application parameters:
+ * expected to be the same for all clients used by an application.
+ * These provide "global", that is application-wide, defaults.
+ * </li>
+ * <li>Client parameters:
+ * specific to an instance of
+ * {@link ch.boye.httpclientandroidlib.client.HttpClient HttpClient}.
+ * These provide client specific defaults.
+ * </li>
+ * <li>Request parameters:
+ * specific to a single request execution.
+ * For overriding client and global defaults.
+ * </li>
+ * <li>Override parameters:
+ * specific to an instance of
+ * {@link ch.boye.httpclientandroidlib.client.HttpClient HttpClient}.
+ * These can be used to set parameters that cannot be overridden
+ * on a per-request basis.
+ * </li>
+ * </ol>
+ * Each stack entry may be <code>null</code>. That is preferable over
+ * an empty params collection, since it avoids searching the empty collection
+ * when looking up parameters.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use configuration classes provided 'ch.boye.httpclientandroidlib.config'
+ * and 'ch.boye.httpclientandroidlib.client.config'
+ */
+@NotThreadSafe
+@Deprecated
+public class ClientParamsStack extends AbstractHttpParams {
+
+ /** The application parameter collection, or <code>null</code>. */
+ protected final HttpParams applicationParams;
+
+ /** The client parameter collection, or <code>null</code>. */
+ protected final HttpParams clientParams;
+
+ /** The request parameter collection, or <code>null</code>. */
+ protected final HttpParams requestParams;
+
+ /** The override parameter collection, or <code>null</code>. */
+ protected final HttpParams overrideParams;
+
+
+ /**
+ * Creates a new parameter stack from elements.
+ * The arguments will be stored as-is, there is no copying to
+ * prevent modification.
+ *
+ * @param aparams application parameters, or <code>null</code>
+ * @param cparams client parameters, or <code>null</code>
+ * @param rparams request parameters, or <code>null</code>
+ * @param oparams override parameters, or <code>null</code>
+ */
+ public ClientParamsStack(final HttpParams aparams, final HttpParams cparams,
+ final HttpParams rparams, final HttpParams oparams) {
+ applicationParams = aparams;
+ clientParams = cparams;
+ requestParams = rparams;
+ overrideParams = oparams;
+ }
+
+
+ /**
+ * Creates a copy of a parameter stack.
+ * The new stack will have the exact same entries as the argument stack.
+ * There is no copying of parameters.
+ *
+ * @param stack the stack to copy
+ */
+ public ClientParamsStack(final ClientParamsStack stack) {
+ this(stack.getApplicationParams(),
+ stack.getClientParams(),
+ stack.getRequestParams(),
+ stack.getOverrideParams());
+ }
+
+
+ /**
+ * Creates a modified copy of a parameter stack.
+ * The new stack will contain the explicitly passed elements.
+ * For elements where the explicit argument is <code>null</code>,
+ * the corresponding element from the argument stack is used.
+ * There is no copying of parameters.
+ *
+ * @param stack the stack to modify
+ * @param aparams application parameters, or <code>null</code>
+ * @param cparams client parameters, or <code>null</code>
+ * @param rparams request parameters, or <code>null</code>
+ * @param oparams override parameters, or <code>null</code>
+ */
+ public ClientParamsStack(final ClientParamsStack stack,
+ final HttpParams aparams, final HttpParams cparams,
+ final HttpParams rparams, final HttpParams oparams) {
+ this((aparams != null) ? aparams : stack.getApplicationParams(),
+ (cparams != null) ? cparams : stack.getClientParams(),
+ (rparams != null) ? rparams : stack.getRequestParams(),
+ (oparams != null) ? oparams : stack.getOverrideParams());
+ }
+
+
+ /**
+ * Obtains the application parameters of this stack.
+ *
+ * @return the application parameters, or <code>null</code>
+ */
+ public final HttpParams getApplicationParams() {
+ return applicationParams;
+ }
+
+ /**
+ * Obtains the client parameters of this stack.
+ *
+ * @return the client parameters, or <code>null</code>
+ */
+ public final HttpParams getClientParams() {
+ return clientParams;
+ }
+
+ /**
+ * Obtains the request parameters of this stack.
+ *
+ * @return the request parameters, or <code>null</code>
+ */
+ public final HttpParams getRequestParams() {
+ return requestParams;
+ }
+
+ /**
+ * Obtains the override parameters of this stack.
+ *
+ * @return the override parameters, or <code>null</code>
+ */
+ public final HttpParams getOverrideParams() {
+ return overrideParams;
+ }
+
+
+ /**
+ * Obtains a parameter from this stack.
+ * See class comment for search order.
+ *
+ * @param name the name of the parameter to obtain
+ *
+ * @return the highest-priority value for that parameter, or
+ * <code>null</code> if it is not set anywhere in this stack
+ */
+ public Object getParameter(final String name) {
+ Args.notNull(name, "Parameter name");
+
+ Object result = null;
+
+ if (overrideParams != null) {
+ result = overrideParams.getParameter(name);
+ }
+ if ((result == null) && (requestParams != null)) {
+ result = requestParams.getParameter(name);
+ }
+ if ((result == null) && (clientParams != null)) {
+ result = clientParams.getParameter(name);
+ }
+ if ((result == null) && (applicationParams != null)) {
+ result = applicationParams.getParameter(name);
+ }
+ return result;
+ }
+
+ /**
+ * Does <i>not</i> set a parameter.
+ * Parameter stacks are read-only. It is possible, though discouraged,
+ * to access and modify specific stack entries.
+ * Derived classes may change this behavior.
+ *
+ * @param name ignored
+ * @param value ignored
+ *
+ * @return nothing
+ *
+ * @throws UnsupportedOperationException always
+ */
+ public HttpParams setParameter(final String name, final Object value)
+ throws UnsupportedOperationException {
+
+ throw new UnsupportedOperationException
+ ("Setting parameters in a stack is not supported.");
+ }
+
+
+ /**
+ * Does <i>not</i> remove a parameter.
+ * Parameter stacks are read-only. It is possible, though discouraged,
+ * to access and modify specific stack entries.
+ * Derived classes may change this behavior.
+ *
+ * @param name ignored
+ *
+ * @return nothing
+ *
+ * @throws UnsupportedOperationException always
+ */
+ public boolean removeParameter(final String name) {
+ throw new UnsupportedOperationException
+ ("Removing parameters in a stack is not supported.");
+ }
+
+
+ /**
+ * Does <i>not</i> copy parameters.
+ * Parameter stacks are lightweight objects, expected to be instantiated
+ * as needed and to be used only in a very specific context. On top of
+ * that, they are read-only. The typical copy operation to prevent
+ * accidental modification of parameters passed by the application to
+ * a framework object is therefore pointless and disabled.
+ * Create a new stack if you really need a copy.
+ * <br/>
+ * Derived classes may change this behavior.
+ *
+ * @return <code>this</code> parameter stack
+ */
+ public HttpParams copy() {
+ return this;
+ }
+
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/Clock.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/Clock.java
new file mode 100644
index 0000000000..14eeec4a02
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/Clock.java
@@ -0,0 +1,43 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client;
+
+/**
+ * Interface used to enable easier testing of time-related behavior.
+ *
+ * @since 4.2
+ *
+ */
+interface Clock {
+
+ /**
+ * Returns the current time, expressed as the number of
+ * milliseconds since the epoch.
+ * @return current time
+ */
+ long getCurrentTime();
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/CloseableHttpClient.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/CloseableHttpClient.java
new file mode 100644
index 0000000000..c2fc5a5de5
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/CloseableHttpClient.java
@@ -0,0 +1,244 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.reflect.UndeclaredThrowableException;
+import java.net.URI;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+import ch.boye.httpclientandroidlib.client.HttpClient;
+import ch.boye.httpclientandroidlib.client.ResponseHandler;
+import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.client.utils.URIUtils;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.EntityUtils;
+
+/**
+ * Base implementation of {@link HttpClient} that also implements {@link Closeable}.
+ *
+ * @since 4.3
+ */
+@ThreadSafe
+public abstract class CloseableHttpClient implements HttpClient, Closeable {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ protected abstract CloseableHttpResponse doExecute(HttpHost target, HttpRequest request,
+ HttpContext context) throws IOException, ClientProtocolException;
+
+ /**
+ * {@inheritDoc}
+ */
+ public CloseableHttpResponse execute(
+ final HttpHost target,
+ final HttpRequest request,
+ final HttpContext context) throws IOException, ClientProtocolException {
+ return doExecute(target, request, context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public CloseableHttpResponse execute(
+ final HttpUriRequest request,
+ final HttpContext context) throws IOException, ClientProtocolException {
+ Args.notNull(request, "HTTP request");
+ return doExecute(determineTarget(request), request, context);
+ }
+
+ private static HttpHost determineTarget(final HttpUriRequest request) throws ClientProtocolException {
+ // A null target may be acceptable if there is a default target.
+ // Otherwise, the null target is detected in the director.
+ HttpHost target = null;
+
+ final URI requestURI = request.getURI();
+ if (requestURI.isAbsolute()) {
+ target = URIUtils.extractHost(requestURI);
+ if (target == null) {
+ throw new ClientProtocolException("URI does not specify a valid host name: "
+ + requestURI);
+ }
+ }
+ return target;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public CloseableHttpResponse execute(
+ final HttpUriRequest request) throws IOException, ClientProtocolException {
+ return execute(request, (HttpContext) null);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public CloseableHttpResponse execute(
+ final HttpHost target,
+ final HttpRequest request) throws IOException, ClientProtocolException {
+ return doExecute(target, request, (HttpContext) null);
+ }
+
+ /**
+ * Executes a request using the default context and processes the
+ * response using the given response handler. The content entity associated
+ * with the response is fully consumed and the underlying connection is
+ * released back to the connection manager automatically in all cases
+ * relieving individual {@link ResponseHandler}s from having to manage
+ * resource deallocation internally.
+ *
+ * @param request the request to execute
+ * @param responseHandler the response handler
+ *
+ * @return the response object as generated by the response handler.
+ * @throws IOException in case of a problem or the connection was aborted
+ * @throws ClientProtocolException in case of an http protocol error
+ */
+ public <T> T execute(final HttpUriRequest request,
+ final ResponseHandler<? extends T> responseHandler) throws IOException,
+ ClientProtocolException {
+ return execute(request, responseHandler, null);
+ }
+
+ /**
+ * Executes a request using the default context and processes the
+ * response using the given response handler. The content entity associated
+ * with the response is fully consumed and the underlying connection is
+ * released back to the connection manager automatically in all cases
+ * relieving individual {@link ResponseHandler}s from having to manage
+ * resource deallocation internally.
+ *
+ * @param request the request to execute
+ * @param responseHandler the response handler
+ * @param context the context to use for the execution, or
+ * <code>null</code> to use the default context
+ *
+ * @return the response object as generated by the response handler.
+ * @throws IOException in case of a problem or the connection was aborted
+ * @throws ClientProtocolException in case of an http protocol error
+ */
+ public <T> T execute(final HttpUriRequest request,
+ final ResponseHandler<? extends T> responseHandler, final HttpContext context)
+ throws IOException, ClientProtocolException {
+ final HttpHost target = determineTarget(request);
+ return execute(target, request, responseHandler, context);
+ }
+
+ /**
+ * Executes a request using the default context and processes the
+ * response using the given response handler. The content entity associated
+ * with the response is fully consumed and the underlying connection is
+ * released back to the connection manager automatically in all cases
+ * relieving individual {@link ResponseHandler}s from having to manage
+ * resource deallocation internally.
+ *
+ * @param target the target host for the request.
+ * Implementations may accept <code>null</code>
+ * if they can still determine a route, for example
+ * to a default target or by inspecting the request.
+ * @param request the request to execute
+ * @param responseHandler the response handler
+ *
+ * @return the response object as generated by the response handler.
+ * @throws IOException in case of a problem or the connection was aborted
+ * @throws ClientProtocolException in case of an http protocol error
+ */
+ public <T> T execute(final HttpHost target, final HttpRequest request,
+ final ResponseHandler<? extends T> responseHandler) throws IOException,
+ ClientProtocolException {
+ return execute(target, request, responseHandler, null);
+ }
+
+ /**
+ * Executes a request using the default context and processes the
+ * response using the given response handler. The content entity associated
+ * with the response is fully consumed and the underlying connection is
+ * released back to the connection manager automatically in all cases
+ * relieving individual {@link ResponseHandler}s from having to manage
+ * resource deallocation internally.
+ *
+ * @param target the target host for the request.
+ * Implementations may accept <code>null</code>
+ * if they can still determine a route, for example
+ * to a default target or by inspecting the request.
+ * @param request the request to execute
+ * @param responseHandler the response handler
+ * @param context the context to use for the execution, or
+ * <code>null</code> to use the default context
+ *
+ * @return the response object as generated by the response handler.
+ * @throws IOException in case of a problem or the connection was aborted
+ * @throws ClientProtocolException in case of an http protocol error
+ */
+ public <T> T execute(final HttpHost target, final HttpRequest request,
+ final ResponseHandler<? extends T> responseHandler, final HttpContext context)
+ throws IOException, ClientProtocolException {
+ Args.notNull(responseHandler, "Response handler");
+
+ final HttpResponse response = execute(target, request, context);
+
+ final T result;
+ try {
+ result = responseHandler.handleResponse(response);
+ } catch (final Exception t) {
+ final HttpEntity entity = response.getEntity();
+ try {
+ EntityUtils.consume(entity);
+ } catch (final Exception t2) {
+ // Log this exception. The original exception is more
+ // important and will be thrown to the caller.
+ this.log.warn("Error consuming content after an exception.", t2);
+ }
+ if (t instanceof RuntimeException) {
+ throw (RuntimeException) t;
+ }
+ if (t instanceof IOException) {
+ throw (IOException) t;
+ }
+ throw new UndeclaredThrowableException(t);
+ }
+
+ // Handling the response was successful. Ensure that the content has
+ // been fully consumed.
+ final HttpEntity entity = response.getEntity();
+ EntityUtils.consume(entity);
+ return result;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/CloseableHttpResponseProxy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/CloseableHttpResponseProxy.java
new file mode 100644
index 0000000000..9869740dfb
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/CloseableHttpResponseProxy.java
@@ -0,0 +1,87 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
+import ch.boye.httpclientandroidlib.util.EntityUtils;
+
+/**
+ * @since 4.3
+ */
+@NotThreadSafe
+class CloseableHttpResponseProxy implements InvocationHandler {
+
+ private final HttpResponse original;
+
+ CloseableHttpResponseProxy(final HttpResponse original) {
+ super();
+ this.original = original;
+ }
+
+ public void close() throws IOException {
+ final HttpEntity entity = this.original.getEntity();
+ EntityUtils.consume(entity);
+ }
+
+ public Object invoke(
+ final Object proxy, final Method method, final Object[] args) throws Throwable {
+ final String mname = method.getName();
+ if (mname.equals("close")) {
+ close();
+ return null;
+ } else {
+ try {
+ return method.invoke(original, args);
+ } catch (final InvocationTargetException ex) {
+ final Throwable cause = ex.getCause();
+ if (cause != null) {
+ throw cause;
+ } else {
+ throw ex;
+ }
+ }
+ }
+ }
+
+ public static CloseableHttpResponse newProxy(final HttpResponse original) {
+ return (CloseableHttpResponse) Proxy.newProxyInstance(
+ CloseableHttpResponseProxy.class.getClassLoader(),
+ new Class<?>[] { CloseableHttpResponse.class },
+ new CloseableHttpResponseProxy(original));
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/ContentEncodingHttpClient.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/ContentEncodingHttpClient.java
new file mode 100644
index 0000000000..f4169225e5
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/ContentEncodingHttpClient.java
@@ -0,0 +1,93 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client;
+
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.client.protocol.RequestAcceptEncoding;
+import ch.boye.httpclientandroidlib.client.protocol.ResponseContentEncoding;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionManager;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.BasicHttpProcessor;
+
+/**
+ * {@link DefaultHttpClient} sub-class which includes a {@link RequestAcceptEncoding}
+ * for the request and response.
+ *
+ * <b>Deprecation note:</b> due to the way this class modifies a response body
+ * without changing the response headers to reflect the entity changes, it cannot
+ * be used as the &quot;backend&quot; for a caching {@link
+ * ch.boye.httpclientandroidlib.client.HttpClient} and still have uncompressed responses be cached.
+ * Users are encouraged to use the {@link DecompressingHttpClient} instead
+ * of this class, which can be wired in either before or after caching, depending on
+ * whether you want to cache responses in compressed or uncompressed form.
+ *
+ * @since 4.1
+ *
+ * @deprecated (4.2) use {@link HttpClientBuilder}
+ */
+@Deprecated
+@ThreadSafe // since DefaultHttpClient is
+public class ContentEncodingHttpClient extends DefaultHttpClient {
+
+ /**
+ * Creates a new HTTP client from parameters and a connection manager.
+ *
+ * @param params the parameters
+ * @param conman the connection manager
+ */
+ public ContentEncodingHttpClient(final ClientConnectionManager conman, final HttpParams params) {
+ super(conman, params);
+ }
+
+ /**
+ * @param params
+ */
+ public ContentEncodingHttpClient(final HttpParams params) {
+ this(null, params);
+ }
+
+ /**
+ *
+ */
+ public ContentEncodingHttpClient() {
+ this(null);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected BasicHttpProcessor createHttpProcessor() {
+ final BasicHttpProcessor result = super.createHttpProcessor();
+
+ result.addRequestInterceptor(new RequestAcceptEncoding());
+ result.addResponseInterceptor(new ResponseContentEncoding());
+
+ return result;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DecompressingHttpClient.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DecompressingHttpClient.java
new file mode 100644
index 0000000000..4d55303ada
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DecompressingHttpClient.java
@@ -0,0 +1,214 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.io.IOException;
+import java.net.URI;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpRequestInterceptor;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpResponseInterceptor;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+import ch.boye.httpclientandroidlib.client.HttpClient;
+import ch.boye.httpclientandroidlib.client.ResponseHandler;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.client.protocol.RequestAcceptEncoding;
+import ch.boye.httpclientandroidlib.client.protocol.ResponseContentEncoding;
+import ch.boye.httpclientandroidlib.client.utils.URIUtils;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionManager;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.EntityUtils;
+
+/**
+ * <p>Decorator adding support for compressed responses. This class sets
+ * the <code>Accept-Encoding</code> header on requests to indicate
+ * support for the <code>gzip</code> and <code>deflate</code>
+ * compression schemes; it then checks the <code>Content-Encoding</code>
+ * header on the response to uncompress any compressed response bodies.
+ * The {@link java.io.InputStream} of the entity will contain the uncompressed
+ * content.</p>
+ *
+ * <p><b>N.B.</b> Any upstream clients of this class need to be aware that
+ * this effectively obscures visibility into the length of a server
+ * response body, since the <code>Content-Length</code> header will
+ * correspond to the compressed entity length received from the server,
+ * but the content length experienced by reading the response body may
+ * be different (hopefully higher!).</p>
+ *
+ * <p>That said, this decorator is compatible with the
+ * <code>CachingHttpClient</code> in that the two decorators can be added
+ * in either order and still have cacheable responses be cached.</p>
+ *
+ * @since 4.2
+ *
+ * @deprecated (4.3) use {@link HttpClientBuilder}
+ */
+@Deprecated
+public class DecompressingHttpClient implements HttpClient {
+
+ private final HttpClient backend;
+ private final HttpRequestInterceptor acceptEncodingInterceptor;
+ private final HttpResponseInterceptor contentEncodingInterceptor;
+
+ /**
+ * Constructs a decorator to ask for and handle compressed
+ * entities on the fly.
+ */
+ public DecompressingHttpClient() {
+ this(new DefaultHttpClient());
+ }
+
+ /**
+ * Constructs a decorator to ask for and handle compressed
+ * entities on the fly.
+ * @param backend the {@link HttpClient} to use for actually
+ * issuing requests
+ */
+ public DecompressingHttpClient(final HttpClient backend) {
+ this(backend, new RequestAcceptEncoding(), new ResponseContentEncoding());
+ }
+
+ DecompressingHttpClient(final HttpClient backend,
+ final HttpRequestInterceptor requestInterceptor,
+ final HttpResponseInterceptor responseInterceptor) {
+ this.backend = backend;
+ this.acceptEncodingInterceptor = requestInterceptor;
+ this.contentEncodingInterceptor = responseInterceptor;
+ }
+
+ public HttpParams getParams() {
+ return backend.getParams();
+ }
+
+ public ClientConnectionManager getConnectionManager() {
+ return backend.getConnectionManager();
+ }
+
+ public HttpResponse execute(final HttpUriRequest request) throws IOException,
+ ClientProtocolException {
+ return execute(getHttpHost(request), request, (HttpContext)null);
+ }
+
+ /**
+ * Gets the HttpClient to issue request.
+ *
+ * @return the HttpClient to issue request
+ */
+ public HttpClient getHttpClient() {
+ return this.backend;
+ }
+
+ HttpHost getHttpHost(final HttpUriRequest request) {
+ final URI uri = request.getURI();
+ return URIUtils.extractHost(uri);
+ }
+
+ public HttpResponse execute(final HttpUriRequest request, final HttpContext context)
+ throws IOException, ClientProtocolException {
+ return execute(getHttpHost(request), request, context);
+ }
+
+ public HttpResponse execute(final HttpHost target, final HttpRequest request)
+ throws IOException, ClientProtocolException {
+ return execute(target, request, (HttpContext)null);
+ }
+
+ public HttpResponse execute(final HttpHost target, final HttpRequest request,
+ final HttpContext context) throws IOException, ClientProtocolException {
+ try {
+ final HttpContext localContext = context != null ? context : new BasicHttpContext();
+ final HttpRequest wrapped;
+ if (request instanceof HttpEntityEnclosingRequest) {
+ wrapped = new EntityEnclosingRequestWrapper((HttpEntityEnclosingRequest) request);
+ } else {
+ wrapped = new RequestWrapper(request);
+ }
+ acceptEncodingInterceptor.process(wrapped, localContext);
+ final HttpResponse response = backend.execute(target, wrapped, localContext);
+ try {
+ contentEncodingInterceptor.process(response, localContext);
+ if (Boolean.TRUE.equals(localContext.getAttribute(ResponseContentEncoding.UNCOMPRESSED))) {
+ response.removeHeaders("Content-Length");
+ response.removeHeaders("Content-Encoding");
+ response.removeHeaders("Content-MD5");
+ }
+ return response;
+ } catch (final HttpException ex) {
+ EntityUtils.consume(response.getEntity());
+ throw ex;
+ } catch (final IOException ex) {
+ EntityUtils.consume(response.getEntity());
+ throw ex;
+ } catch (final RuntimeException ex) {
+ EntityUtils.consume(response.getEntity());
+ throw ex;
+ }
+ } catch (final HttpException e) {
+ throw new ClientProtocolException(e);
+ }
+ }
+
+ public <T> T execute(final HttpUriRequest request,
+ final ResponseHandler<? extends T> responseHandler) throws IOException,
+ ClientProtocolException {
+ return execute(getHttpHost(request), request, responseHandler);
+ }
+
+ public <T> T execute(final HttpUriRequest request,
+ final ResponseHandler<? extends T> responseHandler, final HttpContext context)
+ throws IOException, ClientProtocolException {
+ return execute(getHttpHost(request), request, responseHandler, context);
+ }
+
+ public <T> T execute(final HttpHost target, final HttpRequest request,
+ final ResponseHandler<? extends T> responseHandler) throws IOException,
+ ClientProtocolException {
+ return execute(target, request, responseHandler, null);
+ }
+
+ public <T> T execute(final HttpHost target, final HttpRequest request,
+ final ResponseHandler<? extends T> responseHandler, final HttpContext context)
+ throws IOException, ClientProtocolException {
+ final HttpResponse response = execute(target, request, context);
+ try {
+ return responseHandler.handleResponse(response);
+ } finally {
+ final HttpEntity entity = response.getEntity();
+ if (entity != null) {
+ EntityUtils.consume(entity);
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultBackoffStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultBackoffStrategy.java
new file mode 100644
index 0000000000..5a5e67caa3
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultBackoffStrategy.java
@@ -0,0 +1,54 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.net.ConnectException;
+import java.net.SocketTimeoutException;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.client.ConnectionBackoffStrategy;
+
+/**
+ * This {@link ConnectionBackoffStrategy} backs off either for a raw
+ * network socket or connection timeout or if the server explicitly
+ * sends a 503 (Service Unavailable) response.
+ *
+ * @since 4.2
+ */
+public class DefaultBackoffStrategy implements ConnectionBackoffStrategy {
+
+ public boolean shouldBackoff(final Throwable t) {
+ return (t instanceof SocketTimeoutException
+ || t instanceof ConnectException);
+ }
+
+ public boolean shouldBackoff(final HttpResponse resp) {
+ return (resp.getStatusLine().getStatusCode() == HttpStatus.SC_SERVICE_UNAVAILABLE);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultConnectionKeepAliveStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultConnectionKeepAliveStrategy.java
new file mode 100644
index 0000000000..a5524bd435
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultConnectionKeepAliveStrategy.java
@@ -0,0 +1,71 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client;
+
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.HeaderElementIterator;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.conn.ConnectionKeepAliveStrategy;
+import ch.boye.httpclientandroidlib.message.BasicHeaderElementIterator;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Default implementation of a strategy deciding duration
+ * that a connection can remain idle.
+ *
+ * The default implementation looks solely at the 'Keep-Alive'
+ * header's timeout token.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class DefaultConnectionKeepAliveStrategy implements ConnectionKeepAliveStrategy {
+
+ public static final DefaultConnectionKeepAliveStrategy INSTANCE = new DefaultConnectionKeepAliveStrategy();
+
+ public long getKeepAliveDuration(final HttpResponse response, final HttpContext context) {
+ Args.notNull(response, "HTTP response");
+ final HeaderElementIterator it = new BasicHeaderElementIterator(
+ response.headerIterator(HTTP.CONN_KEEP_ALIVE));
+ while (it.hasNext()) {
+ final HeaderElement he = it.nextElement();
+ final String param = he.getName();
+ final String value = he.getValue();
+ if (value != null && param.equalsIgnoreCase("timeout")) {
+ try {
+ return Long.parseLong(value) * 1000;
+ } catch(final NumberFormatException ignore) {
+ }
+ }
+ }
+ return -1;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultHttpClient.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultHttpClient.java
new file mode 100644
index 0000000000..55e5ab07ea
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultHttpClient.java
@@ -0,0 +1,225 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.client.protocol.RequestAddCookies;
+import ch.boye.httpclientandroidlib.client.protocol.RequestAuthCache;
+import ch.boye.httpclientandroidlib.client.protocol.RequestClientConnControl;
+import ch.boye.httpclientandroidlib.client.protocol.RequestDefaultHeaders;
+import ch.boye.httpclientandroidlib.client.protocol.RequestProxyAuthentication;
+import ch.boye.httpclientandroidlib.client.protocol.RequestTargetAuthentication;
+import ch.boye.httpclientandroidlib.client.protocol.ResponseProcessCookies;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionManager;
+import ch.boye.httpclientandroidlib.params.HttpConnectionParams;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.params.HttpProtocolParams;
+import ch.boye.httpclientandroidlib.params.SyncBasicHttpParams;
+import ch.boye.httpclientandroidlib.protocol.BasicHttpProcessor;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.protocol.RequestContent;
+import ch.boye.httpclientandroidlib.protocol.RequestExpectContinue;
+import ch.boye.httpclientandroidlib.protocol.RequestTargetHost;
+import ch.boye.httpclientandroidlib.protocol.RequestUserAgent;
+
+/**
+ * Default implementation of {@link ch.boye.httpclientandroidlib.client.HttpClient} pre-configured
+ * for most common use scenarios.
+ * <p>
+ * Please see the Javadoc for {@link #createHttpProcessor()} for the details of the interceptors
+ * that are set up by default.
+ * <p>
+ * Additional interceptors can be added as follows, but
+ * take care not to add the same interceptor more than once.
+ * <pre>
+ * DefaultHttpClient httpclient = new DefaultHttpClient();
+ * httpclient.addRequestInterceptor(new RequestAcceptEncoding());
+ * httpclient.addResponseInterceptor(new ResponseContentEncoding());
+ * </pre>
+ * <p>
+ * This class sets up the following parameters if not explicitly set:
+ * <ul>
+ * <li>Version: HttpVersion.HTTP_1_1</li>
+ * <li>ContentCharset: HTTP.DEFAULT_CONTENT_CHARSET</li>
+ * <li>NoTcpDelay: true</li>
+ * <li>SocketBufferSize: 8192</li>
+ * <li>UserAgent: Apache-HttpClient/release (java 1.5)</li>
+ * </ul>
+ * <p>
+ * The following parameters can be used to customize the behavior of this
+ * class:
+ * <ul>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#PROTOCOL_VERSION}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#STRICT_TRANSFER_ENCODING}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#HTTP_ELEMENT_CHARSET}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#USE_EXPECT_CONTINUE}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#WAIT_FOR_CONTINUE}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#USER_AGENT}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#TCP_NODELAY}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#SO_TIMEOUT}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#SO_LINGER}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#SO_REUSEADDR}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#SOCKET_BUFFER_SIZE}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#CONNECTION_TIMEOUT}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#MAX_LINE_LENGTH}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#MAX_HEADER_COUNT}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#STALE_CONNECTION_CHECK}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames#FORCED_ROUTE}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames#LOCAL_ADDRESS}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames#DEFAULT_PROXY}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.cookie.params.CookieSpecPNames#DATE_PATTERNS}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.cookie.params.CookieSpecPNames#SINGLE_COOKIE_HEADER}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.auth.params.AuthPNames#CREDENTIAL_CHARSET}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#COOKIE_POLICY}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#HANDLE_AUTHENTICATION}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#HANDLE_REDIRECTS}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#MAX_REDIRECTS}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#ALLOW_CIRCULAR_REDIRECTS}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#VIRTUAL_HOST}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#DEFAULT_HOST}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#DEFAULT_HEADERS}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#CONN_MANAGER_TIMEOUT}</li>
+ * </ul>
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link HttpClientBuilder}.
+ */
+@ThreadSafe
+@Deprecated
+public class DefaultHttpClient extends AbstractHttpClient {
+
+ /**
+ * Creates a new HTTP client from parameters and a connection manager.
+ *
+ * @param params the parameters
+ * @param conman the connection manager
+ */
+ public DefaultHttpClient(
+ final ClientConnectionManager conman,
+ final HttpParams params) {
+ super(conman, params);
+ }
+
+
+ /**
+ * @since 4.1
+ */
+ public DefaultHttpClient(
+ final ClientConnectionManager conman) {
+ super(conman, null);
+ }
+
+
+ public DefaultHttpClient(final HttpParams params) {
+ super(null, params);
+ }
+
+
+ public DefaultHttpClient() {
+ super(null, null);
+ }
+
+
+ /**
+ * Creates the default set of HttpParams by invoking {@link DefaultHttpClient#setDefaultHttpParams(HttpParams)}
+ *
+ * @return a new instance of {@link SyncBasicHttpParams} with the defaults applied to it.
+ */
+ @Override
+ protected HttpParams createHttpParams() {
+ final HttpParams params = new SyncBasicHttpParams();
+ setDefaultHttpParams(params);
+ return params;
+ }
+
+ /**
+ * Saves the default set of HttpParams in the provided parameter.
+ * These are:
+ * <ul>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#PROTOCOL_VERSION}:
+ * 1.1</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#HTTP_CONTENT_CHARSET}:
+ * ISO-8859-1</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#TCP_NODELAY}:
+ * true</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#SOCKET_BUFFER_SIZE}:
+ * 8192</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#USER_AGENT}:
+ * Apache-HttpClient/<release> (java 1.5)</li>
+ * </ul>
+ */
+ public static void setDefaultHttpParams(final HttpParams params) {
+ HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
+ HttpProtocolParams.setContentCharset(params, HTTP.DEF_CONTENT_CHARSET.name());
+ HttpConnectionParams.setTcpNoDelay(params, true);
+ HttpConnectionParams.setSocketBufferSize(params, 8192);
+ HttpProtocolParams.setUserAgent(params, HttpClientBuilder.DEFAULT_USER_AGENT);
+ }
+
+ /**
+ * Create the processor with the following interceptors:
+ * <ul>
+ * <li>{@link RequestDefaultHeaders}</li>
+ * <li>{@link RequestContent}</li>
+ * <li>{@link RequestTargetHost}</li>
+ * <li>{@link RequestClientConnControl}</li>
+ * <li>{@link RequestUserAgent}</li>
+ * <li>{@link RequestExpectContinue}</li>
+ * <li>{@link RequestAddCookies}</li>
+ * <li>{@link ResponseProcessCookies}</li>
+ * <li>{@link RequestAuthCache}</li>
+ * <li>{@link RequestTargetAuthentication}</li>
+ * <li>{@link RequestProxyAuthentication}</li>
+ * </ul>
+ * <p>
+ * @return the processor with the added interceptors.
+ */
+ @Override
+ protected BasicHttpProcessor createHttpProcessor() {
+ final BasicHttpProcessor httpproc = new BasicHttpProcessor();
+ httpproc.addInterceptor(new RequestDefaultHeaders());
+ // Required protocol interceptors
+ httpproc.addInterceptor(new RequestContent());
+ httpproc.addInterceptor(new RequestTargetHost());
+ // Recommended protocol interceptors
+ httpproc.addInterceptor(new RequestClientConnControl());
+ httpproc.addInterceptor(new RequestUserAgent());
+ httpproc.addInterceptor(new RequestExpectContinue());
+ // HTTP state management interceptors
+ httpproc.addInterceptor(new RequestAddCookies());
+ httpproc.addInterceptor(new ResponseProcessCookies());
+ // HTTP authentication interceptors
+ httpproc.addInterceptor(new RequestAuthCache());
+ httpproc.addInterceptor(new RequestTargetAuthentication());
+ httpproc.addInterceptor(new RequestProxyAuthentication());
+ return httpproc;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultHttpRequestRetryHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultHttpRequestRetryHandler.java
new file mode 100644
index 0000000000..1721c8e70a
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultHttpRequestRetryHandler.java
@@ -0,0 +1,203 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.ConnectException;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.net.ssl.SSLException;
+
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.HttpRequestRetryHandler;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * The default {@link HttpRequestRetryHandler} used by request executors.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class DefaultHttpRequestRetryHandler implements HttpRequestRetryHandler {
+
+ public static final DefaultHttpRequestRetryHandler INSTANCE = new DefaultHttpRequestRetryHandler();
+
+ /** the number of times a method will be retried */
+ private final int retryCount;
+
+ /** Whether or not methods that have successfully sent their request will be retried */
+ private final boolean requestSentRetryEnabled;
+
+ private final Set<Class<? extends IOException>> nonRetriableClasses;
+
+ /**
+ * Create the request retry handler using the specified IOException classes
+ *
+ * @param retryCount how many times to retry; 0 means no retries
+ * @param requestSentRetryEnabled true if it's OK to retry requests that have been sent
+ * @param clazzes the IOException types that should not be retried
+ * @since 4.3
+ */
+ protected DefaultHttpRequestRetryHandler(
+ final int retryCount,
+ final boolean requestSentRetryEnabled,
+ final Collection<Class<? extends IOException>> clazzes) {
+ super();
+ this.retryCount = retryCount;
+ this.requestSentRetryEnabled = requestSentRetryEnabled;
+ this.nonRetriableClasses = new HashSet<Class<? extends IOException>>();
+ for (final Class<? extends IOException> clazz: clazzes) {
+ this.nonRetriableClasses.add(clazz);
+ }
+ }
+
+ /**
+ * Create the request retry handler using the following list of
+ * non-retriable IOException classes: <br>
+ * <ul>
+ * <li>InterruptedIOException</li>
+ * <li>UnknownHostException</li>
+ * <li>ConnectException</li>
+ * <li>SSLException</li>
+ * </ul>
+ * @param retryCount how many times to retry; 0 means no retries
+ * @param requestSentRetryEnabled true if it's OK to retry requests that have been sent
+ */
+ @SuppressWarnings("unchecked")
+ public DefaultHttpRequestRetryHandler(final int retryCount, final boolean requestSentRetryEnabled) {
+ this(retryCount, requestSentRetryEnabled, Arrays.asList(
+ InterruptedIOException.class,
+ UnknownHostException.class,
+ ConnectException.class,
+ SSLException.class));
+ }
+
+ /**
+ * Create the request retry handler with a retry count of 3, requestSentRetryEnabled false
+ * and using the following list of non-retriable IOException classes: <br>
+ * <ul>
+ * <li>InterruptedIOException</li>
+ * <li>UnknownHostException</li>
+ * <li>ConnectException</li>
+ * <li>SSLException</li>
+ * </ul>
+ */
+ public DefaultHttpRequestRetryHandler() {
+ this(3, false);
+ }
+ /**
+ * Used <code>retryCount</code> and <code>requestSentRetryEnabled</code> to determine
+ * if the given method should be retried.
+ */
+ public boolean retryRequest(
+ final IOException exception,
+ final int executionCount,
+ final HttpContext context) {
+ Args.notNull(exception, "Exception parameter");
+ Args.notNull(context, "HTTP context");
+ if (executionCount > this.retryCount) {
+ // Do not retry if over max retry count
+ return false;
+ }
+ if (this.nonRetriableClasses.contains(exception.getClass())) {
+ return false;
+ } else {
+ for (final Class<? extends IOException> rejectException : this.nonRetriableClasses) {
+ if (rejectException.isInstance(exception)) {
+ return false;
+ }
+ }
+ }
+ final HttpClientContext clientContext = HttpClientContext.adapt(context);
+ final HttpRequest request = clientContext.getRequest();
+
+ if(requestIsAborted(request)){
+ return false;
+ }
+
+ if (handleAsIdempotent(request)) {
+ // Retry if the request is considered idempotent
+ return true;
+ }
+
+ if (!clientContext.isRequestSent() || this.requestSentRetryEnabled) {
+ // Retry if the request has not been sent fully or
+ // if it's OK to retry methods that have been sent
+ return true;
+ }
+ // otherwise do not retry
+ return false;
+ }
+
+ /**
+ * @return <code>true</code> if this handler will retry methods that have
+ * successfully sent their request, <code>false</code> otherwise
+ */
+ public boolean isRequestSentRetryEnabled() {
+ return requestSentRetryEnabled;
+ }
+
+ /**
+ * @return the maximum number of times a method will be retried
+ */
+ public int getRetryCount() {
+ return retryCount;
+ }
+
+ /**
+ * @since 4.2
+ */
+ protected boolean handleAsIdempotent(final HttpRequest request) {
+ return !(request instanceof HttpEntityEnclosingRequest);
+ }
+
+ /**
+ * @since 4.2
+ *
+ * @deprecated (4.3)
+ */
+ @Deprecated
+ protected boolean requestIsAborted(final HttpRequest request) {
+ HttpRequest req = request;
+ if (request instanceof RequestWrapper) { // does not forward request to original
+ req = ((RequestWrapper) request).getOriginal();
+ }
+ return (req instanceof HttpUriRequest && ((HttpUriRequest)req).isAborted());
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultProxyAuthenticationHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultProxyAuthenticationHandler.java
new file mode 100644
index 0000000000..a70057ae51
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultProxyAuthenticationHandler.java
@@ -0,0 +1,90 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.util.List;
+import java.util.Map;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.auth.AUTH;
+import ch.boye.httpclientandroidlib.auth.MalformedChallengeException;
+import ch.boye.httpclientandroidlib.auth.params.AuthPNames;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Default {@link ch.boye.httpclientandroidlib.client.AuthenticationHandler} implementation
+ * for proxy host authentication.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) use {@link ProxyAuthenticationStrategy}
+ */
+@Deprecated
+@Immutable
+public class DefaultProxyAuthenticationHandler extends AbstractAuthenticationHandler {
+
+ public DefaultProxyAuthenticationHandler() {
+ super();
+ }
+
+ public boolean isAuthenticationRequested(
+ final HttpResponse response,
+ final HttpContext context) {
+ Args.notNull(response, "HTTP response");
+ final int status = response.getStatusLine().getStatusCode();
+ return status == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED;
+ }
+
+ public Map<String, Header> getChallenges(
+ final HttpResponse response,
+ final HttpContext context) throws MalformedChallengeException {
+ Args.notNull(response, "HTTP response");
+ final Header[] headers = response.getHeaders(AUTH.PROXY_AUTH);
+ return parseChallenges(headers);
+ }
+
+ @Override
+ protected List<String> getAuthPreferences(
+ final HttpResponse response,
+ final HttpContext context) {
+ @SuppressWarnings("unchecked")
+ final
+ List<String> authpref = (List<String>) response.getParams().getParameter(
+ AuthPNames.PROXY_AUTH_PREF);
+ if (authpref != null) {
+ return authpref;
+ } else {
+ return super.getAuthPreferences(response, context);
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultRedirectHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultRedirectHandler.java
new file mode 100644
index 0000000000..6bd58c0358
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultRedirectHandler.java
@@ -0,0 +1,180 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.CircularRedirectException;
+import ch.boye.httpclientandroidlib.client.RedirectHandler;
+import ch.boye.httpclientandroidlib.client.methods.HttpGet;
+import ch.boye.httpclientandroidlib.client.methods.HttpHead;
+import ch.boye.httpclientandroidlib.client.params.ClientPNames;
+import ch.boye.httpclientandroidlib.client.utils.URIUtils;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.ExecutionContext;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+
+/**
+ * Default implementation of {@link RedirectHandler}.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.1) use {@link DefaultRedirectStrategy}.
+ */
+@Immutable
+@Deprecated
+public class DefaultRedirectHandler implements RedirectHandler {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ private static final String REDIRECT_LOCATIONS = "http.protocol.redirect-locations";
+
+ public DefaultRedirectHandler() {
+ super();
+ }
+
+ public boolean isRedirectRequested(
+ final HttpResponse response,
+ final HttpContext context) {
+ Args.notNull(response, "HTTP response");
+
+ final int statusCode = response.getStatusLine().getStatusCode();
+ switch (statusCode) {
+ case HttpStatus.SC_MOVED_TEMPORARILY:
+ case HttpStatus.SC_MOVED_PERMANENTLY:
+ case HttpStatus.SC_TEMPORARY_REDIRECT:
+ final HttpRequest request = (HttpRequest) context.getAttribute(
+ ExecutionContext.HTTP_REQUEST);
+ final String method = request.getRequestLine().getMethod();
+ return method.equalsIgnoreCase(HttpGet.METHOD_NAME)
+ || method.equalsIgnoreCase(HttpHead.METHOD_NAME);
+ case HttpStatus.SC_SEE_OTHER:
+ return true;
+ default:
+ return false;
+ } //end of switch
+ }
+
+ public URI getLocationURI(
+ final HttpResponse response,
+ final HttpContext context) throws ProtocolException {
+ Args.notNull(response, "HTTP response");
+ //get the location header to find out where to redirect to
+ final Header locationHeader = response.getFirstHeader("location");
+ if (locationHeader == null) {
+ // got a redirect response, but no location header
+ throw new ProtocolException(
+ "Received redirect response " + response.getStatusLine()
+ + " but no location header");
+ }
+ final String location = locationHeader.getValue();
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Redirect requested to location '" + location + "'");
+ }
+
+ URI uri;
+ try {
+ uri = new URI(location);
+ } catch (final URISyntaxException ex) {
+ throw new ProtocolException("Invalid redirect URI: " + location, ex);
+ }
+
+ final HttpParams params = response.getParams();
+ // rfc2616 demands the location value be a complete URI
+ // Location = "Location" ":" absoluteURI
+ if (!uri.isAbsolute()) {
+ if (params.isParameterTrue(ClientPNames.REJECT_RELATIVE_REDIRECT)) {
+ throw new ProtocolException("Relative redirect location '"
+ + uri + "' not allowed");
+ }
+ // Adjust location URI
+ final HttpHost target = (HttpHost) context.getAttribute(
+ ExecutionContext.HTTP_TARGET_HOST);
+ Asserts.notNull(target, "Target host");
+
+ final HttpRequest request = (HttpRequest) context.getAttribute(
+ ExecutionContext.HTTP_REQUEST);
+
+ try {
+ final URI requestURI = new URI(request.getRequestLine().getUri());
+ final URI absoluteRequestURI = URIUtils.rewriteURI(requestURI, target, true);
+ uri = URIUtils.resolve(absoluteRequestURI, uri);
+ } catch (final URISyntaxException ex) {
+ throw new ProtocolException(ex.getMessage(), ex);
+ }
+ }
+
+ if (params.isParameterFalse(ClientPNames.ALLOW_CIRCULAR_REDIRECTS)) {
+
+ RedirectLocations redirectLocations = (RedirectLocations) context.getAttribute(
+ REDIRECT_LOCATIONS);
+
+ if (redirectLocations == null) {
+ redirectLocations = new RedirectLocations();
+ context.setAttribute(REDIRECT_LOCATIONS, redirectLocations);
+ }
+
+ final URI redirectURI;
+ if (uri.getFragment() != null) {
+ try {
+ final HttpHost target = new HttpHost(
+ uri.getHost(),
+ uri.getPort(),
+ uri.getScheme());
+ redirectURI = URIUtils.rewriteURI(uri, target, true);
+ } catch (final URISyntaxException ex) {
+ throw new ProtocolException(ex.getMessage(), ex);
+ }
+ } else {
+ redirectURI = uri;
+ }
+
+ if (redirectLocations.contains(redirectURI)) {
+ throw new CircularRedirectException("Circular redirect to '" +
+ redirectURI + "'");
+ } else {
+ redirectLocations.add(redirectURI);
+ }
+ }
+
+ return uri;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultRedirectStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultRedirectStrategy.java
new file mode 100644
index 0000000000..e7d9315d2c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultRedirectStrategy.java
@@ -0,0 +1,233 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Locale;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.CircularRedirectException;
+import ch.boye.httpclientandroidlib.client.RedirectStrategy;
+import ch.boye.httpclientandroidlib.client.config.RequestConfig;
+import ch.boye.httpclientandroidlib.client.methods.HttpGet;
+import ch.boye.httpclientandroidlib.client.methods.HttpHead;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.client.methods.RequestBuilder;
+import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
+import ch.boye.httpclientandroidlib.client.utils.URIBuilder;
+import ch.boye.httpclientandroidlib.client.utils.URIUtils;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+import ch.boye.httpclientandroidlib.util.TextUtils;
+
+/**
+ * Default implementation of {@link RedirectStrategy}. This strategy honors the restrictions
+ * on automatic redirection of entity enclosing methods such as POST and PUT imposed by the
+ * HTTP specification. <tt>302 Moved Temporarily</tt>, <tt>301 Moved Permanently</tt> and
+ * <tt>307 Temporary Redirect</tt> status codes will result in an automatic redirect of
+ * HEAD and GET methods only. POST and PUT methods will not be automatically redirected
+ * as requiring user confirmation.
+ * <p/>
+ * The restriction on automatic redirection of POST methods can be relaxed by using
+ * {@link LaxRedirectStrategy} instead of {@link DefaultRedirectStrategy}.
+ *
+ * @see LaxRedirectStrategy
+ * @since 4.1
+ */
+@Immutable
+public class DefaultRedirectStrategy implements RedirectStrategy {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ /**
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.client.protocol.HttpClientContext#REDIRECT_LOCATIONS}.
+ */
+ @Deprecated
+ public static final String REDIRECT_LOCATIONS = "http.protocol.redirect-locations";
+
+ public static final DefaultRedirectStrategy INSTANCE = new DefaultRedirectStrategy();
+
+ /**
+ * Redirectable methods.
+ */
+ private static final String[] REDIRECT_METHODS = new String[] {
+ HttpGet.METHOD_NAME,
+ HttpHead.METHOD_NAME
+ };
+
+ public DefaultRedirectStrategy() {
+ super();
+ }
+
+ public boolean isRedirected(
+ final HttpRequest request,
+ final HttpResponse response,
+ final HttpContext context) throws ProtocolException {
+ Args.notNull(request, "HTTP request");
+ Args.notNull(response, "HTTP response");
+
+ final int statusCode = response.getStatusLine().getStatusCode();
+ final String method = request.getRequestLine().getMethod();
+ final Header locationHeader = response.getFirstHeader("location");
+ switch (statusCode) {
+ case HttpStatus.SC_MOVED_TEMPORARILY:
+ return isRedirectable(method) && locationHeader != null;
+ case HttpStatus.SC_MOVED_PERMANENTLY:
+ case HttpStatus.SC_TEMPORARY_REDIRECT:
+ return isRedirectable(method);
+ case HttpStatus.SC_SEE_OTHER:
+ return true;
+ default:
+ return false;
+ } //end of switch
+ }
+
+ public URI getLocationURI(
+ final HttpRequest request,
+ final HttpResponse response,
+ final HttpContext context) throws ProtocolException {
+ Args.notNull(request, "HTTP request");
+ Args.notNull(response, "HTTP response");
+ Args.notNull(context, "HTTP context");
+
+ final HttpClientContext clientContext = HttpClientContext.adapt(context);
+
+ //get the location header to find out where to redirect to
+ final Header locationHeader = response.getFirstHeader("location");
+ if (locationHeader == null) {
+ // got a redirect response, but no location header
+ throw new ProtocolException(
+ "Received redirect response " + response.getStatusLine()
+ + " but no location header");
+ }
+ final String location = locationHeader.getValue();
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Redirect requested to location '" + location + "'");
+ }
+
+ final RequestConfig config = clientContext.getRequestConfig();
+
+ URI uri = createLocationURI(location);
+
+ // rfc2616 demands the location value be a complete URI
+ // Location = "Location" ":" absoluteURI
+ try {
+ if (!uri.isAbsolute()) {
+ if (!config.isRelativeRedirectsAllowed()) {
+ throw new ProtocolException("Relative redirect location '"
+ + uri + "' not allowed");
+ }
+ // Adjust location URI
+ final HttpHost target = clientContext.getTargetHost();
+ Asserts.notNull(target, "Target host");
+ final URI requestURI = new URI(request.getRequestLine().getUri());
+ final URI absoluteRequestURI = URIUtils.rewriteURI(requestURI, target, false);
+ uri = URIUtils.resolve(absoluteRequestURI, uri);
+ }
+ } catch (final URISyntaxException ex) {
+ throw new ProtocolException(ex.getMessage(), ex);
+ }
+
+ RedirectLocations redirectLocations = (RedirectLocations) clientContext.getAttribute(
+ HttpClientContext.REDIRECT_LOCATIONS);
+ if (redirectLocations == null) {
+ redirectLocations = new RedirectLocations();
+ context.setAttribute(HttpClientContext.REDIRECT_LOCATIONS, redirectLocations);
+ }
+ if (!config.isCircularRedirectsAllowed()) {
+ if (redirectLocations.contains(uri)) {
+ throw new CircularRedirectException("Circular redirect to '" + uri + "'");
+ }
+ }
+ redirectLocations.add(uri);
+ return uri;
+ }
+
+ /**
+ * @since 4.1
+ */
+ protected URI createLocationURI(final String location) throws ProtocolException {
+ try {
+ final URIBuilder b = new URIBuilder(new URI(location).normalize());
+ final String host = b.getHost();
+ if (host != null) {
+ b.setHost(host.toLowerCase(Locale.ENGLISH));
+ }
+ final String path = b.getPath();
+ if (TextUtils.isEmpty(path)) {
+ b.setPath("/");
+ }
+ return b.build();
+ } catch (final URISyntaxException ex) {
+ throw new ProtocolException("Invalid redirect URI: " + location, ex);
+ }
+ }
+
+ /**
+ * @since 4.2
+ */
+ protected boolean isRedirectable(final String method) {
+ for (final String m: REDIRECT_METHODS) {
+ if (m.equalsIgnoreCase(method)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public HttpUriRequest getRedirect(
+ final HttpRequest request,
+ final HttpResponse response,
+ final HttpContext context) throws ProtocolException {
+ final URI uri = getLocationURI(request, response, context);
+ final String method = request.getRequestLine().getMethod();
+ if (method.equalsIgnoreCase(HttpHead.METHOD_NAME)) {
+ return new HttpHead(uri);
+ } else if (method.equalsIgnoreCase(HttpGet.METHOD_NAME)) {
+ return new HttpGet(uri);
+ } else {
+ final int status = response.getStatusLine().getStatusCode();
+ if (status == HttpStatus.SC_TEMPORARY_REDIRECT) {
+ return RequestBuilder.copy(request).setUri(uri).build();
+ } else {
+ return new HttpGet(uri);
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultRedirectStrategyAdaptor.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultRedirectStrategyAdaptor.java
new file mode 100644
index 0000000000..b49fb7f73b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultRedirectStrategyAdaptor.java
@@ -0,0 +1,81 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.net.URI;
+
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.RedirectHandler;
+import ch.boye.httpclientandroidlib.client.RedirectStrategy;
+import ch.boye.httpclientandroidlib.client.methods.HttpGet;
+import ch.boye.httpclientandroidlib.client.methods.HttpHead;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * @deprecated (4.1) do not use
+ */
+@Immutable
+@Deprecated
+class DefaultRedirectStrategyAdaptor implements RedirectStrategy {
+
+ private final RedirectHandler handler;
+
+ public DefaultRedirectStrategyAdaptor(final RedirectHandler handler) {
+ super();
+ this.handler = handler;
+ }
+
+ public boolean isRedirected(
+ final HttpRequest request,
+ final HttpResponse response,
+ final HttpContext context) throws ProtocolException {
+ return this.handler.isRedirectRequested(response, context);
+ }
+
+ public HttpUriRequest getRedirect(
+ final HttpRequest request,
+ final HttpResponse response,
+ final HttpContext context) throws ProtocolException {
+ final URI uri = this.handler.getLocationURI(response, context);
+ final String method = request.getRequestLine().getMethod();
+ if (method.equalsIgnoreCase(HttpHead.METHOD_NAME)) {
+ return new HttpHead(uri);
+ } else {
+ return new HttpGet(uri);
+ }
+ }
+
+ public RedirectHandler getHandler() {
+ return this.handler;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultRequestDirector.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultRequestDirector.java
new file mode 100644
index 0000000000..03d9e1e5ff
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultRequestDirector.java
@@ -0,0 +1,1150 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.concurrent.TimeUnit;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.ConnectionReuseStrategy;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.NoHttpResponseException;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.auth.AuthProtocolState;
+import ch.boye.httpclientandroidlib.auth.AuthScheme;
+import ch.boye.httpclientandroidlib.auth.AuthState;
+import ch.boye.httpclientandroidlib.auth.UsernamePasswordCredentials;
+import ch.boye.httpclientandroidlib.client.AuthenticationHandler;
+import ch.boye.httpclientandroidlib.client.AuthenticationStrategy;
+import ch.boye.httpclientandroidlib.client.HttpRequestRetryHandler;
+import ch.boye.httpclientandroidlib.client.NonRepeatableRequestException;
+import ch.boye.httpclientandroidlib.client.RedirectException;
+import ch.boye.httpclientandroidlib.client.RedirectHandler;
+import ch.boye.httpclientandroidlib.client.RedirectStrategy;
+import ch.boye.httpclientandroidlib.client.RequestDirector;
+import ch.boye.httpclientandroidlib.client.UserTokenHandler;
+import ch.boye.httpclientandroidlib.client.methods.AbortableHttpRequest;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.client.params.ClientPNames;
+import ch.boye.httpclientandroidlib.client.params.HttpClientParams;
+import ch.boye.httpclientandroidlib.client.protocol.ClientContext;
+import ch.boye.httpclientandroidlib.client.utils.URIUtils;
+import ch.boye.httpclientandroidlib.conn.BasicManagedEntity;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionManager;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionRequest;
+import ch.boye.httpclientandroidlib.conn.ConnectionKeepAliveStrategy;
+import ch.boye.httpclientandroidlib.conn.ManagedClientConnection;
+import ch.boye.httpclientandroidlib.conn.routing.BasicRouteDirector;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRouteDirector;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoutePlanner;
+import ch.boye.httpclientandroidlib.conn.scheme.Scheme;
+import ch.boye.httpclientandroidlib.entity.BufferedHttpEntity;
+import ch.boye.httpclientandroidlib.impl.auth.BasicScheme;
+import ch.boye.httpclientandroidlib.impl.conn.ConnectionShutdownException;
+import ch.boye.httpclientandroidlib.message.BasicHttpRequest;
+import ch.boye.httpclientandroidlib.params.HttpConnectionParams;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.params.HttpProtocolParams;
+import ch.boye.httpclientandroidlib.protocol.ExecutionContext;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.protocol.HttpProcessor;
+import ch.boye.httpclientandroidlib.protocol.HttpRequestExecutor;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.EntityUtils;
+
+/**
+ * Default implementation of {@link RequestDirector}.
+ * <p>
+ * The following parameters can be used to customize the behavior of this
+ * class:
+ * <ul>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#PROTOCOL_VERSION}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#STRICT_TRANSFER_ENCODING}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#HTTP_ELEMENT_CHARSET}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#USE_EXPECT_CONTINUE}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#WAIT_FOR_CONTINUE}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#USER_AGENT}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#SOCKET_BUFFER_SIZE}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#MAX_LINE_LENGTH}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#MAX_HEADER_COUNT}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#SO_TIMEOUT}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#SO_LINGER}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#SO_REUSEADDR}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#TCP_NODELAY}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#CONNECTION_TIMEOUT}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#STALE_CONNECTION_CHECK}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames#FORCED_ROUTE}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames#LOCAL_ADDRESS}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames#DEFAULT_PROXY}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.cookie.params.CookieSpecPNames#DATE_PATTERNS}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.cookie.params.CookieSpecPNames#SINGLE_COOKIE_HEADER}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.auth.params.AuthPNames#CREDENTIAL_CHARSET}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#COOKIE_POLICY}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#HANDLE_AUTHENTICATION}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#HANDLE_REDIRECTS}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#MAX_REDIRECTS}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#ALLOW_CIRCULAR_REDIRECTS}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#VIRTUAL_HOST}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#DEFAULT_HOST}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#DEFAULT_HEADERS}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#CONN_MANAGER_TIMEOUT}</li>
+ * </ul>
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3)
+ */
+@Deprecated
+@NotThreadSafe // e.g. managedConn
+public class DefaultRequestDirector implements RequestDirector {
+
+ public HttpClientAndroidLog log;
+
+ /** The connection manager. */
+ protected final ClientConnectionManager connManager;
+
+ /** The route planner. */
+ protected final HttpRoutePlanner routePlanner;
+
+ /** The connection re-use strategy. */
+ protected final ConnectionReuseStrategy reuseStrategy;
+
+ /** The keep-alive duration strategy. */
+ protected final ConnectionKeepAliveStrategy keepAliveStrategy;
+
+ /** The request executor. */
+ protected final HttpRequestExecutor requestExec;
+
+ /** The HTTP protocol processor. */
+ protected final HttpProcessor httpProcessor;
+
+ /** The request retry handler. */
+ protected final HttpRequestRetryHandler retryHandler;
+
+ /** The redirect handler. */
+ @Deprecated
+ protected final RedirectHandler redirectHandler;
+
+ /** The redirect strategy. */
+ protected final RedirectStrategy redirectStrategy;
+
+ /** The target authentication handler. */
+ @Deprecated
+ protected final AuthenticationHandler targetAuthHandler;
+
+ /** The target authentication handler. */
+ protected final AuthenticationStrategy targetAuthStrategy;
+
+ /** The proxy authentication handler. */
+ @Deprecated
+ protected final AuthenticationHandler proxyAuthHandler;
+
+ /** The proxy authentication handler. */
+ protected final AuthenticationStrategy proxyAuthStrategy;
+
+ /** The user token handler. */
+ protected final UserTokenHandler userTokenHandler;
+
+ /** The HTTP parameters. */
+ protected final HttpParams params;
+
+ /** The currently allocated connection. */
+ protected ManagedClientConnection managedConn;
+
+ protected final AuthState targetAuthState;
+
+ protected final AuthState proxyAuthState;
+
+ private final HttpAuthenticator authenticator;
+
+ private int execCount;
+
+ private int redirectCount;
+
+ private final int maxRedirects;
+
+ private HttpHost virtualHost;
+
+ @Deprecated
+ public DefaultRequestDirector(
+ final HttpRequestExecutor requestExec,
+ final ClientConnectionManager conman,
+ final ConnectionReuseStrategy reustrat,
+ final ConnectionKeepAliveStrategy kastrat,
+ final HttpRoutePlanner rouplan,
+ final HttpProcessor httpProcessor,
+ final HttpRequestRetryHandler retryHandler,
+ final RedirectHandler redirectHandler,
+ final AuthenticationHandler targetAuthHandler,
+ final AuthenticationHandler proxyAuthHandler,
+ final UserTokenHandler userTokenHandler,
+ final HttpParams params) {
+ this(new HttpClientAndroidLog(DefaultRequestDirector.class),
+ requestExec, conman, reustrat, kastrat, rouplan, httpProcessor, retryHandler,
+ new DefaultRedirectStrategyAdaptor(redirectHandler),
+ new AuthenticationStrategyAdaptor(targetAuthHandler),
+ new AuthenticationStrategyAdaptor(proxyAuthHandler),
+ userTokenHandler,
+ params);
+ }
+
+
+ @Deprecated
+ public DefaultRequestDirector(
+ final HttpClientAndroidLog log,
+ final HttpRequestExecutor requestExec,
+ final ClientConnectionManager conman,
+ final ConnectionReuseStrategy reustrat,
+ final ConnectionKeepAliveStrategy kastrat,
+ final HttpRoutePlanner rouplan,
+ final HttpProcessor httpProcessor,
+ final HttpRequestRetryHandler retryHandler,
+ final RedirectStrategy redirectStrategy,
+ final AuthenticationHandler targetAuthHandler,
+ final AuthenticationHandler proxyAuthHandler,
+ final UserTokenHandler userTokenHandler,
+ final HttpParams params) {
+ this(new HttpClientAndroidLog(DefaultRequestDirector.class),
+ requestExec, conman, reustrat, kastrat, rouplan, httpProcessor, retryHandler,
+ redirectStrategy,
+ new AuthenticationStrategyAdaptor(targetAuthHandler),
+ new AuthenticationStrategyAdaptor(proxyAuthHandler),
+ userTokenHandler,
+ params);
+ }
+
+ /**
+ * @since 4.2
+ */
+ public DefaultRequestDirector(
+ final HttpClientAndroidLog log,
+ final HttpRequestExecutor requestExec,
+ final ClientConnectionManager conman,
+ final ConnectionReuseStrategy reustrat,
+ final ConnectionKeepAliveStrategy kastrat,
+ final HttpRoutePlanner rouplan,
+ final HttpProcessor httpProcessor,
+ final HttpRequestRetryHandler retryHandler,
+ final RedirectStrategy redirectStrategy,
+ final AuthenticationStrategy targetAuthStrategy,
+ final AuthenticationStrategy proxyAuthStrategy,
+ final UserTokenHandler userTokenHandler,
+ final HttpParams params) {
+
+ Args.notNull(log, "Log");
+ Args.notNull(requestExec, "Request executor");
+ Args.notNull(conman, "Client connection manager");
+ Args.notNull(reustrat, "Connection reuse strategy");
+ Args.notNull(kastrat, "Connection keep alive strategy");
+ Args.notNull(rouplan, "Route planner");
+ Args.notNull(httpProcessor, "HTTP protocol processor");
+ Args.notNull(retryHandler, "HTTP request retry handler");
+ Args.notNull(redirectStrategy, "Redirect strategy");
+ Args.notNull(targetAuthStrategy, "Target authentication strategy");
+ Args.notNull(proxyAuthStrategy, "Proxy authentication strategy");
+ Args.notNull(userTokenHandler, "User token handler");
+ Args.notNull(params, "HTTP parameters");
+ this.log = log;
+ this.authenticator = new HttpAuthenticator(log);
+ this.requestExec = requestExec;
+ this.connManager = conman;
+ this.reuseStrategy = reustrat;
+ this.keepAliveStrategy = kastrat;
+ this.routePlanner = rouplan;
+ this.httpProcessor = httpProcessor;
+ this.retryHandler = retryHandler;
+ this.redirectStrategy = redirectStrategy;
+ this.targetAuthStrategy = targetAuthStrategy;
+ this.proxyAuthStrategy = proxyAuthStrategy;
+ this.userTokenHandler = userTokenHandler;
+ this.params = params;
+
+ if (redirectStrategy instanceof DefaultRedirectStrategyAdaptor) {
+ this.redirectHandler = ((DefaultRedirectStrategyAdaptor) redirectStrategy).getHandler();
+ } else {
+ this.redirectHandler = null;
+ }
+ if (targetAuthStrategy instanceof AuthenticationStrategyAdaptor) {
+ this.targetAuthHandler = ((AuthenticationStrategyAdaptor) targetAuthStrategy).getHandler();
+ } else {
+ this.targetAuthHandler = null;
+ }
+ if (proxyAuthStrategy instanceof AuthenticationStrategyAdaptor) {
+ this.proxyAuthHandler = ((AuthenticationStrategyAdaptor) proxyAuthStrategy).getHandler();
+ } else {
+ this.proxyAuthHandler = null;
+ }
+
+ this.managedConn = null;
+
+ this.execCount = 0;
+ this.redirectCount = 0;
+ this.targetAuthState = new AuthState();
+ this.proxyAuthState = new AuthState();
+ this.maxRedirects = this.params.getIntParameter(ClientPNames.MAX_REDIRECTS, 100);
+ }
+
+
+ private RequestWrapper wrapRequest(
+ final HttpRequest request) throws ProtocolException {
+ if (request instanceof HttpEntityEnclosingRequest) {
+ return new EntityEnclosingRequestWrapper(
+ (HttpEntityEnclosingRequest) request);
+ } else {
+ return new RequestWrapper(
+ request);
+ }
+ }
+
+
+ protected void rewriteRequestURI(
+ final RequestWrapper request,
+ final HttpRoute route) throws ProtocolException {
+ try {
+
+ URI uri = request.getURI();
+ if (route.getProxyHost() != null && !route.isTunnelled()) {
+ // Make sure the request URI is absolute
+ if (!uri.isAbsolute()) {
+ final HttpHost target = route.getTargetHost();
+ uri = URIUtils.rewriteURI(uri, target, true);
+ } else {
+ uri = URIUtils.rewriteURI(uri);
+ }
+ } else {
+ // Make sure the request URI is relative
+ if (uri.isAbsolute()) {
+ uri = URIUtils.rewriteURI(uri, null, true);
+ } else {
+ uri = URIUtils.rewriteURI(uri);
+ }
+ }
+ request.setURI(uri);
+
+ } catch (final URISyntaxException ex) {
+ throw new ProtocolException("Invalid URI: " +
+ request.getRequestLine().getUri(), ex);
+ }
+ }
+
+
+ // non-javadoc, see interface ClientRequestDirector
+ public HttpResponse execute(final HttpHost targetHost, final HttpRequest request,
+ final HttpContext context)
+ throws HttpException, IOException {
+
+ context.setAttribute(ClientContext.TARGET_AUTH_STATE, targetAuthState);
+ context.setAttribute(ClientContext.PROXY_AUTH_STATE, proxyAuthState);
+
+ HttpHost target = targetHost;
+
+ final HttpRequest orig = request;
+ final RequestWrapper origWrapper = wrapRequest(orig);
+ origWrapper.setParams(params);
+ final HttpRoute origRoute = determineRoute(target, origWrapper, context);
+
+ virtualHost = (HttpHost) origWrapper.getParams().getParameter(ClientPNames.VIRTUAL_HOST);
+
+ // HTTPCLIENT-1092 - add the port if necessary
+ if (virtualHost != null && virtualHost.getPort() == -1) {
+ final HttpHost host = (target != null) ? target : origRoute.getTargetHost();
+ final int port = host.getPort();
+ if (port != -1){
+ virtualHost = new HttpHost(virtualHost.getHostName(), port, virtualHost.getSchemeName());
+ }
+ }
+
+ RoutedRequest roureq = new RoutedRequest(origWrapper, origRoute);
+
+ boolean reuse = false;
+ boolean done = false;
+ try {
+ HttpResponse response = null;
+ while (!done) {
+ // In this loop, the RoutedRequest may be replaced by a
+ // followup request and route. The request and route passed
+ // in the method arguments will be replaced. The original
+ // request is still available in 'orig'.
+
+ final RequestWrapper wrapper = roureq.getRequest();
+ final HttpRoute route = roureq.getRoute();
+ response = null;
+
+ // See if we have a user token bound to the execution context
+ Object userToken = context.getAttribute(ClientContext.USER_TOKEN);
+
+ // Allocate connection if needed
+ if (managedConn == null) {
+ final ClientConnectionRequest connRequest = connManager.requestConnection(
+ route, userToken);
+ if (orig instanceof AbortableHttpRequest) {
+ ((AbortableHttpRequest) orig).setConnectionRequest(connRequest);
+ }
+
+ final long timeout = HttpClientParams.getConnectionManagerTimeout(params);
+ try {
+ managedConn = connRequest.getConnection(timeout, TimeUnit.MILLISECONDS);
+ } catch(final InterruptedException interrupted) {
+ Thread.currentThread().interrupt();
+ throw new InterruptedIOException();
+ }
+
+ if (HttpConnectionParams.isStaleCheckingEnabled(params)) {
+ // validate connection
+ if (managedConn.isOpen()) {
+ this.log.debug("Stale connection check");
+ if (managedConn.isStale()) {
+ this.log.debug("Stale connection detected");
+ managedConn.close();
+ }
+ }
+ }
+ }
+
+ if (orig instanceof AbortableHttpRequest) {
+ ((AbortableHttpRequest) orig).setReleaseTrigger(managedConn);
+ }
+
+ try {
+ tryConnect(roureq, context);
+ } catch (final TunnelRefusedException ex) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug(ex.getMessage());
+ }
+ response = ex.getResponse();
+ break;
+ }
+
+ final String userinfo = wrapper.getURI().getUserInfo();
+ if (userinfo != null) {
+ targetAuthState.update(
+ new BasicScheme(), new UsernamePasswordCredentials(userinfo));
+ }
+
+ // Get target. Even if there's virtual host, we may need the target to set the port.
+ if (virtualHost != null) {
+ target = virtualHost;
+ } else {
+ final URI requestURI = wrapper.getURI();
+ if (requestURI.isAbsolute()) {
+ target = URIUtils.extractHost(requestURI);
+ }
+ }
+ if (target == null) {
+ target = route.getTargetHost();
+ }
+
+ // Reset headers on the request wrapper
+ wrapper.resetHeaders();
+ // Re-write request URI if needed
+ rewriteRequestURI(wrapper, route);
+
+ // Populate the execution context
+ context.setAttribute(ExecutionContext.HTTP_TARGET_HOST, target);
+ context.setAttribute(ClientContext.ROUTE, route);
+ context.setAttribute(ExecutionContext.HTTP_CONNECTION, managedConn);
+
+ // Run request protocol interceptors
+ requestExec.preProcess(wrapper, httpProcessor, context);
+
+ response = tryExecute(roureq, context);
+ if (response == null) {
+ // Need to start over
+ continue;
+ }
+
+ // Run response protocol interceptors
+ response.setParams(params);
+ requestExec.postProcess(response, httpProcessor, context);
+
+
+ // The connection is in or can be brought to a re-usable state.
+ reuse = reuseStrategy.keepAlive(response, context);
+ if (reuse) {
+ // Set the idle duration of this connection
+ final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
+ if (this.log.isDebugEnabled()) {
+ final String s;
+ if (duration > 0) {
+ s = "for " + duration + " " + TimeUnit.MILLISECONDS;
+ } else {
+ s = "indefinitely";
+ }
+ this.log.debug("Connection can be kept alive " + s);
+ }
+ managedConn.setIdleDuration(duration, TimeUnit.MILLISECONDS);
+ }
+
+ final RoutedRequest followup = handleResponse(roureq, response, context);
+ if (followup == null) {
+ done = true;
+ } else {
+ if (reuse) {
+ // Make sure the response body is fully consumed, if present
+ final HttpEntity entity = response.getEntity();
+ EntityUtils.consume(entity);
+ // entity consumed above is not an auto-release entity,
+ // need to mark the connection re-usable explicitly
+ managedConn.markReusable();
+ } else {
+ managedConn.close();
+ if (proxyAuthState.getState().compareTo(AuthProtocolState.CHALLENGED) > 0
+ && proxyAuthState.getAuthScheme() != null
+ && proxyAuthState.getAuthScheme().isConnectionBased()) {
+ this.log.debug("Resetting proxy auth state");
+ proxyAuthState.reset();
+ }
+ if (targetAuthState.getState().compareTo(AuthProtocolState.CHALLENGED) > 0
+ && targetAuthState.getAuthScheme() != null
+ && targetAuthState.getAuthScheme().isConnectionBased()) {
+ this.log.debug("Resetting target auth state");
+ targetAuthState.reset();
+ }
+ }
+ // check if we can use the same connection for the followup
+ if (!followup.getRoute().equals(roureq.getRoute())) {
+ releaseConnection();
+ }
+ roureq = followup;
+ }
+
+ if (managedConn != null) {
+ if (userToken == null) {
+ userToken = userTokenHandler.getUserToken(context);
+ context.setAttribute(ClientContext.USER_TOKEN, userToken);
+ }
+ if (userToken != null) {
+ managedConn.setState(userToken);
+ }
+ }
+
+ } // while not done
+
+
+ // check for entity, release connection if possible
+ if ((response == null) || (response.getEntity() == null) ||
+ !response.getEntity().isStreaming()) {
+ // connection not needed and (assumed to be) in re-usable state
+ if (reuse) {
+ managedConn.markReusable();
+ }
+ releaseConnection();
+ } else {
+ // install an auto-release entity
+ HttpEntity entity = response.getEntity();
+ entity = new BasicManagedEntity(entity, managedConn, reuse);
+ response.setEntity(entity);
+ }
+
+ return response;
+
+ } catch (final ConnectionShutdownException ex) {
+ final InterruptedIOException ioex = new InterruptedIOException(
+ "Connection has been shut down");
+ ioex.initCause(ex);
+ throw ioex;
+ } catch (final HttpException ex) {
+ abortConnection();
+ throw ex;
+ } catch (final IOException ex) {
+ abortConnection();
+ throw ex;
+ } catch (final RuntimeException ex) {
+ abortConnection();
+ throw ex;
+ }
+ } // execute
+
+ /**
+ * Establish connection either directly or through a tunnel and retry in case of
+ * a recoverable I/O failure
+ */
+ private void tryConnect(
+ final RoutedRequest req, final HttpContext context) throws HttpException, IOException {
+ final HttpRoute route = req.getRoute();
+ final HttpRequest wrapper = req.getRequest();
+
+ int connectCount = 0;
+ for (;;) {
+ context.setAttribute(ExecutionContext.HTTP_REQUEST, wrapper);
+ // Increment connect count
+ connectCount++;
+ try {
+ if (!managedConn.isOpen()) {
+ managedConn.open(route, context, params);
+ } else {
+ managedConn.setSocketTimeout(HttpConnectionParams.getSoTimeout(params));
+ }
+ establishRoute(route, context);
+ break;
+ } catch (final IOException ex) {
+ try {
+ managedConn.close();
+ } catch (final IOException ignore) {
+ }
+ if (retryHandler.retryRequest(ex, connectCount, context)) {
+ if (this.log.isInfoEnabled()) {
+ this.log.info("I/O exception ("+ ex.getClass().getName() +
+ ") caught when connecting to "
+ + route +
+ ": "
+ + ex.getMessage());
+ if (this.log.isDebugEnabled()) {
+ this.log.debug(ex.getMessage(), ex);
+ }
+ this.log.info("Retrying connect to " + route);
+ }
+ } else {
+ throw ex;
+ }
+ }
+ }
+ }
+
+ /**
+ * Execute request and retry in case of a recoverable I/O failure
+ */
+ private HttpResponse tryExecute(
+ final RoutedRequest req, final HttpContext context) throws HttpException, IOException {
+ final RequestWrapper wrapper = req.getRequest();
+ final HttpRoute route = req.getRoute();
+ HttpResponse response = null;
+
+ Exception retryReason = null;
+ for (;;) {
+ // Increment total exec count (with redirects)
+ execCount++;
+ // Increment exec count for this particular request
+ wrapper.incrementExecCount();
+ if (!wrapper.isRepeatable()) {
+ this.log.debug("Cannot retry non-repeatable request");
+ if (retryReason != null) {
+ throw new NonRepeatableRequestException("Cannot retry request " +
+ "with a non-repeatable request entity. The cause lists the " +
+ "reason the original request failed.", retryReason);
+ } else {
+ throw new NonRepeatableRequestException("Cannot retry request " +
+ "with a non-repeatable request entity.");
+ }
+ }
+
+ try {
+ if (!managedConn.isOpen()) {
+ // If we have a direct route to the target host
+ // just re-open connection and re-try the request
+ if (!route.isTunnelled()) {
+ this.log.debug("Reopening the direct connection.");
+ managedConn.open(route, context, params);
+ } else {
+ // otherwise give up
+ this.log.debug("Proxied connection. Need to start over.");
+ break;
+ }
+ }
+
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Attempt " + execCount + " to execute request");
+ }
+ response = requestExec.execute(wrapper, managedConn, context);
+ break;
+
+ } catch (final IOException ex) {
+ this.log.debug("Closing the connection.");
+ try {
+ managedConn.close();
+ } catch (final IOException ignore) {
+ }
+ if (retryHandler.retryRequest(ex, wrapper.getExecCount(), context)) {
+ if (this.log.isInfoEnabled()) {
+ this.log.info("I/O exception ("+ ex.getClass().getName() +
+ ") caught when processing request to "
+ + route +
+ ": "
+ + ex.getMessage());
+ }
+ if (this.log.isDebugEnabled()) {
+ this.log.debug(ex.getMessage(), ex);
+ }
+ if (this.log.isInfoEnabled()) {
+ this.log.info("Retrying request to " + route);
+ }
+ retryReason = ex;
+ } else {
+ if (ex instanceof NoHttpResponseException) {
+ final NoHttpResponseException updatedex = new NoHttpResponseException(
+ route.getTargetHost().toHostString() + " failed to respond");
+ updatedex.setStackTrace(ex.getStackTrace());
+ throw updatedex;
+ } else {
+ throw ex;
+ }
+ }
+ }
+ }
+ return response;
+ }
+
+ /**
+ * Returns the connection back to the connection manager
+ * and prepares for retrieving a new connection during
+ * the next request.
+ */
+ protected void releaseConnection() {
+ // Release the connection through the ManagedConnection instead of the
+ // ConnectionManager directly. This lets the connection control how
+ // it is released.
+ try {
+ managedConn.releaseConnection();
+ } catch(final IOException ignored) {
+ this.log.debug("IOException releasing connection", ignored);
+ }
+ managedConn = null;
+ }
+
+ /**
+ * Determines the route for a request.
+ * Called by {@link #execute}
+ * to determine the route for either the original or a followup request.
+ *
+ * @param targetHost the target host for the request.
+ * Implementations may accept <code>null</code>
+ * if they can still determine a route, for example
+ * to a default target or by inspecting the request.
+ * @param request the request to execute
+ * @param context the context to use for the execution,
+ * never <code>null</code>
+ *
+ * @return the route the request should take
+ *
+ * @throws HttpException in case of a problem
+ */
+ protected HttpRoute determineRoute(final HttpHost targetHost,
+ final HttpRequest request,
+ final HttpContext context)
+ throws HttpException {
+ return this.routePlanner.determineRoute(
+ targetHost != null ? targetHost : (HttpHost) request.getParams()
+ .getParameter(ClientPNames.DEFAULT_HOST),
+ request, context);
+ }
+
+
+ /**
+ * Establishes the target route.
+ *
+ * @param route the route to establish
+ * @param context the context for the request execution
+ *
+ * @throws HttpException in case of a problem
+ * @throws IOException in case of an IO problem
+ */
+ protected void establishRoute(final HttpRoute route, final HttpContext context)
+ throws HttpException, IOException {
+
+ final HttpRouteDirector rowdy = new BasicRouteDirector();
+ int step;
+ do {
+ final HttpRoute fact = managedConn.getRoute();
+ step = rowdy.nextStep(route, fact);
+
+ switch (step) {
+
+ case HttpRouteDirector.CONNECT_TARGET:
+ case HttpRouteDirector.CONNECT_PROXY:
+ managedConn.open(route, context, this.params);
+ break;
+
+ case HttpRouteDirector.TUNNEL_TARGET: {
+ final boolean secure = createTunnelToTarget(route, context);
+ this.log.debug("Tunnel to target created.");
+ managedConn.tunnelTarget(secure, this.params);
+ } break;
+
+ case HttpRouteDirector.TUNNEL_PROXY: {
+ // The most simple example for this case is a proxy chain
+ // of two proxies, where P1 must be tunnelled to P2.
+ // route: Source -> P1 -> P2 -> Target (3 hops)
+ // fact: Source -> P1 -> Target (2 hops)
+ final int hop = fact.getHopCount()-1; // the hop to establish
+ final boolean secure = createTunnelToProxy(route, hop, context);
+ this.log.debug("Tunnel to proxy created.");
+ managedConn.tunnelProxy(route.getHopTarget(hop),
+ secure, this.params);
+ } break;
+
+
+ case HttpRouteDirector.LAYER_PROTOCOL:
+ managedConn.layerProtocol(context, this.params);
+ break;
+
+ case HttpRouteDirector.UNREACHABLE:
+ throw new HttpException("Unable to establish route: " +
+ "planned = " + route + "; current = " + fact);
+ case HttpRouteDirector.COMPLETE:
+ // do nothing
+ break;
+ default:
+ throw new IllegalStateException("Unknown step indicator "
+ + step + " from RouteDirector.");
+ }
+
+ } while (step > HttpRouteDirector.COMPLETE);
+
+ } // establishConnection
+
+
+ /**
+ * Creates a tunnel to the target server.
+ * The connection must be established to the (last) proxy.
+ * A CONNECT request for tunnelling through the proxy will
+ * be created and sent, the response received and checked.
+ * This method does <i>not</i> update the connection with
+ * information about the tunnel, that is left to the caller.
+ *
+ * @param route the route to establish
+ * @param context the context for request execution
+ *
+ * @return <code>true</code> if the tunnelled route is secure,
+ * <code>false</code> otherwise.
+ * The implementation here always returns <code>false</code>,
+ * but derived classes may override.
+ *
+ * @throws HttpException in case of a problem
+ * @throws IOException in case of an IO problem
+ */
+ protected boolean createTunnelToTarget(final HttpRoute route,
+ final HttpContext context)
+ throws HttpException, IOException {
+
+ final HttpHost proxy = route.getProxyHost();
+ final HttpHost target = route.getTargetHost();
+ HttpResponse response = null;
+
+ for (;;) {
+ if (!this.managedConn.isOpen()) {
+ this.managedConn.open(route, context, this.params);
+ }
+
+ final HttpRequest connect = createConnectRequest(route, context);
+ connect.setParams(this.params);
+
+ // Populate the execution context
+ context.setAttribute(ExecutionContext.HTTP_TARGET_HOST, target);
+ context.setAttribute(ClientContext.ROUTE, route);
+ context.setAttribute(ExecutionContext.HTTP_PROXY_HOST, proxy);
+ context.setAttribute(ExecutionContext.HTTP_CONNECTION, managedConn);
+ context.setAttribute(ExecutionContext.HTTP_REQUEST, connect);
+
+ this.requestExec.preProcess(connect, this.httpProcessor, context);
+
+ response = this.requestExec.execute(connect, this.managedConn, context);
+
+ response.setParams(this.params);
+ this.requestExec.postProcess(response, this.httpProcessor, context);
+
+ final int status = response.getStatusLine().getStatusCode();
+ if (status < 200) {
+ throw new HttpException("Unexpected response to CONNECT request: " +
+ response.getStatusLine());
+ }
+
+ if (HttpClientParams.isAuthenticating(this.params)) {
+ if (this.authenticator.isAuthenticationRequested(proxy, response,
+ this.proxyAuthStrategy, this.proxyAuthState, context)) {
+ if (this.authenticator.authenticate(proxy, response,
+ this.proxyAuthStrategy, this.proxyAuthState, context)) {
+ // Retry request
+ if (this.reuseStrategy.keepAlive(response, context)) {
+ this.log.debug("Connection kept alive");
+ // Consume response content
+ final HttpEntity entity = response.getEntity();
+ EntityUtils.consume(entity);
+ } else {
+ this.managedConn.close();
+ }
+ } else {
+ break;
+ }
+ } else {
+ break;
+ }
+ }
+ }
+
+ final int status = response.getStatusLine().getStatusCode();
+
+ if (status > 299) {
+
+ // Buffer response content
+ final HttpEntity entity = response.getEntity();
+ if (entity != null) {
+ response.setEntity(new BufferedHttpEntity(entity));
+ }
+
+ this.managedConn.close();
+ throw new TunnelRefusedException("CONNECT refused by proxy: " +
+ response.getStatusLine(), response);
+ }
+
+ this.managedConn.markReusable();
+
+ // How to decide on security of the tunnelled connection?
+ // The socket factory knows only about the segment to the proxy.
+ // Even if that is secure, the hop to the target may be insecure.
+ // Leave it to derived classes, consider insecure by default here.
+ return false;
+
+ } // createTunnelToTarget
+
+
+
+ /**
+ * Creates a tunnel to an intermediate proxy.
+ * This method is <i>not</i> implemented in this class.
+ * It just throws an exception here.
+ *
+ * @param route the route to establish
+ * @param hop the hop in the route to establish now.
+ * <code>route.getHopTarget(hop)</code>
+ * will return the proxy to tunnel to.
+ * @param context the context for request execution
+ *
+ * @return <code>true</code> if the partially tunnelled connection
+ * is secure, <code>false</code> otherwise.
+ *
+ * @throws HttpException in case of a problem
+ * @throws IOException in case of an IO problem
+ */
+ protected boolean createTunnelToProxy(final HttpRoute route, final int hop,
+ final HttpContext context)
+ throws HttpException, IOException {
+
+ // Have a look at createTunnelToTarget and replicate the parts
+ // you need in a custom derived class. If your proxies don't require
+ // authentication, it is not too hard. But for the stock version of
+ // HttpClient, we cannot make such simplifying assumptions and would
+ // have to include proxy authentication code. The HttpComponents team
+ // is currently not in a position to support rarely used code of this
+ // complexity. Feel free to submit patches that refactor the code in
+ // createTunnelToTarget to facilitate re-use for proxy tunnelling.
+
+ throw new HttpException("Proxy chains are not supported.");
+ }
+
+
+
+ /**
+ * Creates the CONNECT request for tunnelling.
+ * Called by {@link #createTunnelToTarget createTunnelToTarget}.
+ *
+ * @param route the route to establish
+ * @param context the context for request execution
+ *
+ * @return the CONNECT request for tunnelling
+ */
+ protected HttpRequest createConnectRequest(final HttpRoute route,
+ final HttpContext context) {
+ // see RFC 2817, section 5.2 and
+ // INTERNET-DRAFT: Tunneling TCP based protocols through
+ // Web proxy servers
+
+ final HttpHost target = route.getTargetHost();
+
+ final String host = target.getHostName();
+ int port = target.getPort();
+ if (port < 0) {
+ final Scheme scheme = connManager.getSchemeRegistry().
+ getScheme(target.getSchemeName());
+ port = scheme.getDefaultPort();
+ }
+
+ final StringBuilder buffer = new StringBuilder(host.length() + 6);
+ buffer.append(host);
+ buffer.append(':');
+ buffer.append(Integer.toString(port));
+
+ final String authority = buffer.toString();
+ final ProtocolVersion ver = HttpProtocolParams.getVersion(params);
+ final HttpRequest req = new BasicHttpRequest
+ ("CONNECT", authority, ver);
+
+ return req;
+ }
+
+
+ /**
+ * Analyzes a response to check need for a followup.
+ *
+ * @param roureq the request and route.
+ * @param response the response to analayze
+ * @param context the context used for the current request execution
+ *
+ * @return the followup request and route if there is a followup, or
+ * <code>null</code> if the response should be returned as is
+ *
+ * @throws HttpException in case of a problem
+ * @throws IOException in case of an IO problem
+ */
+ protected RoutedRequest handleResponse(final RoutedRequest roureq,
+ final HttpResponse response,
+ final HttpContext context)
+ throws HttpException, IOException {
+
+ final HttpRoute route = roureq.getRoute();
+ final RequestWrapper request = roureq.getRequest();
+
+ final HttpParams params = request.getParams();
+
+ if (HttpClientParams.isAuthenticating(params)) {
+ HttpHost target = (HttpHost) context.getAttribute(ExecutionContext.HTTP_TARGET_HOST);
+ if (target == null) {
+ target = route.getTargetHost();
+ }
+ if (target.getPort() < 0) {
+ final Scheme scheme = connManager.getSchemeRegistry().getScheme(target);
+ target = new HttpHost(target.getHostName(), scheme.getDefaultPort(), target.getSchemeName());
+ }
+
+ final boolean targetAuthRequested = this.authenticator.isAuthenticationRequested(
+ target, response, this.targetAuthStrategy, targetAuthState, context);
+
+ HttpHost proxy = route.getProxyHost();
+ // if proxy is not set use target host instead
+ if (proxy == null) {
+ proxy = route.getTargetHost();
+ }
+ final boolean proxyAuthRequested = this.authenticator.isAuthenticationRequested(
+ proxy, response, this.proxyAuthStrategy, proxyAuthState, context);
+
+ if (targetAuthRequested) {
+ if (this.authenticator.authenticate(target, response,
+ this.targetAuthStrategy, this.targetAuthState, context)) {
+ // Re-try the same request via the same route
+ return roureq;
+ }
+ }
+ if (proxyAuthRequested) {
+ if (this.authenticator.authenticate(proxy, response,
+ this.proxyAuthStrategy, this.proxyAuthState, context)) {
+ // Re-try the same request via the same route
+ return roureq;
+ }
+ }
+ }
+
+ if (HttpClientParams.isRedirecting(params) &&
+ this.redirectStrategy.isRedirected(request, response, context)) {
+
+ if (redirectCount >= maxRedirects) {
+ throw new RedirectException("Maximum redirects ("
+ + maxRedirects + ") exceeded");
+ }
+ redirectCount++;
+
+ // Virtual host cannot be used any longer
+ virtualHost = null;
+
+ final HttpUriRequest redirect = redirectStrategy.getRedirect(request, response, context);
+ final HttpRequest orig = request.getOriginal();
+ redirect.setHeaders(orig.getAllHeaders());
+
+ final URI uri = redirect.getURI();
+ final HttpHost newTarget = URIUtils.extractHost(uri);
+ if (newTarget == null) {
+ throw new ProtocolException("Redirect URI does not specify a valid host name: " + uri);
+ }
+
+ // Reset auth states if redirecting to another host
+ if (!route.getTargetHost().equals(newTarget)) {
+ this.log.debug("Resetting target auth state");
+ targetAuthState.reset();
+ final AuthScheme authScheme = proxyAuthState.getAuthScheme();
+ if (authScheme != null && authScheme.isConnectionBased()) {
+ this.log.debug("Resetting proxy auth state");
+ proxyAuthState.reset();
+ }
+ }
+
+ final RequestWrapper wrapper = wrapRequest(redirect);
+ wrapper.setParams(params);
+
+ final HttpRoute newRoute = determineRoute(newTarget, wrapper, context);
+ final RoutedRequest newRequest = new RoutedRequest(wrapper, newRoute);
+
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Redirecting to '" + uri + "' via " + newRoute);
+ }
+
+ return newRequest;
+ }
+
+ return null;
+ } // handleResponse
+
+
+ /**
+ * Shuts down the connection.
+ * This method is called from a <code>catch</code> block in
+ * {@link #execute execute} during exception handling.
+ */
+ private void abortConnection() {
+ final ManagedClientConnection mcc = managedConn;
+ if (mcc != null) {
+ // we got here as the result of an exception
+ // no response will be returned, release the connection
+ managedConn = null;
+ try {
+ mcc.abortConnection();
+ } catch (final IOException ex) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug(ex.getMessage(), ex);
+ }
+ }
+ // ensure the connection manager properly releases this connection
+ try {
+ mcc.releaseConnection();
+ } catch(final IOException ignored) {
+ this.log.debug("Error releasing connection", ignored);
+ }
+ }
+ } // abortConnection
+
+
+} // class DefaultClientRequestDirector
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultServiceUnavailableRetryStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultServiceUnavailableRetryStrategy.java
new file mode 100644
index 0000000000..8e7279aab1
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultServiceUnavailableRetryStrategy.java
@@ -0,0 +1,80 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.ServiceUnavailableRetryStrategy;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Default implementation of the {@link ServiceUnavailableRetryStrategy} interface.
+ * that retries <code>503</code> (Service Unavailable) responses for a fixed number of times
+ * at a fixed interval.
+ *
+ * @since 4.2
+ */
+@Immutable
+public class DefaultServiceUnavailableRetryStrategy implements ServiceUnavailableRetryStrategy {
+
+ /**
+ * Maximum number of allowed retries if the server responds with a HTTP code
+ * in our retry code list. Default value is 1.
+ */
+ private final int maxRetries;
+
+ /**
+ * Retry interval between subsequent requests, in milliseconds. Default
+ * value is 1 second.
+ */
+ private final long retryInterval;
+
+ public DefaultServiceUnavailableRetryStrategy(final int maxRetries, final int retryInterval) {
+ super();
+ Args.positive(maxRetries, "Max retries");
+ Args.positive(retryInterval, "Retry interval");
+ this.maxRetries = maxRetries;
+ this.retryInterval = retryInterval;
+ }
+
+ public DefaultServiceUnavailableRetryStrategy() {
+ this(1, 1000);
+ }
+
+ public boolean retryRequest(final HttpResponse response, final int executionCount, final HttpContext context) {
+ return executionCount <= maxRetries &&
+ response.getStatusLine().getStatusCode() == HttpStatus.SC_SERVICE_UNAVAILABLE;
+ }
+
+ public long getRetryInterval() {
+ return retryInterval;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultTargetAuthenticationHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultTargetAuthenticationHandler.java
new file mode 100644
index 0000000000..a2a57e1d53
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultTargetAuthenticationHandler.java
@@ -0,0 +1,91 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.util.List;
+import java.util.Map;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.auth.AUTH;
+import ch.boye.httpclientandroidlib.auth.MalformedChallengeException;
+import ch.boye.httpclientandroidlib.auth.params.AuthPNames;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Default {@link ch.boye.httpclientandroidlib.client.AuthenticationHandler} implementation
+ * for target host authentication.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) use {@link TargetAuthenticationStrategy}
+ */
+@Deprecated
+@Immutable
+public class DefaultTargetAuthenticationHandler extends AbstractAuthenticationHandler {
+
+ public DefaultTargetAuthenticationHandler() {
+ super();
+ }
+
+ public boolean isAuthenticationRequested(
+ final HttpResponse response,
+ final HttpContext context) {
+ Args.notNull(response, "HTTP response");
+ final int status = response.getStatusLine().getStatusCode();
+ return status == HttpStatus.SC_UNAUTHORIZED;
+ }
+
+ public Map<String, Header> getChallenges(
+ final HttpResponse response,
+ final HttpContext context) throws MalformedChallengeException {
+ Args.notNull(response, "HTTP response");
+ final Header[] headers = response.getHeaders(AUTH.WWW_AUTH);
+ return parseChallenges(headers);
+ }
+
+ @Override
+ protected List<String> getAuthPreferences(
+ final HttpResponse response,
+ final HttpContext context) {
+ @SuppressWarnings("unchecked")
+ final
+ List<String> authpref = (List<String>) response.getParams().getParameter(
+ AuthPNames.TARGET_AUTH_PREF);
+ if (authpref != null) {
+ return authpref;
+ } else {
+ return super.getAuthPreferences(response, context);
+ }
+ }
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultUserTokenHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultUserTokenHandler.java
new file mode 100644
index 0000000000..c6d2a8daa0
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/DefaultUserTokenHandler.java
@@ -0,0 +1,101 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.security.Principal;
+
+import javax.net.ssl.SSLSession;
+
+import ch.boye.httpclientandroidlib.HttpConnection;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.auth.AuthScheme;
+import ch.boye.httpclientandroidlib.auth.AuthState;
+import ch.boye.httpclientandroidlib.auth.Credentials;
+import ch.boye.httpclientandroidlib.client.UserTokenHandler;
+import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
+import ch.boye.httpclientandroidlib.conn.ManagedHttpClientConnection;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * Default implementation of {@link UserTokenHandler}. This class will use
+ * an instance of {@link Principal} as a state object for HTTP connections,
+ * if it can be obtained from the given execution context. This helps ensure
+ * persistent connections created with a particular user identity within
+ * a particular security context can be reused by the same user only.
+ * <p>
+ * DefaultUserTokenHandler will use the user principle of connection
+ * based authentication schemes such as NTLM or that of the SSL session
+ * with the client authentication turned on. If both are unavailable,
+ * <code>null</code> token will be returned.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class DefaultUserTokenHandler implements UserTokenHandler {
+
+ public static final DefaultUserTokenHandler INSTANCE = new DefaultUserTokenHandler();
+
+ public Object getUserToken(final HttpContext context) {
+
+ final HttpClientContext clientContext = HttpClientContext.adapt(context);
+
+ Principal userPrincipal = null;
+
+ final AuthState targetAuthState = clientContext.getTargetAuthState();
+ if (targetAuthState != null) {
+ userPrincipal = getAuthPrincipal(targetAuthState);
+ if (userPrincipal == null) {
+ final AuthState proxyAuthState = clientContext.getProxyAuthState();
+ userPrincipal = getAuthPrincipal(proxyAuthState);
+ }
+ }
+
+ if (userPrincipal == null) {
+ final HttpConnection conn = clientContext.getConnection();
+ if (conn.isOpen() && conn instanceof ManagedHttpClientConnection) {
+ final SSLSession sslsession = ((ManagedHttpClientConnection) conn).getSSLSession();
+ if (sslsession != null) {
+ userPrincipal = sslsession.getLocalPrincipal();
+ }
+ }
+ }
+
+ return userPrincipal;
+ }
+
+ private static Principal getAuthPrincipal(final AuthState authState) {
+ final AuthScheme scheme = authState.getAuthScheme();
+ if (scheme != null && scheme.isComplete() && scheme.isConnectionBased()) {
+ final Credentials creds = authState.getCredentials();
+ if (creds != null) {
+ return creds.getUserPrincipal();
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/EntityEnclosingRequestWrapper.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/EntityEnclosingRequestWrapper.java
new file mode 100644
index 0000000000..45fbbe0a60
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/EntityEnclosingRequestWrapper.java
@@ -0,0 +1,113 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.entity.HttpEntityWrapper;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+
+/**
+ * A wrapper class for {@link HttpEntityEnclosingRequest}s that can
+ * be used to change properties of the current request without
+ * modifying the original object.
+ * </p>
+ * This class is also capable of resetting the request headers to
+ * the state of the original request.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) do not use.
+ */
+@Deprecated
+@NotThreadSafe // e.g. [gs]etEntity()
+public class EntityEnclosingRequestWrapper extends RequestWrapper
+ implements HttpEntityEnclosingRequest {
+
+ private HttpEntity entity;
+ private boolean consumed;
+
+ public EntityEnclosingRequestWrapper(final HttpEntityEnclosingRequest request)
+ throws ProtocolException {
+ super(request);
+ setEntity(request.getEntity());
+ }
+
+ public HttpEntity getEntity() {
+ return this.entity;
+ }
+
+ public void setEntity(final HttpEntity entity) {
+ this.entity = entity != null ? new EntityWrapper(entity) : null;
+ this.consumed = false;
+ }
+
+ public boolean expectContinue() {
+ final Header expect = getFirstHeader(HTTP.EXPECT_DIRECTIVE);
+ return expect != null && HTTP.EXPECT_CONTINUE.equalsIgnoreCase(expect.getValue());
+ }
+
+ @Override
+ public boolean isRepeatable() {
+ return this.entity == null || this.entity.isRepeatable() || !this.consumed;
+ }
+
+ class EntityWrapper extends HttpEntityWrapper {
+
+ EntityWrapper(final HttpEntity entity) {
+ super(entity);
+ }
+
+ @Override
+ public void consumeContent() throws IOException {
+ consumed = true;
+ super.consumeContent();
+ }
+
+ @Override
+ public InputStream getContent() throws IOException {
+ consumed = true;
+ return super.getContent();
+ }
+
+ @Override
+ public void writeTo(final OutputStream outstream) throws IOException {
+ consumed = true;
+ super.writeTo(outstream);
+ }
+
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/FutureRequestExecutionMetrics.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/FutureRequestExecutionMetrics.java
new file mode 100644
index 0000000000..888e7e3a52
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/FutureRequestExecutionMetrics.java
@@ -0,0 +1,156 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Collection of different counters used to gather metrics for {@link FutureRequestExecutionService}.
+ */
+public final class FutureRequestExecutionMetrics {
+
+ private final AtomicLong activeConnections = new AtomicLong();
+ private final AtomicLong scheduledConnections = new AtomicLong();
+ private final DurationCounter successfulConnections = new DurationCounter();
+ private final DurationCounter failedConnections = new DurationCounter();
+ private final DurationCounter requests = new DurationCounter();
+ private final DurationCounter tasks = new DurationCounter();
+
+ FutureRequestExecutionMetrics() {
+ }
+
+ AtomicLong getActiveConnections() {
+ return activeConnections;
+ }
+
+ AtomicLong getScheduledConnections() {
+ return scheduledConnections;
+ }
+
+ DurationCounter getSuccessfulConnections() {
+ return successfulConnections;
+ }
+
+ DurationCounter getFailedConnections() {
+ return failedConnections;
+ }
+
+ DurationCounter getRequests() {
+ return requests;
+ }
+
+ DurationCounter getTasks() {
+ return tasks;
+ }
+
+ public long getActiveConnectionCount() {
+ return activeConnections.get();
+ }
+
+ public long getScheduledConnectionCount() {
+ return scheduledConnections.get();
+ }
+
+ public long getSuccessfulConnectionCount() {
+ return successfulConnections.count();
+ }
+
+ public long getSuccessfulConnectionAverageDuration() {
+ return successfulConnections.averageDuration();
+ }
+
+ public long getFailedConnectionCount() {
+ return failedConnections.count();
+ }
+
+ public long getFailedConnectionAverageDuration() {
+ return failedConnections.averageDuration();
+ }
+
+ public long getRequestCount() {
+ return requests.count();
+ }
+
+ public long getRequestAverageDuration() {
+ return requests.averageDuration();
+ }
+
+ public long getTaskCount() {
+ return tasks.count();
+ }
+
+ public long getTaskAverageDuration() {
+ return tasks.averageDuration();
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("[activeConnections=").append(activeConnections)
+ .append(", scheduledConnections=").append(scheduledConnections)
+ .append(", successfulConnections=").append(successfulConnections)
+ .append(", failedConnections=").append(failedConnections)
+ .append(", requests=").append(requests)
+ .append(", tasks=").append(tasks)
+ .append("]");
+ return builder.toString();
+ }
+
+ /**
+ * A counter that can measure duration and number of events.
+ */
+ static class DurationCounter {
+
+ private final AtomicLong count = new AtomicLong(0);
+ private final AtomicLong cumulativeDuration = new AtomicLong(0);
+
+ public void increment(final long startTime) {
+ count.incrementAndGet();
+ cumulativeDuration.addAndGet(System.currentTimeMillis() - startTime);
+ }
+
+ public long count() {
+ return count.get();
+ }
+
+ public long averageDuration() {
+ final long counter = count.get();
+ return counter > 0 ? cumulativeDuration.get() / counter : 0;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("[count=").append(count())
+ .append(", averageDuration=").append(averageDuration())
+ .append("]");
+ return builder.toString();
+ }
+
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/FutureRequestExecutionService.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/FutureRequestExecutionService.java
new file mode 100644
index 0000000000..26fa9dba33
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/FutureRequestExecutionService.java
@@ -0,0 +1,142 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.client.HttpClient;
+import ch.boye.httpclientandroidlib.client.ResponseHandler;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.concurrent.FutureCallback;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * HttpAsyncClientWithFuture wraps calls to execute with a {@link HttpRequestFutureTask}
+ * and schedules them using the provided executor service. Scheduled calls may be cancelled.
+ */
+@ThreadSafe
+public class FutureRequestExecutionService implements Closeable {
+
+ private final HttpClient httpclient;
+ private final ExecutorService executorService;
+ private final FutureRequestExecutionMetrics metrics = new FutureRequestExecutionMetrics();
+ private final AtomicBoolean closed = new AtomicBoolean(false);
+
+ /**
+ * Create a new FutureRequestExecutionService.
+ *
+ * @param httpclient
+ * you should tune your httpclient instance to match your needs. You should
+ * align the max number of connections in the pool and the number of threads
+ * in the executor; it doesn't make sense to have more threads than connections
+ * and if you have less connections than threads, the threads will just end up
+ * blocking on getting a connection from the pool.
+ * @param executorService
+ * any executorService will do here. E.g.
+ * {@link java.util.concurrent.Executors#newFixedThreadPool(int)}
+ */
+ public FutureRequestExecutionService(
+ final HttpClient httpclient,
+ final ExecutorService executorService) {
+ this.httpclient = httpclient;
+ this.executorService = executorService;
+ }
+
+ /**
+ * Schedule a request for execution.
+ *
+ * @param <T>
+ *
+ * @param request
+ * request to execute
+ * @param responseHandler
+ * handler that will process the response.
+ * @return HttpAsyncClientFutureTask for the scheduled request.
+ * @throws InterruptedException
+ */
+ public <T> HttpRequestFutureTask<T> execute(
+ final HttpUriRequest request,
+ final HttpContext context,
+ final ResponseHandler<T> responseHandler) {
+ return execute(request, context, responseHandler, null);
+ }
+
+ /**
+ * Schedule a request for execution.
+ *
+ * @param <T>
+ *
+ * @param request
+ * request to execute
+ * @param context
+ * optional context; use null if not needed.
+ * @param responseHandler
+ * handler that will process the response.
+ * @param callback
+ * callback handler that will be called when the request is scheduled,
+ * started, completed, failed, or cancelled.
+ * @return HttpAsyncClientFutureTask for the scheduled request.
+ * @throws InterruptedException
+ */
+ public <T> HttpRequestFutureTask<T> execute(
+ final HttpUriRequest request,
+ final HttpContext context,
+ final ResponseHandler<T> responseHandler,
+ final FutureCallback<T> callback) {
+ if(closed.get()) {
+ throw new IllegalStateException("Close has been called on this httpclient instance.");
+ }
+ metrics.getScheduledConnections().incrementAndGet();
+ final HttpRequestTaskCallable<T> callable = new HttpRequestTaskCallable<T>(
+ httpclient, request, context, responseHandler, callback, metrics);
+ final HttpRequestFutureTask<T> httpRequestFutureTask = new HttpRequestFutureTask<T>(
+ request, callable);
+ executorService.execute(httpRequestFutureTask);
+
+ return httpRequestFutureTask;
+ }
+
+ /**
+ * @return metrics gathered for this instance.
+ * @see FutureRequestExecutionMetrics
+ */
+ public FutureRequestExecutionMetrics metrics() {
+ return metrics;
+ }
+
+ public void close() throws IOException {
+ closed.set(true);
+ executorService.shutdownNow();
+ if (httpclient instanceof Closeable) {
+ ((Closeable) httpclient).close();
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/HttpAuthenticator.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/HttpAuthenticator.java
new file mode 100644
index 0000000000..745ae4735b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/HttpAuthenticator.java
@@ -0,0 +1,61 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.auth.AuthState;
+import ch.boye.httpclientandroidlib.client.AuthenticationStrategy;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * @deprecated (4.3) reserved for internal use.
+ *
+ */
+@Deprecated
+public class HttpAuthenticator extends ch.boye.httpclientandroidlib.impl.auth.HttpAuthenticator {
+
+ public HttpAuthenticator(final HttpClientAndroidLog log) {
+ super(log);
+ }
+
+ public HttpAuthenticator() {
+ super();
+ }
+
+ public boolean authenticate (
+ final HttpHost host,
+ final HttpResponse response,
+ final AuthenticationStrategy authStrategy,
+ final AuthState authState,
+ final HttpContext context) {
+ return handleAuthChallenge(host, response, authStrategy, authState, context);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/HttpClientBuilder.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/HttpClientBuilder.java
new file mode 100644
index 0000000000..00313e668f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/HttpClientBuilder.java
@@ -0,0 +1,954 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.io.Closeable;
+import java.net.ProxySelector;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+
+import ch.boye.httpclientandroidlib.ConnectionReuseStrategy;
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequestInterceptor;
+import ch.boye.httpclientandroidlib.HttpResponseInterceptor;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.auth.AuthSchemeProvider;
+import ch.boye.httpclientandroidlib.client.AuthenticationStrategy;
+import ch.boye.httpclientandroidlib.client.BackoffManager;
+import ch.boye.httpclientandroidlib.client.ConnectionBackoffStrategy;
+import ch.boye.httpclientandroidlib.client.CookieStore;
+import ch.boye.httpclientandroidlib.client.CredentialsProvider;
+import ch.boye.httpclientandroidlib.client.HttpRequestRetryHandler;
+import ch.boye.httpclientandroidlib.client.RedirectStrategy;
+import ch.boye.httpclientandroidlib.client.ServiceUnavailableRetryStrategy;
+import ch.boye.httpclientandroidlib.client.UserTokenHandler;
+import ch.boye.httpclientandroidlib.client.config.AuthSchemes;
+import ch.boye.httpclientandroidlib.client.config.CookieSpecs;
+import ch.boye.httpclientandroidlib.client.config.RequestConfig;
+import ch.boye.httpclientandroidlib.client.protocol.RequestAcceptEncoding;
+import ch.boye.httpclientandroidlib.client.protocol.RequestAddCookies;
+import ch.boye.httpclientandroidlib.client.protocol.RequestAuthCache;
+import ch.boye.httpclientandroidlib.client.protocol.RequestClientConnControl;
+import ch.boye.httpclientandroidlib.client.protocol.RequestDefaultHeaders;
+import ch.boye.httpclientandroidlib.client.protocol.RequestExpectContinue;
+import ch.boye.httpclientandroidlib.client.protocol.ResponseContentEncoding;
+import ch.boye.httpclientandroidlib.client.protocol.ResponseProcessCookies;
+import ch.boye.httpclientandroidlib.config.ConnectionConfig;
+import ch.boye.httpclientandroidlib.config.Lookup;
+import ch.boye.httpclientandroidlib.config.RegistryBuilder;
+import ch.boye.httpclientandroidlib.config.SocketConfig;
+import ch.boye.httpclientandroidlib.conn.ConnectionKeepAliveStrategy;
+import ch.boye.httpclientandroidlib.conn.HttpClientConnectionManager;
+import ch.boye.httpclientandroidlib.conn.SchemePortResolver;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoutePlanner;
+import ch.boye.httpclientandroidlib.conn.socket.ConnectionSocketFactory;
+import ch.boye.httpclientandroidlib.conn.socket.LayeredConnectionSocketFactory;
+import ch.boye.httpclientandroidlib.conn.socket.PlainConnectionSocketFactory;
+import ch.boye.httpclientandroidlib.conn.ssl.SSLConnectionSocketFactory;
+import ch.boye.httpclientandroidlib.conn.ssl.SSLContexts;
+import ch.boye.httpclientandroidlib.conn.ssl.X509HostnameVerifier;
+import ch.boye.httpclientandroidlib.cookie.CookieSpecProvider;
+import ch.boye.httpclientandroidlib.impl.DefaultConnectionReuseStrategy;
+import ch.boye.httpclientandroidlib.impl.NoConnectionReuseStrategy;
+import ch.boye.httpclientandroidlib.impl.auth.BasicSchemeFactory;
+import ch.boye.httpclientandroidlib.impl.auth.DigestSchemeFactory;
+/* KerberosSchemeFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.impl.auth.NTLMSchemeFactory;
+/* SPNegoSchemeFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.impl.conn.DefaultProxyRoutePlanner;
+import ch.boye.httpclientandroidlib.impl.conn.DefaultRoutePlanner;
+import ch.boye.httpclientandroidlib.impl.conn.DefaultSchemePortResolver;
+import ch.boye.httpclientandroidlib.impl.conn.PoolingHttpClientConnectionManager;
+import ch.boye.httpclientandroidlib.impl.conn.SystemDefaultRoutePlanner;
+import ch.boye.httpclientandroidlib.impl.cookie.BestMatchSpecFactory;
+import ch.boye.httpclientandroidlib.impl.cookie.BrowserCompatSpecFactory;
+import ch.boye.httpclientandroidlib.impl.cookie.IgnoreSpecFactory;
+import ch.boye.httpclientandroidlib.impl.cookie.NetscapeDraftSpecFactory;
+import ch.boye.httpclientandroidlib.impl.cookie.RFC2109SpecFactory;
+import ch.boye.httpclientandroidlib.impl.cookie.RFC2965SpecFactory;
+import ch.boye.httpclientandroidlib.impl.execchain.BackoffStrategyExec;
+import ch.boye.httpclientandroidlib.impl.execchain.ClientExecChain;
+import ch.boye.httpclientandroidlib.impl.execchain.MainClientExec;
+import ch.boye.httpclientandroidlib.impl.execchain.ProtocolExec;
+import ch.boye.httpclientandroidlib.impl.execchain.RedirectExec;
+import ch.boye.httpclientandroidlib.impl.execchain.RetryExec;
+import ch.boye.httpclientandroidlib.impl.execchain.ServiceUnavailableRetryExec;
+import ch.boye.httpclientandroidlib.protocol.HttpProcessor;
+import ch.boye.httpclientandroidlib.protocol.HttpProcessorBuilder;
+import ch.boye.httpclientandroidlib.protocol.HttpRequestExecutor;
+import ch.boye.httpclientandroidlib.protocol.RequestContent;
+import ch.boye.httpclientandroidlib.protocol.RequestTargetHost;
+import ch.boye.httpclientandroidlib.protocol.RequestUserAgent;
+import ch.boye.httpclientandroidlib.util.TextUtils;
+import ch.boye.httpclientandroidlib.util.VersionInfo;
+
+/**
+ * Builder for {@link CloseableHttpClient} instances.
+ * <p/>
+ * When a particular component is not explicitly this class will
+ * use its default implementation. System properties will be taken
+ * into account when configuring the default implementations when
+ * {@link #useSystemProperties()} method is called prior to calling
+ * {@link #build()}.
+ * <ul>
+ * <li>ssl.TrustManagerFactory.algorithm</li>
+ * <li>javax.net.ssl.trustStoreType</li>
+ * <li>javax.net.ssl.trustStore</li>
+ * <li>javax.net.ssl.trustStoreProvider</li>
+ * <li>javax.net.ssl.trustStorePassword</li>
+ * <li>ssl.KeyManagerFactory.algorithm</li>
+ * <li>javax.net.ssl.keyStoreType</li>
+ * <li>javax.net.ssl.keyStore</li>
+ * <li>javax.net.ssl.keyStoreProvider</li>
+ * <li>javax.net.ssl.keyStorePassword</li>
+ * <li>https.protocols</li>
+ * <li>https.cipherSuites</li>
+ * <li>http.proxyHost</li>
+ * <li>http.proxyPort</li>
+ * <li>http.nonProxyHosts</li>
+ * <li>http.keepAlive</li>
+ * <li>http.maxConnections</li>
+ * <li>http.agent</li>
+ * </ul>
+ * <p/>
+ * Please note that some settings used by this class can be mutually
+ * exclusive and may not apply when building {@link CloseableHttpClient}
+ * instances.
+ *
+ * @since 4.3
+ */
+@NotThreadSafe
+public class HttpClientBuilder {
+
+ private HttpRequestExecutor requestExec;
+ private X509HostnameVerifier hostnameVerifier;
+ private LayeredConnectionSocketFactory sslSocketFactory;
+ private SSLContext sslcontext;
+ private HttpClientConnectionManager connManager;
+ private SchemePortResolver schemePortResolver;
+ private ConnectionReuseStrategy reuseStrategy;
+ private ConnectionKeepAliveStrategy keepAliveStrategy;
+ private AuthenticationStrategy targetAuthStrategy;
+ private AuthenticationStrategy proxyAuthStrategy;
+ private UserTokenHandler userTokenHandler;
+ private HttpProcessor httpprocessor;
+
+ private LinkedList<HttpRequestInterceptor> requestFirst;
+ private LinkedList<HttpRequestInterceptor> requestLast;
+ private LinkedList<HttpResponseInterceptor> responseFirst;
+ private LinkedList<HttpResponseInterceptor> responseLast;
+
+ private HttpRequestRetryHandler retryHandler;
+ private HttpRoutePlanner routePlanner;
+ private RedirectStrategy redirectStrategy;
+ private ConnectionBackoffStrategy connectionBackoffStrategy;
+ private BackoffManager backoffManager;
+ private ServiceUnavailableRetryStrategy serviceUnavailStrategy;
+ private Lookup<AuthSchemeProvider> authSchemeRegistry;
+ private Lookup<CookieSpecProvider> cookieSpecRegistry;
+ private CookieStore cookieStore;
+ private CredentialsProvider credentialsProvider;
+ private String userAgent;
+ private HttpHost proxy;
+ private Collection<? extends Header> defaultHeaders;
+ private SocketConfig defaultSocketConfig;
+ private ConnectionConfig defaultConnectionConfig;
+ private RequestConfig defaultRequestConfig;
+
+ private boolean systemProperties;
+ private boolean redirectHandlingDisabled;
+ private boolean automaticRetriesDisabled;
+ private boolean contentCompressionDisabled;
+ private boolean cookieManagementDisabled;
+ private boolean authCachingDisabled;
+ private boolean connectionStateDisabled;
+
+ private int maxConnTotal = 0;
+ private int maxConnPerRoute = 0;
+
+ private List<Closeable> closeables;
+
+ static final String DEFAULT_USER_AGENT;
+ static {
+ final VersionInfo vi = VersionInfo.loadVersionInfo
+ ("ch.boye.httpclientandroidlib.client", HttpClientBuilder.class.getClassLoader());
+ final String release = (vi != null) ?
+ vi.getRelease() : VersionInfo.UNAVAILABLE;
+ DEFAULT_USER_AGENT = "Apache-HttpClient/" + release + " (java 1.5)";
+ }
+
+ public static HttpClientBuilder create() {
+ return new HttpClientBuilder();
+ }
+
+ protected HttpClientBuilder() {
+ super();
+ }
+
+ /**
+ * Assigns {@link HttpRequestExecutor} instance.
+ */
+ public final HttpClientBuilder setRequestExecutor(final HttpRequestExecutor requestExec) {
+ this.requestExec = requestExec;
+ return this;
+ }
+
+ /**
+ * Assigns {@link X509HostnameVerifier} instance.
+ * <p/>
+ * Please note this value can be overridden by the {@link #setConnectionManager(
+ * ch.boye.httpclientandroidlib.conn.HttpClientConnectionManager)} and the {@link #setSSLSocketFactory(
+ * ch.boye.httpclientandroidlib.conn.socket.LayeredConnectionSocketFactory)} methods.
+ */
+ public final HttpClientBuilder setHostnameVerifier(final X509HostnameVerifier hostnameVerifier) {
+ this.hostnameVerifier = hostnameVerifier;
+ return this;
+ }
+
+ /**
+ * Assigns {@link SSLContext} instance.
+ * <p/>
+ * <p/>
+ * Please note this value can be overridden by the {@link #setConnectionManager(
+ * ch.boye.httpclientandroidlib.conn.HttpClientConnectionManager)} and the {@link #setSSLSocketFactory(
+ * ch.boye.httpclientandroidlib.conn.socket.LayeredConnectionSocketFactory)} methods.
+ */
+ public final HttpClientBuilder setSslcontext(final SSLContext sslcontext) {
+ this.sslcontext = sslcontext;
+ return this;
+ }
+
+ /**
+ * Assigns {@link LayeredConnectionSocketFactory} instance.
+ * <p/>
+ * Please note this value can be overridden by the {@link #setConnectionManager(
+ * ch.boye.httpclientandroidlib.conn.HttpClientConnectionManager)} method.
+ */
+ public final HttpClientBuilder setSSLSocketFactory(
+ final LayeredConnectionSocketFactory sslSocketFactory) {
+ this.sslSocketFactory = sslSocketFactory;
+ return this;
+ }
+
+ /**
+ * Assigns maximum total connection value.
+ * <p/>
+ * Please note this value can be overridden by the {@link #setConnectionManager(
+ * ch.boye.httpclientandroidlib.conn.HttpClientConnectionManager)} method.
+ */
+ public final HttpClientBuilder setMaxConnTotal(final int maxConnTotal) {
+ this.maxConnTotal = maxConnTotal;
+ return this;
+ }
+
+ /**
+ * Assigns maximum connection per route value.
+ * <p/>
+ * Please note this value can be overridden by the {@link #setConnectionManager(
+ * ch.boye.httpclientandroidlib.conn.HttpClientConnectionManager)} method.
+ */
+ public final HttpClientBuilder setMaxConnPerRoute(final int maxConnPerRoute) {
+ this.maxConnPerRoute = maxConnPerRoute;
+ return this;
+ }
+
+ /**
+ * Assigns default {@link SocketConfig}.
+ * <p/>
+ * Please note this value can be overridden by the {@link #setConnectionManager(
+ * ch.boye.httpclientandroidlib.conn.HttpClientConnectionManager)} method.
+ */
+ public final HttpClientBuilder setDefaultSocketConfig(final SocketConfig config) {
+ this.defaultSocketConfig = config;
+ return this;
+ }
+
+ /**
+ * Assigns default {@link ConnectionConfig}.
+ * <p/>
+ * Please note this value can be overridden by the {@link #setConnectionManager(
+ * ch.boye.httpclientandroidlib.conn.HttpClientConnectionManager)} method.
+ */
+ public final HttpClientBuilder setDefaultConnectionConfig(final ConnectionConfig config) {
+ this.defaultConnectionConfig = config;
+ return this;
+ }
+
+ /**
+ * Assigns {@link HttpClientConnectionManager} instance.
+ */
+ public final HttpClientBuilder setConnectionManager(
+ final HttpClientConnectionManager connManager) {
+ this.connManager = connManager;
+ return this;
+ }
+
+ /**
+ * Assigns {@link ConnectionReuseStrategy} instance.
+ */
+ public final HttpClientBuilder setConnectionReuseStrategy(
+ final ConnectionReuseStrategy reuseStrategy) {
+ this.reuseStrategy = reuseStrategy;
+ return this;
+ }
+
+ /**
+ * Assigns {@link ConnectionKeepAliveStrategy} instance.
+ */
+ public final HttpClientBuilder setKeepAliveStrategy(
+ final ConnectionKeepAliveStrategy keepAliveStrategy) {
+ this.keepAliveStrategy = keepAliveStrategy;
+ return this;
+ }
+
+ /**
+ * Assigns {@link AuthenticationStrategy} instance for proxy
+ * authentication.
+ */
+ public final HttpClientBuilder setTargetAuthenticationStrategy(
+ final AuthenticationStrategy targetAuthStrategy) {
+ this.targetAuthStrategy = targetAuthStrategy;
+ return this;
+ }
+
+ /**
+ * Assigns {@link AuthenticationStrategy} instance for target
+ * host authentication.
+ */
+ public final HttpClientBuilder setProxyAuthenticationStrategy(
+ final AuthenticationStrategy proxyAuthStrategy) {
+ this.proxyAuthStrategy = proxyAuthStrategy;
+ return this;
+ }
+
+ /**
+ * Assigns {@link UserTokenHandler} instance.
+ * <p/>
+ * Please note this value can be overridden by the {@link #disableConnectionState()}
+ * method.
+ */
+ public final HttpClientBuilder setUserTokenHandler(final UserTokenHandler userTokenHandler) {
+ this.userTokenHandler = userTokenHandler;
+ return this;
+ }
+
+ /**
+ * Disables connection state tracking.
+ */
+ public final HttpClientBuilder disableConnectionState() {
+ connectionStateDisabled = true;
+ return this;
+ }
+
+ /**
+ * Assigns {@link SchemePortResolver} instance.
+ */
+ public final HttpClientBuilder setSchemePortResolver(
+ final SchemePortResolver schemePortResolver) {
+ this.schemePortResolver = schemePortResolver;
+ return this;
+ }
+
+ /**
+ * Assigns <tt>User-Agent</tt> value.
+ * <p/>
+ * Please note this value can be overridden by the {@link #setHttpProcessor(
+ * ch.boye.httpclientandroidlib.protocol.HttpProcessor)} method.
+ */
+ public final HttpClientBuilder setUserAgent(final String userAgent) {
+ this.userAgent = userAgent;
+ return this;
+ }
+
+ /**
+ * Assigns default request header values.
+ * <p/>
+ * Please note this value can be overridden by the {@link #setHttpProcessor(
+ * ch.boye.httpclientandroidlib.protocol.HttpProcessor)} method.
+ */
+ public final HttpClientBuilder setDefaultHeaders(final Collection<? extends Header> defaultHeaders) {
+ this.defaultHeaders = defaultHeaders;
+ return this;
+ }
+
+ /**
+ * Adds this protocol interceptor to the head of the protocol processing list.
+ * <p/>
+ * Please note this value can be overridden by the {@link #setHttpProcessor(
+ * ch.boye.httpclientandroidlib.protocol.HttpProcessor)} method.
+ */
+ public final HttpClientBuilder addInterceptorFirst(final HttpResponseInterceptor itcp) {
+ if (itcp == null) {
+ return this;
+ }
+ if (responseFirst == null) {
+ responseFirst = new LinkedList<HttpResponseInterceptor>();
+ }
+ responseFirst.addFirst(itcp);
+ return this;
+ }
+
+ /**
+ * Adds this protocol interceptor to the tail of the protocol processing list.
+ * <p/>
+ * Please note this value can be overridden by the {@link #setHttpProcessor(
+ * ch.boye.httpclientandroidlib.protocol.HttpProcessor)} method.
+ */
+ public final HttpClientBuilder addInterceptorLast(final HttpResponseInterceptor itcp) {
+ if (itcp == null) {
+ return this;
+ }
+ if (responseLast == null) {
+ responseLast = new LinkedList<HttpResponseInterceptor>();
+ }
+ responseLast.addLast(itcp);
+ return this;
+ }
+
+ /**
+ * Adds this protocol interceptor to the head of the protocol processing list.
+ * <p/>
+ * Please note this value can be overridden by the {@link #setHttpProcessor(
+ * ch.boye.httpclientandroidlib.protocol.HttpProcessor)} method.
+ */
+ public final HttpClientBuilder addInterceptorFirst(final HttpRequestInterceptor itcp) {
+ if (itcp == null) {
+ return this;
+ }
+ if (requestFirst == null) {
+ requestFirst = new LinkedList<HttpRequestInterceptor>();
+ }
+ requestFirst.addFirst(itcp);
+ return this;
+ }
+
+ /**
+ * Adds this protocol interceptor to the tail of the protocol processing list.
+ * <p/>
+ * Please note this value can be overridden by the {@link #setHttpProcessor(
+ * ch.boye.httpclientandroidlib.protocol.HttpProcessor)} method.
+ */
+ public final HttpClientBuilder addInterceptorLast(final HttpRequestInterceptor itcp) {
+ if (itcp == null) {
+ return this;
+ }
+ if (requestLast == null) {
+ requestLast = new LinkedList<HttpRequestInterceptor>();
+ }
+ requestLast.addLast(itcp);
+ return this;
+ }
+
+ /**
+ * Disables state (cookie) management.
+ * <p/>
+ * Please note this value can be overridden by the {@link #setHttpProcessor(
+ * ch.boye.httpclientandroidlib.protocol.HttpProcessor)} method.
+ */
+ public final HttpClientBuilder disableCookieManagement() {
+ this.cookieManagementDisabled = true;
+ return this;
+ }
+
+ /**
+ * Disables automatic content decompression.
+ * <p/>
+ * Please note this value can be overridden by the {@link #setHttpProcessor(
+ * ch.boye.httpclientandroidlib.protocol.HttpProcessor)} method.
+ */
+ public final HttpClientBuilder disableContentCompression() {
+ contentCompressionDisabled = true;
+ return this;
+ }
+
+ /**
+ * Disables authentication scheme caching.
+ * <p/>
+ * Please note this value can be overridden by the {@link #setHttpProcessor(
+ * ch.boye.httpclientandroidlib.protocol.HttpProcessor)} method.
+ */
+ public final HttpClientBuilder disableAuthCaching() {
+ this.authCachingDisabled = true;
+ return this;
+ }
+
+ /**
+ * Assigns {@link HttpProcessor} instance.
+ */
+ public final HttpClientBuilder setHttpProcessor(final HttpProcessor httpprocessor) {
+ this.httpprocessor = httpprocessor;
+ return this;
+ }
+
+ /**
+ * Assigns {@link HttpRequestRetryHandler} instance.
+ * <p/>
+ * Please note this value can be overridden by the {@link #disableAutomaticRetries()}
+ * method.
+ */
+ public final HttpClientBuilder setRetryHandler(final HttpRequestRetryHandler retryHandler) {
+ this.retryHandler = retryHandler;
+ return this;
+ }
+
+ /**
+ * Disables automatic request recovery and re-execution.
+ */
+ public final HttpClientBuilder disableAutomaticRetries() {
+ automaticRetriesDisabled = true;
+ return this;
+ }
+
+ /**
+ * Assigns default proxy value.
+ * <p/>
+ * Please note this value can be overridden by the {@link #setRoutePlanner(
+ * ch.boye.httpclientandroidlib.conn.routing.HttpRoutePlanner)} method.
+ */
+ public final HttpClientBuilder setProxy(final HttpHost proxy) {
+ this.proxy = proxy;
+ return this;
+ }
+
+ /**
+ * Assigns {@link HttpRoutePlanner} instance.
+ */
+ public final HttpClientBuilder setRoutePlanner(final HttpRoutePlanner routePlanner) {
+ this.routePlanner = routePlanner;
+ return this;
+ }
+
+ /**
+ * Assigns {@link RedirectStrategy} instance.
+ * <p/>
+ * Please note this value can be overridden by the {@link #disableRedirectHandling()}
+ * method.
+` */
+ public final HttpClientBuilder setRedirectStrategy(final RedirectStrategy redirectStrategy) {
+ this.redirectStrategy = redirectStrategy;
+ return this;
+ }
+
+ /**
+ * Disables automatic redirect handling.
+ */
+ public final HttpClientBuilder disableRedirectHandling() {
+ redirectHandlingDisabled = true;
+ return this;
+ }
+
+ /**
+ * Assigns {@link ConnectionBackoffStrategy} instance.
+ */
+ public final HttpClientBuilder setConnectionBackoffStrategy(
+ final ConnectionBackoffStrategy connectionBackoffStrategy) {
+ this.connectionBackoffStrategy = connectionBackoffStrategy;
+ return this;
+ }
+
+ /**
+ * Assigns {@link BackoffManager} instance.
+ */
+ public final HttpClientBuilder setBackoffManager(final BackoffManager backoffManager) {
+ this.backoffManager = backoffManager;
+ return this;
+ }
+
+ /**
+ * Assigns {@link ServiceUnavailableRetryStrategy} instance.
+ */
+ public final HttpClientBuilder setServiceUnavailableRetryStrategy(
+ final ServiceUnavailableRetryStrategy serviceUnavailStrategy) {
+ this.serviceUnavailStrategy = serviceUnavailStrategy;
+ return this;
+ }
+
+ /**
+ * Assigns default {@link CookieStore} instance which will be used for
+ * request execution if not explicitly set in the client execution context.
+ */
+ public final HttpClientBuilder setDefaultCookieStore(final CookieStore cookieStore) {
+ this.cookieStore = cookieStore;
+ return this;
+ }
+
+ /**
+ * Assigns default {@link CredentialsProvider} instance which will be used
+ * for request execution if not explicitly set in the client execution
+ * context.
+ */
+ public final HttpClientBuilder setDefaultCredentialsProvider(
+ final CredentialsProvider credentialsProvider) {
+ this.credentialsProvider = credentialsProvider;
+ return this;
+ }
+
+ /**
+ * Assigns default {@link ch.boye.httpclientandroidlib.auth.AuthScheme} registry which will
+ * be used for request execution if not explicitly set in the client execution
+ * context.
+ */
+ public final HttpClientBuilder setDefaultAuthSchemeRegistry(
+ final Lookup<AuthSchemeProvider> authSchemeRegistry) {
+ this.authSchemeRegistry = authSchemeRegistry;
+ return this;
+ }
+
+ /**
+ * Assigns default {@link ch.boye.httpclientandroidlib.cookie.CookieSpec} registry which will
+ * be used for request execution if not explicitly set in the client execution
+ * context.
+ */
+ public final HttpClientBuilder setDefaultCookieSpecRegistry(
+ final Lookup<CookieSpecProvider> cookieSpecRegistry) {
+ this.cookieSpecRegistry = cookieSpecRegistry;
+ return this;
+ }
+
+ /**
+ * Assigns default {@link RequestConfig} instance which will be used
+ * for request execution if not explicitly set in the client execution
+ * context.
+ */
+ public final HttpClientBuilder setDefaultRequestConfig(final RequestConfig config) {
+ this.defaultRequestConfig = config;
+ return this;
+ }
+
+ /**
+ * Use system properties when creating and configuring default
+ * implementations.
+ */
+ public final HttpClientBuilder useSystemProperties() {
+ systemProperties = true;
+ return this;
+ }
+
+ /**
+ * For internal use.
+ */
+ protected ClientExecChain decorateMainExec(final ClientExecChain mainExec) {
+ return mainExec;
+ }
+
+ /**
+ * For internal use.
+ */
+ protected ClientExecChain decorateProtocolExec(final ClientExecChain protocolExec) {
+ return protocolExec;
+ }
+
+ /**
+ * For internal use.
+ */
+ protected void addCloseable(final Closeable closeable) {
+ if (closeable == null) {
+ return;
+ }
+ if (closeables == null) {
+ closeables = new ArrayList<Closeable>();
+ }
+ closeables.add(closeable);
+ }
+
+ private static String[] split(final String s) {
+ if (TextUtils.isBlank(s)) {
+ return null;
+ }
+ return s.split(" *, *");
+ }
+
+ public CloseableHttpClient build() {
+ // Create main request executor
+ HttpRequestExecutor requestExec = this.requestExec;
+ if (requestExec == null) {
+ requestExec = new HttpRequestExecutor();
+ }
+ HttpClientConnectionManager connManager = this.connManager;
+ if (connManager == null) {
+ LayeredConnectionSocketFactory sslSocketFactory = this.sslSocketFactory;
+ if (sslSocketFactory == null) {
+ final String[] supportedProtocols = systemProperties ? split(
+ System.getProperty("https.protocols")) : null;
+ final String[] supportedCipherSuites = systemProperties ? split(
+ System.getProperty("https.cipherSuites")) : null;
+ X509HostnameVerifier hostnameVerifier = this.hostnameVerifier;
+ if (hostnameVerifier == null) {
+ hostnameVerifier = SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER;
+ }
+ if (sslcontext != null) {
+ sslSocketFactory = new SSLConnectionSocketFactory(
+ sslcontext, supportedProtocols, supportedCipherSuites, hostnameVerifier);
+ } else {
+ if (systemProperties) {
+ sslSocketFactory = new SSLConnectionSocketFactory(
+ (SSLSocketFactory) SSLSocketFactory.getDefault(),
+ supportedProtocols, supportedCipherSuites, hostnameVerifier);
+ } else {
+ sslSocketFactory = new SSLConnectionSocketFactory(
+ SSLContexts.createDefault(),
+ hostnameVerifier);
+ }
+ }
+ }
+ @SuppressWarnings("resource")
+ final PoolingHttpClientConnectionManager poolingmgr = new PoolingHttpClientConnectionManager(
+ RegistryBuilder.<ConnectionSocketFactory>create()
+ .register("http", PlainConnectionSocketFactory.getSocketFactory())
+ .register("https", sslSocketFactory)
+ .build());
+ if (defaultSocketConfig != null) {
+ poolingmgr.setDefaultSocketConfig(defaultSocketConfig);
+ }
+ if (defaultConnectionConfig != null) {
+ poolingmgr.setDefaultConnectionConfig(defaultConnectionConfig);
+ }
+ if (systemProperties) {
+ String s = System.getProperty("http.keepAlive", "true");
+ if ("true".equalsIgnoreCase(s)) {
+ s = System.getProperty("http.maxConnections", "5");
+ final int max = Integer.parseInt(s);
+ poolingmgr.setDefaultMaxPerRoute(max);
+ poolingmgr.setMaxTotal(2 * max);
+ }
+ }
+ if (maxConnTotal > 0) {
+ poolingmgr.setMaxTotal(maxConnTotal);
+ }
+ if (maxConnPerRoute > 0) {
+ poolingmgr.setDefaultMaxPerRoute(maxConnPerRoute);
+ }
+ connManager = poolingmgr;
+ }
+ ConnectionReuseStrategy reuseStrategy = this.reuseStrategy;
+ if (reuseStrategy == null) {
+ if (systemProperties) {
+ final String s = System.getProperty("http.keepAlive", "true");
+ if ("true".equalsIgnoreCase(s)) {
+ reuseStrategy = DefaultConnectionReuseStrategy.INSTANCE;
+ } else {
+ reuseStrategy = NoConnectionReuseStrategy.INSTANCE;
+ }
+ } else {
+ reuseStrategy = DefaultConnectionReuseStrategy.INSTANCE;
+ }
+ }
+ ConnectionKeepAliveStrategy keepAliveStrategy = this.keepAliveStrategy;
+ if (keepAliveStrategy == null) {
+ keepAliveStrategy = DefaultConnectionKeepAliveStrategy.INSTANCE;
+ }
+ AuthenticationStrategy targetAuthStrategy = this.targetAuthStrategy;
+ if (targetAuthStrategy == null) {
+ targetAuthStrategy = TargetAuthenticationStrategy.INSTANCE;
+ }
+ AuthenticationStrategy proxyAuthStrategy = this.proxyAuthStrategy;
+ if (proxyAuthStrategy == null) {
+ proxyAuthStrategy = ProxyAuthenticationStrategy.INSTANCE;
+ }
+ UserTokenHandler userTokenHandler = this.userTokenHandler;
+ if (userTokenHandler == null) {
+ if (!connectionStateDisabled) {
+ userTokenHandler = DefaultUserTokenHandler.INSTANCE;
+ } else {
+ userTokenHandler = NoopUserTokenHandler.INSTANCE;
+ }
+ }
+ ClientExecChain execChain = new MainClientExec(
+ requestExec,
+ connManager,
+ reuseStrategy,
+ keepAliveStrategy,
+ targetAuthStrategy,
+ proxyAuthStrategy,
+ userTokenHandler);
+
+ execChain = decorateMainExec(execChain);
+
+ HttpProcessor httpprocessor = this.httpprocessor;
+ if (httpprocessor == null) {
+
+ String userAgent = this.userAgent;
+ if (userAgent == null) {
+ if (systemProperties) {
+ userAgent = System.getProperty("http.agent");
+ }
+ if (userAgent == null) {
+ userAgent = DEFAULT_USER_AGENT;
+ }
+ }
+
+ final HttpProcessorBuilder b = HttpProcessorBuilder.create();
+ if (requestFirst != null) {
+ for (final HttpRequestInterceptor i: requestFirst) {
+ b.addFirst(i);
+ }
+ }
+ if (responseFirst != null) {
+ for (final HttpResponseInterceptor i: responseFirst) {
+ b.addFirst(i);
+ }
+ }
+ b.addAll(
+ new RequestDefaultHeaders(defaultHeaders),
+ new RequestContent(),
+ new RequestTargetHost(),
+ new RequestClientConnControl(),
+ new RequestUserAgent(userAgent),
+ new RequestExpectContinue());
+ if (!cookieManagementDisabled) {
+ b.add(new RequestAddCookies());
+ }
+ if (!contentCompressionDisabled) {
+ b.add(new RequestAcceptEncoding());
+ }
+ if (!authCachingDisabled) {
+ b.add(new RequestAuthCache());
+ }
+ if (!cookieManagementDisabled) {
+ b.add(new ResponseProcessCookies());
+ }
+ if (!contentCompressionDisabled) {
+ b.add(new ResponseContentEncoding());
+ }
+ if (requestLast != null) {
+ for (final HttpRequestInterceptor i: requestLast) {
+ b.addLast(i);
+ }
+ }
+ if (responseLast != null) {
+ for (final HttpResponseInterceptor i: responseLast) {
+ b.addLast(i);
+ }
+ }
+ httpprocessor = b.build();
+ }
+ execChain = new ProtocolExec(execChain, httpprocessor);
+
+ execChain = decorateProtocolExec(execChain);
+
+ // Add request retry executor, if not disabled
+ if (!automaticRetriesDisabled) {
+ HttpRequestRetryHandler retryHandler = this.retryHandler;
+ if (retryHandler == null) {
+ retryHandler = DefaultHttpRequestRetryHandler.INSTANCE;
+ }
+ execChain = new RetryExec(execChain, retryHandler);
+ }
+
+ HttpRoutePlanner routePlanner = this.routePlanner;
+ if (routePlanner == null) {
+ SchemePortResolver schemePortResolver = this.schemePortResolver;
+ if (schemePortResolver == null) {
+ schemePortResolver = DefaultSchemePortResolver.INSTANCE;
+ }
+ if (proxy != null) {
+ routePlanner = new DefaultProxyRoutePlanner(proxy, schemePortResolver);
+ } else if (systemProperties) {
+ routePlanner = new SystemDefaultRoutePlanner(
+ schemePortResolver, ProxySelector.getDefault());
+ } else {
+ routePlanner = new DefaultRoutePlanner(schemePortResolver);
+ }
+ }
+ // Add redirect executor, if not disabled
+ if (!redirectHandlingDisabled) {
+ RedirectStrategy redirectStrategy = this.redirectStrategy;
+ if (redirectStrategy == null) {
+ redirectStrategy = DefaultRedirectStrategy.INSTANCE;
+ }
+ execChain = new RedirectExec(execChain, routePlanner, redirectStrategy);
+ }
+
+ // Optionally, add service unavailable retry executor
+ final ServiceUnavailableRetryStrategy serviceUnavailStrategy = this.serviceUnavailStrategy;
+ if (serviceUnavailStrategy != null) {
+ execChain = new ServiceUnavailableRetryExec(execChain, serviceUnavailStrategy);
+ }
+ // Optionally, add connection back-off executor
+ final BackoffManager backoffManager = this.backoffManager;
+ final ConnectionBackoffStrategy connectionBackoffStrategy = this.connectionBackoffStrategy;
+ if (backoffManager != null && connectionBackoffStrategy != null) {
+ execChain = new BackoffStrategyExec(execChain, connectionBackoffStrategy, backoffManager);
+ }
+
+ Lookup<AuthSchemeProvider> authSchemeRegistry = this.authSchemeRegistry;
+ if (authSchemeRegistry == null) {
+ authSchemeRegistry = RegistryBuilder.<AuthSchemeProvider>create()
+ .register(AuthSchemes.BASIC, new BasicSchemeFactory())
+ .register(AuthSchemes.DIGEST, new DigestSchemeFactory())
+ .register(AuthSchemes.NTLM, new NTLMSchemeFactory())
+ /* SPNegoSchemeFactory removed by HttpClient for Android script. */
+ /* KerberosSchemeFactory removed by HttpClient for Android script. */
+ .build();
+ }
+ Lookup<CookieSpecProvider> cookieSpecRegistry = this.cookieSpecRegistry;
+ if (cookieSpecRegistry == null) {
+ cookieSpecRegistry = RegistryBuilder.<CookieSpecProvider>create()
+ .register(CookieSpecs.BEST_MATCH, new BestMatchSpecFactory())
+ .register(CookieSpecs.STANDARD, new RFC2965SpecFactory())
+ .register(CookieSpecs.BROWSER_COMPATIBILITY, new BrowserCompatSpecFactory())
+ .register(CookieSpecs.NETSCAPE, new NetscapeDraftSpecFactory())
+ .register(CookieSpecs.IGNORE_COOKIES, new IgnoreSpecFactory())
+ .register("rfc2109", new RFC2109SpecFactory())
+ .register("rfc2965", new RFC2965SpecFactory())
+ .build();
+ }
+
+ CookieStore defaultCookieStore = this.cookieStore;
+ if (defaultCookieStore == null) {
+ defaultCookieStore = new BasicCookieStore();
+ }
+
+ CredentialsProvider defaultCredentialsProvider = this.credentialsProvider;
+ if (defaultCredentialsProvider == null) {
+ if (systemProperties) {
+ defaultCredentialsProvider = new SystemDefaultCredentialsProvider();
+ } else {
+ defaultCredentialsProvider = new BasicCredentialsProvider();
+ }
+ }
+
+ return new InternalHttpClient(
+ execChain,
+ connManager,
+ routePlanner,
+ cookieSpecRegistry,
+ authSchemeRegistry,
+ defaultCookieStore,
+ defaultCredentialsProvider,
+ defaultRequestConfig != null ? defaultRequestConfig : RequestConfig.DEFAULT,
+ closeables != null ? new ArrayList<Closeable>(closeables) : null);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/HttpClients.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/HttpClients.java
new file mode 100644
index 0000000000..294ac636b8
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/HttpClients.java
@@ -0,0 +1,85 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.conn.HttpClientConnectionManager;
+import ch.boye.httpclientandroidlib.impl.conn.PoolingHttpClientConnectionManager;
+
+/**
+ * Factory methods for {@link CloseableHttpClient} instances.
+ * @since 4.3
+ */
+@Immutable
+public class HttpClients {
+
+ private HttpClients() {
+ super();
+ }
+
+ /**
+ * Creates builder object for construction of custom
+ * {@link CloseableHttpClient} instances.
+ */
+ public static HttpClientBuilder custom() {
+ return HttpClientBuilder.create();
+ }
+
+ /**
+ * Creates {@link CloseableHttpClient} instance with default
+ * configuration.
+ */
+ public static CloseableHttpClient createDefault() {
+ return HttpClientBuilder.create().build();
+ }
+
+ /**
+ * Creates {@link CloseableHttpClient} instance with default
+ * configuration based on ssytem properties.
+ */
+ public static CloseableHttpClient createSystem() {
+ return HttpClientBuilder.create().useSystemProperties().build();
+ }
+
+ /**
+ * Creates {@link CloseableHttpClient} instance that implements
+ * the most basic HTTP protocol support.
+ */
+ public static CloseableHttpClient createMinimal() {
+ return new MinimalHttpClient(new PoolingHttpClientConnectionManager());
+ }
+
+ /**
+ * Creates {@link CloseableHttpClient} instance that implements
+ * the most basic HTTP protocol support.
+ */
+ public static CloseableHttpClient createMinimal(final HttpClientConnectionManager connManager) {
+ return new MinimalHttpClient(connManager);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/HttpRequestFutureTask.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/HttpRequestFutureTask.java
new file mode 100644
index 0000000000..b5f932403e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/HttpRequestFutureTask.java
@@ -0,0 +1,118 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.util.concurrent.FutureTask;
+
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+
+/**
+ * FutureTask implementation that wraps a HttpAsyncClientCallable and exposes various task
+ * specific metrics.
+ *
+ * @param <V>
+ */
+public class HttpRequestFutureTask<V> extends FutureTask<V> {
+
+ private final HttpUriRequest request;
+ private final HttpRequestTaskCallable<V> callable;
+
+ public HttpRequestFutureTask(
+ final HttpUriRequest request,
+ final HttpRequestTaskCallable<V> httpCallable) {
+ super(httpCallable);
+ this.request = request;
+ this.callable = httpCallable;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.util.concurrent.FutureTask#cancel(boolean)
+ */
+ @Override
+ public boolean cancel(final boolean mayInterruptIfRunning) {
+ callable.cancel();
+ if (mayInterruptIfRunning) {
+ request.abort();
+ }
+ return super.cancel(mayInterruptIfRunning);
+ }
+
+ /**
+ * @return the time in millis the task was scheduled.
+ */
+ public long scheduledTime() {
+ return callable.getScheduled();
+ }
+
+ /**
+ * @return the time in millis the task was started.
+ */
+ public long startedTime() {
+ return callable.getStarted();
+ }
+
+ /**
+ * @return the time in millis the task was finished/cancelled.
+ */
+ public long endedTime() {
+ if (isDone()) {
+ return callable.getEnded();
+ } else {
+ throw new IllegalStateException("Task is not done yet");
+ }
+ }
+
+ /**
+ * @return the time in millis it took to make the request (excluding the time it was
+ * scheduled to be executed).
+ */
+ public long requestDuration() {
+ if (isDone()) {
+ return endedTime() - startedTime();
+ } else {
+ throw new IllegalStateException("Task is not done yet");
+ }
+ }
+
+ /**
+ * @return the time in millis it took to execute the task from the moment it was scheduled.
+ */
+ public long taskDuration() {
+ if (isDone()) {
+ return endedTime() - scheduledTime();
+ } else {
+ throw new IllegalStateException("Task is not done yet");
+ }
+ }
+
+ @Override
+ public String toString() {
+ return request.getRequestLine().getUri();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/HttpRequestTaskCallable.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/HttpRequestTaskCallable.java
new file mode 100644
index 0000000000..42e95f0aa2
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/HttpRequestTaskCallable.java
@@ -0,0 +1,119 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import ch.boye.httpclientandroidlib.client.HttpClient;
+import ch.boye.httpclientandroidlib.client.ResponseHandler;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.concurrent.FutureCallback;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+class HttpRequestTaskCallable<V> implements Callable<V> {
+
+ private final HttpUriRequest request;
+ private final HttpClient httpclient;
+ private final AtomicBoolean cancelled = new AtomicBoolean(false);
+
+ private final long scheduled = System.currentTimeMillis();
+ private long started = -1;
+ private long ended = -1;
+
+ private final HttpContext context;
+ private final ResponseHandler<V> responseHandler;
+ private final FutureCallback<V> callback;
+
+ private final FutureRequestExecutionMetrics metrics;
+
+ HttpRequestTaskCallable(
+ final HttpClient httpClient,
+ final HttpUriRequest request,
+ final HttpContext context,
+ final ResponseHandler<V> responseHandler,
+ final FutureCallback<V> callback,
+ final FutureRequestExecutionMetrics metrics) {
+ this.httpclient = httpClient;
+ this.responseHandler = responseHandler;
+ this.request = request;
+ this.context = context;
+ this.callback = callback;
+ this.metrics = metrics;
+ }
+
+ public long getScheduled() {
+ return scheduled;
+ }
+
+ public long getStarted() {
+ return started;
+ }
+
+ public long getEnded() {
+ return ended;
+ }
+
+ public V call() throws Exception {
+ if (!cancelled.get()) {
+ try {
+ metrics.getActiveConnections().incrementAndGet();
+ started = System.currentTimeMillis();
+ try {
+ metrics.getScheduledConnections().decrementAndGet();
+ final V result = httpclient.execute(request, responseHandler, context);
+ ended = System.currentTimeMillis();
+ metrics.getSuccessfulConnections().increment(started);
+ if (callback != null) {
+ callback.completed(result);
+ }
+ return result;
+ } catch (final Exception e) {
+ metrics.getFailedConnections().increment(started);
+ ended = System.currentTimeMillis();
+ if (callback != null) {
+ callback.failed(e);
+ }
+ throw e;
+ }
+ } finally {
+ metrics.getRequests().increment(started);
+ metrics.getTasks().increment(started);
+ metrics.getActiveConnections().decrementAndGet();
+ }
+ } else {
+ throw new IllegalStateException("call has been cancelled for request " + request.getURI());
+ }
+ }
+
+ public void cancel() {
+ cancelled.set(true);
+ if (callback != null) {
+ callback.cancelled();
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/InternalHttpClient.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/InternalHttpClient.java
new file mode 100644
index 0000000000..c445fe1811
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/InternalHttpClient.java
@@ -0,0 +1,242 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.auth.AuthSchemeProvider;
+import ch.boye.httpclientandroidlib.auth.AuthState;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+import ch.boye.httpclientandroidlib.client.CookieStore;
+import ch.boye.httpclientandroidlib.client.CredentialsProvider;
+import ch.boye.httpclientandroidlib.client.config.RequestConfig;
+import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
+import ch.boye.httpclientandroidlib.client.methods.Configurable;
+import ch.boye.httpclientandroidlib.client.methods.HttpExecutionAware;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestWrapper;
+import ch.boye.httpclientandroidlib.client.params.ClientPNames;
+import ch.boye.httpclientandroidlib.client.params.HttpClientParamConfig;
+import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
+import ch.boye.httpclientandroidlib.config.Lookup;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionManager;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionRequest;
+import ch.boye.httpclientandroidlib.conn.HttpClientConnectionManager;
+import ch.boye.httpclientandroidlib.conn.ManagedClientConnection;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoutePlanner;
+import ch.boye.httpclientandroidlib.conn.scheme.SchemeRegistry;
+import ch.boye.httpclientandroidlib.cookie.CookieSpecProvider;
+import ch.boye.httpclientandroidlib.impl.execchain.ClientExecChain;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.params.HttpParamsNames;
+import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Internal class.
+ *
+ * @since 4.3
+ */
+@ThreadSafe
+@SuppressWarnings("deprecation")
+class InternalHttpClient extends CloseableHttpClient {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ private final ClientExecChain execChain;
+ private final HttpClientConnectionManager connManager;
+ private final HttpRoutePlanner routePlanner;
+ private final Lookup<CookieSpecProvider> cookieSpecRegistry;
+ private final Lookup<AuthSchemeProvider> authSchemeRegistry;
+ private final CookieStore cookieStore;
+ private final CredentialsProvider credentialsProvider;
+ private final RequestConfig defaultConfig;
+ private final List<Closeable> closeables;
+
+ public InternalHttpClient(
+ final ClientExecChain execChain,
+ final HttpClientConnectionManager connManager,
+ final HttpRoutePlanner routePlanner,
+ final Lookup<CookieSpecProvider> cookieSpecRegistry,
+ final Lookup<AuthSchemeProvider> authSchemeRegistry,
+ final CookieStore cookieStore,
+ final CredentialsProvider credentialsProvider,
+ final RequestConfig defaultConfig,
+ final List<Closeable> closeables) {
+ super();
+ Args.notNull(execChain, "HTTP client exec chain");
+ Args.notNull(connManager, "HTTP connection manager");
+ Args.notNull(routePlanner, "HTTP route planner");
+ this.execChain = execChain;
+ this.connManager = connManager;
+ this.routePlanner = routePlanner;
+ this.cookieSpecRegistry = cookieSpecRegistry;
+ this.authSchemeRegistry = authSchemeRegistry;
+ this.cookieStore = cookieStore;
+ this.credentialsProvider = credentialsProvider;
+ this.defaultConfig = defaultConfig;
+ this.closeables = closeables;
+ }
+
+ private HttpRoute determineRoute(
+ final HttpHost target,
+ final HttpRequest request,
+ final HttpContext context) throws HttpException {
+ HttpHost host = target;
+ if (host == null) {
+ host = (HttpHost) request.getParams().getParameter(ClientPNames.DEFAULT_HOST);
+ }
+ return this.routePlanner.determineRoute(host, request, context);
+ }
+
+ private void setupContext(final HttpClientContext context) {
+ if (context.getAttribute(HttpClientContext.TARGET_AUTH_STATE) == null) {
+ context.setAttribute(HttpClientContext.TARGET_AUTH_STATE, new AuthState());
+ }
+ if (context.getAttribute(HttpClientContext.PROXY_AUTH_STATE) == null) {
+ context.setAttribute(HttpClientContext.PROXY_AUTH_STATE, new AuthState());
+ }
+ if (context.getAttribute(HttpClientContext.AUTHSCHEME_REGISTRY) == null) {
+ context.setAttribute(HttpClientContext.AUTHSCHEME_REGISTRY, this.authSchemeRegistry);
+ }
+ if (context.getAttribute(HttpClientContext.COOKIESPEC_REGISTRY) == null) {
+ context.setAttribute(HttpClientContext.COOKIESPEC_REGISTRY, this.cookieSpecRegistry);
+ }
+ if (context.getAttribute(HttpClientContext.COOKIE_STORE) == null) {
+ context.setAttribute(HttpClientContext.COOKIE_STORE, this.cookieStore);
+ }
+ if (context.getAttribute(HttpClientContext.CREDS_PROVIDER) == null) {
+ context.setAttribute(HttpClientContext.CREDS_PROVIDER, this.credentialsProvider);
+ }
+ if (context.getAttribute(HttpClientContext.REQUEST_CONFIG) == null) {
+ context.setAttribute(HttpClientContext.REQUEST_CONFIG, this.defaultConfig);
+ }
+ }
+
+ @Override
+ protected CloseableHttpResponse doExecute(
+ final HttpHost target,
+ final HttpRequest request,
+ final HttpContext context) throws IOException, ClientProtocolException {
+ Args.notNull(request, "HTTP request");
+ HttpExecutionAware execAware = null;
+ if (request instanceof HttpExecutionAware) {
+ execAware = (HttpExecutionAware) request;
+ }
+ try {
+ final HttpRequestWrapper wrapper = HttpRequestWrapper.wrap(request);
+ final HttpClientContext localcontext = HttpClientContext.adapt(
+ context != null ? context : new BasicHttpContext());
+ RequestConfig config = null;
+ if (request instanceof Configurable) {
+ config = ((Configurable) request).getConfig();
+ }
+ if (config == null) {
+ final HttpParams params = request.getParams();
+ if (params instanceof HttpParamsNames) {
+ if (!((HttpParamsNames) params).getNames().isEmpty()) {
+ config = HttpClientParamConfig.getRequestConfig(params);
+ }
+ } else {
+ config = HttpClientParamConfig.getRequestConfig(params);
+ }
+ }
+ if (config != null) {
+ localcontext.setRequestConfig(config);
+ }
+ setupContext(localcontext);
+ final HttpRoute route = determineRoute(target, wrapper, localcontext);
+ return this.execChain.execute(route, wrapper, localcontext, execAware);
+ } catch (final HttpException httpException) {
+ throw new ClientProtocolException(httpException);
+ }
+ }
+
+ public void close() {
+ this.connManager.shutdown();
+ if (this.closeables != null) {
+ for (final Closeable closeable: this.closeables) {
+ try {
+ closeable.close();
+ } catch (final IOException ex) {
+ this.log.error(ex.getMessage(), ex);
+ }
+ }
+ }
+ }
+
+ public HttpParams getParams() {
+ throw new UnsupportedOperationException();
+ }
+
+ public ClientConnectionManager getConnectionManager() {
+
+ return new ClientConnectionManager() {
+
+ public void shutdown() {
+ connManager.shutdown();
+ }
+
+ public ClientConnectionRequest requestConnection(
+ final HttpRoute route, final Object state) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void releaseConnection(
+ final ManagedClientConnection conn,
+ final long validDuration, final TimeUnit timeUnit) {
+ throw new UnsupportedOperationException();
+ }
+
+ public SchemeRegistry getSchemeRegistry() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void closeIdleConnections(final long idletime, final TimeUnit tunit) {
+ connManager.closeIdleConnections(idletime, tunit);
+ }
+
+ public void closeExpiredConnections() {
+ connManager.closeExpiredConnections();
+ }
+
+ };
+
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/LaxRedirectStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/LaxRedirectStrategy.java
new file mode 100644
index 0000000000..47688e6420
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/LaxRedirectStrategy.java
@@ -0,0 +1,65 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.methods.HttpGet;
+import ch.boye.httpclientandroidlib.client.methods.HttpHead;
+import ch.boye.httpclientandroidlib.client.methods.HttpPost;
+
+/**
+ * Lax {@link ch.boye.httpclientandroidlib.client.RedirectStrategy} implementation
+ * that automatically redirects all HEAD, GET and POST requests.
+ * This strategy relaxes restrictions on automatic redirection of
+ * POST methods imposed by the HTTP specification.
+ *
+ * @since 4.2
+ */
+@Immutable
+public class LaxRedirectStrategy extends DefaultRedirectStrategy {
+
+ /**
+ * Redirectable methods.
+ */
+ private static final String[] REDIRECT_METHODS = new String[] {
+ HttpGet.METHOD_NAME,
+ HttpPost.METHOD_NAME,
+ HttpHead.METHOD_NAME
+ };
+
+ @Override
+ protected boolean isRedirectable(final String method) {
+ for (final String m: REDIRECT_METHODS) {
+ if (m.equalsIgnoreCase(method)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/MinimalHttpClient.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/MinimalHttpClient.java
new file mode 100644
index 0000000000..8b0c211272
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/MinimalHttpClient.java
@@ -0,0 +1,156 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+import ch.boye.httpclientandroidlib.client.config.RequestConfig;
+import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
+import ch.boye.httpclientandroidlib.client.methods.Configurable;
+import ch.boye.httpclientandroidlib.client.methods.HttpExecutionAware;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestWrapper;
+import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionManager;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionRequest;
+import ch.boye.httpclientandroidlib.conn.HttpClientConnectionManager;
+import ch.boye.httpclientandroidlib.conn.ManagedClientConnection;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.conn.scheme.SchemeRegistry;
+import ch.boye.httpclientandroidlib.impl.DefaultConnectionReuseStrategy;
+import ch.boye.httpclientandroidlib.impl.execchain.MinimalClientExec;
+import ch.boye.httpclientandroidlib.params.BasicHttpParams;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.protocol.HttpRequestExecutor;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Internal class.
+ *
+ * @since 4.3
+ */
+@ThreadSafe
+@SuppressWarnings("deprecation")
+class MinimalHttpClient extends CloseableHttpClient {
+
+ private final HttpClientConnectionManager connManager;
+ private final MinimalClientExec requestExecutor;
+ private final HttpParams params;
+
+ public MinimalHttpClient(
+ final HttpClientConnectionManager connManager) {
+ super();
+ this.connManager = Args.notNull(connManager, "HTTP connection manager");
+ this.requestExecutor = new MinimalClientExec(
+ new HttpRequestExecutor(),
+ connManager,
+ DefaultConnectionReuseStrategy.INSTANCE,
+ DefaultConnectionKeepAliveStrategy.INSTANCE);
+ this.params = new BasicHttpParams();
+ }
+
+ @Override
+ protected CloseableHttpResponse doExecute(
+ final HttpHost target,
+ final HttpRequest request,
+ final HttpContext context) throws IOException, ClientProtocolException {
+ Args.notNull(target, "Target host");
+ Args.notNull(request, "HTTP request");
+ HttpExecutionAware execAware = null;
+ if (request instanceof HttpExecutionAware) {
+ execAware = (HttpExecutionAware) request;
+ }
+ try {
+ final HttpRequestWrapper wrapper = HttpRequestWrapper.wrap(request);
+ final HttpClientContext localcontext = HttpClientContext.adapt(
+ context != null ? context : new BasicHttpContext());
+ final HttpRoute route = new HttpRoute(target);
+ RequestConfig config = null;
+ if (request instanceof Configurable) {
+ config = ((Configurable) request).getConfig();
+ }
+ if (config != null) {
+ localcontext.setRequestConfig(config);
+ }
+ return this.requestExecutor.execute(route, wrapper, localcontext, execAware);
+ } catch (final HttpException httpException) {
+ throw new ClientProtocolException(httpException);
+ }
+ }
+
+ public HttpParams getParams() {
+ return this.params;
+ }
+
+ public void close() {
+ this.connManager.shutdown();
+ }
+
+ public ClientConnectionManager getConnectionManager() {
+
+ return new ClientConnectionManager() {
+
+ public void shutdown() {
+ connManager.shutdown();
+ }
+
+ public ClientConnectionRequest requestConnection(
+ final HttpRoute route, final Object state) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void releaseConnection(
+ final ManagedClientConnection conn,
+ final long validDuration, final TimeUnit timeUnit) {
+ throw new UnsupportedOperationException();
+ }
+
+ public SchemeRegistry getSchemeRegistry() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void closeIdleConnections(final long idletime, final TimeUnit tunit) {
+ connManager.closeIdleConnections(idletime, tunit);
+ }
+
+ public void closeExpiredConnections() {
+ connManager.closeExpiredConnections();
+ }
+
+ };
+
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/NoopUserTokenHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/NoopUserTokenHandler.java
new file mode 100644
index 0000000000..1e8eb3bde0
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/NoopUserTokenHandler.java
@@ -0,0 +1,47 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.UserTokenHandler;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * Noop implementation of {@link UserTokenHandler} that always returns <code>null</code>.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class NoopUserTokenHandler implements UserTokenHandler {
+
+ public static final NoopUserTokenHandler INSTANCE = new NoopUserTokenHandler();
+
+ public Object getUserToken(final HttpContext context) {
+ return null;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/NullBackoffStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/NullBackoffStrategy.java
new file mode 100644
index 0000000000..8cad78d754
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/NullBackoffStrategy.java
@@ -0,0 +1,47 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.ConnectionBackoffStrategy;
+
+/**
+ * This is a {@link ConnectionBackoffStrategy} that never backs off,
+ * for compatibility with existing behavior.
+ *
+ * @since 4.2
+ */
+public class NullBackoffStrategy implements ConnectionBackoffStrategy {
+
+ public boolean shouldBackoff(final Throwable t) {
+ return false;
+ }
+
+ public boolean shouldBackoff(final HttpResponse resp) {
+ return false;
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/ProxyAuthenticationStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/ProxyAuthenticationStrategy.java
new file mode 100644
index 0000000000..636143630b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/ProxyAuthenticationStrategy.java
@@ -0,0 +1,57 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.util.Collection;
+
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.auth.AUTH;
+import ch.boye.httpclientandroidlib.client.config.RequestConfig;
+
+/**
+ * Default {@link ch.boye.httpclientandroidlib.client.AuthenticationStrategy} implementation
+ * for proxy host authentication.
+ *
+ * @since 4.2
+ */
+@Immutable
+public class ProxyAuthenticationStrategy extends AuthenticationStrategyImpl {
+
+ public static final ProxyAuthenticationStrategy INSTANCE = new ProxyAuthenticationStrategy();
+
+ public ProxyAuthenticationStrategy() {
+ super(HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED, AUTH.PROXY_AUTH);
+ }
+
+ @Override
+ Collection<String> getPreferredAuthSchemes(final RequestConfig config) {
+ return config.getProxyPreferredAuthSchemes();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/ProxyClient.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/ProxyClient.java
new file mode 100644
index 0000000000..e1211bc4b2
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/ProxyClient.java
@@ -0,0 +1,254 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.io.IOException;
+import java.net.Socket;
+
+import ch.boye.httpclientandroidlib.ConnectionReuseStrategy;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.auth.AUTH;
+import ch.boye.httpclientandroidlib.auth.AuthSchemeRegistry;
+import ch.boye.httpclientandroidlib.auth.AuthScope;
+import ch.boye.httpclientandroidlib.auth.AuthState;
+import ch.boye.httpclientandroidlib.auth.Credentials;
+import ch.boye.httpclientandroidlib.client.config.AuthSchemes;
+import ch.boye.httpclientandroidlib.client.config.RequestConfig;
+import ch.boye.httpclientandroidlib.client.params.HttpClientParamConfig;
+import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
+import ch.boye.httpclientandroidlib.client.protocol.RequestClientConnControl;
+import ch.boye.httpclientandroidlib.config.ConnectionConfig;
+import ch.boye.httpclientandroidlib.conn.HttpConnectionFactory;
+import ch.boye.httpclientandroidlib.conn.ManagedHttpClientConnection;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.conn.routing.RouteInfo.LayerType;
+import ch.boye.httpclientandroidlib.conn.routing.RouteInfo.TunnelType;
+import ch.boye.httpclientandroidlib.entity.BufferedHttpEntity;
+import ch.boye.httpclientandroidlib.impl.DefaultConnectionReuseStrategy;
+import ch.boye.httpclientandroidlib.impl.auth.BasicSchemeFactory;
+import ch.boye.httpclientandroidlib.impl.auth.DigestSchemeFactory;
+import ch.boye.httpclientandroidlib.impl.auth.HttpAuthenticator;
+/* KerberosSchemeFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.impl.auth.NTLMSchemeFactory;
+/* SPNegoSchemeFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.impl.conn.ManagedHttpClientConnectionFactory;
+import ch.boye.httpclientandroidlib.impl.execchain.TunnelRefusedException;
+import ch.boye.httpclientandroidlib.message.BasicHttpRequest;
+import ch.boye.httpclientandroidlib.params.BasicHttpParams;
+import ch.boye.httpclientandroidlib.params.HttpParamConfig;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.protocol.HttpCoreContext;
+import ch.boye.httpclientandroidlib.protocol.HttpProcessor;
+import ch.boye.httpclientandroidlib.protocol.HttpRequestExecutor;
+import ch.boye.httpclientandroidlib.protocol.ImmutableHttpProcessor;
+import ch.boye.httpclientandroidlib.protocol.RequestTargetHost;
+import ch.boye.httpclientandroidlib.protocol.RequestUserAgent;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.EntityUtils;
+
+/**
+ * ProxyClient can be used to establish a tunnel via an HTTP proxy.
+ */
+@SuppressWarnings("deprecation")
+public class ProxyClient {
+
+ private final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory;
+ private final ConnectionConfig connectionConfig;
+ private final RequestConfig requestConfig;
+ private final HttpProcessor httpProcessor;
+ private final HttpRequestExecutor requestExec;
+ private final ProxyAuthenticationStrategy proxyAuthStrategy;
+ private final HttpAuthenticator authenticator;
+ private final AuthState proxyAuthState;
+ private final AuthSchemeRegistry authSchemeRegistry;
+ private final ConnectionReuseStrategy reuseStrategy;
+
+ /**
+ * @since 4.3
+ */
+ public ProxyClient(
+ final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory,
+ final ConnectionConfig connectionConfig,
+ final RequestConfig requestConfig) {
+ super();
+ this.connFactory = connFactory != null ? connFactory : ManagedHttpClientConnectionFactory.INSTANCE;
+ this.connectionConfig = connectionConfig != null ? connectionConfig : ConnectionConfig.DEFAULT;
+ this.requestConfig = requestConfig != null ? requestConfig : RequestConfig.DEFAULT;
+ this.httpProcessor = new ImmutableHttpProcessor(
+ new RequestTargetHost(), new RequestClientConnControl(), new RequestUserAgent());
+ this.requestExec = new HttpRequestExecutor();
+ this.proxyAuthStrategy = new ProxyAuthenticationStrategy();
+ this.authenticator = new HttpAuthenticator();
+ this.proxyAuthState = new AuthState();
+ this.authSchemeRegistry = new AuthSchemeRegistry();
+ this.authSchemeRegistry.register(AuthSchemes.BASIC, new BasicSchemeFactory());
+ this.authSchemeRegistry.register(AuthSchemes.DIGEST, new DigestSchemeFactory());
+ this.authSchemeRegistry.register(AuthSchemes.NTLM, new NTLMSchemeFactory());
+ /* SPNegoSchemeFactory removed by HttpClient for Android script. */
+ /* KerberosSchemeFactory removed by HttpClient for Android script. */
+ this.reuseStrategy = new DefaultConnectionReuseStrategy();
+ }
+
+ /**
+ * @deprecated (4.3) use {@link ProxyClient#ProxyClient(HttpConnectionFactory, ConnectionConfig, RequestConfig)}
+ */
+ @Deprecated
+ public ProxyClient(final HttpParams params) {
+ this(null,
+ HttpParamConfig.getConnectionConfig(params),
+ HttpClientParamConfig.getRequestConfig(params));
+ }
+
+ /**
+ * @since 4.3
+ */
+ public ProxyClient(final RequestConfig requestConfig) {
+ this(null, null, requestConfig);
+ }
+
+ public ProxyClient() {
+ this(null, null, null);
+ }
+
+ /**
+ * @deprecated (4.3) do not use.
+ */
+ @Deprecated
+ public HttpParams getParams() {
+ return new BasicHttpParams();
+ }
+
+ /**
+ * @deprecated (4.3) do not use.
+ */
+ @Deprecated
+ public AuthSchemeRegistry getAuthSchemeRegistry() {
+ return this.authSchemeRegistry;
+ }
+
+ public Socket tunnel(
+ final HttpHost proxy,
+ final HttpHost target,
+ final Credentials credentials) throws IOException, HttpException {
+ Args.notNull(proxy, "Proxy host");
+ Args.notNull(target, "Target host");
+ Args.notNull(credentials, "Credentials");
+ HttpHost host = target;
+ if (host.getPort() <= 0) {
+ host = new HttpHost(host.getHostName(), 80, host.getSchemeName());
+ }
+ final HttpRoute route = new HttpRoute(
+ host,
+ this.requestConfig.getLocalAddress(),
+ proxy, false, TunnelType.TUNNELLED, LayerType.PLAIN);
+
+ final ManagedHttpClientConnection conn = this.connFactory.create(
+ route, this.connectionConfig);
+ final HttpContext context = new BasicHttpContext();
+ HttpResponse response;
+
+ final HttpRequest connect = new BasicHttpRequest(
+ "CONNECT", host.toHostString(), HttpVersion.HTTP_1_1);
+
+ final BasicCredentialsProvider credsProvider = new BasicCredentialsProvider();
+ credsProvider.setCredentials(new AuthScope(proxy), credentials);
+
+ // Populate the execution context
+ context.setAttribute(HttpCoreContext.HTTP_TARGET_HOST, target);
+ context.setAttribute(HttpCoreContext.HTTP_CONNECTION, conn);
+ context.setAttribute(HttpCoreContext.HTTP_REQUEST, connect);
+ context.setAttribute(HttpClientContext.HTTP_ROUTE, route);
+ context.setAttribute(HttpClientContext.PROXY_AUTH_STATE, this.proxyAuthState);
+ context.setAttribute(HttpClientContext.CREDS_PROVIDER, credsProvider);
+ context.setAttribute(HttpClientContext.AUTHSCHEME_REGISTRY, this.authSchemeRegistry);
+ context.setAttribute(HttpClientContext.REQUEST_CONFIG, this.requestConfig);
+
+ this.requestExec.preProcess(connect, this.httpProcessor, context);
+
+ for (;;) {
+ if (!conn.isOpen()) {
+ final Socket socket = new Socket(proxy.getHostName(), proxy.getPort());
+ conn.bind(socket);
+ }
+
+ this.authenticator.generateAuthResponse(connect, this.proxyAuthState, context);
+
+ response = this.requestExec.execute(connect, conn, context);
+
+ final int status = response.getStatusLine().getStatusCode();
+ if (status < 200) {
+ throw new HttpException("Unexpected response to CONNECT request: " +
+ response.getStatusLine());
+ }
+ if (this.authenticator.isAuthenticationRequested(proxy, response,
+ this.proxyAuthStrategy, this.proxyAuthState, context)) {
+ if (this.authenticator.handleAuthChallenge(proxy, response,
+ this.proxyAuthStrategy, this.proxyAuthState, context)) {
+ // Retry request
+ if (this.reuseStrategy.keepAlive(response, context)) {
+ // Consume response content
+ final HttpEntity entity = response.getEntity();
+ EntityUtils.consume(entity);
+ } else {
+ conn.close();
+ }
+ // discard previous auth header
+ connect.removeHeaders(AUTH.PROXY_AUTH_RESP);
+ } else {
+ break;
+ }
+ } else {
+ break;
+ }
+ }
+
+ final int status = response.getStatusLine().getStatusCode();
+
+ if (status > 299) {
+
+ // Buffer response content
+ final HttpEntity entity = response.getEntity();
+ if (entity != null) {
+ response.setEntity(new BufferedHttpEntity(entity));
+ }
+
+ conn.close();
+ throw new TunnelRefusedException("CONNECT refused by proxy: " +
+ response.getStatusLine(), response);
+ }
+ return conn.getSocket();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/RedirectLocations.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/RedirectLocations.java
new file mode 100644
index 0000000000..872dc167ff
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/RedirectLocations.java
@@ -0,0 +1,227 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.net.URI;
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+
+/**
+ * This class represents a collection of {@link java.net.URI}s used
+ * as redirect locations.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe // HashSet/ArrayList are not synch.
+public class RedirectLocations extends AbstractList<Object> {
+
+ private final Set<URI> unique;
+ private final List<URI> all;
+
+ public RedirectLocations() {
+ super();
+ this.unique = new HashSet<URI>();
+ this.all = new ArrayList<URI>();
+ }
+
+ /**
+ * Test if the URI is present in the collection.
+ */
+ public boolean contains(final URI uri) {
+ return this.unique.contains(uri);
+ }
+
+ /**
+ * Adds a new URI to the collection.
+ */
+ public void add(final URI uri) {
+ this.unique.add(uri);
+ this.all.add(uri);
+ }
+
+ /**
+ * Removes a URI from the collection.
+ */
+ public boolean remove(final URI uri) {
+ final boolean removed = this.unique.remove(uri);
+ if (removed) {
+ final Iterator<URI> it = this.all.iterator();
+ while (it.hasNext()) {
+ final URI current = it.next();
+ if (current.equals(uri)) {
+ it.remove();
+ }
+ }
+ }
+ return removed;
+ }
+
+ /**
+ * Returns all redirect {@link URI}s in the order they were added to the collection.
+ *
+ * @return list of all URIs
+ *
+ * @since 4.1
+ */
+ public List<URI> getAll() {
+ return new ArrayList<URI>(this.all);
+ }
+
+ /**
+ * Returns the URI at the specified position in this list.
+ *
+ * @param index
+ * index of the location to return
+ * @return the URI at the specified position in this list
+ * @throws IndexOutOfBoundsException
+ * if the index is out of range (
+ * <tt>index &lt; 0 || index &gt;= size()</tt>)
+ * @since 4.3
+ */
+ @Override
+ public URI get(final int index) {
+ return this.all.get(index);
+ }
+
+ /**
+ * Returns the number of elements in this list. If this list contains more
+ * than <tt>Integer.MAX_VALUE</tt> elements, returns
+ * <tt>Integer.MAX_VALUE</tt>.
+ *
+ * @return the number of elements in this list
+ * @since 4.3
+ */
+ @Override
+ public int size() {
+ return this.all.size();
+ }
+
+ /**
+ * Replaces the URI at the specified position in this list with the
+ * specified element (must be a URI).
+ *
+ * @param index
+ * index of the element to replace
+ * @param element
+ * URI to be stored at the specified position
+ * @return the URI previously at the specified position
+ * @throws UnsupportedOperationException
+ * if the <tt>set</tt> operation is not supported by this list
+ * @throws ClassCastException
+ * if the element is not a {@link URI}
+ * @throws NullPointerException
+ * if the specified element is null and this list does not
+ * permit null elements
+ * @throws IndexOutOfBoundsException
+ * if the index is out of range (
+ * <tt>index &lt; 0 || index &gt;= size()</tt>)
+ * @since 4.3
+ */
+ @Override
+ public Object set(final int index, final Object element) {
+ final URI removed = this.all.set(index, (URI) element);
+ this.unique.remove(removed);
+ this.unique.add((URI) element);
+ if (this.all.size() != this.unique.size()) {
+ this.unique.addAll(this.all);
+ }
+ return removed;
+ }
+
+ /**
+ * Inserts the specified element at the specified position in this list
+ * (must be a URI). Shifts the URI currently at that position (if any) and
+ * any subsequent URIs to the right (adds one to their indices).
+ *
+ * @param index
+ * index at which the specified element is to be inserted
+ * @param element
+ * URI to be inserted
+ * @throws UnsupportedOperationException
+ * if the <tt>add</tt> operation is not supported by this list
+ * @throws ClassCastException
+ * if the element is not a {@link URI}
+ * @throws NullPointerException
+ * if the specified element is null and this list does not
+ * permit null elements
+ * @throws IndexOutOfBoundsException
+ * if the index is out of range (
+ * <tt>index &lt; 0 || index &gt; size()</tt>)
+ * @since 4.3
+ */
+ @Override
+ public void add(final int index, final Object element) {
+ this.all.add(index, (URI) element);
+ this.unique.add((URI) element);
+ }
+
+ /**
+ * Removes the URI at the specified position in this list. Shifts any
+ * subsequent URIs to the left (subtracts one from their indices). Returns
+ * the URI that was removed from the list.
+ *
+ * @param index
+ * the index of the URI to be removed
+ * @return the URI previously at the specified position
+ * @throws IndexOutOfBoundsException
+ * if the index is out of range (
+ * <tt>index &lt; 0 || index &gt;= size()</tt>)
+ * @since 4.3
+ */
+ @Override
+ public URI remove(final int index) {
+ final URI removed = this.all.remove(index);
+ this.unique.remove(removed);
+ if (this.all.size() != this.unique.size()) {
+ this.unique.addAll(this.all);
+ }
+ return removed;
+ }
+
+ /**
+ * Returns <tt>true</tt> if this collection contains the specified element.
+ * More formally, returns <tt>true</tt> if and only if this collection
+ * contains at least one element <tt>e</tt> such that
+ * <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>.
+ *
+ * @param o element whose presence in this collection is to be tested
+ * @return <tt>true</tt> if this collection contains the specified
+ * element
+ */
+ @Override
+ public boolean contains(final Object o) {
+ return this.unique.contains(o);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/RequestWrapper.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/RequestWrapper.java
new file mode 100644
index 0000000000..3577b61a6d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/RequestWrapper.java
@@ -0,0 +1,164 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.RequestLine;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.message.AbstractHttpMessage;
+import ch.boye.httpclientandroidlib.message.BasicRequestLine;
+import ch.boye.httpclientandroidlib.params.HttpProtocolParams;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * A wrapper class for {@link HttpRequest}s that can be used to change
+ * properties of the current request without modifying the original
+ * object.
+ * </p>
+ * This class is also capable of resetting the request headers to
+ * the state of the original request.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) do not use.
+ */
+@NotThreadSafe
+@Deprecated
+public class RequestWrapper extends AbstractHttpMessage implements HttpUriRequest {
+
+ private final HttpRequest original;
+
+ private URI uri;
+ private String method;
+ private ProtocolVersion version;
+ private int execCount;
+
+ public RequestWrapper(final HttpRequest request) throws ProtocolException {
+ super();
+ Args.notNull(request, "HTTP request");
+ this.original = request;
+ setParams(request.getParams());
+ setHeaders(request.getAllHeaders());
+ // Make a copy of the original URI
+ if (request instanceof HttpUriRequest) {
+ this.uri = ((HttpUriRequest) request).getURI();
+ this.method = ((HttpUriRequest) request).getMethod();
+ this.version = null;
+ } else {
+ final RequestLine requestLine = request.getRequestLine();
+ try {
+ this.uri = new URI(requestLine.getUri());
+ } catch (final URISyntaxException ex) {
+ throw new ProtocolException("Invalid request URI: "
+ + requestLine.getUri(), ex);
+ }
+ this.method = requestLine.getMethod();
+ this.version = request.getProtocolVersion();
+ }
+ this.execCount = 0;
+ }
+
+ public void resetHeaders() {
+ // Make a copy of original headers
+ this.headergroup.clear();
+ setHeaders(this.original.getAllHeaders());
+ }
+
+ public String getMethod() {
+ return this.method;
+ }
+
+ public void setMethod(final String method) {
+ Args.notNull(method, "Method name");
+ this.method = method;
+ }
+
+ public ProtocolVersion getProtocolVersion() {
+ if (this.version == null) {
+ this.version = HttpProtocolParams.getVersion(getParams());
+ }
+ return this.version;
+ }
+
+ public void setProtocolVersion(final ProtocolVersion version) {
+ this.version = version;
+ }
+
+
+ public URI getURI() {
+ return this.uri;
+ }
+
+ public void setURI(final URI uri) {
+ this.uri = uri;
+ }
+
+ public RequestLine getRequestLine() {
+ final String method = getMethod();
+ final ProtocolVersion ver = getProtocolVersion();
+ String uritext = null;
+ if (uri != null) {
+ uritext = uri.toASCIIString();
+ }
+ if (uritext == null || uritext.length() == 0) {
+ uritext = "/";
+ }
+ return new BasicRequestLine(method, uritext, ver);
+ }
+
+ public void abort() throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean isAborted() {
+ return false;
+ }
+
+ public HttpRequest getOriginal() {
+ return this.original;
+ }
+
+ public boolean isRepeatable() {
+ return true;
+ }
+
+ public int getExecCount() {
+ return this.execCount;
+ }
+
+ public void incrementExecCount() {
+ this.execCount++;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/RoutedRequest.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/RoutedRequest.java
new file mode 100644
index 0000000000..2e5ff76e92
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/RoutedRequest.java
@@ -0,0 +1,67 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+
+/**
+ * A request with the route along which it should be sent.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) do not use.
+ */
+@Deprecated
+@NotThreadSafe // RequestWrapper is @NotThreadSafe
+public class RoutedRequest {
+
+ protected final RequestWrapper request; // @NotThreadSafe
+ protected final HttpRoute route; // @Immutable
+
+ /**
+ * Creates a new routed request.
+ *
+ * @param req the request
+ * @param route the route
+ */
+ public RoutedRequest(final RequestWrapper req, final HttpRoute route) {
+ super();
+ this.request = req;
+ this.route = route;
+ }
+
+ public final RequestWrapper getRequest() {
+ return request;
+ }
+
+ public final HttpRoute getRoute() {
+ return route;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/StandardHttpRequestRetryHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/StandardHttpRequestRetryHandler.java
new file mode 100644
index 0000000000..ec0ce7e00f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/StandardHttpRequestRetryHandler.java
@@ -0,0 +1,80 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * {@link ch.boye.httpclientandroidlib.client.HttpRequestRetryHandler} which assumes
+ * that all requested HTTP methods which should be idempotent according
+ * to RFC-2616 are in fact idempotent and can be retried.
+ * <p/>
+ * According to RFC-2616 section 9.1.2 the idempotent HTTP methods are:
+ * GET, HEAD, PUT, DELETE, OPTIONS, and TRACE
+ *
+ * @since 4.2
+ */
+@Immutable
+public class StandardHttpRequestRetryHandler extends DefaultHttpRequestRetryHandler {
+
+ private final Map<String, Boolean> idempotentMethods;
+
+ /**
+ * Default constructor
+ */
+ public StandardHttpRequestRetryHandler(final int retryCount, final boolean requestSentRetryEnabled) {
+ super(retryCount, requestSentRetryEnabled);
+ this.idempotentMethods = new ConcurrentHashMap<String, Boolean>();
+ this.idempotentMethods.put("GET", Boolean.TRUE);
+ this.idempotentMethods.put("HEAD", Boolean.TRUE);
+ this.idempotentMethods.put("PUT", Boolean.TRUE);
+ this.idempotentMethods.put("DELETE", Boolean.TRUE);
+ this.idempotentMethods.put("OPTIONS", Boolean.TRUE);
+ this.idempotentMethods.put("TRACE", Boolean.TRUE);
+ }
+
+ /**
+ * Default constructor
+ */
+ public StandardHttpRequestRetryHandler() {
+ this(3, false);
+ }
+
+ @Override
+ protected boolean handleAsIdempotent(final HttpRequest request) {
+ final String method = request.getRequestLine().getMethod().toUpperCase(Locale.ENGLISH);
+ final Boolean b = this.idempotentMethods.get(method);
+ return b != null && b.booleanValue();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/SystemClock.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/SystemClock.java
new file mode 100644
index 0000000000..b037af8418
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/SystemClock.java
@@ -0,0 +1,40 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client;
+
+/**
+ * The actual system clock.
+ *
+ * @since 4.2
+ */
+class SystemClock implements Clock {
+
+ public long getCurrentTime() {
+ return System.currentTimeMillis();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/SystemDefaultCredentialsProvider.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/SystemDefaultCredentialsProvider.java
new file mode 100644
index 0000000000..1327b71dc9
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/SystemDefaultCredentialsProvider.java
@@ -0,0 +1,145 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.net.Authenticator;
+import java.net.PasswordAuthentication;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.auth.AuthScope;
+import ch.boye.httpclientandroidlib.auth.Credentials;
+import ch.boye.httpclientandroidlib.auth.NTCredentials;
+import ch.boye.httpclientandroidlib.auth.UsernamePasswordCredentials;
+import ch.boye.httpclientandroidlib.client.CredentialsProvider;
+import ch.boye.httpclientandroidlib.client.config.AuthSchemes;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Implementation of {@link CredentialsProvider} backed by standard
+ * JRE {@link Authenticator}.
+ *
+ * @since 4.3
+ */
+@ThreadSafe
+public class SystemDefaultCredentialsProvider implements CredentialsProvider {
+
+ private static final Map<String, String> SCHEME_MAP;
+
+ static {
+ SCHEME_MAP = new ConcurrentHashMap<String, String>();
+ SCHEME_MAP.put(AuthSchemes.BASIC.toUpperCase(Locale.ENGLISH), "Basic");
+ SCHEME_MAP.put(AuthSchemes.DIGEST.toUpperCase(Locale.ENGLISH), "Digest");
+ SCHEME_MAP.put(AuthSchemes.NTLM.toUpperCase(Locale.ENGLISH), "NTLM");
+ SCHEME_MAP.put(AuthSchemes.SPNEGO.toUpperCase(Locale.ENGLISH), "SPNEGO");
+ SCHEME_MAP.put(AuthSchemes.KERBEROS.toUpperCase(Locale.ENGLISH), "Kerberos");
+ }
+
+ private static String translateScheme(final String key) {
+ if (key == null) {
+ return null;
+ }
+ final String s = SCHEME_MAP.get(key);
+ return s != null ? s : key;
+ }
+
+ private final BasicCredentialsProvider internal;
+
+ /**
+ * Default constructor.
+ */
+ public SystemDefaultCredentialsProvider() {
+ super();
+ this.internal = new BasicCredentialsProvider();
+ }
+
+ public void setCredentials(final AuthScope authscope, final Credentials credentials) {
+ internal.setCredentials(authscope, credentials);
+ }
+
+ private static PasswordAuthentication getSystemCreds(
+ final AuthScope authscope,
+ final Authenticator.RequestorType requestorType) {
+ final String hostname = authscope.getHost();
+ final int port = authscope.getPort();
+ final String protocol = port == 443 ? "https" : "http";
+ return Authenticator.requestPasswordAuthentication(
+ hostname,
+ null,
+ port,
+ protocol,
+ null,
+ translateScheme(authscope.getScheme()),
+ null,
+ requestorType);
+ }
+
+ public Credentials getCredentials(final AuthScope authscope) {
+ Args.notNull(authscope, "Auth scope");
+ final Credentials localcreds = internal.getCredentials(authscope);
+ if (localcreds != null) {
+ return localcreds;
+ }
+ if (authscope.getHost() != null) {
+ PasswordAuthentication systemcreds = getSystemCreds(
+ authscope, Authenticator.RequestorType.SERVER);
+ if (systemcreds == null) {
+ systemcreds = getSystemCreds(
+ authscope, Authenticator.RequestorType.PROXY);
+ }
+ if (systemcreds != null) {
+ final String domain = System.getProperty("http.auth.ntlm.domain");
+ if (domain != null) {
+ return new NTCredentials(
+ systemcreds.getUserName(),
+ new String(systemcreds.getPassword()),
+ null, domain);
+ } else {
+ if (AuthSchemes.NTLM.equalsIgnoreCase(authscope.getScheme())) {
+ // Domian may be specified in a fully qualified user name
+ return new NTCredentials(
+ systemcreds.getUserName(),
+ new String(systemcreds.getPassword()),
+ null, null);
+ } else {
+ return new UsernamePasswordCredentials(
+ systemcreds.getUserName(),
+ new String(systemcreds.getPassword()));
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ public void clear() {
+ internal.clear();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/SystemDefaultHttpClient.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/SystemDefaultHttpClient.java
new file mode 100644
index 0000000000..0806537edc
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/SystemDefaultHttpClient.java
@@ -0,0 +1,149 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.net.ProxySelector;
+
+import ch.boye.httpclientandroidlib.ConnectionReuseStrategy;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionManager;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoutePlanner;
+import ch.boye.httpclientandroidlib.impl.DefaultConnectionReuseStrategy;
+import ch.boye.httpclientandroidlib.impl.NoConnectionReuseStrategy;
+import ch.boye.httpclientandroidlib.impl.conn.PoolingClientConnectionManager;
+import ch.boye.httpclientandroidlib.impl.conn.ProxySelectorRoutePlanner;
+import ch.boye.httpclientandroidlib.impl.conn.SchemeRegistryFactory;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+/**
+ * An extension of {@link DefaultHttpClient} pre-configured using system properties.
+ * <p>
+ * The following system properties are taken into account by this class:
+ * <ul>
+ * <li>ssl.TrustManagerFactory.algorithm</li>
+ * <li>javax.net.ssl.trustStoreType</li>
+ * <li>javax.net.ssl.trustStore</li>
+ * <li>javax.net.ssl.trustStoreProvider</li>
+ * <li>javax.net.ssl.trustStorePassword</li>
+ * <li>java.home</li>
+ * <li>ssl.KeyManagerFactory.algorithm</li>
+ * <li>javax.net.ssl.keyStoreType</li>
+ * <li>javax.net.ssl.keyStore</li>
+ * <li>javax.net.ssl.keyStoreProvider</li>
+ * <li>javax.net.ssl.keyStorePassword</li>
+ * <li>http.proxyHost</li>
+ * <li>http.proxyPort</li>
+ * <li>http.nonProxyHosts</li>
+ * <li>http.keepAlive</li>
+ * <li>http.maxConnections</li>
+ * </ul>
+ * <p>
+ * <p>
+ * The following parameters can be used to customize the behavior of this
+ * class:
+ * <ul>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#PROTOCOL_VERSION}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#STRICT_TRANSFER_ENCODING}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#HTTP_ELEMENT_CHARSET}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#USE_EXPECT_CONTINUE}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#WAIT_FOR_CONTINUE}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#USER_AGENT}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#TCP_NODELAY}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#SO_TIMEOUT}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#SO_LINGER}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#SO_REUSEADDR}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#SOCKET_BUFFER_SIZE}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#CONNECTION_TIMEOUT}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#MAX_LINE_LENGTH}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#MAX_HEADER_COUNT}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#STALE_CONNECTION_CHECK}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames#FORCED_ROUTE}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames#LOCAL_ADDRESS}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames#DEFAULT_PROXY}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.cookie.params.CookieSpecPNames#DATE_PATTERNS}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.cookie.params.CookieSpecPNames#SINGLE_COOKIE_HEADER}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.auth.params.AuthPNames#CREDENTIAL_CHARSET}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#COOKIE_POLICY}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#HANDLE_AUTHENTICATION}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#HANDLE_REDIRECTS}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#MAX_REDIRECTS}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#ALLOW_CIRCULAR_REDIRECTS}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#VIRTUAL_HOST}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#DEFAULT_HOST}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#DEFAULT_HEADERS}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.client.params.ClientPNames#CONN_MANAGER_TIMEOUT}</li>
+ * </ul>
+ * </p>
+ *
+ * @since 4.2
+ *
+ * @deprecated (4.3) use {@link HttpClientBuilder}
+ */
+@ThreadSafe
+@Deprecated
+public class SystemDefaultHttpClient extends DefaultHttpClient {
+
+ public SystemDefaultHttpClient(final HttpParams params) {
+ super(null, params);
+ }
+
+ public SystemDefaultHttpClient() {
+ super(null, null);
+ }
+
+ @Override
+ protected ClientConnectionManager createClientConnectionManager() {
+ final PoolingClientConnectionManager connmgr = new PoolingClientConnectionManager(
+ SchemeRegistryFactory.createSystemDefault());
+ String s = System.getProperty("http.keepAlive", "true");
+ if ("true".equalsIgnoreCase(s)) {
+ s = System.getProperty("http.maxConnections", "5");
+ final int max = Integer.parseInt(s);
+ connmgr.setDefaultMaxPerRoute(max);
+ connmgr.setMaxTotal(2 * max);
+ }
+ return connmgr;
+ }
+
+ @Override
+ protected HttpRoutePlanner createHttpRoutePlanner() {
+ return new ProxySelectorRoutePlanner(getConnectionManager().getSchemeRegistry(),
+ ProxySelector.getDefault());
+ }
+
+ @Override
+ protected ConnectionReuseStrategy createConnectionReuseStrategy() {
+ final String s = System.getProperty("http.keepAlive", "true");
+ if ("true".equalsIgnoreCase(s)) {
+ return new DefaultConnectionReuseStrategy();
+ } else {
+ return new NoConnectionReuseStrategy();
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/TargetAuthenticationStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/TargetAuthenticationStrategy.java
new file mode 100644
index 0000000000..486167dc13
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/TargetAuthenticationStrategy.java
@@ -0,0 +1,57 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import java.util.Collection;
+
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.auth.AUTH;
+import ch.boye.httpclientandroidlib.client.config.RequestConfig;
+
+/**
+ * Default {@link ch.boye.httpclientandroidlib.client.AuthenticationStrategy} implementation
+ * for proxy host authentication.
+ *
+ * @since 4.2
+ */
+@Immutable
+public class TargetAuthenticationStrategy extends AuthenticationStrategyImpl {
+
+ public static final TargetAuthenticationStrategy INSTANCE = new TargetAuthenticationStrategy();
+
+ public TargetAuthenticationStrategy() {
+ super(HttpStatus.SC_UNAUTHORIZED, AUTH.WWW_AUTH);
+ }
+
+ @Override
+ Collection<String> getPreferredAuthSchemes(final RequestConfig config) {
+ return config.getTargetPreferredAuthSchemes();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/TunnelRefusedException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/TunnelRefusedException.java
new file mode 100644
index 0000000000..97edac1f0e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/TunnelRefusedException.java
@@ -0,0 +1,58 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Signals that the tunnel request was rejected by the proxy host.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) reserved for internal use.
+ */
+@Deprecated
+@Immutable
+public class TunnelRefusedException extends HttpException {
+
+ private static final long serialVersionUID = -8646722842745617323L;
+
+ private final HttpResponse response;
+
+ public TunnelRefusedException(final String message, final HttpResponse response) {
+ super(message);
+ this.response = response;
+ }
+
+ public HttpResponse getResponse() {
+ return this.response;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/AsynchronousValidationRequest.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/AsynchronousValidationRequest.java
new file mode 100644
index 0000000000..60ad6ca1f5
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/AsynchronousValidationRequest.java
@@ -0,0 +1,178 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
+import ch.boye.httpclientandroidlib.client.methods.HttpExecutionAware;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestWrapper;
+import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
+import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+
+/**
+ * Class used to represent an asynchronous revalidation event, such as with
+ * "stale-while-revalidate"
+ */
+class AsynchronousValidationRequest implements Runnable {
+ private final AsynchronousValidator parent;
+ private final CachingExec cachingExec;
+ private final HttpRoute route;
+ private final HttpRequestWrapper request;
+ private final HttpClientContext context;
+ private final HttpExecutionAware execAware;
+ private final HttpCacheEntry cacheEntry;
+ private final String identifier;
+ private final int consecutiveFailedAttempts;
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ /**
+ * Used internally by {@link AsynchronousValidator} to schedule a
+ * revalidation.
+ * @param request
+ * @param context
+ * @param cacheEntry
+ * @param identifier
+ * @param consecutiveFailedAttempts
+ */
+ AsynchronousValidationRequest(
+ final AsynchronousValidator parent,
+ final CachingExec cachingExec,
+ final HttpRoute route,
+ final HttpRequestWrapper request,
+ final HttpClientContext context,
+ final HttpExecutionAware execAware,
+ final HttpCacheEntry cacheEntry,
+ final String identifier,
+ final int consecutiveFailedAttempts) {
+ this.parent = parent;
+ this.cachingExec = cachingExec;
+ this.route = route;
+ this.request = request;
+ this.context = context;
+ this.execAware = execAware;
+ this.cacheEntry = cacheEntry;
+ this.identifier = identifier;
+ this.consecutiveFailedAttempts = consecutiveFailedAttempts;
+ }
+
+ public void run() {
+ try {
+ if (revalidateCacheEntry()) {
+ parent.jobSuccessful(identifier);
+ } else {
+ parent.jobFailed(identifier);
+ }
+ } finally {
+ parent.markComplete(identifier);
+ }
+ }
+
+ /**
+ * Revalidate the cache entry and return if the operation was successful.
+ * Success means a connection to the server was established and replay did
+ * not indicate a server error.
+ * @return <code>true</code> if the cache entry was successfully validated;
+ * otherwise <code>false</code>
+ */
+ protected boolean revalidateCacheEntry() {
+ try {
+ final CloseableHttpResponse httpResponse = cachingExec.revalidateCacheEntry(route, request, context, execAware, cacheEntry);
+ try {
+ final int statusCode = httpResponse.getStatusLine().getStatusCode();
+ return isNotServerError(statusCode) && isNotStale(httpResponse);
+ } finally {
+ httpResponse.close();
+ }
+ } catch (final IOException ioe) {
+ log.debug("Asynchronous revalidation failed due to I/O error", ioe);
+ return false;
+ } catch (final HttpException pe) {
+ log.error("HTTP protocol exception during asynchronous revalidation", pe);
+ return false;
+ } catch (final RuntimeException re) {
+ log.error("RuntimeException thrown during asynchronous revalidation: " + re);
+ return false;
+ }
+ }
+
+ /**
+ * Return whether the status code indicates a server error or not.
+ * @param statusCode the status code to be checked
+ * @return if the status code indicates a server error or not
+ */
+ private boolean isNotServerError(final int statusCode) {
+ return statusCode < 500;
+ }
+
+ /**
+ * Try to detect if the returned response is generated from a stale cache entry.
+ * @param httpResponse the response to be checked
+ * @return whether the response is stale or not
+ */
+ private boolean isNotStale(final HttpResponse httpResponse) {
+ final Header[] warnings = httpResponse.getHeaders(HeaderConstants.WARNING);
+ if (warnings != null)
+ {
+ for (final Header warning : warnings)
+ {
+ /**
+ * warn-codes
+ * 110 = Response is stale
+ * 111 = Revalidation failed
+ */
+ final String warningValue = warning.getValue();
+ if (warningValue.startsWith("110") || warningValue.startsWith("111"))
+ {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ String getIdentifier() {
+ return identifier;
+ }
+
+ /**
+ * The number of consecutively failed revalidation attempts.
+ * @return the number of consecutively failed revalidation attempts.
+ */
+ public int getConsecutiveFailedAttempts() {
+ return consecutiveFailedAttempts;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/AsynchronousValidator.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/AsynchronousValidator.java
new file mode 100644
index 0000000000..05765a2363
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/AsynchronousValidator.java
@@ -0,0 +1,150 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.RejectedExecutionException;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
+import ch.boye.httpclientandroidlib.client.methods.HttpExecutionAware;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestWrapper;
+import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+
+/**
+ * Class used for asynchronous revalidations to be used when the "stale-
+ * while-revalidate" directive is present
+ */
+class AsynchronousValidator implements Closeable {
+ private final SchedulingStrategy schedulingStrategy;
+ private final Set<String> queued;
+ private final CacheKeyGenerator cacheKeyGenerator;
+ private final FailureCache failureCache;
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ /**
+ * Create AsynchronousValidator which will make revalidation requests
+ * using an {@link ImmediateSchedulingStrategy}. Its thread
+ * pool will be configured according to the given {@link CacheConfig}.
+ * @param config specifies thread pool settings. See
+ * {@link CacheConfig#getAsynchronousWorkersMax()},
+ * {@link CacheConfig#getAsynchronousWorkersCore()},
+ * {@link CacheConfig#getAsynchronousWorkerIdleLifetimeSecs()},
+ * and {@link CacheConfig#getRevalidationQueueSize()}.
+ */
+ public AsynchronousValidator(final CacheConfig config) {
+ this(new ImmediateSchedulingStrategy(config));
+ }
+
+ /**
+ * Create AsynchronousValidator which will make revalidation requests
+ * using the supplied {@link SchedulingStrategy}. Closing the validator
+ * will also close the given schedulingStrategy.
+ * @param schedulingStrategy used to maintain a pool of worker threads and
+ * schedules when requests are executed
+ */
+ AsynchronousValidator(final SchedulingStrategy schedulingStrategy) {
+ this.schedulingStrategy = schedulingStrategy;
+ this.queued = new HashSet<String>();
+ this.cacheKeyGenerator = new CacheKeyGenerator();
+ this.failureCache = new DefaultFailureCache();
+ }
+
+ public void close() throws IOException {
+ schedulingStrategy.close();
+ }
+
+ /**
+ * Schedules an asynchronous revalidation
+ */
+ public synchronized void revalidateCacheEntry(
+ final CachingExec cachingExec,
+ final HttpRoute route,
+ final HttpRequestWrapper request,
+ final HttpClientContext context,
+ final HttpExecutionAware execAware,
+ final HttpCacheEntry entry) {
+ // getVariantURI will fall back on getURI if no variants exist
+ final String uri = cacheKeyGenerator.getVariantURI(context.getTargetHost(), request, entry);
+
+ if (!queued.contains(uri)) {
+ final int consecutiveFailedAttempts = failureCache.getErrorCount(uri);
+ final AsynchronousValidationRequest revalidationRequest =
+ new AsynchronousValidationRequest(
+ this, cachingExec, route, request, context, execAware, entry, uri, consecutiveFailedAttempts);
+
+ try {
+ schedulingStrategy.schedule(revalidationRequest);
+ queued.add(uri);
+ } catch (final RejectedExecutionException ree) {
+ log.debug("Revalidation for [" + uri + "] not scheduled: " + ree);
+ }
+ }
+ }
+
+ /**
+ * Removes an identifier from the internal list of revalidation jobs in
+ * progress. This is meant to be called by
+ * {@link AsynchronousValidationRequest#run()} once the revalidation is
+ * complete, using the identifier passed in during constructions.
+ * @param identifier
+ */
+ synchronized void markComplete(final String identifier) {
+ queued.remove(identifier);
+ }
+
+ /**
+ * The revalidation job was successful thus the number of consecutive
+ * failed attempts will be reset to zero. Should be called by
+ * {@link AsynchronousValidationRequest#run()}.
+ * @param identifier the revalidation job's unique identifier
+ */
+ void jobSuccessful(final String identifier) {
+ failureCache.resetErrorCount(identifier);
+ }
+
+ /**
+ * The revalidation job did fail and thus the number of consecutive failed
+ * attempts will be increased. Should be called by
+ * {@link AsynchronousValidationRequest#run()}.
+ * @param identifier the revalidation job's unique identifier
+ */
+ void jobFailed(final String identifier) {
+ failureCache.increaseErrorCount(identifier);
+ }
+
+ Set<String> getScheduledIdentifiers() {
+ return Collections.unmodifiableSet(queued);
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/BasicHttpCache.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/BasicHttpCache.java
new file mode 100644
index 0000000000..4347e173d9
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/BasicHttpCache.java
@@ -0,0 +1,376 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheInvalidator;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheStorage;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheUpdateCallback;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheUpdateException;
+import ch.boye.httpclientandroidlib.client.cache.Resource;
+import ch.boye.httpclientandroidlib.client.cache.ResourceFactory;
+import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
+import ch.boye.httpclientandroidlib.entity.ByteArrayEntity;
+import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+
+class BasicHttpCache implements HttpCache {
+ private static final Set<String> safeRequestMethods = new HashSet<String>(
+ Arrays.asList(HeaderConstants.HEAD_METHOD,
+ HeaderConstants.GET_METHOD, HeaderConstants.OPTIONS_METHOD,
+ HeaderConstants.TRACE_METHOD));
+
+ private final CacheKeyGenerator uriExtractor;
+ private final ResourceFactory resourceFactory;
+ private final long maxObjectSizeBytes;
+ private final CacheEntryUpdater cacheEntryUpdater;
+ private final CachedHttpResponseGenerator responseGenerator;
+ private final HttpCacheInvalidator cacheInvalidator;
+ private final HttpCacheStorage storage;
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ public BasicHttpCache(
+ final ResourceFactory resourceFactory,
+ final HttpCacheStorage storage,
+ final CacheConfig config,
+ final CacheKeyGenerator uriExtractor,
+ final HttpCacheInvalidator cacheInvalidator) {
+ this.resourceFactory = resourceFactory;
+ this.uriExtractor = uriExtractor;
+ this.cacheEntryUpdater = new CacheEntryUpdater(resourceFactory);
+ this.maxObjectSizeBytes = config.getMaxObjectSize();
+ this.responseGenerator = new CachedHttpResponseGenerator();
+ this.storage = storage;
+ this.cacheInvalidator = cacheInvalidator;
+ }
+
+ public BasicHttpCache(
+ final ResourceFactory resourceFactory,
+ final HttpCacheStorage storage,
+ final CacheConfig config,
+ final CacheKeyGenerator uriExtractor) {
+ this( resourceFactory, storage, config, uriExtractor,
+ new CacheInvalidator(uriExtractor, storage));
+ }
+
+ public BasicHttpCache(
+ final ResourceFactory resourceFactory,
+ final HttpCacheStorage storage,
+ final CacheConfig config) {
+ this( resourceFactory, storage, config, new CacheKeyGenerator());
+ }
+
+ public BasicHttpCache(final CacheConfig config) {
+ this(new HeapResourceFactory(), new BasicHttpCacheStorage(config), config);
+ }
+
+ public BasicHttpCache() {
+ this(CacheConfig.DEFAULT);
+ }
+
+ public void flushCacheEntriesFor(final HttpHost host, final HttpRequest request)
+ throws IOException {
+ if (!safeRequestMethods.contains(request.getRequestLine().getMethod())) {
+ final String uri = uriExtractor.getURI(host, request);
+ storage.removeEntry(uri);
+ }
+ }
+
+ public void flushInvalidatedCacheEntriesFor(final HttpHost host, final HttpRequest request, final HttpResponse response) {
+ if (!safeRequestMethods.contains(request.getRequestLine().getMethod())) {
+ cacheInvalidator.flushInvalidatedCacheEntries(host, request, response);
+ }
+ }
+
+ void storeInCache(
+ final HttpHost target, final HttpRequest request, final HttpCacheEntry entry) throws IOException {
+ if (entry.hasVariants()) {
+ storeVariantEntry(target, request, entry);
+ } else {
+ storeNonVariantEntry(target, request, entry);
+ }
+ }
+
+ void storeNonVariantEntry(
+ final HttpHost target, final HttpRequest req, final HttpCacheEntry entry) throws IOException {
+ final String uri = uriExtractor.getURI(target, req);
+ storage.putEntry(uri, entry);
+ }
+
+ void storeVariantEntry(
+ final HttpHost target,
+ final HttpRequest req,
+ final HttpCacheEntry entry) throws IOException {
+ final String parentURI = uriExtractor.getURI(target, req);
+ final String variantURI = uriExtractor.getVariantURI(target, req, entry);
+ storage.putEntry(variantURI, entry);
+
+ final HttpCacheUpdateCallback callback = new HttpCacheUpdateCallback() {
+
+ public HttpCacheEntry update(final HttpCacheEntry existing) throws IOException {
+ return doGetUpdatedParentEntry(
+ req.getRequestLine().getUri(), existing, entry,
+ uriExtractor.getVariantKey(req, entry),
+ variantURI);
+ }
+
+ };
+
+ try {
+ storage.updateEntry(parentURI, callback);
+ } catch (final HttpCacheUpdateException e) {
+ log.warn("Could not update key [" + parentURI + "]", e);
+ }
+ }
+
+ public void reuseVariantEntryFor(final HttpHost target, final HttpRequest req,
+ final Variant variant) throws IOException {
+ final String parentCacheKey = uriExtractor.getURI(target, req);
+ final HttpCacheEntry entry = variant.getEntry();
+ final String variantKey = uriExtractor.getVariantKey(req, entry);
+ final String variantCacheKey = variant.getCacheKey();
+
+ final HttpCacheUpdateCallback callback = new HttpCacheUpdateCallback() {
+ public HttpCacheEntry update(final HttpCacheEntry existing)
+ throws IOException {
+ return doGetUpdatedParentEntry(req.getRequestLine().getUri(),
+ existing, entry, variantKey, variantCacheKey);
+ }
+ };
+
+ try {
+ storage.updateEntry(parentCacheKey, callback);
+ } catch (final HttpCacheUpdateException e) {
+ log.warn("Could not update key [" + parentCacheKey + "]", e);
+ }
+ }
+
+ boolean isIncompleteResponse(final HttpResponse resp, final Resource resource) {
+ final int status = resp.getStatusLine().getStatusCode();
+ if (status != HttpStatus.SC_OK
+ && status != HttpStatus.SC_PARTIAL_CONTENT) {
+ return false;
+ }
+ final Header hdr = resp.getFirstHeader(HTTP.CONTENT_LEN);
+ if (hdr == null) {
+ return false;
+ }
+ final int contentLength;
+ try {
+ contentLength = Integer.parseInt(hdr.getValue());
+ } catch (final NumberFormatException nfe) {
+ return false;
+ }
+ return (resource.length() < contentLength);
+ }
+
+ CloseableHttpResponse generateIncompleteResponseError(
+ final HttpResponse response, final Resource resource) {
+ final int contentLength = Integer.parseInt(response.getFirstHeader(HTTP.CONTENT_LEN).getValue());
+ final HttpResponse error =
+ new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_BAD_GATEWAY, "Bad Gateway");
+ error.setHeader("Content-Type","text/plain;charset=UTF-8");
+ final String msg = String.format("Received incomplete response " +
+ "with Content-Length %d but actual body length %d",
+ contentLength, resource.length());
+ final byte[] msgBytes = msg.getBytes();
+ error.setHeader("Content-Length", Integer.toString(msgBytes.length));
+ error.setEntity(new ByteArrayEntity(msgBytes));
+ return Proxies.enhanceResponse(error);
+ }
+
+ HttpCacheEntry doGetUpdatedParentEntry(
+ final String requestId,
+ final HttpCacheEntry existing,
+ final HttpCacheEntry entry,
+ final String variantKey,
+ final String variantCacheKey) throws IOException {
+ HttpCacheEntry src = existing;
+ if (src == null) {
+ src = entry;
+ }
+
+ Resource resource = null;
+ if (src.getResource() != null) {
+ resource = resourceFactory.copy(requestId, src.getResource());
+ }
+ final Map<String,String> variantMap = new HashMap<String,String>(src.getVariantMap());
+ variantMap.put(variantKey, variantCacheKey);
+ return new HttpCacheEntry(
+ src.getRequestDate(),
+ src.getResponseDate(),
+ src.getStatusLine(),
+ src.getAllHeaders(),
+ resource,
+ variantMap);
+ }
+
+ public HttpCacheEntry updateCacheEntry(final HttpHost target, final HttpRequest request,
+ final HttpCacheEntry stale, final HttpResponse originResponse,
+ final Date requestSent, final Date responseReceived) throws IOException {
+ final HttpCacheEntry updatedEntry = cacheEntryUpdater.updateCacheEntry(
+ request.getRequestLine().getUri(),
+ stale,
+ requestSent,
+ responseReceived,
+ originResponse);
+ storeInCache(target, request, updatedEntry);
+ return updatedEntry;
+ }
+
+ public HttpCacheEntry updateVariantCacheEntry(final HttpHost target, final HttpRequest request,
+ final HttpCacheEntry stale, final HttpResponse originResponse,
+ final Date requestSent, final Date responseReceived, final String cacheKey) throws IOException {
+ final HttpCacheEntry updatedEntry = cacheEntryUpdater.updateCacheEntry(
+ request.getRequestLine().getUri(),
+ stale,
+ requestSent,
+ responseReceived,
+ originResponse);
+ storage.putEntry(cacheKey, updatedEntry);
+ return updatedEntry;
+ }
+
+ public HttpResponse cacheAndReturnResponse(final HttpHost host, final HttpRequest request,
+ final HttpResponse originResponse, final Date requestSent, final Date responseReceived)
+ throws IOException {
+ return cacheAndReturnResponse(host, request,
+ Proxies.enhanceResponse(originResponse), requestSent,
+ responseReceived);
+ }
+
+ public CloseableHttpResponse cacheAndReturnResponse(
+ final HttpHost host,
+ final HttpRequest request,
+ final CloseableHttpResponse originResponse,
+ final Date requestSent,
+ final Date responseReceived) throws IOException {
+
+ boolean closeOriginResponse = true;
+ final SizeLimitedResponseReader responseReader = getResponseReader(request, originResponse);
+ try {
+ responseReader.readResponse();
+
+ if (responseReader.isLimitReached()) {
+ closeOriginResponse = false;
+ return responseReader.getReconstructedResponse();
+ }
+
+ final Resource resource = responseReader.getResource();
+ if (isIncompleteResponse(originResponse, resource)) {
+ return generateIncompleteResponseError(originResponse, resource);
+ }
+
+ final HttpCacheEntry entry = new HttpCacheEntry(
+ requestSent,
+ responseReceived,
+ originResponse.getStatusLine(),
+ originResponse.getAllHeaders(),
+ resource);
+ storeInCache(host, request, entry);
+ return responseGenerator.generateResponse(entry);
+ } finally {
+ if (closeOriginResponse) {
+ originResponse.close();
+ }
+ }
+ }
+
+ SizeLimitedResponseReader getResponseReader(final HttpRequest request,
+ final CloseableHttpResponse backEndResponse) {
+ return new SizeLimitedResponseReader(
+ resourceFactory, maxObjectSizeBytes, request, backEndResponse);
+ }
+
+ public HttpCacheEntry getCacheEntry(final HttpHost host, final HttpRequest request) throws IOException {
+ final HttpCacheEntry root = storage.getEntry(uriExtractor.getURI(host, request));
+ if (root == null) {
+ return null;
+ }
+ if (!root.hasVariants()) {
+ return root;
+ }
+ final String variantCacheKey = root.getVariantMap().get(uriExtractor.getVariantKey(request, root));
+ if (variantCacheKey == null) {
+ return null;
+ }
+ return storage.getEntry(variantCacheKey);
+ }
+
+ public void flushInvalidatedCacheEntriesFor(final HttpHost host,
+ final HttpRequest request) throws IOException {
+ cacheInvalidator.flushInvalidatedCacheEntries(host, request);
+ }
+
+ public Map<String, Variant> getVariantCacheEntriesWithEtags(final HttpHost host, final HttpRequest request)
+ throws IOException {
+ final Map<String,Variant> variants = new HashMap<String,Variant>();
+ final HttpCacheEntry root = storage.getEntry(uriExtractor.getURI(host, request));
+ if (root == null || !root.hasVariants()) {
+ return variants;
+ }
+ for(final Map.Entry<String, String> variant : root.getVariantMap().entrySet()) {
+ final String variantKey = variant.getKey();
+ final String variantCacheKey = variant.getValue();
+ addVariantWithEtag(variantKey, variantCacheKey, variants);
+ }
+ return variants;
+ }
+
+ private void addVariantWithEtag(final String variantKey,
+ final String variantCacheKey, final Map<String, Variant> variants)
+ throws IOException {
+ final HttpCacheEntry entry = storage.getEntry(variantCacheKey);
+ if (entry == null) {
+ return;
+ }
+ final Header etagHeader = entry.getFirstHeader(HeaderConstants.ETAG);
+ if (etagHeader == null) {
+ return;
+ }
+ variants.put(etagHeader.getValue(), new Variant(variantKey, variantCacheKey, entry));
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/BasicHttpCacheStorage.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/BasicHttpCacheStorage.java
new file mode 100644
index 0000000000..2257308587
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/BasicHttpCacheStorage.java
@@ -0,0 +1,96 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheStorage;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheUpdateCallback;
+
+/**
+ * Basic {@link HttpCacheStorage} implementation backed by an instance of
+ * {@link java.util.LinkedHashMap}. In other words, cache entries and
+ * the cached response bodies are held in-memory. This cache does NOT
+ * deallocate resources associated with the cache entries; it is intended
+ * for use with {@link HeapResource} and similar. This is the default cache
+ * storage backend used by {@link CachingHttpClients}.
+ *
+ * @since 4.1
+ */
+@ThreadSafe
+public class BasicHttpCacheStorage implements HttpCacheStorage {
+
+ private final CacheMap entries;
+
+ public BasicHttpCacheStorage(final CacheConfig config) {
+ super();
+ this.entries = new CacheMap(config.getMaxCacheEntries());
+ }
+
+ /**
+ * Places a HttpCacheEntry in the cache
+ *
+ * @param url
+ * Url to use as the cache key
+ * @param entry
+ * HttpCacheEntry to place in the cache
+ */
+ public synchronized void putEntry(final String url, final HttpCacheEntry entry) throws IOException {
+ entries.put(url, entry);
+ }
+
+ /**
+ * Gets an entry from the cache, if it exists
+ *
+ * @param url
+ * Url that is the cache key
+ * @return HttpCacheEntry if one exists, or null for cache miss
+ */
+ public synchronized HttpCacheEntry getEntry(final String url) throws IOException {
+ return entries.get(url);
+ }
+
+ /**
+ * Removes a HttpCacheEntry from the cache
+ *
+ * @param url
+ * Url that is the cache key
+ */
+ public synchronized void removeEntry(final String url) throws IOException {
+ entries.remove(url);
+ }
+
+ public synchronized void updateEntry(
+ final String url,
+ final HttpCacheUpdateCallback callback) throws IOException {
+ final HttpCacheEntry existingEntry = entries.get(url);
+ entries.put(url, callback.update(existingEntry));
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/BasicIdGenerator.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/BasicIdGenerator.java
new file mode 100644
index 0000000000..defdc39990
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/BasicIdGenerator.java
@@ -0,0 +1,86 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Formatter;
+import java.util.Locale;
+
+import ch.boye.httpclientandroidlib.annotation.GuardedBy;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+
+/**
+ * Should produce reasonably unique tokens.
+ */
+@ThreadSafe
+class BasicIdGenerator {
+
+ private final String hostname;
+ private final SecureRandom rnd;
+
+ @GuardedBy("this")
+ private long count;
+
+ public BasicIdGenerator() {
+ super();
+ String hostname;
+ try {
+ hostname = InetAddress.getLocalHost().getHostName();
+ } catch (final UnknownHostException ex) {
+ hostname = "localhost";
+ }
+ this.hostname = hostname;
+ try {
+ this.rnd = SecureRandom.getInstance("SHA1PRNG");
+ } catch (final NoSuchAlgorithmException ex) {
+ throw new Error(ex);
+ }
+ this.rnd.setSeed(System.currentTimeMillis());
+ }
+
+ public synchronized void generate(final StringBuilder buffer) {
+ this.count++;
+ final int rndnum = this.rnd.nextInt();
+ buffer.append(System.currentTimeMillis());
+ buffer.append('.');
+ final Formatter formatter = new Formatter(buffer, Locale.US);
+ formatter.format("%1$016x-%2$08x", this.count, rndnum);
+ formatter.close();
+ buffer.append('.');
+ buffer.append(this.hostname);
+ }
+
+ public String generate() {
+ final StringBuilder buffer = new StringBuilder();
+ generate(buffer);
+ return buffer.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheConfig.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheConfig.java
new file mode 100644
index 0000000000..964ea27193
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheConfig.java
@@ -0,0 +1,764 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * <p>Java Beans-style configuration for a {@link CachingHttpClient}. Any class
+ * in the caching module that has configuration options should take a
+ * {@link CacheConfig} argument in one of its constructors. A
+ * {@code CacheConfig} instance has sane and conservative defaults, so the
+ * easiest way to specify options is to get an instance and then set just
+ * the options you want to modify from their defaults.</p>
+ *
+ * <p><b>N.B.</b> This class is only for caching-specific configuration; to
+ * configure the behavior of the rest of the client, configure the
+ * {@link ch.boye.httpclientandroidlib.client.HttpClient} used as the &quot;backend&quot;
+ * for the {@code CachingHttpClient}.</p>
+ *
+ * <p>Cache configuration can be grouped into the following categories:</p>
+ *
+ * <p><b>Cache size.</b> If the backend storage supports these limits, you
+ * can specify the {@link CacheConfig#getMaxCacheEntries maximum number of
+ * cache entries} as well as the {@link CacheConfig#getMaxObjectSizeBytes
+ * maximum cacheable response body size}.</p>
+ *
+ * <p><b>Public/private caching.</b> By default, the caching module considers
+ * itself to be a shared (public) cache, and will not, for example, cache
+ * responses to requests with {@code Authorization} headers or responses
+ * marked with {@code Cache-Control: private}. If, however, the cache
+ * is only going to be used by one logical "user" (behaving similarly to a
+ * browser cache), then you will want to {@link
+ * CacheConfig#setSharedCache(boolean) turn off the shared cache setting}.</p>
+ *
+ * <p><b>303 caching</b>. RFC2616 explicitly disallows caching 303 responses;
+ * however, the HTTPbis working group says they can be cached
+ * if explicitly indicated in the response headers and permitted by the request method.
+ * (They also indicate that disallowing 303 caching is actually an unintended
+ * spec error in RFC2616).
+ * This behavior is off by default, to err on the side of a conservative
+ * adherence to the existing standard, but you may want to
+ * {@link Builder#setAllow303Caching(boolean) enable it}.
+ *
+ * <p><b>Weak ETags on PUT/DELETE If-Match requests</b>. RFC2616 explicitly
+ * prohibits the use of weak validators in non-GET requests, however, the
+ * HTTPbis working group says while the limitation for weak validators on ranged
+ * requests makes sense, weak ETag validation is useful on full non-GET
+ * requests; e.g., PUT with If-Match. This behavior is off by default, to err on
+ * the side of a conservative adherence to the existing standard, but you may
+ * want to {@link Builder#setWeakETagOnPutDeleteAllowed(boolean) enable it}.
+ *
+ * <p><b>Heuristic caching</b>. Per RFC2616, a cache may cache certain cache
+ * entries even if no explicit cache control headers are set by the origin.
+ * This behavior is off by default, but you may want to turn this on if you
+ * are working with an origin that doesn't set proper headers but where you
+ * still want to cache the responses. You will want to {@link
+ * CacheConfig#setHeuristicCachingEnabled(boolean) enable heuristic caching},
+ * then specify either a {@link CacheConfig#getHeuristicDefaultLifetime()
+ * default freshness lifetime} and/or a {@link
+ * CacheConfig#setHeuristicCoefficient(float) fraction of the time since
+ * the resource was last modified}. See Sections
+ * <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.2">
+ * 13.2.2</a> and <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.4">
+ * 13.2.4</a> of the HTTP/1.1 RFC for more details on heuristic caching.</p>
+ *
+ * <p><b>Background validation</b>. The cache module supports the
+ * {@code stale-while-revalidate} directive of
+ * <a href="http://tools.ietf.org/html/rfc5861">RFC5861</a>, which allows
+ * certain cache entry revalidations to happen in the background. You may
+ * want to tweak the settings for the {@link
+ * CacheConfig#getAsynchronousWorkersCore() minimum} and {@link
+ * CacheConfig#getAsynchronousWorkersMax() maximum} number of background
+ * worker threads, as well as the {@link
+ * CacheConfig#getAsynchronousWorkerIdleLifetimeSecs() maximum time they
+ * can be idle before being reclaimed}. You can also control the {@link
+ * CacheConfig#getRevalidationQueueSize() size of the queue} used for
+ * revalidations when there aren't enough workers to keep up with demand.</b>
+ */
+public class CacheConfig implements Cloneable {
+
+ /** Default setting for the maximum object size that will be
+ * cached, in bytes.
+ */
+ public final static int DEFAULT_MAX_OBJECT_SIZE_BYTES = 8192;
+
+ /** Default setting for the maximum number of cache entries
+ * that will be retained.
+ */
+ public final static int DEFAULT_MAX_CACHE_ENTRIES = 1000;
+
+ /** Default setting for the number of retries on a failed
+ * cache update
+ */
+ public final static int DEFAULT_MAX_UPDATE_RETRIES = 1;
+
+ /** Default setting for 303 caching
+ */
+ public final static boolean DEFAULT_303_CACHING_ENABLED = false;
+
+ /** Default setting to allow weak tags on PUT/DELETE methods
+ */
+ public final static boolean DEFAULT_WEAK_ETAG_ON_PUTDELETE_ALLOWED = false;
+
+ /** Default setting for heuristic caching
+ */
+ public final static boolean DEFAULT_HEURISTIC_CACHING_ENABLED = false;
+
+ /** Default coefficient used to heuristically determine freshness
+ * lifetime from the Last-Modified time of a cache entry.
+ */
+ public final static float DEFAULT_HEURISTIC_COEFFICIENT = 0.1f;
+
+ /** Default lifetime in seconds to be assumed when we cannot calculate
+ * freshness heuristically.
+ */
+ public final static long DEFAULT_HEURISTIC_LIFETIME = 0;
+
+ /** Default number of worker threads to allow for background revalidations
+ * resulting from the stale-while-revalidate directive.
+ */
+ public static final int DEFAULT_ASYNCHRONOUS_WORKERS_MAX = 1;
+
+ /** Default minimum number of worker threads to allow for background
+ * revalidations resulting from the stale-while-revalidate directive.
+ */
+ public static final int DEFAULT_ASYNCHRONOUS_WORKERS_CORE = 1;
+
+ /** Default maximum idle lifetime for a background revalidation thread
+ * before it gets reclaimed.
+ */
+ public static final int DEFAULT_ASYNCHRONOUS_WORKER_IDLE_LIFETIME_SECS = 60;
+
+ /** Default maximum queue length for background revalidation requests.
+ */
+ public static final int DEFAULT_REVALIDATION_QUEUE_SIZE = 100;
+
+ public static final CacheConfig DEFAULT = new Builder().build();
+
+ // TODO: make final
+ private long maxObjectSize;
+ private int maxCacheEntries;
+ private int maxUpdateRetries;
+ private boolean allow303Caching;
+ private boolean weakETagOnPutDeleteAllowed;
+ private boolean heuristicCachingEnabled;
+ private float heuristicCoefficient;
+ private long heuristicDefaultLifetime;
+ private boolean isSharedCache;
+ private int asynchronousWorkersMax;
+ private int asynchronousWorkersCore;
+ private int asynchronousWorkerIdleLifetimeSecs;
+ private int revalidationQueueSize;
+ private boolean neverCacheHTTP10ResponsesWithQuery;
+
+ /**
+ * @deprecated (4.3) use {@link Builder}.
+ */
+ @Deprecated
+ public CacheConfig() {
+ super();
+ this.maxObjectSize = DEFAULT_MAX_OBJECT_SIZE_BYTES;
+ this.maxCacheEntries = DEFAULT_MAX_CACHE_ENTRIES;
+ this.maxUpdateRetries = DEFAULT_MAX_UPDATE_RETRIES;
+ this.allow303Caching = DEFAULT_303_CACHING_ENABLED;
+ this.weakETagOnPutDeleteAllowed = DEFAULT_WEAK_ETAG_ON_PUTDELETE_ALLOWED;
+ this.heuristicCachingEnabled = DEFAULT_HEURISTIC_CACHING_ENABLED;
+ this.heuristicCoefficient = DEFAULT_HEURISTIC_COEFFICIENT;
+ this.heuristicDefaultLifetime = DEFAULT_HEURISTIC_LIFETIME;
+ this.isSharedCache = true;
+ this.asynchronousWorkersMax = DEFAULT_ASYNCHRONOUS_WORKERS_MAX;
+ this.asynchronousWorkersCore = DEFAULT_ASYNCHRONOUS_WORKERS_CORE;
+ this.asynchronousWorkerIdleLifetimeSecs = DEFAULT_ASYNCHRONOUS_WORKER_IDLE_LIFETIME_SECS;
+ this.revalidationQueueSize = DEFAULT_REVALIDATION_QUEUE_SIZE;
+ }
+
+ CacheConfig(
+ final long maxObjectSize,
+ final int maxCacheEntries,
+ final int maxUpdateRetries,
+ final boolean allow303Caching,
+ final boolean weakETagOnPutDeleteAllowed,
+ final boolean heuristicCachingEnabled,
+ final float heuristicCoefficient,
+ final long heuristicDefaultLifetime,
+ final boolean isSharedCache,
+ final int asynchronousWorkersMax,
+ final int asynchronousWorkersCore,
+ final int asynchronousWorkerIdleLifetimeSecs,
+ final int revalidationQueueSize,
+ final boolean neverCacheHTTP10ResponsesWithQuery) {
+ super();
+ this.maxObjectSize = maxObjectSize;
+ this.maxCacheEntries = maxCacheEntries;
+ this.maxUpdateRetries = maxUpdateRetries;
+ this.allow303Caching = allow303Caching;
+ this.weakETagOnPutDeleteAllowed = weakETagOnPutDeleteAllowed;
+ this.heuristicCachingEnabled = heuristicCachingEnabled;
+ this.heuristicCoefficient = heuristicCoefficient;
+ this.heuristicDefaultLifetime = heuristicDefaultLifetime;
+ this.isSharedCache = isSharedCache;
+ this.asynchronousWorkersMax = asynchronousWorkersMax;
+ this.asynchronousWorkersCore = asynchronousWorkersCore;
+ this.asynchronousWorkerIdleLifetimeSecs = asynchronousWorkerIdleLifetimeSecs;
+ this.revalidationQueueSize = revalidationQueueSize;
+ }
+
+ /**
+ * Returns the current maximum response body size that will be cached.
+ * @return size in bytes
+ *
+ * @deprecated (4.2) use {@link #getMaxObjectSize()}
+ */
+ @Deprecated
+ public int getMaxObjectSizeBytes() {
+ return maxObjectSize > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) maxObjectSize;
+ }
+
+ /**
+ * Specifies the maximum response body size that will be eligible for caching.
+ * @param maxObjectSizeBytes size in bytes
+ *
+ * @deprecated (4.2) use {@link Builder}.
+ */
+ @Deprecated
+ public void setMaxObjectSizeBytes(final int maxObjectSizeBytes) {
+ if (maxObjectSizeBytes > Integer.MAX_VALUE) {
+ this.maxObjectSize = Integer.MAX_VALUE;
+ } else {
+ this.maxObjectSize = maxObjectSizeBytes;
+ }
+ }
+
+ /**
+ * Returns the current maximum response body size that will be cached.
+ * @return size in bytes
+ *
+ * @since 4.2
+ */
+ public long getMaxObjectSize() {
+ return maxObjectSize;
+ }
+
+ /**
+ * Specifies the maximum response body size that will be eligible for caching.
+ * @param maxObjectSize size in bytes
+ *
+ * @since 4.2
+ *
+ * @deprecated (4.3) use {@link Builder}.
+ */
+ @Deprecated
+ public void setMaxObjectSize(final long maxObjectSize) {
+ this.maxObjectSize = maxObjectSize;
+ }
+
+ /**
+ * Returns whether the cache will never cache HTTP 1.0 responses with a query string or not.
+ * @return {@code true} to not cache query string responses, {@code false} to cache if explicit cache headers are
+ * found
+ */
+ public boolean isNeverCacheHTTP10ResponsesWithQuery() {
+ return neverCacheHTTP10ResponsesWithQuery;
+ }
+
+ /**
+ * Returns the maximum number of cache entries the cache will retain.
+ */
+ public int getMaxCacheEntries() {
+ return maxCacheEntries;
+ }
+
+ /**
+ * Sets the maximum number of cache entries the cache will retain.
+ *
+ * @deprecated (4.3) use {@link Builder}.
+ */
+ @Deprecated
+ public void setMaxCacheEntries(final int maxCacheEntries) {
+ this.maxCacheEntries = maxCacheEntries;
+ }
+
+ /**
+ * Returns the number of times to retry a cache update on failure
+ */
+ public int getMaxUpdateRetries(){
+ return maxUpdateRetries;
+ }
+
+ /**
+ * Sets the number of times to retry a cache update on failure
+ *
+ * @deprecated (4.3) use {@link Builder}.
+ */
+ @Deprecated
+ public void setMaxUpdateRetries(final int maxUpdateRetries){
+ this.maxUpdateRetries = maxUpdateRetries;
+ }
+
+ /**
+ * Returns whether 303 caching is enabled.
+ * @return {@code true} if it is enabled.
+ */
+ public boolean is303CachingEnabled() {
+ return allow303Caching;
+ }
+
+ /**
+ * Returns whether weak etags is allowed with PUT/DELETE methods.
+ * @return {@code true} if it is allowed.
+ */
+ public boolean isWeakETagOnPutDeleteAllowed() {
+ return weakETagOnPutDeleteAllowed;
+ }
+
+ /**
+ * Returns whether heuristic caching is enabled.
+ * @return {@code true} if it is enabled.
+ */
+ public boolean isHeuristicCachingEnabled() {
+ return heuristicCachingEnabled;
+ }
+
+ /**
+ * Enables or disables heuristic caching.
+ * @param heuristicCachingEnabled should be {@code true} to
+ * permit heuristic caching, {@code false} to disable it.
+ *
+ * @deprecated (4.3) use {@link Builder}.
+ */
+ @Deprecated
+ public void setHeuristicCachingEnabled(final boolean heuristicCachingEnabled) {
+ this.heuristicCachingEnabled = heuristicCachingEnabled;
+ }
+
+ /**
+ * Returns lifetime coefficient used in heuristic freshness caching.
+ */
+ public float getHeuristicCoefficient() {
+ return heuristicCoefficient;
+ }
+
+ /**
+ * Sets coefficient to be used in heuristic freshness caching. This is
+ * interpreted as the fraction of the time between the {@code Last-Modified}
+ * and {@code Date} headers of a cached response during which the cached
+ * response will be considered heuristically fresh.
+ * @param heuristicCoefficient should be between {@code 0.0} and
+ * {@code 1.0}.
+ *
+ * @deprecated (4.3) use {@link Builder}.
+ */
+ @Deprecated
+ public void setHeuristicCoefficient(final float heuristicCoefficient) {
+ this.heuristicCoefficient = heuristicCoefficient;
+ }
+
+ /**
+ * Get the default lifetime to be used if heuristic freshness calculation is
+ * not possible.
+ */
+ public long getHeuristicDefaultLifetime() {
+ return heuristicDefaultLifetime;
+ }
+
+ /**
+ * Sets default lifetime in seconds to be used if heuristic freshness
+ * calculation is not possible. Explicit cache control directives on
+ * either the request or origin response will override this, as will
+ * the heuristic {@code Last-Modified} freshness calculation if it is
+ * available.
+ * @param heuristicDefaultLifetimeSecs is the number of seconds to
+ * consider a cache-eligible response fresh in the absence of other
+ * information. Set this to {@code 0} to disable this style of
+ * heuristic caching.
+ *
+ * @deprecated (4.3) use {@link Builder}.
+ */
+ @Deprecated
+ public void setHeuristicDefaultLifetime(final long heuristicDefaultLifetimeSecs) {
+ this.heuristicDefaultLifetime = heuristicDefaultLifetimeSecs;
+ }
+
+ /**
+ * Returns whether the cache will behave as a shared cache or not.
+ * @return {@code true} for a shared cache, {@code false} for a non-
+ * shared (private) cache
+ */
+ public boolean isSharedCache() {
+ return isSharedCache;
+ }
+
+ /**
+ * Sets whether the cache should behave as a shared cache or not.
+ * @param isSharedCache true to behave as a shared cache, false to
+ * behave as a non-shared (private) cache. To have the cache
+ * behave like a browser cache, you want to set this to {@code false}.
+ *
+ * @deprecated (4.3) use {@link Builder}.
+ */
+ @Deprecated
+ public void setSharedCache(final boolean isSharedCache) {
+ this.isSharedCache = isSharedCache;
+ }
+
+ /**
+ * Returns the maximum number of threads to allow for background
+ * revalidations due to the {@code stale-while-revalidate} directive. A
+ * value of 0 means background revalidations are disabled.
+ */
+ public int getAsynchronousWorkersMax() {
+ return asynchronousWorkersMax;
+ }
+
+ /**
+ * Sets the maximum number of threads to allow for background
+ * revalidations due to the {@code stale-while-revalidate} directive.
+ * @param max number of threads; a value of 0 disables background
+ * revalidations.
+ *
+ * @deprecated (4.3) use {@link Builder}.
+ */
+ @Deprecated
+ public void setAsynchronousWorkersMax(final int max) {
+ this.asynchronousWorkersMax = max;
+ }
+
+ /**
+ * Returns the minimum number of threads to keep alive for background
+ * revalidations due to the {@code stale-while-revalidate} directive.
+ */
+ public int getAsynchronousWorkersCore() {
+ return asynchronousWorkersCore;
+ }
+
+ /**
+ * Sets the minimum number of threads to keep alive for background
+ * revalidations due to the {@code stale-while-revalidate} directive.
+ * @param min should be greater than zero and less than or equal
+ * to <code>getAsynchronousWorkersMax()</code>
+ *
+ * @deprecated (4.3) use {@link Builder}.
+ */
+ @Deprecated
+ public void setAsynchronousWorkersCore(final int min) {
+ this.asynchronousWorkersCore = min;
+ }
+
+ /**
+ * Returns the current maximum idle lifetime in seconds for a
+ * background revalidation worker thread. If a worker thread is idle
+ * for this long, and there are more than the core number of worker
+ * threads alive, the worker will be reclaimed.
+ */
+ public int getAsynchronousWorkerIdleLifetimeSecs() {
+ return asynchronousWorkerIdleLifetimeSecs;
+ }
+
+ /**
+ * Sets the current maximum idle lifetime in seconds for a
+ * background revalidation worker thread. If a worker thread is idle
+ * for this long, and there are more than the core number of worker
+ * threads alive, the worker will be reclaimed.
+ * @param secs idle lifetime in seconds
+ *
+ * @deprecated (4.3) use {@link Builder}.
+ */
+ @Deprecated
+ public void setAsynchronousWorkerIdleLifetimeSecs(final int secs) {
+ this.asynchronousWorkerIdleLifetimeSecs = secs;
+ }
+
+ /**
+ * Returns the current maximum queue size for background revalidations.
+ */
+ public int getRevalidationQueueSize() {
+ return revalidationQueueSize;
+ }
+
+ /**
+ * Sets the current maximum queue size for background revalidations.
+ *
+ * @deprecated (4.3) use {@link Builder}.
+ */
+ @Deprecated
+ public void setRevalidationQueueSize(final int size) {
+ this.revalidationQueueSize = size;
+ }
+
+ @Override
+ protected CacheConfig clone() throws CloneNotSupportedException {
+ return (CacheConfig) super.clone();
+ }
+
+ public static Builder custom() {
+ return new Builder();
+ }
+
+ public static Builder copy(final CacheConfig config) {
+ Args.notNull(config, "Cache config");
+ return new Builder()
+ .setMaxObjectSize(config.getMaxObjectSize())
+ .setMaxCacheEntries(config.getMaxCacheEntries())
+ .setMaxUpdateRetries(config.getMaxUpdateRetries())
+ .setHeuristicCachingEnabled(config.isHeuristicCachingEnabled())
+ .setHeuristicCoefficient(config.getHeuristicCoefficient())
+ .setHeuristicDefaultLifetime(config.getHeuristicDefaultLifetime())
+ .setSharedCache(config.isSharedCache())
+ .setAsynchronousWorkersMax(config.getAsynchronousWorkersMax())
+ .setAsynchronousWorkersCore(config.getAsynchronousWorkersCore())
+ .setAsynchronousWorkerIdleLifetimeSecs(config.getAsynchronousWorkerIdleLifetimeSecs())
+ .setRevalidationQueueSize(config.getRevalidationQueueSize())
+ .setNeverCacheHTTP10ResponsesWithQueryString(config.isNeverCacheHTTP10ResponsesWithQuery());
+ }
+
+
+ public static class Builder {
+
+ private long maxObjectSize;
+ private int maxCacheEntries;
+ private int maxUpdateRetries;
+ private boolean allow303Caching;
+ private boolean weakETagOnPutDeleteAllowed;
+ private boolean heuristicCachingEnabled;
+ private float heuristicCoefficient;
+ private long heuristicDefaultLifetime;
+ private boolean isSharedCache;
+ private int asynchronousWorkersMax;
+ private int asynchronousWorkersCore;
+ private int asynchronousWorkerIdleLifetimeSecs;
+ private int revalidationQueueSize;
+ private boolean neverCacheHTTP10ResponsesWithQuery;
+
+ Builder() {
+ this.maxObjectSize = DEFAULT_MAX_OBJECT_SIZE_BYTES;
+ this.maxCacheEntries = DEFAULT_MAX_CACHE_ENTRIES;
+ this.maxUpdateRetries = DEFAULT_MAX_UPDATE_RETRIES;
+ this.allow303Caching = DEFAULT_303_CACHING_ENABLED;
+ this.weakETagOnPutDeleteAllowed = DEFAULT_WEAK_ETAG_ON_PUTDELETE_ALLOWED;
+ this.heuristicCachingEnabled = false;
+ this.heuristicCoefficient = DEFAULT_HEURISTIC_COEFFICIENT;
+ this.heuristicDefaultLifetime = DEFAULT_HEURISTIC_LIFETIME;
+ this.isSharedCache = true;
+ this.asynchronousWorkersMax = DEFAULT_ASYNCHRONOUS_WORKERS_MAX;
+ this.asynchronousWorkersCore = DEFAULT_ASYNCHRONOUS_WORKERS_CORE;
+ this.asynchronousWorkerIdleLifetimeSecs = DEFAULT_ASYNCHRONOUS_WORKER_IDLE_LIFETIME_SECS;
+ this.revalidationQueueSize = DEFAULT_REVALIDATION_QUEUE_SIZE;
+ }
+
+ /**
+ * Specifies the maximum response body size that will be eligible for caching.
+ * @param maxObjectSize size in bytes
+ */
+ public Builder setMaxObjectSize(final long maxObjectSize) {
+ this.maxObjectSize = maxObjectSize;
+ return this;
+ }
+
+ /**
+ * Sets the maximum number of cache entries the cache will retain.
+ */
+ public Builder setMaxCacheEntries(final int maxCacheEntries) {
+ this.maxCacheEntries = maxCacheEntries;
+ return this;
+ }
+
+ /**
+ * Sets the number of times to retry a cache update on failure
+ */
+ public Builder setMaxUpdateRetries(final int maxUpdateRetries) {
+ this.maxUpdateRetries = maxUpdateRetries;
+ return this;
+ }
+
+ /**
+ * Enables or disables 303 caching.
+ * @param allow303Caching should be {@code true} to
+ * permit 303 caching, {@code false} to disable it.
+ */
+ public Builder setAllow303Caching(final boolean allow303Caching) {
+ this.allow303Caching = allow303Caching;
+ return this;
+ }
+
+ /**
+ * Allows or disallows weak etags to be used with PUT/DELETE If-Match requests.
+ * @param weakETagOnPutDeleteAllowed should be {@code true} to
+ * permit weak etags, {@code false} to reject them.
+ */
+ public Builder setWeakETagOnPutDeleteAllowed(final boolean weakETagOnPutDeleteAllowed) {
+ this.weakETagOnPutDeleteAllowed = weakETagOnPutDeleteAllowed;
+ return this;
+ }
+
+ /**
+ * Enables or disables heuristic caching.
+ * @param heuristicCachingEnabled should be {@code true} to
+ * permit heuristic caching, {@code false} to enable it.
+ */
+ public Builder setHeuristicCachingEnabled(final boolean heuristicCachingEnabled) {
+ this.heuristicCachingEnabled = heuristicCachingEnabled;
+ return this;
+ }
+
+ /**
+ * Sets coefficient to be used in heuristic freshness caching. This is
+ * interpreted as the fraction of the time between the {@code Last-Modified}
+ * and {@code Date} headers of a cached response during which the cached
+ * response will be considered heuristically fresh.
+ * @param heuristicCoefficient should be between {@code 0.0} and
+ * {@code 1.0}.
+ */
+ public Builder setHeuristicCoefficient(final float heuristicCoefficient) {
+ this.heuristicCoefficient = heuristicCoefficient;
+ return this;
+ }
+
+ /**
+ * Sets default lifetime in seconds to be used if heuristic freshness
+ * calculation is not possible. Explicit cache control directives on
+ * either the request or origin response will override this, as will
+ * the heuristic {@code Last-Modified} freshness calculation if it is
+ * available.
+ * @param heuristicDefaultLifetime is the number of seconds to
+ * consider a cache-eligible response fresh in the absence of other
+ * information. Set this to {@code 0} to disable this style of
+ * heuristic caching.
+ */
+ public Builder setHeuristicDefaultLifetime(final long heuristicDefaultLifetime) {
+ this.heuristicDefaultLifetime = heuristicDefaultLifetime;
+ return this;
+ }
+
+ /**
+ * Sets whether the cache should behave as a shared cache or not.
+ * @param isSharedCache true to behave as a shared cache, false to
+ * behave as a non-shared (private) cache. To have the cache
+ * behave like a browser cache, you want to set this to {@code false}.
+ */
+ public Builder setSharedCache(final boolean isSharedCache) {
+ this.isSharedCache = isSharedCache;
+ return this;
+ }
+
+ /**
+ * Sets the maximum number of threads to allow for background
+ * revalidations due to the {@code stale-while-revalidate} directive.
+ * @param asynchronousWorkersMax number of threads; a value of 0 disables background
+ * revalidations.
+ */
+ public Builder setAsynchronousWorkersMax(final int asynchronousWorkersMax) {
+ this.asynchronousWorkersMax = asynchronousWorkersMax;
+ return this;
+ }
+
+ /**
+ * Sets the minimum number of threads to keep alive for background
+ * revalidations due to the {@code stale-while-revalidate} directive.
+ * @param asynchronousWorkersCore should be greater than zero and less than or equal
+ * to <code>getAsynchronousWorkersMax()</code>
+ */
+ public Builder setAsynchronousWorkersCore(final int asynchronousWorkersCore) {
+ this.asynchronousWorkersCore = asynchronousWorkersCore;
+ return this;
+ }
+
+ /**
+ * Sets the current maximum idle lifetime in seconds for a
+ * background revalidation worker thread. If a worker thread is idle
+ * for this long, and there are more than the core number of worker
+ * threads alive, the worker will be reclaimed.
+ * @param asynchronousWorkerIdleLifetimeSecs idle lifetime in seconds
+ */
+ public Builder setAsynchronousWorkerIdleLifetimeSecs(final int asynchronousWorkerIdleLifetimeSecs) {
+ this.asynchronousWorkerIdleLifetimeSecs = asynchronousWorkerIdleLifetimeSecs;
+ return this;
+ }
+
+ /**
+ * Sets the current maximum queue size for background revalidations.
+ */
+ public Builder setRevalidationQueueSize(final int revalidationQueueSize) {
+ this.revalidationQueueSize = revalidationQueueSize;
+ return this;
+ }
+
+ /**
+ * Sets whether the cache should never cache HTTP 1.0 responses with a query string or not.
+ * @param neverCacheHTTP10ResponsesWithQuery true to never cache responses with a query
+ * string, false to cache if explicit cache headers are found. Set this to {@code true}
+ * to better emulate IE, which also never caches responses, regardless of what caching
+ * headers may be present.
+ */
+ public Builder setNeverCacheHTTP10ResponsesWithQueryString(
+ final boolean neverCacheHTTP10ResponsesWithQuery) {
+ this.neverCacheHTTP10ResponsesWithQuery = neverCacheHTTP10ResponsesWithQuery;
+ return this;
+ }
+
+ public CacheConfig build() {
+ return new CacheConfig(
+ maxObjectSize,
+ maxCacheEntries,
+ maxUpdateRetries,
+ allow303Caching,
+ weakETagOnPutDeleteAllowed,
+ heuristicCachingEnabled,
+ heuristicCoefficient,
+ heuristicDefaultLifetime,
+ isSharedCache,
+ asynchronousWorkersMax,
+ asynchronousWorkersCore,
+ asynchronousWorkerIdleLifetimeSecs,
+ revalidationQueueSize,
+ neverCacheHTTP10ResponsesWithQuery);
+ }
+
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("[maxObjectSize=").append(this.maxObjectSize)
+ .append(", maxCacheEntries=").append(this.maxCacheEntries)
+ .append(", maxUpdateRetries=").append(this.maxUpdateRetries)
+ .append(", 303CachingEnabled=").append(this.allow303Caching)
+ .append(", weakETagOnPutDeleteAllowed=").append(this.weakETagOnPutDeleteAllowed)
+ .append(", heuristicCachingEnabled=").append(this.heuristicCachingEnabled)
+ .append(", heuristicCoefficient=").append(this.heuristicCoefficient)
+ .append(", heuristicDefaultLifetime=").append(this.heuristicDefaultLifetime)
+ .append(", isSharedCache=").append(this.isSharedCache)
+ .append(", asynchronousWorkersMax=").append(this.asynchronousWorkersMax)
+ .append(", asynchronousWorkersCore=").append(this.asynchronousWorkersCore)
+ .append(", asynchronousWorkerIdleLifetimeSecs=").append(this.asynchronousWorkerIdleLifetimeSecs)
+ .append(", revalidationQueueSize=").append(this.revalidationQueueSize)
+ .append(", neverCacheHTTP10ResponsesWithQuery=").append(this.neverCacheHTTP10ResponsesWithQuery)
+ .append("]");
+ return builder.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheEntity.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheEntity.java
new file mode 100644
index 0000000000..1906166d16
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheEntity.java
@@ -0,0 +1,99 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Serializable;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.util.Args;
+
+@Immutable
+class CacheEntity implements HttpEntity, Serializable {
+
+ private static final long serialVersionUID = -3467082284120936233L;
+
+ private final HttpCacheEntry cacheEntry;
+
+ public CacheEntity(final HttpCacheEntry cacheEntry) {
+ super();
+ this.cacheEntry = cacheEntry;
+ }
+
+ public Header getContentType() {
+ return this.cacheEntry.getFirstHeader(HTTP.CONTENT_TYPE);
+ }
+
+ public Header getContentEncoding() {
+ return this.cacheEntry.getFirstHeader(HTTP.CONTENT_ENCODING);
+ }
+
+ public boolean isChunked() {
+ return false;
+ }
+
+ public boolean isRepeatable() {
+ return true;
+ }
+
+ public long getContentLength() {
+ return this.cacheEntry.getResource().length();
+ }
+
+ public InputStream getContent() throws IOException {
+ return this.cacheEntry.getResource().getInputStream();
+ }
+
+ public void writeTo(final OutputStream outstream) throws IOException {
+ Args.notNull(outstream, "Output stream");
+ final InputStream instream = this.cacheEntry.getResource().getInputStream();
+ try {
+ IOUtils.copy(instream, outstream);
+ } finally {
+ instream.close();
+ }
+ }
+
+ public boolean isStreaming() {
+ return false;
+ }
+
+ public void consumeContent() throws IOException {
+ }
+
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheEntryUpdater.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheEntryUpdater.java
new file mode 100644
index 0000000000..381a7508dc
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheEntryUpdater.java
@@ -0,0 +1,173 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.ListIterator;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
+import ch.boye.httpclientandroidlib.client.cache.Resource;
+import ch.boye.httpclientandroidlib.client.cache.ResourceFactory;
+import ch.boye.httpclientandroidlib.client.utils.DateUtils;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Update a {@link HttpCacheEntry} with new or updated information based on the latest
+ * 304 status response from the Server. Use the {@link HttpResponse} to perform
+ * the update.
+ *
+ * @since 4.1
+ */
+@Immutable
+class CacheEntryUpdater {
+
+ private final ResourceFactory resourceFactory;
+
+ CacheEntryUpdater() {
+ this(new HeapResourceFactory());
+ }
+
+ CacheEntryUpdater(final ResourceFactory resourceFactory) {
+ super();
+ this.resourceFactory = resourceFactory;
+ }
+
+ /**
+ * Update the entry with the new information from the response. Should only be used for
+ * 304 responses.
+ *
+ * @param requestId
+ * @param entry The cache Entry to be updated
+ * @param requestDate When the request was performed
+ * @param responseDate When the response was gotten
+ * @param response The HttpResponse from the backend server call
+ * @return HttpCacheEntry an updated version of the cache entry
+ * @throws java.io.IOException if something bad happens while trying to read the body from the original entry
+ */
+ public HttpCacheEntry updateCacheEntry(
+ final String requestId,
+ final HttpCacheEntry entry,
+ final Date requestDate,
+ final Date responseDate,
+ final HttpResponse response) throws IOException {
+ Args.check(response.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED,
+ "Response must have 304 status code");
+ final Header[] mergedHeaders = mergeHeaders(entry, response);
+ Resource resource = null;
+ if (entry.getResource() != null) {
+ resource = resourceFactory.copy(requestId, entry.getResource());
+ }
+ return new HttpCacheEntry(
+ requestDate,
+ responseDate,
+ entry.getStatusLine(),
+ mergedHeaders,
+ resource);
+ }
+
+ protected Header[] mergeHeaders(final HttpCacheEntry entry, final HttpResponse response) {
+
+ if (entryAndResponseHaveDateHeader(entry, response)
+ && entryDateHeaderNewerThenResponse(entry, response)) {
+ // Don't merge headers, keep the entry's headers as they are newer.
+ return entry.getAllHeaders();
+ }
+
+ final List<Header> cacheEntryHeaderList = new ArrayList<Header>(Arrays.asList(entry
+ .getAllHeaders()));
+ removeCacheHeadersThatMatchResponse(cacheEntryHeaderList, response);
+ removeCacheEntry1xxWarnings(cacheEntryHeaderList, entry);
+ cacheEntryHeaderList.addAll(Arrays.asList(response.getAllHeaders()));
+
+ return cacheEntryHeaderList.toArray(new Header[cacheEntryHeaderList.size()]);
+ }
+
+ private void removeCacheHeadersThatMatchResponse(final List<Header> cacheEntryHeaderList,
+ final HttpResponse response) {
+ for (final Header responseHeader : response.getAllHeaders()) {
+ final ListIterator<Header> cacheEntryHeaderListIter = cacheEntryHeaderList.listIterator();
+
+ while (cacheEntryHeaderListIter.hasNext()) {
+ final String cacheEntryHeaderName = cacheEntryHeaderListIter.next().getName();
+
+ if (cacheEntryHeaderName.equals(responseHeader.getName())) {
+ cacheEntryHeaderListIter.remove();
+ }
+ }
+ }
+ }
+
+ private void removeCacheEntry1xxWarnings(final List<Header> cacheEntryHeaderList, final HttpCacheEntry entry) {
+ final ListIterator<Header> cacheEntryHeaderListIter = cacheEntryHeaderList.listIterator();
+
+ while (cacheEntryHeaderListIter.hasNext()) {
+ final String cacheEntryHeaderName = cacheEntryHeaderListIter.next().getName();
+
+ if (HeaderConstants.WARNING.equals(cacheEntryHeaderName)) {
+ for (final Header cacheEntryWarning : entry.getHeaders(HeaderConstants.WARNING)) {
+ if (cacheEntryWarning.getValue().startsWith("1")) {
+ cacheEntryHeaderListIter.remove();
+ }
+ }
+ }
+ }
+ }
+
+ private boolean entryDateHeaderNewerThenResponse(final HttpCacheEntry entry, final HttpResponse response) {
+ final Date entryDate = DateUtils.parseDate(entry.getFirstHeader(HTTP.DATE_HEADER)
+ .getValue());
+ final Date responseDate = DateUtils.parseDate(response.getFirstHeader(HTTP.DATE_HEADER)
+ .getValue());
+ if (entryDate == null || responseDate == null) {
+ return false;
+ }
+ if (!entryDate.after(responseDate)) {
+ return false;
+ }
+ return true;
+ }
+
+ private boolean entryAndResponseHaveDateHeader(final HttpCacheEntry entry, final HttpResponse response) {
+ if (entry.getFirstHeader(HTTP.DATE_HEADER) != null
+ && response.getFirstHeader(HTTP.DATE_HEADER) != null) {
+ return true;
+ }
+
+ return false;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheInvalidator.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheInvalidator.java
new file mode 100644
index 0000000000..19e62869e2
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheInvalidator.java
@@ -0,0 +1,288 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Date;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheInvalidator;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheStorage;
+import ch.boye.httpclientandroidlib.client.utils.DateUtils;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+
+/**
+ * Given a particular HttpRequest, flush any cache entries that this request
+ * would invalidate.
+ *
+ * @since 4.1
+ */
+@Immutable
+class CacheInvalidator implements HttpCacheInvalidator {
+
+ private final HttpCacheStorage storage;
+ private final CacheKeyGenerator cacheKeyGenerator;
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ /**
+ * Create a new {@link CacheInvalidator} for a given {@link HttpCache} and
+ * {@link CacheKeyGenerator}.
+ *
+ * @param uriExtractor Provides identifiers for the keys to store cache entries
+ * @param storage the cache to store items away in
+ */
+ public CacheInvalidator(
+ final CacheKeyGenerator uriExtractor,
+ final HttpCacheStorage storage) {
+ this.cacheKeyGenerator = uriExtractor;
+ this.storage = storage;
+ }
+
+ /**
+ * Remove cache entries from the cache that are no longer fresh or
+ * have been invalidated in some way.
+ *
+ * @param host The backend host we are talking to
+ * @param req The HttpRequest to that host
+ */
+ public void flushInvalidatedCacheEntries(final HttpHost host, final HttpRequest req) {
+ if (requestShouldNotBeCached(req)) {
+ log.debug("Request should not be cached");
+
+ final String theUri = cacheKeyGenerator.getURI(host, req);
+
+ final HttpCacheEntry parent = getEntry(theUri);
+
+ log.debug("parent entry: " + parent);
+
+ if (parent != null) {
+ for (final String variantURI : parent.getVariantMap().values()) {
+ flushEntry(variantURI);
+ }
+ flushEntry(theUri);
+ }
+ final URL reqURL = getAbsoluteURL(theUri);
+ if (reqURL == null) {
+ log.error("Couldn't transform request into valid URL");
+ return;
+ }
+ final Header clHdr = req.getFirstHeader("Content-Location");
+ if (clHdr != null) {
+ final String contentLocation = clHdr.getValue();
+ if (!flushAbsoluteUriFromSameHost(reqURL, contentLocation)) {
+ flushRelativeUriFromSameHost(reqURL, contentLocation);
+ }
+ }
+ final Header lHdr = req.getFirstHeader("Location");
+ if (lHdr != null) {
+ flushAbsoluteUriFromSameHost(reqURL, lHdr.getValue());
+ }
+ }
+ }
+
+ private void flushEntry(final String uri) {
+ try {
+ storage.removeEntry(uri);
+ } catch (final IOException ioe) {
+ log.warn("unable to flush cache entry", ioe);
+ }
+ }
+
+ private HttpCacheEntry getEntry(final String theUri) {
+ try {
+ return storage.getEntry(theUri);
+ } catch (final IOException ioe) {
+ log.warn("could not retrieve entry from storage", ioe);
+ }
+ return null;
+ }
+
+ protected void flushUriIfSameHost(final URL requestURL, final URL targetURL) {
+ final URL canonicalTarget = getAbsoluteURL(cacheKeyGenerator.canonicalizeUri(targetURL.toString()));
+ if (canonicalTarget == null) {
+ return;
+ }
+ if (canonicalTarget.getAuthority().equalsIgnoreCase(requestURL.getAuthority())) {
+ flushEntry(canonicalTarget.toString());
+ }
+ }
+
+ protected void flushRelativeUriFromSameHost(final URL reqURL, final String relUri) {
+ final URL relURL = getRelativeURL(reqURL, relUri);
+ if (relURL == null) {
+ return;
+ }
+ flushUriIfSameHost(reqURL, relURL);
+ }
+
+
+ protected boolean flushAbsoluteUriFromSameHost(final URL reqURL, final String uri) {
+ final URL absURL = getAbsoluteURL(uri);
+ if (absURL == null) {
+ return false;
+ }
+ flushUriIfSameHost(reqURL,absURL);
+ return true;
+ }
+
+ private URL getAbsoluteURL(final String uri) {
+ URL absURL = null;
+ try {
+ absURL = new URL(uri);
+ } catch (final MalformedURLException mue) {
+ // nop
+ }
+ return absURL;
+ }
+
+ private URL getRelativeURL(final URL reqURL, final String relUri) {
+ URL relURL = null;
+ try {
+ relURL = new URL(reqURL,relUri);
+ } catch (final MalformedURLException e) {
+ // nop
+ }
+ return relURL;
+ }
+
+ protected boolean requestShouldNotBeCached(final HttpRequest req) {
+ final String method = req.getRequestLine().getMethod();
+ return notGetOrHeadRequest(method);
+ }
+
+ private boolean notGetOrHeadRequest(final String method) {
+ return !(HeaderConstants.GET_METHOD.equals(method) || HeaderConstants.HEAD_METHOD
+ .equals(method));
+ }
+
+ /** Flushes entries that were invalidated by the given response
+ * received for the given host/request pair.
+ */
+ public void flushInvalidatedCacheEntries(final HttpHost host,
+ final HttpRequest request, final HttpResponse response) {
+ final int status = response.getStatusLine().getStatusCode();
+ if (status < 200 || status > 299) {
+ return;
+ }
+ final URL reqURL = getAbsoluteURL(cacheKeyGenerator.getURI(host, request));
+ if (reqURL == null) {
+ return;
+ }
+ final URL contentLocation = getContentLocationURL(reqURL, response);
+ if (contentLocation != null) {
+ flushLocationCacheEntry(reqURL, response, contentLocation);
+ }
+ final URL location = getLocationURL(reqURL, response);
+ if (location != null) {
+ flushLocationCacheEntry(reqURL, response, location);
+ }
+ }
+
+ private void flushLocationCacheEntry(final URL reqURL,
+ final HttpResponse response, final URL location) {
+ final String cacheKey = cacheKeyGenerator.canonicalizeUri(location.toString());
+ final HttpCacheEntry entry = getEntry(cacheKey);
+ if (entry == null) {
+ return;
+ }
+
+ // do not invalidate if response is strictly older than entry
+ // or if the etags match
+
+ if (responseDateOlderThanEntryDate(response, entry)) {
+ return;
+ }
+ if (!responseAndEntryEtagsDiffer(response, entry)) {
+ return;
+ }
+
+ flushUriIfSameHost(reqURL, location);
+ }
+
+ private URL getContentLocationURL(final URL reqURL, final HttpResponse response) {
+ final Header clHeader = response.getFirstHeader("Content-Location");
+ if (clHeader == null) {
+ return null;
+ }
+ final String contentLocation = clHeader.getValue();
+ final URL canonURL = getAbsoluteURL(contentLocation);
+ if (canonURL != null) {
+ return canonURL;
+ }
+ return getRelativeURL(reqURL, contentLocation);
+ }
+
+ private URL getLocationURL(final URL reqURL, final HttpResponse response) {
+ final Header clHeader = response.getFirstHeader("Location");
+ if (clHeader == null) {
+ return null;
+ }
+ final String location = clHeader.getValue();
+ final URL canonURL = getAbsoluteURL(location);
+ if (canonURL != null) {
+ return canonURL;
+ }
+ return getRelativeURL(reqURL, location);
+ }
+
+ private boolean responseAndEntryEtagsDiffer(final HttpResponse response,
+ final HttpCacheEntry entry) {
+ final Header entryEtag = entry.getFirstHeader(HeaderConstants.ETAG);
+ final Header responseEtag = response.getFirstHeader(HeaderConstants.ETAG);
+ if (entryEtag == null || responseEtag == null) {
+ return false;
+ }
+ return (!entryEtag.getValue().equals(responseEtag.getValue()));
+ }
+
+ private boolean responseDateOlderThanEntryDate(final HttpResponse response,
+ final HttpCacheEntry entry) {
+ final Header entryDateHeader = entry.getFirstHeader(HTTP.DATE_HEADER);
+ final Header responseDateHeader = response.getFirstHeader(HTTP.DATE_HEADER);
+ if (entryDateHeader == null || responseDateHeader == null) {
+ /* be conservative; should probably flush */
+ return false;
+ }
+ final Date entryDate = DateUtils.parseDate(entryDateHeader.getValue());
+ final Date responseDate = DateUtils.parseDate(responseDateHeader.getValue());
+ if (entryDate == null || responseDate == null) {
+ return false;
+ }
+ return responseDate.before(entryDate);
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheKeyGenerator.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheKeyGenerator.java
new file mode 100644
index 0000000000..b0628041f1
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheKeyGenerator.java
@@ -0,0 +1,178 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.Consts;
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
+import ch.boye.httpclientandroidlib.client.utils.URIUtils;
+
+/**
+ * @since 4.1
+ */
+@Immutable
+class CacheKeyGenerator {
+
+ private static final URI BASE_URI = URI.create("http://example.com/");
+
+ /**
+ * For a given {@link HttpHost} and {@link HttpRequest} get a URI from the
+ * pair that I can use as an identifier KEY into my HttpCache
+ *
+ * @param host The host for this request
+ * @param req the {@link HttpRequest}
+ * @return String the extracted URI
+ */
+ public String getURI(final HttpHost host, final HttpRequest req) {
+ if (isRelativeRequest(req)) {
+ return canonicalizeUri(String.format("%s%s", host.toString(), req.getRequestLine().getUri()));
+ }
+ return canonicalizeUri(req.getRequestLine().getUri());
+ }
+
+ public String canonicalizeUri(final String uri) {
+ try {
+ final URI normalized = URIUtils.resolve(BASE_URI, uri);
+ final URL u = new URL(normalized.toASCIIString());
+ final String protocol = u.getProtocol();
+ final String hostname = u.getHost();
+ final int port = canonicalizePort(u.getPort(), protocol);
+ final String path = u.getPath();
+ final String query = u.getQuery();
+ final String file = (query != null) ? (path + "?" + query) : path;
+ final URL out = new URL(protocol, hostname, port, file);
+ return out.toString();
+ } catch (final IllegalArgumentException e) {
+ return uri;
+ } catch (final MalformedURLException e) {
+ return uri;
+ }
+ }
+
+ private int canonicalizePort(final int port, final String protocol) {
+ if (port == -1 && "http".equalsIgnoreCase(protocol)) {
+ return 80;
+ } else if (port == -1 && "https".equalsIgnoreCase(protocol)) {
+ return 443;
+ }
+ return port;
+ }
+
+ private boolean isRelativeRequest(final HttpRequest req) {
+ final String requestUri = req.getRequestLine().getUri();
+ return ("*".equals(requestUri) || requestUri.startsWith("/"));
+ }
+
+ protected String getFullHeaderValue(final Header[] headers) {
+ if (headers == null) {
+ return "";
+ }
+
+ final StringBuilder buf = new StringBuilder("");
+ boolean first = true;
+ for (final Header hdr : headers) {
+ if (!first) {
+ buf.append(", ");
+ }
+ buf.append(hdr.getValue().trim());
+ first = false;
+
+ }
+ return buf.toString();
+ }
+
+ /**
+ * For a given {@link HttpHost} and {@link HttpRequest} if the request has a
+ * VARY header - I need to get an additional URI from the pair of host and
+ * request so that I can also store the variant into my HttpCache.
+ *
+ * @param host The host for this request
+ * @param req the {@link HttpRequest}
+ * @param entry the parent entry used to track the variants
+ * @return String the extracted variant URI
+ */
+ public String getVariantURI(final HttpHost host, final HttpRequest req, final HttpCacheEntry entry) {
+ if (!entry.hasVariants()) {
+ return getURI(host, req);
+ }
+ return getVariantKey(req, entry) + getURI(host, req);
+ }
+
+ /**
+ * Compute a "variant key" from the headers of a given request that are
+ * covered by the Vary header of a given cache entry. Any request whose
+ * varying headers match those of this request should have the same
+ * variant key.
+ * @param req originating request
+ * @param entry cache entry in question that has variants
+ * @return a <code>String</code> variant key
+ */
+ public String getVariantKey(final HttpRequest req, final HttpCacheEntry entry) {
+ final List<String> variantHeaderNames = new ArrayList<String>();
+ for (final Header varyHdr : entry.getHeaders(HeaderConstants.VARY)) {
+ for (final HeaderElement elt : varyHdr.getElements()) {
+ variantHeaderNames.add(elt.getName());
+ }
+ }
+ Collections.sort(variantHeaderNames);
+
+ StringBuilder buf;
+ try {
+ buf = new StringBuilder("{");
+ boolean first = true;
+ for (final String headerName : variantHeaderNames) {
+ if (!first) {
+ buf.append("&");
+ }
+ buf.append(URLEncoder.encode(headerName, Consts.UTF_8.name()));
+ buf.append("=");
+ buf.append(URLEncoder.encode(getFullHeaderValue(req.getHeaders(headerName)),
+ Consts.UTF_8.name()));
+ first = false;
+ }
+ buf.append("}");
+ } catch (final UnsupportedEncodingException uee) {
+ throw new RuntimeException("couldn't encode to UTF-8", uee);
+ }
+ return buf.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheMap.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheMap.java
new file mode 100644
index 0000000000..0ed80920cb
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheMap.java
@@ -0,0 +1,50 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
+
+final class CacheMap extends LinkedHashMap<String, HttpCacheEntry> {
+
+ private static final long serialVersionUID = -7750025207539768511L;
+
+ private final int maxEntries;
+
+ CacheMap(final int maxEntries) {
+ super(20, 0.75f, true);
+ this.maxEntries = maxEntries;
+ }
+
+ @Override
+ protected boolean removeEldestEntry(final Map.Entry<String, HttpCacheEntry> eldest) {
+ return size() > this.maxEntries;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheValidityPolicy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheValidityPolicy.java
new file mode 100644
index 0000000000..333dfbbfbb
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheValidityPolicy.java
@@ -0,0 +1,320 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.util.Date;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
+import ch.boye.httpclientandroidlib.client.utils.DateUtils;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+
+/**
+ * @since 4.1
+ */
+@Immutable
+class CacheValidityPolicy {
+
+ public static final long MAX_AGE = 2147483648L;
+
+ CacheValidityPolicy() {
+ super();
+ }
+
+ public long getCurrentAgeSecs(final HttpCacheEntry entry, final Date now) {
+ return getCorrectedInitialAgeSecs(entry) + getResidentTimeSecs(entry, now);
+ }
+
+ public long getFreshnessLifetimeSecs(final HttpCacheEntry entry) {
+ final long maxage = getMaxAge(entry);
+ if (maxage > -1) {
+ return maxage;
+ }
+
+ final Date dateValue = entry.getDate();
+ if (dateValue == null) {
+ return 0L;
+ }
+
+ final Date expiry = getExpirationDate(entry);
+ if (expiry == null) {
+ return 0;
+ }
+ final long diff = expiry.getTime() - dateValue.getTime();
+ return (diff / 1000);
+ }
+
+ public boolean isResponseFresh(final HttpCacheEntry entry, final Date now) {
+ return (getCurrentAgeSecs(entry, now) < getFreshnessLifetimeSecs(entry));
+ }
+
+ /**
+ * Decides if this response is fresh enough based Last-Modified and Date, if available.
+ * This entry is meant to be used when isResponseFresh returns false. The algorithm is as follows:
+ *
+ * if last-modified and date are defined, freshness lifetime is coefficient*(date-lastModified),
+ * else freshness lifetime is defaultLifetime
+ *
+ * @param entry the cache entry
+ * @param now what time is it currently (When is right NOW)
+ * @param coefficient Part of the heuristic for cache entry freshness
+ * @param defaultLifetime How long can I assume a cache entry is default TTL
+ * @return {@code true} if the response is fresh
+ */
+ public boolean isResponseHeuristicallyFresh(final HttpCacheEntry entry,
+ final Date now, final float coefficient, final long defaultLifetime) {
+ return (getCurrentAgeSecs(entry, now) < getHeuristicFreshnessLifetimeSecs(entry, coefficient, defaultLifetime));
+ }
+
+ public long getHeuristicFreshnessLifetimeSecs(final HttpCacheEntry entry,
+ final float coefficient, final long defaultLifetime) {
+ final Date dateValue = entry.getDate();
+ final Date lastModifiedValue = getLastModifiedValue(entry);
+
+ if (dateValue != null && lastModifiedValue != null) {
+ final long diff = dateValue.getTime() - lastModifiedValue.getTime();
+ if (diff < 0) {
+ return 0;
+ }
+ return (long)(coefficient * (diff / 1000));
+ }
+
+ return defaultLifetime;
+ }
+
+ public boolean isRevalidatable(final HttpCacheEntry entry) {
+ return entry.getFirstHeader(HeaderConstants.ETAG) != null
+ || entry.getFirstHeader(HeaderConstants.LAST_MODIFIED) != null;
+ }
+
+ public boolean mustRevalidate(final HttpCacheEntry entry) {
+ return hasCacheControlDirective(entry, HeaderConstants.CACHE_CONTROL_MUST_REVALIDATE);
+ }
+
+ public boolean proxyRevalidate(final HttpCacheEntry entry) {
+ return hasCacheControlDirective(entry, HeaderConstants.CACHE_CONTROL_PROXY_REVALIDATE);
+ }
+
+ public boolean mayReturnStaleWhileRevalidating(final HttpCacheEntry entry, final Date now) {
+ for (final Header h : entry.getHeaders(HeaderConstants.CACHE_CONTROL)) {
+ for(final HeaderElement elt : h.getElements()) {
+ if (HeaderConstants.STALE_WHILE_REVALIDATE.equalsIgnoreCase(elt.getName())) {
+ try {
+ final int allowedStalenessLifetime = Integer.parseInt(elt.getValue());
+ if (getStalenessSecs(entry, now) <= allowedStalenessLifetime) {
+ return true;
+ }
+ } catch (final NumberFormatException nfe) {
+ // skip malformed directive
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public boolean mayReturnStaleIfError(final HttpRequest request,
+ final HttpCacheEntry entry, final Date now) {
+ final long stalenessSecs = getStalenessSecs(entry, now);
+ return mayReturnStaleIfError(request.getHeaders(HeaderConstants.CACHE_CONTROL),
+ stalenessSecs)
+ || mayReturnStaleIfError(entry.getHeaders(HeaderConstants.CACHE_CONTROL),
+ stalenessSecs);
+ }
+
+ private boolean mayReturnStaleIfError(final Header[] headers, final long stalenessSecs) {
+ boolean result = false;
+ for(final Header h : headers) {
+ for(final HeaderElement elt : h.getElements()) {
+ if (HeaderConstants.STALE_IF_ERROR.equals(elt.getName())) {
+ try {
+ final int staleIfErrorSecs = Integer.parseInt(elt.getValue());
+ if (stalenessSecs <= staleIfErrorSecs) {
+ result = true;
+ break;
+ }
+ } catch (final NumberFormatException nfe) {
+ // skip malformed directive
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * @deprecated (4.3) use {@link HttpCacheEntry#getDate()}.
+ * @param entry
+ * @return the Date of the entry
+ */
+ @Deprecated
+ protected Date getDateValue(final HttpCacheEntry entry) {
+ return entry.getDate();
+ }
+
+ protected Date getLastModifiedValue(final HttpCacheEntry entry) {
+ final Header dateHdr = entry.getFirstHeader(HeaderConstants.LAST_MODIFIED);
+ if (dateHdr == null) {
+ return null;
+ }
+ return DateUtils.parseDate(dateHdr.getValue());
+ }
+
+ protected long getContentLengthValue(final HttpCacheEntry entry) {
+ final Header cl = entry.getFirstHeader(HTTP.CONTENT_LEN);
+ if (cl == null) {
+ return -1;
+ }
+
+ try {
+ return Long.parseLong(cl.getValue());
+ } catch (final NumberFormatException ex) {
+ return -1;
+ }
+ }
+
+ protected boolean hasContentLengthHeader(final HttpCacheEntry entry) {
+ return null != entry.getFirstHeader(HTTP.CONTENT_LEN);
+ }
+
+ /**
+ * This matters for deciding whether the cache entry is valid to serve as a
+ * response. If these values do not match, we might have a partial response
+ *
+ * @param entry The cache entry we are currently working with
+ * @return boolean indicating whether actual length matches Content-Length
+ */
+ protected boolean contentLengthHeaderMatchesActualLength(final HttpCacheEntry entry) {
+ return !hasContentLengthHeader(entry) || getContentLengthValue(entry) == entry.getResource().length();
+ }
+
+ protected long getApparentAgeSecs(final HttpCacheEntry entry) {
+ final Date dateValue = entry.getDate();
+ if (dateValue == null) {
+ return MAX_AGE;
+ }
+ final long diff = entry.getResponseDate().getTime() - dateValue.getTime();
+ if (diff < 0L) {
+ return 0;
+ }
+ return (diff / 1000);
+ }
+
+ protected long getAgeValue(final HttpCacheEntry entry) {
+ long ageValue = 0;
+ for (final Header hdr : entry.getHeaders(HeaderConstants.AGE)) {
+ long hdrAge;
+ try {
+ hdrAge = Long.parseLong(hdr.getValue());
+ if (hdrAge < 0) {
+ hdrAge = MAX_AGE;
+ }
+ } catch (final NumberFormatException nfe) {
+ hdrAge = MAX_AGE;
+ }
+ ageValue = (hdrAge > ageValue) ? hdrAge : ageValue;
+ }
+ return ageValue;
+ }
+
+ protected long getCorrectedReceivedAgeSecs(final HttpCacheEntry entry) {
+ final long apparentAge = getApparentAgeSecs(entry);
+ final long ageValue = getAgeValue(entry);
+ return (apparentAge > ageValue) ? apparentAge : ageValue;
+ }
+
+ protected long getResponseDelaySecs(final HttpCacheEntry entry) {
+ final long diff = entry.getResponseDate().getTime() - entry.getRequestDate().getTime();
+ return (diff / 1000L);
+ }
+
+ protected long getCorrectedInitialAgeSecs(final HttpCacheEntry entry) {
+ return getCorrectedReceivedAgeSecs(entry) + getResponseDelaySecs(entry);
+ }
+
+ protected long getResidentTimeSecs(final HttpCacheEntry entry, final Date now) {
+ final long diff = now.getTime() - entry.getResponseDate().getTime();
+ return (diff / 1000L);
+ }
+
+ protected long getMaxAge(final HttpCacheEntry entry) {
+ long maxage = -1;
+ for (final Header hdr : entry.getHeaders(HeaderConstants.CACHE_CONTROL)) {
+ for (final HeaderElement elt : hdr.getElements()) {
+ if (HeaderConstants.CACHE_CONTROL_MAX_AGE.equals(elt.getName())
+ || "s-maxage".equals(elt.getName())) {
+ try {
+ final long currMaxAge = Long.parseLong(elt.getValue());
+ if (maxage == -1 || currMaxAge < maxage) {
+ maxage = currMaxAge;
+ }
+ } catch (final NumberFormatException nfe) {
+ // be conservative if can't parse
+ maxage = 0;
+ }
+ }
+ }
+ }
+ return maxage;
+ }
+
+ protected Date getExpirationDate(final HttpCacheEntry entry) {
+ final Header expiresHeader = entry.getFirstHeader(HeaderConstants.EXPIRES);
+ if (expiresHeader == null) {
+ return null;
+ }
+ return DateUtils.parseDate(expiresHeader.getValue());
+ }
+
+ public boolean hasCacheControlDirective(final HttpCacheEntry entry,
+ final String directive) {
+ for (final Header h : entry.getHeaders(HeaderConstants.CACHE_CONTROL)) {
+ for(final HeaderElement elt : h.getElements()) {
+ if (directive.equalsIgnoreCase(elt.getName())) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public long getStalenessSecs(final HttpCacheEntry entry, final Date now) {
+ final long age = getCurrentAgeSecs(entry, now);
+ final long freshness = getFreshnessLifetimeSecs(entry);
+ if (age <= freshness) {
+ return 0L;
+ }
+ return (age - freshness);
+ }
+
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheableRequestPolicy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheableRequestPolicy.java
new file mode 100644
index 0000000000..500da01dcb
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CacheableRequestPolicy.java
@@ -0,0 +1,96 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
+
+/**
+ * Determines if an HttpRequest is allowed to be served from the cache.
+ *
+ * @since 4.1
+ */
+@Immutable
+class CacheableRequestPolicy {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ /**
+ * Determines if an HttpRequest can be served from the cache.
+ *
+ * @param request
+ * an HttpRequest
+ * @return boolean Is it possible to serve this request from cache
+ */
+ public boolean isServableFromCache(final HttpRequest request) {
+ final String method = request.getRequestLine().getMethod();
+
+ final ProtocolVersion pv = request.getRequestLine().getProtocolVersion();
+ if (HttpVersion.HTTP_1_1.compareToVersion(pv) != 0) {
+ log.trace("non-HTTP/1.1 request was not serveable from cache");
+ return false;
+ }
+
+ if (!method.equals(HeaderConstants.GET_METHOD)) {
+ log.trace("non-GET request was not serveable from cache");
+ return false;
+ }
+
+ if (request.getHeaders(HeaderConstants.PRAGMA).length > 0) {
+ log.trace("request with Pragma header was not serveable from cache");
+ return false;
+ }
+
+ final Header[] cacheControlHeaders = request.getHeaders(HeaderConstants.CACHE_CONTROL);
+ for (final Header cacheControl : cacheControlHeaders) {
+ for (final HeaderElement cacheControlElement : cacheControl.getElements()) {
+ if (HeaderConstants.CACHE_CONTROL_NO_STORE.equalsIgnoreCase(cacheControlElement
+ .getName())) {
+ log.trace("Request with no-store was not serveable from cache");
+ return false;
+ }
+
+ if (HeaderConstants.CACHE_CONTROL_NO_CACHE.equalsIgnoreCase(cacheControlElement
+ .getName())) {
+ log.trace("Request with no-cache was not serveable from cache");
+ return false;
+ }
+ }
+ }
+
+ log.trace("Request was serveable from cache");
+ return true;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CachedHttpResponseGenerator.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CachedHttpResponseGenerator.java
new file mode 100644
index 0000000000..f42529e57c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CachedHttpResponseGenerator.java
@@ -0,0 +1,166 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.util.Date;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
+import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
+import ch.boye.httpclientandroidlib.client.utils.DateUtils;
+import ch.boye.httpclientandroidlib.message.BasicHeader;
+import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+
+/**
+ * Rebuilds an {@link HttpResponse} from a {@link net.sf.ehcache.CacheEntry}
+ *
+ * @since 4.1
+ */
+@Immutable
+class CachedHttpResponseGenerator {
+
+ private final CacheValidityPolicy validityStrategy;
+
+ CachedHttpResponseGenerator(final CacheValidityPolicy validityStrategy) {
+ super();
+ this.validityStrategy = validityStrategy;
+ }
+
+ CachedHttpResponseGenerator() {
+ this(new CacheValidityPolicy());
+ }
+
+ /**
+ * If I was able to use a {@link CacheEntity} to response to the {@link ch.boye.httpclientandroidlib.HttpRequest} then
+ * generate an {@link HttpResponse} based on the cache entry.
+ * @param entry
+ * {@link CacheEntity} to transform into an {@link HttpResponse}
+ * @return {@link HttpResponse} that was constructed
+ */
+ CloseableHttpResponse generateResponse(final HttpCacheEntry entry) {
+
+ final Date now = new Date();
+ final HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, entry
+ .getStatusCode(), entry.getReasonPhrase());
+
+ response.setHeaders(entry.getAllHeaders());
+
+ if (entry.getResource() != null) {
+ final HttpEntity entity = new CacheEntity(entry);
+ addMissingContentLengthHeader(response, entity);
+ response.setEntity(entity);
+ }
+
+ final long age = this.validityStrategy.getCurrentAgeSecs(entry, now);
+ if (age > 0) {
+ if (age >= Integer.MAX_VALUE) {
+ response.setHeader(HeaderConstants.AGE, "2147483648");
+ } else {
+ response.setHeader(HeaderConstants.AGE, "" + ((int) age));
+ }
+ }
+
+ return Proxies.enhanceResponse(response);
+ }
+
+ /**
+ * Generate a 304 - Not Modified response from a {@link CacheEntity}. This should be
+ * used to respond to conditional requests, when the entry exists or has been re-validated.
+ */
+ CloseableHttpResponse generateNotModifiedResponse(final HttpCacheEntry entry) {
+
+ final HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1,
+ HttpStatus.SC_NOT_MODIFIED, "Not Modified");
+
+ // The response MUST include the following headers
+ // (http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)
+
+ // - Date, unless its omission is required by section 14.8.1
+ Header dateHeader = entry.getFirstHeader(HTTP.DATE_HEADER);
+ if (dateHeader == null) {
+ dateHeader = new BasicHeader(HTTP.DATE_HEADER, DateUtils.formatDate(new Date()));
+ }
+ response.addHeader(dateHeader);
+
+ // - ETag and/or Content-Location, if the header would have been sent
+ // in a 200 response to the same request
+ final Header etagHeader = entry.getFirstHeader(HeaderConstants.ETAG);
+ if (etagHeader != null) {
+ response.addHeader(etagHeader);
+ }
+
+ final Header contentLocationHeader = entry.getFirstHeader("Content-Location");
+ if (contentLocationHeader != null) {
+ response.addHeader(contentLocationHeader);
+ }
+
+ // - Expires, Cache-Control, and/or Vary, if the field-value might
+ // differ from that sent in any previous response for the same
+ // variant
+ final Header expiresHeader = entry.getFirstHeader(HeaderConstants.EXPIRES);
+ if (expiresHeader != null) {
+ response.addHeader(expiresHeader);
+ }
+
+ final Header cacheControlHeader = entry.getFirstHeader(HeaderConstants.CACHE_CONTROL);
+ if (cacheControlHeader != null) {
+ response.addHeader(cacheControlHeader);
+ }
+
+ final Header varyHeader = entry.getFirstHeader(HeaderConstants.VARY);
+ if (varyHeader != null) {
+ response.addHeader(varyHeader);
+ }
+
+ return Proxies.enhanceResponse(response);
+ }
+
+ private void addMissingContentLengthHeader(final HttpResponse response, final HttpEntity entity) {
+ if (transferEncodingIsPresent(response)) {
+ return;
+ }
+
+ Header contentLength = response.getFirstHeader(HTTP.CONTENT_LEN);
+ if (contentLength == null) {
+ contentLength = new BasicHeader(HTTP.CONTENT_LEN, Long.toString(entity
+ .getContentLength()));
+ response.setHeader(contentLength);
+ }
+ }
+
+ private boolean transferEncodingIsPresent(final HttpResponse response) {
+ final Header hdr = response.getFirstHeader(HTTP.TRANSFER_ENCODING);
+ return hdr != null;
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CachedResponseSuitabilityChecker.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CachedResponseSuitabilityChecker.java
new file mode 100644
index 0000000000..a289729d32
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CachedResponseSuitabilityChecker.java
@@ -0,0 +1,346 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.util.Date;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
+import ch.boye.httpclientandroidlib.client.utils.DateUtils;
+
+/**
+ * Determines whether a given {@link HttpCacheEntry} is suitable to be
+ * used as a response for a given {@link HttpRequest}.
+ *
+ * @since 4.1
+ */
+@Immutable
+class CachedResponseSuitabilityChecker {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ private final boolean sharedCache;
+ private final boolean useHeuristicCaching;
+ private final float heuristicCoefficient;
+ private final long heuristicDefaultLifetime;
+ private final CacheValidityPolicy validityStrategy;
+
+ CachedResponseSuitabilityChecker(final CacheValidityPolicy validityStrategy,
+ final CacheConfig config) {
+ super();
+ this.validityStrategy = validityStrategy;
+ this.sharedCache = config.isSharedCache();
+ this.useHeuristicCaching = config.isHeuristicCachingEnabled();
+ this.heuristicCoefficient = config.getHeuristicCoefficient();
+ this.heuristicDefaultLifetime = config.getHeuristicDefaultLifetime();
+ }
+
+ CachedResponseSuitabilityChecker(final CacheConfig config) {
+ this(new CacheValidityPolicy(), config);
+ }
+
+ private boolean isFreshEnough(final HttpCacheEntry entry, final HttpRequest request, final Date now) {
+ if (validityStrategy.isResponseFresh(entry, now)) {
+ return true;
+ }
+ if (useHeuristicCaching &&
+ validityStrategy.isResponseHeuristicallyFresh(entry, now, heuristicCoefficient, heuristicDefaultLifetime)) {
+ return true;
+ }
+ if (originInsistsOnFreshness(entry)) {
+ return false;
+ }
+ final long maxstale = getMaxStale(request);
+ if (maxstale == -1) {
+ return false;
+ }
+ return (maxstale > validityStrategy.getStalenessSecs(entry, now));
+ }
+
+ private boolean originInsistsOnFreshness(final HttpCacheEntry entry) {
+ if (validityStrategy.mustRevalidate(entry)) {
+ return true;
+ }
+ if (!sharedCache) {
+ return false;
+ }
+ return validityStrategy.proxyRevalidate(entry) ||
+ validityStrategy.hasCacheControlDirective(entry, "s-maxage");
+ }
+
+ private long getMaxStale(final HttpRequest request) {
+ long maxstale = -1;
+ for(final Header h : request.getHeaders(HeaderConstants.CACHE_CONTROL)) {
+ for(final HeaderElement elt : h.getElements()) {
+ if (HeaderConstants.CACHE_CONTROL_MAX_STALE.equals(elt.getName())) {
+ if ((elt.getValue() == null || "".equals(elt.getValue().trim()))
+ && maxstale == -1) {
+ maxstale = Long.MAX_VALUE;
+ } else {
+ try {
+ long val = Long.parseLong(elt.getValue());
+ if (val < 0) {
+ val = 0;
+ }
+ if (maxstale == -1 || val < maxstale) {
+ maxstale = val;
+ }
+ } catch (final NumberFormatException nfe) {
+ // err on the side of preserving semantic transparency
+ maxstale = 0;
+ }
+ }
+ }
+ }
+ }
+ return maxstale;
+ }
+
+ /**
+ * Determine if I can utilize a {@link HttpCacheEntry} to respond to the given
+ * {@link HttpRequest}
+ *
+ * @param host
+ * {@link HttpHost}
+ * @param request
+ * {@link HttpRequest}
+ * @param entry
+ * {@link HttpCacheEntry}
+ * @param now
+ * Right now in time
+ * @return boolean yes/no answer
+ */
+ public boolean canCachedResponseBeUsed(final HttpHost host, final HttpRequest request, final HttpCacheEntry entry, final Date now) {
+
+ if (!isFreshEnough(entry, request, now)) {
+ log.trace("Cache entry was not fresh enough");
+ return false;
+ }
+
+ if (!validityStrategy.contentLengthHeaderMatchesActualLength(entry)) {
+ log.debug("Cache entry Content-Length and header information do not match");
+ return false;
+ }
+
+ if (hasUnsupportedConditionalHeaders(request)) {
+ log.debug("Request contained conditional headers we don't handle");
+ return false;
+ }
+
+ if (!isConditional(request) && entry.getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
+ return false;
+ }
+
+ if (isConditional(request) && !allConditionalsMatch(request, entry, now)) {
+ return false;
+ }
+
+ for (final Header ccHdr : request.getHeaders(HeaderConstants.CACHE_CONTROL)) {
+ for (final HeaderElement elt : ccHdr.getElements()) {
+ if (HeaderConstants.CACHE_CONTROL_NO_CACHE.equals(elt.getName())) {
+ log.trace("Response contained NO CACHE directive, cache was not suitable");
+ return false;
+ }
+
+ if (HeaderConstants.CACHE_CONTROL_NO_STORE.equals(elt.getName())) {
+ log.trace("Response contained NO STORE directive, cache was not suitable");
+ return false;
+ }
+
+ if (HeaderConstants.CACHE_CONTROL_MAX_AGE.equals(elt.getName())) {
+ try {
+ final int maxage = Integer.parseInt(elt.getValue());
+ if (validityStrategy.getCurrentAgeSecs(entry, now) > maxage) {
+ log.trace("Response from cache was NOT suitable due to max age");
+ return false;
+ }
+ } catch (final NumberFormatException ex) {
+ // err conservatively
+ log.debug("Response from cache was malformed" + ex.getMessage());
+ return false;
+ }
+ }
+
+ if (HeaderConstants.CACHE_CONTROL_MAX_STALE.equals(elt.getName())) {
+ try {
+ final int maxstale = Integer.parseInt(elt.getValue());
+ if (validityStrategy.getFreshnessLifetimeSecs(entry) > maxstale) {
+ log.trace("Response from cache was not suitable due to Max stale freshness");
+ return false;
+ }
+ } catch (final NumberFormatException ex) {
+ // err conservatively
+ log.debug("Response from cache was malformed: " + ex.getMessage());
+ return false;
+ }
+ }
+
+ if (HeaderConstants.CACHE_CONTROL_MIN_FRESH.equals(elt.getName())) {
+ try {
+ final long minfresh = Long.parseLong(elt.getValue());
+ if (minfresh < 0L) {
+ return false;
+ }
+ final long age = validityStrategy.getCurrentAgeSecs(entry, now);
+ final long freshness = validityStrategy.getFreshnessLifetimeSecs(entry);
+ if (freshness - age < minfresh) {
+ log.trace("Response from cache was not suitable due to min fresh " +
+ "freshness requirement");
+ return false;
+ }
+ } catch (final NumberFormatException ex) {
+ // err conservatively
+ log.debug("Response from cache was malformed: " + ex.getMessage());
+ return false;
+ }
+ }
+ }
+ }
+
+ log.trace("Response from cache was suitable");
+ return true;
+ }
+
+ /**
+ * Is this request the type of conditional request we support?
+ * @param request The current httpRequest being made
+ * @return {@code true} if the request is supported
+ */
+ public boolean isConditional(final HttpRequest request) {
+ return hasSupportedEtagValidator(request) || hasSupportedLastModifiedValidator(request);
+ }
+
+ /**
+ * Check that conditionals that are part of this request match
+ * @param request The current httpRequest being made
+ * @param entry the cache entry
+ * @param now right NOW in time
+ * @return {@code true} if the request matches all conditionals
+ */
+ public boolean allConditionalsMatch(final HttpRequest request, final HttpCacheEntry entry, final Date now) {
+ final boolean hasEtagValidator = hasSupportedEtagValidator(request);
+ final boolean hasLastModifiedValidator = hasSupportedLastModifiedValidator(request);
+
+ final boolean etagValidatorMatches = (hasEtagValidator) && etagValidatorMatches(request, entry);
+ final boolean lastModifiedValidatorMatches = (hasLastModifiedValidator) && lastModifiedValidatorMatches(request, entry, now);
+
+ if ((hasEtagValidator && hasLastModifiedValidator)
+ && !(etagValidatorMatches && lastModifiedValidatorMatches)) {
+ return false;
+ } else if (hasEtagValidator && !etagValidatorMatches) {
+ return false;
+ }
+
+ if (hasLastModifiedValidator && !lastModifiedValidatorMatches) {
+ return false;
+ }
+ return true;
+ }
+
+ private boolean hasUnsupportedConditionalHeaders(final HttpRequest request) {
+ return (request.getFirstHeader(HeaderConstants.IF_RANGE) != null
+ || request.getFirstHeader(HeaderConstants.IF_MATCH) != null
+ || hasValidDateField(request, HeaderConstants.IF_UNMODIFIED_SINCE));
+ }
+
+ private boolean hasSupportedEtagValidator(final HttpRequest request) {
+ return request.containsHeader(HeaderConstants.IF_NONE_MATCH);
+ }
+
+ private boolean hasSupportedLastModifiedValidator(final HttpRequest request) {
+ return hasValidDateField(request, HeaderConstants.IF_MODIFIED_SINCE);
+ }
+
+ /**
+ * Check entry against If-None-Match
+ * @param request The current httpRequest being made
+ * @param entry the cache entry
+ * @return boolean does the etag validator match
+ */
+ private boolean etagValidatorMatches(final HttpRequest request, final HttpCacheEntry entry) {
+ final Header etagHeader = entry.getFirstHeader(HeaderConstants.ETAG);
+ final String etag = (etagHeader != null) ? etagHeader.getValue() : null;
+ final Header[] ifNoneMatch = request.getHeaders(HeaderConstants.IF_NONE_MATCH);
+ if (ifNoneMatch != null) {
+ for (final Header h : ifNoneMatch) {
+ for (final HeaderElement elt : h.getElements()) {
+ final String reqEtag = elt.toString();
+ if (("*".equals(reqEtag) && etag != null)
+ || reqEtag.equals(etag)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check entry against If-Modified-Since, if If-Modified-Since is in the future it is invalid as per
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
+ * @param request The current httpRequest being made
+ * @param entry the cache entry
+ * @param now right NOW in time
+ * @return boolean Does the last modified header match
+ */
+ private boolean lastModifiedValidatorMatches(final HttpRequest request, final HttpCacheEntry entry, final Date now) {
+ final Header lastModifiedHeader = entry.getFirstHeader(HeaderConstants.LAST_MODIFIED);
+ Date lastModified = null;
+ if (lastModifiedHeader != null) {
+ lastModified = DateUtils.parseDate(lastModifiedHeader.getValue());
+ }
+ if (lastModified == null) {
+ return false;
+ }
+
+ for (final Header h : request.getHeaders(HeaderConstants.IF_MODIFIED_SINCE)) {
+ final Date ifModifiedSince = DateUtils.parseDate(h.getValue());
+ if (ifModifiedSince != null) {
+ if (ifModifiedSince.after(now) || lastModified.after(ifModifiedSince)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ private boolean hasValidDateField(final HttpRequest request, final String headerName) {
+ for(final Header h : request.getHeaders(headerName)) {
+ final Date date = DateUtils.parseDate(h.getValue());
+ return date != null;
+ }
+ return false;
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CachingExec.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CachingExec.java
new file mode 100644
index 0000000000..cf9b8154a1
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CachingExec.java
@@ -0,0 +1,870 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpMessage;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.RequestLine;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.client.cache.CacheResponseStatus;
+import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheContext;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheStorage;
+import ch.boye.httpclientandroidlib.client.cache.ResourceFactory;
+import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
+import ch.boye.httpclientandroidlib.client.methods.HttpExecutionAware;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestWrapper;
+import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
+import ch.boye.httpclientandroidlib.client.utils.DateUtils;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.impl.execchain.ClientExecChain;
+import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.VersionInfo;
+
+/**
+ * Request executor in the request execution chain that is responsible for
+ * transparent client-side caching. The current implementation is conditionally
+ * compliant with HTTP/1.1 (meaning all the MUST and MUST NOTs are obeyed),
+ * although quite a lot, though not all, of the SHOULDs and SHOULD NOTs
+ * are obeyed too.
+ * <p/>
+ * Folks that would like to experiment with alternative storage backends
+ * should look at the {@link HttpCacheStorage} interface and the related
+ * package documentation there. You may also be interested in the provided
+ * {@link ch.boye.httpclientandroidlib.impl.client.cache.ehcache.EhcacheHttpCacheStorage
+ * EhCache} and {@link
+ * ch.boye.httpclientandroidlib.impl.client.cache.memcached.MemcachedHttpCacheStorage
+ * memcached} storage backends.
+ * <p/>
+ * Further responsibilities such as communication with the opposite
+ * endpoint is delegated to the next executor in the request execution
+ * chain.
+ *
+ * @since 4.3
+ */
+@ThreadSafe // So long as the responseCache implementation is threadsafe
+public class CachingExec implements ClientExecChain {
+
+ private final static boolean SUPPORTS_RANGE_AND_CONTENT_RANGE_HEADERS = false;
+
+ private final AtomicLong cacheHits = new AtomicLong();
+ private final AtomicLong cacheMisses = new AtomicLong();
+ private final AtomicLong cacheUpdates = new AtomicLong();
+
+ private final Map<ProtocolVersion, String> viaHeaders = new HashMap<ProtocolVersion, String>(4);
+
+ private final CacheConfig cacheConfig;
+ private final ClientExecChain backend;
+ private final HttpCache responseCache;
+ private final CacheValidityPolicy validityPolicy;
+ private final CachedHttpResponseGenerator responseGenerator;
+ private final CacheableRequestPolicy cacheableRequestPolicy;
+ private final CachedResponseSuitabilityChecker suitabilityChecker;
+ private final ConditionalRequestBuilder conditionalRequestBuilder;
+ private final ResponseProtocolCompliance responseCompliance;
+ private final RequestProtocolCompliance requestCompliance;
+ private final ResponseCachingPolicy responseCachingPolicy;
+
+ private final AsynchronousValidator asynchRevalidator;
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ public CachingExec(
+ final ClientExecChain backend,
+ final HttpCache cache,
+ final CacheConfig config) {
+ this(backend, cache, config, null);
+ }
+
+ public CachingExec(
+ final ClientExecChain backend,
+ final HttpCache cache,
+ final CacheConfig config,
+ final AsynchronousValidator asynchRevalidator) {
+ super();
+ Args.notNull(backend, "HTTP backend");
+ Args.notNull(cache, "HttpCache");
+ this.cacheConfig = config != null ? config : CacheConfig.DEFAULT;
+ this.backend = backend;
+ this.responseCache = cache;
+ this.validityPolicy = new CacheValidityPolicy();
+ this.responseGenerator = new CachedHttpResponseGenerator(this.validityPolicy);
+ this.cacheableRequestPolicy = new CacheableRequestPolicy();
+ this.suitabilityChecker = new CachedResponseSuitabilityChecker(this.validityPolicy, this.cacheConfig);
+ this.conditionalRequestBuilder = new ConditionalRequestBuilder();
+ this.responseCompliance = new ResponseProtocolCompliance();
+ this.requestCompliance = new RequestProtocolCompliance(this.cacheConfig.isWeakETagOnPutDeleteAllowed());
+ this.responseCachingPolicy = new ResponseCachingPolicy(
+ this.cacheConfig.getMaxObjectSize(), this.cacheConfig.isSharedCache(),
+ this.cacheConfig.isNeverCacheHTTP10ResponsesWithQuery(), this.cacheConfig.is303CachingEnabled());
+ this.asynchRevalidator = asynchRevalidator;
+ }
+
+ public CachingExec(
+ final ClientExecChain backend,
+ final ResourceFactory resourceFactory,
+ final HttpCacheStorage storage,
+ final CacheConfig config) {
+ this(backend, new BasicHttpCache(resourceFactory, storage, config), config);
+ }
+
+ public CachingExec(final ClientExecChain backend) {
+ this(backend, new BasicHttpCache(), CacheConfig.DEFAULT);
+ }
+
+ CachingExec(
+ final ClientExecChain backend,
+ final HttpCache responseCache,
+ final CacheValidityPolicy validityPolicy,
+ final ResponseCachingPolicy responseCachingPolicy,
+ final CachedHttpResponseGenerator responseGenerator,
+ final CacheableRequestPolicy cacheableRequestPolicy,
+ final CachedResponseSuitabilityChecker suitabilityChecker,
+ final ConditionalRequestBuilder conditionalRequestBuilder,
+ final ResponseProtocolCompliance responseCompliance,
+ final RequestProtocolCompliance requestCompliance,
+ final CacheConfig config,
+ final AsynchronousValidator asynchRevalidator) {
+ this.cacheConfig = config != null ? config : CacheConfig.DEFAULT;
+ this.backend = backend;
+ this.responseCache = responseCache;
+ this.validityPolicy = validityPolicy;
+ this.responseCachingPolicy = responseCachingPolicy;
+ this.responseGenerator = responseGenerator;
+ this.cacheableRequestPolicy = cacheableRequestPolicy;
+ this.suitabilityChecker = suitabilityChecker;
+ this.conditionalRequestBuilder = conditionalRequestBuilder;
+ this.responseCompliance = responseCompliance;
+ this.requestCompliance = requestCompliance;
+ this.asynchRevalidator = asynchRevalidator;
+ }
+
+ /**
+ * Reports the number of times that the cache successfully responded
+ * to an {@link HttpRequest} without contacting the origin server.
+ * @return the number of cache hits
+ */
+ public long getCacheHits() {
+ return cacheHits.get();
+ }
+
+ /**
+ * Reports the number of times that the cache contacted the origin
+ * server because it had no appropriate response cached.
+ * @return the number of cache misses
+ */
+ public long getCacheMisses() {
+ return cacheMisses.get();
+ }
+
+ /**
+ * Reports the number of times that the cache was able to satisfy
+ * a response by revalidating an existing but stale cache entry.
+ * @return the number of cache revalidations
+ */
+ public long getCacheUpdates() {
+ return cacheUpdates.get();
+ }
+
+ public CloseableHttpResponse execute(
+ final HttpRoute route,
+ final HttpRequestWrapper request) throws IOException, HttpException {
+ return execute(route, request, HttpClientContext.create(), null);
+ }
+
+ public CloseableHttpResponse execute(
+ final HttpRoute route,
+ final HttpRequestWrapper request,
+ final HttpClientContext context) throws IOException, HttpException {
+ return execute(route, request, context, null);
+ }
+
+ public CloseableHttpResponse execute(
+ final HttpRoute route,
+ final HttpRequestWrapper request,
+ final HttpClientContext context,
+ final HttpExecutionAware execAware) throws IOException, HttpException {
+
+ final HttpHost target = context.getTargetHost();
+ final String via = generateViaHeader(request.getOriginal());
+
+ // default response context
+ setResponseStatus(context, CacheResponseStatus.CACHE_MISS);
+
+ if (clientRequestsOurOptions(request)) {
+ setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
+ return Proxies.enhanceResponse(new OptionsHttp11Response());
+ }
+
+ final HttpResponse fatalErrorResponse = getFatallyNoncompliantResponse(request, context);
+ if (fatalErrorResponse != null) {
+ return Proxies.enhanceResponse(fatalErrorResponse);
+ }
+
+ requestCompliance.makeRequestCompliant(request);
+ request.addHeader("Via",via);
+
+ flushEntriesInvalidatedByRequest(context.getTargetHost(), request);
+
+ if (!cacheableRequestPolicy.isServableFromCache(request)) {
+ log.debug("Request is not servable from cache");
+ return callBackend(route, request, context, execAware);
+ }
+
+ final HttpCacheEntry entry = satisfyFromCache(target, request);
+ if (entry == null) {
+ log.debug("Cache miss");
+ return handleCacheMiss(route, request, context, execAware);
+ } else {
+ return handleCacheHit(route, request, context, execAware, entry);
+ }
+ }
+
+ private CloseableHttpResponse handleCacheHit(
+ final HttpRoute route,
+ final HttpRequestWrapper request,
+ final HttpClientContext context,
+ final HttpExecutionAware execAware,
+ final HttpCacheEntry entry) throws IOException, HttpException {
+ final HttpHost target = context.getTargetHost();
+ recordCacheHit(target, request);
+ CloseableHttpResponse out = null;
+ final Date now = getCurrentDate();
+ if (suitabilityChecker.canCachedResponseBeUsed(target, request, entry, now)) {
+ log.debug("Cache hit");
+ out = generateCachedResponse(request, context, entry, now);
+ } else if (!mayCallBackend(request)) {
+ log.debug("Cache entry not suitable but only-if-cached requested");
+ out = generateGatewayTimeout(context);
+ } else if (!(entry.getStatusCode() == HttpStatus.SC_NOT_MODIFIED
+ && !suitabilityChecker.isConditional(request))) {
+ log.debug("Revalidating cache entry");
+ return revalidateCacheEntry(route, request, context, execAware, entry, now);
+ } else {
+ log.debug("Cache entry not usable; calling backend");
+ return callBackend(route, request, context, execAware);
+ }
+ context.setAttribute(HttpClientContext.HTTP_ROUTE, route);
+ context.setAttribute(HttpClientContext.HTTP_TARGET_HOST, target);
+ context.setAttribute(HttpClientContext.HTTP_REQUEST, request);
+ context.setAttribute(HttpClientContext.HTTP_RESPONSE, out);
+ context.setAttribute(HttpClientContext.HTTP_REQ_SENT, Boolean.TRUE);
+ return out;
+ }
+
+ private CloseableHttpResponse revalidateCacheEntry(
+ final HttpRoute route,
+ final HttpRequestWrapper request,
+ final HttpClientContext context,
+ final HttpExecutionAware execAware,
+ final HttpCacheEntry entry,
+ final Date now) throws HttpException {
+
+ try {
+ if (asynchRevalidator != null
+ && !staleResponseNotAllowed(request, entry, now)
+ && validityPolicy.mayReturnStaleWhileRevalidating(entry, now)) {
+ log.trace("Serving stale with asynchronous revalidation");
+ final CloseableHttpResponse resp = generateCachedResponse(request, context, entry, now);
+ asynchRevalidator.revalidateCacheEntry(this, route, request, context, execAware, entry);
+ return resp;
+ }
+ return revalidateCacheEntry(route, request, context, execAware, entry);
+ } catch (final IOException ioex) {
+ return handleRevalidationFailure(request, context, entry, now);
+ }
+ }
+
+ private CloseableHttpResponse handleCacheMiss(
+ final HttpRoute route,
+ final HttpRequestWrapper request,
+ final HttpClientContext context,
+ final HttpExecutionAware execAware) throws IOException, HttpException {
+ final HttpHost target = context.getTargetHost();
+ recordCacheMiss(target, request);
+
+ if (!mayCallBackend(request)) {
+ return Proxies.enhanceResponse(
+ new BasicHttpResponse(
+ HttpVersion.HTTP_1_1, HttpStatus.SC_GATEWAY_TIMEOUT, "Gateway Timeout"));
+ }
+
+ final Map<String, Variant> variants = getExistingCacheVariants(target, request);
+ if (variants != null && variants.size() > 0) {
+ return negotiateResponseFromVariants(route, request, context,
+ execAware, variants);
+ }
+
+ return callBackend(route, request, context, execAware);
+ }
+
+ private HttpCacheEntry satisfyFromCache(
+ final HttpHost target, final HttpRequestWrapper request) {
+ HttpCacheEntry entry = null;
+ try {
+ entry = responseCache.getCacheEntry(target, request);
+ } catch (final IOException ioe) {
+ log.warn("Unable to retrieve entries from cache", ioe);
+ }
+ return entry;
+ }
+
+ private HttpResponse getFatallyNoncompliantResponse(
+ final HttpRequestWrapper request,
+ final HttpContext context) {
+ HttpResponse fatalErrorResponse = null;
+ final List<RequestProtocolError> fatalError = requestCompliance.requestIsFatallyNonCompliant(request);
+
+ for (final RequestProtocolError error : fatalError) {
+ setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
+ fatalErrorResponse = requestCompliance.getErrorForRequest(error);
+ }
+ return fatalErrorResponse;
+ }
+
+ private Map<String, Variant> getExistingCacheVariants(
+ final HttpHost target,
+ final HttpRequestWrapper request) {
+ Map<String,Variant> variants = null;
+ try {
+ variants = responseCache.getVariantCacheEntriesWithEtags(target, request);
+ } catch (final IOException ioe) {
+ log.warn("Unable to retrieve variant entries from cache", ioe);
+ }
+ return variants;
+ }
+
+ private void recordCacheMiss(final HttpHost target, final HttpRequestWrapper request) {
+ cacheMisses.getAndIncrement();
+ if (log.isTraceEnabled()) {
+ final RequestLine rl = request.getRequestLine();
+ log.trace("Cache miss [host: " + target + "; uri: " + rl.getUri() + "]");
+ }
+ }
+
+ private void recordCacheHit(final HttpHost target, final HttpRequestWrapper request) {
+ cacheHits.getAndIncrement();
+ if (log.isTraceEnabled()) {
+ final RequestLine rl = request.getRequestLine();
+ log.trace("Cache hit [host: " + target + "; uri: " + rl.getUri() + "]");
+ }
+ }
+
+ private void recordCacheUpdate(final HttpContext context) {
+ cacheUpdates.getAndIncrement();
+ setResponseStatus(context, CacheResponseStatus.VALIDATED);
+ }
+
+ private void flushEntriesInvalidatedByRequest(
+ final HttpHost target,
+ final HttpRequestWrapper request) {
+ try {
+ responseCache.flushInvalidatedCacheEntriesFor(target, request);
+ } catch (final IOException ioe) {
+ log.warn("Unable to flush invalidated entries from cache", ioe);
+ }
+ }
+
+ private CloseableHttpResponse generateCachedResponse(final HttpRequestWrapper request,
+ final HttpContext context, final HttpCacheEntry entry, final Date now) {
+ final CloseableHttpResponse cachedResponse;
+ if (request.containsHeader(HeaderConstants.IF_NONE_MATCH)
+ || request.containsHeader(HeaderConstants.IF_MODIFIED_SINCE)) {
+ cachedResponse = responseGenerator.generateNotModifiedResponse(entry);
+ } else {
+ cachedResponse = responseGenerator.generateResponse(entry);
+ }
+ setResponseStatus(context, CacheResponseStatus.CACHE_HIT);
+ if (validityPolicy.getStalenessSecs(entry, now) > 0L) {
+ cachedResponse.addHeader(HeaderConstants.WARNING,"110 localhost \"Response is stale\"");
+ }
+ return cachedResponse;
+ }
+
+ private CloseableHttpResponse handleRevalidationFailure(
+ final HttpRequestWrapper request,
+ final HttpContext context,
+ final HttpCacheEntry entry,
+ final Date now) {
+ if (staleResponseNotAllowed(request, entry, now)) {
+ return generateGatewayTimeout(context);
+ } else {
+ return unvalidatedCacheHit(context, entry);
+ }
+ }
+
+ private CloseableHttpResponse generateGatewayTimeout(
+ final HttpContext context) {
+ setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
+ return Proxies.enhanceResponse(new BasicHttpResponse(
+ HttpVersion.HTTP_1_1, HttpStatus.SC_GATEWAY_TIMEOUT,
+ "Gateway Timeout"));
+ }
+
+ private CloseableHttpResponse unvalidatedCacheHit(
+ final HttpContext context, final HttpCacheEntry entry) {
+ final CloseableHttpResponse cachedResponse = responseGenerator.generateResponse(entry);
+ setResponseStatus(context, CacheResponseStatus.CACHE_HIT);
+ cachedResponse.addHeader(HeaderConstants.WARNING, "111 localhost \"Revalidation failed\"");
+ return cachedResponse;
+ }
+
+ private boolean staleResponseNotAllowed(
+ final HttpRequestWrapper request,
+ final HttpCacheEntry entry,
+ final Date now) {
+ return validityPolicy.mustRevalidate(entry)
+ || (cacheConfig.isSharedCache() && validityPolicy.proxyRevalidate(entry))
+ || explicitFreshnessRequest(request, entry, now);
+ }
+
+ private boolean mayCallBackend(final HttpRequestWrapper request) {
+ for (final Header h: request.getHeaders(HeaderConstants.CACHE_CONTROL)) {
+ for (final HeaderElement elt : h.getElements()) {
+ if ("only-if-cached".equals(elt.getName())) {
+ log.trace("Request marked only-if-cached");
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ private boolean explicitFreshnessRequest(
+ final HttpRequestWrapper request,
+ final HttpCacheEntry entry,
+ final Date now) {
+ for(final Header h : request.getHeaders(HeaderConstants.CACHE_CONTROL)) {
+ for(final HeaderElement elt : h.getElements()) {
+ if (HeaderConstants.CACHE_CONTROL_MAX_STALE.equals(elt.getName())) {
+ try {
+ final int maxstale = Integer.parseInt(elt.getValue());
+ final long age = validityPolicy.getCurrentAgeSecs(entry, now);
+ final long lifetime = validityPolicy.getFreshnessLifetimeSecs(entry);
+ if (age - lifetime > maxstale) {
+ return true;
+ }
+ } catch (final NumberFormatException nfe) {
+ return true;
+ }
+ } else if (HeaderConstants.CACHE_CONTROL_MIN_FRESH.equals(elt.getName())
+ || HeaderConstants.CACHE_CONTROL_MAX_AGE.equals(elt.getName())) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private String generateViaHeader(final HttpMessage msg) {
+
+ final ProtocolVersion pv = msg.getProtocolVersion();
+ final String existingEntry = viaHeaders.get(pv);
+ if (existingEntry != null) {
+ return existingEntry;
+ }
+
+ final VersionInfo vi = VersionInfo.loadVersionInfo("ch.boye.httpclientandroidlib.client", getClass().getClassLoader());
+ final String release = (vi != null) ? vi.getRelease() : VersionInfo.UNAVAILABLE;
+
+ String value;
+ if ("http".equalsIgnoreCase(pv.getProtocol())) {
+ value = String.format("%d.%d localhost (Apache-HttpClient/%s (cache))", pv.getMajor(), pv.getMinor(),
+ release);
+ } else {
+ value = String.format("%s/%d.%d localhost (Apache-HttpClient/%s (cache))", pv.getProtocol(), pv.getMajor(),
+ pv.getMinor(), release);
+ }
+ viaHeaders.put(pv, value);
+
+ return value;
+ }
+
+ private void setResponseStatus(final HttpContext context, final CacheResponseStatus value) {
+ if (context != null) {
+ context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, value);
+ }
+ }
+
+ /**
+ * Reports whether this {@code CachingHttpClient} implementation
+ * supports byte-range requests as specified by the {@code Range}
+ * and {@code Content-Range} headers.
+ * @return {@code true} if byte-range requests are supported
+ */
+ public boolean supportsRangeAndContentRangeHeaders() {
+ return SUPPORTS_RANGE_AND_CONTENT_RANGE_HEADERS;
+ }
+
+ Date getCurrentDate() {
+ return new Date();
+ }
+
+ boolean clientRequestsOurOptions(final HttpRequest request) {
+ final RequestLine line = request.getRequestLine();
+
+ if (!HeaderConstants.OPTIONS_METHOD.equals(line.getMethod())) {
+ return false;
+ }
+
+ if (!"*".equals(line.getUri())) {
+ return false;
+ }
+
+ if (!"0".equals(request.getFirstHeader(HeaderConstants.MAX_FORWARDS).getValue())) {
+ return false;
+ }
+
+ return true;
+ }
+
+ CloseableHttpResponse callBackend(
+ final HttpRoute route,
+ final HttpRequestWrapper request,
+ final HttpClientContext context,
+ final HttpExecutionAware execAware) throws IOException, HttpException {
+
+ final Date requestDate = getCurrentDate();
+
+ log.trace("Calling the backend");
+ final CloseableHttpResponse backendResponse = backend.execute(route, request, context, execAware);
+ try {
+ backendResponse.addHeader("Via", generateViaHeader(backendResponse));
+ return handleBackendResponse(route, request, context, execAware,
+ requestDate, getCurrentDate(), backendResponse);
+ } catch (final IOException ex) {
+ backendResponse.close();
+ throw ex;
+ } catch (final RuntimeException ex) {
+ backendResponse.close();
+ throw ex;
+ }
+ }
+
+ private boolean revalidationResponseIsTooOld(final HttpResponse backendResponse,
+ final HttpCacheEntry cacheEntry) {
+ final Header entryDateHeader = cacheEntry.getFirstHeader(HTTP.DATE_HEADER);
+ final Header responseDateHeader = backendResponse.getFirstHeader(HTTP.DATE_HEADER);
+ if (entryDateHeader != null && responseDateHeader != null) {
+ final Date entryDate = DateUtils.parseDate(entryDateHeader.getValue());
+ final Date respDate = DateUtils.parseDate(responseDateHeader.getValue());
+ if (entryDate == null || respDate == null) {
+ // either backend response or cached entry did not have a valid
+ // Date header, so we can't tell if they are out of order
+ // according to the origin clock; thus we can skip the
+ // unconditional retry recommended in 13.2.6 of RFC 2616.
+ return false;
+ }
+ if (respDate.before(entryDate)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ CloseableHttpResponse negotiateResponseFromVariants(
+ final HttpRoute route,
+ final HttpRequestWrapper request,
+ final HttpClientContext context,
+ final HttpExecutionAware execAware,
+ final Map<String, Variant> variants) throws IOException, HttpException {
+ final HttpRequestWrapper conditionalRequest = conditionalRequestBuilder
+ .buildConditionalRequestFromVariants(request, variants);
+
+ final Date requestDate = getCurrentDate();
+ final CloseableHttpResponse backendResponse = backend.execute(
+ route, conditionalRequest, context, execAware);
+ try {
+ final Date responseDate = getCurrentDate();
+
+ backendResponse.addHeader("Via", generateViaHeader(backendResponse));
+
+ if (backendResponse.getStatusLine().getStatusCode() != HttpStatus.SC_NOT_MODIFIED) {
+ return handleBackendResponse(
+ route, request, context, execAware,
+ requestDate, responseDate, backendResponse);
+ }
+
+ final Header resultEtagHeader = backendResponse.getFirstHeader(HeaderConstants.ETAG);
+ if (resultEtagHeader == null) {
+ log.warn("304 response did not contain ETag");
+ IOUtils.consume(backendResponse.getEntity());
+ backendResponse.close();
+ return callBackend(route, request, context, execAware);
+ }
+
+ final String resultEtag = resultEtagHeader.getValue();
+ final Variant matchingVariant = variants.get(resultEtag);
+ if (matchingVariant == null) {
+ log.debug("304 response did not contain ETag matching one sent in If-None-Match");
+ IOUtils.consume(backendResponse.getEntity());
+ backendResponse.close();
+ return callBackend(route, request, context, execAware);
+ }
+
+ final HttpCacheEntry matchedEntry = matchingVariant.getEntry();
+
+ if (revalidationResponseIsTooOld(backendResponse, matchedEntry)) {
+ IOUtils.consume(backendResponse.getEntity());
+ backendResponse.close();
+ return retryRequestUnconditionally(route, request, context, execAware, matchedEntry);
+ }
+
+ recordCacheUpdate(context);
+
+ final HttpCacheEntry responseEntry = getUpdatedVariantEntry(
+ context.getTargetHost(), conditionalRequest, requestDate, responseDate,
+ backendResponse, matchingVariant, matchedEntry);
+ backendResponse.close();
+
+ final CloseableHttpResponse resp = responseGenerator.generateResponse(responseEntry);
+ tryToUpdateVariantMap(context.getTargetHost(), request, matchingVariant);
+
+ if (shouldSendNotModifiedResponse(request, responseEntry)) {
+ return responseGenerator.generateNotModifiedResponse(responseEntry);
+ }
+ return resp;
+ } catch (final IOException ex) {
+ backendResponse.close();
+ throw ex;
+ } catch (final RuntimeException ex) {
+ backendResponse.close();
+ throw ex;
+ }
+ }
+
+ private CloseableHttpResponse retryRequestUnconditionally(
+ final HttpRoute route,
+ final HttpRequestWrapper request,
+ final HttpClientContext context,
+ final HttpExecutionAware execAware,
+ final HttpCacheEntry matchedEntry) throws IOException, HttpException {
+ final HttpRequestWrapper unconditional = conditionalRequestBuilder
+ .buildUnconditionalRequest(request, matchedEntry);
+ return callBackend(route, unconditional, context, execAware);
+ }
+
+ private HttpCacheEntry getUpdatedVariantEntry(
+ final HttpHost target,
+ final HttpRequestWrapper conditionalRequest,
+ final Date requestDate,
+ final Date responseDate,
+ final CloseableHttpResponse backendResponse,
+ final Variant matchingVariant,
+ final HttpCacheEntry matchedEntry) throws IOException {
+ HttpCacheEntry responseEntry = matchedEntry;
+ try {
+ responseEntry = responseCache.updateVariantCacheEntry(target, conditionalRequest,
+ matchedEntry, backendResponse, requestDate, responseDate, matchingVariant.getCacheKey());
+ } catch (final IOException ioe) {
+ log.warn("Could not update cache entry", ioe);
+ } finally {
+ backendResponse.close();
+ }
+ return responseEntry;
+ }
+
+ private void tryToUpdateVariantMap(
+ final HttpHost target,
+ final HttpRequestWrapper request,
+ final Variant matchingVariant) {
+ try {
+ responseCache.reuseVariantEntryFor(target, request, matchingVariant);
+ } catch (final IOException ioe) {
+ log.warn("Could not update cache entry to reuse variant", ioe);
+ }
+ }
+
+ private boolean shouldSendNotModifiedResponse(
+ final HttpRequestWrapper request,
+ final HttpCacheEntry responseEntry) {
+ return (suitabilityChecker.isConditional(request)
+ && suitabilityChecker.allConditionalsMatch(request, responseEntry, new Date()));
+ }
+
+ CloseableHttpResponse revalidateCacheEntry(
+ final HttpRoute route,
+ final HttpRequestWrapper request,
+ final HttpClientContext context,
+ final HttpExecutionAware execAware,
+ final HttpCacheEntry cacheEntry) throws IOException, HttpException {
+
+ final HttpRequestWrapper conditionalRequest = conditionalRequestBuilder.buildConditionalRequest(request, cacheEntry);
+
+ Date requestDate = getCurrentDate();
+ CloseableHttpResponse backendResponse = backend.execute(
+ route, conditionalRequest, context, execAware);
+ Date responseDate = getCurrentDate();
+
+ if (revalidationResponseIsTooOld(backendResponse, cacheEntry)) {
+ backendResponse.close();
+ final HttpRequestWrapper unconditional = conditionalRequestBuilder
+ .buildUnconditionalRequest(request, cacheEntry);
+ requestDate = getCurrentDate();
+ backendResponse = backend.execute(route, unconditional, context, execAware);
+ responseDate = getCurrentDate();
+ }
+
+ backendResponse.addHeader(HeaderConstants.VIA, generateViaHeader(backendResponse));
+
+ final int statusCode = backendResponse.getStatusLine().getStatusCode();
+ if (statusCode == HttpStatus.SC_NOT_MODIFIED || statusCode == HttpStatus.SC_OK) {
+ recordCacheUpdate(context);
+ }
+
+ if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
+ final HttpCacheEntry updatedEntry = responseCache.updateCacheEntry(
+ context.getTargetHost(), request, cacheEntry,
+ backendResponse, requestDate, responseDate);
+ if (suitabilityChecker.isConditional(request)
+ && suitabilityChecker.allConditionalsMatch(request, updatedEntry, new Date())) {
+ return responseGenerator
+ .generateNotModifiedResponse(updatedEntry);
+ }
+ return responseGenerator.generateResponse(updatedEntry);
+ }
+
+ if (staleIfErrorAppliesTo(statusCode)
+ && !staleResponseNotAllowed(request, cacheEntry, getCurrentDate())
+ && validityPolicy.mayReturnStaleIfError(request, cacheEntry, responseDate)) {
+ try {
+ final CloseableHttpResponse cachedResponse = responseGenerator.generateResponse(cacheEntry);
+ cachedResponse.addHeader(HeaderConstants.WARNING, "110 localhost \"Response is stale\"");
+ return cachedResponse;
+ } finally {
+ backendResponse.close();
+ }
+ }
+ return handleBackendResponse(
+ route, conditionalRequest, context, execAware,
+ requestDate, responseDate, backendResponse);
+ }
+
+ private boolean staleIfErrorAppliesTo(final int statusCode) {
+ return statusCode == HttpStatus.SC_INTERNAL_SERVER_ERROR
+ || statusCode == HttpStatus.SC_BAD_GATEWAY
+ || statusCode == HttpStatus.SC_SERVICE_UNAVAILABLE
+ || statusCode == HttpStatus.SC_GATEWAY_TIMEOUT;
+ }
+
+ CloseableHttpResponse handleBackendResponse(
+ final HttpRoute route,
+ final HttpRequestWrapper request,
+ final HttpClientContext context,
+ final HttpExecutionAware execAware,
+ final Date requestDate,
+ final Date responseDate,
+ final CloseableHttpResponse backendResponse) throws IOException {
+
+ log.trace("Handling Backend response");
+ responseCompliance.ensureProtocolCompliance(request, backendResponse);
+
+ final HttpHost target = context.getTargetHost();
+ final boolean cacheable = responseCachingPolicy.isResponseCacheable(request, backendResponse);
+ responseCache.flushInvalidatedCacheEntriesFor(target, request, backendResponse);
+ if (cacheable && !alreadyHaveNewerCacheEntry(target, request, backendResponse)) {
+ storeRequestIfModifiedSinceFor304Response(request, backendResponse);
+ return responseCache.cacheAndReturnResponse(target, request,
+ backendResponse, requestDate, responseDate);
+ }
+ if (!cacheable) {
+ try {
+ responseCache.flushCacheEntriesFor(target, request);
+ } catch (final IOException ioe) {
+ log.warn("Unable to flush invalid cache entries", ioe);
+ }
+ }
+ return backendResponse;
+ }
+
+ /**
+ * For 304 Not modified responses, adds a "Last-Modified" header with the
+ * value of the "If-Modified-Since" header passed in the request. This
+ * header is required to be able to reuse match the cache entry for
+ * subsequent requests but as defined in http specifications it is not
+ * included in 304 responses by backend servers. This header will not be
+ * included in the resulting response.
+ */
+ private void storeRequestIfModifiedSinceFor304Response(
+ final HttpRequest request, final HttpResponse backendResponse) {
+ if (backendResponse.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
+ final Header h = request.getFirstHeader("If-Modified-Since");
+ if (h != null) {
+ backendResponse.addHeader("Last-Modified", h.getValue());
+ }
+ }
+ }
+
+ private boolean alreadyHaveNewerCacheEntry(final HttpHost target, final HttpRequestWrapper request,
+ final HttpResponse backendResponse) {
+ HttpCacheEntry existing = null;
+ try {
+ existing = responseCache.getCacheEntry(target, request);
+ } catch (final IOException ioe) {
+ // nop
+ }
+ if (existing == null) {
+ return false;
+ }
+ final Header entryDateHeader = existing.getFirstHeader(HTTP.DATE_HEADER);
+ if (entryDateHeader == null) {
+ return false;
+ }
+ final Header responseDateHeader = backendResponse.getFirstHeader(HTTP.DATE_HEADER);
+ if (responseDateHeader == null) {
+ return false;
+ }
+ final Date entryDate = DateUtils.parseDate(entryDateHeader.getValue());
+ final Date responseDate = DateUtils.parseDate(responseDateHeader.getValue());
+ if (entryDate == null || responseDate == null) {
+ return false;
+ }
+ return responseDate.before(entryDate);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CachingHttpClientBuilder.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CachingHttpClientBuilder.java
new file mode 100644
index 0000000000..385324f32b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CachingHttpClientBuilder.java
@@ -0,0 +1,149 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.File;
+
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheInvalidator;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheStorage;
+import ch.boye.httpclientandroidlib.client.cache.ResourceFactory;
+import ch.boye.httpclientandroidlib.impl.client.HttpClientBuilder;
+import ch.boye.httpclientandroidlib.impl.execchain.ClientExecChain;
+
+/**
+ * Builder for {@link ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient}
+ * instances capable of client-side caching.
+ *
+ * @since 4.3
+ */
+public class CachingHttpClientBuilder extends HttpClientBuilder {
+
+ private ResourceFactory resourceFactory;
+ private HttpCacheStorage storage;
+ private File cacheDir;
+ private CacheConfig cacheConfig;
+ private SchedulingStrategy schedulingStrategy;
+ private HttpCacheInvalidator httpCacheInvalidator;
+
+ public static CachingHttpClientBuilder create() {
+ return new CachingHttpClientBuilder();
+ }
+
+ protected CachingHttpClientBuilder() {
+ super();
+ }
+
+ public final CachingHttpClientBuilder setResourceFactory(
+ final ResourceFactory resourceFactory) {
+ this.resourceFactory = resourceFactory;
+ return this;
+ }
+
+ public final CachingHttpClientBuilder setHttpCacheStorage(
+ final HttpCacheStorage storage) {
+ this.storage = storage;
+ return this;
+ }
+
+ public final CachingHttpClientBuilder setCacheDir(
+ final File cacheDir) {
+ this.cacheDir = cacheDir;
+ return this;
+ }
+
+ public final CachingHttpClientBuilder setCacheConfig(
+ final CacheConfig cacheConfig) {
+ this.cacheConfig = cacheConfig;
+ return this;
+ }
+
+ public final CachingHttpClientBuilder setSchedulingStrategy(
+ final SchedulingStrategy schedulingStrategy) {
+ this.schedulingStrategy = schedulingStrategy;
+ return this;
+ }
+
+ public final CachingHttpClientBuilder setHttpCacheInvalidator(
+ final HttpCacheInvalidator cacheInvalidator) {
+ this.httpCacheInvalidator = cacheInvalidator;
+ return this;
+ }
+
+ @Override
+ protected ClientExecChain decorateMainExec(final ClientExecChain mainExec) {
+ final CacheConfig config = this.cacheConfig != null ? this.cacheConfig : CacheConfig.DEFAULT;
+ ResourceFactory resourceFactory = this.resourceFactory;
+ if (resourceFactory == null) {
+ if (this.cacheDir == null) {
+ resourceFactory = new HeapResourceFactory();
+ } else {
+ resourceFactory = new FileResourceFactory(cacheDir);
+ }
+ }
+ HttpCacheStorage storage = this.storage;
+ if (storage == null) {
+ if (this.cacheDir == null) {
+ storage = new BasicHttpCacheStorage(config);
+ } else {
+ final ManagedHttpCacheStorage managedStorage = new ManagedHttpCacheStorage(config);
+ addCloseable(managedStorage);
+ storage = managedStorage;
+ }
+ }
+ final AsynchronousValidator revalidator = createAsynchronousRevalidator(config);
+ final CacheKeyGenerator uriExtractor = new CacheKeyGenerator();
+
+ HttpCacheInvalidator cacheInvalidator = this.httpCacheInvalidator;
+ if (cacheInvalidator == null) {
+ cacheInvalidator = new CacheInvalidator(uriExtractor, storage);
+ }
+
+ return new CachingExec(mainExec,
+ new BasicHttpCache(
+ resourceFactory,
+ storage, config,
+ uriExtractor,
+ cacheInvalidator), config, revalidator);
+ }
+
+ private AsynchronousValidator createAsynchronousRevalidator(final CacheConfig config) {
+ if (config.getAsynchronousWorkersMax() > 0) {
+ final SchedulingStrategy configuredSchedulingStrategy = createSchedulingStrategy(config);
+ final AsynchronousValidator revalidator = new AsynchronousValidator(
+ configuredSchedulingStrategy);
+ addCloseable(revalidator);
+ return revalidator;
+ }
+ return null;
+ }
+
+ @SuppressWarnings("resource")
+ private SchedulingStrategy createSchedulingStrategy(final CacheConfig config) {
+ return schedulingStrategy != null ? schedulingStrategy : new ImmediateSchedulingStrategy(config);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CachingHttpClients.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CachingHttpClients.java
new file mode 100644
index 0000000000..328c147f38
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CachingHttpClients.java
@@ -0,0 +1,74 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.File;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient;
+
+/**
+ * Factory methods for {@link CloseableHttpClient} instances
+ * capable of client-side caching.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class CachingHttpClients {
+
+ private CachingHttpClients() {
+ super();
+ }
+
+ /**
+ * Creates builder object for construction of custom
+ * {@link CloseableHttpClient} instances.
+ */
+ public static CachingHttpClientBuilder custom() {
+ return CachingHttpClientBuilder.create();
+ }
+
+ /**
+ * Creates {@link CloseableHttpClient} instance that uses a memory bound
+ * response cache.
+ */
+ public static CloseableHttpClient createMemoryBound() {
+ return CachingHttpClientBuilder.create().build();
+ }
+
+ /**
+ * Creates {@link CloseableHttpClient} instance that uses a file system
+ * bound response cache.
+ *
+ * @param cacheDir location of response cache.
+ */
+ public static CloseableHttpClient createFileBound(final File cacheDir) {
+ return CachingHttpClientBuilder.create().setCacheDir(cacheDir).build();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CombinedEntity.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CombinedEntity.java
new file mode 100644
index 0000000000..62bfaff3eb
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/CombinedEntity.java
@@ -0,0 +1,104 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.SequenceInputStream;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.client.cache.Resource;
+import ch.boye.httpclientandroidlib.entity.AbstractHttpEntity;
+import ch.boye.httpclientandroidlib.util.Args;
+
+@NotThreadSafe
+class CombinedEntity extends AbstractHttpEntity {
+
+ private final Resource resource;
+ private final InputStream combinedStream;
+
+ CombinedEntity(final Resource resource, final InputStream instream) throws IOException {
+ super();
+ this.resource = resource;
+ this.combinedStream = new SequenceInputStream(
+ new ResourceStream(resource.getInputStream()), instream);
+ }
+
+ public long getContentLength() {
+ return -1;
+ }
+
+ public boolean isRepeatable() {
+ return false;
+ }
+
+ public boolean isStreaming() {
+ return true;
+ }
+
+ public InputStream getContent() throws IOException, IllegalStateException {
+ return this.combinedStream;
+ }
+
+ public void writeTo(final OutputStream outstream) throws IOException {
+ Args.notNull(outstream, "Output stream");
+ final InputStream instream = getContent();
+ try {
+ int l;
+ final byte[] tmp = new byte[2048];
+ while ((l = instream.read(tmp)) != -1) {
+ outstream.write(tmp, 0, l);
+ }
+ } finally {
+ instream.close();
+ }
+ }
+
+ private void dispose() {
+ this.resource.dispose();
+ }
+
+ class ResourceStream extends FilterInputStream {
+
+ protected ResourceStream(final InputStream in) {
+ super(in);
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ super.close();
+ } finally {
+ dispose();
+ }
+ }
+
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ConditionalRequestBuilder.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ConditionalRequestBuilder.java
new file mode 100644
index 0000000000..0edc4fb859
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ConditionalRequestBuilder.java
@@ -0,0 +1,140 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.util.Map;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestWrapper;
+
+/**
+ * @since 4.1
+ */
+@Immutable
+class ConditionalRequestBuilder {
+
+ /**
+ * When a {@link HttpCacheEntry} is stale but 'might' be used as a response
+ * to an {@link ch.boye.httpclientandroidlib.HttpRequest} we will attempt to revalidate
+ * the entry with the origin. Build the origin {@link ch.boye.httpclientandroidlib.HttpRequest}
+ * here and return it.
+ *
+ * @param request the original request from the caller
+ * @param cacheEntry the entry that needs to be re-validated
+ * @return the wrapped request
+ * @throws ProtocolException when I am unable to build a new origin request.
+ */
+ public HttpRequestWrapper buildConditionalRequest(final HttpRequestWrapper request, final HttpCacheEntry cacheEntry)
+ throws ProtocolException {
+ final HttpRequestWrapper newRequest = HttpRequestWrapper.wrap(request.getOriginal());
+ newRequest.setHeaders(request.getAllHeaders());
+ final Header eTag = cacheEntry.getFirstHeader(HeaderConstants.ETAG);
+ if (eTag != null) {
+ newRequest.setHeader(HeaderConstants.IF_NONE_MATCH, eTag.getValue());
+ }
+ final Header lastModified = cacheEntry.getFirstHeader(HeaderConstants.LAST_MODIFIED);
+ if (lastModified != null) {
+ newRequest.setHeader(HeaderConstants.IF_MODIFIED_SINCE, lastModified.getValue());
+ }
+ boolean mustRevalidate = false;
+ for(final Header h : cacheEntry.getHeaders(HeaderConstants.CACHE_CONTROL)) {
+ for(final HeaderElement elt : h.getElements()) {
+ if (HeaderConstants.CACHE_CONTROL_MUST_REVALIDATE.equalsIgnoreCase(elt.getName())
+ || HeaderConstants.CACHE_CONTROL_PROXY_REVALIDATE.equalsIgnoreCase(elt.getName())) {
+ mustRevalidate = true;
+ break;
+ }
+ }
+ }
+ if (mustRevalidate) {
+ newRequest.addHeader(HeaderConstants.CACHE_CONTROL, HeaderConstants.CACHE_CONTROL_MAX_AGE + "=0");
+ }
+ return newRequest;
+
+ }
+
+ /**
+ * When a {@link HttpCacheEntry} does not exist for a specific
+ * {@link ch.boye.httpclientandroidlib.HttpRequest} we attempt to see if an existing
+ * {@link HttpCacheEntry} is appropriate by building a conditional
+ * {@link ch.boye.httpclientandroidlib.HttpRequest} using the variants' ETag values.
+ * If no such values exist, the request is unmodified
+ *
+ * @param request the original request from the caller
+ * @param variants
+ * @return the wrapped request
+ */
+ public HttpRequestWrapper buildConditionalRequestFromVariants(final HttpRequestWrapper request,
+ final Map<String, Variant> variants) {
+ final HttpRequestWrapper newRequest = HttpRequestWrapper.wrap(request.getOriginal());
+ newRequest.setHeaders(request.getAllHeaders());
+
+ // we do not support partial content so all etags are used
+ final StringBuilder etags = new StringBuilder();
+ boolean first = true;
+ for(final String etag : variants.keySet()) {
+ if (!first) {
+ etags.append(",");
+ }
+ first = false;
+ etags.append(etag);
+ }
+
+ newRequest.setHeader(HeaderConstants.IF_NONE_MATCH, etags.toString());
+ return newRequest;
+ }
+
+ /**
+ * Returns a request to unconditionally validate a cache entry with
+ * the origin. In certain cases (due to multiple intervening caches)
+ * our cache may actually receive a response to a normal conditional
+ * validation where the Date header is actually older than that of
+ * our current cache entry. In this case, the protocol recommendation
+ * is to retry the validation and force syncup with the origin.
+ * @param request client request we are trying to satisfy
+ * @param entry existing cache entry we are trying to validate
+ * @return an unconditional validation request
+ */
+ public HttpRequestWrapper buildUnconditionalRequest(final HttpRequestWrapper request, final HttpCacheEntry entry) {
+ final HttpRequestWrapper newRequest = HttpRequestWrapper.wrap(request.getOriginal());
+ newRequest.setHeaders(request.getAllHeaders());
+ newRequest.addHeader(HeaderConstants.CACHE_CONTROL,HeaderConstants.CACHE_CONTROL_NO_CACHE);
+ newRequest.addHeader(HeaderConstants.PRAGMA,HeaderConstants.CACHE_CONTROL_NO_CACHE);
+ newRequest.removeHeaders(HeaderConstants.IF_RANGE);
+ newRequest.removeHeaders(HeaderConstants.IF_MATCH);
+ newRequest.removeHeaders(HeaderConstants.IF_NONE_MATCH);
+ newRequest.removeHeaders(HeaderConstants.IF_UNMODIFIED_SINCE);
+ newRequest.removeHeaders(HeaderConstants.IF_MODIFIED_SINCE);
+ return newRequest;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/DefaultFailureCache.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/DefaultFailureCache.java
new file mode 100644
index 0000000000..3646c3c326
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/DefaultFailureCache.java
@@ -0,0 +1,143 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * Implements a bounded failure cache. The oldest entries are discarded when
+ * the maximum size is exceeded.
+ *
+ * @since 4.3
+ */
+@ThreadSafe
+public class DefaultFailureCache implements FailureCache {
+
+ static final int DEFAULT_MAX_SIZE = 1000;
+ static final int MAX_UPDATE_TRIES = 10;
+
+ private final int maxSize;
+ private final ConcurrentMap<String, FailureCacheValue> storage;
+
+ /**
+ * Create a new failure cache with the maximum size of
+ * {@link #DEFAULT_MAX_SIZE}.
+ */
+ public DefaultFailureCache() {
+ this(DEFAULT_MAX_SIZE);
+ }
+
+ /**
+ * Creates a new failure cache with the specified maximum size.
+ * @param maxSize the maximum number of entries the cache should store
+ */
+ public DefaultFailureCache(final int maxSize) {
+ this.maxSize = maxSize;
+ this.storage = new ConcurrentHashMap<String, FailureCacheValue>();
+ }
+
+ public int getErrorCount(final String identifier) {
+ if (identifier == null) {
+ throw new IllegalArgumentException("identifier may not be null");
+ }
+ final FailureCacheValue storedErrorCode = storage.get(identifier);
+ return storedErrorCode != null ? storedErrorCode.getErrorCount() : 0;
+ }
+
+ public void resetErrorCount(final String identifier) {
+ if (identifier == null) {
+ throw new IllegalArgumentException("identifier may not be null");
+ }
+ storage.remove(identifier);
+ }
+
+ public void increaseErrorCount(final String identifier) {
+ if (identifier == null) {
+ throw new IllegalArgumentException("identifier may not be null");
+ }
+ updateValue(identifier);
+ removeOldestEntryIfMapSizeExceeded();
+ }
+
+ private void updateValue(final String identifier) {
+ /**
+ * Due to concurrency it is possible that someone else is modifying an
+ * entry before we could write back our updated value. So we keep
+ * trying until it is our turn.
+ *
+ * In case there is a lot of contention on that identifier, a thread
+ * might starve. Thus it gives up after a certain number of failed
+ * update tries.
+ */
+ for (int i = 0; i < MAX_UPDATE_TRIES; i++) {
+ final FailureCacheValue oldValue = storage.get(identifier);
+ if (oldValue == null) {
+ final FailureCacheValue newValue = new FailureCacheValue(identifier, 1);
+ if (storage.putIfAbsent(identifier, newValue) == null) {
+ return;
+ }
+ }
+ else {
+ final int errorCount = oldValue.getErrorCount();
+ if (errorCount == Integer.MAX_VALUE) {
+ return;
+ }
+ final FailureCacheValue newValue = new FailureCacheValue(identifier, errorCount + 1);
+ if (storage.replace(identifier, oldValue, newValue)) {
+ return;
+ }
+ }
+ }
+ }
+
+ private void removeOldestEntryIfMapSizeExceeded() {
+ if (storage.size() > maxSize) {
+ final FailureCacheValue valueWithOldestTimestamp = findValueWithOldestTimestamp();
+ if (valueWithOldestTimestamp != null) {
+ storage.remove(valueWithOldestTimestamp.getKey(), valueWithOldestTimestamp);
+ }
+ }
+ }
+
+ private FailureCacheValue findValueWithOldestTimestamp() {
+ long oldestTimestamp = Long.MAX_VALUE;
+ FailureCacheValue oldestValue = null;
+ for (final Map.Entry<String, FailureCacheValue> storageEntry : storage.entrySet()) {
+ final FailureCacheValue value = storageEntry.getValue();
+ final long creationTimeInNanos = value.getCreationTimeInNanos();
+ if (creationTimeInNanos < oldestTimestamp) {
+ oldestTimestamp = creationTimeInNanos;
+ oldestValue = storageEntry.getValue();
+ }
+ }
+ return oldestValue;
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/DefaultHttpCacheEntrySerializer.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/DefaultHttpCacheEntrySerializer.java
new file mode 100644
index 0000000000..0d78a2eb18
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/DefaultHttpCacheEntrySerializer.java
@@ -0,0 +1,71 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntrySerializationException;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntrySerializer;
+
+/**
+ * {@link HttpCacheEntrySerializer} implementation that uses the default (native)
+ * serialization.
+ *
+ * @see java.io.Serializable
+ *
+ * @since 4.1
+ */
+@Immutable
+public class DefaultHttpCacheEntrySerializer implements HttpCacheEntrySerializer {
+
+ public void writeTo(final HttpCacheEntry cacheEntry, final OutputStream os) throws IOException {
+ final ObjectOutputStream oos = new ObjectOutputStream(os);
+ try {
+ oos.writeObject(cacheEntry);
+ } finally {
+ oos.close();
+ }
+ }
+
+ public HttpCacheEntry readFrom(final InputStream is) throws IOException {
+ final ObjectInputStream ois = new ObjectInputStream(is);
+ try {
+ return (HttpCacheEntry) ois.readObject();
+ } catch (final ClassNotFoundException ex) {
+ throw new HttpCacheEntrySerializationException("Class not found: " + ex.getMessage(), ex);
+ } finally {
+ ois.close();
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ExponentialBackOffSchedulingStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ExponentialBackOffSchedulingStrategy.java
new file mode 100644
index 0000000000..2862055834
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ExponentialBackOffSchedulingStrategy.java
@@ -0,0 +1,174 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An implementation that backs off exponentially based on the number of
+ * consecutive failed attempts stored in the
+ * {@link AsynchronousValidationRequest}. It uses the following defaults:
+ * <pre>
+ * no delay in case it was never tried or didn't fail so far
+ * 6 secs delay for one failed attempt (= {@link #getInitialExpiryInMillis()})
+ * 60 secs delay for two failed attempts
+ * 10 mins delay for three failed attempts
+ * 100 mins delay for four failed attempts
+ * ~16 hours delay for five failed attempts
+ * 24 hours delay for six or more failed attempts (= {@link #getMaxExpiryInMillis()})
+ * </pre>
+ *
+ * The following equation is used to calculate the delay for a specific revalidation request:
+ * <pre>
+ * delay = {@link #getInitialExpiryInMillis()} * Math.pow({@link #getBackOffRate()}, {@link AsynchronousValidationRequest#getConsecutiveFailedAttempts()} - 1))
+ * </pre>
+ * The resulting delay won't exceed {@link #getMaxExpiryInMillis()}.
+ *
+ * @since 4.3
+ */
+@ThreadSafe
+public class ExponentialBackOffSchedulingStrategy implements SchedulingStrategy {
+
+ public static final long DEFAULT_BACK_OFF_RATE = 10;
+ public static final long DEFAULT_INITIAL_EXPIRY_IN_MILLIS = TimeUnit.SECONDS.toMillis(6);
+ public static final long DEFAULT_MAX_EXPIRY_IN_MILLIS = TimeUnit.SECONDS.toMillis(86400);
+
+ private final long backOffRate;
+ private final long initialExpiryInMillis;
+ private final long maxExpiryInMillis;
+
+ private final ScheduledExecutorService executor;
+
+ /**
+ * Create a new scheduling strategy using a fixed pool of worker threads.
+ * @param cacheConfig the thread pool configuration to be used; not <code>null</code>
+ * @see ch.boye.httpclientandroidlib.impl.client.cache.CacheConfig#getAsynchronousWorkersMax()
+ * @see #DEFAULT_BACK_OFF_RATE
+ * @see #DEFAULT_INITIAL_EXPIRY_IN_MILLIS
+ * @see #DEFAULT_MAX_EXPIRY_IN_MILLIS
+ */
+ public ExponentialBackOffSchedulingStrategy(final CacheConfig cacheConfig) {
+ this(cacheConfig,
+ DEFAULT_BACK_OFF_RATE,
+ DEFAULT_INITIAL_EXPIRY_IN_MILLIS,
+ DEFAULT_MAX_EXPIRY_IN_MILLIS);
+ }
+
+ /**
+ * Create a new scheduling strategy by using a fixed pool of worker threads and the
+ * given parameters to calculated the delay.
+ *
+ * @param cacheConfig the thread pool configuration to be used; not <code>null</code>
+ * @param backOffRate the back off rate to be used; not negative
+ * @param initialExpiryInMillis the initial expiry in milli seconds; not negative
+ * @param maxExpiryInMillis the upper limit of the delay in milli seconds; not negative
+ * @see ch.boye.httpclientandroidlib.impl.client.cache.CacheConfig#getAsynchronousWorkersMax()
+ * @see ExponentialBackOffSchedulingStrategy
+ */
+ public ExponentialBackOffSchedulingStrategy(
+ final CacheConfig cacheConfig,
+ final long backOffRate,
+ final long initialExpiryInMillis,
+ final long maxExpiryInMillis) {
+ this(createThreadPoolFromCacheConfig(cacheConfig),
+ backOffRate,
+ initialExpiryInMillis,
+ maxExpiryInMillis);
+ }
+
+ private static ScheduledThreadPoolExecutor createThreadPoolFromCacheConfig(
+ final CacheConfig cacheConfig) {
+ final ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(
+ cacheConfig.getAsynchronousWorkersMax());
+ scheduledThreadPoolExecutor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
+ return scheduledThreadPoolExecutor;
+ }
+
+ ExponentialBackOffSchedulingStrategy(
+ final ScheduledExecutorService executor,
+ final long backOffRate,
+ final long initialExpiryInMillis,
+ final long maxExpiryInMillis) {
+ this.executor = checkNotNull("executor", executor);
+ this.backOffRate = checkNotNegative("backOffRate", backOffRate);
+ this.initialExpiryInMillis = checkNotNegative("initialExpiryInMillis", initialExpiryInMillis);
+ this.maxExpiryInMillis = checkNotNegative("maxExpiryInMillis", maxExpiryInMillis);
+ }
+
+ public void schedule(
+ final AsynchronousValidationRequest revalidationRequest) {
+ checkNotNull("revalidationRequest", revalidationRequest);
+ final int consecutiveFailedAttempts = revalidationRequest.getConsecutiveFailedAttempts();
+ final long delayInMillis = calculateDelayInMillis(consecutiveFailedAttempts);
+ executor.schedule(revalidationRequest, delayInMillis, TimeUnit.MILLISECONDS);
+ }
+
+ public void close() {
+ executor.shutdown();
+ }
+
+ public long getBackOffRate() {
+ return backOffRate;
+ }
+
+ public long getInitialExpiryInMillis() {
+ return initialExpiryInMillis;
+ }
+
+ public long getMaxExpiryInMillis() {
+ return maxExpiryInMillis;
+ }
+
+ protected long calculateDelayInMillis(final int consecutiveFailedAttempts) {
+ if (consecutiveFailedAttempts > 0) {
+ final long delayInSeconds = (long) (initialExpiryInMillis *
+ Math.pow(backOffRate, consecutiveFailedAttempts - 1));
+ return Math.min(delayInSeconds, maxExpiryInMillis);
+ }
+ else {
+ return 0;
+ }
+ }
+
+ protected static <T> T checkNotNull(final String parameterName, final T value) {
+ if (value == null) {
+ throw new IllegalArgumentException(parameterName + " may not be null");
+ }
+ return value;
+ }
+
+ protected static long checkNotNegative(final String parameterName, final long value) {
+ if (value < 0) {
+ throw new IllegalArgumentException(parameterName + " may not be negative");
+ }
+ return value;
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/FailureCache.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/FailureCache.java
new file mode 100644
index 0000000000..a30fc54d99
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/FailureCache.java
@@ -0,0 +1,57 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+/**
+ * Increase and reset the number of errors associated with a specific
+ * identifier.
+ *
+ * @since 4.3
+ */
+public interface FailureCache {
+
+ /**
+ * Get the current error count.
+ * @param identifier the identifier for which the error count is requested
+ * @return the currently known error count or zero if there is no record
+ */
+ int getErrorCount(String identifier);
+
+ /**
+ * Reset the error count back to zero.
+ * @param identifier the identifier for which the error count should be
+ * reset
+ */
+ void resetErrorCount(String identifier);
+
+ /**
+ * Increases the error count by one.
+ * @param identifier the identifier for which the error count should be
+ * increased
+ */
+ void increaseErrorCount(String identifier);
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/FailureCacheValue.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/FailureCacheValue.java
new file mode 100644
index 0000000000..48bbf55fc4
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/FailureCacheValue.java
@@ -0,0 +1,67 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * The error count with a creation timestamp and its associated key.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class FailureCacheValue {
+
+ private final long creationTimeInNanos;
+ private final String key;
+ private final int errorCount;
+
+ public FailureCacheValue(final String key, final int errorCount) {
+ this.creationTimeInNanos = System.nanoTime();
+ this.key = key;
+ this.errorCount = errorCount;
+ }
+
+ public long getCreationTimeInNanos() {
+ return creationTimeInNanos;
+ }
+
+ public String getKey()
+ {
+ return key;
+ }
+
+ public int getErrorCount() {
+ return errorCount;
+ }
+
+ @Override
+ public String toString() {
+ return "[entry creationTimeInNanos=" + creationTimeInNanos + "; " +
+ "key=" + key + "; errorCount=" + errorCount + ']';
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/FileResource.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/FileResource.java
new file mode 100644
index 0000000000..934ecd13d0
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/FileResource.java
@@ -0,0 +1,77 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.client.cache.Resource;
+
+/**
+ * Cache resource backed by a file.
+ *
+ * @since 4.1
+ */
+@ThreadSafe
+public class FileResource implements Resource {
+
+ private static final long serialVersionUID = 4132244415919043397L;
+
+ private final File file;
+
+ private volatile boolean disposed;
+
+ public FileResource(final File file) {
+ super();
+ this.file = file;
+ this.disposed = false;
+ }
+
+ synchronized File getFile() {
+ return this.file;
+ }
+
+ public synchronized InputStream getInputStream() throws IOException {
+ return new FileInputStream(this.file);
+ }
+
+ public synchronized long length() {
+ return this.file.length();
+ }
+
+ public synchronized void dispose() {
+ if (this.disposed) {
+ return;
+ }
+ this.disposed = true;
+ this.file.delete();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/FileResourceFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/FileResourceFactory.java
new file mode 100644
index 0000000000..8522c54b50
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/FileResourceFactory.java
@@ -0,0 +1,111 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.cache.InputLimit;
+import ch.boye.httpclientandroidlib.client.cache.Resource;
+import ch.boye.httpclientandroidlib.client.cache.ResourceFactory;
+
+/**
+ * Generates {@link Resource} instances whose body is stored in a temporary file.
+ *
+ * @since 4.1
+ */
+@Immutable
+public class FileResourceFactory implements ResourceFactory {
+
+ private final File cacheDir;
+ private final BasicIdGenerator idgen;
+
+ public FileResourceFactory(final File cacheDir) {
+ super();
+ this.cacheDir = cacheDir;
+ this.idgen = new BasicIdGenerator();
+ }
+
+ private File generateUniqueCacheFile(final String requestId) {
+ final StringBuilder buffer = new StringBuilder();
+ this.idgen.generate(buffer);
+ buffer.append('.');
+ final int len = Math.min(requestId.length(), 100);
+ for (int i = 0; i < len; i++) {
+ final char ch = requestId.charAt(i);
+ if (Character.isLetterOrDigit(ch) || ch == '.') {
+ buffer.append(ch);
+ } else {
+ buffer.append('-');
+ }
+ }
+ return new File(this.cacheDir, buffer.toString());
+ }
+
+ public Resource generate(
+ final String requestId,
+ final InputStream instream,
+ final InputLimit limit) throws IOException {
+ final File file = generateUniqueCacheFile(requestId);
+ final FileOutputStream outstream = new FileOutputStream(file);
+ try {
+ final byte[] buf = new byte[2048];
+ long total = 0;
+ int l;
+ while ((l = instream.read(buf)) != -1) {
+ outstream.write(buf, 0, l);
+ total += l;
+ if (limit != null && total > limit.getValue()) {
+ limit.reached();
+ break;
+ }
+ }
+ } finally {
+ outstream.close();
+ }
+ return new FileResource(file);
+ }
+
+ public Resource copy(
+ final String requestId,
+ final Resource resource) throws IOException {
+ final File file = generateUniqueCacheFile(requestId);
+
+ if (resource instanceof FileResource) {
+ final File src = ((FileResource) resource).getFile();
+ IOUtils.copyFile(src, file);
+ } else {
+ final FileOutputStream out = new FileOutputStream(file);
+ IOUtils.copyAndClose(resource.getInputStream(), out);
+ }
+ return new FileResource(file);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/HeapResource.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/HeapResource.java
new file mode 100644
index 0000000000..e4b353a3c9
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/HeapResource.java
@@ -0,0 +1,67 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.cache.Resource;
+
+/**
+ * Cache resource backed by a byte array on the heap.
+ *
+ * @since 4.1
+ */
+@Immutable
+public class HeapResource implements Resource {
+
+ private static final long serialVersionUID = -2078599905620463394L;
+
+ private final byte[] b;
+
+ public HeapResource(final byte[] b) {
+ super();
+ this.b = b;
+ }
+
+ byte[] getByteArray() {
+ return this.b;
+ }
+
+ public InputStream getInputStream() {
+ return new ByteArrayInputStream(this.b);
+ }
+
+ public long length() {
+ return this.b.length;
+ }
+
+ public void dispose() {
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/HeapResourceFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/HeapResourceFactory.java
new file mode 100644
index 0000000000..f45cc09656
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/HeapResourceFactory.java
@@ -0,0 +1,83 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.cache.InputLimit;
+import ch.boye.httpclientandroidlib.client.cache.Resource;
+import ch.boye.httpclientandroidlib.client.cache.ResourceFactory;
+
+/**
+ * Generates {@link Resource} instances stored entirely in heap.
+ *
+ * @since 4.1
+ */
+@Immutable
+public class HeapResourceFactory implements ResourceFactory {
+
+ public Resource generate(
+ final String requestId,
+ final InputStream instream,
+ final InputLimit limit) throws IOException {
+ final ByteArrayOutputStream outstream = new ByteArrayOutputStream();
+ final byte[] buf = new byte[2048];
+ long total = 0;
+ int l;
+ while ((l = instream.read(buf)) != -1) {
+ outstream.write(buf, 0, l);
+ total += l;
+ if (limit != null && total > limit.getValue()) {
+ limit.reached();
+ break;
+ }
+ }
+ return createResource(outstream.toByteArray());
+ }
+
+ public Resource copy(
+ final String requestId,
+ final Resource resource) throws IOException {
+ byte[] body;
+ if (resource instanceof HeapResource) {
+ body = ((HeapResource) resource).getByteArray();
+ } else {
+ final ByteArrayOutputStream outstream = new ByteArrayOutputStream();
+ IOUtils.copyAndClose(resource.getInputStream(), outstream);
+ body = outstream.toByteArray();
+ }
+ return createResource(body);
+ }
+
+ Resource createResource(final byte[] buf) {
+ return new HeapResource(buf);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/HttpCache.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/HttpCache.java
new file mode 100644
index 0000000000..48037b5f53
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/HttpCache.java
@@ -0,0 +1,166 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.Map;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
+import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
+
+/**
+ * @since 4.1
+ */
+interface HttpCache {
+
+ /**
+ * Clear all matching {@link HttpCacheEntry}s.
+ * @param host
+ * @param request
+ * @throws IOException
+ */
+ void flushCacheEntriesFor(HttpHost host, HttpRequest request)
+ throws IOException;
+
+ /**
+ * Clear invalidated matching {@link HttpCacheEntry}s
+ * @param host
+ * @param request
+ * @throws IOException
+ */
+ void flushInvalidatedCacheEntriesFor(HttpHost host, HttpRequest request)
+ throws IOException;
+
+ /** Clear any entries that may be invalidated by the given response to
+ * a particular request.
+ * @param host
+ * @param request
+ * @param response
+ */
+ void flushInvalidatedCacheEntriesFor(HttpHost host, HttpRequest request,
+ HttpResponse response);
+
+ /**
+ * Retrieve matching {@link HttpCacheEntry} from the cache if it exists
+ * @param host
+ * @param request
+ * @return the matching {@link HttpCacheEntry} or {@code null}
+ * @throws IOException
+ */
+ HttpCacheEntry getCacheEntry(HttpHost host, HttpRequest request)
+ throws IOException;
+
+ /**
+ * Retrieve all variants from the cache, if there are no variants then an empty
+ * {@link Map} is returned
+ * @param host
+ * @param request
+ * @return a <code>Map</code> mapping Etags to variant cache entries
+ * @throws IOException
+ */
+ Map<String,Variant> getVariantCacheEntriesWithEtags(HttpHost host, HttpRequest request)
+ throws IOException;
+
+ /**
+ * Store a {@link HttpResponse} in the cache if possible, and return
+ * @param host
+ * @param request
+ * @param originResponse
+ * @param requestSent
+ * @param responseReceived
+ * @return the {@link HttpResponse}
+ * @throws IOException
+ */
+ HttpResponse cacheAndReturnResponse(
+ HttpHost host, HttpRequest request, HttpResponse originResponse,
+ Date requestSent, Date responseReceived)
+ throws IOException;
+
+ /**
+ * Store a {@link HttpResponse} in the cache if possible, and return
+ * @param host
+ * @param request
+ * @param originResponse
+ * @param requestSent
+ * @param responseReceived
+ * @return the {@link HttpResponse}
+ * @throws IOException
+ */
+ CloseableHttpResponse cacheAndReturnResponse(HttpHost host,
+ HttpRequest request, CloseableHttpResponse originResponse,
+ Date requestSent, Date responseReceived)
+ throws IOException;
+
+ /**
+ * Update a {@link HttpCacheEntry} using a 304 {@link HttpResponse}.
+ * @param target
+ * @param request
+ * @param stale
+ * @param originResponse
+ * @param requestSent
+ * @param responseReceived
+ * @return the updated {@link HttpCacheEntry}
+ * @throws IOException
+ */
+ HttpCacheEntry updateCacheEntry(
+ HttpHost target, HttpRequest request, HttpCacheEntry stale, HttpResponse originResponse,
+ Date requestSent, Date responseReceived)
+ throws IOException;
+
+ /**
+ * Update a specific {@link HttpCacheEntry} representing a cached variant
+ * using a 304 {@link HttpResponse}.
+ * @param target host for client request
+ * @param request actual request from upstream client
+ * @param stale current variant cache entry
+ * @param originResponse 304 response received from origin
+ * @param requestSent when the validating request was sent
+ * @param responseReceived when the validating response was received
+ * @param cacheKey where in the cache this entry is currently stored
+ * @return the updated {@link HttpCacheEntry}
+ * @throws IOException
+ */
+ HttpCacheEntry updateVariantCacheEntry(HttpHost target, HttpRequest request,
+ HttpCacheEntry stale, HttpResponse originResponse, Date requestSent,
+ Date responseReceived, String cacheKey)
+ throws IOException;
+
+ /**
+ * Specifies cache should reuse the given cached variant to satisfy
+ * requests whose varying headers match those of the given client request.
+ * @param target host of the upstream client request
+ * @param req request sent by upstream client
+ * @param variant variant cache entry to reuse
+ * @throws IOException may be thrown during cache update
+ */
+ void reuseVariantEntryFor(HttpHost target, final HttpRequest req,
+ final Variant variant) throws IOException;
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/IOUtils.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/IOUtils.java
new file mode 100644
index 0000000000..aaf41b2c9d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/IOUtils.java
@@ -0,0 +1,109 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.nio.channels.FileChannel;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+@Immutable
+class IOUtils {
+
+ static void consume(final HttpEntity entity) throws IOException {
+ if (entity == null) {
+ return;
+ }
+ if (entity.isStreaming()) {
+ final InputStream instream = entity.getContent();
+ if (instream != null) {
+ instream.close();
+ }
+ }
+ }
+
+ static void copy(final InputStream in, final OutputStream out) throws IOException {
+ final byte[] buf = new byte[2048];
+ int len;
+ while ((len = in.read(buf)) != -1) {
+ out.write(buf, 0, len);
+ }
+ }
+
+ static void closeSilently(final Closeable closable) {
+ try {
+ closable.close();
+ } catch (final IOException ignore) {
+ }
+ }
+
+ static void copyAndClose(final InputStream in, final OutputStream out) throws IOException {
+ try {
+ copy(in, out);
+ in.close();
+ out.close();
+ } catch (final IOException ex) {
+ closeSilently(in);
+ closeSilently(out);
+ // Propagate the original exception
+ throw ex;
+ }
+ }
+
+ static void copyFile(final File in, final File out) throws IOException {
+ final RandomAccessFile f1 = new RandomAccessFile(in, "r");
+ final RandomAccessFile f2 = new RandomAccessFile(out, "rw");
+ try {
+ final FileChannel c1 = f1.getChannel();
+ final FileChannel c2 = f2.getChannel();
+ try {
+ c1.transferTo(0, f1.length(), c2);
+ c1.close();
+ c2.close();
+ } catch (final IOException ex) {
+ closeSilently(c1);
+ closeSilently(c2);
+ // Propagate the original exception
+ throw ex;
+ }
+ f1.close();
+ f2.close();
+ } catch (final IOException ex) {
+ closeSilently(f1);
+ closeSilently(f2);
+ // Propagate the original exception
+ throw ex;
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ImmediateSchedulingStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ImmediateSchedulingStrategy.java
new file mode 100644
index 0000000000..71d6338251
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ImmediateSchedulingStrategy.java
@@ -0,0 +1,88 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Immediately schedules any incoming validation request. Relies on
+ * {@link CacheConfig} to configure the used {@link java.util.concurrent.ThreadPoolExecutor}.
+ *
+ * @since 4.3
+ */
+@ThreadSafe
+public class ImmediateSchedulingStrategy implements SchedulingStrategy {
+
+ private final ExecutorService executor;
+
+ /**
+ * Uses a {@link java.util.concurrent.ThreadPoolExecutor} which is configured according to the
+ * given {@link CacheConfig}.
+ * @param cacheConfig specifies thread pool settings. See
+ * {@link CacheConfig#getAsynchronousWorkersMax()},
+ * {@link CacheConfig#getAsynchronousWorkersCore()},
+ * {@link CacheConfig#getAsynchronousWorkerIdleLifetimeSecs()},
+ * and {@link CacheConfig#getRevalidationQueueSize()}.
+ */
+ public ImmediateSchedulingStrategy(final CacheConfig cacheConfig) {
+ this(new ThreadPoolExecutor(
+ cacheConfig.getAsynchronousWorkersCore(),
+ cacheConfig.getAsynchronousWorkersMax(),
+ cacheConfig.getAsynchronousWorkerIdleLifetimeSecs(),
+ TimeUnit.SECONDS,
+ new ArrayBlockingQueue<Runnable>(cacheConfig.getRevalidationQueueSize()))
+ );
+ }
+
+ ImmediateSchedulingStrategy(final ExecutorService executor) {
+ this.executor = executor;
+ }
+
+ public void schedule(final AsynchronousValidationRequest revalidationRequest) {
+ if (revalidationRequest == null) {
+ throw new IllegalArgumentException("AsynchronousValidationRequest may not be null");
+ }
+
+ executor.execute(revalidationRequest);
+ }
+
+ public void close() {
+ executor.shutdown();
+ }
+
+ /**
+ * Visible for testing.
+ */
+ void awaitTermination(final long timeout, final TimeUnit unit) throws InterruptedException {
+ executor.awaitTermination(timeout, unit);
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ManagedHttpCacheStorage.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ManagedHttpCacheStorage.java
new file mode 100644
index 0000000000..3a2589cc32
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ManagedHttpCacheStorage.java
@@ -0,0 +1,163 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.ref.ReferenceQueue;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheStorage;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheUpdateCallback;
+import ch.boye.httpclientandroidlib.client.cache.Resource;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * {@link HttpCacheStorage} implementation capable of deallocating resources associated with
+ * the cache entries. This cache keeps track of cache entries using
+ * {@link java.lang.ref.PhantomReference} and maintains a collection of all resources that
+ * are no longer in use. The cache, however, does not automatically deallocates associated
+ * resources by invoking {@link Resource#dispose()} method. The consumer MUST periodically
+ * call {@link #cleanResources()} method to trigger resource deallocation. The cache can be
+ * permanently shut down using {@link #shutdown()} method. All resources associated with
+ * the entries used by the cache will be deallocated.
+ *
+ * This {@link HttpCacheStorage} implementation is intended for use with {@link FileResource}
+ * and similar.
+ *
+ * @since 4.1
+ */
+@ThreadSafe
+public class ManagedHttpCacheStorage implements HttpCacheStorage, Closeable {
+
+ private final CacheMap entries;
+ private final ReferenceQueue<HttpCacheEntry> morque;
+ private final Set<ResourceReference> resources;
+ private final AtomicBoolean active;
+
+ public ManagedHttpCacheStorage(final CacheConfig config) {
+ super();
+ this.entries = new CacheMap(config.getMaxCacheEntries());
+ this.morque = new ReferenceQueue<HttpCacheEntry>();
+ this.resources = new HashSet<ResourceReference>();
+ this.active = new AtomicBoolean(true);
+ }
+
+ private void ensureValidState() throws IllegalStateException {
+ if (!this.active.get()) {
+ throw new IllegalStateException("Cache has been shut down");
+ }
+ }
+
+ private void keepResourceReference(final HttpCacheEntry entry) {
+ final Resource resource = entry.getResource();
+ if (resource != null) {
+ // Must deallocate the resource when the entry is no longer in used
+ final ResourceReference ref = new ResourceReference(entry, this.morque);
+ this.resources.add(ref);
+ }
+ }
+
+ public void putEntry(final String url, final HttpCacheEntry entry) throws IOException {
+ Args.notNull(url, "URL");
+ Args.notNull(entry, "Cache entry");
+ ensureValidState();
+ synchronized (this) {
+ this.entries.put(url, entry);
+ keepResourceReference(entry);
+ }
+ }
+
+ public HttpCacheEntry getEntry(final String url) throws IOException {
+ Args.notNull(url, "URL");
+ ensureValidState();
+ synchronized (this) {
+ return this.entries.get(url);
+ }
+ }
+
+ public void removeEntry(final String url) throws IOException {
+ Args.notNull(url, "URL");
+ ensureValidState();
+ synchronized (this) {
+ // Cannot deallocate the associated resources immediately as the
+ // cache entry may still be in use
+ this.entries.remove(url);
+ }
+ }
+
+ public void updateEntry(
+ final String url,
+ final HttpCacheUpdateCallback callback) throws IOException {
+ Args.notNull(url, "URL");
+ Args.notNull(callback, "Callback");
+ ensureValidState();
+ synchronized (this) {
+ final HttpCacheEntry existing = this.entries.get(url);
+ final HttpCacheEntry updated = callback.update(existing);
+ this.entries.put(url, updated);
+ if (existing != updated) {
+ keepResourceReference(updated);
+ }
+ }
+ }
+
+ public void cleanResources() {
+ if (this.active.get()) {
+ ResourceReference ref;
+ while ((ref = (ResourceReference) this.morque.poll()) != null) {
+ synchronized (this) {
+ this.resources.remove(ref);
+ }
+ ref.getResource().dispose();
+ }
+ }
+ }
+
+ public void shutdown() {
+ if (this.active.compareAndSet(true, false)) {
+ synchronized (this) {
+ this.entries.clear();
+ for (final ResourceReference ref: this.resources) {
+ ref.getResource().dispose();
+ }
+ this.resources.clear();
+ while (this.morque.poll() != null) {
+ }
+ }
+ }
+ }
+
+ public void close() {
+ shutdown();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/OptionsHttp11Response.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/OptionsHttp11Response.java
new file mode 100644
index 0000000000..c2e07b52df
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/OptionsHttp11Response.java
@@ -0,0 +1,182 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.util.Locale;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderIterator;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.StatusLine;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.message.AbstractHttpMessage;
+import ch.boye.httpclientandroidlib.message.BasicStatusLine;
+import ch.boye.httpclientandroidlib.params.BasicHttpParams;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+/**
+ * @since 4.1
+ */
+@SuppressWarnings("deprecation")
+@Immutable
+final class OptionsHttp11Response extends AbstractHttpMessage implements HttpResponse {
+
+ private final StatusLine statusLine = new BasicStatusLine(HttpVersion.HTTP_1_1,
+ HttpStatus.SC_NOT_IMPLEMENTED, "");
+ private final ProtocolVersion version = HttpVersion.HTTP_1_1;
+
+ public StatusLine getStatusLine() {
+ return statusLine;
+ }
+
+ public void setStatusLine(final StatusLine statusline) {
+ // No-op on purpose, this class is not going to be doing any work.
+ }
+
+ public void setStatusLine(final ProtocolVersion ver, final int code) {
+ // No-op on purpose, this class is not going to be doing any work.
+ }
+
+ public void setStatusLine(final ProtocolVersion ver, final int code, final String reason) {
+ // No-op on purpose, this class is not going to be doing any work.
+ }
+
+ public void setStatusCode(final int code) throws IllegalStateException {
+ // No-op on purpose, this class is not going to be doing any work.
+ }
+
+ public void setReasonPhrase(final String reason) throws IllegalStateException {
+ // No-op on purpose, this class is not going to be doing any work.
+ }
+
+ public HttpEntity getEntity() {
+ return null;
+ }
+
+ public void setEntity(final HttpEntity entity) {
+ // No-op on purpose, this class is not going to be doing any work.
+ }
+
+ public Locale getLocale() {
+ return null;
+ }
+
+ public void setLocale(final Locale loc) {
+ // No-op on purpose, this class is not going to be doing any work.
+ }
+
+ public ProtocolVersion getProtocolVersion() {
+ return version;
+ }
+
+ @Override
+ public boolean containsHeader(final String name) {
+ return this.headergroup.containsHeader(name);
+ }
+
+ @Override
+ public Header[] getHeaders(final String name) {
+ return this.headergroup.getHeaders(name);
+ }
+
+ @Override
+ public Header getFirstHeader(final String name) {
+ return this.headergroup.getFirstHeader(name);
+ }
+
+ @Override
+ public Header getLastHeader(final String name) {
+ return this.headergroup.getLastHeader(name);
+ }
+
+ @Override
+ public Header[] getAllHeaders() {
+ return this.headergroup.getAllHeaders();
+ }
+
+ @Override
+ public void addHeader(final Header header) {
+ // No-op on purpose, this class is not going to be doing any work.
+ }
+
+ @Override
+ public void addHeader(final String name, final String value) {
+ // No-op on purpose, this class is not going to be doing any work.
+ }
+
+ @Override
+ public void setHeader(final Header header) {
+ // No-op on purpose, this class is not going to be doing any work.
+ }
+
+ @Override
+ public void setHeader(final String name, final String value) {
+ // No-op on purpose, this class is not going to be doing any work.
+ }
+
+ @Override
+ public void setHeaders(final Header[] headers) {
+ // No-op on purpose, this class is not going to be doing any work.
+ }
+
+ @Override
+ public void removeHeader(final Header header) {
+ // No-op on purpose, this class is not going to be doing any work.
+ }
+
+ @Override
+ public void removeHeaders(final String name) {
+ // No-op on purpose, this class is not going to be doing any work.
+ }
+
+ @Override
+ public HeaderIterator headerIterator() {
+ return this.headergroup.iterator();
+ }
+
+ @Override
+ public HeaderIterator headerIterator(final String name) {
+ return this.headergroup.iterator(name);
+ }
+
+ @Override
+ public HttpParams getParams() {
+ if (this.params == null) {
+ this.params = new BasicHttpParams();
+ }
+ return this.params;
+ }
+
+ @Override
+ public void setParams(final HttpParams params) {
+ // No-op on purpose, this class is not going to be doing any work.
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/Proxies.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/Proxies.java
new file mode 100644
index 0000000000..4499d82883
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/Proxies.java
@@ -0,0 +1,56 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.lang.reflect.Proxy;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Proxies for HTTP message objects.
+ *
+ * @since 4.3
+ */
+@NotThreadSafe
+class Proxies {
+
+ public static CloseableHttpResponse enhanceResponse(final HttpResponse original) {
+ Args.notNull(original, "HTTP response");
+ if (original instanceof CloseableHttpResponse) {
+ return (CloseableHttpResponse) original;
+ } else {
+ return (CloseableHttpResponse) Proxy.newProxyInstance(
+ ResponseProxyHandler.class.getClassLoader(),
+ new Class<?>[] { CloseableHttpResponse.class },
+ new ResponseProxyHandler(original));
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/RequestProtocolCompliance.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/RequestProtocolCompliance.java
new file mode 100644
index 0000000000..ac7ab439ca
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/RequestProtocolCompliance.java
@@ -0,0 +1,376 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestWrapper;
+import ch.boye.httpclientandroidlib.entity.AbstractHttpEntity;
+import ch.boye.httpclientandroidlib.entity.ContentType;
+import ch.boye.httpclientandroidlib.message.BasicHeader;
+import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
+import ch.boye.httpclientandroidlib.message.BasicStatusLine;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+
+/**
+ * @since 4.1
+ */
+@Immutable
+class RequestProtocolCompliance {
+ private final boolean weakETagOnPutDeleteAllowed;
+
+ public RequestProtocolCompliance() {
+ super();
+ this.weakETagOnPutDeleteAllowed = false;
+ }
+
+ public RequestProtocolCompliance(final boolean weakETagOnPutDeleteAllowed) {
+ super();
+ this.weakETagOnPutDeleteAllowed = weakETagOnPutDeleteAllowed;
+ }
+
+ private static final List<String> disallowedWithNoCache =
+ Arrays.asList(HeaderConstants.CACHE_CONTROL_MIN_FRESH, HeaderConstants.CACHE_CONTROL_MAX_STALE, HeaderConstants.CACHE_CONTROL_MAX_AGE);
+
+ /**
+ * Test to see if the {@link HttpRequest} is HTTP1.1 compliant or not
+ * and if not, we can not continue.
+ *
+ * @param request the HttpRequest Object
+ * @return list of {@link RequestProtocolError}
+ */
+ public List<RequestProtocolError> requestIsFatallyNonCompliant(final HttpRequest request) {
+ final List<RequestProtocolError> theErrors = new ArrayList<RequestProtocolError>();
+
+ RequestProtocolError anError = requestHasWeakETagAndRange(request);
+ if (anError != null) {
+ theErrors.add(anError);
+ }
+
+ if (!weakETagOnPutDeleteAllowed) {
+ anError = requestHasWeekETagForPUTOrDELETEIfMatch(request);
+ if (anError != null) {
+ theErrors.add(anError);
+ }
+ }
+
+ anError = requestContainsNoCacheDirectiveWithFieldName(request);
+ if (anError != null) {
+ theErrors.add(anError);
+ }
+
+ return theErrors;
+ }
+
+ /**
+ * If the {@link HttpRequest} is non-compliant but 'fixable' we go ahead and
+ * fix the request here.
+ *
+ * @param request the request to check for compliance
+ * @throws ClientProtocolException when we have trouble making the request compliant
+ */
+ public void makeRequestCompliant(final HttpRequestWrapper request)
+ throws ClientProtocolException {
+
+ if (requestMustNotHaveEntity(request)) {
+ ((HttpEntityEnclosingRequest) request).setEntity(null);
+ }
+
+ verifyRequestWithExpectContinueFlagHas100continueHeader(request);
+ verifyOPTIONSRequestWithBodyHasContentType(request);
+ decrementOPTIONSMaxForwardsIfGreaterThen0(request);
+ stripOtherFreshnessDirectivesWithNoCache(request);
+
+ if (requestVersionIsTooLow(request)
+ || requestMinorVersionIsTooHighMajorVersionsMatch(request)) {
+ request.setProtocolVersion(HttpVersion.HTTP_1_1);
+ }
+ }
+
+ private void stripOtherFreshnessDirectivesWithNoCache(final HttpRequest request) {
+ final List<HeaderElement> outElts = new ArrayList<HeaderElement>();
+ boolean shouldStrip = false;
+ for(final Header h : request.getHeaders(HeaderConstants.CACHE_CONTROL)) {
+ for(final HeaderElement elt : h.getElements()) {
+ if (!disallowedWithNoCache.contains(elt.getName())) {
+ outElts.add(elt);
+ }
+ if (HeaderConstants.CACHE_CONTROL_NO_CACHE.equals(elt.getName())) {
+ shouldStrip = true;
+ }
+ }
+ }
+ if (!shouldStrip) {
+ return;
+ }
+ request.removeHeaders(HeaderConstants.CACHE_CONTROL);
+ request.setHeader(HeaderConstants.CACHE_CONTROL, buildHeaderFromElements(outElts));
+ }
+
+ private String buildHeaderFromElements(final List<HeaderElement> outElts) {
+ final StringBuilder newHdr = new StringBuilder("");
+ boolean first = true;
+ for(final HeaderElement elt : outElts) {
+ if (!first) {
+ newHdr.append(",");
+ } else {
+ first = false;
+ }
+ newHdr.append(elt.toString());
+ }
+ return newHdr.toString();
+ }
+
+ private boolean requestMustNotHaveEntity(final HttpRequest request) {
+ return HeaderConstants.TRACE_METHOD.equals(request.getRequestLine().getMethod())
+ && request instanceof HttpEntityEnclosingRequest;
+ }
+
+ private void decrementOPTIONSMaxForwardsIfGreaterThen0(final HttpRequest request) {
+ if (!HeaderConstants.OPTIONS_METHOD.equals(request.getRequestLine().getMethod())) {
+ return;
+ }
+
+ final Header maxForwards = request.getFirstHeader(HeaderConstants.MAX_FORWARDS);
+ if (maxForwards == null) {
+ return;
+ }
+
+ request.removeHeaders(HeaderConstants.MAX_FORWARDS);
+ final int currentMaxForwards = Integer.parseInt(maxForwards.getValue());
+
+ request.setHeader(HeaderConstants.MAX_FORWARDS, Integer.toString(currentMaxForwards - 1));
+ }
+
+ private void verifyOPTIONSRequestWithBodyHasContentType(final HttpRequest request) {
+ if (!HeaderConstants.OPTIONS_METHOD.equals(request.getRequestLine().getMethod())) {
+ return;
+ }
+
+ if (!(request instanceof HttpEntityEnclosingRequest)) {
+ return;
+ }
+
+ addContentTypeHeaderIfMissing((HttpEntityEnclosingRequest) request);
+ }
+
+ private void addContentTypeHeaderIfMissing(final HttpEntityEnclosingRequest request) {
+ if (request.getEntity().getContentType() == null) {
+ ((AbstractHttpEntity) request.getEntity()).setContentType(
+ ContentType.APPLICATION_OCTET_STREAM.getMimeType());
+ }
+ }
+
+ private void verifyRequestWithExpectContinueFlagHas100continueHeader(final HttpRequest request) {
+ if (request instanceof HttpEntityEnclosingRequest) {
+
+ if (((HttpEntityEnclosingRequest) request).expectContinue()
+ && ((HttpEntityEnclosingRequest) request).getEntity() != null) {
+ add100ContinueHeaderIfMissing(request);
+ } else {
+ remove100ContinueHeaderIfExists(request);
+ }
+ } else {
+ remove100ContinueHeaderIfExists(request);
+ }
+ }
+
+ private void remove100ContinueHeaderIfExists(final HttpRequest request) {
+ boolean hasHeader = false;
+
+ final Header[] expectHeaders = request.getHeaders(HTTP.EXPECT_DIRECTIVE);
+ List<HeaderElement> expectElementsThatAreNot100Continue = new ArrayList<HeaderElement>();
+
+ for (final Header h : expectHeaders) {
+ for (final HeaderElement elt : h.getElements()) {
+ if (!(HTTP.EXPECT_CONTINUE.equalsIgnoreCase(elt.getName()))) {
+ expectElementsThatAreNot100Continue.add(elt);
+ } else {
+ hasHeader = true;
+ }
+ }
+
+ if (hasHeader) {
+ request.removeHeader(h);
+ for (final HeaderElement elt : expectElementsThatAreNot100Continue) {
+ final BasicHeader newHeader = new BasicHeader(HTTP.EXPECT_DIRECTIVE, elt.getName());
+ request.addHeader(newHeader);
+ }
+ return;
+ } else {
+ expectElementsThatAreNot100Continue = new ArrayList<HeaderElement>();
+ }
+ }
+ }
+
+ private void add100ContinueHeaderIfMissing(final HttpRequest request) {
+ boolean hasHeader = false;
+
+ for (final Header h : request.getHeaders(HTTP.EXPECT_DIRECTIVE)) {
+ for (final HeaderElement elt : h.getElements()) {
+ if (HTTP.EXPECT_CONTINUE.equalsIgnoreCase(elt.getName())) {
+ hasHeader = true;
+ }
+ }
+ }
+
+ if (!hasHeader) {
+ request.addHeader(HTTP.EXPECT_DIRECTIVE, HTTP.EXPECT_CONTINUE);
+ }
+ }
+
+ protected boolean requestMinorVersionIsTooHighMajorVersionsMatch(final HttpRequest request) {
+ final ProtocolVersion requestProtocol = request.getProtocolVersion();
+ if (requestProtocol.getMajor() != HttpVersion.HTTP_1_1.getMajor()) {
+ return false;
+ }
+
+ if (requestProtocol.getMinor() > HttpVersion.HTTP_1_1.getMinor()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ protected boolean requestVersionIsTooLow(final HttpRequest request) {
+ return request.getProtocolVersion().compareToVersion(HttpVersion.HTTP_1_1) < 0;
+ }
+
+ /**
+ * Extract error information about the {@link HttpRequest} telling the 'caller'
+ * that a problem occured.
+ *
+ * @param errorCheck What type of error should I get
+ * @return The {@link HttpResponse} that is the error generated
+ */
+ public HttpResponse getErrorForRequest(final RequestProtocolError errorCheck) {
+ switch (errorCheck) {
+ case BODY_BUT_NO_LENGTH_ERROR:
+ return new BasicHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1,
+ HttpStatus.SC_LENGTH_REQUIRED, ""));
+
+ case WEAK_ETAG_AND_RANGE_ERROR:
+ return new BasicHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1,
+ HttpStatus.SC_BAD_REQUEST, "Weak eTag not compatible with byte range"));
+
+ case WEAK_ETAG_ON_PUTDELETE_METHOD_ERROR:
+ return new BasicHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1,
+ HttpStatus.SC_BAD_REQUEST,
+ "Weak eTag not compatible with PUT or DELETE requests"));
+
+ case NO_CACHE_DIRECTIVE_WITH_FIELD_NAME:
+ return new BasicHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1,
+ HttpStatus.SC_BAD_REQUEST,
+ "No-Cache directive MUST NOT include a field name"));
+
+ default:
+ throw new IllegalStateException(
+ "The request was compliant, therefore no error can be generated for it.");
+
+ }
+ }
+
+ private RequestProtocolError requestHasWeakETagAndRange(final HttpRequest request) {
+ // TODO: Should these be looking at all the headers marked as Range?
+ final String method = request.getRequestLine().getMethod();
+ if (!(HeaderConstants.GET_METHOD.equals(method))) {
+ return null;
+ }
+
+ final Header range = request.getFirstHeader(HeaderConstants.RANGE);
+ if (range == null) {
+ return null;
+ }
+
+ final Header ifRange = request.getFirstHeader(HeaderConstants.IF_RANGE);
+ if (ifRange == null) {
+ return null;
+ }
+
+ final String val = ifRange.getValue();
+ if (val.startsWith("W/")) {
+ return RequestProtocolError.WEAK_ETAG_AND_RANGE_ERROR;
+ }
+
+ return null;
+ }
+
+ private RequestProtocolError requestHasWeekETagForPUTOrDELETEIfMatch(final HttpRequest request) {
+ // TODO: Should these be looking at all the headers marked as If-Match/If-None-Match?
+
+ final String method = request.getRequestLine().getMethod();
+ if (!(HeaderConstants.PUT_METHOD.equals(method) || HeaderConstants.DELETE_METHOD
+ .equals(method))) {
+ return null;
+ }
+
+ final Header ifMatch = request.getFirstHeader(HeaderConstants.IF_MATCH);
+ if (ifMatch != null) {
+ final String val = ifMatch.getValue();
+ if (val.startsWith("W/")) {
+ return RequestProtocolError.WEAK_ETAG_ON_PUTDELETE_METHOD_ERROR;
+ }
+ } else {
+ final Header ifNoneMatch = request.getFirstHeader(HeaderConstants.IF_NONE_MATCH);
+ if (ifNoneMatch == null) {
+ return null;
+ }
+
+ final String val2 = ifNoneMatch.getValue();
+ if (val2.startsWith("W/")) {
+ return RequestProtocolError.WEAK_ETAG_ON_PUTDELETE_METHOD_ERROR;
+ }
+ }
+
+ return null;
+ }
+
+ private RequestProtocolError requestContainsNoCacheDirectiveWithFieldName(final HttpRequest request) {
+ for(final Header h : request.getHeaders(HeaderConstants.CACHE_CONTROL)) {
+ for(final HeaderElement elt : h.getElements()) {
+ if (HeaderConstants.CACHE_CONTROL_NO_CACHE.equalsIgnoreCase(elt.getName())
+ && elt.getValue() != null) {
+ return RequestProtocolError.NO_CACHE_DIRECTIVE_WITH_FIELD_NAME;
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/RequestProtocolError.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/RequestProtocolError.java
new file mode 100644
index 0000000000..1cc5668c7f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/RequestProtocolError.java
@@ -0,0 +1,40 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+/**
+ * @since 4.1
+ */
+enum RequestProtocolError {
+
+ UNKNOWN,
+ BODY_BUT_NO_LENGTH_ERROR,
+ WEAK_ETAG_ON_PUTDELETE_METHOD_ERROR,
+ WEAK_ETAG_AND_RANGE_ERROR,
+ NO_CACHE_DIRECTIVE_WITH_FIELD_NAME
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ResourceReference.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ResourceReference.java
new file mode 100644
index 0000000000..5e3f368f18
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ResourceReference.java
@@ -0,0 +1,62 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
+import ch.boye.httpclientandroidlib.client.cache.Resource;
+import ch.boye.httpclientandroidlib.util.Args;
+
+@Immutable
+class ResourceReference extends PhantomReference<HttpCacheEntry> {
+
+ private final Resource resource;
+
+ public ResourceReference(final HttpCacheEntry entry, final ReferenceQueue<HttpCacheEntry> q) {
+ super(entry, q);
+ Args.notNull(entry.getResource(), "Resource");
+ this.resource = entry.getResource();
+ }
+
+ public Resource getResource() {
+ return this.resource;
+ }
+
+ @Override
+ public int hashCode() {
+ return this.resource.hashCode();
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ return this.resource.equals(obj);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ResponseCachingPolicy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ResponseCachingPolicy.java
new file mode 100644
index 0000000000..abadb88d45
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ResponseCachingPolicy.java
@@ -0,0 +1,311 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.HttpMessage;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
+import ch.boye.httpclientandroidlib.client.utils.DateUtils;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+
+/**
+ * Determines if an HttpResponse can be cached.
+ *
+ * @since 4.1
+ */
+@Immutable
+class ResponseCachingPolicy {
+
+ private static final String[] AUTH_CACHEABLE_PARAMS = {
+ "s-maxage", HeaderConstants.CACHE_CONTROL_MUST_REVALIDATE, HeaderConstants.PUBLIC
+ };
+ private final long maxObjectSizeBytes;
+ private final boolean sharedCache;
+ private final boolean neverCache1_0ResponsesWithQueryString;
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+ private static final Set<Integer> cacheableStatuses =
+ new HashSet<Integer>(Arrays.asList(HttpStatus.SC_OK,
+ HttpStatus.SC_NON_AUTHORITATIVE_INFORMATION,
+ HttpStatus.SC_MULTIPLE_CHOICES,
+ HttpStatus.SC_MOVED_PERMANENTLY,
+ HttpStatus.SC_GONE));
+ private final Set<Integer> uncacheableStatuses;
+ /**
+ * Define a cache policy that limits the size of things that should be stored
+ * in the cache to a maximum of {@link HttpResponse} bytes in size.
+ *
+ * @param maxObjectSizeBytes the size to limit items into the cache
+ * @param sharedCache whether to behave as a shared cache (true) or a
+ * non-shared/private cache (false)
+ * @param neverCache1_0ResponsesWithQueryString true to never cache HTTP 1.0 responses with a query string, false
+ * to cache if explicit cache headers are found.
+ * @param allow303Caching if this policy is permitted to cache 303 response
+ */
+ public ResponseCachingPolicy(final long maxObjectSizeBytes,
+ final boolean sharedCache,
+ final boolean neverCache1_0ResponsesWithQueryString,
+ final boolean allow303Caching) {
+ this.maxObjectSizeBytes = maxObjectSizeBytes;
+ this.sharedCache = sharedCache;
+ this.neverCache1_0ResponsesWithQueryString = neverCache1_0ResponsesWithQueryString;
+ if (allow303Caching) {
+ uncacheableStatuses = new HashSet<Integer>(
+ Arrays.asList(HttpStatus.SC_PARTIAL_CONTENT));
+ } else {
+ uncacheableStatuses = new HashSet<Integer>(Arrays.asList(
+ HttpStatus.SC_PARTIAL_CONTENT, HttpStatus.SC_SEE_OTHER));
+ }
+ }
+
+ /**
+ * Determines if an HttpResponse can be cached.
+ *
+ * @param httpMethod What type of request was this, a GET, PUT, other?
+ * @param response The origin response
+ * @return <code>true</code> if response is cacheable
+ */
+ public boolean isResponseCacheable(final String httpMethod, final HttpResponse response) {
+ boolean cacheable = false;
+
+ if (!HeaderConstants.GET_METHOD.equals(httpMethod)) {
+ log.debug("Response was not cacheable.");
+ return false;
+ }
+
+ final int status = response.getStatusLine().getStatusCode();
+ if (cacheableStatuses.contains(status)) {
+ // these response codes MAY be cached
+ cacheable = true;
+ } else if (uncacheableStatuses.contains(status)) {
+ return false;
+ } else if (unknownStatusCode(status)) {
+ // a response with an unknown status code MUST NOT be
+ // cached
+ return false;
+ }
+
+ final Header contentLength = response.getFirstHeader(HTTP.CONTENT_LEN);
+ if (contentLength != null) {
+ final int contentLengthValue = Integer.parseInt(contentLength.getValue());
+ if (contentLengthValue > this.maxObjectSizeBytes) {
+ return false;
+ }
+ }
+
+ final Header[] ageHeaders = response.getHeaders(HeaderConstants.AGE);
+
+ if (ageHeaders.length > 1) {
+ return false;
+ }
+
+ final Header[] expiresHeaders = response.getHeaders(HeaderConstants.EXPIRES);
+
+ if (expiresHeaders.length > 1) {
+ return false;
+ }
+
+ final Header[] dateHeaders = response.getHeaders(HTTP.DATE_HEADER);
+
+ if (dateHeaders.length != 1) {
+ return false;
+ }
+
+ final Date date = DateUtils.parseDate(dateHeaders[0].getValue());
+ if (date == null) {
+ return false;
+ }
+
+ for (final Header varyHdr : response.getHeaders(HeaderConstants.VARY)) {
+ for (final HeaderElement elem : varyHdr.getElements()) {
+ if ("*".equals(elem.getName())) {
+ return false;
+ }
+ }
+ }
+
+ if (isExplicitlyNonCacheable(response)) {
+ return false;
+ }
+
+ return (cacheable || isExplicitlyCacheable(response));
+ }
+
+ private boolean unknownStatusCode(final int status) {
+ if (status >= 100 && status <= 101) {
+ return false;
+ }
+ if (status >= 200 && status <= 206) {
+ return false;
+ }
+ if (status >= 300 && status <= 307) {
+ return false;
+ }
+ if (status >= 400 && status <= 417) {
+ return false;
+ }
+ if (status >= 500 && status <= 505) {
+ return false;
+ }
+ return true;
+ }
+
+ protected boolean isExplicitlyNonCacheable(final HttpResponse response) {
+ final Header[] cacheControlHeaders = response.getHeaders(HeaderConstants.CACHE_CONTROL);
+ for (final Header header : cacheControlHeaders) {
+ for (final HeaderElement elem : header.getElements()) {
+ if (HeaderConstants.CACHE_CONTROL_NO_STORE.equals(elem.getName())
+ || HeaderConstants.CACHE_CONTROL_NO_CACHE.equals(elem.getName())
+ || (sharedCache && HeaderConstants.PRIVATE.equals(elem.getName()))) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ protected boolean hasCacheControlParameterFrom(final HttpMessage msg, final String[] params) {
+ final Header[] cacheControlHeaders = msg.getHeaders(HeaderConstants.CACHE_CONTROL);
+ for (final Header header : cacheControlHeaders) {
+ for (final HeaderElement elem : header.getElements()) {
+ for (final String param : params) {
+ if (param.equalsIgnoreCase(elem.getName())) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ protected boolean isExplicitlyCacheable(final HttpResponse response) {
+ if (response.getFirstHeader(HeaderConstants.EXPIRES) != null) {
+ return true;
+ }
+ final String[] cacheableParams = { HeaderConstants.CACHE_CONTROL_MAX_AGE, "s-maxage",
+ HeaderConstants.CACHE_CONTROL_MUST_REVALIDATE,
+ HeaderConstants.CACHE_CONTROL_PROXY_REVALIDATE,
+ HeaderConstants.PUBLIC
+ };
+ return hasCacheControlParameterFrom(response, cacheableParams);
+ }
+
+ /**
+ * Determine if the {@link HttpResponse} gotten from the origin is a
+ * cacheable response.
+ *
+ * @param request the {@link HttpRequest} that generated an origin hit
+ * @param response the {@link HttpResponse} from the origin
+ * @return <code>true</code> if response is cacheable
+ */
+ public boolean isResponseCacheable(final HttpRequest request, final HttpResponse response) {
+ if (requestProtocolGreaterThanAccepted(request)) {
+ log.debug("Response was not cacheable.");
+ return false;
+ }
+
+ final String[] uncacheableRequestDirectives = { HeaderConstants.CACHE_CONTROL_NO_STORE };
+ if (hasCacheControlParameterFrom(request,uncacheableRequestDirectives)) {
+ return false;
+ }
+
+ if (request.getRequestLine().getUri().contains("?")) {
+ if (neverCache1_0ResponsesWithQueryString && from1_0Origin(response)) {
+ log.debug("Response was not cacheable as it had a query string.");
+ return false;
+ } else if (!isExplicitlyCacheable(response)) {
+ log.debug("Response was not cacheable as it is missing explicit caching headers.");
+ return false;
+ }
+ }
+
+ if (expiresHeaderLessOrEqualToDateHeaderAndNoCacheControl(response)) {
+ return false;
+ }
+
+ if (sharedCache) {
+ final Header[] authNHeaders = request.getHeaders(HeaderConstants.AUTHORIZATION);
+ if (authNHeaders != null && authNHeaders.length > 0
+ && !hasCacheControlParameterFrom(response, AUTH_CACHEABLE_PARAMS)) {
+ return false;
+ }
+ }
+
+ final String method = request.getRequestLine().getMethod();
+ return isResponseCacheable(method, response);
+ }
+
+ private boolean expiresHeaderLessOrEqualToDateHeaderAndNoCacheControl(
+ final HttpResponse response) {
+ if (response.getFirstHeader(HeaderConstants.CACHE_CONTROL) != null) {
+ return false;
+ }
+ final Header expiresHdr = response.getFirstHeader(HeaderConstants.EXPIRES);
+ final Header dateHdr = response.getFirstHeader(HTTP.DATE_HEADER);
+ if (expiresHdr == null || dateHdr == null) {
+ return false;
+ }
+ final Date expires = DateUtils.parseDate(expiresHdr.getValue());
+ final Date date = DateUtils.parseDate(dateHdr.getValue());
+ if (expires == null || date == null) {
+ return false;
+ }
+ return expires.equals(date) || expires.before(date);
+ }
+
+ private boolean from1_0Origin(final HttpResponse response) {
+ final Header via = response.getFirstHeader(HeaderConstants.VIA);
+ if (via != null) {
+ for(final HeaderElement elt : via.getElements()) {
+ final String proto = elt.toString().split("\\s")[0];
+ if (proto.contains("/")) {
+ return proto.equals("HTTP/1.0");
+ } else {
+ return proto.equals("1.0");
+ }
+ }
+ }
+ return HttpVersion.HTTP_1_0.equals(response.getProtocolVersion());
+ }
+
+ private boolean requestProtocolGreaterThanAccepted(final HttpRequest req) {
+ return req.getProtocolVersion().compareToVersion(HttpVersion.HTTP_1_1) > 0;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ResponseProtocolCompliance.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ResponseProtocolCompliance.java
new file mode 100644
index 0000000000..7f5ebb4836
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ResponseProtocolCompliance.java
@@ -0,0 +1,251 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestWrapper;
+import ch.boye.httpclientandroidlib.client.utils.DateUtils;
+import ch.boye.httpclientandroidlib.message.BasicHeader;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+
+/**
+ * @since 4.1
+ */
+@Immutable
+class ResponseProtocolCompliance {
+
+ private static final String UNEXPECTED_100_CONTINUE = "The incoming request did not contain a "
+ + "100-continue header, but the response was a Status 100, continue.";
+ private static final String UNEXPECTED_PARTIAL_CONTENT = "partial content was returned for a request that did not ask for it";
+
+ /**
+ * When we get a response from a down stream server (Origin Server)
+ * we attempt to see if it is HTTP 1.1 Compliant and if not, attempt to
+ * make it so.
+ *
+ * @param request The {@link HttpRequest} that generated an origin hit and response
+ * @param response The {@link HttpResponse} from the origin server
+ * @throws IOException Bad things happened
+ */
+ public void ensureProtocolCompliance(final HttpRequestWrapper request, final HttpResponse response)
+ throws IOException {
+ if (backendResponseMustNotHaveBody(request, response)) {
+ consumeBody(response);
+ response.setEntity(null);
+ }
+
+ requestDidNotExpect100ContinueButResponseIsOne(request, response);
+
+ transferEncodingIsNotReturnedTo1_0Client(request, response);
+
+ ensurePartialContentIsNotSentToAClientThatDidNotRequestIt(request, response);
+
+ ensure200ForOPTIONSRequestWithNoBodyHasContentLengthZero(request, response);
+
+ ensure206ContainsDateHeader(response);
+
+ ensure304DoesNotContainExtraEntityHeaders(response);
+
+ identityIsNotUsedInContentEncoding(response);
+
+ warningsWithNonMatchingWarnDatesAreRemoved(response);
+ }
+
+ private void consumeBody(final HttpResponse response) throws IOException {
+ final HttpEntity body = response.getEntity();
+ if (body != null) {
+ IOUtils.consume(body);
+ }
+ }
+
+ private void warningsWithNonMatchingWarnDatesAreRemoved(
+ final HttpResponse response) {
+ final Date responseDate = DateUtils.parseDate(response.getFirstHeader(HTTP.DATE_HEADER).getValue());
+ if (responseDate == null) {
+ return;
+ }
+
+ final Header[] warningHeaders = response.getHeaders(HeaderConstants.WARNING);
+
+ if (warningHeaders == null || warningHeaders.length == 0) {
+ return;
+ }
+
+ final List<Header> newWarningHeaders = new ArrayList<Header>();
+ boolean modified = false;
+ for(final Header h : warningHeaders) {
+ for(final WarningValue wv : WarningValue.getWarningValues(h)) {
+ final Date warnDate = wv.getWarnDate();
+ if (warnDate == null || warnDate.equals(responseDate)) {
+ newWarningHeaders.add(new BasicHeader(HeaderConstants.WARNING,wv.toString()));
+ } else {
+ modified = true;
+ }
+ }
+ }
+ if (modified) {
+ response.removeHeaders(HeaderConstants.WARNING);
+ for(final Header h : newWarningHeaders) {
+ response.addHeader(h);
+ }
+ }
+ }
+
+ private void identityIsNotUsedInContentEncoding(final HttpResponse response) {
+ final Header[] hdrs = response.getHeaders(HTTP.CONTENT_ENCODING);
+ if (hdrs == null || hdrs.length == 0) {
+ return;
+ }
+ final List<Header> newHeaders = new ArrayList<Header>();
+ boolean modified = false;
+ for (final Header h : hdrs) {
+ final StringBuilder buf = new StringBuilder();
+ boolean first = true;
+ for (final HeaderElement elt : h.getElements()) {
+ if ("identity".equalsIgnoreCase(elt.getName())) {
+ modified = true;
+ } else {
+ if (!first) {
+ buf.append(",");
+ }
+ buf.append(elt.toString());
+ first = false;
+ }
+ }
+ final String newHeaderValue = buf.toString();
+ if (!"".equals(newHeaderValue)) {
+ newHeaders.add(new BasicHeader(HTTP.CONTENT_ENCODING, newHeaderValue));
+ }
+ }
+ if (!modified) {
+ return;
+ }
+ response.removeHeaders(HTTP.CONTENT_ENCODING);
+ for (final Header h : newHeaders) {
+ response.addHeader(h);
+ }
+ }
+
+ private void ensure206ContainsDateHeader(final HttpResponse response) {
+ if (response.getFirstHeader(HTTP.DATE_HEADER) == null) {
+ response.addHeader(HTTP.DATE_HEADER, DateUtils.formatDate(new Date()));
+ }
+
+ }
+
+ private void ensurePartialContentIsNotSentToAClientThatDidNotRequestIt(final HttpRequest request,
+ final HttpResponse response) throws IOException {
+ if (request.getFirstHeader(HeaderConstants.RANGE) != null
+ || response.getStatusLine().getStatusCode() != HttpStatus.SC_PARTIAL_CONTENT) {
+ return;
+ }
+
+ consumeBody(response);
+ throw new ClientProtocolException(UNEXPECTED_PARTIAL_CONTENT);
+ }
+
+ private void ensure200ForOPTIONSRequestWithNoBodyHasContentLengthZero(final HttpRequest request,
+ final HttpResponse response) {
+ if (!request.getRequestLine().getMethod().equalsIgnoreCase(HeaderConstants.OPTIONS_METHOD)) {
+ return;
+ }
+
+ if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
+ return;
+ }
+
+ if (response.getFirstHeader(HTTP.CONTENT_LEN) == null) {
+ response.addHeader(HTTP.CONTENT_LEN, "0");
+ }
+ }
+
+ private void ensure304DoesNotContainExtraEntityHeaders(final HttpResponse response) {
+ final String[] disallowedEntityHeaders = { HeaderConstants.ALLOW, HTTP.CONTENT_ENCODING,
+ "Content-Language", HTTP.CONTENT_LEN, "Content-MD5",
+ "Content-Range", HTTP.CONTENT_TYPE, HeaderConstants.LAST_MODIFIED
+ };
+ if (response.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
+ for(final String hdr : disallowedEntityHeaders) {
+ response.removeHeaders(hdr);
+ }
+ }
+ }
+
+ private boolean backendResponseMustNotHaveBody(final HttpRequest request, final HttpResponse backendResponse) {
+ return HeaderConstants.HEAD_METHOD.equals(request.getRequestLine().getMethod())
+ || backendResponse.getStatusLine().getStatusCode() == HttpStatus.SC_NO_CONTENT
+ || backendResponse.getStatusLine().getStatusCode() == HttpStatus.SC_RESET_CONTENT
+ || backendResponse.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED;
+ }
+
+ private void requestDidNotExpect100ContinueButResponseIsOne(final HttpRequestWrapper request,
+ final HttpResponse response) throws IOException {
+ if (response.getStatusLine().getStatusCode() != HttpStatus.SC_CONTINUE) {
+ return;
+ }
+
+ final HttpRequest originalRequest = request.getOriginal();
+ if (originalRequest instanceof HttpEntityEnclosingRequest) {
+ if (((HttpEntityEnclosingRequest)originalRequest).expectContinue()) {
+ return;
+ }
+ }
+ consumeBody(response);
+ throw new ClientProtocolException(UNEXPECTED_100_CONTINUE);
+ }
+
+ private void transferEncodingIsNotReturnedTo1_0Client(final HttpRequestWrapper request,
+ final HttpResponse response) {
+ final HttpRequest originalRequest = request.getOriginal();
+ if (originalRequest.getProtocolVersion().compareToVersion(HttpVersion.HTTP_1_1) >= 0) {
+ return;
+ }
+
+ removeResponseTransferEncoding(response);
+ }
+
+ private void removeResponseTransferEncoding(final HttpResponse response) {
+ response.removeHeaders("TE");
+ response.removeHeaders(HTTP.TRANSFER_ENCODING);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ResponseProxyHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ResponseProxyHandler.java
new file mode 100644
index 0000000000..a541b27780
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/ResponseProxyHandler.java
@@ -0,0 +1,88 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+
+/**
+ * A proxy class that can enhance an arbitrary {@link HttpResponse} with
+ * {@link Closeable#close()} method.
+ *
+ * @since 4.3
+ */
+@NotThreadSafe
+class ResponseProxyHandler implements InvocationHandler {
+
+ private static final Method CLOSE_METHOD;
+
+ static {
+ try {
+ CLOSE_METHOD = Closeable.class.getMethod("close");
+ } catch (final NoSuchMethodException ex) {
+ throw new Error(ex);
+ }
+ }
+
+ private final HttpResponse original;
+
+ ResponseProxyHandler(final HttpResponse original) {
+ super();
+ this.original = original;
+ }
+
+ public void close() throws IOException {
+ IOUtils.consume(original.getEntity());
+ }
+
+ public Object invoke(
+ final Object proxy, final Method method, final Object[] args) throws Throwable {
+ if (method.equals(CLOSE_METHOD)) {
+ close();
+ return null;
+ } else {
+ try {
+ return method.invoke(this.original, args);
+ } catch (final InvocationTargetException ex) {
+ final Throwable cause = ex.getCause();
+ if (cause != null) {
+ throw cause;
+ } else {
+ throw ex;
+ }
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/SchedulingStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/SchedulingStrategy.java
new file mode 100644
index 0000000000..0940fbc831
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/SchedulingStrategy.java
@@ -0,0 +1,45 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.Closeable;
+
+/**
+ * Specifies when revalidation requests are scheduled.
+ *
+ * @since 4.3
+ */
+public interface SchedulingStrategy extends Closeable
+{
+ /**
+ * Schedule an {@link AsynchronousValidationRequest} to be executed.
+ *
+ * @param revalidationRequest the request to be executed; not <code>null</code>
+ * @throws java.util.concurrent.RejectedExecutionException if the request could not be scheduled for execution
+ */
+ void schedule(AsynchronousValidationRequest revalidationRequest);
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/SizeLimitedResponseReader.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/SizeLimitedResponseReader.java
new file mode 100644
index 0000000000..4c36ebb489
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/SizeLimitedResponseReader.java
@@ -0,0 +1,150 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Proxy;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.client.cache.InputLimit;
+import ch.boye.httpclientandroidlib.client.cache.Resource;
+import ch.boye.httpclientandroidlib.client.cache.ResourceFactory;
+import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
+import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
+
+/**
+ * @since 4.1
+ */
+@NotThreadSafe
+class SizeLimitedResponseReader {
+
+ private final ResourceFactory resourceFactory;
+ private final long maxResponseSizeBytes;
+ private final HttpRequest request;
+ private final CloseableHttpResponse response;
+
+ private InputStream instream;
+ private InputLimit limit;
+ private Resource resource;
+ private boolean consumed;
+
+ /**
+ * Create an {@link HttpResponse} that is limited in size, this allows for checking
+ * the size of objects that will be stored in the cache.
+ */
+ public SizeLimitedResponseReader(
+ final ResourceFactory resourceFactory,
+ final long maxResponseSizeBytes,
+ final HttpRequest request,
+ final CloseableHttpResponse response) {
+ super();
+ this.resourceFactory = resourceFactory;
+ this.maxResponseSizeBytes = maxResponseSizeBytes;
+ this.request = request;
+ this.response = response;
+ }
+
+ protected void readResponse() throws IOException {
+ if (!consumed) {
+ doConsume();
+ }
+ }
+
+ private void ensureNotConsumed() {
+ if (consumed) {
+ throw new IllegalStateException("Response has already been consumed");
+ }
+ }
+
+ private void ensureConsumed() {
+ if (!consumed) {
+ throw new IllegalStateException("Response has not been consumed");
+ }
+ }
+
+ private void doConsume() throws IOException {
+ ensureNotConsumed();
+ consumed = true;
+
+ limit = new InputLimit(maxResponseSizeBytes);
+
+ final HttpEntity entity = response.getEntity();
+ if (entity == null) {
+ return;
+ }
+ final String uri = request.getRequestLine().getUri();
+ instream = entity.getContent();
+ try {
+ resource = resourceFactory.generate(uri, instream, limit);
+ } finally {
+ if (!limit.isReached()) {
+ instream.close();
+ }
+ }
+ }
+
+ boolean isLimitReached() {
+ ensureConsumed();
+ return limit.isReached();
+ }
+
+ Resource getResource() {
+ ensureConsumed();
+ return resource;
+ }
+
+ CloseableHttpResponse getReconstructedResponse() throws IOException {
+ ensureConsumed();
+ final HttpResponse reconstructed = new BasicHttpResponse(response.getStatusLine());
+ reconstructed.setHeaders(response.getAllHeaders());
+
+ final CombinedEntity combinedEntity = new CombinedEntity(resource, instream);
+ final HttpEntity entity = response.getEntity();
+ if (entity != null) {
+ combinedEntity.setContentType(entity.getContentType());
+ combinedEntity.setContentEncoding(entity.getContentEncoding());
+ combinedEntity.setChunked(entity.isChunked());
+ }
+ reconstructed.setEntity(combinedEntity);
+ return (CloseableHttpResponse) Proxy.newProxyInstance(
+ ResponseProxyHandler.class.getClassLoader(),
+ new Class<?>[] { CloseableHttpResponse.class },
+ new ResponseProxyHandler(reconstructed) {
+
+ @Override
+ public void close() throws IOException {
+ response.close();
+ }
+
+ });
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/Variant.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/Variant.java
new file mode 100644
index 0000000000..f631b02115
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/Variant.java
@@ -0,0 +1,55 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
+
+/** Records a set of information describing a cached variant. */
+class Variant {
+
+ private final String variantKey;
+ private final String cacheKey;
+ private final HttpCacheEntry entry;
+
+ public Variant(final String variantKey, final String cacheKey, final HttpCacheEntry entry) {
+ this.variantKey = variantKey;
+ this.cacheKey = cacheKey;
+ this.entry = entry;
+ }
+
+ public String getVariantKey() {
+ return variantKey;
+ }
+
+ public String getCacheKey() {
+ return cacheKey;
+ }
+
+ public HttpCacheEntry getEntry() {
+ return entry;
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/WarningValue.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/WarningValue.java
new file mode 100644
index 0000000000..a3c2ce67df
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/WarningValue.java
@@ -0,0 +1,370 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.client.cache;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.client.utils.DateUtils;
+
+/** This class provides for parsing and understanding Warning headers. As
+ * the Warning header can be multi-valued, but the values can contain
+ * separators like commas inside quoted strings, we cannot use the regular
+ * {@link Header#getElements()} call to access the values.
+ */
+class WarningValue {
+
+ private int offs;
+ private int init_offs;
+ private final String src;
+ private int warnCode;
+ private String warnAgent;
+ private String warnText;
+ private Date warnDate;
+
+ WarningValue(final String s) {
+ this(s, 0);
+ }
+
+ WarningValue(final String s, final int offs) {
+ this.offs = this.init_offs = offs;
+ this.src = s;
+ consumeWarnValue();
+ }
+
+ /** Returns an array of the parseable warning values contained
+ * in the given header value, which is assumed to be a
+ * Warning header. Improperly formatted warning values will be
+ * skipped, in keeping with the philosophy of "ignore what you
+ * cannot understand."
+ * @param h Warning {@link Header} to parse
+ * @return array of <code>WarnValue</code> objects
+ */
+ public static WarningValue[] getWarningValues(final Header h) {
+ final List<WarningValue> out = new ArrayList<WarningValue>();
+ final String src = h.getValue();
+ int offs = 0;
+ while(offs < src.length()) {
+ try {
+ final WarningValue wv = new WarningValue(src, offs);
+ out.add(wv);
+ offs = wv.offs;
+ } catch (final IllegalArgumentException e) {
+ final int nextComma = src.indexOf(',', offs);
+ if (nextComma == -1) {
+ break;
+ }
+ offs = nextComma + 1;
+ }
+ }
+ final WarningValue[] wvs = {};
+ return out.toArray(wvs);
+ }
+
+ /*
+ * LWS = [CRLF] 1*( SP | HT )
+ * CRLF = CR LF
+ */
+ protected void consumeLinearWhitespace() {
+ while(offs < src.length()) {
+ switch(src.charAt(offs)) {
+ case '\r':
+ if (offs+2 >= src.length()
+ || src.charAt(offs+1) != '\n'
+ || (src.charAt(offs+2) != ' '
+ && src.charAt(offs+2) != '\t')) {
+ return;
+ }
+ offs += 2;
+ break;
+ case ' ':
+ case '\t':
+ break;
+ default:
+ return;
+ }
+ offs++;
+ }
+ }
+
+ /*
+ * CHAR = <any US-ASCII character (octets 0 - 127)>
+ */
+ private boolean isChar(final char c) {
+ final int i = c;
+ return (i >= 0 && i <= 127);
+ }
+
+ /*
+ * CTL = <any US-ASCII control character
+ (octets 0 - 31) and DEL (127)>
+ */
+ private boolean isControl(final char c) {
+ final int i = c;
+ return (i == 127 || (i >=0 && i <= 31));
+ }
+
+ /*
+ * separators = "(" | ")" | "<" | ">" | "@"
+ * | "," | ";" | ":" | "\" | <">
+ * | "/" | "[" | "]" | "?" | "="
+ * | "{" | "}" | SP | HT
+ */
+ private boolean isSeparator(final char c) {
+ return (c == '(' || c == ')' || c == '<' || c == '>'
+ || c == '@' || c == ',' || c == ';' || c == ':'
+ || c == '\\' || c == '\"' || c == '/'
+ || c == '[' || c == ']' || c == '?' || c == '='
+ || c == '{' || c == '}' || c == ' ' || c == '\t');
+ }
+
+ /*
+ * token = 1*<any CHAR except CTLs or separators>
+ */
+ protected void consumeToken() {
+ if (!isTokenChar(src.charAt(offs))) {
+ parseError();
+ }
+ while(offs < src.length()) {
+ if (!isTokenChar(src.charAt(offs))) {
+ break;
+ }
+ offs++;
+ }
+ }
+
+ private boolean isTokenChar(final char c) {
+ return (isChar(c) && !isControl(c) && !isSeparator(c));
+ }
+
+ private static final String TOPLABEL = "\\p{Alpha}([\\p{Alnum}-]*\\p{Alnum})?";
+ private static final String DOMAINLABEL = "\\p{Alnum}([\\p{Alnum}-]*\\p{Alnum})?";
+ private static final String HOSTNAME = "(" + DOMAINLABEL + "\\.)*" + TOPLABEL + "\\.?";
+ private static final String IPV4ADDRESS = "\\d+\\.\\d+\\.\\d+\\.\\d+";
+ private static final String HOST = "(" + HOSTNAME + ")|(" + IPV4ADDRESS + ")";
+ private static final String PORT = "\\d*";
+ private static final String HOSTPORT = "(" + HOST + ")(\\:" + PORT + ")?";
+ private static final Pattern HOSTPORT_PATTERN = Pattern.compile(HOSTPORT);
+
+ protected void consumeHostPort() {
+ final Matcher m = HOSTPORT_PATTERN.matcher(src.substring(offs));
+ if (!m.find()) {
+ parseError();
+ }
+ if (m.start() != 0) {
+ parseError();
+ }
+ offs += m.end();
+ }
+
+
+ /*
+ * warn-agent = ( host [ ":" port ] ) | pseudonym
+ * pseudonym = token
+ */
+ protected void consumeWarnAgent() {
+ final int curr_offs = offs;
+ try {
+ consumeHostPort();
+ warnAgent = src.substring(curr_offs, offs);
+ consumeCharacter(' ');
+ return;
+ } catch (final IllegalArgumentException e) {
+ offs = curr_offs;
+ }
+ consumeToken();
+ warnAgent = src.substring(curr_offs, offs);
+ consumeCharacter(' ');
+ }
+
+ /*
+ * quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
+ * qdtext = <any TEXT except <">>
+ */
+ protected void consumeQuotedString() {
+ if (src.charAt(offs) != '\"') {
+ parseError();
+ }
+ offs++;
+ boolean foundEnd = false;
+ while(offs < src.length() && !foundEnd) {
+ final char c = src.charAt(offs);
+ if (offs + 1 < src.length() && c == '\\'
+ && isChar(src.charAt(offs+1))) {
+ offs += 2; // consume quoted-pair
+ } else if (c == '\"') {
+ foundEnd = true;
+ offs++;
+ } else if (c != '\"' && !isControl(c)) {
+ offs++;
+ } else {
+ parseError();
+ }
+ }
+ if (!foundEnd) {
+ parseError();
+ }
+ }
+
+ /*
+ * warn-text = quoted-string
+ */
+ protected void consumeWarnText() {
+ final int curr = offs;
+ consumeQuotedString();
+ warnText = src.substring(curr, offs);
+ }
+
+ private static final String MONTH = "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec";
+ private static final String WEEKDAY = "Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday";
+ private static final String WKDAY = "Mon|Tue|Wed|Thu|Fri|Sat|Sun";
+ private static final String TIME = "\\d{2}:\\d{2}:\\d{2}";
+ private static final String DATE3 = "(" + MONTH + ") ( |\\d)\\d";
+ private static final String DATE2 = "\\d{2}-(" + MONTH + ")-\\d{2}";
+ private static final String DATE1 = "\\d{2} (" + MONTH + ") \\d{4}";
+ private static final String ASCTIME_DATE = "(" + WKDAY + ") (" + DATE3 + ") (" + TIME + ") \\d{4}";
+ private static final String RFC850_DATE = "(" + WEEKDAY + "), (" + DATE2 + ") (" + TIME + ") GMT";
+ private static final String RFC1123_DATE = "(" + WKDAY + "), (" + DATE1 + ") (" + TIME + ") GMT";
+ private static final String HTTP_DATE = "(" + RFC1123_DATE + ")|(" + RFC850_DATE + ")|(" + ASCTIME_DATE + ")";
+ private static final String WARN_DATE = "\"(" + HTTP_DATE + ")\"";
+ private static final Pattern WARN_DATE_PATTERN = Pattern.compile(WARN_DATE);
+
+ /*
+ * warn-date = <"> HTTP-date <">
+ */
+ protected void consumeWarnDate() {
+ final int curr = offs;
+ final Matcher m = WARN_DATE_PATTERN.matcher(src.substring(offs));
+ if (!m.lookingAt()) {
+ parseError();
+ }
+ offs += m.end();
+ warnDate = DateUtils.parseDate(src.substring(curr+1,offs-1));
+ }
+
+ /*
+ * warning-value = warn-code SP warn-agent SP warn-text [SP warn-date]
+ */
+ protected void consumeWarnValue() {
+ consumeLinearWhitespace();
+ consumeWarnCode();
+ consumeWarnAgent();
+ consumeWarnText();
+ if (offs + 1 < src.length() && src.charAt(offs) == ' ' && src.charAt(offs+1) == '\"') {
+ consumeCharacter(' ');
+ consumeWarnDate();
+ }
+ consumeLinearWhitespace();
+ if (offs != src.length()) {
+ consumeCharacter(',');
+ }
+ }
+
+ protected void consumeCharacter(final char c) {
+ if (offs + 1 > src.length()
+ || c != src.charAt(offs)) {
+ parseError();
+ }
+ offs++;
+ }
+
+ /*
+ * warn-code = 3DIGIT
+ */
+ protected void consumeWarnCode() {
+ if (offs + 4 > src.length()
+ || !Character.isDigit(src.charAt(offs))
+ || !Character.isDigit(src.charAt(offs + 1))
+ || !Character.isDigit(src.charAt(offs + 2))
+ || src.charAt(offs + 3) != ' ') {
+ parseError();
+ }
+ warnCode = Integer.parseInt(src.substring(offs,offs+3));
+ offs += 4;
+ }
+
+ private void parseError() {
+ final String s = src.substring(init_offs);
+ throw new IllegalArgumentException("Bad warn code \"" + s + "\"");
+ }
+
+ /** Returns the 3-digit code associated with this warning.
+ * @return <code>int</code>
+ */
+ public int getWarnCode() { return warnCode; }
+
+ /** Returns the "warn-agent" string associated with this warning,
+ * which is either the name or pseudonym of the server that added
+ * this particular Warning header.
+ * @return {@link String}
+ */
+ public String getWarnAgent() { return warnAgent; }
+
+ /** Returns the human-readable warning text for this warning. Note
+ * that the original quoted-string is returned here, including
+ * escaping for any contained characters. In other words, if the
+ * header was:
+ * <pre>
+ * Warning: 110 fred "Response is stale"
+ * </pre>
+ * then this method will return <code>"\"Response is stale\""</code>
+ * (surrounding quotes included).
+ * @return {@link String}
+ */
+ public String getWarnText() { return warnText; }
+
+ /** Returns the date and time when this warning was added, or
+ * <code>null</code> if a warning date was not supplied in the
+ * header.
+ * @return {@link Date}
+ */
+ public Date getWarnDate() { return warnDate; }
+
+ /** Formats a <code>WarningValue</code> as a {@link String}
+ * suitable for including in a header. For example, you can:
+ * <pre>
+ * WarningValue wv = ...;
+ * HttpResponse resp = ...;
+ * resp.addHeader("Warning", wv.toString());
+ * </pre>
+ * @return {@link String}
+ */
+ @Override
+ public String toString() {
+ if (warnDate != null) {
+ return String.format("%d %s %s \"%s\"", warnCode,
+ warnAgent, warnText, DateUtils.formatDate(warnDate));
+ } else {
+ return String.format("%d %s %s", warnCode, warnAgent, warnText);
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/package.html b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/package.html
new file mode 100644
index 0000000000..4c208e7a9b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/cache/package.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+<!--
+====================================================================
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+====================================================================
+
+This software consists of voluntary contributions made by many
+individuals on behalf of the Apache Software Foundation. For more
+information on the Apache Software Foundation, please see
+<http://www.apache.org/>.
+-->
+</head>
+<body bgcolor="white">
+
+<p>
+This package contains a cache module that can be used for HTTP/1.1
+client-side caching. The primary classes in this package are the
+{@link org.apache.http.impl.client.cache.CachingHttpClient},
+which is a drop-in replacement for
+a {@link org.apache.http.impl.client.DefaultHttpClient} that adds
+caching, and the {@link org.apache.http.impl.client.cache.CacheConfig}
+class that can be used for configuring it.
+</p>
+</body>
+</html>
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/package-info.java
new file mode 100644
index 0000000000..1efbf4cec0
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/client/package-info.java
@@ -0,0 +1,51 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Default HTTP client implementation.
+ * <p/>
+ * The usual execution flow can be demonstrated by the code snippet below:
+ * <pre>
+ * CloseableHttpClient httpclient = HttpClients.createDefault();
+ * try {
+ * HttpGet httpGet = new HttpGet("http://targethost/homepage");
+ * CloseableHttpResponse response = httpclient.execute(httpGet);
+ * try {
+ * System.out.println(response.getStatusLine());
+ * HttpEntity entity = response.getEntity();
+ * // do something useful with the response body
+ * // and ensure it is fully consumed
+ * EntityUtils.consume(entity);
+ * } finally {
+ * response.close();
+ * }
+ * } finally {
+ * httpclient.close();
+ * }
+ * </pre>
+ */
+package ch.boye.httpclientandroidlib.impl.client;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/AbstractClientConnAdapter.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/AbstractClientConnAdapter.java
new file mode 100644
index 0000000000..ea2524f6a1
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/AbstractClientConnAdapter.java
@@ -0,0 +1,369 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+
+import ch.boye.httpclientandroidlib.HttpConnectionMetrics;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionManager;
+import ch.boye.httpclientandroidlib.conn.ManagedClientConnection;
+import ch.boye.httpclientandroidlib.conn.OperatedClientConnection;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * Abstract adapter from {@link OperatedClientConnection operated} to
+ * {@link ManagedClientConnection managed} client connections.
+ * Read and write methods are delegated to the wrapped connection.
+ * Operations affecting the connection state have to be implemented
+ * by derived classes. Operations for querying the connection state
+ * are delegated to the wrapped connection if there is one, or
+ * return a default value if there is none.
+ * <p>
+ * This adapter tracks the checkpoints for reusable communication states,
+ * as indicated by {@link #markReusable markReusable} and queried by
+ * {@link #isMarkedReusable isMarkedReusable}.
+ * All send and receive operations will automatically clear the mark.
+ * <p>
+ * Connection release calls are delegated to the connection manager,
+ * if there is one. {@link #abortConnection abortConnection} will
+ * clear the reusability mark first. The connection manager is
+ * expected to tolerate multiple calls to the release method.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) do not use
+ */
+@Deprecated
+@NotThreadSafe
+public abstract class AbstractClientConnAdapter implements ManagedClientConnection, HttpContext {
+
+ /**
+ * The connection manager.
+ */
+ private final ClientConnectionManager connManager;
+
+ /** The wrapped connection. */
+ private volatile OperatedClientConnection wrappedConnection;
+
+ /** The reusability marker. */
+ private volatile boolean markedReusable;
+
+ /** True if the connection has been shut down or released. */
+ private volatile boolean released;
+
+ /** The duration this is valid for while idle (in ms). */
+ private volatile long duration;
+
+ /**
+ * Creates a new connection adapter.
+ * The adapter is initially <i>not</i>
+ * {@link #isMarkedReusable marked} as reusable.
+ *
+ * @param mgr the connection manager, or <code>null</code>
+ * @param conn the connection to wrap, or <code>null</code>
+ */
+ protected AbstractClientConnAdapter(final ClientConnectionManager mgr,
+ final OperatedClientConnection conn) {
+ super();
+ connManager = mgr;
+ wrappedConnection = conn;
+ markedReusable = false;
+ released = false;
+ duration = Long.MAX_VALUE;
+ }
+
+ /**
+ * Detaches this adapter from the wrapped connection.
+ * This adapter becomes useless.
+ */
+ protected synchronized void detach() {
+ wrappedConnection = null;
+ duration = Long.MAX_VALUE;
+ }
+
+ protected OperatedClientConnection getWrappedConnection() {
+ return wrappedConnection;
+ }
+
+ protected ClientConnectionManager getManager() {
+ return connManager;
+ }
+
+ /**
+ * @deprecated (4.1) use {@link #assertValid(OperatedClientConnection)}
+ */
+ @Deprecated
+ protected final void assertNotAborted() throws InterruptedIOException {
+ if (isReleased()) {
+ throw new InterruptedIOException("Connection has been shut down");
+ }
+ }
+
+ /**
+ * @since 4.1
+ * @return value of released flag
+ */
+ protected boolean isReleased() {
+ return released;
+ }
+
+ /**
+ * Asserts that there is a valid wrapped connection to delegate to.
+ *
+ * @throws ConnectionShutdownException if there is no wrapped connection
+ * or connection has been aborted
+ */
+ protected final void assertValid(
+ final OperatedClientConnection wrappedConn) throws ConnectionShutdownException {
+ if (isReleased() || wrappedConn == null) {
+ throw new ConnectionShutdownException();
+ }
+ }
+
+ public boolean isOpen() {
+ final OperatedClientConnection conn = getWrappedConnection();
+ if (conn == null) {
+ return false;
+ }
+
+ return conn.isOpen();
+ }
+
+ public boolean isStale() {
+ if (isReleased()) {
+ return true;
+ }
+ final OperatedClientConnection conn = getWrappedConnection();
+ if (conn == null) {
+ return true;
+ }
+
+ return conn.isStale();
+ }
+
+ public void setSocketTimeout(final int timeout) {
+ final OperatedClientConnection conn = getWrappedConnection();
+ assertValid(conn);
+ conn.setSocketTimeout(timeout);
+ }
+
+ public int getSocketTimeout() {
+ final OperatedClientConnection conn = getWrappedConnection();
+ assertValid(conn);
+ return conn.getSocketTimeout();
+ }
+
+ public HttpConnectionMetrics getMetrics() {
+ final OperatedClientConnection conn = getWrappedConnection();
+ assertValid(conn);
+ return conn.getMetrics();
+ }
+
+ public void flush() throws IOException {
+ final OperatedClientConnection conn = getWrappedConnection();
+ assertValid(conn);
+ conn.flush();
+ }
+
+ public boolean isResponseAvailable(final int timeout) throws IOException {
+ final OperatedClientConnection conn = getWrappedConnection();
+ assertValid(conn);
+ return conn.isResponseAvailable(timeout);
+ }
+
+ public void receiveResponseEntity(final HttpResponse response)
+ throws HttpException, IOException {
+ final OperatedClientConnection conn = getWrappedConnection();
+ assertValid(conn);
+ unmarkReusable();
+ conn.receiveResponseEntity(response);
+ }
+
+ public HttpResponse receiveResponseHeader()
+ throws HttpException, IOException {
+ final OperatedClientConnection conn = getWrappedConnection();
+ assertValid(conn);
+ unmarkReusable();
+ return conn.receiveResponseHeader();
+ }
+
+ public void sendRequestEntity(final HttpEntityEnclosingRequest request)
+ throws HttpException, IOException {
+ final OperatedClientConnection conn = getWrappedConnection();
+ assertValid(conn);
+ unmarkReusable();
+ conn.sendRequestEntity(request);
+ }
+
+ public void sendRequestHeader(final HttpRequest request)
+ throws HttpException, IOException {
+ final OperatedClientConnection conn = getWrappedConnection();
+ assertValid(conn);
+ unmarkReusable();
+ conn.sendRequestHeader(request);
+ }
+
+ public InetAddress getLocalAddress() {
+ final OperatedClientConnection conn = getWrappedConnection();
+ assertValid(conn);
+ return conn.getLocalAddress();
+ }
+
+ public int getLocalPort() {
+ final OperatedClientConnection conn = getWrappedConnection();
+ assertValid(conn);
+ return conn.getLocalPort();
+ }
+
+ public InetAddress getRemoteAddress() {
+ final OperatedClientConnection conn = getWrappedConnection();
+ assertValid(conn);
+ return conn.getRemoteAddress();
+ }
+
+ public int getRemotePort() {
+ final OperatedClientConnection conn = getWrappedConnection();
+ assertValid(conn);
+ return conn.getRemotePort();
+ }
+
+ public boolean isSecure() {
+ final OperatedClientConnection conn = getWrappedConnection();
+ assertValid(conn);
+ return conn.isSecure();
+ }
+
+ public void bind(final Socket socket) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ public Socket getSocket() {
+ final OperatedClientConnection conn = getWrappedConnection();
+ assertValid(conn);
+ if (!isOpen()) {
+ return null;
+ }
+ return conn.getSocket();
+ }
+
+ public SSLSession getSSLSession() {
+ final OperatedClientConnection conn = getWrappedConnection();
+ assertValid(conn);
+ if (!isOpen()) {
+ return null;
+ }
+
+ SSLSession result = null;
+ final Socket sock = conn.getSocket();
+ if (sock instanceof SSLSocket) {
+ result = ((SSLSocket)sock).getSession();
+ }
+ return result;
+ }
+
+ public void markReusable() {
+ markedReusable = true;
+ }
+
+ public void unmarkReusable() {
+ markedReusable = false;
+ }
+
+ public boolean isMarkedReusable() {
+ return markedReusable;
+ }
+
+ public void setIdleDuration(final long duration, final TimeUnit unit) {
+ if(duration > 0) {
+ this.duration = unit.toMillis(duration);
+ } else {
+ this.duration = -1;
+ }
+ }
+
+ public synchronized void releaseConnection() {
+ if (released) {
+ return;
+ }
+ released = true;
+ connManager.releaseConnection(this, duration, TimeUnit.MILLISECONDS);
+ }
+
+ public synchronized void abortConnection() {
+ if (released) {
+ return;
+ }
+ released = true;
+ unmarkReusable();
+ try {
+ shutdown();
+ } catch (final IOException ignore) {
+ }
+ connManager.releaseConnection(this, duration, TimeUnit.MILLISECONDS);
+ }
+
+ public Object getAttribute(final String id) {
+ final OperatedClientConnection conn = getWrappedConnection();
+ assertValid(conn);
+ if (conn instanceof HttpContext) {
+ return ((HttpContext) conn).getAttribute(id);
+ } else {
+ return null;
+ }
+ }
+
+ public Object removeAttribute(final String id) {
+ final OperatedClientConnection conn = getWrappedConnection();
+ assertValid(conn);
+ if (conn instanceof HttpContext) {
+ return ((HttpContext) conn).removeAttribute(id);
+ } else {
+ return null;
+ }
+ }
+
+ public void setAttribute(final String id, final Object obj) {
+ final OperatedClientConnection conn = getWrappedConnection();
+ assertValid(conn);
+ if (conn instanceof HttpContext) {
+ ((HttpContext) conn).setAttribute(id, obj);
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/AbstractPoolEntry.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/AbstractPoolEntry.java
new file mode 100644
index 0000000000..cbbe727b5e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/AbstractPoolEntry.java
@@ -0,0 +1,262 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionOperator;
+import ch.boye.httpclientandroidlib.conn.OperatedClientConnection;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.conn.routing.RouteTracker;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+
+/**
+ * A pool entry for use by connection manager implementations.
+ * Pool entries work in conjunction with an
+ * {@link AbstractClientConnAdapter adapter}.
+ * The adapter is handed out to applications that obtain a connection.
+ * The pool entry stores the underlying connection and tracks the
+ * {@link HttpRoute route} established.
+ * The adapter delegates methods for establishing the route to
+ * its pool entry.
+ * <p>
+ * If the managed connections is released or revoked, the adapter
+ * gets disconnected, but the pool entry still contains the
+ * underlying connection and the established route.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) do not use
+ */
+@Deprecated
+public abstract class AbstractPoolEntry {
+
+ /** The connection operator. */
+ protected final ClientConnectionOperator connOperator;
+
+ /** The underlying connection being pooled or used. */
+ protected final OperatedClientConnection connection;
+
+ /** The route for which this entry gets allocated. */
+ //@@@ currently accessed from connection manager(s) as attribute
+ //@@@ avoid that, derived classes should decide whether update is allowed
+ //@@@ SCCM: yes, TSCCM: no
+ protected volatile HttpRoute route;
+
+ /** Connection state object */
+ protected volatile Object state;
+
+ /** The tracked route, or <code>null</code> before tracking starts. */
+ protected volatile RouteTracker tracker;
+
+
+ /**
+ * Creates a new pool entry.
+ *
+ * @param connOperator the Connection Operator for this entry
+ * @param route the planned route for the connection,
+ * or <code>null</code>
+ */
+ protected AbstractPoolEntry(final ClientConnectionOperator connOperator,
+ final HttpRoute route) {
+ super();
+ Args.notNull(connOperator, "Connection operator");
+ this.connOperator = connOperator;
+ this.connection = connOperator.createConnection();
+ this.route = route;
+ this.tracker = null;
+ }
+
+ /**
+ * Returns the state object associated with this pool entry.
+ *
+ * @return The state object
+ */
+ public Object getState() {
+ return state;
+ }
+
+ /**
+ * Assigns a state object to this pool entry.
+ *
+ * @param state The state object
+ */
+ public void setState(final Object state) {
+ this.state = state;
+ }
+
+ /**
+ * Opens the underlying connection.
+ *
+ * @param route the route along which to open the connection
+ * @param context the context for opening the connection
+ * @param params the parameters for opening the connection
+ *
+ * @throws IOException in case of a problem
+ */
+ public void open(final HttpRoute route,
+ final HttpContext context, final HttpParams params)
+ throws IOException {
+
+ Args.notNull(route, "Route");
+ Args.notNull(params, "HTTP parameters");
+ if (this.tracker != null) {
+ Asserts.check(!this.tracker.isConnected(), "Connection already open");
+ }
+ // - collect the arguments
+ // - call the operator
+ // - update the tracking data
+ // In this order, we can be sure that only a successful
+ // opening of the connection will be tracked.
+
+ this.tracker = new RouteTracker(route);
+ final HttpHost proxy = route.getProxyHost();
+
+ connOperator.openConnection
+ (this.connection,
+ (proxy != null) ? proxy : route.getTargetHost(),
+ route.getLocalAddress(),
+ context, params);
+
+ final RouteTracker localTracker = tracker; // capture volatile
+
+ // If this tracker was reset while connecting,
+ // fail early.
+ if (localTracker == null) {
+ throw new InterruptedIOException("Request aborted");
+ }
+
+ if (proxy == null) {
+ localTracker.connectTarget(this.connection.isSecure());
+ } else {
+ localTracker.connectProxy(proxy, this.connection.isSecure());
+ }
+
+ }
+
+ /**
+ * Tracks tunnelling of the connection to the target.
+ * The tunnel has to be established outside by sending a CONNECT
+ * request to the (last) proxy.
+ *
+ * @param secure <code>true</code> if the tunnel should be
+ * considered secure, <code>false</code> otherwise
+ * @param params the parameters for tunnelling the connection
+ *
+ * @throws IOException in case of a problem
+ */
+ public void tunnelTarget(final boolean secure, final HttpParams params)
+ throws IOException {
+
+ Args.notNull(params, "HTTP parameters");
+ Asserts.notNull(this.tracker, "Route tracker");
+ Asserts.check(this.tracker.isConnected(), "Connection not open");
+ Asserts.check(!this.tracker.isTunnelled(), "Connection is already tunnelled");
+
+ this.connection.update(null, tracker.getTargetHost(),
+ secure, params);
+ this.tracker.tunnelTarget(secure);
+ }
+
+ /**
+ * Tracks tunnelling of the connection to a chained proxy.
+ * The tunnel has to be established outside by sending a CONNECT
+ * request to the previous proxy.
+ *
+ * @param next the proxy to which the tunnel was established.
+ * See {@link ch.boye.httpclientandroidlib.conn.ManagedClientConnection#tunnelProxy
+ * ManagedClientConnection.tunnelProxy}
+ * for details.
+ * @param secure <code>true</code> if the tunnel should be
+ * considered secure, <code>false</code> otherwise
+ * @param params the parameters for tunnelling the connection
+ *
+ * @throws IOException in case of a problem
+ */
+ public void tunnelProxy(final HttpHost next, final boolean secure, final HttpParams params)
+ throws IOException {
+
+ Args.notNull(next, "Next proxy");
+ Args.notNull(params, "Parameters");
+
+ Asserts.notNull(this.tracker, "Route tracker");
+ Asserts.check(this.tracker.isConnected(), "Connection not open");
+
+ this.connection.update(null, next, secure, params);
+ this.tracker.tunnelProxy(next, secure);
+ }
+
+ /**
+ * Layers a protocol on top of an established tunnel.
+ *
+ * @param context the context for layering
+ * @param params the parameters for layering
+ *
+ * @throws IOException in case of a problem
+ */
+ public void layerProtocol(final HttpContext context, final HttpParams params)
+ throws IOException {
+
+ //@@@ is context allowed to be null? depends on operator?
+ Args.notNull(params, "HTTP parameters");
+ Asserts.notNull(this.tracker, "Route tracker");
+ Asserts.check(this.tracker.isConnected(), "Connection not open");
+ Asserts.check(this.tracker.isTunnelled(), "Protocol layering without a tunnel not supported");
+ Asserts.check(!this.tracker.isLayered(), "Multiple protocol layering not supported");
+ // - collect the arguments
+ // - call the operator
+ // - update the tracking data
+ // In this order, we can be sure that only a successful
+ // layering on top of the connection will be tracked.
+
+ final HttpHost target = tracker.getTargetHost();
+
+ connOperator.updateSecureConnection(this.connection, target,
+ context, params);
+
+ this.tracker.layerProtocol(this.connection.isSecure());
+
+ }
+
+ /**
+ * Shuts down the entry.
+ *
+ * If {@link #open(HttpRoute, HttpContext, HttpParams)} is in progress,
+ * this will cause that open to possibly throw an {@link IOException}.
+ */
+ protected void shutdownEntry() {
+ tracker = null;
+ state = null;
+ }
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/AbstractPooledConnAdapter.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/AbstractPooledConnAdapter.java
new file mode 100644
index 0000000000..47f0feda52
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/AbstractPooledConnAdapter.java
@@ -0,0 +1,191 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionManager;
+import ch.boye.httpclientandroidlib.conn.OperatedClientConnection;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * Abstract adapter from pool {@link AbstractPoolEntry entries} to
+ * {@link ch.boye.httpclientandroidlib.conn.ManagedClientConnection managed}
+ * client connections.
+ * The connection in the pool entry is used to initialize the base class.
+ * In addition, methods to establish a route are delegated to the
+ * pool entry. {@link #shutdown shutdown} and {@link #close close}
+ * will clear the tracked route in the pool entry and call the
+ * respective method of the wrapped connection.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) do not use
+ */
+@Deprecated
+public abstract class AbstractPooledConnAdapter extends AbstractClientConnAdapter {
+
+ /** The wrapped pool entry. */
+ protected volatile AbstractPoolEntry poolEntry;
+
+ /**
+ * Creates a new connection adapter.
+ *
+ * @param manager the connection manager
+ * @param entry the pool entry for the connection being wrapped
+ */
+ protected AbstractPooledConnAdapter(final ClientConnectionManager manager,
+ final AbstractPoolEntry entry) {
+ super(manager, entry.connection);
+ this.poolEntry = entry;
+ }
+
+ public String getId() {
+ return null;
+ }
+
+ /**
+ * Obtains the pool entry.
+ *
+ * @return the pool entry, or <code>null</code> if detached
+ *
+ * @deprecated (4.0.1)
+ */
+ @Deprecated
+ protected AbstractPoolEntry getPoolEntry() {
+ return this.poolEntry;
+ }
+
+ /**
+ * Asserts that there is a valid pool entry.
+ *
+ * @throws ConnectionShutdownException if there is no pool entry
+ * or connection has been aborted
+ *
+ * @see #assertValid(OperatedClientConnection)
+ */
+ protected void assertValid(final AbstractPoolEntry entry) {
+ if (isReleased() || entry == null) {
+ throw new ConnectionShutdownException();
+ }
+ }
+
+ /**
+ * @deprecated (4.1) use {@link #assertValid(AbstractPoolEntry)}
+ */
+ @Deprecated
+ protected final void assertAttached() {
+ if (poolEntry == null) {
+ throw new ConnectionShutdownException();
+ }
+ }
+
+ /**
+ * Detaches this adapter from the wrapped connection.
+ * This adapter becomes useless.
+ */
+ @Override
+ protected synchronized void detach() {
+ poolEntry = null;
+ super.detach();
+ }
+
+ public HttpRoute getRoute() {
+ final AbstractPoolEntry entry = getPoolEntry();
+ assertValid(entry);
+ return (entry.tracker == null) ? null : entry.tracker.toRoute();
+ }
+
+ public void open(final HttpRoute route,
+ final HttpContext context, final HttpParams params)
+ throws IOException {
+ final AbstractPoolEntry entry = getPoolEntry();
+ assertValid(entry);
+ entry.open(route, context, params);
+ }
+
+ public void tunnelTarget(final boolean secure, final HttpParams params)
+ throws IOException {
+ final AbstractPoolEntry entry = getPoolEntry();
+ assertValid(entry);
+ entry.tunnelTarget(secure, params);
+ }
+
+ public void tunnelProxy(final HttpHost next, final boolean secure, final HttpParams params)
+ throws IOException {
+ final AbstractPoolEntry entry = getPoolEntry();
+ assertValid(entry);
+ entry.tunnelProxy(next, secure, params);
+ }
+
+ public void layerProtocol(final HttpContext context, final HttpParams params)
+ throws IOException {
+ final AbstractPoolEntry entry = getPoolEntry();
+ assertValid(entry);
+ entry.layerProtocol(context, params);
+ }
+
+ public void close() throws IOException {
+ final AbstractPoolEntry entry = getPoolEntry();
+ if (entry != null) {
+ entry.shutdownEntry();
+ }
+
+ final OperatedClientConnection conn = getWrappedConnection();
+ if (conn != null) {
+ conn.close();
+ }
+ }
+
+ public void shutdown() throws IOException {
+ final AbstractPoolEntry entry = getPoolEntry();
+ if (entry != null) {
+ entry.shutdownEntry();
+ }
+
+ final OperatedClientConnection conn = getWrappedConnection();
+ if (conn != null) {
+ conn.shutdown();
+ }
+ }
+
+ public Object getState() {
+ final AbstractPoolEntry entry = getPoolEntry();
+ assertValid(entry);
+ return entry.getState();
+ }
+
+ public void setState(final Object state) {
+ final AbstractPoolEntry entry = getPoolEntry();
+ assertValid(entry);
+ entry.setState(state);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/BasicClientConnectionManager.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/BasicClientConnectionManager.java
new file mode 100644
index 0000000000..176e281502
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/BasicClientConnectionManager.java
@@ -0,0 +1,276 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.HttpClientConnection;
+import ch.boye.httpclientandroidlib.annotation.GuardedBy;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionManager;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionOperator;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionRequest;
+import ch.boye.httpclientandroidlib.conn.ManagedClientConnection;
+import ch.boye.httpclientandroidlib.conn.OperatedClientConnection;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.conn.scheme.SchemeRegistry;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+
+/**
+ * A connection manager for a single connection. This connection manager maintains only one active
+ * connection. Even though this class is fully thread-safe it ought to be used by one execution
+ * thread only, as only one thread a time can lease the connection at a time.
+ * <p/>
+ * This connection manager will make an effort to reuse the connection for subsequent requests
+ * with the same {@link HttpRoute route}. It will, however, close the existing connection and
+ * open it for the given route, if the route of the persistent connection does not match that
+ * of the connection request. If the connection has been already been allocated
+ * {@link IllegalStateException} is thrown.
+ * <p/>
+ * This connection manager implementation should be used inside an EJB container instead of
+ * {@link PoolingClientConnectionManager}.
+ *
+ * @since 4.2
+ *
+ * @deprecated (4.3) use {@link BasicHttpClientConnectionManager}.
+ */
+@ThreadSafe
+@Deprecated
+public class BasicClientConnectionManager implements ClientConnectionManager {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ private static final AtomicLong COUNTER = new AtomicLong();
+
+ /** The message to be logged on multiple allocation. */
+ public final static String MISUSE_MESSAGE =
+ "Invalid use of BasicClientConnManager: connection still allocated.\n" +
+ "Make sure to release the connection before allocating another one.";
+
+ /** The schemes supported by this connection manager. */
+ private final SchemeRegistry schemeRegistry;
+
+ /** The operator for opening and updating connections. */
+ private final ClientConnectionOperator connOperator;
+
+ /** The one and only entry in this pool. */
+ @GuardedBy("this")
+ private HttpPoolEntry poolEntry;
+
+ /** The currently issued managed connection, if any. */
+ @GuardedBy("this")
+ private ManagedClientConnectionImpl conn;
+
+ /** Indicates whether this connection manager is shut down. */
+ @GuardedBy("this")
+ private volatile boolean shutdown;
+
+ /**
+ * Creates a new simple connection manager.
+ *
+ * @param schreg the scheme registry
+ */
+ public BasicClientConnectionManager(final SchemeRegistry schreg) {
+ Args.notNull(schreg, "Scheme registry");
+ this.schemeRegistry = schreg;
+ this.connOperator = createConnectionOperator(schreg);
+ }
+
+ public BasicClientConnectionManager() {
+ this(SchemeRegistryFactory.createDefault());
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ shutdown();
+ } finally { // Make sure we call overridden method even if shutdown barfs
+ super.finalize();
+ }
+ }
+
+ public SchemeRegistry getSchemeRegistry() {
+ return this.schemeRegistry;
+ }
+
+ protected ClientConnectionOperator createConnectionOperator(final SchemeRegistry schreg) {
+ return new DefaultClientConnectionOperator(schreg);
+ }
+
+ public final ClientConnectionRequest requestConnection(
+ final HttpRoute route,
+ final Object state) {
+
+ return new ClientConnectionRequest() {
+
+ public void abortRequest() {
+ // Nothing to abort, since requests are immediate.
+ }
+
+ public ManagedClientConnection getConnection(
+ final long timeout, final TimeUnit tunit) {
+ return BasicClientConnectionManager.this.getConnection(
+ route, state);
+ }
+
+ };
+ }
+
+ private void assertNotShutdown() {
+ Asserts.check(!this.shutdown, "Connection manager has been shut down");
+ }
+
+ ManagedClientConnection getConnection(final HttpRoute route, final Object state) {
+ Args.notNull(route, "Route");
+ synchronized (this) {
+ assertNotShutdown();
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Get connection for route " + route);
+ }
+ Asserts.check(this.conn == null, MISUSE_MESSAGE);
+ if (this.poolEntry != null && !this.poolEntry.getPlannedRoute().equals(route)) {
+ this.poolEntry.close();
+ this.poolEntry = null;
+ }
+ if (this.poolEntry == null) {
+ final String id = Long.toString(COUNTER.getAndIncrement());
+ final OperatedClientConnection conn = this.connOperator.createConnection();
+ this.poolEntry = new HttpPoolEntry(this.log, id, route, conn, 0, TimeUnit.MILLISECONDS);
+ }
+ final long now = System.currentTimeMillis();
+ if (this.poolEntry.isExpired(now)) {
+ this.poolEntry.close();
+ this.poolEntry.getTracker().reset();
+ }
+ this.conn = new ManagedClientConnectionImpl(this, this.connOperator, this.poolEntry);
+ return this.conn;
+ }
+ }
+
+ private void shutdownConnection(final HttpClientConnection conn) {
+ try {
+ conn.shutdown();
+ } catch (final IOException iox) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("I/O exception shutting down connection", iox);
+ }
+ }
+ }
+
+ public void releaseConnection(final ManagedClientConnection conn, final long keepalive, final TimeUnit tunit) {
+ Args.check(conn instanceof ManagedClientConnectionImpl, "Connection class mismatch, " +
+ "connection not obtained from this manager");
+ final ManagedClientConnectionImpl managedConn = (ManagedClientConnectionImpl) conn;
+ synchronized (managedConn) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Releasing connection " + conn);
+ }
+ if (managedConn.getPoolEntry() == null) {
+ return; // already released
+ }
+ final ClientConnectionManager manager = managedConn.getManager();
+ Asserts.check(manager == this, "Connection not obtained from this manager");
+ synchronized (this) {
+ if (this.shutdown) {
+ shutdownConnection(managedConn);
+ return;
+ }
+ try {
+ if (managedConn.isOpen() && !managedConn.isMarkedReusable()) {
+ shutdownConnection(managedConn);
+ }
+ if (managedConn.isMarkedReusable()) {
+ this.poolEntry.updateExpiry(keepalive, tunit != null ? tunit : TimeUnit.MILLISECONDS);
+ if (this.log.isDebugEnabled()) {
+ final String s;
+ if (keepalive > 0) {
+ s = "for " + keepalive + " " + tunit;
+ } else {
+ s = "indefinitely";
+ }
+ this.log.debug("Connection can be kept alive " + s);
+ }
+ }
+ } finally {
+ managedConn.detach();
+ this.conn = null;
+ if (this.poolEntry.isClosed()) {
+ this.poolEntry = null;
+ }
+ }
+ }
+ }
+ }
+
+ public void closeExpiredConnections() {
+ synchronized (this) {
+ assertNotShutdown();
+ final long now = System.currentTimeMillis();
+ if (this.poolEntry != null && this.poolEntry.isExpired(now)) {
+ this.poolEntry.close();
+ this.poolEntry.getTracker().reset();
+ }
+ }
+ }
+
+ public void closeIdleConnections(final long idletime, final TimeUnit tunit) {
+ Args.notNull(tunit, "Time unit");
+ synchronized (this) {
+ assertNotShutdown();
+ long time = tunit.toMillis(idletime);
+ if (time < 0) {
+ time = 0;
+ }
+ final long deadline = System.currentTimeMillis() - time;
+ if (this.poolEntry != null && this.poolEntry.getUpdated() <= deadline) {
+ this.poolEntry.close();
+ this.poolEntry.getTracker().reset();
+ }
+ }
+ }
+
+ public void shutdown() {
+ synchronized (this) {
+ this.shutdown = true;
+ try {
+ if (this.poolEntry != null) {
+ this.poolEntry.close();
+ }
+ } finally {
+ this.poolEntry = null;
+ this.conn = null;
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/BasicHttpClientConnectionManager.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/BasicHttpClientConnectionManager.java
new file mode 100644
index 0000000000..1049a41518
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/BasicHttpClientConnectionManager.java
@@ -0,0 +1,370 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.HttpClientConnection;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.annotation.GuardedBy;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.config.ConnectionConfig;
+import ch.boye.httpclientandroidlib.config.Lookup;
+import ch.boye.httpclientandroidlib.config.Registry;
+import ch.boye.httpclientandroidlib.config.RegistryBuilder;
+import ch.boye.httpclientandroidlib.config.SocketConfig;
+import ch.boye.httpclientandroidlib.conn.ConnectionRequest;
+import ch.boye.httpclientandroidlib.conn.DnsResolver;
+import ch.boye.httpclientandroidlib.conn.HttpClientConnectionManager;
+import ch.boye.httpclientandroidlib.conn.HttpConnectionFactory;
+import ch.boye.httpclientandroidlib.conn.SchemePortResolver;
+import ch.boye.httpclientandroidlib.conn.ManagedHttpClientConnection;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.conn.socket.ConnectionSocketFactory;
+import ch.boye.httpclientandroidlib.conn.socket.PlainConnectionSocketFactory;
+import ch.boye.httpclientandroidlib.conn.ssl.SSLConnectionSocketFactory;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+import ch.boye.httpclientandroidlib.util.LangUtils;
+
+/**
+ * A connection manager for a single connection. This connection manager maintains only one active
+ * connection. Even though this class is fully thread-safe it ought to be used by one execution
+ * thread only, as only one thread a time can lease the connection at a time.
+ * <p/>
+ * This connection manager will make an effort to reuse the connection for subsequent requests
+ * with the same {@link HttpRoute route}. It will, however, close the existing connection and
+ * open it for the given route, if the route of the persistent connection does not match that
+ * of the connection request. If the connection has been already been allocated
+ * {@link IllegalStateException} is thrown.
+ * <p/>
+ * This connection manager implementation should be used inside an EJB container instead of
+ * {@link PoolingHttpClientConnectionManager}.
+ *
+ * @since 4.3
+ */
+@ThreadSafe
+public class BasicHttpClientConnectionManager implements HttpClientConnectionManager, Closeable {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ private final HttpClientConnectionOperator connectionOperator;
+ private final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory;
+
+ @GuardedBy("this")
+ private ManagedHttpClientConnection conn;
+
+ @GuardedBy("this")
+ private HttpRoute route;
+
+ @GuardedBy("this")
+ private Object state;
+
+ @GuardedBy("this")
+ private long updated;
+
+ @GuardedBy("this")
+ private long expiry;
+
+ @GuardedBy("this")
+ private boolean leased;
+
+ @GuardedBy("this")
+ private SocketConfig socketConfig;
+
+ @GuardedBy("this")
+ private ConnectionConfig connConfig;
+
+ private final AtomicBoolean isShutdown;
+
+ private static Registry<ConnectionSocketFactory> getDefaultRegistry() {
+ return RegistryBuilder.<ConnectionSocketFactory>create()
+ .register("http", PlainConnectionSocketFactory.getSocketFactory())
+ .register("https", SSLConnectionSocketFactory.getSocketFactory())
+ .build();
+ }
+
+ public BasicHttpClientConnectionManager(
+ final Lookup<ConnectionSocketFactory> socketFactoryRegistry,
+ final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory,
+ final SchemePortResolver schemePortResolver,
+ final DnsResolver dnsResolver) {
+ super();
+ this.connectionOperator = new HttpClientConnectionOperator(
+ socketFactoryRegistry, schemePortResolver, dnsResolver);
+ this.connFactory = connFactory != null ? connFactory : ManagedHttpClientConnectionFactory.INSTANCE;
+ this.expiry = Long.MAX_VALUE;
+ this.socketConfig = SocketConfig.DEFAULT;
+ this.connConfig = ConnectionConfig.DEFAULT;
+ this.isShutdown = new AtomicBoolean(false);
+ }
+
+ public BasicHttpClientConnectionManager(
+ final Lookup<ConnectionSocketFactory> socketFactoryRegistry,
+ final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory) {
+ this(socketFactoryRegistry, connFactory, null, null);
+ }
+
+ public BasicHttpClientConnectionManager(
+ final Lookup<ConnectionSocketFactory> socketFactoryRegistry) {
+ this(socketFactoryRegistry, null, null, null);
+ }
+
+ public BasicHttpClientConnectionManager() {
+ this(getDefaultRegistry(), null, null, null);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ shutdown();
+ } finally { // Make sure we call overridden method even if shutdown barfs
+ super.finalize();
+ }
+ }
+
+ public void close() {
+ shutdown();
+ }
+
+ HttpRoute getRoute() {
+ return route;
+ }
+
+ Object getState() {
+ return state;
+ }
+
+ public synchronized SocketConfig getSocketConfig() {
+ return socketConfig;
+ }
+
+ public synchronized void setSocketConfig(final SocketConfig socketConfig) {
+ this.socketConfig = socketConfig != null ? socketConfig : SocketConfig.DEFAULT;
+ }
+
+ public synchronized ConnectionConfig getConnectionConfig() {
+ return connConfig;
+ }
+
+ public synchronized void setConnectionConfig(final ConnectionConfig connConfig) {
+ this.connConfig = connConfig != null ? connConfig : ConnectionConfig.DEFAULT;
+ }
+
+ public final ConnectionRequest requestConnection(
+ final HttpRoute route,
+ final Object state) {
+ Args.notNull(route, "Route");
+ return new ConnectionRequest() {
+
+ public boolean cancel() {
+ // Nothing to abort, since requests are immediate.
+ return false;
+ }
+
+ public HttpClientConnection get(final long timeout, final TimeUnit tunit) {
+ return BasicHttpClientConnectionManager.this.getConnection(
+ route, state);
+ }
+
+ };
+ }
+
+ private void closeConnection() {
+ if (this.conn != null) {
+ this.log.debug("Closing connection");
+ try {
+ this.conn.close();
+ } catch (final IOException iox) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("I/O exception closing connection", iox);
+ }
+ }
+ this.conn = null;
+ }
+ }
+
+ private void shutdownConnection() {
+ if (this.conn != null) {
+ this.log.debug("Shutting down connection");
+ try {
+ this.conn.shutdown();
+ } catch (final IOException iox) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("I/O exception shutting down connection", iox);
+ }
+ }
+ this.conn = null;
+ }
+ }
+
+ private void checkExpiry() {
+ if (this.conn != null && System.currentTimeMillis() >= this.expiry) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Connection expired @ " + new Date(this.expiry));
+ }
+ closeConnection();
+ }
+ }
+
+ synchronized HttpClientConnection getConnection(final HttpRoute route, final Object state) {
+ Asserts.check(!this.isShutdown.get(), "Connection manager has been shut down");
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Get connection for route " + route);
+ }
+ Asserts.check(!this.leased, "Connection is still allocated");
+ if (!LangUtils.equals(this.route, route) || !LangUtils.equals(this.state, state)) {
+ closeConnection();
+ }
+ this.route = route;
+ this.state = state;
+ checkExpiry();
+ if (this.conn == null) {
+ this.conn = this.connFactory.create(route, this.connConfig);
+ }
+ this.leased = true;
+ return this.conn;
+ }
+
+ public synchronized void releaseConnection(
+ final HttpClientConnection conn,
+ final Object state,
+ final long keepalive, final TimeUnit tunit) {
+ Args.notNull(conn, "Connection");
+ Asserts.check(conn == this.conn, "Connection not obtained from this manager");
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Releasing connection " + conn);
+ }
+ if (this.isShutdown.get()) {
+ return;
+ }
+ try {
+ this.updated = System.currentTimeMillis();
+ if (!this.conn.isOpen()) {
+ this.conn = null;
+ this.route = null;
+ this.conn = null;
+ this.expiry = Long.MAX_VALUE;
+ } else {
+ this.state = state;
+ if (this.log.isDebugEnabled()) {
+ final String s;
+ if (keepalive > 0) {
+ s = "for " + keepalive + " " + tunit;
+ } else {
+ s = "indefinitely";
+ }
+ this.log.debug("Connection can be kept alive " + s);
+ }
+ if (keepalive > 0) {
+ this.expiry = this.updated + tunit.toMillis(keepalive);
+ } else {
+ this.expiry = Long.MAX_VALUE;
+ }
+ }
+ } finally {
+ this.leased = false;
+ }
+ }
+
+ public void connect(
+ final HttpClientConnection conn,
+ final HttpRoute route,
+ final int connectTimeout,
+ final HttpContext context) throws IOException {
+ Args.notNull(conn, "Connection");
+ Args.notNull(route, "HTTP route");
+ Asserts.check(conn == this.conn, "Connection not obtained from this manager");
+ final HttpHost host;
+ if (route.getProxyHost() != null) {
+ host = route.getProxyHost();
+ } else {
+ host = route.getTargetHost();
+ }
+ final InetSocketAddress localAddress = route.getLocalSocketAddress();
+ this.connectionOperator.connect(this.conn, host, localAddress,
+ connectTimeout, this.socketConfig, context);
+ }
+
+ public void upgrade(
+ final HttpClientConnection conn,
+ final HttpRoute route,
+ final HttpContext context) throws IOException {
+ Args.notNull(conn, "Connection");
+ Args.notNull(route, "HTTP route");
+ Asserts.check(conn == this.conn, "Connection not obtained from this manager");
+ this.connectionOperator.upgrade(this.conn, route.getTargetHost(), context);
+ }
+
+ public void routeComplete(
+ final HttpClientConnection conn,
+ final HttpRoute route,
+ final HttpContext context) throws IOException {
+ }
+
+ public synchronized void closeExpiredConnections() {
+ if (this.isShutdown.get()) {
+ return;
+ }
+ if (!this.leased) {
+ checkExpiry();
+ }
+ }
+
+ public synchronized void closeIdleConnections(final long idletime, final TimeUnit tunit) {
+ Args.notNull(tunit, "Time unit");
+ if (this.isShutdown.get()) {
+ return;
+ }
+ if (!this.leased) {
+ long time = tunit.toMillis(idletime);
+ if (time < 0) {
+ time = 0;
+ }
+ final long deadline = System.currentTimeMillis() - time;
+ if (this.updated <= deadline) {
+ closeConnection();
+ }
+ }
+ }
+
+ public synchronized void shutdown() {
+ if (this.isShutdown.compareAndSet(false, true)) {
+ shutdownConnection();
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/CPool.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/CPool.java
new file mode 100644
index 0000000000..040960c711
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/CPool.java
@@ -0,0 +1,67 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.conn.ManagedHttpClientConnection;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.pool.AbstractConnPool;
+import ch.boye.httpclientandroidlib.pool.ConnFactory;
+
+/**
+ * @since 4.3
+ */
+@ThreadSafe
+class CPool extends AbstractConnPool<HttpRoute, ManagedHttpClientConnection, CPoolEntry> {
+
+ private static final AtomicLong COUNTER = new AtomicLong();
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(CPool.class);
+ private final long timeToLive;
+ private final TimeUnit tunit;
+
+ public CPool(
+ final ConnFactory<HttpRoute, ManagedHttpClientConnection> connFactory,
+ final int defaultMaxPerRoute, final int maxTotal,
+ final long timeToLive, final TimeUnit tunit) {
+ super(connFactory, defaultMaxPerRoute, maxTotal);
+ this.timeToLive = timeToLive;
+ this.tunit = tunit;
+ }
+
+ @Override
+ protected CPoolEntry createEntry(final HttpRoute route, final ManagedHttpClientConnection conn) {
+ final String id = Long.toString(COUNTER.getAndIncrement());
+ return new CPoolEntry(this.log, id, route, conn, this.timeToLive, this.tunit);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/CPoolEntry.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/CPoolEntry.java
new file mode 100644
index 0000000000..f5ecfdc5ec
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/CPoolEntry.java
@@ -0,0 +1,101 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+import ch.boye.httpclientandroidlib.HttpClientConnection;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.conn.ManagedHttpClientConnection;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.pool.PoolEntry;
+
+/**
+ * @since 4.3
+ */
+@ThreadSafe
+class CPoolEntry extends PoolEntry<HttpRoute, ManagedHttpClientConnection> {
+
+ public HttpClientAndroidLog log;
+ private volatile boolean routeComplete;
+
+ public CPoolEntry(
+ final HttpClientAndroidLog log,
+ final String id,
+ final HttpRoute route,
+ final ManagedHttpClientConnection conn,
+ final long timeToLive, final TimeUnit tunit) {
+ super(id, route, conn, timeToLive, tunit);
+ this.log = log;
+ }
+
+ public void markRouteComplete() {
+ this.routeComplete = true;
+ }
+
+ public boolean isRouteComplete() {
+ return this.routeComplete;
+ }
+
+ public void closeConnection() throws IOException {
+ final HttpClientConnection conn = getConnection();
+ conn.close();
+ }
+
+ public void shutdownConnection() throws IOException {
+ final HttpClientConnection conn = getConnection();
+ conn.shutdown();
+ }
+
+ @Override
+ public boolean isExpired(final long now) {
+ final boolean expired = super.isExpired(now);
+ if (expired && this.log.isDebugEnabled()) {
+ this.log.debug("Connection " + this + " expired @ " + new Date(getExpiry()));
+ }
+ return expired;
+ }
+
+ @Override
+ public boolean isClosed() {
+ final HttpClientConnection conn = getConnection();
+ return !conn.isOpen();
+ }
+
+ @Override
+ public void close() {
+ try {
+ closeConnection();
+ } catch (final IOException ex) {
+ this.log.debug("I/O error closing connection", ex);
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/CPoolProxy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/CPoolProxy.java
new file mode 100644
index 0000000000..9ac67a10de
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/CPoolProxy.java
@@ -0,0 +1,245 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+
+import javax.net.ssl.SSLSession;
+
+import ch.boye.httpclientandroidlib.HttpClientConnection;
+import ch.boye.httpclientandroidlib.HttpConnectionMetrics;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.conn.ManagedHttpClientConnection;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * @since 4.3
+ */
+@NotThreadSafe
+class CPoolProxy implements ManagedHttpClientConnection, HttpContext {
+
+ private volatile CPoolEntry poolEntry;
+
+ CPoolProxy(final CPoolEntry entry) {
+ super();
+ this.poolEntry = entry;
+ }
+
+ CPoolEntry getPoolEntry() {
+ return this.poolEntry;
+ }
+
+ CPoolEntry detach() {
+ final CPoolEntry local = this.poolEntry;
+ this.poolEntry = null;
+ return local;
+ }
+
+ ManagedHttpClientConnection getConnection() {
+ final CPoolEntry local = this.poolEntry;
+ if (local == null) {
+ return null;
+ }
+ return local.getConnection();
+ }
+
+ ManagedHttpClientConnection getValidConnection() {
+ final ManagedHttpClientConnection conn = getConnection();
+ if (conn == null) {
+ throw new ConnectionShutdownException();
+ }
+ return conn;
+ }
+
+ public void close() throws IOException {
+ final CPoolEntry local = this.poolEntry;
+ if (local != null) {
+ local.closeConnection();
+ }
+ }
+
+ public void shutdown() throws IOException {
+ final CPoolEntry local = this.poolEntry;
+ if (local != null) {
+ local.shutdownConnection();
+ }
+ }
+
+ public boolean isOpen() {
+ final CPoolEntry local = this.poolEntry;
+ if (local != null) {
+ return !local.isClosed();
+ } else {
+ return false;
+ }
+ }
+
+ public boolean isStale() {
+ final HttpClientConnection conn = getConnection();
+ if (conn != null) {
+ return conn.isStale();
+ } else {
+ return true;
+ }
+ }
+
+ public void setSocketTimeout(final int timeout) {
+ getValidConnection().setSocketTimeout(timeout);
+ }
+
+ public int getSocketTimeout() {
+ return getValidConnection().getSocketTimeout();
+ }
+
+ public String getId() {
+ return getValidConnection().getId();
+ }
+
+ public void bind(final Socket socket) throws IOException {
+ getValidConnection().bind(socket);
+ }
+
+ public Socket getSocket() {
+ return getValidConnection().getSocket();
+ }
+
+ public SSLSession getSSLSession() {
+ return getValidConnection().getSSLSession();
+ }
+
+ public boolean isResponseAvailable(final int timeout) throws IOException {
+ return getValidConnection().isResponseAvailable(timeout);
+ }
+
+ public void sendRequestHeader(final HttpRequest request) throws HttpException, IOException {
+ getValidConnection().sendRequestHeader(request);
+ }
+
+ public void sendRequestEntity(final HttpEntityEnclosingRequest request) throws HttpException, IOException {
+ getValidConnection().sendRequestEntity(request);
+ }
+
+ public HttpResponse receiveResponseHeader() throws HttpException, IOException {
+ return getValidConnection().receiveResponseHeader();
+ }
+
+ public void receiveResponseEntity(final HttpResponse response) throws HttpException, IOException {
+ getValidConnection().receiveResponseEntity(response);
+ }
+
+ public void flush() throws IOException {
+ getValidConnection().flush();
+ }
+
+ public HttpConnectionMetrics getMetrics() {
+ return getValidConnection().getMetrics();
+ }
+
+ public InetAddress getLocalAddress() {
+ return getValidConnection().getLocalAddress();
+ }
+
+ public int getLocalPort() {
+ return getValidConnection().getLocalPort();
+ }
+
+ public InetAddress getRemoteAddress() {
+ return getValidConnection().getRemoteAddress();
+ }
+
+ public int getRemotePort() {
+ return getValidConnection().getRemotePort();
+ }
+
+ public Object getAttribute(final String id) {
+ final ManagedHttpClientConnection conn = getValidConnection();
+ if (conn instanceof HttpContext) {
+ return ((HttpContext) conn).getAttribute(id);
+ } else {
+ return null;
+ }
+ }
+
+ public void setAttribute(final String id, final Object obj) {
+ final ManagedHttpClientConnection conn = getValidConnection();
+ if (conn instanceof HttpContext) {
+ ((HttpContext) conn).setAttribute(id, obj);
+ }
+ }
+
+ public Object removeAttribute(final String id) {
+ final ManagedHttpClientConnection conn = getValidConnection();
+ if (conn instanceof HttpContext) {
+ return ((HttpContext) conn).removeAttribute(id);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("CPoolProxy{");
+ final ManagedHttpClientConnection conn = getConnection();
+ if (conn != null) {
+ sb.append(conn);
+ } else {
+ sb.append("detached");
+ }
+ sb.append('}');
+ return sb.toString();
+ }
+
+ public static HttpClientConnection newProxy(final CPoolEntry poolEntry) {
+ return new CPoolProxy(poolEntry);
+ }
+
+ private static CPoolProxy getProxy(final HttpClientConnection conn) {
+ if (!CPoolProxy.class.isInstance(conn)) {
+ throw new IllegalStateException("Unexpected connection proxy class: " + conn.getClass());
+ }
+ return CPoolProxy.class.cast(conn);
+ }
+
+ public static CPoolEntry getPoolEntry(final HttpClientConnection proxy) {
+ final CPoolEntry entry = getProxy(proxy).getPoolEntry();
+ if (entry == null) {
+ throw new ConnectionShutdownException();
+ }
+ return entry;
+ }
+
+ public static CPoolEntry detach(final HttpClientConnection conn) {
+ return getProxy(conn).detach();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/ConnectionShutdownException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/ConnectionShutdownException.java
new file mode 100644
index 0000000000..03820a38fb
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/ConnectionShutdownException.java
@@ -0,0 +1,50 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Signals that the connection has been shut down or released back to the
+ * the connection pool
+ *
+ * @since 4.1
+ */
+@Immutable
+public class ConnectionShutdownException extends IllegalStateException {
+
+ private static final long serialVersionUID = 5868657401162844497L;
+
+ /**
+ * Creates a new ConnectionShutdownException with a <tt>null</tt> detail message.
+ */
+ public ConnectionShutdownException() {
+ super();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultClientConnection.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultClientConnection.java
new file mode 100644
index 0000000000..f9003b1440
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultClientConnection.java
@@ -0,0 +1,292 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.Socket;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpResponseFactory;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.conn.OperatedClientConnection;
+import ch.boye.httpclientandroidlib.conn.ManagedHttpClientConnection;
+import ch.boye.httpclientandroidlib.impl.SocketHttpClientConnection;
+import ch.boye.httpclientandroidlib.io.HttpMessageParser;
+import ch.boye.httpclientandroidlib.io.SessionInputBuffer;
+import ch.boye.httpclientandroidlib.io.SessionOutputBuffer;
+import ch.boye.httpclientandroidlib.params.BasicHttpParams;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.params.HttpProtocolParams;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Default implementation of an operated client connection.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link ManagedHttpClientConnectionFactory}.
+ */
+@NotThreadSafe // connSecure, targetHost
+@Deprecated
+public class DefaultClientConnection extends SocketHttpClientConnection
+ implements OperatedClientConnection, ManagedHttpClientConnection, HttpContext {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+ public HttpClientAndroidLog headerLog = new HttpClientAndroidLog("ch.boye.httpclientandroidlib.headers");
+ public HttpClientAndroidLog wireLog = new HttpClientAndroidLog("ch.boye.httpclientandroidlib.wire");
+
+ /** The unconnected socket */
+ private volatile Socket socket;
+
+ /** The target host of this connection. */
+ private HttpHost targetHost;
+
+ /** Whether this connection is secure. */
+ private boolean connSecure;
+
+ /** True if this connection was shutdown. */
+ private volatile boolean shutdown;
+
+ /** connection specific attributes */
+ private final Map<String, Object> attributes;
+
+ public DefaultClientConnection() {
+ super();
+ this.attributes = new HashMap<String, Object>();
+ }
+
+ public String getId() {
+ return null;
+ }
+
+ public final HttpHost getTargetHost() {
+ return this.targetHost;
+ }
+
+ public final boolean isSecure() {
+ return this.connSecure;
+ }
+
+ @Override
+ public final Socket getSocket() {
+ return this.socket;
+ }
+
+ public SSLSession getSSLSession() {
+ if (this.socket instanceof SSLSocket) {
+ return ((SSLSocket) this.socket).getSession();
+ } else {
+ return null;
+ }
+ }
+
+ public void opening(final Socket sock, final HttpHost target) throws IOException {
+ assertNotOpen();
+ this.socket = sock;
+ this.targetHost = target;
+
+ // Check for shutdown after assigning socket, so that
+ if (this.shutdown) {
+ sock.close(); // allow this to throw...
+ // ...but if it doesn't, explicitly throw one ourselves.
+ throw new InterruptedIOException("Connection already shutdown");
+ }
+ }
+
+ public void openCompleted(final boolean secure, final HttpParams params) throws IOException {
+ Args.notNull(params, "Parameters");
+ assertNotOpen();
+ this.connSecure = secure;
+ bind(this.socket, params);
+ }
+
+ /**
+ * Force-closes this connection.
+ * If the connection is still in the process of being open (the method
+ * {@link #opening opening} was already called but
+ * {@link #openCompleted openCompleted} was not), the associated
+ * socket that is being connected to a remote address will be closed.
+ * That will interrupt a thread that is blocked on connecting
+ * the socket.
+ * If the connection is not yet open, this will prevent the connection
+ * from being opened.
+ *
+ * @throws IOException in case of a problem
+ */
+ @Override
+ public void shutdown() throws IOException {
+ shutdown = true;
+ try {
+ super.shutdown();
+ if (log.isDebugEnabled()) {
+ log.debug("Connection " + this + " shut down");
+ }
+ final Socket sock = this.socket; // copy volatile attribute
+ if (sock != null) {
+ sock.close();
+ }
+ } catch (final IOException ex) {
+ log.debug("I/O error shutting down connection", ex);
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ super.close();
+ if (log.isDebugEnabled()) {
+ log.debug("Connection " + this + " closed");
+ }
+ } catch (final IOException ex) {
+ log.debug("I/O error closing connection", ex);
+ }
+ }
+
+ @Override
+ protected SessionInputBuffer createSessionInputBuffer(
+ final Socket socket,
+ final int buffersize,
+ final HttpParams params) throws IOException {
+ SessionInputBuffer inbuffer = super.createSessionInputBuffer(
+ socket,
+ buffersize > 0 ? buffersize : 8192,
+ params);
+ if (wireLog.isDebugEnabled()) {
+ inbuffer = new LoggingSessionInputBuffer(
+ inbuffer,
+ new Wire(wireLog),
+ HttpProtocolParams.getHttpElementCharset(params));
+ }
+ return inbuffer;
+ }
+
+ @Override
+ protected SessionOutputBuffer createSessionOutputBuffer(
+ final Socket socket,
+ final int buffersize,
+ final HttpParams params) throws IOException {
+ SessionOutputBuffer outbuffer = super.createSessionOutputBuffer(
+ socket,
+ buffersize > 0 ? buffersize : 8192,
+ params);
+ if (wireLog.isDebugEnabled()) {
+ outbuffer = new LoggingSessionOutputBuffer(
+ outbuffer,
+ new Wire(wireLog),
+ HttpProtocolParams.getHttpElementCharset(params));
+ }
+ return outbuffer;
+ }
+
+ @Override
+ protected HttpMessageParser<HttpResponse> createResponseParser(
+ final SessionInputBuffer buffer,
+ final HttpResponseFactory responseFactory,
+ final HttpParams params) {
+ // override in derived class to specify a line parser
+ return new DefaultHttpResponseParser
+ (buffer, null, responseFactory, params);
+ }
+
+ public void bind(final Socket socket) throws IOException {
+ bind(socket, new BasicHttpParams());
+ }
+
+ public void update(final Socket sock, final HttpHost target,
+ final boolean secure, final HttpParams params)
+ throws IOException {
+
+ assertOpen();
+ Args.notNull(target, "Target host");
+ Args.notNull(params, "Parameters");
+
+ if (sock != null) {
+ this.socket = sock;
+ bind(sock, params);
+ }
+ targetHost = target;
+ connSecure = secure;
+ }
+
+ @Override
+ public HttpResponse receiveResponseHeader() throws HttpException, IOException {
+ final HttpResponse response = super.receiveResponseHeader();
+ if (log.isDebugEnabled()) {
+ log.debug("Receiving response: " + response.getStatusLine());
+ }
+ if (headerLog.isDebugEnabled()) {
+ headerLog.debug("<< " + response.getStatusLine().toString());
+ final Header[] headers = response.getAllHeaders();
+ for (final Header header : headers) {
+ headerLog.debug("<< " + header.toString());
+ }
+ }
+ return response;
+ }
+
+ @Override
+ public void sendRequestHeader(final HttpRequest request) throws HttpException, IOException {
+ if (log.isDebugEnabled()) {
+ log.debug("Sending request: " + request.getRequestLine());
+ }
+ super.sendRequestHeader(request);
+ if (headerLog.isDebugEnabled()) {
+ headerLog.debug(">> " + request.getRequestLine().toString());
+ final Header[] headers = request.getAllHeaders();
+ for (final Header header : headers) {
+ headerLog.debug(">> " + header.toString());
+ }
+ }
+ }
+
+ public Object getAttribute(final String id) {
+ return this.attributes.get(id);
+ }
+
+ public Object removeAttribute(final String id) {
+ return this.attributes.remove(id);
+ }
+
+ public void setAttribute(final String id, final Object obj) {
+ this.attributes.put(id, obj);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultClientConnectionOperator.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultClientConnectionOperator.java
new file mode 100644
index 0000000000..3cdf706ec8
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultClientConnectionOperator.java
@@ -0,0 +1,263 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.io.IOException;
+import java.net.ConnectException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.client.protocol.ClientContext;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionOperator;
+import ch.boye.httpclientandroidlib.conn.ConnectTimeoutException;
+import ch.boye.httpclientandroidlib.conn.DnsResolver;
+import ch.boye.httpclientandroidlib.conn.HttpInetSocketAddress;
+import ch.boye.httpclientandroidlib.conn.OperatedClientConnection;
+import ch.boye.httpclientandroidlib.conn.scheme.Scheme;
+import ch.boye.httpclientandroidlib.conn.scheme.SchemeLayeredSocketFactory;
+import ch.boye.httpclientandroidlib.conn.scheme.SchemeRegistry;
+import ch.boye.httpclientandroidlib.conn.scheme.SchemeSocketFactory;
+import ch.boye.httpclientandroidlib.params.HttpConnectionParams;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+
+/**
+ * Default implementation of a {@link ClientConnectionOperator}. It uses a {@link SchemeRegistry}
+ * to look up {@link SchemeSocketFactory} objects.
+ * <p>
+ * This connection operator is multihome network aware and will attempt to retry failed connects
+ * against all known IP addresses sequentially until the connect is successful or all known
+ * addresses fail to respond. Please note the same
+ * {@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#CONNECTION_TIMEOUT} value will be used
+ * for each connection attempt, so in the worst case the total elapsed time before timeout
+ * can be <code>CONNECTION_TIMEOUT * n</code> where <code>n</code> is the number of IP addresses
+ * of the given host. One can disable multihome support by overriding
+ * the {@link #resolveHostname(String)} method and returning only one IP address for the given
+ * host name.
+ * <p>
+ * The following parameters can be used to customize the behavior of this
+ * class:
+ * <ul>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#HTTP_ELEMENT_CHARSET}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#SO_TIMEOUT}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#SO_LINGER}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#SO_REUSEADDR}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#TCP_NODELAY}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#SOCKET_BUFFER_SIZE}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#CONNECTION_TIMEOUT}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#MAX_LINE_LENGTH}</li>
+ * </ul>
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link PoolingHttpClientConnectionManager}.
+ */
+@Deprecated
+@ThreadSafe
+public class DefaultClientConnectionOperator implements ClientConnectionOperator {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ /** The scheme registry for looking up socket factories. */
+ protected final SchemeRegistry schemeRegistry; // @ThreadSafe
+
+ /** the custom-configured DNS lookup mechanism. */
+ protected final DnsResolver dnsResolver;
+
+ /**
+ * Creates a new client connection operator for the given scheme registry.
+ *
+ * @param schemes the scheme registry
+ *
+ * @since 4.2
+ */
+ public DefaultClientConnectionOperator(final SchemeRegistry schemes) {
+ Args.notNull(schemes, "Scheme registry");
+ this.schemeRegistry = schemes;
+ this.dnsResolver = new SystemDefaultDnsResolver();
+ }
+
+ /**
+ * Creates a new client connection operator for the given scheme registry
+ * and the given custom DNS lookup mechanism.
+ *
+ * @param schemes
+ * the scheme registry
+ * @param dnsResolver
+ * the custom DNS lookup mechanism
+ */
+ public DefaultClientConnectionOperator(final SchemeRegistry schemes,final DnsResolver dnsResolver) {
+ Args.notNull(schemes, "Scheme registry");
+
+ Args.notNull(dnsResolver, "DNS resolver");
+
+ this.schemeRegistry = schemes;
+ this.dnsResolver = dnsResolver;
+ }
+
+ public OperatedClientConnection createConnection() {
+ return new DefaultClientConnection();
+ }
+
+ private SchemeRegistry getSchemeRegistry(final HttpContext context) {
+ SchemeRegistry reg = (SchemeRegistry) context.getAttribute(
+ ClientContext.SCHEME_REGISTRY);
+ if (reg == null) {
+ reg = this.schemeRegistry;
+ }
+ return reg;
+ }
+
+ public void openConnection(
+ final OperatedClientConnection conn,
+ final HttpHost target,
+ final InetAddress local,
+ final HttpContext context,
+ final HttpParams params) throws IOException {
+ Args.notNull(conn, "Connection");
+ Args.notNull(target, "Target host");
+ Args.notNull(params, "HTTP parameters");
+ Asserts.check(!conn.isOpen(), "Connection must not be open");
+
+ final SchemeRegistry registry = getSchemeRegistry(context);
+ final Scheme schm = registry.getScheme(target.getSchemeName());
+ final SchemeSocketFactory sf = schm.getSchemeSocketFactory();
+
+ final InetAddress[] addresses = resolveHostname(target.getHostName());
+ final int port = schm.resolvePort(target.getPort());
+ for (int i = 0; i < addresses.length; i++) {
+ final InetAddress address = addresses[i];
+ final boolean last = i == addresses.length - 1;
+
+ Socket sock = sf.createSocket(params);
+ conn.opening(sock, target);
+
+ final InetSocketAddress remoteAddress = new HttpInetSocketAddress(target, address, port);
+ InetSocketAddress localAddress = null;
+ if (local != null) {
+ localAddress = new InetSocketAddress(local, 0);
+ }
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Connecting to " + remoteAddress);
+ }
+ try {
+ final Socket connsock = sf.connectSocket(sock, remoteAddress, localAddress, params);
+ if (sock != connsock) {
+ sock = connsock;
+ conn.opening(sock, target);
+ }
+ prepareSocket(sock, context, params);
+ conn.openCompleted(sf.isSecure(sock), params);
+ return;
+ } catch (final ConnectException ex) {
+ if (last) {
+ throw ex;
+ }
+ } catch (final ConnectTimeoutException ex) {
+ if (last) {
+ throw ex;
+ }
+ }
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Connect to " + remoteAddress + " timed out. " +
+ "Connection will be retried using another IP address");
+ }
+ }
+ }
+
+ public void updateSecureConnection(
+ final OperatedClientConnection conn,
+ final HttpHost target,
+ final HttpContext context,
+ final HttpParams params) throws IOException {
+ Args.notNull(conn, "Connection");
+ Args.notNull(target, "Target host");
+ Args.notNull(params, "Parameters");
+ Asserts.check(conn.isOpen(), "Connection must be open");
+
+ final SchemeRegistry registry = getSchemeRegistry(context);
+ final Scheme schm = registry.getScheme(target.getSchemeName());
+ Asserts.check(schm.getSchemeSocketFactory() instanceof SchemeLayeredSocketFactory,
+ "Socket factory must implement SchemeLayeredSocketFactory");
+ final SchemeLayeredSocketFactory lsf = (SchemeLayeredSocketFactory) schm.getSchemeSocketFactory();
+ final Socket sock = lsf.createLayeredSocket(
+ conn.getSocket(), target.getHostName(), schm.resolvePort(target.getPort()), params);
+ prepareSocket(sock, context, params);
+ conn.update(sock, target, lsf.isSecure(sock), params);
+ }
+
+ /**
+ * Performs standard initializations on a newly created socket.
+ *
+ * @param sock the socket to prepare
+ * @param context the context for the connection
+ * @param params the parameters from which to prepare the socket
+ *
+ * @throws IOException in case of an IO problem
+ */
+ protected void prepareSocket(
+ final Socket sock,
+ final HttpContext context,
+ final HttpParams params) throws IOException {
+ sock.setTcpNoDelay(HttpConnectionParams.getTcpNoDelay(params));
+ sock.setSoTimeout(HttpConnectionParams.getSoTimeout(params));
+
+ final int linger = HttpConnectionParams.getLinger(params);
+ if (linger >= 0) {
+ sock.setSoLinger(linger > 0, linger);
+ }
+ }
+
+ /**
+ * Resolves the given host name to an array of corresponding IP addresses, based on the
+ * configured name service on the provided DNS resolver. If one wasn't provided, the system
+ * configuration is used.
+ *
+ * @param host host name to resolve
+ * @return array of IP addresses
+ * @exception UnknownHostException if no IP address for the host could be determined.
+ *
+ * @see DnsResolver
+ * @see SystemDefaultDnsResolver
+ *
+ * @since 4.1
+ */
+ protected InetAddress[] resolveHostname(final String host) throws UnknownHostException {
+ return dnsResolver.resolve(host);
+ }
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultHttpResponseParser.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultHttpResponseParser.java
new file mode 100644
index 0000000000..9f249e5f7c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultHttpResponseParser.java
@@ -0,0 +1,168 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpResponseFactory;
+import ch.boye.httpclientandroidlib.NoHttpResponseException;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.StatusLine;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.config.MessageConstraints;
+import ch.boye.httpclientandroidlib.impl.DefaultHttpResponseFactory;
+import ch.boye.httpclientandroidlib.impl.io.AbstractMessageParser;
+import ch.boye.httpclientandroidlib.io.SessionInputBuffer;
+import ch.boye.httpclientandroidlib.message.LineParser;
+import ch.boye.httpclientandroidlib.message.ParserCursor;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Lenient HTTP response parser implementation that can skip malformed data until
+ * a valid HTTP response message head is encountered.
+ *
+ * @since 4.2
+ */
+@SuppressWarnings("deprecation")
+@NotThreadSafe
+public class DefaultHttpResponseParser extends AbstractMessageParser<HttpResponse> {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ private final HttpResponseFactory responseFactory;
+ private final CharArrayBuffer lineBuf;
+
+ /**
+ * @deprecated (4.3) use {@link DefaultHttpResponseParser#DefaultHttpResponseParser(
+ * SessionInputBuffer, LineParser, HttpResponseFactory, MessageConstraints)}
+ */
+ @Deprecated
+ public DefaultHttpResponseParser(
+ final SessionInputBuffer buffer,
+ final LineParser parser,
+ final HttpResponseFactory responseFactory,
+ final HttpParams params) {
+ super(buffer, parser, params);
+ Args.notNull(responseFactory, "Response factory");
+ this.responseFactory = responseFactory;
+ this.lineBuf = new CharArrayBuffer(128);
+ }
+
+ /**
+ * Creates new instance of DefaultHttpResponseParser.
+ *
+ * @param buffer the session input buffer.
+ * @param lineParser the line parser. If <code>null</code>
+ * {@link ch.boye.httpclientandroidlib.message.BasicLineParser#INSTANCE} will be used.
+ * @param responseFactory HTTP response factory. If <code>null</code>
+ * {@link DefaultHttpResponseFactory#INSTANCE} will be used.
+ * @param constraints the message constraints. If <code>null</code>
+ * {@link MessageConstraints#DEFAULT} will be used.
+ *
+ * @since 4.3
+ */
+ public DefaultHttpResponseParser(
+ final SessionInputBuffer buffer,
+ final LineParser lineParser,
+ final HttpResponseFactory responseFactory,
+ final MessageConstraints constraints) {
+ super(buffer, lineParser, constraints);
+ this.responseFactory = responseFactory != null ? responseFactory :
+ DefaultHttpResponseFactory.INSTANCE;
+ this.lineBuf = new CharArrayBuffer(128);
+ }
+
+ /**
+ * Creates new instance of DefaultHttpResponseParser.
+ *
+ * @param buffer the session input buffer.
+ * @param constraints the message constraints. If <code>null</code>
+ * {@link MessageConstraints#DEFAULT} will be used.
+ *
+ * @since 4.3
+ */
+ public DefaultHttpResponseParser(
+ final SessionInputBuffer buffer, final MessageConstraints constraints) {
+ this(buffer, null, null, constraints);
+ }
+
+ /**
+ * Creates new instance of DefaultHttpResponseParser.
+ *
+ * @param buffer the session input buffer.
+ *
+ * @since 4.3
+ */
+ public DefaultHttpResponseParser(final SessionInputBuffer buffer) {
+ this(buffer, null, null, MessageConstraints.DEFAULT);
+ }
+
+ @Override
+ protected HttpResponse parseHead(
+ final SessionInputBuffer sessionBuffer) throws IOException, HttpException {
+ //read out the HTTP status string
+ int count = 0;
+ ParserCursor cursor = null;
+ do {
+ // clear the buffer
+ this.lineBuf.clear();
+ final int i = sessionBuffer.readLine(this.lineBuf);
+ if (i == -1 && count == 0) {
+ // The server just dropped connection on us
+ throw new NoHttpResponseException("The target server failed to respond");
+ }
+ cursor = new ParserCursor(0, this.lineBuf.length());
+ if (lineParser.hasProtocolVersion(this.lineBuf, cursor)) {
+ // Got one
+ break;
+ } else if (i == -1 || reject(this.lineBuf, count)) {
+ // Giving up
+ throw new ProtocolException("The server failed to respond with a " +
+ "valid HTTP response");
+ }
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Garbage in response: " + this.lineBuf.toString());
+ }
+ count++;
+ } while(true);
+ //create the status line from the status string
+ final StatusLine statusline = lineParser.parseStatusLine(this.lineBuf, cursor);
+ return this.responseFactory.newHttpResponse(statusline, null);
+ }
+
+ protected boolean reject(final CharArrayBuffer line, final int count) {
+ return false;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultHttpResponseParserFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultHttpResponseParserFactory.java
new file mode 100644
index 0000000000..a229b2ca67
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultHttpResponseParserFactory.java
@@ -0,0 +1,77 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpResponseFactory;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.config.MessageConstraints;
+import ch.boye.httpclientandroidlib.impl.DefaultHttpResponseFactory;
+import ch.boye.httpclientandroidlib.io.HttpMessageParser;
+import ch.boye.httpclientandroidlib.io.HttpMessageParserFactory;
+import ch.boye.httpclientandroidlib.io.SessionInputBuffer;
+import ch.boye.httpclientandroidlib.message.BasicLineParser;
+import ch.boye.httpclientandroidlib.message.LineParser;
+
+/**
+ * Default factory for response message parsers.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class DefaultHttpResponseParserFactory implements HttpMessageParserFactory<HttpResponse> {
+
+ public static final DefaultHttpResponseParserFactory INSTANCE = new DefaultHttpResponseParserFactory();
+
+ private final LineParser lineParser;
+ private final HttpResponseFactory responseFactory;
+
+ public DefaultHttpResponseParserFactory(
+ final LineParser lineParser,
+ final HttpResponseFactory responseFactory) {
+ super();
+ this.lineParser = lineParser != null ? lineParser : BasicLineParser.INSTANCE;
+ this.responseFactory = responseFactory != null ? responseFactory
+ : DefaultHttpResponseFactory.INSTANCE;
+ }
+
+ public DefaultHttpResponseParserFactory(
+ final HttpResponseFactory responseFactory) {
+ this(null, responseFactory);
+ }
+
+ public DefaultHttpResponseParserFactory() {
+ this(null, null);
+ }
+
+ public HttpMessageParser<HttpResponse> create(final SessionInputBuffer buffer,
+ final MessageConstraints constraints) {
+ return new DefaultHttpResponseParser(buffer, lineParser, responseFactory, constraints);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultHttpRoutePlanner.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultHttpRoutePlanner.java
new file mode 100644
index 0000000000..4b35253298
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultHttpRoutePlanner.java
@@ -0,0 +1,123 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.conn;
+
+
+import java.net.InetAddress;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.conn.params.ConnRouteParams;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoutePlanner;
+import ch.boye.httpclientandroidlib.conn.scheme.Scheme;
+import ch.boye.httpclientandroidlib.conn.scheme.SchemeRegistry;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+
+/**
+ * Default implementation of an {@link HttpRoutePlanner}. This implementation
+ * is based on {@link ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames parameters}.
+ * It will not make use of any Java system properties, nor of system or
+ * browser proxy settings.
+ * <p>
+ * The following parameters can be used to customize the behavior of this
+ * class:
+ * <ul>
+ * <li>{@link ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames#DEFAULT_PROXY}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames#LOCAL_ADDRESS}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames#FORCED_ROUTE}</li>
+ * </ul>
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link DefaultRoutePlanner}
+ */
+@ThreadSafe
+@Deprecated
+public class DefaultHttpRoutePlanner implements HttpRoutePlanner {
+
+ /** The scheme registry. */
+ protected final SchemeRegistry schemeRegistry; // class is @ThreadSafe
+
+ /**
+ * Creates a new default route planner.
+ *
+ * @param schreg the scheme registry
+ */
+ public DefaultHttpRoutePlanner(final SchemeRegistry schreg) {
+ Args.notNull(schreg, "Scheme registry");
+ schemeRegistry = schreg;
+ }
+
+ public HttpRoute determineRoute(final HttpHost target,
+ final HttpRequest request,
+ final HttpContext context)
+ throws HttpException {
+
+ Args.notNull(request, "HTTP request");
+
+ // If we have a forced route, we can do without a target.
+ HttpRoute route =
+ ConnRouteParams.getForcedRoute(request.getParams());
+ if (route != null) {
+ return route;
+ }
+
+ // If we get here, there is no forced route.
+ // So we need a target to compute a route.
+
+ Asserts.notNull(target, "Target host");
+
+ final InetAddress local =
+ ConnRouteParams.getLocalAddress(request.getParams());
+ final HttpHost proxy =
+ ConnRouteParams.getDefaultProxy(request.getParams());
+
+ final Scheme schm;
+ try {
+ schm = this.schemeRegistry.getScheme(target.getSchemeName());
+ } catch (final IllegalStateException ex) {
+ throw new HttpException(ex.getMessage());
+ }
+ // as it is typically used for TLS/SSL, we assume that
+ // a layered scheme implies a secure connection
+ final boolean secure = schm.isLayered();
+
+ if (proxy == null) {
+ route = new HttpRoute(target, local, secure);
+ } else {
+ route = new HttpRoute(target, local, proxy, secure);
+ }
+ return route;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultManagedHttpClientConnection.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultManagedHttpClientConnection.java
new file mode 100644
index 0000000000..c46b934059
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultManagedHttpClientConnection.java
@@ -0,0 +1,135 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.Socket;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CharsetEncoder;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.config.MessageConstraints;
+import ch.boye.httpclientandroidlib.conn.ManagedHttpClientConnection;
+import ch.boye.httpclientandroidlib.entity.ContentLengthStrategy;
+import ch.boye.httpclientandroidlib.impl.DefaultBHttpClientConnection;
+import ch.boye.httpclientandroidlib.io.HttpMessageParserFactory;
+import ch.boye.httpclientandroidlib.io.HttpMessageWriterFactory;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * Default {@link ManagedHttpClientConnection} implementation.
+ * @since 4.3
+ */
+@NotThreadSafe
+public class DefaultManagedHttpClientConnection extends DefaultBHttpClientConnection
+ implements ManagedHttpClientConnection, HttpContext {
+
+ private final String id;
+ private final Map<String, Object> attributes;
+
+ private volatile boolean shutdown;
+
+ public DefaultManagedHttpClientConnection(
+ final String id,
+ final int buffersize,
+ final int fragmentSizeHint,
+ final CharsetDecoder chardecoder,
+ final CharsetEncoder charencoder,
+ final MessageConstraints constraints,
+ final ContentLengthStrategy incomingContentStrategy,
+ final ContentLengthStrategy outgoingContentStrategy,
+ final HttpMessageWriterFactory<HttpRequest> requestWriterFactory,
+ final HttpMessageParserFactory<HttpResponse> responseParserFactory) {
+ super(buffersize, fragmentSizeHint, chardecoder, charencoder,
+ constraints, incomingContentStrategy, outgoingContentStrategy,
+ requestWriterFactory, responseParserFactory);
+ this.id = id;
+ this.attributes = new ConcurrentHashMap<String, Object>();
+ }
+
+ public DefaultManagedHttpClientConnection(
+ final String id,
+ final int buffersize) {
+ this(id, buffersize, buffersize, null, null, null, null, null, null, null);
+ }
+
+ public String getId() {
+ return this.id;
+ }
+
+ @Override
+ public void shutdown() throws IOException {
+ this.shutdown = true;
+ super.shutdown();
+ }
+
+ public Object getAttribute(final String id) {
+ return this.attributes.get(id);
+ }
+
+ public Object removeAttribute(final String id) {
+ return this.attributes.remove(id);
+ }
+
+ public void setAttribute(final String id, final Object obj) {
+ this.attributes.put(id, obj);
+ }
+
+ @Override
+ public void bind(final Socket socket) throws IOException {
+ if (this.shutdown) {
+ socket.close(); // allow this to throw...
+ // ...but if it doesn't, explicitly throw one ourselves.
+ throw new InterruptedIOException("Connection already shutdown");
+ }
+ super.bind(socket);
+ }
+
+ @Override
+ public Socket getSocket() {
+ return super.getSocket();
+ }
+
+ public SSLSession getSSLSession() {
+ final Socket socket = super.getSocket();
+ if (socket instanceof SSLSocket) {
+ return ((SSLSocket) socket).getSession();
+ } else {
+ return null;
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultProxyRoutePlanner.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultProxyRoutePlanner.java
new file mode 100644
index 0000000000..fd569556ce
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultProxyRoutePlanner.java
@@ -0,0 +1,66 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.conn.SchemePortResolver;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Implementation of an {@link ch.boye.httpclientandroidlib.conn.routing.HttpRoutePlanner}
+ * that routes requests through a default proxy.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class DefaultProxyRoutePlanner extends DefaultRoutePlanner {
+
+ private final HttpHost proxy;
+
+ public DefaultProxyRoutePlanner(final HttpHost proxy, final SchemePortResolver schemePortResolver) {
+ super(schemePortResolver);
+ this.proxy = Args.notNull(proxy, "Proxy host");
+ }
+
+ public DefaultProxyRoutePlanner(final HttpHost proxy) {
+ this(proxy, null);
+ }
+
+ @Override
+ protected HttpHost determineProxy(
+ final HttpHost target,
+ final HttpRequest request,
+ final HttpContext context) throws HttpException {
+ return proxy;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultResponseParser.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultResponseParser.java
new file mode 100644
index 0000000000..e82452ff5c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultResponseParser.java
@@ -0,0 +1,125 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpMessage;
+import ch.boye.httpclientandroidlib.HttpResponseFactory;
+import ch.boye.httpclientandroidlib.NoHttpResponseException;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.StatusLine;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.impl.io.AbstractMessageParser;
+import ch.boye.httpclientandroidlib.io.SessionInputBuffer;
+import ch.boye.httpclientandroidlib.message.LineParser;
+import ch.boye.httpclientandroidlib.message.ParserCursor;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Default HTTP response parser implementation.
+ * <p>
+ * The following parameters can be used to customize the behavior of this
+ * class:
+ * <ul>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#MAX_HEADER_COUNT}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#MAX_LINE_LENGTH}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.conn.params.ConnConnectionPNames#MAX_STATUS_LINE_GARBAGE}</li>
+ * </ul>
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) use {@link DefaultHttpResponseParser}
+ */
+@Deprecated
+@ThreadSafe // no public methods
+public class DefaultResponseParser extends AbstractMessageParser<HttpMessage> {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ private final HttpResponseFactory responseFactory;
+ private final CharArrayBuffer lineBuf;
+ private final int maxGarbageLines;
+
+ public DefaultResponseParser(
+ final SessionInputBuffer buffer,
+ final LineParser parser,
+ final HttpResponseFactory responseFactory,
+ final HttpParams params) {
+ super(buffer, parser, params);
+ Args.notNull(responseFactory, "Response factory");
+ this.responseFactory = responseFactory;
+ this.lineBuf = new CharArrayBuffer(128);
+ this.maxGarbageLines = getMaxGarbageLines(params);
+ }
+
+ protected int getMaxGarbageLines(final HttpParams params) {
+ return params.getIntParameter(
+ ch.boye.httpclientandroidlib.conn.params.ConnConnectionPNames.MAX_STATUS_LINE_GARBAGE,
+ Integer.MAX_VALUE);
+ }
+
+ @Override
+ protected HttpMessage parseHead(
+ final SessionInputBuffer sessionBuffer) throws IOException, HttpException {
+ //read out the HTTP status string
+ int count = 0;
+ ParserCursor cursor = null;
+ do {
+ // clear the buffer
+ this.lineBuf.clear();
+ final int i = sessionBuffer.readLine(this.lineBuf);
+ if (i == -1 && count == 0) {
+ // The server just dropped connection on us
+ throw new NoHttpResponseException("The target server failed to respond");
+ }
+ cursor = new ParserCursor(0, this.lineBuf.length());
+ if (lineParser.hasProtocolVersion(this.lineBuf, cursor)) {
+ // Got one
+ break;
+ } else if (i == -1 || count >= this.maxGarbageLines) {
+ // Giving up
+ throw new ProtocolException("The server failed to respond with a " +
+ "valid HTTP response");
+ }
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Garbage in response: " + this.lineBuf.toString());
+ }
+ count++;
+ } while(true);
+ //create the status line from the status string
+ final StatusLine statusline = lineParser.parseStatusLine(this.lineBuf, cursor);
+ return this.responseFactory.newHttpResponse(statusline, null);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultRoutePlanner.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultRoutePlanner.java
new file mode 100644
index 0000000000..2c08a7118f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultRoutePlanner.java
@@ -0,0 +1,107 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.net.InetAddress;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.config.RequestConfig;
+import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
+import ch.boye.httpclientandroidlib.conn.SchemePortResolver;
+import ch.boye.httpclientandroidlib.conn.UnsupportedSchemeException;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoutePlanner;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Default implementation of an {@link HttpRoutePlanner}. It will not make use of
+ * any Java system properties, nor of system or browser proxy settings.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class DefaultRoutePlanner implements HttpRoutePlanner {
+
+ private final SchemePortResolver schemePortResolver;
+
+ public DefaultRoutePlanner(final SchemePortResolver schemePortResolver) {
+ super();
+ this.schemePortResolver = schemePortResolver != null ? schemePortResolver :
+ DefaultSchemePortResolver.INSTANCE;
+ }
+
+ public HttpRoute determineRoute(
+ final HttpHost host,
+ final HttpRequest request,
+ final HttpContext context) throws HttpException {
+ Args.notNull(request, "Request");
+ if (host == null) {
+ throw new ProtocolException("Target host is not specified");
+ }
+ final HttpClientContext clientContext = HttpClientContext.adapt(context);
+ final RequestConfig config = clientContext.getRequestConfig();
+ final InetAddress local = config.getLocalAddress();
+ HttpHost proxy = config.getProxy();
+ if (proxy == null) {
+ proxy = determineProxy(host, request, context);
+ }
+
+ final HttpHost target;
+ if (host.getPort() <= 0) {
+ try {
+ target = new HttpHost(
+ host.getHostName(),
+ this.schemePortResolver.resolve(host),
+ host.getSchemeName());
+ } catch (final UnsupportedSchemeException ex) {
+ throw new HttpException(ex.getMessage());
+ }
+ } else {
+ target = host;
+ }
+ final boolean secure = target.getSchemeName().equalsIgnoreCase("https");
+ if (proxy == null) {
+ return new HttpRoute(target, local, secure);
+ } else {
+ return new HttpRoute(target, local, proxy, secure);
+ }
+ }
+
+ protected HttpHost determineProxy(
+ final HttpHost target,
+ final HttpRequest request,
+ final HttpContext context) throws HttpException {
+ return null;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultSchemePortResolver.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultSchemePortResolver.java
new file mode 100644
index 0000000000..4b384f6811
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/DefaultSchemePortResolver.java
@@ -0,0 +1,61 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.conn.SchemePortResolver;
+import ch.boye.httpclientandroidlib.conn.UnsupportedSchemeException;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Default {@link SchemePortResolver}.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class DefaultSchemePortResolver implements SchemePortResolver {
+
+ public static final DefaultSchemePortResolver INSTANCE = new DefaultSchemePortResolver();
+
+ public int resolve(final HttpHost host) throws UnsupportedSchemeException {
+ Args.notNull(host, "HTTP host");
+ final int port = host.getPort();
+ if (port > 0) {
+ return port;
+ }
+ final String name = host.getSchemeName();
+ if (name.equalsIgnoreCase("http")) {
+ return 80;
+ } else if (name.equalsIgnoreCase("https")) {
+ return 443;
+ } else {
+ throw new UnsupportedSchemeException(name + " protocol is not supported");
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/HttpClientConnectionOperator.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/HttpClientConnectionOperator.java
new file mode 100644
index 0000000000..a133682374
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/HttpClientConnectionOperator.java
@@ -0,0 +1,173 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.io.IOException;
+import java.net.ConnectException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
+import ch.boye.httpclientandroidlib.config.Lookup;
+import ch.boye.httpclientandroidlib.config.SocketConfig;
+import ch.boye.httpclientandroidlib.conn.ConnectTimeoutException;
+import ch.boye.httpclientandroidlib.conn.DnsResolver;
+import ch.boye.httpclientandroidlib.conn.HttpHostConnectException;
+import ch.boye.httpclientandroidlib.conn.ManagedHttpClientConnection;
+import ch.boye.httpclientandroidlib.conn.SchemePortResolver;
+import ch.boye.httpclientandroidlib.conn.UnsupportedSchemeException;
+import ch.boye.httpclientandroidlib.conn.socket.ConnectionSocketFactory;
+import ch.boye.httpclientandroidlib.conn.socket.LayeredConnectionSocketFactory;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+
+@Immutable
+class HttpClientConnectionOperator {
+
+ static final String SOCKET_FACTORY_REGISTRY = "http.socket-factory-registry";
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ private final Lookup<ConnectionSocketFactory> socketFactoryRegistry;
+ private final SchemePortResolver schemePortResolver;
+ private final DnsResolver dnsResolver;
+
+ HttpClientConnectionOperator(
+ final Lookup<ConnectionSocketFactory> socketFactoryRegistry,
+ final SchemePortResolver schemePortResolver,
+ final DnsResolver dnsResolver) {
+ super();
+ Args.notNull(socketFactoryRegistry, "Socket factory registry");
+ this.socketFactoryRegistry = socketFactoryRegistry;
+ this.schemePortResolver = schemePortResolver != null ? schemePortResolver :
+ DefaultSchemePortResolver.INSTANCE;
+ this.dnsResolver = dnsResolver != null ? dnsResolver :
+ SystemDefaultDnsResolver.INSTANCE;
+ }
+
+ @SuppressWarnings("unchecked")
+ private Lookup<ConnectionSocketFactory> getSocketFactoryRegistry(final HttpContext context) {
+ Lookup<ConnectionSocketFactory> reg = (Lookup<ConnectionSocketFactory>) context.getAttribute(
+ SOCKET_FACTORY_REGISTRY);
+ if (reg == null) {
+ reg = this.socketFactoryRegistry;
+ }
+ return reg;
+ }
+
+ public void connect(
+ final ManagedHttpClientConnection conn,
+ final HttpHost host,
+ final InetSocketAddress localAddress,
+ final int connectTimeout,
+ final SocketConfig socketConfig,
+ final HttpContext context) throws IOException {
+ final Lookup<ConnectionSocketFactory> registry = getSocketFactoryRegistry(context);
+ final ConnectionSocketFactory sf = registry.lookup(host.getSchemeName());
+ if (sf == null) {
+ throw new UnsupportedSchemeException(host.getSchemeName() +
+ " protocol is not supported");
+ }
+ final InetAddress[] addresses = this.dnsResolver.resolve(host.getHostName());
+ final int port = this.schemePortResolver.resolve(host);
+ for (int i = 0; i < addresses.length; i++) {
+ final InetAddress address = addresses[i];
+ final boolean last = i == addresses.length - 1;
+
+ Socket sock = sf.createSocket(context);
+ sock.setSoTimeout(socketConfig.getSoTimeout());
+ sock.setReuseAddress(socketConfig.isSoReuseAddress());
+ sock.setTcpNoDelay(socketConfig.isTcpNoDelay());
+ sock.setKeepAlive(socketConfig.isSoKeepAlive());
+ final int linger = socketConfig.getSoLinger();
+ if (linger >= 0) {
+ sock.setSoLinger(linger > 0, linger);
+ }
+ conn.bind(sock);
+
+ final InetSocketAddress remoteAddress = new InetSocketAddress(address, port);
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Connecting to " + remoteAddress);
+ }
+ try {
+ sock = sf.connectSocket(
+ connectTimeout, sock, host, remoteAddress, localAddress, context);
+ conn.bind(sock);
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Connection established " + conn);
+ }
+ return;
+ } catch (final SocketTimeoutException ex) {
+ if (last) {
+ throw new ConnectTimeoutException(ex, host, addresses);
+ }
+ } catch (final ConnectException ex) {
+ if (last) {
+ final String msg = ex.getMessage();
+ if ("Connection timed out".equals(msg)) {
+ throw new ConnectTimeoutException(ex, host, addresses);
+ } else {
+ throw new HttpHostConnectException(ex, host, addresses);
+ }
+ }
+ }
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Connect to " + remoteAddress + " timed out. " +
+ "Connection will be retried using another IP address");
+ }
+ }
+ }
+
+ public void upgrade(
+ final ManagedHttpClientConnection conn,
+ final HttpHost host,
+ final HttpContext context) throws IOException {
+ final HttpClientContext clientContext = HttpClientContext.adapt(context);
+ final Lookup<ConnectionSocketFactory> registry = getSocketFactoryRegistry(clientContext);
+ final ConnectionSocketFactory sf = registry.lookup(host.getSchemeName());
+ if (sf == null) {
+ throw new UnsupportedSchemeException(host.getSchemeName() +
+ " protocol is not supported");
+ }
+ if (!(sf instanceof LayeredConnectionSocketFactory)) {
+ throw new UnsupportedSchemeException(host.getSchemeName() +
+ " protocol does not support connection upgrade");
+ }
+ final LayeredConnectionSocketFactory lsf = (LayeredConnectionSocketFactory) sf;
+ Socket sock = conn.getSocket();
+ final int port = this.schemePortResolver.resolve(host);
+ sock = lsf.createLayeredSocket(sock, host.getHostName(), port, context);
+ conn.bind(sock);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/HttpConnPool.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/HttpConnPool.java
new file mode 100644
index 0000000000..e10bd6cf6e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/HttpConnPool.java
@@ -0,0 +1,84 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionOperator;
+import ch.boye.httpclientandroidlib.conn.OperatedClientConnection;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.pool.AbstractConnPool;
+import ch.boye.httpclientandroidlib.pool.ConnFactory;
+
+/**
+ * @since 4.2
+ *
+ * @deprecated (4.3) no longer used.
+ */
+@Deprecated
+class HttpConnPool extends AbstractConnPool<HttpRoute, OperatedClientConnection, HttpPoolEntry> {
+
+ private static final AtomicLong COUNTER = new AtomicLong();
+
+ public HttpClientAndroidLog log;
+ private final long timeToLive;
+ private final TimeUnit tunit;
+
+ public HttpConnPool(final HttpClientAndroidLog log,
+ final ClientConnectionOperator connOperator,
+ final int defaultMaxPerRoute, final int maxTotal,
+ final long timeToLive, final TimeUnit tunit) {
+ super(new InternalConnFactory(connOperator), defaultMaxPerRoute, maxTotal);
+ this.log = log;
+ this.timeToLive = timeToLive;
+ this.tunit = tunit;
+ }
+
+ @Override
+ protected HttpPoolEntry createEntry(final HttpRoute route, final OperatedClientConnection conn) {
+ final String id = Long.toString(COUNTER.getAndIncrement());
+ return new HttpPoolEntry(this.log, id, route, conn, this.timeToLive, this.tunit);
+ }
+
+ static class InternalConnFactory implements ConnFactory<HttpRoute, OperatedClientConnection> {
+
+ private final ClientConnectionOperator connOperator;
+
+ InternalConnFactory(final ClientConnectionOperator connOperator) {
+ this.connOperator = connOperator;
+ }
+
+ public OperatedClientConnection create(final HttpRoute route) throws IOException {
+ return connOperator.createConnection();
+ }
+
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/HttpPoolEntry.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/HttpPoolEntry.java
new file mode 100644
index 0000000000..d040231265
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/HttpPoolEntry.java
@@ -0,0 +1,98 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+import ch.boye.httpclientandroidlib.conn.OperatedClientConnection;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.conn.routing.RouteTracker;
+import ch.boye.httpclientandroidlib.pool.PoolEntry;
+
+/**
+ * @since 4.2
+ *
+ * @deprecated (4.3) no longer used.
+ */
+@Deprecated
+class HttpPoolEntry extends PoolEntry<HttpRoute, OperatedClientConnection> {
+
+ public HttpClientAndroidLog log;
+ private final RouteTracker tracker;
+
+ public HttpPoolEntry(
+ final HttpClientAndroidLog log,
+ final String id,
+ final HttpRoute route,
+ final OperatedClientConnection conn,
+ final long timeToLive, final TimeUnit tunit) {
+ super(id, route, conn, timeToLive, tunit);
+ this.log = log;
+ this.tracker = new RouteTracker(route);
+ }
+
+ @Override
+ public boolean isExpired(final long now) {
+ final boolean expired = super.isExpired(now);
+ if (expired && this.log.isDebugEnabled()) {
+ this.log.debug("Connection " + this + " expired @ " + new Date(getExpiry()));
+ }
+ return expired;
+ }
+
+ RouteTracker getTracker() {
+ return this.tracker;
+ }
+
+ HttpRoute getPlannedRoute() {
+ return getRoute();
+ }
+
+ HttpRoute getEffectiveRoute() {
+ return this.tracker.toRoute();
+ }
+
+ @Override
+ public boolean isClosed() {
+ final OperatedClientConnection conn = getConnection();
+ return !conn.isOpen();
+ }
+
+ @Override
+ public void close() {
+ final OperatedClientConnection conn = getConnection();
+ try {
+ conn.close();
+ } catch (final IOException ex) {
+ this.log.debug("I/O error closing connection", ex);
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/IdleConnectionHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/IdleConnectionHandler.java
new file mode 100644
index 0000000000..cbcd5d04f2
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/IdleConnectionHandler.java
@@ -0,0 +1,181 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.TimeUnit;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.HttpConnection;
+
+// Currently only used by AbstractConnPool
+/**
+ * A helper class for connection managers to track idle connections.
+ *
+ * <p>This class is not synchronized.</p>
+ *
+ * @see ch.boye.httpclientandroidlib.conn.ClientConnectionManager#closeIdleConnections
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.1) no longer used
+ */
+@Deprecated
+public class IdleConnectionHandler {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ /** Holds connections and the time they were added. */
+ private final Map<HttpConnection,TimeValues> connectionToTimes;
+
+
+ public IdleConnectionHandler() {
+ super();
+ connectionToTimes = new HashMap<HttpConnection,TimeValues>();
+ }
+
+ /**
+ * Registers the given connection with this handler. The connection will be held until
+ * {@link #remove} or {@link #closeIdleConnections} is called.
+ *
+ * @param connection the connection to add
+ *
+ * @see #remove
+ */
+ public void add(final HttpConnection connection, final long validDuration, final TimeUnit unit) {
+
+ final long timeAdded = System.currentTimeMillis();
+
+ if (log.isDebugEnabled()) {
+ log.debug("Adding connection at: " + timeAdded);
+ }
+
+ connectionToTimes.put(connection, new TimeValues(timeAdded, validDuration, unit));
+ }
+
+ /**
+ * Removes the given connection from the list of connections to be closed when idle.
+ * This will return true if the connection is still valid, and false
+ * if the connection should be considered expired and not used.
+ *
+ * @param connection
+ * @return True if the connection is still valid.
+ */
+ public boolean remove(final HttpConnection connection) {
+ final TimeValues times = connectionToTimes.remove(connection);
+ if(times == null) {
+ log.warn("Removing a connection that never existed!");
+ return true;
+ } else {
+ return System.currentTimeMillis() <= times.timeExpires;
+ }
+ }
+
+ /**
+ * Removes all connections referenced by this handler.
+ */
+ public void removeAll() {
+ this.connectionToTimes.clear();
+ }
+
+ /**
+ * Closes connections that have been idle for at least the given amount of time.
+ *
+ * @param idleTime the minimum idle time, in milliseconds, for connections to be closed
+ */
+ public void closeIdleConnections(final long idleTime) {
+
+ // the latest time for which connections will be closed
+ final long idleTimeout = System.currentTimeMillis() - idleTime;
+
+ if (log.isDebugEnabled()) {
+ log.debug("Checking for connections, idle timeout: " + idleTimeout);
+ }
+
+ for (final Entry<HttpConnection, TimeValues> entry : connectionToTimes.entrySet()) {
+ final HttpConnection conn = entry.getKey();
+ final TimeValues times = entry.getValue();
+ final long connectionTime = times.timeAdded;
+ if (connectionTime <= idleTimeout) {
+ if (log.isDebugEnabled()) {
+ log.debug("Closing idle connection, connection time: " + connectionTime);
+ }
+ try {
+ conn.close();
+ } catch (final IOException ex) {
+ log.debug("I/O error closing connection", ex);
+ }
+ }
+ }
+ }
+
+
+ public void closeExpiredConnections() {
+ final long now = System.currentTimeMillis();
+ if (log.isDebugEnabled()) {
+ log.debug("Checking for expired connections, now: " + now);
+ }
+
+ for (final Entry<HttpConnection, TimeValues> entry : connectionToTimes.entrySet()) {
+ final HttpConnection conn = entry.getKey();
+ final TimeValues times = entry.getValue();
+ if(times.timeExpires <= now) {
+ if (log.isDebugEnabled()) {
+ log.debug("Closing connection, expired @: " + times.timeExpires);
+ }
+ try {
+ conn.close();
+ } catch (final IOException ex) {
+ log.debug("I/O error closing connection", ex);
+ }
+ }
+ }
+ }
+
+ private static class TimeValues {
+ private final long timeAdded;
+ private final long timeExpires;
+
+ /**
+ * @param now The current time in milliseconds
+ * @param validDuration The duration this connection is valid for
+ * @param validUnit The unit of time the duration is specified in.
+ */
+ TimeValues(final long now, final long validDuration, final TimeUnit validUnit) {
+ this.timeAdded = now;
+ if(validDuration > 0) {
+ this.timeExpires = now + validUnit.toMillis(validDuration);
+ } else {
+ this.timeExpires = Long.MAX_VALUE;
+ }
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/InMemoryDnsResolver.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/InMemoryDnsResolver.java
new file mode 100644
index 0000000000..34f02b11b3
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/InMemoryDnsResolver.java
@@ -0,0 +1,94 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.conn.DnsResolver;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * In-memory {@link DnsResolver} implementation.
+ *
+ * @since 4.2
+ */
+public class InMemoryDnsResolver implements DnsResolver {
+
+ /** Logger associated to this class. */
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(InMemoryDnsResolver.class);
+
+ /**
+ * In-memory collection that will hold the associations between a host name
+ * and an array of InetAddress instances.
+ */
+ private final Map<String, InetAddress[]> dnsMap;
+
+ /**
+ * Builds a DNS resolver that will resolve the host names against a
+ * collection held in-memory.
+ */
+ public InMemoryDnsResolver() {
+ dnsMap = new ConcurrentHashMap<String, InetAddress[]>();
+ }
+
+ /**
+ * Associates the given array of IP addresses to the given host in this DNS overrider.
+ * The IP addresses are assumed to be already resolved.
+ *
+ * @param host
+ * The host name to be associated with the given IP.
+ * @param ips
+ * array of IP addresses to be resolved by this DNS overrider to the given
+ * host name.
+ */
+ public void add(final String host, final InetAddress... ips) {
+ Args.notNull(host, "Host name");
+ Args.notNull(ips, "Array of IP addresses");
+ dnsMap.put(host, ips);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public InetAddress[] resolve(final String host) throws UnknownHostException {
+ final InetAddress[] resolvedAddresses = dnsMap.get(host);
+ if (log.isInfoEnabled()) {
+ log.info("Resolving " + host + " to " + Arrays.deepToString(resolvedAddresses));
+ }
+ if(resolvedAddresses == null){
+ throw new UnknownHostException(host + " cannot be resolved");
+ }
+ return resolvedAddresses;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/LoggingInputStream.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/LoggingInputStream.java
new file mode 100644
index 0000000000..0c117395cb
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/LoggingInputStream.java
@@ -0,0 +1,145 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Internal class.
+ *
+ * @since 4.3
+ */
+@NotThreadSafe
+class LoggingInputStream extends InputStream {
+
+ private final InputStream in;
+ private final Wire wire;
+
+ public LoggingInputStream(final InputStream in, final Wire wire) {
+ super();
+ this.in = in;
+ this.wire = wire;
+ }
+
+ @Override
+ public int read() throws IOException {
+ try {
+ final int b = in.read();
+ if (b == -1) {
+ wire.input("end of stream");
+ } else {
+ wire.input(b);
+ }
+ return b;
+ } catch (IOException ex) {
+ wire.input("[read] I/O error: " + ex.getMessage());
+ throw ex;
+ }
+ }
+
+ @Override
+ public int read(final byte[] b) throws IOException {
+ try {
+ final int bytesRead = in.read(b);
+ if (bytesRead == -1) {
+ wire.input("end of stream");
+ } else if (bytesRead > 0) {
+ wire.input(b, 0, bytesRead);
+ }
+ return bytesRead;
+ } catch (IOException ex) {
+ wire.input("[read] I/O error: " + ex.getMessage());
+ throw ex;
+ }
+ }
+
+ @Override
+ public int read(final byte[] b, final int off, final int len) throws IOException {
+ try {
+ final int bytesRead = in.read(b, off, len);
+ if (bytesRead == -1) {
+ wire.input("end of stream");
+ } else if (bytesRead > 0) {
+ wire.input(b, off, bytesRead);
+ }
+ return bytesRead;
+ } catch (IOException ex) {
+ wire.input("[read] I/O error: " + ex.getMessage());
+ throw ex;
+ }
+ }
+
+ @Override
+ public long skip(final long n) throws IOException {
+ try {
+ return super.skip(n);
+ } catch (IOException ex) {
+ wire.input("[skip] I/O error: " + ex.getMessage());
+ throw ex;
+ }
+ }
+
+ @Override
+ public int available() throws IOException {
+ try {
+ return in.available();
+ } catch (IOException ex) {
+ wire.input("[available] I/O error : " + ex.getMessage());
+ throw ex;
+ }
+ }
+
+ @Override
+ public void mark(final int readlimit) {
+ super.mark(readlimit);
+ }
+
+ @Override
+ public void reset() throws IOException {
+ super.reset();
+ }
+
+ @Override
+ public boolean markSupported() {
+ return false;
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ in.close();
+ } catch (IOException ex) {
+ wire.input("[close] I/O error: " + ex.getMessage());
+ throw ex;
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/LoggingManagedHttpClientConnection.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/LoggingManagedHttpClientConnection.java
new file mode 100644
index 0000000000..5b06179c0c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/LoggingManagedHttpClientConnection.java
@@ -0,0 +1,132 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.config.MessageConstraints;
+import ch.boye.httpclientandroidlib.entity.ContentLengthStrategy;
+import ch.boye.httpclientandroidlib.io.HttpMessageParserFactory;
+import ch.boye.httpclientandroidlib.io.HttpMessageWriterFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CharsetEncoder;
+
+@NotThreadSafe
+class LoggingManagedHttpClientConnection extends DefaultManagedHttpClientConnection {
+
+ public HttpClientAndroidLog log;
+ private final HttpClientAndroidLog headerlog;
+ private final Wire wire;
+
+ public LoggingManagedHttpClientConnection(
+ final String id,
+ final HttpClientAndroidLog log,
+ final HttpClientAndroidLog headerlog,
+ final HttpClientAndroidLog wirelog,
+ final int buffersize,
+ final int fragmentSizeHint,
+ final CharsetDecoder chardecoder,
+ final CharsetEncoder charencoder,
+ final MessageConstraints constraints,
+ final ContentLengthStrategy incomingContentStrategy,
+ final ContentLengthStrategy outgoingContentStrategy,
+ final HttpMessageWriterFactory<HttpRequest> requestWriterFactory,
+ final HttpMessageParserFactory<HttpResponse> responseParserFactory) {
+ super(id, buffersize, fragmentSizeHint, chardecoder, charencoder,
+ constraints, incomingContentStrategy, outgoingContentStrategy,
+ requestWriterFactory, responseParserFactory);
+ this.log = log;
+ this.headerlog = headerlog;
+ this.wire = new Wire(wirelog, id);
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug(getId() + ": Close connection");
+ }
+ super.close();
+ }
+
+ @Override
+ public void shutdown() throws IOException {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug(getId() + ": Shutdown connection");
+ }
+ super.shutdown();
+ }
+
+ @Override
+ protected InputStream getSocketInputStream(final Socket socket) throws IOException {
+ InputStream in = super.getSocketInputStream(socket);
+ if (this.wire.enabled()) {
+ in = new LoggingInputStream(in, this.wire);
+ }
+ return in;
+ }
+
+ @Override
+ protected OutputStream getSocketOutputStream(final Socket socket) throws IOException {
+ OutputStream out = super.getSocketOutputStream(socket);
+ if (this.wire.enabled()) {
+ out = new LoggingOutputStream(out, this.wire);
+ }
+ return out;
+ }
+
+ @Override
+ protected void onResponseReceived(final HttpResponse response) {
+ if (response != null && this.headerlog.isDebugEnabled()) {
+ this.headerlog.debug(getId() + " << " + response.getStatusLine().toString());
+ final Header[] headers = response.getAllHeaders();
+ for (final Header header : headers) {
+ this.headerlog.debug(getId() + " << " + header.toString());
+ }
+ }
+ }
+
+ @Override
+ protected void onRequestSubmitted(final HttpRequest request) {
+ if (request != null && this.headerlog.isDebugEnabled()) {
+ this.headerlog.debug(getId() + " >> " + request.getRequestLine().toString());
+ final Header[] headers = request.getAllHeaders();
+ for (final Header header : headers) {
+ this.headerlog.debug(getId() + " >> " + header.toString());
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/LoggingOutputStream.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/LoggingOutputStream.java
new file mode 100644
index 0000000000..86c47b5022
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/LoggingOutputStream.java
@@ -0,0 +1,104 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Internal class.
+ *
+ * @since 4.3
+ */
+@NotThreadSafe
+class LoggingOutputStream extends OutputStream {
+
+ private final OutputStream out;
+ private final Wire wire;
+
+ public LoggingOutputStream(final OutputStream out, final Wire wire) {
+ super();
+ this.out = out;
+ this.wire = wire;
+ }
+
+ @Override
+ public void write(final int b) throws IOException {
+ try {
+ wire.output(b);
+ } catch (IOException ex) {
+ wire.output("[write] I/O error: " + ex.getMessage());
+ throw ex;
+ }
+ }
+
+ @Override
+ public void write(final byte[] b) throws IOException {
+ try {
+ wire.output(b);
+ out.write(b);
+ } catch (IOException ex) {
+ wire.output("[write] I/O error: " + ex.getMessage());
+ throw ex;
+ }
+ }
+
+ @Override
+ public void write(final byte[] b, final int off, final int len) throws IOException {
+ try {
+ wire.output(b, off, len);
+ out.write(b, off, len);
+ } catch (IOException ex) {
+ wire.output("[write] I/O error: " + ex.getMessage());
+ throw ex;
+ }
+ }
+
+ @Override
+ public void flush() throws IOException {
+ try {
+ out.flush();
+ } catch (IOException ex) {
+ wire.output("[flush] I/O error: " + ex.getMessage());
+ throw ex;
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ out.close();
+ } catch (IOException ex) {
+ wire.output("[close] I/O error: " + ex.getMessage());
+ throw ex;
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/LoggingSessionInputBuffer.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/LoggingSessionInputBuffer.java
new file mode 100644
index 0000000000..95617206e4
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/LoggingSessionInputBuffer.java
@@ -0,0 +1,138 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.Consts;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.io.EofSensor;
+import ch.boye.httpclientandroidlib.io.HttpTransportMetrics;
+import ch.boye.httpclientandroidlib.io.SessionInputBuffer;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Logs all data read to the wire LOG.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) no longer used.
+ */
+@Immutable
+@Deprecated
+public class LoggingSessionInputBuffer implements SessionInputBuffer, EofSensor {
+
+ /** Original session input buffer. */
+ private final SessionInputBuffer in;
+
+ private final EofSensor eofSensor;
+
+ /** The wire log to use for writing. */
+ private final Wire wire;
+
+ private final String charset;
+
+ /**
+ * Create an instance that wraps the specified session input buffer.
+ * @param in The session input buffer.
+ * @param wire The wire log to use.
+ * @param charset protocol charset, <code>ASCII</code> if <code>null</code>
+ */
+ public LoggingSessionInputBuffer(
+ final SessionInputBuffer in, final Wire wire, final String charset) {
+ super();
+ this.in = in;
+ this.eofSensor = in instanceof EofSensor ? (EofSensor) in : null;
+ this.wire = wire;
+ this.charset = charset != null ? charset : Consts.ASCII.name();
+ }
+
+ public LoggingSessionInputBuffer(final SessionInputBuffer in, final Wire wire) {
+ this(in, wire, null);
+ }
+
+ public boolean isDataAvailable(final int timeout) throws IOException {
+ return this.in.isDataAvailable(timeout);
+ }
+
+ public int read(final byte[] b, final int off, final int len) throws IOException {
+ final int l = this.in.read(b, off, len);
+ if (this.wire.enabled() && l > 0) {
+ this.wire.input(b, off, l);
+ }
+ return l;
+ }
+
+ public int read() throws IOException {
+ final int l = this.in.read();
+ if (this.wire.enabled() && l != -1) {
+ this.wire.input(l);
+ }
+ return l;
+ }
+
+ public int read(final byte[] b) throws IOException {
+ final int l = this.in.read(b);
+ if (this.wire.enabled() && l > 0) {
+ this.wire.input(b, 0, l);
+ }
+ return l;
+ }
+
+ public String readLine() throws IOException {
+ final String s = this.in.readLine();
+ if (this.wire.enabled() && s != null) {
+ final String tmp = s + "\r\n";
+ this.wire.input(tmp.getBytes(this.charset));
+ }
+ return s;
+ }
+
+ public int readLine(final CharArrayBuffer buffer) throws IOException {
+ final int l = this.in.readLine(buffer);
+ if (this.wire.enabled() && l >= 0) {
+ final int pos = buffer.length() - l;
+ final String s = new String(buffer.buffer(), pos, l);
+ final String tmp = s + "\r\n";
+ this.wire.input(tmp.getBytes(this.charset));
+ }
+ return l;
+ }
+
+ public HttpTransportMetrics getMetrics() {
+ return this.in.getMetrics();
+ }
+
+ public boolean isEof() {
+ if (this.eofSensor != null) {
+ return this.eofSensor.isEof();
+ } else {
+ return false;
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/LoggingSessionOutputBuffer.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/LoggingSessionOutputBuffer.java
new file mode 100644
index 0000000000..bf1100b4ef
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/LoggingSessionOutputBuffer.java
@@ -0,0 +1,118 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.Consts;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.io.HttpTransportMetrics;
+import ch.boye.httpclientandroidlib.io.SessionOutputBuffer;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Logs all data written to the wire LOG.
+ * @since 4.0
+ * @deprecated (4.3) no longer used.
+ */
+@Immutable
+@Deprecated
+public class LoggingSessionOutputBuffer implements SessionOutputBuffer {
+
+ /** Original data transmitter. */
+ private final SessionOutputBuffer out;
+
+ /** The wire log to use. */
+ private final Wire wire;
+
+ private final String charset;
+
+ /**
+ * Create an instance that wraps the specified session output buffer.
+ * @param out The session output buffer.
+ * @param wire The Wire log to use.
+ * @param charset protocol charset, <code>ASCII</code> if <code>null</code>
+ */
+ public LoggingSessionOutputBuffer(
+ final SessionOutputBuffer out, final Wire wire, final String charset) {
+ super();
+ this.out = out;
+ this.wire = wire;
+ this.charset = charset != null ? charset : Consts.ASCII.name();
+ }
+
+ public LoggingSessionOutputBuffer(final SessionOutputBuffer out, final Wire wire) {
+ this(out, wire, null);
+ }
+
+ public void write(final byte[] b, final int off, final int len) throws IOException {
+ this.out.write(b, off, len);
+ if (this.wire.enabled()) {
+ this.wire.output(b, off, len);
+ }
+ }
+
+ public void write(final int b) throws IOException {
+ this.out.write(b);
+ if (this.wire.enabled()) {
+ this.wire.output(b);
+ }
+ }
+
+ public void write(final byte[] b) throws IOException {
+ this.out.write(b);
+ if (this.wire.enabled()) {
+ this.wire.output(b);
+ }
+ }
+
+ public void flush() throws IOException {
+ this.out.flush();
+ }
+
+ public void writeLine(final CharArrayBuffer buffer) throws IOException {
+ this.out.writeLine(buffer);
+ if (this.wire.enabled()) {
+ final String s = new String(buffer.buffer(), 0, buffer.length());
+ final String tmp = s + "\r\n";
+ this.wire.output(tmp.getBytes(this.charset));
+ }
+ }
+
+ public void writeLine(final String s) throws IOException {
+ this.out.writeLine(s);
+ if (this.wire.enabled()) {
+ final String tmp = s + "\r\n";
+ this.wire.output(tmp.getBytes(this.charset));
+ }
+ }
+
+ public HttpTransportMetrics getMetrics() {
+ return this.out.getMetrics();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/ManagedClientConnectionImpl.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/ManagedClientConnectionImpl.java
new file mode 100644
index 0000000000..d1db2d8ebc
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/ManagedClientConnectionImpl.java
@@ -0,0 +1,461 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+
+import ch.boye.httpclientandroidlib.HttpConnectionMetrics;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionManager;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionOperator;
+import ch.boye.httpclientandroidlib.conn.ManagedClientConnection;
+import ch.boye.httpclientandroidlib.conn.OperatedClientConnection;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.conn.routing.RouteTracker;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+
+/**
+ * @since 4.2
+ *
+ * @deprecated (4.3) use {@link ManagedHttpClientConnectionFactory}.
+ */
+@Deprecated
+@NotThreadSafe
+class ManagedClientConnectionImpl implements ManagedClientConnection {
+
+ private final ClientConnectionManager manager;
+ private final ClientConnectionOperator operator;
+ private volatile HttpPoolEntry poolEntry;
+ private volatile boolean reusable;
+ private volatile long duration;
+
+ ManagedClientConnectionImpl(
+ final ClientConnectionManager manager,
+ final ClientConnectionOperator operator,
+ final HttpPoolEntry entry) {
+ super();
+ Args.notNull(manager, "Connection manager");
+ Args.notNull(operator, "Connection operator");
+ Args.notNull(entry, "HTTP pool entry");
+ this.manager = manager;
+ this.operator = operator;
+ this.poolEntry = entry;
+ this.reusable = false;
+ this.duration = Long.MAX_VALUE;
+ }
+
+ public String getId() {
+ return null;
+ }
+
+ HttpPoolEntry getPoolEntry() {
+ return this.poolEntry;
+ }
+
+ HttpPoolEntry detach() {
+ final HttpPoolEntry local = this.poolEntry;
+ this.poolEntry = null;
+ return local;
+ }
+
+ public ClientConnectionManager getManager() {
+ return this.manager;
+ }
+
+ private OperatedClientConnection getConnection() {
+ final HttpPoolEntry local = this.poolEntry;
+ if (local == null) {
+ return null;
+ }
+ return local.getConnection();
+ }
+
+ private OperatedClientConnection ensureConnection() {
+ final HttpPoolEntry local = this.poolEntry;
+ if (local == null) {
+ throw new ConnectionShutdownException();
+ }
+ return local.getConnection();
+ }
+
+ private HttpPoolEntry ensurePoolEntry() {
+ final HttpPoolEntry local = this.poolEntry;
+ if (local == null) {
+ throw new ConnectionShutdownException();
+ }
+ return local;
+ }
+
+ public void close() throws IOException {
+ final HttpPoolEntry local = this.poolEntry;
+ if (local != null) {
+ final OperatedClientConnection conn = local.getConnection();
+ local.getTracker().reset();
+ conn.close();
+ }
+ }
+
+ public void shutdown() throws IOException {
+ final HttpPoolEntry local = this.poolEntry;
+ if (local != null) {
+ final OperatedClientConnection conn = local.getConnection();
+ local.getTracker().reset();
+ conn.shutdown();
+ }
+ }
+
+ public boolean isOpen() {
+ final OperatedClientConnection conn = getConnection();
+ if (conn != null) {
+ return conn.isOpen();
+ } else {
+ return false;
+ }
+ }
+
+ public boolean isStale() {
+ final OperatedClientConnection conn = getConnection();
+ if (conn != null) {
+ return conn.isStale();
+ } else {
+ return true;
+ }
+ }
+
+ public void setSocketTimeout(final int timeout) {
+ final OperatedClientConnection conn = ensureConnection();
+ conn.setSocketTimeout(timeout);
+ }
+
+ public int getSocketTimeout() {
+ final OperatedClientConnection conn = ensureConnection();
+ return conn.getSocketTimeout();
+ }
+
+ public HttpConnectionMetrics getMetrics() {
+ final OperatedClientConnection conn = ensureConnection();
+ return conn.getMetrics();
+ }
+
+ public void flush() throws IOException {
+ final OperatedClientConnection conn = ensureConnection();
+ conn.flush();
+ }
+
+ public boolean isResponseAvailable(final int timeout) throws IOException {
+ final OperatedClientConnection conn = ensureConnection();
+ return conn.isResponseAvailable(timeout);
+ }
+
+ public void receiveResponseEntity(
+ final HttpResponse response) throws HttpException, IOException {
+ final OperatedClientConnection conn = ensureConnection();
+ conn.receiveResponseEntity(response);
+ }
+
+ public HttpResponse receiveResponseHeader() throws HttpException, IOException {
+ final OperatedClientConnection conn = ensureConnection();
+ return conn.receiveResponseHeader();
+ }
+
+ public void sendRequestEntity(
+ final HttpEntityEnclosingRequest request) throws HttpException, IOException {
+ final OperatedClientConnection conn = ensureConnection();
+ conn.sendRequestEntity(request);
+ }
+
+ public void sendRequestHeader(
+ final HttpRequest request) throws HttpException, IOException {
+ final OperatedClientConnection conn = ensureConnection();
+ conn.sendRequestHeader(request);
+ }
+
+ public InetAddress getLocalAddress() {
+ final OperatedClientConnection conn = ensureConnection();
+ return conn.getLocalAddress();
+ }
+
+ public int getLocalPort() {
+ final OperatedClientConnection conn = ensureConnection();
+ return conn.getLocalPort();
+ }
+
+ public InetAddress getRemoteAddress() {
+ final OperatedClientConnection conn = ensureConnection();
+ return conn.getRemoteAddress();
+ }
+
+ public int getRemotePort() {
+ final OperatedClientConnection conn = ensureConnection();
+ return conn.getRemotePort();
+ }
+
+ public boolean isSecure() {
+ final OperatedClientConnection conn = ensureConnection();
+ return conn.isSecure();
+ }
+
+ public void bind(final Socket socket) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ public Socket getSocket() {
+ final OperatedClientConnection conn = ensureConnection();
+ return conn.getSocket();
+ }
+
+ public SSLSession getSSLSession() {
+ final OperatedClientConnection conn = ensureConnection();
+ SSLSession result = null;
+ final Socket sock = conn.getSocket();
+ if (sock instanceof SSLSocket) {
+ result = ((SSLSocket)sock).getSession();
+ }
+ return result;
+ }
+
+ public Object getAttribute(final String id) {
+ final OperatedClientConnection conn = ensureConnection();
+ if (conn instanceof HttpContext) {
+ return ((HttpContext) conn).getAttribute(id);
+ } else {
+ return null;
+ }
+ }
+
+ public Object removeAttribute(final String id) {
+ final OperatedClientConnection conn = ensureConnection();
+ if (conn instanceof HttpContext) {
+ return ((HttpContext) conn).removeAttribute(id);
+ } else {
+ return null;
+ }
+ }
+
+ public void setAttribute(final String id, final Object obj) {
+ final OperatedClientConnection conn = ensureConnection();
+ if (conn instanceof HttpContext) {
+ ((HttpContext) conn).setAttribute(id, obj);
+ }
+ }
+
+ public HttpRoute getRoute() {
+ final HttpPoolEntry local = ensurePoolEntry();
+ return local.getEffectiveRoute();
+ }
+
+ public void open(
+ final HttpRoute route,
+ final HttpContext context,
+ final HttpParams params) throws IOException {
+ Args.notNull(route, "Route");
+ Args.notNull(params, "HTTP parameters");
+ final OperatedClientConnection conn;
+ synchronized (this) {
+ if (this.poolEntry == null) {
+ throw new ConnectionShutdownException();
+ }
+ final RouteTracker tracker = this.poolEntry.getTracker();
+ Asserts.notNull(tracker, "Route tracker");
+ Asserts.check(!tracker.isConnected(), "Connection already open");
+ conn = this.poolEntry.getConnection();
+ }
+
+ final HttpHost proxy = route.getProxyHost();
+ this.operator.openConnection(
+ conn,
+ (proxy != null) ? proxy : route.getTargetHost(),
+ route.getLocalAddress(),
+ context, params);
+
+ synchronized (this) {
+ if (this.poolEntry == null) {
+ throw new InterruptedIOException();
+ }
+ final RouteTracker tracker = this.poolEntry.getTracker();
+ if (proxy == null) {
+ tracker.connectTarget(conn.isSecure());
+ } else {
+ tracker.connectProxy(proxy, conn.isSecure());
+ }
+ }
+ }
+
+ public void tunnelTarget(
+ final boolean secure, final HttpParams params) throws IOException {
+ Args.notNull(params, "HTTP parameters");
+ final HttpHost target;
+ final OperatedClientConnection conn;
+ synchronized (this) {
+ if (this.poolEntry == null) {
+ throw new ConnectionShutdownException();
+ }
+ final RouteTracker tracker = this.poolEntry.getTracker();
+ Asserts.notNull(tracker, "Route tracker");
+ Asserts.check(tracker.isConnected(), "Connection not open");
+ Asserts.check(!tracker.isTunnelled(), "Connection is already tunnelled");
+ target = tracker.getTargetHost();
+ conn = this.poolEntry.getConnection();
+ }
+
+ conn.update(null, target, secure, params);
+
+ synchronized (this) {
+ if (this.poolEntry == null) {
+ throw new InterruptedIOException();
+ }
+ final RouteTracker tracker = this.poolEntry.getTracker();
+ tracker.tunnelTarget(secure);
+ }
+ }
+
+ public void tunnelProxy(
+ final HttpHost next, final boolean secure, final HttpParams params) throws IOException {
+ Args.notNull(next, "Next proxy");
+ Args.notNull(params, "HTTP parameters");
+ final OperatedClientConnection conn;
+ synchronized (this) {
+ if (this.poolEntry == null) {
+ throw new ConnectionShutdownException();
+ }
+ final RouteTracker tracker = this.poolEntry.getTracker();
+ Asserts.notNull(tracker, "Route tracker");
+ Asserts.check(tracker.isConnected(), "Connection not open");
+ conn = this.poolEntry.getConnection();
+ }
+
+ conn.update(null, next, secure, params);
+
+ synchronized (this) {
+ if (this.poolEntry == null) {
+ throw new InterruptedIOException();
+ }
+ final RouteTracker tracker = this.poolEntry.getTracker();
+ tracker.tunnelProxy(next, secure);
+ }
+ }
+
+ public void layerProtocol(
+ final HttpContext context, final HttpParams params) throws IOException {
+ Args.notNull(params, "HTTP parameters");
+ final HttpHost target;
+ final OperatedClientConnection conn;
+ synchronized (this) {
+ if (this.poolEntry == null) {
+ throw new ConnectionShutdownException();
+ }
+ final RouteTracker tracker = this.poolEntry.getTracker();
+ Asserts.notNull(tracker, "Route tracker");
+ Asserts.check(tracker.isConnected(), "Connection not open");
+ Asserts.check(tracker.isTunnelled(), "Protocol layering without a tunnel not supported");
+ Asserts.check(!tracker.isLayered(), "Multiple protocol layering not supported");
+ target = tracker.getTargetHost();
+ conn = this.poolEntry.getConnection();
+ }
+ this.operator.updateSecureConnection(conn, target, context, params);
+
+ synchronized (this) {
+ if (this.poolEntry == null) {
+ throw new InterruptedIOException();
+ }
+ final RouteTracker tracker = this.poolEntry.getTracker();
+ tracker.layerProtocol(conn.isSecure());
+ }
+ }
+
+ public Object getState() {
+ final HttpPoolEntry local = ensurePoolEntry();
+ return local.getState();
+ }
+
+ public void setState(final Object state) {
+ final HttpPoolEntry local = ensurePoolEntry();
+ local.setState(state);
+ }
+
+ public void markReusable() {
+ this.reusable = true;
+ }
+
+ public void unmarkReusable() {
+ this.reusable = false;
+ }
+
+ public boolean isMarkedReusable() {
+ return this.reusable;
+ }
+
+ public void setIdleDuration(final long duration, final TimeUnit unit) {
+ if(duration > 0) {
+ this.duration = unit.toMillis(duration);
+ } else {
+ this.duration = -1;
+ }
+ }
+
+ public void releaseConnection() {
+ synchronized (this) {
+ if (this.poolEntry == null) {
+ return;
+ }
+ this.manager.releaseConnection(this, this.duration, TimeUnit.MILLISECONDS);
+ this.poolEntry = null;
+ }
+ }
+
+ public void abortConnection() {
+ synchronized (this) {
+ if (this.poolEntry == null) {
+ return;
+ }
+ this.reusable = false;
+ final OperatedClientConnection conn = this.poolEntry.getConnection();
+ try {
+ conn.shutdown();
+ } catch (final IOException ignore) {
+ }
+ this.manager.releaseConnection(this, this.duration, TimeUnit.MILLISECONDS);
+ this.poolEntry = null;
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/ManagedHttpClientConnectionFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/ManagedHttpClientConnectionFactory.java
new file mode 100644
index 0000000000..51b3844f94
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/ManagedHttpClientConnectionFactory.java
@@ -0,0 +1,121 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CodingErrorAction;
+import java.util.concurrent.atomic.AtomicLong;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.config.ConnectionConfig;
+import ch.boye.httpclientandroidlib.conn.HttpConnectionFactory;
+import ch.boye.httpclientandroidlib.conn.ManagedHttpClientConnection;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.impl.io.DefaultHttpRequestWriterFactory;
+import ch.boye.httpclientandroidlib.io.HttpMessageParserFactory;
+import ch.boye.httpclientandroidlib.io.HttpMessageWriterFactory;
+
+/**
+ * Factory for {@link ManagedHttpClientConnection} instances.
+ * @since 4.3
+ */
+@Immutable
+public class ManagedHttpClientConnectionFactory
+ implements HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> {
+
+ private static final AtomicLong COUNTER = new AtomicLong();
+
+ public static final ManagedHttpClientConnectionFactory INSTANCE = new ManagedHttpClientConnectionFactory();
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(DefaultManagedHttpClientConnection.class);
+ public HttpClientAndroidLog headerlog = new HttpClientAndroidLog("ch.boye.httpclientandroidlib.headers");
+ public HttpClientAndroidLog wirelog = new HttpClientAndroidLog("ch.boye.httpclientandroidlib.wire");
+
+ private final HttpMessageWriterFactory<HttpRequest> requestWriterFactory;
+ private final HttpMessageParserFactory<HttpResponse> responseParserFactory;
+
+ public ManagedHttpClientConnectionFactory(
+ final HttpMessageWriterFactory<HttpRequest> requestWriterFactory,
+ final HttpMessageParserFactory<HttpResponse> responseParserFactory) {
+ super();
+ this.requestWriterFactory = requestWriterFactory != null ? requestWriterFactory :
+ DefaultHttpRequestWriterFactory.INSTANCE;
+ this.responseParserFactory = responseParserFactory != null ? responseParserFactory :
+ DefaultHttpResponseParserFactory.INSTANCE;
+ }
+
+ public ManagedHttpClientConnectionFactory(
+ final HttpMessageParserFactory<HttpResponse> responseParserFactory) {
+ this(null, responseParserFactory);
+ }
+
+ public ManagedHttpClientConnectionFactory() {
+ this(null, null);
+ }
+
+ public ManagedHttpClientConnection create(final HttpRoute route, final ConnectionConfig config) {
+ final ConnectionConfig cconfig = config != null ? config : ConnectionConfig.DEFAULT;
+ CharsetDecoder chardecoder = null;
+ CharsetEncoder charencoder = null;
+ final Charset charset = cconfig.getCharset();
+ final CodingErrorAction malformedInputAction = cconfig.getMalformedInputAction() != null ?
+ cconfig.getMalformedInputAction() : CodingErrorAction.REPORT;
+ final CodingErrorAction unmappableInputAction = cconfig.getUnmappableInputAction() != null ?
+ cconfig.getUnmappableInputAction() : CodingErrorAction.REPORT;
+ if (charset != null) {
+ chardecoder = charset.newDecoder();
+ chardecoder.onMalformedInput(malformedInputAction);
+ chardecoder.onUnmappableCharacter(unmappableInputAction);
+ charencoder = charset.newEncoder();
+ charencoder.onMalformedInput(malformedInputAction);
+ charencoder.onUnmappableCharacter(unmappableInputAction);
+ }
+ final String id = "http-outgoing-" + Long.toString(COUNTER.getAndIncrement());
+ return new LoggingManagedHttpClientConnection(
+ id,
+ log,
+ headerlog,
+ wirelog,
+ cconfig.getBufferSize(),
+ cconfig.getFragmentSizeHint(),
+ chardecoder,
+ charencoder,
+ cconfig.getMessageConstraints(),
+ null,
+ null,
+ requestWriterFactory,
+ responseParserFactory);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/PoolingClientConnectionManager.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/PoolingClientConnectionManager.java
new file mode 100644
index 0000000000..f9685b81ee
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/PoolingClientConnectionManager.java
@@ -0,0 +1,328 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionManager;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionOperator;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionRequest;
+import ch.boye.httpclientandroidlib.conn.ConnectionPoolTimeoutException;
+import ch.boye.httpclientandroidlib.conn.DnsResolver;
+import ch.boye.httpclientandroidlib.conn.ManagedClientConnection;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.conn.scheme.SchemeRegistry;
+import ch.boye.httpclientandroidlib.pool.ConnPoolControl;
+import ch.boye.httpclientandroidlib.pool.PoolStats;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+
+/**
+ * Manages a pool of {@link ch.boye.httpclientandroidlib.conn.OperatedClientConnection}
+ * and is able to service connection requests from multiple execution threads.
+ * Connections are pooled on a per route basis. A request for a route which
+ * already the manager has persistent connections for available in the pool
+ * will be services by leasing a connection from the pool rather than
+ * creating a brand new connection.
+ * <p>
+ * PoolingConnectionManager maintains a maximum limit of connection on
+ * a per route basis and in total. Per default this implementation will
+ * create no more than than 2 concurrent connections per given route
+ * and no more 20 connections in total. For many real-world applications
+ * these limits may prove too constraining, especially if they use HTTP
+ * as a transport protocol for their services. Connection limits, however,
+ * can be adjusted using HTTP parameters.
+ *
+ * @since 4.2
+ *
+ * @deprecated (4.3) use {@link PoolingHttpClientConnectionManager}.
+ */
+@Deprecated
+@ThreadSafe
+public class PoolingClientConnectionManager implements ClientConnectionManager, ConnPoolControl<HttpRoute> {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ private final SchemeRegistry schemeRegistry;
+
+ private final HttpConnPool pool;
+
+ private final ClientConnectionOperator operator;
+
+ /** the custom-configured DNS lookup mechanism. */
+ private final DnsResolver dnsResolver;
+
+ public PoolingClientConnectionManager(final SchemeRegistry schreg) {
+ this(schreg, -1, TimeUnit.MILLISECONDS);
+ }
+
+ public PoolingClientConnectionManager(final SchemeRegistry schreg,final DnsResolver dnsResolver) {
+ this(schreg, -1, TimeUnit.MILLISECONDS,dnsResolver);
+ }
+
+ public PoolingClientConnectionManager() {
+ this(SchemeRegistryFactory.createDefault());
+ }
+
+ public PoolingClientConnectionManager(
+ final SchemeRegistry schemeRegistry,
+ final long timeToLive, final TimeUnit tunit) {
+ this(schemeRegistry, timeToLive, tunit, new SystemDefaultDnsResolver());
+ }
+
+ public PoolingClientConnectionManager(final SchemeRegistry schemeRegistry,
+ final long timeToLive, final TimeUnit tunit,
+ final DnsResolver dnsResolver) {
+ super();
+ Args.notNull(schemeRegistry, "Scheme registry");
+ Args.notNull(dnsResolver, "DNS resolver");
+ this.schemeRegistry = schemeRegistry;
+ this.dnsResolver = dnsResolver;
+ this.operator = createConnectionOperator(schemeRegistry);
+ this.pool = new HttpConnPool(this.log, this.operator, 2, 20, timeToLive, tunit);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ shutdown();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Hook for creating the connection operator.
+ * It is called by the constructor.
+ * Derived classes can override this method to change the
+ * instantiation of the operator.
+ * The default implementation here instantiates
+ * {@link DefaultClientConnectionOperator DefaultClientConnectionOperator}.
+ *
+ * @param schreg the scheme registry.
+ *
+ * @return the connection operator to use
+ */
+ protected ClientConnectionOperator createConnectionOperator(final SchemeRegistry schreg) {
+ return new DefaultClientConnectionOperator(schreg, this.dnsResolver);
+ }
+
+ public SchemeRegistry getSchemeRegistry() {
+ return this.schemeRegistry;
+ }
+
+ private String format(final HttpRoute route, final Object state) {
+ final StringBuilder buf = new StringBuilder();
+ buf.append("[route: ").append(route).append("]");
+ if (state != null) {
+ buf.append("[state: ").append(state).append("]");
+ }
+ return buf.toString();
+ }
+
+ private String formatStats(final HttpRoute route) {
+ final StringBuilder buf = new StringBuilder();
+ final PoolStats totals = this.pool.getTotalStats();
+ final PoolStats stats = this.pool.getStats(route);
+ buf.append("[total kept alive: ").append(totals.getAvailable()).append("; ");
+ buf.append("route allocated: ").append(stats.getLeased() + stats.getAvailable());
+ buf.append(" of ").append(stats.getMax()).append("; ");
+ buf.append("total allocated: ").append(totals.getLeased() + totals.getAvailable());
+ buf.append(" of ").append(totals.getMax()).append("]");
+ return buf.toString();
+ }
+
+ private String format(final HttpPoolEntry entry) {
+ final StringBuilder buf = new StringBuilder();
+ buf.append("[id: ").append(entry.getId()).append("]");
+ buf.append("[route: ").append(entry.getRoute()).append("]");
+ final Object state = entry.getState();
+ if (state != null) {
+ buf.append("[state: ").append(state).append("]");
+ }
+ return buf.toString();
+ }
+
+ public ClientConnectionRequest requestConnection(
+ final HttpRoute route,
+ final Object state) {
+ Args.notNull(route, "HTTP route");
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Connection request: " + format(route, state) + formatStats(route));
+ }
+ final Future<HttpPoolEntry> future = this.pool.lease(route, state);
+
+ return new ClientConnectionRequest() {
+
+ public void abortRequest() {
+ future.cancel(true);
+ }
+
+ public ManagedClientConnection getConnection(
+ final long timeout,
+ final TimeUnit tunit) throws InterruptedException, ConnectionPoolTimeoutException {
+ return leaseConnection(future, timeout, tunit);
+ }
+
+ };
+
+ }
+
+ ManagedClientConnection leaseConnection(
+ final Future<HttpPoolEntry> future,
+ final long timeout,
+ final TimeUnit tunit) throws InterruptedException, ConnectionPoolTimeoutException {
+ final HttpPoolEntry entry;
+ try {
+ entry = future.get(timeout, tunit);
+ if (entry == null || future.isCancelled()) {
+ throw new InterruptedException();
+ }
+ Asserts.check(entry.getConnection() != null, "Pool entry with no connection");
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Connection leased: " + format(entry) + formatStats(entry.getRoute()));
+ }
+ return new ManagedClientConnectionImpl(this, this.operator, entry);
+ } catch (final ExecutionException ex) {
+ Throwable cause = ex.getCause();
+ if (cause == null) {
+ cause = ex;
+ }
+ this.log.error("Unexpected exception leasing connection from pool", cause);
+ // Should never happen
+ throw new InterruptedException();
+ } catch (final TimeoutException ex) {
+ throw new ConnectionPoolTimeoutException("Timeout waiting for connection from pool");
+ }
+ }
+
+ public void releaseConnection(
+ final ManagedClientConnection conn, final long keepalive, final TimeUnit tunit) {
+
+ Args.check(conn instanceof ManagedClientConnectionImpl, "Connection class mismatch, " +
+ "connection not obtained from this manager");
+ final ManagedClientConnectionImpl managedConn = (ManagedClientConnectionImpl) conn;
+ Asserts.check(managedConn.getManager() == this, "Connection not obtained from this manager");
+ synchronized (managedConn) {
+ final HttpPoolEntry entry = managedConn.detach();
+ if (entry == null) {
+ return;
+ }
+ try {
+ if (managedConn.isOpen() && !managedConn.isMarkedReusable()) {
+ try {
+ managedConn.shutdown();
+ } catch (final IOException iox) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("I/O exception shutting down released connection", iox);
+ }
+ }
+ }
+ // Only reusable connections can be kept alive
+ if (managedConn.isMarkedReusable()) {
+ entry.updateExpiry(keepalive, tunit != null ? tunit : TimeUnit.MILLISECONDS);
+ if (this.log.isDebugEnabled()) {
+ final String s;
+ if (keepalive > 0) {
+ s = "for " + keepalive + " " + tunit;
+ } else {
+ s = "indefinitely";
+ }
+ this.log.debug("Connection " + format(entry) + " can be kept alive " + s);
+ }
+ }
+ } finally {
+ this.pool.release(entry, managedConn.isMarkedReusable());
+ }
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Connection released: " + format(entry) + formatStats(entry.getRoute()));
+ }
+ }
+ }
+
+ public void shutdown() {
+ this.log.debug("Connection manager is shutting down");
+ try {
+ this.pool.shutdown();
+ } catch (final IOException ex) {
+ this.log.debug("I/O exception shutting down connection manager", ex);
+ }
+ this.log.debug("Connection manager shut down");
+ }
+
+ public void closeIdleConnections(final long idleTimeout, final TimeUnit tunit) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Closing connections idle longer than " + idleTimeout + " " + tunit);
+ }
+ this.pool.closeIdle(idleTimeout, tunit);
+ }
+
+ public void closeExpiredConnections() {
+ this.log.debug("Closing expired connections");
+ this.pool.closeExpired();
+ }
+
+ public int getMaxTotal() {
+ return this.pool.getMaxTotal();
+ }
+
+ public void setMaxTotal(final int max) {
+ this.pool.setMaxTotal(max);
+ }
+
+ public int getDefaultMaxPerRoute() {
+ return this.pool.getDefaultMaxPerRoute();
+ }
+
+ public void setDefaultMaxPerRoute(final int max) {
+ this.pool.setDefaultMaxPerRoute(max);
+ }
+
+ public int getMaxPerRoute(final HttpRoute route) {
+ return this.pool.getMaxPerRoute(route);
+ }
+
+ public void setMaxPerRoute(final HttpRoute route, final int max) {
+ this.pool.setMaxPerRoute(route, max);
+ }
+
+ public PoolStats getTotalStats() {
+ return this.pool.getTotalStats();
+ }
+
+ public PoolStats getStats(final HttpRoute route) {
+ return this.pool.getStats(route);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/PoolingHttpClientConnectionManager.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/PoolingHttpClientConnectionManager.java
new file mode 100644
index 0000000000..a2606b4e65
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/PoolingHttpClientConnectionManager.java
@@ -0,0 +1,516 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.HttpClientConnection;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.config.ConnectionConfig;
+import ch.boye.httpclientandroidlib.config.Lookup;
+import ch.boye.httpclientandroidlib.config.Registry;
+import ch.boye.httpclientandroidlib.config.RegistryBuilder;
+import ch.boye.httpclientandroidlib.config.SocketConfig;
+import ch.boye.httpclientandroidlib.conn.ConnectionPoolTimeoutException;
+import ch.boye.httpclientandroidlib.conn.ConnectionRequest;
+import ch.boye.httpclientandroidlib.conn.DnsResolver;
+import ch.boye.httpclientandroidlib.conn.HttpClientConnectionManager;
+import ch.boye.httpclientandroidlib.conn.HttpConnectionFactory;
+import ch.boye.httpclientandroidlib.conn.SchemePortResolver;
+import ch.boye.httpclientandroidlib.conn.ManagedHttpClientConnection;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.conn.socket.ConnectionSocketFactory;
+import ch.boye.httpclientandroidlib.conn.socket.PlainConnectionSocketFactory;
+import ch.boye.httpclientandroidlib.conn.ssl.SSLConnectionSocketFactory;
+import ch.boye.httpclientandroidlib.pool.ConnFactory;
+import ch.boye.httpclientandroidlib.pool.ConnPoolControl;
+import ch.boye.httpclientandroidlib.pool.PoolStats;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+
+/**
+ * <tt>ClientConnectionPoolManager</tt> maintains a pool of
+ * {@link HttpClientConnection}s and is able to service connection requests
+ * from multiple execution threads. Connections are pooled on a per route
+ * basis. A request for a route which already the manager has persistent
+ * connections for available in the pool will be services by leasing
+ * a connection from the pool rather than creating a brand new connection.
+ * <p/>
+ * <tt>ClientConnectionPoolManager</tt> maintains a maximum limit of connection
+ * on a per route basis and in total. Per default this implementation will
+ * create no more than than 2 concurrent connections per given route
+ * and no more 20 connections in total. For many real-world applications
+ * these limits may prove too constraining, especially if they use HTTP
+ * as a transport protocol for their services. Connection limits, however,
+ * can be adjusted using {@link ConnPoolControl} methods.
+ *
+ * @since 4.3
+ */
+@ThreadSafe
+public class PoolingHttpClientConnectionManager
+ implements HttpClientConnectionManager, ConnPoolControl<HttpRoute>, Closeable {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ private final ConfigData configData;
+ private final CPool pool;
+ private final HttpClientConnectionOperator connectionOperator;
+ private final AtomicBoolean isShutDown;
+
+ private static Registry<ConnectionSocketFactory> getDefaultRegistry() {
+ return RegistryBuilder.<ConnectionSocketFactory>create()
+ .register("http", PlainConnectionSocketFactory.getSocketFactory())
+ .register("https", SSLConnectionSocketFactory.getSocketFactory())
+ .build();
+ }
+
+ public PoolingHttpClientConnectionManager() {
+ this(getDefaultRegistry());
+ }
+
+ public PoolingHttpClientConnectionManager(final long timeToLive, final TimeUnit tunit) {
+ this(getDefaultRegistry(), null, null ,null, timeToLive, tunit);
+ }
+
+ public PoolingHttpClientConnectionManager(
+ final Registry<ConnectionSocketFactory> socketFactoryRegistry) {
+ this(socketFactoryRegistry, null, null);
+ }
+
+ public PoolingHttpClientConnectionManager(
+ final Registry<ConnectionSocketFactory> socketFactoryRegistry,
+ final DnsResolver dnsResolver) {
+ this(socketFactoryRegistry, null, dnsResolver);
+ }
+
+ public PoolingHttpClientConnectionManager(
+ final Registry<ConnectionSocketFactory> socketFactoryRegistry,
+ final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory) {
+ this(socketFactoryRegistry, connFactory, null);
+ }
+
+ public PoolingHttpClientConnectionManager(
+ final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory) {
+ this(getDefaultRegistry(), connFactory, null);
+ }
+
+ public PoolingHttpClientConnectionManager(
+ final Registry<ConnectionSocketFactory> socketFactoryRegistry,
+ final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory,
+ final DnsResolver dnsResolver) {
+ this(socketFactoryRegistry, connFactory, null, dnsResolver, -1, TimeUnit.MILLISECONDS);
+ }
+
+ public PoolingHttpClientConnectionManager(
+ final Registry<ConnectionSocketFactory> socketFactoryRegistry,
+ final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory,
+ final SchemePortResolver schemePortResolver,
+ final DnsResolver dnsResolver,
+ final long timeToLive, final TimeUnit tunit) {
+ super();
+ this.configData = new ConfigData();
+ this.pool = new CPool(
+ new InternalConnectionFactory(this.configData, connFactory), 2, 20, timeToLive, tunit);
+ this.connectionOperator = new HttpClientConnectionOperator(
+ socketFactoryRegistry, schemePortResolver, dnsResolver);
+ this.isShutDown = new AtomicBoolean(false);
+ }
+
+ PoolingHttpClientConnectionManager(
+ final CPool pool,
+ final Lookup<ConnectionSocketFactory> socketFactoryRegistry,
+ final SchemePortResolver schemePortResolver,
+ final DnsResolver dnsResolver) {
+ super();
+ this.configData = new ConfigData();
+ this.pool = pool;
+ this.connectionOperator = new HttpClientConnectionOperator(
+ socketFactoryRegistry, schemePortResolver, dnsResolver);
+ this.isShutDown = new AtomicBoolean(false);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ shutdown();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ public void close() {
+ shutdown();
+ }
+
+ private String format(final HttpRoute route, final Object state) {
+ final StringBuilder buf = new StringBuilder();
+ buf.append("[route: ").append(route).append("]");
+ if (state != null) {
+ buf.append("[state: ").append(state).append("]");
+ }
+ return buf.toString();
+ }
+
+ private String formatStats(final HttpRoute route) {
+ final StringBuilder buf = new StringBuilder();
+ final PoolStats totals = this.pool.getTotalStats();
+ final PoolStats stats = this.pool.getStats(route);
+ buf.append("[total kept alive: ").append(totals.getAvailable()).append("; ");
+ buf.append("route allocated: ").append(stats.getLeased() + stats.getAvailable());
+ buf.append(" of ").append(stats.getMax()).append("; ");
+ buf.append("total allocated: ").append(totals.getLeased() + totals.getAvailable());
+ buf.append(" of ").append(totals.getMax()).append("]");
+ return buf.toString();
+ }
+
+ private String format(final CPoolEntry entry) {
+ final StringBuilder buf = new StringBuilder();
+ buf.append("[id: ").append(entry.getId()).append("]");
+ buf.append("[route: ").append(entry.getRoute()).append("]");
+ final Object state = entry.getState();
+ if (state != null) {
+ buf.append("[state: ").append(state).append("]");
+ }
+ return buf.toString();
+ }
+
+ public ConnectionRequest requestConnection(
+ final HttpRoute route,
+ final Object state) {
+ Args.notNull(route, "HTTP route");
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Connection request: " + format(route, state) + formatStats(route));
+ }
+ final Future<CPoolEntry> future = this.pool.lease(route, state, null);
+ return new ConnectionRequest() {
+
+ public boolean cancel() {
+ return future.cancel(true);
+ }
+
+ public HttpClientConnection get(
+ final long timeout,
+ final TimeUnit tunit) throws InterruptedException, ExecutionException, ConnectionPoolTimeoutException {
+ return leaseConnection(future, timeout, tunit);
+ }
+
+ };
+
+ }
+
+ protected HttpClientConnection leaseConnection(
+ final Future<CPoolEntry> future,
+ final long timeout,
+ final TimeUnit tunit) throws InterruptedException, ExecutionException, ConnectionPoolTimeoutException {
+ final CPoolEntry entry;
+ try {
+ entry = future.get(timeout, tunit);
+ if (entry == null || future.isCancelled()) {
+ throw new InterruptedException();
+ }
+ Asserts.check(entry.getConnection() != null, "Pool entry with no connection");
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Connection leased: " + format(entry) + formatStats(entry.getRoute()));
+ }
+ return CPoolProxy.newProxy(entry);
+ } catch (final TimeoutException ex) {
+ throw new ConnectionPoolTimeoutException("Timeout waiting for connection from pool");
+ }
+ }
+
+ public void releaseConnection(
+ final HttpClientConnection managedConn,
+ final Object state,
+ final long keepalive, final TimeUnit tunit) {
+ Args.notNull(managedConn, "Managed connection");
+ synchronized (managedConn) {
+ final CPoolEntry entry = CPoolProxy.detach(managedConn);
+ if (entry == null) {
+ return;
+ }
+ final ManagedHttpClientConnection conn = entry.getConnection();
+ try {
+ if (conn.isOpen()) {
+ entry.setState(state);
+ entry.updateExpiry(keepalive, tunit != null ? tunit : TimeUnit.MILLISECONDS);
+ if (this.log.isDebugEnabled()) {
+ final String s;
+ if (keepalive > 0) {
+ s = "for " + (double) keepalive / 1000 + " seconds";
+ } else {
+ s = "indefinitely";
+ }
+ this.log.debug("Connection " + format(entry) + " can be kept alive " + s);
+ }
+ }
+ } finally {
+ this.pool.release(entry, conn.isOpen() && entry.isRouteComplete());
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Connection released: " + format(entry) + formatStats(entry.getRoute()));
+ }
+ }
+ }
+ }
+
+ public void connect(
+ final HttpClientConnection managedConn,
+ final HttpRoute route,
+ final int connectTimeout,
+ final HttpContext context) throws IOException {
+ Args.notNull(managedConn, "Managed Connection");
+ Args.notNull(route, "HTTP route");
+ final ManagedHttpClientConnection conn;
+ synchronized (managedConn) {
+ final CPoolEntry entry = CPoolProxy.getPoolEntry(managedConn);
+ conn = entry.getConnection();
+ }
+ final HttpHost host;
+ if (route.getProxyHost() != null) {
+ host = route.getProxyHost();
+ } else {
+ host = route.getTargetHost();
+ }
+ final InetSocketAddress localAddress = route.getLocalSocketAddress();
+ SocketConfig socketConfig = this.configData.getSocketConfig(host);
+ if (socketConfig == null) {
+ socketConfig = this.configData.getDefaultSocketConfig();
+ }
+ if (socketConfig == null) {
+ socketConfig = SocketConfig.DEFAULT;
+ }
+ this.connectionOperator.connect(
+ conn, host, localAddress, connectTimeout, socketConfig, context);
+ }
+
+ public void upgrade(
+ final HttpClientConnection managedConn,
+ final HttpRoute route,
+ final HttpContext context) throws IOException {
+ Args.notNull(managedConn, "Managed Connection");
+ Args.notNull(route, "HTTP route");
+ final ManagedHttpClientConnection conn;
+ synchronized (managedConn) {
+ final CPoolEntry entry = CPoolProxy.getPoolEntry(managedConn);
+ conn = entry.getConnection();
+ }
+ this.connectionOperator.upgrade(conn, route.getTargetHost(), context);
+ }
+
+ public void routeComplete(
+ final HttpClientConnection managedConn,
+ final HttpRoute route,
+ final HttpContext context) throws IOException {
+ Args.notNull(managedConn, "Managed Connection");
+ Args.notNull(route, "HTTP route");
+ synchronized (managedConn) {
+ final CPoolEntry entry = CPoolProxy.getPoolEntry(managedConn);
+ entry.markRouteComplete();
+ }
+ }
+
+ public void shutdown() {
+ if (this.isShutDown.compareAndSet(false, true)) {
+ this.log.debug("Connection manager is shutting down");
+ try {
+ this.pool.shutdown();
+ } catch (final IOException ex) {
+ this.log.debug("I/O exception shutting down connection manager", ex);
+ }
+ this.log.debug("Connection manager shut down");
+ }
+ }
+
+ public void closeIdleConnections(final long idleTimeout, final TimeUnit tunit) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Closing connections idle longer than " + idleTimeout + " " + tunit);
+ }
+ this.pool.closeIdle(idleTimeout, tunit);
+ }
+
+ public void closeExpiredConnections() {
+ this.log.debug("Closing expired connections");
+ this.pool.closeExpired();
+ }
+
+ public int getMaxTotal() {
+ return this.pool.getMaxTotal();
+ }
+
+ public void setMaxTotal(final int max) {
+ this.pool.setMaxTotal(max);
+ }
+
+ public int getDefaultMaxPerRoute() {
+ return this.pool.getDefaultMaxPerRoute();
+ }
+
+ public void setDefaultMaxPerRoute(final int max) {
+ this.pool.setDefaultMaxPerRoute(max);
+ }
+
+ public int getMaxPerRoute(final HttpRoute route) {
+ return this.pool.getMaxPerRoute(route);
+ }
+
+ public void setMaxPerRoute(final HttpRoute route, final int max) {
+ this.pool.setMaxPerRoute(route, max);
+ }
+
+ public PoolStats getTotalStats() {
+ return this.pool.getTotalStats();
+ }
+
+ public PoolStats getStats(final HttpRoute route) {
+ return this.pool.getStats(route);
+ }
+
+ public SocketConfig getDefaultSocketConfig() {
+ return this.configData.getDefaultSocketConfig();
+ }
+
+ public void setDefaultSocketConfig(final SocketConfig defaultSocketConfig) {
+ this.configData.setDefaultSocketConfig(defaultSocketConfig);
+ }
+
+ public ConnectionConfig getDefaultConnectionConfig() {
+ return this.configData.getDefaultConnectionConfig();
+ }
+
+ public void setDefaultConnectionConfig(final ConnectionConfig defaultConnectionConfig) {
+ this.configData.setDefaultConnectionConfig(defaultConnectionConfig);
+ }
+
+ public SocketConfig getSocketConfig(final HttpHost host) {
+ return this.configData.getSocketConfig(host);
+ }
+
+ public void setSocketConfig(final HttpHost host, final SocketConfig socketConfig) {
+ this.configData.setSocketConfig(host, socketConfig);
+ }
+
+ public ConnectionConfig getConnectionConfig(final HttpHost host) {
+ return this.configData.getConnectionConfig(host);
+ }
+
+ public void setConnectionConfig(final HttpHost host, final ConnectionConfig connectionConfig) {
+ this.configData.setConnectionConfig(host, connectionConfig);
+ }
+
+ static class ConfigData {
+
+ private final Map<HttpHost, SocketConfig> socketConfigMap;
+ private final Map<HttpHost, ConnectionConfig> connectionConfigMap;
+ private volatile SocketConfig defaultSocketConfig;
+ private volatile ConnectionConfig defaultConnectionConfig;
+
+ ConfigData() {
+ super();
+ this.socketConfigMap = new ConcurrentHashMap<HttpHost, SocketConfig>();
+ this.connectionConfigMap = new ConcurrentHashMap<HttpHost, ConnectionConfig>();
+ }
+
+ public SocketConfig getDefaultSocketConfig() {
+ return this.defaultSocketConfig;
+ }
+
+ public void setDefaultSocketConfig(final SocketConfig defaultSocketConfig) {
+ this.defaultSocketConfig = defaultSocketConfig;
+ }
+
+ public ConnectionConfig getDefaultConnectionConfig() {
+ return this.defaultConnectionConfig;
+ }
+
+ public void setDefaultConnectionConfig(final ConnectionConfig defaultConnectionConfig) {
+ this.defaultConnectionConfig = defaultConnectionConfig;
+ }
+
+ public SocketConfig getSocketConfig(final HttpHost host) {
+ return this.socketConfigMap.get(host);
+ }
+
+ public void setSocketConfig(final HttpHost host, final SocketConfig socketConfig) {
+ this.socketConfigMap.put(host, socketConfig);
+ }
+
+ public ConnectionConfig getConnectionConfig(final HttpHost host) {
+ return this.connectionConfigMap.get(host);
+ }
+
+ public void setConnectionConfig(final HttpHost host, final ConnectionConfig connectionConfig) {
+ this.connectionConfigMap.put(host, connectionConfig);
+ }
+
+ }
+
+ static class InternalConnectionFactory implements ConnFactory<HttpRoute, ManagedHttpClientConnection> {
+
+ private final ConfigData configData;
+ private final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory;
+
+ InternalConnectionFactory(
+ final ConfigData configData,
+ final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory) {
+ super();
+ this.configData = configData != null ? configData : new ConfigData();
+ this.connFactory = connFactory != null ? connFactory :
+ ManagedHttpClientConnectionFactory.INSTANCE;
+ }
+
+ public ManagedHttpClientConnection create(final HttpRoute route) throws IOException {
+ ConnectionConfig config = null;
+ if (route.getProxyHost() != null) {
+ config = this.configData.getConnectionConfig(route.getProxyHost());
+ }
+ if (config == null) {
+ config = this.configData.getConnectionConfig(route.getTargetHost());
+ }
+ if (config == null) {
+ config = this.configData.getDefaultConnectionConfig();
+ }
+ if (config == null) {
+ config = ConnectionConfig.DEFAULT;
+ }
+ return this.connFactory.create(route, config);
+ }
+
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/ProxySelectorRoutePlanner.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/ProxySelectorRoutePlanner.java
new file mode 100644
index 0000000000..8c68e5e189
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/ProxySelectorRoutePlanner.java
@@ -0,0 +1,279 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.conn;
+
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.conn.params.ConnRouteParams;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoutePlanner;
+import ch.boye.httpclientandroidlib.conn.scheme.Scheme;
+import ch.boye.httpclientandroidlib.conn.scheme.SchemeRegistry;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+
+
+/**
+ * Default implementation of an {@link HttpRoutePlanner}.
+ * This implementation is based on {@link java.net.ProxySelector}.
+ * By default, it will pick up the proxy settings of the JVM, either
+ * from system properties or from the browser running the application.
+ * Additionally, it interprets some
+ * {@link ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames parameters},
+ * though not the {@link
+ * ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames#DEFAULT_PROXY DEFAULT_PROXY}.
+ * <p>
+ * The following parameters can be used to customize the behavior of this
+ * class:
+ * <ul>
+ * <li>{@link ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames#LOCAL_ADDRESS}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames#FORCED_ROUTE}</li>
+ * </ul>
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link SystemDefaultRoutePlanner}
+ */
+@NotThreadSafe // e.g [gs]etProxySelector()
+@Deprecated
+public class ProxySelectorRoutePlanner implements HttpRoutePlanner {
+
+ /** The scheme registry. */
+ protected final SchemeRegistry schemeRegistry; // @ThreadSafe
+
+ /** The proxy selector to use, or <code>null</code> for system default. */
+ protected ProxySelector proxySelector;
+
+ /**
+ * Creates a new proxy selector route planner.
+ *
+ * @param schreg the scheme registry
+ * @param prosel the proxy selector, or
+ * <code>null</code> for the system default
+ */
+ public ProxySelectorRoutePlanner(final SchemeRegistry schreg,
+ final ProxySelector prosel) {
+ Args.notNull(schreg, "SchemeRegistry");
+ schemeRegistry = schreg;
+ proxySelector = prosel;
+ }
+
+ /**
+ * Obtains the proxy selector to use.
+ *
+ * @return the proxy selector, or <code>null</code> for the system default
+ */
+ public ProxySelector getProxySelector() {
+ return this.proxySelector;
+ }
+
+ /**
+ * Sets the proxy selector to use.
+ *
+ * @param prosel the proxy selector, or
+ * <code>null</code> to use the system default
+ */
+ public void setProxySelector(final ProxySelector prosel) {
+ this.proxySelector = prosel;
+ }
+
+ public HttpRoute determineRoute(final HttpHost target,
+ final HttpRequest request,
+ final HttpContext context)
+ throws HttpException {
+
+ Args.notNull(request, "HTTP request");
+
+ // If we have a forced route, we can do without a target.
+ HttpRoute route =
+ ConnRouteParams.getForcedRoute(request.getParams());
+ if (route != null) {
+ return route;
+ }
+
+ // If we get here, there is no forced route.
+ // So we need a target to compute a route.
+
+ Asserts.notNull(target, "Target host");
+
+ final InetAddress local =
+ ConnRouteParams.getLocalAddress(request.getParams());
+ final HttpHost proxy = determineProxy(target, request, context);
+
+ final Scheme schm =
+ this.schemeRegistry.getScheme(target.getSchemeName());
+ // as it is typically used for TLS/SSL, we assume that
+ // a layered scheme implies a secure connection
+ final boolean secure = schm.isLayered();
+
+ if (proxy == null) {
+ route = new HttpRoute(target, local, secure);
+ } else {
+ route = new HttpRoute(target, local, proxy, secure);
+ }
+ return route;
+ }
+
+ /**
+ * Determines a proxy for the given target.
+ *
+ * @param target the planned target, never <code>null</code>
+ * @param request the request to be sent, never <code>null</code>
+ * @param context the context, or <code>null</code>
+ *
+ * @return the proxy to use, or <code>null</code> for a direct route
+ *
+ * @throws HttpException
+ * in case of system proxy settings that cannot be handled
+ */
+ protected HttpHost determineProxy(final HttpHost target,
+ final HttpRequest request,
+ final HttpContext context)
+ throws HttpException {
+
+ // the proxy selector can be 'unset', so we better deal with null here
+ ProxySelector psel = this.proxySelector;
+ if (psel == null) {
+ psel = ProxySelector.getDefault();
+ }
+ if (psel == null) {
+ return null;
+ }
+
+ URI targetURI = null;
+ try {
+ targetURI = new URI(target.toURI());
+ } catch (final URISyntaxException usx) {
+ throw new HttpException
+ ("Cannot convert host to URI: " + target, usx);
+ }
+ final List<Proxy> proxies = psel.select(targetURI);
+
+ final Proxy p = chooseProxy(proxies, target, request, context);
+
+ HttpHost result = null;
+ if (p.type() == Proxy.Type.HTTP) {
+ // convert the socket address to an HttpHost
+ if (!(p.address() instanceof InetSocketAddress)) {
+ throw new HttpException
+ ("Unable to handle non-Inet proxy address: "+p.address());
+ }
+ final InetSocketAddress isa = (InetSocketAddress) p.address();
+ // assume default scheme (http)
+ result = new HttpHost(getHost(isa), isa.getPort());
+ }
+
+ return result;
+ }
+
+ /**
+ * Obtains a host from an {@link InetSocketAddress}.
+ *
+ * @param isa the socket address
+ *
+ * @return a host string, either as a symbolic name or
+ * as a literal IP address string
+ * <br/>
+ * (TODO: determine format for IPv6 addresses, with or without [brackets])
+ */
+ protected String getHost(final InetSocketAddress isa) {
+
+ //@@@ Will this work with literal IPv6 addresses, or do we
+ //@@@ need to wrap these in [] for the string representation?
+ //@@@ Having it in this method at least allows for easy workarounds.
+ return isa.isUnresolved() ?
+ isa.getHostName() : isa.getAddress().getHostAddress();
+
+ }
+
+ /**
+ * Chooses a proxy from a list of available proxies.
+ * The default implementation just picks the first non-SOCKS proxy
+ * from the list. If there are only SOCKS proxies,
+ * {@link Proxy#NO_PROXY Proxy.NO_PROXY} is returned.
+ * Derived classes may implement more advanced strategies,
+ * such as proxy rotation if there are multiple options.
+ *
+ * @param proxies the list of proxies to choose from,
+ * never <code>null</code> or empty
+ * @param target the planned target, never <code>null</code>
+ * @param request the request to be sent, never <code>null</code>
+ * @param context the context, or <code>null</code>
+ *
+ * @return a proxy type
+ */
+ protected Proxy chooseProxy(final List<Proxy> proxies,
+ final HttpHost target,
+ final HttpRequest request,
+ final HttpContext context) {
+ Args.notEmpty(proxies, "List of proxies");
+
+ Proxy result = null;
+
+ // check the list for one we can use
+ for (int i=0; (result == null) && (i < proxies.size()); i++) {
+
+ final Proxy p = proxies.get(i);
+ switch (p.type()) {
+
+ case DIRECT:
+ case HTTP:
+ result = p;
+ break;
+
+ case SOCKS:
+ // SOCKS hosts are not handled on the route level.
+ // The socket may make use of the SOCKS host though.
+ break;
+ }
+ }
+
+ if (result == null) {
+ //@@@ log as warning or info that only a socks proxy is available?
+ // result can only be null if all proxies are socks proxies
+ // socks proxies are not handled on the route planning level
+ result = Proxy.NO_PROXY;
+ }
+
+ return result;
+ }
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/SchemeRegistryFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/SchemeRegistryFactory.java
new file mode 100644
index 0000000000..0a17993d1b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/SchemeRegistryFactory.java
@@ -0,0 +1,90 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.conn.scheme.PlainSocketFactory;
+import ch.boye.httpclientandroidlib.conn.scheme.Scheme;
+import ch.boye.httpclientandroidlib.conn.scheme.SchemeRegistry;
+import ch.boye.httpclientandroidlib.conn.ssl.SSLSocketFactory;
+
+/**
+ * @since 4.1
+ *
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.impl.client.HttpClientBuilder}.
+ */
+@ThreadSafe
+@Deprecated
+public final class SchemeRegistryFactory {
+
+ /**
+ * Initializes default scheme registry based on JSSE defaults. System properties will
+ * not be taken into consideration.
+ */
+ public static SchemeRegistry createDefault() {
+ final SchemeRegistry registry = new SchemeRegistry();
+ registry.register(
+ new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));
+ registry.register(
+ new Scheme("https", 443, SSLSocketFactory.getSocketFactory()));
+ return registry;
+ }
+
+ /**
+ * Initializes default scheme registry using system properties as described in
+ * <a href="http://download.oracle.com/javase/1,5.0/docs/guide/security/jsse/JSSERefGuide.html">
+ * "JavaTM Secure Socket Extension (JSSE) Reference Guide for the JavaTM 2 Platform
+ * Standard Edition 5</a>
+ * <p>
+ * The following system properties are taken into account by this method:
+ * <ul>
+ * <li>ssl.TrustManagerFactory.algorithm</li>
+ * <li>javax.net.ssl.trustStoreType</li>
+ * <li>javax.net.ssl.trustStore</li>
+ * <li>javax.net.ssl.trustStoreProvider</li>
+ * <li>javax.net.ssl.trustStorePassword</li>
+ * <li>java.home</li>
+ * <li>ssl.KeyManagerFactory.algorithm</li>
+ * <li>javax.net.ssl.keyStoreType</li>
+ * <li>javax.net.ssl.keyStore</li>
+ * <li>javax.net.ssl.keyStoreProvider</li>
+ * <li>javax.net.ssl.keyStorePassword</li>
+ * </ul>
+ * <p>
+ *
+ * @since 4.2
+ */
+ public static SchemeRegistry createSystemDefault() {
+ final SchemeRegistry registry = new SchemeRegistry();
+ registry.register(
+ new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));
+ registry.register(
+ new Scheme("https", 443, SSLSocketFactory.getSystemSocketFactory()));
+ return registry;
+ }
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/SingleClientConnManager.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/SingleClientConnManager.java
new file mode 100644
index 0000000000..c2b6c87995
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/SingleClientConnManager.java
@@ -0,0 +1,427 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.annotation.GuardedBy;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionManager;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionOperator;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionRequest;
+import ch.boye.httpclientandroidlib.conn.ManagedClientConnection;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.conn.routing.RouteTracker;
+import ch.boye.httpclientandroidlib.conn.scheme.SchemeRegistry;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+
+/**
+ * A connection manager for a single connection. This connection manager
+ * maintains only one active connection at a time. Even though this class
+ * is thread-safe it ought to be used by one execution thread only.
+ * <p>
+ * SingleClientConnManager will make an effort to reuse the connection
+ * for subsequent requests with the same {@link HttpRoute route}.
+ * It will, however, close the existing connection and open it
+ * for the given route, if the route of the persistent connection does
+ * not match that of the connection request. If the connection has been
+ * already been allocated {@link IllegalStateException} is thrown.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) use {@link BasicClientConnectionManager}
+ */
+@ThreadSafe
+@Deprecated
+public class SingleClientConnManager implements ClientConnectionManager {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ /** The message to be logged on multiple allocation. */
+ public final static String MISUSE_MESSAGE =
+ "Invalid use of SingleClientConnManager: connection still allocated.\n" +
+ "Make sure to release the connection before allocating another one.";
+
+ /** The schemes supported by this connection manager. */
+ protected final SchemeRegistry schemeRegistry;
+
+ /** The operator for opening and updating connections. */
+ protected final ClientConnectionOperator connOperator;
+
+ /** Whether the connection should be shut down on release. */
+ protected final boolean alwaysShutDown;
+
+ /** The one and only entry in this pool. */
+ @GuardedBy("this")
+ protected volatile PoolEntry uniquePoolEntry;
+
+ /** The currently issued managed connection, if any. */
+ @GuardedBy("this")
+ protected volatile ConnAdapter managedConn;
+
+ /** The time of the last connection release, or -1. */
+ @GuardedBy("this")
+ protected volatile long lastReleaseTime;
+
+ /** The time the last released connection expires and shouldn't be reused. */
+ @GuardedBy("this")
+ protected volatile long connectionExpiresTime;
+
+ /** Indicates whether this connection manager is shut down. */
+ protected volatile boolean isShutDown;
+
+ /**
+ * Creates a new simple connection manager.
+ *
+ * @param params the parameters for this manager
+ * @param schreg the scheme registry
+ *
+ * @deprecated (4.1) use {@link SingleClientConnManager#SingleClientConnManager(SchemeRegistry)}
+ */
+ @Deprecated
+ public SingleClientConnManager(final HttpParams params,
+ final SchemeRegistry schreg) {
+ this(schreg);
+ }
+ /**
+ * Creates a new simple connection manager.
+ *
+ * @param schreg the scheme registry
+ */
+ public SingleClientConnManager(final SchemeRegistry schreg) {
+ Args.notNull(schreg, "Scheme registry");
+ this.schemeRegistry = schreg;
+ this.connOperator = createConnectionOperator(schreg);
+ this.uniquePoolEntry = new PoolEntry();
+ this.managedConn = null;
+ this.lastReleaseTime = -1L;
+ this.alwaysShutDown = false; //@@@ from params? as argument?
+ this.isShutDown = false;
+ }
+
+ /**
+ * @since 4.1
+ */
+ public SingleClientConnManager() {
+ this(SchemeRegistryFactory.createDefault());
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ shutdown();
+ } finally { // Make sure we call overridden method even if shutdown barfs
+ super.finalize();
+ }
+ }
+
+ public SchemeRegistry getSchemeRegistry() {
+ return this.schemeRegistry;
+ }
+
+ /**
+ * Hook for creating the connection operator.
+ * It is called by the constructor.
+ * Derived classes can override this method to change the
+ * instantiation of the operator.
+ * The default implementation here instantiates
+ * {@link DefaultClientConnectionOperator DefaultClientConnectionOperator}.
+ *
+ * @param schreg the scheme registry to use, or <code>null</code>
+ *
+ * @return the connection operator to use
+ */
+ protected ClientConnectionOperator
+ createConnectionOperator(final SchemeRegistry schreg) {
+ return new DefaultClientConnectionOperator(schreg);
+ }
+
+ /**
+ * Asserts that this manager is not shut down.
+ *
+ * @throws IllegalStateException if this manager is shut down
+ */
+ protected final void assertStillUp() throws IllegalStateException {
+ Asserts.check(!this.isShutDown, "Manager is shut down");
+ }
+
+ public final ClientConnectionRequest requestConnection(
+ final HttpRoute route,
+ final Object state) {
+
+ return new ClientConnectionRequest() {
+
+ public void abortRequest() {
+ // Nothing to abort, since requests are immediate.
+ }
+
+ public ManagedClientConnection getConnection(
+ final long timeout, final TimeUnit tunit) {
+ return SingleClientConnManager.this.getConnection(
+ route, state);
+ }
+
+ };
+ }
+
+ /**
+ * Obtains a connection.
+ *
+ * @param route where the connection should point to
+ *
+ * @return a connection that can be used to communicate
+ * along the given route
+ */
+ public ManagedClientConnection getConnection(final HttpRoute route, final Object state) {
+ Args.notNull(route, "Route");
+ assertStillUp();
+
+ if (log.isDebugEnabled()) {
+ log.debug("Get connection for route " + route);
+ }
+
+ synchronized (this) {
+
+ Asserts.check(managedConn == null, MISUSE_MESSAGE);
+
+ // check re-usability of the connection
+ boolean recreate = false;
+ boolean shutdown = false;
+
+ // Kill the connection if it expired.
+ closeExpiredConnections();
+
+ if (uniquePoolEntry.connection.isOpen()) {
+ final RouteTracker tracker = uniquePoolEntry.tracker;
+ shutdown = (tracker == null || // can happen if method is aborted
+ !tracker.toRoute().equals(route));
+ } else {
+ // If the connection is not open, create a new PoolEntry,
+ // as the connection may have been marked not reusable,
+ // due to aborts -- and the PoolEntry should not be reused
+ // either. There's no harm in recreating an entry if
+ // the connection is closed.
+ recreate = true;
+ }
+
+ if (shutdown) {
+ recreate = true;
+ try {
+ uniquePoolEntry.shutdown();
+ } catch (final IOException iox) {
+ log.debug("Problem shutting down connection.", iox);
+ }
+ }
+
+ if (recreate) {
+ uniquePoolEntry = new PoolEntry();
+ }
+
+ managedConn = new ConnAdapter(uniquePoolEntry, route);
+
+ return managedConn;
+ }
+ }
+
+ public void releaseConnection(
+ final ManagedClientConnection conn,
+ final long validDuration, final TimeUnit timeUnit) {
+ Args.check(conn instanceof ConnAdapter, "Connection class mismatch, " +
+ "connection not obtained from this manager");
+ assertStillUp();
+
+ if (log.isDebugEnabled()) {
+ log.debug("Releasing connection " + conn);
+ }
+
+ final ConnAdapter sca = (ConnAdapter) conn;
+ synchronized (sca) {
+ if (sca.poolEntry == null)
+ {
+ return; // already released
+ }
+ final ClientConnectionManager manager = sca.getManager();
+ Asserts.check(manager == this, "Connection not obtained from this manager");
+ try {
+ // make sure that the response has been read completely
+ if (sca.isOpen() && (this.alwaysShutDown ||
+ !sca.isMarkedReusable())
+ ) {
+ if (log.isDebugEnabled()) {
+ log.debug
+ ("Released connection open but not reusable.");
+ }
+
+ // make sure this connection will not be re-used
+ // we might have gotten here because of a shutdown trigger
+ // shutdown of the adapter also clears the tracked route
+ sca.shutdown();
+ }
+ } catch (final IOException iox) {
+ if (log.isDebugEnabled()) {
+ log.debug("Exception shutting down released connection.",
+ iox);
+ }
+ } finally {
+ sca.detach();
+ synchronized (this) {
+ managedConn = null;
+ lastReleaseTime = System.currentTimeMillis();
+ if(validDuration > 0) {
+ connectionExpiresTime = timeUnit.toMillis(validDuration) + lastReleaseTime;
+ } else {
+ connectionExpiresTime = Long.MAX_VALUE;
+ }
+ }
+ }
+ }
+ }
+
+ public void closeExpiredConnections() {
+ final long time = connectionExpiresTime;
+ if (System.currentTimeMillis() >= time) {
+ closeIdleConnections(0, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ public void closeIdleConnections(final long idletime, final TimeUnit tunit) {
+ assertStillUp();
+
+ // idletime can be 0 or negative, no problem there
+ Args.notNull(tunit, "Time unit");
+
+ synchronized (this) {
+ if ((managedConn == null) && uniquePoolEntry.connection.isOpen()) {
+ final long cutoff =
+ System.currentTimeMillis() - tunit.toMillis(idletime);
+ if (lastReleaseTime <= cutoff) {
+ try {
+ uniquePoolEntry.close();
+ } catch (final IOException iox) {
+ // ignore
+ log.debug("Problem closing idle connection.", iox);
+ }
+ }
+ }
+ }
+ }
+
+ public void shutdown() {
+ this.isShutDown = true;
+ synchronized (this) {
+ try {
+ if (uniquePoolEntry != null) {
+ uniquePoolEntry.shutdown();
+ }
+ } catch (final IOException iox) {
+ // ignore
+ log.debug("Problem while shutting down manager.", iox);
+ } finally {
+ uniquePoolEntry = null;
+ managedConn = null;
+ }
+ }
+ }
+
+ protected void revokeConnection() {
+ final ConnAdapter conn = managedConn;
+ if (conn == null) {
+ return;
+ }
+ conn.detach();
+
+ synchronized (this) {
+ try {
+ uniquePoolEntry.shutdown();
+ } catch (final IOException iox) {
+ // ignore
+ log.debug("Problem while shutting down connection.", iox);
+ }
+ }
+ }
+
+ /**
+ * The pool entry for this connection manager.
+ */
+ protected class PoolEntry extends AbstractPoolEntry {
+
+ /**
+ * Creates a new pool entry.
+ *
+ */
+ protected PoolEntry() {
+ super(SingleClientConnManager.this.connOperator, null);
+ }
+
+ /**
+ * Closes the connection in this pool entry.
+ */
+ protected void close() throws IOException {
+ shutdownEntry();
+ if (connection.isOpen()) {
+ connection.close();
+ }
+ }
+
+ /**
+ * Shuts down the connection in this pool entry.
+ */
+ protected void shutdown() throws IOException {
+ shutdownEntry();
+ if (connection.isOpen()) {
+ connection.shutdown();
+ }
+ }
+
+ }
+
+ /**
+ * The connection adapter used by this manager.
+ */
+ protected class ConnAdapter extends AbstractPooledConnAdapter {
+
+ /**
+ * Creates a new connection adapter.
+ *
+ * @param entry the pool entry for the connection being wrapped
+ * @param route the planned route for this connection
+ */
+ protected ConnAdapter(final PoolEntry entry, final HttpRoute route) {
+ super(SingleClientConnManager.this, entry);
+ markReusable();
+ entry.route = route;
+ }
+
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/SystemDefaultDnsResolver.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/SystemDefaultDnsResolver.java
new file mode 100644
index 0000000000..bb6b7c60d1
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/SystemDefaultDnsResolver.java
@@ -0,0 +1,47 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import ch.boye.httpclientandroidlib.conn.DnsResolver;
+
+/**
+ * DNS resolver that uses the default OS implementation for resolving host names.
+ *
+ * @since 4.2
+ */
+public class SystemDefaultDnsResolver implements DnsResolver {
+
+ public static final SystemDefaultDnsResolver INSTANCE = new SystemDefaultDnsResolver();
+
+ public InetAddress[] resolve(final String host) throws UnknownHostException {
+ return InetAddress.getAllByName(host);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/SystemDefaultRoutePlanner.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/SystemDefaultRoutePlanner.java
new file mode 100644
index 0000000000..27ea2a3d0b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/SystemDefaultRoutePlanner.java
@@ -0,0 +1,132 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.conn.SchemePortResolver;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * {@link ch.boye.httpclientandroidlib.conn.routing.HttpRoutePlanner} implementation
+ * based on {@link ProxySelector}. By default, this class will pick up
+ * the proxy settings of the JVM, either from system properties
+ * or from the browser running the application.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class SystemDefaultRoutePlanner extends DefaultRoutePlanner {
+
+ private final ProxySelector proxySelector;
+
+ public SystemDefaultRoutePlanner(
+ final SchemePortResolver schemePortResolver,
+ final ProxySelector proxySelector) {
+ super(schemePortResolver);
+ this.proxySelector = proxySelector != null ? proxySelector : ProxySelector.getDefault();
+ }
+
+ public SystemDefaultRoutePlanner(final ProxySelector proxySelector) {
+ this(null, proxySelector);
+ }
+
+ @Override
+ protected HttpHost determineProxy(
+ final HttpHost target,
+ final HttpRequest request,
+ final HttpContext context) throws HttpException {
+ final URI targetURI;
+ try {
+ targetURI = new URI(target.toURI());
+ } catch (final URISyntaxException ex) {
+ throw new HttpException("Cannot convert host to URI: " + target, ex);
+ }
+ final List<Proxy> proxies = this.proxySelector.select(targetURI);
+ final Proxy p = chooseProxy(proxies);
+ HttpHost result = null;
+ if (p.type() == Proxy.Type.HTTP) {
+ // convert the socket address to an HttpHost
+ if (!(p.address() instanceof InetSocketAddress)) {
+ throw new HttpException("Unable to handle non-Inet proxy address: " + p.address());
+ }
+ final InetSocketAddress isa = (InetSocketAddress) p.address();
+ // assume default scheme (http)
+ result = new HttpHost(getHost(isa), isa.getPort());
+ }
+
+ return result;
+ }
+
+ private String getHost(final InetSocketAddress isa) {
+
+ //@@@ Will this work with literal IPv6 addresses, or do we
+ //@@@ need to wrap these in [] for the string representation?
+ //@@@ Having it in this method at least allows for easy workarounds.
+ return isa.isUnresolved() ?
+ isa.getHostName() : isa.getAddress().getHostAddress();
+
+ }
+
+ private Proxy chooseProxy(final List<Proxy> proxies) {
+ Proxy result = null;
+ // check the list for one we can use
+ for (int i=0; (result == null) && (i < proxies.size()); i++) {
+ final Proxy p = proxies.get(i);
+ switch (p.type()) {
+
+ case DIRECT:
+ case HTTP:
+ result = p;
+ break;
+
+ case SOCKS:
+ // SOCKS hosts are not handled on the route level.
+ // The socket may make use of the SOCKS host though.
+ break;
+ }
+ }
+ if (result == null) {
+ //@@@ log as warning or info that only a socks proxy is available?
+ // result can only be null if all proxies are socks proxies
+ // socks proxies are not handled on the route planning level
+ result = Proxy.NO_PROXY;
+ }
+ return result;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/Wire.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/Wire.java
new file mode 100644
index 0000000000..1a678f05d6
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/Wire.java
@@ -0,0 +1,152 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Logs data to the wire LOG.
+ * TODO: make package private. Should not be part of the public API.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class Wire {
+
+ public HttpClientAndroidLog log;
+ private final String id;
+
+ /**
+ * @since 4.3
+ */
+ public Wire(final HttpClientAndroidLog log, final String id) {
+ this.log = log;
+ this.id = id;
+ }
+
+ public Wire(final HttpClientAndroidLog log) {
+ this(log, "");
+ }
+
+ private void wire(final String header, final InputStream instream)
+ throws IOException {
+ final StringBuilder buffer = new StringBuilder();
+ int ch;
+ while ((ch = instream.read()) != -1) {
+ if (ch == 13) {
+ buffer.append("[\\r]");
+ } else if (ch == 10) {
+ buffer.append("[\\n]\"");
+ buffer.insert(0, "\"");
+ buffer.insert(0, header);
+ log.debug(id + " " + buffer.toString());
+ buffer.setLength(0);
+ } else if ((ch < 32) || (ch > 127)) {
+ buffer.append("[0x");
+ buffer.append(Integer.toHexString(ch));
+ buffer.append("]");
+ } else {
+ buffer.append((char) ch);
+ }
+ }
+ if (buffer.length() > 0) {
+ buffer.append('\"');
+ buffer.insert(0, '\"');
+ buffer.insert(0, header);
+ log.debug(id + " " + buffer.toString());
+ }
+ }
+
+
+ public boolean enabled() {
+ return log.isDebugEnabled();
+ }
+
+ public void output(final InputStream outstream)
+ throws IOException {
+ Args.notNull(outstream, "Output");
+ wire(">> ", outstream);
+ }
+
+ public void input(final InputStream instream)
+ throws IOException {
+ Args.notNull(instream, "Input");
+ wire("<< ", instream);
+ }
+
+ public void output(final byte[] b, final int off, final int len)
+ throws IOException {
+ Args.notNull(b, "Output");
+ wire(">> ", new ByteArrayInputStream(b, off, len));
+ }
+
+ public void input(final byte[] b, final int off, final int len)
+ throws IOException {
+ Args.notNull(b, "Input");
+ wire("<< ", new ByteArrayInputStream(b, off, len));
+ }
+
+ public void output(final byte[] b)
+ throws IOException {
+ Args.notNull(b, "Output");
+ wire(">> ", new ByteArrayInputStream(b));
+ }
+
+ public void input(final byte[] b)
+ throws IOException {
+ Args.notNull(b, "Input");
+ wire("<< ", new ByteArrayInputStream(b));
+ }
+
+ public void output(final int b)
+ throws IOException {
+ output(new byte[] {(byte) b});
+ }
+
+ public void input(final int b)
+ throws IOException {
+ input(new byte[] {(byte) b});
+ }
+
+ public void output(final String s)
+ throws IOException {
+ Args.notNull(s, "Output");
+ output(s.getBytes());
+ }
+
+ public void input(final String s)
+ throws IOException {
+ Args.notNull(s, "Input");
+ input(s.getBytes());
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/package-info.java
new file mode 100644
index 0000000000..49305e4b77
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Default implementations of client connection management
+ * functions.
+ */
+package ch.boye.httpclientandroidlib.impl.conn;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/AbstractConnPool.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/AbstractConnPool.java
new file mode 100644
index 0000000000..f85539c2d9
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/AbstractConnPool.java
@@ -0,0 +1,234 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn.tsccm;
+
+import java.io.IOException;
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.annotation.GuardedBy;
+import ch.boye.httpclientandroidlib.conn.ConnectionPoolTimeoutException;
+import ch.boye.httpclientandroidlib.conn.OperatedClientConnection;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.impl.conn.IdleConnectionHandler;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * An abstract connection pool.
+ * It is used by the {@link ThreadSafeClientConnManager}.
+ * The abstract pool includes a {@link #poolLock}, which is used to
+ * synchronize access to the internal pool datastructures.
+ * Don't use <code>synchronized</code> for that purpose!
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) use {@link ch.boye.httpclientandroidlib.pool.AbstractConnPool}
+ */
+@Deprecated
+public abstract class AbstractConnPool {
+
+ public HttpClientAndroidLog log;
+
+ /**
+ * The global lock for this pool.
+ */
+ protected final Lock poolLock;
+
+ /** References to issued connections */
+ @GuardedBy("poolLock")
+ protected Set<BasicPoolEntry> leasedConnections;
+
+ /** The current total number of connections. */
+ @GuardedBy("poolLock")
+ protected int numConnections;
+
+ /** Indicates whether this pool is shut down. */
+ protected volatile boolean isShutDown;
+
+ protected Set<BasicPoolEntryRef> issuedConnections;
+
+ protected ReferenceQueue<Object> refQueue;
+
+ protected IdleConnectionHandler idleConnHandler;
+
+ /**
+ * Creates a new connection pool.
+ */
+ protected AbstractConnPool() {
+ super();
+ this.log = new HttpClientAndroidLog(getClass());
+ this.leasedConnections = new HashSet<BasicPoolEntry>();
+ this.idleConnHandler = new IdleConnectionHandler();
+ this.poolLock = new ReentrantLock();
+ }
+
+ public void enableConnectionGC()
+ throws IllegalStateException {
+ }
+
+ /**
+ * Obtains a pool entry with a connection within the given timeout.
+ *
+ * @param route the route for which to get the connection
+ * @param timeout the timeout, 0 or negative for no timeout
+ * @param tunit the unit for the <code>timeout</code>,
+ * may be <code>null</code> only if there is no timeout
+ *
+ * @return pool entry holding a connection for the route
+ *
+ * @throws ConnectionPoolTimeoutException
+ * if the timeout expired
+ * @throws InterruptedException
+ * if the calling thread was interrupted
+ */
+ public final
+ BasicPoolEntry getEntry(
+ final HttpRoute route,
+ final Object state,
+ final long timeout,
+ final TimeUnit tunit)
+ throws ConnectionPoolTimeoutException, InterruptedException {
+ return requestPoolEntry(route, state).getPoolEntry(timeout, tunit);
+ }
+
+ /**
+ * Returns a new {@link PoolEntryRequest}, from which a {@link BasicPoolEntry}
+ * can be obtained, or the request can be aborted.
+ */
+ public abstract PoolEntryRequest requestPoolEntry(HttpRoute route, Object state);
+
+
+ /**
+ * Returns an entry into the pool.
+ * The connection of the entry is expected to be in a suitable state,
+ * either open and re-usable, or closed. The pool will not make any
+ * attempt to determine whether it can be re-used or not.
+ *
+ * @param entry the entry for the connection to release
+ * @param reusable <code>true</code> if the entry is deemed
+ * reusable, <code>false</code> otherwise.
+ * @param validDuration The duration that the entry should remain free and reusable.
+ * @param timeUnit The unit of time the duration is measured in.
+ */
+ public abstract void freeEntry(BasicPoolEntry entry, boolean reusable, long validDuration, TimeUnit timeUnit)
+ ;
+
+ public void handleReference(final Reference<?> ref) {
+ }
+
+ protected abstract void handleLostEntry(HttpRoute route);
+
+ /**
+ * Closes idle connections.
+ *
+ * @param idletime the time the connections should have been idle
+ * in order to be closed now
+ * @param tunit the unit for the <code>idletime</code>
+ */
+ public void closeIdleConnections(final long idletime, final TimeUnit tunit) {
+
+ // idletime can be 0 or negative, no problem there
+ Args.notNull(tunit, "Time unit");
+
+ poolLock.lock();
+ try {
+ idleConnHandler.closeIdleConnections(tunit.toMillis(idletime));
+ } finally {
+ poolLock.unlock();
+ }
+ }
+
+ public void closeExpiredConnections() {
+ poolLock.lock();
+ try {
+ idleConnHandler.closeExpiredConnections();
+ } finally {
+ poolLock.unlock();
+ }
+ }
+
+
+ /**
+ * Deletes all entries for closed connections.
+ */
+ public abstract void deleteClosedConnections();
+
+ /**
+ * Shuts down this pool and all associated resources.
+ * Overriding methods MUST call the implementation here!
+ */
+ public void shutdown() {
+
+ poolLock.lock();
+ try {
+
+ if (isShutDown) {
+ return;
+ }
+
+ // close all connections that are issued to an application
+ final Iterator<BasicPoolEntry> iter = leasedConnections.iterator();
+ while (iter.hasNext()) {
+ final BasicPoolEntry entry = iter.next();
+ iter.remove();
+ closeConnection(entry.getConnection());
+ }
+ idleConnHandler.removeAll();
+
+ isShutDown = true;
+
+ } finally {
+ poolLock.unlock();
+ }
+ }
+
+
+ /**
+ * Closes a connection from this pool.
+ *
+ * @param conn the connection to close, or <code>null</code>
+ */
+ protected void closeConnection(final OperatedClientConnection conn) {
+ if (conn != null) {
+ try {
+ conn.close();
+ } catch (final IOException ex) {
+ log.debug("I/O error closing connection", ex);
+ }
+ }
+ }
+
+} // class AbstractConnPool
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/BasicPoolEntry.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/BasicPoolEntry.java
new file mode 100644
index 0000000000..2c5bf05cec
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/BasicPoolEntry.java
@@ -0,0 +1,163 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn.tsccm;
+
+import java.lang.ref.ReferenceQueue;
+import java.util.concurrent.TimeUnit;
+
+import ch.boye.httpclientandroidlib.conn.ClientConnectionOperator;
+import ch.boye.httpclientandroidlib.conn.OperatedClientConnection;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.impl.conn.AbstractPoolEntry;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Basic implementation of a connection pool entry.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) use {@link ch.boye.httpclientandroidlib.pool.PoolEntry}
+ */
+@Deprecated
+public class BasicPoolEntry extends AbstractPoolEntry {
+
+ private final long created;
+
+ private long updated;
+ private final long validUntil;
+ private long expiry;
+
+ public BasicPoolEntry(final ClientConnectionOperator op,
+ final HttpRoute route,
+ final ReferenceQueue<Object> queue) {
+ super(op, route);
+ Args.notNull(route, "HTTP route");
+ this.created = System.currentTimeMillis();
+ this.validUntil = Long.MAX_VALUE;
+ this.expiry = this.validUntil;
+ }
+
+ /**
+ * Creates a new pool entry.
+ *
+ * @param op the connection operator
+ * @param route the planned route for the connection
+ */
+ public BasicPoolEntry(final ClientConnectionOperator op,
+ final HttpRoute route) {
+ this(op, route, -1, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Creates a new pool entry with a specified maximum lifetime.
+ *
+ * @param op the connection operator
+ * @param route the planned route for the connection
+ * @param connTTL maximum lifetime of this entry, <=0 implies "infinity"
+ * @param timeunit TimeUnit of connTTL
+ *
+ * @since 4.1
+ */
+ public BasicPoolEntry(final ClientConnectionOperator op,
+ final HttpRoute route, final long connTTL, final TimeUnit timeunit) {
+ super(op, route);
+ Args.notNull(route, "HTTP route");
+ this.created = System.currentTimeMillis();
+ if (connTTL > 0) {
+ this.validUntil = this.created + timeunit.toMillis(connTTL);
+ } else {
+ this.validUntil = Long.MAX_VALUE;
+ }
+ this.expiry = this.validUntil;
+ }
+
+ protected final OperatedClientConnection getConnection() {
+ return super.connection;
+ }
+
+ protected final HttpRoute getPlannedRoute() {
+ return super.route;
+ }
+
+ protected final BasicPoolEntryRef getWeakRef() {
+ return null;
+ }
+
+ @Override
+ protected void shutdownEntry() {
+ super.shutdownEntry();
+ }
+
+ /**
+ * @since 4.1
+ */
+ public long getCreated() {
+ return this.created;
+ }
+
+ /**
+ * @since 4.1
+ */
+ public long getUpdated() {
+ return this.updated;
+ }
+
+ /**
+ * @since 4.1
+ */
+ public long getExpiry() {
+ return this.expiry;
+ }
+
+ public long getValidUntil() {
+ return this.validUntil;
+ }
+
+ /**
+ * @since 4.1
+ */
+ public void updateExpiry(final long time, final TimeUnit timeunit) {
+ this.updated = System.currentTimeMillis();
+ final long newExpiry;
+ if (time > 0) {
+ newExpiry = this.updated + timeunit.toMillis(time);
+ } else {
+ newExpiry = Long.MAX_VALUE;
+ }
+ this.expiry = Math.min(validUntil, newExpiry);
+ }
+
+ /**
+ * @since 4.1
+ */
+ public boolean isExpired(final long now) {
+ return now >= this.expiry;
+ }
+
+}
+
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/BasicPoolEntryRef.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/BasicPoolEntryRef.java
new file mode 100644
index 0000000000..202e6b1022
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/BasicPoolEntryRef.java
@@ -0,0 +1,76 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn.tsccm;
+
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * A weak reference to a {@link BasicPoolEntry BasicPoolEntry}.
+ * This reference explicitly keeps the planned route, so the connection
+ * can be reclaimed if it is lost to garbage collection.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) do not use
+ */
+@Deprecated
+public class BasicPoolEntryRef extends WeakReference<BasicPoolEntry> {
+
+ /** The planned route of the entry. */
+ private final HttpRoute route; // HttpRoute is @Immutable
+
+
+ /**
+ * Creates a new reference to a pool entry.
+ *
+ * @param entry the pool entry, must not be <code>null</code>
+ * @param queue the reference queue, or <code>null</code>
+ */
+ public BasicPoolEntryRef(final BasicPoolEntry entry,
+ final ReferenceQueue<Object> queue) {
+ super(entry, queue);
+ Args.notNull(entry, "Pool entry");
+ route = entry.getPlannedRoute();
+ }
+
+
+ /**
+ * Obtain the planned route for the referenced entry.
+ * The planned route is still available, even if the entry is gone.
+ *
+ * @return the planned route
+ */
+ public final HttpRoute getRoute() {
+ return this.route;
+ }
+
+} // class BasicPoolEntryRef
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/BasicPooledConnAdapter.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/BasicPooledConnAdapter.java
new file mode 100644
index 0000000000..3d6433a2dc
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/BasicPooledConnAdapter.java
@@ -0,0 +1,75 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn.tsccm;
+
+import ch.boye.httpclientandroidlib.conn.ClientConnectionManager;
+import ch.boye.httpclientandroidlib.impl.conn.AbstractPoolEntry;
+import ch.boye.httpclientandroidlib.impl.conn.AbstractPooledConnAdapter;
+
+/**
+ * A connection wrapper and callback handler.
+ * All connections given out by the manager are wrappers which
+ * can be {@link #detach detach}ed to prevent further use on release.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) do not use
+ */
+@Deprecated
+public class BasicPooledConnAdapter extends AbstractPooledConnAdapter {
+
+ /**
+ * Creates a new adapter.
+ *
+ * @param tsccm the connection manager
+ * @param entry the pool entry for the connection being wrapped
+ */
+ protected BasicPooledConnAdapter(final ThreadSafeClientConnManager tsccm,
+ final AbstractPoolEntry entry) {
+ super(tsccm, entry);
+ markReusable();
+ }
+
+ @Override
+ protected ClientConnectionManager getManager() {
+ // override needed only to make method visible in this package
+ return super.getManager();
+ }
+
+ @Override
+ protected AbstractPoolEntry getPoolEntry() {
+ // override needed only to make method visible in this package
+ return super.getPoolEntry();
+ }
+
+ @Override
+ protected void detach() {
+ // override needed only to make method visible in this package
+ super.detach();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/ConnPoolByRoute.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/ConnPoolByRoute.java
new file mode 100644
index 0000000000..22f6a089d5
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/ConnPoolByRoute.java
@@ -0,0 +1,829 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn.tsccm;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.conn.ClientConnectionOperator;
+import ch.boye.httpclientandroidlib.conn.ConnectionPoolTimeoutException;
+import ch.boye.httpclientandroidlib.conn.OperatedClientConnection;
+import ch.boye.httpclientandroidlib.conn.params.ConnManagerParams;
+import ch.boye.httpclientandroidlib.conn.params.ConnPerRoute;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+
+/**
+ * A connection pool that maintains connections by route.
+ * This class is derived from <code>MultiThreadedHttpConnectionManager</code>
+ * in HttpClient 3.x, see there for original authors. It implements the same
+ * algorithm for connection re-use and connection-per-host enforcement:
+ * <ul>
+ * <li>connections are re-used only for the exact same route</li>
+ * <li>connection limits are enforced per route rather than per host</li>
+ * </ul>
+ * Note that access to the pool data structures is synchronized via the
+ * {@link AbstractConnPool#poolLock poolLock} in the base class,
+ * not via <code>synchronized</code> methods.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) use {@link ch.boye.httpclientandroidlib.pool.AbstractConnPool}
+ */
+@Deprecated
+public class ConnPoolByRoute extends AbstractConnPool {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ private final Lock poolLock;
+
+ /** Connection operator for this pool */
+ protected final ClientConnectionOperator operator;
+
+ /** Connections per route lookup */
+ protected final ConnPerRoute connPerRoute;
+
+ /** References to issued connections */
+ protected final Set<BasicPoolEntry> leasedConnections;
+
+ /** The list of free connections */
+ protected final Queue<BasicPoolEntry> freeConnections;
+
+ /** The list of WaitingThreads waiting for a connection */
+ protected final Queue<WaitingThread> waitingThreads;
+
+ /** Map of route-specific pools */
+ protected final Map<HttpRoute, RouteSpecificPool> routeToPool;
+
+ private final long connTTL;
+
+ private final TimeUnit connTTLTimeUnit;
+
+ protected volatile boolean shutdown;
+
+ protected volatile int maxTotalConnections;
+
+ protected volatile int numConnections;
+
+ /**
+ * Creates a new connection pool, managed by route.
+ *
+ * @since 4.1
+ */
+ public ConnPoolByRoute(
+ final ClientConnectionOperator operator,
+ final ConnPerRoute connPerRoute,
+ final int maxTotalConnections) {
+ this(operator, connPerRoute, maxTotalConnections, -1, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * @since 4.1
+ */
+ public ConnPoolByRoute(
+ final ClientConnectionOperator operator,
+ final ConnPerRoute connPerRoute,
+ final int maxTotalConnections,
+ final long connTTL,
+ final TimeUnit connTTLTimeUnit) {
+ super();
+ Args.notNull(operator, "Connection operator");
+ Args.notNull(connPerRoute, "Connections per route");
+ this.poolLock = super.poolLock;
+ this.leasedConnections = super.leasedConnections;
+ this.operator = operator;
+ this.connPerRoute = connPerRoute;
+ this.maxTotalConnections = maxTotalConnections;
+ this.freeConnections = createFreeConnQueue();
+ this.waitingThreads = createWaitingThreadQueue();
+ this.routeToPool = createRouteToPoolMap();
+ this.connTTL = connTTL;
+ this.connTTLTimeUnit = connTTLTimeUnit;
+ }
+
+ protected Lock getLock() {
+ return this.poolLock;
+ }
+
+ /**
+ * Creates a new connection pool, managed by route.
+ *
+ * @deprecated (4.1) use {@link ConnPoolByRoute#ConnPoolByRoute(ClientConnectionOperator, ConnPerRoute, int)}
+ */
+ @Deprecated
+ public ConnPoolByRoute(final ClientConnectionOperator operator, final HttpParams params) {
+ this(operator,
+ ConnManagerParams.getMaxConnectionsPerRoute(params),
+ ConnManagerParams.getMaxTotalConnections(params));
+ }
+
+ /**
+ * Creates the queue for {@link #freeConnections}.
+ * Called once by the constructor.
+ *
+ * @return a queue
+ */
+ protected Queue<BasicPoolEntry> createFreeConnQueue() {
+ return new LinkedList<BasicPoolEntry>();
+ }
+
+ /**
+ * Creates the queue for {@link #waitingThreads}.
+ * Called once by the constructor.
+ *
+ * @return a queue
+ */
+ protected Queue<WaitingThread> createWaitingThreadQueue() {
+ return new LinkedList<WaitingThread>();
+ }
+
+ /**
+ * Creates the map for {@link #routeToPool}.
+ * Called once by the constructor.
+ *
+ * @return a map
+ */
+ protected Map<HttpRoute, RouteSpecificPool> createRouteToPoolMap() {
+ return new HashMap<HttpRoute, RouteSpecificPool>();
+ }
+
+
+ /**
+ * Creates a new route-specific pool.
+ * Called by {@link #getRoutePool} when necessary.
+ *
+ * @param route the route
+ *
+ * @return the new pool
+ */
+ protected RouteSpecificPool newRouteSpecificPool(final HttpRoute route) {
+ return new RouteSpecificPool(route, this.connPerRoute);
+ }
+
+
+ /**
+ * Creates a new waiting thread.
+ * Called by {@link #getRoutePool} when necessary.
+ *
+ * @param cond the condition to wait for
+ * @param rospl the route specific pool, or <code>null</code>
+ *
+ * @return a waiting thread representation
+ */
+ protected WaitingThread newWaitingThread(final Condition cond,
+ final RouteSpecificPool rospl) {
+ return new WaitingThread(cond, rospl);
+ }
+
+ private void closeConnection(final BasicPoolEntry entry) {
+ final OperatedClientConnection conn = entry.getConnection();
+ if (conn != null) {
+ try {
+ conn.close();
+ } catch (final IOException ex) {
+ log.debug("I/O error closing connection", ex);
+ }
+ }
+ }
+
+ /**
+ * Get a route-specific pool of available connections.
+ *
+ * @param route the route
+ * @param create whether to create the pool if it doesn't exist
+ *
+ * @return the pool for the argument route,
+ * never <code>null</code> if <code>create</code> is <code>true</code>
+ */
+ protected RouteSpecificPool getRoutePool(final HttpRoute route,
+ final boolean create) {
+ RouteSpecificPool rospl = null;
+ poolLock.lock();
+ try {
+
+ rospl = routeToPool.get(route);
+ if ((rospl == null) && create) {
+ // no pool for this route yet (or anymore)
+ rospl = newRouteSpecificPool(route);
+ routeToPool.put(route, rospl);
+ }
+
+ } finally {
+ poolLock.unlock();
+ }
+
+ return rospl;
+ }
+
+ public int getConnectionsInPool(final HttpRoute route) {
+ poolLock.lock();
+ try {
+ // don't allow a pool to be created here!
+ final RouteSpecificPool rospl = getRoutePool(route, false);
+ return (rospl != null) ? rospl.getEntryCount() : 0;
+
+ } finally {
+ poolLock.unlock();
+ }
+ }
+
+ public int getConnectionsInPool() {
+ poolLock.lock();
+ try {
+ return numConnections;
+ } finally {
+ poolLock.unlock();
+ }
+ }
+
+ @Override
+ public PoolEntryRequest requestPoolEntry(
+ final HttpRoute route,
+ final Object state) {
+
+ final WaitingThreadAborter aborter = new WaitingThreadAborter();
+
+ return new PoolEntryRequest() {
+
+ public void abortRequest() {
+ poolLock.lock();
+ try {
+ aborter.abort();
+ } finally {
+ poolLock.unlock();
+ }
+ }
+
+ public BasicPoolEntry getPoolEntry(
+ final long timeout,
+ final TimeUnit tunit)
+ throws InterruptedException, ConnectionPoolTimeoutException {
+ return getEntryBlocking(route, state, timeout, tunit, aborter);
+ }
+
+ };
+ }
+
+ /**
+ * Obtains a pool entry with a connection within the given timeout.
+ * If a {@link WaitingThread} is used to block, {@link WaitingThreadAborter#setWaitingThread(WaitingThread)}
+ * must be called before blocking, to allow the thread to be interrupted.
+ *
+ * @param route the route for which to get the connection
+ * @param timeout the timeout, 0 or negative for no timeout
+ * @param tunit the unit for the <code>timeout</code>,
+ * may be <code>null</code> only if there is no timeout
+ * @param aborter an object which can abort a {@link WaitingThread}.
+ *
+ * @return pool entry holding a connection for the route
+ *
+ * @throws ConnectionPoolTimeoutException
+ * if the timeout expired
+ * @throws InterruptedException
+ * if the calling thread was interrupted
+ */
+ protected BasicPoolEntry getEntryBlocking(
+ final HttpRoute route, final Object state,
+ final long timeout, final TimeUnit tunit,
+ final WaitingThreadAborter aborter)
+ throws ConnectionPoolTimeoutException, InterruptedException {
+
+ Date deadline = null;
+ if (timeout > 0) {
+ deadline = new Date
+ (System.currentTimeMillis() + tunit.toMillis(timeout));
+ }
+
+ BasicPoolEntry entry = null;
+ poolLock.lock();
+ try {
+
+ RouteSpecificPool rospl = getRoutePool(route, true);
+ WaitingThread waitingThread = null;
+
+ while (entry == null) {
+ Asserts.check(!shutdown, "Connection pool shut down");
+
+ if (log.isDebugEnabled()) {
+ log.debug("[" + route + "] total kept alive: " + freeConnections.size() +
+ ", total issued: " + leasedConnections.size() +
+ ", total allocated: " + numConnections + " out of " + maxTotalConnections);
+ }
+
+ // the cases to check for:
+ // - have a free connection for that route
+ // - allowed to create a free connection for that route
+ // - can delete and replace a free connection for another route
+ // - need to wait for one of the things above to come true
+
+ entry = getFreeEntry(rospl, state);
+ if (entry != null) {
+ break;
+ }
+
+ final boolean hasCapacity = rospl.getCapacity() > 0;
+
+ if (log.isDebugEnabled()) {
+ log.debug("Available capacity: " + rospl.getCapacity()
+ + " out of " + rospl.getMaxEntries()
+ + " [" + route + "][" + state + "]");
+ }
+
+ if (hasCapacity && numConnections < maxTotalConnections) {
+
+ entry = createEntry(rospl, operator);
+
+ } else if (hasCapacity && !freeConnections.isEmpty()) {
+
+ deleteLeastUsedEntry();
+ // if least used entry's route was the same as rospl,
+ // rospl is now out of date : we preemptively refresh
+ rospl = getRoutePool(route, true);
+ entry = createEntry(rospl, operator);
+
+ } else {
+
+ if (log.isDebugEnabled()) {
+ log.debug("Need to wait for connection" +
+ " [" + route + "][" + state + "]");
+ }
+
+ if (waitingThread == null) {
+ waitingThread =
+ newWaitingThread(poolLock.newCondition(), rospl);
+ aborter.setWaitingThread(waitingThread);
+ }
+
+ boolean success = false;
+ try {
+ rospl.queueThread(waitingThread);
+ waitingThreads.add(waitingThread);
+ success = waitingThread.await(deadline);
+
+ } finally {
+ // In case of 'success', we were woken up by the
+ // connection pool and should now have a connection
+ // waiting for us, or else we're shutting down.
+ // Just continue in the loop, both cases are checked.
+ rospl.removeThread(waitingThread);
+ waitingThreads.remove(waitingThread);
+ }
+
+ // check for spurious wakeup vs. timeout
+ if (!success && (deadline != null) &&
+ (deadline.getTime() <= System.currentTimeMillis())) {
+ throw new ConnectionPoolTimeoutException
+ ("Timeout waiting for connection from pool");
+ }
+ }
+ } // while no entry
+
+ } finally {
+ poolLock.unlock();
+ }
+ return entry;
+ }
+
+ @Override
+ public void freeEntry(final BasicPoolEntry entry, final boolean reusable, final long validDuration, final TimeUnit timeUnit) {
+
+ final HttpRoute route = entry.getPlannedRoute();
+ if (log.isDebugEnabled()) {
+ log.debug("Releasing connection" +
+ " [" + route + "][" + entry.getState() + "]");
+ }
+
+ poolLock.lock();
+ try {
+ if (shutdown) {
+ // the pool is shut down, release the
+ // connection's resources and get out of here
+ closeConnection(entry);
+ return;
+ }
+
+ // no longer issued, we keep a hard reference now
+ leasedConnections.remove(entry);
+
+ final RouteSpecificPool rospl = getRoutePool(route, true);
+
+ if (reusable && rospl.getCapacity() >= 0) {
+ if (log.isDebugEnabled()) {
+ final String s;
+ if (validDuration > 0) {
+ s = "for " + validDuration + " " + timeUnit;
+ } else {
+ s = "indefinitely";
+ }
+ log.debug("Pooling connection" +
+ " [" + route + "][" + entry.getState() + "]; keep alive " + s);
+ }
+ rospl.freeEntry(entry);
+ entry.updateExpiry(validDuration, timeUnit);
+ freeConnections.add(entry);
+ } else {
+ closeConnection(entry);
+ rospl.dropEntry();
+ numConnections--;
+ }
+
+ notifyWaitingThread(rospl);
+
+ } finally {
+ poolLock.unlock();
+ }
+ }
+
+ /**
+ * If available, get a free pool entry for a route.
+ *
+ * @param rospl the route-specific pool from which to get an entry
+ *
+ * @return an available pool entry for the given route, or
+ * <code>null</code> if none is available
+ */
+ protected BasicPoolEntry getFreeEntry(final RouteSpecificPool rospl, final Object state) {
+
+ BasicPoolEntry entry = null;
+ poolLock.lock();
+ try {
+ boolean done = false;
+ while(!done) {
+
+ entry = rospl.allocEntry(state);
+
+ if (entry != null) {
+ if (log.isDebugEnabled()) {
+ log.debug("Getting free connection"
+ + " [" + rospl.getRoute() + "][" + state + "]");
+
+ }
+ freeConnections.remove(entry);
+ if (entry.isExpired(System.currentTimeMillis())) {
+ // If the free entry isn't valid anymore, get rid of it
+ // and loop to find another one that might be valid.
+ if (log.isDebugEnabled()) {
+ log.debug("Closing expired free connection"
+ + " [" + rospl.getRoute() + "][" + state + "]");
+ }
+ closeConnection(entry);
+ // We use dropEntry instead of deleteEntry because the entry
+ // is no longer "free" (we just allocated it), and deleteEntry
+ // can only be used to delete free entries.
+ rospl.dropEntry();
+ numConnections--;
+ } else {
+ leasedConnections.add(entry);
+ done = true;
+ }
+
+ } else {
+ done = true;
+ if (log.isDebugEnabled()) {
+ log.debug("No free connections"
+ + " [" + rospl.getRoute() + "][" + state + "]");
+ }
+ }
+ }
+ } finally {
+ poolLock.unlock();
+ }
+ return entry;
+ }
+
+
+ /**
+ * Creates a new pool entry.
+ * This method assumes that the new connection will be handed
+ * out immediately.
+ *
+ * @param rospl the route-specific pool for which to create the entry
+ * @param op the operator for creating a connection
+ *
+ * @return the new pool entry for a new connection
+ */
+ protected BasicPoolEntry createEntry(final RouteSpecificPool rospl,
+ final ClientConnectionOperator op) {
+
+ if (log.isDebugEnabled()) {
+ log.debug("Creating new connection [" + rospl.getRoute() + "]");
+ }
+
+ // the entry will create the connection when needed
+ final BasicPoolEntry entry = new BasicPoolEntry(op, rospl.getRoute(), connTTL, connTTLTimeUnit);
+
+ poolLock.lock();
+ try {
+ rospl.createdEntry(entry);
+ numConnections++;
+ leasedConnections.add(entry);
+ } finally {
+ poolLock.unlock();
+ }
+
+ return entry;
+ }
+
+
+ /**
+ * Deletes a given pool entry.
+ * This closes the pooled connection and removes all references,
+ * so that it can be GCed.
+ *
+ * <p><b>Note:</b> Does not remove the entry from the freeConnections list.
+ * It is assumed that the caller has already handled this step.</p>
+ * <!-- @@@ is that a good idea? or rather fix it? -->
+ *
+ * @param entry the pool entry for the connection to delete
+ */
+ protected void deleteEntry(final BasicPoolEntry entry) {
+
+ final HttpRoute route = entry.getPlannedRoute();
+
+ if (log.isDebugEnabled()) {
+ log.debug("Deleting connection"
+ + " [" + route + "][" + entry.getState() + "]");
+ }
+
+ poolLock.lock();
+ try {
+
+ closeConnection(entry);
+
+ final RouteSpecificPool rospl = getRoutePool(route, true);
+ rospl.deleteEntry(entry);
+ numConnections--;
+ if (rospl.isUnused()) {
+ routeToPool.remove(route);
+ }
+
+ } finally {
+ poolLock.unlock();
+ }
+ }
+
+
+ /**
+ * Delete an old, free pool entry to make room for a new one.
+ * Used to replace pool entries with ones for a different route.
+ */
+ protected void deleteLeastUsedEntry() {
+ poolLock.lock();
+ try {
+
+ final BasicPoolEntry entry = freeConnections.remove();
+
+ if (entry != null) {
+ deleteEntry(entry);
+ } else if (log.isDebugEnabled()) {
+ log.debug("No free connection to delete");
+ }
+
+ } finally {
+ poolLock.unlock();
+ }
+ }
+
+ @Override
+ protected void handleLostEntry(final HttpRoute route) {
+
+ poolLock.lock();
+ try {
+
+ final RouteSpecificPool rospl = getRoutePool(route, true);
+ rospl.dropEntry();
+ if (rospl.isUnused()) {
+ routeToPool.remove(route);
+ }
+
+ numConnections--;
+ notifyWaitingThread(rospl);
+
+ } finally {
+ poolLock.unlock();
+ }
+ }
+
+ /**
+ * Notifies a waiting thread that a connection is available.
+ * This will wake a thread waiting in the specific route pool,
+ * if there is one.
+ * Otherwise, a thread in the connection pool will be notified.
+ *
+ * @param rospl the pool in which to notify, or <code>null</code>
+ */
+ protected void notifyWaitingThread(final RouteSpecificPool rospl) {
+
+ //@@@ while this strategy provides for best connection re-use,
+ //@@@ is it fair? only do this if the connection is open?
+ // Find the thread we are going to notify. We want to ensure that
+ // each waiting thread is only interrupted once, so we will remove
+ // it from all wait queues before interrupting.
+ WaitingThread waitingThread = null;
+
+ poolLock.lock();
+ try {
+
+ if ((rospl != null) && rospl.hasThread()) {
+ if (log.isDebugEnabled()) {
+ log.debug("Notifying thread waiting on pool" +
+ " [" + rospl.getRoute() + "]");
+ }
+ waitingThread = rospl.nextThread();
+ } else if (!waitingThreads.isEmpty()) {
+ if (log.isDebugEnabled()) {
+ log.debug("Notifying thread waiting on any pool");
+ }
+ waitingThread = waitingThreads.remove();
+ } else if (log.isDebugEnabled()) {
+ log.debug("Notifying no-one, there are no waiting threads");
+ }
+
+ if (waitingThread != null) {
+ waitingThread.wakeup();
+ }
+
+ } finally {
+ poolLock.unlock();
+ }
+ }
+
+
+ @Override
+ public void deleteClosedConnections() {
+ poolLock.lock();
+ try {
+ final Iterator<BasicPoolEntry> iter = freeConnections.iterator();
+ while (iter.hasNext()) {
+ final BasicPoolEntry entry = iter.next();
+ if (!entry.getConnection().isOpen()) {
+ iter.remove();
+ deleteEntry(entry);
+ }
+ }
+ } finally {
+ poolLock.unlock();
+ }
+ }
+
+ /**
+ * Closes idle connections.
+ *
+ * @param idletime the time the connections should have been idle
+ * in order to be closed now
+ * @param tunit the unit for the <code>idletime</code>
+ */
+ @Override
+ public void closeIdleConnections(final long idletime, final TimeUnit tunit) {
+ Args.notNull(tunit, "Time unit");
+ final long t = idletime > 0 ? idletime : 0;
+ if (log.isDebugEnabled()) {
+ log.debug("Closing connections idle longer than " + t + " " + tunit);
+ }
+ // the latest time for which connections will be closed
+ final long deadline = System.currentTimeMillis() - tunit.toMillis(t);
+ poolLock.lock();
+ try {
+ final Iterator<BasicPoolEntry> iter = freeConnections.iterator();
+ while (iter.hasNext()) {
+ final BasicPoolEntry entry = iter.next();
+ if (entry.getUpdated() <= deadline) {
+ if (log.isDebugEnabled()) {
+ log.debug("Closing connection last used @ " + new Date(entry.getUpdated()));
+ }
+ iter.remove();
+ deleteEntry(entry);
+ }
+ }
+ } finally {
+ poolLock.unlock();
+ }
+ }
+
+ @Override
+ public void closeExpiredConnections() {
+ log.debug("Closing expired connections");
+ final long now = System.currentTimeMillis();
+
+ poolLock.lock();
+ try {
+ final Iterator<BasicPoolEntry> iter = freeConnections.iterator();
+ while (iter.hasNext()) {
+ final BasicPoolEntry entry = iter.next();
+ if (entry.isExpired(now)) {
+ if (log.isDebugEnabled()) {
+ log.debug("Closing connection expired @ " + new Date(entry.getExpiry()));
+ }
+ iter.remove();
+ deleteEntry(entry);
+ }
+ }
+ } finally {
+ poolLock.unlock();
+ }
+ }
+
+ @Override
+ public void shutdown() {
+ poolLock.lock();
+ try {
+ if (shutdown) {
+ return;
+ }
+ shutdown = true;
+
+ // close all connections that are issued to an application
+ final Iterator<BasicPoolEntry> iter1 = leasedConnections.iterator();
+ while (iter1.hasNext()) {
+ final BasicPoolEntry entry = iter1.next();
+ iter1.remove();
+ closeConnection(entry);
+ }
+
+ // close all free connections
+ final Iterator<BasicPoolEntry> iter2 = freeConnections.iterator();
+ while (iter2.hasNext()) {
+ final BasicPoolEntry entry = iter2.next();
+ iter2.remove();
+
+ if (log.isDebugEnabled()) {
+ log.debug("Closing connection"
+ + " [" + entry.getPlannedRoute() + "][" + entry.getState() + "]");
+ }
+ closeConnection(entry);
+ }
+
+ // wake up all waiting threads
+ final Iterator<WaitingThread> iwth = waitingThreads.iterator();
+ while (iwth.hasNext()) {
+ final WaitingThread waiter = iwth.next();
+ iwth.remove();
+ waiter.wakeup();
+ }
+
+ routeToPool.clear();
+
+ } finally {
+ poolLock.unlock();
+ }
+ }
+
+ /**
+ * since 4.1
+ */
+ public void setMaxTotalConnections(final int max) {
+ poolLock.lock();
+ try {
+ maxTotalConnections = max;
+ } finally {
+ poolLock.unlock();
+ }
+ }
+
+
+ /**
+ * since 4.1
+ */
+ public int getMaxTotalConnections() {
+ return maxTotalConnections;
+ }
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/PoolEntryRequest.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/PoolEntryRequest.java
new file mode 100644
index 0000000000..bf7eb147f6
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/PoolEntryRequest.java
@@ -0,0 +1,69 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn.tsccm;
+
+import java.util.concurrent.TimeUnit;
+
+import ch.boye.httpclientandroidlib.conn.ConnectionPoolTimeoutException;
+
+/**
+ * Encapsulates a request for a {@link BasicPoolEntry}.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) use {@link java.util.concurrent.Future}
+ */
+@Deprecated
+public interface PoolEntryRequest {
+
+ /**
+ * Obtains a pool entry with a connection within the given timeout.
+ * If {@link #abortRequest()} is called before this completes
+ * an {@link InterruptedException} is thrown.
+ *
+ * @param timeout the timeout, 0 or negative for no timeout
+ * @param tunit the unit for the <code>timeout</code>,
+ * may be <code>null</code> only if there is no timeout
+ *
+ * @return pool entry holding a connection for the route
+ *
+ * @throws ConnectionPoolTimeoutException
+ * if the timeout expired
+ * @throws InterruptedException
+ * if the calling thread was interrupted or the request was aborted
+ */
+ BasicPoolEntry getPoolEntry(
+ long timeout,
+ TimeUnit tunit) throws InterruptedException, ConnectionPoolTimeoutException;
+
+ /**
+ * Aborts the active or next call to
+ * {@link #getPoolEntry(long, TimeUnit)}.
+ */
+ void abortRequest();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/RouteSpecificPool.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/RouteSpecificPool.java
new file mode 100644
index 0000000000..73964c8d13
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/RouteSpecificPool.java
@@ -0,0 +1,313 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn.tsccm;
+
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.ListIterator;
+import java.util.Queue;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.conn.OperatedClientConnection;
+import ch.boye.httpclientandroidlib.conn.params.ConnPerRoute;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+import ch.boye.httpclientandroidlib.util.LangUtils;
+
+
+/**
+ * A connection sub-pool for a specific route, used by {@link ConnPoolByRoute}.
+ * The methods in this class are unsynchronized. It is expected that the
+ * containing pool takes care of synchronization.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) use {@link ch.boye.httpclientandroidlib.pool.AbstractConnPool}
+ */
+@Deprecated
+public class RouteSpecificPool {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ /** The route this pool is for. */
+ protected final HttpRoute route; //Immutable
+
+ protected final int maxEntries;
+
+ /** Connections per route */
+ protected final ConnPerRoute connPerRoute;
+
+ /**
+ * The list of free entries.
+ * This list is managed LIFO, to increase idle times and
+ * allow for closing connections that are not really needed.
+ */
+ protected final LinkedList<BasicPoolEntry> freeEntries;
+
+ /** The list of threads waiting for this pool. */
+ protected final Queue<WaitingThread> waitingThreads;
+
+ /** The number of created entries. */
+ protected int numEntries;
+
+ /**
+ * @deprecated (4.1) use {@link RouteSpecificPool#RouteSpecificPool(HttpRoute, ConnPerRoute)}
+ */
+ @Deprecated
+ public RouteSpecificPool(final HttpRoute route, final int maxEntries) {
+ this.route = route;
+ this.maxEntries = maxEntries;
+ this.connPerRoute = new ConnPerRoute() {
+ public int getMaxForRoute(final HttpRoute route) {
+ return RouteSpecificPool.this.maxEntries;
+ }
+ };
+ this.freeEntries = new LinkedList<BasicPoolEntry>();
+ this.waitingThreads = new LinkedList<WaitingThread>();
+ this.numEntries = 0;
+ }
+
+
+ /**
+ * Creates a new route-specific pool.
+ *
+ * @param route the route for which to pool
+ * @param connPerRoute the connections per route configuration
+ */
+ public RouteSpecificPool(final HttpRoute route, final ConnPerRoute connPerRoute) {
+ this.route = route;
+ this.connPerRoute = connPerRoute;
+ this.maxEntries = connPerRoute.getMaxForRoute(route);
+ this.freeEntries = new LinkedList<BasicPoolEntry>();
+ this.waitingThreads = new LinkedList<WaitingThread>();
+ this.numEntries = 0;
+ }
+
+
+ /**
+ * Obtains the route for which this pool is specific.
+ *
+ * @return the route
+ */
+ public final HttpRoute getRoute() {
+ return route;
+ }
+
+
+ /**
+ * Obtains the maximum number of entries allowed for this pool.
+ *
+ * @return the max entry number
+ */
+ public final int getMaxEntries() {
+ return maxEntries;
+ }
+
+
+ /**
+ * Indicates whether this pool is unused.
+ * A pool is unused if there is neither an entry nor a waiting thread.
+ * All entries count, not only the free but also the allocated ones.
+ *
+ * @return <code>true</code> if this pool is unused,
+ * <code>false</code> otherwise
+ */
+ public boolean isUnused() {
+ return (numEntries < 1) && waitingThreads.isEmpty();
+ }
+
+
+ /**
+ * Return remaining capacity of this pool
+ *
+ * @return capacity
+ */
+ public int getCapacity() {
+ return connPerRoute.getMaxForRoute(route) - numEntries;
+ }
+
+
+ /**
+ * Obtains the number of entries.
+ * This includes not only the free entries, but also those that
+ * have been created and are currently issued to an application.
+ *
+ * @return the number of entries for the route of this pool
+ */
+ public final int getEntryCount() {
+ return numEntries;
+ }
+
+
+ /**
+ * Obtains a free entry from this pool, if one is available.
+ *
+ * @return an available pool entry, or <code>null</code> if there is none
+ */
+ public BasicPoolEntry allocEntry(final Object state) {
+ if (!freeEntries.isEmpty()) {
+ final ListIterator<BasicPoolEntry> it = freeEntries.listIterator(freeEntries.size());
+ while (it.hasPrevious()) {
+ final BasicPoolEntry entry = it.previous();
+ if (entry.getState() == null || LangUtils.equals(state, entry.getState())) {
+ it.remove();
+ return entry;
+ }
+ }
+ }
+ if (getCapacity() == 0 && !freeEntries.isEmpty()) {
+ final BasicPoolEntry entry = freeEntries.remove();
+ entry.shutdownEntry();
+ final OperatedClientConnection conn = entry.getConnection();
+ try {
+ conn.close();
+ } catch (final IOException ex) {
+ log.debug("I/O error closing connection", ex);
+ }
+ return entry;
+ }
+ return null;
+ }
+
+
+ /**
+ * Returns an allocated entry to this pool.
+ *
+ * @param entry the entry obtained from {@link #allocEntry allocEntry}
+ * or presented to {@link #createdEntry createdEntry}
+ */
+ public void freeEntry(final BasicPoolEntry entry) {
+ if (numEntries < 1) {
+ throw new IllegalStateException
+ ("No entry created for this pool. " + route);
+ }
+ if (numEntries <= freeEntries.size()) {
+ throw new IllegalStateException
+ ("No entry allocated from this pool. " + route);
+ }
+ freeEntries.add(entry);
+ }
+
+
+ /**
+ * Indicates creation of an entry for this pool.
+ * The entry will <i>not</i> be added to the list of free entries,
+ * it is only recognized as belonging to this pool now. It can then
+ * be passed to {@link #freeEntry freeEntry}.
+ *
+ * @param entry the entry that was created for this pool
+ */
+ public void createdEntry(final BasicPoolEntry entry) {
+ Args.check(route.equals(entry.getPlannedRoute()), "Entry not planned for this pool");
+ numEntries++;
+ }
+
+
+ /**
+ * Deletes an entry from this pool.
+ * Only entries that are currently free in this pool can be deleted.
+ * Allocated entries can not be deleted.
+ *
+ * @param entry the entry to delete from this pool
+ *
+ * @return <code>true</code> if the entry was found and deleted, or
+ * <code>false</code> if the entry was not found
+ */
+ public boolean deleteEntry(final BasicPoolEntry entry) {
+
+ final boolean found = freeEntries.remove(entry);
+ if (found) {
+ numEntries--;
+ }
+ return found;
+ }
+
+
+ /**
+ * Forgets about an entry from this pool.
+ * This method is used to indicate that an entry
+ * {@link #allocEntry allocated}
+ * from this pool has been lost and will not be returned.
+ */
+ public void dropEntry() {
+ Asserts.check(numEntries > 0, "There is no entry that could be dropped");
+ numEntries--;
+ }
+
+
+ /**
+ * Adds a waiting thread.
+ * This pool makes no attempt to match waiting threads with pool entries.
+ * It is the caller's responsibility to check that there is no entry
+ * before adding a waiting thread.
+ *
+ * @param wt the waiting thread
+ */
+ public void queueThread(final WaitingThread wt) {
+ Args.notNull(wt, "Waiting thread");
+ this.waitingThreads.add(wt);
+ }
+
+
+ /**
+ * Checks whether there is a waiting thread in this pool.
+ *
+ * @return <code>true</code> if there is a waiting thread,
+ * <code>false</code> otherwise
+ */
+ public boolean hasThread() {
+ return !this.waitingThreads.isEmpty();
+ }
+
+
+ /**
+ * Returns the next thread in the queue.
+ *
+ * @return a waiting thread, or <code>null</code> if there is none
+ */
+ public WaitingThread nextThread() {
+ return this.waitingThreads.peek();
+ }
+
+
+ /**
+ * Removes a waiting thread, if it is queued.
+ *
+ * @param wt the waiting thread
+ */
+ public void removeThread(final WaitingThread wt) {
+ if (wt == null) {
+ return;
+ }
+
+ this.waitingThreads.remove(wt);
+ }
+
+
+} // class RouteSpecificPool
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/ThreadSafeClientConnManager.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/ThreadSafeClientConnManager.java
new file mode 100644
index 0000000000..1a1ff5c37b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/ThreadSafeClientConnManager.java
@@ -0,0 +1,377 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn.tsccm;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionManager;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionOperator;
+import ch.boye.httpclientandroidlib.conn.ClientConnectionRequest;
+import ch.boye.httpclientandroidlib.conn.ConnectionPoolTimeoutException;
+import ch.boye.httpclientandroidlib.conn.ManagedClientConnection;
+import ch.boye.httpclientandroidlib.conn.params.ConnPerRouteBean;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.conn.scheme.SchemeRegistry;
+import ch.boye.httpclientandroidlib.impl.conn.DefaultClientConnectionOperator;
+import ch.boye.httpclientandroidlib.impl.conn.SchemeRegistryFactory;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+
+/**
+ * Manages a pool of {@link ch.boye.httpclientandroidlib.conn.OperatedClientConnection }
+ * and is able to service connection requests from multiple execution threads.
+ * Connections are pooled on a per route basis. A request for a route which
+ * already the manager has persistent connections for available in the pool
+ * will be services by leasing a connection from the pool rather than
+ * creating a brand new connection.
+ * <p>
+ * ThreadSafeClientConnManager maintains a maximum limit of connection on
+ * a per route basis and in total. Per default this implementation will
+ * create no more than than 2 concurrent connections per given route
+ * and no more 20 connections in total. For many real-world applications
+ * these limits may prove too constraining, especially if they use HTTP
+ * as a transport protocol for their services. Connection limits, however,
+ * can be adjusted using HTTP parameters.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) use {@link ch.boye.httpclientandroidlib.impl.conn.PoolingHttpClientConnectionManager}
+ */
+@ThreadSafe
+@Deprecated
+public class ThreadSafeClientConnManager implements ClientConnectionManager {
+
+ public HttpClientAndroidLog log;
+
+ /** The schemes supported by this connection manager. */
+ protected final SchemeRegistry schemeRegistry; // @ThreadSafe
+
+ protected final AbstractConnPool connectionPool;
+
+ /** The pool of connections being managed. */
+ protected final ConnPoolByRoute pool;
+
+ /** The operator for opening and updating connections. */
+ protected final ClientConnectionOperator connOperator; // DefaultClientConnectionOperator is @ThreadSafe
+
+ protected final ConnPerRouteBean connPerRoute;
+
+ /**
+ * Creates a new thread safe connection manager.
+ *
+ * @param schreg the scheme registry.
+ */
+ public ThreadSafeClientConnManager(final SchemeRegistry schreg) {
+ this(schreg, -1, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * @since 4.1
+ */
+ public ThreadSafeClientConnManager() {
+ this(SchemeRegistryFactory.createDefault());
+ }
+
+ /**
+ * Creates a new thread safe connection manager.
+ *
+ * @param schreg the scheme registry.
+ * @param connTTL max connection lifetime, <=0 implies "infinity"
+ * @param connTTLTimeUnit TimeUnit of connTTL
+ *
+ * @since 4.1
+ */
+ public ThreadSafeClientConnManager(final SchemeRegistry schreg,
+ final long connTTL, final TimeUnit connTTLTimeUnit) {
+ this(schreg, connTTL, connTTLTimeUnit, new ConnPerRouteBean());
+ }
+
+ /**
+ * Creates a new thread safe connection manager.
+ *
+ * @param schreg the scheme registry.
+ * @param connTTL max connection lifetime, <=0 implies "infinity"
+ * @param connTTLTimeUnit TimeUnit of connTTL
+ * @param connPerRoute mapping of maximum connections per route,
+ * provided as a dependency so it can be managed externally, e.g.
+ * for dynamic connection pool size management.
+ *
+ * @since 4.2
+ */
+ public ThreadSafeClientConnManager(final SchemeRegistry schreg,
+ final long connTTL, final TimeUnit connTTLTimeUnit, final ConnPerRouteBean connPerRoute) {
+ super();
+ Args.notNull(schreg, "Scheme registry");
+ this.log = new HttpClientAndroidLog(getClass());
+ this.schemeRegistry = schreg;
+ this.connPerRoute = connPerRoute;
+ this.connOperator = createConnectionOperator(schreg);
+ this.pool = createConnectionPool(connTTL, connTTLTimeUnit) ;
+ this.connectionPool = this.pool;
+ }
+
+ /**
+ * Creates a new thread safe connection manager.
+ *
+ * @param params the parameters for this manager.
+ * @param schreg the scheme registry.
+ *
+ * @deprecated (4.1) use {@link ThreadSafeClientConnManager#ThreadSafeClientConnManager(SchemeRegistry)}
+ */
+ @Deprecated
+ public ThreadSafeClientConnManager(final HttpParams params,
+ final SchemeRegistry schreg) {
+ Args.notNull(schreg, "Scheme registry");
+ this.log = new HttpClientAndroidLog(getClass());
+ this.schemeRegistry = schreg;
+ this.connPerRoute = new ConnPerRouteBean();
+ this.connOperator = createConnectionOperator(schreg);
+ this.pool = (ConnPoolByRoute) createConnectionPool(params) ;
+ this.connectionPool = this.pool;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ shutdown();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Hook for creating the connection pool.
+ *
+ * @return the connection pool to use
+ *
+ * @deprecated (4.1) use #createConnectionPool(long, TimeUnit))
+ */
+ @Deprecated
+ protected AbstractConnPool createConnectionPool(final HttpParams params) {
+ return new ConnPoolByRoute(connOperator, params);
+ }
+
+ /**
+ * Hook for creating the connection pool.
+ *
+ * @return the connection pool to use
+ *
+ * @since 4.1
+ */
+ protected ConnPoolByRoute createConnectionPool(final long connTTL, final TimeUnit connTTLTimeUnit) {
+ return new ConnPoolByRoute(connOperator, connPerRoute, 20, connTTL, connTTLTimeUnit);
+ }
+
+ /**
+ * Hook for creating the connection operator.
+ * It is called by the constructor.
+ * Derived classes can override this method to change the
+ * instantiation of the operator.
+ * The default implementation here instantiates
+ * {@link DefaultClientConnectionOperator DefaultClientConnectionOperator}.
+ *
+ * @param schreg the scheme registry.
+ *
+ * @return the connection operator to use
+ */
+ protected ClientConnectionOperator
+ createConnectionOperator(final SchemeRegistry schreg) {
+
+ return new DefaultClientConnectionOperator(schreg);// @ThreadSafe
+ }
+
+ public SchemeRegistry getSchemeRegistry() {
+ return this.schemeRegistry;
+ }
+
+ public ClientConnectionRequest requestConnection(
+ final HttpRoute route,
+ final Object state) {
+
+ final PoolEntryRequest poolRequest = pool.requestPoolEntry(
+ route, state);
+
+ return new ClientConnectionRequest() {
+
+ public void abortRequest() {
+ poolRequest.abortRequest();
+ }
+
+ public ManagedClientConnection getConnection(
+ final long timeout, final TimeUnit tunit) throws InterruptedException,
+ ConnectionPoolTimeoutException {
+ Args.notNull(route, "Route");
+
+ if (log.isDebugEnabled()) {
+ log.debug("Get connection: " + route + ", timeout = " + timeout);
+ }
+
+ final BasicPoolEntry entry = poolRequest.getPoolEntry(timeout, tunit);
+ return new BasicPooledConnAdapter(ThreadSafeClientConnManager.this, entry);
+ }
+
+ };
+
+ }
+
+ public void releaseConnection(final ManagedClientConnection conn, final long validDuration, final TimeUnit timeUnit) {
+ Args.check(conn instanceof BasicPooledConnAdapter, "Connection class mismatch, " +
+ "connection not obtained from this manager");
+ final BasicPooledConnAdapter hca = (BasicPooledConnAdapter) conn;
+ if (hca.getPoolEntry() != null) {
+ Asserts.check(hca.getManager() == this, "Connection not obtained from this manager");
+ }
+ synchronized (hca) {
+ final BasicPoolEntry entry = (BasicPoolEntry) hca.getPoolEntry();
+ if (entry == null) {
+ return;
+ }
+ try {
+ // make sure that the response has been read completely
+ if (hca.isOpen() && !hca.isMarkedReusable()) {
+ // In MTHCM, there would be a call to
+ // SimpleHttpConnectionManager.finishLastResponse(conn);
+ // Consuming the response is handled outside in 4.0.
+
+ // make sure this connection will not be re-used
+ // Shut down rather than close, we might have gotten here
+ // because of a shutdown trigger.
+ // Shutdown of the adapter also clears the tracked route.
+ hca.shutdown();
+ }
+ } catch (final IOException iox) {
+ if (log.isDebugEnabled()) {
+ log.debug("Exception shutting down released connection.",
+ iox);
+ }
+ } finally {
+ final boolean reusable = hca.isMarkedReusable();
+ if (log.isDebugEnabled()) {
+ if (reusable) {
+ log.debug("Released connection is reusable.");
+ } else {
+ log.debug("Released connection is not reusable.");
+ }
+ }
+ hca.detach();
+ pool.freeEntry(entry, reusable, validDuration, timeUnit);
+ }
+ }
+ }
+
+ public void shutdown() {
+ log.debug("Shutting down");
+ pool.shutdown();
+ }
+
+ /**
+ * Gets the total number of pooled connections for the given route.
+ * This is the total number of connections that have been created and
+ * are still in use by this connection manager for the route.
+ * This value will not exceed the maximum number of connections per host.
+ *
+ * @param route the route in question
+ *
+ * @return the total number of pooled connections for that route
+ */
+ public int getConnectionsInPool(final HttpRoute route) {
+ return pool.getConnectionsInPool(route);
+ }
+
+ /**
+ * Gets the total number of pooled connections. This is the total number of
+ * connections that have been created and are still in use by this connection
+ * manager. This value will not exceed the maximum number of connections
+ * in total.
+ *
+ * @return the total number of pooled connections
+ */
+ public int getConnectionsInPool() {
+ return pool.getConnectionsInPool();
+ }
+
+ public void closeIdleConnections(final long idleTimeout, final TimeUnit tunit) {
+ if (log.isDebugEnabled()) {
+ log.debug("Closing connections idle longer than " + idleTimeout + " " + tunit);
+ }
+ pool.closeIdleConnections(idleTimeout, tunit);
+ }
+
+ public void closeExpiredConnections() {
+ log.debug("Closing expired connections");
+ pool.closeExpiredConnections();
+ }
+
+ /**
+ * since 4.1
+ */
+ public int getMaxTotal() {
+ return pool.getMaxTotalConnections();
+ }
+
+ /**
+ * since 4.1
+ */
+ public void setMaxTotal(final int max) {
+ pool.setMaxTotalConnections(max);
+ }
+
+ /**
+ * @since 4.1
+ */
+ public int getDefaultMaxPerRoute() {
+ return connPerRoute.getDefaultMaxPerRoute();
+ }
+
+ /**
+ * @since 4.1
+ */
+ public void setDefaultMaxPerRoute(final int max) {
+ connPerRoute.setDefaultMaxPerRoute(max);
+ }
+
+ /**
+ * @since 4.1
+ */
+ public int getMaxForRoute(final HttpRoute route) {
+ return connPerRoute.getMaxForRoute(route);
+ }
+
+ /**
+ * @since 4.1
+ */
+ public void setMaxForRoute(final HttpRoute route, final int max) {
+ connPerRoute.setMaxForRoute(route, max);
+ }
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/WaitingThread.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/WaitingThread.java
new file mode 100644
index 0000000000..c3f46cacf5
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/WaitingThread.java
@@ -0,0 +1,198 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn.tsccm;
+
+
+import java.util.Date;
+import java.util.concurrent.locks.Condition;
+
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Represents a thread waiting for a connection.
+ * This class implements throwaway objects. It is instantiated whenever
+ * a thread needs to wait. Instances are not re-used, except if the
+ * waiting thread experiences a spurious wakeup and continues to wait.
+ * <br/>
+ * All methods assume external synchronization on the condition
+ * passed to the constructor.
+ * Instances of this class do <i>not</i> synchronize access!
+ *
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) do not use
+ */
+@Deprecated
+public class WaitingThread {
+
+ /** The condition on which the thread is waiting. */
+ private final Condition cond;
+
+ /** The route specific pool on which the thread is waiting. */
+ //@@@ replace with generic pool interface
+ private final RouteSpecificPool pool;
+
+ /** The thread that is waiting for an entry. */
+ private Thread waiter;
+
+ /** True if this was interrupted. */
+ private boolean aborted;
+
+
+ /**
+ * Creates a new entry for a waiting thread.
+ *
+ * @param cond the condition for which to wait
+ * @param pool the pool on which the thread will be waiting,
+ * or <code>null</code>
+ */
+ public WaitingThread(final Condition cond, final RouteSpecificPool pool) {
+
+ Args.notNull(cond, "Condition");
+
+ this.cond = cond;
+ this.pool = pool;
+ }
+
+
+ /**
+ * Obtains the condition.
+ *
+ * @return the condition on which to wait, never <code>null</code>
+ */
+ public final Condition getCondition() {
+ // not synchronized
+ return this.cond;
+ }
+
+
+ /**
+ * Obtains the pool, if there is one.
+ *
+ * @return the pool on which a thread is or was waiting,
+ * or <code>null</code>
+ */
+ public final RouteSpecificPool getPool() {
+ // not synchronized
+ return this.pool;
+ }
+
+
+ /**
+ * Obtains the thread, if there is one.
+ *
+ * @return the thread which is waiting, or <code>null</code>
+ */
+ public final Thread getThread() {
+ // not synchronized
+ return this.waiter;
+ }
+
+
+ /**
+ * Blocks the calling thread.
+ * This method returns when the thread is notified or interrupted,
+ * if a timeout occurrs, or if there is a spurious wakeup.
+ * <br/>
+ * This method assumes external synchronization.
+ *
+ * @param deadline when to time out, or <code>null</code> for no timeout
+ *
+ * @return <code>true</code> if the condition was satisfied,
+ * <code>false</code> in case of a timeout.
+ * Typically, a call to {@link #wakeup} is used to indicate
+ * that the condition was satisfied. Since the condition is
+ * accessible outside, this cannot be guaranteed though.
+ *
+ * @throws InterruptedException if the waiting thread was interrupted
+ *
+ * @see #wakeup
+ */
+ public boolean await(final Date deadline)
+ throws InterruptedException {
+
+ // This is only a sanity check. We cannot synchronize here,
+ // the lock would not be released on calling cond.await() below.
+ if (this.waiter != null) {
+ throw new IllegalStateException
+ ("A thread is already waiting on this object." +
+ "\ncaller: " + Thread.currentThread() +
+ "\nwaiter: " + this.waiter);
+ }
+
+ if (aborted) {
+ throw new InterruptedException("Operation interrupted");
+ }
+
+ this.waiter = Thread.currentThread();
+
+ boolean success = false;
+ try {
+ if (deadline != null) {
+ success = this.cond.awaitUntil(deadline);
+ } else {
+ this.cond.await();
+ success = true;
+ }
+ if (aborted) {
+ throw new InterruptedException("Operation interrupted");
+ }
+ } finally {
+ this.waiter = null;
+ }
+ return success;
+
+ } // await
+
+
+ /**
+ * Wakes up the waiting thread.
+ * <br/>
+ * This method assumes external synchronization.
+ */
+ public void wakeup() {
+
+ // If external synchronization and pooling works properly,
+ // this cannot happen. Just a sanity check.
+ if (this.waiter == null) {
+ throw new IllegalStateException
+ ("Nobody waiting on this object.");
+ }
+
+ // One condition might be shared by several WaitingThread instances.
+ // It probably isn't, but just in case: wake all, not just one.
+ this.cond.signalAll();
+ }
+
+ public void interrupt() {
+ aborted = true;
+ this.cond.signalAll();
+ }
+
+
+} // class WaitingThread
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/WaitingThreadAborter.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/WaitingThreadAborter.java
new file mode 100644
index 0000000000..7ad9999961
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/WaitingThreadAborter.java
@@ -0,0 +1,69 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.conn.tsccm;
+
+/**
+ * A simple class that can interrupt a {@link WaitingThread}.
+ *
+ * Must be called with the pool lock held.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) do not use
+ */
+@Deprecated
+public class WaitingThreadAborter {
+
+ private WaitingThread waitingThread;
+ private boolean aborted;
+
+ /**
+ * If a waiting thread has been set, interrupts it.
+ */
+ public void abort() {
+ aborted = true;
+
+ if (waitingThread != null) {
+ waitingThread.interrupt();
+ }
+
+ }
+
+ /**
+ * Sets the waiting thread. If this has already been aborted,
+ * the waiting thread is immediately interrupted.
+ *
+ * @param waitingThread The thread to interrupt when aborting.
+ */
+ public void setWaitingThread(final WaitingThread waitingThread) {
+ this.waitingThread = waitingThread;
+ if (aborted) {
+ waitingThread.interrupt();
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/package-info.java
new file mode 100644
index 0000000000..5115d461fa
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/conn/tsccm/package-info.java
@@ -0,0 +1,33 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Deprecated.
+ *
+ * @deprecated (4.3)
+ */
+package ch.boye.httpclientandroidlib.impl.conn.tsccm;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/AbstractCookieAttributeHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/AbstractCookieAttributeHandler.java
new file mode 100644
index 0000000000..e9406bab23
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/AbstractCookieAttributeHandler.java
@@ -0,0 +1,52 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+import ch.boye.httpclientandroidlib.cookie.CookieAttributeHandler;
+import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+
+/**
+ *
+ * @since 4.0
+ */
+@Immutable
+public abstract class AbstractCookieAttributeHandler implements CookieAttributeHandler {
+
+ public void validate(final Cookie cookie, final CookieOrigin origin)
+ throws MalformedCookieException {
+ // Do nothing
+ }
+
+ public boolean match(final Cookie cookie, final CookieOrigin origin) {
+ // Always match
+ return true;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/AbstractCookieSpec.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/AbstractCookieSpec.java
new file mode 100644
index 0000000000..7dc320ef4d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/AbstractCookieSpec.java
@@ -0,0 +1,104 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.cookie.CookieAttributeHandler;
+import ch.boye.httpclientandroidlib.cookie.CookieSpec;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Abstract cookie specification which can delegate the job of parsing,
+ * validation or matching cookie attributes to a number of arbitrary
+ * {@link CookieAttributeHandler}s.
+ *
+ *
+ * @since 4.0
+ */
+@NotThreadSafe // HashMap is not thread-safe
+public abstract class AbstractCookieSpec implements CookieSpec {
+
+ /**
+ * Stores attribute name -> attribute handler mappings
+ */
+ private final Map<String, CookieAttributeHandler> attribHandlerMap;
+
+ /**
+ * Default constructor
+ * */
+ public AbstractCookieSpec() {
+ super();
+ this.attribHandlerMap = new HashMap<String, CookieAttributeHandler>(10);
+ }
+
+ public void registerAttribHandler(
+ final String name, final CookieAttributeHandler handler) {
+ Args.notNull(name, "Attribute name");
+ Args.notNull(handler, "Attribute handler");
+ this.attribHandlerMap.put(name, handler);
+ }
+
+ /**
+ * Finds an attribute handler {@link CookieAttributeHandler} for the
+ * given attribute. Returns <tt>null</tt> if no attribute handler is
+ * found for the specified attribute.
+ *
+ * @param name attribute name. e.g. Domain, Path, etc.
+ * @return an attribute handler or <tt>null</tt>
+ */
+ protected CookieAttributeHandler findAttribHandler(final String name) {
+ return this.attribHandlerMap.get(name);
+ }
+
+ /**
+ * Gets attribute handler {@link CookieAttributeHandler} for the
+ * given attribute.
+ *
+ * @param name attribute name. e.g. Domain, Path, etc.
+ * @throws IllegalStateException if handler not found for the
+ * specified attribute.
+ */
+ protected CookieAttributeHandler getAttribHandler(final String name) {
+ final CookieAttributeHandler handler = findAttribHandler(name);
+ if (handler == null) {
+ throw new IllegalStateException("Handler not registered for " +
+ name + " attribute.");
+ } else {
+ return handler;
+ }
+ }
+
+ protected Collection<CookieAttributeHandler> getAttribHandlers() {
+ return this.attribHandlerMap.values();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicClientCookie.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicClientCookie.java
new file mode 100644
index 0000000000..c826daada0
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicClientCookie.java
@@ -0,0 +1,361 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.cookie.ClientCookie;
+import ch.boye.httpclientandroidlib.cookie.SetCookie;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Default implementation of {@link SetCookie}.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class BasicClientCookie implements SetCookie, ClientCookie, Cloneable, Serializable {
+
+ private static final long serialVersionUID = -3869795591041535538L;
+
+ /**
+ * Default Constructor taking a name and a value. The value may be null.
+ *
+ * @param name The name.
+ * @param value The value.
+ */
+ public BasicClientCookie(final String name, final String value) {
+ super();
+ Args.notNull(name, "Name");
+ this.name = name;
+ this.attribs = new HashMap<String, String>();
+ this.value = value;
+ }
+
+ /**
+ * Returns the name.
+ *
+ * @return String name The name
+ */
+ public String getName() {
+ return this.name;
+ }
+
+ /**
+ * Returns the value.
+ *
+ * @return String value The current value.
+ */
+ public String getValue() {
+ return this.value;
+ }
+
+ /**
+ * Sets the value
+ *
+ * @param value
+ */
+ public void setValue(final String value) {
+ this.value = value;
+ }
+
+ /**
+ * Returns the comment describing the purpose of this cookie, or
+ * <tt>null</tt> if no such comment has been defined.
+ *
+ * @return comment
+ *
+ * @see #setComment(String)
+ */
+ public String getComment() {
+ return cookieComment;
+ }
+
+ /**
+ * If a user agent (web browser) presents this cookie to a user, the
+ * cookie's purpose will be described using this comment.
+ *
+ * @param comment
+ *
+ * @see #getComment()
+ */
+ public void setComment(final String comment) {
+ cookieComment = comment;
+ }
+
+
+ /**
+ * Returns null. Cookies prior to RFC2965 do not set this attribute
+ */
+ public String getCommentURL() {
+ return null;
+ }
+
+
+ /**
+ * Returns the expiration {@link Date} of the cookie, or <tt>null</tt>
+ * if none exists.
+ * <p><strong>Note:</strong> the object returned by this method is
+ * considered immutable. Changing it (e.g. using setTime()) could result
+ * in undefined behaviour. Do so at your peril. </p>
+ * @return Expiration {@link Date}, or <tt>null</tt>.
+ *
+ * @see #setExpiryDate(java.util.Date)
+ *
+ */
+ public Date getExpiryDate() {
+ return cookieExpiryDate;
+ }
+
+ /**
+ * Sets expiration date.
+ * <p><strong>Note:</strong> the object returned by this method is considered
+ * immutable. Changing it (e.g. using setTime()) could result in undefined
+ * behaviour. Do so at your peril.</p>
+ *
+ * @param expiryDate the {@link Date} after which this cookie is no longer valid.
+ *
+ * @see #getExpiryDate
+ *
+ */
+ public void setExpiryDate (final Date expiryDate) {
+ cookieExpiryDate = expiryDate;
+ }
+
+
+ /**
+ * Returns <tt>false</tt> if the cookie should be discarded at the end
+ * of the "session"; <tt>true</tt> otherwise.
+ *
+ * @return <tt>false</tt> if the cookie should be discarded at the end
+ * of the "session"; <tt>true</tt> otherwise
+ */
+ public boolean isPersistent() {
+ return (null != cookieExpiryDate);
+ }
+
+
+ /**
+ * Returns domain attribute of the cookie.
+ *
+ * @return the value of the domain attribute
+ *
+ * @see #setDomain(java.lang.String)
+ */
+ public String getDomain() {
+ return cookieDomain;
+ }
+
+ /**
+ * Sets the domain attribute.
+ *
+ * @param domain The value of the domain attribute
+ *
+ * @see #getDomain
+ */
+ public void setDomain(final String domain) {
+ if (domain != null) {
+ cookieDomain = domain.toLowerCase(Locale.ENGLISH);
+ } else {
+ cookieDomain = null;
+ }
+ }
+
+
+ /**
+ * Returns the path attribute of the cookie
+ *
+ * @return The value of the path attribute.
+ *
+ * @see #setPath(java.lang.String)
+ */
+ public String getPath() {
+ return cookiePath;
+ }
+
+ /**
+ * Sets the path attribute.
+ *
+ * @param path The value of the path attribute
+ *
+ * @see #getPath
+ *
+ */
+ public void setPath(final String path) {
+ cookiePath = path;
+ }
+
+ /**
+ * @return <code>true</code> if this cookie should only be sent over secure connections.
+ * @see #setSecure(boolean)
+ */
+ public boolean isSecure() {
+ return isSecure;
+ }
+
+ /**
+ * Sets the secure attribute of the cookie.
+ * <p>
+ * When <tt>true</tt> the cookie should only be sent
+ * using a secure protocol (https). This should only be set when
+ * the cookie's originating server used a secure protocol to set the
+ * cookie's value.
+ *
+ * @param secure The value of the secure attribute
+ *
+ * @see #isSecure()
+ */
+ public void setSecure (final boolean secure) {
+ isSecure = secure;
+ }
+
+
+ /**
+ * Returns null. Cookies prior to RFC2965 do not set this attribute
+ */
+ public int[] getPorts() {
+ return null;
+ }
+
+
+ /**
+ * Returns the version of the cookie specification to which this
+ * cookie conforms.
+ *
+ * @return the version of the cookie.
+ *
+ * @see #setVersion(int)
+ *
+ */
+ public int getVersion() {
+ return cookieVersion;
+ }
+
+ /**
+ * Sets the version of the cookie specification to which this
+ * cookie conforms.
+ *
+ * @param version the version of the cookie.
+ *
+ * @see #getVersion
+ */
+ public void setVersion(final int version) {
+ cookieVersion = version;
+ }
+
+ /**
+ * Returns true if this cookie has expired.
+ * @param date Current time
+ *
+ * @return <tt>true</tt> if the cookie has expired.
+ */
+ public boolean isExpired(final Date date) {
+ Args.notNull(date, "Date");
+ return (cookieExpiryDate != null
+ && cookieExpiryDate.getTime() <= date.getTime());
+ }
+
+ public void setAttribute(final String name, final String value) {
+ this.attribs.put(name, value);
+ }
+
+ public String getAttribute(final String name) {
+ return this.attribs.get(name);
+ }
+
+ public boolean containsAttribute(final String name) {
+ return this.attribs.get(name) != null;
+ }
+
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ final BasicClientCookie clone = (BasicClientCookie) super.clone();
+ clone.attribs = new HashMap<String, String>(this.attribs);
+ return clone;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder buffer = new StringBuilder();
+ buffer.append("[version: ");
+ buffer.append(Integer.toString(this.cookieVersion));
+ buffer.append("]");
+ buffer.append("[name: ");
+ buffer.append(this.name);
+ buffer.append("]");
+ buffer.append("[value: ");
+ buffer.append(this.value);
+ buffer.append("]");
+ buffer.append("[domain: ");
+ buffer.append(this.cookieDomain);
+ buffer.append("]");
+ buffer.append("[path: ");
+ buffer.append(this.cookiePath);
+ buffer.append("]");
+ buffer.append("[expiry: ");
+ buffer.append(this.cookieExpiryDate);
+ buffer.append("]");
+ return buffer.toString();
+ }
+
+ // ----------------------------------------------------- Instance Variables
+
+ /** Cookie name */
+ private final String name;
+
+ /** Cookie attributes as specified by the origin server */
+ private Map<String, String> attribs;
+
+ /** Cookie value */
+ private String value;
+
+ /** Comment attribute. */
+ private String cookieComment;
+
+ /** Domain attribute. */
+ private String cookieDomain;
+
+ /** Expiration {@link Date}. */
+ private Date cookieExpiryDate;
+
+ /** Path attribute. */
+ private String cookiePath;
+
+ /** My secure flag. */
+ private boolean isSecure;
+
+ /** The version of the cookie specification I was created from. */
+ private int cookieVersion;
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicClientCookie2.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicClientCookie2.java
new file mode 100644
index 0000000000..7ed0f82b61
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicClientCookie2.java
@@ -0,0 +1,101 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.util.Date;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.cookie.SetCookie2;
+
+/**
+ * Default implementation of {@link SetCookie2}.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class BasicClientCookie2 extends BasicClientCookie implements SetCookie2 {
+
+ private static final long serialVersionUID = -7744598295706617057L;
+
+ private String commentURL;
+ private int[] ports;
+ private boolean discard;
+
+ /**
+ * Default Constructor taking a name and a value. The value may be null.
+ *
+ * @param name The name.
+ * @param value The value.
+ */
+ public BasicClientCookie2(final String name, final String value) {
+ super(name, value);
+ }
+
+ @Override
+ public int[] getPorts() {
+ return this.ports;
+ }
+
+ public void setPorts(final int[] ports) {
+ this.ports = ports;
+ }
+
+ @Override
+ public String getCommentURL() {
+ return this.commentURL;
+ }
+
+ public void setCommentURL(final String commentURL) {
+ this.commentURL = commentURL;
+ }
+
+ public void setDiscard(final boolean discard) {
+ this.discard = discard;
+ }
+
+ @Override
+ public boolean isPersistent() {
+ return !this.discard && super.isPersistent();
+ }
+
+ @Override
+ public boolean isExpired(final Date date) {
+ return this.discard || super.isExpired(date);
+ }
+
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ final BasicClientCookie2 clone = (BasicClientCookie2) super.clone();
+ if (this.ports != null) {
+ clone.ports = this.ports.clone();
+ }
+ return clone;
+ }
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicCommentHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicCommentHandler.java
new file mode 100644
index 0000000000..69d65b9614
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicCommentHandler.java
@@ -0,0 +1,51 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+import ch.boye.httpclientandroidlib.cookie.SetCookie;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ *
+ * @since 4.0
+ */
+@Immutable
+public class BasicCommentHandler extends AbstractCookieAttributeHandler {
+
+ public BasicCommentHandler() {
+ super();
+ }
+
+ public void parse(final SetCookie cookie, final String value)
+ throws MalformedCookieException {
+ Args.notNull(cookie, "Cookie");
+ cookie.setComment(value);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicDomainHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicDomainHandler.java
new file mode 100644
index 0000000000..bad00ec486
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicDomainHandler.java
@@ -0,0 +1,116 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+import ch.boye.httpclientandroidlib.cookie.CookieAttributeHandler;
+import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
+import ch.boye.httpclientandroidlib.cookie.CookieRestrictionViolationException;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+import ch.boye.httpclientandroidlib.cookie.SetCookie;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ *
+ * @since 4.0
+ */
+@Immutable
+public class BasicDomainHandler implements CookieAttributeHandler {
+
+ public BasicDomainHandler() {
+ super();
+ }
+
+ public void parse(final SetCookie cookie, final String value)
+ throws MalformedCookieException {
+ Args.notNull(cookie, "Cookie");
+ if (value == null) {
+ throw new MalformedCookieException("Missing value for domain attribute");
+ }
+ if (value.trim().length() == 0) {
+ throw new MalformedCookieException("Blank value for domain attribute");
+ }
+ cookie.setDomain(value);
+ }
+
+ public void validate(final Cookie cookie, final CookieOrigin origin)
+ throws MalformedCookieException {
+ Args.notNull(cookie, "Cookie");
+ Args.notNull(origin, "Cookie origin");
+ // Validate the cookies domain attribute. NOTE: Domains without
+ // any dots are allowed to support hosts on private LANs that don't
+ // have DNS names. Since they have no dots, to domain-match the
+ // request-host and domain must be identical for the cookie to sent
+ // back to the origin-server.
+ final String host = origin.getHost();
+ String domain = cookie.getDomain();
+ if (domain == null) {
+ throw new CookieRestrictionViolationException("Cookie domain may not be null");
+ }
+ if (host.contains(".")) {
+ // Not required to have at least two dots. RFC 2965.
+ // A Set-Cookie2 with Domain=ajax.com will be accepted.
+
+ // domain must match host
+ if (!host.endsWith(domain)) {
+ if (domain.startsWith(".")) {
+ domain = domain.substring(1, domain.length());
+ }
+ if (!host.equals(domain)) {
+ throw new CookieRestrictionViolationException(
+ "Illegal domain attribute \"" + domain
+ + "\". Domain of origin: \"" + host + "\"");
+ }
+ }
+ } else {
+ if (!host.equals(domain)) {
+ throw new CookieRestrictionViolationException(
+ "Illegal domain attribute \"" + domain
+ + "\". Domain of origin: \"" + host + "\"");
+ }
+ }
+ }
+
+ public boolean match(final Cookie cookie, final CookieOrigin origin) {
+ Args.notNull(cookie, "Cookie");
+ Args.notNull(origin, "Cookie origin");
+ final String host = origin.getHost();
+ String domain = cookie.getDomain();
+ if (domain == null) {
+ return false;
+ }
+ if (host.equals(domain)) {
+ return true;
+ }
+ if (!domain.startsWith(".")) {
+ domain = '.' + domain;
+ }
+ return host.endsWith(domain) || host.equals(domain.substring(1));
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicExpiresHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicExpiresHandler.java
new file mode 100644
index 0000000000..9b9dee5b2e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicExpiresHandler.java
@@ -0,0 +1,66 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.util.Date;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.utils.DateUtils;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+import ch.boye.httpclientandroidlib.cookie.SetCookie;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ *
+ * @since 4.0
+ */
+@Immutable
+public class BasicExpiresHandler extends AbstractCookieAttributeHandler {
+
+ /** Valid date patterns */
+ private final String[] datepatterns;
+
+ public BasicExpiresHandler(final String[] datepatterns) {
+ Args.notNull(datepatterns, "Array of date patterns");
+ this.datepatterns = datepatterns;
+ }
+
+ public void parse(final SetCookie cookie, final String value)
+ throws MalformedCookieException {
+ Args.notNull(cookie, "Cookie");
+ if (value == null) {
+ throw new MalformedCookieException("Missing value for expires attribute");
+ }
+ final Date expiry = DateUtils.parseDate(value, this.datepatterns);
+ if (expiry == null) {
+ throw new MalformedCookieException("Unable to parse expires attribute: "
+ + value);
+ }
+ cookie.setExpiryDate(expiry);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicMaxAgeHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicMaxAgeHandler.java
new file mode 100644
index 0000000000..750361b22b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicMaxAgeHandler.java
@@ -0,0 +1,67 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.util.Date;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+import ch.boye.httpclientandroidlib.cookie.SetCookie;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ *
+ * @since 4.0
+ */
+@Immutable
+public class BasicMaxAgeHandler extends AbstractCookieAttributeHandler {
+
+ public BasicMaxAgeHandler() {
+ super();
+ }
+
+ public void parse(final SetCookie cookie, final String value)
+ throws MalformedCookieException {
+ Args.notNull(cookie, "Cookie");
+ if (value == null) {
+ throw new MalformedCookieException("Missing value for max-age attribute");
+ }
+ final int age;
+ try {
+ age = Integer.parseInt(value);
+ } catch (final NumberFormatException e) {
+ throw new MalformedCookieException ("Invalid max-age attribute: "
+ + value);
+ }
+ if (age < 0) {
+ throw new MalformedCookieException ("Negative max-age attribute: "
+ + value);
+ }
+ cookie.setExpiryDate(new Date(System.currentTimeMillis() + age * 1000L));
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicPathHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicPathHandler.java
new file mode 100644
index 0000000000..e736571482
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicPathHandler.java
@@ -0,0 +1,87 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+import ch.boye.httpclientandroidlib.cookie.CookieAttributeHandler;
+import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
+import ch.boye.httpclientandroidlib.cookie.CookieRestrictionViolationException;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+import ch.boye.httpclientandroidlib.cookie.SetCookie;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.TextUtils;
+
+/**
+ *
+ * @since 4.0
+ */
+@Immutable
+public class BasicPathHandler implements CookieAttributeHandler {
+
+ public BasicPathHandler() {
+ super();
+ }
+
+ public void parse(
+ final SetCookie cookie, final String value) throws MalformedCookieException {
+ Args.notNull(cookie, "Cookie");
+ cookie.setPath(!TextUtils.isBlank(value) ? value : "/");
+ }
+
+ public void validate(final Cookie cookie, final CookieOrigin origin)
+ throws MalformedCookieException {
+ if (!match(cookie, origin)) {
+ throw new CookieRestrictionViolationException(
+ "Illegal path attribute \"" + cookie.getPath()
+ + "\". Path of origin: \"" + origin.getPath() + "\"");
+ }
+ }
+
+ public boolean match(final Cookie cookie, final CookieOrigin origin) {
+ Args.notNull(cookie, "Cookie");
+ Args.notNull(origin, "Cookie origin");
+ final String targetpath = origin.getPath();
+ String topmostPath = cookie.getPath();
+ if (topmostPath == null) {
+ topmostPath = "/";
+ }
+ if (topmostPath.length() > 1 && topmostPath.endsWith("/")) {
+ topmostPath = topmostPath.substring(0, topmostPath.length() - 1);
+ }
+ boolean match = targetpath.startsWith (topmostPath);
+ // if there is a match and these values are not exactly the same we have
+ // to make sure we're not matcing "/foobar" and "/foo"
+ if (match && targetpath.length() != topmostPath.length()) {
+ if (!topmostPath.endsWith("/")) {
+ match = (targetpath.charAt(topmostPath.length()) == '/');
+ }
+ }
+ return match;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicSecureHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicSecureHandler.java
new file mode 100644
index 0000000000..939741fc37
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BasicSecureHandler.java
@@ -0,0 +1,60 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+import ch.boye.httpclientandroidlib.cookie.SetCookie;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ *
+ * @since 4.0
+ */
+@Immutable
+public class BasicSecureHandler extends AbstractCookieAttributeHandler {
+
+ public BasicSecureHandler() {
+ super();
+ }
+
+ public void parse(final SetCookie cookie, final String value)
+ throws MalformedCookieException {
+ Args.notNull(cookie, "Cookie");
+ cookie.setSecure(true);
+ }
+
+ @Override
+ public boolean match(final Cookie cookie, final CookieOrigin origin) {
+ Args.notNull(cookie, "Cookie");
+ Args.notNull(origin, "Cookie origin");
+ return !cookie.isSecure() || origin.isSecure();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BestMatchSpec.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BestMatchSpec.java
new file mode 100644
index 0000000000..bdb06090df
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BestMatchSpec.java
@@ -0,0 +1,207 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.FormattedHeader;
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
+import ch.boye.httpclientandroidlib.cookie.CookieSpec;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+import ch.boye.httpclientandroidlib.cookie.SM;
+import ch.boye.httpclientandroidlib.cookie.SetCookie2;
+import ch.boye.httpclientandroidlib.message.ParserCursor;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * 'Meta' cookie specification that picks up a cookie policy based on
+ * the format of cookies sent with the HTTP response.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe // CookieSpec fields are @NotThreadSafe
+public class BestMatchSpec implements CookieSpec {
+
+ private final String[] datepatterns;
+ private final boolean oneHeader;
+
+ // Cached values of CookieSpec instances
+ private RFC2965Spec strict; // @NotThreadSafe
+ private RFC2109Spec obsoleteStrict; // @NotThreadSafe
+ private BrowserCompatSpec compat; // @NotThreadSafe
+
+ public BestMatchSpec(final String[] datepatterns, final boolean oneHeader) {
+ super();
+ this.datepatterns = datepatterns == null ? null : datepatterns.clone();
+ this.oneHeader = oneHeader;
+ }
+
+ public BestMatchSpec() {
+ this(null, false);
+ }
+
+ private RFC2965Spec getStrict() {
+ if (this.strict == null) {
+ this.strict = new RFC2965Spec(this.datepatterns, this.oneHeader);
+ }
+ return strict;
+ }
+
+ private RFC2109Spec getObsoleteStrict() {
+ if (this.obsoleteStrict == null) {
+ this.obsoleteStrict = new RFC2109Spec(this.datepatterns, this.oneHeader);
+ }
+ return obsoleteStrict;
+ }
+
+ private BrowserCompatSpec getCompat() {
+ if (this.compat == null) {
+ this.compat = new BrowserCompatSpec(this.datepatterns);
+ }
+ return compat;
+ }
+
+ public List<Cookie> parse(
+ final Header header,
+ final CookieOrigin origin) throws MalformedCookieException {
+ Args.notNull(header, "Header");
+ Args.notNull(origin, "Cookie origin");
+ HeaderElement[] helems = header.getElements();
+ boolean versioned = false;
+ boolean netscape = false;
+ for (final HeaderElement helem: helems) {
+ if (helem.getParameterByName("version") != null) {
+ versioned = true;
+ }
+ if (helem.getParameterByName("expires") != null) {
+ netscape = true;
+ }
+ }
+ if (netscape || !versioned) {
+ // Need to parse the header again, because Netscape style cookies do not correctly
+ // support multiple header elements (comma cannot be treated as an element separator)
+ final NetscapeDraftHeaderParser parser = NetscapeDraftHeaderParser.DEFAULT;
+ final CharArrayBuffer buffer;
+ final ParserCursor cursor;
+ if (header instanceof FormattedHeader) {
+ buffer = ((FormattedHeader) header).getBuffer();
+ cursor = new ParserCursor(
+ ((FormattedHeader) header).getValuePos(),
+ buffer.length());
+ } else {
+ final String s = header.getValue();
+ if (s == null) {
+ throw new MalformedCookieException("Header value is null");
+ }
+ buffer = new CharArrayBuffer(s.length());
+ buffer.append(s);
+ cursor = new ParserCursor(0, buffer.length());
+ }
+ helems = new HeaderElement[] { parser.parseHeader(buffer, cursor) };
+ return getCompat().parse(helems, origin);
+ } else {
+ if (SM.SET_COOKIE2.equals(header.getName())) {
+ return getStrict().parse(helems, origin);
+ } else {
+ return getObsoleteStrict().parse(helems, origin);
+ }
+ }
+ }
+
+ public void validate(
+ final Cookie cookie,
+ final CookieOrigin origin) throws MalformedCookieException {
+ Args.notNull(cookie, "Cookie");
+ Args.notNull(origin, "Cookie origin");
+ if (cookie.getVersion() > 0) {
+ if (cookie instanceof SetCookie2) {
+ getStrict().validate(cookie, origin);
+ } else {
+ getObsoleteStrict().validate(cookie, origin);
+ }
+ } else {
+ getCompat().validate(cookie, origin);
+ }
+ }
+
+ public boolean match(final Cookie cookie, final CookieOrigin origin) {
+ Args.notNull(cookie, "Cookie");
+ Args.notNull(origin, "Cookie origin");
+ if (cookie.getVersion() > 0) {
+ if (cookie instanceof SetCookie2) {
+ return getStrict().match(cookie, origin);
+ } else {
+ return getObsoleteStrict().match(cookie, origin);
+ }
+ } else {
+ return getCompat().match(cookie, origin);
+ }
+ }
+
+ public List<Header> formatCookies(final List<Cookie> cookies) {
+ Args.notNull(cookies, "List of cookies");
+ int version = Integer.MAX_VALUE;
+ boolean isSetCookie2 = true;
+ for (final Cookie cookie: cookies) {
+ if (!(cookie instanceof SetCookie2)) {
+ isSetCookie2 = false;
+ }
+ if (cookie.getVersion() < version) {
+ version = cookie.getVersion();
+ }
+ }
+ if (version > 0) {
+ if (isSetCookie2) {
+ return getStrict().formatCookies(cookies);
+ } else {
+ return getObsoleteStrict().formatCookies(cookies);
+ }
+ } else {
+ return getCompat().formatCookies(cookies);
+ }
+ }
+
+ public int getVersion() {
+ return getStrict().getVersion();
+ }
+
+ public Header getVersionHeader() {
+ return getStrict().getVersionHeader();
+ }
+
+ @Override
+ public String toString() {
+ return "best-match";
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BestMatchSpecFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BestMatchSpecFactory.java
new file mode 100644
index 0000000000..c20e4bcd18
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BestMatchSpecFactory.java
@@ -0,0 +1,86 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.util.Collection;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.cookie.CookieSpec;
+import ch.boye.httpclientandroidlib.cookie.CookieSpecFactory;
+import ch.boye.httpclientandroidlib.cookie.CookieSpecProvider;
+import ch.boye.httpclientandroidlib.cookie.params.CookieSpecPNames;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * {@link CookieSpecProvider} implementation that creates and initializes
+ * {@link BestMatchSpec} instances.
+ *
+ * @since 4.0
+ */
+@Immutable
+@SuppressWarnings("deprecation")
+public class BestMatchSpecFactory implements CookieSpecFactory, CookieSpecProvider {
+
+ private final String[] datepatterns;
+ private final boolean oneHeader;
+
+ public BestMatchSpecFactory(final String[] datepatterns, final boolean oneHeader) {
+ super();
+ this.datepatterns = datepatterns;
+ this.oneHeader = oneHeader;
+ }
+
+ public BestMatchSpecFactory() {
+ this(null, false);
+ }
+
+ public CookieSpec newInstance(final HttpParams params) {
+ if (params != null) {
+
+ String[] patterns = null;
+ final Collection<?> param = (Collection<?>) params.getParameter(
+ CookieSpecPNames.DATE_PATTERNS);
+ if (param != null) {
+ patterns = new String[param.size()];
+ patterns = param.toArray(patterns);
+ }
+ final boolean singleHeader = params.getBooleanParameter(
+ CookieSpecPNames.SINGLE_COOKIE_HEADER, false);
+
+ return new BestMatchSpec(patterns, singleHeader);
+ } else {
+ return new BestMatchSpec();
+ }
+ }
+
+ public CookieSpec create(final HttpContext context) {
+ return new BestMatchSpec(this.datepatterns, this.oneHeader);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BrowserCompatSpec.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BrowserCompatSpec.java
new file mode 100644
index 0000000000..6caa719873
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BrowserCompatSpec.java
@@ -0,0 +1,219 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.FormattedHeader;
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.client.utils.DateUtils;
+import ch.boye.httpclientandroidlib.cookie.ClientCookie;
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+import ch.boye.httpclientandroidlib.cookie.SM;
+import ch.boye.httpclientandroidlib.message.BasicHeaderElement;
+import ch.boye.httpclientandroidlib.message.BasicHeaderValueFormatter;
+import ch.boye.httpclientandroidlib.message.BufferedHeader;
+import ch.boye.httpclientandroidlib.message.ParserCursor;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+
+/**
+ * Cookie specification that strives to closely mimic (mis)behavior of
+ * common web browser applications such as Microsoft Internet Explorer
+ * and Mozilla FireFox.
+ *
+ *
+ * @since 4.0
+ */
+@NotThreadSafe // superclass is @NotThreadSafe
+public class BrowserCompatSpec extends CookieSpecBase {
+
+
+ private static final String[] DEFAULT_DATE_PATTERNS = new String[] {
+ DateUtils.PATTERN_RFC1123,
+ DateUtils.PATTERN_RFC1036,
+ DateUtils.PATTERN_ASCTIME,
+ "EEE, dd-MMM-yyyy HH:mm:ss z",
+ "EEE, dd-MMM-yyyy HH-mm-ss z",
+ "EEE, dd MMM yy HH:mm:ss z",
+ "EEE dd-MMM-yyyy HH:mm:ss z",
+ "EEE dd MMM yyyy HH:mm:ss z",
+ "EEE dd-MMM-yyyy HH-mm-ss z",
+ "EEE dd-MMM-yy HH:mm:ss z",
+ "EEE dd MMM yy HH:mm:ss z",
+ "EEE,dd-MMM-yy HH:mm:ss z",
+ "EEE,dd-MMM-yyyy HH:mm:ss z",
+ "EEE, dd-MM-yyyy HH:mm:ss z",
+ };
+
+ private final String[] datepatterns;
+
+ /** Default constructor */
+ public BrowserCompatSpec(final String[] datepatterns, final BrowserCompatSpecFactory.SecurityLevel securityLevel) {
+ super();
+ if (datepatterns != null) {
+ this.datepatterns = datepatterns.clone();
+ } else {
+ this.datepatterns = DEFAULT_DATE_PATTERNS;
+ }
+ switch (securityLevel) {
+ case SECURITYLEVEL_DEFAULT:
+ registerAttribHandler(ClientCookie.PATH_ATTR, new BasicPathHandler());
+ break;
+ case SECURITYLEVEL_IE_MEDIUM:
+ registerAttribHandler(ClientCookie.PATH_ATTR, new BasicPathHandler() {
+ @Override
+ public void validate(final Cookie cookie, final CookieOrigin origin) throws MalformedCookieException {
+ // No validation
+ }
+ }
+ );
+ break;
+ default:
+ throw new RuntimeException("Unknown security level");
+ }
+
+ registerAttribHandler(ClientCookie.DOMAIN_ATTR, new BasicDomainHandler());
+ registerAttribHandler(ClientCookie.MAX_AGE_ATTR, new BasicMaxAgeHandler());
+ registerAttribHandler(ClientCookie.SECURE_ATTR, new BasicSecureHandler());
+ registerAttribHandler(ClientCookie.COMMENT_ATTR, new BasicCommentHandler());
+ registerAttribHandler(ClientCookie.EXPIRES_ATTR, new BasicExpiresHandler(
+ this.datepatterns));
+ registerAttribHandler(ClientCookie.VERSION_ATTR, new BrowserCompatVersionAttributeHandler());
+ }
+
+ /** Default constructor */
+ public BrowserCompatSpec(final String[] datepatterns) {
+ this(datepatterns, BrowserCompatSpecFactory.SecurityLevel.SECURITYLEVEL_DEFAULT);
+ }
+
+ /** Default constructor */
+ public BrowserCompatSpec() {
+ this(null, BrowserCompatSpecFactory.SecurityLevel.SECURITYLEVEL_DEFAULT);
+ }
+
+ public List<Cookie> parse(final Header header, final CookieOrigin origin)
+ throws MalformedCookieException {
+ Args.notNull(header, "Header");
+ Args.notNull(origin, "Cookie origin");
+ final String headername = header.getName();
+ if (!headername.equalsIgnoreCase(SM.SET_COOKIE)) {
+ throw new MalformedCookieException("Unrecognized cookie header '"
+ + header.toString() + "'");
+ }
+ HeaderElement[] helems = header.getElements();
+ boolean versioned = false;
+ boolean netscape = false;
+ for (final HeaderElement helem: helems) {
+ if (helem.getParameterByName("version") != null) {
+ versioned = true;
+ }
+ if (helem.getParameterByName("expires") != null) {
+ netscape = true;
+ }
+ }
+ if (netscape || !versioned) {
+ // Need to parse the header again, because Netscape style cookies do not correctly
+ // support multiple header elements (comma cannot be treated as an element separator)
+ final NetscapeDraftHeaderParser parser = NetscapeDraftHeaderParser.DEFAULT;
+ final CharArrayBuffer buffer;
+ final ParserCursor cursor;
+ if (header instanceof FormattedHeader) {
+ buffer = ((FormattedHeader) header).getBuffer();
+ cursor = new ParserCursor(
+ ((FormattedHeader) header).getValuePos(),
+ buffer.length());
+ } else {
+ final String s = header.getValue();
+ if (s == null) {
+ throw new MalformedCookieException("Header value is null");
+ }
+ buffer = new CharArrayBuffer(s.length());
+ buffer.append(s);
+ cursor = new ParserCursor(0, buffer.length());
+ }
+ helems = new HeaderElement[] { parser.parseHeader(buffer, cursor) };
+ }
+ return parse(helems, origin);
+ }
+
+ private static boolean isQuoteEnclosed(final String s) {
+ return s != null && s.startsWith("\"") && s.endsWith("\"");
+ }
+
+ public List<Header> formatCookies(final List<Cookie> cookies) {
+ Args.notEmpty(cookies, "List of cookies");
+ final CharArrayBuffer buffer = new CharArrayBuffer(20 * cookies.size());
+ buffer.append(SM.COOKIE);
+ buffer.append(": ");
+ for (int i = 0; i < cookies.size(); i++) {
+ final Cookie cookie = cookies.get(i);
+ if (i > 0) {
+ buffer.append("; ");
+ }
+ final String cookieName = cookie.getName();
+ final String cookieValue = cookie.getValue();
+ if (cookie.getVersion() > 0 && !isQuoteEnclosed(cookieValue)) {
+ BasicHeaderValueFormatter.INSTANCE.formatHeaderElement(
+ buffer,
+ new BasicHeaderElement(cookieName, cookieValue),
+ false);
+ } else {
+ // Netscape style cookies do not support quoted values
+ buffer.append(cookieName);
+ buffer.append("=");
+ if (cookieValue != null) {
+ buffer.append(cookieValue);
+ }
+ }
+ }
+ final List<Header> headers = new ArrayList<Header>(1);
+ headers.add(new BufferedHeader(buffer));
+ return headers;
+ }
+
+ public int getVersion() {
+ return 0;
+ }
+
+ public Header getVersionHeader() {
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return "compatibility";
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BrowserCompatSpecFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BrowserCompatSpecFactory.java
new file mode 100644
index 0000000000..f6239b4f3e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BrowserCompatSpecFactory.java
@@ -0,0 +1,92 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.util.Collection;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.cookie.CookieSpec;
+import ch.boye.httpclientandroidlib.cookie.CookieSpecFactory;
+import ch.boye.httpclientandroidlib.cookie.CookieSpecProvider;
+import ch.boye.httpclientandroidlib.cookie.params.CookieSpecPNames;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * {@link CookieSpecProvider} implementation that creates and initializes
+ * {@link BrowserCompatSpec} instances.
+ *
+ * @since 4.0
+ */
+@Immutable
+@SuppressWarnings("deprecation")
+public class BrowserCompatSpecFactory implements CookieSpecFactory, CookieSpecProvider {
+
+ public enum SecurityLevel {
+ SECURITYLEVEL_DEFAULT,
+ SECURITYLEVEL_IE_MEDIUM
+ }
+
+ private final String[] datepatterns;
+ private final SecurityLevel securityLevel;
+
+ public BrowserCompatSpecFactory(final String[] datepatterns, final SecurityLevel securityLevel) {
+ super();
+ this.datepatterns = datepatterns;
+ this.securityLevel = securityLevel;
+ }
+
+ public BrowserCompatSpecFactory(final String[] datepatterns) {
+ this(null, SecurityLevel.SECURITYLEVEL_DEFAULT);
+ }
+
+ public BrowserCompatSpecFactory() {
+ this(null, SecurityLevel.SECURITYLEVEL_DEFAULT);
+ }
+
+ public CookieSpec newInstance(final HttpParams params) {
+ if (params != null) {
+
+ String[] patterns = null;
+ final Collection<?> param = (Collection<?>) params.getParameter(
+ CookieSpecPNames.DATE_PATTERNS);
+ if (param != null) {
+ patterns = new String[param.size()];
+ patterns = param.toArray(patterns);
+ }
+ return new BrowserCompatSpec(patterns, securityLevel);
+ } else {
+ return new BrowserCompatSpec(null, securityLevel);
+ }
+ }
+
+ public CookieSpec create(final HttpContext context) {
+ return new BrowserCompatSpec(this.datepatterns);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BrowserCompatVersionAttributeHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BrowserCompatVersionAttributeHandler.java
new file mode 100644
index 0000000000..210fac962e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/BrowserCompatVersionAttributeHandler.java
@@ -0,0 +1,66 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+import ch.boye.httpclientandroidlib.cookie.SetCookie;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * <tt>"Version"</tt> cookie attribute handler for BrowserCompat cookie spec.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class BrowserCompatVersionAttributeHandler extends
+ AbstractCookieAttributeHandler {
+
+ public BrowserCompatVersionAttributeHandler() {
+ super();
+ }
+
+ /**
+ * Parse cookie version attribute.
+ */
+ public void parse(final SetCookie cookie, final String value)
+ throws MalformedCookieException {
+ Args.notNull(cookie, "Cookie");
+ if (value == null) {
+ throw new MalformedCookieException("Missing value for version attribute");
+ }
+ int version = 0;
+ try {
+ version = Integer.parseInt(value);
+ } catch (final NumberFormatException e) {
+ // Just ignore invalid versions
+ }
+ cookie.setVersion(version);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/CookieSpecBase.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/CookieSpecBase.java
new file mode 100644
index 0000000000..e59c3686e7
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/CookieSpecBase.java
@@ -0,0 +1,121 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+import ch.boye.httpclientandroidlib.cookie.CookieAttributeHandler;
+import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Cookie management functions shared by all specification.
+ *
+ *
+ * @since 4.0
+ */
+@NotThreadSafe // AbstractCookieSpec is not thread-safe
+public abstract class CookieSpecBase extends AbstractCookieSpec {
+
+ protected static String getDefaultPath(final CookieOrigin origin) {
+ String defaultPath = origin.getPath();
+ int lastSlashIndex = defaultPath.lastIndexOf('/');
+ if (lastSlashIndex >= 0) {
+ if (lastSlashIndex == 0) {
+ //Do not remove the very first slash
+ lastSlashIndex = 1;
+ }
+ defaultPath = defaultPath.substring(0, lastSlashIndex);
+ }
+ return defaultPath;
+ }
+
+ protected static String getDefaultDomain(final CookieOrigin origin) {
+ return origin.getHost();
+ }
+
+ protected List<Cookie> parse(final HeaderElement[] elems, final CookieOrigin origin)
+ throws MalformedCookieException {
+ final List<Cookie> cookies = new ArrayList<Cookie>(elems.length);
+ for (final HeaderElement headerelement : elems) {
+ final String name = headerelement.getName();
+ final String value = headerelement.getValue();
+ if (name == null || name.length() == 0) {
+ throw new MalformedCookieException("Cookie name may not be empty");
+ }
+
+ final BasicClientCookie cookie = new BasicClientCookie(name, value);
+ cookie.setPath(getDefaultPath(origin));
+ cookie.setDomain(getDefaultDomain(origin));
+
+ // cycle through the parameters
+ final NameValuePair[] attribs = headerelement.getParameters();
+ for (int j = attribs.length - 1; j >= 0; j--) {
+ final NameValuePair attrib = attribs[j];
+ final String s = attrib.getName().toLowerCase(Locale.ENGLISH);
+
+ cookie.setAttribute(s, attrib.getValue());
+
+ final CookieAttributeHandler handler = findAttribHandler(s);
+ if (handler != null) {
+ handler.parse(cookie, attrib.getValue());
+ }
+ }
+ cookies.add(cookie);
+ }
+ return cookies;
+ }
+
+ public void validate(final Cookie cookie, final CookieOrigin origin)
+ throws MalformedCookieException {
+ Args.notNull(cookie, "Cookie");
+ Args.notNull(origin, "Cookie origin");
+ for (final CookieAttributeHandler handler: getAttribHandlers()) {
+ handler.validate(cookie, origin);
+ }
+ }
+
+ public boolean match(final Cookie cookie, final CookieOrigin origin) {
+ Args.notNull(cookie, "Cookie");
+ Args.notNull(origin, "Cookie origin");
+ for (final CookieAttributeHandler handler: getAttribHandlers()) {
+ if (!handler.match(cookie, origin)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/DateParseException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/DateParseException.java
new file mode 100644
index 0000000000..08b387272b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/DateParseException.java
@@ -0,0 +1,62 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * An exception to indicate an error parsing a date string.
+ *
+ * @see DateUtils
+ *
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) no longer used.
+ */
+@Deprecated
+@Immutable
+public class DateParseException extends Exception {
+
+ private static final long serialVersionUID = 4417696455000643370L;
+
+ /**
+ *
+ */
+ public DateParseException() {
+ super();
+ }
+
+ /**
+ * @param message the exception message
+ */
+ public DateParseException(final String message) {
+ super(message);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/DateUtils.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/DateUtils.java
new file mode 100644
index 0000000000..897d6fb008
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/DateUtils.java
@@ -0,0 +1,156 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.util.Date;
+import java.util.TimeZone;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * A utility class for parsing and formatting HTTP dates as used in cookies and
+ * other headers. This class handles dates as defined by RFC 2616 section
+ * 3.3.1 as well as some other common non-standard formats.
+ *
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) Use {@link ch.boye.httpclientandroidlib.client.utils.DateUtils}.
+ */
+@Deprecated
+@Immutable
+public final class DateUtils {
+
+ /**
+ * Date format pattern used to parse HTTP date headers in RFC 1123 format.
+ */
+ public static final String PATTERN_RFC1123 = ch.boye.httpclientandroidlib.client.utils.DateUtils.PATTERN_RFC1123;
+
+ /**
+ * Date format pattern used to parse HTTP date headers in RFC 1036 format.
+ */
+ public static final String PATTERN_RFC1036 = ch.boye.httpclientandroidlib.client.utils.DateUtils.PATTERN_RFC1036;
+
+ /**
+ * Date format pattern used to parse HTTP date headers in ANSI C
+ * <code>asctime()</code> format.
+ */
+ public static final String PATTERN_ASCTIME = ch.boye.httpclientandroidlib.client.utils.DateUtils.PATTERN_ASCTIME;
+
+ public static final TimeZone GMT = TimeZone.getTimeZone("GMT");
+
+ /**
+ * Parses a date value. The formats used for parsing the date value are retrieved from
+ * the default http params.
+ *
+ * @param dateValue the date value to parse
+ *
+ * @return the parsed date
+ *
+ * @throws DateParseException if the value could not be parsed using any of the
+ * supported date formats
+ */
+ public static Date parseDate(final String dateValue) throws DateParseException {
+ return parseDate(dateValue, null, null);
+ }
+
+ /**
+ * Parses the date value using the given date formats.
+ *
+ * @param dateValue the date value to parse
+ * @param dateFormats the date formats to use
+ *
+ * @return the parsed date
+ *
+ * @throws DateParseException if none of the dataFormats could parse the dateValue
+ */
+ public static Date parseDate(final String dateValue, final String[] dateFormats)
+ throws DateParseException {
+ return parseDate(dateValue, dateFormats, null);
+ }
+
+ /**
+ * Parses the date value using the given date formats.
+ *
+ * @param dateValue the date value to parse
+ * @param dateFormats the date formats to use
+ * @param startDate During parsing, two digit years will be placed in the range
+ * <code>startDate</code> to <code>startDate + 100 years</code>. This value may
+ * be <code>null</code>. When <code>null</code> is given as a parameter, year
+ * <code>2000</code> will be used.
+ *
+ * @return the parsed date
+ *
+ * @throws DateParseException if none of the dataFormats could parse the dateValue
+ */
+ public static Date parseDate(
+ final String dateValue,
+ final String[] dateFormats,
+ final Date startDate
+ ) throws DateParseException {
+ final Date d = ch.boye.httpclientandroidlib.client.utils.DateUtils.parseDate(dateValue, dateFormats, startDate);
+ if (d == null) {
+ throw new DateParseException("Unable to parse the date " + dateValue);
+ }
+ return d;
+ }
+
+ /**
+ * Formats the given date according to the RFC 1123 pattern.
+ *
+ * @param date The date to format.
+ * @return An RFC 1123 formatted date string.
+ *
+ * @see #PATTERN_RFC1123
+ */
+ public static String formatDate(final Date date) {
+ return ch.boye.httpclientandroidlib.client.utils.DateUtils.formatDate(date);
+ }
+
+ /**
+ * Formats the given date according to the specified pattern. The pattern
+ * must conform to that used by the {@link java.text.SimpleDateFormat simple
+ * date format} class.
+ *
+ * @param date The date to format.
+ * @param pattern The pattern to use for formatting the date.
+ * @return A formatted date string.
+ *
+ * @throws IllegalArgumentException If the given date pattern is invalid.
+ *
+ * @see java.text.SimpleDateFormat
+ */
+ public static String formatDate(final Date date, final String pattern) {
+ return ch.boye.httpclientandroidlib.client.utils.DateUtils.formatDate(date, pattern);
+ }
+
+ /** This class should not be instantiated. */
+ private DateUtils() {
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/IgnoreSpec.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/IgnoreSpec.java
new file mode 100644
index 0000000000..3aa350a40b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/IgnoreSpec.java
@@ -0,0 +1,63 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.util.Collections;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+
+/**
+ * CookieSpec that ignores all cookies
+ *
+ * @since 4.1
+ */
+@NotThreadSafe // superclass is @NotThreadSafe
+public class IgnoreSpec extends CookieSpecBase {
+
+ public int getVersion() {
+ return 0;
+ }
+
+ public List<Cookie> parse(final Header header, final CookieOrigin origin)
+ throws MalformedCookieException {
+ return Collections.emptyList();
+ }
+
+ public List<Header> formatCookies(final List<Cookie> cookies) {
+ return Collections.emptyList();
+ }
+
+ public Header getVersionHeader() {
+ return null;
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/IgnoreSpecFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/IgnoreSpecFactory.java
new file mode 100644
index 0000000000..426e25cadd
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/IgnoreSpecFactory.java
@@ -0,0 +1,58 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.cookie.CookieSpec;
+import ch.boye.httpclientandroidlib.cookie.CookieSpecFactory;
+import ch.boye.httpclientandroidlib.cookie.CookieSpecProvider;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * {@link CookieSpecProvider} implementation that ignores all cookies.
+ *
+ * @since 4.1
+ */
+@Immutable
+@SuppressWarnings("deprecation")
+public class IgnoreSpecFactory implements CookieSpecFactory, CookieSpecProvider {
+
+ public IgnoreSpecFactory() {
+ super();
+ }
+
+ public CookieSpec newInstance(final HttpParams params) {
+ return new IgnoreSpec();
+ }
+
+ public CookieSpec create(final HttpContext context) {
+ return new IgnoreSpec();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/NetscapeDomainHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/NetscapeDomainHandler.java
new file mode 100644
index 0000000000..adbd40e916
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/NetscapeDomainHandler.java
@@ -0,0 +1,106 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.util.Locale;
+import java.util.StringTokenizer;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
+import ch.boye.httpclientandroidlib.cookie.CookieRestrictionViolationException;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ *
+ * @since 4.0
+ */
+@Immutable
+public class NetscapeDomainHandler extends BasicDomainHandler {
+
+ public NetscapeDomainHandler() {
+ super();
+ }
+
+ @Override
+ public void validate(final Cookie cookie, final CookieOrigin origin)
+ throws MalformedCookieException {
+ super.validate(cookie, origin);
+ // Perform Netscape Cookie draft specific validation
+ final String host = origin.getHost();
+ final String domain = cookie.getDomain();
+ if (host.contains(".")) {
+ final int domainParts = new StringTokenizer(domain, ".").countTokens();
+
+ if (isSpecialDomain(domain)) {
+ if (domainParts < 2) {
+ throw new CookieRestrictionViolationException("Domain attribute \""
+ + domain
+ + "\" violates the Netscape cookie specification for "
+ + "special domains");
+ }
+ } else {
+ if (domainParts < 3) {
+ throw new CookieRestrictionViolationException("Domain attribute \""
+ + domain
+ + "\" violates the Netscape cookie specification");
+ }
+ }
+ }
+ }
+
+ /**
+ * Checks if the given domain is in one of the seven special
+ * top level domains defined by the Netscape cookie specification.
+ * @param domain The domain.
+ * @return True if the specified domain is "special"
+ */
+ private static boolean isSpecialDomain(final String domain) {
+ final String ucDomain = domain.toUpperCase(Locale.ENGLISH);
+ return ucDomain.endsWith(".COM")
+ || ucDomain.endsWith(".EDU")
+ || ucDomain.endsWith(".NET")
+ || ucDomain.endsWith(".GOV")
+ || ucDomain.endsWith(".MIL")
+ || ucDomain.endsWith(".ORG")
+ || ucDomain.endsWith(".INT");
+ }
+
+ @Override
+ public boolean match(final Cookie cookie, final CookieOrigin origin) {
+ Args.notNull(cookie, "Cookie");
+ Args.notNull(origin, "Cookie origin");
+ final String host = origin.getHost();
+ final String domain = cookie.getDomain();
+ if (domain == null) {
+ return false;
+ }
+ return host.endsWith(domain);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/NetscapeDraftHeaderParser.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/NetscapeDraftHeaderParser.java
new file mode 100644
index 0000000000..89a59688f9
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/NetscapeDraftHeaderParser.java
@@ -0,0 +1,138 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.ParseException;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.message.BasicHeaderElement;
+import ch.boye.httpclientandroidlib.message.BasicNameValuePair;
+import ch.boye.httpclientandroidlib.message.ParserCursor;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ *
+ * @since 4.0
+ */
+@Immutable
+public class NetscapeDraftHeaderParser {
+
+ public final static NetscapeDraftHeaderParser DEFAULT = new NetscapeDraftHeaderParser();
+
+ public NetscapeDraftHeaderParser() {
+ super();
+ }
+
+ public HeaderElement parseHeader(
+ final CharArrayBuffer buffer,
+ final ParserCursor cursor) throws ParseException {
+ Args.notNull(buffer, "Char array buffer");
+ Args.notNull(cursor, "Parser cursor");
+ final NameValuePair nvp = parseNameValuePair(buffer, cursor);
+ final List<NameValuePair> params = new ArrayList<NameValuePair>();
+ while (!cursor.atEnd()) {
+ final NameValuePair param = parseNameValuePair(buffer, cursor);
+ params.add(param);
+ }
+ return new BasicHeaderElement(
+ nvp.getName(),
+ nvp.getValue(), params.toArray(new NameValuePair[params.size()]));
+ }
+
+ private NameValuePair parseNameValuePair(
+ final CharArrayBuffer buffer, final ParserCursor cursor) {
+ boolean terminated = false;
+
+ int pos = cursor.getPos();
+ final int indexFrom = cursor.getPos();
+ final int indexTo = cursor.getUpperBound();
+
+ // Find name
+ String name = null;
+ while (pos < indexTo) {
+ final char ch = buffer.charAt(pos);
+ if (ch == '=') {
+ break;
+ }
+ if (ch == ';') {
+ terminated = true;
+ break;
+ }
+ pos++;
+ }
+
+ if (pos == indexTo) {
+ terminated = true;
+ name = buffer.substringTrimmed(indexFrom, indexTo);
+ } else {
+ name = buffer.substringTrimmed(indexFrom, pos);
+ pos++;
+ }
+
+ if (terminated) {
+ cursor.updatePos(pos);
+ return new BasicNameValuePair(name, null);
+ }
+
+ // Find value
+ String value = null;
+ int i1 = pos;
+
+ while (pos < indexTo) {
+ final char ch = buffer.charAt(pos);
+ if (ch == ';') {
+ terminated = true;
+ break;
+ }
+ pos++;
+ }
+
+ int i2 = pos;
+ // Trim leading white spaces
+ while (i1 < i2 && (HTTP.isWhitespace(buffer.charAt(i1)))) {
+ i1++;
+ }
+ // Trim trailing white spaces
+ while ((i2 > i1) && (HTTP.isWhitespace(buffer.charAt(i2 - 1)))) {
+ i2--;
+ }
+ value = buffer.substring(i1, i2);
+ if (terminated) {
+ pos++;
+ }
+ cursor.updatePos(pos);
+ return new BasicNameValuePair(name, value);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/NetscapeDraftSpec.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/NetscapeDraftSpec.java
new file mode 100644
index 0000000000..139e5985c8
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/NetscapeDraftSpec.java
@@ -0,0 +1,171 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.FormattedHeader;
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.cookie.ClientCookie;
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+import ch.boye.httpclientandroidlib.cookie.SM;
+import ch.boye.httpclientandroidlib.message.BufferedHeader;
+import ch.boye.httpclientandroidlib.message.ParserCursor;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * This {@link ch.boye.httpclientandroidlib.cookie.CookieSpec} implementation conforms to
+ * the original draft specification published by Netscape Communications.
+ * It should be avoided unless absolutely necessary for compatibility with
+ * legacy applications.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe // superclass is @NotThreadSafe
+public class NetscapeDraftSpec extends CookieSpecBase {
+
+ protected static final String EXPIRES_PATTERN = "EEE, dd-MMM-yy HH:mm:ss z";
+
+ private final String[] datepatterns;
+
+ /** Default constructor */
+ public NetscapeDraftSpec(final String[] datepatterns) {
+ super();
+ if (datepatterns != null) {
+ this.datepatterns = datepatterns.clone();
+ } else {
+ this.datepatterns = new String[] { EXPIRES_PATTERN };
+ }
+ registerAttribHandler(ClientCookie.PATH_ATTR, new BasicPathHandler());
+ registerAttribHandler(ClientCookie.DOMAIN_ATTR, new NetscapeDomainHandler());
+ registerAttribHandler(ClientCookie.MAX_AGE_ATTR, new BasicMaxAgeHandler());
+ registerAttribHandler(ClientCookie.SECURE_ATTR, new BasicSecureHandler());
+ registerAttribHandler(ClientCookie.COMMENT_ATTR, new BasicCommentHandler());
+ registerAttribHandler(ClientCookie.EXPIRES_ATTR, new BasicExpiresHandler(
+ this.datepatterns));
+ }
+
+ /** Default constructor */
+ public NetscapeDraftSpec() {
+ this(null);
+ }
+
+ /**
+ * Parses the Set-Cookie value into an array of <tt>Cookie</tt>s.
+ *
+ * <p>Syntax of the Set-Cookie HTTP Response Header:</p>
+ *
+ * <p>This is the format a CGI script would use to add to
+ * the HTTP headers a new piece of data which is to be stored by
+ * the client for later retrieval.</p>
+ *
+ * <PRE>
+ * Set-Cookie: NAME=VALUE; expires=DATE; path=PATH; domain=DOMAIN_NAME; secure
+ * </PRE>
+ *
+ * <p>Please note that the Netscape draft specification does not fully conform to the HTTP
+ * header format. Comma character if present in <code>Set-Cookie</code> will not be treated
+ * as a header element separator</p>
+ *
+ * @see <a href="http://web.archive.org/web/20020803110822/http://wp.netscape.com/newsref/std/cookie_spec.html">
+ * The Cookie Spec.</a>
+ *
+ * @param header the <tt>Set-Cookie</tt> received from the server
+ * @return an array of <tt>Cookie</tt>s parsed from the Set-Cookie value
+ * @throws MalformedCookieException if an exception occurs during parsing
+ */
+ public List<Cookie> parse(final Header header, final CookieOrigin origin)
+ throws MalformedCookieException {
+ Args.notNull(header, "Header");
+ Args.notNull(origin, "Cookie origin");
+ if (!header.getName().equalsIgnoreCase(SM.SET_COOKIE)) {
+ throw new MalformedCookieException("Unrecognized cookie header '"
+ + header.toString() + "'");
+ }
+ final NetscapeDraftHeaderParser parser = NetscapeDraftHeaderParser.DEFAULT;
+ final CharArrayBuffer buffer;
+ final ParserCursor cursor;
+ if (header instanceof FormattedHeader) {
+ buffer = ((FormattedHeader) header).getBuffer();
+ cursor = new ParserCursor(
+ ((FormattedHeader) header).getValuePos(),
+ buffer.length());
+ } else {
+ final String s = header.getValue();
+ if (s == null) {
+ throw new MalformedCookieException("Header value is null");
+ }
+ buffer = new CharArrayBuffer(s.length());
+ buffer.append(s);
+ cursor = new ParserCursor(0, buffer.length());
+ }
+ return parse(new HeaderElement[] { parser.parseHeader(buffer, cursor) }, origin);
+ }
+
+ public List<Header> formatCookies(final List<Cookie> cookies) {
+ Args.notEmpty(cookies, "List of cookies");
+ final CharArrayBuffer buffer = new CharArrayBuffer(20 * cookies.size());
+ buffer.append(SM.COOKIE);
+ buffer.append(": ");
+ for (int i = 0; i < cookies.size(); i++) {
+ final Cookie cookie = cookies.get(i);
+ if (i > 0) {
+ buffer.append("; ");
+ }
+ buffer.append(cookie.getName());
+ final String s = cookie.getValue();
+ if (s != null) {
+ buffer.append("=");
+ buffer.append(s);
+ }
+ }
+ final List<Header> headers = new ArrayList<Header>(1);
+ headers.add(new BufferedHeader(buffer));
+ return headers;
+ }
+
+ public int getVersion() {
+ return 0;
+ }
+
+ public Header getVersionHeader() {
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return "netscape";
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/NetscapeDraftSpecFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/NetscapeDraftSpecFactory.java
new file mode 100644
index 0000000000..d1187a1507
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/NetscapeDraftSpecFactory.java
@@ -0,0 +1,81 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.util.Collection;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.cookie.CookieSpec;
+import ch.boye.httpclientandroidlib.cookie.CookieSpecFactory;
+import ch.boye.httpclientandroidlib.cookie.CookieSpecProvider;
+import ch.boye.httpclientandroidlib.cookie.params.CookieSpecPNames;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * {@link CookieSpecProvider} implementation that creates and initializes
+ * {@link NetscapeDraftSpec} instances.
+ *
+ * @since 4.0
+ */
+@Immutable
+@SuppressWarnings("deprecation")
+public class NetscapeDraftSpecFactory implements CookieSpecFactory, CookieSpecProvider {
+
+ private final String[] datepatterns;
+
+ public NetscapeDraftSpecFactory(final String[] datepatterns) {
+ super();
+ this.datepatterns = datepatterns;
+ }
+
+ public NetscapeDraftSpecFactory() {
+ this(null);
+ }
+
+ public CookieSpec newInstance(final HttpParams params) {
+ if (params != null) {
+
+ String[] patterns = null;
+ final Collection<?> param = (Collection<?>) params.getParameter(
+ CookieSpecPNames.DATE_PATTERNS);
+ if (param != null) {
+ patterns = new String[param.size()];
+ patterns = param.toArray(patterns);
+ }
+ return new NetscapeDraftSpec(patterns);
+ } else {
+ return new NetscapeDraftSpec();
+ }
+ }
+
+ public CookieSpec create(final HttpContext context) {
+ return new NetscapeDraftSpec(this.datepatterns);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/PublicSuffixFilter.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/PublicSuffixFilter.java
new file mode 100644
index 0000000000..4777918ba7
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/PublicSuffixFilter.java
@@ -0,0 +1,132 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import ch.boye.httpclientandroidlib.client.utils.Punycode;
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+import ch.boye.httpclientandroidlib.cookie.CookieAttributeHandler;
+import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+import ch.boye.httpclientandroidlib.cookie.SetCookie;
+
+/**
+ * Wraps a CookieAttributeHandler and leverages its match method
+ * to never match a suffix from a black list. May be used to provide
+ * additional security for cross-site attack types by preventing
+ * cookies from apparent domains that are not publicly available.
+ * An uptodate list of suffixes can be obtained from
+ * <a href="http://publicsuffix.org/">publicsuffix.org</a>
+ *
+ * @since 4.0
+ */
+public class PublicSuffixFilter implements CookieAttributeHandler {
+ private final CookieAttributeHandler wrapped;
+ private Set<String> exceptions;
+ private Set<String> suffixes;
+
+ public PublicSuffixFilter(final CookieAttributeHandler wrapped) {
+ this.wrapped = wrapped;
+ }
+
+ /**
+ * Sets the suffix blacklist patterns.
+ * A pattern can be "com", "*.jp"
+ * TODO add support for patterns like "lib.*.us"
+ * @param suffixes
+ */
+ public void setPublicSuffixes(final Collection<String> suffixes) {
+ this.suffixes = new HashSet<String>(suffixes);
+ }
+
+ /**
+ * Sets the exceptions from the blacklist. Exceptions can not be patterns.
+ * TODO add support for patterns
+ * @param exceptions
+ */
+ public void setExceptions(final Collection<String> exceptions) {
+ this.exceptions = new HashSet<String>(exceptions);
+ }
+
+ /**
+ * Never matches if the cookie's domain is from the blacklist.
+ */
+ public boolean match(final Cookie cookie, final CookieOrigin origin) {
+ if (isForPublicSuffix(cookie)) {
+ return false;
+ }
+ return wrapped.match(cookie, origin);
+ }
+
+ public void parse(final SetCookie cookie, final String value) throws MalformedCookieException {
+ wrapped.parse(cookie, value);
+ }
+
+ public void validate(final Cookie cookie, final CookieOrigin origin) throws MalformedCookieException {
+ wrapped.validate(cookie, origin);
+ }
+
+ private boolean isForPublicSuffix(final Cookie cookie) {
+ String domain = cookie.getDomain();
+ if (domain.startsWith(".")) {
+ domain = domain.substring(1);
+ }
+ domain = Punycode.toUnicode(domain);
+
+ // An exception rule takes priority over any other matching rule.
+ if (this.exceptions != null) {
+ if (this.exceptions.contains(domain)) {
+ return false;
+ }
+ }
+
+
+ if (this.suffixes == null) {
+ return false;
+ }
+
+ do {
+ if (this.suffixes.contains(domain)) {
+ return true;
+ }
+ // patterns
+ if (domain.startsWith("*.")) {
+ domain = domain.substring(2);
+ }
+ final int nextdot = domain.indexOf('.');
+ if (nextdot == -1) {
+ break;
+ }
+ domain = "*" + domain.substring(nextdot);
+ } while (domain.length() > 0);
+
+ return false;
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/PublicSuffixListParser.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/PublicSuffixListParser.java
new file mode 100644
index 0000000000..4f0374f99d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/PublicSuffixListParser.java
@@ -0,0 +1,127 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Collection;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Parses the list from <a href="http://publicsuffix.org/">publicsuffix.org</a>
+ * and configures a PublicSuffixFilter.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class PublicSuffixListParser {
+ private static final int MAX_LINE_LEN = 256;
+ private final PublicSuffixFilter filter;
+
+ PublicSuffixListParser(final PublicSuffixFilter filter) {
+ this.filter = filter;
+ }
+
+ /**
+ * Parses the public suffix list format.
+ * When creating the reader from the file, make sure to
+ * use the correct encoding (the original list is in UTF-8).
+ *
+ * @param list the suffix list. The caller is responsible for closing the reader.
+ * @throws IOException on error while reading from list
+ */
+ public void parse(final Reader list) throws IOException {
+ final Collection<String> rules = new ArrayList<String>();
+ final Collection<String> exceptions = new ArrayList<String>();
+ final BufferedReader r = new BufferedReader(list);
+ final StringBuilder sb = new StringBuilder(256);
+ boolean more = true;
+ while (more) {
+ more = readLine(r, sb);
+ String line = sb.toString();
+ if (line.length() == 0) {
+ continue;
+ }
+ if (line.startsWith("//"))
+ {
+ continue; //entire lines can also be commented using //
+ }
+ if (line.startsWith("."))
+ {
+ line = line.substring(1); // A leading dot is optional
+ }
+ // An exclamation mark (!) at the start of a rule marks an exception to a previous wildcard rule
+ final boolean isException = line.startsWith("!");
+ if (isException) {
+ line = line.substring(1);
+ }
+
+ if (isException) {
+ exceptions.add(line);
+ } else {
+ rules.add(line);
+ }
+ }
+
+ filter.setPublicSuffixes(rules);
+ filter.setExceptions(exceptions);
+ }
+
+ /**
+ *
+ * @param r
+ * @param sb
+ * @return false when the end of the stream is reached
+ * @throws IOException
+ */
+ private boolean readLine(final Reader r, final StringBuilder sb) throws IOException {
+ sb.setLength(0);
+ int b;
+ boolean hitWhitespace = false;
+ while ((b = r.read()) != -1) {
+ final char c = (char) b;
+ if (c == '\n') {
+ break;
+ }
+ // Each line is only read up to the first whitespace
+ if (Character.isWhitespace(c)) {
+ hitWhitespace = true;
+ }
+ if (!hitWhitespace) {
+ sb.append(c);
+ }
+ if (sb.length() > MAX_LINE_LEN)
+ {
+ throw new IOException("Line too long"); // prevent excess memory usage
+ }
+ }
+ return (b != -1);
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2109DomainHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2109DomainHandler.java
new file mode 100644
index 0000000000..d669dcb431
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2109DomainHandler.java
@@ -0,0 +1,120 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.util.Locale;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+import ch.boye.httpclientandroidlib.cookie.CookieAttributeHandler;
+import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
+import ch.boye.httpclientandroidlib.cookie.CookieRestrictionViolationException;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+import ch.boye.httpclientandroidlib.cookie.SetCookie;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ *
+ * @since 4.0
+ */
+@Immutable
+public class RFC2109DomainHandler implements CookieAttributeHandler {
+
+ public RFC2109DomainHandler() {
+ super();
+ }
+
+ public void parse(final SetCookie cookie, final String value)
+ throws MalformedCookieException {
+ Args.notNull(cookie, "Cookie");
+ if (value == null) {
+ throw new MalformedCookieException("Missing value for domain attribute");
+ }
+ if (value.trim().length() == 0) {
+ throw new MalformedCookieException("Blank value for domain attribute");
+ }
+ cookie.setDomain(value);
+ }
+
+ public void validate(final Cookie cookie, final CookieOrigin origin)
+ throws MalformedCookieException {
+ Args.notNull(cookie, "Cookie");
+ Args.notNull(origin, "Cookie origin");
+ String host = origin.getHost();
+ final String domain = cookie.getDomain();
+ if (domain == null) {
+ throw new CookieRestrictionViolationException("Cookie domain may not be null");
+ }
+ if (!domain.equals(host)) {
+ int dotIndex = domain.indexOf('.');
+ if (dotIndex == -1) {
+ throw new CookieRestrictionViolationException("Domain attribute \""
+ + domain
+ + "\" does not match the host \""
+ + host + "\"");
+ }
+ // domain must start with dot
+ if (!domain.startsWith(".")) {
+ throw new CookieRestrictionViolationException("Domain attribute \""
+ + domain
+ + "\" violates RFC 2109: domain must start with a dot");
+ }
+ // domain must have at least one embedded dot
+ dotIndex = domain.indexOf('.', 1);
+ if (dotIndex < 0 || dotIndex == domain.length() - 1) {
+ throw new CookieRestrictionViolationException("Domain attribute \""
+ + domain
+ + "\" violates RFC 2109: domain must contain an embedded dot");
+ }
+ host = host.toLowerCase(Locale.ENGLISH);
+ if (!host.endsWith(domain)) {
+ throw new CookieRestrictionViolationException(
+ "Illegal domain attribute \"" + domain
+ + "\". Domain of origin: \"" + host + "\"");
+ }
+ // host minus domain may not contain any dots
+ final String hostWithoutDomain = host.substring(0, host.length() - domain.length());
+ if (hostWithoutDomain.indexOf('.') != -1) {
+ throw new CookieRestrictionViolationException("Domain attribute \""
+ + domain
+ + "\" violates RFC 2109: host minus domain may not contain any dots");
+ }
+ }
+ }
+
+ public boolean match(final Cookie cookie, final CookieOrigin origin) {
+ Args.notNull(cookie, "Cookie");
+ Args.notNull(origin, "Cookie origin");
+ final String host = origin.getHost();
+ final String domain = cookie.getDomain();
+ if (domain == null) {
+ return false;
+ }
+ return host.equals(domain) || (domain.startsWith(".") && host.endsWith(domain));
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2109Spec.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2109Spec.java
new file mode 100644
index 0000000000..1292788a06
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2109Spec.java
@@ -0,0 +1,240 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.client.utils.DateUtils;
+import ch.boye.httpclientandroidlib.cookie.ClientCookie;
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
+import ch.boye.httpclientandroidlib.cookie.CookiePathComparator;
+import ch.boye.httpclientandroidlib.cookie.CookieRestrictionViolationException;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+import ch.boye.httpclientandroidlib.cookie.SM;
+import ch.boye.httpclientandroidlib.message.BufferedHeader;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * RFC 2109 compliant {@link ch.boye.httpclientandroidlib.cookie.CookieSpec} implementation.
+ * This is an older version of the official HTTP state management specification
+ * superseded by RFC 2965.
+ *
+ * @see RFC2965Spec
+ *
+ * @since 4.0
+ */
+@NotThreadSafe // superclass is @NotThreadSafe
+public class RFC2109Spec extends CookieSpecBase {
+
+ private final static CookiePathComparator PATH_COMPARATOR = new CookiePathComparator();
+
+ private final static String[] DATE_PATTERNS = {
+ DateUtils.PATTERN_RFC1123,
+ DateUtils.PATTERN_RFC1036,
+ DateUtils.PATTERN_ASCTIME
+ };
+
+ private final String[] datepatterns;
+ private final boolean oneHeader;
+
+ /** Default constructor */
+ public RFC2109Spec(final String[] datepatterns, final boolean oneHeader) {
+ super();
+ if (datepatterns != null) {
+ this.datepatterns = datepatterns.clone();
+ } else {
+ this.datepatterns = DATE_PATTERNS;
+ }
+ this.oneHeader = oneHeader;
+ registerAttribHandler(ClientCookie.VERSION_ATTR, new RFC2109VersionHandler());
+ registerAttribHandler(ClientCookie.PATH_ATTR, new BasicPathHandler());
+ registerAttribHandler(ClientCookie.DOMAIN_ATTR, new RFC2109DomainHandler());
+ registerAttribHandler(ClientCookie.MAX_AGE_ATTR, new BasicMaxAgeHandler());
+ registerAttribHandler(ClientCookie.SECURE_ATTR, new BasicSecureHandler());
+ registerAttribHandler(ClientCookie.COMMENT_ATTR, new BasicCommentHandler());
+ registerAttribHandler(ClientCookie.EXPIRES_ATTR, new BasicExpiresHandler(
+ this.datepatterns));
+ }
+
+ /** Default constructor */
+ public RFC2109Spec() {
+ this(null, false);
+ }
+
+ public List<Cookie> parse(final Header header, final CookieOrigin origin)
+ throws MalformedCookieException {
+ Args.notNull(header, "Header");
+ Args.notNull(origin, "Cookie origin");
+ if (!header.getName().equalsIgnoreCase(SM.SET_COOKIE)) {
+ throw new MalformedCookieException("Unrecognized cookie header '"
+ + header.toString() + "'");
+ }
+ final HeaderElement[] elems = header.getElements();
+ return parse(elems, origin);
+ }
+
+ @Override
+ public void validate(final Cookie cookie, final CookieOrigin origin)
+ throws MalformedCookieException {
+ Args.notNull(cookie, "Cookie");
+ final String name = cookie.getName();
+ if (name.indexOf(' ') != -1) {
+ throw new CookieRestrictionViolationException("Cookie name may not contain blanks");
+ }
+ if (name.startsWith("$")) {
+ throw new CookieRestrictionViolationException("Cookie name may not start with $");
+ }
+ super.validate(cookie, origin);
+ }
+
+ public List<Header> formatCookies(final List<Cookie> cookies) {
+ Args.notEmpty(cookies, "List of cookies");
+ List<Cookie> cookieList;
+ if (cookies.size() > 1) {
+ // Create a mutable copy and sort the copy.
+ cookieList = new ArrayList<Cookie>(cookies);
+ Collections.sort(cookieList, PATH_COMPARATOR);
+ } else {
+ cookieList = cookies;
+ }
+ if (this.oneHeader) {
+ return doFormatOneHeader(cookieList);
+ } else {
+ return doFormatManyHeaders(cookieList);
+ }
+ }
+
+ private List<Header> doFormatOneHeader(final List<Cookie> cookies) {
+ int version = Integer.MAX_VALUE;
+ // Pick the lowest common denominator
+ for (final Cookie cookie : cookies) {
+ if (cookie.getVersion() < version) {
+ version = cookie.getVersion();
+ }
+ }
+ final CharArrayBuffer buffer = new CharArrayBuffer(40 * cookies.size());
+ buffer.append(SM.COOKIE);
+ buffer.append(": ");
+ buffer.append("$Version=");
+ buffer.append(Integer.toString(version));
+ for (final Cookie cooky : cookies) {
+ buffer.append("; ");
+ final Cookie cookie = cooky;
+ formatCookieAsVer(buffer, cookie, version);
+ }
+ final List<Header> headers = new ArrayList<Header>(1);
+ headers.add(new BufferedHeader(buffer));
+ return headers;
+ }
+
+ private List<Header> doFormatManyHeaders(final List<Cookie> cookies) {
+ final List<Header> headers = new ArrayList<Header>(cookies.size());
+ for (final Cookie cookie : cookies) {
+ final int version = cookie.getVersion();
+ final CharArrayBuffer buffer = new CharArrayBuffer(40);
+ buffer.append("Cookie: ");
+ buffer.append("$Version=");
+ buffer.append(Integer.toString(version));
+ buffer.append("; ");
+ formatCookieAsVer(buffer, cookie, version);
+ headers.add(new BufferedHeader(buffer));
+ }
+ return headers;
+ }
+
+ /**
+ * Return a name/value string suitable for sending in a <tt>"Cookie"</tt>
+ * header as defined in RFC 2109 for backward compatibility with cookie
+ * version 0
+ * @param buffer The char array buffer to use for output
+ * @param name The cookie name
+ * @param value The cookie value
+ * @param version The cookie version
+ */
+ protected void formatParamAsVer(final CharArrayBuffer buffer,
+ final String name, final String value, final int version) {
+ buffer.append(name);
+ buffer.append("=");
+ if (value != null) {
+ if (version > 0) {
+ buffer.append('\"');
+ buffer.append(value);
+ buffer.append('\"');
+ } else {
+ buffer.append(value);
+ }
+ }
+ }
+
+ /**
+ * Return a string suitable for sending in a <tt>"Cookie"</tt> header
+ * as defined in RFC 2109 for backward compatibility with cookie version 0
+ * @param buffer The char array buffer to use for output
+ * @param cookie The {@link Cookie} to be formatted as string
+ * @param version The version to use.
+ */
+ protected void formatCookieAsVer(final CharArrayBuffer buffer,
+ final Cookie cookie, final int version) {
+ formatParamAsVer(buffer, cookie.getName(), cookie.getValue(), version);
+ if (cookie.getPath() != null) {
+ if (cookie instanceof ClientCookie
+ && ((ClientCookie) cookie).containsAttribute(ClientCookie.PATH_ATTR)) {
+ buffer.append("; ");
+ formatParamAsVer(buffer, "$Path", cookie.getPath(), version);
+ }
+ }
+ if (cookie.getDomain() != null) {
+ if (cookie instanceof ClientCookie
+ && ((ClientCookie) cookie).containsAttribute(ClientCookie.DOMAIN_ATTR)) {
+ buffer.append("; ");
+ formatParamAsVer(buffer, "$Domain", cookie.getDomain(), version);
+ }
+ }
+ }
+
+ public int getVersion() {
+ return 1;
+ }
+
+ public Header getVersionHeader() {
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return "rfc2109";
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2109SpecFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2109SpecFactory.java
new file mode 100644
index 0000000000..24802c351c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2109SpecFactory.java
@@ -0,0 +1,86 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.util.Collection;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.cookie.CookieSpec;
+import ch.boye.httpclientandroidlib.cookie.CookieSpecFactory;
+import ch.boye.httpclientandroidlib.cookie.CookieSpecProvider;
+import ch.boye.httpclientandroidlib.cookie.params.CookieSpecPNames;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * {@link CookieSpecProvider} implementation that creates and initializes
+ * {@link RFC2109Spec} instances.
+ *
+ * @since 4.0
+ */
+@Immutable
+@SuppressWarnings("deprecation")
+public class RFC2109SpecFactory implements CookieSpecFactory, CookieSpecProvider {
+
+ private final String[] datepatterns;
+ private final boolean oneHeader;
+
+ public RFC2109SpecFactory(final String[] datepatterns, final boolean oneHeader) {
+ super();
+ this.datepatterns = datepatterns;
+ this.oneHeader = oneHeader;
+ }
+
+ public RFC2109SpecFactory() {
+ this(null, false);
+ }
+
+ public CookieSpec newInstance(final HttpParams params) {
+ if (params != null) {
+
+ String[] patterns = null;
+ final Collection<?> param = (Collection<?>) params.getParameter(
+ CookieSpecPNames.DATE_PATTERNS);
+ if (param != null) {
+ patterns = new String[param.size()];
+ patterns = param.toArray(patterns);
+ }
+ final boolean singleHeader = params.getBooleanParameter(
+ CookieSpecPNames.SINGLE_COOKIE_HEADER, false);
+
+ return new RFC2109Spec(patterns, singleHeader);
+ } else {
+ return new RFC2109Spec();
+ }
+ }
+
+ public CookieSpec create(final HttpContext context) {
+ return new RFC2109Spec(this.datepatterns, this.oneHeader);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2109VersionHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2109VersionHandler.java
new file mode 100644
index 0000000000..a43fe9b22f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2109VersionHandler.java
@@ -0,0 +1,74 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
+import ch.boye.httpclientandroidlib.cookie.CookieRestrictionViolationException;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+import ch.boye.httpclientandroidlib.cookie.SetCookie;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ *
+ * @since 4.0
+ */
+@Immutable
+public class RFC2109VersionHandler extends AbstractCookieAttributeHandler {
+
+ public RFC2109VersionHandler() {
+ super();
+ }
+
+ public void parse(final SetCookie cookie, final String value)
+ throws MalformedCookieException {
+ Args.notNull(cookie, "Cookie");
+ if (value == null) {
+ throw new MalformedCookieException("Missing value for version attribute");
+ }
+ if (value.trim().length() == 0) {
+ throw new MalformedCookieException("Blank value for version attribute");
+ }
+ try {
+ cookie.setVersion(Integer.parseInt(value));
+ } catch (final NumberFormatException e) {
+ throw new MalformedCookieException("Invalid version: "
+ + e.getMessage());
+ }
+ }
+
+ @Override
+ public void validate(final Cookie cookie, final CookieOrigin origin)
+ throws MalformedCookieException {
+ Args.notNull(cookie, "Cookie");
+ if (cookie.getVersion() < 0) {
+ throw new CookieRestrictionViolationException("Cookie version may not be negative");
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965CommentUrlAttributeHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965CommentUrlAttributeHandler.java
new file mode 100644
index 0000000000..10f5f40ec8
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965CommentUrlAttributeHandler.java
@@ -0,0 +1,66 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+import ch.boye.httpclientandroidlib.cookie.CookieAttributeHandler;
+import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+import ch.boye.httpclientandroidlib.cookie.SetCookie;
+import ch.boye.httpclientandroidlib.cookie.SetCookie2;
+
+/**
+ * <tt>"CommentURL"</tt> cookie attribute handler for RFC 2965 cookie spec.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class RFC2965CommentUrlAttributeHandler implements CookieAttributeHandler {
+
+ public RFC2965CommentUrlAttributeHandler() {
+ super();
+ }
+
+ public void parse(final SetCookie cookie, final String commenturl)
+ throws MalformedCookieException {
+ if (cookie instanceof SetCookie2) {
+ final SetCookie2 cookie2 = (SetCookie2) cookie;
+ cookie2.setCommentURL(commenturl);
+ }
+ }
+
+ public void validate(final Cookie cookie, final CookieOrigin origin)
+ throws MalformedCookieException {
+ }
+
+ public boolean match(final Cookie cookie, final CookieOrigin origin) {
+ return true;
+ }
+
+ }
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965DiscardAttributeHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965DiscardAttributeHandler.java
new file mode 100644
index 0000000000..c4bfca460a
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965DiscardAttributeHandler.java
@@ -0,0 +1,66 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+import ch.boye.httpclientandroidlib.cookie.CookieAttributeHandler;
+import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+import ch.boye.httpclientandroidlib.cookie.SetCookie;
+import ch.boye.httpclientandroidlib.cookie.SetCookie2;
+
+/**
+ * <tt>"Discard"</tt> cookie attribute handler for RFC 2965 cookie spec.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class RFC2965DiscardAttributeHandler implements CookieAttributeHandler {
+
+ public RFC2965DiscardAttributeHandler() {
+ super();
+ }
+
+ public void parse(final SetCookie cookie, final String commenturl)
+ throws MalformedCookieException {
+ if (cookie instanceof SetCookie2) {
+ final SetCookie2 cookie2 = (SetCookie2) cookie;
+ cookie2.setDiscard(true);
+ }
+ }
+
+ public void validate(final Cookie cookie, final CookieOrigin origin)
+ throws MalformedCookieException {
+ }
+
+ public boolean match(final Cookie cookie, final CookieOrigin origin) {
+ return true;
+ }
+
+ }
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965DomainAttributeHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965DomainAttributeHandler.java
new file mode 100644
index 0000000000..62c947c55f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965DomainAttributeHandler.java
@@ -0,0 +1,185 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.util.Locale;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.cookie.ClientCookie;
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+import ch.boye.httpclientandroidlib.cookie.CookieAttributeHandler;
+import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
+import ch.boye.httpclientandroidlib.cookie.CookieRestrictionViolationException;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+import ch.boye.httpclientandroidlib.cookie.SetCookie;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * <tt>"Domain"</tt> cookie attribute handler for RFC 2965 cookie spec.
+ *
+ *
+ * @since 3.1
+ */
+@Immutable
+public class RFC2965DomainAttributeHandler implements CookieAttributeHandler {
+
+ public RFC2965DomainAttributeHandler() {
+ super();
+ }
+
+ /**
+ * Parse cookie domain attribute.
+ */
+ public void parse(
+ final SetCookie cookie, final String domain) throws MalformedCookieException {
+ Args.notNull(cookie, "Cookie");
+ if (domain == null) {
+ throw new MalformedCookieException(
+ "Missing value for domain attribute");
+ }
+ if (domain.trim().length() == 0) {
+ throw new MalformedCookieException(
+ "Blank value for domain attribute");
+ }
+ String s = domain;
+ s = s.toLowerCase(Locale.ENGLISH);
+ if (!domain.startsWith(".")) {
+ // Per RFC 2965 section 3.2.2
+ // "... If an explicitly specified value does not start with
+ // a dot, the user agent supplies a leading dot ..."
+ // That effectively implies that the domain attribute
+ // MAY NOT be an IP address of a host name
+ s = '.' + s;
+ }
+ cookie.setDomain(s);
+ }
+
+ /**
+ * Performs domain-match as defined by the RFC2965.
+ * <p>
+ * Host A's name domain-matches host B's if
+ * <ol>
+ * <ul>their host name strings string-compare equal; or</ul>
+ * <ul>A is a HDN string and has the form NB, where N is a non-empty
+ * name string, B has the form .B', and B' is a HDN string. (So,
+ * x.y.com domain-matches .Y.com but not Y.com.)</ul>
+ * </ol>
+ *
+ * @param host host name where cookie is received from or being sent to.
+ * @param domain The cookie domain attribute.
+ * @return true if the specified host matches the given domain.
+ */
+ public boolean domainMatch(final String host, final String domain) {
+ final boolean match = host.equals(domain)
+ || (domain.startsWith(".") && host.endsWith(domain));
+
+ return match;
+ }
+
+ /**
+ * Validate cookie domain attribute.
+ */
+ public void validate(final Cookie cookie, final CookieOrigin origin)
+ throws MalformedCookieException {
+ Args.notNull(cookie, "Cookie");
+ Args.notNull(origin, "Cookie origin");
+ final String host = origin.getHost().toLowerCase(Locale.ENGLISH);
+ if (cookie.getDomain() == null) {
+ throw new CookieRestrictionViolationException("Invalid cookie state: " +
+ "domain not specified");
+ }
+ final String cookieDomain = cookie.getDomain().toLowerCase(Locale.ENGLISH);
+
+ if (cookie instanceof ClientCookie
+ && ((ClientCookie) cookie).containsAttribute(ClientCookie.DOMAIN_ATTR)) {
+ // Domain attribute must start with a dot
+ if (!cookieDomain.startsWith(".")) {
+ throw new CookieRestrictionViolationException("Domain attribute \"" +
+ cookie.getDomain() + "\" violates RFC 2109: domain must start with a dot");
+ }
+
+ // Domain attribute must contain at least one embedded dot,
+ // or the value must be equal to .local.
+ final int dotIndex = cookieDomain.indexOf('.', 1);
+ if (((dotIndex < 0) || (dotIndex == cookieDomain.length() - 1))
+ && (!cookieDomain.equals(".local"))) {
+ throw new CookieRestrictionViolationException(
+ "Domain attribute \"" + cookie.getDomain()
+ + "\" violates RFC 2965: the value contains no embedded dots "
+ + "and the value is not .local");
+ }
+
+ // The effective host name must domain-match domain attribute.
+ if (!domainMatch(host, cookieDomain)) {
+ throw new CookieRestrictionViolationException(
+ "Domain attribute \"" + cookie.getDomain()
+ + "\" violates RFC 2965: effective host name does not "
+ + "domain-match domain attribute.");
+ }
+
+ // effective host name minus domain must not contain any dots
+ final String effectiveHostWithoutDomain = host.substring(
+ 0, host.length() - cookieDomain.length());
+ if (effectiveHostWithoutDomain.indexOf('.') != -1) {
+ throw new CookieRestrictionViolationException("Domain attribute \""
+ + cookie.getDomain() + "\" violates RFC 2965: "
+ + "effective host minus domain may not contain any dots");
+ }
+ } else {
+ // Domain was not specified in header. In this case, domain must
+ // string match request host (case-insensitive).
+ if (!cookie.getDomain().equals(host)) {
+ throw new CookieRestrictionViolationException("Illegal domain attribute: \""
+ + cookie.getDomain() + "\"."
+ + "Domain of origin: \""
+ + host + "\"");
+ }
+ }
+ }
+
+ /**
+ * Match cookie domain attribute.
+ */
+ public boolean match(final Cookie cookie, final CookieOrigin origin) {
+ Args.notNull(cookie, "Cookie");
+ Args.notNull(origin, "Cookie origin");
+ final String host = origin.getHost().toLowerCase(Locale.ENGLISH);
+ final String cookieDomain = cookie.getDomain();
+
+ // The effective host name MUST domain-match the Domain
+ // attribute of the cookie.
+ if (!domainMatch(host, cookieDomain)) {
+ return false;
+ }
+ // effective host name minus domain must not contain any dots
+ final String effectiveHostWithoutDomain = host.substring(
+ 0, host.length() - cookieDomain.length());
+ return effectiveHostWithoutDomain.indexOf('.') == -1;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965PortAttributeHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965PortAttributeHandler.java
new file mode 100644
index 0000000000..4835d8eb5f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965PortAttributeHandler.java
@@ -0,0 +1,160 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.util.StringTokenizer;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.cookie.ClientCookie;
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+import ch.boye.httpclientandroidlib.cookie.CookieAttributeHandler;
+import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
+import ch.boye.httpclientandroidlib.cookie.CookieRestrictionViolationException;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+import ch.boye.httpclientandroidlib.cookie.SetCookie;
+import ch.boye.httpclientandroidlib.cookie.SetCookie2;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * <tt>"Port"</tt> cookie attribute handler for RFC 2965 cookie spec.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class RFC2965PortAttributeHandler implements CookieAttributeHandler {
+
+ public RFC2965PortAttributeHandler() {
+ super();
+ }
+
+ /**
+ * Parses the given Port attribute value (e.g. "8000,8001,8002")
+ * into an array of ports.
+ *
+ * @param portValue port attribute value
+ * @return parsed array of ports
+ * @throws MalformedCookieException if there is a problem in
+ * parsing due to invalid portValue.
+ */
+ private static int[] parsePortAttribute(final String portValue)
+ throws MalformedCookieException {
+ final StringTokenizer st = new StringTokenizer(portValue, ",");
+ final int[] ports = new int[st.countTokens()];
+ try {
+ int i = 0;
+ while(st.hasMoreTokens()) {
+ ports[i] = Integer.parseInt(st.nextToken().trim());
+ if (ports[i] < 0) {
+ throw new MalformedCookieException ("Invalid Port attribute.");
+ }
+ ++i;
+ }
+ } catch (final NumberFormatException e) {
+ throw new MalformedCookieException ("Invalid Port "
+ + "attribute: " + e.getMessage());
+ }
+ return ports;
+ }
+
+ /**
+ * Returns <tt>true</tt> if the given port exists in the given
+ * ports list.
+ *
+ * @param port port of host where cookie was received from or being sent to.
+ * @param ports port list
+ * @return true returns <tt>true</tt> if the given port exists in
+ * the given ports list; <tt>false</tt> otherwise.
+ */
+ private static boolean portMatch(final int port, final int[] ports) {
+ boolean portInList = false;
+ for (final int port2 : ports) {
+ if (port == port2) {
+ portInList = true;
+ break;
+ }
+ }
+ return portInList;
+ }
+
+ /**
+ * Parse cookie port attribute.
+ */
+ public void parse(final SetCookie cookie, final String portValue)
+ throws MalformedCookieException {
+ Args.notNull(cookie, "Cookie");
+ if (cookie instanceof SetCookie2) {
+ final SetCookie2 cookie2 = (SetCookie2) cookie;
+ if (portValue != null && portValue.trim().length() > 0) {
+ final int[] ports = parsePortAttribute(portValue);
+ cookie2.setPorts(ports);
+ }
+ }
+ }
+
+ /**
+ * Validate cookie port attribute. If the Port attribute was specified
+ * in header, the request port must be in cookie's port list.
+ */
+ public void validate(final Cookie cookie, final CookieOrigin origin)
+ throws MalformedCookieException {
+ Args.notNull(cookie, "Cookie");
+ Args.notNull(origin, "Cookie origin");
+ final int port = origin.getPort();
+ if (cookie instanceof ClientCookie
+ && ((ClientCookie) cookie).containsAttribute(ClientCookie.PORT_ATTR)) {
+ if (!portMatch(port, cookie.getPorts())) {
+ throw new CookieRestrictionViolationException(
+ "Port attribute violates RFC 2965: "
+ + "Request port not found in cookie's port list.");
+ }
+ }
+ }
+
+ /**
+ * Match cookie port attribute. If the Port attribute is not specified
+ * in header, the cookie can be sent to any port. Otherwise, the request port
+ * must be in the cookie's port list.
+ */
+ public boolean match(final Cookie cookie, final CookieOrigin origin) {
+ Args.notNull(cookie, "Cookie");
+ Args.notNull(origin, "Cookie origin");
+ final int port = origin.getPort();
+ if (cookie instanceof ClientCookie
+ && ((ClientCookie) cookie).containsAttribute(ClientCookie.PORT_ATTR)) {
+ if (cookie.getPorts() == null) {
+ // Invalid cookie state: port not specified
+ return false;
+ }
+ if (!portMatch(port, cookie.getPorts())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965Spec.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965Spec.java
new file mode 100644
index 0000000000..1c0dbc4cbf
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965Spec.java
@@ -0,0 +1,239 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.cookie.ClientCookie;
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+import ch.boye.httpclientandroidlib.cookie.CookieAttributeHandler;
+import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+import ch.boye.httpclientandroidlib.cookie.SM;
+import ch.boye.httpclientandroidlib.message.BufferedHeader;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * RFC 2965 compliant {@link ch.boye.httpclientandroidlib.cookie.CookieSpec} implementation.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe // superclass is @NotThreadSafe
+public class RFC2965Spec extends RFC2109Spec {
+
+ /**
+ * Default constructor
+ *
+ */
+ public RFC2965Spec() {
+ this(null, false);
+ }
+
+ public RFC2965Spec(final String[] datepatterns, final boolean oneHeader) {
+ super(datepatterns, oneHeader);
+ registerAttribHandler(ClientCookie.DOMAIN_ATTR, new RFC2965DomainAttributeHandler());
+ registerAttribHandler(ClientCookie.PORT_ATTR, new RFC2965PortAttributeHandler());
+ registerAttribHandler(ClientCookie.COMMENTURL_ATTR, new RFC2965CommentUrlAttributeHandler());
+ registerAttribHandler(ClientCookie.DISCARD_ATTR, new RFC2965DiscardAttributeHandler());
+ registerAttribHandler(ClientCookie.VERSION_ATTR, new RFC2965VersionAttributeHandler());
+ }
+
+ @Override
+ public List<Cookie> parse(
+ final Header header,
+ final CookieOrigin origin) throws MalformedCookieException {
+ Args.notNull(header, "Header");
+ Args.notNull(origin, "Cookie origin");
+ if (!header.getName().equalsIgnoreCase(SM.SET_COOKIE2)) {
+ throw new MalformedCookieException("Unrecognized cookie header '"
+ + header.toString() + "'");
+ }
+ final HeaderElement[] elems = header.getElements();
+ return createCookies(elems, adjustEffectiveHost(origin));
+ }
+
+ @Override
+ protected List<Cookie> parse(
+ final HeaderElement[] elems,
+ final CookieOrigin origin) throws MalformedCookieException {
+ return createCookies(elems, adjustEffectiveHost(origin));
+ }
+
+ private List<Cookie> createCookies(
+ final HeaderElement[] elems,
+ final CookieOrigin origin) throws MalformedCookieException {
+ final List<Cookie> cookies = new ArrayList<Cookie>(elems.length);
+ for (final HeaderElement headerelement : elems) {
+ final String name = headerelement.getName();
+ final String value = headerelement.getValue();
+ if (name == null || name.length() == 0) {
+ throw new MalformedCookieException("Cookie name may not be empty");
+ }
+
+ final BasicClientCookie2 cookie = new BasicClientCookie2(name, value);
+ cookie.setPath(getDefaultPath(origin));
+ cookie.setDomain(getDefaultDomain(origin));
+ cookie.setPorts(new int [] { origin.getPort() });
+ // cycle through the parameters
+ final NameValuePair[] attribs = headerelement.getParameters();
+
+ // Eliminate duplicate attributes. The first occurrence takes precedence
+ // See RFC2965: 3.2 Origin Server Role
+ final Map<String, NameValuePair> attribmap =
+ new HashMap<String, NameValuePair>(attribs.length);
+ for (int j = attribs.length - 1; j >= 0; j--) {
+ final NameValuePair param = attribs[j];
+ attribmap.put(param.getName().toLowerCase(Locale.ENGLISH), param);
+ }
+ for (final Map.Entry<String, NameValuePair> entry : attribmap.entrySet()) {
+ final NameValuePair attrib = entry.getValue();
+ final String s = attrib.getName().toLowerCase(Locale.ENGLISH);
+
+ cookie.setAttribute(s, attrib.getValue());
+
+ final CookieAttributeHandler handler = findAttribHandler(s);
+ if (handler != null) {
+ handler.parse(cookie, attrib.getValue());
+ }
+ }
+ cookies.add(cookie);
+ }
+ return cookies;
+ }
+
+ @Override
+ public void validate(
+ final Cookie cookie, final CookieOrigin origin) throws MalformedCookieException {
+ Args.notNull(cookie, "Cookie");
+ Args.notNull(origin, "Cookie origin");
+ super.validate(cookie, adjustEffectiveHost(origin));
+ }
+
+ @Override
+ public boolean match(final Cookie cookie, final CookieOrigin origin) {
+ Args.notNull(cookie, "Cookie");
+ Args.notNull(origin, "Cookie origin");
+ return super.match(cookie, adjustEffectiveHost(origin));
+ }
+
+ /**
+ * Adds valid Port attribute value, e.g. "8000,8001,8002"
+ */
+ @Override
+ protected void formatCookieAsVer(final CharArrayBuffer buffer,
+ final Cookie cookie, final int version) {
+ super.formatCookieAsVer(buffer, cookie, version);
+ // format port attribute
+ if (cookie instanceof ClientCookie) {
+ // Test if the port attribute as set by the origin server is not blank
+ final String s = ((ClientCookie) cookie).getAttribute(ClientCookie.PORT_ATTR);
+ if (s != null) {
+ buffer.append("; $Port");
+ buffer.append("=\"");
+ if (s.trim().length() > 0) {
+ final int[] ports = cookie.getPorts();
+ if (ports != null) {
+ final int len = ports.length;
+ for (int i = 0; i < len; i++) {
+ if (i > 0) {
+ buffer.append(",");
+ }
+ buffer.append(Integer.toString(ports[i]));
+ }
+ }
+ }
+ buffer.append("\"");
+ }
+ }
+ }
+
+ /**
+ * Set 'effective host name' as defined in RFC 2965.
+ * <p>
+ * If a host name contains no dots, the effective host name is
+ * that name with the string .local appended to it. Otherwise
+ * the effective host name is the same as the host name. Note
+ * that all effective host names contain at least one dot.
+ *
+ * @param origin origin where cookie is received from or being sent to.
+ */
+ private static CookieOrigin adjustEffectiveHost(final CookieOrigin origin) {
+ String host = origin.getHost();
+
+ // Test if the host name appears to be a fully qualified DNS name,
+ // IPv4 address or IPv6 address
+ boolean isLocalHost = true;
+ for (int i = 0; i < host.length(); i++) {
+ final char ch = host.charAt(i);
+ if (ch == '.' || ch == ':') {
+ isLocalHost = false;
+ break;
+ }
+ }
+ if (isLocalHost) {
+ host += ".local";
+ return new CookieOrigin(
+ host,
+ origin.getPort(),
+ origin.getPath(),
+ origin.isSecure());
+ } else {
+ return origin;
+ }
+ }
+
+ @Override
+ public int getVersion() {
+ return 1;
+ }
+
+ @Override
+ public Header getVersionHeader() {
+ final CharArrayBuffer buffer = new CharArrayBuffer(40);
+ buffer.append(SM.COOKIE2);
+ buffer.append(": ");
+ buffer.append("$Version=");
+ buffer.append(Integer.toString(getVersion()));
+ return new BufferedHeader(buffer);
+ }
+
+ @Override
+ public String toString() {
+ return "rfc2965";
+ }
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965SpecFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965SpecFactory.java
new file mode 100644
index 0000000000..83b60407cb
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965SpecFactory.java
@@ -0,0 +1,86 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import java.util.Collection;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.cookie.CookieSpec;
+import ch.boye.httpclientandroidlib.cookie.CookieSpecFactory;
+import ch.boye.httpclientandroidlib.cookie.CookieSpecProvider;
+import ch.boye.httpclientandroidlib.cookie.params.CookieSpecPNames;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.HttpContext;
+
+/**
+ * {@link CookieSpecProvider} implementation that creates and initializes
+ * {@link RFC2965Spec} instances.
+ *
+ * @since 4.0
+ */
+@Immutable
+@SuppressWarnings("deprecation")
+public class RFC2965SpecFactory implements CookieSpecFactory, CookieSpecProvider {
+
+ private final String[] datepatterns;
+ private final boolean oneHeader;
+
+ public RFC2965SpecFactory(final String[] datepatterns, final boolean oneHeader) {
+ super();
+ this.datepatterns = datepatterns;
+ this.oneHeader = oneHeader;
+ }
+
+ public RFC2965SpecFactory() {
+ this(null, false);
+ }
+
+ public CookieSpec newInstance(final HttpParams params) {
+ if (params != null) {
+
+ String[] patterns = null;
+ final Collection<?> param = (Collection<?>) params.getParameter(
+ CookieSpecPNames.DATE_PATTERNS);
+ if (param != null) {
+ patterns = new String[param.size()];
+ patterns = param.toArray(patterns);
+ }
+ final boolean singleHeader = params.getBooleanParameter(
+ CookieSpecPNames.SINGLE_COOKIE_HEADER, false);
+
+ return new RFC2965Spec(patterns, singleHeader);
+ } else {
+ return new RFC2965Spec();
+ }
+ }
+
+ public CookieSpec create(final HttpContext context) {
+ return new RFC2965Spec(this.datepatterns, this.oneHeader);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965VersionAttributeHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965VersionAttributeHandler.java
new file mode 100644
index 0000000000..26ae4faa60
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/RFC2965VersionAttributeHandler.java
@@ -0,0 +1,94 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.cookie;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.cookie.ClientCookie;
+import ch.boye.httpclientandroidlib.cookie.Cookie;
+import ch.boye.httpclientandroidlib.cookie.CookieAttributeHandler;
+import ch.boye.httpclientandroidlib.cookie.CookieOrigin;
+import ch.boye.httpclientandroidlib.cookie.CookieRestrictionViolationException;
+import ch.boye.httpclientandroidlib.cookie.MalformedCookieException;
+import ch.boye.httpclientandroidlib.cookie.SetCookie;
+import ch.boye.httpclientandroidlib.cookie.SetCookie2;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * <tt>"Version"</tt> cookie attribute handler for RFC 2965 cookie spec.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class RFC2965VersionAttributeHandler implements CookieAttributeHandler {
+
+ public RFC2965VersionAttributeHandler() {
+ super();
+ }
+
+ /**
+ * Parse cookie version attribute.
+ */
+ public void parse(final SetCookie cookie, final String value)
+ throws MalformedCookieException {
+ Args.notNull(cookie, "Cookie");
+ if (value == null) {
+ throw new MalformedCookieException(
+ "Missing value for version attribute");
+ }
+ int version = -1;
+ try {
+ version = Integer.parseInt(value);
+ } catch (final NumberFormatException e) {
+ version = -1;
+ }
+ if (version < 0) {
+ throw new MalformedCookieException("Invalid cookie version.");
+ }
+ cookie.setVersion(version);
+ }
+
+ /**
+ * validate cookie version attribute. Version attribute is REQUIRED.
+ */
+ public void validate(final Cookie cookie, final CookieOrigin origin)
+ throws MalformedCookieException {
+ Args.notNull(cookie, "Cookie");
+ if (cookie instanceof SetCookie2) {
+ if (cookie instanceof ClientCookie
+ && !((ClientCookie) cookie).containsAttribute(ClientCookie.VERSION_ATTR)) {
+ throw new CookieRestrictionViolationException(
+ "Violates RFC 2965. Version attribute is required.");
+ }
+ }
+ }
+
+ public boolean match(final Cookie cookie, final CookieOrigin origin) {
+ return true;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/package-info.java
new file mode 100644
index 0000000000..1950e7263d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/cookie/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Default implementations of standard and common HTTP state
+ * management policies.
+ */
+package ch.boye.httpclientandroidlib.impl.cookie;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/DisallowIdentityContentLengthStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/DisallowIdentityContentLengthStrategy.java
new file mode 100644
index 0000000000..93556d22b7
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/DisallowIdentityContentLengthStrategy.java
@@ -0,0 +1,63 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.entity;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpMessage;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.entity.ContentLengthStrategy;
+
+/**
+ * Decorator for {@link ContentLengthStrategy} implementations that disallows the use of
+ * identity transfer encoding.
+ *
+ * @since 4.2
+ */
+@Immutable
+public class DisallowIdentityContentLengthStrategy implements ContentLengthStrategy {
+
+ public static final DisallowIdentityContentLengthStrategy INSTANCE =
+ new DisallowIdentityContentLengthStrategy(new LaxContentLengthStrategy(0));
+
+ private final ContentLengthStrategy contentLengthStrategy;
+
+ public DisallowIdentityContentLengthStrategy(final ContentLengthStrategy contentLengthStrategy) {
+ super();
+ this.contentLengthStrategy = contentLengthStrategy;
+ }
+
+ public long determineLength(final HttpMessage message) throws HttpException {
+ final long result = this.contentLengthStrategy.determineLength(message);
+ if (result == ContentLengthStrategy.IDENTITY) {
+ throw new ProtocolException("Identity transfer encoding cannot be used");
+ }
+ return result;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/EntityDeserializer.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/EntityDeserializer.java
new file mode 100644
index 0000000000..5eba3638cf
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/EntityDeserializer.java
@@ -0,0 +1,143 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.entity;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpMessage;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.entity.BasicHttpEntity;
+import ch.boye.httpclientandroidlib.entity.ContentLengthStrategy;
+import ch.boye.httpclientandroidlib.impl.io.ChunkedInputStream;
+import ch.boye.httpclientandroidlib.impl.io.ContentLengthInputStream;
+import ch.boye.httpclientandroidlib.impl.io.IdentityInputStream;
+import ch.boye.httpclientandroidlib.io.SessionInputBuffer;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * HTTP entity deserializer.
+ * <p>
+ * This entity deserializer supports "chunked" and "identitiy" transfer-coding
+ * and content length delimited content.
+ * <p>
+ * This class relies on a specific implementation of
+ * {@link ContentLengthStrategy} to determine the content length or transfer
+ * encoding of the entity.
+ * <p>
+ * This class generates an instance of {@link HttpEntity} based on
+ * properties of the message. The content of the entity will be decoded
+ * transparently for the consumer.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.impl.BHttpConnectionBase}
+ */
+@Immutable // assuming injected dependencies are immutable
+@Deprecated
+public class EntityDeserializer {
+
+ private final ContentLengthStrategy lenStrategy;
+
+ public EntityDeserializer(final ContentLengthStrategy lenStrategy) {
+ super();
+ this.lenStrategy = Args.notNull(lenStrategy, "Content length strategy");
+ }
+
+ /**
+ * Creates a {@link BasicHttpEntity} based on properties of the given
+ * message. The content of the entity is created by wrapping
+ * {@link SessionInputBuffer} with a content decoder depending on the
+ * transfer mechanism used by the message.
+ * <p>
+ * This method is called by the public
+ * {@link #deserialize(SessionInputBuffer, HttpMessage)}.
+ *
+ * @param inbuffer the session input buffer.
+ * @param message the message.
+ * @return HTTP entity.
+ * @throws HttpException in case of HTTP protocol violation.
+ * @throws IOException in case of an I/O error.
+ */
+ protected BasicHttpEntity doDeserialize(
+ final SessionInputBuffer inbuffer,
+ final HttpMessage message) throws HttpException, IOException {
+ final BasicHttpEntity entity = new BasicHttpEntity();
+
+ final long len = this.lenStrategy.determineLength(message);
+ if (len == ContentLengthStrategy.CHUNKED) {
+ entity.setChunked(true);
+ entity.setContentLength(-1);
+ entity.setContent(new ChunkedInputStream(inbuffer));
+ } else if (len == ContentLengthStrategy.IDENTITY) {
+ entity.setChunked(false);
+ entity.setContentLength(-1);
+ entity.setContent(new IdentityInputStream(inbuffer));
+ } else {
+ entity.setChunked(false);
+ entity.setContentLength(len);
+ entity.setContent(new ContentLengthInputStream(inbuffer, len));
+ }
+
+ final Header contentTypeHeader = message.getFirstHeader(HTTP.CONTENT_TYPE);
+ if (contentTypeHeader != null) {
+ entity.setContentType(contentTypeHeader);
+ }
+ final Header contentEncodingHeader = message.getFirstHeader(HTTP.CONTENT_ENCODING);
+ if (contentEncodingHeader != null) {
+ entity.setContentEncoding(contentEncodingHeader);
+ }
+ return entity;
+ }
+
+ /**
+ * Creates an {@link HttpEntity} based on properties of the given message.
+ * The content of the entity is created by wrapping
+ * {@link SessionInputBuffer} with a content decoder depending on the
+ * transfer mechanism used by the message.
+ * <p>
+ * The content of the entity is NOT retrieved by this method.
+ *
+ * @param inbuffer the session input buffer.
+ * @param message the message.
+ * @return HTTP entity.
+ * @throws HttpException in case of HTTP protocol violation.
+ * @throws IOException in case of an I/O error.
+ */
+ public HttpEntity deserialize(
+ final SessionInputBuffer inbuffer,
+ final HttpMessage message) throws HttpException, IOException {
+ Args.notNull(inbuffer, "Session input buffer");
+ Args.notNull(message, "HTTP message");
+ return doDeserialize(inbuffer, message);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/EntitySerializer.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/EntitySerializer.java
new file mode 100644
index 0000000000..f25115ceff
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/EntitySerializer.java
@@ -0,0 +1,121 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.entity;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpMessage;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.entity.ContentLengthStrategy;
+import ch.boye.httpclientandroidlib.impl.io.ChunkedOutputStream;
+import ch.boye.httpclientandroidlib.impl.io.ContentLengthOutputStream;
+import ch.boye.httpclientandroidlib.impl.io.IdentityOutputStream;
+import ch.boye.httpclientandroidlib.io.SessionOutputBuffer;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * HTTP entity serializer.
+ * <p>
+ * This entity serializer currently supports "chunked" and "identitiy"
+ * transfer-coding and content length delimited content.
+ * <p>
+ * This class relies on a specific implementation of
+ * {@link ContentLengthStrategy} to determine the content length or transfer
+ * encoding of the entity.
+ * <p>
+ * This class writes out the content of {@link HttpEntity} to the data stream
+ * using a transfer coding based on properties on the HTTP message.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.impl.BHttpConnectionBase}
+ */
+@Immutable // assuming injected dependencies are immutable
+@Deprecated
+public class EntitySerializer {
+
+ private final ContentLengthStrategy lenStrategy;
+
+ public EntitySerializer(final ContentLengthStrategy lenStrategy) {
+ super();
+ this.lenStrategy = Args.notNull(lenStrategy, "Content length strategy");
+ }
+
+ /**
+ * Creates a transfer codec based on properties of the given HTTP message
+ * and returns {@link OutputStream} instance that transparently encodes
+ * output data as it is being written out to the output stream.
+ * <p>
+ * This method is called by the public
+ * {@link #serialize(SessionOutputBuffer, HttpMessage, HttpEntity)}.
+ *
+ * @param outbuffer the session output buffer.
+ * @param message the HTTP message.
+ * @return output stream.
+ * @throws HttpException in case of HTTP protocol violation.
+ * @throws IOException in case of an I/O error.
+ */
+ protected OutputStream doSerialize(
+ final SessionOutputBuffer outbuffer,
+ final HttpMessage message) throws HttpException, IOException {
+ final long len = this.lenStrategy.determineLength(message);
+ if (len == ContentLengthStrategy.CHUNKED) {
+ return new ChunkedOutputStream(outbuffer);
+ } else if (len == ContentLengthStrategy.IDENTITY) {
+ return new IdentityOutputStream(outbuffer);
+ } else {
+ return new ContentLengthOutputStream(outbuffer, len);
+ }
+ }
+
+ /**
+ * Writes out the content of the given HTTP entity to the session output
+ * buffer based on properties of the given HTTP message.
+ *
+ * @param outbuffer the output session buffer.
+ * @param message the HTTP message.
+ * @param entity the HTTP entity to be written out.
+ * @throws HttpException in case of HTTP protocol violation.
+ * @throws IOException in case of an I/O error.
+ */
+ public void serialize(
+ final SessionOutputBuffer outbuffer,
+ final HttpMessage message,
+ final HttpEntity entity) throws HttpException, IOException {
+ Args.notNull(outbuffer, "Session output buffer");
+ Args.notNull(message, "HTTP message");
+ Args.notNull(entity, "HTTP entity");
+ final OutputStream outstream = doSerialize(outbuffer, message);
+ entity.writeTo(outstream);
+ outstream.close();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/LaxContentLengthStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/LaxContentLengthStrategy.java
new file mode 100644
index 0000000000..0f0dbaf158
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/LaxContentLengthStrategy.java
@@ -0,0 +1,126 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.entity;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpMessage;
+import ch.boye.httpclientandroidlib.ParseException;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.entity.ContentLengthStrategy;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * The lax implementation of the content length strategy. This class will ignore
+ * unrecognized transfer encodings and malformed <code>Content-Length</code>
+ * header values.
+ * <p/>
+ * This class recognizes "chunked" and "identitiy" transfer-coding only.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class LaxContentLengthStrategy implements ContentLengthStrategy {
+
+ public static final LaxContentLengthStrategy INSTANCE = new LaxContentLengthStrategy();
+
+ private final int implicitLen;
+
+ /**
+ * Creates <tt>LaxContentLengthStrategy</tt> instance with the given length used per default
+ * when content length is not explicitly specified in the message.
+ *
+ * @param implicitLen implicit content length.
+ *
+ * @since 4.2
+ */
+ public LaxContentLengthStrategy(final int implicitLen) {
+ super();
+ this.implicitLen = implicitLen;
+ }
+
+ /**
+ * Creates <tt>LaxContentLengthStrategy</tt> instance. {@link ContentLengthStrategy#IDENTITY}
+ * is used per default when content length is not explicitly specified in the message.
+ */
+ public LaxContentLengthStrategy() {
+ this(IDENTITY);
+ }
+
+ public long determineLength(final HttpMessage message) throws HttpException {
+ Args.notNull(message, "HTTP message");
+
+ final Header transferEncodingHeader = message.getFirstHeader(HTTP.TRANSFER_ENCODING);
+ // We use Transfer-Encoding if present and ignore Content-Length.
+ // RFC2616, 4.4 item number 3
+ if (transferEncodingHeader != null) {
+ final HeaderElement[] encodings;
+ try {
+ encodings = transferEncodingHeader.getElements();
+ } catch (final ParseException px) {
+ throw new ProtocolException
+ ("Invalid Transfer-Encoding header value: " +
+ transferEncodingHeader, px);
+ }
+ // The chunked encoding must be the last one applied RFC2616, 14.41
+ final int len = encodings.length;
+ if (HTTP.IDENTITY_CODING.equalsIgnoreCase(transferEncodingHeader.getValue())) {
+ return IDENTITY;
+ } else if ((len > 0) && (HTTP.CHUNK_CODING.equalsIgnoreCase(
+ encodings[len - 1].getName()))) {
+ return CHUNKED;
+ } else {
+ return IDENTITY;
+ }
+ }
+ final Header contentLengthHeader = message.getFirstHeader(HTTP.CONTENT_LEN);
+ if (contentLengthHeader != null) {
+ long contentlen = -1;
+ final Header[] headers = message.getHeaders(HTTP.CONTENT_LEN);
+ for (int i = headers.length - 1; i >= 0; i--) {
+ final Header header = headers[i];
+ try {
+ contentlen = Long.parseLong(header.getValue());
+ break;
+ } catch (final NumberFormatException ignore) {
+ }
+ // See if we can have better luck with another header, if present
+ }
+ if (contentlen >= 0) {
+ return contentlen;
+ } else {
+ return IDENTITY;
+ }
+ }
+ return this.implicitLen;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/StrictContentLengthStrategy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/StrictContentLengthStrategy.java
new file mode 100644
index 0000000000..1693c31116
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/StrictContentLengthStrategy.java
@@ -0,0 +1,116 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.entity;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpMessage;
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.entity.ContentLengthStrategy;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * The strict implementation of the content length strategy. This class
+ * will throw {@link ProtocolException} if it encounters an unsupported
+ * transfer encoding or a malformed <code>Content-Length</code> header
+ * value.
+ * <p>
+ * This class recognizes "chunked" and "identitiy" transfer-coding only.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class StrictContentLengthStrategy implements ContentLengthStrategy {
+
+ public static final StrictContentLengthStrategy INSTANCE = new StrictContentLengthStrategy();
+
+ private final int implicitLen;
+
+ /**
+ * Creates <tt>StrictContentLengthStrategy</tt> instance with the given length used per default
+ * when content length is not explicitly specified in the message.
+ *
+ * @param implicitLen implicit content length.
+ *
+ * @since 4.2
+ */
+ public StrictContentLengthStrategy(final int implicitLen) {
+ super();
+ this.implicitLen = implicitLen;
+ }
+
+ /**
+ * Creates <tt>StrictContentLengthStrategy</tt> instance. {@link ContentLengthStrategy#IDENTITY}
+ * is used per default when content length is not explicitly specified in the message.
+ */
+ public StrictContentLengthStrategy() {
+ this(IDENTITY);
+ }
+
+ public long determineLength(final HttpMessage message) throws HttpException {
+ Args.notNull(message, "HTTP message");
+ // Although Transfer-Encoding is specified as a list, in practice
+ // it is either missing or has the single value "chunked". So we
+ // treat it as a single-valued header here.
+ final Header transferEncodingHeader = message.getFirstHeader(HTTP.TRANSFER_ENCODING);
+ if (transferEncodingHeader != null) {
+ final String s = transferEncodingHeader.getValue();
+ if (HTTP.CHUNK_CODING.equalsIgnoreCase(s)) {
+ if (message.getProtocolVersion().lessEquals(HttpVersion.HTTP_1_0)) {
+ throw new ProtocolException(
+ "Chunked transfer encoding not allowed for " +
+ message.getProtocolVersion());
+ }
+ return CHUNKED;
+ } else if (HTTP.IDENTITY_CODING.equalsIgnoreCase(s)) {
+ return IDENTITY;
+ } else {
+ throw new ProtocolException(
+ "Unsupported transfer encoding: " + s);
+ }
+ }
+ final Header contentLengthHeader = message.getFirstHeader(HTTP.CONTENT_LEN);
+ if (contentLengthHeader != null) {
+ final String s = contentLengthHeader.getValue();
+ try {
+ final long len = Long.parseLong(s);
+ if (len < 0) {
+ throw new ProtocolException("Negative content length: " + s);
+ }
+ return len;
+ } catch (final NumberFormatException e) {
+ throw new ProtocolException("Invalid content length: " + s);
+ }
+ }
+ return this.implicitLen;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/package-info.java
new file mode 100644
index 0000000000..7566c45702
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/entity/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Default implementations of entity content strategies.
+ */
+package ch.boye.httpclientandroidlib.impl.entity;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/BackoffStrategyExec.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/BackoffStrategyExec.java
new file mode 100644
index 0000000000..a639da9342
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/BackoffStrategyExec.java
@@ -0,0 +1,104 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.execchain;
+
+import java.io.IOException;
+import java.lang.reflect.UndeclaredThrowableException;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.BackoffManager;
+import ch.boye.httpclientandroidlib.client.ConnectionBackoffStrategy;
+import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
+import ch.boye.httpclientandroidlib.client.methods.HttpExecutionAware;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestWrapper;
+import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * @since 4.3
+ */
+@Immutable
+public class BackoffStrategyExec implements ClientExecChain {
+
+ private final ClientExecChain requestExecutor;
+ private final ConnectionBackoffStrategy connectionBackoffStrategy;
+ private final BackoffManager backoffManager;
+
+ public BackoffStrategyExec(
+ final ClientExecChain requestExecutor,
+ final ConnectionBackoffStrategy connectionBackoffStrategy,
+ final BackoffManager backoffManager) {
+ super();
+ Args.notNull(requestExecutor, "HTTP client request executor");
+ Args.notNull(connectionBackoffStrategy, "Connection backoff strategy");
+ Args.notNull(backoffManager, "Backoff manager");
+ this.requestExecutor = requestExecutor;
+ this.connectionBackoffStrategy = connectionBackoffStrategy;
+ this.backoffManager = backoffManager;
+ }
+
+ public CloseableHttpResponse execute(
+ final HttpRoute route,
+ final HttpRequestWrapper request,
+ final HttpClientContext context,
+ final HttpExecutionAware execAware) throws IOException, HttpException {
+ Args.notNull(route, "HTTP route");
+ Args.notNull(request, "HTTP request");
+ Args.notNull(context, "HTTP context");
+ CloseableHttpResponse out = null;
+ try {
+ out = this.requestExecutor.execute(route, request, context, execAware);
+ } catch (final Exception ex) {
+ if (out != null) {
+ out.close();
+ }
+ if (this.connectionBackoffStrategy.shouldBackoff(ex)) {
+ this.backoffManager.backOff(route);
+ }
+ if (ex instanceof RuntimeException) {
+ throw (RuntimeException) ex;
+ }
+ if (ex instanceof HttpException) {
+ throw (HttpException) ex;
+ }
+ if (ex instanceof IOException) {
+ throw (IOException) ex;
+ }
+ throw new UndeclaredThrowableException(ex);
+ }
+ if (this.connectionBackoffStrategy.shouldBackoff(out)) {
+ this.backoffManager.backOff(route);
+ } else {
+ this.backoffManager.probe(route);
+ }
+ return out;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/ClientExecChain.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/ClientExecChain.java
new file mode 100644
index 0000000000..2b5e3c8f6d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/ClientExecChain.java
@@ -0,0 +1,74 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.execchain;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
+import ch.boye.httpclientandroidlib.client.methods.HttpExecutionAware;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestWrapper;
+import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+
+/**
+ * This interface represents an element in the HTTP request execution chain. Each element can
+ * either be a decorator around another element that implements a cross cutting aspect or
+ * a self-contained executor capable of producing a response for the given request.
+ * <p/>
+ * Important: please note it is required for decorators that implement post execution aspects
+ * or response post-processing of any sort to release resources associated with the response
+ * by calling {@link CloseableHttpResponse#close()} methods in case of an I/O, protocol or
+ * runtime exception, or in case the response is not propagated to the caller.
+ *
+ * @since 4.3
+ */
+public interface ClientExecChain {
+
+ /**
+ * Executes th request either by transmitting it to the target server or
+ * by passing it onto the next executor in the request execution chain.
+ *
+ * @param route connection route.
+ * @param request current request.
+ * @param clientContext current HTTP context.
+ * @param execAware receiver of notifications of blocking I/O operations.
+ * @return HTTP response either received from the opposite endpoint
+ * or generated locally.
+ * @throws IOException in case of a I/O error.
+ * (this type of exceptions are potentially recoverable).
+ * @throws HttpException in case of an HTTP protocol error
+ * (usually this type of exceptions are non-recoverable).
+ */
+ CloseableHttpResponse execute(
+ HttpRoute route,
+ HttpRequestWrapper request,
+ HttpClientContext clientContext,
+ HttpExecutionAware execAware) throws IOException, HttpException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/ConnectionHolder.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/ConnectionHolder.java
new file mode 100644
index 0000000000..d9bb172864
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/ConnectionHolder.java
@@ -0,0 +1,153 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.execchain;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+import ch.boye.httpclientandroidlib.HttpClientConnection;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.concurrent.Cancellable;
+import ch.boye.httpclientandroidlib.conn.ConnectionReleaseTrigger;
+import ch.boye.httpclientandroidlib.conn.HttpClientConnectionManager;
+
+/**
+ * Internal connection holder.
+ *
+ * @since 4.3
+ */
+@ThreadSafe
+class ConnectionHolder implements ConnectionReleaseTrigger, Cancellable, Closeable {
+
+ public HttpClientAndroidLog log;
+
+ private final HttpClientConnectionManager manager;
+ private final HttpClientConnection managedConn;
+ private volatile boolean reusable;
+ private volatile Object state;
+ private volatile long validDuration;
+ private volatile TimeUnit tunit;
+
+ private volatile boolean released;
+
+ public ConnectionHolder(
+ final HttpClientAndroidLog log,
+ final HttpClientConnectionManager manager,
+ final HttpClientConnection managedConn) {
+ super();
+ this.log = log;
+ this.manager = manager;
+ this.managedConn = managedConn;
+ }
+
+ public boolean isReusable() {
+ return this.reusable;
+ }
+
+ public void markReusable() {
+ this.reusable = true;
+ }
+
+ public void markNonReusable() {
+ this.reusable = false;
+ }
+
+ public void setState(final Object state) {
+ this.state = state;
+ }
+
+ public void setValidFor(final long duration, final TimeUnit tunit) {
+ synchronized (this.managedConn) {
+ this.validDuration = duration;
+ this.tunit = tunit;
+ }
+ }
+
+ public void releaseConnection() {
+ synchronized (this.managedConn) {
+ if (this.released) {
+ return;
+ }
+ this.released = true;
+ if (this.reusable) {
+ this.manager.releaseConnection(this.managedConn,
+ this.state, this.validDuration, this.tunit);
+ } else {
+ try {
+ this.managedConn.close();
+ log.debug("Connection discarded");
+ } catch (final IOException ex) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug(ex.getMessage(), ex);
+ }
+ } finally {
+ this.manager.releaseConnection(
+ this.managedConn, null, 0, TimeUnit.MILLISECONDS);
+ }
+ }
+ }
+ }
+
+ public void abortConnection() {
+ synchronized (this.managedConn) {
+ if (this.released) {
+ return;
+ }
+ this.released = true;
+ try {
+ this.managedConn.shutdown();
+ log.debug("Connection discarded");
+ } catch (final IOException ex) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug(ex.getMessage(), ex);
+ }
+ } finally {
+ this.manager.releaseConnection(
+ this.managedConn, null, 0, TimeUnit.MILLISECONDS);
+ }
+ }
+ }
+
+ public boolean cancel() {
+ final boolean alreadyReleased = this.released;
+ log.debug("Cancelling request execution");
+ abortConnection();
+ return !alreadyReleased;
+ }
+
+ public boolean isReleased() {
+ return this.released;
+ }
+
+ public void close() throws IOException {
+ abortConnection();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/HttpResponseProxy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/HttpResponseProxy.java
new file mode 100644
index 0000000000..1fdc027c89
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/HttpResponseProxy.java
@@ -0,0 +1,185 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.execchain;
+
+import java.io.IOException;
+import java.util.Locale;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderIterator;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.StatusLine;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+/**
+ * A proxy class for {@link ch.boye.httpclientandroidlib.HttpResponse} that can be used to release client connection
+ * associated with the original response.
+ *
+ * @since 4.3
+ */
+@NotThreadSafe
+class HttpResponseProxy implements CloseableHttpResponse {
+
+ private final HttpResponse original;
+ private final ConnectionHolder connHolder;
+
+ public HttpResponseProxy(final HttpResponse original, final ConnectionHolder connHolder) {
+ this.original = original;
+ this.connHolder = connHolder;
+ ResponseEntityProxy.enchance(original, connHolder);
+ }
+
+ public void close() throws IOException {
+ if (this.connHolder != null) {
+ this.connHolder.abortConnection();
+ }
+ }
+
+ public StatusLine getStatusLine() {
+ return original.getStatusLine();
+ }
+
+ public void setStatusLine(final StatusLine statusline) {
+ original.setStatusLine(statusline);
+ }
+
+ public void setStatusLine(final ProtocolVersion ver, final int code) {
+ original.setStatusLine(ver, code);
+ }
+
+ public void setStatusLine(final ProtocolVersion ver, final int code, final String reason) {
+ original.setStatusLine(ver, code, reason);
+ }
+
+ public void setStatusCode(final int code) throws IllegalStateException {
+ original.setStatusCode(code);
+ }
+
+ public void setReasonPhrase(final String reason) throws IllegalStateException {
+ original.setReasonPhrase(reason);
+ }
+
+ public HttpEntity getEntity() {
+ return original.getEntity();
+ }
+
+ public void setEntity(final HttpEntity entity) {
+ original.setEntity(entity);
+ }
+
+ public Locale getLocale() {
+ return original.getLocale();
+ }
+
+ public void setLocale(final Locale loc) {
+ original.setLocale(loc);
+ }
+
+ public ProtocolVersion getProtocolVersion() {
+ return original.getProtocolVersion();
+ }
+
+ public boolean containsHeader(final String name) {
+ return original.containsHeader(name);
+ }
+
+ public Header[] getHeaders(final String name) {
+ return original.getHeaders(name);
+ }
+
+ public Header getFirstHeader(final String name) {
+ return original.getFirstHeader(name);
+ }
+
+ public Header getLastHeader(final String name) {
+ return original.getLastHeader(name);
+ }
+
+ public Header[] getAllHeaders() {
+ return original.getAllHeaders();
+ }
+
+ public void addHeader(final Header header) {
+ original.addHeader(header);
+ }
+
+ public void addHeader(final String name, final String value) {
+ original.addHeader(name, value);
+ }
+
+ public void setHeader(final Header header) {
+ original.setHeader(header);
+ }
+
+ public void setHeader(final String name, final String value) {
+ original.setHeader(name, value);
+ }
+
+ public void setHeaders(final Header[] headers) {
+ original.setHeaders(headers);
+ }
+
+ public void removeHeader(final Header header) {
+ original.removeHeader(header);
+ }
+
+ public void removeHeaders(final String name) {
+ original.removeHeaders(name);
+ }
+
+ public HeaderIterator headerIterator() {
+ return original.headerIterator();
+ }
+
+ public HeaderIterator headerIterator(final String name) {
+ return original.headerIterator(name);
+ }
+
+ @Deprecated
+ public HttpParams getParams() {
+ return original.getParams();
+ }
+
+ @Deprecated
+ public void setParams(final HttpParams params) {
+ original.setParams(params);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("HttpResponseProxy{");
+ sb.append(original);
+ sb.append('}');
+ return sb.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/MainClientExec.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/MainClientExec.java
new file mode 100644
index 0000000000..cf1dbfde49
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/MainClientExec.java
@@ -0,0 +1,568 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.execchain;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.ConnectionReuseStrategy;
+import ch.boye.httpclientandroidlib.HttpClientConnection;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.auth.AUTH;
+import ch.boye.httpclientandroidlib.auth.AuthProtocolState;
+import ch.boye.httpclientandroidlib.auth.AuthState;
+import ch.boye.httpclientandroidlib.client.AuthenticationStrategy;
+import ch.boye.httpclientandroidlib.client.NonRepeatableRequestException;
+import ch.boye.httpclientandroidlib.client.UserTokenHandler;
+import ch.boye.httpclientandroidlib.client.config.RequestConfig;
+import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
+import ch.boye.httpclientandroidlib.client.methods.HttpExecutionAware;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestWrapper;
+import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
+import ch.boye.httpclientandroidlib.client.protocol.RequestClientConnControl;
+import ch.boye.httpclientandroidlib.conn.ConnectionKeepAliveStrategy;
+import ch.boye.httpclientandroidlib.conn.ConnectionRequest;
+import ch.boye.httpclientandroidlib.conn.HttpClientConnectionManager;
+import ch.boye.httpclientandroidlib.conn.routing.BasicRouteDirector;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRouteDirector;
+import ch.boye.httpclientandroidlib.conn.routing.RouteTracker;
+import ch.boye.httpclientandroidlib.entity.BufferedHttpEntity;
+import ch.boye.httpclientandroidlib.impl.auth.HttpAuthenticator;
+import ch.boye.httpclientandroidlib.impl.conn.ConnectionShutdownException;
+import ch.boye.httpclientandroidlib.message.BasicHttpRequest;
+import ch.boye.httpclientandroidlib.protocol.HttpCoreContext;
+import ch.boye.httpclientandroidlib.protocol.HttpProcessor;
+import ch.boye.httpclientandroidlib.protocol.HttpRequestExecutor;
+import ch.boye.httpclientandroidlib.protocol.ImmutableHttpProcessor;
+import ch.boye.httpclientandroidlib.protocol.RequestTargetHost;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.EntityUtils;
+
+/**
+ * The last request executor in the HTTP request execution chain
+ * that is responsible for execution of request / response
+ * exchanges with the opposite endpoint.
+ * This executor will automatically retry the request in case
+ * of an authentication challenge by an intermediate proxy or
+ * by the target server.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class MainClientExec implements ClientExecChain {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ private final HttpRequestExecutor requestExecutor;
+ private final HttpClientConnectionManager connManager;
+ private final ConnectionReuseStrategy reuseStrategy;
+ private final ConnectionKeepAliveStrategy keepAliveStrategy;
+ private final HttpProcessor proxyHttpProcessor;
+ private final AuthenticationStrategy targetAuthStrategy;
+ private final AuthenticationStrategy proxyAuthStrategy;
+ private final HttpAuthenticator authenticator;
+ private final UserTokenHandler userTokenHandler;
+ private final HttpRouteDirector routeDirector;
+
+
+ public MainClientExec(
+ final HttpRequestExecutor requestExecutor,
+ final HttpClientConnectionManager connManager,
+ final ConnectionReuseStrategy reuseStrategy,
+ final ConnectionKeepAliveStrategy keepAliveStrategy,
+ final AuthenticationStrategy targetAuthStrategy,
+ final AuthenticationStrategy proxyAuthStrategy,
+ final UserTokenHandler userTokenHandler) {
+ Args.notNull(requestExecutor, "HTTP request executor");
+ Args.notNull(connManager, "Client connection manager");
+ Args.notNull(reuseStrategy, "Connection reuse strategy");
+ Args.notNull(keepAliveStrategy, "Connection keep alive strategy");
+ Args.notNull(targetAuthStrategy, "Target authentication strategy");
+ Args.notNull(proxyAuthStrategy, "Proxy authentication strategy");
+ Args.notNull(userTokenHandler, "User token handler");
+ this.authenticator = new HttpAuthenticator();
+ this.proxyHttpProcessor = new ImmutableHttpProcessor(
+ new RequestTargetHost(), new RequestClientConnControl());
+ this.routeDirector = new BasicRouteDirector();
+ this.requestExecutor = requestExecutor;
+ this.connManager = connManager;
+ this.reuseStrategy = reuseStrategy;
+ this.keepAliveStrategy = keepAliveStrategy;
+ this.targetAuthStrategy = targetAuthStrategy;
+ this.proxyAuthStrategy = proxyAuthStrategy;
+ this.userTokenHandler = userTokenHandler;
+ }
+
+ public CloseableHttpResponse execute(
+ final HttpRoute route,
+ final HttpRequestWrapper request,
+ final HttpClientContext context,
+ final HttpExecutionAware execAware) throws IOException, HttpException {
+ Args.notNull(route, "HTTP route");
+ Args.notNull(request, "HTTP request");
+ Args.notNull(context, "HTTP context");
+
+ AuthState targetAuthState = context.getTargetAuthState();
+ if (targetAuthState == null) {
+ targetAuthState = new AuthState();
+ context.setAttribute(HttpClientContext.TARGET_AUTH_STATE, targetAuthState);
+ }
+ AuthState proxyAuthState = context.getProxyAuthState();
+ if (proxyAuthState == null) {
+ proxyAuthState = new AuthState();
+ context.setAttribute(HttpClientContext.PROXY_AUTH_STATE, proxyAuthState);
+ }
+
+ if (request instanceof HttpEntityEnclosingRequest) {
+ RequestEntityProxy.enhance((HttpEntityEnclosingRequest) request);
+ }
+
+ Object userToken = context.getUserToken();
+
+ final ConnectionRequest connRequest = connManager.requestConnection(route, userToken);
+ if (execAware != null) {
+ if (execAware.isAborted()) {
+ connRequest.cancel();
+ throw new RequestAbortedException("Request aborted");
+ } else {
+ execAware.setCancellable(connRequest);
+ }
+ }
+
+ final RequestConfig config = context.getRequestConfig();
+
+ final HttpClientConnection managedConn;
+ try {
+ final int timeout = config.getConnectionRequestTimeout();
+ managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS);
+ } catch(final InterruptedException interrupted) {
+ Thread.currentThread().interrupt();
+ throw new RequestAbortedException("Request aborted", interrupted);
+ } catch(final ExecutionException ex) {
+ Throwable cause = ex.getCause();
+ if (cause == null) {
+ cause = ex;
+ }
+ throw new RequestAbortedException("Request execution failed", cause);
+ }
+
+ context.setAttribute(HttpCoreContext.HTTP_CONNECTION, managedConn);
+
+ if (config.isStaleConnectionCheckEnabled()) {
+ // validate connection
+ if (managedConn.isOpen()) {
+ this.log.debug("Stale connection check");
+ if (managedConn.isStale()) {
+ this.log.debug("Stale connection detected");
+ managedConn.close();
+ }
+ }
+ }
+
+ final ConnectionHolder connHolder = new ConnectionHolder(this.log, this.connManager, managedConn);
+ try {
+ if (execAware != null) {
+ execAware.setCancellable(connHolder);
+ }
+
+ HttpResponse response;
+ for (int execCount = 1;; execCount++) {
+
+ if (execCount > 1 && !RequestEntityProxy.isRepeatable(request)) {
+ throw new NonRepeatableRequestException("Cannot retry request " +
+ "with a non-repeatable request entity.");
+ }
+
+ if (execAware != null && execAware.isAborted()) {
+ throw new RequestAbortedException("Request aborted");
+ }
+
+ if (!managedConn.isOpen()) {
+ this.log.debug("Opening connection " + route);
+ try {
+ establishRoute(proxyAuthState, managedConn, route, request, context);
+ } catch (final TunnelRefusedException ex) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug(ex.getMessage());
+ }
+ response = ex.getResponse();
+ break;
+ }
+ }
+ final int timeout = config.getSocketTimeout();
+ if (timeout >= 0) {
+ managedConn.setSocketTimeout(timeout);
+ }
+
+ if (execAware != null && execAware.isAborted()) {
+ throw new RequestAbortedException("Request aborted");
+ }
+
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Executing request " + request.getRequestLine());
+ }
+
+ if (!request.containsHeader(AUTH.WWW_AUTH_RESP)) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Target auth state: " + targetAuthState.getState());
+ }
+ this.authenticator.generateAuthResponse(request, targetAuthState, context);
+ }
+ if (!request.containsHeader(AUTH.PROXY_AUTH_RESP) && !route.isTunnelled()) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Proxy auth state: " + proxyAuthState.getState());
+ }
+ this.authenticator.generateAuthResponse(request, proxyAuthState, context);
+ }
+
+ response = requestExecutor.execute(request, managedConn, context);
+
+ // The connection is in or can be brought to a re-usable state.
+ if (reuseStrategy.keepAlive(response, context)) {
+ // Set the idle duration of this connection
+ final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
+ if (this.log.isDebugEnabled()) {
+ final String s;
+ if (duration > 0) {
+ s = "for " + duration + " " + TimeUnit.MILLISECONDS;
+ } else {
+ s = "indefinitely";
+ }
+ this.log.debug("Connection can be kept alive " + s);
+ }
+ connHolder.setValidFor(duration, TimeUnit.MILLISECONDS);
+ connHolder.markReusable();
+ } else {
+ connHolder.markNonReusable();
+ }
+
+ if (needAuthentication(
+ targetAuthState, proxyAuthState, route, response, context)) {
+ // Make sure the response body is fully consumed, if present
+ final HttpEntity entity = response.getEntity();
+ if (connHolder.isReusable()) {
+ EntityUtils.consume(entity);
+ } else {
+ managedConn.close();
+ if (proxyAuthState.getState() == AuthProtocolState.SUCCESS
+ && proxyAuthState.getAuthScheme() != null
+ && proxyAuthState.getAuthScheme().isConnectionBased()) {
+ this.log.debug("Resetting proxy auth state");
+ proxyAuthState.reset();
+ }
+ if (targetAuthState.getState() == AuthProtocolState.SUCCESS
+ && targetAuthState.getAuthScheme() != null
+ && targetAuthState.getAuthScheme().isConnectionBased()) {
+ this.log.debug("Resetting target auth state");
+ targetAuthState.reset();
+ }
+ }
+ // discard previous auth headers
+ final HttpRequest original = request.getOriginal();
+ if (!original.containsHeader(AUTH.WWW_AUTH_RESP)) {
+ request.removeHeaders(AUTH.WWW_AUTH_RESP);
+ }
+ if (!original.containsHeader(AUTH.PROXY_AUTH_RESP)) {
+ request.removeHeaders(AUTH.PROXY_AUTH_RESP);
+ }
+ } else {
+ break;
+ }
+ }
+
+ if (userToken == null) {
+ userToken = userTokenHandler.getUserToken(context);
+ context.setAttribute(HttpClientContext.USER_TOKEN, userToken);
+ }
+ if (userToken != null) {
+ connHolder.setState(userToken);
+ }
+
+ // check for entity, release connection if possible
+ final HttpEntity entity = response.getEntity();
+ if (entity == null || !entity.isStreaming()) {
+ // connection not needed and (assumed to be) in re-usable state
+ connHolder.releaseConnection();
+ return new HttpResponseProxy(response, null);
+ } else {
+ return new HttpResponseProxy(response, connHolder);
+ }
+ } catch (final ConnectionShutdownException ex) {
+ final InterruptedIOException ioex = new InterruptedIOException(
+ "Connection has been shut down");
+ ioex.initCause(ex);
+ throw ioex;
+ } catch (final HttpException ex) {
+ connHolder.abortConnection();
+ throw ex;
+ } catch (final IOException ex) {
+ connHolder.abortConnection();
+ throw ex;
+ } catch (final RuntimeException ex) {
+ connHolder.abortConnection();
+ throw ex;
+ }
+ }
+
+ /**
+ * Establishes the target route.
+ */
+ void establishRoute(
+ final AuthState proxyAuthState,
+ final HttpClientConnection managedConn,
+ final HttpRoute route,
+ final HttpRequest request,
+ final HttpClientContext context) throws HttpException, IOException {
+ final RequestConfig config = context.getRequestConfig();
+ final int timeout = config.getConnectTimeout();
+ final RouteTracker tracker = new RouteTracker(route);
+ int step;
+ do {
+ final HttpRoute fact = tracker.toRoute();
+ step = this.routeDirector.nextStep(route, fact);
+
+ switch (step) {
+
+ case HttpRouteDirector.CONNECT_TARGET:
+ this.connManager.connect(
+ managedConn,
+ route,
+ timeout > 0 ? timeout : 0,
+ context);
+ tracker.connectTarget(route.isSecure());
+ break;
+ case HttpRouteDirector.CONNECT_PROXY:
+ this.connManager.connect(
+ managedConn,
+ route,
+ timeout > 0 ? timeout : 0,
+ context);
+ final HttpHost proxy = route.getProxyHost();
+ tracker.connectProxy(proxy, false);
+ break;
+ case HttpRouteDirector.TUNNEL_TARGET: {
+ final boolean secure = createTunnelToTarget(
+ proxyAuthState, managedConn, route, request, context);
+ this.log.debug("Tunnel to target created.");
+ tracker.tunnelTarget(secure);
+ } break;
+
+ case HttpRouteDirector.TUNNEL_PROXY: {
+ // The most simple example for this case is a proxy chain
+ // of two proxies, where P1 must be tunnelled to P2.
+ // route: Source -> P1 -> P2 -> Target (3 hops)
+ // fact: Source -> P1 -> Target (2 hops)
+ final int hop = fact.getHopCount()-1; // the hop to establish
+ final boolean secure = createTunnelToProxy(route, hop, context);
+ this.log.debug("Tunnel to proxy created.");
+ tracker.tunnelProxy(route.getHopTarget(hop), secure);
+ } break;
+
+ case HttpRouteDirector.LAYER_PROTOCOL:
+ this.connManager.upgrade(managedConn, route, context);
+ tracker.layerProtocol(route.isSecure());
+ break;
+
+ case HttpRouteDirector.UNREACHABLE:
+ throw new HttpException("Unable to establish route: " +
+ "planned = " + route + "; current = " + fact);
+ case HttpRouteDirector.COMPLETE:
+ this.connManager.routeComplete(managedConn, route, context);
+ break;
+ default:
+ throw new IllegalStateException("Unknown step indicator "
+ + step + " from RouteDirector.");
+ }
+
+ } while (step > HttpRouteDirector.COMPLETE);
+ }
+
+ /**
+ * Creates a tunnel to the target server.
+ * The connection must be established to the (last) proxy.
+ * A CONNECT request for tunnelling through the proxy will
+ * be created and sent, the response received and checked.
+ * This method does <i>not</i> update the connection with
+ * information about the tunnel, that is left to the caller.
+ */
+ private boolean createTunnelToTarget(
+ final AuthState proxyAuthState,
+ final HttpClientConnection managedConn,
+ final HttpRoute route,
+ final HttpRequest request,
+ final HttpClientContext context) throws HttpException, IOException {
+
+ final RequestConfig config = context.getRequestConfig();
+ final int timeout = config.getConnectTimeout();
+
+ final HttpHost target = route.getTargetHost();
+ final HttpHost proxy = route.getProxyHost();
+ HttpResponse response = null;
+
+ final String authority = target.toHostString();
+ final HttpRequest connect = new BasicHttpRequest("CONNECT", authority, request.getProtocolVersion());
+
+ this.requestExecutor.preProcess(connect, this.proxyHttpProcessor, context);
+
+ while (response == null) {
+ if (!managedConn.isOpen()) {
+ this.connManager.connect(
+ managedConn,
+ route,
+ timeout > 0 ? timeout : 0,
+ context);
+ }
+
+ connect.removeHeaders(AUTH.PROXY_AUTH_RESP);
+ this.authenticator.generateAuthResponse(connect, proxyAuthState, context);
+
+ response = this.requestExecutor.execute(connect, managedConn, context);
+
+ final int status = response.getStatusLine().getStatusCode();
+ if (status < 200) {
+ throw new HttpException("Unexpected response to CONNECT request: " +
+ response.getStatusLine());
+ }
+
+ if (config.isAuthenticationEnabled()) {
+ if (this.authenticator.isAuthenticationRequested(proxy, response,
+ this.proxyAuthStrategy, proxyAuthState, context)) {
+ if (this.authenticator.handleAuthChallenge(proxy, response,
+ this.proxyAuthStrategy, proxyAuthState, context)) {
+ // Retry request
+ if (this.reuseStrategy.keepAlive(response, context)) {
+ this.log.debug("Connection kept alive");
+ // Consume response content
+ final HttpEntity entity = response.getEntity();
+ EntityUtils.consume(entity);
+ } else {
+ managedConn.close();
+ }
+ response = null;
+ }
+ }
+ }
+ }
+
+ final int status = response.getStatusLine().getStatusCode();
+
+ if (status > 299) {
+
+ // Buffer response content
+ final HttpEntity entity = response.getEntity();
+ if (entity != null) {
+ response.setEntity(new BufferedHttpEntity(entity));
+ }
+
+ managedConn.close();
+ throw new TunnelRefusedException("CONNECT refused by proxy: " +
+ response.getStatusLine(), response);
+ }
+
+ // How to decide on security of the tunnelled connection?
+ // The socket factory knows only about the segment to the proxy.
+ // Even if that is secure, the hop to the target may be insecure.
+ // Leave it to derived classes, consider insecure by default here.
+ return false;
+ }
+
+ /**
+ * Creates a tunnel to an intermediate proxy.
+ * This method is <i>not</i> implemented in this class.
+ * It just throws an exception here.
+ */
+ private boolean createTunnelToProxy(
+ final HttpRoute route,
+ final int hop,
+ final HttpClientContext context) throws HttpException {
+
+ // Have a look at createTunnelToTarget and replicate the parts
+ // you need in a custom derived class. If your proxies don't require
+ // authentication, it is not too hard. But for the stock version of
+ // HttpClient, we cannot make such simplifying assumptions and would
+ // have to include proxy authentication code. The HttpComponents team
+ // is currently not in a position to support rarely used code of this
+ // complexity. Feel free to submit patches that refactor the code in
+ // createTunnelToTarget to facilitate re-use for proxy tunnelling.
+
+ throw new HttpException("Proxy chains are not supported.");
+ }
+
+ private boolean needAuthentication(
+ final AuthState targetAuthState,
+ final AuthState proxyAuthState,
+ final HttpRoute route,
+ final HttpResponse response,
+ final HttpClientContext context) {
+ final RequestConfig config = context.getRequestConfig();
+ if (config.isAuthenticationEnabled()) {
+ HttpHost target = context.getTargetHost();
+ if (target == null) {
+ target = route.getTargetHost();
+ }
+ if (target.getPort() < 0) {
+ target = new HttpHost(
+ target.getHostName(),
+ route.getTargetHost().getPort(),
+ target.getSchemeName());
+ }
+ final boolean targetAuthRequested = this.authenticator.isAuthenticationRequested(
+ target, response, this.targetAuthStrategy, targetAuthState, context);
+
+ HttpHost proxy = route.getProxyHost();
+ // if proxy is not set use target host instead
+ if (proxy == null) {
+ proxy = route.getTargetHost();
+ }
+ final boolean proxyAuthRequested = this.authenticator.isAuthenticationRequested(
+ proxy, response, this.proxyAuthStrategy, proxyAuthState, context);
+
+ if (targetAuthRequested) {
+ return this.authenticator.handleAuthChallenge(target, response,
+ this.targetAuthStrategy, targetAuthState, context);
+ }
+ if (proxyAuthRequested) {
+ return this.authenticator.handleAuthChallenge(proxy, response,
+ this.proxyAuthStrategy, proxyAuthState, context);
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/MinimalClientExec.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/MinimalClientExec.java
new file mode 100644
index 0000000000..16f6176ebc
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/MinimalClientExec.java
@@ -0,0 +1,251 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.execchain;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.ConnectionReuseStrategy;
+import ch.boye.httpclientandroidlib.HttpClientConnection;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.config.RequestConfig;
+import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
+import ch.boye.httpclientandroidlib.client.methods.HttpExecutionAware;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestWrapper;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
+import ch.boye.httpclientandroidlib.client.protocol.RequestClientConnControl;
+import ch.boye.httpclientandroidlib.client.utils.URIUtils;
+import ch.boye.httpclientandroidlib.conn.ConnectionKeepAliveStrategy;
+import ch.boye.httpclientandroidlib.conn.ConnectionRequest;
+import ch.boye.httpclientandroidlib.conn.HttpClientConnectionManager;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.impl.conn.ConnectionShutdownException;
+import ch.boye.httpclientandroidlib.protocol.HttpCoreContext;
+import ch.boye.httpclientandroidlib.protocol.HttpProcessor;
+import ch.boye.httpclientandroidlib.protocol.HttpRequestExecutor;
+import ch.boye.httpclientandroidlib.protocol.ImmutableHttpProcessor;
+import ch.boye.httpclientandroidlib.protocol.RequestContent;
+import ch.boye.httpclientandroidlib.protocol.RequestTargetHost;
+import ch.boye.httpclientandroidlib.protocol.RequestUserAgent;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.VersionInfo;
+
+/**
+ * Request executor that implements the most fundamental aspects of
+ * the HTTP specification and the most straight-forward request / response
+ * exchange with the target server. This executor does not support
+ * execution via proxy and will make no attempts to retry the request
+ * in case of a redirect, authentication challenge or I/O error.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class MinimalClientExec implements ClientExecChain {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ private final HttpRequestExecutor requestExecutor;
+ private final HttpClientConnectionManager connManager;
+ private final ConnectionReuseStrategy reuseStrategy;
+ private final ConnectionKeepAliveStrategy keepAliveStrategy;
+ private final HttpProcessor httpProcessor;
+
+ public MinimalClientExec(
+ final HttpRequestExecutor requestExecutor,
+ final HttpClientConnectionManager connManager,
+ final ConnectionReuseStrategy reuseStrategy,
+ final ConnectionKeepAliveStrategy keepAliveStrategy) {
+ Args.notNull(requestExecutor, "HTTP request executor");
+ Args.notNull(connManager, "Client connection manager");
+ Args.notNull(reuseStrategy, "Connection reuse strategy");
+ Args.notNull(keepAliveStrategy, "Connection keep alive strategy");
+ this.httpProcessor = new ImmutableHttpProcessor(
+ new RequestContent(),
+ new RequestTargetHost(),
+ new RequestClientConnControl(),
+ new RequestUserAgent(VersionInfo.getUserAgent(
+ "Apache-HttpClient", "ch.boye.httpclientandroidlib.client", getClass())));
+ this.requestExecutor = requestExecutor;
+ this.connManager = connManager;
+ this.reuseStrategy = reuseStrategy;
+ this.keepAliveStrategy = keepAliveStrategy;
+ }
+
+ static void rewriteRequestURI(
+ final HttpRequestWrapper request,
+ final HttpRoute route) throws ProtocolException {
+ try {
+ URI uri = request.getURI();
+ if (uri != null) {
+ // Make sure the request URI is relative
+ if (uri.isAbsolute()) {
+ uri = URIUtils.rewriteURI(uri, null, true);
+ } else {
+ uri = URIUtils.rewriteURI(uri);
+ }
+ request.setURI(uri);
+ }
+ } catch (final URISyntaxException ex) {
+ throw new ProtocolException("Invalid URI: " + request.getRequestLine().getUri(), ex);
+ }
+ }
+
+ public CloseableHttpResponse execute(
+ final HttpRoute route,
+ final HttpRequestWrapper request,
+ final HttpClientContext context,
+ final HttpExecutionAware execAware) throws IOException, HttpException {
+ Args.notNull(route, "HTTP route");
+ Args.notNull(request, "HTTP request");
+ Args.notNull(context, "HTTP context");
+
+ rewriteRequestURI(request, route);
+
+ final ConnectionRequest connRequest = connManager.requestConnection(route, null);
+ if (execAware != null) {
+ if (execAware.isAborted()) {
+ connRequest.cancel();
+ throw new RequestAbortedException("Request aborted");
+ } else {
+ execAware.setCancellable(connRequest);
+ }
+ }
+
+ final RequestConfig config = context.getRequestConfig();
+
+ final HttpClientConnection managedConn;
+ try {
+ final int timeout = config.getConnectionRequestTimeout();
+ managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS);
+ } catch(final InterruptedException interrupted) {
+ Thread.currentThread().interrupt();
+ throw new RequestAbortedException("Request aborted", interrupted);
+ } catch(final ExecutionException ex) {
+ Throwable cause = ex.getCause();
+ if (cause == null) {
+ cause = ex;
+ }
+ throw new RequestAbortedException("Request execution failed", cause);
+ }
+
+ final ConnectionHolder releaseTrigger = new ConnectionHolder(log, connManager, managedConn);
+ try {
+ if (execAware != null) {
+ if (execAware.isAborted()) {
+ releaseTrigger.close();
+ throw new RequestAbortedException("Request aborted");
+ } else {
+ execAware.setCancellable(releaseTrigger);
+ }
+ }
+
+ if (!managedConn.isOpen()) {
+ final int timeout = config.getConnectTimeout();
+ this.connManager.connect(
+ managedConn,
+ route,
+ timeout > 0 ? timeout : 0,
+ context);
+ this.connManager.routeComplete(managedConn, route, context);
+ }
+ final int timeout = config.getSocketTimeout();
+ if (timeout >= 0) {
+ managedConn.setSocketTimeout(timeout);
+ }
+
+ HttpHost target = null;
+ final HttpRequest original = request.getOriginal();
+ if (original instanceof HttpUriRequest) {
+ final URI uri = ((HttpUriRequest) original).getURI();
+ if (uri.isAbsolute()) {
+ target = new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme());
+ }
+ }
+ if (target == null) {
+ target = route.getTargetHost();
+ }
+
+ context.setAttribute(HttpCoreContext.HTTP_TARGET_HOST, target);
+ context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
+ context.setAttribute(HttpCoreContext.HTTP_CONNECTION, managedConn);
+ context.setAttribute(HttpClientContext.HTTP_ROUTE, route);
+
+ httpProcessor.process(request, context);
+ final HttpResponse response = requestExecutor.execute(request, managedConn, context);
+ httpProcessor.process(response, context);
+
+ // The connection is in or can be brought to a re-usable state.
+ if (reuseStrategy.keepAlive(response, context)) {
+ // Set the idle duration of this connection
+ final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
+ releaseTrigger.setValidFor(duration, TimeUnit.MILLISECONDS);
+ releaseTrigger.markReusable();
+ } else {
+ releaseTrigger.markNonReusable();
+ }
+
+ // check for entity, release connection if possible
+ final HttpEntity entity = response.getEntity();
+ if (entity == null || !entity.isStreaming()) {
+ // connection not needed and (assumed to be) in re-usable state
+ releaseTrigger.releaseConnection();
+ return new HttpResponseProxy(response, null);
+ } else {
+ return new HttpResponseProxy(response, releaseTrigger);
+ }
+ } catch (final ConnectionShutdownException ex) {
+ final InterruptedIOException ioex = new InterruptedIOException(
+ "Connection has been shut down");
+ ioex.initCause(ex);
+ throw ioex;
+ } catch (final HttpException ex) {
+ releaseTrigger.abortConnection();
+ throw ex;
+ } catch (final IOException ex) {
+ releaseTrigger.abortConnection();
+ throw ex;
+ } catch (final RuntimeException ex) {
+ releaseTrigger.abortConnection();
+ throw ex;
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/ProtocolExec.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/ProtocolExec.java
new file mode 100644
index 0000000000..4effac603b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/ProtocolExec.java
@@ -0,0 +1,214 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.execchain;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.auth.AuthScope;
+import ch.boye.httpclientandroidlib.auth.UsernamePasswordCredentials;
+import ch.boye.httpclientandroidlib.client.CredentialsProvider;
+import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
+import ch.boye.httpclientandroidlib.client.methods.HttpExecutionAware;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestWrapper;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.client.params.ClientPNames;
+import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
+import ch.boye.httpclientandroidlib.client.utils.URIUtils;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.impl.client.BasicCredentialsProvider;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.HttpCoreContext;
+import ch.boye.httpclientandroidlib.protocol.HttpProcessor;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Request executor in the request execution chain that is responsible
+ * for implementation of HTTP specification requirements.
+ * Internally this executor relies on a {@link HttpProcessor} to populate
+ * requisite HTTP request headers, process HTTP response headers and update
+ * session state in {@link HttpClientContext}.
+ * <p/>
+ * Further responsibilities such as communication with the opposite
+ * endpoint is delegated to the next executor in the request execution
+ * chain.
+ *
+ * @since 4.3
+ */
+@Immutable
+@SuppressWarnings("deprecation")
+public class ProtocolExec implements ClientExecChain {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ private final ClientExecChain requestExecutor;
+ private final HttpProcessor httpProcessor;
+
+ public ProtocolExec(final ClientExecChain requestExecutor, final HttpProcessor httpProcessor) {
+ Args.notNull(requestExecutor, "HTTP client request executor");
+ Args.notNull(httpProcessor, "HTTP protocol processor");
+ this.requestExecutor = requestExecutor;
+ this.httpProcessor = httpProcessor;
+ }
+
+ void rewriteRequestURI(
+ final HttpRequestWrapper request,
+ final HttpRoute route) throws ProtocolException {
+ try {
+ URI uri = request.getURI();
+ if (uri != null) {
+ if (route.getProxyHost() != null && !route.isTunnelled()) {
+ // Make sure the request URI is absolute
+ if (!uri.isAbsolute()) {
+ final HttpHost target = route.getTargetHost();
+ uri = URIUtils.rewriteURI(uri, target, true);
+ } else {
+ uri = URIUtils.rewriteURI(uri);
+ }
+ } else {
+ // Make sure the request URI is relative
+ if (uri.isAbsolute()) {
+ uri = URIUtils.rewriteURI(uri, null, true);
+ } else {
+ uri = URIUtils.rewriteURI(uri);
+ }
+ }
+ request.setURI(uri);
+ }
+ } catch (final URISyntaxException ex) {
+ throw new ProtocolException("Invalid URI: " + request.getRequestLine().getUri(), ex);
+ }
+ }
+
+ public CloseableHttpResponse execute(
+ final HttpRoute route,
+ final HttpRequestWrapper request,
+ final HttpClientContext context,
+ final HttpExecutionAware execAware) throws IOException,
+ HttpException {
+ Args.notNull(route, "HTTP route");
+ Args.notNull(request, "HTTP request");
+ Args.notNull(context, "HTTP context");
+
+ final HttpRequest original = request.getOriginal();
+ URI uri = null;
+ if (original instanceof HttpUriRequest) {
+ uri = ((HttpUriRequest) original).getURI();
+ } else {
+ final String uriString = original.getRequestLine().getUri();
+ try {
+ uri = URI.create(uriString);
+ } catch (final IllegalArgumentException ex) {
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Unable to parse '" + uriString + "' as a valid URI; " +
+ "request URI and Host header may be inconsistent", ex);
+ }
+ }
+
+ }
+ request.setURI(uri);
+
+ // Re-write request URI if needed
+ rewriteRequestURI(request, route);
+
+ final HttpParams params = request.getParams();
+ HttpHost virtualHost = (HttpHost) params.getParameter(ClientPNames.VIRTUAL_HOST);
+ // HTTPCLIENT-1092 - add the port if necessary
+ if (virtualHost != null && virtualHost.getPort() == -1) {
+ final int port = route.getTargetHost().getPort();
+ if (port != -1) {
+ virtualHost = new HttpHost(virtualHost.getHostName(), port,
+ virtualHost.getSchemeName());
+ }
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Using virtual host" + virtualHost);
+ }
+ }
+
+ HttpHost target = null;
+ if (virtualHost != null) {
+ target = virtualHost;
+ } else {
+ if (uri != null && uri.isAbsolute() && uri.getHost() != null) {
+ target = new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme());
+ }
+ }
+ if (target == null) {
+ target = route.getTargetHost();
+ }
+
+ // Get user info from the URI
+ if (uri != null) {
+ final String userinfo = uri.getUserInfo();
+ if (userinfo != null) {
+ CredentialsProvider credsProvider = context.getCredentialsProvider();
+ if (credsProvider == null) {
+ credsProvider = new BasicCredentialsProvider();
+ context.setCredentialsProvider(credsProvider);
+ }
+ credsProvider.setCredentials(
+ new AuthScope(target),
+ new UsernamePasswordCredentials(userinfo));
+ }
+ }
+
+ // Run request protocol interceptors
+ context.setAttribute(HttpCoreContext.HTTP_TARGET_HOST, target);
+ context.setAttribute(HttpClientContext.HTTP_ROUTE, route);
+ context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
+
+ this.httpProcessor.process(request, context);
+
+ final CloseableHttpResponse response = this.requestExecutor.execute(route, request,
+ context, execAware);
+ try {
+ // Run response protocol interceptors
+ context.setAttribute(HttpCoreContext.HTTP_RESPONSE, response);
+ this.httpProcessor.process(response, context);
+ return response;
+ } catch (final RuntimeException ex) {
+ response.close();
+ throw ex;
+ } catch (final IOException ex) {
+ response.close();
+ throw ex;
+ } catch (final HttpException ex) {
+ response.close();
+ throw ex;
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/RedirectExec.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/RedirectExec.java
new file mode 100644
index 0000000000..c870662f74
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/RedirectExec.java
@@ -0,0 +1,185 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.execchain;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.auth.AuthScheme;
+import ch.boye.httpclientandroidlib.auth.AuthState;
+import ch.boye.httpclientandroidlib.client.RedirectException;
+import ch.boye.httpclientandroidlib.client.RedirectStrategy;
+import ch.boye.httpclientandroidlib.client.config.RequestConfig;
+import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
+import ch.boye.httpclientandroidlib.client.methods.HttpExecutionAware;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestWrapper;
+import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
+import ch.boye.httpclientandroidlib.client.utils.URIUtils;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoutePlanner;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.EntityUtils;
+
+/**
+ * Request executor in the request execution chain that is responsible
+ * for handling of request redirects.
+ * <p/>
+ * Further responsibilities such as communication with the opposite
+ * endpoint is delegated to the next executor in the request execution
+ * chain.
+ *
+ * @since 4.3
+ */
+@ThreadSafe
+public class RedirectExec implements ClientExecChain {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ private final ClientExecChain requestExecutor;
+ private final RedirectStrategy redirectStrategy;
+ private final HttpRoutePlanner routePlanner;
+
+ public RedirectExec(
+ final ClientExecChain requestExecutor,
+ final HttpRoutePlanner routePlanner,
+ final RedirectStrategy redirectStrategy) {
+ super();
+ Args.notNull(requestExecutor, "HTTP client request executor");
+ Args.notNull(routePlanner, "HTTP route planner");
+ Args.notNull(redirectStrategy, "HTTP redirect strategy");
+ this.requestExecutor = requestExecutor;
+ this.routePlanner = routePlanner;
+ this.redirectStrategy = redirectStrategy;
+ }
+
+ public CloseableHttpResponse execute(
+ final HttpRoute route,
+ final HttpRequestWrapper request,
+ final HttpClientContext context,
+ final HttpExecutionAware execAware) throws IOException, HttpException {
+ Args.notNull(route, "HTTP route");
+ Args.notNull(request, "HTTP request");
+ Args.notNull(context, "HTTP context");
+
+ final List<URI> redirectLocations = context.getRedirectLocations();
+ if (redirectLocations != null) {
+ redirectLocations.clear();
+ }
+
+ final RequestConfig config = context.getRequestConfig();
+ final int maxRedirects = config.getMaxRedirects() > 0 ? config.getMaxRedirects() : 50;
+ HttpRoute currentRoute = route;
+ HttpRequestWrapper currentRequest = request;
+ for (int redirectCount = 0;;) {
+ final CloseableHttpResponse response = requestExecutor.execute(
+ currentRoute, currentRequest, context, execAware);
+ try {
+ if (config.isRedirectsEnabled() &&
+ this.redirectStrategy.isRedirected(currentRequest, response, context)) {
+
+ if (redirectCount >= maxRedirects) {
+ throw new RedirectException("Maximum redirects ("+ maxRedirects + ") exceeded");
+ }
+ redirectCount++;
+
+ final HttpRequest redirect = this.redirectStrategy.getRedirect(
+ currentRequest, response, context);
+ if (!redirect.headerIterator().hasNext()) {
+ final HttpRequest original = request.getOriginal();
+ redirect.setHeaders(original.getAllHeaders());
+ }
+ currentRequest = HttpRequestWrapper.wrap(redirect);
+
+ if (currentRequest instanceof HttpEntityEnclosingRequest) {
+ RequestEntityProxy.enhance((HttpEntityEnclosingRequest) currentRequest);
+ }
+
+ final URI uri = currentRequest.getURI();
+ final HttpHost newTarget = URIUtils.extractHost(uri);
+ if (newTarget == null) {
+ throw new ProtocolException("Redirect URI does not specify a valid host name: " +
+ uri);
+ }
+
+ // Reset virtual host and auth states if redirecting to another host
+ if (!currentRoute.getTargetHost().equals(newTarget)) {
+ final AuthState targetAuthState = context.getTargetAuthState();
+ if (targetAuthState != null) {
+ this.log.debug("Resetting target auth state");
+ targetAuthState.reset();
+ }
+ final AuthState proxyAuthState = context.getProxyAuthState();
+ if (proxyAuthState != null) {
+ final AuthScheme authScheme = proxyAuthState.getAuthScheme();
+ if (authScheme != null && authScheme.isConnectionBased()) {
+ this.log.debug("Resetting proxy auth state");
+ proxyAuthState.reset();
+ }
+ }
+ }
+
+ currentRoute = this.routePlanner.determineRoute(newTarget, currentRequest, context);
+ if (this.log.isDebugEnabled()) {
+ this.log.debug("Redirecting to '" + uri + "' via " + currentRoute);
+ }
+ EntityUtils.consume(response.getEntity());
+ response.close();
+ } else {
+ return response;
+ }
+ } catch (final RuntimeException ex) {
+ response.close();
+ throw ex;
+ } catch (final IOException ex) {
+ response.close();
+ throw ex;
+ } catch (final HttpException ex) {
+ // Protocol exception related to a direct.
+ // The underlying connection may still be salvaged.
+ try {
+ EntityUtils.consume(response.getEntity());
+ } catch (final IOException ioex) {
+ this.log.debug("I/O error while releasing connection", ioex);
+ } finally {
+ response.close();
+ }
+ throw ex;
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/RequestAbortedException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/RequestAbortedException.java
new file mode 100644
index 0000000000..be58be8f88
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/RequestAbortedException.java
@@ -0,0 +1,55 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.execchain;
+
+import java.io.InterruptedIOException;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Signals that the request has been aborted.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class RequestAbortedException extends InterruptedIOException {
+
+ private static final long serialVersionUID = 4973849966012490112L;
+
+ public RequestAbortedException(final String message) {
+ super(message);
+ }
+
+ public RequestAbortedException(final String message, final Throwable cause) {
+ super(message);
+ if (cause != null) {
+ initCause(cause);
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/RequestEntityProxy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/RequestEntityProxy.java
new file mode 100644
index 0000000000..d5b3966849
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/RequestEntityProxy.java
@@ -0,0 +1,137 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.execchain;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+
+/**
+ * A Proxy class for {@link ch.boye.httpclientandroidlib.HttpEntity} enclosed in a request message.
+ *
+ * @since 4.3
+ */
+@NotThreadSafe
+class RequestEntityProxy implements HttpEntity {
+
+ static void enhance(final HttpEntityEnclosingRequest request) {
+ final HttpEntity entity = request.getEntity();
+ if (entity != null && !entity.isRepeatable() && !isEnhanced(entity)) {
+ request.setEntity(new RequestEntityProxy(entity));
+ }
+ }
+
+ static boolean isEnhanced(final HttpEntity entity) {
+ return entity instanceof RequestEntityProxy;
+ }
+
+ static boolean isRepeatable(final HttpRequest request) {
+ if (request instanceof HttpEntityEnclosingRequest) {
+ final HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
+ if (entity != null) {
+ if (isEnhanced(entity)) {
+ final RequestEntityProxy proxy = (RequestEntityProxy) entity;
+ if (!proxy.isConsumed()) {
+ return true;
+ }
+ }
+ return entity.isRepeatable();
+ }
+ }
+ return true;
+ }
+
+ private final HttpEntity original;
+ private boolean consumed = false;
+
+ RequestEntityProxy(final HttpEntity original) {
+ super();
+ this.original = original;
+ }
+
+ public HttpEntity getOriginal() {
+ return original;
+ }
+
+ public boolean isConsumed() {
+ return consumed;
+ }
+
+ public boolean isRepeatable() {
+ return original.isRepeatable();
+ }
+
+ public boolean isChunked() {
+ return original.isChunked();
+ }
+
+ public long getContentLength() {
+ return original.getContentLength();
+ }
+
+ public Header getContentType() {
+ return original.getContentType();
+ }
+
+ public Header getContentEncoding() {
+ return original.getContentEncoding();
+ }
+
+ public InputStream getContent() throws IOException, IllegalStateException {
+ return original.getContent();
+ }
+
+ public void writeTo(final OutputStream outstream) throws IOException {
+ consumed = true;
+ original.writeTo(outstream);
+ }
+
+ public boolean isStreaming() {
+ return original.isStreaming();
+ }
+
+ @Deprecated
+ public void consumeContent() throws IOException {
+ consumed = true;
+ original.consumeContent();
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("RequestEntityProxy{");
+ sb.append(original);
+ sb.append('}');
+ return sb.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/ResponseEntityProxy.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/ResponseEntityProxy.java
new file mode 100644
index 0000000000..8a5df1cf8f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/ResponseEntityProxy.java
@@ -0,0 +1,152 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.execchain;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.SocketException;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.conn.EofSensorInputStream;
+import ch.boye.httpclientandroidlib.conn.EofSensorWatcher;
+import ch.boye.httpclientandroidlib.entity.HttpEntityWrapper;
+
+/**
+ * A wrapper class for {@link HttpEntity} enclosed in a response message.
+ *
+ * @since 4.3
+ */
+@NotThreadSafe
+class ResponseEntityProxy extends HttpEntityWrapper implements EofSensorWatcher {
+
+ private final ConnectionHolder connHolder;
+
+ public static void enchance(final HttpResponse response, final ConnectionHolder connHolder) {
+ final HttpEntity entity = response.getEntity();
+ if (entity != null && entity.isStreaming() && connHolder != null) {
+ response.setEntity(new ResponseEntityProxy(entity, connHolder));
+ }
+ }
+
+ ResponseEntityProxy(final HttpEntity entity, final ConnectionHolder connHolder) {
+ super(entity);
+ this.connHolder = connHolder;
+ }
+
+ private void cleanup() {
+ if (this.connHolder != null) {
+ this.connHolder.abortConnection();
+ }
+ }
+
+ public void releaseConnection() throws IOException {
+ if (this.connHolder != null) {
+ try {
+ if (this.connHolder.isReusable()) {
+ this.connHolder.releaseConnection();
+ }
+ } finally {
+ cleanup();
+ }
+ }
+ }
+
+ @Override
+ public boolean isRepeatable() {
+ return false;
+ }
+
+ @Override
+ public InputStream getContent() throws IOException {
+ return new EofSensorInputStream(this.wrappedEntity.getContent(), this);
+ }
+
+ @Deprecated
+ @Override
+ public void consumeContent() throws IOException {
+ releaseConnection();
+ }
+
+ @Override
+ public void writeTo(final OutputStream outstream) throws IOException {
+ try {
+ this.wrappedEntity.writeTo(outstream);
+ releaseConnection();
+ } finally {
+ cleanup();
+ }
+ }
+
+ public boolean eofDetected(final InputStream wrapped) throws IOException {
+ try {
+ // there may be some cleanup required, such as
+ // reading trailers after the response body:
+ wrapped.close();
+ releaseConnection();
+ } finally {
+ cleanup();
+ }
+ return false;
+ }
+
+ public boolean streamClosed(final InputStream wrapped) throws IOException {
+ try {
+ final boolean open = connHolder != null && !connHolder.isReleased();
+ // this assumes that closing the stream will
+ // consume the remainder of the response body:
+ try {
+ wrapped.close();
+ releaseConnection();
+ } catch (final SocketException ex) {
+ if (open) {
+ throw ex;
+ }
+ }
+ } finally {
+ cleanup();
+ }
+ return false;
+ }
+
+ public boolean streamAbort(final InputStream wrapped) throws IOException {
+ cleanup();
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("ResponseEntityProxy{");
+ sb.append(wrappedEntity);
+ sb.append('}');
+ return sb.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/RetryExec.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/RetryExec.java
new file mode 100644
index 0000000000..ab78e3a83b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/RetryExec.java
@@ -0,0 +1,126 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.execchain;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.NoHttpResponseException;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.HttpRequestRetryHandler;
+import ch.boye.httpclientandroidlib.client.NonRepeatableRequestException;
+import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
+import ch.boye.httpclientandroidlib.client.methods.HttpExecutionAware;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestWrapper;
+import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Request executor in the request execution chain that is responsible
+ * for making a decision whether a request failed due to an I/O error
+ * should be re-executed.
+ * <p/>
+ * Further responsibilities such as communication with the opposite
+ * endpoint is delegated to the next executor in the request execution
+ * chain.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class RetryExec implements ClientExecChain {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ private final ClientExecChain requestExecutor;
+ private final HttpRequestRetryHandler retryHandler;
+
+ public RetryExec(
+ final ClientExecChain requestExecutor,
+ final HttpRequestRetryHandler retryHandler) {
+ Args.notNull(requestExecutor, "HTTP request executor");
+ Args.notNull(retryHandler, "HTTP request retry handler");
+ this.requestExecutor = requestExecutor;
+ this.retryHandler = retryHandler;
+ }
+
+ public CloseableHttpResponse execute(
+ final HttpRoute route,
+ final HttpRequestWrapper request,
+ final HttpClientContext context,
+ final HttpExecutionAware execAware) throws IOException, HttpException {
+ Args.notNull(route, "HTTP route");
+ Args.notNull(request, "HTTP request");
+ Args.notNull(context, "HTTP context");
+ final Header[] origheaders = request.getAllHeaders();
+ for (int execCount = 1;; execCount++) {
+ try {
+ return this.requestExecutor.execute(route, request, context, execAware);
+ } catch (final IOException ex) {
+ if (execAware != null && execAware.isAborted()) {
+ this.log.debug("Request has been aborted");
+ throw ex;
+ }
+ if (retryHandler.retryRequest(ex, execCount, context)) {
+ if (this.log.isInfoEnabled()) {
+ this.log.info("I/O exception ("+ ex.getClass().getName() +
+ ") caught when processing request to "
+ + route +
+ ": "
+ + ex.getMessage());
+ }
+ if (this.log.isDebugEnabled()) {
+ this.log.debug(ex.getMessage(), ex);
+ }
+ if (!RequestEntityProxy.isRepeatable(request)) {
+ this.log.debug("Cannot retry non-repeatable request");
+ throw new NonRepeatableRequestException("Cannot retry request " +
+ "with a non-repeatable request entity", ex);
+ }
+ request.setHeaders(origheaders);
+ if (this.log.isInfoEnabled()) {
+ this.log.info("Retrying request to " + route);
+ }
+ } else {
+ if (ex instanceof NoHttpResponseException) {
+ final NoHttpResponseException updatedex = new NoHttpResponseException(
+ route.getTargetHost().toHostString() + " failed to respond");
+ updatedex.setStackTrace(ex.getStackTrace());
+ throw updatedex;
+ } else {
+ throw ex;
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/ServiceUnavailableRetryExec.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/ServiceUnavailableRetryExec.java
new file mode 100644
index 0000000000..1382d998a8
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/ServiceUnavailableRetryExec.java
@@ -0,0 +1,108 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.execchain;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+
+import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog;
+/* LogFactory removed by HttpClient for Android script. */
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.client.ServiceUnavailableRetryStrategy;
+import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse;
+import ch.boye.httpclientandroidlib.client.methods.HttpExecutionAware;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestWrapper;
+import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
+import ch.boye.httpclientandroidlib.conn.routing.HttpRoute;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Request executor in the request execution chain that is responsible
+ * for making a decision whether a request that received a non-2xx response
+ * from the target server should be re-executed.
+ * <p/>
+ * Further responsibilities such as communication with the opposite
+ * endpoint is delegated to the next executor in the request execution
+ * chain.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class ServiceUnavailableRetryExec implements ClientExecChain {
+
+ public HttpClientAndroidLog log = new HttpClientAndroidLog(getClass());
+
+ private final ClientExecChain requestExecutor;
+ private final ServiceUnavailableRetryStrategy retryStrategy;
+
+ public ServiceUnavailableRetryExec(
+ final ClientExecChain requestExecutor,
+ final ServiceUnavailableRetryStrategy retryStrategy) {
+ super();
+ Args.notNull(requestExecutor, "HTTP request executor");
+ Args.notNull(retryStrategy, "Retry strategy");
+ this.requestExecutor = requestExecutor;
+ this.retryStrategy = retryStrategy;
+ }
+
+ public CloseableHttpResponse execute(
+ final HttpRoute route,
+ final HttpRequestWrapper request,
+ final HttpClientContext context,
+ final HttpExecutionAware execAware) throws IOException, HttpException {
+ final Header[] origheaders = request.getAllHeaders();
+ for (int c = 1;; c++) {
+ final CloseableHttpResponse response = this.requestExecutor.execute(
+ route, request, context, execAware);
+ try {
+ if (this.retryStrategy.retryRequest(response, c, context)) {
+ response.close();
+ final long nextInterval = this.retryStrategy.getRetryInterval();
+ if (nextInterval > 0) {
+ try {
+ this.log.trace("Wait for " + nextInterval);
+ Thread.sleep(nextInterval);
+ } catch (final InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new InterruptedIOException();
+ }
+ }
+ request.setHeaders(origheaders);
+ } else {
+ return response;
+ }
+ } catch (final RuntimeException ex) {
+ response.close();
+ throw ex;
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/TunnelRefusedException.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/TunnelRefusedException.java
new file mode 100644
index 0000000000..5545c7b84a
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/TunnelRefusedException.java
@@ -0,0 +1,55 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.execchain;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Signals that the tunnel request was rejected by the proxy host.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class TunnelRefusedException extends HttpException {
+
+ private static final long serialVersionUID = -8646722842745617323L;
+
+ private final HttpResponse response;
+
+ public TunnelRefusedException(final String message, final HttpResponse response) {
+ super(message);
+ this.response = response;
+ }
+
+ public HttpResponse getResponse() {
+ return this.response;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/package-info.java
new file mode 100644
index 0000000000..740a9eec7a
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/execchain/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * HTTP request execution chain APIs.
+ */
+package ch.boye.httpclientandroidlib.impl.execchain;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/AbstractMessageParser.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/AbstractMessageParser.java
new file mode 100644
index 0000000000..cd7aceb52a
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/AbstractMessageParser.java
@@ -0,0 +1,284 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpMessage;
+import ch.boye.httpclientandroidlib.MessageConstraintException;
+import ch.boye.httpclientandroidlib.ParseException;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.config.MessageConstraints;
+import ch.boye.httpclientandroidlib.io.HttpMessageParser;
+import ch.boye.httpclientandroidlib.io.SessionInputBuffer;
+import ch.boye.httpclientandroidlib.message.BasicLineParser;
+import ch.boye.httpclientandroidlib.message.LineParser;
+import ch.boye.httpclientandroidlib.params.HttpParamConfig;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Abstract base class for HTTP message parsers that obtain input from
+ * an instance of {@link SessionInputBuffer}.
+ *
+ * @since 4.0
+ */
+@SuppressWarnings("deprecation")
+@NotThreadSafe
+public abstract class AbstractMessageParser<T extends HttpMessage> implements HttpMessageParser<T> {
+
+ private static final int HEAD_LINE = 0;
+ private static final int HEADERS = 1;
+
+ private final SessionInputBuffer sessionBuffer;
+ private final MessageConstraints messageConstraints;
+ private final List<CharArrayBuffer> headerLines;
+ protected final LineParser lineParser;
+
+ private int state;
+ private T message;
+
+ /**
+ * Creates an instance of AbstractMessageParser.
+ *
+ * @param buffer the session input buffer.
+ * @param parser the line parser.
+ * @param params HTTP parameters.
+ *
+ * @deprecated (4.3) use {@link AbstractMessageParser#AbstractMessageParser(SessionInputBuffer,
+ * LineParser, MessageConstraints)}
+ */
+ @Deprecated
+ public AbstractMessageParser(
+ final SessionInputBuffer buffer,
+ final LineParser parser,
+ final HttpParams params) {
+ super();
+ Args.notNull(buffer, "Session input buffer");
+ Args.notNull(params, "HTTP parameters");
+ this.sessionBuffer = buffer;
+ this.messageConstraints = HttpParamConfig.getMessageConstraints(params);
+ this.lineParser = (parser != null) ? parser : BasicLineParser.INSTANCE;
+ this.headerLines = new ArrayList<CharArrayBuffer>();
+ this.state = HEAD_LINE;
+ }
+
+ /**
+ * Creates new instance of AbstractMessageParser.
+ *
+ * @param buffer the session input buffer.
+ * @param lineParser the line parser. If <code>null</code> {@link BasicLineParser#INSTANCE}
+ * will be used.
+ * @param constraints the message constraints. If <code>null</code>
+ * {@link MessageConstraints#DEFAULT} will be used.
+ *
+ * @since 4.3
+ */
+ public AbstractMessageParser(
+ final SessionInputBuffer buffer,
+ final LineParser lineParser,
+ final MessageConstraints constraints) {
+ super();
+ this.sessionBuffer = Args.notNull(buffer, "Session input buffer");
+ this.lineParser = lineParser != null ? lineParser : BasicLineParser.INSTANCE;
+ this.messageConstraints = constraints != null ? constraints : MessageConstraints.DEFAULT;
+ this.headerLines = new ArrayList<CharArrayBuffer>();
+ this.state = HEAD_LINE;
+ }
+
+ /**
+ * Parses HTTP headers from the data receiver stream according to the generic
+ * format as given in Section 3.1 of RFC 822, RFC-2616 Section 4 and 19.3.
+ *
+ * @param inbuffer Session input buffer
+ * @param maxHeaderCount maximum number of headers allowed. If the number
+ * of headers received from the data stream exceeds maxCount value, an
+ * IOException will be thrown. Setting this parameter to a negative value
+ * or zero will disable the check.
+ * @param maxLineLen maximum number of characters for a header line,
+ * including the continuation lines. Setting this parameter to a negative
+ * value or zero will disable the check.
+ * @return array of HTTP headers
+ * @param parser line parser to use. Can be <code>null</code>, in which case
+ * the default implementation of this interface will be used.
+ *
+ * @throws IOException in case of an I/O error
+ * @throws HttpException in case of HTTP protocol violation
+ */
+ public static Header[] parseHeaders(
+ final SessionInputBuffer inbuffer,
+ final int maxHeaderCount,
+ final int maxLineLen,
+ final LineParser parser) throws HttpException, IOException {
+ final List<CharArrayBuffer> headerLines = new ArrayList<CharArrayBuffer>();
+ return parseHeaders(inbuffer, maxHeaderCount, maxLineLen,
+ parser != null ? parser : BasicLineParser.INSTANCE,
+ headerLines);
+ }
+
+ /**
+ * Parses HTTP headers from the data receiver stream according to the generic
+ * format as given in Section 3.1 of RFC 822, RFC-2616 Section 4 and 19.3.
+ *
+ * @param inbuffer Session input buffer
+ * @param maxHeaderCount maximum number of headers allowed. If the number
+ * of headers received from the data stream exceeds maxCount value, an
+ * IOException will be thrown. Setting this parameter to a negative value
+ * or zero will disable the check.
+ * @param maxLineLen maximum number of characters for a header line,
+ * including the continuation lines. Setting this parameter to a negative
+ * value or zero will disable the check.
+ * @param parser line parser to use.
+ * @param headerLines List of header lines. This list will be used to store
+ * intermediate results. This makes it possible to resume parsing of
+ * headers in case of a {@link java.io.InterruptedIOException}.
+ *
+ * @return array of HTTP headers
+ *
+ * @throws IOException in case of an I/O error
+ * @throws HttpException in case of HTTP protocol violation
+ *
+ * @since 4.1
+ */
+ public static Header[] parseHeaders(
+ final SessionInputBuffer inbuffer,
+ final int maxHeaderCount,
+ final int maxLineLen,
+ final LineParser parser,
+ final List<CharArrayBuffer> headerLines) throws HttpException, IOException {
+ Args.notNull(inbuffer, "Session input buffer");
+ Args.notNull(parser, "Line parser");
+ Args.notNull(headerLines, "Header line list");
+
+ CharArrayBuffer current = null;
+ CharArrayBuffer previous = null;
+ for (;;) {
+ if (current == null) {
+ current = new CharArrayBuffer(64);
+ } else {
+ current.clear();
+ }
+ final int l = inbuffer.readLine(current);
+ if (l == -1 || current.length() < 1) {
+ break;
+ }
+ // Parse the header name and value
+ // Check for folded headers first
+ // Detect LWS-char see HTTP/1.0 or HTTP/1.1 Section 2.2
+ // discussion on folded headers
+ if ((current.charAt(0) == ' ' || current.charAt(0) == '\t') && previous != null) {
+ // we have continuation folded header
+ // so append value
+ int i = 0;
+ while (i < current.length()) {
+ final char ch = current.charAt(i);
+ if (ch != ' ' && ch != '\t') {
+ break;
+ }
+ i++;
+ }
+ if (maxLineLen > 0
+ && previous.length() + 1 + current.length() - i > maxLineLen) {
+ throw new MessageConstraintException("Maximum line length limit exceeded");
+ }
+ previous.append(' ');
+ previous.append(current, i, current.length() - i);
+ } else {
+ headerLines.add(current);
+ previous = current;
+ current = null;
+ }
+ if (maxHeaderCount > 0 && headerLines.size() >= maxHeaderCount) {
+ throw new MessageConstraintException("Maximum header count exceeded");
+ }
+ }
+ final Header[] headers = new Header[headerLines.size()];
+ for (int i = 0; i < headerLines.size(); i++) {
+ final CharArrayBuffer buffer = headerLines.get(i);
+ try {
+ headers[i] = parser.parseHeader(buffer);
+ } catch (final ParseException ex) {
+ throw new ProtocolException(ex.getMessage());
+ }
+ }
+ return headers;
+ }
+
+ /**
+ * Subclasses must override this method to generate an instance of
+ * {@link HttpMessage} based on the initial input from the session buffer.
+ * <p>
+ * Usually this method is expected to read just the very first line or
+ * the very first valid from the data stream and based on the input generate
+ * an appropriate instance of {@link HttpMessage}.
+ *
+ * @param sessionBuffer the session input buffer.
+ * @return HTTP message based on the input from the session buffer.
+ * @throws IOException in case of an I/O error.
+ * @throws HttpException in case of HTTP protocol violation.
+ * @throws ParseException in case of a parse error.
+ */
+ protected abstract T parseHead(SessionInputBuffer sessionBuffer)
+ throws IOException, HttpException, ParseException;
+
+ public T parse() throws IOException, HttpException {
+ final int st = this.state;
+ switch (st) {
+ case HEAD_LINE:
+ try {
+ this.message = parseHead(this.sessionBuffer);
+ } catch (final ParseException px) {
+ throw new ProtocolException(px.getMessage(), px);
+ }
+ this.state = HEADERS;
+ //$FALL-THROUGH$
+ case HEADERS:
+ final Header[] headers = AbstractMessageParser.parseHeaders(
+ this.sessionBuffer,
+ this.messageConstraints.getMaxHeaderCount(),
+ this.messageConstraints.getMaxLineLength(),
+ this.lineParser,
+ this.headerLines);
+ this.message.setHeaders(headers);
+ final T result = this.message;
+ this.message = null;
+ this.headerLines.clear();
+ this.state = HEAD_LINE;
+ return result;
+ default:
+ throw new IllegalStateException("Inconsistent parser state");
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/AbstractMessageWriter.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/AbstractMessageWriter.java
new file mode 100644
index 0000000000..56caf58d53
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/AbstractMessageWriter.java
@@ -0,0 +1,119 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderIterator;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpMessage;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.io.HttpMessageWriter;
+import ch.boye.httpclientandroidlib.io.SessionOutputBuffer;
+import ch.boye.httpclientandroidlib.message.BasicLineFormatter;
+import ch.boye.httpclientandroidlib.message.LineFormatter;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Abstract base class for HTTP message writers that serialize output to
+ * an instance of {@link SessionOutputBuffer}.
+ *
+ * @since 4.0
+ */
+@SuppressWarnings("deprecation")
+@NotThreadSafe
+public abstract class AbstractMessageWriter<T extends HttpMessage> implements HttpMessageWriter<T> {
+
+ protected final SessionOutputBuffer sessionBuffer;
+ protected final CharArrayBuffer lineBuf;
+ protected final LineFormatter lineFormatter;
+
+ /**
+ * Creates an instance of AbstractMessageWriter.
+ *
+ * @param buffer the session output buffer.
+ * @param formatter the line formatter.
+ * @param params HTTP parameters.
+ *
+ * @deprecated (4.3) use
+ * {@link AbstractMessageWriter#AbstractMessageWriter(SessionOutputBuffer, LineFormatter)}
+ */
+ @Deprecated
+ public AbstractMessageWriter(final SessionOutputBuffer buffer,
+ final LineFormatter formatter,
+ final HttpParams params) {
+ super();
+ Args.notNull(buffer, "Session input buffer");
+ this.sessionBuffer = buffer;
+ this.lineBuf = new CharArrayBuffer(128);
+ this.lineFormatter = (formatter != null) ? formatter : BasicLineFormatter.INSTANCE;
+ }
+
+ /**
+ * Creates an instance of AbstractMessageWriter.
+ *
+ * @param buffer the session output buffer.
+ * @param formatter the line formatter If <code>null</code> {@link BasicLineFormatter#INSTANCE}
+ * will be used.
+ *
+ * @since 4.3
+ */
+ public AbstractMessageWriter(
+ final SessionOutputBuffer buffer,
+ final LineFormatter formatter) {
+ super();
+ this.sessionBuffer = Args.notNull(buffer, "Session input buffer");
+ this.lineFormatter = (formatter != null) ? formatter : BasicLineFormatter.INSTANCE;
+ this.lineBuf = new CharArrayBuffer(128);
+ }
+
+ /**
+ * Subclasses must override this method to write out the first header line
+ * based on the {@link HttpMessage} passed as a parameter.
+ *
+ * @param message the message whose first line is to be written out.
+ * @throws IOException in case of an I/O error.
+ */
+ protected abstract void writeHeadLine(T message) throws IOException;
+
+ public void write(final T message) throws IOException, HttpException {
+ Args.notNull(message, "HTTP message");
+ writeHeadLine(message);
+ for (final HeaderIterator it = message.headerIterator(); it.hasNext(); ) {
+ final Header header = it.nextHeader();
+ this.sessionBuffer.writeLine
+ (lineFormatter.formatHeader(this.lineBuf, header));
+ }
+ this.lineBuf.clear();
+ this.sessionBuffer.writeLine(this.lineBuf);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/AbstractSessionInputBuffer.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/AbstractSessionInputBuffer.java
new file mode 100644
index 0000000000..e53e07a309
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/AbstractSessionInputBuffer.java
@@ -0,0 +1,401 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
+import java.nio.charset.CodingErrorAction;
+
+import ch.boye.httpclientandroidlib.Consts;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.io.BufferInfo;
+import ch.boye.httpclientandroidlib.io.HttpTransportMetrics;
+import ch.boye.httpclientandroidlib.io.SessionInputBuffer;
+import ch.boye.httpclientandroidlib.params.CoreConnectionPNames;
+import ch.boye.httpclientandroidlib.params.CoreProtocolPNames;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.ByteArrayBuffer;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Abstract base class for session input buffers that stream data from
+ * an arbitrary {@link InputStream}. This class buffers input data in
+ * an internal byte array for optimal input performance.
+ * <p>
+ * {@link #readLine(CharArrayBuffer)} and {@link #readLine()} methods of this
+ * class treat a lone LF as valid line delimiters in addition to CR-LF required
+ * by the HTTP specification.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link SessionInputBufferImpl}
+ */
+@NotThreadSafe
+@Deprecated
+public abstract class AbstractSessionInputBuffer implements SessionInputBuffer, BufferInfo {
+
+ private InputStream instream;
+ private byte[] buffer;
+ private ByteArrayBuffer linebuffer;
+ private Charset charset;
+ private boolean ascii;
+ private int maxLineLen;
+ private int minChunkLimit;
+ private HttpTransportMetricsImpl metrics;
+ private CodingErrorAction onMalformedCharAction;
+ private CodingErrorAction onUnmappableCharAction;
+
+ private int bufferpos;
+ private int bufferlen;
+ private CharsetDecoder decoder;
+ private CharBuffer cbuf;
+
+ public AbstractSessionInputBuffer() {
+ }
+
+ /**
+ * Initializes this session input buffer.
+ *
+ * @param instream the source input stream.
+ * @param buffersize the size of the internal buffer.
+ * @param params HTTP parameters.
+ */
+ protected void init(final InputStream instream, final int buffersize, final HttpParams params) {
+ Args.notNull(instream, "Input stream");
+ Args.notNegative(buffersize, "Buffer size");
+ Args.notNull(params, "HTTP parameters");
+ this.instream = instream;
+ this.buffer = new byte[buffersize];
+ this.bufferpos = 0;
+ this.bufferlen = 0;
+ this.linebuffer = new ByteArrayBuffer(buffersize);
+ final String charset = (String) params.getParameter(CoreProtocolPNames.HTTP_ELEMENT_CHARSET);
+ this.charset = charset != null ? Charset.forName(charset) : Consts.ASCII;
+ this.ascii = this.charset.equals(Consts.ASCII);
+ this.decoder = null;
+ this.maxLineLen = params.getIntParameter(CoreConnectionPNames.MAX_LINE_LENGTH, -1);
+ this.minChunkLimit = params.getIntParameter(CoreConnectionPNames.MIN_CHUNK_LIMIT, 512);
+ this.metrics = createTransportMetrics();
+ final CodingErrorAction a1 = (CodingErrorAction) params.getParameter(
+ CoreProtocolPNames.HTTP_MALFORMED_INPUT_ACTION);
+ this.onMalformedCharAction = a1 != null ? a1 : CodingErrorAction.REPORT;
+ final CodingErrorAction a2 = (CodingErrorAction) params.getParameter(
+ CoreProtocolPNames.HTTP_UNMAPPABLE_INPUT_ACTION);
+ this.onUnmappableCharAction = a2 != null? a2 : CodingErrorAction.REPORT;
+ }
+
+ /**
+ * @since 4.1
+ */
+ protected HttpTransportMetricsImpl createTransportMetrics() {
+ return new HttpTransportMetricsImpl();
+ }
+
+ /**
+ * @since 4.1
+ */
+ public int capacity() {
+ return this.buffer.length;
+ }
+
+ /**
+ * @since 4.1
+ */
+ public int length() {
+ return this.bufferlen - this.bufferpos;
+ }
+
+ /**
+ * @since 4.1
+ */
+ public int available() {
+ return capacity() - length();
+ }
+
+ protected int fillBuffer() throws IOException {
+ // compact the buffer if necessary
+ if (this.bufferpos > 0) {
+ final int len = this.bufferlen - this.bufferpos;
+ if (len > 0) {
+ System.arraycopy(this.buffer, this.bufferpos, this.buffer, 0, len);
+ }
+ this.bufferpos = 0;
+ this.bufferlen = len;
+ }
+ final int l;
+ final int off = this.bufferlen;
+ final int len = this.buffer.length - off;
+ l = this.instream.read(this.buffer, off, len);
+ if (l == -1) {
+ return -1;
+ } else {
+ this.bufferlen = off + l;
+ this.metrics.incrementBytesTransferred(l);
+ return l;
+ }
+ }
+
+ protected boolean hasBufferedData() {
+ return this.bufferpos < this.bufferlen;
+ }
+
+ public int read() throws IOException {
+ int noRead;
+ while (!hasBufferedData()) {
+ noRead = fillBuffer();
+ if (noRead == -1) {
+ return -1;
+ }
+ }
+ return this.buffer[this.bufferpos++] & 0xff;
+ }
+
+ public int read(final byte[] b, final int off, final int len) throws IOException {
+ if (b == null) {
+ return 0;
+ }
+ if (hasBufferedData()) {
+ final int chunk = Math.min(len, this.bufferlen - this.bufferpos);
+ System.arraycopy(this.buffer, this.bufferpos, b, off, chunk);
+ this.bufferpos += chunk;
+ return chunk;
+ }
+ // If the remaining capacity is big enough, read directly from the
+ // underlying input stream bypassing the buffer.
+ if (len > this.minChunkLimit) {
+ final int read = this.instream.read(b, off, len);
+ if (read > 0) {
+ this.metrics.incrementBytesTransferred(read);
+ }
+ return read;
+ } else {
+ // otherwise read to the buffer first
+ while (!hasBufferedData()) {
+ final int noRead = fillBuffer();
+ if (noRead == -1) {
+ return -1;
+ }
+ }
+ final int chunk = Math.min(len, this.bufferlen - this.bufferpos);
+ System.arraycopy(this.buffer, this.bufferpos, b, off, chunk);
+ this.bufferpos += chunk;
+ return chunk;
+ }
+ }
+
+ public int read(final byte[] b) throws IOException {
+ if (b == null) {
+ return 0;
+ }
+ return read(b, 0, b.length);
+ }
+
+ private int locateLF() {
+ for (int i = this.bufferpos; i < this.bufferlen; i++) {
+ if (this.buffer[i] == HTTP.LF) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Reads a complete line of characters up to a line delimiter from this
+ * session buffer into the given line buffer. The number of chars actually
+ * read is returned as an integer. The line delimiter itself is discarded.
+ * If no char is available because the end of the stream has been reached,
+ * the value <code>-1</code> is returned. This method blocks until input
+ * data is available, end of file is detected, or an exception is thrown.
+ * <p>
+ * This method treats a lone LF as a valid line delimiters in addition
+ * to CR-LF required by the HTTP specification.
+ *
+ * @param charbuffer the line buffer.
+ * @return one line of characters
+ * @exception IOException if an I/O error occurs.
+ */
+ public int readLine(final CharArrayBuffer charbuffer) throws IOException {
+ Args.notNull(charbuffer, "Char array buffer");
+ int noRead = 0;
+ boolean retry = true;
+ while (retry) {
+ // attempt to find end of line (LF)
+ final int i = locateLF();
+ if (i != -1) {
+ // end of line found.
+ if (this.linebuffer.isEmpty()) {
+ // the entire line is preset in the read buffer
+ return lineFromReadBuffer(charbuffer, i);
+ }
+ retry = false;
+ final int len = i + 1 - this.bufferpos;
+ this.linebuffer.append(this.buffer, this.bufferpos, len);
+ this.bufferpos = i + 1;
+ } else {
+ // end of line not found
+ if (hasBufferedData()) {
+ final int len = this.bufferlen - this.bufferpos;
+ this.linebuffer.append(this.buffer, this.bufferpos, len);
+ this.bufferpos = this.bufferlen;
+ }
+ noRead = fillBuffer();
+ if (noRead == -1) {
+ retry = false;
+ }
+ }
+ if (this.maxLineLen > 0 && this.linebuffer.length() >= this.maxLineLen) {
+ throw new IOException("Maximum line length limit exceeded");
+ }
+ }
+ if (noRead == -1 && this.linebuffer.isEmpty()) {
+ // indicate the end of stream
+ return -1;
+ }
+ return lineFromLineBuffer(charbuffer);
+ }
+
+ /**
+ * Reads a complete line of characters up to a line delimiter from this
+ * session buffer. The line delimiter itself is discarded. If no char is
+ * available because the end of the stream has been reached,
+ * <code>null</code> is returned. This method blocks until input data is
+ * available, end of file is detected, or an exception is thrown.
+ * <p>
+ * This method treats a lone LF as a valid line delimiters in addition
+ * to CR-LF required by the HTTP specification.
+ *
+ * @return HTTP line as a string
+ * @exception IOException if an I/O error occurs.
+ */
+ private int lineFromLineBuffer(final CharArrayBuffer charbuffer)
+ throws IOException {
+ // discard LF if found
+ int len = this.linebuffer.length();
+ if (len > 0) {
+ if (this.linebuffer.byteAt(len - 1) == HTTP.LF) {
+ len--;
+ }
+ // discard CR if found
+ if (len > 0) {
+ if (this.linebuffer.byteAt(len - 1) == HTTP.CR) {
+ len--;
+ }
+ }
+ }
+ if (this.ascii) {
+ charbuffer.append(this.linebuffer, 0, len);
+ } else {
+ final ByteBuffer bbuf = ByteBuffer.wrap(this.linebuffer.buffer(), 0, len);
+ len = appendDecoded(charbuffer, bbuf);
+ }
+ this.linebuffer.clear();
+ return len;
+ }
+
+ private int lineFromReadBuffer(final CharArrayBuffer charbuffer, final int position)
+ throws IOException {
+ final int off = this.bufferpos;
+ int i = position;
+ this.bufferpos = i + 1;
+ if (i > off && this.buffer[i - 1] == HTTP.CR) {
+ // skip CR if found
+ i--;
+ }
+ int len = i - off;
+ if (this.ascii) {
+ charbuffer.append(this.buffer, off, len);
+ } else {
+ final ByteBuffer bbuf = ByteBuffer.wrap(this.buffer, off, len);
+ len = appendDecoded(charbuffer, bbuf);
+ }
+ return len;
+ }
+
+ private int appendDecoded(
+ final CharArrayBuffer charbuffer, final ByteBuffer bbuf) throws IOException {
+ if (!bbuf.hasRemaining()) {
+ return 0;
+ }
+ if (this.decoder == null) {
+ this.decoder = this.charset.newDecoder();
+ this.decoder.onMalformedInput(this.onMalformedCharAction);
+ this.decoder.onUnmappableCharacter(this.onUnmappableCharAction);
+ }
+ if (this.cbuf == null) {
+ this.cbuf = CharBuffer.allocate(1024);
+ }
+ this.decoder.reset();
+ int len = 0;
+ while (bbuf.hasRemaining()) {
+ final CoderResult result = this.decoder.decode(bbuf, this.cbuf, true);
+ len += handleDecodingResult(result, charbuffer, bbuf);
+ }
+ final CoderResult result = this.decoder.flush(this.cbuf);
+ len += handleDecodingResult(result, charbuffer, bbuf);
+ this.cbuf.clear();
+ return len;
+ }
+
+ private int handleDecodingResult(
+ final CoderResult result,
+ final CharArrayBuffer charbuffer,
+ final ByteBuffer bbuf) throws IOException {
+ if (result.isError()) {
+ result.throwException();
+ }
+ this.cbuf.flip();
+ final int len = this.cbuf.remaining();
+ while (this.cbuf.hasRemaining()) {
+ charbuffer.append(this.cbuf.get());
+ }
+ this.cbuf.compact();
+ return len;
+ }
+
+ public String readLine() throws IOException {
+ final CharArrayBuffer charbuffer = new CharArrayBuffer(64);
+ final int l = readLine(charbuffer);
+ if (l != -1) {
+ return charbuffer.toString();
+ } else {
+ return null;
+ }
+ }
+
+ public HttpTransportMetrics getMetrics() {
+ return this.metrics;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/AbstractSessionOutputBuffer.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/AbstractSessionOutputBuffer.java
new file mode 100644
index 0000000000..a2c87d0848
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/AbstractSessionOutputBuffer.java
@@ -0,0 +1,307 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CoderResult;
+import java.nio.charset.CodingErrorAction;
+
+import ch.boye.httpclientandroidlib.Consts;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.io.BufferInfo;
+import ch.boye.httpclientandroidlib.io.HttpTransportMetrics;
+import ch.boye.httpclientandroidlib.io.SessionOutputBuffer;
+import ch.boye.httpclientandroidlib.params.CoreConnectionPNames;
+import ch.boye.httpclientandroidlib.params.CoreProtocolPNames;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.ByteArrayBuffer;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Abstract base class for session output buffers that stream data to
+ * an arbitrary {@link OutputStream}. This class buffers small chunks of
+ * output data in an internal byte array for optimal output performance.
+ * </p>
+ * {@link #writeLine(CharArrayBuffer)} and {@link #writeLine(String)} methods
+ * of this class use CR-LF as a line delimiter.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link SessionOutputBufferImpl}
+ */
+@NotThreadSafe
+@Deprecated
+public abstract class AbstractSessionOutputBuffer implements SessionOutputBuffer, BufferInfo {
+
+ private static final byte[] CRLF = new byte[] {HTTP.CR, HTTP.LF};
+
+ private OutputStream outstream;
+ private ByteArrayBuffer buffer;
+ private Charset charset;
+ private boolean ascii;
+ private int minChunkLimit;
+ private HttpTransportMetricsImpl metrics;
+ private CodingErrorAction onMalformedCharAction;
+ private CodingErrorAction onUnmappableCharAction;
+
+ private CharsetEncoder encoder;
+ private ByteBuffer bbuf;
+
+ protected AbstractSessionOutputBuffer(
+ final OutputStream outstream,
+ final int buffersize,
+ final Charset charset,
+ final int minChunkLimit,
+ final CodingErrorAction malformedCharAction,
+ final CodingErrorAction unmappableCharAction) {
+ super();
+ Args.notNull(outstream, "Input stream");
+ Args.notNegative(buffersize, "Buffer size");
+ this.outstream = outstream;
+ this.buffer = new ByteArrayBuffer(buffersize);
+ this.charset = charset != null ? charset : Consts.ASCII;
+ this.ascii = this.charset.equals(Consts.ASCII);
+ this.encoder = null;
+ this.minChunkLimit = minChunkLimit >= 0 ? minChunkLimit : 512;
+ this.metrics = createTransportMetrics();
+ this.onMalformedCharAction = malformedCharAction != null ? malformedCharAction :
+ CodingErrorAction.REPORT;
+ this.onUnmappableCharAction = unmappableCharAction != null? unmappableCharAction :
+ CodingErrorAction.REPORT;
+ }
+
+ public AbstractSessionOutputBuffer() {
+ }
+
+ protected void init(final OutputStream outstream, final int buffersize, final HttpParams params) {
+ Args.notNull(outstream, "Input stream");
+ Args.notNegative(buffersize, "Buffer size");
+ Args.notNull(params, "HTTP parameters");
+ this.outstream = outstream;
+ this.buffer = new ByteArrayBuffer(buffersize);
+ final String charset = (String) params.getParameter(CoreProtocolPNames.HTTP_ELEMENT_CHARSET);
+ this.charset = charset != null ? Charset.forName(charset) : Consts.ASCII;
+ this.ascii = this.charset.equals(Consts.ASCII);
+ this.encoder = null;
+ this.minChunkLimit = params.getIntParameter(CoreConnectionPNames.MIN_CHUNK_LIMIT, 512);
+ this.metrics = createTransportMetrics();
+ final CodingErrorAction a1 = (CodingErrorAction) params.getParameter(
+ CoreProtocolPNames.HTTP_MALFORMED_INPUT_ACTION);
+ this.onMalformedCharAction = a1 != null ? a1 : CodingErrorAction.REPORT;
+ final CodingErrorAction a2 = (CodingErrorAction) params.getParameter(
+ CoreProtocolPNames.HTTP_UNMAPPABLE_INPUT_ACTION);
+ this.onUnmappableCharAction = a2 != null? a2 : CodingErrorAction.REPORT;
+ }
+
+ /**
+ * @since 4.1
+ */
+ protected HttpTransportMetricsImpl createTransportMetrics() {
+ return new HttpTransportMetricsImpl();
+ }
+
+ /**
+ * @since 4.1
+ */
+ public int capacity() {
+ return this.buffer.capacity();
+ }
+
+ /**
+ * @since 4.1
+ */
+ public int length() {
+ return this.buffer.length();
+ }
+
+ /**
+ * @since 4.1
+ */
+ public int available() {
+ return capacity() - length();
+ }
+
+ protected void flushBuffer() throws IOException {
+ final int len = this.buffer.length();
+ if (len > 0) {
+ this.outstream.write(this.buffer.buffer(), 0, len);
+ this.buffer.clear();
+ this.metrics.incrementBytesTransferred(len);
+ }
+ }
+
+ public void flush() throws IOException {
+ flushBuffer();
+ this.outstream.flush();
+ }
+
+ public void write(final byte[] b, final int off, final int len) throws IOException {
+ if (b == null) {
+ return;
+ }
+ // Do not want to buffer large-ish chunks
+ // if the byte array is larger then MIN_CHUNK_LIMIT
+ // write it directly to the output stream
+ if (len > this.minChunkLimit || len > this.buffer.capacity()) {
+ // flush the buffer
+ flushBuffer();
+ // write directly to the out stream
+ this.outstream.write(b, off, len);
+ this.metrics.incrementBytesTransferred(len);
+ } else {
+ // Do not let the buffer grow unnecessarily
+ final int freecapacity = this.buffer.capacity() - this.buffer.length();
+ if (len > freecapacity) {
+ // flush the buffer
+ flushBuffer();
+ }
+ // buffer
+ this.buffer.append(b, off, len);
+ }
+ }
+
+ public void write(final byte[] b) throws IOException {
+ if (b == null) {
+ return;
+ }
+ write(b, 0, b.length);
+ }
+
+ public void write(final int b) throws IOException {
+ if (this.buffer.isFull()) {
+ flushBuffer();
+ }
+ this.buffer.append(b);
+ }
+
+ /**
+ * Writes characters from the specified string followed by a line delimiter
+ * to this session buffer.
+ * <p>
+ * This method uses CR-LF as a line delimiter.
+ *
+ * @param s the line.
+ * @exception IOException if an I/O error occurs.
+ */
+ public void writeLine(final String s) throws IOException {
+ if (s == null) {
+ return;
+ }
+ if (s.length() > 0) {
+ if (this.ascii) {
+ for (int i = 0; i < s.length(); i++) {
+ write(s.charAt(i));
+ }
+ } else {
+ final CharBuffer cbuf = CharBuffer.wrap(s);
+ writeEncoded(cbuf);
+ }
+ }
+ write(CRLF);
+ }
+
+ /**
+ * Writes characters from the specified char array followed by a line
+ * delimiter to this session buffer.
+ * <p>
+ * This method uses CR-LF as a line delimiter.
+ *
+ * @param charbuffer the buffer containing chars of the line.
+ * @exception IOException if an I/O error occurs.
+ */
+ public void writeLine(final CharArrayBuffer charbuffer) throws IOException {
+ if (charbuffer == null) {
+ return;
+ }
+ if (this.ascii) {
+ int off = 0;
+ int remaining = charbuffer.length();
+ while (remaining > 0) {
+ int chunk = this.buffer.capacity() - this.buffer.length();
+ chunk = Math.min(chunk, remaining);
+ if (chunk > 0) {
+ this.buffer.append(charbuffer, off, chunk);
+ }
+ if (this.buffer.isFull()) {
+ flushBuffer();
+ }
+ off += chunk;
+ remaining -= chunk;
+ }
+ } else {
+ final CharBuffer cbuf = CharBuffer.wrap(charbuffer.buffer(), 0, charbuffer.length());
+ writeEncoded(cbuf);
+ }
+ write(CRLF);
+ }
+
+ private void writeEncoded(final CharBuffer cbuf) throws IOException {
+ if (!cbuf.hasRemaining()) {
+ return;
+ }
+ if (this.encoder == null) {
+ this.encoder = this.charset.newEncoder();
+ this.encoder.onMalformedInput(this.onMalformedCharAction);
+ this.encoder.onUnmappableCharacter(this.onUnmappableCharAction);
+ }
+ if (this.bbuf == null) {
+ this.bbuf = ByteBuffer.allocate(1024);
+ }
+ this.encoder.reset();
+ while (cbuf.hasRemaining()) {
+ final CoderResult result = this.encoder.encode(cbuf, this.bbuf, true);
+ handleEncodingResult(result);
+ }
+ final CoderResult result = this.encoder.flush(this.bbuf);
+ handleEncodingResult(result);
+ this.bbuf.clear();
+ }
+
+ private void handleEncodingResult(final CoderResult result) throws IOException {
+ if (result.isError()) {
+ result.throwException();
+ }
+ this.bbuf.flip();
+ while (this.bbuf.hasRemaining()) {
+ write(this.bbuf.get());
+ }
+ this.bbuf.compact();
+ }
+
+ public HttpTransportMetrics getMetrics() {
+ return this.metrics;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/ChunkedInputStream.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/ChunkedInputStream.java
new file mode 100644
index 0000000000..447bd676d7
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/ChunkedInputStream.java
@@ -0,0 +1,301 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.MalformedChunkCodingException;
+import ch.boye.httpclientandroidlib.TruncatedChunkException;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.io.BufferInfo;
+import ch.boye.httpclientandroidlib.io.SessionInputBuffer;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Implements chunked transfer coding. The content is received in small chunks.
+ * Entities transferred using this input stream can be of unlimited length.
+ * After the stream is read to the end, it provides access to the trailers,
+ * if any.
+ * <p>
+ * Note that this class NEVER closes the underlying stream, even when close
+ * gets called. Instead, it will read until the "end" of its chunking on
+ * close, which allows for the seamless execution of subsequent HTTP 1.1
+ * requests, while not requiring the client to remember to read the entire
+ * contents of the response.
+ *
+ *
+ * @since 4.0
+ *
+ */
+@NotThreadSafe
+public class ChunkedInputStream extends InputStream {
+
+ private static final int CHUNK_LEN = 1;
+ private static final int CHUNK_DATA = 2;
+ private static final int CHUNK_CRLF = 3;
+
+ private static final int BUFFER_SIZE = 2048;
+
+ /** The session input buffer */
+ private final SessionInputBuffer in;
+
+ private final CharArrayBuffer buffer;
+
+ private int state;
+
+ /** The chunk size */
+ private int chunkSize;
+
+ /** The current position within the current chunk */
+ private int pos;
+
+ /** True if we've reached the end of stream */
+ private boolean eof = false;
+
+ /** True if this stream is closed */
+ private boolean closed = false;
+
+ private Header[] footers = new Header[] {};
+
+ /**
+ * Wraps session input stream and reads chunk coded input.
+ *
+ * @param in The session input buffer
+ */
+ public ChunkedInputStream(final SessionInputBuffer in) {
+ super();
+ this.in = Args.notNull(in, "Session input buffer");
+ this.pos = 0;
+ this.buffer = new CharArrayBuffer(16);
+ this.state = CHUNK_LEN;
+ }
+
+ @Override
+ public int available() throws IOException {
+ if (this.in instanceof BufferInfo) {
+ final int len = ((BufferInfo) this.in).length();
+ return Math.min(len, this.chunkSize - this.pos);
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * <p> Returns all the data in a chunked stream in coalesced form. A chunk
+ * is followed by a CRLF. The method returns -1 as soon as a chunksize of 0
+ * is detected.</p>
+ *
+ * <p> Trailer headers are read automatically at the end of the stream and
+ * can be obtained with the getResponseFooters() method.</p>
+ *
+ * @return -1 of the end of the stream has been reached or the next data
+ * byte
+ * @throws IOException in case of an I/O error
+ */
+ @Override
+ public int read() throws IOException {
+ if (this.closed) {
+ throw new IOException("Attempted read from closed stream.");
+ }
+ if (this.eof) {
+ return -1;
+ }
+ if (state != CHUNK_DATA) {
+ nextChunk();
+ if (this.eof) {
+ return -1;
+ }
+ }
+ final int b = in.read();
+ if (b != -1) {
+ pos++;
+ if (pos >= chunkSize) {
+ state = CHUNK_CRLF;
+ }
+ }
+ return b;
+ }
+
+ /**
+ * Read some bytes from the stream.
+ * @param b The byte array that will hold the contents from the stream.
+ * @param off The offset into the byte array at which bytes will start to be
+ * placed.
+ * @param len the maximum number of bytes that can be returned.
+ * @return The number of bytes returned or -1 if the end of stream has been
+ * reached.
+ * @throws IOException in case of an I/O error
+ */
+ @Override
+ public int read (final byte[] b, final int off, final int len) throws IOException {
+
+ if (closed) {
+ throw new IOException("Attempted read from closed stream.");
+ }
+
+ if (eof) {
+ return -1;
+ }
+ if (state != CHUNK_DATA) {
+ nextChunk();
+ if (eof) {
+ return -1;
+ }
+ }
+ final int bytesRead = in.read(b, off, Math.min(len, chunkSize - pos));
+ if (bytesRead != -1) {
+ pos += bytesRead;
+ if (pos >= chunkSize) {
+ state = CHUNK_CRLF;
+ }
+ return bytesRead;
+ } else {
+ eof = true;
+ throw new TruncatedChunkException("Truncated chunk "
+ + "( expected size: " + chunkSize
+ + "; actual size: " + pos + ")");
+ }
+ }
+
+ /**
+ * Read some bytes from the stream.
+ * @param b The byte array that will hold the contents from the stream.
+ * @return The number of bytes returned or -1 if the end of stream has been
+ * reached.
+ * @throws IOException in case of an I/O error
+ */
+ @Override
+ public int read (final byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ /**
+ * Read the next chunk.
+ * @throws IOException in case of an I/O error
+ */
+ private void nextChunk() throws IOException {
+ chunkSize = getChunkSize();
+ if (chunkSize < 0) {
+ throw new MalformedChunkCodingException("Negative chunk size");
+ }
+ state = CHUNK_DATA;
+ pos = 0;
+ if (chunkSize == 0) {
+ eof = true;
+ parseTrailerHeaders();
+ }
+ }
+
+ /**
+ * Expects the stream to start with a chunksize in hex with optional
+ * comments after a semicolon. The line must end with a CRLF: "a3; some
+ * comment\r\n" Positions the stream at the start of the next line.
+ */
+ private int getChunkSize() throws IOException {
+ final int st = this.state;
+ switch (st) {
+ case CHUNK_CRLF:
+ this.buffer.clear();
+ final int bytesRead1 = this.in.readLine(this.buffer);
+ if (bytesRead1 == -1) {
+ return 0;
+ }
+ if (!this.buffer.isEmpty()) {
+ throw new MalformedChunkCodingException(
+ "Unexpected content at the end of chunk");
+ }
+ state = CHUNK_LEN;
+ //$FALL-THROUGH$
+ case CHUNK_LEN:
+ this.buffer.clear();
+ final int bytesRead2 = this.in.readLine(this.buffer);
+ if (bytesRead2 == -1) {
+ return 0;
+ }
+ int separator = this.buffer.indexOf(';');
+ if (separator < 0) {
+ separator = this.buffer.length();
+ }
+ try {
+ return Integer.parseInt(this.buffer.substringTrimmed(0, separator), 16);
+ } catch (final NumberFormatException e) {
+ throw new MalformedChunkCodingException("Bad chunk header");
+ }
+ default:
+ throw new IllegalStateException("Inconsistent codec state");
+ }
+ }
+
+ /**
+ * Reads and stores the Trailer headers.
+ * @throws IOException in case of an I/O error
+ */
+ private void parseTrailerHeaders() throws IOException {
+ try {
+ this.footers = AbstractMessageParser.parseHeaders
+ (in, -1, -1, null);
+ } catch (final HttpException ex) {
+ final IOException ioe = new MalformedChunkCodingException("Invalid footer: "
+ + ex.getMessage());
+ ioe.initCause(ex);
+ throw ioe;
+ }
+ }
+
+ /**
+ * Upon close, this reads the remainder of the chunked message,
+ * leaving the underlying socket at a position to start reading the
+ * next response without scanning.
+ * @throws IOException in case of an I/O error
+ */
+ @Override
+ public void close() throws IOException {
+ if (!closed) {
+ try {
+ if (!eof) {
+ // read and discard the remainder of the message
+ final byte buff[] = new byte[BUFFER_SIZE];
+ while (read(buff) >= 0) {
+ }
+ }
+ } finally {
+ eof = true;
+ closed = true;
+ }
+ }
+ }
+
+ public Header[] getFooters() {
+ return this.footers.clone();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/ChunkedOutputStream.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/ChunkedOutputStream.java
new file mode 100644
index 0000000000..828df923c7
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/ChunkedOutputStream.java
@@ -0,0 +1,208 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.io.SessionOutputBuffer;
+
+/**
+ * Implements chunked transfer coding. The content is sent in small chunks.
+ * Entities transferred using this output stream can be of unlimited length.
+ * Writes are buffered to an internal buffer (2048 default size).
+ * <p>
+ * Note that this class NEVER closes the underlying stream, even when close
+ * gets called. Instead, the stream will be marked as closed and no further
+ * output will be permitted.
+ *
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class ChunkedOutputStream extends OutputStream {
+
+ // ----------------------------------------------------- Instance Variables
+ private final SessionOutputBuffer out;
+
+ private final byte[] cache;
+
+ private int cachePosition = 0;
+
+ private boolean wroteLastChunk = false;
+
+ /** True if the stream is closed. */
+ private boolean closed = false;
+
+ /**
+ * Wraps a session output buffer and chunk-encodes the output.
+ *
+ * @param out The session output buffer
+ * @param bufferSize The minimum chunk size (excluding last chunk)
+ * @throws IOException not thrown
+ *
+ * @deprecated (4.3) use {@link ChunkedOutputStream#ChunkedOutputStream(int, SessionOutputBuffer)}
+ */
+ @Deprecated
+ public ChunkedOutputStream(final SessionOutputBuffer out, final int bufferSize)
+ throws IOException {
+ this(bufferSize, out);
+ }
+
+ /**
+ * Wraps a session output buffer and chunks the output. The default buffer
+ * size of 2048 was chosen because the chunk overhead is less than 0.5%
+ *
+ * @param out the output buffer to wrap
+ * @throws IOException not thrown
+ *
+ * @deprecated (4.3) use {@link ChunkedOutputStream#ChunkedOutputStream(int, SessionOutputBuffer)}
+ */
+ @Deprecated
+ public ChunkedOutputStream(final SessionOutputBuffer out)
+ throws IOException {
+ this(2048, out);
+ }
+
+ /**
+ * Wraps a session output buffer and chunk-encodes the output.
+ *
+ * @param bufferSize The minimum chunk size (excluding last chunk)
+ * @param out The session output buffer
+ */
+ public ChunkedOutputStream(final int bufferSize, final SessionOutputBuffer out) {
+ super();
+ this.cache = new byte[bufferSize];
+ this.out = out;
+ }
+
+ /**
+ * Writes the cache out onto the underlying stream
+ */
+ protected void flushCache() throws IOException {
+ if (this.cachePosition > 0) {
+ this.out.writeLine(Integer.toHexString(this.cachePosition));
+ this.out.write(this.cache, 0, this.cachePosition);
+ this.out.writeLine("");
+ this.cachePosition = 0;
+ }
+ }
+
+ /**
+ * Writes the cache and bufferToAppend to the underlying stream
+ * as one large chunk
+ */
+ protected void flushCacheWithAppend(final byte bufferToAppend[], final int off, final int len) throws IOException {
+ this.out.writeLine(Integer.toHexString(this.cachePosition + len));
+ this.out.write(this.cache, 0, this.cachePosition);
+ this.out.write(bufferToAppend, off, len);
+ this.out.writeLine("");
+ this.cachePosition = 0;
+ }
+
+ protected void writeClosingChunk() throws IOException {
+ // Write the final chunk.
+ this.out.writeLine("0");
+ this.out.writeLine("");
+ }
+
+ // ----------------------------------------------------------- Public Methods
+ /**
+ * Must be called to ensure the internal cache is flushed and the closing
+ * chunk is written.
+ * @throws IOException in case of an I/O error
+ */
+ public void finish() throws IOException {
+ if (!this.wroteLastChunk) {
+ flushCache();
+ writeClosingChunk();
+ this.wroteLastChunk = true;
+ }
+ }
+
+ // -------------------------------------------- OutputStream Methods
+ @Override
+ public void write(final int b) throws IOException {
+ if (this.closed) {
+ throw new IOException("Attempted write to closed stream.");
+ }
+ this.cache[this.cachePosition] = (byte) b;
+ this.cachePosition++;
+ if (this.cachePosition == this.cache.length) {
+ flushCache();
+ }
+ }
+
+ /**
+ * Writes the array. If the array does not fit within the buffer, it is
+ * not split, but rather written out as one large chunk.
+ */
+ @Override
+ public void write(final byte b[]) throws IOException {
+ write(b, 0, b.length);
+ }
+
+ /**
+ * Writes the array. If the array does not fit within the buffer, it is
+ * not split, but rather written out as one large chunk.
+ */
+ @Override
+ public void write(final byte src[], final int off, final int len) throws IOException {
+ if (this.closed) {
+ throw new IOException("Attempted write to closed stream.");
+ }
+ if (len >= this.cache.length - this.cachePosition) {
+ flushCacheWithAppend(src, off, len);
+ } else {
+ System.arraycopy(src, off, cache, this.cachePosition, len);
+ this.cachePosition += len;
+ }
+ }
+
+ /**
+ * Flushes the content buffer and the underlying stream.
+ */
+ @Override
+ public void flush() throws IOException {
+ flushCache();
+ this.out.flush();
+ }
+
+ /**
+ * Finishes writing to the underlying stream, but does NOT close the underlying stream.
+ */
+ @Override
+ public void close() throws IOException {
+ if (!this.closed) {
+ this.closed = true;
+ finish();
+ this.out.flush();
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/ContentLengthInputStream.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/ContentLengthInputStream.java
new file mode 100644
index 0000000000..65a271cb8c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/ContentLengthInputStream.java
@@ -0,0 +1,232 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import ch.boye.httpclientandroidlib.ConnectionClosedException;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.io.BufferInfo;
+import ch.boye.httpclientandroidlib.io.SessionInputBuffer;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Input stream that cuts off after a defined number of bytes. This class
+ * is used to receive content of HTTP messages where the end of the content
+ * entity is determined by the value of the <code>Content-Length header</code>.
+ * Entities transferred using this stream can be maximum {@link Long#MAX_VALUE}
+ * long.
+ * <p>
+ * Note that this class NEVER closes the underlying stream, even when close
+ * gets called. Instead, it will read until the "end" of its limit on
+ * close, which allows for the seamless execution of subsequent HTTP 1.1
+ * requests, while not requiring the client to remember to read the entire
+ * contents of the response.
+ *
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class ContentLengthInputStream extends InputStream {
+
+ private static final int BUFFER_SIZE = 2048;
+ /**
+ * The maximum number of bytes that can be read from the stream. Subsequent
+ * read operations will return -1.
+ */
+ private final long contentLength;
+
+ /** The current position */
+ private long pos = 0;
+
+ /** True if the stream is closed. */
+ private boolean closed = false;
+
+ /**
+ * Wrapped input stream that all calls are delegated to.
+ */
+ private SessionInputBuffer in = null;
+
+ /**
+ * Wraps a session input buffer and cuts off output after a defined number
+ * of bytes.
+ *
+ * @param in The session input buffer
+ * @param contentLength The maximum number of bytes that can be read from
+ * the stream. Subsequent read operations will return -1.
+ */
+ public ContentLengthInputStream(final SessionInputBuffer in, final long contentLength) {
+ super();
+ this.in = Args.notNull(in, "Session input buffer");
+ this.contentLength = Args.notNegative(contentLength, "Content length");
+ }
+
+ /**
+ * <p>Reads until the end of the known length of content.</p>
+ *
+ * <p>Does not close the underlying socket input, but instead leaves it
+ * primed to parse the next response.</p>
+ * @throws IOException If an IO problem occurs.
+ */
+ @Override
+ public void close() throws IOException {
+ if (!closed) {
+ try {
+ if (pos < contentLength) {
+ final byte buffer[] = new byte[BUFFER_SIZE];
+ while (read(buffer) >= 0) {
+ }
+ }
+ } finally {
+ // close after above so that we don't throw an exception trying
+ // to read after closed!
+ closed = true;
+ }
+ }
+ }
+
+ @Override
+ public int available() throws IOException {
+ if (this.in instanceof BufferInfo) {
+ final int len = ((BufferInfo) this.in).length();
+ return Math.min(len, (int) (this.contentLength - this.pos));
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Read the next byte from the stream
+ * @return The next byte or -1 if the end of stream has been reached.
+ * @throws IOException If an IO problem occurs
+ * @see java.io.InputStream#read()
+ */
+ @Override
+ public int read() throws IOException {
+ if (closed) {
+ throw new IOException("Attempted read from closed stream.");
+ }
+
+ if (pos >= contentLength) {
+ return -1;
+ }
+ final int b = this.in.read();
+ if (b == -1) {
+ if (pos < contentLength) {
+ throw new ConnectionClosedException(
+ "Premature end of Content-Length delimited message body (expected: "
+ + contentLength + "; received: " + pos);
+ }
+ } else {
+ pos++;
+ }
+ return b;
+ }
+
+ /**
+ * Does standard {@link InputStream#read(byte[], int, int)} behavior, but
+ * also notifies the watcher when the contents have been consumed.
+ *
+ * @param b The byte array to fill.
+ * @param off Start filling at this position.
+ * @param len The number of bytes to attempt to read.
+ * @return The number of bytes read, or -1 if the end of content has been
+ * reached.
+ *
+ * @throws java.io.IOException Should an error occur on the wrapped stream.
+ */
+ @Override
+ public int read (final byte[] b, final int off, final int len) throws java.io.IOException {
+ if (closed) {
+ throw new IOException("Attempted read from closed stream.");
+ }
+
+ if (pos >= contentLength) {
+ return -1;
+ }
+
+ int chunk = len;
+ if (pos + len > contentLength) {
+ chunk = (int) (contentLength - pos);
+ }
+ final int count = this.in.read(b, off, chunk);
+ if (count == -1 && pos < contentLength) {
+ throw new ConnectionClosedException(
+ "Premature end of Content-Length delimited message body (expected: "
+ + contentLength + "; received: " + pos);
+ }
+ if (count > 0) {
+ pos += count;
+ }
+ return count;
+ }
+
+
+ /**
+ * Read more bytes from the stream.
+ * @param b The byte array to put the new data in.
+ * @return The number of bytes read into the buffer.
+ * @throws IOException If an IO problem occurs
+ * @see java.io.InputStream#read(byte[])
+ */
+ @Override
+ public int read(final byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ /**
+ * Skips and discards a number of bytes from the input stream.
+ * @param n The number of bytes to skip.
+ * @return The actual number of bytes skipped. <= 0 if no bytes
+ * are skipped.
+ * @throws IOException If an error occurs while skipping bytes.
+ * @see InputStream#skip(long)
+ */
+ @Override
+ public long skip(final long n) throws IOException {
+ if (n <= 0) {
+ return 0;
+ }
+ final byte[] buffer = new byte[BUFFER_SIZE];
+ // make sure we don't skip more bytes than are
+ // still available
+ long remaining = Math.min(n, this.contentLength - this.pos);
+ // skip and keep track of the bytes actually skipped
+ long count = 0;
+ while (remaining > 0) {
+ final int l = read(buffer, 0, (int)Math.min(BUFFER_SIZE, remaining));
+ if (l == -1) {
+ break;
+ }
+ count += l;
+ remaining -= l;
+ }
+ return count;
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/ContentLengthOutputStream.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/ContentLengthOutputStream.java
new file mode 100644
index 0000000000..1c4e588d01
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/ContentLengthOutputStream.java
@@ -0,0 +1,136 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.io.SessionOutputBuffer;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Output stream that cuts off after a defined number of bytes. This class
+ * is used to send content of HTTP messages where the end of the content entity
+ * is determined by the value of the <code>Content-Length header</code>.
+ * Entities transferred using this stream can be maximum {@link Long#MAX_VALUE}
+ * long.
+ * <p>
+ * Note that this class NEVER closes the underlying stream, even when close
+ * gets called. Instead, the stream will be marked as closed and no further
+ * output will be permitted.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class ContentLengthOutputStream extends OutputStream {
+
+ /**
+ * Wrapped session output buffer.
+ */
+ private final SessionOutputBuffer out;
+
+ /**
+ * The maximum number of bytes that can be written the stream. Subsequent
+ * write operations will be ignored.
+ */
+ private final long contentLength;
+
+ /** Total bytes written */
+ private long total = 0;
+
+ /** True if the stream is closed. */
+ private boolean closed = false;
+
+ /**
+ * Wraps a session output buffer and cuts off output after a defined number
+ * of bytes.
+ *
+ * @param out The session output buffer
+ * @param contentLength The maximum number of bytes that can be written to
+ * the stream. Subsequent write operations will be ignored.
+ *
+ * @since 4.0
+ */
+ public ContentLengthOutputStream(final SessionOutputBuffer out, final long contentLength) {
+ super();
+ this.out = Args.notNull(out, "Session output buffer");
+ this.contentLength = Args.notNegative(contentLength, "Content length");
+ }
+
+ /**
+ * <p>Does not close the underlying socket output.</p>
+ *
+ * @throws IOException If an I/O problem occurs.
+ */
+ @Override
+ public void close() throws IOException {
+ if (!this.closed) {
+ this.closed = true;
+ this.out.flush();
+ }
+ }
+
+ @Override
+ public void flush() throws IOException {
+ this.out.flush();
+ }
+
+ @Override
+ public void write(final byte[] b, final int off, final int len) throws IOException {
+ if (this.closed) {
+ throw new IOException("Attempted write to closed stream.");
+ }
+ if (this.total < this.contentLength) {
+ final long max = this.contentLength - this.total;
+ int chunk = len;
+ if (chunk > max) {
+ chunk = (int) max;
+ }
+ this.out.write(b, off, chunk);
+ this.total += chunk;
+ }
+ }
+
+ @Override
+ public void write(final byte[] b) throws IOException {
+ write(b, 0, b.length);
+ }
+
+ @Override
+ public void write(final int b) throws IOException {
+ if (this.closed) {
+ throw new IOException("Attempted write to closed stream.");
+ }
+ if (this.total < this.contentLength) {
+ this.out.write(b);
+ this.total++;
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestParser.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestParser.java
new file mode 100644
index 0000000000..70383a22e0
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestParser.java
@@ -0,0 +1,140 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.ConnectionClosedException;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpRequestFactory;
+import ch.boye.httpclientandroidlib.ParseException;
+import ch.boye.httpclientandroidlib.RequestLine;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.config.MessageConstraints;
+import ch.boye.httpclientandroidlib.impl.DefaultHttpRequestFactory;
+import ch.boye.httpclientandroidlib.io.SessionInputBuffer;
+import ch.boye.httpclientandroidlib.message.LineParser;
+import ch.boye.httpclientandroidlib.message.ParserCursor;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * HTTP request parser that obtain its input from an instance
+ * of {@link SessionInputBuffer}.
+ *
+ * @since 4.2
+ */
+@SuppressWarnings("deprecation")
+@NotThreadSafe
+public class DefaultHttpRequestParser extends AbstractMessageParser<HttpRequest> {
+
+ private final HttpRequestFactory requestFactory;
+ private final CharArrayBuffer lineBuf;
+
+ /**
+ * Creates an instance of this class.
+ *
+ * @param buffer the session input buffer.
+ * @param lineParser the line parser.
+ * @param requestFactory the factory to use to create
+ * {@link HttpRequest}s.
+ * @param params HTTP parameters.
+ *
+ * @deprecated (4.3) use
+ * {@link DefaultHttpRequestParser#DefaultHttpRequestParser(SessionInputBuffer, LineParser,
+ * HttpRequestFactory, MessageConstraints)}
+ */
+ @Deprecated
+ public DefaultHttpRequestParser(
+ final SessionInputBuffer buffer,
+ final LineParser lineParser,
+ final HttpRequestFactory requestFactory,
+ final HttpParams params) {
+ super(buffer, lineParser, params);
+ this.requestFactory = Args.notNull(requestFactory, "Request factory");
+ this.lineBuf = new CharArrayBuffer(128);
+ }
+
+ /**
+ * Creates new instance of DefaultHttpRequestParser.
+ *
+ * @param buffer the session input buffer.
+ * @param lineParser the line parser. If <code>null</code>
+ * {@link ch.boye.httpclientandroidlib.message.BasicLineParser#INSTANCE} will be used.
+ * @param requestFactory the response factory. If <code>null</code>
+ * {@link DefaultHttpRequestFactory#INSTANCE} will be used.
+ * @param constraints the message constraints. If <code>null</code>
+ * {@link MessageConstraints#DEFAULT} will be used.
+ *
+ * @since 4.3
+ */
+ public DefaultHttpRequestParser(
+ final SessionInputBuffer buffer,
+ final LineParser lineParser,
+ final HttpRequestFactory requestFactory,
+ final MessageConstraints constraints) {
+ super(buffer, lineParser, constraints);
+ this.requestFactory = requestFactory != null ? requestFactory :
+ DefaultHttpRequestFactory.INSTANCE;
+ this.lineBuf = new CharArrayBuffer(128);
+ }
+
+ /**
+ * @since 4.3
+ */
+ public DefaultHttpRequestParser(
+ final SessionInputBuffer buffer,
+ final MessageConstraints constraints) {
+ this(buffer, null, null, constraints);
+ }
+
+ /**
+ * @since 4.3
+ */
+ public DefaultHttpRequestParser(final SessionInputBuffer buffer) {
+ this(buffer, null, null, MessageConstraints.DEFAULT);
+ }
+
+ @Override
+ protected HttpRequest parseHead(
+ final SessionInputBuffer sessionBuffer)
+ throws IOException, HttpException, ParseException {
+
+ this.lineBuf.clear();
+ final int i = sessionBuffer.readLine(this.lineBuf);
+ if (i == -1) {
+ throw new ConnectionClosedException("Client closed connection");
+ }
+ final ParserCursor cursor = new ParserCursor(0, this.lineBuf.length());
+ final RequestLine requestline = this.lineParser.parseRequestLine(this.lineBuf, cursor);
+ return this.requestFactory.newHttpRequest(requestline);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestParserFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestParserFactory.java
new file mode 100644
index 0000000000..81e8860596
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestParserFactory.java
@@ -0,0 +1,71 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpRequestFactory;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.config.MessageConstraints;
+import ch.boye.httpclientandroidlib.impl.DefaultHttpRequestFactory;
+import ch.boye.httpclientandroidlib.io.HttpMessageParser;
+import ch.boye.httpclientandroidlib.io.HttpMessageParserFactory;
+import ch.boye.httpclientandroidlib.io.SessionInputBuffer;
+import ch.boye.httpclientandroidlib.message.BasicLineParser;
+import ch.boye.httpclientandroidlib.message.LineParser;
+
+/**
+ * Default factory for request message parsers.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class DefaultHttpRequestParserFactory implements HttpMessageParserFactory<HttpRequest> {
+
+ public static final DefaultHttpRequestParserFactory INSTANCE = new DefaultHttpRequestParserFactory();
+
+ private final LineParser lineParser;
+ private final HttpRequestFactory requestFactory;
+
+ public DefaultHttpRequestParserFactory(final LineParser lineParser,
+ final HttpRequestFactory requestFactory) {
+ super();
+ this.lineParser = lineParser != null ? lineParser : BasicLineParser.INSTANCE;
+ this.requestFactory = requestFactory != null ? requestFactory
+ : DefaultHttpRequestFactory.INSTANCE;
+ }
+
+ public DefaultHttpRequestParserFactory() {
+ this(null, null);
+ }
+
+ public HttpMessageParser<HttpRequest> create(final SessionInputBuffer buffer,
+ final MessageConstraints constraints) {
+ return new DefaultHttpRequestParser(buffer, lineParser, requestFactory, constraints);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestWriter.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestWriter.java
new file mode 100644
index 0000000000..0f3b31e189
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestWriter.java
@@ -0,0 +1,69 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.io.SessionOutputBuffer;
+import ch.boye.httpclientandroidlib.message.LineFormatter;
+
+/**
+ * HTTP request writer that serializes its output to an instance of {@link SessionOutputBuffer}.
+ *
+ * @since 4.3
+ */
+@NotThreadSafe
+public class DefaultHttpRequestWriter extends AbstractMessageWriter<HttpRequest> {
+
+ /**
+ * Creates an instance of DefaultHttpRequestWriter.
+ *
+ * @param buffer the session output buffer.
+ * @param formatter the line formatter If <code>null</code>
+ * {@link ch.boye.httpclientandroidlib.message.BasicLineFormatter#INSTANCE}
+ * will be used.
+ */
+ public DefaultHttpRequestWriter(
+ final SessionOutputBuffer buffer,
+ final LineFormatter formatter) {
+ super(buffer, formatter);
+ }
+
+ public DefaultHttpRequestWriter(final SessionOutputBuffer buffer) {
+ this(buffer, null);
+ }
+
+ @Override
+ protected void writeHeadLine(final HttpRequest message) throws IOException {
+ lineFormatter.formatRequestLine(this.lineBuf, message.getRequestLine());
+ this.sessionBuffer.writeLine(this.lineBuf);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestWriterFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestWriterFactory.java
new file mode 100644
index 0000000000..410cc2ac31
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpRequestWriterFactory.java
@@ -0,0 +1,63 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.io.HttpMessageWriter;
+import ch.boye.httpclientandroidlib.io.HttpMessageWriterFactory;
+import ch.boye.httpclientandroidlib.io.SessionOutputBuffer;
+import ch.boye.httpclientandroidlib.message.BasicLineFormatter;
+import ch.boye.httpclientandroidlib.message.LineFormatter;
+
+/**
+ * Default factory for request message writers.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class DefaultHttpRequestWriterFactory implements HttpMessageWriterFactory<HttpRequest> {
+
+ public static final DefaultHttpRequestWriterFactory INSTANCE = new DefaultHttpRequestWriterFactory();
+
+ private final LineFormatter lineFormatter;
+
+ public DefaultHttpRequestWriterFactory(final LineFormatter lineFormatter) {
+ super();
+ this.lineFormatter = lineFormatter != null ? lineFormatter : BasicLineFormatter.INSTANCE;
+ }
+
+ public DefaultHttpRequestWriterFactory() {
+ this(null);
+ }
+
+ public HttpMessageWriter<HttpRequest> create(final SessionOutputBuffer buffer) {
+ return new DefaultHttpRequestWriter(buffer, lineFormatter);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseParser.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseParser.java
new file mode 100644
index 0000000000..a8428e8122
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseParser.java
@@ -0,0 +1,141 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpResponseFactory;
+import ch.boye.httpclientandroidlib.NoHttpResponseException;
+import ch.boye.httpclientandroidlib.ParseException;
+import ch.boye.httpclientandroidlib.StatusLine;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.config.MessageConstraints;
+import ch.boye.httpclientandroidlib.impl.DefaultHttpResponseFactory;
+import ch.boye.httpclientandroidlib.io.SessionInputBuffer;
+import ch.boye.httpclientandroidlib.message.LineParser;
+import ch.boye.httpclientandroidlib.message.ParserCursor;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * HTTP response parser that obtain its input from an instance
+ * of {@link SessionInputBuffer}.
+ *
+ * @since 4.2
+ */
+@SuppressWarnings("deprecation")
+@NotThreadSafe
+public class DefaultHttpResponseParser extends AbstractMessageParser<HttpResponse> {
+
+ private final HttpResponseFactory responseFactory;
+ private final CharArrayBuffer lineBuf;
+
+ /**
+ * Creates an instance of this class.
+ *
+ * @param buffer the session input buffer.
+ * @param lineParser the line parser.
+ * @param responseFactory the factory to use to create
+ * {@link HttpResponse}s.
+ * @param params HTTP parameters.
+ *
+ * @deprecated (4.3) use
+ * {@link DefaultHttpResponseParser#DefaultHttpResponseParser(SessionInputBuffer, LineParser,
+ * HttpResponseFactory, MessageConstraints)}
+ */
+ @Deprecated
+ public DefaultHttpResponseParser(
+ final SessionInputBuffer buffer,
+ final LineParser lineParser,
+ final HttpResponseFactory responseFactory,
+ final HttpParams params) {
+ super(buffer, lineParser, params);
+ this.responseFactory = Args.notNull(responseFactory, "Response factory");
+ this.lineBuf = new CharArrayBuffer(128);
+ }
+
+ /**
+ * Creates new instance of DefaultHttpResponseParser.
+ *
+ * @param buffer the session input buffer.
+ * @param lineParser the line parser. If <code>null</code>
+ * {@link ch.boye.httpclientandroidlib.message.BasicLineParser#INSTANCE} will be used
+ * @param responseFactory the response factory. If <code>null</code>
+ * {@link DefaultHttpResponseFactory#INSTANCE} will be used.
+ * @param constraints the message constraints. If <code>null</code>
+ * {@link MessageConstraints#DEFAULT} will be used.
+ *
+ * @since 4.3
+ */
+ public DefaultHttpResponseParser(
+ final SessionInputBuffer buffer,
+ final LineParser lineParser,
+ final HttpResponseFactory responseFactory,
+ final MessageConstraints constraints) {
+ super(buffer, lineParser, constraints);
+ this.responseFactory = responseFactory != null ? responseFactory :
+ DefaultHttpResponseFactory.INSTANCE;
+ this.lineBuf = new CharArrayBuffer(128);
+ }
+
+ /**
+ * @since 4.3
+ */
+ public DefaultHttpResponseParser(
+ final SessionInputBuffer buffer,
+ final MessageConstraints constraints) {
+ this(buffer, null, null, constraints);
+ }
+
+ /**
+ * @since 4.3
+ */
+ public DefaultHttpResponseParser(final SessionInputBuffer buffer) {
+ this(buffer, null, null, MessageConstraints.DEFAULT);
+ }
+
+ @Override
+ protected HttpResponse parseHead(
+ final SessionInputBuffer sessionBuffer)
+ throws IOException, HttpException, ParseException {
+
+ this.lineBuf.clear();
+ final int i = sessionBuffer.readLine(this.lineBuf);
+ if (i == -1) {
+ throw new NoHttpResponseException("The target server failed to respond");
+ }
+ //create the status line from the status string
+ final ParserCursor cursor = new ParserCursor(0, this.lineBuf.length());
+ final StatusLine statusline = lineParser.parseStatusLine(this.lineBuf, cursor);
+ return this.responseFactory.newHttpResponse(statusline, null);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseParserFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseParserFactory.java
new file mode 100644
index 0000000000..318abcf704
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseParserFactory.java
@@ -0,0 +1,71 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpResponseFactory;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.config.MessageConstraints;
+import ch.boye.httpclientandroidlib.impl.DefaultHttpResponseFactory;
+import ch.boye.httpclientandroidlib.io.HttpMessageParser;
+import ch.boye.httpclientandroidlib.io.HttpMessageParserFactory;
+import ch.boye.httpclientandroidlib.io.SessionInputBuffer;
+import ch.boye.httpclientandroidlib.message.BasicLineParser;
+import ch.boye.httpclientandroidlib.message.LineParser;
+
+/**
+ * Default factory for response message parsers.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class DefaultHttpResponseParserFactory implements HttpMessageParserFactory<HttpResponse> {
+
+ public static final DefaultHttpResponseParserFactory INSTANCE = new DefaultHttpResponseParserFactory();
+
+ private final LineParser lineParser;
+ private final HttpResponseFactory responseFactory;
+
+ public DefaultHttpResponseParserFactory(final LineParser lineParser,
+ final HttpResponseFactory responseFactory) {
+ super();
+ this.lineParser = lineParser != null ? lineParser : BasicLineParser.INSTANCE;
+ this.responseFactory = responseFactory != null ? responseFactory
+ : DefaultHttpResponseFactory.INSTANCE;
+ }
+
+ public DefaultHttpResponseParserFactory() {
+ this(null, null);
+ }
+
+ public HttpMessageParser<HttpResponse> create(final SessionInputBuffer buffer,
+ final MessageConstraints constraints) {
+ return new DefaultHttpResponseParser(buffer, lineParser, responseFactory, constraints);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseWriter.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseWriter.java
new file mode 100644
index 0000000000..8cdef00a5c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseWriter.java
@@ -0,0 +1,69 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.io.SessionOutputBuffer;
+import ch.boye.httpclientandroidlib.message.LineFormatter;
+
+/**
+ * HTTP response writer that serializes its output to an instance of {@link SessionOutputBuffer}.
+ *
+ * @since 4.3
+ */
+@NotThreadSafe
+public class DefaultHttpResponseWriter extends AbstractMessageWriter<HttpResponse> {
+
+ /**
+ * Creates an instance of DefaultHttpResponseWriter.
+ *
+ * @param buffer the session output buffer.
+ * @param formatter the line formatter If <code>null</code>
+ * {@link ch.boye.httpclientandroidlib.message.BasicLineFormatter#INSTANCE}
+ * will be used.
+ */
+ public DefaultHttpResponseWriter(
+ final SessionOutputBuffer buffer,
+ final LineFormatter formatter) {
+ super(buffer, formatter);
+ }
+
+ public DefaultHttpResponseWriter(final SessionOutputBuffer buffer) {
+ super(buffer, null);
+ }
+
+ @Override
+ protected void writeHeadLine(final HttpResponse message) throws IOException {
+ lineFormatter.formatStatusLine(this.lineBuf, message.getStatusLine());
+ this.sessionBuffer.writeLine(this.lineBuf);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseWriterFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseWriterFactory.java
new file mode 100644
index 0000000000..9dc29606d9
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/DefaultHttpResponseWriterFactory.java
@@ -0,0 +1,63 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.io.HttpMessageWriter;
+import ch.boye.httpclientandroidlib.io.HttpMessageWriterFactory;
+import ch.boye.httpclientandroidlib.io.SessionOutputBuffer;
+import ch.boye.httpclientandroidlib.message.BasicLineFormatter;
+import ch.boye.httpclientandroidlib.message.LineFormatter;
+
+/**
+ * Default factory for response message writers.
+ *
+ * @since 4.3
+ */
+@Immutable
+public class DefaultHttpResponseWriterFactory implements HttpMessageWriterFactory<HttpResponse> {
+
+ public static final DefaultHttpResponseWriterFactory INSTANCE = new DefaultHttpResponseWriterFactory();
+
+ private final LineFormatter lineFormatter;
+
+ public DefaultHttpResponseWriterFactory(final LineFormatter lineFormatter) {
+ super();
+ this.lineFormatter = lineFormatter != null ? lineFormatter : BasicLineFormatter.INSTANCE;
+ }
+
+ public DefaultHttpResponseWriterFactory() {
+ this(null);
+ }
+
+ public HttpMessageWriter<HttpResponse> create(final SessionOutputBuffer buffer) {
+ return new DefaultHttpResponseWriter(buffer, lineFormatter);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/HttpRequestParser.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/HttpRequestParser.java
new file mode 100644
index 0000000000..9c68b69381
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/HttpRequestParser.java
@@ -0,0 +1,102 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.ConnectionClosedException;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpMessage;
+import ch.boye.httpclientandroidlib.HttpRequestFactory;
+import ch.boye.httpclientandroidlib.ParseException;
+import ch.boye.httpclientandroidlib.RequestLine;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.io.SessionInputBuffer;
+import ch.boye.httpclientandroidlib.message.LineParser;
+import ch.boye.httpclientandroidlib.message.ParserCursor;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * HTTP request parser that obtain its input from an instance
+ * of {@link SessionInputBuffer}.
+ * <p>
+ * The following parameters can be used to customize the behavior of this
+ * class:
+ * <ul>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#MAX_HEADER_COUNT}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#MAX_LINE_LENGTH}</li>
+ * </ul>
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) use {@link DefaultHttpRequestParser}
+ */
+@Deprecated
+@NotThreadSafe
+public class HttpRequestParser extends AbstractMessageParser<HttpMessage> {
+
+ private final HttpRequestFactory requestFactory;
+ private final CharArrayBuffer lineBuf;
+
+ /**
+ * Creates an instance of this class.
+ *
+ * @param buffer the session input buffer.
+ * @param parser the line parser.
+ * @param requestFactory the factory to use to create
+ * {@link ch.boye.httpclientandroidlib.HttpRequest}s.
+ * @param params HTTP parameters.
+ */
+ public HttpRequestParser(
+ final SessionInputBuffer buffer,
+ final LineParser parser,
+ final HttpRequestFactory requestFactory,
+ final HttpParams params) {
+ super(buffer, parser, params);
+ this.requestFactory = Args.notNull(requestFactory, "Request factory");
+ this.lineBuf = new CharArrayBuffer(128);
+ }
+
+ @Override
+ protected HttpMessage parseHead(
+ final SessionInputBuffer sessionBuffer)
+ throws IOException, HttpException, ParseException {
+
+ this.lineBuf.clear();
+ final int i = sessionBuffer.readLine(this.lineBuf);
+ if (i == -1) {
+ throw new ConnectionClosedException("Client closed connection");
+ }
+ final ParserCursor cursor = new ParserCursor(0, this.lineBuf.length());
+ final RequestLine requestline = this.lineParser.parseRequestLine(this.lineBuf, cursor);
+ return this.requestFactory.newHttpRequest(requestline);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/HttpRequestWriter.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/HttpRequestWriter.java
new file mode 100644
index 0000000000..3603ef5733
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/HttpRequestWriter.java
@@ -0,0 +1,62 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.io.SessionOutputBuffer;
+import ch.boye.httpclientandroidlib.message.LineFormatter;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+/**
+ * HTTP request writer that serializes its output to an instance
+ * of {@link SessionOutputBuffer}.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link DefaultHttpRequestWriter}
+ */
+@NotThreadSafe
+@Deprecated
+public class HttpRequestWriter extends AbstractMessageWriter<HttpRequest> {
+
+ public HttpRequestWriter(final SessionOutputBuffer buffer,
+ final LineFormatter formatter,
+ final HttpParams params) {
+ super(buffer, formatter, params);
+ }
+
+ @Override
+ protected void writeHeadLine(final HttpRequest message) throws IOException {
+ lineFormatter.formatRequestLine(this.lineBuf, message.getRequestLine());
+ this.sessionBuffer.writeLine(this.lineBuf);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/HttpResponseParser.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/HttpResponseParser.java
new file mode 100644
index 0000000000..a94eae36c7
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/HttpResponseParser.java
@@ -0,0 +1,103 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpMessage;
+import ch.boye.httpclientandroidlib.HttpResponseFactory;
+import ch.boye.httpclientandroidlib.NoHttpResponseException;
+import ch.boye.httpclientandroidlib.ParseException;
+import ch.boye.httpclientandroidlib.StatusLine;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.io.SessionInputBuffer;
+import ch.boye.httpclientandroidlib.message.LineParser;
+import ch.boye.httpclientandroidlib.message.ParserCursor;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * HTTP response parser that obtain its input from an instance
+ * of {@link SessionInputBuffer}.
+ * <p>
+ * The following parameters can be used to customize the behavior of this
+ * class:
+ * <ul>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#MAX_HEADER_COUNT}</li>
+ * <li>{@link ch.boye.httpclientandroidlib.params.CoreConnectionPNames#MAX_LINE_LENGTH}</li>
+ * </ul>
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) use {@link DefaultHttpResponseParser}
+ */
+@Deprecated
+@NotThreadSafe
+public class HttpResponseParser extends AbstractMessageParser<HttpMessage> {
+
+ private final HttpResponseFactory responseFactory;
+ private final CharArrayBuffer lineBuf;
+
+ /**
+ * Creates an instance of this class.
+ *
+ * @param buffer the session input buffer.
+ * @param parser the line parser.
+ * @param responseFactory the factory to use to create
+ * {@link ch.boye.httpclientandroidlib.HttpResponse}s.
+ * @param params HTTP parameters.
+ */
+ public HttpResponseParser(
+ final SessionInputBuffer buffer,
+ final LineParser parser,
+ final HttpResponseFactory responseFactory,
+ final HttpParams params) {
+ super(buffer, parser, params);
+ this.responseFactory = Args.notNull(responseFactory, "Response factory");
+ this.lineBuf = new CharArrayBuffer(128);
+ }
+
+ @Override
+ protected HttpMessage parseHead(
+ final SessionInputBuffer sessionBuffer)
+ throws IOException, HttpException, ParseException {
+
+ this.lineBuf.clear();
+ final int i = sessionBuffer.readLine(this.lineBuf);
+ if (i == -1) {
+ throw new NoHttpResponseException("The target server failed to respond");
+ }
+ //create the status line from the status string
+ final ParserCursor cursor = new ParserCursor(0, this.lineBuf.length());
+ final StatusLine statusline = lineParser.parseStatusLine(this.lineBuf, cursor);
+ return this.responseFactory.newHttpResponse(statusline, null);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/HttpResponseWriter.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/HttpResponseWriter.java
new file mode 100644
index 0000000000..2e82ade182
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/HttpResponseWriter.java
@@ -0,0 +1,62 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.io.SessionOutputBuffer;
+import ch.boye.httpclientandroidlib.message.LineFormatter;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+/**
+ * HTTP response writer that serializes its output to an instance
+ * of {@link SessionOutputBuffer}.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link DefaultHttpResponseWriter}
+ */
+@NotThreadSafe
+@Deprecated
+public class HttpResponseWriter extends AbstractMessageWriter<HttpResponse> {
+
+ public HttpResponseWriter(final SessionOutputBuffer buffer,
+ final LineFormatter formatter,
+ final HttpParams params) {
+ super(buffer, formatter, params);
+ }
+
+ @Override
+ protected void writeHeadLine(final HttpResponse message) throws IOException {
+ lineFormatter.formatStatusLine(this.lineBuf, message.getStatusLine());
+ this.sessionBuffer.writeLine(this.lineBuf);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/HttpTransportMetricsImpl.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/HttpTransportMetricsImpl.java
new file mode 100644
index 0000000000..8bd4eae5f5
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/HttpTransportMetricsImpl.java
@@ -0,0 +1,63 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.io.HttpTransportMetrics;
+
+/**
+ * Default implementation of {@link HttpTransportMetrics}.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class HttpTransportMetricsImpl implements HttpTransportMetrics {
+
+ private long bytesTransferred = 0;
+
+ public HttpTransportMetricsImpl() {
+ super();
+ }
+
+ public long getBytesTransferred() {
+ return this.bytesTransferred;
+ }
+
+ public void setBytesTransferred(final long count) {
+ this.bytesTransferred = count;
+ }
+
+ public void incrementBytesTransferred(final long count) {
+ this.bytesTransferred += count;
+ }
+
+ public void reset() {
+ this.bytesTransferred = 0;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/IdentityInputStream.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/IdentityInputStream.java
new file mode 100644
index 0000000000..98e1cf81a4
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/IdentityInputStream.java
@@ -0,0 +1,99 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.io.BufferInfo;
+import ch.boye.httpclientandroidlib.io.SessionInputBuffer;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Input stream that reads data without any transformation. The end of the
+ * content entity is demarcated by closing the underlying connection
+ * (EOF condition). Entities transferred using this input stream can be of
+ * unlimited length.
+ * <p>
+ * Note that this class NEVER closes the underlying stream, even when close
+ * gets called. Instead, it will read until the end of the stream (until
+ * <code>-1</code> is returned).
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class IdentityInputStream extends InputStream {
+
+ private final SessionInputBuffer in;
+
+ private boolean closed = false;
+
+ /**
+ * Wraps session input stream and reads input until the the end of stream.
+ *
+ * @param in The session input buffer
+ */
+ public IdentityInputStream(final SessionInputBuffer in) {
+ super();
+ this.in = Args.notNull(in, "Session input buffer");
+ }
+
+ @Override
+ public int available() throws IOException {
+ if (this.in instanceof BufferInfo) {
+ return ((BufferInfo) this.in).length();
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ this.closed = true;
+ }
+
+ @Override
+ public int read() throws IOException {
+ if (this.closed) {
+ return -1;
+ } else {
+ return this.in.read();
+ }
+ }
+
+ @Override
+ public int read(final byte[] b, final int off, final int len) throws IOException {
+ if (this.closed) {
+ return -1;
+ } else {
+ return this.in.read(b, off, len);
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/IdentityOutputStream.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/IdentityOutputStream.java
new file mode 100644
index 0000000000..ac2f613b79
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/IdentityOutputStream.java
@@ -0,0 +1,104 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.io.SessionOutputBuffer;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Output stream that writes data without any transformation. The end of
+ * the content entity is demarcated by closing the underlying connection
+ * (EOF condition). Entities transferred using this input stream can be of
+ * unlimited length.
+ * <p>
+ * Note that this class NEVER closes the underlying stream, even when close
+ * gets called. Instead, the stream will be marked as closed and no further
+ * output will be permitted.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class IdentityOutputStream extends OutputStream {
+
+ /**
+ * Wrapped session output buffer.
+ */
+ private final SessionOutputBuffer out;
+
+ /** True if the stream is closed. */
+ private boolean closed = false;
+
+ public IdentityOutputStream(final SessionOutputBuffer out) {
+ super();
+ this.out = Args.notNull(out, "Session output buffer");
+ }
+
+ /**
+ * <p>Does not close the underlying socket output.</p>
+ *
+ * @throws IOException If an I/O problem occurs.
+ */
+ @Override
+ public void close() throws IOException {
+ if (!this.closed) {
+ this.closed = true;
+ this.out.flush();
+ }
+ }
+
+ @Override
+ public void flush() throws IOException {
+ this.out.flush();
+ }
+
+ @Override
+ public void write(final byte[] b, final int off, final int len) throws IOException {
+ if (this.closed) {
+ throw new IOException("Attempted write to closed stream.");
+ }
+ this.out.write(b, off, len);
+ }
+
+ @Override
+ public void write(final byte[] b) throws IOException {
+ write(b, 0, b.length);
+ }
+
+ @Override
+ public void write(final int b) throws IOException {
+ if (this.closed) {
+ throw new IOException("Attempted write to closed stream.");
+ }
+ this.out.write(b);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/SessionInputBufferImpl.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/SessionInputBufferImpl.java
new file mode 100644
index 0000000000..ed86cb94bf
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/SessionInputBufferImpl.java
@@ -0,0 +1,399 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
+
+import ch.boye.httpclientandroidlib.MessageConstraintException;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.config.MessageConstraints;
+import ch.boye.httpclientandroidlib.io.BufferInfo;
+import ch.boye.httpclientandroidlib.io.HttpTransportMetrics;
+import ch.boye.httpclientandroidlib.io.SessionInputBuffer;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+import ch.boye.httpclientandroidlib.util.ByteArrayBuffer;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Abstract base class for session input buffers that stream data from
+ * an arbitrary {@link InputStream}. This class buffers input data in
+ * an internal byte array for optimal input performance.
+ * <p/>
+ * {@link #readLine(CharArrayBuffer)} and {@link #readLine()} methods of this
+ * class treat a lone LF as valid line delimiters in addition to CR-LF required
+ * by the HTTP specification.
+ *
+ * @since 4.3
+ */
+@NotThreadSafe
+public class SessionInputBufferImpl implements SessionInputBuffer, BufferInfo {
+
+ private final HttpTransportMetricsImpl metrics;
+ private final byte[] buffer;
+ private final ByteArrayBuffer linebuffer;
+ private final int minChunkLimit;
+ private final MessageConstraints constraints;
+ private final CharsetDecoder decoder;
+
+ private InputStream instream;
+ private int bufferpos;
+ private int bufferlen;
+ private CharBuffer cbuf;
+
+ /**
+ * Creates new instance of SessionInputBufferImpl.
+ *
+ * @param metrics HTTP transport metrics.
+ * @param buffersize buffer size. Must be a positive number.
+ * @param minChunkLimit size limit below which data chunks should be buffered in memory
+ * in order to minimize native method invocations on the underlying network socket.
+ * The optimal value of this parameter can be platform specific and defines a trade-off
+ * between performance of memory copy operations and that of native method invocation.
+ * If negative default chunk limited will be used.
+ * @param constraints Message constraints. If <code>null</code>
+ * {@link MessageConstraints#DEFAULT} will be used.
+ * @param chardecoder chardecoder to be used for decoding HTTP protocol elements.
+ * If <code>null</code> simple type cast will be used for byte to char conversion.
+ */
+ public SessionInputBufferImpl(
+ final HttpTransportMetricsImpl metrics,
+ final int buffersize,
+ final int minChunkLimit,
+ final MessageConstraints constraints,
+ final CharsetDecoder chardecoder) {
+ Args.notNull(metrics, "HTTP transport metrcis");
+ Args.positive(buffersize, "Buffer size");
+ this.metrics = metrics;
+ this.buffer = new byte[buffersize];
+ this.bufferpos = 0;
+ this.bufferlen = 0;
+ this.minChunkLimit = minChunkLimit >= 0 ? minChunkLimit : 512;
+ this.constraints = constraints != null ? constraints : MessageConstraints.DEFAULT;
+ this.linebuffer = new ByteArrayBuffer(buffersize);
+ this.decoder = chardecoder;
+ }
+
+ public SessionInputBufferImpl(
+ final HttpTransportMetricsImpl metrics,
+ final int buffersize) {
+ this(metrics, buffersize, buffersize, null, null);
+ }
+
+ public void bind(final InputStream instream) {
+ this.instream = instream;
+ }
+
+ public boolean isBound() {
+ return this.instream != null;
+ }
+
+ public int capacity() {
+ return this.buffer.length;
+ }
+
+ public int length() {
+ return this.bufferlen - this.bufferpos;
+ }
+
+ public int available() {
+ return capacity() - length();
+ }
+
+ private int streamRead(final byte[] b, final int off, final int len) throws IOException {
+ Asserts.notNull(this.instream, "Input stream");
+ return this.instream.read(b, off, len);
+ }
+
+ public int fillBuffer() throws IOException {
+ // compact the buffer if necessary
+ if (this.bufferpos > 0) {
+ final int len = this.bufferlen - this.bufferpos;
+ if (len > 0) {
+ System.arraycopy(this.buffer, this.bufferpos, this.buffer, 0, len);
+ }
+ this.bufferpos = 0;
+ this.bufferlen = len;
+ }
+ final int l;
+ final int off = this.bufferlen;
+ final int len = this.buffer.length - off;
+ l = streamRead(this.buffer, off, len);
+ if (l == -1) {
+ return -1;
+ } else {
+ this.bufferlen = off + l;
+ this.metrics.incrementBytesTransferred(l);
+ return l;
+ }
+ }
+
+ public boolean hasBufferedData() {
+ return this.bufferpos < this.bufferlen;
+ }
+
+ public void clear() {
+ this.bufferpos = 0;
+ this.bufferlen = 0;
+ }
+
+ public int read() throws IOException {
+ int noRead;
+ while (!hasBufferedData()) {
+ noRead = fillBuffer();
+ if (noRead == -1) {
+ return -1;
+ }
+ }
+ return this.buffer[this.bufferpos++] & 0xff;
+ }
+
+ public int read(final byte[] b, final int off, final int len) throws IOException {
+ if (b == null) {
+ return 0;
+ }
+ if (hasBufferedData()) {
+ final int chunk = Math.min(len, this.bufferlen - this.bufferpos);
+ System.arraycopy(this.buffer, this.bufferpos, b, off, chunk);
+ this.bufferpos += chunk;
+ return chunk;
+ }
+ // If the remaining capacity is big enough, read directly from the
+ // underlying input stream bypassing the buffer.
+ if (len > this.minChunkLimit) {
+ final int read = streamRead(b, off, len);
+ if (read > 0) {
+ this.metrics.incrementBytesTransferred(read);
+ }
+ return read;
+ } else {
+ // otherwise read to the buffer first
+ while (!hasBufferedData()) {
+ final int noRead = fillBuffer();
+ if (noRead == -1) {
+ return -1;
+ }
+ }
+ final int chunk = Math.min(len, this.bufferlen - this.bufferpos);
+ System.arraycopy(this.buffer, this.bufferpos, b, off, chunk);
+ this.bufferpos += chunk;
+ return chunk;
+ }
+ }
+
+ public int read(final byte[] b) throws IOException {
+ if (b == null) {
+ return 0;
+ }
+ return read(b, 0, b.length);
+ }
+
+ private int locateLF() {
+ for (int i = this.bufferpos; i < this.bufferlen; i++) {
+ if (this.buffer[i] == HTTP.LF) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Reads a complete line of characters up to a line delimiter from this
+ * session buffer into the given line buffer. The number of chars actually
+ * read is returned as an integer. The line delimiter itself is discarded.
+ * If no char is available because the end of the stream has been reached,
+ * the value <code>-1</code> is returned. This method blocks until input
+ * data is available, end of file is detected, or an exception is thrown.
+ * <p>
+ * This method treats a lone LF as a valid line delimiters in addition
+ * to CR-LF required by the HTTP specification.
+ *
+ * @param charbuffer the line buffer.
+ * @return one line of characters
+ * @exception IOException if an I/O error occurs.
+ */
+ public int readLine(final CharArrayBuffer charbuffer) throws IOException {
+ Args.notNull(charbuffer, "Char array buffer");
+ int noRead = 0;
+ boolean retry = true;
+ while (retry) {
+ // attempt to find end of line (LF)
+ final int i = locateLF();
+ if (i != -1) {
+ // end of line found.
+ if (this.linebuffer.isEmpty()) {
+ // the entire line is preset in the read buffer
+ return lineFromReadBuffer(charbuffer, i);
+ }
+ retry = false;
+ final int len = i + 1 - this.bufferpos;
+ this.linebuffer.append(this.buffer, this.bufferpos, len);
+ this.bufferpos = i + 1;
+ } else {
+ // end of line not found
+ if (hasBufferedData()) {
+ final int len = this.bufferlen - this.bufferpos;
+ this.linebuffer.append(this.buffer, this.bufferpos, len);
+ this.bufferpos = this.bufferlen;
+ }
+ noRead = fillBuffer();
+ if (noRead == -1) {
+ retry = false;
+ }
+ }
+ final int maxLineLen = this.constraints.getMaxLineLength();
+ if (maxLineLen > 0 && this.linebuffer.length() >= maxLineLen) {
+ throw new MessageConstraintException("Maximum line length limit exceeded");
+ }
+ }
+ if (noRead == -1 && this.linebuffer.isEmpty()) {
+ // indicate the end of stream
+ return -1;
+ }
+ return lineFromLineBuffer(charbuffer);
+ }
+
+ /**
+ * Reads a complete line of characters up to a line delimiter from this
+ * session buffer. The line delimiter itself is discarded. If no char is
+ * available because the end of the stream has been reached,
+ * <code>null</code> is returned. This method blocks until input data is
+ * available, end of file is detected, or an exception is thrown.
+ * <p>
+ * This method treats a lone LF as a valid line delimiters in addition
+ * to CR-LF required by the HTTP specification.
+ *
+ * @return HTTP line as a string
+ * @exception IOException if an I/O error occurs.
+ */
+ private int lineFromLineBuffer(final CharArrayBuffer charbuffer)
+ throws IOException {
+ // discard LF if found
+ int len = this.linebuffer.length();
+ if (len > 0) {
+ if (this.linebuffer.byteAt(len - 1) == HTTP.LF) {
+ len--;
+ }
+ // discard CR if found
+ if (len > 0) {
+ if (this.linebuffer.byteAt(len - 1) == HTTP.CR) {
+ len--;
+ }
+ }
+ }
+ if (this.decoder == null) {
+ charbuffer.append(this.linebuffer, 0, len);
+ } else {
+ final ByteBuffer bbuf = ByteBuffer.wrap(this.linebuffer.buffer(), 0, len);
+ len = appendDecoded(charbuffer, bbuf);
+ }
+ this.linebuffer.clear();
+ return len;
+ }
+
+ private int lineFromReadBuffer(final CharArrayBuffer charbuffer, final int position)
+ throws IOException {
+ int pos = position;
+ final int off = this.bufferpos;
+ int len;
+ this.bufferpos = pos + 1;
+ if (pos > off && this.buffer[pos - 1] == HTTP.CR) {
+ // skip CR if found
+ pos--;
+ }
+ len = pos - off;
+ if (this.decoder == null) {
+ charbuffer.append(this.buffer, off, len);
+ } else {
+ final ByteBuffer bbuf = ByteBuffer.wrap(this.buffer, off, len);
+ len = appendDecoded(charbuffer, bbuf);
+ }
+ return len;
+ }
+
+ private int appendDecoded(
+ final CharArrayBuffer charbuffer, final ByteBuffer bbuf) throws IOException {
+ if (!bbuf.hasRemaining()) {
+ return 0;
+ }
+ if (this.cbuf == null) {
+ this.cbuf = CharBuffer.allocate(1024);
+ }
+ this.decoder.reset();
+ int len = 0;
+ while (bbuf.hasRemaining()) {
+ final CoderResult result = this.decoder.decode(bbuf, this.cbuf, true);
+ len += handleDecodingResult(result, charbuffer, bbuf);
+ }
+ final CoderResult result = this.decoder.flush(this.cbuf);
+ len += handleDecodingResult(result, charbuffer, bbuf);
+ this.cbuf.clear();
+ return len;
+ }
+
+ private int handleDecodingResult(
+ final CoderResult result,
+ final CharArrayBuffer charbuffer,
+ final ByteBuffer bbuf) throws IOException {
+ if (result.isError()) {
+ result.throwException();
+ }
+ this.cbuf.flip();
+ final int len = this.cbuf.remaining();
+ while (this.cbuf.hasRemaining()) {
+ charbuffer.append(this.cbuf.get());
+ }
+ this.cbuf.compact();
+ return len;
+ }
+
+ public String readLine() throws IOException {
+ final CharArrayBuffer charbuffer = new CharArrayBuffer(64);
+ final int l = readLine(charbuffer);
+ if (l != -1) {
+ return charbuffer.toString();
+ } else {
+ return null;
+ }
+ }
+
+ public boolean isDataAvailable(final int timeout) throws IOException {
+ return hasBufferedData();
+ }
+
+ public HttpTransportMetrics getMetrics() {
+ return this.metrics;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/SessionOutputBufferImpl.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/SessionOutputBufferImpl.java
new file mode 100644
index 0000000000..99ca871cec
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/SessionOutputBufferImpl.java
@@ -0,0 +1,283 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CoderResult;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.io.BufferInfo;
+import ch.boye.httpclientandroidlib.io.HttpTransportMetrics;
+import ch.boye.httpclientandroidlib.io.SessionOutputBuffer;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+import ch.boye.httpclientandroidlib.util.ByteArrayBuffer;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Abstract base class for session output buffers that stream data to
+ * an arbitrary {@link OutputStream}. This class buffers small chunks of
+ * output data in an internal byte array for optimal output performance.
+ * </p>
+ * {@link #writeLine(CharArrayBuffer)} and {@link #writeLine(String)} methods
+ * of this class use CR-LF as a line delimiter.
+ *
+ * @since 4.3
+ */
+@NotThreadSafe
+public class SessionOutputBufferImpl implements SessionOutputBuffer, BufferInfo {
+
+ private static final byte[] CRLF = new byte[] {HTTP.CR, HTTP.LF};
+
+ private final HttpTransportMetricsImpl metrics;
+ private final ByteArrayBuffer buffer;
+ private final int fragementSizeHint;
+ private final CharsetEncoder encoder;
+
+ private OutputStream outstream;
+ private ByteBuffer bbuf;
+
+ /**
+ * Creates new instance of SessionOutputBufferImpl.
+ *
+ * @param metrics HTTP transport metrics.
+ * @param buffersize buffer size. Must be a positive number.
+ * @param fragementSizeHint fragment size hint defining a minimal size of a fragment
+ * that should be written out directly to the socket bypassing the session buffer.
+ * Value <code>0</code> disables fragment buffering.
+ * @param charencoder charencoder to be used for encoding HTTP protocol elements.
+ * If <code>null</code> simple type cast will be used for char to byte conversion.
+ */
+ public SessionOutputBufferImpl(
+ final HttpTransportMetricsImpl metrics,
+ final int buffersize,
+ final int fragementSizeHint,
+ final CharsetEncoder charencoder) {
+ super();
+ Args.positive(buffersize, "Buffer size");
+ Args.notNull(metrics, "HTTP transport metrcis");
+ this.metrics = metrics;
+ this.buffer = new ByteArrayBuffer(buffersize);
+ this.fragementSizeHint = fragementSizeHint >= 0 ? fragementSizeHint : 0;
+ this.encoder = charencoder;
+ }
+
+ public SessionOutputBufferImpl(
+ final HttpTransportMetricsImpl metrics,
+ final int buffersize) {
+ this(metrics, buffersize, buffersize, null);
+ }
+
+ public void bind(final OutputStream outstream) {
+ this.outstream = outstream;
+ }
+
+ public boolean isBound() {
+ return this.outstream != null;
+ }
+
+ public int capacity() {
+ return this.buffer.capacity();
+ }
+
+ public int length() {
+ return this.buffer.length();
+ }
+
+ public int available() {
+ return capacity() - length();
+ }
+
+ private void streamWrite(final byte[] b, final int off, final int len) throws IOException {
+ Asserts.notNull(outstream, "Output stream");
+ this.outstream.write(b, off, len);
+ }
+
+ private void flushStream() throws IOException {
+ if (this.outstream != null) {
+ this.outstream.flush();
+ }
+ }
+
+ private void flushBuffer() throws IOException {
+ final int len = this.buffer.length();
+ if (len > 0) {
+ streamWrite(this.buffer.buffer(), 0, len);
+ this.buffer.clear();
+ this.metrics.incrementBytesTransferred(len);
+ }
+ }
+
+ public void flush() throws IOException {
+ flushBuffer();
+ flushStream();
+ }
+
+ public void write(final byte[] b, final int off, final int len) throws IOException {
+ if (b == null) {
+ return;
+ }
+ // Do not want to buffer large-ish chunks
+ // if the byte array is larger then MIN_CHUNK_LIMIT
+ // write it directly to the output stream
+ if (len > this.fragementSizeHint || len > this.buffer.capacity()) {
+ // flush the buffer
+ flushBuffer();
+ // write directly to the out stream
+ streamWrite(b, off, len);
+ this.metrics.incrementBytesTransferred(len);
+ } else {
+ // Do not let the buffer grow unnecessarily
+ final int freecapacity = this.buffer.capacity() - this.buffer.length();
+ if (len > freecapacity) {
+ // flush the buffer
+ flushBuffer();
+ }
+ // buffer
+ this.buffer.append(b, off, len);
+ }
+ }
+
+ public void write(final byte[] b) throws IOException {
+ if (b == null) {
+ return;
+ }
+ write(b, 0, b.length);
+ }
+
+ public void write(final int b) throws IOException {
+ if (this.fragementSizeHint > 0) {
+ if (this.buffer.isFull()) {
+ flushBuffer();
+ }
+ this.buffer.append(b);
+ } else {
+ flushBuffer();
+ this.outstream.write(b);
+ }
+ }
+
+ /**
+ * Writes characters from the specified string followed by a line delimiter
+ * to this session buffer.
+ * <p>
+ * This method uses CR-LF as a line delimiter.
+ *
+ * @param s the line.
+ * @exception IOException if an I/O error occurs.
+ */
+ public void writeLine(final String s) throws IOException {
+ if (s == null) {
+ return;
+ }
+ if (s.length() > 0) {
+ if (this.encoder == null) {
+ for (int i = 0; i < s.length(); i++) {
+ write(s.charAt(i));
+ }
+ } else {
+ final CharBuffer cbuf = CharBuffer.wrap(s);
+ writeEncoded(cbuf);
+ }
+ }
+ write(CRLF);
+ }
+
+ /**
+ * Writes characters from the specified char array followed by a line
+ * delimiter to this session buffer.
+ * <p>
+ * This method uses CR-LF as a line delimiter.
+ *
+ * @param charbuffer the buffer containing chars of the line.
+ * @exception IOException if an I/O error occurs.
+ */
+ public void writeLine(final CharArrayBuffer charbuffer) throws IOException {
+ if (charbuffer == null) {
+ return;
+ }
+ if (this.encoder == null) {
+ int off = 0;
+ int remaining = charbuffer.length();
+ while (remaining > 0) {
+ int chunk = this.buffer.capacity() - this.buffer.length();
+ chunk = Math.min(chunk, remaining);
+ if (chunk > 0) {
+ this.buffer.append(charbuffer, off, chunk);
+ }
+ if (this.buffer.isFull()) {
+ flushBuffer();
+ }
+ off += chunk;
+ remaining -= chunk;
+ }
+ } else {
+ final CharBuffer cbuf = CharBuffer.wrap(charbuffer.buffer(), 0, charbuffer.length());
+ writeEncoded(cbuf);
+ }
+ write(CRLF);
+ }
+
+ private void writeEncoded(final CharBuffer cbuf) throws IOException {
+ if (!cbuf.hasRemaining()) {
+ return;
+ }
+ if (this.bbuf == null) {
+ this.bbuf = ByteBuffer.allocate(1024);
+ }
+ this.encoder.reset();
+ while (cbuf.hasRemaining()) {
+ final CoderResult result = this.encoder.encode(cbuf, this.bbuf, true);
+ handleEncodingResult(result);
+ }
+ final CoderResult result = this.encoder.flush(this.bbuf);
+ handleEncodingResult(result);
+ this.bbuf.clear();
+ }
+
+ private void handleEncodingResult(final CoderResult result) throws IOException {
+ if (result.isError()) {
+ result.throwException();
+ }
+ this.bbuf.flip();
+ while (this.bbuf.hasRemaining()) {
+ write(this.bbuf.get());
+ }
+ this.bbuf.compact();
+ }
+
+ public HttpTransportMetrics getMetrics() {
+ return this.metrics;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/SocketInputBuffer.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/SocketInputBuffer.java
new file mode 100644
index 0000000000..4bba8f894a
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/SocketInputBuffer.java
@@ -0,0 +1,108 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import java.io.IOException;
+import java.net.Socket;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.io.EofSensor;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * {@link ch.boye.httpclientandroidlib.io.SessionInputBuffer} implementation
+ * bound to a {@link Socket}.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link SessionInputBufferImpl}
+ */
+@NotThreadSafe
+@Deprecated
+public class SocketInputBuffer extends AbstractSessionInputBuffer implements EofSensor {
+
+ private final Socket socket;
+
+ private boolean eof;
+
+ /**
+ * Creates an instance of this class.
+ *
+ * @param socket the socket to read data from.
+ * @param buffersize the size of the internal buffer. If this number is less
+ * than <code>0</code> it is set to the value of
+ * {@link Socket#getReceiveBufferSize()}. If resultant number is less
+ * than <code>1024</code> it is set to <code>1024</code>.
+ * @param params HTTP parameters.
+ */
+ public SocketInputBuffer(
+ final Socket socket,
+ final int buffersize,
+ final HttpParams params) throws IOException {
+ super();
+ Args.notNull(socket, "Socket");
+ this.socket = socket;
+ this.eof = false;
+ int n = buffersize;
+ if (n < 0) {
+ n = socket.getReceiveBufferSize();
+ }
+ if (n < 1024) {
+ n = 1024;
+ }
+ init(socket.getInputStream(), n, params);
+ }
+
+ @Override
+ protected int fillBuffer() throws IOException {
+ final int i = super.fillBuffer();
+ this.eof = i == -1;
+ return i;
+ }
+
+ public boolean isDataAvailable(final int timeout) throws IOException {
+ boolean result = hasBufferedData();
+ if (!result) {
+ final int oldtimeout = this.socket.getSoTimeout();
+ try {
+ this.socket.setSoTimeout(timeout);
+ fillBuffer();
+ result = hasBufferedData();
+ } finally {
+ socket.setSoTimeout(oldtimeout);
+ }
+ }
+ return result;
+ }
+
+ public boolean isEof() {
+ return this.eof;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/SocketOutputBuffer.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/SocketOutputBuffer.java
new file mode 100644
index 0000000000..9c9ff2d9f9
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/SocketOutputBuffer.java
@@ -0,0 +1,75 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.impl.io;
+
+import java.io.IOException;
+import java.net.Socket;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * {@link ch.boye.httpclientandroidlib.io.SessionOutputBuffer} implementation
+ * bound to a {@link Socket}.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link SessionOutputBufferImpl}
+ */
+@NotThreadSafe
+@Deprecated
+public class SocketOutputBuffer extends AbstractSessionOutputBuffer {
+
+ /**
+ * Creates an instance of this class.
+ *
+ * @param socket the socket to write data to.
+ * @param buffersize the size of the internal buffer. If this number is less
+ * than <code>0</code> it is set to the value of
+ * {@link Socket#getSendBufferSize()}. If resultant number is less
+ * than <code>1024</code> it is set to <code>1024</code>.
+ * @param params HTTP parameters.
+ */
+ public SocketOutputBuffer(
+ final Socket socket,
+ final int buffersize,
+ final HttpParams params) throws IOException {
+ super();
+ Args.notNull(socket, "Socket");
+ int n = buffersize;
+ if (n < 0) {
+ n = socket.getSendBufferSize();
+ }
+ if (n < 1024) {
+ n = 1024;
+ }
+ init(socket.getOutputStream(), n, params);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/package-info.java
new file mode 100644
index 0000000000..24c7bee79a
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/io/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Default implementations of message parses and writers
+ * for synchronous, blocking communication.
+ */
+package ch.boye.httpclientandroidlib.impl.io;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/package-info.java
new file mode 100644
index 0000000000..4cf3cdb3a7
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Default implementations of HTTP connections for synchronous,
+ * blocking communication.
+ */
+package ch.boye.httpclientandroidlib.impl;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/pool/BasicConnFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/pool/BasicConnFactory.java
new file mode 100644
index 0000000000..a9e26a435e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/pool/BasicConnFactory.java
@@ -0,0 +1,177 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.pool;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLSocketFactory;
+
+import ch.boye.httpclientandroidlib.HttpClientConnection;
+import ch.boye.httpclientandroidlib.HttpConnectionFactory;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.config.ConnectionConfig;
+import ch.boye.httpclientandroidlib.config.SocketConfig;
+import ch.boye.httpclientandroidlib.impl.DefaultBHttpClientConnection;
+import ch.boye.httpclientandroidlib.impl.DefaultBHttpClientConnectionFactory;
+import ch.boye.httpclientandroidlib.params.CoreConnectionPNames;
+import ch.boye.httpclientandroidlib.params.HttpParamConfig;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.pool.ConnFactory;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * A very basic {@link ConnFactory} implementation that creates
+ * {@link HttpClientConnection} instances given a {@link HttpHost} instance.
+ *
+ * @see HttpHost
+ * @since 4.2
+ */
+@SuppressWarnings("deprecation")
+@Immutable
+public class BasicConnFactory implements ConnFactory<HttpHost, HttpClientConnection> {
+
+ private final SocketFactory plainfactory;
+ private final SSLSocketFactory sslfactory;
+ private final int connectTimeout;
+ private final SocketConfig sconfig;
+ private final HttpConnectionFactory<? extends HttpClientConnection> connFactory;
+
+ /**
+ * @deprecated (4.3) use
+ * {@link BasicConnFactory#BasicConnFactory(SocketFactory, SSLSocketFactory, int,
+ * SocketConfig, ConnectionConfig)}.
+ */
+ @Deprecated
+ public BasicConnFactory(final SSLSocketFactory sslfactory, final HttpParams params) {
+ super();
+ Args.notNull(params, "HTTP params");
+ this.plainfactory = null;
+ this.sslfactory = sslfactory;
+ this.connectTimeout = params.getIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 0);
+ this.sconfig = HttpParamConfig.getSocketConfig(params);
+ this.connFactory = new DefaultBHttpClientConnectionFactory(
+ HttpParamConfig.getConnectionConfig(params));
+ }
+
+ /**
+ * @deprecated (4.3) use
+ * {@link BasicConnFactory#BasicConnFactory(int, SocketConfig, ConnectionConfig)}.
+ */
+ @Deprecated
+ public BasicConnFactory(final HttpParams params) {
+ this(null, params);
+ }
+
+ /**
+ * @since 4.3
+ */
+ public BasicConnFactory(
+ final SocketFactory plainfactory,
+ final SSLSocketFactory sslfactory,
+ final int connectTimeout,
+ final SocketConfig sconfig,
+ final ConnectionConfig cconfig) {
+ super();
+ this.plainfactory = plainfactory;
+ this.sslfactory = sslfactory;
+ this.connectTimeout = connectTimeout;
+ this.sconfig = sconfig != null ? sconfig : SocketConfig.DEFAULT;
+ this.connFactory = new DefaultBHttpClientConnectionFactory(
+ cconfig != null ? cconfig : ConnectionConfig.DEFAULT);
+ }
+
+ /**
+ * @since 4.3
+ */
+ public BasicConnFactory(
+ final int connectTimeout, final SocketConfig sconfig, final ConnectionConfig cconfig) {
+ this(null, null, connectTimeout, sconfig, cconfig);
+ }
+
+ /**
+ * @since 4.3
+ */
+ public BasicConnFactory(final SocketConfig sconfig, final ConnectionConfig cconfig) {
+ this(null, null, 0, sconfig, cconfig);
+ }
+
+ /**
+ * @since 4.3
+ */
+ public BasicConnFactory() {
+ this(null, null, 0, SocketConfig.DEFAULT, ConnectionConfig.DEFAULT);
+ }
+
+ /**
+ * @deprecated (4.3) no longer used.
+ */
+ @Deprecated
+ protected HttpClientConnection create(final Socket socket, final HttpParams params) throws IOException {
+ final int bufsize = params.getIntParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE, 8 * 1024);
+ final DefaultBHttpClientConnection conn = new DefaultBHttpClientConnection(bufsize);
+ conn.bind(socket);
+ return conn;
+ }
+
+ public HttpClientConnection create(final HttpHost host) throws IOException {
+ final String scheme = host.getSchemeName();
+ Socket socket = null;
+ if ("http".equalsIgnoreCase(scheme)) {
+ socket = this.plainfactory != null ? this.plainfactory.createSocket() :
+ new Socket();
+ } if ("https".equalsIgnoreCase(scheme)) {
+ socket = (this.sslfactory != null ? this.sslfactory :
+ SSLSocketFactory.getDefault()).createSocket();
+ }
+ if (socket == null) {
+ throw new IOException(scheme + " scheme is not supported");
+ }
+ final String hostname = host.getHostName();
+ int port = host.getPort();
+ if (port == -1) {
+ if (host.getSchemeName().equalsIgnoreCase("http")) {
+ port = 80;
+ } else if (host.getSchemeName().equalsIgnoreCase("https")) {
+ port = 443;
+ }
+ }
+ socket.setSoTimeout(this.sconfig.getSoTimeout());
+ socket.connect(new InetSocketAddress(hostname, port), this.connectTimeout);
+ socket.setTcpNoDelay(this.sconfig.isTcpNoDelay());
+ final int linger = this.sconfig.getSoLinger();
+ if (linger >= 0) {
+ socket.setSoLinger(linger > 0, linger);
+ }
+ socket.setKeepAlive(this.sconfig.isSoKeepAlive());
+ return this.connFactory.createConnection(socket);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/pool/BasicConnPool.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/pool/BasicConnPool.java
new file mode 100644
index 0000000000..c99badd3e9
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/pool/BasicConnPool.java
@@ -0,0 +1,89 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.pool;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+import ch.boye.httpclientandroidlib.HttpClientConnection;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.config.ConnectionConfig;
+import ch.boye.httpclientandroidlib.config.SocketConfig;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.pool.AbstractConnPool;
+import ch.boye.httpclientandroidlib.pool.ConnFactory;
+
+/**
+ * A very basic {@link ch.boye.httpclientandroidlib.pool.ConnPool} implementation that
+ * represents a pool of blocking {@link HttpClientConnection} connections
+ * identified by an {@link HttpHost} instance. Please note this pool
+ * implementation does not support complex routes via a proxy cannot
+ * differentiate between direct and proxied connections.
+ *
+ * @see HttpHost
+ * @since 4.2
+ */
+@SuppressWarnings("deprecation")
+@ThreadSafe
+public class BasicConnPool extends AbstractConnPool<HttpHost, HttpClientConnection, BasicPoolEntry> {
+
+ private static final AtomicLong COUNTER = new AtomicLong();
+
+ public BasicConnPool(final ConnFactory<HttpHost, HttpClientConnection> connFactory) {
+ super(connFactory, 2, 20);
+ }
+
+ /**
+ * @deprecated (4.3) use {@link BasicConnPool#BasicConnPool(SocketConfig, ConnectionConfig)}
+ */
+ @Deprecated
+ public BasicConnPool(final HttpParams params) {
+ super(new BasicConnFactory(params), 2, 20);
+ }
+
+ /**
+ * @since 4.3
+ */
+ public BasicConnPool(final SocketConfig sconfig, final ConnectionConfig cconfig) {
+ super(new BasicConnFactory(sconfig, cconfig), 2, 20);
+ }
+
+ /**
+ * @since 4.3
+ */
+ public BasicConnPool() {
+ super(new BasicConnFactory(SocketConfig.DEFAULT, ConnectionConfig.DEFAULT), 2, 20);
+ }
+
+ @Override
+ protected BasicPoolEntry createEntry(
+ final HttpHost host,
+ final HttpClientConnection conn) {
+ return new BasicPoolEntry(Long.toString(COUNTER.getAndIncrement()), host, conn);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/pool/BasicPoolEntry.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/pool/BasicPoolEntry.java
new file mode 100644
index 0000000000..465525c33f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/pool/BasicPoolEntry.java
@@ -0,0 +1,64 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.impl.pool;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpClientConnection;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.pool.PoolEntry;
+
+/**
+ * A very basic {@link PoolEntry} implementation that represents an entry
+ * in a pool of blocking {@link HttpClientConnection}s identified by
+ * an {@link HttpHost} instance.
+ *
+ * @see HttpHost
+ * @since 4.2
+ */
+@ThreadSafe
+public class BasicPoolEntry extends PoolEntry<HttpHost, HttpClientConnection> {
+
+ public BasicPoolEntry(final String id, final HttpHost route, final HttpClientConnection conn) {
+ super(id, route, conn);
+ }
+
+ @Override
+ public void close() {
+ try {
+ this.getConnection().close();
+ } catch (final IOException ignore) {
+ }
+ }
+
+ @Override
+ public boolean isClosed() {
+ return !this.getConnection().isOpen();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/pool/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/pool/package-info.java
new file mode 100644
index 0000000000..534e1885c6
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/pool/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Default implementations of client side connection pools
+ * for synchronous, blocking communication.
+ */
+package ch.boye.httpclientandroidlib.impl.pool;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/BufferInfo.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/BufferInfo.java
new file mode 100644
index 0000000000..b75ddd8fc0
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/BufferInfo.java
@@ -0,0 +1,58 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.io;
+
+/**
+ * Basic buffer properties.
+ *
+ * @since 4.1
+ */
+public interface BufferInfo {
+
+ /**
+ * Return length data stored in the buffer
+ *
+ * @return data length
+ */
+ int length();
+
+ /**
+ * Returns total capacity of the buffer
+ *
+ * @return total capacity
+ */
+ int capacity();
+
+ /**
+ * Returns available space in the buffer.
+ *
+ * @return available space.
+ */
+ int available();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/EofSensor.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/EofSensor.java
new file mode 100644
index 0000000000..bd577b7e42
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/EofSensor.java
@@ -0,0 +1,42 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.io;
+
+/**
+ * EOF sensor.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) no longer used.
+ */
+@Deprecated
+public interface EofSensor {
+
+ boolean isEof();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/HttpMessageParser.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/HttpMessageParser.java
new file mode 100644
index 0000000000..2a6f4f3f86
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/HttpMessageParser.java
@@ -0,0 +1,56 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.io;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpMessage;
+
+/**
+ * Abstract message parser intended to build HTTP messages from an arbitrary data source.
+ *
+ * @param <T>
+ * {@link HttpMessage} or a subclass
+ *
+ * @since 4.0
+ */
+public interface HttpMessageParser<T extends HttpMessage> {
+
+ /**
+ * Generates an instance of {@link HttpMessage} from the underlying data
+ * source.
+ *
+ * @return HTTP message
+ * @throws IOException in case of an I/O error
+ * @throws HttpException in case of HTTP protocol violation
+ */
+ T parse()
+ throws IOException, HttpException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/HttpMessageParserFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/HttpMessageParserFactory.java
new file mode 100644
index 0000000000..c550120a5a
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/HttpMessageParserFactory.java
@@ -0,0 +1,42 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.io;
+
+import ch.boye.httpclientandroidlib.HttpMessage;
+import ch.boye.httpclientandroidlib.config.MessageConstraints;
+
+/**
+ * Factory for {@link HttpMessageParser} instances.
+ *
+ * @since 4.3
+ */
+public interface HttpMessageParserFactory<T extends HttpMessage> {
+
+ HttpMessageParser<T> create(SessionInputBuffer buffer, MessageConstraints constraints);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/HttpMessageWriter.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/HttpMessageWriter.java
new file mode 100644
index 0000000000..7b4557e92d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/HttpMessageWriter.java
@@ -0,0 +1,54 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.io;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpMessage;
+
+/**
+ * Abstract message writer intended to serialize HTTP messages to an arbitrary
+ * data sink.
+ *
+ * @since 4.0
+ */
+public interface HttpMessageWriter<T extends HttpMessage> {
+
+ /**
+ * Serializes an instance of {@link HttpMessage} to the underlying data
+ * sink.
+ *
+ * @param message HTTP message
+ * @throws IOException in case of an I/O error
+ * @throws HttpException in case of HTTP protocol violation
+ */
+ void write(T message)
+ throws IOException, HttpException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/HttpMessageWriterFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/HttpMessageWriterFactory.java
new file mode 100644
index 0000000000..3e8ad05524
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/HttpMessageWriterFactory.java
@@ -0,0 +1,41 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.io;
+
+import ch.boye.httpclientandroidlib.HttpMessage;
+
+/**
+ * Factory for {@link HttpMessageWriter} instances.
+ *
+ * @since 4.3
+ */
+public interface HttpMessageWriterFactory<T extends HttpMessage> {
+
+ HttpMessageWriter<T> create(SessionOutputBuffer buffer);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/HttpTransportMetrics.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/HttpTransportMetrics.java
new file mode 100644
index 0000000000..6eeb6ede11
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/HttpTransportMetrics.java
@@ -0,0 +1,48 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.io;
+
+/**
+ * The point of access to the statistics of {@link SessionInputBuffer} or
+ * {@link SessionOutputBuffer}.
+ *
+ * @since 4.0
+ */
+public interface HttpTransportMetrics {
+
+ /**
+ * Returns the number of bytes transferred.
+ */
+ long getBytesTransferred();
+
+ /**
+ * Resets the counts
+ */
+ void reset();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/SessionInputBuffer.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/SessionInputBuffer.java
new file mode 100644
index 0000000000..80417801db
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/SessionInputBuffer.java
@@ -0,0 +1,152 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.io;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Session input buffer for blocking connections. This interface is similar to
+ * InputStream class, but it also provides methods for reading lines of text.
+ * <p>
+ * Implementing classes are also expected to manage intermediate data buffering
+ * for optimal input performance.
+ *
+ * @since 4.0
+ */
+public interface SessionInputBuffer {
+
+ /**
+ * Reads up to <code>len</code> bytes of data from the session buffer into
+ * an array of bytes. An attempt is made to read as many as
+ * <code>len</code> bytes, but a smaller number may be read, possibly
+ * zero. The number of bytes actually read is returned as an integer.
+ *
+ * <p> This method blocks until input data is available, end of file is
+ * detected, or an exception is thrown.
+ *
+ * <p> If <code>off</code> is negative, or <code>len</code> is negative, or
+ * <code>off+len</code> is greater than the length of the array
+ * <code>b</code>, then an <code>IndexOutOfBoundsException</code> is
+ * thrown.
+ *
+ * @param b the buffer into which the data is read.
+ * @param off the start offset in array <code>b</code>
+ * at which the data is written.
+ * @param len the maximum number of bytes to read.
+ * @return the total number of bytes read into the buffer, or
+ * <code>-1</code> if there is no more data because the end of
+ * the stream has been reached.
+ * @exception IOException if an I/O error occurs.
+ */
+ int read(byte[] b, int off, int len) throws IOException;
+
+ /**
+ * Reads some number of bytes from the session buffer and stores them into
+ * the buffer array <code>b</code>. The number of bytes actually read is
+ * returned as an integer. This method blocks until input data is
+ * available, end of file is detected, or an exception is thrown.
+ *
+ * @param b the buffer into which the data is read.
+ * @return the total number of bytes read into the buffer, or
+ * <code>-1</code> is there is no more data because the end of
+ * the stream has been reached.
+ * @exception IOException if an I/O error occurs.
+ */
+ int read(byte[] b) throws IOException;
+
+ /**
+ * Reads the next byte of data from this session buffer. The value byte is
+ * returned as an <code>int</code> in the range <code>0</code> to
+ * <code>255</code>. If no byte is available because the end of the stream
+ * has been reached, the value <code>-1</code> is returned. This method
+ * blocks until input data is available, the end of the stream is detected,
+ * or an exception is thrown.
+ *
+ * @return the next byte of data, or <code>-1</code> if the end of the
+ * stream is reached.
+ * @exception IOException if an I/O error occurs.
+ */
+ int read() throws IOException;
+
+ /**
+ * Reads a complete line of characters up to a line delimiter from this
+ * session buffer into the given line buffer. The number of chars actually
+ * read is returned as an integer. The line delimiter itself is discarded.
+ * If no char is available because the end of the stream has been reached,
+ * the value <code>-1</code> is returned. This method blocks until input
+ * data is available, end of file is detected, or an exception is thrown.
+ * <p>
+ * The choice of a char encoding and line delimiter sequence is up to the
+ * specific implementations of this interface.
+ *
+ * @param buffer the line buffer.
+ * @return one line of characters
+ * @exception IOException if an I/O error occurs.
+ */
+ int readLine(CharArrayBuffer buffer) throws IOException;
+
+ /**
+ * Reads a complete line of characters up to a line delimiter from this
+ * session buffer. The line delimiter itself is discarded. If no char is
+ * available because the end of the stream has been reached,
+ * <code>null</code> is returned. This method blocks until input data is
+ * available, end of file is detected, or an exception is thrown.
+ * <p>
+ * The choice of a char encoding and line delimiter sequence is up to the
+ * specific implementations of this interface.
+ *
+ * @return HTTP line as a string
+ * @exception IOException if an I/O error occurs.
+ */
+ String readLine() throws IOException;
+
+ /** Blocks until some data becomes available in the session buffer or the
+ * given timeout period in milliseconds elapses. If the timeout value is
+ * <code>0</code> this method blocks indefinitely.
+ *
+ * @param timeout in milliseconds.
+ * @return <code>true</code> if some data is available in the session
+ * buffer or <code>false</code> otherwise.
+ * @exception IOException if an I/O error occurs.
+ *
+ * @deprecated (4.3) do not use. This function should be provided at the
+ * connection level
+ */
+ @Deprecated
+ boolean isDataAvailable(int timeout) throws IOException;
+
+ /**
+ * Returns {@link HttpTransportMetrics} for this session buffer.
+ *
+ * @return transport metrics.
+ */
+ HttpTransportMetrics getMetrics();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/SessionOutputBuffer.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/SessionOutputBuffer.java
new file mode 100644
index 0000000000..544d17d5a6
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/SessionOutputBuffer.java
@@ -0,0 +1,120 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.io;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Session output buffer for blocking connections. This interface is similar to
+ * OutputStream class, but it also provides methods for writing lines of text.
+ * <p>
+ * Implementing classes are also expected to manage intermediate data buffering
+ * for optimal output performance.
+ *
+ * @since 4.0
+ */
+public interface SessionOutputBuffer {
+
+ /**
+ * Writes <code>len</code> bytes from the specified byte array
+ * starting at offset <code>off</code> to this session buffer.
+ * <p>
+ * If <code>off</code> is negative, or <code>len</code> is negative, or
+ * <code>off+len</code> is greater than the length of the array
+ * <code>b</code>, then an <tt>IndexOutOfBoundsException</tt> is thrown.
+ *
+ * @param b the data.
+ * @param off the start offset in the data.
+ * @param len the number of bytes to write.
+ * @exception IOException if an I/O error occurs.
+ */
+ void write(byte[] b, int off, int len) throws IOException;
+
+ /**
+ * Writes <code>b.length</code> bytes from the specified byte array
+ * to this session buffer.
+ *
+ * @param b the data.
+ * @exception IOException if an I/O error occurs.
+ */
+ void write(byte[] b) throws IOException;
+
+ /**
+ * Writes the specified byte to this session buffer.
+ *
+ * @param b the <code>byte</code>.
+ * @exception IOException if an I/O error occurs.
+ */
+ void write(int b) throws IOException;
+
+ /**
+ * Writes characters from the specified string followed by a line delimiter
+ * to this session buffer.
+ * <p>
+ * The choice of a char encoding and line delimiter sequence is up to the
+ * specific implementations of this interface.
+ *
+ * @param s the line.
+ * @exception IOException if an I/O error occurs.
+ */
+ void writeLine(String s) throws IOException;
+
+ /**
+ * Writes characters from the specified char array followed by a line
+ * delimiter to this session buffer.
+ * <p>
+ * The choice of a char encoding and line delimiter sequence is up to the
+ * specific implementations of this interface.
+ *
+ * @param buffer the buffer containing chars of the line.
+ * @exception IOException if an I/O error occurs.
+ */
+ void writeLine(CharArrayBuffer buffer) throws IOException;
+
+ /**
+ * Flushes this session buffer and forces any buffered output bytes
+ * to be written out. The general contract of <code>flush</code> is
+ * that calling it is an indication that, if any bytes previously
+ * written have been buffered by the implementation of the output
+ * stream, such bytes should immediately be written to their
+ * intended destination.
+ *
+ * @exception IOException if an I/O error occurs.
+ */
+ void flush() throws IOException;
+
+ /**
+ * Returns {@link HttpTransportMetrics} for this session buffer.
+ *
+ * @return transport metrics.
+ */
+ HttpTransportMetrics getMetrics();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/package-info.java
new file mode 100644
index 0000000000..8899c5ad6e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/io/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * HTTP message parser and writer APIs for synchronous, blocking
+ * communication.
+ */
+package ch.boye.httpclientandroidlib.io;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/AbstractHttpMessage.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/AbstractHttpMessage.java
new file mode 100644
index 0000000000..24f7aef4e4
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/AbstractHttpMessage.java
@@ -0,0 +1,164 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderIterator;
+import ch.boye.httpclientandroidlib.HttpMessage;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.params.BasicHttpParams;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Basic implementation of {@link HttpMessage}.
+ *
+ * @since 4.0
+ */
+@SuppressWarnings("deprecation")
+@NotThreadSafe
+public abstract class AbstractHttpMessage implements HttpMessage {
+
+ protected HeaderGroup headergroup;
+
+ @Deprecated
+ protected HttpParams params;
+
+ /**
+ * @deprecated (4.3) use {@link AbstractHttpMessage#AbstractHttpMessage()}
+ */
+ @Deprecated
+ protected AbstractHttpMessage(final HttpParams params) {
+ super();
+ this.headergroup = new HeaderGroup();
+ this.params = params;
+ }
+
+ protected AbstractHttpMessage() {
+ this(null);
+ }
+
+ // non-javadoc, see interface HttpMessage
+ public boolean containsHeader(final String name) {
+ return this.headergroup.containsHeader(name);
+ }
+
+ // non-javadoc, see interface HttpMessage
+ public Header[] getHeaders(final String name) {
+ return this.headergroup.getHeaders(name);
+ }
+
+ // non-javadoc, see interface HttpMessage
+ public Header getFirstHeader(final String name) {
+ return this.headergroup.getFirstHeader(name);
+ }
+
+ // non-javadoc, see interface HttpMessage
+ public Header getLastHeader(final String name) {
+ return this.headergroup.getLastHeader(name);
+ }
+
+ // non-javadoc, see interface HttpMessage
+ public Header[] getAllHeaders() {
+ return this.headergroup.getAllHeaders();
+ }
+
+ // non-javadoc, see interface HttpMessage
+ public void addHeader(final Header header) {
+ this.headergroup.addHeader(header);
+ }
+
+ // non-javadoc, see interface HttpMessage
+ public void addHeader(final String name, final String value) {
+ Args.notNull(name, "Header name");
+ this.headergroup.addHeader(new BasicHeader(name, value));
+ }
+
+ // non-javadoc, see interface HttpMessage
+ public void setHeader(final Header header) {
+ this.headergroup.updateHeader(header);
+ }
+
+ // non-javadoc, see interface HttpMessage
+ public void setHeader(final String name, final String value) {
+ Args.notNull(name, "Header name");
+ this.headergroup.updateHeader(new BasicHeader(name, value));
+ }
+
+ // non-javadoc, see interface HttpMessage
+ public void setHeaders(final Header[] headers) {
+ this.headergroup.setHeaders(headers);
+ }
+
+ // non-javadoc, see interface HttpMessage
+ public void removeHeader(final Header header) {
+ this.headergroup.removeHeader(header);
+ }
+
+ // non-javadoc, see interface HttpMessage
+ public void removeHeaders(final String name) {
+ if (name == null) {
+ return;
+ }
+ for (final HeaderIterator i = this.headergroup.iterator(); i.hasNext(); ) {
+ final Header header = i.nextHeader();
+ if (name.equalsIgnoreCase(header.getName())) {
+ i.remove();
+ }
+ }
+ }
+
+ // non-javadoc, see interface HttpMessage
+ public HeaderIterator headerIterator() {
+ return this.headergroup.iterator();
+ }
+
+ // non-javadoc, see interface HttpMessage
+ public HeaderIterator headerIterator(final String name) {
+ return this.headergroup.iterator(name);
+ }
+
+ /**
+ * @deprecated (4.3) use constructor parameters of configuration API provided by HttpClient
+ */
+ @Deprecated
+ public HttpParams getParams() {
+ if (this.params == null) {
+ this.params = new BasicHttpParams();
+ }
+ return this.params;
+ }
+
+ /**
+ * @deprecated (4.3) use constructor parameters of configuration API provided by HttpClient
+ */
+ @Deprecated
+ public void setParams(final HttpParams params) {
+ this.params = Args.notNull(params, "HTTP parameters");
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeader.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeader.java
new file mode 100644
index 0000000000..170bf6bfec
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeader.java
@@ -0,0 +1,91 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import java.io.Serializable;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.ParseException;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Basic implementation of {@link Header}.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class BasicHeader implements Header, Cloneable, Serializable {
+
+ private static final long serialVersionUID = -5427236326487562174L;
+
+ private final String name;
+ private final String value;
+
+ /**
+ * Constructor with name and value
+ *
+ * @param name the header name
+ * @param value the header value
+ */
+ public BasicHeader(final String name, final String value) {
+ super();
+ this.name = Args.notNull(name, "Name");
+ this.value = value;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public String getValue() {
+ return this.value;
+ }
+
+ @Override
+ public String toString() {
+ // no need for non-default formatting in toString()
+ return BasicLineFormatter.INSTANCE.formatHeader(null, this).toString();
+ }
+
+ public HeaderElement[] getElements() throws ParseException {
+ if (this.value != null) {
+ // result intentionally not cached, it's probably not used again
+ return BasicHeaderValueParser.parseElements(this.value, null);
+ } else {
+ return new HeaderElement[] {};
+ }
+ }
+
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeaderElement.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeaderElement.java
new file mode 100644
index 0000000000..e815f2cb1e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeaderElement.java
@@ -0,0 +1,162 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.LangUtils;
+
+/**
+ * Basic implementation of {@link HeaderElement}
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class BasicHeaderElement implements HeaderElement, Cloneable {
+
+ private final String name;
+ private final String value;
+ private final NameValuePair[] parameters;
+
+ /**
+ * Constructor with name, value and parameters.
+ *
+ * @param name header element name
+ * @param value header element value. May be <tt>null</tt>
+ * @param parameters header element parameters. May be <tt>null</tt>.
+ * Parameters are copied by reference, not by value
+ */
+ public BasicHeaderElement(
+ final String name,
+ final String value,
+ final NameValuePair[] parameters) {
+ super();
+ this.name = Args.notNull(name, "Name");
+ this.value = value;
+ if (parameters != null) {
+ this.parameters = parameters;
+ } else {
+ this.parameters = new NameValuePair[] {};
+ }
+ }
+
+ /**
+ * Constructor with name and value.
+ *
+ * @param name header element name
+ * @param value header element value. May be <tt>null</tt>
+ */
+ public BasicHeaderElement(final String name, final String value) {
+ this(name, value, null);
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public String getValue() {
+ return this.value;
+ }
+
+ public NameValuePair[] getParameters() {
+ return this.parameters.clone();
+ }
+
+ public int getParameterCount() {
+ return this.parameters.length;
+ }
+
+ public NameValuePair getParameter(final int index) {
+ // ArrayIndexOutOfBoundsException is appropriate
+ return this.parameters[index];
+ }
+
+ public NameValuePair getParameterByName(final String name) {
+ Args.notNull(name, "Name");
+ NameValuePair found = null;
+ for (final NameValuePair current : this.parameters) {
+ if (current.getName().equalsIgnoreCase(name)) {
+ found = current;
+ break;
+ }
+ }
+ return found;
+ }
+
+ @Override
+ public boolean equals(final Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object instanceof HeaderElement) {
+ final BasicHeaderElement that = (BasicHeaderElement) object;
+ return this.name.equals(that.name)
+ && LangUtils.equals(this.value, that.value)
+ && LangUtils.equals(this.parameters, that.parameters);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = LangUtils.HASH_SEED;
+ hash = LangUtils.hashCode(hash, this.name);
+ hash = LangUtils.hashCode(hash, this.value);
+ for (final NameValuePair parameter : this.parameters) {
+ hash = LangUtils.hashCode(hash, parameter);
+ }
+ return hash;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder buffer = new StringBuilder();
+ buffer.append(this.name);
+ if (this.value != null) {
+ buffer.append("=");
+ buffer.append(this.value);
+ }
+ for (final NameValuePair parameter : this.parameters) {
+ buffer.append("; ");
+ buffer.append(parameter);
+ }
+ return buffer.toString();
+ }
+
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ // parameters array is considered immutable
+ // no need to make a copy of it
+ return super.clone();
+ }
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeaderElementIterator.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeaderElementIterator.java
new file mode 100644
index 0000000000..f14f00f0d1
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeaderElementIterator.java
@@ -0,0 +1,151 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import java.util.NoSuchElementException;
+
+import ch.boye.httpclientandroidlib.FormattedHeader;
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.HeaderElementIterator;
+import ch.boye.httpclientandroidlib.HeaderIterator;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Basic implementation of a {@link HeaderElementIterator}.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class BasicHeaderElementIterator implements HeaderElementIterator {
+
+ private final HeaderIterator headerIt;
+ private final HeaderValueParser parser;
+
+ private HeaderElement currentElement = null;
+ private CharArrayBuffer buffer = null;
+ private ParserCursor cursor = null;
+
+ /**
+ * Creates a new instance of BasicHeaderElementIterator
+ */
+ public BasicHeaderElementIterator(
+ final HeaderIterator headerIterator,
+ final HeaderValueParser parser) {
+ this.headerIt = Args.notNull(headerIterator, "Header iterator");
+ this.parser = Args.notNull(parser, "Parser");
+ }
+
+
+ public BasicHeaderElementIterator(final HeaderIterator headerIterator) {
+ this(headerIterator, BasicHeaderValueParser.INSTANCE);
+ }
+
+
+ private void bufferHeaderValue() {
+ this.cursor = null;
+ this.buffer = null;
+ while (this.headerIt.hasNext()) {
+ final Header h = this.headerIt.nextHeader();
+ if (h instanceof FormattedHeader) {
+ this.buffer = ((FormattedHeader) h).getBuffer();
+ this.cursor = new ParserCursor(0, this.buffer.length());
+ this.cursor.updatePos(((FormattedHeader) h).getValuePos());
+ break;
+ } else {
+ final String value = h.getValue();
+ if (value != null) {
+ this.buffer = new CharArrayBuffer(value.length());
+ this.buffer.append(value);
+ this.cursor = new ParserCursor(0, this.buffer.length());
+ break;
+ }
+ }
+ }
+ }
+
+ private void parseNextElement() {
+ // loop while there are headers left to parse
+ while (this.headerIt.hasNext() || this.cursor != null) {
+ if (this.cursor == null || this.cursor.atEnd()) {
+ // get next header value
+ bufferHeaderValue();
+ }
+ // Anything buffered?
+ if (this.cursor != null) {
+ // loop while there is data in the buffer
+ while (!this.cursor.atEnd()) {
+ final HeaderElement e = this.parser.parseHeaderElement(this.buffer, this.cursor);
+ if (!(e.getName().length() == 0 && e.getValue() == null)) {
+ // Found something
+ this.currentElement = e;
+ return;
+ }
+ }
+ // if at the end of the buffer
+ if (this.cursor.atEnd()) {
+ // discard it
+ this.cursor = null;
+ this.buffer = null;
+ }
+ }
+ }
+ }
+
+ public boolean hasNext() {
+ if (this.currentElement == null) {
+ parseNextElement();
+ }
+ return this.currentElement != null;
+ }
+
+ public HeaderElement nextElement() throws NoSuchElementException {
+ if (this.currentElement == null) {
+ parseNextElement();
+ }
+
+ if (this.currentElement == null) {
+ throw new NoSuchElementException("No more header elements available");
+ }
+
+ final HeaderElement element = this.currentElement;
+ this.currentElement = null;
+ return element;
+ }
+
+ public final Object next() throws NoSuchElementException {
+ return nextElement();
+ }
+
+ public void remove() throws UnsupportedOperationException {
+ throw new UnsupportedOperationException("Remove not supported");
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeaderIterator.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeaderIterator.java
new file mode 100644
index 0000000000..5c8c87fbf4
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeaderIterator.java
@@ -0,0 +1,175 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import java.util.NoSuchElementException;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderIterator;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Basic implementation of a {@link HeaderIterator}.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class BasicHeaderIterator implements HeaderIterator {
+
+ /**
+ * An array of headers to iterate over.
+ * Not all elements of this array are necessarily part of the iteration.
+ * This array will never be modified by the iterator.
+ * Derived implementations are expected to adhere to this restriction.
+ */
+ protected final Header[] allHeaders;
+
+
+ /**
+ * The position of the next header in {@link #allHeaders allHeaders}.
+ * Negative if the iteration is over.
+ */
+ protected int currentIndex;
+
+
+ /**
+ * The header name to filter by.
+ * <code>null</code> to iterate over all headers in the array.
+ */
+ protected String headerName;
+
+
+
+ /**
+ * Creates a new header iterator.
+ *
+ * @param headers an array of headers over which to iterate
+ * @param name the name of the headers over which to iterate, or
+ * <code>null</code> for any
+ */
+ public BasicHeaderIterator(final Header[] headers, final String name) {
+ super();
+ this.allHeaders = Args.notNull(headers, "Header array");
+ this.headerName = name;
+ this.currentIndex = findNext(-1);
+ }
+
+
+ /**
+ * Determines the index of the next header.
+ *
+ * @param pos one less than the index to consider first,
+ * -1 to search for the first header
+ *
+ * @return the index of the next header that matches the filter name,
+ * or negative if there are no more headers
+ */
+ protected int findNext(final int pos) {
+ int from = pos;
+ if (from < -1) {
+ return -1;
+ }
+
+ final int to = this.allHeaders.length-1;
+ boolean found = false;
+ while (!found && (from < to)) {
+ from++;
+ found = filterHeader(from);
+ }
+ return found ? from : -1;
+ }
+
+
+ /**
+ * Checks whether a header is part of the iteration.
+ *
+ * @param index the index of the header to check
+ *
+ * @return <code>true</code> if the header should be part of the
+ * iteration, <code>false</code> to skip
+ */
+ protected boolean filterHeader(final int index) {
+ return (this.headerName == null) ||
+ this.headerName.equalsIgnoreCase(this.allHeaders[index].getName());
+ }
+
+
+ // non-javadoc, see interface HeaderIterator
+ public boolean hasNext() {
+ return (this.currentIndex >= 0);
+ }
+
+
+ /**
+ * Obtains the next header from this iteration.
+ *
+ * @return the next header in this iteration
+ *
+ * @throws NoSuchElementException if there are no more headers
+ */
+ public Header nextHeader()
+ throws NoSuchElementException {
+
+ final int current = this.currentIndex;
+ if (current < 0) {
+ throw new NoSuchElementException("Iteration already finished.");
+ }
+
+ this.currentIndex = findNext(current);
+
+ return this.allHeaders[current];
+ }
+
+
+ /**
+ * Returns the next header.
+ * Same as {@link #nextHeader nextHeader}, but not type-safe.
+ *
+ * @return the next header in this iteration
+ *
+ * @throws NoSuchElementException if there are no more headers
+ */
+ public final Object next()
+ throws NoSuchElementException {
+ return nextHeader();
+ }
+
+
+ /**
+ * Removing headers is not supported.
+ *
+ * @throws UnsupportedOperationException always
+ */
+ public void remove()
+ throws UnsupportedOperationException {
+
+ throw new UnsupportedOperationException
+ ("Removing headers is not supported.");
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeaderValueFormatter.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeaderValueFormatter.java
new file mode 100644
index 0000000000..b7e7317594
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeaderValueFormatter.java
@@ -0,0 +1,422 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Basic implementation for formatting header value elements.
+ * Instances of this class are stateless and thread-safe.
+ * Derived classes are expected to maintain these properties.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class BasicHeaderValueFormatter implements HeaderValueFormatter {
+
+ /**
+ * A default instance of this class, for use as default or fallback.
+ * Note that {@link BasicHeaderValueFormatter} is not a singleton, there
+ * can be many instances of the class itself and of derived classes.
+ * The instance here provides non-customized, default behavior.
+ *
+ * @deprecated (4.3) use {@link #INSTANCE}
+ */
+ @Deprecated
+ public final static
+ BasicHeaderValueFormatter DEFAULT = new BasicHeaderValueFormatter();
+
+ public final static BasicHeaderValueFormatter INSTANCE = new BasicHeaderValueFormatter();
+
+ /**
+ * Special characters that can be used as separators in HTTP parameters.
+ * These special characters MUST be in a quoted string to be used within
+ * a parameter value .
+ */
+ public final static String SEPARATORS = " ;,:@()<>\\\"/[]?={}\t";
+
+ /**
+ * Unsafe special characters that must be escaped using the backslash
+ * character
+ */
+ public final static String UNSAFE_CHARS = "\"\\";
+
+ public BasicHeaderValueFormatter() {
+ super();
+ }
+
+ /**
+ * Formats an array of header elements.
+ *
+ * @param elems the header elements to format
+ * @param quote <code>true</code> to always format with quoted values,
+ * <code>false</code> to use quotes only when necessary
+ * @param formatter the formatter to use, or <code>null</code>
+ * for the {@link #INSTANCE default}
+ *
+ * @return the formatted header elements
+ */
+ public static
+ String formatElements(final HeaderElement[] elems,
+ final boolean quote,
+ final HeaderValueFormatter formatter) {
+ return (formatter != null ? formatter : BasicHeaderValueFormatter.INSTANCE)
+ .formatElements(null, elems, quote).toString();
+ }
+
+
+ // non-javadoc, see interface HeaderValueFormatter
+ public CharArrayBuffer formatElements(final CharArrayBuffer charBuffer,
+ final HeaderElement[] elems,
+ final boolean quote) {
+ Args.notNull(elems, "Header element array");
+ final int len = estimateElementsLen(elems);
+ CharArrayBuffer buffer = charBuffer;
+ if (buffer == null) {
+ buffer = new CharArrayBuffer(len);
+ } else {
+ buffer.ensureCapacity(len);
+ }
+
+ for (int i=0; i<elems.length; i++) {
+ if (i > 0) {
+ buffer.append(", ");
+ }
+ formatHeaderElement(buffer, elems[i], quote);
+ }
+
+ return buffer;
+ }
+
+
+ /**
+ * Estimates the length of formatted header elements.
+ *
+ * @param elems the header elements to format, or <code>null</code>
+ *
+ * @return a length estimate, in number of characters
+ */
+ protected int estimateElementsLen(final HeaderElement[] elems) {
+ if ((elems == null) || (elems.length < 1)) {
+ return 0;
+ }
+
+ int result = (elems.length-1) * 2; // elements separated by ", "
+ for (final HeaderElement elem : elems) {
+ result += estimateHeaderElementLen(elem);
+ }
+
+ return result;
+ }
+
+
+
+ /**
+ * Formats a header element.
+ *
+ * @param elem the header element to format
+ * @param quote <code>true</code> to always format with quoted values,
+ * <code>false</code> to use quotes only when necessary
+ * @param formatter the formatter to use, or <code>null</code>
+ * for the {@link #INSTANCE default}
+ *
+ * @return the formatted header element
+ */
+ public static
+ String formatHeaderElement(final HeaderElement elem,
+ final boolean quote,
+ final HeaderValueFormatter formatter) {
+ return (formatter != null ? formatter : BasicHeaderValueFormatter.INSTANCE)
+ .formatHeaderElement(null, elem, quote).toString();
+ }
+
+
+ // non-javadoc, see interface HeaderValueFormatter
+ public CharArrayBuffer formatHeaderElement(final CharArrayBuffer charBuffer,
+ final HeaderElement elem,
+ final boolean quote) {
+ Args.notNull(elem, "Header element");
+ final int len = estimateHeaderElementLen(elem);
+ CharArrayBuffer buffer = charBuffer;
+ if (buffer == null) {
+ buffer = new CharArrayBuffer(len);
+ } else {
+ buffer.ensureCapacity(len);
+ }
+
+ buffer.append(elem.getName());
+ final String value = elem.getValue();
+ if (value != null) {
+ buffer.append('=');
+ doFormatValue(buffer, value, quote);
+ }
+
+ final int parcnt = elem.getParameterCount();
+ if (parcnt > 0) {
+ for (int i=0; i<parcnt; i++) {
+ buffer.append("; ");
+ formatNameValuePair(buffer, elem.getParameter(i), quote);
+ }
+ }
+
+ return buffer;
+ }
+
+
+ /**
+ * Estimates the length of a formatted header element.
+ *
+ * @param elem the header element to format, or <code>null</code>
+ *
+ * @return a length estimate, in number of characters
+ */
+ protected int estimateHeaderElementLen(final HeaderElement elem) {
+ if (elem == null) {
+ return 0;
+ }
+
+ int result = elem.getName().length(); // name
+ final String value = elem.getValue();
+ if (value != null) {
+ // assume quotes, but no escaped characters
+ result += 3 + value.length(); // ="value"
+ }
+
+ final int parcnt = elem.getParameterCount();
+ if (parcnt > 0) {
+ for (int i=0; i<parcnt; i++) {
+ result += 2 + // ; <param>
+ estimateNameValuePairLen(elem.getParameter(i));
+ }
+ }
+
+ return result;
+ }
+
+
+
+
+ /**
+ * Formats a set of parameters.
+ *
+ * @param nvps the parameters to format
+ * @param quote <code>true</code> to always format with quoted values,
+ * <code>false</code> to use quotes only when necessary
+ * @param formatter the formatter to use, or <code>null</code>
+ * for the {@link #INSTANCE default}
+ *
+ * @return the formatted parameters
+ */
+ public static
+ String formatParameters(final NameValuePair[] nvps,
+ final boolean quote,
+ final HeaderValueFormatter formatter) {
+ return (formatter != null ? formatter : BasicHeaderValueFormatter.INSTANCE)
+ .formatParameters(null, nvps, quote).toString();
+ }
+
+
+ // non-javadoc, see interface HeaderValueFormatter
+ public CharArrayBuffer formatParameters(final CharArrayBuffer charBuffer,
+ final NameValuePair[] nvps,
+ final boolean quote) {
+ Args.notNull(nvps, "Header parameter array");
+ final int len = estimateParametersLen(nvps);
+ CharArrayBuffer buffer = charBuffer;
+ if (buffer == null) {
+ buffer = new CharArrayBuffer(len);
+ } else {
+ buffer.ensureCapacity(len);
+ }
+
+ for (int i = 0; i < nvps.length; i++) {
+ if (i > 0) {
+ buffer.append("; ");
+ }
+ formatNameValuePair(buffer, nvps[i], quote);
+ }
+
+ return buffer;
+ }
+
+
+ /**
+ * Estimates the length of formatted parameters.
+ *
+ * @param nvps the parameters to format, or <code>null</code>
+ *
+ * @return a length estimate, in number of characters
+ */
+ protected int estimateParametersLen(final NameValuePair[] nvps) {
+ if ((nvps == null) || (nvps.length < 1)) {
+ return 0;
+ }
+
+ int result = (nvps.length-1) * 2; // "; " between the parameters
+ for (final NameValuePair nvp : nvps) {
+ result += estimateNameValuePairLen(nvp);
+ }
+
+ return result;
+ }
+
+
+ /**
+ * Formats a name-value pair.
+ *
+ * @param nvp the name-value pair to format
+ * @param quote <code>true</code> to always format with a quoted value,
+ * <code>false</code> to use quotes only when necessary
+ * @param formatter the formatter to use, or <code>null</code>
+ * for the {@link #INSTANCE default}
+ *
+ * @return the formatted name-value pair
+ */
+ public static
+ String formatNameValuePair(final NameValuePair nvp,
+ final boolean quote,
+ final HeaderValueFormatter formatter) {
+ return (formatter != null ? formatter : BasicHeaderValueFormatter.INSTANCE)
+ .formatNameValuePair(null, nvp, quote).toString();
+ }
+
+
+ // non-javadoc, see interface HeaderValueFormatter
+ public CharArrayBuffer formatNameValuePair(final CharArrayBuffer charBuffer,
+ final NameValuePair nvp,
+ final boolean quote) {
+ Args.notNull(nvp, "Name / value pair");
+ final int len = estimateNameValuePairLen(nvp);
+ CharArrayBuffer buffer = charBuffer;
+ if (buffer == null) {
+ buffer = new CharArrayBuffer(len);
+ } else {
+ buffer.ensureCapacity(len);
+ }
+
+ buffer.append(nvp.getName());
+ final String value = nvp.getValue();
+ if (value != null) {
+ buffer.append('=');
+ doFormatValue(buffer, value, quote);
+ }
+
+ return buffer;
+ }
+
+
+ /**
+ * Estimates the length of a formatted name-value pair.
+ *
+ * @param nvp the name-value pair to format, or <code>null</code>
+ *
+ * @return a length estimate, in number of characters
+ */
+ protected int estimateNameValuePairLen(final NameValuePair nvp) {
+ if (nvp == null) {
+ return 0;
+ }
+
+ int result = nvp.getName().length(); // name
+ final String value = nvp.getValue();
+ if (value != null) {
+ // assume quotes, but no escaped characters
+ result += 3 + value.length(); // ="value"
+ }
+ return result;
+ }
+
+
+ /**
+ * Actually formats the value of a name-value pair.
+ * This does not include a leading = character.
+ * Called from {@link #formatNameValuePair formatNameValuePair}.
+ *
+ * @param buffer the buffer to append to, never <code>null</code>
+ * @param value the value to append, never <code>null</code>
+ * @param quote <code>true</code> to always format with quotes,
+ * <code>false</code> to use quotes only when necessary
+ */
+ protected void doFormatValue(final CharArrayBuffer buffer,
+ final String value,
+ final boolean quote) {
+
+ boolean quoteFlag = quote;
+ if (!quoteFlag) {
+ for (int i = 0; (i < value.length()) && !quoteFlag; i++) {
+ quoteFlag = isSeparator(value.charAt(i));
+ }
+ }
+
+ if (quoteFlag) {
+ buffer.append('"');
+ }
+ for (int i = 0; i < value.length(); i++) {
+ final char ch = value.charAt(i);
+ if (isUnsafe(ch)) {
+ buffer.append('\\');
+ }
+ buffer.append(ch);
+ }
+ if (quoteFlag) {
+ buffer.append('"');
+ }
+ }
+
+
+ /**
+ * Checks whether a character is a {@link #SEPARATORS separator}.
+ *
+ * @param ch the character to check
+ *
+ * @return <code>true</code> if the character is a separator,
+ * <code>false</code> otherwise
+ */
+ protected boolean isSeparator(final char ch) {
+ return SEPARATORS.indexOf(ch) >= 0;
+ }
+
+
+ /**
+ * Checks whether a character is {@link #UNSAFE_CHARS unsafe}.
+ *
+ * @param ch the character to check
+ *
+ * @return <code>true</code> if the character is unsafe,
+ * <code>false</code> otherwise
+ */
+ protected boolean isUnsafe(final char ch) {
+ return UNSAFE_CHARS.indexOf(ch) >= 0;
+ }
+
+
+} // class BasicHeaderValueFormatter
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeaderValueParser.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeaderValueParser.java
new file mode 100644
index 0000000000..2e23b23bf6
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHeaderValueParser.java
@@ -0,0 +1,359 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.ParseException;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Basic implementation for parsing header values into elements.
+ * Instances of this class are stateless and thread-safe.
+ * Derived classes are expected to maintain these properties.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class BasicHeaderValueParser implements HeaderValueParser {
+
+ /**
+ * A default instance of this class, for use as default or fallback.
+ * Note that {@link BasicHeaderValueParser} is not a singleton, there
+ * can be many instances of the class itself and of derived classes.
+ * The instance here provides non-customized, default behavior.
+ *
+ * @deprecated (4.3) use {@link #INSTANCE}
+ */
+ @Deprecated
+ public final static
+ BasicHeaderValueParser DEFAULT = new BasicHeaderValueParser();
+
+ public final static BasicHeaderValueParser INSTANCE = new BasicHeaderValueParser();
+
+ private final static char PARAM_DELIMITER = ';';
+ private final static char ELEM_DELIMITER = ',';
+ private final static char[] ALL_DELIMITERS = new char[] {
+ PARAM_DELIMITER,
+ ELEM_DELIMITER
+ };
+
+ public BasicHeaderValueParser() {
+ super();
+ }
+
+ /**
+ * Parses elements with the given parser.
+ *
+ * @param value the header value to parse
+ * @param parser the parser to use, or <code>null</code> for default
+ *
+ * @return array holding the header elements, never <code>null</code>
+ */
+ public static
+ HeaderElement[] parseElements(final String value,
+ final HeaderValueParser parser) throws ParseException {
+ Args.notNull(value, "Value");
+
+ final CharArrayBuffer buffer = new CharArrayBuffer(value.length());
+ buffer.append(value);
+ final ParserCursor cursor = new ParserCursor(0, value.length());
+ return (parser != null ? parser : BasicHeaderValueParser.INSTANCE)
+ .parseElements(buffer, cursor);
+ }
+
+
+ // non-javadoc, see interface HeaderValueParser
+ public HeaderElement[] parseElements(final CharArrayBuffer buffer,
+ final ParserCursor cursor) {
+ Args.notNull(buffer, "Char array buffer");
+ Args.notNull(cursor, "Parser cursor");
+ final List<HeaderElement> elements = new ArrayList<HeaderElement>();
+ while (!cursor.atEnd()) {
+ final HeaderElement element = parseHeaderElement(buffer, cursor);
+ if (!(element.getName().length() == 0 && element.getValue() == null)) {
+ elements.add(element);
+ }
+ }
+ return elements.toArray(new HeaderElement[elements.size()]);
+ }
+
+
+ /**
+ * Parses an element with the given parser.
+ *
+ * @param value the header element to parse
+ * @param parser the parser to use, or <code>null</code> for default
+ *
+ * @return the parsed header element
+ */
+ public static
+ HeaderElement parseHeaderElement(final String value,
+ final HeaderValueParser parser) throws ParseException {
+ Args.notNull(value, "Value");
+
+ final CharArrayBuffer buffer = new CharArrayBuffer(value.length());
+ buffer.append(value);
+ final ParserCursor cursor = new ParserCursor(0, value.length());
+ return (parser != null ? parser : BasicHeaderValueParser.INSTANCE)
+ .parseHeaderElement(buffer, cursor);
+ }
+
+
+ // non-javadoc, see interface HeaderValueParser
+ public HeaderElement parseHeaderElement(final CharArrayBuffer buffer,
+ final ParserCursor cursor) {
+ Args.notNull(buffer, "Char array buffer");
+ Args.notNull(cursor, "Parser cursor");
+ final NameValuePair nvp = parseNameValuePair(buffer, cursor);
+ NameValuePair[] params = null;
+ if (!cursor.atEnd()) {
+ final char ch = buffer.charAt(cursor.getPos() - 1);
+ if (ch != ELEM_DELIMITER) {
+ params = parseParameters(buffer, cursor);
+ }
+ }
+ return createHeaderElement(nvp.getName(), nvp.getValue(), params);
+ }
+
+
+ /**
+ * Creates a header element.
+ * Called from {@link #parseHeaderElement}.
+ *
+ * @return a header element representing the argument
+ */
+ protected HeaderElement createHeaderElement(
+ final String name,
+ final String value,
+ final NameValuePair[] params) {
+ return new BasicHeaderElement(name, value, params);
+ }
+
+
+ /**
+ * Parses parameters with the given parser.
+ *
+ * @param value the parameter list to parse
+ * @param parser the parser to use, or <code>null</code> for default
+ *
+ * @return array holding the parameters, never <code>null</code>
+ */
+ public static
+ NameValuePair[] parseParameters(final String value,
+ final HeaderValueParser parser) throws ParseException {
+ Args.notNull(value, "Value");
+
+ final CharArrayBuffer buffer = new CharArrayBuffer(value.length());
+ buffer.append(value);
+ final ParserCursor cursor = new ParserCursor(0, value.length());
+ return (parser != null ? parser : BasicHeaderValueParser.INSTANCE)
+ .parseParameters(buffer, cursor);
+ }
+
+
+
+ // non-javadoc, see interface HeaderValueParser
+ public NameValuePair[] parseParameters(final CharArrayBuffer buffer,
+ final ParserCursor cursor) {
+ Args.notNull(buffer, "Char array buffer");
+ Args.notNull(cursor, "Parser cursor");
+ int pos = cursor.getPos();
+ final int indexTo = cursor.getUpperBound();
+
+ while (pos < indexTo) {
+ final char ch = buffer.charAt(pos);
+ if (HTTP.isWhitespace(ch)) {
+ pos++;
+ } else {
+ break;
+ }
+ }
+ cursor.updatePos(pos);
+ if (cursor.atEnd()) {
+ return new NameValuePair[] {};
+ }
+
+ final List<NameValuePair> params = new ArrayList<NameValuePair>();
+ while (!cursor.atEnd()) {
+ final NameValuePair param = parseNameValuePair(buffer, cursor);
+ params.add(param);
+ final char ch = buffer.charAt(cursor.getPos() - 1);
+ if (ch == ELEM_DELIMITER) {
+ break;
+ }
+ }
+
+ return params.toArray(new NameValuePair[params.size()]);
+ }
+
+ /**
+ * Parses a name-value-pair with the given parser.
+ *
+ * @param value the NVP to parse
+ * @param parser the parser to use, or <code>null</code> for default
+ *
+ * @return the parsed name-value pair
+ */
+ public static
+ NameValuePair parseNameValuePair(final String value,
+ final HeaderValueParser parser) throws ParseException {
+ Args.notNull(value, "Value");
+
+ final CharArrayBuffer buffer = new CharArrayBuffer(value.length());
+ buffer.append(value);
+ final ParserCursor cursor = new ParserCursor(0, value.length());
+ return (parser != null ? parser : BasicHeaderValueParser.INSTANCE)
+ .parseNameValuePair(buffer, cursor);
+ }
+
+
+ // non-javadoc, see interface HeaderValueParser
+ public NameValuePair parseNameValuePair(final CharArrayBuffer buffer,
+ final ParserCursor cursor) {
+ return parseNameValuePair(buffer, cursor, ALL_DELIMITERS);
+ }
+
+ private static boolean isOneOf(final char ch, final char[] chs) {
+ if (chs != null) {
+ for (final char ch2 : chs) {
+ if (ch == ch2) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public NameValuePair parseNameValuePair(final CharArrayBuffer buffer,
+ final ParserCursor cursor,
+ final char[] delimiters) {
+ Args.notNull(buffer, "Char array buffer");
+ Args.notNull(cursor, "Parser cursor");
+
+ boolean terminated = false;
+
+ int pos = cursor.getPos();
+ final int indexFrom = cursor.getPos();
+ final int indexTo = cursor.getUpperBound();
+
+ // Find name
+ final String name;
+ while (pos < indexTo) {
+ final char ch = buffer.charAt(pos);
+ if (ch == '=') {
+ break;
+ }
+ if (isOneOf(ch, delimiters)) {
+ terminated = true;
+ break;
+ }
+ pos++;
+ }
+
+ if (pos == indexTo) {
+ terminated = true;
+ name = buffer.substringTrimmed(indexFrom, indexTo);
+ } else {
+ name = buffer.substringTrimmed(indexFrom, pos);
+ pos++;
+ }
+
+ if (terminated) {
+ cursor.updatePos(pos);
+ return createNameValuePair(name, null);
+ }
+
+ // Find value
+ final String value;
+ int i1 = pos;
+
+ boolean qouted = false;
+ boolean escaped = false;
+ while (pos < indexTo) {
+ final char ch = buffer.charAt(pos);
+ if (ch == '"' && !escaped) {
+ qouted = !qouted;
+ }
+ if (!qouted && !escaped && isOneOf(ch, delimiters)) {
+ terminated = true;
+ break;
+ }
+ if (escaped) {
+ escaped = false;
+ } else {
+ escaped = qouted && ch == '\\';
+ }
+ pos++;
+ }
+
+ int i2 = pos;
+ // Trim leading white spaces
+ while (i1 < i2 && (HTTP.isWhitespace(buffer.charAt(i1)))) {
+ i1++;
+ }
+ // Trim trailing white spaces
+ while ((i2 > i1) && (HTTP.isWhitespace(buffer.charAt(i2 - 1)))) {
+ i2--;
+ }
+ // Strip away quotes if necessary
+ if (((i2 - i1) >= 2)
+ && (buffer.charAt(i1) == '"')
+ && (buffer.charAt(i2 - 1) == '"')) {
+ i1++;
+ i2--;
+ }
+ value = buffer.substring(i1, i2);
+ if (terminated) {
+ pos++;
+ }
+ cursor.updatePos(pos);
+ return createNameValuePair(name, value);
+ }
+
+ /**
+ * Creates a name-value pair.
+ * Called from {@link #parseNameValuePair}.
+ *
+ * @param name the name
+ * @param value the value, or <code>null</code>
+ *
+ * @return a name-value pair representing the arguments
+ */
+ protected NameValuePair createNameValuePair(final String name, final String value) {
+ return new BasicNameValuePair(name, value);
+ }
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHttpEntityEnclosingRequest.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHttpEntityEnclosingRequest.java
new file mode 100644
index 0000000000..fcb560ccd7
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHttpEntityEnclosingRequest.java
@@ -0,0 +1,75 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.RequestLine;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+
+/**
+ * Basic implementation of {@link HttpEntityEnclosingRequest}.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class BasicHttpEntityEnclosingRequest
+ extends BasicHttpRequest implements HttpEntityEnclosingRequest {
+
+ private HttpEntity entity;
+
+ public BasicHttpEntityEnclosingRequest(final String method, final String uri) {
+ super(method, uri);
+ }
+
+ public BasicHttpEntityEnclosingRequest(final String method, final String uri,
+ final ProtocolVersion ver) {
+ super(method, uri, ver);
+ }
+
+ public BasicHttpEntityEnclosingRequest(final RequestLine requestline) {
+ super(requestline);
+ }
+
+ public HttpEntity getEntity() {
+ return this.entity;
+ }
+
+ public void setEntity(final HttpEntity entity) {
+ this.entity = entity;
+ }
+
+ public boolean expectContinue() {
+ final Header expect = getFirstHeader(HTTP.EXPECT_DIRECTIVE);
+ return expect != null && HTTP.EXPECT_CONTINUE.equalsIgnoreCase(expect.getValue());
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHttpRequest.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHttpRequest.java
new file mode 100644
index 0000000000..be795a08e3
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHttpRequest.java
@@ -0,0 +1,114 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.RequestLine;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Basic implementation of {@link HttpRequest}.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class BasicHttpRequest extends AbstractHttpMessage implements HttpRequest {
+
+ private final String method;
+ private final String uri;
+
+ private RequestLine requestline;
+
+ /**
+ * Creates an instance of this class using the given request method
+ * and URI.
+ *
+ * @param method request method.
+ * @param uri request URI.
+ */
+ public BasicHttpRequest(final String method, final String uri) {
+ super();
+ this.method = Args.notNull(method, "Method name");
+ this.uri = Args.notNull(uri, "Request URI");
+ this.requestline = null;
+ }
+
+ /**
+ * Creates an instance of this class using the given request method, URI
+ * and the HTTP protocol version.
+ *
+ * @param method request method.
+ * @param uri request URI.
+ * @param ver HTTP protocol version.
+ */
+ public BasicHttpRequest(final String method, final String uri, final ProtocolVersion ver) {
+ this(new BasicRequestLine(method, uri, ver));
+ }
+
+ /**
+ * Creates an instance of this class using the given request line.
+ *
+ * @param requestline request line.
+ */
+ public BasicHttpRequest(final RequestLine requestline) {
+ super();
+ this.requestline = Args.notNull(requestline, "Request line");
+ this.method = requestline.getMethod();
+ this.uri = requestline.getUri();
+ }
+
+ /**
+ * Returns the HTTP protocol version to be used for this request.
+ *
+ * @see #BasicHttpRequest(String, String)
+ */
+ public ProtocolVersion getProtocolVersion() {
+ return getRequestLine().getProtocolVersion();
+ }
+
+ /**
+ * Returns the request line of this request.
+ *
+ * @see #BasicHttpRequest(String, String)
+ */
+ public RequestLine getRequestLine() {
+ if (this.requestline == null) {
+ this.requestline = new BasicRequestLine(this.method, this.uri, HttpVersion.HTTP_1_1);
+ }
+ return this.requestline;
+ }
+
+ @Override
+ public String toString() {
+ return this.method + " " + this.uri + " " + this.headergroup;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHttpResponse.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHttpResponse.java
new file mode 100644
index 0000000000..f16b1ca598
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicHttpResponse.java
@@ -0,0 +1,218 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import java.util.Locale;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.ReasonPhraseCatalog;
+import ch.boye.httpclientandroidlib.StatusLine;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Basic implementation of {@link HttpResponse}.
+ *
+ * @see ch.boye.httpclientandroidlib.impl.DefaultHttpResponseFactory
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class BasicHttpResponse extends AbstractHttpMessage implements HttpResponse {
+
+ private StatusLine statusline;
+ private ProtocolVersion ver;
+ private int code;
+ private String reasonPhrase;
+ private HttpEntity entity;
+ private final ReasonPhraseCatalog reasonCatalog;
+ private Locale locale;
+
+ /**
+ * Creates a new response.
+ * This is the constructor to which all others map.
+ *
+ * @param statusline the status line
+ * @param catalog the reason phrase catalog, or
+ * <code>null</code> to disable automatic
+ * reason phrase lookup
+ * @param locale the locale for looking up reason phrases, or
+ * <code>null</code> for the system locale
+ */
+ public BasicHttpResponse(final StatusLine statusline,
+ final ReasonPhraseCatalog catalog,
+ final Locale locale) {
+ super();
+ this.statusline = Args.notNull(statusline, "Status line");
+ this.ver = statusline.getProtocolVersion();
+ this.code = statusline.getStatusCode();
+ this.reasonPhrase = statusline.getReasonPhrase();
+ this.reasonCatalog = catalog;
+ this.locale = locale;
+ }
+
+ /**
+ * Creates a response from a status line.
+ * The response will not have a reason phrase catalog and
+ * use the system default locale.
+ *
+ * @param statusline the status line
+ */
+ public BasicHttpResponse(final StatusLine statusline) {
+ super();
+ this.statusline = Args.notNull(statusline, "Status line");
+ this.ver = statusline.getProtocolVersion();
+ this.code = statusline.getStatusCode();
+ this.reasonPhrase = statusline.getReasonPhrase();
+ this.reasonCatalog = null;
+ this.locale = null;
+ }
+
+ /**
+ * Creates a response from elements of a status line.
+ * The response will not have a reason phrase catalog and
+ * use the system default locale.
+ *
+ * @param ver the protocol version of the response
+ * @param code the status code of the response
+ * @param reason the reason phrase to the status code, or
+ * <code>null</code>
+ */
+ public BasicHttpResponse(final ProtocolVersion ver,
+ final int code,
+ final String reason) {
+ super();
+ Args.notNegative(code, "Status code");
+ this.statusline = null;
+ this.ver = ver;
+ this.code = code;
+ this.reasonPhrase = reason;
+ this.reasonCatalog = null;
+ this.locale = null;
+ }
+
+
+ // non-javadoc, see interface HttpMessage
+ public ProtocolVersion getProtocolVersion() {
+ return this.ver;
+ }
+
+ // non-javadoc, see interface HttpResponse
+ public StatusLine getStatusLine() {
+ if (this.statusline == null) {
+ this.statusline = new BasicStatusLine(
+ this.ver != null ? this.ver : HttpVersion.HTTP_1_1,
+ this.code,
+ this.reasonPhrase != null ? this.reasonPhrase : getReason(this.code));
+ }
+ return this.statusline;
+ }
+
+ // non-javadoc, see interface HttpResponse
+ public HttpEntity getEntity() {
+ return this.entity;
+ }
+
+ public Locale getLocale() {
+ return this.locale;
+ }
+
+ // non-javadoc, see interface HttpResponse
+ public void setStatusLine(final StatusLine statusline) {
+ this.statusline = Args.notNull(statusline, "Status line");
+ this.ver = statusline.getProtocolVersion();
+ this.code = statusline.getStatusCode();
+ this.reasonPhrase = statusline.getReasonPhrase();
+ }
+
+ // non-javadoc, see interface HttpResponse
+ public void setStatusLine(final ProtocolVersion ver, final int code) {
+ Args.notNegative(code, "Status code");
+ this.statusline = null;
+ this.ver = ver;
+ this.code = code;
+ this.reasonPhrase = null;
+ }
+
+ // non-javadoc, see interface HttpResponse
+ public void setStatusLine(
+ final ProtocolVersion ver, final int code, final String reason) {
+ Args.notNegative(code, "Status code");
+ this.statusline = null;
+ this.ver = ver;
+ this.code = code;
+ this.reasonPhrase = reason;
+ }
+
+ // non-javadoc, see interface HttpResponse
+ public void setStatusCode(final int code) {
+ Args.notNegative(code, "Status code");
+ this.statusline = null;
+ this.code = code;
+ this.reasonPhrase = null;
+ }
+
+ // non-javadoc, see interface HttpResponse
+ public void setReasonPhrase(final String reason) {
+ this.statusline = null;
+ this.reasonPhrase = reason;
+ }
+
+ // non-javadoc, see interface HttpResponse
+ public void setEntity(final HttpEntity entity) {
+ this.entity = entity;
+ }
+
+ public void setLocale(final Locale locale) {
+ this.locale = Args.notNull(locale, "Locale");
+ this.statusline = null;
+ }
+
+ /**
+ * Looks up a reason phrase.
+ * This method evaluates the currently set catalog and locale.
+ * It also handles a missing catalog.
+ *
+ * @param code the status code for which to look up the reason
+ *
+ * @return the reason phrase, or <code>null</code> if there is none
+ */
+ protected String getReason(final int code) {
+ return this.reasonCatalog != null ? this.reasonCatalog.getReason(code,
+ this.locale != null ? this.locale : Locale.getDefault()) : null;
+ }
+
+ @Override
+ public String toString() {
+ return getStatusLine() + " " + this.headergroup;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicLineFormatter.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicLineFormatter.java
new file mode 100644
index 0000000000..6eabfd48fb
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicLineFormatter.java
@@ -0,0 +1,319 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import ch.boye.httpclientandroidlib.FormattedHeader;
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.RequestLine;
+import ch.boye.httpclientandroidlib.StatusLine;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Interface for formatting elements of the HEAD section of an HTTP message.
+ * This is the complement to {@link LineParser}.
+ * There are individual methods for formatting a request line, a
+ * status line, or a header line. The formatting does <i>not</i> include the
+ * trailing line break sequence CR-LF.
+ * The formatted lines are returned in memory, the formatter does not depend
+ * on any specific IO mechanism.
+ * Instances of this interface are expected to be stateless and thread-safe.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class BasicLineFormatter implements LineFormatter {
+
+ /**
+ * A default instance of this class, for use as default or fallback.
+ * Note that {@link BasicLineFormatter} is not a singleton, there can
+ * be many instances of the class itself and of derived classes.
+ * The instance here provides non-customized, default behavior.
+ *
+ * @deprecated (4.3) use {@link #INSTANCE}
+ */
+ @Deprecated
+ public final static BasicLineFormatter DEFAULT = new BasicLineFormatter();
+
+ public final static BasicLineFormatter INSTANCE = new BasicLineFormatter();
+
+ public BasicLineFormatter() {
+ super();
+ }
+
+ /**
+ * Obtains a buffer for formatting.
+ *
+ * @param charBuffer a buffer already available, or <code>null</code>
+ *
+ * @return the cleared argument buffer if there is one, or
+ * a new empty buffer that can be used for formatting
+ */
+ protected CharArrayBuffer initBuffer(final CharArrayBuffer charBuffer) {
+ CharArrayBuffer buffer = charBuffer;
+ if (buffer != null) {
+ buffer.clear();
+ } else {
+ buffer = new CharArrayBuffer(64);
+ }
+ return buffer;
+ }
+
+
+ /**
+ * Formats a protocol version.
+ *
+ * @param version the protocol version to format
+ * @param formatter the formatter to use, or
+ * <code>null</code> for the
+ * {@link #INSTANCE default}
+ *
+ * @return the formatted protocol version
+ */
+ public static
+ String formatProtocolVersion(final ProtocolVersion version,
+ final LineFormatter formatter) {
+ return (formatter != null ? formatter : BasicLineFormatter.INSTANCE)
+ .appendProtocolVersion(null, version).toString();
+ }
+
+
+ // non-javadoc, see interface LineFormatter
+ public CharArrayBuffer appendProtocolVersion(final CharArrayBuffer buffer,
+ final ProtocolVersion version) {
+ Args.notNull(version, "Protocol version");
+ // can't use initBuffer, that would clear the argument!
+ CharArrayBuffer result = buffer;
+ final int len = estimateProtocolVersionLen(version);
+ if (result == null) {
+ result = new CharArrayBuffer(len);
+ } else {
+ result.ensureCapacity(len);
+ }
+
+ result.append(version.getProtocol());
+ result.append('/');
+ result.append(Integer.toString(version.getMajor()));
+ result.append('.');
+ result.append(Integer.toString(version.getMinor()));
+
+ return result;
+ }
+
+
+ /**
+ * Guesses the length of a formatted protocol version.
+ * Needed to guess the length of a formatted request or status line.
+ *
+ * @param version the protocol version to format, or <code>null</code>
+ *
+ * @return the estimated length of the formatted protocol version,
+ * in characters
+ */
+ protected int estimateProtocolVersionLen(final ProtocolVersion version) {
+ return version.getProtocol().length() + 4; // room for "HTTP/1.1"
+ }
+
+
+ /**
+ * Formats a request line.
+ *
+ * @param reqline the request line to format
+ * @param formatter the formatter to use, or
+ * <code>null</code> for the
+ * {@link #INSTANCE default}
+ *
+ * @return the formatted request line
+ */
+ public static String formatRequestLine(final RequestLine reqline,
+ final LineFormatter formatter) {
+ return (formatter != null ? formatter : BasicLineFormatter.INSTANCE)
+ .formatRequestLine(null, reqline).toString();
+ }
+
+
+ // non-javadoc, see interface LineFormatter
+ public CharArrayBuffer formatRequestLine(final CharArrayBuffer buffer,
+ final RequestLine reqline) {
+ Args.notNull(reqline, "Request line");
+ final CharArrayBuffer result = initBuffer(buffer);
+ doFormatRequestLine(result, reqline);
+
+ return result;
+ }
+
+
+ /**
+ * Actually formats a request line.
+ * Called from {@link #formatRequestLine}.
+ *
+ * @param buffer the empty buffer into which to format,
+ * never <code>null</code>
+ * @param reqline the request line to format, never <code>null</code>
+ */
+ protected void doFormatRequestLine(final CharArrayBuffer buffer,
+ final RequestLine reqline) {
+ final String method = reqline.getMethod();
+ final String uri = reqline.getUri();
+
+ // room for "GET /index.html HTTP/1.1"
+ final int len = method.length() + 1 + uri.length() + 1 +
+ estimateProtocolVersionLen(reqline.getProtocolVersion());
+ buffer.ensureCapacity(len);
+
+ buffer.append(method);
+ buffer.append(' ');
+ buffer.append(uri);
+ buffer.append(' ');
+ appendProtocolVersion(buffer, reqline.getProtocolVersion());
+ }
+
+
+
+ /**
+ * Formats a status line.
+ *
+ * @param statline the status line to format
+ * @param formatter the formatter to use, or
+ * <code>null</code> for the
+ * {@link #INSTANCE default}
+ *
+ * @return the formatted status line
+ */
+ public static String formatStatusLine(final StatusLine statline,
+ final LineFormatter formatter) {
+ return (formatter != null ? formatter : BasicLineFormatter.INSTANCE)
+ .formatStatusLine(null, statline).toString();
+ }
+
+
+ // non-javadoc, see interface LineFormatter
+ public CharArrayBuffer formatStatusLine(final CharArrayBuffer buffer,
+ final StatusLine statline) {
+ Args.notNull(statline, "Status line");
+ final CharArrayBuffer result = initBuffer(buffer);
+ doFormatStatusLine(result, statline);
+
+ return result;
+ }
+
+
+ /**
+ * Actually formats a status line.
+ * Called from {@link #formatStatusLine}.
+ *
+ * @param buffer the empty buffer into which to format,
+ * never <code>null</code>
+ * @param statline the status line to format, never <code>null</code>
+ */
+ protected void doFormatStatusLine(final CharArrayBuffer buffer,
+ final StatusLine statline) {
+
+ int len = estimateProtocolVersionLen(statline.getProtocolVersion())
+ + 1 + 3 + 1; // room for "HTTP/1.1 200 "
+ final String reason = statline.getReasonPhrase();
+ if (reason != null) {
+ len += reason.length();
+ }
+ buffer.ensureCapacity(len);
+
+ appendProtocolVersion(buffer, statline.getProtocolVersion());
+ buffer.append(' ');
+ buffer.append(Integer.toString(statline.getStatusCode()));
+ buffer.append(' '); // keep whitespace even if reason phrase is empty
+ if (reason != null) {
+ buffer.append(reason);
+ }
+ }
+
+
+ /**
+ * Formats a header.
+ *
+ * @param header the header to format
+ * @param formatter the formatter to use, or
+ * <code>null</code> for the
+ * {@link #INSTANCE default}
+ *
+ * @return the formatted header
+ */
+ public static String formatHeader(final Header header,
+ final LineFormatter formatter) {
+ return (formatter != null ? formatter : BasicLineFormatter.INSTANCE)
+ .formatHeader(null, header).toString();
+ }
+
+
+ // non-javadoc, see interface LineFormatter
+ public CharArrayBuffer formatHeader(final CharArrayBuffer buffer,
+ final Header header) {
+ Args.notNull(header, "Header");
+ final CharArrayBuffer result;
+
+ if (header instanceof FormattedHeader) {
+ // If the header is backed by a buffer, re-use the buffer
+ result = ((FormattedHeader)header).getBuffer();
+ } else {
+ result = initBuffer(buffer);
+ doFormatHeader(result, header);
+ }
+ return result;
+
+ } // formatHeader
+
+
+ /**
+ * Actually formats a header.
+ * Called from {@link #formatHeader}.
+ *
+ * @param buffer the empty buffer into which to format,
+ * never <code>null</code>
+ * @param header the header to format, never <code>null</code>
+ */
+ protected void doFormatHeader(final CharArrayBuffer buffer,
+ final Header header) {
+ final String name = header.getName();
+ final String value = header.getValue();
+
+ int len = name.length() + 2;
+ if (value != null) {
+ len += value.length();
+ }
+ buffer.ensureCapacity(len);
+
+ buffer.append(name);
+ buffer.append(": ");
+ if (value != null) {
+ buffer.append(value);
+ }
+ }
+
+
+} // class BasicLineFormatter
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicLineParser.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicLineParser.java
new file mode 100644
index 0000000000..dabde0f62c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicLineParser.java
@@ -0,0 +1,456 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.ParseException;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.RequestLine;
+import ch.boye.httpclientandroidlib.StatusLine;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Basic parser for lines in the head section of an HTTP message.
+ * There are individual methods for parsing a request line, a
+ * status line, or a header line.
+ * The lines to parse are passed in memory, the parser does not depend
+ * on any specific IO mechanism.
+ * Instances of this class are stateless and thread-safe.
+ * Derived classes MUST maintain these properties.
+ *
+ * <p>
+ * Note: This class was created by refactoring parsing code located in
+ * various other classes. The author tags from those other classes have
+ * been replicated here, although the association with the parsing code
+ * taken from there has not been traced.
+ * </p>
+ *
+ * @since 4.0
+ */
+@Immutable
+public class BasicLineParser implements LineParser {
+
+ /**
+ * A default instance of this class, for use as default or fallback.
+ * Note that {@link BasicLineParser} is not a singleton, there can
+ * be many instances of the class itself and of derived classes.
+ * The instance here provides non-customized, default behavior.
+ *
+ * @deprecated (4.3) use {@link #INSTANCE}
+ */
+ @Deprecated
+ public final static BasicLineParser DEFAULT = new BasicLineParser();
+
+ public final static BasicLineParser INSTANCE = new BasicLineParser();
+
+ /**
+ * A version of the protocol to parse.
+ * The version is typically not relevant, but the protocol name.
+ */
+ protected final ProtocolVersion protocol;
+
+
+ /**
+ * Creates a new line parser for the given HTTP-like protocol.
+ *
+ * @param proto a version of the protocol to parse, or
+ * <code>null</code> for HTTP. The actual version
+ * is not relevant, only the protocol name.
+ */
+ public BasicLineParser(final ProtocolVersion proto) {
+ this.protocol = proto != null? proto : HttpVersion.HTTP_1_1;
+ }
+
+
+ /**
+ * Creates a new line parser for HTTP.
+ */
+ public BasicLineParser() {
+ this(null);
+ }
+
+
+ public static
+ ProtocolVersion parseProtocolVersion(final String value,
+ final LineParser parser) throws ParseException {
+ Args.notNull(value, "Value");
+
+ final CharArrayBuffer buffer = new CharArrayBuffer(value.length());
+ buffer.append(value);
+ final ParserCursor cursor = new ParserCursor(0, value.length());
+ return (parser != null ? parser : BasicLineParser.INSTANCE)
+ .parseProtocolVersion(buffer, cursor);
+ }
+
+
+ // non-javadoc, see interface LineParser
+ public ProtocolVersion parseProtocolVersion(final CharArrayBuffer buffer,
+ final ParserCursor cursor) throws ParseException {
+ Args.notNull(buffer, "Char array buffer");
+ Args.notNull(cursor, "Parser cursor");
+ final String protoname = this.protocol.getProtocol();
+ final int protolength = protoname.length();
+
+ final int indexFrom = cursor.getPos();
+ final int indexTo = cursor.getUpperBound();
+
+ skipWhitespace(buffer, cursor);
+
+ int i = cursor.getPos();
+
+ // long enough for "HTTP/1.1"?
+ if (i + protolength + 4 > indexTo) {
+ throw new ParseException
+ ("Not a valid protocol version: " +
+ buffer.substring(indexFrom, indexTo));
+ }
+
+ // check the protocol name and slash
+ boolean ok = true;
+ for (int j=0; ok && (j<protolength); j++) {
+ ok = (buffer.charAt(i+j) == protoname.charAt(j));
+ }
+ if (ok) {
+ ok = (buffer.charAt(i+protolength) == '/');
+ }
+ if (!ok) {
+ throw new ParseException
+ ("Not a valid protocol version: " +
+ buffer.substring(indexFrom, indexTo));
+ }
+
+ i += protolength+1;
+
+ final int period = buffer.indexOf('.', i, indexTo);
+ if (period == -1) {
+ throw new ParseException
+ ("Invalid protocol version number: " +
+ buffer.substring(indexFrom, indexTo));
+ }
+ final int major;
+ try {
+ major = Integer.parseInt(buffer.substringTrimmed(i, period));
+ } catch (final NumberFormatException e) {
+ throw new ParseException
+ ("Invalid protocol major version number: " +
+ buffer.substring(indexFrom, indexTo));
+ }
+ i = period + 1;
+
+ int blank = buffer.indexOf(' ', i, indexTo);
+ if (blank == -1) {
+ blank = indexTo;
+ }
+ final int minor;
+ try {
+ minor = Integer.parseInt(buffer.substringTrimmed(i, blank));
+ } catch (final NumberFormatException e) {
+ throw new ParseException(
+ "Invalid protocol minor version number: " +
+ buffer.substring(indexFrom, indexTo));
+ }
+
+ cursor.updatePos(blank);
+
+ return createProtocolVersion(major, minor);
+
+ } // parseProtocolVersion
+
+
+ /**
+ * Creates a protocol version.
+ * Called from {@link #parseProtocolVersion}.
+ *
+ * @param major the major version number, for example 1 in HTTP/1.0
+ * @param minor the minor version number, for example 0 in HTTP/1.0
+ *
+ * @return the protocol version
+ */
+ protected ProtocolVersion createProtocolVersion(final int major, final int minor) {
+ return protocol.forVersion(major, minor);
+ }
+
+
+
+ // non-javadoc, see interface LineParser
+ public boolean hasProtocolVersion(final CharArrayBuffer buffer,
+ final ParserCursor cursor) {
+ Args.notNull(buffer, "Char array buffer");
+ Args.notNull(cursor, "Parser cursor");
+ int index = cursor.getPos();
+
+ final String protoname = this.protocol.getProtocol();
+ final int protolength = protoname.length();
+
+ if (buffer.length() < protolength+4)
+ {
+ return false; // not long enough for "HTTP/1.1"
+ }
+
+ if (index < 0) {
+ // end of line, no tolerance for trailing whitespace
+ // this works only for single-digit major and minor version
+ index = buffer.length() -4 -protolength;
+ } else if (index == 0) {
+ // beginning of line, tolerate leading whitespace
+ while ((index < buffer.length()) &&
+ HTTP.isWhitespace(buffer.charAt(index))) {
+ index++;
+ }
+ } // else within line, don't tolerate whitespace
+
+
+ if (index + protolength + 4 > buffer.length()) {
+ return false;
+ }
+
+
+ // just check protocol name and slash, no need to analyse the version
+ boolean ok = true;
+ for (int j=0; ok && (j<protolength); j++) {
+ ok = (buffer.charAt(index+j) == protoname.charAt(j));
+ }
+ if (ok) {
+ ok = (buffer.charAt(index+protolength) == '/');
+ }
+
+ return ok;
+ }
+
+
+
+ public static
+ RequestLine parseRequestLine(final String value,
+ final LineParser parser) throws ParseException {
+ Args.notNull(value, "Value");
+
+ final CharArrayBuffer buffer = new CharArrayBuffer(value.length());
+ buffer.append(value);
+ final ParserCursor cursor = new ParserCursor(0, value.length());
+ return (parser != null ? parser : BasicLineParser.INSTANCE)
+ .parseRequestLine(buffer, cursor);
+ }
+
+
+ /**
+ * Parses a request line.
+ *
+ * @param buffer a buffer holding the line to parse
+ *
+ * @return the parsed request line
+ *
+ * @throws ParseException in case of a parse error
+ */
+ public RequestLine parseRequestLine(final CharArrayBuffer buffer,
+ final ParserCursor cursor) throws ParseException {
+
+ Args.notNull(buffer, "Char array buffer");
+ Args.notNull(cursor, "Parser cursor");
+ final int indexFrom = cursor.getPos();
+ final int indexTo = cursor.getUpperBound();
+
+ try {
+ skipWhitespace(buffer, cursor);
+ int i = cursor.getPos();
+
+ int blank = buffer.indexOf(' ', i, indexTo);
+ if (blank < 0) {
+ throw new ParseException("Invalid request line: " +
+ buffer.substring(indexFrom, indexTo));
+ }
+ final String method = buffer.substringTrimmed(i, blank);
+ cursor.updatePos(blank);
+
+ skipWhitespace(buffer, cursor);
+ i = cursor.getPos();
+
+ blank = buffer.indexOf(' ', i, indexTo);
+ if (blank < 0) {
+ throw new ParseException("Invalid request line: " +
+ buffer.substring(indexFrom, indexTo));
+ }
+ final String uri = buffer.substringTrimmed(i, blank);
+ cursor.updatePos(blank);
+
+ final ProtocolVersion ver = parseProtocolVersion(buffer, cursor);
+
+ skipWhitespace(buffer, cursor);
+ if (!cursor.atEnd()) {
+ throw new ParseException("Invalid request line: " +
+ buffer.substring(indexFrom, indexTo));
+ }
+
+ return createRequestLine(method, uri, ver);
+ } catch (final IndexOutOfBoundsException e) {
+ throw new ParseException("Invalid request line: " +
+ buffer.substring(indexFrom, indexTo));
+ }
+ } // parseRequestLine
+
+
+ /**
+ * Instantiates a new request line.
+ * Called from {@link #parseRequestLine}.
+ *
+ * @param method the request method
+ * @param uri the requested URI
+ * @param ver the protocol version
+ *
+ * @return a new status line with the given data
+ */
+ protected RequestLine createRequestLine(final String method,
+ final String uri,
+ final ProtocolVersion ver) {
+ return new BasicRequestLine(method, uri, ver);
+ }
+
+
+
+ public static
+ StatusLine parseStatusLine(final String value,
+ final LineParser parser) throws ParseException {
+ Args.notNull(value, "Value");
+
+ final CharArrayBuffer buffer = new CharArrayBuffer(value.length());
+ buffer.append(value);
+ final ParserCursor cursor = new ParserCursor(0, value.length());
+ return (parser != null ? parser : BasicLineParser.INSTANCE)
+ .parseStatusLine(buffer, cursor);
+ }
+
+
+ // non-javadoc, see interface LineParser
+ public StatusLine parseStatusLine(final CharArrayBuffer buffer,
+ final ParserCursor cursor) throws ParseException {
+ Args.notNull(buffer, "Char array buffer");
+ Args.notNull(cursor, "Parser cursor");
+ final int indexFrom = cursor.getPos();
+ final int indexTo = cursor.getUpperBound();
+
+ try {
+ // handle the HTTP-Version
+ final ProtocolVersion ver = parseProtocolVersion(buffer, cursor);
+
+ // handle the Status-Code
+ skipWhitespace(buffer, cursor);
+ int i = cursor.getPos();
+
+ int blank = buffer.indexOf(' ', i, indexTo);
+ if (blank < 0) {
+ blank = indexTo;
+ }
+ final int statusCode;
+ final String s = buffer.substringTrimmed(i, blank);
+ for (int j = 0; j < s.length(); j++) {
+ if (!Character.isDigit(s.charAt(j))) {
+ throw new ParseException(
+ "Status line contains invalid status code: "
+ + buffer.substring(indexFrom, indexTo));
+ }
+ }
+ try {
+ statusCode = Integer.parseInt(s);
+ } catch (final NumberFormatException e) {
+ throw new ParseException(
+ "Status line contains invalid status code: "
+ + buffer.substring(indexFrom, indexTo));
+ }
+ //handle the Reason-Phrase
+ i = blank;
+ final String reasonPhrase;
+ if (i < indexTo) {
+ reasonPhrase = buffer.substringTrimmed(i, indexTo);
+ } else {
+ reasonPhrase = "";
+ }
+ return createStatusLine(ver, statusCode, reasonPhrase);
+
+ } catch (final IndexOutOfBoundsException e) {
+ throw new ParseException("Invalid status line: " +
+ buffer.substring(indexFrom, indexTo));
+ }
+ } // parseStatusLine
+
+
+ /**
+ * Instantiates a new status line.
+ * Called from {@link #parseStatusLine}.
+ *
+ * @param ver the protocol version
+ * @param status the status code
+ * @param reason the reason phrase
+ *
+ * @return a new status line with the given data
+ */
+ protected StatusLine createStatusLine(final ProtocolVersion ver,
+ final int status,
+ final String reason) {
+ return new BasicStatusLine(ver, status, reason);
+ }
+
+
+
+ public static
+ Header parseHeader(final String value,
+ final LineParser parser) throws ParseException {
+ Args.notNull(value, "Value");
+
+ final CharArrayBuffer buffer = new CharArrayBuffer(value.length());
+ buffer.append(value);
+ return (parser != null ? parser : BasicLineParser.INSTANCE)
+ .parseHeader(buffer);
+ }
+
+
+ // non-javadoc, see interface LineParser
+ public Header parseHeader(final CharArrayBuffer buffer)
+ throws ParseException {
+
+ // the actual parser code is in the constructor of BufferedHeader
+ return new BufferedHeader(buffer);
+ }
+
+
+ /**
+ * Helper to skip whitespace.
+ */
+ protected void skipWhitespace(final CharArrayBuffer buffer, final ParserCursor cursor) {
+ int pos = cursor.getPos();
+ final int indexTo = cursor.getUpperBound();
+ while ((pos < indexTo) &&
+ HTTP.isWhitespace(buffer.charAt(pos))) {
+ pos++;
+ }
+ cursor.updatePos(pos);
+ }
+
+} // class BasicLineParser
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicListHeaderIterator.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicListHeaderIterator.java
new file mode 100644
index 0000000000..ce9db2657c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicListHeaderIterator.java
@@ -0,0 +1,190 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderIterator;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+
+/**
+ * Implementation of a {@link HeaderIterator} based on a {@link List}.
+ * For use by {@link HeaderGroup}.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class BasicListHeaderIterator implements HeaderIterator {
+
+ /**
+ * A list of headers to iterate over.
+ * Not all elements of this array are necessarily part of the iteration.
+ */
+ protected final List<Header> allHeaders;
+
+
+ /**
+ * The position of the next header in {@link #allHeaders allHeaders}.
+ * Negative if the iteration is over.
+ */
+ protected int currentIndex;
+
+
+ /**
+ * The position of the last returned header.
+ * Negative if none has been returned so far.
+ */
+ protected int lastIndex;
+
+
+ /**
+ * The header name to filter by.
+ * <code>null</code> to iterate over all headers in the array.
+ */
+ protected String headerName;
+
+
+
+ /**
+ * Creates a new header iterator.
+ *
+ * @param headers a list of headers over which to iterate
+ * @param name the name of the headers over which to iterate, or
+ * <code>null</code> for any
+ */
+ public BasicListHeaderIterator(final List<Header> headers, final String name) {
+ super();
+ this.allHeaders = Args.notNull(headers, "Header list");
+ this.headerName = name;
+ this.currentIndex = findNext(-1);
+ this.lastIndex = -1;
+ }
+
+
+ /**
+ * Determines the index of the next header.
+ *
+ * @param pos one less than the index to consider first,
+ * -1 to search for the first header
+ *
+ * @return the index of the next header that matches the filter name,
+ * or negative if there are no more headers
+ */
+ protected int findNext(final int pos) {
+ int from = pos;
+ if (from < -1) {
+ return -1;
+ }
+
+ final int to = this.allHeaders.size()-1;
+ boolean found = false;
+ while (!found && (from < to)) {
+ from++;
+ found = filterHeader(from);
+ }
+ return found ? from : -1;
+ }
+
+
+ /**
+ * Checks whether a header is part of the iteration.
+ *
+ * @param index the index of the header to check
+ *
+ * @return <code>true</code> if the header should be part of the
+ * iteration, <code>false</code> to skip
+ */
+ protected boolean filterHeader(final int index) {
+ if (this.headerName == null) {
+ return true;
+ }
+
+ // non-header elements, including null, will trigger exceptions
+ final String name = (this.allHeaders.get(index)).getName();
+
+ return this.headerName.equalsIgnoreCase(name);
+ }
+
+
+ // non-javadoc, see interface HeaderIterator
+ public boolean hasNext() {
+ return (this.currentIndex >= 0);
+ }
+
+
+ /**
+ * Obtains the next header from this iteration.
+ *
+ * @return the next header in this iteration
+ *
+ * @throws NoSuchElementException if there are no more headers
+ */
+ public Header nextHeader()
+ throws NoSuchElementException {
+
+ final int current = this.currentIndex;
+ if (current < 0) {
+ throw new NoSuchElementException("Iteration already finished.");
+ }
+
+ this.lastIndex = current;
+ this.currentIndex = findNext(current);
+
+ return this.allHeaders.get(current);
+ }
+
+
+ /**
+ * Returns the next header.
+ * Same as {@link #nextHeader nextHeader}, but not type-safe.
+ *
+ * @return the next header in this iteration
+ *
+ * @throws NoSuchElementException if there are no more headers
+ */
+ public final Object next()
+ throws NoSuchElementException {
+ return nextHeader();
+ }
+
+
+ /**
+ * Removes the header that was returned last.
+ */
+ public void remove()
+ throws UnsupportedOperationException {
+ Asserts.check(this.lastIndex >= 0, "No header to remove");
+ this.allHeaders.remove(this.lastIndex);
+ this.lastIndex = -1;
+ this.currentIndex--; // adjust for the removed element
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicNameValuePair.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicNameValuePair.java
new file mode 100644
index 0000000000..530e5042e4
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicNameValuePair.java
@@ -0,0 +1,111 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import java.io.Serializable;
+
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.LangUtils;
+
+/**
+ * Basic implementation of {@link NameValuePair}.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class BasicNameValuePair implements NameValuePair, Cloneable, Serializable {
+
+ private static final long serialVersionUID = -6437800749411518984L;
+
+ private final String name;
+ private final String value;
+
+ /**
+ * Default Constructor taking a name and a value. The value may be null.
+ *
+ * @param name The name.
+ * @param value The value.
+ */
+ public BasicNameValuePair(final String name, final String value) {
+ super();
+ this.name = Args.notNull(name, "Name");
+ this.value = value;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public String getValue() {
+ return this.value;
+ }
+
+ @Override
+ public String toString() {
+ // don't call complex default formatting for a simple toString
+
+ if (this.value == null) {
+ return name;
+ }
+ final int len = this.name.length() + 1 + this.value.length();
+ final StringBuilder buffer = new StringBuilder(len);
+ buffer.append(this.name);
+ buffer.append("=");
+ buffer.append(this.value);
+ return buffer.toString();
+ }
+
+ @Override
+ public boolean equals(final Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object instanceof NameValuePair) {
+ final BasicNameValuePair that = (BasicNameValuePair) object;
+ return this.name.equals(that.name)
+ && LangUtils.equals(this.value, that.value);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = LangUtils.HASH_SEED;
+ hash = LangUtils.hashCode(hash, this.name);
+ hash = LangUtils.hashCode(hash, this.value);
+ return hash;
+ }
+
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicRequestLine.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicRequestLine.java
new file mode 100644
index 0000000000..40c838b124
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicRequestLine.java
@@ -0,0 +1,83 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import java.io.Serializable;
+
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.RequestLine;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Basic implementation of {@link RequestLine}.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class BasicRequestLine implements RequestLine, Cloneable, Serializable {
+
+ private static final long serialVersionUID = 2810581718468737193L;
+
+ private final ProtocolVersion protoversion;
+ private final String method;
+ private final String uri;
+
+ public BasicRequestLine(final String method,
+ final String uri,
+ final ProtocolVersion version) {
+ super();
+ this.method = Args.notNull(method, "Method");
+ this.uri = Args.notNull(uri, "URI");
+ this.protoversion = Args.notNull(version, "Version");
+ }
+
+ public String getMethod() {
+ return this.method;
+ }
+
+ public ProtocolVersion getProtocolVersion() {
+ return this.protoversion;
+ }
+
+ public String getUri() {
+ return this.uri;
+ }
+
+ @Override
+ public String toString() {
+ // no need for non-default formatting in toString()
+ return BasicLineFormatter.INSTANCE.formatRequestLine(null, this).toString();
+ }
+
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicStatusLine.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicStatusLine.java
new file mode 100644
index 0000000000..cce1f25d78
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicStatusLine.java
@@ -0,0 +1,100 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import java.io.Serializable;
+
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.StatusLine;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Basic implementation of {@link StatusLine}
+ *
+ * @since 4.0
+ */
+@Immutable
+public class BasicStatusLine implements StatusLine, Cloneable, Serializable {
+
+ private static final long serialVersionUID = -2443303766890459269L;
+
+ // ----------------------------------------------------- Instance Variables
+
+ /** The protocol version. */
+ private final ProtocolVersion protoVersion;
+
+ /** The status code. */
+ private final int statusCode;
+
+ /** The reason phrase. */
+ private final String reasonPhrase;
+
+ // ----------------------------------------------------------- Constructors
+ /**
+ * Creates a new status line with the given version, status, and reason.
+ *
+ * @param version the protocol version of the response
+ * @param statusCode the status code of the response
+ * @param reasonPhrase the reason phrase to the status code, or
+ * <code>null</code>
+ */
+ public BasicStatusLine(final ProtocolVersion version, final int statusCode,
+ final String reasonPhrase) {
+ super();
+ this.protoVersion = Args.notNull(version, "Version");
+ this.statusCode = Args.notNegative(statusCode, "Status code");
+ this.reasonPhrase = reasonPhrase;
+ }
+
+ // --------------------------------------------------------- Public Methods
+
+ public int getStatusCode() {
+ return this.statusCode;
+ }
+
+ public ProtocolVersion getProtocolVersion() {
+ return this.protoVersion;
+ }
+
+ public String getReasonPhrase() {
+ return this.reasonPhrase;
+ }
+
+ @Override
+ public String toString() {
+ // no need for non-default formatting in toString()
+ return BasicLineFormatter.INSTANCE.formatStatusLine(null, this).toString();
+ }
+
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicTokenIterator.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicTokenIterator.java
new file mode 100644
index 0000000000..98bd841b5b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BasicTokenIterator.java
@@ -0,0 +1,414 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import java.util.NoSuchElementException;
+
+import ch.boye.httpclientandroidlib.HeaderIterator;
+import ch.boye.httpclientandroidlib.ParseException;
+import ch.boye.httpclientandroidlib.TokenIterator;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Basic implementation of a {@link TokenIterator}.
+ * This implementation parses <tt>#token<tt> sequences as
+ * defined by RFC 2616, section 2.
+ * It extends that definition somewhat beyond US-ASCII.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class BasicTokenIterator implements TokenIterator {
+
+ /** The HTTP separator characters. Defined in RFC 2616, section 2.2. */
+ // the order of the characters here is adjusted to put the
+ // most likely candidates at the beginning of the collection
+ public final static String HTTP_SEPARATORS = " ,;=()<>@:\\\"/[]?{}\t";
+
+
+ /** The iterator from which to obtain the next header. */
+ protected final HeaderIterator headerIt;
+
+ /**
+ * The value of the current header.
+ * This is the header value that includes {@link #currentToken}.
+ * Undefined if the iteration is over.
+ */
+ protected String currentHeader;
+
+ /**
+ * The token to be returned by the next call to {@link #nextToken()}.
+ * <code>null</code> if the iteration is over.
+ */
+ protected String currentToken;
+
+ /**
+ * The position after {@link #currentToken} in {@link #currentHeader}.
+ * Undefined if the iteration is over.
+ */
+ protected int searchPos;
+
+
+ /**
+ * Creates a new instance of {@link BasicTokenIterator}.
+ *
+ * @param headerIterator the iterator for the headers to tokenize
+ */
+ public BasicTokenIterator(final HeaderIterator headerIterator) {
+ super();
+ this.headerIt = Args.notNull(headerIterator, "Header iterator");
+ this.searchPos = findNext(-1);
+ }
+
+
+ // non-javadoc, see interface TokenIterator
+ public boolean hasNext() {
+ return (this.currentToken != null);
+ }
+
+
+ /**
+ * Obtains the next token from this iteration.
+ *
+ * @return the next token in this iteration
+ *
+ * @throws NoSuchElementException if the iteration is already over
+ * @throws ParseException if an invalid header value is encountered
+ */
+ public String nextToken()
+ throws NoSuchElementException, ParseException {
+
+ if (this.currentToken == null) {
+ throw new NoSuchElementException("Iteration already finished.");
+ }
+
+ final String result = this.currentToken;
+ // updates currentToken, may trigger ParseException:
+ this.searchPos = findNext(this.searchPos);
+
+ return result;
+ }
+
+
+ /**
+ * Returns the next token.
+ * Same as {@link #nextToken}, but with generic return type.
+ *
+ * @return the next token in this iteration
+ *
+ * @throws NoSuchElementException if there are no more tokens
+ * @throws ParseException if an invalid header value is encountered
+ */
+ public final Object next()
+ throws NoSuchElementException, ParseException {
+ return nextToken();
+ }
+
+
+ /**
+ * Removing tokens is not supported.
+ *
+ * @throws UnsupportedOperationException always
+ */
+ public final void remove()
+ throws UnsupportedOperationException {
+
+ throw new UnsupportedOperationException
+ ("Removing tokens is not supported.");
+ }
+
+
+ /**
+ * Determines the next token.
+ * If found, the token is stored in {@link #currentToken}.
+ * The return value indicates the position after the token
+ * in {@link #currentHeader}. If necessary, the next header
+ * will be obtained from {@link #headerIt}.
+ * If not found, {@link #currentToken} is set to <code>null</code>.
+ *
+ * @param pos the position in the current header at which to
+ * start the search, -1 to search in the first header
+ *
+ * @return the position after the found token in the current header, or
+ * negative if there was no next token
+ *
+ * @throws ParseException if an invalid header value is encountered
+ */
+ protected int findNext(final int pos) throws ParseException {
+ int from = pos;
+ if (from < 0) {
+ // called from the constructor, initialize the first header
+ if (!this.headerIt.hasNext()) {
+ return -1;
+ }
+ this.currentHeader = this.headerIt.nextHeader().getValue();
+ from = 0;
+ } else {
+ // called after a token, make sure there is a separator
+ from = findTokenSeparator(from);
+ }
+
+ final int start = findTokenStart(from);
+ if (start < 0) {
+ this.currentToken = null;
+ return -1; // nothing found
+ }
+
+ final int end = findTokenEnd(start);
+ this.currentToken = createToken(this.currentHeader, start, end);
+ return end;
+ }
+
+
+ /**
+ * Creates a new token to be returned.
+ * Called from {@link #findNext findNext} after the token is identified.
+ * The default implementation simply calls
+ * {@link java.lang.String#substring String.substring}.
+ * <br/>
+ * If header values are significantly longer than tokens, and some
+ * tokens are permanently referenced by the application, there can
+ * be problems with garbage collection. A substring will hold a
+ * reference to the full characters of the original string and
+ * therefore occupies more memory than might be expected.
+ * To avoid this, override this method and create a new string
+ * instead of a substring.
+ *
+ * @param value the full header value from which to create a token
+ * @param start the index of the first token character
+ * @param end the index after the last token character
+ *
+ * @return a string representing the token identified by the arguments
+ */
+ protected String createToken(final String value, final int start, final int end) {
+ return value.substring(start, end);
+ }
+
+
+ /**
+ * Determines the starting position of the next token.
+ * This method will iterate over headers if necessary.
+ *
+ * @param pos the position in the current header at which to
+ * start the search
+ *
+ * @return the position of the token start in the current header,
+ * negative if no token start could be found
+ */
+ protected int findTokenStart(final int pos) {
+ int from = Args.notNegative(pos, "Search position");
+ boolean found = false;
+ while (!found && (this.currentHeader != null)) {
+
+ final int to = this.currentHeader.length();
+ while (!found && (from < to)) {
+
+ final char ch = this.currentHeader.charAt(from);
+ if (isTokenSeparator(ch) || isWhitespace(ch)) {
+ // whitspace and token separators are skipped
+ from++;
+ } else if (isTokenChar(this.currentHeader.charAt(from))) {
+ // found the start of a token
+ found = true;
+ } else {
+ throw new ParseException
+ ("Invalid character before token (pos " + from +
+ "): " + this.currentHeader);
+ }
+ }
+ if (!found) {
+ if (this.headerIt.hasNext()) {
+ this.currentHeader = this.headerIt.nextHeader().getValue();
+ from = 0;
+ } else {
+ this.currentHeader = null;
+ }
+ }
+ } // while headers
+
+ return found ? from : -1;
+ }
+
+
+ /**
+ * Determines the position of the next token separator.
+ * Because of multi-header joining rules, the end of a
+ * header value is a token separator. This method does
+ * therefore not need to iterate over headers.
+ *
+ * @param pos the position in the current header at which to
+ * start the search
+ *
+ * @return the position of a token separator in the current header,
+ * or at the end
+ *
+ * @throws ParseException
+ * if a new token is found before a token separator.
+ * RFC 2616, section 2.1 explicitly requires a comma between
+ * tokens for <tt>#</tt>.
+ */
+ protected int findTokenSeparator(final int pos) {
+ int from = Args.notNegative(pos, "Search position");
+ boolean found = false;
+ final int to = this.currentHeader.length();
+ while (!found && (from < to)) {
+ final char ch = this.currentHeader.charAt(from);
+ if (isTokenSeparator(ch)) {
+ found = true;
+ } else if (isWhitespace(ch)) {
+ from++;
+ } else if (isTokenChar(ch)) {
+ throw new ParseException
+ ("Tokens without separator (pos " + from +
+ "): " + this.currentHeader);
+ } else {
+ throw new ParseException
+ ("Invalid character after token (pos " + from +
+ "): " + this.currentHeader);
+ }
+ }
+
+ return from;
+ }
+
+
+ /**
+ * Determines the ending position of the current token.
+ * This method will not leave the current header value,
+ * since the end of the header value is a token boundary.
+ *
+ * @param from the position of the first character of the token
+ *
+ * @return the position after the last character of the token.
+ * The behavior is undefined if <code>from</code> does not
+ * point to a token character in the current header value.
+ */
+ protected int findTokenEnd(final int from) {
+ Args.notNegative(from, "Search position");
+ final int to = this.currentHeader.length();
+ int end = from+1;
+ while ((end < to) && isTokenChar(this.currentHeader.charAt(end))) {
+ end++;
+ }
+
+ return end;
+ }
+
+
+ /**
+ * Checks whether a character is a token separator.
+ * RFC 2616, section 2.1 defines comma as the separator for
+ * <tt>#token</tt> sequences. The end of a header value will
+ * also separate tokens, but that is not a character check.
+ *
+ * @param ch the character to check
+ *
+ * @return <code>true</code> if the character is a token separator,
+ * <code>false</code> otherwise
+ */
+ protected boolean isTokenSeparator(final char ch) {
+ return (ch == ',');
+ }
+
+
+ /**
+ * Checks whether a character is a whitespace character.
+ * RFC 2616, section 2.2 defines space and horizontal tab as whitespace.
+ * The optional preceeding line break is irrelevant, since header
+ * continuation is handled transparently when parsing messages.
+ *
+ * @param ch the character to check
+ *
+ * @return <code>true</code> if the character is whitespace,
+ * <code>false</code> otherwise
+ */
+ protected boolean isWhitespace(final char ch) {
+
+ // we do not use Character.isWhitspace(ch) here, since that allows
+ // many control characters which are not whitespace as per RFC 2616
+ return ((ch == '\t') || Character.isSpaceChar(ch));
+ }
+
+
+ /**
+ * Checks whether a character is a valid token character.
+ * Whitespace, control characters, and HTTP separators are not
+ * valid token characters. The HTTP specification (RFC 2616, section 2.2)
+ * defines tokens only for the US-ASCII character set, this
+ * method extends the definition to other character sets.
+ *
+ * @param ch the character to check
+ *
+ * @return <code>true</code> if the character is a valid token start,
+ * <code>false</code> otherwise
+ */
+ protected boolean isTokenChar(final char ch) {
+
+ // common sense extension of ALPHA + DIGIT
+ if (Character.isLetterOrDigit(ch)) {
+ return true;
+ }
+
+ // common sense extension of CTL
+ if (Character.isISOControl(ch)) {
+ return false;
+ }
+
+ // no common sense extension for this
+ if (isHttpSeparator(ch)) {
+ return false;
+ }
+
+ // RFC 2616, section 2.2 defines a token character as
+ // "any CHAR except CTLs or separators". The controls
+ // and separators are included in the checks above.
+ // This will yield unexpected results for Unicode format characters.
+ // If that is a problem, overwrite isHttpSeparator(char) to filter
+ // out the false positives.
+ return true;
+ }
+
+
+ /**
+ * Checks whether a character is an HTTP separator.
+ * The implementation in this class checks only for the HTTP separators
+ * defined in RFC 2616, section 2.2. If you need to detect other
+ * separators beyond the US-ASCII character set, override this method.
+ *
+ * @param ch the character to check
+ *
+ * @return <code>true</code> if the character is an HTTP separator
+ */
+ protected boolean isHttpSeparator(final char ch) {
+ return (HTTP_SEPARATORS.indexOf(ch) >= 0);
+ }
+
+
+} // class BasicTokenIterator
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BufferedHeader.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BufferedHeader.java
new file mode 100644
index 0000000000..11b70bdcb0
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/BufferedHeader.java
@@ -0,0 +1,130 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import java.io.Serializable;
+
+import ch.boye.httpclientandroidlib.FormattedHeader;
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.ParseException;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * This class represents a raw HTTP header whose content is parsed 'on demand'
+ * only when the header value needs to be consumed.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class BufferedHeader implements FormattedHeader, Cloneable, Serializable {
+
+ private static final long serialVersionUID = -2768352615787625448L;
+
+ /**
+ * Header name.
+ */
+ private final String name;
+
+ /**
+ * The buffer containing the entire header line.
+ */
+ private final CharArrayBuffer buffer;
+
+ /**
+ * The beginning of the header value in the buffer
+ */
+ private final int valuePos;
+
+
+ /**
+ * Creates a new header from a buffer.
+ * The name of the header will be parsed immediately,
+ * the value only if it is accessed.
+ *
+ * @param buffer the buffer containing the header to represent
+ *
+ * @throws ParseException in case of a parse error
+ */
+ public BufferedHeader(final CharArrayBuffer buffer)
+ throws ParseException {
+
+ super();
+ Args.notNull(buffer, "Char array buffer");
+ final int colon = buffer.indexOf(':');
+ if (colon == -1) {
+ throw new ParseException
+ ("Invalid header: " + buffer.toString());
+ }
+ final String s = buffer.substringTrimmed(0, colon);
+ if (s.length() == 0) {
+ throw new ParseException
+ ("Invalid header: " + buffer.toString());
+ }
+ this.buffer = buffer;
+ this.name = s;
+ this.valuePos = colon + 1;
+ }
+
+
+ public String getName() {
+ return this.name;
+ }
+
+ public String getValue() {
+ return this.buffer.substringTrimmed(this.valuePos, this.buffer.length());
+ }
+
+ public HeaderElement[] getElements() throws ParseException {
+ final ParserCursor cursor = new ParserCursor(0, this.buffer.length());
+ cursor.updatePos(this.valuePos);
+ return BasicHeaderValueParser.INSTANCE.parseElements(this.buffer, cursor);
+ }
+
+ public int getValuePos() {
+ return this.valuePos;
+ }
+
+ public CharArrayBuffer getBuffer() {
+ return this.buffer;
+ }
+
+ @Override
+ public String toString() {
+ return this.buffer.toString();
+ }
+
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ // buffer is considered immutable
+ // no need to make a copy of it
+ return super.clone();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/HeaderGroup.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/HeaderGroup.java
new file mode 100644
index 0000000000..fe8cdce95c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/HeaderGroup.java
@@ -0,0 +1,311 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HeaderIterator;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * A class for combining a set of headers.
+ * This class allows for multiple headers with the same name and
+ * keeps track of the order in which headers were added.
+ *
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class HeaderGroup implements Cloneable, Serializable {
+
+ private static final long serialVersionUID = 2608834160639271617L;
+
+ /** The list of headers for this group, in the order in which they were added */
+ private final List<Header> headers;
+
+ /**
+ * Constructor for HeaderGroup.
+ */
+ public HeaderGroup() {
+ this.headers = new ArrayList<Header>(16);
+ }
+
+ /**
+ * Removes any contained headers.
+ */
+ public void clear() {
+ headers.clear();
+ }
+
+ /**
+ * Adds the given header to the group. The order in which this header was
+ * added is preserved.
+ *
+ * @param header the header to add
+ */
+ public void addHeader(final Header header) {
+ if (header == null) {
+ return;
+ }
+ headers.add(header);
+ }
+
+ /**
+ * Removes the given header.
+ *
+ * @param header the header to remove
+ */
+ public void removeHeader(final Header header) {
+ if (header == null) {
+ return;
+ }
+ headers.remove(header);
+ }
+
+ /**
+ * Replaces the first occurence of the header with the same name. If no header with
+ * the same name is found the given header is added to the end of the list.
+ *
+ * @param header the new header that should replace the first header with the same
+ * name if present in the list.
+ */
+ public void updateHeader(final Header header) {
+ if (header == null) {
+ return;
+ }
+ // HTTPCORE-361 : we don't use the for-each syntax, i.e.
+ // for (Header header : headers)
+ // as that creates an Iterator that needs to be garbage-collected
+ for (int i = 0; i < this.headers.size(); i++) {
+ final Header current = this.headers.get(i);
+ if (current.getName().equalsIgnoreCase(header.getName())) {
+ this.headers.set(i, header);
+ return;
+ }
+ }
+ this.headers.add(header);
+ }
+
+ /**
+ * Sets all of the headers contained within this group overriding any
+ * existing headers. The headers are added in the order in which they appear
+ * in the array.
+ *
+ * @param headers the headers to set
+ */
+ public void setHeaders(final Header[] headers) {
+ clear();
+ if (headers == null) {
+ return;
+ }
+ Collections.addAll(this.headers, headers);
+ }
+
+ /**
+ * Gets a header representing all of the header values with the given name.
+ * If more that one header with the given name exists the values will be
+ * combined with a "," as per RFC 2616.
+ *
+ * <p>Header name comparison is case insensitive.
+ *
+ * @param name the name of the header(s) to get
+ * @return a header with a condensed value or <code>null</code> if no
+ * headers by the given name are present
+ */
+ public Header getCondensedHeader(final String name) {
+ final Header[] hdrs = getHeaders(name);
+
+ if (hdrs.length == 0) {
+ return null;
+ } else if (hdrs.length == 1) {
+ return hdrs[0];
+ } else {
+ final CharArrayBuffer valueBuffer = new CharArrayBuffer(128);
+ valueBuffer.append(hdrs[0].getValue());
+ for (int i = 1; i < hdrs.length; i++) {
+ valueBuffer.append(", ");
+ valueBuffer.append(hdrs[i].getValue());
+ }
+
+ return new BasicHeader(name.toLowerCase(Locale.ENGLISH), valueBuffer.toString());
+ }
+ }
+
+ /**
+ * Gets all of the headers with the given name. The returned array
+ * maintains the relative order in which the headers were added.
+ *
+ * <p>Header name comparison is case insensitive.
+ *
+ * @param name the name of the header(s) to get
+ *
+ * @return an array of length >= 0
+ */
+ public Header[] getHeaders(final String name) {
+ final List<Header> headersFound = new ArrayList<Header>();
+ // HTTPCORE-361 : we don't use the for-each syntax, i.e.
+ // for (Header header : headers)
+ // as that creates an Iterator that needs to be garbage-collected
+ for (int i = 0; i < this.headers.size(); i++) {
+ final Header header = this.headers.get(i);
+ if (header.getName().equalsIgnoreCase(name)) {
+ headersFound.add(header);
+ }
+ }
+
+ return headersFound.toArray(new Header[headersFound.size()]);
+ }
+
+ /**
+ * Gets the first header with the given name.
+ *
+ * <p>Header name comparison is case insensitive.
+ *
+ * @param name the name of the header to get
+ * @return the first header or <code>null</code>
+ */
+ public Header getFirstHeader(final String name) {
+ // HTTPCORE-361 : we don't use the for-each syntax, i.e.
+ // for (Header header : headers)
+ // as that creates an Iterator that needs to be garbage-collected
+ for (int i = 0; i < this.headers.size(); i++) {
+ final Header header = this.headers.get(i);
+ if (header.getName().equalsIgnoreCase(name)) {
+ return header;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets the last header with the given name.
+ *
+ * <p>Header name comparison is case insensitive.
+ *
+ * @param name the name of the header to get
+ * @return the last header or <code>null</code>
+ */
+ public Header getLastHeader(final String name) {
+ // start at the end of the list and work backwards
+ for (int i = headers.size() - 1; i >= 0; i--) {
+ final Header header = headers.get(i);
+ if (header.getName().equalsIgnoreCase(name)) {
+ return header;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets all of the headers contained within this group.
+ *
+ * @return an array of length >= 0
+ */
+ public Header[] getAllHeaders() {
+ return headers.toArray(new Header[headers.size()]);
+ }
+
+ /**
+ * Tests if headers with the given name are contained within this group.
+ *
+ * <p>Header name comparison is case insensitive.
+ *
+ * @param name the header name to test for
+ * @return <code>true</code> if at least one header with the name is
+ * contained, <code>false</code> otherwise
+ */
+ public boolean containsHeader(final String name) {
+ // HTTPCORE-361 : we don't use the for-each syntax, i.e.
+ // for (Header header : headers)
+ // as that creates an Iterator that needs to be garbage-collected
+ for (int i = 0; i < this.headers.size(); i++) {
+ final Header header = this.headers.get(i);
+ if (header.getName().equalsIgnoreCase(name)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns an iterator over this group of headers.
+ *
+ * @return iterator over this group of headers.
+ *
+ * @since 4.0
+ */
+ public HeaderIterator iterator() {
+ return new BasicListHeaderIterator(this.headers, null);
+ }
+
+ /**
+ * Returns an iterator over the headers with a given name in this group.
+ *
+ * @param name the name of the headers over which to iterate, or
+ * <code>null</code> for all headers
+ *
+ * @return iterator over some headers in this group.
+ *
+ * @since 4.0
+ */
+ public HeaderIterator iterator(final String name) {
+ return new BasicListHeaderIterator(this.headers, name);
+ }
+
+ /**
+ * Returns a copy of this object
+ *
+ * @return copy of this object
+ *
+ * @since 4.0
+ */
+ public HeaderGroup copy() {
+ final HeaderGroup clone = new HeaderGroup();
+ clone.headers.addAll(this.headers);
+ return clone;
+ }
+
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+
+ @Override
+ public String toString() {
+ return this.headers.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/HeaderValueFormatter.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/HeaderValueFormatter.java
new file mode 100644
index 0000000000..8a1acc04ef
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/HeaderValueFormatter.java
@@ -0,0 +1,122 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Interface for formatting elements of a header value.
+ * This is the complement to {@link HeaderValueParser}.
+ * Instances of this interface are expected to be stateless and thread-safe.
+ *
+ * <p>
+ * All formatting methods accept an optional buffer argument.
+ * If a buffer is passed in, the formatted element will be appended
+ * and the modified buffer is returned. If no buffer is passed in,
+ * a new buffer will be created and filled with the formatted element.
+ * In both cases, the caller is allowed to modify the returned buffer.
+ * </p>
+ *
+ * @since 4.0
+ */
+public interface HeaderValueFormatter {
+
+ /**
+ * Formats an array of header elements.
+ *
+ * @param buffer the buffer to append to, or
+ * <code>null</code> to create a new buffer
+ * @param elems the header elements to format
+ * @param quote <code>true</code> to always format with quoted values,
+ * <code>false</code> to use quotes only when necessary
+ *
+ * @return a buffer with the formatted header elements.
+ * If the <code>buffer</code> argument was not <code>null</code>,
+ * that buffer will be used and returned.
+ */
+ CharArrayBuffer formatElements(CharArrayBuffer buffer,
+ HeaderElement[] elems,
+ boolean quote);
+
+ /**
+ * Formats one header element.
+ *
+ * @param buffer the buffer to append to, or
+ * <code>null</code> to create a new buffer
+ * @param elem the header element to format
+ * @param quote <code>true</code> to always format with quoted values,
+ * <code>false</code> to use quotes only when necessary
+ *
+ * @return a buffer with the formatted header element.
+ * If the <code>buffer</code> argument was not <code>null</code>,
+ * that buffer will be used and returned.
+ */
+ CharArrayBuffer formatHeaderElement(CharArrayBuffer buffer,
+ HeaderElement elem,
+ boolean quote);
+
+ /**
+ * Formats the parameters of a header element.
+ * That's a list of name-value pairs, to be separated by semicolons.
+ * This method will <i>not</i> generate a leading semicolon.
+ *
+ * @param buffer the buffer to append to, or
+ * <code>null</code> to create a new buffer
+ * @param nvps the parameters (name-value pairs) to format
+ * @param quote <code>true</code> to always format with quoted values,
+ * <code>false</code> to use quotes only when necessary
+ *
+ * @return a buffer with the formatted parameters.
+ * If the <code>buffer</code> argument was not <code>null</code>,
+ * that buffer will be used and returned.
+ */
+ CharArrayBuffer formatParameters(CharArrayBuffer buffer,
+ NameValuePair[] nvps,
+ boolean quote);
+
+ /**
+ * Formats one name-value pair, where the value is optional.
+ *
+ * @param buffer the buffer to append to, or
+ * <code>null</code> to create a new buffer
+ * @param nvp the name-value pair to format
+ * @param quote <code>true</code> to always format with a quoted value,
+ * <code>false</code> to use quotes only when necessary
+ *
+ * @return a buffer with the formatted name-value pair.
+ * If the <code>buffer</code> argument was not <code>null</code>,
+ * that buffer will be used and returned.
+ */
+ CharArrayBuffer formatNameValuePair(CharArrayBuffer buffer,
+ NameValuePair nvp,
+ boolean quote);
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/HeaderValueParser.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/HeaderValueParser.java
new file mode 100644
index 0000000000..5286249c70
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/HeaderValueParser.java
@@ -0,0 +1,134 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.ParseException;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Interface for parsing header values into elements.
+ * Instances of this interface are expected to be stateless and thread-safe.
+ *
+ * @since 4.0
+ */
+public interface HeaderValueParser {
+
+ /**
+ * Parses a header value into elements.
+ * Parse errors are indicated as <code>RuntimeException</code>.
+ * <p>
+ * Some HTTP headers (such as the set-cookie header) have values that
+ * can be decomposed into multiple elements. In order to be processed
+ * by this parser, such headers must be in the following form:
+ * </p>
+ * <pre>
+ * header = [ element ] *( "," [ element ] )
+ * element = name [ "=" [ value ] ] *( ";" [ param ] )
+ * param = name [ "=" [ value ] ]
+ *
+ * name = token
+ * value = ( token | quoted-string )
+ *
+ * token = 1*&lt;any char except "=", ",", ";", &lt;"&gt; and
+ * white space&gt;
+ * quoted-string = &lt;"&gt; *( text | quoted-char ) &lt;"&gt;
+ * text = any char except &lt;"&gt;
+ * quoted-char = "\" char
+ * </pre>
+ * <p>
+ * Any amount of white space is allowed between any part of the
+ * header, element or param and is ignored. A missing value in any
+ * element or param will be stored as the empty {@link String};
+ * if the "=" is also missing <var>null</var> will be stored instead.
+ * </p>
+ *
+ * @param buffer buffer holding the header value to parse
+ * @param cursor the parser cursor containing the current position and
+ * the bounds within the buffer for the parsing operation
+ *
+ * @return an array holding all elements of the header value
+ *
+ * @throws ParseException in case of a parse error
+ */
+ HeaderElement[] parseElements(
+ CharArrayBuffer buffer,
+ ParserCursor cursor) throws ParseException;
+
+ /**
+ * Parses a single header element.
+ * A header element consist of a semicolon-separate list
+ * of name=value definitions.
+ *
+ * @param buffer buffer holding the element to parse
+ * @param cursor the parser cursor containing the current position and
+ * the bounds within the buffer for the parsing operation
+ *
+ * @return the parsed element
+ *
+ * @throws ParseException in case of a parse error
+ */
+ HeaderElement parseHeaderElement(
+ CharArrayBuffer buffer,
+ ParserCursor cursor) throws ParseException;
+
+ /**
+ * Parses a list of name-value pairs.
+ * These lists are used to specify parameters to a header element.
+ * Parse errors are indicated as <code>ParseException</code>.
+ *
+ * @param buffer buffer holding the name-value list to parse
+ * @param cursor the parser cursor containing the current position and
+ * the bounds within the buffer for the parsing operation
+ *
+ * @return an array holding all items of the name-value list
+ *
+ * @throws ParseException in case of a parse error
+ */
+ NameValuePair[] parseParameters(
+ CharArrayBuffer buffer,
+ ParserCursor cursor) throws ParseException;
+
+
+ /**
+ * Parses a name=value specification, where the = and value are optional.
+ *
+ * @param buffer the buffer holding the name-value pair to parse
+ * @param cursor the parser cursor containing the current position and
+ * the bounds within the buffer for the parsing operation
+ *
+ * @return the name-value pair, where the value is <code>null</code>
+ * if no value is specified
+ */
+ NameValuePair parseNameValuePair(
+ CharArrayBuffer buffer,
+ ParserCursor cursor) throws ParseException;
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/LineFormatter.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/LineFormatter.java
new file mode 100644
index 0000000000..33e751a759
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/LineFormatter.java
@@ -0,0 +1,131 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.RequestLine;
+import ch.boye.httpclientandroidlib.StatusLine;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Interface for formatting elements of the HEAD section of an HTTP message.
+ * This is the complement to {@link LineParser}.
+ * There are individual methods for formatting a request line, a
+ * status line, or a header line. The formatting does <i>not</i> include the
+ * trailing line break sequence CR-LF.
+ * Instances of this interface are expected to be stateless and thread-safe.
+ *
+ * <p>
+ * The formatted lines are returned in memory, the formatter does not depend
+ * on any specific IO mechanism.
+ * In order to avoid unnecessary creation of temporary objects,
+ * a buffer can be passed as argument to all formatting methods.
+ * The implementation may or may not actually use that buffer for formatting.
+ * If it is used, the buffer will first be cleared by the
+ * <code>formatXXX</code> methods.
+ * The argument buffer can always be re-used after the call. The buffer
+ * returned as the result, if it is different from the argument buffer,
+ * MUST NOT be modified.
+ * </p>
+ *
+ * @since 4.0
+ */
+public interface LineFormatter {
+
+ /**
+ * Formats a protocol version.
+ * This method does <i>not</i> follow the general contract for
+ * <code>buffer</code> arguments.
+ * It does <i>not</i> clear the argument buffer, but appends instead.
+ * The returned buffer can always be modified by the caller.
+ * Because of these differing conventions, it is not named
+ * <code>formatProtocolVersion</code>.
+ *
+ * @param buffer a buffer to which to append, or <code>null</code>
+ * @param version the protocol version to format
+ *
+ * @return a buffer with the formatted protocol version appended.
+ * The caller is allowed to modify the result buffer.
+ * If the <code>buffer</code> argument is not <code>null</code>,
+ * the returned buffer is the argument buffer.
+ */
+ CharArrayBuffer appendProtocolVersion(CharArrayBuffer buffer,
+ ProtocolVersion version);
+
+ /**
+ * Formats a request line.
+ *
+ * @param buffer a buffer available for formatting, or
+ * <code>null</code>.
+ * The buffer will be cleared before use.
+ * @param reqline the request line to format
+ *
+ * @return the formatted request line
+ */
+ CharArrayBuffer formatRequestLine(CharArrayBuffer buffer,
+ RequestLine reqline);
+
+ /**
+ * Formats a status line.
+ *
+ * @param buffer a buffer available for formatting, or
+ * <code>null</code>.
+ * The buffer will be cleared before use.
+ * @param statline the status line to format
+ *
+ * @return the formatted status line
+ *
+ * @throws ch.boye.httpclientandroidlib.ParseException in case of a parse error
+ */
+ CharArrayBuffer formatStatusLine(CharArrayBuffer buffer,
+ StatusLine statline);
+
+ /**
+ * Formats a header.
+ * Due to header continuation, the result may be multiple lines.
+ * In order to generate well-formed HTTP, the lines in the result
+ * must be separated by the HTTP line break sequence CR-LF.
+ * There is <i>no</i> trailing CR-LF in the result.
+ * <br/>
+ * See the class comment for details about the buffer argument.
+ *
+ * @param buffer a buffer available for formatting, or
+ * <code>null</code>.
+ * The buffer will be cleared before use.
+ * @param header the header to format
+ *
+ * @return a buffer holding the formatted header, never <code>null</code>.
+ * The returned buffer may be different from the argument buffer.
+ *
+ * @throws ch.boye.httpclientandroidlib.ParseException in case of a parse error
+ */
+ CharArrayBuffer formatHeader(CharArrayBuffer buffer,
+ Header header);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/LineParser.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/LineParser.java
new file mode 100644
index 0000000000..d94ca3d752
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/LineParser.java
@@ -0,0 +1,137 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.ParseException;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.RequestLine;
+import ch.boye.httpclientandroidlib.StatusLine;
+import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
+
+/**
+ * Interface for parsing lines in the HEAD section of an HTTP message.
+ * There are individual methods for parsing a request line, a
+ * status line, or a header line.
+ * The lines to parse are passed in memory, the parser does not depend
+ * on any specific IO mechanism.
+ * Instances of this interface are expected to be stateless and thread-safe.
+ *
+ * @since 4.0
+ */
+public interface LineParser {
+
+ /**
+ * Parses the textual representation of a protocol version.
+ * This is needed for parsing request lines (last element)
+ * as well as status lines (first element).
+ *
+ * @param buffer a buffer holding the protocol version to parse
+ * @param cursor the parser cursor containing the current position and
+ * the bounds within the buffer for the parsing operation
+ *
+ * @return the parsed protocol version
+ *
+ * @throws ParseException in case of a parse error
+ */
+ ProtocolVersion parseProtocolVersion(
+ CharArrayBuffer buffer,
+ ParserCursor cursor) throws ParseException;
+
+ /**
+ * Checks whether there likely is a protocol version in a line.
+ * This method implements a <i>heuristic</i> to check for a
+ * likely protocol version specification. It does <i>not</i>
+ * guarantee that {@link #parseProtocolVersion} would not
+ * detect a parse error.
+ * This can be used to detect garbage lines before a request
+ * or status line.
+ *
+ * @param buffer a buffer holding the line to inspect
+ * @param cursor the cursor at which to check for a protocol version, or
+ * negative for "end of line". Whether the check tolerates
+ * whitespace before or after the protocol version is
+ * implementation dependent.
+ *
+ * @return <code>true</code> if there is a protocol version at the
+ * argument index (possibly ignoring whitespace),
+ * <code>false</code> otherwise
+ */
+ boolean hasProtocolVersion(
+ CharArrayBuffer buffer,
+ ParserCursor cursor);
+
+ /**
+ * Parses a request line.
+ *
+ * @param buffer a buffer holding the line to parse
+ * @param cursor the parser cursor containing the current position and
+ * the bounds within the buffer for the parsing operation
+ *
+ * @return the parsed request line
+ *
+ * @throws ParseException in case of a parse error
+ */
+ RequestLine parseRequestLine(
+ CharArrayBuffer buffer,
+ ParserCursor cursor) throws ParseException;
+
+ /**
+ * Parses a status line.
+ *
+ * @param buffer a buffer holding the line to parse
+ * @param cursor the parser cursor containing the current position and
+ * the bounds within the buffer for the parsing operation
+ *
+ * @return the parsed status line
+ *
+ * @throws ParseException in case of a parse error
+ */
+ StatusLine parseStatusLine(
+ CharArrayBuffer buffer,
+ ParserCursor cursor) throws ParseException;
+
+ /**
+ * Creates a header from a line.
+ * The full header line is expected here. Header continuation lines
+ * must be joined by the caller before invoking this method.
+ *
+ * @param buffer a buffer holding the full header line.
+ * This buffer MUST NOT be re-used afterwards, since
+ * the returned object may reference the contents later.
+ *
+ * @return the header in the argument buffer.
+ * The returned object MAY be a wrapper for the argument buffer.
+ * The argument buffer MUST NOT be re-used or changed afterwards.
+ *
+ * @throws ParseException in case of a parse error
+ */
+ Header parseHeader(CharArrayBuffer buffer)
+ throws ParseException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/ParserCursor.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/ParserCursor.java
new file mode 100644
index 0000000000..163352bb71
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/ParserCursor.java
@@ -0,0 +1,100 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.message;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+
+/**
+ * This class represents a context of a parsing operation:
+ * <ul>
+ * <li>the current position the parsing operation is expected to start at</li>
+ * <li>the bounds limiting the scope of the parsing operation</li>
+ * </ul>
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public class ParserCursor {
+
+ private final int lowerBound;
+ private final int upperBound;
+ private int pos;
+
+ public ParserCursor(final int lowerBound, final int upperBound) {
+ super();
+ if (lowerBound < 0) {
+ throw new IndexOutOfBoundsException("Lower bound cannot be negative");
+ }
+ if (lowerBound > upperBound) {
+ throw new IndexOutOfBoundsException("Lower bound cannot be greater then upper bound");
+ }
+ this.lowerBound = lowerBound;
+ this.upperBound = upperBound;
+ this.pos = lowerBound;
+ }
+
+ public int getLowerBound() {
+ return this.lowerBound;
+ }
+
+ public int getUpperBound() {
+ return this.upperBound;
+ }
+
+ public int getPos() {
+ return this.pos;
+ }
+
+ public void updatePos(final int pos) {
+ if (pos < this.lowerBound) {
+ throw new IndexOutOfBoundsException("pos: "+pos+" < lowerBound: "+this.lowerBound);
+ }
+ if (pos > this.upperBound) {
+ throw new IndexOutOfBoundsException("pos: "+pos+" > upperBound: "+this.upperBound);
+ }
+ this.pos = pos;
+ }
+
+ public boolean atEnd() {
+ return this.pos >= this.upperBound;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder buffer = new StringBuilder();
+ buffer.append('[');
+ buffer.append(Integer.toString(this.lowerBound));
+ buffer.append('>');
+ buffer.append(Integer.toString(this.pos));
+ buffer.append('>');
+ buffer.append(Integer.toString(this.upperBound));
+ buffer.append(']');
+ return buffer.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/package-info.java
new file mode 100644
index 0000000000..bac6c2e327
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/message/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Core HTTP message components, message element parser
+ * and writer APIs and their default implementations.
+ */
+package ch.boye.httpclientandroidlib.message;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/package-info.java
new file mode 100644
index 0000000000..3001825af5
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/package-info.java
@@ -0,0 +1,42 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Core HTTP component APIs and primitives.
+ * <p/>
+ * These deal with the fundamental things required for using the
+ * HTTP protocol, such as representing a
+ * {@link ch.boye.httpclientandroidlib.HttpMessage message} including it's
+ * {@link ch.boye.httpclientandroidlib.Header headers} and optional
+ * {@link ch.boye.httpclientandroidlib.HttpEntity entity}, and
+ * {@link ch.boye.httpclientandroidlib.HttpConnection connections}
+ * over which messages are sent. In order to prepare messages
+ * before sending or after receiving, there are interceptors for
+ * {@link ch.boye.httpclientandroidlib.HttpRequestInterceptor requests} and
+ * {@link ch.boye.httpclientandroidlib.HttpResponseInterceptor responses}.
+ */
+package ch.boye.httpclientandroidlib;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/AbstractHttpParams.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/AbstractHttpParams.java
new file mode 100644
index 0000000000..0d41b8f007
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/AbstractHttpParams.java
@@ -0,0 +1,124 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.params;
+
+import java.util.Set;
+
+/**
+ * Abstract base class for parameter collections.
+ * Type specific setters and getters are mapped to the abstract,
+ * generic getters and setters.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use configuration classes provided 'ch.boye.httpclientandroidlib.config'
+ * and 'ch.boye.httpclientandroidlib.client.config'
+ */
+@Deprecated
+public abstract class AbstractHttpParams implements HttpParams, HttpParamsNames {
+
+ /**
+ * Instantiates parameters.
+ */
+ protected AbstractHttpParams() {
+ super();
+ }
+
+ public long getLongParameter(final String name, final long defaultValue) {
+ final Object param = getParameter(name);
+ if (param == null) {
+ return defaultValue;
+ }
+ return ((Long) param).longValue();
+ }
+
+ public HttpParams setLongParameter(final String name, final long value) {
+ setParameter(name, Long.valueOf(value));
+ return this;
+ }
+
+ public int getIntParameter(final String name, final int defaultValue) {
+ final Object param = getParameter(name);
+ if (param == null) {
+ return defaultValue;
+ }
+ return ((Integer) param).intValue();
+ }
+
+ public HttpParams setIntParameter(final String name, final int value) {
+ setParameter(name, Integer.valueOf(value));
+ return this;
+ }
+
+ public double getDoubleParameter(final String name, final double defaultValue) {
+ final Object param = getParameter(name);
+ if (param == null) {
+ return defaultValue;
+ }
+ return ((Double) param).doubleValue();
+ }
+
+ public HttpParams setDoubleParameter(final String name, final double value) {
+ setParameter(name, Double.valueOf(value));
+ return this;
+ }
+
+ public boolean getBooleanParameter(final String name, final boolean defaultValue) {
+ final Object param = getParameter(name);
+ if (param == null) {
+ return defaultValue;
+ }
+ return ((Boolean) param).booleanValue();
+ }
+
+ public HttpParams setBooleanParameter(final String name, final boolean value) {
+ setParameter(name, value ? Boolean.TRUE : Boolean.FALSE);
+ return this;
+ }
+
+ public boolean isParameterTrue(final String name) {
+ return getBooleanParameter(name, false);
+ }
+
+ public boolean isParameterFalse(final String name) {
+ return !getBooleanParameter(name, false);
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p/>
+ * Dummy implementation - must be overridden by subclasses.
+ *
+ * @since 4.2
+ * @throws UnsupportedOperationException - always
+ */
+ public Set<String> getNames(){
+ throw new UnsupportedOperationException();
+ }
+
+} // class AbstractHttpParams
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/BasicHttpParams.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/BasicHttpParams.java
new file mode 100644
index 0000000000..420ac1a08f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/BasicHttpParams.java
@@ -0,0 +1,190 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.params;
+
+import java.io.Serializable;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+
+/**
+ * Default implementation of {@link HttpParams} interface.
+ * <p>
+ * Please note access to the internal structures of this class is not
+ * synchronized and therefore this class may be thread-unsafe.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use configuration classes provided 'ch.boye.httpclientandroidlib.config'
+ * and 'ch.boye.httpclientandroidlib.client.config'
+ */
+@Deprecated
+@ThreadSafe
+public class BasicHttpParams extends AbstractHttpParams implements Serializable, Cloneable {
+
+ private static final long serialVersionUID = -7086398485908701455L;
+
+ /** Map of HTTP parameters that this collection contains. */
+ private final Map<String, Object> parameters = new ConcurrentHashMap<String, Object>();
+
+ public BasicHttpParams() {
+ super();
+ }
+
+ public Object getParameter(final String name) {
+ return this.parameters.get(name);
+ }
+
+ public HttpParams setParameter(final String name, final Object value) {
+ if (name == null) {
+ return this;
+ }
+ if (value != null) {
+ this.parameters.put(name, value);
+ } else {
+ this.parameters.remove(name);
+ }
+ return this;
+ }
+
+ public boolean removeParameter(final String name) {
+ //this is to avoid the case in which the key has a null value
+ if (this.parameters.containsKey(name)) {
+ this.parameters.remove(name);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Assigns the value to all the parameter with the given names
+ *
+ * @param names array of parameter names
+ * @param value parameter value
+ */
+ public void setParameters(final String[] names, final Object value) {
+ for (final String name : names) {
+ setParameter(name, value);
+ }
+ }
+
+ /**
+ * Is the parameter set?
+ * <p>
+ * Uses {@link #getParameter(String)} (which is overrideable) to
+ * fetch the parameter value, if any.
+ * <p>
+ * Also @see {@link #isParameterSetLocally(String)}
+ *
+ * @param name parameter name
+ * @return true if parameter is defined and non-null
+ */
+ public boolean isParameterSet(final String name) {
+ return getParameter(name) != null;
+ }
+
+ /**
+ * Is the parameter set in this object?
+ * <p>
+ * The parameter value is fetched directly.
+ * <p>
+ * Also @see {@link #isParameterSet(String)}
+ *
+ * @param name parameter name
+ * @return true if parameter is defined and non-null
+ */
+ public boolean isParameterSetLocally(final String name) {
+ return this.parameters.get(name) != null;
+ }
+
+ /**
+ * Removes all parameters from this collection.
+ */
+ public void clear() {
+ this.parameters.clear();
+ }
+
+ /**
+ * Creates a copy of these parameters.
+ * This implementation calls {@link #clone()}.
+ *
+ * @return a new set of params holding a copy of the
+ * <i>local</i> parameters in this object.
+ *
+ * @throws UnsupportedOperationException if the clone() fails
+ */
+ public HttpParams copy() {
+ try {
+ return (HttpParams) clone();
+ } catch (final CloneNotSupportedException ex) {
+ throw new UnsupportedOperationException("Cloning not supported");
+ }
+ }
+
+ /**
+ * Clones the instance.
+ * Uses {@link #copyParams(HttpParams)} to copy the parameters.
+ */
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ final BasicHttpParams clone = (BasicHttpParams) super.clone();
+ copyParams(clone);
+ return clone;
+ }
+
+ /**
+ * Copies the locally defined parameters to the argument parameters.
+ * This method is called from {@link #clone()}.
+ *
+ * @param target the parameters to which to copy
+ * @since 4.2
+ */
+ public void copyParams(final HttpParams target) {
+ for (final Map.Entry<String, Object> me : this.parameters.entrySet()) {
+ target.setParameter(me.getKey(), me.getValue());
+ }
+ }
+
+ /**
+ * Returns the current set of names.
+ *
+ * Changes to the underlying HttpParams are not reflected
+ * in the set - it is a snapshot.
+ *
+ * @return the names, as a Set<String>
+ * @since 4.2
+ */
+ @Override
+ public Set<String> getNames() {
+ return new HashSet<String>(this.parameters.keySet());
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/CoreConnectionPNames.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/CoreConnectionPNames.java
new file mode 100644
index 0000000000..dc9c63429b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/CoreConnectionPNames.java
@@ -0,0 +1,170 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.params;
+
+/**
+ * Defines parameter names for connections in HttpCore.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use configuration classes provided 'ch.boye.httpclientandroidlib.config'
+ * and 'ch.boye.httpclientandroidlib.client.config'
+ */
+@Deprecated
+public interface CoreConnectionPNames {
+
+ /**
+ * Defines the socket timeout (<code>SO_TIMEOUT</code>) in milliseconds,
+ * which is the timeout for waiting for data or, put differently,
+ * a maximum period inactivity between two consecutive data packets).
+ * A timeout value of zero is interpreted as an infinite timeout.
+ * <p>
+ * This parameter expects a value of type {@link Integer}.
+ * </p>
+ * @see java.net.SocketOptions#SO_TIMEOUT
+ */
+ public static final String SO_TIMEOUT = "http.socket.timeout";
+
+ /**
+ * Determines whether Nagle's algorithm is to be used. The Nagle's algorithm
+ * tries to conserve bandwidth by minimizing the number of segments that are
+ * sent. When applications wish to decrease network latency and increase
+ * performance, they can disable Nagle's algorithm (that is enable
+ * TCP_NODELAY). Data will be sent earlier, at the cost of an increase
+ * in bandwidth consumption.
+ * <p>
+ * This parameter expects a value of type {@link Boolean}.
+ * </p>
+ * @see java.net.SocketOptions#TCP_NODELAY
+ */
+ public static final String TCP_NODELAY = "http.tcp.nodelay";
+
+ /**
+ * Determines the size of the internal socket buffer used to buffer data
+ * while receiving / transmitting HTTP messages.
+ * <p>
+ * This parameter expects a value of type {@link Integer}.
+ * </p>
+ */
+ public static final String SOCKET_BUFFER_SIZE = "http.socket.buffer-size";
+
+ /**
+ * Sets SO_LINGER with the specified linger time in seconds. The maximum
+ * timeout value is platform specific. Value <code>0</code> implies that
+ * the option is disabled. Value <code>-1</code> implies that the JRE
+ * default is used. The setting only affects the socket close operation.
+ * <p>
+ * This parameter expects a value of type {@link Integer}.
+ * </p>
+ * @see java.net.SocketOptions#SO_LINGER
+ */
+ public static final String SO_LINGER = "http.socket.linger";
+
+ /**
+ * Defines whether the socket can be bound even though a previous connection is
+ * still in a timeout state.
+ * <p>
+ * This parameter expects a value of type {@link Boolean}.
+ * </p>
+ * @see java.net.Socket#setReuseAddress(boolean)
+ *
+ * @since 4.1
+ */
+ public static final String SO_REUSEADDR = "http.socket.reuseaddr";
+
+ /**
+ * Determines the timeout in milliseconds until a connection is established.
+ * A timeout value of zero is interpreted as an infinite timeout.
+ * <p>
+ * Please note this parameter can only be applied to connections that
+ * are bound to a particular local address.
+ * <p>
+ * This parameter expects a value of type {@link Integer}.
+ * </p>
+ */
+ public static final String CONNECTION_TIMEOUT = "http.connection.timeout";
+
+ /**
+ * Determines whether stale connection check is to be used. The stale
+ * connection check can cause up to 30 millisecond overhead per request and
+ * should be used only when appropriate. For performance critical
+ * operations this check should be disabled.
+ * <p>
+ * This parameter expects a value of type {@link Boolean}.
+ * </p>
+ */
+ public static final String STALE_CONNECTION_CHECK = "http.connection.stalecheck";
+
+ /**
+ * Determines the maximum line length limit. If set to a positive value,
+ * any HTTP line exceeding this limit will cause an IOException. A negative
+ * or zero value will effectively disable the check.
+ * <p>
+ * This parameter expects a value of type {@link Integer}.
+ * </p>
+ */
+ public static final String MAX_LINE_LENGTH = "http.connection.max-line-length";
+
+ /**
+ * Determines the maximum HTTP header count allowed. If set to a positive
+ * value, the number of HTTP headers received from the data stream exceeding
+ * this limit will cause an IOException. A negative or zero value will
+ * effectively disable the check.
+ * <p>
+ * This parameter expects a value of type {@link Integer}.
+ * </p>
+ */
+ public static final String MAX_HEADER_COUNT = "http.connection.max-header-count";
+
+ /**
+ * Defines the size limit below which data chunks should be buffered in a session I/O buffer
+ * in order to minimize native method invocations on the underlying network socket.
+ * The optimal value of this parameter can be platform specific and defines a trade-off
+ * between performance of memory copy operations and that of native method invocation.
+ * <p>
+ * This parameter expects a value of type {@link Integer}.
+ * </p>
+ *
+ * @since 4.1
+ */
+ public static final String MIN_CHUNK_LIMIT = "http.connection.min-chunk-limit";
+
+
+ /**
+ * Defines whether or not TCP is to send automatically a keepalive probe to the peer
+ * after an interval of inactivity (no data exchanged in either direction) between this
+ * host and the peer. The purpose of this option is to detect if the peer host crashes.
+ * <p>
+ * This parameter expects a value of type {@link Boolean}.
+ * </p>
+ * @see java.net.SocketOptions#SO_KEEPALIVE
+ * @since 4.2
+ */
+ public static final String SO_KEEPALIVE = "http.socket.keepalive";
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/CoreProtocolPNames.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/CoreProtocolPNames.java
new file mode 100644
index 0000000000..68cf63c8bf
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/CoreProtocolPNames.java
@@ -0,0 +1,152 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.params;
+
+/**
+ * Defines parameter names for protocol execution in HttpCore.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use configuration classes provided 'ch.boye.httpclientandroidlib.config'
+ * and 'ch.boye.httpclientandroidlib.client.config'
+ */
+@Deprecated
+public interface CoreProtocolPNames {
+
+ /**
+ * Defines the {@link ch.boye.httpclientandroidlib.ProtocolVersion} used per default.
+ * <p>
+ * This parameter expects a value of type {@link ch.boye.httpclientandroidlib.ProtocolVersion}.
+ * </p>
+ */
+ public static final String PROTOCOL_VERSION = "http.protocol.version";
+
+ /**
+ * Defines the charset to be used for encoding HTTP protocol elements.
+ * <p>
+ * This parameter expects a value of type {@link String}.
+ * </p>
+ */
+ public static final String HTTP_ELEMENT_CHARSET = "http.protocol.element-charset";
+
+ /**
+ * Defines the charset to be used per default for encoding content body.
+ * <p>
+ * This parameter expects a value of type {@link String}.
+ * </p>
+ */
+ public static final String HTTP_CONTENT_CHARSET = "http.protocol.content-charset";
+
+ /**
+ * Defines the content of the <code>User-Agent</code> header.
+ * <p>
+ * This parameter expects a value of type {@link String}.
+ * </p>
+ */
+ public static final String USER_AGENT = "http.useragent";
+
+ /**
+ * Defines the content of the <code>Server</code> header.
+ * <p>
+ * This parameter expects a value of type {@link String}.
+ * </p>
+ */
+ public static final String ORIGIN_SERVER = "http.origin-server";
+
+ /**
+ * Defines whether responses with an invalid <code>Transfer-Encoding</code>
+ * header should be rejected.
+ * <p>
+ * This parameter expects a value of type {@link Boolean}.
+ * </p>
+ */
+ public static final String STRICT_TRANSFER_ENCODING = "http.protocol.strict-transfer-encoding";
+
+ /**
+ * <p>
+ * Activates 'Expect: 100-Continue' handshake for the
+ * entity enclosing methods. The purpose of the 'Expect: 100-Continue'
+ * handshake is to allow a client that is sending a request message with
+ * a request body to determine if the origin server is willing to
+ * accept the request (based on the request headers) before the client
+ * sends the request body.
+ * </p>
+ *
+ * <p>
+ * The use of the 'Expect: 100-continue' handshake can result in
+ * a noticeable performance improvement for entity enclosing requests
+ * (such as POST and PUT) that require the target server's
+ * authentication.
+ * </p>
+ *
+ * <p>
+ * 'Expect: 100-continue' handshake should be used with
+ * caution, as it may cause problems with HTTP servers and
+ * proxies that do not support HTTP/1.1 protocol.
+ * </p>
+ *
+ * This parameter expects a value of type {@link Boolean}.
+ */
+ public static final String USE_EXPECT_CONTINUE = "http.protocol.expect-continue";
+
+ /**
+ * <p>
+ * Defines the maximum period of time in milliseconds the client should spend
+ * waiting for a 100-continue response.
+ * </p>
+ *
+ * This parameter expects a value of type {@link Integer}.
+ */
+ public static final String WAIT_FOR_CONTINUE = "http.protocol.wait-for-continue";
+
+ /**
+ * <p>
+ * Defines the action to perform upon receiving a malformed input. If the input byte sequence
+ * is not legal for this charset then the input is said to be malformed
+ * </p>
+ *
+ * This parameter expects a value of type {@link java.nio.charset.CodingErrorAction}
+ *
+ * @since 4.2
+ */
+ public static final String HTTP_MALFORMED_INPUT_ACTION = "http.malformed.input.action";
+
+ /**
+ * <p>
+ * Defines the action to perform upon receiving an unmappable input. If the input byte sequence
+ * is legal but cannot be mapped to a valid Unicode character then the input is said to be
+ * unmappable
+ * </p>
+ *
+ * This parameter expects a value of type {@link java.nio.charset.CodingErrorAction}
+ *
+ * @since 4.2
+ */
+ public static final String HTTP_UNMAPPABLE_INPUT_ACTION = "http.unmappable.input.action";
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/DefaultedHttpParams.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/DefaultedHttpParams.java
new file mode 100644
index 0000000000..88be440c3a
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/DefaultedHttpParams.java
@@ -0,0 +1,163 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.params;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * {@link HttpParams} implementation that delegates resolution of a parameter
+ * to the given default {@link HttpParams} instance if the parameter is not
+ * present in the local one. The state of the local collection can be mutated,
+ * whereas the default collection is treated as read-only.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use configuration classes provided 'ch.boye.httpclientandroidlib.config'
+ * and 'ch.boye.httpclientandroidlib.client.config'
+ */
+@Deprecated
+public final class DefaultedHttpParams extends AbstractHttpParams {
+
+ private final HttpParams local;
+ private final HttpParams defaults;
+
+ /**
+ * Create the defaulted set of HttpParams.
+ *
+ * @param local the mutable set of HttpParams
+ * @param defaults the default set of HttpParams, not mutated by this class
+ */
+ public DefaultedHttpParams(final HttpParams local, final HttpParams defaults) {
+ super();
+ this.local = Args.notNull(local, "Local HTTP parameters");
+ this.defaults = defaults;
+ }
+
+ /**
+ * Creates a copy of the local collection with the same default
+ */
+ public HttpParams copy() {
+ final HttpParams clone = this.local.copy();
+ return new DefaultedHttpParams(clone, this.defaults);
+ }
+
+ /**
+ * Retrieves the value of the parameter from the local collection and, if the
+ * parameter is not set locally, delegates its resolution to the default
+ * collection.
+ */
+ public Object getParameter(final String name) {
+ Object obj = this.local.getParameter(name);
+ if (obj == null && this.defaults != null) {
+ obj = this.defaults.getParameter(name);
+ }
+ return obj;
+ }
+
+ /**
+ * Attempts to remove the parameter from the local collection. This method
+ * <i>does not</i> modify the default collection.
+ */
+ public boolean removeParameter(final String name) {
+ return this.local.removeParameter(name);
+ }
+
+ /**
+ * Sets the parameter in the local collection. This method <i>does not</i>
+ * modify the default collection.
+ */
+ public HttpParams setParameter(final String name, final Object value) {
+ return this.local.setParameter(name, value);
+ }
+
+ /**
+ *
+ * @return the default HttpParams collection
+ */
+ public HttpParams getDefaults() {
+ return this.defaults;
+ }
+
+ /**
+ * Returns the current set of names
+ * from both the local and default HttpParams instances.
+ *
+ * Changes to the underlying HttpParams intances are not reflected
+ * in the set - it is a snapshot.
+ *
+ * @return the combined set of names, as a Set<String>
+ * @since 4.2
+ * @throws UnsupportedOperationException if either the local or default HttpParams instances do not implement HttpParamsNames
+ */
+ @Override
+ public Set<String> getNames() {
+ final Set<String> combined = new HashSet<String>(getNames(defaults));
+ combined.addAll(getNames(this.local));
+ return combined ;
+ }
+
+ /**
+ * Returns the current set of defaults names.
+ *
+ * Changes to the underlying HttpParams are not reflected
+ * in the set - it is a snapshot.
+ *
+ * @return the names, as a Set<String>
+ * @since 4.2
+ * @throws UnsupportedOperationException if the default HttpParams instance does not implement HttpParamsNames
+ */
+ public Set<String> getDefaultNames() {
+ return new HashSet<String>(getNames(this.defaults));
+ }
+
+ /**
+ * Returns the current set of local names.
+ *
+ * Changes to the underlying HttpParams are not reflected
+ * in the set - it is a snapshot.
+ *
+ * @return the names, as a Set<String>
+ * @since 4.2
+ * @throws UnsupportedOperationException if the local HttpParams instance does not implement HttpParamsNames
+ */
+ public Set<String> getLocalNames() {
+ return new HashSet<String>(getNames(this.local));
+ }
+
+ // Helper method
+ private Set<String> getNames(final HttpParams params) {
+ if (params instanceof HttpParamsNames) {
+ return ((HttpParamsNames) params).getNames();
+ }
+ throw new UnsupportedOperationException("HttpParams instance does not implement HttpParamsNames");
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpAbstractParamBean.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpAbstractParamBean.java
new file mode 100644
index 0000000000..b3010f1cea
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpAbstractParamBean.java
@@ -0,0 +1,48 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.params;
+
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * @since 4.0
+ *
+ * @deprecated (4.3) use configuration classes provided 'ch.boye.httpclientandroidlib.config'
+ * and 'ch.boye.httpclientandroidlib.client.config'
+ */
+@Deprecated
+public abstract class HttpAbstractParamBean {
+
+ protected final HttpParams params;
+
+ public HttpAbstractParamBean (final HttpParams params) {
+ super();
+ this.params = Args.notNull(params, "HTTP parameters");
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpConnectionParamBean.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpConnectionParamBean.java
new file mode 100644
index 0000000000..d6e6cc4718
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpConnectionParamBean.java
@@ -0,0 +1,71 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.params;
+
+/**
+ * This is a Java Bean class that can be used to wrap an instance of
+ * {@link HttpParams} and manipulate HTTP connection parameters using Java Beans
+ * conventions.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use configuration classes provided 'ch.boye.httpclientandroidlib.config'
+ * and 'ch.boye.httpclientandroidlib.client.config'
+ */
+@Deprecated
+public class HttpConnectionParamBean extends HttpAbstractParamBean {
+
+ public HttpConnectionParamBean (final HttpParams params) {
+ super(params);
+ }
+
+ public void setSoTimeout (final int soTimeout) {
+ HttpConnectionParams.setSoTimeout(params, soTimeout);
+ }
+
+ public void setTcpNoDelay (final boolean tcpNoDelay) {
+ HttpConnectionParams.setTcpNoDelay(params, tcpNoDelay);
+ }
+
+ public void setSocketBufferSize (final int socketBufferSize) {
+ HttpConnectionParams.setSocketBufferSize(params, socketBufferSize);
+ }
+
+ public void setLinger (final int linger) {
+ HttpConnectionParams.setLinger(params, linger);
+ }
+
+ public void setConnectionTimeout (final int connectionTimeout) {
+ HttpConnectionParams.setConnectionTimeout(params, connectionTimeout);
+ }
+
+ public void setStaleCheckingEnabled (final boolean staleCheckingEnabled) {
+ HttpConnectionParams.setStaleCheckingEnabled(params, staleCheckingEnabled);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpConnectionParams.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpConnectionParams.java
new file mode 100644
index 0000000000..2efae2c377
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpConnectionParams.java
@@ -0,0 +1,243 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.params;
+
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Utility class for accessing connection parameters in {@link HttpParams}.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use configuration classes provided 'ch.boye.httpclientandroidlib.config'
+ * and 'ch.boye.httpclientandroidlib.client.config'
+ */
+@Deprecated
+public final class HttpConnectionParams implements CoreConnectionPNames {
+
+ private HttpConnectionParams() {
+ super();
+ }
+
+ /**
+ * Obtains value of the {@link CoreConnectionPNames#SO_TIMEOUT} parameter.
+ * If not set, defaults to <code>0</code>.
+ *
+ * @param params HTTP parameters.
+ * @return SO_TIMEOUT.
+ */
+ public static int getSoTimeout(final HttpParams params) {
+ Args.notNull(params, "HTTP parameters");
+ return params.getIntParameter(CoreConnectionPNames.SO_TIMEOUT, 0);
+ }
+
+ /**
+ * Sets value of the {@link CoreConnectionPNames#SO_TIMEOUT} parameter.
+ *
+ * @param params HTTP parameters.
+ * @param timeout SO_TIMEOUT.
+ */
+ public static void setSoTimeout(final HttpParams params, final int timeout) {
+ Args.notNull(params, "HTTP parameters");
+ params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, timeout);
+
+ }
+
+ /**
+ * Obtains value of the {@link CoreConnectionPNames#SO_REUSEADDR} parameter.
+ * If not set, defaults to <code>false</code>.
+ *
+ * @param params HTTP parameters.
+ * @return SO_REUSEADDR.
+ *
+ * @since 4.1
+ */
+ public static boolean getSoReuseaddr(final HttpParams params) {
+ Args.notNull(params, "HTTP parameters");
+ return params.getBooleanParameter(CoreConnectionPNames.SO_REUSEADDR, false);
+ }
+
+ /**
+ * Sets value of the {@link CoreConnectionPNames#SO_REUSEADDR} parameter.
+ *
+ * @param params HTTP parameters.
+ * @param reuseaddr SO_REUSEADDR.
+ *
+ * @since 4.1
+ */
+ public static void setSoReuseaddr(final HttpParams params, final boolean reuseaddr) {
+ Args.notNull(params, "HTTP parameters");
+ params.setBooleanParameter(CoreConnectionPNames.SO_REUSEADDR, reuseaddr);
+ }
+
+ /**
+ * Obtains value of the {@link CoreConnectionPNames#TCP_NODELAY} parameter.
+ * If not set, defaults to <code>true</code>.
+ *
+ * @param params HTTP parameters.
+ * @return Nagle's algorithm flag
+ */
+ public static boolean getTcpNoDelay(final HttpParams params) {
+ Args.notNull(params, "HTTP parameters");
+ return params.getBooleanParameter(CoreConnectionPNames.TCP_NODELAY, true);
+ }
+
+ /**
+ * Sets value of the {@link CoreConnectionPNames#TCP_NODELAY} parameter.
+ *
+ * @param params HTTP parameters.
+ * @param value Nagle's algorithm flag
+ */
+ public static void setTcpNoDelay(final HttpParams params, final boolean value) {
+ Args.notNull(params, "HTTP parameters");
+ params.setBooleanParameter(CoreConnectionPNames.TCP_NODELAY, value);
+ }
+
+ /**
+ * Obtains value of the {@link CoreConnectionPNames#SOCKET_BUFFER_SIZE}
+ * parameter. If not set, defaults to <code>-1</code>.
+ *
+ * @param params HTTP parameters.
+ * @return socket buffer size
+ */
+ public static int getSocketBufferSize(final HttpParams params) {
+ Args.notNull(params, "HTTP parameters");
+ return params.getIntParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE, -1);
+ }
+
+ /**
+ * Sets value of the {@link CoreConnectionPNames#SOCKET_BUFFER_SIZE}
+ * parameter.
+ *
+ * @param params HTTP parameters.
+ * @param size socket buffer size
+ */
+ public static void setSocketBufferSize(final HttpParams params, final int size) {
+ Args.notNull(params, "HTTP parameters");
+ params.setIntParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE, size);
+ }
+
+ /**
+ * Obtains value of the {@link CoreConnectionPNames#SO_LINGER} parameter.
+ * If not set, defaults to <code>-1</code>.
+ *
+ * @param params HTTP parameters.
+ * @return SO_LINGER.
+ */
+ public static int getLinger(final HttpParams params) {
+ Args.notNull(params, "HTTP parameters");
+ return params.getIntParameter(CoreConnectionPNames.SO_LINGER, -1);
+ }
+
+ /**
+ * Sets value of the {@link CoreConnectionPNames#SO_LINGER} parameter.
+ *
+ * @param params HTTP parameters.
+ * @param value SO_LINGER.
+ */
+ public static void setLinger(final HttpParams params, final int value) {
+ Args.notNull(params, "HTTP parameters");
+ params.setIntParameter(CoreConnectionPNames.SO_LINGER, value);
+ }
+
+ /**
+ * Obtains value of the {@link CoreConnectionPNames#CONNECTION_TIMEOUT}
+ * parameter. If not set, defaults to <code>0</code>.
+ *
+ * @param params HTTP parameters.
+ * @return connect timeout.
+ */
+ public static int getConnectionTimeout(final HttpParams params) {
+ Args.notNull(params, "HTTP parameters");
+ return params.getIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 0);
+ }
+
+ /**
+ * Sets value of the {@link CoreConnectionPNames#CONNECTION_TIMEOUT}
+ * parameter.
+ *
+ * @param params HTTP parameters.
+ * @param timeout connect timeout.
+ */
+ public static void setConnectionTimeout(final HttpParams params, final int timeout) {
+ Args.notNull(params, "HTTP parameters");
+ params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, timeout);
+ }
+
+ /**
+ * Obtains value of the {@link CoreConnectionPNames#STALE_CONNECTION_CHECK}
+ * parameter. If not set, defaults to <code>true</code>.
+ *
+ * @param params HTTP parameters.
+ * @return stale connection check flag.
+ */
+ public static boolean isStaleCheckingEnabled(final HttpParams params) {
+ Args.notNull(params, "HTTP parameters");
+ return params.getBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, true);
+ }
+
+ /**
+ * Sets value of the {@link CoreConnectionPNames#STALE_CONNECTION_CHECK}
+ * parameter.
+ *
+ * @param params HTTP parameters.
+ * @param value stale connection check flag.
+ */
+ public static void setStaleCheckingEnabled(final HttpParams params, final boolean value) {
+ Args.notNull(params, "HTTP parameters");
+ params.setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, value);
+ }
+
+ /**
+ * Obtains value of the {@link CoreConnectionPNames#SO_KEEPALIVE} parameter.
+ * If not set, defaults to <code>false</code>.
+ *
+ * @param params HTTP parameters.
+ * @return SO_KEEPALIVE.
+ *
+ * @since 4.2
+ */
+ public static boolean getSoKeepalive(final HttpParams params) {
+ Args.notNull(params, "HTTP parameters");
+ return params.getBooleanParameter(CoreConnectionPNames.SO_KEEPALIVE, false);
+ }
+
+ /**
+ * Sets value of the {@link CoreConnectionPNames#SO_KEEPALIVE} parameter.
+ *
+ * @param params HTTP parameters.
+ * @param enableKeepalive SO_KEEPALIVE.
+ *
+ * @since 4.2
+ */
+ public static void setSoKeepalive(final HttpParams params, final boolean enableKeepalive) {
+ Args.notNull(params, "HTTP parameters");
+ params.setBooleanParameter(CoreConnectionPNames.SO_KEEPALIVE, enableKeepalive);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpParamConfig.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpParamConfig.java
new file mode 100644
index 0000000000..91a62a657d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpParamConfig.java
@@ -0,0 +1,78 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.params;
+
+import java.nio.charset.Charset;
+import java.nio.charset.CodingErrorAction;
+
+import ch.boye.httpclientandroidlib.config.ConnectionConfig;
+import ch.boye.httpclientandroidlib.config.MessageConstraints;
+import ch.boye.httpclientandroidlib.config.SocketConfig;
+
+/**
+ * @deprecated (4.3) provided for compatibility with {@link HttpParams}. Do not use.
+ *
+ * @since 4.3
+ */
+@Deprecated
+public final class HttpParamConfig {
+
+ private HttpParamConfig() {
+ }
+
+ public static SocketConfig getSocketConfig(final HttpParams params) {
+ return SocketConfig.custom()
+ .setSoTimeout(params.getIntParameter(CoreConnectionPNames.SO_TIMEOUT, 0))
+ .setSoReuseAddress(params.getBooleanParameter(CoreConnectionPNames.SO_REUSEADDR, false))
+ .setSoKeepAlive(params.getBooleanParameter(CoreConnectionPNames.SO_KEEPALIVE, false))
+ .setSoLinger(params.getIntParameter(CoreConnectionPNames.SO_LINGER, -1))
+ .setTcpNoDelay(params.getBooleanParameter(CoreConnectionPNames.TCP_NODELAY, true))
+ .build();
+ }
+
+ public static MessageConstraints getMessageConstraints(final HttpParams params) {
+ return MessageConstraints.custom()
+ .setMaxHeaderCount(params.getIntParameter(CoreConnectionPNames.MAX_HEADER_COUNT, -1))
+ .setMaxLineLength(params.getIntParameter(CoreConnectionPNames.MAX_LINE_LENGTH, -1))
+ .build();
+ }
+
+ public static ConnectionConfig getConnectionConfig(final HttpParams params) {
+ final MessageConstraints messageConstraints = getMessageConstraints(params);
+ final String csname = (String) params.getParameter(CoreProtocolPNames.HTTP_ELEMENT_CHARSET);
+ return ConnectionConfig.custom()
+ .setCharset(csname != null ? Charset.forName(csname) : null)
+ .setMalformedInputAction((CodingErrorAction)
+ params.getParameter(CoreProtocolPNames.HTTP_MALFORMED_INPUT_ACTION))
+ .setMalformedInputAction((CodingErrorAction)
+ params.getParameter(CoreProtocolPNames.HTTP_UNMAPPABLE_INPUT_ACTION))
+ .setMessageConstraints(messageConstraints)
+ .build();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpParams.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpParams.java
new file mode 100644
index 0000000000..7eb780b37d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpParams.java
@@ -0,0 +1,195 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.params;
+
+/**
+ * HttpParams interface represents a collection of immutable values that define
+ * a runtime behavior of a component. HTTP parameters should be simple objects:
+ * integers, doubles, strings, collections and objects that remain immutable
+ * at runtime. HttpParams is expected to be used in 'write once - read many' mode.
+ * Once initialized, HTTP parameters are not expected to mutate in
+ * the course of HTTP message processing.
+ * <p>
+ * The purpose of this interface is to define a behavior of other components.
+ * Usually each complex component has its own HTTP parameter collection.
+ * <p>
+ * Instances of this interface can be linked together to form a hierarchy.
+ * In the simplest form one set of parameters can use content of another one
+ * to obtain default values of parameters not present in the local set.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use configuration classes provided 'ch.boye.httpclientandroidlib.config'
+ * and 'ch.boye.httpclientandroidlib.client.config'
+ */
+@Deprecated
+public interface HttpParams {
+
+ /**
+ * Obtains the value of the given parameter.
+ *
+ * @param name the parent name.
+ *
+ * @return an object that represents the value of the parameter,
+ * <code>null</code> if the parameter is not set or if it
+ * is explicitly set to <code>null</code>
+ *
+ * @see #setParameter(String, Object)
+ */
+ Object getParameter(String name);
+
+ /**
+ * Assigns the value to the parameter with the given name.
+ *
+ * @param name parameter name
+ * @param value parameter value
+ */
+ HttpParams setParameter(String name, Object value);
+
+ /**
+ * Creates a copy of these parameters.
+ *
+ * @return a new set of parameters holding the same values as this one
+ */
+ HttpParams copy();
+
+ /**
+ * Removes the parameter with the specified name.
+ *
+ * @param name parameter name
+ *
+ * @return true if the parameter existed and has been removed, false else.
+ */
+ boolean removeParameter(String name);
+
+ /**
+ * Returns a {@link Long} parameter value with the given name.
+ * If the parameter is not explicitly set, the default value is returned.
+ *
+ * @param name the parent name.
+ * @param defaultValue the default value.
+ *
+ * @return a {@link Long} that represents the value of the parameter.
+ *
+ * @see #setLongParameter(String, long)
+ */
+ long getLongParameter(String name, long defaultValue);
+
+ /**
+ * Assigns a {@link Long} to the parameter with the given name
+ *
+ * @param name parameter name
+ * @param value parameter value
+ */
+ HttpParams setLongParameter(String name, long value);
+
+ /**
+ * Returns an {@link Integer} parameter value with the given name.
+ * If the parameter is not explicitly set, the default value is returned.
+ *
+ * @param name the parent name.
+ * @param defaultValue the default value.
+ *
+ * @return a {@link Integer} that represents the value of the parameter.
+ *
+ * @see #setIntParameter(String, int)
+ */
+ int getIntParameter(String name, int defaultValue);
+
+ /**
+ * Assigns an {@link Integer} to the parameter with the given name
+ *
+ * @param name parameter name
+ * @param value parameter value
+ */
+ HttpParams setIntParameter(String name, int value);
+
+ /**
+ * Returns a {@link Double} parameter value with the given name.
+ * If the parameter is not explicitly set, the default value is returned.
+ *
+ * @param name the parent name.
+ * @param defaultValue the default value.
+ *
+ * @return a {@link Double} that represents the value of the parameter.
+ *
+ * @see #setDoubleParameter(String, double)
+ */
+ double getDoubleParameter(String name, double defaultValue);
+
+ /**
+ * Assigns a {@link Double} to the parameter with the given name
+ *
+ * @param name parameter name
+ * @param value parameter value
+ */
+ HttpParams setDoubleParameter(String name, double value);
+
+ /**
+ * Returns a {@link Boolean} parameter value with the given name.
+ * If the parameter is not explicitly set, the default value is returned.
+ *
+ * @param name the parent name.
+ * @param defaultValue the default value.
+ *
+ * @return a {@link Boolean} that represents the value of the parameter.
+ *
+ * @see #setBooleanParameter(String, boolean)
+ */
+ boolean getBooleanParameter(String name, boolean defaultValue);
+
+ /**
+ * Assigns a {@link Boolean} to the parameter with the given name
+ *
+ * @param name parameter name
+ * @param value parameter value
+ */
+ HttpParams setBooleanParameter(String name, boolean value);
+
+ /**
+ * Checks if a boolean parameter is set to <code>true</code>.
+ *
+ * @param name parameter name
+ *
+ * @return <tt>true</tt> if the parameter is set to value <tt>true</tt>,
+ * <tt>false</tt> if it is not set or set to <code>false</code>
+ */
+ boolean isParameterTrue(String name);
+
+ /**
+ * Checks if a boolean parameter is not set or <code>false</code>.
+ *
+ * @param name parameter name
+ *
+ * @return <tt>true</tt> if the parameter is either not set or
+ * set to value <tt>false</tt>,
+ * <tt>false</tt> if it is set to <code>true</code>
+ */
+ boolean isParameterFalse(String name);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpParamsNames.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpParamsNames.java
new file mode 100644
index 0000000000..0c4a34001b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpParamsNames.java
@@ -0,0 +1,57 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.params;
+
+import java.util.Set;
+
+/**
+ * Gives access to the full set of parameter names.
+ *
+ * @see HttpParams
+ *
+ * @since 4.2
+ *
+ * @deprecated (4.3) use configuration classes provided 'ch.boye.httpclientandroidlib.config'
+ * and 'ch.boye.httpclientandroidlib.client.config'
+ */
+@Deprecated
+public interface HttpParamsNames {
+
+ /**
+ * Returns the current set of names;
+ * in the case of stacked parameters, returns the names
+ * from all the participating HttpParams instances.
+ *
+ * Changes to the underlying HttpParams are not reflected
+ * in the set - it is a snapshot.
+ *
+ * @return the names, as a Set<String>
+ */
+ Set<String> getNames();
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpProtocolParamBean.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpProtocolParamBean.java
new file mode 100644
index 0000000000..368bffd5e0
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpProtocolParamBean.java
@@ -0,0 +1,69 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.params;
+
+import ch.boye.httpclientandroidlib.HttpVersion;
+
+/**
+ * This is a Java Bean class that can be used to wrap an instance of
+ * {@link HttpParams} and manipulate HTTP protocol parameters using Java Beans
+ * conventions.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use configuration classes provided 'ch.boye.httpclientandroidlib.config'
+ * and 'ch.boye.httpclientandroidlib.client.config'
+ */
+@Deprecated
+public class HttpProtocolParamBean extends HttpAbstractParamBean {
+
+ public HttpProtocolParamBean (final HttpParams params) {
+ super(params);
+ }
+
+ public void setHttpElementCharset (final String httpElementCharset) {
+ HttpProtocolParams.setHttpElementCharset(params, httpElementCharset);
+ }
+
+ public void setContentCharset (final String contentCharset) {
+ HttpProtocolParams.setContentCharset(params, contentCharset);
+ }
+
+ public void setVersion (final HttpVersion version) {
+ HttpProtocolParams.setVersion(params, version);
+ }
+
+ public void setUserAgent (final String userAgent) {
+ HttpProtocolParams.setUserAgent(params, userAgent);
+ }
+
+ public void setUseExpectContinue (final boolean useExpectContinue) {
+ HttpProtocolParams.setUseExpectContinue(params, useExpectContinue);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpProtocolParams.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpProtocolParams.java
new file mode 100644
index 0000000000..2ef3e53ee1
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/HttpProtocolParams.java
@@ -0,0 +1,240 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.params;
+
+import java.nio.charset.CodingErrorAction;
+
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Utility class for accessing protocol parameters in {@link HttpParams}.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use configuration classes provided 'ch.boye.httpclientandroidlib.config'
+ * and 'ch.boye.httpclientandroidlib.client.config'
+ */
+@Deprecated
+public final class HttpProtocolParams implements CoreProtocolPNames {
+
+ private HttpProtocolParams() {
+ super();
+ }
+
+ /**
+ * Obtains value of the {@link CoreProtocolPNames#HTTP_ELEMENT_CHARSET} parameter.
+ * If not set, defaults to <code>US-ASCII</code>.
+ *
+ * @param params HTTP parameters.
+ * @return HTTP element charset.
+ */
+ public static String getHttpElementCharset(final HttpParams params) {
+ Args.notNull(params, "HTTP parameters");
+ String charset = (String) params.getParameter
+ (CoreProtocolPNames.HTTP_ELEMENT_CHARSET);
+ if (charset == null) {
+ charset = HTTP.DEF_PROTOCOL_CHARSET.name();
+ }
+ return charset;
+ }
+
+ /**
+ * Sets value of the {@link CoreProtocolPNames#HTTP_ELEMENT_CHARSET} parameter.
+ *
+ * @param params HTTP parameters.
+ * @param charset HTTP element charset.
+ */
+ public static void setHttpElementCharset(final HttpParams params, final String charset) {
+ Args.notNull(params, "HTTP parameters");
+ params.setParameter(CoreProtocolPNames.HTTP_ELEMENT_CHARSET, charset);
+ }
+
+ /**
+ * Obtains value of the {@link CoreProtocolPNames#HTTP_CONTENT_CHARSET} parameter.
+ * If not set, defaults to <code>ISO-8859-1</code>.
+ *
+ * @param params HTTP parameters.
+ * @return HTTP content charset.
+ */
+ public static String getContentCharset(final HttpParams params) {
+ Args.notNull(params, "HTTP parameters");
+ String charset = (String) params.getParameter
+ (CoreProtocolPNames.HTTP_CONTENT_CHARSET);
+ if (charset == null) {
+ charset = HTTP.DEF_CONTENT_CHARSET.name();
+ }
+ return charset;
+ }
+
+ /**
+ * Sets value of the {@link CoreProtocolPNames#HTTP_CONTENT_CHARSET} parameter.
+ *
+ * @param params HTTP parameters.
+ * @param charset HTTP content charset.
+ */
+ public static void setContentCharset(final HttpParams params, final String charset) {
+ Args.notNull(params, "HTTP parameters");
+ params.setParameter(CoreProtocolPNames.HTTP_CONTENT_CHARSET, charset);
+ }
+
+ /**
+ * Obtains value of the {@link CoreProtocolPNames#PROTOCOL_VERSION} parameter.
+ * If not set, defaults to {@link HttpVersion#HTTP_1_1}.
+ *
+ * @param params HTTP parameters.
+ * @return HTTP protocol version.
+ */
+ public static ProtocolVersion getVersion(final HttpParams params) {
+ Args.notNull(params, "HTTP parameters");
+ final Object param = params.getParameter
+ (CoreProtocolPNames.PROTOCOL_VERSION);
+ if (param == null) {
+ return HttpVersion.HTTP_1_1;
+ }
+ return (ProtocolVersion)param;
+ }
+
+ /**
+ * Sets value of the {@link CoreProtocolPNames#PROTOCOL_VERSION} parameter.
+ *
+ * @param params HTTP parameters.
+ * @param version HTTP protocol version.
+ */
+ public static void setVersion(final HttpParams params, final ProtocolVersion version) {
+ Args.notNull(params, "HTTP parameters");
+ params.setParameter(CoreProtocolPNames.PROTOCOL_VERSION, version);
+ }
+
+ /**
+ * Obtains value of the {@link CoreProtocolPNames#USER_AGENT} parameter.
+ * If not set, returns <code>null</code>.
+ *
+ * @param params HTTP parameters.
+ * @return User agent string.
+ */
+ public static String getUserAgent(final HttpParams params) {
+ Args.notNull(params, "HTTP parameters");
+ return (String) params.getParameter(CoreProtocolPNames.USER_AGENT);
+ }
+
+ /**
+ * Sets value of the {@link CoreProtocolPNames#USER_AGENT} parameter.
+ *
+ * @param params HTTP parameters.
+ * @param useragent User agent string.
+ */
+ public static void setUserAgent(final HttpParams params, final String useragent) {
+ Args.notNull(params, "HTTP parameters");
+ params.setParameter(CoreProtocolPNames.USER_AGENT, useragent);
+ }
+
+ /**
+ * Obtains value of the {@link CoreProtocolPNames#USE_EXPECT_CONTINUE} parameter.
+ * If not set, returns <code>false</code>.
+ *
+ * @param params HTTP parameters.
+ * @return User agent string.
+ */
+ public static boolean useExpectContinue(final HttpParams params) {
+ Args.notNull(params, "HTTP parameters");
+ return params.getBooleanParameter(CoreProtocolPNames.USE_EXPECT_CONTINUE, false);
+ }
+
+ /**
+ * Sets value of the {@link CoreProtocolPNames#USE_EXPECT_CONTINUE} parameter.
+ *
+ * @param params HTTP parameters.
+ * @param b expect-continue flag.
+ */
+ public static void setUseExpectContinue(final HttpParams params, final boolean b) {
+ Args.notNull(params, "HTTP parameters");
+ params.setBooleanParameter(CoreProtocolPNames.USE_EXPECT_CONTINUE, b);
+ }
+
+ /**
+ * Obtains value of the {@link CoreProtocolPNames#HTTP_MALFORMED_INPUT_ACTION} parameter.
+ * @param params HTTP parameters.
+ * @return Action to perform upon receiving a malformed input
+ *
+ * @since 4.2
+ */
+ public static CodingErrorAction getMalformedInputAction(final HttpParams params) {
+ Args.notNull(params, "HTTP parameters");
+ final Object param = params.getParameter(CoreProtocolPNames.HTTP_MALFORMED_INPUT_ACTION);
+ if (param == null) {
+ // the default CodingErrorAction
+ return CodingErrorAction.REPORT;
+ }
+ return (CodingErrorAction) param;
+ }
+
+ /**
+ * Sets value of the {@link CoreProtocolPNames#HTTP_MALFORMED_INPUT_ACTION} parameter.
+ * @param params HTTP parameters
+ * @param action action to perform on malformed inputs
+ *
+ * @since 4.2
+ */
+ public static void setMalformedInputAction(final HttpParams params, final CodingErrorAction action) {
+ Args.notNull(params, "HTTP parameters");
+ params.setParameter(CoreProtocolPNames.HTTP_MALFORMED_INPUT_ACTION, action);
+ }
+
+ /**
+ * Obtains the value of the {@link CoreProtocolPNames#HTTP_UNMAPPABLE_INPUT_ACTION} parameter.
+ * @param params HTTP parameters
+ * @return Action to perform upon receiving a unmapped input
+ *
+ * @since 4.2
+ */
+ public static CodingErrorAction getUnmappableInputAction(final HttpParams params) {
+ Args.notNull(params, "HTTP parameters");
+ final Object param = params.getParameter(CoreProtocolPNames.HTTP_UNMAPPABLE_INPUT_ACTION);
+ if (param == null) {
+ // the default CodingErrorAction
+ return CodingErrorAction.REPORT;
+ }
+ return (CodingErrorAction) param;
+ }
+
+ /**
+ * Sets the value of the {@link CoreProtocolPNames#HTTP_UNMAPPABLE_INPUT_ACTION} parameter.
+ * @param params HTTP parameters
+ * @param action action to perform on un mappable inputs
+ *
+ * @since 4.2
+ */
+ public static void setUnmappableInputAction(final HttpParams params, final CodingErrorAction action) {
+ Args.notNull(params, "HTTP parameters");
+ params.setParameter(CoreProtocolPNames.HTTP_UNMAPPABLE_INPUT_ACTION, action);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/SyncBasicHttpParams.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/SyncBasicHttpParams.java
new file mode 100644
index 0000000000..30393a0748
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/SyncBasicHttpParams.java
@@ -0,0 +1,89 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.params;
+
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+
+/**
+ * Thread-safe extension of the {@link BasicHttpParams}.
+ *
+ * @since 4.1
+ *
+ * @deprecated (4.3) use configuration classes provided 'ch.boye.httpclientandroidlib.config'
+ * and 'ch.boye.httpclientandroidlib.client.config'
+ */
+@ThreadSafe
+@Deprecated
+public class SyncBasicHttpParams extends BasicHttpParams {
+
+ private static final long serialVersionUID = 5387834869062660642L;
+
+ public SyncBasicHttpParams() {
+ super();
+ }
+
+ @Override
+ public synchronized boolean removeParameter(final String name) {
+ return super.removeParameter(name);
+ }
+
+ @Override
+ public synchronized HttpParams setParameter(final String name, final Object value) {
+ return super.setParameter(name, value);
+ }
+
+ @Override
+ public synchronized Object getParameter(final String name) {
+ return super.getParameter(name);
+ }
+
+ @Override
+ public synchronized boolean isParameterSet(final String name) {
+ return super.isParameterSet(name);
+ }
+
+ @Override
+ public synchronized boolean isParameterSetLocally(final String name) {
+ return super.isParameterSetLocally(name);
+ }
+
+ @Override
+ public synchronized void setParameters(final String[] names, final Object value) {
+ super.setParameters(names, value);
+ }
+
+ @Override
+ public synchronized void clear() {
+ super.clear();
+ }
+
+ @Override
+ public synchronized Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/package-info.java
new file mode 100644
index 0000000000..0589b0ff9d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/params/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Deprecated.
+ * @deprecated (4.3).
+ */
+package ch.boye.httpclientandroidlib.params;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/AbstractConnPool.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/AbstractConnPool.java
new file mode 100644
index 0000000000..35aeb656f8
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/AbstractConnPool.java
@@ -0,0 +1,533 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.pool;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.concurrent.FutureCallback;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+
+/**
+ * Abstract synchronous (blocking) pool of connections.
+ * <p/>
+ * Please note that this class does not maintain its own pool of execution {@link Thread}s.
+ * Therefore, one <b>must</b> call {@link Future#get()} or {@link Future#get(long, TimeUnit)}
+ * method on the {@link Future} object returned by the
+ * {@link #lease(Object, Object, FutureCallback)} method in order for the lease operation
+ * to complete.
+ *
+ * @param <T> the route type that represents the opposite endpoint of a pooled
+ * connection.
+ * @param <C> the connection type.
+ * @param <E> the type of the pool entry containing a pooled connection.
+ * @since 4.2
+ */
+@ThreadSafe
+public abstract class AbstractConnPool<T, C, E extends PoolEntry<T, C>>
+ implements ConnPool<T, E>, ConnPoolControl<T> {
+
+ private final Lock lock;
+ private final ConnFactory<T, C> connFactory;
+ private final Map<T, RouteSpecificPool<T, C, E>> routeToPool;
+ private final Set<E> leased;
+ private final LinkedList<E> available;
+ private final LinkedList<PoolEntryFuture<E>> pending;
+ private final Map<T, Integer> maxPerRoute;
+
+ private volatile boolean isShutDown;
+ private volatile int defaultMaxPerRoute;
+ private volatile int maxTotal;
+
+ public AbstractConnPool(
+ final ConnFactory<T, C> connFactory,
+ final int defaultMaxPerRoute,
+ final int maxTotal) {
+ super();
+ this.connFactory = Args.notNull(connFactory, "Connection factory");
+ this.defaultMaxPerRoute = Args.notNegative(defaultMaxPerRoute, "Max per route value");
+ this.maxTotal = Args.notNegative(maxTotal, "Max total value");
+ this.lock = new ReentrantLock();
+ this.routeToPool = new HashMap<T, RouteSpecificPool<T, C, E>>();
+ this.leased = new HashSet<E>();
+ this.available = new LinkedList<E>();
+ this.pending = new LinkedList<PoolEntryFuture<E>>();
+ this.maxPerRoute = new HashMap<T, Integer>();
+ }
+
+ /**
+ * Creates a new entry for the given connection with the given route.
+ */
+ protected abstract E createEntry(T route, C conn);
+
+ /**
+ * @since 4.3
+ */
+ protected void onLease(final E entry) {
+ }
+
+ /**
+ * @since 4.3
+ */
+ protected void onRelease(final E entry) {
+ }
+
+ public boolean isShutdown() {
+ return this.isShutDown;
+ }
+
+ /**
+ * Shuts down the pool.
+ */
+ public void shutdown() throws IOException {
+ if (this.isShutDown) {
+ return ;
+ }
+ this.isShutDown = true;
+ this.lock.lock();
+ try {
+ for (final E entry: this.available) {
+ entry.close();
+ }
+ for (final E entry: this.leased) {
+ entry.close();
+ }
+ for (final RouteSpecificPool<T, C, E> pool: this.routeToPool.values()) {
+ pool.shutdown();
+ }
+ this.routeToPool.clear();
+ this.leased.clear();
+ this.available.clear();
+ } finally {
+ this.lock.unlock();
+ }
+ }
+
+ private RouteSpecificPool<T, C, E> getPool(final T route) {
+ RouteSpecificPool<T, C, E> pool = this.routeToPool.get(route);
+ if (pool == null) {
+ pool = new RouteSpecificPool<T, C, E>(route) {
+
+ @Override
+ protected E createEntry(final C conn) {
+ return AbstractConnPool.this.createEntry(route, conn);
+ }
+
+ };
+ this.routeToPool.put(route, pool);
+ }
+ return pool;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p/>
+ * Please note that this class does not maintain its own pool of execution
+ * {@link Thread}s. Therefore, one <b>must</b> call {@link Future#get()}
+ * or {@link Future#get(long, TimeUnit)} method on the {@link Future}
+ * returned by this method in order for the lease operation to complete.
+ */
+ public Future<E> lease(final T route, final Object state, final FutureCallback<E> callback) {
+ Args.notNull(route, "Route");
+ Asserts.check(!this.isShutDown, "Connection pool shut down");
+ return new PoolEntryFuture<E>(this.lock, callback) {
+
+ @Override
+ public E getPoolEntry(
+ final long timeout,
+ final TimeUnit tunit)
+ throws InterruptedException, TimeoutException, IOException {
+ final E entry = getPoolEntryBlocking(route, state, timeout, tunit, this);
+ onLease(entry);
+ return entry;
+ }
+
+ };
+ }
+
+ /**
+ * Attempts to lease a connection for the given route and with the given
+ * state from the pool.
+ * <p/>
+ * Please note that this class does not maintain its own pool of execution
+ * {@link Thread}s. Therefore, one <b>must</b> call {@link Future#get()}
+ * or {@link Future#get(long, TimeUnit)} method on the {@link Future}
+ * returned by this method in order for the lease operation to complete.
+ *
+ * @param route route of the connection.
+ * @param state arbitrary object that represents a particular state
+ * (usually a security principal or a unique token identifying
+ * the user whose credentials have been used while establishing the connection).
+ * May be <code>null</code>.
+ * @return future for a leased pool entry.
+ */
+ public Future<E> lease(final T route, final Object state) {
+ return lease(route, state, null);
+ }
+
+ private E getPoolEntryBlocking(
+ final T route, final Object state,
+ final long timeout, final TimeUnit tunit,
+ final PoolEntryFuture<E> future)
+ throws IOException, InterruptedException, TimeoutException {
+
+ Date deadline = null;
+ if (timeout > 0) {
+ deadline = new Date
+ (System.currentTimeMillis() + tunit.toMillis(timeout));
+ }
+
+ this.lock.lock();
+ try {
+ final RouteSpecificPool<T, C, E> pool = getPool(route);
+ E entry = null;
+ while (entry == null) {
+ Asserts.check(!this.isShutDown, "Connection pool shut down");
+ for (;;) {
+ entry = pool.getFree(state);
+ if (entry == null) {
+ break;
+ }
+ if (entry.isClosed() || entry.isExpired(System.currentTimeMillis())) {
+ entry.close();
+ this.available.remove(entry);
+ pool.free(entry, false);
+ } else {
+ break;
+ }
+ }
+ if (entry != null) {
+ this.available.remove(entry);
+ this.leased.add(entry);
+ return entry;
+ }
+
+ // New connection is needed
+ final int maxPerRoute = getMax(route);
+ // Shrink the pool prior to allocating a new connection
+ final int excess = Math.max(0, pool.getAllocatedCount() + 1 - maxPerRoute);
+ if (excess > 0) {
+ for (int i = 0; i < excess; i++) {
+ final E lastUsed = pool.getLastUsed();
+ if (lastUsed == null) {
+ break;
+ }
+ lastUsed.close();
+ this.available.remove(lastUsed);
+ pool.remove(lastUsed);
+ }
+ }
+
+ if (pool.getAllocatedCount() < maxPerRoute) {
+ final int totalUsed = this.leased.size();
+ final int freeCapacity = Math.max(this.maxTotal - totalUsed, 0);
+ if (freeCapacity > 0) {
+ final int totalAvailable = this.available.size();
+ if (totalAvailable > freeCapacity - 1) {
+ if (!this.available.isEmpty()) {
+ final E lastUsed = this.available.removeLast();
+ lastUsed.close();
+ final RouteSpecificPool<T, C, E> otherpool = getPool(lastUsed.getRoute());
+ otherpool.remove(lastUsed);
+ }
+ }
+ final C conn = this.connFactory.create(route);
+ entry = pool.add(conn);
+ this.leased.add(entry);
+ return entry;
+ }
+ }
+
+ boolean success = false;
+ try {
+ pool.queue(future);
+ this.pending.add(future);
+ success = future.await(deadline);
+ } finally {
+ // In case of 'success', we were woken up by the
+ // connection pool and should now have a connection
+ // waiting for us, or else we're shutting down.
+ // Just continue in the loop, both cases are checked.
+ pool.unqueue(future);
+ this.pending.remove(future);
+ }
+ // check for spurious wakeup vs. timeout
+ if (!success && (deadline != null) &&
+ (deadline.getTime() <= System.currentTimeMillis())) {
+ break;
+ }
+ }
+ throw new TimeoutException("Timeout waiting for connection");
+ } finally {
+ this.lock.unlock();
+ }
+ }
+
+ public void release(final E entry, final boolean reusable) {
+ this.lock.lock();
+ try {
+ if (this.leased.remove(entry)) {
+ final RouteSpecificPool<T, C, E> pool = getPool(entry.getRoute());
+ pool.free(entry, reusable);
+ if (reusable && !this.isShutDown) {
+ this.available.addFirst(entry);
+ onRelease(entry);
+ } else {
+ entry.close();
+ }
+ PoolEntryFuture<E> future = pool.nextPending();
+ if (future != null) {
+ this.pending.remove(future);
+ } else {
+ future = this.pending.poll();
+ }
+ if (future != null) {
+ future.wakeup();
+ }
+ }
+ } finally {
+ this.lock.unlock();
+ }
+ }
+
+ private int getMax(final T route) {
+ final Integer v = this.maxPerRoute.get(route);
+ if (v != null) {
+ return v.intValue();
+ } else {
+ return this.defaultMaxPerRoute;
+ }
+ }
+
+ public void setMaxTotal(final int max) {
+ Args.notNegative(max, "Max value");
+ this.lock.lock();
+ try {
+ this.maxTotal = max;
+ } finally {
+ this.lock.unlock();
+ }
+ }
+
+ public int getMaxTotal() {
+ this.lock.lock();
+ try {
+ return this.maxTotal;
+ } finally {
+ this.lock.unlock();
+ }
+ }
+
+ public void setDefaultMaxPerRoute(final int max) {
+ Args.notNegative(max, "Max per route value");
+ this.lock.lock();
+ try {
+ this.defaultMaxPerRoute = max;
+ } finally {
+ this.lock.unlock();
+ }
+ }
+
+ public int getDefaultMaxPerRoute() {
+ this.lock.lock();
+ try {
+ return this.defaultMaxPerRoute;
+ } finally {
+ this.lock.unlock();
+ }
+ }
+
+ public void setMaxPerRoute(final T route, final int max) {
+ Args.notNull(route, "Route");
+ Args.notNegative(max, "Max per route value");
+ this.lock.lock();
+ try {
+ this.maxPerRoute.put(route, Integer.valueOf(max));
+ } finally {
+ this.lock.unlock();
+ }
+ }
+
+ public int getMaxPerRoute(final T route) {
+ Args.notNull(route, "Route");
+ this.lock.lock();
+ try {
+ return getMax(route);
+ } finally {
+ this.lock.unlock();
+ }
+ }
+
+ public PoolStats getTotalStats() {
+ this.lock.lock();
+ try {
+ return new PoolStats(
+ this.leased.size(),
+ this.pending.size(),
+ this.available.size(),
+ this.maxTotal);
+ } finally {
+ this.lock.unlock();
+ }
+ }
+
+ public PoolStats getStats(final T route) {
+ Args.notNull(route, "Route");
+ this.lock.lock();
+ try {
+ final RouteSpecificPool<T, C, E> pool = getPool(route);
+ return new PoolStats(
+ pool.getLeasedCount(),
+ pool.getPendingCount(),
+ pool.getAvailableCount(),
+ getMax(route));
+ } finally {
+ this.lock.unlock();
+ }
+ }
+
+ /**
+ * Enumerates all available connections.
+ *
+ * @since 4.3
+ */
+ protected void enumAvailable(final PoolEntryCallback<T, C> callback) {
+ this.lock.lock();
+ try {
+ final Iterator<E> it = this.available.iterator();
+ while (it.hasNext()) {
+ final E entry = it.next();
+ callback.process(entry);
+ if (entry.isClosed()) {
+ final RouteSpecificPool<T, C, E> pool = getPool(entry.getRoute());
+ pool.remove(entry);
+ it.remove();
+ }
+ }
+ purgePoolMap();
+ } finally {
+ this.lock.unlock();
+ }
+ }
+
+ /**
+ * Enumerates all leased connections.
+ *
+ * @since 4.3
+ */
+ protected void enumLeased(final PoolEntryCallback<T, C> callback) {
+ this.lock.lock();
+ try {
+ final Iterator<E> it = this.leased.iterator();
+ while (it.hasNext()) {
+ final E entry = it.next();
+ callback.process(entry);
+ }
+ } finally {
+ this.lock.unlock();
+ }
+ }
+
+ private void purgePoolMap() {
+ final Iterator<Map.Entry<T, RouteSpecificPool<T, C, E>>> it = this.routeToPool.entrySet().iterator();
+ while (it.hasNext()) {
+ final Map.Entry<T, RouteSpecificPool<T, C, E>> entry = it.next();
+ final RouteSpecificPool<T, C, E> pool = entry.getValue();
+ if (pool.getPendingCount() + pool.getAllocatedCount() == 0) {
+ it.remove();
+ }
+ }
+ }
+
+ /**
+ * Closes connections that have been idle longer than the given period
+ * of time and evicts them from the pool.
+ *
+ * @param idletime maximum idle time.
+ * @param tunit time unit.
+ */
+ public void closeIdle(final long idletime, final TimeUnit tunit) {
+ Args.notNull(tunit, "Time unit");
+ long time = tunit.toMillis(idletime);
+ if (time < 0) {
+ time = 0;
+ }
+ final long deadline = System.currentTimeMillis() - time;
+ enumAvailable(new PoolEntryCallback<T, C>() {
+
+ public void process(final PoolEntry<T, C> entry) {
+ if (entry.getUpdated() <= deadline) {
+ entry.close();
+ }
+ }
+
+ });
+ }
+
+ /**
+ * Closes expired connections and evicts them from the pool.
+ */
+ public void closeExpired() {
+ final long now = System.currentTimeMillis();
+ enumAvailable(new PoolEntryCallback<T, C>() {
+
+ public void process(final PoolEntry<T, C> entry) {
+ if (entry.isExpired(now)) {
+ entry.close();
+ }
+ }
+
+ });
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder buffer = new StringBuilder();
+ buffer.append("[leased: ");
+ buffer.append(this.leased);
+ buffer.append("][available: ");
+ buffer.append(this.available);
+ buffer.append("][pending: ");
+ buffer.append(this.pending);
+ buffer.append("]");
+ return buffer.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/ConnFactory.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/ConnFactory.java
new file mode 100644
index 0000000000..e2319639cf
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/ConnFactory.java
@@ -0,0 +1,44 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.pool;
+
+import java.io.IOException;
+
+/**
+ * Factory for poolable blocking connections.
+ *
+ * @param <T> the route type that represents the opposite endpoint of a pooled
+ * connection.
+ * @param <C> the connection type.
+ * @since 4.2
+ */
+public interface ConnFactory<T, C> {
+
+ C create(T route) throws IOException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/ConnPool.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/ConnPool.java
new file mode 100644
index 0000000000..803ecda883
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/ConnPool.java
@@ -0,0 +1,68 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.pool;
+
+import java.util.concurrent.Future;
+
+import ch.boye.httpclientandroidlib.concurrent.FutureCallback;
+
+/**
+ * <tt>ConnPool</tt> represents a shared pool connections can be leased from
+ * and released back to.
+ *
+ * @param <T> the route type that represents the opposite endpoint of a pooled
+ * connection.
+ * @param <E> the type of the pool entry containing a pooled connection.
+ * @since 4.2
+ */
+public interface ConnPool<T, E> {
+
+ /**
+ * Attempts to lease a connection for the given route and with the given
+ * state from the pool.
+ *
+ * @param route route of the connection.
+ * @param state arbitrary object that represents a particular state
+ * (usually a security principal or a unique token identifying
+ * the user whose credentials have been used while establishing the connection).
+ * May be <code>null</code>.
+ * @param callback operation completion callback.
+ *
+ * @return future for a leased pool entry.
+ */
+ Future<E> lease(final T route, final Object state, final FutureCallback<E> callback);
+
+ /**
+ * Releases the pool entry back to the pool.
+ *
+ * @param entry pool entry leased from the pool
+ * @param reusable flag indicating whether or not the released connection
+ * is in a consistent state and is safe for further use.
+ */
+ void release(E entry, boolean reusable);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/ConnPoolControl.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/ConnPoolControl.java
new file mode 100644
index 0000000000..841ba7ccc4
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/ConnPoolControl.java
@@ -0,0 +1,56 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.pool;
+
+/**
+ * Interface to control runtime properties of a {@link ConnPool} such as
+ * maximum total number of connections or maximum connections per route
+ * allowed.
+ *
+ * @param <T> the route type that represents the opposite endpoint of a pooled
+ * connection.
+ * @since 4.2
+ */
+public interface ConnPoolControl<T> {
+
+ void setMaxTotal(int max);
+
+ int getMaxTotal();
+
+ void setDefaultMaxPerRoute(int max);
+
+ int getDefaultMaxPerRoute();
+
+ void setMaxPerRoute(final T route, int max);
+
+ int getMaxPerRoute(final T route);
+
+ PoolStats getTotalStats();
+
+ PoolStats getStats(final T route);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/PoolEntry.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/PoolEntry.java
new file mode 100644
index 0000000000..db52afb6fd
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/PoolEntry.java
@@ -0,0 +1,183 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.pool;
+
+import java.util.concurrent.TimeUnit;
+
+import ch.boye.httpclientandroidlib.annotation.GuardedBy;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Pool entry containing a pool connection object along with its route.
+ * <p/>
+ * The connection contained by the pool entry may have an expiration time which
+ * can be either set upon construction time or updated with
+ * the {@link #updateExpiry(long, TimeUnit)}.
+ * <p/>
+ * Pool entry may also have an object associated with it that represents
+ * a connection state (usually a security principal or a unique token identifying
+ * the user whose credentials have been used while establishing the connection).
+ *
+ * @param <T> the route type that represents the opposite endpoint of a pooled
+ * connection.
+ * @param <C> the connection type.
+ * @since 4.2
+ */
+@ThreadSafe
+public abstract class PoolEntry<T, C> {
+
+ private final String id;
+ private final T route;
+ private final C conn;
+ private final long created;
+ private final long validUnit;
+
+ @GuardedBy("this")
+ private long updated;
+
+ @GuardedBy("this")
+ private long expiry;
+
+ private volatile Object state;
+
+ /**
+ * Creates new <tt>PoolEntry</tt> instance.
+ *
+ * @param id unique identifier of the pool entry. May be <code>null</code>.
+ * @param route route to the opposite endpoint.
+ * @param conn the connection.
+ * @param timeToLive maximum time to live. May be zero if the connection
+ * does not have an expiry deadline.
+ * @param tunit time unit.
+ */
+ public PoolEntry(final String id, final T route, final C conn,
+ final long timeToLive, final TimeUnit tunit) {
+ super();
+ Args.notNull(route, "Route");
+ Args.notNull(conn, "Connection");
+ Args.notNull(tunit, "Time unit");
+ this.id = id;
+ this.route = route;
+ this.conn = conn;
+ this.created = System.currentTimeMillis();
+ if (timeToLive > 0) {
+ this.validUnit = this.created + tunit.toMillis(timeToLive);
+ } else {
+ this.validUnit = Long.MAX_VALUE;
+ }
+ this.expiry = this.validUnit;
+ }
+
+ /**
+ * Creates new <tt>PoolEntry</tt> instance without an expiry deadline.
+ *
+ * @param id unique identifier of the pool entry. May be <code>null</code>.
+ * @param route route to the opposite endpoint.
+ * @param conn the connection.
+ */
+ public PoolEntry(final String id, final T route, final C conn) {
+ this(id, route, conn, 0, TimeUnit.MILLISECONDS);
+ }
+
+ public String getId() {
+ return this.id;
+ }
+
+ public T getRoute() {
+ return this.route;
+ }
+
+ public C getConnection() {
+ return this.conn;
+ }
+
+ public long getCreated() {
+ return this.created;
+ }
+
+ public long getValidUnit() {
+ return this.validUnit;
+ }
+
+ public Object getState() {
+ return this.state;
+ }
+
+ public void setState(final Object state) {
+ this.state = state;
+ }
+
+ public synchronized long getUpdated() {
+ return this.updated;
+ }
+
+ public synchronized long getExpiry() {
+ return this.expiry;
+ }
+
+ public synchronized void updateExpiry(final long time, final TimeUnit tunit) {
+ Args.notNull(tunit, "Time unit");
+ this.updated = System.currentTimeMillis();
+ final long newExpiry;
+ if (time > 0) {
+ newExpiry = this.updated + tunit.toMillis(time);
+ } else {
+ newExpiry = Long.MAX_VALUE;
+ }
+ this.expiry = Math.min(newExpiry, this.validUnit);
+ }
+
+ public synchronized boolean isExpired(final long now) {
+ return now >= this.expiry;
+ }
+
+ /**
+ * Invalidates the pool entry and closes the pooled connection associated
+ * with it.
+ */
+ public abstract void close();
+
+ /**
+ * Returns <code>true</code> if the pool entry has been invalidated.
+ */
+ public abstract boolean isClosed();
+
+ @Override
+ public String toString() {
+ final StringBuilder buffer = new StringBuilder();
+ buffer.append("[id:");
+ buffer.append(this.id);
+ buffer.append("][route:");
+ buffer.append(this.route);
+ buffer.append("][state:");
+ buffer.append(this.state);
+ buffer.append("]");
+ return buffer.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/PoolEntryCallback.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/PoolEntryCallback.java
new file mode 100644
index 0000000000..5c70d63da3
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/PoolEntryCallback.java
@@ -0,0 +1,41 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.pool;
+
+/**
+ * Pool entry callabck.
+ *
+ * @param <T> the route type that represents the opposite endpoint of a pooled
+ * connection.
+ * @param <C> the connection type.
+ * @since 4.3
+ */
+public interface PoolEntryCallback<T, C> {
+
+ void process(PoolEntry<T, C> entry);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/PoolEntryFuture.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/PoolEntryFuture.java
new file mode 100644
index 0000000000..743abe8c0c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/PoolEntryFuture.java
@@ -0,0 +1,155 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.pool;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.concurrent.FutureCallback;
+import ch.boye.httpclientandroidlib.util.Args;
+
+@ThreadSafe
+abstract class PoolEntryFuture<T> implements Future<T> {
+
+ private final Lock lock;
+ private final FutureCallback<T> callback;
+ private final Condition condition;
+ private volatile boolean cancelled;
+ private volatile boolean completed;
+ private T result;
+
+ PoolEntryFuture(final Lock lock, final FutureCallback<T> callback) {
+ super();
+ this.lock = lock;
+ this.condition = lock.newCondition();
+ this.callback = callback;
+ }
+
+ public boolean cancel(final boolean mayInterruptIfRunning) {
+ this.lock.lock();
+ try {
+ if (this.completed) {
+ return false;
+ }
+ this.completed = true;
+ this.cancelled = true;
+ if (this.callback != null) {
+ this.callback.cancelled();
+ }
+ this.condition.signalAll();
+ return true;
+ } finally {
+ this.lock.unlock();
+ }
+ }
+
+ public boolean isCancelled() {
+ return this.cancelled;
+ }
+
+ public boolean isDone() {
+ return this.completed;
+ }
+
+ public T get() throws InterruptedException, ExecutionException {
+ try {
+ return get(0, TimeUnit.MILLISECONDS);
+ } catch (final TimeoutException ex) {
+ throw new ExecutionException(ex);
+ }
+ }
+
+ public T get(
+ final long timeout,
+ final TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+ Args.notNull(unit, "Time unit");
+ this.lock.lock();
+ try {
+ if (this.completed) {
+ return this.result;
+ }
+ this.result = getPoolEntry(timeout, unit);
+ this.completed = true;
+ if (this.callback != null) {
+ this.callback.completed(this.result);
+ }
+ return result;
+ } catch (final IOException ex) {
+ this.completed = true;
+ this.result = null;
+ if (this.callback != null) {
+ this.callback.failed(ex);
+ }
+ throw new ExecutionException(ex);
+ } finally {
+ this.lock.unlock();
+ }
+ }
+
+ protected abstract T getPoolEntry(
+ long timeout, TimeUnit unit) throws IOException, InterruptedException, TimeoutException;
+
+ public boolean await(final Date deadline) throws InterruptedException {
+ this.lock.lock();
+ try {
+ if (this.cancelled) {
+ throw new InterruptedException("Operation interrupted");
+ }
+ final boolean success;
+ if (deadline != null) {
+ success = this.condition.awaitUntil(deadline);
+ } else {
+ this.condition.await();
+ success = true;
+ }
+ if (this.cancelled) {
+ throw new InterruptedException("Operation interrupted");
+ }
+ return success;
+ } finally {
+ this.lock.unlock();
+ }
+
+ }
+
+ public void wakeup() {
+ this.lock.lock();
+ try {
+ this.condition.signalAll();
+ } finally {
+ this.lock.unlock();
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/PoolStats.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/PoolStats.java
new file mode 100644
index 0000000000..17e948ee56
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/PoolStats.java
@@ -0,0 +1,114 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.pool;
+
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+
+/**
+ * Pool statistics.
+ * <p>
+ * The total number of connections in the pool is equal to {@code available} plus {@code leased}.
+ * </p>
+ *
+ * @since 4.2
+ */
+@Immutable
+public class PoolStats {
+
+ private final int leased;
+ private final int pending;
+ private final int available;
+ private final int max;
+
+ public PoolStats(final int leased, final int pending, final int free, final int max) {
+ super();
+ this.leased = leased;
+ this.pending = pending;
+ this.available = free;
+ this.max = max;
+ }
+
+ /**
+ * Gets the number of persistent connections tracked by the connection manager currently being used to execute
+ * requests.
+ * <p>
+ * The total number of connections in the pool is equal to {@code available} plus {@code leased}.
+ * </p>
+ *
+ * @return the number of persistent connections.
+ */
+ public int getLeased() {
+ return this.leased;
+ }
+
+ /**
+ * Gets the number of connection requests being blocked awaiting a free connection. This can happen only if there
+ * are more worker threads contending for fewer connections.
+ *
+ * @return the number of connection requests being blocked awaiting a free connection.
+ */
+ public int getPending() {
+ return this.pending;
+ }
+
+ /**
+ * Gets the number idle persistent connections.
+ * <p>
+ * The total number of connections in the pool is equal to {@code available} plus {@code leased}.
+ * </p>
+ *
+ * @return number idle persistent connections.
+ */
+ public int getAvailable() {
+ return this.available;
+ }
+
+ /**
+ * Gets the maximum number of allowed persistent connections.
+ *
+ * @return the maximum number of allowed persistent connections.
+ */
+ public int getMax() {
+ return this.max;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder buffer = new StringBuilder();
+ buffer.append("[leased: ");
+ buffer.append(this.leased);
+ buffer.append("; pending: ");
+ buffer.append(this.pending);
+ buffer.append("; available: ");
+ buffer.append(this.available);
+ buffer.append("; max: ");
+ buffer.append(this.max);
+ buffer.append("]");
+ return buffer.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/RouteSpecificPool.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/RouteSpecificPool.java
new file mode 100644
index 0000000000..7023f79aa1
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/RouteSpecificPool.java
@@ -0,0 +1,184 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.pool;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Set;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.Asserts;
+
+@NotThreadSafe
+abstract class RouteSpecificPool<T, C, E extends PoolEntry<T, C>> {
+
+ private final T route;
+ private final Set<E> leased;
+ private final LinkedList<E> available;
+ private final LinkedList<PoolEntryFuture<E>> pending;
+
+ RouteSpecificPool(final T route) {
+ super();
+ this.route = route;
+ this.leased = new HashSet<E>();
+ this.available = new LinkedList<E>();
+ this.pending = new LinkedList<PoolEntryFuture<E>>();
+ }
+
+ protected abstract E createEntry(C conn);
+
+ public final T getRoute() {
+ return route;
+ }
+
+ public int getLeasedCount() {
+ return this.leased.size();
+ }
+
+ public int getPendingCount() {
+ return this.pending.size();
+ }
+
+ public int getAvailableCount() {
+ return this.available.size();
+ }
+
+ public int getAllocatedCount() {
+ return this.available.size() + this.leased.size();
+ }
+
+ public E getFree(final Object state) {
+ if (!this.available.isEmpty()) {
+ if (state != null) {
+ final Iterator<E> it = this.available.iterator();
+ while (it.hasNext()) {
+ final E entry = it.next();
+ if (state.equals(entry.getState())) {
+ it.remove();
+ this.leased.add(entry);
+ return entry;
+ }
+ }
+ }
+ final Iterator<E> it = this.available.iterator();
+ while (it.hasNext()) {
+ final E entry = it.next();
+ if (entry.getState() == null) {
+ it.remove();
+ this.leased.add(entry);
+ return entry;
+ }
+ }
+ }
+ return null;
+ }
+
+ public E getLastUsed() {
+ if (!this.available.isEmpty()) {
+ return this.available.getLast();
+ } else {
+ return null;
+ }
+ }
+
+ public boolean remove(final E entry) {
+ Args.notNull(entry, "Pool entry");
+ if (!this.available.remove(entry)) {
+ if (!this.leased.remove(entry)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public void free(final E entry, final boolean reusable) {
+ Args.notNull(entry, "Pool entry");
+ final boolean found = this.leased.remove(entry);
+ Asserts.check(found, "Entry %s has not been leased from this pool", entry);
+ if (reusable) {
+ this.available.addFirst(entry);
+ }
+ }
+
+ public E add(final C conn) {
+ final E entry = createEntry(conn);
+ this.leased.add(entry);
+ return entry;
+ }
+
+ public void queue(final PoolEntryFuture<E> future) {
+ if (future == null) {
+ return;
+ }
+ this.pending.add(future);
+ }
+
+ public PoolEntryFuture<E> nextPending() {
+ return this.pending.poll();
+ }
+
+ public void unqueue(final PoolEntryFuture<E> future) {
+ if (future == null) {
+ return;
+ }
+
+ this.pending.remove(future);
+ }
+
+ public void shutdown() {
+ for (final PoolEntryFuture<E> future: this.pending) {
+ future.cancel(true);
+ }
+ this.pending.clear();
+ for (final E entry: this.available) {
+ entry.close();
+ }
+ this.available.clear();
+ for (final E entry: this.leased) {
+ entry.close();
+ }
+ this.leased.clear();
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder buffer = new StringBuilder();
+ buffer.append("[route: ");
+ buffer.append(this.route);
+ buffer.append("][leased: ");
+ buffer.append(this.leased.size());
+ buffer.append("][available: ");
+ buffer.append(this.available.size());
+ buffer.append("][pending: ");
+ buffer.append(this.pending.size());
+ buffer.append("]");
+ return buffer.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/package-info.java
new file mode 100644
index 0000000000..fdca4e6959
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/pool/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Client side connection pools APIs for synchronous, blocking
+ * communication.
+ */
+package ch.boye.httpclientandroidlib.pool;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/BasicHttpContext.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/BasicHttpContext.java
new file mode 100644
index 0000000000..72c247a5e5
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/BasicHttpContext.java
@@ -0,0 +1,95 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Default implementation of {@link HttpContext}.
+ * <p>
+ * Please note instances of this class can be thread unsafe if the
+ * parent context is not thread safe.
+ *
+ * @since 4.0
+ */
+@ThreadSafe
+public class BasicHttpContext implements HttpContext {
+
+ private final HttpContext parentContext;
+ private final Map<String, Object> map;
+
+ public BasicHttpContext() {
+ this(null);
+ }
+
+ public BasicHttpContext(final HttpContext parentContext) {
+ super();
+ this.map = new ConcurrentHashMap<String, Object>();
+ this.parentContext = parentContext;
+ }
+
+ public Object getAttribute(final String id) {
+ Args.notNull(id, "Id");
+ Object obj = this.map.get(id);
+ if (obj == null && this.parentContext != null) {
+ obj = this.parentContext.getAttribute(id);
+ }
+ return obj;
+ }
+
+ public void setAttribute(final String id, final Object obj) {
+ Args.notNull(id, "Id");
+ if (obj != null) {
+ this.map.put(id, obj);
+ } else {
+ this.map.remove(id);
+ }
+ }
+
+ public Object removeAttribute(final String id) {
+ Args.notNull(id, "Id");
+ return this.map.remove(id);
+ }
+
+ /**
+ * @since 4.2
+ */
+ public void clear() {
+ this.map.clear();
+ }
+
+ @Override
+ public String toString() {
+ return this.map.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/BasicHttpProcessor.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/BasicHttpProcessor.java
new file mode 100644
index 0000000000..1fa203cef4
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/BasicHttpProcessor.java
@@ -0,0 +1,246 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpRequestInterceptor;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpResponseInterceptor;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Default implementation of {@link HttpProcessor}.
+ * <p>
+ * Please note access to the internal structures of this class is not
+ * synchronized and therefore this class may be thread-unsafe.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3)
+ */
+@NotThreadSafe
+@Deprecated
+public final class BasicHttpProcessor implements
+ HttpProcessor, HttpRequestInterceptorList, HttpResponseInterceptorList, Cloneable {
+
+ // Don't allow direct access, as nulls are not allowed
+ protected final List<HttpRequestInterceptor> requestInterceptors = new ArrayList<HttpRequestInterceptor>();
+ protected final List<HttpResponseInterceptor> responseInterceptors = new ArrayList<HttpResponseInterceptor>();
+
+ public void addRequestInterceptor(final HttpRequestInterceptor itcp) {
+ if (itcp == null) {
+ return;
+ }
+ this.requestInterceptors.add(itcp);
+ }
+
+ public void addRequestInterceptor(
+ final HttpRequestInterceptor itcp, final int index) {
+ if (itcp == null) {
+ return;
+ }
+ this.requestInterceptors.add(index, itcp);
+ }
+
+ public void addResponseInterceptor(
+ final HttpResponseInterceptor itcp, final int index) {
+ if (itcp == null) {
+ return;
+ }
+ this.responseInterceptors.add(index, itcp);
+ }
+
+ public void removeRequestInterceptorByClass(final Class<? extends HttpRequestInterceptor> clazz) {
+ for (final Iterator<HttpRequestInterceptor> it = this.requestInterceptors.iterator();
+ it.hasNext(); ) {
+ final Object request = it.next();
+ if (request.getClass().equals(clazz)) {
+ it.remove();
+ }
+ }
+ }
+
+ public void removeResponseInterceptorByClass(final Class<? extends HttpResponseInterceptor> clazz) {
+ for (final Iterator<HttpResponseInterceptor> it = this.responseInterceptors.iterator();
+ it.hasNext(); ) {
+ final Object request = it.next();
+ if (request.getClass().equals(clazz)) {
+ it.remove();
+ }
+ }
+ }
+
+ public final void addInterceptor(final HttpRequestInterceptor interceptor) {
+ addRequestInterceptor(interceptor);
+ }
+
+ public final void addInterceptor(final HttpRequestInterceptor interceptor, final int index) {
+ addRequestInterceptor(interceptor, index);
+ }
+
+ public int getRequestInterceptorCount() {
+ return this.requestInterceptors.size();
+ }
+
+ public HttpRequestInterceptor getRequestInterceptor(final int index) {
+ if ((index < 0) || (index >= this.requestInterceptors.size())) {
+ return null;
+ }
+ return this.requestInterceptors.get(index);
+ }
+
+ public void clearRequestInterceptors() {
+ this.requestInterceptors.clear();
+ }
+
+ public void addResponseInterceptor(final HttpResponseInterceptor itcp) {
+ if (itcp == null) {
+ return;
+ }
+ this.responseInterceptors.add(itcp);
+ }
+
+ public final void addInterceptor(final HttpResponseInterceptor interceptor) {
+ addResponseInterceptor(interceptor);
+ }
+
+ public final void addInterceptor(final HttpResponseInterceptor interceptor, final int index) {
+ addResponseInterceptor(interceptor, index);
+ }
+
+ public int getResponseInterceptorCount() {
+ return this.responseInterceptors.size();
+ }
+
+ public HttpResponseInterceptor getResponseInterceptor(final int index) {
+ if ((index < 0) || (index >= this.responseInterceptors.size())) {
+ return null;
+ }
+ return this.responseInterceptors.get(index);
+ }
+
+ public void clearResponseInterceptors() {
+ this.responseInterceptors.clear();
+ }
+
+ /**
+ * Sets the interceptor lists.
+ * First, both interceptor lists maintained by this processor
+ * will be cleared.
+ * Subsequently,
+ * elements of the argument list that are request interceptors will be
+ * added to the request interceptor list.
+ * Elements that are response interceptors will be
+ * added to the response interceptor list.
+ * Elements that are both request and response interceptor will be
+ * added to both lists.
+ * Elements that are neither request nor response interceptor
+ * will be ignored.
+ *
+ * @param list the list of request and response interceptors
+ * from which to initialize
+ */
+ public void setInterceptors(final List<?> list) {
+ Args.notNull(list, "Inteceptor list");
+ this.requestInterceptors.clear();
+ this.responseInterceptors.clear();
+ for (final Object obj : list) {
+ if (obj instanceof HttpRequestInterceptor) {
+ addInterceptor((HttpRequestInterceptor) obj);
+ }
+ if (obj instanceof HttpResponseInterceptor) {
+ addInterceptor((HttpResponseInterceptor) obj);
+ }
+ }
+ }
+
+ /**
+ * Clears both interceptor lists maintained by this processor.
+ */
+ public void clearInterceptors() {
+ clearRequestInterceptors();
+ clearResponseInterceptors();
+ }
+
+ public void process(
+ final HttpRequest request,
+ final HttpContext context)
+ throws IOException, HttpException {
+ for (final HttpRequestInterceptor interceptor : this.requestInterceptors) {
+ interceptor.process(request, context);
+ }
+ }
+
+ public void process(
+ final HttpResponse response,
+ final HttpContext context)
+ throws IOException, HttpException {
+ for (final HttpResponseInterceptor interceptor : this.responseInterceptors) {
+ interceptor.process(response, context);
+ }
+ }
+
+ /**
+ * Sets up the target to have the same list of interceptors
+ * as the current instance.
+ *
+ * @param target object to be initialised
+ */
+ protected void copyInterceptors(final BasicHttpProcessor target) {
+ target.requestInterceptors.clear();
+ target.requestInterceptors.addAll(this.requestInterceptors);
+ target.responseInterceptors.clear();
+ target.responseInterceptors.addAll(this.responseInterceptors);
+ }
+
+ /**
+ * Creates a copy of this instance
+ *
+ * @return new instance of the BasicHttpProcessor
+ */
+ public BasicHttpProcessor copy() {
+ final BasicHttpProcessor clone = new BasicHttpProcessor();
+ copyInterceptors(clone);
+ return clone;
+ }
+
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ final BasicHttpProcessor clone = (BasicHttpProcessor) super.clone();
+ copyInterceptors(clone);
+ return clone;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ChainBuilder.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ChainBuilder.java
new file mode 100644
index 0000000000..19971ffa1b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ChainBuilder.java
@@ -0,0 +1,126 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+
+/**
+ * Builder class to build a linked list (chain) of unique class instances. Each class can have
+ * only one instance in the list. Useful for building lists of protocol interceptors.
+ *
+ * @see ImmutableHttpProcessor
+ *
+ * @since 4.3
+ */
+@NotThreadSafe
+final class ChainBuilder<E> {
+
+ private final LinkedList<E> list;
+ private final Map<Class<?>, E> uniqueClasses;
+
+ public ChainBuilder() {
+ this.list = new LinkedList<E>();
+ this.uniqueClasses = new HashMap<Class<?>, E>();
+ }
+
+ private void ensureUnique(final E e) {
+ final E previous = this.uniqueClasses.remove(e.getClass());
+ if (previous != null) {
+ this.list.remove(previous);
+ }
+ this.uniqueClasses.put(e.getClass(), e);
+ }
+
+ public ChainBuilder<E> addFirst(final E e) {
+ if (e == null) {
+ return this;
+ }
+ ensureUnique(e);
+ this.list.addFirst(e);
+ return this;
+ }
+
+ public ChainBuilder<E> addLast(final E e) {
+ if (e == null) {
+ return this;
+ }
+ ensureUnique(e);
+ this.list.addLast(e);
+ return this;
+ }
+
+ public ChainBuilder<E> addAllFirst(final Collection<E> c) {
+ if (c == null) {
+ return this;
+ }
+ for (final E e: c) {
+ addFirst(e);
+ }
+ return this;
+ }
+
+ public ChainBuilder<E> addAllFirst(final E... c) {
+ if (c == null) {
+ return this;
+ }
+ for (final E e: c) {
+ addFirst(e);
+ }
+ return this;
+ }
+
+ public ChainBuilder<E> addAllLast(final Collection<E> c) {
+ if (c == null) {
+ return this;
+ }
+ for (final E e: c) {
+ addLast(e);
+ }
+ return this;
+ }
+
+ public ChainBuilder<E> addAllLast(final E... c) {
+ if (c == null) {
+ return this;
+ }
+ for (final E e: c) {
+ addLast(e);
+ }
+ return this;
+ }
+
+ public LinkedList<E> build() {
+ return new LinkedList<E>(this.list);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/DefaultedHttpContext.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/DefaultedHttpContext.java
new file mode 100644
index 0000000000..63ef87b98f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/DefaultedHttpContext.java
@@ -0,0 +1,84 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * {@link HttpContext} implementation that delegates resolution of an attribute
+ * to the given default {@link HttpContext} instance if the attribute is not
+ * present in the local one. The state of the local context can be mutated,
+ * whereas the default context is treated as read-only.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) no longer used.
+ */
+@Deprecated
+public final class DefaultedHttpContext implements HttpContext {
+
+ private final HttpContext local;
+ private final HttpContext defaults;
+
+ public DefaultedHttpContext(final HttpContext local, final HttpContext defaults) {
+ super();
+ this.local = Args.notNull(local, "HTTP context");
+ this.defaults = defaults;
+ }
+
+ public Object getAttribute(final String id) {
+ final Object obj = this.local.getAttribute(id);
+ if (obj == null) {
+ return this.defaults.getAttribute(id);
+ } else {
+ return obj;
+ }
+ }
+
+ public Object removeAttribute(final String id) {
+ return this.local.removeAttribute(id);
+ }
+
+ public void setAttribute(final String id, final Object obj) {
+ this.local.setAttribute(id, obj);
+ }
+
+ public HttpContext getDefaults() {
+ return this.defaults;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder buf = new StringBuilder();
+ buf.append("[local: ").append(this.local);
+ buf.append("defaults: ").append(this.defaults);
+ buf.append("]");
+ return buf.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ExecutionContext.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ExecutionContext.java
new file mode 100644
index 0000000000..b27a481df3
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ExecutionContext.java
@@ -0,0 +1,80 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+/**
+ * {@link HttpContext} attribute names for protocol execution.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3) use {@link HttpCoreContext}.
+ */
+@Deprecated
+public interface ExecutionContext {
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.HttpConnection} object that
+ * represents the actual HTTP connection.
+ */
+ public static final String HTTP_CONNECTION = "http.connection";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.HttpRequest} object that
+ * represents the actual HTTP request.
+ */
+ public static final String HTTP_REQUEST = "http.request";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.HttpResponse} object that
+ * represents the actual HTTP response.
+ */
+ public static final String HTTP_RESPONSE = "http.response";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.HttpHost} object that
+ * represents the connection target.
+ */
+ public static final String HTTP_TARGET_HOST = "http.target_host";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.HttpHost} object that
+ * represents the connection proxy.
+ *
+ * @deprecated (4.3) do not use.
+ */
+ @Deprecated
+ public static final String HTTP_PROXY_HOST = "http.proxy_host";
+
+ /**
+ * Attribute name of a {@link Boolean} object that represents the
+ * the flag indicating whether the actual request has been fully transmitted
+ * to the target host.
+ */
+ public static final String HTTP_REQ_SENT = "http.request_sent";
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HTTP.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HTTP.java
new file mode 100644
index 0000000000..593b3bfdfb
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HTTP.java
@@ -0,0 +1,135 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import java.nio.charset.Charset;
+
+import ch.boye.httpclientandroidlib.Consts;
+
+/**
+ * Constants and static helpers related to the HTTP protocol.
+ *
+ * @since 4.0
+ */
+public final class HTTP {
+
+ public static final int CR = 13; // <US-ASCII CR, carriage return (13)>
+ public static final int LF = 10; // <US-ASCII LF, linefeed (10)>
+ public static final int SP = 32; // <US-ASCII SP, space (32)>
+ public static final int HT = 9; // <US-ASCII HT, horizontal-tab (9)>
+
+ /** HTTP header definitions */
+ public static final String TRANSFER_ENCODING = "Transfer-Encoding";
+ public static final String CONTENT_LEN = "Content-Length";
+ public static final String CONTENT_TYPE = "Content-Type";
+ public static final String CONTENT_ENCODING = "Content-Encoding";
+ public static final String EXPECT_DIRECTIVE = "Expect";
+ public static final String CONN_DIRECTIVE = "Connection";
+ public static final String TARGET_HOST = "Host";
+ public static final String USER_AGENT = "User-Agent";
+ public static final String DATE_HEADER = "Date";
+ public static final String SERVER_HEADER = "Server";
+
+ /** HTTP expectations */
+ public static final String EXPECT_CONTINUE = "100-continue";
+
+ /** HTTP connection control */
+ public static final String CONN_CLOSE = "Close";
+ public static final String CONN_KEEP_ALIVE = "Keep-Alive";
+
+ /** Transfer encoding definitions */
+ public static final String CHUNK_CODING = "chunked";
+ public static final String IDENTITY_CODING = "identity";
+
+ public static final Charset DEF_CONTENT_CHARSET = Consts.ISO_8859_1;
+ public static final Charset DEF_PROTOCOL_CHARSET = Consts.ASCII;
+
+ /**
+ * @deprecated (4.2)
+ */
+ @Deprecated
+ public static final String UTF_8 = "UTF-8";
+ /**
+ * @deprecated (4.2)
+ */
+ @Deprecated
+ public static final String UTF_16 = "UTF-16";
+ /**
+ * @deprecated (4.2)
+ */
+ @Deprecated
+ public static final String US_ASCII = "US-ASCII";
+ /**
+ * @deprecated (4.2)
+ */
+ @Deprecated
+ public static final String ASCII = "ASCII";
+ /**
+ * @deprecated (4.2)
+ */
+ @Deprecated
+ public static final String ISO_8859_1 = "ISO-8859-1";
+ /**
+ * @deprecated (4.2)
+ */
+ @Deprecated
+ public static final String DEFAULT_CONTENT_CHARSET = ISO_8859_1;
+ /**
+ * @deprecated (4.2)
+ */
+ @Deprecated
+ public static final String DEFAULT_PROTOCOL_CHARSET = US_ASCII;
+ /**
+ * @deprecated (4.2)
+ */
+ @Deprecated
+ public final static String OCTET_STREAM_TYPE = "application/octet-stream";
+ /**
+ * @deprecated (4.2)
+ */
+ @Deprecated
+ public final static String PLAIN_TEXT_TYPE = "text/plain";
+ /**
+ * @deprecated (4.2)
+ */
+ @Deprecated
+ public final static String CHARSET_PARAM = "; charset=";
+ /**
+ * @deprecated (4.2)
+ */
+ @Deprecated
+ public final static String DEFAULT_CONTENT_TYPE = OCTET_STREAM_TYPE;
+
+ public static boolean isWhitespace(final char ch) {
+ return ch == SP || ch == HT || ch == CR || ch == LF;
+ }
+
+ private HTTP() {
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpContext.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpContext.java
new file mode 100644
index 0000000000..c2b70ccea8
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpContext.java
@@ -0,0 +1,75 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+/**
+ * HttpContext represents execution state of an HTTP process. It is a structure
+ * that can be used to map an attribute name to an attribute value.
+ * <p/>
+ * The primary purpose of the HTTP context is to facilitate information sharing
+ * among various logically related components. HTTP context can be used
+ * to store a processing state for one message or several consecutive messages.
+ * Multiple logically related messages can participate in a logical session
+ * if the same context is reused between consecutive messages.
+ * <p>/
+ * IMPORTANT: Please note HTTP context implementation, even when thread safe,
+ * may not be used concurrently by multiple threads, as the context may contain
+ * thread unsafe attributes.
+ *
+ * @since 4.0
+ */
+public interface HttpContext {
+
+ /** The prefix reserved for use by HTTP components. "http." */
+ public static final String RESERVED_PREFIX = "http.";
+
+ /**
+ * Obtains attribute with the given name.
+ *
+ * @param id the attribute name.
+ * @return attribute value, or <code>null</code> if not set.
+ */
+ Object getAttribute(String id);
+
+ /**
+ * Sets value of the attribute with the given name.
+ *
+ * @param id the attribute name.
+ * @param obj the attribute value.
+ */
+ void setAttribute(String id, Object obj);
+
+ /**
+ * Removes attribute with the given name from the context.
+ *
+ * @param id the attribute name.
+ * @return attribute value, or <code>null</code> if not set.
+ */
+ Object removeAttribute(String id);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpCoreContext.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpCoreContext.java
new file mode 100644
index 0000000000..3d42627511
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpCoreContext.java
@@ -0,0 +1,152 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import ch.boye.httpclientandroidlib.HttpConnection;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Implementation of {@link HttpContext} that provides convenience
+ * setters for user assignable attributes and getter for readable attributes.
+ *
+ * @since 4.3
+ */
+@NotThreadSafe
+public class HttpCoreContext implements HttpContext {
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.HttpConnection} object that
+ * represents the actual HTTP connection.
+ */
+ public static final String HTTP_CONNECTION = "http.connection";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.HttpRequest} object that
+ * represents the actual HTTP request.
+ */
+ public static final String HTTP_REQUEST = "http.request";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.HttpResponse} object that
+ * represents the actual HTTP response.
+ */
+ public static final String HTTP_RESPONSE = "http.response";
+
+ /**
+ * Attribute name of a {@link ch.boye.httpclientandroidlib.HttpHost} object that
+ * represents the connection target.
+ */
+ public static final String HTTP_TARGET_HOST = "http.target_host";
+
+ /**
+ * Attribute name of a {@link Boolean} object that represents the
+ * the flag indicating whether the actual request has been fully transmitted
+ * to the target host.
+ */
+ public static final String HTTP_REQ_SENT = "http.request_sent";
+
+ public static HttpCoreContext create() {
+ return new HttpCoreContext(new BasicHttpContext());
+ }
+
+ public static HttpCoreContext adapt(final HttpContext context) {
+ Args.notNull(context, "HTTP context");
+ if (context instanceof HttpCoreContext) {
+ return (HttpCoreContext) context;
+ } else {
+ return new HttpCoreContext(context);
+ }
+ }
+
+ private final HttpContext context;
+
+ public HttpCoreContext(final HttpContext context) {
+ super();
+ this.context = context;
+ }
+
+ public HttpCoreContext() {
+ super();
+ this.context = new BasicHttpContext();
+ }
+
+ public Object getAttribute(final String id) {
+ return context.getAttribute(id);
+ }
+
+ public void setAttribute(final String id, final Object obj) {
+ context.setAttribute(id, obj);
+ }
+
+ public Object removeAttribute(final String id) {
+ return context.removeAttribute(id);
+ }
+
+ public <T> T getAttribute(final String attribname, final Class<T> clazz) {
+ Args.notNull(clazz, "Attribute class");
+ final Object obj = getAttribute(attribname);
+ if (obj == null) {
+ return null;
+ }
+ return clazz.cast(obj);
+ }
+
+ public <T extends HttpConnection> T getConnection(final Class<T> clazz) {
+ return getAttribute(HTTP_CONNECTION, clazz);
+ }
+
+ public HttpConnection getConnection() {
+ return getAttribute(HTTP_CONNECTION, HttpConnection.class);
+ }
+
+ public HttpRequest getRequest() {
+ return getAttribute(HTTP_REQUEST, HttpRequest.class);
+ }
+
+ public boolean isRequestSent() {
+ final Boolean b = getAttribute(HTTP_REQ_SENT, Boolean.class);
+ return b != null && b.booleanValue();
+ }
+
+ public HttpResponse getResponse() {
+ return getAttribute(HTTP_RESPONSE, HttpResponse.class);
+ }
+
+ public void setTargetHost(final HttpHost host) {
+ setAttribute(HTTP_TARGET_HOST, host);
+ }
+
+ public HttpHost getTargetHost() {
+ return getAttribute(HTTP_TARGET_HOST, HttpHost.class);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpDateGenerator.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpDateGenerator.java
new file mode 100644
index 0000000000..8e99c1ea2b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpDateGenerator.java
@@ -0,0 +1,77 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import ch.boye.httpclientandroidlib.annotation.GuardedBy;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+
+/**
+ * Generates a date in the format required by the HTTP protocol.
+ *
+ * @since 4.0
+ */
+@ThreadSafe
+public class HttpDateGenerator {
+
+ /** Date format pattern used to generate the header in RFC 1123 format. */
+ public static final
+ String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
+
+ /** The time zone to use in the date header. */
+ public static final TimeZone GMT = TimeZone.getTimeZone("GMT");
+
+ @GuardedBy("this")
+ private final DateFormat dateformat;
+ @GuardedBy("this")
+ private long dateAsLong = 0L;
+ @GuardedBy("this")
+ private String dateAsText = null;
+
+ public HttpDateGenerator() {
+ super();
+ this.dateformat = new SimpleDateFormat(PATTERN_RFC1123, Locale.US);
+ this.dateformat.setTimeZone(GMT);
+ }
+
+ public synchronized String getCurrentDate() {
+ final long now = System.currentTimeMillis();
+ if (now - this.dateAsLong > 1000) {
+ // Generate new date string
+ this.dateAsText = this.dateformat.format(new Date(now));
+ this.dateAsLong = now;
+ }
+ return this.dateAsText;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpExpectationVerifier.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpExpectationVerifier.java
new file mode 100644
index 0000000000..1057046166
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpExpectationVerifier.java
@@ -0,0 +1,81 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+
+/**
+ * Defines an interface to verify whether an incoming HTTP request meets
+ * the target server's expectations.
+ *<p>
+ * The Expect request-header field is used to indicate that particular
+ * server behaviors are required by the client.
+ *</p>
+ *<pre>
+ * Expect = "Expect" ":" 1#expectation
+ *
+ * expectation = "100-continue" | expectation-extension
+ * expectation-extension = token [ "=" ( token | quoted-string )
+ * *expect-params ]
+ * expect-params = ";" token [ "=" ( token | quoted-string ) ]
+ *</pre>
+ *<p>
+ * A server that does not understand or is unable to comply with any of
+ * the expectation values in the Expect field of a request MUST respond
+ * with appropriate error status. The server MUST respond with a 417
+ * (Expectation Failed) status if any of the expectations cannot be met
+ * or, if there are other problems with the request, some other 4xx
+ * status.
+ *</p>
+ *
+ * @since 4.0
+ */
+public interface HttpExpectationVerifier {
+
+ /**
+ * Verifies whether the given request meets the server's expectations.
+ * <p>
+ * If the request fails to meet particular criteria, this method can
+ * trigger a terminal response back to the client by setting the status
+ * code of the response object to a value greater or equal to
+ * <code>200</code>. In this case the client will not have to transmit
+ * the request body. If the request meets expectations this method can
+ * terminate without modifying the response object. Per default the status
+ * code of the response object will be set to <code>100</code>.
+ *
+ * @param request the HTTP request.
+ * @param response the HTTP response.
+ * @param context the HTTP context.
+ * @throws HttpException in case of an HTTP protocol violation.
+ */
+ void verify(HttpRequest request, HttpResponse response, HttpContext context)
+ throws HttpException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpProcessor.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpProcessor.java
new file mode 100644
index 0000000000..e4dac6795d
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpProcessor.java
@@ -0,0 +1,55 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import ch.boye.httpclientandroidlib.HttpRequestInterceptor;
+import ch.boye.httpclientandroidlib.HttpResponseInterceptor;
+
+/**
+ * HTTP protocol processor is a collection of protocol interceptors that
+ * implements the 'Chain of Responsibility' pattern, where each individual
+ * protocol interceptor is expected to work on a particular aspect of the HTTP
+ * protocol the interceptor is responsible for.
+ * <p>
+ * Usually the order in which interceptors are executed should not matter as
+ * long as they do not depend on a particular state of the execution context.
+ * If protocol interceptors have interdependencies and therefore must be
+ * executed in a particular order, they should be added to the protocol
+ * processor in the same sequence as their expected execution order.
+ * <p>
+ * Protocol interceptors must be implemented as thread-safe. Similarly to
+ * servlets, protocol interceptors should not use instance variables unless
+ * access to those variables is synchronized.
+ *
+ * @since 4.0
+ */
+public interface HttpProcessor
+ extends HttpRequestInterceptor, HttpResponseInterceptor {
+
+ // no additional methods
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpProcessorBuilder.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpProcessorBuilder.java
new file mode 100644
index 0000000000..29dfccd605
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpProcessorBuilder.java
@@ -0,0 +1,151 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import ch.boye.httpclientandroidlib.HttpRequestInterceptor;
+import ch.boye.httpclientandroidlib.HttpResponseInterceptor;
+
+/**
+ * Builder for {@link HttpProcessor} instances.
+ *
+ * @since 4.3
+ */
+public class HttpProcessorBuilder {
+
+ private ChainBuilder<HttpRequestInterceptor> requestChainBuilder;
+ private ChainBuilder<HttpResponseInterceptor> responseChainBuilder;
+
+ public static HttpProcessorBuilder create() {
+ return new HttpProcessorBuilder();
+ }
+
+ HttpProcessorBuilder() {
+ super();
+ }
+
+ private ChainBuilder<HttpRequestInterceptor> getRequestChainBuilder() {
+ if (requestChainBuilder == null) {
+ requestChainBuilder = new ChainBuilder<HttpRequestInterceptor>();
+ }
+ return requestChainBuilder;
+ }
+
+ private ChainBuilder<HttpResponseInterceptor> getResponseChainBuilder() {
+ if (responseChainBuilder == null) {
+ responseChainBuilder = new ChainBuilder<HttpResponseInterceptor>();
+ }
+ return responseChainBuilder;
+ }
+
+ public HttpProcessorBuilder addFirst(final HttpRequestInterceptor e) {
+ if (e == null) {
+ return this;
+ }
+ getRequestChainBuilder().addFirst(e);
+ return this;
+ }
+
+ public HttpProcessorBuilder addLast(final HttpRequestInterceptor e) {
+ if (e == null) {
+ return this;
+ }
+ getRequestChainBuilder().addLast(e);
+ return this;
+ }
+
+ public HttpProcessorBuilder add(final HttpRequestInterceptor e) {
+ return addLast(e);
+ }
+
+ public HttpProcessorBuilder addAllFirst(final HttpRequestInterceptor... e) {
+ if (e == null) {
+ return this;
+ }
+ getRequestChainBuilder().addAllFirst(e);
+ return this;
+ }
+
+ public HttpProcessorBuilder addAllLast(final HttpRequestInterceptor... e) {
+ if (e == null) {
+ return this;
+ }
+ getRequestChainBuilder().addAllLast(e);
+ return this;
+ }
+
+ public HttpProcessorBuilder addAll(final HttpRequestInterceptor... e) {
+ return addAllLast(e);
+ }
+
+ public HttpProcessorBuilder addFirst(final HttpResponseInterceptor e) {
+ if (e == null) {
+ return this;
+ }
+ getResponseChainBuilder().addFirst(e);
+ return this;
+ }
+
+ public HttpProcessorBuilder addLast(final HttpResponseInterceptor e) {
+ if (e == null) {
+ return this;
+ }
+ getResponseChainBuilder().addLast(e);
+ return this;
+ }
+
+ public HttpProcessorBuilder add(final HttpResponseInterceptor e) {
+ return addLast(e);
+ }
+
+ public HttpProcessorBuilder addAllFirst(final HttpResponseInterceptor... e) {
+ if (e == null) {
+ return this;
+ }
+ getResponseChainBuilder().addAllFirst(e);
+ return this;
+ }
+
+ public HttpProcessorBuilder addAllLast(final HttpResponseInterceptor... e) {
+ if (e == null) {
+ return this;
+ }
+ getResponseChainBuilder().addAllLast(e);
+ return this;
+ }
+
+ public HttpProcessorBuilder addAll(final HttpResponseInterceptor... e) {
+ return addAllLast(e);
+ }
+
+ public HttpProcessor build() {
+ return new ImmutableHttpProcessor(
+ requestChainBuilder != null ? requestChainBuilder.build() : null,
+ responseChainBuilder != null ? responseChainBuilder.build() : null);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestExecutor.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestExecutor.java
new file mode 100644
index 0000000000..d895f04208
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestExecutor.java
@@ -0,0 +1,311 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpClientConnection;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * <tt>HttpRequestExecutor</tt> is a client side HTTP protocol handler based
+ * on the blocking (classic) I/O model.
+ * <p/>
+ * <tt>HttpRequestExecutor</tt> relies on {@link HttpProcessor} to generate
+ * mandatory protocol headers for all outgoing messages and apply common,
+ * cross-cutting message transformations to all incoming and outgoing messages.
+ * Application specific processing can be implemented outside
+ * <tt>HttpRequestExecutor</tt> once the request has been executed and
+ * a response has been received.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class HttpRequestExecutor {
+
+ public static final int DEFAULT_WAIT_FOR_CONTINUE = 3000;
+
+ private final int waitForContinue;
+
+ /**
+ * Creates new instance of HttpRequestExecutor.
+ *
+ * @since 4.3
+ */
+ public HttpRequestExecutor(final int waitForContinue) {
+ super();
+ this.waitForContinue = Args.positive(waitForContinue, "Wait for continue time");
+ }
+
+ public HttpRequestExecutor() {
+ this(DEFAULT_WAIT_FOR_CONTINUE);
+ }
+
+ /**
+ * Decide whether a response comes with an entity.
+ * The implementation in this class is based on RFC 2616.
+ * <br/>
+ * Derived executors can override this method to handle
+ * methods and response codes not specified in RFC 2616.
+ *
+ * @param request the request, to obtain the executed method
+ * @param response the response, to obtain the status code
+ */
+ protected boolean canResponseHaveBody(final HttpRequest request,
+ final HttpResponse response) {
+
+ if ("HEAD".equalsIgnoreCase(request.getRequestLine().getMethod())) {
+ return false;
+ }
+ final int status = response.getStatusLine().getStatusCode();
+ return status >= HttpStatus.SC_OK
+ && status != HttpStatus.SC_NO_CONTENT
+ && status != HttpStatus.SC_NOT_MODIFIED
+ && status != HttpStatus.SC_RESET_CONTENT;
+ }
+
+ /**
+ * Sends the request and obtain a response.
+ *
+ * @param request the request to execute.
+ * @param conn the connection over which to execute the request.
+ *
+ * @return the response to the request.
+ *
+ * @throws IOException in case of an I/O error.
+ * @throws HttpException in case of HTTP protocol violation or a processing
+ * problem.
+ */
+ public HttpResponse execute(
+ final HttpRequest request,
+ final HttpClientConnection conn,
+ final HttpContext context) throws IOException, HttpException {
+ Args.notNull(request, "HTTP request");
+ Args.notNull(conn, "Client connection");
+ Args.notNull(context, "HTTP context");
+ try {
+ HttpResponse response = doSendRequest(request, conn, context);
+ if (response == null) {
+ response = doReceiveResponse(request, conn, context);
+ }
+ return response;
+ } catch (final IOException ex) {
+ closeConnection(conn);
+ throw ex;
+ } catch (final HttpException ex) {
+ closeConnection(conn);
+ throw ex;
+ } catch (final RuntimeException ex) {
+ closeConnection(conn);
+ throw ex;
+ }
+ }
+
+ private static void closeConnection(final HttpClientConnection conn) {
+ try {
+ conn.close();
+ } catch (final IOException ignore) {
+ }
+ }
+
+ /**
+ * Pre-process the given request using the given protocol processor and
+ * initiates the process of request execution.
+ *
+ * @param request the request to prepare
+ * @param processor the processor to use
+ * @param context the context for sending the request
+ *
+ * @throws IOException in case of an I/O error.
+ * @throws HttpException in case of HTTP protocol violation or a processing
+ * problem.
+ */
+ public void preProcess(
+ final HttpRequest request,
+ final HttpProcessor processor,
+ final HttpContext context) throws HttpException, IOException {
+ Args.notNull(request, "HTTP request");
+ Args.notNull(processor, "HTTP processor");
+ Args.notNull(context, "HTTP context");
+ context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
+ processor.process(request, context);
+ }
+
+ /**
+ * Send the given request over the given connection.
+ * <p>
+ * This method also handles the expect-continue handshake if necessary.
+ * If it does not have to handle an expect-continue handshake, it will
+ * not use the connection for reading or anything else that depends on
+ * data coming in over the connection.
+ *
+ * @param request the request to send, already
+ * {@link #preProcess preprocessed}
+ * @param conn the connection over which to send the request,
+ * already established
+ * @param context the context for sending the request
+ *
+ * @return a terminal response received as part of an expect-continue
+ * handshake, or
+ * <code>null</code> if the expect-continue handshake is not used
+ *
+ * @throws IOException in case of an I/O error.
+ * @throws HttpException in case of HTTP protocol violation or a processing
+ * problem.
+ */
+ protected HttpResponse doSendRequest(
+ final HttpRequest request,
+ final HttpClientConnection conn,
+ final HttpContext context) throws IOException, HttpException {
+ Args.notNull(request, "HTTP request");
+ Args.notNull(conn, "Client connection");
+ Args.notNull(context, "HTTP context");
+
+ HttpResponse response = null;
+
+ context.setAttribute(HttpCoreContext.HTTP_CONNECTION, conn);
+ context.setAttribute(HttpCoreContext.HTTP_REQ_SENT, Boolean.FALSE);
+
+ conn.sendRequestHeader(request);
+ if (request instanceof HttpEntityEnclosingRequest) {
+ // Check for expect-continue handshake. We have to flush the
+ // headers and wait for an 100-continue response to handle it.
+ // If we get a different response, we must not send the entity.
+ boolean sendentity = true;
+ final ProtocolVersion ver =
+ request.getRequestLine().getProtocolVersion();
+ if (((HttpEntityEnclosingRequest) request).expectContinue() &&
+ !ver.lessEquals(HttpVersion.HTTP_1_0)) {
+
+ conn.flush();
+ // As suggested by RFC 2616 section 8.2.3, we don't wait for a
+ // 100-continue response forever. On timeout, send the entity.
+ if (conn.isResponseAvailable(this.waitForContinue)) {
+ response = conn.receiveResponseHeader();
+ if (canResponseHaveBody(request, response)) {
+ conn.receiveResponseEntity(response);
+ }
+ final int status = response.getStatusLine().getStatusCode();
+ if (status < 200) {
+ if (status != HttpStatus.SC_CONTINUE) {
+ throw new ProtocolException(
+ "Unexpected response: " + response.getStatusLine());
+ }
+ // discard 100-continue
+ response = null;
+ } else {
+ sendentity = false;
+ }
+ }
+ }
+ if (sendentity) {
+ conn.sendRequestEntity((HttpEntityEnclosingRequest) request);
+ }
+ }
+ conn.flush();
+ context.setAttribute(HttpCoreContext.HTTP_REQ_SENT, Boolean.TRUE);
+ return response;
+ }
+
+ /**
+ * Waits for and receives a response.
+ * This method will automatically ignore intermediate responses
+ * with status code 1xx.
+ *
+ * @param request the request for which to obtain the response
+ * @param conn the connection over which the request was sent
+ * @param context the context for receiving the response
+ *
+ * @return the terminal response, not yet post-processed
+ *
+ * @throws IOException in case of an I/O error.
+ * @throws HttpException in case of HTTP protocol violation or a processing
+ * problem.
+ */
+ protected HttpResponse doReceiveResponse(
+ final HttpRequest request,
+ final HttpClientConnection conn,
+ final HttpContext context) throws HttpException, IOException {
+ Args.notNull(request, "HTTP request");
+ Args.notNull(conn, "Client connection");
+ Args.notNull(context, "HTTP context");
+ HttpResponse response = null;
+ int statusCode = 0;
+
+ while (response == null || statusCode < HttpStatus.SC_OK) {
+
+ response = conn.receiveResponseHeader();
+ if (canResponseHaveBody(request, response)) {
+ conn.receiveResponseEntity(response);
+ }
+ statusCode = response.getStatusLine().getStatusCode();
+
+ } // while intermediate response
+
+ return response;
+ }
+
+ /**
+ * Post-processes the given response using the given protocol processor and
+ * completes the process of request execution.
+ * <p>
+ * This method does <i>not</i> read the response entity, if any.
+ * The connection over which content of the response entity is being
+ * streamed from cannot be reused until
+ * {@link ch.boye.httpclientandroidlib.util.EntityUtils#consume(ch.boye.httpclientandroidlib.HttpEntity)}
+ * has been invoked.
+ *
+ * @param response the response object to post-process
+ * @param processor the processor to use
+ * @param context the context for post-processing the response
+ *
+ * @throws IOException in case of an I/O error.
+ * @throws HttpException in case of HTTP protocol violation or a processing
+ * problem.
+ */
+ public void postProcess(
+ final HttpResponse response,
+ final HttpProcessor processor,
+ final HttpContext context) throws HttpException, IOException {
+ Args.notNull(response, "HTTP response");
+ Args.notNull(processor, "HTTP processor");
+ Args.notNull(context, "HTTP context");
+ context.setAttribute(HttpCoreContext.HTTP_RESPONSE, response);
+ processor.process(response, context);
+ }
+
+} // class HttpRequestExecutor
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestHandler.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestHandler.java
new file mode 100644
index 0000000000..b948ae65d7
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestHandler.java
@@ -0,0 +1,62 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+
+/**
+ * HttpRequestHandler represents a routine for processing of a specific group
+ * of HTTP requests. Protocol handlers are designed to take care of protocol
+ * specific aspects, whereas individual request handlers are expected to take
+ * care of application specific HTTP processing. The main purpose of a request
+ * handler is to generate a response object with a content entity to be sent
+ * back to the client in response to the given request
+ *
+ * @since 4.0
+ */
+public interface HttpRequestHandler {
+
+ /**
+ * Handles the request and produces a response to be sent back to
+ * the client.
+ *
+ * @param request the HTTP request.
+ * @param response the HTTP response.
+ * @param context the HTTP execution context.
+ * @throws IOException in case of an I/O error.
+ * @throws HttpException in case of HTTP protocol violation or a processing
+ * problem.
+ */
+ void handle(HttpRequest request, HttpResponse response, HttpContext context)
+ throws HttpException, IOException;
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestHandlerMapper.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestHandlerMapper.java
new file mode 100644
index 0000000000..e97da4e888
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestHandlerMapper.java
@@ -0,0 +1,50 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import ch.boye.httpclientandroidlib.HttpRequest;
+
+/**
+ * HttpRequestHandlerMapper can be used to resolve an instance of
+ * {@link HttpRequestHandler} matching a particular {@link HttpRequest}. Usually the
+ * mapped request handler will be used to process the request.
+ *
+ * @since 4.3
+ */
+public interface HttpRequestHandlerMapper {
+
+ /**
+ * Looks up a handler matching the given request.
+ *
+ * @param request the request to map to a handler
+ * @return HTTP request handler or <code>null</code> if no match
+ * is found.
+ */
+ HttpRequestHandler lookup(HttpRequest request);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestHandlerRegistry.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestHandlerRegistry.java
new file mode 100644
index 0000000000..3592d00ac4
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestHandlerRegistry.java
@@ -0,0 +1,107 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import java.util.Map;
+
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Maintains a map of HTTP request handlers keyed by a request URI pattern.
+ * <br>
+ * Patterns may have three formats:
+ * <ul>
+ * <li><code>*</code></li>
+ * <li><code>*&lt;uri&gt;</code></li>
+ * <li><code>&lt;uri&gt;*</code></li>
+ * </ul>
+ * <br>
+ * This class can be used to resolve an instance of
+ * {@link HttpRequestHandler} matching a particular request URI. Usually the
+ * resolved request handler will be used to process the request with the
+ * specified request URI.
+ *
+ * @since 4.0
+ * @deprecated (4.3) use {@link UriHttpRequestHandlerMapper}
+ */
+@ThreadSafe // provided injected dependencies are thread-safe
+@Deprecated
+public class HttpRequestHandlerRegistry implements HttpRequestHandlerResolver {
+
+ private final UriPatternMatcher<HttpRequestHandler> matcher;
+
+ public HttpRequestHandlerRegistry() {
+ matcher = new UriPatternMatcher<HttpRequestHandler>();
+ }
+
+ /**
+ * Registers the given {@link HttpRequestHandler} as a handler for URIs
+ * matching the given pattern.
+ *
+ * @param pattern the pattern to register the handler for.
+ * @param handler the handler.
+ */
+ public void register(final String pattern, final HttpRequestHandler handler) {
+ Args.notNull(pattern, "URI request pattern");
+ Args.notNull(handler, "Request handler");
+ matcher.register(pattern, handler);
+ }
+
+ /**
+ * Removes registered handler, if exists, for the given pattern.
+ *
+ * @param pattern the pattern to unregister the handler for.
+ */
+ public void unregister(final String pattern) {
+ matcher.unregister(pattern);
+ }
+
+ /**
+ * Sets handlers from the given map.
+ * @param map the map containing handlers keyed by their URI patterns.
+ */
+ public void setHandlers(final Map<String, HttpRequestHandler> map) {
+ matcher.setObjects(map);
+ }
+
+ /**
+ * Get the handler map.
+ * @return The map of handlers and their associated URI patterns.
+ *
+ * @since 4.2
+ */
+ public Map<String, HttpRequestHandler> getHandlers() {
+ return matcher.getObjects();
+ }
+
+ public HttpRequestHandler lookup(final String requestURI) {
+ return matcher.lookup(requestURI);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestHandlerResolver.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestHandlerResolver.java
new file mode 100644
index 0000000000..a898b8d186
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestHandlerResolver.java
@@ -0,0 +1,51 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+/**
+ * HttpRequestHandlerResolver can be used to resolve an instance of
+ * {@link HttpRequestHandler} matching a particular request URI. Usually the
+ * mapped request handler will be used to process the request with the
+ * specified request URI.
+ *
+ * @since 4.0
+ * @deprecated see {@link HttpRequestHandlerMapper}
+ */
+@Deprecated
+public interface HttpRequestHandlerResolver {
+
+ /**
+ * Looks up a handler matching the given request URI.
+ *
+ * @param requestURI the request URI
+ * @return HTTP request handler or <code>null</code> if no match
+ * is found.
+ */
+ HttpRequestHandler lookup(String requestURI);
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestInterceptorList.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestInterceptorList.java
new file mode 100644
index 0000000000..da4fd6f860
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpRequestInterceptorList.java
@@ -0,0 +1,103 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.HttpRequestInterceptor;
+
+/**
+ * Provides access to an ordered list of request interceptors.
+ * Lists are expected to be built upfront and used read-only afterwards
+ * for {@link HttpProcessor processing}.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3)
+ */
+@Deprecated
+public interface HttpRequestInterceptorList {
+
+ /**
+ * Appends a request interceptor to this list.
+ *
+ * @param interceptor the request interceptor to add
+ */
+ void addRequestInterceptor(HttpRequestInterceptor interceptor);
+
+ /**
+ * Inserts a request interceptor at the specified index.
+ *
+ * @param interceptor the request interceptor to add
+ * @param index the index to insert the interceptor at
+ */
+ void addRequestInterceptor(HttpRequestInterceptor interceptor, int index);
+
+ /**
+ * Obtains the current size of this list.
+ *
+ * @return the number of request interceptors in this list
+ */
+ int getRequestInterceptorCount();
+
+ /**
+ * Obtains a request interceptor from this list.
+ *
+ * @param index the index of the interceptor to obtain,
+ * 0 for first
+ *
+ * @return the interceptor at the given index, or
+ * <code>null</code> if the index is out of range
+ */
+ HttpRequestInterceptor getRequestInterceptor(int index);
+
+ /**
+ * Removes all request interceptors from this list.
+ */
+ void clearRequestInterceptors();
+
+ /**
+ * Removes all request interceptor of the specified class
+ *
+ * @param clazz the class of the instances to be removed.
+ */
+ void removeRequestInterceptorByClass(Class<? extends HttpRequestInterceptor> clazz);
+
+ /**
+ * Sets the request interceptors in this list.
+ * This list will be cleared and re-initialized to contain
+ * all request interceptors from the argument list.
+ * If the argument list includes elements that are not request
+ * interceptors, the behavior is implementation dependent.
+ *
+ * @param list the list of request interceptors
+ */
+ void setInterceptors(List<?> list);
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpResponseInterceptorList.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpResponseInterceptorList.java
new file mode 100644
index 0000000000..9265b6738b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpResponseInterceptorList.java
@@ -0,0 +1,103 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.HttpResponseInterceptor;
+
+/**
+ * Provides access to an ordered list of response interceptors.
+ * Lists are expected to be built upfront and used read-only afterwards
+ * for {@link HttpProcessor processing}.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.3)
+ */
+@Deprecated
+public interface HttpResponseInterceptorList {
+
+ /**
+ * Appends a response interceptor to this list.
+ *
+ * @param interceptor the response interceptor to add
+ */
+ void addResponseInterceptor(HttpResponseInterceptor interceptor);
+
+ /**
+ * Inserts a response interceptor at the specified index.
+ *
+ * @param interceptor the response interceptor to add
+ * @param index the index to insert the interceptor at
+ */
+ void addResponseInterceptor(HttpResponseInterceptor interceptor, int index);
+
+ /**
+ * Obtains the current size of this list.
+ *
+ * @return the number of response interceptors in this list
+ */
+ int getResponseInterceptorCount();
+
+ /**
+ * Obtains a response interceptor from this list.
+ *
+ * @param index the index of the interceptor to obtain,
+ * 0 for first
+ *
+ * @return the interceptor at the given index, or
+ * <code>null</code> if the index is out of range
+ */
+ HttpResponseInterceptor getResponseInterceptor(int index);
+
+ /**
+ * Removes all response interceptors from this list.
+ */
+ void clearResponseInterceptors();
+
+ /**
+ * Removes all response interceptor of the specified class
+ *
+ * @param clazz the class of the instances to be removed.
+ */
+ void removeResponseInterceptorByClass(Class<? extends HttpResponseInterceptor> clazz);
+
+ /**
+ * Sets the response interceptors in this list.
+ * This list will be cleared and re-initialized to contain
+ * all response interceptors from the argument list.
+ * If the argument list includes elements that are not response
+ * interceptors, the behavior is implementation dependent.
+ *
+ * @param list the list of response interceptors
+ */
+ void setInterceptors(List<?> list);
+
+}
+
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpService.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpService.java
new file mode 100644
index 0000000000..13defa2d9f
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/HttpService.java
@@ -0,0 +1,447 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.ConnectionReuseStrategy;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpResponseFactory;
+import ch.boye.httpclientandroidlib.HttpServerConnection;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.MethodNotSupportedException;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.UnsupportedHttpVersionException;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.entity.ByteArrayEntity;
+import ch.boye.httpclientandroidlib.impl.DefaultConnectionReuseStrategy;
+import ch.boye.httpclientandroidlib.impl.DefaultHttpResponseFactory;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+import ch.boye.httpclientandroidlib.util.EncodingUtils;
+import ch.boye.httpclientandroidlib.util.EntityUtils;
+
+/**
+ * <tt>HttpService</tt> is a server side HTTP protocol handler based on
+ * the classic (blocking) I/O model.
+ * <p/>
+ * <tt>HttpService</tt> relies on {@link HttpProcessor} to generate mandatory
+ * protocol headers for all outgoing messages and apply common, cross-cutting
+ * message transformations to all incoming and outgoing messages, whereas
+ * individual {@link HttpRequestHandler}s are expected to implement
+ * application specific content generation and processing.
+ * <p/>
+ * <tt>HttpService</tt> uses {@link HttpRequestHandlerMapper} to map
+ * matching request handler for a particular request URI of an incoming HTTP
+ * request.
+ * <p/>
+ * <tt>HttpService</tt> can use optional {@link HttpExpectationVerifier}
+ * to ensure that incoming requests meet server's expectations.
+ *
+ * @since 4.0
+ */
+@SuppressWarnings("deprecation")
+@Immutable // provided injected dependencies are immutable and deprecated methods are not used
+public class HttpService {
+
+ /**
+ * TODO: make all variables final in the next major version
+ */
+ private volatile HttpParams params = null;
+ private volatile HttpProcessor processor = null;
+ private volatile HttpRequestHandlerMapper handlerMapper = null;
+ private volatile ConnectionReuseStrategy connStrategy = null;
+ private volatile HttpResponseFactory responseFactory = null;
+ private volatile HttpExpectationVerifier expectationVerifier = null;
+
+ /**
+ * Create a new HTTP service.
+ *
+ * @param processor the processor to use on requests and responses
+ * @param connStrategy the connection reuse strategy
+ * @param responseFactory the response factory
+ * @param handlerResolver the handler resolver. May be null.
+ * @param expectationVerifier the expectation verifier. May be null.
+ * @param params the HTTP parameters
+ *
+ * @since 4.1
+ * @deprecated (4.3) use {@link HttpService#HttpService(HttpProcessor, ConnectionReuseStrategy,
+ * HttpResponseFactory, HttpRequestHandlerMapper, HttpExpectationVerifier)}
+ */
+ @Deprecated
+ public HttpService(
+ final HttpProcessor processor,
+ final ConnectionReuseStrategy connStrategy,
+ final HttpResponseFactory responseFactory,
+ final HttpRequestHandlerResolver handlerResolver,
+ final HttpExpectationVerifier expectationVerifier,
+ final HttpParams params) {
+ this(processor,
+ connStrategy,
+ responseFactory,
+ new HttpRequestHandlerResolverAdapter(handlerResolver),
+ expectationVerifier);
+ this.params = params;
+ }
+
+ /**
+ * Create a new HTTP service.
+ *
+ * @param processor the processor to use on requests and responses
+ * @param connStrategy the connection reuse strategy
+ * @param responseFactory the response factory
+ * @param handlerResolver the handler resolver. May be null.
+ * @param params the HTTP parameters
+ *
+ * @since 4.1
+ * @deprecated (4.3) use {@link HttpService#HttpService(HttpProcessor, ConnectionReuseStrategy,
+ * HttpResponseFactory, HttpRequestHandlerMapper)}
+ */
+ @Deprecated
+ public HttpService(
+ final HttpProcessor processor,
+ final ConnectionReuseStrategy connStrategy,
+ final HttpResponseFactory responseFactory,
+ final HttpRequestHandlerResolver handlerResolver,
+ final HttpParams params) {
+ this(processor,
+ connStrategy,
+ responseFactory,
+ new HttpRequestHandlerResolverAdapter(handlerResolver),
+ null);
+ this.params = params;
+ }
+
+ /**
+ * Create a new HTTP service.
+ *
+ * @param proc the processor to use on requests and responses
+ * @param connStrategy the connection reuse strategy
+ * @param responseFactory the response factory
+ *
+ * @deprecated (4.1) use {@link HttpService#HttpService(HttpProcessor,
+ * ConnectionReuseStrategy, HttpResponseFactory, HttpRequestHandlerResolver, HttpParams)}
+ */
+ @Deprecated
+ public HttpService(
+ final HttpProcessor proc,
+ final ConnectionReuseStrategy connStrategy,
+ final HttpResponseFactory responseFactory) {
+ super();
+ setHttpProcessor(proc);
+ setConnReuseStrategy(connStrategy);
+ setResponseFactory(responseFactory);
+ }
+
+ /**
+ * Create a new HTTP service.
+ *
+ * @param processor the processor to use on requests and responses
+ * @param connStrategy the connection reuse strategy. If <code>null</code>
+ * {@link DefaultConnectionReuseStrategy#INSTANCE} will be used.
+ * @param responseFactory the response factory. If <code>null</code>
+ * {@link DefaultHttpResponseFactory#INSTANCE} will be used.
+ * @param handlerMapper the handler mapper. May be null.
+ * @param expectationVerifier the expectation verifier. May be null.
+ *
+ * @since 4.3
+ */
+ public HttpService(
+ final HttpProcessor processor,
+ final ConnectionReuseStrategy connStrategy,
+ final HttpResponseFactory responseFactory,
+ final HttpRequestHandlerMapper handlerMapper,
+ final HttpExpectationVerifier expectationVerifier) {
+ super();
+ this.processor = Args.notNull(processor, "HTTP processor");
+ this.connStrategy = connStrategy != null ? connStrategy :
+ DefaultConnectionReuseStrategy.INSTANCE;
+ this.responseFactory = responseFactory != null ? responseFactory :
+ DefaultHttpResponseFactory.INSTANCE;
+ this.handlerMapper = handlerMapper;
+ this.expectationVerifier = expectationVerifier;
+ }
+
+ /**
+ * Create a new HTTP service.
+ *
+ * @param processor the processor to use on requests and responses
+ * @param connStrategy the connection reuse strategy. If <code>null</code>
+ * {@link DefaultConnectionReuseStrategy#INSTANCE} will be used.
+ * @param responseFactory the response factory. If <code>null</code>
+ * {@link DefaultHttpResponseFactory#INSTANCE} will be used.
+ * @param handlerMapper the handler mapper. May be null.
+ *
+ * @since 4.3
+ */
+ public HttpService(
+ final HttpProcessor processor,
+ final ConnectionReuseStrategy connStrategy,
+ final HttpResponseFactory responseFactory,
+ final HttpRequestHandlerMapper handlerMapper) {
+ this(processor, connStrategy, responseFactory, handlerMapper, null);
+ }
+
+ /**
+ * Create a new HTTP service.
+ *
+ * @param processor the processor to use on requests and responses
+ * @param handlerMapper the handler mapper. May be null.
+ *
+ * @since 4.3
+ */
+ public HttpService(
+ final HttpProcessor processor, final HttpRequestHandlerMapper handlerMapper) {
+ this(processor, null, null, handlerMapper, null);
+ }
+
+ /**
+ * @deprecated (4.1) set {@link HttpProcessor} using constructor
+ */
+ @Deprecated
+ public void setHttpProcessor(final HttpProcessor processor) {
+ Args.notNull(processor, "HTTP processor");
+ this.processor = processor;
+ }
+
+ /**
+ * @deprecated (4.1) set {@link ConnectionReuseStrategy} using constructor
+ */
+ @Deprecated
+ public void setConnReuseStrategy(final ConnectionReuseStrategy connStrategy) {
+ Args.notNull(connStrategy, "Connection reuse strategy");
+ this.connStrategy = connStrategy;
+ }
+
+ /**
+ * @deprecated (4.1) set {@link HttpResponseFactory} using constructor
+ */
+ @Deprecated
+ public void setResponseFactory(final HttpResponseFactory responseFactory) {
+ Args.notNull(responseFactory, "Response factory");
+ this.responseFactory = responseFactory;
+ }
+
+ /**
+ * @deprecated (4.1) set {@link HttpResponseFactory} using constructor
+ */
+ @Deprecated
+ public void setParams(final HttpParams params) {
+ this.params = params;
+ }
+
+ /**
+ * @deprecated (4.1) set {@link HttpRequestHandlerResolver} using constructor
+ */
+ @Deprecated
+ public void setHandlerResolver(final HttpRequestHandlerResolver handlerResolver) {
+ this.handlerMapper = new HttpRequestHandlerResolverAdapter(handlerResolver);
+ }
+
+ /**
+ * @deprecated (4.1) set {@link HttpExpectationVerifier} using constructor
+ */
+ @Deprecated
+ public void setExpectationVerifier(final HttpExpectationVerifier expectationVerifier) {
+ this.expectationVerifier = expectationVerifier;
+ }
+
+ /**
+ * @deprecated (4.3) no longer used.
+ */
+ @Deprecated
+ public HttpParams getParams() {
+ return this.params;
+ }
+
+ /**
+ * Handles receives one HTTP request over the given connection within the
+ * given execution context and sends a response back to the client.
+ *
+ * @param conn the active connection to the client
+ * @param context the actual execution context.
+ * @throws IOException in case of an I/O error.
+ * @throws HttpException in case of HTTP protocol violation or a processing
+ * problem.
+ */
+ public void handleRequest(
+ final HttpServerConnection conn,
+ final HttpContext context) throws IOException, HttpException {
+
+ context.setAttribute(HttpCoreContext.HTTP_CONNECTION, conn);
+
+ HttpResponse response = null;
+
+ try {
+
+ final HttpRequest request = conn.receiveRequestHeader();
+ if (request instanceof HttpEntityEnclosingRequest) {
+
+ if (((HttpEntityEnclosingRequest) request).expectContinue()) {
+ response = this.responseFactory.newHttpResponse(HttpVersion.HTTP_1_1,
+ HttpStatus.SC_CONTINUE, context);
+ if (this.expectationVerifier != null) {
+ try {
+ this.expectationVerifier.verify(request, response, context);
+ } catch (final HttpException ex) {
+ response = this.responseFactory.newHttpResponse(HttpVersion.HTTP_1_0,
+ HttpStatus.SC_INTERNAL_SERVER_ERROR, context);
+ handleException(ex, response);
+ }
+ }
+ if (response.getStatusLine().getStatusCode() < 200) {
+ // Send 1xx response indicating the server expections
+ // have been met
+ conn.sendResponseHeader(response);
+ conn.flush();
+ response = null;
+ conn.receiveRequestEntity((HttpEntityEnclosingRequest) request);
+ }
+ } else {
+ conn.receiveRequestEntity((HttpEntityEnclosingRequest) request);
+ }
+ }
+
+ context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
+
+ if (response == null) {
+ response = this.responseFactory.newHttpResponse(HttpVersion.HTTP_1_1,
+ HttpStatus.SC_OK, context);
+ this.processor.process(request, context);
+ doService(request, response, context);
+ }
+
+ // Make sure the request content is fully consumed
+ if (request instanceof HttpEntityEnclosingRequest) {
+ final HttpEntity entity = ((HttpEntityEnclosingRequest)request).getEntity();
+ EntityUtils.consume(entity);
+ }
+
+ } catch (final HttpException ex) {
+ response = this.responseFactory.newHttpResponse
+ (HttpVersion.HTTP_1_0, HttpStatus.SC_INTERNAL_SERVER_ERROR,
+ context);
+ handleException(ex, response);
+ }
+
+ context.setAttribute(HttpCoreContext.HTTP_RESPONSE, response);
+
+ this.processor.process(response, context);
+ conn.sendResponseHeader(response);
+ conn.sendResponseEntity(response);
+ conn.flush();
+
+ if (!this.connStrategy.keepAlive(response, context)) {
+ conn.close();
+ }
+ }
+
+ /**
+ * Handles the given exception and generates an HTTP response to be sent
+ * back to the client to inform about the exceptional condition encountered
+ * in the course of the request processing.
+ *
+ * @param ex the exception.
+ * @param response the HTTP response.
+ */
+ protected void handleException(final HttpException ex, final HttpResponse response) {
+ if (ex instanceof MethodNotSupportedException) {
+ response.setStatusCode(HttpStatus.SC_NOT_IMPLEMENTED);
+ } else if (ex instanceof UnsupportedHttpVersionException) {
+ response.setStatusCode(HttpStatus.SC_HTTP_VERSION_NOT_SUPPORTED);
+ } else if (ex instanceof ProtocolException) {
+ response.setStatusCode(HttpStatus.SC_BAD_REQUEST);
+ } else {
+ response.setStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR);
+ }
+ String message = ex.getMessage();
+ if (message == null) {
+ message = ex.toString();
+ }
+ final byte[] msg = EncodingUtils.getAsciiBytes(message);
+ final ByteArrayEntity entity = new ByteArrayEntity(msg);
+ entity.setContentType("text/plain; charset=US-ASCII");
+ response.setEntity(entity);
+ }
+
+ /**
+ * The default implementation of this method attempts to resolve an
+ * {@link HttpRequestHandler} for the request URI of the given request
+ * and, if found, executes its
+ * {@link HttpRequestHandler#handle(HttpRequest, HttpResponse, HttpContext)}
+ * method.
+ * <p>
+ * Super-classes can override this method in order to provide a custom
+ * implementation of the request processing logic.
+ *
+ * @param request the HTTP request.
+ * @param response the HTTP response.
+ * @param context the execution context.
+ * @throws IOException in case of an I/O error.
+ * @throws HttpException in case of HTTP protocol violation or a processing
+ * problem.
+ */
+ protected void doService(
+ final HttpRequest request,
+ final HttpResponse response,
+ final HttpContext context) throws HttpException, IOException {
+ HttpRequestHandler handler = null;
+ if (this.handlerMapper != null) {
+ handler = this.handlerMapper.lookup(request);
+ }
+ if (handler != null) {
+ handler.handle(request, response, context);
+ } else {
+ response.setStatusCode(HttpStatus.SC_NOT_IMPLEMENTED);
+ }
+ }
+
+ /**
+ * Adaptor class to transition from HttpRequestHandlerResolver to HttpRequestHandlerMapper.
+ */
+ @Deprecated
+ private static class HttpRequestHandlerResolverAdapter implements HttpRequestHandlerMapper {
+
+ private final HttpRequestHandlerResolver resolver;
+
+ public HttpRequestHandlerResolverAdapter(final HttpRequestHandlerResolver resolver) {
+ this.resolver = resolver;
+ }
+
+ public HttpRequestHandler lookup(final HttpRequest request) {
+ return resolver.lookup(request.getRequestLine().getUri());
+ }
+
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ImmutableHttpProcessor.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ImmutableHttpProcessor.java
new file mode 100644
index 0000000000..8cb80c91b9
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ImmutableHttpProcessor.java
@@ -0,0 +1,143 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.protocol;
+
+import java.io.IOException;
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpRequestInterceptor;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpResponseInterceptor;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+
+/**
+ * Immutable {@link HttpProcessor}.
+ *
+ * @since 4.1
+ */
+@ThreadSafe // provided injected dependencies are immutable
+public final class ImmutableHttpProcessor implements HttpProcessor {
+
+ private final HttpRequestInterceptor[] requestInterceptors;
+ private final HttpResponseInterceptor[] responseInterceptors;
+
+ public ImmutableHttpProcessor(
+ final HttpRequestInterceptor[] requestInterceptors,
+ final HttpResponseInterceptor[] responseInterceptors) {
+ super();
+ if (requestInterceptors != null) {
+ final int l = requestInterceptors.length;
+ this.requestInterceptors = new HttpRequestInterceptor[l];
+ System.arraycopy(requestInterceptors, 0, this.requestInterceptors, 0, l);
+ } else {
+ this.requestInterceptors = new HttpRequestInterceptor[0];
+ }
+ if (responseInterceptors != null) {
+ final int l = responseInterceptors.length;
+ this.responseInterceptors = new HttpResponseInterceptor[l];
+ System.arraycopy(responseInterceptors, 0, this.responseInterceptors, 0, l);
+ } else {
+ this.responseInterceptors = new HttpResponseInterceptor[0];
+ }
+ }
+
+ /**
+ * @since 4.3
+ */
+ public ImmutableHttpProcessor(
+ final List<HttpRequestInterceptor> requestInterceptors,
+ final List<HttpResponseInterceptor> responseInterceptors) {
+ super();
+ if (requestInterceptors != null) {
+ final int l = requestInterceptors.size();
+ this.requestInterceptors = requestInterceptors.toArray(new HttpRequestInterceptor[l]);
+ } else {
+ this.requestInterceptors = new HttpRequestInterceptor[0];
+ }
+ if (responseInterceptors != null) {
+ final int l = responseInterceptors.size();
+ this.responseInterceptors = responseInterceptors.toArray(new HttpResponseInterceptor[l]);
+ } else {
+ this.responseInterceptors = new HttpResponseInterceptor[0];
+ }
+ }
+
+ /**
+ * @deprecated (4.3) do not use.
+ */
+ @Deprecated
+ public ImmutableHttpProcessor(
+ final HttpRequestInterceptorList requestInterceptors,
+ final HttpResponseInterceptorList responseInterceptors) {
+ super();
+ if (requestInterceptors != null) {
+ final int count = requestInterceptors.getRequestInterceptorCount();
+ this.requestInterceptors = new HttpRequestInterceptor[count];
+ for (int i = 0; i < count; i++) {
+ this.requestInterceptors[i] = requestInterceptors.getRequestInterceptor(i);
+ }
+ } else {
+ this.requestInterceptors = new HttpRequestInterceptor[0];
+ }
+ if (responseInterceptors != null) {
+ final int count = responseInterceptors.getResponseInterceptorCount();
+ this.responseInterceptors = new HttpResponseInterceptor[count];
+ for (int i = 0; i < count; i++) {
+ this.responseInterceptors[i] = responseInterceptors.getResponseInterceptor(i);
+ }
+ } else {
+ this.responseInterceptors = new HttpResponseInterceptor[0];
+ }
+ }
+
+ public ImmutableHttpProcessor(final HttpRequestInterceptor... requestInterceptors) {
+ this(requestInterceptors, null);
+ }
+
+ public ImmutableHttpProcessor(final HttpResponseInterceptor... responseInterceptors) {
+ this(null, responseInterceptors);
+ }
+
+ public void process(
+ final HttpRequest request,
+ final HttpContext context) throws IOException, HttpException {
+ for (final HttpRequestInterceptor requestInterceptor : this.requestInterceptors) {
+ requestInterceptor.process(request, context);
+ }
+ }
+
+ public void process(
+ final HttpResponse response,
+ final HttpContext context) throws IOException, HttpException {
+ for (final HttpResponseInterceptor responseInterceptor : this.responseInterceptors) {
+ responseInterceptor.process(response, context);
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestConnControl.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestConnControl.java
new file mode 100644
index 0000000000..1db509599c
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestConnControl.java
@@ -0,0 +1,69 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpRequestInterceptor;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * RequestConnControl is responsible for adding <code>Connection</code> header
+ * to the outgoing requests, which is essential for managing persistence of
+ * <code>HTTP/1.0</code> connections. This interceptor is recommended for
+ * client side protocol processors.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class RequestConnControl implements HttpRequestInterceptor {
+
+ public RequestConnControl() {
+ super();
+ }
+
+ public void process(final HttpRequest request, final HttpContext context)
+ throws HttpException, IOException {
+ Args.notNull(request, "HTTP request");
+
+ final String method = request.getRequestLine().getMethod();
+ if (method.equalsIgnoreCase("CONNECT")) {
+ return;
+ }
+
+ if (!request.containsHeader(HTTP.CONN_DIRECTIVE)) {
+ // Default policy is to keep connection alive
+ // whenever possible
+ request.addHeader(HTTP.CONN_DIRECTIVE, HTTP.CONN_KEEP_ALIVE);
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestContent.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestContent.java
new file mode 100644
index 0000000000..2f90ad6d8b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestContent.java
@@ -0,0 +1,127 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpRequestInterceptor;
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * RequestContent is the most important interceptor for outgoing requests.
+ * It is responsible for delimiting content length by adding
+ * <code>Content-Length</code> or <code>Transfer-Content</code> headers based
+ * on the properties of the enclosed entity and the protocol version.
+ * This interceptor is required for correct functioning of client side protocol
+ * processors.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class RequestContent implements HttpRequestInterceptor {
+
+ private final boolean overwrite;
+
+ /**
+ * Default constructor. The <code>Content-Length</code> or <code>Transfer-Encoding</code>
+ * will cause the interceptor to throw {@link ProtocolException} if already present in the
+ * response message.
+ */
+ public RequestContent() {
+ this(false);
+ }
+
+ /**
+ * Constructor that can be used to fine-tune behavior of this interceptor.
+ *
+ * @param overwrite If set to <code>true</code> the <code>Content-Length</code> and
+ * <code>Transfer-Encoding</code> headers will be created or updated if already present.
+ * If set to <code>false</code> the <code>Content-Length</code> and
+ * <code>Transfer-Encoding</code> headers will cause the interceptor to throw
+ * {@link ProtocolException} if already present in the response message.
+ *
+ * @since 4.2
+ */
+ public RequestContent(final boolean overwrite) {
+ super();
+ this.overwrite = overwrite;
+ }
+
+ public void process(final HttpRequest request, final HttpContext context)
+ throws HttpException, IOException {
+ Args.notNull(request, "HTTP request");
+ if (request instanceof HttpEntityEnclosingRequest) {
+ if (this.overwrite) {
+ request.removeHeaders(HTTP.TRANSFER_ENCODING);
+ request.removeHeaders(HTTP.CONTENT_LEN);
+ } else {
+ if (request.containsHeader(HTTP.TRANSFER_ENCODING)) {
+ throw new ProtocolException("Transfer-encoding header already present");
+ }
+ if (request.containsHeader(HTTP.CONTENT_LEN)) {
+ throw new ProtocolException("Content-Length header already present");
+ }
+ }
+ final ProtocolVersion ver = request.getRequestLine().getProtocolVersion();
+ final HttpEntity entity = ((HttpEntityEnclosingRequest)request).getEntity();
+ if (entity == null) {
+ request.addHeader(HTTP.CONTENT_LEN, "0");
+ return;
+ }
+ // Must specify a transfer encoding or a content length
+ if (entity.isChunked() || entity.getContentLength() < 0) {
+ if (ver.lessEquals(HttpVersion.HTTP_1_0)) {
+ throw new ProtocolException(
+ "Chunked transfer encoding not allowed for " + ver);
+ }
+ request.addHeader(HTTP.TRANSFER_ENCODING, HTTP.CHUNK_CODING);
+ } else {
+ request.addHeader(HTTP.CONTENT_LEN, Long.toString(entity.getContentLength()));
+ }
+ // Specify a content type if known
+ if (entity.getContentType() != null && !request.containsHeader(
+ HTTP.CONTENT_TYPE )) {
+ request.addHeader(entity.getContentType());
+ }
+ // Specify a content encoding if known
+ if (entity.getContentEncoding() != null && !request.containsHeader(
+ HTTP.CONTENT_ENCODING)) {
+ request.addHeader(entity.getContentEncoding());
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestDate.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestDate.java
new file mode 100644
index 0000000000..a2a0fa8b60
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestDate.java
@@ -0,0 +1,65 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpRequestInterceptor;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * RequestDate interceptor is responsible for adding <code>Date</code> header
+ * to the outgoing requests This interceptor is optional for client side
+ * protocol processors.
+ *
+ * @since 4.0
+ */
+@ThreadSafe
+public class RequestDate implements HttpRequestInterceptor {
+
+ private static final HttpDateGenerator DATE_GENERATOR = new HttpDateGenerator();
+
+ public RequestDate() {
+ super();
+ }
+
+ public void process(final HttpRequest request, final HttpContext context)
+ throws HttpException, IOException {
+ Args.notNull(request, "HTTP request");
+ if ((request instanceof HttpEntityEnclosingRequest) &&
+ !request.containsHeader(HTTP.DATE_HEADER)) {
+ final String httpdate = DATE_GENERATOR.getCurrentDate();
+ request.setHeader(HTTP.DATE_HEADER, httpdate);
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestExpectContinue.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestExpectContinue.java
new file mode 100644
index 0000000000..16cb8cd1cd
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestExpectContinue.java
@@ -0,0 +1,93 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpRequestInterceptor;
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.params.CoreProtocolPNames;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * RequestExpectContinue is responsible for enabling the 'expect-continue'
+ * handshake by adding <code>Expect</code> header. This interceptor is
+ * recommended for client side protocol processors.
+ *
+ * @since 4.0
+ */
+@Immutable
+@SuppressWarnings("deprecation")
+public class RequestExpectContinue implements HttpRequestInterceptor {
+
+ private final boolean activeByDefault;
+
+ /**
+ * @deprecated (4.3) use {@link ch.boye.httpclientandroidlib.protocol.RequestExpectContinue#RequestExpectContinue(boolean)}
+ */
+ @Deprecated
+ public RequestExpectContinue() {
+ this(false);
+ }
+
+ /**
+ * @since 4.3
+ */
+ public RequestExpectContinue(final boolean activeByDefault) {
+ super();
+ this.activeByDefault = activeByDefault;
+ }
+
+ public void process(final HttpRequest request, final HttpContext context)
+ throws HttpException, IOException {
+ Args.notNull(request, "HTTP request");
+
+ if (!request.containsHeader(HTTP.EXPECT_DIRECTIVE)) {
+ if (request instanceof HttpEntityEnclosingRequest) {
+ final ProtocolVersion ver = request.getRequestLine().getProtocolVersion();
+ final HttpEntity entity = ((HttpEntityEnclosingRequest)request).getEntity();
+ // Do not send the expect header if request body is known to be empty
+ if (entity != null
+ && entity.getContentLength() != 0 && !ver.lessEquals(HttpVersion.HTTP_1_0)) {
+ final boolean active = request.getParams().getBooleanParameter(
+ CoreProtocolPNames.USE_EXPECT_CONTINUE, this.activeByDefault);
+ if (active) {
+ request.addHeader(HTTP.EXPECT_DIRECTIVE, HTTP.EXPECT_CONTINUE);
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestTargetHost.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestTargetHost.java
new file mode 100644
index 0000000000..531a8a235e
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestTargetHost.java
@@ -0,0 +1,95 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import java.io.IOException;
+import java.net.InetAddress;
+
+import ch.boye.httpclientandroidlib.HttpConnection;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpHost;
+import ch.boye.httpclientandroidlib.HttpInetConnection;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpRequestInterceptor;
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * RequestTargetHost is responsible for adding <code>Host</code> header. This
+ * interceptor is required for client side protocol processors.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class RequestTargetHost implements HttpRequestInterceptor {
+
+ public RequestTargetHost() {
+ super();
+ }
+
+ public void process(final HttpRequest request, final HttpContext context)
+ throws HttpException, IOException {
+ Args.notNull(request, "HTTP request");
+
+ final HttpCoreContext corecontext = HttpCoreContext.adapt(context);
+
+ final ProtocolVersion ver = request.getRequestLine().getProtocolVersion();
+ final String method = request.getRequestLine().getMethod();
+ if (method.equalsIgnoreCase("CONNECT") && ver.lessEquals(HttpVersion.HTTP_1_0)) {
+ return;
+ }
+
+ if (!request.containsHeader(HTTP.TARGET_HOST)) {
+ HttpHost targethost = corecontext.getTargetHost();
+ if (targethost == null) {
+ final HttpConnection conn = corecontext.getConnection();
+ if (conn instanceof HttpInetConnection) {
+ // Populate the context with a default HTTP host based on the
+ // inet address of the target host
+ final InetAddress address = ((HttpInetConnection) conn).getRemoteAddress();
+ final int port = ((HttpInetConnection) conn).getRemotePort();
+ if (address != null) {
+ targethost = new HttpHost(address.getHostName(), port);
+ }
+ }
+ if (targethost == null) {
+ if (ver.lessEquals(HttpVersion.HTTP_1_0)) {
+ return;
+ } else {
+ throw new ProtocolException("Target host missing");
+ }
+ }
+ }
+ request.addHeader(HTTP.TARGET_HOST, targethost.toHostString());
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestUserAgent.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestUserAgent.java
new file mode 100644
index 0000000000..23f4a45a7a
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/RequestUserAgent.java
@@ -0,0 +1,79 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpRequestInterceptor;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.params.CoreProtocolPNames;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * RequestUserAgent is responsible for adding <code>User-Agent</code> header.
+ * This interceptor is recommended for client side protocol processors.
+ *
+ * @since 4.0
+ */
+@SuppressWarnings("deprecation")
+@Immutable
+public class RequestUserAgent implements HttpRequestInterceptor {
+
+ private final String userAgent;
+
+ public RequestUserAgent(final String userAgent) {
+ super();
+ this.userAgent = userAgent;
+ }
+
+ public RequestUserAgent() {
+ this(null);
+ }
+
+ public void process(final HttpRequest request, final HttpContext context)
+ throws HttpException, IOException {
+ Args.notNull(request, "HTTP request");
+ if (!request.containsHeader(HTTP.USER_AGENT)) {
+ String s = null;
+ final HttpParams params = request.getParams();
+ if (params != null) {
+ s = (String) params.getParameter(CoreProtocolPNames.USER_AGENT);
+ }
+ if (s == null) {
+ s = this.userAgent;
+ }
+ if (s != null) {
+ request.addHeader(HTTP.USER_AGENT, s);
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ResponseConnControl.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ResponseConnControl.java
new file mode 100644
index 0000000000..29014e32f2
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ResponseConnControl.java
@@ -0,0 +1,105 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpResponseInterceptor;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * ResponseConnControl is responsible for adding <code>Connection</code> header
+ * to the outgoing responses, which is essential for managing persistence of
+ * <code>HTTP/1.0</code> connections. This interceptor is recommended for
+ * server side protocol processors.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class ResponseConnControl implements HttpResponseInterceptor {
+
+ public ResponseConnControl() {
+ super();
+ }
+
+ public void process(final HttpResponse response, final HttpContext context)
+ throws HttpException, IOException {
+ Args.notNull(response, "HTTP response");
+
+ final HttpCoreContext corecontext = HttpCoreContext.adapt(context);
+
+ // Always drop connection after certain type of responses
+ final int status = response.getStatusLine().getStatusCode();
+ if (status == HttpStatus.SC_BAD_REQUEST ||
+ status == HttpStatus.SC_REQUEST_TIMEOUT ||
+ status == HttpStatus.SC_LENGTH_REQUIRED ||
+ status == HttpStatus.SC_REQUEST_TOO_LONG ||
+ status == HttpStatus.SC_REQUEST_URI_TOO_LONG ||
+ status == HttpStatus.SC_SERVICE_UNAVAILABLE ||
+ status == HttpStatus.SC_NOT_IMPLEMENTED) {
+ response.setHeader(HTTP.CONN_DIRECTIVE, HTTP.CONN_CLOSE);
+ return;
+ }
+ final Header explicit = response.getFirstHeader(HTTP.CONN_DIRECTIVE);
+ if (explicit != null && HTTP.CONN_CLOSE.equalsIgnoreCase(explicit.getValue())) {
+ // Connection persistence explicitly disabled
+ return;
+ }
+ // Always drop connection for HTTP/1.0 responses and below
+ // if the content body cannot be correctly delimited
+ final HttpEntity entity = response.getEntity();
+ if (entity != null) {
+ final ProtocolVersion ver = response.getStatusLine().getProtocolVersion();
+ if (entity.getContentLength() < 0 &&
+ (!entity.isChunked() || ver.lessEquals(HttpVersion.HTTP_1_0))) {
+ response.setHeader(HTTP.CONN_DIRECTIVE, HTTP.CONN_CLOSE);
+ return;
+ }
+ }
+ // Drop connection if requested by the client or request was <= 1.0
+ final HttpRequest request = corecontext.getRequest();
+ if (request != null) {
+ final Header header = request.getFirstHeader(HTTP.CONN_DIRECTIVE);
+ if (header != null) {
+ response.setHeader(HTTP.CONN_DIRECTIVE, header.getValue());
+ } else if (request.getProtocolVersion().lessEquals(HttpVersion.HTTP_1_0)) {
+ response.setHeader(HTTP.CONN_DIRECTIVE, HTTP.CONN_CLOSE);
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ResponseContent.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ResponseContent.java
new file mode 100644
index 0000000000..e2dcf2eefb
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ResponseContent.java
@@ -0,0 +1,133 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpResponseInterceptor;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.HttpVersion;
+import ch.boye.httpclientandroidlib.ProtocolException;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * ResponseContent is the most important interceptor for outgoing responses.
+ * It is responsible for delimiting content length by adding
+ * <code>Content-Length</code> or <code>Transfer-Content</code> headers based
+ * on the properties of the enclosed entity and the protocol version.
+ * This interceptor is required for correct functioning of server side protocol
+ * processors.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class ResponseContent implements HttpResponseInterceptor {
+
+ private final boolean overwrite;
+
+ /**
+ * Default constructor. The <code>Content-Length</code> or <code>Transfer-Encoding</code>
+ * will cause the interceptor to throw {@link ProtocolException} if already present in the
+ * response message.
+ */
+ public ResponseContent() {
+ this(false);
+ }
+
+ /**
+ * Constructor that can be used to fine-tune behavior of this interceptor.
+ *
+ * @param overwrite If set to <code>true</code> the <code>Content-Length</code> and
+ * <code>Transfer-Encoding</code> headers will be created or updated if already present.
+ * If set to <code>false</code> the <code>Content-Length</code> and
+ * <code>Transfer-Encoding</code> headers will cause the interceptor to throw
+ * {@link ProtocolException} if already present in the response message.
+ *
+ * @since 4.2
+ */
+ public ResponseContent(final boolean overwrite) {
+ super();
+ this.overwrite = overwrite;
+ }
+
+ /**
+ * Processes the response (possibly updating or inserting) Content-Length and Transfer-Encoding headers.
+ * @param response The HttpResponse to modify.
+ * @param context Unused.
+ * @throws ProtocolException If either the Content-Length or Transfer-Encoding headers are found.
+ * @throws IllegalArgumentException If the response is null.
+ */
+ public void process(final HttpResponse response, final HttpContext context)
+ throws HttpException, IOException {
+ Args.notNull(response, "HTTP response");
+ if (this.overwrite) {
+ response.removeHeaders(HTTP.TRANSFER_ENCODING);
+ response.removeHeaders(HTTP.CONTENT_LEN);
+ } else {
+ if (response.containsHeader(HTTP.TRANSFER_ENCODING)) {
+ throw new ProtocolException("Transfer-encoding header already present");
+ }
+ if (response.containsHeader(HTTP.CONTENT_LEN)) {
+ throw new ProtocolException("Content-Length header already present");
+ }
+ }
+ final ProtocolVersion ver = response.getStatusLine().getProtocolVersion();
+ final HttpEntity entity = response.getEntity();
+ if (entity != null) {
+ final long len = entity.getContentLength();
+ if (entity.isChunked() && !ver.lessEquals(HttpVersion.HTTP_1_0)) {
+ response.addHeader(HTTP.TRANSFER_ENCODING, HTTP.CHUNK_CODING);
+ } else if (len >= 0) {
+ response.addHeader(HTTP.CONTENT_LEN, Long.toString(entity.getContentLength()));
+ }
+ // Specify a content type if known
+ if (entity.getContentType() != null && !response.containsHeader(
+ HTTP.CONTENT_TYPE )) {
+ response.addHeader(entity.getContentType());
+ }
+ // Specify a content encoding if known
+ if (entity.getContentEncoding() != null && !response.containsHeader(
+ HTTP.CONTENT_ENCODING)) {
+ response.addHeader(entity.getContentEncoding());
+ }
+ } else {
+ final int status = response.getStatusLine().getStatusCode();
+ if (status != HttpStatus.SC_NO_CONTENT
+ && status != HttpStatus.SC_NOT_MODIFIED
+ && status != HttpStatus.SC_RESET_CONTENT) {
+ response.addHeader(HTTP.CONTENT_LEN, "0");
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ResponseDate.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ResponseDate.java
new file mode 100644
index 0000000000..300ff761e3
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ResponseDate.java
@@ -0,0 +1,66 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpResponseInterceptor;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * ResponseDate is responsible for adding <code>Date<c/ode> header to the
+ * outgoing responses. This interceptor is recommended for server side protocol
+ * processors.
+ *
+ * @since 4.0
+ */
+@ThreadSafe
+public class ResponseDate implements HttpResponseInterceptor {
+
+ private static final HttpDateGenerator DATE_GENERATOR = new HttpDateGenerator();
+
+ public ResponseDate() {
+ super();
+ }
+
+ public void process(final HttpResponse response, final HttpContext context)
+ throws HttpException, IOException {
+ Args.notNull(response, "HTTP response");
+ final int status = response.getStatusLine().getStatusCode();
+ if ((status >= HttpStatus.SC_OK) &&
+ !response.containsHeader(HTTP.DATE_HEADER)) {
+ final String httpdate = DATE_GENERATOR.getCurrentDate();
+ response.setHeader(HTTP.DATE_HEADER, httpdate);
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ResponseServer.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ResponseServer.java
new file mode 100644
index 0000000000..f0672a0066
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/ResponseServer.java
@@ -0,0 +1,71 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import java.io.IOException;
+
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpResponseInterceptor;
+import ch.boye.httpclientandroidlib.annotation.Immutable;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * ResponseServer is responsible for adding <code>Server</code> header. This
+ * interceptor is recommended for server side protocol processors.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class ResponseServer implements HttpResponseInterceptor {
+
+ private final String originServer;
+
+ /**
+ * @since 4.3
+ */
+ public ResponseServer(final String originServer) {
+ super();
+ this.originServer = originServer;
+ }
+
+ public ResponseServer() {
+ this(null);
+ }
+
+ public void process(final HttpResponse response, final HttpContext context)
+ throws HttpException, IOException {
+ Args.notNull(response, "HTTP response");
+ if (!response.containsHeader(HTTP.SERVER_HEADER)) {
+ if (this.originServer != null) {
+ response.addHeader(HTTP.SERVER_HEADER, this.originServer);
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/SyncBasicHttpContext.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/SyncBasicHttpContext.java
new file mode 100644
index 0000000000..60138764b7
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/SyncBasicHttpContext.java
@@ -0,0 +1,74 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+/**
+ * Thread-safe extension of the {@link BasicHttpContext}.
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) HttpContext instances may not be shared by multiple threads
+ */
+@Deprecated
+public class SyncBasicHttpContext extends BasicHttpContext {
+
+ public SyncBasicHttpContext(final HttpContext parentContext) {
+ super(parentContext);
+ }
+
+ /**
+ * @since 4.2
+ */
+ public SyncBasicHttpContext() {
+ super();
+ }
+
+ @Override
+ public synchronized Object getAttribute(final String id) {
+ return super.getAttribute(id);
+ }
+
+ @Override
+ public synchronized void setAttribute(final String id, final Object obj) {
+ super.setAttribute(id, obj);
+ }
+
+ @Override
+ public synchronized Object removeAttribute(final String id) {
+ return super.removeAttribute(id);
+ }
+
+ /**
+ * @since 4.2
+ */
+ @Override
+ public synchronized void clear() {
+ super.clear();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/UriHttpRequestHandlerMapper.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/UriHttpRequestHandlerMapper.java
new file mode 100644
index 0000000000..f09dea6114
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/UriHttpRequestHandlerMapper.java
@@ -0,0 +1,115 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import ch.boye.httpclientandroidlib.HttpRequest;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Maintains a map of HTTP request handlers keyed by a request URI pattern.
+ * <br>
+ * Patterns may have three formats:
+ * <ul>
+ * <li><code>*</code></li>
+ * <li><code>*&lt;uri&gt;</code></li>
+ * <li><code>&lt;uri&gt;*</code></li>
+ * </ul>
+ * <br>
+ * This class can be used to map an instance of
+ * {@link HttpRequestHandler} matching a particular request URI. Usually the
+ * mapped request handler will be used to process the request with the
+ * specified request URI.
+ *
+ * @since 4.3
+ */
+@ThreadSafe // provided injected dependencies are thread-safe
+public class UriHttpRequestHandlerMapper implements HttpRequestHandlerMapper {
+
+ private final UriPatternMatcher<HttpRequestHandler> matcher;
+
+ protected UriHttpRequestHandlerMapper(final UriPatternMatcher<HttpRequestHandler> matcher) {
+ super();
+ this.matcher = Args.notNull(matcher, "Pattern matcher");
+ }
+
+ public UriHttpRequestHandlerMapper() {
+ this(new UriPatternMatcher<HttpRequestHandler>());
+ }
+
+ /**
+ * Registers the given {@link HttpRequestHandler} as a handler for URIs
+ * matching the given pattern.
+ *
+ * @param pattern the pattern to register the handler for.
+ * @param handler the handler.
+ */
+ public void register(final String pattern, final HttpRequestHandler handler) {
+ Args.notNull(pattern, "Pattern");
+ Args.notNull(handler, "Handler");
+ matcher.register(pattern, handler);
+ }
+
+ /**
+ * Removes registered handler, if exists, for the given pattern.
+ *
+ * @param pattern the pattern to unregister the handler for.
+ */
+ public void unregister(final String pattern) {
+ matcher.unregister(pattern);
+ }
+
+ /**
+ * Extracts request path from the given {@link HttpRequest}
+ */
+ protected String getRequestPath(final HttpRequest request) {
+ String uriPath = request.getRequestLine().getUri();
+ int index = uriPath.indexOf("?");
+ if (index != -1) {
+ uriPath = uriPath.substring(0, index);
+ } else {
+ index = uriPath.indexOf("#");
+ if (index != -1) {
+ uriPath = uriPath.substring(0, index);
+ }
+ }
+ return uriPath;
+ }
+
+ /**
+ * Looks up a handler matching the given request URI.
+ *
+ * @param request the request
+ * @return handler or <code>null</code> if no match is found.
+ */
+ public HttpRequestHandler lookup(final HttpRequest request) {
+ Args.notNull(request, "HTTP request");
+ return matcher.lookup(getRequestPath(request));
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/UriPatternMatcher.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/UriPatternMatcher.java
new file mode 100644
index 0000000000..77c46a70c9
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/UriPatternMatcher.java
@@ -0,0 +1,165 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.protocol;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import ch.boye.httpclientandroidlib.annotation.GuardedBy;
+import ch.boye.httpclientandroidlib.annotation.ThreadSafe;
+import ch.boye.httpclientandroidlib.util.Args;
+
+/**
+ * Maintains a map of objects keyed by a request URI pattern.
+ * <br>
+ * Patterns may have three formats:
+ * <ul>
+ * <li><code>*</code></li>
+ * <li><code>*&lt;uri&gt;</code></li>
+ * <li><code>&lt;uri&gt;*</code></li>
+ * </ul>
+ * <br>
+ * This class can be used to resolve an object matching a particular request
+ * URI.
+ *
+ * @since 4.0
+ */
+@ThreadSafe
+public class UriPatternMatcher<T> {
+
+ @GuardedBy("this")
+ private final Map<String, T> map;
+
+ public UriPatternMatcher() {
+ super();
+ this.map = new HashMap<String, T>();
+ }
+
+ /**
+ * Registers the given object for URIs matching the given pattern.
+ *
+ * @param pattern the pattern to register the handler for.
+ * @param obj the object.
+ */
+ public synchronized void register(final String pattern, final T obj) {
+ Args.notNull(pattern, "URI request pattern");
+ this.map.put(pattern, obj);
+ }
+
+ /**
+ * Removes registered object, if exists, for the given pattern.
+ *
+ * @param pattern the pattern to unregister.
+ */
+ public synchronized void unregister(final String pattern) {
+ if (pattern == null) {
+ return;
+ }
+ this.map.remove(pattern);
+ }
+
+ /**
+ * @deprecated (4.1) do not use
+ */
+ @Deprecated
+ public synchronized void setHandlers(final Map<String, T> map) {
+ Args.notNull(map, "Map of handlers");
+ this.map.clear();
+ this.map.putAll(map);
+ }
+
+ /**
+ * @deprecated (4.1) do not use
+ */
+ @Deprecated
+ public synchronized void setObjects(final Map<String, T> map) {
+ Args.notNull(map, "Map of handlers");
+ this.map.clear();
+ this.map.putAll(map);
+ }
+
+ /**
+ * @deprecated (4.1) do not use
+ */
+ @Deprecated
+ public synchronized Map<String, T> getObjects() {
+ return this.map;
+ }
+
+ /**
+ * Looks up an object matching the given request path.
+ *
+ * @param path the request path
+ * @return object or <code>null</code> if no match is found.
+ */
+ public synchronized T lookup(final String path) {
+ Args.notNull(path, "Request path");
+ // direct match?
+ T obj = this.map.get(path);
+ if (obj == null) {
+ // pattern match?
+ String bestMatch = null;
+ for (final String pattern : this.map.keySet()) {
+ if (matchUriRequestPattern(pattern, path)) {
+ // we have a match. is it any better?
+ if (bestMatch == null
+ || (bestMatch.length() < pattern.length())
+ || (bestMatch.length() == pattern.length() && pattern.endsWith("*"))) {
+ obj = this.map.get(pattern);
+ bestMatch = pattern;
+ }
+ }
+ }
+ }
+ return obj;
+ }
+
+ /**
+ * Tests if the given request path matches the given pattern.
+ *
+ * @param pattern the pattern
+ * @param path the request path
+ * @return <code>true</code> if the request URI matches the pattern,
+ * <code>false</code> otherwise.
+ */
+ protected boolean matchUriRequestPattern(final String pattern, final String path) {
+ if (pattern.equals("*")) {
+ return true;
+ } else {
+ return
+ (pattern.endsWith("*") && path.startsWith(pattern.substring(0, pattern.length() - 1))) ||
+ (pattern.startsWith("*") && path.endsWith(pattern.substring(1, pattern.length())));
+ }
+ }
+
+ @Override
+ public String toString() {
+ return this.map.toString();
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/package-info.java
new file mode 100644
index 0000000000..97fcbfdc18
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/protocol/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Core HTTP protocol execution framework and HTTP protocol handlers
+ * for synchronous, blocking communication.
+ */
+package ch.boye.httpclientandroidlib.protocol;
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/Args.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/Args.java
new file mode 100644
index 0000000000..16e6ec4bfb
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/Args.java
@@ -0,0 +1,111 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.util;
+
+import java.util.Collection;
+
+public class Args {
+
+ public static void check(final boolean expression, final String message) {
+ if (!expression) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ public static void check(final boolean expression, final String message, final Object... args) {
+ if (!expression) {
+ throw new IllegalArgumentException(String.format(message, args));
+ }
+ }
+
+ public static <T> T notNull(final T argument, final String name) {
+ if (argument == null) {
+ throw new IllegalArgumentException(name + " may not be null");
+ }
+ return argument;
+ }
+
+ public static <T extends CharSequence> T notEmpty(final T argument, final String name) {
+ if (argument == null) {
+ throw new IllegalArgumentException(name + " may not be null");
+ }
+ if (TextUtils.isEmpty(argument)) {
+ throw new IllegalArgumentException(name + " may not be empty");
+ }
+ return argument;
+ }
+
+ public static <T extends CharSequence> T notBlank(final T argument, final String name) {
+ if (argument == null) {
+ throw new IllegalArgumentException(name + " may not be null");
+ }
+ if (TextUtils.isBlank(argument)) {
+ throw new IllegalArgumentException(name + " may not be blank");
+ }
+ return argument;
+ }
+
+ public static <E, T extends Collection<E>> T notEmpty(final T argument, final String name) {
+ if (argument == null) {
+ throw new IllegalArgumentException(name + " may not be null");
+ }
+ if (argument.isEmpty()) {
+ throw new IllegalArgumentException(name + " may not be empty");
+ }
+ return argument;
+ }
+
+ public static int positive(final int n, final String name) {
+ if (n <= 0) {
+ throw new IllegalArgumentException(name + " may not be negative or zero");
+ }
+ return n;
+ }
+
+ public static long positive(final long n, final String name) {
+ if (n <= 0) {
+ throw new IllegalArgumentException(name + " may not be negative or zero");
+ }
+ return n;
+ }
+
+ public static int notNegative(final int n, final String name) {
+ if (n < 0) {
+ throw new IllegalArgumentException(name + " may not be negative");
+ }
+ return n;
+ }
+
+ public static long notNegative(final long n, final String name) {
+ if (n < 0) {
+ throw new IllegalArgumentException(name + " may not be negative");
+ }
+ return n;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/Asserts.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/Asserts.java
new file mode 100644
index 0000000000..4515e15777
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/Asserts.java
@@ -0,0 +1,62 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.util;
+
+public class Asserts {
+
+ public static void check(final boolean expression, final String message) {
+ if (!expression) {
+ throw new IllegalStateException(message);
+ }
+ }
+
+ public static void check(final boolean expression, final String message, final Object... args) {
+ if (!expression) {
+ throw new IllegalStateException(String.format(message, args));
+ }
+ }
+
+ public static void notNull(final Object object, final String name) {
+ if (object == null) {
+ throw new IllegalStateException(name + " is null");
+ }
+ }
+
+ public static void notEmpty(final CharSequence s, final String name) {
+ if (TextUtils.isEmpty(s)) {
+ throw new IllegalStateException(name + " is empty");
+ }
+ }
+
+ public static void notBlank(final CharSequence s, final String name) {
+ if (TextUtils.isBlank(s)) {
+ throw new IllegalStateException(name + " is blank");
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/ByteArrayBuffer.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/ByteArrayBuffer.java
new file mode 100644
index 0000000000..cbf7c943da
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/ByteArrayBuffer.java
@@ -0,0 +1,347 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.util;
+
+import java.io.Serializable;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+
+/**
+ * A resizable byte array.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public final class ByteArrayBuffer implements Serializable {
+
+ private static final long serialVersionUID = 4359112959524048036L;
+
+ private byte[] buffer;
+ private int len;
+
+ /**
+ * Creates an instance of {@link ByteArrayBuffer} with the given initial
+ * capacity.
+ *
+ * @param capacity the capacity
+ */
+ public ByteArrayBuffer(final int capacity) {
+ super();
+ Args.notNegative(capacity, "Buffer capacity");
+ this.buffer = new byte[capacity];
+ }
+
+ private void expand(final int newlen) {
+ final byte newbuffer[] = new byte[Math.max(this.buffer.length << 1, newlen)];
+ System.arraycopy(this.buffer, 0, newbuffer, 0, this.len);
+ this.buffer = newbuffer;
+ }
+
+ /**
+ * Appends <code>len</code> bytes to this buffer from the given source
+ * array starting at index <code>off</code>. The capacity of the buffer
+ * is increased, if necessary, to accommodate all <code>len</code> bytes.
+ *
+ * @param b the bytes to be appended.
+ * @param off the index of the first byte to append.
+ * @param len the number of bytes to append.
+ * @throws IndexOutOfBoundsException if <code>off</code> if out of
+ * range, <code>len</code> is negative, or
+ * <code>off</code> + <code>len</code> is out of range.
+ */
+ public void append(final byte[] b, final int off, final int len) {
+ if (b == null) {
+ return;
+ }
+ if ((off < 0) || (off > b.length) || (len < 0) ||
+ ((off + len) < 0) || ((off + len) > b.length)) {
+ throw new IndexOutOfBoundsException("off: "+off+" len: "+len+" b.length: "+b.length);
+ }
+ if (len == 0) {
+ return;
+ }
+ final int newlen = this.len + len;
+ if (newlen > this.buffer.length) {
+ expand(newlen);
+ }
+ System.arraycopy(b, off, this.buffer, this.len, len);
+ this.len = newlen;
+ }
+
+ /**
+ * Appends <code>b</code> byte to this buffer. The capacity of the buffer
+ * is increased, if necessary, to accommodate the additional byte.
+ *
+ * @param b the byte to be appended.
+ */
+ public void append(final int b) {
+ final int newlen = this.len + 1;
+ if (newlen > this.buffer.length) {
+ expand(newlen);
+ }
+ this.buffer[this.len] = (byte)b;
+ this.len = newlen;
+ }
+
+ /**
+ * Appends <code>len</code> chars to this buffer from the given source
+ * array starting at index <code>off</code>. The capacity of the buffer
+ * is increased if necessary to accommodate all <code>len</code> chars.
+ * <p>
+ * The chars are converted to bytes using simple cast.
+ *
+ * @param b the chars to be appended.
+ * @param off the index of the first char to append.
+ * @param len the number of bytes to append.
+ * @throws IndexOutOfBoundsException if <code>off</code> if out of
+ * range, <code>len</code> is negative, or
+ * <code>off</code> + <code>len</code> is out of range.
+ */
+ public void append(final char[] b, final int off, final int len) {
+ if (b == null) {
+ return;
+ }
+ if ((off < 0) || (off > b.length) || (len < 0) ||
+ ((off + len) < 0) || ((off + len) > b.length)) {
+ throw new IndexOutOfBoundsException("off: "+off+" len: "+len+" b.length: "+b.length);
+ }
+ if (len == 0) {
+ return;
+ }
+ final int oldlen = this.len;
+ final int newlen = oldlen + len;
+ if (newlen > this.buffer.length) {
+ expand(newlen);
+ }
+ for (int i1 = off, i2 = oldlen; i2 < newlen; i1++, i2++) {
+ this.buffer[i2] = (byte) b[i1];
+ }
+ this.len = newlen;
+ }
+
+ /**
+ * Appends <code>len</code> chars to this buffer from the given source
+ * char array buffer starting at index <code>off</code>. The capacity
+ * of the buffer is increased if necessary to accommodate all
+ * <code>len</code> chars.
+ * <p>
+ * The chars are converted to bytes using simple cast.
+ *
+ * @param b the chars to be appended.
+ * @param off the index of the first char to append.
+ * @param len the number of bytes to append.
+ * @throws IndexOutOfBoundsException if <code>off</code> if out of
+ * range, <code>len</code> is negative, or
+ * <code>off</code> + <code>len</code> is out of range.
+ */
+ public void append(final CharArrayBuffer b, final int off, final int len) {
+ if (b == null) {
+ return;
+ }
+ append(b.buffer(), off, len);
+ }
+
+ /**
+ * Clears content of the buffer. The underlying byte array is not resized.
+ */
+ public void clear() {
+ this.len = 0;
+ }
+
+ /**
+ * Converts the content of this buffer to an array of bytes.
+ *
+ * @return byte array
+ */
+ public byte[] toByteArray() {
+ final byte[] b = new byte[this.len];
+ if (this.len > 0) {
+ System.arraycopy(this.buffer, 0, b, 0, this.len);
+ }
+ return b;
+ }
+
+ /**
+ * Returns the <code>byte</code> value in this buffer at the specified
+ * index. The index argument must be greater than or equal to
+ * <code>0</code>, and less than the length of this buffer.
+ *
+ * @param i the index of the desired byte value.
+ * @return the byte value at the specified index.
+ * @throws IndexOutOfBoundsException if <code>index</code> is
+ * negative or greater than or equal to {@link #length()}.
+ */
+ public int byteAt(final int i) {
+ return this.buffer[i];
+ }
+
+ /**
+ * Returns the current capacity. The capacity is the amount of storage
+ * available for newly appended bytes, beyond which an allocation
+ * will occur.
+ *
+ * @return the current capacity
+ */
+ public int capacity() {
+ return this.buffer.length;
+ }
+
+ /**
+ * Returns the length of the buffer (byte count).
+ *
+ * @return the length of the buffer
+ */
+ public int length() {
+ return this.len;
+ }
+
+ /**
+ * Ensures that the capacity is at least equal to the specified minimum.
+ * If the current capacity is less than the argument, then a new internal
+ * array is allocated with greater capacity. If the <code>required</code>
+ * argument is non-positive, this method takes no action.
+ *
+ * @param required the minimum required capacity.
+ *
+ * @since 4.1
+ */
+ public void ensureCapacity(final int required) {
+ if (required <= 0) {
+ return;
+ }
+ final int available = this.buffer.length - this.len;
+ if (required > available) {
+ expand(this.len + required);
+ }
+ }
+
+ /**
+ * Returns reference to the underlying byte array.
+ *
+ * @return the byte array.
+ */
+ public byte[] buffer() {
+ return this.buffer;
+ }
+
+ /**
+ * Sets the length of the buffer. The new length value is expected to be
+ * less than the current capacity and greater than or equal to
+ * <code>0</code>.
+ *
+ * @param len the new length
+ * @throws IndexOutOfBoundsException if the
+ * <code>len</code> argument is greater than the current
+ * capacity of the buffer or less than <code>0</code>.
+ */
+ public void setLength(final int len) {
+ if (len < 0 || len > this.buffer.length) {
+ throw new IndexOutOfBoundsException("len: "+len+" < 0 or > buffer len: "+this.buffer.length);
+ }
+ this.len = len;
+ }
+
+ /**
+ * Returns <code>true</code> if this buffer is empty, that is, its
+ * {@link #length()} is equal to <code>0</code>.
+ * @return <code>true</code> if this buffer is empty, <code>false</code>
+ * otherwise.
+ */
+ public boolean isEmpty() {
+ return this.len == 0;
+ }
+
+ /**
+ * Returns <code>true</code> if this buffer is full, that is, its
+ * {@link #length()} is equal to its {@link #capacity()}.
+ * @return <code>true</code> if this buffer is full, <code>false</code>
+ * otherwise.
+ */
+ public boolean isFull() {
+ return this.len == this.buffer.length;
+ }
+
+ /**
+ * Returns the index within this buffer of the first occurrence of the
+ * specified byte, starting the search at the specified
+ * <code>beginIndex</code> and finishing at <code>endIndex</code>.
+ * If no such byte occurs in this buffer within the specified bounds,
+ * <code>-1</code> is returned.
+ * <p>
+ * There is no restriction on the value of <code>beginIndex</code> and
+ * <code>endIndex</code>. If <code>beginIndex</code> is negative,
+ * it has the same effect as if it were zero. If <code>endIndex</code> is
+ * greater than {@link #length()}, it has the same effect as if it were
+ * {@link #length()}. If the <code>beginIndex</code> is greater than
+ * the <code>endIndex</code>, <code>-1</code> is returned.
+ *
+ * @param b the byte to search for.
+ * @param from the index to start the search from.
+ * @param to the index to finish the search at.
+ * @return the index of the first occurrence of the byte in the buffer
+ * within the given bounds, or <code>-1</code> if the byte does
+ * not occur.
+ *
+ * @since 4.1
+ */
+ public int indexOf(final byte b, final int from, final int to) {
+ int beginIndex = from;
+ if (beginIndex < 0) {
+ beginIndex = 0;
+ }
+ int endIndex = to;
+ if (endIndex > this.len) {
+ endIndex = this.len;
+ }
+ if (beginIndex > endIndex) {
+ return -1;
+ }
+ for (int i = beginIndex; i < endIndex; i++) {
+ if (this.buffer[i] == b) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the index within this buffer of the first occurrence of the
+ * specified byte, starting the search at <code>0</code> and finishing
+ * at {@link #length()}. If no such byte occurs in this buffer within
+ * those bounds, <code>-1</code> is returned.
+ *
+ * @param b the byte to search for.
+ * @return the index of the first occurrence of the byte in the
+ * buffer, or <code>-1</code> if the byte does not occur.
+ *
+ * @since 4.1
+ */
+ public int indexOf(final byte b) {
+ return indexOf(b, 0, this.len);
+ }
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/CharArrayBuffer.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/CharArrayBuffer.java
new file mode 100644
index 0000000000..88dc36c7a9
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/CharArrayBuffer.java
@@ -0,0 +1,464 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.util;
+
+import java.io.Serializable;
+
+import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+
+/**
+ * A resizable char array.
+ *
+ * @since 4.0
+ */
+@NotThreadSafe
+public final class CharArrayBuffer implements Serializable {
+
+ private static final long serialVersionUID = -6208952725094867135L;
+
+ private char[] buffer;
+ private int len;
+
+ /**
+ * Creates an instance of {@link CharArrayBuffer} with the given initial
+ * capacity.
+ *
+ * @param capacity the capacity
+ */
+ public CharArrayBuffer(final int capacity) {
+ super();
+ Args.notNegative(capacity, "Buffer capacity");
+ this.buffer = new char[capacity];
+ }
+
+ private void expand(final int newlen) {
+ final char newbuffer[] = new char[Math.max(this.buffer.length << 1, newlen)];
+ System.arraycopy(this.buffer, 0, newbuffer, 0, this.len);
+ this.buffer = newbuffer;
+ }
+
+ /**
+ * Appends <code>len</code> chars to this buffer from the given source
+ * array starting at index <code>off</code>. The capacity of the buffer
+ * is increased, if necessary, to accommodate all <code>len</code> chars.
+ *
+ * @param b the chars to be appended.
+ * @param off the index of the first char to append.
+ * @param len the number of chars to append.
+ * @throws IndexOutOfBoundsException if <code>off</code> is out of
+ * range, <code>len</code> is negative, or
+ * <code>off</code> + <code>len</code> is out of range.
+ */
+ public void append(final char[] b, final int off, final int len) {
+ if (b == null) {
+ return;
+ }
+ if ((off < 0) || (off > b.length) || (len < 0) ||
+ ((off + len) < 0) || ((off + len) > b.length)) {
+ throw new IndexOutOfBoundsException("off: "+off+" len: "+len+" b.length: "+b.length);
+ }
+ if (len == 0) {
+ return;
+ }
+ final int newlen = this.len + len;
+ if (newlen > this.buffer.length) {
+ expand(newlen);
+ }
+ System.arraycopy(b, off, this.buffer, this.len, len);
+ this.len = newlen;
+ }
+
+ /**
+ * Appends chars of the given string to this buffer. The capacity of the
+ * buffer is increased, if necessary, to accommodate all chars.
+ *
+ * @param str the string.
+ */
+ public void append(final String str) {
+ final String s = str != null ? str : "null";
+ final int strlen = s.length();
+ final int newlen = this.len + strlen;
+ if (newlen > this.buffer.length) {
+ expand(newlen);
+ }
+ s.getChars(0, strlen, this.buffer, this.len);
+ this.len = newlen;
+ }
+
+ /**
+ * Appends <code>len</code> chars to this buffer from the given source
+ * buffer starting at index <code>off</code>. The capacity of the
+ * destination buffer is increased, if necessary, to accommodate all
+ * <code>len</code> chars.
+ *
+ * @param b the source buffer to be appended.
+ * @param off the index of the first char to append.
+ * @param len the number of chars to append.
+ * @throws IndexOutOfBoundsException if <code>off</code> is out of
+ * range, <code>len</code> is negative, or
+ * <code>off</code> + <code>len</code> is out of range.
+ */
+ public void append(final CharArrayBuffer b, final int off, final int len) {
+ if (b == null) {
+ return;
+ }
+ append(b.buffer, off, len);
+ }
+
+ /**
+ * Appends all chars to this buffer from the given source buffer starting
+ * at index <code>0</code>. The capacity of the destination buffer is
+ * increased, if necessary, to accommodate all {@link #length()} chars.
+ *
+ * @param b the source buffer to be appended.
+ */
+ public void append(final CharArrayBuffer b) {
+ if (b == null) {
+ return;
+ }
+ append(b.buffer,0, b.len);
+ }
+
+ /**
+ * Appends <code>ch</code> char to this buffer. The capacity of the buffer
+ * is increased, if necessary, to accommodate the additional char.
+ *
+ * @param ch the char to be appended.
+ */
+ public void append(final char ch) {
+ final int newlen = this.len + 1;
+ if (newlen > this.buffer.length) {
+ expand(newlen);
+ }
+ this.buffer[this.len] = ch;
+ this.len = newlen;
+ }
+
+ /**
+ * Appends <code>len</code> bytes to this buffer from the given source
+ * array starting at index <code>off</code>. The capacity of the buffer
+ * is increased, if necessary, to accommodate all <code>len</code> bytes.
+ * <p>
+ * The bytes are converted to chars using simple cast.
+ *
+ * @param b the bytes to be appended.
+ * @param off the index of the first byte to append.
+ * @param len the number of bytes to append.
+ * @throws IndexOutOfBoundsException if <code>off</code> is out of
+ * range, <code>len</code> is negative, or
+ * <code>off</code> + <code>len</code> is out of range.
+ */
+ public void append(final byte[] b, final int off, final int len) {
+ if (b == null) {
+ return;
+ }
+ if ((off < 0) || (off > b.length) || (len < 0) ||
+ ((off + len) < 0) || ((off + len) > b.length)) {
+ throw new IndexOutOfBoundsException("off: "+off+" len: "+len+" b.length: "+b.length);
+ }
+ if (len == 0) {
+ return;
+ }
+ final int oldlen = this.len;
+ final int newlen = oldlen + len;
+ if (newlen > this.buffer.length) {
+ expand(newlen);
+ }
+ for (int i1 = off, i2 = oldlen; i2 < newlen; i1++, i2++) {
+ this.buffer[i2] = (char) (b[i1] & 0xff);
+ }
+ this.len = newlen;
+ }
+
+ /**
+ * Appends <code>len</code> bytes to this buffer from the given source
+ * array starting at index <code>off</code>. The capacity of the buffer
+ * is increased, if necessary, to accommodate all <code>len</code> bytes.
+ * <p>
+ * The bytes are converted to chars using simple cast.
+ *
+ * @param b the bytes to be appended.
+ * @param off the index of the first byte to append.
+ * @param len the number of bytes to append.
+ * @throws IndexOutOfBoundsException if <code>off</code> is out of
+ * range, <code>len</code> is negative, or
+ * <code>off</code> + <code>len</code> is out of range.
+ */
+ public void append(final ByteArrayBuffer b, final int off, final int len) {
+ if (b == null) {
+ return;
+ }
+ append(b.buffer(), off, len);
+ }
+
+ /**
+ * Appends chars of the textual representation of the given object to this
+ * buffer. The capacity of the buffer is increased, if necessary, to
+ * accommodate all chars.
+ *
+ * @param obj the object.
+ */
+ public void append(final Object obj) {
+ append(String.valueOf(obj));
+ }
+
+ /**
+ * Clears content of the buffer. The underlying char array is not resized.
+ */
+ public void clear() {
+ this.len = 0;
+ }
+
+ /**
+ * Converts the content of this buffer to an array of chars.
+ *
+ * @return char array
+ */
+ public char[] toCharArray() {
+ final char[] b = new char[this.len];
+ if (this.len > 0) {
+ System.arraycopy(this.buffer, 0, b, 0, this.len);
+ }
+ return b;
+ }
+
+ /**
+ * Returns the <code>char</code> value in this buffer at the specified
+ * index. The index argument must be greater than or equal to
+ * <code>0</code>, and less than the length of this buffer.
+ *
+ * @param i the index of the desired char value.
+ * @return the char value at the specified index.
+ * @throws IndexOutOfBoundsException if <code>index</code> is
+ * negative or greater than or equal to {@link #length()}.
+ */
+ public char charAt(final int i) {
+ return this.buffer[i];
+ }
+
+ /**
+ * Returns reference to the underlying char array.
+ *
+ * @return the char array.
+ */
+ public char[] buffer() {
+ return this.buffer;
+ }
+
+ /**
+ * Returns the current capacity. The capacity is the amount of storage
+ * available for newly appended chars, beyond which an allocation will
+ * occur.
+ *
+ * @return the current capacity
+ */
+ public int capacity() {
+ return this.buffer.length;
+ }
+
+ /**
+ * Returns the length of the buffer (char count).
+ *
+ * @return the length of the buffer
+ */
+ public int length() {
+ return this.len;
+ }
+
+ /**
+ * Ensures that the capacity is at least equal to the specified minimum.
+ * If the current capacity is less than the argument, then a new internal
+ * array is allocated with greater capacity. If the <code>required</code>
+ * argument is non-positive, this method takes no action.
+ *
+ * @param required the minimum required capacity.
+ */
+ public void ensureCapacity(final int required) {
+ if (required <= 0) {
+ return;
+ }
+ final int available = this.buffer.length - this.len;
+ if (required > available) {
+ expand(this.len + required);
+ }
+ }
+
+ /**
+ * Sets the length of the buffer. The new length value is expected to be
+ * less than the current capacity and greater than or equal to
+ * <code>0</code>.
+ *
+ * @param len the new length
+ * @throws IndexOutOfBoundsException if the
+ * <code>len</code> argument is greater than the current
+ * capacity of the buffer or less than <code>0</code>.
+ */
+ public void setLength(final int len) {
+ if (len < 0 || len > this.buffer.length) {
+ throw new IndexOutOfBoundsException("len: "+len+" < 0 or > buffer len: "+this.buffer.length);
+ }
+ this.len = len;
+ }
+
+ /**
+ * Returns <code>true</code> if this buffer is empty, that is, its
+ * {@link #length()} is equal to <code>0</code>.
+ * @return <code>true</code> if this buffer is empty, <code>false</code>
+ * otherwise.
+ */
+ public boolean isEmpty() {
+ return this.len == 0;
+ }
+
+ /**
+ * Returns <code>true</code> if this buffer is full, that is, its
+ * {@link #length()} is equal to its {@link #capacity()}.
+ * @return <code>true</code> if this buffer is full, <code>false</code>
+ * otherwise.
+ */
+ public boolean isFull() {
+ return this.len == this.buffer.length;
+ }
+
+ /**
+ * Returns the index within this buffer of the first occurrence of the
+ * specified character, starting the search at the specified
+ * <code>beginIndex</code> and finishing at <code>endIndex</code>.
+ * If no such character occurs in this buffer within the specified bounds,
+ * <code>-1</code> is returned.
+ * <p>
+ * There is no restriction on the value of <code>beginIndex</code> and
+ * <code>endIndex</code>. If <code>beginIndex</code> is negative,
+ * it has the same effect as if it were zero. If <code>endIndex</code> is
+ * greater than {@link #length()}, it has the same effect as if it were
+ * {@link #length()}. If the <code>beginIndex</code> is greater than
+ * the <code>endIndex</code>, <code>-1</code> is returned.
+ *
+ * @param ch the char to search for.
+ * @param from the index to start the search from.
+ * @param to the index to finish the search at.
+ * @return the index of the first occurrence of the character in the buffer
+ * within the given bounds, or <code>-1</code> if the character does
+ * not occur.
+ */
+ public int indexOf(final int ch, final int from, final int to) {
+ int beginIndex = from;
+ if (beginIndex < 0) {
+ beginIndex = 0;
+ }
+ int endIndex = to;
+ if (endIndex > this.len) {
+ endIndex = this.len;
+ }
+ if (beginIndex > endIndex) {
+ return -1;
+ }
+ for (int i = beginIndex; i < endIndex; i++) {
+ if (this.buffer[i] == ch) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the index within this buffer of the first occurrence of the
+ * specified character, starting the search at <code>0</code> and finishing
+ * at {@link #length()}. If no such character occurs in this buffer within
+ * those bounds, <code>-1</code> is returned.
+ *
+ * @param ch the char to search for.
+ * @return the index of the first occurrence of the character in the
+ * buffer, or <code>-1</code> if the character does not occur.
+ */
+ public int indexOf(final int ch) {
+ return indexOf(ch, 0, this.len);
+ }
+
+ /**
+ * Returns a substring of this buffer. The substring begins at the specified
+ * <code>beginIndex</code> and extends to the character at index
+ * <code>endIndex - 1</code>.
+ *
+ * @param beginIndex the beginning index, inclusive.
+ * @param endIndex the ending index, exclusive.
+ * @return the specified substring.
+ * @exception StringIndexOutOfBoundsException if the
+ * <code>beginIndex</code> is negative, or
+ * <code>endIndex</code> is larger than the length of this
+ * buffer, or <code>beginIndex</code> is larger than
+ * <code>endIndex</code>.
+ */
+ public String substring(final int beginIndex, final int endIndex) {
+ return new String(this.buffer, beginIndex, endIndex - beginIndex);
+ }
+
+ /**
+ * Returns a substring of this buffer with leading and trailing whitespace
+ * omitted. The substring begins with the first non-whitespace character
+ * from <code>beginIndex</code> and extends to the last
+ * non-whitespace character with the index lesser than
+ * <code>endIndex</code>.
+ *
+ * @param from the beginning index, inclusive.
+ * @param to the ending index, exclusive.
+ * @return the specified substring.
+ * @exception IndexOutOfBoundsException if the
+ * <code>beginIndex</code> is negative, or
+ * <code>endIndex</code> is larger than the length of this
+ * buffer, or <code>beginIndex</code> is larger than
+ * <code>endIndex</code>.
+ */
+ public String substringTrimmed(final int from, final int to) {
+ int beginIndex = from;
+ int endIndex = to;
+ if (beginIndex < 0) {
+ throw new IndexOutOfBoundsException("Negative beginIndex: "+beginIndex);
+ }
+ if (endIndex > this.len) {
+ throw new IndexOutOfBoundsException("endIndex: "+endIndex+" > length: "+this.len);
+ }
+ if (beginIndex > endIndex) {
+ throw new IndexOutOfBoundsException("beginIndex: "+beginIndex+" > endIndex: "+endIndex);
+ }
+ while (beginIndex < endIndex && HTTP.isWhitespace(this.buffer[beginIndex])) {
+ beginIndex++;
+ }
+ while (endIndex > beginIndex && HTTP.isWhitespace(this.buffer[endIndex - 1])) {
+ endIndex--;
+ }
+ return new String(this.buffer, beginIndex, endIndex - beginIndex);
+ }
+
+ @Override
+ public String toString() {
+ return new String(this.buffer, 0, this.len);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/CharsetUtils.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/CharsetUtils.java
new file mode 100644
index 0000000000..e6e75e0ad6
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/CharsetUtils.java
@@ -0,0 +1,58 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.util;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.UnsupportedCharsetException;
+
+public class CharsetUtils {
+
+ public static Charset lookup(final String name) {
+ if (name == null) {
+ return null;
+ }
+ try {
+ return Charset.forName(name);
+ } catch (final UnsupportedCharsetException ex) {
+ return null;
+ }
+ }
+
+ public static Charset get(final String name) throws UnsupportedEncodingException {
+ if (name == null) {
+ return null;
+ }
+ try {
+ return Charset.forName(name);
+ } catch (final UnsupportedCharsetException ex) {
+ throw new UnsupportedEncodingException(name);
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/EncodingUtils.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/EncodingUtils.java
new file mode 100644
index 0000000000..964553909b
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/EncodingUtils.java
@@ -0,0 +1,152 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.util;
+
+import java.io.UnsupportedEncodingException;
+
+import ch.boye.httpclientandroidlib.Consts;
+
+/**
+ * The home for utility methods that handle various encoding tasks.
+ *
+ *
+ * @since 4.0
+ */
+public final class EncodingUtils {
+
+ /**
+ * Converts the byte array of HTTP content characters to a string. If
+ * the specified charset is not supported, default system encoding
+ * is used.
+ *
+ * @param data the byte array to be encoded
+ * @param offset the index of the first byte to encode
+ * @param length the number of bytes to encode
+ * @param charset the desired character encoding
+ * @return The result of the conversion.
+ */
+ public static String getString(
+ final byte[] data,
+ final int offset,
+ final int length,
+ final String charset) {
+ Args.notNull(data, "Input");
+ Args.notEmpty(charset, "Charset");
+ try {
+ return new String(data, offset, length, charset);
+ } catch (final UnsupportedEncodingException e) {
+ return new String(data, offset, length);
+ }
+ }
+
+
+ /**
+ * Converts the byte array of HTTP content characters to a string. If
+ * the specified charset is not supported, default system encoding
+ * is used.
+ *
+ * @param data the byte array to be encoded
+ * @param charset the desired character encoding
+ * @return The result of the conversion.
+ */
+ public static String getString(final byte[] data, final String charset) {
+ Args.notNull(data, "Input");
+ return getString(data, 0, data.length, charset);
+ }
+
+ /**
+ * Converts the specified string to a byte array. If the charset is not supported the
+ * default system charset is used.
+ *
+ * @param data the string to be encoded
+ * @param charset the desired character encoding
+ * @return The resulting byte array.
+ */
+ public static byte[] getBytes(final String data, final String charset) {
+ Args.notNull(data, "Input");
+ Args.notEmpty(charset, "Charset");
+ try {
+ return data.getBytes(charset);
+ } catch (final UnsupportedEncodingException e) {
+ return data.getBytes();
+ }
+ }
+
+ /**
+ * Converts the specified string to byte array of ASCII characters.
+ *
+ * @param data the string to be encoded
+ * @return The string as a byte array.
+ */
+ public static byte[] getAsciiBytes(final String data) {
+ Args.notNull(data, "Input");
+ try {
+ return data.getBytes(Consts.ASCII.name());
+ } catch (final UnsupportedEncodingException e) {
+ throw new Error("ASCII not supported");
+ }
+ }
+
+ /**
+ * Converts the byte array of ASCII characters to a string. This method is
+ * to be used when decoding content of HTTP elements (such as response
+ * headers)
+ *
+ * @param data the byte array to be encoded
+ * @param offset the index of the first byte to encode
+ * @param length the number of bytes to encode
+ * @return The string representation of the byte array
+ */
+ public static String getAsciiString(final byte[] data, final int offset, final int length) {
+ Args.notNull(data, "Input");
+ try {
+ return new String(data, offset, length, Consts.ASCII.name());
+ } catch (final UnsupportedEncodingException e) {
+ throw new Error("ASCII not supported");
+ }
+ }
+
+ /**
+ * Converts the byte array of ASCII characters to a string. This method is
+ * to be used when decoding content of HTTP elements (such as response
+ * headers)
+ *
+ * @param data the byte array to be encoded
+ * @return The string representation of the byte array
+ */
+ public static String getAsciiString(final byte[] data) {
+ Args.notNull(data, "Input");
+ return getAsciiString(data, 0, data.length);
+ }
+
+ /**
+ * This class should not be instantiated.
+ */
+ private EncodingUtils() {
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/EntityUtils.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/EntityUtils.java
new file mode 100644
index 0000000000..6bc19ef1e2
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/EntityUtils.java
@@ -0,0 +1,291 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.UnsupportedCharsetException;
+
+import ch.boye.httpclientandroidlib.HeaderElement;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.ParseException;
+import ch.boye.httpclientandroidlib.entity.ContentType;
+import ch.boye.httpclientandroidlib.protocol.HTTP;
+
+/**
+ * Static helpers for dealing with {@link HttpEntity}s.
+ *
+ * @since 4.0
+ */
+public final class EntityUtils {
+
+ private EntityUtils() {
+ }
+
+ /**
+ * Ensures that the entity content is fully consumed and the content stream, if exists,
+ * is closed. The process is done, <i>quietly</i> , without throwing any IOException.
+ *
+ * @param entity the entity to consume.
+ *
+ *
+ * @since 4.2
+ */
+ public static void consumeQuietly(final HttpEntity entity) {
+ try {
+ consume(entity);
+ } catch (final IOException ignore) {
+ }
+ }
+
+ /**
+ * Ensures that the entity content is fully consumed and the content stream, if exists,
+ * is closed.
+ *
+ * @param entity the entity to consume.
+ * @throws IOException if an error occurs reading the input stream
+ *
+ * @since 4.1
+ */
+ public static void consume(final HttpEntity entity) throws IOException {
+ if (entity == null) {
+ return;
+ }
+ if (entity.isStreaming()) {
+ final InputStream instream = entity.getContent();
+ if (instream != null) {
+ instream.close();
+ }
+ }
+ }
+
+ /**
+ * Updates an entity in a response by first consuming an existing entity, then setting the new one.
+ *
+ * @param response the response with an entity to update; must not be null.
+ * @param entity the entity to set in the response.
+ * @throws IOException if an error occurs while reading the input stream on the existing
+ * entity.
+ * @throws IllegalArgumentException if response is null.
+ *
+ * @since 4.3
+ */
+ public static void updateEntity(
+ final HttpResponse response, final HttpEntity entity) throws IOException {
+ Args.notNull(response, "Response");
+ consume(response.getEntity());
+ response.setEntity(entity);
+ }
+
+ /**
+ * Read the contents of an entity and return it as a byte array.
+ *
+ * @param entity the entity to read from=
+ * @return byte array containing the entity content. May be null if
+ * {@link HttpEntity#getContent()} is null.
+ * @throws IOException if an error occurs reading the input stream
+ * @throws IllegalArgumentException if entity is null or if content length > Integer.MAX_VALUE
+ */
+ public static byte[] toByteArray(final HttpEntity entity) throws IOException {
+ Args.notNull(entity, "Entity");
+ final InputStream instream = entity.getContent();
+ if (instream == null) {
+ return null;
+ }
+ try {
+ Args.check(entity.getContentLength() <= Integer.MAX_VALUE,
+ "HTTP entity too large to be buffered in memory");
+ int i = (int)entity.getContentLength();
+ if (i < 0) {
+ i = 4096;
+ }
+ final ByteArrayBuffer buffer = new ByteArrayBuffer(i);
+ final byte[] tmp = new byte[4096];
+ int l;
+ while((l = instream.read(tmp)) != -1) {
+ buffer.append(tmp, 0, l);
+ }
+ return buffer.toByteArray();
+ } finally {
+ instream.close();
+ }
+ }
+
+ /**
+ * Obtains character set of the entity, if known.
+ *
+ * @param entity must not be null
+ * @return the character set, or null if not found
+ * @throws ParseException if header elements cannot be parsed
+ * @throws IllegalArgumentException if entity is null
+ *
+ * @deprecated (4.1.3) use {@link ContentType#getOrDefault(HttpEntity)}
+ */
+ @Deprecated
+ public static String getContentCharSet(final HttpEntity entity) throws ParseException {
+ Args.notNull(entity, "Entity");
+ String charset = null;
+ if (entity.getContentType() != null) {
+ final HeaderElement values[] = entity.getContentType().getElements();
+ if (values.length > 0) {
+ final NameValuePair param = values[0].getParameterByName("charset");
+ if (param != null) {
+ charset = param.getValue();
+ }
+ }
+ }
+ return charset;
+ }
+
+ /**
+ * Obtains MIME type of the entity, if known.
+ *
+ * @param entity must not be null
+ * @return the character set, or null if not found
+ * @throws ParseException if header elements cannot be parsed
+ * @throws IllegalArgumentException if entity is null
+ *
+ * @since 4.1
+ *
+ * @deprecated (4.1.3) use {@link ContentType#getOrDefault(HttpEntity)}
+ */
+ @Deprecated
+ public static String getContentMimeType(final HttpEntity entity) throws ParseException {
+ Args.notNull(entity, "Entity");
+ String mimeType = null;
+ if (entity.getContentType() != null) {
+ final HeaderElement values[] = entity.getContentType().getElements();
+ if (values.length > 0) {
+ mimeType = values[0].getName();
+ }
+ }
+ return mimeType;
+ }
+
+ /**
+ * Get the entity content as a String, using the provided default character set
+ * if none is found in the entity.
+ * If defaultCharset is null, the default "ISO-8859-1" is used.
+ *
+ * @param entity must not be null
+ * @param defaultCharset character set to be applied if none found in the entity
+ * @return the entity content as a String. May be null if
+ * {@link HttpEntity#getContent()} is null.
+ * @throws ParseException if header elements cannot be parsed
+ * @throws IllegalArgumentException if entity is null or if content length > Integer.MAX_VALUE
+ * @throws IOException if an error occurs reading the input stream
+ * @throws UnsupportedCharsetException Thrown when the named charset is not available in
+ * this instance of the Java virtual machine
+ */
+ public static String toString(
+ final HttpEntity entity, final Charset defaultCharset) throws IOException, ParseException {
+ Args.notNull(entity, "Entity");
+ final InputStream instream = entity.getContent();
+ if (instream == null) {
+ return null;
+ }
+ try {
+ Args.check(entity.getContentLength() <= Integer.MAX_VALUE,
+ "HTTP entity too large to be buffered in memory");
+ int i = (int)entity.getContentLength();
+ if (i < 0) {
+ i = 4096;
+ }
+ Charset charset = null;
+ try {
+ final ContentType contentType = ContentType.get(entity);
+ if (contentType != null) {
+ charset = contentType.getCharset();
+ }
+ } catch (final UnsupportedCharsetException ex) {
+ throw new UnsupportedEncodingException(ex.getMessage());
+ }
+ if (charset == null) {
+ charset = defaultCharset;
+ }
+ if (charset == null) {
+ charset = HTTP.DEF_CONTENT_CHARSET;
+ }
+ final Reader reader = new InputStreamReader(instream, charset);
+ final CharArrayBuffer buffer = new CharArrayBuffer(i);
+ final char[] tmp = new char[1024];
+ int l;
+ while((l = reader.read(tmp)) != -1) {
+ buffer.append(tmp, 0, l);
+ }
+ return buffer.toString();
+ } finally {
+ instream.close();
+ }
+ }
+
+ /**
+ * Get the entity content as a String, using the provided default character set
+ * if none is found in the entity.
+ * If defaultCharset is null, the default "ISO-8859-1" is used.
+ *
+ * @param entity must not be null
+ * @param defaultCharset character set to be applied if none found in the entity
+ * @return the entity content as a String. May be null if
+ * {@link HttpEntity#getContent()} is null.
+ * @throws ParseException if header elements cannot be parsed
+ * @throws IllegalArgumentException if entity is null or if content length > Integer.MAX_VALUE
+ * @throws IOException if an error occurs reading the input stream
+ * @throws UnsupportedCharsetException Thrown when the named charset is not available in
+ * this instance of the Java virtual machine
+ */
+ public static String toString(
+ final HttpEntity entity, final String defaultCharset) throws IOException, ParseException {
+ return toString(entity, defaultCharset != null ? Charset.forName(defaultCharset) : null);
+ }
+
+ /**
+ * Read the contents of an entity and return it as a String.
+ * The content is converted using the character set from the entity (if any),
+ * failing that, "ISO-8859-1" is used.
+ *
+ * @param entity the entity to convert to a string; must not be null
+ * @return String containing the content.
+ * @throws ParseException if header elements cannot be parsed
+ * @throws IllegalArgumentException if entity is null or if content length > Integer.MAX_VALUE
+ * @throws IOException if an error occurs reading the input stream
+ * @throws UnsupportedCharsetException Thrown when the named charset is not available in
+ * this instance of the Java virtual machine
+ */
+ public static String toString(final HttpEntity entity)
+ throws IOException, ParseException {
+ return toString(entity, (Charset)null);
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/ExceptionUtils.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/ExceptionUtils.java
new file mode 100644
index 0000000000..a0c3001acc
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/ExceptionUtils.java
@@ -0,0 +1,82 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package ch.boye.httpclientandroidlib.util;
+
+import java.lang.reflect.Method;
+
+/**
+ * The home for utility methods that handle various exception-related tasks.
+ *
+ *
+ * @since 4.0
+ *
+ * @deprecated (4.2) no longer used
+ */
+@Deprecated
+public final class ExceptionUtils {
+
+ /** A reference to Throwable's initCause method, or null if it's not there in this JVM */
+ static private final Method INIT_CAUSE_METHOD = getInitCauseMethod();
+
+ /**
+ * Returns a <code>Method<code> allowing access to
+ * {@link Throwable#initCause(Throwable) initCause} method of {@link Throwable},
+ * or <code>null</code> if the method
+ * does not exist.
+ *
+ * @return A <code>Method<code> for <code>Throwable.initCause</code>, or
+ * <code>null</code> if unavailable.
+ */
+ static private Method getInitCauseMethod() {
+ try {
+ final Class<?>[] paramsClasses = new Class[] { Throwable.class };
+ return Throwable.class.getMethod("initCause", paramsClasses);
+ } catch (final NoSuchMethodException e) {
+ return null;
+ }
+ }
+
+ /**
+ * If we're running on JDK 1.4 or later, initialize the cause for the given throwable.
+ *
+ * @param throwable The throwable.
+ * @param cause The cause of the throwable.
+ */
+ public static void initCause(final Throwable throwable, final Throwable cause) {
+ if (INIT_CAUSE_METHOD != null) {
+ try {
+ INIT_CAUSE_METHOD.invoke(throwable, cause);
+ } catch (final Exception e) {
+ // Well, with no logging, the only option is to munch the exception
+ }
+ }
+ }
+
+ private ExceptionUtils() {
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/LangUtils.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/LangUtils.java
new file mode 100644
index 0000000000..9a4a29f342
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/LangUtils.java
@@ -0,0 +1,101 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.util;
+
+/**
+ * A set of utility methods to help produce consistent
+ * {@link Object#equals equals} and {@link Object#hashCode hashCode} methods.
+ *
+ *
+ * @since 4.0
+ */
+public final class LangUtils {
+
+ public static final int HASH_SEED = 17;
+ public static final int HASH_OFFSET = 37;
+
+ /** Disabled default constructor. */
+ private LangUtils() {
+ }
+
+ public static int hashCode(final int seed, final int hashcode) {
+ return seed * HASH_OFFSET + hashcode;
+ }
+
+ public static int hashCode(final int seed, final boolean b) {
+ return hashCode(seed, b ? 1 : 0);
+ }
+
+ public static int hashCode(final int seed, final Object obj) {
+ return hashCode(seed, obj != null ? obj.hashCode() : 0);
+ }
+
+ /**
+ * Check if two objects are equal.
+ *
+ * @param obj1 first object to compare, may be {@code null}
+ * @param obj2 second object to compare, may be {@code null}
+ * @return {@code true} if the objects are equal or both null
+ */
+ public static boolean equals(final Object obj1, final Object obj2) {
+ return obj1 == null ? obj2 == null : obj1.equals(obj2);
+ }
+
+ /**
+ * Check if two object arrays are equal.
+ * <p>
+ * <ul>
+ * <li>If both parameters are null, return {@code true}</li>
+ * <li>If one parameter is null, return {@code false}</li>
+ * <li>If the array lengths are different, return {@code false}</li>
+ * <li>Compare array elements using .equals(); return {@code false} if any comparisons fail.</li>
+ * <li>Return {@code true}</li>
+ * </ul>
+ *
+ * @param a1 first array to compare, may be {@code null}
+ * @param a2 second array to compare, may be {@code null}
+ * @return {@code true} if the arrays are equal or both null
+ */
+ public static boolean equals(final Object[] a1, final Object[] a2) {
+ if (a1 == null) {
+ return a2 == null;
+ } else {
+ if (a2 != null && a1.length == a2.length) {
+ for (int i = 0; i < a1.length; i++) {
+ if (!equals(a1[i], a2[i])) {
+ return false;
+ }
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/NetUtils.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/NetUtils.java
new file mode 100644
index 0000000000..8d0f4926a3
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/NetUtils.java
@@ -0,0 +1,54 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.util;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+
+/**
+ * @since 4.3
+ */
+public final class NetUtils {
+
+ public static void formatAddress(
+ final StringBuilder buffer,
+ final SocketAddress socketAddress) {
+ Args.notNull(buffer, "Buffer");
+ Args.notNull(socketAddress, "Socket address");
+ if (socketAddress instanceof InetSocketAddress) {
+ final InetSocketAddress socketaddr = ((InetSocketAddress) socketAddress);
+ final InetAddress inetaddr = socketaddr.getAddress();
+ buffer.append(inetaddr != null ? inetaddr.getHostAddress() : inetaddr)
+ .append(':').append(socketaddr.getPort());
+ } else {
+ buffer.append(socketAddress);
+ }
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/TextUtils.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/TextUtils.java
new file mode 100644
index 0000000000..52d8286ff3
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/TextUtils.java
@@ -0,0 +1,54 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.util;
+
+/**
+ * @since 4.3
+ */
+public final class TextUtils {
+
+ public static boolean isEmpty(final CharSequence s) {
+ if (s == null) {
+ return true;
+ }
+ return s.length() == 0;
+ }
+
+ public static boolean isBlank(final CharSequence s) {
+ if (s == null) {
+ return true;
+ }
+ for (int i = 0; i < s.length(); i++) {
+ if (!Character.isWhitespace(s.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+}
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/VersionInfo.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/VersionInfo.java
new file mode 100644
index 0000000000..61042459ae
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/VersionInfo.java
@@ -0,0 +1,324 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package ch.boye.httpclientandroidlib.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * Provides access to version information for HTTP components.
+ * Static methods are used to extract version information from property
+ * files that are automatically packaged with HTTP component release JARs.
+ * <br/>
+ * All available version information is provided in strings, where
+ * the string format is informal and subject to change without notice.
+ * Version information is provided for debugging output and interpretation
+ * by humans, not for automated processing in applications.
+ *
+ * @since 4.0
+ */
+public class VersionInfo {
+
+ /** A string constant for unavailable information. */
+ public final static String UNAVAILABLE = "UNAVAILABLE";
+
+ /** The filename of the version information files. */
+ public final static String VERSION_PROPERTY_FILE = "version.properties";
+
+ // the property names
+ public final static String PROPERTY_MODULE = "info.module";
+ public final static String PROPERTY_RELEASE = "info.release";
+ public final static String PROPERTY_TIMESTAMP = "info.timestamp";
+
+
+ /** The package that contains the version information. */
+ private final String infoPackage;
+
+ /** The module from the version info. */
+ private final String infoModule;
+
+ /** The release from the version info. */
+ private final String infoRelease;
+
+ /** The timestamp from the version info. */
+ private final String infoTimestamp;
+
+ /** The classloader from which the version info was obtained. */
+ private final String infoClassloader;
+
+
+ /**
+ * Instantiates version information.
+ *
+ * @param pckg the package
+ * @param module the module, or <code>null</code>
+ * @param release the release, or <code>null</code>
+ * @param time the build time, or <code>null</code>
+ * @param clsldr the class loader, or <code>null</code>
+ */
+ protected VersionInfo(final String pckg, final String module,
+ final String release, final String time, final String clsldr) {
+ Args.notNull(pckg, "Package identifier");
+ infoPackage = pckg;
+ infoModule = (module != null) ? module : UNAVAILABLE;
+ infoRelease = (release != null) ? release : UNAVAILABLE;
+ infoTimestamp = (time != null) ? time : UNAVAILABLE;
+ infoClassloader = (clsldr != null) ? clsldr : UNAVAILABLE;
+ }
+
+
+ /**
+ * Obtains the package name.
+ * The package name identifies the module or informal unit.
+ *
+ * @return the package name, never <code>null</code>
+ */
+ public final String getPackage() {
+ return infoPackage;
+ }
+
+ /**
+ * Obtains the name of the versioned module or informal unit.
+ * This data is read from the version information for the package.
+ *
+ * @return the module name, never <code>null</code>
+ */
+ public final String getModule() {
+ return infoModule;
+ }
+
+ /**
+ * Obtains the release of the versioned module or informal unit.
+ * This data is read from the version information for the package.
+ *
+ * @return the release version, never <code>null</code>
+ */
+ public final String getRelease() {
+ return infoRelease;
+ }
+
+ /**
+ * Obtains the timestamp of the versioned module or informal unit.
+ * This data is read from the version information for the package.
+ *
+ * @return the timestamp, never <code>null</code>
+ */
+ public final String getTimestamp() {
+ return infoTimestamp;
+ }
+
+ /**
+ * Obtains the classloader used to read the version information.
+ * This is just the <code>toString</code> output of the classloader,
+ * since the version information should not keep a reference to
+ * the classloader itself. That could prevent garbage collection.
+ *
+ * @return the classloader description, never <code>null</code>
+ */
+ public final String getClassloader() {
+ return infoClassloader;
+ }
+
+
+ /**
+ * Provides the version information in human-readable format.
+ *
+ * @return a string holding this version information
+ */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder
+ (20 + infoPackage.length() + infoModule.length() +
+ infoRelease.length() + infoTimestamp.length() +
+ infoClassloader.length());
+
+ sb.append("VersionInfo(")
+ .append(infoPackage).append(':').append(infoModule);
+
+ // If version info is missing, a single "UNAVAILABLE" for the module
+ // is sufficient. Everything else just clutters the output.
+ if (!UNAVAILABLE.equals(infoRelease)) {
+ sb.append(':').append(infoRelease);
+ }
+ if (!UNAVAILABLE.equals(infoTimestamp)) {
+ sb.append(':').append(infoTimestamp);
+ }
+
+ sb.append(')');
+
+ if (!UNAVAILABLE.equals(infoClassloader)) {
+ sb.append('@').append(infoClassloader);
+ }
+
+ return sb.toString();
+ }
+
+
+ /**
+ * Loads version information for a list of packages.
+ *
+ * @param pckgs the packages for which to load version info
+ * @param clsldr the classloader to load from, or
+ * <code>null</code> for the thread context classloader
+ *
+ * @return the version information for all packages found,
+ * never <code>null</code>
+ */
+ public static VersionInfo[] loadVersionInfo(final String[] pckgs,
+ final ClassLoader clsldr) {
+ Args.notNull(pckgs, "Package identifier array");
+ final List<VersionInfo> vil = new ArrayList<VersionInfo>(pckgs.length);
+ for (final String pckg : pckgs) {
+ final VersionInfo vi = loadVersionInfo(pckg, clsldr);
+ if (vi != null) {
+ vil.add(vi);
+ }
+ }
+
+ return vil.toArray(new VersionInfo[vil.size()]);
+ }
+
+
+ /**
+ * Loads version information for a package.
+ *
+ * @param pckg the package for which to load version information,
+ * for example "ch.boye.httpclientandroidlib".
+ * The package name should NOT end with a dot.
+ * @param clsldr the classloader to load from, or
+ * <code>null</code> for the thread context classloader
+ *
+ * @return the version information for the argument package, or
+ * <code>null</code> if not available
+ */
+ public static VersionInfo loadVersionInfo(final String pckg,
+ final ClassLoader clsldr) {
+ Args.notNull(pckg, "Package identifier");
+ final ClassLoader cl = clsldr != null ? clsldr : Thread.currentThread().getContextClassLoader();
+
+ Properties vip = null; // version info properties, if available
+ try {
+ // ch.boye.httpclientandroidlib becomes
+ // org/apache/http/version.properties
+ final InputStream is = cl.getResourceAsStream
+ (pckg.replace('.', '/') + "/" + VERSION_PROPERTY_FILE);
+ if (is != null) {
+ try {
+ final Properties props = new Properties();
+ props.load(is);
+ vip = props;
+ } finally {
+ is.close();
+ }
+ }
+ } catch (final IOException ex) {
+ // shamelessly munch this exception
+ }
+
+ VersionInfo result = null;
+ if (vip != null) {
+ result = fromMap(pckg, vip, cl);
+ }
+
+ return result;
+ }
+
+
+ /**
+ * Instantiates version information from properties.
+ *
+ * @param pckg the package for the version information
+ * @param info the map from string keys to string values,
+ * for example {@link java.util.Properties}
+ * @param clsldr the classloader, or <code>null</code>
+ *
+ * @return the version information
+ */
+ protected static VersionInfo fromMap(final String pckg, final Map<?, ?> info,
+ final ClassLoader clsldr) {
+ Args.notNull(pckg, "Package identifier");
+ String module = null;
+ String release = null;
+ String timestamp = null;
+
+ if (info != null) {
+ module = (String) info.get(PROPERTY_MODULE);
+ if ((module != null) && (module.length() < 1)) {
+ module = null;
+ }
+
+ release = (String) info.get(PROPERTY_RELEASE);
+ if ((release != null) && ((release.length() < 1) ||
+ (release.equals("${pom.version}")))) {
+ release = null;
+ }
+
+ timestamp = (String) info.get(PROPERTY_TIMESTAMP);
+ if ((timestamp != null) &&
+ ((timestamp.length() < 1) ||
+ (timestamp.equals("${mvn.timestamp}")))
+ ) {
+ timestamp = null;
+ }
+ } // if info
+
+ String clsldrstr = null;
+ if (clsldr != null) {
+ clsldrstr = clsldr.toString();
+ }
+
+ return new VersionInfo(pckg, module, release, timestamp, clsldrstr);
+ }
+
+ /**
+ * Sets the user agent to {@code "<name>/<release> (Java 1.5 minimum; Java/<java.version>)"}.
+ * <p/>
+ * For example:
+ * <pre>"Apache-HttpClient/4.3 (Java 1.5 minimum; Java/1.6.0_35)"</pre>
+ *
+ * @param name the component name, like "Apache-HttpClient".
+ * @param pkg
+ * the package for which to load version information, for example "ch.boye.httpclientandroidlib". The package name
+ * should NOT end with a dot.
+ * @param cls
+ * the class' class loader to load from, or <code>null</code> for the thread context class loader
+ * @since 4.3
+ */
+ public static String getUserAgent(final String name, final String pkg, final Class<?> cls) {
+ // determine the release version from packaged version info
+ final VersionInfo vi = VersionInfo.loadVersionInfo(pkg, cls.getClassLoader());
+ final String release = (vi != null) ? vi.getRelease() : VersionInfo.UNAVAILABLE;
+ final String javaVersion = System.getProperty("java.version");
+ return name + "/" + release + " (Java 1.5 minimum; Java/" + javaVersion + ")";
+ }
+
+} // class VersionInfo
diff --git a/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/package-info.java b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/package-info.java
new file mode 100644
index 0000000000..8cc0a0f2db
--- /dev/null
+++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/util/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Core utility classes.
+ */
+package ch.boye.httpclientandroidlib.util;
diff --git a/mobile/android/thirdparty/com/adjust/sdk/ActivityHandler.java b/mobile/android/thirdparty/com/adjust/sdk/ActivityHandler.java
new file mode 100644
index 0000000000..8c7858b97b
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/ActivityHandler.java
@@ -0,0 +1,781 @@
+//
+// ActivityHandler.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2013-06-25.
+// Copyright (c) 2013 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+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.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+
+import org.json.JSONObject;
+
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static com.adjust.sdk.Constants.ACTIVITY_STATE_FILENAME;
+import static com.adjust.sdk.Constants.ATTRIBUTION_FILENAME;
+import static com.adjust.sdk.Constants.LOGTAG;
+
+public class ActivityHandler extends HandlerThread implements IActivityHandler {
+
+ private static long TIMER_INTERVAL;
+ private static long TIMER_START;
+ private static long SESSION_INTERVAL;
+ private static long SUBSESSION_INTERVAL;
+ private static final String TIME_TRAVEL = "Time travel!";
+ private static final String ADJUST_PREFIX = "adjust_";
+ private static final String ACTIVITY_STATE_NAME = "Activity state";
+ private static final String ATTRIBUTION_NAME = "Attribution";
+
+ private SessionHandler sessionHandler;
+ private IPackageHandler packageHandler;
+ private ActivityState activityState;
+ private ILogger logger;
+ private static ScheduledExecutorService timer;
+ private boolean enabled;
+ private boolean offline;
+
+ private DeviceInfo deviceInfo;
+ private AdjustConfig adjustConfig; // always valid after construction
+ private AdjustAttribution attribution;
+ private IAttributionHandler attributionHandler;
+
+ private ActivityHandler(AdjustConfig adjustConfig) {
+ super(LOGTAG, MIN_PRIORITY);
+ setDaemon(true);
+ start();
+
+ logger = AdjustFactory.getLogger();
+ sessionHandler = new SessionHandler(getLooper(), this);
+ enabled = true;
+ init(adjustConfig);
+
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.INIT;
+ sessionHandler.sendMessage(message);
+ }
+
+ @Override
+ public void init(AdjustConfig adjustConfig) {
+ this.adjustConfig = adjustConfig;
+ }
+
+ public static ActivityHandler getInstance(AdjustConfig adjustConfig) {
+ if (adjustConfig == null) {
+ AdjustFactory.getLogger().error("AdjustConfig missing");
+ return null;
+ }
+
+ if (!adjustConfig.isValid()) {
+ AdjustFactory.getLogger().error("AdjustConfig not initialized correctly");
+ return null;
+ }
+
+ ActivityHandler activityHandler = new ActivityHandler(adjustConfig);
+ return activityHandler;
+ }
+
+ @Override
+ public void trackSubsessionStart() {
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.START;
+ sessionHandler.sendMessage(message);
+ }
+
+ @Override
+ public void trackSubsessionEnd() {
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.END;
+ sessionHandler.sendMessage(message);
+ }
+
+ @Override
+ public void trackEvent(AdjustEvent event) {
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.EVENT;
+ message.obj = event;
+ sessionHandler.sendMessage(message);
+ }
+
+ @Override
+ public void finishedTrackingActivity(JSONObject jsonResponse) {
+ if (jsonResponse == null) {
+ return;
+ }
+
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.FINISH_TRACKING;
+ message.obj = jsonResponse;
+ sessionHandler.sendMessage(message);
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ if (enabled == this.enabled) {
+ if (enabled) {
+ logger.debug("Adjust already enabled");
+ } else {
+ logger.debug("Adjust already disabled");
+ }
+ return;
+ }
+ this.enabled = enabled;
+ if (activityState != null) {
+ activityState.enabled = enabled;
+ }
+ if (enabled) {
+ if (toPause()) {
+ logger.info("Package and attribution handler remain paused due to the SDK is offline");
+ } else {
+ logger.info("Resuming package handler and attribution handler to enabled the SDK");
+ }
+ trackSubsessionStart();
+ } else {
+ logger.info("Pausing package handler and attribution handler to disable the SDK");
+ trackSubsessionEnd();
+ }
+ }
+
+ @Override
+ public void setOfflineMode(boolean offline) {
+ if (offline == this.offline) {
+ if (offline) {
+ logger.debug("Adjust already in offline mode");
+ } else {
+ logger.debug("Adjust already in online mode");
+ }
+ return;
+ }
+ this.offline = offline;
+ if (offline) {
+ logger.info("Pausing package and attribution handler to put in offline mode");
+ } else {
+ if (toPause()) {
+ logger.info("Package and attribution handler remain paused because the SDK is disabled");
+ } else {
+ logger.info("Resuming package handler and attribution handler to put in online mode");
+ }
+ }
+ updateStatus();
+ }
+
+ @Override
+ public boolean isEnabled() {
+ if (activityState != null) {
+ return activityState.enabled;
+ } else {
+ return enabled;
+ }
+ }
+
+ @Override
+ public void readOpenUrl(Uri url, long clickTime) {
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.DEEP_LINK;
+ UrlClickTime urlClickTime = new UrlClickTime(url, clickTime);
+ message.obj = urlClickTime;
+ sessionHandler.sendMessage(message);
+ }
+
+ @Override
+ public boolean tryUpdateAttribution(AdjustAttribution attribution) {
+ if (attribution == null) return false;
+
+ if (attribution.equals(this.attribution)) {
+ return false;
+ }
+
+ saveAttribution(attribution);
+ launchAttributionListener();
+ return true;
+ }
+
+ private void saveAttribution(AdjustAttribution attribution) {
+ this.attribution = attribution;
+ writeAttribution();
+ }
+
+ private void launchAttributionListener() {
+ if (adjustConfig.onAttributionChangedListener == null) {
+ return;
+ }
+ Handler handler = new Handler(adjustConfig.context.getMainLooper());
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ adjustConfig.onAttributionChangedListener.onAttributionChanged(attribution);
+ }
+ };
+ handler.post(runnable);
+ }
+
+ @Override
+ public void setAskingAttribution(boolean askingAttribution) {
+ activityState.askingAttribution = askingAttribution;
+ writeActivityState();
+ }
+
+ @Override
+ public ActivityPackage getAttributionPackage() {
+ long now = System.currentTimeMillis();
+ PackageBuilder attributionBuilder = new PackageBuilder(adjustConfig,
+ deviceInfo,
+ activityState,
+ now);
+ return attributionBuilder.buildAttributionPackage();
+ }
+
+ @Override
+ public void sendReferrer(String referrer, long clickTime) {
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.SEND_REFERRER;
+ ReferrerClickTime referrerClickTime = new ReferrerClickTime(referrer, clickTime);
+ message.obj = referrerClickTime;
+ sessionHandler.sendMessage(message);
+ }
+
+ private class UrlClickTime {
+ Uri url;
+ long clickTime;
+
+ UrlClickTime(Uri url, long clickTime) {
+ this.url = url;
+ this.clickTime = clickTime;
+ }
+ }
+
+ private class ReferrerClickTime {
+ String referrer;
+ long clickTime;
+
+ ReferrerClickTime(String referrer, long clickTime) {
+ this.referrer = referrer;
+ this.clickTime = clickTime;
+ }
+ }
+
+ private void updateStatus() {
+ Message message = Message.obtain();
+ message.arg1 = SessionHandler.UPDATE_STATUS;
+ sessionHandler.sendMessage(message);
+ }
+
+ private static final class SessionHandler extends Handler {
+ private static final int BASE_ADDRESS = 72630;
+ private static final int INIT = BASE_ADDRESS + 1;
+ private static final int START = BASE_ADDRESS + 2;
+ private static final int END = BASE_ADDRESS + 3;
+ private static final int EVENT = BASE_ADDRESS + 4;
+ private static final int FINISH_TRACKING = BASE_ADDRESS + 5;
+ private static final int DEEP_LINK = BASE_ADDRESS + 6;
+ private static final int SEND_REFERRER = BASE_ADDRESS + 7;
+ private static final int UPDATE_STATUS = BASE_ADDRESS + 8;
+
+ private final WeakReference<ActivityHandler> sessionHandlerReference;
+
+ protected SessionHandler(Looper looper, ActivityHandler sessionHandler) {
+ super(looper);
+ this.sessionHandlerReference = new WeakReference<ActivityHandler>(sessionHandler);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ super.handleMessage(message);
+
+ ActivityHandler sessionHandler = sessionHandlerReference.get();
+ if (sessionHandler == null) {
+ return;
+ }
+
+ switch (message.arg1) {
+ case INIT:
+ sessionHandler.initInternal();
+ break;
+ case START:
+ sessionHandler.startInternal();
+ break;
+ case END:
+ sessionHandler.endInternal();
+ break;
+ case EVENT:
+ AdjustEvent event = (AdjustEvent) message.obj;
+ sessionHandler.trackEventInternal(event);
+ break;
+ case FINISH_TRACKING:
+ JSONObject jsonResponse = (JSONObject) message.obj;
+ sessionHandler.finishedTrackingActivityInternal(jsonResponse);
+ break;
+ case DEEP_LINK:
+ UrlClickTime urlClickTime = (UrlClickTime) message.obj;
+ sessionHandler.readOpenUrlInternal(urlClickTime.url, urlClickTime.clickTime);
+ break;
+ case SEND_REFERRER:
+ ReferrerClickTime referrerClickTime = (ReferrerClickTime) message.obj;
+ sessionHandler.sendReferrerInternal(referrerClickTime.referrer, referrerClickTime.clickTime);
+ break;
+ case UPDATE_STATUS:
+ sessionHandler.updateStatusInternal();
+ break;
+ }
+ }
+ }
+
+ private void initInternal() {
+ TIMER_INTERVAL = AdjustFactory.getTimerInterval();
+ TIMER_START = AdjustFactory.getTimerStart();
+ SESSION_INTERVAL = AdjustFactory.getSessionInterval();
+ SUBSESSION_INTERVAL = AdjustFactory.getSubsessionInterval();
+
+ deviceInfo = new DeviceInfo(adjustConfig.context, adjustConfig.sdkPrefix);
+
+ if (adjustConfig.environment == AdjustConfig.ENVIRONMENT_PRODUCTION) {
+ logger.setLogLevel(LogLevel.ASSERT);
+ } else {
+ logger.setLogLevel(adjustConfig.logLevel);
+ }
+
+ if (adjustConfig.eventBufferingEnabled) {
+ logger.info("Event buffering is enabled");
+ }
+
+ String playAdId = Util.getPlayAdId(adjustConfig.context);
+ if (playAdId == null) {
+ logger.info("Unable to get Google Play Services Advertising ID at start time");
+ }
+
+ if (adjustConfig.defaultTracker != null) {
+ logger.info("Default tracker: '%s'", adjustConfig.defaultTracker);
+ }
+
+ if (adjustConfig.referrer != null) {
+ sendReferrer(adjustConfig.referrer, adjustConfig.referrerClickTime); // send to background queue to make sure that activityState is valid
+ }
+
+ readAttribution();
+ readActivityState();
+
+ packageHandler = AdjustFactory.getPackageHandler(this, adjustConfig.context, toPause());
+
+ startInternal();
+ }
+
+ private void startInternal() {
+ // it shouldn't start if it was disabled after a first session
+ if (activityState != null
+ && !activityState.enabled) {
+ return;
+ }
+
+ updateStatusInternal();
+
+ processSession();
+
+ checkAttributionState();
+
+ startTimer();
+ }
+
+ private void processSession() {
+ long now = System.currentTimeMillis();
+
+ // very first session
+ if (activityState == null) {
+ activityState = new ActivityState();
+ activityState.sessionCount = 1; // this is the first session
+
+ transferSessionPackage(now);
+ activityState.resetSessionAttributes(now);
+ activityState.enabled = this.enabled;
+ writeActivityState();
+ return;
+ }
+
+ long lastInterval = now - activityState.lastActivity;
+
+ if (lastInterval < 0) {
+ logger.error(TIME_TRAVEL);
+ activityState.lastActivity = now;
+ writeActivityState();
+ return;
+ }
+
+ // new session
+ if (lastInterval > SESSION_INTERVAL) {
+ activityState.sessionCount++;
+ activityState.lastInterval = lastInterval;
+
+ transferSessionPackage(now);
+ activityState.resetSessionAttributes(now);
+ writeActivityState();
+ return;
+ }
+
+ // new subsession
+ if (lastInterval > SUBSESSION_INTERVAL) {
+ activityState.subsessionCount++;
+ activityState.sessionLength += lastInterval;
+ activityState.lastActivity = now;
+ writeActivityState();
+ logger.info("Started subsession %d of session %d",
+ activityState.subsessionCount,
+ activityState.sessionCount);
+ }
+ }
+
+ private void checkAttributionState() {
+ // if there is no attribution saved, or there is one being asked
+ if (attribution == null || activityState.askingAttribution) {
+ getAttributionHandler().getAttribution();
+ }
+ }
+
+ private void endInternal() {
+ packageHandler.pauseSending();
+ getAttributionHandler().pauseSending();
+ stopTimer();
+ if (updateActivityState(System.currentTimeMillis())) {
+ writeActivityState();
+ }
+ }
+
+ private void trackEventInternal(AdjustEvent event) {
+ if (!checkEvent(event)) return;
+ if (!activityState.enabled) return;
+
+ long now = System.currentTimeMillis();
+
+ activityState.eventCount++;
+ updateActivityState(now);
+
+ PackageBuilder eventBuilder = new PackageBuilder(adjustConfig, deviceInfo, activityState, now);
+ ActivityPackage eventPackage = eventBuilder.buildEventPackage(event);
+ packageHandler.addPackage(eventPackage);
+
+ if (adjustConfig.eventBufferingEnabled) {
+ logger.info("Buffered event %s", eventPackage.getSuffix());
+ } else {
+ packageHandler.sendFirstPackage();
+ }
+
+ writeActivityState();
+ }
+
+ private void finishedTrackingActivityInternal(JSONObject jsonResponse) {
+ if (jsonResponse == null) {
+ return;
+ }
+
+ String deeplink = jsonResponse.optString("deeplink", null);
+ launchDeeplinkMain(deeplink);
+ getAttributionHandler().checkAttribution(jsonResponse);
+ }
+
+ private void sendReferrerInternal(String referrer, long clickTime) {
+ ActivityPackage clickPackage = buildQueryStringClickPackage(referrer,
+ "reftag",
+ clickTime);
+ if (clickPackage == null) {
+ return;
+ }
+
+ getAttributionHandler().getAttribution();
+
+ packageHandler.sendClickPackage(clickPackage);
+ }
+
+ private void readOpenUrlInternal(Uri url, long clickTime) {
+ if (url == null) {
+ return;
+ }
+
+ String queryString = url.getQuery();
+
+ ActivityPackage clickPackage = buildQueryStringClickPackage(queryString, "deeplink", clickTime);
+ if (clickPackage == null) {
+ return;
+ }
+
+ getAttributionHandler().getAttribution();
+
+ packageHandler.sendClickPackage(clickPackage);
+ }
+
+ private ActivityPackage buildQueryStringClickPackage(String queryString, String source, long clickTime) {
+ if (queryString == null) {
+ return null;
+ }
+
+ long now = System.currentTimeMillis();
+ Map<String, String> queryStringParameters = new HashMap<String, String>();
+ AdjustAttribution queryStringAttribution = new AdjustAttribution();
+ boolean hasAdjustTags = false;
+
+ String[] queryPairs = queryString.split("&");
+ for (String pair : queryPairs) {
+ if (readQueryString(pair, queryStringParameters, queryStringAttribution)) {
+ hasAdjustTags = true;
+ }
+ }
+
+ if (!hasAdjustTags) {
+ return null;
+ }
+
+ String reftag = queryStringParameters.remove("reftag");
+
+ PackageBuilder builder = new PackageBuilder(adjustConfig, deviceInfo, activityState, now);
+ builder.extraParameters = queryStringParameters;
+ builder.attribution = queryStringAttribution;
+ builder.reftag = reftag;
+ ActivityPackage clickPackage = builder.buildClickPackage(source, clickTime);
+ return clickPackage;
+ }
+
+ private boolean readQueryString(String queryString,
+ Map<String, String> extraParameters,
+ AdjustAttribution queryStringAttribution) {
+ String[] pairComponents = queryString.split("=");
+ if (pairComponents.length != 2) return false;
+
+ String key = pairComponents[0];
+ if (!key.startsWith(ADJUST_PREFIX)) return false;
+
+ String value = pairComponents[1];
+ if (value.length() == 0) return false;
+
+ String keyWOutPrefix = key.substring(ADJUST_PREFIX.length());
+ if (keyWOutPrefix.length() == 0) return false;
+
+ if (!trySetAttribution(queryStringAttribution, keyWOutPrefix, value)) {
+ extraParameters.put(keyWOutPrefix, value);
+ }
+
+ return true;
+ }
+
+ private boolean trySetAttribution(AdjustAttribution queryStringAttribution,
+ String key,
+ String value) {
+ if (key.equals("tracker")) {
+ queryStringAttribution.trackerName = value;
+ return true;
+ }
+
+ if (key.equals("campaign")) {
+ queryStringAttribution.campaign = value;
+ return true;
+ }
+
+ if (key.equals("adgroup")) {
+ queryStringAttribution.adgroup = value;
+ return true;
+ }
+
+ if (key.equals("creative")) {
+ queryStringAttribution.creative = value;
+ return true;
+ }
+
+ return false;
+ }
+
+ private void updateStatusInternal() {
+ updateAttributionHandlerStatus();
+ updatePackageHandlerStatus();
+ }
+
+ private void updateAttributionHandlerStatus() {
+ if (attributionHandler == null) {
+ return;
+ }
+ if (toPause()) {
+ attributionHandler.pauseSending();
+ } else {
+ attributionHandler.resumeSending();
+ }
+ }
+
+ private void updatePackageHandlerStatus() {
+ if (packageHandler == null) {
+ return;
+ }
+ if (toPause()) {
+ packageHandler.pauseSending();
+ } else {
+ packageHandler.resumeSending();
+ }
+ }
+
+ private void launchDeeplinkMain(String deeplink) {
+ if (deeplink == null) return;
+
+ Uri location = Uri.parse(deeplink);
+ Intent mapIntent = new Intent(Intent.ACTION_VIEW, location);
+ mapIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ // Verify it resolves
+ PackageManager packageManager = adjustConfig.context.getPackageManager();
+ List<ResolveInfo> activities = packageManager.queryIntentActivities(mapIntent, 0);
+ boolean isIntentSafe = activities.size() > 0;
+
+ // Start an activity if it's safe
+ if (!isIntentSafe) {
+ logger.error("Unable to open deep link (%s)", deeplink);
+ return;
+ }
+
+ logger.info("Open deep link (%s)", deeplink);
+ adjustConfig.context.startActivity(mapIntent);
+ }
+
+ private boolean updateActivityState(long now) {
+ long lastInterval = now - activityState.lastActivity;
+ // ignore late updates
+ if (lastInterval > SESSION_INTERVAL) {
+ return false;
+ }
+ activityState.lastActivity = now;
+
+ if (lastInterval < 0) {
+ logger.error(TIME_TRAVEL);
+ } else {
+ activityState.sessionLength += lastInterval;
+ activityState.timeSpent += lastInterval;
+ }
+ return true;
+ }
+
+ public static boolean deleteActivityState(Context context) {
+ return context.deleteFile(ACTIVITY_STATE_FILENAME);
+ }
+
+ public static boolean deleteAttribution(Context context) {
+ return context.deleteFile(ATTRIBUTION_FILENAME);
+ }
+
+ private void transferSessionPackage(long now) {
+ PackageBuilder builder = new PackageBuilder(adjustConfig, deviceInfo, activityState, now);
+ ActivityPackage sessionPackage = builder.buildSessionPackage();
+ packageHandler.addPackage(sessionPackage);
+ packageHandler.sendFirstPackage();
+ }
+
+ private void startTimer() {
+ stopTimer();
+
+ if (!activityState.enabled) {
+ return;
+ }
+ timer = Executors.newSingleThreadScheduledExecutor();
+ timer.scheduleWithFixedDelay(new Runnable() {
+ @Override
+ public void run() {
+ timerFired();
+ }
+ }, TIMER_START, TIMER_INTERVAL, TimeUnit.MILLISECONDS);
+ }
+
+ private void stopTimer() {
+ if (timer != null) {
+ timer.shutdown();
+ timer = null;
+ }
+ }
+
+ private void timerFired() {
+ if (!activityState.enabled) {
+ stopTimer();
+ return;
+ }
+
+ packageHandler.sendFirstPackage();
+
+ if (updateActivityState(System.currentTimeMillis())) {
+ writeActivityState();
+ }
+ }
+
+ private void readActivityState() {
+ try {
+ /**
+ * Mozilla:
+ * readObject is a generic object, and can therefore return arbitrary generic objects
+ * that might not match the expected type. Therefore there will be an implicit cast
+ * here, which can fail. Therefore we have to add the catch (ClassCastException)
+ * Note: this has been fixed in upstream, we only need this for the version we are still shipping.
+ */
+ activityState = Util.readObject(adjustConfig.context, ACTIVITY_STATE_FILENAME, ACTIVITY_STATE_NAME);
+ } catch (ClassCastException e) {
+ activityState = null;
+ }
+ }
+
+ private void readAttribution() {
+ try {
+ /**
+ * Mozilla: (same as in readActivityState() )
+ * readObject is a generic object, and can therefore return arbitrary generic objects
+ * that might not match the expected type. Therefore there will be an implicit cast
+ * here, which can fail. Therefore we have to add the catch (ClassCastException)
+ * Note: this has been fixed in upstream, we only need this for the version we are still shipping.
+ */
+ attribution = Util.readObject(adjustConfig.context, ATTRIBUTION_FILENAME, ATTRIBUTION_NAME);
+ } catch (ClassCastException e) {
+ activityState = null;
+ }
+ }
+
+ private void writeActivityState() {
+ Util.writeObject(activityState, adjustConfig.context, ACTIVITY_STATE_FILENAME, ACTIVITY_STATE_NAME);
+ }
+
+ private void writeAttribution() {
+ Util.writeObject(attribution, adjustConfig.context, ATTRIBUTION_FILENAME, ATTRIBUTION_NAME);
+ }
+
+ private boolean checkEvent(AdjustEvent event) {
+ if (event == null) {
+ logger.error("Event missing");
+ return false;
+ }
+
+ if (!event.isValid()) {
+ logger.error("Event not initialized correctly");
+ return false;
+ }
+
+ return true;
+ }
+
+ // lazy initialization to prevent null activity state before first session
+ private IAttributionHandler getAttributionHandler() {
+ if (attributionHandler == null) {
+ ActivityPackage attributionPackage = getAttributionPackage();
+ attributionHandler = AdjustFactory.getAttributionHandler(this,
+ attributionPackage,
+ toPause());
+ }
+ return attributionHandler;
+ }
+
+ private boolean toPause() {
+ return offline || !isEnabled();
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/ActivityKind.java b/mobile/android/thirdparty/com/adjust/sdk/ActivityKind.java
new file mode 100644
index 0000000000..a255b83a91
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/ActivityKind.java
@@ -0,0 +1,35 @@
+package com.adjust.sdk;
+
+public enum ActivityKind {
+ UNKNOWN, SESSION, EVENT, CLICK, ATTRIBUTION;
+
+ public static ActivityKind fromString(String string) {
+ if ("session".equals(string)) {
+ return SESSION;
+ } else if ("event".equals(string)) {
+ return EVENT;
+ } else if ("click".equals(string)) {
+ return CLICK;
+ } else if ("attribution".equals(string)) {
+ return ATTRIBUTION;
+ } else {
+ return UNKNOWN;
+ }
+ }
+
+ @Override
+ public String toString() {
+ switch (this) {
+ case SESSION:
+ return "session";
+ case EVENT:
+ return "event";
+ case CLICK:
+ return "click";
+ case ATTRIBUTION:
+ return "attribution";
+ default:
+ return "unknown";
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/ActivityPackage.java b/mobile/android/thirdparty/com/adjust/sdk/ActivityPackage.java
new file mode 100644
index 0000000000..27ab969fd5
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/ActivityPackage.java
@@ -0,0 +1,100 @@
+//
+// ActivityPackage.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2013-06-25.
+// Copyright (c) 2013 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import java.io.Serializable;
+import java.util.Map;
+
+public class ActivityPackage implements Serializable {
+ private static final long serialVersionUID = -35935556512024097L;
+
+ // data
+ private String path;
+ private String clientSdk;
+ private Map<String, String> parameters;
+
+ // logs
+ private ActivityKind activityKind;
+ private String suffix;
+
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ public String getClientSdk() {
+ return clientSdk;
+ }
+
+ public void setClientSdk(String clientSdk) {
+ this.clientSdk = clientSdk;
+ }
+
+ public Map<String, String> getParameters() {
+ return parameters;
+ }
+
+ public void setParameters(Map<String, String> parameters) {
+ this.parameters = parameters;
+ }
+
+ public ActivityKind getActivityKind() {
+ return activityKind;
+ }
+
+ public void setActivityKind(ActivityKind activityKind) {
+ this.activityKind = activityKind;
+ }
+
+ public String getSuffix() {
+ return suffix;
+ }
+
+ public void setSuffix(String suffix) {
+ this.suffix = suffix;
+ }
+
+ public String toString() {
+ return String.format("%s%s", activityKind.toString(), suffix);
+ }
+
+ public String getExtendedString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append(String.format("Path: %s\n", path));
+ builder.append(String.format("ClientSdk: %s\n", clientSdk));
+
+ if (parameters != null) {
+ builder.append("Parameters:");
+ for (Map.Entry<String, String> entry : parameters.entrySet()) {
+ builder.append(String.format("\n\t%-16s %s", entry.getKey(), entry.getValue()));
+ }
+ }
+ return builder.toString();
+ }
+
+ protected String getSuccessMessage() {
+ try {
+ return String.format("Tracked %s%s", activityKind.toString(), suffix);
+ } catch (NullPointerException e) {
+ return "Tracked ???";
+ }
+ }
+
+ protected String getFailureMessage() {
+ try {
+ return String.format("Failed to track %s%s", activityKind.toString(), suffix);
+ } catch (NullPointerException e) {
+ return "Failed to track ???";
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/ActivityState.java b/mobile/android/thirdparty/com/adjust/sdk/ActivityState.java
new file mode 100644
index 0000000000..41ad2ca3b1
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/ActivityState.java
@@ -0,0 +1,151 @@
+//
+// ActivityState.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2013-06-25.
+// Copyright (c) 2013 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectInputStream.GetField;
+import java.io.Serializable;
+import java.util.Calendar;
+import java.util.Locale;
+
+public class ActivityState implements Serializable, Cloneable {
+ private static final long serialVersionUID = 9039439291143138148L;
+ private transient String readErrorMessage = "Unable to read '%s' field in migration device with message (%s)";
+ private transient ILogger logger;
+
+ // persistent data
+ protected String uuid;
+ protected boolean enabled;
+ protected boolean askingAttribution;
+
+ // global counters
+ protected int eventCount;
+ protected int sessionCount;
+
+ // session attributes
+ protected int subsessionCount;
+ protected long sessionLength; // all durations in milliseconds
+ protected long timeSpent;
+ protected long lastActivity; // all times in milliseconds since 1970
+
+ protected long lastInterval;
+
+ protected ActivityState() {
+ logger = AdjustFactory.getLogger();
+ // create UUID for new devices
+ uuid = Util.createUuid();
+ enabled = true;
+ askingAttribution = false;
+
+ eventCount = 0; // no events yet
+ sessionCount = 0; // the first session just started
+ subsessionCount = -1; // we don't know how many subsessions this first session will have
+ sessionLength = -1; // same for session length and time spent
+ timeSpent = -1; // this information will be collected and attached to the next session
+ lastActivity = -1;
+ lastInterval = -1;
+ }
+
+ protected void resetSessionAttributes(long now) {
+ subsessionCount = 1; // first subsession
+ sessionLength = 0; // no session length yet
+ timeSpent = 0; // no time spent yet
+ lastActivity = now;
+ lastInterval = -1;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.US,
+ "ec:%d sc:%d ssc:%d sl:%.1f ts:%.1f la:%s uuid:%s",
+ eventCount, sessionCount, subsessionCount,
+ sessionLength / 1000.0, timeSpent / 1000.0,
+ stamp(lastActivity), uuid);
+ }
+
+ @Override
+ public ActivityState clone() {
+ try {
+ return (ActivityState) super.clone();
+ } catch (CloneNotSupportedException e) {
+ return null;
+ }
+ }
+
+
+ private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
+ GetField fields = stream.readFields();
+
+ eventCount = readIntField(fields, "eventCount", 0);
+ sessionCount = readIntField(fields, "sessionCount", 0);
+ subsessionCount = readIntField(fields, "subsessionCount", -1);
+ sessionLength = readLongField(fields, "sessionLength", -1l);
+ timeSpent = readLongField(fields, "timeSpent", -1l);
+ lastActivity = readLongField(fields, "lastActivity", -1l);
+ lastInterval = readLongField(fields, "lastInterval", -1l);
+
+ // new fields
+ uuid = readStringField(fields, "uuid", null);
+ enabled = readBooleanField(fields, "enabled", true);
+ askingAttribution = readBooleanField(fields, "askingAttribution", false);
+
+ // create UUID for migrating devices
+ if (uuid == null) {
+ uuid = Util.createUuid();
+ }
+ }
+
+ private String readStringField(GetField fields, String name, String defaultValue) {
+ try {
+ return (String) fields.get(name, defaultValue);
+ } catch (Exception e) {
+ logger.debug(readErrorMessage, name, e.getMessage());
+ return defaultValue;
+ }
+ }
+
+ private boolean readBooleanField(GetField fields, String name, boolean defaultValue) {
+ try {
+ return fields.get(name, defaultValue);
+ } catch (Exception e) {
+ logger.debug(readErrorMessage, name, e.getMessage());
+ return defaultValue;
+ }
+ }
+
+ private int readIntField(GetField fields, String name, int defaultValue) {
+ try {
+ return fields.get(name, defaultValue);
+ } catch (Exception e) {
+ logger.debug(readErrorMessage, name, e.getMessage());
+ return defaultValue;
+ }
+ }
+
+ private long readLongField(GetField fields, String name, long defaultValue) {
+ try {
+ return fields.get(name, defaultValue);
+ } catch (Exception e) {
+ logger.debug(readErrorMessage, name, e.getMessage());
+ return defaultValue;
+ }
+ }
+
+ private static String stamp(long dateMillis) {
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(dateMillis);
+ return String.format(Locale.US,
+ "%02d:%02d:%02d",
+ calendar.HOUR_OF_DAY,
+ calendar.MINUTE,
+ calendar.SECOND);
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/Adjust.java b/mobile/android/thirdparty/com/adjust/sdk/Adjust.java
new file mode 100644
index 0000000000..3b81a077bf
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/Adjust.java
@@ -0,0 +1,79 @@
+//
+// Adjust.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2012-10-11.
+// Copyright (c) 2012-2014 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import android.net.Uri;
+
+/**
+ * The main interface to Adjust.
+ * Use the methods of this class to tell Adjust about the usage of your app.
+ * See the README for details.
+ */
+public class Adjust {
+
+ private static AdjustInstance defaultInstance;
+
+ private Adjust() {
+ }
+
+ public static synchronized AdjustInstance getDefaultInstance() {
+ if (defaultInstance == null) {
+ defaultInstance = new AdjustInstance();
+ }
+ return defaultInstance;
+ }
+
+ public static void onCreate(AdjustConfig adjustConfig) {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.onCreate(adjustConfig);
+ }
+
+ public static void trackEvent(AdjustEvent event) {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.trackEvent(event);
+ }
+
+ public static void onResume() {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.onResume();
+ }
+
+ public static void onPause() {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.onPause();
+ }
+
+ public static void setEnabled(boolean enabled) {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.setEnabled(enabled);
+ }
+
+ public static boolean isEnabled() {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ return adjustInstance.isEnabled();
+ }
+
+ public static void appWillOpenUrl(Uri url) {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.appWillOpenUrl(url);
+ }
+
+ public static void setReferrer(String referrer) {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.sendReferrer(referrer);
+ }
+
+ public static void setOfflineMode(boolean enabled) {
+ AdjustInstance adjustInstance = Adjust.getDefaultInstance();
+ adjustInstance.setOfflineMode(enabled);
+ }
+}
+
+
diff --git a/mobile/android/thirdparty/com/adjust/sdk/AdjustAttribution.java b/mobile/android/thirdparty/com/adjust/sdk/AdjustAttribution.java
new file mode 100644
index 0000000000..4e3abb0175
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/AdjustAttribution.java
@@ -0,0 +1,62 @@
+package com.adjust.sdk;
+
+import org.json.JSONObject;
+
+import java.io.Serializable;
+
+/**
+ * Created by pfms on 07/11/14.
+ */
+public class AdjustAttribution implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ public String trackerToken;
+ public String trackerName;
+ public String network;
+ public String campaign;
+ public String adgroup;
+ public String creative;
+
+ public static AdjustAttribution fromJson(JSONObject jsonObject) {
+ if (jsonObject == null) return null;
+
+ AdjustAttribution attribution = new AdjustAttribution();
+
+ attribution.trackerToken = jsonObject.optString("tracker_token", null);
+ attribution.trackerName = jsonObject.optString("tracker_name", null);
+ attribution.network = jsonObject.optString("network", null);
+ attribution.campaign = jsonObject.optString("campaign", null);
+ attribution.adgroup = jsonObject.optString("adgroup", null);
+ attribution.creative = jsonObject.optString("creative", null);
+
+ return attribution;
+ }
+
+ public boolean equals(Object other) {
+ if (other == this) return true;
+ if (other == null) return false;
+ if (getClass() != other.getClass()) return false;
+ AdjustAttribution otherAttribution = (AdjustAttribution) other;
+
+ if (!equalString(trackerToken, otherAttribution.trackerToken)) return false;
+ if (!equalString(trackerName, otherAttribution.trackerName)) return false;
+ if (!equalString(network, otherAttribution.network)) return false;
+ if (!equalString(campaign, otherAttribution.campaign)) return false;
+ if (!equalString(adgroup, otherAttribution.adgroup)) return false;
+ if (!equalString(creative, otherAttribution.creative)) return false;
+ return true;
+ }
+
+ private boolean equalString(String first, String second) {
+ if (first == null || second == null) {
+ return first == null && second == null;
+ }
+ return first.equals(second);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("tt:%s tn:%s net:%s cam:%s adg:%s cre:%s",
+ trackerToken, trackerName, network, campaign, adgroup, creative);
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/AdjustConfig.java b/mobile/android/thirdparty/com/adjust/sdk/AdjustConfig.java
new file mode 100644
index 0000000000..148a5f670b
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/AdjustConfig.java
@@ -0,0 +1,128 @@
+package com.adjust.sdk;
+
+import android.content.Context;
+
+/**
+ * Created by pfms on 06/11/14.
+ */
+public class AdjustConfig {
+ Context context;
+ String appToken;
+ String environment;
+ LogLevel logLevel;
+ String sdkPrefix;
+ Boolean eventBufferingEnabled;
+ String defaultTracker;
+ OnAttributionChangedListener onAttributionChangedListener;
+ String referrer;
+ long referrerClickTime;
+ Boolean knownDevice;
+
+ public static final String ENVIRONMENT_SANDBOX = "sandbox";
+ public static final String ENVIRONMENT_PRODUCTION = "production";
+
+ public AdjustConfig(Context context, String appToken, String environment) {
+ if (!isValid(context, appToken, environment)) {
+ return;
+ }
+
+ this.context = context.getApplicationContext();
+ this.appToken = appToken;
+ this.environment = environment;
+
+ // default values
+ this.logLevel = LogLevel.INFO;
+ this.eventBufferingEnabled = false;
+ }
+
+ public void setEventBufferingEnabled(Boolean eventBufferingEnabled) {
+ this.eventBufferingEnabled = eventBufferingEnabled;
+ }
+
+ public void setLogLevel(LogLevel logLevel) {
+ this.logLevel = logLevel;
+ }
+
+ public void setSdkPrefix(String sdkPrefix) {
+ this.sdkPrefix = sdkPrefix;
+ }
+
+ public void setDefaultTracker(String defaultTracker) {
+ this.defaultTracker = defaultTracker;
+ }
+
+ public void setOnAttributionChangedListener(OnAttributionChangedListener onAttributionChangedListener) {
+ this.onAttributionChangedListener = onAttributionChangedListener;
+ }
+
+ public boolean hasListener() {
+ return onAttributionChangedListener != null;
+ }
+
+ public boolean isValid() {
+ return appToken != null;
+ }
+
+ private boolean isValid(Context context, String appToken, String environment) {
+ if (!checkAppToken(appToken)) return false;
+ if (!checkEnvironment(environment)) return false;
+ if (!checkContext(context)) return false;
+
+ return true;
+ }
+
+ private static boolean checkContext(Context context) {
+ ILogger logger = AdjustFactory.getLogger();
+ if (context == null) {
+ logger.error("Missing context");
+ return false;
+ }
+
+ if (!Util.checkPermission(context, android.Manifest.permission.INTERNET)) {
+ logger.error("Missing permission: INTERNET");
+ return false;
+ }
+
+ return true;
+ }
+
+ private static boolean checkAppToken(String appToken) {
+ ILogger logger = AdjustFactory.getLogger();
+ if (appToken == null) {
+ logger.error("Missing App Token.");
+ return false;
+ }
+
+ if (appToken.length() != 12) {
+ logger.error("Malformed App Token '%s'", appToken);
+ return false;
+ }
+
+ return true;
+ }
+
+ private static boolean checkEnvironment(String environment) {
+ ILogger logger = AdjustFactory.getLogger();
+ if (environment == null) {
+ logger.error("Missing environment");
+ return false;
+ }
+
+ if (environment == AdjustConfig.ENVIRONMENT_SANDBOX) {
+ logger.Assert("SANDBOX: Adjust is running in Sandbox mode. " +
+ "Use this setting for testing. " +
+ "Don't forget to set the environment to `production` before publishing!");
+ return true;
+ }
+ if (environment == AdjustConfig.ENVIRONMENT_PRODUCTION) {
+ logger.Assert(
+ "PRODUCTION: Adjust is running in Production mode. " +
+ "Use this setting only for the build that you want to publish. " +
+ "Set the environment to `sandbox` if you want to test your app!");
+ return true;
+ }
+
+ logger.error("Unknown environment '%s'", environment);
+ return false;
+ }
+} \ No newline at end of file
diff --git a/mobile/android/thirdparty/com/adjust/sdk/AdjustEvent.java b/mobile/android/thirdparty/com/adjust/sdk/AdjustEvent.java
new file mode 100644
index 0000000000..f037181830
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/AdjustEvent.java
@@ -0,0 +1,112 @@
+package com.adjust.sdk;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Created by pfms on 05/11/14.
+ */
+public class AdjustEvent {
+ String eventToken;
+ Double revenue;
+ String currency;
+ Map<String, String> callbackParameters;
+ Map<String, String> partnerParameters;
+
+ private static ILogger logger = AdjustFactory.getLogger();
+
+ public AdjustEvent(String eventToken) {
+ if (!checkEventToken(eventToken, logger)) return;
+
+ this.eventToken = eventToken;
+ }
+
+ public void setRevenue(double revenue, String currency) {
+ if (!checkRevenue(revenue, currency)) return;
+
+ this.revenue = revenue;
+ this.currency = currency;
+ }
+
+ public void addCallbackParameter(String key, String value) {
+ if (!isValidParameter(key, "key", "Callback")) return;
+ if (!isValidParameter(value, "value", "Callback")) return;
+
+ if (callbackParameters == null) {
+ callbackParameters = new HashMap<String, String>();
+ }
+
+ String previousValue = callbackParameters.put(key, value);
+
+ if (previousValue != null) {
+ logger.warn("key %s was overwritten", key);
+ }
+ }
+
+ public void addPartnerParameter(String key, String value) {
+ if (!isValidParameter(key, "key", "Partner")) return;
+ if (!isValidParameter(value, "value", "Partner")) return;
+
+ if (partnerParameters == null) {
+ partnerParameters = new HashMap<String, String>();
+ }
+
+ String previousValue = partnerParameters.put(key, value);
+
+ if (previousValue != null) {
+ logger.warn("key %s was overwritten", key);
+ }
+ }
+
+ public boolean isValid() {
+ return eventToken != null;
+ }
+
+ private static boolean checkEventToken(String eventToken, ILogger logger) {
+ if (eventToken == null) {
+ logger.error("Missing Event Token");
+ return false;
+ }
+ if (eventToken.length() != 6) {
+ logger.error("Malformed Event Token '%s'", eventToken);
+ return false;
+ }
+ return true;
+ }
+
+ private boolean checkRevenue(Double revenue, String currency) {
+ if (revenue != null) {
+ if (revenue < 0.0) {
+ logger.error("Invalid amount %.4f", revenue);
+ return false;
+ }
+
+ if (currency == null) {
+ logger.error("Currency must be set with revenue");
+ return false;
+ }
+ if (currency == "") {
+ logger.error("Currency is empty");
+ return false;
+ }
+
+ } else if (currency != null) {
+ logger.error("Revenue must be set with currency");
+ return false;
+ }
+ return true;
+ }
+
+ private boolean isValidParameter(String attribute, String attributeType, String parameterName) {
+ if (attribute == null) {
+ logger.error("%s parameter %s is missing", parameterName, attributeType);
+ return false;
+ }
+ if (attribute == "") {
+ logger.error("%s parameter %s is empty", parameterName, attributeType);
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/AdjustFactory.java b/mobile/android/thirdparty/com/adjust/sdk/AdjustFactory.java
new file mode 100644
index 0000000000..802af6416f
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/AdjustFactory.java
@@ -0,0 +1,141 @@
+package com.adjust.sdk;
+
+import android.content.Context;
+
+import ch.boye.httpclientandroidlib.client.HttpClient;
+import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+
+public class AdjustFactory {
+ private static IPackageHandler packageHandler = null;
+ private static IRequestHandler requestHandler = null;
+ private static IAttributionHandler attributionHandler = null;
+ private static IActivityHandler activityHandler = null;
+ private static ILogger logger = null;
+ private static HttpClient httpClient = null;
+
+ private static long timerInterval = -1;
+ private static long timerStart = -1;
+ private static long sessionInterval = -1;
+ private static long subsessionInterval = -1;
+
+ public static IPackageHandler getPackageHandler(ActivityHandler activityHandler,
+ Context context,
+ boolean startPaused) {
+ if (packageHandler == null) {
+ return new PackageHandler(activityHandler, context, startPaused);
+ }
+ packageHandler.init(activityHandler, context, startPaused);
+ return packageHandler;
+ }
+
+ public static IRequestHandler getRequestHandler(IPackageHandler packageHandler) {
+ if (requestHandler == null) {
+ return new RequestHandler(packageHandler);
+ }
+ requestHandler.init(packageHandler);
+ return requestHandler;
+ }
+
+ public static ILogger getLogger() {
+ if (logger == null) {
+ // Logger needs to be "static" to retain the configuration throughout the app
+ logger = new Logger();
+ }
+ return logger;
+ }
+
+ public static HttpClient getHttpClient(HttpParams params) {
+ if (httpClient == null) {
+ return new DefaultHttpClient(params);
+ }
+ return httpClient;
+ }
+
+ public static long getTimerInterval() {
+ if (timerInterval == -1) {
+ return Constants.ONE_MINUTE;
+ }
+ return timerInterval;
+ }
+
+ public static long getTimerStart() {
+ if (timerStart == -1) {
+ return 0;
+ }
+ return timerStart;
+ }
+
+ public static long getSessionInterval() {
+ if (sessionInterval == -1) {
+ return Constants.THIRTY_MINUTES;
+ }
+ return sessionInterval;
+ }
+
+ public static long getSubsessionInterval() {
+ if (subsessionInterval == -1) {
+ return Constants.ONE_SECOND;
+ }
+ return subsessionInterval;
+ }
+
+ public static IActivityHandler getActivityHandler(AdjustConfig config) {
+ if (activityHandler == null) {
+ return ActivityHandler.getInstance(config);
+ }
+ activityHandler.init(config);
+ return activityHandler;
+ }
+
+ public static IAttributionHandler getAttributionHandler(IActivityHandler activityHandler,
+ ActivityPackage attributionPackage,
+ boolean startPaused) {
+ if (attributionHandler == null) {
+ return new AttributionHandler(activityHandler, attributionPackage, startPaused);
+ }
+ attributionHandler.init(activityHandler, attributionPackage, startPaused);
+ return attributionHandler;
+ }
+
+ public static void setPackageHandler(IPackageHandler packageHandler) {
+ AdjustFactory.packageHandler = packageHandler;
+ }
+
+ public static void setRequestHandler(IRequestHandler requestHandler) {
+ AdjustFactory.requestHandler = requestHandler;
+ }
+
+ public static void setLogger(ILogger logger) {
+ AdjustFactory.logger = logger;
+ }
+
+ public static void setHttpClient(HttpClient httpClient) {
+ AdjustFactory.httpClient = httpClient;
+ }
+
+ public static void setTimerInterval(long timerInterval) {
+ AdjustFactory.timerInterval = timerInterval;
+ }
+
+ public static void setTimerStart(long timerStart) {
+ AdjustFactory.timerStart = timerStart;
+ }
+
+ public static void setSessionInterval(long sessionInterval) {
+ AdjustFactory.sessionInterval = sessionInterval;
+ }
+
+ public static void setSubsessionInterval(long subsessionInterval) {
+ AdjustFactory.subsessionInterval = subsessionInterval;
+ }
+
+ public static void setActivityHandler(IActivityHandler activityHandler) {
+ AdjustFactory.activityHandler = activityHandler;
+ }
+
+ public static void setAttributionHandler(IAttributionHandler attributionHandler) {
+ AdjustFactory.attributionHandler = attributionHandler;
+ }
+
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/AdjustInstance.java b/mobile/android/thirdparty/com/adjust/sdk/AdjustInstance.java
new file mode 100644
index 0000000000..158fb7ca1a
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/AdjustInstance.java
@@ -0,0 +1,86 @@
+package com.adjust.sdk;
+
+import android.net.Uri;
+
+/**
+ * Created by pfms on 04/12/14.
+ */
+public class AdjustInstance {
+
+ private String referrer;
+ private long referrerClickTime;
+ private ActivityHandler activityHandler;
+
+ private static ILogger getLogger() {
+ return AdjustFactory.getLogger();
+ }
+
+ public void onCreate(AdjustConfig adjustConfig) {
+ if (activityHandler != null) {
+ getLogger().error("Adjust already initialized");
+ return;
+ }
+
+ adjustConfig.referrer = this.referrer;
+ adjustConfig.referrerClickTime = this.referrerClickTime;
+
+ activityHandler = ActivityHandler.getInstance(adjustConfig);
+ }
+
+ public void trackEvent(AdjustEvent event) {
+ if (!checkActivityHandler()) return;
+ activityHandler.trackEvent(event);
+ }
+
+ public void onResume() {
+ if (!checkActivityHandler()) return;
+ activityHandler.trackSubsessionStart();
+ }
+
+ public void onPause() {
+ if (!checkActivityHandler()) return;
+ activityHandler.trackSubsessionEnd();
+ }
+
+ public void setEnabled(boolean enabled) {
+ if (!checkActivityHandler()) return;
+ activityHandler.setEnabled(enabled);
+ }
+
+ public boolean isEnabled() {
+ if (!checkActivityHandler()) return false;
+ return activityHandler.isEnabled();
+ }
+
+ public void appWillOpenUrl(Uri url) {
+ if (!checkActivityHandler()) return;
+ long clickTime = System.currentTimeMillis();
+ activityHandler.readOpenUrl(url, clickTime);
+ }
+
+ public void sendReferrer(String referrer) {
+ long clickTime = System.currentTimeMillis();
+ // sendReferrer might be triggered before Adjust
+ if (activityHandler == null) {
+ // save it to inject in the config before launch
+ this.referrer = referrer;
+ this.referrerClickTime = clickTime;
+ } else {
+ activityHandler.sendReferrer(referrer, clickTime);
+ }
+ }
+
+ public void setOfflineMode(boolean enabled) {
+ if (!checkActivityHandler()) return;
+ activityHandler.setOfflineMode(enabled);
+ }
+
+ private boolean checkActivityHandler() {
+ if (activityHandler == null) {
+ getLogger().error("Please initialize Adjust by calling 'onCreate' before");
+ return false;
+ } else {
+ return true;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/AdjustReferrerReceiver.java b/mobile/android/thirdparty/com/adjust/sdk/AdjustReferrerReceiver.java
new file mode 100644
index 0000000000..cfeecd8d00
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/AdjustReferrerReceiver.java
@@ -0,0 +1,35 @@
+package com.adjust.sdk;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+
+import static com.adjust.sdk.Constants.ENCODING;
+import static com.adjust.sdk.Constants.MALFORMED;
+import static com.adjust.sdk.Constants.REFERRER;
+
+// support multiple BroadcastReceivers for the INSTALL_REFERRER:
+// http://blog.appington.com/2012/08/01/giving-credit-for-android-app-installs
+
+public class AdjustReferrerReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String rawReferrer = intent.getStringExtra(REFERRER);
+ if (null == rawReferrer) {
+ return;
+ }
+
+ String referrer;
+ try {
+ referrer = URLDecoder.decode(rawReferrer, ENCODING);
+ } catch (UnsupportedEncodingException e) {
+ referrer = MALFORMED;
+ }
+
+ AdjustInstance adjust = Adjust.getDefaultInstance();
+ adjust.sendReferrer(referrer);
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/AttributionHandler.java b/mobile/android/thirdparty/com/adjust/sdk/AttributionHandler.java
new file mode 100644
index 0000000000..0d550a83a8
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/AttributionHandler.java
@@ -0,0 +1,155 @@
+package com.adjust.sdk;
+
+import android.net.Uri;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.HttpClient;
+import ch.boye.httpclientandroidlib.client.methods.HttpGet;
+import org.json.JSONObject;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Created by pfms on 07/11/14.
+ */
+public class AttributionHandler implements IAttributionHandler {
+ private ScheduledExecutorService scheduler;
+ private IActivityHandler activityHandler;
+ private ILogger logger;
+ private ActivityPackage attributionPackage;
+ private ScheduledFuture waitingTask;
+ private HttpClient httpClient;
+ private boolean paused;
+
+ public AttributionHandler(IActivityHandler activityHandler,
+ ActivityPackage attributionPackage,
+ boolean startPaused) {
+ scheduler = Executors.newSingleThreadScheduledExecutor();
+ logger = AdjustFactory.getLogger();
+ httpClient = Util.getHttpClient();
+ init(activityHandler, attributionPackage, startPaused);
+ }
+
+ @Override
+ public void init(IActivityHandler activityHandler,
+ ActivityPackage attributionPackage,
+ boolean startPaused) {
+ this.activityHandler = activityHandler;
+ this.attributionPackage = attributionPackage;
+ this.paused = startPaused;
+ }
+
+ @Override
+ public void getAttribution() {
+ getAttribution(0);
+ }
+
+ @Override
+ public void checkAttribution(final JSONObject jsonResponse) {
+ scheduler.submit(new Runnable() {
+ @Override
+ public void run() {
+ checkAttributionInternal(jsonResponse);
+ }
+ });
+ }
+
+ @Override
+ public void pauseSending() {
+ paused = true;
+ }
+
+ @Override
+ public void resumeSending() {
+ paused = false;
+ }
+
+ private void getAttribution(int delayInMilliseconds) {
+ if (waitingTask != null) {
+ waitingTask.cancel(false);
+ }
+
+ if (delayInMilliseconds != 0) {
+ logger.debug("Waiting to query attribution in %d milliseconds", delayInMilliseconds);
+ }
+
+ waitingTask = scheduler.schedule(new Runnable() {
+ @Override
+ public void run() {
+ getAttributionInternal();
+ }
+ }, delayInMilliseconds, TimeUnit.MILLISECONDS);
+ }
+
+ private void checkAttributionInternal(JSONObject jsonResponse) {
+ if (jsonResponse == null) return;
+
+ JSONObject attributionJson = jsonResponse.optJSONObject("attribution");
+ AdjustAttribution attribution = AdjustAttribution.fromJson(attributionJson);
+
+ int timerMilliseconds = jsonResponse.optInt("ask_in", -1);
+
+ // without ask_in attribute
+ if (timerMilliseconds < 0) {
+ activityHandler.tryUpdateAttribution(attribution);
+
+ activityHandler.setAskingAttribution(false);
+
+ return;
+ }
+
+ activityHandler.setAskingAttribution(true);
+
+ getAttribution(timerMilliseconds);
+ }
+
+ private void getAttributionInternal() {
+ if (paused) {
+ logger.debug("Attribution Handler is paused");
+ return;
+ }
+ logger.verbose("%s", attributionPackage.getExtendedString());
+ HttpResponse httpResponse = null;
+ try {
+ HttpGet request = getRequest(attributionPackage);
+ httpResponse = httpClient.execute(request);
+ } catch (Exception e) {
+ logger.error("Failed to get attribution (%s)", e.getMessage());
+ return;
+ }
+
+ JSONObject jsonResponse = Util.parseJsonResponse(httpResponse, logger);
+
+ checkAttributionInternal(jsonResponse);
+ }
+
+ private Uri buildUri(ActivityPackage attributionPackage) {
+ Uri.Builder uriBuilder = new Uri.Builder();
+
+ uriBuilder.scheme(Constants.SCHEME);
+ uriBuilder.authority(Constants.AUTHORITY);
+ uriBuilder.appendPath(attributionPackage.getPath());
+
+ for (Map.Entry<String, String> entry : attributionPackage.getParameters().entrySet()) {
+ uriBuilder.appendQueryParameter(entry.getKey(), entry.getValue());
+ }
+
+ return uriBuilder.build();
+ }
+
+ private HttpGet getRequest(ActivityPackage attributionPackage) throws URISyntaxException {
+ HttpGet request = new HttpGet();
+ Uri uri = buildUri(attributionPackage);
+ request.setURI(new URI(uri.toString()));
+
+ request.addHeader("Client-SDK", attributionPackage.getClientSdk());
+
+ return request;
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/Constants.java b/mobile/android/thirdparty/com/adjust/sdk/Constants.java
new file mode 100644
index 0000000000..7a97cb2f4e
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/Constants.java
@@ -0,0 +1,53 @@
+//
+// Constants.java
+// Adjust
+//
+// Created by keyboardsurfer on 2013-11-08.
+// Copyright (c) 2012-2014 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author keyboardsurfer
+ * @since 8.11.13
+ */
+public interface Constants {
+ int ONE_SECOND = 1000;
+ int ONE_MINUTE = 60 * ONE_SECOND;
+ int THIRTY_MINUTES = 30 * ONE_MINUTE;
+
+ int CONNECTION_TIMEOUT = Constants.ONE_MINUTE;
+ int SOCKET_TIMEOUT = Constants.ONE_MINUTE;
+
+ String BASE_URL = "https://app.adjust.com";
+ String SCHEME = "https";
+ String AUTHORITY = "app.adjust.com";
+ String CLIENT_SDK = "android4.0.0";
+ String LOGTAG = "Adjust";
+
+ String ACTIVITY_STATE_FILENAME = "AdjustIoActivityState";
+ String ATTRIBUTION_FILENAME = "AdjustAttribution";
+
+ String MALFORMED = "malformed";
+ String SMALL = "small";
+ String NORMAL = "normal";
+ String LONG = "long";
+ String LARGE = "large";
+ String XLARGE = "xlarge";
+ String LOW = "low";
+ String MEDIUM = "medium";
+ String HIGH = "high";
+ String REFERRER = "referrer";
+
+ String ENCODING = "UTF-8";
+ String MD5 = "MD5";
+ String SHA1 = "SHA-1";
+
+ // List of known plugins, possibly not active
+ List<String> PLUGINS = Arrays.asList();
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/DeviceInfo.java b/mobile/android/thirdparty/com/adjust/sdk/DeviceInfo.java
new file mode 100644
index 0000000000..5cccb77f40
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/DeviceInfo.java
@@ -0,0 +1,290 @@
+package com.adjust.sdk;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.util.DisplayMetrics;
+
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.util.Locale;
+import java.util.Map;
+
+import static com.adjust.sdk.Constants.ENCODING;
+import static com.adjust.sdk.Constants.HIGH;
+import static com.adjust.sdk.Constants.LARGE;
+import static com.adjust.sdk.Constants.LONG;
+import static com.adjust.sdk.Constants.LOW;
+import static com.adjust.sdk.Constants.MD5;
+import static com.adjust.sdk.Constants.MEDIUM;
+import static com.adjust.sdk.Constants.NORMAL;
+import static com.adjust.sdk.Constants.SHA1;
+import static com.adjust.sdk.Constants.SMALL;
+import static com.adjust.sdk.Constants.XLARGE;
+
+/**
+ * Created by pfms on 06/11/14.
+ */
+class DeviceInfo {
+ String macSha1;
+ String macShortMd5;
+ String androidId;
+ String fbAttributionId;
+ String clientSdk;
+ String packageName;
+ String appVersion;
+ String deviceType;
+ String deviceName;
+ String deviceManufacturer;
+ String osName;
+ String osVersion;
+ String language;
+ String country;
+ String screenSize;
+ String screenFormat;
+ String screenDensity;
+ String displayWidth;
+ String displayHeight;
+ Map<String, String> pluginKeys;
+
+ DeviceInfo(Context context, String sdkPrefix) {
+ Resources resources = context.getResources();
+ DisplayMetrics displayMetrics = resources.getDisplayMetrics();
+ Configuration configuration = resources.getConfiguration();
+ Locale locale = configuration.locale;
+ int screenLayout = configuration.screenLayout;
+ boolean isGooglePlayServicesAvailable = Reflection.isGooglePlayServicesAvailable(context);
+ String macAddress = getMacAddress(context, isGooglePlayServicesAvailable);
+
+ packageName = getPackageName(context);
+ appVersion = getAppVersion(context);
+ deviceType = getDeviceType(screenLayout);
+ deviceName = getDeviceName();
+ deviceManufacturer = getDeviceManufacturer();
+ osName = getOsName();
+ osVersion = getOsVersion();
+ language = getLanguage(locale);
+ country = getCountry(locale);
+ screenSize = getScreenSize(screenLayout);
+ screenFormat = getScreenFormat(screenLayout);
+ screenDensity = getScreenDensity(displayMetrics);
+ displayWidth = getDisplayWidth(displayMetrics);
+ displayHeight = getDisplayHeight(displayMetrics);
+ clientSdk = getClientSdk(sdkPrefix);
+ androidId = getAndroidId(context, isGooglePlayServicesAvailable);
+ fbAttributionId = getFacebookAttributionId(context);
+ pluginKeys = Reflection.getPluginKeys(context);
+ macSha1 = getMacSha1(macAddress);
+ macShortMd5 = getMacShortMd5(macAddress);
+ }
+
+ private String getMacAddress(Context context, boolean isGooglePlayServicesAvailable) {
+ if (!isGooglePlayServicesAvailable) {
+ if (!!Util.checkPermission(context, android.Manifest.permission.ACCESS_WIFI_STATE)) {
+ AdjustFactory.getLogger().warn("Missing permission: ACCESS_WIFI_STATE");
+ }
+ return Reflection.getMacAddress(context);
+ } else {
+ return null;
+ }
+ }
+
+ private String getPackageName(Context context) {
+ return context.getPackageName();
+ }
+
+ private String getAppVersion(Context context) {
+ try {
+ PackageManager packageManager = context.getPackageManager();
+ String name = context.getPackageName();
+ PackageInfo info = packageManager.getPackageInfo(name, 0);
+ return info.versionName;
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
+ }
+ }
+
+ private String getDeviceType(int screenLayout) {
+ int screenSize = screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
+
+ switch (screenSize) {
+ case Configuration.SCREENLAYOUT_SIZE_SMALL:
+ case Configuration.SCREENLAYOUT_SIZE_NORMAL:
+ return "phone";
+ case Configuration.SCREENLAYOUT_SIZE_LARGE:
+ case 4:
+ return "tablet";
+ default:
+ return null;
+ }
+ }
+
+ private String getDeviceName() {
+ return Build.MODEL;
+ }
+
+ private String getDeviceManufacturer() {
+ return Build.MANUFACTURER;
+ }
+
+ private String getOsName() {
+ return "android";
+ }
+
+ private String getOsVersion() {
+ return osVersion = "" + Build.VERSION.SDK_INT;
+ }
+
+ private String getLanguage(Locale locale) {
+ return locale.getLanguage();
+ }
+
+ private String getCountry(Locale locale) {
+ return locale.getCountry();
+ }
+
+ private String getScreenSize(int screenLayout) {
+ int screenSize = screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
+
+ switch (screenSize) {
+ case Configuration.SCREENLAYOUT_SIZE_SMALL:
+ return SMALL;
+ case Configuration.SCREENLAYOUT_SIZE_NORMAL:
+ return NORMAL;
+ case Configuration.SCREENLAYOUT_SIZE_LARGE:
+ return LARGE;
+ case 4:
+ return XLARGE;
+ default:
+ return null;
+ }
+ }
+
+ private String getScreenFormat(int screenLayout) {
+ int screenFormat = screenLayout & Configuration.SCREENLAYOUT_LONG_MASK;
+
+ switch (screenFormat) {
+ case Configuration.SCREENLAYOUT_LONG_YES:
+ return LONG;
+ case Configuration.SCREENLAYOUT_LONG_NO:
+ return NORMAL;
+ default:
+ return null;
+ }
+ }
+
+ private String getScreenDensity(DisplayMetrics displayMetrics) {
+ int density = displayMetrics.densityDpi;
+ int low = (DisplayMetrics.DENSITY_MEDIUM + DisplayMetrics.DENSITY_LOW) / 2;
+ int high = (DisplayMetrics.DENSITY_MEDIUM + DisplayMetrics.DENSITY_HIGH) / 2;
+
+ if (0 == density) {
+ return null;
+ } else if (density < low) {
+ return LOW;
+ } else if (density > high) {
+ return HIGH;
+ }
+ return MEDIUM;
+ }
+
+ private String getDisplayWidth(DisplayMetrics displayMetrics) {
+ return String.valueOf(displayMetrics.widthPixels);
+ }
+
+ private String getDisplayHeight(DisplayMetrics displayMetrics) {
+ return String.valueOf(displayMetrics.heightPixels);
+ }
+
+ private String getClientSdk(String sdkPrefix) {
+ if (sdkPrefix == null) {
+ return Constants.CLIENT_SDK;
+ } else {
+ return String.format("%s@%s", sdkPrefix, Constants.CLIENT_SDK);
+ }
+ }
+
+ private String getMacSha1(String macAddress) {
+ if (macAddress == null) {
+ return null;
+ }
+ String macSha1 = sha1(macAddress);
+
+ return macSha1;
+ }
+
+ private String getMacShortMd5(String macAddress) {
+ if (macAddress == null) {
+ return null;
+ }
+ String macShort = macAddress.replaceAll(":", "");
+ String macShortMd5 = md5(macShort);
+
+ return macShortMd5;
+ }
+
+ private String getAndroidId(Context context, boolean isGooglePlayServicesAvailable) {
+ if (!isGooglePlayServicesAvailable) {
+ return Reflection.getAndroidId(context);
+ } else {
+ return null;
+ }
+ }
+
+ private String sha1(final String text) {
+ return hash(text, SHA1);
+ }
+
+ private String md5(final String text) {
+ return hash(text, MD5);
+ }
+
+ private String hash(final String text, final String method) {
+ String hashString = null;
+ try {
+ final byte[] bytes = text.getBytes(ENCODING);
+ final MessageDigest mesd = MessageDigest.getInstance(method);
+ mesd.update(bytes, 0, bytes.length);
+ final byte[] hash = mesd.digest();
+ hashString = convertToHex(hash);
+ } catch (Exception e) {
+ }
+ return hashString;
+ }
+
+ private static String convertToHex(final byte[] bytes) {
+ final BigInteger bigInt = new BigInteger(1, bytes);
+ final String formatString = "%0" + (bytes.length << 1) + "x";
+ return String.format(formatString, bigInt);
+ }
+
+ private String getFacebookAttributionId(final Context context) {
+ try {
+ final ContentResolver contentResolver = context.getContentResolver();
+ final Uri uri = Uri.parse("content://com.facebook.katana.provider.AttributionIdProvider");
+ final String columnName = "aid";
+ final String[] projection = {columnName};
+ final Cursor cursor = contentResolver.query(uri, projection, null, null, null);
+
+ if (null == cursor) {
+ return null;
+ }
+ if (!cursor.moveToFirst()) {
+ cursor.close();
+ return null;
+ }
+
+ final String attributionId = cursor.getString(cursor.getColumnIndex(columnName));
+ cursor.close();
+ return attributionId;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/IActivityHandler.java b/mobile/android/thirdparty/com/adjust/sdk/IActivityHandler.java
new file mode 100644
index 0000000000..10b92205da
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/IActivityHandler.java
@@ -0,0 +1,36 @@
+package com.adjust.sdk;
+
+import android.net.Uri;
+
+import org.json.JSONObject;
+
+/**
+ * Created by pfms on 15/12/14.
+ */
+public interface IActivityHandler {
+ public void init(AdjustConfig config);
+
+ public void trackSubsessionStart();
+
+ public void trackSubsessionEnd();
+
+ public void trackEvent(AdjustEvent event);
+
+ public void finishedTrackingActivity(JSONObject jsonResponse);
+
+ public void setEnabled(boolean enabled);
+
+ public boolean isEnabled();
+
+ public void readOpenUrl(Uri url, long clickTime);
+
+ public boolean tryUpdateAttribution(AdjustAttribution attribution);
+
+ public void sendReferrer(String referrer, long clickTime);
+
+ public void setOfflineMode(boolean enabled);
+
+ public void setAskingAttribution(boolean askingAttribution);
+
+ public ActivityPackage getAttributionPackage();
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/IAttributionHandler.java b/mobile/android/thirdparty/com/adjust/sdk/IAttributionHandler.java
new file mode 100644
index 0000000000..d4e701f750
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/IAttributionHandler.java
@@ -0,0 +1,20 @@
+package com.adjust.sdk;
+
+import org.json.JSONObject;
+
+/**
+ * Created by pfms on 15/12/14.
+ */
+public interface IAttributionHandler {
+ public void init(IActivityHandler activityHandler,
+ ActivityPackage attributionPackage,
+ boolean startPaused);
+
+ public void getAttribution();
+
+ public void checkAttribution(JSONObject jsonResponse);
+
+ public void pauseSending();
+
+ public void resumeSending();
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/ILogger.java b/mobile/android/thirdparty/com/adjust/sdk/ILogger.java
new file mode 100644
index 0000000000..28f92af4b6
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/ILogger.java
@@ -0,0 +1,20 @@
+package com.adjust.sdk;
+
+public interface ILogger {
+ public void setLogLevel(LogLevel logLevel);
+
+ public void setLogLevelString(String logLevelString);
+
+ public void verbose(String message, Object... parameters);
+
+ public void debug(String message, Object... parameters);
+
+ public void info(String message, Object... parameters);
+
+ public void warn(String message, Object... parameters);
+
+ public void error(String message, Object... parameters);
+
+ public void Assert(String message, Object... parameters);
+
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/IPackageHandler.java b/mobile/android/thirdparty/com/adjust/sdk/IPackageHandler.java
new file mode 100644
index 0000000000..99c3003648
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/IPackageHandler.java
@@ -0,0 +1,27 @@
+package com.adjust.sdk;
+
+import android.content.Context;
+
+import org.json.JSONObject;
+
+public interface IPackageHandler {
+ public void init(IActivityHandler activityHandler, Context context, boolean startPaused);
+
+ public void addPackage(ActivityPackage pack);
+
+ public void sendFirstPackage();
+
+ public void sendNextPackage();
+
+ public void closeFirstPackage();
+
+ public void pauseSending();
+
+ public void resumeSending();
+
+ public String getFailureMessage();
+
+ public void finishedTrackingActivity(JSONObject jsonResponse);
+
+ public void sendClickPackage(ActivityPackage clickPackage);
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/IRequestHandler.java b/mobile/android/thirdparty/com/adjust/sdk/IRequestHandler.java
new file mode 100644
index 0000000000..5b18e2ee9d
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/IRequestHandler.java
@@ -0,0 +1,9 @@
+package com.adjust.sdk;
+
+public interface IRequestHandler {
+ public void init(IPackageHandler packageHandler);
+
+ public void sendPackage(ActivityPackage pack);
+
+ public void sendClickPackage(ActivityPackage clickPackage);
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/LICENSE b/mobile/android/thirdparty/com/adjust/sdk/LICENSE
new file mode 100644
index 0000000000..25e1d5eb5a
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2012-2014 adjust GmbH,
+http://www.adjust.com
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/mobile/android/thirdparty/com/adjust/sdk/LogLevel.java b/mobile/android/thirdparty/com/adjust/sdk/LogLevel.java
new file mode 100644
index 0000000000..5c0b410c23
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/LogLevel.java
@@ -0,0 +1,19 @@
+package com.adjust.sdk;
+
+import android.util.Log;
+
+/**
+ * Created by pfms on 11/03/15.
+ */
+public enum LogLevel {
+ VERBOSE(Log.VERBOSE), DEBUG(Log.DEBUG), INFO(Log.INFO), WARN(Log.WARN), ERROR(Log.ERROR), ASSERT(Log.ASSERT);
+ final int androidLogLevel;
+
+ LogLevel(final int androidLogLevel) {
+ this.androidLogLevel = androidLogLevel;
+ }
+
+ public int getAndroidLogLevel() {
+ return androidLogLevel;
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/Logger.java b/mobile/android/thirdparty/com/adjust/sdk/Logger.java
new file mode 100644
index 0000000000..86a644d4af
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/Logger.java
@@ -0,0 +1,107 @@
+//
+// Logger.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2013-04-18.
+// Copyright (c) 2013 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import android.util.Log;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+import static com.adjust.sdk.Constants.LOGTAG;
+
+public class Logger implements ILogger {
+
+ private LogLevel logLevel;
+ private static String formatErrorMessage = "Error formating log message: %s, with params: %s";
+
+ public Logger() {
+ setLogLevel(LogLevel.INFO);
+ }
+
+ @Override
+ public void setLogLevel(LogLevel logLevel) {
+ this.logLevel = logLevel;
+ }
+
+ @Override
+ public void setLogLevelString(String logLevelString) {
+ if (null != logLevelString) {
+ try {
+ setLogLevel(LogLevel.valueOf(logLevelString.toUpperCase(Locale.US)));
+ } catch (IllegalArgumentException iae) {
+ error("Malformed logLevel '%s', falling back to 'info'", logLevelString);
+ }
+ }
+ }
+
+ @Override
+ public void verbose(String message, Object... parameters) {
+ if (logLevel.androidLogLevel <= Log.VERBOSE) {
+ try {
+ Log.v(LOGTAG, String.format(message, parameters));
+ } catch (Exception e) {
+ Log.e(LOGTAG, String.format(formatErrorMessage, message, Arrays.toString(parameters)));
+ }
+ }
+ }
+
+ @Override
+ public void debug(String message, Object... parameters) {
+ if (logLevel.androidLogLevel <= Log.DEBUG) {
+ try {
+ Log.d(LOGTAG, String.format(message, parameters));
+ } catch (Exception e) {
+ Log.e(LOGTAG, String.format(formatErrorMessage, message, Arrays.toString(parameters)));
+ }
+ }
+ }
+
+ @Override
+ public void info(String message, Object... parameters) {
+ if (logLevel.androidLogLevel <= Log.INFO) {
+ try {
+ Log.i(LOGTAG, String.format(message, parameters));
+ } catch (Exception e) {
+ Log.e(LOGTAG, String.format(formatErrorMessage, message, Arrays.toString(parameters)));
+ }
+ }
+ }
+
+ @Override
+ public void warn(String message, Object... parameters) {
+ if (logLevel.androidLogLevel <= Log.WARN) {
+ try {
+ Log.w(LOGTAG, String.format(message, parameters));
+ } catch (Exception e) {
+ Log.e(LOGTAG, String.format(formatErrorMessage, message, Arrays.toString(parameters)));
+ }
+ }
+ }
+
+ @Override
+ public void error(String message, Object... parameters) {
+ if (logLevel.androidLogLevel <= Log.ERROR) {
+ try {
+ Log.e(LOGTAG, String.format(message, parameters));
+ } catch (Exception e) {
+ Log.e(LOGTAG, String.format(formatErrorMessage, message, Arrays.toString(parameters)));
+ }
+ }
+ }
+
+ @Override
+ public void Assert(String message, Object... parameters) {
+ try {
+ Log.println(Log.ASSERT, LOGTAG, String.format(message, parameters));
+ } catch (Exception e) {
+ Log.e(LOGTAG, String.format(formatErrorMessage, message, Arrays.toString(parameters)));
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/OnAttributionChangedListener.java b/mobile/android/thirdparty/com/adjust/sdk/OnAttributionChangedListener.java
new file mode 100644
index 0000000000..137d50d4de
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/OnAttributionChangedListener.java
@@ -0,0 +1,5 @@
+package com.adjust.sdk;
+
+public interface OnAttributionChangedListener {
+ public void onAttributionChanged(AdjustAttribution attribution);
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/PackageBuilder.java b/mobile/android/thirdparty/com/adjust/sdk/PackageBuilder.java
new file mode 100644
index 0000000000..3a43045fd1
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/PackageBuilder.java
@@ -0,0 +1,291 @@
+//
+// PackageBuilder.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2013-06-25.
+// Copyright (c) 2013 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import android.text.TextUtils;
+
+import org.json.JSONObject;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+class PackageBuilder {
+ private AdjustConfig adjustConfig;
+ private DeviceInfo deviceInfo;
+ private ActivityState activityState;
+ private long createdAt;
+
+ // reattributions
+ Map<String, String> extraParameters;
+ AdjustAttribution attribution;
+ String reftag;
+
+ private static ILogger logger = AdjustFactory.getLogger();
+
+ public PackageBuilder(AdjustConfig adjustConfig,
+ DeviceInfo deviceInfo,
+ ActivityState activityState,
+ long createdAt) {
+ this.adjustConfig = adjustConfig;
+ this.deviceInfo = deviceInfo;
+ this.activityState = activityState.clone();
+ this.createdAt = createdAt;
+ }
+
+ public ActivityPackage buildSessionPackage() {
+ Map<String, String> parameters = getDefaultParameters();
+ addDuration(parameters, "last_interval", activityState.lastInterval);
+ addString(parameters, "default_tracker", adjustConfig.defaultTracker);
+
+ ActivityPackage sessionPackage = getDefaultActivityPackage();
+ sessionPackage.setPath("/session");
+ sessionPackage.setActivityKind(ActivityKind.SESSION);
+ sessionPackage.setSuffix("");
+ sessionPackage.setParameters(parameters);
+
+ return sessionPackage;
+ }
+
+ public ActivityPackage buildEventPackage(AdjustEvent event) {
+ Map<String, String> parameters = getDefaultParameters();
+ addInt(parameters, "event_count", activityState.eventCount);
+ addString(parameters, "event_token", event.eventToken);
+ addDouble(parameters, "revenue", event.revenue);
+ addString(parameters, "currency", event.currency);
+ addMapJson(parameters, "callback_params", event.callbackParameters);
+ addMapJson(parameters, "partner_params", event.partnerParameters);
+
+ ActivityPackage eventPackage = getDefaultActivityPackage();
+ eventPackage.setPath("/event");
+ eventPackage.setActivityKind(ActivityKind.EVENT);
+ eventPackage.setSuffix(getEventSuffix(event));
+ eventPackage.setParameters(parameters);
+
+ return eventPackage;
+ }
+
+ public ActivityPackage buildClickPackage(String source, long clickTime) {
+ Map<String, String> parameters = getDefaultParameters();
+
+ addString(parameters, "source", source);
+ addDate(parameters, "click_time", clickTime);
+ addString(parameters, "reftag", reftag);
+ addMapJson(parameters, "params", extraParameters);
+ injectAttribution(parameters);
+
+ ActivityPackage clickPackage = getDefaultActivityPackage();
+ clickPackage.setPath("/sdk_click");
+ clickPackage.setActivityKind(ActivityKind.CLICK);
+ clickPackage.setSuffix("");
+ clickPackage.setParameters(parameters);
+
+ return clickPackage;
+ }
+
+ public ActivityPackage buildAttributionPackage() {
+ Map<String, String> parameters = getIdsParameters();
+
+ ActivityPackage attributionPackage = getDefaultActivityPackage();
+ attributionPackage.setPath("attribution"); // does not contain '/' because of Uri.Builder.appendPath
+ attributionPackage.setActivityKind(ActivityKind.ATTRIBUTION);
+ attributionPackage.setSuffix("");
+ attributionPackage.setParameters(parameters);
+
+ return attributionPackage;
+ }
+
+ private ActivityPackage getDefaultActivityPackage() {
+ ActivityPackage activityPackage = new ActivityPackage();
+ activityPackage.setClientSdk(deviceInfo.clientSdk);
+ return activityPackage;
+ }
+
+ private Map<String, String> getDefaultParameters() {
+ Map<String, String> parameters = new HashMap<String, String>();
+
+ injectDeviceInfo(parameters);
+ injectConfig(parameters);
+ injectActivityState(parameters);
+ addDate(parameters, "created_at", createdAt);
+
+ // general
+ checkDeviceIds(parameters);
+
+ return parameters;
+ }
+
+ private Map<String, String> getIdsParameters() {
+ Map<String, String> parameters = new HashMap<String, String>();
+
+ injectDeviceInfoIds(parameters);
+ injectConfig(parameters);
+ injectActivityStateIds(parameters);
+
+ checkDeviceIds(parameters);
+
+ return parameters;
+ }
+
+ private void injectDeviceInfo(Map<String, String> parameters) {
+ injectDeviceInfoIds(parameters);
+ addString(parameters, "fb_id", deviceInfo.fbAttributionId);
+ addString(parameters, "package_name", deviceInfo.packageName);
+ addString(parameters, "app_version", deviceInfo.appVersion);
+ addString(parameters, "device_type", deviceInfo.deviceType);
+ addString(parameters, "device_name", deviceInfo.deviceName);
+ addString(parameters, "device_manufacturer", deviceInfo.deviceManufacturer);
+ addString(parameters, "os_name", deviceInfo.osName);
+ addString(parameters, "os_version", deviceInfo.osVersion);
+ addString(parameters, "language", deviceInfo.language);
+ addString(parameters, "country", deviceInfo.country);
+ addString(parameters, "screen_size", deviceInfo.screenSize);
+ addString(parameters, "screen_format", deviceInfo.screenFormat);
+ addString(parameters, "screen_density", deviceInfo.screenDensity);
+ addString(parameters, "display_width", deviceInfo.displayWidth);
+ addString(parameters, "display_height", deviceInfo.displayHeight);
+ fillPluginKeys(parameters);
+ }
+
+ private void injectDeviceInfoIds(Map<String, String> parameters) {
+ addString(parameters, "mac_sha1", deviceInfo.macSha1);
+ addString(parameters, "mac_md5", deviceInfo.macShortMd5);
+ addString(parameters, "android_id", deviceInfo.androidId);
+ }
+
+ private void injectConfig(Map<String, String> parameters) {
+ addString(parameters, "app_token", adjustConfig.appToken);
+ addString(parameters, "environment", adjustConfig.environment);
+ addBoolean(parameters, "device_known", adjustConfig.knownDevice);
+ addBoolean(parameters, "needs_attribution_data", adjustConfig.hasListener());
+
+ String playAdId = Util.getPlayAdId(adjustConfig.context);
+ addString(parameters, "gps_adid", playAdId);
+ Boolean isTrackingEnabled = Util.isPlayTrackingEnabled(adjustConfig.context);
+ addBoolean(parameters, "tracking_enabled", isTrackingEnabled);
+ }
+
+ private void injectActivityState(Map<String, String> parameters) {
+ injectActivityStateIds(parameters);
+ addInt(parameters, "session_count", activityState.sessionCount);
+ addInt(parameters, "subsession_count", activityState.subsessionCount);
+ addDuration(parameters, "session_length", activityState.sessionLength);
+ addDuration(parameters, "time_spent", activityState.timeSpent);
+ }
+
+ private void injectActivityStateIds(Map<String, String> parameters) {
+ addString(parameters, "android_uuid", activityState.uuid);
+ }
+
+ private void injectAttribution(Map<String, String> parameters) {
+ if (attribution == null) {
+ return;
+ }
+ addString(parameters, "tracker", attribution.trackerName);
+ addString(parameters, "campaign", attribution.campaign);
+ addString(parameters, "adgroup", attribution.adgroup);
+ addString(parameters, "creative", attribution.creative);
+ }
+
+ private void checkDeviceIds(Map<String, String> parameters) {
+ if (!parameters.containsKey("mac_sha1")
+ && !parameters.containsKey("mac_md5")
+ && !parameters.containsKey("android_id")
+ && !parameters.containsKey("gps_adid")) {
+ logger.error("Missing device id's. Please check if Proguard is correctly set with Adjust SDK");
+ }
+ }
+
+ private void fillPluginKeys(Map<String, String> parameters) {
+ if (deviceInfo.pluginKeys == null) {
+ return;
+ }
+
+ for (Map.Entry<String, String> entry : deviceInfo.pluginKeys.entrySet()) {
+ addString(parameters, entry.getKey(), entry.getValue());
+ }
+ }
+
+ private String getEventSuffix(AdjustEvent event) {
+ if (event.revenue == null) {
+ return String.format(" '%s'", event.eventToken);
+ } else {
+ return String.format(Locale.US, " (%.4f %s, '%s')", event.revenue, event.currency, event.eventToken);
+ }
+ }
+
+ private void addString(Map<String, String> parameters, String key, String value) {
+ if (TextUtils.isEmpty(value)) {
+ return;
+ }
+
+ parameters.put(key, value);
+ }
+
+ private void addInt(Map<String, String> parameters, String key, long value) {
+ if (value < 0) {
+ return;
+ }
+
+ String valueString = Long.toString(value);
+ addString(parameters, key, valueString);
+ }
+
+ private void addDate(Map<String, String> parameters, String key, long value) {
+ if (value < 0) {
+ return;
+ }
+
+ String dateString = Util.dateFormat(value);
+ addString(parameters, key, dateString);
+ }
+
+ private void addDuration(Map<String, String> parameters, String key, long durationInMilliSeconds) {
+ if (durationInMilliSeconds < 0) {
+ return;
+ }
+
+ long durationInSeconds = (durationInMilliSeconds + 500) / 1000;
+ addInt(parameters, key, durationInSeconds);
+ }
+
+ private void addMapJson(Map<String, String> parameters, String key, Map<String, String> map) {
+ if (map == null) {
+ return;
+ }
+
+ if (map.size() == 0) {
+ return;
+ }
+
+ JSONObject jsonObject = new JSONObject(map);
+ String jsonString = jsonObject.toString();
+
+ addString(parameters, key, jsonString);
+ }
+
+ private void addBoolean(Map<String, String> parameters, String key, Boolean value) {
+ if (value == null) {
+ return;
+ }
+
+ int intValue = value ? 1 : 0;
+
+ addInt(parameters, key, intValue);
+ }
+
+ private void addDouble(Map<String, String> parameters, String key, Double value) {
+ if (value == null) return;
+
+ String doubleString = String.format("%.5f", value);
+
+ addString(parameters, key, doubleString);
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/PackageHandler.java b/mobile/android/thirdparty/com/adjust/sdk/PackageHandler.java
new file mode 100644
index 0000000000..d0a84ccd1d
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/PackageHandler.java
@@ -0,0 +1,274 @@
+//
+// PackageHandler.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2013-06-25.
+// Copyright (c) 2013 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+
+import org.json.JSONObject;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.OptionalDataException;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+// persistent
+public class PackageHandler extends HandlerThread implements IPackageHandler {
+ private static final String PACKAGE_QUEUE_FILENAME = "AdjustIoPackageQueue";
+
+ private final InternalHandler internalHandler;
+ private IRequestHandler requestHandler;
+ private IActivityHandler activityHandler;
+ private List<ActivityPackage> packageQueue;
+ private AtomicBoolean isSending;
+ private boolean paused;
+ private Context context;
+ private ILogger logger;
+
+ public PackageHandler(IActivityHandler activityHandler,
+ Context context,
+ boolean startPaused) {
+ super(Constants.LOGTAG, MIN_PRIORITY);
+ setDaemon(true);
+ start();
+ this.internalHandler = new InternalHandler(getLooper(), this);
+ this.logger = AdjustFactory.getLogger();
+
+ init(activityHandler, context, startPaused);
+
+ Message message = Message.obtain();
+ message.arg1 = InternalHandler.INIT;
+ internalHandler.sendMessage(message);
+ }
+
+ @Override
+ public void init(IActivityHandler activityHandler, Context context, boolean startPaused) {
+ this.activityHandler = activityHandler;
+ this.context = context;
+ this.paused = startPaused;
+ }
+
+ // add a package to the queue
+ @Override
+ public void addPackage(ActivityPackage pack) {
+ Message message = Message.obtain();
+ message.arg1 = InternalHandler.ADD;
+ message.obj = pack;
+ internalHandler.sendMessage(message);
+ }
+
+ // try to send the oldest package
+ @Override
+ public void sendFirstPackage() {
+ Message message = Message.obtain();
+ message.arg1 = InternalHandler.SEND_FIRST;
+ internalHandler.sendMessage(message);
+ }
+
+ // remove oldest package and try to send the next one
+ // (after success or possibly permanent failure)
+ @Override
+ public void sendNextPackage() {
+ Message message = Message.obtain();
+ message.arg1 = InternalHandler.SEND_NEXT;
+ internalHandler.sendMessage(message);
+ }
+
+ // close the package to retry in the future (after temporary failure)
+ @Override
+ public void closeFirstPackage() {
+ isSending.set(false);
+ }
+
+ // interrupt the sending loop after the current request has finished
+ @Override
+ public void pauseSending() {
+ paused = true;
+ }
+
+ // allow sending requests again
+ @Override
+ public void resumeSending() {
+ paused = false;
+ }
+
+ // short info about how failing packages are handled
+ @Override
+ public String getFailureMessage() {
+ return "Will retry later.";
+ }
+
+ @Override
+ public void finishedTrackingActivity(JSONObject jsonResponse) {
+ activityHandler.finishedTrackingActivity(jsonResponse);
+ }
+
+ @Override
+ public void sendClickPackage(ActivityPackage clickPackage) {
+ logger.debug("Sending click package (%s)", clickPackage);
+ logger.verbose("%s", clickPackage.getExtendedString());
+ requestHandler.sendClickPackage(clickPackage);
+ }
+
+ private static final class InternalHandler extends Handler {
+ private static final int INIT = 1;
+ private static final int ADD = 2;
+ private static final int SEND_NEXT = 3;
+ private static final int SEND_FIRST = 4;
+
+ private final WeakReference<PackageHandler> packageHandlerReference;
+
+ protected InternalHandler(Looper looper, PackageHandler packageHandler) {
+ super(looper);
+ this.packageHandlerReference = new WeakReference<PackageHandler>(packageHandler);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ super.handleMessage(message);
+
+ PackageHandler packageHandler = packageHandlerReference.get();
+ if (null == packageHandler) {
+ return;
+ }
+
+ switch (message.arg1) {
+ case INIT:
+ packageHandler.initInternal();
+ break;
+ case ADD:
+ ActivityPackage activityPackage = (ActivityPackage) message.obj;
+ packageHandler.addInternal(activityPackage);
+ break;
+ case SEND_FIRST:
+ packageHandler.sendFirstInternal();
+ break;
+ case SEND_NEXT:
+ packageHandler.sendNextInternal();
+ break;
+ }
+ }
+ }
+
+ // internal methods run in dedicated queue thread
+
+ private void initInternal() {
+ requestHandler = AdjustFactory.getRequestHandler(this);
+
+ isSending = new AtomicBoolean();
+
+ readPackageQueue();
+ }
+
+ private void addInternal(ActivityPackage newPackage) {
+ packageQueue.add(newPackage);
+ logger.debug("Added package %d (%s)", packageQueue.size(), newPackage);
+ logger.verbose("%s", newPackage.getExtendedString());
+
+ writePackageQueue();
+ }
+
+ private void sendFirstInternal() {
+ if (packageQueue.isEmpty()) {
+ return;
+ }
+
+ if (paused) {
+ logger.debug("Package handler is paused");
+ return;
+ }
+ if (isSending.getAndSet(true)) {
+ logger.verbose("Package handler is already sending");
+ return;
+ }
+
+ ActivityPackage firstPackage = packageQueue.get(0);
+ requestHandler.sendPackage(firstPackage);
+ }
+
+ private void sendNextInternal() {
+ packageQueue.remove(0);
+ writePackageQueue();
+ isSending.set(false);
+ sendFirstInternal();
+ }
+
+ private void readPackageQueue() {
+ try {
+ FileInputStream inputStream = context.openFileInput(PACKAGE_QUEUE_FILENAME);
+ BufferedInputStream bufferedStream = new BufferedInputStream(inputStream);
+ ObjectInputStream objectStream = new ObjectInputStream(bufferedStream);
+
+ try {
+ Object object = objectStream.readObject();
+ @SuppressWarnings("unchecked")
+ List<ActivityPackage> packageQueue = (List<ActivityPackage>) object;
+ logger.debug("Package handler read %d packages", packageQueue.size());
+ this.packageQueue = packageQueue;
+ return;
+ } catch (ClassNotFoundException e) {
+ logger.error("Failed to find package queue class");
+ } catch (OptionalDataException e) {
+ /* no-op */
+ } catch (IOException e) {
+ logger.error("Failed to read package queue object");
+ } catch (ClassCastException e) {
+ logger.error("Failed to cast package queue object");
+ } finally {
+ objectStream.close();
+ }
+ } catch (FileNotFoundException e) {
+ logger.verbose("Package queue file not found");
+ } catch (Exception e) {
+ logger.error("Failed to read package queue file");
+ }
+
+ // start with a fresh package queue in case of any exception
+ packageQueue = new ArrayList<ActivityPackage>();
+ }
+
+ public static Boolean deletePackageQueue(Context context) {
+ return context.deleteFile(PACKAGE_QUEUE_FILENAME);
+ }
+
+
+ private void writePackageQueue() {
+ try {
+ FileOutputStream outputStream = context.openFileOutput(PACKAGE_QUEUE_FILENAME, Context.MODE_PRIVATE);
+ BufferedOutputStream bufferedStream = new BufferedOutputStream(outputStream);
+ ObjectOutputStream objectStream = new ObjectOutputStream(bufferedStream);
+
+ try {
+ objectStream.writeObject(packageQueue);
+ logger.debug("Package handler wrote %d packages", packageQueue.size());
+ } catch (NotSerializableException e) {
+ logger.error("Failed to serialize packages");
+ } finally {
+ objectStream.close();
+ }
+ } catch (Exception e) {
+ logger.error("Failed to write packages (%s)", e.getLocalizedMessage());
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/Reflection.java b/mobile/android/thirdparty/com/adjust/sdk/Reflection.java
new file mode 100644
index 0000000000..d9d9a9dbc3
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/Reflection.java
@@ -0,0 +1,210 @@
+package com.adjust.sdk;
+
+import android.content.Context;
+
+import com.adjust.sdk.plugin.Plugin;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static com.adjust.sdk.Constants.PLUGINS;
+
+public class Reflection {
+
+ public static String getPlayAdId(Context context) {
+ try {
+ Object AdvertisingInfoObject = getAdvertisingInfoObject(context);
+
+ String playAdid = (String) invokeInstanceMethod(AdvertisingInfoObject, "getId", null);
+
+ return playAdid;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ public static Boolean isPlayTrackingEnabled(Context context) {
+ try {
+ Object AdvertisingInfoObject = getAdvertisingInfoObject(context);
+
+ Boolean isLimitedTrackingEnabled = (Boolean) invokeInstanceMethod(AdvertisingInfoObject, "isLimitAdTrackingEnabled", null);
+
+ return !isLimitedTrackingEnabled;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ public static boolean isGooglePlayServicesAvailable(Context context) {
+ try {
+ Integer isGooglePlayServicesAvailableStatusCode = (Integer) invokeStaticMethod(
+ "com.google.android.gms.common.GooglePlayServicesUtil",
+ "isGooglePlayServicesAvailable",
+ new Class[]{Context.class}, context
+ );
+
+ boolean isGooglePlayServicesAvailable = (Boolean) isConnectionResultSuccess(isGooglePlayServicesAvailableStatusCode);
+
+ return isGooglePlayServicesAvailable;
+ } catch (Throwable t) {
+ return false;
+ }
+ }
+
+ public static String getMacAddress(Context context) {
+ try {
+ String macSha1 = (String) invokeStaticMethod(
+ "com.adjust.sdk.plugin.MacAddressUtil",
+ "getMacAddress",
+ new Class[]{Context.class}, context
+ );
+
+ return macSha1;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ public static String getAndroidId(Context context) {
+ try {
+ String androidId = (String) invokeStaticMethod("com.adjust.sdk.plugin.AndroidIdUtil", "getAndroidId"
+ , new Class[]{Context.class}, context);
+
+ return androidId;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ public static String getSha1EmailAddress(Context context, String key) {
+ try {
+ String sha1EmailAddress = (String) invokeStaticMethod("com.adjust.sdk.plugin.EmailUtil", "getSha1EmailAddress"
+ , new Class[]{Context.class, String.class}, context, key);
+
+ return sha1EmailAddress;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ private static Object getAdvertisingInfoObject(Context context)
+ throws Exception {
+ return invokeStaticMethod("com.google.android.gms.ads.identifier.AdvertisingIdClient",
+ "getAdvertisingIdInfo",
+ new Class[]{Context.class}, context
+ );
+ }
+
+ private static boolean isConnectionResultSuccess(Integer statusCode) {
+ if (statusCode == null) {
+ return false;
+ }
+
+ try {
+ Class ConnectionResultClass = Class.forName("com.google.android.gms.common.ConnectionResult");
+
+ Field SuccessField = ConnectionResultClass.getField("SUCCESS");
+
+ int successStatusCode = SuccessField.getInt(null);
+
+ return successStatusCode == statusCode;
+ } catch (Throwable t) {
+ return false;
+ }
+ }
+
+ public static Class forName(String className) {
+ try {
+ Class classObject = Class.forName(className);
+ return classObject;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ public static Object createDefaultInstance(String className) {
+ Class classObject = forName(className);
+ Object instance = createDefaultInstance(classObject);
+ return instance;
+ }
+
+ public static Object createDefaultInstance(Class classObject) {
+ try {
+ Object instance = classObject.newInstance();
+ return instance;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ public static Object createInstance(String className, Class[] cArgs, Object... args) {
+ try {
+ Class classObject = Class.forName(className);
+ @SuppressWarnings("unchecked")
+ Constructor constructor = classObject.getConstructor(cArgs);
+ Object instance = constructor.newInstance(args);
+ return instance;
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ public static Object invokeStaticMethod(String className, String methodName, Class[] cArgs, Object... args)
+ throws Exception {
+ Class classObject = Class.forName(className);
+
+ return invokeMethod(classObject, methodName, null, cArgs, args);
+ }
+
+ public static Object invokeInstanceMethod(Object instance, String methodName, Class[] cArgs, Object... args)
+ throws Exception {
+ Class classObject = instance.getClass();
+
+ return invokeMethod(classObject, methodName, instance, cArgs, args);
+ }
+
+ public static Object invokeMethod(Class classObject, String methodName, Object instance, Class[] cArgs, Object... args)
+ throws Exception {
+ @SuppressWarnings("unchecked")
+ Method methodObject = classObject.getMethod(methodName, cArgs);
+
+ Object resultObject = methodObject.invoke(instance, args);
+
+ return resultObject;
+ }
+
+ public static Map<String, String> getPluginKeys(Context context) {
+ Map<String, String> pluginKeys = new HashMap<String, String>();
+
+ for (Plugin plugin : getPlugins()) {
+ Map.Entry<String, String> pluginEntry = plugin.getParameter(context);
+ if (pluginEntry != null) {
+ pluginKeys.put(pluginEntry.getKey(), pluginEntry.getValue());
+ }
+ }
+
+ if (pluginKeys.size() == 0) {
+ return null;
+ } else {
+ return pluginKeys;
+ }
+ }
+
+ private static List<Plugin> getPlugins() {
+ List<Plugin> plugins = new ArrayList<Plugin>(PLUGINS.size());
+
+ for (String pluginName : PLUGINS) {
+ Object pluginObject = Reflection.createDefaultInstance(pluginName);
+ if (pluginObject != null && pluginObject instanceof Plugin) {
+ plugins.add((Plugin) pluginObject);
+ }
+ }
+
+ return plugins;
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/RequestHandler.java b/mobile/android/thirdparty/com/adjust/sdk/RequestHandler.java
new file mode 100644
index 0000000000..84d45d0ce6
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/RequestHandler.java
@@ -0,0 +1,210 @@
+//
+// RequestHandler.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2013-06-25.
+// Copyright (c) 2013 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+import ch.boye.httpclientandroidlib.client.HttpClient;
+import ch.boye.httpclientandroidlib.client.entity.UrlEncodedFormEntity;
+import ch.boye.httpclientandroidlib.client.methods.HttpPost;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.client.utils.URLEncodedUtils;
+import ch.boye.httpclientandroidlib.message.BasicNameValuePair;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.lang.ref.WeakReference;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+public class RequestHandler extends HandlerThread implements IRequestHandler {
+ private InternalHandler internalHandler;
+ private IPackageHandler packageHandler;
+ private HttpClient httpClient;
+ private ILogger logger;
+
+ public RequestHandler(IPackageHandler packageHandler) {
+ super(Constants.LOGTAG, MIN_PRIORITY);
+ setDaemon(true);
+ start();
+
+ this.logger = AdjustFactory.getLogger();
+ this.internalHandler = new InternalHandler(getLooper(), this);
+ init(packageHandler);
+
+ Message message = Message.obtain();
+ message.arg1 = InternalHandler.INIT;
+ internalHandler.sendMessage(message);
+ }
+
+ @Override
+ public void init(IPackageHandler packageHandler) {
+ this.packageHandler = packageHandler;
+ }
+
+ @Override
+ public void sendPackage(ActivityPackage pack) {
+ Message message = Message.obtain();
+ message.arg1 = InternalHandler.SEND;
+ message.obj = pack;
+ internalHandler.sendMessage(message);
+ }
+
+ @Override
+ public void sendClickPackage(ActivityPackage clickPackage) {
+ Message message = Message.obtain();
+ message.arg1 = InternalHandler.SEND_CLICK;
+ message.obj = clickPackage;
+ internalHandler.sendMessage(message);
+
+ }
+
+ private static final class InternalHandler extends Handler {
+ private static final int INIT = 72401;
+ private static final int SEND = 72400;
+ private static final int SEND_CLICK = 72402;
+
+ private final WeakReference<RequestHandler> requestHandlerReference;
+
+ protected InternalHandler(Looper looper, RequestHandler requestHandler) {
+ super(looper);
+ this.requestHandlerReference = new WeakReference<RequestHandler>(requestHandler);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ super.handleMessage(message);
+
+ RequestHandler requestHandler = requestHandlerReference.get();
+ if (null == requestHandler) {
+ return;
+ }
+
+ switch (message.arg1) {
+ case INIT:
+ requestHandler.initInternal();
+ break;
+ case SEND:
+ ActivityPackage activityPackage = (ActivityPackage) message.obj;
+ requestHandler.sendInternal(activityPackage, true);
+ break;
+ case SEND_CLICK:
+ ActivityPackage clickPackage = (ActivityPackage) message.obj;
+ requestHandler.sendInternal(clickPackage, false);
+ break;
+ }
+ }
+ }
+
+ private void initInternal() {
+ httpClient = Util.getHttpClient();
+ }
+
+ private void sendInternal(ActivityPackage activityPackage, boolean sendToPackageHandler) {
+ try {
+ HttpUriRequest request = getRequest(activityPackage);
+ HttpResponse response = httpClient.execute(request);
+ requestFinished(response, sendToPackageHandler);
+ } catch (UnsupportedEncodingException e) {
+ sendNextPackage(activityPackage, "Failed to encode parameters", e, sendToPackageHandler);
+ } catch (ClientProtocolException e) {
+ closePackage(activityPackage, "Client protocol error", e, sendToPackageHandler);
+ } catch (SocketTimeoutException e) {
+ closePackage(activityPackage, "Request timed out", e, sendToPackageHandler);
+ } catch (IOException e) {
+ closePackage(activityPackage, "Request failed", e, sendToPackageHandler);
+ } catch (Throwable e) {
+ sendNextPackage(activityPackage, "Runtime exception", e, sendToPackageHandler);
+ }
+ }
+
+ private void requestFinished(HttpResponse response, boolean sendToPackageHandler) {
+ JSONObject jsonResponse = Util.parseJsonResponse(response, logger);
+
+ if (jsonResponse == null) {
+ if (sendToPackageHandler) {
+ packageHandler.closeFirstPackage();
+ }
+ return;
+ }
+
+ packageHandler.finishedTrackingActivity(jsonResponse);
+ if (sendToPackageHandler) {
+ packageHandler.sendNextPackage();
+ }
+ }
+
+ // close current package because it failed
+ private void closePackage(ActivityPackage activityPackage, String message, Throwable throwable, boolean sendToPackageHandler) {
+ final String packageMessage = activityPackage.getFailureMessage();
+ final String handlerMessage = packageHandler.getFailureMessage();
+ final String reasonString = getReasonString(message, throwable);
+ logger.error("%s. (%s) %s", packageMessage, reasonString, handlerMessage);
+
+ if (sendToPackageHandler) {
+ packageHandler.closeFirstPackage();
+ }
+ }
+
+ // send next package because the current package failed
+ private void sendNextPackage(ActivityPackage activityPackage, String message, Throwable throwable, boolean sendToPackageHandler) {
+ final String failureMessage = activityPackage.getFailureMessage();
+ final String reasonString = getReasonString(message, throwable);
+ logger.error("%s. (%s)", failureMessage, reasonString);
+
+ if (sendToPackageHandler) {
+ packageHandler.sendNextPackage();
+ }
+ }
+
+ private String getReasonString(String message, Throwable throwable) {
+ if (throwable != null) {
+ return String.format("%s: %s", message, throwable);
+ } else {
+ return String.format("%s", message);
+ }
+ }
+
+ private HttpUriRequest getRequest(ActivityPackage activityPackage) throws UnsupportedEncodingException {
+ String url = Constants.BASE_URL + activityPackage.getPath();
+ HttpPost request = new HttpPost(url);
+
+ String language = Locale.getDefault().getLanguage();
+ request.addHeader("Client-SDK", activityPackage.getClientSdk());
+ request.addHeader("Accept-Language", language);
+
+ List<NameValuePair> pairs = new ArrayList<NameValuePair>();
+ for (Map.Entry<String, String> entry : activityPackage.getParameters().entrySet()) {
+ NameValuePair pair = new BasicNameValuePair(entry.getKey(), entry.getValue());
+ pairs.add(pair);
+ }
+
+ long now = System.currentTimeMillis();
+ String dateString = Util.dateFormat(now);
+ NameValuePair sentAtPair = new BasicNameValuePair("sent_at", dateString);
+ pairs.add(sentAtPair);
+
+ UrlEncodedFormEntity entity = new UrlEncodedFormEntity(pairs);
+ entity.setContentType(URLEncodedUtils.CONTENT_TYPE);
+ request.setEntity(entity);
+
+ return request;
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/UnitTestActivity.java b/mobile/android/thirdparty/com/adjust/sdk/UnitTestActivity.java
new file mode 100644
index 0000000000..799fb89826
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/UnitTestActivity.java
@@ -0,0 +1,38 @@
+package com.adjust.sdk;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+
+public class UnitTestActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ //setContentView(com.adjust.sdk.test.R.layout.activity_unit_test);
+ }
+
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ //getMenuInflater().inflate(com.adjust.sdk.test.R.menu.menu_unit_test, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // Handle action bar item clicks here. The action bar will
+ // automatically handle clicks on the Home/Up button, so long
+ // as you specify a parent activity in AndroidManifest.xml.
+/* int id = item.getItemId();
+
+ //noinspection SimplifiableIfStatement
+ if (id == com.adjust.sdk.test.R.id.action_settings) {
+ return true;
+ }
+*/
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/Util.java b/mobile/android/thirdparty/com/adjust/sdk/Util.java
new file mode 100644
index 0000000000..84c47f87e7
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/Util.java
@@ -0,0 +1,202 @@
+//
+// Util.java
+// Adjust
+//
+// Created by Christian Wellenbrock on 2012-10-11.
+// Copyright (c) 2012-2014 adjust GmbH. All rights reserved.
+// See the file MIT-LICENSE for copying permission.
+//
+
+package com.adjust.sdk;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.client.HttpClient;
+import ch.boye.httpclientandroidlib.params.BasicHttpParams;
+import ch.boye.httpclientandroidlib.params.HttpConnectionParams;
+import ch.boye.httpclientandroidlib.params.HttpParams;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.OptionalDataException;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+import java.util.UUID;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Collects utility functions used by Adjust.
+ */
+public class Util {
+
+ private static SimpleDateFormat dateFormat;
+ private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'Z";
+
+ protected static String createUuid() {
+ return UUID.randomUUID().toString();
+ }
+
+ public static String quote(String string) {
+ if (string == null) {
+ return null;
+ }
+
+ Pattern pattern = Pattern.compile("\\s");
+ Matcher matcher = pattern.matcher(string);
+ if (!matcher.find()) {
+ return string;
+ }
+
+ return String.format("'%s'", string);
+ }
+
+ public static String dateFormat(long date) {
+ if (null == dateFormat) {
+ dateFormat = new SimpleDateFormat(DATE_FORMAT, Locale.US);
+ }
+ return dateFormat.format(date);
+ }
+
+ public static String getPlayAdId(Context context) {
+ return Reflection.getPlayAdId(context);
+ }
+
+ public static Boolean isPlayTrackingEnabled(Context context) {
+ return Reflection.isPlayTrackingEnabled(context);
+ }
+
+ public static <T> T readObject(Context context, String filename, String objectName) {
+ ILogger logger = AdjustFactory.getLogger();
+ try {
+ FileInputStream inputStream = context.openFileInput(filename);
+ BufferedInputStream bufferedStream = new BufferedInputStream(inputStream);
+ ObjectInputStream objectStream = new ObjectInputStream(bufferedStream);
+
+ try {
+ @SuppressWarnings("unchecked")
+ T t = (T) objectStream.readObject();
+ logger.debug("Read %s: %s", objectName, t);
+ return t;
+ } catch (ClassNotFoundException e) {
+ logger.error("Failed to find %s class", objectName);
+ } catch (OptionalDataException e) {
+ /* no-op */
+ } catch (IOException e) {
+ logger.error("Failed to read %s object", objectName);
+ } catch (ClassCastException e) {
+ logger.error("Failed to cast %s object", objectName);
+ } finally {
+ objectStream.close();
+ }
+
+ } catch (FileNotFoundException e) {
+ logger.verbose("%s file not found", objectName);
+ } catch (Exception e) {
+ logger.error("Failed to open %s file for reading (%s)", objectName, e);
+ }
+
+ return null;
+ }
+
+ public static <T> void writeObject(T object, Context context, String filename, String objectName) {
+ ILogger logger = AdjustFactory.getLogger();
+ try {
+ FileOutputStream outputStream = context.openFileOutput(filename, Context.MODE_PRIVATE);
+ BufferedOutputStream bufferedStream = new BufferedOutputStream(outputStream);
+ ObjectOutputStream objectStream = new ObjectOutputStream(bufferedStream);
+
+ try {
+ objectStream.writeObject(object);
+ logger.debug("Wrote %s: %s", objectName, object);
+ } catch (NotSerializableException e) {
+ logger.error("Failed to serialize %s", objectName);
+ } finally {
+ objectStream.close();
+ }
+
+ } catch (Exception e) {
+ logger.error("Failed to open %s for writing (%s)", objectName, e);
+ }
+ }
+
+ public static String parseResponse(HttpResponse httpResponse, ILogger logger) {
+ try {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ httpResponse.getEntity().writeTo(out);
+ out.close();
+ String response = out.toString().trim();
+ logger.verbose("Response: %s", response);
+ return response;
+ } catch (Exception e) {
+ logger.error("Failed to parse response (%s)", e);
+ return null;
+ }
+ }
+
+ public static JSONObject parseJsonResponse(HttpResponse httpResponse, ILogger logger) {
+ if (httpResponse == null) {
+ return null;
+ }
+ String stringResponse = null;
+ try {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ httpResponse.getEntity().writeTo(out);
+ out.close();
+ stringResponse = out.toString().trim();
+ } catch (Exception e) {
+ logger.error("Failed to parse response (%s)", e.getMessage());
+ }
+
+ logger.verbose("Response: %s", stringResponse);
+ if (stringResponse == null) return null;
+
+ JSONObject jsonResponse = null;
+ try {
+ jsonResponse = new JSONObject(stringResponse);
+ } catch (JSONException e) {
+ logger.error("Failed to parse json response: %s (%s)", stringResponse, e.getMessage());
+ }
+
+ if (jsonResponse == null) return null;
+
+ String message = jsonResponse.optString("message", null);
+
+ if (message == null) {
+ message = "No message found";
+ }
+
+ if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+ logger.info("%s", message);
+ } else {
+ logger.error("%s", message);
+ }
+
+ return jsonResponse;
+ }
+
+ public static HttpClient getHttpClient() {
+ HttpParams httpParams = new BasicHttpParams();
+ HttpConnectionParams.setConnectionTimeout(httpParams, Constants.CONNECTION_TIMEOUT);
+ HttpConnectionParams.setSoTimeout(httpParams, Constants.SOCKET_TIMEOUT);
+ return AdjustFactory.getHttpClient(httpParams);
+ }
+
+ public static boolean checkPermission(Context context, String permission) {
+ int result = context.checkCallingOrSelfPermission(permission);
+ return result == PackageManager.PERMISSION_GRANTED;
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/plugin/AndroidIdUtil.java b/mobile/android/thirdparty/com/adjust/sdk/plugin/AndroidIdUtil.java
new file mode 100644
index 0000000000..96a072287d
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/plugin/AndroidIdUtil.java
@@ -0,0 +1,10 @@
+package com.adjust.sdk.plugin;
+
+import android.content.Context;
+import android.provider.Settings.Secure;
+
+public class AndroidIdUtil {
+ public static String getAndroidId(final Context context) {
+ return Secure.getString(context.getContentResolver(), Secure.ANDROID_ID);
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/plugin/MacAddressUtil.java b/mobile/android/thirdparty/com/adjust/sdk/plugin/MacAddressUtil.java
new file mode 100644
index 0000000000..c8bdbadd73
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/plugin/MacAddressUtil.java
@@ -0,0 +1,82 @@
+package com.adjust.sdk.plugin;
+
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import android.text.TextUtils;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.Locale;
+
+public class MacAddressUtil {
+ public static String getMacAddress(Context context) {
+ final String rawAddress = getRawMacAddress(context);
+ if (rawAddress == null) {
+ return null;
+ }
+ final String upperAddress = rawAddress.toUpperCase(Locale.US);
+ return removeSpaceString(upperAddress);
+ }
+
+ private static String getRawMacAddress(Context context) {
+ // android devices should have a wlan address
+ final String wlanAddress = loadAddress("wlan0");
+ if (wlanAddress != null) {
+ return wlanAddress;
+ }
+
+ // emulators should have an ethernet address
+ final String ethAddress = loadAddress("eth0");
+ if (ethAddress != null) {
+ return ethAddress;
+ }
+
+ // query the wifi manager (requires the ACCESS_WIFI_STATE permission)
+ try {
+ final WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ final String wifiAddress = wifiManager.getConnectionInfo().getMacAddress();
+ if (wifiAddress != null) {
+ return wifiAddress;
+ }
+ } catch (Exception e) {
+ /* no-op */
+ }
+
+ return null;
+ }
+
+ private static String loadAddress(final String interfaceName) {
+ try {
+ final String filePath = "/sys/class/net/" + interfaceName + "/address";
+ final StringBuilder fileData = new StringBuilder(1000);
+ final BufferedReader reader = new BufferedReader(new FileReader(filePath), 1024);
+ final char[] buf = new char[1024];
+ int numRead;
+
+ String readData;
+ while ((numRead = reader.read(buf)) != -1) {
+ readData = String.valueOf(buf, 0, numRead);
+ fileData.append(readData);
+ }
+
+ reader.close();
+ return fileData.toString();
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ private static String removeSpaceString(final String inputString) {
+ if (inputString == null) {
+ return null;
+ }
+
+ String outputString = inputString.replaceAll("\\s", "");
+ if (TextUtils.isEmpty(outputString)) {
+ return null;
+ }
+
+ return outputString;
+ }
+}
diff --git a/mobile/android/thirdparty/com/adjust/sdk/plugin/Plugin.java b/mobile/android/thirdparty/com/adjust/sdk/plugin/Plugin.java
new file mode 100644
index 0000000000..ab704e6d36
--- /dev/null
+++ b/mobile/android/thirdparty/com/adjust/sdk/plugin/Plugin.java
@@ -0,0 +1,12 @@
+package com.adjust.sdk.plugin;
+
+import android.content.Context;
+
+import java.util.Map;
+
+/**
+ * Created by pfms on 18/09/14.
+ */
+public interface Plugin {
+ Map.Entry<String, String> getParameter(Context context);
+}
diff --git a/mobile/android/thirdparty/com/jakewharton/disklrucache/DiskLruCache.java b/mobile/android/thirdparty/com/jakewharton/disklrucache/DiskLruCache.java
new file mode 100644
index 0000000000..e81e7fbcca
--- /dev/null
+++ b/mobile/android/thirdparty/com/jakewharton/disklrucache/DiskLruCache.java
@@ -0,0 +1,943 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jakewharton.disklrucache;
+
+import java.io.BufferedWriter;
+import java.io.Closeable;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A cache that uses a bounded amount of space on a filesystem. Each cache
+ * entry has a string key and a fixed number of values. Each key must match
+ * the regex <strong>[a-z0-9_-]{1,120}</strong>. Values are byte sequences,
+ * accessible as streams or files. Each value must be between {@code 0} and
+ * {@code Integer.MAX_VALUE} bytes in length.
+ *
+ * <p>The cache stores its data in a directory on the filesystem. This
+ * directory must be exclusive to the cache; the cache may delete or overwrite
+ * files from its directory. It is an error for multiple processes to use the
+ * same cache directory at the same time.
+ *
+ * <p>This cache limits the number of bytes that it will store on the
+ * filesystem. When the number of stored bytes exceeds the limit, the cache will
+ * remove entries in the background until the limit is satisfied. The limit is
+ * not strict: the cache may temporarily exceed it while waiting for files to be
+ * deleted. The limit does not include filesystem overhead or the cache
+ * journal so space-sensitive applications should set a conservative limit.
+ *
+ * <p>Clients call {@link #edit} to create or update the values of an entry. An
+ * entry may have only one editor at one time; if a value is not available to be
+ * edited then {@link #edit} will return null.
+ * <ul>
+ * <li>When an entry is being <strong>created</strong> it is necessary to
+ * supply a full set of values; the empty value should be used as a
+ * placeholder if necessary.
+ * <li>When an entry is being <strong>edited</strong>, it is not necessary
+ * to supply data for every value; values default to their previous
+ * value.
+ * </ul>
+ * Every {@link #edit} call must be matched by a call to {@link Editor#commit}
+ * or {@link Editor#abort}. Committing is atomic: a read observes the full set
+ * of values as they were before or after the commit, but never a mix of values.
+ *
+ * <p>Clients call {@link #get} to read a snapshot of an entry. The read will
+ * observe the value at the time that {@link #get} was called. Updates and
+ * removals after the call do not impact ongoing reads.
+ *
+ * <p>This class is tolerant of some I/O errors. If files are missing from the
+ * filesystem, the corresponding entries will be dropped from the cache. If
+ * an error occurs while writing a cache value, the edit will fail silently.
+ * Callers should handle other problems by catching {@code IOException} and
+ * responding appropriately.
+ */
+public final class DiskLruCache implements Closeable {
+ static final String JOURNAL_FILE = "journal";
+ static final String JOURNAL_FILE_TEMP = "journal.tmp";
+ static final String JOURNAL_FILE_BACKUP = "journal.bkp";
+ static final String MAGIC = "libcore.io.DiskLruCache";
+ static final String VERSION_1 = "1";
+ static final long ANY_SEQUENCE_NUMBER = -1;
+ static final String STRING_KEY_PATTERN = "[a-z0-9_-]{1,120}";
+ static final Pattern LEGAL_KEY_PATTERN = Pattern.compile(STRING_KEY_PATTERN);
+ private static final String CLEAN = "CLEAN";
+ private static final String DIRTY = "DIRTY";
+ private static final String REMOVE = "REMOVE";
+ private static final String READ = "READ";
+
+ /*
+ * This cache uses a journal file named "journal". A typical journal file
+ * looks like this:
+ * libcore.io.DiskLruCache
+ * 1
+ * 100
+ * 2
+ *
+ * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
+ * DIRTY 335c4c6028171cfddfbaae1a9c313c52
+ * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
+ * REMOVE 335c4c6028171cfddfbaae1a9c313c52
+ * DIRTY 1ab96a171faeeee38496d8b330771a7a
+ * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
+ * READ 335c4c6028171cfddfbaae1a9c313c52
+ * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
+ *
+ * The first five lines of the journal form its header. They are the
+ * constant string "libcore.io.DiskLruCache", the disk cache's version,
+ * the application's version, the value count, and a blank line.
+ *
+ * Each of the subsequent lines in the file is a record of the state of a
+ * cache entry. Each line contains space-separated values: a state, a key,
+ * and optional state-specific values.
+ * o DIRTY lines track that an entry is actively being created or updated.
+ * Every successful DIRTY action should be followed by a CLEAN or REMOVE
+ * action. DIRTY lines without a matching CLEAN or REMOVE indicate that
+ * temporary files may need to be deleted.
+ * o CLEAN lines track a cache entry that has been successfully published
+ * and may be read. A publish line is followed by the lengths of each of
+ * its values.
+ * o READ lines track accesses for LRU.
+ * o REMOVE lines track entries that have been deleted.
+ *
+ * The journal file is appended to as cache operations occur. The journal may
+ * occasionally be compacted by dropping redundant lines. A temporary file named
+ * "journal.tmp" will be used during compaction; that file should be deleted if
+ * it exists when the cache is opened.
+ */
+
+ private final File directory;
+ private final File journalFile;
+ private final File journalFileTmp;
+ private final File journalFileBackup;
+ private final int appVersion;
+ private long maxSize;
+ private final int valueCount;
+ private long size = 0;
+ private Writer journalWriter;
+ private final LinkedHashMap<String, Entry> lruEntries =
+ new LinkedHashMap<String, Entry>(0, 0.75f, true);
+ private int redundantOpCount;
+
+ /**
+ * To differentiate between old and current snapshots, each entry is given
+ * a sequence number each time an edit is committed. A snapshot is stale if
+ * its sequence number is not equal to its entry's sequence number.
+ */
+ private long nextSequenceNumber = 0;
+
+ /** This cache uses a single background thread to evict entries. */
+ final ThreadPoolExecutor executorService =
+ new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
+ private final Callable<Void> cleanupCallable = new Callable<Void>() {
+ public Void call() throws Exception {
+ synchronized (DiskLruCache.this) {
+ if (journalWriter == null) {
+ return null; // Closed.
+ }
+ trimToSize();
+ if (journalRebuildRequired()) {
+ rebuildJournal();
+ redundantOpCount = 0;
+ }
+ }
+ return null;
+ }
+ };
+
+ private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
+ this.directory = directory;
+ this.appVersion = appVersion;
+ this.journalFile = new File(directory, JOURNAL_FILE);
+ this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
+ this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
+ this.valueCount = valueCount;
+ this.maxSize = maxSize;
+ }
+
+ /**
+ * Opens the cache in {@code directory}, creating a cache if none exists
+ * there.
+ *
+ * @param directory a writable directory
+ * @param valueCount the number of values per cache entry. Must be positive.
+ * @param maxSize the maximum number of bytes this cache should use to store
+ * @throws IOException if reading or writing the cache directory fails
+ */
+ public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
+ throws IOException {
+ if (maxSize <= 0) {
+ throw new IllegalArgumentException("maxSize <= 0");
+ }
+ if (valueCount <= 0) {
+ throw new IllegalArgumentException("valueCount <= 0");
+ }
+
+ // If a bkp file exists, use it instead.
+ File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
+ if (backupFile.exists()) {
+ File journalFile = new File(directory, JOURNAL_FILE);
+ // If journal file also exists just delete backup file.
+ if (journalFile.exists()) {
+ backupFile.delete();
+ } else {
+ renameTo(backupFile, journalFile, false);
+ }
+ }
+
+ // Prefer to pick up where we left off.
+ DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
+ if (cache.journalFile.exists()) {
+ try {
+ cache.readJournal();
+ cache.processJournal();
+ return cache;
+ } catch (IOException journalIsCorrupt) {
+ System.out
+ .println("DiskLruCache "
+ + directory
+ + " is corrupt: "
+ + journalIsCorrupt.getMessage()
+ + ", removing");
+ cache.delete();
+ }
+ }
+
+ // Create a new empty cache.
+ directory.mkdirs();
+ cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
+ cache.rebuildJournal();
+ return cache;
+ }
+
+ private void readJournal() throws IOException {
+ StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
+ try {
+ String magic = reader.readLine();
+ String version = reader.readLine();
+ String appVersionString = reader.readLine();
+ String valueCountString = reader.readLine();
+ String blank = reader.readLine();
+ if (!MAGIC.equals(magic)
+ || !VERSION_1.equals(version)
+ || !Integer.toString(appVersion).equals(appVersionString)
+ || !Integer.toString(valueCount).equals(valueCountString)
+ || !"".equals(blank)) {
+ throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
+ + valueCountString + ", " + blank + "]");
+ }
+
+ int lineCount = 0;
+ while (true) {
+ try {
+ readJournalLine(reader.readLine());
+ lineCount++;
+ } catch (EOFException endOfJournal) {
+ break;
+ }
+ }
+ redundantOpCount = lineCount - lruEntries.size();
+
+ // If we ended on a truncated line, rebuild the journal before appending to it.
+ if (reader.hasUnterminatedLine()) {
+ rebuildJournal();
+ } else {
+ journalWriter = new BufferedWriter(new OutputStreamWriter(
+ new FileOutputStream(journalFile, true), Util.US_ASCII));
+ }
+ } finally {
+ Util.closeQuietly(reader);
+ }
+ }
+
+ private void readJournalLine(String line) throws IOException {
+ int firstSpace = line.indexOf(' ');
+ if (firstSpace == -1) {
+ throw new IOException("unexpected journal line: " + line);
+ }
+
+ int keyBegin = firstSpace + 1;
+ int secondSpace = line.indexOf(' ', keyBegin);
+ final String key;
+ if (secondSpace == -1) {
+ key = line.substring(keyBegin);
+ if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
+ lruEntries.remove(key);
+ return;
+ }
+ } else {
+ key = line.substring(keyBegin, secondSpace);
+ }
+
+ Entry entry = lruEntries.get(key);
+ if (entry == null) {
+ entry = new Entry(key);
+ lruEntries.put(key, entry);
+ }
+
+ if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
+ String[] parts = line.substring(secondSpace + 1).split(" ");
+ entry.readable = true;
+ entry.currentEditor = null;
+ entry.setLengths(parts);
+ } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
+ entry.currentEditor = new Editor(entry);
+ } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
+ // This work was already done by calling lruEntries.get().
+ } else {
+ throw new IOException("unexpected journal line: " + line);
+ }
+ }
+
+ /**
+ * Computes the initial size and collects garbage as a part of opening the
+ * cache. Dirty entries are assumed to be inconsistent and will be deleted.
+ */
+ private void processJournal() throws IOException {
+ deleteIfExists(journalFileTmp);
+ for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
+ Entry entry = i.next();
+ if (entry.currentEditor == null) {
+ for (int t = 0; t < valueCount; t++) {
+ size += entry.lengths[t];
+ }
+ } else {
+ entry.currentEditor = null;
+ for (int t = 0; t < valueCount; t++) {
+ deleteIfExists(entry.getCleanFile(t));
+ deleteIfExists(entry.getDirtyFile(t));
+ }
+ i.remove();
+ }
+ }
+ }
+
+ /**
+ * Creates a new journal that omits redundant information. This replaces the
+ * current journal if it exists.
+ */
+ private synchronized void rebuildJournal() throws IOException {
+ if (journalWriter != null) {
+ journalWriter.close();
+ }
+
+ Writer writer = new BufferedWriter(
+ new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
+ try {
+ writer.write(MAGIC);
+ writer.write("\n");
+ writer.write(VERSION_1);
+ writer.write("\n");
+ writer.write(Integer.toString(appVersion));
+ writer.write("\n");
+ writer.write(Integer.toString(valueCount));
+ writer.write("\n");
+ writer.write("\n");
+
+ for (Entry entry : lruEntries.values()) {
+ if (entry.currentEditor != null) {
+ writer.write(DIRTY + ' ' + entry.key + '\n');
+ } else {
+ writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+ }
+ }
+ } finally {
+ writer.close();
+ }
+
+ if (journalFile.exists()) {
+ renameTo(journalFile, journalFileBackup, true);
+ }
+ renameTo(journalFileTmp, journalFile, false);
+ journalFileBackup.delete();
+
+ journalWriter = new BufferedWriter(
+ new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
+ }
+
+ private static void deleteIfExists(File file) throws IOException {
+ if (file.exists() && !file.delete()) {
+ throw new IOException();
+ }
+ }
+
+ private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
+ if (deleteDestination) {
+ deleteIfExists(to);
+ }
+ if (!from.renameTo(to)) {
+ throw new IOException();
+ }
+ }
+
+ /**
+ * Returns a snapshot of the entry named {@code key}, or null if it doesn't
+ * exist is not currently readable. If a value is returned, it is moved to
+ * the head of the LRU queue.
+ */
+ public synchronized Snapshot get(String key) throws IOException {
+ checkNotClosed();
+ validateKey(key);
+ Entry entry = lruEntries.get(key);
+ if (entry == null) {
+ return null;
+ }
+
+ if (!entry.readable) {
+ return null;
+ }
+
+ // Open all streams eagerly to guarantee that we see a single published
+ // snapshot. If we opened streams lazily then the streams could come
+ // from different edits.
+ InputStream[] ins = new InputStream[valueCount];
+ try {
+ for (int i = 0; i < valueCount; i++) {
+ ins[i] = new FileInputStream(entry.getCleanFile(i));
+ }
+ } catch (FileNotFoundException e) {
+ // A file must have been deleted manually!
+ for (int i = 0; i < valueCount; i++) {
+ if (ins[i] != null) {
+ Util.closeQuietly(ins[i]);
+ } else {
+ break;
+ }
+ }
+ return null;
+ }
+
+ redundantOpCount++;
+ journalWriter.append(READ + ' ' + key + '\n');
+ if (journalRebuildRequired()) {
+ executorService.submit(cleanupCallable);
+ }
+
+ return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
+ }
+
+ /**
+ * Returns an editor for the entry named {@code key}, or null if another
+ * edit is in progress.
+ */
+ public Editor edit(String key) throws IOException {
+ return edit(key, ANY_SEQUENCE_NUMBER);
+ }
+
+ private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
+ checkNotClosed();
+ validateKey(key);
+ Entry entry = lruEntries.get(key);
+ if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
+ || entry.sequenceNumber != expectedSequenceNumber)) {
+ return null; // Snapshot is stale.
+ }
+ if (entry == null) {
+ entry = new Entry(key);
+ lruEntries.put(key, entry);
+ } else if (entry.currentEditor != null) {
+ return null; // Another edit is in progress.
+ }
+
+ Editor editor = new Editor(entry);
+ entry.currentEditor = editor;
+
+ // Flush the journal before creating files to prevent file leaks.
+ journalWriter.write(DIRTY + ' ' + key + '\n');
+ journalWriter.flush();
+ return editor;
+ }
+
+ /** Returns the directory where this cache stores its data. */
+ public File getDirectory() {
+ return directory;
+ }
+
+ /**
+ * Returns the maximum number of bytes that this cache should use to store
+ * its data.
+ */
+ public synchronized long getMaxSize() {
+ return maxSize;
+ }
+
+ /**
+ * Changes the maximum number of bytes the cache can store and queues a job
+ * to trim the existing store, if necessary.
+ */
+ public synchronized void setMaxSize(long maxSize) {
+ this.maxSize = maxSize;
+ executorService.submit(cleanupCallable);
+ }
+
+ /**
+ * Returns the number of bytes currently being used to store the values in
+ * this cache. This may be greater than the max size if a background
+ * deletion is pending.
+ */
+ public synchronized long size() {
+ return size;
+ }
+
+ private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
+ Entry entry = editor.entry;
+ if (entry.currentEditor != editor) {
+ throw new IllegalStateException();
+ }
+
+ // If this edit is creating the entry for the first time, every index must have a value.
+ if (success && !entry.readable) {
+ for (int i = 0; i < valueCount; i++) {
+ if (!editor.written[i]) {
+ editor.abort();
+ throw new IllegalStateException("Newly created entry didn't create value for index " + i);
+ }
+ if (!entry.getDirtyFile(i).exists()) {
+ editor.abort();
+ return;
+ }
+ }
+ }
+
+ for (int i = 0; i < valueCount; i++) {
+ File dirty = entry.getDirtyFile(i);
+ if (success) {
+ if (dirty.exists()) {
+ File clean = entry.getCleanFile(i);
+ dirty.renameTo(clean);
+ long oldLength = entry.lengths[i];
+ long newLength = clean.length();
+ entry.lengths[i] = newLength;
+ size = size - oldLength + newLength;
+ }
+ } else {
+ deleteIfExists(dirty);
+ }
+ }
+
+ redundantOpCount++;
+ entry.currentEditor = null;
+ if (entry.readable | success) {
+ entry.readable = true;
+ journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+ if (success) {
+ entry.sequenceNumber = nextSequenceNumber++;
+ }
+ } else {
+ lruEntries.remove(entry.key);
+ journalWriter.write(REMOVE + ' ' + entry.key + '\n');
+ }
+ journalWriter.flush();
+
+ if (size > maxSize || journalRebuildRequired()) {
+ executorService.submit(cleanupCallable);
+ }
+ }
+
+ /**
+ * We only rebuild the journal when it will halve the size of the journal
+ * and eliminate at least 2000 ops.
+ */
+ private boolean journalRebuildRequired() {
+ final int redundantOpCompactThreshold = 2000;
+ return redundantOpCount >= redundantOpCompactThreshold //
+ && redundantOpCount >= lruEntries.size();
+ }
+
+ /**
+ * Drops the entry for {@code key} if it exists and can be removed. Entries
+ * actively being edited cannot be removed.
+ *
+ * @return true if an entry was removed.
+ */
+ public synchronized boolean remove(String key) throws IOException {
+ checkNotClosed();
+ validateKey(key);
+ Entry entry = lruEntries.get(key);
+ if (entry == null || entry.currentEditor != null) {
+ return false;
+ }
+
+ for (int i = 0; i < valueCount; i++) {
+ File file = entry.getCleanFile(i);
+ if (file.exists() && !file.delete()) {
+ throw new IOException("failed to delete " + file);
+ }
+ size -= entry.lengths[i];
+ entry.lengths[i] = 0;
+ }
+
+ redundantOpCount++;
+ journalWriter.append(REMOVE + ' ' + key + '\n');
+ lruEntries.remove(key);
+
+ if (journalRebuildRequired()) {
+ executorService.submit(cleanupCallable);
+ }
+
+ return true;
+ }
+
+ /** Returns true if this cache has been closed. */
+ public synchronized boolean isClosed() {
+ return journalWriter == null;
+ }
+
+ private void checkNotClosed() {
+ if (journalWriter == null) {
+ throw new IllegalStateException("cache is closed");
+ }
+ }
+
+ /** Force buffered operations to the filesystem. */
+ public synchronized void flush() throws IOException {
+ checkNotClosed();
+ trimToSize();
+ journalWriter.flush();
+ }
+
+ /** Closes this cache. Stored values will remain on the filesystem. */
+ public synchronized void close() throws IOException {
+ if (journalWriter == null) {
+ return; // Already closed.
+ }
+ for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
+ if (entry.currentEditor != null) {
+ entry.currentEditor.abort();
+ }
+ }
+ trimToSize();
+ journalWriter.close();
+ journalWriter = null;
+ }
+
+ private void trimToSize() throws IOException {
+ while (size > maxSize) {
+ Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
+ remove(toEvict.getKey());
+ }
+ }
+
+ /**
+ * Closes the cache and deletes all of its stored values. This will delete
+ * all files in the cache directory including files that weren't created by
+ * the cache.
+ */
+ public void delete() throws IOException {
+ close();
+ Util.deleteContents(directory);
+ }
+
+ private void validateKey(String key) {
+ Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
+ if (!matcher.matches()) {
+ throw new IllegalArgumentException("keys must match regex "
+ + STRING_KEY_PATTERN + ": \"" + key + "\"");
+ }
+ }
+
+ private static String inputStreamToString(InputStream in) throws IOException {
+ return Util.readFully(new InputStreamReader(in, Util.UTF_8));
+ }
+
+ /** A snapshot of the values for an entry. */
+ public final class Snapshot implements Closeable {
+ private final String key;
+ private final long sequenceNumber;
+ private final InputStream[] ins;
+ private final long[] lengths;
+
+ private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) {
+ this.key = key;
+ this.sequenceNumber = sequenceNumber;
+ this.ins = ins;
+ this.lengths = lengths;
+ }
+
+ /**
+ * Returns an editor for this snapshot's entry, or null if either the
+ * entry has changed since this snapshot was created or if another edit
+ * is in progress.
+ */
+ public Editor edit() throws IOException {
+ return DiskLruCache.this.edit(key, sequenceNumber);
+ }
+
+ /** Returns the unbuffered stream with the value for {@code index}. */
+ public InputStream getInputStream(int index) {
+ return ins[index];
+ }
+
+ /** Returns the string value for {@code index}. */
+ public String getString(int index) throws IOException {
+ return inputStreamToString(getInputStream(index));
+ }
+
+ /** Returns the byte length of the value for {@code index}. */
+ public long getLength(int index) {
+ return lengths[index];
+ }
+
+ public void close() {
+ for (InputStream in : ins) {
+ Util.closeQuietly(in);
+ }
+ }
+ }
+
+ private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() {
+ @Override
+ public void write(int b) throws IOException {
+ // Eat all writes silently. Nom nom.
+ }
+ };
+
+ /** Edits the values for an entry. */
+ public final class Editor {
+ private final Entry entry;
+ private final boolean[] written;
+ private boolean hasErrors;
+ private boolean committed;
+
+ private Editor(Entry entry) {
+ this.entry = entry;
+ this.written = (entry.readable) ? null : new boolean[valueCount];
+ }
+
+ /**
+ * Returns an unbuffered input stream to read the last committed value,
+ * or null if no value has been committed.
+ */
+ public InputStream newInputStream(int index) throws IOException {
+ synchronized (DiskLruCache.this) {
+ if (entry.currentEditor != this) {
+ throw new IllegalStateException();
+ }
+ if (!entry.readable) {
+ return null;
+ }
+ try {
+ return new FileInputStream(entry.getCleanFile(index));
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Returns the last committed value as a string, or null if no value
+ * has been committed.
+ */
+ public String getString(int index) throws IOException {
+ InputStream in = newInputStream(index);
+ return in != null ? inputStreamToString(in) : null;
+ }
+
+ /**
+ * Returns a new unbuffered output stream to write the value at
+ * {@code index}. If the underlying output stream encounters errors
+ * when writing to the filesystem, this edit will be aborted when
+ * {@link #commit} is called. The returned output stream does not throw
+ * IOExceptions.
+ */
+ public OutputStream newOutputStream(int index) throws IOException {
+ if (index < 0 || index >= valueCount) {
+ throw new IllegalArgumentException("Expected index " + index + " to "
+ + "be greater than 0 and less than the maximum value count "
+ + "of " + valueCount);
+ }
+ synchronized (DiskLruCache.this) {
+ if (entry.currentEditor != this) {
+ throw new IllegalStateException();
+ }
+ if (!entry.readable) {
+ written[index] = true;
+ }
+ File dirtyFile = entry.getDirtyFile(index);
+ FileOutputStream outputStream;
+ try {
+ outputStream = new FileOutputStream(dirtyFile);
+ } catch (FileNotFoundException e) {
+ // Attempt to recreate the cache directory.
+ directory.mkdirs();
+ try {
+ outputStream = new FileOutputStream(dirtyFile);
+ } catch (FileNotFoundException e2) {
+ // We are unable to recover. Silently eat the writes.
+ return NULL_OUTPUT_STREAM;
+ }
+ }
+ return new FaultHidingOutputStream(outputStream);
+ }
+ }
+
+ /** Sets the value at {@code index} to {@code value}. */
+ public void set(int index, String value) throws IOException {
+ Writer writer = null;
+ try {
+ writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8);
+ writer.write(value);
+ } finally {
+ Util.closeQuietly(writer);
+ }
+ }
+
+ /**
+ * Commits this edit so it is visible to readers. This releases the
+ * edit lock so another edit may be started on the same key.
+ */
+ public void commit() throws IOException {
+ if (hasErrors) {
+ completeEdit(this, false);
+ remove(entry.key); // The previous entry is stale.
+ } else {
+ completeEdit(this, true);
+ }
+ committed = true;
+ }
+
+ /**
+ * Aborts this edit. This releases the edit lock so another edit may be
+ * started on the same key.
+ */
+ public void abort() throws IOException {
+ completeEdit(this, false);
+ }
+
+ public void abortUnlessCommitted() {
+ if (!committed) {
+ try {
+ abort();
+ } catch (IOException ignored) {
+ }
+ }
+ }
+
+ private class FaultHidingOutputStream extends FilterOutputStream {
+ private FaultHidingOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ @Override public void write(int oneByte) {
+ try {
+ out.write(oneByte);
+ } catch (IOException e) {
+ hasErrors = true;
+ }
+ }
+
+ @Override public void write(byte[] buffer, int offset, int length) {
+ try {
+ out.write(buffer, offset, length);
+ } catch (IOException e) {
+ hasErrors = true;
+ }
+ }
+
+ @Override public void close() {
+ try {
+ out.close();
+ } catch (IOException e) {
+ hasErrors = true;
+ }
+ }
+
+ @Override public void flush() {
+ try {
+ out.flush();
+ } catch (IOException e) {
+ hasErrors = true;
+ }
+ }
+ }
+ }
+
+ private final class Entry {
+ private final String key;
+
+ /** Lengths of this entry's files. */
+ private final long[] lengths;
+
+ /** True if this entry has ever been published. */
+ private boolean readable;
+
+ /** The ongoing edit or null if this entry is not being edited. */
+ private Editor currentEditor;
+
+ /** The sequence number of the most recently committed edit to this entry. */
+ private long sequenceNumber;
+
+ private Entry(String key) {
+ this.key = key;
+ this.lengths = new long[valueCount];
+ }
+
+ public String getLengths() throws IOException {
+ StringBuilder result = new StringBuilder();
+ for (long size : lengths) {
+ result.append(' ').append(size);
+ }
+ return result.toString();
+ }
+
+ /** Set lengths using decimal numbers like "10123". */
+ private void setLengths(String[] strings) throws IOException {
+ if (strings.length != valueCount) {
+ throw invalidLengths(strings);
+ }
+
+ try {
+ for (int i = 0; i < strings.length; i++) {
+ lengths[i] = Long.parseLong(strings[i]);
+ }
+ } catch (NumberFormatException e) {
+ throw invalidLengths(strings);
+ }
+ }
+
+ private IOException invalidLengths(String[] strings) throws IOException {
+ throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
+ }
+
+ public File getCleanFile(int i) {
+ return new File(directory, key + "." + i);
+ }
+
+ public File getDirtyFile(int i) {
+ return new File(directory, key + "." + i + ".tmp");
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/jakewharton/disklrucache/StrictLineReader.java b/mobile/android/thirdparty/com/jakewharton/disklrucache/StrictLineReader.java
new file mode 100644
index 0000000000..c90691ce45
--- /dev/null
+++ b/mobile/android/thirdparty/com/jakewharton/disklrucache/StrictLineReader.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jakewharton.disklrucache;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+
+/**
+ * Buffers input from an {@link InputStream} for reading lines.
+ *
+ * <p>This class is used for buffered reading of lines. For purposes of this class, a line ends
+ * with "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated
+ * line at end of input is invalid and will be ignored, the caller may use {@code
+ * hasUnterminatedLine()} to detect it after catching the {@code EOFException}.
+ *
+ * <p>This class is intended for reading input that strictly consists of lines, such as line-based
+ * cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction
+ * with {@link java.io.InputStreamReader} provides similar functionality, this class uses different
+ * end-of-input reporting and a more restrictive definition of a line.
+ *
+ * <p>This class supports only charsets that encode '\r' and '\n' as a single byte with value 13
+ * and 10, respectively, and the representation of no other character contains these values.
+ * We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1.
+ * The default charset is US_ASCII.
+ */
+class StrictLineReader implements Closeable {
+ private static final byte CR = (byte) '\r';
+ private static final byte LF = (byte) '\n';
+
+ private final InputStream in;
+ private final Charset charset;
+
+ /*
+ * Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
+ * and the data in the range [pos, end) is buffered for reading. At end of input, if there is
+ * an unterminated line, we set end == -1, otherwise end == pos. If the underlying
+ * {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
+ */
+ private byte[] buf;
+ private int pos;
+ private int end;
+
+ /**
+ * Constructs a new {@code LineReader} with the specified charset and the default capacity.
+ *
+ * @param in the {@code InputStream} to read data from.
+ * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
+ * supported.
+ * @throws NullPointerException if {@code in} or {@code charset} is null.
+ * @throws IllegalArgumentException if the specified charset is not supported.
+ */
+ public StrictLineReader(InputStream in, Charset charset) {
+ this(in, 8192, charset);
+ }
+
+ /**
+ * Constructs a new {@code LineReader} with the specified capacity and charset.
+ *
+ * @param in the {@code InputStream} to read data from.
+ * @param capacity the capacity of the buffer.
+ * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
+ * supported.
+ * @throws NullPointerException if {@code in} or {@code charset} is null.
+ * @throws IllegalArgumentException if {@code capacity} is negative or zero
+ * or the specified charset is not supported.
+ */
+ public StrictLineReader(InputStream in, int capacity, Charset charset) {
+ if (in == null || charset == null) {
+ throw new NullPointerException();
+ }
+ if (capacity < 0) {
+ throw new IllegalArgumentException("capacity <= 0");
+ }
+ if (!(charset.equals(Util.US_ASCII))) {
+ throw new IllegalArgumentException("Unsupported encoding");
+ }
+
+ this.in = in;
+ this.charset = charset;
+ buf = new byte[capacity];
+ }
+
+ /**
+ * Closes the reader by closing the underlying {@code InputStream} and
+ * marking this reader as closed.
+ *
+ * @throws IOException for errors when closing the underlying {@code InputStream}.
+ */
+ public void close() throws IOException {
+ synchronized (in) {
+ if (buf != null) {
+ buf = null;
+ in.close();
+ }
+ }
+ }
+
+ /**
+ * Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"},
+ * this end of line marker is not included in the result.
+ *
+ * @return the next line from the input.
+ * @throws IOException for underlying {@code InputStream} errors.
+ * @throws EOFException for the end of source stream.
+ */
+ public String readLine() throws IOException {
+ synchronized (in) {
+ if (buf == null) {
+ throw new IOException("LineReader is closed");
+ }
+
+ // Read more data if we are at the end of the buffered data.
+ // Though it's an error to read after an exception, we will let {@code fillBuf()}
+ // throw again if that happens; thus we need to handle end == -1 as well as end == pos.
+ if (pos >= end) {
+ fillBuf();
+ }
+ // Try to find LF in the buffered data and return the line if successful.
+ for (int i = pos; i != end; ++i) {
+ if (buf[i] == LF) {
+ int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;
+ String res = new String(buf, pos, lineEnd - pos, charset.name());
+ pos = i + 1;
+ return res;
+ }
+ }
+
+ // Let's anticipate up to 80 characters on top of those already read.
+ ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {
+ @Override
+ public String toString() {
+ int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
+ try {
+ return new String(buf, 0, length, charset.name());
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e); // Since we control the charset this will never happen.
+ }
+ }
+ };
+
+ while (true) {
+ out.write(buf, pos, end - pos);
+ // Mark unterminated line in case fillBuf throws EOFException or IOException.
+ end = -1;
+ fillBuf();
+ // Try to find LF in the buffered data and return the line if successful.
+ for (int i = pos; i != end; ++i) {
+ if (buf[i] == LF) {
+ if (i != pos) {
+ out.write(buf, pos, i - pos);
+ }
+ pos = i + 1;
+ return out.toString();
+ }
+ }
+ }
+ }
+ }
+
+ public boolean hasUnterminatedLine() {
+ return end == -1;
+ }
+
+ /**
+ * Reads new input data into the buffer. Call only with pos == end or end == -1,
+ * depending on the desired outcome if the function throws.
+ */
+ private void fillBuf() throws IOException {
+ int result = in.read(buf, 0, buf.length);
+ if (result == -1) {
+ throw new EOFException();
+ }
+ pos = 0;
+ end = result;
+ }
+}
+
diff --git a/mobile/android/thirdparty/com/jakewharton/disklrucache/Util.java b/mobile/android/thirdparty/com/jakewharton/disklrucache/Util.java
new file mode 100644
index 0000000000..0a7dba9bc2
--- /dev/null
+++ b/mobile/android/thirdparty/com/jakewharton/disklrucache/Util.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jakewharton.disklrucache;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.nio.charset.Charset;
+
+/** Junk drawer of utility methods. */
+final class Util {
+ static final Charset US_ASCII = Charset.forName("US-ASCII");
+ static final Charset UTF_8 = Charset.forName("UTF-8");
+
+ private Util() {
+ }
+
+ static String readFully(Reader reader) throws IOException {
+ try {
+ StringWriter writer = new StringWriter();
+ char[] buffer = new char[1024];
+ int count;
+ while ((count = reader.read(buffer)) != -1) {
+ writer.write(buffer, 0, count);
+ }
+ return writer.toString();
+ } finally {
+ reader.close();
+ }
+ }
+
+ /**
+ * Deletes the contents of {@code dir}. Throws an IOException if any file
+ * could not be deleted, or if {@code dir} is not a readable directory.
+ */
+ static void deleteContents(File dir) throws IOException {
+ File[] files = dir.listFiles();
+ if (files == null) {
+ throw new IOException("not a readable directory: " + dir);
+ }
+ for (File file : files) {
+ if (file.isDirectory()) {
+ deleteContents(file);
+ }
+ if (!file.delete()) {
+ throw new IOException("failed to delete file: " + file);
+ }
+ }
+ }
+
+ static void closeQuietly(/*Auto*/Closeable closeable) {
+ if (closeable != null) {
+ try {
+ closeable.close();
+ } catch (RuntimeException rethrown) {
+ throw rethrown;
+ } catch (Exception ignored) {
+ }
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/keepsafe/switchboard/AsyncConfigLoader.java b/mobile/android/thirdparty/com/keepsafe/switchboard/AsyncConfigLoader.java
new file mode 100644
index 0000000000..2cff4b4c36
--- /dev/null
+++ b/mobile/android/thirdparty/com/keepsafe/switchboard/AsyncConfigLoader.java
@@ -0,0 +1,54 @@
+/*
+ Copyright 2012 KeepSafe Software Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+package com.keepsafe.switchboard;
+
+
+import android.content.Context;
+import android.os.AsyncTask;
+
+/**
+ * An async loader to load user config in background thread based on internal generated UUID.
+ *
+ * Call <code>AsyncConfigLoader.execute()</code> to load SwitchBoard.loadConfig() with own ID.
+ * To use your custom UUID call <code>AsyncConfigLoader.execute(uuid)</code> with uuid being your unique user id
+ * as a String
+ *
+ * @author Philipp Berner
+ *
+ */
+public class AsyncConfigLoader extends AsyncTask<Void, Void, Void> {
+
+ private Context context;
+ private String defaultServerUrl;
+
+ /**
+ * Sets the params for async loading either SwitchBoard.updateConfigServerUrl()
+ * or SwitchBoard.loadConfig.
+ * Loads config with a custom UUID
+ * @param c Application context
+ * @param defaultServerUrl Default URL endpoint for Switchboard config.
+ */
+ public AsyncConfigLoader(Context c, String defaultServerUrl) {
+ this.context = c;
+ this.defaultServerUrl = defaultServerUrl;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ SwitchBoard.loadConfig(context, defaultServerUrl);
+ return null;
+ }
+}
diff --git a/mobile/android/thirdparty/com/keepsafe/switchboard/DeviceUuidFactory.java b/mobile/android/thirdparty/com/keepsafe/switchboard/DeviceUuidFactory.java
new file mode 100644
index 0000000000..c4476d2cd0
--- /dev/null
+++ b/mobile/android/thirdparty/com/keepsafe/switchboard/DeviceUuidFactory.java
@@ -0,0 +1,70 @@
+/*
+ Copyright 2012 KeepSafe Software Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+package com.keepsafe.switchboard;
+
+import java.util.UUID;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+/**
+ * Generates a UUID and stores is persistent as in the apps shared preferences.
+ *
+ * @author Philipp Berner
+ */
+public class DeviceUuidFactory {
+ protected static final String PREFS_FILE = "com.keepsafe.switchboard.uuid";
+ protected static final String PREFS_DEVICE_ID = "device_id";
+
+ private static UUID uuid = null;
+
+ public DeviceUuidFactory(Context context) {
+ if (uuid == null) {
+ synchronized (DeviceUuidFactory.class) {
+ if (uuid == null) {
+ final SharedPreferences prefs = context
+ .getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
+ final String id = prefs.getString(PREFS_DEVICE_ID, null);
+
+ if (id != null) {
+ // Use the ids previously computed and stored in the prefs file
+ uuid = UUID.fromString(id);
+ } else {
+ uuid = UUID.randomUUID();
+
+ // Write the value out to the prefs file
+ prefs.edit().putString(PREFS_DEVICE_ID, uuid.toString()).apply();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns a unique UUID for the current android device. As with all UUIDs,
+ * this unique ID is "very highly likely" to be unique across all Android
+ * devices. Much more so than ANDROID_ID is.
+ *
+ * The UUID is generated with <code>UUID.randomUUID()</code>.
+ *
+ * @return a UUID that may be used to uniquely identify your device for most
+ * purposes.
+ */
+ public UUID getDeviceUuid() {
+ return uuid;
+ }
+
+} \ No newline at end of file
diff --git a/mobile/android/thirdparty/com/keepsafe/switchboard/Preferences.java b/mobile/android/thirdparty/com/keepsafe/switchboard/Preferences.java
new file mode 100644
index 0000000000..f7f6f7cb78
--- /dev/null
+++ b/mobile/android/thirdparty/com/keepsafe/switchboard/Preferences.java
@@ -0,0 +1,105 @@
+/*
+ Copyright 2012 KeepSafe Software Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+package com.keepsafe.switchboard;
+
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.support.annotation.Nullable;
+
+/**
+ * Application preferences for SwitchBoard.
+ * @author Philipp Berner
+ *
+ */
+public class Preferences {
+
+ private static final String switchBoardSettings = "com.keepsafe.switchboard.settings";
+
+ private static final String CONFIG_JSON = "config-json";
+ private static final String OVERRIDE_PREFIX = "experiment.override.";
+
+
+ /**
+ * Gets the user config as a JSON string.
+ * @param c Context
+ * @return Config JSON
+ */
+ @Nullable public static String getDynamicConfigJson(Context c) {
+ final SharedPreferences prefs = c.getApplicationContext().getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE);
+ return prefs.getString(CONFIG_JSON, null);
+ }
+
+ /**
+ * Saves the user config as a JSON sting.
+ * @param c Context
+ * @param configJson Config JSON
+ */
+ public static void setDynamicConfigJson(Context c, String configJson) {
+ final SharedPreferences.Editor editor = c.getApplicationContext().
+ getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE).edit();
+ editor.putString(CONFIG_JSON, configJson);
+ editor.apply();
+ }
+
+ /**
+ * Gets the override value for an experiment.
+ *
+ * @param c Context
+ * @param experimentName Experiment name
+ * @return Whether or not the experiment should be enabled, or null if there is no override.
+ */
+ @Nullable public static Boolean getOverrideValue(Context c, String experimentName) {
+ final SharedPreferences prefs = c.getApplicationContext().
+ getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE);
+
+ final String key = OVERRIDE_PREFIX + experimentName;
+ if (prefs.contains(key)) {
+ // This will never fall back to the default value.
+ return prefs.getBoolean(key, false);
+ }
+
+ // Default to returning null if no override was found.
+ return null;
+ }
+
+ /**
+ * Saves an override value for an experiment.
+ *
+ * @param c Context
+ * @param experimentName Experiment name
+ * @param isEnabled Whether or not to enable the experiment
+ */
+ public static void setOverrideValue(Context c, String experimentName, boolean isEnabled) {
+ final SharedPreferences.Editor editor = c.getApplicationContext().
+ getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE).edit();
+ editor.putBoolean(OVERRIDE_PREFIX + experimentName, isEnabled);
+ editor.apply();
+ }
+
+ /**
+ * Clears the override value for an experiment.
+ *
+ * @param c Context
+ * @param experimentName Experiment name
+ */
+ public static void clearOverrideValue(Context c, String experimentName) {
+ final SharedPreferences.Editor editor = c.getApplicationContext().
+ getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE).edit();
+ editor.remove(OVERRIDE_PREFIX + experimentName);
+ editor.apply();
+ }
+}
diff --git a/mobile/android/thirdparty/com/keepsafe/switchboard/Switch.java b/mobile/android/thirdparty/com/keepsafe/switchboard/Switch.java
new file mode 100644
index 0000000000..5307750bbc
--- /dev/null
+++ b/mobile/android/thirdparty/com/keepsafe/switchboard/Switch.java
@@ -0,0 +1,72 @@
+/*
+ Copyright 2012 KeepSafe Software Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+package com.keepsafe.switchboard;
+
+import org.json.JSONObject;
+
+import android.content.Context;
+
+/**
+ * Single instance of an existing experiment for easier and cleaner code.
+ *
+ * @author Philipp Berner
+ *
+ */
+public class Switch {
+
+ private Context context;
+ private String experimentName;
+
+ /**
+ * Creates an instance of a single experiment to give more convenient access to its values.
+ * When the given experiment does not exist, it will give back default valued that can be found
+ * in <code>Switchboard</code>. Developer has to know that experiment exists when using it.
+ * @param c Application context
+ * @param experimentName Name of the experiment as defined on the server
+ */
+ public Switch(Context c, String experimentName) {
+ this.context = c;
+ this.experimentName = experimentName;
+ }
+
+ /**
+ * Returns true if the experiment is active for this particular user.
+ * @return Status of the experiment and false when experiment does not exist.
+ */
+ public boolean isActive() {
+ return SwitchBoard.isInExperiment(context, experimentName);
+ }
+
+ /**
+ * Returns true if the experiment has additional values.
+ * @return true when values exist
+ */
+ public boolean hasValues() {
+ return SwitchBoard.hasExperimentValues(context, experimentName);
+ }
+
+ /**
+ * Gives back all the experiment values in a JSONObject. This function checks if
+ * values exists. If no values exist, it returns null.
+ * @return Values in JSONObject or null if non
+ */
+ public JSONObject getValues() {
+ if(hasValues())
+ return SwitchBoard.getExperimentValuesFromJson(context, experimentName);
+ else
+ return null;
+ }
+}
diff --git a/mobile/android/thirdparty/com/keepsafe/switchboard/SwitchBoard.java b/mobile/android/thirdparty/com/keepsafe/switchboard/SwitchBoard.java
new file mode 100644
index 0000000000..e99144045c
--- /dev/null
+++ b/mobile/android/thirdparty/com/keepsafe/switchboard/SwitchBoard.java
@@ -0,0 +1,390 @@
+/*
+ Copyright 2012 KeepSafe Software Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+package com.keepsafe.switchboard;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.MissingResourceException;
+import java.util.zip.CRC32;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.json.JSONArray;
+
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+
+/**
+ * SwitchBoard is the core class of the KeepSafe Switchboard mobile A/B testing framework.
+ * This class provides a bunch of static methods that can be used in your app to run A/B tests.
+ *
+ * The SwitchBoard supports production and staging environment.
+ *
+ * For usage <code>initDefaultServerUrls</code> for first time usage. Server URLs can be updates from
+ * a remote location with <code>initConfigServerUrl</code>.
+ *
+ * To run a experiment use <code>isInExperiment()</code>. The experiment name has to match the one you
+ * setup on the server.
+ * All functions are design to be safe for programming mistakes and network connection issues. If the
+ * experiment does not exists it will return false and pretend the user is not part of it.
+ *
+ * @author Philipp Berner
+ *
+ */
+public class SwitchBoard {
+
+ private static final String TAG = "SwitchBoard";
+
+ /** Set if the application is run in debug mode. */
+ public static boolean DEBUG = true;
+
+ // Top-level experiment keys.
+ private static final String KEY_DATA = "data";
+ private static final String KEY_NAME = "name";
+ private static final String KEY_MATCH = "match";
+ private static final String KEY_BUCKETS = "buckets";
+ private static final String KEY_VALUES = "values";
+
+ // Match keys.
+ private static final String KEY_APP_ID = "appId";
+ private static final String KEY_COUNTRY = "country";
+ private static final String KEY_DEVICE = "device";
+ private static final String KEY_LANG = "lang";
+ private static final String KEY_MANUFACTURER = "manufacturer";
+ private static final String KEY_VERSION = "version";
+
+ // Bucket keys.
+ private static final String KEY_MIN = "min";
+ private static final String KEY_MAX = "max";
+
+ /**
+ * Loads a new config for a user. This method does network I/O, so it
+ * should not be called on the main thread.
+ *
+ * @param c ApplicationContext
+ * @param serverUrl Server URL endpoint.
+ */
+ static void loadConfig(Context c, @NonNull String serverUrl) {
+ final URL url;
+ try {
+ url = new URL(serverUrl);
+ } catch (MalformedURLException e) {
+ Log.e(TAG, "Exception creating server URL", e);
+ return;
+ }
+
+ final String result = readFromUrlGET(url);
+ if (DEBUG) Log.d(TAG, "Result: " + result);
+ if (result == null) {
+ return;
+ }
+
+ // Cache result locally in shared preferences.
+ Preferences.setDynamicConfigJson(c, result);
+ }
+
+ public static boolean isInBucket(Context c, int low, int high) {
+ final int userBucket = getUserBucket(c);
+ return (userBucket >= low) && (userBucket < high);
+ }
+
+ /**
+ * Looks up in config if user is in certain experiment. Returns false as a default value when experiment
+ * does not exist.
+ * Experiment names are defined server side as Key in array for return values.
+ * @param experimentName Name of the experiment to lookup
+ * @return returns value for experiment or false if experiment does not exist.
+ */
+ public static boolean isInExperiment(Context c, String experimentName) {
+ final Boolean override = Preferences.getOverrideValue(c, experimentName);
+ if (override != null) {
+ return override;
+ }
+
+ final String config = Preferences.getDynamicConfigJson(c);
+ if (config == null) {
+ return false;
+ }
+
+ try {
+ // TODO: cache the array into a mapping so we don't do a loop everytime we are looking for a experiment key
+ final JSONArray experiments = new JSONObject(config).getJSONArray(KEY_DATA);
+ JSONObject experiment = null;
+
+ for (int i = 0; i < experiments.length(); i++) {
+ JSONObject entry = experiments.getJSONObject(i);
+ final String name = entry.getString(KEY_NAME);
+ if (name.equals(experimentName)) {
+ experiment = entry;
+ break;
+ }
+ }
+
+ if (experiment == null) {
+ return false;
+ }
+
+ if (!isMatch(c, experiment.optJSONObject(KEY_MATCH))) {
+ return false;
+ }
+
+ final JSONObject buckets = experiment.getJSONObject(KEY_BUCKETS);
+ final boolean inExperiment = isInBucket(c, buckets.getInt(KEY_MIN), buckets.getInt(KEY_MAX));
+
+ if (DEBUG) {
+ Log.d(TAG, experimentName + " = " + inExperiment);
+ }
+ return inExperiment;
+ } catch (JSONException e) {
+ // If the experiment name is not found in the JSON, just return false.
+ // There is no need to log an error, since we don't really care if an
+ // inactive experiment is missing from the config.
+ return false;
+ }
+ }
+
+ private static List<String> getExperimentNames(Context c) throws JSONException {
+ // TODO: cache the array into a mapping so we don't do a loop everytime we are looking for a experiment key
+ final List<String> returnList = new ArrayList<>();
+ final String config = Preferences.getDynamicConfigJson(c);
+ final JSONArray experiments = new JSONObject(config).getJSONArray(KEY_DATA);
+
+ for (int i = 0; i < experiments.length(); i++) {
+ JSONObject entry = experiments.getJSONObject(i);
+ returnList.add(entry.getString(KEY_NAME));
+ }
+ return returnList;
+ }
+
+ @Nullable
+ private static JSONObject getExperiment(Context c, String experimentName) throws JSONException {
+ // TODO: cache the array into a mapping so we don't do a loop everytime we are looking for a experiment key
+ final String config = Preferences.getDynamicConfigJson(c);
+ final JSONArray experiments = new JSONObject(config).getJSONArray(KEY_DATA);
+ JSONObject experiment = null;
+
+ for (int i = 0; i < experiments.length(); i++) {
+ JSONObject entry = experiments.getJSONObject(i);
+ if (entry.getString(KEY_NAME).equals(experimentName)) {
+ experiment = entry;
+ break;
+ }
+ }
+ return experiment;
+ }
+
+ private static boolean isMatch(Context c, @Nullable JSONObject matchKeys) {
+ // If no match keys are specified, default to enabling the experiment.
+ if (matchKeys == null) {
+ return true;
+ }
+
+ if (matchKeys.has(KEY_APP_ID)) {
+ final String packageName = c.getPackageName();
+ try {
+ if (!packageName.matches(matchKeys.getString(KEY_APP_ID))) {
+ return false;
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "Exception matching appId", e);
+ }
+ }
+
+ if (matchKeys.has(KEY_COUNTRY)) {
+ try {
+ final String country = Locale.getDefault().getISO3Country();
+ if (!country.matches(matchKeys.getString(KEY_COUNTRY))) {
+ return false;
+ }
+ } catch (MissingResourceException|JSONException e) {
+ Log.e(TAG, "Exception matching country", e);
+ }
+ }
+
+ if (matchKeys.has(KEY_DEVICE)) {
+ final String device = Build.DEVICE;
+ try {
+ if (!device.matches(matchKeys.getString(KEY_DEVICE))) {
+ return false;
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "Exception matching device", e);
+ }
+
+ }
+ if (matchKeys.has(KEY_LANG)) {
+ try {
+ final String lang = Locale.getDefault().getISO3Language();
+ if (!lang.matches(matchKeys.getString(KEY_LANG))) {
+ return false;
+ }
+ } catch (MissingResourceException|JSONException e) {
+ Log.e(TAG, "Exception matching lang", e);
+ }
+ }
+ if (matchKeys.has(KEY_MANUFACTURER)) {
+ final String manufacturer = Build.MANUFACTURER;
+ try {
+ if (!manufacturer.matches(matchKeys.getString(KEY_MANUFACTURER))) {
+ return false;
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "Exception matching manufacturer", e);
+ }
+ }
+
+ if (matchKeys.has(KEY_VERSION)) {
+ try {
+ final String version = c.getPackageManager().getPackageInfo(c.getPackageName(), 0).versionName;
+ if (!version.matches(matchKeys.getString(KEY_VERSION))) {
+ return false;
+ }
+ } catch (NameNotFoundException|JSONException e) {
+ Log.e(TAG, "Exception matching version", e);
+ }
+ }
+
+ // Default to return true if no matches failed.
+ return true;
+ }
+
+ /**
+ * @return a list of all active experiments.
+ */
+ public static List<String> getActiveExperiments(Context c) {
+ final List<String> returnList = new ArrayList<>();
+
+ final String config = Preferences.getDynamicConfigJson(c);
+ if (config == null) {
+ return returnList;
+ }
+
+ try {
+ final JSONObject data = new JSONObject(config);
+ final List<String> experiments = getExperimentNames(c);
+
+ for (int i = 0; i < experiments.size(); i++) {
+ final String name = experiments.get(i);
+
+ // Check override value before reading saved JSON.
+ Boolean isActive = Preferences.getOverrideValue(c, name);
+ if (isActive == null) {
+ // TODO: This is inefficient because it will check all the match cases on all experiments.
+ isActive = isInExperiment(c, name);
+ }
+ if (isActive) {
+ returnList.add(name);
+ }
+ }
+ } catch (JSONException e) {
+ // Something went wrong!
+ }
+
+ return returnList;
+ }
+
+ /**
+ * Checks if a certain experiment has additional values.
+ * @param c ApplicationContext
+ * @param experimentName Name of the experiment
+ * @return true when experiment exists
+ */
+ public static boolean hasExperimentValues(Context c, String experimentName) {
+ return getExperimentValuesFromJson(c, experimentName) != null;
+ }
+
+ /**
+ * Returns the experiment value as a JSONObject.
+ * @param experimentName Name of the experiment
+ * @return Experiment value as String, null if experiment does not exist.
+ */
+ @Nullable
+ public static JSONObject getExperimentValuesFromJson(Context c, String experimentName) {
+ final String config = Preferences.getDynamicConfigJson(c);
+
+ if (config == null) {
+ return null;
+ }
+
+ try {
+ final JSONObject experiment = getExperiment(c, experimentName);
+ if (experiment == null) {
+ return null;
+ }
+ return experiment.getJSONObject(KEY_VALUES);
+ } catch (JSONException e) {
+ Log.e(TAG, "Could not create JSON object from config string", e);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a String containing the server response from a GET request
+ * @param url URL for GET request.
+ * @return Returns String from server or null when failed.
+ */
+ @Nullable private static String readFromUrlGET(URL url) {
+ try {
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestMethod("GET");
+ connection.setUseCaches(false);
+
+ InputStream is = connection.getInputStream();
+ InputStreamReader inputStreamReader = new InputStreamReader(is);
+ BufferedReader bufferReader = new BufferedReader(inputStreamReader, 8192);
+ String line;
+ StringBuilder resultContent = new StringBuilder();
+ while ((line = bufferReader.readLine()) != null) {
+ resultContent.append(line);
+ }
+ bufferReader.close();
+
+ return resultContent.toString();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ /**
+ * Return the bucket number of the user. There are 100 possible buckets.
+ */
+ private static int getUserBucket(Context c) {
+ final DeviceUuidFactory df = new DeviceUuidFactory(c);
+ final String uuid = df.getDeviceUuid().toString();
+
+ CRC32 crc = new CRC32();
+ crc.update(uuid.getBytes());
+ long checksum = crc.getValue();
+ return (int)(checksum % 100L);
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/leakcanary/LeakCanary.java b/mobile/android/thirdparty/com/squareup/leakcanary/LeakCanary.java
new file mode 100644
index 0000000000..a9e44437d7
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/leakcanary/LeakCanary.java
@@ -0,0 +1,21 @@
+/* -*- 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 com.squareup.leakcanary;
+
+import android.app.Application;
+
+/**
+ * A no-op version of {@link LeakCanary} that can be used in release builds.
+ */
+public final class LeakCanary {
+ public static RefWatcher install(Application application) {
+ return RefWatcher.DISABLED;
+ }
+
+ private LeakCanary() {
+ throw new AssertionError();
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/leakcanary/RefWatcher.java b/mobile/android/thirdparty/com/squareup/leakcanary/RefWatcher.java
new file mode 100644
index 0000000000..3c75dcdee5
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/leakcanary/RefWatcher.java
@@ -0,0 +1,20 @@
+/* -*- 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 com.squareup.leakcanary;
+
+/**
+ * No-op implementation of {@link RefWatcher} for release builds. Please use {@link
+ * RefWatcher#DISABLED}.
+ */
+public final class RefWatcher {
+ public static final RefWatcher DISABLED = new RefWatcher();
+
+ private RefWatcher() {}
+
+ public void watch(Object watchedReference) {}
+
+ public void watch(Object watchedReference, String referenceName) {}
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Action.java b/mobile/android/thirdparty/com/squareup/picasso/Action.java
new file mode 100644
index 0000000000..5f176d770c
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Action.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+
+abstract class Action<T> {
+ static class RequestWeakReference<T> extends WeakReference<T> {
+ final Action action;
+
+ public RequestWeakReference(Action action, T referent, ReferenceQueue<? super T> q) {
+ super(referent, q);
+ this.action = action;
+ }
+ }
+
+ final Picasso picasso;
+ final Request data;
+ final WeakReference<T> target;
+ final boolean skipCache;
+ final boolean noFade;
+ final int errorResId;
+ final Drawable errorDrawable;
+ final String key;
+
+ boolean cancelled;
+
+ Action(Picasso picasso, T target, Request data, boolean skipCache, boolean noFade,
+ int errorResId, Drawable errorDrawable, String key) {
+ this.picasso = picasso;
+ this.data = data;
+ this.target = new RequestWeakReference<T>(this, target, picasso.referenceQueue);
+ this.skipCache = skipCache;
+ this.noFade = noFade;
+ this.errorResId = errorResId;
+ this.errorDrawable = errorDrawable;
+ this.key = key;
+ }
+
+ abstract void complete(Bitmap result, Picasso.LoadedFrom from);
+
+ abstract void error();
+
+ void cancel() {
+ cancelled = true;
+ }
+
+ Request getData() {
+ return data;
+ }
+
+ T getTarget() {
+ return target.get();
+ }
+
+ String getKey() {
+ return key;
+ }
+
+ boolean isCancelled() {
+ return cancelled;
+ }
+
+ Picasso getPicasso() {
+ return picasso;
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/AssetBitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/AssetBitmapHunter.java
new file mode 100644
index 0000000000..c0245ed3fa
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/AssetBitmapHunter.java
@@ -0,0 +1,51 @@
+package com.squareup.picasso;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import java.io.IOException;
+import java.io.InputStream;
+
+import static com.squareup.picasso.Picasso.LoadedFrom.DISK;
+
+class AssetBitmapHunter extends BitmapHunter {
+ private AssetManager assetManager;
+
+ public AssetBitmapHunter(Context context, Picasso picasso, Dispatcher dispatcher, Cache cache,
+ Stats stats, Action action) {
+ super(picasso, dispatcher, cache, stats, action);
+ assetManager = context.getAssets();
+ }
+
+ @Override Bitmap decode(Request data) throws IOException {
+ String filePath = data.uri.toString().substring(ASSET_PREFIX_LENGTH);
+ return decodeAsset(filePath);
+ }
+
+ @Override Picasso.LoadedFrom getLoadedFrom() {
+ return DISK;
+ }
+
+ Bitmap decodeAsset(String filePath) throws IOException {
+ BitmapFactory.Options options = null;
+ if (data.hasSize()) {
+ options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ InputStream is = null;
+ try {
+ is = assetManager.open(filePath);
+ BitmapFactory.decodeStream(is, null, options);
+ } finally {
+ Utils.closeQuietly(is);
+ }
+ calculateInSampleSize(data.targetWidth, data.targetHeight, options);
+ }
+ InputStream is = assetManager.open(filePath);
+ try {
+ return BitmapFactory.decodeStream(is, null, options);
+ } finally {
+ Utils.closeQuietly(is);
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/BitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/BitmapHunter.java
new file mode 100644
index 0000000000..b8aa87c484
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/BitmapHunter.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.provider.MediaStore;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Future;
+
+import static android.content.ContentResolver.SCHEME_ANDROID_RESOURCE;
+import static android.content.ContentResolver.SCHEME_CONTENT;
+import static android.content.ContentResolver.SCHEME_FILE;
+import static android.provider.ContactsContract.Contacts;
+import static com.squareup.picasso.Picasso.LoadedFrom.MEMORY;
+
+abstract class BitmapHunter implements Runnable {
+
+ /**
+ * Global lock for bitmap decoding to ensure that we are only are decoding one at a time. Since
+ * this will only ever happen in background threads we help avoid excessive memory thrashing as
+ * well as potential OOMs. Shamelessly stolen from Volley.
+ */
+ private static final Object DECODE_LOCK = new Object();
+ private static final String ANDROID_ASSET = "android_asset";
+ protected static final int ASSET_PREFIX_LENGTH =
+ (SCHEME_FILE + ":///" + ANDROID_ASSET + "/").length();
+
+ final Picasso picasso;
+ final Dispatcher dispatcher;
+ final Cache cache;
+ final Stats stats;
+ final String key;
+ final Request data;
+ final List<Action> actions;
+ final boolean skipMemoryCache;
+
+ Bitmap result;
+ Future<?> future;
+ Picasso.LoadedFrom loadedFrom;
+ Exception exception;
+ int exifRotation; // Determined during decoding of original resource.
+
+ BitmapHunter(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats, Action action) {
+ this.picasso = picasso;
+ this.dispatcher = dispatcher;
+ this.cache = cache;
+ this.stats = stats;
+ this.key = action.getKey();
+ this.data = action.getData();
+ this.skipMemoryCache = action.skipCache;
+ this.actions = new ArrayList<Action>(4);
+ attach(action);
+ }
+
+ protected void setExifRotation(int exifRotation) {
+ this.exifRotation = exifRotation;
+ }
+
+ @Override public void run() {
+ try {
+ Thread.currentThread().setName(Utils.THREAD_PREFIX + data.getName());
+
+ result = hunt();
+
+ if (result == null) {
+ dispatcher.dispatchFailed(this);
+ } else {
+ dispatcher.dispatchComplete(this);
+ }
+ } catch (Downloader.ResponseException e) {
+ exception = e;
+ dispatcher.dispatchFailed(this);
+ } catch (IOException e) {
+ exception = e;
+ dispatcher.dispatchRetry(this);
+ } catch (OutOfMemoryError e) {
+ StringWriter writer = new StringWriter();
+ stats.createSnapshot().dump(new PrintWriter(writer));
+ exception = new RuntimeException(writer.toString(), e);
+ dispatcher.dispatchFailed(this);
+ } catch (Exception e) {
+ exception = e;
+ dispatcher.dispatchFailed(this);
+ } finally {
+ Thread.currentThread().setName(Utils.THREAD_IDLE_NAME);
+ }
+ }
+
+ abstract Bitmap decode(Request data) throws IOException;
+
+ Bitmap hunt() throws IOException {
+ Bitmap bitmap;
+
+ if (!skipMemoryCache) {
+ bitmap = cache.get(key);
+ if (bitmap != null) {
+ stats.dispatchCacheHit();
+ loadedFrom = MEMORY;
+ return bitmap;
+ }
+ }
+
+ bitmap = decode(data);
+
+ if (bitmap != null) {
+ stats.dispatchBitmapDecoded(bitmap);
+ if (data.needsTransformation() || exifRotation != 0) {
+ synchronized (DECODE_LOCK) {
+ if (data.needsMatrixTransform() || exifRotation != 0) {
+ bitmap = transformResult(data, bitmap, exifRotation);
+ }
+ if (data.hasCustomTransformations()) {
+ bitmap = applyCustomTransformations(data.transformations, bitmap);
+ }
+ }
+ stats.dispatchBitmapTransformed(bitmap);
+ }
+ }
+
+ return bitmap;
+ }
+
+ void attach(Action action) {
+ actions.add(action);
+ }
+
+ void detach(Action action) {
+ actions.remove(action);
+ }
+
+ boolean cancel() {
+ return actions.isEmpty() && future != null && future.cancel(false);
+ }
+
+ boolean isCancelled() {
+ return future != null && future.isCancelled();
+ }
+
+ boolean shouldSkipMemoryCache() {
+ return skipMemoryCache;
+ }
+
+ boolean shouldRetry(boolean airplaneMode, NetworkInfo info) {
+ return false;
+ }
+
+ Bitmap getResult() {
+ return result;
+ }
+
+ String getKey() {
+ return key;
+ }
+
+ Request getData() {
+ return data;
+ }
+
+ List<Action> getActions() {
+ return actions;
+ }
+
+ Exception getException() {
+ return exception;
+ }
+
+ Picasso.LoadedFrom getLoadedFrom() {
+ return loadedFrom;
+ }
+
+ static BitmapHunter forRequest(Context context, Picasso picasso, Dispatcher dispatcher,
+ Cache cache, Stats stats, Action action, Downloader downloader) {
+ if (action.getData().resourceId != 0) {
+ return new ResourceBitmapHunter(context, picasso, dispatcher, cache, stats, action);
+ }
+ Uri uri = action.getData().uri;
+ String scheme = uri.getScheme();
+ if (SCHEME_CONTENT.equals(scheme)) {
+ if (Contacts.CONTENT_URI.getHost().equals(uri.getHost()) //
+ && !uri.getPathSegments().contains(Contacts.Photo.CONTENT_DIRECTORY)) {
+ return new ContactsPhotoBitmapHunter(context, picasso, dispatcher, cache, stats, action);
+ } else if (MediaStore.AUTHORITY.equals(uri.getAuthority())) {
+ return new MediaStoreBitmapHunter(context, picasso, dispatcher, cache, stats, action);
+ } else {
+ return new ContentStreamBitmapHunter(context, picasso, dispatcher, cache, stats, action);
+ }
+ } else if (SCHEME_FILE.equals(scheme)) {
+ if (!uri.getPathSegments().isEmpty() && ANDROID_ASSET.equals(uri.getPathSegments().get(0))) {
+ return new AssetBitmapHunter(context, picasso, dispatcher, cache, stats, action);
+ }
+ return new FileBitmapHunter(context, picasso, dispatcher, cache, stats, action);
+ } else if (SCHEME_ANDROID_RESOURCE.equals(scheme)) {
+ return new ResourceBitmapHunter(context, picasso, dispatcher, cache, stats, action);
+ } else {
+ return new NetworkBitmapHunter(picasso, dispatcher, cache, stats, action, downloader);
+ }
+ }
+
+ static void calculateInSampleSize(int reqWidth, int reqHeight, BitmapFactory.Options options) {
+ calculateInSampleSize(reqWidth, reqHeight, options.outWidth, options.outHeight, options);
+ }
+
+ static void calculateInSampleSize(int reqWidth, int reqHeight, int width, int height,
+ BitmapFactory.Options options) {
+ int sampleSize = 1;
+ if (height > reqHeight || width > reqWidth) {
+ final int heightRatio = Math.round((float) height / (float) reqHeight);
+ final int widthRatio = Math.round((float) width / (float) reqWidth);
+ sampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
+ }
+
+ options.inSampleSize = sampleSize;
+ options.inJustDecodeBounds = false;
+ }
+
+ static Bitmap applyCustomTransformations(List<Transformation> transformations, Bitmap result) {
+ for (int i = 0, count = transformations.size(); i < count; i++) {
+ final Transformation transformation = transformations.get(i);
+ Bitmap newResult = transformation.transform(result);
+
+ if (newResult == null) {
+ final StringBuilder builder = new StringBuilder() //
+ .append("Transformation ")
+ .append(transformation.key())
+ .append(" returned null after ")
+ .append(i)
+ .append(" previous transformation(s).\n\nTransformation list:\n");
+ for (Transformation t : transformations) {
+ builder.append(t.key()).append('\n');
+ }
+ Picasso.HANDLER.post(new Runnable() {
+ @Override public void run() {
+ throw new NullPointerException(builder.toString());
+ }
+ });
+ return null;
+ }
+
+ if (newResult == result && result.isRecycled()) {
+ Picasso.HANDLER.post(new Runnable() {
+ @Override public void run() {
+ throw new IllegalStateException("Transformation "
+ + transformation.key()
+ + " returned input Bitmap but recycled it.");
+ }
+ });
+ return null;
+ }
+
+ // If the transformation returned a new bitmap ensure they recycled the original.
+ if (newResult != result && !result.isRecycled()) {
+ Picasso.HANDLER.post(new Runnable() {
+ @Override public void run() {
+ throw new IllegalStateException("Transformation "
+ + transformation.key()
+ + " mutated input Bitmap but failed to recycle the original.");
+ }
+ });
+ return null;
+ }
+
+ result = newResult;
+ }
+ return result;
+ }
+
+ static Bitmap transformResult(Request data, Bitmap result, int exifRotation) {
+ int inWidth = result.getWidth();
+ int inHeight = result.getHeight();
+
+ int drawX = 0;
+ int drawY = 0;
+ int drawWidth = inWidth;
+ int drawHeight = inHeight;
+
+ Matrix matrix = new Matrix();
+
+ if (data.needsMatrixTransform()) {
+ int targetWidth = data.targetWidth;
+ int targetHeight = data.targetHeight;
+
+ float targetRotation = data.rotationDegrees;
+ if (targetRotation != 0) {
+ if (data.hasRotationPivot) {
+ matrix.setRotate(targetRotation, data.rotationPivotX, data.rotationPivotY);
+ } else {
+ matrix.setRotate(targetRotation);
+ }
+ }
+
+ if (data.centerCrop) {
+ float widthRatio = targetWidth / (float) inWidth;
+ float heightRatio = targetHeight / (float) inHeight;
+ float scale;
+ if (widthRatio > heightRatio) {
+ scale = widthRatio;
+ int newSize = (int) Math.ceil(inHeight * (heightRatio / widthRatio));
+ drawY = (inHeight - newSize) / 2;
+ drawHeight = newSize;
+ } else {
+ scale = heightRatio;
+ int newSize = (int) Math.ceil(inWidth * (widthRatio / heightRatio));
+ drawX = (inWidth - newSize) / 2;
+ drawWidth = newSize;
+ }
+ matrix.preScale(scale, scale);
+ } else if (data.centerInside) {
+ float widthRatio = targetWidth / (float) inWidth;
+ float heightRatio = targetHeight / (float) inHeight;
+ float scale = widthRatio < heightRatio ? widthRatio : heightRatio;
+ matrix.preScale(scale, scale);
+ } else if (targetWidth != 0 && targetHeight != 0 //
+ && (targetWidth != inWidth || targetHeight != inHeight)) {
+ // If an explicit target size has been specified and they do not match the results bounds,
+ // pre-scale the existing matrix appropriately.
+ float sx = targetWidth / (float) inWidth;
+ float sy = targetHeight / (float) inHeight;
+ matrix.preScale(sx, sy);
+ }
+ }
+
+ if (exifRotation != 0) {
+ matrix.preRotate(exifRotation);
+ }
+
+ Bitmap newResult =
+ Bitmap.createBitmap(result, drawX, drawY, drawWidth, drawHeight, matrix, true);
+ if (newResult != result) {
+ result.recycle();
+ result = newResult;
+ }
+
+ return result;
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Cache.java b/mobile/android/thirdparty/com/squareup/picasso/Cache.java
new file mode 100644
index 0000000000..bce297f9ed
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Cache.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.graphics.Bitmap;
+
+/**
+ * A memory cache for storing the most recently used images.
+ * <p/>
+ * <em>Note:</em> The {@link Cache} is accessed by multiple threads. You must ensure
+ * your {@link Cache} implementation is thread safe when {@link Cache#get(String)} or {@link
+ * Cache#set(String, android.graphics.Bitmap)} is called.
+ */
+public interface Cache {
+ /** Retrieve an image for the specified {@code key} or {@code null}. */
+ Bitmap get(String key);
+
+ /** Store an image in the cache for the specified {@code key}. */
+ void set(String key, Bitmap bitmap);
+
+ /** Returns the current size of the cache in bytes. */
+ int size();
+
+ /** Returns the maximum size in bytes that the cache can hold. */
+ int maxSize();
+
+ /** Clears the cache. */
+ void clear();
+
+ /** A cache which does not store any values. */
+ Cache NONE = new Cache() {
+ @Override public Bitmap get(String key) {
+ return null;
+ }
+
+ @Override public void set(String key, Bitmap bitmap) {
+ // Ignore.
+ }
+
+ @Override public int size() {
+ return 0;
+ }
+
+ @Override public int maxSize() {
+ return 0;
+ }
+
+ @Override public void clear() {
+ }
+ };
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Callback.java b/mobile/android/thirdparty/com/squareup/picasso/Callback.java
new file mode 100644
index 0000000000..d936208891
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Callback.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+public interface Callback {
+ void onSuccess();
+
+ void onError();
+
+ public static class EmptyCallback implements Callback {
+
+ @Override public void onSuccess() {
+ }
+
+ @Override public void onError() {
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/ContactsPhotoBitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/ContactsPhotoBitmapHunter.java
new file mode 100644
index 0000000000..9444167b42
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/ContactsPhotoBitmapHunter.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.provider.ContactsContract;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH;
+import static android.provider.ContactsContract.Contacts.openContactPhotoInputStream;
+import static com.squareup.picasso.Picasso.LoadedFrom.DISK;
+
+class ContactsPhotoBitmapHunter extends BitmapHunter {
+ /** A lookup uri (e.g. content://com.android.contacts/contacts/lookup/3570i61d948d30808e537) */
+ private static final int ID_LOOKUP = 1;
+ /** A contact thumbnail uri (e.g. content://com.android.contacts/contacts/38/photo) */
+ private static final int ID_THUMBNAIL = 2;
+ /** A contact uri (e.g. content://com.android.contacts/contacts/38) */
+ private static final int ID_CONTACT = 3;
+ /**
+ * A contact display photo (high resolution) uri
+ * (e.g. content://com.android.contacts/display_photo/5)
+ */
+ private static final int ID_DISPLAY_PHOTO = 4;
+
+ private static final UriMatcher matcher;
+
+ static {
+ matcher = new UriMatcher(UriMatcher.NO_MATCH);
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", ID_LOOKUP);
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", ID_LOOKUP);
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", ID_THUMBNAIL);
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", ID_CONTACT);
+ matcher.addURI(ContactsContract.AUTHORITY, "display_photo/#", ID_DISPLAY_PHOTO);
+ }
+
+ final Context context;
+
+ ContactsPhotoBitmapHunter(Context context, Picasso picasso, Dispatcher dispatcher, Cache cache,
+ Stats stats, Action action) {
+ super(picasso, dispatcher, cache, stats, action);
+ this.context = context;
+ }
+
+ @Override Bitmap decode(Request data) throws IOException {
+ InputStream is = null;
+ try {
+ is = getInputStream();
+ return decodeStream(is, data);
+ } finally {
+ Utils.closeQuietly(is);
+ }
+ }
+
+ @Override Picasso.LoadedFrom getLoadedFrom() {
+ return DISK;
+ }
+
+ private InputStream getInputStream() throws IOException {
+ ContentResolver contentResolver = context.getContentResolver();
+ Uri uri = getData().uri;
+ switch (matcher.match(uri)) {
+ case ID_LOOKUP:
+ uri = ContactsContract.Contacts.lookupContact(contentResolver, uri);
+ if (uri == null) {
+ return null;
+ }
+ // Resolved the uri to a contact uri, intentionally fall through to process the resolved uri
+ case ID_CONTACT:
+ if (SDK_INT < ICE_CREAM_SANDWICH) {
+ return openContactPhotoInputStream(contentResolver, uri);
+ } else {
+ return ContactPhotoStreamIcs.get(contentResolver, uri);
+ }
+ case ID_THUMBNAIL:
+ case ID_DISPLAY_PHOTO:
+ return contentResolver.openInputStream(uri);
+ default:
+ throw new IllegalStateException("Invalid uri: " + uri);
+ }
+ }
+
+ private Bitmap decodeStream(InputStream stream, Request data) throws IOException {
+ if (stream == null) {
+ return null;
+ }
+ BitmapFactory.Options options = null;
+ if (data.hasSize()) {
+ options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ InputStream is = getInputStream();
+ try {
+ BitmapFactory.decodeStream(is, null, options);
+ } finally {
+ Utils.closeQuietly(is);
+ }
+ calculateInSampleSize(data.targetWidth, data.targetHeight, options);
+ }
+ return BitmapFactory.decodeStream(stream, null, options);
+ }
+
+ @TargetApi(ICE_CREAM_SANDWICH)
+ private static class ContactPhotoStreamIcs {
+ static InputStream get(ContentResolver contentResolver, Uri uri) {
+ return openContactPhotoInputStream(contentResolver, uri, true);
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/ContentStreamBitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/ContentStreamBitmapHunter.java
new file mode 100644
index 0000000000..624ffe0785
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/ContentStreamBitmapHunter.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import java.io.IOException;
+import java.io.InputStream;
+
+import static com.squareup.picasso.Picasso.LoadedFrom.DISK;
+
+class ContentStreamBitmapHunter extends BitmapHunter {
+ final Context context;
+
+ ContentStreamBitmapHunter(Context context, Picasso picasso, Dispatcher dispatcher, Cache cache,
+ Stats stats, Action action) {
+ super(picasso, dispatcher, cache, stats, action);
+ this.context = context;
+ }
+
+ @Override Bitmap decode(Request data)
+ throws IOException {
+ return decodeContentStream(data);
+ }
+
+ @Override Picasso.LoadedFrom getLoadedFrom() {
+ return DISK;
+ }
+
+ protected Bitmap decodeContentStream(Request data) throws IOException {
+ ContentResolver contentResolver = context.getContentResolver();
+ BitmapFactory.Options options = null;
+ if (data.hasSize()) {
+ options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ InputStream is = null;
+ try {
+ is = contentResolver.openInputStream(data.uri);
+ BitmapFactory.decodeStream(is, null, options);
+ } finally {
+ Utils.closeQuietly(is);
+ }
+ calculateInSampleSize(data.targetWidth, data.targetHeight, options);
+ }
+ InputStream is = contentResolver.openInputStream(data.uri);
+ try {
+ return BitmapFactory.decodeStream(is, null, options);
+ } finally {
+ Utils.closeQuietly(is);
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/DeferredRequestCreator.java b/mobile/android/thirdparty/com/squareup/picasso/DeferredRequestCreator.java
new file mode 100644
index 0000000000..fbdaab1c33
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/DeferredRequestCreator.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.view.ViewTreeObserver;
+import android.widget.ImageView;
+import java.lang.ref.WeakReference;
+
+class DeferredRequestCreator implements ViewTreeObserver.OnPreDrawListener {
+
+ final RequestCreator creator;
+ final WeakReference<ImageView> target;
+ Callback callback;
+
+ DeferredRequestCreator(RequestCreator creator, ImageView target, Callback callback) {
+ this.creator = creator;
+ this.target = new WeakReference<ImageView>(target);
+ this.callback = callback;
+ target.getViewTreeObserver().addOnPreDrawListener(this);
+ }
+
+ @Override public boolean onPreDraw() {
+ ImageView target = this.target.get();
+ if (target == null) {
+ return true;
+ }
+ ViewTreeObserver vto = target.getViewTreeObserver();
+ if (!vto.isAlive()) {
+ return true;
+ }
+
+ int width = target.getMeasuredWidth();
+ int height = target.getMeasuredHeight();
+
+ if (width <= 0 || height <= 0) {
+ return true;
+ }
+
+ vto.removeOnPreDrawListener(this);
+
+ this.creator.unfit().resize(width, height).into(target, callback);
+ return true;
+ }
+
+ void cancel() {
+ callback = null;
+ ImageView target = this.target.get();
+ if (target == null) {
+ return;
+ }
+ ViewTreeObserver vto = target.getViewTreeObserver();
+ if (!vto.isAlive()) {
+ return;
+ }
+ vto.removeOnPreDrawListener(this);
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Dispatcher.java b/mobile/android/thirdparty/com/squareup/picasso/Dispatcher.java
new file mode 100644
index 0000000000..6401431fb2
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Dispatcher.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.Manifest;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+
+import static android.content.Context.CONNECTIVITY_SERVICE;
+import static android.content.Intent.ACTION_AIRPLANE_MODE_CHANGED;
+import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
+import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
+import static com.squareup.picasso.BitmapHunter.forRequest;
+
+class Dispatcher {
+ private static final int RETRY_DELAY = 500;
+ private static final int AIRPLANE_MODE_ON = 1;
+ private static final int AIRPLANE_MODE_OFF = 0;
+
+ static final int REQUEST_SUBMIT = 1;
+ static final int REQUEST_CANCEL = 2;
+ static final int REQUEST_GCED = 3;
+ static final int HUNTER_COMPLETE = 4;
+ static final int HUNTER_RETRY = 5;
+ static final int HUNTER_DECODE_FAILED = 6;
+ static final int HUNTER_DELAY_NEXT_BATCH = 7;
+ static final int HUNTER_BATCH_COMPLETE = 8;
+ static final int NETWORK_STATE_CHANGE = 9;
+ static final int AIRPLANE_MODE_CHANGE = 10;
+
+ private static final String DISPATCHER_THREAD_NAME = "Dispatcher";
+ private static final int BATCH_DELAY = 200; // ms
+
+ final DispatcherThread dispatcherThread;
+ final Context context;
+ final ExecutorService service;
+ final Downloader downloader;
+ final Map<String, BitmapHunter> hunterMap;
+ final Handler handler;
+ final Handler mainThreadHandler;
+ final Cache cache;
+ final Stats stats;
+ final List<BitmapHunter> batch;
+ final NetworkBroadcastReceiver receiver;
+
+ NetworkInfo networkInfo;
+ boolean airplaneMode;
+
+ Dispatcher(Context context, ExecutorService service, Handler mainThreadHandler,
+ Downloader downloader, Cache cache, Stats stats) {
+ this.dispatcherThread = new DispatcherThread();
+ this.dispatcherThread.start();
+ this.context = context;
+ this.service = service;
+ this.hunterMap = new LinkedHashMap<String, BitmapHunter>();
+ this.handler = new DispatcherHandler(dispatcherThread.getLooper(), this);
+ this.downloader = downloader;
+ this.mainThreadHandler = mainThreadHandler;
+ this.cache = cache;
+ this.stats = stats;
+ this.batch = new ArrayList<BitmapHunter>(4);
+ this.airplaneMode = Utils.isAirplaneModeOn(this.context);
+ this.receiver = new NetworkBroadcastReceiver(this.context);
+ receiver.register();
+ }
+
+ void shutdown() {
+ service.shutdown();
+ dispatcherThread.quit();
+ receiver.unregister();
+ }
+
+ void dispatchSubmit(Action action) {
+ handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
+ }
+
+ void dispatchCancel(Action action) {
+ handler.sendMessage(handler.obtainMessage(REQUEST_CANCEL, action));
+ }
+
+ void dispatchComplete(BitmapHunter hunter) {
+ handler.sendMessage(handler.obtainMessage(HUNTER_COMPLETE, hunter));
+ }
+
+ void dispatchRetry(BitmapHunter hunter) {
+ handler.sendMessageDelayed(handler.obtainMessage(HUNTER_RETRY, hunter), RETRY_DELAY);
+ }
+
+ void dispatchFailed(BitmapHunter hunter) {
+ handler.sendMessage(handler.obtainMessage(HUNTER_DECODE_FAILED, hunter));
+ }
+
+ void dispatchNetworkStateChange(NetworkInfo info) {
+ handler.sendMessage(handler.obtainMessage(NETWORK_STATE_CHANGE, info));
+ }
+
+ void dispatchAirplaneModeChange(boolean airplaneMode) {
+ handler.sendMessage(handler.obtainMessage(AIRPLANE_MODE_CHANGE,
+ airplaneMode ? AIRPLANE_MODE_ON : AIRPLANE_MODE_OFF, 0));
+ }
+
+ void performSubmit(Action action) {
+ BitmapHunter hunter = hunterMap.get(action.getKey());
+ if (hunter != null) {
+ hunter.attach(action);
+ return;
+ }
+
+ if (service.isShutdown()) {
+ return;
+ }
+
+ hunter = forRequest(context, action.getPicasso(), this, cache, stats, action, downloader);
+ hunter.future = service.submit(hunter);
+ hunterMap.put(action.getKey(), hunter);
+ }
+
+ void performCancel(Action action) {
+ String key = action.getKey();
+ BitmapHunter hunter = hunterMap.get(key);
+ if (hunter != null) {
+ hunter.detach(action);
+ if (hunter.cancel()) {
+ hunterMap.remove(key);
+ }
+ }
+ }
+
+ void performRetry(BitmapHunter hunter) {
+ if (hunter.isCancelled()) return;
+
+ if (service.isShutdown()) {
+ performError(hunter);
+ return;
+ }
+
+ if (hunter.shouldRetry(airplaneMode, networkInfo)) {
+ hunter.future = service.submit(hunter);
+ } else {
+ performError(hunter);
+ }
+ }
+
+ void performComplete(BitmapHunter hunter) {
+ if (!hunter.shouldSkipMemoryCache()) {
+ cache.set(hunter.getKey(), hunter.getResult());
+ }
+ hunterMap.remove(hunter.getKey());
+ batch(hunter);
+ }
+
+ void performBatchComplete() {
+ List<BitmapHunter> copy = new ArrayList<BitmapHunter>(batch);
+ batch.clear();
+ mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage(HUNTER_BATCH_COMPLETE, copy));
+ }
+
+ void performError(BitmapHunter hunter) {
+ hunterMap.remove(hunter.getKey());
+ batch(hunter);
+ }
+
+ void performAirplaneModeChange(boolean airplaneMode) {
+ this.airplaneMode = airplaneMode;
+ }
+
+ void performNetworkStateChange(NetworkInfo info) {
+ networkInfo = info;
+ if (service instanceof PicassoExecutorService) {
+ ((PicassoExecutorService) service).adjustThreadCount(info);
+ }
+ }
+
+ private void batch(BitmapHunter hunter) {
+ if (hunter.isCancelled()) {
+ return;
+ }
+ batch.add(hunter);
+ if (!handler.hasMessages(HUNTER_DELAY_NEXT_BATCH)) {
+ handler.sendEmptyMessageDelayed(HUNTER_DELAY_NEXT_BATCH, BATCH_DELAY);
+ }
+ }
+
+ private static class DispatcherHandler extends Handler {
+ private final Dispatcher dispatcher;
+
+ public DispatcherHandler(Looper looper, Dispatcher dispatcher) {
+ super(looper);
+ this.dispatcher = dispatcher;
+ }
+
+ @Override public void handleMessage(final Message msg) {
+ switch (msg.what) {
+ case REQUEST_SUBMIT: {
+ Action action = (Action) msg.obj;
+ dispatcher.performSubmit(action);
+ break;
+ }
+ case REQUEST_CANCEL: {
+ Action action = (Action) msg.obj;
+ dispatcher.performCancel(action);
+ break;
+ }
+ case HUNTER_COMPLETE: {
+ BitmapHunter hunter = (BitmapHunter) msg.obj;
+ dispatcher.performComplete(hunter);
+ break;
+ }
+ case HUNTER_RETRY: {
+ BitmapHunter hunter = (BitmapHunter) msg.obj;
+ dispatcher.performRetry(hunter);
+ break;
+ }
+ case HUNTER_DECODE_FAILED: {
+ BitmapHunter hunter = (BitmapHunter) msg.obj;
+ dispatcher.performError(hunter);
+ break;
+ }
+ case HUNTER_DELAY_NEXT_BATCH: {
+ dispatcher.performBatchComplete();
+ break;
+ }
+ case NETWORK_STATE_CHANGE: {
+ NetworkInfo info = (NetworkInfo) msg.obj;
+ dispatcher.performNetworkStateChange(info);
+ break;
+ }
+ case AIRPLANE_MODE_CHANGE: {
+ dispatcher.performAirplaneModeChange(msg.arg1 == AIRPLANE_MODE_ON);
+ break;
+ }
+ default:
+ Picasso.HANDLER.post(new Runnable() {
+ @Override public void run() {
+ throw new AssertionError("Unknown handler message received: " + msg.what);
+ }
+ });
+ }
+ }
+ }
+
+ static class DispatcherThread extends HandlerThread {
+ DispatcherThread() {
+ super(Utils.THREAD_PREFIX + DISPATCHER_THREAD_NAME, THREAD_PRIORITY_BACKGROUND);
+ }
+ }
+
+ private class NetworkBroadcastReceiver extends BroadcastReceiver {
+ private static final String EXTRA_AIRPLANE_STATE = "state";
+
+ private final ConnectivityManager connectivityManager;
+
+ NetworkBroadcastReceiver(Context context) {
+ connectivityManager = (ConnectivityManager) context.getSystemService(CONNECTIVITY_SERVICE);
+ }
+
+ void register() {
+ boolean shouldScanState = service instanceof PicassoExecutorService && //
+ Utils.hasPermission(context, Manifest.permission.ACCESS_NETWORK_STATE);
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(ACTION_AIRPLANE_MODE_CHANGED);
+ if (shouldScanState) {
+ filter.addAction(CONNECTIVITY_ACTION);
+ }
+ context.registerReceiver(this, filter);
+ }
+
+ void unregister() {
+ context.unregisterReceiver(this);
+ }
+
+ @Override public void onReceive(Context context, Intent intent) {
+ // On some versions of Android this may be called with a null Intent
+ if (null == intent) {
+ return;
+ }
+
+ String action = intent.getAction();
+ Bundle extras = intent.getExtras();
+
+ if (ACTION_AIRPLANE_MODE_CHANGED.equals(action)) {
+ dispatchAirplaneModeChange(extras.getBoolean(EXTRA_AIRPLANE_STATE, false));
+ } else if (CONNECTIVITY_ACTION.equals(action)) {
+ dispatchNetworkStateChange(connectivityManager.getActiveNetworkInfo());
+ }
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Downloader.java b/mobile/android/thirdparty/com/squareup/picasso/Downloader.java
new file mode 100644
index 0000000000..33a9093716
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Downloader.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import java.io.IOException;
+import java.io.InputStream;
+
+/** A mechanism to load images from external resources such as a disk cache and/or the internet. */
+public interface Downloader {
+ /**
+ * Download the specified image {@code url} from the internet.
+ *
+ * @param uri Remote image URL.
+ * @param localCacheOnly If {@code true} the URL should only be loaded if available in a local
+ * disk cache.
+ * @return {@link Response} containing either a {@link Bitmap} representation of the request or an
+ * {@link InputStream} for the image data. {@code null} can be returned to indicate a problem
+ * loading the bitmap.
+ * @throws IOException if the requested URL cannot successfully be loaded.
+ */
+ Response load(Uri uri, boolean localCacheOnly) throws IOException;
+
+ /** Thrown for non-2XX responses. */
+ class ResponseException extends IOException {
+ public ResponseException(String message) {
+ super(message);
+ }
+ }
+
+ /** Response stream or bitmap and info. */
+ class Response {
+ final InputStream stream;
+ final Bitmap bitmap;
+ final boolean cached;
+
+ /**
+ * Response image and info.
+ *
+ * @param bitmap Image.
+ * @param loadedFromCache {@code true} if the source of the image is from a local disk cache.
+ */
+ public Response(Bitmap bitmap, boolean loadedFromCache) {
+ if (bitmap == null) {
+ throw new IllegalArgumentException("Bitmap may not be null.");
+ }
+ this.stream = null;
+ this.bitmap = bitmap;
+ this.cached = loadedFromCache;
+ }
+
+ /**
+ * Response stream and info.
+ *
+ * @param stream Image data stream.
+ * @param loadedFromCache {@code true} if the source of the stream is from a local disk cache.
+ */
+ public Response(InputStream stream, boolean loadedFromCache) {
+ if (stream == null) {
+ throw new IllegalArgumentException("Stream may not be null.");
+ }
+ this.stream = stream;
+ this.bitmap = null;
+ this.cached = loadedFromCache;
+ }
+
+ /**
+ * Input stream containing image data.
+ * <p>
+ * If this returns {@code null}, image data will be available via {@link #getBitmap()}.
+ */
+ public InputStream getInputStream() {
+ return stream;
+ }
+
+ /**
+ * Bitmap representing the image.
+ * <p>
+ * If this returns {@code null}, image data will be available via {@link #getInputStream()}.
+ */
+ public Bitmap getBitmap() {
+ return bitmap;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/FetchAction.java b/mobile/android/thirdparty/com/squareup/picasso/FetchAction.java
new file mode 100644
index 0000000000..d8f1c3fb48
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/FetchAction.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.graphics.Bitmap;
+
+class FetchAction extends Action<Void> {
+ FetchAction(Picasso picasso, Request data, boolean skipCache, String key) {
+ super(picasso, null, data, skipCache, false, 0, null, key);
+ }
+
+ @Override void complete(Bitmap result, Picasso.LoadedFrom from) {
+ }
+
+ @Override public void error() {
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/FileBitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/FileBitmapHunter.java
new file mode 100644
index 0000000000..dac38fb80e
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/FileBitmapHunter.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.media.ExifInterface;
+import android.net.Uri;
+import java.io.IOException;
+
+import static android.media.ExifInterface.ORIENTATION_NORMAL;
+import static android.media.ExifInterface.ORIENTATION_ROTATE_180;
+import static android.media.ExifInterface.ORIENTATION_ROTATE_270;
+import static android.media.ExifInterface.ORIENTATION_ROTATE_90;
+import static android.media.ExifInterface.TAG_ORIENTATION;
+
+class FileBitmapHunter extends ContentStreamBitmapHunter {
+
+ FileBitmapHunter(Context context, Picasso picasso, Dispatcher dispatcher, Cache cache,
+ Stats stats, Action action) {
+ super(context, picasso, dispatcher, cache, stats, action);
+ }
+
+ @Override Bitmap decode(Request data)
+ throws IOException {
+ setExifRotation(getFileExifRotation(data.uri));
+ return super.decode(data);
+ }
+
+ static int getFileExifRotation(Uri uri) throws IOException {
+ ExifInterface exifInterface = new ExifInterface(uri.getPath());
+ int orientation = exifInterface.getAttributeInt(TAG_ORIENTATION, ORIENTATION_NORMAL);
+ switch (orientation) {
+ case ORIENTATION_ROTATE_90:
+ return 90;
+ case ORIENTATION_ROTATE_180:
+ return 180;
+ case ORIENTATION_ROTATE_270:
+ return 270;
+ default:
+ return 0;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/GetAction.java b/mobile/android/thirdparty/com/squareup/picasso/GetAction.java
new file mode 100644
index 0000000000..e30024e897
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/GetAction.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.graphics.Bitmap;
+
+class GetAction extends Action<Void> {
+ GetAction(Picasso picasso, Request data, boolean skipCache, String key) {
+ super(picasso, null, data, skipCache, false, 0, null, key);
+ }
+
+ @Override void complete(Bitmap result, Picasso.LoadedFrom from) {
+ }
+
+ @Override public void error() {
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/ImageViewAction.java b/mobile/android/thirdparty/com/squareup/picasso/ImageViewAction.java
new file mode 100644
index 0000000000..16f5509077
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/ImageViewAction.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.widget.ImageView;
+
+class ImageViewAction extends Action<ImageView> {
+
+ Callback callback;
+
+ ImageViewAction(Picasso picasso, ImageView imageView, Request data, boolean skipCache,
+ boolean noFade, int errorResId, Drawable errorDrawable, String key, Callback callback) {
+ super(picasso, imageView, data, skipCache, noFade, errorResId, errorDrawable, key);
+ this.callback = callback;
+ }
+
+ @Override public void complete(Bitmap result, Picasso.LoadedFrom from) {
+ if (result == null) {
+ throw new AssertionError(
+ String.format("Attempted to complete action with no result!\n%s", this));
+ }
+
+ ImageView target = this.target.get();
+ if (target == null) {
+ return;
+ }
+
+ Context context = picasso.context;
+ boolean debugging = picasso.debugging;
+ PicassoDrawable.setBitmap(target, context, result, from, noFade, debugging);
+
+ if (callback != null) {
+ callback.onSuccess();
+ }
+ }
+
+ @Override public void error() {
+ ImageView target = this.target.get();
+ if (target == null) {
+ return;
+ }
+ if (errorResId != 0) {
+ target.setImageResource(errorResId);
+ } else if (errorDrawable != null) {
+ target.setImageDrawable(errorDrawable);
+ }
+
+ if (callback != null) {
+ callback.onError();
+ }
+ }
+
+ @Override void cancel() {
+ super.cancel();
+ if (callback != null) {
+ callback = null;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/LruCache.java b/mobile/android/thirdparty/com/squareup/picasso/LruCache.java
new file mode 100644
index 0000000000..5d5f07fcb0
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/LruCache.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/** A memory cache which uses a least-recently used eviction policy. */
+public class LruCache implements Cache {
+ final LinkedHashMap<String, Bitmap> map;
+ private final int maxSize;
+
+ private int size;
+ private int putCount;
+ private int evictionCount;
+ private int hitCount;
+ private int missCount;
+
+ /** Create a cache using an appropriate portion of the available RAM as the maximum size. */
+ public LruCache(Context context) {
+ this(Utils.calculateMemoryCacheSize(context));
+ }
+
+ /** Create a cache with a given maximum size in bytes. */
+ public LruCache(int maxSize) {
+ if (maxSize <= 0) {
+ throw new IllegalArgumentException("Max size must be positive.");
+ }
+ this.maxSize = maxSize;
+ this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
+ }
+
+ @Override public Bitmap get(String key) {
+ if (key == null) {
+ throw new NullPointerException("key == null");
+ }
+
+ Bitmap mapValue;
+ synchronized (this) {
+ mapValue = map.get(key);
+ if (mapValue != null) {
+ hitCount++;
+ return mapValue;
+ }
+ missCount++;
+ }
+
+ return null;
+ }
+
+ @Override public void set(String key, Bitmap bitmap) {
+ if (key == null || bitmap == null) {
+ throw new NullPointerException("key == null || bitmap == null");
+ }
+
+ Bitmap previous;
+ synchronized (this) {
+ putCount++;
+ size += Utils.getBitmapBytes(bitmap);
+ previous = map.put(key, bitmap);
+ if (previous != null) {
+ size -= Utils.getBitmapBytes(previous);
+ }
+ }
+
+ trimToSize(maxSize);
+ }
+
+ private void trimToSize(int maxSize) {
+ while (true) {
+ String key;
+ Bitmap value;
+ synchronized (this) {
+ if (size < 0 || (map.isEmpty() && size != 0)) {
+ throw new IllegalStateException(
+ getClass().getName() + ".sizeOf() is reporting inconsistent results!");
+ }
+
+ if (size <= maxSize || map.isEmpty()) {
+ break;
+ }
+
+ Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next();
+ key = toEvict.getKey();
+ value = toEvict.getValue();
+ map.remove(key);
+ size -= Utils.getBitmapBytes(value);
+ evictionCount++;
+ }
+ }
+ }
+
+ /** Clear the cache. */
+ public final void evictAll() {
+ trimToSize(-1); // -1 will evict 0-sized elements
+ }
+
+ /** Returns the sum of the sizes of the entries in this cache. */
+ public final synchronized int size() {
+ return size;
+ }
+
+ /** Returns the maximum sum of the sizes of the entries in this cache. */
+ public final synchronized int maxSize() {
+ return maxSize;
+ }
+
+ public final synchronized void clear() {
+ evictAll();
+ }
+
+ /** Returns the number of times {@link #get} returned a value. */
+ public final synchronized int hitCount() {
+ return hitCount;
+ }
+
+ /** Returns the number of times {@link #get} returned {@code null}. */
+ public final synchronized int missCount() {
+ return missCount;
+ }
+
+ /** Returns the number of times {@link #set(String, Bitmap)} was called. */
+ public final synchronized int putCount() {
+ return putCount;
+ }
+
+ /** Returns the number of values that have been evicted. */
+ public final synchronized int evictionCount() {
+ return evictionCount;
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/MarkableInputStream.java b/mobile/android/thirdparty/com/squareup/picasso/MarkableInputStream.java
new file mode 100644
index 0000000000..17043a1b03
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/MarkableInputStream.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * An input stream wrapper that supports unlimited independent cursors for
+ * marking and resetting. Each cursor is a token, and it's the caller's
+ * responsibility to keep track of these.
+ */
+final class MarkableInputStream extends InputStream {
+ private final InputStream in;
+
+ private long offset;
+ private long reset;
+ private long limit;
+
+ private long defaultMark = -1;
+
+ public MarkableInputStream(InputStream in) {
+ if (!in.markSupported()) {
+ in = new BufferedInputStream(in);
+ }
+ this.in = in;
+ }
+
+ /** Marks this place in the stream so we can reset back to it later. */
+ @Override public void mark(int readLimit) {
+ defaultMark = savePosition(readLimit);
+ }
+
+ /**
+ * Returns an opaque token representing the current position in the stream.
+ * Call {@link #reset(long)} to return to this position in the stream later.
+ * It is an error to call {@link #reset(long)} after consuming more than
+ * {@code readLimit} bytes from this stream.
+ */
+ public long savePosition(int readLimit) {
+ long offsetLimit = offset + readLimit;
+ if (limit < offsetLimit) {
+ setLimit(offsetLimit);
+ }
+ return offset;
+ }
+
+ /**
+ * Makes sure that the underlying stream can backtrack the full range from
+ * {@code reset} thru {@code limit}. Since we can't call {@code mark()}
+ * without also adjusting the reset-to-position on the underlying stream this
+ * method resets first and then marks the union of the two byte ranges. On
+ * buffered streams this additional cursor motion shouldn't result in any
+ * additional I/O.
+ */
+ private void setLimit(long limit) {
+ try {
+ if (reset < offset && offset <= this.limit) {
+ in.reset();
+ in.mark((int) (limit - reset));
+ skip(reset, offset);
+ } else {
+ reset = offset;
+ in.mark((int) (limit - offset));
+ }
+ this.limit = limit;
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to mark: " + e);
+ }
+ }
+
+ /** Resets the stream to the most recent {@link #mark mark}. */
+ @Override public void reset() throws IOException {
+ reset(defaultMark);
+ }
+
+ /** Resets the stream to the position recorded by {@code token}. */
+ public void reset(long token) throws IOException {
+ if (offset > limit || token < reset) {
+ throw new IOException("Cannot reset");
+ }
+ in.reset();
+ skip(reset, token);
+ offset = token;
+ }
+
+ /** Skips {@code target - current} bytes and returns. */
+ private void skip(long current, long target) throws IOException {
+ while (current < target) {
+ long skipped = in.skip(target - current);
+ if (skipped == 0) {
+ if (read() == -1) {
+ break; // EOF
+ } else {
+ skipped = 1;
+ }
+ }
+ current += skipped;
+ }
+ }
+
+ @Override public int read() throws IOException {
+ int result = in.read();
+ if (result != -1) {
+ offset++;
+ }
+ return result;
+ }
+
+ @Override public int read(byte[] buffer) throws IOException {
+ int count = in.read(buffer);
+ if (count != -1) {
+ offset += count;
+ }
+ return count;
+ }
+
+ @Override public int read(byte[] buffer, int offset, int length) throws IOException {
+ int count = in.read(buffer, offset, length);
+ if (count != -1) {
+ this.offset += count;
+ }
+ return count;
+ }
+
+ @Override public long skip(long byteCount) throws IOException {
+ long skipped = in.skip(byteCount);
+ offset += skipped;
+ return skipped;
+ }
+
+ @Override public int available() throws IOException {
+ return in.available();
+ }
+
+ @Override public void close() throws IOException {
+ in.close();
+ }
+
+ @Override public boolean markSupported() {
+ return in.markSupported();
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/MediaStoreBitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/MediaStoreBitmapHunter.java
new file mode 100644
index 0000000000..4f8ae1c24a
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/MediaStoreBitmapHunter.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.provider.MediaStore;
+import java.io.IOException;
+
+import static android.content.ContentUris.parseId;
+import static android.provider.MediaStore.Images.Thumbnails.FULL_SCREEN_KIND;
+import static android.provider.MediaStore.Images.Thumbnails.MICRO_KIND;
+import static android.provider.MediaStore.Images.Thumbnails.MINI_KIND;
+import static android.provider.MediaStore.Images.Thumbnails.getThumbnail;
+import static com.squareup.picasso.MediaStoreBitmapHunter.PicassoKind.FULL;
+import static com.squareup.picasso.MediaStoreBitmapHunter.PicassoKind.MICRO;
+import static com.squareup.picasso.MediaStoreBitmapHunter.PicassoKind.MINI;
+
+class MediaStoreBitmapHunter extends ContentStreamBitmapHunter {
+ private static final String[] CONTENT_ORIENTATION = new String[] {
+ MediaStore.Images.ImageColumns.ORIENTATION
+ };
+
+ MediaStoreBitmapHunter(Context context, Picasso picasso, Dispatcher dispatcher, Cache cache,
+ Stats stats, Action action) {
+ super(context, picasso, dispatcher, cache, stats, action);
+ }
+
+ @Override Bitmap decode(Request data) throws IOException {
+ ContentResolver contentResolver = context.getContentResolver();
+ setExifRotation(getExitOrientation(contentResolver, data.uri));
+
+ if (data.hasSize()) {
+ PicassoKind picassoKind = getPicassoKind(data.targetWidth, data.targetHeight);
+ if (picassoKind == FULL) {
+ return super.decode(data);
+ }
+
+ long id = parseId(data.uri);
+
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+
+ calculateInSampleSize(data.targetWidth, data.targetHeight, picassoKind.width,
+ picassoKind.height, options);
+
+ Bitmap result = getThumbnail(contentResolver, id, picassoKind.androidKind, options);
+
+ if (result != null) {
+ return result;
+ }
+ }
+
+ return super.decode(data);
+ }
+
+ static PicassoKind getPicassoKind(int targetWidth, int targetHeight) {
+ if (targetWidth <= MICRO.width && targetHeight <= MICRO.height) {
+ return MICRO;
+ } else if (targetWidth <= MINI.width && targetHeight <= MINI.height) {
+ return MINI;
+ }
+ return FULL;
+ }
+
+ static int getExitOrientation(ContentResolver contentResolver, Uri uri) {
+ Cursor cursor = null;
+ try {
+ cursor = contentResolver.query(uri, CONTENT_ORIENTATION, null, null, null);
+ if (cursor == null || !cursor.moveToFirst()) {
+ return 0;
+ }
+ return cursor.getInt(0);
+ } catch (RuntimeException ignored) {
+ // If the orientation column doesn't exist, assume no rotation.
+ return 0;
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ enum PicassoKind {
+ MICRO(MICRO_KIND, 96, 96),
+ MINI(MINI_KIND, 512, 384),
+ FULL(FULL_SCREEN_KIND, -1, -1);
+
+ final int androidKind;
+ final int width;
+ final int height;
+
+ PicassoKind(int androidKind, int width, int height) {
+ this.androidKind = androidKind;
+ this.width = width;
+ this.height = height;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/NetworkBitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/NetworkBitmapHunter.java
new file mode 100644
index 0000000000..6d148211df
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/NetworkBitmapHunter.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.NetworkInfo;
+import java.io.IOException;
+import java.io.InputStream;
+
+import static com.squareup.picasso.Downloader.Response;
+import static com.squareup.picasso.Picasso.LoadedFrom.DISK;
+import static com.squareup.picasso.Picasso.LoadedFrom.NETWORK;
+
+class NetworkBitmapHunter extends BitmapHunter {
+ static final int DEFAULT_RETRY_COUNT = 2;
+ private static final int MARKER = 65536;
+
+ private final Downloader downloader;
+
+ int retryCount;
+
+ public NetworkBitmapHunter(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats,
+ Action action, Downloader downloader) {
+ super(picasso, dispatcher, cache, stats, action);
+ this.downloader = downloader;
+ this.retryCount = DEFAULT_RETRY_COUNT;
+ }
+
+ @Override Bitmap decode(Request data) throws IOException {
+ boolean loadFromLocalCacheOnly = retryCount == 0;
+
+ Response response = downloader.load(data.uri, loadFromLocalCacheOnly);
+ if (response == null) {
+ return null;
+ }
+
+ loadedFrom = response.cached ? DISK : NETWORK;
+
+ Bitmap result = response.getBitmap();
+ if (result != null) {
+ return result;
+ }
+
+ InputStream is = response.getInputStream();
+ try {
+ return decodeStream(is, data);
+ } finally {
+ Utils.closeQuietly(is);
+ }
+ }
+
+ @Override boolean shouldRetry(boolean airplaneMode, NetworkInfo info) {
+ boolean hasRetries = retryCount > 0;
+ if (!hasRetries) {
+ return false;
+ }
+ retryCount--;
+ return info == null || info.isConnectedOrConnecting();
+ }
+
+ private Bitmap decodeStream(InputStream stream, Request data) throws IOException {
+ if (stream == null) {
+ return null;
+ }
+ MarkableInputStream markStream = new MarkableInputStream(stream);
+ stream = markStream;
+
+ long mark = markStream.savePosition(MARKER);
+
+ boolean isWebPFile = Utils.isWebPFile(stream);
+ markStream.reset(mark);
+ // When decode WebP network stream, BitmapFactory throw JNI Exception and make app crash.
+ // Decode byte array instead
+ if (isWebPFile) {
+ byte[] bytes = Utils.toByteArray(stream);
+ BitmapFactory.Options options = null;
+ if (data.hasSize()) {
+ options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+
+ BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
+ calculateInSampleSize(data.targetWidth, data.targetHeight, options);
+ }
+ return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
+ } else {
+ BitmapFactory.Options options = null;
+ if (data.hasSize()) {
+ options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+
+ BitmapFactory.decodeStream(stream, null, options);
+ calculateInSampleSize(data.targetWidth, data.targetHeight, options);
+
+ markStream.reset(mark);
+ }
+ return BitmapFactory.decodeStream(stream, null, options);
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Picasso.java b/mobile/android/thirdparty/com/squareup/picasso/Picasso.java
new file mode 100644
index 0000000000..9b510f977e
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Picasso.java
@@ -0,0 +1,522 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.widget.ImageView;
+import java.io.File;
+import java.lang.ref.ReferenceQueue;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.concurrent.ExecutorService;
+
+import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
+import static com.squareup.picasso.Action.RequestWeakReference;
+import static com.squareup.picasso.Dispatcher.HUNTER_BATCH_COMPLETE;
+import static com.squareup.picasso.Dispatcher.REQUEST_GCED;
+import static com.squareup.picasso.Utils.THREAD_PREFIX;
+
+/**
+ * Image downloading, transformation, and caching manager.
+ * <p/>
+ * Use {@link #with(android.content.Context)} for the global singleton instance or construct your
+ * own instance with {@link Builder}.
+ */
+public class Picasso {
+
+ /** Callbacks for Picasso events. */
+ public interface Listener {
+ /**
+ * Invoked when an image has failed to load. This is useful for reporting image failures to a
+ * remote analytics service, for example.
+ */
+ void onImageLoadFailed(Picasso picasso, Uri uri, Exception exception);
+ }
+
+ /**
+ * A transformer that is called immediately before every request is submitted. This can be used to
+ * modify any information about a request.
+ * <p>
+ * For example, if you use a CDN you can change the hostname for the image based on the current
+ * location of the user in order to get faster download speeds.
+ * <p>
+ * <b>NOTE:</b> This is a beta feature. The API is subject to change in a backwards incompatible
+ * way at any time.
+ */
+ public interface RequestTransformer {
+ /**
+ * Transform a request before it is submitted to be processed.
+ *
+ * @return The original request or a new request to replace it. Must not be null.
+ */
+ Request transformRequest(Request request);
+
+ /** A {@link RequestTransformer} which returns the original request. */
+ RequestTransformer IDENTITY = new RequestTransformer() {
+ @Override public Request transformRequest(Request request) {
+ return request;
+ }
+ };
+ }
+
+ static final Handler HANDLER = new Handler(Looper.getMainLooper()) {
+ @Override public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case HUNTER_BATCH_COMPLETE: {
+ @SuppressWarnings("unchecked") List<BitmapHunter> batch = (List<BitmapHunter>) msg.obj;
+ for (BitmapHunter hunter : batch) {
+ hunter.picasso.complete(hunter);
+ }
+ break;
+ }
+ case REQUEST_GCED: {
+ Action action = (Action) msg.obj;
+ action.picasso.cancelExistingRequest(action.getTarget());
+ break;
+ }
+ default:
+ throw new AssertionError("Unknown handler message received: " + msg.what);
+ }
+ }
+ };
+
+ static Picasso singleton = null;
+
+ private final Listener listener;
+ private final RequestTransformer requestTransformer;
+ private final CleanupThread cleanupThread;
+
+ final Context context;
+ final Dispatcher dispatcher;
+ final Cache cache;
+ final Stats stats;
+ final Map<Object, Action> targetToAction;
+ final Map<ImageView, DeferredRequestCreator> targetToDeferredRequestCreator;
+ final ReferenceQueue<Object> referenceQueue;
+
+ boolean debugging;
+ boolean shutdown;
+
+ Picasso(Context context, Dispatcher dispatcher, Cache cache, Listener listener,
+ RequestTransformer requestTransformer, Stats stats, boolean debugging) {
+ this.context = context;
+ this.dispatcher = dispatcher;
+ this.cache = cache;
+ this.listener = listener;
+ this.requestTransformer = requestTransformer;
+ this.stats = stats;
+ this.targetToAction = new WeakHashMap<Object, Action>();
+ this.targetToDeferredRequestCreator = new WeakHashMap<ImageView, DeferredRequestCreator>();
+ this.debugging = debugging;
+ this.referenceQueue = new ReferenceQueue<Object>();
+ this.cleanupThread = new CleanupThread(referenceQueue, HANDLER);
+ this.cleanupThread.start();
+ }
+
+ /** Cancel any existing requests for the specified target {@link ImageView}. */
+ public void cancelRequest(ImageView view) {
+ cancelExistingRequest(view);
+ }
+
+ /** Cancel any existing requests for the specified {@link Target} instance. */
+ public void cancelRequest(Target target) {
+ cancelExistingRequest(target);
+ }
+
+ /**
+ * Start an image request using the specified URI.
+ * <p>
+ * Passing {@code null} as a {@code uri} will not trigger any request but will set a placeholder,
+ * if one is specified.
+ *
+ * @see #load(File)
+ * @see #load(String)
+ * @see #load(int)
+ */
+ public RequestCreator load(Uri uri) {
+ return new RequestCreator(this, uri, 0);
+ }
+
+ /**
+ * Start an image request using the specified path. This is a convenience method for calling
+ * {@link #load(Uri)}.
+ * <p>
+ * This path may be a remote URL, file resource (prefixed with {@code file:}), content resource
+ * (prefixed with {@code content:}), or android resource (prefixed with {@code
+ * android.resource:}.
+ * <p>
+ * Passing {@code null} as a {@code path} will not trigger any request but will set a
+ * placeholder, if one is specified.
+ *
+ * @see #load(Uri)
+ * @see #load(File)
+ * @see #load(int)
+ */
+ public RequestCreator load(String path) {
+ if (path == null) {
+ return new RequestCreator(this, null, 0);
+ }
+ if (path.trim().length() == 0) {
+ throw new IllegalArgumentException("Path must not be empty.");
+ }
+ return load(Uri.parse(path));
+ }
+
+ /**
+ * Start an image request using the specified image file. This is a convenience method for
+ * calling {@link #load(Uri)}.
+ * <p>
+ * Passing {@code null} as a {@code file} will not trigger any request but will set a
+ * placeholder, if one is specified.
+ *
+ * @see #load(Uri)
+ * @see #load(String)
+ * @see #load(int)
+ */
+ public RequestCreator load(File file) {
+ if (file == null) {
+ return new RequestCreator(this, null, 0);
+ }
+ return load(Uri.fromFile(file));
+ }
+
+ /**
+ * Start an image request using the specified drawable resource ID.
+ *
+ * @see #load(Uri)
+ * @see #load(String)
+ * @see #load(File)
+ */
+ public RequestCreator load(int resourceId) {
+ if (resourceId == 0) {
+ throw new IllegalArgumentException("Resource ID must not be zero.");
+ }
+ return new RequestCreator(this, null, resourceId);
+ }
+
+ /** {@code true} if debug display, logging, and statistics are enabled. */
+ @SuppressWarnings("UnusedDeclaration") public boolean isDebugging() {
+ return debugging;
+ }
+
+ /** Toggle whether debug display, logging, and statistics are enabled. */
+ @SuppressWarnings("UnusedDeclaration") public void setDebugging(boolean debugging) {
+ this.debugging = debugging;
+ }
+
+ /** Creates a {@link StatsSnapshot} of the current stats for this instance. */
+ @SuppressWarnings("UnusedDeclaration") public StatsSnapshot getSnapshot() {
+ return stats.createSnapshot();
+ }
+
+ /** Stops this instance from accepting further requests. */
+ public void shutdown() {
+ if (this == singleton) {
+ throw new UnsupportedOperationException("Default singleton instance cannot be shutdown.");
+ }
+ if (shutdown) {
+ return;
+ }
+ cache.clear();
+ cleanupThread.shutdown();
+ stats.shutdown();
+ dispatcher.shutdown();
+ for (DeferredRequestCreator deferredRequestCreator : targetToDeferredRequestCreator.values()) {
+ deferredRequestCreator.cancel();
+ }
+ targetToDeferredRequestCreator.clear();
+ shutdown = true;
+ }
+
+ Request transformRequest(Request request) {
+ Request transformed = requestTransformer.transformRequest(request);
+ if (transformed == null) {
+ throw new IllegalStateException("Request transformer "
+ + requestTransformer.getClass().getCanonicalName()
+ + " returned null for "
+ + request);
+ }
+ return transformed;
+ }
+
+ void defer(ImageView view, DeferredRequestCreator request) {
+ targetToDeferredRequestCreator.put(view, request);
+ }
+
+ void enqueueAndSubmit(Action action) {
+ Object target = action.getTarget();
+ if (target != null) {
+ cancelExistingRequest(target);
+ targetToAction.put(target, action);
+ }
+ submit(action);
+ }
+
+ void submit(Action action) {
+ dispatcher.dispatchSubmit(action);
+ }
+
+ Bitmap quickMemoryCacheCheck(String key) {
+ Bitmap cached = cache.get(key);
+ if (cached != null) {
+ stats.dispatchCacheHit();
+ } else {
+ stats.dispatchCacheMiss();
+ }
+ return cached;
+ }
+
+ void complete(BitmapHunter hunter) {
+ List<Action> joined = hunter.getActions();
+ if (joined.isEmpty()) {
+ return;
+ }
+
+ Uri uri = hunter.getData().uri;
+ Exception exception = hunter.getException();
+ Bitmap result = hunter.getResult();
+ LoadedFrom from = hunter.getLoadedFrom();
+
+ for (Action join : joined) {
+ if (join.isCancelled()) {
+ continue;
+ }
+ targetToAction.remove(join.getTarget());
+ if (result != null) {
+ if (from == null) {
+ throw new AssertionError("LoadedFrom cannot be null.");
+ }
+ join.complete(result, from);
+ } else {
+ join.error();
+ }
+ }
+
+ if (listener != null && exception != null) {
+ listener.onImageLoadFailed(this, uri, exception);
+ }
+ }
+
+ private void cancelExistingRequest(Object target) {
+ Action action = targetToAction.remove(target);
+ if (action != null) {
+ action.cancel();
+ dispatcher.dispatchCancel(action);
+ }
+ if (target instanceof ImageView) {
+ ImageView targetImageView = (ImageView) target;
+ DeferredRequestCreator deferredRequestCreator =
+ targetToDeferredRequestCreator.remove(targetImageView);
+ if (deferredRequestCreator != null) {
+ deferredRequestCreator.cancel();
+ }
+ }
+ }
+
+ private static class CleanupThread extends Thread {
+ private final ReferenceQueue<?> referenceQueue;
+ private final Handler handler;
+
+ CleanupThread(ReferenceQueue<?> referenceQueue, Handler handler) {
+ this.referenceQueue = referenceQueue;
+ this.handler = handler;
+ setDaemon(true);
+ setName(THREAD_PREFIX + "refQueue");
+ }
+
+ @Override public void run() {
+ Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);
+ while (true) {
+ try {
+ RequestWeakReference<?> remove = (RequestWeakReference<?>) referenceQueue.remove();
+ handler.sendMessage(handler.obtainMessage(REQUEST_GCED, remove.action));
+ } catch (InterruptedException e) {
+ break;
+ } catch (final Exception e) {
+ handler.post(new Runnable() {
+ @Override public void run() {
+ throw new RuntimeException(e);
+ }
+ });
+ break;
+ }
+ }
+ }
+
+ void shutdown() {
+ interrupt();
+ }
+ }
+
+ /**
+ * The global default {@link Picasso} instance.
+ * <p>
+ * This instance is automatically initialized with defaults that are suitable to most
+ * implementations.
+ * <ul>
+ * <li>LRU memory cache of 15% the available application RAM</li>
+ * <li>Disk cache of 2% storage space up to 50MB but no less than 5MB. (Note: this is only
+ * available on API 14+ <em>or</em> if you are using a standalone library that provides a disk
+ * cache on all API levels like OkHttp)</li>
+ * <li>Three download threads for disk and network access.</li>
+ * </ul>
+ * <p>
+ * If these settings do not meet the requirements of your application you can construct your own
+ * instance with full control over the configuration by using {@link Picasso.Builder}.
+ */
+ public static Picasso with(Context context) {
+ if (singleton == null) {
+ singleton = new Builder(context).build();
+ }
+ return singleton;
+ }
+
+ /** Fluent API for creating {@link Picasso} instances. */
+ @SuppressWarnings("UnusedDeclaration") // Public API.
+ public static class Builder {
+ private final Context context;
+ private Downloader downloader;
+ private ExecutorService service;
+ private Cache cache;
+ private Listener listener;
+ private RequestTransformer transformer;
+ private boolean debugging;
+
+ /** Start building a new {@link Picasso} instance. */
+ public Builder(Context context) {
+ if (context == null) {
+ throw new IllegalArgumentException("Context must not be null.");
+ }
+ this.context = context.getApplicationContext();
+ }
+
+ /** Specify the {@link Downloader} that will be used for downloading images. */
+ public Builder downloader(Downloader downloader) {
+ if (downloader == null) {
+ throw new IllegalArgumentException("Downloader must not be null.");
+ }
+ if (this.downloader != null) {
+ throw new IllegalStateException("Downloader already set.");
+ }
+ this.downloader = downloader;
+ return this;
+ }
+
+ /** Specify the executor service for loading images in the background. */
+ public Builder executor(ExecutorService executorService) {
+ if (executorService == null) {
+ throw new IllegalArgumentException("Executor service must not be null.");
+ }
+ if (this.service != null) {
+ throw new IllegalStateException("Executor service already set.");
+ }
+ this.service = executorService;
+ return this;
+ }
+
+ /** Specify the memory cache used for the most recent images. */
+ public Builder memoryCache(Cache memoryCache) {
+ if (memoryCache == null) {
+ throw new IllegalArgumentException("Memory cache must not be null.");
+ }
+ if (this.cache != null) {
+ throw new IllegalStateException("Memory cache already set.");
+ }
+ this.cache = memoryCache;
+ return this;
+ }
+
+ /** Specify a listener for interesting events. */
+ public Builder listener(Listener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("Listener must not be null.");
+ }
+ if (this.listener != null) {
+ throw new IllegalStateException("Listener already set.");
+ }
+ this.listener = listener;
+ return this;
+ }
+
+ /**
+ * Specify a transformer for all incoming requests.
+ * <p>
+ * <b>NOTE:</b> This is a beta feature. The API is subject to change in a backwards incompatible
+ * way at any time.
+ */
+ public Builder requestTransformer(RequestTransformer transformer) {
+ if (transformer == null) {
+ throw new IllegalArgumentException("Transformer must not be null.");
+ }
+ if (this.transformer != null) {
+ throw new IllegalStateException("Transformer already set.");
+ }
+ this.transformer = transformer;
+ return this;
+ }
+
+ /** Whether debugging is enabled or not. */
+ public Builder debugging(boolean debugging) {
+ this.debugging = debugging;
+ return this;
+ }
+
+ /** Create the {@link Picasso} instance. */
+ public Picasso build() {
+ Context context = this.context;
+
+ if (downloader == null) {
+ downloader = Utils.createDefaultDownloader(context);
+ }
+ if (cache == null) {
+ cache = new LruCache(context);
+ }
+ if (service == null) {
+ service = new PicassoExecutorService();
+ }
+ if (transformer == null) {
+ transformer = RequestTransformer.IDENTITY;
+ }
+
+ Stats stats = new Stats(cache);
+
+ Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);
+
+ return new Picasso(context, dispatcher, cache, listener, transformer, stats, debugging);
+ }
+ }
+
+ /** Describes where the image was loaded from. */
+ public enum LoadedFrom {
+ MEMORY(Color.GREEN),
+ DISK(Color.YELLOW),
+ NETWORK(Color.RED);
+
+ final int debugColor;
+
+ private LoadedFrom(int debugColor) {
+ this.debugColor = debugColor;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/PicassoDrawable.java b/mobile/android/thirdparty/com/squareup/picasso/PicassoDrawable.java
new file mode 100644
index 0000000000..07f762c314
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/PicassoDrawable.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.AnimationDrawable;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.SystemClock;
+import android.widget.ImageView;
+
+import static android.graphics.Color.WHITE;
+import static com.squareup.picasso.Picasso.LoadedFrom.MEMORY;
+
+final class PicassoDrawable extends Drawable {
+ // Only accessed from main thread.
+ private static final Paint DEBUG_PAINT = new Paint();
+
+ private static final float FADE_DURATION = 200f; //ms
+
+ /**
+ * Create or update the drawable on the target {@link ImageView} to display the supplied bitmap
+ * image.
+ */
+ static void setBitmap(ImageView target, Context context, Bitmap bitmap,
+ Picasso.LoadedFrom loadedFrom, boolean noFade, boolean debugging) {
+ Drawable placeholder = target.getDrawable();
+ if (placeholder instanceof AnimationDrawable) {
+ ((AnimationDrawable) placeholder).stop();
+ }
+ PicassoDrawable drawable =
+ new PicassoDrawable(context, placeholder, bitmap, loadedFrom, noFade, debugging);
+ target.setImageDrawable(drawable);
+ }
+
+ /**
+ * Create or update the drawable on the target {@link ImageView} to display the supplied
+ * placeholder image.
+ */
+ static void setPlaceholder(ImageView target, int placeholderResId, Drawable placeholderDrawable) {
+ if (placeholderResId != 0) {
+ target.setImageResource(placeholderResId);
+ } else {
+ target.setImageDrawable(placeholderDrawable);
+ }
+ if (target.getDrawable() instanceof AnimationDrawable) {
+ ((AnimationDrawable) target.getDrawable()).start();
+ }
+ }
+
+ private final boolean debugging;
+ private final float density;
+ private final Picasso.LoadedFrom loadedFrom;
+ final BitmapDrawable image;
+
+ Drawable placeholder;
+
+ long startTimeMillis;
+ boolean animating;
+ int alpha = 0xFF;
+
+ PicassoDrawable(Context context, Drawable placeholder, Bitmap bitmap,
+ Picasso.LoadedFrom loadedFrom, boolean noFade, boolean debugging) {
+ Resources res = context.getResources();
+
+ this.debugging = debugging;
+ this.density = res.getDisplayMetrics().density;
+
+ this.loadedFrom = loadedFrom;
+
+ this.image = new BitmapDrawable(res, bitmap);
+
+ boolean fade = loadedFrom != MEMORY && !noFade;
+ if (fade) {
+ this.placeholder = placeholder;
+ animating = true;
+ startTimeMillis = SystemClock.uptimeMillis();
+ }
+ }
+
+ @Override public void draw(Canvas canvas) {
+ if (!animating) {
+ image.draw(canvas);
+ } else {
+ float normalized = (SystemClock.uptimeMillis() - startTimeMillis) / FADE_DURATION;
+ if (normalized >= 1f) {
+ animating = false;
+ placeholder = null;
+ image.draw(canvas);
+ } else {
+ if (placeholder != null) {
+ placeholder.draw(canvas);
+ }
+
+ int partialAlpha = (int) (alpha * normalized);
+ image.setAlpha(partialAlpha);
+ image.draw(canvas);
+ image.setAlpha(alpha);
+ invalidateSelf();
+ }
+ }
+
+ if (debugging) {
+ drawDebugIndicator(canvas);
+ }
+ }
+
+ @Override public int getIntrinsicWidth() {
+ return image.getIntrinsicWidth();
+ }
+
+ @Override public int getIntrinsicHeight() {
+ return image.getIntrinsicHeight();
+ }
+
+ @Override public void setAlpha(int alpha) {
+ this.alpha = alpha;
+ if (placeholder != null) {
+ placeholder.setAlpha(alpha);
+ }
+ image.setAlpha(alpha);
+ }
+
+ @Override public void setColorFilter(ColorFilter cf) {
+ if (placeholder != null) {
+ placeholder.setColorFilter(cf);
+ }
+ image.setColorFilter(cf);
+ }
+
+ @Override public int getOpacity() {
+ return image.getOpacity();
+ }
+
+ @Override protected void onBoundsChange(Rect bounds) {
+ super.onBoundsChange(bounds);
+
+ image.setBounds(bounds);
+ if (placeholder != null) {
+ placeholder.setBounds(bounds);
+ }
+ }
+
+ private void drawDebugIndicator(Canvas canvas) {
+ DEBUG_PAINT.setColor(WHITE);
+ Path path = getTrianglePath(new Point(0, 0), (int) (16 * density));
+ canvas.drawPath(path, DEBUG_PAINT);
+
+ DEBUG_PAINT.setColor(loadedFrom.debugColor);
+ path = getTrianglePath(new Point(0, 0), (int) (15 * density));
+ canvas.drawPath(path, DEBUG_PAINT);
+ }
+
+ private static Path getTrianglePath(Point p1, int width) {
+ Point p2 = new Point(p1.x + width, p1.y);
+ Point p3 = new Point(p1.x, p1.y + width);
+
+ Path path = new Path();
+ path.moveTo(p1.x, p1.y);
+ path.lineTo(p2.x, p2.y);
+ path.lineTo(p3.x, p3.y);
+
+ return path;
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/PicassoExecutorService.java b/mobile/android/thirdparty/com/squareup/picasso/PicassoExecutorService.java
new file mode 100644
index 0000000000..875dd2dda2
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/PicassoExecutorService.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.telephony.TelephonyManager;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The default {@link java.util.concurrent.ExecutorService} used for new {@link Picasso} instances.
+ * <p/>
+ * Exists as a custom type so that we can differentiate the use of defaults versus a user-supplied
+ * instance.
+ */
+class PicassoExecutorService extends ThreadPoolExecutor {
+ private static final int DEFAULT_THREAD_COUNT = 3;
+
+ PicassoExecutorService() {
+ super(DEFAULT_THREAD_COUNT, DEFAULT_THREAD_COUNT, 0, TimeUnit.MILLISECONDS,
+ new LinkedBlockingQueue<Runnable>(), new Utils.PicassoThreadFactory());
+ }
+
+ void adjustThreadCount(NetworkInfo info) {
+ if (info == null || !info.isConnectedOrConnecting()) {
+ setThreadCount(DEFAULT_THREAD_COUNT);
+ return;
+ }
+ switch (info.getType()) {
+ case ConnectivityManager.TYPE_WIFI:
+ case ConnectivityManager.TYPE_WIMAX:
+ case ConnectivityManager.TYPE_ETHERNET:
+ setThreadCount(4);
+ break;
+ case ConnectivityManager.TYPE_MOBILE:
+ switch (info.getSubtype()) {
+ case TelephonyManager.NETWORK_TYPE_LTE: // 4G
+ case TelephonyManager.NETWORK_TYPE_HSPAP:
+ case TelephonyManager.NETWORK_TYPE_EHRPD:
+ setThreadCount(3);
+ break;
+ case TelephonyManager.NETWORK_TYPE_UMTS: // 3G
+ case TelephonyManager.NETWORK_TYPE_CDMA:
+ case TelephonyManager.NETWORK_TYPE_EVDO_0:
+ case TelephonyManager.NETWORK_TYPE_EVDO_A:
+ case TelephonyManager.NETWORK_TYPE_EVDO_B:
+ setThreadCount(2);
+ break;
+ case TelephonyManager.NETWORK_TYPE_GPRS: // 2G
+ case TelephonyManager.NETWORK_TYPE_EDGE:
+ setThreadCount(1);
+ break;
+ default:
+ setThreadCount(DEFAULT_THREAD_COUNT);
+ }
+ break;
+ default:
+ setThreadCount(DEFAULT_THREAD_COUNT);
+ }
+ }
+
+ private void setThreadCount(int threadCount) {
+ setCorePoolSize(threadCount);
+ setMaximumPoolSize(threadCount);
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Request.java b/mobile/android/thirdparty/com/squareup/picasso/Request.java
new file mode 100644
index 0000000000..8e9c324608
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Request.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.net.Uri;
+import java.util.ArrayList;
+import java.util.List;
+
+import static java.util.Collections.unmodifiableList;
+
+/** Immutable data about an image and the transformations that will be applied to it. */
+public final class Request {
+ /**
+ * The image URI.
+ * <p>
+ * This is mutually exclusive with {@link #resourceId}.
+ */
+ public final Uri uri;
+ /**
+ * The image resource ID.
+ * <p>
+ * This is mutually exclusive with {@link #uri}.
+ */
+ public final int resourceId;
+ /** List of custom transformations to be applied after the built-in transformations. */
+ public final List<Transformation> transformations;
+ /** Target image width for resizing. */
+ public final int targetWidth;
+ /** Target image height for resizing. */
+ public final int targetHeight;
+ /**
+ * True if the final image should use the 'centerCrop' scale technique.
+ * <p>
+ * This is mutually exclusive with {@link #centerInside}.
+ */
+ public final boolean centerCrop;
+ /**
+ * True if the final image should use the 'centerInside' scale technique.
+ * <p>
+ * This is mutually exclusive with {@link #centerCrop}.
+ */
+ public final boolean centerInside;
+ /** Amount to rotate the image in degrees. */
+ public final float rotationDegrees;
+ /** Rotation pivot on the X axis. */
+ public final float rotationPivotX;
+ /** Rotation pivot on the Y axis. */
+ public final float rotationPivotY;
+ /** Whether or not {@link #rotationPivotX} and {@link #rotationPivotY} are set. */
+ public final boolean hasRotationPivot;
+
+ private Request(Uri uri, int resourceId, List<Transformation> transformations, int targetWidth,
+ int targetHeight, boolean centerCrop, boolean centerInside, float rotationDegrees,
+ float rotationPivotX, float rotationPivotY, boolean hasRotationPivot) {
+ this.uri = uri;
+ this.resourceId = resourceId;
+ if (transformations == null) {
+ this.transformations = null;
+ } else {
+ this.transformations = unmodifiableList(transformations);
+ }
+ this.targetWidth = targetWidth;
+ this.targetHeight = targetHeight;
+ this.centerCrop = centerCrop;
+ this.centerInside = centerInside;
+ this.rotationDegrees = rotationDegrees;
+ this.rotationPivotX = rotationPivotX;
+ this.rotationPivotY = rotationPivotY;
+ this.hasRotationPivot = hasRotationPivot;
+ }
+
+ String getName() {
+ if (uri != null) {
+ return uri.getPath();
+ }
+ return Integer.toHexString(resourceId);
+ }
+
+ public boolean hasSize() {
+ return targetWidth != 0;
+ }
+
+ boolean needsTransformation() {
+ return needsMatrixTransform() || hasCustomTransformations();
+ }
+
+ boolean needsMatrixTransform() {
+ return targetWidth != 0 || rotationDegrees != 0;
+ }
+
+ boolean hasCustomTransformations() {
+ return transformations != null;
+ }
+
+ public Builder buildUpon() {
+ return new Builder(this);
+ }
+
+ /** Builder for creating {@link Request} instances. */
+ public static final class Builder {
+ private Uri uri;
+ private int resourceId;
+ private int targetWidth;
+ private int targetHeight;
+ private boolean centerCrop;
+ private boolean centerInside;
+ private float rotationDegrees;
+ private float rotationPivotX;
+ private float rotationPivotY;
+ private boolean hasRotationPivot;
+ private List<Transformation> transformations;
+
+ /** Start building a request using the specified {@link Uri}. */
+ public Builder(Uri uri) {
+ setUri(uri);
+ }
+
+ /** Start building a request using the specified resource ID. */
+ public Builder(int resourceId) {
+ setResourceId(resourceId);
+ }
+
+ Builder(Uri uri, int resourceId) {
+ this.uri = uri;
+ this.resourceId = resourceId;
+ }
+
+ private Builder(Request request) {
+ uri = request.uri;
+ resourceId = request.resourceId;
+ targetWidth = request.targetWidth;
+ targetHeight = request.targetHeight;
+ centerCrop = request.centerCrop;
+ centerInside = request.centerInside;
+ rotationDegrees = request.rotationDegrees;
+ rotationPivotX = request.rotationPivotX;
+ rotationPivotY = request.rotationPivotY;
+ hasRotationPivot = request.hasRotationPivot;
+ if (request.transformations != null) {
+ transformations = new ArrayList<Transformation>(request.transformations);
+ }
+ }
+
+ boolean hasImage() {
+ return uri != null || resourceId != 0;
+ }
+
+ boolean hasSize() {
+ return targetWidth != 0;
+ }
+
+ /**
+ * Set the target image Uri.
+ * <p>
+ * This will clear an image resource ID if one is set.
+ */
+ public Builder setUri(Uri uri) {
+ if (uri == null) {
+ throw new IllegalArgumentException("Image URI may not be null.");
+ }
+ this.uri = uri;
+ this.resourceId = 0;
+ return this;
+ }
+
+ /**
+ * Set the target image resource ID.
+ * <p>
+ * This will clear an image Uri if one is set.
+ */
+ public Builder setResourceId(int resourceId) {
+ if (resourceId == 0) {
+ throw new IllegalArgumentException("Image resource ID may not be 0.");
+ }
+ this.resourceId = resourceId;
+ this.uri = null;
+ return this;
+ }
+
+ /** Resize the image to the specified size in pixels. */
+ public Builder resize(int targetWidth, int targetHeight) {
+ if (targetWidth <= 0) {
+ throw new IllegalArgumentException("Width must be positive number.");
+ }
+ if (targetHeight <= 0) {
+ throw new IllegalArgumentException("Height must be positive number.");
+ }
+ this.targetWidth = targetWidth;
+ this.targetHeight = targetHeight;
+ return this;
+ }
+
+ /** Clear the resize transformation, if any. This will also clear center crop/inside if set. */
+ public Builder clearResize() {
+ targetWidth = 0;
+ targetHeight = 0;
+ centerCrop = false;
+ centerInside = false;
+ return this;
+ }
+
+ /**
+ * Crops an image inside of the bounds specified by {@link #resize(int, int)} rather than
+ * distorting the aspect ratio. This cropping technique scales the image so that it fills the
+ * requested bounds and then crops the extra.
+ */
+ public Builder centerCrop() {
+ if (centerInside) {
+ throw new IllegalStateException("Center crop can not be used after calling centerInside");
+ }
+ centerCrop = true;
+ return this;
+ }
+
+ /** Clear the center crop transformation flag, if set. */
+ public Builder clearCenterCrop() {
+ centerCrop = false;
+ return this;
+ }
+
+ /**
+ * Centers an image inside of the bounds specified by {@link #resize(int, int)}. This scales
+ * the image so that both dimensions are equal to or less than the requested bounds.
+ */
+ public Builder centerInside() {
+ if (centerCrop) {
+ throw new IllegalStateException("Center inside can not be used after calling centerCrop");
+ }
+ centerInside = true;
+ return this;
+ }
+
+ /** Clear the center inside transformation flag, if set. */
+ public Builder clearCenterInside() {
+ centerInside = false;
+ return this;
+ }
+
+ /** Rotate the image by the specified degrees. */
+ public Builder rotate(float degrees) {
+ rotationDegrees = degrees;
+ return this;
+ }
+
+ /** Rotate the image by the specified degrees around a pivot point. */
+ public Builder rotate(float degrees, float pivotX, float pivotY) {
+ rotationDegrees = degrees;
+ rotationPivotX = pivotX;
+ rotationPivotY = pivotY;
+ hasRotationPivot = true;
+ return this;
+ }
+
+ /** Clear the rotation transformation, if any. */
+ public Builder clearRotation() {
+ rotationDegrees = 0;
+ rotationPivotX = 0;
+ rotationPivotY = 0;
+ hasRotationPivot = false;
+ return this;
+ }
+
+ /**
+ * Add a custom transformation to be applied to the image.
+ * <p/>
+ * Custom transformations will always be run after the built-in transformations.
+ */
+ public Builder transform(Transformation transformation) {
+ if (transformation == null) {
+ throw new IllegalArgumentException("Transformation must not be null.");
+ }
+ if (transformations == null) {
+ transformations = new ArrayList<Transformation>(2);
+ }
+ transformations.add(transformation);
+ return this;
+ }
+
+ /** Create the immutable {@link Request} object. */
+ public Request build() {
+ if (centerInside && centerCrop) {
+ throw new IllegalStateException("Center crop and center inside can not be used together.");
+ }
+ if (centerCrop && targetWidth == 0) {
+ throw new IllegalStateException("Center crop requires calling resize.");
+ }
+ if (centerInside && targetWidth == 0) {
+ throw new IllegalStateException("Center inside requires calling resize.");
+ }
+ return new Request(uri, resourceId, transformations, targetWidth, targetHeight, centerCrop,
+ centerInside, rotationDegrees, rotationPivotX, rotationPivotY, hasRotationPivot);
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/RequestCreator.java b/mobile/android/thirdparty/com/squareup/picasso/RequestCreator.java
new file mode 100644
index 0000000000..3a5ca3a9f8
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/RequestCreator.java
@@ -0,0 +1,374 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.widget.ImageView;
+import java.io.IOException;
+
+import static com.squareup.picasso.BitmapHunter.forRequest;
+import static com.squareup.picasso.Picasso.LoadedFrom.MEMORY;
+import static com.squareup.picasso.Utils.checkNotMain;
+import static com.squareup.picasso.Utils.createKey;
+
+/** Fluent API for building an image download request. */
+@SuppressWarnings("UnusedDeclaration") // Public API.
+public class RequestCreator {
+ private final Picasso picasso;
+ private final Request.Builder data;
+
+ private boolean skipMemoryCache;
+ private boolean noFade;
+ private boolean deferred;
+ private int placeholderResId;
+ private Drawable placeholderDrawable;
+ private int errorResId;
+ private Drawable errorDrawable;
+
+ RequestCreator(Picasso picasso, Uri uri, int resourceId) {
+ if (picasso.shutdown) {
+ throw new IllegalStateException(
+ "Picasso instance already shut down. Cannot submit new requests.");
+ }
+ this.picasso = picasso;
+ this.data = new Request.Builder(uri, resourceId);
+ }
+
+ /**
+ * A placeholder drawable to be used while the image is being loaded. If the requested image is
+ * not immediately available in the memory cache then this resource will be set on the target
+ * {@link ImageView}.
+ */
+ public RequestCreator placeholder(int placeholderResId) {
+ if (placeholderResId == 0) {
+ throw new IllegalArgumentException("Placeholder image resource invalid.");
+ }
+ if (placeholderDrawable != null) {
+ throw new IllegalStateException("Placeholder image already set.");
+ }
+ this.placeholderResId = placeholderResId;
+ return this;
+ }
+
+ /**
+ * A placeholder drawable to be used while the image is being loaded. If the requested image is
+ * not immediately available in the memory cache then this resource will be set on the target
+ * {@link ImageView}.
+ * <p>
+ * If you are not using a placeholder image but want to clear an existing image (such as when
+ * used in an {@link android.widget.Adapter adapter}), pass in {@code null}.
+ */
+ public RequestCreator placeholder(Drawable placeholderDrawable) {
+ if (placeholderResId != 0) {
+ throw new IllegalStateException("Placeholder image already set.");
+ }
+ this.placeholderDrawable = placeholderDrawable;
+ return this;
+ }
+
+ /** An error drawable to be used if the request image could not be loaded. */
+ public RequestCreator error(int errorResId) {
+ if (errorResId == 0) {
+ throw new IllegalArgumentException("Error image resource invalid.");
+ }
+ if (errorDrawable != null) {
+ throw new IllegalStateException("Error image already set.");
+ }
+ this.errorResId = errorResId;
+ return this;
+ }
+
+ /** An error drawable to be used if the request image could not be loaded. */
+ public RequestCreator error(Drawable errorDrawable) {
+ if (errorDrawable == null) {
+ throw new IllegalArgumentException("Error image may not be null.");
+ }
+ if (errorResId != 0) {
+ throw new IllegalStateException("Error image already set.");
+ }
+ this.errorDrawable = errorDrawable;
+ return this;
+ }
+
+ /**
+ * Attempt to resize the image to fit exactly into the target {@link ImageView}'s bounds. This
+ * will result in delayed execution of the request until the {@link ImageView} has been measured.
+ * <p/>
+ * <em>Note:</em> This method works only when your target is an {@link ImageView}.
+ */
+ public RequestCreator fit() {
+ deferred = true;
+ return this;
+ }
+
+ /** Internal use only. Used by {@link DeferredRequestCreator}. */
+ RequestCreator unfit() {
+ deferred = false;
+ return this;
+ }
+
+ /** Resize the image to the specified dimension size. */
+ public RequestCreator resizeDimen(int targetWidthResId, int targetHeightResId) {
+ Resources resources = picasso.context.getResources();
+ int targetWidth = resources.getDimensionPixelSize(targetWidthResId);
+ int targetHeight = resources.getDimensionPixelSize(targetHeightResId);
+ return resize(targetWidth, targetHeight);
+ }
+
+ /** Resize the image to the specified size in pixels. */
+ public RequestCreator resize(int targetWidth, int targetHeight) {
+ data.resize(targetWidth, targetHeight);
+ return this;
+ }
+
+ /**
+ * Crops an image inside of the bounds specified by {@link #resize(int, int)} rather than
+ * distorting the aspect ratio. This cropping technique scales the image so that it fills the
+ * requested bounds and then crops the extra.
+ */
+ public RequestCreator centerCrop() {
+ data.centerCrop();
+ return this;
+ }
+
+ /**
+ * Centers an image inside of the bounds specified by {@link #resize(int, int)}. This scales
+ * the image so that both dimensions are equal to or less than the requested bounds.
+ */
+ public RequestCreator centerInside() {
+ data.centerInside();
+ return this;
+ }
+
+ /** Rotate the image by the specified degrees. */
+ public RequestCreator rotate(float degrees) {
+ data.rotate(degrees);
+ return this;
+ }
+
+ /** Rotate the image by the specified degrees around a pivot point. */
+ public RequestCreator rotate(float degrees, float pivotX, float pivotY) {
+ data.rotate(degrees, pivotX, pivotY);
+ return this;
+ }
+
+ /**
+ * Add a custom transformation to be applied to the image.
+ * <p/>
+ * Custom transformations will always be run after the built-in transformations.
+ */
+ // TODO show example of calling resize after a transform in the javadoc
+ public RequestCreator transform(Transformation transformation) {
+ data.transform(transformation);
+ return this;
+ }
+
+ /**
+ * Indicate that this action should not use the memory cache for attempting to load or save the
+ * image. This can be useful when you know an image will only ever be used once (e.g., loading
+ * an image from the filesystem and uploading to a remote server).
+ */
+ public RequestCreator skipMemoryCache() {
+ skipMemoryCache = true;
+ return this;
+ }
+
+ /** Disable brief fade in of images loaded from the disk cache or network. */
+ public RequestCreator noFade() {
+ noFade = true;
+ return this;
+ }
+
+ /** Synchronously fulfill this request. Must not be called from the main thread. */
+ public Bitmap get() throws IOException {
+ checkNotMain();
+ if (deferred) {
+ throw new IllegalStateException("Fit cannot be used with get.");
+ }
+ if (!data.hasImage()) {
+ return null;
+ }
+
+ Request finalData = picasso.transformRequest(data.build());
+ String key = createKey(finalData);
+
+ Action action = new GetAction(picasso, finalData, skipMemoryCache, key);
+ return forRequest(picasso.context, picasso, picasso.dispatcher, picasso.cache, picasso.stats,
+ action, picasso.dispatcher.downloader).hunt();
+ }
+
+ /**
+ * Asynchronously fulfills the request without a {@link ImageView} or {@link Target}. This is
+ * useful when you want to warm up the cache with an image.
+ */
+ public void fetch() {
+ if (deferred) {
+ throw new IllegalStateException("Fit cannot be used with fetch.");
+ }
+ if (data.hasImage()) {
+ Request finalData = picasso.transformRequest(data.build());
+ String key = createKey(finalData);
+
+ Action action = new FetchAction(picasso, finalData, skipMemoryCache, key);
+ picasso.enqueueAndSubmit(action);
+ }
+ }
+
+ /**
+ * Asynchronously fulfills the request into the specified {@link Target}. In most cases, you
+ * should use this when you are dealing with a custom {@link android.view.View View} or view
+ * holder which should implement the {@link Target} interface.
+ * <p>
+ * Implementing on a {@link android.view.View View}:
+ * <blockquote><pre>
+ * public class ProfileView extends FrameLayout implements Target {
+ * {@literal @}Override public void onBitmapLoaded(Bitmap bitmap, LoadedFrom from) {
+ * setBackgroundDrawable(new BitmapDrawable(bitmap));
+ * }
+ *
+ * {@literal @}Override public void onBitmapFailed() {
+ * setBackgroundResource(R.drawable.profile_error);
+ * }
+ * }
+ * </pre></blockquote>
+ * Implementing on a view holder object for use inside of an adapter:
+ * <blockquote><pre>
+ * public class ViewHolder implements Target {
+ * public FrameLayout frame;
+ * public TextView name;
+ *
+ * {@literal @}Override public void onBitmapLoaded(Bitmap bitmap, LoadedFrom from) {
+ * frame.setBackgroundDrawable(new BitmapDrawable(bitmap));
+ * }
+ *
+ * {@literal @}Override public void onBitmapFailed() {
+ * frame.setBackgroundResource(R.drawable.profile_error);
+ * }
+ * }
+ * </pre></blockquote>
+ * <p>
+ * <em>Note:</em> This method keeps a weak reference to the {@link Target} instance and will be
+ * garbage collected if you do not keep a strong reference to it. To receive callbacks when an
+ * image is loaded use {@link #into(android.widget.ImageView, Callback)}.
+ */
+ public void into(Target target) {
+ if (target == null) {
+ throw new IllegalArgumentException("Target must not be null.");
+ }
+ if (deferred) {
+ throw new IllegalStateException("Fit cannot be used with a Target.");
+ }
+
+ Drawable drawable =
+ placeholderResId != 0 ? picasso.context.getResources().getDrawable(placeholderResId)
+ : placeholderDrawable;
+
+ if (!data.hasImage()) {
+ picasso.cancelRequest(target);
+ target.onPrepareLoad(drawable);
+ return;
+ }
+
+ Request finalData = picasso.transformRequest(data.build());
+ String requestKey = createKey(finalData);
+
+ if (!skipMemoryCache) {
+ Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
+ if (bitmap != null) {
+ picasso.cancelRequest(target);
+ target.onBitmapLoaded(bitmap, MEMORY);
+ return;
+ }
+ }
+
+ target.onPrepareLoad(drawable);
+
+ Action action = new TargetAction(picasso, target, finalData, skipMemoryCache, requestKey);
+ picasso.enqueueAndSubmit(action);
+ }
+
+ /**
+ * Asynchronously fulfills the request into the specified {@link ImageView}.
+ * <p/>
+ * <em>Note:</em> This method keeps a weak reference to the {@link ImageView} instance and will
+ * automatically support object recycling.
+ */
+ public void into(ImageView target) {
+ into(target, null);
+ }
+
+ /**
+ * Asynchronously fulfills the request into the specified {@link ImageView} and invokes the
+ * target {@link Callback} if it's not {@code null}.
+ * <p/>
+ * <em>Note:</em> The {@link Callback} param is a strong reference and will prevent your
+ * {@link android.app.Activity} or {@link android.app.Fragment} from being garbage collected. If
+ * you use this method, it is <b>strongly</b> recommended you invoke an adjacent
+ * {@link Picasso#cancelRequest(android.widget.ImageView)} call to prevent temporary leaking.
+ */
+ public void into(ImageView target, Callback callback) {
+ if (target == null) {
+ throw new IllegalArgumentException("Target must not be null.");
+ }
+
+ if (!data.hasImage()) {
+ picasso.cancelRequest(target);
+ PicassoDrawable.setPlaceholder(target, placeholderResId, placeholderDrawable);
+ return;
+ }
+
+ if (deferred) {
+ if (data.hasSize()) {
+ throw new IllegalStateException("Fit cannot be used with resize.");
+ }
+ int measuredWidth = target.getMeasuredWidth();
+ int measuredHeight = target.getMeasuredHeight();
+ if (measuredWidth == 0 || measuredHeight == 0) {
+ PicassoDrawable.setPlaceholder(target, placeholderResId, placeholderDrawable);
+ picasso.defer(target, new DeferredRequestCreator(this, target, callback));
+ return;
+ }
+ data.resize(measuredWidth, measuredHeight);
+ }
+
+ Request finalData = picasso.transformRequest(data.build());
+ String requestKey = createKey(finalData);
+
+ if (!skipMemoryCache) {
+ Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
+ if (bitmap != null) {
+ picasso.cancelRequest(target);
+ PicassoDrawable.setBitmap(target, picasso.context, bitmap, MEMORY, noFade,
+ picasso.debugging);
+ if (callback != null) {
+ callback.onSuccess();
+ }
+ return;
+ }
+ }
+
+ PicassoDrawable.setPlaceholder(target, placeholderResId, placeholderDrawable);
+
+ Action action =
+ new ImageViewAction(picasso, target, finalData, skipMemoryCache, noFade, errorResId,
+ errorDrawable, requestKey, callback);
+
+ picasso.enqueueAndSubmit(action);
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/ResourceBitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/ResourceBitmapHunter.java
new file mode 100644
index 0000000000..fee76b2001
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/ResourceBitmapHunter.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import java.io.IOException;
+
+import static com.squareup.picasso.Picasso.LoadedFrom.DISK;
+
+class ResourceBitmapHunter extends BitmapHunter {
+ private final Context context;
+
+ ResourceBitmapHunter(Context context, Picasso picasso, Dispatcher dispatcher, Cache cache,
+ Stats stats, Action action) {
+ super(picasso, dispatcher, cache, stats, action);
+ this.context = context;
+ }
+
+ @Override Bitmap decode(Request data) throws IOException {
+ Resources res = Utils.getResources(context, data);
+ int id = Utils.getResourceId(res, data);
+ return decodeResource(res, id, data);
+ }
+
+ @Override Picasso.LoadedFrom getLoadedFrom() {
+ return DISK;
+ }
+
+ private Bitmap decodeResource(Resources resources, int id, Request data) {
+ BitmapFactory.Options bitmapOptions = null;
+ if (data.hasSize()) {
+ bitmapOptions = new BitmapFactory.Options();
+ bitmapOptions.inJustDecodeBounds = true;
+ BitmapFactory.decodeResource(resources, id, bitmapOptions);
+ calculateInSampleSize(data.targetWidth, data.targetHeight, bitmapOptions);
+ }
+ return BitmapFactory.decodeResource(resources, id, bitmapOptions);
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Stats.java b/mobile/android/thirdparty/com/squareup/picasso/Stats.java
new file mode 100644
index 0000000000..3eaac0249b
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Stats.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.graphics.Bitmap;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+
+import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
+
+class Stats {
+ private static final int CACHE_HIT = 0;
+ private static final int CACHE_MISS = 1;
+ private static final int BITMAP_DECODE_FINISHED = 2;
+ private static final int BITMAP_TRANSFORMED_FINISHED = 3;
+
+ private static final String STATS_THREAD_NAME = Utils.THREAD_PREFIX + "Stats";
+
+ final HandlerThread statsThread;
+ final Cache cache;
+ final Handler handler;
+
+ long cacheHits;
+ long cacheMisses;
+ long totalOriginalBitmapSize;
+ long totalTransformedBitmapSize;
+ long averageOriginalBitmapSize;
+ long averageTransformedBitmapSize;
+ int originalBitmapCount;
+ int transformedBitmapCount;
+
+ Stats(Cache cache) {
+ this.cache = cache;
+ this.statsThread = new HandlerThread(STATS_THREAD_NAME, THREAD_PRIORITY_BACKGROUND);
+ this.statsThread.start();
+ this.handler = new StatsHandler(statsThread.getLooper(), this);
+ }
+
+ void dispatchBitmapDecoded(Bitmap bitmap) {
+ processBitmap(bitmap, BITMAP_DECODE_FINISHED);
+ }
+
+ void dispatchBitmapTransformed(Bitmap bitmap) {
+ processBitmap(bitmap, BITMAP_TRANSFORMED_FINISHED);
+ }
+
+ void dispatchCacheHit() {
+ handler.sendEmptyMessage(CACHE_HIT);
+ }
+
+ void dispatchCacheMiss() {
+ handler.sendEmptyMessage(CACHE_MISS);
+ }
+
+ void shutdown() {
+ statsThread.quit();
+ }
+
+ void performCacheHit() {
+ cacheHits++;
+ }
+
+ void performCacheMiss() {
+ cacheMisses++;
+ }
+
+ void performBitmapDecoded(long size) {
+ originalBitmapCount++;
+ totalOriginalBitmapSize += size;
+ averageOriginalBitmapSize = getAverage(originalBitmapCount, totalOriginalBitmapSize);
+ }
+
+ void performBitmapTransformed(long size) {
+ transformedBitmapCount++;
+ totalTransformedBitmapSize += size;
+ averageTransformedBitmapSize = getAverage(originalBitmapCount, totalTransformedBitmapSize);
+ }
+
+ synchronized StatsSnapshot createSnapshot() {
+ return new StatsSnapshot(cache.maxSize(), cache.size(), cacheHits, cacheMisses,
+ totalOriginalBitmapSize, totalTransformedBitmapSize, averageOriginalBitmapSize,
+ averageTransformedBitmapSize, originalBitmapCount, transformedBitmapCount,
+ System.currentTimeMillis());
+ }
+
+ private void processBitmap(Bitmap bitmap, int what) {
+ // Never send bitmaps to the handler as they could be recycled before we process them.
+ int bitmapSize = Utils.getBitmapBytes(bitmap);
+ handler.sendMessage(handler.obtainMessage(what, bitmapSize, 0));
+ }
+
+ private static long getAverage(int count, long totalSize) {
+ return totalSize / count;
+ }
+
+ private static class StatsHandler extends Handler {
+
+ private final Stats stats;
+
+ public StatsHandler(Looper looper, Stats stats) {
+ super(looper);
+ this.stats = stats;
+ }
+
+ @Override public void handleMessage(final Message msg) {
+ switch (msg.what) {
+ case CACHE_HIT:
+ stats.performCacheHit();
+ break;
+ case CACHE_MISS:
+ stats.performCacheMiss();
+ break;
+ case BITMAP_DECODE_FINISHED:
+ stats.performBitmapDecoded(msg.arg1);
+ break;
+ case BITMAP_TRANSFORMED_FINISHED:
+ stats.performBitmapTransformed(msg.arg1);
+ break;
+ default:
+ Picasso.HANDLER.post(new Runnable() {
+ @Override public void run() {
+ throw new AssertionError("Unhandled stats message." + msg.what);
+ }
+ });
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/mobile/android/thirdparty/com/squareup/picasso/StatsSnapshot.java b/mobile/android/thirdparty/com/squareup/picasso/StatsSnapshot.java
new file mode 100644
index 0000000000..5f276ebf27
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/StatsSnapshot.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.util.Log;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+/** Represents all stats for a {@link Picasso} instance at a single point in time. */
+public class StatsSnapshot {
+ private static final String TAG = "Picasso";
+
+ public final int maxSize;
+ public final int size;
+ public final long cacheHits;
+ public final long cacheMisses;
+ public final long totalOriginalBitmapSize;
+ public final long totalTransformedBitmapSize;
+ public final long averageOriginalBitmapSize;
+ public final long averageTransformedBitmapSize;
+ public final int originalBitmapCount;
+ public final int transformedBitmapCount;
+
+ public final long timeStamp;
+
+ public StatsSnapshot(int maxSize, int size, long cacheHits, long cacheMisses,
+ long totalOriginalBitmapSize, long totalTransformedBitmapSize, long averageOriginalBitmapSize,
+ long averageTransformedBitmapSize, int originalBitmapCount, int transformedBitmapCount,
+ long timeStamp) {
+ this.maxSize = maxSize;
+ this.size = size;
+ this.cacheHits = cacheHits;
+ this.cacheMisses = cacheMisses;
+ this.totalOriginalBitmapSize = totalOriginalBitmapSize;
+ this.totalTransformedBitmapSize = totalTransformedBitmapSize;
+ this.averageOriginalBitmapSize = averageOriginalBitmapSize;
+ this.averageTransformedBitmapSize = averageTransformedBitmapSize;
+ this.originalBitmapCount = originalBitmapCount;
+ this.transformedBitmapCount = transformedBitmapCount;
+ this.timeStamp = timeStamp;
+ }
+
+ /** Prints out this {@link StatsSnapshot} into log. */
+ public void dump() {
+ StringWriter logWriter = new StringWriter();
+ dump(new PrintWriter(logWriter));
+ Log.i(TAG, logWriter.toString());
+ }
+
+ /** Prints out this {@link StatsSnapshot} with the the provided {@link PrintWriter}. */
+ public void dump(PrintWriter writer) {
+ writer.println("===============BEGIN PICASSO STATS ===============");
+ writer.println("Memory Cache Stats");
+ writer.print(" Max Cache Size: ");
+ writer.println(maxSize);
+ writer.print(" Cache Size: ");
+ writer.println(size);
+ writer.print(" Cache % Full: ");
+ writer.println((int) Math.ceil((float) size / maxSize * 100));
+ writer.print(" Cache Hits: ");
+ writer.println(cacheHits);
+ writer.print(" Cache Misses: ");
+ writer.println(cacheMisses);
+ writer.println("Bitmap Stats");
+ writer.print(" Total Bitmaps Decoded: ");
+ writer.println(originalBitmapCount);
+ writer.print(" Total Bitmap Size: ");
+ writer.println(totalOriginalBitmapSize);
+ writer.print(" Total Transformed Bitmaps: ");
+ writer.println(transformedBitmapCount);
+ writer.print(" Total Transformed Bitmap Size: ");
+ writer.println(totalTransformedBitmapSize);
+ writer.print(" Average Bitmap Size: ");
+ writer.println(averageOriginalBitmapSize);
+ writer.print(" Average Transformed Bitmap Size: ");
+ writer.println(averageTransformedBitmapSize);
+ writer.println("===============END PICASSO STATS ===============");
+ writer.flush();
+ }
+
+ @Override public String toString() {
+ return "StatsSnapshot{"
+ + "maxSize="
+ + maxSize
+ + ", size="
+ + size
+ + ", cacheHits="
+ + cacheHits
+ + ", cacheMisses="
+ + cacheMisses
+ + ", totalOriginalBitmapSize="
+ + totalOriginalBitmapSize
+ + ", totalTransformedBitmapSize="
+ + totalTransformedBitmapSize
+ + ", averageOriginalBitmapSize="
+ + averageOriginalBitmapSize
+ + ", averageTransformedBitmapSize="
+ + averageTransformedBitmapSize
+ + ", originalBitmapCount="
+ + originalBitmapCount
+ + ", transformedBitmapCount="
+ + transformedBitmapCount
+ + ", timeStamp="
+ + timeStamp
+ + '}';
+ }
+} \ No newline at end of file
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Target.java b/mobile/android/thirdparty/com/squareup/picasso/Target.java
new file mode 100644
index 0000000000..ad3ce6fcf6
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Target.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+
+import static com.squareup.picasso.Picasso.LoadedFrom;
+
+/**
+ * Represents an arbitrary listener for image loading.
+ * <p/>
+ * Objects implementing this class <strong>must</strong> have a working implementation of
+ * {@link #equals(Object)} and {@link #hashCode()} for proper storage internally. Instances of this
+ * interface will also be compared to determine if view recycling is occurring. It is recommended
+ * that you add this interface directly on to a custom view type when using in an adapter to ensure
+ * correct recycling behavior.
+ */
+public interface Target {
+ /**
+ * Callback when an image has been successfully loaded.
+ * <p/>
+ * <strong>Note:</strong> You must not recycle the bitmap.
+ */
+ void onBitmapLoaded(Bitmap bitmap, LoadedFrom from);
+
+ /** Callback indicating the image could not be successfully loaded. */
+ void onBitmapFailed(Drawable errorDrawable);
+
+ /** Callback invoked right before your request is submitted. */
+ void onPrepareLoad(Drawable placeHolderDrawable);
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/TargetAction.java b/mobile/android/thirdparty/com/squareup/picasso/TargetAction.java
new file mode 100644
index 0000000000..77a40f51dd
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/TargetAction.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.graphics.Bitmap;
+
+final class TargetAction extends Action<Target> {
+
+ TargetAction(Picasso picasso, Target target, Request data, boolean skipCache, String key) {
+ super(picasso, target, data, skipCache, false, 0, null, key);
+ }
+
+ @Override void complete(Bitmap result, Picasso.LoadedFrom from) {
+ if (result == null) {
+ throw new AssertionError(
+ String.format("Attempted to complete action with no result!\n%s", this));
+ }
+ Target target = getTarget();
+ if (target != null) {
+ target.onBitmapLoaded(result, from);
+ if (result.isRecycled()) {
+ throw new IllegalStateException("Target callback must not recycle bitmap!");
+ }
+ }
+ }
+
+ @Override void error() {
+ Target target = getTarget();
+ if (target != null) {
+ target.onBitmapFailed(errorDrawable);
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Transformation.java b/mobile/android/thirdparty/com/squareup/picasso/Transformation.java
new file mode 100644
index 0000000000..2c59f160c5
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Transformation.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.graphics.Bitmap;
+
+/** Image transformation. */
+public interface Transformation {
+ /**
+ * Transform the source bitmap into a new bitmap. If you create a new bitmap instance, you must
+ * call {@link android.graphics.Bitmap#recycle()} on {@code source}. You may return the original
+ * if no transformation is required.
+ */
+ Bitmap transform(Bitmap source);
+
+ /**
+ * Returns a unique key for the transformation, used for caching purposes. If the transformation
+ * has parameters (e.g. size, scale factor, etc) then these should be part of the key.
+ */
+ String key();
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/UrlConnectionDownloader.java b/mobile/android/thirdparty/com/squareup/picasso/UrlConnectionDownloader.java
new file mode 100644
index 0000000000..50f9b2b983
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/UrlConnectionDownloader.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.content.Context;
+import android.net.Uri;
+import android.net.http.HttpResponseCache;
+import android.os.Build;
+import java.io.File;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import static com.squareup.picasso.Utils.parseResponseSourceHeader;
+
+/**
+ * A {@link Downloader} which uses {@link HttpURLConnection} to download images. A disk cache of 2%
+ * of the total available space will be used (capped at 50MB) will automatically be installed in the
+ * application's cache directory, when available.
+ */
+public class UrlConnectionDownloader implements Downloader {
+ static final String RESPONSE_SOURCE = "X-Android-Response-Source";
+
+ private static final Object lock = new Object();
+ static volatile Object cache;
+
+ private final Context context;
+
+ public UrlConnectionDownloader(Context context) {
+ this.context = context.getApplicationContext();
+ }
+
+ protected HttpURLConnection openConnection(Uri path) throws IOException {
+ HttpURLConnection connection = (HttpURLConnection) new URL(path.toString()).openConnection();
+ connection.setConnectTimeout(Utils.DEFAULT_CONNECT_TIMEOUT);
+ connection.setReadTimeout(Utils.DEFAULT_READ_TIMEOUT);
+ return connection;
+ }
+
+ @Override public Response load(Uri uri, boolean localCacheOnly) throws IOException {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ installCacheIfNeeded(context);
+ }
+
+ HttpURLConnection connection = openConnection(uri);
+ connection.setUseCaches(true);
+ if (localCacheOnly) {
+ connection.setRequestProperty("Cache-Control", "only-if-cached,max-age=" + Integer.MAX_VALUE);
+ }
+
+ int responseCode = connection.getResponseCode();
+ if (responseCode >= 300) {
+ connection.disconnect();
+ throw new ResponseException(responseCode + " " + connection.getResponseMessage());
+ }
+
+ boolean fromCache = parseResponseSourceHeader(connection.getHeaderField(RESPONSE_SOURCE));
+
+ return new Response(connection.getInputStream(), fromCache);
+ }
+
+ private static void installCacheIfNeeded(Context context) {
+ // DCL + volatile should be safe after Java 5.
+ if (cache == null) {
+ try {
+ synchronized (lock) {
+ if (cache == null) {
+ cache = ResponseCacheIcs.install(context);
+ }
+ }
+ } catch (IOException ignored) {
+ }
+ }
+ }
+
+ private static class ResponseCacheIcs {
+ static Object install(Context context) throws IOException {
+ File cacheDir = Utils.createDefaultCacheDir(context);
+ HttpResponseCache cache = HttpResponseCache.getInstalled();
+ if (cache == null) {
+ long maxSize = Utils.calculateDiskCacheSize(cacheDir);
+ cache = HttpResponseCache.install(cacheDir, maxSize);
+ }
+ return cache;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Utils.java b/mobile/android/thirdparty/com/squareup/picasso/Utils.java
new file mode 100644
index 0000000000..bafe93f98f
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Utils.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.picasso;
+
+import android.annotation.TargetApi;
+import android.app.ActivityManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.os.Looper;
+import android.os.Process;
+import android.os.StatFs;
+import android.provider.Settings;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.concurrent.ThreadFactory;
+
+import static android.content.Context.ACTIVITY_SERVICE;
+import static android.content.pm.ApplicationInfo.FLAG_LARGE_HEAP;
+import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES.HONEYCOMB;
+import static android.os.Build.VERSION_CODES.HONEYCOMB_MR1;
+import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
+import static android.provider.Settings.System.AIRPLANE_MODE_ON;
+
+final class Utils {
+ static final String THREAD_PREFIX = "Picasso-";
+ static final String THREAD_IDLE_NAME = THREAD_PREFIX + "Idle";
+ static final int DEFAULT_READ_TIMEOUT = 20 * 1000; // 20s
+ static final int DEFAULT_CONNECT_TIMEOUT = 15 * 1000; // 15s
+ private static final String PICASSO_CACHE = "picasso-cache";
+ private static final int KEY_PADDING = 50; // Determined by exact science.
+ private static final int MIN_DISK_CACHE_SIZE = 5 * 1024 * 1024; // 5MB
+ private static final int MAX_DISK_CACHE_SIZE = 50 * 1024 * 1024; // 50MB
+
+ /* WebP file header
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | 'R' | 'I' | 'F' | 'F' |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | File Size |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | 'W' | 'E' | 'B' | 'P' |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+ private static final int WEBP_FILE_HEADER_SIZE = 12;
+ private static final String WEBP_FILE_HEADER_RIFF = "RIFF";
+ private static final String WEBP_FILE_HEADER_WEBP = "WEBP";
+
+ private Utils() {
+ // No instances.
+ }
+
+ static int getBitmapBytes(Bitmap bitmap) {
+ int result;
+ if (SDK_INT >= HONEYCOMB_MR1) {
+ result = BitmapHoneycombMR1.getByteCount(bitmap);
+ } else {
+ result = bitmap.getRowBytes() * bitmap.getHeight();
+ }
+ if (result < 0) {
+ throw new IllegalStateException("Negative size: " + bitmap);
+ }
+ return result;
+ }
+
+ static void checkNotMain() {
+ if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
+ throw new IllegalStateException("Method call should not happen from the main thread.");
+ }
+ }
+
+ static String createKey(Request data) {
+ StringBuilder builder;
+
+ if (data.uri != null) {
+ String path = data.uri.toString();
+ builder = new StringBuilder(path.length() + KEY_PADDING);
+ builder.append(path);
+ } else {
+ builder = new StringBuilder(KEY_PADDING);
+ builder.append(data.resourceId);
+ }
+ builder.append('\n');
+
+ if (data.rotationDegrees != 0) {
+ builder.append("rotation:").append(data.rotationDegrees);
+ if (data.hasRotationPivot) {
+ builder.append('@').append(data.rotationPivotX).append('x').append(data.rotationPivotY);
+ }
+ builder.append('\n');
+ }
+ if (data.targetWidth != 0) {
+ builder.append("resize:").append(data.targetWidth).append('x').append(data.targetHeight);
+ builder.append('\n');
+ }
+ if (data.centerCrop) {
+ builder.append("centerCrop\n");
+ } else if (data.centerInside) {
+ builder.append("centerInside\n");
+ }
+
+ if (data.transformations != null) {
+ //noinspection ForLoopReplaceableByForEach
+ for (int i = 0, count = data.transformations.size(); i < count; i++) {
+ builder.append(data.transformations.get(i).key());
+ builder.append('\n');
+ }
+ }
+
+ return builder.toString();
+ }
+
+ static void closeQuietly(InputStream is) {
+ if (is == null) return;
+ try {
+ is.close();
+ } catch (IOException ignored) {
+ }
+ }
+
+ /** Returns {@code true} if header indicates the response body was loaded from the disk cache. */
+ static boolean parseResponseSourceHeader(String header) {
+ if (header == null) {
+ return false;
+ }
+ String[] parts = header.split(" ", 2);
+ if ("CACHE".equals(parts[0])) {
+ return true;
+ }
+ if (parts.length == 1) {
+ return false;
+ }
+ try {
+ return "CONDITIONAL_CACHE".equals(parts[0]) && Integer.parseInt(parts[1]) == 304;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
+ static Downloader createDefaultDownloader(Context context) {
+ return new UrlConnectionDownloader(context);
+ }
+
+ static File createDefaultCacheDir(Context context) {
+ File cache = new File(context.getApplicationContext().getCacheDir(), PICASSO_CACHE);
+ if (!cache.exists()) {
+ cache.mkdirs();
+ }
+ return cache;
+ }
+
+ static long calculateDiskCacheSize(File dir) {
+ long size = MIN_DISK_CACHE_SIZE;
+
+ try {
+ StatFs statFs = new StatFs(dir.getAbsolutePath());
+ long available = ((long) statFs.getBlockCount()) * statFs.getBlockSize();
+ // Target 2% of the total space.
+ size = available / 50;
+ } catch (IllegalArgumentException ignored) {
+ }
+
+ // Bound inside min/max size for disk cache.
+ return Math.max(Math.min(size, MAX_DISK_CACHE_SIZE), MIN_DISK_CACHE_SIZE);
+ }
+
+ static int calculateMemoryCacheSize(Context context) {
+ ActivityManager am = (ActivityManager) context.getSystemService(ACTIVITY_SERVICE);
+ boolean largeHeap = (context.getApplicationInfo().flags & FLAG_LARGE_HEAP) != 0;
+ int memoryClass = am.getMemoryClass();
+ if (largeHeap && SDK_INT >= HONEYCOMB) {
+ memoryClass = ActivityManagerHoneycomb.getLargeMemoryClass(am);
+ }
+ // Target ~15% of the available heap.
+ return 1024 * 1024 * memoryClass / 7;
+ }
+
+ static boolean isAirplaneModeOn(Context context) {
+ ContentResolver contentResolver = context.getContentResolver();
+ return Settings.System.getInt(contentResolver, AIRPLANE_MODE_ON, 0) != 0;
+ }
+
+ static boolean hasPermission(Context context, String permission) {
+ return context.checkCallingOrSelfPermission(permission) == PackageManager.PERMISSION_GRANTED;
+ }
+
+ static byte[] toByteArray(InputStream input) throws IOException {
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1024 * 4];
+ int n = 0;
+ while (-1 != (n = input.read(buffer))) {
+ byteArrayOutputStream.write(buffer, 0, n);
+ }
+ return byteArrayOutputStream.toByteArray();
+ }
+
+ static boolean isWebPFile(InputStream stream) throws IOException {
+ byte[] fileHeaderBytes = new byte[WEBP_FILE_HEADER_SIZE];
+ boolean isWebPFile = false;
+ if (stream.read(fileHeaderBytes, 0, WEBP_FILE_HEADER_SIZE) == WEBP_FILE_HEADER_SIZE) {
+ // If a file's header starts with RIFF and end with WEBP, the file is a WebP file
+ isWebPFile = WEBP_FILE_HEADER_RIFF.equals(new String(fileHeaderBytes, 0, 4, "US-ASCII"))
+ && WEBP_FILE_HEADER_WEBP.equals(new String(fileHeaderBytes, 8, 4, "US-ASCII"));
+ }
+ return isWebPFile;
+ }
+
+ static int getResourceId(Resources resources, Request data) throws FileNotFoundException {
+ if (data.resourceId != 0 || data.uri == null) {
+ return data.resourceId;
+ }
+
+ String pkg = data.uri.getAuthority();
+ if (pkg == null) throw new FileNotFoundException("No package provided: " + data.uri);
+
+ int id;
+ List<String> segments = data.uri.getPathSegments();
+ if (segments == null || segments.isEmpty()) {
+ throw new FileNotFoundException("No path segments: " + data.uri);
+ } else if (segments.size() == 1) {
+ try {
+ id = Integer.parseInt(segments.get(0));
+ } catch (NumberFormatException e) {
+ throw new FileNotFoundException("Last path segment is not a resource ID: " + data.uri);
+ }
+ } else if (segments.size() == 2) {
+ String type = segments.get(0);
+ String name = segments.get(1);
+
+ id = resources.getIdentifier(name, type, pkg);
+ } else {
+ throw new FileNotFoundException("More than two path segments: " + data.uri);
+ }
+ return id;
+ }
+
+ static Resources getResources(Context context, Request data) throws FileNotFoundException {
+ if (data.resourceId != 0 || data.uri == null) {
+ return context.getResources();
+ }
+
+ String pkg = data.uri.getAuthority();
+ if (pkg == null) throw new FileNotFoundException("No package provided: " + data.uri);
+ try {
+ PackageManager pm = context.getPackageManager();
+ return pm.getResourcesForApplication(pkg);
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new FileNotFoundException("Unable to obtain resources for package: " + data.uri);
+ }
+ }
+
+ @TargetApi(HONEYCOMB)
+ private static class ActivityManagerHoneycomb {
+ static int getLargeMemoryClass(ActivityManager activityManager) {
+ return activityManager.getLargeMemoryClass();
+ }
+ }
+
+ static class PicassoThreadFactory implements ThreadFactory {
+ @SuppressWarnings("NullableProblems")
+ public Thread newThread(Runnable r) {
+ return new PicassoThread(r);
+ }
+ }
+
+ private static class PicassoThread extends Thread {
+ public PicassoThread(Runnable r) {
+ super(r);
+ }
+
+ @Override public void run() {
+ Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);
+ super.run();
+ }
+ }
+
+ @TargetApi(HONEYCOMB_MR1)
+ private static class BitmapHoneycombMR1 {
+ static int getByteCount(Bitmap bitmap) {
+ return bitmap.getByteCount();
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/org/json/simple/ItemList.java b/mobile/android/thirdparty/org/json/simple/ItemList.java
new file mode 100644
index 0000000000..07231e6733
--- /dev/null
+++ b/mobile/android/thirdparty/org/json/simple/ItemList.java
@@ -0,0 +1,147 @@
+/*
+ * $Id: ItemList.java,v 1.1 2006/04/15 14:10:48 platform Exp $
+ * Created on 2006-3-24
+ */
+package org.json.simple;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/**
+ * |a:b:c| => |a|,|b|,|c|
+ * |:| => ||,||
+ * |a:| => |a|,||
+ * @author FangYidong<fangyidong@yahoo.com.cn>
+ */
+public class ItemList {
+ private String sp=",";
+ List items=new ArrayList();
+
+
+ public ItemList(){}
+
+
+ public ItemList(String s){
+ this.split(s,sp,items);
+ }
+
+ public ItemList(String s,String sp){
+ this.sp=s;
+ this.split(s,sp,items);
+ }
+
+ public ItemList(String s,String sp,boolean isMultiToken){
+ split(s,sp,items,isMultiToken);
+ }
+
+ public List getItems(){
+ return this.items;
+ }
+
+ public String[] getArray(){
+ return (String[])this.items.toArray();
+ }
+
+ public void split(String s,String sp,List append,boolean isMultiToken){
+ if(s==null || sp==null)
+ return;
+ if(isMultiToken){
+ StringTokenizer tokens=new StringTokenizer(s,sp);
+ while(tokens.hasMoreTokens()){
+ append.add(tokens.nextToken().trim());
+ }
+ }
+ else{
+ this.split(s,sp,append);
+ }
+ }
+
+ public void split(String s,String sp,List append){
+ if(s==null || sp==null)
+ return;
+ int pos=0;
+ int prevPos=0;
+ do{
+ prevPos=pos;
+ pos=s.indexOf(sp,pos);
+ if(pos==-1)
+ break;
+ append.add(s.substring(prevPos,pos).trim());
+ pos+=sp.length();
+ }while(pos!=-1);
+ append.add(s.substring(prevPos).trim());
+ }
+
+ public void setSP(String sp){
+ this.sp=sp;
+ }
+
+ public void add(int i,String item){
+ if(item==null)
+ return;
+ items.add(i,item.trim());
+ }
+
+ public void add(String item){
+ if(item==null)
+ return;
+ items.add(item.trim());
+ }
+
+ public void addAll(ItemList list){
+ items.addAll(list.items);
+ }
+
+ public void addAll(String s){
+ this.split(s,sp,items);
+ }
+
+ public void addAll(String s,String sp){
+ this.split(s,sp,items);
+ }
+
+ public void addAll(String s,String sp,boolean isMultiToken){
+ this.split(s,sp,items,isMultiToken);
+ }
+
+ /**
+ * @param i 0-based
+ * @return
+ */
+ public String get(int i){
+ return (String)items.get(i);
+ }
+
+ public int size(){
+ return items.size();
+ }
+
+ public String toString(){
+ return toString(sp);
+ }
+
+ public String toString(String sp){
+ StringBuffer sb=new StringBuffer();
+
+ for(int i=0;i<items.size();i++){
+ if(i==0)
+ sb.append(items.get(i));
+ else{
+ sb.append(sp);
+ sb.append(items.get(i));
+ }
+ }
+ return sb.toString();
+
+ }
+
+ public void clear(){
+ items.clear();
+ }
+
+ public void reset(){
+ sp=",";
+ items.clear();
+ }
+}
diff --git a/mobile/android/thirdparty/org/json/simple/JSONArray.java b/mobile/android/thirdparty/org/json/simple/JSONArray.java
new file mode 100644
index 0000000000..57167f482f
--- /dev/null
+++ b/mobile/android/thirdparty/org/json/simple/JSONArray.java
@@ -0,0 +1,107 @@
+/*
+ * $Id: JSONArray.java,v 1.1 2006/04/15 14:10:48 platform Exp $
+ * Created on 2006-4-10
+ */
+package org.json.simple;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+
+/**
+ * A JSON array. JSONObject supports java.util.List interface.
+ *
+ * @author FangYidong<fangyidong@yahoo.com.cn>
+ */
+public class JSONArray extends ArrayList implements List, JSONAware, JSONStreamAware {
+ private static final long serialVersionUID = 3957988303675231981L;
+
+ /**
+ * Encode a list into JSON text and write it to out.
+ * If this list is also a JSONStreamAware or a JSONAware, JSONStreamAware and JSONAware specific behaviours will be ignored at this top level.
+ *
+ * @see org.json.simple.JSONValue#writeJSONString(Object, Writer)
+ *
+ * @param list
+ * @param out
+ */
+ public static void writeJSONString(List list, Writer out) throws IOException{
+ if(list == null){
+ out.write("null");
+ return;
+ }
+
+ boolean first = true;
+ Iterator iter=list.iterator();
+
+ out.write('[');
+ while(iter.hasNext()){
+ if(first)
+ first = false;
+ else
+ out.write(',');
+
+ Object value=iter.next();
+ if(value == null){
+ out.write("null");
+ continue;
+ }
+
+ JSONValue.writeJSONString(value, out);
+ }
+ out.write(']');
+ }
+
+ public void writeJSONString(Writer out) throws IOException{
+ writeJSONString(this, out);
+ }
+
+ /**
+ * Convert a list to JSON text. The result is a JSON array.
+ * If this list is also a JSONAware, JSONAware specific behaviours will be omitted at this top level.
+ *
+ * @see org.json.simple.JSONValue#toJSONString(Object)
+ *
+ * @param list
+ * @return JSON text, or "null" if list is null.
+ */
+ public static String toJSONString(List list){
+ if(list == null)
+ return "null";
+
+ boolean first = true;
+ StringBuffer sb = new StringBuffer();
+ Iterator iter=list.iterator();
+
+ sb.append('[');
+ while(iter.hasNext()){
+ if(first)
+ first = false;
+ else
+ sb.append(',');
+
+ Object value=iter.next();
+ if(value == null){
+ sb.append("null");
+ continue;
+ }
+ sb.append(JSONValue.toJSONString(value));
+ }
+ sb.append(']');
+ return sb.toString();
+ }
+
+ public String toJSONString(){
+ return toJSONString(this);
+ }
+
+ public String toString() {
+ return toJSONString();
+ }
+
+
+
+}
diff --git a/mobile/android/thirdparty/org/json/simple/JSONAware.java b/mobile/android/thirdparty/org/json/simple/JSONAware.java
new file mode 100644
index 0000000000..89f1525104
--- /dev/null
+++ b/mobile/android/thirdparty/org/json/simple/JSONAware.java
@@ -0,0 +1,12 @@
+package org.json.simple;
+
+/**
+ * Beans that support customized output of JSON text shall implement this interface.
+ * @author FangYidong<fangyidong@yahoo.com.cn>
+ */
+public interface JSONAware {
+ /**
+ * @return JSON text
+ */
+ String toJSONString();
+}
diff --git a/mobile/android/thirdparty/org/json/simple/JSONObject.java b/mobile/android/thirdparty/org/json/simple/JSONObject.java
new file mode 100644
index 0000000000..d4401e1144
--- /dev/null
+++ b/mobile/android/thirdparty/org/json/simple/JSONObject.java
@@ -0,0 +1,129 @@
+/*
+ * $Id: JSONObject.java,v 1.1 2006/04/15 14:10:48 platform Exp $
+ * Created on 2006-4-10
+ */
+package org.json.simple;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * A JSON object. Key value pairs are unordered. JSONObject supports java.util.Map interface.
+ *
+ * @author FangYidong<fangyidong@yahoo.com.cn>
+ */
+public class JSONObject extends HashMap implements Map, JSONAware, JSONStreamAware{
+ private static final long serialVersionUID = -503443796854799292L;
+
+ /**
+ * Encode a map into JSON text and write it to out.
+ * If this map is also a JSONAware or JSONStreamAware, JSONAware or JSONStreamAware specific behaviours will be ignored at this top level.
+ *
+ * @see org.json.simple.JSONValue#writeJSONString(Object, Writer)
+ *
+ * @param map
+ * @param out
+ */
+ public static void writeJSONString(Map map, Writer out) throws IOException {
+ if(map == null){
+ out.write("null");
+ return;
+ }
+
+ boolean first = true;
+ Iterator iter=map.entrySet().iterator();
+
+ out.write('{');
+ while(iter.hasNext()){
+ if(first)
+ first = false;
+ else
+ out.write(',');
+ Map.Entry entry=(Map.Entry)iter.next();
+ out.write('\"');
+ out.write(escape(String.valueOf(entry.getKey())));
+ out.write('\"');
+ out.write(':');
+ JSONValue.writeJSONString(entry.getValue(), out);
+ }
+ out.write('}');
+ }
+
+ public void writeJSONString(Writer out) throws IOException{
+ writeJSONString(this, out);
+ }
+
+ /**
+ * Convert a map to JSON text. The result is a JSON object.
+ * If this map is also a JSONAware, JSONAware specific behaviours will be omitted at this top level.
+ *
+ * @see org.json.simple.JSONValue#toJSONString(Object)
+ *
+ * @param map
+ * @return JSON text, or "null" if map is null.
+ */
+ public static String toJSONString(Map map){
+ if(map == null)
+ return "null";
+
+ StringBuffer sb = new StringBuffer();
+ boolean first = true;
+ Iterator iter=map.entrySet().iterator();
+
+ sb.append('{');
+ while(iter.hasNext()){
+ if(first)
+ first = false;
+ else
+ sb.append(',');
+
+ Map.Entry entry=(Map.Entry)iter.next();
+ toJSONString(String.valueOf(entry.getKey()),entry.getValue(), sb);
+ }
+ sb.append('}');
+ return sb.toString();
+ }
+
+ public String toJSONString(){
+ return toJSONString(this);
+ }
+
+ private static String toJSONString(String key,Object value, StringBuffer sb){
+ sb.append('\"');
+ if(key == null)
+ sb.append("null");
+ else
+ JSONValue.escape(key, sb);
+ sb.append('\"').append(':');
+
+ sb.append(JSONValue.toJSONString(value));
+
+ return sb.toString();
+ }
+
+ public String toString(){
+ return toJSONString();
+ }
+
+ public static String toString(String key,Object value){
+ StringBuffer sb = new StringBuffer();
+ toJSONString(key, value, sb);
+ return sb.toString();
+ }
+
+ /**
+ * Escape quotes, \, /, \r, \n, \b, \f, \t and other control characters (U+0000 through U+001F).
+ * It's the same as JSONValue.escape() only for compatibility here.
+ *
+ * @see org.json.simple.JSONValue#escape(String)
+ *
+ * @param s
+ * @return
+ */
+ public static String escape(String s){
+ return JSONValue.escape(s);
+ }
+}
diff --git a/mobile/android/thirdparty/org/json/simple/JSONStreamAware.java b/mobile/android/thirdparty/org/json/simple/JSONStreamAware.java
new file mode 100644
index 0000000000..c2287c459e
--- /dev/null
+++ b/mobile/android/thirdparty/org/json/simple/JSONStreamAware.java
@@ -0,0 +1,15 @@
+package org.json.simple;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Beans that support customized output of JSON text to a writer shall implement this interface.
+ * @author FangYidong<fangyidong@yahoo.com.cn>
+ */
+public interface JSONStreamAware {
+ /**
+ * write JSON string to out.
+ */
+ void writeJSONString(Writer out) throws IOException;
+}
diff --git a/mobile/android/thirdparty/org/json/simple/JSONValue.java b/mobile/android/thirdparty/org/json/simple/JSONValue.java
new file mode 100644
index 0000000000..aba3c40c20
--- /dev/null
+++ b/mobile/android/thirdparty/org/json/simple/JSONValue.java
@@ -0,0 +1,272 @@
+/*
+ * $Id: JSONValue.java,v 1.1 2006/04/15 14:37:04 platform Exp $
+ * Created on 2006-4-15
+ */
+package org.json.simple;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.Writer;
+import java.util.List;
+import java.util.Map;
+
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+
+
+/**
+ * @author FangYidong<fangyidong@yahoo.com.cn>
+ */
+public class JSONValue {
+ /**
+ * Parse JSON text into java object from the input source.
+ * Please use parseWithException() if you don't want to ignore the exception.
+ *
+ * @see org.json.simple.parser.JSONParser#parse(Reader)
+ * @see #parseWithException(Reader)
+ *
+ * @param in
+ * @return Instance of the following:
+ * org.json.simple.JSONObject,
+ * org.json.simple.JSONArray,
+ * java.lang.String,
+ * java.lang.Number,
+ * java.lang.Boolean,
+ * null
+ *
+ */
+ public static Object parse(Reader in){
+ try{
+ JSONParser parser=new JSONParser();
+ return parser.parse(in);
+ }
+ catch(Exception e){
+ return null;
+ }
+ }
+
+ public static Object parse(String s){
+ StringReader in=new StringReader(s);
+ return parse(in);
+ }
+
+ /**
+ * Parse JSON text into java object from the input source.
+ *
+ * @see org.json.simple.parser.JSONParser
+ *
+ * @param in
+ * @return Instance of the following:
+ * org.json.simple.JSONObject,
+ * org.json.simple.JSONArray,
+ * java.lang.String,
+ * java.lang.Number,
+ * java.lang.Boolean,
+ * null
+ *
+ * @throws IOException
+ * @throws ParseException
+ */
+ public static Object parseWithException(Reader in) throws IOException, ParseException{
+ JSONParser parser=new JSONParser();
+ return parser.parse(in);
+ }
+
+ public static Object parseWithException(String s) throws ParseException{
+ JSONParser parser=new JSONParser();
+ return parser.parse(s);
+ }
+
+ /**
+ * Encode an object into JSON text and write it to out.
+ * <p>
+ * If this object is a Map or a List, and it's also a JSONStreamAware or a JSONAware, JSONStreamAware or JSONAware will be considered firstly.
+ * <p>
+ * DO NOT call this method from writeJSONString(Writer) of a class that implements both JSONStreamAware and (Map or List) with
+ * "this" as the first parameter, use JSONObject.writeJSONString(Map, Writer) or JSONArray.writeJSONString(List, Writer) instead.
+ *
+ * @see org.json.simple.JSONObject#writeJSONString(Map, Writer)
+ * @see org.json.simple.JSONArray#writeJSONString(List, Writer)
+ *
+ * @param value
+ * @param writer
+ */
+ public static void writeJSONString(Object value, Writer out) throws IOException {
+ if(value == null){
+ out.write("null");
+ return;
+ }
+
+ if(value instanceof String){
+ out.write('\"');
+ out.write(escape((String)value));
+ out.write('\"');
+ return;
+ }
+
+ if(value instanceof Double){
+ if(((Double)value).isInfinite() || ((Double)value).isNaN())
+ out.write("null");
+ else
+ out.write(value.toString());
+ return;
+ }
+
+ if(value instanceof Float){
+ if(((Float)value).isInfinite() || ((Float)value).isNaN())
+ out.write("null");
+ else
+ out.write(value.toString());
+ return;
+ }
+
+ if(value instanceof Number){
+ out.write(value.toString());
+ return;
+ }
+
+ if(value instanceof Boolean){
+ out.write(value.toString());
+ return;
+ }
+
+ if((value instanceof JSONStreamAware)){
+ ((JSONStreamAware)value).writeJSONString(out);
+ return;
+ }
+
+ if((value instanceof JSONAware)){
+ out.write(((JSONAware)value).toJSONString());
+ return;
+ }
+
+ if(value instanceof Map){
+ JSONObject.writeJSONString((Map)value, out);
+ return;
+ }
+
+ if(value instanceof List){
+ JSONArray.writeJSONString((List)value, out);
+ return;
+ }
+
+ out.write(value.toString());
+ }
+
+ /**
+ * Convert an object to JSON text.
+ * <p>
+ * If this object is a Map or a List, and it's also a JSONAware, JSONAware will be considered firstly.
+ * <p>
+ * DO NOT call this method from toJSONString() of a class that implements both JSONAware and Map or List with
+ * "this" as the parameter, use JSONObject.toJSONString(Map) or JSONArray.toJSONString(List) instead.
+ *
+ * @see org.json.simple.JSONObject#toJSONString(Map)
+ * @see org.json.simple.JSONArray#toJSONString(List)
+ *
+ * @param value
+ * @return JSON text, or "null" if value is null or it's an NaN or an INF number.
+ */
+ public static String toJSONString(Object value){
+ if(value == null)
+ return "null";
+
+ if(value instanceof String)
+ return "\""+escape((String)value)+"\"";
+
+ if(value instanceof Double){
+ if(((Double)value).isInfinite() || ((Double)value).isNaN())
+ return "null";
+ else
+ return value.toString();
+ }
+
+ if(value instanceof Float){
+ if(((Float)value).isInfinite() || ((Float)value).isNaN())
+ return "null";
+ else
+ return value.toString();
+ }
+
+ if(value instanceof Number)
+ return value.toString();
+
+ if(value instanceof Boolean)
+ return value.toString();
+
+ if((value instanceof JSONAware))
+ return ((JSONAware)value).toJSONString();
+
+ if(value instanceof Map)
+ return JSONObject.toJSONString((Map)value);
+
+ if(value instanceof List)
+ return JSONArray.toJSONString((List)value);
+
+ return value.toString();
+ }
+
+ /**
+ * Escape quotes, \, /, \r, \n, \b, \f, \t and other control characters (U+0000 through U+001F).
+ * @param s
+ * @return
+ */
+ public static String escape(String s){
+ if(s==null)
+ return null;
+ StringBuffer sb = new StringBuffer();
+ escape(s, sb);
+ return sb.toString();
+ }
+
+ /**
+ * @param s - Must not be null.
+ * @param sb
+ */
+ static void escape(String s, StringBuffer sb) {
+ for(int i=0;i<s.length();i++){
+ char ch=s.charAt(i);
+ switch(ch){
+ case '"':
+ sb.append("\\\"");
+ break;
+ case '\\':
+ sb.append("\\\\");
+ break;
+ case '\b':
+ sb.append("\\b");
+ break;
+ case '\f':
+ sb.append("\\f");
+ break;
+ case '\n':
+ sb.append("\\n");
+ break;
+ case '\r':
+ sb.append("\\r");
+ break;
+ case '\t':
+ sb.append("\\t");
+ break;
+ case '/':
+ sb.append("\\/");
+ break;
+ default:
+ //Reference: http://www.unicode.org/versions/Unicode5.1.0/
+ if((ch>='\u0000' && ch<='\u001F') || (ch>='\u007F' && ch<='\u009F') || (ch>='\u2000' && ch<='\u20FF')){
+ String ss=Integer.toHexString(ch);
+ sb.append("\\u");
+ for(int k=0;k<4-ss.length();k++){
+ sb.append('0');
+ }
+ sb.append(ss.toUpperCase());
+ }
+ else{
+ sb.append(ch);
+ }
+ }
+ }//for
+ }
+
+}
diff --git a/mobile/android/thirdparty/org/json/simple/LICENSE.txt b/mobile/android/thirdparty/org/json/simple/LICENSE.txt
new file mode 100644
index 0000000000..d645695673
--- /dev/null
+++ b/mobile/android/thirdparty/org/json/simple/LICENSE.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/mobile/android/thirdparty/org/json/simple/parser/ContainerFactory.java b/mobile/android/thirdparty/org/json/simple/parser/ContainerFactory.java
new file mode 100644
index 0000000000..366ac4dead
--- /dev/null
+++ b/mobile/android/thirdparty/org/json/simple/parser/ContainerFactory.java
@@ -0,0 +1,23 @@
+package org.json.simple.parser;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Container factory for creating containers for JSON object and JSON array.
+ *
+ * @see org.json.simple.parser.JSONParser#parse(java.io.Reader, ContainerFactory)
+ *
+ * @author FangYidong<fangyidong@yahoo.com.cn>
+ */
+public interface ContainerFactory {
+ /**
+ * @return A Map instance to store JSON object, or null if you want to use org.json.simple.JSONObject.
+ */
+ Map createObjectContainer();
+
+ /**
+ * @return A List instance to store JSON array, or null if you want to use org.json.simple.JSONArray.
+ */
+ List creatArrayContainer();
+}
diff --git a/mobile/android/thirdparty/org/json/simple/parser/ContentHandler.java b/mobile/android/thirdparty/org/json/simple/parser/ContentHandler.java
new file mode 100644
index 0000000000..ae8d065579
--- /dev/null
+++ b/mobile/android/thirdparty/org/json/simple/parser/ContentHandler.java
@@ -0,0 +1,110 @@
+package org.json.simple.parser;
+
+import java.io.IOException;
+
+/**
+ * A simplified and stoppable SAX-like content handler for stream processing of JSON text.
+ *
+ * @see org.xml.sax.ContentHandler
+ * @see org.json.simple.parser.JSONParser#parse(java.io.Reader, ContentHandler, boolean)
+ *
+ * @author FangYidong<fangyidong@yahoo.com.cn>
+ */
+public interface ContentHandler {
+ /**
+ * Receive notification of the beginning of JSON processing.
+ * The parser will invoke this method only once.
+ *
+ * @throws ParseException
+ * - JSONParser will stop and throw the same exception to the caller when receiving this exception.
+ */
+ void startJSON() throws ParseException, IOException;
+
+ /**
+ * Receive notification of the end of JSON processing.
+ *
+ * @throws ParseException
+ */
+ void endJSON() throws ParseException, IOException;
+
+ /**
+ * Receive notification of the beginning of a JSON object.
+ *
+ * @return false if the handler wants to stop parsing after return.
+ * @throws ParseException
+ * - JSONParser will stop and throw the same exception to the caller when receiving this exception.
+ * @see #endJSON
+ */
+ boolean startObject() throws ParseException, IOException;
+
+ /**
+ * Receive notification of the end of a JSON object.
+ *
+ * @return false if the handler wants to stop parsing after return.
+ * @throws ParseException
+ *
+ * @see #startObject
+ */
+ boolean endObject() throws ParseException, IOException;
+
+ /**
+ * Receive notification of the beginning of a JSON object entry.
+ *
+ * @param key - Key of a JSON object entry.
+ *
+ * @return false if the handler wants to stop parsing after return.
+ * @throws ParseException
+ *
+ * @see #endObjectEntry
+ */
+ boolean startObjectEntry(String key) throws ParseException, IOException;
+
+ /**
+ * Receive notification of the end of the value of previous object entry.
+ *
+ * @return false if the handler wants to stop parsing after return.
+ * @throws ParseException
+ *
+ * @see #startObjectEntry
+ */
+ boolean endObjectEntry() throws ParseException, IOException;
+
+ /**
+ * Receive notification of the beginning of a JSON array.
+ *
+ * @return false if the handler wants to stop parsing after return.
+ * @throws ParseException
+ *
+ * @see #endArray
+ */
+ boolean startArray() throws ParseException, IOException;
+
+ /**
+ * Receive notification of the end of a JSON array.
+ *
+ * @return false if the handler wants to stop parsing after return.
+ * @throws ParseException
+ *
+ * @see #startArray
+ */
+ boolean endArray() throws ParseException, IOException;
+
+ /**
+ * Receive notification of the JSON primitive values:
+ * java.lang.String,
+ * java.lang.Number,
+ * java.lang.Boolean
+ * null
+ *
+ * @param value - Instance of the following:
+ * java.lang.String,
+ * java.lang.Number,
+ * java.lang.Boolean
+ * null
+ *
+ * @return false if the handler wants to stop parsing after return.
+ * @throws ParseException
+ */
+ boolean primitive(Object value) throws ParseException, IOException;
+
+}
diff --git a/mobile/android/thirdparty/org/json/simple/parser/JSONParser.java b/mobile/android/thirdparty/org/json/simple/parser/JSONParser.java
new file mode 100644
index 0000000000..9acaa377fe
--- /dev/null
+++ b/mobile/android/thirdparty/org/json/simple/parser/JSONParser.java
@@ -0,0 +1,533 @@
+/*
+ * $Id: JSONParser.java,v 1.1 2006/04/15 14:10:48 platform Exp $
+ * Created on 2006-4-15
+ */
+package org.json.simple.parser;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+
+
+/**
+ * Parser for JSON text. Please note that JSONParser is NOT thread-safe.
+ *
+ * @author FangYidong<fangyidong@yahoo.com.cn>
+ */
+public class JSONParser {
+ public static final int S_INIT=0;
+ public static final int S_IN_FINISHED_VALUE=1;//string,number,boolean,null,object,array
+ public static final int S_IN_OBJECT=2;
+ public static final int S_IN_ARRAY=3;
+ public static final int S_PASSED_PAIR_KEY=4;
+ public static final int S_IN_PAIR_VALUE=5;
+ public static final int S_END=6;
+ public static final int S_IN_ERROR=-1;
+
+ private LinkedList handlerStatusStack;
+ private Yylex lexer = new Yylex((Reader)null);
+ private Yytoken token = null;
+ private int status = S_INIT;
+
+ private int peekStatus(LinkedList statusStack){
+ if(statusStack.size()==0)
+ return -1;
+ Integer status=(Integer)statusStack.getFirst();
+ return status.intValue();
+ }
+
+ /**
+ * Reset the parser to the initial state without resetting the underlying reader.
+ *
+ */
+ public void reset(){
+ token = null;
+ status = S_INIT;
+ handlerStatusStack = null;
+ }
+
+ /**
+ * Reset the parser to the initial state with a new character reader.
+ *
+ * @param in - The new character reader.
+ * @throws IOException
+ * @throws ParseException
+ */
+ public void reset(Reader in){
+ lexer.yyreset(in);
+ reset();
+ }
+
+ /**
+ * @return The position of the beginning of the current token.
+ */
+ public int getPosition(){
+ return lexer.getPosition();
+ }
+
+ public Object parse(String s) throws ParseException{
+ return parse(s, (ContainerFactory)null);
+ }
+
+ public Object parse(String s, ContainerFactory containerFactory) throws ParseException{
+ StringReader in=new StringReader(s);
+ try{
+ return parse(in, containerFactory);
+ }
+ catch(IOException ie){
+ /*
+ * Actually it will never happen.
+ */
+ throw new ParseException(-1, ParseException.ERROR_UNEXPECTED_EXCEPTION, ie);
+ }
+ }
+
+ public Object parse(Reader in) throws IOException, ParseException{
+ return parse(in, (ContainerFactory)null);
+ }
+
+ /**
+ * Parse JSON text into java object from the input source.
+ *
+ * @param in
+ * @param containerFactory - Use this factory to createyour own JSON object and JSON array containers.
+ * @return Instance of the following:
+ * org.json.simple.JSONObject,
+ * org.json.simple.JSONArray,
+ * java.lang.String,
+ * java.lang.Number,
+ * java.lang.Boolean,
+ * null
+ *
+ * @throws IOException
+ * @throws ParseException
+ */
+ public Object parse(Reader in, ContainerFactory containerFactory) throws IOException, ParseException{
+ reset(in);
+ LinkedList statusStack = new LinkedList();
+ LinkedList valueStack = new LinkedList();
+
+ try{
+ do{
+ nextToken();
+ switch(status){
+ case S_INIT:
+ switch(token.type){
+ case Yytoken.TYPE_VALUE:
+ status=S_IN_FINISHED_VALUE;
+ statusStack.addFirst(new Integer(status));
+ valueStack.addFirst(token.value);
+ break;
+ case Yytoken.TYPE_LEFT_BRACE:
+ status=S_IN_OBJECT;
+ statusStack.addFirst(new Integer(status));
+ valueStack.addFirst(createObjectContainer(containerFactory));
+ break;
+ case Yytoken.TYPE_LEFT_SQUARE:
+ status=S_IN_ARRAY;
+ statusStack.addFirst(new Integer(status));
+ valueStack.addFirst(createArrayContainer(containerFactory));
+ break;
+ default:
+ status=S_IN_ERROR;
+ }//inner switch
+ break;
+
+ case S_IN_FINISHED_VALUE:
+ if(token.type==Yytoken.TYPE_EOF)
+ return valueStack.removeFirst();
+ else
+ throw new ParseException(getPosition(), ParseException.ERROR_UNEXPECTED_TOKEN, token);
+
+ case S_IN_OBJECT:
+ switch(token.type){
+ case Yytoken.TYPE_COMMA:
+ break;
+ case Yytoken.TYPE_VALUE:
+ if(token.value instanceof String){
+ String key=(String)token.value;
+ valueStack.addFirst(key);
+ status=S_PASSED_PAIR_KEY;
+ statusStack.addFirst(new Integer(status));
+ }
+ else{
+ status=S_IN_ERROR;
+ }
+ break;
+ case Yytoken.TYPE_RIGHT_BRACE:
+ if(valueStack.size()>1){
+ statusStack.removeFirst();
+ valueStack.removeFirst();
+ status=peekStatus(statusStack);
+ }
+ else{
+ status=S_IN_FINISHED_VALUE;
+ }
+ break;
+ default:
+ status=S_IN_ERROR;
+ break;
+ }//inner switch
+ break;
+
+ case S_PASSED_PAIR_KEY:
+ switch(token.type){
+ case Yytoken.TYPE_COLON:
+ break;
+ case Yytoken.TYPE_VALUE:
+ statusStack.removeFirst();
+ String key=(String)valueStack.removeFirst();
+ Map parent=(Map)valueStack.getFirst();
+ parent.put(key,token.value);
+ status=peekStatus(statusStack);
+ break;
+ case Yytoken.TYPE_LEFT_SQUARE:
+ statusStack.removeFirst();
+ key=(String)valueStack.removeFirst();
+ parent=(Map)valueStack.getFirst();
+ List newArray=createArrayContainer(containerFactory);
+ parent.put(key,newArray);
+ status=S_IN_ARRAY;
+ statusStack.addFirst(new Integer(status));
+ valueStack.addFirst(newArray);
+ break;
+ case Yytoken.TYPE_LEFT_BRACE:
+ statusStack.removeFirst();
+ key=(String)valueStack.removeFirst();
+ parent=(Map)valueStack.getFirst();
+ Map newObject=createObjectContainer(containerFactory);
+ parent.put(key,newObject);
+ status=S_IN_OBJECT;
+ statusStack.addFirst(new Integer(status));
+ valueStack.addFirst(newObject);
+ break;
+ default:
+ status=S_IN_ERROR;
+ }
+ break;
+
+ case S_IN_ARRAY:
+ switch(token.type){
+ case Yytoken.TYPE_COMMA:
+ break;
+ case Yytoken.TYPE_VALUE:
+ List val=(List)valueStack.getFirst();
+ val.add(token.value);
+ break;
+ case Yytoken.TYPE_RIGHT_SQUARE:
+ if(valueStack.size()>1){
+ statusStack.removeFirst();
+ valueStack.removeFirst();
+ status=peekStatus(statusStack);
+ }
+ else{
+ status=S_IN_FINISHED_VALUE;
+ }
+ break;
+ case Yytoken.TYPE_LEFT_BRACE:
+ val=(List)valueStack.getFirst();
+ Map newObject=createObjectContainer(containerFactory);
+ val.add(newObject);
+ status=S_IN_OBJECT;
+ statusStack.addFirst(new Integer(status));
+ valueStack.addFirst(newObject);
+ break;
+ case Yytoken.TYPE_LEFT_SQUARE:
+ val=(List)valueStack.getFirst();
+ List newArray=createArrayContainer(containerFactory);
+ val.add(newArray);
+ status=S_IN_ARRAY;
+ statusStack.addFirst(new Integer(status));
+ valueStack.addFirst(newArray);
+ break;
+ default:
+ status=S_IN_ERROR;
+ }//inner switch
+ break;
+ case S_IN_ERROR:
+ throw new ParseException(getPosition(), ParseException.ERROR_UNEXPECTED_TOKEN, token);
+ }//switch
+ if(status==S_IN_ERROR){
+ throw new ParseException(getPosition(), ParseException.ERROR_UNEXPECTED_TOKEN, token);
+ }
+ }while(token.type!=Yytoken.TYPE_EOF);
+ }
+ catch(IOException ie){
+ throw ie;
+ }
+
+ throw new ParseException(getPosition(), ParseException.ERROR_UNEXPECTED_TOKEN, token);
+ }
+
+ private void nextToken() throws ParseException, IOException{
+ token = lexer.yylex();
+ if(token == null)
+ token = new Yytoken(Yytoken.TYPE_EOF, null);
+ }
+
+ private Map createObjectContainer(ContainerFactory containerFactory){
+ if(containerFactory == null)
+ return new JSONObject();
+ Map m = containerFactory.createObjectContainer();
+
+ if(m == null)
+ return new JSONObject();
+ return m;
+ }
+
+ private List createArrayContainer(ContainerFactory containerFactory){
+ if(containerFactory == null)
+ return new JSONArray();
+ List l = containerFactory.creatArrayContainer();
+
+ if(l == null)
+ return new JSONArray();
+ return l;
+ }
+
+ public void parse(String s, ContentHandler contentHandler) throws ParseException{
+ parse(s, contentHandler, false);
+ }
+
+ public void parse(String s, ContentHandler contentHandler, boolean isResume) throws ParseException{
+ StringReader in=new StringReader(s);
+ try{
+ parse(in, contentHandler, isResume);
+ }
+ catch(IOException ie){
+ /*
+ * Actually it will never happen.
+ */
+ throw new ParseException(-1, ParseException.ERROR_UNEXPECTED_EXCEPTION, ie);
+ }
+ }
+
+ public void parse(Reader in, ContentHandler contentHandler) throws IOException, ParseException{
+ parse(in, contentHandler, false);
+ }
+
+ /**
+ * Stream processing of JSON text.
+ *
+ * @see ContentHandler
+ *
+ * @param in
+ * @param contentHandler
+ * @param isResume - Indicates if it continues previous parsing operation.
+ * If set to true, resume parsing the old stream, and parameter 'in' will be ignored.
+ * If this method is called for the first time in this instance, isResume will be ignored.
+ *
+ * @throws IOException
+ * @throws ParseException
+ */
+ public void parse(Reader in, ContentHandler contentHandler, boolean isResume) throws IOException, ParseException{
+ if(!isResume){
+ reset(in);
+ handlerStatusStack = new LinkedList();
+ }
+ else{
+ if(handlerStatusStack == null){
+ isResume = false;
+ reset(in);
+ handlerStatusStack = new LinkedList();
+ }
+ }
+
+ LinkedList statusStack = handlerStatusStack;
+
+ try{
+ do{
+ switch(status){
+ case S_INIT:
+ contentHandler.startJSON();
+ nextToken();
+ switch(token.type){
+ case Yytoken.TYPE_VALUE:
+ status=S_IN_FINISHED_VALUE;
+ statusStack.addFirst(new Integer(status));
+ if(!contentHandler.primitive(token.value))
+ return;
+ break;
+ case Yytoken.TYPE_LEFT_BRACE:
+ status=S_IN_OBJECT;
+ statusStack.addFirst(new Integer(status));
+ if(!contentHandler.startObject())
+ return;
+ break;
+ case Yytoken.TYPE_LEFT_SQUARE:
+ status=S_IN_ARRAY;
+ statusStack.addFirst(new Integer(status));
+ if(!contentHandler.startArray())
+ return;
+ break;
+ default:
+ status=S_IN_ERROR;
+ }//inner switch
+ break;
+
+ case S_IN_FINISHED_VALUE:
+ nextToken();
+ if(token.type==Yytoken.TYPE_EOF){
+ contentHandler.endJSON();
+ status = S_END;
+ return;
+ }
+ else{
+ status = S_IN_ERROR;
+ throw new ParseException(getPosition(), ParseException.ERROR_UNEXPECTED_TOKEN, token);
+ }
+
+ case S_IN_OBJECT:
+ nextToken();
+ switch(token.type){
+ case Yytoken.TYPE_COMMA:
+ break;
+ case Yytoken.TYPE_VALUE:
+ if(token.value instanceof String){
+ String key=(String)token.value;
+ status=S_PASSED_PAIR_KEY;
+ statusStack.addFirst(new Integer(status));
+ if(!contentHandler.startObjectEntry(key))
+ return;
+ }
+ else{
+ status=S_IN_ERROR;
+ }
+ break;
+ case Yytoken.TYPE_RIGHT_BRACE:
+ if(statusStack.size()>1){
+ statusStack.removeFirst();
+ status=peekStatus(statusStack);
+ }
+ else{
+ status=S_IN_FINISHED_VALUE;
+ }
+ if(!contentHandler.endObject())
+ return;
+ break;
+ default:
+ status=S_IN_ERROR;
+ break;
+ }//inner switch
+ break;
+
+ case S_PASSED_PAIR_KEY:
+ nextToken();
+ switch(token.type){
+ case Yytoken.TYPE_COLON:
+ break;
+ case Yytoken.TYPE_VALUE:
+ statusStack.removeFirst();
+ status=peekStatus(statusStack);
+ if(!contentHandler.primitive(token.value))
+ return;
+ if(!contentHandler.endObjectEntry())
+ return;
+ break;
+ case Yytoken.TYPE_LEFT_SQUARE:
+ statusStack.removeFirst();
+ statusStack.addFirst(new Integer(S_IN_PAIR_VALUE));
+ status=S_IN_ARRAY;
+ statusStack.addFirst(new Integer(status));
+ if(!contentHandler.startArray())
+ return;
+ break;
+ case Yytoken.TYPE_LEFT_BRACE:
+ statusStack.removeFirst();
+ statusStack.addFirst(new Integer(S_IN_PAIR_VALUE));
+ status=S_IN_OBJECT;
+ statusStack.addFirst(new Integer(status));
+ if(!contentHandler.startObject())
+ return;
+ break;
+ default:
+ status=S_IN_ERROR;
+ }
+ break;
+
+ case S_IN_PAIR_VALUE:
+ /*
+ * S_IN_PAIR_VALUE is just a marker to indicate the end of an object entry, it doesn't proccess any token,
+ * therefore delay consuming token until next round.
+ */
+ statusStack.removeFirst();
+ status = peekStatus(statusStack);
+ if(!contentHandler.endObjectEntry())
+ return;
+ break;
+
+ case S_IN_ARRAY:
+ nextToken();
+ switch(token.type){
+ case Yytoken.TYPE_COMMA:
+ break;
+ case Yytoken.TYPE_VALUE:
+ if(!contentHandler.primitive(token.value))
+ return;
+ break;
+ case Yytoken.TYPE_RIGHT_SQUARE:
+ if(statusStack.size()>1){
+ statusStack.removeFirst();
+ status=peekStatus(statusStack);
+ }
+ else{
+ status=S_IN_FINISHED_VALUE;
+ }
+ if(!contentHandler.endArray())
+ return;
+ break;
+ case Yytoken.TYPE_LEFT_BRACE:
+ status=S_IN_OBJECT;
+ statusStack.addFirst(new Integer(status));
+ if(!contentHandler.startObject())
+ return;
+ break;
+ case Yytoken.TYPE_LEFT_SQUARE:
+ status=S_IN_ARRAY;
+ statusStack.addFirst(new Integer(status));
+ if(!contentHandler.startArray())
+ return;
+ break;
+ default:
+ status=S_IN_ERROR;
+ }//inner switch
+ break;
+
+ case S_END:
+ return;
+
+ case S_IN_ERROR:
+ throw new ParseException(getPosition(), ParseException.ERROR_UNEXPECTED_TOKEN, token);
+ }//switch
+ if(status==S_IN_ERROR){
+ throw new ParseException(getPosition(), ParseException.ERROR_UNEXPECTED_TOKEN, token);
+ }
+ }while(token.type!=Yytoken.TYPE_EOF);
+ }
+ catch(IOException ie){
+ status = S_IN_ERROR;
+ throw ie;
+ }
+ catch(ParseException pe){
+ status = S_IN_ERROR;
+ throw pe;
+ }
+ catch(RuntimeException re){
+ status = S_IN_ERROR;
+ throw re;
+ }
+ catch(Error e){
+ status = S_IN_ERROR;
+ throw e;
+ }
+
+ status = S_IN_ERROR;
+ throw new ParseException(getPosition(), ParseException.ERROR_UNEXPECTED_TOKEN, token);
+ }
+}
diff --git a/mobile/android/thirdparty/org/json/simple/parser/ParseException.java b/mobile/android/thirdparty/org/json/simple/parser/ParseException.java
new file mode 100644
index 0000000000..a5a5407f9d
--- /dev/null
+++ b/mobile/android/thirdparty/org/json/simple/parser/ParseException.java
@@ -0,0 +1,96 @@
+package org.json.simple.parser;
+
+/**
+ * ParseException explains why and where the error occurs in source JSON text.
+ *
+ * @author FangYidong<fangyidong@yahoo.com.cn>
+ *
+ */
+public class ParseException extends Exception {
+ private static final long serialVersionUID = -7880698968187728548L;
+
+ public static final int ERROR_UNEXPECTED_CHAR = 0;
+ public static final int ERROR_UNEXPECTED_TOKEN = 1;
+ public static final int ERROR_UNEXPECTED_EXCEPTION = 2;
+
+ private int errorType;
+ private Object unexpectedObject;
+ private int position;
+
+ public ParseException(int errorType, Throwable throwable) {
+ this(-1, errorType, null, throwable);
+ }
+
+ public ParseException(int errorType, Object unexpectedObject) {
+ this(-1, errorType, unexpectedObject);
+ }
+
+ public ParseException(int position, int errorType, Object unexpectedObject) {
+ this(-1, errorType, unexpectedObject, null);
+ }
+
+ public ParseException(int position, int errorType, Object unexpectedObject, Throwable throwable) {
+ super(throwable);
+ this.position = position;
+ this.errorType = errorType;
+ this.unexpectedObject = unexpectedObject;
+ }
+
+ public int getErrorType() {
+ return errorType;
+ }
+
+ public void setErrorType(int errorType) {
+ this.errorType = errorType;
+ }
+
+ /**
+ * @see org.json.simple.parser.JSONParser#getPosition()
+ *
+ * @return The character position (starting with 0) of the input where the error occurs.
+ */
+ public int getPosition() {
+ return position;
+ }
+
+ public void setPosition(int position) {
+ this.position = position;
+ }
+
+ /**
+ * @see org.json.simple.parser.Yytoken
+ *
+ * @return One of the following base on the value of errorType:
+ * ERROR_UNEXPECTED_CHAR java.lang.Character
+ * ERROR_UNEXPECTED_TOKEN org.json.simple.parser.Yytoken
+ * ERROR_UNEXPECTED_EXCEPTION java.lang.Exception
+ */
+ public Object getUnexpectedObject() {
+ return unexpectedObject;
+ }
+
+ public void setUnexpectedObject(Object unexpectedObject) {
+ this.unexpectedObject = unexpectedObject;
+ }
+
+ @Override
+ public String toString(){
+ StringBuffer sb = new StringBuffer();
+
+ switch(errorType){
+ case ERROR_UNEXPECTED_CHAR:
+ sb.append("Unexpected character (").append(unexpectedObject).append(") at position ").append(position).append(".");
+ break;
+ case ERROR_UNEXPECTED_TOKEN:
+ sb.append("Unexpected token ").append(unexpectedObject).append(" at position ").append(position).append(".");
+ break;
+ case ERROR_UNEXPECTED_EXCEPTION:
+ sb.append("Unexpected exception at position ").append(position).append(": ").append(unexpectedObject);
+ break;
+ default:
+ sb.append("Unkown error at position ").append(position).append(".");
+ break;
+ }
+ return sb.toString();
+ }
+}
diff --git a/mobile/android/thirdparty/org/json/simple/parser/Yylex.java b/mobile/android/thirdparty/org/json/simple/parser/Yylex.java
new file mode 100644
index 0000000000..42ce508eb4
--- /dev/null
+++ b/mobile/android/thirdparty/org/json/simple/parser/Yylex.java
@@ -0,0 +1,688 @@
+/* The following code was generated by JFlex 1.4.2 */
+
+package org.json.simple.parser;
+
+class Yylex {
+
+ /** This character denotes the end of file */
+ public static final int YYEOF = -1;
+
+ /** initial size of the lookahead buffer */
+ private static final int ZZ_BUFFERSIZE = 16384;
+
+ /** lexical states */
+ public static final int YYINITIAL = 0;
+ public static final int STRING_BEGIN = 2;
+
+ /**
+ * ZZ_LEXSTATE[l] is the state in the DFA for the lexical state l
+ * ZZ_LEXSTATE[l+1] is the state in the DFA for the lexical state l
+ * at the beginning of a line
+ * l is of the form l = 2*k, k a non negative integer
+ */
+ private static final int ZZ_LEXSTATE[] = {
+ 0, 0, 1, 1
+ };
+
+ /**
+ * Translates characters to character classes
+ */
+ private static final String ZZ_CMAP_PACKED =
+ "\11\0\1\7\1\7\2\0\1\7\22\0\1\7\1\0\1\11\10\0"+
+ "\1\6\1\31\1\2\1\4\1\12\12\3\1\32\6\0\4\1\1\5"+
+ "\1\1\24\0\1\27\1\10\1\30\3\0\1\22\1\13\2\1\1\21"+
+ "\1\14\5\0\1\23\1\0\1\15\3\0\1\16\1\24\1\17\1\20"+
+ "\5\0\1\25\1\0\1\26\uff82\0";
+
+ /**
+ * Translates characters to character classes
+ */
+ private static final char [] ZZ_CMAP = zzUnpackCMap(ZZ_CMAP_PACKED);
+
+ /**
+ * Translates DFA states to action switch labels.
+ */
+ private static final int [] ZZ_ACTION = zzUnpackAction();
+
+ private static final String ZZ_ACTION_PACKED_0 =
+ "\2\0\2\1\1\2\1\3\1\4\3\1\1\5\1\6"+
+ "\1\7\1\10\1\11\1\12\1\13\1\14\1\15\5\0"+
+ "\1\14\1\16\1\17\1\20\1\21\1\22\1\23\1\24"+
+ "\1\0\1\25\1\0\1\25\4\0\1\26\1\27\2\0"+
+ "\1\30";
+
+ private static int [] zzUnpackAction() {
+ int [] result = new int[45];
+ int offset = 0;
+ offset = zzUnpackAction(ZZ_ACTION_PACKED_0, offset, result);
+ return result;
+ }
+
+ private static int zzUnpackAction(String packed, int offset, int [] result) {
+ int i = 0; /* index in packed string */
+ int j = offset; /* index in unpacked array */
+ int l = packed.length();
+ while (i < l) {
+ int count = packed.charAt(i++);
+ int value = packed.charAt(i++);
+ do result[j++] = value; while (--count > 0);
+ }
+ return j;
+ }
+
+
+ /**
+ * Translates a state to a row index in the transition table
+ */
+ private static final int [] ZZ_ROWMAP = zzUnpackRowMap();
+
+ private static final String ZZ_ROWMAP_PACKED_0 =
+ "\0\0\0\33\0\66\0\121\0\154\0\207\0\66\0\242"+
+ "\0\275\0\330\0\66\0\66\0\66\0\66\0\66\0\66"+
+ "\0\363\0\u010e\0\66\0\u0129\0\u0144\0\u015f\0\u017a\0\u0195"+
+ "\0\66\0\66\0\66\0\66\0\66\0\66\0\66\0\66"+
+ "\0\u01b0\0\u01cb\0\u01e6\0\u01e6\0\u0201\0\u021c\0\u0237\0\u0252"+
+ "\0\66\0\66\0\u026d\0\u0288\0\66";
+
+ private static int [] zzUnpackRowMap() {
+ int [] result = new int[45];
+ int offset = 0;
+ offset = zzUnpackRowMap(ZZ_ROWMAP_PACKED_0, offset, result);
+ return result;
+ }
+
+ private static int zzUnpackRowMap(String packed, int offset, int [] result) {
+ int i = 0; /* index in packed string */
+ int j = offset; /* index in unpacked array */
+ int l = packed.length();
+ while (i < l) {
+ int high = packed.charAt(i++) << 16;
+ result[j++] = high | packed.charAt(i++);
+ }
+ return j;
+ }
+
+ /**
+ * The transition table of the DFA
+ */
+ private static final int ZZ_TRANS [] = {
+ 2, 2, 3, 4, 2, 2, 2, 5, 2, 6,
+ 2, 2, 7, 8, 2, 9, 2, 2, 2, 2,
+ 2, 10, 11, 12, 13, 14, 15, 16, 16, 16,
+ 16, 16, 16, 16, 16, 17, 18, 16, 16, 16,
+ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+ 16, 16, 16, 16, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, 4, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, 4, 19, 20, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, 20, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, 5, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ 21, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, 22, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ 23, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, 16, 16, 16, 16, 16, 16, 16,
+ 16, -1, -1, 16, 16, 16, 16, 16, 16, 16,
+ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+ -1, -1, -1, -1, -1, -1, -1, -1, 24, 25,
+ 26, 27, 28, 29, 30, 31, 32, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ 33, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, 34, 35, -1, -1,
+ 34, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ 36, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, 37, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, 38, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, 39, -1, 39, -1, 39, -1, -1,
+ -1, -1, -1, 39, 39, -1, -1, -1, -1, 39,
+ 39, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, 33, -1, 20, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, 20, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, 35,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, 38, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, 40,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, 41, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, 42, -1, 42, -1, 42,
+ -1, -1, -1, -1, -1, 42, 42, -1, -1, -1,
+ -1, 42, 42, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, 43, -1, 43, -1, 43, -1, -1, -1,
+ -1, -1, 43, 43, -1, -1, -1, -1, 43, 43,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, 44,
+ -1, 44, -1, 44, -1, -1, -1, -1, -1, 44,
+ 44, -1, -1, -1, -1, 44, 44, -1, -1, -1,
+ -1, -1, -1, -1, -1,
+ };
+
+ /* error codes */
+ private static final int ZZ_UNKNOWN_ERROR = 0;
+ private static final int ZZ_NO_MATCH = 1;
+ private static final int ZZ_PUSHBACK_2BIG = 2;
+
+ /* error messages for the codes above */
+ private static final String ZZ_ERROR_MSG[] = {
+ "Unkown internal scanner error",
+ "Error: could not match input",
+ "Error: pushback value was too large"
+ };
+
+ /**
+ * ZZ_ATTRIBUTE[aState] contains the attributes of state <code>aState</code>
+ */
+ private static final int [] ZZ_ATTRIBUTE = zzUnpackAttribute();
+
+ private static final String ZZ_ATTRIBUTE_PACKED_0 =
+ "\2\0\1\11\3\1\1\11\3\1\6\11\2\1\1\11"+
+ "\5\0\10\11\1\0\1\1\1\0\1\1\4\0\2\11"+
+ "\2\0\1\11";
+
+ private static int [] zzUnpackAttribute() {
+ int [] result = new int[45];
+ int offset = 0;
+ offset = zzUnpackAttribute(ZZ_ATTRIBUTE_PACKED_0, offset, result);
+ return result;
+ }
+
+ private static int zzUnpackAttribute(String packed, int offset, int [] result) {
+ int i = 0; /* index in packed string */
+ int j = offset; /* index in unpacked array */
+ int l = packed.length();
+ while (i < l) {
+ int count = packed.charAt(i++);
+ int value = packed.charAt(i++);
+ do result[j++] = value; while (--count > 0);
+ }
+ return j;
+ }
+
+ /** the input device */
+ private java.io.Reader zzReader;
+
+ /** the current state of the DFA */
+ private int zzState;
+
+ /** the current lexical state */
+ private int zzLexicalState = YYINITIAL;
+
+ /** this buffer contains the current text to be matched and is
+ the source of the yytext() string */
+ private char zzBuffer[] = new char[ZZ_BUFFERSIZE];
+
+ /** the textposition at the last accepting state */
+ private int zzMarkedPos;
+
+ /** the current text position in the buffer */
+ private int zzCurrentPos;
+
+ /** startRead marks the beginning of the yytext() string in the buffer */
+ private int zzStartRead;
+
+ /** endRead marks the last character in the buffer, that has been read
+ from input */
+ private int zzEndRead;
+
+ /** number of newlines encountered up to the start of the matched text */
+ private int yyline;
+
+ /** the number of characters up to the start of the matched text */
+ private int yychar;
+
+ /**
+ * the number of characters from the last newline up to the start of the
+ * matched text
+ */
+ private int yycolumn;
+
+ /**
+ * zzAtBOL == true <=> the scanner is currently at the beginning of a line
+ */
+ private boolean zzAtBOL = true;
+
+ /** zzAtEOF == true <=> the scanner is at the EOF */
+ private boolean zzAtEOF;
+
+ /* user code: */
+private StringBuffer sb=new StringBuffer();
+
+int getPosition(){
+ return yychar;
+}
+
+
+
+ /**
+ * Creates a new scanner
+ * There is also a java.io.InputStream version of this constructor.
+ *
+ * @param in the java.io.Reader to read input from.
+ */
+ Yylex(java.io.Reader in) {
+ this.zzReader = in;
+ }
+
+ /**
+ * Creates a new scanner.
+ * There is also java.io.Reader version of this constructor.
+ *
+ * @param in the java.io.Inputstream to read input from.
+ */
+ Yylex(java.io.InputStream in) {
+ this(new java.io.InputStreamReader(in));
+ }
+
+ /**
+ * Unpacks the compressed character translation table.
+ *
+ * @param packed the packed character translation table
+ * @return the unpacked character translation table
+ */
+ private static char [] zzUnpackCMap(String packed) {
+ char [] map = new char[0x10000];
+ int i = 0; /* index in packed string */
+ int j = 0; /* index in unpacked array */
+ while (i < 90) {
+ int count = packed.charAt(i++);
+ char value = packed.charAt(i++);
+ do map[j++] = value; while (--count > 0);
+ }
+ return map;
+ }
+
+
+ /**
+ * Refills the input buffer.
+ *
+ * @return <code>false</code>, iff there was new input.
+ *
+ * @exception java.io.IOException if any I/O-Error occurs
+ */
+ private boolean zzRefill() throws java.io.IOException {
+
+ /* first: make room (if you can) */
+ if (zzStartRead > 0) {
+ System.arraycopy(zzBuffer, zzStartRead,
+ zzBuffer, 0,
+ zzEndRead-zzStartRead);
+
+ /* translate stored positions */
+ zzEndRead-= zzStartRead;
+ zzCurrentPos-= zzStartRead;
+ zzMarkedPos-= zzStartRead;
+ zzStartRead = 0;
+ }
+
+ /* is the buffer big enough? */
+ if (zzCurrentPos >= zzBuffer.length) {
+ /* if not: blow it up */
+ char newBuffer[] = new char[zzCurrentPos*2];
+ System.arraycopy(zzBuffer, 0, newBuffer, 0, zzBuffer.length);
+ zzBuffer = newBuffer;
+ }
+
+ /* finally: fill the buffer with new input */
+ int numRead = zzReader.read(zzBuffer, zzEndRead,
+ zzBuffer.length-zzEndRead);
+
+ if (numRead > 0) {
+ zzEndRead+= numRead;
+ return false;
+ }
+ // unlikely but not impossible: read 0 characters, but not at end of stream
+ if (numRead == 0) {
+ int c = zzReader.read();
+ if (c == -1) {
+ return true;
+ } else {
+ zzBuffer[zzEndRead++] = (char) c;
+ return false;
+ }
+ }
+
+ // numRead < 0
+ return true;
+ }
+
+
+ /**
+ * Closes the input stream.
+ */
+ public final void yyclose() throws java.io.IOException {
+ zzAtEOF = true; /* indicate end of file */
+ zzEndRead = zzStartRead; /* invalidate buffer */
+
+ if (zzReader != null)
+ zzReader.close();
+ }
+
+
+ /**
+ * Resets the scanner to read from a new input stream.
+ * Does not close the old reader.
+ *
+ * All internal variables are reset, the old input stream
+ * <b>cannot</b> be reused (internal buffer is discarded and lost).
+ * Lexical state is set to <tt>ZZ_INITIAL</tt>.
+ *
+ * @param reader the new input stream
+ */
+ public final void yyreset(java.io.Reader reader) {
+ zzReader = reader;
+ zzAtBOL = true;
+ zzAtEOF = false;
+ zzEndRead = zzStartRead = 0;
+ zzCurrentPos = zzMarkedPos = 0;
+ yyline = yychar = yycolumn = 0;
+ zzLexicalState = YYINITIAL;
+ }
+
+
+ /**
+ * Returns the current lexical state.
+ */
+ public final int yystate() {
+ return zzLexicalState;
+ }
+
+
+ /**
+ * Enters a new lexical state
+ *
+ * @param newState the new lexical state
+ */
+ public final void yybegin(int newState) {
+ zzLexicalState = newState;
+ }
+
+
+ /**
+ * Returns the text matched by the current regular expression.
+ */
+ public final String yytext() {
+ return new String( zzBuffer, zzStartRead, zzMarkedPos-zzStartRead );
+ }
+
+
+ /**
+ * Returns the character at position <tt>pos</tt> from the
+ * matched text.
+ *
+ * It is equivalent to yytext().charAt(pos), but faster
+ *
+ * @param pos the position of the character to fetch.
+ * A value from 0 to yylength()-1.
+ *
+ * @return the character at position pos
+ */
+ public final char yycharat(int pos) {
+ return zzBuffer[zzStartRead+pos];
+ }
+
+
+ /**
+ * Returns the length of the matched text region.
+ */
+ public final int yylength() {
+ return zzMarkedPos-zzStartRead;
+ }
+
+
+ /**
+ * Reports an error that occured while scanning.
+ *
+ * In a wellformed scanner (no or only correct usage of
+ * yypushback(int) and a match-all fallback rule) this method
+ * will only be called with things that "Can't Possibly Happen".
+ * If this method is called, something is seriously wrong
+ * (e.g. a JFlex bug producing a faulty scanner etc.).
+ *
+ * Usual syntax/scanner level error handling should be done
+ * in error fallback rules.
+ *
+ * @param errorCode the code of the errormessage to display
+ */
+ private void zzScanError(int errorCode) {
+ String message;
+ try {
+ message = ZZ_ERROR_MSG[errorCode];
+ }
+ catch (ArrayIndexOutOfBoundsException e) {
+ message = ZZ_ERROR_MSG[ZZ_UNKNOWN_ERROR];
+ }
+
+ throw new Error(message);
+ }
+
+
+ /**
+ * Pushes the specified amount of characters back into the input stream.
+ *
+ * They will be read again by then next call of the scanning method
+ *
+ * @param number the number of characters to be read again.
+ * This number must not be greater than yylength()!
+ */
+ public void yypushback(int number) {
+ if ( number > yylength() )
+ zzScanError(ZZ_PUSHBACK_2BIG);
+
+ zzMarkedPos -= number;
+ }
+
+
+ /**
+ * Resumes scanning until the next regular expression is matched,
+ * the end of input is encountered or an I/O-Error occurs.
+ *
+ * @return the next token
+ * @exception java.io.IOException if any I/O-Error occurs
+ */
+ public Yytoken yylex() throws java.io.IOException, ParseException {
+ int zzInput;
+ int zzAction;
+
+ // cached fields:
+ int zzCurrentPosL;
+ int zzMarkedPosL;
+ int zzEndReadL = zzEndRead;
+ char [] zzBufferL = zzBuffer;
+ char [] zzCMapL = ZZ_CMAP;
+
+ int [] zzTransL = ZZ_TRANS;
+ int [] zzRowMapL = ZZ_ROWMAP;
+ int [] zzAttrL = ZZ_ATTRIBUTE;
+
+ while (true) {
+ zzMarkedPosL = zzMarkedPos;
+
+ yychar+= zzMarkedPosL-zzStartRead;
+
+ zzAction = -1;
+
+ zzCurrentPosL = zzCurrentPos = zzStartRead = zzMarkedPosL;
+
+ zzState = ZZ_LEXSTATE[zzLexicalState];
+
+
+ zzForAction: {
+ while (true) {
+
+ if (zzCurrentPosL < zzEndReadL)
+ zzInput = zzBufferL[zzCurrentPosL++];
+ else if (zzAtEOF) {
+ zzInput = YYEOF;
+ break zzForAction;
+ }
+ else {
+ // store back cached positions
+ zzCurrentPos = zzCurrentPosL;
+ zzMarkedPos = zzMarkedPosL;
+ boolean eof = zzRefill();
+ // get translated positions and possibly new buffer
+ zzCurrentPosL = zzCurrentPos;
+ zzMarkedPosL = zzMarkedPos;
+ zzBufferL = zzBuffer;
+ zzEndReadL = zzEndRead;
+ if (eof) {
+ zzInput = YYEOF;
+ break zzForAction;
+ }
+ else {
+ zzInput = zzBufferL[zzCurrentPosL++];
+ }
+ }
+ int zzNext = zzTransL[ zzRowMapL[zzState] + zzCMapL[zzInput] ];
+ if (zzNext == -1) break zzForAction;
+ zzState = zzNext;
+
+ int zzAttributes = zzAttrL[zzState];
+ if ( (zzAttributes & 1) == 1 ) {
+ zzAction = zzState;
+ zzMarkedPosL = zzCurrentPosL;
+ if ( (zzAttributes & 8) == 8 ) break zzForAction;
+ }
+
+ }
+ }
+
+ // store back cached position
+ zzMarkedPos = zzMarkedPosL;
+
+ switch (zzAction < 0 ? zzAction : ZZ_ACTION[zzAction]) {
+ case 11:
+ { sb.append(yytext());
+ }
+ case 25: break;
+ case 4:
+ { sb.delete(0, sb.length());yybegin(STRING_BEGIN);
+ }
+ case 26: break;
+ case 16:
+ { sb.append('\b');
+ }
+ case 27: break;
+ case 6:
+ { return new Yytoken(Yytoken.TYPE_RIGHT_BRACE,null);
+ }
+ case 28: break;
+ case 23:
+ { Boolean val=Boolean.valueOf(yytext()); return new Yytoken(Yytoken.TYPE_VALUE, val);
+ }
+ case 29: break;
+ case 22:
+ { return new Yytoken(Yytoken.TYPE_VALUE, null);
+ }
+ case 30: break;
+ case 13:
+ { yybegin(YYINITIAL);return new Yytoken(Yytoken.TYPE_VALUE, sb.toString());
+ }
+ case 31: break;
+ case 12:
+ { sb.append('\\');
+ }
+ case 32: break;
+ case 21:
+ { Double val=Double.valueOf(yytext()); return new Yytoken(Yytoken.TYPE_VALUE, val);
+ }
+ case 33: break;
+ case 1:
+ { throw new ParseException(yychar, ParseException.ERROR_UNEXPECTED_CHAR, new Character(yycharat(0)));
+ }
+ case 34: break;
+ case 8:
+ { return new Yytoken(Yytoken.TYPE_RIGHT_SQUARE,null);
+ }
+ case 35: break;
+ case 19:
+ { sb.append('\r');
+ }
+ case 36: break;
+ case 15:
+ { sb.append('/');
+ }
+ case 37: break;
+ case 10:
+ { return new Yytoken(Yytoken.TYPE_COLON,null);
+ }
+ case 38: break;
+ case 14:
+ { sb.append('"');
+ }
+ case 39: break;
+ case 5:
+ { return new Yytoken(Yytoken.TYPE_LEFT_BRACE,null);
+ }
+ case 40: break;
+ case 17:
+ { sb.append('\f');
+ }
+ case 41: break;
+ case 24:
+ { try{
+ int ch=Integer.parseInt(yytext().substring(2),16);
+ sb.append((char)ch);
+ }
+ catch(Exception e){
+ throw new ParseException(yychar, ParseException.ERROR_UNEXPECTED_EXCEPTION, e);
+ }
+ }
+ case 42: break;
+ case 20:
+ { sb.append('\t');
+ }
+ case 43: break;
+ case 7:
+ { return new Yytoken(Yytoken.TYPE_LEFT_SQUARE,null);
+ }
+ case 44: break;
+ case 2:
+ { Long val=Long.valueOf(yytext()); return new Yytoken(Yytoken.TYPE_VALUE, val);
+ }
+ case 45: break;
+ case 18:
+ { sb.append('\n');
+ }
+ case 46: break;
+ case 9:
+ { return new Yytoken(Yytoken.TYPE_COMMA,null);
+ }
+ case 47: break;
+ case 3:
+ {
+ }
+ case 48: break;
+ default:
+ if (zzInput == YYEOF && zzStartRead == zzCurrentPos) {
+ zzAtEOF = true;
+ return null;
+ }
+ else {
+ zzScanError(ZZ_NO_MATCH);
+ }
+ }
+ }
+ }
+
+
+}
diff --git a/mobile/android/thirdparty/org/json/simple/parser/Yytoken.java b/mobile/android/thirdparty/org/json/simple/parser/Yytoken.java
new file mode 100644
index 0000000000..ff14e27c1b
--- /dev/null
+++ b/mobile/android/thirdparty/org/json/simple/parser/Yytoken.java
@@ -0,0 +1,58 @@
+/*
+ * $Id: Yytoken.java,v 1.1 2006/04/15 14:10:48 platform Exp $
+ * Created on 2006-4-15
+ */
+package org.json.simple.parser;
+
+/**
+ * @author FangYidong<fangyidong@yahoo.com.cn>
+ */
+public class Yytoken {
+ public static final int TYPE_VALUE=0;//JSON primitive value: string,number,boolean,null
+ public static final int TYPE_LEFT_BRACE=1;
+ public static final int TYPE_RIGHT_BRACE=2;
+ public static final int TYPE_LEFT_SQUARE=3;
+ public static final int TYPE_RIGHT_SQUARE=4;
+ public static final int TYPE_COMMA=5;
+ public static final int TYPE_COLON=6;
+ public static final int TYPE_EOF=-1;//end of file
+
+ public int type=0;
+ public Object value=null;
+
+ public Yytoken(int type,Object value){
+ this.type=type;
+ this.value=value;
+ }
+
+ public String toString(){
+ StringBuffer sb = new StringBuffer();
+ switch(type){
+ case TYPE_VALUE:
+ sb.append("VALUE(").append(value).append(")");
+ break;
+ case TYPE_LEFT_BRACE:
+ sb.append("LEFT BRACE({)");
+ break;
+ case TYPE_RIGHT_BRACE:
+ sb.append("RIGHT BRACE(})");
+ break;
+ case TYPE_LEFT_SQUARE:
+ sb.append("LEFT SQUARE([)");
+ break;
+ case TYPE_RIGHT_SQUARE:
+ sb.append("RIGHT SQUARE(])");
+ break;
+ case TYPE_COMMA:
+ sb.append("COMMA(,)");
+ break;
+ case TYPE_COLON:
+ sb.append("COLON(:)");
+ break;
+ case TYPE_EOF:
+ sb.append("END OF FILE");
+ break;
+ }
+ return sb.toString();
+ }
+}
diff --git a/mobile/android/thirdparty/org/lucasr/dspec/DesignSpec.java b/mobile/android/thirdparty/org/lucasr/dspec/DesignSpec.java
new file mode 100644
index 0000000000..43505be4e2
--- /dev/null
+++ b/mobile/android/thirdparty/org/lucasr/dspec/DesignSpec.java
@@ -0,0 +1,645 @@
+/*
+ * Copyright (C) 2014 Lucas Rocha
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.lucasr.dspec;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Draw a baseline grid, keylines, and spacing markers on top of a {@link View}.
+ *
+ * A {@link DesignSpec} can be configure programmatically as follows:
+ * <ol>
+ * <li>Toggle baseline grid visibility with {@link #setBaselineGridVisible(boolean)}.</li>
+ * <li>Change baseline grid cell width with {@link #setBaselineGridCellSize(float)}.
+ * <li>Change baseline grid color with {@link #setBaselineGridColor(int)}.
+ * <li>Toggle keylines visibility with {@link #setKeylinesVisible(boolean)}.
+ * <li>Change keylines color with {@link #setKeylinesColor(int)}.
+ * <li>Add keylines with {@link #addKeyline(float, From)}.
+ * <li>Toggle spacings visibility with {@link #setSpacingsVisible(boolean)}.
+ * <li>Change spacings color with {@link #setSpacingsColor(int)}.
+ * <li>Add spacing with {@link #addSpacing(float, float, From)}.
+ * </ol>
+ *
+ * You can also define a {@link DesignSpec} via a raw JSON resource as follows:
+ * <pre>
+ * {
+ * "baselineGridVisible": true,
+ * "baselineGridCellSize": 8,
+ * "keylines": [
+ * { "offset": 16,
+ * "from": "LEFT" },
+ * { "offset": 72,
+ * "from": "LEFT" },
+ * { "offset": 16,
+ * "from": "RIGHT" }
+ * ],
+ * "spacings": [
+ * { "offset": 0,
+ * "size": 16,
+ * "from": "LEFT" },
+ * { "offset": 56,
+ * "size": 16,
+ * "from": "LEFT" },
+ * { "offset": 0,
+ * "size": 16,
+ * "from": "RIGHT" }
+ * ]
+ * }
+ * </pre>
+ *
+ * The {@link From} arguments implicitly define the orientation of the given
+ * keyline or spacing i.e. {@link From#LEFT}, {@link From#RIGHT}, {@link From#HORIZONTAL_CENTER}
+ * are implicitly vertical; and {@link From#TOP}, {@link From#BOTTOM}, {@link From#VERTICAL_CENTER}
+ * are implicitly horizontal.
+ *
+ * The {@link From} arguments also define the 'direction' of the offsets and sizes in keylines and
+ * spacings. For example, a keyline using {@link From#RIGHT} will have its offset measured from
+ * right to left of the target {@link View}.
+ *
+ * The easiest way to use a {@link DesignSpec} is by enclosing your target {@link View} with
+ * a {@link DesignSpecFrameLayout} using the {@code designSpec} attribute as follows:
+ * <pre>
+ * <org.lucasr.dspec.DesignSpecFrameLayout
+ * xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:id="@+id/design_spec"
+ * android:layout_width="match_parent"
+ * android:layout_height="match_parent"
+ * app:designSpec="@raw/my_spec">
+ *
+ * ...
+ *
+ * </org.lucasr.dspec.DesignSpecFrameLayout>
+ * </pre>
+ *
+ * Where {@code @raw/my_spec} is a raw JSON resource. Because the {@link DesignSpec} is
+ * defined in an Android resource, you can vary it according to the target form factor using
+ * well-known resource qualifiers making it easy to define different specs for phones and tablets.
+ *
+ * Because {@link DesignSpec} is a {@link Drawable}, you can simply add it to any
+ * {@link android.view.ViewOverlay} if you're running your app on API level >= 18:
+ *
+ * <pre>
+ * DesignSpec designSpec = DesignSpec.fromResource(someView, R.raw.some_spec);
+ * someView.getOverlay().add(designSpec);
+ * </pre>
+ *
+ * @see DesignSpecFrameLayout
+ * @see #fromResource(View, int)
+ */
+public class DesignSpec extends Drawable {
+ private static final boolean DEFAULT_BASELINE_GRID_VISIBLE = false;
+ private static final boolean DEFAULT_KEYLINES_VISIBLE = true;
+ private static final boolean DEFAULT_SPACINGS_VISIBLE = true;
+
+ private static final int DEFAULT_BASELINE_GRID_CELL_SIZE_DIP = 8;
+
+ private static final String DEFAULT_BASELINE_GRID_COLOR = "#44C2185B";
+ private static final String DEFAULT_KEYLINE_COLOR = "#CCC2185B";
+ private static final String DEFAULT_SPACING_COLOR = "#CC89FDFD";
+
+ private static final float KEYLINE_STROKE_WIDTH_DIP = 1.1f;
+
+ private static final String JSON_KEY_BASELINE_GRID_VISIBLE = "baselineGridVisible";
+ private static final String JSON_KEY_BASELINE_GRID_CELL_SIZE = "baselineGridCellSize";
+ private static final String JSON_KEY_BASELINE_GRID_COLOR = "baselineGridColor";
+
+ private static final String JSON_KEY_KEYLINES_VISIBLE = "keylinesVisible";
+ private static final String JSON_KEY_KEYLINES_COLOR = "keylinesColor";
+ private static final String JSON_KEY_KEYLINES = "keylines";
+
+ private static final String JSON_KEY_OFFSET = "offset";
+ private static final String JSON_KEY_SIZE = "size";
+ private static final String JSON_KEY_FROM = "from";
+
+ private static final String JSON_KEY_SPACINGS_VISIBLE = "spacingsVisible";
+ private static final String JSON_KEY_SPACINGS_COLOR = "spacingsColor";
+ private static final String JSON_KEY_SPACINGS = "spacings";
+
+ /**
+ * Defined the reference point from which keyline/spacing offsets and sizes
+ * will be calculated.
+ */
+ public enum From {
+ LEFT,
+ RIGHT,
+ TOP,
+ BOTTOM,
+ VERTICAL_CENTER,
+ HORIZONTAL_CENTER
+ }
+
+ private static class Keyline {
+ public final float position;
+ public final From from;
+
+ public Keyline(float position, From from) {
+ this.position = position;
+ this.from = from;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Keyline)) {
+ return false;
+ }
+
+ if (o == this) {
+ return true;
+ }
+
+ final Keyline other = (Keyline) o;
+ return (this.position == other.position && this.from == other.from);
+ }
+ }
+
+ private static class Spacing {
+ public final float offset;
+ public final float size;
+ public final From from;
+
+ public Spacing(float offset, float size, From from) {
+ this.offset = offset;
+ this.size = size;
+ this.from = from;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Keyline)) {
+ return false;
+ }
+
+ if (o == this) {
+ return true;
+ }
+
+ final Spacing other = (Spacing) o;
+ return (this.offset == other.offset &&
+ this.size == other.size &&
+ this.from == other.from);
+ }
+ }
+
+ private final View mHostView;
+
+ private final float mDensity;
+
+ private boolean mBaselineGridVisible = DEFAULT_BASELINE_GRID_VISIBLE;
+ private float mBaselineGridCellSize;
+ private final Paint mBaselineGridPaint;
+
+ private boolean mKeylinesVisible = DEFAULT_KEYLINES_VISIBLE;
+ private final Paint mKeylinesPaint;
+ private final List<Keyline> mKeylines;
+
+ private boolean mSpacingsVisible = DEFAULT_SPACINGS_VISIBLE;
+ private final Paint mSpacingsPaint;
+ private final List<Spacing> mSpacings;
+
+ public DesignSpec(Resources resources, View hostView) {
+ mHostView = hostView;
+ mDensity = resources.getDisplayMetrics().density;
+
+ mKeylines = new ArrayList<Keyline>();
+ mSpacings = new ArrayList<Spacing>();
+
+ mBaselineGridPaint = new Paint();
+ mBaselineGridPaint.setColor(Color.parseColor(DEFAULT_BASELINE_GRID_COLOR));
+
+ mKeylinesPaint = new Paint();
+ mKeylinesPaint.setStrokeWidth(KEYLINE_STROKE_WIDTH_DIP * mDensity);
+ mKeylinesPaint.setColor(Color.parseColor(DEFAULT_KEYLINE_COLOR));
+
+ mSpacingsPaint = new Paint();
+ mSpacingsPaint.setColor(Color.parseColor(DEFAULT_SPACING_COLOR));
+
+ mBaselineGridCellSize = mDensity * DEFAULT_BASELINE_GRID_CELL_SIZE_DIP;
+ }
+
+ /**
+ * Whether or not the baseline grid should be drawn.
+ */
+ public boolean isBaselineGridVisible() {
+ return mBaselineGridVisible;
+ }
+
+ /**
+ * Sets the baseline grid visibility.
+ */
+ public DesignSpec setBaselineGridVisible(boolean visible) {
+ if (mBaselineGridVisible == visible) {
+ return this;
+ }
+
+ mBaselineGridVisible = visible;
+ invalidateSelf();
+
+ return this;
+ }
+
+ /**
+ * Sets the size of the baseline grid cells. By default, it uses the
+ * material design 8dp cell size.
+ */
+ public DesignSpec setBaselineGridCellSize(float cellSize) {
+ if (mBaselineGridCellSize == cellSize) {
+ return this;
+ }
+
+ mBaselineGridCellSize = cellSize;
+ invalidateSelf();
+
+ return this;
+ }
+
+ /**
+ * Sets the baseline grid color.
+ */
+ public DesignSpec setBaselineGridColor(int color) {
+ if (mBaselineGridPaint.getColor() == color) {
+ return this;
+ }
+
+ mBaselineGridPaint.setColor(color);
+ invalidateSelf();
+
+ return this;
+ }
+
+ /**
+ * Whether or not the keylines should be drawn.
+ */
+ public boolean areKeylinesVisible() {
+ return mKeylinesVisible;
+ }
+
+ /**
+ * Sets the visibility of keylines.
+ */
+ public DesignSpec setKeylinesVisible(boolean visible) {
+ if (mKeylinesVisible == visible) {
+ return this;
+ }
+
+ mKeylinesVisible = visible;
+ invalidateSelf();
+
+ return this;
+ }
+
+ /**
+ * Sets the keyline color.
+ */
+ public DesignSpec setKeylinesColor(int color) {
+ if (mKeylinesPaint.getColor() == color) {
+ return this;
+ }
+
+ mKeylinesPaint.setColor(color);
+ invalidateSelf();
+
+ return this;
+ }
+
+ /**
+ * Adds a keyline to the {@link DesignSpec}.
+ */
+ public DesignSpec addKeyline(float position, From from) {
+ final Keyline keyline = new Keyline(position * mDensity, from);
+ if (mKeylines.contains(keyline)) {
+ return this;
+ }
+
+ mKeylines.add(keyline);
+ return this;
+ }
+
+ /**
+ * Whether or not the spacing markers should be drawn.
+ */
+ public boolean areSpacingsVisible() {
+ return mSpacingsVisible;
+ }
+
+ /**
+ * Sets the visibility of spacing markers.
+ */
+ public DesignSpec setSpacingsVisible(boolean visible) {
+ if (mSpacingsVisible == visible) {
+ return this;
+ }
+
+ mSpacingsVisible = visible;
+ invalidateSelf();
+
+ return this;
+ }
+
+ /**
+ * Sets the spacing mark color.
+ */
+ public DesignSpec setSpacingsColor(int color) {
+ if (mSpacingsPaint.getColor() == color) {
+ return this;
+ }
+
+ mSpacingsPaint.setColor(color);
+ invalidateSelf();
+
+ return this;
+ }
+
+ /**
+ * Adds a spacing mark to the {@link DesignSpec}.
+ */
+ public DesignSpec addSpacing(float position, float size, From from) {
+ final Spacing spacing = new Spacing(position * mDensity, size * mDensity, from);
+ if (mSpacings.contains(spacing)) {
+ return this;
+ }
+
+ mSpacings.add(spacing);
+ return this;
+ }
+
+ private void drawBaselineGrid(Canvas canvas) {
+ if (!mBaselineGridVisible) {
+ return;
+ }
+
+ final int width = getIntrinsicWidth();
+ final int height = getIntrinsicHeight();
+
+ float x = mBaselineGridCellSize;
+ while (x < width) {
+ canvas.drawLine(x, 0, x, height, mBaselineGridPaint);
+ x += mBaselineGridCellSize;
+ }
+
+ float y = mBaselineGridCellSize;
+ while (y < height) {
+ canvas.drawLine(0, y, width, y, mBaselineGridPaint);
+ y += mBaselineGridCellSize;
+ }
+ }
+
+ private void drawKeylines(Canvas canvas) {
+ if (!mKeylinesVisible) {
+ return;
+ }
+
+ final int width = getIntrinsicWidth();
+ final int height = getIntrinsicHeight();
+
+ final int count = mKeylines.size();
+ for (int i = 0; i < count; i++) {
+ final Keyline keyline = mKeylines.get(i);
+
+ final float position;
+ switch (keyline.from) {
+ case LEFT:
+ case TOP:
+ position = keyline.position;
+ break;
+
+ case RIGHT:
+ position = width - keyline.position;
+ break;
+
+ case BOTTOM:
+ position = height - keyline.position;
+ break;
+
+ case VERTICAL_CENTER:
+ position = (height / 2) + keyline.position;
+ break;
+
+ case HORIZONTAL_CENTER:
+ position = (width / 2) + keyline.position;
+ break;
+
+ default:
+ throw new IllegalStateException("Invalid keyline offset");
+ }
+
+ switch (keyline.from) {
+ case LEFT:
+ case RIGHT:
+ case HORIZONTAL_CENTER:
+ canvas.drawLine(position, 0, position, height, mKeylinesPaint);
+ break;
+
+ case TOP:
+ case BOTTOM:
+ case VERTICAL_CENTER:
+ canvas.drawLine(0, position, width, position, mKeylinesPaint);
+ break;
+ }
+ }
+ }
+
+ private void drawSpacings(Canvas canvas) {
+ if (!mSpacingsVisible) {
+ return;
+ }
+
+ final int width = getIntrinsicWidth();
+ final int height = getIntrinsicHeight();
+
+ final int count = mSpacings.size();
+ for (int i = 0; i < count; i++) {
+ final Spacing spacing = mSpacings.get(i);
+
+ final float position1;
+ final float position2;
+ switch (spacing.from) {
+ case LEFT:
+ case TOP:
+ position1 = spacing.offset;
+ position2 = position1 + spacing.size;
+ break;
+
+ case RIGHT:
+ position1 = width - spacing.offset + spacing.size;
+ position2 = width - spacing.offset;
+ break;
+
+ case BOTTOM:
+ position1 = height - spacing.offset + spacing.size;
+ position2 = height - spacing.offset;
+ break;
+
+ case VERTICAL_CENTER:
+ position1 = (height / 2) + spacing.offset;
+ position2 = position1 + spacing.size;
+ break;
+
+ case HORIZONTAL_CENTER:
+ position1 = (width / 2) + spacing.offset;
+ position2 = position1 + spacing.size;
+ break;
+
+ default:
+ throw new IllegalStateException("Invalid spacing offset");
+ }
+
+ switch (spacing.from) {
+ case LEFT:
+ case RIGHT:
+ case HORIZONTAL_CENTER:
+ canvas.drawRect(position1, 0, position2, height, mSpacingsPaint);
+ break;
+
+ case TOP:
+ case BOTTOM:
+ case VERTICAL_CENTER:
+ canvas.drawRect(0, position1, width, position2, mSpacingsPaint);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Draws the {@link DesignSpec}. You should call this in your {@link View}'s
+ * {@link View#onDraw(Canvas)} method if you're not simply enclosing it with a
+ * {@link DesignSpecFrameLayout}.
+ */
+ @Override
+ public void draw(Canvas canvas) {
+ drawSpacings(canvas);
+ drawBaselineGrid(canvas);
+ drawKeylines(canvas);
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mHostView.getWidth();
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mHostView.getHeight();
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ mBaselineGridPaint.setAlpha(alpha);
+ mKeylinesPaint.setAlpha(alpha);
+ mSpacingsPaint.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ mBaselineGridPaint.setColorFilter(cf);
+ mKeylinesPaint.setColorFilter(cf);
+ mSpacingsPaint.setColorFilter(cf);
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ /**
+ * Creates a new {@link DesignSpec} instance from a resource ID using a {@link View}
+ * that will provide the {@link DesignSpec}'s intrinsic dimensions.
+ *
+ * @param view The {@link View} who will own the new {@link DesignSpec} instance.
+ * @param resId The resource ID pointing to a raw JSON resource.
+ * @return The newly created {@link DesignSpec} instance.
+ */
+ public static DesignSpec fromResource(View view, int resId) {
+ final Resources resources = view.getResources();
+ final DesignSpec spec = new DesignSpec(resources, view);
+ if (resId == 0) {
+ return spec;
+ }
+
+ final JSONObject json;
+ try {
+ json = RawResource.getAsJSON(resources, resId);
+ } catch (IOException e) {
+ throw new IllegalStateException("Could not read design spec resource", e);
+ }
+
+ final float density = resources.getDisplayMetrics().density;
+
+ spec.setBaselineGridCellSize(density * json.optInt(JSON_KEY_BASELINE_GRID_CELL_SIZE,
+ DEFAULT_BASELINE_GRID_CELL_SIZE_DIP));
+
+ spec.setBaselineGridVisible(json.optBoolean(JSON_KEY_BASELINE_GRID_VISIBLE,
+ DEFAULT_BASELINE_GRID_VISIBLE));
+ spec.setKeylinesVisible(json.optBoolean(JSON_KEY_KEYLINES_VISIBLE,
+ DEFAULT_KEYLINES_VISIBLE));
+ spec.setSpacingsVisible(json.optBoolean(JSON_KEY_SPACINGS_VISIBLE,
+ DEFAULT_SPACINGS_VISIBLE));
+
+ spec.setBaselineGridColor(Color.parseColor(json.optString(JSON_KEY_BASELINE_GRID_COLOR,
+ DEFAULT_BASELINE_GRID_COLOR)));
+ spec.setKeylinesColor(Color.parseColor(json.optString(JSON_KEY_KEYLINES_COLOR,
+ DEFAULT_KEYLINE_COLOR)));
+ spec.setSpacingsColor(Color.parseColor(json.optString(JSON_KEY_SPACINGS_COLOR,
+ DEFAULT_SPACING_COLOR)));
+
+ final JSONArray keylines = json.optJSONArray(JSON_KEY_KEYLINES);
+ if (keylines != null) {
+ final int keylineCount = keylines.length();
+ for (int i = 0; i < keylineCount; i++) {
+ try {
+ final JSONObject keyline = keylines.getJSONObject(i);
+ spec.addKeyline(keyline.getInt(JSON_KEY_OFFSET),
+ From.valueOf(keyline.getString(JSON_KEY_FROM).toUpperCase()));
+ } catch (JSONException e) {
+ continue;
+ }
+ }
+ }
+
+ final JSONArray spacings = json.optJSONArray(JSON_KEY_SPACINGS);
+ if (spacings != null) {
+ final int spacingCount = spacings.length();
+ for (int i = 0; i < spacingCount; i++) {
+ try {
+ final JSONObject spacing = spacings.getJSONObject(i);
+ spec.addSpacing(spacing.getInt(JSON_KEY_OFFSET), spacing.getInt(JSON_KEY_SIZE),
+ From.valueOf(spacing.getString(JSON_KEY_FROM).toUpperCase()));
+ } catch (JSONException e) {
+ continue;
+ }
+ }
+ }
+
+ return spec;
+ }
+}
diff --git a/mobile/android/thirdparty/org/lucasr/dspec/RawResource.java b/mobile/android/thirdparty/org/lucasr/dspec/RawResource.java
new file mode 100644
index 0000000000..f2e743b111
--- /dev/null
+++ b/mobile/android/thirdparty/org/lucasr/dspec/RawResource.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2014 Lucas Rocha
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.lucasr.dspec;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringWriter;
+
+class RawResource {
+ public static JSONObject getAsJSON(Resources resources, int id) throws IOException {
+ InputStreamReader reader = null;
+
+ try {
+ final InputStream is = resources.openRawResource(id);
+ if (is == null) {
+ return null;
+ }
+
+ reader = new InputStreamReader(is, "UTF-8");
+
+ final char[] buffer = new char[1024];
+ final StringWriter s = new StringWriter();
+
+ int n;
+ while ((n = reader.read(buffer, 0, buffer.length)) != -1) {
+ s.write(buffer, 0, n);
+ }
+
+ return new JSONObject(s.toString());
+ } catch (JSONException e) {
+ throw new IllegalStateException("Invalid design spec JSON resource", e);
+ } finally {
+ if (reader != null) {
+ reader.close();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/BinaryDecoder.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/BinaryDecoder.java
new file mode 100644
index 0000000000..82e77e0099
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/BinaryDecoder.java
@@ -0,0 +1,43 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec;
+
+/**
+ * Defines common decoding methods for byte array decoders.
+ *
+ * @author Apache Software Foundation
+ * @version $Id: BinaryDecoder.java 1075406 2011-02-28 16:18:26Z ggregory $
+ */
+public interface BinaryDecoder extends Decoder {
+
+ /**
+ * Decodes a byte array and returns the results as a byte array.
+ *
+ * @param source A byte array which has been encoded with the
+ * appropriate encoder
+ *
+ * @return a byte array that contains decoded content
+ *
+ * @throws DecoderException A decoder exception is thrown
+ * if a Decoder encounters a failure condition during
+ * the decode process.
+ */
+ byte[] decode(byte[] source) throws DecoderException;
+}
+
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/BinaryEncoder.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/BinaryEncoder.java
new file mode 100644
index 0000000000..5b1be202c4
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/BinaryEncoder.java
@@ -0,0 +1,43 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec;
+
+/**
+ * Defines common encoding methods for byte array encoders.
+ *
+ * @author Apache Software Foundation
+ * @version $Id: BinaryEncoder.java 1075406 2011-02-28 16:18:26Z ggregory $
+ */
+public interface BinaryEncoder extends Encoder {
+
+ /**
+ * Encodes a byte array and return the encoded data
+ * as a byte array.
+ *
+ * @param source Data to be encoded
+ *
+ * @return A byte array containing the encoded data
+ *
+ * @throws EncoderException thrown if the Encoder
+ * encounters a failure condition during the
+ * encoding process.
+ */
+ byte[] encode(byte[] source) throws EncoderException;
+}
+
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/CharEncoding.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/CharEncoding.java
new file mode 100644
index 0000000000..c4227fb0e9
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/CharEncoding.java
@@ -0,0 +1,127 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec;
+
+/**
+ * Character encoding names required of every implementation of the Java platform.
+ *
+ * From the Java documentation <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard
+ * charsets</a>:
+ * <p>
+ * <cite>Every implementation of the Java platform is required to support the following character encodings. Consult the
+ * release documentation for your implementation to see if any other encodings are supported. Consult the release
+ * documentation for your implementation to see if any other encodings are supported. </cite>
+ * </p>
+ *
+ * <ul>
+ * <li><code>US-ASCII</code><br/>
+ * Seven-bit ASCII, a.k.a. ISO646-US, a.k.a. the Basic Latin block of the Unicode character set.</li>
+ * <li><code>ISO-8859-1</code><br/>
+ * ISO Latin Alphabet No. 1, a.k.a. ISO-LATIN-1.</li>
+ * <li><code>UTF-8</code><br/>
+ * Eight-bit Unicode Transformation Format.</li>
+ * <li><code>UTF-16BE</code><br/>
+ * Sixteen-bit Unicode Transformation Format, big-endian byte order.</li>
+ * <li><code>UTF-16LE</code><br/>
+ * Sixteen-bit Unicode Transformation Format, little-endian byte order.</li>
+ * <li><code>UTF-16</code><br/>
+ * Sixteen-bit Unicode Transformation Format, byte order specified by a mandatory initial byte-order mark (either order
+ * accepted on input, big-endian used on output.)</li>
+ * </ul>
+ *
+ * This perhaps would best belong in the [lang] project. Even if a similar interface is defined in [lang], it is not
+ * forseen that [codec] would be made to depend on [lang].
+ *
+ * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+ * @author Apache Software Foundation
+ * @since 1.4
+ * @version $Id: CharEncoding.java 797857 2009-07-25 23:43:33Z ggregory $
+ */
+public class CharEncoding {
+ /**
+ * CharEncodingISO Latin Alphabet No. 1, a.k.a. ISO-LATIN-1. </p>
+ * <p>
+ * Every implementation of the Java platform is required to support this character encoding.
+ * </p>
+ *
+ * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+ */
+ public static final String ISO_8859_1 = "ISO-8859-1";
+
+ /**
+ * <p>
+ * Seven-bit ASCII, also known as ISO646-US, also known as the Basic Latin block of the Unicode character set.
+ * </p>
+ * <p>
+ * Every implementation of the Java platform is required to support this character encoding.
+ * </p>
+ *
+ * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+ */
+ public static final String US_ASCII = "US-ASCII";
+
+ /**
+ * <p>
+ * Sixteen-bit Unicode Transformation Format, The byte order specified by a mandatory initial byte-order mark
+ * (either order accepted on input, big-endian used on output)
+ * </p>
+ * <p>
+ * Every implementation of the Java platform is required to support this character encoding.
+ * </p>
+ *
+ * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+ */
+ public static final String UTF_16 = "UTF-16";
+
+ /**
+ * <p>
+ * Sixteen-bit Unicode Transformation Format, big-endian byte order.
+ * </p>
+ * <p>
+ * Every implementation of the Java platform is required to support this character encoding.
+ * </p>
+ *
+ * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+ */
+ public static final String UTF_16BE = "UTF-16BE";
+
+ /**
+ * <p>
+ * Sixteen-bit Unicode Transformation Format, little-endian byte order.
+ * </p>
+ * <p>
+ * Every implementation of the Java platform is required to support this character encoding.
+ * </p>
+ *
+ * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+ */
+ public static final String UTF_16LE = "UTF-16LE";
+
+ /**
+ * <p>
+ * Eight-bit Unicode Transformation Format.
+ * </p>
+ * <p>
+ * Every implementation of the Java platform is required to support this character encoding.
+ * </p>
+ *
+ * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+ */
+ public static final String UTF_8 = "UTF-8";
+} \ No newline at end of file
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/Decoder.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/Decoder.java
new file mode 100644
index 0000000000..5194feae26
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/Decoder.java
@@ -0,0 +1,56 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec;
+
+/**
+ * <p>Provides the highest level of abstraction for Decoders.
+ * This is the sister interface of {@link Encoder}. All
+ * Decoders implement this common generic interface.</p>
+ *
+ * <p>Allows a user to pass a generic Object to any Decoder
+ * implementation in the codec package.</p>
+ *
+ * <p>One of the two interfaces at the center of the codec package.</p>
+ *
+ * @author Apache Software Foundation
+ * @version $Id: Decoder.java 1075404 2011-02-28 16:17:29Z ggregory $
+ */
+public interface Decoder {
+
+ /**
+ * Decodes an "encoded" Object and returns a "decoded"
+ * Object. Note that the implementation of this
+ * interface will try to cast the Object parameter
+ * to the specific type expected by a particular Decoder
+ * implementation. If a {@link ClassCastException} occurs
+ * this decode method will throw a DecoderException.
+ *
+ * @param source the object to decode
+ *
+ * @return a 'decoded" object
+ *
+ * @throws DecoderException a decoder exception can
+ * be thrown for any number of reasons. Some good
+ * candidates are that the parameter passed to this
+ * method is null, a param cannot be cast to the
+ * appropriate type for a specific encoder.
+ */
+ Object decode(Object source) throws DecoderException;
+}
+
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/DecoderException.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/DecoderException.java
new file mode 100644
index 0000000000..88108a5482
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/DecoderException.java
@@ -0,0 +1,90 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec;
+
+/**
+ * Thrown when there is a failure condition during the decoding process. This exception is thrown when a {@link Decoder}
+ * encounters a decoding specific exception such as invalid data, or characters outside of the expected range.
+ *
+ * @author Apache Software Foundation
+ * @version $Id: DecoderException.java 1080701 2011-03-11 17:52:27Z ggregory $
+ */
+public class DecoderException extends Exception {
+
+ /**
+ * Declares the Serial Version Uid.
+ *
+ * @see <a href="http://c2.com/cgi/wiki?AlwaysDeclareSerialVersionUid">Always Declare Serial Version Uid</a>
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs a new exception with <code>null</code> as its detail message. The cause is not initialized, and may
+ * subsequently be initialized by a call to {@link #initCause}.
+ *
+ * @since 1.4
+ */
+ public DecoderException() {
+ super();
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message. The cause is not initialized, and may subsequently
+ * be initialized by a call to {@link #initCause}.
+ *
+ * @param message
+ * The detail message which is saved for later retrieval by the {@link #getMessage()} method.
+ */
+ public DecoderException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructsa new exception with the specified detail message and cause.
+ *
+ * <p>
+ * Note that the detail message associated with <code>cause</code> is not automatically incorporated into this
+ * exception's detail message.
+ * </p>
+ *
+ * @param message
+ * The detail message which is saved for later retrieval by the {@link #getMessage()} method.
+ * @param cause
+ * The cause which is saved for later retrieval by the {@link #getCause()} method. A <code>null</code>
+ * value is permitted, and indicates that the cause is nonexistent or unknown.
+ * @since 1.4
+ */
+ public DecoderException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Constructs a new exception with the specified cause and a detail message of <code>(cause==null ?
+ * null : cause.toString())</code> (which typically contains the class and detail message of <code>cause</code>).
+ * This constructor is useful for exceptions that are little more than wrappers for other throwables.
+ *
+ * @param cause
+ * The cause which is saved for later retrieval by the {@link #getCause()} method. A <code>null</code>
+ * value is permitted, and indicates that the cause is nonexistent or unknown.
+ * @since 1.4
+ */
+ public DecoderException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/Encoder.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/Encoder.java
new file mode 100644
index 0000000000..1b81af9b20
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/Encoder.java
@@ -0,0 +1,47 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec;
+
+/**
+ * <p>Provides the highest level of abstraction for Encoders.
+ * This is the sister interface of {@link Decoder}. Every implementation of
+ * Encoder provides this common generic interface whic allows a user to pass a
+ * generic Object to any Encoder implementation in the codec package.</p>
+ *
+ * @author Apache Software Foundation
+ * @version $Id: Encoder.java 1075406 2011-02-28 16:18:26Z ggregory $
+ */
+public interface Encoder {
+
+ /**
+ * Encodes an "Object" and returns the encoded content
+ * as an Object. The Objects here may just be <code>byte[]</code>
+ * or <code>String</code>s depending on the implementation used.
+ *
+ * @param source An object ot encode
+ *
+ * @return An "encoded" Object
+ *
+ * @throws EncoderException an encoder exception is
+ * thrown if the encoder experiences a failure
+ * condition during the encoding process.
+ */
+ Object encode(Object source) throws EncoderException;
+}
+
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/EncoderException.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/EncoderException.java
new file mode 100644
index 0000000000..a858abccdd
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/EncoderException.java
@@ -0,0 +1,91 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec;
+
+/**
+ * Thrown when there is a failure condition during the encoding process. This exception is thrown when an
+ * {@link Encoder} encounters a encoding specific exception such as invalid data, inability to calculate a checksum,
+ * characters outside of the expected range.
+ *
+ * @author Apache Software Foundation
+ * @version $Id: EncoderException.java 1080701 2011-03-11 17:52:27Z ggregory $
+ */
+public class EncoderException extends Exception {
+
+ /**
+ * Declares the Serial Version Uid.
+ *
+ * @see <a href="http://c2.com/cgi/wiki?AlwaysDeclareSerialVersionUid">Always Declare Serial Version Uid</a>
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs a new exception with <code>null</code> as its detail message. The cause is not initialized, and may
+ * subsequently be initialized by a call to {@link #initCause}.
+ *
+ * @since 1.4
+ */
+ public EncoderException() {
+ super();
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message. The cause is not initialized, and may subsequently
+ * be initialized by a call to {@link #initCause}.
+ *
+ * @param message
+ * a useful message relating to the encoder specific error.
+ */
+ public EncoderException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message and cause.
+ *
+ * <p>
+ * Note that the detail message associated with <code>cause</code> is not automatically incorporated into this
+ * exception's detail message.
+ * </p>
+ *
+ * @param message
+ * The detail message which is saved for later retrieval by the {@link #getMessage()} method.
+ * @param cause
+ * The cause which is saved for later retrieval by the {@link #getCause()} method. A <code>null</code>
+ * value is permitted, and indicates that the cause is nonexistent or unknown.
+ * @since 1.4
+ */
+ public EncoderException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Constructs a new exception with the specified cause and a detail message of <code>(cause==null ?
+ * null : cause.toString())</code> (which typically contains the class and detail message of <code>cause</code>).
+ * This constructor is useful for exceptions that are little more than wrappers for other throwables.
+ *
+ * @param cause
+ * The cause which is saved for later retrieval by the {@link #getCause()} method. A <code>null</code>
+ * value is permitted, and indicates that the cause is nonexistent or unknown.
+ * @since 1.4
+ */
+ public EncoderException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/StringDecoder.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/StringDecoder.java
new file mode 100644
index 0000000000..a112485b3f
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/StringDecoder.java
@@ -0,0 +1,41 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec;
+
+/**
+ * Defines common decoding methods for String decoders.
+ *
+ * @author Apache Software Foundation
+ * @version $Id: StringDecoder.java 1080701 2011-03-11 17:52:27Z ggregory $
+ */
+public interface StringDecoder extends Decoder {
+
+ /**
+ * Decodes a String and returns a String.
+ *
+ * @param source the String to decode
+ *
+ * @return the encoded String
+ *
+ * @throws DecoderException thrown if there is
+ * an error condition during the Encoding process.
+ */
+ String decode(String source) throws DecoderException;
+}
+
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/StringEncoder.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/StringEncoder.java
new file mode 100644
index 0000000000..2bcc5165d9
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/StringEncoder.java
@@ -0,0 +1,41 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec;
+
+/**
+ * Defines common encoding methods for String encoders.
+ *
+ * @author Apache Software Foundation
+ * @version $Id: StringEncoder.java 1080701 2011-03-11 17:52:27Z ggregory $
+ */
+public interface StringEncoder extends Encoder {
+
+ /**
+ * Encodes a String and returns a String.
+ *
+ * @param source the String to encode
+ *
+ * @return the encoded String
+ *
+ * @throws EncoderException thrown if there is
+ * an error conidition during the Encoding process.
+ */
+ String encode(String source) throws EncoderException;
+}
+
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/StringEncoderComparator.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/StringEncoderComparator.java
new file mode 100644
index 0000000000..8923df571c
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/StringEncoderComparator.java
@@ -0,0 +1,87 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec;
+
+import java.util.Comparator;
+
+/**
+ * Compares Strings using a {@link StringEncoder}. This comparator is used to sort Strings by an encoding scheme such as
+ * Soundex, Metaphone, etc. This class can come in handy if one need to sort Strings by an encoded form of a name such
+ * as Soundex.
+ *
+ * @author Apache Software Foundation
+ * @version $Id: StringEncoderComparator.java 1080701 2011-03-11 17:52:27Z ggregory $
+ */
+@SuppressWarnings("rawtypes")
+public class StringEncoderComparator implements Comparator {
+
+ /**
+ * Internal encoder instance.
+ */
+ private final StringEncoder stringEncoder;
+
+ /**
+ * Constructs a new instance.
+ *
+ * @deprecated Creating an instance without a {@link StringEncoder} leads to a {@link NullPointerException}. Will be
+ * removed in 2.0.
+ */
+ public StringEncoderComparator() {
+ this.stringEncoder = null; // Trying to use this will cause things to break
+ }
+
+ /**
+ * Constructs a new instance with the given algorithm.
+ *
+ * @param stringEncoder
+ * the StringEncoder used for comparisons.
+ */
+ public StringEncoderComparator(StringEncoder stringEncoder) {
+ this.stringEncoder = stringEncoder;
+ }
+
+ /**
+ * Compares two strings based not on the strings themselves, but on an encoding of the two strings using the
+ * StringEncoder this Comparator was created with.
+ *
+ * If an {@link EncoderException} is encountered, return <code>0</code>.
+ *
+ * @param o1
+ * the object to compare
+ * @param o2
+ * the object to compare to
+ * @return the Comparable.compareTo() return code or 0 if an encoding error was caught.
+ * @see Comparable
+ */
+ @SuppressWarnings("unchecked")
+ public int compare(Object o1, Object o2) {
+
+ int compareCode = 0;
+
+ try {
+ Comparable s1 = (Comparable) this.stringEncoder.encode(o1);
+ Comparable s2 = (Comparable) this.stringEncoder.encode(o2);
+ compareCode = s1.compareTo(s2);
+ } catch (EncoderException ee) {
+ compareCode = 0;
+ }
+ return compareCode;
+ }
+
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base32.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base32.java
new file mode 100644
index 0000000000..d0d923e628
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base32.java
@@ -0,0 +1,471 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.binary;
+
+/**
+ * Provides Base32 encoding and decoding as defined by <a href="http://www.ietf.org/rfc/rfc4648.txt">RFC 4648</a>.
+ *
+ * <p>
+ * The class can be parameterized in the following manner with various constructors:
+ * <ul>
+ * <li>Whether to use the "base32hex" variant instead of the default "base32"</li>
+ * <li>Line length: Default 76. Line length that aren't multiples of 8 will still essentially end up being multiples of
+ * 8 in the encoded data.
+ * <li>Line separator: Default is CRLF ("\r\n")</li>
+ * </ul>
+ * </p>
+ * <p>
+ * This class operates directly on byte streams, and not character streams.
+ * </p>
+ * <p>
+ * This class is not thread-safe. Each thread should use its own instance.
+ * </p>
+ *
+ * @see <a href="http://www.ietf.org/rfc/rfc4648.txt">RFC 4648</a>
+ *
+ * @since 1.5
+ * @version $Revision: 1080712 $
+ */
+public class Base32 extends BaseNCodec {
+
+ /**
+ * BASE32 characters are 5 bits in length.
+ * They are formed by taking a block of five octets to form a 40-bit string,
+ * which is converted into eight BASE32 characters.
+ */
+ private static final int BITS_PER_ENCODED_BYTE = 5;
+ private static final int BYTES_PER_ENCODED_BLOCK = 8;
+ private static final int BYTES_PER_UNENCODED_BLOCK = 5;
+
+ /**
+ * Chunk separator per RFC 2045 section 2.1.
+ *
+ * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 2.1</a>
+ */
+ private static final byte[] CHUNK_SEPARATOR = {'\r', '\n'};
+
+ /**
+ * This array is a lookup table that translates Unicode characters drawn from the "Base32 Alphabet" (as specified in
+ * Table 3 of RFC 2045) into their 5-bit positive integer equivalents. Characters that are not in the Base32
+ * alphabet but fall within the bounds of the array are translated to -1.
+ *
+ */
+ private static final byte[] DECODE_TABLE = {
+ // 0 1 2 3 4 5 6 7 8 9 A B C D E F
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10-1f
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 63, // 20-2f
+ -1, -1, 26, 27, 28, 29, 30, 31, -1, -1, -1, -1, -1, -1, -1, -1, // 30-3f 2-7
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 40-4f A-N
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // 50-5a O-Z
+ };
+
+ /**
+ * This array is a lookup table that translates 5-bit positive integer index values into their "Base32 Alphabet"
+ * equivalents as specified in Table 3 of RFC 2045.
+ */
+ private static final byte[] ENCODE_TABLE = {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+ 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+ '2', '3', '4', '5', '6', '7',
+ };
+
+ /**
+ * This array is a lookup table that translates Unicode characters drawn from the "Base32 |Hex Alphabet" (as specified in
+ * Table 3 of RFC 2045) into their 5-bit positive integer equivalents. Characters that are not in the Base32 Hex
+ * alphabet but fall within the bounds of the array are translated to -1.
+ *
+ */
+ private static final byte[] HEX_DECODE_TABLE = {
+ // 0 1 2 3 4 5 6 7 8 9 A B C D E F
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10-1f
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 63, // 20-2f
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, // 30-3f 2-7
+ -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 40-4f A-N
+ 25, 26, 27, 28, 29, 30, 31, 32, // 50-57 O-V
+ };
+
+ /**
+ * This array is a lookup table that translates 5-bit positive integer index values into their "Base32 Hex Alphabet"
+ * equivalents as specified in Table 3 of RFC 2045.
+ */
+ private static final byte[] HEX_ENCODE_TABLE = {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+ 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
+ };
+
+ /** Mask used to extract 5 bits, used when encoding Base32 bytes */
+ private static final int MASK_5BITS = 0x1f;
+
+ // The static final fields above are used for the original static byte[] methods on Base32.
+ // The private member fields below are used with the new streaming approach, which requires
+ // some state be preserved between calls of encode() and decode().
+
+ /**
+ * Place holder for the bytes we're dealing with for our based logic.
+ * Bitwise operations store and extract the encoding or decoding from this variable.
+ */
+ private long bitWorkArea;
+
+ /**
+ * Convenience variable to help us determine when our buffer is going to run out of room and needs resizing.
+ * <code>decodeSize = {@link BYTES_PER_ENCODED_BLOCK} - 1 + lineSeparator.length;</code>
+ */
+ private final int decodeSize;
+
+ /**
+ * Decode table to use.
+ */
+ private final byte[] decodeTable;
+
+ /**
+ * Convenience variable to help us determine when our buffer is going to run out of room and needs resizing.
+ * <code>encodeSize = {@link BYTES_PER_ENCODED_BLOCK} + lineSeparator.length;</code>
+ */
+ private final int encodeSize;
+
+ /**
+ * Encode table to use.
+ */
+ private final byte[] encodeTable;
+
+ /**
+ * Line separator for encoding. Not used when decoding. Only used if lineLength > 0.
+ */
+ private final byte[] lineSeparator;
+
+ /**
+ * Creates a Base32 codec used for decoding and encoding.
+ * <p>
+ * When encoding the line length is 0 (no chunking).
+ * </p>
+ *
+ */
+ public Base32() {
+ this(false);
+ }
+
+ /**
+ * Creates a Base32 codec used for decoding and encoding.
+ * <p>
+ * When encoding the line length is 0 (no chunking).
+ * </p>
+ * @param useHex if <code>true</code> then use Base32 Hex alphabet
+ */
+ public Base32(boolean useHex) {
+ this(0, null, useHex);
+ }
+
+ /**
+ * Creates a Base32 codec used for decoding and encoding.
+ * <p>
+ * When encoding the line length is given in the constructor, the line separator is CRLF.
+ * </p>
+ *
+ * @param lineLength
+ * Each line of encoded data will be at most of the given length (rounded down to nearest multiple of 8).
+ * If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when decoding.
+ */
+ public Base32(int lineLength) {
+ this(lineLength, CHUNK_SEPARATOR);
+ }
+
+ /**
+ * Creates a Base32 codec used for decoding and encoding.
+ * <p>
+ * When encoding the line length and line separator are given in the constructor.
+ * </p>
+ * <p>
+ * Line lengths that aren't multiples of 8 will still essentially end up being multiples of 8 in the encoded data.
+ * </p>
+ *
+ * @param lineLength
+ * Each line of encoded data will be at most of the given length (rounded down to nearest multiple of 8).
+ * If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when decoding.
+ * @param lineSeparator
+ * Each line of encoded data will end with this sequence of bytes.
+ * @throws IllegalArgumentException
+ * The provided lineSeparator included some Base32 characters. That's not going to work!
+ */
+ public Base32(int lineLength, byte[] lineSeparator) {
+ this(lineLength, lineSeparator, false);
+ }
+
+ /**
+ * Creates a Base32 / Base32 Hex codec used for decoding and encoding.
+ * <p>
+ * When encoding the line length and line separator are given in the constructor.
+ * </p>
+ * <p>
+ * Line lengths that aren't multiples of 8 will still essentially end up being multiples of 8 in the encoded data.
+ * </p>
+ *
+ * @param lineLength
+ * Each line of encoded data will be at most of the given length (rounded down to nearest multiple of 8).
+ * If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when decoding.
+ * @param lineSeparator
+ * Each line of encoded data will end with this sequence of bytes.
+ * @param useHex if <code>true</code>, then use Base32 Hex alphabet, otherwise use Base32 alphabet
+ * @throws IllegalArgumentException
+ * The provided lineSeparator included some Base32 characters. That's not going to work!
+ * Or the lineLength > 0 and lineSeparator is null.
+ */
+ public Base32(int lineLength, byte[] lineSeparator, boolean useHex) {
+ super(BYTES_PER_UNENCODED_BLOCK, BYTES_PER_ENCODED_BLOCK,
+ lineLength,
+ lineSeparator == null ? 0 : lineSeparator.length);
+ if (useHex){
+ this.encodeTable = HEX_ENCODE_TABLE;
+ this.decodeTable = HEX_DECODE_TABLE;
+ } else {
+ this.encodeTable = ENCODE_TABLE;
+ this.decodeTable = DECODE_TABLE;
+ }
+ if (lineLength > 0) {
+ if (lineSeparator == null) {
+ throw new IllegalArgumentException("lineLength "+lineLength+" > 0, but lineSeparator is null");
+ }
+ // Must be done after initializing the tables
+ if (containsAlphabetOrPad(lineSeparator)) {
+ String sep = StringUtils.newStringUtf8(lineSeparator);
+ throw new IllegalArgumentException("lineSeparator must not contain Base32 characters: [" + sep + "]");
+ }
+ this.encodeSize = BYTES_PER_ENCODED_BLOCK + lineSeparator.length;
+ this.lineSeparator = new byte[lineSeparator.length];
+ System.arraycopy(lineSeparator, 0, this.lineSeparator, 0, lineSeparator.length);
+ } else {
+ this.encodeSize = BYTES_PER_ENCODED_BLOCK;
+ this.lineSeparator = null;
+ }
+ this.decodeSize = this.encodeSize - 1;
+ }
+
+ /**
+ * <p>
+ * Decodes all of the provided data, starting at inPos, for inAvail bytes. Should be called at least twice: once
+ * with the data to decode, and once with inAvail set to "-1" to alert decoder that EOF has been reached. The "-1"
+ * call is not necessary when decoding, but it doesn't hurt, either.
+ * </p>
+ * <p>
+ * Ignores all non-Base32 characters. This is how chunked (e.g. 76 character) data is handled, since CR and LF are
+ * silently ignored, but has implications for other bytes, too. This method subscribes to the garbage-in,
+ * garbage-out philosophy: it will not check the provided data for validity.
+ * </p>
+ *
+ * @param in
+ * byte[] array of ascii data to Base32 decode.
+ * @param inPos
+ * Position to start reading data from.
+ * @param inAvail
+ * Amount of bytes available from input for encoding.
+ *
+ * Output is written to {@link #buffer} as 8-bit octets, using {@link pos} as the buffer position
+ */
+ void decode(byte[] in, int inPos, int inAvail) { // package protected for access from I/O streams
+ if (eof) {
+ return;
+ }
+ if (inAvail < 0) {
+ eof = true;
+ }
+ for (int i = 0; i < inAvail; i++) {
+ byte b = in[inPos++];
+ if (b == PAD) {
+ // We're done.
+ eof = true;
+ break;
+ } else {
+ ensureBufferSize(decodeSize);
+ if (b >= 0 && b < this.decodeTable.length) {
+ int result = this.decodeTable[b];
+ if (result >= 0) {
+ modulus = (modulus+1) % BYTES_PER_ENCODED_BLOCK;
+ bitWorkArea = (bitWorkArea << BITS_PER_ENCODED_BYTE) + result; // collect decoded bytes
+ if (modulus == 0) { // we can output the 5 bytes
+ buffer[pos++] = (byte) ((bitWorkArea >> 32) & MASK_8BITS);
+ buffer[pos++] = (byte) ((bitWorkArea >> 24) & MASK_8BITS);
+ buffer[pos++] = (byte) ((bitWorkArea >> 16) & MASK_8BITS);
+ buffer[pos++] = (byte) ((bitWorkArea >> 8) & MASK_8BITS);
+ buffer[pos++] = (byte) (bitWorkArea & MASK_8BITS);
+ }
+ }
+ }
+ }
+ }
+
+ // Two forms of EOF as far as Base32 decoder is concerned: actual
+ // EOF (-1) and first time '=' character is encountered in stream.
+ // This approach makes the '=' padding characters completely optional.
+ if (eof && modulus >= 2) { // if modulus < 2, nothing to do
+ ensureBufferSize(decodeSize);
+
+ // we ignore partial bytes, i.e. only multiples of 8 count
+ switch (modulus) {
+ case 2 : // 10 bits, drop 2 and output one byte
+ buffer[pos++] = (byte) ((bitWorkArea >> 2) & MASK_8BITS);
+ break;
+ case 3 : // 15 bits, drop 7 and output 1 byte
+ buffer[pos++] = (byte) ((bitWorkArea >> 7) & MASK_8BITS);
+ break;
+ case 4 : // 20 bits = 2*8 + 4
+ bitWorkArea = bitWorkArea >> 4; // drop 4 bits
+ buffer[pos++] = (byte) ((bitWorkArea >> 8) & MASK_8BITS);
+ buffer[pos++] = (byte) ((bitWorkArea) & MASK_8BITS);
+ break;
+ case 5 : // 25bits = 3*8 + 1
+ bitWorkArea = bitWorkArea >> 1;
+ buffer[pos++] = (byte) ((bitWorkArea >> 16) & MASK_8BITS);
+ buffer[pos++] = (byte) ((bitWorkArea >> 8) & MASK_8BITS);
+ buffer[pos++] = (byte) ((bitWorkArea) & MASK_8BITS);
+ break;
+ case 6 : // 30bits = 3*8 + 6
+ bitWorkArea = bitWorkArea >> 6;
+ buffer[pos++] = (byte) ((bitWorkArea >> 16) & MASK_8BITS);
+ buffer[pos++] = (byte) ((bitWorkArea >> 8) & MASK_8BITS);
+ buffer[pos++] = (byte) ((bitWorkArea) & MASK_8BITS);
+ break;
+ case 7 : // 35 = 4*8 +3
+ bitWorkArea = bitWorkArea >> 3;
+ buffer[pos++] = (byte) ((bitWorkArea >> 24) & MASK_8BITS);
+ buffer[pos++] = (byte) ((bitWorkArea >> 16) & MASK_8BITS);
+ buffer[pos++] = (byte) ((bitWorkArea >> 8) & MASK_8BITS);
+ buffer[pos++] = (byte) ((bitWorkArea) & MASK_8BITS);
+ break;
+ }
+ }
+ }
+
+ /**
+ * <p>
+ * Encodes all of the provided data, starting at inPos, for inAvail bytes. Must be called at least twice: once with
+ * the data to encode, and once with inAvail set to "-1" to alert encoder that EOF has been reached, so flush last
+ * remaining bytes (if not multiple of 5).
+ * </p>
+ *
+ * @param in
+ * byte[] array of binary data to Base32 encode.
+ * @param inPos
+ * Position to start reading data from.
+ * @param inAvail
+ * Amount of bytes available from input for encoding.
+ */
+ void encode(byte[] in, int inPos, int inAvail) { // package protected for access from I/O streams
+ if (eof) {
+ return;
+ }
+ // inAvail < 0 is how we're informed of EOF in the underlying data we're
+ // encoding.
+ if (inAvail < 0) {
+ eof = true;
+ if (0 == modulus && lineLength == 0) {
+ return; // no leftovers to process and not using chunking
+ }
+ ensureBufferSize(encodeSize);
+ int savedPos = pos;
+ switch (modulus) { // % 5
+ case 1 : // Only 1 octet; take top 5 bits then remainder
+ buffer[pos++] = encodeTable[(int)(bitWorkArea >> 3) & MASK_5BITS]; // 8-1*5 = 3
+ buffer[pos++] = encodeTable[(int)(bitWorkArea << 2) & MASK_5BITS]; // 5-3=2
+ buffer[pos++] = PAD;
+ buffer[pos++] = PAD;
+ buffer[pos++] = PAD;
+ buffer[pos++] = PAD;
+ buffer[pos++] = PAD;
+ buffer[pos++] = PAD;
+ break;
+
+ case 2 : // 2 octets = 16 bits to use
+ buffer[pos++] = encodeTable[(int)(bitWorkArea >> 11) & MASK_5BITS]; // 16-1*5 = 11
+ buffer[pos++] = encodeTable[(int)(bitWorkArea >> 6) & MASK_5BITS]; // 16-2*5 = 6
+ buffer[pos++] = encodeTable[(int)(bitWorkArea >> 1) & MASK_5BITS]; // 16-3*5 = 1
+ buffer[pos++] = encodeTable[(int)(bitWorkArea << 4) & MASK_5BITS]; // 5-1 = 4
+ buffer[pos++] = PAD;
+ buffer[pos++] = PAD;
+ buffer[pos++] = PAD;
+ buffer[pos++] = PAD;
+ break;
+ case 3 : // 3 octets = 24 bits to use
+ buffer[pos++] = encodeTable[(int)(bitWorkArea >> 19) & MASK_5BITS]; // 24-1*5 = 19
+ buffer[pos++] = encodeTable[(int)(bitWorkArea >> 14) & MASK_5BITS]; // 24-2*5 = 14
+ buffer[pos++] = encodeTable[(int)(bitWorkArea >> 9) & MASK_5BITS]; // 24-3*5 = 9
+ buffer[pos++] = encodeTable[(int)(bitWorkArea >> 4) & MASK_5BITS]; // 24-4*5 = 4
+ buffer[pos++] = encodeTable[(int)(bitWorkArea << 1) & MASK_5BITS]; // 5-4 = 1
+ buffer[pos++] = PAD;
+ buffer[pos++] = PAD;
+ buffer[pos++] = PAD;
+ break;
+ case 4 : // 4 octets = 32 bits to use
+ buffer[pos++] = encodeTable[(int)(bitWorkArea >> 27) & MASK_5BITS]; // 32-1*5 = 27
+ buffer[pos++] = encodeTable[(int)(bitWorkArea >> 22) & MASK_5BITS]; // 32-2*5 = 22
+ buffer[pos++] = encodeTable[(int)(bitWorkArea >> 17) & MASK_5BITS]; // 32-3*5 = 17
+ buffer[pos++] = encodeTable[(int)(bitWorkArea >> 12) & MASK_5BITS]; // 32-4*5 = 12
+ buffer[pos++] = encodeTable[(int)(bitWorkArea >> 7) & MASK_5BITS]; // 32-5*5 = 7
+ buffer[pos++] = encodeTable[(int)(bitWorkArea >> 2) & MASK_5BITS]; // 32-6*5 = 2
+ buffer[pos++] = encodeTable[(int)(bitWorkArea << 3) & MASK_5BITS]; // 5-2 = 3
+ buffer[pos++] = PAD;
+ break;
+ }
+ currentLinePos += pos - savedPos; // keep track of current line position
+ // if currentPos == 0 we are at the start of a line, so don't add CRLF
+ if (lineLength > 0 && currentLinePos > 0){ // add chunk separator if required
+ System.arraycopy(lineSeparator, 0, buffer, pos, lineSeparator.length);
+ pos += lineSeparator.length;
+ }
+ } else {
+ for (int i = 0; i < inAvail; i++) {
+ ensureBufferSize(encodeSize);
+ modulus = (modulus+1) % BYTES_PER_UNENCODED_BLOCK;
+ int b = in[inPos++];
+ if (b < 0) {
+ b += 256;
+ }
+ bitWorkArea = (bitWorkArea << 8) + b; // BITS_PER_BYTE
+ if (0 == modulus) { // we have enough bytes to create our output
+ buffer[pos++] = encodeTable[(int)(bitWorkArea >> 35) & MASK_5BITS];
+ buffer[pos++] = encodeTable[(int)(bitWorkArea >> 30) & MASK_5BITS];
+ buffer[pos++] = encodeTable[(int)(bitWorkArea >> 25) & MASK_5BITS];
+ buffer[pos++] = encodeTable[(int)(bitWorkArea >> 20) & MASK_5BITS];
+ buffer[pos++] = encodeTable[(int)(bitWorkArea >> 15) & MASK_5BITS];
+ buffer[pos++] = encodeTable[(int)(bitWorkArea >> 10) & MASK_5BITS];
+ buffer[pos++] = encodeTable[(int)(bitWorkArea >> 5) & MASK_5BITS];
+ buffer[pos++] = encodeTable[(int)bitWorkArea & MASK_5BITS];
+ currentLinePos += BYTES_PER_ENCODED_BLOCK;
+ if (lineLength > 0 && lineLength <= currentLinePos) {
+ System.arraycopy(lineSeparator, 0, buffer, pos, lineSeparator.length);
+ pos += lineSeparator.length;
+ currentLinePos = 0;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns whether or not the <code>octet</code> is in the Base32 alphabet.
+ *
+ * @param octet
+ * The value to test
+ * @return <code>true</code> if the value is defined in the the Base32 alphabet <code>false</code> otherwise.
+ */
+ public boolean isInAlphabet(byte octet) {
+ return octet >= 0 && octet < decodeTable.length && decodeTable[octet] != -1;
+ }
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base32InputStream.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base32InputStream.java
new file mode 100644
index 0000000000..acb284f9b7
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base32InputStream.java
@@ -0,0 +1,85 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.binary;
+
+import java.io.InputStream;
+
+/**
+ * Provides Base32 encoding and decoding in a streaming fashion (unlimited size). When encoding the default lineLength
+ * is 76 characters and the default lineEnding is CRLF, but these can be overridden by using the appropriate
+ * constructor.
+ * <p>
+ * The default behaviour of the Base32InputStream is to DECODE, whereas the default behaviour of the Base32OutputStream
+ * is to ENCODE, but this behaviour can be overridden by using a different constructor.
+ * </p>
+ * <p>
+ * Since this class operates directly on byte streams, and not character streams, it is hard-coded to only encode/decode
+ * character encodings which are compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252, UTF-8, etc).
+ * </p>
+ *
+ * @version $Revision: 1063784 $
+ * @see <a href="http://www.ietf.org/rfc/rfc4648.txt">RFC 4648</a>
+ * @since 1.5
+ */
+public class Base32InputStream extends BaseNCodecInputStream {
+
+ /**
+ * Creates a Base32InputStream such that all data read is Base32-decoded from the original provided InputStream.
+ *
+ * @param in
+ * InputStream to wrap.
+ */
+ public Base32InputStream(InputStream in) {
+ this(in, false);
+ }
+
+ /**
+ * Creates a Base32InputStream such that all data read is either Base32-encoded or Base32-decoded from the original
+ * provided InputStream.
+ *
+ * @param in
+ * InputStream to wrap.
+ * @param doEncode
+ * true if we should encode all data read from us, false if we should decode.
+ */
+ public Base32InputStream(InputStream in, boolean doEncode) {
+ super(in, new Base32(false), doEncode);
+ }
+
+ /**
+ * Creates a Base32InputStream such that all data read is either Base32-encoded or Base32-decoded from the original
+ * provided InputStream.
+ *
+ * @param in
+ * InputStream to wrap.
+ * @param doEncode
+ * true if we should encode all data read from us, false if we should decode.
+ * @param lineLength
+ * If doEncode is true, each line of encoded data will contain lineLength characters (rounded down to
+ * nearest multiple of 4). If lineLength <=0, the encoded data is not divided into lines. If doEncode is
+ * false, lineLength is ignored.
+ * @param lineSeparator
+ * If doEncode is true, each line of encoded data will be terminated with this byte sequence (e.g. \r\n).
+ * If lineLength <= 0, the lineSeparator is not used. If doEncode is false lineSeparator is ignored.
+ */
+ public Base32InputStream(InputStream in, boolean doEncode, int lineLength, byte[] lineSeparator) {
+ super(in, new Base32(lineLength, lineSeparator), doEncode);
+ }
+
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base32OutputStream.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base32OutputStream.java
new file mode 100644
index 0000000000..af2f4d7188
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base32OutputStream.java
@@ -0,0 +1,85 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.binary;
+
+import java.io.OutputStream;
+
+/**
+ * Provides Base32 encoding and decoding in a streaming fashion (unlimited size). When encoding the default lineLength
+ * is 76 characters and the default lineEnding is CRLF, but these can be overridden by using the appropriate
+ * constructor.
+ * <p>
+ * The default behaviour of the Base32OutputStream is to ENCODE, whereas the default behaviour of the Base32InputStream
+ * is to DECODE. But this behaviour can be overridden by using a different constructor.
+ * </p>
+ * <p>
+ * Since this class operates directly on byte streams, and not character streams, it is hard-coded to only encode/decode
+ * character encodings which are compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252, UTF-8, etc).
+ * </p>
+ *
+ * @version $Revision: 1064132 $
+ * @see <a href="http://www.ietf.org/rfc/rfc4648.txt">RFC 4648</a>
+ * @since 1.5
+ */
+public class Base32OutputStream extends BaseNCodecOutputStream {
+
+ /**
+ * Creates a Base32OutputStream such that all data written is Base32-encoded to the original provided OutputStream.
+ *
+ * @param out
+ * OutputStream to wrap.
+ */
+ public Base32OutputStream(OutputStream out) {
+ this(out, true);
+ }
+
+ /**
+ * Creates a Base32OutputStream such that all data written is either Base32-encoded or Base32-decoded to the
+ * original provided OutputStream.
+ *
+ * @param out
+ * OutputStream to wrap.
+ * @param doEncode
+ * true if we should encode all data written to us, false if we should decode.
+ */
+ public Base32OutputStream(OutputStream out, boolean doEncode) {
+ super(out, new Base32(false), doEncode);
+ }
+
+ /**
+ * Creates a Base32OutputStream such that all data written is either Base32-encoded or Base32-decoded to the
+ * original provided OutputStream.
+ *
+ * @param out
+ * OutputStream to wrap.
+ * @param doEncode
+ * true if we should encode all data written to us, false if we should decode.
+ * @param lineLength
+ * If doEncode is true, each line of encoded data will contain lineLength characters (rounded down to
+ * nearest multiple of 4). If lineLength <=0, the encoded data is not divided into lines. If doEncode is
+ * false, lineLength is ignored.
+ * @param lineSeparator
+ * If doEncode is true, each line of encoded data will be terminated with this byte sequence (e.g. \r\n).
+ * If lineLength <= 0, the lineSeparator is not used. If doEncode is false lineSeparator is ignored.
+ */
+ public Base32OutputStream(OutputStream out, boolean doEncode, int lineLength, byte[] lineSeparator) {
+ super(out, new Base32(lineLength, lineSeparator), doEncode);
+ }
+
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base64.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base64.java
new file mode 100644
index 0000000000..07ed1a4c41
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base64.java
@@ -0,0 +1,756 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.binary;
+
+import java.math.BigInteger;
+
+/**
+ * Provides Base64 encoding and decoding as defined by <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>.
+ *
+ * <p>
+ * This class implements section <cite>6.8. Base64 Content-Transfer-Encoding</cite> from RFC 2045 <cite>Multipurpose
+ * Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies</cite> by Freed and Borenstein.
+ * </p>
+ * <p>
+ * The class can be parameterized in the following manner with various constructors:
+ * <ul>
+ * <li>URL-safe mode: Default off.</li>
+ * <li>Line length: Default 76. Line length that aren't multiples of 4 will still essentially end up being multiples of
+ * 4 in the encoded data.
+ * <li>Line separator: Default is CRLF ("\r\n")</li>
+ * </ul>
+ * </p>
+ * <p>
+ * Since this class operates directly on byte streams, and not character streams, it is hard-coded to only encode/decode
+ * character encodings which are compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252, UTF-8, etc).
+ * </p>
+ * <p>
+ * This class is not thread-safe. Each thread should use its own instance.
+ * </p>
+ *
+ * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>
+ * @author Apache Software Foundation
+ * @since 1.0
+ * @version $Revision: 1080712 $
+ */
+public class Base64 extends BaseNCodec {
+
+ /**
+ * BASE32 characters are 6 bits in length.
+ * They are formed by taking a block of 3 octets to form a 24-bit string,
+ * which is converted into 4 BASE64 characters.
+ */
+ private static final int BITS_PER_ENCODED_BYTE = 6;
+ private static final int BYTES_PER_UNENCODED_BLOCK = 3;
+ private static final int BYTES_PER_ENCODED_BLOCK = 4;
+
+ /**
+ * Chunk separator per RFC 2045 section 2.1.
+ *
+ * <p>
+ * N.B. The next major release may break compatibility and make this field private.
+ * </p>
+ *
+ * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 2.1</a>
+ */
+ static final byte[] CHUNK_SEPARATOR = {'\r', '\n'};
+
+ /**
+ * This array is a lookup table that translates 6-bit positive integer index values into their "Base64 Alphabet"
+ * equivalents as specified in Table 1 of RFC 2045.
+ *
+ * Thanks to "commons" project in ws.apache.org for this code.
+ * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
+ */
+ private static final byte[] STANDARD_ENCODE_TABLE = {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+ 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+ 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
+ };
+
+ /**
+ * This is a copy of the STANDARD_ENCODE_TABLE above, but with + and /
+ * changed to - and _ to make the encoded Base64 results more URL-SAFE.
+ * This table is only used when the Base64's mode is set to URL-SAFE.
+ */
+ private static final byte[] URL_SAFE_ENCODE_TABLE = {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+ 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+ 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_'
+ };
+
+ /**
+ * This array is a lookup table that translates Unicode characters drawn from the "Base64 Alphabet" (as specified in
+ * Table 1 of RFC 2045) into their 6-bit positive integer equivalents. Characters that are not in the Base64
+ * alphabet but fall within the bounds of the array are translated to -1.
+ *
+ * Note: '+' and '-' both decode to 62. '/' and '_' both decode to 63. This means decoder seamlessly handles both
+ * URL_SAFE and STANDARD base64. (The encoder, on the other hand, needs to know ahead of time what to emit).
+ *
+ * Thanks to "commons" project in ws.apache.org for this code.
+ * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
+ */
+ private static final byte[] DECODE_TABLE = {
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, 52, 53, 54,
+ 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 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, -1, -1, -1, -1, 63, -1, 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, 51
+ };
+
+ /**
+ * Base64 uses 6-bit fields.
+ */
+ /** Mask used to extract 6 bits, used when encoding */
+ private static final int MASK_6BITS = 0x3f;
+
+ // The static final fields above are used for the original static byte[] methods on Base64.
+ // The private member fields below are used with the new streaming approach, which requires
+ // some state be preserved between calls of encode() and decode().
+
+ /**
+ * Encode table to use: either STANDARD or URL_SAFE. Note: the DECODE_TABLE above remains static because it is able
+ * to decode both STANDARD and URL_SAFE streams, but the encodeTable must be a member variable so we can switch
+ * between the two modes.
+ */
+ private final byte[] encodeTable;
+
+ // Only one decode table currently; keep for consistency with Base32 code
+ private final byte[] decodeTable = DECODE_TABLE;
+
+ /**
+ * Line separator for encoding. Not used when decoding. Only used if lineLength > 0.
+ */
+ private final byte[] lineSeparator;
+
+ /**
+ * Convenience variable to help us determine when our buffer is going to run out of room and needs resizing.
+ * <code>decodeSize = 3 + lineSeparator.length;</code>
+ */
+ private final int decodeSize;
+
+ /**
+ * Convenience variable to help us determine when our buffer is going to run out of room and needs resizing.
+ * <code>encodeSize = 4 + lineSeparator.length;</code>
+ */
+ private final int encodeSize;
+
+ /**
+ * Place holder for the bytes we're dealing with for our based logic.
+ * Bitwise operations store and extract the encoding or decoding from this variable.
+ */
+ private int bitWorkArea;
+
+ /**
+ * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode.
+ * <p>
+ * When encoding the line length is 0 (no chunking), and the encoding table is STANDARD_ENCODE_TABLE.
+ * </p>
+ *
+ * <p>
+ * When decoding all variants are supported.
+ * </p>
+ */
+ public Base64() {
+ this(0);
+ }
+
+ /**
+ * Creates a Base64 codec used for decoding (all modes) and encoding in the given URL-safe mode.
+ * <p>
+ * When encoding the line length is 76, the line separator is CRLF, and the encoding table is STANDARD_ENCODE_TABLE.
+ * </p>
+ *
+ * <p>
+ * When decoding all variants are supported.
+ * </p>
+ *
+ * @param urlSafe
+ * if <code>true</code>, URL-safe encoding is used. In most cases this should be set to
+ * <code>false</code>.
+ * @since 1.4
+ */
+ public Base64(boolean urlSafe) {
+ this(MIME_CHUNK_SIZE, CHUNK_SEPARATOR, urlSafe);
+ }
+
+ /**
+ * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode.
+ * <p>
+ * When encoding the line length is given in the constructor, the line separator is CRLF, and the encoding table is
+ * STANDARD_ENCODE_TABLE.
+ * </p>
+ * <p>
+ * Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data.
+ * </p>
+ * <p>
+ * When decoding all variants are supported.
+ * </p>
+ *
+ * @param lineLength
+ * Each line of encoded data will be at most of the given length (rounded down to nearest multiple of 4).
+ * If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when decoding.
+ * @since 1.4
+ */
+ public Base64(int lineLength) {
+ this(lineLength, CHUNK_SEPARATOR);
+ }
+
+ /**
+ * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode.
+ * <p>
+ * When encoding the line length and line separator are given in the constructor, and the encoding table is
+ * STANDARD_ENCODE_TABLE.
+ * </p>
+ * <p>
+ * Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data.
+ * </p>
+ * <p>
+ * When decoding all variants are supported.
+ * </p>
+ *
+ * @param lineLength
+ * Each line of encoded data will be at most of the given length (rounded down to nearest multiple of 4).
+ * If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when decoding.
+ * @param lineSeparator
+ * Each line of encoded data will end with this sequence of bytes.
+ * @throws IllegalArgumentException
+ * Thrown when the provided lineSeparator included some base64 characters.
+ * @since 1.4
+ */
+ public Base64(int lineLength, byte[] lineSeparator) {
+ this(lineLength, lineSeparator, false);
+ }
+
+ /**
+ * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode.
+ * <p>
+ * When encoding the line length and line separator are given in the constructor, and the encoding table is
+ * STANDARD_ENCODE_TABLE.
+ * </p>
+ * <p>
+ * Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data.
+ * </p>
+ * <p>
+ * When decoding all variants are supported.
+ * </p>
+ *
+ * @param lineLength
+ * Each line of encoded data will be at most of the given length (rounded down to nearest multiple of 4).
+ * If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when decoding.
+ * @param lineSeparator
+ * Each line of encoded data will end with this sequence of bytes.
+ * @param urlSafe
+ * Instead of emitting '+' and '/' we emit '-' and '_' respectively. urlSafe is only applied to encode
+ * operations. Decoding seamlessly handles both modes.
+ * @throws IllegalArgumentException
+ * The provided lineSeparator included some base64 characters. That's not going to work!
+ * @since 1.4
+ */
+ public Base64(int lineLength, byte[] lineSeparator, boolean urlSafe) {
+ super(BYTES_PER_UNENCODED_BLOCK, BYTES_PER_ENCODED_BLOCK,
+ lineLength,
+ lineSeparator == null ? 0 : lineSeparator.length);
+ // TODO could be simplified if there is no requirement to reject invalid line sep when length <=0
+ // @see test case Base64Test.testConstructors()
+ if (lineSeparator != null) {
+ if (containsAlphabetOrPad(lineSeparator)) {
+ String sep = StringUtils.newStringUtf8(lineSeparator);
+ throw new IllegalArgumentException("lineSeparator must not contain base64 characters: [" + sep + "]");
+ }
+ if (lineLength > 0){ // null line-sep forces no chunking rather than throwing IAE
+ this.encodeSize = BYTES_PER_ENCODED_BLOCK + lineSeparator.length;
+ this.lineSeparator = new byte[lineSeparator.length];
+ System.arraycopy(lineSeparator, 0, this.lineSeparator, 0, lineSeparator.length);
+ } else {
+ this.encodeSize = BYTES_PER_ENCODED_BLOCK;
+ this.lineSeparator = null;
+ }
+ } else {
+ this.encodeSize = BYTES_PER_ENCODED_BLOCK;
+ this.lineSeparator = null;
+ }
+ this.decodeSize = this.encodeSize - 1;
+ this.encodeTable = urlSafe ? URL_SAFE_ENCODE_TABLE : STANDARD_ENCODE_TABLE;
+ }
+
+ /**
+ * Returns our current encode mode. True if we're URL-SAFE, false otherwise.
+ *
+ * @return true if we're in URL-SAFE mode, false otherwise.
+ * @since 1.4
+ */
+ public boolean isUrlSafe() {
+ return this.encodeTable == URL_SAFE_ENCODE_TABLE;
+ }
+
+ /**
+ * <p>
+ * Encodes all of the provided data, starting at inPos, for inAvail bytes. Must be called at least twice: once with
+ * the data to encode, and once with inAvail set to "-1" to alert encoder that EOF has been reached, so flush last
+ * remaining bytes (if not multiple of 3).
+ * </p>
+ * <p>
+ * Thanks to "commons" project in ws.apache.org for the bitwise operations, and general approach.
+ * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
+ * </p>
+ *
+ * @param in
+ * byte[] array of binary data to base64 encode.
+ * @param inPos
+ * Position to start reading data from.
+ * @param inAvail
+ * Amount of bytes available from input for encoding.
+ */
+ void encode(byte[] in, int inPos, int inAvail) {
+ if (eof) {
+ return;
+ }
+ // inAvail < 0 is how we're informed of EOF in the underlying data we're
+ // encoding.
+ if (inAvail < 0) {
+ eof = true;
+ if (0 == modulus && lineLength == 0) {
+ return; // no leftovers to process and not using chunking
+ }
+ ensureBufferSize(encodeSize);
+ int savedPos = pos;
+ switch (modulus) { // 0-2
+ case 1 : // 8 bits = 6 + 2
+ buffer[pos++] = encodeTable[(bitWorkArea >> 2) & MASK_6BITS]; // top 6 bits
+ buffer[pos++] = encodeTable[(bitWorkArea << 4) & MASK_6BITS]; // remaining 2
+ // URL-SAFE skips the padding to further reduce size.
+ if (encodeTable == STANDARD_ENCODE_TABLE) {
+ buffer[pos++] = PAD;
+ buffer[pos++] = PAD;
+ }
+ break;
+
+ case 2 : // 16 bits = 6 + 6 + 4
+ buffer[pos++] = encodeTable[(bitWorkArea >> 10) & MASK_6BITS];
+ buffer[pos++] = encodeTable[(bitWorkArea >> 4) & MASK_6BITS];
+ buffer[pos++] = encodeTable[(bitWorkArea << 2) & MASK_6BITS];
+ // URL-SAFE skips the padding to further reduce size.
+ if (encodeTable == STANDARD_ENCODE_TABLE) {
+ buffer[pos++] = PAD;
+ }
+ break;
+ }
+ currentLinePos += pos - savedPos; // keep track of current line position
+ // if currentPos == 0 we are at the start of a line, so don't add CRLF
+ if (lineLength > 0 && currentLinePos > 0) {
+ System.arraycopy(lineSeparator, 0, buffer, pos, lineSeparator.length);
+ pos += lineSeparator.length;
+ }
+ } else {
+ for (int i = 0; i < inAvail; i++) {
+ ensureBufferSize(encodeSize);
+ modulus = (modulus+1) % BYTES_PER_UNENCODED_BLOCK;
+ int b = in[inPos++];
+ if (b < 0) {
+ b += 256;
+ }
+ bitWorkArea = (bitWorkArea << 8) + b; // BITS_PER_BYTE
+ if (0 == modulus) { // 3 bytes = 24 bits = 4 * 6 bits to extract
+ buffer[pos++] = encodeTable[(bitWorkArea >> 18) & MASK_6BITS];
+ buffer[pos++] = encodeTable[(bitWorkArea >> 12) & MASK_6BITS];
+ buffer[pos++] = encodeTable[(bitWorkArea >> 6) & MASK_6BITS];
+ buffer[pos++] = encodeTable[bitWorkArea & MASK_6BITS];
+ currentLinePos += BYTES_PER_ENCODED_BLOCK;
+ if (lineLength > 0 && lineLength <= currentLinePos) {
+ System.arraycopy(lineSeparator, 0, buffer, pos, lineSeparator.length);
+ pos += lineSeparator.length;
+ currentLinePos = 0;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * <p>
+ * Decodes all of the provided data, starting at inPos, for inAvail bytes. Should be called at least twice: once
+ * with the data to decode, and once with inAvail set to "-1" to alert decoder that EOF has been reached. The "-1"
+ * call is not necessary when decoding, but it doesn't hurt, either.
+ * </p>
+ * <p>
+ * Ignores all non-base64 characters. This is how chunked (e.g. 76 character) data is handled, since CR and LF are
+ * silently ignored, but has implications for other bytes, too. This method subscribes to the garbage-in,
+ * garbage-out philosophy: it will not check the provided data for validity.
+ * </p>
+ * <p>
+ * Thanks to "commons" project in ws.apache.org for the bitwise operations, and general approach.
+ * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
+ * </p>
+ *
+ * @param in
+ * byte[] array of ascii data to base64 decode.
+ * @param inPos
+ * Position to start reading data from.
+ * @param inAvail
+ * Amount of bytes available from input for encoding.
+ */
+ void decode(byte[] in, int inPos, int inAvail) {
+ if (eof) {
+ return;
+ }
+ if (inAvail < 0) {
+ eof = true;
+ }
+ for (int i = 0; i < inAvail; i++) {
+ ensureBufferSize(decodeSize);
+ byte b = in[inPos++];
+ if (b == PAD) {
+ // We're done.
+ eof = true;
+ break;
+ } else {
+ if (b >= 0 && b < DECODE_TABLE.length) {
+ int result = DECODE_TABLE[b];
+ if (result >= 0) {
+ modulus = (modulus+1) % BYTES_PER_ENCODED_BLOCK;
+ bitWorkArea = (bitWorkArea << BITS_PER_ENCODED_BYTE) + result;
+ if (modulus == 0) {
+ buffer[pos++] = (byte) ((bitWorkArea >> 16) & MASK_8BITS);
+ buffer[pos++] = (byte) ((bitWorkArea >> 8) & MASK_8BITS);
+ buffer[pos++] = (byte) (bitWorkArea & MASK_8BITS);
+ }
+ }
+ }
+ }
+ }
+
+ // Two forms of EOF as far as base64 decoder is concerned: actual
+ // EOF (-1) and first time '=' character is encountered in stream.
+ // This approach makes the '=' padding characters completely optional.
+ if (eof && modulus != 0) {
+ ensureBufferSize(decodeSize);
+
+ // We have some spare bits remaining
+ // Output all whole multiples of 8 bits and ignore the rest
+ switch (modulus) {
+ // case 1: // 6 bits - ignore entirely
+ // break;
+ case 2 : // 12 bits = 8 + 4
+ bitWorkArea = bitWorkArea >> 4; // dump the extra 4 bits
+ buffer[pos++] = (byte) ((bitWorkArea) & MASK_8BITS);
+ break;
+ case 3 : // 18 bits = 8 + 8 + 2
+ bitWorkArea = bitWorkArea >> 2; // dump 2 bits
+ buffer[pos++] = (byte) ((bitWorkArea >> 8) & MASK_8BITS);
+ buffer[pos++] = (byte) ((bitWorkArea) & MASK_8BITS);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Returns whether or not the <code>octet</code> is in the base 64 alphabet.
+ *
+ * @param octet
+ * The value to test
+ * @return <code>true</code> if the value is defined in the the base 64 alphabet, <code>false</code> otherwise.
+ * @since 1.4
+ */
+ public static boolean isBase64(byte octet) {
+ return octet == PAD_DEFAULT || (octet >= 0 && octet < DECODE_TABLE.length && DECODE_TABLE[octet] != -1);
+ }
+
+ /**
+ * Tests a given String to see if it contains only valid characters within the Base64 alphabet. Currently the
+ * method treats whitespace as valid.
+ *
+ * @param base64
+ * String to test
+ * @return <code>true</code> if all characters in the String are valid characters in the Base64 alphabet or if
+ * the String is empty; <code>false</code>, otherwise
+ * @since 1.5
+ */
+ public static boolean isBase64(String base64) {
+ return isBase64(StringUtils.getBytesUtf8(base64));
+ }
+
+ /**
+ * Tests a given byte array to see if it contains only valid characters within the Base64 alphabet. Currently the
+ * method treats whitespace as valid.
+ *
+ * @param arrayOctet
+ * byte array to test
+ * @return <code>true</code> if all bytes are valid characters in the Base64 alphabet or if the byte array is empty;
+ * <code>false</code>, otherwise
+ * @deprecated 1.5 Use {@link #isBase64(byte[])}, will be removed in 2.0.
+ */
+ public static boolean isArrayByteBase64(byte[] arrayOctet) {
+ return isBase64(arrayOctet);
+ }
+
+ /**
+ * Tests a given byte array to see if it contains only valid characters within the Base64 alphabet. Currently the
+ * method treats whitespace as valid.
+ *
+ * @param arrayOctet
+ * byte array to test
+ * @return <code>true</code> if all bytes are valid characters in the Base64 alphabet or if the byte array is empty;
+ * <code>false</code>, otherwise
+ * @since 1.5
+ */
+ public static boolean isBase64(byte[] arrayOctet) {
+ for (int i = 0; i < arrayOctet.length; i++) {
+ if (!isBase64(arrayOctet[i]) && !isWhiteSpace(arrayOctet[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Encodes binary data using the base64 algorithm but does not chunk the output.
+ *
+ * @param binaryData
+ * binary data to encode
+ * @return byte[] containing Base64 characters in their UTF-8 representation.
+ */
+ public static byte[] encodeBase64(byte[] binaryData) {
+ return encodeBase64(binaryData, false);
+ }
+
+ /**
+ * Encodes binary data using the base64 algorithm but does not chunk the output.
+ *
+ * NOTE: We changed the behaviour of this method from multi-line chunking (commons-codec-1.4) to
+ * single-line non-chunking (commons-codec-1.5).
+ *
+ * @param binaryData
+ * binary data to encode
+ * @return String containing Base64 characters.
+ * @since 1.4 (NOTE: 1.4 chunked the output, whereas 1.5 does not).
+ */
+ public static String encodeBase64String(byte[] binaryData) {
+ return StringUtils.newStringUtf8(encodeBase64(binaryData, false));
+ }
+
+ /**
+ * Encodes binary data using a URL-safe variation of the base64 algorithm but does not chunk the output. The
+ * url-safe variation emits - and _ instead of + and / characters.
+ *
+ * @param binaryData
+ * binary data to encode
+ * @return byte[] containing Base64 characters in their UTF-8 representation.
+ * @since 1.4
+ */
+ public static byte[] encodeBase64URLSafe(byte[] binaryData) {
+ return encodeBase64(binaryData, false, true);
+ }
+
+ /**
+ * Encodes binary data using a URL-safe variation of the base64 algorithm but does not chunk the output. The
+ * url-safe variation emits - and _ instead of + and / characters.
+ *
+ * @param binaryData
+ * binary data to encode
+ * @return String containing Base64 characters
+ * @since 1.4
+ */
+ public static String encodeBase64URLSafeString(byte[] binaryData) {
+ return StringUtils.newStringUtf8(encodeBase64(binaryData, false, true));
+ }
+
+ /**
+ * Encodes binary data using the base64 algorithm and chunks the encoded output into 76 character blocks
+ *
+ * @param binaryData
+ * binary data to encode
+ * @return Base64 characters chunked in 76 character blocks
+ */
+ public static byte[] encodeBase64Chunked(byte[] binaryData) {
+ return encodeBase64(binaryData, true);
+ }
+
+ /**
+ * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks.
+ *
+ * @param binaryData
+ * Array containing binary data to encode.
+ * @param isChunked
+ * if <code>true</code> this encoder will chunk the base64 output into 76 character blocks
+ * @return Base64-encoded data.
+ * @throws IllegalArgumentException
+ * Thrown when the input array needs an output array bigger than {@link Integer#MAX_VALUE}
+ */
+ public static byte[] encodeBase64(byte[] binaryData, boolean isChunked) {
+ return encodeBase64(binaryData, isChunked, false);
+ }
+
+ /**
+ * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks.
+ *
+ * @param binaryData
+ * Array containing binary data to encode.
+ * @param isChunked
+ * if <code>true</code> this encoder will chunk the base64 output into 76 character blocks
+ * @param urlSafe
+ * if <code>true</code> this encoder will emit - and _ instead of the usual + and / characters.
+ * @return Base64-encoded data.
+ * @throws IllegalArgumentException
+ * Thrown when the input array needs an output array bigger than {@link Integer#MAX_VALUE}
+ * @since 1.4
+ */
+ public static byte[] encodeBase64(byte[] binaryData, boolean isChunked, boolean urlSafe) {
+ return encodeBase64(binaryData, isChunked, urlSafe, Integer.MAX_VALUE);
+ }
+
+ /**
+ * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks.
+ *
+ * @param binaryData
+ * Array containing binary data to encode.
+ * @param isChunked
+ * if <code>true</code> this encoder will chunk the base64 output into 76 character blocks
+ * @param urlSafe
+ * if <code>true</code> this encoder will emit - and _ instead of the usual + and / characters.
+ * @param maxResultSize
+ * The maximum result size to accept.
+ * @return Base64-encoded data.
+ * @throws IllegalArgumentException
+ * Thrown when the input array needs an output array bigger than maxResultSize
+ * @since 1.4
+ */
+ public static byte[] encodeBase64(byte[] binaryData, boolean isChunked, boolean urlSafe, int maxResultSize) {
+ if (binaryData == null || binaryData.length == 0) {
+ return binaryData;
+ }
+
+ // Create this so can use the super-class method
+ // Also ensures that the same roundings are performed by the ctor and the code
+ Base64 b64 = isChunked ? new Base64(urlSafe) : new Base64(0, CHUNK_SEPARATOR, urlSafe);
+ long len = b64.getEncodedLength(binaryData);
+ if (len > maxResultSize) {
+ throw new IllegalArgumentException("Input array too big, the output array would be bigger (" +
+ len +
+ ") than the specified maximum size of " +
+ maxResultSize);
+ }
+
+ return b64.encode(binaryData);
+ }
+
+ /**
+ * Decodes a Base64 String into octets
+ *
+ * @param base64String
+ * String containing Base64 data
+ * @return Array containing decoded data.
+ * @since 1.4
+ */
+ public static byte[] decodeBase64(String base64String) {
+ return new Base64().decode(base64String);
+ }
+
+ /**
+ * Decodes Base64 data into octets
+ *
+ * @param base64Data
+ * Byte array containing Base64 data
+ * @return Array containing decoded data.
+ */
+ public static byte[] decodeBase64(byte[] base64Data) {
+ return new Base64().decode(base64Data);
+ }
+
+ // Implementation of the Encoder Interface
+
+ // Implementation of integer encoding used for crypto
+ /**
+ * Decodes a byte64-encoded integer according to crypto standards such as W3C's XML-Signature
+ *
+ * @param pArray
+ * a byte array containing base64 character data
+ * @return A BigInteger
+ * @since 1.4
+ */
+ public static BigInteger decodeInteger(byte[] pArray) {
+ return new BigInteger(1, decodeBase64(pArray));
+ }
+
+ /**
+ * Encodes to a byte64-encoded integer according to crypto standards such as W3C's XML-Signature
+ *
+ * @param bigInt
+ * a BigInteger
+ * @return A byte array containing base64 character data
+ * @throws NullPointerException
+ * if null is passed in
+ * @since 1.4
+ */
+ public static byte[] encodeInteger(BigInteger bigInt) {
+ if (bigInt == null) {
+ throw new NullPointerException("encodeInteger called with null parameter");
+ }
+ return encodeBase64(toIntegerBytes(bigInt), false);
+ }
+
+ /**
+ * Returns a byte-array representation of a <code>BigInteger</code> without sign bit.
+ *
+ * @param bigInt
+ * <code>BigInteger</code> to be converted
+ * @return a byte array representation of the BigInteger parameter
+ */
+ static byte[] toIntegerBytes(BigInteger bigInt) {
+ int bitlen = bigInt.bitLength();
+ // round bitlen
+ bitlen = ((bitlen + 7) >> 3) << 3;
+ byte[] bigBytes = bigInt.toByteArray();
+
+ if (((bigInt.bitLength() % 8) != 0) && (((bigInt.bitLength() / 8) + 1) == (bitlen / 8))) {
+ return bigBytes;
+ }
+ // set up params for copying everything but sign bit
+ int startSrc = 0;
+ int len = bigBytes.length;
+
+ // if bigInt is exactly byte-aligned, just skip signbit in copy
+ if ((bigInt.bitLength() % 8) == 0) {
+ startSrc = 1;
+ len--;
+ }
+ int startDst = bitlen / 8 - len; // to pad w/ nulls as per spec
+ byte[] resizedBytes = new byte[bitlen / 8];
+ System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len);
+ return resizedBytes;
+ }
+
+ /**
+ * Returns whether or not the <code>octet</code> is in the Base32 alphabet.
+ *
+ * @param octet
+ * The value to test
+ * @return <code>true</code> if the value is defined in the the Base32 alphabet <code>false</code> otherwise.
+ */
+ protected boolean isInAlphabet(byte octet) {
+ return octet >= 0 && octet < decodeTable.length && decodeTable[octet] != -1;
+ }
+
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base64InputStream.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base64InputStream.java
new file mode 100644
index 0000000000..cf99ceb91d
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base64InputStream.java
@@ -0,0 +1,89 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.binary;
+
+import java.io.InputStream;
+
+/**
+ * Provides Base64 encoding and decoding in a streaming fashion (unlimited size). When encoding the default lineLength
+ * is 76 characters and the default lineEnding is CRLF, but these can be overridden by using the appropriate
+ * constructor.
+ * <p>
+ * The default behaviour of the Base64InputStream is to DECODE, whereas the default behaviour of the Base64OutputStream
+ * is to ENCODE, but this behaviour can be overridden by using a different constructor.
+ * </p>
+ * <p>
+ * This class implements section <cite>6.8. Base64 Content-Transfer-Encoding</cite> from RFC 2045 <cite>Multipurpose
+ * Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies</cite> by Freed and Borenstein.
+ * </p>
+ * <p>
+ * Since this class operates directly on byte streams, and not character streams, it is hard-coded to only encode/decode
+ * character encodings which are compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252, UTF-8, etc).
+ * </p>
+ *
+ * @author Apache Software Foundation
+ * @version $Id: Base64InputStream.java 1064424 2011-01-28 02:02:46Z sebb $
+ * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>
+ * @since 1.4
+ */
+public class Base64InputStream extends BaseNCodecInputStream {
+
+ /**
+ * Creates a Base64InputStream such that all data read is Base64-decoded from the original provided InputStream.
+ *
+ * @param in
+ * InputStream to wrap.
+ */
+ public Base64InputStream(InputStream in) {
+ this(in, false);
+ }
+
+ /**
+ * Creates a Base64InputStream such that all data read is either Base64-encoded or Base64-decoded from the original
+ * provided InputStream.
+ *
+ * @param in
+ * InputStream to wrap.
+ * @param doEncode
+ * true if we should encode all data read from us, false if we should decode.
+ */
+ public Base64InputStream(InputStream in, boolean doEncode) {
+ super(in, new Base64(false), doEncode);
+ }
+
+ /**
+ * Creates a Base64InputStream such that all data read is either Base64-encoded or Base64-decoded from the original
+ * provided InputStream.
+ *
+ * @param in
+ * InputStream to wrap.
+ * @param doEncode
+ * true if we should encode all data read from us, false if we should decode.
+ * @param lineLength
+ * If doEncode is true, each line of encoded data will contain lineLength characters (rounded down to
+ * nearest multiple of 4). If lineLength <=0, the encoded data is not divided into lines. If doEncode is
+ * false, lineLength is ignored.
+ * @param lineSeparator
+ * If doEncode is true, each line of encoded data will be terminated with this byte sequence (e.g. \r\n).
+ * If lineLength <= 0, the lineSeparator is not used. If doEncode is false lineSeparator is ignored.
+ */
+ public Base64InputStream(InputStream in, boolean doEncode, int lineLength, byte[] lineSeparator) {
+ super(in, new Base64(lineLength, lineSeparator), doEncode);
+ }
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base64OutputStream.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base64OutputStream.java
new file mode 100644
index 0000000000..df880c2b95
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Base64OutputStream.java
@@ -0,0 +1,89 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.binary;
+
+import java.io.OutputStream;
+
+/**
+ * Provides Base64 encoding and decoding in a streaming fashion (unlimited size). When encoding the default lineLength
+ * is 76 characters and the default lineEnding is CRLF, but these can be overridden by using the appropriate
+ * constructor.
+ * <p>
+ * The default behaviour of the Base64OutputStream is to ENCODE, whereas the default behaviour of the Base64InputStream
+ * is to DECODE. But this behaviour can be overridden by using a different constructor.
+ * </p>
+ * <p>
+ * This class implements section <cite>6.8. Base64 Content-Transfer-Encoding</cite> from RFC 2045 <cite>Multipurpose
+ * Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies</cite> by Freed and Borenstein.
+ * </p>
+ * <p>
+ * Since this class operates directly on byte streams, and not character streams, it is hard-coded to only encode/decode
+ * character encodings which are compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252, UTF-8, etc).
+ * </p>
+ *
+ * @author Apache Software Foundation
+ * @version $Id: Base64OutputStream.java 1064424 2011-01-28 02:02:46Z sebb $
+ * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>
+ * @since 1.4
+ */
+public class Base64OutputStream extends BaseNCodecOutputStream {
+
+ /**
+ * Creates a Base64OutputStream such that all data written is Base64-encoded to the original provided OutputStream.
+ *
+ * @param out
+ * OutputStream to wrap.
+ */
+ public Base64OutputStream(OutputStream out) {
+ this(out, true);
+ }
+
+ /**
+ * Creates a Base64OutputStream such that all data written is either Base64-encoded or Base64-decoded to the
+ * original provided OutputStream.
+ *
+ * @param out
+ * OutputStream to wrap.
+ * @param doEncode
+ * true if we should encode all data written to us, false if we should decode.
+ */
+ public Base64OutputStream(OutputStream out, boolean doEncode) {
+ super(out,new Base64(false), doEncode);
+ }
+
+ /**
+ * Creates a Base64OutputStream such that all data written is either Base64-encoded or Base64-decoded to the
+ * original provided OutputStream.
+ *
+ * @param out
+ * OutputStream to wrap.
+ * @param doEncode
+ * true if we should encode all data written to us, false if we should decode.
+ * @param lineLength
+ * If doEncode is true, each line of encoded data will contain lineLength characters (rounded down to
+ * nearest multiple of 4). If lineLength <=0, the encoded data is not divided into lines. If doEncode is
+ * false, lineLength is ignored.
+ * @param lineSeparator
+ * If doEncode is true, each line of encoded data will be terminated with this byte sequence (e.g. \r\n).
+ * If lineLength <= 0, the lineSeparator is not used. If doEncode is false lineSeparator is ignored.
+ */
+ public Base64OutputStream(OutputStream out, boolean doEncode, int lineLength, byte[] lineSeparator) {
+ super(out, new Base64(lineLength, lineSeparator), doEncode);
+ }
+} \ No newline at end of file
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/BaseNCodec.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/BaseNCodec.java
new file mode 100644
index 0000000000..c2ed4efef6
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/BaseNCodec.java
@@ -0,0 +1,445 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.binary;
+
+import org.mozilla.apache.commons.codec.BinaryDecoder;
+import org.mozilla.apache.commons.codec.BinaryEncoder;
+import org.mozilla.apache.commons.codec.DecoderException;
+import org.mozilla.apache.commons.codec.EncoderException;
+
+/**
+ * Abstract superclass for Base-N encoders and decoders.
+ *
+ * <p>
+ * This class is not thread-safe.
+ * Each thread should use its own instance.
+ * </p>
+ */
+public abstract class BaseNCodec implements BinaryEncoder, BinaryDecoder {
+
+ /**
+ * MIME chunk size per RFC 2045 section 6.8.
+ *
+ * <p>
+ * The {@value} character limit does not count the trailing CRLF, but counts all other characters, including any
+ * equal signs.
+ * </p>
+ *
+ * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 6.8</a>
+ */
+ public static final int MIME_CHUNK_SIZE = 76;
+
+ /**
+ * PEM chunk size per RFC 1421 section 4.3.2.4.
+ *
+ * <p>
+ * The {@value} character limit does not count the trailing CRLF, but counts all other characters, including any
+ * equal signs.
+ * </p>
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc1421">RFC 1421 section 4.3.2.4</a>
+ */
+ public static final int PEM_CHUNK_SIZE = 64;
+
+ private static final int DEFAULT_BUFFER_RESIZE_FACTOR = 2;
+
+ /**
+ * Defines the default buffer size - currently {@value}
+ * - must be large enough for at least one encoded block+separator
+ */
+ private static final int DEFAULT_BUFFER_SIZE = 8192;
+
+ /** Mask used to extract 8 bits, used in decoding bytes */
+ protected static final int MASK_8BITS = 0xff;
+
+ /**
+ * Byte used to pad output.
+ */
+ protected static final byte PAD_DEFAULT = '='; // Allow static access to default
+
+ protected final byte PAD = PAD_DEFAULT; // instance variable just in case it needs to vary later
+
+ /** Number of bytes in each full block of unencoded data, e.g. 4 for Base64 and 5 for Base32 */
+ private final int unencodedBlockSize;
+
+ /** Number of bytes in each full block of encoded data, e.g. 3 for Base64 and 8 for Base32 */
+ private final int encodedBlockSize;
+
+ /**
+ * Chunksize for encoding. Not used when decoding.
+ * A value of zero or less implies no chunking of the encoded data.
+ * Rounded down to nearest multiple of encodedBlockSize.
+ */
+ protected final int lineLength;
+
+ /**
+ * Size of chunk separator. Not used unless {@link #lineLength} > 0.
+ */
+ private final int chunkSeparatorLength;
+
+ /**
+ * Buffer for streaming.
+ */
+ protected byte[] buffer;
+
+ /**
+ * Position where next character should be written in the buffer.
+ */
+ protected int pos;
+
+ /**
+ * Position where next character should be read from the buffer.
+ */
+ private int readPos;
+
+ /**
+ * Boolean flag to indicate the EOF has been reached. Once EOF has been reached, this object becomes useless,
+ * and must be thrown away.
+ */
+ protected boolean eof;
+
+ /**
+ * Variable tracks how many characters have been written to the current line. Only used when encoding. We use it to
+ * make sure each encoded line never goes beyond lineLength (if lineLength > 0).
+ */
+ protected int currentLinePos;
+
+ /**
+ * Writes to the buffer only occur after every 3/5 reads when encoding, and every 4/8 reads when decoding.
+ * This variable helps track that.
+ */
+ protected int modulus;
+
+ /**
+ * Note <code>lineLength</code> is rounded down to the nearest multiple of {@link #encodedBlockSize}
+ * If <code>chunkSeparatorLength</code> is zero, then chunking is disabled.
+ * @param unencodedBlockSize the size of an unencoded block (e.g. Base64 = 3)
+ * @param encodedBlockSize the size of an encoded block (e.g. Base64 = 4)
+ * @param lineLength if &gt; 0, use chunking with a length <code>lineLength</code>
+ * @param chunkSeparatorLength the chunk separator length, if relevant
+ */
+ protected BaseNCodec(int unencodedBlockSize, int encodedBlockSize, int lineLength, int chunkSeparatorLength){
+ this.unencodedBlockSize = unencodedBlockSize;
+ this.encodedBlockSize = encodedBlockSize;
+ this.lineLength = (lineLength > 0 && chunkSeparatorLength > 0) ? (lineLength / encodedBlockSize) * encodedBlockSize : 0;
+ this.chunkSeparatorLength = chunkSeparatorLength;
+ }
+
+ /**
+ * Returns true if this object has buffered data for reading.
+ *
+ * @return true if there is data still available for reading.
+ */
+ boolean hasData() { // package protected for access from I/O streams
+ return this.buffer != null;
+ }
+
+ /**
+ * Returns the amount of buffered data available for reading.
+ *
+ * @return The amount of buffered data available for reading.
+ */
+ int available() { // package protected for access from I/O streams
+ return buffer != null ? pos - readPos : 0;
+ }
+
+ /**
+ * Get the default buffer size. Can be overridden.
+ *
+ * @return {@link #DEFAULT_BUFFER_SIZE}
+ */
+ protected int getDefaultBufferSize() {
+ return DEFAULT_BUFFER_SIZE;
+ }
+
+ /** Increases our buffer by the {@link #DEFAULT_BUFFER_RESIZE_FACTOR}. */
+ private void resizeBuffer() {
+ if (buffer == null) {
+ buffer = new byte[getDefaultBufferSize()];
+ pos = 0;
+ readPos = 0;
+ } else {
+ byte[] b = new byte[buffer.length * DEFAULT_BUFFER_RESIZE_FACTOR];
+ System.arraycopy(buffer, 0, b, 0, buffer.length);
+ buffer = b;
+ }
+ }
+
+ /**
+ * Ensure that the buffer has room for <code>size</code> bytes
+ *
+ * @param size minimum spare space required
+ */
+ protected void ensureBufferSize(int size){
+ if ((buffer == null) || (buffer.length < pos + size)){
+ resizeBuffer();
+ }
+ }
+
+ /**
+ * Extracts buffered data into the provided byte[] array, starting at position bPos,
+ * up to a maximum of bAvail bytes. Returns how many bytes were actually extracted.
+ *
+ * @param b
+ * byte[] array to extract the buffered data into.
+ * @param bPos
+ * position in byte[] array to start extraction at.
+ * @param bAvail
+ * amount of bytes we're allowed to extract. We may extract fewer (if fewer are available).
+ * @return The number of bytes successfully extracted into the provided byte[] array.
+ */
+ int readResults(byte[] b, int bPos, int bAvail) { // package protected for access from I/O streams
+ if (buffer != null) {
+ int len = Math.min(available(), bAvail);
+ System.arraycopy(buffer, readPos, b, bPos, len);
+ readPos += len;
+ if (readPos >= pos) {
+ buffer = null; // so hasData() will return false, and this method can return -1
+ }
+ return len;
+ }
+ return eof ? -1 : 0;
+ }
+
+ /**
+ * Checks if a byte value is whitespace or not.
+ * Whitespace is taken to mean: space, tab, CR, LF
+ * @param byteToCheck
+ * the byte to check
+ * @return true if byte is whitespace, false otherwise
+ */
+ protected static boolean isWhiteSpace(byte byteToCheck) {
+ switch (byteToCheck) {
+ case ' ' :
+ case '\n' :
+ case '\r' :
+ case '\t' :
+ return true;
+ default :
+ return false;
+ }
+ }
+
+ /**
+ * Resets this object to its initial newly constructed state.
+ */
+ private void reset() {
+ buffer = null;
+ pos = 0;
+ readPos = 0;
+ currentLinePos = 0;
+ modulus = 0;
+ eof = false;
+ }
+
+ /**
+ * Encodes an Object using the Base-N algorithm. This method is provided in order to satisfy the requirements of the
+ * Encoder interface, and will throw an EncoderException if the supplied object is not of type byte[].
+ *
+ * @param pObject
+ * Object to encode
+ * @return An object (of type byte[]) containing the Base-N encoded data which corresponds to the byte[] supplied.
+ * @throws EncoderException
+ * if the parameter supplied is not of type byte[]
+ */
+ public Object encode(Object pObject) throws EncoderException {
+ if (!(pObject instanceof byte[])) {
+ throw new EncoderException("Parameter supplied to Base-N encode is not a byte[]");
+ }
+ return encode((byte[]) pObject);
+ }
+
+ /**
+ * Encodes a byte[] containing binary data, into a String containing characters in the Base-N alphabet.
+ *
+ * @param pArray
+ * a byte array containing binary data
+ * @return A String containing only Base-N character data
+ */
+ public String encodeToString(byte[] pArray) {
+ return StringUtils.newStringUtf8(encode(pArray));
+ }
+
+ /**
+ * Decodes an Object using the Base-N algorithm. This method is provided in order to satisfy the requirements of the
+ * Decoder interface, and will throw a DecoderException if the supplied object is not of type byte[] or String.
+ *
+ * @param pObject
+ * Object to decode
+ * @return An object (of type byte[]) containing the binary data which corresponds to the byte[] or String supplied.
+ * @throws DecoderException
+ * if the parameter supplied is not of type byte[]
+ */
+ public Object decode(Object pObject) throws DecoderException {
+ if (pObject instanceof byte[]) {
+ return decode((byte[]) pObject);
+ } else if (pObject instanceof String) {
+ return decode((String) pObject);
+ } else {
+ throw new DecoderException("Parameter supplied to Base-N decode is not a byte[] or a String");
+ }
+ }
+
+ /**
+ * Decodes a String containing characters in the Base-N alphabet.
+ *
+ * @param pArray
+ * A String containing Base-N character data
+ * @return a byte array containing binary data
+ */
+ public byte[] decode(String pArray) {
+ return decode(StringUtils.getBytesUtf8(pArray));
+ }
+
+ /**
+ * Decodes a byte[] containing characters in the Base-N alphabet.
+ *
+ * @param pArray
+ * A byte array containing Base-N character data
+ * @return a byte array containing binary data
+ */
+ public byte[] decode(byte[] pArray) {
+ reset();
+ if (pArray == null || pArray.length == 0) {
+ return pArray;
+ }
+ decode(pArray, 0, pArray.length);
+ decode(pArray, 0, -1); // Notify decoder of EOF.
+ byte[] result = new byte[pos];
+ readResults(result, 0, result.length);
+ return result;
+ }
+
+ /**
+ * Encodes a byte[] containing binary data, into a byte[] containing characters in the alphabet.
+ *
+ * @param pArray
+ * a byte array containing binary data
+ * @return A byte array containing only the basen alphabetic character data
+ */
+ public byte[] encode(byte[] pArray) {
+ reset();
+ if (pArray == null || pArray.length == 0) {
+ return pArray;
+ }
+ encode(pArray, 0, pArray.length);
+ encode(pArray, 0, -1); // Notify encoder of EOF.
+ byte[] buf = new byte[pos - readPos];
+ readResults(buf, 0, buf.length);
+ return buf;
+ }
+
+ /**
+ * Encodes a byte[] containing binary data, into a String containing characters in the appropriate alphabet.
+ * Uses UTF8 encoding.
+ *
+ * @param pArray a byte array containing binary data
+ * @return String containing only character data in the appropriate alphabet.
+ */
+ public String encodeAsString(byte[] pArray){
+ return StringUtils.newStringUtf8(encode(pArray));
+ }
+
+ abstract void encode(byte[] pArray, int i, int length); // package protected for access from I/O streams
+
+ abstract void decode(byte[] pArray, int i, int length); // package protected for access from I/O streams
+
+ /**
+ * Returns whether or not the <code>octet</code> is in the current alphabet.
+ * Does not allow whitespace or pad.
+ *
+ * @param value The value to test
+ *
+ * @return <code>true</code> if the value is defined in the current alphabet, <code>false</code> otherwise.
+ */
+ protected abstract boolean isInAlphabet(byte value);
+
+ /**
+ * Tests a given byte array to see if it contains only valid characters within the alphabet.
+ * The method optionally treats whitespace and pad as valid.
+ *
+ * @param arrayOctet byte array to test
+ * @param allowWSPad if <code>true</code>, then whitespace and PAD are also allowed
+ *
+ * @return <code>true</code> if all bytes are valid characters in the alphabet or if the byte array is empty;
+ * <code>false</code>, otherwise
+ */
+ public boolean isInAlphabet(byte[] arrayOctet, boolean allowWSPad) {
+ for (int i = 0; i < arrayOctet.length; i++) {
+ if (!isInAlphabet(arrayOctet[i]) &&
+ (!allowWSPad || (arrayOctet[i] != PAD) && !isWhiteSpace(arrayOctet[i]))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Tests a given String to see if it contains only valid characters within the alphabet.
+ * The method treats whitespace and PAD as valid.
+ *
+ * @param basen String to test
+ * @return <code>true</code> if all characters in the String are valid characters in the alphabet or if
+ * the String is empty; <code>false</code>, otherwise
+ * @see #isInAlphabet(byte[], boolean)
+ */
+ public boolean isInAlphabet(String basen) {
+ return isInAlphabet(StringUtils.getBytesUtf8(basen), true);
+ }
+
+ /**
+ * Tests a given byte array to see if it contains any characters within the alphabet or PAD.
+ *
+ * Intended for use in checking line-ending arrays
+ *
+ * @param arrayOctet
+ * byte array to test
+ * @return <code>true</code> if any byte is a valid character in the alphabet or PAD; <code>false</code> otherwise
+ */
+ protected boolean containsAlphabetOrPad(byte[] arrayOctet) {
+ if (arrayOctet == null) {
+ return false;
+ }
+ for (int i = 0; i < arrayOctet.length; i++) {
+ if (PAD == arrayOctet[i] || isInAlphabet(arrayOctet[i])) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Calculates the amount of space needed to encode the supplied array.
+ *
+ * @param pArray byte[] array which will later be encoded
+ *
+ * @return amount of space needed to encoded the supplied array.
+ * Returns a long since a max-len array will require > Integer.MAX_VALUE
+ */
+ public long getEncodedLength(byte[] pArray) {
+ // Calculate non-chunked size - rounded up to allow for padding
+ // cast to long is needed to avoid possibility of overflow
+ long len = ((pArray.length + unencodedBlockSize-1) / unencodedBlockSize) * (long) encodedBlockSize;
+ if (lineLength > 0) { // We're using chunking
+ // Round up to nearest multiple
+ len += ((len + lineLength-1) / lineLength) * chunkSeparatorLength;
+ }
+ return len;
+ }
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/BaseNCodecInputStream.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/BaseNCodecInputStream.java
new file mode 100644
index 0000000000..0aa879b155
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/BaseNCodecInputStream.java
@@ -0,0 +1,132 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.binary;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Abstract superclass for Base-N input streams.
+ *
+ * @since 1.5
+ */
+public class BaseNCodecInputStream extends FilterInputStream {
+
+ private final boolean doEncode;
+
+ private final BaseNCodec baseNCodec;
+
+ private final byte[] singleByte = new byte[1];
+
+ protected BaseNCodecInputStream(InputStream in, BaseNCodec baseNCodec, boolean doEncode) {
+ super(in);
+ this.doEncode = doEncode;
+ this.baseNCodec = baseNCodec;
+ }
+
+ /**
+ * Reads one <code>byte</code> from this input stream.
+ *
+ * @return the byte as an integer in the range 0 to 255. Returns -1 if EOF has been reached.
+ * @throws IOException
+ * if an I/O error occurs.
+ */
+ public int read() throws IOException {
+ int r = read(singleByte, 0, 1);
+ while (r == 0) {
+ r = read(singleByte, 0, 1);
+ }
+ if (r > 0) {
+ return singleByte[0] < 0 ? 256 + singleByte[0] : singleByte[0];
+ }
+ return -1;
+ }
+
+ /**
+ * Attempts to read <code>len</code> bytes into the specified <code>b</code> array starting at <code>offset</code>
+ * from this InputStream.
+ *
+ * @param b
+ * destination byte array
+ * @param offset
+ * where to start writing the bytes
+ * @param len
+ * maximum number of bytes to read
+ *
+ * @return number of bytes read
+ * @throws IOException
+ * if an I/O error occurs.
+ * @throws NullPointerException
+ * if the byte array parameter is null
+ * @throws IndexOutOfBoundsException
+ * if offset, len or buffer size are invalid
+ */
+ public int read(byte b[], int offset, int len) throws IOException {
+ if (b == null) {
+ throw new NullPointerException();
+ } else if (offset < 0 || len < 0) {
+ throw new IndexOutOfBoundsException();
+ } else if (offset > b.length || offset + len > b.length) {
+ throw new IndexOutOfBoundsException();
+ } else if (len == 0) {
+ return 0;
+ } else {
+ int readLen = 0;
+ /*
+ Rationale for while-loop on (readLen == 0):
+ -----
+ Base32.readResults() usually returns > 0 or EOF (-1). In the
+ rare case where it returns 0, we just keep trying.
+
+ This is essentially an undocumented contract for InputStream
+ implementors that want their code to work properly with
+ java.io.InputStreamReader, since the latter hates it when
+ InputStream.read(byte[]) returns a zero. Unfortunately our
+ readResults() call must return 0 if a large amount of the data
+ being decoded was non-base32, so this while-loop enables proper
+ interop with InputStreamReader for that scenario.
+ -----
+ This is a fix for CODEC-101
+ */
+ while (readLen == 0) {
+ if (!baseNCodec.hasData()) {
+ byte[] buf = new byte[doEncode ? 4096 : 8192];
+ int c = in.read(buf);
+ if (doEncode) {
+ baseNCodec.encode(buf, 0, c);
+ } else {
+ baseNCodec.decode(buf, 0, c);
+ }
+ }
+ readLen = baseNCodec.readResults(b, offset, len);
+ }
+ return readLen;
+ }
+ }
+ /**
+ * {@inheritDoc}
+ *
+ * @return false
+ */
+ public boolean markSupported() {
+ return false; // not an easy job to support marks
+ }
+
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/BaseNCodecOutputStream.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/BaseNCodecOutputStream.java
new file mode 100644
index 0000000000..bdcbd4d34b
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/BaseNCodecOutputStream.java
@@ -0,0 +1,142 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.binary;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Abstract superclass for Base-N output streams.
+ *
+ * @since 1.5
+ */
+public class BaseNCodecOutputStream extends FilterOutputStream {
+
+ private final boolean doEncode;
+
+ private final BaseNCodec baseNCodec;
+
+ private final byte[] singleByte = new byte[1];
+
+ public BaseNCodecOutputStream(OutputStream out, BaseNCodec basedCodec, boolean doEncode) {
+ super(out);
+ this.baseNCodec = basedCodec;
+ this.doEncode = doEncode;
+ }
+
+ /**
+ * Writes the specified <code>byte</code> to this output stream.
+ *
+ * @param i
+ * source byte
+ * @throws IOException
+ * if an I/O error occurs.
+ */
+ public void write(int i) throws IOException {
+ singleByte[0] = (byte) i;
+ write(singleByte, 0, 1);
+ }
+
+ /**
+ * Writes <code>len</code> bytes from the specified <code>b</code> array starting at <code>offset</code> to this
+ * output stream.
+ *
+ * @param b
+ * source byte array
+ * @param offset
+ * where to start reading the bytes
+ * @param len
+ * maximum number of bytes to write
+ *
+ * @throws IOException
+ * if an I/O error occurs.
+ * @throws NullPointerException
+ * if the byte array parameter is null
+ * @throws IndexOutOfBoundsException
+ * if offset, len or buffer size are invalid
+ */
+ public void write(byte b[], int offset, int len) throws IOException {
+ if (b == null) {
+ throw new NullPointerException();
+ } else if (offset < 0 || len < 0) {
+ throw new IndexOutOfBoundsException();
+ } else if (offset > b.length || offset + len > b.length) {
+ throw new IndexOutOfBoundsException();
+ } else if (len > 0) {
+ if (doEncode) {
+ baseNCodec.encode(b, offset, len);
+ } else {
+ baseNCodec.decode(b, offset, len);
+ }
+ flush(false);
+ }
+ }
+
+ /**
+ * Flushes this output stream and forces any buffered output bytes to be written out to the stream. If propogate is
+ * true, the wrapped stream will also be flushed.
+ *
+ * @param propogate
+ * boolean flag to indicate whether the wrapped OutputStream should also be flushed.
+ * @throws IOException
+ * if an I/O error occurs.
+ */
+ private void flush(boolean propogate) throws IOException {
+ int avail = baseNCodec.available();
+ if (avail > 0) {
+ byte[] buf = new byte[avail];
+ int c = baseNCodec.readResults(buf, 0, avail);
+ if (c > 0) {
+ out.write(buf, 0, c);
+ }
+ }
+ if (propogate) {
+ out.flush();
+ }
+ }
+
+ /**
+ * Flushes this output stream and forces any buffered output bytes to be written out to the stream.
+ *
+ * @throws IOException
+ * if an I/O error occurs.
+ */
+ public void flush() throws IOException {
+ flush(true);
+ }
+
+ /**
+ * Closes this output stream and releases any system resources associated with the stream.
+ *
+ * @throws IOException
+ * if an I/O error occurs.
+ */
+ public void close() throws IOException {
+ // Notify encoder of EOF (-1).
+ if (doEncode) {
+ baseNCodec.encode(singleByte, 0, -1);
+ } else {
+ baseNCodec.decode(singleByte, 0, -1);
+ }
+ flush();
+ out.close();
+ }
+
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/BinaryCodec.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/BinaryCodec.java
new file mode 100644
index 0000000000..141474151f
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/BinaryCodec.java
@@ -0,0 +1,297 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.binary;
+
+import org.mozilla.apache.commons.codec.BinaryDecoder;
+import org.mozilla.apache.commons.codec.BinaryEncoder;
+import org.mozilla.apache.commons.codec.DecoderException;
+import org.mozilla.apache.commons.codec.EncoderException;
+
+/**
+ * Converts between byte arrays and strings of "0"s and "1"s.
+ *
+ * TODO: may want to add more bit vector functions like and/or/xor/nand
+ * TODO: also might be good to generate boolean[] from byte[] et cetera.
+ *
+ * @author Apache Software Foundation
+ * @since 1.3
+ * @version $Id: BinaryCodec.java 1080701 2011-03-11 17:52:27Z ggregory $
+ */
+public class BinaryCodec implements BinaryDecoder, BinaryEncoder {
+ /*
+ * tried to avoid using ArrayUtils to minimize dependencies while using these empty arrays - dep is just not worth
+ * it.
+ */
+ /** Empty char array. */
+ private static final char[] EMPTY_CHAR_ARRAY = new char[0];
+
+ /** Empty byte array. */
+ private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+
+ /** Mask for bit 0 of a byte. */
+ private static final int BIT_0 = 1;
+
+ /** Mask for bit 1 of a byte. */
+ private static final int BIT_1 = 0x02;
+
+ /** Mask for bit 2 of a byte. */
+ private static final int BIT_2 = 0x04;
+
+ /** Mask for bit 3 of a byte. */
+ private static final int BIT_3 = 0x08;
+
+ /** Mask for bit 4 of a byte. */
+ private static final int BIT_4 = 0x10;
+
+ /** Mask for bit 5 of a byte. */
+ private static final int BIT_5 = 0x20;
+
+ /** Mask for bit 6 of a byte. */
+ private static final int BIT_6 = 0x40;
+
+ /** Mask for bit 7 of a byte. */
+ private static final int BIT_7 = 0x80;
+
+ private static final int[] BITS = {BIT_0, BIT_1, BIT_2, BIT_3, BIT_4, BIT_5, BIT_6, BIT_7};
+
+ /**
+ * Converts an array of raw binary data into an array of ASCII 0 and 1 characters.
+ *
+ * @param raw
+ * the raw binary data to convert
+ * @return 0 and 1 ASCII character bytes one for each bit of the argument
+ * @see org.mozilla.apache.commons.codec.BinaryEncoder#encode(byte[])
+ */
+ public byte[] encode(byte[] raw) {
+ return toAsciiBytes(raw);
+ }
+
+ /**
+ * Converts an array of raw binary data into an array of ASCII 0 and 1 chars.
+ *
+ * @param raw
+ * the raw binary data to convert
+ * @return 0 and 1 ASCII character chars one for each bit of the argument
+ * @throws EncoderException
+ * if the argument is not a byte[]
+ * @see org.mozilla.apache.commons.codec.Encoder#encode(Object)
+ */
+ public Object encode(Object raw) throws EncoderException {
+ if (!(raw instanceof byte[])) {
+ throw new EncoderException("argument not a byte array");
+ }
+ return toAsciiChars((byte[]) raw);
+ }
+
+ /**
+ * Decodes a byte array where each byte represents an ASCII '0' or '1'.
+ *
+ * @param ascii
+ * each byte represents an ASCII '0' or '1'
+ * @return the raw encoded binary where each bit corresponds to a byte in the byte array argument
+ * @throws DecoderException
+ * if argument is not a byte[], char[] or String
+ * @see org.mozilla.apache.commons.codec.Decoder#decode(Object)
+ */
+ public Object decode(Object ascii) throws DecoderException {
+ if (ascii == null) {
+ return EMPTY_BYTE_ARRAY;
+ }
+ if (ascii instanceof byte[]) {
+ return fromAscii((byte[]) ascii);
+ }
+ if (ascii instanceof char[]) {
+ return fromAscii((char[]) ascii);
+ }
+ if (ascii instanceof String) {
+ return fromAscii(((String) ascii).toCharArray());
+ }
+ throw new DecoderException("argument not a byte array");
+ }
+
+ /**
+ * Decodes a byte array where each byte represents an ASCII '0' or '1'.
+ *
+ * @param ascii
+ * each byte represents an ASCII '0' or '1'
+ * @return the raw encoded binary where each bit corresponds to a byte in the byte array argument
+ * @see org.mozilla.apache.commons.codec.Decoder#decode(Object)
+ */
+ public byte[] decode(byte[] ascii) {
+ return fromAscii(ascii);
+ }
+
+ /**
+ * Decodes a String where each char of the String represents an ASCII '0' or '1'.
+ *
+ * @param ascii
+ * String of '0' and '1' characters
+ * @return the raw encoded binary where each bit corresponds to a byte in the byte array argument
+ * @see org.mozilla.apache.commons.codec.Decoder#decode(Object)
+ */
+ public byte[] toByteArray(String ascii) {
+ if (ascii == null) {
+ return EMPTY_BYTE_ARRAY;
+ }
+ return fromAscii(ascii.toCharArray());
+ }
+
+ // ------------------------------------------------------------------------
+ //
+ // static codec operations
+ //
+ // ------------------------------------------------------------------------
+ /**
+ * Decodes a char array where each char represents an ASCII '0' or '1'.
+ *
+ * @param ascii
+ * each char represents an ASCII '0' or '1'
+ * @return the raw encoded binary where each bit corresponds to a char in the char array argument
+ */
+ public static byte[] fromAscii(char[] ascii) {
+ if (ascii == null || ascii.length == 0) {
+ return EMPTY_BYTE_ARRAY;
+ }
+ // get length/8 times bytes with 3 bit shifts to the right of the length
+ byte[] l_raw = new byte[ascii.length >> 3];
+ /*
+ * We decr index jj by 8 as we go along to not recompute indices using multiplication every time inside the
+ * loop.
+ */
+ for (int ii = 0, jj = ascii.length - 1; ii < l_raw.length; ii++, jj -= 8) {
+ for (int bits = 0; bits < BITS.length; ++bits) {
+ if (ascii[jj - bits] == '1') {
+ l_raw[ii] |= BITS[bits];
+ }
+ }
+ }
+ return l_raw;
+ }
+
+ /**
+ * Decodes a byte array where each byte represents an ASCII '0' or '1'.
+ *
+ * @param ascii
+ * each byte represents an ASCII '0' or '1'
+ * @return the raw encoded binary where each bit corresponds to a byte in the byte array argument
+ */
+ public static byte[] fromAscii(byte[] ascii) {
+ if (isEmpty(ascii)) {
+ return EMPTY_BYTE_ARRAY;
+ }
+ // get length/8 times bytes with 3 bit shifts to the right of the length
+ byte[] l_raw = new byte[ascii.length >> 3];
+ /*
+ * We decr index jj by 8 as we go along to not recompute indices using multiplication every time inside the
+ * loop.
+ */
+ for (int ii = 0, jj = ascii.length - 1; ii < l_raw.length; ii++, jj -= 8) {
+ for (int bits = 0; bits < BITS.length; ++bits) {
+ if (ascii[jj - bits] == '1') {
+ l_raw[ii] |= BITS[bits];
+ }
+ }
+ }
+ return l_raw;
+ }
+
+ /**
+ * Returns <code>true</code> if the given array is <code>null</code> or empty (size 0.)
+ *
+ * @param array
+ * the source array
+ * @return <code>true</code> if the given array is <code>null</code> or empty (size 0.)
+ */
+ private static boolean isEmpty(byte[] array) {
+ return array == null || array.length == 0;
+ }
+
+ /**
+ * Converts an array of raw binary data into an array of ASCII 0 and 1 character bytes - each byte is a truncated
+ * char.
+ *
+ * @param raw
+ * the raw binary data to convert
+ * @return an array of 0 and 1 character bytes for each bit of the argument
+ * @see org.mozilla.apache.commons.codec.BinaryEncoder#encode(byte[])
+ */
+ public static byte[] toAsciiBytes(byte[] raw) {
+ if (isEmpty(raw)) {
+ return EMPTY_BYTE_ARRAY;
+ }
+ // get 8 times the bytes with 3 bit shifts to the left of the length
+ byte[] l_ascii = new byte[raw.length << 3];
+ /*
+ * We decr index jj by 8 as we go along to not recompute indices using multiplication every time inside the
+ * loop.
+ */
+ for (int ii = 0, jj = l_ascii.length - 1; ii < raw.length; ii++, jj -= 8) {
+ for (int bits = 0; bits < BITS.length; ++bits) {
+ if ((raw[ii] & BITS[bits]) == 0) {
+ l_ascii[jj - bits] = '0';
+ } else {
+ l_ascii[jj - bits] = '1';
+ }
+ }
+ }
+ return l_ascii;
+ }
+
+ /**
+ * Converts an array of raw binary data into an array of ASCII 0 and 1 characters.
+ *
+ * @param raw
+ * the raw binary data to convert
+ * @return an array of 0 and 1 characters for each bit of the argument
+ * @see org.mozilla.apache.commons.codec.BinaryEncoder#encode(byte[])
+ */
+ public static char[] toAsciiChars(byte[] raw) {
+ if (isEmpty(raw)) {
+ return EMPTY_CHAR_ARRAY;
+ }
+ // get 8 times the bytes with 3 bit shifts to the left of the length
+ char[] l_ascii = new char[raw.length << 3];
+ /*
+ * We decr index jj by 8 as we go along to not recompute indices using multiplication every time inside the
+ * loop.
+ */
+ for (int ii = 0, jj = l_ascii.length - 1; ii < raw.length; ii++, jj -= 8) {
+ for (int bits = 0; bits < BITS.length; ++bits) {
+ if ((raw[ii] & BITS[bits]) == 0) {
+ l_ascii[jj - bits] = '0';
+ } else {
+ l_ascii[jj - bits] = '1';
+ }
+ }
+ }
+ return l_ascii;
+ }
+
+ /**
+ * Converts an array of raw binary data into a String of ASCII 0 and 1 characters.
+ *
+ * @param raw
+ * the raw binary data to convert
+ * @return a String of 0 and 1 characters representing the binary data
+ * @see org.mozilla.apache.commons.codec.BinaryEncoder#encode(byte[])
+ */
+ public static String toAsciiString(byte[] raw) {
+ return new String(toAsciiChars(raw));
+ }
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Hex.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Hex.java
new file mode 100644
index 0000000000..a2e34fe34c
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/Hex.java
@@ -0,0 +1,302 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.binary;
+
+import java.io.UnsupportedEncodingException;
+
+import org.mozilla.apache.commons.codec.BinaryDecoder;
+import org.mozilla.apache.commons.codec.BinaryEncoder;
+import org.mozilla.apache.commons.codec.CharEncoding;
+import org.mozilla.apache.commons.codec.DecoderException;
+import org.mozilla.apache.commons.codec.EncoderException;
+
+/**
+ * Converts hexadecimal Strings. The charset used for certain operation can be set, the default is set in
+ * {@link #DEFAULT_CHARSET_NAME}
+ *
+ * @since 1.1
+ * @author Apache Software Foundation
+ * @version $Id: Hex.java 1080701 2011-03-11 17:52:27Z ggregory $
+ */
+public class Hex implements BinaryEncoder, BinaryDecoder {
+
+ /**
+ * Default charset name is {@link CharEncoding#UTF_8}
+ *
+ * @since 1.4
+ */
+ public static final String DEFAULT_CHARSET_NAME = CharEncoding.UTF_8;
+
+ /**
+ * Used to build output as Hex
+ */
+ private static final char[] DIGITS_LOWER = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
+
+ /**
+ * Used to build output as Hex
+ */
+ private static final char[] DIGITS_UPPER = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
+
+ /**
+ * Converts an array of characters representing hexadecimal values into an array of bytes of those same values. The
+ * returned array will be half the length of the passed array, as it takes two characters to represent any given
+ * byte. An exception is thrown if the passed char array has an odd number of elements.
+ *
+ * @param data
+ * An array of characters containing hexadecimal digits
+ * @return A byte array containing binary data decoded from the supplied char array.
+ * @throws DecoderException
+ * Thrown if an odd number or illegal of characters is supplied
+ */
+ public static byte[] decodeHex(char[] data) throws DecoderException {
+
+ int len = data.length;
+
+ if ((len & 0x01) != 0) {
+ throw new DecoderException("Odd number of characters.");
+ }
+
+ byte[] out = new byte[len >> 1];
+
+ // two characters form the hex value.
+ for (int i = 0, j = 0; j < len; i++) {
+ int f = toDigit(data[j], j) << 4;
+ j++;
+ f = f | toDigit(data[j], j);
+ j++;
+ out[i] = (byte) (f & 0xFF);
+ }
+
+ return out;
+ }
+
+ /**
+ * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order.
+ * The returned array will be double the length of the passed array, as it takes two characters to represent any
+ * given byte.
+ *
+ * @param data
+ * a byte[] to convert to Hex characters
+ * @return A char[] containing hexadecimal characters
+ */
+ public static char[] encodeHex(byte[] data) {
+ return encodeHex(data, true);
+ }
+
+ /**
+ * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order.
+ * The returned array will be double the length of the passed array, as it takes two characters to represent any
+ * given byte.
+ *
+ * @param data
+ * a byte[] to convert to Hex characters
+ * @param toLowerCase
+ * <code>true</code> converts to lowercase, <code>false</code> to uppercase
+ * @return A char[] containing hexadecimal characters
+ * @since 1.4
+ */
+ public static char[] encodeHex(byte[] data, boolean toLowerCase) {
+ return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER);
+ }
+
+ /**
+ * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order.
+ * The returned array will be double the length of the passed array, as it takes two characters to represent any
+ * given byte.
+ *
+ * @param data
+ * a byte[] to convert to Hex characters
+ * @param toDigits
+ * the output alphabet
+ * @return A char[] containing hexadecimal characters
+ * @since 1.4
+ */
+ protected static char[] encodeHex(byte[] data, char[] toDigits) {
+ int l = data.length;
+ char[] out = new char[l << 1];
+ // two characters form the hex value.
+ for (int i = 0, j = 0; i < l; i++) {
+ out[j++] = toDigits[(0xF0 & data[i]) >>> 4];
+ out[j++] = toDigits[0x0F & data[i]];
+ }
+ return out;
+ }
+
+ /**
+ * Converts an array of bytes into a String representing the hexadecimal values of each byte in order. The returned
+ * String will be double the length of the passed array, as it takes two characters to represent any given byte.
+ *
+ * @param data
+ * a byte[] to convert to Hex characters
+ * @return A String containing hexadecimal characters
+ * @since 1.4
+ */
+ public static String encodeHexString(byte[] data) {
+ return new String(encodeHex(data));
+ }
+
+ /**
+ * Converts a hexadecimal character to an integer.
+ *
+ * @param ch
+ * A character to convert to an integer digit
+ * @param index
+ * The index of the character in the source
+ * @return An integer
+ * @throws DecoderException
+ * Thrown if ch is an illegal hex character
+ */
+ protected static int toDigit(char ch, int index) throws DecoderException {
+ int digit = Character.digit(ch, 16);
+ if (digit == -1) {
+ throw new DecoderException("Illegal hexadecimal character " + ch + " at index " + index);
+ }
+ return digit;
+ }
+
+ private final String charsetName;
+
+ /**
+ * Creates a new codec with the default charset name {@link #DEFAULT_CHARSET_NAME}
+ */
+ public Hex() {
+ // use default encoding
+ this.charsetName = DEFAULT_CHARSET_NAME;
+ }
+
+ /**
+ * Creates a new codec with the given charset name.
+ *
+ * @param csName
+ * the charset name.
+ * @since 1.4
+ */
+ public Hex(String csName) {
+ this.charsetName = csName;
+ }
+
+ /**
+ * Converts an array of character bytes representing hexadecimal values into an array of bytes of those same values.
+ * The returned array will be half the length of the passed array, as it takes two characters to represent any given
+ * byte. An exception is thrown if the passed char array has an odd number of elements.
+ *
+ * @param array
+ * An array of character bytes containing hexadecimal digits
+ * @return A byte array containing binary data decoded from the supplied byte array (representing characters).
+ * @throws DecoderException
+ * Thrown if an odd number of characters is supplied to this function
+ * @see #decodeHex(char[])
+ */
+ public byte[] decode(byte[] array) throws DecoderException {
+ try {
+ return decodeHex(new String(array, getCharsetName()).toCharArray());
+ } catch (UnsupportedEncodingException e) {
+ throw new DecoderException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Converts a String or an array of character bytes representing hexadecimal values into an array of bytes of those
+ * same values. The returned array will be half the length of the passed String or array, as it takes two characters
+ * to represent any given byte. An exception is thrown if the passed char array has an odd number of elements.
+ *
+ * @param object
+ * A String or, an array of character bytes containing hexadecimal digits
+ * @return A byte array containing binary data decoded from the supplied byte array (representing characters).
+ * @throws DecoderException
+ * Thrown if an odd number of characters is supplied to this function or the object is not a String or
+ * char[]
+ * @see #decodeHex(char[])
+ */
+ public Object decode(Object object) throws DecoderException {
+ try {
+ char[] charArray = object instanceof String ? ((String) object).toCharArray() : (char[]) object;
+ return decodeHex(charArray);
+ } catch (ClassCastException e) {
+ throw new DecoderException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Converts an array of bytes into an array of bytes for the characters representing the hexadecimal values of each
+ * byte in order. The returned array will be double the length of the passed array, as it takes two characters to
+ * represent any given byte.
+ * <p>
+ * The conversion from hexadecimal characters to the returned bytes is performed with the charset named by
+ * {@link #getCharsetName()}.
+ * </p>
+ *
+ * @param array
+ * a byte[] to convert to Hex characters
+ * @return A byte[] containing the bytes of the hexadecimal characters
+ * @throws IllegalStateException
+ * if the charsetName is invalid. This API throws {@link IllegalStateException} instead of
+ * {@link UnsupportedEncodingException} for backward compatibility.
+ * @see #encodeHex(byte[])
+ */
+ public byte[] encode(byte[] array) {
+ return StringUtils.getBytesUnchecked(encodeHexString(array), getCharsetName());
+ }
+
+ /**
+ * Converts a String or an array of bytes into an array of characters representing the hexadecimal values of each
+ * byte in order. The returned array will be double the length of the passed String or array, as it takes two
+ * characters to represent any given byte.
+ * <p>
+ * The conversion from hexadecimal characters to bytes to be encoded to performed with the charset named by
+ * {@link #getCharsetName()}.
+ * </p>
+ *
+ * @param object
+ * a String, or byte[] to convert to Hex characters
+ * @return A char[] containing hexadecimal characters
+ * @throws EncoderException
+ * Thrown if the given object is not a String or byte[]
+ * @see #encodeHex(byte[])
+ */
+ public Object encode(Object object) throws EncoderException {
+ try {
+ byte[] byteArray = object instanceof String ? ((String) object).getBytes(getCharsetName()) : (byte[]) object;
+ return encodeHex(byteArray);
+ } catch (ClassCastException e) {
+ throw new EncoderException(e.getMessage(), e);
+ } catch (UnsupportedEncodingException e) {
+ throw new EncoderException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Gets the charset name.
+ *
+ * @return the charset name.
+ * @since 1.4
+ */
+ public String getCharsetName() {
+ return this.charsetName;
+ }
+
+ /**
+ * Returns a string representation of the object, which includes the charset name.
+ *
+ * @return a string representation of the object.
+ */
+ public String toString() {
+ return super.toString() + "[charsetName=" + this.charsetName + "]";
+ }
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/StringUtils.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/StringUtils.java
new file mode 100644
index 0000000000..7bf960124c
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/StringUtils.java
@@ -0,0 +1,287 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.binary;
+
+import java.io.UnsupportedEncodingException;
+
+import org.mozilla.apache.commons.codec.CharEncoding;
+
+/**
+ * Converts String to and from bytes using the encodings required by the Java specification. These encodings are specified in <a
+ * href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+ *
+ * @see CharEncoding
+ * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+ * @author <a href="mailto:ggregory@seagullsw.com">Gary Gregory</a>
+ * @version $Id: StringUtils.java 950460 2010-06-02 09:43:02Z sebb $
+ * @since 1.4
+ */
+public class StringUtils {
+
+ /**
+ * Encodes the given string into a sequence of bytes using the ISO-8859-1 charset, storing the result into a new
+ * byte array.
+ *
+ * @param string
+ * the String to encode, may be <code>null</code>
+ * @return encoded bytes, or <code>null</code> if the input string was <code>null</code>
+ * @throws IllegalStateException
+ * Thrown when the charset is missing, which should be never according the the Java specification.
+ * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+ * @see #getBytesUnchecked(String, String)
+ */
+ public static byte[] getBytesIso8859_1(String string) {
+ return StringUtils.getBytesUnchecked(string, CharEncoding.ISO_8859_1);
+ }
+
+ /**
+ * Encodes the given string into a sequence of bytes using the US-ASCII charset, storing the result into a new byte
+ * array.
+ *
+ * @param string
+ * the String to encode, may be <code>null</code>
+ * @return encoded bytes, or <code>null</code> if the input string was <code>null</code>
+ * @throws IllegalStateException
+ * Thrown when the charset is missing, which should be never according the the Java specification.
+ * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+ * @see #getBytesUnchecked(String, String)
+ */
+ public static byte[] getBytesUsAscii(String string) {
+ return StringUtils.getBytesUnchecked(string, CharEncoding.US_ASCII);
+ }
+
+ /**
+ * Encodes the given string into a sequence of bytes using the UTF-16 charset, storing the result into a new byte
+ * array.
+ *
+ * @param string
+ * the String to encode, may be <code>null</code>
+ * @return encoded bytes, or <code>null</code> if the input string was <code>null</code>
+ * @throws IllegalStateException
+ * Thrown when the charset is missing, which should be never according the the Java specification.
+ * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+ * @see #getBytesUnchecked(String, String)
+ */
+ public static byte[] getBytesUtf16(String string) {
+ return StringUtils.getBytesUnchecked(string, CharEncoding.UTF_16);
+ }
+
+ /**
+ * Encodes the given string into a sequence of bytes using the UTF-16BE charset, storing the result into a new byte
+ * array.
+ *
+ * @param string
+ * the String to encode, may be <code>null</code>
+ * @return encoded bytes, or <code>null</code> if the input string was <code>null</code>
+ * @throws IllegalStateException
+ * Thrown when the charset is missing, which should be never according the the Java specification.
+ * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+ * @see #getBytesUnchecked(String, String)
+ */
+ public static byte[] getBytesUtf16Be(String string) {
+ return StringUtils.getBytesUnchecked(string, CharEncoding.UTF_16BE);
+ }
+
+ /**
+ * Encodes the given string into a sequence of bytes using the UTF-16LE charset, storing the result into a new byte
+ * array.
+ *
+ * @param string
+ * the String to encode, may be <code>null</code>
+ * @return encoded bytes, or <code>null</code> if the input string was <code>null</code>
+ * @throws IllegalStateException
+ * Thrown when the charset is missing, which should be never according the the Java specification.
+ * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+ * @see #getBytesUnchecked(String, String)
+ */
+ public static byte[] getBytesUtf16Le(String string) {
+ return StringUtils.getBytesUnchecked(string, CharEncoding.UTF_16LE);
+ }
+
+ /**
+ * Encodes the given string into a sequence of bytes using the UTF-8 charset, storing the result into a new byte
+ * array.
+ *
+ * @param string
+ * the String to encode, may be <code>null</code>
+ * @return encoded bytes, or <code>null</code> if the input string was <code>null</code>
+ * @throws IllegalStateException
+ * Thrown when the charset is missing, which should be never according the the Java specification.
+ * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+ * @see #getBytesUnchecked(String, String)
+ */
+ public static byte[] getBytesUtf8(String string) {
+ return StringUtils.getBytesUnchecked(string, CharEncoding.UTF_8);
+ }
+
+ /**
+ * Encodes the given string into a sequence of bytes using the named charset, storing the result into a new byte
+ * array.
+ * <p>
+ * This method catches {@link UnsupportedEncodingException} and rethrows it as {@link IllegalStateException}, which
+ * should never happen for a required charset name. Use this method when the encoding is required to be in the JRE.
+ * </p>
+ *
+ * @param string
+ * the String to encode, may be <code>null</code>
+ * @param charsetName
+ * The name of a required {@link java.nio.charset.Charset}
+ * @return encoded bytes, or <code>null</code> if the input string was <code>null</code>
+ * @throws IllegalStateException
+ * Thrown when a {@link UnsupportedEncodingException} is caught, which should never happen for a
+ * required charset name.
+ * @see CharEncoding
+ * @see String#getBytes(String)
+ */
+ public static byte[] getBytesUnchecked(String string, String charsetName) {
+ if (string == null) {
+ return null;
+ }
+ try {
+ return string.getBytes(charsetName);
+ } catch (UnsupportedEncodingException e) {
+ throw StringUtils.newIllegalStateException(charsetName, e);
+ }
+ }
+
+ private static IllegalStateException newIllegalStateException(String charsetName, UnsupportedEncodingException e) {
+ return new IllegalStateException(charsetName + ": " + e);
+ }
+
+ /**
+ * Constructs a new <code>String</code> by decoding the specified array of bytes using the given charset.
+ * <p>
+ * This method catches {@link UnsupportedEncodingException} and re-throws it as {@link IllegalStateException}, which
+ * should never happen for a required charset name. Use this method when the encoding is required to be in the JRE.
+ * </p>
+ *
+ * @param bytes
+ * The bytes to be decoded into characters, may be <code>null</code>
+ * @param charsetName
+ * The name of a required {@link java.nio.charset.Charset}
+ * @return A new <code>String</code> decoded from the specified array of bytes using the given charset,
+ * or <code>null</code> if the input byte arrray was <code>null</code>.
+ * @throws IllegalStateException
+ * Thrown when a {@link UnsupportedEncodingException} is caught, which should never happen for a
+ * required charset name.
+ * @see CharEncoding
+ * @see String#String(byte[], String)
+ */
+ public static String newString(byte[] bytes, String charsetName) {
+ if (bytes == null) {
+ return null;
+ }
+ try {
+ return new String(bytes, charsetName);
+ } catch (UnsupportedEncodingException e) {
+ throw StringUtils.newIllegalStateException(charsetName, e);
+ }
+ }
+
+ /**
+ * Constructs a new <code>String</code> by decoding the specified array of bytes using the ISO-8859-1 charset.
+ *
+ * @param bytes
+ * The bytes to be decoded into characters, may be <code>null</code>
+ * @return A new <code>String</code> decoded from the specified array of bytes using the ISO-8859-1 charset,
+ * or <code>null</code> if the input byte array was <code>null</code>.
+ * @throws IllegalStateException
+ * Thrown when a {@link UnsupportedEncodingException} is caught, which should never happen since the
+ * charset is required.
+ */
+ public static String newStringIso8859_1(byte[] bytes) {
+ return StringUtils.newString(bytes, CharEncoding.ISO_8859_1);
+ }
+
+ /**
+ * Constructs a new <code>String</code> by decoding the specified array of bytes using the US-ASCII charset.
+ *
+ * @param bytes
+ * The bytes to be decoded into characters
+ * @return A new <code>String</code> decoded from the specified array of bytes using the US-ASCII charset,
+ * or <code>null</code> if the input byte array was <code>null</code>.
+ * @throws IllegalStateException
+ * Thrown when a {@link UnsupportedEncodingException} is caught, which should never happen since the
+ * charset is required.
+ */
+ public static String newStringUsAscii(byte[] bytes) {
+ return StringUtils.newString(bytes, CharEncoding.US_ASCII);
+ }
+
+ /**
+ * Constructs a new <code>String</code> by decoding the specified array of bytes using the UTF-16 charset.
+ *
+ * @param bytes
+ * The bytes to be decoded into characters
+ * @return A new <code>String</code> decoded from the specified array of bytes using the UTF-16 charset
+ * or <code>null</code> if the input byte array was <code>null</code>.
+ * @throws IllegalStateException
+ * Thrown when a {@link UnsupportedEncodingException} is caught, which should never happen since the
+ * charset is required.
+ */
+ public static String newStringUtf16(byte[] bytes) {
+ return StringUtils.newString(bytes, CharEncoding.UTF_16);
+ }
+
+ /**
+ * Constructs a new <code>String</code> by decoding the specified array of bytes using the UTF-16BE charset.
+ *
+ * @param bytes
+ * The bytes to be decoded into characters
+ * @return A new <code>String</code> decoded from the specified array of bytes using the UTF-16BE charset,
+ * or <code>null</code> if the input byte array was <code>null</code>.
+ * @throws IllegalStateException
+ * Thrown when a {@link UnsupportedEncodingException} is caught, which should never happen since the
+ * charset is required.
+ */
+ public static String newStringUtf16Be(byte[] bytes) {
+ return StringUtils.newString(bytes, CharEncoding.UTF_16BE);
+ }
+
+ /**
+ * Constructs a new <code>String</code> by decoding the specified array of bytes using the UTF-16LE charset.
+ *
+ * @param bytes
+ * The bytes to be decoded into characters
+ * @return A new <code>String</code> decoded from the specified array of bytes using the UTF-16LE charset,
+ * or <code>null</code> if the input byte array was <code>null</code>.
+ * @throws IllegalStateException
+ * Thrown when a {@link UnsupportedEncodingException} is caught, which should never happen since the
+ * charset is required.
+ */
+ public static String newStringUtf16Le(byte[] bytes) {
+ return StringUtils.newString(bytes, CharEncoding.UTF_16LE);
+ }
+
+ /**
+ * Constructs a new <code>String</code> by decoding the specified array of bytes using the UTF-8 charset.
+ *
+ * @param bytes
+ * The bytes to be decoded into characters
+ * @return A new <code>String</code> decoded from the specified array of bytes using the UTF-8 charset,
+ * or <code>null</code> if the input byte array was <code>null</code>.
+ * @throws IllegalStateException
+ * Thrown when a {@link UnsupportedEncodingException} is caught, which should never happen since the
+ * charset is required.
+ */
+ public static String newStringUtf8(byte[] bytes) {
+ return StringUtils.newString(bytes, CharEncoding.UTF_8);
+ }
+
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/package.html b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/package.html
new file mode 100644
index 0000000000..13345ece40
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/binary/package.html
@@ -0,0 +1,21 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<html>
+ <body>
+ Base64, Base32, Binary, and Hexadecimal String encoding and decoding.
+ </body>
+</html>
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/digest/DigestUtils.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/digest/DigestUtils.java
new file mode 100644
index 0000000000..2421bb0fe2
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/digest/DigestUtils.java
@@ -0,0 +1,583 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.digest;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import org.mozilla.apache.commons.codec.binary.Hex;
+import org.mozilla.apache.commons.codec.binary.StringUtils;
+
+/**
+ * Operations to simplify common {@link java.security.MessageDigest} tasks. This class is thread safe.
+ *
+ * @author Apache Software Foundation
+ * @version $Id: DigestUtils.java 1064793 2011-01-28 17:42:55Z ggregory $
+ */
+public class DigestUtils {
+
+ private static final int STREAM_BUFFER_LENGTH = 1024;
+
+ /**
+ * Read through an InputStream and returns the digest for the data
+ *
+ * @param digest
+ * The MessageDigest to use (e.g. MD5)
+ * @param data
+ * Data to digest
+ * @return MD5 digest
+ * @throws IOException
+ * On error reading from the stream
+ */
+ private static byte[] digest(MessageDigest digest, InputStream data) throws IOException {
+ byte[] buffer = new byte[STREAM_BUFFER_LENGTH];
+ int read = data.read(buffer, 0, STREAM_BUFFER_LENGTH);
+
+ while (read > -1) {
+ digest.update(buffer, 0, read);
+ read = data.read(buffer, 0, STREAM_BUFFER_LENGTH);
+ }
+
+ return digest.digest();
+ }
+
+ /**
+ * Calls {@link StringUtils#getBytesUtf8(String)}
+ *
+ * @param data
+ * the String to encode
+ * @return encoded bytes
+ */
+ private static byte[] getBytesUtf8(String data) {
+ return StringUtils.getBytesUtf8(data);
+ }
+
+ /**
+ * Returns a <code>MessageDigest</code> for the given <code>algorithm</code>.
+ *
+ * @param algorithm
+ * the name of the algorithm requested. See <a
+ * href="http://java.sun.com/j2se/1.3/docs/guide/security/CryptoSpec.html#AppA">Appendix A in the Java
+ * Cryptography Architecture API Specification & Reference</a> for information about standard algorithm
+ * names.
+ * @return An MD5 digest instance.
+ * @see MessageDigest#getInstance(String)
+ * @throws RuntimeException
+ * when a {@link java.security.NoSuchAlgorithmException} is caught.
+ */
+ static MessageDigest getDigest(String algorithm) {
+ try {
+ return MessageDigest.getInstance(algorithm);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e.getMessage());
+ }
+ }
+
+ /**
+ * Returns an MD5 MessageDigest.
+ *
+ * @return An MD5 digest instance.
+ * @throws RuntimeException
+ * when a {@link java.security.NoSuchAlgorithmException} is caught.
+ */
+ private static MessageDigest getMd5Digest() {
+ return getDigest("MD5");
+ }
+
+ /**
+ * Returns an SHA-256 digest.
+ * <p>
+ * Throws a <code>RuntimeException</code> on JRE versions prior to 1.4.0.
+ * </p>
+ *
+ * @return An SHA-256 digest instance.
+ * @throws RuntimeException
+ * when a {@link java.security.NoSuchAlgorithmException} is caught.
+ */
+ private static MessageDigest getSha256Digest() {
+ return getDigest("SHA-256");
+ }
+
+ /**
+ * Returns an SHA-384 digest.
+ * <p>
+ * Throws a <code>RuntimeException</code> on JRE versions prior to 1.4.0.
+ * </p>
+ *
+ * @return An SHA-384 digest instance.
+ * @throws RuntimeException
+ * when a {@link java.security.NoSuchAlgorithmException} is caught.
+ */
+ private static MessageDigest getSha384Digest() {
+ return getDigest("SHA-384");
+ }
+
+ /**
+ * Returns an SHA-512 digest.
+ * <p>
+ * Throws a <code>RuntimeException</code> on JRE versions prior to 1.4.0.
+ * </p>
+ *
+ * @return An SHA-512 digest instance.
+ * @throws RuntimeException
+ * when a {@link java.security.NoSuchAlgorithmException} is caught.
+ */
+ private static MessageDigest getSha512Digest() {
+ return getDigest("SHA-512");
+ }
+
+ /**
+ * Returns an SHA-1 digest.
+ *
+ * @return An SHA-1 digest instance.
+ * @throws RuntimeException
+ * when a {@link java.security.NoSuchAlgorithmException} is caught.
+ */
+ private static MessageDigest getShaDigest() {
+ return getDigest("SHA");
+ }
+
+ /**
+ * Calculates the MD5 digest and returns the value as a 16 element <code>byte[]</code>.
+ *
+ * @param data
+ * Data to digest
+ * @return MD5 digest
+ */
+ public static byte[] md5(byte[] data) {
+ return getMd5Digest().digest(data);
+ }
+
+ /**
+ * Calculates the MD5 digest and returns the value as a 16 element <code>byte[]</code>.
+ *
+ * @param data
+ * Data to digest
+ * @return MD5 digest
+ * @throws IOException
+ * On error reading from the stream
+ * @since 1.4
+ */
+ public static byte[] md5(InputStream data) throws IOException {
+ return digest(getMd5Digest(), data);
+ }
+
+ /**
+ * Calculates the MD5 digest and returns the value as a 16 element <code>byte[]</code>.
+ *
+ * @param data
+ * Data to digest
+ * @return MD5 digest
+ */
+ public static byte[] md5(String data) {
+ return md5(getBytesUtf8(data));
+ }
+
+ /**
+ * Calculates the MD5 digest and returns the value as a 32 character hex string.
+ *
+ * @param data
+ * Data to digest
+ * @return MD5 digest as a hex string
+ */
+ public static String md5Hex(byte[] data) {
+ return Hex.encodeHexString(md5(data));
+ }
+
+ /**
+ * Calculates the MD5 digest and returns the value as a 32 character hex string.
+ *
+ * @param data
+ * Data to digest
+ * @return MD5 digest as a hex string
+ * @throws IOException
+ * On error reading from the stream
+ * @since 1.4
+ */
+ public static String md5Hex(InputStream data) throws IOException {
+ return Hex.encodeHexString(md5(data));
+ }
+
+ /**
+ * Calculates the MD5 digest and returns the value as a 32 character hex string.
+ *
+ * @param data
+ * Data to digest
+ * @return MD5 digest as a hex string
+ */
+ public static String md5Hex(String data) {
+ return Hex.encodeHexString(md5(data));
+ }
+
+ /**
+ * Calculates the SHA-1 digest and returns the value as a <code>byte[]</code>.
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-1 digest
+ */
+ public static byte[] sha(byte[] data) {
+ return getShaDigest().digest(data);
+ }
+
+ /**
+ * Calculates the SHA-1 digest and returns the value as a <code>byte[]</code>.
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-1 digest
+ * @throws IOException
+ * On error reading from the stream
+ * @since 1.4
+ */
+ public static byte[] sha(InputStream data) throws IOException {
+ return digest(getShaDigest(), data);
+ }
+
+ /**
+ * Calculates the SHA-1 digest and returns the value as a <code>byte[]</code>.
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-1 digest
+ */
+ public static byte[] sha(String data) {
+ return sha(getBytesUtf8(data));
+ }
+
+ /**
+ * Calculates the SHA-256 digest and returns the value as a <code>byte[]</code>.
+ * <p>
+ * Throws a <code>RuntimeException</code> on JRE versions prior to 1.4.0.
+ * </p>
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-256 digest
+ * @since 1.4
+ */
+ public static byte[] sha256(byte[] data) {
+ return getSha256Digest().digest(data);
+ }
+
+ /**
+ * Calculates the SHA-256 digest and returns the value as a <code>byte[]</code>.
+ * <p>
+ * Throws a <code>RuntimeException</code> on JRE versions prior to 1.4.0.
+ * </p>
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-256 digest
+ * @throws IOException
+ * On error reading from the stream
+ * @since 1.4
+ */
+ public static byte[] sha256(InputStream data) throws IOException {
+ return digest(getSha256Digest(), data);
+ }
+
+ /**
+ * Calculates the SHA-256 digest and returns the value as a <code>byte[]</code>.
+ * <p>
+ * Throws a <code>RuntimeException</code> on JRE versions prior to 1.4.0.
+ * </p>
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-256 digest
+ * @since 1.4
+ */
+ public static byte[] sha256(String data) {
+ return sha256(getBytesUtf8(data));
+ }
+
+ /**
+ * Calculates the SHA-256 digest and returns the value as a hex string.
+ * <p>
+ * Throws a <code>RuntimeException</code> on JRE versions prior to 1.4.0.
+ * </p>
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-256 digest as a hex string
+ * @since 1.4
+ */
+ public static String sha256Hex(byte[] data) {
+ return Hex.encodeHexString(sha256(data));
+ }
+
+ /**
+ * Calculates the SHA-256 digest and returns the value as a hex string.
+ * <p>
+ * Throws a <code>RuntimeException</code> on JRE versions prior to 1.4.0.
+ * </p>
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-256 digest as a hex string
+ * @throws IOException
+ * On error reading from the stream
+ * @since 1.4
+ */
+ public static String sha256Hex(InputStream data) throws IOException {
+ return Hex.encodeHexString(sha256(data));
+ }
+
+ /**
+ * Calculates the SHA-256 digest and returns the value as a hex string.
+ * <p>
+ * Throws a <code>RuntimeException</code> on JRE versions prior to 1.4.0.
+ * </p>
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-256 digest as a hex string
+ * @since 1.4
+ */
+ public static String sha256Hex(String data) {
+ return Hex.encodeHexString(sha256(data));
+ }
+
+ /**
+ * Calculates the SHA-384 digest and returns the value as a <code>byte[]</code>.
+ * <p>
+ * Throws a <code>RuntimeException</code> on JRE versions prior to 1.4.0.
+ * </p>
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-384 digest
+ * @since 1.4
+ */
+ public static byte[] sha384(byte[] data) {
+ return getSha384Digest().digest(data);
+ }
+
+ /**
+ * Calculates the SHA-384 digest and returns the value as a <code>byte[]</code>.
+ * <p>
+ * Throws a <code>RuntimeException</code> on JRE versions prior to 1.4.0.
+ * </p>
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-384 digest
+ * @throws IOException
+ * On error reading from the stream
+ * @since 1.4
+ */
+ public static byte[] sha384(InputStream data) throws IOException {
+ return digest(getSha384Digest(), data);
+ }
+
+ /**
+ * Calculates the SHA-384 digest and returns the value as a <code>byte[]</code>.
+ * <p>
+ * Throws a <code>RuntimeException</code> on JRE versions prior to 1.4.0.
+ * </p>
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-384 digest
+ * @since 1.4
+ */
+ public static byte[] sha384(String data) {
+ return sha384(getBytesUtf8(data));
+ }
+
+ /**
+ * Calculates the SHA-384 digest and returns the value as a hex string.
+ * <p>
+ * Throws a <code>RuntimeException</code> on JRE versions prior to 1.4.0.
+ * </p>
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-384 digest as a hex string
+ * @since 1.4
+ */
+ public static String sha384Hex(byte[] data) {
+ return Hex.encodeHexString(sha384(data));
+ }
+
+ /**
+ * Calculates the SHA-384 digest and returns the value as a hex string.
+ * <p>
+ * Throws a <code>RuntimeException</code> on JRE versions prior to 1.4.0.
+ * </p>
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-384 digest as a hex string
+ * @throws IOException
+ * On error reading from the stream
+ * @since 1.4
+ */
+ public static String sha384Hex(InputStream data) throws IOException {
+ return Hex.encodeHexString(sha384(data));
+ }
+
+ /**
+ * Calculates the SHA-384 digest and returns the value as a hex string.
+ * <p>
+ * Throws a <code>RuntimeException</code> on JRE versions prior to 1.4.0.
+ * </p>
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-384 digest as a hex string
+ * @since 1.4
+ */
+ public static String sha384Hex(String data) {
+ return Hex.encodeHexString(sha384(data));
+ }
+
+ /**
+ * Calculates the SHA-512 digest and returns the value as a <code>byte[]</code>.
+ * <p>
+ * Throws a <code>RuntimeException</code> on JRE versions prior to 1.4.0.
+ * </p>
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-512 digest
+ * @since 1.4
+ */
+ public static byte[] sha512(byte[] data) {
+ return getSha512Digest().digest(data);
+ }
+
+ /**
+ * Calculates the SHA-512 digest and returns the value as a <code>byte[]</code>.
+ * <p>
+ * Throws a <code>RuntimeException</code> on JRE versions prior to 1.4.0.
+ * </p>
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-512 digest
+ * @throws IOException
+ * On error reading from the stream
+ * @since 1.4
+ */
+ public static byte[] sha512(InputStream data) throws IOException {
+ return digest(getSha512Digest(), data);
+ }
+
+ /**
+ * Calculates the SHA-512 digest and returns the value as a <code>byte[]</code>.
+ * <p>
+ * Throws a <code>RuntimeException</code> on JRE versions prior to 1.4.0.
+ * </p>
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-512 digest
+ * @since 1.4
+ */
+ public static byte[] sha512(String data) {
+ return sha512(getBytesUtf8(data));
+ }
+
+ /**
+ * Calculates the SHA-512 digest and returns the value as a hex string.
+ * <p>
+ * Throws a <code>RuntimeException</code> on JRE versions prior to 1.4.0.
+ * </p>
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-512 digest as a hex string
+ * @since 1.4
+ */
+ public static String sha512Hex(byte[] data) {
+ return Hex.encodeHexString(sha512(data));
+ }
+
+ /**
+ * Calculates the SHA-512 digest and returns the value as a hex string.
+ * <p>
+ * Throws a <code>RuntimeException</code> on JRE versions prior to 1.4.0.
+ * </p>
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-512 digest as a hex string
+ * @throws IOException
+ * On error reading from the stream
+ * @since 1.4
+ */
+ public static String sha512Hex(InputStream data) throws IOException {
+ return Hex.encodeHexString(sha512(data));
+ }
+
+ /**
+ * Calculates the SHA-512 digest and returns the value as a hex string.
+ * <p>
+ * Throws a <code>RuntimeException</code> on JRE versions prior to 1.4.0.
+ * </p>
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-512 digest as a hex string
+ * @since 1.4
+ */
+ public static String sha512Hex(String data) {
+ return Hex.encodeHexString(sha512(data));
+ }
+
+ /**
+ * Calculates the SHA-1 digest and returns the value as a hex string.
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-1 digest as a hex string
+ */
+ public static String shaHex(byte[] data) {
+ return Hex.encodeHexString(sha(data));
+ }
+
+ /**
+ * Calculates the SHA-1 digest and returns the value as a hex string.
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-1 digest as a hex string
+ * @throws IOException
+ * On error reading from the stream
+ * @since 1.4
+ */
+ public static String shaHex(InputStream data) throws IOException {
+ return Hex.encodeHexString(sha(data));
+ }
+
+ /**
+ * Calculates the SHA-1 digest and returns the value as a hex string.
+ *
+ * @param data
+ * Data to digest
+ * @return SHA-1 digest as a hex string
+ */
+ public static String shaHex(String data) {
+ return Hex.encodeHexString(sha(data));
+ }
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/digest/package.html b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/digest/package.html
new file mode 100644
index 0000000000..1da9762769
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/digest/package.html
@@ -0,0 +1,21 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<html>
+ <body>
+ Simplifies common {@link java.security.MessageDigest} tasks.
+ </body>
+</html>
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/AbstractCaverphone.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/AbstractCaverphone.java
new file mode 100644
index 0000000000..fbcc75943d
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/AbstractCaverphone.java
@@ -0,0 +1,78 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.language;
+
+import org.mozilla.apache.commons.codec.EncoderException;
+import org.mozilla.apache.commons.codec.StringEncoder;
+
+/**
+ * Encodes a string into a Caverphone value.
+ *
+ * This is an algorithm created by the Caversham Project at the University of Otago. It implements the Caverphone 2.0
+ * algorithm:
+ *
+ * @author Apache Software Foundation
+ * @version $Id: Caverphone.java 1075947 2011-03-01 17:56:14Z ggregory $
+ * @see <a href="http://en.wikipedia.org/wiki/Caverphone">Wikipedia - Caverphone</a>
+ * @since 1.5
+ */
+public abstract class AbstractCaverphone implements StringEncoder {
+
+ /**
+ * Creates an instance of the Caverphone encoder
+ */
+ public AbstractCaverphone() {
+ super();
+ }
+
+ /**
+ * Encodes an Object using the caverphone algorithm. This method is provided in order to satisfy the requirements of
+ * the Encoder interface, and will throw an EncoderException if the supplied object is not of type java.lang.String.
+ *
+ * @param source
+ * Object to encode
+ * @return An object (or type java.lang.String) containing the caverphone code which corresponds to the String
+ * supplied.
+ * @throws EncoderException
+ * if the parameter supplied is not of type java.lang.String
+ */
+ public Object encode(Object source) throws EncoderException {
+ if (!(source instanceof String)) {
+ throw new EncoderException("Parameter supplied to Caverphone encode is not of type java.lang.String");
+ }
+ return this.encode((String) source);
+ }
+
+ /**
+ * Tests if the encodings of two strings are equal.
+ *
+ * This method might be promoted to a new AbstractStringEncoder superclass.
+ *
+ * @param str1
+ * First of two strings to compare
+ * @param str2
+ * Second of two strings to compare
+ * @return <code>true</code> if the encodings of these strings are identical, <code>false</code> otherwise.
+ * @throws EncoderException
+ */
+ public boolean isEncodeEqual(String str1, String str2) throws EncoderException {
+ return this.encode(str1).equals(this.encode(str2));
+ }
+
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/Caverphone.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/Caverphone.java
new file mode 100644
index 0000000000..062fa4135d
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/Caverphone.java
@@ -0,0 +1,104 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.language;
+
+import org.mozilla.apache.commons.codec.EncoderException;
+import org.mozilla.apache.commons.codec.StringEncoder;
+
+/**
+ * Encodes a string into a Caverphone 2.0 value. Delegate to a {@link Caverphone2} instance.
+ *
+ * This is an algorithm created by the Caversham Project at the University of Otago. It implements the Caverphone 2.0
+ * algorithm:
+ *
+ * @author Apache Software Foundation
+ * @version $Id: Caverphone.java 1079535 2011-03-08 20:54:37Z ggregory $
+ * @see <a href="http://en.wikipedia.org/wiki/Caverphone">Wikipedia - Caverphone</a>
+ * @see <a href="http://caversham.otago.ac.nz/files/working/ctp150804.pdf">Caverphone 2.0 specification</a>
+ * @since 1.4
+ * @deprecated 1.5 Replaced by {@link Caverphone2}, will be removed in 2.0.
+ */
+public class Caverphone implements StringEncoder {
+
+ /**
+ * Delegate to a {@link Caverphone2} instance to avoid code duplication.
+ */
+ final private Caverphone2 encoder = new Caverphone2();
+
+ /**
+ * Creates an instance of the Caverphone encoder
+ */
+ public Caverphone() {
+ super();
+ }
+
+ /**
+ * Encodes the given String into a Caverphone value.
+ *
+ * @param source
+ * String the source string
+ * @return A caverphone code for the given String
+ */
+ public String caverphone(String source) {
+ return this.encoder.encode(source);
+ }
+
+ /**
+ * Encodes an Object using the caverphone algorithm. This method is provided in order to satisfy the requirements of
+ * the Encoder interface, and will throw an EncoderException if the supplied object is not of type java.lang.String.
+ *
+ * @param pObject
+ * Object to encode
+ * @return An object (or type java.lang.String) containing the caverphone code which corresponds to the String
+ * supplied.
+ * @throws EncoderException
+ * if the parameter supplied is not of type java.lang.String
+ */
+ public Object encode(Object pObject) throws EncoderException {
+ if (!(pObject instanceof String)) {
+ throw new EncoderException("Parameter supplied to Caverphone encode is not of type java.lang.String");
+ }
+ return this.caverphone((String) pObject);
+ }
+
+ /**
+ * Encodes a String using the Caverphone algorithm.
+ *
+ * @param pString
+ * String object to encode
+ * @return The caverphone code corresponding to the String supplied
+ */
+ public String encode(String pString) {
+ return this.caverphone(pString);
+ }
+
+ /**
+ * Tests if the caverphones of two strings are identical.
+ *
+ * @param str1
+ * First of two strings to compare
+ * @param str2
+ * Second of two strings to compare
+ * @return <code>true</code> if the caverphones of these strings are identical, <code>false</code> otherwise.
+ */
+ public boolean isCaverphoneEqual(String str1, String str2) {
+ return this.caverphone(str1).equals(this.caverphone(str2));
+ }
+
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/Caverphone1.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/Caverphone1.java
new file mode 100644
index 0000000000..e0d2e1de36
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/Caverphone1.java
@@ -0,0 +1,126 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.language;
+
+/**
+ * Encodes a string into a Caverphone 1.0 value.
+ *
+ * This is an algorithm created by the Caversham Project at the University of Otago. It implements the Caverphone 1.0
+ * algorithm:
+ *
+ * @author Apache Software Foundation
+ * @version $Id: Caverphone.java 1075947 2011-03-01 17:56:14Z ggregory $
+ * @see <a href="http://en.wikipedia.org/wiki/Caverphone">Wikipedia - Caverphone</a>
+ * @see <a href="http://caversham.otago.ac.nz/files/working/ctp060902.pdf">Caverphone 1.0 specification</a>
+ * @since 1.5
+ */
+public class Caverphone1 extends AbstractCaverphone {
+
+ private static final String SIX_1 = "111111";
+
+ /**
+ * Encodes the given String into a Caverphone value.
+ *
+ * @param source
+ * String the source string
+ * @return A caverphone code for the given String
+ */
+ public String encode(String source) {
+ String txt = source;
+ if (txt == null || txt.length() == 0) {
+ return SIX_1;
+ }
+
+ // 1. Convert to lowercase
+ txt = txt.toLowerCase(java.util.Locale.ENGLISH);
+
+ // 2. Remove anything not A-Z
+ txt = txt.replaceAll("[^a-z]", "");
+
+ // 3. Handle various start options
+ // 2 is a temporary placeholder to indicate a consonant which we are no longer interested in.
+ txt = txt.replaceAll("^cough", "cou2f");
+ txt = txt.replaceAll("^rough", "rou2f");
+ txt = txt.replaceAll("^tough", "tou2f");
+ txt = txt.replaceAll("^enough", "enou2f");
+ txt = txt.replaceAll("^gn", "2n");
+
+ // End
+ txt = txt.replaceAll("mb$", "m2");
+
+ // 4. Handle replacements
+ txt = txt.replaceAll("cq", "2q");
+ txt = txt.replaceAll("ci", "si");
+ txt = txt.replaceAll("ce", "se");
+ txt = txt.replaceAll("cy", "sy");
+ txt = txt.replaceAll("tch", "2ch");
+ txt = txt.replaceAll("c", "k");
+ txt = txt.replaceAll("q", "k");
+ txt = txt.replaceAll("x", "k");
+ txt = txt.replaceAll("v", "f");
+ txt = txt.replaceAll("dg", "2g");
+ txt = txt.replaceAll("tio", "sio");
+ txt = txt.replaceAll("tia", "sia");
+ txt = txt.replaceAll("d", "t");
+ txt = txt.replaceAll("ph", "fh");
+ txt = txt.replaceAll("b", "p");
+ txt = txt.replaceAll("sh", "s2");
+ txt = txt.replaceAll("z", "s");
+ txt = txt.replaceAll("^[aeiou]", "A");
+ // 3 is a temporary placeholder marking a vowel
+ txt = txt.replaceAll("[aeiou]", "3");
+ txt = txt.replaceAll("3gh3", "3kh3");
+ txt = txt.replaceAll("gh", "22");
+ txt = txt.replaceAll("g", "k");
+ txt = txt.replaceAll("s+", "S");
+ txt = txt.replaceAll("t+", "T");
+ txt = txt.replaceAll("p+", "P");
+ txt = txt.replaceAll("k+", "K");
+ txt = txt.replaceAll("f+", "F");
+ txt = txt.replaceAll("m+", "M");
+ txt = txt.replaceAll("n+", "N");
+ txt = txt.replaceAll("w3", "W3");
+ txt = txt.replaceAll("wy", "Wy"); // 1.0 only
+ txt = txt.replaceAll("wh3", "Wh3");
+ txt = txt.replaceAll("why", "Why"); // 1.0 only
+ txt = txt.replaceAll("w", "2");
+ txt = txt.replaceAll("^h", "A");
+ txt = txt.replaceAll("h", "2");
+ txt = txt.replaceAll("r3", "R3");
+ txt = txt.replaceAll("ry", "Ry"); // 1.0 only
+ txt = txt.replaceAll("r", "2");
+ txt = txt.replaceAll("l3", "L3");
+ txt = txt.replaceAll("ly", "Ly"); // 1.0 only
+ txt = txt.replaceAll("l", "2");
+ txt = txt.replaceAll("j", "y"); // 1.0 only
+ txt = txt.replaceAll("y3", "Y3"); // 1.0 only
+ txt = txt.replaceAll("y", "2"); // 1.0 only
+
+ // 5. Handle removals
+ txt = txt.replaceAll("2", "");
+ txt = txt.replaceAll("3", "");
+
+ // 6. put ten 1s on the end
+ txt = txt + SIX_1;
+
+ // 7. take the first six characters as the code
+ return txt.substring(0, SIX_1.length());
+ }
+
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/Caverphone2.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/Caverphone2.java
new file mode 100644
index 0000000000..a05b560e77
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/Caverphone2.java
@@ -0,0 +1,129 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.language;
+
+/**
+ * Encodes a string into a Caverphone 2.0 value.
+ *
+ * This is an algorithm created by the Caversham Project at the University of Otago. It implements the Caverphone 2.0
+ * algorithm:
+ *
+ * @author Apache Software Foundation
+ * @version $Id: Caverphone.java 1075947 2011-03-01 17:56:14Z ggregory $
+ * @see <a href="http://en.wikipedia.org/wiki/Caverphone">Wikipedia - Caverphone</a>
+ * @see <a href="http://caversham.otago.ac.nz/files/working/ctp150804.pdf">Caverphone 2.0 specification</a>
+ * @since 1.5
+ */
+public class Caverphone2 extends AbstractCaverphone {
+
+ private static final String TEN_1 = "1111111111";
+
+ /**
+ * Encodes the given String into a Caverphone 2.0 value.
+ *
+ * @param source
+ * String the source string
+ * @return A caverphone code for the given String
+ */
+ public String encode(String source) {
+ String txt = source;
+ if (txt == null || txt.length() == 0) {
+ return TEN_1;
+ }
+
+ // 1. Convert to lowercase
+ txt = txt.toLowerCase(java.util.Locale.ENGLISH);
+
+ // 2. Remove anything not A-Z
+ txt = txt.replaceAll("[^a-z]", "");
+
+ // 2.5. Remove final e
+ txt = txt.replaceAll("e$", ""); // 2.0 only
+
+ // 3. Handle various start options
+ txt = txt.replaceAll("^cough", "cou2f");
+ txt = txt.replaceAll("^rough", "rou2f");
+ txt = txt.replaceAll("^tough", "tou2f");
+ txt = txt.replaceAll("^enough", "enou2f"); // 2.0 only
+ txt = txt.replaceAll("^trough", "trou2f"); // 2.0 only - note the spec says ^enough here again, c+p error I assume
+ txt = txt.replaceAll("^gn", "2n");
+
+ // End
+ txt = txt.replaceAll("mb$", "m2");
+
+ // 4. Handle replacements
+ txt = txt.replaceAll("cq", "2q");
+ txt = txt.replaceAll("ci", "si");
+ txt = txt.replaceAll("ce", "se");
+ txt = txt.replaceAll("cy", "sy");
+ txt = txt.replaceAll("tch", "2ch");
+ txt = txt.replaceAll("c", "k");
+ txt = txt.replaceAll("q", "k");
+ txt = txt.replaceAll("x", "k");
+ txt = txt.replaceAll("v", "f");
+ txt = txt.replaceAll("dg", "2g");
+ txt = txt.replaceAll("tio", "sio");
+ txt = txt.replaceAll("tia", "sia");
+ txt = txt.replaceAll("d", "t");
+ txt = txt.replaceAll("ph", "fh");
+ txt = txt.replaceAll("b", "p");
+ txt = txt.replaceAll("sh", "s2");
+ txt = txt.replaceAll("z", "s");
+ txt = txt.replaceAll("^[aeiou]", "A");
+ txt = txt.replaceAll("[aeiou]", "3");
+ txt = txt.replaceAll("j", "y"); // 2.0 only
+ txt = txt.replaceAll("^y3", "Y3"); // 2.0 only
+ txt = txt.replaceAll("^y", "A"); // 2.0 only
+ txt = txt.replaceAll("y", "3"); // 2.0 only
+ txt = txt.replaceAll("3gh3", "3kh3");
+ txt = txt.replaceAll("gh", "22");
+ txt = txt.replaceAll("g", "k");
+ txt = txt.replaceAll("s+", "S");
+ txt = txt.replaceAll("t+", "T");
+ txt = txt.replaceAll("p+", "P");
+ txt = txt.replaceAll("k+", "K");
+ txt = txt.replaceAll("f+", "F");
+ txt = txt.replaceAll("m+", "M");
+ txt = txt.replaceAll("n+", "N");
+ txt = txt.replaceAll("w3", "W3");
+ txt = txt.replaceAll("wh3", "Wh3");
+ txt = txt.replaceAll("w$", "3"); // 2.0 only
+ txt = txt.replaceAll("w", "2");
+ txt = txt.replaceAll("^h", "A");
+ txt = txt.replaceAll("h", "2");
+ txt = txt.replaceAll("r3", "R3");
+ txt = txt.replaceAll("r$", "3"); // 2.0 only
+ txt = txt.replaceAll("r", "2");
+ txt = txt.replaceAll("l3", "L3");
+ txt = txt.replaceAll("l$", "3"); // 2.0 only
+ txt = txt.replaceAll("l", "2");
+
+ // 5. Handle removals
+ txt = txt.replaceAll("2", "");
+ txt = txt.replaceAll("3$", "A"); // 2.0 only
+ txt = txt.replaceAll("3", "");
+
+ // 6. put ten 1s on the end
+ txt = txt + TEN_1;
+
+ // 7. take the first ten characters as the code
+ return txt.substring(0, TEN_1.length());
+ }
+
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/ColognePhonetic.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/ColognePhonetic.java
new file mode 100644
index 0000000000..ae62857903
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/ColognePhonetic.java
@@ -0,0 +1,417 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.language;
+
+import java.util.Locale;
+
+import org.mozilla.apache.commons.codec.EncoderException;
+import org.mozilla.apache.commons.codec.StringEncoder;
+
+/**
+ * <p>
+ * Encodes a string into a Cologne Phonetic value.
+ * </p>
+ * <p>
+ * Implements the <a href="http://de.wikipedia.org/wiki/K%C3%B6lner_Phonetik">“Kölner Phoneticâ€</a> (Cologne Phonetic)
+ * algorithm issued by Hans Joachim Postel in 1969.
+ * </p>
+ *
+ * <p>
+ * The <i>Kölner Phonetik</i> is a phonetic algorithm which is optimized for the German language. It is related to the
+ * well-known soundex algorithm.
+ * </p>
+ *
+ * <h2>Algorithm</h2>
+ *
+ * <ul>
+ *
+ * <li>
+ * <h3>Step 1:</h3>
+ * After preprocessing (convertion to upper case, transcription of <a
+ * href="http://en.wikipedia.org/wiki/Germanic_umlaut">germanic umlauts</a>, removal of non alphabetical characters) the
+ * letters of the supplied text are replaced by their phonetic code according to the folowing table.
+ * <table border="1">
+ * <tbody>
+ * <tr>
+ * <th>Letter</th>
+ * <th>Context</th>
+ * <th align="center">Code</th>
+ * </tr>
+ * <tr>
+ * <td>A, E, I, J, O, U, Y</td>
+ * <td></td>
+ * <td align="center">0</td>
+ * </tr>
+ * <tr>
+ *
+ * <td>H</td>
+ * <td></td>
+ * <td align="center">-</td>
+ * </tr>
+ * <tr>
+ * <td>B</td>
+ * <td></td>
+ * <td rowspan="2" align="center">1</td>
+ * </tr>
+ * <tr>
+ * <td>P</td>
+ * <td>not before H</td>
+ *
+ * </tr>
+ * <tr>
+ * <td>D, T</td>
+ * <td>not before C, S, Z</td>
+ * <td align="center">2</td>
+ * </tr>
+ * <tr>
+ * <td>F, V, W</td>
+ * <td></td>
+ * <td rowspan="2" align="center">3</td>
+ * </tr>
+ * <tr>
+ *
+ * <td>P</td>
+ * <td>before H</td>
+ * </tr>
+ * <tr>
+ * <td>G, K, Q</td>
+ * <td></td>
+ * <td rowspan="3" align="center">4</td>
+ * </tr>
+ * <tr>
+ * <td rowspan="2">C</td>
+ * <td>at onset before A, H, K, L, O, Q, R, U, X</td>
+ *
+ * </tr>
+ * <tr>
+ * <td>before A, H, K, O, Q, U, X except after S, Z</td>
+ * </tr>
+ * <tr>
+ * <td>X</td>
+ * <td>not after C, K, Q</td>
+ * <td align="center">48</td>
+ * </tr>
+ * <tr>
+ * <td>L</td>
+ * <td></td>
+ *
+ * <td align="center">5</td>
+ * </tr>
+ * <tr>
+ * <td>M, N</td>
+ * <td></td>
+ * <td align="center">6</td>
+ * </tr>
+ * <tr>
+ * <td>R</td>
+ * <td></td>
+ * <td align="center">7</td>
+ * </tr>
+ *
+ * <tr>
+ * <td>S, Z</td>
+ * <td></td>
+ * <td rowspan="6" align="center">8</td>
+ * </tr>
+ * <tr>
+ * <td rowspan="3">C</td>
+ * <td>after S, Z</td>
+ * </tr>
+ * <tr>
+ * <td>at onset except before A, H, K, L, O, Q, R, U, X</td>
+ * </tr>
+ *
+ * <tr>
+ * <td>not before A, H, K, O, Q, U, X</td>
+ * </tr>
+ * <tr>
+ * <td>D, T</td>
+ * <td>before C, S, Z</td>
+ * </tr>
+ * <tr>
+ * <td>X</td>
+ * <td>after C, K, Q</td>
+ * </tr>
+ * </tbody>
+ * </table>
+ * <p>
+ * <small><i>(Source: <a href= "http://de.wikipedia.org/wiki/K%C3%B6lner_Phonetik#Buchstabencodes" >Wikipedia (de):
+ * Kölner Phonetik – Buchstabencodes</a>)</i></small>
+ * </p>
+ *
+ * <h4>Example:</h4>
+ *
+ * {@code "Müller-Lüdenscheidt" => "MULLERLUDENSCHEIDT" => "6005507500206880022"}
+ *
+ * </li>
+ *
+ * <li>
+ * <h3>Step 2:</h3>
+ * Collapse of all multiple consecutive code digits.
+ * <h4>Example:</h4>
+ * {@code "6005507500206880022" => "6050750206802"}</li>
+ *
+ * <li>
+ * <h3>Step 3:</h3>
+ * Removal of all codes “0†except at the beginning. This means that two or more identical consecutive digits can occur
+ * if they occur after removing the "0" digits.
+ *
+ * <h4>Example:</h4>
+ * {@code "6050750206802" => "65752682"}</li>
+ *
+ * </ul>
+ *
+ * @see <a href="http://de.wikipedia.org/wiki/K%C3%B6lner_Phonetik">Wikipedia (de): Kölner Phonetik (in German)</a>
+ * @author Apache Software Foundation
+ * @since 1.5
+ */
+public class ColognePhonetic implements StringEncoder {
+
+ private abstract class CologneBuffer {
+
+ protected final char[] data;
+
+ protected int length = 0;
+
+ public CologneBuffer(char[] data) {
+ this.data = data;
+ this.length = data.length;
+ }
+
+ public CologneBuffer(int buffSize) {
+ this.data = new char[buffSize];
+ this.length = 0;
+ }
+
+ protected abstract char[] copyData(int start, final int length);
+
+ public int length() {
+ return length;
+ }
+
+ public String toString() {
+ return new String(copyData(0, length));
+ }
+ }
+
+ private class CologneOutputBuffer extends CologneBuffer {
+
+ public CologneOutputBuffer(int buffSize) {
+ super(buffSize);
+ }
+
+ public void addRight(char chr) {
+ data[length] = chr;
+ length++;
+ }
+
+ protected char[] copyData(int start, final int length) {
+ char[] newData = new char[length];
+ System.arraycopy(data, start, newData, 0, length);
+ return newData;
+ }
+ }
+
+ private class CologneInputBuffer extends CologneBuffer {
+
+ public CologneInputBuffer(char[] data) {
+ super(data);
+ }
+
+ public void addLeft(char ch) {
+ length++;
+ data[getNextPos()] = ch;
+ }
+
+ protected char[] copyData(int start, final int length) {
+ char[] newData = new char[length];
+ System.arraycopy(data, data.length - this.length + start, newData, 0, length);
+ return newData;
+ }
+
+ public char getNextChar() {
+ return data[getNextPos()];
+ }
+
+ protected int getNextPos() {
+ return data.length - length;
+ }
+
+ public char removeNext() {
+ char ch = getNextChar();
+ length--;
+ return ch;
+ }
+ }
+
+ private static final char[][] PREPROCESS_MAP = new char[][]{{'\u00C4', 'A'}, // Ä
+ {'\u00DC', 'U'}, // Ü
+ {'\u00D6', 'O'}, // Ö
+ {'\u00DF', 'S'} // ß
+ };
+
+ /*
+ * Returns whether the array contains the key, or not.
+ */
+ private static boolean arrayContains(char[] arr, char key) {
+ for (int i = 0; i < arr.length; i++) {
+ if (arr[i] == key) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * <p>
+ * <b>colognePhonetic()</b> is the actual implementations of the <i>Kölner Phonetik</i> algorithm.
+ * </p>
+ * <p>
+ * In contrast to the initial description of the algorithm, this implementation does the encoding in one pass.
+ * </p>
+ *
+ * @param text
+ * @return the corresponding encoding according to the <i>Kölner Phonetik</i> algorithm
+ */
+ public String colognePhonetic(String text) {
+ if (text == null) {
+ return null;
+ }
+
+ text = preprocess(text);
+
+ CologneOutputBuffer output = new CologneOutputBuffer(text.length() * 2);
+ CologneInputBuffer input = new CologneInputBuffer(text.toCharArray());
+
+ char nextChar;
+
+ char lastChar = '-';
+ char lastCode = '/';
+ char code;
+ char chr;
+
+ int rightLength = input.length();
+
+ while (rightLength > 0) {
+ chr = input.removeNext();
+
+ if ((rightLength = input.length()) > 0) {
+ nextChar = input.getNextChar();
+ } else {
+ nextChar = '-';
+ }
+
+ if (arrayContains(new char[]{'A', 'E', 'I', 'J', 'O', 'U', 'Y'}, chr)) {
+ code = '0';
+ } else if (chr == 'H' || chr < 'A' || chr > 'Z') {
+ if (lastCode == '/') {
+ continue;
+ }
+ code = '-';
+ } else if (chr == 'B' || (chr == 'P' && nextChar != 'H')) {
+ code = '1';
+ } else if ((chr == 'D' || chr == 'T') && !arrayContains(new char[]{'S', 'C', 'Z'}, nextChar)) {
+ code = '2';
+ } else if (arrayContains(new char[]{'W', 'F', 'P', 'V'}, chr)) {
+ code = '3';
+ } else if (arrayContains(new char[]{'G', 'K', 'Q'}, chr)) {
+ code = '4';
+ } else if (chr == 'X' && !arrayContains(new char[]{'C', 'K', 'Q'}, lastChar)) {
+ code = '4';
+ input.addLeft('S');
+ rightLength++;
+ } else if (chr == 'S' || chr == 'Z') {
+ code = '8';
+ } else if (chr == 'C') {
+ if (lastCode == '/') {
+ if (arrayContains(new char[]{'A', 'H', 'K', 'L', 'O', 'Q', 'R', 'U', 'X'}, nextChar)) {
+ code = '4';
+ } else {
+ code = '8';
+ }
+ } else {
+ if (arrayContains(new char[]{'S', 'Z'}, lastChar) ||
+ !arrayContains(new char[]{'A', 'H', 'O', 'U', 'K', 'Q', 'X'}, nextChar)) {
+ code = '8';
+ } else {
+ code = '4';
+ }
+ }
+ } else if (arrayContains(new char[]{'T', 'D', 'X'}, chr)) {
+ code = '8';
+ } else if (chr == 'R') {
+ code = '7';
+ } else if (chr == 'L') {
+ code = '5';
+ } else if (chr == 'M' || chr == 'N') {
+ code = '6';
+ } else {
+ code = chr;
+ }
+
+ if (code != '-' && (lastCode != code && (code != '0' || lastCode == '/') || code < '0' || code > '8')) {
+ output.addRight(code);
+ }
+
+ lastChar = chr;
+ lastCode = code;
+ }
+ return output.toString();
+ }
+
+ public Object encode(Object object) throws EncoderException {
+ if (!(object instanceof String)) {
+ throw new EncoderException("This method’s parameter was expected to be of the type " +
+ String.class.getName() +
+ ". But actually it was of the type " +
+ object.getClass().getName() +
+ ".");
+ }
+ return encode((String) object);
+ }
+
+ public String encode(String text) {
+ return colognePhonetic(text);
+ }
+
+ public boolean isEncodeEqual(String text1, String text2) {
+ return colognePhonetic(text1).equals(colognePhonetic(text2));
+ }
+
+ /*
+ * Converts the string to upper case and replaces germanic umlauts, and the “ßâ€.
+ */
+ private String preprocess(String text) {
+ text = text.toUpperCase(Locale.GERMAN);
+
+ char[] chrs = text.toCharArray();
+
+ for (int index = 0; index < chrs.length; index++) {
+ if (chrs[index] > 'Z') {
+ for (int replacement = 0; replacement < PREPROCESS_MAP.length; replacement++) {
+ if (chrs[index] == PREPROCESS_MAP[replacement][0]) {
+ chrs[index] = PREPROCESS_MAP[replacement][1];
+ break;
+ }
+ }
+ }
+ }
+ return new String(chrs);
+ }
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/DoubleMetaphone.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/DoubleMetaphone.java
new file mode 100644
index 0000000000..5069ade72c
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/DoubleMetaphone.java
@@ -0,0 +1,1106 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.language;
+
+import org.mozilla.apache.commons.codec.EncoderException;
+import org.mozilla.apache.commons.codec.StringEncoder;
+
+/**
+ * Encodes a string into a double metaphone value.
+ * This Implementation is based on the algorithm by <CITE>Lawrence Philips</CITE>.
+ * <ul>
+ * <li>Original Article: <a
+ * href="http://www.cuj.com/documents/s=8038/cuj0006philips/">
+ * http://www.cuj.com/documents/s=8038/cuj0006philips/</a></li>
+ * <li>Original Source Code: <a href="ftp://ftp.cuj.com/pub/2000/1806/philips.zip">
+ * ftp://ftp.cuj.com/pub/2000/1806/philips.zip</a></li>
+ * </ul>
+ *
+ * @author Apache Software Foundation
+ * @version $Id: DoubleMetaphone.java 1072742 2011-02-20 21:39:03Z ggregory $
+ */
+public class DoubleMetaphone implements StringEncoder {
+
+ /**
+ * "Vowels" to test for
+ */
+ private static final String VOWELS = "AEIOUY";
+
+ /**
+ * Prefixes when present which are not pronounced
+ */
+ private static final String[] SILENT_START =
+ { "GN", "KN", "PN", "WR", "PS" };
+ private static final String[] L_R_N_M_B_H_F_V_W_SPACE =
+ { "L", "R", "N", "M", "B", "H", "F", "V", "W", " " };
+ private static final String[] ES_EP_EB_EL_EY_IB_IL_IN_IE_EI_ER =
+ { "ES", "EP", "EB", "EL", "EY", "IB", "IL", "IN", "IE", "EI", "ER" };
+ private static final String[] L_T_K_S_N_M_B_Z =
+ { "L", "T", "K", "S", "N", "M", "B", "Z" };
+
+ /**
+ * Maximum length of an encoding, default is 4
+ */
+ private int maxCodeLen = 4;
+
+ /**
+ * Creates an instance of this DoubleMetaphone encoder
+ */
+ public DoubleMetaphone() {
+ super();
+ }
+
+ /**
+ * Encode a value with Double Metaphone
+ *
+ * @param value String to encode
+ * @return an encoded string
+ */
+ public String doubleMetaphone(String value) {
+ return doubleMetaphone(value, false);
+ }
+
+ /**
+ * Encode a value with Double Metaphone, optionally using the alternate
+ * encoding.
+ *
+ * @param value String to encode
+ * @param alternate use alternate encode
+ * @return an encoded string
+ */
+ public String doubleMetaphone(String value, boolean alternate) {
+ value = cleanInput(value);
+ if (value == null) {
+ return null;
+ }
+
+ boolean slavoGermanic = isSlavoGermanic(value);
+ int index = isSilentStart(value) ? 1 : 0;
+
+ DoubleMetaphoneResult result = new DoubleMetaphoneResult(this.getMaxCodeLen());
+
+ while (!result.isComplete() && index <= value.length() - 1) {
+ switch (value.charAt(index)) {
+ case 'A':
+ case 'E':
+ case 'I':
+ case 'O':
+ case 'U':
+ case 'Y':
+ index = handleAEIOUY(result, index);
+ break;
+ case 'B':
+ result.append('P');
+ index = charAt(value, index + 1) == 'B' ? index + 2 : index + 1;
+ break;
+ case '\u00C7':
+ // A C with a Cedilla
+ result.append('S');
+ index++;
+ break;
+ case 'C':
+ index = handleC(value, result, index);
+ break;
+ case 'D':
+ index = handleD(value, result, index);
+ break;
+ case 'F':
+ result.append('F');
+ index = charAt(value, index + 1) == 'F' ? index + 2 : index + 1;
+ break;
+ case 'G':
+ index = handleG(value, result, index, slavoGermanic);
+ break;
+ case 'H':
+ index = handleH(value, result, index);
+ break;
+ case 'J':
+ index = handleJ(value, result, index, slavoGermanic);
+ break;
+ case 'K':
+ result.append('K');
+ index = charAt(value, index + 1) == 'K' ? index + 2 : index + 1;
+ break;
+ case 'L':
+ index = handleL(value, result, index);
+ break;
+ case 'M':
+ result.append('M');
+ index = conditionM0(value, index) ? index + 2 : index + 1;
+ break;
+ case 'N':
+ result.append('N');
+ index = charAt(value, index + 1) == 'N' ? index + 2 : index + 1;
+ break;
+ case '\u00D1':
+ // N with a tilde (spanish ene)
+ result.append('N');
+ index++;
+ break;
+ case 'P':
+ index = handleP(value, result, index);
+ break;
+ case 'Q':
+ result.append('K');
+ index = charAt(value, index + 1) == 'Q' ? index + 2 : index + 1;
+ break;
+ case 'R':
+ index = handleR(value, result, index, slavoGermanic);
+ break;
+ case 'S':
+ index = handleS(value, result, index, slavoGermanic);
+ break;
+ case 'T':
+ index = handleT(value, result, index);
+ break;
+ case 'V':
+ result.append('F');
+ index = charAt(value, index + 1) == 'V' ? index + 2 : index + 1;
+ break;
+ case 'W':
+ index = handleW(value, result, index);
+ break;
+ case 'X':
+ index = handleX(value, result, index);
+ break;
+ case 'Z':
+ index = handleZ(value, result, index, slavoGermanic);
+ break;
+ default:
+ index++;
+ break;
+ }
+ }
+
+ return alternate ? result.getAlternate() : result.getPrimary();
+ }
+
+ /**
+ * Encode the value using DoubleMetaphone. It will only work if
+ * <code>obj</code> is a <code>String</code> (like <code>Metaphone</code>).
+ *
+ * @param obj Object to encode (should be of type String)
+ * @return An encoded Object (will be of type String)
+ * @throws EncoderException encode parameter is not of type String
+ */
+ public Object encode(Object obj) throws EncoderException {
+ if (!(obj instanceof String)) {
+ throw new EncoderException("DoubleMetaphone encode parameter is not of type String");
+ }
+ return doubleMetaphone((String) obj);
+ }
+
+ /**
+ * Encode the value using DoubleMetaphone.
+ *
+ * @param value String to encode
+ * @return An encoded String
+ */
+ public String encode(String value) {
+ return doubleMetaphone(value);
+ }
+
+ /**
+ * Check if the Double Metaphone values of two <code>String</code> values
+ * are equal.
+ *
+ * @param value1 The left-hand side of the encoded {@link String#equals(Object)}.
+ * @param value2 The right-hand side of the encoded {@link String#equals(Object)}.
+ * @return <code>true</code> if the encoded <code>String</code>s are equal;
+ * <code>false</code> otherwise.
+ * @see #isDoubleMetaphoneEqual(String,String,boolean)
+ */
+ public boolean isDoubleMetaphoneEqual(String value1, String value2) {
+ return isDoubleMetaphoneEqual(value1, value2, false);
+ }
+
+ /**
+ * Check if the Double Metaphone values of two <code>String</code> values
+ * are equal, optionally using the alternate value.
+ *
+ * @param value1 The left-hand side of the encoded {@link String#equals(Object)}.
+ * @param value2 The right-hand side of the encoded {@link String#equals(Object)}.
+ * @param alternate use the alternate value if <code>true</code>.
+ * @return <code>true</code> if the encoded <code>String</code>s are equal;
+ * <code>false</code> otherwise.
+ */
+ public boolean isDoubleMetaphoneEqual(String value1,
+ String value2,
+ boolean alternate) {
+ return doubleMetaphone(value1, alternate).equals(doubleMetaphone
+ (value2, alternate));
+ }
+
+ /**
+ * Returns the maxCodeLen.
+ * @return int
+ */
+ public int getMaxCodeLen() {
+ return this.maxCodeLen;
+ }
+
+ /**
+ * Sets the maxCodeLen.
+ * @param maxCodeLen The maxCodeLen to set
+ */
+ public void setMaxCodeLen(int maxCodeLen) {
+ this.maxCodeLen = maxCodeLen;
+ }
+
+ //-- BEGIN HANDLERS --//
+
+ /**
+ * Handles 'A', 'E', 'I', 'O', 'U', and 'Y' cases
+ */
+ private int handleAEIOUY(DoubleMetaphoneResult result, int
+ index) {
+ if (index == 0) {
+ result.append('A');
+ }
+ return index + 1;
+ }
+
+ /**
+ * Handles 'C' cases
+ */
+ private int handleC(String value,
+ DoubleMetaphoneResult result,
+ int index) {
+ if (conditionC0(value, index)) { // very confusing, moved out
+ result.append('K');
+ index += 2;
+ } else if (index == 0 && contains(value, index, 6, "CAESAR")) {
+ result.append('S');
+ index += 2;
+ } else if (contains(value, index, 2, "CH")) {
+ index = handleCH(value, result, index);
+ } else if (contains(value, index, 2, "CZ") &&
+ !contains(value, index - 2, 4, "WICZ")) {
+ //-- "Czerny" --//
+ result.append('S', 'X');
+ index += 2;
+ } else if (contains(value, index + 1, 3, "CIA")) {
+ //-- "focaccia" --//
+ result.append('X');
+ index += 3;
+ } else if (contains(value, index, 2, "CC") &&
+ !(index == 1 && charAt(value, 0) == 'M')) {
+ //-- double "cc" but not "McClelland" --//
+ return handleCC(value, result, index);
+ } else if (contains(value, index, 2, "CK", "CG", "CQ")) {
+ result.append('K');
+ index += 2;
+ } else if (contains(value, index, 2, "CI", "CE", "CY")) {
+ //-- Italian vs. English --//
+ if (contains(value, index, 3, "CIO", "CIE", "CIA")) {
+ result.append('S', 'X');
+ } else {
+ result.append('S');
+ }
+ index += 2;
+ } else {
+ result.append('K');
+ if (contains(value, index + 1, 2, " C", " Q", " G")) {
+ //-- Mac Caffrey, Mac Gregor --//
+ index += 3;
+ } else if (contains(value, index + 1, 1, "C", "K", "Q") &&
+ !contains(value, index + 1, 2, "CE", "CI")) {
+ index += 2;
+ } else {
+ index++;
+ }
+ }
+
+ return index;
+ }
+
+ /**
+ * Handles 'CC' cases
+ */
+ private int handleCC(String value,
+ DoubleMetaphoneResult result,
+ int index) {
+ if (contains(value, index + 2, 1, "I", "E", "H") &&
+ !contains(value, index + 2, 2, "HU")) {
+ //-- "bellocchio" but not "bacchus" --//
+ if ((index == 1 && charAt(value, index - 1) == 'A') ||
+ contains(value, index - 1, 5, "UCCEE", "UCCES")) {
+ //-- "accident", "accede", "succeed" --//
+ result.append("KS");
+ } else {
+ //-- "bacci", "bertucci", other Italian --//
+ result.append('X');
+ }
+ index += 3;
+ } else { // Pierce's rule
+ result.append('K');
+ index += 2;
+ }
+
+ return index;
+ }
+
+ /**
+ * Handles 'CH' cases
+ */
+ private int handleCH(String value,
+ DoubleMetaphoneResult result,
+ int index) {
+ if (index > 0 && contains(value, index, 4, "CHAE")) { // Michael
+ result.append('K', 'X');
+ return index + 2;
+ } else if (conditionCH0(value, index)) {
+ //-- Greek roots ("chemistry", "chorus", etc.) --//
+ result.append('K');
+ return index + 2;
+ } else if (conditionCH1(value, index)) {
+ //-- Germanic, Greek, or otherwise 'ch' for 'kh' sound --//
+ result.append('K');
+ return index + 2;
+ } else {
+ if (index > 0) {
+ if (contains(value, 0, 2, "MC")) {
+ result.append('K');
+ } else {
+ result.append('X', 'K');
+ }
+ } else {
+ result.append('X');
+ }
+ return index + 2;
+ }
+ }
+
+ /**
+ * Handles 'D' cases
+ */
+ private int handleD(String value,
+ DoubleMetaphoneResult result,
+ int index) {
+ if (contains(value, index, 2, "DG")) {
+ //-- "Edge" --//
+ if (contains(value, index + 2, 1, "I", "E", "Y")) {
+ result.append('J');
+ index += 3;
+ //-- "Edgar" --//
+ } else {
+ result.append("TK");
+ index += 2;
+ }
+ } else if (contains(value, index, 2, "DT", "DD")) {
+ result.append('T');
+ index += 2;
+ } else {
+ result.append('T');
+ index++;
+ }
+ return index;
+ }
+
+ /**
+ * Handles 'G' cases
+ */
+ private int handleG(String value,
+ DoubleMetaphoneResult result,
+ int index,
+ boolean slavoGermanic) {
+ if (charAt(value, index + 1) == 'H') {
+ index = handleGH(value, result, index);
+ } else if (charAt(value, index + 1) == 'N') {
+ if (index == 1 && isVowel(charAt(value, 0)) && !slavoGermanic) {
+ result.append("KN", "N");
+ } else if (!contains(value, index + 2, 2, "EY") &&
+ charAt(value, index + 1) != 'Y' && !slavoGermanic) {
+ result.append("N", "KN");
+ } else {
+ result.append("KN");
+ }
+ index = index + 2;
+ } else if (contains(value, index + 1, 2, "LI") && !slavoGermanic) {
+ result.append("KL", "L");
+ index += 2;
+ } else if (index == 0 && (charAt(value, index + 1) == 'Y' || contains(value, index + 1, 2, ES_EP_EB_EL_EY_IB_IL_IN_IE_EI_ER))) {
+ //-- -ges-, -gep-, -gel-, -gie- at beginning --//
+ result.append('K', 'J');
+ index += 2;
+ } else if ((contains(value, index + 1, 2, "ER") ||
+ charAt(value, index + 1) == 'Y') &&
+ !contains(value, 0, 6, "DANGER", "RANGER", "MANGER") &&
+ !contains(value, index - 1, 1, "E", "I") &&
+ !contains(value, index - 1, 3, "RGY", "OGY")) {
+ //-- -ger-, -gy- --//
+ result.append('K', 'J');
+ index += 2;
+ } else if (contains(value, index + 1, 1, "E", "I", "Y") ||
+ contains(value, index - 1, 4, "AGGI", "OGGI")) {
+ //-- Italian "biaggi" --//
+ if ((contains(value, 0 ,4, "VAN ", "VON ") || contains(value, 0, 3, "SCH")) || contains(value, index + 1, 2, "ET")) {
+ //-- obvious germanic --//
+ result.append('K');
+ } else if (contains(value, index + 1, 3, "IER")) {
+ result.append('J');
+ } else {
+ result.append('J', 'K');
+ }
+ index += 2;
+ } else if (charAt(value, index + 1) == 'G') {
+ index += 2;
+ result.append('K');
+ } else {
+ index++;
+ result.append('K');
+ }
+ return index;
+ }
+
+ /**
+ * Handles 'GH' cases
+ */
+ private int handleGH(String value,
+ DoubleMetaphoneResult result,
+ int index) {
+ if (index > 0 && !isVowel(charAt(value, index - 1))) {
+ result.append('K');
+ index += 2;
+ } else if (index == 0) {
+ if (charAt(value, index + 2) == 'I') {
+ result.append('J');
+ } else {
+ result.append('K');
+ }
+ index += 2;
+ } else if ((index > 1 && contains(value, index - 2, 1, "B", "H", "D")) ||
+ (index > 2 && contains(value, index - 3, 1, "B", "H", "D")) ||
+ (index > 3 && contains(value, index - 4, 1, "B", "H"))) {
+ //-- Parker's rule (with some further refinements) - "hugh"
+ index += 2;
+ } else {
+ if (index > 2 && charAt(value, index - 1) == 'U' &&
+ contains(value, index - 3, 1, "C", "G", "L", "R", "T")) {
+ //-- "laugh", "McLaughlin", "cough", "gough", "rough", "tough"
+ result.append('F');
+ } else if (index > 0 && charAt(value, index - 1) != 'I') {
+ result.append('K');
+ }
+ index += 2;
+ }
+ return index;
+ }
+
+ /**
+ * Handles 'H' cases
+ */
+ private int handleH(String value,
+ DoubleMetaphoneResult result,
+ int index) {
+ //-- only keep if first & before vowel or between 2 vowels --//
+ if ((index == 0 || isVowel(charAt(value, index - 1))) &&
+ isVowel(charAt(value, index + 1))) {
+ result.append('H');
+ index += 2;
+ //-- also takes car of "HH" --//
+ } else {
+ index++;
+ }
+ return index;
+ }
+
+ /**
+ * Handles 'J' cases
+ */
+ private int handleJ(String value, DoubleMetaphoneResult result, int index,
+ boolean slavoGermanic) {
+ if (contains(value, index, 4, "JOSE") || contains(value, 0, 4, "SAN ")) {
+ //-- obvious Spanish, "Jose", "San Jacinto" --//
+ if ((index == 0 && (charAt(value, index + 4) == ' ') ||
+ value.length() == 4) || contains(value, 0, 4, "SAN ")) {
+ result.append('H');
+ } else {
+ result.append('J', 'H');
+ }
+ index++;
+ } else {
+ if (index == 0 && !contains(value, index, 4, "JOSE")) {
+ result.append('J', 'A');
+ } else if (isVowel(charAt(value, index - 1)) && !slavoGermanic &&
+ (charAt(value, index + 1) == 'A' || charAt(value, index + 1) == 'O')) {
+ result.append('J', 'H');
+ } else if (index == value.length() - 1) {
+ result.append('J', ' ');
+ } else if (!contains(value, index + 1, 1, L_T_K_S_N_M_B_Z) && !contains(value, index - 1, 1, "S", "K", "L")) {
+ result.append('J');
+ }
+
+ if (charAt(value, index + 1) == 'J') {
+ index += 2;
+ } else {
+ index++;
+ }
+ }
+ return index;
+ }
+
+ /**
+ * Handles 'L' cases
+ */
+ private int handleL(String value,
+ DoubleMetaphoneResult result,
+ int index) {
+ if (charAt(value, index + 1) == 'L') {
+ if (conditionL0(value, index)) {
+ result.appendPrimary('L');
+ } else {
+ result.append('L');
+ }
+ index += 2;
+ } else {
+ index++;
+ result.append('L');
+ }
+ return index;
+ }
+
+ /**
+ * Handles 'P' cases
+ */
+ private int handleP(String value,
+ DoubleMetaphoneResult result,
+ int index) {
+ if (charAt(value, index + 1) == 'H') {
+ result.append('F');
+ index += 2;
+ } else {
+ result.append('P');
+ index = contains(value, index + 1, 1, "P", "B") ? index + 2 : index + 1;
+ }
+ return index;
+ }
+
+ /**
+ * Handles 'R' cases
+ */
+ private int handleR(String value,
+ DoubleMetaphoneResult result,
+ int index,
+ boolean slavoGermanic) {
+ if (index == value.length() - 1 && !slavoGermanic &&
+ contains(value, index - 2, 2, "IE") &&
+ !contains(value, index - 4, 2, "ME", "MA")) {
+ result.appendAlternate('R');
+ } else {
+ result.append('R');
+ }
+ return charAt(value, index + 1) == 'R' ? index + 2 : index + 1;
+ }
+
+ /**
+ * Handles 'S' cases
+ */
+ private int handleS(String value,
+ DoubleMetaphoneResult result,
+ int index,
+ boolean slavoGermanic) {
+ if (contains(value, index - 1, 3, "ISL", "YSL")) {
+ //-- special cases "island", "isle", "carlisle", "carlysle" --//
+ index++;
+ } else if (index == 0 && contains(value, index, 5, "SUGAR")) {
+ //-- special case "sugar-" --//
+ result.append('X', 'S');
+ index++;
+ } else if (contains(value, index, 2, "SH")) {
+ if (contains(value, index + 1, 4,
+ "HEIM", "HOEK", "HOLM", "HOLZ")) {
+ //-- germanic --//
+ result.append('S');
+ } else {
+ result.append('X');
+ }
+ index += 2;
+ } else if (contains(value, index, 3, "SIO", "SIA") || contains(value, index, 4, "SIAN")) {
+ //-- Italian and Armenian --//
+ if (slavoGermanic) {
+ result.append('S');
+ } else {
+ result.append('S', 'X');
+ }
+ index += 3;
+ } else if ((index == 0 && contains(value, index + 1, 1, "M", "N", "L", "W")) || contains(value, index + 1, 1, "Z")) {
+ //-- german & anglicisations, e.g. "smith" match "schmidt" //
+ // "snider" match "schneider" --//
+ //-- also, -sz- in slavic language altho in hungarian it //
+ // is pronounced "s" --//
+ result.append('S', 'X');
+ index = contains(value, index + 1, 1, "Z") ? index + 2 : index + 1;
+ } else if (contains(value, index, 2, "SC")) {
+ index = handleSC(value, result, index);
+ } else {
+ if (index == value.length() - 1 && contains(value, index - 2,
+ 2, "AI", "OI")){
+ //-- french e.g. "resnais", "artois" --//
+ result.appendAlternate('S');
+ } else {
+ result.append('S');
+ }
+ index = contains(value, index + 1, 1, "S", "Z") ? index + 2 : index + 1;
+ }
+ return index;
+ }
+
+ /**
+ * Handles 'SC' cases
+ */
+ private int handleSC(String value,
+ DoubleMetaphoneResult result,
+ int index) {
+ if (charAt(value, index + 2) == 'H') {
+ //-- Schlesinger's rule --//
+ if (contains(value, index + 3,
+ 2, "OO", "ER", "EN", "UY", "ED", "EM")) {
+ //-- Dutch origin, e.g. "school", "schooner" --//
+ if (contains(value, index + 3, 2, "ER", "EN")) {
+ //-- "schermerhorn", "schenker" --//
+ result.append("X", "SK");
+ } else {
+ result.append("SK");
+ }
+ } else {
+ if (index == 0 && !isVowel(charAt(value, 3)) && charAt(value, 3) != 'W') {
+ result.append('X', 'S');
+ } else {
+ result.append('X');
+ }
+ }
+ } else if (contains(value, index + 2, 1, "I", "E", "Y")) {
+ result.append('S');
+ } else {
+ result.append("SK");
+ }
+ return index + 3;
+ }
+
+ /**
+ * Handles 'T' cases
+ */
+ private int handleT(String value,
+ DoubleMetaphoneResult result,
+ int index) {
+ if (contains(value, index, 4, "TION")) {
+ result.append('X');
+ index += 3;
+ } else if (contains(value, index, 3, "TIA", "TCH")) {
+ result.append('X');
+ index += 3;
+ } else if (contains(value, index, 2, "TH") || contains(value, index,
+ 3, "TTH")) {
+ if (contains(value, index + 2, 2, "OM", "AM") ||
+ //-- special case "thomas", "thames" or germanic --//
+ contains(value, 0, 4, "VAN ", "VON ") ||
+ contains(value, 0, 3, "SCH")) {
+ result.append('T');
+ } else {
+ result.append('0', 'T');
+ }
+ index += 2;
+ } else {
+ result.append('T');
+ index = contains(value, index + 1, 1, "T", "D") ? index + 2 : index + 1;
+ }
+ return index;
+ }
+
+ /**
+ * Handles 'W' cases
+ */
+ private int handleW(String value,
+ DoubleMetaphoneResult result,
+ int index) {
+ if (contains(value, index, 2, "WR")) {
+ //-- can also be in middle of word --//
+ result.append('R');
+ index += 2;
+ } else {
+ if (index == 0 && (isVowel(charAt(value, index + 1)) ||
+ contains(value, index, 2, "WH"))) {
+ if (isVowel(charAt(value, index + 1))) {
+ //-- Wasserman should match Vasserman --//
+ result.append('A', 'F');
+ } else {
+ //-- need Uomo to match Womo --//
+ result.append('A');
+ }
+ index++;
+ } else if ((index == value.length() - 1 && isVowel(charAt(value, index - 1))) ||
+ contains(value, index - 1,
+ 5, "EWSKI", "EWSKY", "OWSKI", "OWSKY") ||
+ contains(value, 0, 3, "SCH")) {
+ //-- Arnow should match Arnoff --//
+ result.appendAlternate('F');
+ index++;
+ } else if (contains(value, index, 4, "WICZ", "WITZ")) {
+ //-- Polish e.g. "filipowicz" --//
+ result.append("TS", "FX");
+ index += 4;
+ } else {
+ index++;
+ }
+ }
+ return index;
+ }
+
+ /**
+ * Handles 'X' cases
+ */
+ private int handleX(String value,
+ DoubleMetaphoneResult result,
+ int index) {
+ if (index == 0) {
+ result.append('S');
+ index++;
+ } else {
+ if (!((index == value.length() - 1) &&
+ (contains(value, index - 3, 3, "IAU", "EAU") ||
+ contains(value, index - 2, 2, "AU", "OU")))) {
+ //-- French e.g. breaux --//
+ result.append("KS");
+ }
+ index = contains(value, index + 1, 1, "C", "X") ? index + 2 : index + 1;
+ }
+ return index;
+ }
+
+ /**
+ * Handles 'Z' cases
+ */
+ private int handleZ(String value, DoubleMetaphoneResult result, int index,
+ boolean slavoGermanic) {
+ if (charAt(value, index + 1) == 'H') {
+ //-- Chinese pinyin e.g. "zhao" or Angelina "Zhang" --//
+ result.append('J');
+ index += 2;
+ } else {
+ if (contains(value, index + 1, 2, "ZO", "ZI", "ZA") || (slavoGermanic && (index > 0 && charAt(value, index - 1) != 'T'))) {
+ result.append("S", "TS");
+ } else {
+ result.append('S');
+ }
+ index = charAt(value, index + 1) == 'Z' ? index + 2 : index + 1;
+ }
+ return index;
+ }
+
+ //-- BEGIN CONDITIONS --//
+
+ /**
+ * Complex condition 0 for 'C'
+ */
+ private boolean conditionC0(String value, int index) {
+ if (contains(value, index, 4, "CHIA")) {
+ return true;
+ } else if (index <= 1) {
+ return false;
+ } else if (isVowel(charAt(value, index - 2))) {
+ return false;
+ } else if (!contains(value, index - 1, 3, "ACH")) {
+ return false;
+ } else {
+ char c = charAt(value, index + 2);
+ return (c != 'I' && c != 'E') ||
+ contains(value, index - 2, 6, "BACHER", "MACHER");
+ }
+ }
+
+ /**
+ * Complex condition 0 for 'CH'
+ */
+ private boolean conditionCH0(String value, int index) {
+ if (index != 0) {
+ return false;
+ } else if (!contains(value, index + 1, 5, "HARAC", "HARIS") &&
+ !contains(value, index + 1, 3, "HOR", "HYM", "HIA", "HEM")) {
+ return false;
+ } else if (contains(value, 0, 5, "CHORE")) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Complex condition 1 for 'CH'
+ */
+ private boolean conditionCH1(String value, int index) {
+ return ((contains(value, 0, 4, "VAN ", "VON ") || contains(value, 0,
+ 3, "SCH")) ||
+ contains(value, index - 2, 6, "ORCHES", "ARCHIT", "ORCHID") ||
+ contains(value, index + 2, 1, "T", "S") ||
+ ((contains(value, index - 1, 1, "A", "O", "U", "E") || index == 0) &&
+ (contains(value, index + 2, 1, L_R_N_M_B_H_F_V_W_SPACE) || index + 1 == value.length() - 1)));
+ }
+
+ /**
+ * Complex condition 0 for 'L'
+ */
+ private boolean conditionL0(String value, int index) {
+ if (index == value.length() - 3 &&
+ contains(value, index - 1, 4, "ILLO", "ILLA", "ALLE")) {
+ return true;
+ } else if ((contains(value, value.length() - 2, 2, "AS", "OS") ||
+ contains(value, value.length() - 1, 1, "A", "O")) &&
+ contains(value, index - 1, 4, "ALLE")) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Complex condition 0 for 'M'
+ */
+ private boolean conditionM0(String value, int index) {
+ if (charAt(value, index + 1) == 'M') {
+ return true;
+ }
+ return contains(value, index - 1, 3, "UMB") &&
+ ((index + 1) == value.length() - 1 || contains(value,
+ index + 2, 2, "ER"));
+ }
+
+ //-- BEGIN HELPER FUNCTIONS --//
+
+ /**
+ * Determines whether or not a value is of slavo-germanic orgin. A value is
+ * of slavo-germanic origin if it contians any of 'W', 'K', 'CZ', or 'WITZ'.
+ */
+ private boolean isSlavoGermanic(String value) {
+ return value.indexOf('W') > -1 || value.indexOf('K') > -1 ||
+ value.indexOf("CZ") > -1 || value.indexOf("WITZ") > -1;
+ }
+
+ /**
+ * Determines whether or not a character is a vowel or not
+ */
+ private boolean isVowel(char ch) {
+ return VOWELS.indexOf(ch) != -1;
+ }
+
+ /**
+ * Determines whether or not the value starts with a silent letter. It will
+ * return <code>true</code> if the value starts with any of 'GN', 'KN',
+ * 'PN', 'WR' or 'PS'.
+ */
+ private boolean isSilentStart(String value) {
+ boolean result = false;
+ for (int i = 0; i < SILENT_START.length; i++) {
+ if (value.startsWith(SILENT_START[i])) {
+ result = true;
+ break;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Cleans the input
+ */
+ private String cleanInput(String input) {
+ if (input == null) {
+ return null;
+ }
+ input = input.trim();
+ if (input.length() == 0) {
+ return null;
+ }
+ return input.toUpperCase(java.util.Locale.ENGLISH);
+ }
+
+ /**
+ * Gets the character at index <code>index</code> if available, otherwise
+ * it returns <code>Character.MIN_VALUE</code> so that there is some sort
+ * of a default
+ */
+ protected char charAt(String value, int index) {
+ if (index < 0 || index >= value.length()) {
+ return Character.MIN_VALUE;
+ }
+ return value.charAt(index);
+ }
+
+ /**
+ * Shortcut method with 1 criteria
+ */
+ private static boolean contains(String value, int start, int length,
+ String criteria) {
+ return contains(value, start, length,
+ new String[] { criteria });
+ }
+
+ /**
+ * Shortcut method with 2 criteria
+ */
+ private static boolean contains(String value, int start, int length,
+ String criteria1, String criteria2) {
+ return contains(value, start, length,
+ new String[] { criteria1, criteria2 });
+ }
+
+ /**
+ * Shortcut method with 3 criteria
+ */
+ private static boolean contains(String value, int start, int length,
+ String criteria1, String criteria2,
+ String criteria3) {
+ return contains(value, start, length,
+ new String[] { criteria1, criteria2, criteria3 });
+ }
+
+ /**
+ * Shortcut method with 4 criteria
+ */
+ private static boolean contains(String value, int start, int length,
+ String criteria1, String criteria2,
+ String criteria3, String criteria4) {
+ return contains(value, start, length,
+ new String[] { criteria1, criteria2, criteria3,
+ criteria4 });
+ }
+
+ /**
+ * Shortcut method with 5 criteria
+ */
+ private static boolean contains(String value, int start, int length,
+ String criteria1, String criteria2,
+ String criteria3, String criteria4,
+ String criteria5) {
+ return contains(value, start, length,
+ new String[] { criteria1, criteria2, criteria3,
+ criteria4, criteria5 });
+ }
+
+ /**
+ * Shortcut method with 6 criteria
+ */
+ private static boolean contains(String value, int start, int length,
+ String criteria1, String criteria2,
+ String criteria3, String criteria4,
+ String criteria5, String criteria6) {
+ return contains(value, start, length,
+ new String[] { criteria1, criteria2, criteria3,
+ criteria4, criteria5, criteria6 });
+ }
+
+ /**
+ * Determines whether <code>value</code> contains any of the criteria starting at index <code>start</code> and
+ * matching up to length <code>length</code>
+ */
+ protected static boolean contains(String value, int start, int length,
+ String[] criteria) {
+ boolean result = false;
+ if (start >= 0 && start + length <= value.length()) {
+ String target = value.substring(start, start + length);
+
+ for (int i = 0; i < criteria.length; i++) {
+ if (target.equals(criteria[i])) {
+ result = true;
+ break;
+ }
+ }
+ }
+ return result;
+ }
+
+ //-- BEGIN INNER CLASSES --//
+
+ /**
+ * Inner class for storing results, since there is the optional alternate
+ * encoding.
+ */
+ public class DoubleMetaphoneResult {
+
+ private StringBuffer primary = new StringBuffer(getMaxCodeLen());
+ private StringBuffer alternate = new StringBuffer(getMaxCodeLen());
+ private int maxLength;
+
+ public DoubleMetaphoneResult(int maxLength) {
+ this.maxLength = maxLength;
+ }
+
+ public void append(char value) {
+ appendPrimary(value);
+ appendAlternate(value);
+ }
+
+ public void append(char primary, char alternate) {
+ appendPrimary(primary);
+ appendAlternate(alternate);
+ }
+
+ public void appendPrimary(char value) {
+ if (this.primary.length() < this.maxLength) {
+ this.primary.append(value);
+ }
+ }
+
+ public void appendAlternate(char value) {
+ if (this.alternate.length() < this.maxLength) {
+ this.alternate.append(value);
+ }
+ }
+
+ public void append(String value) {
+ appendPrimary(value);
+ appendAlternate(value);
+ }
+
+ public void append(String primary, String alternate) {
+ appendPrimary(primary);
+ appendAlternate(alternate);
+ }
+
+ public void appendPrimary(String value) {
+ int addChars = this.maxLength - this.primary.length();
+ if (value.length() <= addChars) {
+ this.primary.append(value);
+ } else {
+ this.primary.append(value.substring(0, addChars));
+ }
+ }
+
+ public void appendAlternate(String value) {
+ int addChars = this.maxLength - this.alternate.length();
+ if (value.length() <= addChars) {
+ this.alternate.append(value);
+ } else {
+ this.alternate.append(value.substring(0, addChars));
+ }
+ }
+
+ public String getPrimary() {
+ return this.primary.toString();
+ }
+
+ public String getAlternate() {
+ return this.alternate.toString();
+ }
+
+ public boolean isComplete() {
+ return this.primary.length() >= this.maxLength &&
+ this.alternate.length() >= this.maxLength;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/Metaphone.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/Metaphone.java
new file mode 100644
index 0000000000..ec7cd3813e
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/Metaphone.java
@@ -0,0 +1,408 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.language;
+
+import org.mozilla.apache.commons.codec.EncoderException;
+import org.mozilla.apache.commons.codec.StringEncoder;
+
+/**
+ * Encodes a string into a Metaphone value.
+ * <p>
+ * Initial Java implementation by <CITE>William B. Brogden. December, 1997</CITE>.
+ * Permission given by <CITE>wbrogden</CITE> for code to be used anywhere.
+ * </p>
+ * <p>
+ * <CITE>Hanging on the Metaphone</CITE> by <CITE>Lawrence Philips</CITE> in <CITE>Computer Language of Dec. 1990, p
+ * 39.</CITE>
+ * </p>
+ * <p>
+ * Note, that this does not match the algorithm that ships with PHP, or the algorithm
+ * found in the Perl <a href="http://search.cpan.org/~mschwern/Text-Metaphone-1.96/Metaphone.pm">Text:Metaphone-1.96</a>.
+ * They have had undocumented changes from the originally published algorithm.
+ * For more information, see <a href="https://issues.apache.org/jira/browse/CODEC-57">CODEC-57</a>.
+ * </p>
+ *
+ * @author Apache Software Foundation
+ * @version $Id: Metaphone.java 1080867 2011-03-12 06:06:46Z ggregory $
+ */
+public class Metaphone implements StringEncoder {
+
+ /**
+ * Five values in the English language
+ */
+ private static final String VOWELS = "AEIOU" ;
+
+ /**
+ * Variable used in Metaphone algorithm
+ */
+ private static final String FRONTV = "EIY" ;
+
+ /**
+ * Variable used in Metaphone algorithm
+ */
+ private static final String VARSON = "CSPTG" ;
+
+ /**
+ * The max code length for metaphone is 4
+ */
+ private int maxCodeLen = 4 ;
+
+ /**
+ * Creates an instance of the Metaphone encoder
+ */
+ public Metaphone() {
+ super();
+ }
+
+ /**
+ * Find the metaphone value of a String. This is similar to the
+ * soundex algorithm, but better at finding similar sounding words.
+ * All input is converted to upper case.
+ * Limitations: Input format is expected to be a single ASCII word
+ * with only characters in the A - Z range, no punctuation or numbers.
+ *
+ * @param txt String to find the metaphone code for
+ * @return A metaphone code corresponding to the String supplied
+ */
+ public String metaphone(String txt) {
+ boolean hard = false ;
+ if ((txt == null) || (txt.length() == 0)) {
+ return "" ;
+ }
+ // single character is itself
+ if (txt.length() == 1) {
+ return txt.toUpperCase(java.util.Locale.ENGLISH) ;
+ }
+
+ char[] inwd = txt.toUpperCase(java.util.Locale.ENGLISH).toCharArray() ;
+
+ StringBuffer local = new StringBuffer(40); // manipulate
+ StringBuffer code = new StringBuffer(10) ; // output
+ // handle initial 2 characters exceptions
+ switch(inwd[0]) {
+ case 'K' :
+ case 'G' :
+ case 'P' : /* looking for KN, etc*/
+ if (inwd[1] == 'N') {
+ local.append(inwd, 1, inwd.length - 1);
+ } else {
+ local.append(inwd);
+ }
+ break;
+ case 'A': /* looking for AE */
+ if (inwd[1] == 'E') {
+ local.append(inwd, 1, inwd.length - 1);
+ } else {
+ local.append(inwd);
+ }
+ break;
+ case 'W' : /* looking for WR or WH */
+ if (inwd[1] == 'R') { // WR -> R
+ local.append(inwd, 1, inwd.length - 1);
+ break ;
+ }
+ if (inwd[1] == 'H') {
+ local.append(inwd, 1, inwd.length - 1);
+ local.setCharAt(0, 'W'); // WH -> W
+ } else {
+ local.append(inwd);
+ }
+ break;
+ case 'X' : /* initial X becomes S */
+ inwd[0] = 'S';
+ local.append(inwd);
+ break ;
+ default :
+ local.append(inwd);
+ } // now local has working string with initials fixed
+
+ int wdsz = local.length();
+ int n = 0 ;
+
+ while ((code.length() < this.getMaxCodeLen()) &&
+ (n < wdsz) ) { // max code size of 4 works well
+ char symb = local.charAt(n) ;
+ // remove duplicate letters except C
+ if ((symb != 'C') && (isPreviousChar( local, n, symb )) ) {
+ n++ ;
+ } else { // not dup
+ switch(symb) {
+ case 'A' : case 'E' : case 'I' : case 'O' : case 'U' :
+ if (n == 0) {
+ code.append(symb);
+ }
+ break ; // only use vowel if leading char
+ case 'B' :
+ if ( isPreviousChar(local, n, 'M') &&
+ isLastChar(wdsz, n) ) { // B is silent if word ends in MB
+ break;
+ }
+ code.append(symb);
+ break;
+ case 'C' : // lots of C special cases
+ /* discard if SCI, SCE or SCY */
+ if ( isPreviousChar(local, n, 'S') &&
+ !isLastChar(wdsz, n) &&
+ (FRONTV.indexOf(local.charAt(n + 1)) >= 0) ) {
+ break;
+ }
+ if (regionMatch(local, n, "CIA")) { // "CIA" -> X
+ code.append('X');
+ break;
+ }
+ if (!isLastChar(wdsz, n) &&
+ (FRONTV.indexOf(local.charAt(n + 1)) >= 0)) {
+ code.append('S');
+ break; // CI,CE,CY -> S
+ }
+ if (isPreviousChar(local, n, 'S') &&
+ isNextChar(local, n, 'H') ) { // SCH->sk
+ code.append('K') ;
+ break ;
+ }
+ if (isNextChar(local, n, 'H')) { // detect CH
+ if ((n == 0) &&
+ (wdsz >= 3) &&
+ isVowel(local,2) ) { // CH consonant -> K consonant
+ code.append('K');
+ } else {
+ code.append('X'); // CHvowel -> X
+ }
+ } else {
+ code.append('K');
+ }
+ break ;
+ case 'D' :
+ if (!isLastChar(wdsz, n + 1) &&
+ isNextChar(local, n, 'G') &&
+ (FRONTV.indexOf(local.charAt(n + 2)) >= 0)) { // DGE DGI DGY -> J
+ code.append('J'); n += 2 ;
+ } else {
+ code.append('T');
+ }
+ break ;
+ case 'G' : // GH silent at end or before consonant
+ if (isLastChar(wdsz, n + 1) &&
+ isNextChar(local, n, 'H')) {
+ break;
+ }
+ if (!isLastChar(wdsz, n + 1) &&
+ isNextChar(local,n,'H') &&
+ !isVowel(local,n+2)) {
+ break;
+ }
+ if ((n > 0) &&
+ ( regionMatch(local, n, "GN") ||
+ regionMatch(local, n, "GNED") ) ) {
+ break; // silent G
+ }
+ if (isPreviousChar(local, n, 'G')) {
+ // NOTE: Given that duplicated chars are removed, I don't see how this can ever be true
+ hard = true ;
+ } else {
+ hard = false ;
+ }
+ if (!isLastChar(wdsz, n) &&
+ (FRONTV.indexOf(local.charAt(n + 1)) >= 0) &&
+ (!hard)) {
+ code.append('J');
+ } else {
+ code.append('K');
+ }
+ break ;
+ case 'H':
+ if (isLastChar(wdsz, n)) {
+ break ; // terminal H
+ }
+ if ((n > 0) &&
+ (VARSON.indexOf(local.charAt(n - 1)) >= 0)) {
+ break;
+ }
+ if (isVowel(local,n+1)) {
+ code.append('H'); // Hvowel
+ }
+ break;
+ case 'F':
+ case 'J' :
+ case 'L' :
+ case 'M':
+ case 'N' :
+ case 'R' :
+ code.append(symb);
+ break;
+ case 'K' :
+ if (n > 0) { // not initial
+ if (!isPreviousChar(local, n, 'C')) {
+ code.append(symb);
+ }
+ } else {
+ code.append(symb); // initial K
+ }
+ break ;
+ case 'P' :
+ if (isNextChar(local,n,'H')) {
+ // PH -> F
+ code.append('F');
+ } else {
+ code.append(symb);
+ }
+ break ;
+ case 'Q' :
+ code.append('K');
+ break;
+ case 'S' :
+ if (regionMatch(local,n,"SH") ||
+ regionMatch(local,n,"SIO") ||
+ regionMatch(local,n,"SIA")) {
+ code.append('X');
+ } else {
+ code.append('S');
+ }
+ break;
+ case 'T' :
+ if (regionMatch(local,n,"TIA") ||
+ regionMatch(local,n,"TIO")) {
+ code.append('X');
+ break;
+ }
+ if (regionMatch(local,n,"TCH")) {
+ // Silent if in "TCH"
+ break;
+ }
+ // substitute numeral 0 for TH (resembles theta after all)
+ if (regionMatch(local,n,"TH")) {
+ code.append('0');
+ } else {
+ code.append('T');
+ }
+ break ;
+ case 'V' :
+ code.append('F'); break ;
+ case 'W' : case 'Y' : // silent if not followed by vowel
+ if (!isLastChar(wdsz,n) &&
+ isVowel(local,n+1)) {
+ code.append(symb);
+ }
+ break ;
+ case 'X' :
+ code.append('K'); code.append('S');
+ break ;
+ case 'Z' :
+ code.append('S'); break ;
+ } // end switch
+ n++ ;
+ } // end else from symb != 'C'
+ if (code.length() > this.getMaxCodeLen()) {
+ code.setLength(this.getMaxCodeLen());
+ }
+ }
+ return code.toString();
+ }
+
+ private boolean isVowel(StringBuffer string, int index) {
+ return VOWELS.indexOf(string.charAt(index)) >= 0;
+ }
+
+ private boolean isPreviousChar(StringBuffer string, int index, char c) {
+ boolean matches = false;
+ if( index > 0 &&
+ index < string.length() ) {
+ matches = string.charAt(index - 1) == c;
+ }
+ return matches;
+ }
+
+ private boolean isNextChar(StringBuffer string, int index, char c) {
+ boolean matches = false;
+ if( index >= 0 &&
+ index < string.length() - 1 ) {
+ matches = string.charAt(index + 1) == c;
+ }
+ return matches;
+ }
+
+ private boolean regionMatch(StringBuffer string, int index, String test) {
+ boolean matches = false;
+ if( index >= 0 &&
+ (index + test.length() - 1) < string.length() ) {
+ String substring = string.substring( index, index + test.length());
+ matches = substring.equals( test );
+ }
+ return matches;
+ }
+
+ private boolean isLastChar(int wdsz, int n) {
+ return n + 1 == wdsz;
+ }
+
+
+ /**
+ * Encodes an Object using the metaphone algorithm. This method
+ * is provided in order to satisfy the requirements of the
+ * Encoder interface, and will throw an EncoderException if the
+ * supplied object is not of type java.lang.String.
+ *
+ * @param pObject Object to encode
+ * @return An object (or type java.lang.String) containing the
+ * metaphone code which corresponds to the String supplied.
+ * @throws EncoderException if the parameter supplied is not
+ * of type java.lang.String
+ */
+ public Object encode(Object pObject) throws EncoderException {
+ if (!(pObject instanceof String)) {
+ throw new EncoderException("Parameter supplied to Metaphone encode is not of type java.lang.String");
+ }
+ return metaphone((String) pObject);
+ }
+
+ /**
+ * Encodes a String using the Metaphone algorithm.
+ *
+ * @param pString String object to encode
+ * @return The metaphone code corresponding to the String supplied
+ */
+ public String encode(String pString) {
+ return metaphone(pString);
+ }
+
+ /**
+ * Tests is the metaphones of two strings are identical.
+ *
+ * @param str1 First of two strings to compare
+ * @param str2 Second of two strings to compare
+ * @return <code>true</code> if the metaphones of these strings are identical,
+ * <code>false</code> otherwise.
+ */
+ public boolean isMetaphoneEqual(String str1, String str2) {
+ return metaphone(str1).equals(metaphone(str2));
+ }
+
+ /**
+ * Returns the maxCodeLen.
+ * @return int
+ */
+ public int getMaxCodeLen() { return this.maxCodeLen; }
+
+ /**
+ * Sets the maxCodeLen.
+ * @param maxCodeLen The maxCodeLen to set
+ */
+ public void setMaxCodeLen(int maxCodeLen) { this.maxCodeLen = maxCodeLen; }
+
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/RefinedSoundex.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/RefinedSoundex.java
new file mode 100644
index 0000000000..7fb730cea1
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/RefinedSoundex.java
@@ -0,0 +1,203 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.language;
+
+import org.mozilla.apache.commons.codec.EncoderException;
+import org.mozilla.apache.commons.codec.StringEncoder;
+
+/**
+ * Encodes a string into a Refined Soundex value. A refined soundex code is
+ * optimized for spell checking words. Soundex method originally developed by
+ * <CITE>Margaret Odell</CITE> and <CITE>Robert Russell</CITE>.
+ *
+ * @author Apache Software Foundation
+ * @version $Id: RefinedSoundex.java 1064455 2011-01-28 04:40:27Z ggregory $
+ */
+public class RefinedSoundex implements StringEncoder {
+
+ /**
+ * @since 1.4
+ */
+ public static final String US_ENGLISH_MAPPING_STRING = "01360240043788015936020505";
+
+ /**
+ * RefinedSoundex is *refined* for a number of reasons one being that the
+ * mappings have been altered. This implementation contains default
+ * mappings for US English.
+ */
+ private static final char[] US_ENGLISH_MAPPING = US_ENGLISH_MAPPING_STRING.toCharArray();
+
+ /**
+ * Every letter of the alphabet is "mapped" to a numerical value. This char
+ * array holds the values to which each letter is mapped. This
+ * implementation contains a default map for US_ENGLISH
+ */
+ private final char[] soundexMapping;
+
+ /**
+ * This static variable contains an instance of the RefinedSoundex using
+ * the US_ENGLISH mapping.
+ */
+ public static final RefinedSoundex US_ENGLISH = new RefinedSoundex();
+
+ /**
+ * Creates an instance of the RefinedSoundex object using the default US
+ * English mapping.
+ */
+ public RefinedSoundex() {
+ this.soundexMapping = US_ENGLISH_MAPPING;
+ }
+
+ /**
+ * Creates a refined soundex instance using a custom mapping. This
+ * constructor can be used to customize the mapping, and/or possibly
+ * provide an internationalized mapping for a non-Western character set.
+ *
+ * @param mapping
+ * Mapping array to use when finding the corresponding code for
+ * a given character
+ */
+ public RefinedSoundex(char[] mapping) {
+ this.soundexMapping = new char[mapping.length];
+ System.arraycopy(mapping, 0, this.soundexMapping, 0, mapping.length);
+ }
+
+ /**
+ * Creates a refined Soundex instance using a custom mapping. This constructor can be used to customize the mapping,
+ * and/or possibly provide an internationalized mapping for a non-Western character set.
+ *
+ * @param mapping
+ * Mapping string to use when finding the corresponding code for a given character
+ * @since 1.4
+ */
+ public RefinedSoundex(String mapping) {
+ this.soundexMapping = mapping.toCharArray();
+ }
+
+ /**
+ * Returns the number of characters in the two encoded Strings that are the
+ * same. This return value ranges from 0 to the length of the shortest
+ * encoded String: 0 indicates little or no similarity, and 4 out of 4 (for
+ * example) indicates strong similarity or identical values. For refined
+ * Soundex, the return value can be greater than 4.
+ *
+ * @param s1
+ * A String that will be encoded and compared.
+ * @param s2
+ * A String that will be encoded and compared.
+ * @return The number of characters in the two encoded Strings that are the
+ * same from 0 to to the length of the shortest encoded String.
+ *
+ * @see SoundexUtils#difference(StringEncoder,String,String)
+ * @see <a href="http://msdn.microsoft.com/library/default.asp?url=/library/en-us/tsqlref/ts_de-dz_8co5.asp">
+ * MS T-SQL DIFFERENCE</a>
+ *
+ * @throws EncoderException
+ * if an error occurs encoding one of the strings
+ * @since 1.3
+ */
+ public int difference(String s1, String s2) throws EncoderException {
+ return SoundexUtils.difference(this, s1, s2);
+ }
+
+ /**
+ * Encodes an Object using the refined soundex algorithm. This method is
+ * provided in order to satisfy the requirements of the Encoder interface,
+ * and will throw an EncoderException if the supplied object is not of type
+ * java.lang.String.
+ *
+ * @param pObject
+ * Object to encode
+ * @return An object (or type java.lang.String) containing the refined
+ * soundex code which corresponds to the String supplied.
+ * @throws EncoderException
+ * if the parameter supplied is not of type java.lang.String
+ */
+ public Object encode(Object pObject) throws EncoderException {
+ if (!(pObject instanceof String)) {
+ throw new EncoderException("Parameter supplied to RefinedSoundex encode is not of type java.lang.String");
+ }
+ return soundex((String) pObject);
+ }
+
+ /**
+ * Encodes a String using the refined soundex algorithm.
+ *
+ * @param pString
+ * A String object to encode
+ * @return A Soundex code corresponding to the String supplied
+ */
+ public String encode(String pString) {
+ return soundex(pString);
+ }
+
+ /**
+ * Returns the mapping code for a given character. The mapping codes are
+ * maintained in an internal char array named soundexMapping, and the
+ * default values of these mappings are US English.
+ *
+ * @param c
+ * char to get mapping for
+ * @return A character (really a numeral) to return for the given char
+ */
+ char getMappingCode(char c) {
+ if (!Character.isLetter(c)) {
+ return 0;
+ }
+ return this.soundexMapping[Character.toUpperCase(c) - 'A'];
+ }
+
+ /**
+ * Retreives the Refined Soundex code for a given String object.
+ *
+ * @param str
+ * String to encode using the Refined Soundex algorithm
+ * @return A soundex code for the String supplied
+ */
+ public String soundex(String str) {
+ if (str == null) {
+ return null;
+ }
+ str = SoundexUtils.clean(str);
+ if (str.length() == 0) {
+ return str;
+ }
+
+ StringBuffer sBuf = new StringBuffer();
+ sBuf.append(str.charAt(0));
+
+ char last, current;
+ last = '*';
+
+ for (int i = 0; i < str.length(); i++) {
+
+ current = getMappingCode(str.charAt(i));
+ if (current == last) {
+ continue;
+ } else if (current != 0) {
+ sBuf.append(current);
+ }
+
+ last = current;
+
+ }
+
+ return sBuf.toString();
+ }
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/Soundex.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/Soundex.java
new file mode 100644
index 0000000000..76805bca3a
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/Soundex.java
@@ -0,0 +1,279 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.language;
+
+import org.mozilla.apache.commons.codec.EncoderException;
+import org.mozilla.apache.commons.codec.StringEncoder;
+
+/**
+ * Encodes a string into a Soundex value. Soundex is an encoding used to relate similar names, but can also be used as a
+ * general purpose scheme to find word with similar phonemes.
+ *
+ * @author Apache Software Foundation
+ * @version $Id: Soundex.java 1064454 2011-01-28 04:40:02Z ggregory $
+ */
+public class Soundex implements StringEncoder {
+
+ /**
+ * This is a default mapping of the 26 letters used in US English. A value of <code>0</code> for a letter position
+ * means do not encode.
+ * <p>
+ * (This constant is provided as both an implementation convenience and to allow Javadoc to pick
+ * up the value for the constant values page.)
+ * </p>
+ *
+ * @see #US_ENGLISH_MAPPING
+ */
+ public static final String US_ENGLISH_MAPPING_STRING = "01230120022455012623010202";
+
+ /**
+ * This is a default mapping of the 26 letters used in US English. A value of <code>0</code> for a letter position
+ * means do not encode.
+ *
+ * @see Soundex#Soundex(char[])
+ */
+ private static final char[] US_ENGLISH_MAPPING = US_ENGLISH_MAPPING_STRING.toCharArray();
+
+ /**
+ * An instance of Soundex using the US_ENGLISH_MAPPING mapping.
+ *
+ * @see #US_ENGLISH_MAPPING
+ */
+ public static final Soundex US_ENGLISH = new Soundex();
+
+
+ /**
+ * Encodes the Strings and returns the number of characters in the two encoded Strings that are the same. This
+ * return value ranges from 0 through 4: 0 indicates little or no similarity, and 4 indicates strong similarity or
+ * identical values.
+ *
+ * @param s1
+ * A String that will be encoded and compared.
+ * @param s2
+ * A String that will be encoded and compared.
+ * @return The number of characters in the two encoded Strings that are the same from 0 to 4.
+ *
+ * @see SoundexUtils#difference(StringEncoder,String,String)
+ * @see <a href="http://msdn.microsoft.com/library/default.asp?url=/library/en-us/tsqlref/ts_de-dz_8co5.asp"> MS
+ * T-SQL DIFFERENCE </a>
+ *
+ * @throws EncoderException
+ * if an error occurs encoding one of the strings
+ * @since 1.3
+ */
+ public int difference(String s1, String s2) throws EncoderException {
+ return SoundexUtils.difference(this, s1, s2);
+ }
+
+ /**
+ * The maximum length of a Soundex code - Soundex codes are only four characters by definition.
+ *
+ * @deprecated This feature is not needed since the encoding size must be constant. Will be removed in 2.0.
+ */
+ private int maxLength = 4;
+
+ /**
+ * Every letter of the alphabet is "mapped" to a numerical value. This char array holds the values to which each
+ * letter is mapped. This implementation contains a default map for US_ENGLISH
+ */
+ private final char[] soundexMapping;
+
+ /**
+ * Creates an instance using US_ENGLISH_MAPPING
+ *
+ * @see Soundex#Soundex(char[])
+ * @see Soundex#US_ENGLISH_MAPPING
+ */
+ public Soundex() {
+ this.soundexMapping = US_ENGLISH_MAPPING;
+ }
+
+ /**
+ * Creates a soundex instance using the given mapping. This constructor can be used to provide an internationalized
+ * mapping for a non-Western character set.
+ *
+ * Every letter of the alphabet is "mapped" to a numerical value. This char array holds the values to which each
+ * letter is mapped. This implementation contains a default map for US_ENGLISH
+ *
+ * @param mapping
+ * Mapping array to use when finding the corresponding code for a given character
+ */
+ public Soundex(char[] mapping) {
+ this.soundexMapping = new char[mapping.length];
+ System.arraycopy(mapping, 0, this.soundexMapping, 0, mapping.length);
+ }
+
+ /**
+ * Creates a refined soundex instance using a custom mapping. This constructor can be used to customize the mapping,
+ * and/or possibly provide an internationalized mapping for a non-Western character set.
+ *
+ * @param mapping
+ * Mapping string to use when finding the corresponding code for a given character
+ * @since 1.4
+ */
+ public Soundex(String mapping) {
+ this.soundexMapping = mapping.toCharArray();
+ }
+
+ /**
+ * Encodes an Object using the soundex algorithm. This method is provided in order to satisfy the requirements of
+ * the Encoder interface, and will throw an EncoderException if the supplied object is not of type java.lang.String.
+ *
+ * @param pObject
+ * Object to encode
+ * @return An object (or type java.lang.String) containing the soundex code which corresponds to the String
+ * supplied.
+ * @throws EncoderException
+ * if the parameter supplied is not of type java.lang.String
+ * @throws IllegalArgumentException
+ * if a character is not mapped
+ */
+ public Object encode(Object pObject) throws EncoderException {
+ if (!(pObject instanceof String)) {
+ throw new EncoderException("Parameter supplied to Soundex encode is not of type java.lang.String");
+ }
+ return soundex((String) pObject);
+ }
+
+ /**
+ * Encodes a String using the soundex algorithm.
+ *
+ * @param pString
+ * A String object to encode
+ * @return A Soundex code corresponding to the String supplied
+ * @throws IllegalArgumentException
+ * if a character is not mapped
+ */
+ public String encode(String pString) {
+ return soundex(pString);
+ }
+
+ /**
+ * Used internally by the SoundEx algorithm.
+ *
+ * Consonants from the same code group separated by W or H are treated as one.
+ *
+ * @param str
+ * the cleaned working string to encode (in upper case).
+ * @param index
+ * the character position to encode
+ * @return Mapping code for a particular character
+ * @throws IllegalArgumentException
+ * if the character is not mapped
+ */
+ private char getMappingCode(String str, int index) {
+ // map() throws IllegalArgumentException
+ char mappedChar = this.map(str.charAt(index));
+ // HW rule check
+ if (index > 1 && mappedChar != '0') {
+ char hwChar = str.charAt(index - 1);
+ if ('H' == hwChar || 'W' == hwChar) {
+ char preHWChar = str.charAt(index - 2);
+ char firstCode = this.map(preHWChar);
+ if (firstCode == mappedChar || 'H' == preHWChar || 'W' == preHWChar) {
+ return 0;
+ }
+ }
+ }
+ return mappedChar;
+ }
+
+ /**
+ * Returns the maxLength. Standard Soundex
+ *
+ * @deprecated This feature is not needed since the encoding size must be constant. Will be removed in 2.0.
+ * @return int
+ */
+ public int getMaxLength() {
+ return this.maxLength;
+ }
+
+ /**
+ * Returns the soundex mapping.
+ *
+ * @return soundexMapping.
+ */
+ private char[] getSoundexMapping() {
+ return this.soundexMapping;
+ }
+
+ /**
+ * Maps the given upper-case character to its Soundex code.
+ *
+ * @param ch
+ * An upper-case character.
+ * @return A Soundex code.
+ * @throws IllegalArgumentException
+ * Thrown if <code>ch</code> is not mapped.
+ */
+ private char map(char ch) {
+ int index = ch - 'A';
+ if (index < 0 || index >= this.getSoundexMapping().length) {
+ throw new IllegalArgumentException("The character is not mapped: " + ch);
+ }
+ return this.getSoundexMapping()[index];
+ }
+
+ /**
+ * Sets the maxLength.
+ *
+ * @deprecated This feature is not needed since the encoding size must be constant. Will be removed in 2.0.
+ * @param maxLength
+ * The maxLength to set
+ */
+ public void setMaxLength(int maxLength) {
+ this.maxLength = maxLength;
+ }
+
+ /**
+ * Retrieves the Soundex code for a given String object.
+ *
+ * @param str
+ * String to encode using the Soundex algorithm
+ * @return A soundex code for the String supplied
+ * @throws IllegalArgumentException
+ * if a character is not mapped
+ */
+ public String soundex(String str) {
+ if (str == null) {
+ return null;
+ }
+ str = SoundexUtils.clean(str);
+ if (str.length() == 0) {
+ return str;
+ }
+ char out[] = {'0', '0', '0', '0'};
+ char last, mapped;
+ int incount = 1, count = 1;
+ out[0] = str.charAt(0);
+ // getMappingCode() throws IllegalArgumentException
+ last = getMappingCode(str, 0);
+ while ((incount < str.length()) && (count < out.length)) {
+ mapped = getMappingCode(str, incount++);
+ if (mapped != 0) {
+ if ((mapped != '0') && (mapped != last)) {
+ out[count++] = mapped;
+ }
+ last = mapped;
+ }
+ }
+ return new String(out);
+ }
+
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/SoundexUtils.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/SoundexUtils.java
new file mode 100644
index 0000000000..3e5a16a5ae
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/SoundexUtils.java
@@ -0,0 +1,124 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.language;
+
+import org.mozilla.apache.commons.codec.EncoderException;
+import org.mozilla.apache.commons.codec.StringEncoder;
+
+/**
+ * Utility methods for {@link Soundex} and {@link RefinedSoundex} classes.
+ *
+ * @author Apache Software Foundation
+ * @version $Id: SoundexUtils.java 658834 2008-05-21 19:57:51Z niallp $
+ * @since 1.3
+ */
+final class SoundexUtils {
+
+ /**
+ * Cleans up the input string before Soundex processing by only returning
+ * upper case letters.
+ *
+ * @param str
+ * The String to clean.
+ * @return A clean String.
+ */
+ static String clean(String str) {
+ if (str == null || str.length() == 0) {
+ return str;
+ }
+ int len = str.length();
+ char[] chars = new char[len];
+ int count = 0;
+ for (int i = 0; i < len; i++) {
+ if (Character.isLetter(str.charAt(i))) {
+ chars[count++] = str.charAt(i);
+ }
+ }
+ if (count == len) {
+ return str.toUpperCase(java.util.Locale.ENGLISH);
+ }
+ return new String(chars, 0, count).toUpperCase(java.util.Locale.ENGLISH);
+ }
+
+ /**
+ * Encodes the Strings and returns the number of characters in the two
+ * encoded Strings that are the same.
+ * <ul>
+ * <li>For Soundex, this return value ranges from 0 through 4: 0 indicates
+ * little or no similarity, and 4 indicates strong similarity or identical
+ * values.</li>
+ * <li>For refined Soundex, the return value can be greater than 4.</li>
+ * </ul>
+ *
+ * @param encoder
+ * The encoder to use to encode the Strings.
+ * @param s1
+ * A String that will be encoded and compared.
+ * @param s2
+ * A String that will be encoded and compared.
+ * @return The number of characters in the two Soundex encoded Strings that
+ * are the same.
+ *
+ * @see #differenceEncoded(String,String)
+ * @see <a href="http://msdn.microsoft.com/library/default.asp?url=/library/en-us/tsqlref/ts_de-dz_8co5.asp">
+ * MS T-SQL DIFFERENCE</a>
+ *
+ * @throws EncoderException
+ * if an error occurs encoding one of the strings
+ */
+ static int difference(StringEncoder encoder, String s1, String s2) throws EncoderException {
+ return differenceEncoded(encoder.encode(s1), encoder.encode(s2));
+ }
+
+ /**
+ * Returns the number of characters in the two Soundex encoded Strings that
+ * are the same.
+ * <ul>
+ * <li>For Soundex, this return value ranges from 0 through 4: 0 indicates
+ * little or no similarity, and 4 indicates strong similarity or identical
+ * values.</li>
+ * <li>For refined Soundex, the return value can be greater than 4.</li>
+ * </ul>
+ *
+ * @param es1
+ * An encoded String.
+ * @param es2
+ * An encoded String.
+ * @return The number of characters in the two Soundex encoded Strings that
+ * are the same.
+ *
+ * @see <a href="http://msdn.microsoft.com/library/default.asp?url=/library/en-us/tsqlref/ts_de-dz_8co5.asp">
+ * MS T-SQL DIFFERENCE</a>
+ */
+ static int differenceEncoded(String es1, String es2) {
+
+ if (es1 == null || es2 == null) {
+ return 0;
+ }
+ int lengthToMatch = Math.min(es1.length(), es2.length());
+ int diff = 0;
+ for (int i = 0; i < lengthToMatch; i++) {
+ if (es1.charAt(i) == es2.charAt(i)) {
+ diff++;
+ }
+ }
+ return diff;
+ }
+
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/package.html b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/package.html
new file mode 100644
index 0000000000..6e33766894
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/language/package.html
@@ -0,0 +1,21 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<html>
+ <body>
+ Language and phonetic encoders.
+ </body>
+</html>
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/BCodec.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/BCodec.java
new file mode 100644
index 0000000000..b694888ebe
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/BCodec.java
@@ -0,0 +1,209 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.net;
+
+import java.io.UnsupportedEncodingException;
+
+import org.mozilla.apache.commons.codec.DecoderException;
+import org.mozilla.apache.commons.codec.EncoderException;
+import org.mozilla.apache.commons.codec.CharEncoding;
+import org.mozilla.apache.commons.codec.StringDecoder;
+import org.mozilla.apache.commons.codec.StringEncoder;
+import org.mozilla.apache.commons.codec.binary.Base64;
+
+/**
+ * <p>
+ * Identical to the Base64 encoding defined by <a href="http://www.ietf.org/rfc/rfc1521.txt">RFC
+ * 1521</a> and allows a character set to be specified.
+ * </p>
+ *
+ * <p>
+ * <a href="http://www.ietf.org/rfc/rfc1522.txt">RFC 1522</a> describes techniques to allow the encoding of non-ASCII
+ * text in various portions of a RFC 822 [2] message header, in a manner which is unlikely to confuse existing message
+ * handling software.
+ * </p>
+ *
+ * @see <a href="http://www.ietf.org/rfc/rfc1522.txt">MIME (Multipurpose Internet Mail Extensions) Part Two: Message
+ * Header Extensions for Non-ASCII Text</a>
+ *
+ * @author Apache Software Foundation
+ * @since 1.3
+ * @version $Id: BCodec.java 797857 2009-07-25 23:43:33Z ggregory $
+ */
+public class BCodec extends RFC1522Codec implements StringEncoder, StringDecoder {
+ /**
+ * The default charset used for string decoding and encoding.
+ */
+ private final String charset;
+
+ /**
+ * Default constructor.
+ */
+ public BCodec() {
+ this(CharEncoding.UTF_8);
+ }
+
+ /**
+ * Constructor which allows for the selection of a default charset
+ *
+ * @param charset
+ * the default string charset to use.
+ *
+ * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+ */
+ public BCodec(final String charset) {
+ super();
+ this.charset = charset;
+ }
+
+ protected String getEncoding() {
+ return "B";
+ }
+
+ protected byte[] doEncoding(byte[] bytes) {
+ if (bytes == null) {
+ return null;
+ }
+ return Base64.encodeBase64(bytes);
+ }
+
+ protected byte[] doDecoding(byte[] bytes) {
+ if (bytes == null) {
+ return null;
+ }
+ return Base64.decodeBase64(bytes);
+ }
+
+ /**
+ * Encodes a string into its Base64 form using the specified charset. Unsafe characters are escaped.
+ *
+ * @param value
+ * string to convert to Base64 form
+ * @param charset
+ * the charset for <code>value</code>
+ * @return Base64 string
+ *
+ * @throws EncoderException
+ * thrown if a failure condition is encountered during the encoding process.
+ */
+ public String encode(final String value, final String charset) throws EncoderException {
+ if (value == null) {
+ return null;
+ }
+ try {
+ return encodeText(value, charset);
+ } catch (UnsupportedEncodingException e) {
+ throw new EncoderException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Encodes a string into its Base64 form using the default charset. Unsafe characters are escaped.
+ *
+ * @param value
+ * string to convert to Base64 form
+ * @return Base64 string
+ *
+ * @throws EncoderException
+ * thrown if a failure condition is encountered during the encoding process.
+ */
+ public String encode(String value) throws EncoderException {
+ if (value == null) {
+ return null;
+ }
+ return encode(value, getDefaultCharset());
+ }
+
+ /**
+ * Decodes a Base64 string into its original form. Escaped characters are converted back to their original
+ * representation.
+ *
+ * @param value
+ * Base64 string to convert into its original form
+ * @return original string
+ * @throws DecoderException
+ * A decoder exception is thrown if a failure condition is encountered during the decode process.
+ */
+ public String decode(String value) throws DecoderException {
+ if (value == null) {
+ return null;
+ }
+ try {
+ return decodeText(value);
+ } catch (UnsupportedEncodingException e) {
+ throw new DecoderException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Encodes an object into its Base64 form using the default charset. Unsafe characters are escaped.
+ *
+ * @param value
+ * object to convert to Base64 form
+ * @return Base64 object
+ *
+ * @throws EncoderException
+ * thrown if a failure condition is encountered during the encoding process.
+ */
+ public Object encode(Object value) throws EncoderException {
+ if (value == null) {
+ return null;
+ } else if (value instanceof String) {
+ return encode((String) value);
+ } else {
+ throw new EncoderException("Objects of type " +
+ value.getClass().getName() +
+ " cannot be encoded using BCodec");
+ }
+ }
+
+ /**
+ * Decodes a Base64 object into its original form. Escaped characters are converted back to their original
+ * representation.
+ *
+ * @param value
+ * Base64 object to convert into its original form
+ *
+ * @return original object
+ *
+ * @throws DecoderException
+ * Thrown if the argument is not a <code>String</code>. Thrown if a failure condition is
+ * encountered during the decode process.
+ */
+ public Object decode(Object value) throws DecoderException {
+ if (value == null) {
+ return null;
+ } else if (value instanceof String) {
+ return decode((String) value);
+ } else {
+ throw new DecoderException("Objects of type " +
+ value.getClass().getName() +
+ " cannot be decoded using BCodec");
+ }
+ }
+
+ /**
+ * The default charset used for string decoding and encoding.
+ *
+ * @return the default string charset.
+ */
+ public String getDefaultCharset() {
+ return this.charset;
+ }
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/QCodec.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/QCodec.java
new file mode 100644
index 0000000000..d174bcdff7
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/QCodec.java
@@ -0,0 +1,312 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.net;
+
+import java.io.UnsupportedEncodingException;
+import java.util.BitSet;
+
+import org.mozilla.apache.commons.codec.DecoderException;
+import org.mozilla.apache.commons.codec.EncoderException;
+import org.mozilla.apache.commons.codec.CharEncoding;
+import org.mozilla.apache.commons.codec.StringDecoder;
+import org.mozilla.apache.commons.codec.StringEncoder;
+
+/**
+ * <p>
+ * Similar to the Quoted-Printable content-transfer-encoding defined in <a
+ * href="http://www.ietf.org/rfc/rfc1521.txt">RFC 1521</a> and designed to allow text containing mostly ASCII
+ * characters to be decipherable on an ASCII terminal without decoding.
+ * </p>
+ *
+ * <p>
+ * <a href="http://www.ietf.org/rfc/rfc1522.txt">RFC 1522</a> describes techniques to allow the encoding of non-ASCII
+ * text in various portions of a RFC 822 [2] message header, in a manner which is unlikely to confuse existing message
+ * handling software.
+ * </p>
+ *
+ * @see <a href="http://www.ietf.org/rfc/rfc1522.txt">MIME (Multipurpose Internet Mail Extensions) Part Two: Message
+ * Header Extensions for Non-ASCII Text</a>
+ *
+ * @author Apache Software Foundation
+ * @since 1.3
+ * @version $Id: QCodec.java 797857 2009-07-25 23:43:33Z ggregory $
+ */
+public class QCodec extends RFC1522Codec implements StringEncoder, StringDecoder {
+ /**
+ * The default charset used for string decoding and encoding.
+ */
+ private final String charset;
+
+ /**
+ * BitSet of printable characters as defined in RFC 1522.
+ */
+ private static final BitSet PRINTABLE_CHARS = new BitSet(256);
+ // Static initializer for printable chars collection
+ static {
+ // alpha characters
+ PRINTABLE_CHARS.set(' ');
+ PRINTABLE_CHARS.set('!');
+ PRINTABLE_CHARS.set('"');
+ PRINTABLE_CHARS.set('#');
+ PRINTABLE_CHARS.set('$');
+ PRINTABLE_CHARS.set('%');
+ PRINTABLE_CHARS.set('&');
+ PRINTABLE_CHARS.set('\'');
+ PRINTABLE_CHARS.set('(');
+ PRINTABLE_CHARS.set(')');
+ PRINTABLE_CHARS.set('*');
+ PRINTABLE_CHARS.set('+');
+ PRINTABLE_CHARS.set(',');
+ PRINTABLE_CHARS.set('-');
+ PRINTABLE_CHARS.set('.');
+ PRINTABLE_CHARS.set('/');
+ for (int i = '0'; i <= '9'; i++) {
+ PRINTABLE_CHARS.set(i);
+ }
+ PRINTABLE_CHARS.set(':');
+ PRINTABLE_CHARS.set(';');
+ PRINTABLE_CHARS.set('<');
+ PRINTABLE_CHARS.set('>');
+ PRINTABLE_CHARS.set('@');
+ for (int i = 'A'; i <= 'Z'; i++) {
+ PRINTABLE_CHARS.set(i);
+ }
+ PRINTABLE_CHARS.set('[');
+ PRINTABLE_CHARS.set('\\');
+ PRINTABLE_CHARS.set(']');
+ PRINTABLE_CHARS.set('^');
+ PRINTABLE_CHARS.set('`');
+ for (int i = 'a'; i <= 'z'; i++) {
+ PRINTABLE_CHARS.set(i);
+ }
+ PRINTABLE_CHARS.set('{');
+ PRINTABLE_CHARS.set('|');
+ PRINTABLE_CHARS.set('}');
+ PRINTABLE_CHARS.set('~');
+ }
+
+ private static final byte BLANK = 32;
+
+ private static final byte UNDERSCORE = 95;
+
+ private boolean encodeBlanks = false;
+
+ /**
+ * Default constructor.
+ */
+ public QCodec() {
+ this(CharEncoding.UTF_8);
+ }
+
+ /**
+ * Constructor which allows for the selection of a default charset
+ *
+ * @param charset
+ * the default string charset to use.
+ *
+ * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+ */
+ public QCodec(final String charset) {
+ super();
+ this.charset = charset;
+ }
+
+ protected String getEncoding() {
+ return "Q";
+ }
+
+ protected byte[] doEncoding(byte[] bytes) {
+ if (bytes == null) {
+ return null;
+ }
+ byte[] data = QuotedPrintableCodec.encodeQuotedPrintable(PRINTABLE_CHARS, bytes);
+ if (this.encodeBlanks) {
+ for (int i = 0; i < data.length; i++) {
+ if (data[i] == BLANK) {
+ data[i] = UNDERSCORE;
+ }
+ }
+ }
+ return data;
+ }
+
+ protected byte[] doDecoding(byte[] bytes) throws DecoderException {
+ if (bytes == null) {
+ return null;
+ }
+ boolean hasUnderscores = false;
+ for (int i = 0; i < bytes.length; i++) {
+ if (bytes[i] == UNDERSCORE) {
+ hasUnderscores = true;
+ break;
+ }
+ }
+ if (hasUnderscores) {
+ byte[] tmp = new byte[bytes.length];
+ for (int i = 0; i < bytes.length; i++) {
+ byte b = bytes[i];
+ if (b != UNDERSCORE) {
+ tmp[i] = b;
+ } else {
+ tmp[i] = BLANK;
+ }
+ }
+ return QuotedPrintableCodec.decodeQuotedPrintable(tmp);
+ }
+ return QuotedPrintableCodec.decodeQuotedPrintable(bytes);
+ }
+
+ /**
+ * Encodes a string into its quoted-printable form using the specified charset. Unsafe characters are escaped.
+ *
+ * @param pString
+ * string to convert to quoted-printable form
+ * @param charset
+ * the charset for pString
+ * @return quoted-printable string
+ *
+ * @throws EncoderException
+ * thrown if a failure condition is encountered during the encoding process.
+ */
+ public String encode(final String pString, final String charset) throws EncoderException {
+ if (pString == null) {
+ return null;
+ }
+ try {
+ return encodeText(pString, charset);
+ } catch (UnsupportedEncodingException e) {
+ throw new EncoderException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Encodes a string into its quoted-printable form using the default charset. Unsafe characters are escaped.
+ *
+ * @param pString
+ * string to convert to quoted-printable form
+ * @return quoted-printable string
+ *
+ * @throws EncoderException
+ * thrown if a failure condition is encountered during the encoding process.
+ */
+ public String encode(String pString) throws EncoderException {
+ if (pString == null) {
+ return null;
+ }
+ return encode(pString, getDefaultCharset());
+ }
+
+ /**
+ * Decodes a quoted-printable string into its original form. Escaped characters are converted back to their original
+ * representation.
+ *
+ * @param pString
+ * quoted-printable string to convert into its original form
+ *
+ * @return original string
+ *
+ * @throws DecoderException
+ * A decoder exception is thrown if a failure condition is encountered during the decode process.
+ */
+ public String decode(String pString) throws DecoderException {
+ if (pString == null) {
+ return null;
+ }
+ try {
+ return decodeText(pString);
+ } catch (UnsupportedEncodingException e) {
+ throw new DecoderException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Encodes an object into its quoted-printable form using the default charset. Unsafe characters are escaped.
+ *
+ * @param pObject
+ * object to convert to quoted-printable form
+ * @return quoted-printable object
+ *
+ * @throws EncoderException
+ * thrown if a failure condition is encountered during the encoding process.
+ */
+ public Object encode(Object pObject) throws EncoderException {
+ if (pObject == null) {
+ return null;
+ } else if (pObject instanceof String) {
+ return encode((String) pObject);
+ } else {
+ throw new EncoderException("Objects of type " +
+ pObject.getClass().getName() +
+ " cannot be encoded using Q codec");
+ }
+ }
+
+ /**
+ * Decodes a quoted-printable object into its original form. Escaped characters are converted back to their original
+ * representation.
+ *
+ * @param pObject
+ * quoted-printable object to convert into its original form
+ *
+ * @return original object
+ *
+ * @throws DecoderException
+ * Thrown if the argument is not a <code>String</code>. Thrown if a failure condition is
+ * encountered during the decode process.
+ */
+ public Object decode(Object pObject) throws DecoderException {
+ if (pObject == null) {
+ return null;
+ } else if (pObject instanceof String) {
+ return decode((String) pObject);
+ } else {
+ throw new DecoderException("Objects of type " +
+ pObject.getClass().getName() +
+ " cannot be decoded using Q codec");
+ }
+ }
+
+ /**
+ * The default charset used for string decoding and encoding.
+ *
+ * @return the default string charset.
+ */
+ public String getDefaultCharset() {
+ return this.charset;
+ }
+
+ /**
+ * Tests if optional tranformation of SPACE characters is to be used
+ *
+ * @return <code>true</code> if SPACE characters are to be transformed, <code>false</code> otherwise
+ */
+ public boolean isEncodeBlanks() {
+ return this.encodeBlanks;
+ }
+
+ /**
+ * Defines whether optional tranformation of SPACE characters is to be used
+ *
+ * @param b
+ * <code>true</code> if SPACE characters are to be transformed, <code>false</code> otherwise
+ */
+ public void setEncodeBlanks(boolean b) {
+ this.encodeBlanks = b;
+ }
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/QuotedPrintableCodec.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/QuotedPrintableCodec.java
new file mode 100644
index 0000000000..c9b5e61720
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/QuotedPrintableCodec.java
@@ -0,0 +1,388 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.net;
+
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.BitSet;
+
+import org.mozilla.apache.commons.codec.BinaryDecoder;
+import org.mozilla.apache.commons.codec.BinaryEncoder;
+import org.mozilla.apache.commons.codec.DecoderException;
+import org.mozilla.apache.commons.codec.EncoderException;
+import org.mozilla.apache.commons.codec.CharEncoding;
+import org.mozilla.apache.commons.codec.StringDecoder;
+import org.mozilla.apache.commons.codec.StringEncoder;
+import org.mozilla.apache.commons.codec.binary.StringUtils;
+
+/**
+ * <p>
+ * Codec for the Quoted-Printable section of <a href="http://www.ietf.org/rfc/rfc1521.txt">RFC 1521</a>.
+ * </p>
+ * <p>
+ * The Quoted-Printable encoding is intended to represent data that largely consists of octets that correspond to
+ * printable characters in the ASCII character set. It encodes the data in such a way that the resulting octets are
+ * unlikely to be modified by mail transport. If the data being encoded are mostly ASCII text, the encoded form of the
+ * data remains largely recognizable by humans. A body which is entirely ASCII may also be encoded in Quoted-Printable
+ * to ensure the integrity of the data should the message pass through a character- translating, and/or line-wrapping
+ * gateway.
+ * </p>
+ *
+ * <p>
+ * Note:
+ * </p>
+ * <p>
+ * Rules #3, #4, and #5 of the quoted-printable spec are not implemented yet because the complete quoted-printable spec
+ * does not lend itself well into the byte[] oriented codec framework. Complete the codec once the steamable codec
+ * framework is ready. The motivation behind providing the codec in a partial form is that it can already come in handy
+ * for those applications that do not require quoted-printable line formatting (rules #3, #4, #5), for instance Q codec.
+ * </p>
+ *
+ * @see <a href="http://www.ietf.org/rfc/rfc1521.txt"> RFC 1521 MIME (Multipurpose Internet Mail Extensions) Part One:
+ * Mechanisms for Specifying and Describing the Format of Internet Message Bodies </a>
+ *
+ * @author Apache Software Foundation
+ * @since 1.3
+ * @version $Id: QuotedPrintableCodec.java 1080712 2011-03-11 18:26:59Z ggregory $
+ */
+public class QuotedPrintableCodec implements BinaryEncoder, BinaryDecoder, StringEncoder, StringDecoder {
+ /**
+ * The default charset used for string decoding and encoding.
+ */
+ private final String charset;
+
+ /**
+ * BitSet of printable characters as defined in RFC 1521.
+ */
+ private static final BitSet PRINTABLE_CHARS = new BitSet(256);
+
+ private static final byte ESCAPE_CHAR = '=';
+
+ private static final byte TAB = 9;
+
+ private static final byte SPACE = 32;
+ // Static initializer for printable chars collection
+ static {
+ // alpha characters
+ for (int i = 33; i <= 60; i++) {
+ PRINTABLE_CHARS.set(i);
+ }
+ for (int i = 62; i <= 126; i++) {
+ PRINTABLE_CHARS.set(i);
+ }
+ PRINTABLE_CHARS.set(TAB);
+ PRINTABLE_CHARS.set(SPACE);
+ }
+
+ /**
+ * Default constructor.
+ */
+ public QuotedPrintableCodec() {
+ this(CharEncoding.UTF_8);
+ }
+
+ /**
+ * Constructor which allows for the selection of a default charset
+ *
+ * @param charset
+ * the default string charset to use.
+ */
+ public QuotedPrintableCodec(String charset) {
+ super();
+ this.charset = charset;
+ }
+
+ /**
+ * Encodes byte into its quoted-printable representation.
+ *
+ * @param b
+ * byte to encode
+ * @param buffer
+ * the buffer to write to
+ */
+ private static final void encodeQuotedPrintable(int b, ByteArrayOutputStream buffer) {
+ buffer.write(ESCAPE_CHAR);
+ char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16));
+ char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16));
+ buffer.write(hex1);
+ buffer.write(hex2);
+ }
+
+ /**
+ * Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped.
+ *
+ * <p>
+ * This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
+ * RFC 1521 and is suitable for encoding binary data and unformatted text.
+ * </p>
+ *
+ * @param printable
+ * bitset of characters deemed quoted-printable
+ * @param bytes
+ * array of bytes to be encoded
+ * @return array of bytes containing quoted-printable data
+ */
+ public static final byte[] encodeQuotedPrintable(BitSet printable, byte[] bytes) {
+ if (bytes == null) {
+ return null;
+ }
+ if (printable == null) {
+ printable = PRINTABLE_CHARS;
+ }
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ for (int i = 0; i < bytes.length; i++) {
+ int b = bytes[i];
+ if (b < 0) {
+ b = 256 + b;
+ }
+ if (printable.get(b)) {
+ buffer.write(b);
+ } else {
+ encodeQuotedPrintable(b, buffer);
+ }
+ }
+ return buffer.toByteArray();
+ }
+
+ /**
+ * Decodes an array quoted-printable characters into an array of original bytes. Escaped characters are converted
+ * back to their original representation.
+ *
+ * <p>
+ * This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
+ * RFC 1521.
+ * </p>
+ *
+ * @param bytes
+ * array of quoted-printable characters
+ * @return array of original bytes
+ * @throws DecoderException
+ * Thrown if quoted-printable decoding is unsuccessful
+ */
+ public static final byte[] decodeQuotedPrintable(byte[] bytes) throws DecoderException {
+ if (bytes == null) {
+ return null;
+ }
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ for (int i = 0; i < bytes.length; i++) {
+ int b = bytes[i];
+ if (b == ESCAPE_CHAR) {
+ try {
+ int u = Utils.digit16(bytes[++i]);
+ int l = Utils.digit16(bytes[++i]);
+ buffer.write((char) ((u << 4) + l));
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new DecoderException("Invalid quoted-printable encoding", e);
+ }
+ } else {
+ buffer.write(b);
+ }
+ }
+ return buffer.toByteArray();
+ }
+
+ /**
+ * Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped.
+ *
+ * <p>
+ * This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
+ * RFC 1521 and is suitable for encoding binary data and unformatted text.
+ * </p>
+ *
+ * @param bytes
+ * array of bytes to be encoded
+ * @return array of bytes containing quoted-printable data
+ */
+ public byte[] encode(byte[] bytes) {
+ return encodeQuotedPrintable(PRINTABLE_CHARS, bytes);
+ }
+
+ /**
+ * Decodes an array of quoted-printable characters into an array of original bytes. Escaped characters are converted
+ * back to their original representation.
+ *
+ * <p>
+ * This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
+ * RFC 1521.
+ * </p>
+ *
+ * @param bytes
+ * array of quoted-printable characters
+ * @return array of original bytes
+ * @throws DecoderException
+ * Thrown if quoted-printable decoding is unsuccessful
+ */
+ public byte[] decode(byte[] bytes) throws DecoderException {
+ return decodeQuotedPrintable(bytes);
+ }
+
+ /**
+ * Encodes a string into its quoted-printable form using the default string charset. Unsafe characters are escaped.
+ *
+ * <p>
+ * This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
+ * RFC 1521 and is suitable for encoding binary data.
+ * </p>
+ *
+ * @param pString
+ * string to convert to quoted-printable form
+ * @return quoted-printable string
+ *
+ * @throws EncoderException
+ * Thrown if quoted-printable encoding is unsuccessful
+ *
+ * @see #getDefaultCharset()
+ */
+ public String encode(String pString) throws EncoderException {
+ if (pString == null) {
+ return null;
+ }
+ try {
+ return encode(pString, getDefaultCharset());
+ } catch (UnsupportedEncodingException e) {
+ throw new EncoderException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Decodes a quoted-printable string into its original form using the specified string charset. Escaped characters
+ * are converted back to their original representation.
+ *
+ * @param pString
+ * quoted-printable string to convert into its original form
+ * @param charset
+ * the original string charset
+ * @return original string
+ * @throws DecoderException
+ * Thrown if quoted-printable decoding is unsuccessful
+ * @throws UnsupportedEncodingException
+ * Thrown if charset is not supported
+ */
+ public String decode(String pString, String charset) throws DecoderException, UnsupportedEncodingException {
+ if (pString == null) {
+ return null;
+ }
+ return new String(decode(StringUtils.getBytesUsAscii(pString)), charset);
+ }
+
+ /**
+ * Decodes a quoted-printable string into its original form using the default string charset. Escaped characters are
+ * converted back to their original representation.
+ *
+ * @param pString
+ * quoted-printable string to convert into its original form
+ * @return original string
+ * @throws DecoderException
+ * Thrown if quoted-printable decoding is unsuccessful.
+ * Thrown if charset is not supported.
+ * @see #getDefaultCharset()
+ */
+ public String decode(String pString) throws DecoderException {
+ if (pString == null) {
+ return null;
+ }
+ try {
+ return decode(pString, getDefaultCharset());
+ } catch (UnsupportedEncodingException e) {
+ throw new DecoderException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Encodes an object into its quoted-printable safe form. Unsafe characters are escaped.
+ *
+ * @param pObject
+ * string to convert to a quoted-printable form
+ * @return quoted-printable object
+ * @throws EncoderException
+ * Thrown if quoted-printable encoding is not applicable to objects of this type or if encoding is
+ * unsuccessful
+ */
+ public Object encode(Object pObject) throws EncoderException {
+ if (pObject == null) {
+ return null;
+ } else if (pObject instanceof byte[]) {
+ return encode((byte[]) pObject);
+ } else if (pObject instanceof String) {
+ return encode((String) pObject);
+ } else {
+ throw new EncoderException("Objects of type " +
+ pObject.getClass().getName() +
+ " cannot be quoted-printable encoded");
+ }
+ }
+
+ /**
+ * Decodes a quoted-printable object into its original form. Escaped characters are converted back to their original
+ * representation.
+ *
+ * @param pObject
+ * quoted-printable object to convert into its original form
+ * @return original object
+ * @throws DecoderException
+ * Thrown if the argument is not a <code>String</code> or <code>byte[]</code>. Thrown if a failure condition is
+ * encountered during the decode process.
+ */
+ public Object decode(Object pObject) throws DecoderException {
+ if (pObject == null) {
+ return null;
+ } else if (pObject instanceof byte[]) {
+ return decode((byte[]) pObject);
+ } else if (pObject instanceof String) {
+ return decode((String) pObject);
+ } else {
+ throw new DecoderException("Objects of type " +
+ pObject.getClass().getName() +
+ " cannot be quoted-printable decoded");
+ }
+ }
+
+ /**
+ * Returns the default charset used for string decoding and encoding.
+ *
+ * @return the default string charset.
+ */
+ public String getDefaultCharset() {
+ return this.charset;
+ }
+
+ /**
+ * Encodes a string into its quoted-printable form using the specified charset. Unsafe characters are escaped.
+ *
+ * <p>
+ * This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in
+ * RFC 1521 and is suitable for encoding binary data and unformatted text.
+ * </p>
+ *
+ * @param pString
+ * string to convert to quoted-printable form
+ * @param charset
+ * the charset for pString
+ * @return quoted-printable string
+ *
+ * @throws UnsupportedEncodingException
+ * Thrown if the charset is not supported
+ */
+ public String encode(String pString, String charset) throws UnsupportedEncodingException {
+ if (pString == null) {
+ return null;
+ }
+ return StringUtils.newStringUsAscii(encode(pString.getBytes(charset)));
+ }
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/RFC1522Codec.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/RFC1522Codec.java
new file mode 100644
index 0000000000..f11a450cb6
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/RFC1522Codec.java
@@ -0,0 +1,179 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.net;
+
+import java.io.UnsupportedEncodingException;
+
+import org.mozilla.apache.commons.codec.DecoderException;
+import org.mozilla.apache.commons.codec.EncoderException;
+import org.mozilla.apache.commons.codec.binary.StringUtils;
+
+/**
+ * <p>
+ * Implements methods common to all codecs defined in RFC 1522.
+ * </p>
+ *
+ * <p>
+ * <a href="http://www.ietf.org/rfc/rfc1522.txt">RFC 1522</a>
+ * describes techniques to allow the encoding of non-ASCII text in
+ * various portions of a RFC 822 [2] message header, in a manner which
+ * is unlikely to confuse existing message handling software.
+ * </p>
+
+ * @see <a href="http://www.ietf.org/rfc/rfc1522.txt">
+ * MIME (Multipurpose Internet Mail Extensions) Part Two:
+ * Message Header Extensions for Non-ASCII Text</a>
+ * </p>
+ *
+ * @author Apache Software Foundation
+ * @since 1.3
+ * @version $Id: RFC1522Codec.java 798428 2009-07-28 07:32:49Z ggregory $
+ */
+abstract class RFC1522Codec {
+
+ /**
+ * Separator.
+ */
+ protected static final char SEP = '?';
+
+ /**
+ * Prefix
+ */
+ protected static final String POSTFIX = "?=";
+
+ /**
+ * Postfix
+ */
+ protected static final String PREFIX = "=?";
+
+ /**
+ * Applies an RFC 1522 compliant encoding scheme to the given string of text with the
+ * given charset. This method constructs the "encoded-word" header common to all the
+ * RFC 1522 codecs and then invokes {@link #doEncoding(byte [])} method of a concrete
+ * class to perform the specific enconding.
+ *
+ * @param text a string to encode
+ * @param charset a charset to be used
+ *
+ * @return RFC 1522 compliant "encoded-word"
+ *
+ * @throws EncoderException thrown if there is an error conidition during the Encoding
+ * process.
+ * @throws UnsupportedEncodingException thrown if charset is not supported
+ *
+ * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+ */
+ protected String encodeText(final String text, final String charset)
+ throws EncoderException, UnsupportedEncodingException
+ {
+ if (text == null) {
+ return null;
+ }
+ StringBuffer buffer = new StringBuffer();
+ buffer.append(PREFIX);
+ buffer.append(charset);
+ buffer.append(SEP);
+ buffer.append(getEncoding());
+ buffer.append(SEP);
+ byte [] rawdata = doEncoding(text.getBytes(charset));
+ buffer.append(StringUtils.newStringUsAscii(rawdata));
+ buffer.append(POSTFIX);
+ return buffer.toString();
+ }
+
+ /**
+ * Applies an RFC 1522 compliant decoding scheme to the given string of text. This method
+ * processes the "encoded-word" header common to all the RFC 1522 codecs and then invokes
+ * {@link #doEncoding(byte [])} method of a concrete class to perform the specific deconding.
+ *
+ * @param text a string to decode
+ * @return A new decoded String or <code>null</code> if the input is <code>null</code>.
+ *
+ * @throws DecoderException thrown if there is an error conidition during the Decoding
+ * process.
+ * @throws UnsupportedEncodingException thrown if charset specified in the "encoded-word"
+ * header is not supported
+ */
+ protected String decodeText(final String text)
+ throws DecoderException, UnsupportedEncodingException
+ {
+ if (text == null) {
+ return null;
+ }
+ if ((!text.startsWith(PREFIX)) || (!text.endsWith(POSTFIX))) {
+ throw new DecoderException("RFC 1522 violation: malformed encoded content");
+ }
+ int terminator = text.length() - 2;
+ int from = 2;
+ int to = text.indexOf(SEP, from);
+ if (to == terminator) {
+ throw new DecoderException("RFC 1522 violation: charset token not found");
+ }
+ String charset = text.substring(from, to);
+ if (charset.equals("")) {
+ throw new DecoderException("RFC 1522 violation: charset not specified");
+ }
+ from = to + 1;
+ to = text.indexOf(SEP, from);
+ if (to == terminator) {
+ throw new DecoderException("RFC 1522 violation: encoding token not found");
+ }
+ String encoding = text.substring(from, to);
+ if (!getEncoding().equalsIgnoreCase(encoding)) {
+ throw new DecoderException("This codec cannot decode " +
+ encoding + " encoded content");
+ }
+ from = to + 1;
+ to = text.indexOf(SEP, from);
+ byte[] data = StringUtils.getBytesUsAscii(text.substring(from, to));
+ data = doDecoding(data);
+ return new String(data, charset);
+ }
+
+ /**
+ * Returns the codec name (referred to as encoding in the RFC 1522)
+ *
+ * @return name of the codec
+ */
+ protected abstract String getEncoding();
+
+ /**
+ * Encodes an array of bytes using the defined encoding scheme
+ *
+ * @param bytes Data to be encoded
+ *
+ * @return A byte array containing the encoded data
+ *
+ * @throws EncoderException thrown if the Encoder encounters a failure condition
+ * during the encoding process.
+ */
+ protected abstract byte[] doEncoding(byte[] bytes) throws EncoderException;
+
+ /**
+ * Decodes an array of bytes using the defined encoding scheme
+ *
+ * @param bytes Data to be decoded
+ *
+ * @return a byte array that contains decoded data
+ *
+ * @throws DecoderException A decoder exception is thrown if a Decoder encounters a
+ * failure condition during the decode process.
+ */
+ protected abstract byte[] doDecoding(byte[] bytes) throws DecoderException;
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/URLCodec.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/URLCodec.java
new file mode 100644
index 0000000000..74699310f7
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/URLCodec.java
@@ -0,0 +1,362 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.net;
+
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.BitSet;
+
+import org.mozilla.apache.commons.codec.BinaryDecoder;
+import org.mozilla.apache.commons.codec.BinaryEncoder;
+import org.mozilla.apache.commons.codec.DecoderException;
+import org.mozilla.apache.commons.codec.EncoderException;
+import org.mozilla.apache.commons.codec.CharEncoding;
+import org.mozilla.apache.commons.codec.StringDecoder;
+import org.mozilla.apache.commons.codec.StringEncoder;
+import org.mozilla.apache.commons.codec.binary.StringUtils;
+
+/**
+ * <p>Implements the 'www-form-urlencoded' encoding scheme,
+ * also misleadingly known as URL encoding.</p>
+ *
+ * <p>For more detailed information please refer to
+ * <a href="http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1">
+ * Chapter 17.13.4 'Form content types'</a> of the
+ * <a href="http://www.w3.org/TR/html4/">HTML 4.01 Specification<a></p>
+ *
+ * <p>
+ * This codec is meant to be a replacement for standard Java classes
+ * {@link java.net.URLEncoder} and {@link java.net.URLDecoder}
+ * on older Java platforms, as these classes in Java versions below
+ * 1.4 rely on the platform's default charset encoding.
+ * </p>
+ *
+ * @author Apache Software Foundation
+ * @since 1.2
+ * @version $Id: URLCodec.java 1079537 2011-03-08 20:56:19Z ggregory $
+ */
+public class URLCodec implements BinaryEncoder, BinaryDecoder, StringEncoder, StringDecoder {
+
+ /**
+ * Radix used in encoding and decoding.
+ */
+ static final int RADIX = 16;
+
+ /**
+ * The default charset used for string decoding and encoding. Consider this field final. The next major release may
+ * break compatibility and make this field be final.
+ */
+ protected String charset;
+
+ /**
+ * Release 1.5 made this field final.
+ */
+ protected static final byte ESCAPE_CHAR = '%';
+ /**
+ * BitSet of www-form-url safe characters.
+ */
+ protected static final BitSet WWW_FORM_URL = new BitSet(256);
+
+ // Static initializer for www_form_url
+ static {
+ // alpha characters
+ for (int i = 'a'; i <= 'z'; i++) {
+ WWW_FORM_URL.set(i);
+ }
+ for (int i = 'A'; i <= 'Z'; i++) {
+ WWW_FORM_URL.set(i);
+ }
+ // numeric characters
+ for (int i = '0'; i <= '9'; i++) {
+ WWW_FORM_URL.set(i);
+ }
+ // special chars
+ WWW_FORM_URL.set('-');
+ WWW_FORM_URL.set('_');
+ WWW_FORM_URL.set('.');
+ WWW_FORM_URL.set('*');
+ // blank to be replaced with +
+ WWW_FORM_URL.set(' ');
+ }
+
+
+ /**
+ * Default constructor.
+ */
+ public URLCodec() {
+ this(CharEncoding.UTF_8);
+ }
+
+ /**
+ * Constructor which allows for the selection of a default charset
+ *
+ * @param charset the default string charset to use.
+ */
+ public URLCodec(String charset) {
+ super();
+ this.charset = charset;
+ }
+
+ /**
+ * Encodes an array of bytes into an array of URL safe 7-bit characters. Unsafe characters are escaped.
+ *
+ * @param urlsafe
+ * bitset of characters deemed URL safe
+ * @param bytes
+ * array of bytes to convert to URL safe characters
+ * @return array of bytes containing URL safe characters
+ */
+ public static final byte[] encodeUrl(BitSet urlsafe, byte[] bytes) {
+ if (bytes == null) {
+ return null;
+ }
+ if (urlsafe == null) {
+ urlsafe = WWW_FORM_URL;
+ }
+
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ for (int i = 0; i < bytes.length; i++) {
+ int b = bytes[i];
+ if (b < 0) {
+ b = 256 + b;
+ }
+ if (urlsafe.get(b)) {
+ if (b == ' ') {
+ b = '+';
+ }
+ buffer.write(b);
+ } else {
+ buffer.write(ESCAPE_CHAR);
+ char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, RADIX));
+ char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, RADIX));
+ buffer.write(hex1);
+ buffer.write(hex2);
+ }
+ }
+ return buffer.toByteArray();
+ }
+
+ /**
+ * Decodes an array of URL safe 7-bit characters into an array of
+ * original bytes. Escaped characters are converted back to their
+ * original representation.
+ *
+ * @param bytes array of URL safe characters
+ * @return array of original bytes
+ * @throws DecoderException Thrown if URL decoding is unsuccessful
+ */
+ public static final byte[] decodeUrl(byte[] bytes) throws DecoderException {
+ if (bytes == null) {
+ return null;
+ }
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ for (int i = 0; i < bytes.length; i++) {
+ int b = bytes[i];
+ if (b == '+') {
+ buffer.write(' ');
+ } else if (b == ESCAPE_CHAR) {
+ try {
+ int u = Utils.digit16(bytes[++i]);
+ int l = Utils.digit16(bytes[++i]);
+ buffer.write((char) ((u << 4) + l));
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new DecoderException("Invalid URL encoding: ", e);
+ }
+ } else {
+ buffer.write(b);
+ }
+ }
+ return buffer.toByteArray();
+ }
+
+ /**
+ * Encodes an array of bytes into an array of URL safe 7-bit
+ * characters. Unsafe characters are escaped.
+ *
+ * @param bytes array of bytes to convert to URL safe characters
+ * @return array of bytes containing URL safe characters
+ */
+ public byte[] encode(byte[] bytes) {
+ return encodeUrl(WWW_FORM_URL, bytes);
+ }
+
+
+ /**
+ * Decodes an array of URL safe 7-bit characters into an array of
+ * original bytes. Escaped characters are converted back to their
+ * original representation.
+ *
+ * @param bytes array of URL safe characters
+ * @return array of original bytes
+ * @throws DecoderException Thrown if URL decoding is unsuccessful
+ */
+ public byte[] decode(byte[] bytes) throws DecoderException {
+ return decodeUrl(bytes);
+ }
+
+ /**
+ * Encodes a string into its URL safe form using the specified string charset. Unsafe characters are escaped.
+ *
+ * @param pString
+ * string to convert to a URL safe form
+ * @param charset
+ * the charset for pString
+ * @return URL safe string
+ * @throws UnsupportedEncodingException
+ * Thrown if charset is not supported
+ */
+ public String encode(String pString, String charset) throws UnsupportedEncodingException {
+ if (pString == null) {
+ return null;
+ }
+ return StringUtils.newStringUsAscii(encode(pString.getBytes(charset)));
+ }
+
+ /**
+ * Encodes a string into its URL safe form using the default string
+ * charset. Unsafe characters are escaped.
+ *
+ * @param pString string to convert to a URL safe form
+ * @return URL safe string
+ * @throws EncoderException Thrown if URL encoding is unsuccessful
+ *
+ * @see #getDefaultCharset()
+ */
+ public String encode(String pString) throws EncoderException {
+ if (pString == null) {
+ return null;
+ }
+ try {
+ return encode(pString, getDefaultCharset());
+ } catch (UnsupportedEncodingException e) {
+ throw new EncoderException(e.getMessage(), e);
+ }
+ }
+
+
+ /**
+ * Decodes a URL safe string into its original form using the
+ * specified encoding. Escaped characters are converted back
+ * to their original representation.
+ *
+ * @param pString URL safe string to convert into its original form
+ * @param charset the original string charset
+ * @return original string
+ * @throws DecoderException Thrown if URL decoding is unsuccessful
+ * @throws UnsupportedEncodingException Thrown if charset is not
+ * supported
+ */
+ public String decode(String pString, String charset) throws DecoderException, UnsupportedEncodingException {
+ if (pString == null) {
+ return null;
+ }
+ return new String(decode(StringUtils.getBytesUsAscii(pString)), charset);
+ }
+
+ /**
+ * Decodes a URL safe string into its original form using the default
+ * string charset. Escaped characters are converted back to their
+ * original representation.
+ *
+ * @param pString URL safe string to convert into its original form
+ * @return original string
+ * @throws DecoderException Thrown if URL decoding is unsuccessful
+ *
+ * @see #getDefaultCharset()
+ */
+ public String decode(String pString) throws DecoderException {
+ if (pString == null) {
+ return null;
+ }
+ try {
+ return decode(pString, getDefaultCharset());
+ } catch (UnsupportedEncodingException e) {
+ throw new DecoderException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Encodes an object into its URL safe form. Unsafe characters are
+ * escaped.
+ *
+ * @param pObject string to convert to a URL safe form
+ * @return URL safe object
+ * @throws EncoderException Thrown if URL encoding is not
+ * applicable to objects of this type or
+ * if encoding is unsuccessful
+ */
+ public Object encode(Object pObject) throws EncoderException {
+ if (pObject == null) {
+ return null;
+ } else if (pObject instanceof byte[]) {
+ return encode((byte[])pObject);
+ } else if (pObject instanceof String) {
+ return encode((String)pObject);
+ } else {
+ throw new EncoderException("Objects of type " +
+ pObject.getClass().getName() + " cannot be URL encoded");
+
+ }
+ }
+
+ /**
+ * Decodes a URL safe object into its original form. Escaped characters are converted back to their original
+ * representation.
+ *
+ * @param pObject
+ * URL safe object to convert into its original form
+ * @return original object
+ * @throws DecoderException
+ * Thrown if the argument is not a <code>String</code> or <code>byte[]</code>. Thrown if a failure condition is
+ * encountered during the decode process.
+ */
+ public Object decode(Object pObject) throws DecoderException {
+ if (pObject == null) {
+ return null;
+ } else if (pObject instanceof byte[]) {
+ return decode((byte[]) pObject);
+ } else if (pObject instanceof String) {
+ return decode((String) pObject);
+ } else {
+ throw new DecoderException("Objects of type " + pObject.getClass().getName() + " cannot be URL decoded");
+
+ }
+ }
+
+ /**
+ * The <code>String</code> encoding used for decoding and encoding.
+ *
+ * @return Returns the encoding.
+ *
+ * @deprecated Use {@link #getDefaultCharset()}, will be removed in 2.0.
+ */
+ public String getEncoding() {
+ return this.charset;
+ }
+
+ /**
+ * The default charset used for string decoding and encoding.
+ *
+ * @return the default string charset.
+ */
+ public String getDefaultCharset() {
+ return this.charset;
+ }
+
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/Utils.java b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/Utils.java
new file mode 100644
index 0000000000..adfe845133
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/Utils.java
@@ -0,0 +1,50 @@
+// Mozilla has modified this file - see http://hg.mozilla.org/ for details.
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.apache.commons.codec.net;
+
+import org.mozilla.apache.commons.codec.DecoderException;
+
+/**
+ * Utility methods for this package.
+ *
+ * @author <a href="mailto:ggregory@seagullsw.com">Gary Gregory</a>
+ * @version $Id: Utils.java 798611 2009-07-28 17:10:44Z ggregory $
+ * @since 1.4
+ */
+class Utils {
+
+ /**
+ * Returns the numeric value of the character <code>b</code> in radix 16.
+ *
+ * @param b
+ * The byte to be converted.
+ * @return The numeric value represented by the character in radix 16.
+ *
+ * @throws DecoderException
+ * Thrown when the byte is not valid per {@link Character#digit(char,int)}
+ */
+ static int digit16(byte b) throws DecoderException {
+ int i = Character.digit((char) b, 16);
+ if (i == -1) {
+ throw new DecoderException("Invalid URL encoding: not a valid digit (radix " + URLCodec.RADIX + "): " + b);
+ }
+ return i;
+ }
+
+}
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/package.html b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/package.html
new file mode 100644
index 0000000000..2b8ceab2eb
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/net/package.html
@@ -0,0 +1,23 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<html>
+ <body>
+ <p>
+ Network related encoding and decoding.
+ </p>
+ </body>
+</html>
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/overview.html b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/overview.html
new file mode 100644
index 0000000000..29939dba54
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/overview.html
@@ -0,0 +1,29 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<!-- $Id: overview.html 561548 2007-07-31 21:16:26Z mbenson $ -->
+<html>
+<body>
+<p>
+This document is the API specification for the Apache Commons Codec Library, version 1.3.
+</p>
+<p>
+This library requires a JRE version of 1.2.2 or greater.
+The hypertext links originating from this document point to Sun's version 1.3 API as the 1.2.2 API documentation
+is no longer on-line.
+</p>
+</body>
+</html>
diff --git a/mobile/android/thirdparty/org/mozilla/apache/commons/codec/package.html b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/package.html
new file mode 100644
index 0000000000..da4a3ea529
--- /dev/null
+++ b/mobile/android/thirdparty/org/mozilla/apache/commons/codec/package.html
@@ -0,0 +1,100 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+ <head>
+ </head>
+ <body>
+ <p>Interfaces and classes used by
+ the various implementations in the sub-packages.</p>
+
+ <p>Definitive implementations of commonly used encoders and decoders.</p>
+
+ <p>Codec is currently comprised of a modest set of utilities and a
+ simple framework for String encoding and decoding in three categories:
+ Binary Encoders, Language Encoders, and Network Encoders. </p>
+
+ <h4><a name="Common Encoders">Binary Encoders</a></h4>
+
+ <table border="1" width="100%" cellspacing="2" cellpadding="3">
+ <tbody>
+ <tr>
+ <td>
+ <a href="binary/Base64.html">
+ org.apache.commons.codec.binary.Base64</a>
+ </td>
+ <td>
+ Provides Base64 content-transfer-encoding as defined in
+ <a href="http://www.ietf.org/rfc/rfc2045.txt"> RFC 2045</a>
+ </td>
+ <td>Production</td>
+ </tr>
+ <tr>
+ <td>
+ <a href="binary/Hex.html">
+ org.apache.commons.codec.binary.Hex</a>
+ </td>
+ <td>
+ Converts an array of bytes into an array of characters
+ representing the hexidecimal values of each byte in order
+ </td>
+ <td>Production</td>
+ </tr>
+ </tbody>
+ </table>
+ <h4>
+ <a name="Language Encoders">Language Encoders</a>
+ </h4>
+ <p>
+ Codec contains a number of commonly used language and phonetic
+ encoders
+ </p>
+ <table border="1" width="100%" cellspacing="2" cellpadding="3">
+ <tbody>
+ <tr>
+ <td>
+ <a href="#">org.apache.commons.codec.language.Soundex</a>
+ </td>
+ <td>Implementation of the Soundex algorithm.</td>
+ <td>Production</td>
+ </tr>
+ <tr>
+ <td>
+ <a href="#">org.apache.commons.codec.language.Metaphone</a>
+ </td>
+ <td>Implementation of the Metaphone algorithm.</td>
+ <td>Production</td>
+ </tr>
+ </tbody>
+ </table>
+ <h4><a name="Network_Encoders">Network Encoders</a></h4>
+ <h4> </h4>
+ <p> Codec contains network related encoders </p>
+ <table border="1" width="100%" cellspacing="2" cellpadding="3">
+ <tbody>
+ <tr>
+ <td>
+ <a href="#">org.apache.commons.codec.net.URLCodec</a>
+ </td>
+ <td>Implements the 'www-form-urlencoded' encoding scheme.</td>
+ <td>Production</td>
+ </tr>
+ </tbody>
+ </table>
+ <br>
+ </body>
+</html>
diff --git a/mobile/locales/Makefile.in b/mobile/locales/Makefile.in
new file mode 100644
index 0000000000..d4f0237b9f
--- /dev/null
+++ b/mobile/locales/Makefile.in
@@ -0,0 +1,151 @@
+# -*- makefile -*-
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#####################################################################################
+# Dependency build overhead:
+# o always create/update/hard link targets boomkarks.json & searchplugins
+# o latest symlink will be correct for the current locale / local build.
+# o logic is essentially FORCE for language packs w/o all the build overhead
+# o phase 2: replace hard links with a user function able to derive path
+# based on current locale.
+#####################################################################################
+
+include $(topsrcdir)/config/config.mk
+
+USE_AUTOTARGETS_MK=1
+include $(topsrcdir)/config/makefiles/makeutils.mk
+
+# Separate items of contention
+tgt-gendir = .deps/generated_$(AB_CD)
+
+GENERATED_DIRS += .deps
+
+ifdef LOCALE_MERGEDIR
+vpath book%.inc $(LOCALE_MERGEDIR)/mobile/profile
+endif
+vpath book%.inc $(LOCALE_SRCDIR)/profile
+ifdef LOCALE_MERGEDIR
+vpath book%.inc @srcdir@/en-US/profile
+endif
+
+$(call errorIfEmpty,MOZ_BRANDING_DIRECTORY)
+SUBMAKEFILES += \
+ $(DEPTH)/$(MOZ_BRANDING_DIRECTORY)/Makefile \
+ $(DEPTH)/$(MOZ_BRANDING_DIRECTORY)/locales/Makefile \
+ $(NULL)
+
+###########################################################################
+# Default target, preserve existing functionality for:
+# gmake -C $obj/mobile/locales
+###########################################################################
+search-jar-default: search-jar
+
+
+###########################################################################
+## Searchlist plugin config
+plugin-file-array = \
+ $(wildcard $(LOCALE_SRCDIR)/searchplugins/list.txt) \
+ $(srcdir)/en-US/searchplugins/list.txt \
+ $(NULL)
+
+###########################################################################
+plugin_file = $(firstword $(plugin-file-array))
+plugin-file-ts = $(tgt-gendir)/$(subst $(topsrcdir)/,$(NULL),$(plugin_file)).ts
+
+GARBAGE += $(plugin-file-ts)
+# ---------------------------------------------------------------------------
+# plugin-file-ts track searchlist file used ($path/list.txt)
+# and time when the file was last modified.
+###########################################################################
+plugin-file-ts-preqs = \
+ $(call mkdir_deps,$(dir $(plugin-file-ts))) \
+ $(plugin_file) \
+ $(NULL)
+
+###########################################################################
+# Detect locale changes. Force stale deps when searchlist file
+# or content has changed.
+$(plugin-file-ts): $(plugin-file-ts-preqs)
+ @touch $@
+
+
+###########################################################################
+search-jar-common = tmp-search.jar.mn
+search-jar = $(tgt-gendir)/$(search-jar-common)
+search-jar-ts = $(search-jar).ts
+
+GARBAGE += $(search-jar) $(search-jar-ts) $(search-jar-common)
+# ---------------------------------------------------------------------------
+# search-jar contains a list of providers for the search plugin
+###########################################################################
+SEARCH_PLUGINS = $(shell cat $(plugin_file))
+SEARCH_PLUGINS := $(subst :hidden,,$(SEARCH_PLUGINS))
+$(call errorIfEmpty,SEARCH_PLUGINS)
+
+search-jar-preqs = \
+ $(plugin-file-ts) \
+ $(if $(IS_LANGUAGE_REPACK),FORCE) \
+ $(NULL)
+
+.PHONY: search-jar
+search-jar: $(search-jar)
+$(search-jar): $(search-jar-preqs)
+ @echo '\nGenerating: search-jar'
+ printf '$(AB_CD).jar:' > $@
+ ln -fn $@ .
+ printf '$(foreach plugin,$(SEARCH_PLUGINS),$(subst __PLUGIN_SUBST__,$(plugin), \n locale/$(AB_CD)/browser/searchplugins/__PLUGIN_SUBST__.xml (__PLUGIN_SUBST__.xml)))' >> $@
+ @echo >> $@
+
+###################
+search-dir-deps = \
+ $(plugin-file) \
+ $(dir-chrome) \
+ $(NULL)
+
+search-preqs =\
+ $(call mkdir_deps,$(dir $(search-jar-ts))) \
+ $(call mkdir_deps,$(FINAL_TARGET)/chrome) \
+ $(search-jar) \
+ $(search-dir-deps) \
+ $(if $(IS_LANGUAGE_REPACK),FORCE) \
+ $(GLOBAL_DEPS) \
+ $(NULL)
+
+.PHONY: searchplugins
+searchplugins: $(search-preqs)
+ $(call py_action,jar_maker,\
+ $(QUIET) -d $(FINAL_TARGET) \
+ -s $(topsrcdir)/$(relativesrcdir)/en-US/searchplugins \
+ -s $(LOCALE_SRCDIR)/searchplugins \
+ $(MAKE_JARS_FLAGS) $(search-jar))
+ $(TOUCH) $@
+
+include $(topsrcdir)/config/rules.mk
+
+
+#############
+libs-preqs =\
+ $(call mkdir-deps,$(DIST)/install) \
+ $(NULL)
+
+libs-%: $(libs-preqs)
+ $(display-deps)
+ @$(MAKE) -C $(DEPTH)/toolkit/locales libs-$*
+ @$(MAKE) -C $(DEPTH)/intl/locales AB_CD=$* XPI_NAME=locale-$*
+ @$(MAKE) -B searchplugins AB_CD=$* XPI_NAME=locale-$*
+ @$(MAKE) libs AB_CD=$* XPI_NAME=locale-$* PREF_DIR=defaults/pref
+ @$(MAKE) -C $(DEPTH)/$(MOZ_BRANDING_DIRECTORY)/locales AB_CD=$* XPI_NAME=locale-$*
+
+# Tailored target to just add the chrome processing for multi-locale builds
+chrome-%:
+ $(display-deps)
+ @$(MAKE) -B searchplugins AB_CD=$*
+ @$(MAKE) chrome AB_CD=$*
+ @$(MAKE) -C $(DEPTH)/$(MOZ_BRANDING_DIRECTORY)/locales chrome AB_CD=$*
+
+NO_JA_JP_MAC_AB_CD := $(if $(filter ja-JP-mac, $(AB_CD)),ja,$(AB_CD))
+
+
+export:: searchplugins
diff --git a/mobile/locales/en-US/chrome/region.properties b/mobile/locales/en-US/chrome/region.properties
new file mode 100644
index 0000000000..6d89a6f0fe
--- /dev/null
+++ b/mobile/locales/en-US/chrome/region.properties
@@ -0,0 +1,96 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Default search engine
+browser.search.defaultenginename=Google
+
+# Search engine order (order displayed in the search bar dropdown).
+browser.search.order.1=Google
+browser.search.order.2=Yahoo
+browser.search.order.3=Bing
+
+# These override the equivalents above when the client detects that it is in
+# US market only.
+browser.search.defaultenginename.US=Yahoo
+browser.search.order.US.1=Yahoo
+browser.search.order.US.2=Google
+browser.search.order.US.3=Bing
+
+# increment this number when anything gets changed in the list below. This will
+# cause Firefox to re-read these prefs and inject any new handlers into the
+# profile database. Note that "new" is defined as "has a different URL"; this
+# means that it's not possible to update the name of existing handler, so
+# don't make any spelling errors here.
+gecko.handlerService.defaultHandlersVersion=3
+
+# The default set of protocol handlers for webcal:
+gecko.handlerService.schemes.webcal.0.name=30 Boxes
+gecko.handlerService.schemes.webcal.0.uriTemplate=https://30boxes.com/external/widget?refer=ff&url=%s
+
+# The default set of protocol handlers for mailto:
+gecko.handlerService.schemes.mailto.0.name=Yahoo! Mail
+gecko.handlerService.schemes.mailto.0.uriTemplate=https://compose.mail.yahoo.com/?To=%s
+gecko.handlerService.schemes.mailto.1.name=Gmail
+gecko.handlerService.schemes.mailto.1.uriTemplate=https://mail.google.com/mail/?extsrc=mailto&url=%s
+
+# This is the default set of web based feed handlers shown in the reader
+# selection UI
+browser.contentHandlers.types.0.title=My Yahoo!
+browser.contentHandlers.types.0.uri=https://add.my.yahoo.com/rss?url=%s
+
+# Order of suggested websites displayed in the Top Sites panel.
+# Values for these keys must correspond to the name used in the keys that
+# define each suggested website's details. For example:
+# browser.suggestedsites.list.0=NAME
+# browser.suggestedsites.NAME.title=Displayed name
+# browser.suggestedsites.NAME.url=Website URL
+# browser.suggestedsites.NAME.bgcolor= Color (hex format)
+#
+# Note that if you remove or add items to this set, you need to adjust
+# mobile/android/tests/browser/robocop/testDistribution.java
+# to reflect the new set of IDs reported as tiles data.
+#
+browser.suggestedsites.list.0=facebook
+browser.suggestedsites.list.1=youtube
+browser.suggestedsites.list.2=amazon
+browser.suggestedsites.list.3=wikipedia
+browser.suggestedsites.list.4=twitter
+
+browser.suggestedsites.facebook.title=Facebook
+browser.suggestedsites.facebook.url=https://m.facebook.com/
+browser.suggestedsites.facebook.bgcolor=#385185
+
+browser.suggestedsites.youtube.title=YouTube
+browser.suggestedsites.youtube.url=https://m.youtube.com/
+browser.suggestedsites.youtube.bgcolor=#cd201f
+
+browser.suggestedsites.amazon.title=Amazon
+browser.suggestedsites.amazon.url=https://www.amazon.com/
+browser.suggestedsites.amazon.bgcolor=#000000
+
+browser.suggestedsites.wikipedia.title=Wikipedia
+browser.suggestedsites.wikipedia.url=https://www.wikipedia.org/
+browser.suggestedsites.wikipedia.bgcolor=#000000
+
+browser.suggestedsites.twitter.title=Twitter
+browser.suggestedsites.twitter.url=https://mobile.twitter.com/
+browser.suggestedsites.twitter.bgcolor=#55acee
+
+browser.suggestedsites.restricted.list.0=restricted_fxsupport
+browser.suggestedsites.restricted.list.1=webmaker
+browser.suggestedsites.restricted.list.2=restricted_mozilla
+
+browser.suggestedsites.restricted_fxsupport.title=Firefox Help and Support for restricted profiles on Android tablets
+browser.suggestedsites.restricted_fxsupport.url=https://support.mozilla.org/kb/controlledaccess
+browser.suggestedsites.restricted_fxsupport.bgcolor=#f37c00
+
+browser.suggestedsites.webmaker.title=Learn the Web: Mozilla Webmaker
+browser.suggestedsites.webmaker.url=https://webmaker.org/
+browser.suggestedsites.webmaker.bgcolor=#f37c00
+
+# LOCALIZATION NOTE: browser.suggestedsites.restricted_mozilla.url must be different from browser.suggestedsites.mozilla.url
+browser.suggestedsites.restricted_mozilla.title=The Mozilla Project
+browser.suggestedsites.restricted_mozilla.url=https://www.mozilla.org
+browser.suggestedsites.restricted_mozilla.bgcolor=#ce4e41
+browser.suggestedsites.restricted_mozilla.trackingid=632
diff --git a/mobile/locales/en-US/overrides/appstrings.properties b/mobile/locales/en-US/overrides/appstrings.properties
new file mode 100644
index 0000000000..7eab5f794c
--- /dev/null
+++ b/mobile/locales/en-US/overrides/appstrings.properties
@@ -0,0 +1,41 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+malformedURI=The URL is not valid and cannot be loaded.
+fileNotFound=Firefox can't find the file at %S.
+fileAccessDenied=The file at %S is not readable.
+dnsNotFound=Firefox can't find the server at %S.
+unknownProtocolFound=Firefox doesn't know how to open this address, because one of the following protocols (%S) isn't associated with any program or is not allowed in this context.
+connectionFailure=Firefox can't establish a connection to the server at %S.
+netInterrupt=The connection to %S was interrupted while the page was loading.
+netTimeout=The server at %S is taking too long to respond.
+redirectLoop=Firefox has detected that the server is redirecting the request for this address in a way that will never complete.
+## LOCALIZATION NOTE (confirmRepostPrompt): In this item, don't translate "%S"
+confirmRepostPrompt=To display this page, %S must send information that will repeat any action (such as a search or order confirmation) that was performed earlier.
+resendButton.label=Resend
+unknownSocketType=Firefox doesn't know how to communicate with the server.
+netReset=The connection to the server was reset while the page was loading.
+notCached=This document is no longer available.
+netOffline=Firefox is currently in offline mode and can't browse the Web.
+isprinting=The document cannot change while Printing or in Print Preview.
+deniedPortAccess=This address uses a network port which is normally used for purposes other than Web browsing. Firefox has canceled the request for your protection.
+proxyResolveFailure=Firefox is configured to use a proxy server that can't be found.
+proxyConnectFailure=Firefox is configured to use a proxy server that is refusing connections.
+contentEncodingError=The page you are trying to view cannot be shown because it uses an invalid or unsupported form of compression.
+unsafeContentType=The page you are trying to view cannot be shown because it is contained in a file type that may not be safe to open. Please contact the website owners to inform them of this problem.
+externalProtocolTitle=External Protocol Request
+externalProtocolPrompt=An external application must be launched to handle %1$S: links.\n\n\nRequested link:\n\n%2$S\n\nApplication: %3$S\n\n\nIf you were not expecting this request it may be an attempt to exploit a weakness in that other program. Cancel this request unless you are sure it is not malicious.\n
+#LOCALIZATION NOTE (externalProtocolUnknown): The following string is shown if the application name can't be determined
+externalProtocolUnknown=<Unknown>
+externalProtocolChkMsg=Remember my choice for all links of this type.
+externalProtocolLaunchBtn=Launch application
+malwareBlocked=The site at %S has been reported as an attack site and has been blocked based on your security preferences.
+deceptiveBlocked=This web page at %S has been reported as a deceptive site and has been blocked based on your security preferences.
+unwantedBlocked=The site at %S has been reported as serving unwanted software and has been blocked based on your security preferences.
+cspBlocked=This page has a content security policy that prevents it from being loaded in this way.
+corruptedContentErrorv2=The site at %S has experienced a network protocol violation that cannot be repaired.
+remoteXUL=This page uses an unsupported technology that is no longer available by default in Firefox.
+sslv3Used=Firefox cannot guarantee the safety of your data on %S because it uses SSLv3, a broken security protocol.
+weakCryptoUsed=The owner of %S has configured their website improperly. To protect your information from being stolen, Firefox has not connected to this website.
+inadequateSecurityError=The website tried to negotiate an inadequate level of security.
diff --git a/mobile/locales/en-US/overrides/netError.dtd b/mobile/locales/en-US/overrides/netError.dtd
new file mode 100644
index 0000000000..82d2a8e371
--- /dev/null
+++ b/mobile/locales/en-US/overrides/netError.dtd
@@ -0,0 +1,222 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+
+<!ENTITY loadError.label "Problem loading page">
+<!ENTITY retry.label "Try Again">
+
+<!-- Specific error messages -->
+
+<!ENTITY connectionFailure.title "Unable to connect">
+<!ENTITY connectionFailure.longDesc2 "&sharedLongDesc3;">
+
+<!ENTITY deniedPortAccess.title "This address is restricted">
+<!ENTITY deniedPortAccess.longDesc "">
+
+<!ENTITY dnsNotFound.title "Server not found">
+<!-- LOCALIZATION NOTE (dnsNotFound.longDesc4) This string contains markup including widgets for searching
+ or enabling wifi connections. The text inside tags should be localized. Do not change the ids. -->
+<!ENTITY dnsNotFound.longDesc4 "
+<ul>
+ <li>Check the address for typing errors such as
+ <strong>ww</strong>.example.com instead of
+ <strong>www</strong>.example.com</li>
+ <div id='searchbox'>
+ <input id='searchtext' type='search'></input>
+ <button id='searchbutton'>Search</button>
+ </div>
+ <li>If you are unable to load any pages, check your device’s data or Wi-Fi connection.
+ <button id='wifi'>Enable Wi-Fi</button>
+ </li>
+</ul>
+">
+
+<!ENTITY fileNotFound.title "File not found">
+<!ENTITY fileNotFound.longDesc "
+<ul>
+ <li>Check the file name for capitalization or other typing errors.</li>
+ <li>Check to see if the file was moved, renamed or deleted.</li>
+</ul>
+">
+
+<!ENTITY fileAccessDenied.title "Access to the file was denied">
+<!ENTITY fileAccessDenied.longDesc "
+<ul>
+ <li>It may have been removed, moved, or file permissions may be preventing access.</li>
+</ul>
+">
+
+<!ENTITY generic.title "Oops.">
+<!ENTITY generic.longDesc "
+<p>&brandShortName; can’t load this page for some reason.</p>
+">
+
+<!ENTITY malformedURI.title "The address isn’t valid">
+<!-- LOCALIZATION NOTE (malformedURI.longDesc2) This string contains markup including widgets for searching
+ or enabling wifi connections. The text inside the tags should be localized. Do not touch the ids. -->
+<!ENTITY malformedURI.longDesc2 "
+<ul>
+ <li>Web addresses are usually written like
+ <strong>http://www.example.com/</strong></li>
+ <div id='searchbox'>
+ <input id='searchtext' type='search'></input>
+ <button id='searchbutton'>Search</button>
+ </div>
+ <li>Make sure that you’re using forward slashes (i.e.
+ <strong>/</strong>).</li>
+</ul>
+">
+
+<!ENTITY netInterrupt.title "The connection was interrupted">
+<!ENTITY netInterrupt.longDesc2 "&sharedLongDesc3;">
+
+<!ENTITY notCached.title "Document Expired">
+<!ENTITY notCached.longDesc "<p>The requested document is not available in &brandShortName;’s cache.</p><ul><li>As a security precaution, &brandShortName; does not automatically re-request sensitive documents.</li><li>Click Try Again to re-request the document from the website.</li></ul>">
+
+<!ENTITY netOffline.title "Offline mode">
+<!-- LOCALIZATION NOTE (netOffline.longDesc3) This string contains markup including widgets enabling wifi connections.
+ The text inside the tags should be localized. Do not touch the ids. -->
+<!ENTITY netOffline.longDesc3 "
+<ul>
+ <li>Try again. &brandShortName; will attempt to open a connection and reload the page.
+ <button id='wifi'>Enable Wi-Fi</button>
+ </li>
+</ul>
+">
+
+<!ENTITY contentEncodingError.title "Content Encoding Error">
+<!ENTITY contentEncodingError.longDesc "
+<ul>
+ <li>Please contact the website owners to inform them of this problem.</li>
+</ul>
+">
+
+<!ENTITY unsafeContentType.title "Unsafe File Type">
+<!ENTITY unsafeContentType.longDesc "
+<ul>
+ <li>Please contact the website owners to inform them of this problem.</li>
+</ul>
+">
+
+<!ENTITY netReset.title "The connection was reset">
+<!ENTITY netReset.longDesc2 "&sharedLongDesc3;">
+
+<!ENTITY netTimeout.title "The connection has timed out">
+<!ENTITY netTimeout.longDesc2 "&sharedLongDesc3;">
+
+<!ENTITY unknownProtocolFound.title "The address wasn’t understood">
+<!ENTITY unknownProtocolFound.longDesc "
+<ul>
+ <li>You might need to install other software to open this address.</li>
+</ul>
+">
+
+<!ENTITY proxyConnectFailure.title "The proxy server is refusing connections">
+<!ENTITY proxyConnectFailure.longDesc "
+<ul>
+ <li>Check the proxy settings to make sure that they are correct.</li>
+ <li>Contact your network administrator to make sure the proxy server is
+ working.</li>
+</ul>
+">
+
+<!ENTITY proxyResolveFailure.title "Unable to find the proxy server">
+<!-- LOCALIZATION NOTE (proxyResolveFailure.longDesc3) This string contains markup including widgets for enabling wifi connections.
+ The text inside the tags should be localized. Do not touch the ids. -->
+<!ENTITY proxyResolveFailure.longDesc3 "
+<ul>
+ <li>Check the proxy settings to make sure that they are correct.</li>
+ <li>Check to make sure your device has a working data or Wi-Fi connection.
+ <button id='wifi'>Enable Wi-Fi</button>
+ </li>
+</ul>
+">
+
+<!ENTITY redirectLoop.title "The page isn’t redirecting properly">
+<!ENTITY redirectLoop.longDesc "
+<ul>
+ <li>This problem can sometimes be caused by disabling or refusing to accept
+ cookies.</li>
+</ul>
+">
+
+<!ENTITY unknownSocketType.title "Unexpected response from server">
+<!ENTITY unknownSocketType.longDesc "
+<ul>
+ <li>Check to make sure your system has the Personal Security Manager
+ installed.</li>
+ <li>This might be due to a non-standard configuration on the server.</li>
+</ul>
+">
+
+<!ENTITY nssFailure2.title "Secure Connection Failed">
+<!ENTITY nssFailure2.longDesc2 "
+<ul>
+ <li>The page you are trying to view cannot be shown because the authenticity of the received data could not be verified.</li>
+ <li>Please contact the website owners to inform them of this problem.</li>
+</ul>
+">
+
+<!ENTITY nssBadCert.title "Secure Connection Failed">
+<!ENTITY nssBadCert.longDesc2 "
+<ul>
+ <li>This could be a problem with the server’s configuration, or it could be
+someone trying to impersonate the server.</li>
+ <li>If you have connected to this server successfully in the past, the error may
+be temporary, and you can try again later.</li>
+</ul>
+">
+
+<!-- LOCALIZATION NOTE (sharedLongDesc3) This string contains markup including widgets for enabling wifi connections.
+ The text inside the tags should be localized. Do not touch the ids. -->
+<!ENTITY sharedLongDesc3 "
+<ul>
+ <li>The site could be temporarily unavailable or too busy. Try again in a few moments.</li>
+ <li>If you are unable to load any pages, check your mobile device’s data or Wi-Fi connection.
+ <button id='wifi'>Enable Wi-Fi</button>
+ </li>
+</ul>
+">
+
+<!ENTITY cspBlocked.title "Blocked by Content Security Policy">
+<!ENTITY cspBlocked.longDesc "<p>&brandShortName; prevented this page from loading in this way because the page has a content security policy that disallows it.</p>">
+
+<!ENTITY corruptedContentErrorv2.title "Corrupted Content Error">
+<!ENTITY corruptedContentErrorv2.longDesc "<p>The page you are trying to view cannot be shown because an error in the data transmission was detected.</p><ul><li>Please contact the website owners to inform them of this problem.</li></ul>">
+
+<!ENTITY securityOverride.linkText "Or you can add an exception…">
+<!ENTITY securityOverride.getMeOutOfHereButton "Get me out of here!">
+<!ENTITY securityOverride.exceptionButtonLabel "Add Exception…">
+
+<!-- LOCALIZATION NOTE (securityOverride.warningContent) - Do not translate the
+contents of the <xul:button> tags. The only language content is the label= field,
+which uses strings already defined above. The button is included here (instead of
+netError.xhtml) because it exposes functionality specific to firefox. -->
+
+<!ENTITY securityOverride.warningContent "
+<p>You should not add an exception if you are using an internet connection that you do not trust completely or if you are not used to seeing a warning for this server.</p>
+
+<button id='getMeOutOfHereButton'>&securityOverride.getMeOutOfHereButton;</button>
+<button id='exceptionDialogButton'>&securityOverride.exceptionButtonLabel;</button>
+">
+
+<!ENTITY remoteXUL.title "Remote XUL">
+<!ENTITY remoteXUL.longDesc "<p><ul><li>Please contact the website owners to inform them of this problem.</li></ul></p>">
+
+<!ENTITY sslv3Used.title "Unable to Connect Securely">
+<!-- LOCALIZATION NOTE (sslv3Used.longDesc) - Do not translate
+ "SSL_ERROR_UNSUPPORTED_VERSION". -->
+<!ENTITY sslv3Used.longDesc "Advanced info: SSL_ERROR_UNSUPPORTED_VERSION">
+
+<!ENTITY weakCryptoUsed.title "Your connection is not secure">
+<!-- LOCALIZATION NOTE (weakCryptoUsed.longDesc) - Do not translate
+ "SSL_ERROR_NO_CYPHER_OVERLAP". -->
+<!ENTITY weakCryptoUsed.longDesc "Advanced info: SSL_ERROR_NO_CYPHER_OVERLAP">
+
+<!ENTITY inadequateSecurityError.title "Your connection is not secure">
+<!-- LOCALIZATION NOTE (inadequateSecurityError.longDesc) - Do not translate
+ "NS_ERROR_NET_INADEQUATE_SECURITY". -->
+<!ENTITY inadequateSecurityError.longDesc "<p><span class='hostname'></span> uses security technology that is outdated and vulnerable to attack. An attacker could easily reveal information which you thought to be safe. The website administrator will need to fix the server first before you can visit the site.</p><p>Error code: NS_ERROR_NET_INADEQUATE_SECURITY</p>">
diff --git a/mobile/locales/en-US/overrides/passwordmgr.properties b/mobile/locales/en-US/overrides/passwordmgr.properties
new file mode 100644
index 0000000000..7e083b0049
--- /dev/null
+++ b/mobile/locales/en-US/overrides/passwordmgr.properties
@@ -0,0 +1,22 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# String will be replaced by brandShortName.
+saveLogin=Would you like %S to remember this login?
+rememberButton=Remember
+neverButton=Never
+
+# String is the login's hostname
+updatePassword=Update saved password for %S?
+updatePasswordNoUser=Update saved password for this login?
+updateButton=Update
+dontUpdateButton=Don't update
+
+userSelectText=Please confirm which user you are changing the password for
+passwordChangeTitle=Confirm Password Change
+
+# Strings used by PromptService.js
+rememberPassword=Use Password Manager to remember this password.
+username=Username
+password=Password
diff --git a/mobile/locales/en-US/searchplugins/amazondotcom.xml b/mobile/locales/en-US/searchplugins/amazondotcom.xml
new file mode 100644
index 0000000000..84b1c7e76b
--- /dev/null
+++ b/mobile/locales/en-US/searchplugins/amazondotcom.xml
@@ -0,0 +1,16 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Amazon.com</ShortName>
+<InputEncoding>ISO-8859-1</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAAXNSR0IArs4c6QAACixJREFUeAHtXQ1MldcZfuRXlIr4A1MoYAf+xKBswlKmdTJ1Q7RZdc5kM7XWsXaGWk2WTDPc0qlzOmZqnHGZoxrntuiydB1UXZSRqS1h03WTVdDa+osov/I3qAicve93ufTC/eHc673f+ejOmxzu933n/c77nuc5/+fcC/CJjKDLXArlFFopCB38igFjytgyxoz1APkM3ZVQ0KCbgwFjzZgbwmxo8M0B3rGAM+YjgunPdyhspKDFXASeInPVTMABCvHm2tbW+hCI5eaHO4cnNCRKEGhjArhd0qIIgSBFdrXZPgQ0AYqLgiZAE6AYAcXmdQ3QBChGQLF5XQM0AYoRUGxe1wBNgGIEFJvXNUAToBgBxeZ1DdAEKEZAsfkQxfZ9Nh8SEoKwsDD09PTg4cOHPqej+sVhQcC4ceOwdOlSZGRkICUlxQhJSUkIDuYNPaCpqQm3bt3CzZs3UVlZiVOnTqG8vNwgRzXAMvYdN4otdT1//nxBYIpHjx4Jb6W2tlbk5+eLqKgoS+WJCBnsj9ODwQqm38fHx4uSkhJvMXep39zcLNauXWt6HlwA7c4HaxGwZMkS0dDQ4BLMx3m4b98+dwCofm4dAhYuXOhTcyNLzObNm1WD7cq+NQiYNm2a4OYikNLZ2SmSk5NdgaDymTUIOHnyZCCx70/70KFDKsF2sm2JYyk02sHZs2dlRmyoqanB4cOHcfnyZbS2tmLq1KlYs2YN0tLSpN5vbGxEbGyspYaoTqx40YP75d2jR4/2l1BPFzt37hQjR450shkUFCR27drl6dUBcenp6U5pmJ1nB3tqmyAGr76+fgBArm4KCgqGBI0mX65edXq2YsWKIdNyACigusoX41JTUzFhwgTKr3uhzhnbtm1zr9AXc+TIkSF1WIHmGVJ6ZigpJ+DatWtG+9/d3e02v8eOHUNbW5vbeHtERUWF/dLjJy9tWEWUrwV1dHRgwYIFxrpOXFwcEhMTMWXKFCPwek9oaCi2b98uhVd7e7uUHi/kWUUs4wmvat6+fdsI58+f9wkfT7XIMUHqdxxvlV5bxxM/wEC9rVQqsnpSiT2mkmVqgEw+uDnicf/MmTMxY8YMozOdPHkyOEyaNAkTJ06UScZSOpYnIDMzEzk5OcjOzjYmW1Zqv/3BpCUJCA8Px+rVq7Fp0ybwMPXTLJYjYN68eSgsLAQtzn2ace/Pm6U64S1btuDcuXP/N+AzC5apAVu3bpUe77PjPJLh/V9enKOlDNAmjjFnWL9+PUd7FCuNgtjRgK51yKS/aNEiwkRO6urqxMaNG0VMTIyT39OnT5dKZMeOHU7vyvgZCB3lNSAiIgIHDx6kvA0tpaWlWL58ubEM7UrbfkrCVZxVnyknYOXKlcayw1AAXbhwAcuWLQPtarlVHY4EKO+E161b5xZQxwg6YuIRfNYdjnMEpQSMHj0avBs2lNy5cwdnzpwZSm1YzoSVEsCTLJmFMdllZj41JyPR0dEyaqboKCUgISFBKpOe2n3HBPj4oozwsrdVRCkBtL8rhYNMieWZ8+LFi6XSmzNnDkaM4PMI6kUpAXTmUwoB2kQ3JlmelGnDvv+wric9juMtSVmyhkrLH/HKJiVZWVlSEydW2rBhg1s/aQlDOh274qVLlwQtb7tNk4A1K840Q04Zos140dvba8fE42dXV5egFVJBTUd/OrQPIPbu3evxPU+Re/bs6U/LRMAH21RHAGeaRjieMHKKq6qqErRJL06fPi3oixlO8d4+sMAZIbUEcNOiUmjkNLhEmn2vloDIyEhx7949v3Fw8eJFo4bIJLh7926zwXZlTy0B3AzRdqN0X+AJ2OLiYkGza8GkXr161a0qnZ4QtGztCgwVz9QTwCTk5uYKBsZX2b9/v6DFuH4A6UyRqK6udkqODngJCzQ7/X5S3q1BAPvB+wJXrlxxAs3Tg7KyMsHfJXOVDzo5MeDcKa0pidmzZ7vUdXyfjg2J1DiI59IgXsmCePXLEHn0mZMKkTDOv3jxdJAd8lqSY4DrDUBvr9evenyBj56sWrXKCPSNGfCCnaPQsBXUvIAPbxUVFeHEiROO0U7XPOs9cOCAsXOWl5dnfDop9T1IiQW+/xXga3TSfWKkOy3gj+8B3/iV+3hvYnwm4P3XbL9Anf8W8Na/vTHpnS4f3OXzPnzssKWlxTgjSjXCu0QktHetAL5HKxkhEmsDnTSBH/WKRKISKj4TkDge+BNtv37uSeDvN4D8PwN/rZKwaFGVl2lVvPVj4E4Tfe+4A2im0Eb3wUTIdPqZ7Re/CLz0jM35FtoTGrvJPxnxmQA2HxEGvLEG+GaGzZmKu8AvSoHf/QPo7PKPg1ZJZewo4MHrNm/OXQO+9HP/eCZR4dwbYpC/VQhsfpP6AmoVZtEq76+fB+7uBn72dSDJ87F/9wkrjmG/f5ADHH8JGN/XF0QTAXZ59yP71eN/PlYNcDSfReeo3ngBmEJNk12YlGI6sv8b+rcFpyuBdqrSVpXYMcCqdFttznzK5iX7n7ETeO828PzTlI8Xbc+X7gdO/sc/OfEbAezOKGqSfvIcQMM2BHHKDtLVA5z9AHibCHmbnL9e7xCp6DKaBljL0wj0LwBcgIIdfL7RCLz8W+AMFRyWQmpqvz0XuEnPU34IdFN+/CF+JcDu0NNUgn65Gkjz8E2gqvs2Mt75EPgnlbC7D+xvB+6TxvCYmwzMozD3swCN9Z0KSncv8HoJ8Fox0NHXj4XSb4LcK6DmiAh79bitn/OXlwEhgJ3jDSeu0j9+FphG4+uhpLaNiLhlq+4f1gFcAm80ADXNQA+B4o3EUHOSNJ5KaozNNo9iuFA86WErmFobFF0CflQEVFQPtEYTMmPEV9NCaW79hJiBWr7dBYwAuzs8jHshkzrqbGAqAeKtcDvc9F+gjgh60GHLvL1kcjMXFgJEhtM/QKDdzbER9I9ZougZlVhZ6aH0j18AfvoX4H0axbkSHm7TLNgY+ZRfd6Xh+7OAE2B3jWsEt7PfnQ9wieJqrVLutwJ/uAjsKwU+qnfvCfcT9wuAvN9TP/COez1fY0wjwNFBHnFwx/fsLOAZao/NIqORatKb/wKOUYn/Gw0IZJZRRoYCcxKBd6mvCoQoIcAxI1HUbHx1JpBNIZ0yOmOS3HKAYxrurhvagTJqMspo3M4Alt/w3+jFnU1vnysnYLDDXOJmxQOfT7B1oHFjgTjqPGOfAMYQWWOorQ+ndp/7ho8f0XIB/VxcIwFdT4E7bW5OOHCH/kHt4NStd285AmQg4o7d25GRTLoqdIYlASqACpRNKktaVCKgCVCJPtnWBGgCFCOg2LyuAZoAxQgoNq9rgCZAMQKKzesaoAlQjIBi87oGaAIUI6DYvK4BmgDFCCg2zzWAtru1KEKgjQmoVGRcmyXsmYBCjYQyBAzseVeshALtsupgIgaMOWNvCJ0d0yQQBmYVQgafMR8gzEYuhXIKdGzJNGfMyrRqO4wpY8sY95f8/wEKvBLprcz3zwAAAABJRU5ErkJggg==</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://completion.amazon.com/search/complete?q={searchTerms}&amp;search-alias=aps&amp;mkt=1"/>
+<Url type="text/html" method="GET" template="https://www.amazon.com/gp/aw/s">
+ <Param name="k" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+ <Param name="tag" value="mozilla-20"/>
+</Url>
+<SearchForm>https://www.amazon.com/</SearchForm>
+</SearchPlugin>
diff --git a/mobile/locales/en-US/searchplugins/bing.xml b/mobile/locales/en-US/searchplugins/bing.xml
new file mode 100644
index 0000000000..696a49ebf3
--- /dev/null
+++ b/mobile/locales/en-US/searchplugins/bing.xml
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Bing</ShortName>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAAXNSR0IArs4c6QAABl5JREFUeAHtnXtQVFUcx793AcEEtEEkyywfgFASk+GjIjKE7CG9p7HBMZXMqUbUIZx0YCL7QxOaxmaqUUz/oMaZTBxLwsEVM8skRVE0HiEhRhEhxkNQgdvvXGcnBWt3Ze+ec9bzm4Fl7z17fr/z/ZzfvXfPOfeiwWa6rmFV2gJ6mwINkdD1ANsu9eoCBTStDTpOUk25yMjeCE3TWa2aUfV7b96Crt48QI833qtfJiugWeFnSUb62j8s1NM1Jb7Jevernjo66/CkvRe8O16hnp/ar4zaYLYCY7Gv6AzLAHbcV8ZHgRSLccLl41x5pYsdlgHqaodXVyDtLbx8K7+XFVAAOPcEBUAB4KwAZ/cqAxQAzgpwdq8yQAHgrABn9yoDFADOCnB2rzJAAeCsAGf3KgMUAM4KcHavMuBGBXBsYRqW3z8dowOHcZaAr3sN7ywzlke4Oww9I8dwqes69tfX4rPjh/HFyTKc7ep0dyhc/XEHcGXrL/X0oLCmgmCUYkdVOTq7u6/c7ZF/e4vUKh8vL8wKu8v4ab94AfkVx7Gy+BvUt54TKUyXxiLsSdh/kC/mRN2HyOEhLm2waJUJC0A0ocyKRwEwS1kH61UAHBTKrGIKgFnKOlivAuCgUGYVUwDMUtbBehUAB4Uyq5gCYJayDtarADgolFnFFACzlHWwXgXAQaHMKqYAmKWsg/UKNRpqL+b0adPR3duLLSeOoKG91V5xKfZLlQElDaeRk5iE+iUZsCYvwvzoyRjq6yeF0P8VpFQA9tbVoKr5T1g0Cx4ZE4qNs15E47IsfPn8XDw3YSJ8aT5BNpMKABN3Q+nBqzT29fbGsxFR2PrCywaMTwlK/J2hYHcfymDSAdhc9hMu9lx7qnKo32DMo8PS7jmLcGZJJt5PSMKkkaOE5iAdgL86O4ypSnuqjgwIxNKpcTiUshSVry1HZmwCxt8cZO9jbt8vHQCm0PrSH50SKixoBLIenonqN1bg4PxUpE6ORcgQf6fqMKuwVJehNhGKf/0F1c1NCA0Ktm1y+HXybaPBfnLo8GStrcbn5aXYRpP/bbQIgIdJmQFsIdOGI85lQV9xvSwWJI4Lx+anZhsn77773fVeSgBMnP87GTsr3mAfH2c/4rLy0gJoOt+B7RXlLhOCV0XSAmCCZX5biILqn3lp5xK/UgOopBPxE1tyEb0+G1vKj6CHxolkM6kB2MQua/wds/PzEP7RarpEPYALEq0p9QgANhA1Lc14dedWjPnwXeysZs/HE988CgCTe/jgIXg77lE8Nn6C+OpThFJ+EbuWst40Qvp6zAOG+MNoTEgW8wgAiWPD8EHi04gIlm8ltdQAxtHgGhvxTAq/W5YO3y9OKQH4+wzCytgZWDolDmw+YKDWq/O7fB149ANtvROfZ1MsyRMnYU38k2DDza6w/adPYfGu7a6o6rrqkAZAzMjbsW7mM5g66o7ramjfD9X/3YJ069c0wX+07y63vhceAOvpm2iace49MfS864FPM3Z1X8LaH4qx+vs9OE9/8zbhAbA5XlcIz4TeSrfBpu3+CnXU+0Ux4QG4QvxjjQ1IpeM8W1UhmgkPYCCCNdOQdcbeQmN8qIduCBfRPBJAd28PPjl8AJkkfovgd957HABrbZVxuDnR1Chih+8Xk8cAOEUjoWlFO5BfKdcsGbfR0Je25aHu3Nl+PcLZDR20mmHlngJEfrxGOvFZW7k9rIM59/PyxpIpD+GtB+MR6OQiW/aUFfZQj+X0ZUrmldJcATAIzIJvGoIsGsNfeO80sOUi9uxQQz0WF+bjwG919ooKv18IADaVIoaPQPaMJDweGmHbdNVrY3sbVhQXYNPREvqPUJ5hQgGwSTqDlp6zlWtRIbcam9hi3HUl32HVviK0clrBZovN1a9CAmCNZMvL50XHIIEmWzL37kLV2SZXt12I+oQFIIQ6bgjC/hnPDUHcyC4UAM70FQAFgLMCnN2rDFAAOCvA2b3KAAWAswKc3asMUAA4K8DZvcoABYCzApzdqwxQADgrwNm9ygDuADStjXMMN6570t5Ck6ty3E7oiZhIe3YIyvXEtknSplwNuq5hVVoRoMdLErSHhKlZkZGdQHPfmg4/SzKt0bJ6SMskaAZpzTQn7f+95eRyJiyg6FNoQUIkZUaABC2RJ0R2sXP5fJtLPX+j0fEp+n8Aqe+mBAmGWdQAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" template="https://www.bing.com/osjson.aspx">
+ <Param name="query" value="{searchTerms}"/>
+ <Param name="language" value="{moz:locale}"/>
+</Url>
+<!-- this is effectively x-moz-phonesearch, but search service expects a text/html entry -->
+<Url type="text/html" method="GET" template="https://www.bing.com/search">
+ <Param name="q" value="{searchTerms}" />
+ <Param name="pc" value="MOZB" />
+ <Param name="form" value="MOZMBA" />
+</Url>
+<Url type="application/x-moz-tabletsearch" method="GET" template="https://www.bing.com/search">
+ <Param name="q" value="{searchTerms}" />
+ <Param name="pc" value="MOZA" />
+ <Param name="form" value="MOZAT" />
+</Url>
+<SearchForm>http://www.bing.com</SearchForm>
+</SearchPlugin>
diff --git a/mobile/locales/en-US/searchplugins/duckduckgo.xml b/mobile/locales/en-US/searchplugins/duckduckgo.xml
new file mode 100644
index 0000000000..9e96ae21a0
--- /dev/null
+++ b/mobile/locales/en-US/searchplugins/duckduckgo.xml
@@ -0,0 +1,23 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>DuckDuckGo</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAAXNSR0IArs4c6QAAGKVJREFUeAHtXQm4HFWV/qv37e15W/KSvCwQSUIgJIEIArIIRD4UFDWCKBDCMIJsH6g4KKB+CjIKOmjAIaAQMjIwH8oMMCiKAoFIFoFsQHaSvLyt3+t+3a/79T7nVKf63a7XXV1V3R1CJuf7uuvWXc6995yqu5xz7ikJByCTyUi7L5m/JJPBkgwwm6J9StqRawUoIEkhKZPZJElYPnnl2uWSJBGZAYn/dl6+oC0TyzyOTOZsvj8CVaaAJL0kOaXLpvxmTbeFn/wjxK8ywdXo6UFnmjPtpV1fnndVOoN/V+c5cl99ClgkLLXQQHRV9as6UkMhCvB8S0MQZhVKPBJXfQrwYsdC1RxZ7VSf1sVq8NmKpRxS8VYb7C0TYGvtgMXjg8XlgUQ/i9OFdGwEmZEI0vyLhJHs2YtE7z4glTykulCsMYckAxyTj4Zr1gK4PjYXjo4psBHxJWKCXsgQ8ZPEhPjenRh59x8Y2bQG8d3v6y1+UPNJOxbPkzcEB7VWdWUWK9zHfRy+Uz8N9+wTYa2pV+co+z4VCiC68U2EX30e0bffANKpsnFWAsGHygD7+E7UnH0xfKecC2ttQyX6owtHamgQ4VUvIvTS00h07dJVplqZPhQGODpnoP6zV8Bz4pmQLLwOKA7JgT6ZSIn9u+VraihA4/0wMlEa8+MjsDhckNw0H7i8xMR6MFPt7ZPlq62xuThiSsmk04i8+RcE/vAo4rve08xbrcSDygBbSweavnozPPNOK9qfVMCPyIbVGNm4BtFNa5HydxfNWyrB2tQG96z5cM1eAM+xC2GtbypaJLLuFfgf+xnNHXuL5qlGwkFhgGR3oO4zl9NTfzk4rAZ+kiNr/4bwK88h+s5qfjTVWcq/lyxwz1kI32nnwzP/dPnNUSPNJOL0NvwGwWd/Aw4fDKg6A5zTZ6P5uh/CTktINfDEOPTC7zD04u/kJaQ6vVr3vJStPXcxahctLjjhJ2gp2/fA7Yht21itJuTwVpUBted/BY2Lr4Nky19C8no98MxyDP3paWRi0VxjDnZAcrpR+6mLUXfhlbB6a/KqzySTGPjdAxh6bkVefKVvqsIAye1FCz31nhNOHdNeXgb6n7gf6eDAmLQPK8JS14jGS29Azannj2kCzw29v/wuTfrDY9IqEVFxBnBn2r79b3DSSkeEJE2mfb+6EyOb14rRh1TYdcw8NH/9LtjGteW1K0YrpO67v1GVh6aiDOBVTtt3Hhgz3kfWv4a+ZXcgHQ7KHUt4ablIc4K7rl5e//MegEUK6aEBpOjNSHTtpqs/jwgH68birZWZoH57eV7o/tF1FV8lVYwBTPz2ux6GrX5cjlakcMDgk79EkFYWOSCdXGjC0QjNWIDOmcdi/Imn0RxhzyUrgfg+EiNsXofI+ldp5/o6rYwO7oa97oKvoeHLNH9RexVIBvqx/46rKsoE6w2zx9+pVGD2ysNO+/cegl14dVke07/sLoT+9NQYtM4hP2p2voPEnu1I0AbK23l0Xke5AL8Vzmkz4fvEIvp9GqANW3zvDiCZGIOvGhGx999GonsPzWP0gBzYLLIQ0HPCJxB+448VWzyU/QbwhNv+vV/njfk8nPTe983sk1uAOqlp9ORfvJTW5SdR56wFchSOSg72w//I3bRn+GvhDFWI5b1Dy83/KkteFfQ8J+z//tUVmZjLfgNab/oJ3MecoLQN/OT3/uzWosRPeOoQOedSpEkMMLBzKwL0i+7bjVhvF0BLUpuvliSfhZliIZGD7+RzYJ/Qiehbqw6KyJnF2/Edm+H9+Dm5N4GHWUfHNAy//mKu32YDZb0BvM5v+sqNubp5zO9fdidJHJ/LxakDCV8DIuMmwpKMQSImZGiPkLHakXR6kKTJOeWtQ01LG9omTUbjrBNgpU1TIYhuWY+ee66noWCkUHLF47ynnIfma3+QN1T6V9xf9j7B9BvAO1xe6yvjI/c48NSDtKt9UrPzVhI7uAI94HnAERqAM9gPJ927+/fCu38b6sN+1FlSsJGyBSRoczS1FsRnb26XRdcZmhOSg33IEN5qQmLPNqojDvexJ+Wqcc+cjyjJrVIDvbk4owFTbwDLcybc+595y83IW6/LT6TRBnB+C8n/a8/+PLynLIKDhhejwFJNHiZ4Zy2/fVVcMbXeel/eBpOXp/tu/aJp2ZGpN6D+oiXwnnhGjk4sMu7+8XWGn8KMzQEvDWPtNMl5SCHD4mQzwEtFW2MLvAs+SU/oQnl+YDVlNYCVOb6Tz5VVo4zfSnNWJpXCyJZ1pqrTFsYXQMnrfZZqitD/4F1Ik2DNCMQa2zHu5nvResk3SJbvNlJUM6/r6Dnyqow3VNWA9PAQ7ejvyEPN9GC6mAHDDGB5vihSHl79kjwOGql8pGk8Wpd8C7VzTzFSTHdee/sktN7yU3o884WAuhGUyMhPe0hYaDA9mC5mwBADWJMlKlPYEsH/GHXUAKRpYrWedykaaENTTWCFPg+V1YKBJ36O1HAoh57pwvQxCoYYwGpEEYLPPoYUrUCMQM/8RZiy8HQjRUzn5fY6Jh1lurxWQZbmBn//SF4WNX3yEovc6GYA61pZh6sAy/RZkWIEEjWN8B01K09kYaS8nry7tu7Fk4+/gueefh3DwzE0XfFNPcVM5eFVFyuVFGD6MJ2MgG4GsPWCuObnypkJRmDgYwvRWl83pkigqxsvrPwznnz4RWxY9faYdL0Rr//5LSxbsQG7Pgjg3fd68d0fvIjtYbds8qIXh5F8rExijZ4CTB+mkxHQN0uRvIZNRxRgbVHwhZXKre4rS0FrWtrz8r+w4o94em0U8ydaMWd6LQZ7A9j29lZMP8740DGxzYt77lqUwy9LYz/YA+8XrsnaAuVSKhfgUaDuwstzOmam08CK+3TbHeliABtNiXY7LB42qtHi4Yd/dsEyYeeq1Xh1lwX33nISGjvylSBmSDRxVj7TeH/QOHmSjMoxbRbi2zeZQatZhkeByJq/0gN6npyP6cT0iv7jNc1ySqKuIYgt1kRgtaJRiNVlbXSsHm+uaG8wgRsvm10R4ueQFgmw7rdaoKaHml5a9epigCj/kDm+/hUtnAXTEiRkYxBtPOeePg8tk8t/8gtWqIr0LvxU3v5FlVzWLZvSsD2TAiK9lLhi15IMYENZq2904oySlsqMzUzKkd3t8t5BAYfXowQrfn35qf/Br+95GJvXbZRxsyU1G/xWBciOiY3JFGB6Md30QEkGqBttVqlOJwTl9qRKWBf09QXAv3Lgvtt+hhue2Irla4aw7rX1ePn5V2V0aj1vOXWoy7IlnwhquolpYrg0A46ZK+YnU2+TVg0HrJFjg6Ovqog4EU/in6/5Kc5a+qj8u+kb94HjjEI6HsN/vZu1aqtzWTFzajOeeCb7dDqPOtYoOt352YxSBJeKbmKaGC7JAMeEKbn86ZEo4h9szd0bCdiHsxYRI8HBgsUeuf9xrOrONuc7ZzfjjuvPw6M/f7xgXq1INgJzS1nGdYfTuGL5u3A7sho2uS8GVKBa9ajT2IaVpcIKiHRT4gpdtRlAwiw+HKFAovsDJWj4ag9nCR8eHi5Ydsv2nlz8slX9uOX+l7B522hcLrFEgHXMNy6aAjuy9v/1ljiuu+Z8uRQLzcT+lEBlOFk0dZfr0SEM1NwH8LEgcdWS2G+eAc5QdugJxBIFOzajoxZ/6YnJaYPRDN7cl8S1C8zpB85feinmn7UL29/biTmnzIevtiZXJ+uVdQNbpPAvra8Em9C7yRKbgenG9OM4LdBkAJ/JEiFZxhtgIZWhfTgA/4HlqIiXw0tu+ireuvaneCOYleOfXB/CFTf+kzqb7vvWqZ3gnxokksZqgdWXhPeYIXiPDcLRSmpOYkA6YkOsy43w22RQ8N4oM9V4xDeA05h+ZTGArYhFEAVPYrzesMvfhRAp3uP93XAINkRc3uHz4cFHv4uud96ROz3+2Dl5sie9dZTKxwqVPCACO8ePwD0lBNe0YTgnjDUWtniScE8Pyb+elZMwsmt0Myni4sMjIqjpJ6YpYc03gA2RROBJuBxwDexHaNJMRLr3jmEA42Vh1oTjjy+nCs2yGVqJ8TzmaCHDgM4IXJOH4ZoUheTUd14sGXAg3ussWgef3BFBTT8xTQlrMoCNrkQoV8/qHiDbH4KhwCDMje5ia3SE00SQFM098Z1AdD1S3c9j4vWbSQWqj+C5GlJkTvlWPQJ/a6bjUdkVVS5NCPCxKRHU9BPTlLAmAyyOfG5naI1dDjj798nFByIxZEVk5WBTlSUCZwYfByK05k/sJcIHyZw0n9Dykk97CshDmolbEHq7HqE3G5EM2vPSCt3wSR8R1PQT05SwJgN4UyOCxLY6ZYAtFoEtHEC/SeuHglXHyV6n+3vIBP9QMNlMZLzbhfBbDQhvJIsHYoJe4AODIrCJZinQZID6UIKeMa1UhR4ahoZ8hSfiUmXHpPsfQGb/7fSkF17ajsmvEZEetiG8qRbDG+vADDADfFpTBD1DtiYDRMEZI9YzpokNKBTmldAQTcTh/XvQqFoJFcpfLC7TfRsyfb8olqwrPhOzILK1BsNE+OhOWvHpXO8XQ85HZUVQ009MU8LaDFCpHEWljILA6NVFJogMPBE3Gi2s5B96VpP4PF4n+lywuJOw1SToIB6JJnhDRcArmZEPPPJ6PrqDCEYTbKVAbVimR2WryQC2DBbBPn6yeGsqrKyEBqIJdJrCQGc1wi8XLRnd4UPvUxOJsEIWorGFlpppHs/TlSO4UIMcVCvk1fRT5+d7zRmGvY6wubkC9vZOJWj6yjtiBxnm9qcs8kl1U4gs+RtEEUdwFR3GFonPiSQJl5ePVSQ+V8Mn9BVgusleW5SIIldNBrDLF/Y6ooB81rcC0kSXfx+SpB+I94ziVurQc5XqPls0m3CiqGieaiWIb4BMN+HhLVanNgOoFJ/VUoBFvXxsqFxwEwMYwiYZAPd8SHWfKdiM+jN6aWdrfjaV7BnaHUdQc0IAdQv9ZPSr78Q8n6wUfVOwqxw9oDkHMIKRLf+Ad/4nc7hY0xPbuiF3bybgOrAhCwSCoAHDFEgdD9GMSucCht/IK8+ynAlLdyC4ugmRd2uQCmt0kaYDOxGYRROOjihcHZGsAE54LB3tUfQ9ky+UzKvwwI1aA8Z+ivSARuuyxdnZkQhuYoDaJE9M1xN2D3bTuJxGfyyDaXoKFMpjIalp5/MI/Gomaub2kdnM6FxlrU2g8Zxu+ZcaohXRgIOOwfKcQxSn+cDqSckrI1tdnE5oUkQR4GVqcPW4Iqn50UwXEdR0E9PEcEkGsKepFJ3vVRTzrhnH0X7ARwfUwiIeQ2EpGadTMn0YaGyTFfyitbURRKlBss9cVY/g6/VwTw3DO3sIHpJaikMQM4N/RiHyvg+Df2rTJYIgKaLsCESpg6XGej10lWQAI41u+Dt8dEiNgYnlXXg2wi//Xr43++fy78VIQyuiXbvhmXyUKTQ58S89xNHtPvkHcsTp7iQp51T6TaQhpS0r09dTQcLvoP0B7YZpY5boy5eDaZWXDdfoqK4CTC+9oIsBbHikMIAR15DLl3IZ4KZ5IDB9HkK93aYZUPCIKw0zvBfgH4PkSGcZ0UHjfGMMtoYE7QlouKJxPj1sRWLQKYseeFNmhOgy8gN/7AJHhPBrL4i3mmFdDOBjOezmS9kJO2ccL2t79Gw0itXOS1GGwFAIrcUylYiXVNLaQtlZmJZ7OwplKDPOQl5WPPNOz2FhOsk+6XIx2gFhvtfISIoM9rGmANtc1l3wVeXW1NU52AMplUBfovgkWAqxtcgJylLlKplee95iMswdHa5kOhlwCKiPAdRidnDHpxEVqDn9AlgbmpVbw1eJVkGugW4M0ZCRUsmc9CLjjpfTBr31FMvHvktrz/tyLpnpw3QyAroZwApndnCnADvYYIcW5YCyIYvo3LQUqsvMsaBCeMzE1Z7zRfmUpFKW6aNWzCtpxa66GcAI2LugCGxxbBcMt8Q0PWFX314521B/r57sBfPwqcgPA6zkrkB9WlRNHz3tMsQAdu3IHqQUYNFE05XfUm4NX90D2Yl4MJyvSzWCyCX4qdBTrqvFjhGHoW4XRNt42U25s8KcgelixvWl4Zawa0fROpqP68vuZAo2UzuSpaIWOubTmzTcjBxi51FzyB1l6d3q5qlOPPSFevz2M7V4bV7WUjuHxGDARe4K+LC2Aqy6ZbqYAcM9Z7+a7NpRBD4IZ/agMusHRkhbkvDnmyHuCr2Pn6y7Gcs33YO/d/8FsVRhkxg2ZREPD4rt4rC/3ooVF9ThD2f6MFCXtWjYNsmhzqb7nt0qNF9zZ17+4LO/Ne3EyTADuGb2q8k+EhRgA6SWG35s6mC0q2+PjCbc9YGCTr5uD2zCNvox8ZkJt7y6GI9tuQ97wzvy8vEN78xT9DmKIS/JbmqscjhDYp/Vc9x45KJ67GnN3+4M1liQtFIGE8AuOEWpJ9OB6WEW8lumEwsPQexXs/2Oh3MuKZ1Tj0HT126RHSrpRCNnU1ZCwQE/GoSCKZVJSSw1gte6/lf+zWw8AYs6F6PDNw0rNi/DlsG1iFw5Kgpo7U/CQZvdPW3FuxckJjQF1JoboQEFgvWfvxoecuCkAB9WZDqIQ7KSpvdavIUlMLBTU/arKfoL4lVRkly3GJGWekhJz+CPxNEph7J/fMKxGGweWI+1+9chRrvchpqx+XrGle5WxEUMGKM6K1YjfeXijAvRcPHVeRm4/+U6dzU1BCmtYKem4qqI4xu/9HW5sUqeUldrNAR7JAh/hhw3CRokm0WbiENk9CaVYcYQc+gfgjykDxl31W15XeF+V8Kpa1kM4BaxU1P2oSYCN5afGL3gpv0AS/Njgvm73aI9UfIbQlJg08BzhB7wLDgDLdf/iOrKTuBchvvL/a4ElNGFbPVsvMVOTcVJmRvbfPXtqFP5lijWYEUwF+7ODkecz2XTsVQcO/oUq2JMvHJmbUyCEFFDTqRabrwn73Ql95P7qzZaE4oZCpbNAK6ND23LTk3Jr6YIjYuvpY3at0uujlg0zTAYCOSKu21ZcXIuQhXgKYK+f2YanFqqXnq1Gr50LcYtuU222FYqYb+h3E+jh9SV8oWuFWEAI+b9ATs1Fd8EjueJefz3H9U8GiS/AUTR/vjoqqTWIa6JGFM+sPVDmE7SiOf4UiQrjJCa87Q3Izjr7xG0+EfVlPmlaVIdHhUsimnWhnFou30Z6i+8QoyW+1Vpp61cQcUYwMiYCV13XDlmTuAl6oQfPwEvOWEtBBZSUTqDfRikiTh94Bhro6ulUFY5jsXhx7XNRDIpYW9vGju6sr+RcDM+N/UanBmbgRM3RLHkmSCWPh3AqesiaPOPMtdFTGoIjd4rFXkWfBIT7v4PuGfOU6LkK4/53K9qfNzBlNO+vNYVuGEb0hZy8Sg6d1KyRcl5t3/53WOkhvtPvgiBaXNx2tGT4Zs+S85+0ysXYzgRUorK19lNC3DR9CswkfYA/qgfPeEe2Og8VrOnGQ2u7FvDFgn771qaV45veOnZ00SnKGMptPWPMoAP1DVdfis8cz8xtgytdqrpPd2U074xrVRHkCtJdmrKxqksKxLd3Nibx6PmrM/JnhL5yKtiQZx01yDcMQONtOGqmThVxrhz6F10D++Bhcbk45o/jq/NvJk2YF9CnSO76fLYPWj2NqPJ3QS3MGnbxrXT+nyD/E0xsWn2ZPbJ90Wyk4eFHGvUf26p7Kzb0ZGtU8kvfz9g5S8w8Nt76dU2rtRX8JS6VuUNECvV+oIG+/wM/fVZBP/7cYSTaew8/xp0WhOYeW7W8q1/pBtbBzfgGNr51juNWRDFdmxB179cJjYlF2YjKnY6W3PmhTk3M7lECvA8dlh8QUPpFFtSaH1Dhtf0PGxsi5E8p6cLp5ybr+RW8Bi99pAL5cial+Vi/LUM70lnyd8qc6neSgUvixQOu2/IKJ3jKx/bbLpM+ytKaSICf0cAtJewkqmHuAEScekJ85McIvMZHgadHzu+4NOu4OGd7WH7FSWlk8rVyHfE2CkqHy3lp5PHZclqoY2Rk4yv6PthdJXjE7HslZXhNLxLDodsSCYeMlfqFq//774jJnaew2xNfORLeofatyRJ26SYQaoZVs49m1eyxRobmR35lmQJSua+pkouX9jrCK/TSw0nIkqWqspfU+XPoJB195GvqYrUMRM+4Pgi73vCtNljuyDWx7JgjPccfCaLrfWOfE/YDJG1ytATzU4vSjm+0EJxqKZVVBZ0qHbyUG4XM8C8of+h3LOPQtskKWQhweKmj0JbD8c2klJoExlz4OHDsXMfhT7Rw7+cmJCRdl6ygL5Mljn7o9Dow6aNkvTSlJVrzqEhSMpITuky+j7TS4dN5w71jhCtmeYy7ZW28puw+5L5S0iUchUJJ1kjoq2UVQoeueqlQJiG+4087ExeuXY5E58L/h92t1mNYWFeRQAAAABJRU5ErkJggg==</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://ac.duckduckgo.com/ac/">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="type" value="list"/>
+</Url>
+<!-- this is effectively x-moz-phonesearch, but search service expects a text/html entry -->
+<Url type="text/html" method="GET" template="https://duckduckgo.com/">
+ <Param name="q" value="{searchTerms}" />
+ <Param name="t" value="fpas" />
+</Url>
+<Url type="application/x-moz-tabletsearch" method="GET" template="https://duckduckgo.com/">
+ <Param name="q" value="{searchTerms}" />
+ <Param name="t" value="ftas" />
+</Url>
+<SearchForm>https://duckduckgo.com</SearchForm>
+</SearchPlugin>
diff --git a/mobile/locales/en-US/searchplugins/google-nocodes.xml b/mobile/locales/en-US/searchplugins/google-nocodes.xml
new file mode 100644
index 0000000000..7ed636ae91
--- /dev/null
+++ b/mobile/locales/en-US/searchplugins/google-nocodes.xml
@@ -0,0 +1,16 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Google</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAHqklEQVR4Ae2dVXgbSRaFe5lelnnfluFlmCnMzMzMzMwMCjPMKOsxM7MVh5kTc0AMJePDWd+dT172WupuVXVc+b4zwZF0/9N1763qVpUS+AHgSzU1NWOrq6vPN8rr9/shihhj6v8fziKmxJYYE2sACon+Q/pp419mGAi4oQ0h1sQ8YMCXeMMnOPzfk4sJX1Iah8Q4AYNvFYZQOlIoL/EPtHWaQOwVfgVXitgrrfVqFyUGRcLnG4uix4cVH744cSl8A5NSZLrhG7Mi4fONXZHwuZjAvwZIiW+ANECmHf3ZKKLDZ3YbvJZ8uE8cgmP5PDhmjoN9zADY+nWCtdN7sLZ/C7Yen8A2oDMc4weD/o3rwE54kuPAykuFN0EREb7v3h249u+EfXR/WNu+AWub10OWbXB3OLeuhfeihb8JItcAZrXCE3EW9olDCZwuolFCI4lZX8gi3CS3C+4zx2Dr2YYghUddPoDLtI1M528At9TDGDyxn9NVSVC4yNa7HdxRZq6piIsBvopyOGZNIAhCyLloBqUl/gaEA74nJwO2Xm0pcKFkG9IDvlvXw26CEk74LtN2ClZYUVvriY/SwwTORZgxuDavpiDFV8d34Htw7yXqgnw+ONcsNgx8b17Wy9WGOlfOl/B5GeA6fkDCD9YArYovBRRYRtBatu4fwz6sF+xTR8E+ZSR1MLB1/VBc+M2wVfSAzx4/ogUyzYBT2+ratAqepFiaQ/zv933xHJ60RLg2roCtX0chr3xirHsKcsyeqAl4+8h+X8xUXc6Qir8nLpJGBwf4HGuANz1Jg7Wa9+H+7CT8WoxInxeeyM+ox+cPX3cDnE7YBnZRBZ9yOit9onmgNMu1DekuFHySomX+98TtVwXfsWwOrY7qFizVCN+lYqHunGk3Anx2NOT8BP7orrC2C777cW1cqSblyJvytU9MaMj4Gkh12W/APuSjFsOnok25Wt6UV6H6or8Q/CbV5/wK7iX/vx7YhvZUuxQsDaixFhL0/1Tmt8FO9GjWAG9RXmt9gkI7A2rvLyXg/1O1qZ/A2uOd/4DvXLVQPhekSfqxvEqgm1Vdzh/gnNL+Hwa0exOstEQaoPpFXI8JcMuU9T14dnyRkhxLZ9P/Lw1Q2/9XV0YT3KBUHdsF3vMFkE/SMfUjoPbBWoIanHJ+BL9fm55/zCHGRXfLtTGBviWp6gXqrg0K2oC6yz00u4o+Wce4KP26IDWgzvJG0AbUPtpieANO5zExDKgv+H3wNaAyyvAGbE8QxICG3J8HbUDNi3zDG7Au2i+IAZnfCX4EOO8Y3oBlf+U6AqQB889yHQEyBS38TBZhrgasiuRqgGxDN8VxTUFyIrYvlb8BHJci+BsQVWzgxbhjueNQWH5RkwBMqerVfVvwBlju8VqMU7Ec7c76MWanT8dr5j6YlbNeiBXJ5w4/2q4P3oDS5wa7IXM353X0jBsHgk9641w/lNjLuRuQdDn4NNZnp3bvH5ZbkvG5A/FuxKAA/CYtyN/K3YAlZn6zYJISyEV63JSvyfgONmRNI9j/U3llF7jBv1ESWgE+V6RN/tf1sZTK7N9geNIUgtysesRPxgu3jYsB8z8NrYt68kzEB7NKTE3wi/I6om3UCALcIk3IWA4v84UVftzF0K7+SUe1Sz+6PJp4KHc8FVgCG5RWFO0G87OwwL/y2I8OG0MzwFwo8MO5EbfOEsyQNSd3A1w+t67wr5f8vYsJCX7nzYzaVjEfziU5vS50jh2vyoSRqQvxRKf2NPWqH502hT773ZVsgG0rkx7lEkhVei9iME7cjNIsJVU6n2GtZT9Gny0OGX77DQzlVoN8S5KKKoFUq76J02G+k0AjK6TPUeKogOnqGXzw+RB6Pbxu7ouJcZ+FZIBJo8W3sHxJ76GtFB9HDtfEBFKbqBFYadmDuAcZKHdUNZsCiyuv0eihi4CA/9fXG5awBl23W1sMf8BuBodb1y/paW9CVoklAEBzfRQ5DD3jJ2Nk2kKMTluM/kkz0DFmLN4MovvqEjMRQ47eaZEB2bf0/5qqLjpw7TMKVljR8siEyJTmC28SM/ZWBfPzNgttAmlc4l502OT+D/hTjjP4mN/YBviYD4sLtgtvQt/4uehjKmuCP3w/Q5XtJdkzjtrJVZa9wpvwceQIjP7UgiEmhgrtW07+GzZtu3RMeBOGp85HmdXNa8Mm/U3IeFKINlEjhYS/sfggvMyrP3zem/ZRLz8+Y5kw4D+MHIaUx3mtZ9fEQF2IuJuMzjHjuMKfnLWSJo58t63kKVr9PHojAp9EjQgr+BnZa3G56pbcOTcgq9uGM7djMTRlrm7QaWZO85Kbz+7xj1nkzbvvvHiIHZeO0xIDQVM926U0Y76biArnU/E37xZt+3qbx4H8sos4eM2MubmbMDZ9SaMxM2nth+DizXP9qYiiQ/QYMgzTs9dgffEBnL0dh6tVt+lWp/jb14tugjzAQUqeISMNEDAdyWOs5EFu8ihDrvBlDZBFWB5nKw90lgc6yyPNVYjjof5SxJ4MOG/8K8qYn43YKzU1NWOFDLQVfCZirwD4UqMTGfyDb10XATEn9mQA6adhM0EqAP+nxJ7gB/QlGhKUl0QrzIwxw6c5YkpsiTGxDnD/GwMDxywT49owAAAAAElFTkSuQmCC</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://www.google.com/complete/search?client=firefox&amp;q={searchTerms}"/>
+<Url type="text/html" method="GET" template="https://www.google.com/search">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="ie" value="utf-8"/>
+ <Param name="oe" value="utf-8"/>
+</Url>
+<SearchForm>https://www.google.com</SearchForm>
+</SearchPlugin>
diff --git a/mobile/locales/en-US/searchplugins/google.xml b/mobile/locales/en-US/searchplugins/google.xml
new file mode 100644
index 0000000000..2a264c27cb
--- /dev/null
+++ b/mobile/locales/en-US/searchplugins/google.xml
@@ -0,0 +1,17 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Google</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAHqklEQVR4Ae2dVXgbSRaFe5lelnnfluFlmCnMzMzMzMwMCjPMKOsxM7MVh5kTc0AMJePDWd+dT172WupuVXVc+b4zwZF0/9N1763qVpUS+AHgSzU1NWOrq6vPN8rr9/shihhj6v8fziKmxJYYE2sACon+Q/pp419mGAi4oQ0h1sQ8YMCXeMMnOPzfk4sJX1Iah8Q4AYNvFYZQOlIoL/EPtHWaQOwVfgVXitgrrfVqFyUGRcLnG4uix4cVH744cSl8A5NSZLrhG7Mi4fONXZHwuZjAvwZIiW+ANECmHf3ZKKLDZ3YbvJZ8uE8cgmP5PDhmjoN9zADY+nWCtdN7sLZ/C7Yen8A2oDMc4weD/o3rwE54kuPAykuFN0EREb7v3h249u+EfXR/WNu+AWub10OWbXB3OLeuhfeihb8JItcAZrXCE3EW9olDCZwuolFCI4lZX8gi3CS3C+4zx2Dr2YYghUddPoDLtI1M528At9TDGDyxn9NVSVC4yNa7HdxRZq6piIsBvopyOGZNIAhCyLloBqUl/gaEA74nJwO2Xm0pcKFkG9IDvlvXw26CEk74LtN2ClZYUVvriY/SwwTORZgxuDavpiDFV8d34Htw7yXqgnw+ONcsNgx8b17Wy9WGOlfOl/B5GeA6fkDCD9YArYovBRRYRtBatu4fwz6sF+xTR8E+ZSR1MLB1/VBc+M2wVfSAzx4/ogUyzYBT2+ratAqepFiaQ/zv933xHJ60RLg2roCtX0chr3xirHsKcsyeqAl4+8h+X8xUXc6Qir8nLpJGBwf4HGuANz1Jg7Wa9+H+7CT8WoxInxeeyM+ox+cPX3cDnE7YBnZRBZ9yOit9onmgNMu1DekuFHySomX+98TtVwXfsWwOrY7qFizVCN+lYqHunGk3Anx2NOT8BP7orrC2C777cW1cqSblyJvytU9MaMj4Gkh12W/APuSjFsOnok25Wt6UV6H6or8Q/CbV5/wK7iX/vx7YhvZUuxQsDaixFhL0/1Tmt8FO9GjWAG9RXmt9gkI7A2rvLyXg/1O1qZ/A2uOd/4DvXLVQPhekSfqxvEqgm1Vdzh/gnNL+Hwa0exOstEQaoPpFXI8JcMuU9T14dnyRkhxLZ9P/Lw1Q2/9XV0YT3KBUHdsF3vMFkE/SMfUjoPbBWoIanHJ+BL9fm55/zCHGRXfLtTGBviWp6gXqrg0K2oC6yz00u4o+Wce4KP26IDWgzvJG0AbUPtpieANO5zExDKgv+H3wNaAyyvAGbE8QxICG3J8HbUDNi3zDG7Au2i+IAZnfCX4EOO8Y3oBlf+U6AqQB889yHQEyBS38TBZhrgasiuRqgGxDN8VxTUFyIrYvlb8BHJci+BsQVWzgxbhjueNQWH5RkwBMqerVfVvwBlju8VqMU7Ec7c76MWanT8dr5j6YlbNeiBXJ5w4/2q4P3oDS5wa7IXM353X0jBsHgk9641w/lNjLuRuQdDn4NNZnp3bvH5ZbkvG5A/FuxKAA/CYtyN/K3YAlZn6zYJISyEV63JSvyfgONmRNI9j/U3llF7jBv1ESWgE+V6RN/tf1sZTK7N9geNIUgtysesRPxgu3jYsB8z8NrYt68kzEB7NKTE3wi/I6om3UCALcIk3IWA4v84UVftzF0K7+SUe1Sz+6PJp4KHc8FVgCG5RWFO0G87OwwL/y2I8OG0MzwFwo8MO5EbfOEsyQNSd3A1w+t67wr5f8vYsJCX7nzYzaVjEfziU5vS50jh2vyoSRqQvxRKf2NPWqH502hT773ZVsgG0rkx7lEkhVei9iME7cjNIsJVU6n2GtZT9Gny0OGX77DQzlVoN8S5KKKoFUq76J02G+k0AjK6TPUeKogOnqGXzw+RB6Pbxu7ouJcZ+FZIBJo8W3sHxJ76GtFB9HDtfEBFKbqBFYadmDuAcZKHdUNZsCiyuv0eihi4CA/9fXG5awBl23W1sMf8BuBodb1y/paW9CVoklAEBzfRQ5DD3jJ2Nk2kKMTluM/kkz0DFmLN4MovvqEjMRQ47eaZEB2bf0/5qqLjpw7TMKVljR8siEyJTmC28SM/ZWBfPzNgttAmlc4l502OT+D/hTjjP4mN/YBviYD4sLtgtvQt/4uehjKmuCP3w/Q5XtJdkzjtrJVZa9wpvwceQIjP7UgiEmhgrtW07+GzZtu3RMeBOGp85HmdXNa8Mm/U3IeFKINlEjhYS/sfggvMyrP3zem/ZRLz8+Y5kw4D+MHIaUx3mtZ9fEQF2IuJuMzjHjuMKfnLWSJo58t63kKVr9PHojAp9EjQgr+BnZa3G56pbcOTcgq9uGM7djMTRlrm7QaWZO85Kbz+7xj1nkzbvvvHiIHZeO0xIDQVM926U0Y76biArnU/E37xZt+3qbx4H8sos4eM2MubmbMDZ9SaMxM2nth+DizXP9qYiiQ/QYMgzTs9dgffEBnL0dh6tVt+lWp/jb14tugjzAQUqeISMNEDAdyWOs5EFu8ihDrvBlDZBFWB5nKw90lgc6yyPNVYjjof5SxJ4MOG/8K8qYn43YKzU1NWOFDLQVfCZirwD4UqMTGfyDb10XATEn9mQA6adhM0EqAP+nxJ7gB/QlGhKUl0QrzIwxw6c5YkpsiTGxDnD/GwMDxywT49owAAAAAElFTkSuQmCC</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://www.google.com/complete/search?client=firefox&amp;q={searchTerms}"/>
+<Url type="text/html" method="GET" template="https://www.google.com/search">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="ie" value="utf-8"/>
+ <Param name="oe" value="utf-8"/>
+ <Param name="client" value="firefox-b"/>
+</Url>
+<SearchForm>https://www.google.com</SearchForm>
+</SearchPlugin>
diff --git a/mobile/locales/en-US/searchplugins/list.txt b/mobile/locales/en-US/searchplugins/list.txt
new file mode 100644
index 0000000000..dc02e6cb70
--- /dev/null
+++ b/mobile/locales/en-US/searchplugins/list.txt
@@ -0,0 +1,8 @@
+amazondotcom
+google:hidden
+google-nocodes
+twitter
+wikipedia
+yahoo
+bing
+duckduckgo
diff --git a/mobile/locales/en-US/searchplugins/qwant.xml b/mobile/locales/en-US/searchplugins/qwant.xml
new file mode 100644
index 0000000000..4b585b743e
--- /dev/null
+++ b/mobile/locales/en-US/searchplugins/qwant.xml
@@ -0,0 +1,18 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Qwant</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAAXNSR0IArs4c6QAAFjhJREFUeAHtXQmQHNV5/vqac+/VnjpWKwkQQsjIYIEpYzuhAmWEITgcRWETDFQlTpVjEoMPSJmAq+xylV0OFCZlBwwV21UBhxDkgCHkMmBsQAJiQCAt6EDS3vfc3dPd+f6e6dnZA7zS9uxsivlVvX3M6/f+9/3H+9/VUvA+lE6n17iueymT7FQUZSPPXbyvf59XPvA/EackQegnTu/w/DjvH4vFYkffCxhloR9SqVQ3n9/Blz/PjLSF0tSeLQ4BYmgTwweY+vZ4PN4/9615AiD4l/Cln/GlurmJa/cnjgAxTRDTz1IIu8pzUctv6HL+kgn/tQZ+OSrBXBPTesFWMC7PsWQBRc0X8EvPyhPWroNBgEIgxO4f+5bggS0+nz/s4w81txMMzu+bC7EWd7RZ2gTfBd1RA/99MQv0R2ItkeQdkqkioSbPh/iwFu0IIstEtAKbRa3XCbzE+TXwlwl4v5iiwl8qLmin/7B2XnYEdqo0Benh1qgKCAj2YgHS661RdRDoVumLaqFndcCHYO+HoVVioVZsTQBV1oGaAGoCqDICVS5er3L5iyzehqKMM+0wzyM8T0DV9kHXnoGV3wq45/BZExu1VXDcdp5bocBYZN7VTaZwIM6tLgsLl64I2Op+Av0qQd/HRINQ1SQ0VbymjCG6BN+Bocu1PHMlqkDSiiOV74KhbIKObdDcLfy1nb+vTFpRAnDdHLT8c9CsX0CxX4YeM4FYlHgbsDlaIiPlnrYQ6IVIRDFtGzAVDSFd9QRimTEKYxt09+M8tlN04YVerdqzlSEAN0/Qn4KW+ym19k3wD2zLheNQq0XbqfWuoVMOBC8UhqLRvXiWQNzKhCFiGTdDsMumNFRVQSikwHb4Y74HIfciGBSGoqwMQVRdAKr1MozsD6jfezwtzBN4wdzDUM5y490XtN5VeKNpFEbIE4iihSgM3vO5zSRjuVDBSrzcZv8xDFVehWNtQMi+kpaxY3aCKtxVTwBuBkbmH6DnfgJFt2CbPsBEwQO8eC4JwL8vnKFQNEznIUqLsJUQ0ogjq0SgyfvMrpAj08yhUEilddHQnD9A2P0c24imOSmW77YqAlDy7yKUuoMNKrU+R1fD+vpeo6T5Aq4AWRSAtLMCuIAq6XP8k84DSTYTCeaRtvisrgnhaASNdFf1IbYDdFPyDj3ZPJLnkagGJ9eFqP3nTMdoqgq07GGokn0VkcytjHD64Wbo4wmEYDyXBCAPPP4gAGYJtICcEMB5TnE6w+LhYcs/bjzGdiKElGlTKDb0jIK4oaExpKOOZ4NtgaQtNRm8sdImzNAonrcfRZcdxWna8g8ML6sA1MQLCB37MpRVSbji630qgu0LIk9gzRwB55ES4HlkqPE80dXwoDXI/J0nJD5zdR1OLOah6z9ziPR0Lu8dBn1SHa1ChBHTRRhsj10Vr9s9eCW5CdNqA8LqczBtE9u1U32uluW8bC5ITeyBceiL0JxpuI10DXUFtRfttulKTKKbzSrIEPQsNdwk4Dx5YDsEzCHgAr5c27wmfiW3lW9ohEPtn1Hv2dj5mi/CiVIYihbBS/q5GFO7+I5N70ZHRwvR2EL/ETt1Z+ibZ2dQwbtlsQAl8zZCh26Gak974SUS7ETlFFgZ+m0CbhJpMQibDatNIARsAUsjyGIVEnEqBl0IO105CU1dnZBFeKiwwwxJCb6mWtDZuIqLkUjKkZCoSMzCy09uTba+B9l47EqN4ZzuFoTFepjU5R9bcfCf6ouI0R2drPUU367sqeICUOwphN75KsEfgcNIRxHNPawhw7MVJlACMg8BWdoD7xDseK0TcIP4Zu04wpFtiNadyYZzM591Mn0dD+mgOXCUDIEdZQ/4ABL5NzCN1+GGJykMCroYXfkwSnEuO2ojqUm82L8fO1afjLAmQqBg2VnIc678SfwazW4D2pRm/7WKnSvsglwYb98KI/kLOFmiKqpIP6K+FUWqxUam0SaAfERUPNfCs03Vp4JDC9NC9B7UtV2GWOP5MMKyeGNxZLkTmLBexJD5BFLOXiJOq8iLVClUWtnebAw/HO2m1tvoYOS0o/skhIpC8NJEDLSbTbg6dBFCFR5TEoWoGGkj/wZjYhccRjsCgpAySf+SUxmGsh3gM889yA/yO48Ie62a3ox451+he/NP0dj+p8cFvmRlUHPbQxdia/x72Bj5GhvYNQxPC1WV8iw2IOJ2ZFxpKCmW0McGOE83Jb9SWGyERiJTeI6dxEpT5QRgjkA//H2qNGvqOVmeea2MyzCCyzCRvVKLDSJrKIIQRqLUeiW2Ay2n/hjxjmvZWC5ttlRRdLSFzsfWur9DMy6EznZEXJ3pFMeUWKYvhJf630bellHXohAyFl5W3sIxZ7iiMqiYAIyj93MkcojdfvJPgL0jSe1Ps0jWUZYlGXJNkjqHBJzWq9Cw+W7o0V7veVB/DKURm2JfxrrQFwi4jrxIoYxECIPJCbw00Ic83ZIIQdoPcYW/yr8krUxZ6mAvZ3MSUN5q5iD0wUfgit8X8IV/nj3tL6tLKFlwQ2E2tkrHdYht+Dob6coNknVHPoPeyF/D4kCcqjkEWo6CdogQBhIT2C2WUBSCuKKj2jDeto8EhMz8bCoiAO0Ix3eUtITYJfCRJdhiAX6J1Hqdz+plmLntckR6vzSfuwo8aWbbcKr2RaQmNyGV6EAuW8+QlX0IEvtoGEiNYM/gfnpLjtAKj5qL3far1J8yzQmQr+DD0OwA1MEnvLja45mVEFKmWFSeN74A5BkDfVc5C6FNN3tm7yVchj8Xt5+L54/G8OC7h6DkM7QG9iH0LPsRaTb4aSSnkrCywzh19XqMpurxdJ4urCGFHZHgd2cFLgBtiOP6SgIOe7deC+tZOF3MNIsqCsPDmIJwDTay22+j24ksA+wzRUibc9uHTsMLY4N4M5ln51Bl5408ZDgqKh0RuqWxfovh6lrY7GWroRAemhqriADK9HGGwRO+os/RKQAI+L7v56XnekwWVS4Aan/+9GvhNPWccHFLebFON/CNbWfQAQqDjM689oBtgiqNMM8MVW2Oh+i8Vq0cnklPY9SWiCJYClQASuoA1Om9HGijv5QxATnYDijTDD3Lic7Vja9B/rQ/KX+67Nef7OzE+as4Xyzd7XnEKMgqmjGHLyY4xfnbdGJeqqU+CFQA6tiL1CCL/p9sySFWwIYWWerZLO1nL3fzZXRB8aXyv+T3/+ykkzkPTWbL+SvmWhBA8YZjKM+udAFo47sLwPvgk3clSe0SQfjEirpKHeyNF/hPqnr+SHsbTjc48c/+wWyilYoFiBWT3Hwer+ZSMD3tmp1yKXfBWYCdY6TzVsH/C0fiXBNsfNNzGl+astN9Jl1Q21L4Duxdg0PQn1q9BkpOfGVZtqIoMjFRFADYSz6aN3HMklmJ4Cg4ATBsU3i40rGhq1T+g4PF95HRPt5LxcoqZ6/9aHA1CCCn89auhjaZpvLMdkWeAGQYRYiCyDFwOMQGOUiaa3cnnLeaG6AFsBKc5lX3kOkJZqWzQgd5cOwFG3lIaRwNddq2nHA5lXjxpNYmdEQi6Od8p9vk98TJLy3AZQOscNWFR5w7CFoAgVmAsm8IygMFzeeAPCOLIlSiVUMUyF4ejOLcWDPcus7ijyvjVMcoqKeRnSyZFZI5ULFWHgK+uB7GpSVGB+iGgqTgBHB4sqD1AryfK+vjLV8QBZrgzRsM7ZwWuKHqRz9zQVzTUE9NZ5uVpJbIfKhgTvfjuSE/Md3QGBvjIMmHasl5qvs58TGjKIX85F7Wj4ggpCR+R8Q9xt4vZ6RWGrVyOYun6QRdmaafJ9vi911ZeuHXi/VIewNcwXEfmAD0xx/1GJ7HmghAKiMkpYUY8q1AinJGzCMBm8taFHFFpFJnrHDneSnvMqA/gQnA6emd0RSfOamMTAWWRxcc6l2JxG/KzLAlfMuKL4am5S6I9kBjKEs388YJXwUmAHvnZQUTnsuKzIj5E+O8VHKMlFYgJWRdTDm2ck1X5OYsGnDBhJvUeqwzWgLlPrAw1G18j/WVUhFxQ/X0+yKASW60kFVXXOW8kmggmSI7ZRKgFSjSEZtIo4U991V6A5c8tqCDEzlBUmAW4LasmsX/LCallyl1k05a/yiUUdntsnIoz3Dz8HSiEHaWsSXecpXWhg1GF+qUqFeFpoDjh+AE0NnNRnaB7MSf+pEQL5URToAcPFhWzepfiva/O8Xuu8T9QsIz18qo3G3Tvr63sGZI4OfvHTJtFiAtgNiJ5e62d3DJ4QJuSCojHRxpjKX95US8+jK7yyuIXhkaYXhJ/vyG2NRgJ7iWlAN1dW2NJctwOQ7UxbGsICmw3AR8d916ao8gPoekIZZIiGs/ZTm69pvnKZTgJzfmlLro26feOVzi281x2WOGmz/4r+OkdaxOASIZjqinFXTJyt4AKbjcCLy9ZVvBfOcyKJYtvUv5oCM7OuqBA9B+97u5qapyP0j389/vHvXCTQEeOYMa7yLW3ICm1e2csBezZbU4DrSOew7q2VsOkoITALlyzjr7vRtiWR0ngUaRf/2Rfw6yHiec18Nv9WFKxqhk3oKuR0jGgNo3roUuC3+LpHDYemsksKDRz7Y0alN6sJQL5/TtcFsXGOfnFGR+OONtyPAEwApqzz4L9bXXllLckt8dyWTw4Mv74UwTeFmgSuWQBVmheASrerl21NvZx2Jo3QpD5w9HZwSy5MKLGQRqAS5XENjnfIzRUFHNybhUKDE0iZG+AaRGxmYiJXZ8jL+/tzDaGFRtjjOfu557DYMT5FXaqCIJ6K3ruhCpj3u8y2OVE/g9lFEPd9oETYEKQJizP3VJgUcKweIWl/GDI0gMc3yafjUxMgIzyYZABETBaHv2QH/on4Ku06Lye7rvKH7yymGO9cwOBlTupGnftLYEvmSmMPQ8Lx7iColFZX1ciYIXwBlnwT5lK8yRaYwdHEaOQvD5FmuYOtbP9aIc0pVoScz9Rz9a9rD0jaEJ3PzEbpjS0/W6VwXMHGp/Y2cr6ts4Z1HsE0j0E6f7+RgFUAkKXABcYob0xVciOcANEtR6H3xhXq4tup7pgcGZutAPh2+/HerbfTPPKni1b2QKNzzyHIZlA9qsgUHhlR0tCT3LOpRqOIxPEvwWWadYAQpeAGRSvfjTyG/fzs0N80mqkZ6aQnqMwxFSUVqBMjyM8C23cOMGd8lXkF45NorPPfwrHJ5iOJanALj+s7CAlW2Vyk8atK5HYzc7lEXBSOQTM7PYWb9QTYJhtGI7ZPJPPgnziivY/3LKjHyGadGy1vXrYci3ICgEcUluczPMr3wF9h+eP5MwoKtfvnkQN/9yN8bZEZTJaVdnt0pn59HgNiQOtLkaG91IFK1bdZzSPMk9xpwTiMRxVdjB5Y2crKkQVUwAAmrm+uuhPfwwuBdvHgkMIU6Et/Su5+JYsQQ+kHaBghm5/OsIf54bKlqWXnGTQyDfe2EA97wyxH0BBJpDygI2F3yyPN8BsHBRAt46mxpQX5fHltYkPhQ2cXt7HSLCV4XI5yD47Ml0+JvfhLl6dWl+vrwQqZKZzSIxOMSrYgU53Ze1mzD281EcuWEXJh96A/Zktvy1476WzRaJbCsXA+yAzY0fbojj+QK+SFymF71Duuq8l146G+aEZeC1qWZsorAqCb5URrvtttv+Vi4qQUpDA5QNG+A8yunKBVyRLwSd20wNb6N1HsdwNibdDk6O52D+dgCT/84NEwNJxuIcxGNjqB5nb1SM6/xeHfZUFv/LT2XbGvdV8tmCOk0BcEc3lLjGbxEBv0kZiLFhPquOzytElXNBZQxnv/MdKHfeCTZ7C7YHKhu7Vb3rkI904U33Mla5qKFMr9FNGNzrZbHBVFujMNY2wOiqg94c5WZvRidXnQaV58XQXc+YuGe3ikzERo770TwplGMrAuhkm9RF1ydGIf6Bfa8vtVm4dbVV6l8upqzFplkWAYj2Z266Cdr99y/YHog2RmIakr3XYVD5MPfwze4c+ZWhDXgC0ehWNF6nuBB4479cBaOTKy0WSfc+b+GuX3P/MBeNZWLsLHJvmswTeSQCaOFMXQ8/eyACEBLmKN9rG/P41jqTOy69p4H9CTi79+CLDWv0u99F/uqradKFOkm9pHB6Xa5kXIM3059AX387u9Kmtz5noZxkblYsIcudfxmH6cJUT8noOOgvzjVwy8f5ESfu1qmfchBPMtoh7oWPhjAzGbX1wZd8RTg03X/kBpMvHAwjId9LCJCCH957L+a4yyR2773IRqMI//hBLp5rxDg28jiZ3/npYJ350SXOv2bT/eyNtiBaz6+fSO0FgIDpxrMNftLMwrf/R+GXW2wY/KhHJq7yyylUCZm3EEsojmd5xcsfhnK7+IWQsb4Q7uk1sZa7/IOg5ROAcEshRO6+G6Orz8G+b73O5TdR0WkqMXeheNtq6BJyJsYODyHeUo+G9mZoHACTHnXQdO2ZBj9RYOHO/6IVEfT4tE326JYa2GDLfmb2fEUG0ojLLKTMw8j3QI6wvRqmFXC0KBCWllcAwjL9d+tXr8HJW/binVuegH00QQufsXmZiRJKjk8jl8qgsYPW0MBOUgWs4aozxBLy+MbT9DKMekLynaFRbtjjrvlwCz8AQj5ECGS50EUhWn/TZOLM2Ay/HrNL+CNueNlJKtR+yRZsf/pGtFy5ldrPSMer7gwrIgiLa3LG3h3GBIcQHMbnsnYzaLqMPd9vX6ggxtkul6vjFFqbdiRLwZS8kNdHk4b4uriFneykBUlVEYBfgci6Jmx54Aqc/vNrEP+IdNj4MSUePswla5iYxvCBAaQnU9REUUc/h2DOF28xcNenVTRHWSLXK1kDOXYA2UkroqMyMr2AlnJj08LR2VK4qKoAfMZXXbQZ25+6Aac8eDkazuvh0ITGr3uKKLi5m4lEEHl+VGj8yDDGj45wW2nw1vCJDRp++BkNqxvo7Lhx2zyULbgeRqXb6CK/tsryNm77PAd1Xp5+wHFwKw3u9ItHMPrYXow+vR/pvlEYssGbJMN68n0gjZMmTYyUIp0N6H3ochgdi+8H/D5WDow7uOkxC325MOouiGENo50ftGfRbQTT6M4tf8UJoJxBJ5tH6q1hJPYcQ+LVfqQoDHMwAWuK88v8LdbZiG1PXo9QgAKQ8oeTLoVg4tAaflfooy5OZ3RUKVrRAlio0jY/I+Pw84lydrljMdzVwHEiiVeCpfG0i74xF2evrayX/n8ngGBhrn5ulRVv9eu34jmoCaDKIqoJoCaAKiNQ5eLlf9KTJbM1qgICgr24oP4qlF0rsoBAv/xPeu/U0KgOAoK9WMDj1Sm+VqpgX/sPnaukB/T/sjB1vRqLxY7SFB6oEh8f2GIFc8HeG2bk/yXWTYns48PghhU/sND+/ooT6wSx3hyPx7lSiSQXfHANf6jMmOvv5+kDk0IwJn1WMJdKl3rCfLCL9zfVhFA5XShie1MRa6+geZN7dEeXMOHPKKWaOwpQFsRU3I5ovih6iUoW4D+RBEx4Cu/v40vSUtdoCQgUMbyPmIrPnwW+ZDvPAsrLSqfTa/jipXy2kxlt5Lmb9zXLKAdpzjVxkqEdaVOlg/s47x+TaGdOstLt/wFBXnCCUaUiKAAAAABJRU5ErkJggg==</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://api.qwant.com/api/suggest/">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="client" value="ff_android"/>
+</Url>
+<Url type="text/html" method="GET" template="https://www.qwant.com/">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="client" value="ff_android"/>
+</Url>
+<SearchForm>https://www.qwant.com/</SearchForm>
+</SearchPlugin> \ No newline at end of file
diff --git a/mobile/locales/en-US/searchplugins/twitter.xml b/mobile/locales/en-US/searchplugins/twitter.xml
new file mode 100644
index 0000000000..1334c4726f
--- /dev/null
+++ b/mobile/locales/en-US/searchplugins/twitter.xml
@@ -0,0 +1,13 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+ <ShortName>Twitter</ShortName>
+ <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAAXNSR0IArs4c6QAACW5JREFUeAHtXGmMFEUUft07x87sAS6ngILKYkTDoaLiFeSIKP4wGggmxoSsaOIRE3+q0R8aNeKtMUZB/WEMh5JIFGMEMYiCyC2Xigi4wCKw7DJ7zOwx7ft6aZ0duuesqt7Beklv73R1V736vjpevX7VBp0Ry7KMucub65JW8n4yaCxZVOWk6bMABAyKMaa7TcNcuPiufosMw7CQq4E/s79sGUrtnR8TWdPwW4tsBIzVFAneu2xWZYOJlq/Blw14ev7c0LnBA3tjzvKm+VYy+V76Lfq3fAR4OJpvMvh18ovSJbghgPnWtCdct1R9TT4CbOyY2tqRj7NnCWxpmp6JOkEJApoAJTB7F6IJ8MZGSYomQAnM3oVoAryxUZKiCVACs3chmgBvbJSkaAKUwOxdiCbAGxslKZoAJTB7F6IJ8MZGSYomQAnM3oVoAryxUZKiCVACs3chmgBvbJSkaAKUwOxdSMA76dxICXDcx/DqMjqv3KDqsEmxjiQ1tlt0NNZN/K/vcs4SMGFIgKaMDNOEoUGKBs/GOdFFtLWhk9b91UEbj3SefYPLlZqIQV1M2umEHdLjckf+l6QRgLHNjwZ2Ibf2eRMidPmgzFULc/J1I4L28XtjN324rY32nep2RXBw1KQ7Ly2ncUzqY1+fdr2n0IuZtSww1xru7g9eVUELfmyhLnGNJas2k4cH6eFJFRQqy3prrxtqa8rouVuqaOHWNlr1Z4edVhUyaMKQIF09LEjX8FHGLeqTnXHqFlwfKQRMvzhME4cG6CEG482Nrb0qK+vHtFEhJj1acPYmzxUPXBmlKwYFaVCFSZecV0a45sih5iR98Vvc+SnsLJwAKD39orCt4I0XBKmlI0IfbGsXprBbRpcNDFDdxMLBT83zetY5XeI8X7z9c6uU3izcDB3Crac/D0GOzLwkTI9OilLZf5ecJCHnINfgUe5pAeE16VGvg6eFF39ooQPNPfND/zCGJnHtVlxOZ+AEAely04Uh2wR8ZX0Lxd3nufRHcv592+gwDYzKYbehJUlvccuHxTSVh7jJI0L20PTUd7Gc9ct2o3ACBle4z4DjudW8MLWaXuc54eCZ1pRNuWzpgP2O2vJstxWc3s1m3DM3V/07qVs8Ab+0vpWOxMTZd2c314LV7Xkw01AzvNqk59nauJUnaREC6yV1uBORZ2oe0DfVolqyO06bj+a2ZkjNJ9P/wgk43pa5dQS5g9RNjNDTN1XSiCID82AmqhC0/I9/aafle8VbQcIJ+Ls1MwEOYFcMDtCC6dU0b3yEKoKFjeEDXeYbJ39RZ0zCr//USit+S4jKslc+wueAIy3dbHpaVMkLmWyCxQ0m0VtGhWn1nwlauS9B2XpQap415cLbT2r29uT73LoY/XpSsOWQUorwGsBXsvZgz2oypZyM/5ZzM5hVG6a3ZlbT49dW0CReecK8zCYoS6Y0J5JSwYfuwnsAMl3Frfl2BjRfwSLO8c9g8YMJDw6zfY1ddIRNwnQ50X72tfR7ivltGtl7cTH541kpBNSzmbZqfwdNvzhUsH7oFTfwqhQHBMPaH+wsO8ZENMbhUk5SFL5miRJX4MiSQgAw+WhHG41lj+SwIi0dB1/MKVhL0BDnivyzCgJyGGnzq6hj0cB6eHVDKzXFBbsP81OnqLvbOuXrLpwA+M2x2JoyMmQPFU+sidFfp+WO1UWhnOHhXE3qDFlkTRI+BMFyGM0r1NE1Pd7JGL89ahL4BilrjQTe0NAqz/x01BROwGF+15oqVew9xFGKItLn41V/4UPQ7uNd9ntTrwJL6TrMX9kinIAEdwDRDivZILjlD1e0iqFTOAGozOe/indauYEk89reE/JbP/SXQgCiC9YdEuu2lQm2W96bBLud3crANSkEIOOFHOZxok2+HY2yRAvcINvYBaJCpBGARcyz38foFEehlZpsYfBVRc1JIwCgH+WJ7Jm1MTrQ1Ns07euErNovx/fvVm+pBKBAWBNYDeNtErp2X5d6XrXvZFNalQhfiLkpDr/94l1x+uL3BCGA6qrzQzRmQO/AJ7fn/Li2cp9aC046AdW8Cn7k6gp2yiXtSDNETSB0pS+ujeH7+e5Afi+Tim0k0glAJDGCpqZwy+/rspSjHhS8AugFg/Q5AKWhYn1dYCh8f0ht6wcmSgjYe7LLfkPWV0lAANa7m9v442HqRQkBqBYWZhvq1Sxu8oVxBUc97/fJVFZGQJKb1xsclrgpx90o+YJY6P0Yepbt8W+IVEYAAMLmhlc5yOkrjv8BIX4LXhYt4FhP2eEtmeqplAAogsp+uL2dnuTF2cbDnb4RgQbwGjeGfALBMgFZaJox+9NGX9sigmvHDQ4SAm0HREzeWdOzHajQCuXyHMB/Z1MbrfXB6knXT3kPSFcAURMA4kteJUc5RhThijIFgbbvbekb4KOe0hdi2cDEihjxofdcHiHsXJQpMDff54143ype7Waqk+QqexeNuHvs472dwRcVvOVdGlErR9bBAPjlb3WOtkz6OGlKCUBrH9mvjK7j7aQzeJOGqmgJeDhf3tAidGeLA2CxZ2EE9GOnW4LtTLicsUsGTjh8GgDXh1aW8fbPAGFPQC5h68VWynkeky08sEt2tVMnDz99UYQRgAl0Pm8Vxcbm1P21flUaC6xFvPqWGdsvom7CzdAxbE7eNy5q+/tFKJhvHngBBOcfvgFRCiKcAKfSiGTGzpdJ5/NmC/eNk86tQs57OIwEIfE/1ncI/5yAEAU9MhE2BKXnv/1YF+FAtPSNF4Ts9wHY/i9SEPS7lcNH1hxM0GGBW0dF6pgtL2k9wK1gfACjtiZAGKbGDAhwAG+AsBEjF4EZCZDrOfYUQVM7jnVy9LWvi/hc1M56T47Vz5pPTjfEGESEfOCAwCytYFJgGYGcqpBp/4/Vaju/mrIPDm/BbhgVYYK2Uor/KCUgvW5ov9h6hKPBTiyt8JX0+hTyW7LnpRCV/l/PaAJ85lsToAnwGQGfi9c9QBPgMwI+F697gCbAZwR8Ll73AE2Azwj4XLzuAZoAnxHwuXjdAzQBPiPgc/G6B2gCfEbA5+KxW07ch5B9rkzJFc/Ym7wvZ3fJKX6uKMzYm6ZhLjxX6lNq9QD2hmVZxpzPmr4hsqaVWgVKW19j9dK7+8/gb5MaFkWC93KMwurSrlApac9YM+bAHpEhtqAnzF3eXJe0kvfzxDyW54YqJ02fBSAAYwdjPg87i+/qt8hu+JztP8DF2aE2lt4MAAAAAElFTkSuQmCC</Image>
+ <Url type="text/html" method="GET" template="https://mobile.twitter.com/search/">
+ <Param name="q" value="{searchTerms}"/>
+ </Url>
+ <InputEncoding>UTF-8</InputEncoding>
+ <SearchForm>https://mobile.twitter.com/search</SearchForm>
+</SearchPlugin>
diff --git a/mobile/locales/en-US/searchplugins/wikipedia.xml b/mobile/locales/en-US/searchplugins/wikipedia.xml
new file mode 100644
index 0000000000..2043036e6b
--- /dev/null
+++ b/mobile/locales/en-US/searchplugins/wikipedia.xml
@@ -0,0 +1,23 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Wikipedia</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://en.wikipedia.org/w/api.php">
+ <Param name="action" value="opensearch"/>
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<Url type="text/html" method="GET" template="https://en.wikipedia.org/wiki/Special:Search">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<!-- Search activity -->
+<Url type="text/html" method="GET" rel="mobile" template="https://en.m.wikipedia.org/wiki/Special:Search">
+ <Param name="search" value="{searchTerms}"/>
+ <Param name="sourceid" value="Mozilla-search"/>
+</Url>
+<SearchForm>https://en.wikipedia.org/wiki/Special:Search</SearchForm>
+</SearchPlugin>
diff --git a/mobile/locales/en-US/searchplugins/yahoo.xml b/mobile/locales/en-US/searchplugins/yahoo.xml
new file mode 100644
index 0000000000..21e23fdda3
--- /dev/null
+++ b/mobile/locales/en-US/searchplugins/yahoo.xml
@@ -0,0 +1,45 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Yahoo</ShortName>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAAXNSR0IArs4c6QAACJ5JREFUeAHtXWlsVFUUPm9mOt2gLWKXIFtLKRZIGoIS/CEJa7ASEExdECPYVhYjiAlq0CAgIJBIABUFWkqKEaGiEKRAoAIKBEGBmNJCW2ihQGnL0oW2dJmO9wyly3DPw/bN2+SehPS9c+bee873vbvf95CgSZzglOIgPZ7dJjgB+rO/nR/YxF+PIFApAWSxnJLSIDZZAonBDMB0AHFwKMwJNd8zEkbivRB1EWDgZ0jgOyUNht+w4JMvwFcXcPfc8UFvwlySXob0RADnBvcfiXv1EWDNT6JFAie2+0L0QSCBNUGuDlef4h/zUhF7C8NAjHb0exA6IwFCdERAEKAj+Fi0IEAQoDMCOhcvaoAgQGcEdC5e1ABBgM4I6Fy8qAGCAJ0R0Ll4UQN0JsCmdvmDYoMh8tlAuH6hCipK61xbQE9084GQcF8IjfCDAxsK4cKxO2q7wc1/8LgQwH/Fl6qhtKAaHA1OsNkt4O1vheCevvBPxk3I/l1d31Qn4OKpcpidGgOdu9q5IPSOCYAPBx+FRgfXrJrS5m2B+K/6Q0hvP24ZJYyQX1Zc5No8qVS9CcKnPnXeedJnJGBUYk/SrpZh7Lu9SPCxzOTZWVBX3ahW8c35qk4AlnQo5SpkHr7VXKj7xWufR4F/F9UrY3OxASF2iFsQ2XzvfnFyVzH8vbvEXa3KvSYEoOfrp2dCfS2/nQl40g6vLIxSJUBepm980Q/8A714JqitdsCm2ee4NjWUmhFQlFMFO5bRberYWT2he/9OasTYJs/IIYEwYlr3NrrWN2mLc+HmlXutVapea0YARrFz+UW4dv4uNyCrzQLTVkdzbZ5SSizahK8HgCS5TuM8lG1hViXsXpX/kF5NhaYENNQ5Yf2MTDKemNHB8Mz4ENKu1DDi7R5sSBxEZrNhZiY46l3npcjfeNqgKQHofNaR23BkyzUyjqmrogGHiJ4W7OSx7afkcOpV1cf8vLI9HymvFDdd6rxsqCqvd9Pevw3r4w8T5oVzbUqUk5f1A+zseXL3Tr3sUJmXxlM6XQgoL66DrZ/kkDFMmh8Jwb19SXt7Ddjxjn6Hnmv8MP8CVJSwWboOogsBGOf+by/DpdPl3JC9fa2sQ8bzwcoFO97EdQPBwo6g8ST3ZBlbDrnCM2mi040AJ5tkbpyVCU4nv9MbMiEUcB1JqYyZ0RP6DA7kZuNwOAE7XvRFL9GNAAw4989yOJhUSMYev7Y/ePl03MWAYDtMXkp3vPvXXYb80xVk+VoYOh6dh7zDvqC6gu6QX/ooosMlTVnBZrxB/Blvxc06+HEB3Q91uNB2JtSdAFys+2lJHun2xI/7QEhE+zvkqOeCYPhUesa79dMLUF3WQJarlUF3AjDQ9DUFUJRXxY3Z7mOF+LUDuDZKabGyGe839Iw3/2wFHNxIN31UvmroDUEAzpA3f5BNxjf4xRAYMjGUtLsbxszsBRGD+B0v/nbTnHO6dryt/TUEAegQLv+e2Vfa2rc219gh+3Rij/YjJCjMDq8voVdWj20v0mXGS7ltGALQwZT3s6Chnj8m7NrdF15d1JeKo1n/1pfR9FJzjQO2sFm4kcRQBOC+cfraAhKf2Dnh0CuGfp1h4Iiu8Pzkp8j0u1Ze0nSpmXSklcFQBKBfaYvzoKy4tpWLLZdWqwTTvxvY9G5nix6vrF6Sa6m5rbbl7mZhDexcSe9HtPxS2yvDEVBT0QC4NkNJ1NAu3OHluLnh0D2a3tDBfWkt9ngpvym94QhAR39je8h5p8oon13Lyn6BLXvIXXv4yO7xZv1xG45vKyLz09NgSAKALQ/hqQRKgkK94c2VTzebceHOx7+FkGYDu2hsdELKHDqv1r/V49qYBDAkck+UwYmfb5CYjErsAdHDurgW7IZOCiN/l5FcCPln9F3vIZ1jBvai9h7+cqRcKo1suASxJmsYeHnzx//Xc+6yztcCoeH8w1W46fNe3yP3T+Rp5HN7izFsDcBASi7VwK+rC8iYukV1IsHHRGmLcg0NPvpoaALQwR1L6WEp2inB2rH368uU2TB6wxNwr9IhOyylkMS1Ja1POFC+yOkNTwA6f2jzVXL7khfc2f2lcHoPva7ES6OXzhQE4Jbhpv84lHQ0NELKXOMOO92JNgUB6PT5o3fgeNqjJ1P71l2Ba9n8vQX34I1wbxoCEKxtn+W4JlYUcJW36mD7Qv23GSn/eHpTEYBP9il2dJwSPPxbdUf/bUbKP57eVARgAJmH6PcMSvKreTEaWmc6Aupr+Rs2iLKFLVebTUxHAHXCDYG32gQBqj+AeNSQEkEAhYwH9RJxxhOLsIga4EGkiaxsbOuREnzLxmxiOo99OvM3XhB4PJBlNjEdAXLtvKgBGjx+ckNN0QfoTIBc7dDAtQ4V8b9qguRqR4fQ0SCR6QiQA5l620YDHDtchOkIwBNwlIhOmELGg3rq/A8WYbPT5HjQBY9mZboagB9TosTGjqiYTUznsUXGY7nmyajEyIRjTJeJt1pdzuIhLbOJ6TyWW4wTfYAGj5/cMFT0ARoQILsaKjNE1cC1DhVhuibIzr4jQYnoAyhkPKi3+9EE4Dc/zSam89hL5mNOcs2TUYkxHQFyK55mnAfQ20tGfWSa/MJPzdRWNbhe0MBagaclzDgKMh0By8f/BfeqHFDJvnbS/J0ftgQUFOYN3jL9g1GfJ9MRUJJf8zCW7CWrsiL+u8UP/9hYGtP1AcaCT7k3ggDlGCrKQRCgCD7liQUByjFUlIMgQBF8yhMLApRjqCgHQYAi+JQnFgQox1BRDoIARfApTywIUI6hohyQgEpFOYjEShCoxPdNzPNauZJQDZgWsccakGRA3x4Xl5IkJzilONh7gP0d+bhEbYQ4JZAy0uCF0awJkpwS+E5BhREcexx8QKybMGfYN8n9mpAez24T2PI6/vcV9BdSHyQSf9uDQGVTf5uUBrHJ+OBj4n8BJ5EPp7ErQgIAAAAASUVORK5CYII=</Image>
+<Url type="application/x-suggestions+json" method="GET"
+ template="https://search.yahoo.com/sugg/ff">
+ <Param name="output" value="fxjson" />
+ <Param name="appid" value="ffm" />
+ <Param name="command" value="{searchTerms}" />
+ <Param name="nresults" value="4" />
+</Url>
+<!-- phone search -->
+<Url type="text/html" method="GET" template="https://search.yahoo.com/yhs/search" rel="searchform">
+ <Param name="ei" value="UTF-8" />
+ <Param name="hspart" value="mozilla" />
+ <Param name="hsimp" value="yhsm-002" />
+ <Param name="p" value="{searchTerms}" />
+</Url>
+<!-- tablet search -->
+<Url type="application/x-moz-tabletsearch" method="GET" template="https://search.yahoo.com/yhs/search">
+ <Param name="ei" value="UTF-8" />
+ <Param name="hspart" value="mozilla" />
+ <Param name="hsimp" value="yhst-002" />
+ <Param name="p" value="{searchTerms}" />
+</Url>
+<!-- phone Search activity -->
+<Url type="text/html" method="GET" rel="mobile" template="https://search.yahoo.com/yhs/search">
+ <Param name="ei" value="UTF-8" />
+ <Param name="hspart" value="mozilla" />
+ <Param name="hsimp" value="yhsm-001" />
+ <Param name="p" value="{searchTerms}" />
+</Url>
+<!-- Tablet Search activity -->
+<Url type="application/x-moz-tabletsearch" method="GET"
+ rel="mobile" template="https://search.yahoo.com/yhs/search">
+ <Param name="ei" value="UTF-8" />
+ <Param name="hspart" value="mozilla" />
+ <Param name="hsimp" value="yhst-002" />
+ <Param name="p" value="{searchTerms}" />
+</Url>
+</SearchPlugin>
diff --git a/mobile/locales/filter.py b/mobile/locales/filter.py
new file mode 100644
index 0000000000..c14c2d48e6
--- /dev/null
+++ b/mobile/locales/filter.py
@@ -0,0 +1,45 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""This routine controls which localizable files and entries are
+reported and l10n-merged.
+This needs to stay in sync with the copy in mobile/android/locales.
+"""
+
+def test(mod, path, entity = None):
+ import re
+ # ignore anything but mobile, which is our local repo checkout name
+ if mod not in ("netwerk", "dom", "toolkit", "security/manager",
+ "devtools/shared",
+ "services/sync", "mobile",
+ "mobile/android/base", "mobile/android"):
+ return "ignore"
+
+ if mod not in ("mobile", "mobile/android"):
+ # we only have exceptions for mobile*
+ return "error"
+ if mod == "mobile/android":
+ if not entity:
+ if (re.match(r"mobile-l10n.js", path) or
+ re.match(r"defines.inc", path)):
+ return "ignore"
+ if path == "defines.inc":
+ if entity == "MOZ_LANGPACK_CONTRIBUTORS":
+ return "ignore"
+ return "error"
+
+ # we're in mod == "mobile"
+ if re.match(r"searchplugins\/.+\.xml", path):
+ return "ignore"
+ if path == "chrome/region.properties":
+ # only region.properties exceptions remain
+ if (re.match(r"browser\.search\.order\.[1-9]", entity) or
+ re.match(r"browser\.search\.[a-zA-Z]+\.US", entity) or
+ re.match(r"browser\.contentHandlers\.types\.[0-5]", entity) or
+ re.match(r"gecko\.handlerService\.schemes\.", entity) or
+ re.match(r"gecko\.handlerService\.defaultHandlersVersion", entity) or
+ re.match(r"browser\.suggestedsites\.", entity)):
+ return "ignore"
+
+ return "error"
diff --git a/mobile/locales/jar.mn b/mobile/locales/jar.mn
new file mode 100644
index 0000000000..0e65e4b2c2
--- /dev/null
+++ b/mobile/locales/jar.mn
@@ -0,0 +1,18 @@
+#filter substitution
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+@AB_CD@.jar:
+% locale browser @AB_CD@ %locale/@AB_CD@/browser/
+ locale/@AB_CD@/browser/region.properties (%chrome/region.properties)
+ locale/@AB_CD@/browser/searchplugins/list.txt (%searchplugins/list.txt)
+
+# Fennec-specific overrides of generic strings
+ locale/@AB_CD@/browser/netError.dtd (%overrides/netError.dtd)
+% override chrome://global/locale/netError.dtd chrome://browser/locale/netError.dtd
+ locale/@AB_CD@/browser/appstrings.properties (%overrides/appstrings.properties)
+% override chrome://global/locale/appstrings.properties chrome://browser/locale/appstrings.properties
+ locale/@AB_CD@/browser/passwordmgr.properties (%overrides/passwordmgr.properties)
+% override chrome://passwordmgr/locale/passwordmgr.properties chrome://browser/locale/passwordmgr.properties
diff --git a/mobile/locales/l10n.ini b/mobile/locales/l10n.ini
new file mode 100644
index 0000000000..43ded369a8
--- /dev/null
+++ b/mobile/locales/l10n.ini
@@ -0,0 +1,19 @@
+; This Source Code Form is subject to the terms of the Mozilla Public
+; License, v. 2.0. If a copy of the MPL was not distributed with this
+; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Control which directories and modules are part of mobile
+# on the l10n dashboard.
+# Changes here should be triggered by changes in
+# mobile/android/locales/l10n.ini.
+
+[general]
+depth = ../..
+all = mobile/android/locales/all-locales
+
+[compare]
+dirs = mobile mobile/android mobile/android/base
+
+[includes]
+toolkit = toolkit/locales/l10n.ini
+services_sync = services/sync/locales/l10n.ini
diff --git a/mobile/locales/moz.build b/mobile/locales/moz.build
new file mode 100644
index 0000000000..eb4454d28f
--- /dev/null
+++ b/mobile/locales/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file